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.
Files changed (96) hide show
  1. package/README.md +65 -22
  2. package/SECURITY.md +1 -1
  3. package/dist/index.js +2 -0
  4. package/dist/lib/executor.js +16 -1
  5. package/dist/lib/policy.js +301 -22
  6. package/dist/lib/progress.js +499 -113
  7. package/dist/lib/runtime.js +109 -40
  8. package/dist/lib/structured.js +327 -11
  9. package/dist/native-loader.js +11 -12
  10. package/index.d.ts +54 -6
  11. package/mustard.d.ts +23 -1
  12. package/package.json +34 -25
  13. package/Cargo.lock +0 -1579
  14. package/Cargo.toml +0 -40
  15. package/crates/mustard/Cargo.toml +0 -31
  16. package/crates/mustard/src/cancellation.rs +0 -28
  17. package/crates/mustard/src/diagnostic.rs +0 -145
  18. package/crates/mustard/src/ir.rs +0 -435
  19. package/crates/mustard/src/lib.rs +0 -21
  20. package/crates/mustard/src/limits.rs +0 -22
  21. package/crates/mustard/src/parser/expressions.rs +0 -723
  22. package/crates/mustard/src/parser/mod.rs +0 -115
  23. package/crates/mustard/src/parser/operators.rs +0 -105
  24. package/crates/mustard/src/parser/patterns.rs +0 -123
  25. package/crates/mustard/src/parser/scope.rs +0 -107
  26. package/crates/mustard/src/parser/statements.rs +0 -298
  27. package/crates/mustard/src/parser/tests/acceptance.rs +0 -339
  28. package/crates/mustard/src/parser/tests/mod.rs +0 -2
  29. package/crates/mustard/src/parser/tests/rejections.rs +0 -107
  30. package/crates/mustard/src/runtime/accounting.rs +0 -613
  31. package/crates/mustard/src/runtime/api.rs +0 -192
  32. package/crates/mustard/src/runtime/async_runtime/mod.rs +0 -5
  33. package/crates/mustard/src/runtime/async_runtime/promises.rs +0 -246
  34. package/crates/mustard/src/runtime/async_runtime/reactions.rs +0 -400
  35. package/crates/mustard/src/runtime/async_runtime/scheduler.rs +0 -224
  36. package/crates/mustard/src/runtime/builtins/arrays.rs +0 -1205
  37. package/crates/mustard/src/runtime/builtins/collections.rs +0 -573
  38. package/crates/mustard/src/runtime/builtins/install.rs +0 -501
  39. package/crates/mustard/src/runtime/builtins/intl.rs +0 -553
  40. package/crates/mustard/src/runtime/builtins/mod.rs +0 -25
  41. package/crates/mustard/src/runtime/builtins/objects.rs +0 -405
  42. package/crates/mustard/src/runtime/builtins/primitives.rs +0 -859
  43. package/crates/mustard/src/runtime/builtins/promises.rs +0 -335
  44. package/crates/mustard/src/runtime/builtins/regexp.rs +0 -356
  45. package/crates/mustard/src/runtime/builtins/strings.rs +0 -803
  46. package/crates/mustard/src/runtime/builtins/support.rs +0 -561
  47. package/crates/mustard/src/runtime/bytecode.rs +0 -123
  48. package/crates/mustard/src/runtime/compiler/assignments.rs +0 -690
  49. package/crates/mustard/src/runtime/compiler/bindings.rs +0 -92
  50. package/crates/mustard/src/runtime/compiler/context.rs +0 -46
  51. package/crates/mustard/src/runtime/compiler/control.rs +0 -342
  52. package/crates/mustard/src/runtime/compiler/expressions.rs +0 -372
  53. package/crates/mustard/src/runtime/compiler/mod.rs +0 -173
  54. package/crates/mustard/src/runtime/compiler/statements.rs +0 -459
  55. package/crates/mustard/src/runtime/conversions/boundary.rs +0 -293
  56. package/crates/mustard/src/runtime/conversions/coercions.rs +0 -217
  57. package/crates/mustard/src/runtime/conversions/errors.rs +0 -118
  58. package/crates/mustard/src/runtime/conversions/mod.rs +0 -14
  59. package/crates/mustard/src/runtime/conversions/operators.rs +0 -334
  60. package/crates/mustard/src/runtime/env.rs +0 -355
  61. package/crates/mustard/src/runtime/exceptions.rs +0 -377
  62. package/crates/mustard/src/runtime/gc.rs +0 -595
  63. package/crates/mustard/src/runtime/mod.rs +0 -318
  64. package/crates/mustard/src/runtime/properties.rs +0 -1762
  65. package/crates/mustard/src/runtime/serialization.rs +0 -127
  66. package/crates/mustard/src/runtime/shared.rs +0 -108
  67. package/crates/mustard/src/runtime/snapshot_validation_tests.rs +0 -93
  68. package/crates/mustard/src/runtime/state.rs +0 -652
  69. package/crates/mustard/src/runtime/tests/async_host.rs +0 -104
  70. package/crates/mustard/src/runtime/tests/collections.rs +0 -50
  71. package/crates/mustard/src/runtime/tests/diagnostics.rs +0 -36
  72. package/crates/mustard/src/runtime/tests/exceptions.rs +0 -122
  73. package/crates/mustard/src/runtime/tests/execution.rs +0 -553
  74. package/crates/mustard/src/runtime/tests/gc.rs +0 -533
  75. package/crates/mustard/src/runtime/tests/mod.rs +0 -56
  76. package/crates/mustard/src/runtime/tests/serialization.rs +0 -170
  77. package/crates/mustard/src/runtime/validation/bytecode.rs +0 -484
  78. package/crates/mustard/src/runtime/validation/mod.rs +0 -14
  79. package/crates/mustard/src/runtime/validation/policy.rs +0 -94
  80. package/crates/mustard/src/runtime/validation/snapshot.rs +0 -406
  81. package/crates/mustard/src/runtime/validation/walk.rs +0 -206
  82. package/crates/mustard/src/runtime/vm.rs +0 -1016
  83. package/crates/mustard/src/span.rs +0 -22
  84. package/crates/mustard/src/structured.rs +0 -107
  85. package/crates/mustard-bridge/Cargo.toml +0 -17
  86. package/crates/mustard-bridge/src/codec.rs +0 -46
  87. package/crates/mustard-bridge/src/dto.rs +0 -99
  88. package/crates/mustard-bridge/src/lib.rs +0 -12
  89. package/crates/mustard-bridge/src/operations.rs +0 -142
  90. package/crates/mustard-node/Cargo.toml +0 -24
  91. package/crates/mustard-node/build.rs +0 -3
  92. package/crates/mustard-node/src/lib.rs +0 -236
  93. package/crates/mustard-sidecar/Cargo.toml +0 -21
  94. package/crates/mustard-sidecar/src/lib.rs +0 -134
  95. package/crates/mustard-sidecar/src/main.rs +0 -36
  96. 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 default and fully verified
88
- path is still source-build installation from a clean checkout or packed source
89
- tarball, where `npm install` compiles the native addon locally. Optional
90
- prebuilt binaries now have a separate release flow for the documented target
91
- matrix, but the loader now only accepts validated `.node` artifacts from the
92
- expected optional package layout. Source-build fallback remains the baseline
93
- path, now ships `Cargo.lock`, and builds the addon in release mode. It still
94
- requires a Rust toolchain plus Node.js on the target machine.
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 Rust addon locally and then runs the Node and packaging
104
- smoke tests. Prebuilt binaries are intentionally deferred until the package
105
- shape is stable.
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`, and
463
- `Number.isFinite` are supported
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 result = await program.run({
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 `capabilities` or `console`, explicit `limits` as an object (use `{}` for
772
- default runtime limits), and the original `snapshotKey`. The dumped token
773
- authenticates the snapshot bytes before any loaded capability metadata is
774
- trusted, and same-process dumps stay single-use.
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/keppoai/mustardscript/security/advisories/new
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,
@@ -28,14 +28,29 @@ function cloneJobRecord(record) {
28
28
  }
29
29
 
30
30
  function clonePersistedProgress(progress) {
31
- return {
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) {
@@ -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 { defineEnumerableProperty, hasOwnProperty, isAccessorDescriptor } = require('./structured.js');
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 encoded = cloneSnapshotPolicy(policy);
287
+ const chunks = [getEncodedSnapshotPolicyPrefix(policy)];
131
288
  if (typeof options?.snapshotId === 'string' && options.snapshotId.length > 0) {
132
- encoded.snapshot_id = options.snapshotId;
289
+ chunks.push(',"snapshot_id":', JSON.stringify(options.snapshotId));
133
290
  }
134
- if (options?.snapshotKey !== undefined) {
135
- const snapshotKey = cloneSnapshotKey(options.snapshotKey);
136
- encoded.snapshot_key_base64 = snapshotKey.toString('base64');
137
- encoded.snapshot_key_digest = snapshotKeyDigest(snapshotKey);
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
- encoded.snapshot_token = options.snapshotToken;
301
+ chunks.push(',"snapshot_token":', JSON.stringify(options.snapshotToken));
141
302
  }
142
- return JSON.stringify(encoded);
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 actualSnapshotId = snapshotIdentity(snapshot);
182
- if (expectedSnapshotId !== undefined && actualSnapshotId !== expectedSnapshotId) {
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, actualSnapshotId);
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: normalizeSnapshotKey(snapshotKey, 'options.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 snapshotKey = normalizeSnapshotKey(
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: createExecutionPolicy({ ...options, limits }).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
  };