Diffinery

codician/diffinerypublic

Open-source, agent-first GitHub and GitHub Actions alternative that runs on your own Cloudflare account: git hosting, pull requests, code review and drop-in GitHub Actions YAML on Workers, Durable Objects, R2 and Containers.

50 lines · 2.8 KBRaw History
1import { b64url, fromB64url } from "./util.js";
2
3interface Jwk extends JsonWebKey {
4 kid?: string;
5}
6
7const certCache = new Map<string, { keys: Jwk[]; at: number }>();
8
9/** Verify a Cloudflare Access JWT (RS256) and return its claims. */
10export async function verifyAccessJwt(token: string, teamDomain: string, aud: string): Promise<Record<string, unknown>> {
11 const [h, p, s] = token.split(".");
12 if (!h || !p || !s) throw new Error("malformed JWT");
13 const header = JSON.parse(new TextDecoder().decode(fromB64url(h))) as { kid?: string; alg?: string };
14 if (header.alg !== "RS256") throw new Error("unexpected alg");
15 const certsUrl = `https://${teamDomain}/cdn-cgi/access/certs`;
16 let cached = certCache.get(certsUrl);
17 if (!cached || Date.now() - cached.at > 10 * 60_000 || !cached.keys.some((k) => k.kid === header.kid)) {
18 const res = await fetch(certsUrl);
19 if (!res.ok) throw new Error(`failed to fetch Access certs: ${res.status}`);
20 cached = { keys: ((await res.json()) as { keys: Jwk[] }).keys, at: Date.now() };
21 certCache.set(certsUrl, cached);
22 }
23 const jwk = cached.keys.find((k) => k.kid === header.kid);
24 if (!jwk) throw new Error("unknown signing key");
25 const key = await crypto.subtle.importKey("jwk", jwk, { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, false, ["verify"]);
26 const ok = await crypto.subtle.verify("RSASSA-PKCS1-v1_5", key, fromB64url(s) as BufferSource, new TextEncoder().encode(`${h}.${p}`));
27 if (!ok) throw new Error("bad signature");
28 const claims = JSON.parse(new TextDecoder().decode(fromB64url(p))) as Record<string, unknown>;
29 const now = Math.floor(Date.now() / 1000);
30 if (typeof claims.exp === "number" && claims.exp < now) throw new Error("token expired");
31 const auds = Array.isArray(claims.aud) ? claims.aud : [claims.aud];
32 if (!auds.includes(aud)) throw new Error("audience mismatch");
33 if (claims.iss !== `https://${teamDomain}`) throw new Error("issuer mismatch");
34 return claims;
35}
36
37/** Sign an ES256 JWT with a private JWK (used for job OIDC tokens). */
38export async function signEs256(privateJwk: Jwk, claims: Record<string, unknown>): Promise<string> {
39 const key = await crypto.subtle.importKey("jwk", privateJwk, { name: "ECDSA", namedCurve: "P-256" }, false, ["sign"]);
40 const enc = (o: unknown) => b64url(new TextEncoder().encode(JSON.stringify(o)));
41 const head = enc({ alg: "ES256", typ: "JWT", kid: privateJwk.kid ?? "diffinery-1" });
42 const body = enc(claims);
43 const sig = new Uint8Array(await crypto.subtle.sign({ name: "ECDSA", hash: "SHA-256" }, key, new TextEncoder().encode(`${head}.${body}`)));
44 return `${head}.${body}.${b64url(sig)}`;
45}
46
47export function publicJwk(privateJwk: Jwk): Jwk {
48 const { d: _d, key_ops: _ops, ...pub } = privateJwk;
49 return { ...pub, use: "sig", alg: "ES256", kid: privateJwk.kid ?? "diffinery-1" };
50}