+
    jHàj\{  ã                   ó@  a € R: t?0 t R t^ RIt^ RIt^ RIt^ RIt^ RIt^ RIt^ RIt^ RI	t	^ RI
Ht ^ RIHt ^ RIHtHt ^ RIHtHtHt ^ RIHtHt ]P0                  ! ]4      tRt^tRtR	tRt^t ^t!R;t"Rt#R
 R lt$R R lt%R R lt&R R lt'R R lt(R R lt)R R lt*R R lt+R R lt,R R lt-R R lt.R  R! lt/R" t0R# R$ lt1R% R& lt2R' R( lt3R) R* lt4R+ R, lt5R- R. lt6R/RR0R/R1 R2 llt7/ t8] ^ k ]Pr                  ! 4       t:R3 R4 lt;R5 R6 lt<R7 t= ! R8 R94      t>R# )<aè  DM pairing: code-based approval of new users on messaging platforms.

Unknown users receive a one-time pairing code that the bot owner approves via the
CLI, instead of static user-ID allowlists. Security properties (OWASP + NIST SP
800-63-4): 8-char codes from a 32-char unambiguous alphabet via ``secrets``, 1-hour
expiry, max 3 pending per platform, 1 request per user per 10 min, lockout after 5
failed approvals, chmod 0600 data files, codes never logged. Storage: ~/.hermes/pairing/
N©ÚPath)ÚOptional)Úexpand_whatsapp_aliasesÚnormalize_whatsapp_identifier)Úget_default_hermes_rootÚget_hermes_dirÚget_hermes_home)Úatomic_json_writeÚfile_signatureÚ ABCDEFGHJKLMNPQRSTUVWXYZ23456789i  iX  c                ó$   € V ^8„  d   QhR\         /# ©é   Úreturnr   )Úformats   "Ú4/home/ubuntu/.hermes/hermes-agent/gateway/pairing.pyÚ__annotate__r   :   s   € ÷ fñ fœdñ fó    c                  ó6   € \         e   \         # \        RR4      # )Núplatforms/pairingÚpairing)ÚPAIRING_DIRr   © r   r   Ú_default_pairing_dirr   :   s   € Ü%Ò1Œ;Ðe´~ÐFYÐ[dÓ7eÐer   c                óF   € V ^8„  d   QhR\         R\        \         ,          /# ©r   Úplatformr   ©Ústrr   )r   s   "r   r   r   >   s   € ÷ ñ ¬#ð ´(¼3µ-ñ r   c           	     óÈ  € T ;'       g    RP                  4       P                  4       p \        P                  ! \        4      ;_uu_ 4        ^ RIHp V! 4        RRR4       \        P                  ! \        4      ;_uu_ 4        ^ RIHp ^ RI	H
p V! V RVP                  V 4      4      ;'       g    RuuRRR4       #   + '       g   i     Ll; i  + '       g   i     R# ; i)aç  Allowlist env var name for ``platform`` (its ``PlatformEntry.allowed_users_env``), or None.

Approving a code also writes the user into an already-configured allowlist (revoke removes
them) so the operator's list stays the visible source of truth. Platforms without one (or with
no allowlist configured) keep the pairing store as the sole grant record (authz union). See
#23778. Discovers plugins first: ``hermes pairing approve`` runs in a CLI process that may not
have loaded them yet.Ú )Údiscover_pluginsN)Úauthz_field)Úplatform_registryÚallowed_users_env)ÚlowerÚstripÚ
contextlibÚsuppressÚ	ExceptionÚhermes_cli.pluginsr"   Úgateway.platform_envr#   Úgateway.platform_registryr$   Úget)r   r"   r#   r$   s   &   r   Ú_allowlist_env_for_platformr/   >   s›   € ð —�˜B×%Ñ%Ó'×-Ñ-Ó/€HÜ	×	Ò	œY×	'Õ	'Ý7ÙÔ÷ 
(ô 
×	Ò	œY×	'Õ	'Ý4Ý?Ù˜8Ð%8Ð:K×:OÑ:OÐPXÓ:YÓZ×bÐbÐ^b÷ 
(Ò	'÷ 
(×	'ú÷ 
(Ö	'ñ ús   ÁB=Â/CÂ=C	ÃC!	c                ó0   € V ^8„  d   QhR\         R\        /# )r   Úrawr   ©r   Úlist)r   s   "r   r   r   Q   s   € ÷ Bñ Bœ#ð B¤$ñ Br   c                 ó˜   € V P                  R 4       Uu. uF*  qP                  4       '       g   K  VP                  4       NK,  	  up# u upi )Ú,)Úsplitr'   )r1   Úuids   & r   Ú_split_allowlistr8   Q   s0   € Ø#&§9¡9¨S¤>ÓA¡>˜C·Y±Y·[ŒKˆC�I‰IŽK¡>ÑAÐAùÒAs
   ”A°Ac                ó0   € V ^8„  d   QhR\         R\        /# r   ©r   Úbool)r   s   "r   r   r   U   s   € ÷ Nñ N¬sð N´tñ Nr   c                óX   € T ;'       g    RP                  4       P                  4       R9   # )uK   True for Baileys WhatsApp and Meta Cloud â€” same phone/JID identity rules.r!   >   ÚwhatsappÚwhatsapp_cloud)r'   r&   )r   s   &r   Ú _platform_uses_whatsapp_identityr?   U   s&   € à�NˆN˜×!Ñ!Ó#×)Ñ)Ó+Ð/MÑMÐMr   c                ó<   € V ^8„  d   QhR\         R\         R\         /# ©r   r   Úuser_idr   ©r   )r   s   "r   r   r   Z   s&   € ÷ Fñ F¤ð F¬sð F´sñ Fr   c                ó˜   € \        T;'       g    R4      P                  4       p\        V 4      '       d   \        V4      ;'       g    T# T# )zHNormalize platform-specific user IDs before persisting / comparing them.r!   )r   r'   r?   r   )r   rB   Úraw_user_ids   && r   Ú_normalize_user_idrF   Z   sJ   € ä�g—m�m Ó$×*Ñ*Ó,€KÜJjÐks×JtÒJtÔ)¨+Ó6×EÐE¸+ð  Fð  {Fð  Fr   c                óR   € V ^8„  d   QhR\         R\         R\        \         ,          /# rA   )r   Úset)r   s   "r   r   r   `   s%   € ÷ 	ñ 	œsð 	¬Sð 	´S¼µXñ 	r   c                ó  € \        T;'       g    R4      P                  4       pV'       g   \        4       # V\        W4      0p\	        V 4      '       d   VP                  \        V4      4       VP                  R4       V# )z<All known equivalent user IDs for auth / allowlist matching.r!   )r   r'   rH   rF   r?   Úupdater   Údiscard)r   rB   rE   Úaliasess   &&  r   Ú_user_id_aliasesrM   `   sc   € ä�g—m�m Ó$×*Ñ*Ó,€KßÜ‹uˆØÔ.¨xÓEÐF€GÜ'¨×1Ò1Ø�‰Ô.¨{Ó;Ô<Ø‡O�O�BÔØ€Nr   c                óH   € V ^8„  d   QhR\         R\         R\         R\        /# )r   r   ÚleftÚrightr   r:   )r   s   "r   r   r   l   s.   € ÷ Sñ Sœcð S¬ð S´Sð S¼Tñ Sr   c                ób   € \        W4      p\        T;'       d    V\        W4      ,          4      # )z4True when two user IDs represent the same principal.)rM   r;   )r   rO   rP   Úleft_aliasess   &&& r   Ú_user_ids_matchrS   l   s*   € ä# HÓ3€LÜ�×QÐQ Ô0@ÀÓ0QÕ!QÓRÐRr   c                óH   € V ^8„  d   QhR\         R\        R\         R\        /# )r   r   ÚapprovedrB   r   )r   Údictr3   )r   s   "r   r   r   r   s.   € ÷ Pñ PœCð P¬4ð P¼#ð PÄ$ñ Pr   c                 óX   € V Uu. uF  p\        WV4      '       g   K  VNK  	  up# u upi ©N)rS   )r   rU   rB   r7   s   &&& r   Ú_matching_idsrY   r   s%   € Ù#ÓO™8�C¤°xÀg×'N�CˆC™8ÑOÐOùÒOs   …'ž'c                ó0   € V ^8„  d   QhR\         R\         /# )r   Úenv_varr   rC   )r   s   "r   r   r   v   s   € ÷ :ñ :¤ð :¬ñ :r   c                óP   € ^ RI Hp V! V R4      ;'       g    RP                  4       # )a  Read a platform allowlist env var through the profile secret scope.

Under multiplexing the process env may hold ANOTHER profile's allowlist, so a
scoped miss must return empty rather than borrow it. The shared reader owns the
contract: a bound-scope failure propagates (never silently borrows the env),
while the unscoped default-profile path keeps the legacy ``os.getenv`` read.
Writes (``save_env_value``/``remove_env_value``) target the active profile's
``.env`` / installed scope, not ``os.environ``.

See #88441.
)Úget_scoped_secretr!   )Úgateway.platforms._sharedr]   r'   )r[   r]   s   & r   Ú_read_allowlist_envr_   v   s%   € õ <á˜g rÓ*×0Ð0¨b×7Ñ7Ó9Ð9r   c                ó$   € V ^8„  d   QhR\         /# )r   r   rC   )r   s   "r   r   r   ‡   s   € ÷ Eñ E¤Cñ Er   c                óp   € \        V 4      pV'       d   \        V4      MRpV'       d   V\        V4      3# R# )zå``(env_var, ids)`` for a platform whose allowlist is configured, else None.

An unconfigured allowlist means an open gateway: the pairing store stays the
sole grant record and we must never lock the gateway by materializing one.
r!   N)r/   r_   r8   )r   r[   Úcurrents   &  r   Ú_configured_allowlistrc   ‡   s5   € ô *¨(Ó3€Gß.5Ô! 'Ô*¸2€Gß3:ˆGÔ% gÓ.Ð/ÐDÀÐDr   c                ó4   € V ^8„  d   QhR\         R\        RR/# )r   r[   Úidsr   Nr2   )r   s   "r   r   r   ’   s&   € ÷ Uñ U¤#ð U¬Dð U°Tñ Ur   c                óä   € \         P                  ! \        4      ;_uu_ 4        ^ RIHpHp V'       d   V! V RP                  V4      4      MV! V 4       RRR4       R#   + '       g   i     R# ; i)ziBest-effort persist (empty list removes the key); the pairing store grant still authorizes via the union.)Úsave_env_valueÚremove_env_valuer5   N)r(   r)   r*   Úhermes_cli.configrg   rh   Újoin)r[   re   rg   rh   s   &&  r   Ú_write_allowlist_envrk   ’   sB   € ä	×	Ò	œY×	'Õ	'ßFß25‰�w §¡¨£Ô.Ñ;KÈGÓ;Tø÷ 
(×	'×	'Ò	'ús   £1AÁA/	c                ó4   € V ^8„  d   QhR\         R\         RR/# ©r   r   rB   r   NrC   )r   s   "r   r   r   ™   s!   € ÷ 8ñ 8¤#ð 8´ð 8¸ñ 8r   c                ó”   € \        V 4      pVf   R# Vw  r4RV9   g   \        V4      V9   d   R# \        V. VO\        V4      N4       R# )zGAdd ``user_id`` to the platform allowlist env var IF one is configured.NÚ*)rc   r   rk   )r   rB   Ú
configuredr[   re   s   &&   r   Ú_sync_allowlist_addrq   ™   sJ   € ä& xÓ0€JØÒÙØ�L€GØ
ˆc„z”S˜“\ SÔ(ÙÜ˜Ð"6 CÐ"6¬¨W«Ð"6Ö7r   c               #  ó–  "  € Rp \         P                  ! \        4      ;_uu_ 4        ^ RIHp V! 4       p RRR4       V f   R# \        V RR4      ;'       g    / .\        V RR4      ;'       g    / P                  4       OpV F.  pT;'       g    / P                  4        F  pVf   K	  Vx € K  	  K0  	  R#   + '       g   i     L‰; i5i)zDYield adapters from the in-process GatewayRunner, if one is running.N)Ú_gateway_runner_refÚadaptersÚ_profile_adapters)r(   r)   r*   Úgateway.runrs   ÚgetattrÚvalues)Úrunnerrs   ÚmappingsÚmappingÚadapters        r   Ú_iter_live_gateway_adaptersr}   ¤   s¦   é € à€FÜ	×	Ò	œY×	'Õ	'Ý3Ù$Ó&ˆ÷ 
(ð ‚~ÙÜ˜ 
¨DÓ1×7Ð7°RÐv¼7À6ÐK^Ð`dÓ;e×;kÐ;kÐik×:sÑ:sÓ:uÐv€HÛˆØŸ˜ 2×-Ñ-Ö/ˆGØÔ"Ø”ó 0ó ÷ 
(×	'üs3   ‚%C	§B6µ!C	ÁC	Á.C	ÂC	Â(C	Â6C	ÃC	c                ó$   € V ^8„  d   QhR\         /# r   rC   )r   s   "r   r   r   ³   s   € ÷ Nñ N¤sñ Nr   c                 ó¾   € \        \        V R R4      RR4      p\        T;'       g    \        V RR4      ;'       g    R4      P                  4       P                  4       # )r   NÚvalueÚnamer!   )rw   r   r'   r&   )r|   r€   s   & r   Ú_adapter_platform_namer‚   ³   sO   € Ü”G˜G Z°Ó6¸ÀÓF€EÜˆu×<Ð<œ ¨°Ó6×<Ð<¸"Ó=×CÑCÓE×KÑKÓMÐMr   c                ó0   € V ^8„  d   QhR\         R\         /# )r   r   rB   rC   )r   s   "r   r   r   ¸   s   € ÷ 
ñ 
´ð 
¼cñ 
r   c                óR  aa€ R VV3R llp\        V \        4      '       d%   RP                  \        V\	        V 4      4      4      # \        V \
        \        34      '       d   \        \        W04      4      # \        V \        \        34      '       d   \        \        W04      4      # V # )z?Drop alias-equivalent allowlist entries while preserving ``*``.c                ó$   € V ^8„  d   QhR\         /# r   ©r;   )r   s   "r   r   Ú._purge_allowlist_entries.<locals>.__annotate__º   s   € ÷ dñ d”tñ dr   c                 ó”   <€ \        V 4      P                  4       R 8H  ;'       g%    \        S\        V 4      \        S4      4      '       * # )ro   )r   r'   rS   )Úentryr   rB   s   &€€r   ÚkeepÚ&_purge_allowlist_entries.<locals>.keepº   s:   ø€ Ü�5‹z×ÑÓ! SÑ(×cÐc´ÀÌ#ÈeË*ÔVYÐZaÓVbÓ0cÔ,cÐcr   r5   )	Ú
isinstancer   rj   Úfilterr8   rH   Ú	frozensetr3   Útuple)Úentriesr   rB   rŠ   s   &ff r   Ú_purge_allowlist_entriesr‘   ¸   s€   ù€ ÷dñ dä�'œ3×ÒØ�x‰xœ˜tÔ%5°gÓ%>Ó?Ó@Ð@Ü�'œC¤Ð+×,Ò,Ü”6˜$Ó(Ó)Ð)Ü�'œD¤%˜=×)Ò)Ü”F˜4Ó)Ó*Ð*Ø€Nr   c                ó4   € V ^8„  d   QhR\         R\         RR/# rm   rC   )r   s   "r   r   r   Å   s&   € ÷ fñ f´#ð fÄð fÈñ fr   c           	     óŽ  € T ;'       g    RP                  4       P                  4       pV'       d*   \        T;'       g    R4      P                  4       '       g   R# \        4        EF1  p\	        V4      V8w  d   K  R FŒ  p\        W44      '       g   K  \        P                  ! \        4      ;_uu_ 4        \        W44      p\        WRV4      p\        V\        4      '       d   VP                  V4       M\        W4V4       RRR4       KŽ  	  \        \        VRR4      RR4      p\        V\        4      '       g   KØ  R FS  pW‡9   g   K  \        P                  ! \        4      ;_uu_ 4        \        VP!                  V4      W!4      Wx&   RRR4       KU  	  EK4  	  R#   + '       g   i     EK2  ; i  + '       g   i     K„  ; i)z‘Clear revoked principals from in-process adapter allowlist snapshots,
so intake and adapter-owned controls do not keep authorizing until restart.r!   NÚconfigÚextra)Ú_allow_fromÚ_allowed_user_ids)Ú
allow_fromÚallowed_users)r'   r&   r   r}   r‚   Úhasattrr(   r)   r*   rw   r‘   rŒ   rH   Úintersection_updateÚsetattrrV   r.   )	r   rB   Úplatform_namer|   Úattrrb   Úpurgedr•   Úkeys	   &&       r   Ú#_sync_live_adapter_allowlist_remover¡   Å   sB  € ð —^�^ ×*Ñ*Ó,×2Ñ2Ó4€Mß¤ G§M M¨rÓ 2× 8Ñ 8× :Ò :ÙÜ.×0ˆÜ! 'Ó*¨mÔ;ÙÛ8ˆDÜ�w×%Ô%Ü×(Ò(¬×3Õ3Ü% gÓ4�GÜ5°gÈgÓV�FÜ! '¬3×/Ò/à×3Ñ3°FÕ;ä ¨vÔ6÷ 4Ñ3ñ 9ô œ ¨°4Ó8¸'À4ÓHˆÜ�eœT×"Ô"Û6�Ø–<Ü#×,Ò,¬Y×7Õ7Ü%=¸e¿i¹iÈ»nÈmÓ%e˜™
÷ 8Ñ7ô 7ó 1÷
 4×3Ñ3ú÷ 8×7Ð7ús   Â6AF	Å-F3	ÆF0Æ3Gc                ó4   € V ^8„  d   QhR\         R\         RR/# rm   rC   )r   s   "r   r   r   à   s!   € ÷ ;ñ ;¤Sð ;´3ð ;¸4ñ ;r   c                óª   € \        V 4      pVf   R# Vw  r4\        W@V4      p\        V4      \        V4      8X  d   R# \        W54       \	        W4       R# )a<  Remove ``user_id`` (and WhatsApp alias equivalents) from the allowlist.

Approve mirrors a normalized phone while revoke is often given a JID/device
form, so matching uses alias rules -- exact delete would leave the sender authorized.
An unconfigured allowlist is left alone (config-only snapshots are not touched).
N)rc   r‘   Úlenrk   r¡   )r   rB   rp   r[   re   Ú	remainings   &&    r   Ú_sync_allowlist_remover¦   à   sM   € ô ' xÓ0€JØÒÙØ�L€GÜ(¨¸Ó@€IÜ
ˆ9ƒ~œ˜S›Ô!ÙÜ˜Ô,Ü'¨Ö:r   c                ó0   € V ^8„  d   QhR\         R\        /# )r   Úpathr   ©r   rV   )r   s   "r   r   r   ò   s   € ÷ !ñ !œ$ð !¤4ñ !r   c                óF  € V P                  4       '       g   / #  \        P                  ! V P                  RR7      4      p\	        V\
        4      '       d   V# / #   \         dž   p T P                  4       pRTP                   R\        TP                  4      R
R  2pM  \         d    Rp Mi ; i\        \        R4      '       d   \        P                  ! 4       MRp\        P!                  R	YYB4       / u Rp?# Rp?i\        P"                  \        3 d    / u # i ; i)a  Read a JSON object; {} when missing, malformed, unreadable, or not a dict.

PermissionError is logged loudly: a 0600 file owned by another uid (Docker:
``docker exec`` as root wrote it, the gosu-dropped gateway can't read it)
would otherwise silently leave the user unauthorized.
z	utf-8-sig)Úencodingz
owner_uid=z mode=Nz<stat failed>Úgeteuidzn/aa)  Pairing file %s exists but is not readable as uid=%s (%s; %s). If you ran `docker exec <container> hermes pairing approve ...` as root, re-run with `docker exec -u hermes <container> ...` and chown the existing file to the hermes user, or restart the container so the entrypoint can fix ownership.éüÿÿÿ)ÚexistsÚjsonÚloadsÚ	read_textrŒ   rV   ÚPermissionErrorÚstatÚst_uidÚoctÚst_modeÚOSErrorrš   Úosr¬   ÚloggerÚwarningÚJSONDecodeError)r¨   ÚdataÚeÚstÚ
owner_infoÚeuids   &     r   Ú_load_json_filerÁ   ò   sõ   € ð �;‰;�=Š=Øˆ	ðÜ�zŠz˜$Ÿ.™.°+˜.Ó>Ó?ˆÜ! $¬×-Ò-ˆtÐ5°2Ð5øÜô ð	)ð
 —‘“ˆBØ% b§i¡i [°´s¸2¿:¹:³ÀrÀsÐ7KÐ6LÐM‰JøÜô 	)Ø(ŠJð	)úä&¤r¨9×5Ò5Œr�zŠzŒ|¸5ˆÜ�‰ð=ð
 ˜
ô	
ð �	ûÜ× Ñ ¤'Ð*ô ØŠ	ðúsN   š=A ÁA ÁD Á&8BÂC=ÂB/Â,C=Â.B/Â/AC=Ã7D Ã=D ÄD ÄD c                ó4   € V ^8„  d   QhR\         R\        RR/# )r   r¨   r¼   r   Nr©   )r   s   "r   r   r     s!   € ÷ .ñ .œ$ð .¤dð .¨tñ .r   c                 ó    € \        WR R7       R# )i€  )ÚmodeN)r
   )r¨   r¼   s   &&r   Ú_save_json_filerÅ     s   € Ü�d u×-r   ÚhomeÚactivec                ó`   € V ^8„  d   QhR\         \        ,          R\         \        ,          RR/# )r   rÆ   rÇ   r   N)r   r   )r   s   "r   r   r     s+   € ÷ 7ñ 7¬´$­ð 7ÌÔQUÍð 7Ðbfñ 7r   c                ó�  € T ;'       g    \        4       p V R,          pVe   TM	\        4       pVP                  4       VP                  4       8X  d   V R,          R,          MTpVP                  4       '       d$   VP                  4       VP                  4       8X  d   R# VP	                  RRR7       VP                  R4       F€  pVP                  4       '       d   \        V4      M/ pV'       g   K0  \        WP                  ,          4      pVP                  V4       WV8w  g   Kd  \        WP                  ,          V4       K‚  	  R# )zøMerge split legacy (``pairing``) / new (``platforms/pairing``) data into the active dir.

If both exist, approved users in the inactive location must not be silently
ignored (they would be asked for a fresh code). Active data wins on key conflict.
r   NÚ	platformsT©ÚparentsÚexist_okz*.json)r	   r   Úresolver®   ÚmkdirÚglobÚis_filerÁ   r�   rJ   rÅ   )rÆ   rÇ   Úold_dirÚ	alternateÚsrcÚmergedrb   s   $$     r   Ú_migrate_split_pairing_dirsrÖ     sñ   € ð ×$Ð$”?Ó$€DØ�YÕ€GØÒ)‰VÔ/CÓ/E€FØ28·.±.Ó2BÀgÇoÁoÓFWÔ2W��{Õ" YÖ.Ð]d€IØ×Ñ×Ò §¡Ó!1°Y×5FÑ5FÓ5HÔ!HÙØ
‡L�L˜¨€LÔ-Ø�~‰~˜hÖ'ˆØ),¯©¯ª” Ô%¸BˆßÙÜ! &¯8©8Õ"3Ó4ˆØ�‰�gÔØÖÜ˜F§X¡XÕ-¨vÖ6ó (r   c                óD   € V ^8„  d   QhR\         R\        P                  /# )r   Ú	directoryr   )r   Ú	threadingÚlocal)r   s   "r   r   r   5  s"   € ÷ Oñ O¤ð O¬¯©ñ Or   c                óÀ   € \         ;_uu_ 4        \        P                  \        V 4      \        P
                  ! 4       4      uuRRR4       #   + '       g   i     R# ; i)zWPer-directory reentrancy holder for ``_file_lock`` (shared by every store on that dir).N)Ú_dir_lock_holders_guardÚ_dir_lock_holdersÚ
setdefaultr   rÙ   rÚ   )rØ   s   &r   Ú_dir_lock_holderrß   5  s2   € ç	 Ö	 Ü ×+Ñ+¬C°	«N¼I¿OºOÓ<MÓN÷ 
!×	 ×	 Ó	 ús   �2AÁA	c                ó$   € V ^8„  d   QhR\         /# r   r†   )r   s   "r   r   r   ;  s   € ÷ Kñ Kœtñ Kr   c                 óV   € \        V \        4      ;'       d    R V 9   ;'       d    RV 9   # )ÚsaltÚhash)rŒ   rV   )r‰   s   &r   Ú_is_hashed_entryrä   ;  s'   € Ü�eœTÓ"×JÐJ v°¡×JÐJ¸6ÀU¹?ÐJr   c                ó”   € \        V \        4      '       d   V P                  R4      MRp\        V\        \        34      '       d   V# R# )zPNumeric ``created_at`` of a pending entry, or None for malformed/legacy entries.Ú
created_atN)rŒ   rV   r.   ÚintÚfloat)Úinforæ   s   & r   Ú_entry_created_atrê   ?  s9   € ä+5°d¼D×+AÒ+A�—‘˜,Ô'Àt€JÜ# J´´e°×=Ò=ˆ:ÐGÀ4ÐGr   c                   óÌ  a € ] tR tRt o RtRDV 3R lR llt]V 3R lR l4       tV 3R lR	 ltV 3R
 lR lt	V 3R lR lt
V 3R lR ltV 3R lR lt]P                  R 4       t]! ]4      t]! ]4      tV 3R lR ltV 3R lR ltV 3R lR ltRDV 3R lR lltREV 3R lR lltV 3R lR lt]V 3R lR  l4       tV 3R! lR" ltREV 3R# lR$ lltV 3R% lR& lt]V 3R' lR( l4       tV 3R) lR* lt RDV 3R+ lR, llt!RDV 3R- lR. llt"V 3R/ lR0 lt#V 3R1 lR2 lt$V 3R3 lR4 lt%V 3R5 lR6 lt&V 3R7 lR8 lt'V 3R9 lR: lt(V 3R; lR< lt)V 3R= lR> lt*V 3R? lR@ lt+V 3RA lRB lt,RCt-V t.R# )FÚPairingStoreiE  ac  Pairing codes and approved user lists.

Files per platform: ``{platform}-pending.json``, ``{platform}-approved.json``, plus
shared ``_rate_limits.json``. With ``profile="<name>"`` storage resolves from that
profile's HERMES_HOME exactly as ``hermes -p <name> pairing ...`` does, so multiplex
gateways and profile-scoped CLI approvals share one whitelist.
Nc                ó0   <€ V ^8„  d   QhRS[ S[,          /# )r   Úprofile©r   r   )r   Ú__classdict__s   "€r   r   ÚPairingStore.__annotate__N  s   ø€ ÷ (ñ (¡©¥ñ (r   c                ó^  € R pV'       d$   \        4       pVR8X  d   TMVR,          V,          pV'       d   \        RRVR7      M	\        4       V n        V P                  P	                  RRR7       \        W P                  R7       \        P                  ! 4       V n        Wn	        / V n
        R # )	NÚdefaultÚprofilesr   r   )rÆ   TrË   )rÆ   rÇ   )r   r   r   Ú_dirrÏ   rÖ   rÙ   ÚRLockÚ_lockÚ_profileÚ_approved_cache)Úselfrî   Úprofile_homeÚroots   &&  r   Ú__init__ÚPairingStore.__init__N  s   € ØˆßÜ*Ó,ˆDØ#*¨iÔ#7™4¸TÀJÕ=NÐQXÕ=XˆLßY`”NÐ#6¸	ÈÕUÔfzÓf|ˆŒ	Ø�	‰	�‰ ¨tˆÔ4ä#¨¿i¹iÕHÜ—_’_Ó&ˆŒ
ØŒØ%'ˆÖr   c                ó0   <€ V ^8„  d   QhRS[ S[,          /# r   rï   )r   rð   s   "€r   r   rñ   \  s   ø€ ÷ ñ ™¡#�ñ r   c                ó   € V P                   # )zCProfile name this store is scoped to, or None for the global store.)rø   ©rú   s   &r   rî   ÚPairingStore.profile[  s   € ð �}‰}Ðr   c                ó&   <€ V ^8„  d   QhRS[ RS[/# r   ©r   r   )r   rð   s   "€r   r   rñ   `  s   ø€ ÷ 6ñ 6¡cð 6©dñ 6r   c                ó.   € V P                   V R 2,          # )z-pending.json©rõ   ©rú   r   s   &&r   Ú_pending_pathÚPairingStore._pending_path`  s   € Ø�y‰y˜h˜Z }Ð5Õ5Ð5r   c                ó&   <€ V ^8„  d   QhRS[ RS[/# r   r  )r   rð   s   "€r   r   rñ   c  s   ø€ ÷ 7ñ 7¡sð 7©tñ 7r   c                ó.   € V P                   V R 2,          # )z-approved.jsonr  r  s   &&r   Ú_approved_pathÚPairingStore._approved_pathc  s   € Ø�y‰y˜h˜Z ~Ð6Õ6Ð6r   c                ó    <€ V ^8„  d   QhRS[ /# r   r   )r   rð   s   "€r   r   rñ   f  s   ø€ ÷ /ñ /¡$ñ /r   c                ó(   € V P                   R ,          # )z_rate_limits.jsonr  r  s   &r   Ú_rate_limit_pathÚPairingStore._rate_limit_pathf  s   € Ø�y‰yÐ.Õ.Ð.r   c                ó    <€ V ^8„  d   QhRS[ /# r   r   )r   rð   s   "€r   r   rñ   i  s   ø€ ÷ ,ñ ,¡Tñ ,r   c                ó(   € V P                   R ,          # )z_declined.jsonr  r  s   &r   Ú_decline_stamp_pathÚ PairingStore._decline_stamp_pathi  s   € Ø�y‰yÐ+Õ+Ð+r   c                ó$   <€ V ^8„  d   QhRS[ RR/# ©r   r   r   NrC   )r   rð   s   "€r   r   rñ   l  s   ø€ ÷ 
(ñ 
(©ð 
(°ñ 
(r   c                óf  € V P                  V4      pV P                  V4      p\        P                  ! 4       pVP                  4        UUu/ uF+  w  rV\	        V4      ;pf   K  WG,
          \
        8:  g   K)  WVbK-  	  ppp\        V4      \        V4      8w  d   V P                  W(4       R# R# u uppi )zdRemove expired pending codes; malformed/legacy entries (no numeric ``created_at``) count as expired.N)r  Ú
_load_jsonÚtimeÚitemsrê   ÚCODE_TTL_SECONDSr¤   Ú
_save_json)	rú   r   r¨   ÚpendingÚnowÚkÚvÚcreatedÚlives	   &&       r   Ú_cleanup_expiredÚPairingStore._cleanup_expiredl  s›   € à×!Ñ! (Ó+ˆØ—/‘/ $Ó'ˆÜ�iŠi‹kˆà$Ÿ]™]œ_ô
Ù,‘T�QÜ,¨QÓ/Ð/�ô ØBEÅ-ÔTdÑAdô ˆAŠD™_ð 	ñ 
ô ˆt‹9œ˜G›Ô$Ø�O‰O˜DÖ'ñ %ùó	
s   ÁB-Á#B-Á6B-c           	   #  óF  "  € ^ RI Hp V! V P                  R,          \        V P                  4      RRV P                   R24      ;_uu_ 4        V P                  ;_uu_ 4        Rx € RRR4       RRR4       R#   + '       g   i     L; i  + '       g   i     R# ; i5i)a³  Serialize load->mutate->save across stores and processes: the dashboard builds a
store per request and the CLI runs in its own process, so ``self._lock`` alone is not enough.
The file lock is taken FIRST: ``is_approved`` runs on the gateway event loop under
``self._lock``, so holding it while waiting (up to 10 s) on another process would stall
every chat on that loop. ``is_approved`` never takes the file lock, so the order is safe.)Ú
_file_lockz.lockg      $@z.Timed out waiting for the pairing store lock (Ú)N)Úhermes_cli.authr'  rõ   rß   r÷   )rú   r'  s   & r   Ú_lockedÚPairingStore._lockedx  su   é € õ 	/ÙØ�I‰I˜ÕÔ!1°$·)±)Ó!<¸dØ<¸T¿Y¹Y¸KÀqÐI÷
õ 
ð �:�:‹:Û÷ ÷
ñ 
÷ �:ú÷
÷ 
ð 
üs6   ‚AB!ÁBÁ#A:	Á(BÁ0
B!Á:B
ÂBÂB	Â	B!c                ó<   <€ V ^8„  d   QhRS[ S[,          RS[RS[/# )r   r   Úsuffixr   )r   r   r3   )r   rð   s   "€r   r   rñ   ‰  s+   ø€ ÷ Gñ G¡8©C¥=ð G¹#ð GÁ$ñ Gr   c                ó:   € V'       d   V.# V P                  V4      # rX   )Ú_all_platforms)rú   r   r-  s   &&&r   Ú
_platformsÚPairingStore._platforms‰  s   € ß%�ˆzÐF¨4×+>Ñ+>¸vÓ+FÐFr   c                ó&   <€ V ^8„  d   QhRS[ RS[/# r   ©r   rV   )r   rð   s   "€r   r   rñ   Ž  s   ø€ ÷ ñ ¡sð ©tñ r   c                ó¶  € V P                  V4      p VP                  R 4      ;_uu_ 4       p\        P                  ! VP	                  4       4      pVP
                  .\        V4      O5pV P                  P                  V4      pVe"   V^ ,          V8X  d   V^,          uuRRR4       # \        P                  ! V4      p\        V\        4      '       d   TM/ pRRR4       XX3T P                  T&   T#   + '       g   i     L#; i  \        \        P                  \        3 d1    T P                  P!                  TR4       T P#                  T4      u # i ; i)ÚrbN)r  Úopenr¸   ÚfstatÚfilenoÚst_devr   rù   r.   r¯   ÚloadrŒ   rV   r·   r»   ÚUnicodeDecodeErrorÚpopr  )rú   r   r¨   Ústreamr¾   r    Úcachedr¼   s   &&      r   Ú_load_approvedÚPairingStore._load_approvedŽ  s  € Ø×"Ñ" 8Ó,ˆð	)ð —‘˜4—” FÜ—X’X˜fŸm™m›oÓ.�Ø—y‘yÐ6¤>°"Ó#5Ñ6�Ø×-Ñ-×1Ñ1°(Ó;�ØÒ%¨&°­)°sÔ*:Ø! !�9÷ !’ô —y’y Ó(�Ü)¨$´×5Ò5‘t¸2�÷ !ð +.¨t¨ˆ×Ñ˜XÑ&Øˆ÷ !—ûô œ×-Ñ-Ô/AÐBô 	)Ø× Ñ ×$Ñ$ X¨tÔ4à—?‘? 4Ó(Ò(ð	)ús<   “D ¬A3C5Â
D Â*0C5ÃD Ã5D	Ä D ÄD ÄAEÅEc                ó,   <€ V ^8„  d   QhRS[ RS[ RS[/# rA   r:   )r   rð   s   "€r   r   rñ   ¢  s'   ø€ ÷ Yñ Y¡Cð Y±#ð Y¹$ñ Yr   c           
     ó°   € V P                   ;_uu_ 4        \        \        WP                  V4      V4      4      uuRRR4       #   + '       g   i     R# ; i)z3Check if a user is approved (paired) on a platform.N)r÷   r;   rY   r?  )rú   r   rB   s   &&&r   Úis_approvedÚPairingStore.is_approved¢  s1   € à�Z�Z‹ZÜœ h×0CÑ0CÀHÓ0MÈwÓWÓX÷ �Z�Z‹Zús   •$AÁA	c                ó4   <€ V ^8„  d   QhRS[ R,          RS[/# ©r   r   Nr   r2   )r   rð   s   "€r   r   rñ   §  s   ø€ ÷ 
ñ 
¡c¨D¥jð 
¹Dñ 
r   c           
     óÖ   € V P                  VR4       UUUu. uFB  pV P                  V P                  V4      4      P                  4        F  w  r4RVRV/VCNK  	  KD  	  uppp# u upppi )z5List approved users, optionally filtered by platform.rU   r   rB   )r0  r  r  r  )rú   r   Úpr7   ré   s   &&   r   Úlist_approvedÚPairingStore.list_approved§  sn   € ð —_‘_ X¨zÔ:õ
á:�Ø!Ÿ_™_¨T×-@Ñ-@ÀÓ-CÓD×JÑJÖL‘	�ð ˜˜I sÐ3¨dÓ3áLñ 4Ù:ó
ð 	
ùô 
s   —AA$c                ó0   <€ V ^8„  d   QhRS[ RS[ RS[ RR/# )r   r   rB   Ú	user_namer   NrC   )r   rð   s   "€r   r   rñ   ¯  s*   ø€ ÷ 	:ñ 	:¡cð 	:±Cð 	:ÁCð 	:ÐQUñ 	:r   c                ó  € V P                  V P                  V4      4      p\        W4      p\        WV4       F  pWF K  	  RVR\        P                  ! 4       /WE&   V P                  V P                  V4      V4       \        W4       R# )zEAdd a user to the approved list. Must be called under self._locked().rL  Úapproved_atN)r  r  rF   rY   r  r  rq   )rú   r   rB   rL  rU   Únormalized_user_idÚapproved_user_ids   &&&&   r   Ú_approve_userÚPairingStore._approve_user¯  sx   € à—?‘? 4×#6Ñ#6°xÓ#@ÓAˆÜ/°ÓBÐÜ -¨hÐBTÖ UÐØÒ*ñ !Và(3°YÀÌtÏyÊyË{Ð'[ˆÑ$Ø�‰˜×+Ñ+¨HÓ5°xÔ@ä˜HÖ9r   c                ó,   <€ V ^8„  d   QhRS[ RS[ RS[/# rA   r:   )r   rð   s   "€r   r   rñ   º  s"   ø€ ÷ ñ ™sð ©Sð ±Tñ r   c                óB  € V P                  V4      pV P                  4       ;_uu_ 4        V P                  V4      p\        WV4      pV'       g    RRR4       R# V F  pWF K  	  V P	                  W44       \        W4        RRR4       R#   + '       g   i     R# ; i)z<Remove a user from the approved list. Returns True if found.NFT)r  r*  r  rY   r  r¦   )rú   r   rB   r¨   rU   Úmatching_idsrP  s   &&&    r   ÚrevokeÚPairingStore.revokeº  sz   € à×"Ñ" 8Ó,ˆØ�\‰\�^�^Ø—‘ tÓ,ˆHÜ(¨¸WÓEˆLßØ÷	 ‰^ó
 %1Ð ØÒ.ñ %1à�O‰O˜DÔ+ä" 8Ô5Ø÷ �^�^Š^ús   ª'BÁ(BÂB	c                ó,   <€ V ^8„  d   QhRS[ RS[RS[ /# )r   Úcoderâ   r   )r   Úbytes)r   rð   s   "€r   r   rñ   Ì  s'   ø€ ÷ Gñ G™ð G¡Eð G©cñ Gr   c                ót   € \         P                  ! WP                  R 4      ,           4      P                  4       # )zutf-8)ÚhashlibÚsha256ÚencodeÚ	hexdigest)rY  râ   s   &&r   Ú
_hash_codeÚPairingStore._hash_codeË  s&   € ä�~Š~˜d§[¡[°Ó%9Õ9Ó:×DÑDÓFÐFr   c          
      ó8   <€ V ^8„  d   QhRS[ RS[RS[ RS[RS[/# )r   r   r  Úmatched_keyÚmatched_entryr   r3  )r   rð   s   "€r   r   rñ   Ï  s2   ø€ ÷ 	ñ 	©ð 	±tð 	É#ð 	Ñ^bð 	Ñgkñ 	r   c                óè   € W# V P                  V P                  V4      V4       V P                  V4       RVR,          RVP                  RR4      /pV P	                  WR,          VR,          4       V# )zHRemove a pending request and approve its user. Must hold self._locked().rB   rL  r!   )r  r  Ú_reset_failed_attemptsr.   rQ  )rú   r   r  rc  rd  Úresults   &&&&& r   Ú_finish_approvalÚPairingStore._finish_approvalÏ  sr   € àÐ Ø�‰˜×*Ñ*¨8Ó4°gÔ>ð 	×#Ñ# HÔ-Ø˜]¨9Õ5°{ÀM×DUÑDUÐVaÐceÓDfÐgˆØ×Ñ˜8¨IÕ%6¸¸{Õ8KÔLØˆr   c          	      óB   <€ V ^8„  d   QhRS[ RS[ RS[ RS[S[ ,          /# )r   r   rB   rL  r   r   )r   rð   s   "€r   r   rñ   Ú  s0   ø€ ÷ ñ ¡cð ±Cð ÁCð ÑQYÑZ]ÕQ^ñ r   c                ó  € V P                  4       ;_uu_ 4        V P                  V4       \        W4      pV P                  V4      '       g   V P	                  W4      '       d    RRR4       R# V P                  V P                  V4      4      p\        V4      \        8¼  d    RRR4       R# RP                  R \        \        4       4       4      p\        P                  ! ^4      pRV P                  Wg4      RVP                  4       RVRVR\         P                   ! 4       /V\"        P$                  ! ^4      &   V P'                  V P                  V4      V4       V P)                  W4       VuuRRR4       #   + '       g   i     R# ; i)	zÒGenerate a pairing code for a new user.

Returns None if the user is rate-limited, the platform hit MAX_PENDING_PER_PLATFORM,
or the platform is locked out. Only a salted SHA-256 hash of the code is persisted.
Nr!   c              3   óV   "  € T F  p\         P                  ! \        4      x € K!  	  R # 5irX   )ÚsecretsÚchoiceÚALPHABET)Ú.0Ú_s   & r   Ú	<genexpr>Ú-PairingStore.generate_code.<locals>.<genexpr>è  s   é € ÐPÑ=O¸œ7Ÿ>š>¬(×3Ð3Ó=Oùs   ‚')rã   râ   rB   rL  ræ   )r*  r$  rF   Ú_is_locked_outÚ_is_rate_limitedr  r  r¤   ÚMAX_PENDING_PER_PLATFORMrj   ÚrangeÚCODE_LENGTHr¸   Úurandomr`  Úhexr  rm  Ú	token_hexr  Ú_record_rate_limit)rú   r   rB   rL  rO  r  rY  râ   s   &&&&    r   Úgenerate_codeÚPairingStore.generate_codeÚ  s.  € ð �\‰\�^�^Ø×!Ñ! (Ô+Ü!3°HÓ!FÐØ×"Ñ" 8×,Ò,°×0EÑ0EÀh×0XÒ0XØ÷	 ‰^ð
 —o‘o d×&8Ñ&8¸Ó&BÓCˆGÜ�7‹|Ô7Ô7Ø÷ ‰^ð —7‘7ÑP¼UÄ;Ô=OÓPÓPˆDÜ—:’:˜b“>ˆDð ˜Ÿ™¨Ó3°V¸T¿X¹X»ZØÐ-¨{¸IÀ|ÔUY×U^ÒU^ÓU`ð-ˆG”G×%Ò% aÓ(Ñ)ð �O‰O˜D×.Ñ.¨xÓ8¸'ÔBØ×#Ñ# HÔ6Ø÷# �^�^‹^ús   ™AE:Á/5E:Â.CE:Å:F	c                ó<   <€ V ^8„  d   QhRS[ RS[ RS[S[,          /# )r   r   rY  r   ©r   r   rV   )r   rð   s   "€r   r   rñ   ó  s&   ø€ ÷ ñ ¡Sð ±ð ¹Á½ñ r   c           	     ó  € V P                  4       ;_uu_ 4        V P                  V4       RP                  \        T;'       g    R4      P	                  4       P                  4       4      pV P                  V4      '       d    RRR4       R# V P                  V P                  V4      4      pVP                  4        F†  w  rE\        V4      '       g   K   \        P                  VR,          4      p\        P                  ! T P!                  Y&4      TR,          4      '       g   Kj  T P#                  YYE4      u uuRRR4       # 	  V P%                  V4        RRR4       R#   \         d     K´  i ; i  + '       g   i     R# ; i)a:  Approve a pairing code and add its user to the approved list.

Returns ``{user_id, user_name}``, or ``None`` if the code is invalid/expired OR the
platform is locked out (disambiguate with ``_is_locked_out``). Constant-time
salted-hash compare; legacy plaintext entries are ignored and pruned at TTL.

See #10195.
r!   Nrâ   rã   )r*  r$  rj   r   Úupperr6   rt  r  r  r  rä   rZ  ÚfromhexÚ
ValueErrorrm  Úcompare_digestr`  rh  Ú_record_failed_attempt)rú   r   rY  r  Úentry_idr‰   râ   s   &&&    r   Úapprove_codeÚPairingStore.approve_codeó  s*  € ð �\‰\�^�^Ø×!Ñ! (Ô+ð —7‘7œ3˜tŸz˜z r›?×0Ñ0Ó2×8Ñ8Ó:Ó;ˆDà×"Ñ" 8×,Ò,Ø÷ ‰^ð —o‘o d×&8Ñ&8¸Ó&BÓCˆGà#*§=¡=¦?‘�Ü'¨×.Ò.ÙðÜ Ÿ=™=¨¨v­Ó7�Dô ×)Ò)¨$¯/©/¸$Ó*EÀuÈVÅ}×UÔUØ×0Ñ0°ÀHÓTÑT÷% Š^ñ $3ð ×'Ñ'¨Ô1Ø÷) ‰^øô "ô Úðú÷ �^ˆ^úsI   ™A(E5ÂE5ÂAE5ÃE#Ã20E5Ä'E5ÅE5Å#E2	Å.E5Å1E2	Å2E5Å5F	c                ó&   <€ V ^8„  d   QhRS[ RS[/# )r   r€   r   r:   )r   rð   s   "€r   r   rñ     s   ø€ ÷ Vñ V¡Sð V©Tñ Vr   c                óÜ   € \        T ;'       g    R4      P                  4       p \        V 4      ^8H  ;'       d5    \        ;QJ d    R V  4       F  '       d   K   R# 	  R# ! R V  4       4      # )z§True when ``value`` is shaped like a ``list_pending`` request id (16 hex chars);
pairing codes are 8 uppercase chars, so callers accepting either can dispatch on this.r!   c              3   ó*   "  € T F	  qR 9   x € K  	  R# 5i)Ú0123456789abcdefABCDEFNr   )rp  Úcs   & r   rr  Ú5PairingStore.looks_like_request_id.<locals>.<genexpr>  s   é € Ð'UÉuÈ!Ð-EÖ(EËuùs   ‚FT)r   r'   r¤   Úall)r€   s   &r   Úlooks_like_request_idÚ"PairingStore.looks_like_request_id  sW   € ô �E—K�K˜RÓ ×&Ñ&Ó(ˆÜ�5‹z˜RÑ×UÐU§C£CÑ'UÉuÓ'U§C¤CÐU¢CÐU CÑ'UÉuÓ'UÓ$UÐUr   c                ó<   <€ V ^8„  d   QhRS[ RS[ RS[S[,          /# )r   r   Ú
request_idr   r€  )r   rð   s   "€r   r   rñ     s&   ø€ ÷ ñ ©ð ¹ð ÁÉ$Åñ r   c           	     óX  € V P                  4       ;_uu_ 4        V P                  V4       \        T;'       g    R4      P                  4       P	                  4       pV'       g    RRR4       R# V P                  V P                  V4      4      pVP                  4        Fj  w  rE\        V4      '       g   K  \        P                  ! \        V4      P	                  4       V4      '       g   KN  V P                  WWE4      u uuRRR4       # 	   RRR4       R#   + '       g   i     R# ; i)a‹  Approve a pending request by its server-side request id (admin surfaces that
must never reveal the DM'd code). Returns ``{user_id, user_name}`` or ``None``.

Neither counts toward nor is gated by the brute-force lockout: a request id is only
obtainable by an authenticated admin, so a stale id is "the row expired", not an
attack -- counting it would let a few GUI clicks lock the operator out.
r!   N)r*  r$  r   r'   r&   r  r  r  rä   rm  r…  rh  )rú   r   r”  r  r‡  r‰   s   &&&   r   Úapprove_requestÚPairingStore.approve_request  sÑ   € ð �\‰\�^�^Ø×!Ñ! (Ô+Ü˜Z×-Ð-¨2Ó.×4Ñ4Ó6×<Ñ<Ó>ˆJßØ÷	 ‰^ð
 —o‘o d×&8Ñ&8¸Ó&BÓCˆGØ#*§=¡=¦?‘�Ü# E×*Ô*¬w×/EÒ/EÄcÈ(Ãm×FYÑFYÓF[Ð]g×/hÔ/hØ×0Ñ0°ÀHÓTÑT÷ Š^ñ $3ð ÷ �^�^Š^ús+   ™A
DÁ$DÁ/ADÂ71DÃ-DÄDÄD)	c                ó4   <€ V ^8„  d   QhRS[ R,          RS[/# rF  r2   )r   rð   s   "€r   r   rñ   ,  s   ø€ ÷ ñ ¡S¨4¥Zð ¹4ñ r   c                óØ  € . pV P                  4       ;_uu_ 4        V P                  VR4       EF  pV P                  V4       V P                  V P	                  V4      4      P                  4        FÔ  w  rE\        V4      pVf   K  \        VP                  R4      \        4      ;'       d     \        VP                  R4      \        4      pTP                  RTRV'       d   \        V4      MRRVP                  RR4      R	VP                  R	R4      R
\        \        P                  ! 4       V,
          ^<,          4      /4       KÖ  	  EK  	  RRR4       V#   + '       g   i     T# ; i)z˜List pending requests (codes are never returned; each exposes a ``request_id``
for :meth:`approve_request`; legacy pre-hash entries report an empty id).r  Nrã   râ   r   r”  r!   rB   rL  Úage_minutes)r*  r0  r$  r  r  r  rê   rŒ   r.   r   Úappendrç   r  )rú   r   ÚresultsrH  r‡  ré   ræ   Ú	is_moderns   &&      r   Úlist_pendingÚPairingStore.list_pending,  s  € ð ˆØ�\‰\�^�^Ø—_‘_ X¨y×9�Ø×%Ñ% aÔ(Ø&*§o¡o°d×6HÑ6HÈÓ6KÓ&L×&RÑ&RÖ&T‘N�HÜ!2°4Ó!8�JØ!Ò)Ù Ü *¨4¯8©8°FÓ+;¼SÓ A× gÐ gÄjÐQU×QYÑQYÐZ`ÓQaÔcfÓFg�IØ—N‘NØ" AØ$·y¤c¨(¤mÀbØ! 4§8¡8¨I°rÓ#:¸KÈÏÉÐR]Ð_aÓIbØ%¤s¬D¯IªI«K¸*Õ,DÈÕ+JÓ'Kð	$ö ô 'Uñ :÷ ð ˆ÷ Ž^ð ˆús   ›BEÂ-5EÃ#A+EÅE)	c                ó4   <€ V ^8„  d   QhRS[ R,          RS[/# rF  )r   rç   )r   rð   s   "€r   r   rñ   @  s   ø€ ÷ ñ ¡c¨D¥jð ¹Cñ r   c                óH  € V P                  4       ;_uu_ 4        ^ pV P                  VR4       FT  pV\        V P                  V P	                  V4      4      4      ,          pV P                  V P	                  V4      / 4       KV  	  RRR4       V#   + '       g   i     X# ; i)z2Clear all pending requests. Returns count removed.r  N)r*  r0  r¤   r  r  r  )rú   r   ÚcountrH  s   &&  r   Úclear_pendingÚPairingStore.clear_pending@  s~   € à�\‰\�^�^ØˆEØ—_‘_ X¨yÖ9�Øœ˜TŸ_™_¨T×-?Ñ-?ÀÓ-BÓCÓDÕD�Ø—‘ × 2Ñ 2°1Ó 5°rÖ:ñ :÷ ð
 ˆ÷ Ž^ð
 ˆús   ™A-BÂB!	c                ó    <€ V ^8„  d   QhRS[ /# r   ©rV   )r   rð   s   "€r   r   rñ   K  s   ø€ ÷ 8ñ 8™ñ 8r   c                ó@   € V P                  V P                  4       4      # rX   )r  r  r  s   &r   Ú_limitsÚPairingStore._limitsK  s   € Ø�‰˜t×4Ñ4Ó6Ó7Ð7r   c                ó$   <€ V ^8„  d   QhRS[ RR/# )r   Úlimitsr   Nr¦  )r   rð   s   "€r   r   rñ   N  s   ø€ ÷ 9ñ 9¡4ð 9¨Dñ 9r   c                óF   € V P                  V P                  4       V4       R # rX   )r  r  )rú   r«  s   &&r   Ú_save_limitsÚPairingStore._save_limitsN  s   € Ø�‰˜×-Ñ-Ó/°Ö8r   c                ó,   <€ V ^8„  d   QhRS[ RS[ RS[/# rA   r:   )r   rð   s   "€r   r   rñ   Q  s"   ø€ ÷ 
ñ 
©ð 
±sð 
¹tñ 
r   c                óÆ   aa€ V P                  4       o\        ;QJ d*    VV3R l\        SV4       4       F  '       g   K   R# 	  R# ! VV3R l\        SV4       4       4      # )zCWhether a user (under any alias) has requested a code too recently.c              3   ó”   <"  € T F=  p\         P                   ! 4       SP                  S R V 2^ 4      ,
          \        8  x € K?  	  R# 5i©Ú:N)r  r.   ÚRATE_LIMIT_SECONDS)rp  Úaliasr«  r   s   & €€r   rr  Ú0PairingStore._is_rate_limited.<locals>.<genexpr>T  s?   øé € ð 
á<�ô �YŠY‹[˜6Ÿ:™:¨¨
°!°E°7Ð&;¸QÓ?Õ?ÔCUÖUÛ<ùs   ƒAATF)r¨  ÚanyrM   )rú   r   rB   r«  s   &f&@r   ru  ÚPairingStore._is_rate_limitedQ  sW   ù€ à—‘“ˆß‹sõ 
ä)¨(°GÔ<ó
�sŒsð 	
Šsð 	
ˆsõ 
ä)¨(°GÔ<ó
ó 
ð 	
r   c                ó*   <€ V ^8„  d   QhRS[ RS[ RR/# rm   rC   )r   rð   s   "€r   r   rñ   Y  s"   ø€ ÷ "ñ "©3ð "¹ð "Àñ "r   c                ó¨   € V P                  4       p\        P                  ! 4       p\        W4       F  pWCV R V 2&   K  	  V P                  V4       R# r²  )r¨  r  rM   r­  )rú   r   rB   r«  r  rµ  s   &&&   r   r|  ÚPairingStore._record_rate_limitY  sH   € Ø—‘“ˆÜ�iŠi‹kˆÜ% hÖ8ˆEØ,/�h�Z˜q  Ð(Ó)ñ 9à×Ñ˜&Ö!r   c                ó,   <€ V ^8„  d   QhRS[ RS[ RS[/# rA   r:   )r   rð   s   "€r   r   rñ   b  s"   ø€ ÷ 
ñ 
©3ð 
¹ð 
Áñ 
r   c                ó  aaaa€ V P                  V P                  4       4      o\        P                  ! 4       o\        ;QJ d,    VVVV3R l\	        SV4       4       F  '       g   K   R# 	  R# ! VVVV3R l\	        SV4       4       4      # )z^Whether this sender (under any alias) was sent a polite decline within DECLINE_DEDUPE_SECONDS.c              3   ó¬   <"  € T FI  p\        SP                  S R V 24      ;o\        \        34      ;'       d    SS,
          \        8  x € KK  	  R# 5ir²  )rŒ   r.   rç   rè   ÚDECLINE_DEDUPE_SECONDS)rp  rµ  r  r   ÚstampedÚstampss   & €€€€r   rr  Ú2PairingStore.has_recent_decline.<locals>.<genexpr>f  sT   øé € ð 
á<�ô  &§*¡*°¨z¸¸5¸'Ð-BÓ"CÐC�wÄcÌ5À\ÓR×ÐÐX[Ð^eÕXeÔiÑWÔÛ<ùs
   ƒ7A»ATF)r  r  r  r·  rM   )rú   r   rB   r  rÀ  rÁ  s   &f&@@@r   Úhas_recent_declineÚPairingStore.has_recent_declineb  sn   û€ à—‘ ×!9Ñ!9Ó!;Ó<ˆÜ�iŠi‹kˆß‹s÷ 
ä)¨(°GÔ<ó
�sŒsð 	
Šsð 	
ˆs÷ 
ä)¨(°GÔ<ó
ó 
ð 	
r   c                ó*   <€ V ^8„  d   QhRS[ RS[ RR/# rm   rC   )r   rð   s   "€r   r   rñ   k  s'   ø€ ÷ @ñ @¡sð @±Sð @¸Tñ @r   c                óþ  € V P                  4       ;_uu_ 4        \        P                  ! 4       pV P                  V P                  4       4      P	                  4        UUu/ uF8  w  rE\        V\        \        34      '       g   K#  W5,
          \        8  g   K6  WEbK:  	  ppp\        W4       F  pW6V RV 2&   K  	  V P                  V P                  4       V4       RRR4       R# u uppi   + '       g   i     R# ; i)z§Stamp the sender (all aliases) and prune expired stamps. Callers stamp BEFORE sending so a
delivery failure cannot become a decline storm on the sender's next message.r³  N)r*  r  r  r  r  rŒ   rç   rè   r¿  rM   r  )rú   r   rB   r  r    r€   rÁ  rµ  s   &&&     r   Úrecord_declineÚPairingStore.record_declinek  sÊ   € ð �\‰\�^�^Ü—)’)“+ˆCà-1¯_©_¸T×=UÑ=UÓ=WÓ-X×-^Ñ-^Ô-`ôÙ-`™z˜sÜ˜e¤c¬5 \×2ô Ø8;½ÔG]Ñ7]ô �’
Ñ-`ð ñ ô *¨(Ö<�Ø03˜(˜ 1 U GÐ,Ó-ñ =à�O‰O˜D×4Ñ4Ó6¸Ô?÷ ‰^ùó÷ �^ˆ^ús*   ™AC+Á C%
ÂC%
ÂC%
Â>C+Ã%C+Ã+C<	c                ó&   <€ V ^8„  d   QhRS[ RS[/# r   r:   )r   rð   s   "€r   r   rñ   x  s   ø€ ÷ Kñ K¡sð K©tñ Kr   c                ót   € \         P                   ! 4       V P                  4       P                  R V 2^ 4      8  # )ú	_lockout:)r  r¨  r.   r  s   &&r   rt  ÚPairingStore._is_locked_outx  s-   € Ü�yŠy‹{˜TŸ\™\›^×/Ñ/°)¸H¸:Ð0FÈÓJÑJÐJr   c                ó$   <€ V ^8„  d   QhRS[ RR/# r  rC   )r   rð   s   "€r   r   rñ   {  s   ø€ ÷ "ñ "©sð "°tñ "r   c           	     ó4  € V P                  4       pRV 2pVP                  V^ 4      ^,           pWBV&   V\        8¼  dJ   \        P                  ! 4       \        ,           VRV 2&   ^ W#&   \        RV R\         R\         R2RR7       V P                  V4       R	# )
zMRecord a failed approval attempt; triggers lockout after MAX_FAILED_ATTEMPTS.ú
_failures:rË  z[pairing] Platform z locked out for zs after z failed attemptsT)ÚflushN)r¨  r.   ÚMAX_FAILED_ATTEMPTSr  ÚLOCKOUT_SECONDSÚprintr­  )rú   r   r«  Úfail_keyÚfailss   &&   r   r†  Ú#PairingStore._record_failed_attempt{  s¡   € à—‘“ˆØ ˜zÐ*ˆØ—
‘
˜8 QÓ'¨!Õ+ˆØ ˆxÑØÔ'Ô'Ü-1¯YªY«[¼?Õ-JˆF�Y˜x˜jÐ)Ñ*Ø ˆFÑÜÐ'¨ zÐ1AÄ/ÐARð SÜ.Ð/Ð/?ðAØHLõNà×Ñ˜&Ö!r   c                ó$   <€ V ^8„  d   QhRS[ RR/# r  rC   )r   rð   s   "€r   r   rñ   ˆ  s   ø€ ÷ &ñ &©sð &°tñ &r   c                óŒ   € V P                  4       pRV 2pVP                  V4      '       d   ^ W#&   V P                  V4       R# R# )zUClear the failed-approval counter after a success (it tracks *consecutive* failures).rÏ  N)r¨  r.   r­  )rú   r   r«  rÔ  s   &&  r   rf  Ú#PairingStore._reset_failed_attemptsˆ  sC   € à—‘“ˆØ ˜zÐ*ˆØ�:‰:�h×ÒØ ˆFÑØ×Ñ˜fÖ%ñ  r   c                ó&   <€ V ^8„  d   QhRS[ RS[/# )r   r-  r   r2   )r   rð   s   "€r   r   rñ   �  s   ø€ ÷ ?ñ ?¡Sð ?©Tñ ?r   c                ó¶   a€ RV R2oV3R lV P                   P                  4        4       pV Uu. uF  q3P                  R4      '       d   K  VNK  	  up# u upi )z[Platforms that have a ``-<suffix>.json`` data file (``_``-prefixed files are shared state).Ú-z.jsonc              3   ó    <"  € T FC  qP                   P                  S4      '       g   K%  VP                   P                  SR 4      x € KE  	  R# 5i)r!   N)r�   ÚendswithÚreplace)rp  ÚfÚtails   & €r   rr  Ú.PairingStore._all_platforms.<locals>.<genexpr>“  s6   øé € ÐdÑ7J°!ÏfÉfÏoÉoÐ^b×NcÔ-�Q—V‘V—^‘^ D¨"×-Ð-Ó7Jùs
   ƒ"Aª$Arq  )rõ   ÚiterdirÚ
startswith)rú   r-  rÊ   rH  rá  s   &&  @r   r/  ÚPairingStore._all_platforms�  sK   ø€ à�6�(˜%Ð ˆÜd°t·y±y×7HÑ7HÔ7JÓdˆ	Ù$Ó>™9�a¯L©L¸×,=—�™9Ñ>Ð>ùÒ>s   °AÁA)rù   rõ   r÷   rø   rX   )r!   )/Ú__name__Ú
__module__Ú__qualname__Ú__firstlineno__Ú__doc__rý   Úpropertyrî   r  r  r  r  r$  r(   Úcontextmanagerr*  ÚstaticmethodrÁ   r  rÅ   r  r0  r?  rC  rI  rQ  rV  r`  rh  r}  rˆ  r‘  r–  rž  r£  r¨  r­  ru  r|  rÃ  rÇ  rt  r†  rf  r/  Ú__static_attributes__Ú__classdictcell__)rð   s   @r   rì   rì   E  sš  ø‡ € ñ÷(ò (ð ÷ó ð÷6ð 6÷7ð 7÷/ð /÷,ð ,÷
(ð 
(ð ×Ññó ðñ ˜oÓ.€JÙ˜oÓ.€J÷Gð G÷
ð ÷(Yð Y÷

ò 
÷	:ò 	:÷ð ð" ÷Gó ðG÷	ð 	÷ò ÷2ð ð> ÷Vó ðV÷ð ÷&ò ÷(ò ÷8ð 8÷9ð 9÷
ð 
÷"ð "÷
ð 
÷@ð @÷Kð K÷"ð "÷&ð &÷?ö ?r   rì   c                ó@   € V ^8„  d   Qh/ ^ \         9   d
   \        ;R&   # )r   rÝ   )Ú__conditional_annotations__rV   )r   s   "r   r   r      s    € ÷ ñ ÷`	 Ò ”4Ñ òa	r   i€Q )@rñ  rê  r(   r\  r¯   Úloggingr¸   rm  rÙ   r  Úpathlibr   Útypingr   Úgateway.whatsapp_identityr   r   Úhermes_constantsr   r   r	   Úutilsr
   r   Ú	getLoggerræ  r¹   ro  rx  r  r´  rÒ  rv  rÑ  r¿  r   r   r/   r8   r?   rF   rM   rS   rY   r_   rc   rk   rq   r}   r‚   r‘   r¡   r¦   rÁ   rÅ   rÖ   rÝ   ÚLockrÜ   rß   rä   rê   rì   r   )rñ  s   @r   Ú<module>rú     s1  øðöó Û Û Û Û 	Û Û Û Ý Ý ç \ß UÑ Uß 3à	×	Ò	˜8Ó	$€ð .€Ø€àÐ ØÐ Ø€ØÐ ØÐ Ø"Ð ð €õfõõ&BõNõ
Fõ	õSõPõ:õ"EõUõ8òõNõ

õfõ6;õ$!õH.ð7¸$ð 7ÐY]÷ 7ð. Ð Ó Ø#Ÿ.š.Ó*Ð õOõKòH÷O?ó O?r   