import { loadControlPlaneConfig, loadControlPlaneOrigin, } from "../configuration/control-plane.ts"; import { requiredSecret } from "../configuration/secrets.ts"; import type { Bindings } from "../configuration/validation.ts"; import { encode, decode, hash, random, opaque, LOGIN_PURPOSE, HEALTH_PURPOSE, DOMAIN_HEALTH_PURPOSE, type BridgeClaims, } from "../shared/bridge.ts"; import { customDomainOrigin } from "../shared/domain-origin.ts"; import type { Env } from "./config.ts"; import { OAuthError } from "./errors.ts"; import { authenticatedPrincipal, vault } from "./session.ts"; import { registryName } from "./installation-registry.ts"; import { unwrap, type Installation } from "./installation-metadata.ts"; import type { BridgeRequest } from "./vault.ts"; import { beginOAuth, form, privateResponse } from "./http.ts"; import { domainJson } from "./domain-api.ts"; const denied = () => new OAuthError("forbidden"); const registry = (env: Env, subject: string) => env.INSTALLATIONS.get(env.INSTALLATIONS.idFromName(registryName(subject))); async function owned(env: Env, subject: string, id: string) { const record = unwrap(await registry(env, subject).get(subject, id)); // Identity remains useful after deployment-grant expiry, including failed upgrades. if (!record || !record.installedRelease || !record.resources.runtimeOrigin) throw denied(); return record; } async function allowedAudience( env: Env, subject: string, id: string, requested?: string, ) { const record = await owned(env, subject, id); const management = record.resources.runtimeOrigin!; const audience = requested ?? management; if (audience === management) return { record, audience }; if (customDomainOrigin(audience) !== audience) throw denied(); const domain = unwrap(await registry(env, subject).getDomain(subject, id)); if (!domain || domain.status !== "active" || domain.origin !== audience) throw denied(); return { record, audience }; } export async function healthDomain( env: Env, record: Installation, origin: string, network: typeof fetch = fetch, ) { if (customDomainOrigin(origin) !== origin) throw denied(); const state = random(); const challenge = random(); const assertion = await signBridgeAssertion(env, { aud: origin, sub: record.ownerSubject, installationId: record.installationId, purpose: DOMAIN_HEALTH_PURPOSE, state, challenge, }); const response = await network( new Request(new URL("/auth/domain-health", origin), { method: "POST", redirect: "manual", signal: AbortSignal.timeout(10_000), headers: { Authorization: `Bearer ${assertion}` }, }), ); if (!response.ok || Number(response.headers.get("Content-Length")) > 4096) throw denied(); const result = (await domainJson(response)) as Record; const expected = { installationId: record.installationId, origin, state, challenge, }; if ( Object.keys(result).sort().join() !== Object.keys(expected).sort().join() || Object.entries(expected).some(([key, value]) => result[key] !== value) ) throw denied(); } export async function signBridgeAssertion( env: Env, claims: Omit, ) { const config = loadControlPlaneConfig(env).config; if (!config.bridge) throw denied(); const privateKey = requiredSecret( env as unknown as Bindings, "FLAREBOT_BRIDGE_SIGNING_KEY", 1, ).reveal(); const key = await crypto.subtle.importKey( "pkcs8", decode(privateKey), { name: "Ed25519" }, false, ["sign"], ); const now = Math.floor(Date.now() / 1000); const header = encode( new TextEncoder().encode( JSON.stringify({ alg: "EdDSA", kid: config.bridge.keyId, typ: "JWT" }), ), ); const body = encode( new TextEncoder().encode( JSON.stringify({ ...claims, iss: config.publicOrigin, iat: now, exp: now + 60, jti: random(), }), ), ); const input = `${header}.${body}`; const signature = new Uint8Array( await crypto.subtle.sign("Ed25519", key, new TextEncoder().encode(input)), ); // Fail setup when the deployed secret does not match its deliberately pinned key. const publicKey = await crypto.subtle.importKey( "raw", decode(config.bridge.publicKey), { name: "Ed25519" }, false, ["verify"], ); if ( !(await crypto.subtle.verify( "Ed25519", publicKey, signature, new TextEncoder().encode(input), )) ) throw denied(); return `${input}.${encode(signature)}`; } async function issueCode(env: Env, subject: string, input: BridgeRequest) { if (input.expiresAt <= Date.now()) throw denied(); const { audience } = await allowedAudience( env, subject, input.installationId, input.audience, ); const code = random(); await vault(env, "code", await hash(code)).createCode({ ...input, subject, audience, expiresAt: Date.now() + 60_000, }); const callback = new URL("/auth/callback", audience); callback.search = new URLSearchParams({ code, state: input.state, }).toString(); return callback.href; } export async function resumeBridge( env: Env, subject: string, ref: string, bindingHash: string, ) { const input = await vault(env, "continuation", ref).claimContinuation( bindingHash, ); if (!input) throw denied(); return issueCode(env, subject, input); } function exact(params: URLSearchParams, names: string[]) { if ( [...params.keys()].length !== names.length || names.some((name) => params.getAll(name).length !== 1) ) throw denied(); return Object.fromEntries(names.map((name) => [name, params.get(name)!])); } export async function handleBridge( request: Request, env: Env, ): Promise { const url = new URL(request.url); if (!["/auth/bridge", "/auth/bridge/exchange"].includes(url.pathname)) return null; try { if (url.origin !== loadControlPlaneOrigin(env) || request.url.length > 2048) throw denied(); if (url.pathname === "/auth/bridge" && request.method === "GET") { if ( request.headers.has("Upgrade") || (request.headers.has("Sec-Fetch-Mode") && request.headers.get("Sec-Fetch-Mode") !== "navigate") ) throw denied(); const names = url.searchParams.has("audience") ? ["installationId", "state", "challenge", "audience"] : ["installationId", "state", "challenge"]; const input = exact(url.searchParams, names); if ( !/^[a-f0-9]{32}$/.test(input.installationId) || !opaque(input.state) || !opaque(input.challenge) ) throw denied(); const pending = { ...input, expiresAt: Date.now() + 600_000, } as BridgeRequest; let principal; try { principal = await authenticatedPrincipal(request, env); } catch (error) { if ( !(error instanceof OAuthError) || error.code !== "reauthorization_required" ) throw error; return beginOAuth(request, env, pending); } return privateResponse( new Response(null, { status: 303, headers: { Location: await issueCode(env, principal.subject, pending), }, }), ); } if ( url.pathname === "/auth/bridge/exchange" && request.method === "POST" && !url.search ) { // Server exchange is deliberately separate from browser-origin mutations. if ( request.headers.has("Origin") || request.headers.has("Cookie") || request.headers.has("Sec-Fetch-Site") ) throw denied(); const input = exact(await form(request), [ "code", "verifier", "installationId", "audience", "state", ]); if ( !opaque(input.code) || !opaque(input.verifier) || !opaque(input.state) || !/^[a-f0-9]{32}$/.test(input.installationId) ) throw denied(); const challenge = await hash(input.verifier); const code = await vault(env, "code", await hash(input.code)).claimCode({ installationId: input.installationId, audience: input.audience, state: input.state, challenge, }); if (!code) throw denied(); await allowedAudience( env, code.subject, code.installationId, code.audience, ); const assertion = await signBridgeAssertion(env, { aud: code.audience, sub: code.subject, installationId: code.installationId, purpose: LOGIN_PURPOSE, state: code.state, challenge: code.challenge, }); return privateResponse(Response.json({ assertion })); } throw denied(); } catch { return privateResponse( Response.json({ error: "bridge_denied" }, { status: 403 }), ); } } // Tokens stay inside this callback; callers may persist only successful metadata. export async function healthInstallation( env: Env, record: Installation, network: typeof fetch = fetch, deployedOperationId: string | null = record.operationId, ): Promise { if ( !record.resources.runtimeOrigin || !record.desiredRelease || !deployedOperationId ) throw denied(); const state = random(); const challenge = random(); const release = record.desiredRelease; const assertion = await signBridgeAssertion(env, { aud: record.resources.runtimeOrigin, sub: record.ownerSubject, installationId: record.installationId, purpose: HEALTH_PURPOSE, state, challenge, operationId: deployedOperationId, artifactDigest: release.artifactDigest, version: release.version, }); const response = await network( new Request( new URL("/auth/bootstrap-health", record.resources.runtimeOrigin), { method: "POST", redirect: "manual", signal: AbortSignal.timeout(60_000), headers: { Authorization: `Bearer ${assertion}` }, }, ), ); if (!response.ok || Number(response.headers.get("Content-Length")) > 4096) throw denied(); const reader = response.body?.getReader(); if (!reader) throw denied(); let body = ""; let size = 0; const decoder = new TextDecoder(); while (true) { const chunk = await reader.read(); if (chunk.done) break; size += chunk.value.byteLength; if (size > 4096) { await reader.cancel(); throw denied(); } body += decoder.decode(chunk.value, { stream: true }); } body += decoder.decode(); const result = JSON.parse(body); const expected = { installationId: record.installationId, operationId: deployedOperationId, artifactDigest: release.artifactDigest, version: release.version, state, challenge, identity: "ready", nativeParent: "ready", sandbox: "booted-and-destroyed", bindings: "present", assets: "ready", authentication: "required", }; if ( JSON.stringify(Object.keys(result).sort()) !== JSON.stringify(Object.keys(expected).sort()) || Object.entries(expected).some(([key, value]) => result[key] !== value) ) throw denied(); }