Skip to content

Commit 75a072c

Browse files
committed
(feat) implement JWT parsing and error handling in CLI commands
1 parent 667a4c9 commit 75a072c

5 files changed

Lines changed: 68 additions & 8 deletions

File tree

cli/src/index.ts

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,25 @@ import { sepolia, foundry } from "viem/chains";
66
import { importAbi } from "@/importAbi.js";
77
import packageJson from "../package.json" with { type: "json" };
88
import { exit } from "node:process";
9+
import { parseJwt } from "./oidc.js";
910

1011
const COMMAND_NAME = "fcf";
1112
const COMMAND_DESCRIPTION = "FCF CLI tool."
1213
const VERSION = packageJson?.version || "v0.0.1";
1314

15+
function die(err: any): never {
16+
let error = "unknown error";
17+
if (err instanceof Error) error = err.message;
18+
console.error(`${COMMAND_NAME}: ${error}; exiting.`)
19+
exit(1);
20+
}
21+
1422
// import abi -> created by forge build
1523
let abi: Abi | null = null;
1624
try {
1725
abi = importAbi();
1826
} catch (err) {
19-
let error = "unknown error";
20-
if (err instanceof Error) error = err.message;
21-
console.error(`${COMMAND_NAME}: ${error}; exiting.`)
22-
exit(1);
27+
die(err);
2328
}
2429

2530
const RepoRegisteredEvent = parseAbiItem(
@@ -48,17 +53,32 @@ function clients() {
4853

4954
program
5055
.command("register")
51-
.requiredOption("--repo-id <n>", "GitHub repository_id")
52-
.requiredOption("--owner-id <n>", "GitHub owner numeric id")
56+
.requiredOption("--oidcToken <token>", "GitHub's OIDC repository token")
5357
// needs to be removed in prod or be a default
5458
.requiredOption("--contract <addr>", "deployed RIK address")
5559
.action(async (opts) => {
5660
const { wallet, publicC, account } = clients();
61+
62+
const jwt = (() => {
63+
try {
64+
return parseJwt(opts.oidcToken);
65+
} catch(err) {
66+
die(err);
67+
}
68+
})();
69+
70+
const expectedAud = account.address.toLowerCase();
71+
if (jwt.payload.aud?.toLowerCase() !== expectedAud) {
72+
die(new Error(`aud mismatch: want ${expectedAud}, got ${jwt.payload.aud}`));
73+
}
74+
const repoId = BigInt(jwt.payload.repository_id);
75+
const ownerId = BigInt(jwt.payload.repository_owner_id);
76+
5777
const hash = await wallet.writeContract({
5878
address: opts.contract as `0x${string}`,
5979
abi,
6080
functionName: "register",
61-
args: [BigInt(opts.repoId), BigInt(opts.ownerId)],
81+
args: [repoId, ownerId],
6282
account,
6383
});
6484
const r = await publicC.waitForTransactionReceipt({ hash });

cli/src/oidc.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
function b64urlDecode(s: string): Buffer {
2+
s = s.replace(/-/g, "+").replace(/_/g, "/");
3+
while (s.length % 4) s += "=";
4+
return Buffer.from(s, "base64");
5+
}
6+
7+
export type JwtParts = {
8+
headerB64: string; payloadB64: string; signatureB64: string;
9+
payload: any;
10+
}
11+
12+
export function parseJwt(jwt: string): JwtParts {
13+
const [headerB64, payloadB64, signatureB64] = jwt.split(".");
14+
if (!payloadB64 || !headerB64 || !signatureB64) throw Error("Failed to decode jwt");
15+
const payload = JSON.parse(b64urlDecode(payloadB64).toString("utf-8"));
16+
return { headerB64, payloadB64, signatureB64, payload };
17+
}

cli/test/oidc.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { test, expect } from "vitest";
2+
import { parseJwt } from "@/oidc.js";
3+
4+
test("parses GitHub OIDC payload", () => {
5+
// real (but expired) token
6+
const jwt = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6IjB0WVZjTSJ9" +
7+
".eyJyZXBvc2l0b3J5X2lkIjoiMTIzNDUiLCJyZXBvc2l0b3J5X293bmV" +
8+
"yX2lkIjoiOTk5IiwiYXVkIjoiMHhmMzlmIn0.AAAA";
9+
10+
const p = parseJwt(jwt).payload;
11+
expect(p.repository_id).toBe("12345");
12+
expect(p.repository_owner_id).toBe("999");
13+
});

cli/tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
// know bug -> https://github.com/egoist/tsup/issues/1389
55
"ignoreDeprecations": "6.0",
66
// File Layout
7-
"rootDir": "./src",
7+
"rootDir": ".",
88
"outDir": "./dist",
99
"paths": {
1010
"@/*": ["./src/*"]

cli/vitest.config.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { fileURLToPath } from "node:url";
2+
import { defineConfig } from "vitest/config";
3+
4+
export default defineConfig({
5+
resolve: {
6+
alias: {
7+
"@": fileURLToPath(new URL("./src", import.meta.url)),
8+
},
9+
},
10+
});

0 commit comments

Comments
 (0)