@secondlayer/shared 11.2.0 → 11.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/archive/instance-diagnosis.d.ts +101 -0
- package/dist/src/archive/instance-diagnosis.js +134 -0
- package/dist/src/archive/instance-diagnosis.js.map +10 -0
- package/dist/src/archive/reference.d.ts +170 -0
- package/dist/src/archive/reference.js +231 -0
- package/dist/src/archive/reference.js.map +13 -0
- package/dist/src/index-http.js +8 -1
- package/dist/src/index-http.js.map +3 -3
- package/dist/src/index-internal-auth.d.ts +15 -1
- package/dist/src/index-internal-auth.js +9 -1
- package/dist/src/index-internal-auth.js.map +3 -3
- package/package.json +9 -1
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One reading of `/status` (or `/public/status`), shared by the CLI
|
|
3
|
+
* (`status`, `doctor`, `context`) and the SDK (`sl.instance.diagnose`).
|
|
4
|
+
*
|
|
5
|
+
* These surfaces used to each interpret the same payload on their own and
|
|
6
|
+
* disagreed in the most common first-run state: on a freshly self-hosted
|
|
7
|
+
* instance with no chain data, `status` printed DEGRADED while `doctor` printed
|
|
8
|
+
* "All checks passed", and neither named the cause or a next command. An
|
|
9
|
+
* operator (or an agent) reading both had no way to tell whether it was safe to
|
|
10
|
+
* proceed. The verdict now lives here so the surfaces cannot drift apart, and
|
|
11
|
+
* every issue carries the exact command that resolves it.
|
|
12
|
+
*/
|
|
13
|
+
interface PublicStatusDecoder {
|
|
14
|
+
decoder: string;
|
|
15
|
+
status: string;
|
|
16
|
+
}
|
|
17
|
+
interface PublicStatusService {
|
|
18
|
+
name: string;
|
|
19
|
+
status: string;
|
|
20
|
+
}
|
|
21
|
+
/** The subset of `/status` a diagnosis reads. Everything is optional:
|
|
22
|
+
* an older instance may not send a field, and a missing field must never be
|
|
23
|
+
* read as a failure. */
|
|
24
|
+
interface PublicStatus {
|
|
25
|
+
status?: string;
|
|
26
|
+
chainTip?: number | null;
|
|
27
|
+
chainIntegrity?: {
|
|
28
|
+
ok?: boolean
|
|
29
|
+
maxHeight?: number
|
|
30
|
+
reason?: string | null
|
|
31
|
+
};
|
|
32
|
+
streams?: {
|
|
33
|
+
status?: string
|
|
34
|
+
tip?: {
|
|
35
|
+
lag_seconds?: number
|
|
36
|
+
block_height?: number
|
|
37
|
+
} | null
|
|
38
|
+
};
|
|
39
|
+
index?: {
|
|
40
|
+
status?: string
|
|
41
|
+
decoders?: PublicStatusDecoder[]
|
|
42
|
+
};
|
|
43
|
+
services?: PublicStatusService[];
|
|
44
|
+
timestamp?: string;
|
|
45
|
+
[key: string]: unknown;
|
|
46
|
+
}
|
|
47
|
+
interface InstanceIssue {
|
|
48
|
+
/** What is wrong, in one line. */
|
|
49
|
+
title: string;
|
|
50
|
+
/** Why — assembled only from what the payload actually reports. */
|
|
51
|
+
detail?: string;
|
|
52
|
+
/** What to run next, most useful first. */
|
|
53
|
+
nextSteps: string[];
|
|
54
|
+
}
|
|
55
|
+
type InstanceState = "healthy" | "empty-index" | "degraded";
|
|
56
|
+
interface InstanceDiagnosis {
|
|
57
|
+
state: InstanceState;
|
|
58
|
+
/** The API's own top-level verdict, passed through unmodified. */
|
|
59
|
+
overall: string;
|
|
60
|
+
issues: InstanceIssue[];
|
|
61
|
+
}
|
|
62
|
+
declare const BOOTSTRAP_STEP = "Restore verified history: secondlayer bootstrap --against <manifest>";
|
|
63
|
+
declare const OBSERVER_STEP = "Confirm your node streams into this instance — secondlayer observer --mode indexer --endpoint secondlayer:3700 prints the stanza its config needs";
|
|
64
|
+
declare const SELF_HOST_DOCS_STEP = "Docs: https://www.secondlayer.tools/docs/self-host";
|
|
65
|
+
/** `true` when the instance is reachable but has never indexed a block. */
|
|
66
|
+
declare function hasNoChainData(status: PublicStatus): boolean;
|
|
67
|
+
/**
|
|
68
|
+
* Turn a `/status` payload into a verdict plus zero or more issues.
|
|
69
|
+
* Only facts present in the payload become issues — when the API says degraded
|
|
70
|
+
* and names no failing surface, the issue says exactly that rather than
|
|
71
|
+
* guessing a cause.
|
|
72
|
+
*/
|
|
73
|
+
declare function diagnoseInstanceStatus(status: PublicStatus): InstanceDiagnosis;
|
|
74
|
+
interface ContextProbe {
|
|
75
|
+
/** The endpoint the snapshot was read from. */
|
|
76
|
+
apiUrl: string;
|
|
77
|
+
/** `false` only for the metered archive deployment, which has accounts. */
|
|
78
|
+
selfHosted: boolean;
|
|
79
|
+
/** `true` when INSTANCE_TOKEN / SL_API_KEY resolved to a value. */
|
|
80
|
+
hasCredential: boolean;
|
|
81
|
+
/** `/public/status` when it answered, `null` when the probe failed. */
|
|
82
|
+
status: PublicStatus | null;
|
|
83
|
+
/** Why the probe failed, when it did. */
|
|
84
|
+
statusError?: string;
|
|
85
|
+
}
|
|
86
|
+
interface ContextExplanation {
|
|
87
|
+
/** One line naming the likeliest cause of the nulls below. */
|
|
88
|
+
summary: string;
|
|
89
|
+
/** Per-null-field reason. Fields that carry a value are absent. */
|
|
90
|
+
nulls: Record<string, string>;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Explain every null in a `context` snapshot.
|
|
94
|
+
*
|
|
95
|
+
* The SDK degrades each read to `null` on failure, so an all-null snapshot is
|
|
96
|
+
* indistinguishable from "not bootstrapped", "unauthorized", and "broken" —
|
|
97
|
+
* which is exactly what a caller needs to tell apart. Each reason here is
|
|
98
|
+
* derived from a live probe of the same instance, never assumed.
|
|
99
|
+
*/
|
|
100
|
+
declare function explainContextNulls(snapshot: Record<string, unknown>, probe: ContextProbe): ContextExplanation;
|
|
101
|
+
export { hasNoChainData, explainContextNulls, diagnoseInstanceStatus, SELF_HOST_DOCS_STEP, PublicStatusService, PublicStatusDecoder, PublicStatus, OBSERVER_STEP, InstanceState, InstanceIssue, InstanceDiagnosis, ContextProbe, ContextExplanation, BOOTSTRAP_STEP };
|
|
@@ -0,0 +1,134 @@
|
|
|
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/instance-diagnosis.ts
|
|
18
|
+
var BOOTSTRAP_STEP = "Restore verified history: secondlayer bootstrap --against <manifest>";
|
|
19
|
+
var OBSERVER_STEP = "Confirm your node streams into this instance — secondlayer observer --mode indexer --endpoint secondlayer:3700 prints the stanza its config needs";
|
|
20
|
+
var SELF_HOST_DOCS_STEP = "Docs: https://www.secondlayer.tools/docs/self-host";
|
|
21
|
+
function hasNoChainData(status) {
|
|
22
|
+
const integrity = status.chainIntegrity;
|
|
23
|
+
if (!integrity || integrity.reason === "check_failed")
|
|
24
|
+
return false;
|
|
25
|
+
if (integrity.maxHeight !== 0)
|
|
26
|
+
return false;
|
|
27
|
+
return (status.chainTip ?? null) === null;
|
|
28
|
+
}
|
|
29
|
+
function emptyIndexIssue(status) {
|
|
30
|
+
const decoders = status.index?.decoders ?? [];
|
|
31
|
+
const stalled = decoders.filter((d) => d.status !== "ok").length;
|
|
32
|
+
const decoderClause = stalled > 0 ? ` and ${stalled} decoder${stalled === 1 ? "" : "s"} report unavailable because there is nothing to decode` : "";
|
|
33
|
+
return {
|
|
34
|
+
title: "No blocks indexed yet — this instance holds 0 blocks.",
|
|
35
|
+
detail: `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.`,
|
|
36
|
+
nextSteps: [BOOTSTRAP_STEP, OBSERVER_STEP, SELF_HOST_DOCS_STEP]
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
var SERVICE_STEPS = [
|
|
40
|
+
"See the raw per-service report: secondlayer status --json",
|
|
41
|
+
"Check the containers from docker/oss: docker compose ps, then docker compose logs --tail=50 secondlayer"
|
|
42
|
+
];
|
|
43
|
+
function diagnoseInstanceStatus(status) {
|
|
44
|
+
const overall = String(status.status ?? "unknown");
|
|
45
|
+
if (hasNoChainData(status)) {
|
|
46
|
+
return { state: "empty-index", overall, issues: [emptyIndexIssue(status)] };
|
|
47
|
+
}
|
|
48
|
+
const issues = [];
|
|
49
|
+
for (const service of status.services ?? []) {
|
|
50
|
+
if (service.status === "ok")
|
|
51
|
+
continue;
|
|
52
|
+
issues.push({
|
|
53
|
+
title: `Service ${service.name} reports ${service.status}.`,
|
|
54
|
+
nextSteps: SERVICE_STEPS
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
const integrity = status.chainIntegrity;
|
|
58
|
+
if (integrity && integrity.ok === false) {
|
|
59
|
+
issues.push({
|
|
60
|
+
title: "Chain-data integrity check failed.",
|
|
61
|
+
detail: integrity.reason ?? "The tip the instance serves is not backed by the history it implies.",
|
|
62
|
+
nextSteps: [
|
|
63
|
+
"Verify against the archive: secondlayer verify all --against <manifest>",
|
|
64
|
+
"Repair what diverged: secondlayer repair --against <manifest> --apply"
|
|
65
|
+
]
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
if (issues.length === 0 && overall !== "healthy") {
|
|
69
|
+
issues.push({
|
|
70
|
+
title: `The API reports status=${overall} but names no failing service.`,
|
|
71
|
+
detail: "Every service in the report is ok, so the degradation is in a surface this command does not summarize.",
|
|
72
|
+
nextSteps: SERVICE_STEPS
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
state: issues.length > 0 ? "degraded" : "healthy",
|
|
77
|
+
overall,
|
|
78
|
+
issues
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
var CONTEXT_FIELDS = [
|
|
82
|
+
"account",
|
|
83
|
+
"streamsTip",
|
|
84
|
+
"indexTip",
|
|
85
|
+
"subgraphs",
|
|
86
|
+
"subscriptions",
|
|
87
|
+
"activeOperations"
|
|
88
|
+
];
|
|
89
|
+
function explainContextNulls(snapshot, probe) {
|
|
90
|
+
const nullFields = CONTEXT_FIELDS.filter((field) => (snapshot[field] ?? null) === null);
|
|
91
|
+
if (nullFields.length === 0) {
|
|
92
|
+
return { summary: "Every field resolved.", nulls: {} };
|
|
93
|
+
}
|
|
94
|
+
if (!probe.status) {
|
|
95
|
+
const reason = `No instance answered at ${probe.apiUrl} (${probe.statusError ?? "unreachable"}), so this read never reached an API.`;
|
|
96
|
+
return {
|
|
97
|
+
summary: `${reason} Start it from docker/oss with: docker compose up -d — or point SL_API_URL at the instance you meant.`,
|
|
98
|
+
nulls: Object.fromEntries(nullFields.map((field) => [field, reason]))
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
const emptyIndex = hasNoChainData(probe.status);
|
|
102
|
+
const blocks = probe.status.chainIntegrity?.maxHeight ?? null;
|
|
103
|
+
const accountReason = probe.selfHosted ? "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." : "Not authenticated — run `secondlayer login`, or export INSTANCE_TOKEN.";
|
|
104
|
+
const listReason = unresolvedReadReason(probe, blocks);
|
|
105
|
+
const chainReason = emptyIndex ? `No blocks indexed yet — this instance holds 0 blocks, so there is no tip to report. ${BOOTSTRAP_STEP}` : listReason;
|
|
106
|
+
const nulls = {};
|
|
107
|
+
for (const field of nullFields) {
|
|
108
|
+
if (field === "account")
|
|
109
|
+
nulls[field] = accountReason;
|
|
110
|
+
else if (field === "streamsTip" || field === "indexTip")
|
|
111
|
+
nulls[field] = chainReason;
|
|
112
|
+
else if (field === "activeOperations")
|
|
113
|
+
nulls[field] = (snapshot.subgraphs ?? null) === null ? "Not probed: in-flight operations are read per subgraph, and the subgraph list did not resolve." : listReason;
|
|
114
|
+
else
|
|
115
|
+
nulls[field] = listReason;
|
|
116
|
+
}
|
|
117
|
+
const summary = emptyIndex ? `Instance at ${probe.apiUrl} is reachable but holds 0 blocks — the chain fields are null because nothing has been indexed yet. ${BOOTSTRAP_STEP}` : nullFields.length === 1 && nullFields[0] === "account" ? `Instance at ${probe.apiUrl} is reachable and indexed to block ${blocks ?? "unknown"}; only \`account\` is null, which is normal when self-hosting.` : `Instance at ${probe.apiUrl} is reachable, but ${nullFields.length} field${nullFields.length === 1 ? "" : "s"} did not resolve — see \`nulls\` below for each one.`;
|
|
118
|
+
return { summary, nulls };
|
|
119
|
+
}
|
|
120
|
+
function unresolvedReadReason(probe, blocks) {
|
|
121
|
+
const where = `The instance at ${probe.apiUrl} is reachable${blocks !== null ? ` and indexed to block ${blocks}` : ""}, but this read returned nothing.`;
|
|
122
|
+
return probe.hasCredential ? `${where} Check the surface directly: secondlayer status --json` : `${where} No credential resolved: export INSTANCE_TOKEN — an instance published past loopback requires it on every /v1 read.`;
|
|
123
|
+
}
|
|
124
|
+
export {
|
|
125
|
+
hasNoChainData,
|
|
126
|
+
explainContextNulls,
|
|
127
|
+
diagnoseInstanceStatus,
|
|
128
|
+
SELF_HOST_DOCS_STEP,
|
|
129
|
+
OBSERVER_STEP,
|
|
130
|
+
BOOTSTRAP_STEP
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
//# debugId=C2B46C6D2D1CDDE564756E2164756E21
|
|
134
|
+
//# sourceMappingURL=instance-diagnosis.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/archive/instance-diagnosis.ts"],
|
|
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 / SL_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\"subscriptions\",\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 SL_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// subscription 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
|
+
],
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "C2B46C6D2D1CDDE564756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cheap, SQL-computed digests over a height range — the basis of a verify that
|
|
3
|
+
* runs in seconds instead of hours.
|
|
4
|
+
*
|
|
5
|
+
* The archive's per-object `sha256:parquet-object` digests can only be checked
|
|
6
|
+
* by regenerating the Parquet locally, which costs a full export. That is a
|
|
7
|
+
* reasonable paranoid check and a terrible first-run experience: nobody will
|
|
8
|
+
* spend two hours to discover whether they are broken. These digests are
|
|
9
|
+
* computed by the database itself, over the columns that actually carry chain
|
|
10
|
+
* identity, so an operator gets an answer before they lose interest.
|
|
11
|
+
*
|
|
12
|
+
* What this catches is not a subset chosen for convenience — it is precisely
|
|
13
|
+
* the defect class seen in production: missing heights, a canonical child whose
|
|
14
|
+
* `parent_hash` names a block we no longer hold, duplicate canonical rows at one
|
|
15
|
+
* height, and any altered block/transaction identity. What it deliberately does
|
|
16
|
+
* NOT catch is a difference in bulky payload columns (`raw_tx`, event `data`)
|
|
17
|
+
* that leaves identity intact; `--deep` re-exists for that.
|
|
18
|
+
*
|
|
19
|
+
* Determinism rules, so two independent databases agree:
|
|
20
|
+
* - Aggregate over an explicit ORDER BY, never table order.
|
|
21
|
+
* - Hash each row to a fixed-width digest FIRST, then aggregate those, so the
|
|
22
|
+
* intermediate string is 32 bytes/row rather than the whole row.
|
|
23
|
+
* - Normalize NULL to a sentinel that cannot collide with a real value.
|
|
24
|
+
* - Never include a column whose representation is environment-dependent
|
|
25
|
+
* (timestamps with timezone rendering, generated ids, `created_at`).
|
|
26
|
+
*/
|
|
27
|
+
declare const RANGE_DIGEST_SPEC: "md5:sql-identity-v1";
|
|
28
|
+
type RangeDigestDataset = "blocks" | "transactions" | "events";
|
|
29
|
+
type RangeDigest = {
|
|
30
|
+
dataset: RangeDigestDataset
|
|
31
|
+
from_block: number
|
|
32
|
+
to_block: number
|
|
33
|
+
row_count: number
|
|
34
|
+
/** null when the range holds no rows — an empty range has no digest, and
|
|
35
|
+
* that is materially different from "a digest that happens to be of zero
|
|
36
|
+
* rows", which would compare equal across datasets. */
|
|
37
|
+
digest: string | null
|
|
38
|
+
digest_spec: typeof RANGE_DIGEST_SPEC
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* v1 canonical byte encoding for chain identity — the shape that lets
|
|
42
|
+
* independently-run runtimes agree on the same block.
|
|
43
|
+
*
|
|
44
|
+
* The exact-body digest (`sha256:parquet-object`) proves capture fidelity to one
|
|
45
|
+
* source: replay the same bytes, get the same rows. It cannot prove two nodes
|
|
46
|
+
* observed the same chain, because their Parquet encoders legally differ in
|
|
47
|
+
* column padding, compression, JSON key order, and null placement — two
|
|
48
|
+
* operators processing the same block can ship byte-different Parquets that
|
|
49
|
+
* carry identical content.
|
|
50
|
+
*
|
|
51
|
+
* The semantic digest is the shape that lets independent runtimes agree. Every
|
|
52
|
+
* value on a block/transaction/event is normalized into a canonical byte string
|
|
53
|
+
* by rules that MUST NOT change without a version bump. Two v1 computations
|
|
54
|
+
* over the same range agree, or one is wrong about the chain — never about
|
|
55
|
+
* formatting.
|
|
56
|
+
*
|
|
57
|
+
* Encoding rules (v1):
|
|
58
|
+
* - Fields join with `\x1f` (ASCII US, unit separator — cannot legally
|
|
59
|
+
* appear in any chain value; a printable delimiter risks collision with
|
|
60
|
+
* contract-emitted strings).
|
|
61
|
+
* - Null encodes as `\x00` (single NUL). Distinct from empty string `""`.
|
|
62
|
+
* - Integers encode as their base-10 string. bigint → decimal string.
|
|
63
|
+
* - JSON payload columns (function_args, event data) encode as canonical
|
|
64
|
+
* JSON: object keys sorted lexicographically, no whitespace, bigint →
|
|
65
|
+
* quoted decimal string, non-finite numbers rejected.
|
|
66
|
+
* - Field order per record type is fixed and never reordered.
|
|
67
|
+
* - Digest = sha256(bytes), lowercase hex.
|
|
68
|
+
*
|
|
69
|
+
* Rollup rules (v1) — order MUST match the archive's stream order or an
|
|
70
|
+
* honest re-will disagree with the manifest:
|
|
71
|
+
* - blocks: (height asc)
|
|
72
|
+
* - transactions: (block_height asc, tx_index asc, tx_id asc)
|
|
73
|
+
* - events: (block_height asc, event_index asc, tx_id asc)
|
|
74
|
+
* Per-partition rollup is `sha256(concat(row_digest_bytes))` in stream order.
|
|
75
|
+
*
|
|
76
|
+
* Test vectors under `packages/shared/src/archive/__fixtures__/semantic-digest/`
|
|
77
|
+
* pin the encoding. Any change to bytes must bump the spec version.
|
|
78
|
+
*/
|
|
79
|
+
declare const SEMANTIC_DIGEST_SPEC_V1: "sha256:semantic-v1";
|
|
80
|
+
type SemanticDigestSpec = typeof SEMANTIC_DIGEST_SPEC_V1;
|
|
81
|
+
/**
|
|
82
|
+
* Per-partition digest record — the shape that lands in the snapshot manifest.
|
|
83
|
+
* Complements the existing `sha256:parquet-object` per-partition digest by
|
|
84
|
+
* proving semantic identity independent of file encoding.
|
|
85
|
+
*/
|
|
86
|
+
interface PartitionSemanticDigest {
|
|
87
|
+
dataset: "blocks" | "transactions" | "events";
|
|
88
|
+
from_block: number;
|
|
89
|
+
to_block: number;
|
|
90
|
+
row_count: number;
|
|
91
|
+
digest: string | null;
|
|
92
|
+
digest_spec: SemanticDigestSpec;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Loading and trusting an archive reference — shared by the CLI and the SDK
|
|
96
|
+
* so the two cannot disagree about what "the archive says".
|
|
97
|
+
*
|
|
98
|
+
* A reference is either an https URL or a local path to a snapshot manifest.
|
|
99
|
+
* The SDK only passes `http(s)://` sources; local paths stay CLI. Partition
|
|
100
|
+
* objects live beside the snapshot at `<root>/<dataset>/<name>.parquet`,
|
|
101
|
+
* where the root is two levels above the manifest
|
|
102
|
+
* (`<root>/snapshots/<digest>.json`).
|
|
103
|
+
*/
|
|
104
|
+
type ArchivePartition = {
|
|
105
|
+
dataset: string
|
|
106
|
+
from_block: number
|
|
107
|
+
to_block: number
|
|
108
|
+
path: string
|
|
109
|
+
row_count: number
|
|
110
|
+
byte_size: number
|
|
111
|
+
sha256: string
|
|
112
|
+
};
|
|
113
|
+
type ArchiveManifest = {
|
|
114
|
+
network?: string
|
|
115
|
+
coverage?: {
|
|
116
|
+
from_block: number
|
|
117
|
+
to_block: number
|
|
118
|
+
}
|
|
119
|
+
partition_size_blocks?: number
|
|
120
|
+
range_digests?: RangeDigest[]
|
|
121
|
+
partition_semantic_digests?: PartitionSemanticDigest[]
|
|
122
|
+
partitions?: ArchivePartition[]
|
|
123
|
+
signature?: string
|
|
124
|
+
key_id?: string
|
|
125
|
+
[key: string]: unknown
|
|
126
|
+
};
|
|
127
|
+
type LoadedReference = {
|
|
128
|
+
manifest: ArchiveManifest
|
|
129
|
+
origin: string
|
|
130
|
+
/** Base for resolving partition paths; a URL prefix or a directory. */
|
|
131
|
+
root: string
|
|
132
|
+
isRemote: boolean
|
|
133
|
+
};
|
|
134
|
+
type LoadReferenceOptions = {
|
|
135
|
+
/** Key used to verify a signed `latest.json` pointer. The pointer names
|
|
136
|
+
* which snapshot is current, so without this an attacker holding only
|
|
137
|
+
* bucket write access could roll every reader back to an older, still
|
|
138
|
+
* validly signed, snapshot. */
|
|
139
|
+
publicKeyPem?: string
|
|
140
|
+
/** Test/SDK seam; production CLI leaves this unset and uses global fetch. */
|
|
141
|
+
fetchImpl?: (input: string | URL | Request, init?: RequestInit) => Promise<Response>
|
|
142
|
+
};
|
|
143
|
+
declare function loadReference(source: string, options?: LoadReferenceOptions): Promise<LoadedReference>;
|
|
144
|
+
type SignatureResult = {
|
|
145
|
+
verified: boolean
|
|
146
|
+
reason?: string
|
|
147
|
+
};
|
|
148
|
+
/**
|
|
149
|
+
* An unsigned or unverifiable reference is worthless as a source of truth:
|
|
150
|
+
* comparing against it yields a confident answer with nothing behind it, and
|
|
151
|
+
* repairing from it would write unverified data into a live database.
|
|
152
|
+
*/
|
|
153
|
+
declare function checkSignature(manifest: ArchiveManifest, publicKeyPem: string | undefined, insecure: boolean): SignatureResult;
|
|
154
|
+
/**
|
|
155
|
+
* Which key a manifest is checked against, in trust order: an explicit
|
|
156
|
+
* `--public-key`, then `ARCHIVE_SIGNING_PUBLIC_KEY` / `STREAMS_SIGNING_PUBLIC_KEY`
|
|
157
|
+
* from the environment, then the hosted key endpoint over https (never in OSS
|
|
158
|
+
* mode, which must work with no path to api.secondlayer.tools), and finally the
|
|
159
|
+
* key compiled into this release. The compiled key means a fresh self-hosted
|
|
160
|
+
* instance can bootstrap offline; the tradeoff is that rotating it is a CLI
|
|
161
|
+
* release. Plaintext `http://` key sources are never consulted.
|
|
162
|
+
*/
|
|
163
|
+
declare function resolveArchivePublicKey(input: {
|
|
164
|
+
explicitPem?: string
|
|
165
|
+
envPem?: string
|
|
166
|
+
allowHostedApi?: boolean
|
|
167
|
+
/** Test seam for the hosted endpoint; production callers leave it unset. */
|
|
168
|
+
hostedKeyUrl?: string
|
|
169
|
+
}): Promise<string>;
|
|
170
|
+
export { resolveArchivePublicKey, loadReference, checkSignature, SignatureResult, LoadedReference, LoadReferenceOptions, ArchivePartition, ArchiveManifest };
|
|
@@ -0,0 +1,231 @@
|
|
|
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/streams-bulk-manifest.ts
|
|
71
|
+
function canonicalStreamsBulkManifestPayload(manifest) {
|
|
72
|
+
const { signature: _signature, key_id: _keyId, ...rest } = manifest;
|
|
73
|
+
return JSON.stringify(rest);
|
|
74
|
+
}
|
|
75
|
+
function normalizePem(pem) {
|
|
76
|
+
return pem.includes("\\n") ? pem.replace(/\\n/g, `
|
|
77
|
+
`) : pem;
|
|
78
|
+
}
|
|
79
|
+
function signStreamsBulkManifest(manifest, privateKeyPem) {
|
|
80
|
+
const pem = normalizePem(privateKeyPem);
|
|
81
|
+
const privateKey = loadEd25519PrivateKey(pem);
|
|
82
|
+
const keyId = ed25519KeyId(publicKeyPemFromPrivate(pem));
|
|
83
|
+
const { signature: _signature, key_id: _keyId, ...base } = manifest;
|
|
84
|
+
const payload = JSON.stringify(base);
|
|
85
|
+
return {
|
|
86
|
+
...base,
|
|
87
|
+
signature: signEd25519(payload, privateKey),
|
|
88
|
+
key_id: keyId
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function verifyStreamsBulkManifestSignature(manifest, publicKeyPem) {
|
|
92
|
+
if (!manifest.signature)
|
|
93
|
+
return false;
|
|
94
|
+
const payload = canonicalStreamsBulkManifestPayload(manifest);
|
|
95
|
+
return verifyEd25519(payload, manifest.signature, loadEd25519PublicKey(publicKeyPem));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// src/archive/root-key.ts
|
|
99
|
+
var ARCHIVE_ROOT_PUBLIC_KEY_PEM = [
|
|
100
|
+
"-----BEGIN PUBLIC KEY-----",
|
|
101
|
+
"MCowBQYDK2VwAyEAq7wXm235JkFbElDmTVdNtmTKqRLx4PzjBgZhyREUFis=",
|
|
102
|
+
"-----END PUBLIC KEY-----",
|
|
103
|
+
""
|
|
104
|
+
].join(`
|
|
105
|
+
`);
|
|
106
|
+
var ARCHIVE_ROOT_KEY_ID = "fHQWzs9ML2WIYakf";
|
|
107
|
+
|
|
108
|
+
// src/archive/reference.ts
|
|
109
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
110
|
+
import { readFile } from "node:fs/promises";
|
|
111
|
+
import { dirname, join, resolve } from "node:path";
|
|
112
|
+
function isLatestPointer(value) {
|
|
113
|
+
const pointer = value;
|
|
114
|
+
return typeof pointer.snapshot_path === "string" && (value.partitions === undefined || value.partitions.length === 0);
|
|
115
|
+
}
|
|
116
|
+
var SNAPSHOT_PATH_PATTERN = /^snapshots\/[0-9a-f]{64}\.json$/;
|
|
117
|
+
async function fetchJson(url, fetchImpl) {
|
|
118
|
+
const response = await fetchImpl(url, {
|
|
119
|
+
signal: AbortSignal.timeout(30000)
|
|
120
|
+
});
|
|
121
|
+
if (!response.ok) {
|
|
122
|
+
throw new Error(`could not fetch manifest (${response.status} ${response.statusText})`);
|
|
123
|
+
}
|
|
124
|
+
return response.json();
|
|
125
|
+
}
|
|
126
|
+
async function loadReference(source, options = {}) {
|
|
127
|
+
const isRemote = /^https?:\/\//.test(source);
|
|
128
|
+
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
129
|
+
let manifest;
|
|
130
|
+
let root;
|
|
131
|
+
if (isRemote) {
|
|
132
|
+
manifest = await fetchJson(source, fetchImpl);
|
|
133
|
+
const url = new URL(source);
|
|
134
|
+
url.pathname = url.pathname.replace(/\/snapshots\/[^/]+$/, "").replace(/\/[^/]+\.json$/, "");
|
|
135
|
+
root = url.toString();
|
|
136
|
+
} else {
|
|
137
|
+
const path = resolve(source);
|
|
138
|
+
manifest = JSON.parse(await readFile(path, "utf8"));
|
|
139
|
+
root = isLatestPointer(manifest) ? dirname(path) : dirname(dirname(path));
|
|
140
|
+
}
|
|
141
|
+
if (!isLatestPointer(manifest)) {
|
|
142
|
+
return { manifest, origin: source, root, isRemote };
|
|
143
|
+
}
|
|
144
|
+
const pointer = manifest;
|
|
145
|
+
const snapshotPath = pointer.snapshot_path;
|
|
146
|
+
if (!SNAPSHOT_PATH_PATTERN.test(snapshotPath)) {
|
|
147
|
+
throw new Error(`pointer names an invalid snapshot path: ${snapshotPath} (expected snapshots/<sha256>.json)`);
|
|
148
|
+
}
|
|
149
|
+
if (typeof pointer.snapshot_digest !== "string" || !/^[0-9a-f]{64}$/.test(pointer.snapshot_digest)) {
|
|
150
|
+
throw new Error("pointer carries no snapshot_digest, so it cannot prove which snapshot is current");
|
|
151
|
+
}
|
|
152
|
+
if (pointer.signature !== undefined) {
|
|
153
|
+
if (!options.publicKeyPem) {
|
|
154
|
+
throw new Error("pointer is signed but no public key is available to verify it");
|
|
155
|
+
}
|
|
156
|
+
let verified = false;
|
|
157
|
+
try {
|
|
158
|
+
verified = verifyStreamsBulkManifestSignature(pointer, options.publicKeyPem);
|
|
159
|
+
} catch {
|
|
160
|
+
verified = false;
|
|
161
|
+
}
|
|
162
|
+
if (!verified) {
|
|
163
|
+
throw new Error("pointer signature did not verify against the archive key");
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const resolved = isRemote ? await fetchJson(`${root.replace(/\/$/, "")}/${snapshotPath}`, fetchImpl) : JSON.parse(await readFile(join(root, snapshotPath), "utf8"));
|
|
167
|
+
const digest = createHash2("sha256").update(JSON.stringify((({ signature: _s, key_id: _k, ...rest }) => rest)(resolved))).digest("hex");
|
|
168
|
+
if (digest !== pointer.snapshot_digest) {
|
|
169
|
+
throw new Error(`pointer/snapshot mismatch: latest.json names ${pointer.snapshot_digest}, resolved manifest hashes to ${digest}`);
|
|
170
|
+
}
|
|
171
|
+
return { manifest: resolved, origin: source, root, isRemote };
|
|
172
|
+
}
|
|
173
|
+
function checkSignature(manifest, publicKeyPem, insecure) {
|
|
174
|
+
if (insecure)
|
|
175
|
+
return { verified: false, reason: "signature check skipped" };
|
|
176
|
+
if (!manifest.signature) {
|
|
177
|
+
return { verified: false, reason: "manifest carries no signature" };
|
|
178
|
+
}
|
|
179
|
+
if (!publicKeyPem) {
|
|
180
|
+
return {
|
|
181
|
+
verified: false,
|
|
182
|
+
reason: "no public key available to verify with"
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
try {
|
|
186
|
+
const verified = verifyStreamsBulkManifestSignature(manifest, publicKeyPem);
|
|
187
|
+
return {
|
|
188
|
+
verified,
|
|
189
|
+
reason: verified ? undefined : "signature did not verify"
|
|
190
|
+
};
|
|
191
|
+
} catch (err) {
|
|
192
|
+
return {
|
|
193
|
+
verified: false,
|
|
194
|
+
reason: err instanceof Error ? err.message : "signature check failed"
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
var HOSTED_SIGNING_KEY_URL = "https://api.secondlayer.tools/public/streams/signing-key";
|
|
199
|
+
async function fetchHostedKey(url) {
|
|
200
|
+
if (!url.startsWith("https://"))
|
|
201
|
+
return;
|
|
202
|
+
try {
|
|
203
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(1e4) });
|
|
204
|
+
if (!response.ok)
|
|
205
|
+
return;
|
|
206
|
+
const body = await response.json();
|
|
207
|
+
return typeof body.public_key_pem === "string" ? body.public_key_pem : undefined;
|
|
208
|
+
} catch {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
async function resolveArchivePublicKey(input) {
|
|
213
|
+
if (input.explicitPem)
|
|
214
|
+
return input.explicitPem;
|
|
215
|
+
if (input.envPem)
|
|
216
|
+
return input.envPem;
|
|
217
|
+
if (input.allowHostedApi) {
|
|
218
|
+
const hosted = await fetchHostedKey(input.hostedKeyUrl ?? HOSTED_SIGNING_KEY_URL);
|
|
219
|
+
if (hosted)
|
|
220
|
+
return hosted;
|
|
221
|
+
}
|
|
222
|
+
return ARCHIVE_ROOT_PUBLIC_KEY_PEM;
|
|
223
|
+
}
|
|
224
|
+
export {
|
|
225
|
+
resolveArchivePublicKey,
|
|
226
|
+
loadReference,
|
|
227
|
+
checkSignature
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
//# debugId=C1F257F42AE1841A64756E2164756E21
|
|
231
|
+
//# sourceMappingURL=reference.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/crypto/ed25519.ts", "../src/streams-bulk-manifest.ts", "../src/archive/root-key.ts", "../src/archive/reference.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\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",
|
|
7
|
+
"/**\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",
|
|
8
|
+
"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"
|
|
9
|
+
],
|
|
10
|
+
"mappings": "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",
|
|
11
|
+
"debugId": "C1F257F42AE1841A64756E2164756E21",
|
|
12
|
+
"names": []
|
|
13
|
+
}
|
package/dist/src/index-http.js
CHANGED
|
@@ -19,6 +19,7 @@ var INDEX_INTERNAL_TENANT_ID = "tenant_index_internal";
|
|
|
19
19
|
var INTERNAL_STREAMS_TENANT_ID = "tenant_streams_decode_internal";
|
|
20
20
|
var DEFAULT_INTERNAL_INDEX_BASE_URL = "http://api:3800";
|
|
21
21
|
var MISSING_INTERNAL_STREAMS_API_KEY = "Set STREAMS_INTERNAL_API_KEY (archive; prefix sl-int_) or INSTANCE_TOKEN (OSS). Empty env no longer falls back to a committed secret.";
|
|
22
|
+
var MISSING_INTERNAL_INDEX_API_KEY = "Set INDEX_INTERNAL_API_KEY (archive; prefix sl-int_) or INSTANCE_TOKEN (OSS). Without one, Index reads go out anonymously and are capped at the last 24h, so any seek into older history 402s.";
|
|
22
23
|
function defaultInternalIndexApiKey(env = process.env) {
|
|
23
24
|
const internal = env.INDEX_INTERNAL_API_KEY?.trim();
|
|
24
25
|
if (internal)
|
|
@@ -37,6 +38,12 @@ function defaultInternalStreamsApiKey(env = process.env) {
|
|
|
37
38
|
return instance;
|
|
38
39
|
return;
|
|
39
40
|
}
|
|
41
|
+
function requireInternalIndexApiKey(env = process.env) {
|
|
42
|
+
const key = defaultInternalIndexApiKey(env);
|
|
43
|
+
if (!key)
|
|
44
|
+
throw new Error(MISSING_INTERNAL_INDEX_API_KEY);
|
|
45
|
+
return key;
|
|
46
|
+
}
|
|
40
47
|
function requireInternalStreamsApiKey(env = process.env) {
|
|
41
48
|
const key = defaultInternalStreamsApiKey(env);
|
|
42
49
|
if (!key)
|
|
@@ -179,5 +186,5 @@ export {
|
|
|
179
186
|
IndexHttpClient
|
|
180
187
|
};
|
|
181
188
|
|
|
182
|
-
//# debugId=
|
|
189
|
+
//# debugId=1D8416E6B0F3E28D64756E2164756E21
|
|
183
190
|
//# sourceMappingURL=index-http.js.map
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/index-internal-auth.ts", "../src/index-http.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"// Internal service credentials for first-party consumers of /v1/index and\n// /v1/streams over HTTP (decoder, subgraph processor PublicApiBlockSource).\n// Seeded as internal tenants with NO account_id, so these reads are\n// unmetered (metering gates on account_id). Lives in shared so the API (seed)\n// and the consumers import it without a cycle.\n//\n// Resolution: STREAMS_INTERNAL_API_KEY / INDEX_INTERNAL_API_KEY, then\n// INSTANCE_TOKEN (OSS). No committed fallback. Empty-string env values fall\n// through (`trim()`, then empty): compose interpolates\n// `${STREAMS_INTERNAL_API_KEY:-}` which is \"\" when unset.\n//\n// Archive keys must not use the `sk-sl_` prefix; generate with `sl-int_`.\n// OSS compose should omit the internal env and send INSTANCE_TOKEN.\nexport const INDEX_INTERNAL_TENANT_ID = \"tenant_index_internal\";\nexport const INTERNAL_STREAMS_TENANT_ID = \"tenant_streams_decode_internal\";\n\nconst DEFAULT_INTERNAL_INDEX_BASE_URL = \"http://api:3800\";\n\nconst MISSING_INTERNAL_STREAMS_API_KEY =\n\t\"Set STREAMS_INTERNAL_API_KEY (archive; prefix sl-int_) or INSTANCE_TOKEN (OSS). Empty env no longer falls back to a committed secret.\";\n\nexport function defaultInternalIndexApiKey(\n\tenv: NodeJS.ProcessEnv = process.env,\n): string | undefined {\n\tconst internal = env.INDEX_INTERNAL_API_KEY?.trim();\n\tif (internal) return internal;\n\tconst instance = env.INSTANCE_TOKEN?.trim();\n\tif (instance) return instance;\n\treturn undefined;\n}\n\nexport function defaultInternalStreamsApiKey(\n\tenv: NodeJS.ProcessEnv = process.env,\n): string | undefined {\n\tconst internal = env.STREAMS_INTERNAL_API_KEY?.trim();\n\tif (internal) return internal;\n\tconst instance = env.INSTANCE_TOKEN?.trim();\n\tif (instance) return instance;\n\treturn undefined;\n}\n\n/** Decoder startup: refuse to run with neither credential. */\nexport function requireInternalStreamsApiKey(\n\tenv: NodeJS.ProcessEnv = process.env,\n): string {\n\tconst key = defaultInternalStreamsApiKey(env);\n\tif (!key) throw new Error(MISSING_INTERNAL_STREAMS_API_KEY);\n\treturn key;\n}\n\n/** Shared env contract for decoder + subgraph HTTP dogfood. */\nexport function defaultInternalIndexBaseUrl(): string {\n\treturn (\n\t\tprocess.env.SUBGRAPH_INDEX_API_URL ||\n\t\tprocess.env.STREAMS_API_URL ||\n\t\tDEFAULT_INTERNAL_INDEX_BASE_URL\n\t);\n}\n",
|
|
5
|
+
"// Internal service credentials for first-party consumers of /v1/index and\n// /v1/streams over HTTP (decoder, subgraph processor PublicApiBlockSource).\n// Seeded as internal tenants with NO account_id, so these reads are\n// unmetered (metering gates on account_id). Lives in shared so the API (seed)\n// and the consumers import it without a cycle.\n//\n// Resolution: STREAMS_INTERNAL_API_KEY / INDEX_INTERNAL_API_KEY, then\n// INSTANCE_TOKEN (OSS). No committed fallback. Empty-string env values fall\n// through (`trim()`, then empty): compose interpolates\n// `${STREAMS_INTERNAL_API_KEY:-}` which is \"\" when unset.\n//\n// Archive keys must not use the `sk-sl_` prefix; generate with `sl-int_`.\n// OSS compose should omit the internal env and send INSTANCE_TOKEN.\nexport const INDEX_INTERNAL_TENANT_ID = \"tenant_index_internal\";\nexport const INTERNAL_STREAMS_TENANT_ID = \"tenant_streams_decode_internal\";\n\nconst DEFAULT_INTERNAL_INDEX_BASE_URL = \"http://api:3800\";\n\nconst MISSING_INTERNAL_STREAMS_API_KEY =\n\t\"Set STREAMS_INTERNAL_API_KEY (archive; prefix sl-int_) or INSTANCE_TOKEN (OSS). Empty env no longer falls back to a committed secret.\";\n\nconst MISSING_INTERNAL_INDEX_API_KEY =\n\t\"Set INDEX_INTERNAL_API_KEY (archive; prefix sl-int_) or INSTANCE_TOKEN (OSS). Without one, Index reads go out anonymously and are capped at the last 24h, so any seek into older history 402s.\";\n\nexport function defaultInternalIndexApiKey(\n\tenv: NodeJS.ProcessEnv = process.env,\n): string | undefined {\n\tconst internal = env.INDEX_INTERNAL_API_KEY?.trim();\n\tif (internal) return internal;\n\tconst instance = env.INSTANCE_TOKEN?.trim();\n\tif (instance) return instance;\n\treturn undefined;\n}\n\nexport function defaultInternalStreamsApiKey(\n\tenv: NodeJS.ProcessEnv = process.env,\n): string | undefined {\n\tconst internal = env.STREAMS_INTERNAL_API_KEY?.trim();\n\tif (internal) return internal;\n\tconst instance = env.INSTANCE_TOKEN?.trim();\n\tif (instance) return instance;\n\treturn undefined;\n}\n\n/**\n * Platform-mode Index consumer: refuse to run with neither credential.\n *\n * A keyless Index read is not an error — it is an anonymous read, and the API\n * serves it for the last 24h (`api/src/index/free-window.ts`). That makes a\n * missing key invisible right up until a consumer's cursor falls out of the\n * window, at which point every read 402s and the container goes unhealthy,\n * possibly weeks after the misconfiguration. Callers that seek older history\n * on a platform-mode instance should fail here at startup instead.\n *\n * Self-hosted (oss/dedicated) instances are not gated, so they must NOT call\n * this — `defaultInternalIndexApiKey()` returning undefined is correct there.\n */\nexport function requireInternalIndexApiKey(\n\tenv: NodeJS.ProcessEnv = process.env,\n): string {\n\tconst key = defaultInternalIndexApiKey(env);\n\tif (!key) throw new Error(MISSING_INTERNAL_INDEX_API_KEY);\n\treturn key;\n}\n\n/** Decoder startup: refuse to run with neither credential. */\nexport function requireInternalStreamsApiKey(\n\tenv: NodeJS.ProcessEnv = process.env,\n): string {\n\tconst key = defaultInternalStreamsApiKey(env);\n\tif (!key) throw new Error(MISSING_INTERNAL_STREAMS_API_KEY);\n\treturn key;\n}\n\n/** Shared env contract for decoder + subgraph HTTP dogfood. */\nexport function defaultInternalIndexBaseUrl(): string {\n\treturn (\n\t\tprocess.env.SUBGRAPH_INDEX_API_URL ||\n\t\tprocess.env.STREAMS_API_URL ||\n\t\tDEFAULT_INTERNAL_INDEX_BASE_URL\n\t);\n}\n",
|
|
6
6
|
"import {\n\tdefaultInternalIndexApiKey,\n\tdefaultInternalIndexBaseUrl,\n\tdefaultInternalStreamsApiKey,\n} from \"./index-internal-auth.ts\";\n\n/**\n * Low-level transport for the public Index (`/v1/index`) + Streams clock\n * (`/v1/streams`) HTTP APIs: cursor-paginated reads, tip, reorgs. Lives in\n * `shared` (a leaf both the SDK and the subgraph runtime depend on) so the wire\n * format has one home and no package cycle. The SDK's ergonomic client should\n * eventually consume these row types too (see the plan's convergence task).\n *\n * This is intentionally minimal — just the GETs the subgraph runtime's\n * PublicApiBlockSource needs. It is NOT the SDK's full client (walk/consume/\n * retries/auth resolution).\n */\n\nconst PAGE_LIMIT = 1000;\n\n// Transport resilience for the streams-index data plane. The api runs N>1\n// replicas behind Caddy; during a rolling deploy one replica is briefly\n// unreachable, surfacing as a thrown fetch (connection refused/reset) or a\n// Caddy 502/503/504 while it fails over. Retrying a few times with backoff\n// makes a single-replica recreate transparent to the subgraph-processor /\n// decoder — closing the processors-depend-on-api coupling.\nconst MAX_ATTEMPTS = 4;\nconst RETRY_BASE_MS = 150;\nconst RETRYABLE_STATUS = new Set([502, 503, 504]);\n\nconst delay = (ms: number): Promise<void> =>\n\tnew Promise((resolve) => setTimeout(resolve, ms));\n\ntype Envelope<K extends string, T> = {\n\t[P in K]: T[];\n} & { next_cursor: string | null };\n\n// ── Index API wire shapes (single source of truth) ─────────────────────────\nexport type IndexBlockRow = {\n\tblock_height: number;\n\tblock_hash: string;\n\tparent_hash: string;\n\tburn_block_height: number;\n\tburn_block_hash: string | null;\n\t/** Nakamoto StacksBlockId. Absent from pre-2026-07 API deployments. */\n\tindex_block_hash?: string | null;\n\tblock_time: string | null;\n};\n\ntype IndexEventCommon = {\n\tblock_height: number;\n\ttx_id: string;\n\ttx_index?: number;\n\tevent_index: number;\n\tcontract_id: string | null;\n\t/** Submitting-tx context — present only when the read passed `tx_context=true`.\n\t * Lets a consumer build the tx for `ctx.tx` without a separate\n\t * walkTransactions (the ~37x reindex over-fetch; see indexing-speed plan). */\n\ttx_sender?: string | null;\n\ttx_type?: string | null;\n\ttx_status?: string | null;\n\ttx_contract_id?: string | null;\n\ttx_function_name?: string | null;\n};\n\nexport type IndexEventRow = IndexEventCommon &\n\t(\n\t\t| {\n\t\t\t\tevent_type: \"ft_transfer\" | \"ft_mint\" | \"ft_burn\";\n\t\t\t\tasset_identifier: string;\n\t\t\t\tsender?: string;\n\t\t\t\trecipient?: string;\n\t\t\t\tamount: string;\n\t\t }\n\t\t| {\n\t\t\t\tevent_type: \"nft_transfer\" | \"nft_mint\" | \"nft_burn\";\n\t\t\t\tasset_identifier: string;\n\t\t\t\tsender?: string;\n\t\t\t\trecipient?: string;\n\t\t\t\tvalue: string;\n\t\t }\n\t\t| {\n\t\t\t\tevent_type: \"stx_transfer\" | \"stx_mint\" | \"stx_burn\";\n\t\t\t\tsender?: string;\n\t\t\t\trecipient?: string;\n\t\t\t\tamount: string;\n\t\t\t\tmemo?: string | null;\n\t\t }\n\t\t| {\n\t\t\t\tevent_type: \"stx_lock\";\n\t\t\t\tsender: string;\n\t\t\t\tamount: string;\n\t\t\t\tpayload: { unlock_height: string | null };\n\t\t }\n\t\t| {\n\t\t\t\tevent_type: \"print\";\n\t\t\t\tpayload: {\n\t\t\t\t\ttopic: string | null;\n\t\t\t\t\tvalue: unknown;\n\t\t\t\t\traw_value: string | null;\n\t\t\t\t};\n\t\t }\n\t);\n\nexport type IndexTransactionRow = {\n\ttx_id: string;\n\tblock_height: number;\n\tblock_time?: string | null;\n\tburn_block_height?: number | null;\n\ttx_index: number;\n\ttx_type: string;\n\tsender: string;\n\tstatus: string;\n\tcontract_call?: {\n\t\tcontract_id: string;\n\t\tfunction_name: string;\n\t\tfunction_args_hex?: string[] | null;\n\t\tresult_hex?: string | null;\n\t} | null;\n\tsmart_contract?: { contract_id: string | null } | null;\n};\n\nexport type StreamsReorgRow = {\n\tid: string;\n\tdetected_at: string;\n\tfork_point_height: number;\n\torphaned_range: { from: string; to: string };\n\tnew_canonical_tip: string;\n};\n\nexport type IndexHttpOptions = {\n\t/** Base URL for /v1/index (the decoded data plane). */\n\tindexBaseUrl: string;\n\t/** Bearer for /v1/index. Empty/unset omits the header. */\n\tindexApiKey?: string;\n\t/** Base URL for /v1/streams (the canonical clock). */\n\tstreamsBaseUrl: string;\n\t/** Bearer for /v1/streams. Empty/unset omits the header. */\n\tstreamsApiKey?: string;\n};\n\nexport class IndexHttpClient {\n\tprivate readonly indexBaseUrl: string;\n\tprivate readonly indexApiKey: string | undefined;\n\tprivate readonly streamsBaseUrl: string;\n\tprivate readonly streamsApiKey: string | undefined;\n\n\tconstructor(opts: IndexHttpOptions) {\n\t\tthis.indexBaseUrl = opts.indexBaseUrl.replace(/\\/+$/, \"\");\n\t\tthis.indexApiKey = opts.indexApiKey ?? defaultInternalIndexApiKey();\n\t\tthis.streamsBaseUrl = opts.streamsBaseUrl.replace(/\\/+$/, \"\");\n\t\tthis.streamsApiKey = opts.streamsApiKey;\n\t}\n\n\tprivate async get<T>(url: string, apiKey: string | undefined): Promise<T> {\n\t\t// Index reads are anon — omit the header entirely when no key is set, so\n\t\t// an empty key reads anonymously rather than 401-ing as an invalid bearer.\n\t\tconst headers: Record<string, string> = apiKey\n\t\t\t? { authorization: `Bearer ${apiKey}` }\n\t\t\t: {};\n\t\tlet lastErr: unknown;\n\t\tfor (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {\n\t\t\tlet res: Response;\n\t\t\ttry {\n\t\t\t\tres = await fetch(url, { headers });\n\t\t\t} catch (err) {\n\t\t\t\t// An explicit abort/cancel is intentional — surface it immediately\n\t\t\t\t// rather than burning the retry budget masking it as transient.\n\t\t\t\tif (err instanceof Error && err.name === \"AbortError\") throw err;\n\t\t\t\t// Otherwise a transport-level failure (connection refused/reset) —\n\t\t\t\t// e.g. an api replica mid-recreate. Retry; the next attempt\n\t\t\t\t// round-robins to a healthy replica.\n\t\t\t\tlastErr = err;\n\t\t\t\tif (attempt >= MAX_ATTEMPTS) break;\n\t\t\t\tawait delay(RETRY_BASE_MS * 2 ** (attempt - 1));\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (!res.ok) {\n\t\t\t\tif (RETRYABLE_STATUS.has(res.status) && attempt < MAX_ATTEMPTS) {\n\t\t\t\t\t// Drain the body so the connection can be reused, then back off.\n\t\t\t\t\tawait res.text().catch(() => {});\n\t\t\t\t\tawait delay(RETRY_BASE_MS * 2 ** (attempt - 1));\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tthrow new Error(`GET ${url} → ${res.status} ${await res.text()}`);\n\t\t\t}\n\t\t\treturn (await res.json()) as T;\n\t\t}\n\t\tthrow (\n\t\t\tlastErr ?? new Error(`GET ${url} failed after ${MAX_ATTEMPTS} attempts`)\n\t\t);\n\t}\n\n\t/** Fetch a single cursor page of an Index collection. */\n\tprivate async getPage<K extends string, T>(\n\t\tpath: string,\n\t\tkey: K,\n\t\tparams: URLSearchParams,\n\t): Promise<{ items: T[]; next_cursor: string | null }> {\n\t\tconst env: Envelope<K, T> = await this.get(\n\t\t\t`${this.indexBaseUrl}${path}?${params}`,\n\t\t\tthis.indexApiKey,\n\t\t);\n\t\treturn { items: env[key], next_cursor: env.next_cursor };\n\t}\n\n\t/** Drain a cursor-paginated Index collection over [fromHeight, toHeight]. */\n\tprivate async walk<K extends string, T>(\n\t\tpath: string,\n\t\tkey: K,\n\t\tfromHeight: number,\n\t\ttoHeight: number,\n\t\textraParams: Record<string, string> = {},\n\t): Promise<T[]> {\n\t\tconst out: T[] = [];\n\t\tlet cursor: string | null = null;\n\t\tdo {\n\t\t\tconst params = new URLSearchParams({\n\t\t\t\tto_height: String(toHeight),\n\t\t\t\tlimit: String(PAGE_LIMIT),\n\t\t\t\t...extraParams,\n\t\t\t});\n\t\t\t// from_height and cursor are mutually exclusive — anchor the first page\n\t\t\t// by height, then page forward by cursor only.\n\t\t\tif (cursor) params.set(\"cursor\", cursor);\n\t\t\telse params.set(\"from_height\", String(fromHeight));\n\t\t\tconst { items, next_cursor } = await this.getPage<K, T>(\n\t\t\t\tpath,\n\t\t\t\tkey,\n\t\t\t\tparams,\n\t\t\t);\n\t\t\tout.push(...items);\n\t\t\tcursor = next_cursor;\n\t\t} while (cursor);\n\t\treturn out;\n\t}\n\n\t/**\n\t * Fetch ONE page of contract-call transactions filtered to `contractId`.\n\t * Unlike walk(), this does NOT drain — the caller pages by feeding back\n\t * next_cursor — so a sparse high-volume filter (e.g. a single contract over\n\t * all history) costs one request per batch, not O(all-history) per tick.\n\t * `cursor` is exclusive (rows strictly after it); on the first call pass\n\t * `fromHeight` to anchor the backfill start instead.\n\t */\n\tasync fetchContractCalls(\n\t\tcontractId: string,\n\t\topts: {\n\t\t\ttoHeight: number;\n\t\t\tcursor?: string | null;\n\t\t\tfromHeight?: number;\n\t\t\tlimit?: number;\n\t\t},\n\t): Promise<{\n\t\ttransactions: IndexTransactionRow[];\n\t\tnext_cursor: string | null;\n\t}> {\n\t\tconst params = new URLSearchParams({\n\t\t\tto_height: String(opts.toHeight),\n\t\t\tlimit: String(opts.limit ?? PAGE_LIMIT),\n\t\t\tcontract_id: contractId,\n\t\t});\n\t\tif (opts.cursor) params.set(\"cursor\", opts.cursor);\n\t\telse params.set(\"from_height\", String(opts.fromHeight ?? 0));\n\t\tconst { items, next_cursor } = await this.getPage<\n\t\t\t\"transactions\",\n\t\t\tIndexTransactionRow\n\t\t>(\"/v1/index/transactions\", \"transactions\", params);\n\t\treturn { transactions: items, next_cursor };\n\t}\n\n\twalkBlocks(fromHeight: number, toHeight: number): Promise<IndexBlockRow[]> {\n\t\treturn this.walk<\"blocks\", IndexBlockRow>(\n\t\t\t\"/v1/index/blocks\",\n\t\t\t\"blocks\",\n\t\t\tfromHeight,\n\t\t\ttoHeight,\n\t\t);\n\t}\n\n\t/** Lowest block height in [fromHeight, toHeight] with a matching event, or\n\t * null. One page, limit 1 — built for sparse-scan probes. */\n\tasync firstEventHeight(\n\t\teventType: string,\n\t\tfromHeight: number,\n\t\ttoHeight: number,\n\t\tcontractId?: string,\n\t): Promise<number | null> {\n\t\tconst params = new URLSearchParams({\n\t\t\tevent_type: eventType,\n\t\t\tfrom_height: String(fromHeight),\n\t\t\tto_height: String(toHeight),\n\t\t\tlimit: \"1\",\n\t\t\t...(contractId ? { contract_id: contractId } : {}),\n\t\t});\n\t\tconst { items } = await this.getPage<\"events\", IndexEventRow>(\n\t\t\t\"/v1/index/events\",\n\t\t\t\"events\",\n\t\t\tparams,\n\t\t);\n\t\treturn items[0]?.block_height ?? null;\n\t}\n\n\twalkEvents(\n\t\teventType: string,\n\t\tfromHeight: number,\n\t\ttoHeight: number,\n\t\t/** Join the submitting tx so each row carries `tx_*` — lets an event-only\n\t\t * subgraph skip walkTransactions and build `ctx.tx` from the event. */\n\t\twithTx = false,\n\t): Promise<IndexEventRow[]> {\n\t\treturn this.walk<\"events\", IndexEventRow>(\n\t\t\t\"/v1/index/events\",\n\t\t\t\"events\",\n\t\t\tfromHeight,\n\t\t\ttoHeight,\n\t\t\t{\n\t\t\t\tevent_type: eventType,\n\t\t\t\t...(withTx ? { tx_context: \"true\" } : {}),\n\t\t\t},\n\t\t);\n\t}\n\n\twalkTransactions(\n\t\tfromHeight: number,\n\t\ttoHeight: number,\n\t): Promise<IndexTransactionRow[]> {\n\t\treturn this.walk<\"transactions\", IndexTransactionRow>(\n\t\t\t\"/v1/index/transactions\",\n\t\t\t\"transactions\",\n\t\t\tfromHeight,\n\t\t\ttoHeight,\n\t\t);\n\t}\n\n\t/** Canonical tip height from the Streams clock. */\n\tasync getStreamsTip(): Promise<number> {\n\t\tconst tip = await this.get<{ block_height: number }>(\n\t\t\t`${this.streamsBaseUrl}/v1/streams/tip`,\n\t\t\tthis.streamsApiKey,\n\t\t);\n\t\treturn Number(tip.block_height) || 0;\n\t}\n\n\t/**\n\t * Highest block height the Index data plane can serve (tip is inline in every\n\t * envelope). This is the data-availability bound — a consumer must not\n\t * process past it, even if the Streams clock is ahead.\n\t */\n\tasync getIndexTip(): Promise<number> {\n\t\tconst env = await this.get<{ tip: { block_height: number } }>(\n\t\t\t`${this.indexBaseUrl}/v1/index/blocks?limit=1`,\n\t\t\tthis.indexApiKey,\n\t\t);\n\t\treturn Number(env.tip?.block_height) || 0;\n\t}\n\n\t/** Reorgs since a resume token (wall-clock `detected_at`-keyed). */\n\tasync listReorgs(\n\t\tsince: string,\n\t): Promise<{ reorgs: StreamsReorgRow[]; next_since: string | null }> {\n\t\tconst params = new URLSearchParams({ since });\n\t\treturn this.get(\n\t\t\t`${this.streamsBaseUrl}/v1/streams/reorgs?${params}`,\n\t\t\tthis.streamsApiKey,\n\t\t);\n\t}\n}\n\n/** Decoder + subgraph processor client. One env contract; omits bearer when no key. */\nexport function createInternalIndexHttpClient(): IndexHttpClient {\n\tconst baseUrl = defaultInternalIndexBaseUrl();\n\treturn new IndexHttpClient({\n\t\tindexBaseUrl: baseUrl,\n\t\tstreamsBaseUrl: baseUrl,\n\t\tindexApiKey: defaultInternalIndexApiKey(),\n\t\tstreamsApiKey: defaultInternalStreamsApiKey(),\n\t});\n}\n"
|
|
7
7
|
],
|
|
8
|
-
"mappings": ";;;;;;;;;;;;;;;;;AAaO,IAAM,2BAA2B;AACjC,IAAM,6BAA6B;AAE1C,IAAM,kCAAkC;AAExC,IAAM,mCACL;AAEM,SAAS,0BAA0B,CACzC,MAAyB,QAAQ,KACZ;AAAA,EACrB,MAAM,WAAW,IAAI,wBAAwB,KAAK;AAAA,EAClD,IAAI;AAAA,IAAU,OAAO;AAAA,EACrB,MAAM,WAAW,IAAI,gBAAgB,KAAK;AAAA,EAC1C,IAAI;AAAA,IAAU,OAAO;AAAA,EACrB;AAAA;AAGM,SAAS,4BAA4B,CAC3C,MAAyB,QAAQ,KACZ;AAAA,EACrB,MAAM,WAAW,IAAI,0BAA0B,KAAK;AAAA,EACpD,IAAI;AAAA,IAAU,OAAO;AAAA,EACrB,MAAM,WAAW,IAAI,gBAAgB,KAAK;AAAA,EAC1C,IAAI;AAAA,IAAU,OAAO;AAAA,EACrB;AAAA;
|
|
9
|
-
"debugId": "
|
|
8
|
+
"mappings": "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",
|
|
9
|
+
"debugId": "1D8416E6B0F3E28D64756E2164756E21",
|
|
10
10
|
"names": []
|
|
11
11
|
}
|
|
@@ -2,8 +2,22 @@ declare const INDEX_INTERNAL_TENANT_ID = "tenant_index_internal";
|
|
|
2
2
|
declare const INTERNAL_STREAMS_TENANT_ID = "tenant_streams_decode_internal";
|
|
3
3
|
declare function defaultInternalIndexApiKey(env?: NodeJS.ProcessEnv): string | undefined;
|
|
4
4
|
declare function defaultInternalStreamsApiKey(env?: NodeJS.ProcessEnv): string | undefined;
|
|
5
|
+
/**
|
|
6
|
+
* Platform-mode Index consumer: refuse to run with neither credential.
|
|
7
|
+
*
|
|
8
|
+
* A keyless Index read is not an error — it is an anonymous read, and the API
|
|
9
|
+
* serves it for the last 24h (`api/src/index/free-window.ts`). That makes a
|
|
10
|
+
* missing key invisible right up until a consumer's cursor falls out of the
|
|
11
|
+
* window, at which point every read 402s and the container goes unhealthy,
|
|
12
|
+
* possibly weeks after the misconfiguration. Callers that seek older history
|
|
13
|
+
* on a platform-mode instance should fail here at startup instead.
|
|
14
|
+
*
|
|
15
|
+
* Self-hosted (oss/dedicated) instances are not gated, so they must NOT call
|
|
16
|
+
* this — `defaultInternalIndexApiKey()` returning undefined is correct there.
|
|
17
|
+
*/
|
|
18
|
+
declare function requireInternalIndexApiKey(env?: NodeJS.ProcessEnv): string;
|
|
5
19
|
/** Decoder startup: refuse to run with neither credential. */
|
|
6
20
|
declare function requireInternalStreamsApiKey(env?: NodeJS.ProcessEnv): string;
|
|
7
21
|
/** Shared env contract for decoder + subgraph HTTP dogfood. */
|
|
8
22
|
declare function defaultInternalIndexBaseUrl(): string;
|
|
9
|
-
export { requireInternalStreamsApiKey, defaultInternalStreamsApiKey, defaultInternalIndexBaseUrl, defaultInternalIndexApiKey, INTERNAL_STREAMS_TENANT_ID, INDEX_INTERNAL_TENANT_ID };
|
|
23
|
+
export { requireInternalStreamsApiKey, requireInternalIndexApiKey, defaultInternalStreamsApiKey, defaultInternalIndexBaseUrl, defaultInternalIndexApiKey, INTERNAL_STREAMS_TENANT_ID, INDEX_INTERNAL_TENANT_ID };
|
|
@@ -19,6 +19,7 @@ var INDEX_INTERNAL_TENANT_ID = "tenant_index_internal";
|
|
|
19
19
|
var INTERNAL_STREAMS_TENANT_ID = "tenant_streams_decode_internal";
|
|
20
20
|
var DEFAULT_INTERNAL_INDEX_BASE_URL = "http://api:3800";
|
|
21
21
|
var MISSING_INTERNAL_STREAMS_API_KEY = "Set STREAMS_INTERNAL_API_KEY (archive; prefix sl-int_) or INSTANCE_TOKEN (OSS). Empty env no longer falls back to a committed secret.";
|
|
22
|
+
var MISSING_INTERNAL_INDEX_API_KEY = "Set INDEX_INTERNAL_API_KEY (archive; prefix sl-int_) or INSTANCE_TOKEN (OSS). Without one, Index reads go out anonymously and are capped at the last 24h, so any seek into older history 402s.";
|
|
22
23
|
function defaultInternalIndexApiKey(env = process.env) {
|
|
23
24
|
const internal = env.INDEX_INTERNAL_API_KEY?.trim();
|
|
24
25
|
if (internal)
|
|
@@ -37,6 +38,12 @@ function defaultInternalStreamsApiKey(env = process.env) {
|
|
|
37
38
|
return instance;
|
|
38
39
|
return;
|
|
39
40
|
}
|
|
41
|
+
function requireInternalIndexApiKey(env = process.env) {
|
|
42
|
+
const key = defaultInternalIndexApiKey(env);
|
|
43
|
+
if (!key)
|
|
44
|
+
throw new Error(MISSING_INTERNAL_INDEX_API_KEY);
|
|
45
|
+
return key;
|
|
46
|
+
}
|
|
40
47
|
function requireInternalStreamsApiKey(env = process.env) {
|
|
41
48
|
const key = defaultInternalStreamsApiKey(env);
|
|
42
49
|
if (!key)
|
|
@@ -48,6 +55,7 @@ function defaultInternalIndexBaseUrl() {
|
|
|
48
55
|
}
|
|
49
56
|
export {
|
|
50
57
|
requireInternalStreamsApiKey,
|
|
58
|
+
requireInternalIndexApiKey,
|
|
51
59
|
defaultInternalStreamsApiKey,
|
|
52
60
|
defaultInternalIndexBaseUrl,
|
|
53
61
|
defaultInternalIndexApiKey,
|
|
@@ -55,5 +63,5 @@ export {
|
|
|
55
63
|
INDEX_INTERNAL_TENANT_ID
|
|
56
64
|
};
|
|
57
65
|
|
|
58
|
-
//# debugId=
|
|
66
|
+
//# debugId=D4294FDC89E3160364756E2164756E21
|
|
59
67
|
//# sourceMappingURL=index-internal-auth.js.map
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/index-internal-auth.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"// Internal service credentials for first-party consumers of /v1/index and\n// /v1/streams over HTTP (decoder, subgraph processor PublicApiBlockSource).\n// Seeded as internal tenants with NO account_id, so these reads are\n// unmetered (metering gates on account_id). Lives in shared so the API (seed)\n// and the consumers import it without a cycle.\n//\n// Resolution: STREAMS_INTERNAL_API_KEY / INDEX_INTERNAL_API_KEY, then\n// INSTANCE_TOKEN (OSS). No committed fallback. Empty-string env values fall\n// through (`trim()`, then empty): compose interpolates\n// `${STREAMS_INTERNAL_API_KEY:-}` which is \"\" when unset.\n//\n// Archive keys must not use the `sk-sl_` prefix; generate with `sl-int_`.\n// OSS compose should omit the internal env and send INSTANCE_TOKEN.\nexport const INDEX_INTERNAL_TENANT_ID = \"tenant_index_internal\";\nexport const INTERNAL_STREAMS_TENANT_ID = \"tenant_streams_decode_internal\";\n\nconst DEFAULT_INTERNAL_INDEX_BASE_URL = \"http://api:3800\";\n\nconst MISSING_INTERNAL_STREAMS_API_KEY =\n\t\"Set STREAMS_INTERNAL_API_KEY (archive; prefix sl-int_) or INSTANCE_TOKEN (OSS). Empty env no longer falls back to a committed secret.\";\n\nexport function defaultInternalIndexApiKey(\n\tenv: NodeJS.ProcessEnv = process.env,\n): string | undefined {\n\tconst internal = env.INDEX_INTERNAL_API_KEY?.trim();\n\tif (internal) return internal;\n\tconst instance = env.INSTANCE_TOKEN?.trim();\n\tif (instance) return instance;\n\treturn undefined;\n}\n\nexport function defaultInternalStreamsApiKey(\n\tenv: NodeJS.ProcessEnv = process.env,\n): string | undefined {\n\tconst internal = env.STREAMS_INTERNAL_API_KEY?.trim();\n\tif (internal) return internal;\n\tconst instance = env.INSTANCE_TOKEN?.trim();\n\tif (instance) return instance;\n\treturn undefined;\n}\n\n/** Decoder startup: refuse to run with neither credential. */\nexport function requireInternalStreamsApiKey(\n\tenv: NodeJS.ProcessEnv = process.env,\n): string {\n\tconst key = defaultInternalStreamsApiKey(env);\n\tif (!key) throw new Error(MISSING_INTERNAL_STREAMS_API_KEY);\n\treturn key;\n}\n\n/** Shared env contract for decoder + subgraph HTTP dogfood. */\nexport function defaultInternalIndexBaseUrl(): string {\n\treturn (\n\t\tprocess.env.SUBGRAPH_INDEX_API_URL ||\n\t\tprocess.env.STREAMS_API_URL ||\n\t\tDEFAULT_INTERNAL_INDEX_BASE_URL\n\t);\n}\n"
|
|
5
|
+
"// Internal service credentials for first-party consumers of /v1/index and\n// /v1/streams over HTTP (decoder, subgraph processor PublicApiBlockSource).\n// Seeded as internal tenants with NO account_id, so these reads are\n// unmetered (metering gates on account_id). Lives in shared so the API (seed)\n// and the consumers import it without a cycle.\n//\n// Resolution: STREAMS_INTERNAL_API_KEY / INDEX_INTERNAL_API_KEY, then\n// INSTANCE_TOKEN (OSS). No committed fallback. Empty-string env values fall\n// through (`trim()`, then empty): compose interpolates\n// `${STREAMS_INTERNAL_API_KEY:-}` which is \"\" when unset.\n//\n// Archive keys must not use the `sk-sl_` prefix; generate with `sl-int_`.\n// OSS compose should omit the internal env and send INSTANCE_TOKEN.\nexport const INDEX_INTERNAL_TENANT_ID = \"tenant_index_internal\";\nexport const INTERNAL_STREAMS_TENANT_ID = \"tenant_streams_decode_internal\";\n\nconst DEFAULT_INTERNAL_INDEX_BASE_URL = \"http://api:3800\";\n\nconst MISSING_INTERNAL_STREAMS_API_KEY =\n\t\"Set STREAMS_INTERNAL_API_KEY (archive; prefix sl-int_) or INSTANCE_TOKEN (OSS). Empty env no longer falls back to a committed secret.\";\n\nconst MISSING_INTERNAL_INDEX_API_KEY =\n\t\"Set INDEX_INTERNAL_API_KEY (archive; prefix sl-int_) or INSTANCE_TOKEN (OSS). Without one, Index reads go out anonymously and are capped at the last 24h, so any seek into older history 402s.\";\n\nexport function defaultInternalIndexApiKey(\n\tenv: NodeJS.ProcessEnv = process.env,\n): string | undefined {\n\tconst internal = env.INDEX_INTERNAL_API_KEY?.trim();\n\tif (internal) return internal;\n\tconst instance = env.INSTANCE_TOKEN?.trim();\n\tif (instance) return instance;\n\treturn undefined;\n}\n\nexport function defaultInternalStreamsApiKey(\n\tenv: NodeJS.ProcessEnv = process.env,\n): string | undefined {\n\tconst internal = env.STREAMS_INTERNAL_API_KEY?.trim();\n\tif (internal) return internal;\n\tconst instance = env.INSTANCE_TOKEN?.trim();\n\tif (instance) return instance;\n\treturn undefined;\n}\n\n/**\n * Platform-mode Index consumer: refuse to run with neither credential.\n *\n * A keyless Index read is not an error — it is an anonymous read, and the API\n * serves it for the last 24h (`api/src/index/free-window.ts`). That makes a\n * missing key invisible right up until a consumer's cursor falls out of the\n * window, at which point every read 402s and the container goes unhealthy,\n * possibly weeks after the misconfiguration. Callers that seek older history\n * on a platform-mode instance should fail here at startup instead.\n *\n * Self-hosted (oss/dedicated) instances are not gated, so they must NOT call\n * this — `defaultInternalIndexApiKey()` returning undefined is correct there.\n */\nexport function requireInternalIndexApiKey(\n\tenv: NodeJS.ProcessEnv = process.env,\n): string {\n\tconst key = defaultInternalIndexApiKey(env);\n\tif (!key) throw new Error(MISSING_INTERNAL_INDEX_API_KEY);\n\treturn key;\n}\n\n/** Decoder startup: refuse to run with neither credential. */\nexport function requireInternalStreamsApiKey(\n\tenv: NodeJS.ProcessEnv = process.env,\n): string {\n\tconst key = defaultInternalStreamsApiKey(env);\n\tif (!key) throw new Error(MISSING_INTERNAL_STREAMS_API_KEY);\n\treturn key;\n}\n\n/** Shared env contract for decoder + subgraph HTTP dogfood. */\nexport function defaultInternalIndexBaseUrl(): string {\n\treturn (\n\t\tprocess.env.SUBGRAPH_INDEX_API_URL ||\n\t\tprocess.env.STREAMS_API_URL ||\n\t\tDEFAULT_INTERNAL_INDEX_BASE_URL\n\t);\n}\n"
|
|
6
6
|
],
|
|
7
|
-
"mappings": ";;;;;;;;;;;;;;;;;AAaO,IAAM,2BAA2B;AACjC,IAAM,6BAA6B;AAE1C,IAAM,kCAAkC;AAExC,IAAM,mCACL;AAEM,SAAS,0BAA0B,CACzC,MAAyB,QAAQ,KACZ;AAAA,EACrB,MAAM,WAAW,IAAI,wBAAwB,KAAK;AAAA,EAClD,IAAI;AAAA,IAAU,OAAO;AAAA,EACrB,MAAM,WAAW,IAAI,gBAAgB,KAAK;AAAA,EAC1C,IAAI;AAAA,IAAU,OAAO;AAAA,EACrB;AAAA;AAGM,SAAS,4BAA4B,CAC3C,MAAyB,QAAQ,KACZ;AAAA,EACrB,MAAM,WAAW,IAAI,0BAA0B,KAAK;AAAA,EACpD,IAAI;AAAA,IAAU,OAAO;AAAA,EACrB,MAAM,WAAW,IAAI,gBAAgB,KAAK;AAAA,EAC1C,IAAI;AAAA,IAAU,OAAO;AAAA,EACrB;AAAA;
|
|
8
|
-
"debugId": "
|
|
7
|
+
"mappings": ";;;;;;;;;;;;;;;;;AAaO,IAAM,2BAA2B;AACjC,IAAM,6BAA6B;AAE1C,IAAM,kCAAkC;AAExC,IAAM,mCACL;AAED,IAAM,iCACL;AAEM,SAAS,0BAA0B,CACzC,MAAyB,QAAQ,KACZ;AAAA,EACrB,MAAM,WAAW,IAAI,wBAAwB,KAAK;AAAA,EAClD,IAAI;AAAA,IAAU,OAAO;AAAA,EACrB,MAAM,WAAW,IAAI,gBAAgB,KAAK;AAAA,EAC1C,IAAI;AAAA,IAAU,OAAO;AAAA,EACrB;AAAA;AAGM,SAAS,4BAA4B,CAC3C,MAAyB,QAAQ,KACZ;AAAA,EACrB,MAAM,WAAW,IAAI,0BAA0B,KAAK;AAAA,EACpD,IAAI;AAAA,IAAU,OAAO;AAAA,EACrB,MAAM,WAAW,IAAI,gBAAgB,KAAK;AAAA,EAC1C,IAAI;AAAA,IAAU,OAAO;AAAA,EACrB;AAAA;AAgBM,SAAS,0BAA0B,CACzC,MAAyB,QAAQ,KACxB;AAAA,EACT,MAAM,MAAM,2BAA2B,GAAG;AAAA,EAC1C,IAAI,CAAC;AAAA,IAAK,MAAM,IAAI,MAAM,8BAA8B;AAAA,EACxD,OAAO;AAAA;AAID,SAAS,4BAA4B,CAC3C,MAAyB,QAAQ,KACxB;AAAA,EACT,MAAM,MAAM,6BAA6B,GAAG;AAAA,EAC5C,IAAI,CAAC;AAAA,IAAK,MAAM,IAAI,MAAM,gCAAgC;AAAA,EAC1D,OAAO;AAAA;AAID,SAAS,2BAA2B,GAAW;AAAA,EACrD,OACC,QAAQ,IAAI,0BACZ,QAAQ,IAAI,mBACZ;AAAA;",
|
|
8
|
+
"debugId": "D4294FDC89E3160364756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@secondlayer/shared",
|
|
3
|
-
"version": "11.
|
|
3
|
+
"version": "11.3.0",
|
|
4
4
|
"description": "Internal building blocks shared across Secondlayer packages.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -141,6 +141,10 @@
|
|
|
141
141
|
"types": "./dist/src/archive/key-registry.d.ts",
|
|
142
142
|
"import": "./dist/src/archive/key-registry.js"
|
|
143
143
|
},
|
|
144
|
+
"./archive/reference": {
|
|
145
|
+
"types": "./dist/src/archive/reference.d.ts",
|
|
146
|
+
"import": "./dist/src/archive/reference.js"
|
|
147
|
+
},
|
|
144
148
|
"./archive/copy-loader": {
|
|
145
149
|
"types": "./dist/src/archive/copy-loader.d.ts",
|
|
146
150
|
"import": "./dist/src/archive/copy-loader.js"
|
|
@@ -165,6 +169,10 @@
|
|
|
165
169
|
"types": "./dist/src/archive/derived-stage-report.d.ts",
|
|
166
170
|
"import": "./dist/src/archive/derived-stage-report.js"
|
|
167
171
|
},
|
|
172
|
+
"./archive/instance-diagnosis": {
|
|
173
|
+
"types": "./dist/src/archive/instance-diagnosis.d.ts",
|
|
174
|
+
"import": "./dist/src/archive/instance-diagnosis.js"
|
|
175
|
+
},
|
|
168
176
|
"./coverage": {
|
|
169
177
|
"types": "./dist/src/coverage/index.d.ts",
|
|
170
178
|
"import": "./dist/src/coverage/index.js"
|