import { createPrivateKey, createPublicKey, sign as edSign, verify as edVerify } from "node:crypto"; import { SANDBOX_ID } from "../ids/tunnel-ids.js"; // A sandbox proves identity with a signed grant; a hostname's embedded id resolves ownership by parsing, not a registry // lookup. Platform, ingress and daemon must agree on every string here. Routing is per h2 stream, since one TLS // connection can carry multiple sandboxes' hosts. // The reachability grant // Version prefix: an ingress that doesn't recognize it refuses the tunnel rather than negotiating a shape. const GRANT_PREFIX = "ig1"; const base64url = (bytes: Buffer): string => bytes.toString("base64url"); // Signed claim; wire fields are `sub` (12-hex sandbox id) and `iat` (seconds). No expiry: the grant lives for a // container's whole life, and revocation is deleting the sandbox row. export interface ReachabilityGrant { readonly sandboxId: string; readonly issuedAt: number; } // Mints a grant: an Ed25519 signature over the JSON `{sub, iat}` payload, with a PEM/PKCS8 private key. export const mintReachabilityGrant = (privateKeyPem: string, sandboxId: string, issuedAtMs: number): string => { if (!SANDBOX_ID.test(sandboxId)) { throw new Error(`a reachability grant names a 12-hex sandbox id, got "${sandboxId}"`); } const payload = Buffer.from(JSON.stringify({ sub: sandboxId, iat: Math.floor(issuedAtMs / 1000) }), "utf8"); const signature = edSign(null, payload, createPrivateKey(privateKeyPem)); return `${GRANT_PREFIX}.${base64url(payload)}.${base64url(signature)}`; }; // Verifies a grant against the platform's public key; any malformed shape returns undefined instead of throwing. export const verifyReachabilityGrant = (publicKeyPem: string, token: string): ReachabilityGrant | undefined => { const parts = token.split("."); if (parts.length !== 3 || parts[0] !== GRANT_PREFIX) { return undefined; } try { const payload = Buffer.from(parts[1] as string, "base64url"); const signature = Buffer.from(parts[2] as string, "base64url"); if (!edVerify(null, payload, createPublicKey(publicKeyPem), signature)) { return undefined; } const parsed = JSON.parse(payload.toString("utf8")) as { sub?: unknown; iat?: unknown }; if (typeof parsed.sub !== "string" || !SANDBOX_ID.test(parsed.sub) || typeof parsed.iat !== "number") { return undefined; } return { sandboxId: parsed.sub, issuedAt: parsed.iat }; } catch { return undefined; } }; // Host to owner routing // Which sandbox owns a Host, declared with the names it parses (ids/hostnames.ts) and re-exported for this subpath's // callers: the leftmost DNS label is `sandbox-` or ends in `-` (12-hex). export { hostOwnerId } from "../ids/hostnames.js"; // Wire constants // Tunnel door on the ingress (the tunnel crate's TUNNEL_PATH); versioned, so a new session shape adds a path. `/tunnel/v1` // is the edge's alone to answer now, for netd versions that predate this one. export const INGRESS_TUNNEL_PATH = "/tunnel/v2"; // Grant travels as a header on the tunnel upgrade; this connection is never a browser's. export const INGRESS_GRANT_HEADER = "x-intentic-grant"; // Env vars every lane hands the daemon; the daemon reads them directly, not the entrypoint. export const ENV_INGRESS_URL = "INGRESS_URL"; export const ENV_SANDBOX_GRANT = "SANDBOX_GRANT"; // What the edge (_platform/ingress, Rust) must also honor, held by its suites and by ingress-contract.fixture.json: // - Register: verify the grant offline; if PLATFORM_URL is set, check the sandbox exists (GET /api/reachability/) // and cache the answer, failing open if the platform doesn't respond. A 404 refuses the tunnel. // - Displacement: a new tunnel for an id closes the old session (code 4001) and takes the registration. // - Streams: every exchange is a stream of its own carrying HTTP/1.1 (yamux over a WebSocket, or QUIC). Over TCP a // transfer rides the bulk socket: a preview's request, or a route netd announced (tunnel-bulk.ts); over QUIC a // stream that sends a megabyte without pausing yields to the others. // - Liveness: netd pings every 15s; a peer silent for 45s is unregistered. // - Routing: host maps via hostOwnerId to a registered tunnel; no tunnel answers 502 naming the sandbox label, with // the verdict header and CORS of edge-verdict.ts, and admits the preflight of the request it is about to refuse. // - The tunnel door and any host without a sandbox id are served directly by the ingress, never routed. // - `/health` lists the tunnel doors the build serves (`"doors":["/tunnel/v1","/tunnel/v2"]`); a sandbox image moves // onto a tag sandboxes pull only once the live edge lists INGRESS_TUNNEL_PATH (_tools/scripts/image/require-edge-door.sh).