
    `gjݕ                    "   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 ej.                  e      Z G d d	e      Zd
ZdZdZdZdZ dZ!dZ" ejF                  d      Z$dUdZ%dVdZ&dWdZ'dXdZ(dYdZ)dZdZ*dXdZ+ddd	 	 	 	 	 	 	 	 	 d[dZ,ddddd	 	 	 	 	 	 	 	 	 d\dZ-d]dZ.e G d d             Z/ ed !       G d" d#             Z0d^d$Z1d^d%Z2d_d&Z3d`d'Z4dad(Z5dbd)Z6eed*	 	 	 	 	 dcd+Z7edddd,	 	 	 	 	 	 	 	 	 	 	 ddd-Z8dd.	 	 	 	 	 ded/Z9dfd0Z:dgd1Z;dhd2Z<did3Z=djd4Z>dkd5Z?ed dd6	 	 	 	 	 	 	 dld7Z@did8ZAdmd9ZBdnd:ZCdod;ZDe d<d=	 	 	 	 	 	 	 dpd>ZEdqd?ZFdrd@ZGdsdAZH	 	 	 	 	 	 	 	 dtdBZIddddCdD	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 dudEZJddddF	 	 	 	 	 	 	 	 	 	 	 	 	 dvdGZKdwdHZLdZdIZM	 	 	 	 	 	 dxdJZNdXdKZOdydLZPdzdMZQdNdO	 	 	 	 	 	 	 d{dPZRd dQ	 	 	 	 	 	 	 d|dRZSeTfd}dSZUd~dTZVy# e$ r dZY w xY w)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Tuplec                      e Zd ZdZy)PhotonDashboardAuthErrorzERaised when Photon rejects a device-flow token for the dashboard API.N)__name__
__module____qualname____doc__     P/root/.hermes/venv/lib/python3.12/site-packages/plugins/platforms/photon/auth.pyr   r   9   s    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}$c                     	 ddl m}  t         |              dz  S # t        $ r. t        t        j
                  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   sN    C4O%&44 CBGG&&{34{BBCs    4AAc                 >   t               } | j                         si S 	 | j                  dd      5 }t        j                  |      xs i cd d d        S # 1 sw Y   y xY w# t
        t        j                  f$ r#}t        j                  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/   a   s    D;;=	YYsWY- 	'99R=&B	' 	' 	'T))* 6a@	s:   A  A
	A  AA  A   B9BBBc                d   t               }|j                  j                  dd       |j                  d      }|j	                  dd      5 }t        j                  | |dd       d d d        	 t        j                  |d	       |j                  |       y # 1 sw Y   2xY w# t        $ r Y )w xY w)
NT)parentsexist_okz	.json.tmpwr"   r#      )indent	sort_keysi  )r   parentmkdirwith_suffixr&   r'   dumpr   chmodr)   replace)datar   tmpr-   s       r   
_save_authr?   m   s    DKKdT2


;
'C	#	( 6B		$156
e KK6 6  s   B/B# B #	B/.B/c                 v   t               } | j                  di       j                  d      xs g }t        |t              r9|r7|d   j                  d      xs |d   j                  d      }|rt	        |      S | j                  di       j                  di       }|j                  d      rt	        |d         S y)zFReturn the device-flow bearer token stored by ``login()`` or ``None``.credential_poolphotonr   access_tokentoken	providersN)r/   get
isinstanceliststr)authpoolrD   legacys       r   load_photon_tokenrM   z   s    <D88%r*..x8>BD$$QN+CtAw{{7/Cu:XXk2&**8R8Fzz.!6.)**r   c                    t               }| t        t        j                               dg|j                  di       d<   t	        |       y)zBPersist a dashboard bearer token under ``credential_pool.photon``.)rC   	issued_atrA   rB   Nr/   inttime
setdefaultr?   )rD   rJ   s     r   store_photon_tokenrT      s>    <DS-=>8DOO%r*84 tr   c                 x   t        j                  d      } t        j                  d      }| r|r| |fS t               }|j                  di       j                  d      xs g }t	        |t
              rF|rD|d   }|j                  d      xs |j                  d      }| xs ||xs |j                  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_SECRETrA   photon_projectr   spectrum_project_id
project_idproject_secretr   getenvr/   rF   rG   rH   )env_idenv_secrJ   projentrysids         r   load_project_credentialsrc      s     YY*+Fii/0G'w<D88%r*../?@FBD$$Qii-.I%))L2I#wE%))4D*EFF7?r   c                 0   t        j                  d      } | r| S t               }|j                  di       j                  d      xs g }t	        |t
              r>|r<|d   }|j                  d      xs$ |j                  d      xs |j                  d      S y)	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_IDrA   rX   r   rY   dashboard_project_idrZ   Nr\   )r^   rJ   r`   ra   s       r   load_dashboard_project_idrg      s     YY45F<D88%r*../?@FBD$$QII+, 'yy/0'yy&	

 r   )rf   namec                    t               }| |t        t        j                               d}|r||d<   |r||d<   |g|j                  di       d<   t	        |       t        | |       y)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.
    )rY   r[   rO   rf   rh   rA   rX   N)r/   rQ   rR   rS   r?   _persist_runtime_env)rY   r[   rf   rh   rJ   records         r   store_project_credentialsrl      sn      <D2(%F
 )=%&v@FxDOO%r*+;<t,n=r   phone_numberassigned_phone_numberuser_idrf   c                    | s|syt               }dt        t        j                               i}| r| |d<   |r||d<   |r||d<   |r||d<   |g|j                  di       d<   t	        |       y)	zEPersist non-secret Photon user numbers for offline ``status`` output.NrO   rn   ro   rp   rf   rA   photon_userrP   )rn   ro   rp   rf   rJ   rk   s         r   store_user_numbersrs      s      5<D)3tyy{+;<F!-~*?&'#y)=%&=CHDOO%r*=9tr   c                    	 ddl m} 	  |d|         |d|       y# t        $ r t        j	                  d       Y yw xY w# t
        $ r }t        j	                  d|       Y d}~y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 writeNrV   rW   z1photon: could not write project creds to .env: %s)hermes_cli.configru   ImportErrorr+   r,   r   )rY   r[   ru   r.   s       r   rj   rj      sm    4O*,?@.?  VW  OJANNOs#    A  == 	A)	A$$A)c                  J    e Zd ZU ded<   ded<   ded<   ded<   ded<   ded	<   y
)
DeviceCoderI   device_code	user_codeverification_uriOptional[str]verification_uri_completerQ   
expires_inintervalN)r   r   r   __annotations__r   r   r   ry   ry     s#    N,,OMr   ry   T)frozenc                  &    e Zd ZU dZded<   ded<   y)_DeviceTokenCandidatez<A token-like value extracted from the device-token response.rI   sourcerD   N)r   r   r   r   r   r   r   r   r   r     s    FKJr   r   c                 Z    t        j                  d      xs t        j                  d      S )NPHOTON_DASHBOARD_HOST/)r   r]   DEFAULT_DASHBOARD_HOSTrstripr   r   r   _dashboard_hostr     s$    II-.H2HPPQTUUr   c                 Z    t        j                  d      xs t        j                  d      S )NPHOTON_SPECTRUM_HOSTr   )r   r]   DEFAULT_SPECTRUM_HOSTr   r   r   r   _spectrum_hostr   "  s#    II,-F1FNNsSSr   c                    dd|  iS )NAuthorizationBearer r   )rD   s    r   _bearerr   &  s    wug.//r   c                l    t        |  d| j                  d            j                  d      }dd| iS )N:r"   asciir   zBasic )r   encodedecode)rZ   r[   rD   s      r   _basicr   *  s@    An%56==gFGNNwWEveW-..r   c                   	 | j                         }t        |t              rAdD ]"  }|j	                  |      }|st        |      c S  t        j                  |d      d d S t        | dd      xs d}|r|d d S dS # t        $ r d }Y xw xY w)N)errormessagedetailT)r6   i  text zno response body)r'   r   rG   dictrF   rI   dumpsgetattr)respr=   keyvalr   s        r   _response_error_detailr   /  s    yy{ $1 	 C((3-C3x	  zz$$/554$*D4:5#55  s   A= =B
Bc           
     b    t        | dd      }|dk  ry t        d| d| dt        |              )Nstatus_code     zPhoton z failed: HTTP z: )r   RuntimeErrorr   )r   actionstatuss      r   _raise_for_statusr   >  sD    T=#.F|

&xr2H2N1OP r   )	client_idscopec                   t         t        d      t                d}d| i}|r||d<   t        j                  ||d      }|j	                          |j                         }t        |d   |d   |d	   |j                  d
      t        |j                  d      xs t              t        |j                  d      xs t                    S )zBPOST ``/api/auth/device/code`` and return the device + user codes.)httpx is required for Photon device loginz/api/auth/device/coder   r         >@r'   timeoutrz   r{   r|   r~   r   r   )rz   r{   r|   r~   r   r   )httpxr   r   postraise_for_statusr'   ry   rF   rQ   DEFAULT_POLL_TIMEOUTDEFAULT_POLL_INTERVAL)r   r   urlbodyr   r=   s         r   request_device_coder   G  s     }FGG4
5C'3DW::cd3D99;D'{#01"&((+F"Gtxx-E1EFTXXj)B-BC r   )r   r   r   
on_pendingc               v   t         t        d      t                d}t        j                         |xs | j                  xs t
        z   }||n| j                  xs t        }t        j                         |k  rt        j                  |       	 t        j                  |d| j                  |dd      }|j                  d	k(  r_i }
	 |j                         xs i }t!        |t"              r|ni }
t+        |
t-        |d
i             }|st        d      |d   j.                  S |j                  dk(  r|dz  }|rt1        |       |j                  dk(  ri }
	 |j                         xs i }
|
j3                  d      xs |
j3                  d      xs d}|dk(  r|rt1        |       F|dk(  r|dz  }|rt1        |       _|dv rt        d|       t        d|xs |j4                         t        j                  d|j                  |j4                  dd	        t        j                         |k  rt7        d      # t         j                  $ r!}	t        j                  d|	       Y d}	~	d}	~	ww xY w# t$        t&        t        j(                  f$ r i }
Y w xY w# t        j(                  $ r Y Iw xY w)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_typerz   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   rR   r   r   r   r   sleepr   rz   RequestErrorr+   r,   r   r'   rG   r   	TypeError
ValueErrorr*   !_device_response_token_candidatesr   rD   _saferF   r   TimeoutError)coder   r   r   r   r   deadliner   r   r.   r   decoded
candidateserrs                 r   poll_for_tokenr   ^  s   & }FGG5
6Cyy{gPP<PQH ,H4==3YDYE
))+
 

5	::"P#'#3#3!*
 D s"#%D))++",Wd";w ;gdIr:J "J 
 a=&&&s"RKEj!s"Dyy{(b ((7#@txx	':@bC--*%k!
*%88"%:3%#@AA!<S=MDII<NOPP;diio	
s ))+
 z 6
77c !! 	NNA1E	 z4+?+?@ , '' s<   &I 
(I: J! I7I22I7: JJ!J87J8r   c                  g t               d
fd} |d| j                  d              |d| j                  d             | j                  d      }t        |t              r |d|j                  d             | j                  d      }t        |t              r0 |d|j                  d              |d|j                  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.
    c                    t        |      }|r|v ry j                  |       j                  t        | |             y )Nr   rD   )_clean_bearer_tokenaddappendr   )r   valuerD   r   seens      r   r   z._device_response_token_candidates.<locals>.add  s:    #E*/vUKLr   rC   accessTokensessionzsession.access_tokenr=   zdata.access_tokenzdata.accessTokenzset-auth-token)r   rI   r   r   returnNone)setrF   rG   r   _header_value)r   r   r   r   r=   r   r   s        @@r   r   r     s     JDM 01txx./hhy!G'4 "GKK$?@88FD$.!9: 78-1ABCr   c                    t        | t              sy | j                         }|j                         j	                  d      r|dd  j                         }|xs d S )Nzbearer    )rG   rI   striplower
startswith)r   rD   s     r   r   r     sJ    eS!KKME{{}	*ab	!=Dr   c                L   | sy 	 | j                  |      }|rt        |      S 	 	 t        |       j	                         D ]?  \  }}t        |      j                         |j                         k(  s1|s4t        |      c S  	 y # t        $ r Y iw xY w# t        t        f$ r Y y w xY wN)rF   rI   AttributeErrorr   itemsr   r   r   )r   rh   r   r   s       r   r   r     s    D!u: w---/ 	"JC3x~~4::</E5z!	"
    z" s5   B A	B /B 2B ?B 	BBB#"B#c                z    t         t        d      t                |  }t        j                  |dd| id      S )Nr   r   r   r   r   r   )r   r   r   rF   )r   rD   r   s      r   _dashboard_getr     sK    }FGGv
&C99 GE7"34 r   c                z   t        d|       }|j                  dv rt        d      |j                          |j	                         }t        |t              r|j                  d      nd}t        |t              r|st        d      t        d|       }|j                  dv rt        d      |j                          |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'   rG   r   rF   )rD   r   r=   r   projects_resps        r   validate_photon_tokenr     s     159D:%&
 	
 	99;D)$5488F4DdD!&$
 	
 ##3U;M  J.&+
 	
 ""$Kr   c                P   | st        d      d}d}| D ]%  }	 t        |j                         |j                  c S  |.dj                  d | D              xs d}t        | d| d      |||t        d      # t        $ r}|}|}Y d}~yd}~wt        $ r}|}Y d}~d}~ww xY w)	zBReturn the first candidate token that passes dashboard validation.zHPhoton returned 200 but no token candidate in the device-token response.Nz, c              3  4   K   | ]  }|j                     y wr   )r   ).0cs     r   	<genexpr>z-_validated_dashboard_token.<locals>.<genexpr>4  s     9AHH9s   nonez@ Device login returned no project-valid dashboard token (tried: z).z.Photon did not return a usable dashboard token)r   r   rD   r   r   join)r   dashboard_error
last_error	candidateexcsourcess         r   _validated_dashboard_tokenr    s    
 	
 ;?O*.J 
			!)//2??"
 "))9j99CV&  ''.ir3
 	 
G
HH ( 	!OJ 	J	s#    A;;	B%BB%B  B%c                2    	  |         y # t         $ r Y y w xY wr   )r   )fns    r   r   r   >  s    
 s   
 	)r   open_browseron_user_codec                ,   t        |       rt        fd       |r2	 ddl}j                  xs j                  }|j                  |d       t        |       }t        d|      g}t        |      }t        |       |S # t        $ r Y >w xY w)	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>R  s    l4( r   r   Nr4   )newpollr   )r   r   
webbrowserr~   r|   r&   r   r   r   r  rT   )	r   r  r  r  targetfirst_tokenr   rD   r   s	     `     @r   login_device_flowr  E  s     3D()	33Lt7L7LFOOFO* !;K'v[IJJ&z2EuL  		s   1B 	BBc                    t         t        d      t                d}t        j                  |t	        |       d      }|j                          |j                         xs i S )uE   GET ``/api/auth/get-session`` — confirm the token + fetch the user.httpx is required for Photonr   r   r   )r   r   r   rF   r   r   r'   rD   r   r   s      r   get_sessionr  e  sW    }9::4
5C99S'%.$?D99;"r   c                    t        | t              r| S t        | t              rkdD ]f  }| j                  |      }t        |t              r|c S t        |t              s9dD ])  }|j                  |      }t        |t              s%|c c S  h g S )N)r=   projectsuserslinesr   )r  r  r  r   )rG   rH   r   rF   )r=   r   inner
nested_keynesteds        r   _unwrap_listr  r  s    $$B 	&CHHSME%&%&"I &J"YYz2F!&$/%&	& Ir   c                    t         t        d      t                d}t        j                  |t	        |       d      }|j                          t        |j                               S )u7   GET ``/api/projects`` — return the caller's projects.r  /api/projectsr   r   r   r   r   rF   r   r   r  r'   r  s      r   list_projectsr    sV    }9::}
-C99S'%.$?D		$$r   c                    |xs dj                         j                         }t        |       D ]:  }|j                  d      xs dj                         j                         |k(  s8|c S  y)z?Return the first project whose name matches (case-insensitive).r   rh   N)r   r   r  rF   )rD   rh   r  r`   s       r   find_project_by_namer     sc    jb!'')Fe$ HHV"))+113v=K r   zUnited States)rh   locationc               X   t         t        d      t                d}||ddd}t        j                  ||t	        |       d      }|j                          |j                         xs i }|j                  d      rt        d|d          |j                  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).
    z-httpx is required for Photon project creationr  F)rh   r!  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'   rF   )rD   rh   r!  r   r   r   r=   s          r   create_projectr(    s     }JKK}
-C	D ::cgendKD99;"Dxx;DM?KLL88D>NOOKr   c                f   t         t        d      t                d| d}t        j                  |i t	        |       d      }|j                          |j                         xs i }|j                  d      rt        d|d          |j                  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.
    r  r   z/regenerate-secretr   r%  r   z!Photon regenerate-secret failed: projectSecretz2Photon regenerate-secret returned no projectSecret)	r   r   r   r   r   r   r'   rF   rI   )rD   rZ   r   r   r=   secrets         r   regenerate_project_secretr,    s     }9::~j\9K
LC::cGENDID99;"Dxx>tG}oNOOXXo&FOPPv;r   c                8    t        j                  dd| xs d      S )z?Reduce a phone string to ``+`` and digits for dedup comparison.z[^\d+]r   )resub)phones    r   _normalize_phoner1    s    66)R"--r   c                    t         t        d      t                d|  d}t        j                  |t	        | |      d      }t        |d       t        |j                               S )uD   GET Spectrum Cloud ``/projects/{id}/users/`` → ``SpectrumUser[]``.r  
/projects//users/r   r   z
list-users)r   r   r   rF   r   r   r  r'   )rZ   r[   r   r   s       r   
list_usersr5    s]    }9::jG
<C99S&^"DdSDdL)		$$r   c                    t        |      }t        | |      D ]'  }t        |j                  d      xs d      |k(  s%|c S  y)zFReturn an existing Spectrum user with the given phone number, or None.phoneNumberr   N)r1  r5  rF   )rZ   r[   rn   r  r   s        r   find_user_by_phoner8    sI     l+F:~6 DHH]39r:fDK r   F)
first_name	last_nameemailsend_invitec               @   t         t        d      t        j                  |      st	        d|      t                d|  d}d|d}|rt        j                  d       |r||d<   |r||d	<   |r||d
<   t        j                  ||t        | |      d      }	t        |	d       |	j                         xs i }
|
j                  d      rt        d|
d          |
j                  d      xs |
j                  d      xs |
}t        |t              r|S t        d      )zBPOST Spectrum Cloud ``/projects/{id}/users/`` and return the user.z*httpx is required for Photon user creationz4phone_number must be E.164 (e.g. +15551234567); got r3  r4  shared)typer7  z?photon: send_invite is ignored by Spectrum shared-user creation	firstNamelastNamer;  r   r%  zcreate-userr   zPhoton create-user failed: r   r=   z2Photon create-user returned an unexpected response)r   r   E164_REmatchr   r   r+   debugr   r   r   r'   rF   rG   r   )rZ   r[   rn   r9  r:  r;  r<  r   r   r   r=   r   s               r   create_userrE    s-    }GHH==&B<BRS
 	
 jG
<C$,\JDVW&[$ZW::z>2	D dM*99;"Dxx8gHII88F7txx/74D$
K
LLr   )r9  r:  r;  c               R    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).
    F)rn   r9  r:  r;  T)r8  rE  )rZ   r[   rn   r9  r:  r;  existingr   s           r   register_user_if_absentrH    sJ     "*nlKH!D :r   c                H    | sy| j                  d      }|rt        |      S 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)rF   rI   )r   r   s     r   user_assigned_linerK  ,  s*     
(((
)C3s8$$r   c                    t               } | j                  di       j                  d      xs g }t        |t              r|r|d   xs i }t        |t              rs|j                  d      xs |j                  d      }|j                  d      xs |j                  d      }|s|r'|rt        |      n	t               |rt        |      fS dfS t               dfS )	zEReturn ``(operator_phone_number, assigned_phone_number)`` for status.rA   rr   r   rn   r7  ro   rJ  N)r/   rF   rG   rH   r   rI   _configured_operator_phone)rJ   user_entriesra   r0  assigneds        r   load_user_numbersrP  ;  s    <D88-r266}EKL,%,Q%2eT"IIn-I=1IE		12 49923  "'CJ-G-I%-CM 37  &'--r   c                l   t               \  }}d}|rt        | ||      }nt        | |      }t        |      dk(  r|d   }d}|}|rZ|j	                  d      }t        t        |j	                  d      xs d            }t        j                  |      r|}t        |      }t               }	|s?t               }
|
r3|	r1	 t        |
|	d      }|r |j	                  d      rt        |d         }	 t!        |||rt        |      nd|	
       ||fS # t        $ r }t        j                  d	|       Y d}~Dd}~ww xY w)zFRefresh cached user numbers from Photon without provisioning anything.N   r   r&  r7  r   Fcreate_if_missingz6photon: could not refresh iMessage line for status: %srm   )rP  r8  r5  lenrF   r1  rI   rB  rC  rK  rg   rM   get_imessage_liner   r+   rD  rs   )rZ   r[   r0  cached_assignedr   r  rp   rO  dashboard_phonedashboard_iddashboard_tokenliner.   s                r   refresh_user_numbersr\  O  s8    /0E?%)D!*neD:~6u:?8DG-H((4.*3txx/F/L"+MN==)#E%d+,.L+-|8(# &+ DHH]3"4#67H& 'GT)	 (?  La s   :D
 
	D3D..D3c                 B   t        d      } | r"t        |       }t        j                  |      r|S t        d      }|syg }t	        j
                  d|      D ]4  }t        |      }t        j                  |      s$|j                  |       6 t        |      dk(  r|d   S y)zDInfer the operator's E.164 number from existing Photon env settings.PHOTON_HOME_CHANNELPHOTON_ALLOWED_USERSNz[,\s]+rR  r   )_get_config_env_valuer1  rB  rC  r.  splitr   rU  )home
normalizedallowedr   parts        r   rM  rM    s     !67D%d+
==$#$:;GJG, *%d+
==$j)* :!!}r   c                h    	 ddl m}  ||       S # t        $ r t        j                  |       cY S w xY w)Nr   )get_env_value)rv   rg  r   r   r]   )r   rg  s     r   r`  r`    s6    3   yy~s    11c                    t         t        d      t                d| d}t        j                  |t	        |       d      }|j                          t        |j                               S )uO   GET ``/api/projects/{id}/lines`` → ``[{id, platform, phoneNumber, status}]``.r  r   /linesr   r   r  )rD   rZ   r   r   s       r   
list_linesrj    s\    }9::~j\
@C99S'%.$?D		$$r   imessageplatformc               B   t         t        d      t                d| d}t        j                  |d|it	        |       d      }|j                          |j                         xs i }|j                  d      rt        d|d          |j                  d	      xs |S )
z:POST ``/api/projects/{id}/lines`` to provision a new line.r  r   ri  rm  r   r%  r   zPhoton add-line failed: r[  r'  )rD   rZ   rm  r   r   r=   s         r   add_linero    s     }9::~j\
@C:::x('%.$D 	99;"Dxx5d7m_EFF88F#t#r   rS  c                   t        | |      D ],  }|j                  d      xs dj                         dk(  s*|c S  |r	 t        | |d      S y# t        $ r }t
        j                  d|       Y d}~yd}~ww xY w)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.
    rm  r   rk  rl  z2photon: could not auto-provision iMessage line: %sN)rj  rF   r   ro  r   r+   r,   )rD   rZ   rT  r[  r.   s        r   rV  rV    s     5*- HHZ &B--/:=K 	E:
CC   	NNOQRS	s   A 	A7A22A7c           	     "   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j                  |             y)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   ✗ missingrZ   project_key3   ✗ missing (run `hermes photon setup --phone ...`)rn   ro   zPhoton iMessage statusuB   ──────────────────────z  device token        : z  project id          : z  project secret      : z  my number           : z  assigned number     : 
N)rM   rc   rP  r   )emitlabelsrb   secr0  rO  rowss          r   print_credential_summaryr}    s      F)+6 > ()HC #&3=F<,/L]F=')OE8Q > "K "#
 	!L"VN%;;"VL%99"VM%::"VN%;;"V,C%DDD 	4r   c                 l    dd} dd}dd}dd}dd} |         |        |        |        |       dS )zEReturn a fully pre-formatted credential status dict (no raw secrets).c                     t               rdS dS )Nrr  rs  )rM   r   r   r   _present_tokenz*credential_summary.<locals>._present_token  s    -/L	
:	
r   c                 (    t               \  } }| xs dS )Nru  rc   )rb   _secs     r   _present_project_idz/credential_summary.<locals>._present_project_id   s    ,.	T#m#r   c                 (    t               \  } }|rdS dS )Nrr  ru  r  )_sidr{  s     r   _present_secretz+credential_summary.<locals>._present_secret  s    ,.	c"|55r   c                 (    t               \  } }| xs dS )Nrw  rP  )r0  	_assigneds     r   _present_phonez*credential_summary.<locals>._present_phone  s    ,.yMMMr   c                 (    t               \  } }|xs dS )Nrs  r  )_phonerO  s     r   _present_assigned_phonez3credential_summary.<locals>._present_assigned_phone  s    ,.DDDr   )rt  rZ   rv  rn   ro   r   rI   r   )r  r  r  r  r  s        r   credential_summaryr    sG    
$6NE
 '()+&(&(!8!: r   )r   r   )r   Dict[str, Any])r=   r  r   r   )r   r}   )rD   rI   r   r   )r   #Tuple[Optional[str], Optional[str]])
rY   rI   r[   rI   rf   r}   rh   r}   r   r   )
rn   r}   ro   r}   rp   r}   rf   r}   r   r   )rY   rI   r[   rI   r   r   r  )rD   rI   r   Dict[str, str])rZ   rI   r[   rI   r   r  )r   r   r   rI   )r   r   r   rI   r   r   )r   rI   r   r}   r   ry   )r   ry   r   rI   r   Optional[int]r   r  r   zOptional[Callable[[], None]]r   rI   )r   r  r   Optional[Any]r   rH   )r   r   r   r}   )r   r  rh   rI   r   r}   )r   rI   rD   rI   r   r   )rD   rI   r   r  )r   rH   r   rI   )r  zCallable[[], None]r   r   )r   rI   r  boolr  z(Optional[Callable[['DeviceCode'], None]]r   rI   )r=   r   r   List[Dict[str, Any]])rD   rI   r   r  )rD   rI   rh   rI   r   Optional[Dict[str, Any]])rD   rI   rh   rI   r!  rI   r   r  )rD   rI   rZ   rI   r   rI   )r0  rI   r   rI   )rZ   rI   r[   rI   r   r  )rZ   rI   r[   rI   rn   rI   r   r  )rZ   rI   r[   rI   rn   rI   r9  r}   r:  r}   r;  r}   r<  r  r   r  )rZ   rI   r[   rI   rn   rI   r9  r}   r:  r}   r;  r}   r   zTuple[Dict[str, Any], bool])r   r  r   r}   )rZ   rI   r[   rI   r   r  )r   rI   r   r}   )rD   rI   rZ   rI   r   r  )rD   rI   rZ   rI   rm  rI   r   r  )rD   rI   rZ   rI   rT  r  r   r  )ry  r   r   r   )r   r  )Wr   
__future__r   r'   loggingr   r.  rR   base64r   dataclassesr   pathlibr   typingr   r   r	   r
   r   r   r   rw   	getLoggerr   r+   r   r   DEFAULT_CLIENT_IDDEFAULT_SCOPEr   r   DEFAULT_PROJECT_NAMEr   r   compilerB  r   r/   r?   rM   rT   rc   rg   rl   rs   rj   ry   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r  r   r  r  r  r  r   r(  r,  r1  r5  r8  rE  rH  rK  rP  r\  rM  r`  rj  ro  rV  printr}  r  r   r   r   <module>r     sz  #H #   	 	   !  = = 
		8	$P| P ! &3 7  &    
"**)
*C	
,8 +/>> > (	>
 > 
>B #'+/!*. ) 	
 ( 
0O,    $  VT0/
6 *-2?4 '!"/3U8
U8 U8 	U8
 U8 -U8 	U8v " 
    
	 F$@I> '=A	  ;	
 	@ % %#	  	
 <..
%%(8;  !%#(M(M(M 	(M
 (M (M (M (M (M` !%# 	
    !:%.(..%(.(.b*% 5?$$$.1$$$ ?C7;. */ #LK  Es    H HH