import { afterAll, beforeAll, test } from "bun:test"; import assert from "node:assert/strict"; import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { createSqliteDb, openDatabase } from "../../src/runtime/bun/sqlite"; import { serve } from "../../src/runtime/bun/server"; import { provisionWith, TEST_SECRET_CIPHER } from "../contract/harness"; import type { WebhookSecurity } from "../../src/core/webhook-security"; /** * Signed webhook delivery against a real HTTP receiver over a real socket. * * The contract suite proves a delivery row is queued. This proves the request * actually leaves the process, arrives with a signature the receiver can verify * independently, and that a rejecting receiver is recorded as a failure rather * than silently dropped. */ const directory = mkdtempSync(join(tmpdir(), "titen-hook-")); const dbPath = join(directory, "titen.db"); const SECRET = "webhook-shared-secret-value"; interface Received { signature: string | null; signatureV2: string | null; timestamp: string | null; event: string | null; delivery: string | null; body: string; } let titen: Awaited>; let receiver: { stop: () => void; port: number }; let received: Received[] = []; let rejectNext = false; let key: string; let transportMode: "deliver" | "unreachable" | "timeout" | "redirect" = "deliver"; let resolvedAddresses = ["93.184.216.34"]; const WEBHOOK_HOST = "hooks.example.com"; /** Independent verification: recompute the HMAC the way an integrator would. */ async function hmacHex(secret: string, payload: string): Promise { const imported = await crypto.subtle.importKey( "raw", new TextEncoder().encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"], ); const mac = await crypto.subtle.sign("HMAC", imported, new TextEncoder().encode(payload)); return [...new Uint8Array(mac)].map((b) => b.toString(16).padStart(2, "0")).join(""); } async function call(method: string, path: string, body?: unknown) { const res = await fetch(`${titen.url}${path}`, { method, headers: { "content-type": "application/json", authorization: `Bearer ${key}` }, body: body === undefined ? undefined : JSON.stringify(body), }); return { status: res.status, body: (await res.json()) as any }; } beforeAll(async () => { const webhookSecurity: WebhookSecurity = { allowedHostnames: [WEBHOOK_HOST], timeoutMs: 30, resolve: async () => resolvedAddresses, async dispatch({ url, addresses, init }) { if (transportMode === "unreachable") throw new Error("unreachable"); if (transportMode === "redirect") return { response: new Response(null, { status: 302, headers: { location: "http://169.254.169.254/" } }), connectedAddress: addresses[0]!, }; if (transportMode === "timeout") await new Promise((_, reject) => { init.signal?.addEventListener("abort", () => reject(init.signal?.reason), { once: true }); }); const destination = new URL(url); return { response: await fetch(`http://127.0.0.1:${receiver.port}${destination.pathname}`, init), connectedAddress: addresses[0]!, }; }, }; titen = await serve({ dbPath, port: 0, hostname: "127.0.0.1", quiet: true, revision: "hook", maintenanceIntervalMs: 0, secretCipher: TEST_SECRET_CIPHER, webhookSecurity, }); const provisioned = await provisionWith(createSqliteDb(openDatabase(dbPath)), { scopes: ["*"] }); key = provisioned.key; // @ts-ignore - Bun global is provided by the runtime. const server = Bun.serve({ port: 0, hostname: "127.0.0.1", async fetch(request: Request) { received.push({ signature: request.headers.get("x-titen-signature"), signatureV2: request.headers.get("x-titen-signature-v2"), timestamp: request.headers.get("x-titen-timestamp"), event: request.headers.get("x-titen-event"), delivery: request.headers.get("x-titen-delivery"), body: await request.text(), }); return new Response(rejectNext ? "no" : "ok", { status: rejectNext ? 500 : 200 }); }, }); assert.ok(server.port, "the HTTP receiver must have a TCP port"); receiver = { stop: () => server.stop(true), port: server.port }; }); afterAll(async () => { receiver.stop(); await titen.stop(); rmSync(directory, { recursive: true, force: true }); }); test("a delivered event arrives signed, and the signature verifies independently", async () => { received = []; rejectNext = false; transportMode = "deliver"; resolvedAddresses = ["93.184.216.34"]; const hook = await call("POST", "/v1/webhooks", { url: `https://${WEBHOOK_HOST}/sink`, secret: SECRET, events: ["*"], }); assert.equal(hook.status, 201); assert.deepEqual(hook.body.data.signing.v2, { header: "X-Titen-Signature-V2", timestamp_header: "X-Titen-Timestamp", delivery_header: "X-Titen-Delivery", signed_content: "..", algorithm: "HMAC-SHA256", format: "sha256=", tolerance_seconds: 300, }); // Any canonical write emits an event, which is what gets delivered. const observation = await call("POST", "/v1/observations", { subject_id: "user_hook", kind: "tool_result", content: "Webhook delivery integration evidence.", source: { type: "tool", ref: "hook#1" }, trust: "verified", }); assert.equal(observation.status, 201); const drained = await call("POST", "/v1/webhooks/deliver", {}); assert.equal(drained.status, 200); assert.ok(drained.body.data.events_drained >= 1); assert.ok(received.length >= 1, "the receiver must have been contacted over HTTP"); const first = received[0]!; assert.equal(first.event, "observation.appended"); assert.ok(first.delivery?.startsWith("dlv_"), "a delivery id must identify the attempt"); assert.match(first.signature ?? "", /^sha256=[0-9a-f]{64}$/); // The receiver can authenticate the payload without trusting the sender. const expected = await hmacHex(SECRET, first.body); assert.equal( first.signature, `sha256=${expected}`, "the signature must verify against the shared secret and the exact body", ); // A tampered body must not verify, which is the property the header exists for. const tampered = await hmacHex(SECRET, `${first.body} `); assert.notEqual(first.signature, `sha256=${tampered}`); // The timestamped signature binds the attempt time and the delivery id, so a // receiver can reject an old capture and drop a duplicate delivery. assert.match(first.timestamp ?? "", /^\d+$/); assert.ok(Math.abs(Number(first.timestamp) - Date.now() / 1000) < 300, "the timestamp must be current"); assert.equal( first.signatureV2, `sha256=${await hmacHex(SECRET, `${first.timestamp}.${first.delivery}.${first.body}`)}`, "the V2 signature must verify against the timestamp, the delivery id, and the exact body", ); assert.notEqual( first.signatureV2, `sha256=${await hmacHex(SECRET, `${Number(first.timestamp) + 1}.${first.delivery}.${first.body}`)}`, ); const payload = JSON.parse(first.body); assert.ok(payload, "the body must be the JSON event payload"); assert.ok( !first.body.includes(SECRET) && !first.body.includes("titen_sk_"), "a delivery must carry no credential", ); }); test("a rejecting receiver is recorded as a failure and scheduled for retry", async () => { received = []; rejectNext = true; transportMode = "deliver"; const hook = await call("POST", "/v1/webhooks", { url: `https://${WEBHOOK_HOST}/failing`, secret: SECRET, events: ["claim.materialized"], }); assert.equal(hook.status, 201); const hookId = hook.body.data.webhook_id as string; const observation = await call("POST", "/v1/observations", { subject_id: "user_hook_fail", kind: "tool_result", content: "Evidence for a delivery that will be refused.", source: { type: "tool", ref: "hook#2" }, trust: "verified", }); await call("POST", "/v1/consolidations", { subject_id: "user_hook_fail", claims: [ { kind: "procedural", statement: "A refused delivery must be retried, not lost.", sources: [ { observation_id: observation.body.data.observation_id, relation: "supports" }, ], }, ], }); const drain = await call("POST", "/v1/webhooks/deliver", {}); assert.ok(received.length >= 1, "the failing receiver must still have been contacted"); const deliveries = await call("GET", `/v1/webhooks/${hookId}/deliveries`, undefined); assert.equal(deliveries.status, 200); const attempted = deliveries.body.data.deliveries; assert.ok(attempted.length >= 1, "the attempt must be recorded"); const record = attempted[0]; assert.match( record.event_id, /^evt_/, "a delivery must identify the event it attempted, not its kind", ); assert.equal(record.response_status, 500, "the receiver's status must be stored"); assert.equal(record.attempts, 1); assert.ok(record.next_retry_at, "a failed delivery must be scheduled for retry"); // oldest_retry_at is the minimum over every pending delivery of this principal. // Retry times now come from each attempt's own finish time, so another hook's // delivery in the same pass can be a few milliseconds earlier, never later. const oldestRetry = Date.parse(drain.body.data.oldest_retry_at); assert.ok(oldestRetry <= Date.parse(record.next_retry_at), "the oldest retry is not after this one"); assert.ok(Date.parse(record.next_retry_at) - oldestRetry < 10_000, "both retries come from the same pass"); assert.equal(record.status, "pending", "it stays pending until attempts are exhausted"); rejectNext = false; }); test("an unreachable destination fails without taking down the request", async () => { received = []; transportMode = "unreachable"; const hook = await call("POST", "/v1/webhooks", { url: `https://${WEBHOOK_HOST}/unreachable`, secret: SECRET, events: ["*"], }); assert.equal(hook.status, 201); const hookId = hook.body.data.webhook_id as string; await call("POST", "/v1/observations", { subject_id: "user_hook_unreachable", kind: "tool_result", content: "Evidence whose delivery target does not answer.", source: { type: "tool", ref: "hook#3" }, trust: "verified", }); const drained = await call("POST", "/v1/webhooks/deliver", {}); assert.equal(drained.status, 200, "a network failure must not fail the drain request"); const deliveries = await call("GET", `/v1/webhooks/${hookId}/deliveries`, undefined); assert.ok(deliveries.body.data.deliveries.length >= 1, "the attempt is still recorded"); assert.equal( deliveries.body.data.deliveries[0].response_status, null, "no status exists when the connection never completed", ); }); test("a second event of the same kind is also delivered", async () => { // The regression this file exists to prevent. Delivery dedup once matched on // event kind, so the first observation.appended was sent and every later one was // silently skipped forever. received = []; rejectNext = false; transportMode = "deliver"; const hook = await call("POST", "/v1/webhooks", { url: `https://${WEBHOOK_HOST}/repeat`, secret: SECRET, events: ["observation.appended"], }); assert.equal(hook.status, 201); const hookId = hook.body.data.webhook_id as string; const write = async (marker: string) => { const res = await call("POST", "/v1/observations", { subject_id: "user_hook_repeat", kind: "tool_result", content: `Repeat delivery evidence ${marker}.`, source: { type: "tool", ref: `repeat#${marker}` }, trust: "verified", }); assert.equal(res.status, 201); return res.body.data.observation_id as string; }; await write("first"); await call("POST", "/v1/webhooks/deliver", {}); const afterFirst = ( await call("GET", `/v1/webhooks/${hookId}/deliveries`, undefined) ).body.data.deliveries.length; assert.ok(afterFirst >= 1, "the first event of this kind must be delivered"); await write("second"); await call("POST", "/v1/webhooks/deliver", {}); const afterSecond = ( await call("GET", `/v1/webhooks/${hookId}/deliveries`, undefined) ).body.data.deliveries; assert.ok( afterSecond.length > afterFirst, `a later event of the same kind must also be delivered; had ${afterFirst}, now ${afterSecond.length}`, ); // Each attempt names a distinct event, which is what makes dedup correct. const ids = afterSecond.map((d: any) => d.event_id); assert.equal(new Set(ids).size, ids.length, "each delivery must target one event"); }); test("draining twice does not deliver the same event twice", async () => { received = []; rejectNext = false; transportMode = "deliver"; const hook = await call("POST", "/v1/webhooks", { url: `https://${WEBHOOK_HOST}/once`, secret: SECRET, events: ["*"], }); const hookId = hook.body.data.webhook_id as string; await call("POST", "/v1/observations", { subject_id: "user_hook_once", kind: "tool_result", content: "Evidence that must be delivered exactly once.", source: { type: "tool", ref: "once#1" }, trust: "verified", }); await call("POST", "/v1/webhooks/deliver", {}); const first = (await call("GET", `/v1/webhooks/${hookId}/deliveries`, undefined)).body.data .deliveries.length; await call("POST", "/v1/webhooks/deliver", {}); const second = (await call("GET", `/v1/webhooks/${hookId}/deliveries`, undefined)).body.data .deliveries.length; assert.equal(second, first, "a repeated drain must not re-deliver an event"); });