§
    ‰eîiÌ,  ã                   óÀ   — 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mZ ddl	m
Z
 ddlmZ dZdZdZd	ZdZd
ZdZ edd¦  «        Zdededdfd„Z G d„ d¦  «        ZdS )a±  
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)Úget_hermes_dirÚ ABCDEFGHJKLMNPQRSTUVWXYZ23456789é   i  iX  é   é   zplatforms/pairingÚpairingÚpathÚdataÚreturnc                 óŽ  — | 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          j        |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.
    T©ÚparentsÚexist_okz.tmp)ÚdirÚsuffixÚwúutf-8©ÚencodingNi€  )ÚparentÚmkdirÚtempfileÚmkstempÚstrÚosÚfdopenÚwriteÚflushÚfsyncÚfilenoÚreplaceÚchmodÚOSErrorÚBaseExceptionÚunlink)r   r   ÚfdÚtmp_pathÚfs        ú1/home/div/.hermes/hermes-agent/gateway/pairing.pyÚ_secure_writer,   1   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Ñ Ô Ð ð	!ð 	!ð 	!ñ 	!ô 	!ð 	!ð 	!ð 	!ð 	!ð 	!ð 	!øøøð 	!ð 	!ð 	!ð 	!õ 	Œ
�8�S ™YœYÑ'Ô'Ð'ð	ÝŒH�T˜5Ñ!Ô!Ð!Ð!Ð!øÝð 	ð 	ð 	ØˆDˆDð	øøøøåð ð ð ð	ÝŒI�hÑÔÐÐøÝð 	ð 	ð 	ØˆDð	øøøàðøøøss   Á	D Á AB<Â0D Â<C Ã D ÃC Ã%D Ã*D Ä
DÄD ÄDÄD Ä
EÄD2Ä1EÄ2
D?Ä<EÄ>D?Ä?Ec            
       ó�  — e Zd ZdZ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 dedefd„Zd!dedededdfd„Zdededefd„Z	 d!dedededee         fd„Zdededee
         fd„Zd dedefd„Zd d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efd„ZdS )"Ú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
    c                 ón   — t                                dd¬¦  «         t          j        ¦   «         | _        d S )NTr   )ÚPAIRING_DIRr   Ú	threadingÚRLockÚ_lock©Úselfs    r+   Ú__init__zPairingStore.__init__U   s0   € Ý×Ò $°ÐÑ6Ô6Ð6õ ”_Ñ&Ô&ˆŒ
ˆ
ˆ
ó    Úplatformr   c                 ó   — t           |› d�z  S )Nz-pending.json©r0   ©r5   r8   s     r+   Ú_pending_pathzPairingStore._pending_path[   s   € Ý Ð7Ð7Ð7Ñ7Ð7r7   c                 ó   — t           |› d�z  S )Nz-approved.jsonr:   r;   s     r+   Ú_approved_pathzPairingStore._approved_path^   s   € Ý Ð8Ð8Ð8Ñ8Ð8r7   c                 ó   — t           dz  S )Nz_rate_limits.jsonr:   r4   s    r+   Ú_rate_limit_pathzPairingStore._rate_limit_patha   s   € ÝÐ0Ñ0Ð0r7   r   c                 ó¼   — |                      ¦   «         rG	 t          j        |                     d¬¦  «        ¦  «        S # t          j        t
          f$ r i cY S w xY wi S )Nr   r   )ÚexistsÚjsonÚloadsÚ	read_textÚJSONDecodeErrorr%   )r5   r   s     r+   Ú
_load_jsonzPairingStore._load_jsond   sf   € Ø�;Š;‰=Œ=ð 	ðÝ”z $§.¢.¸' .Ñ"BÔ"BÑCÔCÐCøÝÔ(­'Ð2ð ð ð Ø�	�	�	ðøøøàˆ	s   –'> ¾AÁAr   Nc                 óP   — t          |t          j        |dd¬¦  «        ¦  «         d S )Né   F)ÚindentÚensure_ascii)r,   rC   Údumps)r5   r   r   s      r+   Ú
_save_jsonzPairingStore._save_jsonl   s)   € Ý�d�DœJ t°AÀEÐJÑJÔJÑKÔKÐKÐKÐKr7   Úuser_idc                 óZ   — |                       |                      |¦  «        ¦  «        }||v S )z3Check if a user is approved (paired) on a platform.)rG   r>   )r5   r8   rN   Úapproveds       r+   Úis_approvedzPairingStore.is_approvedq   s,   € à—?’? 4×#6Ò#6°xÑ#@Ô#@ÑAÔAˆØ˜(Ð"Ð"r7   c                 ó   — g }|r|gn|                       d¦  «        }|D ]^}|                      |                      |¦  «        ¦  «        }|                     ¦   «         D ]\  }}|                     ||dœ|¥¦  «         Œ Œ_|S )z5List approved users, optionally filtered by platform.rP   )r8   rN   )Ú_all_platformsrG   r>   ÚitemsÚappend)r5   r8   ÚresultsÚ	platformsÚprP   ÚuidÚinfos           r+   Úlist_approvedzPairingStore.list_approvedv   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àˆr7   Ú Ú	user_namec                 óÚ   — |                       |                      |¦  «        ¦  «        }|t          j        ¦   «         dœ||<   |                      |                      |¦  «        |¦  «         dS )zAAdd a user to the approved list. Must be called under self._lock.)r]   Úapproved_atN)rG   r>   ÚtimerM   )r5   r8   rN   r]   rP   s        r+   Ú_approve_userzPairingStore._approve_user€   sh   € à—?’? 4×#6Ò#6°xÑ#@Ô#@ÑAÔAˆà"Ýœ9™;œ;ð
ð 
ˆ�Ñð 	�Š˜×+Ò+¨HÑ5Ô5°xÑ@Ô@Ð@Ð@Ð@r7   c                 óð   — |                       |¦  «        }| j        5  |                      |¦  «        }||v r'||= |                      ||¦  «         	 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.NTF)r>   r3   rG   rM   )r5   r8   rN   r   rP   s        r+   ÚrevokezPairingStore.revoke‰   sß   € à×"Ò" 8Ñ,Ô,ˆØŒZð 	ð 	Ø—’ tÑ,Ô,ˆHØ˜(Ð"Ð"Ø˜WÐ%Ø—’  hÑ/Ô/Ð/Øð	ð 	ð 	ñ 	ô 	ð 	ð 	ð 	à"ð	ð 	ð 	ñ 	ô 	ð 	ð 	ð 	ð 	ð 	ð 	øøøð 	ð 	ð 	ð 	ð ˆus   �4A+Á+A/Á2A/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œ||<   |                      |                      |¦  «        |¦  «         |                      ||¦  «         |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
        Nr\   c              3   óH   K  — | ]}t          j        t          ¦  «        V — Œd S ©N)ÚsecretsÚchoiceÚALPHABET)Ú.0Ú_s     r+   ú	<genexpr>z-PairingStore.generate_code.<locals>.<genexpr>²   s,   è è € ÐPÐP¸�7œ>­(Ñ3Ô3ÐPÐPÐPÐPÐPÐPr7   )rN   r]   Ú
created_at)r3   Ú_cleanup_expiredÚ_is_locked_outÚ_is_rate_limitedrG   r<   ÚlenÚMAX_PENDING_PER_PLATFORMÚjoinÚrangeÚCODE_LENGTHr`   rM   Ú_record_rate_limit)r5   r8   rN   r]   ÚpendingÚcodes         r+   Úgenerate_codezPairingStore.generate_code–   sô  € ð ŒZð 	ð 	Ø×!Ò! (Ñ+Ô+Ð+ð ×"Ò" 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ð #Ø&Ý"œi™kœkðð ˆG�D‰Mð
 �OŠO˜D×.Ò.¨xÑ8Ô8¸'ÑBÔBÐBð ×#Ò# H¨gÑ6Ô6Ð6àð=	ð 	ð 	ð 	ñ 	ô 	ð 	ð 	ð 	ð 	ð 	ð 	øøøð 	ð 	ð 	ð 	ð 	ð 	s%   ˆ,EÁEÁ%AEÂ3BEÅEÅErx   c           	      ót  — | j         5  |                      |¦  «         |                     ¦   «                              ¦   «         }|                      |                      |¦  «        ¦  «        }||vr#|                      |¦  «         	 ddd¦  «         dS |                     |¦  «        }|                      |                      |¦  «        |¦  «         |  	                    ||d         | 
                    dd¦  «        ¦  «         |d         | 
                    dd¦  «        dœcddd¦  «         S # 1 swxY w Y   dS )z 
        Approve a pairing code. Adds the user to the approved list.

        Returns {user_id, user_name} on success, None if code is invalid/expired.
        NrN   r]   r\   )rN   r]   )r3   rn   ÚupperÚstriprG   r<   Ú_record_failed_attemptÚpoprM   ra   Úget)r5   r8   rx   rw   Úentrys        r+   Úapprove_codezPairingStore.approve_codeÁ   sŽ  € ð ŒZð 	ð 	Ø×!Ò! (Ñ+Ô+Ð+Ø—:’:‘<”<×%Ò%Ñ'Ô'ˆDà—o’o d×&8Ò&8¸Ñ&BÔ&BÑCÔCˆGØ˜7Ð"Ð"Ø×+Ò+¨HÑ5Ô5Ð5Øð	ð 	ð 	ñ 	ô 	ð 	ð 	ð 	ð —K’K Ñ%Ô%ˆEØ�OŠO˜D×.Ò.¨xÑ8Ô8¸'ÑBÔBÐBð ×Ò˜x¨¨yÔ)9¸5¿9º9À[ÐRTÑ;UÔ;UÑVÔVÐVð ! Ô+Ø"ŸYšY {°BÑ7Ô7ðð ð	ð 	ð 	ð 	ñ 	ô 	ð 	ð 	ð 	ð 	ð 	ð 	øøøð 	ð 	ð 	ð 	ð 	ð 	s   ˆA>D-ÂBD-Ä-D1Ä4D1c                 ó¸  — g }|r|gn|                       d¦  «        }|D ]º}|                      |¦  «         |                      |                      |¦  «        ¦  «        }|                     ¦   «         D ]f\  }}t          t          j        ¦   «         |d         z
  dz  ¦  «        }|                     |||d         |                     dd¦  «        |dœ¦  «         ŒgŒ»|S )z?List pending pairing requests, optionally filtered by platform.rw   rm   é<   rN   r]   r\   )r8   rx   rN   r]   Úage_minutes)	rS   rn   rG   r<   rT   Úintr`   rU   r   )	r5   r8   rV   rW   rX   rw   rx   rZ   Úage_mins	            r+   Úlist_pendingzPairingStore.list_pendingÛ   sö   € àˆØ"*ÐN�X�J�J°×0CÒ0CÀIÑ0NÔ0Nˆ	Øð 	ð 	ˆAØ×!Ò! !Ñ$Ô$Ð$Ø—o’o d×&8Ò&8¸Ñ&;Ô&;Ñ<Ô<ˆGØ%Ÿmšm™oœoð ð ‘
��dÝ�tœy™{œ{¨T°,Ô-?Ñ?À2ÑEÑFÔF�Ø—’Ø !Ø Ø# IœØ!%§¢¨+°rÑ!:Ô!:Ø#*ð ð  ñ ô ð ð ðð ˆr7   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   rw   N)r3   rS   rG   r<   rq   rM   )r5   r8   ÚcountrW   rX   rw   s         r+   Ú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|› �}|                     |d¦  «        }t          j        ¦   «         |z
  t          k     S )z2Check if a user has requested a code too recently.ú:r   )rG   r@   r   r`   ÚRATE_LIMIT_SECONDS)r5   r8   rN   ÚlimitsÚkeyÚlast_requests         r+   rp   zPairingStore._is_rate_limitedú   s\   € à—’ ×!6Ò!6Ñ!8Ô!8Ñ9Ô9ˆØÐ%Ð%˜GÐ%Ð%ˆØ—z’z # qÑ)Ô)ˆÝ”	‘”˜lÑ*Õ.@Ò@Ð@r7   c                 óÞ   — |                       |                      ¦   «         ¦  «        }|› d|› �}t          j        ¦   «         ||<   |                      |                      ¦   «         |¦  «         dS )z7Record the time of a pairing request for rate limiting.rŒ   N)rG   r@   r`   rM   )r5   r8   rN   rŽ   r�   s        r+   rv   zPairingStore._record_rate_limit  sg   € à—’ ×!6Ò!6Ñ!8Ô!8Ñ9Ô9ˆØÐ%Ð%˜GÐ%Ð%ˆÝ”i‘k”kˆˆs‰Ø�Š˜×-Ò-Ñ/Ô/°Ñ8Ô8Ð8Ð8Ð8r7   c                 ó´   — |                       |                      ¦   «         ¦  «        }d|› �}|                     |d¦  «        }t          j        ¦   «         |k     S )zBCheck if a platform is in lockout due to failed approval attempts.ú	_lockout:r   )rG   r@   r   r`   )r5   r8   rŽ   Úlockout_keyÚlockout_untils        r+   ro   zPairingStore._is_locked_out  sP   € à—’ ×!6Ò!6Ñ!8Ô!8Ñ9Ô9ˆØ, (Ð,Ð,ˆØŸ
š
 ;°Ñ2Ô2ˆÝŒy‰{Œ{˜]Ò*Ð*r7   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.z
_failures:r   é   r“   z[pairing] Platform z locked out for zs after z failed attemptsT)r    N)rG   r@   r   ÚMAX_FAILED_ATTEMPTSr`   ÚLOCKOUT_SECONDSÚprintrM   )r5   r8   rŽ   Úfail_keyÚfailsr”   s         r+   r}   z#PairingStore._record_failed_attempt  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Ð8r7   c                 ó  ‡— |                       |¦  «        }|                      |¦  «        }t          j        ¦   «         Šˆfd„|                     ¦   «         D ¦   «         }|r |D ]}||= Œ|                      ||¦  «         dS dS )zRemove expired pending codes.c                 óB   •— g | ]\  }}‰|d          z
  t           k    ¯|‘ŒS )rm   )ÚCODE_TTL_SECONDS)rj   rx   rZ   Únows      €r+   ú
<listcomp>z1PairingStore._cleanup_expired.<locals>.<listcomp>$  s=   ø€ ð 
ð 
ð 
Ù�T˜4Ø�d˜<Ô(Ñ(Õ,<Ò<Ð<ð Ø<Ð<Ð<r7   N)r<   rG   r`   rT   rM   )r5   r8   r   rw   Úexpiredrx   r    s         @r+   rn   zPairingStore._cleanup_expired  s¨   ø€ à×!Ò! (Ñ+Ô+ˆØ—/’/ $Ñ'Ô'ˆÝŒi‰kŒkˆð
ð 
ð 
ð 
Ø#*§=¢=¡?¤?ð
ñ 
ô 
ˆð ð 	+Øð "ð "�Ø˜D�M�MØ�OŠO˜D 'Ñ*Ô*Ð*Ð*Ð*ð	+ð 	+r7   r   c                 ó  — g }t                                ¦   «         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.jsonr\   rk   )r0   ÚiterdirÚnameÚendswithr#   Ú
startswithrU   )r5   r   rW   r*   r8   s        r+   rS   zPairingStore._all_platforms-  s–   € àˆ	Ý×$Ò$Ñ&Ô&ð 	/ð 	/ˆAØŒv�ŠÐ0 6Ð0Ð0Ð0Ñ1Ô1ð /Øœ6Ÿ>š>Ð*;¨fÐ*;Ð*;Ð*;¸RÑ@Ô@�Ø×*Ò*¨3Ñ/Ô/ð /Ø×$Ò$ XÑ.Ô.Ð.øØÐr7   rf   )r\   )Ú__name__Ú
__module__Ú__qualname__Ú__doc__r6   r   r   r<   r>   r@   ÚdictrG   rM   ÚboolrQ   Úlistr[   ra   rc   r   ry   r�   r‡   r…   rŠ   rp   rv   ro   r}   rn   rS   © r7   r+   r.   r.   K   s5  € € € € € ðð ð'ð 'ð 'ð8 cð 8¨dð 8ð 8ð 8ð 8ð9 sð 9¨tð 9ð 9ð 9ð 9ð1 $ð 1ð 1ð 1ð 1ð˜tð ¨ð ð ð ð ðL˜tð L¨4ð L°Dð Lð Lð Lð Lð
# Cð #°#ð #¸$ð #ð #ð #ð #ð
ð  cð °Tð ð ð ð ðAð A cð A°Cð AÀCð AÐQUð Að Að Að Að	˜sð 	¨Sð 	°Tð 	ð 	ð 	ð 	ð =?ð)ð )Øð)Ø&)ð)Ø69ð)à	�#Œð)ð )ð )ð )ðV Sð °ð ¸À¼ð ð ð ð ð4ð  Sð °Dð ð ð ð ð$	ð 	 cð 	°Sð 	ð 	ð 	ð 	ðA¨ð A°sð A¸tð Að Að Að Að9¨3ð 9¸ð 9Àð 9ð 9ð 9ð 9ð+ sð +¨tð +ð +ð +ð +ð9¨sð 9°tð 9ð 9ð 9ð 9ð +¨ð +°ð +ð +ð +ð +ð Sð ¨Tð ð ð ð ð ð r7   r.   )r¬   rC   r   rg   r   r1   r`   Úpathlibr   Útypingr   Úhermes_constantsr   ri   ru   rŸ   r�   r™   rr   r˜   r0   r   r,   r.   r°   r7   r+   ú<module>r´      s$  ððð ð( €€€Ø 	€	€	€	Ø €€€Ø €€€Ø Ð Ð Ð Ø €€€Ø Ð Ð Ð Ð Ð Ø Ð Ð Ð Ð Ð à +Ð +Ð +Ð +Ð +Ð +ð .€Ø€ð Ð ØÐ Ø€ð Ð ØÐ àˆnÐ0°)Ñ<Ô<€ð˜ð  Cð ¨Dð ð ð ð ð4jð jð jð jð jñ jô jð jð jð jr7   