@secondlayer/subgraphs 4.2.0 → 4.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,19 +1,20 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../src/runtime/emitter.ts", "../src/runtime/formats/index.ts", "../src/runtime/formats/cloudevents.ts", "../src/runtime/formats/cloudflare.ts", "../src/runtime/formats/inngest.ts", "../src/runtime/formats/raw.ts", "../src/runtime/formats/standard-webhooks.ts", "../src/runtime/formats/trigger.ts", "../src/runtime/subscription-state.ts", "../src/runtime/emitter-matcher.ts"],
3
+ "sources": ["../src/runtime/emitter.ts", "../src/runtime/formats/index.ts", "../src/runtime/formats/cloudevents.ts", "../src/runtime/formats/cloudflare.ts", "../src/runtime/formats/inngest.ts", "../src/runtime/formats/raw.ts", "../src/runtime/formats/standard-webhooks.ts", "../src/runtime/formats/trigger.ts", "../src/runtime/hosted-meter.ts", "../src/runtime/webhook-state.ts", "../src/runtime/emitter-matcher.ts"],
4
4
  "sourcesContent": [
5
- "import { randomUUID } from \"node:crypto\";\nimport { lookup as dnsLookup } from \"node:dns/promises\";\nimport {\n\ttype Database,\n\ttype Subscription,\n\ttype SubscriptionOutbox,\n\tgetTargetDb,\n} from \"@secondlayer/shared/db\";\nimport { getSubscriptionSigningSecret } from \"@secondlayer/shared/db/queries/subscriptions\";\nimport { logger } from \"@secondlayer/shared/logger\";\nimport { listen, targetListenerUrl } from \"@secondlayer/shared/queue/listener\";\nimport type { SubscriptionTestResult } from \"@secondlayer/shared/schemas/subscriptions\";\nimport { type Kysely, sql } from \"kysely\";\nimport { buildForFormat } from \"./formats/index.ts\";\nimport { refreshMatcher } from \"./subscription-state.ts\";\n\n/**\n * Subscription emitter — drains `subscription_outbox` and POSTs deliveries.\n *\n * Hot path: LISTEN on `subscriptions:new_outbox` and `subscriptions:changed`.\n * On notify, claim a batch with `FOR UPDATE SKIP LOCKED LIMIT 50`, dispatch\n * each row via HTTP, write a `subscription_deliveries` attempt row, then\n * either mark `status='delivered'` or schedule the next attempt.\n *\n * Backoff schedule (attempt → wait):\n * 0 → 30s, 1 → 2m, 2 → 10m, 3 → 1h, 4 → 6h, 5 → 24h, 6 → 72h.\n * After `max_retries` (default 7) attempts → `status='dead'`.\n *\n * Per-sub circuit breaker: 20 consecutive failures → sub flipped to\n * `paused` with `circuit_opened_at=NOW()`. Manual /resume drains backlog.\n *\n * Per-sub concurrency cap: in-memory semaphore, default 4 in-flight HTTP\n * requests per subscription. Sprint-4 adds SSRF allowlist.\n */\n\nconst BATCH_SIZE = 50;\nconst LIVE_SHARE = 0.9; // 90% of batch to non-replay, 10% to replay\nconst BACKOFF_SECONDS = [30, 120, 600, 3600, 21600, 86400, 259200];\nconst CIRCUIT_THRESHOLD = 20;\n/**\n * When a batch is claimed the outbox row's `next_attempt_at` is pushed\n * `LOCK_WINDOW_MS` into the future. Any crash between claim + settle\n * leaves the row re-claimable after this window expires — the SSOT for\n * double-dispatch prevention.\n *\n * Must exceed the maximum possible in-flight delivery time so a slow-but-alive\n * receiver's row is never re-claimed mid-delivery (duplicate dispatch). The max\n * subscription timeout is 300_000ms (see shared/schemas/subscriptions.ts:307).\n */\nexport const MAX_SUBSCRIPTION_TIMEOUT_MS = 300_000;\nexport const LOCK_WINDOW_MS: number = MAX_SUBSCRIPTION_TIMEOUT_MS + 60_000; // 6 min: max timeout + settle margin\n\ninterface RunningState {\n\trunning: boolean;\n\tinFlightBySub: Map<string, number>;\n\tclaimInFlight: boolean;\n}\n\nfunction nextDelaySeconds(attempt: number): number {\n\t// biome-ignore lint/style/noNonNullAssertion: value is non-null after preceding check or by construction; TS narrowing limitation\n\treturn BACKOFF_SECONDS[Math.min(attempt, BACKOFF_SECONDS.length - 1)]!;\n}\n\n// ── SSRF guard ────────────────────────────────────────────────────────\n// Block deliveries to private/loopback/link-local ranges unless\n// SECONDLAYER_ALLOW_PRIVATE_EGRESS=true (self-host + local-dev opt-in).\n\nconst PRIVATE_V4_PATTERNS = [\n\t/^127\\./, // loopback\n\t/^10\\./, // private class A\n\t/^172\\.(1[6-9]|2\\d|3[01])\\./, // private class B\n\t/^192\\.168\\./, // private class C\n\t/^169\\.254\\./, // link-local\n\t/^0\\./, // \"this\" network\n\t/^100\\.(6[4-9]|[7-9]\\d|1[01]\\d|12[0-7])\\./, // CGNAT 100.64/10\n];\n\n/**\n * Classify a raw IP address literal (v4 or v6, no brackets, already lowercased\n * or not) as private/loopback/link-local/reserved. Covers v4 private ranges +\n * link-local (incl. `169.254.169.254`), `0.0.0.0`, IPv6 loopback (`::1`),\n * unspecified (`::`), unique-local (`fc00::/7`), link-local (`fe80::/10`), and\n * IPv4-mapped IPv6 (`::ffff:127.0.0.1`, `::ffff:7f00:0001`).\n *\n * Shared by `isPrivateEgress` (literal-hostname fast-fail) and the resolved-\n * DNS-address check in `checkEgressAllowed` (rebinding mitigation).\n */\nfunction isPrivateIp(address: string): boolean {\n\tconst host = address.toLowerCase();\n\n\tif (host === \"0.0.0.0\") return true;\n\tif (host === \"::\" || host === \"::1\") return true;\n\t// Unique-local (fc00::/7) + link-local (fe80::/10)\n\tif (/^f[cd][0-9a-f]{2}:/.test(host)) return true;\n\tif (/^fe[89ab][0-9a-f]:/.test(host)) return true;\n\n\t// IPv4-mapped IPv6 — `::ffff:127.0.0.1` or `::ffff:7f00:0001`\n\tconst mapped = host.match(/^::ffff:(.+)$/);\n\tif (mapped) {\n\t\t// biome-ignore lint/style/noNonNullAssertion: value is non-null after preceding check or by construction; TS narrowing limitation\n\t\tconst inner = mapped[1]!;\n\t\t// Dotted form: rerun v4 checks.\n\t\tif (/^\\d+\\.\\d+\\.\\d+\\.\\d+$/.test(inner)) {\n\t\t\tfor (const p of PRIVATE_V4_PATTERNS) if (p.test(inner)) return true;\n\t\t}\n\t\t// Hex form: 7f00:0001 → 127.0.0.1\n\t\tconst hex = inner.match(/^([0-9a-f]{1,4}):([0-9a-f]{1,4})$/);\n\t\tif (hex) {\n\t\t\t// biome-ignore lint/style/noNonNullAssertion: value is non-null after preceding check or by construction; TS narrowing limitation\n\t\t\tconst a = Number.parseInt(hex[1]!, 16);\n\t\t\t// biome-ignore lint/style/noNonNullAssertion: value is non-null after preceding check or by construction; TS narrowing limitation\n\t\t\tconst b = Number.parseInt(hex[2]!, 16);\n\t\t\tconst dotted = `${(a >> 8) & 0xff}.${a & 0xff}.${(b >> 8) & 0xff}.${b & 0xff}`;\n\t\t\tfor (const p of PRIVATE_V4_PATTERNS) if (p.test(dotted)) return true;\n\t\t}\n\t\treturn false;\n\t}\n\n\tfor (const p of PRIVATE_V4_PATTERNS) {\n\t\tif (p.test(host)) return true;\n\t}\n\treturn false;\n}\n\n/**\n * Reject hostnames spelled as private IPs (cheap, synchronous, no I/O — a\n * fast-fail pre-filter). DNS-level rebinding (hostname that *resolves* to a\n * private IP at egress time) is NOT caught here — see `checkEgressAllowed`,\n * which resolves DNS and validates every returned address.\n */\nfunction isPrivateEgress(url: string): boolean {\n\tlet parsed: URL;\n\ttry {\n\t\tparsed = new URL(url);\n\t} catch {\n\t\treturn true; // malformed URL: reject\n\t}\n\tif (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\") {\n\t\treturn true;\n\t}\n\n\t// Strip brackets from IPv6 literals.\n\tconst raw = parsed.hostname.toLowerCase();\n\tconst host =\n\t\traw.startsWith(\"[\") && raw.endsWith(\"]\") ? raw.slice(1, -1) : raw;\n\n\tif (host === \"localhost\") return true;\n\treturn isPrivateIp(host);\n}\n\nfunction allowPrivateEgress(): boolean {\n\treturn process.env.SECONDLAYER_ALLOW_PRIVATE_EGRESS === \"true\";\n}\n\ninterface ResolvedAddress {\n\taddress: string;\n\tfamily: number;\n}\n\ntype DnsLookupFn = (hostname: string) => Promise<ResolvedAddress[]>;\n\nasync function defaultDnsLookup(hostname: string): Promise<ResolvedAddress[]> {\n\treturn dnsLookup(hostname, { all: true });\n}\n\nlet dnsLookupImpl: DnsLookupFn = defaultDnsLookup;\n\n/**\n * Test-only seam: inject a fake DNS resolver so tests can simulate a hostname\n * resolving to a private/metadata IP without depending on real DNS or\n * network access. Pass `null` to restore the real `dns.lookup`-backed\n * resolver. Production code never calls this — only `ssrf.test.ts` does.\n */\nexport function __setDnsLookupForTest(fn: DnsLookupFn | null): void {\n\tdnsLookupImpl = fn ?? defaultDnsLookup;\n}\n\nconst ALLOW_HINT = \"(set SECONDLAYER_ALLOW_PRIVATE_EGRESS=true to allow)\";\n\n/**\n * Full egress check: literal fast-fail, then resolve DNS and reject if *any*\n * returned address (not just the first) is private/loopback/link-local —\n * a single private answer among several is enough for an attacker to abuse.\n *\n * Residual TOCTOU: this resolves-and-validates *immediately before* `fetch()`\n * but does not pin the actual connection to the validated address. True\n * pinning (resolve once, connect to that literal IP, keep the original `Host`\n * header/SNI) was attempted per plan f053 via a custom undici `Agent` with\n * `connect.lookup` passed as `fetch()`'s `dispatcher` — the standard Node\n * ecosystem pattern for this. Empirically, under Bun 1.3.10, that hook is\n * silently never invoked (confirmed with a throwaway smoke test hitting a\n * real local HTTP server: `lookupCalled` stayed `false` and the connection\n * failed even though the dispatcher was passed) — Bun's `fetch()` does not\n * implement undici's `connect.lookup` dispatcher hook, whether reached via\n * the global `fetch` or `import { fetch } from \"undici\"` (Bun overrides\n * \"undici\"'s `fetch` export with its own native implementation regardless of\n * an installed real `undici` package). So a second DNS answer between this\n * check and `fetch()`'s own resolution (true rebinding, not just a one-time\n * private answer) could in theory still slip through. This closes the\n * practical rebinding gap (attacker's hostname resolves private at delivery\n * time) without adding a dependency; a network-level egress allowlist/proxy\n * is the recommended defense-in-depth for the remaining TOCTOU sliver.\n */\nexport async function checkEgressAllowed(url: string): Promise<string | null> {\n\tif (isPrivateEgress(url)) {\n\t\treturn `refused private egress: literal address ${ALLOW_HINT}`;\n\t}\n\n\tlet parsed: URL;\n\ttry {\n\t\tparsed = new URL(url);\n\t} catch {\n\t\treturn `refused private egress: malformed URL ${ALLOW_HINT}`;\n\t}\n\tif (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\") {\n\t\treturn `refused private egress: unsupported protocol ${ALLOW_HINT}`;\n\t}\n\n\tconst raw = parsed.hostname.toLowerCase();\n\tconst hostname =\n\t\traw.startsWith(\"[\") && raw.endsWith(\"]\") ? raw.slice(1, -1) : raw;\n\n\tlet resolved: ResolvedAddress[];\n\ttry {\n\t\tresolved = await dnsLookupImpl(hostname);\n\t} catch (err) {\n\t\tconst msg = err instanceof Error ? err.message : String(err);\n\t\treturn `refused private egress: DNS resolution failed (${msg}) ${ALLOW_HINT}`;\n\t}\n\tif (resolved.length === 0) {\n\t\treturn `refused private egress: DNS resolution returned no addresses ${ALLOW_HINT}`;\n\t}\n\tfor (const { address } of resolved) {\n\t\tif (isPrivateIp(address)) {\n\t\t\treturn `refused private egress: hostname resolves to private address ${address} ${ALLOW_HINT}`;\n\t\t}\n\t}\n\treturn null;\n}\n\n/** The wire result of one POST attempt (no DB side effect) — shared by the\n * emitter hot path and the `deliverTestEvent` test path. */\ninterface PostResult {\n\tok: boolean;\n\tstatusCode: number | null;\n\terror: string | null;\n\tdurationMs: number;\n\tresponseBody: string | null;\n\tresponseHeaders: Record<string, string> | null;\n}\n\n/**\n * Bound on redirect hops honored while delivering a webhook. `fetch` is\n * called with `redirect: \"manual\"` so a 3xx never auto-follows — each hop's\n * target is re-validated through `checkEgressAllowed` before it is fetched\n * (see `postToSubscription`), otherwise a `Location` header pointing at a\n * private/metadata address would bypass the egress guard entirely. Counts\n * every fetch issued (the initial request plus each redirect), so this is\n * also the max number of `fetch` calls one delivery attempt can make.\n */\nexport const MAX_REDIRECT_HOPS: number = 3;\n\n/** POST a pre-built body to a subscription URL with the SSRF guard + timeout.\n * Pure transport: returns the attempt result; the caller logs the delivery row.\n * Follows redirects manually (up to `MAX_REDIRECT_HOPS`), re-running the\n * egress guard against every hop's target — the guard only protects the\n * first request otherwise, and a webhook target can redirect to a private/\n * metadata address to read back the response after f053 pinned the guard to\n * just the original URL. */\nasync function postToSubscription(\n\turl: string,\n\tbody: string,\n\theaders: Record<string, string>,\n\ttimeoutMs: number,\n): Promise<PostResult> {\n\tconst start = performance.now();\n\tlet target = url;\n\tlet statusCode: number | null = null;\n\tlet error: string | null = null;\n\tlet ok = false;\n\tlet responseBody = \"\";\n\tlet responseHeaders: Record<string, string> | null = null;\n\ttry {\n\t\tfor (let hop = 0; ; hop++) {\n\t\t\tif (!allowPrivateEgress()) {\n\t\t\t\tconst refusal = await checkEgressAllowed(target);\n\t\t\t\tif (refusal) {\n\t\t\t\t\tlogger.warn(\"[emitter] refused private egress\", {\n\t\t\t\t\t\turl: target,\n\t\t\t\t\t\treason: refusal,\n\t\t\t\t\t});\n\t\t\t\t\treturn {\n\t\t\t\t\t\tok: false,\n\t\t\t\t\t\tstatusCode: null,\n\t\t\t\t\t\terror: refusal,\n\t\t\t\t\t\tdurationMs: Math.round(performance.now() - start),\n\t\t\t\t\t\tresponseBody: null,\n\t\t\t\t\t\tresponseHeaders: null,\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (hop >= MAX_REDIRECT_HOPS) {\n\t\t\t\treturn {\n\t\t\t\t\tok: false,\n\t\t\t\t\tstatusCode,\n\t\t\t\t\terror: `too many redirects (exceeded ${MAX_REDIRECT_HOPS} hops)`,\n\t\t\t\t\tdurationMs: Math.round(performance.now() - start),\n\t\t\t\t\tresponseBody: null,\n\t\t\t\t\tresponseHeaders: null,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tconst res = await fetch(target, {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\theaders,\n\t\t\t\tbody,\n\t\t\t\tredirect: \"manual\",\n\t\t\t\tsignal: AbortSignal.timeout(timeoutMs),\n\t\t\t});\n\t\t\tstatusCode = res.status;\n\n\t\t\tconst location = res.headers.get(\"location\");\n\t\t\tif (res.status >= 300 && res.status < 400 && location) {\n\t\t\t\ttarget = new URL(location, target).toString();\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tok = res.ok;\n\t\t\t// Collect small response preview for the delivery log (≤8KB).\n\t\t\tconst buf = await res.arrayBuffer();\n\t\t\tconst truncated = buf.byteLength > 8192 ? buf.slice(0, 8192) : buf;\n\t\t\tresponseBody = Buffer.from(truncated).toString(\"utf8\");\n\t\t\tresponseHeaders = Object.fromEntries(res.headers.entries());\n\t\t\tbreak;\n\t\t}\n\t} catch (err) {\n\t\terror = err instanceof Error ? err.message : String(err);\n\t}\n\treturn {\n\t\tok,\n\t\tstatusCode,\n\t\terror,\n\t\tdurationMs: Math.round(performance.now() - start),\n\t\tresponseBody: responseBody || null,\n\t\tresponseHeaders,\n\t};\n}\n\n/**\n * Test-only seam: exposes `postToSubscription` (otherwise module-private) so\n * redirect/egress behavior can be exercised directly against a stubbed\n * `fetch` + injected DNS lookup, without standing up the DB-backed emitter\n * loop. Production code never calls this — only tests do.\n */\nexport const __postToSubscriptionForTest: (\n\turl: string,\n\tbody: string,\n\theaders: Record<string, string>,\n\ttimeoutMs: number,\n) => Promise<PostResult> = postToSubscription;\n\nasync function dispatchOne(\n\tdb: Kysely<Database>,\n\toutboxRow: SubscriptionOutbox,\n\tsub: Subscription,\n): Promise<{\n\tok: boolean;\n\tstatusCode: number | null;\n\terror: string | null;\n\tdurationMs: number;\n}> {\n\tconst { body, headers } = buildForFormat(\n\t\toutboxRow,\n\t\tsub,\n\t\tgetSubscriptionSigningSecret(sub),\n\t);\n\tconst r = await postToSubscription(sub.url, body, headers, sub.timeout_ms);\n\n\tconst attempt = outboxRow.attempt + 1;\n\tawait db\n\t\t.insertInto(\"subscription_deliveries\")\n\t\t.values({\n\t\t\toutbox_id: outboxRow.id,\n\t\t\tsubscription_id: outboxRow.subscription_id,\n\t\t\tattempt,\n\t\t\tstatus_code: r.statusCode,\n\t\t\tresponse_headers: r.responseHeaders,\n\t\t\tresponse_body: r.responseBody,\n\t\t\terror_message: r.error,\n\t\t\tduration_ms: r.durationMs,\n\t\t})\n\t\t.execute();\n\n\treturn {\n\t\tok: r.ok,\n\t\tstatusCode: r.statusCode,\n\t\terror: r.error,\n\t\tdurationMs: r.durationMs,\n\t};\n}\n\n/** A representative (non-persisted) outbox row for a test delivery, shaped to the\n * subscription's kind so `buildForFormat` produces a realistic body. */\nfunction buildTestOutboxRow(sub: Subscription): SubscriptionOutbox {\n\tconst now = new Date();\n\treturn {\n\t\tid: randomUUID(),\n\t\tsubscription_id: sub.id,\n\t\tkind: sub.kind,\n\t\tsubgraph_name: sub.subgraph_name ?? null,\n\t\ttable_name: sub.table_name ?? null,\n\t\tblock_height: 0,\n\t\ttx_id: null,\n\t\trow_pk: null,\n\t\tevent_type:\n\t\t\tsub.kind === \"chain\"\n\t\t\t\t? \"chain.test.apply\"\n\t\t\t\t: `${sub.subgraph_name ?? \"subgraph\"}.${sub.table_name ?? \"test\"}.created`,\n\t\tpayload: {\n\t\t\ttest: true,\n\t\t\tmessage: \"Secondlayer test delivery\",\n\t\t\tsubscription_id: sub.id,\n\t\t\tsent_at: now.toISOString(),\n\t\t},\n\t\tdedup_key: `test:${sub.id}:${now.getTime()}`,\n\t\tattempt: 0,\n\t\tnext_attempt_at: now,\n\t\tstatus: \"pending\",\n\t\tis_replay: false,\n\t\tdelivered_at: null,\n\t\tfailed_at: null,\n\t\tlocked_by: null,\n\t\tlocked_until: null,\n\t\tcreated_at: now,\n\t};\n}\n\n/**\n * Build a representative webhook for `sub`'s configured format, POST it (same\n * SSRF guard + timeout + signing as a real delivery), and log a delivery row\n * with a null `outbox_id` so it appears under the subscription's deliveries\n * without being tied to a queued event. Powers `POST /:id/test`.\n */\nexport async function deliverTestEvent(\n\tdb: Kysely<Database>,\n\tsub: Subscription,\n): Promise<SubscriptionTestResult> {\n\tconst testRow = buildTestOutboxRow(sub);\n\tconst { body, headers } = buildForFormat(\n\t\ttestRow,\n\t\tsub,\n\t\tgetSubscriptionSigningSecret(sub),\n\t);\n\tconst r = await postToSubscription(sub.url, body, headers, sub.timeout_ms);\n\tconst inserted = await db\n\t\t.insertInto(\"subscription_deliveries\")\n\t\t.values({\n\t\t\toutbox_id: null,\n\t\t\tsubscription_id: sub.id,\n\t\t\tattempt: 1,\n\t\t\tstatus_code: r.statusCode,\n\t\t\tresponse_headers: r.responseHeaders,\n\t\t\tresponse_body: r.responseBody,\n\t\t\terror_message: r.error,\n\t\t\tduration_ms: r.durationMs,\n\t\t})\n\t\t.returning(\"id\")\n\t\t.executeTakeFirstOrThrow();\n\treturn {\n\t\tok: r.ok,\n\t\tstatusCode: r.statusCode,\n\t\terror: r.error,\n\t\tdurationMs: r.durationMs,\n\t\tdeliveryId: inserted.id,\n\t};\n}\n\nasync function settleDelivered(\n\tdb: Kysely<Database>,\n\toutboxRow: SubscriptionOutbox,\n): Promise<void> {\n\tawait db.transaction().execute(async (tx) => {\n\t\tawait tx\n\t\t\t.updateTable(\"subscription_outbox\")\n\t\t\t.set({\n\t\t\t\tstatus: \"delivered\",\n\t\t\t\tdelivered_at: new Date(),\n\t\t\t\tattempt: outboxRow.attempt + 1,\n\t\t\t\tlocked_by: null,\n\t\t\t\tlocked_until: null,\n\t\t\t})\n\t\t\t.where(\"id\", \"=\", outboxRow.id)\n\t\t\t.execute();\n\t\tawait tx\n\t\t\t.updateTable(\"subscriptions\")\n\t\t\t.set({\n\t\t\t\tlast_delivery_at: new Date(),\n\t\t\t\tlast_success_at: new Date(),\n\t\t\t\tcircuit_failures: 0,\n\t\t\t\tlast_error: null,\n\t\t\t\tupdated_at: new Date(),\n\t\t\t})\n\t\t\t.where(\"id\", \"=\", outboxRow.subscription_id)\n\t\t\t.execute();\n\t});\n}\n\nasync function settleFailed(\n\tdb: Kysely<Database>,\n\toutboxRow: SubscriptionOutbox,\n\tsub: Subscription,\n\terrText: string,\n): Promise<void> {\n\tconst attempt = outboxRow.attempt + 1;\n\tconst isDead = attempt >= sub.max_retries;\n\tconst nextAt = isDead\n\t\t? null\n\t\t: new Date(Date.now() + nextDelaySeconds(outboxRow.attempt) * 1000);\n\n\tawait db.transaction().execute(async (tx) => {\n\t\tawait tx\n\t\t\t.updateTable(\"subscription_outbox\")\n\t\t\t.set({\n\t\t\t\tattempt,\n\t\t\t\tnext_attempt_at: nextAt ?? new Date(),\n\t\t\t\tstatus: isDead ? \"dead\" : \"pending\",\n\t\t\t\tfailed_at: isDead ? new Date() : null,\n\t\t\t\tlocked_by: null,\n\t\t\t\tlocked_until: null,\n\t\t\t})\n\t\t\t.where(\"id\", \"=\", outboxRow.id)\n\t\t\t.execute();\n\n\t\t// Atomic increment — concurrent failures must not clobber each other.\n\t\t// `RETURNING circuit_failures` gives us the post-increment value to\n\t\t// decide whether this failure tripped the circuit.\n\t\tconst incResult = await sql<{ circuit_failures: number }>`\n\t\t\tUPDATE subscriptions\n\t\t\tSET circuit_failures = circuit_failures + 1,\n\t\t\t\tlast_delivery_at = NOW(),\n\t\t\t\tlast_error = ${errText.slice(0, 500)},\n\t\t\t\tupdated_at = NOW()\n\t\t\tWHERE id = ${sub.id}\n\t\t\tRETURNING circuit_failures\n\t\t`.execute(tx);\n\t\tconst newFailures =\n\t\t\tincResult.rows[0]?.circuit_failures ?? sub.circuit_failures + 1;\n\t\tconst shouldTripCircuit = newFailures >= CIRCUIT_THRESHOLD;\n\n\t\tif (shouldTripCircuit) {\n\t\t\t// Transition to paused only on the first failure that crossed\n\t\t\t// the threshold — additional failures in-flight harmlessly\n\t\t\t// re-set the same fields.\n\t\t\tawait tx\n\t\t\t\t.updateTable(\"subscriptions\")\n\t\t\t\t.set({\n\t\t\t\t\tstatus: \"paused\",\n\t\t\t\t\tcircuit_opened_at: new Date(),\n\t\t\t\t\tupdated_at: new Date(),\n\t\t\t\t})\n\t\t\t\t.where(\"id\", \"=\", sub.id)\n\t\t\t\t.execute();\n\t\t\tlogger.warn(\n\t\t\t\t\"Subscription circuit tripped — paused after consecutive failures\",\n\t\t\t\t{\n\t\t\t\t\tsubscription: sub.name,\n\t\t\t\t\tfailures: newFailures,\n\t\t\t\t},\n\t\t\t);\n\t\t}\n\t});\n}\n\nasync function claimAndDrain(\n\tdb: Kysely<Database>,\n\tstate: RunningState,\n\temitterId: string,\n): Promise<number> {\n\tif (state.claimInFlight) return 0;\n\tstate.claimInFlight = true;\n\ttry {\n\t\t// FOR UPDATE SKIP LOCKED — multiple emitters split the batch.\n\t\t// 90/10 live vs replay so a big replay doesn't starve live emits.\n\t\tconst liveLimit = Math.max(1, Math.round(BATCH_SIZE * LIVE_SHARE));\n\t\tconst replayLimit = BATCH_SIZE - liveLimit;\n\t\tconst claimed = await db.transaction().execute(async (tx) => {\n\t\t\tconst live = await sql<SubscriptionOutbox>`\n\t\t\t\t\tSELECT * FROM subscription_outbox\n\t\t\t\t\tWHERE status = 'pending'\n\t\t\t\t\t\tAND next_attempt_at <= NOW()\n\t\t\t\t\t\tAND is_replay = FALSE\n\t\t\t\t\tORDER BY next_attempt_at ASC\n\t\t\t\t\tFOR UPDATE SKIP LOCKED\n\t\t\t\t\tLIMIT ${sql.lit(liveLimit)}\n\t\t\t\t`.execute(tx);\n\t\t\tconst replay = await sql<SubscriptionOutbox>`\n\t\t\t\t\tSELECT * FROM subscription_outbox\n\t\t\t\t\tWHERE status = 'pending'\n\t\t\t\t\t\tAND next_attempt_at <= NOW()\n\t\t\t\t\t\tAND is_replay = TRUE\n\t\t\t\t\tORDER BY next_attempt_at ASC\n\t\t\t\t\tFOR UPDATE SKIP LOCKED\n\t\t\t\t\tLIMIT ${sql.lit(replayLimit)}\n\t\t\t\t`.execute(tx);\n\n\t\t\tconst combined = [...live.rows, ...replay.rows];\n\t\t\tif (combined.length === 0) return [];\n\n\t\t\t// Push `next_attempt_at` forward by the lock window. This is\n\t\t\t// the only defense against double-dispatch if the emitter\n\t\t\t// process crashes mid-HTTP-call: the row won't be re-claimable\n\t\t\t// until `LOCK_WINDOW_MS` elapses, giving us a stale-lock\n\t\t\t// recovery window. `settleDelivered`/`settleFailed` overrides\n\t\t\t// this on the success/failure path.\n\t\t\tconst now = new Date();\n\t\t\tconst lockUntil = new Date(now.getTime() + LOCK_WINDOW_MS);\n\t\t\tawait tx\n\t\t\t\t.updateTable(\"subscription_outbox\")\n\t\t\t\t.set({\n\t\t\t\t\tlocked_by: emitterId,\n\t\t\t\t\tlocked_until: lockUntil,\n\t\t\t\t\tnext_attempt_at: lockUntil,\n\t\t\t\t})\n\t\t\t\t.where(\n\t\t\t\t\t\"id\",\n\t\t\t\t\t\"in\",\n\t\t\t\t\tcombined.map((r) => r.id),\n\t\t\t\t)\n\t\t\t\t.execute();\n\t\t\treturn combined;\n\t\t});\n\n\t\tif (claimed.length === 0) return 0;\n\n\t\t// Hydrate each claimed row's sub once, then dispatch with per-sub\n\t\t// concurrency cap enforced via in-memory semaphore.\n\t\tconst bySubId = new Map<string, SubscriptionOutbox[]>();\n\t\tfor (const row of claimed) {\n\t\t\tconst arr = bySubId.get(row.subscription_id);\n\t\t\tif (arr) arr.push(row);\n\t\t\telse bySubId.set(row.subscription_id, [row]);\n\t\t}\n\n\t\tconst subIds = Array.from(bySubId.keys());\n\t\tconst subs = await db\n\t\t\t.selectFrom(\"subscriptions\")\n\t\t\t.selectAll()\n\t\t\t.where(\"id\", \"in\", subIds)\n\t\t\t.execute();\n\t\tconst subById = new Map(subs.map((s) => [s.id, s]));\n\n\t\tawait Promise.all(\n\t\t\tsubIds.map((subId) =>\n\t\t\t\t// biome-ignore lint/style/noNonNullAssertion: value is non-null after preceding check or by construction; TS narrowing limitation\n\t\t\t\tdrainForSub(db, state, subById.get(subId)!, bySubId.get(subId)!),\n\t\t\t),\n\t\t);\n\n\t\treturn claimed.length;\n\t} finally {\n\t\tstate.claimInFlight = false;\n\t}\n}\n\nasync function drainForSub(\n\tdb: Kysely<Database>,\n\tstate: RunningState,\n\tsub: Subscription,\n\trows: SubscriptionOutbox[],\n): Promise<void> {\n\tconst cap = sub.concurrency || 4;\n\tconst counter = () => state.inFlightBySub.get(sub.id) ?? 0;\n\tconst inc = () => state.inFlightBySub.set(sub.id, counter() + 1);\n\tconst dec = () => state.inFlightBySub.set(sub.id, Math.max(0, counter() - 1));\n\n\tconst queue = [...rows];\n\tconst workers: Promise<void>[] = [];\n\tconst slots = Math.min(cap, queue.length);\n\n\tfor (let i = 0; i < slots; i++) {\n\t\tworkers.push(\n\t\t\t(async () => {\n\t\t\t\twhile (state.running && queue.length > 0) {\n\t\t\t\t\tconst row = queue.shift();\n\t\t\t\t\tif (!row) break;\n\t\t\t\t\tinc();\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst result = await dispatchOne(db, row, sub);\n\t\t\t\t\t\tif (result.ok) {\n\t\t\t\t\t\t\tawait settleDelivered(db, row);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tconst err = result.error ?? `HTTP ${result.statusCode ?? \"?\"}`;\n\t\t\t\t\t\t\tawait settleFailed(db, row, sub, err);\n\t\t\t\t\t\t}\n\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\tlogger.error(\"Emitter dispatch crashed\", {\n\t\t\t\t\t\t\toutboxId: row.id,\n\t\t\t\t\t\t\terror: err instanceof Error ? err.message : String(err),\n\t\t\t\t\t\t});\n\t\t\t\t\t\tawait settleFailed(\n\t\t\t\t\t\t\tdb,\n\t\t\t\t\t\t\trow,\n\t\t\t\t\t\t\tsub,\n\t\t\t\t\t\t\terr instanceof Error ? err.message : String(err),\n\t\t\t\t\t\t);\n\t\t\t\t\t} finally {\n\t\t\t\t\t\tdec();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t})(),\n\t\t);\n\t}\n\tawait Promise.all(workers);\n}\n\nexport interface StartEmitterOptions {\n\t/** Interval for the background poll (ms). Defaults to 2 minutes. */\n\tpollIntervalMs?: number;\n\t/** Retention sweep interval (ms). Defaults to 1 hour. */\n\tretentionIntervalMs?: number;\n}\n\nasync function runRetention(db: Kysely<Database>): Promise<void> {\n\t// delivered outbox >7d, deliveries >30d, dead outbox >90d\n\tawait sql`\n\t\tDELETE FROM subscription_outbox\n\t\tWHERE status = 'delivered' AND delivered_at < NOW() - interval '7 days'\n\t`.execute(db);\n\tawait sql`\n\t\tDELETE FROM subscription_deliveries\n\t\tWHERE dispatched_at < NOW() - interval '30 days'\n\t`.execute(db);\n\tawait sql`\n\t\tDELETE FROM subscription_outbox\n\t\tWHERE status = 'dead' AND failed_at < NOW() - interval '90 days'\n\t`.execute(db);\n}\n\nexport async function startEmitter(\n\topts?: StartEmitterOptions,\n): Promise<() => Promise<void>> {\n\tconst emitterId = `emitter-${Math.random().toString(36).slice(2, 10)}`;\n\tconst db = getTargetDb();\n\tconst state: RunningState = {\n\t\trunning: true,\n\t\tinFlightBySub: new Map(),\n\t\tclaimInFlight: false,\n\t};\n\tconst pollIntervalMs = opts?.pollIntervalMs ?? 120_000;\n\tconst retentionIntervalMs = opts?.retentionIntervalMs ?? 60 * 60_000;\n\n\tlogger.info(\"[emitter] started\", { id: emitterId });\n\n\t// Bootstrap matcher from active subs. Retry with backoff — if this\n\t// stays broken, fail loud rather than run with an empty matcher (which\n\t// would silently drop every block's outbox emissions until the next\n\t// subscription CRUD fired `subscriptions:changed`).\n\tconst MATCHER_BOOT_ATTEMPTS = 5;\n\tlet lastErr: unknown = null;\n\tfor (let i = 0; i < MATCHER_BOOT_ATTEMPTS; i++) {\n\t\ttry {\n\t\t\tawait refreshMatcher(db);\n\t\t\tlastErr = null;\n\t\t\tbreak;\n\t\t} catch (err) {\n\t\t\tlastErr = err;\n\t\t\tconst delayMs = 500 * 2 ** i; // 500ms, 1s, 2s, 4s, 8s\n\t\t\tlogger.warn(\"[emitter] matcher refresh failed, retrying\", {\n\t\t\t\tattempt: i + 1,\n\t\t\t\tdelayMs,\n\t\t\t\terror: err instanceof Error ? err.message : String(err),\n\t\t\t});\n\t\t\tawait new Promise((r) => setTimeout(r, delayMs));\n\t\t}\n\t}\n\tif (lastErr) {\n\t\tthrow new Error(\n\t\t\t`[emitter] matcher refresh failed ${MATCHER_BOOT_ATTEMPTS}×; aborting boot: ${\n\t\t\t\tlastErr instanceof Error ? lastErr.message : String(lastErr)\n\t\t\t}`,\n\t\t);\n\t}\n\n\t// LISTEN on new outbox + sub changes. Both channels fire on the TARGET DB\n\t// (subscription_outbox + subscriptions are control-plane tables), so bind the\n\t// listener there — under the split it is NOT `DATABASE_URL`.\n\tconst listenUrl = targetListenerUrl();\n\tconst stopNew = await listen(\n\t\t\"subscriptions:new_outbox\",\n\t\t() => {\n\t\t\tif (!state.running) return;\n\t\t\tvoid claimAndDrain(db, state, emitterId).catch((err) =>\n\t\t\t\tlogger.error(\"[emitter] claim failed\", {\n\t\t\t\t\terror: err instanceof Error ? err.message : String(err),\n\t\t\t\t}),\n\t\t\t);\n\t\t},\n\t\t{ connectionString: listenUrl },\n\t);\n\tconst stopChanged = await listen(\n\t\t\"subscriptions:changed\",\n\t\t() => {\n\t\t\tif (!state.running) return;\n\t\t\tvoid refreshMatcher(db).catch((err) =>\n\t\t\t\tlogger.error(\"[emitter] matcher refresh failed\", {\n\t\t\t\t\terror: err instanceof Error ? err.message : String(err),\n\t\t\t\t}),\n\t\t\t);\n\t\t},\n\t\t{ connectionString: listenUrl },\n\t);\n\n\t// Poll every pollIntervalMs as a safety net for missed notifications +\n\t// backoff wakeups (rows whose next_attempt_at has passed).\n\tconst poll = setInterval(() => {\n\t\tif (!state.running) return;\n\t\tvoid claimAndDrain(db, state, emitterId).catch((err) =>\n\t\t\tlogger.error(\"[emitter] poll claim failed\", {\n\t\t\t\terror: err instanceof Error ? err.message : String(err),\n\t\t\t}),\n\t\t);\n\t}, pollIntervalMs);\n\n\t// Kick once on startup so any rows that arrived before we started drain.\n\tvoid claimAndDrain(db, state, emitterId);\n\n\t// Retention sweep — hourly by default.\n\tconst retention = setInterval(() => {\n\t\tif (!state.running) return;\n\t\tvoid runRetention(db).catch((err) =>\n\t\t\tlogger.error(\"[emitter] retention failed\", {\n\t\t\t\terror: err instanceof Error ? err.message : String(err),\n\t\t\t}),\n\t\t);\n\t}, retentionIntervalMs);\n\n\treturn async () => {\n\t\tstate.running = false;\n\t\tclearInterval(poll);\n\t\tclearInterval(retention);\n\t\tawait stopNew();\n\t\tawait stopChanged();\n\t\tlogger.info(\"[emitter] stopped\", { id: emitterId });\n\t};\n}\n",
6
- "import { signSecondlayerWebhook } from \"@secondlayer/shared/crypto/secondlayer-webhook\";\nimport type { Subscription, SubscriptionOutbox } from \"@secondlayer/shared/db\";\nimport { logger } from \"@secondlayer/shared/logger\";\nimport { buildCloudEvents } from \"./cloudevents.ts\";\nimport { buildCloudflare } from \"./cloudflare.ts\";\nimport { buildInngest } from \"./inngest.ts\";\nimport { buildRaw } from \"./raw.ts\";\nimport { buildStandardWebhooks } from \"./standard-webhooks.ts\";\nimport { buildTrigger } from \"./trigger.ts\";\n\nexport interface FormatBuildResult {\n\tbody: string;\n\theaders: Record<string, string>;\n}\n\nfunction buildBody(\n\toutboxRow: SubscriptionOutbox,\n\tsub: Subscription,\n\tsigningSecret: string,\n): FormatBuildResult {\n\tswitch (sub.format) {\n\t\tcase \"inngest\":\n\t\t\treturn buildInngest(outboxRow);\n\t\tcase \"trigger\":\n\t\t\treturn buildTrigger(outboxRow, sub);\n\t\tcase \"cloudflare\":\n\t\t\treturn buildCloudflare(outboxRow, sub);\n\t\tcase \"cloudevents\":\n\t\t\treturn buildCloudEvents(outboxRow, sub);\n\t\tcase \"raw\":\n\t\t\treturn buildRaw(outboxRow, sub);\n\t\tcase \"standard-webhooks\":\n\t\t\treturn buildStandardWebhooks(outboxRow, signingSecret);\n\t\tdefault:\n\t\t\tlogger.warn(\n\t\t\t\t\"Unknown subscription format, falling back to standard-webhooks\",\n\t\t\t\t{\n\t\t\t\t\tformat: sub.format,\n\t\t\t\t\tsubscriptionId: sub.id,\n\t\t\t\t},\n\t\t\t);\n\t\t\treturn buildStandardWebhooks(outboxRow, signingSecret);\n\t}\n}\n\n/**\n * Dispatch an outbox row through the format matching the subscription's\n * `format` column. Unknown formats fall back to `standard-webhooks` with\n * a warning log — receivers always get something deliverable.\n *\n * Every delivery, whatever its body shape, also gets the universal Secondlayer\n * authenticity headers (`webhook-id` + `X-Secondlayer-Signature`) so a receiver\n * can prove the payload came from us with one published public key — not just\n * the `standard-webhooks` format. The signature covers the exact body bytes\n * built here, so it must be attached after the body is final.\n */\nexport function buildForFormat(\n\toutboxRow: SubscriptionOutbox,\n\tsub: Subscription,\n\tsigningSecret: string,\n): FormatBuildResult {\n\tconst result = buildBody(outboxRow, sub, signingSecret);\n\tconst sigHeaders = signSecondlayerWebhook(outboxRow.id, result.body);\n\tif (sigHeaders) {\n\t\tresult.headers = { ...result.headers, ...sigHeaders };\n\t}\n\treturn result;\n}\n\nexport {\n\tbuildStandardWebhooks,\n\tbuildInngest,\n\tbuildTrigger,\n\tbuildCloudflare,\n\tbuildCloudEvents,\n\tbuildRaw,\n};\n",
7
- "import type { Subscription, SubscriptionOutbox } from \"@secondlayer/shared/db\";\n\n/**\n * CloudEvents 1.0 structured JSON — https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/formats/json-format.md\n *\n * Body shape:\n * {\n * \"specversion\": \"1.0\",\n * \"type\": \"<subgraph>.<table>.<created|updated|deleted>\",\n * \"source\": \"secondlayer:<subgraph_name>\",\n * \"id\": <outbox.id>, // UUID, used for dedup\n * \"time\": <ISO 8601>,\n * \"datacontenttype\": \"application/json\",\n * \"data\": { ...row }\n * }\n *\n * Content-Type is `application/cloudevents+json; charset=utf-8`. Binary\n * mode (headers as ce-*) isn't needed — structured mode is what every CE\n * SDK accepts out of the box.\n */\n\nexport function buildCloudEvents(\n\toutboxRow: SubscriptionOutbox,\n\t_sub: Subscription,\n): { body: string; headers: Record<string, string> } {\n\tconst event = {\n\t\tspecversion: \"1.0\",\n\t\ttype: outboxRow.event_type,\n\t\tsource: `secondlayer:${outboxRow.subgraph_name}`,\n\t\tid: outboxRow.id,\n\t\ttime: new Date(outboxRow.created_at).toISOString(),\n\t\tdatacontenttype: \"application/json\",\n\t\tdata: outboxRow.payload,\n\t};\n\treturn {\n\t\tbody: JSON.stringify(event),\n\t\theaders: {\n\t\t\t\"content-type\": \"application/cloudevents+json; charset=utf-8\",\n\t\t},\n\t};\n}\n",
8
- "import { decryptSecret } from \"@secondlayer/shared/crypto/secrets\";\nimport type { Subscription, SubscriptionOutbox } from \"@secondlayer/shared/db\";\n\n/**\n * Cloudflare Workflows — https://developers.cloudflare.com/workflows/build/events-and-parameters/\n *\n * POST https://api.cloudflare.com/client/v4/accounts/{account}/workflows/{name}/instances\n * Authorization: Bearer <CF_API_TOKEN>\n *\n * Body: `{ params }` — the workflow entrypoint receives this as the\n * `event.payload` object. We slip the outbox id into `params._subscriptionId`\n * so Workflows can dedupe on replays.\n */\n\nfunction resolveBearer(sub: Subscription): string | null {\n\tconst cfg = sub.auth_config as { token?: string; tokenEnc?: string };\n\tif (cfg.tokenEnc) {\n\t\t// Let decrypt errors propagate — see trigger.ts comment.\n\t\treturn decryptSecret(Buffer.from(cfg.tokenEnc, \"base64\"));\n\t}\n\treturn cfg.token ?? null;\n}\n\nexport function buildCloudflare(\n\toutboxRow: SubscriptionOutbox,\n\tsub: Subscription,\n): { body: string; headers: Record<string, string> } {\n\tconst body = JSON.stringify({\n\t\tparams: {\n\t\t\t...(outboxRow.payload as Record<string, unknown>),\n\t\t\t_type: outboxRow.event_type,\n\t\t\t_outboxId: outboxRow.id,\n\t\t},\n\t});\n\tconst headers: Record<string, string> = {\n\t\t\"content-type\": \"application/json\",\n\t};\n\tconst token = resolveBearer(sub);\n\tif (token) headers.authorization = `Bearer ${token}`;\n\treturn { body, headers };\n}\n",
9
- "import type { SubscriptionOutbox } from \"@secondlayer/shared/db\";\n\n/**\n * Inngest event format — https://www.inngest.com/docs/events/sending-events\n *\n * POST body is a JSON array of one or more events. The Inngest event key\n * goes in the URL path (`https://inn.gs/e/{EVENT_KEY}`), so the user sets\n * that on `subscription.url` directly — no auth header needed.\n *\n * Body per event:\n * - `name` — event name `<subgraph>.<table>.<created|updated|deleted>`\n * so it matches Inngest fn `event` triggers out of the box\n * - `data` — row payload\n * - `id` — dedupes on Inngest's side (mirrors outbox id)\n * - `ts` — unix millis when the event occurred\n * - `v` — version tag, fixed at `\"2026-04-23.v1\"` for forward compat\n */\n\nexport const INNGEST_VERSION = \"2026-04-23.v1\";\n\nexport function buildInngest(outboxRow: SubscriptionOutbox): {\n\tbody: string;\n\theaders: Record<string, string>;\n} {\n\tconst event = {\n\t\tname: outboxRow.event_type,\n\t\tdata: outboxRow.payload,\n\t\tid: outboxRow.id,\n\t\tts: new Date(outboxRow.created_at).getTime(),\n\t\tv: INNGEST_VERSION,\n\t};\n\treturn {\n\t\tbody: JSON.stringify([event]),\n\t\theaders: {\n\t\t\t\"content-type\": \"application/json\",\n\t\t},\n\t};\n}\n",
10
- "import type { Subscription, SubscriptionOutbox } from \"@secondlayer/shared/db\";\n\n/**\n * Raw JSON — the row payload, nothing else. User controls Content-Type +\n * arbitrary headers via `sub.auth_config.headers`. Useful for targets that\n * already have their own wire contract (e.g. Zapier hooks, internal\n * services, Slack webhooks with raw-shaped bodies).\n *\n * Special-case: if `auth_config.authType === \"basic\"` and `basicAuth` is\n * set (`user:pass` base64 or plain user/pass object), emit a Basic header.\n * If `authType === \"bearer\"` with `token`/`tokenEnc`, emit a Bearer header.\n * Otherwise no auth is set — user URLs often include a token in the query\n * string (Slack) or an HMAC at the receiver (Zapier).\n */\n\nexport function buildRaw(\n\toutboxRow: SubscriptionOutbox,\n\tsub: Subscription,\n): { body: string; headers: Record<string, string> } {\n\tconst cfg = sub.auth_config as {\n\t\tcontentType?: string;\n\t\theaders?: Record<string, string>;\n\t\tauthType?: \"bearer\" | \"basic\" | \"none\";\n\t\ttoken?: string;\n\t\tbasicAuth?: string;\n\t};\n\tconst headers: Record<string, string> = {\n\t\t\"content-type\": cfg.contentType ?? \"application/json\",\n\t\t...(cfg.headers ?? {}),\n\t};\n\tif (cfg.authType === \"bearer\" && cfg.token) {\n\t\theaders.authorization = `Bearer ${cfg.token}`;\n\t} else if (cfg.authType === \"basic\" && cfg.basicAuth) {\n\t\theaders.authorization = `Basic ${cfg.basicAuth}`;\n\t}\n\treturn {\n\t\tbody: JSON.stringify(outboxRow.payload),\n\t\theaders,\n\t};\n}\n",
11
- "import { sign } from \"@secondlayer/shared/crypto/standard-webhooks\";\nimport type { SubscriptionOutbox } from \"@secondlayer/shared/db\";\n\n/**\n * Build a Standard Webhooks POST body + headers for an outbox row.\n * https://standardwebhooks.com\n *\n * Body shape: `{ type, timestamp, data }`\n * - `type` — `<subgraph>.<table>.<verb>` where verb is `created`, `updated`,\n * or `deleted`, mapped from the flush op so a receiver sees the full row\n * lifecycle, not just the initial insert.\n * - `timestamp` — ISO 8601 at DISPATCH time (not row creation). Receivers\n * verify `webhook-timestamp` against a tolerance window (default 300s per\n * Svix); stamping at creation would fail verification on every retry\n * beyond the first, since retries fire up to 72h after the row was\n * written. The `webhook-id` header stays stable across retries for\n * receiver dedup.\n * - `data` — row payload (bigints already stringified by flush manifest).\n *\n * Headers: the three standardwebhooks.com headers + Content-Type.\n */\n\nexport interface StandardWebhooksPayload {\n\ttype: string;\n\ttimestamp: string;\n\tdata: Record<string, unknown>;\n}\n\nexport function buildStandardWebhooks(\n\toutboxRow: SubscriptionOutbox,\n\tsigningSecret: string,\n): { body: string; headers: Record<string, string> } {\n\tconst nowSeconds = Math.floor(Date.now() / 1000);\n\tconst payload: StandardWebhooksPayload = {\n\t\ttype: outboxRow.event_type,\n\t\ttimestamp: new Date(nowSeconds * 1000).toISOString(),\n\t\tdata: outboxRow.payload as Record<string, unknown>,\n\t};\n\tconst body = JSON.stringify(payload);\n\tconst sigHeaders = sign(body, signingSecret, {\n\t\tid: outboxRow.id,\n\t\ttimestampSeconds: nowSeconds,\n\t});\n\treturn {\n\t\tbody,\n\t\theaders: {\n\t\t\t\"content-type\": \"application/json\",\n\t\t\t...sigHeaders,\n\t\t},\n\t};\n}\n",
12
- "import { decryptSecret } from \"@secondlayer/shared/crypto/secrets\";\nimport type { Subscription, SubscriptionOutbox } from \"@secondlayer/shared/db\";\n\n/**\n * Trigger.dev v3 task trigger — https://trigger.dev/docs/tasks/overview\n *\n * POST https://api.trigger.dev/api/v1/tasks/{taskId}/trigger\n * Authorization: Bearer <TRIGGER_SECRET_KEY>\n *\n * Body:\n * - `payload` — the row payload\n * - `options` — currently `{ idempotencyKey }` so Trigger dedupes replays\n *\n * The Trigger secret lives in `sub.auth_config.tokenEnc` (encrypted by\n * `crypto/secrets`) or `sub.auth_config.token` (plaintext, dev only).\n */\n\nfunction resolveBearer(sub: Subscription): string | null {\n\tconst cfg = sub.auth_config as {\n\t\tauthType?: string;\n\t\ttoken?: string;\n\t\ttokenEnc?: string;\n\t};\n\tif (cfg.tokenEnc) {\n\t\t// Let decrypt errors propagate — silently sending unauthenticated\n\t\t// requests produces downstream 401s with no hint the secret failed\n\t\t// to decrypt. Surface as a dispatch-time error so the delivery log\n\t\t// records the real cause.\n\t\treturn decryptSecret(Buffer.from(cfg.tokenEnc, \"base64\"));\n\t}\n\treturn cfg.token ?? null;\n}\n\nexport function buildTrigger(\n\toutboxRow: SubscriptionOutbox,\n\tsub: Subscription,\n): { body: string; headers: Record<string, string> } {\n\tconst body = JSON.stringify({\n\t\tpayload: outboxRow.payload,\n\t\toptions: {\n\t\t\tidempotencyKey: outboxRow.id,\n\t\t},\n\t});\n\tconst headers: Record<string, string> = {\n\t\t\"content-type\": \"application/json\",\n\t};\n\tconst token = resolveBearer(sub);\n\tif (token) headers.authorization = `Bearer ${token}`;\n\treturn { body, headers };\n}\n",
13
- "import type { Database, Subscription } from \"@secondlayer/shared/db\";\nimport { listSubscriptions } from \"@secondlayer/shared/db/queries/subscriptions\";\nimport type { Kysely } from \"kysely\";\nimport { sql } from \"kysely\";\nimport { SubscriptionMatcher } from \"./emitter-matcher.ts\";\n\n/**\n * Singleton matcher populated at processor startup and hot-reloaded via\n * `pg_notify('subscriptions:changed')`. The block-processor reads from it\n * to decide which outbox rows to emit for each flushed write.\n *\n * Per-account listing: in oss/dedicated mode the tenant DB holds all subs\n * for the single account; the matcher loads every row. In platform mode\n * the emitter doesn't run at all (control plane only), so this module is\n * dedicated/oss-only.\n */\n\nexport const matcher = new SubscriptionMatcher();\n\nexport async function refreshMatcher(db: Kysely<Database>): Promise<number> {\n\t// listSubscriptions is account-scoped; the emitter wants every active\n\t// sub so we do a raw query.\n\tconst rows = await sql<Subscription>`\n\t\tSELECT * FROM subscriptions WHERE status = 'active'\n\t`.execute(db);\n\tmatcher.setAll(rows.rows);\n\treturn matcher.size();\n}\n\n// Per-account helper used by tests so the DATABASE_URL-based code path is\n// exercised through listSubscriptions (keeps the query helper in the\n// integration surface).\nexport async function refreshMatcherForAccount(\n\tdb: Kysely<Database>,\n\taccountId: string,\n): Promise<number> {\n\tconst rows = await listSubscriptions(db, accountId);\n\tmatcher.setAll(rows.filter((r: Subscription) => r.status === \"active\"));\n\treturn matcher.size();\n}\n",
14
- "import type { Subscription } from \"@secondlayer/shared/db\";\n\n/**\n * Subscription matcher — holds the active-subscriptions cache keyed by\n * `(subgraph_name, table_name)` and evaluates each sub's scalar-only\n * JSONB filter against a row.\n *\n * Filter DSL (v1): scalar operators over string/number/boolean columns.\n * { column: value } → column === value\n * { column: { eq: value } } → column === value\n * { column: { neq: value } } → column !== value\n * { column: { gt|gte|lt|lte: N } } → numeric compare\n * { column: { in: [a, b, c] } } → column ∈ set\n * { } → match all\n *\n * Nested objects, arrays, and non-scalar values are rejected with `false`\n * (never match) — subscribers get logged warnings at sub create time.\n *\n * Multiple conditions AND together. OR is deliberately absent in v1.\n */\n\nexport type FilterPrimitive = string | number | boolean;\nexport type FilterOperator =\n\t| { eq: FilterPrimitive }\n\t| { neq: FilterPrimitive }\n\t| { gt: number | string }\n\t| { gte: number | string }\n\t| { lt: number | string }\n\t| { lte: number | string }\n\t| { in: FilterPrimitive[] };\n\nexport type FilterClause = FilterPrimitive | FilterOperator;\nexport type SubscriptionFilter = Record<string, FilterClause>;\n\nfunction isPrimitive(v: unknown): v is FilterPrimitive {\n\tconst t = typeof v;\n\treturn t === \"string\" || t === \"number\" || t === \"boolean\";\n}\n\n/**\n * Coerce a value to BigInt for arbitrary-precision compare — avoids the\n * silent precision loss of `Number()` on integers past 2^53. Returns\n * `null` for non-integer numerics, which is fine because the filter DSL\n * is documented as scalar-only + integer-amount-aware (Stacks uses\n * uint128 bigint columns everywhere).\n */\nfunction coerceBigInt(v: unknown): bigint | null {\n\tif (typeof v === \"bigint\") return v;\n\tif (typeof v === \"number\") {\n\t\tif (!Number.isFinite(v)) return null;\n\t\tif (!Number.isInteger(v)) return null;\n\t\treturn BigInt(v);\n\t}\n\tif (typeof v === \"string\" && /^-?\\d+$/.test(v)) {\n\t\ttry {\n\t\t\treturn BigInt(v);\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t}\n\treturn null;\n}\n\n/**\n * Float compare fallback — used when a value isn't a clean integer\n * (rare in subgraph columns but defensible for user-supplied filters).\n */\nfunction coerceFloat(v: unknown): number | null {\n\tif (typeof v === \"number\" && Number.isFinite(v)) return v;\n\tif (typeof v === \"bigint\") return Number(v);\n\tif (typeof v === \"string\" && v !== \"\" && !Number.isNaN(Number(v))) {\n\t\treturn Number(v);\n\t}\n\treturn null;\n}\n\nfunction compareNumeric(a: unknown, b: unknown): 0 | 1 | -1 | null {\n\tconst ba = coerceBigInt(a);\n\tconst bb = coerceBigInt(b);\n\tif (ba !== null && bb !== null) {\n\t\tif (ba === bb) return 0;\n\t\treturn ba > bb ? 1 : -1;\n\t}\n\tconst fa = coerceFloat(a);\n\tconst fb = coerceFloat(b);\n\tif (fa === null || fb === null) return null;\n\tif (fa === fb) return 0;\n\treturn fa > fb ? 1 : -1;\n}\n\nfunction matchClause(rowValue: unknown, clause: FilterClause): boolean {\n\t// Reject non-primitive row values — `{ value: {...} }` should not\n\t// match `filter: { value: \"[object Object]\" }` via loose stringify.\n\t// Bigints are not primitives in the JSON sense but are supported\n\t// scalar inputs from subgraph rows.\n\tconst rowIsPrimitive = isPrimitive(rowValue) || typeof rowValue === \"bigint\";\n\n\tif (isPrimitive(clause)) {\n\t\tif (!rowIsPrimitive) return false;\n\t\tif (\n\t\t\ttypeof clause === \"number\" ||\n\t\t\ttypeof rowValue === \"number\" ||\n\t\t\ttypeof rowValue === \"bigint\"\n\t\t) {\n\t\t\tconst cmp = compareNumeric(rowValue, clause);\n\t\t\tif (cmp !== null) return cmp === 0;\n\t\t}\n\t\treturn rowValue === clause || String(rowValue) === String(clause);\n\t}\n\n\tif (clause === null || typeof clause !== \"object\" || Array.isArray(clause)) {\n\t\treturn false;\n\t}\n\n\tconst keys = Object.keys(clause);\n\tif (keys.length !== 1) return false;\n\tconst op = keys[0];\n\n\tconst c = clause as Record<string, unknown>;\n\tswitch (op) {\n\t\tcase \"eq\":\n\t\t\treturn matchClause(rowValue, c.eq as FilterPrimitive);\n\t\tcase \"neq\":\n\t\t\treturn !matchClause(rowValue, c.neq as FilterPrimitive);\n\t\tcase \"gt\":\n\t\tcase \"gte\":\n\t\tcase \"lt\":\n\t\tcase \"lte\": {\n\t\t\tif (!rowIsPrimitive) return false;\n\t\t\tconst cmp = compareNumeric(rowValue, c[op]);\n\t\t\tif (cmp === null) return false;\n\t\t\tif (op === \"gt\") return cmp > 0;\n\t\t\tif (op === \"gte\") return cmp >= 0;\n\t\t\tif (op === \"lt\") return cmp < 0;\n\t\t\treturn cmp <= 0;\n\t\t}\n\t\tcase \"in\": {\n\t\t\tconst list = c.in;\n\t\t\tif (!Array.isArray(list)) return false;\n\t\t\tif (!rowIsPrimitive) return false;\n\t\t\treturn list.some(\n\t\t\t\t(item) => isPrimitive(item) && matchClause(rowValue, item),\n\t\t\t);\n\t\t}\n\t\tdefault:\n\t\t\treturn false;\n\t}\n}\n\nexport function matchesFilter(\n\tfilter: SubscriptionFilter | null | undefined,\n\trow: Record<string, unknown>,\n): boolean {\n\tif (!filter || Object.keys(filter).length === 0) return true;\n\tfor (const [col, clause] of Object.entries(filter)) {\n\t\tif (!matchClause(row[col], clause)) return false;\n\t}\n\treturn true;\n}\n\n// ── Cache keyed by (subgraph_name, table_name) ──────────────────────────\n\ntype MatcherKey = string;\n\nfunction key(subgraphName: string, tableName: string): MatcherKey {\n\treturn `${subgraphName}\u0000${tableName}`;\n}\n\nexport class SubscriptionMatcher {\n\tprivate readonly byKey = new Map<MatcherKey, Subscription[]>();\n\tprivate readonly byId = new Map<string, Subscription>();\n\n\t/** Replace the entire cache with a fresh snapshot. */\n\tsetAll(subs: Subscription[]): void {\n\t\tthis.byKey.clear();\n\t\tthis.byId.clear();\n\t\tfor (const sub of subs) {\n\t\t\tif (sub.status !== \"active\") continue;\n\t\t\t// This matcher keys on (subgraph, table) row writes; chain\n\t\t\t// subscriptions react to raw chain events and never flow through it.\n\t\t\tif (sub.kind !== \"subgraph\" || !sub.subgraph_name || !sub.table_name)\n\t\t\t\tcontinue;\n\t\t\tthis.byId.set(sub.id, sub);\n\t\t\tconst k = key(sub.subgraph_name, sub.table_name);\n\t\t\tconst arr = this.byKey.get(k);\n\t\t\tif (arr) arr.push(sub);\n\t\t\telse this.byKey.set(k, [sub]);\n\t\t}\n\t}\n\n\t/** Returns active subs for a (subgraph, table) whose filter matches the row. */\n\tmatch(\n\t\tsubgraphName: string,\n\t\ttableName: string,\n\t\trow: Record<string, unknown>,\n\t): Subscription[] {\n\t\tconst bucket = this.byKey.get(key(subgraphName, tableName));\n\t\tif (!bucket) return [];\n\t\tconst hits: Subscription[] = [];\n\t\tfor (const sub of bucket) {\n\t\t\tif (matchesFilter(sub.filter as SubscriptionFilter, row)) hits.push(sub);\n\t\t}\n\t\treturn hits;\n\t}\n\n\thas(subgraphName: string, tableName: string): boolean {\n\t\treturn this.byKey.has(key(subgraphName, tableName));\n\t}\n\n\tsize(): number {\n\t\treturn this.byId.size;\n\t}\n\n\tget(id: string): Subscription | undefined {\n\t\treturn this.byId.get(id);\n\t}\n}\n"
5
+ "import { randomUUID } from \"node:crypto\";\nimport { lookup as dnsLookup } from \"node:dns/promises\";\nimport {\n\ttype Database,\n\ttype Webhook,\n\ttype WebhookOutbox,\n\tgetTargetDb,\n} from \"@secondlayer/shared/db\";\nimport { getWebhookSigningSecret } from \"@secondlayer/shared/db/queries/webhooks\";\nimport { logger } from \"@secondlayer/shared/logger\";\nimport { listen, targetListenerUrl } from \"@secondlayer/shared/queue/listener\";\nimport type { WebhookTestResult } from \"@secondlayer/shared/schemas/webhooks\";\nimport { type Kysely, sql } from \"kysely\";\nimport { buildForFormat } from \"./formats/index.ts\";\nimport { meterDeliveryAttempt } from \"./hosted-meter.ts\";\nimport { refreshMatcher } from \"./webhook-state.ts\";\n\n/**\n * Webhook emitter — drains `webhook_outbox` and POSTs deliveries.\n *\n * Hot path: LISTEN on `webhooks:new_outbox` and `webhooks:changed`.\n * On notify, claim a batch with `FOR UPDATE SKIP LOCKED LIMIT 50`, dispatch\n * each row via HTTP, write a `webhook_deliveries` attempt row, then\n * either mark `status='delivered'` or schedule the next attempt.\n *\n * Backoff schedule (attempt → wait):\n * 0 → 30s, 1 → 2m, 2 → 10m, 3 → 1h, 4 → 6h, 5 → 24h, 6 → 72h.\n * After `max_retries` (default 7) attempts → `status='dead'`.\n *\n * Per-sub circuit breaker: 20 consecutive failures → sub flipped to\n * `paused` with `circuit_opened_at=NOW()`. Manual /resume drains backlog.\n *\n * Per-sub concurrency cap: in-memory semaphore, default 4 in-flight HTTP\n * requests per webhook. Sprint-4 adds SSRF allowlist.\n */\n\nconst BATCH_SIZE = 50;\nconst LIVE_SHARE = 0.9; // 90% of batch to non-replay, 10% to replay\nconst BACKOFF_SECONDS = [30, 120, 600, 3600, 21600, 86400, 259200];\nconst CIRCUIT_THRESHOLD = 20;\n/**\n * When a batch is claimed the outbox row's `next_attempt_at` is pushed\n * `LOCK_WINDOW_MS` into the future. Any crash between claim + settle\n * leaves the row re-claimable after this window expires — the SSOT for\n * double-dispatch prevention.\n *\n * Must exceed the maximum possible in-flight delivery time so a slow-but-alive\n * receiver's row is never re-claimed mid-delivery (duplicate dispatch). The max\n * webhook timeout is 300_000ms (see shared/schemas/webhooks.ts:307).\n */\nexport const MAX_WEBHOOK_TIMEOUT_MS = 300_000;\nexport const LOCK_WINDOW_MS: number = MAX_WEBHOOK_TIMEOUT_MS + 60_000; // 6 min: max timeout + settle margin\n\ninterface RunningState {\n\trunning: boolean;\n\tinFlightBySub: Map<string, number>;\n\tclaimInFlight: boolean;\n}\n\nfunction nextDelaySeconds(attempt: number): number {\n\t// biome-ignore lint/style/noNonNullAssertion: value is non-null after preceding check or by construction; TS narrowing limitation\n\treturn BACKOFF_SECONDS[Math.min(attempt, BACKOFF_SECONDS.length - 1)]!;\n}\n\n// ── SSRF guard ────────────────────────────────────────────────────────\n// Block deliveries to private/loopback/link-local ranges unless\n// SECONDLAYER_ALLOW_PRIVATE_EGRESS=true (self-host + local-dev opt-in).\n\nconst PRIVATE_V4_PATTERNS = [\n\t/^127\\./, // loopback\n\t/^10\\./, // private class A\n\t/^172\\.(1[6-9]|2\\d|3[01])\\./, // private class B\n\t/^192\\.168\\./, // private class C\n\t/^169\\.254\\./, // link-local\n\t/^0\\./, // \"this\" network\n\t/^100\\.(6[4-9]|[7-9]\\d|1[01]\\d|12[0-7])\\./, // CGNAT 100.64/10\n];\n\n/**\n * Classify a raw IP address literal (v4 or v6, no brackets, already lowercased\n * or not) as private/loopback/link-local/reserved. Covers v4 private ranges +\n * link-local (incl. `169.254.169.254`), `0.0.0.0`, IPv6 loopback (`::1`),\n * unspecified (`::`), unique-local (`fc00::/7`), link-local (`fe80::/10`), and\n * IPv4-mapped IPv6 (`::ffff:127.0.0.1`, `::ffff:7f00:0001`).\n *\n * Shared by `isPrivateEgress` (literal-hostname fast-fail) and the resolved-\n * DNS-address check in `checkEgressAllowed` (rebinding mitigation).\n */\nfunction isPrivateIp(address: string): boolean {\n\tconst host = address.toLowerCase();\n\n\tif (host === \"0.0.0.0\") return true;\n\tif (host === \"::\" || host === \"::1\") return true;\n\t// Unique-local (fc00::/7) + link-local (fe80::/10)\n\tif (/^f[cd][0-9a-f]{2}:/.test(host)) return true;\n\tif (/^fe[89ab][0-9a-f]:/.test(host)) return true;\n\n\t// IPv4-mapped IPv6 — `::ffff:127.0.0.1` or `::ffff:7f00:0001`\n\tconst mapped = host.match(/^::ffff:(.+)$/);\n\tif (mapped) {\n\t\t// biome-ignore lint/style/noNonNullAssertion: value is non-null after preceding check or by construction; TS narrowing limitation\n\t\tconst inner = mapped[1]!;\n\t\t// Dotted form: rerun v4 checks.\n\t\tif (/^\\d+\\.\\d+\\.\\d+\\.\\d+$/.test(inner)) {\n\t\t\tfor (const p of PRIVATE_V4_PATTERNS) if (p.test(inner)) return true;\n\t\t}\n\t\t// Hex form: 7f00:0001 → 127.0.0.1\n\t\tconst hex = inner.match(/^([0-9a-f]{1,4}):([0-9a-f]{1,4})$/);\n\t\tif (hex) {\n\t\t\t// biome-ignore lint/style/noNonNullAssertion: value is non-null after preceding check or by construction; TS narrowing limitation\n\t\t\tconst a = Number.parseInt(hex[1]!, 16);\n\t\t\t// biome-ignore lint/style/noNonNullAssertion: value is non-null after preceding check or by construction; TS narrowing limitation\n\t\t\tconst b = Number.parseInt(hex[2]!, 16);\n\t\t\tconst dotted = `${(a >> 8) & 0xff}.${a & 0xff}.${(b >> 8) & 0xff}.${b & 0xff}`;\n\t\t\tfor (const p of PRIVATE_V4_PATTERNS) if (p.test(dotted)) return true;\n\t\t}\n\t\treturn false;\n\t}\n\n\tfor (const p of PRIVATE_V4_PATTERNS) {\n\t\tif (p.test(host)) return true;\n\t}\n\treturn false;\n}\n\n/**\n * Reject hostnames spelled as private IPs (cheap, synchronous, no I/O — a\n * fast-fail pre-filter). DNS-level rebinding (hostname that *resolves* to a\n * private IP at egress time) is NOT caught here — see `checkEgressAllowed`,\n * which resolves DNS and validates every returned address.\n */\nfunction isPrivateEgress(url: string): boolean {\n\tlet parsed: URL;\n\ttry {\n\t\tparsed = new URL(url);\n\t} catch {\n\t\treturn true; // malformed URL: reject\n\t}\n\tif (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\") {\n\t\treturn true;\n\t}\n\n\t// Strip brackets from IPv6 literals.\n\tconst raw = parsed.hostname.toLowerCase();\n\tconst host =\n\t\traw.startsWith(\"[\") && raw.endsWith(\"]\") ? raw.slice(1, -1) : raw;\n\n\tif (host === \"localhost\") return true;\n\treturn isPrivateIp(host);\n}\n\nfunction allowPrivateEgress(): boolean {\n\treturn process.env.SECONDLAYER_ALLOW_PRIVATE_EGRESS === \"true\";\n}\n\ninterface ResolvedAddress {\n\taddress: string;\n\tfamily: number;\n}\n\ntype DnsLookupFn = (hostname: string) => Promise<ResolvedAddress[]>;\n\nasync function defaultDnsLookup(hostname: string): Promise<ResolvedAddress[]> {\n\treturn dnsLookup(hostname, { all: true });\n}\n\nlet dnsLookupImpl: DnsLookupFn = defaultDnsLookup;\n\n/**\n * Test-only seam: inject a fake DNS resolver so tests can simulate a hostname\n * resolving to a private/metadata IP without depending on real DNS or\n * network access. Pass `null` to restore the real `dns.lookup`-backed\n * resolver. Production code never calls this — only `ssrf.test.ts` does.\n */\nexport function __setDnsLookupForTest(fn: DnsLookupFn | null): void {\n\tdnsLookupImpl = fn ?? defaultDnsLookup;\n}\n\nconst ALLOW_HINT = \"(set SECONDLAYER_ALLOW_PRIVATE_EGRESS=true to allow)\";\n\n/**\n * Full egress check: literal fast-fail, then resolve DNS and reject if *any*\n * returned address (not just the first) is private/loopback/link-local —\n * a single private answer among several is enough for an attacker to abuse.\n *\n * Residual TOCTOU: this resolves-and-validates *immediately before* `fetch()`\n * but does not pin the actual connection to the validated address. True\n * pinning (resolve once, connect to that literal IP, keep the original `Host`\n * header/SNI) was attempted per plan f053 via a custom undici `Agent` with\n * `connect.lookup` passed as `fetch()`'s `dispatcher` — the standard Node\n * ecosystem pattern for this. Empirically, under Bun 1.3.10, that hook is\n * silently never invoked (confirmed with a throwaway smoke test hitting a\n * real local HTTP server: `lookupCalled` stayed `false` and the connection\n * failed even though the dispatcher was passed) — Bun's `fetch()` does not\n * implement undici's `connect.lookup` dispatcher hook, whether reached via\n * the global `fetch` or `import { fetch } from \"undici\"` (Bun overrides\n * \"undici\"'s `fetch` export with its own native implementation regardless of\n * an installed real `undici` package). So a second DNS answer between this\n * check and `fetch()`'s own resolution (true rebinding, not just a one-time\n * private answer) could in theory still slip through. This closes the\n * practical rebinding gap (attacker's hostname resolves private at delivery\n * time) without adding a dependency; a network-level egress allowlist/proxy\n * is the recommended defense-in-depth for the remaining TOCTOU sliver.\n */\nexport async function checkEgressAllowed(url: string): Promise<string | null> {\n\tif (isPrivateEgress(url)) {\n\t\treturn `refused private egress: literal address ${ALLOW_HINT}`;\n\t}\n\n\tlet parsed: URL;\n\ttry {\n\t\tparsed = new URL(url);\n\t} catch {\n\t\treturn `refused private egress: malformed URL ${ALLOW_HINT}`;\n\t}\n\tif (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\") {\n\t\treturn `refused private egress: unsupported protocol ${ALLOW_HINT}`;\n\t}\n\n\tconst raw = parsed.hostname.toLowerCase();\n\tconst hostname =\n\t\traw.startsWith(\"[\") && raw.endsWith(\"]\") ? raw.slice(1, -1) : raw;\n\n\tlet resolved: ResolvedAddress[];\n\ttry {\n\t\tresolved = await dnsLookupImpl(hostname);\n\t} catch (err) {\n\t\tconst msg = err instanceof Error ? err.message : String(err);\n\t\treturn `refused private egress: DNS resolution failed (${msg}) ${ALLOW_HINT}`;\n\t}\n\tif (resolved.length === 0) {\n\t\treturn `refused private egress: DNS resolution returned no addresses ${ALLOW_HINT}`;\n\t}\n\tfor (const { address } of resolved) {\n\t\tif (isPrivateIp(address)) {\n\t\t\treturn `refused private egress: hostname resolves to private address ${address} ${ALLOW_HINT}`;\n\t\t}\n\t}\n\treturn null;\n}\n\n/** The wire result of one POST attempt (no DB side effect) — shared by the\n * emitter hot path and the `deliverTestEvent` test path. */\ninterface PostResult {\n\tok: boolean;\n\tstatusCode: number | null;\n\terror: string | null;\n\tdurationMs: number;\n\tresponseBody: string | null;\n\tresponseHeaders: Record<string, string> | null;\n}\n\n/**\n * Bound on redirect hops honored while delivering a webhook. `fetch` is\n * called with `redirect: \"manual\"` so a 3xx never auto-follows — each hop's\n * target is re-validated through `checkEgressAllowed` before it is fetched\n * (see `postToWebhook`), otherwise a `Location` header pointing at a\n * private/metadata address would bypass the egress guard entirely. Counts\n * every fetch issued (the initial request plus each redirect), so this is\n * also the max number of `fetch` calls one delivery attempt can make.\n */\nexport const MAX_REDIRECT_HOPS: number = 3;\n\n/** POST a pre-built body to a webhook URL with the SSRF guard + timeout.\n * Pure transport: returns the attempt result; the caller logs the delivery row.\n * Follows redirects manually (up to `MAX_REDIRECT_HOPS`), re-running the\n * egress guard against every hop's target — the guard only protects the\n * first request otherwise, and a webhook target can redirect to a private/\n * metadata address to read back the response after f053 pinned the guard to\n * just the original URL. */\nasync function postToWebhook(\n\turl: string,\n\tbody: string,\n\theaders: Record<string, string>,\n\ttimeoutMs: number,\n): Promise<PostResult> {\n\tconst start = performance.now();\n\tlet target = url;\n\tlet statusCode: number | null = null;\n\tlet error: string | null = null;\n\tlet ok = false;\n\tlet responseBody = \"\";\n\tlet responseHeaders: Record<string, string> | null = null;\n\ttry {\n\t\tfor (let hop = 0; ; hop++) {\n\t\t\tif (!allowPrivateEgress()) {\n\t\t\t\tconst refusal = await checkEgressAllowed(target);\n\t\t\t\tif (refusal) {\n\t\t\t\t\tlogger.warn(\"[emitter] refused private egress\", {\n\t\t\t\t\t\turl: target,\n\t\t\t\t\t\treason: refusal,\n\t\t\t\t\t});\n\t\t\t\t\treturn {\n\t\t\t\t\t\tok: false,\n\t\t\t\t\t\tstatusCode: null,\n\t\t\t\t\t\terror: refusal,\n\t\t\t\t\t\tdurationMs: Math.round(performance.now() - start),\n\t\t\t\t\t\tresponseBody: null,\n\t\t\t\t\t\tresponseHeaders: null,\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (hop >= MAX_REDIRECT_HOPS) {\n\t\t\t\treturn {\n\t\t\t\t\tok: false,\n\t\t\t\t\tstatusCode,\n\t\t\t\t\terror: `too many redirects (exceeded ${MAX_REDIRECT_HOPS} hops)`,\n\t\t\t\t\tdurationMs: Math.round(performance.now() - start),\n\t\t\t\t\tresponseBody: null,\n\t\t\t\t\tresponseHeaders: null,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tconst res = await fetch(target, {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\theaders,\n\t\t\t\tbody,\n\t\t\t\tredirect: \"manual\",\n\t\t\t\tsignal: AbortSignal.timeout(timeoutMs),\n\t\t\t});\n\t\t\tstatusCode = res.status;\n\n\t\t\tconst location = res.headers.get(\"location\");\n\t\t\tif (res.status >= 300 && res.status < 400 && location) {\n\t\t\t\ttarget = new URL(location, target).toString();\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tok = res.ok;\n\t\t\t// Collect small response preview for the delivery log (≤8KB).\n\t\t\tconst buf = await res.arrayBuffer();\n\t\t\tconst truncated = buf.byteLength > 8192 ? buf.slice(0, 8192) : buf;\n\t\t\tresponseBody = Buffer.from(truncated).toString(\"utf8\");\n\t\t\tresponseHeaders = Object.fromEntries(res.headers.entries());\n\t\t\tbreak;\n\t\t}\n\t} catch (err) {\n\t\terror = err instanceof Error ? err.message : String(err);\n\t}\n\treturn {\n\t\tok,\n\t\tstatusCode,\n\t\terror,\n\t\tdurationMs: Math.round(performance.now() - start),\n\t\tresponseBody: responseBody || null,\n\t\tresponseHeaders,\n\t};\n}\n\n/**\n * Test-only seam: exposes `postToWebhook` (otherwise module-private) so\n * redirect/egress behavior can be exercised directly against a stubbed\n * `fetch` + injected DNS lookup, without standing up the DB-backed emitter\n * loop. Production code never calls this — only tests do.\n */\nexport const __postToWebhookForTest: (\n\turl: string,\n\tbody: string,\n\theaders: Record<string, string>,\n\ttimeoutMs: number,\n) => Promise<PostResult> = postToWebhook;\n\nasync function dispatchOne(\n\tdb: Kysely<Database>,\n\toutboxRow: WebhookOutbox,\n\tsub: Webhook,\n): Promise<{\n\tok: boolean;\n\tstatusCode: number | null;\n\terror: string | null;\n\tdurationMs: number;\n}> {\n\tconst { body, headers } = buildForFormat(\n\t\toutboxRow,\n\t\tsub,\n\t\tgetWebhookSigningSecret(sub),\n\t);\n\tconst r = await postToWebhook(sub.url, body, headers, sub.timeout_ms);\n\n\tconst attempt = outboxRow.attempt + 1;\n\tawait db\n\t\t.insertInto(\"webhook_deliveries\")\n\t\t.values({\n\t\t\toutbox_id: outboxRow.id,\n\t\t\twebhook_id: outboxRow.webhook_id,\n\t\t\tattempt,\n\t\t\tstatus_code: r.statusCode,\n\t\t\tresponse_headers: r.responseHeaders,\n\t\t\tresponse_body: r.responseBody,\n\t\t\terror_message: r.error,\n\t\t\tduration_ms: r.durationMs,\n\t\t})\n\t\t.execute();\n\n\tawait meterDeliveryAttempt(sub.account_id, sub.id);\n\n\treturn {\n\t\tok: r.ok,\n\t\tstatusCode: r.statusCode,\n\t\terror: r.error,\n\t\tdurationMs: r.durationMs,\n\t};\n}\n\n/** A representative (non-persisted) outbox row for a test delivery, shaped to the\n * webhook's kind so `buildForFormat` produces a realistic body. */\nfunction buildTestOutboxRow(sub: Webhook): WebhookOutbox {\n\tconst now = new Date();\n\treturn {\n\t\tid: randomUUID(),\n\t\twebhook_id: sub.id,\n\t\tkind: sub.kind,\n\t\tsubgraph_name: sub.subgraph_name ?? null,\n\t\ttable_name: sub.table_name ?? null,\n\t\tblock_height: 0,\n\t\ttx_id: null,\n\t\trow_pk: null,\n\t\tevent_type:\n\t\t\tsub.kind === \"chain\"\n\t\t\t\t? \"chain.test.apply\"\n\t\t\t\t: `${sub.subgraph_name ?? \"subgraph\"}.${sub.table_name ?? \"test\"}.created`,\n\t\tpayload: {\n\t\t\ttest: true,\n\t\t\tmessage: \"Secondlayer test delivery\",\n\t\t\twebhook_id: sub.id,\n\t\t\tsubscription_id: sub.id, // deprecated alias, removed in plan 015\n\t\t\tsent_at: now.toISOString(),\n\t\t},\n\t\tdedup_key: `test:${sub.id}:${now.getTime()}`,\n\t\tattempt: 0,\n\t\tnext_attempt_at: now,\n\t\tstatus: \"pending\",\n\t\tis_replay: false,\n\t\tdelivered_at: null,\n\t\tfailed_at: null,\n\t\tlocked_by: null,\n\t\tlocked_until: null,\n\t\tcreated_at: now,\n\t};\n}\n\n/**\n * Build a representative webhook for `sub`'s configured format, POST it (same\n * SSRF guard + timeout + signing as a real delivery), and log a delivery row\n * with a null `outbox_id` so it appears under the webhook's deliveries\n * without being tied to a queued event. Powers `POST /:id/test`.\n */\nexport async function deliverTestEvent(\n\tdb: Kysely<Database>,\n\tsub: Webhook,\n): Promise<WebhookTestResult> {\n\tconst testRow = buildTestOutboxRow(sub);\n\tconst { body, headers } = buildForFormat(\n\t\ttestRow,\n\t\tsub,\n\t\tgetWebhookSigningSecret(sub),\n\t);\n\tconst r = await postToWebhook(sub.url, body, headers, sub.timeout_ms);\n\tconst inserted = await db\n\t\t.insertInto(\"webhook_deliveries\")\n\t\t.values({\n\t\t\toutbox_id: null,\n\t\t\twebhook_id: sub.id,\n\t\t\tattempt: 1,\n\t\t\tstatus_code: r.statusCode,\n\t\t\tresponse_headers: r.responseHeaders,\n\t\t\tresponse_body: r.responseBody,\n\t\t\terror_message: r.error,\n\t\t\tduration_ms: r.durationMs,\n\t\t})\n\t\t.returning(\"id\")\n\t\t.executeTakeFirstOrThrow();\n\treturn {\n\t\tok: r.ok,\n\t\tstatusCode: r.statusCode,\n\t\terror: r.error,\n\t\tdurationMs: r.durationMs,\n\t\tdeliveryId: inserted.id,\n\t};\n}\n\nasync function settleDelivered(\n\tdb: Kysely<Database>,\n\toutboxRow: WebhookOutbox,\n): Promise<void> {\n\tawait db.transaction().execute(async (tx) => {\n\t\tawait tx\n\t\t\t.updateTable(\"webhook_outbox\")\n\t\t\t.set({\n\t\t\t\tstatus: \"delivered\",\n\t\t\t\tdelivered_at: new Date(),\n\t\t\t\tattempt: outboxRow.attempt + 1,\n\t\t\t\tlocked_by: null,\n\t\t\t\tlocked_until: null,\n\t\t\t})\n\t\t\t.where(\"id\", \"=\", outboxRow.id)\n\t\t\t.execute();\n\t\tawait tx\n\t\t\t.updateTable(\"webhooks\")\n\t\t\t.set({\n\t\t\t\tlast_delivery_at: new Date(),\n\t\t\t\tlast_success_at: new Date(),\n\t\t\t\tcircuit_failures: 0,\n\t\t\t\tlast_error: null,\n\t\t\t\tupdated_at: new Date(),\n\t\t\t})\n\t\t\t.where(\"id\", \"=\", outboxRow.webhook_id)\n\t\t\t.execute();\n\t});\n}\n\nasync function settleFailed(\n\tdb: Kysely<Database>,\n\toutboxRow: WebhookOutbox,\n\tsub: Webhook,\n\terrText: string,\n): Promise<void> {\n\tconst attempt = outboxRow.attempt + 1;\n\tconst isDead = attempt >= sub.max_retries;\n\tconst nextAt = isDead\n\t\t? null\n\t\t: new Date(Date.now() + nextDelaySeconds(outboxRow.attempt) * 1000);\n\n\tawait db.transaction().execute(async (tx) => {\n\t\tawait tx\n\t\t\t.updateTable(\"webhook_outbox\")\n\t\t\t.set({\n\t\t\t\tattempt,\n\t\t\t\tnext_attempt_at: nextAt ?? new Date(),\n\t\t\t\tstatus: isDead ? \"dead\" : \"pending\",\n\t\t\t\tfailed_at: isDead ? new Date() : null,\n\t\t\t\tlocked_by: null,\n\t\t\t\tlocked_until: null,\n\t\t\t})\n\t\t\t.where(\"id\", \"=\", outboxRow.id)\n\t\t\t.execute();\n\n\t\t// Atomic increment — concurrent failures must not clobber each other.\n\t\t// `RETURNING circuit_failures` gives us the post-increment value to\n\t\t// decide whether this failure tripped the circuit.\n\t\tconst incResult = await sql<{ circuit_failures: number }>`\n\t\t\tUPDATE webhooks\n\t\t\tSET circuit_failures = circuit_failures + 1,\n\t\t\t\tlast_delivery_at = NOW(),\n\t\t\t\tlast_error = ${errText.slice(0, 500)},\n\t\t\t\tupdated_at = NOW()\n\t\t\tWHERE id = ${sub.id}\n\t\t\tRETURNING circuit_failures\n\t\t`.execute(tx);\n\t\tconst newFailures =\n\t\t\tincResult.rows[0]?.circuit_failures ?? sub.circuit_failures + 1;\n\t\tconst shouldTripCircuit = newFailures >= CIRCUIT_THRESHOLD;\n\n\t\tif (shouldTripCircuit) {\n\t\t\t// Transition to paused only on the first failure that crossed\n\t\t\t// the threshold — additional failures in-flight harmlessly\n\t\t\t// re-set the same fields.\n\t\t\tawait tx\n\t\t\t\t.updateTable(\"webhooks\")\n\t\t\t\t.set({\n\t\t\t\t\tstatus: \"paused\",\n\t\t\t\t\tcircuit_opened_at: new Date(),\n\t\t\t\t\tupdated_at: new Date(),\n\t\t\t\t})\n\t\t\t\t.where(\"id\", \"=\", sub.id)\n\t\t\t\t.execute();\n\t\t\tlogger.warn(\n\t\t\t\t\"Webhook circuit tripped — paused after consecutive failures\",\n\t\t\t\t{\n\t\t\t\t\twebhook: sub.name,\n\t\t\t\t\tfailures: newFailures,\n\t\t\t\t},\n\t\t\t);\n\t\t}\n\t});\n}\n\nasync function claimAndDrain(\n\tdb: Kysely<Database>,\n\tstate: RunningState,\n\temitterId: string,\n): Promise<number> {\n\tif (state.claimInFlight) return 0;\n\tstate.claimInFlight = true;\n\ttry {\n\t\t// FOR UPDATE SKIP LOCKED — multiple emitters split the batch.\n\t\t// 90/10 live vs replay so a big replay doesn't starve live emits.\n\t\tconst liveLimit = Math.max(1, Math.round(BATCH_SIZE * LIVE_SHARE));\n\t\tconst replayLimit = BATCH_SIZE - liveLimit;\n\t\tconst claimed = await db.transaction().execute(async (tx) => {\n\t\t\tconst live = await sql<WebhookOutbox>`\n\t\t\t\t\tSELECT * FROM webhook_outbox\n\t\t\t\t\tWHERE status = 'pending'\n\t\t\t\t\t\tAND next_attempt_at <= NOW()\n\t\t\t\t\t\tAND is_replay = FALSE\n\t\t\t\t\tORDER BY next_attempt_at ASC\n\t\t\t\t\tFOR UPDATE SKIP LOCKED\n\t\t\t\t\tLIMIT ${sql.lit(liveLimit)}\n\t\t\t\t`.execute(tx);\n\t\t\tconst replay = await sql<WebhookOutbox>`\n\t\t\t\t\tSELECT * FROM webhook_outbox\n\t\t\t\t\tWHERE status = 'pending'\n\t\t\t\t\t\tAND next_attempt_at <= NOW()\n\t\t\t\t\t\tAND is_replay = TRUE\n\t\t\t\t\tORDER BY next_attempt_at ASC\n\t\t\t\t\tFOR UPDATE SKIP LOCKED\n\t\t\t\t\tLIMIT ${sql.lit(replayLimit)}\n\t\t\t\t`.execute(tx);\n\n\t\t\tconst combined = [...live.rows, ...replay.rows];\n\t\t\tif (combined.length === 0) return [];\n\n\t\t\t// Push `next_attempt_at` forward by the lock window. This is\n\t\t\t// the only defense against double-dispatch if the emitter\n\t\t\t// process crashes mid-HTTP-call: the row won't be re-claimable\n\t\t\t// until `LOCK_WINDOW_MS` elapses, giving us a stale-lock\n\t\t\t// recovery window. `settleDelivered`/`settleFailed` overrides\n\t\t\t// this on the success/failure path.\n\t\t\tconst now = new Date();\n\t\t\tconst lockUntil = new Date(now.getTime() + LOCK_WINDOW_MS);\n\t\t\tawait tx\n\t\t\t\t.updateTable(\"webhook_outbox\")\n\t\t\t\t.set({\n\t\t\t\t\tlocked_by: emitterId,\n\t\t\t\t\tlocked_until: lockUntil,\n\t\t\t\t\tnext_attempt_at: lockUntil,\n\t\t\t\t})\n\t\t\t\t.where(\n\t\t\t\t\t\"id\",\n\t\t\t\t\t\"in\",\n\t\t\t\t\tcombined.map((r) => r.id),\n\t\t\t\t)\n\t\t\t\t.execute();\n\t\t\treturn combined;\n\t\t});\n\n\t\tif (claimed.length === 0) return 0;\n\n\t\t// Hydrate each claimed row's sub once, then dispatch with per-sub\n\t\t// concurrency cap enforced via in-memory semaphore.\n\t\tconst bySubId = new Map<string, WebhookOutbox[]>();\n\t\tfor (const row of claimed) {\n\t\t\tconst arr = bySubId.get(row.webhook_id);\n\t\t\tif (arr) arr.push(row);\n\t\t\telse bySubId.set(row.webhook_id, [row]);\n\t\t}\n\n\t\tconst subIds = Array.from(bySubId.keys());\n\t\tconst subs = await db\n\t\t\t.selectFrom(\"webhooks\")\n\t\t\t.selectAll()\n\t\t\t.where(\"id\", \"in\", subIds)\n\t\t\t.execute();\n\t\tconst subById = new Map(subs.map((s) => [s.id, s]));\n\n\t\tawait Promise.all(\n\t\t\tsubIds.map((subId) =>\n\t\t\t\t// biome-ignore lint/style/noNonNullAssertion: value is non-null after preceding check or by construction; TS narrowing limitation\n\t\t\t\tdrainForSub(db, state, subById.get(subId)!, bySubId.get(subId)!),\n\t\t\t),\n\t\t);\n\n\t\treturn claimed.length;\n\t} finally {\n\t\tstate.claimInFlight = false;\n\t}\n}\n\nasync function drainForSub(\n\tdb: Kysely<Database>,\n\tstate: RunningState,\n\tsub: Webhook,\n\trows: WebhookOutbox[],\n): Promise<void> {\n\tif (sub.status !== \"active\") return;\n\tconst cap = sub.concurrency || 4;\n\tconst counter = () => state.inFlightBySub.get(sub.id) ?? 0;\n\tconst inc = () => state.inFlightBySub.set(sub.id, counter() + 1);\n\tconst dec = () => state.inFlightBySub.set(sub.id, Math.max(0, counter() - 1));\n\n\tconst queue = [...rows];\n\tconst workers: Promise<void>[] = [];\n\tconst slots = Math.min(cap, queue.length);\n\n\tfor (let i = 0; i < slots; i++) {\n\t\tworkers.push(\n\t\t\t(async () => {\n\t\t\t\twhile (state.running && queue.length > 0) {\n\t\t\t\t\tconst row = queue.shift();\n\t\t\t\t\tif (!row) break;\n\t\t\t\t\tinc();\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst result = await dispatchOne(db, row, sub);\n\t\t\t\t\t\tif (result.ok) {\n\t\t\t\t\t\t\tawait settleDelivered(db, row);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tconst err = result.error ?? `HTTP ${result.statusCode ?? \"?\"}`;\n\t\t\t\t\t\t\tawait settleFailed(db, row, sub, err);\n\t\t\t\t\t\t}\n\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\tlogger.error(\"Emitter dispatch crashed\", {\n\t\t\t\t\t\t\toutboxId: row.id,\n\t\t\t\t\t\t\terror: err instanceof Error ? err.message : String(err),\n\t\t\t\t\t\t});\n\t\t\t\t\t\tawait settleFailed(\n\t\t\t\t\t\t\tdb,\n\t\t\t\t\t\t\trow,\n\t\t\t\t\t\t\tsub,\n\t\t\t\t\t\t\terr instanceof Error ? err.message : String(err),\n\t\t\t\t\t\t);\n\t\t\t\t\t} finally {\n\t\t\t\t\t\tdec();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t})(),\n\t\t);\n\t}\n\tawait Promise.all(workers);\n}\n\nexport interface StartEmitterOptions {\n\t/** Interval for the background poll (ms). Defaults to 2 minutes. */\n\tpollIntervalMs?: number;\n\t/** Retention sweep interval (ms). Defaults to 1 hour. */\n\tretentionIntervalMs?: number;\n}\n\nasync function runRetention(db: Kysely<Database>): Promise<void> {\n\t// delivered outbox >7d, deliveries >30d, dead outbox >90d\n\tawait sql`\n\t\tDELETE FROM webhook_outbox\n\t\tWHERE status = 'delivered' AND delivered_at < NOW() - interval '7 days'\n\t`.execute(db);\n\tawait sql`\n\t\tDELETE FROM webhook_deliveries\n\t\tWHERE dispatched_at < NOW() - interval '30 days'\n\t`.execute(db);\n\tawait sql`\n\t\tDELETE FROM webhook_outbox\n\t\tWHERE status = 'dead' AND failed_at < NOW() - interval '90 days'\n\t`.execute(db);\n}\n\nexport async function startEmitter(\n\topts?: StartEmitterOptions,\n): Promise<() => Promise<void>> {\n\tconst emitterId = `emitter-${Math.random().toString(36).slice(2, 10)}`;\n\tconst db = getTargetDb();\n\tconst state: RunningState = {\n\t\trunning: true,\n\t\tinFlightBySub: new Map(),\n\t\tclaimInFlight: false,\n\t};\n\tconst pollIntervalMs = opts?.pollIntervalMs ?? 120_000;\n\tconst retentionIntervalMs = opts?.retentionIntervalMs ?? 60 * 60_000;\n\n\tlogger.info(\"[emitter] started\", { id: emitterId });\n\n\t// Bootstrap matcher from active subs. Retry with backoff — if this\n\t// stays broken, fail loud rather than run with an empty matcher (which\n\t// would silently drop every block's outbox emissions until the next\n\t// webhook CRUD fired `webhooks:changed`).\n\tconst MATCHER_BOOT_ATTEMPTS = 5;\n\tlet lastErr: unknown = null;\n\tfor (let i = 0; i < MATCHER_BOOT_ATTEMPTS; i++) {\n\t\ttry {\n\t\t\tawait refreshMatcher(db);\n\t\t\tlastErr = null;\n\t\t\tbreak;\n\t\t} catch (err) {\n\t\t\tlastErr = err;\n\t\t\tconst delayMs = 500 * 2 ** i; // 500ms, 1s, 2s, 4s, 8s\n\t\t\tlogger.warn(\"[emitter] matcher refresh failed, retrying\", {\n\t\t\t\tattempt: i + 1,\n\t\t\t\tdelayMs,\n\t\t\t\terror: err instanceof Error ? err.message : String(err),\n\t\t\t});\n\t\t\tawait new Promise((r) => setTimeout(r, delayMs));\n\t\t}\n\t}\n\tif (lastErr) {\n\t\tthrow new Error(\n\t\t\t`[emitter] matcher refresh failed ${MATCHER_BOOT_ATTEMPTS}×; aborting boot: ${\n\t\t\t\tlastErr instanceof Error ? lastErr.message : String(lastErr)\n\t\t\t}`,\n\t\t);\n\t}\n\n\t// LISTEN on new outbox + sub changes. Both channels fire on the TARGET DB\n\t// (webhook_outbox + webhooks are control-plane tables), so bind the\n\t// listener there — under the split it is NOT `DATABASE_URL`.\n\tconst listenUrl = targetListenerUrl();\n\tconst stopNew = await listen(\n\t\t\"webhooks:new_outbox\",\n\t\t() => {\n\t\t\tif (!state.running) return;\n\t\t\tvoid claimAndDrain(db, state, emitterId).catch((err) =>\n\t\t\t\tlogger.error(\"[emitter] claim failed\", {\n\t\t\t\t\terror: err instanceof Error ? err.message : String(err),\n\t\t\t\t}),\n\t\t\t);\n\t\t},\n\t\t{ connectionString: listenUrl },\n\t);\n\tconst stopChanged = await listen(\n\t\t\"webhooks:changed\",\n\t\t() => {\n\t\t\tif (!state.running) return;\n\t\t\tvoid refreshMatcher(db).catch((err) =>\n\t\t\t\tlogger.error(\"[emitter] matcher refresh failed\", {\n\t\t\t\t\terror: err instanceof Error ? err.message : String(err),\n\t\t\t\t}),\n\t\t\t);\n\t\t},\n\t\t{ connectionString: listenUrl },\n\t);\n\n\t// Poll every pollIntervalMs as a safety net for missed notifications +\n\t// backoff wakeups (rows whose next_attempt_at has passed).\n\tconst poll = setInterval(() => {\n\t\tif (!state.running) return;\n\t\tvoid claimAndDrain(db, state, emitterId).catch((err) =>\n\t\t\tlogger.error(\"[emitter] poll claim failed\", {\n\t\t\t\terror: err instanceof Error ? err.message : String(err),\n\t\t\t}),\n\t\t);\n\t}, pollIntervalMs);\n\n\t// Kick once on startup so any rows that arrived before we started drain.\n\tvoid claimAndDrain(db, state, emitterId);\n\n\t// Retention sweep — hourly by default.\n\tconst retention = setInterval(() => {\n\t\tif (!state.running) return;\n\t\tvoid runRetention(db).catch((err) =>\n\t\t\tlogger.error(\"[emitter] retention failed\", {\n\t\t\t\terror: err instanceof Error ? err.message : String(err),\n\t\t\t}),\n\t\t);\n\t}, retentionIntervalMs);\n\n\treturn async () => {\n\t\tstate.running = false;\n\t\tclearInterval(poll);\n\t\tclearInterval(retention);\n\t\tawait stopNew();\n\t\tawait stopChanged();\n\t\tlogger.info(\"[emitter] stopped\", { id: emitterId });\n\t};\n}\n",
6
+ "import { signSecondlayerWebhook } from \"@secondlayer/shared/crypto/secondlayer-webhook\";\nimport type { Webhook, WebhookOutbox } from \"@secondlayer/shared/db\";\nimport { logger } from \"@secondlayer/shared/logger\";\nimport { buildCloudEvents } from \"./cloudevents.ts\";\nimport { buildCloudflare } from \"./cloudflare.ts\";\nimport { buildInngest } from \"./inngest.ts\";\nimport { buildRaw } from \"./raw.ts\";\nimport { buildStandardWebhooks } from \"./standard-webhooks.ts\";\nimport { buildTrigger } from \"./trigger.ts\";\n\nexport interface FormatBuildResult {\n\tbody: string;\n\theaders: Record<string, string>;\n}\n\nfunction buildBody(\n\toutboxRow: WebhookOutbox,\n\tsub: Webhook,\n\tsigningSecret: string,\n): FormatBuildResult {\n\tswitch (sub.format) {\n\t\tcase \"inngest\":\n\t\t\treturn buildInngest(outboxRow);\n\t\tcase \"trigger\":\n\t\t\treturn buildTrigger(outboxRow, sub);\n\t\tcase \"cloudflare\":\n\t\t\treturn buildCloudflare(outboxRow, sub);\n\t\tcase \"cloudevents\":\n\t\t\treturn buildCloudEvents(outboxRow, sub);\n\t\tcase \"raw\":\n\t\t\treturn buildRaw(outboxRow, sub);\n\t\tcase \"standard-webhooks\":\n\t\t\treturn buildStandardWebhooks(outboxRow, signingSecret);\n\t\tdefault:\n\t\t\tlogger.warn(\"Unknown webhook format, falling back to standard-webhooks\", {\n\t\t\t\tformat: sub.format,\n\t\t\t\twebhookId: sub.id,\n\t\t\t});\n\t\t\treturn buildStandardWebhooks(outboxRow, signingSecret);\n\t}\n}\n\n/**\n * Dispatch an outbox row through the format matching the webhook's\n * `format` column. Unknown formats fall back to `standard-webhooks` with\n * a warning log — receivers always get something deliverable.\n *\n * Every delivery, whatever its body shape, also gets the universal Secondlayer\n * authenticity headers (`webhook-id` + `X-Secondlayer-Signature`) so a receiver\n * can prove the payload came from us with one published public key — not just\n * the `standard-webhooks` format. The signature covers the exact body bytes\n * built here, so it must be attached after the body is final.\n */\nexport function buildForFormat(\n\toutboxRow: WebhookOutbox,\n\tsub: Webhook,\n\tsigningSecret: string,\n): FormatBuildResult {\n\tconst result = buildBody(outboxRow, sub, signingSecret);\n\tconst sigHeaders = signSecondlayerWebhook(outboxRow.id, result.body);\n\tif (sigHeaders) {\n\t\tresult.headers = { ...result.headers, ...sigHeaders };\n\t}\n\treturn result;\n}\n\nexport {\n\tbuildStandardWebhooks,\n\tbuildInngest,\n\tbuildTrigger,\n\tbuildCloudflare,\n\tbuildCloudEvents,\n\tbuildRaw,\n};\n",
7
+ "import type { Webhook, WebhookOutbox } from \"@secondlayer/shared/db\";\n\n/**\n * CloudEvents 1.0 structured JSON — https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/formats/json-format.md\n *\n * Body shape:\n * {\n * \"specversion\": \"1.0\",\n * \"type\": \"<subgraph>.<table>.<created|updated|deleted>\",\n * \"source\": \"secondlayer:<subgraph_name>\",\n * \"id\": <outbox.id>, // UUID, used for dedup\n * \"time\": <ISO 8601>,\n * \"datacontenttype\": \"application/json\",\n * \"data\": { ...row }\n * }\n *\n * Content-Type is `application/cloudevents+json; charset=utf-8`. Binary\n * mode (headers as ce-*) isn't needed — structured mode is what every CE\n * SDK accepts out of the box.\n */\n\nexport function buildCloudEvents(\n\toutboxRow: WebhookOutbox,\n\t_sub: Webhook,\n): { body: string; headers: Record<string, string> } {\n\tconst event = {\n\t\tspecversion: \"1.0\",\n\t\ttype: outboxRow.event_type,\n\t\tsource: `secondlayer:${outboxRow.subgraph_name}`,\n\t\tid: outboxRow.id,\n\t\ttime: new Date(outboxRow.created_at).toISOString(),\n\t\tdatacontenttype: \"application/json\",\n\t\tdata: outboxRow.payload,\n\t};\n\treturn {\n\t\tbody: JSON.stringify(event),\n\t\theaders: {\n\t\t\t\"content-type\": \"application/cloudevents+json; charset=utf-8\",\n\t\t},\n\t};\n}\n",
8
+ "import { decryptSecret } from \"@secondlayer/shared/crypto/secrets\";\nimport type { Webhook, WebhookOutbox } from \"@secondlayer/shared/db\";\n\n/**\n * Cloudflare Workflows — https://developers.cloudflare.com/workflows/build/events-and-parameters/\n *\n * POST https://api.cloudflare.com/client/v4/accounts/{account}/workflows/{name}/instances\n * Authorization: Bearer <CF_API_TOKEN>\n *\n * Body: `{ params }` — the workflow entrypoint receives this as the\n * `event.payload` object. We slip the outbox id into `params._outboxId`\n * so Workflows can dedupe on replays.\n */\n\nfunction resolveBearer(sub: Webhook): string | null {\n\tconst cfg = sub.auth_config as { token?: string; tokenEnc?: string };\n\tif (cfg.tokenEnc) {\n\t\t// Let decrypt errors propagate — see trigger.ts comment.\n\t\treturn decryptSecret(Buffer.from(cfg.tokenEnc, \"base64\"));\n\t}\n\treturn cfg.token ?? null;\n}\n\nexport function buildCloudflare(\n\toutboxRow: WebhookOutbox,\n\tsub: Webhook,\n): { body: string; headers: Record<string, string> } {\n\tconst body = JSON.stringify({\n\t\tparams: {\n\t\t\t...(outboxRow.payload as Record<string, unknown>),\n\t\t\t_type: outboxRow.event_type,\n\t\t\t_outboxId: outboxRow.id,\n\t\t},\n\t});\n\tconst headers: Record<string, string> = {\n\t\t\"content-type\": \"application/json\",\n\t};\n\tconst token = resolveBearer(sub);\n\tif (token) headers.authorization = `Bearer ${token}`;\n\treturn { body, headers };\n}\n",
9
+ "import type { WebhookOutbox } from \"@secondlayer/shared/db\";\n\n/**\n * Inngest event format — https://www.inngest.com/docs/events/sending-events\n *\n * POST body is a JSON array of one or more events. The Inngest event key\n * goes in the URL path (`https://inn.gs/e/{EVENT_KEY}`), so the user sets\n * that on `webhook.url` directly — no auth header needed.\n *\n * Body per event:\n * - `name` — event name `<subgraph>.<table>.<created|updated|deleted>`\n * so it matches Inngest fn `event` triggers out of the box\n * - `data` — row payload\n * - `id` — dedupes on Inngest's side (mirrors outbox id)\n * - `ts` — unix millis when the event occurred\n * - `v` — version tag, fixed at `\"2026-04-23.v1\"` for forward compat\n */\n\nexport const INNGEST_VERSION = \"2026-04-23.v1\";\n\nexport function buildInngest(outboxRow: WebhookOutbox): {\n\tbody: string;\n\theaders: Record<string, string>;\n} {\n\tconst event = {\n\t\tname: outboxRow.event_type,\n\t\tdata: outboxRow.payload,\n\t\tid: outboxRow.id,\n\t\tts: new Date(outboxRow.created_at).getTime(),\n\t\tv: INNGEST_VERSION,\n\t};\n\treturn {\n\t\tbody: JSON.stringify([event]),\n\t\theaders: {\n\t\t\t\"content-type\": \"application/json\",\n\t\t},\n\t};\n}\n",
10
+ "import type { Webhook, WebhookOutbox } from \"@secondlayer/shared/db\";\n\n/**\n * Raw JSON — the row payload, nothing else. User controls Content-Type +\n * arbitrary headers via `sub.auth_config.headers`. Useful for targets that\n * already have their own wire contract (e.g. Zapier hooks, internal\n * services, Slack webhooks with raw-shaped bodies).\n *\n * Special-case: if `auth_config.authType === \"basic\"` and `basicAuth` is\n * set (`user:pass` base64 or plain user/pass object), emit a Basic header.\n * If `authType === \"bearer\"` with `token`/`tokenEnc`, emit a Bearer header.\n * Otherwise no auth is set — user URLs often include a token in the query\n * string (Slack) or an HMAC at the receiver (Zapier).\n */\n\nexport function buildRaw(\n\toutboxRow: WebhookOutbox,\n\tsub: Webhook,\n): { body: string; headers: Record<string, string> } {\n\tconst cfg = sub.auth_config as {\n\t\tcontentType?: string;\n\t\theaders?: Record<string, string>;\n\t\tauthType?: \"bearer\" | \"basic\" | \"none\";\n\t\ttoken?: string;\n\t\tbasicAuth?: string;\n\t};\n\tconst headers: Record<string, string> = {\n\t\t\"content-type\": cfg.contentType ?? \"application/json\",\n\t\t...(cfg.headers ?? {}),\n\t};\n\tif (cfg.authType === \"bearer\" && cfg.token) {\n\t\theaders.authorization = `Bearer ${cfg.token}`;\n\t} else if (cfg.authType === \"basic\" && cfg.basicAuth) {\n\t\theaders.authorization = `Basic ${cfg.basicAuth}`;\n\t}\n\treturn {\n\t\tbody: JSON.stringify(outboxRow.payload),\n\t\theaders,\n\t};\n}\n",
11
+ "import { sign } from \"@secondlayer/shared/crypto/standard-webhooks\";\nimport type { WebhookOutbox } from \"@secondlayer/shared/db\";\n\n/**\n * Build a Standard Webhooks POST body + headers for an outbox row.\n * https://standardwebhooks.com\n *\n * Body shape: `{ type, timestamp, data }`\n * - `type` — `<subgraph>.<table>.<verb>` where verb is `created`, `updated`,\n * or `deleted`, mapped from the flush op so a receiver sees the full row\n * lifecycle, not just the initial insert.\n * - `timestamp` — ISO 8601 at DISPATCH time (not row creation). Receivers\n * verify `webhook-timestamp` against a tolerance window (default 300s per\n * Svix); stamping at creation would fail verification on every retry\n * beyond the first, since retries fire up to 72h after the row was\n * written. The `webhook-id` header stays stable across retries for\n * receiver dedup.\n * - `data` — row payload (bigints already stringified by flush manifest).\n *\n * Headers: the three standardwebhooks.com headers + Content-Type.\n */\n\nexport interface StandardWebhooksPayload {\n\ttype: string;\n\ttimestamp: string;\n\tdata: Record<string, unknown>;\n}\n\nexport function buildStandardWebhooks(\n\toutboxRow: WebhookOutbox,\n\tsigningSecret: string,\n): { body: string; headers: Record<string, string> } {\n\tconst nowSeconds = Math.floor(Date.now() / 1000);\n\tconst payload: StandardWebhooksPayload = {\n\t\ttype: outboxRow.event_type,\n\t\ttimestamp: new Date(nowSeconds * 1000).toISOString(),\n\t\tdata: outboxRow.payload as Record<string, unknown>,\n\t};\n\tconst body = JSON.stringify(payload);\n\tconst sigHeaders = sign(body, signingSecret, {\n\t\tid: outboxRow.id,\n\t\ttimestampSeconds: nowSeconds,\n\t});\n\treturn {\n\t\tbody,\n\t\theaders: {\n\t\t\t\"content-type\": \"application/json\",\n\t\t\t...sigHeaders,\n\t\t},\n\t};\n}\n",
12
+ "import { decryptSecret } from \"@secondlayer/shared/crypto/secrets\";\nimport type { Webhook, WebhookOutbox } from \"@secondlayer/shared/db\";\n\n/**\n * Trigger.dev v3 task trigger — https://trigger.dev/docs/tasks/overview\n *\n * POST https://api.trigger.dev/api/v1/tasks/{taskId}/trigger\n * Authorization: Bearer <TRIGGER_SECRET_KEY>\n *\n * Body:\n * - `payload` — the row payload\n * - `options` — currently `{ idempotencyKey }` so Trigger dedupes replays\n *\n * The Trigger secret lives in `sub.auth_config.tokenEnc` (encrypted by\n * `crypto/secrets`) or `sub.auth_config.token` (plaintext, dev only).\n */\n\nfunction resolveBearer(sub: Webhook): string | null {\n\tconst cfg = sub.auth_config as {\n\t\tauthType?: string;\n\t\ttoken?: string;\n\t\ttokenEnc?: string;\n\t};\n\tif (cfg.tokenEnc) {\n\t\t// Let decrypt errors propagate — silently sending unauthenticated\n\t\t// requests produces downstream 401s with no hint the secret failed\n\t\t// to decrypt. Surface as a dispatch-time error so the delivery log\n\t\t// records the real cause.\n\t\treturn decryptSecret(Buffer.from(cfg.tokenEnc, \"base64\"));\n\t}\n\treturn cfg.token ?? null;\n}\n\nexport function buildTrigger(\n\toutboxRow: WebhookOutbox,\n\tsub: Webhook,\n): { body: string; headers: Record<string, string> } {\n\tconst body = JSON.stringify({\n\t\tpayload: outboxRow.payload,\n\t\toptions: {\n\t\t\tidempotencyKey: outboxRow.id,\n\t\t},\n\t});\n\tconst headers: Record<string, string> = {\n\t\t\"content-type\": \"application/json\",\n\t};\n\tconst token = resolveBearer(sub);\n\tif (token) headers.authorization = `Bearer ${token}`;\n\treturn { body, headers };\n}\n",
13
+ "export type HostedMeterHooks = {\n\tonBlocksProcessed?: (\n\t\taccountId: string,\n\t\tblocks: number,\n\t\tsubgraphName: string,\n\t) => Promise<boolean>;\n\tonDeliveryAttempt?: (\n\t\taccountId: string,\n\t\twebhookId: string,\n\t) => Promise<boolean>;\n};\n\nlet hooks: HostedMeterHooks = {};\n\nexport function setHostedMeterHooks(next: HostedMeterHooks): void {\n\thooks = next;\n}\n\nexport function resetHostedMeterHooks(): void {\n\thooks = {};\n}\n\nexport async function meterBlocksProcessed(\n\taccountId: string | null | undefined,\n\tblocks: number,\n\tsubgraphName: string,\n): Promise<boolean> {\n\tif (!accountId || blocks <= 0) return true;\n\tif (!hooks.onBlocksProcessed) return true;\n\treturn hooks.onBlocksProcessed(accountId, blocks, subgraphName);\n}\n\nexport async function meterDeliveryAttempt(\n\taccountId: string | null | undefined,\n\twebhookId: string,\n): Promise<boolean> {\n\tif (!accountId) return true;\n\tif (!hooks.onDeliveryAttempt) return true;\n\treturn hooks.onDeliveryAttempt(accountId, webhookId);\n}\n",
14
+ "import type { Database, Webhook } from \"@secondlayer/shared/db\";\nimport { listWebhooks } from \"@secondlayer/shared/db/queries/webhooks\";\nimport type { Kysely } from \"kysely\";\nimport { sql } from \"kysely\";\nimport { WebhookMatcher } from \"./emitter-matcher.ts\";\n\n/**\n * Singleton matcher populated at processor startup and hot-reloaded via\n * `pg_notify('webhooks:changed')`. The block-processor reads from it\n * to decide which outbox rows to emit for each flushed write.\n *\n * Per-account listing: in oss/dedicated mode the tenant DB holds all subs\n * for the single account; the matcher loads every row. In platform mode\n * the emitter doesn't run at all (control plane only), so this module is\n * dedicated/oss-only.\n */\n\nexport const matcher = new WebhookMatcher();\n\nexport async function refreshMatcher(db: Kysely<Database>): Promise<number> {\n\t// listWebhooks is account-scoped; the emitter wants every active\n\t// sub so we do a raw query.\n\tconst rows = await sql<Webhook>`\n\t\tSELECT * FROM webhooks WHERE status = 'active'\n\t`.execute(db);\n\tmatcher.setAll(rows.rows);\n\treturn matcher.size();\n}\n\n// Per-account helper used by tests so the DATABASE_URL-based code path is\n// exercised through listWebhooks (keeps the query helper in the\n// integration surface).\nexport async function refreshMatcherForAccount(\n\tdb: Kysely<Database>,\n\taccountId: string,\n): Promise<number> {\n\tconst rows = await listWebhooks(db, accountId);\n\tmatcher.setAll(rows.filter((r: Webhook) => r.status === \"active\"));\n\treturn matcher.size();\n}\n",
15
+ "import type { Webhook } from \"@secondlayer/shared/db\";\n\n/**\n * Webhook matcher — holds the active-webhooks cache keyed by\n * `(subgraph_name, table_name)` and evaluates each sub's scalar-only\n * JSONB filter against a row.\n *\n * Filter DSL (v1): scalar operators over string/number/boolean columns.\n * { column: value } → column === value\n * { column: { eq: value } } → column === value\n * { column: { neq: value } } → column !== value\n * { column: { gt|gte|lt|lte: N } } → numeric compare\n * { column: { in: [a, b, c] } } → column ∈ set\n * { } → match all\n *\n * Nested objects, arrays, and non-scalar values are rejected with `false`\n * (never match) — subscribers get logged warnings at sub create time.\n *\n * Multiple conditions AND together. OR is deliberately absent in v1.\n */\n\nexport type FilterPrimitive = string | number | boolean;\nexport type FilterOperator =\n\t| { eq: FilterPrimitive }\n\t| { neq: FilterPrimitive }\n\t| { gt: number | string }\n\t| { gte: number | string }\n\t| { lt: number | string }\n\t| { lte: number | string }\n\t| { in: FilterPrimitive[] };\n\nexport type FilterClause = FilterPrimitive | FilterOperator;\nexport type WebhookFilter = Record<string, FilterClause>;\n\nfunction isPrimitive(v: unknown): v is FilterPrimitive {\n\tconst t = typeof v;\n\treturn t === \"string\" || t === \"number\" || t === \"boolean\";\n}\n\n/**\n * Coerce a value to BigInt for arbitrary-precision compare — avoids the\n * silent precision loss of `Number()` on integers past 2^53. Returns\n * `null` for non-integer numerics, which is fine because the filter DSL\n * is documented as scalar-only + integer-amount-aware (Stacks uses\n * uint128 bigint columns everywhere).\n */\nfunction coerceBigInt(v: unknown): bigint | null {\n\tif (typeof v === \"bigint\") return v;\n\tif (typeof v === \"number\") {\n\t\tif (!Number.isFinite(v)) return null;\n\t\tif (!Number.isInteger(v)) return null;\n\t\treturn BigInt(v);\n\t}\n\tif (typeof v === \"string\" && /^-?\\d+$/.test(v)) {\n\t\ttry {\n\t\t\treturn BigInt(v);\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t}\n\treturn null;\n}\n\n/**\n * Float compare fallback — used when a value isn't a clean integer\n * (rare in subgraph columns but defensible for user-supplied filters).\n */\nfunction coerceFloat(v: unknown): number | null {\n\tif (typeof v === \"number\" && Number.isFinite(v)) return v;\n\tif (typeof v === \"bigint\") return Number(v);\n\tif (typeof v === \"string\" && v !== \"\" && !Number.isNaN(Number(v))) {\n\t\treturn Number(v);\n\t}\n\treturn null;\n}\n\nfunction compareNumeric(a: unknown, b: unknown): 0 | 1 | -1 | null {\n\tconst ba = coerceBigInt(a);\n\tconst bb = coerceBigInt(b);\n\tif (ba !== null && bb !== null) {\n\t\tif (ba === bb) return 0;\n\t\treturn ba > bb ? 1 : -1;\n\t}\n\tconst fa = coerceFloat(a);\n\tconst fb = coerceFloat(b);\n\tif (fa === null || fb === null) return null;\n\tif (fa === fb) return 0;\n\treturn fa > fb ? 1 : -1;\n}\n\nfunction matchClause(rowValue: unknown, clause: FilterClause): boolean {\n\t// Reject non-primitive row values — `{ value: {...} }` should not\n\t// match `filter: { value: \"[object Object]\" }` via loose stringify.\n\t// Bigints are not primitives in the JSON sense but are supported\n\t// scalar inputs from subgraph rows.\n\tconst rowIsPrimitive = isPrimitive(rowValue) || typeof rowValue === \"bigint\";\n\n\tif (isPrimitive(clause)) {\n\t\tif (!rowIsPrimitive) return false;\n\t\tif (\n\t\t\ttypeof clause === \"number\" ||\n\t\t\ttypeof rowValue === \"number\" ||\n\t\t\ttypeof rowValue === \"bigint\"\n\t\t) {\n\t\t\tconst cmp = compareNumeric(rowValue, clause);\n\t\t\tif (cmp !== null) return cmp === 0;\n\t\t}\n\t\treturn rowValue === clause || String(rowValue) === String(clause);\n\t}\n\n\tif (clause === null || typeof clause !== \"object\" || Array.isArray(clause)) {\n\t\treturn false;\n\t}\n\n\tconst keys = Object.keys(clause);\n\tif (keys.length !== 1) return false;\n\tconst op = keys[0];\n\n\tconst c = clause as Record<string, unknown>;\n\tswitch (op) {\n\t\tcase \"eq\":\n\t\t\treturn matchClause(rowValue, c.eq as FilterPrimitive);\n\t\tcase \"neq\":\n\t\t\treturn !matchClause(rowValue, c.neq as FilterPrimitive);\n\t\tcase \"gt\":\n\t\tcase \"gte\":\n\t\tcase \"lt\":\n\t\tcase \"lte\": {\n\t\t\tif (!rowIsPrimitive) return false;\n\t\t\tconst cmp = compareNumeric(rowValue, c[op]);\n\t\t\tif (cmp === null) return false;\n\t\t\tif (op === \"gt\") return cmp > 0;\n\t\t\tif (op === \"gte\") return cmp >= 0;\n\t\t\tif (op === \"lt\") return cmp < 0;\n\t\t\treturn cmp <= 0;\n\t\t}\n\t\tcase \"in\": {\n\t\t\tconst list = c.in;\n\t\t\tif (!Array.isArray(list)) return false;\n\t\t\tif (!rowIsPrimitive) return false;\n\t\t\treturn list.some(\n\t\t\t\t(item) => isPrimitive(item) && matchClause(rowValue, item),\n\t\t\t);\n\t\t}\n\t\tdefault:\n\t\t\treturn false;\n\t}\n}\n\nexport function matchesFilter(\n\tfilter: WebhookFilter | null | undefined,\n\trow: Record<string, unknown>,\n): boolean {\n\tif (!filter || Object.keys(filter).length === 0) return true;\n\tfor (const [col, clause] of Object.entries(filter)) {\n\t\tif (!matchClause(row[col], clause)) return false;\n\t}\n\treturn true;\n}\n\n// ── Cache keyed by (subgraph_name, table_name) ──────────────────────────\n\ntype MatcherKey = string;\n\nfunction key(subgraphName: string, tableName: string): MatcherKey {\n\treturn `${subgraphName}\u0000${tableName}`;\n}\n\nexport class WebhookMatcher {\n\tprivate readonly byKey = new Map<MatcherKey, Webhook[]>();\n\tprivate readonly byId = new Map<string, Webhook>();\n\n\t/** Replace the entire cache with a fresh snapshot. */\n\tsetAll(subs: Webhook[]): void {\n\t\tthis.byKey.clear();\n\t\tthis.byId.clear();\n\t\tfor (const sub of subs) {\n\t\t\tif (sub.status !== \"active\") continue;\n\t\t\t// This matcher keys on (subgraph, table) row writes; chain\n\t\t\t// webhooks react to raw chain events and never flow through it.\n\t\t\tif (sub.kind !== \"subgraph\" || !sub.subgraph_name || !sub.table_name)\n\t\t\t\tcontinue;\n\t\t\tthis.byId.set(sub.id, sub);\n\t\t\tconst k = key(sub.subgraph_name, sub.table_name);\n\t\t\tconst arr = this.byKey.get(k);\n\t\t\tif (arr) arr.push(sub);\n\t\t\telse this.byKey.set(k, [sub]);\n\t\t}\n\t}\n\n\t/** Returns active subs for a (subgraph, table) whose filter matches the row. */\n\tmatch(\n\t\tsubgraphName: string,\n\t\ttableName: string,\n\t\trow: Record<string, unknown>,\n\t): Webhook[] {\n\t\tconst bucket = this.byKey.get(key(subgraphName, tableName));\n\t\tif (!bucket) return [];\n\t\tconst hits: Webhook[] = [];\n\t\tfor (const sub of bucket) {\n\t\t\tif (matchesFilter(sub.filter as WebhookFilter, row)) hits.push(sub);\n\t\t}\n\t\treturn hits;\n\t}\n\n\thas(subgraphName: string, tableName: string): boolean {\n\t\treturn this.byKey.has(key(subgraphName, tableName));\n\t}\n\n\tsize(): number {\n\t\treturn this.byId.size;\n\t}\n\n\tget(id: string): Webhook | undefined {\n\t\treturn this.byId.get(id);\n\t}\n}\n"
15
16
  ],
16
- "mappings": "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",
17
- "debugId": "C6066230FC5B9BFF64756E2164756E21",
17
+ "mappings": "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",
18
+ "debugId": "51FFE04872B2ADAD64756E2164756E21",
18
19
  "names": []
19
20
  }
@@ -2701,7 +2701,7 @@ function stableStringify(obj) {
2701
2701
  return acc;
2702
2702
  }, {}));
2703
2703
  }
2704
- async function emitSubscriptionOutbox(tx, subgraphName, manifest, matcher, blockHeight) {
2704
+ async function emitWebhookOutbox(tx, subgraphName, manifest, matcher, blockHeight) {
2705
2705
  if (!isEmitOutboxEnabled()) {
2706
2706
  if (!loggedKillSwitch) {
2707
2707
  logger6.warn("SECONDLAYER_EMIT_OUTBOX=false — outbox emission bypassed");
@@ -2720,7 +2720,7 @@ async function emitSubscriptionOutbox(tx, subgraphName, manifest, matcher, block
2720
2720
  const eventType = `${subgraphName}.${write.table}.${OP_VERB[write.op]}`;
2721
2721
  for (const s of subs) {
2722
2722
  rows.push({
2723
- subscription_id: s.id,
2723
+ webhook_id: s.id,
2724
2724
  subgraph_name: subgraphName,
2725
2725
  table_name: write.table,
2726
2726
  block_height: blockHeight,
@@ -2734,7 +2734,7 @@ async function emitSubscriptionOutbox(tx, subgraphName, manifest, matcher, block
2734
2734
  }
2735
2735
  if (rows.length === 0)
2736
2736
  return 0;
2737
- await tx.insertInto("subscription_outbox").values(rows).onConflict((oc) => oc.columns(["subscription_id", "dedup_key"]).doNothing()).execute();
2737
+ await tx.insertInto("webhook_outbox").values(rows).onConflict((oc) => oc.columns(["webhook_id", "dedup_key"]).doNothing()).execute();
2738
2738
  return rows.length;
2739
2739
  }
2740
2740
 
@@ -3047,8 +3047,8 @@ async function answerRead(hostCtx, msg) {
3047
3047
  }
3048
3048
  }
3049
3049
 
3050
- // src/runtime/subscription-state.ts
3051
- import { listSubscriptions } from "@secondlayer/shared/db/queries/subscriptions";
3050
+ // src/runtime/webhook-state.ts
3051
+ import { listWebhooks } from "@secondlayer/shared/db/queries/webhooks";
3052
3052
  import { sql as sql3 } from "kysely";
3053
3053
 
3054
3054
  // src/runtime/emitter-matcher.ts
@@ -3168,7 +3168,7 @@ function key(subgraphName, tableName) {
3168
3168
  return `${subgraphName}\x00${tableName}`;
3169
3169
  }
3170
3170
 
3171
- class SubscriptionMatcher {
3171
+ class WebhookMatcher {
3172
3172
  byKey = new Map;
3173
3173
  byId = new Map;
3174
3174
  setAll(subs) {
@@ -3210,11 +3210,11 @@ class SubscriptionMatcher {
3210
3210
  }
3211
3211
  }
3212
3212
 
3213
- // src/runtime/subscription-state.ts
3214
- var matcher = new SubscriptionMatcher;
3213
+ // src/runtime/webhook-state.ts
3214
+ var matcher = new WebhookMatcher;
3215
3215
  async function refreshMatcher(db) {
3216
3216
  const rows = await sql3`
3217
- SELECT * FROM subscriptions WHERE status = 'active'
3217
+ SELECT * FROM webhooks WHERE status = 'active'
3218
3218
  `.execute(db);
3219
3219
  matcher.setAll(rows.rows);
3220
3220
  return matcher.size();
@@ -3504,7 +3504,7 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
3504
3504
  const manifest = await ctx.flush();
3505
3505
  flushedWrites = manifest.count > 0;
3506
3506
  if (manifest.count > 0) {
3507
- await emitSubscriptionOutbox(tx, subgraphName, manifest, matcher, block.height);
3507
+ await emitWebhookOutbox(tx, subgraphName, manifest, matcher, block.height);
3508
3508
  }
3509
3509
  flushMs = performance.now() - flushStart;
3510
3510
  }
@@ -4638,8 +4638,8 @@ async function reorgOneSubgraph(sg, schema, blockHeight, loadSubgraphDef) {
4638
4638
  for (const [tableName, rows] of Object.entries(revertedByTable)) {
4639
4639
  if (rows.length === 0)
4640
4640
  continue;
4641
- await targetDb.insertInto("subscription_outbox").columns([
4642
- "subscription_id",
4641
+ await targetDb.insertInto("webhook_outbox").columns([
4642
+ "webhook_id",
4643
4643
  "subgraph_name",
4644
4644
  "table_name",
4645
4645
  "block_height",
@@ -4648,8 +4648,8 @@ async function reorgOneSubgraph(sg, schema, blockHeight, loadSubgraphDef) {
4648
4648
  "dedup_key",
4649
4649
  "row_pk",
4650
4650
  "status"
4651
- ]).expression((eb) => eb.selectFrom("subscriptions").select((eb2) => [
4652
- "id as subscription_id",
4651
+ ]).expression((eb) => eb.selectFrom("webhooks").select((eb2) => [
4652
+ "id as webhook_id",
4653
4653
  eb2.val(sg.name).as("subgraph_name"),
4654
4654
  eb2.val(tableName).as("table_name"),
4655
4655
  eb2.val(blockHeight).as("block_height"),
@@ -4667,8 +4667,8 @@ async function reorgOneSubgraph(sg, schema, blockHeight, loadSubgraphDef) {
4667
4667
  reorgRoot: blockHeight
4668
4668
  }).as("row_pk"),
4669
4669
  eb2.val("pending").as("status")
4670
- ]).where("subgraph_name", "=", sg.name).where("table_name", "=", tableName).where("status", "=", "active")).onConflict((oc) => oc.columns(["subscription_id", "dedup_key"]).doNothing()).execute().catch((err) => {
4671
- logger12.error("Failed to emit revert event for subscriptions", {
4670
+ ]).where("subgraph_name", "=", sg.name).where("table_name", "=", tableName).where("status", "=", "active")).onConflict((oc) => oc.columns(["webhook_id", "dedup_key"]).doNothing()).execute().catch((err) => {
4671
+ logger12.error("Failed to emit revert event for webhooks", {
4672
4672
  event: "reorg_revert_emit_dropped",
4673
4673
  subgraph: sg.name,
4674
4674
  table: tableName,
@@ -4720,6 +4720,26 @@ import {
4720
4720
  targetListenerUrl as targetListenerUrl2
4721
4721
  } from "@secondlayer/shared/queue/listener";
4722
4722
 
4723
+ // src/runtime/hosted-meter.ts
4724
+ var hooks = {};
4725
+ function setHostedMeterHooks(next) {
4726
+ hooks = next;
4727
+ }
4728
+ async function meterBlocksProcessed(accountId, blocks, subgraphName) {
4729
+ if (!accountId || blocks <= 0)
4730
+ return true;
4731
+ if (!hooks.onBlocksProcessed)
4732
+ return true;
4733
+ return hooks.onBlocksProcessed(accountId, blocks, subgraphName);
4734
+ }
4735
+ async function meterDeliveryAttempt(accountId, webhookId) {
4736
+ if (!accountId)
4737
+ return true;
4738
+ if (!hooks.onDeliveryAttempt)
4739
+ return true;
4740
+ return hooks.onDeliveryAttempt(accountId, webhookId);
4741
+ }
4742
+
4723
4743
  // src/runtime/streams-reorg-poll.ts
4724
4744
  import { getErrorMessage as getErrorMessage5 } from "@secondlayer/shared";
4725
4745
  import {
@@ -4795,7 +4815,8 @@ async function catchUpAll(subgraphs, db, concurrency) {
4795
4815
  break;
4796
4816
  try {
4797
4817
  const def = await loadSubgraphDefinition(sg);
4798
- await catchUpSubgraph(def, sg.name);
4818
+ const processed = await catchUpSubgraph(def, sg.name);
4819
+ await meterBlocksProcessed(sg.account_id, processed, sg.name);
4799
4820
  } catch (err) {
4800
4821
  const msg = getErrorMessage6(err);
4801
4822
  if (isHandlerNotFoundError(err)) {
@@ -4893,38 +4914,45 @@ async function runSubgraphOperation(operation, signal) {
4893
4914
  const subgraph = await db.selectFrom("subgraphs").selectAll().where("id", "=", operation.subgraph_id).executeTakeFirst();
4894
4915
  if (!subgraph)
4895
4916
  throw new Error(`Subgraph not found: ${operation.subgraph_id}`);
4917
+ if (subgraph.status === "paused")
4918
+ return 0;
4896
4919
  const def = await loadSubgraphDefinition(subgraph);
4897
4920
  const schemaName = subgraph.schema_name ?? pgSchemaName2(subgraph.name);
4921
+ let processed = 0;
4898
4922
  if (operation.kind === "backfill") {
4899
4923
  if (operation.from_block == null || operation.to_block == null) {
4900
4924
  throw new Error("Backfill operation is missing from_block or to_block");
4901
4925
  }
4902
4926
  const resumeFrom = operation.cursor_block != null ? Math.max(Number(operation.from_block), Number(operation.cursor_block) + 1) : Number(operation.from_block);
4903
- const result2 = await backfillSubgraph(def, {
4927
+ const result = await backfillSubgraph(def, {
4904
4928
  fromBlock: resumeFrom,
4905
4929
  toBlock: Number(operation.to_block),
4906
4930
  schemaName,
4907
4931
  operationId: operation.id,
4908
4932
  signal
4909
4933
  });
4910
- return result2.processed;
4911
- }
4912
- const hasResumeMetadata = subgraph.status === "reindexing" && subgraph.reindex_from_block != null && subgraph.reindex_to_block != null;
4913
- if (hasResumeMetadata) {
4914
- const result2 = await resumeReindex(def, {
4915
- schemaName,
4916
- operationId: operation.id,
4917
- signal
4918
- });
4919
- return result2.processed;
4934
+ processed = result.processed;
4935
+ } else {
4936
+ const hasResumeMetadata = subgraph.status === "reindexing" && subgraph.reindex_from_block != null && subgraph.reindex_to_block != null;
4937
+ if (hasResumeMetadata) {
4938
+ const result = await resumeReindex(def, {
4939
+ schemaName,
4940
+ operationId: operation.id,
4941
+ signal
4942
+ });
4943
+ processed = result.processed;
4944
+ } else {
4945
+ const result = await reindexSubgraph(def, {
4946
+ startBlockFloor: operation.from_block == null ? undefined : Number(operation.from_block),
4947
+ schemaName,
4948
+ operationId: operation.id,
4949
+ signal
4950
+ });
4951
+ processed = result.processed;
4952
+ }
4920
4953
  }
4921
- const result = await reindexSubgraph(def, {
4922
- startBlockFloor: operation.from_block == null ? undefined : Number(operation.from_block),
4923
- schemaName,
4924
- operationId: operation.id,
4925
- signal
4926
- });
4927
- return result.processed;
4954
+ await meterBlocksProcessed(subgraph.account_id, processed, subgraph.name);
4955
+ return processed;
4928
4956
  }
4929
4957
  async function startSubgraphOperationRunner(opts) {
4930
4958
  const concurrency = opts?.concurrency ?? DEFAULT_OPERATION_CONCURRENCY;
@@ -5131,5 +5159,5 @@ export {
5131
5159
  startSubgraphOperationRunner
5132
5160
  };
5133
5161
 
5134
- //# debugId=9AD13E77BAE4758164756E2164756E21
5162
+ //# debugId=78658DEE1B56E04464756E2164756E21
5135
5163
  //# sourceMappingURL=processor.js.map