Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
12 kB · 402 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403import * 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<BridgeClaims, "iat" | "exp" | "jti" | "iss">,) { 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<string, unknown>; }), );}
const bridgeDenied = () => Effect.succeed( privateResponse(Response.json({ error: "bridge_denied" }, { status: 403 })), );