@secondlayer/shared 13.11.0 → 13.13.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.
Files changed (41) hide show
  1. package/dist/src/archive/range-digest.d.ts +125 -0
  2. package/dist/src/archive/semantic-digest-builder.d.ts +125 -0
  3. package/dist/src/coverage/index.d.ts +125 -0
  4. package/dist/src/db/index.d.ts +146 -2
  5. package/dist/src/db/index.js +11 -1
  6. package/dist/src/db/index.js.map +3 -3
  7. package/dist/src/db/queries/chain-reorgs.d.ts +125 -0
  8. package/dist/src/db/queries/chain-reorgs.js +11 -1
  9. package/dist/src/db/queries/chain-reorgs.js.map +3 -3
  10. package/dist/src/db/queries/contracts.d.ts +125 -0
  11. package/dist/src/db/queries/instance.d.ts +125 -0
  12. package/dist/src/db/queries/integrity.d.ts +125 -0
  13. package/dist/src/db/queries/subgraph-gaps.d.ts +125 -0
  14. package/dist/src/db/queries/subgraph-operations.d.ts +125 -0
  15. package/dist/src/db/queries/subgraphs.d.ts +125 -0
  16. package/dist/src/db/queries/sync-scope.d.ts +125 -0
  17. package/dist/src/db/queries/webhook-replays.d.ts +1431 -0
  18. package/dist/src/db/queries/webhook-replays.js +156 -0
  19. package/dist/src/db/queries/webhook-replays.js.map +10 -0
  20. package/dist/src/db/queries/webhooks.d.ts +125 -0
  21. package/dist/src/db/schema.d.ts +136 -2
  22. package/dist/src/index.d.ts +146 -2
  23. package/dist/src/index.js +15 -1
  24. package/dist/src/index.js.map +4 -4
  25. package/dist/src/insights/index.d.ts +142 -3
  26. package/dist/src/insights/index.js +40 -3
  27. package/dist/src/insights/index.js.map +3 -3
  28. package/dist/src/node/local-client.d.ts +125 -0
  29. package/dist/src/queue/listener.js +11 -1
  30. package/dist/src/queue/listener.js.map +3 -3
  31. package/dist/src/schemas/index.js +5 -1
  32. package/dist/src/schemas/index.js.map +3 -3
  33. package/dist/src/schemas/webhooks.d.ts +45 -1
  34. package/dist/src/schemas/webhooks.js +6 -1
  35. package/dist/src/schemas/webhooks.js.map +3 -3
  36. package/migrations/0177_oauth_mcp.ts +122 -0
  37. package/migrations/0178_insight_snooze.ts +22 -0
  38. package/migrations/0179_webhook_replays.ts +56 -0
  39. package/migrations/0180_push.ts +54 -0
  40. package/migrations/0181_push_sent.ts +30 -0
  41. package/package.json +5 -1
@@ -0,0 +1,156 @@
1
+ // src/db/queries/webhook-replays.ts
2
+ import { sql } from "kysely";
3
+ var STALE_SCAN_MINUTES = 30;
4
+
5
+ class ReplayInProgressError extends Error {
6
+ constructor() {
7
+ super("replay already in progress for this webhook");
8
+ this.name = "ReplayInProgressError";
9
+ }
10
+ }
11
+ var ONE_SCANNING_CONSTRAINT = "webhook_replays_one_scanning";
12
+ function isOneScanningConflict(err) {
13
+ if (!(err instanceof Error))
14
+ return false;
15
+ const candidate = err;
16
+ return candidate.code === "23505" && (candidate.constraint === ONE_SCANNING_CONSTRAINT || candidate.constraint_name === ONE_SCANNING_CONSTRAINT);
17
+ }
18
+ async function beginReplay(db, input) {
19
+ try {
20
+ await db.transaction().execute(async (trx) => {
21
+ await trx.updateTable("webhook_replays").set({
22
+ status: "failed",
23
+ error: "scan interrupted",
24
+ finished_at: sql`now()`
25
+ }).where("webhook_id", "=", input.webhookId).where("status", "=", "scanning").where("started_at", "<", sql`now() - ${sql.lit(`${STALE_SCAN_MINUTES} minutes`)}::interval`).execute();
26
+ const row = await trx.insertInto("webhook_replays").values({
27
+ id: input.id,
28
+ webhook_id: input.webhookId,
29
+ from_block: input.fromBlock,
30
+ to_block: input.toBlock,
31
+ status: "scanning"
32
+ }).onConflict((oc) => oc.column("id").doUpdateSet({
33
+ status: "scanning",
34
+ started_at: sql`now()`,
35
+ finished_at: null,
36
+ error: null,
37
+ total: null,
38
+ delivered: null,
39
+ gave_up: null
40
+ }).where("webhook_replays.status", "<>", "scanning")).returning("id").executeTakeFirst();
41
+ if (!row)
42
+ throw new ReplayInProgressError;
43
+ });
44
+ } catch (err) {
45
+ if (isOneScanningConflict(err))
46
+ throw new ReplayInProgressError;
47
+ throw err;
48
+ }
49
+ }
50
+ async function finishScan(db, id, counts) {
51
+ await db.updateTable("webhook_replays").set({
52
+ status: "running",
53
+ scanned_count: counts.scanned,
54
+ enqueued_count: sql`enqueued_count + ${counts.enqueued}`
55
+ }).where("id", "=", id).execute();
56
+ }
57
+ async function failScan(db, id, error) {
58
+ await db.updateTable("webhook_replays").set({ status: "failed", error, finished_at: sql`now()` }).where("id", "=", id).execute();
59
+ }
60
+ async function pendingCount(db, id) {
61
+ const row = await db.selectFrom("webhook_outbox").select(sql`count(*)`.as("n")).where("status", "=", "pending").where("replay_id", "=", id).executeTakeFirstOrThrow();
62
+ return Number(row.n);
63
+ }
64
+ async function deadCount(db, webhookId, id) {
65
+ const row = await db.selectFrom("webhook_outbox").select(sql`count(*)`.as("n")).where("webhook_id", "=", webhookId).where("replay_id", "=", id).where("status", "=", "dead").executeTakeFirstOrThrow();
66
+ return Number(row.n);
67
+ }
68
+ function progressOf(record, pending) {
69
+ const done = record.status === "done";
70
+ const total = done ? record.total ?? record.enqueued_count : record.enqueued_count;
71
+ return {
72
+ replayId: record.id,
73
+ webhookId: record.webhook_id,
74
+ status: record.status,
75
+ fromBlock: Number(record.from_block),
76
+ toBlock: Number(record.to_block),
77
+ total,
78
+ processed: done ? total : Math.max(0, total - pending),
79
+ pending: done ? 0 : pending,
80
+ delivered: done ? record.delivered : null,
81
+ gaveUp: done ? record.gave_up : null,
82
+ startedAt: record.started_at.toISOString(),
83
+ finishedAt: record.finished_at ? record.finished_at.toISOString() : null,
84
+ error: record.error
85
+ };
86
+ }
87
+ async function progressWithFinalize(db, record) {
88
+ if (record.status !== "running")
89
+ return progressOf(record, 0);
90
+ const pending = await pendingCount(db, record.id);
91
+ if (pending > 0)
92
+ return progressOf(record, pending);
93
+ const dead = await deadCount(db, record.webhook_id, record.id);
94
+ const finalized = await db.updateTable("webhook_replays").set({
95
+ status: "done",
96
+ finished_at: sql`now()`,
97
+ total: sql`enqueued_count`,
98
+ gave_up: dead,
99
+ delivered: sql`enqueued_count - ${dead}`
100
+ }).where("id", "=", record.id).where("status", "=", "running").returningAll().executeTakeFirst();
101
+ if (finalized)
102
+ return progressOf(finalized, 0);
103
+ const fresh = await db.selectFrom("webhook_replays").selectAll().where("id", "=", record.id).executeTakeFirstOrThrow();
104
+ return progressOf(fresh, fresh.status === "running" ? await pendingCount(db, fresh.id) : 0);
105
+ }
106
+ async function readReplay(db, webhookId, id) {
107
+ const record = await db.selectFrom("webhook_replays").selectAll().where("id", "=", id).where("webhook_id", "=", webhookId).executeTakeFirst();
108
+ return record ? progressWithFinalize(db, record) : null;
109
+ }
110
+ async function listReplays(db, input) {
111
+ if (input.webhookIds.length === 0)
112
+ return [];
113
+ const cutoff = new Date(Date.now() - input.sinceHours * 3600000);
114
+ const records = await db.selectFrom("webhook_replays").selectAll().where("webhook_id", "in", input.webhookIds).where((eb) => eb.or([
115
+ eb("status", "in", ["scanning", "running"]),
116
+ eb("started_at", ">", cutoff)
117
+ ])).orderBy("started_at", "desc").limit(input.limit).execute();
118
+ return Promise.all(records.map((r) => progressWithFinalize(db, r)));
119
+ }
120
+ async function listWebhookReplays(db, webhookId, limit) {
121
+ const records = await db.selectFrom("webhook_replays").selectAll().where("webhook_id", "=", webhookId).orderBy("started_at", "desc").limit(limit).execute();
122
+ return Promise.all(records.map((r) => progressWithFinalize(db, r)));
123
+ }
124
+ async function finalizeReplays(db) {
125
+ const running = await db.selectFrom("webhook_replays").selectAll().where("status", "=", "running").execute();
126
+ for (const record of running)
127
+ await progressWithFinalize(db, record);
128
+ }
129
+ async function deadSince(db, webhookId, since) {
130
+ let q = db.selectFrom("webhook_outbox").select(sql`count(*)`.as("n")).where("webhook_id", "=", webhookId).where("status", "=", "dead");
131
+ if (since)
132
+ q = q.where("failed_at", ">=", since);
133
+ return Number((await q.executeTakeFirstOrThrow()).n);
134
+ }
135
+ async function knownEventsInRange(db, webhookId, from, to) {
136
+ const row = await db.selectFrom("webhook_outbox").select(sql`count(*)`.as("n")).where("webhook_id", "=", webhookId).where("block_height", ">=", from).where("block_height", "<=", to).executeTakeFirstOrThrow();
137
+ return Number(row.n);
138
+ }
139
+ export {
140
+ ReplayInProgressError,
141
+ beginReplay,
142
+ deadCount,
143
+ deadSince,
144
+ failScan,
145
+ finalizeReplays,
146
+ finishScan,
147
+ knownEventsInRange,
148
+ listReplays,
149
+ listWebhookReplays,
150
+ pendingCount,
151
+ progressOf,
152
+ readReplay
153
+ };
154
+
155
+ //# debugId=E8EBA311948ADE2B64756E2164756E21
156
+ //# sourceMappingURL=webhook-replays.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/db/queries/webhook-replays.ts"],
4
+ "sourcesContent": [
5
+ "import { type Kysely, sql } from \"kysely\";\nimport type { ReplayProgress } from \"../../schemas/webhooks.ts\";\nimport type { Database, WebhookReplay } from \"../types.ts\";\n\n/**\n * Stored record of each webhook replay. A replay is `scanning` while the\n * request scans its block range, `running` once its outbox rows exist, `done`\n * when none are pending, `failed` if the scan threw or the process died.\n *\n * Progress while running is `enqueued - pending`, where pending is counted\n * through the existing partial index on pending outbox rows (`webhook_outbox`\n * has no index on `replay_id` on purpose). Rows that gave up are counted once,\n * when the replay finalizes.\n */\n\n/** A `scanning` row older than this is a crashed process, not a live scan. */\nconst STALE_SCAN_MINUTES = 30;\n\n/** Thrown by {@link beginReplay} when a replay is still scanning for the\n * webhook. The route maps this to 409; `force` does not bypass it. */\nexport class ReplayInProgressError extends Error {\n\tconstructor() {\n\t\tsuper(\"replay already in progress for this webhook\");\n\t\tthis.name = \"ReplayInProgressError\";\n\t}\n}\n\nconst ONE_SCANNING_CONSTRAINT = \"webhook_replays_one_scanning\";\n\nfunction isOneScanningConflict(err: unknown): boolean {\n\tif (!(err instanceof Error)) return false;\n\tconst candidate = err as Error & {\n\t\tcode?: string;\n\t\tconstraint?: string;\n\t\tconstraint_name?: string;\n\t};\n\treturn (\n\t\tcandidate.code === \"23505\" &&\n\t\t(candidate.constraint === ONE_SCANNING_CONSTRAINT ||\n\t\t\tcandidate.constraint_name === ONE_SCANNING_CONSTRAINT)\n\t);\n}\n\nexport async function beginReplay(\n\tdb: Kysely<Database>,\n\tinput: {\n\t\tid: string;\n\t\twebhookId: string;\n\t\tfromBlock: number;\n\t\ttoBlock: number;\n\t},\n): Promise<void> {\n\ttry {\n\t\tawait db.transaction().execute(async (trx) => {\n\t\t\tawait trx\n\t\t\t\t.updateTable(\"webhook_replays\")\n\t\t\t\t.set({\n\t\t\t\t\tstatus: \"failed\",\n\t\t\t\t\terror: \"scan interrupted\",\n\t\t\t\t\tfinished_at: sql`now()`,\n\t\t\t\t})\n\t\t\t\t.where(\"webhook_id\", \"=\", input.webhookId)\n\t\t\t\t.where(\"status\", \"=\", \"scanning\")\n\t\t\t\t.where(\n\t\t\t\t\t\"started_at\",\n\t\t\t\t\t\"<\",\n\t\t\t\t\tsql<Date>`now() - ${sql.lit(`${STALE_SCAN_MINUTES} minutes`)}::interval`,\n\t\t\t\t)\n\t\t\t\t.execute();\n\t\t\t// Re-running a range reuses its record. A row of this very id that is\n\t\t\t// still scanning is a concurrent scan: the WHERE leaves it untouched and\n\t\t\t// no row comes back.\n\t\t\tconst row = await trx\n\t\t\t\t.insertInto(\"webhook_replays\")\n\t\t\t\t.values({\n\t\t\t\t\tid: input.id,\n\t\t\t\t\twebhook_id: input.webhookId,\n\t\t\t\t\tfrom_block: input.fromBlock,\n\t\t\t\t\tto_block: input.toBlock,\n\t\t\t\t\tstatus: \"scanning\",\n\t\t\t\t})\n\t\t\t\t.onConflict((oc) =>\n\t\t\t\t\toc\n\t\t\t\t\t\t.column(\"id\")\n\t\t\t\t\t\t.doUpdateSet({\n\t\t\t\t\t\t\tstatus: \"scanning\",\n\t\t\t\t\t\t\tstarted_at: sql`now()`,\n\t\t\t\t\t\t\tfinished_at: null,\n\t\t\t\t\t\t\terror: null,\n\t\t\t\t\t\t\ttotal: null,\n\t\t\t\t\t\t\tdelivered: null,\n\t\t\t\t\t\t\tgave_up: null,\n\t\t\t\t\t\t})\n\t\t\t\t\t\t.where(\"webhook_replays.status\", \"<>\", \"scanning\"),\n\t\t\t\t)\n\t\t\t\t.returning(\"id\")\n\t\t\t\t.executeTakeFirst();\n\t\t\tif (!row) throw new ReplayInProgressError();\n\t\t});\n\t} catch (err) {\n\t\tif (isOneScanningConflict(err)) throw new ReplayInProgressError();\n\t\tthrow err;\n\t}\n}\n\nexport async function finishScan(\n\tdb: Kysely<Database>,\n\tid: string,\n\tcounts: { scanned: number; enqueued: number },\n): Promise<void> {\n\tawait db\n\t\t.updateTable(\"webhook_replays\")\n\t\t.set({\n\t\t\tstatus: \"running\",\n\t\t\tscanned_count: counts.scanned,\n\t\t\tenqueued_count: sql`enqueued_count + ${counts.enqueued}`,\n\t\t})\n\t\t.where(\"id\", \"=\", id)\n\t\t.execute();\n}\n\nexport async function failScan(\n\tdb: Kysely<Database>,\n\tid: string,\n\terror: string,\n): Promise<void> {\n\tawait db\n\t\t.updateTable(\"webhook_replays\")\n\t\t.set({ status: \"failed\", error, finished_at: sql`now()` })\n\t\t.where(\"id\", \"=\", id)\n\t\t.execute();\n}\n\n/** Outbox rows of this replay still waiting to be delivered or to give up. */\nexport async function pendingCount(\n\tdb: Kysely<Database>,\n\tid: string,\n): Promise<number> {\n\tconst row = await db\n\t\t.selectFrom(\"webhook_outbox\")\n\t\t.select(sql<string>`count(*)`.as(\"n\"))\n\t\t.where(\"status\", \"=\", \"pending\")\n\t\t.where(\"replay_id\", \"=\", id)\n\t\t.executeTakeFirstOrThrow();\n\treturn Number(row.n);\n}\n\n/** Outbox rows of this replay that gave up. Finalize only; never on a poll. */\nexport async function deadCount(\n\tdb: Kysely<Database>,\n\twebhookId: string,\n\tid: string,\n): Promise<number> {\n\tconst row = await db\n\t\t.selectFrom(\"webhook_outbox\")\n\t\t.select(sql<string>`count(*)`.as(\"n\"))\n\t\t.where(\"webhook_id\", \"=\", webhookId)\n\t\t.where(\"replay_id\", \"=\", id)\n\t\t.where(\"status\", \"=\", \"dead\")\n\t\t.executeTakeFirstOrThrow();\n\treturn Number(row.n);\n}\n\nexport function progressOf(\n\trecord: WebhookReplay,\n\tpending: number,\n): ReplayProgress {\n\tconst done = record.status === \"done\";\n\tconst total = done\n\t\t? (record.total ?? record.enqueued_count)\n\t\t: record.enqueued_count;\n\treturn {\n\t\treplayId: record.id,\n\t\twebhookId: record.webhook_id,\n\t\tstatus: record.status,\n\t\tfromBlock: Number(record.from_block),\n\t\ttoBlock: Number(record.to_block),\n\t\ttotal,\n\t\tprocessed: done ? total : Math.max(0, total - pending),\n\t\tpending: done ? 0 : pending,\n\t\tdelivered: done ? record.delivered : null,\n\t\tgaveUp: done ? record.gave_up : null,\n\t\tstartedAt: record.started_at.toISOString(),\n\t\tfinishedAt: record.finished_at ? record.finished_at.toISOString() : null,\n\t\terror: record.error,\n\t};\n}\n\n/** Progress for one record, finalizing it once nothing is pending. */\nasync function progressWithFinalize(\n\tdb: Kysely<Database>,\n\trecord: WebhookReplay,\n): Promise<ReplayProgress> {\n\tif (record.status !== \"running\") return progressOf(record, 0);\n\tconst pending = await pendingCount(db, record.id);\n\tif (pending > 0) return progressOf(record, pending);\n\n\tconst dead = await deadCount(db, record.webhook_id, record.id);\n\tconst finalized = await db\n\t\t.updateTable(\"webhook_replays\")\n\t\t.set({\n\t\t\tstatus: \"done\",\n\t\t\tfinished_at: sql`now()`,\n\t\t\ttotal: sql`enqueued_count`,\n\t\t\tgave_up: dead,\n\t\t\tdelivered: sql`enqueued_count - ${dead}`,\n\t\t})\n\t\t.where(\"id\", \"=\", record.id)\n\t\t.where(\"status\", \"=\", \"running\")\n\t\t.returningAll()\n\t\t.executeTakeFirst();\n\tif (finalized) return progressOf(finalized, 0);\n\t// Lost a race to another finalizer (or a requeue): re-read.\n\tconst fresh = await db\n\t\t.selectFrom(\"webhook_replays\")\n\t\t.selectAll()\n\t\t.where(\"id\", \"=\", record.id)\n\t\t.executeTakeFirstOrThrow();\n\treturn progressOf(\n\t\tfresh,\n\t\tfresh.status === \"running\" ? await pendingCount(db, fresh.id) : 0,\n\t);\n}\n\nexport async function readReplay(\n\tdb: Kysely<Database>,\n\twebhookId: string,\n\tid: string,\n): Promise<ReplayProgress | null> {\n\tconst record = await db\n\t\t.selectFrom(\"webhook_replays\")\n\t\t.selectAll()\n\t\t.where(\"id\", \"=\", id)\n\t\t.where(\"webhook_id\", \"=\", webhookId)\n\t\t.executeTakeFirst();\n\treturn record ? progressWithFinalize(db, record) : null;\n}\n\n/** Replays in progress or started within `sinceHours`, newest first. */\nexport async function listReplays(\n\tdb: Kysely<Database>,\n\tinput: { webhookIds: string[]; sinceHours: number; limit: number },\n): Promise<ReplayProgress[]> {\n\tif (input.webhookIds.length === 0) return [];\n\tconst cutoff = new Date(Date.now() - input.sinceHours * 3_600_000);\n\tconst records = await db\n\t\t.selectFrom(\"webhook_replays\")\n\t\t.selectAll()\n\t\t.where(\"webhook_id\", \"in\", input.webhookIds)\n\t\t.where((eb) =>\n\t\t\teb.or([\n\t\t\t\teb(\"status\", \"in\", [\"scanning\", \"running\"]),\n\t\t\t\teb(\"started_at\", \">\", cutoff),\n\t\t\t]),\n\t\t)\n\t\t.orderBy(\"started_at\", \"desc\")\n\t\t.limit(input.limit)\n\t\t.execute();\n\treturn Promise.all(records.map((r) => progressWithFinalize(db, r)));\n}\n\n/** Newest replays of one webhook, finalizing any that have drained. */\nexport async function listWebhookReplays(\n\tdb: Kysely<Database>,\n\twebhookId: string,\n\tlimit: number,\n): Promise<ReplayProgress[]> {\n\tconst records = await db\n\t\t.selectFrom(\"webhook_replays\")\n\t\t.selectAll()\n\t\t.where(\"webhook_id\", \"=\", webhookId)\n\t\t.orderBy(\"started_at\", \"desc\")\n\t\t.limit(limit)\n\t\t.execute();\n\treturn Promise.all(records.map((r) => progressWithFinalize(db, r)));\n}\n\n/** Retention backstop: finalize every running replay that has drained. */\nexport async function finalizeReplays(db: Kysely<Database>): Promise<void> {\n\tconst running = await db\n\t\t.selectFrom(\"webhook_replays\")\n\t\t.selectAll()\n\t\t.where(\"status\", \"=\", \"running\")\n\t\t.execute();\n\tfor (const record of running) await progressWithFinalize(db, record);\n}\n\nexport async function deadSince(\n\tdb: Kysely<Database>,\n\twebhookId: string,\n\tsince: Date | null,\n): Promise<number> {\n\tlet q = db\n\t\t.selectFrom(\"webhook_outbox\")\n\t\t.select(sql<string>`count(*)`.as(\"n\"))\n\t\t.where(\"webhook_id\", \"=\", webhookId)\n\t\t.where(\"status\", \"=\", \"dead\");\n\tif (since) q = q.where(\"failed_at\", \">=\", since);\n\treturn Number((await q.executeTakeFirstOrThrow()).n);\n}\n\nexport async function knownEventsInRange(\n\tdb: Kysely<Database>,\n\twebhookId: string,\n\tfrom: number,\n\tto: number,\n): Promise<number> {\n\tconst row = await db\n\t\t.selectFrom(\"webhook_outbox\")\n\t\t.select(sql<string>`count(*)`.as(\"n\"))\n\t\t.where(\"webhook_id\", \"=\", webhookId)\n\t\t.where(\"block_height\", \">=\", from)\n\t\t.where(\"block_height\", \"<=\", to)\n\t\t.executeTakeFirstOrThrow();\n\treturn Number(row.n);\n}\n"
6
+ ],
7
+ "mappings": "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",
8
+ "debugId": "E8EBA311948ADE2B64756E2164756E21",
9
+ "names": []
10
+ }
@@ -402,6 +402,7 @@ interface InsightsTable {
402
402
  clear_streak: Generated<number>;
403
403
  resolved_at: Date | null;
404
404
  dismissed_at: Date | null;
405
+ snoozed_until: Date | null;
405
406
  }
406
407
  /** "Expected": mutes one rule for one resource until removed. */
407
408
  interface InsightMutesTable {
@@ -895,6 +896,7 @@ interface Database {
895
896
  webhooks: WebhooksTable;
896
897
  webhook_outbox: WebhookOutboxTable;
897
898
  webhook_deliveries: WebhookDeliveriesTable;
899
+ webhook_replays: WebhookReplaysTable;
898
900
  trigger_evaluator_state: TriggerEvaluatorStateTable;
899
901
  decoded_events: DecodedEventsTable;
900
902
  decoder_checkpoints: DecoderCheckpointsTable;
@@ -928,6 +930,12 @@ interface Database {
928
930
  account_balance_alerts: AccountBalanceAlertsTable;
929
931
  webhook_incident_alerts: WebhookIncidentAlertsTable;
930
932
  cli_device_logins: CliDeviceLoginsTable;
933
+ oauth_clients: OauthClientsTable;
934
+ oauth_requests: OauthRequestsTable;
935
+ oauth_codes: OauthCodesTable;
936
+ oauth_grants: OauthGrantsTable;
937
+ oauth_tokens: OauthTokensTable;
938
+ oauth_refresh_retired: OauthRefreshRetiredTable;
931
939
  sentinel_accounts: SentinelAccountsTable;
932
940
  waitlists: WaitlistsTable;
933
941
  waitlist_signups: WaitlistSignupsTable;
@@ -937,6 +945,9 @@ interface Database {
937
945
  insights: InsightsTable;
938
946
  insight_mutes: InsightMutesTable;
939
947
  feedback_tickets: FeedbackTicketsTable;
948
+ push_subscriptions: PushSubscriptionsTable;
949
+ push_prefs: PushPrefsTable;
950
+ push_sent: PushSentTable;
940
951
  }
941
952
  interface ServiceHeartbeatsTable {
942
953
  name: string;
@@ -1029,6 +1040,36 @@ interface WebhookIncidentAlertsTable {
1029
1040
  email_attempts: Generated<number>;
1030
1041
  resolved_at: Date | null;
1031
1042
  }
1043
+ /** One browser/device push subscription; the endpoint is unique. */
1044
+ interface PushSubscriptionsTable {
1045
+ id: Generated<string>;
1046
+ account_id: string;
1047
+ endpoint: string;
1048
+ p256dh: string;
1049
+ auth: string;
1050
+ user_agent: string | null;
1051
+ created_at: Generated<Date>;
1052
+ last_success_at: Date | null;
1053
+ failed_count: Generated<number>;
1054
+ }
1055
+ /** Per-account push toggles. A missing row means the column defaults. */
1056
+ interface PushPrefsTable {
1057
+ account_id: string;
1058
+ webhook_failing: Generated<boolean>;
1059
+ subgraph_stopped: Generated<boolean>;
1060
+ subgraph_not_serving: Generated<boolean>;
1061
+ replay_done: Generated<boolean>;
1062
+ spend_cap: Generated<boolean>;
1063
+ key_repeat: Generated<boolean>;
1064
+ updated_at: Generated<Date>;
1065
+ }
1066
+ /** A push already sent, keyed by what it was about; debounces repeat signals. */
1067
+ interface PushSentTable {
1068
+ account_id: string;
1069
+ key: string;
1070
+ kind: string;
1071
+ sent_at: Generated<Date>;
1072
+ }
1032
1073
  type CliDeviceLoginStatus = "pending" | "approved" | "denied" | "consumed";
1033
1074
  /** The product that started a CLI sign-in; null is `sl login`. */
1034
1075
  type CliDeviceProduct = "sentinel";
@@ -1049,6 +1090,72 @@ interface CliDeviceLoginsTable {
1049
1090
  approved_at: Date | null;
1050
1091
  consumed_at: Date | null;
1051
1092
  }
1093
+ /** An app that registered itself (RFC 7591). Public clients only: no secret. */
1094
+ interface OauthClientsTable {
1095
+ id: string;
1096
+ /** The app's own claim, validated at registration; shown quoted. */
1097
+ name: string;
1098
+ redirect_uris: string[];
1099
+ created_ip: string | null;
1100
+ created_at: Generated<Date>;
1101
+ /** Set when it first gets tokens; never-used clients are purged. */
1102
+ last_used_at: Date | null;
1103
+ }
1104
+ type OauthRequestStatus = "pending" | "approved" | "denied";
1105
+ /** One authorize request waiting for (or answered on) the consent page. */
1106
+ interface OauthRequestsTable {
1107
+ id: string;
1108
+ client_id: string;
1109
+ redirect_uri: string;
1110
+ state: string | null;
1111
+ code_challenge: string;
1112
+ resource: string;
1113
+ status: Generated<OauthRequestStatus>;
1114
+ account_id: string | null;
1115
+ created_at: Generated<Date>;
1116
+ expires_at: Date;
1117
+ answered_at: Date | null;
1118
+ }
1119
+ /** A one-time authorization code, bound to its client, redirect, PKCE
1120
+ * challenge, resource and account. */
1121
+ interface OauthCodesTable {
1122
+ code_hash: string;
1123
+ client_id: string;
1124
+ account_id: string;
1125
+ redirect_uri: string;
1126
+ code_challenge: string;
1127
+ resource: string;
1128
+ /** The grant its one use made, so a replay can revoke it. */
1129
+ grant_id: string | null;
1130
+ created_at: Generated<Date>;
1131
+ expires_at: Date;
1132
+ used_at: Date | null;
1133
+ }
1134
+ /** What a person allowed one app to do: use the hosted MCP as them. */
1135
+ interface OauthGrantsTable {
1136
+ id: Generated<string>;
1137
+ account_id: string;
1138
+ client_id: string;
1139
+ scope: Generated<string>;
1140
+ resource: string;
1141
+ created_at: Generated<Date>;
1142
+ last_used_at: Date | null;
1143
+ revoked_at: Date | null;
1144
+ }
1145
+ type OauthTokenKind = "access" | "refresh";
1146
+ interface OauthTokensTable {
1147
+ token_hash: string;
1148
+ grant_id: string;
1149
+ kind: OauthTokenKind;
1150
+ created_at: Generated<Date>;
1151
+ expires_at: Date;
1152
+ }
1153
+ /** A refresh token a rotation replaced: presented again, it revokes the grant. */
1154
+ interface OauthRefreshRetiredTable {
1155
+ token_hash: string;
1156
+ grant_id: string;
1157
+ retired_at: Generated<Date>;
1158
+ }
1052
1159
  /** Accounts Sentinel's service key may touch: created by Sentinel, or the
1053
1160
  * owner opted in. */
1054
1161
  interface SentinelAccountsTable {
@@ -1107,6 +1214,8 @@ interface WebhookOutboxTable {
1107
1214
  next_attempt_at: Generated<Date>;
1108
1215
  status: ColumnType<OutboxStatus, OutboxStatus | undefined, OutboxStatus>;
1109
1216
  is_replay: Generated<boolean>;
1217
+ /** The replay that created this row; null for live rows. */
1218
+ replay_id: string | null;
1110
1219
  /** The chain block_time of the matched event/tx. Null for rows written
1111
1220
  * before this column existed, and for the settlement scan path (not yet
1112
1221
  * wired). Lets `delivered_at - block_time` measure end-to-end webhook
@@ -1122,6 +1231,22 @@ interface WebhookOutboxTable {
1122
1231
  last_error: string | null;
1123
1232
  created_at: Generated<Date>;
1124
1233
  }
1234
+ type WebhookReplayStatus = "scanning" | "running" | "done" | "failed";
1235
+ interface WebhookReplaysTable {
1236
+ id: string;
1237
+ webhook_id: string;
1238
+ from_block: number | bigint;
1239
+ to_block: number | bigint;
1240
+ status: WebhookReplayStatus;
1241
+ scanned_count: Generated<number>;
1242
+ enqueued_count: Generated<number>;
1243
+ total: number | null;
1244
+ delivered: number | null;
1245
+ gave_up: number | null;
1246
+ error: string | null;
1247
+ started_at: Generated<Date>;
1248
+ finished_at: Date | null;
1249
+ }
1125
1250
  interface WebhookDeliveriesTable {
1126
1251
  id: Generated<string>;
1127
1252
  /** Nullable after migration 0077 — outbox row may be cleaned up while
@@ -401,6 +401,7 @@ interface InsightsTable {
401
401
  clear_streak: Generated<number>;
402
402
  resolved_at: Date | null;
403
403
  dismissed_at: Date | null;
404
+ snoozed_until: Date | null;
404
405
  }
405
406
  /** "Expected": mutes one rule for one resource until removed. */
406
407
  interface InsightMutesTable {
@@ -894,6 +895,7 @@ interface Database {
894
895
  webhooks: WebhooksTable;
895
896
  webhook_outbox: WebhookOutboxTable;
896
897
  webhook_deliveries: WebhookDeliveriesTable;
898
+ webhook_replays: WebhookReplaysTable;
897
899
  trigger_evaluator_state: TriggerEvaluatorStateTable;
898
900
  decoded_events: DecodedEventsTable;
899
901
  decoder_checkpoints: DecoderCheckpointsTable;
@@ -927,6 +929,12 @@ interface Database {
927
929
  account_balance_alerts: AccountBalanceAlertsTable;
928
930
  webhook_incident_alerts: WebhookIncidentAlertsTable;
929
931
  cli_device_logins: CliDeviceLoginsTable;
932
+ oauth_clients: OauthClientsTable;
933
+ oauth_requests: OauthRequestsTable;
934
+ oauth_codes: OauthCodesTable;
935
+ oauth_grants: OauthGrantsTable;
936
+ oauth_tokens: OauthTokensTable;
937
+ oauth_refresh_retired: OauthRefreshRetiredTable;
930
938
  sentinel_accounts: SentinelAccountsTable;
931
939
  waitlists: WaitlistsTable;
932
940
  waitlist_signups: WaitlistSignupsTable;
@@ -936,6 +944,9 @@ interface Database {
936
944
  insights: InsightsTable;
937
945
  insight_mutes: InsightMutesTable;
938
946
  feedback_tickets: FeedbackTicketsTable;
947
+ push_subscriptions: PushSubscriptionsTable;
948
+ push_prefs: PushPrefsTable;
949
+ push_sent: PushSentTable;
939
950
  }
940
951
  interface ServiceHeartbeatsTable {
941
952
  name: string;
@@ -1041,6 +1052,38 @@ interface WebhookIncidentAlertsTable {
1041
1052
  resolved_at: Date | null;
1042
1053
  }
1043
1054
  type WebhookIncidentAlert = Selectable<WebhookIncidentAlertsTable>;
1055
+ /** One browser/device push subscription; the endpoint is unique. */
1056
+ interface PushSubscriptionsTable {
1057
+ id: Generated<string>;
1058
+ account_id: string;
1059
+ endpoint: string;
1060
+ p256dh: string;
1061
+ auth: string;
1062
+ user_agent: string | null;
1063
+ created_at: Generated<Date>;
1064
+ last_success_at: Date | null;
1065
+ failed_count: Generated<number>;
1066
+ }
1067
+ type PushSubscription = Selectable<PushSubscriptionsTable>;
1068
+ /** Per-account push toggles. A missing row means the column defaults. */
1069
+ interface PushPrefsTable {
1070
+ account_id: string;
1071
+ webhook_failing: Generated<boolean>;
1072
+ subgraph_stopped: Generated<boolean>;
1073
+ subgraph_not_serving: Generated<boolean>;
1074
+ replay_done: Generated<boolean>;
1075
+ spend_cap: Generated<boolean>;
1076
+ key_repeat: Generated<boolean>;
1077
+ updated_at: Generated<Date>;
1078
+ }
1079
+ type PushPrefs = Selectable<PushPrefsTable>;
1080
+ /** A push already sent, keyed by what it was about; debounces repeat signals. */
1081
+ interface PushSentTable {
1082
+ account_id: string;
1083
+ key: string;
1084
+ kind: string;
1085
+ sent_at: Generated<Date>;
1086
+ }
1044
1087
  type CliDeviceLoginStatus = "pending" | "approved" | "denied" | "consumed";
1045
1088
  /** The product that started a CLI sign-in; null is `sl login`. */
1046
1089
  type CliDeviceProduct = "sentinel";
@@ -1062,6 +1105,74 @@ interface CliDeviceLoginsTable {
1062
1105
  consumed_at: Date | null;
1063
1106
  }
1064
1107
  type CliDeviceLogin = Selectable<CliDeviceLoginsTable>;
1108
+ /** An app that registered itself (RFC 7591). Public clients only: no secret. */
1109
+ interface OauthClientsTable {
1110
+ id: string;
1111
+ /** The app's own claim, validated at registration; shown quoted. */
1112
+ name: string;
1113
+ redirect_uris: string[];
1114
+ created_ip: string | null;
1115
+ created_at: Generated<Date>;
1116
+ /** Set when it first gets tokens; never-used clients are purged. */
1117
+ last_used_at: Date | null;
1118
+ }
1119
+ type OauthRequestStatus = "pending" | "approved" | "denied";
1120
+ /** One authorize request waiting for (or answered on) the consent page. */
1121
+ interface OauthRequestsTable {
1122
+ id: string;
1123
+ client_id: string;
1124
+ redirect_uri: string;
1125
+ state: string | null;
1126
+ code_challenge: string;
1127
+ resource: string;
1128
+ status: Generated<OauthRequestStatus>;
1129
+ account_id: string | null;
1130
+ created_at: Generated<Date>;
1131
+ expires_at: Date;
1132
+ answered_at: Date | null;
1133
+ }
1134
+ /** A one-time authorization code, bound to its client, redirect, PKCE
1135
+ * challenge, resource and account. */
1136
+ interface OauthCodesTable {
1137
+ code_hash: string;
1138
+ client_id: string;
1139
+ account_id: string;
1140
+ redirect_uri: string;
1141
+ code_challenge: string;
1142
+ resource: string;
1143
+ /** The grant its one use made, so a replay can revoke it. */
1144
+ grant_id: string | null;
1145
+ created_at: Generated<Date>;
1146
+ expires_at: Date;
1147
+ used_at: Date | null;
1148
+ }
1149
+ /** What a person allowed one app to do: use the hosted MCP as them. */
1150
+ interface OauthGrantsTable {
1151
+ id: Generated<string>;
1152
+ account_id: string;
1153
+ client_id: string;
1154
+ scope: Generated<string>;
1155
+ resource: string;
1156
+ created_at: Generated<Date>;
1157
+ last_used_at: Date | null;
1158
+ revoked_at: Date | null;
1159
+ }
1160
+ type OauthTokenKind = "access" | "refresh";
1161
+ interface OauthTokensTable {
1162
+ token_hash: string;
1163
+ grant_id: string;
1164
+ kind: OauthTokenKind;
1165
+ created_at: Generated<Date>;
1166
+ expires_at: Date;
1167
+ }
1168
+ /** A refresh token a rotation replaced: presented again, it revokes the grant. */
1169
+ interface OauthRefreshRetiredTable {
1170
+ token_hash: string;
1171
+ grant_id: string;
1172
+ retired_at: Generated<Date>;
1173
+ }
1174
+ type OauthClient = Selectable<OauthClientsTable>;
1175
+ type OauthGrant = Selectable<OauthGrantsTable>;
1065
1176
  /** Accounts Sentinel's service key may touch: created by Sentinel, or the
1066
1177
  * owner opted in. */
1067
1178
  interface SentinelAccountsTable {
@@ -1167,6 +1278,8 @@ interface WebhookOutboxTable {
1167
1278
  next_attempt_at: Generated<Date>;
1168
1279
  status: ColumnType<OutboxStatus, OutboxStatus | undefined, OutboxStatus>;
1169
1280
  is_replay: Generated<boolean>;
1281
+ /** The replay that created this row; null for live rows. */
1282
+ replay_id: string | null;
1170
1283
  /** The chain block_time of the matched event/tx. Null for rows written
1171
1284
  * before this column existed, and for the settlement scan path (not yet
1172
1285
  * wired). Lets `delivered_at - block_time` measure end-to-end webhook
@@ -1182,7 +1295,28 @@ interface WebhookOutboxTable {
1182
1295
  last_error: string | null;
1183
1296
  created_at: Generated<Date>;
1184
1297
  }
1185
- type WebhookOutbox = Selectable<WebhookOutboxTable>;
1298
+ type WebhookReplayStatus = "scanning" | "running" | "done" | "failed";
1299
+ interface WebhookReplaysTable {
1300
+ id: string;
1301
+ webhook_id: string;
1302
+ from_block: number | bigint;
1303
+ to_block: number | bigint;
1304
+ status: WebhookReplayStatus;
1305
+ scanned_count: Generated<number>;
1306
+ enqueued_count: Generated<number>;
1307
+ total: number | null;
1308
+ delivered: number | null;
1309
+ gave_up: number | null;
1310
+ error: string | null;
1311
+ started_at: Generated<Date>;
1312
+ finished_at: Date | null;
1313
+ }
1314
+ type WebhookReplay = Selectable<WebhookReplaysTable>;
1315
+ /** `replay_id` is optional on the row type so hand-built rows (the test
1316
+ * delivery, fixtures) need not carry it; reads still return it. */
1317
+ type WebhookOutbox = Omit<Selectable<WebhookOutboxTable>, "replay_id"> & {
1318
+ replay_id?: string | null
1319
+ };
1186
1320
  type InsertWebhookOutbox = Insertable<WebhookOutboxTable>;
1187
1321
  type UpdateWebhookOutbox = Updateable<WebhookOutboxTable>;
1188
1322
  interface WebhookDeliveriesTable {
@@ -1326,4 +1460,4 @@ type WaitlistSignup = Selectable<WaitlistSignupsTable>;
1326
1460
  type InsertWaitlistSignup = Insertable<WaitlistSignupsTable>;
1327
1461
  type ContactRequest = Selectable<ContactRequestsTable>;
1328
1462
  type InsertContactRequest = Insertable<ContactRequestsTable>;
1329
- export { Account, AccountBalanceAlerts, AccountBalanceAlertsTable, AccountCredits, AccountCreditsTable, AccountSpendCap, AccountSpendCapsTable, AccountsTable, ApiFailedRequest, ApiFailedRequestsTable, ApiKey, ApiKeysTable, ApiRequestLogRow, ApiRequestLogTable, ArchiveFetch, ArchiveFetchesTable, Block, BlocksTable, BnsMarketplaceAction, BnsMarketplaceEventsTable, BnsNameEventTopic, BnsNameEventsTable, BnsNamesTable, BnsNamespaceEventStatus, BnsNamespaceEventsTable, BnsNamespacesTable, BurnBlockRewardSlotsTable, BurnBlockRewardsTable, ChainReadCacheTable, ChainReorgsTable, CliDeviceLogin, CliDeviceLoginStatus, CliDeviceLoginsTable, CliDeviceProduct, ContactRequest, ContactRequestsTable, Contract, ContractsTable, CoverageSegmentsTable, Database, DeadLetterEventsTable, DecodedEventsTable, DecoderCheckpointsTable, Event, EventsArchiveTable, EventsTable, FailureClass, FailureUnit, FeedbackTicket, FeedbackTicketsTable, IndexProgress, IndexProgressTable, InsertAccount, InsertAccountBalanceAlerts, InsertAccountCredits, InsertAccountSpendCap, InsertApiFailedRequest, InsertApiKey, InsertApiRequestLogRow, InsertArchiveFetch, InsertBlock, InsertContactRequest, InsertContract, InsertEvent, InsertFeedbackTicket, InsertIndexProgress, InsertMagicLink, InsertMempoolTransaction, InsertSession, InsertStateWrite, InsertSubgraph, InsertSubgraphGap, InsertSubgraphHealthSnapshot, InsertSubgraphOperation, InsertTransaction, InsertUsageLedgerRow, InsertVmEvent, InsertWaitlistSignup, InsertWebhook, InsertWebhookDelivery, InsertWebhookOutbox, InsightMutesTable, InsightsTable, InstancesTable, MagicLink, MagicLinksTable, MempoolTransaction, MempoolTransactionsTable, NativeClock, ObserverJournalStatus, ObserverJournalTable, OutboxStatus, PendingForkBlocksTable, Pox4CallsTable, Pox4FunctionName, Pox5CycleSignersTable, Pox5CyclesTable, Pox5EventTopic, Pox5EventsTable, ProcessedStripeEventsTable, RepairMode, RetryState, RunStatus, SbtcEventTopic, SbtcEventsTable, SbtcSettlementsTable, SbtcTokenEventType, SbtcTokenEventsTable, SentinelAccount, SentinelAccountsTable, ServiceHeartbeatsTable, Session, SessionsTable, StageBlockReceiptsTable, StageFailuresTable, StageKind, StageRegistryTable, StageRunsTable, StateWrite, StateWritesArchiveTable, StateWritesTable, Subgraph, SubgraphErrorGroupsTable, SubgraphGap, SubgraphGapsTable, SubgraphHealthSnapshot, SubgraphHealthSnapshotsTable, SubgraphOperation, SubgraphOperationKind, SubgraphOperationStatus, SubgraphOperationsTable, SubgraphProcessingStatsTable, SubgraphViolationsTable, SubgraphsTable, SyncScopesTable, Transaction, TransactionsArchiveTable, TransactionsTable, TriggerEvaluatorState, TriggerEvaluatorStateTable, UpdateAccountBalanceAlerts, UpdateAccountCredits, UpdateAccountSpendCap, UpdateApiKey, UpdateArchiveFetch, UpdateBlock, UpdateContract, UpdateEvent, UpdateIndexProgress, UpdateSubgraph, UpdateSubgraphOperation, UpdateTransaction, UpdateUsageLedgerRow, UpdateVmEvent, UpdateWebhook, UpdateWebhookOutbox, UsageLedgerRow, UsageLedgerTable, VmEvent, VmEventsArchiveTable, VmEventsTable, Waitlist, WaitlistSignup, WaitlistSignupsTable, WaitlistsTable, Webhook, WebhookDeliveriesTable, WebhookDelivery, WebhookFormat, WebhookIncidentAlert, WebhookIncidentAlertsTable, WebhookKind, WebhookOutbox, WebhookOutboxTable, WebhookStatus, WebhooksTable };
1463
+ export { Account, AccountBalanceAlerts, AccountBalanceAlertsTable, AccountCredits, AccountCreditsTable, AccountSpendCap, AccountSpendCapsTable, AccountsTable, ApiFailedRequest, ApiFailedRequestsTable, ApiKey, ApiKeysTable, ApiRequestLogRow, ApiRequestLogTable, ArchiveFetch, ArchiveFetchesTable, Block, BlocksTable, BnsMarketplaceAction, BnsMarketplaceEventsTable, BnsNameEventTopic, BnsNameEventsTable, BnsNamesTable, BnsNamespaceEventStatus, BnsNamespaceEventsTable, BnsNamespacesTable, BurnBlockRewardSlotsTable, BurnBlockRewardsTable, ChainReadCacheTable, ChainReorgsTable, CliDeviceLogin, CliDeviceLoginStatus, CliDeviceLoginsTable, CliDeviceProduct, ContactRequest, ContactRequestsTable, Contract, ContractsTable, CoverageSegmentsTable, Database, DeadLetterEventsTable, DecodedEventsTable, DecoderCheckpointsTable, Event, EventsArchiveTable, EventsTable, FailureClass, FailureUnit, FeedbackTicket, FeedbackTicketsTable, IndexProgress, IndexProgressTable, InsertAccount, InsertAccountBalanceAlerts, InsertAccountCredits, InsertAccountSpendCap, InsertApiFailedRequest, InsertApiKey, InsertApiRequestLogRow, InsertArchiveFetch, InsertBlock, InsertContactRequest, InsertContract, InsertEvent, InsertFeedbackTicket, InsertIndexProgress, InsertMagicLink, InsertMempoolTransaction, InsertSession, InsertStateWrite, InsertSubgraph, InsertSubgraphGap, InsertSubgraphHealthSnapshot, InsertSubgraphOperation, InsertTransaction, InsertUsageLedgerRow, InsertVmEvent, InsertWaitlistSignup, InsertWebhook, InsertWebhookDelivery, InsertWebhookOutbox, InsightMutesTable, InsightsTable, InstancesTable, MagicLink, MagicLinksTable, MempoolTransaction, MempoolTransactionsTable, NativeClock, OauthClient, OauthClientsTable, OauthCodesTable, OauthGrant, OauthGrantsTable, OauthRefreshRetiredTable, OauthRequestStatus, OauthRequestsTable, OauthTokenKind, OauthTokensTable, ObserverJournalStatus, ObserverJournalTable, OutboxStatus, PendingForkBlocksTable, Pox4CallsTable, Pox4FunctionName, Pox5CycleSignersTable, Pox5CyclesTable, Pox5EventTopic, Pox5EventsTable, ProcessedStripeEventsTable, PushPrefs, PushPrefsTable, PushSentTable, PushSubscription, PushSubscriptionsTable, RepairMode, RetryState, RunStatus, SbtcEventTopic, SbtcEventsTable, SbtcSettlementsTable, SbtcTokenEventType, SbtcTokenEventsTable, SentinelAccount, SentinelAccountsTable, ServiceHeartbeatsTable, Session, SessionsTable, StageBlockReceiptsTable, StageFailuresTable, StageKind, StageRegistryTable, StageRunsTable, StateWrite, StateWritesArchiveTable, StateWritesTable, Subgraph, SubgraphErrorGroupsTable, SubgraphGap, SubgraphGapsTable, SubgraphHealthSnapshot, SubgraphHealthSnapshotsTable, SubgraphOperation, SubgraphOperationKind, SubgraphOperationStatus, SubgraphOperationsTable, SubgraphProcessingStatsTable, SubgraphViolationsTable, SubgraphsTable, SyncScopesTable, Transaction, TransactionsArchiveTable, TransactionsTable, TriggerEvaluatorState, TriggerEvaluatorStateTable, UpdateAccountBalanceAlerts, UpdateAccountCredits, UpdateAccountSpendCap, UpdateApiKey, UpdateArchiveFetch, UpdateBlock, UpdateContract, UpdateEvent, UpdateIndexProgress, UpdateSubgraph, UpdateSubgraphOperation, UpdateTransaction, UpdateUsageLedgerRow, UpdateVmEvent, UpdateWebhook, UpdateWebhookOutbox, UsageLedgerRow, UsageLedgerTable, VmEvent, VmEventsArchiveTable, VmEventsTable, Waitlist, WaitlistSignup, WaitlistSignupsTable, WaitlistsTable, Webhook, WebhookDeliveriesTable, WebhookDelivery, WebhookFormat, WebhookIncidentAlert, WebhookIncidentAlertsTable, WebhookKind, WebhookOutbox, WebhookOutboxTable, WebhookReplay, WebhookReplayStatus, WebhookReplaysTable, WebhookStatus, WebhooksTable };