
    epj                       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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ddlmZ dd	lmZ dd
Z 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Z0dd!Z1dd"Z2dd$Z3dd&Z4dd'Z5ddd(dd-Z6ddddd.dd2Z7dd3Z8e G d4 d5                      Z9 ed67           G d8 d9                      Z:dd:Z;dd;Z<dd=Z=dd?Z>ddBZ?ddDZ@e$e%dEddHZAe$ddddIddPZBddQddVZCddXZDddYZEdd[ZFdd\ZGdd^ZHddaZIe$d6ddbddfZJddgZKddiZLddjZMddlZNe(dmdnddpZOddqZPddsZQddtZRdduZSddddvdwdd|ZTdddd}ddZUddZVddZWddZXddZYddZZddZ[ddddZ\d6dddZ]e^fddZ_ddZ`d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, 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 has a single identifier: the dashboard ``id`` *is* the
Spectrum Cloud project id. They used to diverge (a separate
``spectrumProjectId`` field), but the dashboard unified them — every
project is created with matching ids and the pre-existing diverged rows
were backfilled so ``project.id == spectrumProjectId`` everywhere
(dashboard ENG-1582). Spectrum is always enabled and provisioned at
create-time, so there is no enable/toggle step anymore.

The ``spectrum-ts`` SDK (run by the Node sidecar) authenticates to Spectrum
Cloud with ``(id, projectSecret)`` — the same ``id`` used in Dashboard API
paths — which we persist as ``PHOTON_PROJECT_ID`` for the runtime.

Credential storage mirrors every other Hermes channel:

    * runtime SDK creds  -> ``~/.hermes/.env``  (``PHOTON_PROJECT_ID`` =
      project id, ``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Tuple)UnscopedSecretError)
get_secretc                z    	 t          | |          }n$# t          $ r t          j        |           }Y nw xY w||n|S )a  Scope-aware credential read with the default-profile startup fallback.

    Secondary profiles construct their adapters under a profile secret
    scope -- the scope is authoritative and a scoped miss returns ``default``
    (no cross-profile borrow from ``os.environ``, which may hold another
    profile's value). The DEFAULT profile's adapter constructs and sends
    *unscoped* under multiplexing, where a bare ``get_secret`` would raise
    ``UnscopedSecretError`` and crash this path; there ``os.environ`` is that
    profile's own value, so fall back to it. Same pattern as the Slack
    ``SLACK_APP_TOKEN`` read (#59739) and
    ``gateway/platforms/whatsapp_common.py::_get_wsecret``.
    )_scoped_get_secret_UnscopedSecretErrorosgetenv)namedefaultvals      C/home/thesage/.hermes/hermes-agent/plugins/platforms/photon/auth.py_get_scoped_secretr   <   sR     w//   ioo/33w.s    44c                      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__     r   r   r   S   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   	Exceptionr   path
expanduserr$   s    r   _auth_json_pathr*   r   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_authr;   {   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           |                    |j         dt          j                     dt          j                    j	                   }t          j
        t          |          t
          j        t
          j        z  t
          j        z  t          j        t          j        z            }	 t          j        |dd          }n[# t&          $ rN 	 t          j        |           n# t*          $ r Y nw xY w	 |                                 n# t*          $ r Y nw xY w w xY w	 |5  t/          j        | |dd	           |                                 t          j        |                                           d d d            n# 1 swxY w Y   |                    |           d S # t&          $ r( 	 |                                 n# t*          $ r Y nw xY w w xY w)
NT)parentsexist_okz.tmp..wr.   r/      )indent	sort_keys)r*   parentmkdir	with_namer   r   getpiduuiduuid4hexr2   strO_WRONLYO_CREATO_EXCLstatS_IRUSRS_IWUSRfdopenBaseExceptioncloser5   unlinkr3   dumpflushfsyncfilenoreplace)r<   r(   tmpfdr9   s        r   
_save_authr_      sJ   DKdT222 ..DILLBIKKLL$*,,:JLL
M
MC 
C
bj 29,t|#
 
B
Yr3111   	HRLLLL 	 	 	D		JJLLLL 	 	 	D	 	" 	"IdBqD9999HHJJJHRYY[[!!!	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	D   	JJLLLL 	 	 	D	s   C+ +
E6D
E
DEDED10E1
D>;E=D>>EG
 	AF(G
 (F,,G
 /F,0G
 

G<G*)G<*
G74G<6G77G<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)r;   get
isinstancelistrM   )authpoolre   legacys       r   load_photon_tokenrm      s    <<D88%r**..x88>BD$ $ QN++CtAw{{7/C/C 	u::XXk2&&**8R88Fzz.!! +6.)***4r    re   rM   c                   ddl m}  |            5  t                      }| t          t	          j                              dg|                    di           d<   t          |           ddd           dS # 1 swxY w Y   dS )zBPersist a dashboard bearer token under ``credential_pool.photon``.r   _auth_store_lock)rd   	issued_atrb   rc   Nhermes_cli.authrp   r;   inttime
setdefaultr_   )re   rp   rj   s      r   store_photon_tokenrw      s    000000				  ||"TY[[1A1ABB<
)2..x8 	4                 s   AA77A;>A;c                 0   t                      } |                     di           }|                    dg           }t          |t                    r|rg |d<   t	          |            |                     di           }d|v ri |d<   t	          |            dS dS )zRemove any stored Photon dashboard token from auth.json.

    Used to discard a stale/expired token before re-authentication.
    rb   rc   rf   N)r;   rg   rh   ri   r_   )rj   rk   rc   rf   s       r   clear_photon_tokenry      s    
 <<D88%r**DXXh##F&$ F X4b))I9 	(4 r    boolc                h    | sdS 	 t          |            dS # t          $ r Y dS t          $ r Y dS w xY w)u*  Return True if the token is accepted by the dashboard API.

    Delegates to :func:`validate_photon_token`, which checks both
    ``/api/auth/get-session`` and ``/api/projects/`` — the device flow can
    mint tokens that pass the session lookup but are rejected by the project
    APIs, and setup's management calls all hit the project APIs.  A
    definitive rejection (``PhotonDashboardAuthError``) is treated as stale;
    transient failures (network blips, 5xx) are treated as "probably valid"
    so they don't force an unnecessary re-login.
    FT)validate_photon_tokenr   r'   re   s    r   check_photon_token_validr~      sf      ue$$$t#   uu    tts    
1	11#Tuple[Optional[str], Optional[str]]c                    t          j        d          } t          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 )	at  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 unified project id (dashboard id == spectrumProjectId).
    PHOTON_PROJECT_IDPHOTON_PROJECT_SECRETrb   photon_projectr   spectrum_project_id
project_idproject_secret)r   r   r   r;   rg   rh   ri   )env_idenv_secrj   projentrysids         r   load_project_credentialsr      s     Y*++F !899G ' w<<D88%r**../?@@FBD$ G$ GQii-..I%))L2I2I#wE%))4D*E*EFF7?r    c                 d   t          j        d          } | r| S t                      }|                    di                               d          pg }t	          |t
                    rI|rG|d         }|                    d          p)|                    d          p|                    d          S dS )	a  Return the project id used for management API calls.

    Post-unification the dashboard id and the Spectrum id are the same value,
    so we prefer the stored ``spectrum_project_id``: for pre-backfill installs
    the old ``dashboard_project_id`` is the diverged id that the unification
    rewrote (it now 404s), while the Spectrum id always matches the live row.
    Falls back to the legacy keys for older records.
    PHOTON_DASHBOARD_PROJECT_IDrb   r   r   r   dashboard_project_idr   N)r   r   r;   rg   rh   ri   )r   rj   r   r   s       r   load_dashboard_project_idr     s     Y455F <<D88%r**../?@@FBD$ 
$ 
QII+,, 'yy/00'yy&&	

 4r    )r   r   r   r   r   r   c                H   ddl m}  |            5  t                      }| |t          t	          j                              d}|r||d<   |r||d<   |g|                    di           d<   t          |           ddd           n# 1 swxY w Y   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   ro   )r   r   rq   r   r   rb   r   N)rs   rp   r;   rt   ru   rv   r_   _persist_runtime_env)r   r   r   r   rp   rj   records          r   store_project_credentialsr   '  s     100000				  ||#6,TY[[))"
 "

   	B-AF)* 	"!F6NDJ8)2../?@4               ,n=====s   A*BBBphone_numberassigned_phone_numberuser_idr   r   r   r   c                N   | s|sdS ddl m}  |            5  t                      }dt          t	          j                              i}| r| |d<   |r||d<   |r||d<   |r||d<   |g|                    d	i           d
<   t          |           ddd           dS # 1 swxY w Y   dS )zEPersist non-secret Photon user numbers for offline ``status`` output.Nr   ro   rq   r   r   r   r   rb   photon_userrr   )r   r   r   r   rp   rj   r   s          r   store_user_numbersr   I  s4      5 000000				  ||"-s49;;/?/?!@ 	2%1F>"  	D.CF*+ 	( 'F9 	B-AF)*AG)2..}=4                 s   A6BB!B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.configr   ImportErrorr7   r8   r'   )r   r   r   r:   s       r   r   r   d  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_urir`   verification_uri_completert   
expires_inintervalN)r   r   r   __annotations__r   r    r   r   r   z  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   sourcere   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   r   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   r   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   r}   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   re   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)rE   i  text zno response body)r3   r'   rh   dictrg   rM   dumpsgetattr)r   r<   keyr   r   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     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         >@r3   timeoutr   r   r   r   r   r   )r   r   r   r   r   r   )httpxr   r   postraise_for_statusr3   r   rg   rt   DEFAULT_POLL_TIMEOUTDEFAULT_POLL_INTERVAL)r   r   urlbodyr   r<   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   ru   r   r   r   r   sleepr   r   RequestErrorr7   r8   r   r3   rh   r   	TypeError
ValueErrorr6   !_device_response_token_candidatesr   re   _saferg   r   TimeoutError)r   r   r   r   r   r   deadliner   r   r:   r   decoded
candidateserrs                 r   poll_for_tokenr     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]ri   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"   r=   c                    t          |          }|r|v rd S                     |                               t          | |                     d S )Nr   re   )_clean_bearer_tokenaddappendr   )r   r   re   r   seens      r   r  z._device_response_token_candidates.<locals>.add2  s\    #E** 	F/vUKKKLLLLLr    rd   accessTokensessionzsession.access_tokenr<   zdata.access_tokenzdata.accessTokenzset-auth-token)r   rM   r   r   r"   r=   )setrg   rh   r   _header_value)r   r   r  r  r<   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    )rh   rM   striplower
startswith)r   re   s     r   r  r  F  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)rg   rM   AttributeErrorr   itemsr  r   r   )r   r   r   r   s       r   r
  r
  O  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   rg   )r(   re   r   s      r   _dashboard_getr  a  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   r3   rh   r   rg   )re   r   r<   r  projects_resps        r   r|   r|   l  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|   re   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     s8    
   s   
 
)r   open_browseron_user_coder,  r-  (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>  s    ll4(( r    r   NrC   )newpollr  )r   r   
webbrowserr   r   r2   r'   r   r   r(  rw   )	r   r,  r-  r4  targetfirst_tokenr   re   r   s	     `     @r   login_device_flowr7    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   rg   r   r   r3   re   r   r   s      r   get_sessionr;    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)r<   projectsuserslinesr  )r>  r?  r@  r  )rh   ri   r   rg   )r<   r   inner
nested_keynesteds        r   _unwrap_listrD    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.Nr9  /api/projectsr   r  r   r   r   rg   r   r   rD  r3   r:  s      r   list_projectsrH    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   r   N)r  r  rH  rg   )re   r   r5  r   s       r   find_project_by_namerK    s    jb!!''))Fe$$  HHV"))++1133v==KKK >4r    zUnited States)r   locationrL  c                  t           t          d          t                       d}||ddd}t          j        ||t	          |           d          }|                                 |                                pi }t          |t                    st          d          |	                    d	          rt          d
|d	                    |	                    d          du r't          d
|	                    d          p|           |	                    d          }t          |t                    r|n|}|	                    d          st          d          |S )zPOST ``/api/projects`` and return ``{success, id}``.

    Spectrum is always provisioned at create-time, so the request body no
    longer carries a ``spectrum`` flag (the field was dropped from the API).
    Nz-httpx is required for Photon project creationrF  F)r   rL  templateobservabilityr   r3   r   r   z5Photon create-project returned an unexpected responser   zPhoton create-project failed: succeedr   r<   idz1Photon create-project did not return a project id)
r   r   r   r   r   r   r3   rh   r   rg   )	re   r   rL  r   r   r   r<   project_candidateprojects	            r   create_projectrU    sy    }JKKK
-
-
-C	 D :cgenndKKKD99;;"DdD!! TRSSSxx MKDMKKLLLxx	e##JTXXi-@-@-HDJJ
 
 	
 ((3=>OQU3V3V`//\`G;;t PNOOONr    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.
    Nr9  r  z/regenerate-secretr   rP  r   z!Photon regenerate-secret failed: projectSecretz2Photon regenerate-secret returned no projectSecret)	r   r   r   r   r   r   r3   rg   rM   )re   r   r   r   r<   secrets         r   regenerate_project_secretrY  (  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)rZ  s    r   _normalize_phoner^  ?  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[]``.Nr9  
/projects//users/r   r  z
list-users)r   r   r   rg   r   r   rD  r3   )r   r   r   r   s       r   
list_usersrb  D  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)r^  rb  rg   )r   r   r   r5  r  s        r   find_user_by_phonere  N  sb     l++F:~66  DHH]339r::fDDKKK E4r    F)
first_name	last_nameemailsend_inviterf  rg  rh  ri  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 r`  ra  shared)typerd  z?photon: send_invite is ignored by Spectrum shared-user creation	firstNamelastNamerh  r   rP  zcreate-userr   zPhoton create-user failed: r  r<   z2Photon create-user returned an unexpected response)r   r   E164_REmatchr   r   r7   debugr   r   r   r3   rg   rh   r   )r   r   r   rf  rg  rh  ri  r   r   r   r<   r  s               r   create_userrr  Y  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    )rf  rg  rh  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)r   rf  rg  rh  T)re  rr  )r   r   r   rf  rg  rh  existingr  s           r   register_user_if_absentrv    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)rg   rM   )r  r   s     r   user_assigned_linery    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.rb   r   r   r   rd  r   rx  N)r;   rg   rh   ri   r   rM   _configured_operator_phone)rj   user_entriesr   rZ  assigneds        r   load_user_numbersr~    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   rR  rd  r   Fcreate_if_missingz6photon: could not refresh iMessage line for status: %sr   )r~  re  rb  lenrg   r^  rM   ro  rp  ry  r   rm   get_imessage_liner'   r7   rq  r   )r   r   rZ  cached_assignedr  r?  r   r}  dashboard_phonedashboard_iddashboard_tokenliner:   s                r   refresh_user_numbersr    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]+r  r   )_get_config_env_valuer^  ro  rp  r\  splitr  r  )home
normalizedallowedr   parts        r   r{  r{    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   r   )r   r  s     r   r  r  
  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}]``.Nr9  r  /linesr   r  rG  )re   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.Nr9  r  r  r  r   rP  r   zPhoton add-line failed: r  )r   r   r   r   r   r   r3   rg   )re   r   r  r   r   r<   s         r   add_liner    s     }9:::
@
@j
@
@
@C::x('%..$  D 	99;;"Dxx GEd7mEEFFF88F#t#r    r  r  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  rg   r  r  r'   r7   r8   )re   r   r  r  r:   s        r   r  r  0  s     5*--  HHZ  &B--//:==KKK > 	E:
CCCC 	 	 	NNOQRSSS44444	 4s   A 
B
$BB
emitc           	     h   i }t                      rdnd|d<   t                      \  }}|r|n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   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   project_key3   ✗ missing (run `hermes photon setup --phone ...`)r   r   zPhoton iMessage statusuB   ──────────────────────z  device token        : z  project id          : z  project secret      : z  my number           : z  assigned number     : 
N)rm   r   r~  r"  )r  labelsr   secrZ  r}  rowss          r   print_credential_summaryr  G  s     F)++ 	76 > ())HC #&833=F<,/BLL]F='))OE8QQ > K"K "#
 	!L"VN%;;"VL%99"VM%::"VN%;;"V,C%DDD 	D4r    c                     d	d} d	d}d	d}d	d}d	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  )rm   r   r    r   _present_tokenz*credential_summary.<locals>._present_tokeno  s    -// ;LL:	
r    c                 ,    t                      \  } }| pdS )Nr  r   )r   _secs     r   _present_project_idz/credential_summary.<locals>._present_project_idu  s    ,..	T#m#r    c                 0    t                      \  } }|rdndS )Nr  r  r  )_sidr  s     r   _present_secretz+credential_summary.<locals>._present_secrety  s     ,..	c"5||5r    c                 ,    t                      \  } }| pdS )Nr  r~  )rZ  	_assigneds     r   _present_phonez*credential_summary.<locals>._present_phone}  s    ,..yMMMr    c                 ,    t                      \  } }|pdS )Nr  r  )_phoner}  s     r   _present_assigned_phonez3credential_summary.<locals>._present_assigned_phone  s    ,..DDDr    )r  r   r  r   r   r"   rM   r   )r  r  r  r  r  s        r   credential_summaryr  m  s    
 
 
 
$ $ $ $6 6 6 6N N N NE E E E
 '(())++&((&((!8!8!:!:  r    r  )r"   r   )r"   r+   )r<   r+   r"   r=   )r"   r`   )re   rM   r"   r=   )r"   r=   )re   rM   r"   rz   )r"   r   )
r   rM   r   rM   r   r`   r   r`   r"   r=   )
r   r`   r   r`   r   r`   r   r`   r"   r=   )r   rM   r   rM   r"   r=   r  )re   rM   r"   r   )r   rM   r   rM   r"   r   )r   r   r"   rM   )r   r   r   rM   r"   r=   )r   rM   r   r`   r"   r   )r   r   r   rM   r   r   r   r   r   r   r"   rM   )r   r+   r   r   r"   ri   )r   r   r"   r`   )r   r   r   rM   r"   r`   )r(   rM   re   rM   r"   r   )re   rM   r"   r+   )r   ri   r"   rM   )r)  r*  r"   r=   )r   rM   r,  rz   r-  r.  r"   rM   )r<   r   r"   r<  )re   rM   r"   r<  )re   rM   r   rM   r"   rI  )re   rM   r   rM   rL  rM   r"   r+   )re   rM   r   rM   r"   rM   )rZ  rM   r"   rM   )r   rM   r   rM   r"   r<  )r   rM   r   rM   r   rM   r"   rI  )r   rM   r   rM   r   rM   rf  r`   rg  r`   rh  r`   ri  rz   r"   r+   )r   rM   r   rM   r   rM   rf  r`   rg  r`   rh  r`   r"   rs  )r  rI  r"   r`   )r   rM   r   rM   r"   r   )r   rM   r"   r`   )re   rM   r   rM   r"   r<  )re   rM   r   rM   r  rM   r"   r+   )re   rM   r   rM   r  rz   r"   rI  )r  r   r"   r=   )r"   r   )ar   
__future__r   r3   loggingr   r\  rQ   ru   rJ   base64r   dataclassesr   pathlibr   typingr   r   r	   r
   r   r   r   r   agent.secret_scoper   r   r   r   r   	getLoggerr   r7   r   r   DEFAULT_CLIENT_IDDEFAULT_SCOPEr   r   DEFAULT_PROJECT_NAMEr   r   compilero  r*   r;   r_   rm   rw   ry   r~   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r  r
  r  r|   r(  r   r7  r;  rD  rH  rK  rU  rY  r^  rb  re  rr  rv  ry  r~  r  r{  r  r  r  r  printr  r  r   r    r   <module>r     s  # #H # " " " " "   				 				          ! ! ! ! ! !       = = = = = = = = = = = = = = = =LLLL   EEE K J J J J J ? ? ? ? ? ?/ / / /( 
	8	$	$P P P P P| P P P ! &3 7  &    
"*)
*
*C C C C	 	 	 	* * * *Z   	 	 	 	   $   0   ,   8 +/> > > > > >H #'+/!*.     6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	     @       % % % %    %#	# # # # # #L   .. . . .
% % % %     !%#(M (M (M (M (M (M` !%#     :% % % %. . . .(. . . .b   *   % % % % 5?$ $ $ $ $ $$ ?C     . */ # # # # #L     s   A AA