11import { Command } from "commander" ;
2- import { createWalletClient , createPublicClient , http , parseAbiItem } from "viem" ;
2+ import { createWalletClient , createPublicClient , http , parseAbiItem , toHex } from "viem" ;
33import type { Abi } from "viem" ;
44import { privateKeyToAccount } from "viem/accounts" ;
55import { sepolia , foundry } from "viem/chains" ;
66import { importAbi } from "@/importAbi.js" ;
77import packageJson from "../package.json" with { type : "json" } ;
88import { exit } from "node:process" ;
9- import { parseJwt } from "./oidc.js" ;
9+ import { existsSync , mkdirSync , writeFileSync } from "node:fs" ;
10+ import { dirname } from "node:path" ;
11+ import { b64urlToHex , jwtKid , parseJwt } from "./oidc.js" ;
1012
1113const COMMAND_NAME = "fcf" ;
1214const COMMAND_DESCRIPTION = "FCF CLI tool."
1315const VERSION = packageJson ?. version || "v0.0.1" ;
16+ const GITHUB_ISSUER = "https://token.actions.githubusercontent.com" ;
17+ const REGISTER_WORKFLOW_PATH = ".github/workflows/fcf-register.yml" ;
18+ const REGISTER_WORKFLOW = `name: Register Repository
19+
20+ on:
21+ workflow_dispatch:
22+
23+ permissions:
24+ contents: read
25+ id-token: write
26+
27+ jobs:
28+ register:
29+ runs-on: ubuntu-latest
30+ steps:
31+ - uses: actions/checkout@v6
32+
33+ - uses: actions/setup-node@v6
34+ with:
35+ node-version: 24
36+
37+ - name: Register repository
38+ env:
39+ PRIVATE_KEY: \${{ secrets.FCF_PRIVATE_KEY }}
40+ RPC_URL: \${{ vars.FCF_RPC_URL }}
41+ FCF_CONTRACT: \${{ vars.FCF_CONTRACT }}
42+ run: |
43+ npx --yes @freecodexyz/cli register \\
44+ --contract "$FCF_CONTRACT"
45+ ` ;
1446
1547function die ( err : any ) : never {
1648 let error = "unknown error" ;
@@ -51,17 +83,54 @@ function clients() {
5183
5284}
5385
86+ async function requestGithubOidcToken ( audience : string ) : Promise < string > {
87+ const requestUrl = process . env . ACTIONS_ID_TOKEN_REQUEST_URL ;
88+ const requestToken = process . env . ACTIONS_ID_TOKEN_REQUEST_TOKEN ;
89+ if ( ! requestUrl || ! requestToken ) die ( new Error ( "GitHub OIDC env vars not found" ) ) ;
90+
91+ const separator = requestUrl . includes ( "?" ) ? "&" : "?" ;
92+ // include aud in payload -> aud == eth address of signer
93+ const res = await fetch ( `${ requestUrl } ${ separator } audience=${ encodeURIComponent ( audience ) } ` , {
94+ headers : { authorization : `bearer ${ requestToken } ` } ,
95+ } ) ;
96+ if ( ! res . ok ) die ( new Error ( `failed to fetch GitHub OIDC token: ${ res . status } ` ) ) ;
97+
98+ const body = await res . json ( ) as { value ?: string } ;
99+ if ( ! body . value ) die ( new Error ( "GitHub OIDC response did not contain token" ) ) ;
100+ return body . value ;
101+ }
102+
103+ async function fetchJson ( url : string ) : Promise < any > {
104+ const res = await fetch ( url ) ;
105+ if ( ! res . ok ) die ( new Error ( `request failed: ${ url } (${ res . status } )` ) ) ;
106+ return res . json ( ) ;
107+ }
108+
109+ program
110+ . command ( "init" )
111+ . option ( "--force" , "overwrite existing workflow" )
112+ . action ( ( opts ) => {
113+ if ( existsSync ( REGISTER_WORKFLOW_PATH ) && ! opts . force ) {
114+ die ( new Error ( `${ REGISTER_WORKFLOW_PATH } already exists` ) ) ;
115+ }
116+
117+ mkdirSync ( dirname ( REGISTER_WORKFLOW_PATH ) , { recursive : true } ) ;
118+ writeFileSync ( REGISTER_WORKFLOW_PATH , REGISTER_WORKFLOW ) ;
119+ console . log ( `created: ${ REGISTER_WORKFLOW_PATH } ` ) ;
120+ } ) ;
121+
54122program
55123 . command ( "register" )
56- . requiredOption ( "--oidcToken <token>" , "GitHub's OIDC repository token" )
124+ . option ( "--oidc-token <token>" , "GitHub's OIDC repository token" )
57125 // needs to be removed in prod or be a default
58126 . requiredOption ( "--contract <addr>" , "deployed RIK address" )
59127 . action ( async ( opts ) => {
60128 const { wallet, publicC, account } = clients ( ) ;
129+ const oidcToken = opts . oidcToken ?? await requestGithubOidcToken ( account . address . toLowerCase ( ) ) ;
61130
62131 const jwt = ( ( ) => {
63132 try {
64- return parseJwt ( opts . oidcToken ) ;
133+ return parseJwt ( oidcToken ) ;
65134 } catch ( err ) {
66135 die ( err ) ;
67136 }
@@ -73,18 +142,52 @@ program
73142 }
74143 const repoId = BigInt ( jwt . payload . repository_id ) ;
75144 const ownerId = BigInt ( jwt . payload . repository_owner_id ) ;
145+ if ( ! jwt . header . kid ) die ( new Error ( "jwt kid missing" ) ) ;
76146
77147 const hash = await wallet . writeContract ( {
78148 address : opts . contract as `0x${string } `,
79149 abi,
80150 functionName : "register" ,
81- args : [ repoId , ownerId ] ,
151+ args : [
152+ jwtKid ( jwt . header . kid ) ,
153+ toHex ( jwt . headerB64 ) ,
154+ toHex ( jwt . payloadB64 ) ,
155+ b64urlToHex ( jwt . signatureB64 ) ,
156+ repoId ,
157+ ownerId ,
158+ ] ,
82159 account,
83160 } ) ;
84161 const r = await publicC . waitForTransactionReceipt ( { hash } ) ;
85162 console . log ( `registered: ${ hash } status=${ r . status } ` ) ;
86163 } ) ;
87164
165+ program
166+ . command ( "keys" )
167+ . command ( "sync" )
168+ // needs to be removed in prod or be a default
169+ . requiredOption ( "--contract <addr>" , "deployed RIK address" )
170+ . action ( async ( opts ) => {
171+ const { wallet, publicC, account } = clients ( ) ;
172+ const config = await fetchJson ( `${ GITHUB_ISSUER } /.well-known/openid-configuration` ) ;
173+ const jwks = await fetchJson ( config . jwks_uri ) ;
174+
175+ for ( const key of jwks . keys ?? [ ] ) {
176+ if ( key . kty !== "RSA" || ! key . kid || ! key . n || ! key . e ) continue ;
177+
178+ const kid = jwtKid ( key . kid ) ;
179+ const hash = await wallet . writeContract ( {
180+ address : opts . contract as `0x${string } `,
181+ abi,
182+ functionName : "addKey" ,
183+ args : [ kid , b64urlToHex ( key . n ) , b64urlToHex ( key . e ) ] ,
184+ account,
185+ } ) ;
186+ const r = await publicC . waitForTransactionReceipt ( { hash } ) ;
187+ console . log ( `key synced: ${ key . kid } kid=${ kid } status=${ r . status } ` ) ;
188+ }
189+ } ) ;
190+
88191program
89192 . command ( "list" )
90193 // needs to be removed in prod or be a default
@@ -107,4 +210,4 @@ program
107210 }
108211 } ) ;
109212
110- program . parseAsync ( process . argv ) ;
213+ program . parseAsync ( process . argv ) ;
0 commit comments