@secondlayer/shared 11.9.1 → 11.10.1
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 +1 -1
- package/dist/src/archive/instance-diagnosis.js +2 -2
- package/dist/src/archive/instance-diagnosis.js.map +2 -2
- package/dist/src/index-http.d.ts +30 -1
- package/dist/src/index-http.js +8 -2
- package/dist/src/index-http.js.map +3 -3
- package/package.json +1 -1
|
@@ -76,7 +76,7 @@ interface ContextProbe {
|
|
|
76
76
|
apiUrl: string;
|
|
77
77
|
/** `false` only for the metered archive deployment, which has accounts. */
|
|
78
78
|
selfHosted: boolean;
|
|
79
|
-
/** `true` when INSTANCE_TOKEN /
|
|
79
|
+
/** `true` when INSTANCE_TOKEN / SECONDLAYER_API_KEY resolved to a value. */
|
|
80
80
|
hasCredential: boolean;
|
|
81
81
|
/** `/public/status` when it answered, `null` when the probe failed. */
|
|
82
82
|
status: PublicStatus | null;
|
|
@@ -78,7 +78,7 @@ function explainContextNulls(snapshot, probe) {
|
|
|
78
78
|
if (!probe.status) {
|
|
79
79
|
const reason = `No instance answered at ${probe.apiUrl} (${probe.statusError ?? "unreachable"}), so this read never reached an API.`;
|
|
80
80
|
return {
|
|
81
|
-
summary: `${reason} Start it from docker/oss with: docker compose up -d — or point
|
|
81
|
+
summary: `${reason} Start it from docker/oss with: docker compose up -d — or point SECONDLAYER_API_URL at the instance you meant.`,
|
|
82
82
|
nulls: Object.fromEntries(nullFields.map((field) => [field, reason]))
|
|
83
83
|
};
|
|
84
84
|
}
|
|
@@ -114,5 +114,5 @@ export {
|
|
|
114
114
|
hasNoChainData
|
|
115
115
|
};
|
|
116
116
|
|
|
117
|
-
//# debugId=
|
|
117
|
+
//# debugId=774E7BBD29A35EA564756E2164756E21
|
|
118
118
|
//# sourceMappingURL=instance-diagnosis.js.map
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/archive/instance-diagnosis.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"/**\n * One reading of `/status` (or `/public/status`), shared by the CLI\n * (`status`, `doctor`, `context`) and the SDK (`sl.instance.diagnose`).\n *\n * These surfaces used to each interpret the same payload on their own and\n * disagreed in the most common first-run state: on a freshly self-hosted\n * instance with no chain data, `status` printed DEGRADED while `doctor` printed\n * \"All checks passed\", and neither named the cause or a next command. An\n * operator (or an agent) reading both had no way to tell whether it was safe to\n * proceed. The verdict now lives here so the surfaces cannot drift apart, and\n * every issue carries the exact command that resolves it.\n */\n\nexport interface PublicStatusDecoder {\n\tdecoder: string;\n\tstatus: string;\n}\n\nexport interface PublicStatusService {\n\tname: string;\n\tstatus: string;\n}\n\n/** The subset of `/status` a diagnosis reads. Everything is optional:\n * an older instance may not send a field, and a missing field must never be\n * read as a failure. */\nexport interface PublicStatus {\n\tstatus?: string;\n\tchainTip?: number | null;\n\tchainIntegrity?: {\n\t\tok?: boolean;\n\t\tmaxHeight?: number;\n\t\treason?: string | null;\n\t};\n\tstreams?: {\n\t\tstatus?: string;\n\t\ttip?: { lag_seconds?: number; block_height?: number } | null;\n\t};\n\tindex?: { status?: string; decoders?: PublicStatusDecoder[] };\n\tservices?: PublicStatusService[];\n\ttimestamp?: string;\n\t[key: string]: unknown;\n}\n\nexport interface InstanceIssue {\n\t/** What is wrong, in one line. */\n\ttitle: string;\n\t/** Why — assembled only from what the payload actually reports. */\n\tdetail?: string;\n\t/** What to run next, most useful first. */\n\tnextSteps: string[];\n}\n\nexport type InstanceState = \"healthy\" | \"empty-index\" | \"degraded\";\n\nexport interface InstanceDiagnosis {\n\tstate: InstanceState;\n\t/** The API's own top-level verdict, passed through unmodified. */\n\toverall: string;\n\tissues: InstanceIssue[];\n}\n\nexport const BOOTSTRAP_STEP =\n\t\"Restore verified history: secondlayer bootstrap --against <manifest>\";\nexport const OBSERVER_STEP =\n\t\"Confirm your node streams into this instance — secondlayer observer --mode indexer --endpoint secondlayer:3700 prints the stanza its config needs\";\nexport const SELF_HOST_DOCS_STEP =\n\t\"Docs: https://www.secondlayer.tools/docs/self-host\";\n\n/** `true` when the instance is reachable but has never indexed a block. */\nexport function hasNoChainData(status: PublicStatus): boolean {\n\tconst integrity = status.chainIntegrity;\n\t// `check_failed` is the API's placeholder when the integrity query itself\n\t// errored — its maxHeight of 0 means \"unknown\", not \"empty\".\n\tif (!integrity || integrity.reason === \"check_failed\") return false;\n\tif (integrity.maxHeight !== 0) return false;\n\treturn (status.chainTip ?? null) === null;\n}\n\nfunction emptyIndexIssue(status: PublicStatus): InstanceIssue {\n\tconst decoders = status.index?.decoders ?? [];\n\tconst stalled = decoders.filter((d) => d.status !== \"ok\").length;\n\tconst decoderClause =\n\t\tstalled > 0\n\t\t\t? ` and ${stalled} decoder${stalled === 1 ? \"\" : \"s\"} report unavailable because there is nothing to decode`\n\t\t\t: \"\";\n\treturn {\n\t\ttitle: \"No blocks indexed yet — this instance holds 0 blocks.\",\n\t\tdetail: `Streams has no tip${decoderClause}, which is what makes the overall status DEGRADED. This is the expected state for a fresh instance: nothing is broken, but no read returns data until history lands.`,\n\t\tnextSteps: [BOOTSTRAP_STEP, OBSERVER_STEP, SELF_HOST_DOCS_STEP],\n\t};\n}\n\nconst SERVICE_STEPS = [\n\t\"See the raw per-service report: secondlayer status --json\",\n\t\"Check the containers from docker/oss: docker compose ps, then docker compose logs --tail=50 secondlayer\",\n];\n\n/**\n * Turn a `/status` payload into a verdict plus zero or more issues.\n * Only facts present in the payload become issues — when the API says degraded\n * and names no failing surface, the issue says exactly that rather than\n * guessing a cause.\n */\nexport function diagnoseInstanceStatus(\n\tstatus: PublicStatus,\n): InstanceDiagnosis {\n\tconst overall = String(status.status ?? \"unknown\");\n\n\tif (hasNoChainData(status)) {\n\t\treturn { state: \"empty-index\", overall, issues: [emptyIndexIssue(status)] };\n\t}\n\n\tconst issues: InstanceIssue[] = [];\n\tfor (const service of status.services ?? []) {\n\t\tif (service.status === \"ok\") continue;\n\t\tissues.push({\n\t\t\ttitle: `Service ${service.name} reports ${service.status}.`,\n\t\t\tnextSteps: SERVICE_STEPS,\n\t\t});\n\t}\n\n\tconst integrity = status.chainIntegrity;\n\tif (integrity && integrity.ok === false) {\n\t\tissues.push({\n\t\t\ttitle: \"Chain-data integrity check failed.\",\n\t\t\tdetail:\n\t\t\t\tintegrity.reason ??\n\t\t\t\t\"The tip the instance serves is not backed by the history it implies.\",\n\t\t\tnextSteps: [\n\t\t\t\t\"Verify against the archive: secondlayer verify all --against <manifest>\",\n\t\t\t\t\"Repair what diverged: secondlayer repair --against <manifest> --apply\",\n\t\t\t],\n\t\t});\n\t}\n\n\tif (issues.length === 0 && overall !== \"healthy\") {\n\t\t// Do not invent a cause: report the contradiction and where to look.\n\t\tissues.push({\n\t\t\ttitle: `The API reports status=${overall} but names no failing service.`,\n\t\t\tdetail:\n\t\t\t\t\"Every service in the report is ok, so the degradation is in a surface this command does not summarize.\",\n\t\t\tnextSteps: SERVICE_STEPS,\n\t\t});\n\t}\n\n\treturn {\n\t\tstate: issues.length > 0 ? \"degraded\" : \"healthy\",\n\t\toverall,\n\t\tissues,\n\t};\n}\n\n// ── context ────────────────────────────────────────────────────────────\n\nexport interface ContextProbe {\n\t/** The endpoint the snapshot was read from. */\n\tapiUrl: string;\n\t/** `false` only for the metered archive deployment, which has accounts. */\n\tselfHosted: boolean;\n\t/** `true` when INSTANCE_TOKEN / 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\"webhooks\",\n\t\"activeOperations\",\n] as const;\n\n/**\n * Explain every null in a `context` snapshot.\n *\n * The SDK degrades each read to `null` on failure, so an all-null snapshot is\n * indistinguishable from \"not bootstrapped\", \"unauthorized\", and \"broken\" —\n * which is exactly what a caller needs to tell apart. Each reason here is\n * derived from a live probe of the same instance, never assumed.\n */\nexport function explainContextNulls(\n\tsnapshot: Record<string, unknown>,\n\tprobe: ContextProbe,\n): ContextExplanation {\n\tconst nullFields = CONTEXT_FIELDS.filter(\n\t\t(field) => (snapshot[field] ?? null) === null,\n\t);\n\n\tif (nullFields.length === 0) {\n\t\treturn { summary: \"Every field resolved.\", nulls: {} };\n\t}\n\n\tif (!probe.status) {\n\t\tconst reason = `No instance answered at ${probe.apiUrl} (${probe.statusError ?? \"unreachable\"}), so this read never reached an API.`;\n\t\treturn {\n\t\t\tsummary: `${reason} Start it from docker/oss with: docker compose up -d — or point 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// webhook lists are control-plane reads that answer on a bare instance.\n\tconst listReason = unresolvedReadReason(probe, blocks);\n\tconst chainReason = emptyIndex\n\t\t? `No blocks indexed yet — this instance holds 0 blocks, so there is no tip to report. ${BOOTSTRAP_STEP}`\n\t\t: listReason;\n\n\tconst nulls: Record<string, string> = {};\n\tfor (const field of nullFields) {\n\t\tif (field === \"account\") nulls[field] = accountReason;\n\t\telse if (field === \"streamsTip\" || field === \"indexTip\")\n\t\t\tnulls[field] = chainReason;\n\t\telse if (field === \"activeOperations\")\n\t\t\tnulls[field] =\n\t\t\t\t(snapshot.subgraphs ?? null) === null\n\t\t\t\t\t? \"Not probed: in-flight operations are read per subgraph, and the subgraph list did not resolve.\"\n\t\t\t\t\t: listReason;\n\t\telse nulls[field] = listReason;\n\t}\n\n\tconst summary = emptyIndex\n\t\t? `Instance at ${probe.apiUrl} is reachable but holds 0 blocks — the chain fields are null because nothing has been indexed yet. ${BOOTSTRAP_STEP}`\n\t\t: nullFields.length === 1 && nullFields[0] === \"account\"\n\t\t\t? `Instance at ${probe.apiUrl} is reachable and indexed to block ${blocks ?? \"unknown\"}; only \\`account\\` is null, which is normal when self-hosting.`\n\t\t\t: `Instance at ${probe.apiUrl} is reachable, but ${nullFields.length} field${nullFields.length === 1 ? \"\" : \"s\"} did not resolve — see \\`nulls\\` below for each one.`;\n\n\treturn { summary, nulls };\n}\n\nfunction unresolvedReadReason(\n\tprobe: ContextProbe,\n\tblocks: number | null,\n): string {\n\tconst where = `The instance at ${probe.apiUrl} is reachable${blocks !== null ? ` and indexed to block ${blocks}` : \"\"}, but this read returned nothing.`;\n\treturn probe.hasCredential\n\t\t? `${where} Check the surface directly: secondlayer status --json`\n\t\t: `${where} No credential resolved: export INSTANCE_TOKEN — an instance published past loopback requires it on every /v1 read.`;\n}\n"
|
|
5
|
+
"/**\n * One reading of `/status` (or `/public/status`), shared by the CLI\n * (`status`, `doctor`, `context`) and the SDK (`sl.instance.diagnose`).\n *\n * These surfaces used to each interpret the same payload on their own and\n * disagreed in the most common first-run state: on a freshly self-hosted\n * instance with no chain data, `status` printed DEGRADED while `doctor` printed\n * \"All checks passed\", and neither named the cause or a next command. An\n * operator (or an agent) reading both had no way to tell whether it was safe to\n * proceed. The verdict now lives here so the surfaces cannot drift apart, and\n * every issue carries the exact command that resolves it.\n */\n\nexport interface PublicStatusDecoder {\n\tdecoder: string;\n\tstatus: string;\n}\n\nexport interface PublicStatusService {\n\tname: string;\n\tstatus: string;\n}\n\n/** The subset of `/status` a diagnosis reads. Everything is optional:\n * an older instance may not send a field, and a missing field must never be\n * read as a failure. */\nexport interface PublicStatus {\n\tstatus?: string;\n\tchainTip?: number | null;\n\tchainIntegrity?: {\n\t\tok?: boolean;\n\t\tmaxHeight?: number;\n\t\treason?: string | null;\n\t};\n\tstreams?: {\n\t\tstatus?: string;\n\t\ttip?: { lag_seconds?: number; block_height?: number } | null;\n\t};\n\tindex?: { status?: string; decoders?: PublicStatusDecoder[] };\n\tservices?: PublicStatusService[];\n\ttimestamp?: string;\n\t[key: string]: unknown;\n}\n\nexport interface InstanceIssue {\n\t/** What is wrong, in one line. */\n\ttitle: string;\n\t/** Why — assembled only from what the payload actually reports. */\n\tdetail?: string;\n\t/** What to run next, most useful first. */\n\tnextSteps: string[];\n}\n\nexport type InstanceState = \"healthy\" | \"empty-index\" | \"degraded\";\n\nexport interface InstanceDiagnosis {\n\tstate: InstanceState;\n\t/** The API's own top-level verdict, passed through unmodified. */\n\toverall: string;\n\tissues: InstanceIssue[];\n}\n\nexport const BOOTSTRAP_STEP =\n\t\"Restore verified history: secondlayer bootstrap --against <manifest>\";\nexport const OBSERVER_STEP =\n\t\"Confirm your node streams into this instance — secondlayer observer --mode indexer --endpoint secondlayer:3700 prints the stanza its config needs\";\nexport const SELF_HOST_DOCS_STEP =\n\t\"Docs: https://www.secondlayer.tools/docs/self-host\";\n\n/** `true` when the instance is reachable but has never indexed a block. */\nexport function hasNoChainData(status: PublicStatus): boolean {\n\tconst integrity = status.chainIntegrity;\n\t// `check_failed` is the API's placeholder when the integrity query itself\n\t// errored — its maxHeight of 0 means \"unknown\", not \"empty\".\n\tif (!integrity || integrity.reason === \"check_failed\") return false;\n\tif (integrity.maxHeight !== 0) return false;\n\treturn (status.chainTip ?? null) === null;\n}\n\nfunction emptyIndexIssue(status: PublicStatus): InstanceIssue {\n\tconst decoders = status.index?.decoders ?? [];\n\tconst stalled = decoders.filter((d) => d.status !== \"ok\").length;\n\tconst decoderClause =\n\t\tstalled > 0\n\t\t\t? ` and ${stalled} decoder${stalled === 1 ? \"\" : \"s\"} report unavailable because there is nothing to decode`\n\t\t\t: \"\";\n\treturn {\n\t\ttitle: \"No blocks indexed yet — this instance holds 0 blocks.\",\n\t\tdetail: `Streams has no tip${decoderClause}, which is what makes the overall status DEGRADED. This is the expected state for a fresh instance: nothing is broken, but no read returns data until history lands.`,\n\t\tnextSteps: [BOOTSTRAP_STEP, OBSERVER_STEP, SELF_HOST_DOCS_STEP],\n\t};\n}\n\nconst SERVICE_STEPS = [\n\t\"See the raw per-service report: secondlayer status --json\",\n\t\"Check the containers from docker/oss: docker compose ps, then docker compose logs --tail=50 secondlayer\",\n];\n\n/**\n * Turn a `/status` payload into a verdict plus zero or more issues.\n * Only facts present in the payload become issues — when the API says degraded\n * and names no failing surface, the issue says exactly that rather than\n * guessing a cause.\n */\nexport function diagnoseInstanceStatus(\n\tstatus: PublicStatus,\n): InstanceDiagnosis {\n\tconst overall = String(status.status ?? \"unknown\");\n\n\tif (hasNoChainData(status)) {\n\t\treturn { state: \"empty-index\", overall, issues: [emptyIndexIssue(status)] };\n\t}\n\n\tconst issues: InstanceIssue[] = [];\n\tfor (const service of status.services ?? []) {\n\t\tif (service.status === \"ok\") continue;\n\t\tissues.push({\n\t\t\ttitle: `Service ${service.name} reports ${service.status}.`,\n\t\t\tnextSteps: SERVICE_STEPS,\n\t\t});\n\t}\n\n\tconst integrity = status.chainIntegrity;\n\tif (integrity && integrity.ok === false) {\n\t\tissues.push({\n\t\t\ttitle: \"Chain-data integrity check failed.\",\n\t\t\tdetail:\n\t\t\t\tintegrity.reason ??\n\t\t\t\t\"The tip the instance serves is not backed by the history it implies.\",\n\t\t\tnextSteps: [\n\t\t\t\t\"Verify against the archive: secondlayer verify all --against <manifest>\",\n\t\t\t\t\"Repair what diverged: secondlayer repair --against <manifest> --apply\",\n\t\t\t],\n\t\t});\n\t}\n\n\tif (issues.length === 0 && overall !== \"healthy\") {\n\t\t// Do not invent a cause: report the contradiction and where to look.\n\t\tissues.push({\n\t\t\ttitle: `The API reports status=${overall} but names no failing service.`,\n\t\t\tdetail:\n\t\t\t\t\"Every service in the report is ok, so the degradation is in a surface this command does not summarize.\",\n\t\t\tnextSteps: SERVICE_STEPS,\n\t\t});\n\t}\n\n\treturn {\n\t\tstate: issues.length > 0 ? \"degraded\" : \"healthy\",\n\t\toverall,\n\t\tissues,\n\t};\n}\n\n// ── context ────────────────────────────────────────────────────────────\n\nexport interface ContextProbe {\n\t/** The endpoint the snapshot was read from. */\n\tapiUrl: string;\n\t/** `false` only for the metered archive deployment, which has accounts. */\n\tselfHosted: boolean;\n\t/** `true` when INSTANCE_TOKEN / SECONDLAYER_API_KEY resolved to a value. */\n\thasCredential: boolean;\n\t/** `/public/status` when it answered, `null` when the probe failed. */\n\tstatus: PublicStatus | null;\n\t/** Why the probe failed, when it did. */\n\tstatusError?: string;\n}\n\nexport interface ContextExplanation {\n\t/** One line naming the likeliest cause of the nulls below. */\n\tsummary: string;\n\t/** Per-null-field reason. Fields that carry a value are absent. */\n\tnulls: Record<string, string>;\n}\n\nconst CONTEXT_FIELDS = [\n\t\"account\",\n\t\"streamsTip\",\n\t\"indexTip\",\n\t\"subgraphs\",\n\t\"webhooks\",\n\t\"activeOperations\",\n] as const;\n\n/**\n * Explain every null in a `context` snapshot.\n *\n * The SDK degrades each read to `null` on failure, so an all-null snapshot is\n * indistinguishable from \"not bootstrapped\", \"unauthorized\", and \"broken\" —\n * which is exactly what a caller needs to tell apart. Each reason here is\n * derived from a live probe of the same instance, never assumed.\n */\nexport function explainContextNulls(\n\tsnapshot: Record<string, unknown>,\n\tprobe: ContextProbe,\n): ContextExplanation {\n\tconst nullFields = CONTEXT_FIELDS.filter(\n\t\t(field) => (snapshot[field] ?? null) === null,\n\t);\n\n\tif (nullFields.length === 0) {\n\t\treturn { summary: \"Every field resolved.\", nulls: {} };\n\t}\n\n\tif (!probe.status) {\n\t\tconst reason = `No instance answered at ${probe.apiUrl} (${probe.statusError ?? \"unreachable\"}), so this read never reached an API.`;\n\t\treturn {\n\t\t\tsummary: `${reason} Start it from docker/oss with: docker compose up -d — or point SECONDLAYER_API_URL at the instance you meant.`,\n\t\t\tnulls: Object.fromEntries(nullFields.map((field) => [field, reason])),\n\t\t};\n\t}\n\n\tconst emptyIndex = hasNoChainData(probe.status);\n\tconst blocks = probe.status.chainIntegrity?.maxHeight ?? null;\n\tconst accountReason = probe.selfHosted\n\t\t? \"Expected: a self-hosted instance has no account system. Accounts exist only on the metered archive deployment at api.secondlayer.tools, so null here is normal and not a failure.\"\n\t\t: \"Not authenticated — run `secondlayer login`, or export INSTANCE_TOKEN.\";\n\n\t// An empty index explains the chain tips and nothing else: the subgraph and\n\t// webhook lists are control-plane reads that answer on a bare instance.\n\tconst listReason = unresolvedReadReason(probe, blocks);\n\tconst chainReason = emptyIndex\n\t\t? `No blocks indexed yet — this instance holds 0 blocks, so there is no tip to report. ${BOOTSTRAP_STEP}`\n\t\t: listReason;\n\n\tconst nulls: Record<string, string> = {};\n\tfor (const field of nullFields) {\n\t\tif (field === \"account\") nulls[field] = accountReason;\n\t\telse if (field === \"streamsTip\" || field === \"indexTip\")\n\t\t\tnulls[field] = chainReason;\n\t\telse if (field === \"activeOperations\")\n\t\t\tnulls[field] =\n\t\t\t\t(snapshot.subgraphs ?? null) === null\n\t\t\t\t\t? \"Not probed: in-flight operations are read per subgraph, and the subgraph list did not resolve.\"\n\t\t\t\t\t: listReason;\n\t\telse nulls[field] = listReason;\n\t}\n\n\tconst summary = emptyIndex\n\t\t? `Instance at ${probe.apiUrl} is reachable but holds 0 blocks — the chain fields are null because nothing has been indexed yet. ${BOOTSTRAP_STEP}`\n\t\t: nullFields.length === 1 && nullFields[0] === \"account\"\n\t\t\t? `Instance at ${probe.apiUrl} is reachable and indexed to block ${blocks ?? \"unknown\"}; only \\`account\\` is null, which is normal when self-hosting.`\n\t\t\t: `Instance at ${probe.apiUrl} is reachable, but ${nullFields.length} field${nullFields.length === 1 ? \"\" : \"s\"} did not resolve — see \\`nulls\\` below for each one.`;\n\n\treturn { summary, nulls };\n}\n\nfunction unresolvedReadReason(\n\tprobe: ContextProbe,\n\tblocks: number | null,\n): string {\n\tconst where = `The instance at ${probe.apiUrl} is reachable${blocks !== null ? ` and indexed to block ${blocks}` : \"\"}, but this read returned nothing.`;\n\treturn probe.hasCredential\n\t\t? `${where} Check the surface directly: secondlayer status --json`\n\t\t: `${where} No credential resolved: export INSTANCE_TOKEN — an instance published past loopback requires it on every /v1 read.`;\n}\n"
|
|
6
6
|
],
|
|
7
7
|
"mappings": "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",
|
|
8
|
-
"debugId": "
|
|
8
|
+
"debugId": "774E7BBD29A35EA564756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
package/dist/src/index-http.d.ts
CHANGED
|
@@ -115,11 +115,31 @@ type IndexHttpOptions = {
|
|
|
115
115
|
/** Bearer for /v1/streams. Empty/unset omits the header. */
|
|
116
116
|
streamsApiKey?: string
|
|
117
117
|
};
|
|
118
|
+
/** `/v1/index/blocks?limit=1`'s envelope — read for its `tip`, not its rows. */
|
|
119
|
+
type IndexTipEnvelope = {
|
|
120
|
+
tip: {
|
|
121
|
+
block_height: number
|
|
122
|
+
source_block_height?: number
|
|
123
|
+
/**
|
|
124
|
+
* Committed height per classic decoded event_type (plan-063), e.g.
|
|
125
|
+
* `{ ft_transfer: 950, stx_transfer: 940 }`. `null` means that decoder
|
|
126
|
+
* has no checkpoint yet (fail-closed for a caller that references it,
|
|
127
|
+
* not "committed to height 0"); a missing key means an older server
|
|
128
|
+
* that doesn't send this field at all. Absent on an instance built
|
|
129
|
+
* before plan-063 shipped it.
|
|
130
|
+
*/
|
|
131
|
+
decoded_heights?: Record<string, number | null>
|
|
132
|
+
}
|
|
133
|
+
};
|
|
118
134
|
declare class IndexHttpClient {
|
|
119
135
|
private readonly indexBaseUrl;
|
|
120
136
|
private readonly indexApiKey;
|
|
121
137
|
private readonly streamsBaseUrl;
|
|
122
138
|
private readonly streamsApiKey;
|
|
139
|
+
/** Populated by the last `getIndexTipEnvelope()` call (via `getIndexTip`/
|
|
140
|
+
* `getIndexSourceTip`) so `getDecodedHeights()` can read the SAME response
|
|
141
|
+
* synchronously instead of making a second request. */
|
|
142
|
+
private lastTipEnvelope;
|
|
123
143
|
constructor(opts: IndexHttpOptions);
|
|
124
144
|
private get;
|
|
125
145
|
/** Fetch a single cursor page of an Index collection. */
|
|
@@ -159,6 +179,15 @@ declare class IndexHttpClient {
|
|
|
159
179
|
getIndexTip(): Promise<number>;
|
|
160
180
|
/** Ingest tip. VM rows land with the block; decoded `block_height` can lag. */
|
|
161
181
|
getIndexSourceTip(): Promise<number>;
|
|
182
|
+
/**
|
|
183
|
+
* Per-decoder committed heights from the last tip envelope fetched by
|
|
184
|
+
* `getIndexTip()`/`getIndexSourceTip()` — reads the cached response, no
|
|
185
|
+
* extra request. `undefined` before either has been called, or against an
|
|
186
|
+
* older server that doesn't send the field. A caller (the chain evaluator)
|
|
187
|
+
* uses this to bound itself by the decoders it actually reads instead of
|
|
188
|
+
* the conservative cross-decoder floor `block_height` itself carries.
|
|
189
|
+
*/
|
|
190
|
+
getDecodedHeights(): Record<string, number | null> | undefined;
|
|
162
191
|
private getIndexTipEnvelope;
|
|
163
192
|
/** Reorgs since a resume token (wall-clock `detected_at`-keyed). */
|
|
164
193
|
listReorgs(since: string): Promise<{
|
|
@@ -168,4 +197,4 @@ declare class IndexHttpClient {
|
|
|
168
197
|
}
|
|
169
198
|
/** Decoder + subgraph processor client. One env contract; omits bearer when no key. */
|
|
170
199
|
declare function createInternalIndexHttpClient(): IndexHttpClient;
|
|
171
|
-
export { IndexBlockRow, IndexEventRow, IndexHttpClient, IndexHttpOptions, IndexTransactionRow, StreamsReorgRow, createInternalIndexHttpClient };
|
|
200
|
+
export { IndexBlockRow, IndexEventRow, IndexHttpClient, IndexHttpOptions, IndexTipEnvelope, IndexTransactionRow, StreamsReorgRow, createInternalIndexHttpClient };
|
package/dist/src/index-http.js
CHANGED
|
@@ -50,6 +50,7 @@ class IndexHttpClient {
|
|
|
50
50
|
indexApiKey;
|
|
51
51
|
streamsBaseUrl;
|
|
52
52
|
streamsApiKey;
|
|
53
|
+
lastTipEnvelope;
|
|
53
54
|
constructor(opts) {
|
|
54
55
|
this.indexBaseUrl = opts.indexBaseUrl.replace(/\/+$/, "");
|
|
55
56
|
this.indexApiKey = opts.indexApiKey ?? defaultInternalIndexApiKey();
|
|
@@ -155,8 +156,13 @@ class IndexHttpClient {
|
|
|
155
156
|
const env = await this.getIndexTipEnvelope();
|
|
156
157
|
return Number(env.tip?.source_block_height ?? env.tip?.block_height) || 0;
|
|
157
158
|
}
|
|
159
|
+
getDecodedHeights() {
|
|
160
|
+
return this.lastTipEnvelope?.tip.decoded_heights;
|
|
161
|
+
}
|
|
158
162
|
async getIndexTipEnvelope() {
|
|
159
|
-
|
|
163
|
+
const env = await this.get(`${this.indexBaseUrl}/v1/index/blocks?limit=1`, this.indexApiKey);
|
|
164
|
+
this.lastTipEnvelope = env;
|
|
165
|
+
return env;
|
|
160
166
|
}
|
|
161
167
|
async listReorgs(since) {
|
|
162
168
|
const params = new URLSearchParams({ since });
|
|
@@ -177,5 +183,5 @@ export {
|
|
|
177
183
|
createInternalIndexHttpClient
|
|
178
184
|
};
|
|
179
185
|
|
|
180
|
-
//# debugId=
|
|
186
|
+
//# debugId=A70158FAAEE2A6F664756E2164756E21
|
|
181
187
|
//# sourceMappingURL=index-http.js.map
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
"sources": ["../src/index-internal-auth.ts", "../src/index-http.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
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 carry no credential and platform mode 401s every one of them.\";\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 * Platform mode requires a credential on every Index read (a keyless read\n * 401s, `api/src/index/auth.ts`) — a first-party consumer with no internal\n * key would fail every request at call time. 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
|
-
"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\t| {\n\t\t\t\tevent_type: \"nested_contract_call\";\n\t\t\t\tsender?: string | null;\n\t\t\t\tcaller: string;\n\t\t\t\tfunction_name: string;\n\t\t\t\tfunction_args?: unknown;\n\t\t\t\traw_result?: string | null;\n\t\t }\n\t\t| {\n\t\t\t\tevent_type: \"var_set\";\n\t\t\t\tvar_name: string;\n\t\t\t\traw_value?: string | null;\n\t\t }\n\t\t| {\n\t\t\t\tevent_type: \"map_set\" | \"map_insert\";\n\t\t\t\tmap: string;\n\t\t\t\traw_key?: string | null;\n\t\t\t\traw_value?: string | null;\n\t\t }\n\t\t| {\n\t\t\t\tevent_type: \"map_delete\";\n\t\t\t\tmap: string;\n\t\t\t\traw_key?: string | null;\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.getIndexTipEnvelope();\n\t\treturn Number(env.tip?.block_height) || 0;\n\t}\n\n\t/** Ingest tip. VM rows land with the block; decoded `block_height` can lag. */\n\tasync getIndexSourceTip(): Promise<number> {\n\t\tconst env = await this.getIndexTipEnvelope();\n\t\treturn Number(env.tip?.source_block_height ?? env.tip?.block_height) || 0;\n\t}\n\n\tprivate async getIndexTipEnvelope(): Promise<{\n\t\ttip: { block_height: number; source_block_height?: number };\n\t}> {\n\t\treturn this.get<{\n\t\t\ttip: { block_height: number; source_block_height?: number };\n\t\t}>(`${this.indexBaseUrl}/v1/index/blocks?limit=1`, this.indexApiKey);\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"
|
|
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\t| {\n\t\t\t\tevent_type: \"nested_contract_call\";\n\t\t\t\tsender?: string | null;\n\t\t\t\tcaller: string;\n\t\t\t\tfunction_name: string;\n\t\t\t\tfunction_args?: unknown;\n\t\t\t\traw_result?: string | null;\n\t\t }\n\t\t| {\n\t\t\t\tevent_type: \"var_set\";\n\t\t\t\tvar_name: string;\n\t\t\t\traw_value?: string | null;\n\t\t }\n\t\t| {\n\t\t\t\tevent_type: \"map_set\" | \"map_insert\";\n\t\t\t\tmap: string;\n\t\t\t\traw_key?: string | null;\n\t\t\t\traw_value?: string | null;\n\t\t }\n\t\t| {\n\t\t\t\tevent_type: \"map_delete\";\n\t\t\t\tmap: string;\n\t\t\t\traw_key?: string | null;\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\n/** `/v1/index/blocks?limit=1`'s envelope — read for its `tip`, not its rows. */\nexport type IndexTipEnvelope = {\n\ttip: {\n\t\tblock_height: number;\n\t\tsource_block_height?: number;\n\t\t/**\n\t\t * Committed height per classic decoded event_type (plan-063), e.g.\n\t\t * `{ ft_transfer: 950, stx_transfer: 940 }`. `null` means that decoder\n\t\t * has no checkpoint yet (fail-closed for a caller that references it,\n\t\t * not \"committed to height 0\"); a missing key means an older server\n\t\t * that doesn't send this field at all. Absent on an instance built\n\t\t * before plan-063 shipped it.\n\t\t */\n\t\tdecoded_heights?: Record<string, number | null>;\n\t};\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\t/** Populated by the last `getIndexTipEnvelope()` call (via `getIndexTip`/\n\t * `getIndexSourceTip`) so `getDecodedHeights()` can read the SAME response\n\t * synchronously instead of making a second request. */\n\tprivate lastTipEnvelope: IndexTipEnvelope | 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.getIndexTipEnvelope();\n\t\treturn Number(env.tip?.block_height) || 0;\n\t}\n\n\t/** Ingest tip. VM rows land with the block; decoded `block_height` can lag. */\n\tasync getIndexSourceTip(): Promise<number> {\n\t\tconst env = await this.getIndexTipEnvelope();\n\t\treturn Number(env.tip?.source_block_height ?? env.tip?.block_height) || 0;\n\t}\n\n\t/**\n\t * Per-decoder committed heights from the last tip envelope fetched by\n\t * `getIndexTip()`/`getIndexSourceTip()` — reads the cached response, no\n\t * extra request. `undefined` before either has been called, or against an\n\t * older server that doesn't send the field. A caller (the chain evaluator)\n\t * uses this to bound itself by the decoders it actually reads instead of\n\t * the conservative cross-decoder floor `block_height` itself carries.\n\t */\n\tgetDecodedHeights(): Record<string, number | null> | undefined {\n\t\treturn this.lastTipEnvelope?.tip.decoded_heights;\n\t}\n\n\tprivate async getIndexTipEnvelope(): Promise<IndexTipEnvelope> {\n\t\tconst env = await this.get<IndexTipEnvelope>(\n\t\t\t`${this.indexBaseUrl}/v1/index/blocks?limit=1`,\n\t\t\tthis.indexApiKey,\n\t\t);\n\t\tthis.lastTipEnvelope = env;\n\t\treturn env;\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;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;AAaM,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;;;AC1DF,IAAM,aAAa;AAQnB,IAAM,eAAe;AACrB,IAAM,gBAAgB;AACtB,IAAM,mBAAmB,IAAI,IAAI,CAAC,KAAK,KAAK,GAAG,CAAC;AAEhD,IAAM,QAAQ,CAAC,OACd,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,EAAE,CAAC;AAAA;
|
|
9
|
-
"debugId": "
|
|
8
|
+
"mappings": "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",
|
|
9
|
+
"debugId": "A70158FAAEE2A6F664756E2164756E21",
|
|
10
10
|
"names": []
|
|
11
11
|
}
|