@secondlayer/shared 12.1.1 → 12.1.2
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/dist/src/archive/copy-loader.d.ts +1 -0
- package/dist/src/archive/copy-loader.js.map +2 -2
- package/dist/src/archive/instance-diagnosis.d.ts +1 -0
- package/dist/src/archive/instance-diagnosis.js.map +2 -2
- package/dist/src/archive/reference.d.ts +1 -0
- package/dist/src/archive/reference.js.map +2 -2
- package/dist/src/archive/status.d.ts +1 -0
- package/dist/src/archive/status.js.map +2 -2
- package/dist/src/db/queries/subgraphs.js.map +2 -2
- package/dist/src/index-http.d.ts +1 -0
- package/dist/src/index.d.ts +17 -0
- package/dist/src/index.js.map +4 -4
- package/dist/src/node/archive-client.d.ts +1 -0
- package/dist/src/node/archive-client.js.map +2 -2
- package/dist/src/schemas/index.d.ts +15 -0
- package/dist/src/schemas/index.js.map +3 -3
- package/dist/src/schemas/subgraphs.d.ts +6 -0
- package/dist/src/schemas/subgraphs.js.map +2 -2
- package/dist/src/schemas/webhooks.d.ts +18 -5
- package/dist/src/schemas/webhooks.js.map +2 -2
- package/dist/src/streams-rows/index.d.ts +7 -3
- package/dist/src/subgraphs/spec.d.ts +3 -0
- package/dist/src/subgraphs/spec.js.map +2 -2
- package/package.json +2 -2
|
@@ -13,6 +13,7 @@ import { Writable } from "node:stream";
|
|
|
13
13
|
* rows, and lets the CLI and the indexer share one loader rather than growing
|
|
14
14
|
* two subtly different CSV encoders.
|
|
15
15
|
*/
|
|
16
|
+
/** The three archive datasets: blocks, transactions, and events. */
|
|
16
17
|
type ArchiveDataset = "blocks" | "transactions" | "events";
|
|
17
18
|
/** Row shapes exactly as the archive's Parquet columns are named. */
|
|
18
19
|
type ArchiveRow = Record<string, unknown>;
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/archive/copy-loader.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"import { once } from \"node:events\";\nimport type { Writable } from \"node:stream\";\n\n/**\n * Streaming `COPY ... FROM STDIN` loader for canonical archive rows.\n *\n * Restoring history row-by-row is not viable at chain scale: batched INSERTs\n * measured ~2k rows/s on this schema, which is days for a full genesis restore.\n * COPY moves the same data at ~64k rows/s because it is one statement per\n * partition with no per-row round-trip.\n *\n * This module deliberately knows nothing about Parquet or about where rows come\n * from — it accepts the archive's row shape as an async iterable. That keeps the\n * heavy file-format dependency out of every consumer that only needs to write\n * rows, and lets the CLI and the indexer share one loader rather than growing\n * two subtly different CSV encoders.\n */\n\nexport type ArchiveDataset = \"blocks\" | \"transactions\" | \"events\";\n\n/** Row shapes exactly as the archive's Parquet columns are named. */\nexport type ArchiveRow = Record<string, unknown>;\n\n/**\n * CSV encoding for `WITH (FORMAT csv, NULL '\\N')`: every present value is\n * quoted with embedded quotes doubled, so a value that happens to equal the\n * NULL marker cannot be misread as SQL NULL. Only a genuinely absent value is\n * written unquoted as `\\N`.\n */\nexport function csvField(value: string | number | null | undefined): string {\n\tif (value === null || value === undefined) return \"\\\\N\";\n\tconst text = typeof value === \"number\" ? String(value) : value;\n\treturn `\"${text.replace(/\"/g, '\"\"')}\"`;\n}\n\nfunction asNumber(value: unknown): number {\n\treturn typeof value === \"bigint\" ? Number(value) : Number(value as number);\n}\n\nfunction asText(value: unknown): string {\n\treturn Buffer.isBuffer(value) ? value.toString(\"utf8\") : String(value);\n}\n\nfunction asOptionalText(value: unknown): string | null {\n\treturn value === null || value === undefined ? null : asText(value);\n}\n\nexport function toCsvLine(dataset: ArchiveDataset, row: ArchiveRow): string {\n\tif (dataset === \"blocks\") {\n\t\treturn [\n\t\t\tcsvField(asNumber(row.height)),\n\t\t\tcsvField(asText(row.hash)),\n\t\t\tcsvField(asText(row.parent_hash)),\n\t\t\tcsvField(asNumber(row.burn_block_height)),\n\t\t\tcsvField(asOptionalText(row.burn_block_hash)),\n\t\t\tcsvField(asOptionalText(row.index_block_hash)),\n\t\t\tcsvField(asNumber(row.timestamp)),\n\t\t].join(\",\");\n\t}\n\tif (dataset === \"transactions\") {\n\t\treturn [\n\t\t\tcsvField(asText(row.tx_id)),\n\t\t\tcsvField(asNumber(row.block_height)),\n\t\t\tcsvField(asNumber(row.tx_index)),\n\t\t\tcsvField(asText(row.type)),\n\t\t\tcsvField(asText(row.sender)),\n\t\t\tcsvField(asText(row.status)),\n\t\t\tcsvField(asOptionalText(row.contract_id)),\n\t\t\tcsvField(asOptionalText(row.function_name)),\n\t\t\tcsvField(asOptionalText(row.function_args_json)),\n\t\t\tcsvField(asOptionalText(row.raw_result)),\n\t\t\tcsvField(asText(row.raw_tx)),\n\t\t].join(\",\");\n\t}\n\treturn [\n\t\tcsvField(asText(row.tx_id)),\n\t\tcsvField(asNumber(row.block_height)),\n\t\tcsvField(asNumber(row.event_index)),\n\t\tcsvField(asText(row.event_type)),\n\t\tcsvField(asText(row.data_json)),\n\t].join(\",\");\n}\n\n/**\n * `canonical` is omitted from the blocks column list: every archived block is\n * canonical by definition, and the schema defaults it to true, so carrying it\n * through every row would be dead weight.\n */\nexport function copyStatement(dataset: ArchiveDataset): string {\n\tif (dataset === \"blocks\") {\n\t\treturn \"COPY blocks (height, hash, parent_hash, burn_block_height, burn_block_hash, index_block_hash, timestamp) FROM STDIN WITH (FORMAT csv, NULL '\\\\N')\";\n\t}\n\tif (dataset === \"transactions\") {\n\t\treturn \"COPY transactions (tx_id, block_height, tx_index, type, sender, status, contract_id, function_name, function_args, raw_result, raw_tx) FROM STDIN WITH (FORMAT csv, NULL '\\\\N')\";\n\t}\n\treturn \"COPY events (tx_id, block_height, event_index, type, data) FROM STDIN WITH (FORMAT csv, NULL '\\\\N')\";\n}\n\n/**\n * Stream rows onto an open COPY writable. The caller owns opening it (drivers\n * differ in how a COPY stream is obtained) and this owns the wire format,\n * backpressure, and failure semantics.\n *\n * On error the stream is destroyed rather than ended, so Postgres aborts the\n * COPY instead of committing a partial one — an interrupted load must leave\n * nothing behind, not half a partition.\n */\nexport async function writeRowsToCopyStream(params: {\n\twritable: Writable;\n\tdataset: ArchiveDataset;\n\trows: AsyncIterable<ArchiveRow>;\n\tonProgress?: (rowsWritten: number) => void;\n\tprogressEvery?: number;\n}): Promise<number> {\n\tconst { writable, dataset, rows } = params;\n\tconst progressEvery = params.progressEvery ?? 500_000;\n\tlet count = 0;\n\ttry {\n\t\tfor await (const row of rows) {\n\t\t\tconst line = `${toCsvLine(dataset, row)}\\n`;\n\t\t\tif (!writable.write(line)) {\n\t\t\t\tawait once(writable, \"drain\");\n\t\t\t}\n\t\t\tcount++;\n\t\t\tif (params.onProgress && count % progressEvery === 0) {\n\t\t\t\tparams.onProgress(count);\n\t\t\t}\n\t\t}\n\t\tawait new Promise<void>((resolve, reject) => {\n\t\t\twritable.end((error?: Error | null) =>\n\t\t\t\terror ? reject(error) : resolve(),\n\t\t\t);\n\t\t});\n\t} catch (error) {\n\t\twritable.destroy(error instanceof Error ? error : new Error(String(error)));\n\t\tthrow error;\n\t}\n\treturn count;\n}\n"
|
|
5
|
+
"import { once } from \"node:events\";\nimport type { Writable } from \"node:stream\";\n\n/**\n * Streaming `COPY ... FROM STDIN` loader for canonical archive rows.\n *\n * Restoring history row-by-row is not viable at chain scale: batched INSERTs\n * measured ~2k rows/s on this schema, which is days for a full genesis restore.\n * COPY moves the same data at ~64k rows/s because it is one statement per\n * partition with no per-row round-trip.\n *\n * This module deliberately knows nothing about Parquet or about where rows come\n * from — it accepts the archive's row shape as an async iterable. That keeps the\n * heavy file-format dependency out of every consumer that only needs to write\n * rows, and lets the CLI and the indexer share one loader rather than growing\n * two subtly different CSV encoders.\n */\n\n/** The three archive datasets: blocks, transactions, and events. */\nexport type ArchiveDataset = \"blocks\" | \"transactions\" | \"events\";\n\n/** Row shapes exactly as the archive's Parquet columns are named. */\nexport type ArchiveRow = Record<string, unknown>;\n\n/**\n * CSV encoding for `WITH (FORMAT csv, NULL '\\N')`: every present value is\n * quoted with embedded quotes doubled, so a value that happens to equal the\n * NULL marker cannot be misread as SQL NULL. Only a genuinely absent value is\n * written unquoted as `\\N`.\n */\nexport function csvField(value: string | number | null | undefined): string {\n\tif (value === null || value === undefined) return \"\\\\N\";\n\tconst text = typeof value === \"number\" ? String(value) : value;\n\treturn `\"${text.replace(/\"/g, '\"\"')}\"`;\n}\n\nfunction asNumber(value: unknown): number {\n\treturn typeof value === \"bigint\" ? Number(value) : Number(value as number);\n}\n\nfunction asText(value: unknown): string {\n\treturn Buffer.isBuffer(value) ? value.toString(\"utf8\") : String(value);\n}\n\nfunction asOptionalText(value: unknown): string | null {\n\treturn value === null || value === undefined ? null : asText(value);\n}\n\nexport function toCsvLine(dataset: ArchiveDataset, row: ArchiveRow): string {\n\tif (dataset === \"blocks\") {\n\t\treturn [\n\t\t\tcsvField(asNumber(row.height)),\n\t\t\tcsvField(asText(row.hash)),\n\t\t\tcsvField(asText(row.parent_hash)),\n\t\t\tcsvField(asNumber(row.burn_block_height)),\n\t\t\tcsvField(asOptionalText(row.burn_block_hash)),\n\t\t\tcsvField(asOptionalText(row.index_block_hash)),\n\t\t\tcsvField(asNumber(row.timestamp)),\n\t\t].join(\",\");\n\t}\n\tif (dataset === \"transactions\") {\n\t\treturn [\n\t\t\tcsvField(asText(row.tx_id)),\n\t\t\tcsvField(asNumber(row.block_height)),\n\t\t\tcsvField(asNumber(row.tx_index)),\n\t\t\tcsvField(asText(row.type)),\n\t\t\tcsvField(asText(row.sender)),\n\t\t\tcsvField(asText(row.status)),\n\t\t\tcsvField(asOptionalText(row.contract_id)),\n\t\t\tcsvField(asOptionalText(row.function_name)),\n\t\t\tcsvField(asOptionalText(row.function_args_json)),\n\t\t\tcsvField(asOptionalText(row.raw_result)),\n\t\t\tcsvField(asText(row.raw_tx)),\n\t\t].join(\",\");\n\t}\n\treturn [\n\t\tcsvField(asText(row.tx_id)),\n\t\tcsvField(asNumber(row.block_height)),\n\t\tcsvField(asNumber(row.event_index)),\n\t\tcsvField(asText(row.event_type)),\n\t\tcsvField(asText(row.data_json)),\n\t].join(\",\");\n}\n\n/**\n * `canonical` is omitted from the blocks column list: every archived block is\n * canonical by definition, and the schema defaults it to true, so carrying it\n * through every row would be dead weight.\n */\nexport function copyStatement(dataset: ArchiveDataset): string {\n\tif (dataset === \"blocks\") {\n\t\treturn \"COPY blocks (height, hash, parent_hash, burn_block_height, burn_block_hash, index_block_hash, timestamp) FROM STDIN WITH (FORMAT csv, NULL '\\\\N')\";\n\t}\n\tif (dataset === \"transactions\") {\n\t\treturn \"COPY transactions (tx_id, block_height, tx_index, type, sender, status, contract_id, function_name, function_args, raw_result, raw_tx) FROM STDIN WITH (FORMAT csv, NULL '\\\\N')\";\n\t}\n\treturn \"COPY events (tx_id, block_height, event_index, type, data) FROM STDIN WITH (FORMAT csv, NULL '\\\\N')\";\n}\n\n/**\n * Stream rows onto an open COPY writable. The caller owns opening it (drivers\n * differ in how a COPY stream is obtained) and this owns the wire format,\n * backpressure, and failure semantics.\n *\n * On error the stream is destroyed rather than ended, so Postgres aborts the\n * COPY instead of committing a partial one — an interrupted load must leave\n * nothing behind, not half a partition.\n */\nexport async function writeRowsToCopyStream(params: {\n\twritable: Writable;\n\tdataset: ArchiveDataset;\n\trows: AsyncIterable<ArchiveRow>;\n\tonProgress?: (rowsWritten: number) => void;\n\tprogressEvery?: number;\n}): Promise<number> {\n\tconst { writable, dataset, rows } = params;\n\tconst progressEvery = params.progressEvery ?? 500_000;\n\tlet count = 0;\n\ttry {\n\t\tfor await (const row of rows) {\n\t\t\tconst line = `${toCsvLine(dataset, row)}\\n`;\n\t\t\tif (!writable.write(line)) {\n\t\t\t\tawait once(writable, \"drain\");\n\t\t\t}\n\t\t\tcount++;\n\t\t\tif (params.onProgress && count % progressEvery === 0) {\n\t\t\t\tparams.onProgress(count);\n\t\t\t}\n\t\t}\n\t\tawait new Promise<void>((resolve, reject) => {\n\t\t\twritable.end((error?: Error | null) =>\n\t\t\t\terror ? reject(error) : resolve(),\n\t\t\t);\n\t\t});\n\t} catch (error) {\n\t\twritable.destroy(error instanceof Error ? error : new Error(String(error)));\n\t\tthrow error;\n\t}\n\treturn count;\n}\n"
|
|
6
6
|
],
|
|
7
|
-
"mappings": ";AAAA;
|
|
7
|
+
"mappings": "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",
|
|
8
8
|
"debugId": "4643C93B767098ED64756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
|
@@ -53,6 +53,7 @@ interface InstanceIssue {
|
|
|
53
53
|
nextSteps: string[];
|
|
54
54
|
}
|
|
55
55
|
type InstanceState = "healthy" | "empty-index" | "degraded";
|
|
56
|
+
/** An instance's state, the API verdict, and the issues found. */
|
|
56
57
|
interface InstanceDiagnosis {
|
|
57
58
|
state: InstanceState;
|
|
58
59
|
/** The API's own top-level verdict, passed through unmodified. */
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/archive/instance-diagnosis.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"/**\n * One reading of `/status` (or `/public/status`), shared by the CLI\n * (`status`, `doctor`, `context`) and the SDK (`sl.instance.diagnose`).\n *\n * These surfaces used to each interpret the same payload on their own and\n * disagreed in the most common first-run state: on a freshly self-hosted\n * instance with no chain data, `status` printed DEGRADED while `doctor` printed\n * \"All checks passed\", and neither named the cause or a next command. An\n * operator (or an agent) reading both had no way to tell whether it was safe to\n * proceed. The verdict now lives here so the surfaces cannot drift apart, and\n * every issue carries the exact command that resolves it.\n */\n\nexport interface PublicStatusDecoder {\n\tdecoder: string;\n\tstatus: string;\n}\n\nexport interface PublicStatusService {\n\tname: string;\n\tstatus: string;\n}\n\n/** The subset of `/status` a diagnosis reads. Everything is optional:\n * an older instance may not send a field, and a missing field must never be\n * read as a failure. */\nexport interface PublicStatus {\n\tstatus?: string;\n\tchainTip?: number | null;\n\tchainIntegrity?: {\n\t\tok?: boolean;\n\t\tmaxHeight?: number;\n\t\treason?: string | null;\n\t};\n\tstreams?: {\n\t\tstatus?: string;\n\t\ttip?: { lag_seconds?: number; block_height?: number } | null;\n\t};\n\tindex?: { status?: string; decoders?: PublicStatusDecoder[] };\n\tservices?: PublicStatusService[];\n\ttimestamp?: string;\n\t[key: string]: unknown;\n}\n\nexport interface InstanceIssue {\n\t/** What is wrong, in one line. */\n\ttitle: string;\n\t/** Why — assembled only from what the payload actually reports. */\n\tdetail?: string;\n\t/** What to run next, most useful first. */\n\tnextSteps: string[];\n}\n\nexport type InstanceState = \"healthy\" | \"empty-index\" | \"degraded\";\n\nexport interface InstanceDiagnosis {\n\tstate: InstanceState;\n\t/** The API's own top-level verdict, passed through unmodified. */\n\toverall: string;\n\tissues: InstanceIssue[];\n}\n\nexport const BOOTSTRAP_STEP =\n\t\"Restore verified history: secondlayer bootstrap --against <manifest>\";\nexport const OBSERVER_STEP =\n\t\"Confirm your node streams into this instance — secondlayer observer --mode indexer --endpoint secondlayer:3700 prints the stanza its config needs\";\nexport const SELF_HOST_DOCS_STEP =\n\t\"Docs: https://www.secondlayer.tools/docs/self-host\";\n\n/** `true` when the instance is reachable but has never indexed a block. */\nexport function hasNoChainData(status: PublicStatus): boolean {\n\tconst integrity = status.chainIntegrity;\n\t// `check_failed` is the API's placeholder when the integrity query itself\n\t// errored — its maxHeight of 0 means \"unknown\", not \"empty\".\n\tif (!integrity || integrity.reason === \"check_failed\") return false;\n\tif (integrity.maxHeight !== 0) return false;\n\treturn (status.chainTip ?? null) === null;\n}\n\nfunction emptyIndexIssue(status: PublicStatus): InstanceIssue {\n\tconst decoders = status.index?.decoders ?? [];\n\tconst stalled = decoders.filter((d) => d.status !== \"ok\").length;\n\tconst decoderClause =\n\t\tstalled > 0\n\t\t\t? ` and ${stalled} decoder${stalled === 1 ? \"\" : \"s\"} report unavailable because there is nothing to decode`\n\t\t\t: \"\";\n\treturn {\n\t\ttitle: \"No blocks indexed yet — this instance holds 0 blocks.\",\n\t\tdetail: `Streams has no tip${decoderClause}, which is what makes the overall status DEGRADED. This is the expected state for a fresh instance: nothing is broken, but no read returns data until history lands.`,\n\t\tnextSteps: [BOOTSTRAP_STEP, OBSERVER_STEP, SELF_HOST_DOCS_STEP],\n\t};\n}\n\nconst SERVICE_STEPS = [\n\t\"See the raw per-service report: secondlayer status --json\",\n\t\"Check the containers from docker/oss: docker compose ps, then docker compose logs --tail=50 secondlayer\",\n];\n\n/**\n * Turn a `/status` payload into a verdict plus zero or more issues.\n * Only facts present in the payload become issues — when the API says degraded\n * and names no failing surface, the issue says exactly that rather than\n * guessing a cause.\n */\nexport function diagnoseInstanceStatus(\n\tstatus: PublicStatus,\n): InstanceDiagnosis {\n\tconst overall = String(status.status ?? \"unknown\");\n\n\tif (hasNoChainData(status)) {\n\t\treturn { state: \"empty-index\", overall, issues: [emptyIndexIssue(status)] };\n\t}\n\n\tconst issues: InstanceIssue[] = [];\n\tfor (const service of status.services ?? []) {\n\t\tif (service.status === \"ok\") continue;\n\t\tissues.push({\n\t\t\ttitle: `Service ${service.name} reports ${service.status}.`,\n\t\t\tnextSteps: SERVICE_STEPS,\n\t\t});\n\t}\n\n\tconst integrity = status.chainIntegrity;\n\tif (integrity && integrity.ok === false) {\n\t\tissues.push({\n\t\t\ttitle: \"Chain-data integrity check failed.\",\n\t\t\tdetail:\n\t\t\t\tintegrity.reason ??\n\t\t\t\t\"The tip the instance serves is not backed by the history it implies.\",\n\t\t\tnextSteps: [\n\t\t\t\t\"Verify against the archive: secondlayer verify all --against <manifest>\",\n\t\t\t\t\"Repair what diverged: secondlayer repair --against <manifest> --apply\",\n\t\t\t],\n\t\t});\n\t}\n\n\tif (issues.length === 0 && overall !== \"healthy\") {\n\t\t// Do not invent a cause: report the contradiction and where to look.\n\t\tissues.push({\n\t\t\ttitle: `The API reports status=${overall} but names no failing service.`,\n\t\t\tdetail:\n\t\t\t\t\"Every service in the report is ok, so the degradation is in a surface this command does not summarize.\",\n\t\t\tnextSteps: SERVICE_STEPS,\n\t\t});\n\t}\n\n\treturn {\n\t\tstate: issues.length > 0 ? \"degraded\" : \"healthy\",\n\t\toverall,\n\t\tissues,\n\t};\n}\n\n// ── context ────────────────────────────────────────────────────────────\n\nexport interface ContextProbe {\n\t/** The endpoint the snapshot was read from. */\n\tapiUrl: string;\n\t/** `false` only for the metered archive deployment, which has accounts. */\n\tselfHosted: boolean;\n\t/** `true` when INSTANCE_TOKEN / SECONDLAYER_API_KEY resolved to a value. */\n\thasCredential: boolean;\n\t/** `/public/status` when it answered, `null` when the probe failed. */\n\tstatus: PublicStatus | null;\n\t/** Why the probe failed, when it did. */\n\tstatusError?: string;\n}\n\nexport interface ContextExplanation {\n\t/** One line naming the likeliest cause of the nulls below. */\n\tsummary: string;\n\t/** Per-null-field reason. Fields that carry a value are absent. */\n\tnulls: Record<string, string>;\n}\n\nconst CONTEXT_FIELDS = [\n\t\"account\",\n\t\"streamsTip\",\n\t\"indexTip\",\n\t\"subgraphs\",\n\t\"webhooks\",\n\t\"activeOperations\",\n] as const;\n\n/**\n * Explain every null in a `context` snapshot.\n *\n * The SDK degrades each read to `null` on failure, so an all-null snapshot is\n * indistinguishable from \"not bootstrapped\", \"unauthorized\", and \"broken\" —\n * which is exactly what a caller needs to tell apart. Each reason here is\n * derived from a live probe of the same instance, never assumed.\n */\nexport function explainContextNulls(\n\tsnapshot: Record<string, unknown>,\n\tprobe: ContextProbe,\n): ContextExplanation {\n\tconst nullFields = CONTEXT_FIELDS.filter(\n\t\t(field) => (snapshot[field] ?? null) === null,\n\t);\n\n\tif (nullFields.length === 0) {\n\t\treturn { summary: \"Every field resolved.\", nulls: {} };\n\t}\n\n\tif (!probe.status) {\n\t\tconst reason = `No instance answered at ${probe.apiUrl} (${probe.statusError ?? \"unreachable\"}), so this read never reached an API.`;\n\t\treturn {\n\t\t\tsummary: `${reason} Start it from docker/oss with: docker compose up -d — or point SECONDLAYER_API_URL at the instance you meant.`,\n\t\t\tnulls: Object.fromEntries(nullFields.map((field) => [field, reason])),\n\t\t};\n\t}\n\n\tconst emptyIndex = hasNoChainData(probe.status);\n\tconst blocks = probe.status.chainIntegrity?.maxHeight ?? null;\n\tconst accountReason = probe.selfHosted\n\t\t? \"Expected: a self-hosted instance has no account system. Accounts exist only on the metered archive deployment at api.secondlayer.tools, so null here is normal and not a failure.\"\n\t\t: \"Not authenticated — run `secondlayer login`, or export INSTANCE_TOKEN.\";\n\n\t// An empty index explains the chain tips and nothing else: the subgraph and\n\t// webhook lists are control-plane reads that answer on a bare instance.\n\tconst listReason = unresolvedReadReason(probe, blocks);\n\tconst chainReason = emptyIndex\n\t\t? `No blocks indexed yet — this instance holds 0 blocks, so there is no tip to report. ${BOOTSTRAP_STEP}`\n\t\t: listReason;\n\n\tconst nulls: Record<string, string> = {};\n\tfor (const field of nullFields) {\n\t\tif (field === \"account\") nulls[field] = accountReason;\n\t\telse if (field === \"streamsTip\" || field === \"indexTip\")\n\t\t\tnulls[field] = chainReason;\n\t\telse if (field === \"activeOperations\")\n\t\t\tnulls[field] =\n\t\t\t\t(snapshot.subgraphs ?? null) === null\n\t\t\t\t\t? \"Not probed: in-flight operations are read per subgraph, and the subgraph list did not resolve.\"\n\t\t\t\t\t: listReason;\n\t\telse nulls[field] = listReason;\n\t}\n\n\tconst summary = emptyIndex\n\t\t? `Instance at ${probe.apiUrl} is reachable but holds 0 blocks — the chain fields are null because nothing has been indexed yet. ${BOOTSTRAP_STEP}`\n\t\t: nullFields.length === 1 && nullFields[0] === \"account\"\n\t\t\t? `Instance at ${probe.apiUrl} is reachable and indexed to block ${blocks ?? \"unknown\"}; only \\`account\\` is null, which is normal when self-hosting.`\n\t\t\t: `Instance at ${probe.apiUrl} is reachable, but ${nullFields.length} field${nullFields.length === 1 ? \"\" : \"s\"} did not resolve — see \\`nulls\\` below for each one.`;\n\n\treturn { summary, nulls };\n}\n\nfunction unresolvedReadReason(\n\tprobe: ContextProbe,\n\tblocks: number | null,\n): string {\n\tconst where = `The instance at ${probe.apiUrl} is reachable${blocks !== null ? ` and indexed to block ${blocks}` : \"\"}, but this read returned nothing.`;\n\treturn probe.hasCredential\n\t\t? `${where} Check the surface directly: secondlayer status --json`\n\t\t: `${where} No credential resolved: export INSTANCE_TOKEN — an instance published past loopback requires it on every /v1 read.`;\n}\n"
|
|
5
|
+
"/**\n * One reading of `/status` (or `/public/status`), shared by the CLI\n * (`status`, `doctor`, `context`) and the SDK (`sl.instance.diagnose`).\n *\n * These surfaces used to each interpret the same payload on their own and\n * disagreed in the most common first-run state: on a freshly self-hosted\n * instance with no chain data, `status` printed DEGRADED while `doctor` printed\n * \"All checks passed\", and neither named the cause or a next command. An\n * operator (or an agent) reading both had no way to tell whether it was safe to\n * proceed. The verdict now lives here so the surfaces cannot drift apart, and\n * every issue carries the exact command that resolves it.\n */\n\nexport interface PublicStatusDecoder {\n\tdecoder: string;\n\tstatus: string;\n}\n\nexport interface PublicStatusService {\n\tname: string;\n\tstatus: string;\n}\n\n/** The subset of `/status` a diagnosis reads. Everything is optional:\n * an older instance may not send a field, and a missing field must never be\n * read as a failure. */\nexport interface PublicStatus {\n\tstatus?: string;\n\tchainTip?: number | null;\n\tchainIntegrity?: {\n\t\tok?: boolean;\n\t\tmaxHeight?: number;\n\t\treason?: string | null;\n\t};\n\tstreams?: {\n\t\tstatus?: string;\n\t\ttip?: { lag_seconds?: number; block_height?: number } | null;\n\t};\n\tindex?: { status?: string; decoders?: PublicStatusDecoder[] };\n\tservices?: PublicStatusService[];\n\ttimestamp?: string;\n\t[key: string]: unknown;\n}\n\nexport interface InstanceIssue {\n\t/** What is wrong, in one line. */\n\ttitle: string;\n\t/** Why — assembled only from what the payload actually reports. */\n\tdetail?: string;\n\t/** What to run next, most useful first. */\n\tnextSteps: string[];\n}\n\nexport type InstanceState = \"healthy\" | \"empty-index\" | \"degraded\";\n\n/** An instance's state, the API verdict, and the issues found. */\nexport interface InstanceDiagnosis {\n\tstate: InstanceState;\n\t/** The API's own top-level verdict, passed through unmodified. */\n\toverall: string;\n\tissues: InstanceIssue[];\n}\n\nexport const BOOTSTRAP_STEP =\n\t\"Restore verified history: secondlayer bootstrap --against <manifest>\";\nexport const OBSERVER_STEP =\n\t\"Confirm your node streams into this instance — secondlayer observer --mode indexer --endpoint secondlayer:3700 prints the stanza its config needs\";\nexport const SELF_HOST_DOCS_STEP =\n\t\"Docs: https://www.secondlayer.tools/docs/self-host\";\n\n/** `true` when the instance is reachable but has never indexed a block. */\nexport function hasNoChainData(status: PublicStatus): boolean {\n\tconst integrity = status.chainIntegrity;\n\t// `check_failed` is the API's placeholder when the integrity query itself\n\t// errored — its maxHeight of 0 means \"unknown\", not \"empty\".\n\tif (!integrity || integrity.reason === \"check_failed\") return false;\n\tif (integrity.maxHeight !== 0) return false;\n\treturn (status.chainTip ?? null) === null;\n}\n\nfunction emptyIndexIssue(status: PublicStatus): InstanceIssue {\n\tconst decoders = status.index?.decoders ?? [];\n\tconst stalled = decoders.filter((d) => d.status !== \"ok\").length;\n\tconst decoderClause =\n\t\tstalled > 0\n\t\t\t? ` and ${stalled} decoder${stalled === 1 ? \"\" : \"s\"} report unavailable because there is nothing to decode`\n\t\t\t: \"\";\n\treturn {\n\t\ttitle: \"No blocks indexed yet — this instance holds 0 blocks.\",\n\t\tdetail: `Streams has no tip${decoderClause}, which is what makes the overall status DEGRADED. This is the expected state for a fresh instance: nothing is broken, but no read returns data until history lands.`,\n\t\tnextSteps: [BOOTSTRAP_STEP, OBSERVER_STEP, SELF_HOST_DOCS_STEP],\n\t};\n}\n\nconst SERVICE_STEPS = [\n\t\"See the raw per-service report: secondlayer status --json\",\n\t\"Check the containers from docker/oss: docker compose ps, then docker compose logs --tail=50 secondlayer\",\n];\n\n/**\n * Turn a `/status` payload into a verdict plus zero or more issues.\n * Only facts present in the payload become issues — when the API says degraded\n * and names no failing surface, the issue says exactly that rather than\n * guessing a cause.\n */\nexport function diagnoseInstanceStatus(\n\tstatus: PublicStatus,\n): InstanceDiagnosis {\n\tconst overall = String(status.status ?? \"unknown\");\n\n\tif (hasNoChainData(status)) {\n\t\treturn { state: \"empty-index\", overall, issues: [emptyIndexIssue(status)] };\n\t}\n\n\tconst issues: InstanceIssue[] = [];\n\tfor (const service of status.services ?? []) {\n\t\tif (service.status === \"ok\") continue;\n\t\tissues.push({\n\t\t\ttitle: `Service ${service.name} reports ${service.status}.`,\n\t\t\tnextSteps: SERVICE_STEPS,\n\t\t});\n\t}\n\n\tconst integrity = status.chainIntegrity;\n\tif (integrity && integrity.ok === false) {\n\t\tissues.push({\n\t\t\ttitle: \"Chain-data integrity check failed.\",\n\t\t\tdetail:\n\t\t\t\tintegrity.reason ??\n\t\t\t\t\"The tip the instance serves is not backed by the history it implies.\",\n\t\t\tnextSteps: [\n\t\t\t\t\"Verify against the archive: secondlayer verify all --against <manifest>\",\n\t\t\t\t\"Repair what diverged: secondlayer repair --against <manifest> --apply\",\n\t\t\t],\n\t\t});\n\t}\n\n\tif (issues.length === 0 && overall !== \"healthy\") {\n\t\t// Do not invent a cause: report the contradiction and where to look.\n\t\tissues.push({\n\t\t\ttitle: `The API reports status=${overall} but names no failing service.`,\n\t\t\tdetail:\n\t\t\t\t\"Every service in the report is ok, so the degradation is in a surface this command does not summarize.\",\n\t\t\tnextSteps: SERVICE_STEPS,\n\t\t});\n\t}\n\n\treturn {\n\t\tstate: issues.length > 0 ? \"degraded\" : \"healthy\",\n\t\toverall,\n\t\tissues,\n\t};\n}\n\n// ── context ────────────────────────────────────────────────────────────\n\nexport interface ContextProbe {\n\t/** The endpoint the snapshot was read from. */\n\tapiUrl: string;\n\t/** `false` only for the metered archive deployment, which has accounts. */\n\tselfHosted: boolean;\n\t/** `true` when INSTANCE_TOKEN / SECONDLAYER_API_KEY resolved to a value. */\n\thasCredential: boolean;\n\t/** `/public/status` when it answered, `null` when the probe failed. */\n\tstatus: PublicStatus | null;\n\t/** Why the probe failed, when it did. */\n\tstatusError?: string;\n}\n\nexport interface ContextExplanation {\n\t/** One line naming the likeliest cause of the nulls below. */\n\tsummary: string;\n\t/** Per-null-field reason. Fields that carry a value are absent. */\n\tnulls: Record<string, string>;\n}\n\nconst CONTEXT_FIELDS = [\n\t\"account\",\n\t\"streamsTip\",\n\t\"indexTip\",\n\t\"subgraphs\",\n\t\"webhooks\",\n\t\"activeOperations\",\n] as const;\n\n/**\n * Explain every null in a `context` snapshot.\n *\n * The SDK degrades each read to `null` on failure, so an all-null snapshot is\n * indistinguishable from \"not bootstrapped\", \"unauthorized\", and \"broken\" —\n * which is exactly what a caller needs to tell apart. Each reason here is\n * derived from a live probe of the same instance, never assumed.\n */\nexport function explainContextNulls(\n\tsnapshot: Record<string, unknown>,\n\tprobe: ContextProbe,\n): ContextExplanation {\n\tconst nullFields = CONTEXT_FIELDS.filter(\n\t\t(field) => (snapshot[field] ?? null) === null,\n\t);\n\n\tif (nullFields.length === 0) {\n\t\treturn { summary: \"Every field resolved.\", nulls: {} };\n\t}\n\n\tif (!probe.status) {\n\t\tconst reason = `No instance answered at ${probe.apiUrl} (${probe.statusError ?? \"unreachable\"}), so this read never reached an API.`;\n\t\treturn {\n\t\t\tsummary: `${reason} Start it from docker/oss with: docker compose up -d — or point SECONDLAYER_API_URL at the instance you meant.`,\n\t\t\tnulls: Object.fromEntries(nullFields.map((field) => [field, reason])),\n\t\t};\n\t}\n\n\tconst emptyIndex = hasNoChainData(probe.status);\n\tconst blocks = probe.status.chainIntegrity?.maxHeight ?? null;\n\tconst accountReason = probe.selfHosted\n\t\t? \"Expected: a self-hosted instance has no account system. Accounts exist only on the metered archive deployment at api.secondlayer.tools, so null here is normal and not a failure.\"\n\t\t: \"Not authenticated — run `secondlayer login`, or export INSTANCE_TOKEN.\";\n\n\t// An empty index explains the chain tips and nothing else: the subgraph and\n\t// webhook lists are control-plane reads that answer on a bare instance.\n\tconst listReason = unresolvedReadReason(probe, blocks);\n\tconst chainReason = emptyIndex\n\t\t? `No blocks indexed yet — this instance holds 0 blocks, so there is no tip to report. ${BOOTSTRAP_STEP}`\n\t\t: listReason;\n\n\tconst nulls: Record<string, string> = {};\n\tfor (const field of nullFields) {\n\t\tif (field === \"account\") nulls[field] = accountReason;\n\t\telse if (field === \"streamsTip\" || field === \"indexTip\")\n\t\t\tnulls[field] = chainReason;\n\t\telse if (field === \"activeOperations\")\n\t\t\tnulls[field] =\n\t\t\t\t(snapshot.subgraphs ?? null) === null\n\t\t\t\t\t? \"Not probed: in-flight operations are read per subgraph, and the subgraph list did not resolve.\"\n\t\t\t\t\t: listReason;\n\t\telse nulls[field] = listReason;\n\t}\n\n\tconst summary = emptyIndex\n\t\t? `Instance at ${probe.apiUrl} is reachable but holds 0 blocks — the chain fields are null because nothing has been indexed yet. ${BOOTSTRAP_STEP}`\n\t\t: nullFields.length === 1 && nullFields[0] === \"account\"\n\t\t\t? `Instance at ${probe.apiUrl} is reachable and indexed to block ${blocks ?? \"unknown\"}; only \\`account\\` is null, which is normal when self-hosting.`\n\t\t\t: `Instance at ${probe.apiUrl} is reachable, but ${nullFields.length} field${nullFields.length === 1 ? \"\" : \"s\"} did not resolve — see \\`nulls\\` below for each one.`;\n\n\treturn { summary, nulls };\n}\n\nfunction unresolvedReadReason(\n\tprobe: ContextProbe,\n\tblocks: number | null,\n): string {\n\tconst where = `The instance at ${probe.apiUrl} is reachable${blocks !== null ? ` and indexed to block ${blocks}` : \"\"}, but this read returned nothing.`;\n\treturn probe.hasCredential\n\t\t? `${where} Check the surface directly: secondlayer status --json`\n\t\t: `${where} No credential resolved: export INSTANCE_TOKEN — an instance published past loopback requires it on every /v1 read.`;\n}\n"
|
|
6
6
|
],
|
|
7
|
-
"mappings": ";
|
|
7
|
+
"mappings": "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",
|
|
8
8
|
"debugId": "774E7BBD29A35EA564756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
|
@@ -101,6 +101,7 @@ interface PartitionSemanticDigest {
|
|
|
101
101
|
* where the root is two levels above the manifest
|
|
102
102
|
* (`<root>/snapshots/<digest>.json`).
|
|
103
103
|
*/
|
|
104
|
+
/** One archive file: dataset, block range, path, rows, bytes. */
|
|
104
105
|
type ArchivePartition = {
|
|
105
106
|
dataset: string
|
|
106
107
|
from_block: number
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/archive/reference.ts", "../src/crypto/ed25519.ts", "../src/streams-bulk-manifest.ts", "../src/archive/root-key.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"import { createHash } from \"node:crypto\";\nimport { readFile } from \"node:fs/promises\";\nimport { dirname, join, resolve } from \"node:path\";\nimport { verifyStreamsBulkManifestSignature } from \"../streams-bulk-manifest.ts\";\nimport type { RangeDigest } from \"./range-digest.ts\";\nimport { ARCHIVE_ROOT_PUBLIC_KEY_PEM } from \"./root-key.ts\";\nimport type { PartitionSemanticDigest } from \"./semantic-digest.ts\";\n\n/**\n * Loading and trusting an archive reference — shared by the CLI and the SDK\n * so the two cannot disagree about what \"the archive says\".\n *\n * A reference is either an https URL or a local path to a snapshot manifest.\n * The SDK only passes `http(s)://` sources; local paths stay CLI. Partition\n * objects live beside the snapshot at `<root>/<dataset>/<name>.parquet`,\n * where the root is two levels above the manifest\n * (`<root>/snapshots/<digest>.json`).\n */\n\nexport type ArchivePartition = {\n\tdataset: string;\n\tfrom_block: number;\n\tto_block: number;\n\tpath: string;\n\trow_count: number;\n\tbyte_size: number;\n\tsha256: string;\n};\n\nexport type ArchiveManifest = {\n\tnetwork?: string;\n\tcoverage?: { from_block: number; to_block: number };\n\tpartition_size_blocks?: number;\n\trange_digests?: RangeDigest[];\n\tpartition_semantic_digests?: PartitionSemanticDigest[];\n\tpartitions?: ArchivePartition[];\n\tsignature?: string;\n\tkey_id?: string;\n\t[key: string]: unknown;\n};\n\nexport type LoadedReference = {\n\tmanifest: ArchiveManifest;\n\torigin: string;\n\t/** Base for resolving partition paths; a URL prefix or a directory. */\n\troot: string;\n\tisRemote: boolean;\n};\n\n/**\n * The archive root, `latest.json`, is a signed POINTER at a snapshot manifest,\n * not a manifest itself. It is also the only URL a user can be expected to\n * know, so every command accepts it and follows it — anything else makes the\n * obvious command fail with an error about digests, which reads as \"this tool\n * is broken\" rather than \"you passed the wrong file\".\n */\ntype LatestPointer = {\n\tsnapshot_path?: string;\n\tsnapshot_digest?: string;\n\tsignature?: string;\n\tkey_id?: string;\n};\n\nfunction isLatestPointer(value: ArchiveManifest): boolean {\n\tconst pointer = value as LatestPointer;\n\treturn (\n\t\ttypeof pointer.snapshot_path === \"string\" &&\n\t\t(value.partitions === undefined || value.partitions.length === 0)\n\t);\n}\n\n/** The publisher names snapshots by their content digest and nothing else\n * (`packages/indexer/src/archive/promote-snapshot.ts`). Anything looser is\n * a pointer someone rewrote, and it must not steer a fetch. */\nconst SNAPSHOT_PATH_PATTERN = /^snapshots\\/[0-9a-f]{64}\\.json$/;\n\nasync function fetchJson(\n\turl: string,\n\tfetchImpl: (\n\t\tinput: string | URL | Request,\n\t\tinit?: RequestInit,\n\t) => Promise<Response>,\n): Promise<unknown> {\n\tconst response = await fetchImpl(url, {\n\t\tsignal: AbortSignal.timeout(30_000),\n\t});\n\tif (!response.ok) {\n\t\tthrow new Error(\n\t\t\t`could not fetch manifest (${response.status} ${response.statusText})`,\n\t\t);\n\t}\n\treturn response.json();\n}\n\nexport type LoadReferenceOptions = {\n\t/** Key used to verify a signed `latest.json` pointer. The pointer names\n\t * which snapshot is current, so without this an attacker holding only\n\t * bucket write access could roll every reader back to an older, still\n\t * validly signed, snapshot. */\n\tpublicKeyPem?: string;\n\t/** Test/SDK seam; production CLI leaves this unset and uses global fetch. */\n\tfetchImpl?: (\n\t\tinput: string | URL | Request,\n\t\tinit?: RequestInit,\n\t) => Promise<Response>;\n};\n\nexport async function loadReference(\n\tsource: string,\n\toptions: LoadReferenceOptions = {},\n): Promise<LoadedReference> {\n\tconst isRemote = /^https?:\\/\\//.test(source);\n\tconst fetchImpl = options.fetchImpl ?? globalThis.fetch;\n\tlet manifest: ArchiveManifest;\n\tlet root: string;\n\n\tif (isRemote) {\n\t\tmanifest = (await fetchJson(source, fetchImpl)) as ArchiveManifest;\n\t\tconst url = new URL(source);\n\t\t// Both `<root>/latest.json` and `<root>/snapshots/<digest>.json` resolve\n\t\t// their partition paths against `<root>`.\n\t\turl.pathname = url.pathname\n\t\t\t.replace(/\\/snapshots\\/[^/]+$/, \"\")\n\t\t\t.replace(/\\/[^/]+\\.json$/, \"\");\n\t\troot = url.toString();\n\t} else {\n\t\tconst path = resolve(source);\n\t\tmanifest = JSON.parse(await readFile(path, \"utf8\")) as ArchiveManifest;\n\t\troot = isLatestPointer(manifest) ? dirname(path) : dirname(dirname(path));\n\t}\n\n\tif (!isLatestPointer(manifest)) {\n\t\treturn { manifest, origin: source, root, isRemote };\n\t}\n\n\t// Follow the pointer, then prove it led where it claimed. A tampered\n\t// pointer could otherwise redirect to a different — and still validly\n\t// signed — snapshot, which is a downgrade attack rather than a forgery.\n\tconst pointer = manifest as ArchiveManifest & LatestPointer;\n\tconst snapshotPath = pointer.snapshot_path as string;\n\tif (!SNAPSHOT_PATH_PATTERN.test(snapshotPath)) {\n\t\tthrow new Error(\n\t\t\t`pointer names an invalid snapshot path: ${snapshotPath} (expected snapshots/<sha256>.json)`,\n\t\t);\n\t}\n\tif (\n\t\ttypeof pointer.snapshot_digest !== \"string\" ||\n\t\t!/^[0-9a-f]{64}$/.test(pointer.snapshot_digest)\n\t) {\n\t\tthrow new Error(\n\t\t\t\"pointer carries no snapshot_digest, so it cannot prove which snapshot is current\",\n\t\t);\n\t}\n\tif (pointer.signature !== undefined) {\n\t\tif (!options.publicKeyPem) {\n\t\t\tthrow new Error(\n\t\t\t\t\"pointer is signed but no public key is available to verify it\",\n\t\t\t);\n\t\t}\n\t\tlet verified = false;\n\t\ttry {\n\t\t\tverified = verifyStreamsBulkManifestSignature(\n\t\t\t\tpointer as Record<string, unknown>,\n\t\t\t\toptions.publicKeyPem,\n\t\t\t);\n\t\t} catch {\n\t\t\tverified = false;\n\t\t}\n\t\tif (!verified) {\n\t\t\tthrow new Error(\n\t\t\t\t\"pointer signature did not verify against the archive key\",\n\t\t\t);\n\t\t}\n\t}\n\n\tconst resolved = isRemote\n\t\t? ((await fetchJson(\n\t\t\t\t`${root.replace(/\\/$/, \"\")}/${snapshotPath}`,\n\t\t\t\tfetchImpl,\n\t\t\t)) as ArchiveManifest)\n\t\t: (JSON.parse(\n\t\t\t\tawait readFile(join(root, snapshotPath), \"utf8\"),\n\t\t\t) as ArchiveManifest);\n\n\tconst digest = createHash(\"sha256\")\n\t\t.update(\n\t\t\tJSON.stringify(\n\t\t\t\t(({ signature: _s, key_id: _k, ...rest }) => rest)(\n\t\t\t\t\tresolved as Record<string, unknown>,\n\t\t\t\t),\n\t\t\t),\n\t\t)\n\t\t.digest(\"hex\");\n\tif (digest !== pointer.snapshot_digest) {\n\t\tthrow new Error(\n\t\t\t`pointer/snapshot mismatch: latest.json names ${pointer.snapshot_digest}, resolved manifest hashes to ${digest}`,\n\t\t);\n\t}\n\n\treturn { manifest: resolved, origin: source, root, isRemote };\n}\n\nexport type SignatureResult = { verified: boolean; reason?: string };\n\n/**\n * An unsigned or unverifiable reference is worthless as a source of truth:\n * comparing against it yields a confident answer with nothing behind it, and\n * repairing from it would write unverified data into a live database.\n */\nexport function checkSignature(\n\tmanifest: ArchiveManifest,\n\tpublicKeyPem: string | undefined,\n\tinsecure: boolean,\n): SignatureResult {\n\tif (insecure) return { verified: false, reason: \"signature check skipped\" };\n\tif (!manifest.signature) {\n\t\treturn { verified: false, reason: \"manifest carries no signature\" };\n\t}\n\tif (!publicKeyPem) {\n\t\treturn {\n\t\t\tverified: false,\n\t\t\treason: \"no public key available to verify with\",\n\t\t};\n\t}\n\ttry {\n\t\tconst verified = verifyStreamsBulkManifestSignature(\n\t\t\tmanifest as Record<string, unknown>,\n\t\t\tpublicKeyPem,\n\t\t);\n\t\treturn {\n\t\t\tverified,\n\t\t\treason: verified ? undefined : \"signature did not verify\",\n\t\t};\n\t} catch (err) {\n\t\treturn {\n\t\t\tverified: false,\n\t\t\treason: err instanceof Error ? err.message : \"signature check failed\",\n\t\t};\n\t}\n}\n\nconst HOSTED_SIGNING_KEY_URL =\n\t\"https://api.secondlayer.tools/public/streams/signing-key\";\n\nasync function fetchHostedKey(url: string): Promise<string | undefined> {\n\t// A key fetched over plaintext is whoever sits on the wire, not a key.\n\tif (!url.startsWith(\"https://\")) return undefined;\n\ttry {\n\t\tconst response = await fetch(url, { signal: AbortSignal.timeout(10_000) });\n\t\tif (!response.ok) return undefined;\n\t\tconst body = (await response.json()) as { public_key_pem?: string };\n\t\treturn typeof body.public_key_pem === \"string\"\n\t\t\t? body.public_key_pem\n\t\t\t: undefined;\n\t} catch {\n\t\t// Offline is a legitimate state; the compiled root key still applies.\n\t\treturn undefined;\n\t}\n}\n\n/**\n * Which key a manifest is checked against, in trust order: an explicit\n * `--public-key`, then `ARCHIVE_SIGNING_PUBLIC_KEY` / `STREAMS_SIGNING_PUBLIC_KEY`\n * from the environment, then the hosted key endpoint over https (never in OSS\n * mode, which must work with no path to api.secondlayer.tools), and finally the\n * key compiled into this release. The compiled key means a fresh self-hosted\n * instance can bootstrap offline; the tradeoff is that rotating it is a CLI\n * release. Plaintext `http://` key sources are never consulted.\n */\nexport async function resolveArchivePublicKey(input: {\n\texplicitPem?: string;\n\tenvPem?: string;\n\tallowHostedApi?: boolean;\n\t/** Test seam for the hosted endpoint; production callers leave it unset. */\n\thostedKeyUrl?: string;\n}): Promise<string> {\n\tif (input.explicitPem) return input.explicitPem;\n\tif (input.envPem) return input.envPem;\n\tif (input.allowHostedApi) {\n\t\tconst hosted = await fetchHostedKey(\n\t\t\tinput.hostedKeyUrl ?? HOSTED_SIGNING_KEY_URL,\n\t\t);\n\t\tif (hosted) return hosted;\n\t}\n\treturn ARCHIVE_ROOT_PUBLIC_KEY_PEM;\n}\n",
|
|
5
|
+
"import { createHash } from \"node:crypto\";\nimport { readFile } from \"node:fs/promises\";\nimport { dirname, join, resolve } from \"node:path\";\nimport { verifyStreamsBulkManifestSignature } from \"../streams-bulk-manifest.ts\";\nimport type { RangeDigest } from \"./range-digest.ts\";\nimport { ARCHIVE_ROOT_PUBLIC_KEY_PEM } from \"./root-key.ts\";\nimport type { PartitionSemanticDigest } from \"./semantic-digest.ts\";\n\n/**\n * Loading and trusting an archive reference — shared by the CLI and the SDK\n * so the two cannot disagree about what \"the archive says\".\n *\n * A reference is either an https URL or a local path to a snapshot manifest.\n * The SDK only passes `http(s)://` sources; local paths stay CLI. Partition\n * objects live beside the snapshot at `<root>/<dataset>/<name>.parquet`,\n * where the root is two levels above the manifest\n * (`<root>/snapshots/<digest>.json`).\n */\n\n/** One archive file: dataset, block range, path, rows, bytes. */\nexport type ArchivePartition = {\n\tdataset: string;\n\tfrom_block: number;\n\tto_block: number;\n\tpath: string;\n\trow_count: number;\n\tbyte_size: number;\n\tsha256: string;\n};\n\nexport type ArchiveManifest = {\n\tnetwork?: string;\n\tcoverage?: { from_block: number; to_block: number };\n\tpartition_size_blocks?: number;\n\trange_digests?: RangeDigest[];\n\tpartition_semantic_digests?: PartitionSemanticDigest[];\n\tpartitions?: ArchivePartition[];\n\tsignature?: string;\n\tkey_id?: string;\n\t[key: string]: unknown;\n};\n\nexport type LoadedReference = {\n\tmanifest: ArchiveManifest;\n\torigin: string;\n\t/** Base for resolving partition paths; a URL prefix or a directory. */\n\troot: string;\n\tisRemote: boolean;\n};\n\n/**\n * The archive root, `latest.json`, is a signed POINTER at a snapshot manifest,\n * not a manifest itself. It is also the only URL a user can be expected to\n * know, so every command accepts it and follows it — anything else makes the\n * obvious command fail with an error about digests, which reads as \"this tool\n * is broken\" rather than \"you passed the wrong file\".\n */\ntype LatestPointer = {\n\tsnapshot_path?: string;\n\tsnapshot_digest?: string;\n\tsignature?: string;\n\tkey_id?: string;\n};\n\nfunction isLatestPointer(value: ArchiveManifest): boolean {\n\tconst pointer = value as LatestPointer;\n\treturn (\n\t\ttypeof pointer.snapshot_path === \"string\" &&\n\t\t(value.partitions === undefined || value.partitions.length === 0)\n\t);\n}\n\n/** The publisher names snapshots by their content digest and nothing else\n * (`packages/indexer/src/archive/promote-snapshot.ts`). Anything looser is\n * a pointer someone rewrote, and it must not steer a fetch. */\nconst SNAPSHOT_PATH_PATTERN = /^snapshots\\/[0-9a-f]{64}\\.json$/;\n\nasync function fetchJson(\n\turl: string,\n\tfetchImpl: (\n\t\tinput: string | URL | Request,\n\t\tinit?: RequestInit,\n\t) => Promise<Response>,\n): Promise<unknown> {\n\tconst response = await fetchImpl(url, {\n\t\tsignal: AbortSignal.timeout(30_000),\n\t});\n\tif (!response.ok) {\n\t\tthrow new Error(\n\t\t\t`could not fetch manifest (${response.status} ${response.statusText})`,\n\t\t);\n\t}\n\treturn response.json();\n}\n\nexport type LoadReferenceOptions = {\n\t/** Key used to verify a signed `latest.json` pointer. The pointer names\n\t * which snapshot is current, so without this an attacker holding only\n\t * bucket write access could roll every reader back to an older, still\n\t * validly signed, snapshot. */\n\tpublicKeyPem?: string;\n\t/** Test/SDK seam; production CLI leaves this unset and uses global fetch. */\n\tfetchImpl?: (\n\t\tinput: string | URL | Request,\n\t\tinit?: RequestInit,\n\t) => Promise<Response>;\n};\n\nexport async function loadReference(\n\tsource: string,\n\toptions: LoadReferenceOptions = {},\n): Promise<LoadedReference> {\n\tconst isRemote = /^https?:\\/\\//.test(source);\n\tconst fetchImpl = options.fetchImpl ?? globalThis.fetch;\n\tlet manifest: ArchiveManifest;\n\tlet root: string;\n\n\tif (isRemote) {\n\t\tmanifest = (await fetchJson(source, fetchImpl)) as ArchiveManifest;\n\t\tconst url = new URL(source);\n\t\t// Both `<root>/latest.json` and `<root>/snapshots/<digest>.json` resolve\n\t\t// their partition paths against `<root>`.\n\t\turl.pathname = url.pathname\n\t\t\t.replace(/\\/snapshots\\/[^/]+$/, \"\")\n\t\t\t.replace(/\\/[^/]+\\.json$/, \"\");\n\t\troot = url.toString();\n\t} else {\n\t\tconst path = resolve(source);\n\t\tmanifest = JSON.parse(await readFile(path, \"utf8\")) as ArchiveManifest;\n\t\troot = isLatestPointer(manifest) ? dirname(path) : dirname(dirname(path));\n\t}\n\n\tif (!isLatestPointer(manifest)) {\n\t\treturn { manifest, origin: source, root, isRemote };\n\t}\n\n\t// Follow the pointer, then prove it led where it claimed. A tampered\n\t// pointer could otherwise redirect to a different — and still validly\n\t// signed — snapshot, which is a downgrade attack rather than a forgery.\n\tconst pointer = manifest as ArchiveManifest & LatestPointer;\n\tconst snapshotPath = pointer.snapshot_path as string;\n\tif (!SNAPSHOT_PATH_PATTERN.test(snapshotPath)) {\n\t\tthrow new Error(\n\t\t\t`pointer names an invalid snapshot path: ${snapshotPath} (expected snapshots/<sha256>.json)`,\n\t\t);\n\t}\n\tif (\n\t\ttypeof pointer.snapshot_digest !== \"string\" ||\n\t\t!/^[0-9a-f]{64}$/.test(pointer.snapshot_digest)\n\t) {\n\t\tthrow new Error(\n\t\t\t\"pointer carries no snapshot_digest, so it cannot prove which snapshot is current\",\n\t\t);\n\t}\n\tif (pointer.signature !== undefined) {\n\t\tif (!options.publicKeyPem) {\n\t\t\tthrow new Error(\n\t\t\t\t\"pointer is signed but no public key is available to verify it\",\n\t\t\t);\n\t\t}\n\t\tlet verified = false;\n\t\ttry {\n\t\t\tverified = verifyStreamsBulkManifestSignature(\n\t\t\t\tpointer as Record<string, unknown>,\n\t\t\t\toptions.publicKeyPem,\n\t\t\t);\n\t\t} catch {\n\t\t\tverified = false;\n\t\t}\n\t\tif (!verified) {\n\t\t\tthrow new Error(\n\t\t\t\t\"pointer signature did not verify against the archive key\",\n\t\t\t);\n\t\t}\n\t}\n\n\tconst resolved = isRemote\n\t\t? ((await fetchJson(\n\t\t\t\t`${root.replace(/\\/$/, \"\")}/${snapshotPath}`,\n\t\t\t\tfetchImpl,\n\t\t\t)) as ArchiveManifest)\n\t\t: (JSON.parse(\n\t\t\t\tawait readFile(join(root, snapshotPath), \"utf8\"),\n\t\t\t) as ArchiveManifest);\n\n\tconst digest = createHash(\"sha256\")\n\t\t.update(\n\t\t\tJSON.stringify(\n\t\t\t\t(({ signature: _s, key_id: _k, ...rest }) => rest)(\n\t\t\t\t\tresolved as Record<string, unknown>,\n\t\t\t\t),\n\t\t\t),\n\t\t)\n\t\t.digest(\"hex\");\n\tif (digest !== pointer.snapshot_digest) {\n\t\tthrow new Error(\n\t\t\t`pointer/snapshot mismatch: latest.json names ${pointer.snapshot_digest}, resolved manifest hashes to ${digest}`,\n\t\t);\n\t}\n\n\treturn { manifest: resolved, origin: source, root, isRemote };\n}\n\nexport type SignatureResult = { verified: boolean; reason?: string };\n\n/**\n * An unsigned or unverifiable reference is worthless as a source of truth:\n * comparing against it yields a confident answer with nothing behind it, and\n * repairing from it would write unverified data into a live database.\n */\nexport function checkSignature(\n\tmanifest: ArchiveManifest,\n\tpublicKeyPem: string | undefined,\n\tinsecure: boolean,\n): SignatureResult {\n\tif (insecure) return { verified: false, reason: \"signature check skipped\" };\n\tif (!manifest.signature) {\n\t\treturn { verified: false, reason: \"manifest carries no signature\" };\n\t}\n\tif (!publicKeyPem) {\n\t\treturn {\n\t\t\tverified: false,\n\t\t\treason: \"no public key available to verify with\",\n\t\t};\n\t}\n\ttry {\n\t\tconst verified = verifyStreamsBulkManifestSignature(\n\t\t\tmanifest as Record<string, unknown>,\n\t\t\tpublicKeyPem,\n\t\t);\n\t\treturn {\n\t\t\tverified,\n\t\t\treason: verified ? undefined : \"signature did not verify\",\n\t\t};\n\t} catch (err) {\n\t\treturn {\n\t\t\tverified: false,\n\t\t\treason: err instanceof Error ? err.message : \"signature check failed\",\n\t\t};\n\t}\n}\n\nconst HOSTED_SIGNING_KEY_URL =\n\t\"https://api.secondlayer.tools/public/streams/signing-key\";\n\nasync function fetchHostedKey(url: string): Promise<string | undefined> {\n\t// A key fetched over plaintext is whoever sits on the wire, not a key.\n\tif (!url.startsWith(\"https://\")) return undefined;\n\ttry {\n\t\tconst response = await fetch(url, { signal: AbortSignal.timeout(10_000) });\n\t\tif (!response.ok) return undefined;\n\t\tconst body = (await response.json()) as { public_key_pem?: string };\n\t\treturn typeof body.public_key_pem === \"string\"\n\t\t\t? body.public_key_pem\n\t\t\t: undefined;\n\t} catch {\n\t\t// Offline is a legitimate state; the compiled root key still applies.\n\t\treturn undefined;\n\t}\n}\n\n/**\n * Which key a manifest is checked against, in trust order: an explicit\n * `--public-key`, then `ARCHIVE_SIGNING_PUBLIC_KEY` / `STREAMS_SIGNING_PUBLIC_KEY`\n * from the environment, then the hosted key endpoint over https (never in OSS\n * mode, which must work with no path to api.secondlayer.tools), and finally the\n * key compiled into this release. The compiled key means a fresh self-hosted\n * instance can bootstrap offline; the tradeoff is that rotating it is a CLI\n * release. Plaintext `http://` key sources are never consulted.\n */\nexport async function resolveArchivePublicKey(input: {\n\texplicitPem?: string;\n\tenvPem?: string;\n\tallowHostedApi?: boolean;\n\t/** Test seam for the hosted endpoint; production callers leave it unset. */\n\thostedKeyUrl?: string;\n}): Promise<string> {\n\tif (input.explicitPem) return input.explicitPem;\n\tif (input.envPem) return input.envPem;\n\tif (input.allowHostedApi) {\n\t\tconst hosted = await fetchHostedKey(\n\t\t\tinput.hostedKeyUrl ?? HOSTED_SIGNING_KEY_URL,\n\t\t);\n\t\tif (hosted) return hosted;\n\t}\n\treturn ARCHIVE_ROOT_PUBLIC_KEY_PEM;\n}\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
7
|
"import {\n\ted25519KeyId,\n\tloadEd25519PrivateKey,\n\tloadEd25519PublicKey,\n\tpublicKeyPemFromPrivate,\n\tsignEd25519,\n\tverifyEd25519,\n} from \"./crypto/ed25519.ts\";\n\n/**\n * ed25519 signing for the Streams cold-bulk parquet manifest.\n *\n * The live Streams lane is ed25519-signed; the bulk manifest was plain JSON with\n * only per-file sha256, so a tampered manifest+file pair verified cleanly. This\n * signs the manifest itself with the same platform key, so a consumer can trust\n * the file hashes only after the manifest signature checks out — making the two\n * availability lanes symmetric.\n *\n * The signed bytes are the manifest's canonical JSON with the signature envelope\n * fields removed, so signer and verifier agree without a separate canonical\n * form: `signature`/`key_id` are appended last, so stripping them and\n * re-serializing reproduces the exact pre-sign bytes.\n */\ntype SignatureEnvelope = { signature?: string; key_id?: string };\n\n/** The exact bytes a manifest signature covers: the manifest JSON minus the\n * signature envelope fields. */\nexport function canonicalStreamsBulkManifestPayload(\n\tmanifest: Record<string, unknown> & SignatureEnvelope,\n): string {\n\tconst { signature: _signature, key_id: _keyId, ...rest } = manifest;\n\treturn JSON.stringify(rest);\n}\n\nfunction normalizePem(pem: string): string {\n\t// Env transport often escapes newlines; restore real PEM line breaks.\n\treturn pem.includes(\"\\\\n\") ? pem.replace(/\\\\n/g, \"\\n\") : pem;\n}\n\n/**\n * Attach an ed25519 `signature` + `key_id` over the manifest's canonical bytes.\n * Returns a new manifest; re-signing one that already carries a signature signs\n * over its un-enveloped form (idempotent shape).\n */\nexport function signStreamsBulkManifest<\n\tT extends Record<string, unknown> & SignatureEnvelope,\n>(\n\tmanifest: T,\n\tprivateKeyPem: string,\n): T & { signature: string; key_id: string } {\n\tconst pem = normalizePem(privateKeyPem);\n\tconst privateKey = loadEd25519PrivateKey(pem);\n\tconst keyId = ed25519KeyId(publicKeyPemFromPrivate(pem));\n\tconst { signature: _signature, key_id: _keyId, ...base } = manifest;\n\tconst payload = JSON.stringify(base);\n\treturn {\n\t\t...(base as T),\n\t\tsignature: signEd25519(payload, privateKey),\n\t\tkey_id: keyId,\n\t};\n}\n\n/** Verify a manifest's ed25519 signature against the published public key. */\nexport function verifyStreamsBulkManifestSignature(\n\tmanifest: Record<string, unknown> & SignatureEnvelope,\n\tpublicKeyPem: string,\n): boolean {\n\tif (!manifest.signature) return false;\n\tconst payload = canonicalStreamsBulkManifestPayload(manifest);\n\treturn verifyEd25519(\n\t\tpayload,\n\t\tmanifest.signature,\n\t\tloadEd25519PublicKey(publicKeyPem),\n\t);\n}\n",
|
|
8
8
|
"/**\n * The archive signing key, compiled into every release.\n *\n * `secondlayer bootstrap`, `verify`, and `repair` refuse a manifest they\n * cannot verify, so a self-hosted instance with no network path to the hosted\n * API still needs a key it can trust. Shipping it here means the trust root\n * travels with the CLI rather than with whichever host answers\n * `/public/streams/signing-key`.\n *\n * Tradeoff: rotating this key is a CLI release until a root-signed key\n * ceremony lands. Operators who want a different root pin one with\n * `ARCHIVE_SIGNING_PUBLIC_KEY` or `--public-key`, which win over this value.\n *\n * Value: `GET https://api.secondlayer.tools/public/streams/signing-key`,\n * key_id `fHQWzs9ML2WIYakf`, ed25519, fetched 2026-08-27.\n */\nexport const ARCHIVE_ROOT_PUBLIC_KEY_PEM: string = [\n\t\"-----BEGIN PUBLIC KEY-----\",\n\t\"MCowBQYDK2VwAyEAq7wXm235JkFbElDmTVdNtmTKqRLx4PzjBgZhyREUFis=\",\n\t\"-----END PUBLIC KEY-----\",\n\t\"\",\n].join(\"\\n\");\n\nexport const ARCHIVE_ROOT_KEY_ID: string = \"fHQWzs9ML2WIYakf\";\n"
|
|
9
9
|
],
|
|
10
|
-
"mappings": ";;;;;;;;;;;;;;;;;AAAA,uBAAS;AACT;AACA;;;;;;;;;;;;;ACFA;AAAA;AAAA;AAAA;AAAA;AAAA,UAMC;AAAA,YACA;AAAA;AAaM,SAAS,sBAAsB,GAGpC;AAAA,EACD,QAAQ,YAAY,cAAc,oBAAoB,SAAS;AAAA,EAC/D,OAAO;AAAA,IACN,eAAe,WACb,OAAO,EAAE,QAAQ,OAAO,MAAM,QAAQ,CAAC,EACvC,SAAS;AAAA,IACX,cAAc,UAAU,OAAO,EAAE,QAAQ,OAAO,MAAM,OAAO,CAAC,EAAE,SAAS;AAAA,EAC1E;AAAA;AAGM,SAAS,qBAAqB,CAAC,KAAwB;AAAA,EAC7D,OAAO,iBAAiB,GAAG;AAAA;AAGrB,SAAS,oBAAoB,CAAC,KAAwB;AAAA,EAC5D,OAAO,gBAAgB,GAAG;AAAA;AAGpB,SAAS,uBAAuB,CAAC,eAA+B;AAAA,EACtE,OAAO,gBAAgB,iBAAiB,aAAa,CAAC,EACpD,OAAO,EAAE,QAAQ,OAAO,MAAM,OAAO,CAAC,EACtC,SAAS;AAAA;AAIL,SAAS,YAAY,CAAC,cAA8B;AAAA,EAC1D,MAAM,MAAM,gBAAgB,YAAY,EAAE,OAAO;AAAA,IAChD,QAAQ;AAAA,IACR,MAAM;AAAA,EACP,CAAC;AAAA,EACD,OAAO,WAAW,QAAQ,EAAE,OAAO,GAAG,EAAE,OAAO,WAAW,EAAE,MAAM,GAAG,EAAE;AAAA;AAGjE,SAAS,WAAW,CAAC,SAAiB,YAA+B;AAAA,EAC3E,OAAO,SAAS,MAAM,OAAO,KAAK,SAAS,MAAM,GAAG,UAAU,EAAE,SAC/D,QACD;AAAA;AAGM,SAAS,aAAa,CAC5B,SACA,iBACA,WACU;AAAA,EACV,IAAI;AAAA,IACH,OAAO,WACN,MACA,OAAO,KAAK,SAAS,MAAM,GAC3B,WACA,OAAO,KAAK,iBAAiB,QAAQ,CACtC;AAAA,IACC,MAAM;AAAA,IACP,OAAO;AAAA;AAAA;;;AChDF,SAAS,mCAAmC,CAClD,UACS;AAAA,EACT,QAAQ,WAAW,YAAY,QAAQ,WAAW,SAAS;AAAA,EAC3D,OAAO,KAAK,UAAU,IAAI;AAAA;AAG3B,SAAS,YAAY,CAAC,KAAqB;AAAA,EAE1C,OAAO,IAAI,SAAS,KAAK,IAAI,IAAI,QAAQ,QAAQ;AAAA,CAAI,IAAI;AAAA;AAQnD,SAAS,uBAEf,CACA,UACA,eAC4C;AAAA,EAC5C,MAAM,MAAM,aAAa,aAAa;AAAA,EACtC,MAAM,aAAa,sBAAsB,GAAG;AAAA,EAC5C,MAAM,QAAQ,aAAa,wBAAwB,GAAG,CAAC;AAAA,EACvD,QAAQ,WAAW,YAAY,QAAQ,WAAW,SAAS;AAAA,EAC3D,MAAM,UAAU,KAAK,UAAU,IAAI;AAAA,EACnC,OAAO;AAAA,OACF;AAAA,IACJ,WAAW,YAAY,SAAS,UAAU;AAAA,IAC1C,QAAQ;AAAA,EACT;AAAA;AAIM,SAAS,kCAAkC,CACjD,UACA,cACU;AAAA,EACV,IAAI,CAAC,SAAS;AAAA,IAAW,OAAO;AAAA,EAChC,MAAM,UAAU,oCAAoC,QAAQ;AAAA,EAC5D,OAAO,cACN,SACA,SAAS,WACT,qBAAqB,YAAY,CAClC;AAAA;;;ACzDM,IAAM,8BAAsC;AAAA,EAClD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACD,EAAE,KAAK;AAAA,CAAI;AAEJ,IAAM,sBAA8B;;;
|
|
10
|
+
"mappings": ";;;;;;;;;;;;;;;;;AAAA,uBAAS;AACT;AACA;;;;;;;;;;;;;ACFA;AAAA;AAAA;AAAA;AAAA;AAAA,UAMC;AAAA,YACA;AAAA;AAaM,SAAS,sBAAsB,GAGpC;AAAA,EACD,QAAQ,YAAY,cAAc,oBAAoB,SAAS;AAAA,EAC/D,OAAO;AAAA,IACN,eAAe,WACb,OAAO,EAAE,QAAQ,OAAO,MAAM,QAAQ,CAAC,EACvC,SAAS;AAAA,IACX,cAAc,UAAU,OAAO,EAAE,QAAQ,OAAO,MAAM,OAAO,CAAC,EAAE,SAAS;AAAA,EAC1E;AAAA;AAGM,SAAS,qBAAqB,CAAC,KAAwB;AAAA,EAC7D,OAAO,iBAAiB,GAAG;AAAA;AAGrB,SAAS,oBAAoB,CAAC,KAAwB;AAAA,EAC5D,OAAO,gBAAgB,GAAG;AAAA;AAGpB,SAAS,uBAAuB,CAAC,eAA+B;AAAA,EACtE,OAAO,gBAAgB,iBAAiB,aAAa,CAAC,EACpD,OAAO,EAAE,QAAQ,OAAO,MAAM,OAAO,CAAC,EACtC,SAAS;AAAA;AAIL,SAAS,YAAY,CAAC,cAA8B;AAAA,EAC1D,MAAM,MAAM,gBAAgB,YAAY,EAAE,OAAO;AAAA,IAChD,QAAQ;AAAA,IACR,MAAM;AAAA,EACP,CAAC;AAAA,EACD,OAAO,WAAW,QAAQ,EAAE,OAAO,GAAG,EAAE,OAAO,WAAW,EAAE,MAAM,GAAG,EAAE;AAAA;AAGjE,SAAS,WAAW,CAAC,SAAiB,YAA+B;AAAA,EAC3E,OAAO,SAAS,MAAM,OAAO,KAAK,SAAS,MAAM,GAAG,UAAU,EAAE,SAC/D,QACD;AAAA;AAGM,SAAS,aAAa,CAC5B,SACA,iBACA,WACU;AAAA,EACV,IAAI;AAAA,IACH,OAAO,WACN,MACA,OAAO,KAAK,SAAS,MAAM,GAC3B,WACA,OAAO,KAAK,iBAAiB,QAAQ,CACtC;AAAA,IACC,MAAM;AAAA,IACP,OAAO;AAAA;AAAA;;;AChDF,SAAS,mCAAmC,CAClD,UACS;AAAA,EACT,QAAQ,WAAW,YAAY,QAAQ,WAAW,SAAS;AAAA,EAC3D,OAAO,KAAK,UAAU,IAAI;AAAA;AAG3B,SAAS,YAAY,CAAC,KAAqB;AAAA,EAE1C,OAAO,IAAI,SAAS,KAAK,IAAI,IAAI,QAAQ,QAAQ;AAAA,CAAI,IAAI;AAAA;AAQnD,SAAS,uBAEf,CACA,UACA,eAC4C;AAAA,EAC5C,MAAM,MAAM,aAAa,aAAa;AAAA,EACtC,MAAM,aAAa,sBAAsB,GAAG;AAAA,EAC5C,MAAM,QAAQ,aAAa,wBAAwB,GAAG,CAAC;AAAA,EACvD,QAAQ,WAAW,YAAY,QAAQ,WAAW,SAAS;AAAA,EAC3D,MAAM,UAAU,KAAK,UAAU,IAAI;AAAA,EACnC,OAAO;AAAA,OACF;AAAA,IACJ,WAAW,YAAY,SAAS,UAAU;AAAA,IAC1C,QAAQ;AAAA,EACT;AAAA;AAIM,SAAS,kCAAkC,CACjD,UACA,cACU;AAAA,EACV,IAAI,CAAC,SAAS;AAAA,IAAW,OAAO;AAAA,EAChC,MAAM,UAAU,oCAAoC,QAAQ;AAAA,EAC5D,OAAO,cACN,SACA,SAAS,WACT,qBAAqB,YAAY,CAClC;AAAA;;;ACzDM,IAAM,8BAAsC;AAAA,EAClD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACD,EAAE,KAAK;AAAA,CAAI;AAEJ,IAAM,sBAA8B;;;AHyC3C,SAAS,eAAe,CAAC,OAAiC;AAAA,EACzD,MAAM,UAAU;AAAA,EAChB,OACC,OAAO,QAAQ,kBAAkB,aAChC,MAAM,eAAe,aAAa,MAAM,WAAW,WAAW;AAAA;AAOjE,IAAM,wBAAwB;AAE9B,eAAe,SAAS,CACvB,KACA,WAImB;AAAA,EACnB,MAAM,WAAW,MAAM,UAAU,KAAK;AAAA,IACrC,QAAQ,YAAY,QAAQ,KAAM;AAAA,EACnC,CAAC;AAAA,EACD,IAAI,CAAC,SAAS,IAAI;AAAA,IACjB,MAAM,IAAI,MACT,6BAA6B,SAAS,UAAU,SAAS,aAC1D;AAAA,EACD;AAAA,EACA,OAAO,SAAS,KAAK;AAAA;AAgBtB,eAAsB,aAAa,CAClC,QACA,UAAgC,CAAC,GACN;AAAA,EAC3B,MAAM,WAAW,eAAe,KAAK,MAAM;AAAA,EAC3C,MAAM,YAAY,QAAQ,aAAa,WAAW;AAAA,EAClD,IAAI;AAAA,EACJ,IAAI;AAAA,EAEJ,IAAI,UAAU;AAAA,IACb,WAAY,MAAM,UAAU,QAAQ,SAAS;AAAA,IAC7C,MAAM,MAAM,IAAI,IAAI,MAAM;AAAA,IAG1B,IAAI,WAAW,IAAI,SACjB,QAAQ,uBAAuB,EAAE,EACjC,QAAQ,kBAAkB,EAAE;AAAA,IAC9B,OAAO,IAAI,SAAS;AAAA,EACrB,EAAO;AAAA,IACN,MAAM,OAAO,QAAQ,MAAM;AAAA,IAC3B,WAAW,KAAK,MAAM,MAAM,SAAS,MAAM,MAAM,CAAC;AAAA,IAClD,OAAO,gBAAgB,QAAQ,IAAI,QAAQ,IAAI,IAAI,QAAQ,QAAQ,IAAI,CAAC;AAAA;AAAA,EAGzE,IAAI,CAAC,gBAAgB,QAAQ,GAAG;AAAA,IAC/B,OAAO,EAAE,UAAU,QAAQ,QAAQ,MAAM,SAAS;AAAA,EACnD;AAAA,EAKA,MAAM,UAAU;AAAA,EAChB,MAAM,eAAe,QAAQ;AAAA,EAC7B,IAAI,CAAC,sBAAsB,KAAK,YAAY,GAAG;AAAA,IAC9C,MAAM,IAAI,MACT,2CAA2C,iDAC5C;AAAA,EACD;AAAA,EACA,IACC,OAAO,QAAQ,oBAAoB,YACnC,CAAC,iBAAiB,KAAK,QAAQ,eAAe,GAC7C;AAAA,IACD,MAAM,IAAI,MACT,kFACD;AAAA,EACD;AAAA,EACA,IAAI,QAAQ,cAAc,WAAW;AAAA,IACpC,IAAI,CAAC,QAAQ,cAAc;AAAA,MAC1B,MAAM,IAAI,MACT,+DACD;AAAA,IACD;AAAA,IACA,IAAI,WAAW;AAAA,IACf,IAAI;AAAA,MACH,WAAW,mCACV,SACA,QAAQ,YACT;AAAA,MACC,MAAM;AAAA,MACP,WAAW;AAAA;AAAA,IAEZ,IAAI,CAAC,UAAU;AAAA,MACd,MAAM,IAAI,MACT,0DACD;AAAA,IACD;AAAA,EACD;AAAA,EAEA,MAAM,WAAW,WACZ,MAAM,UACR,GAAG,KAAK,QAAQ,OAAO,EAAE,KAAK,gBAC9B,SACD,IACE,KAAK,MACN,MAAM,SAAS,KAAK,MAAM,YAAY,GAAG,MAAM,CAChD;AAAA,EAEF,MAAM,SAAS,YAAW,QAAQ,EAChC,OACA,KAAK,WACH,GAAG,WAAW,IAAI,QAAQ,OAAO,WAAW,MAC5C,QACD,CACD,CACD,EACC,OAAO,KAAK;AAAA,EACd,IAAI,WAAW,QAAQ,iBAAiB;AAAA,IACvC,MAAM,IAAI,MACT,gDAAgD,QAAQ,gDAAgD,QACzG;AAAA,EACD;AAAA,EAEA,OAAO,EAAE,UAAU,UAAU,QAAQ,QAAQ,MAAM,SAAS;AAAA;AAUtD,SAAS,cAAc,CAC7B,UACA,cACA,UACkB;AAAA,EAClB,IAAI;AAAA,IAAU,OAAO,EAAE,UAAU,OAAO,QAAQ,0BAA0B;AAAA,EAC1E,IAAI,CAAC,SAAS,WAAW;AAAA,IACxB,OAAO,EAAE,UAAU,OAAO,QAAQ,gCAAgC;AAAA,EACnE;AAAA,EACA,IAAI,CAAC,cAAc;AAAA,IAClB,OAAO;AAAA,MACN,UAAU;AAAA,MACV,QAAQ;AAAA,IACT;AAAA,EACD;AAAA,EACA,IAAI;AAAA,IACH,MAAM,WAAW,mCAChB,UACA,YACD;AAAA,IACA,OAAO;AAAA,MACN;AAAA,MACA,QAAQ,WAAW,YAAY;AAAA,IAChC;AAAA,IACC,OAAO,KAAK;AAAA,IACb,OAAO;AAAA,MACN,UAAU;AAAA,MACV,QAAQ,eAAe,QAAQ,IAAI,UAAU;AAAA,IAC9C;AAAA;AAAA;AAIF,IAAM,yBACL;AAED,eAAe,cAAc,CAAC,KAA0C;AAAA,EAEvE,IAAI,CAAC,IAAI,WAAW,UAAU;AAAA,IAAG;AAAA,EACjC,IAAI;AAAA,IACH,MAAM,WAAW,MAAM,MAAM,KAAK,EAAE,QAAQ,YAAY,QAAQ,GAAM,EAAE,CAAC;AAAA,IACzE,IAAI,CAAC,SAAS;AAAA,MAAI;AAAA,IAClB,MAAM,OAAQ,MAAM,SAAS,KAAK;AAAA,IAClC,OAAO,OAAO,KAAK,mBAAmB,WACnC,KAAK,iBACL;AAAA,IACF,MAAM;AAAA,IAEP;AAAA;AAAA;AAaF,eAAsB,uBAAuB,CAAC,OAM1B;AAAA,EACnB,IAAI,MAAM;AAAA,IAAa,OAAO,MAAM;AAAA,EACpC,IAAI,MAAM;AAAA,IAAQ,OAAO,MAAM;AAAA,EAC/B,IAAI,MAAM,gBAAgB;AAAA,IACzB,MAAM,SAAS,MAAM,eACpB,MAAM,gBAAgB,sBACvB;AAAA,IACA,IAAI;AAAA,MAAQ,OAAO;AAAA,EACpB;AAAA,EACA,OAAO;AAAA;",
|
|
11
11
|
"debugId": "001D083D88CAE76364756E2164756E21",
|
|
12
12
|
"names": []
|
|
13
13
|
}
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
*/
|
|
32
32
|
declare const ARCHIVE_STATUS_SCHEMA_VERSION = 1;
|
|
33
33
|
type ArchiveState = "fresh" | "lagging" | "stale" | "gap" | "failed-audit" | "source-unavailable";
|
|
34
|
+
/** The archive's state, coverage, and a plain-language reason. */
|
|
34
35
|
type ArchiveStatus = {
|
|
35
36
|
schema_version: typeof ARCHIVE_STATUS_SCHEMA_VERSION
|
|
36
37
|
network: string
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/archive/status.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"/**\n * `status.json` — the archive's operational truth, published short-cache from\n * verified state only.\n *\n * Everything else in the tree is immutable and signed, which makes it durable\n * but silent: a consumer holding a valid manifest cannot tell whether it is\n * current, three days behind, or the last thing published before an incident.\n * This object answers that, and it is the only object allowed to say something\n * unflattering.\n *\n * The distinction this file exists to protect:\n *\n * lagging → the archive trails the chain tip. This is CORRECT and permanent.\n * Two expected causes: twice-weekly publish (Wednesday and Sunday)\n * and the finality boundary. Cadence is the bulk of the gap;\n * finality is hours, not days. An archive that reported `stale`\n * for its own expected lag would be crying wolf forever, and\n * consumers would learn to ignore it.\n * stale → publishing stopped. The lag exceeds what cadence and the\n * finality rule can explain, so something is broken.\n *\n * `state` is derived from measurements, never asserted by the publisher, and\n * every unhealthy state is reachable — an archive that can only report health\n * is decoration.\n */\n\n/**\n * v1 is additive. `source.decoder_head` joined the object without a bump:\n * it does not change the meaning of any existing field, and readers already\n * ignore unknown keys. Bump only if a field is renamed, removed, or\n * re-interpreted.\n */\nexport const ARCHIVE_STATUS_SCHEMA_VERSION = 1;\n\nexport type ArchiveState =\n\t| \"fresh\"\n\t| \"lagging\"\n\t| \"stale\"\n\t| \"gap\"\n\t| \"failed-audit\"\n\t| \"source-unavailable\";\n\nexport type ArchiveStatus = {\n\tschema_version: typeof ARCHIVE_STATUS_SCHEMA_VERSION;\n\tnetwork: string;\n\tstate: ArchiveState;\n\t/** Human-facing one-liner explaining the state, including WHY. */\n\tdetail: string;\n\tgenerated_at: string;\n\tarchive: {\n\t\tsnapshot_digest: string | null;\n\t\tcoverage_to_block: number | null;\n\t\tpromoted_at: string | null;\n\t\tsigning_key_id: string | null;\n\t};\n\tsource: {\n\t\t/** Chain tip as the publisher last observed it. Null when unreachable. */\n\t\ttip_height: number | null;\n\t\t/** Highest height eligible to publish under the finality rule. */\n\t\tfinalized_height: number | null;\n\t\t/**\n\t\t * Highest height the Index decoder (`decode.ft_transfer.v1`) has\n\t\t * committed. Null when the checkpoint cannot be read. Distinct from\n\t\t * `archive.coverage_to_block`, which is the last signed publish.\n\t\t */\n\t\tdecoder_head: number | null;\n\t};\n\tlag: {\n\t\t/** Behind the FINALIZED height — the number that indicates a problem. */\n\t\tblocks_behind_finalized: number | null;\n\t\t/** Behind the raw chain tip — expected to be non-zero forever. */\n\t\tblocks_behind_tip: number | null;\n\t\tseconds_since_promotion: number | null;\n\t};\n\taudit: {\n\t\tcomplete: boolean;\n\t\tchecked_at: string;\n\t} | null;\n\tsignature?: string;\n\tkey_id?: string;\n};\n\nexport type StatusInputs = {\n\tnetwork: string;\n\tsnapshotDigest: string | null;\n\tcoverageToBlock: number | null;\n\tpromotedAt: string | null;\n\tsigningKeyId: string | null;\n\t/** Null when the source could not be reached. */\n\tsourceTipHeight: number | null;\n\tfinalizedHeight: number | null;\n\t/**\n\t * Index decoder checkpoint height. Null when unread or unreachable.\n\t * Reported, never used to derive `state`.\n\t */\n\tdecoderHead?: number | null;\n\taudit: { complete: boolean; checkedAt: string } | null;\n\tnow: Date;\n\t/**\n\t * How far behind the FINALIZED height the archive may fall before it is\n\t * stale rather than merely lagging. Publishing one partition covers 50k\n\t * blocks, so a healthy archive sits well inside this.\n\t */\n\tmaxBlocksBehindFinalized?: number;\n\t/** How long since the last promotion before staleness, regardless of height. */\n\tmaxSecondsSincePromotion?: number;\n};\n\nconst DEFAULT_MAX_BLOCKS_BEHIND_FINALIZED = 60_000;\n/**\n * Must exceed the publish cadence, or the archive reports `stale` on every\n * healthy cycle and the signal means nothing.\n *\n * The publisher runs Wed + Sun, so the longest gap between promotions in a\n * healthy week is 4 days. Five days leaves a day of grace for a slow export or\n * timer jitter, and still fires two days before the next scheduled attempt\n * whenever a cycle is actually missed.\n *\n * It also lands just inside the height rule: at the observed ~7–9k blocks/day\n * the archive is ~45k blocks behind finalized at the five-day mark, so age\n * trips first and reports the more diagnostic message (\"publishing stopped\")\n * rather than the symptom.\n */\nconst DEFAULT_MAX_SECONDS_SINCE_PROMOTION = 5 * 24 * 3_600;\n\n/** systemd `OnCalendar=Wed,Sun *-*-* 08:00:00`. Host is UTC. Job start, not promotion landing. */\nconst PUBLISH_HOUR_UTC = 8;\nconst PUBLISH_WEEKDAYS_UTC = new Set([0, 3]);\n\n/**\n * Next twice-weekly publish fire strictly after `now`. Matches\n * `secondlayer-archive-publish.timer`. Export duration means the promotion\n * lands hours later; this is the scheduled start.\n */\nexport function nextScheduledArchivePublish(now: Date): Date {\n\tfor (let i = 0; i <= 7; i++) {\n\t\tconst candidate = new Date(\n\t\t\tDate.UTC(\n\t\t\t\tnow.getUTCFullYear(),\n\t\t\t\tnow.getUTCMonth(),\n\t\t\t\tnow.getUTCDate() + i,\n\t\t\t\tPUBLISH_HOUR_UTC,\n\t\t\t\t0,\n\t\t\t\t0,\n\t\t\t\t0,\n\t\t\t),\n\t\t);\n\t\tif (PUBLISH_WEEKDAYS_UTC.has(candidate.getUTCDay()) && candidate > now) {\n\t\t\treturn candidate;\n\t\t}\n\t}\n\tthrow new Error(\"nextScheduledArchivePublish: no fire in 7 days\");\n}\n\nfunction publishWeekdayName(date: Date): \"Sunday\" | \"Wednesday\" {\n\treturn date.getUTCDay() === 0 ? \"Sunday\" : \"Wednesday\";\n}\n\n/**\n * Durations here run to days, and \"beyond the 120h objective\" makes a reader do\n * arithmetic before they can tell whether that is alarming.\n */\nfunction formatAge(seconds: number): string {\n\tconst hours = seconds / 3_600;\n\tif (hours < 48) return `${Math.round(hours)}h`;\n\tconst days = hours / 24;\n\t// One decimal only when it changes the reading: 4d and 4.5d are different\n\t// facts during an incident, 5.0d is noise.\n\tconst rounded = Math.round(days * 10) / 10;\n\treturn `${Number.isInteger(rounded) ? rounded : rounded.toFixed(1)}d`;\n}\n\n/**\n * Derive status from measurements. Pure, so every state below is reachable in\n * a test rather than only in production at 3am.\n *\n * Order matters: a failing audit outranks freshness, because an archive that\n * is current and wrong is worse than one that is behind and correct.\n */\nexport function deriveArchiveStatus(inputs: StatusInputs): ArchiveStatus {\n\tconst maxBehind =\n\t\tinputs.maxBlocksBehindFinalized ?? DEFAULT_MAX_BLOCKS_BEHIND_FINALIZED;\n\tconst maxAge =\n\t\tinputs.maxSecondsSincePromotion ?? DEFAULT_MAX_SECONDS_SINCE_PROMOTION;\n\n\tconst secondsSincePromotion = inputs.promotedAt\n\t\t? Math.max(\n\t\t\t\t0,\n\t\t\t\tMath.round(\n\t\t\t\t\t(inputs.now.getTime() - Date.parse(inputs.promotedAt)) / 1000,\n\t\t\t\t),\n\t\t\t)\n\t\t: null;\n\n\tconst behindFinalized =\n\t\tinputs.finalizedHeight !== null && inputs.coverageToBlock !== null\n\t\t\t? Math.max(0, inputs.finalizedHeight - inputs.coverageToBlock)\n\t\t\t: null;\n\tconst behindTip =\n\t\tinputs.sourceTipHeight !== null && inputs.coverageToBlock !== null\n\t\t\t? Math.max(0, inputs.sourceTipHeight - inputs.coverageToBlock)\n\t\t\t: null;\n\n\tconst base = {\n\t\tschema_version:\n\t\t\tARCHIVE_STATUS_SCHEMA_VERSION as typeof ARCHIVE_STATUS_SCHEMA_VERSION,\n\t\tnetwork: inputs.network,\n\t\tgenerated_at: inputs.now.toISOString(),\n\t\tarchive: {\n\t\t\tsnapshot_digest: inputs.snapshotDigest,\n\t\t\tcoverage_to_block: inputs.coverageToBlock,\n\t\t\tpromoted_at: inputs.promotedAt,\n\t\t\tsigning_key_id: inputs.signingKeyId,\n\t\t},\n\t\tsource: {\n\t\t\ttip_height: inputs.sourceTipHeight,\n\t\t\tfinalized_height: inputs.finalizedHeight,\n\t\t\tdecoder_head: inputs.decoderHead ?? null,\n\t\t},\n\t\tlag: {\n\t\t\tblocks_behind_finalized: behindFinalized,\n\t\t\tblocks_behind_tip: behindTip,\n\t\t\tseconds_since_promotion: secondsSincePromotion,\n\t\t},\n\t\taudit: inputs.audit\n\t\t\t? { complete: inputs.audit.complete, checked_at: inputs.audit.checkedAt }\n\t\t\t: null,\n\t};\n\n\t// A failed audit is the loudest signal: current-and-wrong beats behind-and-right.\n\tif (inputs.audit && !inputs.audit.complete) {\n\t\treturn {\n\t\t\t...base,\n\t\t\tstate: \"failed-audit\",\n\t\t\tdetail: `the canonical audit did not pass at ${inputs.audit.checkedAt}; this archive is not safe to restore from`,\n\t\t};\n\t}\n\n\tif (!inputs.snapshotDigest || inputs.coverageToBlock === null) {\n\t\treturn {\n\t\t\t...base,\n\t\t\tstate: \"gap\",\n\t\t\tdetail: \"no snapshot is currently promoted\",\n\t\t};\n\t}\n\n\t// Source unreachable: report the gap in KNOWLEDGE rather than guessing. The\n\t// archive may be perfectly healthy; we cannot currently say so.\n\tif (inputs.sourceTipHeight === null || inputs.finalizedHeight === null) {\n\t\treturn {\n\t\t\t...base,\n\t\t\tstate: \"source-unavailable\",\n\t\t\tdetail:\n\t\t\t\t\"the chain source could not be reached, so freshness cannot be determined\",\n\t\t};\n\t}\n\n\tif (secondsSincePromotion !== null && secondsSincePromotion > maxAge) {\n\t\treturn {\n\t\t\t...base,\n\t\t\tstate: \"stale\",\n\t\t\tdetail: `last promotion was ${formatAge(secondsSincePromotion)} ago, beyond the ${formatAge(maxAge)} freshness objective`,\n\t\t};\n\t}\n\n\tif (behindFinalized !== null && behindFinalized > maxBehind) {\n\t\treturn {\n\t\t\t...base,\n\t\t\tstate: \"stale\",\n\t\t\tdetail: `${behindFinalized} blocks behind the finalized height, beyond the ${maxBehind} threshold`,\n\t\t};\n\t}\n\n\t// Behind the tip. CORRECT and permanent: twice-weekly publish plus the\n\t// finality boundary. Cadence is the bulk of the gap. Do not call this stale.\n\tif (behindTip !== null && behindTip > 0) {\n\t\tconst nextDay = publishWeekdayName(nextScheduledArchivePublish(inputs.now));\n\t\treturn {\n\t\t\t...base,\n\t\t\tstate: \"lagging\",\n\t\t\tdetail: `${behindTip} blocks behind the chain tip, which is expected: publishing is twice weekly (Wednesday and Sunday, next ${nextDay}) and stops at the finality boundary`,\n\t\t};\n\t}\n\n\treturn {\n\t\t...base,\n\t\tstate: \"fresh\",\n\t\tdetail: \"published through the finalized height\",\n\t};\n}\n\n/** States a consumer should refuse to restore from. */\nexport function isRestorable(state: ArchiveState): boolean {\n\treturn state === \"fresh\" || state === \"lagging\";\n}\n"
|
|
5
|
+
"/**\n * `status.json` — the archive's operational truth, published short-cache from\n * verified state only.\n *\n * Everything else in the tree is immutable and signed, which makes it durable\n * but silent: a consumer holding a valid manifest cannot tell whether it is\n * current, three days behind, or the last thing published before an incident.\n * This object answers that, and it is the only object allowed to say something\n * unflattering.\n *\n * The distinction this file exists to protect:\n *\n * lagging → the archive trails the chain tip. This is CORRECT and permanent.\n * Two expected causes: twice-weekly publish (Wednesday and Sunday)\n * and the finality boundary. Cadence is the bulk of the gap;\n * finality is hours, not days. An archive that reported `stale`\n * for its own expected lag would be crying wolf forever, and\n * consumers would learn to ignore it.\n * stale → publishing stopped. The lag exceeds what cadence and the\n * finality rule can explain, so something is broken.\n *\n * `state` is derived from measurements, never asserted by the publisher, and\n * every unhealthy state is reachable — an archive that can only report health\n * is decoration.\n */\n\n/**\n * v1 is additive. `source.decoder_head` joined the object without a bump:\n * it does not change the meaning of any existing field, and readers already\n * ignore unknown keys. Bump only if a field is renamed, removed, or\n * re-interpreted.\n */\nexport const ARCHIVE_STATUS_SCHEMA_VERSION = 1;\n\nexport type ArchiveState =\n\t| \"fresh\"\n\t| \"lagging\"\n\t| \"stale\"\n\t| \"gap\"\n\t| \"failed-audit\"\n\t| \"source-unavailable\";\n\n/** The archive's state, coverage, and a plain-language reason. */\nexport type ArchiveStatus = {\n\tschema_version: typeof ARCHIVE_STATUS_SCHEMA_VERSION;\n\tnetwork: string;\n\tstate: ArchiveState;\n\t/** Human-facing one-liner explaining the state, including WHY. */\n\tdetail: string;\n\tgenerated_at: string;\n\tarchive: {\n\t\tsnapshot_digest: string | null;\n\t\tcoverage_to_block: number | null;\n\t\tpromoted_at: string | null;\n\t\tsigning_key_id: string | null;\n\t};\n\tsource: {\n\t\t/** Chain tip as the publisher last observed it. Null when unreachable. */\n\t\ttip_height: number | null;\n\t\t/** Highest height eligible to publish under the finality rule. */\n\t\tfinalized_height: number | null;\n\t\t/**\n\t\t * Highest height the Index decoder (`decode.ft_transfer.v1`) has\n\t\t * committed. Null when the checkpoint cannot be read. Distinct from\n\t\t * `archive.coverage_to_block`, which is the last signed publish.\n\t\t */\n\t\tdecoder_head: number | null;\n\t};\n\tlag: {\n\t\t/** Behind the FINALIZED height — the number that indicates a problem. */\n\t\tblocks_behind_finalized: number | null;\n\t\t/** Behind the raw chain tip — expected to be non-zero forever. */\n\t\tblocks_behind_tip: number | null;\n\t\tseconds_since_promotion: number | null;\n\t};\n\taudit: {\n\t\tcomplete: boolean;\n\t\tchecked_at: string;\n\t} | null;\n\tsignature?: string;\n\tkey_id?: string;\n};\n\nexport type StatusInputs = {\n\tnetwork: string;\n\tsnapshotDigest: string | null;\n\tcoverageToBlock: number | null;\n\tpromotedAt: string | null;\n\tsigningKeyId: string | null;\n\t/** Null when the source could not be reached. */\n\tsourceTipHeight: number | null;\n\tfinalizedHeight: number | null;\n\t/**\n\t * Index decoder checkpoint height. Null when unread or unreachable.\n\t * Reported, never used to derive `state`.\n\t */\n\tdecoderHead?: number | null;\n\taudit: { complete: boolean; checkedAt: string } | null;\n\tnow: Date;\n\t/**\n\t * How far behind the FINALIZED height the archive may fall before it is\n\t * stale rather than merely lagging. Publishing one partition covers 50k\n\t * blocks, so a healthy archive sits well inside this.\n\t */\n\tmaxBlocksBehindFinalized?: number;\n\t/** How long since the last promotion before staleness, regardless of height. */\n\tmaxSecondsSincePromotion?: number;\n};\n\nconst DEFAULT_MAX_BLOCKS_BEHIND_FINALIZED = 60_000;\n/**\n * Must exceed the publish cadence, or the archive reports `stale` on every\n * healthy cycle and the signal means nothing.\n *\n * The publisher runs Wed + Sun, so the longest gap between promotions in a\n * healthy week is 4 days. Five days leaves a day of grace for a slow export or\n * timer jitter, and still fires two days before the next scheduled attempt\n * whenever a cycle is actually missed.\n *\n * It also lands just inside the height rule: at the observed ~7–9k blocks/day\n * the archive is ~45k blocks behind finalized at the five-day mark, so age\n * trips first and reports the more diagnostic message (\"publishing stopped\")\n * rather than the symptom.\n */\nconst DEFAULT_MAX_SECONDS_SINCE_PROMOTION = 5 * 24 * 3_600;\n\n/** systemd `OnCalendar=Wed,Sun *-*-* 08:00:00`. Host is UTC. Job start, not promotion landing. */\nconst PUBLISH_HOUR_UTC = 8;\nconst PUBLISH_WEEKDAYS_UTC = new Set([0, 3]);\n\n/**\n * Next twice-weekly publish fire strictly after `now`. Matches\n * `secondlayer-archive-publish.timer`. Export duration means the promotion\n * lands hours later; this is the scheduled start.\n */\nexport function nextScheduledArchivePublish(now: Date): Date {\n\tfor (let i = 0; i <= 7; i++) {\n\t\tconst candidate = new Date(\n\t\t\tDate.UTC(\n\t\t\t\tnow.getUTCFullYear(),\n\t\t\t\tnow.getUTCMonth(),\n\t\t\t\tnow.getUTCDate() + i,\n\t\t\t\tPUBLISH_HOUR_UTC,\n\t\t\t\t0,\n\t\t\t\t0,\n\t\t\t\t0,\n\t\t\t),\n\t\t);\n\t\tif (PUBLISH_WEEKDAYS_UTC.has(candidate.getUTCDay()) && candidate > now) {\n\t\t\treturn candidate;\n\t\t}\n\t}\n\tthrow new Error(\"nextScheduledArchivePublish: no fire in 7 days\");\n}\n\nfunction publishWeekdayName(date: Date): \"Sunday\" | \"Wednesday\" {\n\treturn date.getUTCDay() === 0 ? \"Sunday\" : \"Wednesday\";\n}\n\n/**\n * Durations here run to days, and \"beyond the 120h objective\" makes a reader do\n * arithmetic before they can tell whether that is alarming.\n */\nfunction formatAge(seconds: number): string {\n\tconst hours = seconds / 3_600;\n\tif (hours < 48) return `${Math.round(hours)}h`;\n\tconst days = hours / 24;\n\t// One decimal only when it changes the reading: 4d and 4.5d are different\n\t// facts during an incident, 5.0d is noise.\n\tconst rounded = Math.round(days * 10) / 10;\n\treturn `${Number.isInteger(rounded) ? rounded : rounded.toFixed(1)}d`;\n}\n\n/**\n * Derive status from measurements. Pure, so every state below is reachable in\n * a test rather than only in production at 3am.\n *\n * Order matters: a failing audit outranks freshness, because an archive that\n * is current and wrong is worse than one that is behind and correct.\n */\nexport function deriveArchiveStatus(inputs: StatusInputs): ArchiveStatus {\n\tconst maxBehind =\n\t\tinputs.maxBlocksBehindFinalized ?? DEFAULT_MAX_BLOCKS_BEHIND_FINALIZED;\n\tconst maxAge =\n\t\tinputs.maxSecondsSincePromotion ?? DEFAULT_MAX_SECONDS_SINCE_PROMOTION;\n\n\tconst secondsSincePromotion = inputs.promotedAt\n\t\t? Math.max(\n\t\t\t\t0,\n\t\t\t\tMath.round(\n\t\t\t\t\t(inputs.now.getTime() - Date.parse(inputs.promotedAt)) / 1000,\n\t\t\t\t),\n\t\t\t)\n\t\t: null;\n\n\tconst behindFinalized =\n\t\tinputs.finalizedHeight !== null && inputs.coverageToBlock !== null\n\t\t\t? Math.max(0, inputs.finalizedHeight - inputs.coverageToBlock)\n\t\t\t: null;\n\tconst behindTip =\n\t\tinputs.sourceTipHeight !== null && inputs.coverageToBlock !== null\n\t\t\t? Math.max(0, inputs.sourceTipHeight - inputs.coverageToBlock)\n\t\t\t: null;\n\n\tconst base = {\n\t\tschema_version:\n\t\t\tARCHIVE_STATUS_SCHEMA_VERSION as typeof ARCHIVE_STATUS_SCHEMA_VERSION,\n\t\tnetwork: inputs.network,\n\t\tgenerated_at: inputs.now.toISOString(),\n\t\tarchive: {\n\t\t\tsnapshot_digest: inputs.snapshotDigest,\n\t\t\tcoverage_to_block: inputs.coverageToBlock,\n\t\t\tpromoted_at: inputs.promotedAt,\n\t\t\tsigning_key_id: inputs.signingKeyId,\n\t\t},\n\t\tsource: {\n\t\t\ttip_height: inputs.sourceTipHeight,\n\t\t\tfinalized_height: inputs.finalizedHeight,\n\t\t\tdecoder_head: inputs.decoderHead ?? null,\n\t\t},\n\t\tlag: {\n\t\t\tblocks_behind_finalized: behindFinalized,\n\t\t\tblocks_behind_tip: behindTip,\n\t\t\tseconds_since_promotion: secondsSincePromotion,\n\t\t},\n\t\taudit: inputs.audit\n\t\t\t? { complete: inputs.audit.complete, checked_at: inputs.audit.checkedAt }\n\t\t\t: null,\n\t};\n\n\t// A failed audit is the loudest signal: current-and-wrong beats behind-and-right.\n\tif (inputs.audit && !inputs.audit.complete) {\n\t\treturn {\n\t\t\t...base,\n\t\t\tstate: \"failed-audit\",\n\t\t\tdetail: `the canonical audit did not pass at ${inputs.audit.checkedAt}; this archive is not safe to restore from`,\n\t\t};\n\t}\n\n\tif (!inputs.snapshotDigest || inputs.coverageToBlock === null) {\n\t\treturn {\n\t\t\t...base,\n\t\t\tstate: \"gap\",\n\t\t\tdetail: \"no snapshot is currently promoted\",\n\t\t};\n\t}\n\n\t// Source unreachable: report the gap in KNOWLEDGE rather than guessing. The\n\t// archive may be perfectly healthy; we cannot currently say so.\n\tif (inputs.sourceTipHeight === null || inputs.finalizedHeight === null) {\n\t\treturn {\n\t\t\t...base,\n\t\t\tstate: \"source-unavailable\",\n\t\t\tdetail:\n\t\t\t\t\"the chain source could not be reached, so freshness cannot be determined\",\n\t\t};\n\t}\n\n\tif (secondsSincePromotion !== null && secondsSincePromotion > maxAge) {\n\t\treturn {\n\t\t\t...base,\n\t\t\tstate: \"stale\",\n\t\t\tdetail: `last promotion was ${formatAge(secondsSincePromotion)} ago, beyond the ${formatAge(maxAge)} freshness objective`,\n\t\t};\n\t}\n\n\tif (behindFinalized !== null && behindFinalized > maxBehind) {\n\t\treturn {\n\t\t\t...base,\n\t\t\tstate: \"stale\",\n\t\t\tdetail: `${behindFinalized} blocks behind the finalized height, beyond the ${maxBehind} threshold`,\n\t\t};\n\t}\n\n\t// Behind the tip. CORRECT and permanent: twice-weekly publish plus the\n\t// finality boundary. Cadence is the bulk of the gap. Do not call this stale.\n\tif (behindTip !== null && behindTip > 0) {\n\t\tconst nextDay = publishWeekdayName(nextScheduledArchivePublish(inputs.now));\n\t\treturn {\n\t\t\t...base,\n\t\t\tstate: \"lagging\",\n\t\t\tdetail: `${behindTip} blocks behind the chain tip, which is expected: publishing is twice weekly (Wednesday and Sunday, next ${nextDay}) and stops at the finality boundary`,\n\t\t};\n\t}\n\n\treturn {\n\t\t...base,\n\t\tstate: \"fresh\",\n\t\tdetail: \"published through the finalized height\",\n\t};\n}\n\n/** States a consumer should refuse to restore from. */\nexport function isRestorable(state: ArchiveState): boolean {\n\treturn state === \"fresh\" || state === \"lagging\";\n}\n"
|
|
6
6
|
],
|
|
7
|
-
"mappings": ";AAgCO,IAAM,gCAAgC;
|
|
7
|
+
"mappings": "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",
|
|
8
8
|
"debugId": "779DB5C9B354565F64756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
"sourcesContent": [
|
|
5
5
|
"import { type Kysely, sql } from \"kysely\";\nimport { isPlatformMode } from \"../../mode.ts\";\nimport {\n\tBILLING_PAUSED_PREFIX,\n\ttype SubgraphVerification,\n} from \"../../schemas/subgraphs.ts\";\nimport { jsonb } from \"../jsonb.ts\";\nimport type { Database, Subgraph } from \"../types.ts\";\n\n/** Convert a subgraph name to its PostgreSQL schema name. */\nexport function pgSchemaName(subgraphName: string): string {\n\tconst safeName = subgraphName.replace(/-/g, \"_\");\n\treturn `subgraph_${safeName}`;\n}\n\n/** OSS writes empty account_id — name is the local unique key. */\nexport function localSubgraphAccountId(accountId?: string): string {\n\tif (!isPlatformMode()) return \"\";\n\treturn accountId ?? \"\";\n}\n\nexport async function registerSubgraph(\n\tdb: Kysely<Database>,\n\tdata: {\n\t\tname: string;\n\t\tversion: string;\n\t\tdefinition: Record<string, unknown>;\n\t\tschemaHash: string;\n\t\thandlerPath: string;\n\t\tapiKeyId?: string;\n\t\taccountId?: string;\n\t\tschemaName?: string;\n\t\tstartBlock?: number;\n\t\thandlerCode?: string;\n\t\tsourceCode?: string;\n\t\tpin?: string | null;\n\t\tpinPreimage?: string | null;\n\t\tverification?: SubgraphVerification | null;\n\t},\n): Promise<Subgraph> {\n\tconst accountId = localSubgraphAccountId(data.accountId);\n\tconst values = {\n\t\tname: data.name,\n\t\tversion: data.version,\n\t\tdefinition: jsonb<Record<string, unknown>>(data.definition),\n\t\tschema_hash: data.schemaHash,\n\t\thandler_path: data.handlerPath,\n\t\taccount_id: accountId,\n\t\thandler_code: data.handlerCode ?? null,\n\t\tsource_code: data.sourceCode ?? null,\n\t\tschema_name: data.schemaName ?? null,\n\t\tstart_block: data.startBlock ?? 0,\n\t\tpin: data.pin ?? null,\n\t\tpin_preimage: data.pinPreimage ?? null,\n\t\tverification: data.verification\n\t\t\t? jsonb<SubgraphVerification>(data.verification)\n\t\t\t: null,\n\t};\n\tconst updateSet = {\n\t\tversion: data.version,\n\t\tdefinition: jsonb<Record<string, unknown>>(data.definition),\n\t\tschema_hash: data.schemaHash,\n\t\thandler_path: data.handlerPath,\n\t\thandler_code: data.handlerCode ?? null,\n\t\tsource_code: data.sourceCode ?? null,\n\t\tschema_name: data.schemaName ?? null,\n\t\tstart_block: data.startBlock ?? 0,\n\t\tpin: values.pin,\n\t\tpin_preimage: values.pin_preimage,\n\t\tverification: values.verification,\n\t\tupdated_at: new Date(),\n\t};\n\n\t// OSS: name is unique. Upsert by name so leftover rows with a non-empty\n\t// account_id (pre-local-namespace deploys) don't insert a second row.\n\tif (!isPlatformMode()) {\n\t\tconst existing = await db\n\t\t\t.selectFrom(\"subgraphs\")\n\t\t\t.select(\"id\")\n\t\t\t.where(\"name\", \"=\", data.name)\n\t\t\t.executeTakeFirst();\n\t\tif (existing) {\n\t\t\treturn await db\n\t\t\t\t.updateTable(\"subgraphs\")\n\t\t\t\t.set({ ...updateSet, account_id: \"\" })\n\t\t\t\t.where(\"id\", \"=\", existing.id)\n\t\t\t\t.returningAll()\n\t\t\t\t.executeTakeFirstOrThrow();\n\t\t}\n\t\treturn await db\n\t\t\t.insertInto(\"subgraphs\")\n\t\t\t.values(values)\n\t\t\t.returningAll()\n\t\t\t.executeTakeFirstOrThrow();\n\t}\n\n\treturn await db\n\t\t.insertInto(\"subgraphs\")\n\t\t.values(values)\n\t\t.onConflict((oc) =>\n\t\t\toc.columns([\"name\", \"account_id\"]).doUpdateSet(updateSet),\n\t\t)\n\t\t.returningAll()\n\t\t.executeTakeFirstOrThrow();\n}\n\nexport async function getSubgraph(\n\tdb: Kysely<Database>,\n\tname: string,\n\taccountId?: string,\n): Promise<Subgraph | null> {\n\tlet query = db.selectFrom(\"subgraphs\").selectAll().where(\"name\", \"=\", name);\n\n\tif (isPlatformMode() && accountId !== undefined) {\n\t\tquery = query.where(\"account_id\", \"=\", accountId);\n\t}\n\n\treturn (await query.executeTakeFirst()) ?? null;\n}\n\nexport async function listSubgraphs(\n\tdb: Kysely<Database>,\n\taccountId?: string,\n): Promise<Subgraph[]> {\n\tlet query = db.selectFrom(\"subgraphs\").selectAll();\n\tif (isPlatformMode() && accountId !== undefined) {\n\t\tquery = query.where(\"account_id\", \"=\", accountId);\n\t}\n\treturn query.execute();\n}\n\nexport async function updateSubgraphStatus(\n\tdb: Kysely<Database>,\n\tname: string,\n\tstatus: string,\n\tlastProcessedBlock?: number,\n): Promise<void> {\n\tawait db\n\t\t.updateTable(\"subgraphs\")\n\t\t.set({\n\t\t\tstatus,\n\t\t\t...(lastProcessedBlock !== undefined\n\t\t\t\t? { last_processed_block: lastProcessedBlock }\n\t\t\t\t: {}),\n\t\t\tupdated_at: new Date(),\n\t\t})\n\t\t.where(\"name\", \"=\", name)\n\t\t.execute();\n}\n\n/** Live-walk ERROR write (f069): a block whose handlers ALL failed\n * (`processed === 0 && errors > 0`, `block-processor.ts`'s `applyProgress`)\n * is stamped 'error' and its height is recorded as processed, same as\n * `updateSubgraphStatus(db, name, \"error\", blockHeight)` — except the\n * cursor half of that write now goes through the same conditional `<`\n * guard as {@link recordLiveProgress}. Marking a subgraph 'error' is not\n * itself replay-sensitive, but it shares the `last_processed_block` column\n * with the replay guard: an unconditional write here could regress the\n * cursor a racing (successful) writer already advanced past, which is\n * exactly the regress-then-re-walk loop this guard exists to close. In\n * practice `ctx.rollbackTo` (`runner.ts`) means an all-failed block never\n * flushes writes, so a lost race here is always a silent no-op, never a\n * case that needs `CursorRaceLostError`-style tx abort. Returns whether\n * the write applied. Live path only — reindex/backfill's error stamping\n * goes through {@link updateSubgraphStatus} unconditionally, as before. */\nexport async function recordLiveError(\n\tdb: Kysely<Database>,\n\tname: string,\n\tblockHeight: number,\n): Promise<boolean> {\n\tconst result = await db\n\t\t.updateTable(\"subgraphs\")\n\t\t.set({\n\t\t\tstatus: \"error\",\n\t\t\tlast_processed_block: blockHeight,\n\t\t\tupdated_at: new Date(),\n\t\t})\n\t\t.where(\"name\", \"=\", name)\n\t\t.where(\"last_processed_block\", \"<\", blockHeight)\n\t\t.executeTakeFirst();\n\treturn Number(result.numUpdatedRows ?? 0n) > 0;\n}\n\nexport async function recordSubgraphProcessed(\n\tdb: Kysely<Database>,\n\tname: string,\n\tprocessed: number,\n\terrors: number,\n\tlastError?: string,\n): Promise<void> {\n\tawait db\n\t\t.updateTable(\"subgraphs\")\n\t\t.set({\n\t\t\ttotal_processed: sql`total_processed + ${processed}`,\n\t\t\ttotal_errors: sql`total_errors + ${errors}`,\n\t\t\t...(lastError\n\t\t\t\t? { last_error: lastError, last_error_at: new Date() }\n\t\t\t\t: {}),\n\t\t\tupdated_at: new Date(),\n\t\t})\n\t\t.where(\"name\", \"=\", name)\n\t\t.execute();\n}\n\n/** Record a billing pause on the existing health fields. Status and counters\n * are untouched: a paused subgraph is healthy, just waiting on its account. */\nexport async function markSubgraphBillingPaused(\n\tdb: Kysely<Database>,\n\tname: string,\n\tcode: string,\n): Promise<void> {\n\tawait db\n\t\t.updateTable(\"subgraphs\")\n\t\t.set({\n\t\t\tlast_error: `${BILLING_PAUSED_PREFIX}${code}`,\n\t\t\tlast_error_at: new Date(),\n\t\t\tupdated_at: new Date(),\n\t\t})\n\t\t.where(\"name\", \"=\", name)\n\t\t.execute();\n}\n\n/** Clear a billing pause once reads succeed again. Leaves any other\n * `last_error` alone. */\nexport async function clearSubgraphBillingPaused(\n\tdb: Kysely<Database>,\n\tname: string,\n): Promise<void> {\n\tawait db\n\t\t.updateTable(\"subgraphs\")\n\t\t.set({ last_error: null, last_error_at: null, updated_at: new Date() })\n\t\t.where(\"name\", \"=\", name)\n\t\t.where(\"last_error\", \"like\", `${BILLING_PAUSED_PREFIX}%`)\n\t\t.execute();\n}\n\nexport async function updateSubgraphHandlerPath(\n\tdb: Kysely<Database>,\n\tname: string,\n\thandlerPath: string,\n\topts?: {\n\t\thandlerCode?: string;\n\t\tsourceCode?: string;\n\t\tpin?: string | null;\n\t\tpinPreimage?: string | null;\n\t\tverification?: SubgraphVerification | null;\n\t},\n): Promise<void> {\n\tawait db\n\t\t.updateTable(\"subgraphs\")\n\t\t.set({\n\t\t\thandler_path: handlerPath,\n\t\t\t...(opts?.handlerCode != null ? { handler_code: opts.handlerCode } : {}),\n\t\t\t...(opts?.sourceCode != null ? { source_code: opts.sourceCode } : {}),\n\t\t\t...(opts?.pin !== undefined ? { pin: opts.pin } : {}),\n\t\t\t...(opts?.pinPreimage !== undefined\n\t\t\t\t? { pin_preimage: opts.pinPreimage }\n\t\t\t\t: {}),\n\t\t\t...(opts?.verification !== undefined\n\t\t\t\t? {\n\t\t\t\t\t\tverification: opts.verification\n\t\t\t\t\t\t\t? jsonb<SubgraphVerification>(opts.verification)\n\t\t\t\t\t\t\t: null,\n\t\t\t\t\t}\n\t\t\t\t: {}),\n\t\t\tupdated_at: new Date(),\n\t\t})\n\t\t.where(\"name\", \"=\", name)\n\t\t.execute();\n}\n\nexport async function deleteSubgraph(\n\tdb: Kysely<Database>,\n\tname: string,\n\taccountId?: string,\n): Promise<Subgraph | null> {\n\tconst subgraph = await getSubgraph(db, name, accountId);\n\tif (!subgraph) return null;\n\n\t// Use stored schema_name if available, otherwise compute\n\tconst schemaName = subgraph.schema_name ?? pgSchemaName(name);\n\n\t// Drop the subgraph's schema (CASCADE drops all tables within).\n\tawait sql`DROP SCHEMA IF EXISTS ${sql.raw(`\"${schemaName}\"`)} CASCADE`.execute(\n\t\tdb,\n\t);\n\n\t// Remove from registry\n\tawait db.deleteFrom(\"subgraphs\").where(\"id\", \"=\", subgraph.id).execute();\n\n\treturn subgraph;\n}\n\n/** Set or clear a paid subgraph's expiry (NULL = no expiry, e.g. on claim). */\nexport async function updateSubgraphExpiry(\n\tdb: Kysely<Database>,\n\tname: string,\n\taccountId: string,\n\texpiresAt: Date | null,\n): Promise<void> {\n\tawait db\n\t\t.updateTable(\"subgraphs\")\n\t\t.set({ expires_at: expiresAt })\n\t\t.where(\"name\", \"=\", name)\n\t\t.where(\"account_id\", \"=\", accountId)\n\t\t.execute();\n}\n\n/** Live-walk progress write: advances the cursor and PROMOTES toward\n * 'active' (deploying/error → active keeps reorg eligibility, reorg.ts gates\n * on it) but never overwrites an explicit 'reindexing' park — unconditional\n * status stamping let catch-up flap a parked subgraph back into its own path\n * per block, fighting the queued reindex op.\n *\n * `last_processed_block` is now written CONDITIONALLY — `WHERE\n * last_processed_block < lastProcessedBlock`, mirroring\n * `advanceOperationCursor`'s (`subgraph-operations.ts`) comparison shape —\n * and returns whether THIS call actually advanced it. This used to be\n * unconditional by design, on the premise that the only two writers able to\n * advance/rewind a given subgraph's cursor concurrently (the catch-up walk\n * and the reorg handler) serialize on an in-process per-subgraph lock +\n * reorg epoch guard (`packages/subgraphs/src/runtime/catchup.ts`,\n * `subgraphBlockLockHeld` / `reorgEpoch`; f057). That guard is in-process\n * only. The f068 investigation\n * (`docs/internal/audits/asset-holdings-unreproducible-balances-f068.md`)\n * found a cross-process sequence it does not cover: a catch-up leader that\n * loses its advisory-lock connection mid-walk keeps writing (leadership is\n * checked only at walk entry, not per-iteration) while a new leader takes\n * over and walks the same range — both commit increments for overlapping\n * heights, and every regressed cursor write from the laggard re-triggers a\n * full re-walk, growing per-height application without bound. The\n * conditional advance here (paired with the fast-path replay guard armed on\n * the live path in `block-processor.ts`) is now the cross-process\n * guarantee: a block at/below the current cursor can never re-advance it,\n * so a laggard's commits can no longer regress-then-retrigger.\n *\n * This conditionality is why the reorg rewind can no longer go through this\n * function — a rewind's whole point is to move the cursor *backward*, which\n * the `<` guard would silently refuse. Reorg mode calls\n * {@link rewindLiveProgress} instead, which keeps the old unconditional\n * semantics and is safe specifically because the reorg path takes a\n * transaction-scoped advisory lock (`reorg.ts`) other writers can't cross. */\nexport async function recordLiveProgress(\n\tdb: Kysely<Database>,\n\tname: string,\n\tlastProcessedBlock: number,\n): Promise<boolean> {\n\tconst result = await db\n\t\t.updateTable(\"subgraphs\")\n\t\t.set({\n\t\t\tlast_processed_block: lastProcessedBlock,\n\t\t\tstatus: sql`CASE WHEN status = 'reindexing' THEN status ELSE 'active' END`,\n\t\t\tupdated_at: new Date(),\n\t\t})\n\t\t.where(\"name\", \"=\", name)\n\t\t.where(\"last_processed_block\", \"<\", lastProcessedBlock)\n\t\t.executeTakeFirst();\n\treturn Number(result.numUpdatedRows ?? 0n) > 0;\n}\n\n/** Reorg-rewind progress write: the OLD unconditional `recordLiveProgress`\n * semantics, verbatim — moves `last_processed_block` to the fork height\n * regardless of its current value, because that is by definition a\n * backward move the conditional `<` guard in `recordLiveProgress` would\n * refuse. Same status-promotion CASE (never unparks an explicit\n * 'reindexing').\n *\n * Callers: exclusively the subgraph-reorg path\n * (`packages/subgraphs/src/runtime/reorg.ts`, via `block-processor.ts`'s\n * `reorgRewind` mode), which is safe from the cross-process race this\n * function reintroduces because it holds a transaction-scoped Postgres\n * advisory lock (`pg_advisory_xact_lock`) for the whole\n * delete+reprocess+rewind sequence — no other writer can interleave a\n * forward write between the rewind and the fork block's reprocess. Do not\n * call this from the live catch-up path. */\nexport async function rewindLiveProgress(\n\tdb: Kysely<Database>,\n\tname: string,\n\tlastProcessedBlock: number,\n): Promise<void> {\n\tawait db\n\t\t.updateTable(\"subgraphs\")\n\t\t.set({\n\t\t\tlast_processed_block: lastProcessedBlock,\n\t\t\tstatus: sql`CASE WHEN status = 'reindexing' THEN status ELSE 'active' END`,\n\t\t\tupdated_at: new Date(),\n\t\t})\n\t\t.where(\"name\", \"=\", name)\n\t\t.execute();\n}\n",
|
|
6
6
|
"/**\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",
|
|
7
|
-
"import { z } from \"zod\";\n\n// ── Deploy Subgraph Request ─────────────────────────────────────────────────\n\nexport interface DeploySubgraphRequest {\n\tname: string;\n\tdescription?: string;\n\tsources: Record<string, Record<string, unknown>>;\n\tschema: Record<string, unknown>;\n\thandlerCode: string;\n\t/** Override the definition's startBlock for this deploy only. */\n\tstartBlock?: number;\n\t/** Original TypeScript source, persisted so chat can read/diff/edit later. */\n\tsourceCode?: string;\n\t/** Validate the definition + print the DDL plan without deploying. */\n\tdryRun?: boolean;\n}\n\nexport const DeploySubgraphRequestSchema: z.ZodType<DeploySubgraphRequest> =\n\tz.object({\n\t\tname: z\n\t\t\t.string()\n\t\t\t.regex(/^[a-z0-9-]+$/, \"lowercase alphanumeric + hyphens only\")\n\t\t\t.max(63),\n\t\tdescription: z.string().optional(),\n\t\tsources: z\n\t\t\t.record(z.string(), z.record(z.string(), z.unknown()))\n\t\t\t.refine(\n\t\t\t\t(s) => Object.keys(s).length > 0,\n\t\t\t\t\"Must have at least one source\",\n\t\t\t),\n\t\tschema: z.record(z.string(), z.unknown()),\n\t\thandlerCode: z.string().max(1_048_576, \"handler code exceeds 1MB limit\"),\n\t\tstartBlock: z.number().int().nonnegative().optional(),\n\t\tsourceCode: z\n\t\t\t.string()\n\t\t\t.max(1_048_576, \"source code exceeds 1MB limit\")\n\t\t\t.optional(),\n\t\tdryRun: z.boolean().optional(),\n\t});\n\n/**\n * How far a subgraph's rows can be checked without trusting the server,\n * derived from its sources and a scan of its handler. Never configured.\n *\n * - `state`: every input is a named state write, provable from block headers.\n * Handlers run in the deterministic realm. A deployed `state` subgraph is\n * verifiable: one with any reason is refused at deploy.\n * - `events`: an input (events, prints, tx results, `ctx.client`) needs\n * re-execution proofs, which are not served yet.\n * - `none`: a source can never be proven as written (trait scope, tip-first\n * backfill).\n */\nexport interface SubgraphVerification {\n\tlevel: \"state\" | \"events\" | \"none\";\n\t/** What stands between the subgraph and checkable rows, e.g.\n\t * `print_event source \"swaps\" needs event proofs` or\n\t * `subgraph.ts:41:12 Date: wall-clock time differs per run`. */\n\treasons: string[];\n\t/** What stays unproven even at this level, e.g. tx attribution of writes. */\n\tunproven: string[];\n}\n\nexport interface DeploySubgraphResponse {\n\taction: \"created\" | \"unchanged\" | \"handler_updated\" | \"updated\" | \"reindexed\";\n\tsubgraphId: string;\n\tversion: string;\n\tmessage: string;\n\t/** Effective indexing start height. */\n\tstart_block?: number;\n\toperationId?: string;\n\treindexStarted?: boolean;\n\t/** Bounded candidate-event denominator for the reindex just started, when the\n\t * op was classifiable as sparse/light. Absent for heavy ops (no upfront estimate\n\t * possible) — check `sl subgraphs status` after ~30s for a rate-based ETA instead. */\n\testimatedEvents?: number;\n\t/** Non-blocking deploy lints (e.g. handler reads a print field never observed on-chain). */\n\twarnings?: string[];\n\t/** sha256 identity of what was deployed; absent on unbundled deploys. */\n\tpin?: string;\n\tverification?: SubgraphVerification;\n\tdiff?: {\n\t\taddedTables: string[];\n\t\tremovedTables: string[];\n\t\taddedColumns: Record<string, string[]>;\n\t\tbreakingChanges: string[];\n\t};\n}\n\n// Subgraph API response types\n\n/** Prefix of the `last_error` a billing-paused subgraph carries, e.g.\n * `billing_paused: spend_cap_reached`. Stable: the processor writes it and\n * the watchdog and CLI key off it. */\nexport const BILLING_PAUSED_PREFIX = \"billing_paused: \";\n\n/** The billing code in a subgraph's `last_error`, or `null` when it isn't a\n * billing pause. */\nexport function billingPausedCode(\n\tlastError: string | null | undefined,\n): string | null {\n\treturn lastError?.startsWith(BILLING_PAUSED_PREFIX)\n\t\t? lastError.slice(BILLING_PAUSED_PREFIX.length)\n\t\t: null;\n}\n\nexport interface SubgraphSummary {\n\tname: string;\n\tversion: string;\n\tstatus: string;\n\tlastProcessedBlock: number;\n\ttotalProcessed: number;\n\ttotalRows?: number;\n\ttotalErrors: number;\n\ttables: string[];\n\tchainTip: number;\n\tsourceChainTip?: number;\n\ttargetBlock?: number;\n\tprogress: number;\n\tblocksRemaining?: number;\n\tsyncMode?: \"sync\" | \"reindex\";\n\tgapCount: number;\n\t/** history_filling = expected gaps while a tip-first backfill op runs. */\n\tintegrity: \"complete\" | \"gaps_detected\" | \"history_filling\";\n\t/** Most recent indexing error reason + when it occurred, if any. */\n\tlastError?: string | null;\n\tlastErrorAt?: string | null;\n\t/** Last row mutation timestamp; powers per-card freshness. */\n\tupdatedAt?: string | null;\n\tcreatedAt: string;\n}\n\nexport interface SubgraphGapRange {\n\tstart: number;\n\tend: number;\n\tsize: number;\n\treason: string;\n}\n\nexport interface SubgraphSyncInfo {\n\tstatus: \"synced\" | \"catching_up\" | \"reindexing\" | \"error\";\n\tmode?: \"sync\" | \"reindex\";\n\tstartBlock: number;\n\tlastProcessedBlock: number;\n\t/**\n\t * Backward-compatible denominator for progress displays. During reindexing,\n\t * this is the reindex target block rather than the live source chain tip.\n\t */\n\tchainTip: number;\n\tsourceChainTip?: number;\n\ttargetBlock?: number;\n\tblocksRemaining: number;\n\tprocessedBlocks?: number;\n\ttotalBlocks?: number;\n\tprogress: number;\n\t/** Present while the populating operation is queued: approximate claim\n\t * position + honest event denominator + naive start estimate. */\n\tqueue?: {\n\t\tposition: number | null;\n\t\testimatedEvents: number | null;\n\t\testimatedStartSeconds: number | null;\n\t};\n\t/** Event-based progress for sparse syncs (block pct is meaningless when\n\t * most heights are skipped). */\n\testimatedEvents?: number;\n\tprocessedEvents?: number;\n\tetaSeconds?: number | null;\n\tgaps: {\n\t\tcount: number;\n\t\ttotalMissingBlocks: number;\n\t\tranges: SubgraphGapRange[];\n\t};\n\t/** history_filling = expected gaps while a tip-first backfill op runs. */\n\tintegrity: \"complete\" | \"gaps_detected\" | \"history_filling\";\n}\n\nexport interface SubgraphDetail {\n\tname: string;\n\tversion: string;\n\tschemaHash?: string;\n\t/** sha256 over schema, handler bundle, startBlock, network and runtime.\n\t * Null for subgraphs deployed before pins existed. */\n\tpin?: string | null;\n\t/** Null for subgraphs deployed before levels were derived. */\n\tverification?: SubgraphVerification | null;\n\tstatus: string;\n\tlastProcessedBlock: number;\n\tdescription?: string;\n\tsources?: Record<string, unknown>;\n\tdefinition?: Record<string, unknown>;\n\thealth: {\n\t\ttotalProcessed: number;\n\t\ttotalErrors: number;\n\t\terrorRate: number;\n\t\tlastError: string | null;\n\t\tlastErrorAt: string | null;\n\t\t/** True after the processor matched work but user tables still have 0 rows. */\n\t\temptyMapping: boolean;\n\t};\n\tsync: SubgraphSyncInfo;\n\ttables: Record<\n\t\tstring,\n\t\t{\n\t\t\tendpoint: string;\n\t\t\tcolumns: Record<\n\t\t\t\tstring,\n\t\t\t\t{\n\t\t\t\t\ttype: string;\n\t\t\t\t\tnullable?: boolean;\n\t\t\t\t\tindexed?: boolean;\n\t\t\t\t\tsearchable?: boolean;\n\t\t\t\t\tdefault?: string | number | boolean;\n\t\t\t\t}\n\t\t\t>;\n\t\t\trowCount: number;\n\t\t\texample: string;\n\t\t\tindexes?: readonly (readonly string[])[];\n\t\t\tuniqueKeys?: readonly (readonly string[])[];\n\t\t}\n\t>;\n\tcreatedAt: string;\n\tupdatedAt: string;\n}\n\nexport interface SubgraphGapEntry {\n\tstart: number;\n\tend: number;\n\tsize: number;\n\treason: string;\n\tdetectedAt: string;\n\tresolvedAt: string | null;\n}\n\nexport interface SubgraphGapsResponse {\n\tdata: SubgraphGapEntry[];\n\tmeta: {\n\t\ttotal: number;\n\t\ttotalMissingBlocks: number;\n\t\tlimit: number;\n\t\toffset: number;\n\t};\n}\n\nexport interface ReindexResponse {\n\tmessage: string;\n\tfromBlock: number;\n\ttoBlock: number | string;\n\toperationId?: string;\n\tstatus?: \"queued\" | \"running\" | \"cancel_requested\";\n}\n\nexport interface SubgraphQueryParams {\n\tsort?: string;\n\torder?: string;\n\tlimit?: number;\n\toffset?: number;\n\tfields?: string;\n\tfilters?: Record<string, string>;\n}\n\n/**\n * Request shape for `GET /v1/subgraphs/:subgraphName/:tableName/aggregate`.\n * `filters` reuses the list/count where-surface; the rest name the columns to\n * aggregate. SUM/MIN/MAX columns must be numeric (uint/int, plus `_block_height`).\n */\nexport interface SubgraphAggregateParams {\n\tfilters?: Record<string, string>;\n\tcount?: boolean;\n\tcountDistinct?: string[];\n\tsum?: string[];\n\tmin?: string[];\n\tmax?: string[];\n}\n\n/**\n * Aggregate response. Keys are present only for requested aggregates.\n * `count`/`countDistinct` are JSON numbers (counts << 2^53); `sum`/`min`/`max`\n * are lossless strings (NUMERIC `::text`). `sum` of an empty set is `\"0\"`;\n * `min`/`max` are `null` when the filtered set is empty or all-null.\n */\nexport interface SubgraphAggregateResponse {\n\tcount?: number;\n\tcountDistinct?: Record<string, number>;\n\tsum?: Record<string, string>;\n\tmin?: Record<string, string | null>;\n\tmax?: Record<string, string | null>;\n}\n",
|
|
7
|
+
"import { z } from \"zod\";\n\n// ── Deploy Subgraph Request ─────────────────────────────────────────────────\n\n/** The body for deploying a subgraph: sources, schema, and handler code. */\nexport interface DeploySubgraphRequest {\n\tname: string;\n\tdescription?: string;\n\tsources: Record<string, Record<string, unknown>>;\n\tschema: Record<string, unknown>;\n\thandlerCode: string;\n\t/** Override the definition's startBlock for this deploy only. */\n\tstartBlock?: number;\n\t/** Original TypeScript source, persisted so chat can read/diff/edit later. */\n\tsourceCode?: string;\n\t/** Validate the definition + print the DDL plan without deploying. */\n\tdryRun?: boolean;\n}\n\nexport const DeploySubgraphRequestSchema: z.ZodType<DeploySubgraphRequest> =\n\tz.object({\n\t\tname: z\n\t\t\t.string()\n\t\t\t.regex(/^[a-z0-9-]+$/, \"lowercase alphanumeric + hyphens only\")\n\t\t\t.max(63),\n\t\tdescription: z.string().optional(),\n\t\tsources: z\n\t\t\t.record(z.string(), z.record(z.string(), z.unknown()))\n\t\t\t.refine(\n\t\t\t\t(s) => Object.keys(s).length > 0,\n\t\t\t\t\"Must have at least one source\",\n\t\t\t),\n\t\tschema: z.record(z.string(), z.unknown()),\n\t\thandlerCode: z.string().max(1_048_576, \"handler code exceeds 1MB limit\"),\n\t\tstartBlock: z.number().int().nonnegative().optional(),\n\t\tsourceCode: z\n\t\t\t.string()\n\t\t\t.max(1_048_576, \"source code exceeds 1MB limit\")\n\t\t\t.optional(),\n\t\tdryRun: z.boolean().optional(),\n\t});\n\n/**\n * How far a subgraph's rows can be checked without trusting the server,\n * derived from its sources and a scan of its handler. Never configured.\n *\n * - `state`: every input is a named state write, provable from block headers.\n * Handlers run in the deterministic realm. A deployed `state` subgraph is\n * verifiable: one with any reason is refused at deploy.\n * - `events`: an input (events, prints, tx results, `ctx.client`) needs\n * re-execution proofs, which are not served yet.\n * - `none`: a source can never be proven as written (trait scope, tip-first\n * backfill).\n */\nexport interface SubgraphVerification {\n\tlevel: \"state\" | \"events\" | \"none\";\n\t/** What stands between the subgraph and checkable rows, e.g.\n\t * `print_event source \"swaps\" needs event proofs` or\n\t * `subgraph.ts:41:12 Date: wall-clock time differs per run`. */\n\treasons: string[];\n\t/** What stays unproven even at this level, e.g. tx attribution of writes. */\n\tunproven: string[];\n}\n\n/** What a deploy did, the version, and where indexing starts. */\nexport interface DeploySubgraphResponse {\n\taction: \"created\" | \"unchanged\" | \"handler_updated\" | \"updated\" | \"reindexed\";\n\tsubgraphId: string;\n\tversion: string;\n\tmessage: string;\n\t/** Effective indexing start height. */\n\tstart_block?: number;\n\toperationId?: string;\n\treindexStarted?: boolean;\n\t/** Bounded candidate-event denominator for the reindex just started, when the\n\t * op was classifiable as sparse/light. Absent for heavy ops (no upfront estimate\n\t * possible) — check `sl subgraphs status` after ~30s for a rate-based ETA instead. */\n\testimatedEvents?: number;\n\t/** Non-blocking deploy lints (e.g. handler reads a print field never observed on-chain). */\n\twarnings?: string[];\n\t/** sha256 identity of what was deployed; absent on unbundled deploys. */\n\tpin?: string;\n\tverification?: SubgraphVerification;\n\tdiff?: {\n\t\taddedTables: string[];\n\t\tremovedTables: string[];\n\t\taddedColumns: Record<string, string[]>;\n\t\tbreakingChanges: string[];\n\t};\n}\n\n// Subgraph API response types\n\n/** Prefix of the `last_error` a billing-paused subgraph carries, e.g.\n * `billing_paused: spend_cap_reached`. Stable: the processor writes it and\n * the watchdog and CLI key off it. */\nexport const BILLING_PAUSED_PREFIX = \"billing_paused: \";\n\n/** The billing code in a subgraph's `last_error`, or `null` when it isn't a\n * billing pause. */\nexport function billingPausedCode(\n\tlastError: string | null | undefined,\n): string | null {\n\treturn lastError?.startsWith(BILLING_PAUSED_PREFIX)\n\t\t? lastError.slice(BILLING_PAUSED_PREFIX.length)\n\t\t: null;\n}\n\n/** A subgraph's name, status, and indexing progress. */\nexport interface SubgraphSummary {\n\tname: string;\n\tversion: string;\n\tstatus: string;\n\tlastProcessedBlock: number;\n\ttotalProcessed: number;\n\ttotalRows?: number;\n\ttotalErrors: number;\n\ttables: string[];\n\tchainTip: number;\n\tsourceChainTip?: number;\n\ttargetBlock?: number;\n\tprogress: number;\n\tblocksRemaining?: number;\n\tsyncMode?: \"sync\" | \"reindex\";\n\tgapCount: number;\n\t/** history_filling = expected gaps while a tip-first backfill op runs. */\n\tintegrity: \"complete\" | \"gaps_detected\" | \"history_filling\";\n\t/** Most recent indexing error reason + when it occurred, if any. */\n\tlastError?: string | null;\n\tlastErrorAt?: string | null;\n\t/** Last row mutation timestamp; powers per-card freshness. */\n\tupdatedAt?: string | null;\n\tcreatedAt: string;\n}\n\nexport interface SubgraphGapRange {\n\tstart: number;\n\tend: number;\n\tsize: number;\n\treason: string;\n}\n\nexport interface SubgraphSyncInfo {\n\tstatus: \"synced\" | \"catching_up\" | \"reindexing\" | \"error\";\n\tmode?: \"sync\" | \"reindex\";\n\tstartBlock: number;\n\tlastProcessedBlock: number;\n\t/**\n\t * Backward-compatible denominator for progress displays. During reindexing,\n\t * this is the reindex target block rather than the live source chain tip.\n\t */\n\tchainTip: number;\n\tsourceChainTip?: number;\n\ttargetBlock?: number;\n\tblocksRemaining: number;\n\tprocessedBlocks?: number;\n\ttotalBlocks?: number;\n\tprogress: number;\n\t/** Present while the populating operation is queued: approximate claim\n\t * position + honest event denominator + naive start estimate. */\n\tqueue?: {\n\t\tposition: number | null;\n\t\testimatedEvents: number | null;\n\t\testimatedStartSeconds: number | null;\n\t};\n\t/** Event-based progress for sparse syncs (block pct is meaningless when\n\t * most heights are skipped). */\n\testimatedEvents?: number;\n\tprocessedEvents?: number;\n\tetaSeconds?: number | null;\n\tgaps: {\n\t\tcount: number;\n\t\ttotalMissingBlocks: number;\n\t\tranges: SubgraphGapRange[];\n\t};\n\t/** history_filling = expected gaps while a tip-first backfill op runs. */\n\tintegrity: \"complete\" | \"gaps_detected\" | \"history_filling\";\n}\n\n/** A subgraph's version, pin, status, tables, and sources. */\nexport interface SubgraphDetail {\n\tname: string;\n\tversion: string;\n\tschemaHash?: string;\n\t/** sha256 over schema, handler bundle, startBlock, network and runtime.\n\t * Null for subgraphs deployed before pins existed. */\n\tpin?: string | null;\n\t/** Null for subgraphs deployed before levels were derived. */\n\tverification?: SubgraphVerification | null;\n\tstatus: string;\n\tlastProcessedBlock: number;\n\tdescription?: string;\n\tsources?: Record<string, unknown>;\n\tdefinition?: Record<string, unknown>;\n\thealth: {\n\t\ttotalProcessed: number;\n\t\ttotalErrors: number;\n\t\terrorRate: number;\n\t\tlastError: string | null;\n\t\tlastErrorAt: string | null;\n\t\t/** True after the processor matched work but user tables still have 0 rows. */\n\t\temptyMapping: boolean;\n\t};\n\tsync: SubgraphSyncInfo;\n\ttables: Record<\n\t\tstring,\n\t\t{\n\t\t\tendpoint: string;\n\t\t\tcolumns: Record<\n\t\t\t\tstring,\n\t\t\t\t{\n\t\t\t\t\ttype: string;\n\t\t\t\t\tnullable?: boolean;\n\t\t\t\t\tindexed?: boolean;\n\t\t\t\t\tsearchable?: boolean;\n\t\t\t\t\tdefault?: string | number | boolean;\n\t\t\t\t}\n\t\t\t>;\n\t\t\trowCount: number;\n\t\t\texample: string;\n\t\t\tindexes?: readonly (readonly string[])[];\n\t\t\tuniqueKeys?: readonly (readonly string[])[];\n\t\t}\n\t>;\n\tcreatedAt: string;\n\tupdatedAt: string;\n}\n\nexport interface SubgraphGapEntry {\n\tstart: number;\n\tend: number;\n\tsize: number;\n\treason: string;\n\tdetectedAt: string;\n\tresolvedAt: string | null;\n}\n\n/** Missing block ranges in a subgraph, with totals. */\nexport interface SubgraphGapsResponse {\n\tdata: SubgraphGapEntry[];\n\tmeta: {\n\t\ttotal: number;\n\t\ttotalMissingBlocks: number;\n\t\tlimit: number;\n\t\toffset: number;\n\t};\n}\n\n/** A queued reindex and the block range it covers. */\nexport interface ReindexResponse {\n\tmessage: string;\n\tfromBlock: number;\n\ttoBlock: number | string;\n\toperationId?: string;\n\tstatus?: \"queued\" | \"running\" | \"cancel_requested\";\n}\n\nexport interface SubgraphQueryParams {\n\tsort?: string;\n\torder?: string;\n\tlimit?: number;\n\toffset?: number;\n\tfields?: string;\n\tfilters?: Record<string, string>;\n}\n\n/**\n * Request shape for `GET /v1/subgraphs/:subgraphName/:tableName/aggregate`.\n * `filters` reuses the list/count where-surface; the rest name the columns to\n * aggregate. SUM/MIN/MAX columns must be numeric (uint/int, plus `_block_height`).\n */\nexport interface SubgraphAggregateParams {\n\tfilters?: Record<string, string>;\n\tcount?: boolean;\n\tcountDistinct?: string[];\n\tsum?: string[];\n\tmin?: string[];\n\tmax?: string[];\n}\n\n/**\n * Aggregate response. Keys are present only for requested aggregates.\n * `count`/`countDistinct` are JSON numbers (counts << 2^53); `sum`/`min`/`max`\n * are lossless strings (NUMERIC `::text`). `sum` of an empty set is `\"0\"`;\n * `min`/`max` are `null` when the filtered set is empty or all-null.\n */\nexport interface SubgraphAggregateResponse {\n\tcount?: number;\n\tcountDistinct?: Record<string, number>;\n\tsum?: Record<string, string>;\n\tmin?: Record<string, string | null>;\n\tmax?: Record<string, string | null>;\n}\n",
|
|
8
8
|
"import { type RawBuilder, sql } from \"kysely\";\n\n/**\n * Safely encode a JS value as a JSONB literal for Kysely inserts/updates.\n * Kysely + postgres.js double-encodes JSON when using parameterized queries\n * with ::jsonb casts. This uses sql.raw to inline a properly escaped literal.\n *\n * Generic parameter lets callers set the RawBuilder's output type so they\n * don't need to cast at the insert site. Default is `unknown` — widen at\n * the call site when the column type is narrower, e.g. `jsonb<MyShape>(...)`.\n */\nexport function jsonb<T = unknown>(value: T): RawBuilder<T> {\n\tconst escaped = JSON.stringify(value, (_k, v) =>\n\t\ttypeof v === \"bigint\" ? v.toString() : v,\n\t).replace(/'/g, \"''\");\n\treturn sql`${sql.raw(`'${escaped}'::jsonb`)}`;\n}\n\n/**\n * Safely parse a JSONB value from the database.\n * Handles double-encoded strings where postgres.js returns a JSON string\n * instead of a parsed object.\n */\nexport function parseJsonb<T = unknown>(value: unknown): T {\n\tif (typeof value === \"string\") {\n\t\ttry {\n\t\t\treturn JSON.parse(value) as T;\n\t\t} catch {\n\t\t\treturn value as T;\n\t\t}\n\t}\n\treturn (value ?? {}) as T;\n}\n"
|
|
9
9
|
],
|
|
10
|
-
"mappings": ";AAAA,gBAAsB;;;ACmBtB,IAAM,cAA+C;AAAA,EACpD;AAAA,EACA;AAAA,EACA;AACD;AAEA,SAAS,YAAY,GAAyB;AAAA,EAC7C,MAAM,MAAM,QAAQ,IAAI,eAAe,KAAK,EAAE,YAAY;AAAA,EAC1D,IAAI,OAAQ,YAAkC,SAAS,GAAG,GAAG;AAAA,IAC5D,OAAO;AAAA,EACR;AAAA,EACA,OAAO;AAAA;AAUD,SAAS,eAAe,GAAiB;AAAA,EAC/C,MAAM,WAAW,aAAa;AAAA,EAC9B,OAAO,aAAa,YAAY,aAAa;AAAA;AAIvC,SAAS,uBAAuB,GAAyB;AAAA,EAC/D,OAAO,aAAa;AAAA;AASd,SAAS,cAAc,GAAY;AAAA,EACzC,MAAM,WAAW,QAAQ,IAAI,eAAe,KAAK,EAAE,YAAY;AAAA,EAC/D,IAAI,aAAa;AAAA,IAAQ,OAAO;AAAA,EAChC,IAAI,aAAa;AAAA,IAAS,OAAO;AAAA,EACjC,OAAO,gBAAgB,MAAM;AAAA;AAIvB,SAAS,cAAc,GAAY;AAAA,EACzC,OAAO,gBAAgB,MAAM;AAAA;AAIvB,SAAS,SAAS,GAAY;AAAA,EACpC,OAAO,gBAAgB,MAAM;AAAA;;;ACtE9B;
|
|
10
|
+
"mappings": "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",
|
|
11
11
|
"debugId": "4E4F882681C76C4F64756E2164756E21",
|
|
12
12
|
"names": []
|
|
13
13
|
}
|
package/dist/src/index-http.d.ts
CHANGED
package/dist/src/index.d.ts
CHANGED
|
@@ -19,6 +19,7 @@ declare const RETRY_STATES: readonly ["open", "retrying", "halted", "resolved"];
|
|
|
19
19
|
type RetryState = (typeof RETRY_STATES)[number];
|
|
20
20
|
type BootstrapSource = "archive" | "genesis" | "import";
|
|
21
21
|
import { z } from "zod";
|
|
22
|
+
/** The body for deploying a subgraph: sources, schema, and handler code. */
|
|
22
23
|
interface DeploySubgraphRequest {
|
|
23
24
|
name: string;
|
|
24
25
|
description?: string;
|
|
@@ -54,6 +55,7 @@ interface SubgraphVerification {
|
|
|
54
55
|
/** What stays unproven even at this level, e.g. tx attribution of writes. */
|
|
55
56
|
unproven: string[];
|
|
56
57
|
}
|
|
58
|
+
/** What a deploy did, the version, and where indexing starts. */
|
|
57
59
|
interface DeploySubgraphResponse {
|
|
58
60
|
action: "created" | "unchanged" | "handler_updated" | "updated" | "reindexed";
|
|
59
61
|
subgraphId: string;
|
|
@@ -86,6 +88,7 @@ declare const BILLING_PAUSED_PREFIX = "billing_paused: ";
|
|
|
86
88
|
/** The billing code in a subgraph's `last_error`, or `null` when it isn't a
|
|
87
89
|
* billing pause. */
|
|
88
90
|
declare function billingPausedCode(lastError: string | null | undefined): string | null;
|
|
91
|
+
/** A subgraph's name, status, and indexing progress. */
|
|
89
92
|
interface SubgraphSummary {
|
|
90
93
|
name: string;
|
|
91
94
|
version: string;
|
|
@@ -153,6 +156,7 @@ interface SubgraphSyncInfo {
|
|
|
153
156
|
/** history_filling = expected gaps while a tip-first backfill op runs. */
|
|
154
157
|
integrity: "complete" | "gaps_detected" | "history_filling";
|
|
155
158
|
}
|
|
159
|
+
/** A subgraph's version, pin, status, tables, and sources. */
|
|
156
160
|
interface SubgraphDetail {
|
|
157
161
|
name: string;
|
|
158
162
|
version: string;
|
|
@@ -202,6 +206,7 @@ interface SubgraphGapEntry {
|
|
|
202
206
|
detectedAt: string;
|
|
203
207
|
resolvedAt: string | null;
|
|
204
208
|
}
|
|
209
|
+
/** Missing block ranges in a subgraph, with totals. */
|
|
205
210
|
interface SubgraphGapsResponse {
|
|
206
211
|
data: SubgraphGapEntry[];
|
|
207
212
|
meta: {
|
|
@@ -211,6 +216,7 @@ interface SubgraphGapsResponse {
|
|
|
211
216
|
offset: number
|
|
212
217
|
};
|
|
213
218
|
}
|
|
219
|
+
/** A queued reindex and the block range it covers. */
|
|
214
220
|
interface ReindexResponse {
|
|
215
221
|
message: string;
|
|
216
222
|
fromBlock: number;
|
|
@@ -2850,6 +2856,7 @@ type ChainTrigger = {
|
|
|
2850
2856
|
type: "rune_burn"
|
|
2851
2857
|
rune?: string
|
|
2852
2858
|
};
|
|
2859
|
+
/** The body for creating a webhook: name, triggers, URL, delivery options. */
|
|
2853
2860
|
interface CreateWebhookRequest {
|
|
2854
2861
|
name: string;
|
|
2855
2862
|
triggers: ChainTrigger[];
|
|
@@ -2863,6 +2870,7 @@ interface CreateWebhookRequest {
|
|
|
2863
2870
|
interface ParsedCreateWebhookRequest extends Omit<CreateWebhookRequest, "format"> {
|
|
2864
2871
|
format: WebhookFormat2;
|
|
2865
2872
|
}
|
|
2873
|
+
/** The fields of a webhook you can change. */
|
|
2866
2874
|
interface UpdateWebhookRequest {
|
|
2867
2875
|
name?: string;
|
|
2868
2876
|
url?: string;
|
|
@@ -2879,6 +2887,7 @@ interface ReplayWebhookRequest {
|
|
|
2879
2887
|
force?: string;
|
|
2880
2888
|
}
|
|
2881
2889
|
type ParsedReplayWebhookRequest = ReplayWebhookRequest;
|
|
2890
|
+
/** A webhook's name, status, format, and URL. */
|
|
2882
2891
|
interface WebhookSummary {
|
|
2883
2892
|
id: string;
|
|
2884
2893
|
name: string;
|
|
@@ -2911,6 +2920,7 @@ interface WebhookAuth {
|
|
|
2911
2920
|
/** True when a bearer token or basic-auth string is set, whatever it is. */
|
|
2912
2921
|
hasSecret: boolean;
|
|
2913
2922
|
}
|
|
2923
|
+
/** A webhook with its triggers, auth, retry, and timeout settings. */
|
|
2914
2924
|
interface WebhookDetail extends WebhookSummary {
|
|
2915
2925
|
/** Chain-trigger filters. */
|
|
2916
2926
|
triggers: ChainTrigger[];
|
|
@@ -2927,15 +2937,18 @@ interface WebhookDetail extends WebhookSummary {
|
|
|
2927
2937
|
* or on an instance whose config changed since). */
|
|
2928
2938
|
warning: string | null;
|
|
2929
2939
|
}
|
|
2940
|
+
/** The new webhook and its signing secret, shown once. */
|
|
2930
2941
|
interface CreateWebhookResponse {
|
|
2931
2942
|
webhook: WebhookDetail;
|
|
2932
2943
|
/** Plaintext signing secret — surfaced ONCE. Store it server-side. */
|
|
2933
2944
|
signingSecret: string;
|
|
2934
2945
|
}
|
|
2946
|
+
/** The webhook and its new signing secret. */
|
|
2935
2947
|
interface RotateSecretResponse {
|
|
2936
2948
|
webhook: WebhookDetail;
|
|
2937
2949
|
signingSecret: string;
|
|
2938
2950
|
}
|
|
2951
|
+
/** One delivery attempt: status code, error, duration, and response. */
|
|
2939
2952
|
interface DeliveryRow {
|
|
2940
2953
|
id: string;
|
|
2941
2954
|
attempt: number;
|
|
@@ -2959,11 +2972,13 @@ interface DeliveryRow {
|
|
|
2959
2972
|
* (a settlement or reorg outbox row). */
|
|
2960
2973
|
eventIndex: number | null;
|
|
2961
2974
|
}
|
|
2975
|
+
/** How many events a replay scanned and enqueued. */
|
|
2962
2976
|
interface ReplayResult {
|
|
2963
2977
|
replayId: string;
|
|
2964
2978
|
enqueuedCount: number;
|
|
2965
2979
|
scannedCount: number;
|
|
2966
2980
|
}
|
|
2981
|
+
/** A delivery that ran out of retries. */
|
|
2967
2982
|
interface DeadRow {
|
|
2968
2983
|
id: string;
|
|
2969
2984
|
eventType: string;
|
|
@@ -2976,10 +2991,12 @@ interface DeadRow {
|
|
|
2976
2991
|
}
|
|
2977
2992
|
declare function formatWebhookSchemaErrors(error: z3.ZodError): string[];
|
|
2978
2993
|
type SubgraphSpecFormat = "openapi" | "agent" | "markdown";
|
|
2994
|
+
/** Options for generating a subgraph's agent schema. */
|
|
2979
2995
|
interface SubgraphSpecOptions {
|
|
2980
2996
|
serverUrl?: string;
|
|
2981
2997
|
generatedAt?: string;
|
|
2982
2998
|
}
|
|
2999
|
+
/** A subgraph's tables and columns in a form agents can read. */
|
|
2983
3000
|
interface SubgraphAgentSchema {
|
|
2984
3001
|
name: string;
|
|
2985
3002
|
version: string;
|