§
    øžyjÁ�  ã                   óÔ  — d Z ddlZddlZddlZddlZddlZddlZddlZddlZddl	m
Z
 ddlmZ ddlmZmZ ddlmZmZmZ ddlmZ  ej        e¦  «        ZdZd	Zd
ZdZd
ZdZdZ edd¦  «        Z i dd“dd“dd“dd“dd“dd“dd“dd“d d!“d"d#“d$d%“d&d'“d(d)“d*d+“d,d-“d.d/“d0d1“d2d3i¥Z!d4e"d5ee"         fd6„Z#d7e"d5e$fd8„Z%d4e"d5e&fd9„Z'd4e"d:e"d5e"fd;„Z(d4e"d:e"d5e)e"         fd<„Z*d4e"d=e"d>e"d5e&fd?„Z+d@e"d5e"fdA„Z,d4e"d:e"d5dfdB„Z-dC„ Z.d5e"fdD„Z/d4e"d:e"fdE„Z0d4e"d:e"d5dfdF„Z1d4e"d:e"d5dfdG„Z2dHe
d5e3fdI„Z4dJe
dKe
d5dfdL„Z5dddMœdNee
         dOee
         d5dfdP„Z6dHe
dQe"d5dfdR„Z7 G dS„ dT¦  «        Z8dS )Ua±  
DM Pairing System

Code-based approval flow for authorizing new users on messaging platforms.
Instead of static allowlists with user IDs, unknown users receive a one-time
pairing code that the bot owner approves via the CLI.

Security features (based on OWASP + NIST SP 800-63-4 guidance):
  - 8-char codes from 32-char unambiguous alphabet (no 0/O/1/I)
  - Cryptographic randomness via secrets.choice()
  - 1-hour code expiry
  - Max 3 pending codes per platform
  - Rate limiting: 1 request per user per 10 minutes
  - Lockout after 5 failed approval attempts (1 hour)
  - File permissions: chmod 0600 on all data files
  - Codes are never logged to stdout

Storage: ~/.hermes/pairing/
é    N)ÚPath)ÚOptional)Úexpand_whatsapp_aliasesÚnormalize_whatsapp_identifier)Úget_default_hermes_rootÚget_hermes_dirÚget_hermes_home)Úatomic_replaceÚ ABCDEFGHJKLMNPQRSTUVWXYZ23456789é   i  iX  é   é   úplatforms/pairingÚpairingÚtelegramÚTELEGRAM_ALLOWED_USERSÚdiscordÚDISCORD_ALLOWED_USERSÚwhatsappÚWHATSAPP_ALLOWED_USERSÚwhatsapp_cloudÚWHATSAPP_CLOUD_ALLOWED_USERSÚslackÚSLACK_ALLOWED_USERSÚsignalÚSIGNAL_ALLOWED_USERSÚemailÚEMAIL_ALLOWED_USERSÚsmsÚSMS_ALLOWED_USERSÚ
mattermostÚMATTERMOST_ALLOWED_USERSÚmatrixÚMATRIX_ALLOWED_USERSÚdingtalkÚDINGTALK_ALLOWED_USERSÚfeishuÚFEISHU_ALLOWED_USERSÚwecomÚWECOM_ALLOWED_USERSÚwecom_callbackÚWECOM_CALLBACK_ALLOWED_USERSÚweixinÚWEIXIN_ALLOWED_USERSÚbluebubblesÚBLUEBUBBLES_ALLOWED_USERSÚqqbotÚQQ_ALLOWED_USERSÚyuanbaoÚYUANBAO_ALLOWED_USERSÚplatformÚreturnc                 ó  — | pd                      ¦   «                              ¦   «         } t                               | ¦  «        }|r|S 	 ddlm} |                     | ¦  «        }|r|j        r|j        S n# t          $ r Y nw xY wdS )z·Return the per-platform allowlist env var name, or None.

    Falls back to the platform registry for plugin platforms so a plugin's
    own ``allowed_users_env`` is honored too.
    Ú r   )Úplatform_registryN)ÚlowerÚstripÚ_PLATFORM_ALLOWLIST_ENVÚgetÚgateway.platform_registryr9   Úallowed_users_envÚ	Exception)r5   Úenv_varr9   Úentrys       ú5/home/ragecks/.hermes/hermes-agent/gateway/pairing.pyÚ_allowlist_env_for_platformrD   [   s°   € ð �˜B×%Ò%Ñ'Ô'×-Ò-Ñ/Ô/€HÝ%×)Ò)¨(Ñ3Ô3€GØð ØˆðØ?Ð?Ð?Ð?Ð?Ð?à!×%Ò% hÑ/Ô/ˆØð 	+�UÔ,ð 	+ØÔ*Ð*øøÝð ð ð Øˆðøøøàˆ4s   Á*A4 Á4
BÂ BÚrawc                 ó@   — d„ |                       d¦  «        D ¦   «         S )Nc                 ó^   — g | ]*}|                      ¦   «         ¯|                      ¦   «         ‘Œ+S © )r;   )Ú.0Úuids     rC   ú
<listcomp>z$_split_allowlist.<locals>.<listcomp>q   s-   € ÐAÐAÐA˜C°S·Y²Y±[´[ÐAˆC�IŠI‰KŒKÐAÐAÐAó    Ú,)Úsplit)rE   s    rC   Ú_split_allowlistrO   p   s    € ØAÐA 3§9¢9¨S¡>¤>ÐAÑAÔAÐArL   c                 óV   — | pd                      ¦   «                              ¦   «         dv S )uK   True for Baileys WhatsApp and Meta Cloud â€” same phone/JID identity rules.r8   >   r   r   )r;   r:   )r5   s    rC   Ú _platform_uses_whatsapp_identityrQ   t   s*   € àˆN˜×!Ò!Ñ#Ô#×)Ò)Ñ+Ô+Ð/MÐMÐMrL   Úuser_idc                 óŒ   — t          |pd¦  «                             ¦   «         }t          | ¦  «        rt          |¦  «        p|S |S )zHNormalize platform-specific user IDs before persisting / comparing them.r8   )Ústrr;   rQ   r   )r5   rR   Úraw_user_ids      rC   Ú_normalize_user_idrV   y   sI   € å�g�m Ñ$Ô$×*Ò*Ñ,Ô,€KÝ'¨Ñ1Ô1ð IÝ,¨[Ñ9Ô9ÐH¸[ÐHØÐrL   c                 ó  — t          |pd¦  «                             ¦   «         }|st          ¦   «         S |t          | |¦  «        h}t	          | ¦  «        r"|                     t          |¦  «        ¦  «         |                     d¦  «         |S )zCReturn all known equivalent user IDs for auth / allowlist matching.r8   )rT   r;   ÚsetrV   rQ   Úupdater   Údiscard)r5   rR   rU   Úaliasess       rC   Ú_user_id_aliasesr\   �   s‡   € å�g�m Ñ$Ô$×*Ò*Ñ,Ô,€KØð Ý‰uŒuˆàÕ.¨x¸ÑEÔEÐF€GÝ'¨Ñ1Ô1ð =Ø�ŠÕ.¨{Ñ;Ô;Ñ<Ô<Ð<Ø‡O‚O�BÑÔÐØ€NrL   ÚleftÚrightc                 ón   — t          | |¦  «        }t          | |¦  «        }t          |o|o||z  ¦  «        S ©z;Return True when two user IDs represent the same principal.)r\   Úbool)r5   r]   r^   Úleft_aliasesÚright_aliasess        rC   Ú_user_ids_matchrd   Ž   s=   € å# H¨dÑ3Ô3€LÝ$ X¨uÑ5Ô5€MÝ�ÐQ ÐQ°LÀ=Ñ4PÑRÔRÐRrL   rA   c                 óÜ   — 	 ddl m}m} 	  || ¦  «        pd                     ¦   «         S # |$ r Y nw xY wn# t          $ r Y nw xY wt          j        | ¦  «        pd                     ¦   «         S )u×  Read a platform allowlist env var through the profile secret scope.

    Under multiplexing the process env may hold ANOTHER profile's allowlist
    (first-writer-wins YAMLâ†’env bridges), so reads must honor the installed
    scope's verdict â€” including a scoped miss returning empty rather than
    borrowing the process value.  Unscoped callers (single-profile CLI /
    admin endpoints) keep the legacy ``os.getenv`` read.

    TODO(profile-secrets): the grant mirror below still WRITES through
    ``hermes_cli.config.save_env_value`` / ``remove_env_value``, which target
    the root ``.env`` â€” those writes need a profile-aware counterpart before
    pairing grants can be mirrored correctly under multiplexing.
    r   )ÚUnscopedSecretErrorÚ
get_secretr8   )Úagent.secret_scoperf   rg   r;   r@   ÚosÚgetenv)rA   rf   rg   s      rC   Ú_read_allowlist_envrk   •   s©   € ðØFÐFÐFÐFÐFÐFÐFÐFð	Ø�J˜wÑ'Ô'Ð-¨2×4Ò4Ñ6Ô6Ð6øØ"ð 	ð 	ð 	ØˆDð	øøøØøÝð ð ð ØˆðøøøåŒI�gÑÔÐ$ "×+Ò+Ñ-Ô-Ð-s#   ‚6 ‹* ª2¯6 ±2²6 ¶
AÁAc                 óP  — t          | ¦  «        }|sdS t          |¦  «        }|sdS t          |¦  «        }d|v st          |¦  «        |v rdS |                     t          |¦  «        ¦  «         	 ddlm}  ||d                     |¦  «        ¦  «         dS # t          $ r Y dS w xY w)u©  Add ``user_id`` to the platform allowlist env var IF one is configured.

    Option (i): only materialize the grant into the allowlist when the operator
    already runs an allowlist for this platform. On an open gateway (no
    allowlist) we do nothing â€” the pairing store remains the grant record and
    the authz union honors it, so we never silently convert an open gateway into
    a locked one on first pairing.
    NÚ*r   )Úsave_env_valuerM   )	rD   rk   rO   rT   ÚappendÚhermes_cli.configrn   Újoinr@   )r5   rR   rA   ÚcurrentÚidsrn   s         rC   Ú_sync_allowlist_addrt   ¯   sØ   € õ *¨(Ñ3Ô3€GØð ØˆÝ! 'Ñ*Ô*€GØð ØˆÝ
˜7Ñ
#Ô
#€CØ
ˆc€z€z•S˜‘\”\ SÐ(Ð(ØˆØ‡J‚J�s�7‰|Œ|ÑÔÐðØ4Ð4Ð4Ð4Ð4Ð4àˆ�w §¢¨¡¤Ñ.Ô.Ð.Ð.Ð.øÝð ð ð ð 	ˆˆðøøøs   Á0%B Â
B%Â$B%c               #   óJ  K  — 	 ddl m}   | ¦   «         }n# t          $ r Y dS w xY w|€dS t          |dd¦  «        pi }|                     ¦   «         D ]}|�|V — Œ	t          |dd¦  «        pi }|                     ¦   «         D ]!}|pi                      ¦   «         D ]}|�|V — Œ	Œ"dS )zDYield adapters from the in-process GatewayRunner, if one is running.r   )Ú_gateway_runner_refNÚadaptersÚ_profile_adapters)Úgateway.runrv   r@   ÚgetattrÚvalues)rv   Úrunnerrw   ÚadapterÚprofile_adaptersÚmappings         rC   Ú_iter_live_gateway_adaptersr€   Ì   s	  è è € ðØ3Ð3Ð3Ð3Ð3Ð3à$Ð$Ñ&Ô&ˆˆøÝð ð ð Øˆˆðøøøà€~ØˆÝ�v˜z¨4Ñ0Ô0Ð6°B€HØ—?’?Ñ$Ô$ð ð ˆØÐØˆMˆMˆMøÝ˜vÐ':¸DÑAÔAÐGÀRÐØ#×*Ò*Ñ,Ô,ð ð ˆØ˜ 2×-Ò-Ñ/Ô/ð 	ð 	ˆGØÐ"Ø���øð	ðð s   „ •
#¢#c                 ó@  — t          | dd ¦  «        }|�Ft          |dd ¦  «        }|r3t          |¦  «                             ¦   «                              ¦   «         S t          | dd ¦  «        }t          |pd¦  «                             ¦   «                              ¦   «         S )Nr5   ÚvalueÚnamer8   )rz   rT   r;   r:   )r}   r5   r‚   rƒ   s       rC   Ú_adapter_platform_namer„   á   s�   € Ý�w 
¨DÑ1Ô1€HØÐÝ˜ '¨4Ñ0Ô0ˆØð 	.Ý�u‘:”:×#Ò#Ñ%Ô%×+Ò+Ñ-Ô-Ð-Ý�7˜F DÑ)Ô)€DÝˆtˆz�r‰?Œ?× Ò Ñ"Ô"×(Ò(Ñ*Ô*Ð*rL   c                 óN  ‡‡— | €| S t          | t          ¦  «        r3t          | ¦  «        }ˆˆfd„|D ¦   «         }d                     |¦  «        S t          | t          t
          f¦  «        rˆˆfd„| D ¦   «         S t          | t          t          f¦  «        rˆˆfd„| D ¦   «         S | S )z?Drop alias-equivalent allowlist entries while preserving ``*``.Nc           	      ó\   •— g | ](}|d k    st          ‰|t          ‰¦  «        ¦  «        °&|‘Œ)S ©rm   ©rd   rT   )rI   Úpartr5   rR   s     €€rC   rK   z,_purge_allowlist_entries.<locals>.<listcomp>ñ   s@   ø€ ð 
ð 
ð 
ØØ�sŠ{ˆ{¥/°(¸DÅ#ÀgÁ,Ä,Ñ"OÔ"Oˆ{ð Øˆ{ˆ{rL   rM   c           	      ó´   •— h | ]T}t          |¦  «                             ¦   «         d k    s+t          ‰t          |¦  «        t          ‰¦  «        ¦  «        °R|’ŒUS r‡   ©rT   r;   rd   ©rI   rB   r5   rR   s     €€rC   ú	<setcomp>z+_purge_allowlist_entries.<locals>.<setcomp>÷   ó`   ø€ ð 
ð 
ð 
ØÝ�5‰zŒz×ÒÑ!Ô! SÒ(Ð(Ý" 8­S°©Z¬Z½¸W¹¼ÑFÔFð )ð Ø(Ð(Ð(rL   c           	      ó´   •— g | ]T}t          |¦  «                             ¦   «         d k    s+t          ‰t          |¦  «        t          ‰¦  «        ¦  «        °R|‘ŒUS r‡   r‹   rŒ   s     €€rC   rK   z,_purge_allowlist_entries.<locals>.<listcomp>ý   rŽ   rL   )Ú
isinstancerT   rO   rq   rX   Ú	frozensetÚlistÚtuple)Úentriesr5   rR   ÚpartsÚ	remainings    ``  rC   Ú_purge_allowlist_entriesr—   ë   s  øø€ à€ØˆÝ�'�3ÑÔð #Ý  Ñ)Ô)ˆð
ð 
ð 
ð 
ð 
Ø"ð
ñ 
ô 
ˆ	ð �xŠx˜	Ñ"Ô"Ð"Ý�'�C¥Ð+Ñ,Ô,ð 
ð
ð 
ð 
ð 
ð 
Ø&ð
ñ 
ô 
ð 	
õ
 �'�D¥%˜=Ñ)Ô)ð 
ð
ð 
ð 
ð 
ð 
Ø&ð
ñ 
ô 
ð 	
ð
 €NrL   c                 ój  — | pd                      ¦   «                              ¦   «         }|r#t          |pd¦  «                              ¦   «         sdS t          ¦   «         D ]Ô}t	          |¦  «        |k    rŒt          |d¦  «        r<	 t          t          |j        pd¦  «        ||¦  «        |_        n# t          $ r Y nw xY wt          t          |dd¦  «        dd¦  «        }t          |t          ¦  «        r=d|v r9	 t          |                     d¦  «        ||¦  «        |d<   ŒÄ# t          $ r Y ŒÐw xY wŒÕdS )a  Clear revoked principals from in-process adapter allowlist snapshots.

    ``WhatsAppAdapter`` (and Cloud) snapshot ``_allow_from`` at construction.
    Pairing revoke updates ``WHATSAPP_ALLOWED_USERS`` / cloud env, but when the
    revoked principal was the sole entry the env key is removed entirely.
    Intake must not keep authorizing from the stale snapshot until restart.
    r8   NÚ_allow_fromrH   ÚconfigÚextraÚ
allow_from)r;   r:   rT   r€   r„   Úhasattrr—   rX   r™   r@   rz   r�   Údictr=   )r5   rR   Úplatform_namer}   r›   s        rC   Ú#_sync_live_adapter_allowlist_remover      sv  € ð �^ ×*Ò*Ñ,Ô,×2Ò2Ñ4Ô4€MØð ¥ G M¨rÑ 2Ô 2× 8Ò 8Ñ :Ô :ð ØˆÝ.Ñ0Ô0ð ð ˆÝ! 'Ñ*Ô*¨mÒ;Ð;ØÝ�7˜MÑ*Ô*ð 	ðÝ&>Ý˜Ô+Ð1¨rÑ2Ô2°MÀ7ñ'ô '�Ô#Ð#øõ ð ð ð Ø�ðøøøå� ¨°4Ñ8Ô8¸'À4ÑHÔHˆÝ�e�TÑ"Ô"ð 	 |°uÐ'<Ð'<ðÝ&>Ø—I’I˜lÑ+Ô+¨]¸Gñ'ô '��lÑ#Ð#øõ ð ð ð Ø�ðøøøøð!ð s$   Â*B0Â0
B=Â<B=Ã:'D"Ä"
D/Ä.D/c                 ó„  ‡ ‡— t          ‰ ¦  «        }|sdS t          |¦  «        }|sdS t          |¦  «        }ˆ ˆfd„|D ¦   «         }t          |¦  «        t          |¦  «        k    rdS 	 ddlm}m} |r  ||d                     |¦  «        ¦  «         n ||¦  «         n# t          $ r Y nw xY wt          ‰ ‰¦  «         dS )a  Remove ``user_id`` (and WhatsApp alias equivalents) from the allowlist.

    Matching must mirror PairingStore / authz WhatsApp alias rules: approve
    mirrors a normalized phone into ``WHATSAPP_ALLOWED_USERS``, while revoke
    is often invoked with a JID or device-suffix form. Exact-string delete
    would leave the allowlist entry and keep the sender authorized.

    Also clears matching entries from any in-process platform adapter
    ``_allow_from`` snapshot so sole-entry revocation is effective without a
    gateway restart.
    Nc           	      ó\   •— g | ](}|d k    st          ‰|t          ‰¦  «        ¦  «        °&|‘Œ)S r‡   rˆ   )rI   Úir5   rR   s     €€rC   rK   z*_sync_allowlist_remove.<locals>.<listcomp>8  s@   ø€ ð ð ð ØØ�Š8ˆ8�?¨8°Q½¸G¹¼ÑEÔEˆ8ð 	
Øˆ8ˆ8rL   r   )rn   Úremove_env_valuerM   )
rD   rk   rO   Úlenrp   rn   r¤   rq   r@   r    )r5   rR   rA   rr   rs   r–   rn   r¤   s   ``      rC   Ú_sync_allowlist_remover¦   $  s   øø€ õ *¨(Ñ3Ô3€GØð ØˆÝ! 'Ñ*Ô*€GØð ØˆÝ
˜7Ñ
#Ô
#€Cðð ð ð ð Øðñ ô €Iõ ˆ9�~„~�˜S™œÒ!Ð!ØˆðØFÐFÐFÐFÐFÐFÐFÐFàð 	&ØˆN˜7 C§H¢H¨YÑ$7Ô$7Ñ8Ô8Ð8Ð8àÐ˜WÑ%Ô%Ð%øøÝð ð ð Øˆðøøøå'¨°'Ñ:Ô:Ð:Ð:Ð:s   Á*5B  Â 
B-Â,B-Úpathc                 óî   — |                       ¦   «         r`	 t          j        |                      d¬¦  «        ¦  «        }t	          |t
          ¦  «        r|ni S # t          j        t          f$ r i cY S w xY wi S )Núutf-8©Úencoding)ÚexistsÚjsonÚloadsÚ	read_textr�   rž   ÚJSONDecodeErrorÚOSError)r§   Údatas     rC   Ú_load_json_filer³   J  s~   € Ø‡{‚{�}„}ð ð	Ý”:˜dŸnšn°g˜nÑ>Ô>Ñ?Ô?ˆDÝ% d­DÑ1Ô1Ð9�4�4°rÐ9øÝÔ$¥gÐ.ð 	ð 	ð 	ØˆIˆIˆIð	øøøà€Is   –A A ÁA2Á1A2Ú
active_dirÚalternate_dirc           	      ó  — |                      ¦   «         r*|                      ¦   «         |                     ¦   «         k    rdS |                      dd¬¦  «         |                     d¦  «        D ]‘}|                     ¦   «         sŒ| |j        z  }t          |¦  «        }|sŒ3t          |¦  «        }t          |¦  «        }|                     |¦  «         ||k    r%t          |t          j        |dd¬¦  «        ¦  «         Œ’dS )a€  Merge split legacy/new pairing data into the active PairingStore dir.

    Older installs use ``{HERMES_HOME}/pairing`` while newer code/docs may
    write ``{HERMES_HOME}/platforms/pairing``. If both directories exist, the
    gateway must not silently ignore approved users sitting in the inactive
    location; otherwise already-paired Feishu users get asked for a fresh code.
    NT©ÚparentsÚexist_okz*.jsoné   F©ÚindentÚensure_ascii)r¬   ÚresolveÚmkdirÚglobÚis_filerƒ   r³   rž   rY   Ú_secure_writer­   Údumps)r´   rµ   ÚsrcÚdestÚmergedrr   Úbefores          rC   Ú_merge_pairing_dirrÈ   T  s  € ð ×ÒÑ!Ô!ð  Z×%7Ò%7Ñ%9Ô%9¸]×=RÒ=RÑ=TÔ=TÒ%TÐ%TØˆØ×Ò˜T¨DÐÑ1Ô1Ð1Ø×!Ò! (Ñ+Ô+ð Rð RˆØ�{Š{‰}Œ}ð 	ØØ˜CœHÑ$ˆÝ  Ñ%Ô%ˆØð 	ØÝ! $Ñ'Ô'ˆÝ�g‘”ˆà�Š�gÑÔÐØ�VÒÐÝ˜$¥¤
¨6¸!È%Ð PÑ PÔ PÑQÔQÐQøðRð RrL   ©ÚhomeÚactiverÊ   rË   c                 óÎ   — | pt          ¦   «         } | dz  }| dz  dz  }|pt          }|                     ¦   «         |                     ¦   «         k    r|n|}t          ||¦  «         d S )Nr   Ú	platforms)r	   ÚPAIRING_DIRr¾   rÈ   )rÊ   rË   Úold_dirÚnew_dirÚ	alternates        rC   Ú_migrate_split_pairing_dirsrÒ   n  st   € ð
 Ð$•?Ñ$Ô$€DØ�YÑ€GØ�[Ñ  9Ñ,€GØÐ"•{€FØ!Ÿ>š>Ñ+Ô+¨w¯ªÑ/@Ô/@Ò@Ð@��Àg€IÝ�v˜yÑ)Ô)Ð)Ð)Ð)rL   r²   c                 ój  — | j                              dd¬¦  «         t          j        t	          | j         ¦  «        d¬¦  «        \  }}	 t          j        |dd¬¦  «        5 }|                     |¦  «         |                     ¦   «          t          j	        | 
                    ¦   «         ¦  «         ddd¦  «         n# 1 swxY w Y   t          || ¦  «         	 t          j        | d	¦  «         dS # t          $ r Y dS w xY w# t          $ r( 	 t          j        |¦  «         n# t          $ r Y nw xY w‚ w xY w)
uÔ   Write data to file with restrictive permissions (owner read/write only).

    Uses a temp-file + atomic rename so readers always see either the old
    complete file or the new one â€” never a partial write.
    Tr·   z.tmp)ÚdirÚsuffixÚwr©   rª   Ni€  )Úparentr¿   ÚtempfileÚmkstemprT   ri   ÚfdopenÚwriteÚflushÚfsyncÚfilenor
   Úchmodr±   ÚBaseExceptionÚunlink)r§   r²   ÚfdÚtmp_pathÚfs        rC   rÂ   rÂ   {  s‹  € ð 	„K×Ò˜d¨TÐÑ2Ô2Ð2ÝÔ#­¨D¬KÑ(8Ô(8ÀÐHÑHÔH�L€BˆðÝŒY�r˜3¨Ð1Ñ1Ô1ð 	!°QØ�GŠG�D‰MŒMˆMØ�GŠG‰IŒIˆIÝŒH�Q—X’X‘Z”ZÑ Ô Ð ð	!ð 	!ð 	!ñ 	!ô 	!ð 	!ð 	!ð 	!ð 	!ð 	!ð 	!øøøð 	!ð 	!ð 	!ð 	!õ 	�x Ñ&Ô&Ð&ð	ÝŒH�T˜5Ñ!Ô!Ð!Ð!Ð!øÝð 	ð 	ð 	ØˆDˆDð	øøøøåð ð ð ð	ÝŒI�hÑÔÐÐøÝð 	ð 	ð 	ØˆDð	øøøàðøøøss   Á	D  Á AB<Â0D  Â<C Ã D  ÃC ÃD  ÃC/ Ã/
C=Ã9D  Ã<C=Ã=D  Ä 
D2ÄD ÄD2Ä 
D-Ä*D2Ä,D-Ä-D2c            
       ó¨  — e Zd ZdZd2dee         fd„Zedee         fd„¦   «         Zdede	fd„Z
dede	fd	„Zde	fd
„Zde	defd„Zde	deddfd„Zdededefd„Zdededee         fd„Zdedededefd„Zdededefd„Zd2dedefd„Zd3dedededdfd„Zdededefd„Zedededefd„¦   «         Zdededed edef
d!„Z	 d3dedededee         fd"„Zdededee         fd#„Zed$edefd%„¦   «         Z ded&edee         fd'„Z!d2dedefd(„Z"d2dede#fd)„Z$dededefd*„Z%dededdfd+„Z&dedefd,„Z'deddfd-„Z(deddfd.„Z)deddfd/„Z*d0edefd1„Z+dS )4ÚPairingStorea{  
    Manages pairing codes and approved user lists.

    Data files per platform:
      - {platform}-pending.json   : pending pairing requests
      - {platform}-approved.json  : approved (paired) users
      - _rate_limits.json         : rate limit tracking

    When constructed with ``profile="<name>"``, storage resolves from that
    profile's own HERMES_HOME using the same legacy/consolidated layout rules
    as ``hermes -p <name> pairing ...``. This keeps multiplex gateways and
    profile-scoped CLI approvals on one whitelist. Without a profile, storage
    is the global pairing directory for the current HERMES_HOME.
    NÚprofilec                 óR  — |r6t          ¦   «         }|dk    r|n|dz  |z  }t          dd|¬¦  «        | _        nt          | _        | j                             dd¬¦  «         |rt          || j        ¬¦  «         nt          ¦   «          t          j        ¦   «         | _        || _	        d S )	NÚdefaultÚprofilesr   r   )rÊ   Tr·   rÉ   )
r   r   Ú_dirrÎ   r¿   rÒ   Ú	threadingÚRLockÚ_lockÚ_profile)Úselfrç   ÚrootÚprofile_homes       rC   Ú__init__zPairingStore.__init__¥  sÍ   € ð ð 	$Ý*Ñ,Ô,ˆDð ˜iÒ'Ð'ð �à˜JÑ&¨Ñ0ð õ
 'Ø#ØØ!ðñ ô ˆDŒIˆIõ $ˆDŒIØŒ	�Š ¨tˆÑ4Ô4Ð4Øð 	*õ (¨\À$Ä)ÐLÑLÔLÐLÐLõ (Ñ)Ô)Ð)õ ”_Ñ&Ô&ˆŒ
ØˆŒˆˆrL   r6   c                 ó   — | j         S )zCProfile name this store is scoped to, or None for the global store.)rï   ©rð   s    rC   rç   zPairingStore.profileÅ  s   € ð Œ}ÐrL   r5   c                 ó   — | j         |› d�z  S )Nz-pending.json©rë   ©rð   r5   s     rC   Ú_pending_pathzPairingStore._pending_pathÊ  s   € ØŒy˜hÐ5Ð5Ð5Ñ5Ð5rL   c                 ó   — | j         |› d�z  S )Nz-approved.jsonr÷   rø   s     rC   Ú_approved_pathzPairingStore._approved_pathÍ  s   € ØŒy˜hÐ6Ð6Ð6Ñ6Ð6rL   c                 ó   — | j         dz  S )Nz_rate_limits.jsonr÷   rõ   s    rC   Ú_rate_limit_pathzPairingStore._rate_limit_pathÐ  s   € ØŒyÐ.Ñ.Ð.rL   r§   c                 ó  — |                      ¦   «         rò	 t          j        |                     d¬¦  «        ¦  «        S # t          $ r£}	 |                     ¦   «         }d|j        › dt          |j        ¦  «        dd …         › �}n# t          $ r d}Y nw xY wt          t          d¦  «        rt          j        ¦   «         nd}t                               d	||||¦  «         i cY d }~S d }~wt          j        t          f$ r i cY S w xY wi S )
Nr©   rª   z
owner_uid=z mode=éüÿÿÿz<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.)r¬   r­   r®   r¯   ÚPermissionErrorÚstatÚst_uidÚoctÚst_moder±   r�   ri   r   ÚloggerÚwarningr°   )rð   r§   ÚeÚstÚ
owner_infoÚeuids         rC   Ú
_load_jsonzPairingStore._load_jsonÓ  s4  € Ø�;Š;‰=Œ=ð 	ðÝ”z $§.¢.¸' .Ñ"BÔ"BÑCÔCÐCøÝ"ð ð ð ð1ØŸš™œ�BØ!U¨b¬iÐ!UÐ!U½sÀ2Ä:¹¼ÈrÈsÈsÔ?SÐ!UÐ!U�J�JøÝð 1ð 1ð 1Ø!0�J�J�Jð1øøøõ (/­r°9Ñ'=Ô'=ÐH•r”z‘|”|�|À5�Ý—’ðEð
 ˜$ 
¨Añô ð ð �	�	�	�	�	�	øøøøÝÔ(­'Ð2ð ð ð Ø�	�	�	ðøøøàˆ	s@   –'> ¾
DÁ	;BÂC&ÂBÂC&ÂBÂAC&Ã DÃ&DÄDr²   c                 óP   — t          |t          j        |dd¬¦  «        ¦  «         d S )Nrº   Fr»   )rÂ   r­   rÃ   )rð   r§   r²   s      rC   Ú
_save_jsonzPairingStore._save_jsonó  s)   € Ý�d�DœJ t°AÀEÐJÑJÔJÑKÔKÐKÐKÐKrL   rR   c                 ó"   — t          ||¦  «        S )z<Normalize platform-specific user IDs before persisting them.)rV   ©rð   r5   rR   s      rC   rV   zPairingStore._normalize_user_idö  s   € å! (¨GÑ4Ô4Ð4rL   c                 ó"   — t          ||¦  «        S )z@Return all known equivalent user IDs for auth/rate-limit checks.)r\   r  s      rC   r\   zPairingStore._user_id_aliasesú  s   € å ¨'Ñ2Ô2Ð2rL   r]   r^   c                 ó$   — t          |||¦  «        S r`   ©rd   )rð   r5   r]   r^   s       rC   rd   zPairingStore._user_ids_matchþ  s   € å˜x¨¨uÑ5Ô5Ð5rL   c                 ó”   — |                       |                      |¦  «        ¦  «        }|D ]}|                      |||¦  «        r dS ŒdS )z3Check if a user is approved (paired) on a platform.TF)r  rû   rd   )rð   r5   rR   ÚapprovedÚapproved_user_ids        rC   Úis_approvedzPairingStore.is_approved  s]   € à—?’? 4×#6Ò#6°xÑ#@Ô#@ÑAÔAˆØ (ð 	ð 	ÐØ×#Ò# HÐ.>ÀÑHÔHð Ø�t�tðàˆurL   c                 ó   — g }|r|gn|                       d¦  «        }|D ]^}|                      |                      |¦  «        ¦  «        }|                     ¦   «         D ]\  }}|                     ||dœ|¥¦  «         Œ Œ_|S )z5List approved users, optionally filtered by platform.r  )r5   rR   )Ú_all_platformsr  rû   Úitemsro   )rð   r5   ÚresultsrÍ   Úpr  rJ   Úinfos           rC   Úlist_approvedzPairingStore.list_approved  s¡   € àˆØ"*ÐO�X�J�J°×0CÒ0CÀJÑ0OÔ0Oˆ	Øð 	Hð 	HˆAØ—’ t×':Ò':¸1Ñ'=Ô'=Ñ>Ô>ˆHØ%Ÿ^š^Ñ-Ô-ð Hð H‘	��TØ—’¨A¸#ÐFÐFÀÐFÑGÔGÐGÐGðHàˆrL   r8   Ú	user_namec                 ó\  ‡ ‡‡— ‰                       ‰                      ‰¦  «        ¦  «        }‰                      ‰|¦  «        Šˆˆˆ fd„|D ¦   «         }|D ]}||= Œ|t          j        ¦   «         dœ|‰<   ‰                      ‰                      ‰¦  «        |¦  «         t          ‰‰¦  «         dS )zAAdd a user to the approved list. Must be called under self._lock.c                 óB   •— g | ]}‰                      ‰|‰¦  «        ¯|‘ŒS rH   r  )rI   r  Únormalized_user_idr5   rð   s     €€€rC   rK   z.PairingStore._approve_user.<locals>.<listcomp>  sC   ø€ ð 
ð 
ð 
à Ø×#Ò# HÐ.>Ð@RÑSÔSð
Øð
ð 
ð 
rL   )r  Úapproved_atN)r  rû   rV   Útimer  rt   )rð   r5   rR   r  r  Úduplicate_idsr  r"  s   ``     @rC   Ú_approve_userzPairingStore._approve_user  sç   øøø€ à—?’? 4×#6Ò#6°xÑ#@Ô#@ÑAÔAˆØ!×4Ò4°X¸wÑGÔGÐð
ð 
ð 
ð 
ð 
ð 
à$,ð
ñ 
ô 
ˆð
 !.ð 	+ð 	+ÐØÐ)Ð*Ð*ð #Ýœ9™;œ;ð(
ð (
ˆÐ#Ñ$ð 	�Š˜×+Ò+¨HÑ5Ô5°xÑ@Ô@Ð@õ
 	˜HÐ&8Ñ9Ô9Ð9Ð9Ð9rL   c                 ó<  ‡ ‡‡— ‰                       ‰¦  «        }‰ j        5  ‰                      |¦  «        }ˆˆ ˆfd„|D ¦   «         }|r<|D ]}||= Œ‰                      ||¦  «         t	          ‰‰¦  «         	 ddd¦  «         dS 	 ddd¦  «         n# 1 swxY w Y   dS )z<Remove a user from the approved list. Returns True if found.c                 óB   •— g | ]}‰                      ‰|‰¦  «        ¯|‘ŒS rH   r  )rI   r  r5   rð   rR   s     €€€rC   rK   z'PairingStore.revoke.<locals>.<listcomp>2  sB   ø€ ð ð ð à$Ø×'Ò'¨Ð2BÀGÑLÔLðØ ðð ð rL   NTF)rû   rî   r  r  r¦   )rð   r5   rR   r§   r  Úmatching_idsr  s   ```    rC   ÚrevokezPairingStore.revoke-  s;  øøø€ à×"Ò" 8Ñ,Ô,ˆØŒZð 	ð 	Ø—’ tÑ,Ô,ˆHðð ð ð ð ð à(0ðñ ô ˆLð
 ð Ø(4ð 3ð 3Ð$Ø Ð!1Ð2Ð2Ø—’  hÑ/Ô/Ð/õ ' x°Ñ9Ô9Ð9Øð	ð 	ð 	ñ 	ô 	ð 	ð 	ð 	ðð	ð 	ð 	ñ 	ô 	ð 	ð 	ð 	ð 	ð 	ð 	øøøð 	ð 	ð 	ð 	ð  ˆus    ABÂBÂBÚcodeÚsaltc                 óz   — t          j        ||                      d¦  «        z   ¦  «                             ¦   «         S )z6Hash a pairing code with the given salt using SHA-256.r©   )ÚhashlibÚsha256ÚencodeÚ	hexdigest)r+  r,  s     rC   Ú
_hash_codezPairingStore._hash_codeD  s1   € õ Œ~˜d T§[¢[°Ñ%9Ô%9Ñ9Ñ:Ô:×DÒDÑFÔFÐFrL   ÚpendingÚmatched_keyÚmatched_entryc                 ó$  — ||= |                       |                      |¦  «        |¦  «         |                      |¦  «         |                      ||d         |                     dd¦  «        ¦  «         |d         |                     dd¦  «        dœS )zDRemove a pending request and approve its user. Must hold self._lock.rR   r  r8   )rR   r  )r  rù   Ú_reset_failed_attemptsr&  r=   )rð   r5   r3  r4  r5  s        rC   Ú_finish_approvalzPairingStore._finish_approvalI  s¤   € ð �KÐ Ø�Š˜×*Ò*¨8Ñ4Ô4°gÑ>Ô>Ð>ð 	×#Ò# HÑ-Ô-Ð-à×ÒØ�m IÔ.°×0AÒ0AÀ+ÈrÑ0RÔ0Rñ	
ô 	
ð 	
ð
 % YÔ/Ø&×*Ò*¨;¸Ñ;Ô;ð
ð 
ð 	
rL   c                 ó€  — | j         5  |                      |¦  «         |                      ||¦  «        }|                      |¦  «        r	 ddd¦  «         dS |                      ||¦  «        r	 ddd¦  «         dS |                      |                      |¦  «        ¦  «        }t          |¦  «        t          k    r	 ddd¦  «         dS d 	                    d„ t          t          ¦  «        D ¦   «         ¦  «        }t          j        d¦  «        }|                      ||¦  «        }t          j        d¦  «        }	||                     ¦   «         ||t%          j        ¦   «         dœ||	<   |                      |                      |¦  «        |¦  «         |                      ||¦  «         |cddd¦  «         S # 1 swxY w Y   dS )a«  
        Generate a pairing code for a new user.

        Returns the code string, or None if:
          - User is rate-limited (too recent request)
          - Max pending codes reached for this platform
          - User/platform is in lockout due to failed attempts

        The code is NOT stored in plaintext.  Only a salted SHA-256 hash is
        persisted so that reading the pending file does not reveal codes.
        Nr8   c              3   óH   K  — | ]}t          j        t          ¦  «        V — Œd S ©N)ÚsecretsÚchoiceÚALPHABET)rI   Ú_s     rC   ú	<genexpr>z-PairingStore.generate_code.<locals>.<genexpr>�  s,   è è € ÐPÐP¸�7œ>­(Ñ3Ô3ÐPÐPÐPÐPÐPÐPrL   é   r   )Úhashr,  rR   r  Ú
created_at)rî   Ú_cleanup_expiredrV   Ú_is_locked_outÚ_is_rate_limitedr  rù   r¥   ÚMAX_PENDING_PER_PLATFORMrq   ÚrangeÚCODE_LENGTHri   Úurandomr2  r<  Ú	token_hexÚhexr$  r  Ú_record_rate_limit)
rð   r5   rR   r  r"  r3  r+  r,  Ú	code_hashÚentry_ids
             rC   Úgenerate_codezPairingStore.generate_codea  sN  € ð ŒZð (	ð (	Ø×!Ò! (Ñ+Ô+Ð+Ø!%×!8Ò!8¸À7Ñ!KÔ!KÐð ×"Ò" 8Ñ,Ô,ð Øð(	ð (	ð (	ñ (	ô (	ð (	ð (	ð (	ð ×$Ò$ X¨wÑ7Ô7ð Øð(	ð (	ð (	ñ (	ô (	ð (	ð (	ð (	ð —o’o d×&8Ò&8¸Ñ&BÔ&BÑCÔCˆGÝ�7‰|Œ|Õ7Ò7Ð7Øð(	ð (	ð (	ñ (	ô (	ð (	ð (	ð (	ð$ —7’7ÐPÐP½UÅ;Ñ=OÔ=OÐPÑPÔPÑPÔPˆDõ ”:˜b‘>”>ˆDØŸš¨¨dÑ3Ô3ˆIõ Ô(¨Ñ+Ô+ˆHð "ØŸš™
œ
Ø-Ø&Ý"œi™kœkð!ð !ˆG�HÑð �OŠO˜D×.Ò.¨xÑ8Ô8¸'ÑBÔBÐBð ×#Ò# H¨gÑ6Ô6Ð6àðQ(	ð (	ð (	ð (	ñ (	ô (	ð (	ð (	ð (	ð (	ð (	ð (	øøøð (	ð (	ð (	ð (	ð (	ð (	s&   ˆAF3ÁF3Á;AF3Ã	CF3Æ3F7Æ:F7c                 ó  — | j         5  |                      |¦  «         |                     ¦   «                              ¦   «         }|                      |¦  «        r	 ddd¦  «         dS |                      |                      |¦  «        ¦  «        }d}d}|                     ¦   «         D ]�\  }}t          |t          ¦  «        sŒd|vsd|vrŒ$	 t                               |d         ¦  «        }n# t          $ r Y ŒRw xY w|                      ||¦  «        }	t          j        |	|d         ¦  «        r|}|} nŒŽ|€#|                      |¦  «         	 ddd¦  «         dS |                      ||||¦  «        cddd¦  «         S # 1 swxY w Y   dS )u²  
        Approve a pairing code. Adds the user to the approved list.

        Returns ``{user_id, user_name}`` on success, ``None`` if the code is
        invalid/expired OR the platform is currently locked out after
        ``MAX_FAILED_ATTEMPTS`` failed approvals (#10195). Callers can
        disambiguate with ``_is_locked_out(platform)``.

        Verification: the user-provided code is hashed with each stored
        entry's salt and compared to the stored hash using constant-time
        comparison. Pre-hash entries (legacy plaintext-key format from
        pre-upgrade pending.json files) are silently ignored â€” they get
        pruned at TTL by ``_cleanup_expired``.
        Nr,  rB  )rî   rD  Úupperr;   rE  r  rù   r  r�   rž   ÚbytesÚfromhexÚ
ValueErrorr2  r<  Úcompare_digestÚ_record_failed_attemptr8  )
rð   r5   r+  r3  r4  r5  rO  rB   r,  Úcandidate_hashs
             rC   Úapprove_codezPairingStore.approve_code™  sX  € ð ŒZð )	Xð )	XØ×!Ò! (Ñ+Ô+Ð+Ø—:’:‘<”<×%Ò%Ñ'Ô'ˆDð ×"Ò" 8Ñ,Ô,ð Øð)	Xð )	Xð )	Xñ )	Xô )	Xð )	Xð )	Xð )	Xð —o’o d×&8Ò&8¸Ñ&BÔ&BÑCÔCˆGð ˆKØ ˆMØ#*§=¢=¡?¤?ð ð ‘�˜%Ý! %­Ñ.Ô.ð ØØ Ð&Ð&¨&¸Ð*=Ð*=ØðÝ Ÿ=š=¨¨v¬Ñ7Ô7�D�DøÝ!ð ð ð Ø�Hðøøøà!%§¢°°tÑ!<Ô!<�ÝÔ)¨.¸%À¼-ÑHÔHð Ø"*�KØ$)�MØ�Eðð
 Ð"Ø×+Ò+¨HÑ5Ô5Ð5ØðO)	Xð )	Xð )	Xñ )	Xô )	Xð )	Xð )	Xð )	XðR ×(Ò(¨°7¸KÈÑWÔWðS)	Xð )	Xð )	Xð )	Xñ )	Xô )	Xð )	Xð )	Xð )	Xð )	Xð )	Xð )	Xøøøð )	Xð )	Xð )	Xð )	Xð )	Xð )	XsD   ˆAE>Á'A$E>Ã C-Ã,E>Ã-
C:Ã7E>Ã9C:Ã:AE>ÅE>Å>FÆFr‚   c                 ó    — t          | pd¦  «                             ¦   «         } t          | ¦  «        dk    ot          d„ | D ¦   «         ¦  «        S )av  True when ``value`` has the shape of a ``list_pending`` request id.

        Request ids are ``secrets.token_hex(8)`` (16 lowercase hex chars);
        pairing codes are 8 chars from an unambiguous uppercase alphabet that
        excludes every hex letter's ambiguity partner. The two shapes cannot
        collide, so callers accepting either can dispatch on this.
        r8   rA  c              3   ó   K  — | ]}|d v V — Œ	dS )Ú0123456789abcdefABCDEFNrH   )rI   Úcs     rC   r@  z5PairingStore.looks_like_request_id.<locals>.<genexpr>Ý  s(   è è € Ð'UÐ'UÈ!¨Ð-EÐ(EÐ'UÐ'UÐ'UÐ'UÐ'UÐ'UrL   )rT   r;   r¥   Úall)r‚   s    rC   Úlooks_like_request_idz"PairingStore.looks_like_request_idÓ  sN   € õ �E�K˜RÑ Ô ×&Ò&Ñ(Ô(ˆÝ�5‰zŒz˜RÒÐU¥CÐ'UÐ'UÈuÐ'UÑ'UÔ'UÑ$UÔ$UÐUrL   Ú
request_idc                 ór  — | j         5  |                      |¦  «         t          |pd¦  «                             ¦   «                              ¦   «         }|s	 ddd¦  «         dS |                      |                      |¦  «        ¦  «        }|                     ¦   «         D ]~\  }}t          |t          ¦  «        sŒd|vsd|vrŒ$t          j        t          |¦  «                             ¦   «         |¦  «        r&|                      ||||¦  «        c cddd¦  «         S Œ	 ddd¦  «         dS # 1 swxY w Y   dS )a¨  
        Approve a pending pairing request by its server-side request id.

        This is the grant path for authenticated admin surfaces (``hermes
        pairing list``, the dashboard/desktop approve buttons), which show
        pending requests but must never reveal the one-time code DM'd to the
        user. Returns ``{user_id, user_name}`` on success, ``None`` for an
        unknown/expired request id.

        Unlike :meth:`approve_code` this does NOT count a miss toward the
        brute-force lockout, and is not itself gated by one. The lockout
        protects the 8-char code space against guessing over a messaging
        channel; a request id is only ever obtained by an admin already
        authenticated to this store, so a stale id means "the row you clicked
        expired", not an attack. Counting it here let a few GUI clicks on a
        stale list lock the operator out of the CLI's code path too.
        r8   Nr,  rB  )rî   rD  rT   r;   r:   r  rù   r  r�   rž   r<  rV  r8  )rð   r5   r`  r3  rO  rB   s         rC   Úapprove_requestzPairingStore.approve_requestß  sÄ  € ð$ ŒZð 	ð 	Ø×!Ò! (Ñ+Ô+Ð+Ý˜ZÐ-¨2Ñ.Ô.×4Ò4Ñ6Ô6×<Ò<Ñ>Ô>ˆJØð Øð		ð 	ð 	ñ 	ô 	ð 	ð 	ð 	ð —o’o d×&8Ò&8¸Ñ&BÔ&BÑCÔCˆGØ#*§=¢=¡?¤?ð Uð U‘�˜%Ý! %­Ñ.Ô.ð ØØ Ð&Ð&¨&¸Ð*=Ð*=ØÝÔ)­#¨h©-¬-×*=Ò*=Ñ*?Ô*?ÀÑLÔLð UØ×0Ò0°¸7ÀHÈeÑTÔTÐTÐTð	ð 	ð 	ð 	ñ 	ô 	ð 	ð 	ðUð ð	ð 	ð 	ñ 	ô 	ð 	ð 	ð 	ð 	ð 	ð 	ð 	øøøð 	ð 	ð 	ð 	ð 	ð 	s   ˆAD,Á#B-D,ÄD,Ä,D0Ä3D0c                 ób  — g }| j         5  |r|gn|                      d¦  «        }|D �]n}|                      |¦  «         |                      |                      |¦  «        ¦  «        }|                     ¦   «         D �]\  }}t          |t          ¦  «        sŒ|                     d¦  «        }t          |t          t          f¦  «        sŒNt          t          j        ¦   «         |z
  dz  ¦  «        }	t          |                     d¦  «        t          ¦  «        o't          |                     d¦  «        t          ¦  «        }
|                     ||
rt          |¦  «        nd|                     dd¦  «        |                     dd¦  «        |	d	œ¦  «         �Œ�Œp	 d
d
d
¦  «         n# 1 swxY w Y   |S )uz  List pending pairing requests, optionally filtered by platform.

        Codes are stored hashed and are never returned. Each entry exposes a
        server-side ``request_id`` that an authenticated admin surface passes
        to :meth:`approve_request`. Legacy pre-hash entries have no approvable
        id â€” they report an empty ``request_id`` and age out at TTL.
        r3  rC  é<   rB  r,  r8   rR   r  )r5   r`  rR   r  Úage_minutesN)rî   r  rD  r  rù   r  r�   rž   r=   ÚintÚfloatr$  rT   ro   )rð   r5   r  rÍ   r  r3  rO  r  rC  Úage_minÚ	is_moderns              rC   Úlist_pendingzPairingStore.list_pending  sî  € ð ˆØŒZð 	ð 	Ø&.ÐR˜˜
˜
°D×4GÒ4GÈ	Ñ4RÔ4RˆIØð ñ �Ø×%Ò% aÑ(Ô(Ð(ØŸ/š/¨$×*<Ò*<¸QÑ*?Ô*?Ñ@Ô@�Ø&-§m¢m¡o¤oð ñ ‘N�H˜dÝ% d­DÑ1Ô1ð !Ø Ø!%§¢¨,Ñ!7Ô!7�JÝ% jµ3½°,Ñ?Ô?ð !Ø Ý!¥4¤9¡;¤;°Ñ#;¸rÑ"AÑBÔB�GÝ *¨4¯8ª8°FÑ+;Ô+;½SÑ AÔ Að !ÅjØŸš Ñ(Ô(­#ñGô G�Ið —N’NØ$%Ø7@Ð&H¥c¨(¡m¤m mÀbØ#'§8¢8¨I°rÑ#:Ô#:Ø%)§X¢X¨k¸2Ñ%>Ô%>Ø'.ð$ð $ñ ô ð ñ ñðð	ð 	ð 	ñ 	ô 	ð 	ð 	ð 	ð 	ð 	ð 	øøøð 	ð 	ð 	ð 	ð, ˆs   ŠFF$Æ$F(Æ+F(c                 óN  — | j         5  d}|r|gn|                      d¦  «        }|D ]e}|                      |                      |¦  «        ¦  «        }|t	          |¦  «        z  }|                      |                      |¦  «        i ¦  «         Œf	 ddd¦  «         n# 1 swxY w Y   |S )z2Clear all pending requests. Returns count removed.r   r3  N)rî   r  r  rù   r¥   r  )rð   r5   ÚcountrÍ   r  r3  s         rC   Úclear_pendingzPairingStore.clear_pending#  sñ   € àŒZð 	;ð 	;ØˆEØ&.ÐR˜˜
˜
°D×4GÒ4GÈ	Ñ4RÔ4RˆIØð ;ð ;�ØŸ/š/¨$×*<Ò*<¸QÑ*?Ô*?Ñ@Ô@�Ø�˜W™œÑ%�Ø—’ × 2Ò 2°1Ñ 5Ô 5°rÑ:Ô:Ð:Ð:ð;ð	;ð 	;ð 	;ñ 	;ô 	;ð 	;ð 	;ð 	;ð 	;ð 	;ð 	;øøøð 	;ð 	;ð 	;ð 	;ð ˆs   ˆBBÂBÂ!Bc                 ó  — |                       |                      ¦   «         ¦  «        }|                      ||¦  «        D ]A}|› d|› �}|                     |d¦  «        }t	          j        ¦   «         |z
  t
          k     r dS ŒBdS )z2Check if a user has requested a code too recently.ú:r   TF)r  rý   r\   r=   r$  ÚRATE_LIMIT_SECONDS)rð   r5   rR   ÚlimitsÚaliasÚkeyÚlast_requests          rC   rF  zPairingStore._is_rate_limited0  s�   € à—’ ×!6Ò!6Ñ!8Ô!8Ñ9Ô9ˆØ×*Ò*¨8°WÑ=Ô=ð 	ð 	ˆEØÐ'Ð' Ð'Ð'ˆCØ!Ÿ:š: c¨1Ñ-Ô-ˆLÝ”	‘”˜lÑ*Õ.@Ò@Ð@Ø�t�tð AàˆurL   c                 ó  — |                       |                      ¦   «         ¦  «        }t          j        ¦   «         }|                      ||¦  «        D ]}|› d|› �}|||<   Œ|                      |                      ¦   «         |¦  «         dS )z7Record the time of a pairing request for rate limiting.ro  N)r  rý   r$  r\   r  )rð   r5   rR   rq  Únowrr  rs  s          rC   rM  zPairingStore._record_rate_limit:  s�   € à—’ ×!6Ò!6Ñ!8Ô!8Ñ9Ô9ˆÝŒi‰kŒkˆØ×*Ò*¨8°WÑ=Ô=ð 	ð 	ˆEØÐ'Ð' Ð'Ð'ˆCØˆF�3‰KˆKØ�Š˜×-Ò-Ñ/Ô/°Ñ8Ô8Ð8Ð8Ð8rL   c                 ó´   — |                       |                      ¦   «         ¦  «        }d|› �}|                     |d¦  «        }t          j        ¦   «         |k     S )zBCheck if a platform is in lockout due to failed approval attempts.ú	_lockout:r   )r  rý   r=   r$  )rð   r5   rq  Úlockout_keyÚlockout_untils        rC   rE  zPairingStore._is_locked_outC  sP   € à—’ ×!6Ò!6Ñ!8Ô!8Ñ9Ô9ˆØ, (Ð,Ð,ˆØŸ
š
 ;°Ñ2Ô2ˆÝŒy‰{Œ{˜]Ò*Ð*rL   c           	      óš  — |                       |                      ¦   «         ¦  «        }d|› �}|                     |d¦  «        dz   }|||<   |t          k    rMd|› �}t	          j        ¦   «         t
          z   ||<   d||<   t          d|› dt
          › dt          › d�d	¬
¦  «         |                      |                      ¦   «         |¦  «         dS )zMRecord a failed approval attempt. Triggers lockout after MAX_FAILED_ATTEMPTS.ú
_failures:r   é   rx  z[pairing] Platform z locked out for zs after z failed attemptsT)rÜ   N)r  rý   r=   ÚMAX_FAILED_ATTEMPTSr$  ÚLOCKOUT_SECONDSÚprintr  )rð   r5   rq  Úfail_keyÚfailsry  s         rC   rW  z#PairingStore._record_failed_attemptJ  s  € à—’ ×!6Ò!6Ñ!8Ô!8Ñ9Ô9ˆØ* Ð*Ð*ˆØ—
’
˜8 QÑ'Ô'¨!Ñ+ˆØ ˆˆxÑØÕ'Ò'Ð'Ø0 hÐ0Ð0ˆKÝ"&¤)¡+¤+µÑ"?ˆF�;ÑØ ˆF�8ÑÝð A¨ð Að AÅ/ð Að AÝ.ðAð Að AØHLðNñ Nô Nð Nà�Š˜×-Ò-Ñ/Ô/°Ñ8Ô8Ð8Ð8Ð8rL   c                 óæ   — |                       |                      ¦   «         ¦  «        }d|› �}|                     |¦  «        r/d||<   |                      |                      ¦   «         |¦  «         dS dS )a&  Clear the accumulated failed-approval counter after a success.

        Called from the ``approve_code`` success path so that a legitimate
        approval resets the brute-force streak (standard lockout semantics:
        the counter tracks *consecutive* failures, not lifetime ones).
        r|  r   N)r  rý   r=   r  )rð   r5   rq  r�  s       rC   r7  z#PairingStore._reset_failed_attemptsX  s|   € ð —’ ×!6Ò!6Ñ!8Ô!8Ñ9Ô9ˆØ* Ð*Ð*ˆØ�:Š:�hÑÔð 	=Ø ˆF�8ÑØ�OŠO˜D×1Ò1Ñ3Ô3°VÑ<Ô<Ð<Ð<Ð<ð	=ð 	=rL   c                 ó&  — |                       |¦  «        }|                      |¦  «        }t          j        ¦   «         }g }|                     ¦   «         D ]š\  }}t	          |t
          ¦  «        s|                     |¦  «         Œ0|                     d¦  «        }t	          |t          t          f¦  «        s|                     |¦  «         Œw||z
  t          k    r|                     |¦  «         Œ›|r |D ]}||= Œ|                      ||¦  «         dS dS )uí   Remove expired pending codes.

        Tolerant of malformed / legacy entries â€” anything without a numeric
        ``created_at`` is treated as expired (it's effectively unusable
        with the new hash-keyed schema anyway).
        rC  N)rù   r  r$  r  r�   rž   ro   r=   rf  rg  ÚCODE_TTL_SECONDSr  )	rð   r5   r§   r3  rv  ÚexpiredrO  r  rC  s	            rC   rD  zPairingStore._cleanup_expiredg  s   € ð ×!Ò! (Ñ+Ô+ˆØ—/’/ $Ñ'Ô'ˆÝŒi‰kŒkˆØˆØ%Ÿmšm™oœoð 		)ð 		)‰NˆH�dÝ˜d¥DÑ)Ô)ð Ø—’˜xÑ(Ô(Ð(ØØŸš ,Ñ/Ô/ˆJÝ˜j­3µ¨,Ñ7Ô7ð Ø—’˜xÑ(Ô(Ð(ØØ�jÑ Õ$4Ò4Ð4Ø—’˜xÑ(Ô(Ð(øØð 	+Ø#ð &ð &�Ø˜HÐ%Ð%Ø�OŠO˜D 'Ñ*Ô*Ð*Ð*Ð*ð	+ð 	+rL   rÕ   c                 ó  — g }| j                              ¦   «         D ]i}|j                             d|› d�¦  «        rI|j                             d|› d�d¦  «        }|                     d¦  «        s|                     |¦  «         Œj|S )z:List all platforms that have data files of a given suffix.Ú-z.jsonr8   r?  )rë   Úiterdirrƒ   ÚendswithÚreplaceÚ
startswithro   )rð   rÕ   rÍ   rä   r5   s        rC   r  zPairingStore._all_platforms�  s˜   € àˆ	Ø”×"Ò"Ñ$Ô$ð 	/ð 	/ˆAØŒv�ŠÐ0 6Ð0Ð0Ð0Ñ1Ô1ð /Øœ6Ÿ>š>Ð*;¨fÐ*;Ð*;Ð*;¸RÑ@Ô@�Ø×*Ò*¨3Ñ/Ô/ð /Ø×$Ò$ XÑ.Ô.Ð.øØÐrL   r;  )r8   ),Ú__name__Ú
__module__Ú__qualname__Ú__doc__r   rT   ró   Úpropertyrç   r   rù   rû   rý   rž   r  r  rV   rX   r\   ra   rd   r  r’   r  r&  r*  ÚstaticmethodrS  r2  r8  rP  rY  r_  rb  rj  rf  rm  rF  rM  rE  rW  r7  rD  r  rH   rL   rC   ræ   ræ   •  sò  € € € € € ðð ð ð   ¨¤ð  ð  ð  ð  ð@ ð˜ #œð ð ð ñ „Xðð6 cð 6¨dð 6ð 6ð 6ð 6ð7 sð 7¨tð 7ð 7ð 7ð 7ð/ $ð /ð /ð /ð /ð˜tð ¨ð ð ð ð ð@L˜tð L¨4ð L°Dð Lð Lð Lð Lð5¨3ð 5¸ð 5Àð 5ð 5ð 5ð 5ð3¨ð 3°sð 3¸sÀ3¼xð 3ð 3ð 3ð 3ð6¨ð 6°3ð 6¸sð 6Àtð 6ð 6ð 6ð 6ð Cð °#ð ¸$ð ð ð ð ðð  cð °Tð ð ð ð ð:ð : cð :°Cð :ÀCð :ÐQUð :ð :ð :ð :ð.˜sð ¨Sð °Tð ð ð ð ð. ðG˜ð G Eð G¨cð Gð Gð Gñ „\ðGð
Øð
Ø&*ð
Ø9<ð
ØMQð
à	ð
ð 
ð 
ð 
ð2 =?ð6ð 6Øð6Ø&)ð6Ø69ð6à	�#Œð6ð 6ð 6ð 6ðp8X Sð 8X°ð 8X¸À¼ð 8Xð 8Xð 8Xð 8Xðt ð	V Sð 	V¨Tð 	Vð 	Vð 	Vñ „\ð	Vð!¨ð !¸ð !ÀÈ$Äð !ð !ð !ð !ðFð  Sð °Dð ð ð ð ðB	ð 	 cð 	°Sð 	ð 	ð 	ð 	ð¨ð °sð ¸tð ð ð ð ð9¨3ð 9¸ð 9Àð 9ð 9ð 9ð 9ð+ sð +¨tð +ð +ð +ð +ð9¨sð 9°tð 9ð 9ð 9ð 9ð=¨sð =°tð =ð =ð =ð =ð+¨ð +°ð +ð +ð +ð +ð4 Sð ¨Tð ð ð ð ð ð rL   ræ   )9r�  r.  r­   Úloggingri   r<  rØ   rì   r$  Úpathlibr   Útypingr   Úgateway.whatsapp_identityr   r   Úhermes_constantsr   r   r	   Úutilsr
   Ú	getLoggerr�  r  r>  rI  r…  rp  r  rG  r~  rÎ   r<   rT   rD   r’   rO   ra   rQ   rV   rX   r\   rd   rk   rt   r€   r„   r—   r    r¦   rž   r³   rÈ   rÒ   rÂ   ræ   rH   rL   rC   ú<module>rš     s!  ððð ð( €€€Ø €€€Ø €€€Ø 	€	€	€	Ø €€€Ø €€€Ø Ð Ð Ð Ø €€€Ø Ð Ð Ð Ð Ð Ø Ð Ð Ð Ð Ð ðð ð ð ð ð ð ð ðð ð ð ð ð ð ð ð ð ð
 !Ð  Ð  Ð  Ð  Ð  à	ˆÔ	˜8Ñ	$Ô	$€ð .€Ø€ð Ð ØÐ Ø€ð Ð ØÐ àˆnÐ0°)Ñ<Ô<€ðØÐ(ðàÐ&ðð Ð(ðð Ð4ð	ð
 Ð"ðð Ð$ðð Ð"ðð 
Ððð Ð,ðð Ð$ðð Ð(ðð Ð$ðð Ð"ðð Ð4ðð Ð$ðð  Ð.ð!ð" Ðð#ð$ Ð&ð%ð Ð ð,¨#ð °(¸3´-ð ð ð ð ð*B˜#ð B $ð Bð Bð Bð BðN¨sð N°tð Nð Nð Nð Nð
 ð ¨sð °sð ð ð ð ð
˜sð 
¨Sð 
°S¸´Xð 
ð 
ð 
ð 
ðS˜cð S¨ð S°Sð S¸Tð Sð Sð Sð Sð. ð .¨ð .ð .ð .ð .ð4 #ð °ð ¸ð ð ð ð ð:ð ð ð*+ sð +ð +ð +ð +ð°ð ¸cð ð ð ð ð4°#ð Àð Èð ð ð ð ð>#; Sð #;°3ð #;¸4ð #;ð #;ð #;ð #;ðL˜$ð  4ð ð ð ð ðR 4ð R¸ð RÀð Rð Rð Rð Rð8  Ø!ð
*ð 
*ð 
*à
�4Œ.ð
*ð �TŒNð
*ð 
ð	
*ð 
*ð 
*ð 
*ð˜ð  Cð ¨Dð ð ð ð ð4tð tð tð tð tñ tô tð tð tð trL   