poe-code 14.0.5 → 14.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1604 -255
- package/dist/index.js.map +4 -4
- package/dist/metafile.json +1 -1
- package/package.json +1 -1
- package/packages/safe-js/dist/chunks/{chunk-B7H4ZL65.js → chunk-BWVASPJX.js} +2099 -783
- package/packages/safe-js/dist/chunks/chunk-BWVASPJX.js.map +7 -0
- package/packages/safe-js/dist/chunks/{chunk-7B7BKWVF.js → chunk-ETKM3KPA.js} +2 -2
- package/packages/safe-js/dist/cli.js +2 -2
- package/packages/safe-js/dist/core.d.ts +3 -0
- package/packages/safe-js/dist/core.js +5 -1
- package/packages/safe-js/dist/extensions.d.ts +44 -0
- package/packages/safe-js/dist/extensions.js +83 -0
- package/packages/safe-js/dist/index.d.ts +4 -1
- package/packages/safe-js/dist/index.js +6 -2
- package/packages/safe-js/dist/index.js.map +2 -2
- package/packages/safe-js/dist/interp/async.d.ts +1 -0
- package/packages/safe-js/dist/interp/date.d.ts +19 -0
- package/packages/safe-js/dist/interp/date.js +129 -0
- package/packages/safe-js/dist/interp/globals/console-json.js +5 -1
- package/packages/safe-js/dist/interp/globals/date.d.ts +10 -0
- package/packages/safe-js/dist/interp/globals/date.js +95 -0
- package/packages/safe-js/dist/interp/globals/object-array.d.ts +2 -1
- package/packages/safe-js/dist/interp/globals/object-array.js +15 -1
- package/packages/safe-js/dist/interp/globals.d.ts +34 -0
- package/packages/safe-js/dist/interp/globals.js +24 -0
- package/packages/safe-js/dist/interp/host-bridge.d.ts +18 -1
- package/packages/safe-js/dist/interp/host-bridge.js +39 -5
- package/packages/safe-js/dist/interp/host-call.js +4 -0
- package/packages/safe-js/dist/interp/host-capabilities.d.ts +50 -0
- package/packages/safe-js/dist/interp/host-capabilities.js +151 -0
- package/packages/safe-js/dist/interp/interpreter.d.ts +4 -0
- package/packages/safe-js/dist/interp/interpreter.js +41 -6
- package/packages/safe-js/dist/interp/object-model.js +7 -0
- package/packages/safe-js/dist/interp/string-coercion.js +3 -0
- package/packages/safe-js/dist/interp/values.d.ts +2 -1
- package/packages/safe-js/dist/interp/values.js +36 -0
- package/packages/safe-js/dist/lint/rules/known-globals.d.ts +1 -1
- package/packages/safe-js/dist/lint/rules/known-globals.js +1 -0
- package/packages/safe-js/dist/modules/registry.d.ts +3 -1
- package/packages/safe-js/dist/modules/registry.js +2 -1
- package/packages/safe-js/dist/parse/parser.js +4 -2
- package/packages/safe-js/dist/realm.d.ts +46 -0
- package/packages/safe-js/dist/realm.js +779 -0
- package/packages/safe-js/dist/run.d.ts +7 -1
- package/packages/safe-js/dist/run.js +6 -35
- package/packages/safe-js/dist/snapshot/dump-format.js +8 -3
- package/packages/safe-js/dist/snapshot/replay-data.d.ts +4 -1
- package/packages/safe-js/dist/snapshot/replay-data.js +11 -0
- package/packages/safe-js/dist/snapshot/restore.js +6 -0
- package/packages/safe-js/dist/snapshot/serialize.d.ts +5 -2
- package/packages/safe-js/dist/snapshot/serialize.js +8 -2
- package/packages/safe-js/dist/snapshot/validation.js +18 -1
- package/packages/safe-js/dist/chunks/chunk-B7H4ZL65.js.map +0 -7
- /package/packages/safe-js/dist/chunks/{chunk-7B7BKWVF.js.map → chunk-ETKM3KPA.js.map} +0 -0
|
@@ -0,0 +1,779 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import { types } from "node:util";
|
|
3
|
+
import { Budget, SandboxError } from "./interp/budget.js";
|
|
4
|
+
import { CompileScope } from "./interp/regex/compile-guard.js";
|
|
5
|
+
import { createBuiltinBindings } from "./interp/globals.js";
|
|
6
|
+
import { interpret, Scope } from "./interp/interpreter.js";
|
|
7
|
+
import { SandboxJobQueue, runAsyncPrefix, suspendJob } from "./interp/jobs.js";
|
|
8
|
+
import { withCancellationSignal, awaitSandboxValue } from "./interp/cancel.js";
|
|
9
|
+
import { enterRunningState } from "./interp/running-state.js";
|
|
10
|
+
import { runResources } from "./interp/resources.js";
|
|
11
|
+
import { createSandboxPromiseRejectionTracker, withSandboxPromiseRejectionTracker } from "./interp/promise-tracker.js";
|
|
12
|
+
import { copyHostValueToSandbox, wrapCallerInjectedBindings } from "./interp/host-bridge.js";
|
|
13
|
+
import { createLiveHostObject, createGuestReference, exportHostCapability, readGuestReference, readGuestCallback, registerGuestCallback, revokeGuestCallback, revokeHostObject, revokeGuestReference } from "./interp/host-capabilities.js";
|
|
14
|
+
import { deepCopyFromSandbox, isSandboxClosure, measureSandboxData, reconcileCompiledValues } from "./interp/values.js";
|
|
15
|
+
import { getExtensionSetup, readDataRecord, readStringList } from "./extensions.js";
|
|
16
|
+
import { resolveModuleImports } from "./modules/registry.js";
|
|
17
|
+
import { parseExecutableModule } from "./parse/parser.js";
|
|
18
|
+
import { createReplayableRandom } from "./random.js";
|
|
19
|
+
import { hashSource } from "./parse/hash.js";
|
|
20
|
+
import { describeThrownValue } from "./error/shape.js";
|
|
21
|
+
import { encodeReplayData } from "./snapshot/replay-data.js";
|
|
22
|
+
class RealmState {
|
|
23
|
+
options;
|
|
24
|
+
budget;
|
|
25
|
+
lease;
|
|
26
|
+
compilation;
|
|
27
|
+
controller = new AbortController();
|
|
28
|
+
phase = new AsyncLocalStorage();
|
|
29
|
+
queue = new SandboxJobQueue();
|
|
30
|
+
tracker = createSandboxPromiseRejectionTracker();
|
|
31
|
+
bridge;
|
|
32
|
+
limits;
|
|
33
|
+
extensions;
|
|
34
|
+
cleanups = [];
|
|
35
|
+
callbacks = new Map();
|
|
36
|
+
pendingCallbacks = new Set();
|
|
37
|
+
callbackCache = new WeakMap();
|
|
38
|
+
hostObjects = new Set();
|
|
39
|
+
guestReferences = new Map();
|
|
40
|
+
retainedOperations = new WeakMap();
|
|
41
|
+
nestedOperations = new WeakMap();
|
|
42
|
+
convertedModules = new Map();
|
|
43
|
+
nativeConversions = { seen: new WeakMap() };
|
|
44
|
+
modules;
|
|
45
|
+
globals;
|
|
46
|
+
builtinBindings;
|
|
47
|
+
scope;
|
|
48
|
+
active;
|
|
49
|
+
disposal;
|
|
50
|
+
closed = false;
|
|
51
|
+
initialized = false;
|
|
52
|
+
nestedDepth = 0;
|
|
53
|
+
failure;
|
|
54
|
+
constructor(options) {
|
|
55
|
+
this.options = options;
|
|
56
|
+
const limitInput = readDataRecord(options.limits ?? {}, "Realm limits");
|
|
57
|
+
this.limits = {
|
|
58
|
+
extensions: 32,
|
|
59
|
+
hostObjects: 1024,
|
|
60
|
+
callbacks: 1024,
|
|
61
|
+
guestReferences: 1024,
|
|
62
|
+
cleanups: 1024,
|
|
63
|
+
nestedEvaluations: 16
|
|
64
|
+
};
|
|
65
|
+
for (const [name, value] of Object.entries(limitInput)) {
|
|
66
|
+
if (!Object.hasOwn(this.limits, name) || !Number.isSafeInteger(value) || Number(value) < 1)
|
|
67
|
+
throw new TypeError("Realm limits must be positive safe integers with supported names.");
|
|
68
|
+
this.limits[name] = Number(value);
|
|
69
|
+
}
|
|
70
|
+
if (options.extensions !== undefined &&
|
|
71
|
+
(!Array.isArray(options.extensions) || types.isProxy(options.extensions)))
|
|
72
|
+
throw new TypeError("Extensions must be a registration array.");
|
|
73
|
+
const registrations = options.extensions ?? [];
|
|
74
|
+
const extensions = [];
|
|
75
|
+
if (registrations.length > this.limits.extensions)
|
|
76
|
+
throw new RangeError("Realm extension limit exceeded.");
|
|
77
|
+
for (let index = 0; index < registrations.length; index++) {
|
|
78
|
+
const descriptor = Object.getOwnPropertyDescriptor(registrations, String(index));
|
|
79
|
+
if (descriptor === undefined || !("value" in descriptor))
|
|
80
|
+
throw new TypeError("Extension registrations require data properties, not accessors.");
|
|
81
|
+
extensions.push(descriptor.value);
|
|
82
|
+
}
|
|
83
|
+
if (Reflect.ownKeys(registrations).length !== extensions.length + 1)
|
|
84
|
+
throw new TypeError("Extension registrations have unsupported fields.");
|
|
85
|
+
this.extensions = Object.freeze(extensions);
|
|
86
|
+
if (this.extensions.length > this.limits.extensions)
|
|
87
|
+
throw new RangeError("Realm extension limit exceeded.");
|
|
88
|
+
this.globals = readDataRecord(options.bindings ?? {}, "Realm bindings");
|
|
89
|
+
this.modules = readModules(options.modules);
|
|
90
|
+
const grants = new Set(readStringList(options.grants ?? [], "Realm grants"));
|
|
91
|
+
for (const extension of this.extensions)
|
|
92
|
+
getExtensionSetup(extension);
|
|
93
|
+
this.budget = options.budget ?? new Budget({ maxCallDepth: 1000 });
|
|
94
|
+
this.lease = this.budget.acquireCompileOwner(true);
|
|
95
|
+
this.compilation = new CompileScope(this.lease.owner);
|
|
96
|
+
this.bridge = {
|
|
97
|
+
owner: this,
|
|
98
|
+
assertActive: this.assertOpen,
|
|
99
|
+
wrapCallback: this.wrapCallback,
|
|
100
|
+
captureArguments: this.captureArguments,
|
|
101
|
+
invoke: this.invokeHost,
|
|
102
|
+
awaitResult: (operation) => this.nestedOperations.has(operation)
|
|
103
|
+
};
|
|
104
|
+
try {
|
|
105
|
+
this.builtinBindings = createBuiltinBindings({
|
|
106
|
+
budget: this.budget,
|
|
107
|
+
compileOwner: this.lease.owner,
|
|
108
|
+
sink: options.sink,
|
|
109
|
+
clock: options.clock,
|
|
110
|
+
random: createReplayableRandom({ seed: options.randomSeed }).next
|
|
111
|
+
});
|
|
112
|
+
const names = new Set();
|
|
113
|
+
const globals = new Set([...Object.keys(this.builtinBindings), ...Object.keys(this.globals)]);
|
|
114
|
+
const modules = new Map(Object.entries(this.modules).map(([name, exports]) => [name, new Set(Object.keys(exports))]));
|
|
115
|
+
for (const extension of this.extensions) {
|
|
116
|
+
const manifest = extension.manifest;
|
|
117
|
+
if (names.has(manifest.name))
|
|
118
|
+
throw new TypeError(`Duplicate extension '${manifest.name}'.`);
|
|
119
|
+
names.add(manifest.name);
|
|
120
|
+
for (const capability of manifest.capabilities ?? []) {
|
|
121
|
+
if (!grants.has(capability))
|
|
122
|
+
throw new TypeError(`Missing grant '${capability}' for extension '${manifest.name}'.`);
|
|
123
|
+
}
|
|
124
|
+
for (const name of manifest.globals ?? []) {
|
|
125
|
+
if (globals.has(name))
|
|
126
|
+
throw new TypeError(`Conflicting global '${name}'.`);
|
|
127
|
+
globals.add(name);
|
|
128
|
+
}
|
|
129
|
+
for (const [name, exports] of Object.entries(manifest.modules ?? {})) {
|
|
130
|
+
const occupied = modules.get(name) ?? new Set();
|
|
131
|
+
for (const key of exports) {
|
|
132
|
+
if (occupied.has(key))
|
|
133
|
+
throw new TypeError(`Conflicting export '${name}.${key}'.`);
|
|
134
|
+
occupied.add(key);
|
|
135
|
+
}
|
|
136
|
+
modules.set(name, occupied);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
options.signal?.addEventListener("abort", this.abort, { once: true });
|
|
140
|
+
if (options.signal?.aborted)
|
|
141
|
+
this.abort();
|
|
142
|
+
this.budget.setRetainedValues(this, this.retainedRoots);
|
|
143
|
+
this.tracker.onFatalRejection((error) => this.poison(error));
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
this.compilation.dispose();
|
|
147
|
+
this.lease.release();
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
bridgeOptions = () => ({
|
|
152
|
+
budget: this.budget,
|
|
153
|
+
compileOwner: this.lease.owner,
|
|
154
|
+
signal: this.controller.signal,
|
|
155
|
+
realm: this.bridge
|
|
156
|
+
});
|
|
157
|
+
retainedRoots = () => [
|
|
158
|
+
...this.callbacks.values(),
|
|
159
|
+
...Array.from(this.pendingCallbacks, (pending) => pending.closure),
|
|
160
|
+
...this.guestReferences.values()
|
|
161
|
+
];
|
|
162
|
+
captureArguments = (operation, args, copy) => {
|
|
163
|
+
const from = this.retainedOperations.get(operation)?.from ?? args.length;
|
|
164
|
+
const values = copy(args.slice(0, from));
|
|
165
|
+
const captured = [];
|
|
166
|
+
const rollback = () => {
|
|
167
|
+
for (const reference of captured) {
|
|
168
|
+
revokeGuestReference(reference, this);
|
|
169
|
+
this.guestReferences.delete(reference);
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
try {
|
|
173
|
+
for (const value of args.slice(from)) {
|
|
174
|
+
this.checkCollection(this.guestReferences.size + 1, this.limits.guestReferences, "guest reference");
|
|
175
|
+
const root = [value];
|
|
176
|
+
const reference = createGuestReference(root, this, this.assertOpen);
|
|
177
|
+
this.guestReferences.set(reference, root);
|
|
178
|
+
captured.push(reference);
|
|
179
|
+
values.push(reference);
|
|
180
|
+
}
|
|
181
|
+
if (captured.length > 0)
|
|
182
|
+
this.budget.reconcileDataUsage(measureSandboxData([...(this.scope?.retainedValues() ?? []), ...this.retainedRoots()]));
|
|
183
|
+
return { args: values, rollback };
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
rollback();
|
|
187
|
+
if (error instanceof SandboxError)
|
|
188
|
+
this.poison(error);
|
|
189
|
+
throw error;
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
releaseGuestReference = (reference) => {
|
|
193
|
+
readGuestReference(reference, this);
|
|
194
|
+
revokeGuestReference(reference, this);
|
|
195
|
+
this.guestReferences.delete(reference);
|
|
196
|
+
if (this.active === undefined)
|
|
197
|
+
reconcileCompiledValues(this.budget, [...(this.scope?.retainedValues() ?? []), ...this.retainedRoots()], this.compilation);
|
|
198
|
+
};
|
|
199
|
+
assertOpen = () => {
|
|
200
|
+
if (this.failure !== undefined)
|
|
201
|
+
throw this.failure.reason;
|
|
202
|
+
if (this.closed)
|
|
203
|
+
throw new Error("SafeJS realm is closed; capabilities are revoked.");
|
|
204
|
+
this.controller.signal.throwIfAborted();
|
|
205
|
+
};
|
|
206
|
+
abort = () => {
|
|
207
|
+
this.poison(this.options.signal?.reason ?? new SandboxError("aborted"));
|
|
208
|
+
void this.close().catch(() => undefined);
|
|
209
|
+
};
|
|
210
|
+
poison(reason) {
|
|
211
|
+
this.failure ??= { reason };
|
|
212
|
+
this.controller.abort(reason);
|
|
213
|
+
if (this.active === undefined)
|
|
214
|
+
queueMicrotask(() => {
|
|
215
|
+
void this.dispose().catch(() => undefined);
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
chargeWork = (units = 1) => {
|
|
219
|
+
this.assertOpen();
|
|
220
|
+
if (!Number.isSafeInteger(units) || units < 0)
|
|
221
|
+
throw new TypeError("Work charges must be non-negative safe integers.");
|
|
222
|
+
try {
|
|
223
|
+
for (let index = 0; index < units; index++)
|
|
224
|
+
this.budget.visitNode();
|
|
225
|
+
}
|
|
226
|
+
catch (error) {
|
|
227
|
+
this.poison(error);
|
|
228
|
+
throw error;
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
onCleanup = (cleanup) => {
|
|
232
|
+
this.assertOpen();
|
|
233
|
+
if (typeof cleanup !== "function")
|
|
234
|
+
throw new TypeError("Cleanup must be a function.");
|
|
235
|
+
this.checkCollection(this.cleanups.length + 1, this.limits.cleanups, "cleanup");
|
|
236
|
+
this.cleanups.push(cleanup);
|
|
237
|
+
};
|
|
238
|
+
checkCollection(count, limit, name) {
|
|
239
|
+
this.assertOpen();
|
|
240
|
+
try {
|
|
241
|
+
this.budget.allocateCollectionEntries(count);
|
|
242
|
+
}
|
|
243
|
+
catch (error) {
|
|
244
|
+
this.poison(error);
|
|
245
|
+
throw error;
|
|
246
|
+
}
|
|
247
|
+
if (count > limit)
|
|
248
|
+
throw new RangeError(`Realm ${name} limit exceeded.`);
|
|
249
|
+
}
|
|
250
|
+
invokeHost = (operation, call) => {
|
|
251
|
+
this.assertOpen();
|
|
252
|
+
const phase = {
|
|
253
|
+
active: true,
|
|
254
|
+
extension: this.nestedOperations.get(operation),
|
|
255
|
+
evaluating: false,
|
|
256
|
+
pending: new Set()
|
|
257
|
+
};
|
|
258
|
+
return this.phase.run(phase, () => {
|
|
259
|
+
try {
|
|
260
|
+
const result = call();
|
|
261
|
+
if (types.isPromise(result) || phase.pending.size > 0 || phase.failure !== undefined) {
|
|
262
|
+
return Promise.resolve(result)
|
|
263
|
+
.then(async (value) => {
|
|
264
|
+
await Promise.allSettled(phase.pending);
|
|
265
|
+
if (phase.failure !== undefined)
|
|
266
|
+
throw phase.failure.reason;
|
|
267
|
+
this.assertOpen();
|
|
268
|
+
return value;
|
|
269
|
+
}, async (error) => {
|
|
270
|
+
if (error instanceof SandboxError || phase.pending.size > 0)
|
|
271
|
+
this.poison(error);
|
|
272
|
+
await Promise.allSettled(phase.pending);
|
|
273
|
+
throw error;
|
|
274
|
+
})
|
|
275
|
+
.finally(() => {
|
|
276
|
+
phase.active = false;
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
this.assertOpen();
|
|
280
|
+
phase.active = false;
|
|
281
|
+
return result;
|
|
282
|
+
}
|
|
283
|
+
catch (error) {
|
|
284
|
+
phase.active = false;
|
|
285
|
+
if (error instanceof SandboxError)
|
|
286
|
+
this.poison(error);
|
|
287
|
+
if (phase.pending.size > 0) {
|
|
288
|
+
this.poison(error);
|
|
289
|
+
return Promise.allSettled(phase.pending).then(() => {
|
|
290
|
+
throw error;
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
throw error;
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
};
|
|
297
|
+
importValue(value) {
|
|
298
|
+
return copyHostValueToSandbox(value, [], this.bridgeOptions(), { seen: new WeakMap() }, "<realm>");
|
|
299
|
+
}
|
|
300
|
+
exportValue(value) {
|
|
301
|
+
return deepCopyFromSandbox(value, {
|
|
302
|
+
compilation: this.compilation,
|
|
303
|
+
wrapClosure: this.wrapCallback,
|
|
304
|
+
unwrapHostObject: (object) => exportHostCapability(object, this)
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
createHostObject = (definition) => {
|
|
308
|
+
this.checkCollection(this.hostObjects.size + 1, this.limits.hostObjects, "host object");
|
|
309
|
+
const object = createLiveHostObject(definition, {
|
|
310
|
+
owner: this,
|
|
311
|
+
assertActive: this.assertOpen,
|
|
312
|
+
chargeWork: this.chargeWork,
|
|
313
|
+
read: (operation) => {
|
|
314
|
+
const value = this.invokeHost(operation, operation);
|
|
315
|
+
if (types.isPromise(value)) {
|
|
316
|
+
void Promise.resolve(value).catch(() => undefined);
|
|
317
|
+
throw new TypeError("Live property getters must be synchronous.");
|
|
318
|
+
}
|
|
319
|
+
return this.importValue(value);
|
|
320
|
+
},
|
|
321
|
+
write: (operation, value) => {
|
|
322
|
+
const result = this.invokeHost(operation, () => operation(this.exportValue(value)));
|
|
323
|
+
if (types.isPromise(result)) {
|
|
324
|
+
void Promise.resolve(result).catch(() => undefined);
|
|
325
|
+
throw new TypeError("Live property setters must be synchronous.");
|
|
326
|
+
}
|
|
327
|
+
},
|
|
328
|
+
method: (operation) => {
|
|
329
|
+
const value = copyHostValueToSandbox(operation, [], this.bridgeOptions(), this.nativeConversions, "<host-method>");
|
|
330
|
+
if (!isSandboxClosure(value))
|
|
331
|
+
throw new TypeError("Invalid host method.");
|
|
332
|
+
return value;
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
this.hostObjects.add(object);
|
|
336
|
+
try {
|
|
337
|
+
this.budget.chargeDataUsage(1);
|
|
338
|
+
}
|
|
339
|
+
catch (error) {
|
|
340
|
+
this.poison(error);
|
|
341
|
+
throw error;
|
|
342
|
+
}
|
|
343
|
+
return object;
|
|
344
|
+
};
|
|
345
|
+
wrapCallback = (closure) => {
|
|
346
|
+
this.assertOpen();
|
|
347
|
+
const existing = this.callbackCache.get(closure);
|
|
348
|
+
if (existing !== undefined && this.callbacks.has(existing))
|
|
349
|
+
return existing;
|
|
350
|
+
this.checkCollection(this.callbacks.size + 1, this.limits.callbacks, "callback");
|
|
351
|
+
const invokeCallback = this.invokeCallback;
|
|
352
|
+
const callback = function (...args) {
|
|
353
|
+
return invokeCallback(callback, { args, thisValue: this });
|
|
354
|
+
};
|
|
355
|
+
this.callbacks.set(callback, closure);
|
|
356
|
+
this.callbackCache.set(closure, callback);
|
|
357
|
+
registerGuestCallback(callback, {
|
|
358
|
+
owner: this,
|
|
359
|
+
closure,
|
|
360
|
+
assertActive: () => {
|
|
361
|
+
this.assertOpen();
|
|
362
|
+
if (!this.callbacks.has(callback))
|
|
363
|
+
throw new TypeError("Guest callback is revoked.");
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
try {
|
|
367
|
+
this.budget.reconcileDataUsage(measureSandboxData([...(this.scope?.retainedValues() ?? []), ...this.retainedRoots()]));
|
|
368
|
+
}
|
|
369
|
+
catch (error) {
|
|
370
|
+
this.poison(error);
|
|
371
|
+
throw error;
|
|
372
|
+
}
|
|
373
|
+
return callback;
|
|
374
|
+
};
|
|
375
|
+
releaseCallback = (callback) => {
|
|
376
|
+
readGuestCallback(callback, this);
|
|
377
|
+
revokeGuestCallback(callback, this);
|
|
378
|
+
this.callbacks.delete(callback);
|
|
379
|
+
if (this.active === undefined)
|
|
380
|
+
reconcileCompiledValues(this.budget, [...(this.scope?.retainedValues() ?? []), ...this.retainedRoots()], this.compilation);
|
|
381
|
+
};
|
|
382
|
+
invokeCallback = async (callback, options = {}) => {
|
|
383
|
+
if (this.closed || this.failure !== undefined)
|
|
384
|
+
await this.dispose();
|
|
385
|
+
this.assertOpen();
|
|
386
|
+
const closure = readGuestCallback(callback, this);
|
|
387
|
+
this.checkCollection(this.pendingCallbacks.size + 1, this.limits.callbacks, "pending callback");
|
|
388
|
+
const record = { closure };
|
|
389
|
+
this.pendingCallbacks.add(record);
|
|
390
|
+
const invoke = async () => {
|
|
391
|
+
this.assertOpen();
|
|
392
|
+
readGuestCallback(callback, this);
|
|
393
|
+
const leave = enterRunningState(closure);
|
|
394
|
+
const leaveCall = this.budget.enterCall();
|
|
395
|
+
try {
|
|
396
|
+
const values = this.importValue([
|
|
397
|
+
options.thisValue,
|
|
398
|
+
[...(options.args ?? [])]
|
|
399
|
+
]);
|
|
400
|
+
const value = await closure.call(values[1], {
|
|
401
|
+
thisValue: values[0],
|
|
402
|
+
compilation: this.compilation,
|
|
403
|
+
stack: []
|
|
404
|
+
});
|
|
405
|
+
const settled = await suspendJob(awaitSandboxValue(value, this.controller.signal, this.budget));
|
|
406
|
+
return this.exportValue(settled);
|
|
407
|
+
}
|
|
408
|
+
finally {
|
|
409
|
+
leaveCall();
|
|
410
|
+
leave();
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
try {
|
|
414
|
+
if (this.active !== undefined) {
|
|
415
|
+
record.promise = withSandboxPromiseRejectionTracker(this.tracker, () => runResources.run({ signal: this.controller.signal, add: this.onCleanup }, () => withCancellationSignal(this.controller.signal, () => this.phase.getStore()?.active ? runAsyncPrefix(invoke) : this.queue.run(invoke))));
|
|
416
|
+
}
|
|
417
|
+
else {
|
|
418
|
+
record.promise = this.perform(() => this.queue.run(invoke));
|
|
419
|
+
}
|
|
420
|
+
return await record.promise;
|
|
421
|
+
}
|
|
422
|
+
catch (error) {
|
|
423
|
+
if (error instanceof SandboxError)
|
|
424
|
+
this.poison(error);
|
|
425
|
+
throw error;
|
|
426
|
+
}
|
|
427
|
+
finally {
|
|
428
|
+
this.pendingCallbacks.delete(record);
|
|
429
|
+
if (!this.closed && this.active === undefined)
|
|
430
|
+
reconcileCompiledValues(this.budget, [...(this.scope?.retainedValues() ?? []), ...this.retainedRoots()], this.compilation);
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
initialize() {
|
|
434
|
+
if (this.initialized)
|
|
435
|
+
return;
|
|
436
|
+
this.assertOpen();
|
|
437
|
+
this.initialized = true;
|
|
438
|
+
for (const extension of this.extensions) {
|
|
439
|
+
const context = Object.freeze({
|
|
440
|
+
signal: this.controller.signal,
|
|
441
|
+
onCleanup: this.onCleanup,
|
|
442
|
+
chargeWork: this.chargeWork,
|
|
443
|
+
createHostObject: this.createHostObject,
|
|
444
|
+
invokeCallback: this.invokeCallback,
|
|
445
|
+
releaseCallback: this.releaseCallback,
|
|
446
|
+
releaseGuestReference: this.releaseGuestReference,
|
|
447
|
+
retainGuestArguments: (operation, from) => {
|
|
448
|
+
this.assertOpen();
|
|
449
|
+
if (!extension.manifest.capabilities?.includes("guest:retain"))
|
|
450
|
+
throw new TypeError("Retaining arguments requires the guest:retain grant.");
|
|
451
|
+
if (typeof operation !== "function")
|
|
452
|
+
throw new TypeError("Retained operation must be a function.");
|
|
453
|
+
if (!Number.isSafeInteger(from) || from < 0)
|
|
454
|
+
throw new TypeError("Argument index must be a non-negative safe integer.");
|
|
455
|
+
if (this.scope !== undefined)
|
|
456
|
+
throw new TypeError("Retained operations must be registered during setup.");
|
|
457
|
+
const previous = this.retainedOperations.get(operation);
|
|
458
|
+
if (previous !== undefined &&
|
|
459
|
+
(previous.extension !== extension || previous.from !== from))
|
|
460
|
+
throw new TypeError("Conflicting retained operation declaration.");
|
|
461
|
+
this.retainedOperations.set(operation, { extension, from });
|
|
462
|
+
return operation;
|
|
463
|
+
},
|
|
464
|
+
nestedOperation: (operation) => {
|
|
465
|
+
this.assertOpen();
|
|
466
|
+
if (!extension.manifest.capabilities?.includes("source:nested"))
|
|
467
|
+
throw new TypeError("Nested source requires the source:nested grant.");
|
|
468
|
+
if (typeof operation !== "function")
|
|
469
|
+
throw new TypeError("Nested operation must be a function.");
|
|
470
|
+
if (this.scope !== undefined)
|
|
471
|
+
throw new TypeError("Nested operations must be registered during setup.");
|
|
472
|
+
const owner = this.nestedOperations.get(operation);
|
|
473
|
+
if (owner !== undefined && owner !== extension)
|
|
474
|
+
throw new TypeError("Nested operation already belongs to another extension.");
|
|
475
|
+
this.nestedOperations.set(operation, extension);
|
|
476
|
+
return operation;
|
|
477
|
+
},
|
|
478
|
+
evaluateNested: (source) => {
|
|
479
|
+
const phase = this.phase.getStore();
|
|
480
|
+
if (!extension.manifest.capabilities?.includes("source:nested") ||
|
|
481
|
+
!phase?.active ||
|
|
482
|
+
phase.extension !== extension ||
|
|
483
|
+
phase.evaluating) {
|
|
484
|
+
const error = new SandboxError("reentry");
|
|
485
|
+
this.poison(error);
|
|
486
|
+
const rejected = Promise.reject(error);
|
|
487
|
+
void rejected.catch(() => undefined);
|
|
488
|
+
return rejected;
|
|
489
|
+
}
|
|
490
|
+
const pending = this.evaluateNested(source, extension);
|
|
491
|
+
phase.pending.add(pending);
|
|
492
|
+
void pending.then(() => {
|
|
493
|
+
phase.pending.delete(pending);
|
|
494
|
+
}, (reason) => {
|
|
495
|
+
phase.pending.delete(pending);
|
|
496
|
+
phase.failure ??= { reason };
|
|
497
|
+
});
|
|
498
|
+
return pending;
|
|
499
|
+
}
|
|
500
|
+
});
|
|
501
|
+
const output = getExtensionSetup(extension)(context);
|
|
502
|
+
if (types.isPromise(output)) {
|
|
503
|
+
void Promise.resolve(output).catch(() => undefined);
|
|
504
|
+
throw new TypeError("Extension setup must be synchronous.");
|
|
505
|
+
}
|
|
506
|
+
const exports = readDataRecord(output, "Extension exports");
|
|
507
|
+
if (Object.keys(exports).some((key) => key !== "globals" && key !== "modules"))
|
|
508
|
+
throw new TypeError("Unknown extension export field.");
|
|
509
|
+
const globals = readDataRecord(exports.globals ?? {}, "Extension globals");
|
|
510
|
+
const modules = readModules(exports.modules);
|
|
511
|
+
assertNames(Object.keys(globals), extension.manifest.globals ?? [], "global");
|
|
512
|
+
assertNames(Object.keys(modules), Object.keys(extension.manifest.modules ?? {}), "module");
|
|
513
|
+
for (const [name, values] of Object.entries(modules)) {
|
|
514
|
+
assertNames(Object.keys(values), extension.manifest.modules?.[name] ?? [], "module export");
|
|
515
|
+
this.modules[name] ??= Object.create(null);
|
|
516
|
+
Object.assign(this.modules[name], values);
|
|
517
|
+
}
|
|
518
|
+
Object.assign(this.globals, globals);
|
|
519
|
+
}
|
|
520
|
+
const bindings = wrapCallerInjectedBindings(this.globals, this.bridgeOptions());
|
|
521
|
+
this.scope = new Scope(this.builtinBindings, undefined, undefined, { chargeData: false }).child(bindings, { functionBoundary: true });
|
|
522
|
+
}
|
|
523
|
+
async evaluateRaw(source, filename = "<realm>", nested = false) {
|
|
524
|
+
this.assertOpen();
|
|
525
|
+
if (typeof source !== "string")
|
|
526
|
+
throw new TypeError("Realm source must be a string.");
|
|
527
|
+
const module = parseExecutableModule(source, filename, this.lease.owner);
|
|
528
|
+
this.initialize();
|
|
529
|
+
const imports = resolveModuleImports(module, this.modules, {
|
|
530
|
+
...this.bridgeOptions(),
|
|
531
|
+
wrappedModules: this.convertedModules
|
|
532
|
+
});
|
|
533
|
+
for (const [name, value] of Object.entries(imports)) {
|
|
534
|
+
const binding = this.scope.lookup(name);
|
|
535
|
+
if (!binding.found)
|
|
536
|
+
this.scope.declare(name, "const", value);
|
|
537
|
+
else if (binding.value !== value)
|
|
538
|
+
throw new TypeError(`Conflicting import '${name}'.`);
|
|
539
|
+
}
|
|
540
|
+
const result = await interpret({
|
|
541
|
+
type: "BlockStatement",
|
|
542
|
+
body: module.body.filter((statement) => statement.type !== "ImportDeclaration"),
|
|
543
|
+
span: module.span
|
|
544
|
+
}, {
|
|
545
|
+
scope: this.scope,
|
|
546
|
+
useScopeDirectly: true,
|
|
547
|
+
budget: this.budget,
|
|
548
|
+
compilation: this.compilation,
|
|
549
|
+
signal: this.controller.signal,
|
|
550
|
+
surfaceUnhandledThrows: true,
|
|
551
|
+
jobs: this.queue,
|
|
552
|
+
nested,
|
|
553
|
+
assertActive: this.assertOpen
|
|
554
|
+
});
|
|
555
|
+
this.assertOpen();
|
|
556
|
+
return result;
|
|
557
|
+
}
|
|
558
|
+
evaluateNested = async (source, extension) => {
|
|
559
|
+
this.assertOpen();
|
|
560
|
+
const phase = this.phase.getStore();
|
|
561
|
+
if (!phase?.active ||
|
|
562
|
+
phase.extension !== extension ||
|
|
563
|
+
phase.evaluating ||
|
|
564
|
+
this.active === undefined)
|
|
565
|
+
throw new SandboxError("reentry");
|
|
566
|
+
const leave = this.budget.enterCall();
|
|
567
|
+
phase.evaluating = true;
|
|
568
|
+
try {
|
|
569
|
+
if (++this.nestedDepth > this.limits.nestedEvaluations) {
|
|
570
|
+
const error = new SandboxError({
|
|
571
|
+
budget: "callDepth",
|
|
572
|
+
current: this.nestedDepth,
|
|
573
|
+
limit: this.limits.nestedEvaluations
|
|
574
|
+
});
|
|
575
|
+
this.poison(error);
|
|
576
|
+
throw error;
|
|
577
|
+
}
|
|
578
|
+
const result = await this.evaluateRaw(source, "<nested>", true);
|
|
579
|
+
if (!result.ok)
|
|
580
|
+
throw new Error(result.error.message);
|
|
581
|
+
}
|
|
582
|
+
finally {
|
|
583
|
+
this.nestedDepth--;
|
|
584
|
+
phase.evaluating = false;
|
|
585
|
+
leave();
|
|
586
|
+
}
|
|
587
|
+
};
|
|
588
|
+
evaluate = async (source, options = {}) => this.perform(async () => {
|
|
589
|
+
const result = await this.evaluateRaw(source, options.filename);
|
|
590
|
+
if (!result.ok) {
|
|
591
|
+
await this.dispose();
|
|
592
|
+
return { ok: false, error: result.error, stats: result.stats };
|
|
593
|
+
}
|
|
594
|
+
return { ok: true, returnValue: this.exportValue(result.returnValue), stats: result.stats };
|
|
595
|
+
});
|
|
596
|
+
async perform(task) {
|
|
597
|
+
if (this.closed || this.failure !== undefined)
|
|
598
|
+
await this.dispose();
|
|
599
|
+
this.assertOpen();
|
|
600
|
+
if (this.active !== undefined)
|
|
601
|
+
throw new SandboxError("reentry");
|
|
602
|
+
const pending = Promise.resolve().then(() => withSandboxPromiseRejectionTracker(this.tracker, () => runResources.run({ signal: this.controller.signal, add: this.onCleanup }, () => withCancellationSignal(this.controller.signal, task))));
|
|
603
|
+
this.active = pending;
|
|
604
|
+
try {
|
|
605
|
+
const result = await pending;
|
|
606
|
+
await this.queue.drain();
|
|
607
|
+
const unhandled = await this.tracker.findUnhandledRejection();
|
|
608
|
+
if (unhandled !== undefined) {
|
|
609
|
+
const error = new Error(`Unhandled guest promise rejection: ${describeThrownValue(unhandled.reason)}`);
|
|
610
|
+
error.name = "UnhandledRejectionError";
|
|
611
|
+
throw error;
|
|
612
|
+
}
|
|
613
|
+
if (this.failure !== undefined)
|
|
614
|
+
throw this.failure.reason;
|
|
615
|
+
if (!this.closed)
|
|
616
|
+
reconcileCompiledValues(this.budget, [...(this.scope?.retainedValues() ?? []), ...this.retainedRoots()], this.compilation);
|
|
617
|
+
return result;
|
|
618
|
+
}
|
|
619
|
+
catch (error) {
|
|
620
|
+
this.poison(error);
|
|
621
|
+
try {
|
|
622
|
+
await this.dispose();
|
|
623
|
+
}
|
|
624
|
+
catch (cleanup) {
|
|
625
|
+
throw new AggregateError([error, cleanup], "Realm execution and cleanup failed.");
|
|
626
|
+
}
|
|
627
|
+
throw error;
|
|
628
|
+
}
|
|
629
|
+
finally {
|
|
630
|
+
this.active = undefined;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
close = () => {
|
|
634
|
+
this.closed = true;
|
|
635
|
+
this.controller.abort(new Error("SafeJS realm is closed."));
|
|
636
|
+
if (this.phase.getStore()?.active)
|
|
637
|
+
return this.dispose();
|
|
638
|
+
return Promise.allSettled([
|
|
639
|
+
this.active,
|
|
640
|
+
...Array.from(this.pendingCallbacks, (pending) => pending.promise)
|
|
641
|
+
]).then(() => this.dispose());
|
|
642
|
+
};
|
|
643
|
+
dispose() {
|
|
644
|
+
if (this.disposal !== undefined)
|
|
645
|
+
return this.disposal;
|
|
646
|
+
this.closed = true;
|
|
647
|
+
this.controller.abort(new Error("SafeJS realm is closed."));
|
|
648
|
+
this.options.signal?.removeEventListener("abort", this.abort);
|
|
649
|
+
for (const callback of this.callbacks.keys())
|
|
650
|
+
revokeGuestCallback(callback, this);
|
|
651
|
+
this.callbacks.clear();
|
|
652
|
+
for (const object of this.hostObjects)
|
|
653
|
+
revokeHostObject(object, this);
|
|
654
|
+
this.hostObjects.clear();
|
|
655
|
+
for (const reference of this.guestReferences.keys())
|
|
656
|
+
revokeGuestReference(reference, this);
|
|
657
|
+
this.guestReferences.clear();
|
|
658
|
+
this.budget.setRetainedValues(this, undefined);
|
|
659
|
+
this.disposal = (async () => {
|
|
660
|
+
const errors = [];
|
|
661
|
+
for (const cleanup of this.cleanups.splice(0).reverse()) {
|
|
662
|
+
try {
|
|
663
|
+
await cleanup();
|
|
664
|
+
}
|
|
665
|
+
catch (error) {
|
|
666
|
+
errors.push(error);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
this.scope = undefined;
|
|
670
|
+
this.convertedModules.clear();
|
|
671
|
+
for (const key of Object.keys(this.globals))
|
|
672
|
+
delete this.globals[key];
|
|
673
|
+
for (const key of Object.keys(this.modules))
|
|
674
|
+
delete this.modules[key];
|
|
675
|
+
for (const key of Object.keys(this.builtinBindings))
|
|
676
|
+
Reflect.deleteProperty(this.builtinBindings, key);
|
|
677
|
+
this.nativeConversions.seen = new WeakMap();
|
|
678
|
+
reconcileCompiledValues(this.budget, [], this.compilation);
|
|
679
|
+
this.compilation.dispose();
|
|
680
|
+
this.lease.release();
|
|
681
|
+
if (errors.length > 0)
|
|
682
|
+
throw new AggregateError(errors, "Realm cleanup failed.");
|
|
683
|
+
})();
|
|
684
|
+
return this.disposal;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
function readModules(input) {
|
|
688
|
+
const entries = (value, label) => {
|
|
689
|
+
if (types.isMap(value) && !types.isProxy(value)) {
|
|
690
|
+
const result = [...Map.prototype.entries.call(value)];
|
|
691
|
+
if (result.length > 4096 ||
|
|
692
|
+
result.some(([key]) => typeof key !== "string" || key.length === 0))
|
|
693
|
+
throw new TypeError(`${label} requires bounded string keys.`);
|
|
694
|
+
return result;
|
|
695
|
+
}
|
|
696
|
+
return Object.entries(readDataRecord(value, label));
|
|
697
|
+
};
|
|
698
|
+
const modules = Object.create(null);
|
|
699
|
+
for (const [name, exports] of entries(input ?? {}, "Module registry")) {
|
|
700
|
+
const exported = Object.create(null);
|
|
701
|
+
for (const [key, value] of entries(exports, "Module exports"))
|
|
702
|
+
exported[key] = value;
|
|
703
|
+
modules[name] = exported;
|
|
704
|
+
}
|
|
705
|
+
return modules;
|
|
706
|
+
}
|
|
707
|
+
function assertNames(actual, expected, label) {
|
|
708
|
+
if (actual.length !== expected.length || actual.some((name) => !expected.includes(name)))
|
|
709
|
+
throw new TypeError(`Extension ${label} names do not match its manifest.`);
|
|
710
|
+
}
|
|
711
|
+
export function createRealm(options = {}) {
|
|
712
|
+
const state = new RealmState(readRealmOptions(options));
|
|
713
|
+
return Object.freeze({
|
|
714
|
+
extensions: Object.freeze(state.extensions.map((extension) => extension.manifest)),
|
|
715
|
+
evaluate: state.evaluate,
|
|
716
|
+
invokeCallback: state.invokeCallback,
|
|
717
|
+
releaseCallback: state.releaseCallback,
|
|
718
|
+
releaseGuestReference: state.releaseGuestReference,
|
|
719
|
+
close: state.close
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
export async function runWithExtensions(source, options) {
|
|
723
|
+
if (options.snapshot !== undefined ||
|
|
724
|
+
options.snapshotBackend !== undefined ||
|
|
725
|
+
options.snapshotPath !== undefined ||
|
|
726
|
+
options.entryPointArgs !== undefined)
|
|
727
|
+
throw new TypeError("Live extension runs do not support snapshots or entryPointArgs; use a persistent realm.");
|
|
728
|
+
const state = new RealmState(readRealmOptions(options, true));
|
|
729
|
+
try {
|
|
730
|
+
const result = await state.perform(() => state.evaluateRaw(source, options.filename));
|
|
731
|
+
if (result.ok)
|
|
732
|
+
encodeReplayData(result.returnValue);
|
|
733
|
+
return {
|
|
734
|
+
...result,
|
|
735
|
+
snapshot: {
|
|
736
|
+
version: 1,
|
|
737
|
+
sourceHash: hashSource(source),
|
|
738
|
+
bindings: {},
|
|
739
|
+
replayError: "Live realm state cannot be serialized or replayed."
|
|
740
|
+
}
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
catch (error) {
|
|
744
|
+
if (state.disposal === undefined) {
|
|
745
|
+
try {
|
|
746
|
+
await state.close();
|
|
747
|
+
}
|
|
748
|
+
catch (cleanupError) {
|
|
749
|
+
throw new AggregateError([error, cleanupError], "Realm execution and cleanup failed.");
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
throw error;
|
|
753
|
+
}
|
|
754
|
+
finally {
|
|
755
|
+
if (state.disposal === undefined)
|
|
756
|
+
await state.close();
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
function readRealmOptions(value, oneShot = false) {
|
|
760
|
+
const options = readDataRecord(value, "Realm options");
|
|
761
|
+
const supported = new Set([
|
|
762
|
+
"bindings",
|
|
763
|
+
"modules",
|
|
764
|
+
"extensions",
|
|
765
|
+
"grants",
|
|
766
|
+
"budget",
|
|
767
|
+
"signal",
|
|
768
|
+
"sink",
|
|
769
|
+
"randomSeed",
|
|
770
|
+
"clock",
|
|
771
|
+
"limits"
|
|
772
|
+
]);
|
|
773
|
+
for (const [key, entry] of Object.entries(options)) {
|
|
774
|
+
if (supported.has(key) || (oneShot && (key === "filename" || entry === undefined)))
|
|
775
|
+
continue;
|
|
776
|
+
throw new TypeError(`Unsupported ${oneShot ? "extension-run" : "realm"} option '${key}'.`);
|
|
777
|
+
}
|
|
778
|
+
return options;
|
|
779
|
+
}
|