
    $jP                        U d Z ddlmZ ddlZddlZddlZddlmZmZm	Z	m
Z
 dZ ej                    Zi Zded<   daded	<   d
Zd
Z G d de          ZddZddZddZddZddZddZdS ) u  WS-upgrade auth credentials for gated mode.

Browsers cannot set ``Authorization`` on a WebSocket upgrade. In loopback
mode the legacy ``?token=<_SESSION_TOKEN>`` query param works because the
token is injected into the SPA bundle. In gated mode there is no injected
token — so this module provides two credential shapes:

1. **Single-use browser tickets** (``mint_ticket`` / ``consume_ticket``).
   The SPA gets a fresh ticket via the authenticated REST endpoint
   ``POST /api/auth/ws-ticket`` and passes it as ``?ticket=`` on the WS
   upgrade. Single-use, TTL = 30 seconds — a leaked ticket is uninteresting.

2. **A process-lifetime internal credential** (``internal_ws_credential`` /
   ``consume_internal_credential``). This authenticates *server-spawned*
   WS clients — specifically the embedded-TUI PTY child, which attaches to
   ``/api/ws`` (JSON-RPC gateway) and ``/api/pub`` (event sidecar) over
   loopback. A single-use 30s ticket is the wrong shape for that link: the
   child reads its attach URL once at startup and **reuses it on every
   reconnect**, and on a slow cold boot the child may not dial within 30s.
   The internal credential is minted once per process, never expires, is
   multi-use, and — critically — is **never injected into any HTML/SPA**:
   it only ever leaves the process via the spawned child's environment, so
   browser-side XSS cannot read it. A leaked internal credential grants no
   more than a single-use ticket already does (the same two internal WS
   endpoints), and the same Origin / host guards still apply downstream.

In-memory; the dashboard is a single process so no distributed coordination
is needed. The module exposes a small functional API rather than a class so
tests can patch ``time.time`` cleanly.
    )annotationsN)AnyDictOptionalTuple   z%Dict[str, Tuple[int, Dict[str, Any]]]_ticketszOptional[str]_internal_credentialzserver-internalc                      e Zd ZdZdS )TicketInvalidz-Ticket missing, expired, or already consumed.N)__name__
__module____qualname____doc__     C/usr/local/lib/hermes-agent/hermes_cli/dashboard_auth/ws_tickets.pyr   r   :   s        7777r   r   user_idstrproviderreturnc                4   t          j        d          }| |t          t          j                              d}t          5  t          t          j                              t
          z   |ft          |<   t                       ddd           n# 1 swxY w Y   |S )a  Generate a one-shot ticket bound to this user identity.

    The returned token is base64url, 43 bytes of entropy (32-byte random
    seed). Stash returns the ``info`` dict to the caller on consume so the
    WS handler can carry the identity forward into its session log.
        )r   r   	minted_atN)secretstoken_urlsafeinttime_lockTTL_SECONDSr	   _gc_expired_locked)r   r   ticketinfos       r   mint_ticketr$   >   s     "2&&F%% D
 
  	,,{:DA               Ms    ABBBr"   Dict[str, Any]c                <   t          t          j                              }t          5  t                              | d          }|#| r| dd         dz   nd}t          d|           |\  }}||k     rt          d          |cddd           S # 1 swxY w Y   dS )u  Validate and consume. Raises :class:`TicketInvalid` on missing/expired/used.

    Single-use semantics: a successful consume immediately removes the
    ticket from the store, so a second call with the same value raises
    ``TicketInvalid("unknown ticket: …")``.
    N   u   …z<empty>zunknown ticket: expired)r   r   r   r	   popr   )r"   nowentry	truncated
expires_atr#   s         r   consume_ticketr.   Q   s     dikk

C	 
 
VT**= 17Ee++II >9 > >??? 
D	***
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
s   ABBBNonec                     t          t          j                              fdt                                          D             } | D ]}t                              |d           dS )z1Drop expired tickets. Caller must hold ``_lock``.c                ,    g | ]\  }\  }}|k     |S r   r   ).0texp_r*   s       r   
<listcomp>z&_gc_expired_locked.<locals>.<listcomp>i   s&    BBB[Qac		q			r   N)r   r   r	   itemsr)   )r(   r3   r*   s     @r   r!   r!   f   si    
dikk

CBBBBHNN$4$4BBBG  Q r   c                     t           5  t          t          j        d          at          cddd           S # 1 swxY w Y   dS )uU  Return the process-lifetime internal WS credential, minting it once.

    Used by the server to authenticate WS clients it spawns itself (the
    embedded-TUI PTY child). The value is stable for the life of the process,
    multi-use, and never expires — so a server-spawned child can reconnect
    its ``/api/ws`` / ``/api/pub`` sockets indefinitely without re-minting.

    The credential is never injected into the SPA HTML or returned over any
    REST endpoint; it is only ever passed to a child process via its
    environment. See the module docstring for the threat-model rationale.
    Nr   )r   r
   r   r   r   r   r   internal_ws_credentialr9   n   s     
 $ $'#*#8#<#< #$ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $s   "7;;valuec                "   t           5  t          }ddd           n# 1 swxY w Y   | r|t          d          t          j        |                                 |                                          st          d          t          t          dS )u  Validate an internal credential. Raises :class:`TicketInvalid` on mismatch.

    Unlike :func:`consume_ticket` this is **not** single-use — the value is
    not removed on success, so a server-spawned child can present it on every
    (re)connect. Returns the fixed server-internal identity ``info`` dict
    (``{user_id, provider}``), mirroring the ``info`` shape ``consume_ticket``
    returns, so a caller that wants to record the connecting identity can; the
    current ``_ws_auth_ok`` caller validates for the boolean outcome only and
    discards the dict.

    A constant-time compare against the (lazily-minted) credential avoids
    leaking length / prefix information on mismatch. If no internal
    credential has been minted yet, any value is rejected.
    Nzno internal credentialzinternal credential mismatch)r   r   )r   r
   r   r   compare_digestencodeINTERNAL_USER_IDINTERNAL_PROVIDER)r:   expecteds     r   consume_internal_credentialrA      s     
 ( ('( ( ( ( ( ( ( ( ( ( ( ( ( ( ( 6H$4555!%,,..(//2C2CDD <:;;;#%  s     c                 |    t           5  t                                           daddd           dS # 1 swxY w Y   dS )z8Test-only: drop all tickets and the internal credential.N)r   r	   clearr
   r   r   r   _reset_for_testsrD      s     
 $ $#$ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $s   155)r   r   r   r   r   r   )r"   r   r   r%   )r   r/   )r   r   )r:   r   r   r%   )r   
__future__r   r   	threadingr   typingr   r   r   r   r    Lockr   r	   __annotations__r
   r>   r?   	Exceptionr   r$   r.   r!   r9   rA   rD   r   r   r   <module>rK      s_    > # " " " " "       - - - - - - - - - - - -
 	24 4 4 4 4
 '+  * * * * % % 8 8 8 8 8I 8 8 8   &   *   $ $ $ $&   6$ $ $ $ $ $r   