@secondlayer/shared 11.4.2 → 11.5.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.
- package/README.md +4 -4
- package/dist/src/archive/derived-stage-report.d.ts +2 -2
- package/dist/src/archive/derived-stage-report.js.map +1 -1
- package/dist/src/archive/instance-diagnosis.js +2 -2
- package/dist/src/archive/instance-diagnosis.js.map +2 -2
- package/dist/src/archive/range-digest.d.ts +25 -25
- package/dist/src/archive/semantic-digest-builder.d.ts +25 -25
- package/dist/src/archive/status.d.ts +32 -7
- package/dist/src/archive/status.js +20 -3
- package/dist/src/archive/status.js.map +3 -3
- package/dist/src/coverage/index.d.ts +25 -25
- package/dist/src/coverage/index.js.map +1 -1
- package/dist/src/crypto/secondlayer-webhook.d.ts +2 -2
- package/dist/src/crypto/secondlayer-webhook.js.map +1 -1
- package/dist/src/crypto/secrets.js.map +1 -1
- package/dist/src/db/index.d.ts +37 -37
- package/dist/src/db/index.js +4 -4
- package/dist/src/db/index.js.map +3 -3
- package/dist/src/db/queries/chain-reorgs.d.ts +25 -25
- package/dist/src/db/queries/chain-reorgs.js +4 -4
- package/dist/src/db/queries/chain-reorgs.js.map +3 -3
- package/dist/src/db/queries/contracts.d.ts +25 -25
- package/dist/src/db/queries/instance.d.ts +25 -25
- package/dist/src/db/queries/integrity.d.ts +25 -25
- package/dist/src/db/queries/subgraph-gaps.d.ts +25 -25
- package/dist/src/db/queries/subgraph-operations.d.ts +25 -25
- package/dist/src/db/queries/subgraphs.d.ts +25 -25
- package/dist/src/db/queries/subgraphs.js +4 -4
- package/dist/src/db/queries/subgraphs.js.map +3 -3
- package/dist/src/db/queries/sync-scope.d.ts +25 -25
- package/dist/src/db/queries/{subscriptions.d.ts → webhooks.d.ts} +58 -58
- package/dist/src/db/queries/{subscriptions.js → webhooks.js} +38 -38
- package/dist/src/db/queries/webhooks.js.map +13 -0
- package/dist/src/db/schema.d.ts +34 -34
- package/dist/src/index.d.ts +109 -107
- package/dist/src/index.js +51 -54
- package/dist/src/index.js.map +6 -6
- package/dist/src/leader.d.ts +4 -4
- package/dist/src/leader.js +3 -3
- package/dist/src/leader.js.map +3 -3
- package/dist/src/node/local-client.d.ts +25 -25
- package/dist/src/queue/listener.d.ts +2 -2
- package/dist/src/queue/listener.js.map +1 -1
- package/dist/src/runtime/index.js +2 -6
- package/dist/src/runtime/index.js.map +4 -4
- package/dist/src/schemas/index.d.ts +61 -61
- package/dist/src/schemas/index.js +48 -51
- package/dist/src/schemas/index.js.map +5 -5
- package/dist/src/schemas/subgraphs.d.ts +2 -2
- package/dist/src/schemas/subgraphs.js.map +1 -1
- package/dist/src/schemas/{subscriptions.d.ts → webhooks.d.ts} +63 -63
- package/dist/src/schemas/{subscriptions.js → webhooks.js} +49 -52
- package/dist/src/schemas/webhooks.js.map +10 -0
- package/migrations/0126_rename_subscriptions_to_webhooks.ts +96 -0
- package/migrations/0127_rename_webhook_catalog_names.ts +55 -0
- package/package.json +8 -8
- package/dist/src/db/queries/subscriptions.js.map +0 -13
- package/dist/src/schemas/subscriptions.js.map +0 -10
|
@@ -1106,9 +1106,9 @@ interface Database {
|
|
|
1106
1106
|
account_spend_caps: AccountSpendCapsTable;
|
|
1107
1107
|
account_credits: AccountCreditsTable;
|
|
1108
1108
|
hosted_meter_days: HostedMeterDaysTable;
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1109
|
+
webhooks: WebhooksTable;
|
|
1110
|
+
webhook_outbox: WebhookOutboxTable;
|
|
1111
|
+
webhook_deliveries: WebhookDeliveriesTable;
|
|
1112
1112
|
trigger_evaluator_state: TriggerEvaluatorStateTable;
|
|
1113
1113
|
decoded_events: DecodedEventsTable;
|
|
1114
1114
|
decoder_checkpoints: DecoderCheckpointsTable;
|
|
@@ -1228,31 +1228,31 @@ interface ArchiveFetchesTable {
|
|
|
1228
1228
|
via_allowance: Generated<boolean>;
|
|
1229
1229
|
charged_at: Generated<Date>;
|
|
1230
1230
|
}
|
|
1231
|
-
type
|
|
1232
|
-
/** Polymorphic
|
|
1231
|
+
type WebhookStatus = "active" | "paused" | "error";
|
|
1232
|
+
/** Polymorphic webhook mode: `subgraph` reacts to processed table rows;
|
|
1233
1233
|
* `chain` reacts to raw chain events matched directly off the Index/Streams
|
|
1234
1234
|
* clock (no subgraph). See migration 0088. */
|
|
1235
|
-
type
|
|
1236
|
-
type
|
|
1237
|
-
type
|
|
1238
|
-
interface
|
|
1235
|
+
type WebhookKind = "subgraph" | "chain";
|
|
1236
|
+
type WebhookFormat = "standard-webhooks" | "inngest" | "trigger" | "cloudflare" | "cloudevents" | "raw";
|
|
1237
|
+
type WebhookRuntime = "inngest" | "trigger" | "cloudflare" | "node";
|
|
1238
|
+
interface WebhooksTable {
|
|
1239
1239
|
id: Generated<string>;
|
|
1240
1240
|
account_id: string;
|
|
1241
1241
|
project_id: string | null;
|
|
1242
1242
|
name: string;
|
|
1243
|
-
status: ColumnType<
|
|
1244
|
-
kind: ColumnType<
|
|
1245
|
-
/** Null for chain
|
|
1243
|
+
status: ColumnType<WebhookStatus, WebhookStatus | undefined, WebhookStatus>;
|
|
1244
|
+
kind: ColumnType<WebhookKind, WebhookKind | undefined, WebhookKind>;
|
|
1245
|
+
/** Null for chain webhooks (CHECK webhooks_kind_shape). */
|
|
1246
1246
|
subgraph_name: string | null;
|
|
1247
|
-
/** Null for chain
|
|
1247
|
+
/** Null for chain webhooks (CHECK webhooks_kind_shape). */
|
|
1248
1248
|
table_name: string | null;
|
|
1249
1249
|
/** Chain-trigger filter array (the `SubgraphFilter` shape, JSON). Null for
|
|
1250
|
-
* subgraph
|
|
1251
|
-
* import cycle; the Zod schema in schemas/
|
|
1250
|
+
* subgraph webhooks. Typed loosely here to avoid a shared→subgraphs
|
|
1251
|
+
* import cycle; the Zod schema in schemas/webhooks.ts owns the shape. */
|
|
1252
1252
|
triggers: unknown | null;
|
|
1253
1253
|
filter: Generated<unknown>;
|
|
1254
|
-
format: ColumnType<
|
|
1255
|
-
runtime:
|
|
1254
|
+
format: ColumnType<WebhookFormat, WebhookFormat | undefined, WebhookFormat>;
|
|
1255
|
+
runtime: WebhookRuntime | null;
|
|
1256
1256
|
url: string;
|
|
1257
1257
|
signing_secret_enc: Buffer;
|
|
1258
1258
|
auth_config: Generated<unknown>;
|
|
@@ -1268,13 +1268,13 @@ interface SubscriptionsTable {
|
|
|
1268
1268
|
updated_at: Generated<Date>;
|
|
1269
1269
|
}
|
|
1270
1270
|
type OutboxStatus = "pending" | "delivered" | "dead";
|
|
1271
|
-
interface
|
|
1271
|
+
interface WebhookOutboxTable {
|
|
1272
1272
|
id: Generated<string>;
|
|
1273
|
-
|
|
1274
|
-
kind: ColumnType<
|
|
1275
|
-
/** Null for chain-
|
|
1273
|
+
webhook_id: string;
|
|
1274
|
+
kind: ColumnType<WebhookKind, WebhookKind | undefined, WebhookKind>;
|
|
1275
|
+
/** Null for chain-webhook rows. */
|
|
1276
1276
|
subgraph_name: string | null;
|
|
1277
|
-
/** Null for chain-
|
|
1277
|
+
/** Null for chain-webhook rows. */
|
|
1278
1278
|
table_name: string | null;
|
|
1279
1279
|
block_height: number | bigint;
|
|
1280
1280
|
tx_id: string | null;
|
|
@@ -1292,12 +1292,12 @@ interface SubscriptionOutboxTable {
|
|
|
1292
1292
|
locked_until: Date | null;
|
|
1293
1293
|
created_at: Generated<Date>;
|
|
1294
1294
|
}
|
|
1295
|
-
interface
|
|
1295
|
+
interface WebhookDeliveriesTable {
|
|
1296
1296
|
id: Generated<string>;
|
|
1297
1297
|
/** Nullable after migration 0077 — outbox row may be cleaned up while
|
|
1298
1298
|
* delivery telemetry is retained. */
|
|
1299
1299
|
outbox_id: string | null;
|
|
1300
|
-
|
|
1300
|
+
webhook_id: string;
|
|
1301
1301
|
attempt: number;
|
|
1302
1302
|
status_code: number | null;
|
|
1303
1303
|
response_headers: unknown | null;
|
|
@@ -1307,7 +1307,7 @@ interface SubscriptionDeliveriesTable {
|
|
|
1307
1307
|
dispatched_at: Generated<Date>;
|
|
1308
1308
|
}
|
|
1309
1309
|
/** Single-row (id always TRUE) high-water mark for the chain-trigger evaluator.
|
|
1310
|
-
* One loop serves all chain
|
|
1310
|
+
* One loop serves all chain webhooks, so the cursor is global. */
|
|
1311
1311
|
interface TriggerEvaluatorStateTable {
|
|
1312
1312
|
id: Generated<boolean>;
|
|
1313
1313
|
last_processed_block: ColumnType<bigint, bigint | number | undefined, bigint | number>;
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"/**\n * Atomic subgraph adapter — commit effects, rows, journal, cursor,\n * receipt, and failure in one transaction.\n */\n\nimport type { DecoderAdapterFailure } from \"./adapter.ts\";\nimport type { DecoderClockReceipt } from \"./decoder-clock.ts\";\nimport type { EffectMutation } from \"./effect-manifest.ts\";\n\nexport const SUBGRAPH_COMMIT_STEPS = [\n\t\"effects\",\n\t\"rows\",\n\t\"journal\",\n\t\"cursor\",\n\t\"receipt\",\n\t\"failure\",\n] as const;\nexport type SubgraphCommitStep = (typeof SUBGRAPH_COMMIT_STEPS)[number];\n\nexport class SubgraphAdapterCrash extends Error {\n\treadonly name = \"SubgraphAdapterCrash\";\n\tconstructor(readonly step: SubgraphCommitStep) {\n\t\tsuper(`subgraph adapter crash after ${step}`);\n\t}\n}\n\nexport type SubgraphCommitInput = {\n\tstage_id: string;\n\teffects: readonly EffectMutation[];\n\treceipts: readonly (DecoderClockReceipt & { effect_digest: string })[];\n\tfailure?: DecoderAdapterFailure | null;\n\twriteEffects: () => Promise<void>;\n\twriteRows: () => Promise<void>;\n\twriteJournal: () => Promise<void>;\n\twriteCursor: () => Promise<void>;\n\twriteReceipts: () => Promise<void>;\n\twriteFailure: () => Promise<void>;\n\tcrashAfter?: SubgraphCommitStep;\n};\n\nexport async function runSubgraphCommitSteps(\n\tsteps: Record<SubgraphCommitStep, () => Promise<void>>,\n\tcrashAfter?: SubgraphCommitStep,\n): Promise<void> {\n\tfor (const step of SUBGRAPH_COMMIT_STEPS) {\n\t\tawait steps[step]();\n\t\tif (crashAfter === step) throw new SubgraphAdapterCrash(step);\n\t}\n}\n\nexport async function commitSubgraphAdapter(\n\tinput: SubgraphCommitInput,\n\twrap: (fn: () => Promise<void>) => Promise<void> = (fn) => fn(),\n): Promise<void> {\n\tawait wrap(() =>\n\t\trunSubgraphCommitSteps(\n\t\t\t{\n\t\t\t\teffects: input.writeEffects,\n\t\t\t\trows: input.writeRows,\n\t\t\t\tjournal: input.writeJournal,\n\t\t\t\tcursor: input.writeCursor,\n\t\t\t\treceipt: input.writeReceipts,\n\t\t\t\tfailure: input.writeFailure,\n\t\t\t},\n\t\t\tinput.crashAfter,\n\t\t),\n\t);\n}\n",
|
|
14
14
|
"/**\n * Sparse proofs — acknowledge skipped canonical ranges without handler\n * execution. Sparse and sequential paths must agree on chain / effect /\n * final-row digests.\n */\n\nimport { createHash } from \"node:crypto\";\nimport type { CoverageRange } from \"./constraints.ts\";\nimport { type EffectMutation, hashEffectManifest } from \"./effect-manifest.ts\";\nimport { rangeDigestOf } from \"./sealer.ts\";\n\nexport type SparseSkip = CoverageRange & { reason: \"no-match\" | \"filtered\" };\n\nexport type HeightDigest = {\n\theight: number;\n\thash: string;\n\teffect_digest: string;\n\trow_digest: string;\n};\n\nexport function skipReceipt(skip: SparseSkip, chainHash: string): HeightDigest {\n\tconst empty = hashEffectManifest([]);\n\treturn {\n\t\theight: skip.from_height,\n\t\thash: chainHash,\n\t\teffect_digest: empty,\n\t\trow_digest: empty,\n\t};\n}\n\nexport function sequentialDigests(heights: readonly HeightDigest[]): {\n\tchain: string;\n\teffect: string;\n\trow: string;\n} {\n\tconst ordered = [...heights].sort((a, b) => a.height - b.height);\n\treturn {\n\t\tchain: rangeDigestOf(ordered.map((h) => `${h.height}:${h.hash}`)),\n\t\teffect: rangeDigestOf(ordered.map((h) => h.effect_digest)),\n\t\trow: rangeDigestOf(ordered.map((h) => h.row_digest)),\n\t};\n}\n\nexport function sparseDigests(\n\texecuted: readonly HeightDigest[],\n\tskips: readonly { skip: SparseSkip; hash: string }[],\n): { chain: string; effect: string; row: string } {\n\tconst skipped = skips.flatMap(({ skip, hash }) => {\n\t\tconst out: HeightDigest[] = [];\n\t\tfor (let h = skip.from_height; h <= skip.to_height; h++) {\n\t\t\tout.push(skipReceipt({ ...skip, from_height: h, to_height: h }, hash));\n\t\t}\n\t\treturn out;\n\t});\n\treturn sequentialDigests([...executed, ...skipped]);\n}\n\nexport function proofsAgree(\n\tsequential: { chain: string; effect: string; row: string },\n\tsparse: { chain: string; effect: string; row: string },\n): boolean {\n\treturn (\n\t\tsequential.chain === sparse.chain &&\n\t\tsequential.effect === sparse.effect &&\n\t\tsequential.row === sparse.row\n\t);\n}\n\nexport function emptyRowDigest(): string {\n\treturn createHash(\"sha256\").update(\"empty-row\\n\").digest(\"hex\");\n}\n\nexport type { EffectMutation };\n",
|
|
15
15
|
"/**\n * Stateful deep verify — replay into a scratch schema and compare the\n * canonical final-row digest. A seeded historical mutation must be found.\n */\n\nimport { createHash } from \"node:crypto\";\nimport { type EffectMutation, hashEffectManifest } from \"./effect-manifest.ts\";\n\nexport type RowSnapshot = {\n\ttable: string;\n\tkey: string;\n\tvalue: string;\n};\n\nexport function finalRowDigest(rows: readonly RowSnapshot[]): string {\n\tconst ordered = [...rows].sort(\n\t\t(a, b) => a.table.localeCompare(b.table) || a.key.localeCompare(b.key),\n\t);\n\tconst hash = createHash(\"sha256\");\n\tfor (const row of ordered) {\n\t\thash.update(`${row.table}\\t${row.key}\\t${row.value}\\n`);\n\t}\n\treturn hash.digest(\"hex\");\n}\n\nexport function applyMutations(\n\trows: readonly RowSnapshot[],\n\tops: readonly EffectMutation[],\n): RowSnapshot[] {\n\tconst map = new Map(rows.map((r) => [`${r.table}:${r.key}`, { ...r }]));\n\tfor (const op of ops) {\n\t\tconst id = String(op.key.id ?? JSON.stringify(op.key));\n\t\tconst k = `${op.table}:${id}`;\n\t\tif (op.op === \"delete\") {\n\t\t\tmap.delete(k);\n\t\t\tcontinue;\n\t\t}\n\t\tmap.set(k, {\n\t\t\ttable: op.table,\n\t\t\tkey: id,\n\t\t\tvalue: JSON.stringify(op.value ?? {}),\n\t\t});\n\t}\n\treturn [...map.values()];\n}\n\nexport type DeepVerifyResult = {\n\tok: boolean;\n\tlive_digest: string;\n\tscratch_digest: string;\n\tfound_mutation: boolean;\n};\n\nexport function deepVerify(opts: {\n\tlive: readonly RowSnapshot[];\n\treplay: readonly EffectMutation[];\n\tseed?: EffectMutation;\n}): DeepVerifyResult {\n\tconst scratch = applyMutations([], opts.replay);\n\tconst liveDigest = finalRowDigest(opts.live);\n\tconst scratchDigest = finalRowDigest(scratch);\n\tlet found = false;\n\tif (opts.seed) {\n\t\tconst tainted = applyMutations(opts.live, [opts.seed]);\n\t\tfound = finalRowDigest(tainted) !== liveDigest;\n\t}\n\treturn {\n\t\tok: liveDigest === scratchDigest,\n\t\tlive_digest: liveDigest,\n\t\tscratch_digest: scratchDigest,\n\t\tfound_mutation: found,\n\t};\n}\n\nexport function emptyManifestHash(): string {\n\treturn hashEffectManifest([]);\n}\n",
|
|
16
|
-
"/**\n * Queue coverage —
|
|
16
|
+
"/**\n * Queue coverage — webhook decision, outbox, delivery, rollback,\n * DLQ, and replay with cursor fences.\n */\n\nimport type { QueueCounters } from \"./evaluate.ts\";\n\nexport const QUEUE_STAGES = [\n\t\"decision\",\n\t\"outbox\",\n\t\"delivery\",\n\t\"rollback\",\n\t\"dlq\",\n\t\"replay\",\n] as const;\nexport type QueueStage = (typeof QUEUE_STAGES)[number];\n\nexport type QueueEvent =\n\t| { type: \"accept\" }\n\t| { type: \"decide\"; matched: boolean }\n\t| { type: \"enqueue\" }\n\t| { type: \"deliver\" }\n\t| { type: \"fail\" }\n\t| { type: \"dead\" }\n\t| { type: \"requeue\" }\n\t| { type: \"rollback\" }\n\t| { type: \"replay\"; cursor: string };\n\nexport type QueueState = QueueCounters & {\n\tstage: QueueStage;\n\tlast_cursor: string | null;\n\tdedupe: Set<string>;\n};\n\nexport function emptyQueueState(): QueueState {\n\treturn {\n\t\tstage: \"decision\",\n\t\taccepted: 0,\n\t\tdecided: 0,\n\t\tenqueued: 0,\n\t\tdelivered: 0,\n\t\tdead: 0,\n\t\tfence_cursor: null,\n\t\tlast_cursor: null,\n\t\tdedupe: new Set(),\n\t};\n}\n\nexport type QueueApply =\n\t| { ok: true; state: QueueState }\n\t| { ok: false; reason: string; state: QueueState };\n\nexport function applyQueueEvent(\n\tstate: QueueState,\n\tevent: QueueEvent,\n\tdedupeKey?: string,\n): QueueApply {\n\tif (dedupeKey && state.dedupe.has(dedupeKey)) {\n\t\treturn { ok: true, state };\n\t}\n\tconst next: QueueState = {\n\t\t...state,\n\t\tdedupe: new Set(state.dedupe),\n\t};\n\tif (dedupeKey) next.dedupe.add(dedupeKey);\n\n\tswitch (event.type) {\n\t\tcase \"accept\":\n\t\t\tnext.accepted += 1;\n\t\t\tnext.stage = \"decision\";\n\t\t\treturn { ok: true, state: next };\n\t\tcase \"decide\":\n\t\t\tif (next.decided >= next.accepted) {\n\t\t\t\treturn { ok: false, reason: \"decide without accept\", state };\n\t\t\t}\n\t\t\tnext.decided += 1;\n\t\t\tnext.stage = event.matched ? \"outbox\" : \"decision\";\n\t\t\treturn { ok: true, state: next };\n\t\tcase \"enqueue\":\n\t\t\tif (next.enqueued >= next.decided) {\n\t\t\t\treturn { ok: false, reason: \"enqueue without decide\", state };\n\t\t\t}\n\t\t\tnext.enqueued += 1;\n\t\t\tnext.stage = \"outbox\";\n\t\t\treturn { ok: true, state: next };\n\t\tcase \"deliver\":\n\t\t\tif (next.delivered + next.dead >= next.enqueued) {\n\t\t\t\treturn { ok: false, reason: \"deliver without enqueue\", state };\n\t\t\t}\n\t\t\tnext.delivered += 1;\n\t\t\tnext.stage = \"delivery\";\n\t\t\treturn { ok: true, state: next };\n\t\tcase \"fail\":\n\t\t\tnext.stage = \"delivery\";\n\t\t\treturn { ok: true, state: next };\n\t\tcase \"dead\":\n\t\t\tif (next.delivered + next.dead >= next.enqueued) {\n\t\t\t\treturn { ok: false, reason: \"dead without enqueue\", state };\n\t\t\t}\n\t\t\tnext.dead += 1;\n\t\t\tnext.stage = \"dlq\";\n\t\t\treturn { ok: true, state: next };\n\t\tcase \"requeue\":\n\t\t\tif (next.dead < 1)\n\t\t\t\treturn { ok: false, reason: \"requeue empty dlq\", state };\n\t\t\tnext.dead -= 1;\n\t\t\tnext.enqueued += 1;\n\t\t\tnext.stage = \"outbox\";\n\t\t\treturn { ok: true, state: next };\n\t\tcase \"rollback\":\n\t\t\tnext.stage = \"rollback\";\n\t\t\treturn { ok: true, state: next };\n\t\tcase \"replay\":\n\t\t\tnext.stage = \"replay\";\n\t\t\tnext.last_cursor = event.cursor;\n\t\t\tnext.fence_cursor = event.cursor;\n\t\t\treturn { ok: true, state: next };\n\t}\n}\n\nexport function queueCaughtUp(state: QueueState): boolean {\n\treturn (\n\t\tstate.accepted === state.decided &&\n\t\tstate.delivered + state.dead === state.enqueued\n\t);\n}\n",
|
|
17
17
|
"/**\n * Repair SAFETY GATE — decides whether a proposed repair is allowed to run.\n * Only registered safe modes pass; unsafe ranges and mode/defect mismatches are\n * refused.\n *\n * This file plans and refuses. It does not repair. The executor that actually\n * writes is `secondlayer repair --apply` (packages/cli/src/commands/repair.ts),\n * which rewrites blocks from a signed archive inside a transaction.\n *\n * There used to be an `applyRepair` here that returned `{applied: true}`\n * without touching anything. It was removed in 2026-08 rather than\n * implemented: a second executor would have been a fake one, and drills that\n * \"recovered\" through it would have proven nothing.\n */\n\nimport type { RepairMode } from \"./constraints.ts\";\nimport type { CoverageRange } from \"./constraints.ts\";\n\nexport const SAFE_REPAIR_MODES: readonly RepairMode[] = [\n\t\"range_safe\",\n\t\"full_reindex\",\n\t\"archive_replay\",\n];\n\nexport type RepairDefect =\n\t| \"gap\"\n\t| \"digest_mismatch\"\n\t| \"omission\"\n\t| \"version\"\n\t| \"reorg\"\n\t| \"source_gap\";\n\nexport type RepairPlan = {\n\tstage_id: string;\n\trange: CoverageRange;\n\tmode: RepairMode;\n\tdefect: RepairDefect;\n\tsafe: boolean;\n\treason: string;\n};\n\nexport function planRepair(input: {\n\tstage_id: string;\n\trange: CoverageRange;\n\tmode: RepairMode;\n\tdefect: RepairDefect;\n\trange_safe_proven?: boolean;\n}): RepairPlan {\n\tif (input.range.from_height > input.range.to_height) {\n\t\treturn {\n\t\t\t...input,\n\t\t\tsafe: false,\n\t\t\treason: \"inverted range\",\n\t\t};\n\t}\n\tif (!(SAFE_REPAIR_MODES as readonly string[]).includes(input.mode)) {\n\t\treturn {\n\t\t\t...input,\n\t\t\tsafe: false,\n\t\t\treason: `mode ${input.mode} is not registered`,\n\t\t};\n\t}\n\tif (input.mode === \"range_safe\" && !input.range_safe_proven) {\n\t\treturn {\n\t\t\t...input,\n\t\t\tsafe: false,\n\t\t\treason: \"range_safe requires a proven range\",\n\t\t};\n\t}\n\tif (input.defect === \"source_gap\" && input.mode !== \"archive_replay\") {\n\t\treturn {\n\t\t\t...input,\n\t\t\tsafe: false,\n\t\t\treason: \"raw/source gaps require archive_replay\",\n\t\t};\n\t}\n\treturn {\n\t\t...input,\n\t\tsafe: true,\n\t\treason: `dry-run ${input.mode} for ${input.defect}`,\n\t};\n}\n",
|
|
18
18
|
"/**\n * Unified verify targets — library used by CLI (and REST later).\n * Exit codes match `sl verify`: 0 clean, 1 diverged, 2 unanchored.\n */\n\nexport const VERIFY_EXIT = {\n\tCLEAN: 0,\n\tDIVERGED: 1,\n\tUNANCHORED: 2,\n} as const;\nexport type VerifyExit = (typeof VERIFY_EXIT)[keyof typeof VERIFY_EXIT];\n\nexport type VerifyTarget =\n\t| { kind: \"all\" }\n\t| { kind: \"raw\" }\n\t| { kind: \"decode\"; name: string }\n\t| { kind: \"subgraph\"; name: string };\n\nexport type VerifyMode = \"quick\" | \"deep\" | \"anchor\";\n\nexport function parseVerifyTarget(value: string | undefined): VerifyTarget {\n\tconst raw = (value ?? \"raw\").trim();\n\tif (raw === \"all\") return { kind: \"all\" };\n\tif (raw === \"raw\" || raw === \"\") return { kind: \"raw\" };\n\tif (raw.startsWith(\"decode:\")) {\n\t\tconst name = raw.slice(\"decode:\".length).trim();\n\t\tif (!name) throw new Error(\"decode: target needs a decoder name\");\n\t\treturn { kind: \"decode\", name };\n\t}\n\tif (raw.startsWith(\"subgraph:\")) {\n\t\tconst name = raw.slice(\"subgraph:\".length).trim();\n\t\tif (!name) throw new Error(\"subgraph: target needs a subgraph name\");\n\t\treturn { kind: \"subgraph\", name };\n\t}\n\tthrow new Error(\n\t\t`verify target must be all | raw | decode:<name> | subgraph:<name> (got ${value})`,\n\t);\n}\n\nconst RAW_DATASETS = new Set([\"blocks\", \"transactions\", \"events\"]);\n\n/** Whether an archive/coverage dataset belongs to this verify target. */\nexport function datasetMatchesTarget(\n\tdataset: string,\n\ttarget: VerifyTarget,\n): boolean {\n\tif (target.kind === \"all\") return true;\n\tconst name = dataset.toLowerCase();\n\tif (target.kind === \"raw\") return RAW_DATASETS.has(name);\n\tif (target.kind === \"decode\") {\n\t\treturn (\n\t\t\tname === target.name.toLowerCase() ||\n\t\t\tname === `decode:${target.name.toLowerCase()}` ||\n\t\t\tname.endsWith(`_${target.name.toLowerCase()}`)\n\t\t);\n\t}\n\treturn (\n\t\tname === target.name.toLowerCase() ||\n\t\tname === `subgraph:${target.name.toLowerCase()}` ||\n\t\tname.startsWith(`${target.name.toLowerCase()}_`)\n\t);\n}\n\nexport type VerifyReport = {\n\ttarget: VerifyTarget;\n\tmode: VerifyMode;\n\texit: VerifyExit;\n\tstatus: \"clean\" | \"diverged\" | \"unanchored\";\n\tdetail: string;\n};\n\nexport function reportVerify(opts: {\n\ttarget: VerifyTarget;\n\tmode?: VerifyMode;\n\tanchored?: boolean;\n\tdiverged?: boolean;\n\tdetail?: string;\n}): VerifyReport {\n\tconst mode = opts.mode ?? \"quick\";\n\tif (opts.anchored === false) {\n\t\treturn {\n\t\t\ttarget: opts.target,\n\t\t\tmode,\n\t\t\texit: VERIFY_EXIT.UNANCHORED,\n\t\t\tstatus: \"unanchored\",\n\t\t\tdetail: opts.detail ?? \"reference unavailable or unverifiable\",\n\t\t};\n\t}\n\tif (opts.diverged) {\n\t\treturn {\n\t\t\ttarget: opts.target,\n\t\t\tmode,\n\t\t\texit: VERIFY_EXIT.DIVERGED,\n\t\t\tstatus: \"diverged\",\n\t\t\tdetail: opts.detail ?? \"coverage or archive diverged\",\n\t\t};\n\t}\n\tconst label =\n\t\topts.target.kind === \"decode\"\n\t\t\t? `decode:${opts.target.name}`\n\t\t\t: opts.target.kind === \"subgraph\"\n\t\t\t\t? `subgraph:${opts.target.name}`\n\t\t\t\t: opts.target.kind;\n\treturn {\n\t\ttarget: opts.target,\n\t\tmode,\n\t\texit: VERIFY_EXIT.CLEAN,\n\t\tstatus: \"clean\",\n\t\tdetail: opts.detail ?? `${label} ${mode} is clean`,\n\t};\n}\n"
|
|
19
19
|
],
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Only the `standard-webhooks` format carries an HMAC; `raw`/`cloudevents`/etc.
|
|
6
6
|
* carried no Secondlayer proof, so a receiver had no way to know a payload came
|
|
7
7
|
* from us. This signs each delivery with a single platform ed25519 key so any
|
|
8
|
-
* receiver verifies with the published public key — no per-
|
|
8
|
+
* receiver verifies with the published public key — no per-webhook secret,
|
|
9
9
|
* and the body shape stays format-specific.
|
|
10
10
|
*
|
|
11
11
|
* Header names are lowercase to match the format builders' header maps (HTTP
|
|
@@ -28,7 +28,7 @@ declare function getSecondlayerWebhookSigner(): SecondlayerWebhookSigner | null;
|
|
|
28
28
|
declare function resetSecondlayerWebhookSignerForTest(): void;
|
|
29
29
|
/**
|
|
30
30
|
* Boot-time guard for any service whose job is to DELIVER signed webhooks (the
|
|
31
|
-
*
|
|
31
|
+
* webhook-processor). We market every delivery as carrying a verifiable
|
|
32
32
|
* ed25519 signature; if no signing key is configured the signer is a silent
|
|
33
33
|
* no-op and deliveries ship UNSIGNED. That's worse than a missing feature — a
|
|
34
34
|
* partner's verify step fails on data we told them was signed.
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"sourcesContent": [
|
|
5
5
|
"import { z } from \"zod\";\n\n// Parse comma-separated networks\nconst networksSchema = z.string().transform((val) => {\n\tconst networks = val\n\t\t.split(\",\")\n\t\t.map((n) => n.trim())\n\t\t.filter(Boolean);\n\tconst valid = [\"mainnet\", \"testnet\"];\n\tfor (const n of networks) {\n\t\tif (!valid.includes(n)) {\n\t\t\tthrow new Error(\n\t\t\t\t`Invalid network: ${n}. Must be one of: ${valid.join(\", \")}`,\n\t\t\t);\n\t\t}\n\t}\n\treturn networks as (\"mainnet\" | \"testnet\")[];\n});\n\ninterface EnvSchemaOutput {\n\tDATABASE_URL?: string;\n\t/**\n\t * Shared indexer DB (blocks/txs/events). Falls back to DATABASE_URL.\n\t * Set this alongside TARGET_DATABASE_URL to enable dual-DB mode.\n\t */\n\tSOURCE_DATABASE_URL?: string;\n\t/**\n\t * Tenant DB (subgraph schemas + subgraphs table). Falls back to DATABASE_URL.\n\t * Set this alongside SOURCE_DATABASE_URL to enable dual-DB mode.\n\t */\n\tTARGET_DATABASE_URL?: string;\n\tNETWORK?: \"mainnet\" | \"testnet\";\n\tNETWORKS?: (\"mainnet\" | \"testnet\")[];\n\tLOG_LEVEL: \"debug\" | \"info\" | \"warn\" | \"error\";\n\tNODE_ENV: \"development\" | \"production\" | \"test\";\n}\n\n// Cast needed: z.preprocess / z.default create different _input vs _output types\n// that z.ZodType<T> can't represent without explicit input type param\nconst envSchema: z.ZodType<EnvSchemaOutput> = z.object({\n\tDATABASE_URL: z.preprocess(\n\t\t(val) => (typeof val === \"string\" && val.length === 0 ? undefined : val),\n\t\tz.string().url().optional(),\n\t),\n\tSOURCE_DATABASE_URL: z.preprocess(\n\t\t(val) => (typeof val === \"string\" && val.length === 0 ? undefined : val),\n\t\tz.string().url().optional(),\n\t),\n\tTARGET_DATABASE_URL: z.preprocess(\n\t\t(val) => (typeof val === \"string\" && val.length === 0 ? undefined : val),\n\t\tz.string().url().optional(),\n\t),\n\tNETWORK: z.enum([\"mainnet\", \"testnet\"]).optional(),\n\tNETWORKS: networksSchema.optional(),\n\tLOG_LEVEL: z.enum([\"debug\", \"info\", \"warn\", \"error\"]).default(\"info\"),\n\tNODE_ENV: z\n\t\t.enum([\"development\", \"production\", \"test\"])\n\t\t.default(\"development\"),\n}) as unknown as z.ZodType<EnvSchemaOutput>;\n\nexport type Env = EnvSchemaOutput & {\n\tenabledNetworks: (\"mainnet\" | \"testnet\")[];\n};\n\nlet cachedEnv: Env | null = null;\n\nexport function getEnv(): Env {\n\tif (cachedEnv) {\n\t\treturn cachedEnv;\n\t}\n\n\tconst result = envSchema.safeParse(process.env);\n\n\tif (!result.success) {\n\t\tconsole.error(\"❌ Invalid environment configuration:\");\n\t\tconsole.error(z.treeifyError(result.error));\n\t\tthrow new Error(\"Invalid environment configuration\");\n\t}\n\n\t// Compute enabled networks from NETWORKS or NETWORK\n\tlet enabledNetworks: (\"mainnet\" | \"testnet\")[];\n\tif (result.data.NETWORKS && result.data.NETWORKS.length > 0) {\n\t\tenabledNetworks = result.data.NETWORKS;\n\t} else if (result.data.NETWORK) {\n\t\tenabledNetworks = [result.data.NETWORK];\n\t} else {\n\t\tenabledNetworks = [\"mainnet\"]; // Default\n\t}\n\n\tcachedEnv = { ...result.data, enabledNetworks };\n\treturn cachedEnv;\n}\n\n/**\n * True when `NODE_ENV=production`, read at RUNTIME — the single source of truth\n * for prod-vs-dev branching in this package.\n *\n * Why a helper instead of inlining `process.env.NODE_ENV === \"production\"`:\n * bunup/esbuild constant-folds the dot-access `process.env.NODE_ENV` to its\n * BUILD-time value. `@secondlayer/shared` is consumed by other services as its\n * built `dist`, so an inlined check freezes to a literal in the shipped bundle\n * (e.g. `const isProd = false`) and silently ignores the container's real\n * NODE_ENV. The bracket access below is NOT folded, so it stays a runtime read.\n * Route every prod check through here so the footgun lives in exactly one place.\n */\nexport function isProductionEnv(): boolean {\n\t// biome-ignore lint/complexity/useLiteralKeys: bracket access is deliberate — dot-access gets constant-folded by the bundler, freezing this to a build-time literal in the shipped dist.\n\treturn process.env[\"NODE_ENV\"] === \"production\";\n}\n\ntype EnvMap = NodeJS.ProcessEnv;\n\n/**\n * PoX-4 stacking decoder is ON by default — `/v1/index/stacking` is part of the\n * public surface, so the decoder that fills `pox4_calls` runs unless explicitly\n * opted out with `POX4_DECODER_ENABLED=false` (mirrors the sBTC decoder policy).\n */\nexport function isPox4DecoderEnabled(env: EnvMap = process.env): boolean {\n\treturn env.POX4_DECODER_ENABLED !== \"false\";\n}\n\n/**\n * PoX-5 print-event decoder is ON by default (mirrors pox-4/sBTC policy).\n * No activation gating needed: pox-5 events cannot exist before the Epoch 4.0\n * hard fork (Bitcoin block 960,230), so pre-activation the decoder idles at\n * tip. Opt out with `POX5_DECODER_ENABLED=false`.\n */\nexport function isPox5DecoderEnabled(env: EnvMap = process.env): boolean {\n\treturn env.POX5_DECODER_ENABLED !== \"false\";\n}\n\n/** Default ON. Opt out with `SBTC_DECODER_ENABLED=false`. */\nexport function isSbtcDecoderEnabled(env: EnvMap = process.env): boolean {\n\treturn env.SBTC_DECODER_ENABLED !== \"false\";\n}\n\n/** Default OFF. Opt in with `BNS_DECODER_ENABLED=true`. */\nexport function isBnsDecoderEnabled(env: EnvMap = process.env): boolean {\n\treturn env.BNS_DECODER_ENABLED === \"true\";\n}\n\n// Export for testing\nexport { envSchema };\n",
|
|
6
6
|
"import {\n\ttype KeyObject,\n\tcreateHash,\n\tcreatePrivateKey,\n\tcreatePublicKey,\n\tgenerateKeyPairSync,\n\tsign as nodeSign,\n\tverify as nodeVerify,\n} from \"node:crypto\";\n\n/**\n * Asymmetric ed25519 signing for Streams response proofs.\n *\n * Asymmetric (not HMAC) so the proof is real: only the server holds the private\n * key, and any consumer verifies with the published public key — no shared\n * secret to leak. ed25519 uses node's `sign`/`verify` with a `null` algorithm.\n * Keys are PEM (PKCS8 private / SPKI public) for env transport; load once and\n * reuse the KeyObject on hot paths.\n */\n\nexport function generateEd25519KeyPair(): {\n\tprivateKeyPem: string;\n\tpublicKeyPem: string;\n} {\n\tconst { privateKey, publicKey } = generateKeyPairSync(\"ed25519\");\n\treturn {\n\t\tprivateKeyPem: privateKey\n\t\t\t.export({ format: \"pem\", type: \"pkcs8\" })\n\t\t\t.toString(),\n\t\tpublicKeyPem: publicKey.export({ format: \"pem\", type: \"spki\" }).toString(),\n\t};\n}\n\nexport function loadEd25519PrivateKey(pem: string): KeyObject {\n\treturn createPrivateKey(pem);\n}\n\nexport function loadEd25519PublicKey(pem: string): KeyObject {\n\treturn createPublicKey(pem);\n}\n\nexport function publicKeyPemFromPrivate(privateKeyPem: string): string {\n\treturn createPublicKey(createPrivateKey(privateKeyPem))\n\t\t.export({ format: \"pem\", type: \"spki\" })\n\t\t.toString();\n}\n\n/** Stable short id for a public key (rotation hint via X-Signature-KeyId). */\nexport function ed25519KeyId(publicKeyPem: string): string {\n\tconst der = createPublicKey(publicKeyPem).export({\n\t\tformat: \"der\",\n\t\ttype: \"spki\",\n\t});\n\treturn createHash(\"sha256\").update(der).digest(\"base64url\").slice(0, 16);\n}\n\nexport function signEd25519(payload: string, privateKey: KeyObject): string {\n\treturn nodeSign(null, Buffer.from(payload, \"utf8\"), privateKey).toString(\n\t\t\"base64\",\n\t);\n}\n\nexport function verifyEd25519(\n\tpayload: string,\n\tsignatureBase64: string,\n\tpublicKey: KeyObject,\n): boolean {\n\ttry {\n\t\treturn nodeVerify(\n\t\t\tnull,\n\t\t\tBuffer.from(payload, \"utf8\"),\n\t\t\tpublicKey,\n\t\t\tBuffer.from(signatureBase64, \"base64\"),\n\t\t);\n\t} catch {\n\t\treturn false;\n\t}\n}\n",
|
|
7
|
-
"import { isProductionEnv } from \"../env.ts\";\nimport {\n\ted25519KeyId,\n\tloadEd25519PrivateKey,\n\tloadEd25519PublicKey,\n\tpublicKeyPemFromPrivate,\n\tsignEd25519,\n\tverifyEd25519,\n} from \"./ed25519.ts\";\n\n/**\n * Universal Secondlayer webhook authenticity — an ed25519 signature attached to\n * EVERY delivery regardless of body format.\n *\n * Only the `standard-webhooks` format carries an HMAC; `raw`/`cloudevents`/etc.\n * carried no Secondlayer proof, so a receiver had no way to know a payload came\n * from us. This signs each delivery with a single platform ed25519 key so any\n * receiver verifies with the published public key — no per-
|
|
7
|
+
"import { isProductionEnv } from \"../env.ts\";\nimport {\n\ted25519KeyId,\n\tloadEd25519PrivateKey,\n\tloadEd25519PublicKey,\n\tpublicKeyPemFromPrivate,\n\tsignEd25519,\n\tverifyEd25519,\n} from \"./ed25519.ts\";\n\n/**\n * Universal Secondlayer webhook authenticity — an ed25519 signature attached to\n * EVERY delivery regardless of body format.\n *\n * Only the `standard-webhooks` format carries an HMAC; `raw`/`cloudevents`/etc.\n * carried no Secondlayer proof, so a receiver had no way to know a payload came\n * from us. This signs each delivery with a single platform ed25519 key so any\n * receiver verifies with the published public key — no per-webhook secret,\n * and the body shape stays format-specific.\n *\n * Header names are lowercase to match the format builders' header maps (HTTP\n * header names are case-insensitive on the wire).\n */\nexport const WEBHOOK_ID_HEADER = \"webhook-id\";\nexport const SECONDLAYER_SIGNATURE_HEADER = \"x-secondlayer-signature\";\nexport const SECONDLAYER_KEY_ID_HEADER = \"x-secondlayer-signature-keyid\";\n\nexport type SecondlayerWebhookSigner = {\n\tkeyId: string;\n\tpublicKeyPem: string;\n\tsign(payload: string): string;\n};\n\nlet cached: SecondlayerWebhookSigner | null | undefined;\n\nfunction signingKeyFromEnv(): string | undefined {\n\t// A dedicated webhook key if provided, else the platform Streams key — both\n\t// are the same ed25519 \"single platform identity\", so reusing it keeps key\n\t// distribution to one published public key.\n\treturn (\n\t\tprocess.env.SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY ||\n\t\tprocess.env.STREAMS_SIGNING_PRIVATE_KEY ||\n\t\tundefined\n\t);\n}\n\n/**\n * Memoized webhook signer, or null when no key is configured (signing is then a\n * no-op, so the universal header is safe to ship before a key is provisioned).\n */\nexport function getSecondlayerWebhookSigner(): SecondlayerWebhookSigner | null {\n\tif (cached !== undefined) return cached;\n\tconst raw = signingKeyFromEnv();\n\tif (!raw) {\n\t\tcached = null;\n\t\treturn null;\n\t}\n\t// Env transport often escapes newlines; restore real PEM line breaks.\n\tconst pem = raw.includes(\"\\\\n\") ? raw.replace(/\\\\n/g, \"\\n\") : raw;\n\tconst privateKey = loadEd25519PrivateKey(pem);\n\tconst publicKeyPem = publicKeyPemFromPrivate(pem);\n\tcached = {\n\t\tkeyId: ed25519KeyId(publicKeyPem),\n\t\tpublicKeyPem,\n\t\tsign: (payload) => signEd25519(payload, privateKey),\n\t};\n\treturn cached;\n}\n\n/** Reset the memoized signer. Tests only. */\nexport function resetSecondlayerWebhookSignerForTest(): void {\n\tcached = undefined;\n}\n\n/**\n * Boot-time guard for any service whose job is to DELIVER signed webhooks (the\n * webhook-processor). We market every delivery as carrying a verifiable\n * ed25519 signature; if no signing key is configured the signer is a silent\n * no-op and deliveries ship UNSIGNED. That's worse than a missing feature — a\n * partner's verify step fails on data we told them was signed.\n *\n * Unlike `assertDbSplit` (fail-soft: only ever logs), this is fail-HARD in prod:\n * refuse to boot so the misconfig surfaces as a crash-loop instead of unsigned\n * traffic. Keep the throw — don't \"soften\" it back to a warning. Set\n * `ALLOW_UNSIGNED_WEBHOOKS=true` to opt out (self-host / intentional unsigned),\n * which logs the posture loudly in every env so it's never silent.\n */\nexport function assertWebhookSigningConfigured(): void {\n\tif (getSecondlayerWebhookSigner()) return;\n\n\tconst optedOut = process.env.ALLOW_UNSIGNED_WEBHOOKS === \"true\";\n\t// isProductionEnv() reads NODE_ENV at runtime; do NOT inline a\n\t// `process.env.NODE_ENV` check — the bundler constant-folds it and the prod\n\t// throw would silently die in the shipped dist. See env.ts for the why.\n\tconst isProd = isProductionEnv();\n\tconst msg =\n\t\t\"No webhook signing key (SECONDLAYER_WEBHOOK_SIGNING_PRIVATE_KEY / STREAMS_SIGNING_PRIVATE_KEY) — deliveries would ship UNSIGNED while we market signed webhooks\";\n\n\tif (optedOut) {\n\t\tconsole.warn(`⚠️ ${msg}; continuing because ALLOW_UNSIGNED_WEBHOOKS=true`);\n\t\treturn;\n\t}\n\tif (isProd) {\n\t\tthrow new Error(\n\t\t\t`${msg}. Refusing to boot. Set the signing key, or ALLOW_UNSIGNED_WEBHOOKS=true to override.`,\n\t\t);\n\t}\n\tconsole.warn(`⚠️ ${msg} (non-prod: continuing)`);\n}\n\n/**\n * The exact bytes the signature covers: `${webhookId}.${body}`. Binding the id\n * into the signed content stops a captured body from being replayed under a\n * different delivery id.\n */\nfunction signedContent(webhookId: string, body: string): string {\n\treturn `${webhookId}.${body}`;\n}\n\n/**\n * Build the universal authenticity headers for a delivery. Returns null when no\n * signing key is configured (caller leaves the delivery unsigned).\n */\nexport function signSecondlayerWebhook(\n\twebhookId: string,\n\tbody: string,\n): Record<string, string> | null {\n\tconst signer = getSecondlayerWebhookSigner();\n\tif (!signer) return null;\n\treturn {\n\t\t[WEBHOOK_ID_HEADER]: webhookId,\n\t\t[SECONDLAYER_SIGNATURE_HEADER]: signer.sign(signedContent(webhookId, body)),\n\t\t[SECONDLAYER_KEY_ID_HEADER]: signer.keyId,\n\t};\n}\n\n/**\n * Verify a delivery's ed25519 signature over `${webhookId}.${rawBody}` against\n * the published public key. Low-level (raw values); receivers use the SDK's\n * `verifySecondlayerSignature`, which extracts the headers ergonomically.\n */\nexport function verifySecondlayerSignatureValues(\n\trawBody: string,\n\twebhookId: string | undefined,\n\tsignatureBase64: string | undefined,\n\tpublicKeyPem: string,\n): boolean {\n\tif (!webhookId || !signatureBase64) return false;\n\tconst publicKey = loadEd25519PublicKey(publicKeyPem);\n\treturn verifyEd25519(\n\t\tsignedContent(webhookId, rawBody),\n\t\tsignatureBase64,\n\t\tpublicKey,\n\t);\n}\n"
|
|
8
8
|
],
|
|
9
9
|
"mappings": "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",
|
|
10
10
|
"debugId": "4EF56E0B6A85BEF064756E2164756E21",
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"sources": ["../src/mode.ts", "../src/crypto/secrets.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
5
|
"/**\n * Instance modes for the Secondlayer platform.\n *\n * - `oss`: self-hosted, single-tenant. No auth middleware, no platform routes\n * (projects, admin). Everything runs against a single `DATABASE_URL`.\n * Intended for `docker compose up`.\n *\n * - `platform`: the metered archive deployment. Magic-link auth, API keys,\n * accounts, credits, and metered `/v1` reads (Index + Streams + subgraphs)\n * against a single shared DB. No paid tier ladder — one metered `free`\n * tier plus first-party internal service credentials.\n */\n\nexport type InstanceMode = \"oss\" | \"platform\";\n\n/** What the operator wrote in the env — `archive` is the honest name for the\n * metered public-archive deployment and aliases `platform` behaviorally. */\nexport type DeclaredInstanceMode = InstanceMode | \"archive\";\n\nconst VALID_MODES: readonly DeclaredInstanceMode[] = [\n\t\"oss\",\n\t\"platform\",\n\t\"archive\",\n];\n\nfunction declaredMode(): DeclaredInstanceMode {\n\tconst raw = process.env.INSTANCE_MODE?.trim().toLowerCase();\n\tif (raw && (VALID_MODES as readonly string[]).includes(raw)) {\n\t\treturn raw as DeclaredInstanceMode;\n\t}\n\treturn \"oss\";\n}\n\n/**\n * Resolve the active instance mode from `process.env.INSTANCE_MODE`.\n * Defaults to `\"oss\"` — the safest default for self-hosters who deploy\n * without setting the variable. `archive` normalizes to `platform` here so\n * every behavioral branch stays a two-way choice; use\n * `getDeclaredInstanceMode()` for display surfaces.\n */\nexport function getInstanceMode(): InstanceMode {\n\tconst declared = declaredMode();\n\treturn declared === \"archive\" ? \"platform\" : declared;\n}\n\n/** The mode as declared in the env, for status/identity surfaces. */\nexport function getDeclaredInstanceMode(): DeclaredInstanceMode {\n\treturn declaredMode();\n}\n\n/**\n * True when public reads are credit-metered (the free window, retention, and\n * credits gates arm). Platform/archive deployments meter by default;\n * self-host never does unless the operator explicitly sets\n * `METERED_READS=true`. `METERED_READS=false` disarms a metered deployment.\n */\nexport function isMeteredReads(): boolean {\n\tconst override = process.env.METERED_READS?.trim().toLowerCase();\n\tif (override === \"true\") return true;\n\tif (override === \"false\") return false;\n\treturn getInstanceMode() === \"platform\";\n}\n\n/** True when the active mode is `\"platform\"` (shared multi-tenant). */\nexport function isPlatformMode(): boolean {\n\treturn getInstanceMode() === \"platform\";\n}\n\n/** True when the active mode is `\"oss\"` (self-hosted). */\nexport function isOssMode(): boolean {\n\treturn getInstanceMode() === \"oss\";\n}\n",
|
|
6
|
-
"import { createCipheriv, createDecipheriv, randomBytes } from \"node:crypto\";\nimport {\n\tappendFileSync,\n\tcloseSync,\n\texistsSync,\n\topenSync,\n\treadFileSync,\n\tunlinkSync,\n} from \"node:fs\";\nimport { resolve } from \"node:path\";\nimport { getInstanceMode } from \"../mode.ts\";\n\n/**\n * AES-256-GCM symmetric envelope for encrypted secrets at rest (tenant keys,\n *
|
|
6
|
+
"import { createCipheriv, createDecipheriv, randomBytes } from \"node:crypto\";\nimport {\n\tappendFileSync,\n\tcloseSync,\n\texistsSync,\n\topenSync,\n\treadFileSync,\n\tunlinkSync,\n} from \"node:fs\";\nimport { resolve } from \"node:path\";\nimport { getInstanceMode } from \"../mode.ts\";\n\n/**\n * AES-256-GCM symmetric envelope for encrypted secrets at rest (tenant keys,\n * webhook signing secrets, etc.).\n *\n * Ciphertext layout: `iv (12 bytes) || authTag (16 bytes) || ciphertext`\n *\n * The key comes from `SECONDLAYER_SECRETS_KEY` — 32 bytes hex. In OSS mode,\n * if the env var is unset on first use we autogenerate a key and persist it\n * to `.env.local` in the current working directory so subsequent restarts\n * pick it up without user intervention. Dedicated/platform modes throw —\n * those runtimes must provision the key explicitly.\n *\n * Rotation strategy: re-encrypt all rows with the new key and swap the env\n * var. Not zero-downtime, but acceptable at v2 scale. For real KMS (AWS\n * KMS, Vault, GCP KMS), wrap the same byte layout behind an\n * `EncryptSecret`/`DecryptSecret` interface and swap at startup.\n */\n\nconst KEY_ENV = \"SECONDLAYER_SECRETS_KEY\";\nconst IV_LEN = 12;\nconst TAG_LEN = 16;\n\nfunction readExistingKey(envPath: string): string | null {\n\tif (!existsSync(envPath)) return null;\n\tconst contents = readFileSync(envPath, \"utf8\");\n\tconst match = contents.match(/^SECONDLAYER_SECRETS_KEY=([a-fA-F0-9]{64})/m);\n\t// biome-ignore lint/style/noNonNullAssertion: value is non-null after preceding check or by construction; TS narrowing limitation\n\treturn match ? match[1]! : null;\n}\n\n/**\n * Atomic file lock via `openSync(..., \"wx\")` — O_CREAT | O_EXCL. If two\n * processes race on cold-compose start, exactly one creates the lock\n * file; the loser polls until the winner finishes writing `.env.local`,\n * then reads the winner's key. Stale locks (process crashed mid-write)\n * are cleaned after `STALE_LOCK_MS`.\n */\nconst STALE_LOCK_MS = 10_000;\nconst POLL_MS = 25;\n\nfunction bootstrapOssKey(): string {\n\tconst envPath = resolve(process.cwd(), \".env.local\");\n\n\t// Fast path — key already on disk from a prior run.\n\tconst existing = readExistingKey(envPath);\n\tif (existing) {\n\t\tprocess.env[KEY_ENV] = existing;\n\t\treturn existing;\n\t}\n\n\tconst lockPath = `${envPath}.secret-bootstrap.lock`;\n\tlet lockFd: number | null = null;\n\ttry {\n\t\tlockFd = openSync(lockPath, \"wx\", 0o600);\n\t} catch (err) {\n\t\tconst e = err as NodeJS.ErrnoException;\n\t\tif (e.code !== \"EEXIST\") throw err;\n\t}\n\n\tif (lockFd === null) {\n\t\t// Another process is bootstrapping. Poll for its result.\n\t\tconst deadline = Date.now() + STALE_LOCK_MS;\n\t\twhile (Date.now() < deadline) {\n\t\t\tconst key = readExistingKey(envPath);\n\t\t\tif (key) {\n\t\t\t\tprocess.env[KEY_ENV] = key;\n\t\t\t\treturn key;\n\t\t\t}\n\t\t\tBun.sleepSync(POLL_MS);\n\t\t}\n\t\t// Lock holder died mid-write — force-clean and retry once.\n\t\ttry {\n\t\t\tunlinkSync(lockPath);\n\t\t} catch {}\n\t\treturn bootstrapOssKey();\n\t}\n\n\ttry {\n\t\tconst hex = randomBytes(32).toString(\"hex\");\n\t\tconst line = `${existsSync(envPath) ? \"\\n\" : \"\"}${KEY_ENV}=${hex}\\n`;\n\t\tappendFileSync(envPath, line, { mode: 0o600 });\n\t\tprocess.env[KEY_ENV] = hex;\n\t\tconsole.log(\n\t\t\t`[secondlayer] generated ${KEY_ENV}; saved to ${envPath} (mode 0600)`,\n\t\t);\n\t\treturn hex;\n\t} finally {\n\t\tcloseSync(lockFd);\n\t\ttry {\n\t\t\tunlinkSync(lockPath);\n\t\t} catch {}\n\t}\n}\n\nfunction loadKey(): Buffer {\n\tlet hex = process.env[KEY_ENV];\n\tif (!hex) {\n\t\tif (getInstanceMode() === \"oss\") {\n\t\t\thex = bootstrapOssKey();\n\t\t} else {\n\t\t\tthrow new Error(\n\t\t\t\t`${KEY_ENV} not set. Generate one with: openssl rand -hex 32`,\n\t\t\t);\n\t\t}\n\t}\n\tconst key = Buffer.from(hex, \"hex\");\n\tif (key.length !== 32) {\n\t\tthrow new Error(`${KEY_ENV} must be 32 bytes hex (got ${key.length})`);\n\t}\n\treturn key;\n}\n\nlet _cachedKey: Buffer | null = null;\nfunction getKey(): Buffer {\n\tif (!_cachedKey) _cachedKey = loadKey();\n\treturn _cachedKey;\n}\n\n/**\n * Non-throwing check for whether a secrets key is currently resolvable —\n * same resolution order as {@link loadKey} (env var, then the OSS\n * `.env.local` fallback), but never bootstraps a new key (that would\n * generate+persist a key as a side effect of a mere existence check), never\n * throws, and never returns or logs the key itself. Used by callers that\n * need to fail loud on a missing key BEFORE attempting a decrypt (f072).\n */\nexport function secretsKeyAvailable(): boolean {\n\tif (process.env[KEY_ENV]) return true;\n\tif (getInstanceMode() === \"oss\") {\n\t\tconst envPath = resolve(process.cwd(), \".env.local\");\n\t\treturn readExistingKey(envPath) !== null;\n\t}\n\treturn false;\n}\n\nexport function encryptSecret(plaintext: string): Buffer {\n\tconst key = getKey();\n\tconst iv = randomBytes(IV_LEN);\n\tconst cipher = createCipheriv(\"aes-256-gcm\", key, iv);\n\tconst ciphertext = Buffer.concat([\n\t\tcipher.update(plaintext, \"utf8\"),\n\t\tcipher.final(),\n\t]);\n\tconst tag = cipher.getAuthTag();\n\treturn Buffer.concat([iv, tag, ciphertext]);\n}\n\nexport function decryptSecret(envelope: Buffer): string {\n\tconst key = getKey();\n\tconst iv = envelope.subarray(0, IV_LEN);\n\tconst tag = envelope.subarray(IV_LEN, IV_LEN + TAG_LEN);\n\tconst ciphertext = envelope.subarray(IV_LEN + TAG_LEN);\n\tconst decipher = createDecipheriv(\"aes-256-gcm\", key, iv);\n\tdecipher.setAuthTag(tag);\n\treturn decipher.update(ciphertext).toString(\"utf8\") + decipher.final(\"utf8\");\n}\n\n/** Generate a fresh 32-byte hex key suitable for `SECONDLAYER_SECRETS_KEY`. */\nexport function generateSecretsKey(): string {\n\treturn randomBytes(32).toString(\"hex\");\n}\n"
|
|
7
7
|
],
|
|
8
8
|
"mappings": "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",
|
|
9
9
|
"debugId": "8E49B42A8C43DF1864756E2164756E21",
|
package/dist/src/db/index.d.ts
CHANGED
|
@@ -807,9 +807,9 @@ interface Database {
|
|
|
807
807
|
account_spend_caps: AccountSpendCapsTable;
|
|
808
808
|
account_credits: AccountCreditsTable;
|
|
809
809
|
hosted_meter_days: HostedMeterDaysTable;
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
810
|
+
webhooks: WebhooksTable;
|
|
811
|
+
webhook_outbox: WebhookOutboxTable;
|
|
812
|
+
webhook_deliveries: WebhookDeliveriesTable;
|
|
813
813
|
trigger_evaluator_state: TriggerEvaluatorStateTable;
|
|
814
814
|
decoded_events: DecodedEventsTable;
|
|
815
815
|
decoder_checkpoints: DecoderCheckpointsTable;
|
|
@@ -982,31 +982,31 @@ type SubgraphHealthSnapshot = Selectable<SubgraphHealthSnapshotsTable>;
|
|
|
982
982
|
type InsertSubgraphHealthSnapshot = Insertable<SubgraphHealthSnapshotsTable>;
|
|
983
983
|
type SubgraphGap = Selectable<SubgraphGapsTable>;
|
|
984
984
|
type InsertSubgraphGap = Insertable<SubgraphGapsTable>;
|
|
985
|
-
type
|
|
986
|
-
/** Polymorphic
|
|
985
|
+
type WebhookStatus = "active" | "paused" | "error";
|
|
986
|
+
/** Polymorphic webhook mode: `subgraph` reacts to processed table rows;
|
|
987
987
|
* `chain` reacts to raw chain events matched directly off the Index/Streams
|
|
988
988
|
* clock (no subgraph). See migration 0088. */
|
|
989
|
-
type
|
|
990
|
-
type
|
|
991
|
-
type
|
|
992
|
-
interface
|
|
989
|
+
type WebhookKind = "subgraph" | "chain";
|
|
990
|
+
type WebhookFormat = "standard-webhooks" | "inngest" | "trigger" | "cloudflare" | "cloudevents" | "raw";
|
|
991
|
+
type WebhookRuntime = "inngest" | "trigger" | "cloudflare" | "node";
|
|
992
|
+
interface WebhooksTable {
|
|
993
993
|
id: Generated<string>;
|
|
994
994
|
account_id: string;
|
|
995
995
|
project_id: string | null;
|
|
996
996
|
name: string;
|
|
997
|
-
status: ColumnType<
|
|
998
|
-
kind: ColumnType<
|
|
999
|
-
/** Null for chain
|
|
997
|
+
status: ColumnType<WebhookStatus, WebhookStatus | undefined, WebhookStatus>;
|
|
998
|
+
kind: ColumnType<WebhookKind, WebhookKind | undefined, WebhookKind>;
|
|
999
|
+
/** Null for chain webhooks (CHECK webhooks_kind_shape). */
|
|
1000
1000
|
subgraph_name: string | null;
|
|
1001
|
-
/** Null for chain
|
|
1001
|
+
/** Null for chain webhooks (CHECK webhooks_kind_shape). */
|
|
1002
1002
|
table_name: string | null;
|
|
1003
1003
|
/** Chain-trigger filter array (the `SubgraphFilter` shape, JSON). Null for
|
|
1004
|
-
* subgraph
|
|
1005
|
-
* import cycle; the Zod schema in schemas/
|
|
1004
|
+
* subgraph webhooks. Typed loosely here to avoid a shared→subgraphs
|
|
1005
|
+
* import cycle; the Zod schema in schemas/webhooks.ts owns the shape. */
|
|
1006
1006
|
triggers: unknown | null;
|
|
1007
1007
|
filter: Generated<unknown>;
|
|
1008
|
-
format: ColumnType<
|
|
1009
|
-
runtime:
|
|
1008
|
+
format: ColumnType<WebhookFormat, WebhookFormat | undefined, WebhookFormat>;
|
|
1009
|
+
runtime: WebhookRuntime | null;
|
|
1010
1010
|
url: string;
|
|
1011
1011
|
signing_secret_enc: Buffer;
|
|
1012
1012
|
auth_config: Generated<unknown>;
|
|
@@ -1021,17 +1021,17 @@ interface SubscriptionsTable {
|
|
|
1021
1021
|
created_at: Generated<Date>;
|
|
1022
1022
|
updated_at: Generated<Date>;
|
|
1023
1023
|
}
|
|
1024
|
-
type
|
|
1025
|
-
type
|
|
1026
|
-
type
|
|
1024
|
+
type Webhook = Selectable<WebhooksTable>;
|
|
1025
|
+
type InsertWebhook = Insertable<WebhooksTable>;
|
|
1026
|
+
type UpdateWebhook = Updateable<WebhooksTable>;
|
|
1027
1027
|
type OutboxStatus = "pending" | "delivered" | "dead";
|
|
1028
|
-
interface
|
|
1028
|
+
interface WebhookOutboxTable {
|
|
1029
1029
|
id: Generated<string>;
|
|
1030
|
-
|
|
1031
|
-
kind: ColumnType<
|
|
1032
|
-
/** Null for chain-
|
|
1030
|
+
webhook_id: string;
|
|
1031
|
+
kind: ColumnType<WebhookKind, WebhookKind | undefined, WebhookKind>;
|
|
1032
|
+
/** Null for chain-webhook rows. */
|
|
1033
1033
|
subgraph_name: string | null;
|
|
1034
|
-
/** Null for chain-
|
|
1034
|
+
/** Null for chain-webhook rows. */
|
|
1035
1035
|
table_name: string | null;
|
|
1036
1036
|
block_height: number | bigint;
|
|
1037
1037
|
tx_id: string | null;
|
|
@@ -1049,15 +1049,15 @@ interface SubscriptionOutboxTable {
|
|
|
1049
1049
|
locked_until: Date | null;
|
|
1050
1050
|
created_at: Generated<Date>;
|
|
1051
1051
|
}
|
|
1052
|
-
type
|
|
1053
|
-
type
|
|
1054
|
-
type
|
|
1055
|
-
interface
|
|
1052
|
+
type WebhookOutbox = Selectable<WebhookOutboxTable>;
|
|
1053
|
+
type InsertWebhookOutbox = Insertable<WebhookOutboxTable>;
|
|
1054
|
+
type UpdateWebhookOutbox = Updateable<WebhookOutboxTable>;
|
|
1055
|
+
interface WebhookDeliveriesTable {
|
|
1056
1056
|
id: Generated<string>;
|
|
1057
1057
|
/** Nullable after migration 0077 — outbox row may be cleaned up while
|
|
1058
1058
|
* delivery telemetry is retained. */
|
|
1059
1059
|
outbox_id: string | null;
|
|
1060
|
-
|
|
1060
|
+
webhook_id: string;
|
|
1061
1061
|
attempt: number;
|
|
1062
1062
|
status_code: number | null;
|
|
1063
1063
|
response_headers: unknown | null;
|
|
@@ -1066,10 +1066,10 @@ interface SubscriptionDeliveriesTable {
|
|
|
1066
1066
|
duration_ms: number | null;
|
|
1067
1067
|
dispatched_at: Generated<Date>;
|
|
1068
1068
|
}
|
|
1069
|
-
type
|
|
1070
|
-
type
|
|
1069
|
+
type WebhookDelivery = Selectable<WebhookDeliveriesTable>;
|
|
1070
|
+
type InsertWebhookDelivery = Insertable<WebhookDeliveriesTable>;
|
|
1071
1071
|
/** Single-row (id always TRUE) high-water mark for the chain-trigger evaluator.
|
|
1072
|
-
* One loop serves all chain
|
|
1072
|
+
* One loop serves all chain webhooks, so the cursor is global. */
|
|
1073
1073
|
interface TriggerEvaluatorStateTable {
|
|
1074
1074
|
id: Generated<boolean>;
|
|
1075
1075
|
last_processed_block: ColumnType<bigint, bigint | number | undefined, bigint | number>;
|
|
@@ -2007,9 +2007,9 @@ declare const TABLE_TO_DB: {
|
|
|
2007
2007
|
play_provisions: string
|
|
2008
2008
|
account_spend_caps: string
|
|
2009
2009
|
processed_stripe_events: string
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2010
|
+
webhooks: string
|
|
2011
|
+
webhook_outbox: string
|
|
2012
|
+
webhook_deliveries: string
|
|
2013
2013
|
trigger_evaluator_state: string
|
|
2014
2014
|
subgraphs: string
|
|
2015
2015
|
subgraph_operations: string
|
|
@@ -2094,4 +2094,4 @@ declare function getRawClient(role?: "source" | "target"): ReturnType<typeof pos
|
|
|
2094
2094
|
declare function getRawClientFor(url: string): ReturnType<typeof postgres>;
|
|
2095
2095
|
/** Close all DB connection pools. Call in CLI commands to allow process exit. */
|
|
2096
2096
|
declare function closeDb(): Promise<void>;
|
|
2097
|
-
export { sql, setMigrationRole, parseJsonb, onControlPlane, onChainPlane, jsonb, getTargetDb, getSourceDb, getRawClientFor, getRawClient, getMigrationRole, getDbSplitStatus, getDb, closeDb, assertDbSplit, applicationDatabaseUrl, X402PaymentsTable, X402BalancesTable,
|
|
2097
|
+
export { sql, setMigrationRole, parseJsonb, onControlPlane, onChainPlane, jsonb, getTargetDb, getSourceDb, getRawClientFor, getRawClient, getMigrationRole, getDbSplitStatus, getDb, closeDb, assertDbSplit, applicationDatabaseUrl, X402PaymentsTable, X402BalancesTable, WebhooksTable, WebhookStatus, WebhookRuntime, WebhookOutboxTable, WebhookOutbox, WebhookKind, WebhookFormat, WebhookDelivery, WebhookDeliveriesTable, Webhook, UpdateWebhookOutbox, UpdateWebhook, UpdateTransaction, UpdateSubgraphOperation, UpdateSubgraph, UpdateIndexProgress, UpdateHostedMeterDay, UpdateEvent, UpdateContract, UpdateClaimToken, UpdateBlock, UpdateArchiveFetch, UpdateApiKey, UpdateAccountSpendCap, UpdateAccountCredits, TriggerEvaluatorStateTable, TriggerEvaluatorState, TransactionsTable, TransactionsArchiveTable, Transaction, TABLE_TO_DB, SyncScopesTable, SubgraphsTable, SubgraphViolationsTable, SubgraphTableSnapshotsTable, SubgraphProcessingStatsTable, SubgraphOperationsTable, SubgraphOperationStatus, SubgraphOperationKind, SubgraphOperation, SubgraphHealthSnapshotsTable, SubgraphHealthSnapshot, SubgraphGapsTable, SubgraphGap, Subgraph, StageRunsTable, StageRegistryTable, StageKind, StageFailuresTable, StageBlockReceiptsTable, SessionsTable, Session, ServiceHeartbeatsTable, SbtcTokenEventsTable, SbtcTokenEventType, SbtcSupplySnapshotsTable, SbtcSettlementsTable, SbtcEventsTable, SbtcEventTopic, SOURCE_READ_TYPES, SOURCE_READ_PKS, SOURCE_READ_COLUMNS, RunStatus, RetryState, RepairMode, ProcessedStripeEventsTable, Pox5EventsTable, Pox5EventTopic, Pox4SignersDailyTable, Pox4FunctionName, Pox4CyclesDailyTable, Pox4CallsTable, PlayProvisionsTable, PlayProvision, PendingForkBlocksTable, OutboxStatus, ObserverJournalTable, ObserverJournalStatus, NumericAsText, NativeClock, MigrationRole, MempoolTransactionsTable, MempoolTransaction, MagicLinksTable, MagicLink, InstancesTable, InsertWebhookOutbox, InsertWebhookDelivery, InsertWebhook, InsertTransaction, InsertSubgraphOperation, InsertSubgraphHealthSnapshot, InsertSubgraphGap, InsertSubgraph, InsertSession, InsertPlayProvision, InsertMempoolTransaction, InsertMagicLink, InsertIndexProgress, InsertHostedMeterDay, InsertEvent, InsertContract, InsertClaimToken, InsertBlock, InsertArchiveFetch, InsertApiKey, InsertAccountSpendCap, InsertAccountCredits, InsertAccount, IndexProgressTable, IndexProgress, IndexPrimaryKey, IndexColumnType, IndexColumn, HostedMeterDaysTable, HostedMeterDay, FailureUnit, FailureClass, EventsTable, EventsArchiveTable, Event, DecoderCheckpointsTable, DecodedEventsTable, DeadLetterEventsTable, DbSplitStatus, DbReadRow, DbPlane, Database, CoverageSegmentsTable, ContractsTable, Contract, ClaimTokensTable, ClaimToken, ChainReorgsTable, ChainReadCacheTable, BurnBlockRewardsTable, BurnBlockRewardSlotsTable, BnsNamespacesTable, BnsNamespaceEventsTable, BnsNamespaceEventStatus, BnsNamesTable, BnsNameEventsTable, BnsNameEventTopic, BnsMarketplaceEventsTable, BnsMarketplaceAction, BlocksTable, Block, ArchiveFetchesTable, ArchiveFetch, ApiKeysTable, ApiKey, AccountsTable, AccountSpendCapsTable, AccountSpendCap, AccountCreditsTable, AccountCredits, Account };
|
package/dist/src/db/index.js
CHANGED
|
@@ -779,9 +779,9 @@ var TABLE_TO_DB = {
|
|
|
779
779
|
play_provisions: "target",
|
|
780
780
|
account_spend_caps: "target",
|
|
781
781
|
processed_stripe_events: "target",
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
782
|
+
webhooks: "target",
|
|
783
|
+
webhook_outbox: "target",
|
|
784
|
+
webhook_deliveries: "target",
|
|
785
785
|
trigger_evaluator_state: "target",
|
|
786
786
|
subgraphs: "target",
|
|
787
787
|
subgraph_operations: "target",
|
|
@@ -997,5 +997,5 @@ export {
|
|
|
997
997
|
SOURCE_READ_COLUMNS
|
|
998
998
|
};
|
|
999
999
|
|
|
1000
|
-
//# debugId=
|
|
1000
|
+
//# debugId=69352246F7F739CA64756E2164756E21
|
|
1001
1001
|
//# sourceMappingURL=index.js.map
|