
    )j8                    v   d Z ddlmZ ddlZddlZddlZddlZddlZddlm	Z	 ddl
mZ ddlmZ ddlmZmZmZmZmZmZ 	 ddlZn# e$ r dZY nw xY w ej        e          Z G d d	e          Zd
ZdZdZdZdZ dZ!dZ" ej#        d          Z$ddZ%ddZ&ddZ'ddZ(ddZ)dd Z*dd!Z+ddd"dd'Z,ddddd(dd,Z-dd-Z.e G d. d/                      Z/ ed01           G d2 d3                      Z0dd4Z1dd5Z2dd7Z3dd9Z4dd<Z5dd>Z6eed?ddBZ7eddddCddJZ8ddKddPZ9ddRZ:ddSZ;ddUZ<ddVZ=ddXZ>dd[Z?ed0dd\ddaZ@ddbZAdddZBddeZCddgZDddhZEe didjddlZFddmZGddnZHddpZIddqZJddrZKddddsdtddyZLddddzdd|ZMdd~ZNddZOddZPddZQddZRddZSddddZTd0dddZUeVfddZWddZXdS )u  
Photon Dashboard API client + device-code login flow.

This module is pure Python — it intentionally does not depend on
``spectrum-ts``.  Every management-plane operation (login, find/create
project, enable Spectrum, rotate the project secret, register a user,
list the assigned iMessage line) talks to Photon's **Dashboard API** on a
single host, exactly like the official Photon CLI (``photon-hq/cli``):

    Dashboard API   https://app.photon.codes/api/...
                    OAuth 2.0 device flow, Bearer access token

A Photon project carries two distinct identifiers:

    * ``id``                — the Dashboard project id (used in API paths)
    * ``spectrumProjectId`` — the Spectrum Cloud project id, populated when
                              Spectrum is enabled on the project

The ``spectrum-ts`` SDK (run by the Node sidecar) authenticates to Spectrum
Cloud with ``(spectrumProjectId, projectSecret)`` — so the value we persist
as ``PHOTON_PROJECT_ID`` for the runtime is the **spectrumProjectId**, not
the Dashboard ``id``.  The Dashboard ``id`` is kept only for management
calls.

Credential storage mirrors every other Hermes channel:

    * runtime SDK creds  -> ``~/.hermes/.env``  (``PHOTON_PROJECT_ID`` =
      spectrumProjectId, ``PHOTON_PROJECT_SECRET``) via ``save_env_value``
    * management metadata -> ``~/.hermes/auth.json`` under
      ``credential_pool.photon`` (device token),
      ``credential_pool.photon_project`` (dashboard id, spectrum id, name), and
      ``credential_pool.photon_user`` (operator number + assigned text line)

Reference: https://github.com/photon-hq/cli and
https://photon.codes/docs/api-reference/device-login/request-device-+-user-code
    )annotationsN)	b64encode)	dataclass)Path)AnyCallableDictListOptionalTuplec                      e Zd ZdZdS )PhotonDashboardAuthErrorzERaised when Photon rejects a device-flow token for the dashboard API.N)__name__
__module____qualname____doc__     B/home/ubuntu/.hermes/hermes-agent/plugins/platforms/photon/auth.pyr   r   9   s        OOOOr   r   z
photon-clizopenid profile emailzhttps://app.photon.codeszhttps://spectrum.photon.codeszHermes Agent   i  z^\+[1-9]\d{6,14}$returnr   c                     	 ddl m}  t           |                       dz  S # t          $ r2 t          t          j                            d                    dz  cY S w xY w)zDResolve ``~/.hermes/auth.json`` honouring the active Hermes profile.r   get_hermes_homez	auth.jsonz	~/.hermes)hermes_constantsr   r   	Exceptionospath
expanduserr   s    r   _auth_json_pathr    X   s}    C444444OO%%&&44 C C CBG&&{3344{BBBBCs   " 9AADict[str, Any]c                 \   t                      } |                                 si S 	 |                     dd          5 }t          j        |          pi cd d d            S # 1 swxY w Y   d S # t
          t          j        f$ r(}t                              d| |           i cY d }~S d }~ww xY w)Nrutf-8encodingzphoton: could not read %s: %s)	r    existsopenjsonloadOSErrorJSONDecodeErrorloggerwarning)r   fhes      r   
_load_authr1   a   s    D;;== 	YYsWY-- 	'9R==&B	' 	' 	' 	' 	' 	' 	' 	' 	' 	' 	' 	' 	' 	' 	' 	' 	' 	'T)*   6a@@@						s@   A- A A-  A$$A- 'A$(A- -B+B& B+&B+dataNonec                   t                      }|j                            dd           |                    d          }|                    dd          5 }t          j        | |dd           d d d            n# 1 swxY w Y   	 t          j        |d	           n# t          $ r Y nw xY w|
                    |           d S )
NT)parentsexist_okz	.json.tmpwr$   r%      )indent	sort_keysi  )r    parentmkdirwith_suffixr(   r)   dumpr   chmodr+   replace)r2   r   tmpr/   s       r   
_save_authrB   m   s   DKdT222


;
'
'C	#	(	( 6B	$155556 6 6 6 6 6 6 6 6 6 6 6 6 6 6
e   KKs$   A<<B B B 
B+*B+Optional[str]c                    t                      } |                     di                               d          pg }t          |t                    rI|rG|d                             d          p|d                             d          }|rt	          |          S |                     di                               di           }|                    d          rt	          |d                   S dS )zFReturn the device-flow bearer token stored by ``login()`` or ``None``.credential_poolphotonr   access_tokentoken	providersN)r1   get
isinstanceliststr)authpoolrH   legacys       r   load_photon_tokenrQ   z   s    <<D88%r**..x88>BD$ $ QN++CtAw{{7/C/C 	u::XXk2&&**8R88Fzz.!! +6.)***4r   rH   rM   c                    t                      }| t          t          j                              dg|                    di           d<   t	          |           dS )zBPersist a dashboard bearer token under ``credential_pool.photon``.)rG   	issued_atrE   rF   Nr1   inttime
setdefaultrB   )rH   rN   s     r   store_photon_tokenrX      sU    <<DS-=-=>>8DOO%r**84 tr   #Tuple[Optional[str], Optional[str]]c                    t          j        d          } t          j        d          }| r|r| |fS t                      }|                    di                               d          pg }t	          |t
                    rO|rM|d         }|                    d          p|                    d          }| p||p|                    d          fS | |fS )	ul  Return the runtime SDK creds ``(spectrum_project_id, project_secret)``.

    Precedence: process env (``~/.hermes/.env`` is loaded into the gateway's
    environment at startup) wins, then ``auth.json`` for offline / status
    use.  This is the pair the Node sidecar feeds to ``spectrum-ts`` — the id
    is the **spectrumProjectId**, not the Dashboard id.
    PHOTON_PROJECT_IDPHOTON_PROJECT_SECRETrE   photon_projectr   spectrum_project_id
project_idproject_secretr   getenvr1   rJ   rK   rL   )env_idenv_secrN   projentrysids         r   load_project_credentialsrh      s     Y*++Fi/00G ' w<<D88%r**../?@@FBD$ G$ GQii-..I%))L2I2I#wE%))4D*E*EFF7?r   c                 B   t          j        d          } | r| S t                      }|                    di                               d          pg }t	          |t
                    r8|r6|d                             d          p|d                             d          S dS )z;Return the Dashboard project id (for management API calls).PHOTON_DASHBOARD_PROJECT_IDrE   r]   r   dashboard_project_idr_   Nra   )rc   rN   re   s      r   load_dashboard_project_idrl      s    Y455F <<D88%r**../?@@FBD$ P$ PAw{{122Od1gkk,6O6OO4r   )rk   namer^   r`   rk   rm   c                    t                      }| |t          t          j                              d}|r||d<   |r||d<   |g|                    di           d<   t	          |           t          | |           dS )a  Persist project credentials to both .env (runtime) and auth.json (mgmt).

    The runtime SDK creds land in ``~/.hermes/.env`` via the same
    ``save_env_value`` helper every other channel uses, so the gateway picks
    them up from the environment with zero adapter changes.  A copy of the
    non-secret ids (plus the secret, for offline ``status``) is written to
    ``auth.json`` so management commands work even when ``.env`` hasn't been
    loaded into the current process.
    )r^   r`   rS   rk   rm   rE   r]   N)r1   rU   rV   rW   rB   _persist_runtime_env)r^   r`   rk   rm   rN   records         r   store_project_credentialsrq      s      <<D2(%% F
  >)=%& v@FxDOO%r**+;<t,n=====r   phone_numberassigned_phone_numberuser_idrk   rs   rt   ru   c                    | s|sdS t                      }dt          t          j                              i}| r| |d<   |r||d<   |r||d<   |r||d<   |g|                    di           d<   t	          |           dS )	zEPersist non-secret Photon user numbers for offline ``status`` output.NrS   rs   rt   ru   rk   rE   photon_userrT   )rs   rt   ru   rk   rN   rp   s         r   store_user_numbersrx      s       5 <<D)3ty{{+;+;<F .!-~ @*?&' $#y >)=%&=CHDOO%r**=9tr   c                   	 ddl m} n+# t          $ r t                              d           Y dS w xY w	  |d|             |d|           dS # t
          $ r&}t                              d|           Y d}~dS d}~ww xY w)u'  Write the SDK creds to ``~/.hermes/.env`` (canonical runtime store).

    Isolated in its own helper so the secret value flows straight into
    ``save_env_value`` without ever being bound to a printable local in a
    caller — same CodeQL-clean-flow rationale as the rest of this module.
    r   )save_env_valueu=   photon: hermes_cli.config unavailable — skipping .env writeNr[   r\   z1photon: could not write project creds to .env: %s)hermes_cli.configrz   ImportErrorr-   r.   r   )r^   r`   rz   r0   s       r   ro   ro      s    4444444   VWWWO*,?@@@.????? O O OJANNNNNNNNNOs#   	 $11A 
A?A::A?c                  L    e Zd ZU ded<   ded<   ded<   ded<   ded<   ded	<   d
S )
DeviceCoderM   device_code	user_codeverification_urirC   verification_uri_completerU   
expires_inintervalN)r   r   r   __annotations__r   r   r   r~   r~     sO         NNN,,,,OOOMMMMMr   r~   T)frozenc                  (    e Zd ZU dZded<   ded<   dS )_DeviceTokenCandidatez<A token-like value extracted from the device-token response.rM   sourcerH   N)r   r   r   r   r   r   r   r   r   r     s(         FFKKKJJJJJr   r   c                 ^    t          j        d          pt                              d          S )NPHOTON_DASHBOARD_HOST/)r   rb   DEFAULT_DASHBOARD_HOSTrstripr   r   r   _dashboard_hostr     s(    I-..H2HPPQTUUUr   c                 ^    t          j        d          pt                              d          S )NPHOTON_SPECTRUM_HOSTr   )r   rb   DEFAULT_SPECTRUM_HOSTr   r   r   r   _spectrum_hostr     s'    I,--F1FNNsSSSr   Dict[str, str]c                    dd|  iS )NAuthorizationBearer r   )rH   s    r   _bearerr     s    .u..//r   r_   c                    t          |  d|                     d                                        d          }dd| iS )N:r$   asciir   zBasic )r   encodedecode)r_   r`   rH   s      r   _basicr     sM    66n66==gFFGGNNwWWE-e--..r   respr   c                T   	 |                                  }n# t          $ r d }Y nw xY wt          |t                    rKdD ]*}|                    |          }|rt          |          c S +t          j        |d          d d         S t          | dd          pd}|r
|d d         ndS )N)errormessagedetailT)r:   i  text zno response body)r)   r   rK   dictrJ   rM   dumpsgetattr)r   r2   keyvalr   s        r   _response_error_detailr   #  s    yy{{   $ 61 	  	 C((3--C  3xx z$$///554$$*D54::#55s    &&actionc           
     ~    t          | dd          }|dk     rd S t          d| d| dt          |                      )Nstatus_code     zPhoton z failed: HTTP z: )r   RuntimeErrorr   )r   r   statuss      r   _raise_for_statusr   2  sW    T=#..F||
P&PPPP2H2N2NPP  r   )	client_idscoper   r   c                   t           t          d          t                       d}d| i}|r||d<   t          j        ||d          }|                                 |                                }t          |d         |d	         |d
         |                    d          t          |                    d          pt                    t          |                    d          pt                              S )zBPOST ``/api/auth/device/code`` and return the device + user codes.N)httpx is required for Photon device loginz/api/auth/device/coder   r         >@r)   timeoutr   r   r   r   r   r   )r   r   r   r   r   r   )httpxr   r   postraise_for_statusr)   r~   rJ   rU   DEFAULT_POLL_TIMEOUTDEFAULT_POLL_INTERVAL)r   r   urlbodyr   r2   s         r   request_device_coder   ;  s     }FGGG
5
5
5C'3D W:cd333D99;;D'{#01"&((+F"G"Gtxx--E1EFFTXXj))B-BCC   r   )r   r   r   
on_pendingcoder   Optional[int]r   r   Optional[Callable[[], None]]c                  t           t          d          t                       d}t          j                    |p| j        pt
          z   }||n| j        pt          }t          j                    |k     rBt          j        |           	 t          j	        |d| j
        |dd          }n7# t           j        $ r%}	t                              d|	           Y d}	~	}d}	~	ww xY w|j        d	k    ri }
	 |                                pi }t!          |t"                    r|ni }
n$# t$          t&          t          j        f$ r i }
Y nw xY wt+          |
t-          |d
i                     }|st          d          |d         j        S |j        dk    r|dz  }|rt1          |           H|j        dk    ri }
	 |                                pi }
n# t          j        $ r Y nw xY w|
                    d          p|
                    d          pd}|dk    r|rt1          |           |dk    r|dz  }|rt1          |           |dv rt          d|           t          d|p|j                   t                              d|j        |j        dd	                    t          j                    |k     Bt7          d          )a  Poll ``/api/auth/device/token`` until the user approves.

    Mirrors the official CLI's polling loop: sleep first, then poll;
    ``authorization_pending`` keeps the interval, ``slow_down`` adds 5s,
    HTTP 429 adds 10s, and ``access_denied`` / ``expired_token`` abort.

    The bearer token comes from the response body's top-level
    ``access_token`` (better-auth device-grant shape), with
    ``session.access_token`` and the ``set-auth-token`` header kept as
    fallbacks for API drift.
    Nr   z/api/auth/device/tokenz,urn:ietf:params:oauth:grant-type:device_code)
grant_typer   r   r   r   z$photon: device-token poll failed: %sr   headersr   zPhoton returned 200 but no token candidate in the device-token response (expected access_token, data.access_token, accessToken, or set-auth-token).r   i  
   r   r   r   r   authorization_pending	slow_downr   )expired_tokenaccess_deniedzPhoton login failed: zPhoton device token error: z-photon: device-token unexpected status %s: %szPhoton device login timed out)r   r   r   rV   r   r   r   r   sleepr   r   RequestErrorr-   r.   r   r)   rK   r   	TypeError
ValueErrorr,   !_device_response_token_candidatesr   rH   _saferJ   r   TimeoutError)r   r   r   r   r   r   deadliner   r   r0   r   decoded
candidateserrs                 r   poll_for_tokenr   R  sS   & }FGGG
6
6
6Cy{{gPP<PQH ,HH4=3YDYE
)++
 
 
5	:"P#'#3!* 
   DD ! 	 	 	NNA1EEEHHHH	 s""#%D))+++",Wd";";Cwwz4+?@   :gdIr::  J  "J  
 a=&&s""RKE "j!!!s""Dyy{{(b'   ((7##@txx	':':@bC--- &*%%%k!!
 &*%%%888"#@3#@#@AAAOS=MDIOOPPP;dio	
 	
 	
s )++
 
 z 6
7
77s<    B, ,C ;CC 1/D! !EE4G GGr   r   r   Optional[Any]rL   c               J   g t                      dfd} |d|                     d                      |d	|                     d	                     |                     d
          }t          |t                    r |d|                    d                     |                     d          }t          |t                    r> |d|                    d                      |d|                    d	                      |dt	          |d                     S )a}  Extract de-duplicated token candidates from a device-token response.

    Photon's device-token endpoint has returned tokens under several keys
    across versions (``access_token``, ``accessToken``, ``data.*``) and the
    documented ``set-auth-token`` response header.  We collect every shape so
    the caller can validate each against the dashboard API before trusting it.
    r   rM   valuer   r   r3   c                    t          |          }|r|v rd S                     |                               t          | |                     d S )Nr   rH   )_clean_bearer_tokenaddappendr   )r   r   rH   r   seens      r   r   z._device_response_token_candidates.<locals>.add  s\    #E** 	F/vUKKKLLLLLr   rG   accessTokensessionzsession.access_tokenr2   zdata.access_tokenzdata.accessTokenzset-auth-token)r   rM   r   r   r   r3   )setrJ   rK   r   _header_value)r   r   r   r   r2   r   r   s        @@r   r   r     sD    JDM M M M M M M C00111Ctxx..///hhy!!G'4   A"GKK$?$?@@@88FD$ 9.!9!9::: 7 7888C-1ABBCCCr   r   c                    t          | t                    sd S |                                 }|                                                    d          r|dd                                          }|pd S )Nzbearer    )rK   rM   striplower
startswith)r   rH   s     r   r   r     se    eS!! tKKMME{{}}	** "abb	!!=Dr   c                   | sd S 	 |                      |          }|rt          |          S n# t          $ r Y nw xY w	 t          |                                           D ]O\  }}t          |                                          |                                k    r|rt          |          c S Pn# t          t          f$ r Y d S w xY wd S N)rJ   rM   AttributeErrorr   itemsr   r   r   )r   rm   r   r   s       r   r   r     s     tD!! 	u::	   w----// 	" 	"JC3xx~~4::<<//E/5zz!!!	" z"   tt4s$   %- 
::A/B0 .B0 0CCr   c                    t           t          d          t                       |  }t          j        |dd| id          S )Nr   r   r   r   r   r   )r   r   r   rJ   )r   rH   r   s      r   _dashboard_getr     s\    }FGGG
&
&
&C9 "3E"3"34   r   c                   t          d|           }|j        dv rt          d          |                                 |                                }t          |t                    r|                    d          nd}t          |t                    r|st          d          t          d|           }|j        dv rt          d          |                                 |S )	ac  Verify a device-flow token is usable for dashboard project APIs.

    The device flow can return a token that authenticates the Better Auth
    session lookup but is rejected by the project APIs.  Validate against
    ``/api/auth/get-session`` and ``/api/projects/`` so we fail loudly at
    login instead of saving a token that 404s/401s downstream.
    /api/auth/get-session)i  i  zKPhoton issued a device token, but the dashboard session lookup rejected it.userNzTPhoton issued a device token, but the dashboard session lookup did not recognize it./api/projects/zXPhoton device token was accepted for the session lookup but rejected by the project API.)r   r   r   r   r)   rK   r   rJ   )rH   r   r2   r   projects_resps        r   validate_photon_tokenr     s     1599D:%%&
 
 	
 	99;;D)$55?488F4DdD!! 
 
&$
 
 	
 ##3U;;M J..&+
 
 	
 ""$$$Kr   r   c                f   | st          d          d}d}| D ]N}	 t          |j                   |j        c S # t          $ r}|}|}Y d}~3d}~wt          $ r}|}Y d}~Gd}~ww xY w|7d                    d | D                       pd}t          | d| d          |||t          d          )	zBReturn the first candidate token that passes dashboard validation.zHPhoton returned 200 but no token candidate in the device-token response.Nz, c              3  $   K   | ]}|j         V  d S r   )r   ).0cs     r   	<genexpr>z-_validated_dashboard_token.<locals>.<genexpr>(  s$      99AH999999r   nonez@ Device login returned no project-valid dashboard token (tried: z).z.Photon did not return a usable dashboard token)r   r   rH   r   r   join)r   dashboard_error
last_error	candidateexcsourcess         r   _validated_dashboard_tokenr    s9    

 
 	
 ;?O*.J 
 
			!)/222?"""' 	 	 	!OJHHHH 	 	 	JHHHH	 "))99j99999CV& 3 3'.3 3 3
 
 	 
G
H
HHs!   8
A$AA$AA$fnCallable[[], None]c                >    	  |              d S # t           $ r Y d S w xY wr   )r   )r  s    r   r   r   2  s8    
   s   
 
)r   open_browseron_user_coder  boolr  (Optional[Callable[['DeviceCode'], None]]c                P   t          |           rt          fd           |r;	 ddl}j        pj        }|                    |d           n# t          $ r Y nw xY wt          |           }t          d|          g}t          |          }t          |           |S )	zRun the full device-code login flow and persist the token.

    Returns the bearer token.  ``on_user_code`` receives the
    :class:`DeviceCode` so callers can print it + optionally open a browser.
    )r   c                                 S r   r   )r   r  s   r   <lambda>z#login_device_flow.<locals>.<lambda>F  s    ll4(( r   r   Nr8   )newpollr   )r   r   
webbrowserr   r   r(   r   r   r   r  rX   )	r   r  r  r  targetfirst_tokenr   rH   r   s	     `     @r   login_device_flowr  9  s     333D *((((())) 	3Lt7LFOOFO**** 	 	 	D	 !;;;K'v[IIIJJ&z22EuLs   )A 
A"!A"c                    t           t          d          t                       d}t          j        |t	          |           d          }|                                 |                                pi S )uE   GET ``/api/auth/get-session`` — confirm the token + fetch the user.Nhttpx is required for Photonr   r   r   r   r   r   rJ   r   r   r)   rH   r   r   s      r   get_sessionr  Y  sk    }9:::
5
5
5C9S'%..$???D99;;"r   List[Dict[str, Any]]c                X   t          | t                    r| S t          | t                    r}dD ]z}|                     |          }t          |t                    r|c S t          |t                    r5dD ]2}|                    |          }t          |t                    r|c c S 3{g S )N)r2   projectsuserslinesr   )r  r   r!  r   )rK   rL   r   rJ   )r2   r   inner
nested_keynesteds        r   _unwrap_listr%  f  s    $ $ 	&B 	& 	&CHHSMME%&& %&& &"I & &J"YYz22F!&$// &%&Ir   c                   t           t          d          t                       d}t          j        |t	          |           d          }|                                 t          |                                          S )u7   GET ``/api/projects`` — return the caller's projects.Nr  /api/projectsr   r   r   r   r   rJ   r   r   r%  r)   r  s      r   list_projectsr)  v  sn    }9:::
-
-
-C9S'%..$???D		$$$r   Optional[Dict[str, Any]]c                    |pd                                                                 }t          |           D ]E}|                    d          pd                                                                 |k    r|c S FdS )z?Return the first project whose name matches (case-insensitive).r   rm   N)r   r   r)  rJ   )rH   rm   r  re   s       r   find_project_by_namer,    s    jb!!''))Fe$$  HHV"))++1133v==KKK >4r   c                    t           t          d          t                       d| }t          j        |t	          |           d          }|                                 |                                pi S )uM   GET ``/api/projects/{id}`` — includes ``spectrum`` + ``spectrumProjectId``.Nr  r   r   r   r  rH   r_   r   r   s       r   get_projectr/    sp    }9:::
:
:j
:
:C9S'%..$???D99;;"r   zUnited States)rm   locationr0  c                  t           t          d          t                       d}||dddd}t          j        ||t	          |           d          }|                                 |                                pi }|                    d	          rt          d
|d	                    |                    d          st          d          |S )zLPOST ``/api/projects`` with ``spectrum: true`` and return ``{success, id}``.Nz-httpx is required for Photon project creationr'  TF)rm   r0  spectrumtemplateobservabilityr   r)   r   r   r   zPhoton create-project failed: idz1Photon create-project did not return a project idr   r   r   r   r   r   r)   rJ   )rH   rm   r0  r   r   r   r2   s          r   create_projectr8    s     }JKKK
-
-
-C D :cgenndKKKD99;;"Dxx MKDMKKLLL88D>> PNOOOKr   c                ~   t           t          d          t          | |          }|                    d          s]t	                       d| d}t          j        |i t          |           d          }|                                 t          | |          }|                    d          st          d	          |S )
zEnable Spectrum on the project if needed; return the project dict.

    The dashboard exposes Spectrum as a toggle, so we only flip it when
    ``spectrum`` is currently false, then re-fetch to pick up the freshly
    populated ``spectrumProjectId``.
    Nr  r2  r   z/spectrum/toggler   r5  spectrumProjectIdu~   Spectrum is enabled but the project has no spectrumProjectId yet — retry in a moment, or enable Spectrum from the dashboard.)r   r   r/  rJ   r   r   r   r   )rH   r_   re   r   r   s        r   ensure_spectrum_enabledr;    s     }9:::uj))D88J . ""NN*NNNz#BMMM5*--88'(( 
H
 
 	
 Kr   c                   t           t          d          t                       d| d}t          j        |i t	          |           d          }|                                 |                                pi }|                    d          rt          d|d                    |                    d	          }|st          d
          t          |          S )u   POST ``/api/projects/{id}/regenerate-secret`` → the new project secret.

    This is the only way to read a project secret (the dashboard shows it
    exactly once), so callers should persist the returned value immediately.
    Nr  r   z/regenerate-secretr   r5  r   z!Photon regenerate-secret failed: projectSecretz2Photon regenerate-secret returned no projectSecret)	r   r   r   r   r   r   r)   rJ   rM   )rH   r_   r   r   r2   secrets         r   regenerate_project_secretr?    s     }9:::
L
Lj
L
L
LC:cGENNDIIID99;;"Dxx PNtG}NNOOOXXo&&F QOPPPv;;r   phonec                2    t          j        dd| pd          S )z?Reduce a phone string to ``+`` and digits for dedup comparison.z[^\d+]r   )resub)r@  s    r   _normalize_phonerD    s    6)R"---r   c                   t           t          d          t                       d|  d}t          j        |t	          | |          d          }t          |d           t          |                                          S )uD   GET Spectrum Cloud ``/projects/{id}/users/`` → ``SpectrumUser[]``.Nr  
/projects//users/r   r   z
list-users)r   r   r   rJ   r   r   r%  r)   )r_   r`   r   r   s       r   
list_usersrH    sx    }9:::
<
<
<
<
<C9S&^"D"DdSSSDdL)))		$$$r   c                    t          |          }t          | |          D ].}t          |                    d          pd          |k    r|c S /dS )zFReturn an existing Spectrum user with the given phone number, or None.phoneNumberr   N)rD  rH  rJ   )r_   r`   rs   r  r   s        r   find_user_by_phonerK    sb     l++F:~66  DHH]339r::fDDKKK E4r   F)
first_name	last_nameemailsend_inviterL  rM  rN  rO  c                  t           t          d          t                              |          st	          d|          t                       d|  d}d|d}|rt                              d           |r||d	<   |r||d
<   |r||d<   t          j        ||t          | |          d          }	t          |	d           |	                                pi }
|
                    d          rt          d|
d                    |
                    d          p|
                    d          p|
}t          |t                    r|S t          d          )zBPOST Spectrum Cloud ``/projects/{id}/users/`` and return the user.Nz*httpx is required for Photon user creationz4phone_number must be E.164 (e.g. +15551234567); got rF  rG  shared)typerJ  z?photon: send_invite is ignored by Spectrum shared-user creation	firstNamelastNamerN  r   r5  zcreate-userr   zPhoton create-user failed: r   r2   z2Photon create-user returned an unexpected response)r   r   E164_REmatchr   r   r-   debugr   r   r   r)   rJ   rK   r   )r_   r`   rs   rL  rM  rN  rO  r   r   r   r2   r   s               r   create_userrX    s    }GHHH==&& 
S<SS
 
 	
 
<
<
<
<
<C$,\JJD XVWWW '&[ %$Z W:z>22	  D dM***99;;"Dxx JHgHHIII88F7txx//74D$ 
K
L
LLr   )rL  rM  rN  Tuple[Dict[str, Any], bool]c               b    t          | ||          }||dfS t          | |||||          }|dfS )u   Idempotently register a Spectrum user.

    Returns ``(user, created)`` — ``created`` is False when a user with the
    same phone number already exists (the official CLI does no dedup, so we
    add it here to make ``setup`` safely re-runnable).
    NF)rs   rL  rM  rN  T)rK  rX  )r_   r`   rs   rL  rM  rN  existingr   s           r   register_user_if_absentr\  !  sW     "*nlKKH!  D :r   r   c                Z    | sdS |                      d          }|rt          |          ndS )u  Return the iMessage number a Spectrum user is assigned to text on.

    This is the user's ``assignedPhoneNumber`` (the dashboard's "TEXTS ON"
    column) — i.e. the number to text to reach the agent, as opposed to the
    user's own ``phoneNumber``. On shared-number plans there is no dedicated
    entry in ``/lines``, so this per-user field is the source of truth.
    Returns ``None`` when unset (e.g. a freshly created, not-yet-assigned user).
    NassignedPhoneNumber)rJ   rM   )r   r   s     r   user_assigned_liner_  >  s8      t
(((
)
)C$3s888$r   c                    t                      } |                     di                               d          pg }t          |t                    r|r|d         pi }t          |t                    r|                    d          p|                    d          }|                    d          p|                    d          }|s|r2|rt          |          nt                      |rt          |          ndfS t                      dfS )	zEReturn ``(operator_phone_number, assigned_phone_number)`` for status.rE   rw   r   rs   rJ  rt   r^  N)r1   rJ   rK   rL   r   rM   _configured_operator_phone)rN   user_entriesrf   r@  assigneds        r   load_user_numbersrd  M  s   <<D88-r2266}EEKL,%% , Q%2eT"" 
	IIn--I=1I1IE		122 499233    "'ICJJJ-G-I-I%-7CMMM4  &''--r   c                   t                      \  }}d}|rt          | ||          }n+t          | |          }t          |          dk    r|d         }d}|}|rq|                    d          }t          t          |                    d          pd                    }t                              |          r|}t          |          }t                      }	|st                      }
|
rt|	rr	 t          |
|	d          }|r*|                    d          rt          |d                   }n2# t          $ r%}t                              d	|           Y d}~nd}~ww xY wt!          |||rt          |          nd|	
           ||fS )zFRefresh cached user numbers from Photon without provisioning anything.N   r   r6  rJ  r   Fcreate_if_missingz6photon: could not refresh iMessage line for status: %srr   )rd  rK  rH  lenrJ   rD  rM   rU  rV  r_  rl   rQ   get_imessage_liner   r-   rW  rx   )r_   r`   r@  cached_assignedr   r   ru   rc  dashboard_phonedashboard_iddashboard_tokenliner0   s                r   refresh_user_numbersrp  a  s    /00E?%)D !*neDD:~66u::??8DG-H ,((4..*3txx/F/F/L"+M+MNN==)) 	$#E%d++,..L 8+-- 	8| 	88(# &+    8DHH]33 8"4#677H    La        & '1GT)	    (?s   -D, ,
E6EEc                    t          d          } | r+t          |           }t                              |          r|S t          d          }|sdS g }t	          j        d|          D ]@}t          |          }t                              |          r|                    |           At          |          dk    r|d         S dS )zDInfer the operator's E.164 number from existing Photon env settings.PHOTON_HOME_CHANNELPHOTON_ALLOWED_USERSNz[,\s]+rf  r   )_get_config_env_valuerD  rU  rV  rB  splitr   ri  )home
normalizedallowedr   parts        r   ra  ra    s     !677D %d++
==$$ 	#$:;;G tJG,, * *%d++
==$$ 	*j)))
:!!}4r   r   c                p    	 ddl m} n$# t          $ r t          j        |           cY S w xY w ||           S )Nr   )get_env_value)r{   r{  r   r   rb   )r   r{  s     r   rt  rt    s[    3333333   y~~=s   	 **c                   t           t          d          t                       d| d}t          j        |t	          |           d          }|                                 t          |                                          S )uO   GET ``/api/projects/{id}/lines`` → ``[{id, platform, phoneNumber, status}]``.Nr  r   /linesr   r   r(  r.  s       r   
list_linesr~    sv    }9:::
@
@j
@
@
@C9S'%..$???D		$$$r   imessageplatformr  c                  t           t          d          t                       d| d}t          j        |d|it	          |           d          }|                                 |                                pi }|                    d          rt          d	|d                    |                    d
          p|S )z:POST ``/api/projects/{id}/lines`` to provision a new line.Nr  r   r}  r  r   r5  r   zPhoton add-line failed: ro  r7  )rH   r_   r  r   r   r2   s         r   add_liner    s     }9:::
@
@j
@
@
@C::x('%..$  D 	99;;"Dxx GEd7mEEFFF88F#t#r   rg  rh  c                  t          | |          D ]3}|                    d          pd                                dk    r|c S 4|rF	 t          | |d          S # t          $ r&}t
                              d|           Y d}~dS d}~ww xY wdS )zReturn the project's iMessage line (the number to text the agent).

    If none exists and ``create_if_missing`` is set, provision one.  Returns
    ``None`` if there is no line and provisioning failed.
    r  r   r  r  z2photon: could not auto-provision iMessage line: %sN)r~  rJ   r   r  r   r-   r.   )rH   r_   rh  ro  r0   s        r   rj  rj    s     5*--  HHZ  &B--//:==KKK > 	E:
CCCC 	 	 	NNOQRSSS44444	 4s   A 
B
$BB
emitc           
        i }t                      rdnd|d<   t                      \  }}|r|nd|d<   t                      pd|d<   |rdnd|d<   t                      \  }}|r|nd	|d
<   |r|nd|d<   ddd|d         z   d|d         z   d|d         z   d|d         z   d|d
         z   d|d         z   g} | d                    |                     dS )uR  Pretty-print the credential status table via the *emit* callback.

    Every secret-bearing read is reduced to a display literal inside this
    function (``"✓ stored"`` / ``"✗ missing"`` / a non-secret id); the
    callback only ever receives the assembled banner string, so no tainted
    value escapes into the caller's scope.
    
   ✓ stored'   ✗ missing (run `hermes photon setup`)device_token   ✗ missingr^      —rk   project_key3   ✗ missing (run `hermes photon setup --phone ...`)rs   rt   zPhoton iMessage statusuB   ──────────────────────z  device token        : z  dashboard project   : z  spectrum project id : z  project secret      : z  my number           : z  assigned number     : 
N)rQ   rh   rl   rd  r  )r  labelsrg   secr@  rc  rowss          r   print_credential_summaryr    s8     F)++ 	76 > ())HC+.$ACCMF !%>%@%@%IEF!",/BLL]F='))OE8QQ > K"K "#
 	!L"VN%;;"V,B%CC"V,A%BB"VM%::"VN%;;"V,C%DD	D 	D4r   c                     d
d} d
d}d
d}d
d}d
d} |             t                      pd |             |             |             |            d	S )zEReturn a fully pre-formatted credential status dict (no raw secrets).r   rM   c                 &    t                      rdndS )Nr  r  )rQ   r   r   r   _present_tokenz*credential_summary.<locals>._present_token  s    -// ;LL:	
r   c                 ,    t                      \  } }| pdS )Nr  rh   )rg   _secs     r   _present_spectrum_idz0credential_summary.<locals>._present_spectrum_id  s    ,..	T#m#r   c                 0    t                      \  } }|rdndS )Nr  r  r  )_sidr  s     r   _present_secretz+credential_summary.<locals>._present_secret  s     ,..	c"5||5r   c                 ,    t                      \  } }| pdS )Nr  rd  )r@  	_assigneds     r   _present_phonez*credential_summary.<locals>._present_phone  s    ,..yMMMr   c                 ,    t                      \  } }|pdS )Nr  r  )_phonerc  s     r   _present_assigned_phonez3credential_summary.<locals>._present_assigned_phone  s    ,..DDDr   r  )r  rk   r^   r  rs   rt   r   rM   )rl   )r  r  r  r  r  s        r   credential_summaryr  
  s    
 
 
 
$ $ $ $6 6 6 6N N N NE E E E
 '(( 9 ; ; Du3355&((&((!8!8!:!:  r   )r   r   )r   r!   )r2   r!   r   r3   )r   rC   )rH   rM   r   r3   )r   rY   )
r^   rM   r`   rM   rk   rC   rm   rC   r   r3   )
rs   rC   rt   rC   ru   rC   rk   rC   r   r3   )r^   rM   r`   rM   r   r3   r  )rH   rM   r   r   )r_   rM   r`   rM   r   r   )r   r   r   rM   )r   r   r   rM   r   r3   )r   rM   r   rC   r   r~   )r   r~   r   rM   r   r   r   r   r   r   r   rM   )r   r!   r   r   r   rL   )r   r   r   rC   )r   r   rm   rM   r   rC   )r   rM   rH   rM   r   r   )rH   rM   r   r!   )r   rL   r   rM   )r  r	  r   r3   )r   rM   r  r  r  r  r   rM   )r2   r   r   r  )rH   rM   r   r  )rH   rM   rm   rM   r   r*  )rH   rM   r_   rM   r   r!   )rH   rM   rm   rM   r0  rM   r   r!   )rH   rM   r_   rM   r   rM   )r@  rM   r   rM   )r_   rM   r`   rM   r   r  )r_   rM   r`   rM   rs   rM   r   r*  )r_   rM   r`   rM   rs   rM   rL  rC   rM  rC   rN  rC   rO  r  r   r!   )r_   rM   r`   rM   rs   rM   rL  rC   rM  rC   rN  rC   r   rY  )r   r*  r   rC   )r_   rM   r`   rM   r   rY   )r   rM   r   rC   )rH   rM   r_   rM   r   r  )rH   rM   r_   rM   r  rM   r   r!   )rH   rM   r_   rM   rh  r  r   r*  )r  r   r   r3   )r   r   )Yr   
__future__r   r)   loggingr   rB  rV   base64r   dataclassesr   pathlibr   typingr   r   r	   r
   r   r   r   r|   	getLoggerr   r-   r   r   DEFAULT_CLIENT_IDDEFAULT_SCOPEr   r   DEFAULT_PROJECT_NAMEr   r   compilerU  r    r1   rB   rQ   rX   rh   rl   rq   rx   ro   r~   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r  r   r  r  r%  r)  r,  r/  r8  r;  r?  rD  rH  rK  rX  r\  r_  rd  rp  ra  rt  r~  r  rj  printr  r  r   r   r   <module>r     s  # #H # " " " " "   				 				        ! ! ! ! ! !       = = = = = = = = = = = = = = = =LLLL   EEE 
	8	$	$P P P P P| P P P ! &3 7  &    
"*)
*
*C C C C	 	 	 	
 
 
 
         ,	 	 	 	  +/> > > > > >B #'+/!*.     0O O O O,         $       V V V VT T T T0 0 0 0/ / / /
6 6 6 6    *-     4 '!"/3U8 U8 U8 U8 U8 U8v "           F      $      @I I I I>    '=A	     @       % % % %       %#	     6   .   .. . . .
% % % %     !%#(M (M (M (M (M (M` !%#     :% % % %. . . .(. . . .b   *   % % % % 5?$ $ $ $ $ $$ ?C     . */ # # # # #L     s    A AA