@substrat-run/control-plane-api 0.147.0 → 0.148.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/api.d.ts +56 -1
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +801 -379
- package/dist/api.js.map +1 -1
- package/dist/deploy.d.ts +4 -3
- package/dist/deploy.d.ts.map +1 -1
- package/dist/deploy.js +3 -3
- package/dist/deploy.js.map +1 -1
- package/dist/errors.d.ts +19 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +18 -4
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +10 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/lake-gateway.d.ts +54 -0
- package/dist/lake-gateway.d.ts.map +1 -0
- package/dist/lake-gateway.js +158 -0
- package/dist/lake-gateway.js.map +1 -0
- package/dist/platform-entry.generated.d.ts +1 -1
- package/dist/platform-entry.generated.d.ts.map +1 -1
- package/dist/platform-entry.generated.js +2 -2
- package/dist/platform-entry.generated.js.map +1 -1
- package/dist/platform-scope-stores.d.ts +59 -6
- package/dist/platform-scope-stores.d.ts.map +1 -1
- package/dist/platform-scope-stores.js +117 -16
- package/dist/platform-scope-stores.js.map +1 -1
- package/dist/r2-sql-engine.d.ts +50 -0
- package/dist/r2-sql-engine.d.ts.map +1 -0
- package/dist/r2-sql-engine.js +126 -0
- package/dist/r2-sql-engine.js.map +1 -0
- package/dist/scope-copy-cleanup.d.ts +12 -0
- package/dist/scope-copy-cleanup.d.ts.map +1 -1
- package/dist/scope-copy-cleanup.js +14 -3
- package/dist/scope-copy-cleanup.js.map +1 -1
- package/dist/store-entry.d.ts +90 -0
- package/dist/store-entry.d.ts.map +1 -0
- package/dist/store-entry.generated.d.ts +60 -0
- package/dist/store-entry.generated.d.ts.map +1 -0
- package/dist/store-entry.generated.js +62 -0
- package/dist/store-entry.generated.js.map +1 -0
- package/dist/store-entry.js +178 -0
- package/dist/store-entry.js.map +1 -0
- package/dist/store-releases.d.ts +147 -0
- package/dist/store-releases.d.ts.map +1 -0
- package/dist/store-releases.js +298 -0
- package/dist/store-releases.js.map +1 -0
- package/dist/store-script.d.ts +80 -0
- package/dist/store-script.d.ts.map +1 -0
- package/dist/store-script.js +173 -0
- package/dist/store-script.js.map +1 -0
- package/dist/vertical-client.d.ts +4 -0
- package/dist/vertical-client.d.ts.map +1 -1
- package/dist/vertical-client.js +10 -1
- package/dist/vertical-client.js.map +1 -1
- package/package.json +7 -7
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
export const PLATFORM_ENTRY_MODULE = 'substrat-platform-entry.js';
|
|
10
10
|
/** The import specifier the uploader replaces with `./<the bundle's entry>`. */
|
|
11
11
|
export const PLATFORM_ENTRY_PLACEHOLDER = '__SUBSTRAT_VERTICAL_ENTRY__';
|
|
12
|
-
/** The entry itself —
|
|
13
|
-
export const PLATFORM_ENTRY_SOURCE = "// substrat-platform-entry.ts\nimport { AsyncLocalStorage } from \"node:async_hooks\";\n\n// packages/contracts/dist/invocation-record.js\nvar INVOCATION_RECORD_HEADER = \"x-substrat-invocation-record\";\nvar REQUEST_ID_HEADER = \"x-substrat-request-id\";\nvar INVOCATION_RECORD_FIELD_MAX = 128;\nvar FIELDS = [\"operation\", \"problemCode\", \"principalKind\"];\nfunction encodeInvocationRecord(record) {\n const params = new URLSearchParams();\n for (const key of FIELDS) {\n const v = record[key];\n if (typeof v === \"string\" && v !== \"\")\n params.set(key, v.slice(0, INVOCATION_RECORD_FIELD_MAX));\n }\n const value = params.toString();\n return value === \"\" ? null : value;\n}\nvar FIELD_COVERAGE_HEADER = \"x-substrat-field-coverage\";\nfunction invocationLevelOf(status, threw, problemCode) {\n if (threw || status === null || status >= 500)\n return \"error\";\n if (status >= 400)\n return \"warn\";\n if (problemCode)\n return \"warn\";\n return \"info\";\n}\n\n// packages/kernel/dist/invocation-line.js\nfunction invocationLine(f) {\n const emitted = f.emitted;\n return {\n substrat: \"invocation\",\n ...f.kind ? { kind: f.kind } : {},\n tenantId: f.tenantId,\n scopeId: f.scopeId,\n vertical: f.vertical ?? null,\n surface: f.surface ?? null,\n method: f.method ?? null,\n path: f.path ?? null,\n status: f.status ?? null,\n threw: f.threw,\n durationMs: f.durationMs,\n invocationId: f.invocationId,\n level: f.level,\n operation: f.operation ?? null,\n problemCode: f.problemCode ?? null,\n principalKind: f.principalKind ?? null,\n eventCount: emitted ? emitted.total : null,\n eventTypes: emitted ? distinct(emitted.events.map((e) => e.type)) : [],\n entities: emitted && f.withEntities !== false ? distinct(emitted.events.map((e) => e.entity)) : [],\n versionId: f.versionId ?? null,\n ...f.outputFields ? { outputFields: f.outputFields, ...f.fieldCoverageId ? { fieldCoverageId: f.fieldCoverageId } : {} } : {},\n ...f.storeCalls && f.storeCalls.length > 0 ? { storeCalls: f.storeCalls } : {},\n ...f.storeCallsDropped ? { storeCallsDropped: f.storeCallsDropped } : {},\n ...f.async ?? {}\n };\n}\nvar STORE_CALL_LOG_CAP = 16;\nfunction tallyStoreCall(tally, call) {\n const calls = tally.storeCalls ?? (tally.storeCalls = []);\n if (calls.length < STORE_CALL_LOG_CAP) {\n calls.push({ binding: call.binding, operation: call.operation, outcome: call.outcome, durationMs: call.durationMs });\n } else {\n tally.storeCallsDropped = (tally.storeCallsDropped ?? 0) + 1;\n }\n}\nvar STORE_CALL_SINK_KEY = \"substrat.store-call-sink\";\nfunction distinct(values) {\n return [...new Set(values)];\n}\n\n// packages/kernel/dist/ulid.js\nvar B32 = \"0123456789ABCDEFGHJKMNPQRSTVWXYZ\";\nvar MAX_ULID_TIME = 2 ** 48 - 1;\nvar ULID_SHAPE = /^[0-9A-HJKMNP-TV-Z]{26}$/;\nvar unencodable = (t) => `not an encodable ULID instant: ${t} (want 0..${MAX_ULID_TIME})`;\nfunction encodeTime(time) {\n let ts = \"\";\n let t = time;\n for (let i = 0; i < 10; i++) {\n ts = B32[t % 32] + ts;\n t = Math.floor(t / 32);\n }\n return ts;\n}\nfunction createUlid() {\n let lastTime = -1;\n const lastRand = new Array(16).fill(0);\n function freshRandom() {\n const bytes = crypto.getRandomValues(new Uint8Array(16));\n for (let i = 0; i < 16; i++)\n lastRand[i] = bytes[i] % 32;\n }\n function incrementRandom() {\n for (let i = 15; i >= 0; i--) {\n if (lastRand[i] < 31) {\n lastRand[i]++;\n return true;\n }\n lastRand[i] = 0;\n }\n return false;\n }\n const mint = ((now = Date.now()) => {\n if (!Number.isSafeInteger(now))\n throw new RangeError(unencodable(now));\n let time = now;\n if (time <= lastTime) {\n time = lastTime;\n if (!incrementRandom()) {\n time = lastTime + 1;\n freshRandom();\n }\n } else {\n freshRandom();\n }\n if (time < 0 || time > MAX_ULID_TIME)\n throw new RangeError(unencodable(time));\n lastTime = time;\n let r = \"\";\n for (const d of lastRand)\n r += B32[d];\n return encodeTime(time) + r;\n });\n mint.seedFrom = (id) => {\n const time = ulidTime(id);\n if (time < lastTime)\n return;\n const rand = [];\n for (const ch of id.slice(10))\n rand.push(B32.indexOf(ch));\n if (time === lastTime) {\n let ahead = false;\n for (let i = 0; i < 16; i++) {\n if (rand[i] !== lastRand[i]) {\n ahead = rand[i] > lastRand[i];\n break;\n }\n }\n if (!ahead)\n return;\n }\n lastTime = time;\n for (let i = 0; i < 16; i++)\n lastRand[i] = rand[i];\n };\n return mint;\n}\nvar processUlid = createUlid();\nfunction ulid(now = Date.now()) {\n return processUlid(now);\n}\nfunction ulidTime(id) {\n if (!ULID_SHAPE.test(id))\n throw new Error(`not a ULID: ${id}`);\n let t = 0;\n for (const ch of id.slice(0, 10))\n t = t * 32 + B32.indexOf(ch);\n if (t > MAX_ULID_TIME)\n throw new Error(`not a ULID: ${id}`);\n return t;\n}\n\n// packages/contracts/dist/wire-auth.js\nfunction secretMatches(presented, expected) {\n const given = presented ?? \"\";\n let diff = given.length ^ expected.length | (given.length === 0 ? 1 : 0);\n for (let i = 0; i < expected.length; i++) {\n diff |= (i < given.length ? given.charCodeAt(i) : 0) ^ expected.charCodeAt(i);\n }\n return diff === 0;\n}\n\n// packages/vertical-host/src/routed-node.ts\nvar ROUTED_ID = /^[0-9A-HJKMNP-TV-Z]{26}$/;\nvar RouterAssertionError = class extends Error {\n};\nfunction readRoutedNode(headers, options = {}) {\n const rawTenant = headers.get(\"x-substrat-tenant\");\n const rawScope = headers.get(\"x-substrat-scope\");\n if (!rawTenant && !rawScope) return null;\n const { expectedSecret, allowUnsigned = false } = options;\n if (expectedSecret) {\n if (!secretMatches(headers.get(\"x-substrat-router\"), expectedSecret)) {\n throw new RouterAssertionError(\"router assertion is not signed by a known router\");\n }\n } else if (!allowUnsigned) {\n throw new RouterAssertionError(\n \"router assertion cannot be verified: this worker has no ROUTER_SECRET configured\"\n );\n }\n if (!rawTenant || !rawScope) {\n throw new RouterAssertionError(\"router assertion is incomplete\");\n }\n if (!ROUTED_ID.test(rawTenant) || !ROUTED_ID.test(rawScope)) {\n throw new RouterAssertionError(\"router assertion carries a malformed id\");\n }\n return {\n tenantId: rawTenant,\n scopeId: rawScope,\n surface: headers.get(\"x-substrat-surface\") ?? \"app\",\n verticalSlug: headers.get(\"x-substrat-vertical\")\n };\n}\n\n// packages/vertical-host/src/invocation-log.ts\nfunction builtinAsyncLocalStorage() {\n try {\n const proc = globalThis.process;\n const mod = proc?.getBuiltinModule?.(\"node:async_hooks\");\n return typeof mod?.AsyncLocalStorage === \"function\" ? mod.AsyncLocalStorage : void 0;\n } catch {\n return void 0;\n }\n}\nfunction storeCallSlot(provided) {\n try {\n const g = globalThis;\n const key = Symbol.for(STORE_CALL_SINK_KEY);\n const existing = g[key];\n if (typeof existing?.run === \"function\" && typeof existing.current === \"function\") return existing;\n const Als = provided ?? builtinAsyncLocalStorage();\n if (!Als) return void 0;\n const storage = new Als();\n const slot = {\n current() {\n const tally = storage.getStore();\n return tally ? (call) => tallyStoreCall(tally, call) : void 0;\n },\n run: (tally, fn) => storage.run(tally, fn)\n };\n g[key] = slot;\n return slot;\n } catch {\n return void 0;\n }\n}\nfunction runRecordingStoreCalls(record, provided, fn) {\n const slot = storeCallSlot(provided);\n return slot ? slot.run(record, fn) : fn();\n}\nfunction versionIdOf(env) {\n const v = env?.SUBSTRAT_VERSION_ID;\n return typeof v === \"string\" && v.length > 0 ? v : null;\n}\nfunction pathOf(url) {\n try {\n return new URL(url).pathname;\n } catch {\n return \"/\";\n }\n}\nvar STAMPS = /* @__PURE__ */ Symbol.for(\"substrat.invocation-stamp\");\nfunction stamps() {\n const g = globalThis;\n return g[STAMPS] ??= /* @__PURE__ */ new WeakMap();\n}\nfunction invocationStampOf(request) {\n return stamps().get(request);\n}\nfunction withRequestId(response, id) {\n const headers = response?.headers;\n try {\n headers?.set?.(REQUEST_ID_HEADER, id);\n return response;\n } catch {\n }\n try {\n const r = response;\n if (r.status === 101) return response;\n const copy = new Response(r.body, r);\n copy.headers.set(REQUEST_ID_HEADER, id);\n return copy;\n } catch {\n return response;\n }\n}\nfunction beginStamp(request, env, options) {\n const stamp = { invocationId: ulid(), record: {} };\n const fieldCoverageId = armedFieldCoverageId(request.headers, env, options);\n if (fieldCoverageId) stamp.fieldCoverageId = fieldCoverageId;\n stamps().set(request, stamp);\n return stamp;\n}\nfunction armedFieldCoverageId(headers, env, options) {\n let id;\n try {\n id = headers.get(FIELD_COVERAGE_HEADER);\n } catch {\n return void 0;\n }\n if (id === null || !ROUTED_ID.test(id)) return void 0;\n return routedNodeOrNull(headers, env, options) !== null ? id : void 0;\n}\nfunction writeInvocationLine(stamp, done, options) {\n try {\n writeLineOrThrow(stamp, done, options);\n } catch (e) {\n try {\n console.log(JSON.stringify({ substrat: \"invocation-log-fault\", detail: String(e) }));\n } catch {\n }\n }\n}\nfunction writeLineOrThrow(stamp, done, options) {\n const node = routedNodeOrNull(done.request.headers, done.env, options);\n if (!node) return;\n const { record, invocationId } = stamp;\n const line = invocationLine({\n tenantId: node.tenantId,\n scopeId: node.scopeId,\n vertical: node.verticalSlug,\n surface: node.surface,\n method: done.request.method,\n path: pathOf(done.request.url),\n status: done.status,\n threw: done.threw,\n durationMs: Date.now() - done.started,\n invocationId,\n level: invocationLevelOf(done.status, done.threw, record.problemCode),\n operation: record.operation,\n problemCode: record.problemCode,\n principalKind: record.principalKind,\n ...record.emitted ? { emitted: record.emitted } : {},\n versionId: versionIdOf(done.env),\n ...record.outputFields ? { outputFields: record.outputFields } : {},\n ...stamp.fieldCoverageId ? { fieldCoverageId: stamp.fieldCoverageId } : {},\n ...record.storeCalls && record.storeCalls.length > 0 ? { storeCalls: record.storeCalls } : {},\n ...record.storeCallsDropped ? { storeCallsDropped: record.storeCallsDropped } : {}\n });\n console.log(JSON.stringify(line));\n}\nvar OTHER_HANDLERS = [\"scheduled\", \"queue\", \"email\", \"tail\", \"trace\", \"alarm\", \"test\"];\nfunction withInvocationLog(worker, options = {}) {\n const inner = worker?.fetch;\n if (typeof inner !== \"function\") return worker;\n const wrapped = {\n async fetch(request, env, ctx) {\n if (invocationStampOf(request)) return inner.call(worker, request, env, ctx);\n const started = Date.now();\n const stamp = beginStamp(request, env, options);\n let status = null;\n let threw = false;\n try {\n const response = withRequestId(\n await runRecordingStoreCalls(stamp.record, options.asyncLocalStorage, () => inner.call(worker, request, env, ctx)),\n stamp.invocationId\n );\n status = response.status;\n handRecordToRouter(response, stamp.record, request, env, options);\n return response;\n } catch (e) {\n threw = true;\n throw e;\n } finally {\n writeInvocationLine(stamp, { request, env, status, threw, started }, options);\n }\n }\n };\n for (const name of OTHER_HANDLERS) {\n const h = worker[name];\n if (typeof h === \"function\") wrapped[name] = h.bind(worker);\n }\n return wrapped;\n}\nfunction handRecordToRouter(response, record, request, env, options) {\n try {\n const value = encodeInvocationRecord(record);\n if (value === null || !routedNodeOrNull(request.headers, env, options)) return;\n response.headers?.set?.(\n INVOCATION_RECORD_HEADER,\n value\n );\n } catch {\n }\n}\nfunction routedNodeOrNull(headers, env, options) {\n try {\n return readRoutedNode(headers, {\n expectedSecret: options.routerSecret?.(env),\n allowUnsigned: options.allowUnsigned?.(env) ?? false\n });\n } catch (e) {\n if (!(e instanceof RouterAssertionError)) console.log(JSON.stringify({ substrat: \"invocation-log-fault\", detail: String(e) }));\n return null;\n }\n}\n\n// substrat-platform-entry.ts\nimport * as vertical from \"__SUBSTRAT_VERTICAL_ENTRY__\";\nexport * from \"__SUBSTRAT_VERTICAL_ENTRY__\";\nvar substrat_platform_entry_default = withInvocationLog(vertical.default, {\n routerSecret: (env) => env.ROUTER_SECRET,\n allowUnsigned: (env) => env.ALLOW_DEV_NODE === \"true\",\n asyncLocalStorage: AsyncLocalStorage\n});\nexport {\n substrat_platform_entry_default as default\n};\n";
|
|
12
|
+
/** The entry itself — 12959 bytes of ESM. */
|
|
13
|
+
export const PLATFORM_ENTRY_SOURCE = "// substrat-platform-entry.ts\nimport { AsyncLocalStorage } from \"node:async_hooks\";\n\n// packages/contracts/dist/invocation-record.js\nvar INVOCATION_RECORD_HEADER = \"x-substrat-invocation-record\";\nvar REQUEST_ID_HEADER = \"x-substrat-request-id\";\nvar INVOCATION_RECORD_FIELD_MAX = 128;\nvar FIELDS = [\"operation\", \"problemCode\", \"principalKind\"];\nfunction encodeInvocationRecord(record) {\n const params = new URLSearchParams();\n for (const key of FIELDS) {\n const v = record[key];\n if (typeof v === \"string\" && v !== \"\")\n params.set(key, v.slice(0, INVOCATION_RECORD_FIELD_MAX));\n }\n const value = params.toString();\n return value === \"\" ? null : value;\n}\nvar FIELD_COVERAGE_HEADER = \"x-substrat-field-coverage\";\nfunction invocationLevelOf(status, threw, problemCode) {\n if (threw || status === null || status >= 500)\n return \"error\";\n if (status >= 400)\n return \"warn\";\n if (problemCode)\n return \"warn\";\n return \"info\";\n}\n\n// packages/kernel/dist/invocation-line.js\nfunction invocationLine(f) {\n const emitted = f.emitted;\n return {\n substrat: \"invocation\",\n ...f.kind ? { kind: f.kind } : {},\n tenantId: f.tenantId,\n scopeId: f.scopeId,\n vertical: f.vertical ?? null,\n surface: f.surface ?? null,\n method: f.method ?? null,\n path: f.path ?? null,\n status: f.status ?? null,\n threw: f.threw,\n durationMs: f.durationMs,\n invocationId: f.invocationId,\n level: f.level,\n operation: f.operation ?? null,\n problemCode: f.problemCode ?? null,\n principalKind: f.principalKind ?? null,\n eventCount: emitted ? emitted.total : null,\n eventTypes: emitted ? distinct(emitted.events.map((e) => e.type)) : [],\n entities: emitted && f.withEntities !== false ? distinct(emitted.events.map((e) => e.entity)) : [],\n versionId: f.versionId ?? null,\n ...f.outputFields ? { outputFields: f.outputFields, ...f.fieldCoverageId ? { fieldCoverageId: f.fieldCoverageId } : {} } : {},\n ...f.storeCalls && f.storeCalls.length > 0 ? { storeCalls: f.storeCalls } : {},\n ...f.storeCallsDropped ? { storeCallsDropped: f.storeCallsDropped } : {},\n ...f.async ?? {}\n };\n}\nvar STORE_CALL_LOG_CAP = 16;\nfunction tallyStoreCall(tally, call) {\n const calls = tally.storeCalls ?? (tally.storeCalls = []);\n if (calls.length < STORE_CALL_LOG_CAP) {\n calls.push({ binding: call.binding, operation: call.operation, outcome: call.outcome, durationMs: call.durationMs });\n } else {\n tally.storeCallsDropped = (tally.storeCallsDropped ?? 0) + 1;\n }\n}\nvar STORE_CALL_SINK_KEY = \"substrat.store-call-sink\";\nfunction distinct(values) {\n return [...new Set(values)];\n}\n\n// packages/kernel/dist/ulid.js\nvar B32 = \"0123456789ABCDEFGHJKMNPQRSTVWXYZ\";\nvar MAX_ULID_TIME = 2 ** 48 - 1;\nvar ULID_SHAPE = /^[0-9A-HJKMNP-TV-Z]{26}$/;\nvar unencodable = (t) => `not an encodable ULID instant: ${t} (want 0..${MAX_ULID_TIME})`;\nfunction encodeTime(time) {\n let ts = \"\";\n let t = time;\n for (let i = 0; i < 10; i++) {\n ts = B32[t % 32] + ts;\n t = Math.floor(t / 32);\n }\n return ts;\n}\nfunction createUlid() {\n let lastTime = -1;\n const lastRand = new Array(16).fill(0);\n function freshRandom() {\n const bytes = crypto.getRandomValues(new Uint8Array(16));\n for (let i = 0; i < 16; i++)\n lastRand[i] = bytes[i] % 32;\n }\n function incrementRandom() {\n for (let i = 15; i >= 0; i--) {\n if (lastRand[i] < 31) {\n lastRand[i]++;\n return true;\n }\n lastRand[i] = 0;\n }\n return false;\n }\n const mint = ((now = Date.now()) => {\n if (!Number.isSafeInteger(now))\n throw new RangeError(unencodable(now));\n let time = now;\n if (time <= lastTime) {\n time = lastTime;\n if (!incrementRandom()) {\n time = lastTime + 1;\n freshRandom();\n }\n } else {\n freshRandom();\n }\n if (time < 0 || time > MAX_ULID_TIME)\n throw new RangeError(unencodable(time));\n lastTime = time;\n let r = \"\";\n for (const d of lastRand)\n r += B32[d];\n return encodeTime(time) + r;\n });\n mint.seedFrom = (id) => {\n const time = ulidTime(id);\n if (time < lastTime)\n return;\n const rand = [];\n for (const ch of id.slice(10))\n rand.push(B32.indexOf(ch));\n if (time === lastTime) {\n let ahead = false;\n for (let i = 0; i < 16; i++) {\n if (rand[i] !== lastRand[i]) {\n ahead = rand[i] > lastRand[i];\n break;\n }\n }\n if (!ahead)\n return;\n }\n lastTime = time;\n for (let i = 0; i < 16; i++)\n lastRand[i] = rand[i];\n };\n return mint;\n}\nvar processUlid = createUlid();\nfunction ulid(now = Date.now()) {\n return processUlid(now);\n}\nfunction ulidTime(id) {\n if (!ULID_SHAPE.test(id))\n throw new Error(`not a ULID: ${id}`);\n let t = 0;\n for (const ch of id.slice(0, 10))\n t = t * 32 + B32.indexOf(ch);\n if (t > MAX_ULID_TIME)\n throw new Error(`not a ULID: ${id}`);\n return t;\n}\n\n// packages/contracts/dist/wire-auth.js\nfunction secretMatches(presented, expected) {\n const given = presented ?? \"\";\n let diff = given.length ^ expected.length | (given.length === 0 ? 1 : 0);\n for (let i = 0; i < expected.length; i++) {\n diff |= (i < given.length ? given.charCodeAt(i) : 0) ^ expected.charCodeAt(i);\n }\n return diff === 0;\n}\n\n// packages/vertical-host/src/routed-node.ts\nvar ROUTED_ID = /^[0-9A-HJKMNP-TV-Z]{26}$/;\nvar RouterAssertionError = class extends Error {\n};\nfunction readRoutedNode(headers, options = {}) {\n const rawTenant = headers.get(\"x-substrat-tenant\");\n const rawScope = headers.get(\"x-substrat-scope\");\n if (!rawTenant && !rawScope) return null;\n const { expectedSecret, allowUnsigned = false } = options;\n if (expectedSecret) {\n if (!secretMatches(headers.get(\"x-substrat-router\"), expectedSecret)) {\n throw new RouterAssertionError(\"router assertion is not signed by a known router\");\n }\n } else if (!allowUnsigned) {\n throw new RouterAssertionError(\n \"router assertion cannot be verified: this worker has no ROUTER_SECRET configured\"\n );\n }\n if (!rawTenant || !rawScope) {\n throw new RouterAssertionError(\"router assertion is incomplete\");\n }\n if (!ROUTED_ID.test(rawTenant) || !ROUTED_ID.test(rawScope)) {\n throw new RouterAssertionError(\"router assertion carries a malformed id\");\n }\n return {\n tenantId: rawTenant,\n scopeId: rawScope,\n surface: headers.get(\"x-substrat-surface\") ?? \"app\",\n verticalSlug: headers.get(\"x-substrat-vertical\")\n };\n}\n\n// packages/vertical-host/src/invocation-log.ts\nfunction builtinAsyncLocalStorage() {\n try {\n const proc = globalThis.process;\n const mod = proc?.getBuiltinModule?.(\"node:async_hooks\");\n return typeof mod?.AsyncLocalStorage === \"function\" ? mod.AsyncLocalStorage : void 0;\n } catch {\n return void 0;\n }\n}\nfunction storeCallSlot(provided) {\n try {\n const g = globalThis;\n const key = Symbol.for(STORE_CALL_SINK_KEY);\n const existing = g[key];\n if (typeof existing?.run === \"function\" && typeof existing.current === \"function\") return existing;\n const Als = provided ?? builtinAsyncLocalStorage();\n if (!Als) return void 0;\n const storage = new Als();\n const slot = {\n current() {\n const tally = storage.getStore();\n return tally ? (call) => tallyStoreCall(tally, call) : void 0;\n },\n run: (tally, fn) => storage.run(tally, fn)\n };\n g[key] = slot;\n return slot;\n } catch {\n return void 0;\n }\n}\nfunction runRecordingStoreCalls(record, provided, fn) {\n const slot = storeCallSlot(provided);\n return slot ? slot.run(record, fn) : fn();\n}\nfunction versionIdOf(env) {\n const v = env?.SUBSTRAT_VERSION_ID;\n return typeof v === \"string\" && v.length > 0 ? v : null;\n}\nvar UPLOAD_LINK_SECRET_SEGMENT = /^(\\/api\\/attachment-uploads\\/[^/]+\\/[^/]+\\/)[^/]+/;\nvar redactUploadLinkSecret = (pathname) => pathname.replace(UPLOAD_LINK_SECRET_SEGMENT, \"$1[redacted]\");\nfunction pathOf(url) {\n try {\n return redactUploadLinkSecret(new URL(url).pathname);\n } catch {\n return \"/\";\n }\n}\nvar STAMPS = /* @__PURE__ */ Symbol.for(\"substrat.invocation-stamp\");\nfunction stamps() {\n const g = globalThis;\n return g[STAMPS] ??= /* @__PURE__ */ new WeakMap();\n}\nfunction invocationStampOf(request) {\n return stamps().get(request);\n}\nfunction withRequestId(response, id) {\n const headers = response?.headers;\n try {\n headers?.set?.(REQUEST_ID_HEADER, id);\n return response;\n } catch {\n }\n try {\n const r = response;\n if (r.status === 101) return response;\n const copy = new Response(r.body, r);\n copy.headers.set(REQUEST_ID_HEADER, id);\n return copy;\n } catch {\n return response;\n }\n}\nfunction beginStamp(request, env, options) {\n const stamp = { invocationId: ulid(), record: {} };\n const fieldCoverageId = armedFieldCoverageId(request.headers, env, options);\n if (fieldCoverageId) stamp.fieldCoverageId = fieldCoverageId;\n stamps().set(request, stamp);\n return stamp;\n}\nfunction armedFieldCoverageId(headers, env, options) {\n let id;\n try {\n id = headers.get(FIELD_COVERAGE_HEADER);\n } catch {\n return void 0;\n }\n if (id === null || !ROUTED_ID.test(id)) return void 0;\n return routedNodeOrNull(headers, env, options) !== null ? id : void 0;\n}\nfunction writeInvocationLine(stamp, done, options) {\n try {\n writeLineOrThrow(stamp, done, options);\n } catch (e) {\n try {\n console.log(JSON.stringify({ substrat: \"invocation-log-fault\", detail: String(e) }));\n } catch {\n }\n }\n}\nfunction writeLineOrThrow(stamp, done, options) {\n const node = routedNodeOrNull(done.request.headers, done.env, options);\n if (!node) return;\n const { record, invocationId } = stamp;\n const line = invocationLine({\n tenantId: node.tenantId,\n scopeId: node.scopeId,\n vertical: node.verticalSlug,\n surface: node.surface,\n method: done.request.method,\n path: pathOf(done.request.url),\n status: done.status,\n threw: done.threw,\n durationMs: Date.now() - done.started,\n invocationId,\n level: invocationLevelOf(done.status, done.threw, record.problemCode),\n operation: record.operation,\n problemCode: record.problemCode,\n principalKind: record.principalKind,\n ...record.emitted ? { emitted: record.emitted } : {},\n versionId: versionIdOf(done.env),\n ...record.outputFields ? { outputFields: record.outputFields } : {},\n ...stamp.fieldCoverageId ? { fieldCoverageId: stamp.fieldCoverageId } : {},\n ...record.storeCalls && record.storeCalls.length > 0 ? { storeCalls: record.storeCalls } : {},\n ...record.storeCallsDropped ? { storeCallsDropped: record.storeCallsDropped } : {}\n });\n console.log(JSON.stringify(line));\n}\nvar OTHER_HANDLERS = [\"scheduled\", \"queue\", \"email\", \"tail\", \"trace\", \"alarm\", \"test\"];\nfunction withInvocationLog(worker, options = {}) {\n const inner = worker?.fetch;\n if (typeof inner !== \"function\") return worker;\n const wrapped = {\n async fetch(request, env, ctx) {\n if (invocationStampOf(request)) return inner.call(worker, request, env, ctx);\n const started = Date.now();\n const stamp = beginStamp(request, env, options);\n let status = null;\n let threw = false;\n try {\n const response = withRequestId(\n await runRecordingStoreCalls(stamp.record, options.asyncLocalStorage, () => inner.call(worker, request, env, ctx)),\n stamp.invocationId\n );\n status = response.status;\n handRecordToRouter(response, stamp.record, request, env, options);\n return response;\n } catch (e) {\n threw = true;\n throw e;\n } finally {\n writeInvocationLine(stamp, { request, env, status, threw, started }, options);\n }\n }\n };\n for (const name of OTHER_HANDLERS) {\n const h = worker[name];\n if (typeof h === \"function\") wrapped[name] = h.bind(worker);\n }\n return wrapped;\n}\nfunction handRecordToRouter(response, record, request, env, options) {\n try {\n const value = encodeInvocationRecord(record);\n if (value === null || !routedNodeOrNull(request.headers, env, options)) return;\n response.headers?.set?.(\n INVOCATION_RECORD_HEADER,\n value\n );\n } catch {\n }\n}\nfunction routedNodeOrNull(headers, env, options) {\n try {\n return readRoutedNode(headers, {\n expectedSecret: options.routerSecret?.(env),\n allowUnsigned: options.allowUnsigned?.(env) ?? false\n });\n } catch (e) {\n if (!(e instanceof RouterAssertionError)) console.log(JSON.stringify({ substrat: \"invocation-log-fault\", detail: String(e) }));\n return null;\n }\n}\n\n// substrat-platform-entry.ts\nimport * as vertical from \"__SUBSTRAT_VERTICAL_ENTRY__\";\nexport * from \"__SUBSTRAT_VERTICAL_ENTRY__\";\nvar substrat_platform_entry_default = withInvocationLog(vertical.default, {\n routerSecret: (env) => env.ROUTER_SECRET,\n allowUnsigned: (env) => env.ALLOW_DEV_NODE === \"true\",\n asyncLocalStorage: AsyncLocalStorage\n});\nexport {\n substrat_platform_entry_default as default\n};\n";
|
|
14
14
|
/**
|
|
15
15
|
* #2245: the entry's `node:async_hooks` import, as it appears in `PLATFORM_ENTRY_SOURCE`, and
|
|
16
16
|
* the line the uploader puts in its place for a script that can have no `AsyncLocalStorage`
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"platform-entry.generated.js","sourceRoot":"","sources":["../src/platform-entry.generated.ts"],"names":[],"mappings":"AAAA,8KAA8K;AAC9K,sEAAsE;AAEtE;;;;;GAKG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,4BAA4B,CAAC;AAElE,gFAAgF;AAChF,MAAM,CAAC,MAAM,0BAA0B,GAAG,6BAA6B,CAAC;AAExE,6CAA6C;AAC7C,MAAM,CAAC,MAAM,qBAAqB,GAAW,
|
|
1
|
+
{"version":3,"file":"platform-entry.generated.js","sourceRoot":"","sources":["../src/platform-entry.generated.ts"],"names":[],"mappings":"AAAA,8KAA8K;AAC9K,sEAAsE;AAEtE;;;;;GAKG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,4BAA4B,CAAC;AAElE,gFAAgF;AAChF,MAAM,CAAC,MAAM,0BAA0B,GAAG,6BAA6B,CAAC;AAExE,6CAA6C;AAC7C,MAAM,CAAC,MAAM,qBAAqB,GAAW,iqaAAiqa,CAAC;AAE/sa;;;;;GAKG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,yDAAyD,CAAC;AACnG,MAAM,CAAC,MAAM,2BAA2B,GAAG,sCAAsC,CAAC;AAElF;;;GAGG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,8BAA8B,CAAC;AAEtE,8CAA8C;AAC9C,MAAM,CAAC,MAAM,uBAAuB,GAAW,o0TAAo0T,CAAC"}
|
|
@@ -3,11 +3,12 @@ import { type HostAdmin } from '@substrat-run/kernel';
|
|
|
3
3
|
/**
|
|
4
4
|
* A reserved scope-store call, made by the control plane straight at the store
|
|
5
5
|
* script (#2207). The vertical is not on this path: it holds neither the
|
|
6
|
-
* namespace nor the key. The key signs the
|
|
7
|
-
*
|
|
6
|
+
* namespace nor the key. The key signs the envelope here, audience `platform`,
|
|
7
|
+
* which is the only audience a `substrat.*` operation accepts.
|
|
8
8
|
*/
|
|
9
9
|
export interface PlatformScopeStoreDispatch {
|
|
10
|
-
|
|
10
|
+
/** PKCS8 Ed25519 private key, base64url. Signs audience `platform`. */
|
|
11
|
+
readonly platformKey: string;
|
|
11
12
|
fetch(scriptName: string, request: Request): Promise<Response>;
|
|
12
13
|
}
|
|
13
14
|
export interface PlatformScopeStoreTarget {
|
|
@@ -71,10 +72,34 @@ export interface PlatformScopeStores {
|
|
|
71
72
|
}
|
|
72
73
|
/** Bindings a version manifest declares. Missing JSON is none. Unreadable JSON fails closed. */
|
|
73
74
|
export declare function declaredStoresOf(manifestJson: string | null): DeclaredScopeStore[];
|
|
75
|
+
/**
|
|
76
|
+
* The version a scope's deployment runs, read once (#2207, L14). A scope with a serving pin runs
|
|
77
|
+
* the serving pointer's version when the pointer's ref is its pin, and an unknown one otherwise;
|
|
78
|
+
* a scope with no pin runs its bound version. `servingPointer` is the pointer as read, for a scope
|
|
79
|
+
* with a pin: the pair a subject-erasure claim requires to still hold (#2285). A pointer that
|
|
80
|
+
* cannot be read refuses (503): neither the version nor the fence can be named.
|
|
81
|
+
*/
|
|
82
|
+
export interface RunningVersion {
|
|
83
|
+
readonly versionId: string | null;
|
|
84
|
+
readonly servingPointer?: {
|
|
85
|
+
ref: string;
|
|
86
|
+
versionId: string;
|
|
87
|
+
} | null;
|
|
88
|
+
}
|
|
89
|
+
export declare function runningVersionOf(admin: Pick<HostAdmin, 'verticalServing'>, actor: PlatformActorId, scope: Pick<Scope, 'vertical' | 'verticalVersionId' | 'servingRef'>): Promise<RunningVersion>;
|
|
90
|
+
/**
|
|
91
|
+
* Stop the router serving a scope, before its stores are wiped (#2285, L6). The router admits a
|
|
92
|
+
* scope only while one of its hostnames is `active`, and it reads that row on every request, so
|
|
93
|
+
* unbinding is immediate. Unbind is idempotent: a retry after a failed wipe finds nothing left
|
|
94
|
+
* to remove. A hostname set merely non-active would not hold — the issuance reconcile heals a
|
|
95
|
+
* platform hostname back to `active`.
|
|
96
|
+
*/
|
|
97
|
+
export declare function unrouteScope(admin: Pick<HostAdmin, 'listHostnames' | 'unbindHostname'>, actor: PlatformActorId, scope: Pick<Scope, 'tenantId' | 'id'>): Promise<void>;
|
|
74
98
|
/**
|
|
75
99
|
* The scope stores of the version a scope's deployment runs (#2207). A scope on its vertical's
|
|
76
100
|
* serving pin runs the serving pointer's version, and any other scope its bound version; this
|
|
77
|
-
* is the version the router reads a scope's stores from (`READ_ROUTE_SQL`)
|
|
101
|
+
* is the version the router reads a scope's stores from (`READ_ROUTE_SQL`), and the one a restore
|
|
102
|
+
* may import into (L8). The lifecycle reaches more: `scopeStoresOf`. Provisioning sets the
|
|
78
103
|
* serving pin and no bound version, so reading the bound version alone answers "no store" for an
|
|
79
104
|
* ordinary install whose store the router serves. A pin that names another script than the
|
|
80
105
|
* pointer's is unknown, as #2242's erasure gate has it.
|
|
@@ -83,10 +108,38 @@ export declare function declaredStoresOf(manifestJson: string | null): DeclaredS
|
|
|
83
108
|
* answer is none. Otherwise a version that cannot be named, a manifest that cannot be read, or one
|
|
84
109
|
* the gate says may hold a store without naming it refuses (503), rather than let a backup, a
|
|
85
110
|
* wipe, a replay or an erasure skip a store that may hold data.
|
|
111
|
+
*
|
|
112
|
+
* `running` is a resolution the caller already holds (`runningVersionOf`), so a caller that fences
|
|
113
|
+
* on the version reads it once and judges the stores against the same read.
|
|
86
114
|
*/
|
|
87
|
-
export declare function
|
|
88
|
-
/**
|
|
115
|
+
export declare function runningVersionStoresOf(admin: Pick<HostAdmin, 'verticalServing' | 'versionManifest'>, actor: PlatformActorId, scope: Pick<Scope, 'id' | 'vertical' | 'verticalVersionId' | 'servingRef'>, kinds?: readonly StoreKindRegistration[], running?: RunningVersion): Promise<DeclaredScopeStore[]>;
|
|
116
|
+
/**
|
|
117
|
+
* What the lifecycle reaches for a scope (#2286, L17): the running version's stores
|
|
118
|
+
* (`runningVersionStoresOf`, which still fails closed first), and every binding any version of its
|
|
119
|
+
* vertical has declared (`HostAdmin.verticalScopeStores`, read by its keys). Export, wipe, erasure
|
|
120
|
+
* and the meter reach `all`, so an object under a binding a later version dropped is still
|
|
121
|
+
* reached. A restore imports only into `running` (L8).
|
|
122
|
+
*
|
|
123
|
+
* A binding keeps its kind for the life of the vertical, since publish refuses a re-kind; a
|
|
124
|
+
* running version whose kind disagrees with the record, or a record that cannot be read,
|
|
125
|
+
* refuses (503).
|
|
126
|
+
*/
|
|
127
|
+
export declare function scopeStoreReachOf(admin: Pick<HostAdmin, 'verticalServing' | 'versionManifest' | 'verticalScopeStores'>, actor: PlatformActorId, scope: Pick<Scope, 'id' | 'vertical' | 'verticalVersionId' | 'servingRef'>, kinds?: readonly StoreKindRegistration[], running?: RunningVersion): Promise<{
|
|
128
|
+
running: DeclaredScopeStore[];
|
|
129
|
+
all: DeclaredScopeStore[];
|
|
130
|
+
}>;
|
|
131
|
+
/** `scopeStoreReachOf`'s `all`: every store the lifecycle reaches for a scope (#2286). */
|
|
132
|
+
export declare function scopeStoresOf(admin: Pick<HostAdmin, 'verticalServing' | 'versionManifest' | 'verticalScopeStores'>, actor: PlatformActorId, scope: Pick<Scope, 'id' | 'vertical' | 'verticalVersionId' | 'servingRef'>, kinds?: readonly StoreKindRegistration[], running?: RunningVersion): Promise<DeclaredScopeStore[]>;
|
|
133
|
+
/** Sign the envelope and POST the reserved operation at that kind's script. */
|
|
89
134
|
export declare function callPlatformScopeStore(dispatch: PlatformScopeStoreDispatch, target: PlatformScopeStoreTarget, operation: ReservedStoreOperation, input?: unknown): Promise<unknown>;
|
|
135
|
+
/**
|
|
136
|
+
* The same call at a named script and object: the platform's canary (#2300), which no
|
|
137
|
+
* (vertical, scope) names. Its answer is parsed as every reserved answer is.
|
|
138
|
+
*/
|
|
139
|
+
export declare function callPlatformScopeStoreAt(dispatch: PlatformScopeStoreDispatch, target: {
|
|
140
|
+
readonly scriptName: string;
|
|
141
|
+
readonly objectName: string;
|
|
142
|
+
}, operation: ReservedStoreOperation, input?: unknown): Promise<unknown>;
|
|
90
143
|
/** The five reserved operations, one call per declared binding. */
|
|
91
144
|
export declare function platformScopeStoresFor(dispatch: PlatformScopeStoreDispatch): PlatformScopeStores;
|
|
92
145
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"platform-scope-stores.d.ts","sourceRoot":"","sources":["../src/platform-scope-stores.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"platform-scope-stores.d.ts","sourceRoot":"","sources":["../src/platform-scope-stores.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mayHoldScopeStores, scopeStoresDeclaredIn, STORE_KINDS, scopeStoreObjectName, scopeStoreScriptName,
|
|
1
|
+
import { LIST_PAGE_MAX, mayHoldScopeStores, scopeStoresDeclaredIn, STORE_KINDS, issueScopeStoreEnvelope, SCOPE_STORE_ENVELOPE_HEADER, SCOPE_STORE_SIGNATURE_HEADER, ScopeStoreKeyError, scopeStoreObjectName, scopeStoreScriptName, storeEraseResult, storeExportResult, storeSizeResult, StoreError, } from '@substrat-run/contracts';
|
|
2
2
|
import { callReservedStoreOperation } from '@substrat-run/kernel';
|
|
3
3
|
import { ControlPlaneError } from '@substrat-run/control-plane-client';
|
|
4
4
|
import { storedDeployManifest } from './deploy.js';
|
|
@@ -19,10 +19,48 @@ export function declaredStoresOf(manifestJson) {
|
|
|
19
19
|
}
|
|
20
20
|
return parsed.data.scopeStores;
|
|
21
21
|
}
|
|
22
|
+
export async function runningVersionOf(admin, actor, scope) {
|
|
23
|
+
if (!scope.vertical || !scope.servingRef)
|
|
24
|
+
return { versionId: scope.verticalVersionId };
|
|
25
|
+
let pointer;
|
|
26
|
+
try {
|
|
27
|
+
pointer = await admin.verticalServing(actor, scope.vertical);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
throw new ControlPlaneError(503, `the serving pointer of '${scope.vertical}' could not be read`);
|
|
31
|
+
}
|
|
32
|
+
const servingPointer = pointer ? { ref: pointer.ref, versionId: pointer.versionId } : null;
|
|
33
|
+
return { versionId: servingPointer?.ref === scope.servingRef ? servingPointer.versionId : null, servingPointer };
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Stop the router serving a scope, before its stores are wiped (#2285, L6). The router admits a
|
|
37
|
+
* scope only while one of its hostnames is `active`, and it reads that row on every request, so
|
|
38
|
+
* unbinding is immediate. Unbind is idempotent: a retry after a failed wipe finds nothing left
|
|
39
|
+
* to remove. A hostname set merely non-active would not hold — the issuance reconcile heals a
|
|
40
|
+
* platform hostname back to `active`.
|
|
41
|
+
*/
|
|
42
|
+
export async function unrouteScope(admin, actor, scope) {
|
|
43
|
+
// Every page: an unbind deletes the row, so each read starts again from the first hostname left.
|
|
44
|
+
// A row still listed after its unbind would loop forever, so it refuses instead.
|
|
45
|
+
const unbound = new Set();
|
|
46
|
+
for (;;) {
|
|
47
|
+
const page = await admin.listHostnames(actor, { tenantId: scope.tenantId, scopeId: scope.id, limit: LIST_PAGE_MAX });
|
|
48
|
+
if (page.length === 0)
|
|
49
|
+
return;
|
|
50
|
+
for (const { hostname } of page) {
|
|
51
|
+
if (unbound.has(hostname))
|
|
52
|
+
throw new Error(`hostname '${hostname}' is still bound after its unbind`);
|
|
53
|
+
// Only while the row still names this scope: a name rebound elsewhere since the list stays bound.
|
|
54
|
+
await admin.unbindHostname(actor, hostname, { tenantId: scope.tenantId, scopeId: scope.id });
|
|
55
|
+
unbound.add(hostname);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
22
59
|
/**
|
|
23
60
|
* The scope stores of the version a scope's deployment runs (#2207). A scope on its vertical's
|
|
24
61
|
* serving pin runs the serving pointer's version, and any other scope its bound version; this
|
|
25
|
-
* is the version the router reads a scope's stores from (`READ_ROUTE_SQL`)
|
|
62
|
+
* is the version the router reads a scope's stores from (`READ_ROUTE_SQL`), and the one a restore
|
|
63
|
+
* may import into (L8). The lifecycle reaches more: `scopeStoresOf`. Provisioning sets the
|
|
26
64
|
* serving pin and no bound version, so reading the bound version alone answers "no store" for an
|
|
27
65
|
* ordinary install whose store the router serves. A pin that names another script than the
|
|
28
66
|
* pointer's is unknown, as #2242's erasure gate has it.
|
|
@@ -31,15 +69,14 @@ export function declaredStoresOf(manifestJson) {
|
|
|
31
69
|
* answer is none. Otherwise a version that cannot be named, a manifest that cannot be read, or one
|
|
32
70
|
* the gate says may hold a store without naming it refuses (503), rather than let a backup, a
|
|
33
71
|
* wipe, a replay or an erasure skip a store that may hold data.
|
|
72
|
+
*
|
|
73
|
+
* `running` is a resolution the caller already holds (`runningVersionOf`), so a caller that fences
|
|
74
|
+
* on the version reads it once and judges the stores against the same read.
|
|
34
75
|
*/
|
|
35
|
-
export async function
|
|
76
|
+
export async function runningVersionStoresOf(admin, actor, scope, kinds = STORE_KINDS, running) {
|
|
36
77
|
if (!scope.vertical || !mayHoldScopeStores(undefined, kinds))
|
|
37
78
|
return [];
|
|
38
|
-
|
|
39
|
-
if (scope.servingRef) {
|
|
40
|
-
const pointer = await admin.verticalServing(actor, scope.vertical).catch(() => null);
|
|
41
|
-
versionId = pointer?.ref === scope.servingRef ? pointer.versionId : null;
|
|
42
|
-
}
|
|
79
|
+
const { versionId } = running ?? await runningVersionOf(admin, actor, scope);
|
|
43
80
|
const manifestJson = versionId
|
|
44
81
|
? await admin.versionManifest(actor, scope.vertical, versionId).catch(() => undefined)
|
|
45
82
|
: undefined;
|
|
@@ -50,29 +87,93 @@ export async function scopeStoresOf(admin, actor, scope, kinds = STORE_KINDS) {
|
|
|
50
87
|
return declared;
|
|
51
88
|
throw new ControlPlaneError(503, `scope ${scope.id} may hold scope stores, and the version it runs cannot say which`);
|
|
52
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* What the lifecycle reaches for a scope (#2286, L17): the running version's stores
|
|
92
|
+
* (`runningVersionStoresOf`, which still fails closed first), and every binding any version of its
|
|
93
|
+
* vertical has declared (`HostAdmin.verticalScopeStores`, read by its keys). Export, wipe, erasure
|
|
94
|
+
* and the meter reach `all`, so an object under a binding a later version dropped is still
|
|
95
|
+
* reached. A restore imports only into `running` (L8).
|
|
96
|
+
*
|
|
97
|
+
* A binding keeps its kind for the life of the vertical, since publish refuses a re-kind; a
|
|
98
|
+
* running version whose kind disagrees with the record, or a record that cannot be read,
|
|
99
|
+
* refuses (503).
|
|
100
|
+
*/
|
|
101
|
+
export async function scopeStoreReachOf(admin, actor, scope, kinds = STORE_KINDS, running) {
|
|
102
|
+
const current = await runningVersionStoresOf(admin, actor, scope, kinds, running);
|
|
103
|
+
if (!scope.vertical || !mayHoldScopeStores(undefined, kinds))
|
|
104
|
+
return { running: current, all: current };
|
|
105
|
+
let declared;
|
|
106
|
+
try {
|
|
107
|
+
declared = await admin.verticalScopeStores(actor, scope.vertical);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
throw new ControlPlaneError(503, `the scope stores '${scope.vertical}' has declared could not be read`);
|
|
111
|
+
}
|
|
112
|
+
const all = new Map(declared.map((store) => [store.binding, store.kind]));
|
|
113
|
+
for (const store of current) {
|
|
114
|
+
const kind = all.get(store.binding);
|
|
115
|
+
if (kind !== undefined && kind !== store.kind) {
|
|
116
|
+
throw new ControlPlaneError(503, `scope store '${store.binding}' is declared as both '${kind}' and '${store.kind}'`);
|
|
117
|
+
}
|
|
118
|
+
all.set(store.binding, store.kind);
|
|
119
|
+
}
|
|
120
|
+
return { running: current, all: [...all].map(([binding, kind]) => ({ binding, kind })) };
|
|
121
|
+
}
|
|
122
|
+
/** `scopeStoreReachOf`'s `all`: every store the lifecycle reaches for a scope (#2286). */
|
|
123
|
+
export async function scopeStoresOf(admin, actor, scope, kinds = STORE_KINDS, running) {
|
|
124
|
+
return (await scopeStoreReachOf(admin, actor, scope, kinds, running)).all;
|
|
125
|
+
}
|
|
53
126
|
function asControlPlane(error) {
|
|
54
127
|
if (error instanceof StoreError) {
|
|
55
128
|
throw new ControlPlaneError(error.storeStatus, error.message);
|
|
56
129
|
}
|
|
57
130
|
throw error;
|
|
58
131
|
}
|
|
59
|
-
/** Sign
|
|
132
|
+
/** Sign the envelope and POST the reserved operation at that kind's script. */
|
|
60
133
|
export async function callPlatformScopeStore(dispatch, target, operation, input) {
|
|
61
|
-
|
|
134
|
+
return callPlatformScopeStoreAt(dispatch, {
|
|
135
|
+
scriptName: scopeStoreScriptName(target.vertical, target.kind),
|
|
136
|
+
objectName: scopeStoreObjectName(target.tenantId, target.vertical, target.scopeId, target.binding),
|
|
137
|
+
}, operation, input);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* The same call at a named script and object: the platform's canary (#2300), which no
|
|
141
|
+
* (vertical, scope) names. Its answer is parsed as every reserved answer is.
|
|
142
|
+
*/
|
|
143
|
+
export async function callPlatformScopeStoreAt(dispatch, target, operation, input) {
|
|
144
|
+
if (!dispatch.platformKey) {
|
|
62
145
|
throw new ControlPlaneError(503, 'this control plane holds no scope-store key');
|
|
63
146
|
}
|
|
147
|
+
const { scriptName, objectName } = target;
|
|
64
148
|
try {
|
|
65
149
|
return await callReservedStoreOperation(async (request) => {
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
150
|
+
const body = JSON.stringify({ ...request, objectName });
|
|
151
|
+
let issued;
|
|
152
|
+
try {
|
|
153
|
+
issued = await issueScopeStoreEnvelope({
|
|
154
|
+
privateKey: dispatch.platformKey,
|
|
155
|
+
aud: 'platform',
|
|
156
|
+
script: scriptName,
|
|
157
|
+
name: objectName,
|
|
158
|
+
op: request.operation,
|
|
159
|
+
body,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
if (!(error instanceof ScopeStoreKeyError))
|
|
164
|
+
throw error;
|
|
165
|
+
// Set but unusable, e.g. a raw key where PKCS8 belongs. The message never carries the key.
|
|
166
|
+
console.warn(`scope store call refused: STORE_PLATFORM_KEY is set but unusable (${error.message})`);
|
|
167
|
+
throw new ControlPlaneError(503, 'this control plane holds no usable scope-store key');
|
|
168
|
+
}
|
|
169
|
+
const response = await dispatch.fetch(scriptName, new Request('https://scope-store/invoke', {
|
|
69
170
|
method: 'POST',
|
|
70
171
|
headers: {
|
|
71
172
|
'content-type': 'application/json',
|
|
72
|
-
|
|
73
|
-
|
|
173
|
+
[SCOPE_STORE_ENVELOPE_HEADER]: issued.header,
|
|
174
|
+
[SCOPE_STORE_SIGNATURE_HEADER]: issued.signature,
|
|
74
175
|
},
|
|
75
|
-
body
|
|
176
|
+
body,
|
|
76
177
|
}));
|
|
77
178
|
try {
|
|
78
179
|
return await response.json();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"platform-scope-stores.js","sourceRoot":"","sources":["../src/platform-scope-stores.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,kBAAkB,EAClB,qBAAqB,EACrB,WAAW,EACX,oBAAoB,EACpB,oBAAoB,EACpB,
|
|
1
|
+
{"version":3,"file":"platform-scope-stores.js","sourceRoot":"","sources":["../src/platform-scope-stores.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { LakeQueryEngine } from './lake-gateway.js';
|
|
2
|
+
export interface R2SqlEngineOptions {
|
|
3
|
+
accountId: string;
|
|
4
|
+
/** The lake's bucket — `LAKE.bucket` in `scripts/lake-provision.mjs`. */
|
|
5
|
+
bucket: string;
|
|
6
|
+
/** The query token the secrets tooling pushes as `R2_SQL_TOKEN` (`secrets/README.md`). */
|
|
7
|
+
token: string;
|
|
8
|
+
/** Injected for tests. */
|
|
9
|
+
fetchImpl?: typeof fetch;
|
|
10
|
+
/** Injected for tests; `LAKE_QUERY_TIMEOUT_MS` otherwise. */
|
|
11
|
+
timeoutMs?: number;
|
|
12
|
+
/** Injected for tests; `LAKE_RESPONSE_MAX_BYTES` otherwise. */
|
|
13
|
+
maxResponseBytes?: number;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* How long one lake query may take, request and body together. R2 SQL is open beta and
|
|
17
|
+
* names heavy queries as a timeout risk; without a bound, a stalled answer holds the
|
|
18
|
+
* control-plane request open until the platform cuts it, and the caller gets a generic
|
|
19
|
+
* failure instead of the gateway's 502.
|
|
20
|
+
*/
|
|
21
|
+
export declare const LAKE_QUERY_TIMEOUT_MS = 30000;
|
|
22
|
+
/**
|
|
23
|
+
* The largest answer the engine will read. A page is at most `LAKE_EVENTS_LIMIT_MAX` + 1
|
|
24
|
+
* envelope rows, around a kilobyte each, so 4 MiB is far above any honest answer. Anything
|
|
25
|
+
* bigger is refused before it is parsed, never parsed and then trimmed.
|
|
26
|
+
*/
|
|
27
|
+
export declare const LAKE_RESPONSE_MAX_BYTES: number;
|
|
28
|
+
/** A query R2 SQL refused or could not run. The message is Cloudflare's, never a row. */
|
|
29
|
+
export declare class LakeQueryError extends Error {
|
|
30
|
+
constructor(message: string);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The R2 SQL implementation of `LakeQueryEngine` — the Cloudflare row of kernel-design's
|
|
34
|
+
* adapter table ("Query gateway engine | R2 SQL").
|
|
35
|
+
*
|
|
36
|
+
* **Unverified against the real lake.** The endpoint and the response envelope below are
|
|
37
|
+
* written from Cloudflare's documented shape (`POST …/r2-sql/query/<bucket>` with
|
|
38
|
+
* `{ query }`, answering `{ success, errors, result: { rows } }`), and no run in this repo
|
|
39
|
+
* has exercised them. Until a human has, the control plane is deployed without the bucket
|
|
40
|
+
* var that turns this on, and the gateway's route answers 501.
|
|
41
|
+
*
|
|
42
|
+
* **Which permissions the token needs is unverified too.** It is provisioned today as an R2
|
|
43
|
+
* "Admin Read only" token scoped to the lake bucket. Cloudflare's R2 SQL docs list catalog,
|
|
44
|
+
* storage and R2 SQL permissions for a query token, and their example grants storage read
|
|
45
|
+
* and write, so whether read-only is enough has not been checked. The smoke test that turns
|
|
46
|
+
* this engine on establishes the smallest set that works. This client never writes; the
|
|
47
|
+
* gateway only ever sends the SELECT it compiled.
|
|
48
|
+
*/
|
|
49
|
+
export declare function createR2SqlEngine(opts: R2SqlEngineOptions): LakeQueryEngine;
|
|
50
|
+
//# sourceMappingURL=r2-sql-engine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"r2-sql-engine.d.ts","sourceRoot":"","sources":["../src/r2-sql-engine.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAEzD,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,yEAAyE;IACzE,MAAM,EAAE,MAAM,CAAC;IACf,0FAA0F;IAC1F,KAAK,EAAE,MAAM,CAAC;IACd,0BAA0B;IAC1B,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,6DAA6D;IAC7D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,+DAA+D;IAC/D,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,QAAS,CAAC;AAE5C;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,QAAkB,CAAC;AAEvD,yFAAyF;AACzF,qBAAa,cAAe,SAAQ,KAAK;IACvC,YAAY,OAAO,EAAE,MAAM,EAG1B;CACF;AAqCD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,kBAAkB,GAAG,eAAe,CAiD3E"}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How long one lake query may take, request and body together. R2 SQL is open beta and
|
|
3
|
+
* names heavy queries as a timeout risk; without a bound, a stalled answer holds the
|
|
4
|
+
* control-plane request open until the platform cuts it, and the caller gets a generic
|
|
5
|
+
* failure instead of the gateway's 502.
|
|
6
|
+
*/
|
|
7
|
+
export const LAKE_QUERY_TIMEOUT_MS = 30_000;
|
|
8
|
+
/**
|
|
9
|
+
* The largest answer the engine will read. A page is at most `LAKE_EVENTS_LIMIT_MAX` + 1
|
|
10
|
+
* envelope rows, around a kilobyte each, so 4 MiB is far above any honest answer. Anything
|
|
11
|
+
* bigger is refused before it is parsed, never parsed and then trimmed.
|
|
12
|
+
*/
|
|
13
|
+
export const LAKE_RESPONSE_MAX_BYTES = 4 * 1024 * 1024;
|
|
14
|
+
/** A query R2 SQL refused or could not run. The message is Cloudflare's, never a row. */
|
|
15
|
+
export class LakeQueryError extends Error {
|
|
16
|
+
constructor(message) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = 'LakeQueryError';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The response body as text, refused once it passes `maxBytes`: up front when the
|
|
23
|
+
* `content-length` says so, and while streaming when it does not (a chunked answer has none),
|
|
24
|
+
* cancelling the rest rather than reading it.
|
|
25
|
+
*/
|
|
26
|
+
async function readCapped(res, maxBytes) {
|
|
27
|
+
const tooLarge = () => new LakeQueryError(`R2 SQL answer exceeds ${maxBytes} bytes; refused before parsing`);
|
|
28
|
+
const declared = Number(res.headers.get('content-length'));
|
|
29
|
+
if (Number.isFinite(declared) && declared > maxBytes) {
|
|
30
|
+
await res.body?.cancel();
|
|
31
|
+
throw tooLarge();
|
|
32
|
+
}
|
|
33
|
+
if (!res.body)
|
|
34
|
+
return '';
|
|
35
|
+
const reader = res.body.getReader();
|
|
36
|
+
const chunks = [];
|
|
37
|
+
let total = 0;
|
|
38
|
+
for (;;) {
|
|
39
|
+
const { done, value } = await reader.read();
|
|
40
|
+
if (done)
|
|
41
|
+
break;
|
|
42
|
+
total += value.byteLength;
|
|
43
|
+
if (total > maxBytes) {
|
|
44
|
+
await reader.cancel();
|
|
45
|
+
throw tooLarge();
|
|
46
|
+
}
|
|
47
|
+
chunks.push(value);
|
|
48
|
+
}
|
|
49
|
+
const all = new Uint8Array(total);
|
|
50
|
+
let at = 0;
|
|
51
|
+
for (const c of chunks) {
|
|
52
|
+
all.set(c, at);
|
|
53
|
+
at += c.byteLength;
|
|
54
|
+
}
|
|
55
|
+
return new TextDecoder().decode(all);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The R2 SQL implementation of `LakeQueryEngine` — the Cloudflare row of kernel-design's
|
|
59
|
+
* adapter table ("Query gateway engine | R2 SQL").
|
|
60
|
+
*
|
|
61
|
+
* **Unverified against the real lake.** The endpoint and the response envelope below are
|
|
62
|
+
* written from Cloudflare's documented shape (`POST …/r2-sql/query/<bucket>` with
|
|
63
|
+
* `{ query }`, answering `{ success, errors, result: { rows } }`), and no run in this repo
|
|
64
|
+
* has exercised them. Until a human has, the control plane is deployed without the bucket
|
|
65
|
+
* var that turns this on, and the gateway's route answers 501.
|
|
66
|
+
*
|
|
67
|
+
* **Which permissions the token needs is unverified too.** It is provisioned today as an R2
|
|
68
|
+
* "Admin Read only" token scoped to the lake bucket. Cloudflare's R2 SQL docs list catalog,
|
|
69
|
+
* storage and R2 SQL permissions for a query token, and their example grants storage read
|
|
70
|
+
* and write, so whether read-only is enough has not been checked. The smoke test that turns
|
|
71
|
+
* this engine on establishes the smallest set that works. This client never writes; the
|
|
72
|
+
* gateway only ever sends the SELECT it compiled.
|
|
73
|
+
*/
|
|
74
|
+
export function createR2SqlEngine(opts) {
|
|
75
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
76
|
+
const timeoutMs = opts.timeoutMs ?? LAKE_QUERY_TIMEOUT_MS;
|
|
77
|
+
const maxBytes = opts.maxResponseBytes ?? LAKE_RESPONSE_MAX_BYTES;
|
|
78
|
+
const url = `https://api.sql.cloudflarestorage.com/api/v1/accounts/${encodeURIComponent(opts.accountId)}` +
|
|
79
|
+
`/r2-sql/query/${encodeURIComponent(opts.bucket)}`;
|
|
80
|
+
return {
|
|
81
|
+
async query(sql) {
|
|
82
|
+
let res;
|
|
83
|
+
let text;
|
|
84
|
+
try {
|
|
85
|
+
// One signal for the request AND the body read: a server that answers its headers
|
|
86
|
+
// promptly and then stalls the body is the same stall.
|
|
87
|
+
res = await fetchImpl(url, {
|
|
88
|
+
method: 'POST',
|
|
89
|
+
headers: { authorization: `Bearer ${opts.token}`, 'content-type': 'application/json' },
|
|
90
|
+
body: JSON.stringify({ query: sql }),
|
|
91
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
92
|
+
});
|
|
93
|
+
text = await readCapped(res, maxBytes);
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
if (err instanceof LakeQueryError)
|
|
97
|
+
throw err;
|
|
98
|
+
const name = err?.name;
|
|
99
|
+
if (name === 'TimeoutError' || name === 'AbortError') {
|
|
100
|
+
throw new LakeQueryError(`R2 SQL query timed out after ${timeoutMs} ms`);
|
|
101
|
+
}
|
|
102
|
+
throw new LakeQueryError(`R2 SQL request failed: ${err?.message ?? String(err)}`);
|
|
103
|
+
}
|
|
104
|
+
let body;
|
|
105
|
+
try {
|
|
106
|
+
body = JSON.parse(text);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
body = null;
|
|
110
|
+
}
|
|
111
|
+
if (!res.ok || !body || body.success === false) {
|
|
112
|
+
const reason = body?.errors?.map((e) => e.message).filter(Boolean).join('; ') || 'no error detail';
|
|
113
|
+
throw new LakeQueryError(`R2 SQL query failed (${res.status}): ${reason}`);
|
|
114
|
+
}
|
|
115
|
+
const rows = body.result?.rows;
|
|
116
|
+
if (!Array.isArray(rows) || !rows.every((r) => typeof r === 'object' && r !== null && !Array.isArray(r))) {
|
|
117
|
+
// A shape this client does not know is refused, not guessed at: the gateway's
|
|
118
|
+
// tenant check reads `tenant_id` off each row, and a row it cannot read is not one
|
|
119
|
+
// it can vouch for.
|
|
120
|
+
throw new LakeQueryError('R2 SQL answered without a rows array of objects');
|
|
121
|
+
}
|
|
122
|
+
return rows;
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=r2-sql-engine.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"r2-sql-engine.js","sourceRoot":"","sources":["../src/r2-sql-engine.ts"],"names":[],"mappings":"AAgBA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,MAAM,CAAC;AAE5C;;;;GAIG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC;AAEvD,yFAAyF;AACzF,MAAM,OAAO,cAAe,SAAQ,KAAK;IACvC,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC;IAC/B,CAAC;CACF;AAED;;;;GAIG;AACH,KAAK,UAAU,UAAU,CAAC,GAAa,EAAE,QAAgB;IACvD,MAAM,QAAQ,GAAG,GAAG,EAAE,CAAC,IAAI,cAAc,CAAC,yBAAyB,QAAQ,gCAAgC,CAAC,CAAC;IAC7G,MAAM,QAAQ,GAAG,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC,CAAC;IAC3D,IAAI,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,QAAQ,GAAG,QAAQ,EAAE,CAAC;QACrD,MAAM,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC;QACzB,MAAM,QAAQ,EAAE,CAAC;IACnB,CAAC;IACD,IAAI,CAAC,GAAG,CAAC,IAAI;QAAE,OAAO,EAAE,CAAC;IACzB,MAAM,MAAM,GAAG,GAAG,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;IACpC,MAAM,MAAM,GAAiB,EAAE,CAAC;IAChC,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,SAAS,CAAC;QACR,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;QAC5C,IAAI,IAAI;YAAE,MAAM;QAChB,KAAK,IAAI,KAAK,CAAC,UAAU,CAAC;QAC1B,IAAI,KAAK,GAAG,QAAQ,EAAE,CAAC;YACrB,MAAM,MAAM,CAAC,MAAM,EAAE,CAAC;YACtB,MAAM,QAAQ,EAAE,CAAC;QACnB,CAAC;QACD,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACrB,CAAC;IACD,MAAM,GAAG,GAAG,IAAI,UAAU,CAAC,KAAK,CAAC,CAAC;IAClC,IAAI,EAAE,GAAG,CAAC,CAAC;IACX,KAAK,MAAM,CAAC,IAAI,MAAM,EAAE,CAAC;QACvB,GAAG,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QACf,EAAE,IAAI,CAAC,CAAC,UAAU,CAAC;IACrB,CAAC;IACD,OAAO,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;AACvC,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAwB;IACxD,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,IAAI,KAAK,CAAC;IAC1C,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,IAAI,qBAAqB,CAAC;IAC1D,MAAM,QAAQ,GAAG,IAAI,CAAC,gBAAgB,IAAI,uBAAuB,CAAC;IAClE,MAAM,GAAG,GACP,yDAAyD,kBAAkB,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE;QAC7F,iBAAiB,kBAAkB,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;IACrD,OAAO;QACL,KAAK,CAAC,KAAK,CAAC,GAAG;YACb,IAAI,GAAa,CAAC;YAClB,IAAI,IAAY,CAAC;YACjB,IAAI,CAAC;gBACH,kFAAkF;gBAClF,uDAAuD;gBACvD,GAAG,GAAG,MAAM,SAAS,CAAC,GAAG,EAAE;oBACzB,MAAM,EAAE,MAAM;oBACd,OAAO,EAAE,EAAE,aAAa,EAAE,UAAU,IAAI,CAAC,KAAK,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;oBACtF,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC;oBACpC,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,SAAS,CAAC;iBACvC,CAAC,CAAC;gBACH,IAAI,GAAG,MAAM,UAAU,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;YACzC,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,IAAI,GAAG,YAAY,cAAc;oBAAE,MAAM,GAAG,CAAC;gBAC7C,MAAM,IAAI,GAAI,GAAgC,EAAE,IAAI,CAAC;gBACrD,IAAI,IAAI,KAAK,cAAc,IAAI,IAAI,KAAK,YAAY,EAAE,CAAC;oBACrD,MAAM,IAAI,cAAc,CAAC,gCAAgC,SAAS,KAAK,CAAC,CAAC;gBAC3E,CAAC;gBACD,MAAM,IAAI,cAAc,CAAC,0BAA2B,GAAa,EAAE,OAAO,IAAI,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YAC/F,CAAC;YACD,IAAI,IAAgG,CAAC;YACrG,IAAI,CAAC;gBACH,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAgB,CAAC;YACzC,CAAC;YAAC,MAAM,CAAC;gBACP,IAAI,GAAG,IAAI,CAAC;YACd,CAAC;YACD,IAAI,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,OAAO,KAAK,KAAK,EAAE,CAAC;gBAC/C,MAAM,MAAM,GAAG,IAAI,EAAE,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,iBAAiB,CAAC;gBACnG,MAAM,IAAI,cAAc,CAAC,wBAAwB,GAAG,CAAC,MAAM,MAAM,MAAM,EAAE,CAAC,CAAC;YAC7E,CAAC;YACD,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC;YAC/B,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;gBACzG,8EAA8E;gBAC9E,mFAAmF;gBACnF,oBAAoB;gBACpB,MAAM,IAAI,cAAc,CAAC,iDAAiD,CAAC,CAAC;YAC9E,CAAC;YACD,OAAO,IAAiC,CAAC;QAC3C,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -83,4 +83,16 @@ export declare function recordStrandedStorage(admin: HostAdmin, actor: PlatformA
|
|
|
83
83
|
export declare function reapScopeScriptCopies(input: ScopeCopyCleanup, tenantId: TenantId, scopeId: ScopeId): Promise<{
|
|
84
84
|
storageStranded: boolean;
|
|
85
85
|
}>;
|
|
86
|
+
/**
|
|
87
|
+
* The reap claim. The directory claim and new move records serialize on one store. A carry that
|
|
88
|
+
* already began refuses the reap (412) and keeps it retryable; one starting later cannot restore
|
|
89
|
+
* bytes after the scripts have been drained and the row removed. A reap that unroutes the scope
|
|
90
|
+
* takes this before it destroys anything (#2285), so a refusal leaves the scope routed and whole.
|
|
91
|
+
* Taking a claim already held resumes it.
|
|
92
|
+
*/
|
|
93
|
+
export declare function claimScopeScriptReap(input: Pick<ScopeCopyCleanup, 'admin' | 'actor'>, tenantId: TenantId, scopeId: ScopeId): Promise<void>;
|
|
94
|
+
/** `reapScopeScriptCopies` past its claim: delete through every named script, then settle the ledger. */
|
|
95
|
+
export declare function drainScopeScriptCopies(input: ScopeCopyCleanup, tenantId: TenantId, scopeId: ScopeId): Promise<{
|
|
96
|
+
storageStranded: boolean;
|
|
97
|
+
}>;
|
|
86
98
|
//# sourceMappingURL=scope-copy-cleanup.d.ts.map
|