import * as Effect from "effect/Effect"; import { loadControlPlaneConfig, loadControlPlaneOrigin, } from "../configuration/control-plane.ts"; import { requiredSecret } from "../configuration/secrets.ts"; import type { Bindings } from "../configuration/validation.ts"; import { decode, DOMAIN_HEALTH_PURPOSE, encode, HEALTH_PURPOSE, LOGIN_PURPOSE, opaque, random, type BridgeClaims, } from "../shared/bridge.ts"; import { customDomainOrigin } from "../shared/domain-origin.ts"; import type { Env } from "./config.ts"; import { hash } from "./crypto.ts"; import { OAuthError } from "./errors.ts"; import { form, privateResponse } from "./http-response.ts"; import { type Installation } from "./installation-metadata.ts"; import { beginOAuth } from "./oauth-authorization.ts"; import { installationRegistry as registry } from "./registry-client.ts"; import { authenticatedPrincipal, vault } from "./session.ts"; import { bodyJson, withResponse } from "../server/http.ts"; import type { BridgeRequest } from "./vault.ts"; const denied = () => new OAuthError("forbidden"); function owned(env: Env, subject: string, id: string) { return Effect.gen(function* () { const record = yield* registry(env, subject).get(subject, id); // Identity remains useful after deployment-grant expiry, including failed upgrades. if (!record || !record.installedRelease || !record.resources.runtimeOrigin) return yield* Effect.fail(denied()); return record; }); } function allowedAudience( env: Env, subject: string, id: string, requested?: string, ) { return Effect.gen(function* () { const record = yield* owned(env, subject, id); const management = record.resources.runtimeOrigin!; const audience = requested ?? management; if (audience === management) return { record, audience }; if (customDomainOrigin(audience) !== audience) return yield* Effect.fail(denied()); const domain = yield* registry(env, subject).getDomain(subject, id); if (!domain || domain.status !== "active" || domain.origin !== audience) return yield* Effect.fail(denied()); return { record, audience }; }); } export function healthDomain( env: Env, record: Installation, origin: string, network: typeof fetch = fetch, ) { return Effect.gen(function* () { if (customDomainOrigin(origin) !== origin) return yield* Effect.fail(denied()); const state = random(); const challenge = random(); const assertion = yield* signBridgeAssertion(env, { aud: origin, sub: record.ownerSubject, installationId: record.installationId, purpose: DOMAIN_HEALTH_PURPOSE, state, challenge, }); const result = yield* runtimeJson( network, new URL("/auth/domain-health", origin), assertion, 10_000, ); 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) ) return yield* Effect.fail(denied()); }); } export function signBridgeAssertion( env: Env, claims: Omit, ) { return Effect.gen(function* () { const config = loadControlPlaneConfig(env).config; if (!config.bridge) return yield* Effect.fail(denied()); const bridge = config.bridge; const privateKey = requiredSecret( env as unknown as Bindings, "FLAREBOT_BRIDGE_SIGNING_KEY", 1, ).reveal(); const key = yield* Effect.promise(() => 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: 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( yield* Effect.promise(() => crypto.subtle.sign("Ed25519", key, new TextEncoder().encode(input)), ), ); // Fail setup when the deployed secret does not match its deliberately pinned key. const publicKey = yield* Effect.promise(() => crypto.subtle.importKey( "raw", decode(bridge.publicKey), { name: "Ed25519" }, false, ["verify"], ), ); if ( !(yield* Effect.promise(() => crypto.subtle.verify( "Ed25519", publicKey, signature, new TextEncoder().encode(input), ), )) ) return yield* Effect.fail(denied()); return `${input}.${encode(signature)}`; }).pipe(Effect.catchDefect(() => denied())); } function issueCode(env: Env, subject: string, input: BridgeRequest) { return Effect.gen(function* () { if (input.expiresAt <= Date.now()) return yield* Effect.fail(denied()); const { audience } = yield* allowedAudience( env, subject, input.installationId, input.audience, ); const code = random(); yield* vault(env, "code", yield* 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 function resumeBridge( env: Env, subject: string, ref: string, bindingHash: string, ) { return Effect.gen(function* () { const input = yield* vault(env, "continuation", ref).claimContinuation( bindingHash, ); if (!input) return yield* Effect.fail(denied()); return yield* 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 function handleBridge(request: Request, env: Env) { return Effect.gen(function* () { const url = new URL(request.url); if (!["/auth/bridge", "/auth/bridge/exchange"].includes(url.pathname)) return null; if (url.origin !== loadControlPlaneOrigin(env) || request.url.length > 2048) return yield* Effect.fail(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") ) return yield* Effect.fail(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) ) return yield* Effect.fail(denied()); const pending = { ...input, expiresAt: Date.now() + 600_000, } as BridgeRequest; const principal = yield* authenticatedPrincipal(request, env).pipe( Effect.catch((error) => error.code === "reauthorization_required" ? Effect.succeed(null) : Effect.fail(error), ), ); if (!principal) return yield* beginOAuth(request, env, pending); return privateResponse( new Response(null, { status: 303, headers: { Location: yield* 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") ) return yield* Effect.fail(denied()); const input = exact(yield* form(request), [ "code", "verifier", "installationId", "audience", "state", ]); if ( !opaque(input.code) || !opaque(input.verifier) || !opaque(input.state) || !/^[a-f0-9]{32}$/.test(input.installationId) ) return yield* Effect.fail(denied()); const challenge = yield* hash(input.verifier); const code = yield* vault(env, "code", yield* hash(input.code)).claimCode( { installationId: input.installationId, audience: input.audience, state: input.state, challenge, }, ); if (!code) return yield* Effect.fail(denied()); yield* allowedAudience( env, code.subject, code.installationId, code.audience, ); const assertion = yield* 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 })); } return yield* Effect.fail(denied()); }).pipe( Effect.catch(() => bridgeDenied()), Effect.catchDefect(() => bridgeDenied()), ); } // Tokens stay inside this callback; callers may persist only successful metadata. export function healthInstallation( env: Env, record: Installation, network: typeof fetch = fetch, deployedOperationId: string | null = record.operationId, ) { return Effect.gen(function* () { if ( !record.resources.runtimeOrigin || !record.desiredRelease || !deployedOperationId ) return yield* Effect.fail(denied()); const state = random(); const challenge = random(); const release = record.desiredRelease; const assertion = yield* 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 result = yield* runtimeJson( network, new URL("/auth/bootstrap-health", record.resources.runtimeOrigin), assertion, 60_000, ); 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) ) return yield* Effect.fail(denied()); }); } function runtimeJson( network: typeof fetch, url: URL, assertion: string, timeout: number, ) { return withResponse( network, url, { method: "POST", headers: { Authorization: `Bearer ${assertion}` } }, timeout, denied(), (response) => Effect.gen(function* () { if (!response.ok) return yield* denied(); const value = yield* bodyJson(response, 4096, denied()); if (!value || typeof value !== "object" || Array.isArray(value)) return yield* denied(); return value as Record; }), ); } const bridgeDenied = () => Effect.succeed( privateResponse(Response.json({ error: "bridge_denied" }, { status: 403 })), );