@substrat-run/control-plane-api 0.147.0 → 0.149.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 +67 -4
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +1068 -418
- package/dist/api.js.map +1 -1
- package/dist/attachment-sweep.d.ts +29 -0
- package/dist/attachment-sweep.d.ts.map +1 -0
- package/dist/attachment-sweep.js +66 -0
- package/dist/attachment-sweep.js.map +1 -0
- package/dist/browser.d.ts +3 -1
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +3 -0
- package/dist/browser.js.map +1 -1
- package/dist/cf-observability.d.ts.map +1 -1
- package/dist/cf-observability.js +3 -1
- package/dist/cf-observability.js.map +1 -1
- package/dist/deploy.d.ts +26 -11
- package/dist/deploy.d.ts.map +1 -1
- package/dist/deploy.js +33 -18
- 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 +15 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -4
- 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/observability.d.ts +28 -0
- package/dist/observability.d.ts.map +1 -1
- package/dist/observability.js +35 -0
- package/dist/observability.js.map +1 -1
- 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 +65 -12
- package/dist/platform-scope-stores.d.ts.map +1 -1
- package/dist/platform-scope-stores.js +121 -20
- 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 +13 -1
- 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/staff-client.d.ts +10 -2
- package/dist/staff-client.d.ts.map +1 -1
- package/dist/staff-client.js +6 -1
- package/dist/staff-client.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-rebuild.d.ts +142 -0
- package/dist/store-rebuild.d.ts.map +1 -0
- package/dist/store-rebuild.js +223 -0
- package/dist/store-rebuild.js.map +1 -0
- package/dist/store-releases.d.ts +158 -0
- package/dist/store-releases.d.ts.map +1 -0
- package/dist/store-releases.js +332 -0
- package/dist/store-releases.js.map +1 -0
- package/dist/store-script.d.ts +98 -0
- package/dist/store-script.d.ts.map +1 -0
- package/dist/store-script.js +188 -0
- package/dist/store-script.js.map +1 -0
- package/dist/vertical-client.d.ts +33 -2
- package/dist/vertical-client.d.ts.map +1 -1
- package/dist/vertical-client.js +58 -3
- package/dist/vertical-client.js.map +1 -1
- package/package.json +7 -7
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The store script's entry (#2289), and the names it shares with the uploader
|
|
3
|
+
* (store-entry/names.ts). The entry is uploaded as the script's main module. The driver
|
|
4
|
+
* module replaces `__SUBSTRAT_STORE_DRIVER__` and its class name replaces
|
|
5
|
+
* `__SUBSTRAT_STORE_CLASS__`.
|
|
6
|
+
*/
|
|
7
|
+
/** The `caller` audience's public keys: raw Ed25519, base64url, comma-separated. */
|
|
8
|
+
export declare const STORE_CALLER_KEYS_VAR = "SUBSTRAT_STORE_CALLER_KEYS";
|
|
9
|
+
/** The class name the uploader replaces, inside a property access on the driver module. */
|
|
10
|
+
export declare const STORE_CLASS_PLACEHOLDER = "__SUBSTRAT_STORE_CLASS__";
|
|
11
|
+
/** The import specifier the uploader replaces with `./<the driver module>`. */
|
|
12
|
+
export declare const STORE_DRIVER_PLACEHOLDER = "__SUBSTRAT_STORE_DRIVER__";
|
|
13
|
+
/** The entry's module name in the upload. Reserved: a driver release may not ship a module by it (D-63). */
|
|
14
|
+
export declare const STORE_ENTRY_MODULE = "substrat-store-entry.js";
|
|
15
|
+
/**
|
|
16
|
+
* The Python entry's module name in the upload (#2290), the script's main module when the driver
|
|
17
|
+
* is Python. Reserved.
|
|
18
|
+
*/
|
|
19
|
+
export declare const STORE_ENTRY_PYTHON_MODULE = "substrat_store_entry.py";
|
|
20
|
+
/** The binding the entry uses to address that class. */
|
|
21
|
+
export declare const STORE_OBJECT_BINDING = "SUBSTRAT_STORE";
|
|
22
|
+
/** The object class the entry addresses, and the SQLite class the upload migrates. Reserved. */
|
|
23
|
+
export declare const STORE_OBJECT_CLASS = "SubstratStoreObject";
|
|
24
|
+
/** The `platform` audience's public keys, in the same form. */
|
|
25
|
+
export declare const STORE_PLATFORM_KEYS_VAR = "SUBSTRAT_STORE_PLATFORM_KEYS";
|
|
26
|
+
/**
|
|
27
|
+
* The compatibility date a Python driver's store script is uploaded with. One constant, so the
|
|
28
|
+
* upload and the package's probe cannot run different runtimes.
|
|
29
|
+
*/
|
|
30
|
+
export declare const STORE_PYTHON_COMPATIBILITY_DATE = "2026-03-01";
|
|
31
|
+
/** The Python runtime's own flag. The upload adds the two that keep the entry's bindings from the driver. */
|
|
32
|
+
export declare const STORE_PYTHON_COMPATIBILITY_FLAGS: string[];
|
|
33
|
+
/**
|
|
34
|
+
* Every binding the entry owns starts with this. The driver's class is constructed with an env
|
|
35
|
+
* that has none of them, and a driver bundle may not declare one.
|
|
36
|
+
*/
|
|
37
|
+
export declare const STORE_RESERVED_BINDING_PREFIX = "SUBSTRAT_STORE";
|
|
38
|
+
/** Every module name the platform's entries own. A driver release may ship none of them (D-63). */
|
|
39
|
+
export declare const STORE_RESERVED_MODULES: string[];
|
|
40
|
+
/** The store script's own name, as the signer writes it into the envelope's `script` line. */
|
|
41
|
+
export declare const STORE_SCRIPT_VAR = "SUBSTRAT_STORE_SCRIPT";
|
|
42
|
+
/**
|
|
43
|
+
* The verifier the Python entry imports: `store-entry/verify.ts`, bundled as an ES module beside
|
|
44
|
+
* it, so both entries run one verifier. Reserved.
|
|
45
|
+
*/
|
|
46
|
+
export declare const STORE_VERIFIER_MODULE = "substrat-store-verify.js";
|
|
47
|
+
/** The entry itself — 11397 bytes of ESM. */
|
|
48
|
+
export declare const STORE_ENTRY_SOURCE: string;
|
|
49
|
+
/**
|
|
50
|
+
* The Python entry (#2290), uploaded as `substrat_store_entry.py`. The driver module's
|
|
51
|
+
* name (a Python module name, no `.py`) replaces `__SUBSTRAT_STORE_DRIVER__` and its class
|
|
52
|
+
* name replaces `__SUBSTRAT_STORE_CLASS__`, as in the JavaScript entry.
|
|
53
|
+
*/
|
|
54
|
+
export declare const STORE_ENTRY_PYTHON_SOURCE: string;
|
|
55
|
+
/**
|
|
56
|
+
* The verifier the Python entry imports, uploaded beside it as `substrat-store-verify.js`
|
|
57
|
+
* (an ES module). It is `packages/control-plane-api/store-entry/verify.ts`, the check the JavaScript entry runs — 9167 bytes.
|
|
58
|
+
*/
|
|
59
|
+
export declare const STORE_ENTRY_PYTHON_VERIFIER_SOURCE: string;
|
|
60
|
+
//# sourceMappingURL=store-entry.generated.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store-entry.generated.d.ts","sourceRoot":"","sources":["../src/store-entry.generated.ts"],"names":[],"mappings":"AAGA;;;;;GAKG;AAEH,oFAAoF;AACpF,eAAO,MAAM,qBAAqB,+BAA+B,CAAC;AAElE,2FAA2F;AAC3F,eAAO,MAAM,uBAAuB,6BAA6B,CAAC;AAElE,+EAA+E;AAC/E,eAAO,MAAM,wBAAwB,8BAA8B,CAAC;AAEpE,4GAA4G;AAC5G,eAAO,MAAM,kBAAkB,4BAA4B,CAAC;AAE5D;;;GAGG;AACH,eAAO,MAAM,yBAAyB,4BAA4B,CAAC;AAEnE,wDAAwD;AACxD,eAAO,MAAM,oBAAoB,mBAAmB,CAAC;AAErD,gGAAgG;AAChG,eAAO,MAAM,kBAAkB,wBAAwB,CAAC;AAExD,+DAA+D;AAC/D,eAAO,MAAM,uBAAuB,iCAAiC,CAAC;AAEtE;;;GAGG;AACH,eAAO,MAAM,+BAA+B,eAAe,CAAC;AAE5D,6GAA6G;AAC7G,eAAO,MAAM,gCAAgC,UAAqB,CAAC;AAEnE;;;GAGG;AACH,eAAO,MAAM,6BAA6B,mBAAmB,CAAC;AAE9D,mGAAmG;AACnG,eAAO,MAAM,sBAAsB,UAAmF,CAAC;AAEvH,8FAA8F;AAC9F,eAAO,MAAM,gBAAgB,0BAA0B,CAAC;AAExD;;;GAGG;AACH,eAAO,MAAM,qBAAqB,6BAA6B,CAAC;AAEhE,6CAA6C;AAC7C,eAAO,MAAM,kBAAkB,EAAE,MAA8nX,CAAC;AAEhqX;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,EAAE,MAAusK,CAAC;AAEhvK;;;GAGG;AACH,eAAO,MAAM,kCAAkC,EAAE,MAA83S,CAAC"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// GENERATED by tools/store-entry-emit.mts from packages/control-plane-api/store-entry/entry.ts, packages/control-plane-api/store-entry/verify.ts and packages/store-entry-python/src/substrat_store_entry.py — do not edit by hand.
|
|
2
|
+
// Re-emit with `pnpm lint:store-entry`; CI runs it with `--check`.
|
|
3
|
+
/**
|
|
4
|
+
* The store script's entry (#2289), and the names it shares with the uploader
|
|
5
|
+
* (store-entry/names.ts). The entry is uploaded as the script's main module. The driver
|
|
6
|
+
* module replaces `__SUBSTRAT_STORE_DRIVER__` and its class name replaces
|
|
7
|
+
* `__SUBSTRAT_STORE_CLASS__`.
|
|
8
|
+
*/
|
|
9
|
+
/** The `caller` audience's public keys: raw Ed25519, base64url, comma-separated. */
|
|
10
|
+
export const STORE_CALLER_KEYS_VAR = "SUBSTRAT_STORE_CALLER_KEYS";
|
|
11
|
+
/** The class name the uploader replaces, inside a property access on the driver module. */
|
|
12
|
+
export const STORE_CLASS_PLACEHOLDER = "__SUBSTRAT_STORE_CLASS__";
|
|
13
|
+
/** The import specifier the uploader replaces with `./<the driver module>`. */
|
|
14
|
+
export const STORE_DRIVER_PLACEHOLDER = "__SUBSTRAT_STORE_DRIVER__";
|
|
15
|
+
/** The entry's module name in the upload. Reserved: a driver release may not ship a module by it (D-63). */
|
|
16
|
+
export const STORE_ENTRY_MODULE = "substrat-store-entry.js";
|
|
17
|
+
/**
|
|
18
|
+
* The Python entry's module name in the upload (#2290), the script's main module when the driver
|
|
19
|
+
* is Python. Reserved.
|
|
20
|
+
*/
|
|
21
|
+
export const STORE_ENTRY_PYTHON_MODULE = "substrat_store_entry.py";
|
|
22
|
+
/** The binding the entry uses to address that class. */
|
|
23
|
+
export const STORE_OBJECT_BINDING = "SUBSTRAT_STORE";
|
|
24
|
+
/** The object class the entry addresses, and the SQLite class the upload migrates. Reserved. */
|
|
25
|
+
export const STORE_OBJECT_CLASS = "SubstratStoreObject";
|
|
26
|
+
/** The `platform` audience's public keys, in the same form. */
|
|
27
|
+
export const STORE_PLATFORM_KEYS_VAR = "SUBSTRAT_STORE_PLATFORM_KEYS";
|
|
28
|
+
/**
|
|
29
|
+
* The compatibility date a Python driver's store script is uploaded with. One constant, so the
|
|
30
|
+
* upload and the package's probe cannot run different runtimes.
|
|
31
|
+
*/
|
|
32
|
+
export const STORE_PYTHON_COMPATIBILITY_DATE = "2026-03-01";
|
|
33
|
+
/** The Python runtime's own flag. The upload adds the two that keep the entry's bindings from the driver. */
|
|
34
|
+
export const STORE_PYTHON_COMPATIBILITY_FLAGS = ["python_workers"];
|
|
35
|
+
/**
|
|
36
|
+
* Every binding the entry owns starts with this. The driver's class is constructed with an env
|
|
37
|
+
* that has none of them, and a driver bundle may not declare one.
|
|
38
|
+
*/
|
|
39
|
+
export const STORE_RESERVED_BINDING_PREFIX = "SUBSTRAT_STORE";
|
|
40
|
+
/** Every module name the platform's entries own. A driver release may ship none of them (D-63). */
|
|
41
|
+
export const STORE_RESERVED_MODULES = ["substrat-store-entry.js", "substrat_store_entry.py", "substrat-store-verify.js"];
|
|
42
|
+
/** The store script's own name, as the signer writes it into the envelope's `script` line. */
|
|
43
|
+
export const STORE_SCRIPT_VAR = "SUBSTRAT_STORE_SCRIPT";
|
|
44
|
+
/**
|
|
45
|
+
* The verifier the Python entry imports: `store-entry/verify.ts`, bundled as an ES module beside
|
|
46
|
+
* it, so both entries run one verifier. Reserved.
|
|
47
|
+
*/
|
|
48
|
+
export const STORE_VERIFIER_MODULE = "substrat-store-verify.js";
|
|
49
|
+
/** The entry itself — 11397 bytes of ESM. */
|
|
50
|
+
export const STORE_ENTRY_SOURCE = "// packages/control-plane-api/store-entry/entry.ts\nimport { DurableObject } from \"cloudflare:workers\";\nimport * as driverModule from \"__SUBSTRAT_STORE_DRIVER__\";\n\n// packages/control-plane-api/store-entry/names.ts\nvar STORE_OBJECT_BINDING = \"SUBSTRAT_STORE\";\nvar STORE_SCRIPT_VAR = \"SUBSTRAT_STORE_SCRIPT\";\nvar STORE_CALLER_KEYS_VAR = \"SUBSTRAT_STORE_CALLER_KEYS\";\nvar STORE_PLATFORM_KEYS_VAR = \"SUBSTRAT_STORE_PLATFORM_KEYS\";\nvar STORE_RESERVED_BINDING_PREFIX = \"SUBSTRAT_STORE\";\n\n// packages/contracts/dist/scope-store-envelope.js\nvar SCOPE_STORE_ENVELOPE_WINDOW_SECONDS = 60;\nvar SCOPE_STORE_ENVELOPE_HEADER = \"x-substrat-store-envelope\";\nvar SCOPE_STORE_SIGNATURE_HEADER = \"x-substrat-store-signature\";\nvar RESERVED_STORE_OPERATIONS = [\n \"substrat.export\",\n \"substrat.import\",\n \"substrat.size\",\n \"substrat.wipe\",\n \"substrat.erase\"\n];\nvar ADMITTED = {\n \"1\": {\n platform: (op) => RESERVED_STORE_OPERATIONS.includes(op),\n caller: (op) => !op.startsWith(\"substrat.\")\n }\n};\nfunction scopeStoreEnvelopeAdmits(v, aud, op) {\n const byAudience = Object.hasOwn(ADMITTED, String(v)) ? ADMITTED[String(v)] : void 0;\n if (!byAudience || !Object.hasOwn(byAudience, aud))\n return false;\n return byAudience[aud](op);\n}\nfunction bytesToBase64Url(bytes) {\n let binary = \"\";\n for (const byte of bytes)\n binary += String.fromCharCode(byte);\n return btoa(binary).replaceAll(\"+\", \"-\").replaceAll(\"/\", \"_\").replaceAll(\"=\", \"\");\n}\nfunction base64UrlToBytes(value) {\n if (!/^[A-Za-z0-9_-]+$/.test(value))\n return void 0;\n const padded = value.replaceAll(\"-\", \"+\").replaceAll(\"_\", \"/\") + \"=\".repeat((4 - value.length % 4) % 4);\n try {\n const binary = atob(padded);\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++)\n bytes[i] = binary.charCodeAt(i);\n return bytes;\n } catch {\n return void 0;\n }\n}\nfunction copyBytes(bytes) {\n const copy = new ArrayBuffer(bytes.byteLength);\n new Uint8Array(copy).set(bytes);\n return copy;\n}\nfunction isCanonicalBase64Url(text2, length) {\n const bytes = base64UrlToBytes(text2);\n return bytes?.length === length && bytesToBase64Url(bytes) === text2;\n}\nvar NONCE = /^[A-Za-z0-9_-]{22}$/;\nvar KID = /^[A-Za-z0-9_-]{11}$/;\nvar DIGEST = /^[0-9a-f]{64}$/;\nvar IAT = /^(0|[1-9]\\d{0,14})$/;\nfunction parseScopeStoreEnvelope(canonical) {\n const lines = canonical.split(\"\\n\");\n if (lines.length !== 9)\n return void 0;\n const [version, aud, kid, script, name, op, digest, iatText, nonce] = lines;\n if (version !== \"1\" || aud !== \"caller\" && aud !== \"platform\")\n return void 0;\n if (!script || !name || !op)\n return void 0;\n if (!KID.test(kid) || !DIGEST.test(digest) || !IAT.test(iatText) || !NONCE.test(nonce))\n return void 0;\n if (!isCanonicalBase64Url(kid, 8) || !isCanonicalBase64Url(nonce, 16))\n return void 0;\n return { v: 1, aud, kid, script, name, op, digest, iat: Number(iatText), nonce };\n}\nasync function scopeStoreBodyDigest(body) {\n const hash = await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(body));\n return [...new Uint8Array(hash)].map((byte) => byte.toString(16).padStart(2, \"0\")).join(\"\");\n}\nfunction scopeStoreEnvelopeFresh(iat, nowSeconds, windowSeconds = SCOPE_STORE_ENVELOPE_WINDOW_SECONDS) {\n return Math.abs(nowSeconds - iat) <= windowSeconds;\n}\nvar SMALL_ORDER_KEYS = [\n \"0000000000000000000000000000000000000000000000000000000000000000\",\n // y = 0, order 4\n \"0100000000000000000000000000000000000000000000000000000000000000\",\n // the identity, order 1\n \"26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc05\",\n // order 8\n \"c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac037a\",\n // order 8\n \"ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f\",\n // y = p - 1, order 2\n \"edffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f\",\n // y = p, a non-canonical 0\n \"eeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f\"\n // y = p + 1, a non-canonical identity\n];\nfunction isSmallOrderKey(bytes) {\n const unsigned = Uint8Array.from(bytes);\n unsigned[31] = unsigned[31] & 127;\n const hex = [...unsigned].map((byte) => byte.toString(16).padStart(2, \"0\")).join(\"\");\n return SMALL_ORDER_KEYS.includes(hex);\n}\nasync function scopeStoreKeyId(publicKey) {\n const bytes = base64UrlToBytes(publicKey);\n if (!bytes || bytes.length !== 32)\n throw new Error(\"a store public key is not 32 bytes of base64url\");\n if (isSmallOrderKey(bytes))\n throw new Error(\"a store public key is a small-order point\");\n const hash = await crypto.subtle.digest(\"SHA-256\", copyBytes(bytes));\n return bytesToBase64Url(new Uint8Array(hash).slice(0, 8));\n}\nfunction scopeStorePublicKeyProblem(key) {\n if (key.length === 0)\n return \"empty\";\n const bytes = base64UrlToBytes(key);\n if (bytes?.length !== 32)\n return \"length\";\n if (isSmallOrderKey(bytes))\n return \"small-order\";\n if (bytesToBase64Url(bytes) !== key)\n return \"non-canonical\";\n return void 0;\n}\nvar PUBLIC_KEY_PROBLEM = {\n empty: \"a store public key list is empty or has an empty entry\",\n length: \"a store public key is not 32 bytes of base64url\",\n \"small-order\": \"a store public key is a small-order point\",\n \"non-canonical\": \"a store public key is not canonical base64url\"\n};\nfunction parseScopeStorePublicKeys(text2) {\n const keys = text2.split(\",\").map((key) => key.trim());\n if (keys.some((key) => key.length === 0))\n throw new Error(PUBLIC_KEY_PROBLEM.empty);\n const seen = /* @__PURE__ */ new Set();\n for (const key of keys) {\n const problem = scopeStorePublicKeyProblem(key);\n if (problem)\n throw new Error(PUBLIC_KEY_PROBLEM[problem]);\n if (seen.has(key))\n throw new Error(\"a store public key is listed twice\");\n seen.add(key);\n }\n return keys;\n}\nfunction scopeStorePublicKeysFrom(config) {\n const caller = parseScopeStorePublicKeys(config.caller);\n const platform = parseScopeStorePublicKeys(config.platform);\n if (caller.some((key) => platform.includes(key))) {\n throw new Error(\"a store public key is listed for both audiences\");\n }\n return { caller, platform };\n}\nasync function verifyScopeStoreEnvelope(publicKey, canonical, signature) {\n const keyBytes = base64UrlToBytes(publicKey);\n const sigBytes = base64UrlToBytes(signature);\n if (!keyBytes || !sigBytes || sigBytes.length !== 64)\n return false;\n try {\n const key = await crypto.subtle.importKey(\"raw\", copyBytes(keyBytes), { name: \"Ed25519\" }, false, [\"verify\"]);\n return await crypto.subtle.verify(\"Ed25519\", key, copyBytes(sigBytes), new TextEncoder().encode(canonical));\n } catch {\n return false;\n }\n}\nasync function verifyScopeStoreCall(input) {\n const refuse = (refusal) => ({ ok: false, refusal });\n if (!input.envelope || !input.signature)\n return refuse(\"missing\");\n const bytes = base64UrlToBytes(input.envelope);\n if (!bytes)\n return refuse(\"malformed\");\n let canonical;\n try {\n canonical = new TextDecoder(\"utf-8\", { fatal: true }).decode(bytes);\n } catch {\n return refuse(\"malformed\");\n }\n const envelope = parseScopeStoreEnvelope(canonical);\n if (!envelope)\n return refuse(\"malformed\");\n if (!scopeStoreEnvelopeAdmits(envelope.v, envelope.aud, envelope.op))\n return refuse(\"audience\");\n const kidsOf = async (keys) => {\n const kids = /* @__PURE__ */ new Map();\n for (const key of keys) {\n try {\n kids.set(await scopeStoreKeyId(key), key);\n } catch {\n }\n }\n return kids;\n };\n const own = await kidsOf(input.keys[envelope.aud]);\n const other = await kidsOf(input.keys[envelope.aud === \"caller\" ? \"platform\" : \"caller\"]);\n if ([...own.keys()].some((kid) => other.has(kid)))\n return refuse(\"keys\");\n const publicKey = own.get(envelope.kid);\n if (!publicKey)\n return refuse(\"kid\");\n if (!await verifyScopeStoreEnvelope(publicKey, canonical, input.signature))\n return refuse(\"signature\");\n if (envelope.digest !== await scopeStoreBodyDigest(input.body))\n return refuse(\"digest\");\n if (!scopeStoreEnvelopeFresh(envelope.iat, input.nowSeconds, input.windowSeconds))\n return refuse(\"stale\");\n if (envelope.script !== input.script)\n return refuse(\"script\");\n let parsed;\n try {\n parsed = JSON.parse(input.body);\n } catch {\n return refuse(\"body\");\n }\n const field = (key) => typeof parsed === \"object\" && parsed !== null ? Reflect.get(parsed, key) : void 0;\n if (field(\"operation\") !== envelope.op || field(\"objectName\") !== envelope.name)\n return refuse(\"body\");\n return { ok: true, envelope };\n}\n\n// packages/control-plane-api/store-entry/verify.ts\nasync function storeCallVerdict(headers, body, env) {\n const script = env[STORE_SCRIPT_VAR];\n let keys;\n try {\n if (typeof script !== \"string\" || script.length === 0) throw new Error(\"no script name\");\n keys = scopeStorePublicKeysFrom({\n caller: String(env[STORE_CALLER_KEYS_VAR] ?? \"\"),\n platform: String(env[STORE_PLATFORM_KEYS_VAR] ?? \"\")\n });\n } catch {\n return { ok: false, status: 503, message: \"this store script is not configured\" };\n }\n const verdict = await verifyScopeStoreCall({\n envelope: headers.get(SCOPE_STORE_ENVELOPE_HEADER),\n signature: headers.get(SCOPE_STORE_SIGNATURE_HEADER),\n body,\n keys,\n script,\n nowSeconds: Math.floor(Date.now() / 1e3)\n });\n return verdict.ok ? verdict : { ok: false, status: 401, message: `unauthorized: ${verdict.refusal}` };\n}\n\n// packages/control-plane-api/store-entry/entry.ts\nvar Driver = driverModule[\"__SUBSTRAT_STORE_CLASS__\"];\nvar text = (status, message) => new Response(message, { status });\nfunction driverEnvOf(env) {\n return Object.fromEntries(Object.entries(env).filter(([name]) => !name.startsWith(STORE_RESERVED_BINDING_PREFIX)));\n}\nasync function verified(request, body, env) {\n const verdict = await storeCallVerdict(request.headers, body, env);\n return verdict.ok ? verdict.envelope : text(verdict.status, verdict.message);\n}\nvar SubstratStoreObject = class extends DurableObject {\n #driver;\n /** Constructed once per object instance, on first use, so the driver keeps its in-memory state. */\n #driverObject() {\n this.#driver ??= new Driver(this.ctx, driverEnvOf(this.env));\n return this.#driver;\n }\n async fetch(request) {\n const body = await request.text();\n const envelope = await verified(request, body, this.env);\n if (envelope instanceof Response) return envelope;\n const ns = this.env[STORE_OBJECT_BINDING];\n if (!this.ctx.id.equals(ns.idFromName(envelope.name))) return text(401, \"unauthorized: object\");\n return this.#driverObject().fetch(\n new Request(request.url, { method: \"POST\", headers: { \"content-type\": \"application/json\" }, body })\n );\n }\n /** An alarm is the object's own timer, which only the driver sets. It goes to the driver. */\n async alarm(alarmInfo) {\n const driver = this.#driverObject();\n if (typeof driver.alarm === \"function\") await driver.alarm(alarmInfo);\n }\n};\nvar entry_default = {\n async fetch(request, env) {\n const body = await request.text();\n const envelope = await verified(request, body, env);\n if (envelope instanceof Response) return envelope;\n const ns = env[STORE_OBJECT_BINDING];\n return ns.get(ns.idFromName(envelope.name)).fetch(\n new Request(request.url, { method: \"POST\", headers: request.headers, body })\n );\n }\n};\nexport {\n SubstratStoreObject,\n entry_default as default\n};\n";
|
|
51
|
+
/**
|
|
52
|
+
* The Python entry (#2290), uploaded as `substrat_store_entry.py`. The driver module's
|
|
53
|
+
* name (a Python module name, no `.py`) replaces `__SUBSTRAT_STORE_DRIVER__` and its class
|
|
54
|
+
* name replaces `__SUBSTRAT_STORE_CLASS__`, as in the JavaScript entry.
|
|
55
|
+
*/
|
|
56
|
+
export const STORE_ENTRY_PYTHON_SOURCE = "\"\"\"The platform's Python store entry (#2290), the twin of store-entry/entry.ts.\n\nUploaded as the store script's main module when the driver is Python. The driver's class is\nimported through the two placeholders below, which the upload replaces with the driver's module\nname and class name, as it does for the JavaScript entry. The driver is reached only through\nthis module, after the call is verified.\n\nThere is one verifier. Every call is checked by store-entry/verify.ts, bundled as an ES module\nthe upload ships beside this one and imported through the runtime's JavaScript bridge. It runs\ncontracts' verifyScopeStoreCall (RFC scope-stores section 10.3, checks 1-4) against the script\nname and both audiences' public keys this upload was given. This module parses no envelope, so\nit cannot disagree with the JavaScript entry about one. The default entrypoint addresses\nSubstratStoreObject by idFromName of the signed name. The object checks the call again, and\nrefuses unless its own id is the id of that name (check 5), before the driver is constructed: an\nobject is bound to one name by construction, with nothing stored that the driver could rewrite.\n\nThe body is read once, verified as that string, and that same string is what the driver receives.\nThe driver is constructed once per object instance with the object's ctx and an env with none of\nthe entry's SUBSTRAT_STORE* bindings. The upload sets disallow_importable_env and\ndisable_ctx_exports, and both apply to a Python script: `from workers import env` then reads an\nempty env, and ctx.exports is absent.\n\nWhat this is not: a boundary against a hostile driver, and it is weaker here than in JavaScript.\nPython has no private fields. The driver's module is imported by this one, before this module's\nbody has finished, and runs in the same interpreter: it can reach this module through\nsys.modules and replace its functions, and it can reach the entry's object, its full env and the\nstore namespace in it, through the garbage collector. The env strip and the cached driver keep a\ncareless driver away from the entry's bindings, not a hostile one. A driver written to interfere is\nstopped by the review of its release (D-63), not by this module.\n\"\"\"\n\nfrom workers import DurableObject, Request, Response, WorkerEntrypoint, import_from_javascript\nfrom js import Object\n\nfrom __SUBSTRAT_STORE_DRIVER__ import __SUBSTRAT_STORE_CLASS__ as StoreDriver\n\n# Each of these is a name in store-entry/names.ts; test/python-entry.test.ts holds them equal.\nVERIFIER_MODULE = \"./substrat-store-verify.js\"\nOBJECT_BINDING = \"SUBSTRAT_STORE\"\nRESERVED_BINDING_PREFIX = \"SUBSTRAT_STORE\"\n\n\ndef _raw_env(env):\n \"\"\"The JavaScript env the runtime handed over. The SDK wraps it, and has no public unwrap.\"\"\"\n return env._env\n\n\ndef _driver_env(env):\n \"\"\"The env the driver is constructed with: everything but the entry's own bindings.\"\"\"\n entries = [entry for entry in Object.entries(_raw_env(env)) if not str(entry[0]).startswith(RESERVED_BINDING_PREFIX)]\n return Object.fromEntries(entries)\n\n\nasync def _verified(request, body, env):\n \"\"\"The verified envelope's object name, or the response that refuses the call.\"\"\"\n verifier = import_from_javascript(VERIFIER_MODULE)\n verdict = await verifier.storeCallVerdict(request.js_object.headers, body, _raw_env(env))\n if not verdict.ok:\n return Response(verdict.message, status=verdict.status)\n return verdict.envelope.name\n\n\ndef _driver_request(url, body):\n return Request(url, method=\"POST\", headers={\"content-type\": \"application/json\"}, body=body)\n\n\nclass SubstratStoreObject(DurableObject):\n def __init__(self, ctx, env):\n super().__init__(ctx, env)\n self._driver = None\n\n def _driver_object(self):\n \"\"\"Constructed once per object instance, on first use, so the driver keeps its in-memory state.\"\"\"\n if self._driver is None:\n self._driver = StoreDriver(self.ctx, _driver_env(self.env))\n return self._driver\n\n async def fetch(self, request):\n body = await request.text()\n name = await _verified(request, body, self.env)\n if isinstance(name, Response):\n return name\n namespace = getattr(self.env, OBJECT_BINDING)\n if not self.ctx.id.equals(namespace.idFromName(name)):\n return Response(\"unauthorized: object\", status=401)\n return await self._driver_object().fetch(_driver_request(request.url, body))\n\n async def alarm(self, alarm_info=None):\n \"\"\"An alarm is the object's own timer, which only the driver sets. It goes to the driver.\"\"\"\n driver = self._driver_object()\n if hasattr(driver, \"alarm\"):\n await driver.alarm(alarm_info)\n\n\nclass Default(WorkerEntrypoint):\n async def fetch(self, request):\n body = await request.text()\n name = await _verified(request, body, self.env)\n if isinstance(name, Response):\n return name\n namespace = getattr(self.env, OBJECT_BINDING)\n stub = namespace.get(namespace.idFromName(name))\n return await stub.fetch(request.url, method=\"POST\", headers=request.js_object.headers, body=body)\n";
|
|
57
|
+
/**
|
|
58
|
+
* The verifier the Python entry imports, uploaded beside it as `substrat-store-verify.js`
|
|
59
|
+
* (an ES module). It is `packages/control-plane-api/store-entry/verify.ts`, the check the JavaScript entry runs — 9167 bytes.
|
|
60
|
+
*/
|
|
61
|
+
export const STORE_ENTRY_PYTHON_VERIFIER_SOURCE = "// packages/contracts/dist/scope-store-envelope.js\nvar SCOPE_STORE_ENVELOPE_WINDOW_SECONDS = 60;\nvar SCOPE_STORE_ENVELOPE_HEADER = \"x-substrat-store-envelope\";\nvar SCOPE_STORE_SIGNATURE_HEADER = \"x-substrat-store-signature\";\nvar RESERVED_STORE_OPERATIONS = [\n \"substrat.export\",\n \"substrat.import\",\n \"substrat.size\",\n \"substrat.wipe\",\n \"substrat.erase\"\n];\nvar ADMITTED = {\n \"1\": {\n platform: (op) => RESERVED_STORE_OPERATIONS.includes(op),\n caller: (op) => !op.startsWith(\"substrat.\")\n }\n};\nfunction scopeStoreEnvelopeAdmits(v, aud, op) {\n const byAudience = Object.hasOwn(ADMITTED, String(v)) ? ADMITTED[String(v)] : void 0;\n if (!byAudience || !Object.hasOwn(byAudience, aud))\n return false;\n return byAudience[aud](op);\n}\nfunction bytesToBase64Url(bytes) {\n let binary = \"\";\n for (const byte of bytes)\n binary += String.fromCharCode(byte);\n return btoa(binary).replaceAll(\"+\", \"-\").replaceAll(\"/\", \"_\").replaceAll(\"=\", \"\");\n}\nfunction base64UrlToBytes(value) {\n if (!/^[A-Za-z0-9_-]+$/.test(value))\n return void 0;\n const padded = value.replaceAll(\"-\", \"+\").replaceAll(\"_\", \"/\") + \"=\".repeat((4 - value.length % 4) % 4);\n try {\n const binary = atob(padded);\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++)\n bytes[i] = binary.charCodeAt(i);\n return bytes;\n } catch {\n return void 0;\n }\n}\nfunction copyBytes(bytes) {\n const copy = new ArrayBuffer(bytes.byteLength);\n new Uint8Array(copy).set(bytes);\n return copy;\n}\nfunction isCanonicalBase64Url(text, length) {\n const bytes = base64UrlToBytes(text);\n return bytes?.length === length && bytesToBase64Url(bytes) === text;\n}\nvar NONCE = /^[A-Za-z0-9_-]{22}$/;\nvar KID = /^[A-Za-z0-9_-]{11}$/;\nvar DIGEST = /^[0-9a-f]{64}$/;\nvar IAT = /^(0|[1-9]\\d{0,14})$/;\nfunction parseScopeStoreEnvelope(canonical) {\n const lines = canonical.split(\"\\n\");\n if (lines.length !== 9)\n return void 0;\n const [version, aud, kid, script, name, op, digest, iatText, nonce] = lines;\n if (version !== \"1\" || aud !== \"caller\" && aud !== \"platform\")\n return void 0;\n if (!script || !name || !op)\n return void 0;\n if (!KID.test(kid) || !DIGEST.test(digest) || !IAT.test(iatText) || !NONCE.test(nonce))\n return void 0;\n if (!isCanonicalBase64Url(kid, 8) || !isCanonicalBase64Url(nonce, 16))\n return void 0;\n return { v: 1, aud, kid, script, name, op, digest, iat: Number(iatText), nonce };\n}\nasync function scopeStoreBodyDigest(body) {\n const hash = await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(body));\n return [...new Uint8Array(hash)].map((byte) => byte.toString(16).padStart(2, \"0\")).join(\"\");\n}\nfunction scopeStoreEnvelopeFresh(iat, nowSeconds, windowSeconds = SCOPE_STORE_ENVELOPE_WINDOW_SECONDS) {\n return Math.abs(nowSeconds - iat) <= windowSeconds;\n}\nvar SMALL_ORDER_KEYS = [\n \"0000000000000000000000000000000000000000000000000000000000000000\",\n // y = 0, order 4\n \"0100000000000000000000000000000000000000000000000000000000000000\",\n // the identity, order 1\n \"26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc05\",\n // order 8\n \"c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac037a\",\n // order 8\n \"ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f\",\n // y = p - 1, order 2\n \"edffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f\",\n // y = p, a non-canonical 0\n \"eeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f\"\n // y = p + 1, a non-canonical identity\n];\nfunction isSmallOrderKey(bytes) {\n const unsigned = Uint8Array.from(bytes);\n unsigned[31] = unsigned[31] & 127;\n const hex = [...unsigned].map((byte) => byte.toString(16).padStart(2, \"0\")).join(\"\");\n return SMALL_ORDER_KEYS.includes(hex);\n}\nasync function scopeStoreKeyId(publicKey) {\n const bytes = base64UrlToBytes(publicKey);\n if (!bytes || bytes.length !== 32)\n throw new Error(\"a store public key is not 32 bytes of base64url\");\n if (isSmallOrderKey(bytes))\n throw new Error(\"a store public key is a small-order point\");\n const hash = await crypto.subtle.digest(\"SHA-256\", copyBytes(bytes));\n return bytesToBase64Url(new Uint8Array(hash).slice(0, 8));\n}\nfunction scopeStorePublicKeyProblem(key) {\n if (key.length === 0)\n return \"empty\";\n const bytes = base64UrlToBytes(key);\n if (bytes?.length !== 32)\n return \"length\";\n if (isSmallOrderKey(bytes))\n return \"small-order\";\n if (bytesToBase64Url(bytes) !== key)\n return \"non-canonical\";\n return void 0;\n}\nvar PUBLIC_KEY_PROBLEM = {\n empty: \"a store public key list is empty or has an empty entry\",\n length: \"a store public key is not 32 bytes of base64url\",\n \"small-order\": \"a store public key is a small-order point\",\n \"non-canonical\": \"a store public key is not canonical base64url\"\n};\nfunction parseScopeStorePublicKeys(text) {\n const keys = text.split(\",\").map((key) => key.trim());\n if (keys.some((key) => key.length === 0))\n throw new Error(PUBLIC_KEY_PROBLEM.empty);\n const seen = /* @__PURE__ */ new Set();\n for (const key of keys) {\n const problem = scopeStorePublicKeyProblem(key);\n if (problem)\n throw new Error(PUBLIC_KEY_PROBLEM[problem]);\n if (seen.has(key))\n throw new Error(\"a store public key is listed twice\");\n seen.add(key);\n }\n return keys;\n}\nfunction scopeStorePublicKeysFrom(config) {\n const caller = parseScopeStorePublicKeys(config.caller);\n const platform = parseScopeStorePublicKeys(config.platform);\n if (caller.some((key) => platform.includes(key))) {\n throw new Error(\"a store public key is listed for both audiences\");\n }\n return { caller, platform };\n}\nasync function verifyScopeStoreEnvelope(publicKey, canonical, signature) {\n const keyBytes = base64UrlToBytes(publicKey);\n const sigBytes = base64UrlToBytes(signature);\n if (!keyBytes || !sigBytes || sigBytes.length !== 64)\n return false;\n try {\n const key = await crypto.subtle.importKey(\"raw\", copyBytes(keyBytes), { name: \"Ed25519\" }, false, [\"verify\"]);\n return await crypto.subtle.verify(\"Ed25519\", key, copyBytes(sigBytes), new TextEncoder().encode(canonical));\n } catch {\n return false;\n }\n}\nasync function verifyScopeStoreCall(input) {\n const refuse = (refusal) => ({ ok: false, refusal });\n if (!input.envelope || !input.signature)\n return refuse(\"missing\");\n const bytes = base64UrlToBytes(input.envelope);\n if (!bytes)\n return refuse(\"malformed\");\n let canonical;\n try {\n canonical = new TextDecoder(\"utf-8\", { fatal: true }).decode(bytes);\n } catch {\n return refuse(\"malformed\");\n }\n const envelope = parseScopeStoreEnvelope(canonical);\n if (!envelope)\n return refuse(\"malformed\");\n if (!scopeStoreEnvelopeAdmits(envelope.v, envelope.aud, envelope.op))\n return refuse(\"audience\");\n const kidsOf = async (keys) => {\n const kids = /* @__PURE__ */ new Map();\n for (const key of keys) {\n try {\n kids.set(await scopeStoreKeyId(key), key);\n } catch {\n }\n }\n return kids;\n };\n const own = await kidsOf(input.keys[envelope.aud]);\n const other = await kidsOf(input.keys[envelope.aud === \"caller\" ? \"platform\" : \"caller\"]);\n if ([...own.keys()].some((kid) => other.has(kid)))\n return refuse(\"keys\");\n const publicKey = own.get(envelope.kid);\n if (!publicKey)\n return refuse(\"kid\");\n if (!await verifyScopeStoreEnvelope(publicKey, canonical, input.signature))\n return refuse(\"signature\");\n if (envelope.digest !== await scopeStoreBodyDigest(input.body))\n return refuse(\"digest\");\n if (!scopeStoreEnvelopeFresh(envelope.iat, input.nowSeconds, input.windowSeconds))\n return refuse(\"stale\");\n if (envelope.script !== input.script)\n return refuse(\"script\");\n let parsed;\n try {\n parsed = JSON.parse(input.body);\n } catch {\n return refuse(\"body\");\n }\n const field = (key) => typeof parsed === \"object\" && parsed !== null ? Reflect.get(parsed, key) : void 0;\n if (field(\"operation\") !== envelope.op || field(\"objectName\") !== envelope.name)\n return refuse(\"body\");\n return { ok: true, envelope };\n}\n\n// packages/control-plane-api/store-entry/names.ts\nvar STORE_SCRIPT_VAR = \"SUBSTRAT_STORE_SCRIPT\";\nvar STORE_CALLER_KEYS_VAR = \"SUBSTRAT_STORE_CALLER_KEYS\";\nvar STORE_PLATFORM_KEYS_VAR = \"SUBSTRAT_STORE_PLATFORM_KEYS\";\n\n// packages/control-plane-api/store-entry/verify.ts\nasync function storeCallVerdict(headers, body, env) {\n const script = env[STORE_SCRIPT_VAR];\n let keys;\n try {\n if (typeof script !== \"string\" || script.length === 0) throw new Error(\"no script name\");\n keys = scopeStorePublicKeysFrom({\n caller: String(env[STORE_CALLER_KEYS_VAR] ?? \"\"),\n platform: String(env[STORE_PLATFORM_KEYS_VAR] ?? \"\")\n });\n } catch {\n return { ok: false, status: 503, message: \"this store script is not configured\" };\n }\n const verdict = await verifyScopeStoreCall({\n envelope: headers.get(SCOPE_STORE_ENVELOPE_HEADER),\n signature: headers.get(SCOPE_STORE_SIGNATURE_HEADER),\n body,\n keys,\n script,\n nowSeconds: Math.floor(Date.now() / 1e3)\n });\n return verdict.ok ? verdict : { ok: false, status: 401, message: `unauthorized: ${verdict.refusal}` };\n}\nexport {\n storeCallVerdict\n};\n";
|
|
62
|
+
//# sourceMappingURL=store-entry.generated.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store-entry.generated.js","sourceRoot":"","sources":["../src/store-entry.generated.ts"],"names":[],"mappings":"AAAA,oOAAoO;AACpO,mEAAmE;AAEnE;;;;;GAKG;AAEH,oFAAoF;AACpF,MAAM,CAAC,MAAM,qBAAqB,GAAG,4BAA4B,CAAC;AAElE,2FAA2F;AAC3F,MAAM,CAAC,MAAM,uBAAuB,GAAG,0BAA0B,CAAC;AAElE,+EAA+E;AAC/E,MAAM,CAAC,MAAM,wBAAwB,GAAG,2BAA2B,CAAC;AAEpE,4GAA4G;AAC5G,MAAM,CAAC,MAAM,kBAAkB,GAAG,yBAAyB,CAAC;AAE5D;;;GAGG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,yBAAyB,CAAC;AAEnE,wDAAwD;AACxD,MAAM,CAAC,MAAM,oBAAoB,GAAG,gBAAgB,CAAC;AAErD,gGAAgG;AAChG,MAAM,CAAC,MAAM,kBAAkB,GAAG,qBAAqB,CAAC;AAExD,+DAA+D;AAC/D,MAAM,CAAC,MAAM,uBAAuB,GAAG,8BAA8B,CAAC;AAEtE;;;GAGG;AACH,MAAM,CAAC,MAAM,+BAA+B,GAAG,YAAY,CAAC;AAE5D,6GAA6G;AAC7G,MAAM,CAAC,MAAM,gCAAgC,GAAG,CAAC,gBAAgB,CAAC,CAAC;AAEnE;;;GAGG;AACH,MAAM,CAAC,MAAM,6BAA6B,GAAG,gBAAgB,CAAC;AAE9D,mGAAmG;AACnG,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,yBAAyB,EAAC,yBAAyB,EAAC,0BAA0B,CAAC,CAAC;AAEvH,8FAA8F;AAC9F,MAAM,CAAC,MAAM,gBAAgB,GAAG,uBAAuB,CAAC;AAExD;;;GAGG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,0BAA0B,CAAC;AAEhE,6CAA6C;AAC7C,MAAM,CAAC,MAAM,kBAAkB,GAAW,qnXAAqnX,CAAC;AAEhqX;;;;GAIG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAW,8rKAA8rK,CAAC;AAEhvK;;;GAGG;AACH,MAAM,CAAC,MAAM,kCAAkC,GAAW,q3SAAq3S,CAAC"}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { scopeStorePublicKeysFrom } from '@substrat-run/contracts';
|
|
2
|
+
import { STORE_CALLER_KEYS_VAR, STORE_CLASS_PLACEHOLDER, STORE_ENTRY_MODULE, STORE_ENTRY_PYTHON_MODULE, STORE_ENTRY_PYTHON_SOURCE, STORE_ENTRY_PYTHON_VERIFIER_SOURCE, STORE_ENTRY_SOURCE, STORE_DRIVER_PLACEHOLDER, STORE_OBJECT_BINDING, STORE_OBJECT_CLASS, STORE_PLATFORM_KEYS_VAR, STORE_PYTHON_COMPATIBILITY_DATE, STORE_PYTHON_COMPATIBILITY_FLAGS, STORE_RESERVED_BINDING_PREFIX, STORE_RESERVED_MODULES, STORE_SCRIPT_VAR, STORE_VERIFIER_MODULE, } from './store-entry.generated.js';
|
|
3
|
+
export { STORE_CLASS_PLACEHOLDER, STORE_ENTRY_MODULE, STORE_ENTRY_PYTHON_MODULE, STORE_ENTRY_PYTHON_SOURCE, STORE_ENTRY_PYTHON_VERIFIER_SOURCE, STORE_ENTRY_SOURCE, STORE_DRIVER_PLACEHOLDER, STORE_OBJECT_BINDING, STORE_OBJECT_CLASS, STORE_PYTHON_COMPATIBILITY_DATE, STORE_RESERVED_MODULES, STORE_VERIFIER_MODULE, } from './store-entry.generated.js';
|
|
4
|
+
/** A module name the uploader can write and the entry can import: relative, no `..`, JavaScript. */
|
|
5
|
+
const MODULE_NAME = /^(?!.*\.\.)[A-Za-z0-9_][A-Za-z0-9_./-]*\.m?js$/;
|
|
6
|
+
/**
|
|
7
|
+
* The compatibility flag that empties `import { env } from 'cloudflare:workers'`. Without it a
|
|
8
|
+
* driver module reads every binding of the script that way, the entry's own included, whatever
|
|
9
|
+
* env its class is constructed with. The entry itself reads only the env its handlers are given.
|
|
10
|
+
*/
|
|
11
|
+
export const STORE_DISALLOW_IMPORTABLE_ENV = 'disallow_importable_env';
|
|
12
|
+
/**
|
|
13
|
+
* The compatibility flag that keeps `ctx.exports` off. From compatibility date 2025-11-17 it is
|
|
14
|
+
* on by default, and the driver, which is constructed with the object's `ctx`, would get a
|
|
15
|
+
* loopback to the entry's own object class and default export: the namespace its env was
|
|
16
|
+
* stripped of, by another name. Every call through it would still meet the entry's checks, but
|
|
17
|
+
* the driver has no use for it, so the upload turns it off.
|
|
18
|
+
*/
|
|
19
|
+
export const STORE_DISABLE_CTX_EXPORTS = 'disable_ctx_exports';
|
|
20
|
+
/** The flags a driver release may not set, because each would undo one of the two above. */
|
|
21
|
+
const REFUSED_DRIVER_FLAGS = ['allow_importable_env', 'enable_ctx_exports'];
|
|
22
|
+
/** An inline config value. The shared binding schema does not carry `text`, so it is cast once here. */
|
|
23
|
+
const plainText = (name, text) => ({ type: 'plain_text', name, text });
|
|
24
|
+
const encode = (text) => new TextEncoder().encode(text);
|
|
25
|
+
/** A driver class name the entry can import: an identifier, and not the entry's own class. */
|
|
26
|
+
function assertDriverClass(className, identifier) {
|
|
27
|
+
if (!identifier.test(className))
|
|
28
|
+
throw new Error(`store driver class '${className}' is not an identifier`);
|
|
29
|
+
if (className === STORE_OBJECT_CLASS) {
|
|
30
|
+
throw new Error(`store driver class '${className}' is the entry's own object class`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The bindings every store entry, JavaScript or Python, is uploaded with: the object namespace,
|
|
35
|
+
* the script's own name and both audiences' public keys. Throws on an empty script name and on
|
|
36
|
+
* keys `scopeStorePublicKeysFrom` refuses, one key for both audiences included.
|
|
37
|
+
*/
|
|
38
|
+
function storeEntryBindings(options) {
|
|
39
|
+
if (options.script.length === 0 || options.script.includes('\n')) {
|
|
40
|
+
throw new Error('a store script name is empty or contains a newline');
|
|
41
|
+
}
|
|
42
|
+
const keys = scopeStorePublicKeysFrom({
|
|
43
|
+
caller: options.callerPublicKeys.join(','),
|
|
44
|
+
platform: options.platformPublicKeys.join(','),
|
|
45
|
+
});
|
|
46
|
+
return [
|
|
47
|
+
{ type: 'durable_object_namespace', name: STORE_OBJECT_BINDING, class_name: STORE_OBJECT_CLASS },
|
|
48
|
+
plainText(STORE_SCRIPT_VAR, options.script),
|
|
49
|
+
plainText(STORE_CALLER_KEYS_VAR, keys.caller.join(',')),
|
|
50
|
+
plainText(STORE_PLATFORM_KEYS_VAR, keys.platform.join(',')),
|
|
51
|
+
];
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The store script as uploaded (#2289). The entry becomes the main module and imports the
|
|
55
|
+
* driver module that was the bundle's entry. The object class is added to the SQLite migration,
|
|
56
|
+
* and the script name and both audiences' public keys are bound for the entry, which checks
|
|
57
|
+
* every call before the driver's class is constructed.
|
|
58
|
+
*
|
|
59
|
+
* Refused, before anything is built: a class name that is not an identifier or is the entry's
|
|
60
|
+
* own object class; a bundle entry that is not a plain module name; a bundle that ships a
|
|
61
|
+
* module under one of the platform entries' names (`STORE_RESERVED_MODULES`), declares the
|
|
62
|
+
* entry's class, or declares a binding in the entry's reserved `SUBSTRAT_STORE*` space (D-63);
|
|
63
|
+
* keys `scopeStorePublicKeysFrom` refuses, one key for both audiences included; an empty script
|
|
64
|
+
* name; a compatibility flag that would undo one of the upload's own (`allow_importable_env`,
|
|
65
|
+
* `enable_ctx_exports`).
|
|
66
|
+
*
|
|
67
|
+
* The upload carries `disallow_importable_env` and `disable_ctx_exports`, so the driver reaches
|
|
68
|
+
* the entry's bindings neither through `cloudflare:workers` nor through `ctx.exports`.
|
|
69
|
+
*/
|
|
70
|
+
export function withStoreEntry(bundle, options) {
|
|
71
|
+
assertDriverClass(options.className, /^[A-Za-z_$][A-Za-z0-9_$]*$/);
|
|
72
|
+
if (!MODULE_NAME.test(bundle.entry) || STORE_RESERVED_MODULES.includes(bundle.entry)) {
|
|
73
|
+
throw new Error(`store driver entry '${bundle.entry}' is not a module name the store entry can import`);
|
|
74
|
+
}
|
|
75
|
+
const reservedModule = bundle.modules.find((module) => STORE_RESERVED_MODULES.includes(module.name));
|
|
76
|
+
if (reservedModule) {
|
|
77
|
+
throw new Error(`a store driver release may not ship a module named '${reservedModule.name}'`);
|
|
78
|
+
}
|
|
79
|
+
if ((bundle.doClasses ?? []).includes(STORE_OBJECT_CLASS)) {
|
|
80
|
+
throw new Error(`a store driver release may not declare the class '${STORE_OBJECT_CLASS}'`);
|
|
81
|
+
}
|
|
82
|
+
const reserved = (bundle.bindings ?? []).find((binding) => binding.name.startsWith(STORE_RESERVED_BINDING_PREFIX));
|
|
83
|
+
if (reserved) {
|
|
84
|
+
throw new Error(`a store driver release may not declare the binding '${reserved.name}'`);
|
|
85
|
+
}
|
|
86
|
+
const refusedFlag = (bundle.compatibilityFlags ?? []).find((flag) => REFUSED_DRIVER_FLAGS.includes(flag));
|
|
87
|
+
if (refusedFlag) {
|
|
88
|
+
throw new Error(`a store driver release may not set '${refusedFlag}'`);
|
|
89
|
+
}
|
|
90
|
+
const entryBindings = storeEntryBindings(options);
|
|
91
|
+
const source = STORE_ENTRY_SOURCE.split(STORE_DRIVER_PLACEHOLDER)
|
|
92
|
+
.join(`./${bundle.entry}`)
|
|
93
|
+
.split(STORE_CLASS_PLACEHOLDER)
|
|
94
|
+
.join(options.className);
|
|
95
|
+
const driver = 'application/javascript+module';
|
|
96
|
+
const bindings = [...(bundle.bindings ?? []), ...entryBindings];
|
|
97
|
+
return {
|
|
98
|
+
...bundle,
|
|
99
|
+
entry: STORE_ENTRY_MODULE,
|
|
100
|
+
modules: [{ name: STORE_ENTRY_MODULE, content: encode(source), contentType: driver }, ...bundle.modules],
|
|
101
|
+
doClasses: [...(bundle.doClasses ?? []), STORE_OBJECT_CLASS],
|
|
102
|
+
compatibilityFlags: [
|
|
103
|
+
...new Set([...(bundle.compatibilityFlags ?? []), STORE_DISALLOW_IMPORTABLE_ENV, STORE_DISABLE_CTX_EXPORTS]),
|
|
104
|
+
],
|
|
105
|
+
bindings,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* A Python driver module's path: one top-level package that starts with `driver_`, then
|
|
110
|
+
* identifiers, ending in `.py`. The prefix keeps a driver's modules off every name the Python
|
|
111
|
+
* entry imports or the runtime provides (`workers`, `js`, `pyodide`, the entry, its verifier),
|
|
112
|
+
* and off `metadata`, the upload's own form part.
|
|
113
|
+
*/
|
|
114
|
+
const PYTHON_DRIVER_MODULE = /^(driver_[a-z0-9_]+)(?:\/[A-Za-z_][A-Za-z0-9_]*)*\/[A-Za-z_][A-Za-z0-9_]*\.py$/;
|
|
115
|
+
/**
|
|
116
|
+
* The store script as uploaded for a Python driver (#2290, #2210). The Python entry is the main
|
|
117
|
+
* module, with the driver's module and class in place of its two placeholders, and the verifier
|
|
118
|
+
* it imports is shipped beside it, so it runs the JavaScript entry's check. The driver's modules
|
|
119
|
+
* follow, byte for byte. The bindings and the two flags are the JavaScript entry's
|
|
120
|
+
* (`withStoreEntry`), with `python_workers`.
|
|
121
|
+
*
|
|
122
|
+
* Refused, before anything is built (D-63): a class name that is not a Python identifier or is
|
|
123
|
+
* the entry's own class; a module that is not `driver_<package>/…/<module>.py`; modules under more
|
|
124
|
+
* than one package; a module named twice; an entry that is not one of the modules; and
|
|
125
|
+
* whatever `withStoreEntry` refuses of the script name and the keys. A reserved module name
|
|
126
|
+
* cannot pass the path rule, and is refused again by name.
|
|
127
|
+
*/
|
|
128
|
+
export function withPythonStoreEntry(release, options) {
|
|
129
|
+
assertDriverClass(options.className, /^[A-Za-z_][A-Za-z0-9_]*$/);
|
|
130
|
+
assertPythonDriverModules(release);
|
|
131
|
+
const bindings = storeEntryBindings(options);
|
|
132
|
+
const driverModule = release.entry.replace(/\.py$/, '').replaceAll('/', '.');
|
|
133
|
+
const source = STORE_ENTRY_PYTHON_SOURCE.split(STORE_DRIVER_PLACEHOLDER)
|
|
134
|
+
.join(driverModule)
|
|
135
|
+
.split(STORE_CLASS_PLACEHOLDER)
|
|
136
|
+
.join(options.className);
|
|
137
|
+
return {
|
|
138
|
+
mainModule: STORE_ENTRY_PYTHON_MODULE,
|
|
139
|
+
modules: [
|
|
140
|
+
{ name: STORE_ENTRY_PYTHON_MODULE, content: encode(source), contentType: 'text/x-python' },
|
|
141
|
+
{
|
|
142
|
+
name: STORE_VERIFIER_MODULE,
|
|
143
|
+
content: encode(STORE_ENTRY_PYTHON_VERIFIER_SOURCE),
|
|
144
|
+
contentType: 'application/javascript+module',
|
|
145
|
+
},
|
|
146
|
+
...release.modules.map((module) => ({ name: module.name, content: module.content, contentType: 'text/x-python' })),
|
|
147
|
+
],
|
|
148
|
+
compatibilityDate: STORE_PYTHON_COMPATIBILITY_DATE,
|
|
149
|
+
compatibilityFlags: [...STORE_PYTHON_COMPATIBILITY_FLAGS, STORE_DISALLOW_IMPORTABLE_ENV, STORE_DISABLE_CTX_EXPORTS],
|
|
150
|
+
bindings,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* The module rule of a Python driver's release (D-63): every module is `driver_<package>/…/<module>.py`
|
|
155
|
+
* under one package, none twice, and the entry is one of them. The upload applies it when a
|
|
156
|
+
* release arrives (#2300), and `withPythonStoreEntry` again before every script it builds.
|
|
157
|
+
*/
|
|
158
|
+
export function assertPythonDriverModules(release) {
|
|
159
|
+
const packages = new Set();
|
|
160
|
+
const names = new Set();
|
|
161
|
+
for (const module of release.modules) {
|
|
162
|
+
const path = PYTHON_DRIVER_MODULE.exec(module.name);
|
|
163
|
+
if (!path || STORE_RESERVED_MODULES.includes(module.name)) {
|
|
164
|
+
throw new Error(`a store driver release may not ship a module named '${module.name}': a driver module is driver_<package>/…/<module>.py`);
|
|
165
|
+
}
|
|
166
|
+
if (names.has(module.name))
|
|
167
|
+
throw new Error(`a store driver release ships '${module.name}' twice`);
|
|
168
|
+
names.add(module.name);
|
|
169
|
+
packages.add(path[1]);
|
|
170
|
+
}
|
|
171
|
+
if (packages.size > 1) {
|
|
172
|
+
throw new Error(`a store driver release ships more than one package (${[...packages].sort().join(', ')})`);
|
|
173
|
+
}
|
|
174
|
+
if (!names.has(release.entry)) {
|
|
175
|
+
throw new Error(`store driver entry '${release.entry}' is not one of the release's modules`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
//# sourceMappingURL=store-entry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store-entry.js","sourceRoot":"","sources":["../src/store-entry.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The platform's rebuild of every store script (#2309, D-63).
|
|
3
|
+
*
|
|
4
|
+
* A store script is built from a driver's release and from what the platform adds to every script:
|
|
5
|
+
* the entry, its verifier, the compatibility flags and both audiences' public key lists. A push, a
|
|
6
|
+
* serve or a rollout rebuilds the scripts it reaches. This walk rebuilds the rest when the platform's
|
|
7
|
+
* half changes, so a key dropped from a list (a revocation) or a verifier fix reaches every script
|
|
8
|
+
* whether or not its vertical is pushed again.
|
|
9
|
+
*
|
|
10
|
+
* What the platform builds from is one digest, `storePlatformFingerprint`. Each driver has one
|
|
11
|
+
* rebuild row in the directory: the fingerprint it converges on, the last vertical it walked, and
|
|
12
|
+
* the verticals it could not rebuild. A pass:
|
|
13
|
+
*
|
|
14
|
+
* - does nothing for a driver whose row is `done` at the current fingerprint (one directory read);
|
|
15
|
+
* - takes the row's lease (so two passes never walk one driver), then a **shared** hold on the
|
|
16
|
+
* driver, the hold a push takes. A rollout or rollback holds the driver exclusively, so a pass
|
|
17
|
+
* that meets one stops and the next pass continues. Under the shared hold no rollout can move a
|
|
18
|
+
* script between this walk's read of the release a script runs and its upload, which would
|
|
19
|
+
* upload the old release back over the new one;
|
|
20
|
+
* - rebuilds each script at the release it already runs (no `to`), so a rollout that paused part
|
|
21
|
+
* way, or a rollback that has not finished, keeps every script where it is. A script that does not
|
|
22
|
+
* exist is left alone, and one already built from the current fingerprint is not uploaded again;
|
|
23
|
+
* - walks the driver's canary script first, then each vertical that declared the driver, in slug
|
|
24
|
+
* order, recording the cursor after each, then retries the verticals that failed. At most
|
|
25
|
+
* `budget` scripts per pass, across drivers.
|
|
26
|
+
*
|
|
27
|
+
* The cursor is a slug, and a vertical behind it is not walked again. That is sound because every
|
|
28
|
+
* upload builds from the platform's current inputs: a vertical pushed or served after the walk
|
|
29
|
+
* passed its slug was built from the fingerprint the walk converges on, and one ahead of the cursor
|
|
30
|
+
* is reached and found current (no upload).
|
|
31
|
+
*/
|
|
32
|
+
import type { PlatformActorId, ScopeStoreNeed, StoreDriverRegistration } from '@substrat-run/contracts';
|
|
33
|
+
import { type HostAdmin, type StoreDriverRebuildRecord } from '@substrat-run/kernel';
|
|
34
|
+
import { type StoreReleaseBytes } from './store-releases.js';
|
|
35
|
+
import type { ScopeStoreEnsurerOptions } from './store-script.js';
|
|
36
|
+
type LoadRelease = ScopeStoreEnsurerOptions['loadRelease'];
|
|
37
|
+
/**
|
|
38
|
+
* The subrequests a pass of the walk may spend by default. A Worker invocation gets 1000 on the
|
|
39
|
+
* paid plan; the scheduled pass shares its invocation with every other phase, so it takes half.
|
|
40
|
+
* A staff request runs alone and may take {@link STORE_REBUILD_REQUEST_SUBREQUESTS}.
|
|
41
|
+
*/
|
|
42
|
+
export declare const STORE_REBUILD_PASS_SUBREQUESTS = 500;
|
|
43
|
+
export declare const STORE_REBUILD_REQUEST_SUBREQUESTS = 900;
|
|
44
|
+
/** The scripts a pass with `subrequests` to spend may walk: {@link STORE_REBUILD_SUBREQUESTS_PER_SCRIPT} each. */
|
|
45
|
+
export declare const storeRebuildBudget: (subrequests: number) => number;
|
|
46
|
+
/**
|
|
47
|
+
* How long a `done` rebuild stands before a scheduled pass walks it again. A walk that is done
|
|
48
|
+
* proves every script was current when it read it; a control plane still serving the older inputs
|
|
49
|
+
* (a deploy rolling out gradually) can upload a script after that. Walking it again reads each
|
|
50
|
+
* script's settings and uploads only a script that is not current.
|
|
51
|
+
*/
|
|
52
|
+
export declare const STORE_REBUILD_REVERIFY_MS: number;
|
|
53
|
+
/**
|
|
54
|
+
* How long after a fingerprint first appears a walk is not yet proof (#2309 review). While a
|
|
55
|
+
* control-plane deploy rolls out, the version still building from the older inputs can push a
|
|
56
|
+
* script after the walk read it. Only a walk that STARTED this long after its fingerprint was first
|
|
57
|
+
* walked towards proves every script current: a walk that spans passes reads its first scripts when
|
|
58
|
+
* it starts, so when it finishes says nothing about them. A done walk that started inside the window
|
|
59
|
+
* reads as settling, and the scheduled pass walks it again once the window has passed. A deploy that
|
|
60
|
+
* takes longer to roll out is followed by `store-driver rebuild`, or by a person's attestation that
|
|
61
|
+
* it has finished. The default; the control plane reads `STORE_REBUILD_SETTLE_MINUTES`.
|
|
62
|
+
*/
|
|
63
|
+
export declare const STORE_REBUILD_SETTLE_MS: number;
|
|
64
|
+
interface StoreFleetRebuildDeps {
|
|
65
|
+
admin: HostAdmin;
|
|
66
|
+
actor: PlatformActorId;
|
|
67
|
+
drivers: readonly StoreDriverRegistration[];
|
|
68
|
+
/** What the platform builds every script from now (`storePlatformFingerprint`). */
|
|
69
|
+
fingerprint: string;
|
|
70
|
+
bytes: StoreReleaseBytes;
|
|
71
|
+
/** Rebuild one vertical's scripts of a driver at the release each runs, existing scripts only. */
|
|
72
|
+
ensure: (input: {
|
|
73
|
+
vertical: string;
|
|
74
|
+
stores: readonly ScopeStoreNeed[];
|
|
75
|
+
existingOnly: true;
|
|
76
|
+
loadRelease: LoadRelease;
|
|
77
|
+
}) => Promise<void | {
|
|
78
|
+
uploaded: number;
|
|
79
|
+
}>;
|
|
80
|
+
/** Rebuild the driver's canary script the same way. */
|
|
81
|
+
ensureCanary: (input: {
|
|
82
|
+
driver: string;
|
|
83
|
+
existingOnly: true;
|
|
84
|
+
loadRelease: LoadRelease;
|
|
85
|
+
}) => Promise<{
|
|
86
|
+
uploaded: boolean;
|
|
87
|
+
}>;
|
|
88
|
+
/** The most scripts this pass walks, across drivers. */
|
|
89
|
+
budget: number;
|
|
90
|
+
/**
|
|
91
|
+
* When a `done` rebuild is walked again: `always` (a person asked for this pass), or once it has
|
|
92
|
+
* stood {@link STORE_REBUILD_REVERIFY_MS} (the scheduled pass).
|
|
93
|
+
*/
|
|
94
|
+
reverify: 'always' | 'when-due';
|
|
95
|
+
/** For a test: the clock the reverification is judged on. */
|
|
96
|
+
now?: () => number;
|
|
97
|
+
/** The settle window; {@link STORE_REBUILD_SETTLE_MS} by default. */
|
|
98
|
+
settleMs?: number;
|
|
99
|
+
/**
|
|
100
|
+
* A person's attestation that no control-plane deploy is rolling out: the walk starts over from
|
|
101
|
+
* its first script, and once done proves every script current whatever the settle window says.
|
|
102
|
+
* The actor attesting. Only for one driver at a time, from a staff request.
|
|
103
|
+
*/
|
|
104
|
+
attestedBy?: string;
|
|
105
|
+
}
|
|
106
|
+
export interface StoreDriverRebuildPass {
|
|
107
|
+
driver: string;
|
|
108
|
+
/**
|
|
109
|
+
* `current`: done at this fingerprint, nothing walked. `busy`: another pass holds the lease.
|
|
110
|
+
* `held`: a rollout or rollback holds the driver; the next pass continues. `paused`: the budget ran
|
|
111
|
+
* out with scripts left. `done`: every script walked, none failed. `failing`: every script walked,
|
|
112
|
+
* some failed; they are retried on the next pass. `error`: the pass could not walk the driver.
|
|
113
|
+
*/
|
|
114
|
+
outcome: 'current' | 'busy' | 'held' | 'paused' | 'done' | 'failing' | 'error';
|
|
115
|
+
/** The scripts walked this pass: a vertical's slug, or the canary's marker. */
|
|
116
|
+
walked: string[];
|
|
117
|
+
/** The ones of those that were uploaded. */
|
|
118
|
+
uploaded: string[];
|
|
119
|
+
rebuild?: StoreDriverRebuildRecord;
|
|
120
|
+
error?: string;
|
|
121
|
+
}
|
|
122
|
+
/** One pass of the walk over every driver, within `budget` scripts. */
|
|
123
|
+
export declare function runStoreFleetRebuild(deps: StoreFleetRebuildDeps): Promise<StoreDriverRebuildPass[]>;
|
|
124
|
+
/**
|
|
125
|
+
* Whether every script of a driver is built from what the platform builds from now, and which may
|
|
126
|
+
* not be: while the rebuild is towards another fingerprint (or none ran yet), the canary and every
|
|
127
|
+
* vertical that declared the driver; while it runs, the ones ahead of its cursor and the ones that
|
|
128
|
+
* failed. A push or serve may have rebuilt some of them since, and the walk then finds them
|
|
129
|
+
* current, so `pending` may over-count, never under-count. A walk that is done but not settled (see
|
|
130
|
+
* {@link STORE_REBUILD_SETTLE_MS}) is not current: `settling` says when it is walked again. `nextWalk`
|
|
131
|
+
* is when the scheduled pass walks a done rebuild again. A walk under way at lists an earlier walk
|
|
132
|
+
* already proved is `current` and `reverifying` until a script it reaches fails.
|
|
133
|
+
*/
|
|
134
|
+
export declare function storeRebuildStatus(rebuild: StoreDriverRebuildRecord | undefined, fingerprint: string, declared: readonly string[], settleMs?: number): {
|
|
135
|
+
current: boolean;
|
|
136
|
+
pending: string[];
|
|
137
|
+
settling?: string;
|
|
138
|
+
nextWalk?: string;
|
|
139
|
+
reverifying?: boolean;
|
|
140
|
+
};
|
|
141
|
+
export {};
|
|
142
|
+
//# sourceMappingURL=store-rebuild.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store-rebuild.d.ts","sourceRoot":"","sources":["../src/store-rebuild.ts"],"names":[],"mappings":"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"}
|