@secondlayer/shared 7.7.0 → 7.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/dist/src/archive/derived-stage-report.d.ts +99 -0
  2. package/dist/src/archive/derived-stage-report.js +64 -0
  3. package/dist/src/archive/derived-stage-report.js.map +10 -0
  4. package/dist/src/archive/disk-guard.d.ts +46 -0
  5. package/dist/src/archive/disk-guard.js +69 -0
  6. package/dist/src/archive/disk-guard.js.map +10 -0
  7. package/dist/src/archive/key-registry.d.ts +52 -0
  8. package/dist/src/archive/key-registry.js +168 -0
  9. package/dist/src/archive/key-registry.js.map +11 -0
  10. package/dist/src/archive/per-height-digest.d.ts +99 -0
  11. package/dist/src/archive/per-height-digest.js +194 -0
  12. package/dist/src/archive/per-height-digest.js.map +11 -0
  13. package/dist/src/archive/progress.d.ts +36 -0
  14. package/dist/src/archive/progress.js +75 -0
  15. package/dist/src/archive/progress.js.map +10 -0
  16. package/dist/src/archive/range-digest.d.ts +7 -0
  17. package/dist/src/archive/semantic-digest-builder.d.ts +1094 -0
  18. package/dist/src/archive/semantic-digest-builder.js +302 -0
  19. package/dist/src/archive/semantic-digest-builder.js.map +11 -0
  20. package/dist/src/archive/semantic-digest.d.ts +131 -0
  21. package/dist/src/archive/semantic-digest.js +143 -0
  22. package/dist/src/archive/semantic-digest.js.map +10 -0
  23. package/dist/src/archive/status.d.ts +92 -0
  24. package/dist/src/archive/status.js +106 -0
  25. package/dist/src/archive/status.js.map +10 -0
  26. package/dist/src/db/index.d.ts +11 -1
  27. package/dist/src/db/index.js +26 -1
  28. package/dist/src/db/index.js.map +6 -5
  29. package/dist/src/db/queries/chain-reorgs.d.ts +7 -0
  30. package/dist/src/db/queries/chain-reorgs.js +25 -1
  31. package/dist/src/db/queries/chain-reorgs.js.map +6 -5
  32. package/dist/src/db/queries/contracts.d.ts +7 -0
  33. package/dist/src/db/queries/instance.d.ts +1038 -0
  34. package/dist/src/db/queries/instance.js +92 -0
  35. package/dist/src/db/queries/instance.js.map +10 -0
  36. package/dist/src/db/queries/integrity.d.ts +7 -0
  37. package/dist/src/db/queries/subgraph-gaps.d.ts +7 -0
  38. package/dist/src/db/queries/subgraph-operations.d.ts +7 -0
  39. package/dist/src/db/queries/subgraphs.d.ts +10 -1
  40. package/dist/src/db/queries/subgraphs.js +47 -23
  41. package/dist/src/db/queries/subgraphs.js.map +7 -7
  42. package/dist/src/db/queries/subscriptions.d.ts +7 -0
  43. package/dist/src/db/queries/subscriptions.js +47 -31
  44. package/dist/src/db/queries/subscriptions.js.map +5 -5
  45. package/dist/src/db/schema.d.ts +8 -1
  46. package/dist/src/index.d.ts +11 -1
  47. package/dist/src/index.js +26 -1
  48. package/dist/src/index.js.map +6 -5
  49. package/dist/src/node/client.d.ts +11 -1
  50. package/dist/src/node/client.js +42 -4
  51. package/dist/src/node/client.js.map +3 -3
  52. package/dist/src/node/local-client.d.ts +7 -0
  53. package/dist/src/node/nakamoto.d.ts +22 -7
  54. package/dist/src/node/nakamoto.js +46 -5
  55. package/dist/src/node/nakamoto.js.map +3 -3
  56. package/migrations/0115_instances.ts +33 -0
  57. package/package.json +38 -2
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Derived-stage acceptance — what each service reads, writes, and needs
3
+ * beyond the canonical archive.
4
+ *
5
+ * The canonical archive rebuilds `blocks`, `transactions`, and `events`
6
+ * deterministically from R2 alone. Every stage downstream of those tables
7
+ * (decoders, subgraphs, subscription delivery) has its own inputs, outputs,
8
+ * and rebuild requirements — and an operator restoring from the archive
9
+ * needs to know exactly which of those it also needs to source, replay, or
10
+ * re-derive.
11
+ *
12
+ * This module defines the reporting shape. Each producer/service declares
13
+ * itself once (a `ServiceManifest`) and the aggregator emits one report per
14
+ * service to `reports/derived/<service>.json`. Stateful verification —
15
+ * actually re-running the service and comparing outputs — is Phase 4 work
16
+ * (see P4.10 effect manifests, P4.13 stateful deep verify); this shape is
17
+ * what Phase 4 will populate.
18
+ *
19
+ * The report answers three operator questions:
20
+ * 1. What canonical inputs does this service consume? (archive suffices)
21
+ * 2. What external inputs does it need? (RPC endpoints, envelope journal,
22
+ * contract source, ORACLE feeds — anything not in the archive)
23
+ * 3. If I restore from R2 alone, can this service resume? If not, what
24
+ * is missing?
25
+ *
26
+ * Silence is the failure mode we most want to prevent: a service that ships
27
+ * without a manifest is a service whose rebuild story is undocumented.
28
+ * `assertAllServicesDeclared()` fails a build when a known service is missing.
29
+ */
30
+ declare const DERIVED_STAGE_REPORT_SCHEMA_VERSION: 1;
31
+ type ServiceKind = "decoder" | "subgraph-runtime" | "subscription-runtime" | "protocol-producer";
32
+ type CanonicalInput = "blocks" | "transactions" | "events";
33
+ interface ExternalInput {
34
+ /** Short name for the dependency: `stacks-node-rpc`, `hiro-extended-api`,
35
+ * `observer-journal`, `contract-source-registry`, etc. */
36
+ name: string;
37
+ /** Why the archive can't provide this. One sentence. */
38
+ reason: string;
39
+ /** Where an operator can source it. `bundled-stacks`, `own-node`,
40
+ * `hiro-public-api`, `re-derive-from-events`, `manual-import`. */
41
+ source: string;
42
+ /** True if the archive supplies enough to re-derive this dependency
43
+ * without a live external source. Rare but happens (e.g. contract ABIs
44
+ * derivable from `transactions.raw_tx`). */
45
+ rebuildable_from_archive: boolean;
46
+ }
47
+ interface Output {
48
+ /** Table/topic/queue name in the operator's runtime. */
49
+ target: string;
50
+ /** How the operator verifies this output — e.g. `row-count`,
51
+ * `semantic-digest`, `effect-manifest`, `dlq-empty`. */
52
+ verification: string;
53
+ }
54
+ interface ServiceManifest {
55
+ name: string;
56
+ kind: ServiceKind;
57
+ description: string;
58
+ canonical_inputs: CanonicalInput[];
59
+ external_inputs: ExternalInput[];
60
+ outputs: Output[];
61
+ /**
62
+ * Explicit judgment on whether R2 alone can rebuild this service.
63
+ * `false` REQUIRES `external_inputs` to name what else is needed.
64
+ */
65
+ r2_alone_can_rebuild: boolean;
66
+ /** When true, this service can only replay forward from a starting point
67
+ * the operator supplies (e.g. a webhook consumer's last-delivered id).
68
+ * Publish it, don't hide it. */
69
+ requires_operator_state: boolean;
70
+ /** Repo path where the service lives, for cross-reference. */
71
+ source_path: string;
72
+ }
73
+ interface DerivedStageReport {
74
+ schema_version: typeof DERIVED_STAGE_REPORT_SCHEMA_VERSION;
75
+ generated_at: string;
76
+ services: ServiceManifest[];
77
+ }
78
+ /**
79
+ * Compose a report from a set of registered manifests. Sorts services by name
80
+ * so the JSON is stable across runs — a re-emit with unchanged manifests
81
+ * produces byte-identical output, so an operator can diff two archive
82
+ * publish cycles cleanly.
83
+ */
84
+ declare function buildDerivedStageReport(manifests: ReadonlyArray<ServiceManifest>, generatedAt: string): DerivedStageReport;
85
+ type ManifestValidationIssue = {
86
+ name: string
87
+ problem: string
88
+ };
89
+ /**
90
+ * Enforce the invariants the report shape leaves ambiguous:
91
+ * - `r2_alone_can_rebuild: false` REQUIRES at least one external input.
92
+ * Otherwise the report claims a rebuild gap it can't explain, which is
93
+ * worse than declaring nothing.
94
+ * - Names are unique — a duplicate is almost always a copy-paste that
95
+ * silently shadows the earlier declaration.
96
+ * - Every canonical_input is a real dataset name.
97
+ */
98
+ declare function validateServiceManifests(manifests: ReadonlyArray<ServiceManifest>): ManifestValidationIssue[];
99
+ export { validateServiceManifests, buildDerivedStageReport, ServiceManifest, ServiceKind, Output, ManifestValidationIssue, ExternalInput, DerivedStageReport, DERIVED_STAGE_REPORT_SCHEMA_VERSION, CanonicalInput };
@@ -0,0 +1,64 @@
1
+ import { createRequire } from "node:module";
2
+ var __defProp = Object.defineProperty;
3
+ var __returnValue = (v) => v;
4
+ function __exportSetter(name, newValue) {
5
+ this[name] = __returnValue.bind(null, newValue);
6
+ }
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, {
10
+ get: all[name],
11
+ enumerable: true,
12
+ configurable: true,
13
+ set: __exportSetter.bind(all, name)
14
+ });
15
+ };
16
+
17
+ // src/archive/derived-stage-report.ts
18
+ var DERIVED_STAGE_REPORT_SCHEMA_VERSION = 1;
19
+ function buildDerivedStageReport(manifests, generatedAt) {
20
+ const sorted = [...manifests].sort((a, b) => a.name.localeCompare(b.name));
21
+ return {
22
+ schema_version: DERIVED_STAGE_REPORT_SCHEMA_VERSION,
23
+ generated_at: generatedAt,
24
+ services: sorted
25
+ };
26
+ }
27
+ function validateServiceManifests(manifests) {
28
+ const issues = [];
29
+ const seen = new Set;
30
+ const canonicalDatasets = new Set([
31
+ "blocks",
32
+ "transactions",
33
+ "events"
34
+ ]);
35
+ for (const m of manifests) {
36
+ if (seen.has(m.name)) {
37
+ issues.push({ name: m.name, problem: "duplicate service name" });
38
+ }
39
+ seen.add(m.name);
40
+ if (!m.r2_alone_can_rebuild && m.external_inputs.length === 0) {
41
+ issues.push({
42
+ name: m.name,
43
+ problem: "declares r2_alone_can_rebuild=false but lists no external_inputs — the gap is undocumented"
44
+ });
45
+ }
46
+ for (const input of m.canonical_inputs) {
47
+ if (!canonicalDatasets.has(input)) {
48
+ issues.push({
49
+ name: m.name,
50
+ problem: `canonical_inputs contains unknown dataset: ${String(input)}`
51
+ });
52
+ }
53
+ }
54
+ }
55
+ return issues;
56
+ }
57
+ export {
58
+ validateServiceManifests,
59
+ buildDerivedStageReport,
60
+ DERIVED_STAGE_REPORT_SCHEMA_VERSION
61
+ };
62
+
63
+ //# debugId=C79406F0A3036B1164756E2164756E21
64
+ //# sourceMappingURL=derived-stage-report.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/archive/derived-stage-report.ts"],
4
+ "sourcesContent": [
5
+ "/**\n * Derived-stage acceptance — what each service reads, writes, and needs\n * beyond the canonical archive.\n *\n * The canonical archive rebuilds `blocks`, `transactions`, and `events`\n * deterministically from R2 alone. Every stage downstream of those tables\n * (decoders, subgraphs, subscription delivery) has its own inputs, outputs,\n * and rebuild requirements — and an operator restoring from the archive\n * needs to know exactly which of those it also needs to source, replay, or\n * re-derive.\n *\n * This module defines the reporting shape. Each producer/service declares\n * itself once (a `ServiceManifest`) and the aggregator emits one report per\n * service to `reports/derived/<service>.json`. Stateful verification —\n * actually re-running the service and comparing outputs — is Phase 4 work\n * (see P4.10 effect manifests, P4.13 stateful deep verify); this shape is\n * what Phase 4 will populate.\n *\n * The report answers three operator questions:\n * 1. What canonical inputs does this service consume? (archive suffices)\n * 2. What external inputs does it need? (RPC endpoints, envelope journal,\n * contract source, ORACLE feeds — anything not in the archive)\n * 3. If I restore from R2 alone, can this service resume? If not, what\n * is missing?\n *\n * Silence is the failure mode we most want to prevent: a service that ships\n * without a manifest is a service whose rebuild story is undocumented.\n * `assertAllServicesDeclared()` fails a build when a known service is missing.\n */\n\nexport const DERIVED_STAGE_REPORT_SCHEMA_VERSION = 1 as const;\n\nexport type ServiceKind =\n\t| \"decoder\"\n\t| \"subgraph-runtime\"\n\t| \"subscription-runtime\"\n\t| \"protocol-producer\";\n\nexport type CanonicalInput = \"blocks\" | \"transactions\" | \"events\";\n\nexport interface ExternalInput {\n\t/** Short name for the dependency: `stacks-node-rpc`, `hiro-extended-api`,\n\t * `observer-journal`, `contract-source-registry`, etc. */\n\tname: string;\n\t/** Why the archive can't provide this. One sentence. */\n\treason: string;\n\t/** Where an operator can source it. `bundled-stacks`, `own-node`,\n\t * `hiro-public-api`, `re-derive-from-events`, `manual-import`. */\n\tsource: string;\n\t/** True if the archive supplies enough to re-derive this dependency\n\t * without a live external source. Rare but happens (e.g. contract ABIs\n\t * derivable from `transactions.raw_tx`). */\n\trebuildable_from_archive: boolean;\n}\n\nexport interface Output {\n\t/** Table/topic/queue name in the operator's runtime. */\n\ttarget: string;\n\t/** How the operator verifies this output — e.g. `row-count`,\n\t * `semantic-digest`, `effect-manifest`, `dlq-empty`. */\n\tverification: string;\n}\n\nexport interface ServiceManifest {\n\tname: string;\n\tkind: ServiceKind;\n\tdescription: string;\n\tcanonical_inputs: CanonicalInput[];\n\texternal_inputs: ExternalInput[];\n\toutputs: Output[];\n\t/**\n\t * Explicit judgment on whether R2 alone can rebuild this service.\n\t * `false` REQUIRES `external_inputs` to name what else is needed.\n\t */\n\tr2_alone_can_rebuild: boolean;\n\t/** When true, this service can only replay forward from a starting point\n\t * the operator supplies (e.g. a webhook consumer's last-delivered id).\n\t * Publish it, don't hide it. */\n\trequires_operator_state: boolean;\n\t/** Repo path where the service lives, for cross-reference. */\n\tsource_path: string;\n}\n\nexport interface DerivedStageReport {\n\tschema_version: typeof DERIVED_STAGE_REPORT_SCHEMA_VERSION;\n\tgenerated_at: string;\n\tservices: ServiceManifest[];\n}\n\n/**\n * Compose a report from a set of registered manifests. Sorts services by name\n * so the JSON is stable across runs — a re-emit with unchanged manifests\n * produces byte-identical output, so an operator can diff two archive\n * publish cycles cleanly.\n */\nexport function buildDerivedStageReport(\n\tmanifests: ReadonlyArray<ServiceManifest>,\n\tgeneratedAt: string,\n): DerivedStageReport {\n\tconst sorted = [...manifests].sort((a, b) => a.name.localeCompare(b.name));\n\treturn {\n\t\tschema_version: DERIVED_STAGE_REPORT_SCHEMA_VERSION,\n\t\tgenerated_at: generatedAt,\n\t\tservices: sorted,\n\t};\n}\n\nexport type ManifestValidationIssue = {\n\tname: string;\n\tproblem: string;\n};\n\n/**\n * Enforce the invariants the report shape leaves ambiguous:\n * - `r2_alone_can_rebuild: false` REQUIRES at least one external input.\n * Otherwise the report claims a rebuild gap it can't explain, which is\n * worse than declaring nothing.\n * - Names are unique — a duplicate is almost always a copy-paste that\n * silently shadows the earlier declaration.\n * - Every canonical_input is a real dataset name.\n */\nexport function validateServiceManifests(\n\tmanifests: ReadonlyArray<ServiceManifest>,\n): ManifestValidationIssue[] {\n\tconst issues: ManifestValidationIssue[] = [];\n\tconst seen = new Set<string>();\n\tconst canonicalDatasets: ReadonlySet<CanonicalInput> = new Set([\n\t\t\"blocks\",\n\t\t\"transactions\",\n\t\t\"events\",\n\t]);\n\tfor (const m of manifests) {\n\t\tif (seen.has(m.name)) {\n\t\t\tissues.push({ name: m.name, problem: \"duplicate service name\" });\n\t\t}\n\t\tseen.add(m.name);\n\t\tif (!m.r2_alone_can_rebuild && m.external_inputs.length === 0) {\n\t\t\tissues.push({\n\t\t\t\tname: m.name,\n\t\t\t\tproblem:\n\t\t\t\t\t\"declares r2_alone_can_rebuild=false but lists no external_inputs — the gap is undocumented\",\n\t\t\t});\n\t\t}\n\t\tfor (const input of m.canonical_inputs) {\n\t\t\tif (!canonicalDatasets.has(input)) {\n\t\t\t\tissues.push({\n\t\t\t\t\tname: m.name,\n\t\t\t\t\tproblem: `canonical_inputs contains unknown dataset: ${String(input)}`,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\treturn issues;\n}\n"
6
+ ],
7
+ "mappings": ";;;;;;;;;;;;;;;;;AA8BO,IAAM,sCAAsC;AAiE5C,SAAS,uBAAuB,CACtC,WACA,aACqB;AAAA,EACrB,MAAM,SAAS,CAAC,GAAG,SAAS,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,KAAK,cAAc,EAAE,IAAI,CAAC;AAAA,EACzE,OAAO;AAAA,IACN,gBAAgB;AAAA,IAChB,cAAc;AAAA,IACd,UAAU;AAAA,EACX;AAAA;AAiBM,SAAS,wBAAwB,CACvC,WAC4B;AAAA,EAC5B,MAAM,SAAoC,CAAC;AAAA,EAC3C,MAAM,OAAO,IAAI;AAAA,EACjB,MAAM,oBAAiD,IAAI,IAAI;AAAA,IAC9D;AAAA,IACA;AAAA,IACA;AAAA,EACD,CAAC;AAAA,EACD,WAAW,KAAK,WAAW;AAAA,IAC1B,IAAI,KAAK,IAAI,EAAE,IAAI,GAAG;AAAA,MACrB,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,SAAS,yBAAyB,CAAC;AAAA,IAChE;AAAA,IACA,KAAK,IAAI,EAAE,IAAI;AAAA,IACf,IAAI,CAAC,EAAE,wBAAwB,EAAE,gBAAgB,WAAW,GAAG;AAAA,MAC9D,OAAO,KAAK;AAAA,QACX,MAAM,EAAE;AAAA,QACR,SACC;AAAA,MACF,CAAC;AAAA,IACF;AAAA,IACA,WAAW,SAAS,EAAE,kBAAkB;AAAA,MACvC,IAAI,CAAC,kBAAkB,IAAI,KAAK,GAAG;AAAA,QAClC,OAAO,KAAK;AAAA,UACX,MAAM,EAAE;AAAA,UACR,SAAS,8CAA8C,OAAO,KAAK;AAAA,QACpE,CAAC;AAAA,MACF;AAAA,IACD;AAAA,EACD;AAAA,EACA,OAAO;AAAA;",
8
+ "debugId": "C79406F0A3036B1164756E2164756E21",
9
+ "names": []
10
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Free-space guard for bulk archive jobs.
3
+ *
4
+ * Twice on 2026-08-12 a bulk job filled the disk that production Postgres
5
+ * shares: a backfill died at 9.2M/14.4M rows on ENOSPC, and a restore died at
6
+ * 86% of events. Prod survived both, but only just — 2.9GB free with the
7
+ * indexer still ingesting.
8
+ *
9
+ * The failure mode that makes this worth guarding rather than monitoring: the
10
+ * nightly `pg_basebackup` transiently consumes ~150GB, so a job that starts
11
+ * with comfortable headroom can still hit the wall an hour later. Checking
12
+ * once at startup is not enough; the check has to be repeated as the job runs.
13
+ *
14
+ * Waiting is the right response, not failing. The spike passes, and an
15
+ * archive job that pauses for twenty minutes is strictly better than one that
16
+ * dies at 86% or, worse, takes the database down with it.
17
+ */
18
+ type DiskSpace = {
19
+ freeBytes: number
20
+ totalBytes: number
21
+ };
22
+ declare function readDiskSpace(path: string): Promise<DiskSpace>;
23
+ type DiskGuardOptions = {
24
+ /** Any path on the filesystem to watch. */
25
+ path: string
26
+ /** Pause below this many bytes free. */
27
+ minFreeBytes?: number
28
+ /** How long to wait between re-checks while paused. */
29
+ pollMs?: number
30
+ /** Give up after this long rather than pausing forever. */
31
+ maxWaitMs?: number
32
+ onPause?: (free: DiskSpace, waitedMs: number) => void
33
+ sleep?: (ms: number) => Promise<void>
34
+ readSpace?: (path: string) => Promise<DiskSpace>
35
+ };
36
+ declare class DiskSpaceExhausted extends Error {
37
+ constructor(free: number, required: number, waitedMs: number);
38
+ }
39
+ /**
40
+ * Block until there is enough headroom, or throw after `maxWaitMs`.
41
+ *
42
+ * Call this between units of work — before each partition, not once at the
43
+ * start — because the pressure that matters arrives mid-job.
44
+ */
45
+ declare function waitForDiskSpace(options: DiskGuardOptions): Promise<void>;
46
+ export { waitForDiskSpace, readDiskSpace, DiskSpaceExhausted, DiskSpace, DiskGuardOptions };
@@ -0,0 +1,69 @@
1
+ import { createRequire } from "node:module";
2
+ var __defProp = Object.defineProperty;
3
+ var __returnValue = (v) => v;
4
+ function __exportSetter(name, newValue) {
5
+ this[name] = __returnValue.bind(null, newValue);
6
+ }
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, {
10
+ get: all[name],
11
+ enumerable: true,
12
+ configurable: true,
13
+ set: __exportSetter.bind(all, name)
14
+ });
15
+ };
16
+
17
+ // src/archive/disk-guard.ts
18
+ import { statfs } from "node:fs/promises";
19
+ async function readDiskSpace(path) {
20
+ const stats = await statfs(path);
21
+ return {
22
+ freeBytes: stats.bavail * stats.bsize,
23
+ totalBytes: stats.blocks * stats.bsize
24
+ };
25
+ }
26
+ var DEFAULT_MIN_FREE_BYTES = 100 * 1024 ** 3;
27
+ var DEFAULT_POLL_MS = 60000;
28
+ var DEFAULT_MAX_WAIT_MS = 2 * 3600000;
29
+ var MAX_THRESHOLD_FRACTION_OF_DISK = 0.25;
30
+ function effectiveThreshold(configured, totalBytes) {
31
+ if (!Number.isFinite(totalBytes) || totalBytes <= 0)
32
+ return configured;
33
+ return Math.min(configured, totalBytes * MAX_THRESHOLD_FRACTION_OF_DISK);
34
+ }
35
+
36
+ class DiskSpaceExhausted extends Error {
37
+ constructor(free, required, waitedMs) {
38
+ super(`only ${(free / 1024 ** 3).toFixed(1)}GB free (need ${(required / 1024 ** 3).toFixed(1)}GB) after waiting ${Math.round(waitedMs / 60000)}m`);
39
+ this.name = "DiskSpaceExhausted";
40
+ }
41
+ }
42
+ async function waitForDiskSpace(options) {
43
+ const minFree = options.minFreeBytes ?? DEFAULT_MIN_FREE_BYTES;
44
+ const pollMs = options.pollMs ?? DEFAULT_POLL_MS;
45
+ const maxWaitMs = options.maxWaitMs ?? DEFAULT_MAX_WAIT_MS;
46
+ const readSpace = options.readSpace ?? readDiskSpace;
47
+ const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
48
+ let waited = 0;
49
+ while (true) {
50
+ const space = await readSpace(options.path);
51
+ const threshold = effectiveThreshold(minFree, space.totalBytes);
52
+ if (space.freeBytes >= threshold)
53
+ return;
54
+ if (waited >= maxWaitMs) {
55
+ throw new DiskSpaceExhausted(space.freeBytes, threshold, waited);
56
+ }
57
+ options.onPause?.(space, waited);
58
+ await sleep(pollMs);
59
+ waited += pollMs;
60
+ }
61
+ }
62
+ export {
63
+ waitForDiskSpace,
64
+ readDiskSpace,
65
+ DiskSpaceExhausted
66
+ };
67
+
68
+ //# debugId=F49523DF35B78F5364756E2164756E21
69
+ //# sourceMappingURL=disk-guard.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/archive/disk-guard.ts"],
4
+ "sourcesContent": [
5
+ "import { statfs } from \"node:fs/promises\";\n\n/**\n * Free-space guard for bulk archive jobs.\n *\n * Twice on 2026-08-12 a bulk job filled the disk that production Postgres\n * shares: a backfill died at 9.2M/14.4M rows on ENOSPC, and a restore died at\n * 86% of events. Prod survived both, but only just — 2.9GB free with the\n * indexer still ingesting.\n *\n * The failure mode that makes this worth guarding rather than monitoring: the\n * nightly `pg_basebackup` transiently consumes ~150GB, so a job that starts\n * with comfortable headroom can still hit the wall an hour later. Checking\n * once at startup is not enough; the check has to be repeated as the job runs.\n *\n * Waiting is the right response, not failing. The spike passes, and an\n * archive job that pauses for twenty minutes is strictly better than one that\n * dies at 86% or, worse, takes the database down with it.\n */\n\nexport type DiskSpace = { freeBytes: number; totalBytes: number };\n\nexport async function readDiskSpace(path: string): Promise<DiskSpace> {\n\tconst stats = await statfs(path);\n\treturn {\n\t\tfreeBytes: stats.bavail * stats.bsize,\n\t\ttotalBytes: stats.blocks * stats.bsize,\n\t};\n}\n\nexport type DiskGuardOptions = {\n\t/** Any path on the filesystem to watch. */\n\tpath: string;\n\t/** Pause below this many bytes free. */\n\tminFreeBytes?: number;\n\t/** How long to wait between re-checks while paused. */\n\tpollMs?: number;\n\t/** Give up after this long rather than pausing forever. */\n\tmaxWaitMs?: number;\n\tonPause?: (free: DiskSpace, waitedMs: number) => void;\n\tsleep?: (ms: number) => Promise<void>;\n\treadSpace?: (path: string) => Promise<DiskSpace>;\n};\n\n/** 100GB: comfortably above the ~39GB an export needs plus the backup spike. */\nconst DEFAULT_MIN_FREE_BYTES = 100 * 1024 ** 3;\nconst DEFAULT_POLL_MS = 60_000;\nconst DEFAULT_MAX_WAIT_MS = 2 * 3_600_000;\n/**\n * A threshold larger than a share of the whole disk is unsatisfiable, not\n * protective: on a 14GB CI runner or a modest VPS, demanding 100GB free would\n * block forever waiting for space that machine can never have. The absolute\n * default is tuned for the production host; this keeps it from becoming a wall\n * everywhere else.\n */\nconst MAX_THRESHOLD_FRACTION_OF_DISK = 0.25;\n\nfunction effectiveThreshold(configured: number, totalBytes: number): number {\n\tif (!Number.isFinite(totalBytes) || totalBytes <= 0) return configured;\n\treturn Math.min(configured, totalBytes * MAX_THRESHOLD_FRACTION_OF_DISK);\n}\n\nexport class DiskSpaceExhausted extends Error {\n\tconstructor(free: number, required: number, waitedMs: number) {\n\t\tsuper(\n\t\t\t`only ${(free / 1024 ** 3).toFixed(1)}GB free (need ${(required / 1024 ** 3).toFixed(1)}GB) after waiting ${Math.round(waitedMs / 60_000)}m`,\n\t\t);\n\t\tthis.name = \"DiskSpaceExhausted\";\n\t}\n}\n\n/**\n * Block until there is enough headroom, or throw after `maxWaitMs`.\n *\n * Call this between units of work — before each partition, not once at the\n * start — because the pressure that matters arrives mid-job.\n */\nexport async function waitForDiskSpace(\n\toptions: DiskGuardOptions,\n): Promise<void> {\n\tconst minFree = options.minFreeBytes ?? DEFAULT_MIN_FREE_BYTES;\n\tconst pollMs = options.pollMs ?? DEFAULT_POLL_MS;\n\tconst maxWaitMs = options.maxWaitMs ?? DEFAULT_MAX_WAIT_MS;\n\tconst readSpace = options.readSpace ?? readDiskSpace;\n\tconst sleep =\n\t\toptions.sleep ??\n\t\t((ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms)));\n\n\tlet waited = 0;\n\twhile (true) {\n\t\tconst space = await readSpace(options.path);\n\t\tconst threshold = effectiveThreshold(minFree, space.totalBytes);\n\t\tif (space.freeBytes >= threshold) return;\n\t\tif (waited >= maxWaitMs) {\n\t\t\tthrow new DiskSpaceExhausted(space.freeBytes, threshold, waited);\n\t\t}\n\t\toptions.onPause?.(space, waited);\n\t\tawait sleep(pollMs);\n\t\twaited += pollMs;\n\t}\n}\n"
6
+ ],
7
+ "mappings": ";;;;;;;;;;;;;;;;;AAAA;AAsBA,eAAsB,aAAa,CAAC,MAAkC;AAAA,EACrE,MAAM,QAAQ,MAAM,OAAO,IAAI;AAAA,EAC/B,OAAO;AAAA,IACN,WAAW,MAAM,SAAS,MAAM;AAAA,IAChC,YAAY,MAAM,SAAS,MAAM;AAAA,EAClC;AAAA;AAkBD,IAAM,yBAAyB,MAAM,QAAQ;AAC7C,IAAM,kBAAkB;AACxB,IAAM,sBAAsB,IAAI;AAQhC,IAAM,iCAAiC;AAEvC,SAAS,kBAAkB,CAAC,YAAoB,YAA4B;AAAA,EAC3E,IAAI,CAAC,OAAO,SAAS,UAAU,KAAK,cAAc;AAAA,IAAG,OAAO;AAAA,EAC5D,OAAO,KAAK,IAAI,YAAY,aAAa,8BAA8B;AAAA;AAAA;AAGjE,MAAM,2BAA2B,MAAM;AAAA,EAC7C,WAAW,CAAC,MAAc,UAAkB,UAAkB;AAAA,IAC7D,MACC,SAAS,OAAO,QAAQ,GAAG,QAAQ,CAAC,mBAAmB,WAAW,QAAQ,GAAG,QAAQ,CAAC,sBAAsB,KAAK,MAAM,WAAW,KAAM,IACzI;AAAA,IACA,KAAK,OAAO;AAAA;AAEd;AAQA,eAAsB,gBAAgB,CACrC,SACgB;AAAA,EAChB,MAAM,UAAU,QAAQ,gBAAgB;AAAA,EACxC,MAAM,SAAS,QAAQ,UAAU;AAAA,EACjC,MAAM,YAAY,QAAQ,aAAa;AAAA,EACvC,MAAM,YAAY,QAAQ,aAAa;AAAA,EACvC,MAAM,QACL,QAAQ,UACP,CAAC,OAAe,IAAI,QAAc,CAAC,YAAY,WAAW,SAAS,EAAE,CAAC;AAAA,EAExE,IAAI,SAAS;AAAA,EACb,OAAO,MAAM;AAAA,IACZ,MAAM,QAAQ,MAAM,UAAU,QAAQ,IAAI;AAAA,IAC1C,MAAM,YAAY,mBAAmB,SAAS,MAAM,UAAU;AAAA,IAC9D,IAAI,MAAM,aAAa;AAAA,MAAW;AAAA,IAClC,IAAI,UAAU,WAAW;AAAA,MACxB,MAAM,IAAI,mBAAmB,MAAM,WAAW,WAAW,MAAM;AAAA,IAChE;AAAA,IACA,QAAQ,UAAU,OAAO,MAAM;AAAA,IAC/B,MAAM,MAAM,MAAM;AAAA,IAClB,UAAU;AAAA,EACX;AAAA;",
8
+ "debugId": "F49523DF35B78F5364756E2164756E21",
9
+ "names": []
10
+ }
@@ -0,0 +1,52 @@
1
+ /**\\n* Trust root for the canonical archive.\\n*\\n* Day-to-day signing uses an ONLINE key that lives on the publishing host, so\\n* it is exposed to every risk that host is. The archive's actual trust anchor\\n* is an OFFLINE ROOT key that never touches a server: it signs a registry of\\n* online keys, and nothing else. A consumer pins the root public key, verifies\\n* the registry against it, and only then decides whether a manifest's signature\\n* means anything.\\n*\\n* The distinction that makes rotation survivable:\\n*\\n* retired / rotated → the key stops signing NEW objects, but everything it\\n* signed while valid stays valid. Without this, every\\n* rotation would invalidate the entire published history.\\n* compromised → every signature by that key is suspect, including\\n* historical ones, because an attacker holding the key\\n* could have backdated anything.\\n*\\n* Those two cases look identical in a naive "is this key current?" check, and\\n* conflating them either destroys your history on every rotation or accepts\\n* forgeries after a leak.\\n*/
2
+ declare const KEY_REGISTRY_SCHEMA_VERSION = 1;
3
+ type KeyStatus = "active" | "retired" | "compromised";
4
+ type RegisteredKey = {
5
+ key_id: string
6
+ public_key_pem: string
7
+ status: KeyStatus
8
+ /** ISO timestamp this key became valid for signing. */
9
+ valid_from: string
10
+ /** ISO timestamp it stopped signing. Null while active. */
11
+ valid_until: string | null
12
+ /** For `compromised`: the moment trust is withdrawn. Everything this key
13
+ * signed is suspect regardless of when, so this is provenance, not a
14
+ * validity bound. */
15
+ compromised_at?: string
16
+ };
17
+ type KeyRegistry = {
18
+ schema_version: typeof KEY_REGISTRY_SCHEMA_VERSION
19
+ network: string
20
+ keys: RegisteredKey[]
21
+ updated_at: string
22
+ /** Root signature over the registry minus this envelope. */
23
+ signature?: string
24
+ root_key_id?: string
25
+ };
26
+ type KeyTrustResult = {
27
+ trusted: boolean
28
+ /** Machine-readable so callers can map to exit codes and messages. */
29
+ reason: "trusted" | "registry-unsigned" | "registry-signature-invalid" | "unknown-key" | "key-compromised" | "signed-outside-validity"
30
+ detail: string
31
+ };
32
+ /** The exact bytes the root signature covers: the registry minus its envelope. */
33
+ declare function canonicalRegistryPayload(registry: KeyRegistry): string;
34
+ /**
35
+ * Verify the registry itself against the pinned root public key. Everything
36
+ * downstream is meaningless if this fails — an attacker who can serve a
37
+ * registry can otherwise nominate their own signing key.
38
+ */
39
+ declare function verifyRegistry(registry: KeyRegistry, rootPublicKeyPem: string): KeyTrustResult;
40
+ /**
41
+ * Decide whether an object signed by `keyId` at `signedAt` should be trusted.
42
+ *
43
+ * `signedAt` is the object's own generation time, NOT now — that is what lets a
44
+ * rotated key keep vouching for what it signed while it was valid.
45
+ */
46
+ declare function checkKeyTrust(registry: KeyRegistry, keyId: string, signedAt: string): KeyTrustResult;
47
+ /** Look up a key's public PEM for signature verification. */
48
+ declare function publicKeyFor(registry: KeyRegistry, keyId: string): string | undefined;
49
+ /** Derive the registry entry id for a public key, so operators cannot mislabel
50
+ * a key by hand-editing the registry. */
51
+ declare function keyIdFor(publicKeyPem: string): string;
52
+ export { verifyRegistry, publicKeyFor, keyIdFor, checkKeyTrust, canonicalRegistryPayload, RegisteredKey, KeyTrustResult, KeyStatus, KeyRegistry, KEY_REGISTRY_SCHEMA_VERSION };
@@ -0,0 +1,168 @@
1
+ import { createRequire } from "node:module";
2
+ var __defProp = Object.defineProperty;
3
+ var __returnValue = (v) => v;
4
+ function __exportSetter(name, newValue) {
5
+ this[name] = __returnValue.bind(null, newValue);
6
+ }
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, {
10
+ get: all[name],
11
+ enumerable: true,
12
+ configurable: true,
13
+ set: __exportSetter.bind(all, name)
14
+ });
15
+ };
16
+
17
+ // src/crypto/ed25519.ts
18
+ var exports_ed25519 = {};
19
+ __export(exports_ed25519, {
20
+ verifyEd25519: () => verifyEd25519,
21
+ signEd25519: () => signEd25519,
22
+ publicKeyPemFromPrivate: () => publicKeyPemFromPrivate,
23
+ loadEd25519PublicKey: () => loadEd25519PublicKey,
24
+ loadEd25519PrivateKey: () => loadEd25519PrivateKey,
25
+ generateEd25519KeyPair: () => generateEd25519KeyPair,
26
+ ed25519KeyId: () => ed25519KeyId
27
+ });
28
+ import {
29
+ createHash,
30
+ createPrivateKey,
31
+ createPublicKey,
32
+ generateKeyPairSync,
33
+ sign as nodeSign,
34
+ verify as nodeVerify
35
+ } from "node:crypto";
36
+ function generateEd25519KeyPair() {
37
+ const { privateKey, publicKey } = generateKeyPairSync("ed25519");
38
+ return {
39
+ privateKeyPem: privateKey.export({ format: "pem", type: "pkcs8" }).toString(),
40
+ publicKeyPem: publicKey.export({ format: "pem", type: "spki" }).toString()
41
+ };
42
+ }
43
+ function loadEd25519PrivateKey(pem) {
44
+ return createPrivateKey(pem);
45
+ }
46
+ function loadEd25519PublicKey(pem) {
47
+ return createPublicKey(pem);
48
+ }
49
+ function publicKeyPemFromPrivate(privateKeyPem) {
50
+ return createPublicKey(createPrivateKey(privateKeyPem)).export({ format: "pem", type: "spki" }).toString();
51
+ }
52
+ function ed25519KeyId(publicKeyPem) {
53
+ const der = createPublicKey(publicKeyPem).export({
54
+ format: "der",
55
+ type: "spki"
56
+ });
57
+ return createHash("sha256").update(der).digest("base64url").slice(0, 16);
58
+ }
59
+ function signEd25519(payload, privateKey) {
60
+ return nodeSign(null, Buffer.from(payload, "utf8"), privateKey).toString("base64");
61
+ }
62
+ function verifyEd25519(payload, signatureBase64, publicKey) {
63
+ try {
64
+ return nodeVerify(null, Buffer.from(payload, "utf8"), publicKey, Buffer.from(signatureBase64, "base64"));
65
+ } catch {
66
+ return false;
67
+ }
68
+ }
69
+
70
+ // src/archive/key-registry.ts
71
+ var KEY_REGISTRY_SCHEMA_VERSION = 1;
72
+ function canonicalRegistryPayload(registry) {
73
+ const { signature: _s, root_key_id: _r, ...rest } = registry;
74
+ return JSON.stringify(rest);
75
+ }
76
+ function verifyRegistry(registry, rootPublicKeyPem) {
77
+ if (!registry.signature) {
78
+ return {
79
+ trusted: false,
80
+ reason: "registry-unsigned",
81
+ detail: "key registry carries no root signature"
82
+ };
83
+ }
84
+ let valid = false;
85
+ try {
86
+ valid = verifyEd25519(canonicalRegistryPayload(registry), registry.signature, loadEd25519PublicKey(rootPublicKeyPem));
87
+ } catch (err) {
88
+ return {
89
+ trusted: false,
90
+ reason: "registry-signature-invalid",
91
+ detail: err instanceof Error ? err.message : "root verification failed"
92
+ };
93
+ }
94
+ return valid ? {
95
+ trusted: true,
96
+ reason: "trusted",
97
+ detail: "registry verified against root"
98
+ } : {
99
+ trusted: false,
100
+ reason: "registry-signature-invalid",
101
+ detail: "registry signature does not match the pinned root key"
102
+ };
103
+ }
104
+ function checkKeyTrust(registry, keyId, signedAt) {
105
+ const key = registry.keys.find((k) => k.key_id === keyId);
106
+ if (!key) {
107
+ return {
108
+ trusted: false,
109
+ reason: "unknown-key",
110
+ detail: `key ${keyId} is not in the registry`
111
+ };
112
+ }
113
+ if (key.status === "compromised") {
114
+ return {
115
+ trusted: false,
116
+ reason: "key-compromised",
117
+ detail: `key ${keyId} was compromised${key.compromised_at ? ` (${key.compromised_at})` : ""}; every signature it made is suspect`
118
+ };
119
+ }
120
+ const signedTime = Date.parse(signedAt);
121
+ const from = Date.parse(key.valid_from);
122
+ if (Number.isNaN(signedTime) || Number.isNaN(from)) {
123
+ return {
124
+ trusted: false,
125
+ reason: "signed-outside-validity",
126
+ detail: "unparseable timestamp on the object or the key"
127
+ };
128
+ }
129
+ if (signedTime < from) {
130
+ return {
131
+ trusted: false,
132
+ reason: "signed-outside-validity",
133
+ detail: `object predates key validity (${signedAt} < ${key.valid_from})`
134
+ };
135
+ }
136
+ if (key.valid_until !== null) {
137
+ const until = Date.parse(key.valid_until);
138
+ if (!Number.isNaN(until) && signedTime > until) {
139
+ return {
140
+ trusted: false,
141
+ reason: "signed-outside-validity",
142
+ detail: `object postdates key retirement (${signedAt} > ${key.valid_until})`
143
+ };
144
+ }
145
+ }
146
+ return {
147
+ trusted: true,
148
+ reason: "trusted",
149
+ detail: key.status === "active" ? `signed by active key ${keyId}` : `signed by ${keyId} while it was valid (now ${key.status})`
150
+ };
151
+ }
152
+ function publicKeyFor(registry, keyId) {
153
+ return registry.keys.find((k) => k.key_id === keyId)?.public_key_pem;
154
+ }
155
+ function keyIdFor(publicKeyPem) {
156
+ return ed25519KeyId(publicKeyPem);
157
+ }
158
+ export {
159
+ verifyRegistry,
160
+ publicKeyFor,
161
+ keyIdFor,
162
+ checkKeyTrust,
163
+ canonicalRegistryPayload,
164
+ KEY_REGISTRY_SCHEMA_VERSION
165
+ };
166
+
167
+ //# debugId=06FAC7E08C33616364756E2164756E21
168
+ //# sourceMappingURL=key-registry.js.map
@@ -0,0 +1,11 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/crypto/ed25519.ts", "../src/archive/key-registry.ts"],
4
+ "sourcesContent": [
5
+ "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",
6
+ "import {\n\ted25519KeyId,\n\tloadEd25519PublicKey,\n\tverifyEd25519,\n} from \"../crypto/ed25519.ts\";\n\n/**\n * Trust root for the canonical archive.\n *\n * Day-to-day signing uses an ONLINE key that lives on the publishing host, so\n * it is exposed to every risk that host is. The archive's actual trust anchor\n * is an OFFLINE ROOT key that never touches a server: it signs a registry of\n * online keys, and nothing else. A consumer pins the root public key, verifies\n * the registry against it, and only then decides whether a manifest's signature\n * means anything.\n *\n * The distinction that makes rotation survivable:\n *\n * retired / rotated → the key stops signing NEW objects, but everything it\n * signed while valid stays valid. Without this, every\n * rotation would invalidate the entire published history.\n * compromised → every signature by that key is suspect, including\n * historical ones, because an attacker holding the key\n * could have backdated anything.\n *\n * Those two cases look identical in a naive \"is this key current?\" check, and\n * conflating them either destroys your history on every rotation or accepts\n * forgeries after a leak.\n */\n\nexport const KEY_REGISTRY_SCHEMA_VERSION = 1;\n\nexport type KeyStatus = \"active\" | \"retired\" | \"compromised\";\n\nexport type RegisteredKey = {\n\tkey_id: string;\n\tpublic_key_pem: string;\n\tstatus: KeyStatus;\n\t/** ISO timestamp this key became valid for signing. */\n\tvalid_from: string;\n\t/** ISO timestamp it stopped signing. Null while active. */\n\tvalid_until: string | null;\n\t/** For `compromised`: the moment trust is withdrawn. Everything this key\n\t * signed is suspect regardless of when, so this is provenance, not a\n\t * validity bound. */\n\tcompromised_at?: string;\n};\n\nexport type KeyRegistry = {\n\tschema_version: typeof KEY_REGISTRY_SCHEMA_VERSION;\n\tnetwork: string;\n\tkeys: RegisteredKey[];\n\tupdated_at: string;\n\t/** Root signature over the registry minus this envelope. */\n\tsignature?: string;\n\troot_key_id?: string;\n};\n\nexport type KeyTrustResult = {\n\ttrusted: boolean;\n\t/** Machine-readable so callers can map to exit codes and messages. */\n\treason:\n\t\t| \"trusted\"\n\t\t| \"registry-unsigned\"\n\t\t| \"registry-signature-invalid\"\n\t\t| \"unknown-key\"\n\t\t| \"key-compromised\"\n\t\t| \"signed-outside-validity\";\n\tdetail: string;\n};\n\n/** The exact bytes the root signature covers: the registry minus its envelope. */\nexport function canonicalRegistryPayload(registry: KeyRegistry): string {\n\tconst { signature: _s, root_key_id: _r, ...rest } = registry;\n\treturn JSON.stringify(rest);\n}\n\n/**\n * Verify the registry itself against the pinned root public key. Everything\n * downstream is meaningless if this fails — an attacker who can serve a\n * registry can otherwise nominate their own signing key.\n */\nexport function verifyRegistry(\n\tregistry: KeyRegistry,\n\trootPublicKeyPem: string,\n): KeyTrustResult {\n\tif (!registry.signature) {\n\t\treturn {\n\t\t\ttrusted: false,\n\t\t\treason: \"registry-unsigned\",\n\t\t\tdetail: \"key registry carries no root signature\",\n\t\t};\n\t}\n\tlet valid = false;\n\ttry {\n\t\tvalid = verifyEd25519(\n\t\t\tcanonicalRegistryPayload(registry),\n\t\t\tregistry.signature,\n\t\t\tloadEd25519PublicKey(rootPublicKeyPem),\n\t\t);\n\t} catch (err) {\n\t\treturn {\n\t\t\ttrusted: false,\n\t\t\treason: \"registry-signature-invalid\",\n\t\t\tdetail: err instanceof Error ? err.message : \"root verification failed\",\n\t\t};\n\t}\n\treturn valid\n\t\t? {\n\t\t\t\ttrusted: true,\n\t\t\t\treason: \"trusted\",\n\t\t\t\tdetail: \"registry verified against root\",\n\t\t\t}\n\t\t: {\n\t\t\t\ttrusted: false,\n\t\t\t\treason: \"registry-signature-invalid\",\n\t\t\t\tdetail: \"registry signature does not match the pinned root key\",\n\t\t\t};\n}\n\n/**\n * Decide whether an object signed by `keyId` at `signedAt` should be trusted.\n *\n * `signedAt` is the object's own generation time, NOT now — that is what lets a\n * rotated key keep vouching for what it signed while it was valid.\n */\nexport function checkKeyTrust(\n\tregistry: KeyRegistry,\n\tkeyId: string,\n\tsignedAt: string,\n): KeyTrustResult {\n\tconst key = registry.keys.find((k) => k.key_id === keyId);\n\tif (!key) {\n\t\treturn {\n\t\t\ttrusted: false,\n\t\t\treason: \"unknown-key\",\n\t\t\tdetail: `key ${keyId} is not in the registry`,\n\t\t};\n\t}\n\tif (key.status === \"compromised\") {\n\t\treturn {\n\t\t\ttrusted: false,\n\t\t\treason: \"key-compromised\",\n\t\t\tdetail: `key ${keyId} was compromised${key.compromised_at ? ` (${key.compromised_at})` : \"\"}; every signature it made is suspect`,\n\t\t};\n\t}\n\n\tconst signedTime = Date.parse(signedAt);\n\tconst from = Date.parse(key.valid_from);\n\tif (Number.isNaN(signedTime) || Number.isNaN(from)) {\n\t\treturn {\n\t\t\ttrusted: false,\n\t\t\treason: \"signed-outside-validity\",\n\t\t\tdetail: \"unparseable timestamp on the object or the key\",\n\t\t};\n\t}\n\tif (signedTime < from) {\n\t\treturn {\n\t\t\ttrusted: false,\n\t\t\treason: \"signed-outside-validity\",\n\t\t\tdetail: `object predates key validity (${signedAt} < ${key.valid_from})`,\n\t\t};\n\t}\n\tif (key.valid_until !== null) {\n\t\tconst until = Date.parse(key.valid_until);\n\t\tif (!Number.isNaN(until) && signedTime > until) {\n\t\t\treturn {\n\t\t\t\ttrusted: false,\n\t\t\t\treason: \"signed-outside-validity\",\n\t\t\t\tdetail: `object postdates key retirement (${signedAt} > ${key.valid_until})`,\n\t\t\t};\n\t\t}\n\t}\n\n\t// A retired key that was valid at signing time is fine — that is the whole\n\t// point of recording a validity window rather than a boolean.\n\treturn {\n\t\ttrusted: true,\n\t\treason: \"trusted\",\n\t\tdetail:\n\t\t\tkey.status === \"active\"\n\t\t\t\t? `signed by active key ${keyId}`\n\t\t\t\t: `signed by ${keyId} while it was valid (now ${key.status})`,\n\t};\n}\n\n/** Look up a key's public PEM for signature verification. */\nexport function publicKeyFor(\n\tregistry: KeyRegistry,\n\tkeyId: string,\n): string | undefined {\n\treturn registry.keys.find((k) => k.key_id === keyId)?.public_key_pem;\n}\n\n/** Derive the registry entry id for a public key, so operators cannot mislabel\n * a key by hand-editing the registry. */\nexport function keyIdFor(publicKeyPem: string): string {\n\treturn ed25519KeyId(publicKeyPem);\n}\n"
7
+ ],
8
+ "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;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;;;AC7CF,IAAM,8BAA8B;AA0CpC,SAAS,wBAAwB,CAAC,UAA+B;AAAA,EACvE,QAAQ,WAAW,IAAI,aAAa,OAAO,SAAS;AAAA,EACpD,OAAO,KAAK,UAAU,IAAI;AAAA;AAQpB,SAAS,cAAc,CAC7B,UACA,kBACiB;AAAA,EACjB,IAAI,CAAC,SAAS,WAAW;AAAA,IACxB,OAAO;AAAA,MACN,SAAS;AAAA,MACT,QAAQ;AAAA,MACR,QAAQ;AAAA,IACT;AAAA,EACD;AAAA,EACA,IAAI,QAAQ;AAAA,EACZ,IAAI;AAAA,IACH,QAAQ,cACP,yBAAyB,QAAQ,GACjC,SAAS,WACT,qBAAqB,gBAAgB,CACtC;AAAA,IACC,OAAO,KAAK;AAAA,IACb,OAAO;AAAA,MACN,SAAS;AAAA,MACT,QAAQ;AAAA,MACR,QAAQ,eAAe,QAAQ,IAAI,UAAU;AAAA,IAC9C;AAAA;AAAA,EAED,OAAO,QACJ;AAAA,IACA,SAAS;AAAA,IACT,QAAQ;AAAA,IACR,QAAQ;AAAA,EACT,IACC;AAAA,IACA,SAAS;AAAA,IACT,QAAQ;AAAA,IACR,QAAQ;AAAA,EACT;AAAA;AASI,SAAS,aAAa,CAC5B,UACA,OACA,UACiB;AAAA,EACjB,MAAM,MAAM,SAAS,KAAK,KAAK,CAAC,MAAM,EAAE,WAAW,KAAK;AAAA,EACxD,IAAI,CAAC,KAAK;AAAA,IACT,OAAO;AAAA,MACN,SAAS;AAAA,MACT,QAAQ;AAAA,MACR,QAAQ,OAAO;AAAA,IAChB;AAAA,EACD;AAAA,EACA,IAAI,IAAI,WAAW,eAAe;AAAA,IACjC,OAAO;AAAA,MACN,SAAS;AAAA,MACT,QAAQ;AAAA,MACR,QAAQ,OAAO,wBAAwB,IAAI,iBAAiB,KAAK,IAAI,oBAAoB;AAAA,IAC1F;AAAA,EACD;AAAA,EAEA,MAAM,aAAa,KAAK,MAAM,QAAQ;AAAA,EACtC,MAAM,OAAO,KAAK,MAAM,IAAI,UAAU;AAAA,EACtC,IAAI,OAAO,MAAM,UAAU,KAAK,OAAO,MAAM,IAAI,GAAG;AAAA,IACnD,OAAO;AAAA,MACN,SAAS;AAAA,MACT,QAAQ;AAAA,MACR,QAAQ;AAAA,IACT;AAAA,EACD;AAAA,EACA,IAAI,aAAa,MAAM;AAAA,IACtB,OAAO;AAAA,MACN,SAAS;AAAA,MACT,QAAQ;AAAA,MACR,QAAQ,iCAAiC,cAAc,IAAI;AAAA,IAC5D;AAAA,EACD;AAAA,EACA,IAAI,IAAI,gBAAgB,MAAM;AAAA,IAC7B,MAAM,QAAQ,KAAK,MAAM,IAAI,WAAW;AAAA,IACxC,IAAI,CAAC,OAAO,MAAM,KAAK,KAAK,aAAa,OAAO;AAAA,MAC/C,OAAO;AAAA,QACN,SAAS;AAAA,QACT,QAAQ;AAAA,QACR,QAAQ,oCAAoC,cAAc,IAAI;AAAA,MAC/D;AAAA,IACD;AAAA,EACD;AAAA,EAIA,OAAO;AAAA,IACN,SAAS;AAAA,IACT,QAAQ;AAAA,IACR,QACC,IAAI,WAAW,WACZ,wBAAwB,UACxB,aAAa,iCAAiC,IAAI;AAAA,EACvD;AAAA;AAIM,SAAS,YAAY,CAC3B,UACA,OACqB;AAAA,EACrB,OAAO,SAAS,KAAK,KAAK,CAAC,MAAM,EAAE,WAAW,KAAK,GAAG;AAAA;AAKhD,SAAS,QAAQ,CAAC,cAA8B;AAAA,EACtD,OAAO,aAAa,YAAY;AAAA;",
9
+ "debugId": "06FAC7E08C33616364756E2164756E21",
10
+ "names": []
11
+ }