mustardscript 0.1.0 → 0.1.2
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 +65 -22
- package/SECURITY.md +1 -1
- package/dist/index.js +2 -0
- package/dist/lib/executor.js +16 -1
- package/dist/lib/policy.js +301 -22
- package/dist/lib/progress.js +499 -113
- package/dist/lib/runtime.js +109 -40
- package/dist/lib/structured.js +327 -11
- package/dist/native-loader.js +11 -12
- package/index.d.ts +54 -6
- package/mustard.d.ts +23 -1
- package/package.json +34 -25
- package/Cargo.lock +0 -1579
- package/Cargo.toml +0 -40
- package/crates/mustard/Cargo.toml +0 -31
- package/crates/mustard/src/cancellation.rs +0 -28
- package/crates/mustard/src/diagnostic.rs +0 -145
- package/crates/mustard/src/ir.rs +0 -435
- package/crates/mustard/src/lib.rs +0 -21
- package/crates/mustard/src/limits.rs +0 -22
- package/crates/mustard/src/parser/expressions.rs +0 -723
- package/crates/mustard/src/parser/mod.rs +0 -115
- package/crates/mustard/src/parser/operators.rs +0 -105
- package/crates/mustard/src/parser/patterns.rs +0 -123
- package/crates/mustard/src/parser/scope.rs +0 -107
- package/crates/mustard/src/parser/statements.rs +0 -298
- package/crates/mustard/src/parser/tests/acceptance.rs +0 -339
- package/crates/mustard/src/parser/tests/mod.rs +0 -2
- package/crates/mustard/src/parser/tests/rejections.rs +0 -107
- package/crates/mustard/src/runtime/accounting.rs +0 -613
- package/crates/mustard/src/runtime/api.rs +0 -192
- package/crates/mustard/src/runtime/async_runtime/mod.rs +0 -5
- package/crates/mustard/src/runtime/async_runtime/promises.rs +0 -246
- package/crates/mustard/src/runtime/async_runtime/reactions.rs +0 -400
- package/crates/mustard/src/runtime/async_runtime/scheduler.rs +0 -224
- package/crates/mustard/src/runtime/builtins/arrays.rs +0 -1205
- package/crates/mustard/src/runtime/builtins/collections.rs +0 -573
- package/crates/mustard/src/runtime/builtins/install.rs +0 -501
- package/crates/mustard/src/runtime/builtins/intl.rs +0 -553
- package/crates/mustard/src/runtime/builtins/mod.rs +0 -25
- package/crates/mustard/src/runtime/builtins/objects.rs +0 -405
- package/crates/mustard/src/runtime/builtins/primitives.rs +0 -859
- package/crates/mustard/src/runtime/builtins/promises.rs +0 -335
- package/crates/mustard/src/runtime/builtins/regexp.rs +0 -356
- package/crates/mustard/src/runtime/builtins/strings.rs +0 -803
- package/crates/mustard/src/runtime/builtins/support.rs +0 -561
- package/crates/mustard/src/runtime/bytecode.rs +0 -123
- package/crates/mustard/src/runtime/compiler/assignments.rs +0 -690
- package/crates/mustard/src/runtime/compiler/bindings.rs +0 -92
- package/crates/mustard/src/runtime/compiler/context.rs +0 -46
- package/crates/mustard/src/runtime/compiler/control.rs +0 -342
- package/crates/mustard/src/runtime/compiler/expressions.rs +0 -372
- package/crates/mustard/src/runtime/compiler/mod.rs +0 -173
- package/crates/mustard/src/runtime/compiler/statements.rs +0 -459
- package/crates/mustard/src/runtime/conversions/boundary.rs +0 -293
- package/crates/mustard/src/runtime/conversions/coercions.rs +0 -217
- package/crates/mustard/src/runtime/conversions/errors.rs +0 -118
- package/crates/mustard/src/runtime/conversions/mod.rs +0 -14
- package/crates/mustard/src/runtime/conversions/operators.rs +0 -334
- package/crates/mustard/src/runtime/env.rs +0 -355
- package/crates/mustard/src/runtime/exceptions.rs +0 -377
- package/crates/mustard/src/runtime/gc.rs +0 -595
- package/crates/mustard/src/runtime/mod.rs +0 -318
- package/crates/mustard/src/runtime/properties.rs +0 -1762
- package/crates/mustard/src/runtime/serialization.rs +0 -127
- package/crates/mustard/src/runtime/shared.rs +0 -108
- package/crates/mustard/src/runtime/snapshot_validation_tests.rs +0 -93
- package/crates/mustard/src/runtime/state.rs +0 -652
- package/crates/mustard/src/runtime/tests/async_host.rs +0 -104
- package/crates/mustard/src/runtime/tests/collections.rs +0 -50
- package/crates/mustard/src/runtime/tests/diagnostics.rs +0 -36
- package/crates/mustard/src/runtime/tests/exceptions.rs +0 -122
- package/crates/mustard/src/runtime/tests/execution.rs +0 -553
- package/crates/mustard/src/runtime/tests/gc.rs +0 -533
- package/crates/mustard/src/runtime/tests/mod.rs +0 -56
- package/crates/mustard/src/runtime/tests/serialization.rs +0 -170
- package/crates/mustard/src/runtime/validation/bytecode.rs +0 -484
- package/crates/mustard/src/runtime/validation/mod.rs +0 -14
- package/crates/mustard/src/runtime/validation/policy.rs +0 -94
- package/crates/mustard/src/runtime/validation/snapshot.rs +0 -406
- package/crates/mustard/src/runtime/validation/walk.rs +0 -206
- package/crates/mustard/src/runtime/vm.rs +0 -1016
- package/crates/mustard/src/span.rs +0 -22
- package/crates/mustard/src/structured.rs +0 -107
- package/crates/mustard-bridge/Cargo.toml +0 -17
- package/crates/mustard-bridge/src/codec.rs +0 -46
- package/crates/mustard-bridge/src/dto.rs +0 -99
- package/crates/mustard-bridge/src/lib.rs +0 -12
- package/crates/mustard-bridge/src/operations.rs +0 -142
- package/crates/mustard-node/Cargo.toml +0 -24
- package/crates/mustard-node/build.rs +0 -3
- package/crates/mustard-node/src/lib.rs +0 -236
- package/crates/mustard-sidecar/Cargo.toml +0 -21
- package/crates/mustard-sidecar/src/lib.rs +0 -134
- package/crates/mustard-sidecar/src/main.rs +0 -36
- package/dist/install.js +0 -117
package/README.md
CHANGED
|
@@ -67,9 +67,9 @@ The current implementation already supports:
|
|
|
67
67
|
|
|
68
68
|
## Reference Docs
|
|
69
69
|
|
|
70
|
-
- [Security Threat Model](SECURITY_THREAT_MODEL.md)
|
|
70
|
+
- [Security Threat Model](docs/SECURITY_THREAT_MODEL.md)
|
|
71
71
|
- [Security Model](docs/SECURITY_MODEL.md)
|
|
72
|
-
- [Use Case Gaps](USE_CASE_GAPS.md)
|
|
72
|
+
- [Use Case Gaps](docs/USE_CASE_GAPS.md)
|
|
73
73
|
- [Language Contract](docs/LANGUAGE.md)
|
|
74
74
|
- [Host API](docs/HOST_API.md)
|
|
75
75
|
- [Serialization](docs/SERIALIZATION.md)
|
|
@@ -79,30 +79,36 @@ The current implementation already supports:
|
|
|
79
79
|
- [Runtime Value Model](docs/RUNTIME_MODEL.md)
|
|
80
80
|
- [Sidecar Protocol](docs/SIDECAR_PROTOCOL.md)
|
|
81
81
|
- [Benchmarking Notes and Comparison Plan](benchmarks/README.md)
|
|
82
|
+
- [PTC Benchmark Coverage](docs/PTC_BENCHMARK_COVERAGE.md)
|
|
82
83
|
- [Release Guide](docs/RELEASE.md)
|
|
83
84
|
- [Architecture ADRs](docs/ADRs/0001-core-architecture.md)
|
|
84
85
|
|
|
85
86
|
## Installation
|
|
86
87
|
|
|
87
|
-
The release package name is `mustardscript`. The
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
88
|
+
The release package name is `mustardscript`. The published npm package is now
|
|
89
|
+
prebuilt-only: `npm install mustardscript` succeeds only when npm can install a
|
|
90
|
+
matching optional native binding package for the current platform. The loader
|
|
91
|
+
accepts only the documented binding package layout and fails closed when no
|
|
92
|
+
matching prebuilt is present.
|
|
93
|
+
|
|
94
|
+
Current prebuilt target matrix:
|
|
95
|
+
|
|
96
|
+
- `@mustardscript/binding-darwin-arm64`
|
|
97
|
+
- `@mustardscript/binding-darwin-x64`
|
|
98
|
+
- `@mustardscript/binding-linux-x64-gnu`
|
|
99
|
+
- `@mustardscript/binding-win32-x64-msvc`
|
|
95
100
|
|
|
96
101
|
From a clean checkout:
|
|
97
102
|
|
|
98
103
|
```sh
|
|
99
104
|
npm install
|
|
105
|
+
npm run build
|
|
100
106
|
npm test
|
|
101
107
|
```
|
|
102
108
|
|
|
103
|
-
That flow builds the
|
|
104
|
-
|
|
105
|
-
|
|
109
|
+
That maintainer flow installs dependencies, builds the local addon in the
|
|
110
|
+
checkout, and then runs the Node and packaging smoke tests. End-user npm
|
|
111
|
+
installs no longer compile the addon from Rust sources.
|
|
106
112
|
|
|
107
113
|
Release verification and publish guidance live in
|
|
108
114
|
[docs/RELEASE.md](docs/RELEASE.md).
|
|
@@ -116,6 +122,27 @@ Maintainers can run `npm run ralph-loop -- <plan.md>` to repeatedly invoke
|
|
|
116
122
|
marks itself with `[PLAN HAS BEEN COMPLETED]` or `[BLOCKED]`. Use
|
|
117
123
|
`--max-iterations <n>` to cap the loop when needed.
|
|
118
124
|
|
|
125
|
+
## Website Playground
|
|
126
|
+
|
|
127
|
+
The repository now includes an experimental website playground in
|
|
128
|
+
[`website/`](website) that compares:
|
|
129
|
+
|
|
130
|
+
- `MustardScript` guest code running in the browser via a dedicated
|
|
131
|
+
`wasm32-unknown-unknown` build of the Rust core
|
|
132
|
+
- vanilla JavaScript running client-side inside a sandboxed iframe
|
|
133
|
+
|
|
134
|
+
The website build copies a raw `.wasm` artifact into
|
|
135
|
+
`website/public/mustard-playground.wasm` through
|
|
136
|
+
`website/scripts/build-playground-wasm.mjs`. The iframe path is a demo-only
|
|
137
|
+
comparison surface, not a hardened sandbox. It intentionally has no ambient
|
|
138
|
+
host authority beyond the fixed scenario helper set, but a synchronous infinite
|
|
139
|
+
loop in browser JavaScript can still block the page event loop before the
|
|
140
|
+
cooperative timeout/reset logic runs.
|
|
141
|
+
|
|
142
|
+
The current release-mode `.wasm` artifact is also large, roughly 71 MB before
|
|
143
|
+
HTTP compression, so the browser playground should be treated as an
|
|
144
|
+
experimental demo target until bundle-size work lands.
|
|
145
|
+
|
|
119
146
|
## Agent-Style Example
|
|
120
147
|
|
|
121
148
|
See [examples/agent-style.ts](examples/agent-style.ts) for a minimal host loop
|
|
@@ -459,8 +486,9 @@ Current built-in helper support is intentionally conservative:
|
|
|
459
486
|
- `Date.now()`, `new Date(value).getTime()`, `Date.prototype.toISOString()`,
|
|
460
487
|
`Date.prototype.toJSON()`, and the documented UTC field accessors are
|
|
461
488
|
supported
|
|
462
|
-
- `Number.parseInt`, `Number.parseFloat`, `Number.isNaN`,
|
|
463
|
-
`Number.isFinite
|
|
489
|
+
- `Number.parseInt`, `Number.parseFloat`, `Number.isNaN`,
|
|
490
|
+
`Number.isFinite`, and the documented number formatting helpers are
|
|
491
|
+
supported
|
|
464
492
|
- `Intl.DateTimeFormat` and `Intl.NumberFormat` are available in a narrow
|
|
465
493
|
`en-US` / `UTC` subset with explicit fail-closed behavior for unsupported
|
|
466
494
|
locales and options
|
|
@@ -711,6 +739,8 @@ The public Node API should stay small.
|
|
|
711
739
|
Illustrative shape:
|
|
712
740
|
|
|
713
741
|
```ts
|
|
742
|
+
import { ExecutionContext, Mustard } from 'mustardscript'
|
|
743
|
+
|
|
714
744
|
type HostValue =
|
|
715
745
|
| undefined
|
|
716
746
|
| null
|
|
@@ -726,8 +756,7 @@ const program = new Mustard(source, {
|
|
|
726
756
|
inputs: ['x'],
|
|
727
757
|
})
|
|
728
758
|
|
|
729
|
-
const
|
|
730
|
-
inputs: { x: 1 },
|
|
759
|
+
const context = new ExecutionContext({
|
|
731
760
|
capabilities: {
|
|
732
761
|
fetch_data: async (url) => '...',
|
|
733
762
|
},
|
|
@@ -736,12 +765,23 @@ const result = await program.run({
|
|
|
736
765
|
heapLimitBytes: 8 << 20,
|
|
737
766
|
callDepthLimit: 256,
|
|
738
767
|
},
|
|
768
|
+
snapshotKey: 'host-chosen-snapshot-key',
|
|
769
|
+
})
|
|
770
|
+
|
|
771
|
+
const result = await program.run({
|
|
772
|
+
context,
|
|
773
|
+
inputs: { x: 1 },
|
|
739
774
|
})
|
|
740
775
|
```
|
|
741
776
|
|
|
777
|
+
`ExecutionContext` is optional, but it is the intended steady-state path when a
|
|
778
|
+
host will reuse the same capabilities, limits, and snapshot key across many
|
|
779
|
+
`run()` / `start()` / `Progress.load()` calls.
|
|
780
|
+
|
|
742
781
|
Lower-level control should exist for advanced hosts:
|
|
743
782
|
|
|
744
783
|
- `new Mustard(...)`
|
|
784
|
+
- `new ExecutionContext(...)`
|
|
745
785
|
- `Mustard.validateProgram(...)`
|
|
746
786
|
- `run(...)`
|
|
747
787
|
- `start(...)`
|
|
@@ -756,7 +796,7 @@ For hosts managing a large backlog of resumable jobs, the Node wrapper also
|
|
|
756
796
|
exports `MustardExecutor` plus `InMemoryMustardExecutorStore` as a thin
|
|
757
797
|
queue-oriented layer over `start()` / `Progress.dump()` / `Progress.load()`.
|
|
758
798
|
The design and invariants for that layer are documented in
|
|
759
|
-
[MUSTARD_EXECUTOR.md](MUSTARD_EXECUTOR.md).
|
|
799
|
+
[docs/MUSTARD_EXECUTOR.md](docs/MUSTARD_EXECUTOR.md).
|
|
760
800
|
|
|
761
801
|
Native failures are surfaced in Node as typed JavaScript errors:
|
|
762
802
|
`MustardParseError`, `MustardValidationError`, `MustardRuntimeError`,
|
|
@@ -768,10 +808,13 @@ program. It does not prove that a later `run()` or `start()` call will succeed
|
|
|
768
808
|
with a particular host policy, input set, capability map, or runtime limit.
|
|
769
809
|
|
|
770
810
|
`Progress.load(...)` always requires explicit restore authority: the host must
|
|
771
|
-
pass
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
trusted,
|
|
811
|
+
pass either a reusable `ExecutionContext` or explicit `capabilities` /
|
|
812
|
+
`console`, explicit `limits` as an object (use `{}` for default runtime
|
|
813
|
+
limits), and the original `snapshotKey`. The dumped token authenticates the
|
|
814
|
+
snapshot bytes before any loaded capability metadata is trusted, current dumps
|
|
815
|
+
also carry authenticated suspended metadata so `Progress.load(...)` usually
|
|
816
|
+
avoids native re-inspection, legacy dumps still fall back to inspection, and
|
|
817
|
+
same-process dumps stay single-use.
|
|
775
818
|
|
|
776
819
|
The common path should be easy. The advanced path should remain explicit.
|
|
777
820
|
|
package/SECURITY.md
CHANGED
|
@@ -18,7 +18,7 @@ Security issues include:
|
|
|
18
18
|
|
|
19
19
|
Report vulnerabilities through GitHub Security Advisories:
|
|
20
20
|
|
|
21
|
-
- https://github.com/
|
|
21
|
+
- https://github.com/mustardscript/mustardscript/security/advisories/new
|
|
22
22
|
|
|
23
23
|
Do not open public GitHub issues for unpatched security reports.
|
|
24
24
|
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const { loadNative } = require('./native-loader.js');
|
|
4
4
|
const { createExecutorApi } = require('./lib/executor.js');
|
|
5
5
|
const { MustardError } = require('./lib/errors.js');
|
|
6
|
+
const { ExecutionContext } = require('./lib/policy.js');
|
|
6
7
|
const { createProgressApi } = require('./lib/progress.js');
|
|
7
8
|
const { createMustardClass } = require('./lib/runtime.js');
|
|
8
9
|
|
|
@@ -12,6 +13,7 @@ const Mustard = createMustardClass({ native, materializeStep, parseStep });
|
|
|
12
13
|
const { InMemoryMustardExecutorStore, MustardExecutor } = createExecutorApi({ Mustard, Progress });
|
|
13
14
|
|
|
14
15
|
module.exports = {
|
|
16
|
+
ExecutionContext,
|
|
15
17
|
InMemoryMustardExecutorStore,
|
|
16
18
|
MustardError,
|
|
17
19
|
Mustard,
|
package/dist/lib/executor.js
CHANGED
|
@@ -28,14 +28,29 @@ function cloneJobRecord(record) {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
function clonePersistedProgress(progress) {
|
|
31
|
-
|
|
31
|
+
const cloned = {
|
|
32
32
|
capability: progress.capability,
|
|
33
33
|
args: structuredClone(progress.args),
|
|
34
34
|
snapshot: Buffer.from(progress.snapshot),
|
|
35
35
|
snapshot_id: progress.snapshot_id,
|
|
36
36
|
snapshot_key_digest: progress.snapshot_key_digest,
|
|
37
37
|
token: progress.token,
|
|
38
|
+
suspended_manifest:
|
|
39
|
+
typeof progress.suspended_manifest === 'string'
|
|
40
|
+
? progress.suspended_manifest
|
|
41
|
+
: undefined,
|
|
42
|
+
suspended_manifest_token:
|
|
43
|
+
typeof progress.suspended_manifest_token === 'string'
|
|
44
|
+
? progress.suspended_manifest_token
|
|
45
|
+
: undefined,
|
|
38
46
|
};
|
|
47
|
+
if (Buffer.isBuffer(progress.program) || progress.program instanceof Uint8Array) {
|
|
48
|
+
cloned.program = Buffer.from(progress.program);
|
|
49
|
+
}
|
|
50
|
+
if (typeof progress.program_id === 'string') {
|
|
51
|
+
cloned.program_id = progress.program_id;
|
|
52
|
+
}
|
|
53
|
+
return cloned;
|
|
39
54
|
}
|
|
40
55
|
|
|
41
56
|
function sanitizeFailure(error) {
|
package/dist/lib/policy.js
CHANGED
|
@@ -5,7 +5,13 @@ const { types } = require('node:util');
|
|
|
5
5
|
const { loadNative } = require('../native-loader.js');
|
|
6
6
|
|
|
7
7
|
const { MustardError, callNative } = require('./errors.js');
|
|
8
|
-
const {
|
|
8
|
+
const {
|
|
9
|
+
decodeStructured,
|
|
10
|
+
defineEnumerableProperty,
|
|
11
|
+
encodeStructured,
|
|
12
|
+
hasOwnProperty,
|
|
13
|
+
isAccessorDescriptor,
|
|
14
|
+
} = require('./structured.js');
|
|
9
15
|
|
|
10
16
|
const CONSOLE_CAPABILITY_NAMES = {
|
|
11
17
|
log: 'console.log',
|
|
@@ -13,7 +19,18 @@ const CONSOLE_CAPABILITY_NAMES = {
|
|
|
13
19
|
error: 'console.error',
|
|
14
20
|
};
|
|
15
21
|
const DEFAULT_SNAPSHOT_KEY = crypto.randomBytes(32);
|
|
22
|
+
const encodedSnapshotPolicyPrefixCache = new WeakMap();
|
|
16
23
|
let nativeSnapshotHelpers;
|
|
24
|
+
const executionContextHandleRegistry =
|
|
25
|
+
typeof FinalizationRegistry === 'function'
|
|
26
|
+
? new FinalizationRegistry((contextHandle) => {
|
|
27
|
+
try {
|
|
28
|
+
callNative(snapshotNative().releaseExecutionContext, contextHandle);
|
|
29
|
+
} catch {
|
|
30
|
+
// Best-effort cleanup only; process shutdown can race native teardown.
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
: null;
|
|
17
34
|
|
|
18
35
|
function snapshotNative() {
|
|
19
36
|
nativeSnapshotHelpers ??= loadNative();
|
|
@@ -126,20 +143,165 @@ function cloneSnapshotKey(snapshotKey) {
|
|
|
126
143
|
return Buffer.from(snapshotKey);
|
|
127
144
|
}
|
|
128
145
|
|
|
146
|
+
function freezePolicy(policy) {
|
|
147
|
+
return Object.freeze({
|
|
148
|
+
capabilities: Object.freeze(policy.capabilities.slice()),
|
|
149
|
+
limits: Object.freeze({ ...policy.limits }),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function getEncodedSnapshotPolicyPrefix(policy) {
|
|
154
|
+
let cached = encodedSnapshotPolicyPrefixCache.get(policy);
|
|
155
|
+
if (cached !== undefined) {
|
|
156
|
+
return cached;
|
|
157
|
+
}
|
|
158
|
+
cached =
|
|
159
|
+
`{"capabilities":${JSON.stringify(policy.capabilities)}` +
|
|
160
|
+
`,"limits":${JSON.stringify(policy.limits)}`;
|
|
161
|
+
encodedSnapshotPolicyPrefixCache.set(policy, cached);
|
|
162
|
+
return cached;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function resolveSnapshotKeyEncoding(options) {
|
|
166
|
+
if (
|
|
167
|
+
typeof options?.snapshotKeyBase64 === 'string' &&
|
|
168
|
+
options.snapshotKeyBase64.length > 0 &&
|
|
169
|
+
typeof options?.snapshotKeyDigest === 'string' &&
|
|
170
|
+
options.snapshotKeyDigest.length > 0
|
|
171
|
+
) {
|
|
172
|
+
return {
|
|
173
|
+
snapshotKeyBase64: options.snapshotKeyBase64,
|
|
174
|
+
snapshotKeyDigest: options.snapshotKeyDigest,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
if (options?.snapshotKey === undefined) {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
const snapshotKey = cloneSnapshotKey(options.snapshotKey);
|
|
181
|
+
return {
|
|
182
|
+
snapshotKeyBase64: snapshotKey.toString('base64'),
|
|
183
|
+
snapshotKeyDigest: snapshotKeyDigest(snapshotKey),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function assertNoContextOverrides(options, label) {
|
|
188
|
+
if (
|
|
189
|
+
hasOwnProperty(options, 'capabilities') ||
|
|
190
|
+
hasOwnProperty(options, 'console') ||
|
|
191
|
+
hasOwnProperty(options, 'limits') ||
|
|
192
|
+
hasOwnProperty(options, 'snapshotKey')
|
|
193
|
+
) {
|
|
194
|
+
throw new TypeError(
|
|
195
|
+
`${label}.context cannot be combined with capabilities, console, limits, or snapshotKey`,
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
class ExecutionContext {
|
|
201
|
+
#hostHandlers;
|
|
202
|
+
#policy;
|
|
203
|
+
#policyJson;
|
|
204
|
+
#snapshotKey;
|
|
205
|
+
#snapshotKeyBase64;
|
|
206
|
+
#snapshotKeyDigest;
|
|
207
|
+
#nativeHandle;
|
|
208
|
+
#nativeHandleToken;
|
|
209
|
+
|
|
210
|
+
constructor(options = {}) {
|
|
211
|
+
const {
|
|
212
|
+
hostHandlers,
|
|
213
|
+
policy,
|
|
214
|
+
snapshotKey,
|
|
215
|
+
snapshotKeyBase64,
|
|
216
|
+
snapshotKeyDigest: snapshotKeyDigestValue,
|
|
217
|
+
} = createExecutionPolicy(options);
|
|
218
|
+
this.#hostHandlers = hostHandlers;
|
|
219
|
+
this.#policy = freezePolicy(policy);
|
|
220
|
+
this.#policyJson = null;
|
|
221
|
+
this.#snapshotKey = cloneSnapshotKey(snapshotKey);
|
|
222
|
+
this.#snapshotKeyBase64 = snapshotKeyBase64;
|
|
223
|
+
this.#snapshotKeyDigest = snapshotKeyDigestValue;
|
|
224
|
+
this.#nativeHandle = null;
|
|
225
|
+
this.#nativeHandleToken = null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
hostHandlers() {
|
|
229
|
+
return this.#hostHandlers;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
policy() {
|
|
233
|
+
return this.#policy;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
snapshotKey() {
|
|
237
|
+
return cloneSnapshotKey(this.#snapshotKey);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
snapshotKeyMetadata() {
|
|
241
|
+
return {
|
|
242
|
+
snapshotKey: cloneSnapshotKey(this.#snapshotKey),
|
|
243
|
+
snapshotKeyBase64: this.#snapshotKeyBase64,
|
|
244
|
+
snapshotKeyDigest: this.#snapshotKeyDigest,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
policyJson() {
|
|
249
|
+
this.#policyJson ??= JSON.stringify(this.#policy);
|
|
250
|
+
return this.#policyJson;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
nativeHandle() {
|
|
254
|
+
if (this.#nativeHandle !== null) {
|
|
255
|
+
return this.#nativeHandle;
|
|
256
|
+
}
|
|
257
|
+
const nativeHandle = callNative(
|
|
258
|
+
snapshotNative().createExecutionContext,
|
|
259
|
+
this.policyJson(),
|
|
260
|
+
);
|
|
261
|
+
this.#nativeHandle = nativeHandle;
|
|
262
|
+
this.#nativeHandleToken = {};
|
|
263
|
+
executionContextHandleRegistry?.register(this, nativeHandle, this.#nativeHandleToken);
|
|
264
|
+
return nativeHandle;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function resolveExecutionContext(options = {}, label = 'options') {
|
|
269
|
+
const context = options?.context;
|
|
270
|
+
if (context === undefined) {
|
|
271
|
+
return createExecutionPolicy(options);
|
|
272
|
+
}
|
|
273
|
+
if (!(context instanceof ExecutionContext)) {
|
|
274
|
+
throw new TypeError(`${label}.context must be an ExecutionContext`);
|
|
275
|
+
}
|
|
276
|
+
assertNoContextOverrides(options, label);
|
|
277
|
+
const snapshotKeyMetadata = context.snapshotKeyMetadata();
|
|
278
|
+
return {
|
|
279
|
+
hostHandlers: context.hostHandlers(),
|
|
280
|
+
policy: context.policy(),
|
|
281
|
+
nativeContextHandle: context.nativeHandle(),
|
|
282
|
+
...snapshotKeyMetadata,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
129
286
|
function encodeSnapshotPolicy(policy, options = undefined) {
|
|
130
|
-
const
|
|
287
|
+
const chunks = [getEncodedSnapshotPolicyPrefix(policy)];
|
|
131
288
|
if (typeof options?.snapshotId === 'string' && options.snapshotId.length > 0) {
|
|
132
|
-
|
|
289
|
+
chunks.push(',"snapshot_id":', JSON.stringify(options.snapshotId));
|
|
133
290
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
291
|
+
const snapshotKeyEncoding = resolveSnapshotKeyEncoding(options);
|
|
292
|
+
if (snapshotKeyEncoding !== null) {
|
|
293
|
+
chunks.push(
|
|
294
|
+
',"snapshot_key_base64":',
|
|
295
|
+
JSON.stringify(snapshotKeyEncoding.snapshotKeyBase64),
|
|
296
|
+
',"snapshot_key_digest":',
|
|
297
|
+
JSON.stringify(snapshotKeyEncoding.snapshotKeyDigest),
|
|
298
|
+
);
|
|
138
299
|
}
|
|
139
300
|
if (typeof options?.snapshotToken === 'string' && options.snapshotToken.length > 0) {
|
|
140
|
-
|
|
301
|
+
chunks.push(',"snapshot_token":', JSON.stringify(options.snapshotToken));
|
|
141
302
|
}
|
|
142
|
-
|
|
303
|
+
chunks.push('}');
|
|
304
|
+
return chunks.join('');
|
|
143
305
|
}
|
|
144
306
|
|
|
145
307
|
function normalizeSnapshotKey(snapshotKey, label) {
|
|
@@ -164,22 +326,108 @@ function snapshotIdentity(snapshot) {
|
|
|
164
326
|
return callNative(snapshotNative().snapshotIdentity, Buffer.from(snapshot));
|
|
165
327
|
}
|
|
166
328
|
|
|
329
|
+
function programIdentity(program) {
|
|
330
|
+
return crypto.createHash('sha256').update(Buffer.from(program)).digest('hex');
|
|
331
|
+
}
|
|
332
|
+
|
|
167
333
|
function snapshotKeyDigest(snapshotKey) {
|
|
168
334
|
return crypto.createHash('sha256').update(snapshotKey).digest('hex');
|
|
169
335
|
}
|
|
170
336
|
|
|
337
|
+
function suspendedManifestError() {
|
|
338
|
+
return new MustardError(
|
|
339
|
+
'Serialization',
|
|
340
|
+
'Progress.load() rejected tampered or unauthenticated suspended metadata',
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function createSuspendedManifest(capability, args) {
|
|
345
|
+
if (typeof capability !== 'string' || capability.length === 0) {
|
|
346
|
+
throw new TypeError('Progress.dump() requires a suspended capability name');
|
|
347
|
+
}
|
|
348
|
+
if (!Array.isArray(args)) {
|
|
349
|
+
throw new TypeError('Progress.dump() requires suspended args as an array');
|
|
350
|
+
}
|
|
351
|
+
return JSON.stringify({
|
|
352
|
+
capability,
|
|
353
|
+
args: args.map((value) => encodeStructured(value)),
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function parseSuspendedManifest(suspendedManifest) {
|
|
358
|
+
try {
|
|
359
|
+
const manifest = JSON.parse(suspendedManifest);
|
|
360
|
+
if (manifest === null || typeof manifest !== 'object' || Array.isArray(manifest)) {
|
|
361
|
+
throw suspendedManifestError();
|
|
362
|
+
}
|
|
363
|
+
if (typeof manifest.capability !== 'string' || manifest.capability.length === 0) {
|
|
364
|
+
throw suspendedManifestError();
|
|
365
|
+
}
|
|
366
|
+
if (!Array.isArray(manifest.args)) {
|
|
367
|
+
throw suspendedManifestError();
|
|
368
|
+
}
|
|
369
|
+
return {
|
|
370
|
+
capability: manifest.capability,
|
|
371
|
+
args: manifest.args.map((value) => decodeStructured(value)),
|
|
372
|
+
};
|
|
373
|
+
} catch (error) {
|
|
374
|
+
if (error instanceof MustardError) {
|
|
375
|
+
throw error;
|
|
376
|
+
}
|
|
377
|
+
throw suspendedManifestError();
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function suspendedManifestToken(snapshotId, suspendedManifest, snapshotKey) {
|
|
382
|
+
return crypto
|
|
383
|
+
.createHmac('sha256', snapshotKey)
|
|
384
|
+
.update(snapshotId, 'utf8')
|
|
385
|
+
.update('\0', 'utf8')
|
|
386
|
+
.update(suspendedManifest, 'utf8')
|
|
387
|
+
.digest('hex');
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function assertSuspendedManifest(state, snapshotKey, expectedSnapshotId) {
|
|
391
|
+
const suspendedManifest = state.suspended_manifest;
|
|
392
|
+
const token = state.suspended_manifest_token;
|
|
393
|
+
if (suspendedManifest === undefined && token === undefined) {
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
if (
|
|
397
|
+
typeof suspendedManifest !== 'string' ||
|
|
398
|
+
suspendedManifest.length === 0 ||
|
|
399
|
+
typeof token !== 'string' ||
|
|
400
|
+
token.length === 0
|
|
401
|
+
) {
|
|
402
|
+
throw suspendedManifestError();
|
|
403
|
+
}
|
|
404
|
+
const expected = suspendedManifestToken(
|
|
405
|
+
expectedSnapshotId,
|
|
406
|
+
suspendedManifest,
|
|
407
|
+
snapshotKey,
|
|
408
|
+
);
|
|
409
|
+
if (
|
|
410
|
+
token.length !== expected.length ||
|
|
411
|
+
!crypto.timingSafeEqual(Buffer.from(token, 'utf8'), Buffer.from(expected, 'utf8'))
|
|
412
|
+
) {
|
|
413
|
+
throw suspendedManifestError();
|
|
414
|
+
}
|
|
415
|
+
return parseSuspendedManifest(suspendedManifest);
|
|
416
|
+
}
|
|
417
|
+
|
|
171
418
|
function assertSnapshotToken(
|
|
172
419
|
snapshot,
|
|
173
420
|
token,
|
|
174
421
|
snapshotKey,
|
|
175
422
|
expectedSnapshotId = undefined,
|
|
176
423
|
expectedSnapshotKeyDigest = undefined,
|
|
424
|
+
actualSnapshotId = undefined,
|
|
177
425
|
) {
|
|
178
426
|
if (typeof token !== 'string' || token.length === 0) {
|
|
179
427
|
throw new TypeError('Progress.load() requires a dumped progress token');
|
|
180
428
|
}
|
|
181
|
-
const
|
|
182
|
-
if (expectedSnapshotId !== undefined &&
|
|
429
|
+
const resolvedSnapshotId = actualSnapshotId ?? snapshotIdentity(snapshot);
|
|
430
|
+
if (expectedSnapshotId !== undefined && resolvedSnapshotId !== expectedSnapshotId) {
|
|
183
431
|
throw new MustardError(
|
|
184
432
|
'Serialization',
|
|
185
433
|
'Progress.load() rejected a tampered or unauthenticated snapshot',
|
|
@@ -194,7 +442,7 @@ function assertSnapshotToken(
|
|
|
194
442
|
'Progress.load() rejected a mismatched snapshot key digest',
|
|
195
443
|
);
|
|
196
444
|
}
|
|
197
|
-
const expected = snapshotToken(snapshot, snapshotKey,
|
|
445
|
+
const expected = snapshotToken(snapshot, snapshotKey, resolvedSnapshotId);
|
|
198
446
|
if (
|
|
199
447
|
token.length !== expected.length ||
|
|
200
448
|
!crypto.timingSafeEqual(Buffer.from(token, 'utf8'), Buffer.from(expected, 'utf8'))
|
|
@@ -208,17 +456,20 @@ function assertSnapshotToken(
|
|
|
208
456
|
|
|
209
457
|
function createExecutionPolicy({ limits = {}, snapshotKey, ...handlers } = {}) {
|
|
210
458
|
const hostHandlers = collectHostHandlers(handlers);
|
|
459
|
+
const normalizedSnapshotKey = normalizeSnapshotKey(snapshotKey, 'options.snapshotKey');
|
|
211
460
|
return {
|
|
212
461
|
hostHandlers,
|
|
213
462
|
policy: {
|
|
214
463
|
capabilities: Object.keys(hostHandlers),
|
|
215
464
|
limits: encodeRuntimeLimits(limits),
|
|
216
465
|
},
|
|
217
|
-
snapshotKey:
|
|
466
|
+
snapshotKey: normalizedSnapshotKey,
|
|
467
|
+
snapshotKeyBase64: normalizedSnapshotKey.toString('base64'),
|
|
468
|
+
snapshotKeyDigest: snapshotKeyDigest(normalizedSnapshotKey),
|
|
218
469
|
};
|
|
219
470
|
}
|
|
220
471
|
|
|
221
|
-
function resolveProgressLoadContext(state, snapshot, options) {
|
|
472
|
+
function resolveProgressLoadContext(state, snapshot, options, actualSnapshotId = undefined) {
|
|
222
473
|
const expectedSnapshotId =
|
|
223
474
|
typeof state.snapshot_id === 'string' && state.snapshot_id.length > 0
|
|
224
475
|
? state.snapshot_id
|
|
@@ -235,9 +486,30 @@ function resolveProgressLoadContext(state, snapshot, options) {
|
|
|
235
486
|
}
|
|
236
487
|
if (options === undefined || options === null || typeof options !== 'object') {
|
|
237
488
|
throw new TypeError(
|
|
238
|
-
'Progress.load() requires explicit capabilities, limits, and snapshotKey',
|
|
489
|
+
'Progress.load() requires an ExecutionContext or explicit capabilities, limits, and snapshotKey',
|
|
239
490
|
);
|
|
240
491
|
}
|
|
492
|
+
if (hasOwnProperty(options, 'context')) {
|
|
493
|
+
const context = options.context;
|
|
494
|
+
if (!(context instanceof ExecutionContext)) {
|
|
495
|
+
throw new TypeError('Progress.load() options.context must be an ExecutionContext');
|
|
496
|
+
}
|
|
497
|
+
assertNoContextOverrides(options, 'Progress.load() options');
|
|
498
|
+
const snapshotKeyMetadata = context.snapshotKeyMetadata();
|
|
499
|
+
assertSnapshotToken(
|
|
500
|
+
snapshot,
|
|
501
|
+
state.token,
|
|
502
|
+
snapshotKeyMetadata.snapshotKey,
|
|
503
|
+
expectedSnapshotId,
|
|
504
|
+
expectedSnapshotKeyDigest,
|
|
505
|
+
actualSnapshotId,
|
|
506
|
+
);
|
|
507
|
+
return {
|
|
508
|
+
policy: context.policy(),
|
|
509
|
+
nativeContextHandle: context.nativeHandle(),
|
|
510
|
+
...snapshotKeyMetadata,
|
|
511
|
+
};
|
|
512
|
+
}
|
|
241
513
|
if (
|
|
242
514
|
!hasOwnProperty(options, 'capabilities') &&
|
|
243
515
|
!hasOwnProperty(options, 'console')
|
|
@@ -260,33 +532,40 @@ function resolveProgressLoadContext(state, snapshot, options) {
|
|
|
260
532
|
'Progress.load() requires explicit snapshotKey when restoring progress',
|
|
261
533
|
);
|
|
262
534
|
}
|
|
263
|
-
const
|
|
264
|
-
options.snapshotKey,
|
|
265
|
-
'Progress.load() options.snapshotKey',
|
|
266
|
-
);
|
|
535
|
+
const executionPolicy = createExecutionPolicy({ ...options, limits });
|
|
267
536
|
assertSnapshotToken(
|
|
268
537
|
snapshot,
|
|
269
538
|
state.token,
|
|
270
|
-
snapshotKey,
|
|
539
|
+
executionPolicy.snapshotKey,
|
|
271
540
|
expectedSnapshotId,
|
|
272
541
|
expectedSnapshotKeyDigest,
|
|
542
|
+
actualSnapshotId,
|
|
273
543
|
);
|
|
274
544
|
return {
|
|
275
|
-
policy:
|
|
276
|
-
snapshotKey: cloneSnapshotKey(snapshotKey),
|
|
545
|
+
policy: executionPolicy.policy,
|
|
546
|
+
snapshotKey: cloneSnapshotKey(executionPolicy.snapshotKey),
|
|
547
|
+
snapshotKeyBase64: executionPolicy.snapshotKeyBase64,
|
|
548
|
+
snapshotKeyDigest: executionPolicy.snapshotKeyDigest,
|
|
277
549
|
};
|
|
278
550
|
}
|
|
279
551
|
|
|
280
552
|
module.exports = {
|
|
553
|
+
ExecutionContext,
|
|
554
|
+
assertSuspendedManifest,
|
|
281
555
|
cloneSnapshotPolicy,
|
|
282
556
|
cloneSnapshotKey,
|
|
283
557
|
collectHostHandlers,
|
|
558
|
+
createSuspendedManifest,
|
|
284
559
|
createExecutionPolicy,
|
|
285
560
|
encodeRuntimeLimits,
|
|
286
561
|
encodeSnapshotPolicy,
|
|
287
562
|
normalizeSnapshotKey,
|
|
563
|
+
parseSuspendedManifest,
|
|
564
|
+
resolveExecutionContext,
|
|
288
565
|
resolveProgressLoadContext,
|
|
566
|
+
programIdentity,
|
|
289
567
|
snapshotIdentity,
|
|
290
568
|
snapshotKeyDigest,
|
|
291
569
|
snapshotToken,
|
|
570
|
+
suspendedManifestToken,
|
|
292
571
|
};
|