// Shared wire protocol only; customer/browser graphs never import CP modules. export const LOGIN_PURPOSE = "flarebot-owner-login" as const; export const HEALTH_PURPOSE = "flarebot-bootstrap-health" as const; export const PROVIDER_PURPOSE = "flarebot-provider-enabled" as const; export const encode = (bytes: Uint8Array) => btoa(String.fromCharCode(...bytes)) .replaceAll("+", "-") .replaceAll("/", "_") .replaceAll("=", ""); export function decode(value: string): Uint8Array { if (!/^[A-Za-z0-9_-]+$/.test(value)) throw new Error("Invalid bridge encoding"); return Uint8Array.from( atob(value.replaceAll("-", "+").replaceAll("_", "/")), (c) => c.charCodeAt(0), ); } export const random = () => encode(crypto.getRandomValues(new Uint8Array(32))); export const opaque = (value: unknown): value is string => typeof value === "string" && /^[A-Za-z0-9_-]{43}$/.test(value); export const hash = async (value: string) => encode( new Uint8Array( await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value)), ), ); export interface BridgeClaims { iss: string; aud: string; sub: string; installationId: string; purpose: typeof LOGIN_PURPOSE | typeof HEALTH_PURPOSE | typeof PROVIDER_PURPOSE; iat: number; exp: number; jti: string; state: string; challenge: string; operationId?: string; artifactDigest?: string; version?: string; } export async function verifyAssertion( token: string, key: { keyId: string; publicKey: string }, expected: Omit, ): Promise { if (token.length > 4096) throw new Error("Invalid bridge assertion"); const parts = token.split("."); if (parts.length !== 3) throw new Error("Invalid bridge assertion"); const header = JSON.parse(new TextDecoder().decode(decode(parts[0]))); if ( Object.keys(header).sort().join(",") !== "alg,kid,typ" || header.alg !== "EdDSA" || header.typ !== "JWT" || header.kid !== key.keyId ) throw new Error("Invalid bridge key"); const publicKey = await crypto.subtle.importKey( "raw", decode(key.publicKey), { name: "Ed25519" }, false, ["verify"], ); if ( !(await crypto.subtle.verify( "Ed25519", publicKey, decode(parts[2]), new TextEncoder().encode(`${parts[0]}.${parts[1]}`), )) ) throw new Error("Invalid bridge signature"); const claims = JSON.parse( new TextDecoder().decode(decode(parts[1])), ) as BridgeClaims; const now = Math.floor(Date.now() / 1000); const keys = [...Object.keys(expected), "iat", "exp", "jti"].sort(); if ( Object.keys(claims).sort().join(",") !== keys.join(",") || !Number.isSafeInteger(claims.iat) || !Number.isSafeInteger(claims.exp) || claims.iat > now || claims.exp <= now || claims.exp <= claims.iat || claims.exp - claims.iat > 60 || !opaque(claims.jti) ) throw new Error("Invalid bridge claims"); for (const [name, value] of Object.entries(expected)) if (claims[name as keyof BridgeClaims] !== value) throw new Error("Invalid bridge binding"); return claims; }