@lostgradient/weft 0.17.0 → 0.19.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/README.md +3 -1
- package/dist/cli/conformance.js +14 -18
- package/dist/cli/generated/operation-catalog.snapshot.json +331 -16
- package/dist/cli/generated/operation-client.generated.d.ts +39 -6
- package/dist/cli/generated/operation-client.generated.js +6 -0
- package/dist/core/context/child-workflow-pipe.d.ts +1 -1
- package/dist/core/context/child-workflow-pipe.js +1 -1
- package/dist/core/engine/construction.d.ts +2 -0
- package/dist/core/engine/construction.js +3 -0
- package/dist/core/engine/index.js +1 -0
- package/dist/core/engine/listing.d.ts +1 -0
- package/dist/core/engine/listing.js +6 -4
- package/dist/core/weft-error.d.ts +1 -1
- package/dist/core/weft-error.js +2 -1
- package/dist/core/worker-execution-dispatcher.d.ts +8 -0
- package/dist/core/worker-execution-dispatcher.js +4 -0
- package/dist/core/worker-execution-strategy-options.d.ts +16 -0
- package/dist/core/worker-execution-strategy.js +71 -89
- package/dist/core/worker-fault-handling.d.ts +45 -0
- package/dist/core/worker-fault-handling.js +88 -0
- package/dist/core/worker-protocol.d.ts +18 -1
- package/dist/core/worker-protocol.js +1 -1
- package/dist/core/worker-realm-readiness.d.ts +78 -0
- package/dist/core/worker-realm-readiness.js +127 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +28 -1
- package/dist/json-schema.js +1 -1
- package/dist/runtime/portable.d.ts +14 -0
- package/dist/runtime/portable.js +10 -0
- package/dist/server/authorization-scope.d.ts +32 -2
- package/dist/server/index.d.ts +73 -8
- package/dist/server/index.js +11 -3
- package/dist/server/operations/get-principal.d.ts +75 -0
- package/dist/server/operations/get-principal.js +41 -0
- package/dist/server/operations/get-task-diagnostics.d.ts +43 -18
- package/dist/server/operations/get-task-diagnostics.js +87 -126
- package/dist/server/operations/get-worker-diagnostics.d.ts +177 -0
- package/dist/server/operations/get-worker-diagnostics.js +117 -0
- package/dist/server/operations/list-worker-registration-rejections.d.ts +79 -0
- package/dist/server/operations/list-worker-registration-rejections.js +60 -0
- package/dist/server/operations/list-workers.js +1 -3
- package/dist/server/operations/static-registrations.d.ts +28 -0
- package/dist/server/operations/static-registrations.js +248 -0
- package/dist/server/rest-bindings.d.ts +5 -0
- package/dist/server/rest-bindings.js +34 -313
- package/dist/server/runtime/authentication-bridge.js +6 -14
- package/dist/server/runtime/context.d.ts +23 -0
- package/dist/server/runtime/task-dispatch.d.ts +7 -1
- package/dist/server/runtime/task-dispatch.js +126 -55
- package/dist/server/runtime/task-ledger-completion.d.ts +68 -0
- package/dist/server/runtime/task-ledger-completion.js +72 -0
- package/dist/server/runtime/task-ledger-recovery.d.ts +55 -0
- package/dist/server/runtime/task-ledger-recovery.js +73 -0
- package/dist/server/runtime/task-ledger-runtime.d.ts +64 -0
- package/dist/server/runtime/task-ledger-runtime.js +34 -0
- package/dist/server/runtime/task-metrics.d.ts +6 -4
- package/dist/server/runtime/task-polling.d.ts +14 -12
- package/dist/server/runtime/task-polling.js +106 -82
- package/dist/server/runtime/task-reconciliation.d.ts +35 -8
- package/dist/server/runtime/task-reconciliation.js +75 -74
- package/dist/server/runtime/task-result-resolution.d.ts +2 -10
- package/dist/server/runtime/task-result-resolution.js +0 -67
- package/dist/server/runtime/task-result-view.d.ts +82 -0
- package/dist/server/runtime/task-result-view.js +48 -0
- package/dist/server/runtime/websocket-worker-messaging.d.ts +16 -0
- package/dist/server/runtime/websocket-worker-messaging.js +32 -0
- package/dist/server/runtime/websocket-worker-registration.d.ts +41 -0
- package/dist/server/runtime/websocket-worker-registration.js +134 -0
- package/dist/server/runtime/websocket-worker.d.ts +0 -1
- package/dist/server/runtime/websocket-worker.js +64 -116
- package/dist/server/serve-internals.d.ts +9 -6
- package/dist/server/serve-internals.js +14 -40
- package/dist/server/task-ledger-codec.d.ts +40 -0
- package/dist/server/task-ledger-codec.js +195 -0
- package/dist/server/task-ledger-keys.d.ts +13 -0
- package/dist/server/task-ledger-keys.js +5 -0
- package/dist/server/task-ledger-limits.d.ts +34 -0
- package/dist/server/task-ledger-limits.js +5 -0
- package/dist/server/task-ledger-transition-helpers.d.ts +38 -0
- package/dist/server/task-ledger-transition-helpers.js +42 -0
- package/dist/server/task-ledger-transitions-cancellation.d.ts +31 -0
- package/dist/server/task-ledger-transitions-cancellation.js +73 -0
- package/dist/server/task-ledger-transitions.d.ts +129 -0
- package/dist/server/task-ledger-transitions.js +162 -0
- package/dist/server/task-ledger-types.d.ts +220 -0
- package/dist/server/task-ledger-types.js +1 -0
- package/dist/server/task-ledger.d.ts +17 -0
- package/dist/server/task-ledger.js +24 -0
- package/dist/server/task-state.d.ts +25 -152
- package/dist/server/task-state.js +0 -188
- package/dist/server/worker-admission-policy.d.ts +70 -0
- package/dist/server/worker-admission-policy.js +0 -0
- package/dist/storage/compressed-storage.js +1 -1
- package/dist/storage/index.d.ts +0 -2
- package/dist/storage/interface.d.ts +0 -2
- package/dist/storage/interface.js +1 -1
- package/dist/storage/lmdb.js +1 -1
- package/dist/storage/memory.js +1 -1
- package/dist/storage/neon.js +1 -1
- package/dist/storage/postgres.js +1 -1
- package/dist/storage/resolve.js +1 -1
- package/dist/storage/scoped-storage.js +1 -1
- package/dist/storage/testing.js +1 -1
- package/dist/storage/turso.js +1 -1
- package/dist/storage/web-extension.js +2 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/web-extension.js +1 -1
- package/dist/worker/index.d.ts +2 -0
- package/dist/worker/index.js +30 -4
- package/dist/worker/manifest/canonical-json.d.ts +20 -0
- package/dist/worker/manifest/canonical-json.js +13 -0
- package/dist/worker/manifest/capabilities.d.ts +24 -0
- package/dist/worker/manifest/capabilities.js +74 -0
- package/dist/worker/manifest/content-digest.d.ts +20 -0
- package/dist/worker/manifest/content-digest.js +13 -0
- package/dist/worker/manifest/declared-shape-digest.d.ts +14 -0
- package/dist/worker/manifest/declared-shape-digest.js +4 -0
- package/dist/worker/manifest/digest.d.ts +82 -0
- package/dist/worker/manifest/digest.js +9 -0
- package/dist/worker/manifest/execution-identity.d.ts +92 -0
- package/dist/worker/manifest/execution-identity.js +36 -0
- package/dist/worker/manifest/failure.d.ts +59 -0
- package/dist/worker/manifest/failure.js +4 -0
- package/dist/worker/manifest/index.d.ts +22 -0
- package/dist/worker/manifest/index.js +23 -0
- package/dist/worker/manifest/internal-realm.d.ts +50 -0
- package/dist/worker/manifest/internal-realm.js +39 -0
- package/dist/worker/manifest/is-record.d.ts +12 -0
- package/dist/worker/manifest/is-record.js +6 -0
- package/dist/worker/manifest/json-scan.d.ts +26 -0
- package/dist/worker/manifest/json-scan.js +88 -0
- package/dist/worker/manifest/limits.d.ts +138 -0
- package/dist/worker/manifest/limits.js +1 -0
- package/dist/worker/manifest/normalize.d.ts +63 -0
- package/dist/worker/manifest/normalize.js +73 -0
- package/dist/worker/manifest/parse-json.d.ts +24 -0
- package/dist/worker/manifest/parse-json.js +15 -0
- package/dist/worker/manifest/parse.d.ts +86 -0
- package/dist/worker/manifest/parse.js +188 -0
- package/dist/worker/manifest/registry-contract-builder.d.ts +120 -0
- package/dist/worker/manifest/registry-contract-builder.js +65 -0
- package/dist/worker/manifest/types.d.ts +263 -0
- package/dist/worker/manifest/types.js +1 -0
- package/dist/worker/manifest/utf8.d.ts +13 -0
- package/dist/worker/manifest/utf8.js +7 -0
- package/dist/worker/options.d.ts +55 -8
- package/dist/worker/options.js +65 -9
- package/dist/worker/protocol-internals.d.ts +1 -2
- package/dist/worker/protocol-internals.js +0 -5
- package/dist/worker/protocol-messages.d.ts +33 -17
- package/dist/worker/protocol-schemas.d.ts +36 -74
- package/dist/worker/protocol-schemas.js +25 -18
- package/dist/worker/protocol-version.d.ts +18 -14
- package/dist/worker/protocol-version.js +1 -1
- package/dist/worker/protocol.js +1 -1
- package/dist/worker/registry/deployment-consistency.d.ts +78 -0
- package/dist/worker/registry/deployment-consistency.js +27 -0
- package/dist/worker/registry/drain.d.ts +14 -0
- package/dist/worker/registry/drain.js +24 -0
- package/dist/worker/registry/rejections.d.ts +31 -0
- package/dist/worker/registry/rejections.js +10 -0
- package/dist/worker/registry/summary.d.ts +0 -3
- package/dist/worker/registry/summary.js +2 -5
- package/dist/worker/registry/types.d.ts +10 -3
- package/dist/worker/registry.d.ts +47 -2
- package/dist/worker/registry.js +45 -32
- package/dist/workers/workflow-worker-entry.d.ts +12 -7
- package/dist/workers/workflow-worker-entry.js +9 -2
- package/package.json +3 -3
- package/dist/server/task-resolved-record.d.ts +0 -12
- package/dist/server/task-resolved-record.js +0 -35
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { DEFAULT_WORKFLOW_VERSION } from "../../core/versioning.js";
|
|
2
|
+
import { WORKER_PROTOCOL_VERSION } from "../../core/worker-protocol.js";
|
|
3
|
+
import { VERSION } from "../../version.js";
|
|
4
|
+
import { declaredShapeDigest } from "./declared-shape-digest.js";
|
|
5
|
+
import {
|
|
6
|
+
WORKER_MANIFEST_VERSION
|
|
7
|
+
} from "./index.js";
|
|
8
|
+
export const INTERNAL_WORKER_REALM_DEPLOYMENT_NAME = "internal-worker-realm", INTERNAL_WORKER_REALM_RUNTIME_NAME = "internal-worker-realm";
|
|
9
|
+
export function buildDeclaredWorkflowContract(workflowType) {
|
|
10
|
+
const digest = declaredShapeDigest(workflowType);
|
|
11
|
+
return {
|
|
12
|
+
workflowVersion: DEFAULT_WORKFLOW_VERSION,
|
|
13
|
+
workflowRevision: digest,
|
|
14
|
+
contractHash: digest,
|
|
15
|
+
activities: {}
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export function declaredWorkflowContractsMatch(reported, expected) {
|
|
19
|
+
return reported.workflowVersion === expected.workflowVersion && reported.workflowRevision === expected.workflowRevision && reported.contractHash === expected.contractHash && Object.keys(reported.activities).length === 0 && Object.keys(expected.activities).length === 0;
|
|
20
|
+
}
|
|
21
|
+
export function buildInternalRealmManifest(workflowTypes) {
|
|
22
|
+
const sortedTypes = [...workflowTypes].toSorted(), workflows = {};
|
|
23
|
+
for (const workflowType of sortedTypes)
|
|
24
|
+
workflows[workflowType] = buildDeclaredWorkflowContract(workflowType);
|
|
25
|
+
const artifactDigest = declaredShapeDigest(sortedTypes.join(","));
|
|
26
|
+
return {
|
|
27
|
+
manifestVersion: WORKER_MANIFEST_VERSION,
|
|
28
|
+
protocolVersion: WORKER_PROTOCOL_VERSION,
|
|
29
|
+
sdkVersion: VERSION,
|
|
30
|
+
runtime: { name: INTERNAL_WORKER_REALM_RUNTIME_NAME, version: VERSION },
|
|
31
|
+
deployment: {
|
|
32
|
+
name: INTERNAL_WORKER_REALM_DEPLOYMENT_NAME,
|
|
33
|
+
buildId: artifactDigest,
|
|
34
|
+
artifactDigest
|
|
35
|
+
},
|
|
36
|
+
workflows,
|
|
37
|
+
capabilities: {}
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared plain-object predicate for untrusted manifest input.
|
|
3
|
+
*
|
|
4
|
+
* `typeof value === 'object'` alone accepts `Date`, `Map`, `RegExp`, and other
|
|
5
|
+
* exotic-prototype instances — `Object.keys()` on those returns an empty
|
|
6
|
+
* array, so a hostile or malformed manifest field silently parses as an
|
|
7
|
+
* empty collection instead of being rejected. Only a plain object literal or
|
|
8
|
+
* a null-prototype object is a JSON object.
|
|
9
|
+
*
|
|
10
|
+
* @module worker/manifest/is-record
|
|
11
|
+
*/
|
|
12
|
+
export declare function isRecord(value: unknown): value is Readonly<Record<string, unknown>>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Duplicate-key detection for JSON text.
|
|
3
|
+
*
|
|
4
|
+
* `JSON.parse` resolves `{"a":1,"a":2}` to `{a: 2}` silently. For a manifest
|
|
5
|
+
* that is unacceptable: two peers reading the same bytes could disagree about
|
|
6
|
+
* which artifact digest a worker claimed, and a digest computed after parsing
|
|
7
|
+
* would attest to only one of them. Duplicate keys are therefore rejected
|
|
8
|
+
* while the text is still text.
|
|
9
|
+
*
|
|
10
|
+
* The scanner is a structural pass, not a full parser — it locates strings and
|
|
11
|
+
* object boundaries well enough to collect key names per object, and leaves
|
|
12
|
+
* every other validity question to `JSON.parse`, which runs afterwards.
|
|
13
|
+
*
|
|
14
|
+
* @module worker/manifest/json-scan
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Report the first duplicate key in JSON text, or `undefined` when every
|
|
18
|
+
* object has unique keys.
|
|
19
|
+
*
|
|
20
|
+
* Malformed text yields `undefined` rather than a diagnosis: this pass only
|
|
21
|
+
* answers the duplicate-key question, and `JSON.parse` gives a better message
|
|
22
|
+
* for everything else. Text nested deeper than
|
|
23
|
+
* {@link MAX_JSON_SCAN_NESTING_DEPTH} is treated the same way — the scan
|
|
24
|
+
* bails before the open-container stack can grow without bound.
|
|
25
|
+
*/
|
|
26
|
+
export declare function findDuplicateJsonKey(text: string): string | undefined;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { MAX_JSON_SCAN_NESTING_DEPTH } from "./limits.js";
|
|
2
|
+
function readString(text, start) {
|
|
3
|
+
let value = "", index = start + 1;
|
|
4
|
+
while (index < text.length) {
|
|
5
|
+
const char = text[index];
|
|
6
|
+
if (char === "\\") {
|
|
7
|
+
const escaped = text[index + 1];
|
|
8
|
+
if (escaped === void 0)
|
|
9
|
+
return;
|
|
10
|
+
if (escaped === "u") {
|
|
11
|
+
const hex = text.slice(index + 2, index + 6);
|
|
12
|
+
if (!/^[0-9a-fA-F]{4}$/.test(hex))
|
|
13
|
+
return;
|
|
14
|
+
const code = Number.parseInt(hex, 16);
|
|
15
|
+
value += String.fromCharCode(code);
|
|
16
|
+
index += 6;
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
const decoded = {
|
|
20
|
+
'"': '"',
|
|
21
|
+
"\\": "\\",
|
|
22
|
+
"/": "/",
|
|
23
|
+
b: "\b",
|
|
24
|
+
f: "\f",
|
|
25
|
+
n: `
|
|
26
|
+
`,
|
|
27
|
+
r: "\r",
|
|
28
|
+
t: "\t"
|
|
29
|
+
}[escaped];
|
|
30
|
+
if (decoded === void 0)
|
|
31
|
+
return;
|
|
32
|
+
value += decoded;
|
|
33
|
+
index += 2;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (char === '"')
|
|
37
|
+
return { value, next: index + 1 };
|
|
38
|
+
value += char;
|
|
39
|
+
index += 1;
|
|
40
|
+
}
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
export function findDuplicateJsonKey(text) {
|
|
44
|
+
const stack = [];
|
|
45
|
+
let expectKey = !1, index = 0;
|
|
46
|
+
while (index < text.length) {
|
|
47
|
+
const char = text[index];
|
|
48
|
+
if (char === '"') {
|
|
49
|
+
const parsed = readString(text, index);
|
|
50
|
+
if (parsed === void 0)
|
|
51
|
+
return;
|
|
52
|
+
if (expectKey) {
|
|
53
|
+
const keys = stack[stack.length - 1];
|
|
54
|
+
if (keys !== void 0) {
|
|
55
|
+
if (keys.has(parsed.value))
|
|
56
|
+
return parsed.value;
|
|
57
|
+
keys.add(parsed.value);
|
|
58
|
+
}
|
|
59
|
+
expectKey = !1;
|
|
60
|
+
}
|
|
61
|
+
index = parsed.next;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if ((char === "{" || char === "[") && stack.length >= MAX_JSON_SCAN_NESTING_DEPTH)
|
|
65
|
+
return;
|
|
66
|
+
expectKey = applyStructuralCharacter(char, stack, expectKey);
|
|
67
|
+
index += 1;
|
|
68
|
+
}
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
function applyStructuralCharacter(char, stack, expectKey) {
|
|
72
|
+
switch (char) {
|
|
73
|
+
case "{":
|
|
74
|
+
stack.push(new Set);
|
|
75
|
+
return !0;
|
|
76
|
+
case "[":
|
|
77
|
+
stack.push(void 0);
|
|
78
|
+
return !1;
|
|
79
|
+
case "}":
|
|
80
|
+
case "]":
|
|
81
|
+
stack.pop();
|
|
82
|
+
return !1;
|
|
83
|
+
case ",":
|
|
84
|
+
return stack[stack.length - 1] !== void 0;
|
|
85
|
+
default:
|
|
86
|
+
return expectKey;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Semantic bounds applied to a worker manifest before it is normalized,
|
|
3
|
+
* digested, or admitted to a registry.
|
|
4
|
+
*
|
|
5
|
+
* Lives in a leaf module so the type declarations, the hostile-input parser,
|
|
6
|
+
* and the canonical serializer can all depend on the limits without forming an
|
|
7
|
+
* import cycle — the same placement rationale as `worker/protocol-version`.
|
|
8
|
+
*
|
|
9
|
+
* These limits are deliberately separate from the transport's raw frame
|
|
10
|
+
* ceiling. A WebSocket frame guard bounds allocation; these bound the
|
|
11
|
+
* *semantic* shape a worker may assert, which is what keeps registry
|
|
12
|
+
* insertion, diagnostics, and key construction predictable.
|
|
13
|
+
*
|
|
14
|
+
* @module worker/manifest/limits
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Maximum UTF-8 byte length of any single identifier-shaped manifest string —
|
|
18
|
+
* deployment name, build ID, artifact digest, runtime name/version, SDK
|
|
19
|
+
* version, workflow/activity key, workflow revision, or contract hash.
|
|
20
|
+
*
|
|
21
|
+
* Measured in bytes rather than code units so an astral-plane string cannot
|
|
22
|
+
* carry four times the intended payload.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* import { MAX_MANIFEST_IDENTIFIER_BYTES } from '@lostgradient/weft';
|
|
27
|
+
*
|
|
28
|
+
* const buildId = 'release-2026.08.18';
|
|
29
|
+
* const fits = new TextEncoder().encode(buildId).byteLength <= MAX_MANIFEST_IDENTIFIER_BYTES;
|
|
30
|
+
* console.log(fits); // true
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export declare const MAX_MANIFEST_IDENTIFIER_BYTES = 512;
|
|
34
|
+
/**
|
|
35
|
+
* Maximum number of workflow entries one manifest may advertise.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```ts
|
|
39
|
+
* import { MAX_MANIFEST_WORKFLOW_COUNT } from '@lostgradient/weft';
|
|
40
|
+
*
|
|
41
|
+
* const advertised = ['checkout', 'refund'];
|
|
42
|
+
* console.log(advertised.length <= MAX_MANIFEST_WORKFLOW_COUNT); // true
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export declare const MAX_MANIFEST_WORKFLOW_COUNT = 512;
|
|
46
|
+
/**
|
|
47
|
+
* Maximum number of activity entries within one workflow entry.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```ts
|
|
51
|
+
* import { MAX_MANIFEST_ACTIVITY_COUNT } from '@lostgradient/weft';
|
|
52
|
+
*
|
|
53
|
+
* console.log(MAX_MANIFEST_ACTIVITY_COUNT >= 1); // true
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
export declare const MAX_MANIFEST_ACTIVITY_COUNT = 512;
|
|
57
|
+
/**
|
|
58
|
+
* Maximum number of keys in the manifest `capabilities` record.
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```ts
|
|
62
|
+
* import { MAX_MANIFEST_CAPABILITY_COUNT } from '@lostgradient/weft';
|
|
63
|
+
*
|
|
64
|
+
* const capabilities = { gpu: true, region: 'us-east-1' };
|
|
65
|
+
* console.log(Object.keys(capabilities).length <= MAX_MANIFEST_CAPABILITY_COUNT); // true
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
export declare const MAX_MANIFEST_CAPABILITY_COUNT = 64;
|
|
69
|
+
/**
|
|
70
|
+
* Maximum nesting depth permitted inside a single `capabilities` value.
|
|
71
|
+
*
|
|
72
|
+
* Depth 1 is a scalar. `isJSONValue()` is cycle-safe but unbounded, so the
|
|
73
|
+
* manifest parser imposes this itself rather than delegating.
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```ts
|
|
77
|
+
* import { MAX_MANIFEST_CAPABILITY_DEPTH } from '@lostgradient/weft';
|
|
78
|
+
*
|
|
79
|
+
* // { limits: { cpu: 4 } } is depth 3: object, object, scalar.
|
|
80
|
+
* console.log(3 <= MAX_MANIFEST_CAPABILITY_DEPTH); // true
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
export declare const MAX_MANIFEST_CAPABILITY_DEPTH = 8;
|
|
84
|
+
/**
|
|
85
|
+
* Maximum UTF-8 byte length of any single string inside `capabilities`.
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* ```ts
|
|
89
|
+
* import { MAX_MANIFEST_CAPABILITY_STRING_BYTES } from '@lostgradient/weft';
|
|
90
|
+
*
|
|
91
|
+
* const note = 'gpu pool: a100';
|
|
92
|
+
* console.log(note.length <= MAX_MANIFEST_CAPABILITY_STRING_BYTES); // true
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
export declare const MAX_MANIFEST_CAPABILITY_STRING_BYTES = 4096;
|
|
96
|
+
/**
|
|
97
|
+
* Maximum UTF-8 byte length of the canonical normalized manifest.
|
|
98
|
+
*
|
|
99
|
+
* Checked against the canonical serialization — not the received bytes — so a
|
|
100
|
+
* worker cannot evade the bound with whitespace-minimized input and then
|
|
101
|
+
* expand under normalization.
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* ```ts
|
|
105
|
+
* import { canonicalWorkerManifestJson, MAX_NORMALIZED_MANIFEST_BYTES } from '@lostgradient/weft';
|
|
106
|
+
*
|
|
107
|
+
* const canonical = canonicalWorkerManifestJson({
|
|
108
|
+
* manifestVersion: 1,
|
|
109
|
+
* protocolVersion: 2,
|
|
110
|
+
* sdkVersion: '0.18.0',
|
|
111
|
+
* runtime: { name: 'bun', version: '1.3.14' },
|
|
112
|
+
* deployment: { name: 'billing', buildId: 'b3', artifactDigest: 'sha256:41d0' },
|
|
113
|
+
* workflows: {},
|
|
114
|
+
* capabilities: {},
|
|
115
|
+
* });
|
|
116
|
+
*
|
|
117
|
+
* console.log(canonical.length <= MAX_NORMALIZED_MANIFEST_BYTES); // true
|
|
118
|
+
* ```
|
|
119
|
+
*/
|
|
120
|
+
export declare const MAX_NORMALIZED_MANIFEST_BYTES = 262144;
|
|
121
|
+
/**
|
|
122
|
+
* Maximum structural nesting depth the pre-parse duplicate-key scanner will
|
|
123
|
+
* follow into raw JSON text.
|
|
124
|
+
*
|
|
125
|
+
* The scanner pushes one entry onto an open-container stack per `{`/`[`
|
|
126
|
+
* encountered, before `JSON.parse` ever runs. Without a ceiling here, a run
|
|
127
|
+
* of open-brace characters well under the transport frame limit forces
|
|
128
|
+
* millions of allocations. No legitimate manifest — workflows, activities,
|
|
129
|
+
* and an 8-deep `capabilities` value — nests anywhere close to this bound.
|
|
130
|
+
*
|
|
131
|
+
* @example
|
|
132
|
+
* ```ts
|
|
133
|
+
* import { MAX_JSON_SCAN_NESTING_DEPTH } from '@lostgradient/weft';
|
|
134
|
+
*
|
|
135
|
+
* console.log(MAX_JSON_SCAN_NESTING_DEPTH >= 16); // true
|
|
136
|
+
* ```
|
|
137
|
+
*/
|
|
138
|
+
export declare const MAX_JSON_SCAN_NESTING_DEPTH = 64;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const MAX_MANIFEST_IDENTIFIER_BYTES = 512, MAX_MANIFEST_WORKFLOW_COUNT = 512, MAX_MANIFEST_ACTIVITY_COUNT = 512, MAX_MANIFEST_CAPABILITY_COUNT = 64, MAX_MANIFEST_CAPABILITY_DEPTH = 8, MAX_MANIFEST_CAPABILITY_STRING_BYTES = 4096, MAX_NORMALIZED_MANIFEST_BYTES = 262144, MAX_JSON_SCAN_NESTING_DEPTH = 64;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic normalization and canonical serialization for worker
|
|
3
|
+
* manifests.
|
|
4
|
+
*
|
|
5
|
+
* Two manifests that describe the same worker must produce byte-identical
|
|
6
|
+
* canonical output regardless of how their object keys were ordered in the
|
|
7
|
+
* source, so the digest identifies the *content* rather than an accident of
|
|
8
|
+
* construction. Serialization walks the declared manifest shape explicitly
|
|
9
|
+
* instead of generically stringifying the input, which means an unknown extra
|
|
10
|
+
* property can never leak into the canonical bytes and change a digest.
|
|
11
|
+
*
|
|
12
|
+
* @module worker/manifest/normalize
|
|
13
|
+
*/
|
|
14
|
+
import type { WorkerManifest } from './types.ts';
|
|
15
|
+
/**
|
|
16
|
+
* Normalize a manifest into its canonical in-memory form.
|
|
17
|
+
*
|
|
18
|
+
* Field order follows the declared shape and every open-ended record is
|
|
19
|
+
* rebuilt with its keys sorted. The result is a fresh value; the input is not
|
|
20
|
+
* mutated.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* import { normalizeWorkerManifest, WORKER_MANIFEST_VERSION } from '@lostgradient/weft';
|
|
25
|
+
*
|
|
26
|
+
* const normalized = normalizeWorkerManifest({
|
|
27
|
+
* manifestVersion: WORKER_MANIFEST_VERSION,
|
|
28
|
+
* protocolVersion: 2,
|
|
29
|
+
* sdkVersion: '0.18.0',
|
|
30
|
+
* runtime: { name: 'bun', version: '1.3.14' },
|
|
31
|
+
* deployment: { name: 'billing', buildId: 'b3', artifactDigest: 'sha256:41d0' },
|
|
32
|
+
* workflows: {},
|
|
33
|
+
* capabilities: { zeta: 1, alpha: 2 },
|
|
34
|
+
* });
|
|
35
|
+
* console.log(Object.keys(normalized.capabilities)); // ['alpha', 'zeta']
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export declare function normalizeWorkerManifest(manifest: WorkerManifest): WorkerManifest;
|
|
39
|
+
/**
|
|
40
|
+
* Serialize a manifest to its canonical JSON string.
|
|
41
|
+
*
|
|
42
|
+
* The output is the digest input: byte-identical for equivalent manifests and
|
|
43
|
+
* different for any manifest that differs in a field the host cares about.
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```ts
|
|
47
|
+
* import { canonicalWorkerManifestJson, WORKER_MANIFEST_VERSION } from '@lostgradient/weft';
|
|
48
|
+
*
|
|
49
|
+
* const base = {
|
|
50
|
+
* manifestVersion: WORKER_MANIFEST_VERSION,
|
|
51
|
+
* protocolVersion: 2,
|
|
52
|
+
* sdkVersion: '0.18.0',
|
|
53
|
+
* runtime: { name: 'bun', version: '1.3.14' },
|
|
54
|
+
* deployment: { name: 'billing', buildId: 'b3', artifactDigest: 'sha256:41d0' },
|
|
55
|
+
* workflows: {},
|
|
56
|
+
* } as const;
|
|
57
|
+
*
|
|
58
|
+
* const left = canonicalWorkerManifestJson({ ...base, capabilities: { a: 1, b: 2 } });
|
|
59
|
+
* const right = canonicalWorkerManifestJson({ ...base, capabilities: { b: 2, a: 1 } });
|
|
60
|
+
* console.log(left === right); // true
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export declare function canonicalWorkerManifestJson(manifest: WorkerManifest): string;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { canonicalJsonStringify } from "./canonical-json.js";
|
|
2
|
+
function sortedKeys(record) {
|
|
3
|
+
return Object.keys(record).toSorted();
|
|
4
|
+
}
|
|
5
|
+
function sortedRecord(record, normalizeValue) {
|
|
6
|
+
const normalized = Object.create(null);
|
|
7
|
+
for (const key of sortedKeys(record))
|
|
8
|
+
normalized[key] = normalizeValue(record[key]);
|
|
9
|
+
return normalized;
|
|
10
|
+
}
|
|
11
|
+
function cloneJsonValue(value) {
|
|
12
|
+
if (value === null || typeof value !== "object")
|
|
13
|
+
return value;
|
|
14
|
+
if (Array.isArray(value))
|
|
15
|
+
return value.map((entry) => cloneJsonValue(entry));
|
|
16
|
+
const record = value, cloned = Object.create(null);
|
|
17
|
+
for (const key of Object.keys(record))
|
|
18
|
+
cloned[key] = cloneJsonValue(record[key]);
|
|
19
|
+
return cloned;
|
|
20
|
+
}
|
|
21
|
+
function normalizeActivity(activity) {
|
|
22
|
+
return {
|
|
23
|
+
contractHash: activity.contractHash,
|
|
24
|
+
implementationRevision: activity.implementationRevision
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function normalizeWorkflow(workflow) {
|
|
28
|
+
return {
|
|
29
|
+
workflowVersion: workflow.workflowVersion,
|
|
30
|
+
workflowRevision: workflow.workflowRevision,
|
|
31
|
+
contractHash: workflow.contractHash,
|
|
32
|
+
activities: sortedRecord(workflow.activities, normalizeActivity)
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export function normalizeWorkerManifest(manifest) {
|
|
36
|
+
return {
|
|
37
|
+
manifestVersion: manifest.manifestVersion,
|
|
38
|
+
protocolVersion: manifest.protocolVersion,
|
|
39
|
+
sdkVersion: manifest.sdkVersion,
|
|
40
|
+
runtime: { name: manifest.runtime.name, version: manifest.runtime.version },
|
|
41
|
+
deployment: {
|
|
42
|
+
name: manifest.deployment.name,
|
|
43
|
+
buildId: manifest.deployment.buildId,
|
|
44
|
+
artifactDigest: manifest.deployment.artifactDigest
|
|
45
|
+
},
|
|
46
|
+
workflows: sortedRecord(manifest.workflows, normalizeWorkflow),
|
|
47
|
+
capabilities: sortedRecord(manifest.capabilities, cloneJsonValue)
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function canonicalActivityJson(activity) {
|
|
51
|
+
return `{"contractHash":${JSON.stringify(activity.contractHash)},"implementationRevision":${JSON.stringify(activity.implementationRevision)}}`;
|
|
52
|
+
}
|
|
53
|
+
function canonicalWorkflowJson(workflow) {
|
|
54
|
+
const activities = sortedKeys(workflow.activities).map((name) => `${JSON.stringify(name)}:${canonicalActivityJson(workflow.activities[name])}`).join(",");
|
|
55
|
+
return [
|
|
56
|
+
`{"workflowVersion":${JSON.stringify(workflow.workflowVersion)}`,
|
|
57
|
+
`"workflowRevision":${JSON.stringify(workflow.workflowRevision)}`,
|
|
58
|
+
`"contractHash":${JSON.stringify(workflow.contractHash)}`,
|
|
59
|
+
`"activities":{${activities}}}`
|
|
60
|
+
].join(",");
|
|
61
|
+
}
|
|
62
|
+
export function canonicalWorkerManifestJson(manifest) {
|
|
63
|
+
const workflows = sortedKeys(manifest.workflows).map((name) => `${JSON.stringify(name)}:${canonicalWorkflowJson(manifest.workflows[name])}`).join(","), capabilities = sortedKeys(manifest.capabilities).map((key) => `${JSON.stringify(key)}:${canonicalJsonStringify(manifest.capabilities[key])}`).join(",");
|
|
64
|
+
return [
|
|
65
|
+
`{"manifestVersion":${JSON.stringify(manifest.manifestVersion)}`,
|
|
66
|
+
`"protocolVersion":${JSON.stringify(manifest.protocolVersion)}`,
|
|
67
|
+
`"sdkVersion":${JSON.stringify(manifest.sdkVersion)}`,
|
|
68
|
+
`"runtime":{"name":${JSON.stringify(manifest.runtime.name)},"version":${JSON.stringify(manifest.runtime.version)}}`,
|
|
69
|
+
`"deployment":{"name":${JSON.stringify(manifest.deployment.name)},"buildId":${JSON.stringify(manifest.deployment.buildId)},"artifactDigest":${JSON.stringify(manifest.deployment.artifactDigest)}}`,
|
|
70
|
+
`"workflows":{${workflows}}`,
|
|
71
|
+
`"capabilities":{${capabilities}}}`
|
|
72
|
+
].join(",");
|
|
73
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manifest validation starting from raw JSON text.
|
|
3
|
+
*
|
|
4
|
+
* Wire callers that still hold the received text should use this entry point
|
|
5
|
+
* rather than `JSON.parse` followed by {@link parseWorkerManifest}: duplicate
|
|
6
|
+
* object keys are only visible before parsing, and silently taking the last
|
|
7
|
+
* one would let a worker present two different artifact digests in one
|
|
8
|
+
* payload.
|
|
9
|
+
*
|
|
10
|
+
* @module worker/manifest/parse-json
|
|
11
|
+
*/
|
|
12
|
+
import { type WorkerManifestParseResult } from './parse.ts';
|
|
13
|
+
/**
|
|
14
|
+
* Validate an untrusted worker manifest from JSON text.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* import { parseWorkerManifestJson } from '@lostgradient/weft';
|
|
19
|
+
*
|
|
20
|
+
* const result = parseWorkerManifestJson('{"manifestVersion":1,"manifestVersion":2}');
|
|
21
|
+
* console.log(result.ok ? 'accepted' : result.reason); // 'duplicate_key'
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export declare function parseWorkerManifestJson(text: string): WorkerManifestParseResult;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { manifestFailure } from "./failure.js";
|
|
2
|
+
import { findDuplicateJsonKey } from "./json-scan.js";
|
|
3
|
+
import { parseWorkerManifest } from "./parse.js";
|
|
4
|
+
export function parseWorkerManifestJson(text) {
|
|
5
|
+
const duplicate = findDuplicateJsonKey(text);
|
|
6
|
+
if (duplicate !== void 0)
|
|
7
|
+
return manifestFailure("duplicate_key", `manifest JSON declares the key ${JSON.stringify(duplicate)} more than once`);
|
|
8
|
+
let parsed;
|
|
9
|
+
try {
|
|
10
|
+
parsed = JSON.parse(text);
|
|
11
|
+
} catch {
|
|
12
|
+
return manifestFailure("invalid_json", "manifest must be valid JSON");
|
|
13
|
+
}
|
|
14
|
+
return parseWorkerManifest(parsed);
|
|
15
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hostile-input parsing for worker manifests.
|
|
3
|
+
*
|
|
4
|
+
* Everything here treats its input as adversarial: a manifest arrives from a
|
|
5
|
+
* worker process the host does not control, and it decides whether that worker
|
|
6
|
+
* becomes routing-eligible. Every field is proven from `unknown` rather than
|
|
7
|
+
* asserted, every open-ended collection is bounded before it is walked, and
|
|
8
|
+
* the normalized size is checked against the canonical form so a compact
|
|
9
|
+
* payload cannot expand past the ceiling after normalization.
|
|
10
|
+
*
|
|
11
|
+
* @module worker/manifest/parse
|
|
12
|
+
*/
|
|
13
|
+
import { type ManifestValidationFailure } from './failure.ts';
|
|
14
|
+
import type { WorkerManifest } from './types.ts';
|
|
15
|
+
/**
|
|
16
|
+
* A successfully validated manifest, paired with the canonical serialization
|
|
17
|
+
* the digest is computed over so callers do not serialize it a second time.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```ts
|
|
21
|
+
* import {
|
|
22
|
+
* digestCanonicalWorkerManifest,
|
|
23
|
+
* parseWorkerManifest,
|
|
24
|
+
* type WorkerManifestParseSuccess,
|
|
25
|
+
* } from '@lostgradient/weft';
|
|
26
|
+
*
|
|
27
|
+
* const result = parseWorkerManifest({
|
|
28
|
+
* manifestVersion: 1,
|
|
29
|
+
* protocolVersion: 2,
|
|
30
|
+
* sdkVersion: '0.18.0',
|
|
31
|
+
* runtime: { name: 'bun', version: '1.3.14' },
|
|
32
|
+
* deployment: { name: 'billing', buildId: 'b3', artifactDigest: 'sha256:41d0' },
|
|
33
|
+
* workflows: {},
|
|
34
|
+
* capabilities: {},
|
|
35
|
+
* });
|
|
36
|
+
*
|
|
37
|
+
* if (result.ok) {
|
|
38
|
+
* const accepted: WorkerManifestParseSuccess = result;
|
|
39
|
+
* console.log(await digestCanonicalWorkerManifest(accepted.canonicalJson));
|
|
40
|
+
* }
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export type WorkerManifestParseSuccess = Readonly<{
|
|
44
|
+
ok: true;
|
|
45
|
+
/** The normalized manifest, with every open-ended record key sorted. */
|
|
46
|
+
manifest: WorkerManifest;
|
|
47
|
+
/** Canonical serialization of that manifest — the digest input. */
|
|
48
|
+
canonicalJson: string;
|
|
49
|
+
}>;
|
|
50
|
+
/**
|
|
51
|
+
* Outcome of validating an untrusted manifest.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```ts
|
|
55
|
+
* import { parseWorkerManifest, type WorkerManifestParseResult } from '@lostgradient/weft';
|
|
56
|
+
*
|
|
57
|
+
* const result: WorkerManifestParseResult = parseWorkerManifest({});
|
|
58
|
+
* console.log(result.ok);
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
export type WorkerManifestParseResult = WorkerManifestParseSuccess | ManifestValidationFailure;
|
|
62
|
+
/**
|
|
63
|
+
* Validate an untrusted worker manifest.
|
|
64
|
+
*
|
|
65
|
+
* The returned manifest is already normalized, so a caller that stores or
|
|
66
|
+
* digests the result never has to re-derive canonical form. Rejection is a
|
|
67
|
+
* value, not an exception — a bad manifest is an ordinary wire condition.
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* ```ts
|
|
71
|
+
* import { parseWorkerManifest } from '@lostgradient/weft';
|
|
72
|
+
*
|
|
73
|
+
* const result = parseWorkerManifest({
|
|
74
|
+
* manifestVersion: 1,
|
|
75
|
+
* protocolVersion: 2,
|
|
76
|
+
* sdkVersion: '0.18.0',
|
|
77
|
+
* runtime: { name: 'bun', version: '1.3.14' },
|
|
78
|
+
* deployment: { name: 'billing', buildId: 'b3', artifactDigest: 'sha256:41d0' },
|
|
79
|
+
* workflows: {},
|
|
80
|
+
* capabilities: {},
|
|
81
|
+
* });
|
|
82
|
+
*
|
|
83
|
+
* console.log(result.ok ? result.manifest.deployment.name : result.reason);
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
export declare function parseWorkerManifest(value: unknown): WorkerManifestParseResult;
|