@substrat-run/control-plane-api 0.141.0 → 0.143.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/api.d.ts +8 -0
  2. package/dist/api.d.ts.map +1 -1
  3. package/dist/api.js +170 -82
  4. package/dist/api.js.map +1 -1
  5. package/dist/cf-observability.js +2 -2
  6. package/dist/index.d.ts +3 -2
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +3 -2
  9. package/dist/index.js.map +1 -1
  10. package/dist/platform-drain.d.ts +17 -0
  11. package/dist/platform-drain.d.ts.map +1 -1
  12. package/dist/platform-drain.js +48 -27
  13. package/dist/platform-drain.js.map +1 -1
  14. package/dist/platform-entry.d.ts +7 -5
  15. package/dist/platform-entry.d.ts.map +1 -1
  16. package/dist/platform-entry.generated.d.ts +2 -2
  17. package/dist/platform-entry.generated.d.ts.map +1 -1
  18. package/dist/platform-entry.generated.js +5 -5
  19. package/dist/platform-entry.generated.js.map +1 -1
  20. package/dist/platform-entry.js +47 -13
  21. package/dist/platform-entry.js.map +1 -1
  22. package/dist/provider-error.d.ts +34 -5
  23. package/dist/provider-error.d.ts.map +1 -1
  24. package/dist/provider-error.js +60 -5
  25. package/dist/provider-error.js.map +1 -1
  26. package/dist/scope-copy-backfill.d.ts +106 -0
  27. package/dist/scope-copy-backfill.d.ts.map +1 -0
  28. package/dist/scope-copy-backfill.js +418 -0
  29. package/dist/scope-copy-backfill.js.map +1 -0
  30. package/dist/scope-copy-cleanup.d.ts +10 -0
  31. package/dist/scope-copy-cleanup.d.ts.map +1 -1
  32. package/dist/scope-copy-cleanup.js +12 -6
  33. package/dist/scope-copy-cleanup.js.map +1 -1
  34. package/dist/scope-deployment.d.ts +33 -0
  35. package/dist/scope-deployment.d.ts.map +1 -1
  36. package/dist/scope-deployment.js +38 -0
  37. package/dist/scope-deployment.js.map +1 -1
  38. package/dist/staff-client.d.ts +2 -1
  39. package/dist/staff-client.d.ts.map +1 -1
  40. package/dist/staff-client.js +3 -0
  41. package/dist/staff-client.js.map +1 -1
  42. package/dist/vertical-client.d.ts +16 -4
  43. package/dist/vertical-client.d.ts.map +1 -1
  44. package/dist/vertical-client.js +43 -6
  45. package/dist/vertical-client.js.map +1 -1
  46. package/package.json +7 -6
@@ -1,4 +1,4 @@
1
- // GENERATED by tools/platform-entry-emit.mts from packages/kernel/src/invocation-log.ts and packages/adapter-cloudflare/src/scope-sweeper-do.ts — do not edit by hand.
1
+ // GENERATED by tools/platform-entry-emit.mts from packages/vertical-host/src/invocation-log.ts and packages/adapter-cloudflare/src/scope-sweeper-do.ts — do not edit by hand.
2
2
  // Re-emit with `pnpm lint:platform-entry`; CI runs it with `--check`.
3
3
  /**
4
4
  * The platform's entry module (#1893): uploaded in front of a vertical's bundle and made its
@@ -9,13 +9,13 @@
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 — 10039 bytes of ESM. */
13
- export const PLATFORM_ENTRY_SOURCE = "// packages/contracts/dist/invocation-record.js\nvar INVOCATION_RECORD_HEADER = \"x-substrat-invocation-record\";\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/src/ulid.ts\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++) 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)) 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) throw new RangeError(unencodable(time));\n lastTime = time;\n let r = \"\";\n for (const d of lastRand) r += B32[d];\n return encodeTime(time) + r;\n });\n mint.seedFrom = (id) => {\n const time = ulidTime(id);\n if (time < lastTime) return;\n const rand = [];\n for (const ch of id.slice(10)) 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) return;\n }\n lastTime = time;\n for (let i = 0; i < 16; i++) 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)) throw new Error(`not a ULID: ${id}`);\n let t = 0;\n for (const ch of id.slice(0, 10)) t = t * 32 + B32.indexOf(ch);\n if (t > MAX_ULID_TIME) throw new Error(`not a ULID: ${id}`);\n return t;\n}\n\n// packages/kernel/src/platform-call.ts\nfunction secretMatches(presented, expected) {\n if (!presented || presented.length !== expected.length) return false;\n let diff = 0;\n for (let i = 0; i < expected.length; i++) {\n diff |= presented.charCodeAt(i) ^ expected.charCodeAt(i);\n }\n return diff === 0;\n}\n\n// packages/kernel/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/kernel/src/invocation-log.ts\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.async ?? {}\n };\n}\nfunction distinct(values) {\n return [...new Set(values)];\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 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 });\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 = await inner.call(worker, request, env, ctx);\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});\nexport {\n substrat_platform_entry_default as default\n};\n";
12
+ /** The entry itself — 10202 bytes of ESM. */
13
+ export const PLATFORM_ENTRY_SOURCE = "// packages/contracts/dist/invocation-record.js\nvar INVOCATION_RECORD_HEADER = \"x-substrat-invocation-record\";\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.async ?? {}\n };\n}\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 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 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 });\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 = await inner.call(worker, request, env, ctx);\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});\nexport {\n substrat_platform_entry_default as default\n};\n";
14
14
  /**
15
15
  * The scope sweeper the platform supplies (#1902) — uploaded beside the entry, which
16
16
  * re-exports its class, when a vertical declares schedules and brings no sweeper of its own.
17
17
  */
18
18
  export const PLATFORM_SWEEPER_MODULE = 'substrat-platform-sweeper.js';
19
- /** The sweeper itself — 9703 bytes of ESM. */
20
- export const PLATFORM_SWEEPER_SOURCE = "// packages/adapter-cloudflare/src/scope-sweeper-do.ts\nimport { DurableObject } from \"cloudflare:workers\";\nvar SWEEPER_ID = /^[0-9A-HJKMNP-TV-Z]{26}$/;\nfunction parseId(value, what) {\n if (typeof value !== \"string\" || !SWEEPER_ID.test(value)) {\n throw new TypeError(`${what} must be a ULID, got ${JSON.stringify(value)}`);\n }\n return value;\n}\nvar ROSTER_PREFIX = \"scope:\";\nvar JOB_START_PREFIX = \"job-start:\";\nfunction defineScopeSweeperDO(config) {\n return class ScopeSweeperDO extends DurableObject {\n /** The in-flight pass, when one is running — the overlap guard. */\n #pass = null;\n async noteScope(tenantId, scopeId) {\n const t = parseId(tenantId, \"tenantId\");\n const s = parseId(scopeId, \"scopeId\");\n await this.ctx.storage.put(`${ROSTER_PREFIX}${s}`, t);\n await this.ensureArmed();\n return { scopes: await this.#rosterSize() };\n }\n async forgetScope(scopeId) {\n const s = parseId(scopeId, \"scopeId\");\n await this.ctx.storage.delete([`${ROSTER_PREFIX}${s}`, `${JOB_START_PREFIX}${s}`]);\n return { scopes: await this.#rosterSize() };\n }\n /**\n * Arm the loop if it is not armed. Idempotent: an already-set alarm is left\n * exactly where it is, so this is safe to call on every request.\n */\n async ensureArmed() {\n const existing = await this.ctx.storage.getAlarm();\n if (existing !== null) return { armed: false, alarmAt: existing };\n const alarmAt = Date.now() + config.intervalMs;\n await this.ctx.storage.setAlarm(alarmAt);\n return { armed: true, alarmAt };\n }\n /**\n * Run a pass NOW (a manual kick, a just-provisioned scope's first tick, a\n * test). Joins the in-flight pass if one is running — never a second one —\n * and re-arms afterwards while scopes remain.\n */\n async sweepNow() {\n return this.#run();\n }\n /** The loop: one pass, then re-arm. Never throws (see the class doc). */\n async alarm() {\n await this.#run();\n }\n async #rosterSize() {\n return (await this.ctx.storage.list({ prefix: ROSTER_PREFIX })).size;\n }\n async #run() {\n if (this.#pass) return this.#pass;\n this.#pass = (async () => {\n try {\n const report = await this.#sweep();\n config.onPass?.(report, this.env);\n return report;\n } catch (err) {\n const outcome = { error: err instanceof Error ? err.message : String(err) };\n config.onPass?.(outcome, this.env);\n return outcome;\n }\n })();\n try {\n return await this.#pass;\n } finally {\n this.#pass = null;\n if (await this.#rosterSize() > 0) {\n await this.ctx.storage.setAlarm(Date.now() + config.intervalMs);\n }\n }\n }\n /** One pass over the roster: drain + due schedules per scope, errors contained. */\n async #sweep() {\n const host = config.host(this.env);\n const roster = await this.ctx.storage.list({ prefix: ROSTER_PREFIX });\n const report = {\n scopes: roster.size,\n drainTotals: { attempted: 0, delivered: 0, retrying: 0, deadLettered: 0 },\n schedules: { scopes: 0, fired: 0, skipped: 0, failed: 0 },\n ...config.runJobs ? { jobs: { attempted: 0, advanced: 0, completed: 0, retrying: 0, failed: 0 } } : {},\n errors: []\n };\n const registrations = host.registeredSchedules().filter((r) => r.schedules.length > 0);\n const entries = [...roster].map(([key, tenant]) => ({\n scopeId: key.slice(ROSTER_PREFIX.length),\n tenantId: tenant\n }));\n const concurrency = config.concurrency ?? 8;\n let next = 0;\n const worker = async () => {\n while (next < entries.length) {\n const { tenantId, scopeId } = entries[next++];\n if (host.lifecycleHeld) {\n try {\n if (await host.lifecycleHeld(scopeId)) {\n report.held = (report.held ?? 0) + 1;\n continue;\n }\n } catch (err) {\n report.errors.push({ kind: \"lifecycle\", id: scopeId, error: message(err) });\n continue;\n }\n }\n if (config.drainRetries !== false) {\n try {\n const r = await host.drainDue(tenantId, scopeId);\n report.drainTotals.attempted += r.attempted;\n report.drainTotals.delivered += r.delivered;\n report.drainTotals.retrying += r.retrying;\n report.drainTotals.deadLettered += r.deadLettered;\n } catch (err) {\n report.errors.push({ kind: \"drain\", id: scopeId, error: message(err) });\n }\n }\n if (config.runJobs) {\n if (config.startJobs) {\n try {\n const key = `${JOB_START_PREFIX}${scopeId}`;\n const last = await this.ctx.storage.get(key);\n const now = Date.now();\n if (last === void 0 || now - last >= (config.jobStartIntervalMs ?? config.intervalMs)) {\n await config.startJobs(host, tenantId, scopeId);\n await this.ctx.storage.put(key, now);\n }\n } catch (err) {\n report.errors.push({ kind: \"job\", id: `${scopeId}:start`, error: message(err) });\n }\n }\n try {\n const r = await host.runDueJobs(tenantId, scopeId);\n const totals = report.jobs;\n totals.attempted += r.attempted;\n totals.advanced += r.advanced;\n totals.completed += r.completed;\n totals.retrying += r.retrying;\n totals.failed += r.failed;\n for (const error of r.errors) {\n report.errors.push({ kind: \"job\", id: `${scopeId}:${error.runId}`, error: error.error });\n }\n } catch (err) {\n report.errors.push({ kind: \"job\", id: scopeId, error: message(err) });\n }\n }\n let touched = false;\n const passAt = (/* @__PURE__ */ new Date()).toISOString();\n const passRuns = [];\n for (const reg of registrations) {\n try {\n const r = await host.runDueSchedules(reg.moduleId, tenantId, scopeId);\n if (r.fired > 0 || r.failed > 0) touched = true;\n report.schedules.fired += r.fired;\n report.schedules.skipped += r.skipped;\n report.schedules.failed += r.failed;\n for (const e of r.errors) {\n report.errors.push({\n kind: \"schedule\",\n id: `${scopeId}:${e.operation}`,\n error: e.error\n });\n }\n for (const run of r.runs ?? []) {\n passRuns.push({\n kind: \"schedule\",\n operation: run.operation,\n outcome: run.outcome,\n at: passAt,\n ...run.outcome === \"failed\" ? { error: r.errors.find((e) => e.operation === run.operation)?.error ?? null } : {}\n });\n }\n } catch (err) {\n report.errors.push({\n kind: \"schedule\",\n id: `${scopeId}:${reg.moduleId}`,\n error: message(err)\n });\n }\n }\n if (touched) report.schedules.scopes += 1;\n if (host.registeredFreshness && host.checkFreshness) {\n const freshRegs = host.registeredFreshness().filter((r) => r.freshness.length > 0);\n if (freshRegs.length > 0) {\n try {\n const r = await host.checkFreshness(freshRegs[0].moduleId, tenantId, scopeId);\n for (const check of r.checks) {\n passRuns.push({\n kind: \"freshness\",\n eventType: check.eventType,\n outcome: check.outcome,\n at: passAt,\n observedAt: check.observedAt\n });\n }\n } catch (err) {\n report.errors.push({ kind: \"freshness\", id: scopeId, error: message(err) });\n }\n }\n }\n if (passRuns.length > 0 && host.enqueueSweepRuns) {\n try {\n await host.enqueueSweepRuns(scopeId, {\n version: config.versionId?.(this.env) ?? null,\n // The payload caps its batch; a pathological schedule count truncates\n // rather than refusing the whole report.\n entries: passRuns.slice(0, 64)\n });\n } catch {\n }\n }\n }\n };\n await Promise.all(\n Array.from({ length: Math.max(1, Math.min(concurrency, entries.length)) }, worker)\n );\n return report;\n }\n };\n}\nvar message = (err) => err instanceof Error ? err.message : String(err);\n\n// packages/vertical-host/src/scope-sweep-host.ts\nvar HOSTS = /* @__PURE__ */ Symbol.for(\"substrat.scope-sweep-host\");\nfunction registeredScopeSweepHost() {\n return globalThis[HOSTS];\n}\n\n// substrat-platform-sweeper.ts\nvar SweeperDO = defineScopeSweeperDO({\n intervalMs: 12e4,\n versionId: (env) => env.SUBSTRAT_VERSION_ID ?? null,\n host(env) {\n const hostFor = registeredScopeSweepHost();\n if (!hostFor) {\n throw new Error(\"this bundle registered no scope host for the platform sweeper: mount mountPlatformSurface from a current @substrat-run/vertical-host\");\n }\n return hostFor(env);\n },\n onPass(outcome) {\n if (\"error\" in outcome || outcome.errors.length > 0) {\n console.error(JSON.stringify({ substrat: \"scope-sweep\", ...outcome }));\n }\n }\n});\nexport {\n SweeperDO\n};\n";
19
+ /** The sweeper itself — 9754 bytes of ESM. */
20
+ export const PLATFORM_SWEEPER_SOURCE = "// packages/adapter-cloudflare/src/scope-sweeper-do.ts\nimport { DurableObject } from \"cloudflare:workers\";\nvar SWEEPER_ID = /^[0-9A-HJKMNP-TV-Z]{26}$/;\nfunction parseId(value, what) {\n if (typeof value !== \"string\" || !SWEEPER_ID.test(value)) {\n throw new TypeError(`${what} must be a ULID, got ${JSON.stringify(value)}`);\n }\n return value;\n}\nvar ROSTER_PREFIX = \"scope:\";\nvar JOB_START_PREFIX = \"job-start:\";\nfunction defineScopeSweeperDO(config) {\n return class ScopeSweeperDO extends DurableObject {\n /** The in-flight pass, when one is running — the overlap guard. */\n #pass = null;\n async noteScope(tenantId, scopeId) {\n const t = parseId(tenantId, \"tenantId\");\n const s = parseId(scopeId, \"scopeId\");\n await this.ctx.storage.put(`${ROSTER_PREFIX}${s}`, t);\n await this.ensureArmed();\n return { scopes: await this.#rosterSize() };\n }\n async forgetScope(scopeId) {\n const s = parseId(scopeId, \"scopeId\");\n await this.ctx.storage.delete([`${ROSTER_PREFIX}${s}`, `${JOB_START_PREFIX}${s}`]);\n return { scopes: await this.#rosterSize() };\n }\n /**\n * Arm the loop if it is not armed. Idempotent: an already-set alarm is left\n * exactly where it is, so this is safe to call on every request.\n */\n async ensureArmed() {\n const existing = await this.ctx.storage.getAlarm();\n if (existing !== null) return { armed: false, alarmAt: existing };\n const alarmAt = Date.now() + config.intervalMs;\n await this.ctx.storage.setAlarm(alarmAt);\n return { armed: true, alarmAt };\n }\n /**\n * Run a pass NOW (a manual kick, a just-provisioned scope's first tick, a\n * test). Joins the in-flight pass if one is running — never a second one —\n * and re-arms afterwards while scopes remain.\n */\n async sweepNow() {\n return this.#run();\n }\n /** The loop: one pass, then re-arm. Never throws (see the class doc). */\n async alarm() {\n await this.#run();\n }\n async #rosterSize() {\n return (await this.ctx.storage.list({ prefix: ROSTER_PREFIX })).size;\n }\n async #run() {\n if (this.#pass) return this.#pass;\n this.#pass = (async () => {\n try {\n const report = await this.#sweep();\n config.onPass?.(report, this.env);\n return report;\n } catch (err) {\n const outcome = { error: err instanceof Error ? err.message : String(err) };\n config.onPass?.(outcome, this.env);\n return outcome;\n }\n })();\n try {\n return await this.#pass;\n } finally {\n this.#pass = null;\n if (await this.#rosterSize() > 0) {\n await this.ctx.storage.setAlarm(Date.now() + config.intervalMs);\n }\n }\n }\n /** One pass over the roster: drain + due schedules per scope, errors contained. */\n async #sweep() {\n const host = config.host(this.env);\n const roster = await this.ctx.storage.list({ prefix: ROSTER_PREFIX });\n const report = {\n scopes: roster.size,\n drainTotals: { attempted: 0, delivered: 0, retrying: 0, deadLettered: 0 },\n schedules: { scopes: 0, fired: 0, skipped: 0, failed: 0 },\n ...config.runJobs ? { jobs: { attempted: 0, advanced: 0, completed: 0, retrying: 0, failed: 0 } } : {},\n errors: []\n };\n const registrations = host.registeredSchedules().filter((r) => r.schedules.length > 0);\n const entries = [...roster].map(([key, tenant]) => ({\n scopeId: key.slice(ROSTER_PREFIX.length),\n tenantId: tenant\n }));\n const concurrency = config.concurrency ?? 8;\n let next = 0;\n const worker = async () => {\n while (next < entries.length) {\n const { tenantId, scopeId } = entries[next++];\n if (host.lifecycleHeld) {\n try {\n if (await host.lifecycleHeld(scopeId)) {\n report.held = (report.held ?? 0) + 1;\n continue;\n }\n } catch (err) {\n report.errors.push({ kind: \"lifecycle\", id: scopeId, error: message(err) });\n continue;\n }\n }\n if (config.drainRetries !== false) {\n try {\n const r = await host.drainDue(tenantId, scopeId);\n report.drainTotals.attempted += r.attempted;\n report.drainTotals.delivered += r.delivered;\n report.drainTotals.retrying += r.retrying;\n report.drainTotals.deadLettered += r.deadLettered;\n } catch (err) {\n report.errors.push({ kind: \"drain\", id: scopeId, error: message(err) });\n }\n }\n if (config.runJobs) {\n if (config.startJobs) {\n try {\n const key = `${JOB_START_PREFIX}${scopeId}`;\n const last = await this.ctx.storage.get(key);\n const now = Date.now();\n if (last === void 0 || now - last >= (config.jobStartIntervalMs ?? config.intervalMs)) {\n await config.startJobs(host, tenantId, scopeId);\n await this.ctx.storage.put(key, now);\n }\n } catch (err) {\n report.errors.push({ kind: \"job\", id: `${scopeId}:start`, error: message(err) });\n }\n }\n try {\n const r = await host.runDueJobs(tenantId, scopeId);\n const totals = report.jobs;\n totals.attempted += r.attempted;\n totals.advanced += r.advanced;\n totals.completed += r.completed;\n totals.retrying += r.retrying;\n totals.failed += r.failed;\n for (const error of r.errors) {\n report.errors.push({ kind: \"job\", id: `${scopeId}:${error.runId}`, error: error.error });\n }\n } catch (err) {\n report.errors.push({ kind: \"job\", id: scopeId, error: message(err) });\n }\n }\n let touched = false;\n const passAt = (/* @__PURE__ */ new Date()).toISOString();\n const passRuns = [];\n for (const reg of registrations) {\n try {\n const r = await host.runDueSchedules(reg.moduleId, tenantId, scopeId);\n if (r.fired > 0 || r.failed > 0) touched = true;\n report.schedules.fired += r.fired;\n report.schedules.skipped += r.skipped;\n report.schedules.failed += r.failed;\n for (const e of r.errors) {\n report.errors.push({\n kind: \"schedule\",\n id: `${scopeId}:${e.operation}`,\n error: e.error\n });\n }\n for (const run of r.runs ?? []) {\n passRuns.push({\n kind: \"schedule\",\n operation: run.operation,\n outcome: run.outcome,\n at: passAt,\n ...run.outcome === \"failed\" ? { error: r.errors.find((e) => e.operation === run.operation)?.error ?? null } : {}\n });\n }\n } catch (err) {\n report.errors.push({\n kind: \"schedule\",\n id: `${scopeId}:${reg.moduleId}`,\n error: message(err)\n });\n }\n }\n if (touched) report.schedules.scopes += 1;\n if (host.registeredFreshness && host.checkFreshness) {\n const freshRegs = host.registeredFreshness().filter((r) => r.freshness.length > 0);\n if (freshRegs.length > 0) {\n try {\n const r = await host.checkFreshness(freshRegs[0].moduleId, tenantId, scopeId);\n for (const check of r.checks) {\n passRuns.push({\n kind: \"freshness\",\n eventType: check.eventType,\n outcome: check.outcome,\n at: passAt,\n observedAt: check.observedAt\n });\n }\n } catch (err) {\n report.errors.push({ kind: \"freshness\", id: scopeId, error: message(err) });\n }\n }\n }\n if (passRuns.length > 0 && host.enqueueSweepRuns) {\n try {\n await host.enqueueSweepRuns(scopeId, {\n version: config.versionId?.(this.env) ?? null,\n // The payload caps its batch; a pathological schedule count truncates\n // rather than refusing the whole report.\n entries: passRuns.slice(0, 64)\n });\n } catch {\n }\n }\n }\n };\n await Promise.all(\n Array.from({ length: Math.max(1, Math.min(concurrency, entries.length)) }, worker)\n );\n return report;\n }\n };\n}\nvar message = (err) => err instanceof Error ? err.message : String(err);\n\n// packages/vertical-host/src/scope-sweep-host.ts\nvar SCOPE_SWEEP_HOST_KEY = \"substrat.scope-sweep-host\";\nfunction slot() {\n return globalThis;\n}\nfunction registeredScopeSweepHost() {\n return slot()[Symbol.for(SCOPE_SWEEP_HOST_KEY)];\n}\n\n// substrat-platform-sweeper.ts\nvar SweeperDO = defineScopeSweeperDO({\n intervalMs: 12e4,\n versionId: (env) => env.SUBSTRAT_VERSION_ID ?? null,\n host(env) {\n const hostFor = registeredScopeSweepHost();\n if (!hostFor) {\n throw new Error(\"this bundle registered no scope host for the platform sweeper: mount mountPlatformSurface from a current @substrat-run/vertical-host\");\n }\n return hostFor(env);\n },\n onPass(outcome) {\n if (\"error\" in outcome || outcome.errors.length > 0) {\n console.error(JSON.stringify({ substrat: \"scope-sweep\", ...outcome }));\n }\n }\n});\nexport {\n SweeperDO\n};\n";
21
21
  //# sourceMappingURL=platform-entry.generated.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"platform-entry.generated.js","sourceRoot":"","sources":["../src/platform-entry.generated.ts"],"names":[],"mappings":"AAAA,uKAAuK;AACvK,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,ysUAAysU,CAAC;AAEvvU;;;GAGG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,8BAA8B,CAAC;AAEtE,8CAA8C;AAC9C,MAAM,CAAC,MAAM,uBAAuB,GAAW,8wTAA8wT,CAAC"}
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,23UAA23U,CAAC;AAEz6U;;;GAGG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,8BAA8B,CAAC;AAEtE,8CAA8C;AAC9C,MAAM,CAAC,MAAM,uBAAuB,GAAW,o0TAAo0T,CAAC"}
@@ -7,17 +7,17 @@
7
7
  * neither made it impossible, and a vertical that forgot served an empty Logs view that
8
8
  * read as "no traffic". So the platform no longer asks: at upload it puts its own entry
9
9
  * module in front of the bundle's, and that module wraps the vertical's default export
10
- * with the kernel's `withInvocationLog`.
10
+ * with vertical-host's `withInvocationLog`.
11
11
  *
12
12
  * Three facts about bundles decide the shape:
13
13
  *
14
- * - **A bundle that mounts the middleware too is fine.** A current kernel's middleware
14
+ * - **A bundle that mounts the middleware too is fine.** A current middleware
15
15
  * finds the platform's stamp on the request and steps aside, so the request is logged
16
16
  * once, and the operation route still writes its record onto the one line.
17
- * - **An older kernel does not know to step aside**, so wrapping it would log every request
17
+ * - **An older one does not know to step aside**, so wrapping it would log every request
18
18
  * twice. A bundle that writes the line itself (it carries the line's marker) but not the
19
19
  * shared stamp registry is uploaded as it is: it already logs, and re-pushing on a
20
- * current kernel is what moves it over.
20
+ * current vertical-host is what moves it over.
21
21
  * - **The archive script is the bundle store** that promote and backout re-upload from, so
22
22
  * a re-served bundle already holds a platform entry. It is replaced with the current
23
23
  * one, never stacked: the manifest's `entry` is always the vertical's own module.
@@ -29,15 +29,23 @@
29
29
  * `platformSweeperDecision` says when, from the push's declaration alone, and the version
30
30
  * keeps that answer for every later upload.
31
31
  */
32
- import { PLATFORM_SWEEPER_BINDING, PLATFORM_SWEEPER_CLASS, PLATFORM_SWEEPER_VAR, sweeperConflict, } from '@substrat-run/contracts';
32
+ import { PLATFORM_SWEEPER_BINDING, PLATFORM_SWEEPER_CLASS, PLATFORM_SWEEPER_VAR, PLATFORM_SWEEP_HOST_KEY, sweeperConflict, } from '@substrat-run/contracts';
33
33
  import { PLATFORM_ENTRY_MODULE, PLATFORM_ENTRY_PLACEHOLDER, PLATFORM_ENTRY_SOURCE, PLATFORM_SWEEPER_MODULE, PLATFORM_SWEEPER_SOURCE, } from './platform-entry.generated.js';
34
34
  export { PLATFORM_ENTRY_MODULE, PLATFORM_SWEEPER_MODULE };
35
35
  const PLATFORM_MODULES = new Set([PLATFORM_ENTRY_MODULE, PLATFORM_SWEEPER_MODULE]);
36
36
  /** The line `invocationLog` writes, as it appears in any build: `substrat:"invocation"`. */
37
37
  const WRITES_THE_LINE = /substrat['"]?\s*:\s*["']invocation["']/;
38
- /** The registry a current kernel shares with the platform's copy. */
38
+ /** The registry a current middleware shares with the platform's copy. */
39
39
  const SHARES_THE_STAMP = 'substrat.invocation-stamp';
40
40
  const isScript = (m) => m.contentType.includes('javascript') || /\.(m?js|cjs)$/.test(m.name);
41
+ /** The text of each of the vertical's OWN script modules — never ours, never an asset. */
42
+ function* ownScripts(bundle) {
43
+ const decoder = new TextDecoder();
44
+ for (const m of bundle.modules) {
45
+ if (!PLATFORM_MODULES.has(m.name) && isScript(m))
46
+ yield decoder.decode(m.content);
47
+ }
48
+ }
41
49
  /**
42
50
  * Why a bundle is uploaded without the platform's entry, or `undefined` when it gets one.
43
51
  * Exported for the upload's own tests and for anyone asking why a script is not wrapped.
@@ -48,22 +56,40 @@ export function platformEntrySkipReason(bundle) {
48
56
  }
49
57
  if (!bundle.modules.some((m) => m.name === bundle.entry))
50
58
  return 'the bundle holds no module named by its entry';
51
- const decoder = new TextDecoder();
52
59
  let writes = false;
53
60
  let shares = false;
54
- for (const m of bundle.modules) {
55
- if (PLATFORM_MODULES.has(m.name) || !isScript(m))
56
- continue;
57
- const text = decoder.decode(m.content);
61
+ for (const text of ownScripts(bundle)) {
58
62
  writes ||= WRITES_THE_LINE.test(text);
59
63
  shares ||= text.includes(SHARES_THE_STAMP);
60
64
  if (writes && shares)
61
- break; // a current kernel: nothing further could change the answer
65
+ break; // a current middleware: nothing further could change the answer
62
66
  }
63
67
  if (writes && !shares)
64
68
  return 'the bundle writes the invocation line with a kernel that predates the platform stamp';
65
69
  return undefined;
66
70
  }
71
+ /**
72
+ * Whether any of the vertical's own script modules carries the scope-host registry key (#1646).
73
+ *
74
+ * This does not contradict "the bundle's bytes are deliberately not consulted" below. That rule
75
+ * is about IDENTITY: which class is a sweeper, which no export name or minified method says
76
+ * reliably, so it comes from the push's declaration. This is about REGISTRATION: the supplied
77
+ * sweeper runs the host `mountPlatformSurface` puts under `Symbol.for(PLATFORM_SWEEP_HOST_KEY)`,
78
+ * and that key is a string literal every build keeps — the same kind of fact as
79
+ * `SHARES_THE_STAMP` above. A bundler drops it with the registration itself, so a bundle on a
80
+ * vertical-host that predates it, or one that never mounts `mountPlatformSurface`, has none.
81
+ * Such a bundle's roster is never filled either, so its supplied sweeper would never arm and
82
+ * never log: the schedules would go unrun with no error anywhere, which is #1646 itself.
83
+ * The CLI refuses the same case from the installed package (`platformCanSupplySweeper`), but only
84
+ * when it can find it, only without `--allow-unswept-schedules`, and only from a CLI that has the
85
+ * check — so the push route is where it holds for every push.
86
+ */
87
+ function registersSweepHost(bundle) {
88
+ for (const text of ownScripts(bundle))
89
+ if (text.includes(PLATFORM_SWEEP_HOST_KEY))
90
+ return true;
91
+ return false;
92
+ }
67
93
  /**
68
94
  * Whether the platform supplies a version's scope sweeper (#1902) — decided ONCE, by the push
69
95
  * route, from what the push DECLARED (`schedules`, `sweeperClasses`, `bindings`), and recorded
@@ -86,7 +112,9 @@ export function platformEntrySkipReason(bundle) {
86
112
  * half-matches it cannot tell apart, rather than guessing either way: a wrong "it has one"
87
113
  * leaves the schedules unrun, and a wrong "it has none" runs them twice. Last, a bundle the
88
114
  * platform entry would not wrap (`platformEntrySkipReason`) cannot carry the sweeper the entry
89
- * re-exports, and recording "supplied" for it would be a lie every later upload repeats.
115
+ * re-exports, and recording "supplied" for it would be a lie every later upload repeats — and
116
+ * neither can a bundle that registers no scope host for that sweeper to run
117
+ * (`registersSweepHost`, #1646).
90
118
  */
91
119
  export function platformSweeperDecision(
92
120
  /** The push's manifest, or the part of it the decision reads: only whether it declares schedules. */
@@ -135,6 +163,12 @@ declared, bundle) {
135
163
  return refuse(`the platform supplies a sweeper through its entry module, and this bundle cannot take one (${skip}). ` +
136
164
  `Update @substrat-run/kernel and @substrat-run/vertical-host, or export your own sweeper`);
137
165
  }
166
+ if (!registersSweepHost(bundle)) {
167
+ return refuse(`the platform supplies a sweeper that runs the scope host your mountPlatformSurface registers, and this ` +
168
+ `bundle registers none — its @substrat-run/vertical-host predates that registration, or the worker never ` +
169
+ `mounts mountPlatformSurface. Update @substrat-run/vertical-host, or export your own defineScopeSweeperDO ` +
170
+ `class bound as a store`);
171
+ }
138
172
  return { supply: true };
139
173
  }
140
174
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"platform-entry.js","sourceRoot":"","sources":["../src/platform-entry.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"platform-entry.js","sourceRoot":"","sources":["../src/platform-entry.ts"],"names":[],"mappings":"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"}
@@ -1,9 +1,38 @@
1
1
  /**
2
- * Classifying a failed connector delivery: retry it, or quote it as the provider's (#1978).
2
+ * 4xx statuses that are nevertheless worth retrying: the provider is refusing this attempt,
3
+ * not the request. Timeouts and locks clear on their own; a rate limit is an instruction to
4
+ * come back later, which is exactly what the next drain pass does.
5
+ */
6
+ export declare const RETRYABLE_CLIENT_STATUSES: ReadonlySet<number>;
7
+ /**
8
+ * The HTTP status an error carries, if it carries one. Structural on purpose (see above):
9
+ * `ScriveApiError` and any future connector's error type both satisfy it without a shared base
10
+ * class, and an error that carries no status — a network failure, a bug — reads as `undefined`,
11
+ * which is the honest answer and keeps it retryable.
12
+ */
13
+ export declare function providerErrorStatus(error: unknown): number | undefined;
14
+ /**
15
+ * Is this delivery over? — the DRAIN's question, and the only one that decides retries.
16
+ *
17
+ * A non-retryable 4xx, whoever raised it: attempt 101 carries the identical bytes past the
18
+ * identical check. Deliberately blind to WHO refused, because terminality does not depend on
19
+ * it — our own `validation_failed` is as final as the provider's 409, and both statuses come
20
+ * from the same structural read (`SubstratError` carries its catalog status; a connector's
21
+ * error carries the provider's).
22
+ */
23
+ export declare function isTerminalDispatchFailure(error: unknown): boolean;
24
+ /**
25
+ * The PROVIDER refused the request — a 4xx that came back over the wire, not one we raised.
26
+ *
27
+ * The exclusion is the whole point (#841). This predicate reads a bare numeric `status`, and
28
+ * every `SubstratError` carries one from the problem catalog — so a `permission denied:
29
+ * protocol:read` raised on our own side of egress answered `true` here, and the drain
30
+ * captioned it "a client error the provider will refuse identically on retry". Scrive never
31
+ * saw that request. An operator who reads that sentence goes and audits their Scrive account.
3
32
  *
4
- * Moving here from the kernel: only the control plane's drain reads it. For one release the
5
- * definition still lives in `@substrat-run/kernel` and this module re-exports that same
6
- * binding, so an import from either package is the one function. Import it from here.
33
+ * Terminality was never the part that was wrong, so it did not move: see
34
+ * {@link isTerminalDispatchFailure}, which is what the drain now asks. This one answers only
35
+ * "may we quote this as the provider's words", and one of ours never may.
7
36
  */
8
- export { isTerminalDispatchFailure, isTerminalProviderError, providerErrorStatus, RETRYABLE_CLIENT_STATUSES, } from '@substrat-run/kernel';
37
+ export declare function isTerminalProviderError(error: unknown): boolean;
9
38
  //# sourceMappingURL=provider-error.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"provider-error.d.ts","sourceRoot":"","sources":["../src/provider-error.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EACL,yBAAyB,EACzB,uBAAuB,EACvB,mBAAmB,EACnB,yBAAyB,GAC1B,MAAM,sBAAsB,CAAC"}
1
+ {"version":3,"file":"provider-error.d.ts","sourceRoot":"","sources":["../src/provider-error.ts"],"names":[],"mappings":"AAiBA;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,EAAE,WAAW,CAAC,MAAM,CAAiC,CAAC;AAE5F;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAItE;AAED;;;;;;;;GAQG;AACH,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAGjE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE/D"}
@@ -1,9 +1,64 @@
1
1
  /**
2
- * Classifying a failed connector delivery: retry it, or quote it as the provider's (#1978).
2
+ * Is a failed outbound call worth trying again? (#618)
3
3
  *
4
- * Moving here from the kernel: only the control plane's drain reads it. For one release the
5
- * definition still lives in `@substrat-run/kernel` and this module re-exports that same
6
- * binding, so an import from either package is the one function. Import it from here.
4
+ * The platform retries a connector delivery by re-draining its `connector:<provider>` intent,
5
+ * and until now every failure looked the same to that loop: a throw meant "transient", so a
6
+ * provider answering `409 requires valid personal number field` was retried a hundred times
7
+ * over two days. That answer is not a fault to wait out — it is the provider telling the
8
+ * CALLER its request is wrong, and attempt 101 carries the identical bytes.
9
+ *
10
+ * The rule lives in one place rather than in any one connector because both ends need it
11
+ * (here beside the drain since #1978, out of the kernel): a connector decides what status to raise, the drain decides whether to keep the intent
12
+ * drainable, and they must not disagree. It is deliberately STRUCTURAL — any error carrying a
13
+ * numeric `status` — so the drain never has to import a provider's error class to classify it,
14
+ * and a connector gets the behaviour by raising the status it already knows.
15
+ */
16
+ import { isSubstratError } from '@substrat-run/contracts';
17
+ /**
18
+ * 4xx statuses that are nevertheless worth retrying: the provider is refusing this attempt,
19
+ * not the request. Timeouts and locks clear on their own; a rate limit is an instruction to
20
+ * come back later, which is exactly what the next drain pass does.
21
+ */
22
+ export const RETRYABLE_CLIENT_STATUSES = new Set([408, 423, 425, 429]);
23
+ /**
24
+ * The HTTP status an error carries, if it carries one. Structural on purpose (see above):
25
+ * `ScriveApiError` and any future connector's error type both satisfy it without a shared base
26
+ * class, and an error that carries no status — a network failure, a bug — reads as `undefined`,
27
+ * which is the honest answer and keeps it retryable.
28
+ */
29
+ export function providerErrorStatus(error) {
30
+ if (typeof error !== 'object' || error === null)
31
+ return undefined;
32
+ const status = error.status;
33
+ return typeof status === 'number' && Number.isFinite(status) ? status : undefined;
34
+ }
35
+ /**
36
+ * Is this delivery over? — the DRAIN's question, and the only one that decides retries.
37
+ *
38
+ * A non-retryable 4xx, whoever raised it: attempt 101 carries the identical bytes past the
39
+ * identical check. Deliberately blind to WHO refused, because terminality does not depend on
40
+ * it — our own `validation_failed` is as final as the provider's 409, and both statuses come
41
+ * from the same structural read (`SubstratError` carries its catalog status; a connector's
42
+ * error carries the provider's).
43
+ */
44
+ export function isTerminalDispatchFailure(error) {
45
+ const status = providerErrorStatus(error);
46
+ return status !== undefined && status >= 400 && status < 500 && !RETRYABLE_CLIENT_STATUSES.has(status);
47
+ }
48
+ /**
49
+ * The PROVIDER refused the request — a 4xx that came back over the wire, not one we raised.
50
+ *
51
+ * The exclusion is the whole point (#841). This predicate reads a bare numeric `status`, and
52
+ * every `SubstratError` carries one from the problem catalog — so a `permission denied:
53
+ * protocol:read` raised on our own side of egress answered `true` here, and the drain
54
+ * captioned it "a client error the provider will refuse identically on retry". Scrive never
55
+ * saw that request. An operator who reads that sentence goes and audits their Scrive account.
56
+ *
57
+ * Terminality was never the part that was wrong, so it did not move: see
58
+ * {@link isTerminalDispatchFailure}, which is what the drain now asks. This one answers only
59
+ * "may we quote this as the provider's words", and one of ours never may.
7
60
  */
8
- export { isTerminalDispatchFailure, isTerminalProviderError, providerErrorStatus, RETRYABLE_CLIENT_STATUSES, } from '@substrat-run/kernel';
61
+ export function isTerminalProviderError(error) {
62
+ return !isSubstratError(error) && isTerminalDispatchFailure(error);
63
+ }
9
64
  //# sourceMappingURL=provider-error.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"provider-error.js","sourceRoot":"","sources":["../src/provider-error.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EACL,yBAAyB,EACzB,uBAAuB,EACvB,mBAAmB,EACnB,yBAAyB,GAC1B,MAAM,sBAAsB,CAAC"}
1
+ {"version":3,"file":"provider-error.js","sourceRoot":"","sources":["../src/provider-error.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAE1D;;;;GAIG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAwB,IAAI,GAAG,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;AAE5F;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CAAC,KAAc;IAChD,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,SAAS,CAAC;IAClE,MAAM,MAAM,GAAI,KAA8B,CAAC,MAAM,CAAC;IACtD,OAAO,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC;AACpF,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,yBAAyB,CAAC,KAAc;IACtD,MAAM,MAAM,GAAG,mBAAmB,CAAC,KAAK,CAAC,CAAC;IAC1C,OAAO,MAAM,KAAK,SAAS,IAAI,MAAM,IAAI,GAAG,IAAI,MAAM,GAAG,GAAG,IAAI,CAAC,yBAAyB,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;AACzG,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,uBAAuB,CAAC,KAAc;IACpD,OAAO,CAAC,eAAe,CAAC,KAAK,CAAC,IAAI,yBAAyB,CAAC,KAAK,CAAC,CAAC;AACrE,CAAC"}
@@ -0,0 +1,106 @@
1
+ import { type ScopeCopyCleanup } from './scope-copy-cleanup.js';
2
+ /**
3
+ * The backfill of copies made before the directory's copy ledger existed (#1722).
4
+ *
5
+ * A copy move records both its scripts in the ledger before it writes, so reap and erasure reach
6
+ * every copy made since. A scope's data lived in other scripts before that, and nothing names
7
+ * those copies to reap or erasure. The admin log is never swept, so a scope's own rows say where
8
+ * it was routed, and so where its store was. This walks the log, oldest first, one page per
9
+ * call, and records each such script the ledger does not know yet as `retained`.
10
+ *
11
+ * **Where a scope's data lived** is derived from the scope's whole timeline, never from one row:
12
+ * - **A pin.** Each `setScopeServingRef` row names the serving script before and after; while
13
+ * pinned, the scope's route was that script. The pin at birth is the first such row's `before`,
14
+ * or the directory's current pin when the scope has none. An unpin routes the scope to what it
15
+ * falls back on, the version bound then (or its slug, never bound), which is a home too.
16
+ * - **A bind while unpinned.** A `bindScopeVersion` row names a version (and a promote's row the
17
+ * version before it), whose registry row names its script. It is a home only while the scope was
18
+ * not pinned: a pinned scope's bind (a private vertical's promote moves the pointer of every
19
+ * served install) routes nothing there, and its script never held the scope's store.
20
+ * - **Birth by slug.** A non-preview scope born unpinned was provisioned through its vertical's
21
+ * slug, into the script of the version on `prod` at that instant (`listChannelHistory`), and no
22
+ * later row names that script unless a bind happens to. A preview is born by its own bind.
23
+ * - **A fork** is born into the script its source was routed to at that instant, whatever pin its
24
+ * own row took, derived the same way from the source's timeline (the source's slug resolves to
25
+ * its vertical's serving script when that vertical served in place then).
26
+ *
27
+ * What cannot be derived is a **failure**: reported in every response, written as an ops record on
28
+ * a real run (`scope.copy-backfill`), never marked clean, and reported again on a re-run. That is a
29
+ * row naming no version, a version the registry no longer holds or that names no script, a scope
30
+ * or fork source whose directory row is gone, a scope with no `provisionScope` row (its other rows
31
+ * are still derived), a slug with no `prod` version at the time or one that changed while the store
32
+ * may have been born (from `BIRTH_WINDOW_MS` before the birth row to the scope's activation: the
33
+ * row can come before or after the birth, see `slugAt`), a script no deployment answers for, and a
34
+ * store that cannot be read.
35
+ *
36
+ * - **Never a wipe, never a phantom.** `retained` is reached by reap and erasure and is never swept.
37
+ * A real run reads one thing, the derived home's `_substrat_meta`, for the tombstone that proves
38
+ * an earlier wipe ran. A dry run touches no store at all: it resolves each script's deployment and
39
+ * reports what a real run would check.
40
+ * - **Taken over by the ledger.** Once a move's own entry for the same script settles `done`, the
41
+ * backfilled entry settles with it (`COPY_BACKFILL_SUPERSEDE_SQL`).
42
+ * - **Idempotent and resumable.** `backfillScopeScriptCopy` writes nothing for a script the ledger
43
+ * already names for the scope; `nextCursor` is the last row read. Admin-log ids are minted before
44
+ * their row is written, so a row in flight while a page is read can sort below the cursor and be
45
+ * skipped. That is harmless here: the rows this exists for were written before the ledger existed.
46
+ * - **No race with a live move.** The entry is never pending, so no move's confirmation and no lease
47
+ * sweep acts on it; a reap claim refuses it, and an erasure that read the inventory first refuses
48
+ * to finalize on the changed count and is retried.
49
+ * - **Erasures that came first.** Each recorded entry is audited as `backfillScopeCopy` with the
50
+ * erasure epoch it was written under, and every erasure records its own (`ErasureEpochStamp`). A
51
+ * subject whose last erasure did not reach the latest recorded copy (`erasedBeforeBackfill`) is
52
+ * reported by every run, and a real run writes an ops record for each scope (`erased-before`).
53
+ * Erasing them again is the operator's call.
54
+ */
55
+ export declare const BACKFILL_PAGE_MAX = 200;
56
+ /**
57
+ * How long BEFORE its `provisionScope` row a store may have been born. `POST /verticals/:slug/instances`
58
+ * provisions in the vertical first and writes the directory row after, inside one request, whose
59
+ * `retryTransient` waits about three seconds at most: fifteen minutes is a generous margin over
60
+ * that, not a measured bound. The other side of the window has no constant (`birthEnd`).
61
+ */
62
+ export declare const BIRTH_WINDOW_MS: number;
63
+ export type CopyBackfillOutcome =
64
+ /** Dry run: a derived home whose deployment resolves; a real run checks its store, then records it. */
65
+ 'would-record' | 'recorded'
66
+ /** The ledger already names this script for the scope. */
67
+ | 'ledgered'
68
+ /** The scope routes to this script now: the route reaches it. */
69
+ | 'route'
70
+ /** The store holds a wipe's tombstone: nothing of the scope is left there. */
71
+ | 'wiped' | 'failure';
72
+ export interface CopyBackfillEntry {
73
+ /** The admin-log row the script was derived from. */
74
+ fromLogId: string;
75
+ tenantId: string | null;
76
+ scopeId: string | null;
77
+ scriptRef: string | null;
78
+ outcome: CopyBackfillOutcome;
79
+ /** Why a failure is one. */
80
+ reason?: string;
81
+ }
82
+ export interface CopyBackfillPage {
83
+ dryRun: boolean;
84
+ /** The last row read: pass it back as `cursor` to go on. */
85
+ nextCursor: string | null;
86
+ /** The page came back short: the log has no more rows after `nextCursor`. */
87
+ done: boolean;
88
+ rowsRead: number;
89
+ entries: CopyBackfillEntry[];
90
+ counts: Record<CopyBackfillOutcome, number>;
91
+ /**
92
+ * Scopes on this page with a backfilled copy and subjects erased there before it was recorded:
93
+ * those erasures never reached it, so each subject must be erased again. Reported, never run.
94
+ */
95
+ erasedBefore: {
96
+ tenantId: string;
97
+ scopeId: string;
98
+ subjects: string[];
99
+ }[];
100
+ }
101
+ export declare function backfillScopeScriptCopies(input: ScopeCopyCleanup, opts: {
102
+ dryRun: boolean;
103
+ cursor?: string;
104
+ limit?: number;
105
+ }): Promise<CopyBackfillPage>;
106
+ //# sourceMappingURL=scope-copy-backfill.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scope-copy-backfill.d.ts","sourceRoot":"","sources":["../src/scope-copy-backfill.ts"],"names":[],"mappings":"AAEA,OAAO,EAAmE,KAAK,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAEjI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoDG;AAEH,eAAO,MAAM,iBAAiB,MAAM,CAAC;AAErC;;;;;GAKG;AACH,eAAO,MAAM,eAAe,QAAc,CAAC;AAE3C,MAAM,MAAM,mBAAmB;AAC7B,uGAAuG;AACrG,cAAc,GACd,UAAU;AACZ,0DAA0D;GACxD,UAAU;AACZ,iEAAiE;GAC/D,OAAO;AACT,8EAA8E;GAC5E,OAAO,GACP,SAAS,CAAC;AAEd,MAAM,WAAW,iBAAiB;IAChC,qDAAqD;IACrD,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,OAAO,EAAE,mBAAmB,CAAC;IAC7B,4BAA4B;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,OAAO,CAAC;IAChB,4DAA4D;IAC5D,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,6EAA6E;IAC7E,IAAI,EAAE,OAAO,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,iBAAiB,EAAE,CAAC;IAC7B,MAAM,EAAE,MAAM,CAAC,mBAAmB,EAAE,MAAM,CAAC,CAAC;IAC5C;;;OAGG;IACH,YAAY,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;KAAE,EAAE,CAAC;CAC3E;AAsND,wBAAsB,yBAAyB,CAC7C,KAAK,EAAE,gBAAgB,EACvB,IAAI,EAAE;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GACzD,OAAO,CAAC,gBAAgB,CAAC,CA+E3B"}