mustardscript 0.1.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/Cargo.lock +1579 -0
- package/Cargo.toml +40 -0
- package/LICENSE +201 -0
- package/README.md +828 -0
- package/SECURITY.md +34 -0
- package/crates/mustard/Cargo.toml +31 -0
- package/crates/mustard/src/cancellation.rs +28 -0
- package/crates/mustard/src/diagnostic.rs +145 -0
- package/crates/mustard/src/ir.rs +435 -0
- package/crates/mustard/src/lib.rs +21 -0
- package/crates/mustard/src/limits.rs +22 -0
- package/crates/mustard/src/parser/expressions.rs +723 -0
- package/crates/mustard/src/parser/mod.rs +115 -0
- package/crates/mustard/src/parser/operators.rs +105 -0
- package/crates/mustard/src/parser/patterns.rs +123 -0
- package/crates/mustard/src/parser/scope.rs +107 -0
- package/crates/mustard/src/parser/statements.rs +298 -0
- package/crates/mustard/src/parser/tests/acceptance.rs +339 -0
- package/crates/mustard/src/parser/tests/mod.rs +2 -0
- package/crates/mustard/src/parser/tests/rejections.rs +107 -0
- package/crates/mustard/src/runtime/accounting.rs +613 -0
- package/crates/mustard/src/runtime/api.rs +192 -0
- package/crates/mustard/src/runtime/async_runtime/mod.rs +5 -0
- package/crates/mustard/src/runtime/async_runtime/promises.rs +246 -0
- package/crates/mustard/src/runtime/async_runtime/reactions.rs +400 -0
- package/crates/mustard/src/runtime/async_runtime/scheduler.rs +224 -0
- package/crates/mustard/src/runtime/builtins/arrays.rs +1205 -0
- package/crates/mustard/src/runtime/builtins/collections.rs +573 -0
- package/crates/mustard/src/runtime/builtins/install.rs +501 -0
- package/crates/mustard/src/runtime/builtins/intl.rs +553 -0
- package/crates/mustard/src/runtime/builtins/mod.rs +25 -0
- package/crates/mustard/src/runtime/builtins/objects.rs +405 -0
- package/crates/mustard/src/runtime/builtins/primitives.rs +859 -0
- package/crates/mustard/src/runtime/builtins/promises.rs +335 -0
- package/crates/mustard/src/runtime/builtins/regexp.rs +356 -0
- package/crates/mustard/src/runtime/builtins/strings.rs +803 -0
- package/crates/mustard/src/runtime/builtins/support.rs +561 -0
- package/crates/mustard/src/runtime/bytecode.rs +123 -0
- package/crates/mustard/src/runtime/compiler/assignments.rs +690 -0
- package/crates/mustard/src/runtime/compiler/bindings.rs +92 -0
- package/crates/mustard/src/runtime/compiler/context.rs +46 -0
- package/crates/mustard/src/runtime/compiler/control.rs +342 -0
- package/crates/mustard/src/runtime/compiler/expressions.rs +372 -0
- package/crates/mustard/src/runtime/compiler/mod.rs +173 -0
- package/crates/mustard/src/runtime/compiler/statements.rs +459 -0
- package/crates/mustard/src/runtime/conversions/boundary.rs +293 -0
- package/crates/mustard/src/runtime/conversions/coercions.rs +217 -0
- package/crates/mustard/src/runtime/conversions/errors.rs +118 -0
- package/crates/mustard/src/runtime/conversions/mod.rs +14 -0
- package/crates/mustard/src/runtime/conversions/operators.rs +334 -0
- package/crates/mustard/src/runtime/env.rs +355 -0
- package/crates/mustard/src/runtime/exceptions.rs +377 -0
- package/crates/mustard/src/runtime/gc.rs +595 -0
- package/crates/mustard/src/runtime/mod.rs +318 -0
- package/crates/mustard/src/runtime/properties.rs +1762 -0
- package/crates/mustard/src/runtime/serialization.rs +127 -0
- package/crates/mustard/src/runtime/shared.rs +108 -0
- package/crates/mustard/src/runtime/snapshot_validation_tests.rs +93 -0
- package/crates/mustard/src/runtime/state.rs +652 -0
- package/crates/mustard/src/runtime/tests/async_host.rs +104 -0
- package/crates/mustard/src/runtime/tests/collections.rs +50 -0
- package/crates/mustard/src/runtime/tests/diagnostics.rs +36 -0
- package/crates/mustard/src/runtime/tests/exceptions.rs +122 -0
- package/crates/mustard/src/runtime/tests/execution.rs +553 -0
- package/crates/mustard/src/runtime/tests/gc.rs +533 -0
- package/crates/mustard/src/runtime/tests/mod.rs +56 -0
- package/crates/mustard/src/runtime/tests/serialization.rs +170 -0
- package/crates/mustard/src/runtime/validation/bytecode.rs +484 -0
- package/crates/mustard/src/runtime/validation/mod.rs +14 -0
- package/crates/mustard/src/runtime/validation/policy.rs +94 -0
- package/crates/mustard/src/runtime/validation/snapshot.rs +406 -0
- package/crates/mustard/src/runtime/validation/walk.rs +206 -0
- package/crates/mustard/src/runtime/vm.rs +1016 -0
- package/crates/mustard/src/span.rs +22 -0
- package/crates/mustard/src/structured.rs +107 -0
- package/crates/mustard-bridge/Cargo.toml +17 -0
- package/crates/mustard-bridge/src/codec.rs +46 -0
- package/crates/mustard-bridge/src/dto.rs +99 -0
- package/crates/mustard-bridge/src/lib.rs +12 -0
- package/crates/mustard-bridge/src/operations.rs +142 -0
- package/crates/mustard-node/Cargo.toml +24 -0
- package/crates/mustard-node/build.rs +3 -0
- package/crates/mustard-node/src/lib.rs +236 -0
- package/crates/mustard-sidecar/Cargo.toml +21 -0
- package/crates/mustard-sidecar/src/lib.rs +134 -0
- package/crates/mustard-sidecar/src/main.rs +36 -0
- package/dist/index.js +20 -0
- package/dist/install.js +117 -0
- package/dist/lib/cancellation.js +124 -0
- package/dist/lib/errors.js +46 -0
- package/dist/lib/executor.js +555 -0
- package/dist/lib/policy.js +292 -0
- package/dist/lib/progress.js +356 -0
- package/dist/lib/runtime.js +109 -0
- package/dist/lib/structured.js +286 -0
- package/dist/native-loader.js +227 -0
- package/index.d.ts +23 -0
- package/mustard.d.ts +220 -0
- package/package.json +97 -0
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('node:crypto');
|
|
4
|
+
const { types } = require('node:util');
|
|
5
|
+
const { loadNative } = require('../native-loader.js');
|
|
6
|
+
|
|
7
|
+
const { MustardError, callNative } = require('./errors.js');
|
|
8
|
+
const { defineEnumerableProperty, hasOwnProperty, isAccessorDescriptor } = require('./structured.js');
|
|
9
|
+
|
|
10
|
+
const CONSOLE_CAPABILITY_NAMES = {
|
|
11
|
+
log: 'console.log',
|
|
12
|
+
warn: 'console.warn',
|
|
13
|
+
error: 'console.error',
|
|
14
|
+
};
|
|
15
|
+
const DEFAULT_SNAPSHOT_KEY = crypto.randomBytes(32);
|
|
16
|
+
let nativeSnapshotHelpers;
|
|
17
|
+
|
|
18
|
+
function snapshotNative() {
|
|
19
|
+
nativeSnapshotHelpers ??= loadNative();
|
|
20
|
+
return nativeSnapshotHelpers;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function validatePlainHandlerContainer(value, label) {
|
|
24
|
+
if (value === undefined) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value) || types.isProxy(value)) {
|
|
28
|
+
throw new TypeError(`${label} must be a plain object`);
|
|
29
|
+
}
|
|
30
|
+
const prototype = Object.getPrototypeOf(value);
|
|
31
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
32
|
+
throw new TypeError(`${label} must be a plain object`);
|
|
33
|
+
}
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function enumerateHandlerProperties(value, label) {
|
|
38
|
+
const container = validatePlainHandlerContainer(value, label);
|
|
39
|
+
if (container === null) {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
return Object.entries(Object.getOwnPropertyDescriptors(container)).filter(([, descriptor]) => {
|
|
43
|
+
if (!descriptor.enumerable) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
if (isAccessorDescriptor(descriptor)) {
|
|
47
|
+
throw new TypeError(`${label} cannot define accessor properties`);
|
|
48
|
+
}
|
|
49
|
+
return true;
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function collectHostHandlers({ capabilities = {}, console = {} } = {}) {
|
|
54
|
+
const handlers = {};
|
|
55
|
+
for (const [name, descriptor] of enumerateHandlerProperties(
|
|
56
|
+
capabilities,
|
|
57
|
+
'options.capabilities',
|
|
58
|
+
)) {
|
|
59
|
+
defineEnumerableProperty(handlers, name, descriptor.value);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const consoleDescriptors = new Map(
|
|
63
|
+
enumerateHandlerProperties(console, 'options.console'),
|
|
64
|
+
);
|
|
65
|
+
for (const [method, capabilityName] of Object.entries(CONSOLE_CAPABILITY_NAMES)) {
|
|
66
|
+
const descriptor = consoleDescriptors.get(method);
|
|
67
|
+
if (!descriptor) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const handler = descriptor.value;
|
|
71
|
+
if (typeof handler !== 'function') {
|
|
72
|
+
throw new TypeError(`console.${method} must be a function`);
|
|
73
|
+
}
|
|
74
|
+
if (handlers[capabilityName] !== undefined) {
|
|
75
|
+
throw new TypeError(
|
|
76
|
+
`Duplicate handler for ${capabilityName}; use either options.console or options.capabilities`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
handlers[capabilityName] = handler;
|
|
80
|
+
}
|
|
81
|
+
return handlers;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function encodeRuntimeLimits(limits = {}) {
|
|
85
|
+
const encodedLimits = {};
|
|
86
|
+
if (limits.instructionBudget !== undefined) {
|
|
87
|
+
encodedLimits.instruction_budget = limits.instructionBudget;
|
|
88
|
+
}
|
|
89
|
+
if (limits.heapLimitBytes !== undefined) {
|
|
90
|
+
encodedLimits.heap_limit_bytes = limits.heapLimitBytes;
|
|
91
|
+
}
|
|
92
|
+
if (limits.allocationBudget !== undefined) {
|
|
93
|
+
encodedLimits.allocation_budget = limits.allocationBudget;
|
|
94
|
+
}
|
|
95
|
+
if (limits.callDepthLimit !== undefined) {
|
|
96
|
+
encodedLimits.call_depth_limit = limits.callDepthLimit;
|
|
97
|
+
}
|
|
98
|
+
if (limits.maxOutstandingHostCalls !== undefined) {
|
|
99
|
+
encodedLimits.max_outstanding_host_calls = limits.maxOutstandingHostCalls;
|
|
100
|
+
}
|
|
101
|
+
return encodedLimits;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function validateRuntimeLimitsObject(limits, label) {
|
|
105
|
+
if (limits === undefined || limits === null || typeof limits !== 'object') {
|
|
106
|
+
throw new TypeError(`${label} must be a plain object`);
|
|
107
|
+
}
|
|
108
|
+
if (Array.isArray(limits) || types.isProxy(limits)) {
|
|
109
|
+
throw new TypeError(`${label} must be a plain object`);
|
|
110
|
+
}
|
|
111
|
+
const prototype = Object.getPrototypeOf(limits);
|
|
112
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
113
|
+
throw new TypeError(`${label} must be a plain object`);
|
|
114
|
+
}
|
|
115
|
+
return limits;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function cloneSnapshotPolicy(policy) {
|
|
119
|
+
return {
|
|
120
|
+
capabilities: policy.capabilities.slice(),
|
|
121
|
+
limits: { ...policy.limits },
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function cloneSnapshotKey(snapshotKey) {
|
|
126
|
+
return Buffer.from(snapshotKey);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function encodeSnapshotPolicy(policy, options = undefined) {
|
|
130
|
+
const encoded = cloneSnapshotPolicy(policy);
|
|
131
|
+
if (typeof options?.snapshotId === 'string' && options.snapshotId.length > 0) {
|
|
132
|
+
encoded.snapshot_id = options.snapshotId;
|
|
133
|
+
}
|
|
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);
|
|
138
|
+
}
|
|
139
|
+
if (typeof options?.snapshotToken === 'string' && options.snapshotToken.length > 0) {
|
|
140
|
+
encoded.snapshot_token = options.snapshotToken;
|
|
141
|
+
}
|
|
142
|
+
return JSON.stringify(encoded);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function normalizeSnapshotKey(snapshotKey, label) {
|
|
146
|
+
if (snapshotKey === undefined) {
|
|
147
|
+
return cloneSnapshotKey(DEFAULT_SNAPSHOT_KEY);
|
|
148
|
+
}
|
|
149
|
+
if (typeof snapshotKey === 'string') {
|
|
150
|
+
return Buffer.from(snapshotKey, 'utf8');
|
|
151
|
+
}
|
|
152
|
+
if (Buffer.isBuffer(snapshotKey) || snapshotKey instanceof Uint8Array) {
|
|
153
|
+
return Buffer.from(snapshotKey);
|
|
154
|
+
}
|
|
155
|
+
throw new TypeError(`${label} must be a string, Buffer, or Uint8Array`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function snapshotToken(snapshot, snapshotKey, snapshotId = undefined) {
|
|
159
|
+
const identity = snapshotId ?? snapshotIdentity(snapshot);
|
|
160
|
+
return crypto.createHmac('sha256', snapshotKey).update(identity, 'utf8').digest('hex');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function snapshotIdentity(snapshot) {
|
|
164
|
+
return callNative(snapshotNative().snapshotIdentity, Buffer.from(snapshot));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function snapshotKeyDigest(snapshotKey) {
|
|
168
|
+
return crypto.createHash('sha256').update(snapshotKey).digest('hex');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function assertSnapshotToken(
|
|
172
|
+
snapshot,
|
|
173
|
+
token,
|
|
174
|
+
snapshotKey,
|
|
175
|
+
expectedSnapshotId = undefined,
|
|
176
|
+
expectedSnapshotKeyDigest = undefined,
|
|
177
|
+
) {
|
|
178
|
+
if (typeof token !== 'string' || token.length === 0) {
|
|
179
|
+
throw new TypeError('Progress.load() requires a dumped progress token');
|
|
180
|
+
}
|
|
181
|
+
const actualSnapshotId = snapshotIdentity(snapshot);
|
|
182
|
+
if (expectedSnapshotId !== undefined && actualSnapshotId !== expectedSnapshotId) {
|
|
183
|
+
throw new MustardError(
|
|
184
|
+
'Serialization',
|
|
185
|
+
'Progress.load() rejected a tampered or unauthenticated snapshot',
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
if (
|
|
189
|
+
expectedSnapshotKeyDigest !== undefined &&
|
|
190
|
+
snapshotKeyDigest(snapshotKey) !== expectedSnapshotKeyDigest
|
|
191
|
+
) {
|
|
192
|
+
throw new MustardError(
|
|
193
|
+
'Serialization',
|
|
194
|
+
'Progress.load() rejected a mismatched snapshot key digest',
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
const expected = snapshotToken(snapshot, snapshotKey, actualSnapshotId);
|
|
198
|
+
if (
|
|
199
|
+
token.length !== expected.length ||
|
|
200
|
+
!crypto.timingSafeEqual(Buffer.from(token, 'utf8'), Buffer.from(expected, 'utf8'))
|
|
201
|
+
) {
|
|
202
|
+
throw new MustardError(
|
|
203
|
+
'Serialization',
|
|
204
|
+
'Progress.load() rejected a tampered or unauthenticated snapshot',
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function createExecutionPolicy({ limits = {}, snapshotKey, ...handlers } = {}) {
|
|
210
|
+
const hostHandlers = collectHostHandlers(handlers);
|
|
211
|
+
return {
|
|
212
|
+
hostHandlers,
|
|
213
|
+
policy: {
|
|
214
|
+
capabilities: Object.keys(hostHandlers),
|
|
215
|
+
limits: encodeRuntimeLimits(limits),
|
|
216
|
+
},
|
|
217
|
+
snapshotKey: normalizeSnapshotKey(snapshotKey, 'options.snapshotKey'),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function resolveProgressLoadContext(state, snapshot, options) {
|
|
222
|
+
const expectedSnapshotId =
|
|
223
|
+
typeof state.snapshot_id === 'string' && state.snapshot_id.length > 0
|
|
224
|
+
? state.snapshot_id
|
|
225
|
+
: undefined;
|
|
226
|
+
const expectedSnapshotKeyDigest =
|
|
227
|
+
typeof state.snapshot_key_digest === 'string' && state.snapshot_key_digest.length > 0
|
|
228
|
+
? state.snapshot_key_digest
|
|
229
|
+
: undefined;
|
|
230
|
+
if (expectedSnapshotId === undefined) {
|
|
231
|
+
throw new TypeError('Progress.load() requires dumped snapshot_id metadata');
|
|
232
|
+
}
|
|
233
|
+
if (expectedSnapshotKeyDigest === undefined) {
|
|
234
|
+
throw new TypeError('Progress.load() requires dumped snapshot_key_digest metadata');
|
|
235
|
+
}
|
|
236
|
+
if (options === undefined || options === null || typeof options !== 'object') {
|
|
237
|
+
throw new TypeError(
|
|
238
|
+
'Progress.load() requires explicit capabilities, limits, and snapshotKey',
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
if (
|
|
242
|
+
!hasOwnProperty(options, 'capabilities') &&
|
|
243
|
+
!hasOwnProperty(options, 'console')
|
|
244
|
+
) {
|
|
245
|
+
throw new TypeError(
|
|
246
|
+
'Progress.load() requires explicit capabilities when restoring progress',
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
if (!hasOwnProperty(options, 'limits')) {
|
|
250
|
+
throw new TypeError(
|
|
251
|
+
'Progress.load() requires explicit limits when restoring progress',
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
const limits = validateRuntimeLimitsObject(
|
|
255
|
+
options.limits,
|
|
256
|
+
'Progress.load() options.limits',
|
|
257
|
+
);
|
|
258
|
+
if (options.snapshotKey === undefined) {
|
|
259
|
+
throw new TypeError(
|
|
260
|
+
'Progress.load() requires explicit snapshotKey when restoring progress',
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
const snapshotKey = normalizeSnapshotKey(
|
|
264
|
+
options.snapshotKey,
|
|
265
|
+
'Progress.load() options.snapshotKey',
|
|
266
|
+
);
|
|
267
|
+
assertSnapshotToken(
|
|
268
|
+
snapshot,
|
|
269
|
+
state.token,
|
|
270
|
+
snapshotKey,
|
|
271
|
+
expectedSnapshotId,
|
|
272
|
+
expectedSnapshotKeyDigest,
|
|
273
|
+
);
|
|
274
|
+
return {
|
|
275
|
+
policy: createExecutionPolicy({ ...options, limits }).policy,
|
|
276
|
+
snapshotKey: cloneSnapshotKey(snapshotKey),
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
module.exports = {
|
|
281
|
+
cloneSnapshotPolicy,
|
|
282
|
+
cloneSnapshotKey,
|
|
283
|
+
collectHostHandlers,
|
|
284
|
+
createExecutionPolicy,
|
|
285
|
+
encodeRuntimeLimits,
|
|
286
|
+
encodeSnapshotPolicy,
|
|
287
|
+
normalizeSnapshotKey,
|
|
288
|
+
resolveProgressLoadContext,
|
|
289
|
+
snapshotIdentity,
|
|
290
|
+
snapshotKeyDigest,
|
|
291
|
+
snapshotToken,
|
|
292
|
+
};
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const os = require('node:os');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const { performance } = require('node:perf_hooks');
|
|
7
|
+
|
|
8
|
+
const { MustardError, callNative } = require('./errors.js');
|
|
9
|
+
const { getAbortSignal, withCancellationSignal } = require('./cancellation.js');
|
|
10
|
+
const {
|
|
11
|
+
cloneSnapshotPolicy,
|
|
12
|
+
cloneSnapshotKey,
|
|
13
|
+
encodeSnapshotPolicy,
|
|
14
|
+
resolveProgressLoadContext,
|
|
15
|
+
snapshotIdentity,
|
|
16
|
+
snapshotKeyDigest,
|
|
17
|
+
snapshotToken,
|
|
18
|
+
} = require('./policy.js');
|
|
19
|
+
const {
|
|
20
|
+
decodeStructured,
|
|
21
|
+
encodeResumePayloadCancel,
|
|
22
|
+
encodeResumePayloadError,
|
|
23
|
+
encodeResumePayloadValue,
|
|
24
|
+
} = require('./structured.js');
|
|
25
|
+
|
|
26
|
+
const SHARED_PROGRESS_REGISTRY_ROOT = path.join(
|
|
27
|
+
os.tmpdir(),
|
|
28
|
+
'mustard-progress-registry',
|
|
29
|
+
`${process.pid}-${Math.round(performance.timeOrigin)}`,
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
function sharedProgressSnapshotPath(snapshotIdentityValue) {
|
|
33
|
+
return path.join(SHARED_PROGRESS_REGISTRY_ROOT, snapshotIdentityValue);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function ensureSharedProgressRegistryRoot() {
|
|
37
|
+
fs.mkdirSync(SHARED_PROGRESS_REGISTRY_ROOT, { recursive: true });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isSharedProgressSnapshotUsed(snapshotIdentityValue) {
|
|
41
|
+
ensureSharedProgressRegistryRoot();
|
|
42
|
+
return fs.existsSync(sharedProgressSnapshotPath(snapshotIdentityValue));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function releaseSharedProgressSnapshot(snapshotIdentityValue) {
|
|
46
|
+
try {
|
|
47
|
+
fs.rmSync(sharedProgressSnapshotPath(snapshotIdentityValue));
|
|
48
|
+
} catch (error) {
|
|
49
|
+
if (error && typeof error === 'object' && error.code !== 'ENOENT') {
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function claimSharedProgressSnapshot(snapshotIdentityValue) {
|
|
56
|
+
ensureSharedProgressRegistryRoot();
|
|
57
|
+
try {
|
|
58
|
+
const fd = fs.openSync(sharedProgressSnapshotPath(snapshotIdentityValue), 'wx', 0o600);
|
|
59
|
+
fs.closeSync(fd);
|
|
60
|
+
return true;
|
|
61
|
+
} catch (error) {
|
|
62
|
+
if (error && typeof error === 'object' && error.code === 'EEXIST') {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function singleUseRuntimeError() {
|
|
70
|
+
return new MustardError(
|
|
71
|
+
'Runtime',
|
|
72
|
+
'Progress objects are single-use; this suspended execution was already resumed',
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function releaseClaimedSnapshot(native, snapshotIdentityValue) {
|
|
77
|
+
try {
|
|
78
|
+
callNative(native.releaseProgressSnapshot, snapshotIdentityValue);
|
|
79
|
+
} finally {
|
|
80
|
+
releaseSharedProgressSnapshot(snapshotIdentityValue);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function claimSnapshotForLoad(native, snapshotIdentityValue) {
|
|
85
|
+
if (!claimSharedProgressSnapshot(snapshotIdentityValue)) {
|
|
86
|
+
throw singleUseRuntimeError();
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
if (!callNative(native.claimProgressSnapshot, snapshotIdentityValue)) {
|
|
90
|
+
releaseSharedProgressSnapshot(snapshotIdentityValue);
|
|
91
|
+
throw singleUseRuntimeError();
|
|
92
|
+
}
|
|
93
|
+
} catch (error) {
|
|
94
|
+
if (!(error instanceof MustardError)) {
|
|
95
|
+
releaseSharedProgressSnapshot(snapshotIdentityValue);
|
|
96
|
+
}
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return () => {
|
|
101
|
+
releaseClaimedSnapshot(native, snapshotIdentityValue);
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function assertSnapshotNotUsed(native, snapshotIdentityValue) {
|
|
106
|
+
if (
|
|
107
|
+
isSharedProgressSnapshotUsed(snapshotIdentityValue) ||
|
|
108
|
+
callNative(native.isProgressSnapshotUsed, snapshotIdentityValue)
|
|
109
|
+
) {
|
|
110
|
+
throw singleUseRuntimeError();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function createProgressApi(native) {
|
|
115
|
+
class Progress {
|
|
116
|
+
constructor(
|
|
117
|
+
snapshot,
|
|
118
|
+
capability,
|
|
119
|
+
args,
|
|
120
|
+
policy,
|
|
121
|
+
snapshotKey,
|
|
122
|
+
token = undefined,
|
|
123
|
+
claimState = 'unclaimed',
|
|
124
|
+
) {
|
|
125
|
+
this.capability = capability;
|
|
126
|
+
this.args = args;
|
|
127
|
+
this.#snapshot = Buffer.from(snapshot);
|
|
128
|
+
this.#snapshotIdentity = snapshotIdentity(this.#snapshot);
|
|
129
|
+
this.#snapshotKey = cloneSnapshotKey(snapshotKey);
|
|
130
|
+
this.#snapshotKeyDigest = snapshotKeyDigest(this.#snapshotKey);
|
|
131
|
+
this.#snapshotToken = token ?? snapshotToken(this.#snapshot, this.#snapshotKey);
|
|
132
|
+
this.#policy = cloneSnapshotPolicy(policy);
|
|
133
|
+
this.#claimState = claimState;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
#snapshot;
|
|
137
|
+
#snapshotIdentity;
|
|
138
|
+
#snapshotKey;
|
|
139
|
+
#snapshotKeyDigest;
|
|
140
|
+
#snapshotToken;
|
|
141
|
+
#policy;
|
|
142
|
+
#claimState;
|
|
143
|
+
|
|
144
|
+
#consumeSnapshot() {
|
|
145
|
+
if (this.#claimState === 'consumed') {
|
|
146
|
+
throw singleUseRuntimeError();
|
|
147
|
+
}
|
|
148
|
+
if (this.#claimState === 'claimed') {
|
|
149
|
+
this.#claimState = 'consumed';
|
|
150
|
+
return Buffer.from(this.#snapshot);
|
|
151
|
+
}
|
|
152
|
+
if (!claimSharedProgressSnapshot(this.#snapshotIdentity)) {
|
|
153
|
+
throw singleUseRuntimeError();
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
if (!callNative(native.claimProgressSnapshot, this.#snapshotIdentity)) {
|
|
157
|
+
releaseSharedProgressSnapshot(this.#snapshotIdentity);
|
|
158
|
+
throw singleUseRuntimeError();
|
|
159
|
+
}
|
|
160
|
+
} catch (error) {
|
|
161
|
+
if (!(error instanceof MustardError)) {
|
|
162
|
+
releaseSharedProgressSnapshot(this.#snapshotIdentity);
|
|
163
|
+
}
|
|
164
|
+
throw error;
|
|
165
|
+
}
|
|
166
|
+
this.#claimState = 'consumed';
|
|
167
|
+
return Buffer.from(this.#snapshot);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
get snapshot() {
|
|
171
|
+
return Buffer.from(this.#snapshot);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
dump() {
|
|
175
|
+
return {
|
|
176
|
+
capability: this.capability,
|
|
177
|
+
args: this.args.slice(),
|
|
178
|
+
snapshot: this.snapshot,
|
|
179
|
+
snapshot_id: this.#snapshotIdentity,
|
|
180
|
+
snapshot_key_digest: this.#snapshotKeyDigest,
|
|
181
|
+
token: this.#snapshotToken,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
resume(value, options = undefined) {
|
|
186
|
+
const signal = getAbortSignal(options, 'resume options');
|
|
187
|
+
if (signal?.aborted) {
|
|
188
|
+
return this.cancel();
|
|
189
|
+
}
|
|
190
|
+
const payload = encodeResumePayloadValue(value);
|
|
191
|
+
const policyJson = encodeSnapshotPolicy(this.#policy, {
|
|
192
|
+
snapshotId: this.#snapshotIdentity,
|
|
193
|
+
snapshotKey: this.#snapshotKey,
|
|
194
|
+
snapshotToken: this.#snapshotToken,
|
|
195
|
+
});
|
|
196
|
+
const step = parseStep(
|
|
197
|
+
withCancellationSignal(
|
|
198
|
+
native,
|
|
199
|
+
native.resumeProgram,
|
|
200
|
+
[this.#consumeSnapshot(), payload, policyJson],
|
|
201
|
+
signal,
|
|
202
|
+
),
|
|
203
|
+
);
|
|
204
|
+
return materializeStep(step, this.#policy, this.#snapshotKey);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
resumeError(error, options = undefined) {
|
|
208
|
+
const signal = getAbortSignal(options, 'resume options');
|
|
209
|
+
if (signal?.aborted) {
|
|
210
|
+
return this.cancel();
|
|
211
|
+
}
|
|
212
|
+
const payload = encodeResumePayloadError(error);
|
|
213
|
+
const policyJson = encodeSnapshotPolicy(this.#policy, {
|
|
214
|
+
snapshotId: this.#snapshotIdentity,
|
|
215
|
+
snapshotKey: this.#snapshotKey,
|
|
216
|
+
snapshotToken: this.#snapshotToken,
|
|
217
|
+
});
|
|
218
|
+
const step = parseStep(
|
|
219
|
+
withCancellationSignal(
|
|
220
|
+
native,
|
|
221
|
+
native.resumeProgram,
|
|
222
|
+
[this.#consumeSnapshot(), payload, policyJson],
|
|
223
|
+
signal,
|
|
224
|
+
),
|
|
225
|
+
);
|
|
226
|
+
return materializeStep(step, this.#policy, this.#snapshotKey);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
cancel() {
|
|
230
|
+
const policyJson = encodeSnapshotPolicy(this.#policy, {
|
|
231
|
+
snapshotId: this.#snapshotIdentity,
|
|
232
|
+
snapshotKey: this.#snapshotKey,
|
|
233
|
+
snapshotToken: this.#snapshotToken,
|
|
234
|
+
});
|
|
235
|
+
const step = parseStep(
|
|
236
|
+
callNative(
|
|
237
|
+
native.resumeProgram,
|
|
238
|
+
this.#consumeSnapshot(),
|
|
239
|
+
encodeResumePayloadCancel(),
|
|
240
|
+
policyJson,
|
|
241
|
+
),
|
|
242
|
+
);
|
|
243
|
+
return materializeStep(step, this.#policy, this.#snapshotKey);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
static load(state, options = undefined) {
|
|
247
|
+
if (!state || typeof state !== 'object') {
|
|
248
|
+
throw new TypeError('Progress.load() expects a dumped progress object');
|
|
249
|
+
}
|
|
250
|
+
if (!state.snapshot) {
|
|
251
|
+
throw new TypeError('Progress.load() requires snapshot bytes');
|
|
252
|
+
}
|
|
253
|
+
if (typeof state.snapshot_id !== 'string' || state.snapshot_id.length === 0) {
|
|
254
|
+
throw new TypeError('Progress.load() requires dumped snapshot_id metadata');
|
|
255
|
+
}
|
|
256
|
+
if (
|
|
257
|
+
typeof state.snapshot_key_digest !== 'string' ||
|
|
258
|
+
state.snapshot_key_digest.length === 0
|
|
259
|
+
) {
|
|
260
|
+
throw new TypeError('Progress.load() requires dumped snapshot_key_digest metadata');
|
|
261
|
+
}
|
|
262
|
+
if (typeof state.token !== 'string' || state.token.length === 0) {
|
|
263
|
+
throw new TypeError('Progress.load() requires a dumped progress token');
|
|
264
|
+
}
|
|
265
|
+
const snapshot = Buffer.from(state.snapshot);
|
|
266
|
+
let snapshotIdentityValue;
|
|
267
|
+
try {
|
|
268
|
+
snapshotIdentityValue = snapshotIdentity(snapshot);
|
|
269
|
+
} catch (error) {
|
|
270
|
+
if (error instanceof MustardError && error.kind === 'Serialization') {
|
|
271
|
+
throw new MustardError(
|
|
272
|
+
'Serialization',
|
|
273
|
+
'Progress.load() rejected a tampered or unauthenticated snapshot',
|
|
274
|
+
error,
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
throw error;
|
|
278
|
+
}
|
|
279
|
+
if (state.snapshot_id !== snapshotIdentityValue) {
|
|
280
|
+
throw new MustardError(
|
|
281
|
+
'Serialization',
|
|
282
|
+
'Progress.load() rejected a tampered or unauthenticated snapshot',
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
assertSnapshotNotUsed(native, snapshotIdentityValue);
|
|
286
|
+
const context = resolveProgressLoadContext(state, snapshot, options);
|
|
287
|
+
const releaseClaim = claimSnapshotForLoad(native, snapshotIdentityValue);
|
|
288
|
+
try {
|
|
289
|
+
const inspection = parseSnapshotInspection(
|
|
290
|
+
callNative(
|
|
291
|
+
native.inspectSnapshot,
|
|
292
|
+
snapshot,
|
|
293
|
+
encodeSnapshotPolicy(context.policy, {
|
|
294
|
+
snapshotId: state.snapshot_id,
|
|
295
|
+
snapshotKey: context.snapshotKey,
|
|
296
|
+
snapshotToken: state.token,
|
|
297
|
+
}),
|
|
298
|
+
),
|
|
299
|
+
);
|
|
300
|
+
return new Progress(
|
|
301
|
+
snapshot,
|
|
302
|
+
inspection.capability,
|
|
303
|
+
inspection.args,
|
|
304
|
+
context.policy,
|
|
305
|
+
context.snapshotKey,
|
|
306
|
+
state.token,
|
|
307
|
+
'claimed',
|
|
308
|
+
);
|
|
309
|
+
} catch (error) {
|
|
310
|
+
releaseClaim();
|
|
311
|
+
throw error;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function parseStep(stepJson) {
|
|
317
|
+
const step = JSON.parse(stepJson);
|
|
318
|
+
if (step.type === 'completed') {
|
|
319
|
+
return {
|
|
320
|
+
type: 'completed',
|
|
321
|
+
value: decodeStructured(step.value),
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
return {
|
|
325
|
+
type: 'suspended',
|
|
326
|
+
capability: step.capability,
|
|
327
|
+
args: step.args.map(decodeStructured),
|
|
328
|
+
snapshot: Buffer.from(step.snapshot_base64, 'base64'),
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function parseSnapshotInspection(inspectionJson) {
|
|
333
|
+
const inspection = JSON.parse(inspectionJson);
|
|
334
|
+
return {
|
|
335
|
+
capability: inspection.capability,
|
|
336
|
+
args: inspection.args.map(decodeStructured),
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function materializeStep(step, policy, snapshotKey) {
|
|
341
|
+
if (step.type === 'completed') {
|
|
342
|
+
return step.value;
|
|
343
|
+
}
|
|
344
|
+
return new Progress(step.snapshot, step.capability, step.args, policy, snapshotKey);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return {
|
|
348
|
+
Progress,
|
|
349
|
+
materializeStep,
|
|
350
|
+
parseStep,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
module.exports = {
|
|
355
|
+
createProgressApi,
|
|
356
|
+
};
|