
    <j2                    ^   d Z ddlm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Z ed      Zedz  Zedz  Zed	z  Zd
ZddZddZef	 	 	 	 	 ddZdd	 	 	 	 	 	 	 ddZdd	 	 	 	 	 	 	 ddZddZdd	 	 	 	 	 	 	 	 	 	 	 ddZdd dZedk(  r e e             y)!u   refresh_bot_token.py — GitHub App installation token refresh (50-min cycle).

회장 박제 명세 feedback_github_app_key_location_260507.md 준수.
stdlib + PyJWT + cryptography 만 사용 (requests/httpx 금지).
    )annotationsN)Path)Optionalz/home/jay/workspacez=.secrets/jeon-jonghyuk-taskctl-bot.2026-05-05.private-key.pemz2memory/orchestration-audit/bot-token-refresh.jsonlz	.env.keyszhttps://api.github.comc                   i }| j                         s|S 	 | j                  d      j                         D ]  }|j                         }|r|j	                  d      r'|j	                  d      r|t        d      d j                         }d|vrY|j                  dd      \  }}|j                         }|j                         j                  d      j                  d	      }|s|||<    	 |S # t        $ r Y |S w xY w)
u4   라인별 NAME=VALUE 파싱. 'export ' prefix strip.utf-8encoding#zexport N=   "')exists	read_text
splitlinesstrip
startswithlensplit	Exception)env_pathresultraw_linelinekvs         J/home/jay/workspace/.worktrees/task-2551-dev6/scripts/refresh_bot_token.py_parse_env_keysr   %   s    F?? **G*<GGI 	H>>#D4??3/y)C	NO,224$::c1%DAq	A	$**3/Aq		  M  Ms   CC/ %C/ /	C<;C<c                    t         j                  j                  |       }|r|S t        |      }|j                  |       S )u.   os.environ 우선, 없으면 .env.keys 파싱.)osenvirongetr   )keyenv_keys_pathvalparseds       r   _getenvr'   >   s4    
**..
C

]+F::c?    c           	     "   t        d      }g }| r|j                  t        |              |j                  |       |j                  |       |D ]  }|j                         s|c S  t        d|D cg c]  }t	        |       c}       c c}w )uw   env BOT_GITHUB_PRIVATE_KEY_PATH 우선 → 부재 시 fallback.
    둘 다 부재(파일 없음)면 FileNotFoundError.zG/home/jay/.secrets/jeon-jonghyuk-taskctl-bot.2026-05-05.private-key.pemzPEM key not found. Tried: )r   appendr   FileNotFoundErrorstr)r   fallback_pathmain_pem
candidatespcs         r   resolve_pem_pathr2   L   s     ]^HJ$x.)hm$ 88:H 
$j%Ac!f%A$BC %As   1B
)nowc                   |t        t        j                               }|dz
  |dz   | d}t        j                  ||d      S )uD   RS256, iat=now-60 (시계 보정), exp=now+540 (9분). PyJWT 사용.<   i  )iatexpissRS256)	algorithm)inttimejwtencode)app_id	pem_bytesr3   payloads       r   generate_jwtrB   d   sF     {$))+RxSyG
 ::gyG<<r(   g      $@timeoutc          
        t          d| d}t        j                  j                  |ddd|  dddd	
      }	 t        j                  j	                  ||      5 }|j                         j                  d      }t        j                  |      cddd       S # 1 sw Y   yxY w# t        j                  j                  $ r=}|j                         j                  dd      }t        |j                  |      |d}~ww xY w)u   POST /app/installations/{id}/access_tokens.
    200 → {"token": ..., "expires_at": ...}.
    비-200 → raise RuntimeError(status, body).
    z/app/installations/z/access_tokensPOSTr(   zBearer zapplication/vnd.github+jsonz
2022-11-280)AuthorizationAcceptzX-GitHub-Api-VersionzContent-Length)methoddataheadersrC   r   Nreplace)errors)GITHUB_API_BASEurllibrequestRequesturlopenreaddecodejsonloadserror	HTTPErrorRuntimeErrorcode)	jwt_tokeninstallation_idrD   urlreqrespbodyexcs           r   request_installation_tokenrc   u   s     00A
PC
..
 
 &yk23$0!	
	 ! 
C4^^##C#9 	$T99;%%g.D::d#	$ 	$ 	$ <<!! 4xxz   ;388T*34s5   !B$ 4B	B$ B!B$ !B$ $C>8C99C>c                   d}| j                         r0| j                  d      }| j                         j                  dz  }nd}d}|j	                  d      }d	}g }|D ]=  }|j                  |      r|j                  | | d
       d}-|j                  |       ? |s=|r%|d   j                  d
      s|j                  d
       |j                  | | d
       dj                  |      }	| j                  j                  dd       t        j                  | j                  d      \  }
}d	}	 t        j                  |
dd      5 }d}|j                  |	       ddd       t        j                   ||       t        j"                  ||        y# 1 sw Y   6xY w# t$        $ rO |s&	 t        j&                  |
       n# t(        $ r Y nw xY w	 t        j*                  |        # t(        $ r Y  w xY ww xY w)u   라인이 'BOT_GITHUB_TOKEN='로 시작하면 in-place 교체.
    미존재 시 EOF append. atomic (tmpfile + os.replace). chmod 0o600 보존.
    export prefix 추가 X.
    zBOT_GITHUB_TOKEN=r   r   i   i  T)keependsF
parentsexist_okz.env.keys.tmp.)dirprefixwN)r   r   statst_moder   r   r*   endswithjoinparentmkdirtempfilemkstempr    fdopenwritechmodrM   r   closeOSErrorunlink)r   	new_tokentarget_prefixoriginal_textoriginal_modelinesreplaced	new_linesr   new_textfdtmp_path	fd_closedfs                 r   update_env_keysr      s   
 (M  **G*< //%7$$d$3EHI #??=)yk<=HT"# Yr]33D9T"M?9+R89wwy!H
 OO$6##OO$4LB IYYr31 	QIGGH	 	=)


8X&		 	
  
 	IIh 	  		
sl   3F  F4F  FF   G8,GG8	GG8GG8G('G8(	G41G83G44G8)rX   c               L   | j                   j                  dd       t        j                  dt        j                               |||d}|||d<   t        j                  |d      d	z   }| j                  d
d      5 }|j                  |       ddd       y# 1 sw Y   yxY w)u   jsonl append-only.
    status ∈ {refreshed, refresh_rejected, pem_missing, api_error, dry_run}.
    토큰 원문 절대 기록 X.
    sha256_prefix는 hashlib.sha256(token.encode()).hexdigest()[:16].
    Tri   z%Y-%m-%dT%H:%M:%SZ)tsstatussha256_prefix
expires_atNrX   Fensure_asciirg   ar   r   )	rs   rt   r<   strftimegmtimerV   dumpsopenrx   )
audit_pathr   r   r   rX   recordr   r   s           r   append_auditr      s     D48mm0$++-@& 	F w::f51D8D	w	/ 1	  s   ?BB#c                p   t        j                  d      }|j                  ddd       |j                  dt        t              dt         d	
       |j                  dt        t
              dt
         d	
       |j                  |       }t        |j                        }t        |j                        }|j                  }t        d|      }t        d|      }t        d|      }|st        dd       t        |dddd       y|st        dd       t        |dddd       y	 t        |      }	|	j                         }
	 t#        ||
      }	 t'        ||      }|j/                  d&d'      }|j/                  d(d'      }|st        d)d       t        |dddd*       yt1        j2                  |j5                               j7                         dd+ }|dd, }|r:t        |d-||.       d-|||dd/}t        t9        j:                  |d01      d       y2	 t=        ||       t        |d5||.       d5|||d0d/}t        t9        j:                  |d01      d       y2# t         $ r3}t        d| d       t        |dddt        |             Y d}~yd}~ww xY w# t$        $ r-}t        d| d       t        |dddd|        Y d}~yd}~ww xY w# t(        $ rc}|j*                  \  }}t        d| d|dd   d       d!t-        |      cxk  rd"k  rn nd#nd}t        ||ddd$| d|dd          Y d}~yd}~wt$        $ r3}t        d%| d       t        |dddt        |             Y d}~yd}~ww xY w# t$        $ r-}t        d3| d       t        |d||d4|        Y d}~yd}~ww xY w)6u:   CLI entry. exit 0=성공, 1=실패. --dry-run flag 지원.z%GitHub App installation token refresh)descriptionz	--dry-run
store_trueu,   토큰 발급만 수행, .env.keys 미수정)actionhelpz
--env-keysu   .env.keys 경로 (기본값: ))defaultr   z--audit-pathu   audit jsonl 경로 (기본값: BOT_GITHUB_APP_IDBOT_GITHUB_INSTALLATION_IDBOT_GITHUB_PRIVATE_KEY_PATHu#   [ERROR] BOT_GITHUB_APP_ID 미설정T)flush	api_errorNzBOT_GITHUB_APP_ID missing)r   r   r   rX   r   u,   [ERROR] BOT_GITHUB_INSTALLATION_ID 미설정z"BOT_GITHUB_INSTALLATION_ID missingu   [ERROR] PEM 파일 없음: pem_missingu   [ERROR] JWT 생성 실패: zjwt_error: u   [ERROR] GitHub API 오류 HTTP z:    i  i  refresh_rejectedhttp_u   [ERROR] API 호출 실패: tokenre   r   u%   [ERROR] 응답에 token 필드 없음ztoken field missing in response      dry_run)r   r   r   )r   token_prefixr   r   r   Fr   r   u!   [ERROR] .env.keys 갱신 실패: zenv_keys_write_error: 	refreshed)argparseArgumentParseradd_argumentr,   DEFAULT_ENV_KEYS_PATHDEFAULT_AUDIT_PATH
parse_argsr   env_keysr   r   r'   printr   r2   
read_bytesr+   rB   r   rc   rZ   argsr;   r"   hashlibsha256r>   	hexdigestrV   r   r   )argvparserr   r$   r   r   r?   r]   pem_env_pathpem_pathr@   rb   r\   r   status_codera   audit_statusr}   r   r   r   outputs                         r   mainr      s8   $$;F ;  
 )*,-B,C1E  
 &'./A.B!D  
 T"D'Mdoo&JLLG (-8F:MJO8-HL34@-	
 <DI6	
 #L1'')	 3	+IG2 

7B'IL"-J5TB3	
 NN9#3#3#56@@B3BGMSb>L '!		
  ($*
 	djje4DAy1 #	 $ &F 
$**V%
0=i  	+C51> c(	
 	  	+C51>u%	
 	  HHT/}BtDSzlKSWX-0C4D-Js-J)P[+bds5	
  	+C51>c(	
 	d  	1#7tD'!*3%0	
 	sm   )I J K M? 	J')JJ	K&#KK	M<AL==M<	)M77M<?	N5#N00N5__main__)r   r   returnzdict[str, str])r#   r,   r$   r   r   Optional[str])r   r   r-   r   r   r   )r?   r,   r@   bytesr3   zOptional[int]r   r,   )r\   r,   r]   r,   rD   floatr   dict)r   r   r}   r,   r   None)r   r   r   r,   r   r   r   r   rX   r   r   r   )N)r   zOptional[list[str]]r   r;   ) __doc__
__future__r   r   r   rV   r    ru   r<   urllib.errorrP   urllib.requestpathlibr   typingr   r=   	WORKSPACEDEFAULT_PEM_BACKUPr   r   rO   r   r'   r2   rB   rc   r   r   r   __name__
SystemExit r(   r   <module>r      s]   #    	       
 &'	!`` !UU !K/ *2  - 
8 	=== 
	=
 	=* 	444 	4
 
4>:F    !	
   
@m` z
TV
 r(   