+
    Ӄi,                         R 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 Rt^tRtRtRt^t^t]! RR	4      tR
 R lt ! R R4      tR# )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 ABCDEFGHJKLMNPQRSTUVWXYZ23456789i  iX  zplatforms/pairingpairingc                4    V ^8  d   QhR\         R\        RR/#    pathdatareturnN)r   str)formats   ",/home/ubuntu/hermes-agent/gateway/pairing.py__annotate__r   1   s!       C D     c                   V P                   P                  RRR7       \        P                  ! \	        V P                   4      RR7      w  r# \
        P                  ! VRRR7      ;_uu_ 4       pVP                  V4       VP                  4        \
        P                  ! VP                  4       4       RRR4       \
        P                  ! V\	        V 4      4        \
        P                  ! V R	4       R#   + '       g   i     LJ; i  \         d     R# i ; i  \         d+     \
        P                  ! T4       h   \         d     h i ; ii ; i)
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mkstempr   osfdopenwriteflushfsyncfilenoreplacechmodOSErrorBaseExceptionunlink)r   r   fdtmp_pathfs   &&   r   _secure_writer/   1   s     	KKdT2##DKK(8HLBYYr311QGGDMGGIHHQXXZ  2 	

8SY'	HHT5! 21  		 	IIh 	  		sg   !D -AC53(D D 5D	 D DD DD E&D=<E=EE
EEc                     a  ] tR t^Kt o RtR 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
V 3R lR ltR+V 3R lR lltR,V 3R lR lltV 3R lR ltR,V 3R lR lltV 3R lR ltR+V 3R lR lltR+V 3R lR l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V 3R& lR' ltV 3R( lR) ltR*tV tR# )-PairingStorez
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                h    \         P                  R R R7       \        P                  ! 4       V n        R# )Tr   N)PAIRING_DIRr   	threadingRLock_lockselfs   &r   __init__PairingStore.__init__U   s%    $6 __&
r   c                &   < V ^8  d   QhRS[ RS[/# r
   platformr   r   r   )r   __classdict__s   "r   r   PairingStore.__annotate__[   s     8 8c 8d 8r   c                "    \         V R 2,          # )z-pending.jsonr3   r8   r=   s   &&r   _pending_pathPairingStore._pending_path[   s    z777r   c                &   < V ^8  d   QhRS[ RS[/# r<   r>   )r   r?   s   "r   r   r@   ^   s     9 9s 9t 9r   c                "    \         V R 2,          # )z-approved.jsonrB   rC   s   &&r   _approved_pathPairingStore._approved_path^   s    z888r   c                    < V ^8  d   QhRS[ /# )r
   r   r   )r   r?   s   "r   r   r@   a   s     1 1$ 1r   c                    \         R ,          # )z_rate_limits.jsonrB   r7   s   &r   _rate_limit_pathPairingStore._rate_limit_patha   s    000r   c                &   < V ^8  d   QhRS[ RS[/# )r
   r   r   r   dict)r   r?   s   "r   r   r@   d   s      t  r   c                    VP                  4       '       d(    \        P                  ! VP                  R R7      4      # / #   \        P                  \
        3 d    / u # i ; i)r   r   )existsjsonloads	read_textJSONDecodeErrorr)   )r8   r   s   &&r   
_load_jsonPairingStore._load_jsond   sT    ;;==zz$..'."BCC 	 (('2 	s   %A   A A c                *   < V ^8  d   QhRS[ RS[RR/# r	   rO   )r   r?   s   "r   r   r@   l   s'     L Lt L4 LD Lr   c           	     L    \        V\        P                  ! V^RR7      4       R# )r
   F)indentensure_asciiN)r/   rS   dumps)r8   r   r   s   &&&r   
_save_jsonPairingStore._save_jsonl   s    dDJJtAEJKr   c                ,   < V ^8  d   QhRS[ RS[ RS[/# r
   r=   user_idr   r   bool)r   r?   s   "r   r   r@   q   s"     # #C ## #$ #r   c                J    V P                  V P                  V4      4      pW#9   # )z3Check if a user is approved (paired) on a platform.)rW   rH   )r8   r=   rb   approveds   &&& r   is_approvedPairingStore.is_approvedq   s$    ??4#6#6x#@A""r   Nc                &   < V ^8  d   QhRS[ RS[/# r<   r   list)r   r?   s   "r   r   r@   v   s      c T r   c                    . pV'       d   V.MV P                  R4      pV FS  pV P                  V P                  V4      4      pVP                  4        F  w  rgVP	                  RVRV/VC4       K  	  KU  	  V# )z5List approved users, optionally filtered by platform.rf   r=   rb   )_all_platformsrW   rH   itemsappend)r8   r=   results	platformsprf   uidinfos   &&      r   list_approvedPairingStore.list_approvedv   st    "*XJ0C0CJ0O	At':':1'=>H%^^-	
Ay#FFG .  r   c                0   < V ^8  d   QhRS[ RS[ RS[ RR/# )r
   r=   rb   	user_namer   Nr   )r   r?   s   "r   r   r@      s0     A Ac AC AC AQU Ar   c                    V P                  V P                  V4      4      pRVR\        P                  ! 4       /WB&   V P                  V P                  V4      V4       R# )zAAdd a user to the approved list. Must be called under self._lock.rx   approved_atN)rW   rH   timer^   )r8   r=   rb   rx   rf   s   &&&& r   _approve_userPairingStore._approve_user   sP    ??4#6#6x#@A499;
 	++H5x@r   c                ,   < V ^8  d   QhRS[ RS[ RS[/# ra   rc   )r   r?   s   "r   r   r@      s"     	 	s 	S 	T 	r   c                    V P                  V4      pV P                  ;_uu_ 4        V P                  V4      pW$9   d   WB V P                  W44        RRR4       R#  RRR4       R#   + '       g   i     R# ; i)z<Remove a user from the approved list. Returns True if found.NTF)rH   r6   rW   r^   )r8   r=   rb   r   rf   s   &&&  r   revokePairingStore.revoke   se    ""8,ZZZt,H"%/ Z"   Z s   ,A''A8	c          	      B   < V ^8  d   QhRS[ RS[ RS[ RS[S[ ,          /# )r
   r=   rb   rx   r   )r   r   )r   r?   s   "r   r   r@      s1     ) ))&))69)	#)r   c           	     |   V P                   ;_uu_ 4        V P                  V4       V P                  V4      '       d    RRR4       R# 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RVRVR\        P                  ! 4       /WE&   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 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
N c              3   V   "   T F  p\         P                  ! \        4      x  K!  	  R # 5iN)secretschoiceALPHABET).0_s   & r   	<genexpr>-PairingStore.generate_code.<locals>.<genexpr>   s     P=O7>>(33=Os   ')rb   rx   
created_at)r6   _cleanup_expired_is_locked_out_is_rate_limitedrW   rD   lenMAX_PENDING_PER_PLATFORMjoinrangeCODE_LENGTHr|   r^   _record_rate_limit)r8   r=   rb   rx   pendingcodes   &&&&  r   generate_codePairingStore.generate_code   s     ZZZ!!(+ ""8,, Z $$X77 Z ood&8&8&BCG7|77 Z" 77PU;=OPPD 7YdiikGM
 OOD..x8'B ##H6= ZZZs   *D*	D*+5D**A5D**D;	c                <   < V ^8  d   QhRS[ RS[ RS[S[,          /# )r
   r=   r   r   )r   r   rP   )r   r?   s   "r   r   r@      s&      S   r   c           
     :   V P                   ;_uu_ 4        V P                  V4       VP                  4       P                  4       pV P	                  V P                  V4      4      pW#9  d   V P                  V4        RRR4       R# VP                  V4      pV P                  V P                  V4      V4       V P                  WR,          VP                  RR4      4       RVR,          RVP                  RR4      /uuRRR4       #   + '       g   i     R# ; i)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.
Nrb   rx   r   )r6   r   upperstriprW   rD   _record_failed_attemptpopr^   r}   get)r8   r=   r   r   entrys   &&&  r   approve_codePairingStore.approve_code   s     ZZZ!!(+::<%%'Dood&8&8&BCG"++H5 Z KK%EOOD..x8'B xy)9599[RT;UV 5+UYY{B7 ZZZs   A(D	A7D		D	c                &   < V ^8  d   QhRS[ RS[/# r<   rj   )r   r?   s   "r   r   r@      s      S D r   c                   . pV'       d   V.MV P                  R4      pV F  pV P                  V4       V P                  V P                  V4      4      pVP	                  4        Fj  w  rg\        \        P                  ! 4       VR,          ,
          ^<,          4      pVP                  RVRVRVR,          RVP                  RR4      RV/4       Kl  	  K  	  V# )	z?List pending pairing requests, optionally filtered by platform.r   r   r=   r   rb   rx   r   age_minutes)	rm   r   rW   rD   rn   intr|   ro   r   )	r8   r=   rp   rq   rr   r   r   rt   age_mins	   &&       r   list_pendingPairingStore.list_pending   s    "*XJ0C0CI0N	A!!!$ood&8&8&;<G%mmo
tyy{T,-??2EFDtI+r!:!7   .  r   c                &   < V ^8  d   QhRS[ RS[/# r<   )r   r   )r   r?   s   "r   r   r@      s     	 	c 	S 	r   c                \   V P                   ;_uu_ 4        ^ pV'       d   V.MV P                  R4      pV FV  pV P                  V P                  V4      4      pV\	        V4      ,          pV P                  V P                  V4      / 4       KX  	  RRR4       V#   + '       g   i     X# ; i)z2Clear all pending requests. Returns count removed.r   N)r6   rm   rW   rD   r   r^   )r8   r=   countrq   rr   r   s   &&    r   clear_pendingPairingStore.clear_pending   s    ZZZE&.
D4G4G	4RI//$*<*<Q*?@W% 2 21 5r:    Z s   A;BB+	c                ,   < V ^8  d   QhRS[ RS[ RS[/# ra   rc   )r   r?   s   "r   r   r@      s'     A A As At Ar   c                    V P                  V P                  4       4      pV RV 2pVP                  V^ 4      p\        P                  ! 4       V,
          \        8  # )z2Check if a user has requested a code too recently.:)rW   rL   r   r|   RATE_LIMIT_SECONDS)r8   r=   rb   limitskeylast_requests   &&&   r   r   PairingStore._is_rate_limited   sP    !6!6!89
!G9%zz#q)		l*.@@@r   c                *   < V ^8  d   QhRS[ RS[ RR/# )r
   r=   rb   r   Nry   )r   r?   s   "r   r   r@     s"     9 93 9 9 9r   c                    V P                  V P                  4       4      pV RV 2p\        P                  ! 4       W4&   V P                  V P                  4       V4       R# )z7Record the time of a pairing request for rate limiting.r   N)rW   rL   r|   r^   )r8   r=   rb   r   r   s   &&&  r   r   PairingStore._record_rate_limit  sM    !6!6!89
!G9%iik--/8r   c                &   < V ^8  d   QhRS[ RS[/# r<   rc   )r   r?   s   "r   r   r@     s     + +s +t +r   c                    V P                  V P                  4       4      pRV 2pVP                  V^ 4      p\        P                  ! 4       V8  # )zBCheck if a platform is in lockout due to failed approval attempts.	_lockout:)rW   rL   r   r|   )r8   r=   r   lockout_keylockout_untils   &&   r   r   PairingStore._is_locked_out  sF    !6!6!89!(,

;2yy{]**r   c                $   < V ^8  d   QhRS[ RR/# r
   r=   r   Nry   )r   r?   s   "r   r   r@     s     9 9s 9t 9r   c           	     r   V P                  V P                  4       4      pRV 2pVP                  V^ 4      ^,           pWBV&   V\        8  dK   RV 2p\        P                  ! 4       \
        ,           W%&   ^ W#&   \        RV R\
         R\         R2RR7       V P                  V P                  4       V4       R	# )
zMRecord a failed approval attempt. Triggers lockout after MAX_FAILED_ATTEMPTS.z
_failures:r   z[pairing] Platform z locked out for zs after z failed attemptsT)r$   N)rW   rL   r   MAX_FAILED_ATTEMPTSr|   LOCKOUT_SECONDSprintr^   )r8   r=   r   fail_keyfailsr   s   &&    r   r   #PairingStore._record_failed_attempt  s    !6!6!89z*

8Q'!+ x''%hZ0K"&))+"?F F'z1A/AR S.//?AHLN--/8r   c                $   < V ^8  d   QhRS[ RR/# r   ry   )r   r?   s   "r   r   r@     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  rVWFR,          ,
          \        8  g   K  VNK#  	  pppV'       d   V F  pW5 K  	  V P                  W#4       R# R# u uppi )zRemove expired pending codes.r   N)rD   rW   r|   rn   CODE_TTL_SECONDSr^   )r8   r=   r   r   nowr   rt   expireds   &&      r   r   PairingStore._cleanup_expired  s    !!(+//$'iik#*==?
#2ZT<((,<< D? 	 
 M  OOD* 	
s   B,Bc                &   < V ^8  d   QhRS[ RS[/# )r
   r   r   rj   )r   r?   s   "r   r   r@   -  s      S T r   c                "   . p\         P                  4        Ft  pVP                  P                  RV R24      '       g   K*  VP                  P	                  RV R2R4      pVP                  R4      '       d   Kc  VP                  V4       Kv  	  V# )z:List all platforms that have data files of a given suffix.-z.jsonr   r   )r3   iterdirnameendswithr'   
startswithro   )r8   r   rq   r.   r=   s   &&   r   rm   PairingStore._all_platforms-  sz    	$$&Avv6(%01166>>AfXU*;R@**3//$$X.	 '
 r   )r6   r   )r   )__name__
__module____qualname____firstlineno____doc__r9   rD   rH   rL   rW   r^   rg   ru   r}   r   r   r   r   r   r   r   r   r   r   rm   __static_attributes____classdictcell__)r?   s   @r   r1   r1   K   s     '8 89 91 1 L L
# #
 A A	 	) )V 4 $	 	A A9 9+ +9 9 + + r   r1   )r   rS   r!   r   r   r4   r|   pathlibr   typingr   hermes_constantsr   r   r   r   r   r   r   r   r3   r/   r1    r   r   <module>r      sr   (  	       + .      0)<4j jr   