@poe-platform/safe-js 0.1.22 → 0.1.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -116,9 +116,9 @@ This prints `2`. Evaluations share declarations, closures and object identity wi
116
116
  | --- | --- |
117
117
  | `extensions` | Explicit `defineExtension(...)` registrations; `[]`. Setup runs once, on first evaluation, not on construction or unused close. |
118
118
  | `grants` | Granted capability names; `[]`. Every requested capability must be granted before any extension setup runs. |
119
- | `limits` | Positive integer caps: `extensions: 32`, `hostObjects: 1024`, `callbacks: 1024`, `cleanups: 1024`, `nestedEvaluations: 16`. Collection budgets also apply. |
119
+ | `limits` | Positive integer caps: `extensions: 32`, `hostObjects: 1024`, `callbacks: 1024`, `guestReferences: 1024`, `cleanups: 1024`, `nestedEvaluations: 16`. Collection budgets also apply. |
120
120
 
121
- Ordinary host arguments/results are still copied. To preserve live native identity, explicitly create a host object. A guest function crossing to the host becomes an opaque callback: invoke it with `realm.invokeCallback(callback, { thisValue?, args? })`, then `realm.releaseCallback(callback)` when no longer needed. Callbacks and live objects cannot cross realms or survive close. Guest-object argument retention is not yet supported; copying a timer argument does not preserve its guest identity.
121
+ Ordinary host arguments/results are still copied. To preserve live native identity, explicitly create a host object. A guest function crossing to the host becomes an opaque callback: invoke it with `realm.invokeCallback(callback, { thisValue?, args? })`, then `realm.releaseCallback(callback)` when no longer needed. Callbacks and live objects cannot cross realms or survive close. For deferred arguments that must preserve guest identity, opt into retained references as described below.
122
122
 
123
123
  <details>
124
124
  <summary>Trusted extensions and live host objects</summary>
@@ -163,9 +163,15 @@ The manifest requires `version: 1` and a nonempty `name`. Optional `capabilities
163
163
  | `createHostObject({ properties?, methods? })` | Create a realm-owned capability. Properties declare synchronous `get`/`set` functions; methods are host functions. Undeclared members expose no native prototype. |
164
164
  | `invokeCallback(callback, { thisValue?, args? })` | Invoke a captured guest function with the realm's state, cancellation and budgets. Same operation as on the realm. |
165
165
  | `releaseCallback(callback)` | Revoke the callback and release its retained guest state. |
166
+ | `retainGuestArguments(operation, from)` | During setup, opt an operation into opaque argument references starting at the zero-based index `from`. Requires declared and granted `guest:retain`. Earlier arguments keep normal conversion; live host methods preserve the declaration. |
167
+ | `releaseGuestReference(reference)` | Revoke one reference and release its retained state. Also available on the realm. |
166
168
  | `nestedOperation(fn)` | During setup, mark a host operation authorized to run nested source. Requires declared and granted `source:nested`. |
167
169
  | `evaluateNested(source)` | Only inside that extension's authorized operation. Completes before the enclosing call returns to guest code, shares scope/budgets, and propagates errors. Parallel nested evaluations and ordinary source reentry are rejected. |
168
170
 
171
+ For a timer-shaped `schedule(callback, delay, ...args)`, register `context.retainGuestArguments(schedule, 2)`. The host receives normal callback/delay values and opaque `GuestReference` handles for the remaining arguments. Pass those handles to `context.invokeCallback(callback, { args })` to recover the original guest objects and observe mutations made after scheduling. References also work as callback receivers and host return values, including cycles, closures, primitives and live host objects.
172
+
173
+ Release each reference when the host no longer needs it; returning it does not release it. Retained graphs count against data budgets and `limits.guestReferences`. Synchronous native failure releases references captured for that call; asynchronous operations must release theirs in host cleanup. Close revokes all remaining references. Handles cannot be inspected, used in another realm, or serialized into replay/error data. Unmarked operations still copy values.
174
+
169
175
  Live objects do not support native prototypes, property-descriptor manipulation or portable serialization. Realm state is not a checkpoint: snapshot/replay and live-capability error-data conversion are rejected. Extensions are trusted native code; grants are a registration contract, not OS isolation. Native work still needs host timeouts and external process supervision for hard limits. No DOM, timers or browser engine are bundled.
170
176
 
171
177
  For one-shot use, `run(source, { extensions, grants, ... })` accepts the same realm options plus `filename`, returns data only, and closes resources before settling. Run-only features such as snapshots, `entryPointArgs`, `importMeta`, custom clocks/random generators and telemetry are rejected in this mode rather than silently ignored.
@@ -6211,6 +6211,25 @@ function copyFloat32Storage(value, state) {
6211
6211
  var hostObjects = /* @__PURE__ */ new WeakMap();
6212
6212
  var guestObjects = /* @__PURE__ */ new WeakMap();
6213
6213
  var guestCallbacks = /* @__PURE__ */ new WeakMap();
6214
+ var guestReferences = /* @__PURE__ */ new WeakMap();
6215
+ function createGuestReference(root, owner, assertActive) {
6216
+ const reference = Object.freeze(/* @__PURE__ */ Object.create(null));
6217
+ guestReferences.set(reference, { root, owner, assertActive });
6218
+ return reference;
6219
+ }
6220
+ function readGuestReference(reference, owner) {
6221
+ const state = typeof reference === "object" && reference !== null ? guestReferences.get(reference) : void 0;
6222
+ if (state === void 0 || state.owner !== owner)
6223
+ throw new TypeError("Foreign or invalid guest reference.");
6224
+ state.assertActive();
6225
+ if (state.root === void 0) throw new TypeError("Guest reference is revoked.");
6226
+ return state.root[0];
6227
+ }
6228
+ function revokeGuestReference(reference, owner) {
6229
+ const state = guestReferences.get(reference);
6230
+ if (state === void 0 || state.owner !== owner) throw new TypeError("Foreign guest reference.");
6231
+ state.root = void 0;
6232
+ }
6214
6233
  function createLiveHostObject(definition, controller) {
6215
6234
  const input = readDataRecord(definition, "Host object definition");
6216
6235
  if (Object.keys(input).some((key) => key !== "properties" && key !== "methods"))
@@ -6254,9 +6273,10 @@ function isGuestHostObject(value) {
6254
6273
  return typeof value === "object" && value !== null && guestObjects.has(value);
6255
6274
  }
6256
6275
  function isLiveCapability(value) {
6257
- return (typeof value === "object" && value !== null || typeof value === "function") && (hostObjects.has(value) || guestObjects.has(value) || guestCallbacks.has(value));
6276
+ return (typeof value === "object" && value !== null || typeof value === "function") && (hostObjects.has(value) || guestObjects.has(value) || guestCallbacks.has(value) || guestReferences.has(value));
6258
6277
  }
6259
6278
  function importHostCapability(value, owner) {
6279
+ if (guestReferences.has(value)) return readGuestReference(value, owner);
6260
6280
  const object = hostObjects.get(value);
6261
6281
  if (object !== void 0) {
6262
6282
  if (object.controller.owner !== owner) throw new TypeError("Foreign realm host capability.");
@@ -28043,7 +28063,7 @@ function wrapCallerInjectedFunction(name, value, options, state) {
28043
28063
  seen: /* @__PURE__ */ new WeakMap(),
28044
28064
  restored: []
28045
28065
  };
28046
- const hostArgs = deepCopyFromSandbox([...args], {
28066
+ const copyArguments = (values) => deepCopyFromSandbox([...values], {
28047
28067
  compilation,
28048
28068
  unwrapHostObject: options.realm === void 0 ? void 0 : (object) => exportHostCapability(object, options.realm.owner),
28049
28069
  wrapClosure: (closure) => options.realm?.wrapCallback(closure) ?? wrapSandboxClosureForHost(
@@ -28054,13 +28074,21 @@ function wrapCallerInjectedFunction(name, value, options, state) {
28054
28074
  callbacks
28055
28075
  )
28056
28076
  });
28077
+ const captured = options.realm?.captureArguments(callable, args, copyArguments);
28078
+ const hostArgs = captured?.args ?? copyArguments(args);
28057
28079
  const hostCalls = options.hostCalls;
28058
28080
  const operation = options.operation ?? bindingName;
28059
28081
  const moduleId = options.moduleId ?? "<bindings>";
28060
28082
  const policy = readHostOperationPolicy(value) ?? readRegisteredPendingHostCallPolicy(moduleId, operation) ?? "re-issue";
28061
28083
  if (hostCalls === void 0) {
28062
28084
  if (options.realm !== void 0) {
28063
- const result = options.realm.invoke(callable, () => Reflect.apply(callable, void 0, hostArgs));
28085
+ let result;
28086
+ try {
28087
+ result = options.realm.invoke(callable, () => Reflect.apply(callable, void 0, hostArgs));
28088
+ } catch (error) {
28089
+ captured.rollback();
28090
+ throw error;
28091
+ }
28064
28092
  if (options.realm.awaitResult(callable)) {
28065
28093
  return Promise.resolve(result).then((value2) => copyHostResultToSandbox(value2, stackFrames, options));
28066
28094
  }
@@ -30095,6 +30123,7 @@ var RealmState = class {
30095
30123
  extensions: 32,
30096
30124
  hostObjects: 1024,
30097
30125
  callbacks: 1024,
30126
+ guestReferences: 1024,
30098
30127
  cleanups: 1024,
30099
30128
  nestedEvaluations: 16
30100
30129
  };
@@ -30131,6 +30160,7 @@ var RealmState = class {
30131
30160
  owner: this,
30132
30161
  assertActive: this.assertOpen,
30133
30162
  wrapCallback: this.wrapCallback,
30163
+ captureArguments: this.captureArguments,
30134
30164
  invoke: this.invokeHost,
30135
30165
  awaitResult: (operation) => this.nestedOperations.has(operation)
30136
30166
  };
@@ -30170,7 +30200,7 @@ var RealmState = class {
30170
30200
  }
30171
30201
  options.signal?.addEventListener("abort", this.abort, { once: true });
30172
30202
  if (options.signal?.aborted) this.abort();
30173
- this.budget.setRetainedValues(this, this.retainedCallbacks);
30203
+ this.budget.setRetainedValues(this, this.retainedRoots);
30174
30204
  this.tracker.onFatalRejection((error) => this.poison(error));
30175
30205
  } catch (error) {
30176
30206
  this.compilation.dispose();
@@ -30194,6 +30224,8 @@ var RealmState = class {
30194
30224
  pendingCallbacks = /* @__PURE__ */ new Set();
30195
30225
  callbackCache = /* @__PURE__ */ new WeakMap();
30196
30226
  hostObjects = /* @__PURE__ */ new Set();
30227
+ guestReferences = /* @__PURE__ */ new Map();
30228
+ retainedOperations = /* @__PURE__ */ new WeakMap();
30197
30229
  nestedOperations = /* @__PURE__ */ new WeakMap();
30198
30230
  convertedModules = /* @__PURE__ */ new Map();
30199
30231
  nativeConversions = { seen: /* @__PURE__ */ new WeakMap() };
@@ -30213,10 +30245,56 @@ var RealmState = class {
30213
30245
  signal: this.controller.signal,
30214
30246
  realm: this.bridge
30215
30247
  });
30216
- retainedCallbacks = () => [
30248
+ retainedRoots = () => [
30217
30249
  ...this.callbacks.values(),
30218
- ...Array.from(this.pendingCallbacks, (pending) => pending.closure)
30250
+ ...Array.from(this.pendingCallbacks, (pending) => pending.closure),
30251
+ ...this.guestReferences.values()
30219
30252
  ];
30253
+ captureArguments = (operation, args, copy) => {
30254
+ const from = this.retainedOperations.get(operation)?.from ?? args.length;
30255
+ const values = copy(args.slice(0, from));
30256
+ const captured = [];
30257
+ const rollback = () => {
30258
+ for (const reference of captured) {
30259
+ revokeGuestReference(reference, this);
30260
+ this.guestReferences.delete(reference);
30261
+ }
30262
+ };
30263
+ try {
30264
+ for (const value of args.slice(from)) {
30265
+ this.checkCollection(
30266
+ this.guestReferences.size + 1,
30267
+ this.limits.guestReferences,
30268
+ "guest reference"
30269
+ );
30270
+ const root = [value];
30271
+ const reference = createGuestReference(root, this, this.assertOpen);
30272
+ this.guestReferences.set(reference, root);
30273
+ captured.push(reference);
30274
+ values.push(reference);
30275
+ }
30276
+ if (captured.length > 0)
30277
+ this.budget.reconcileDataUsage(
30278
+ measureSandboxData([...this.scope?.retainedValues() ?? [], ...this.retainedRoots()])
30279
+ );
30280
+ return { args: values, rollback };
30281
+ } catch (error) {
30282
+ rollback();
30283
+ if (error instanceof SandboxError) this.poison(error);
30284
+ throw error;
30285
+ }
30286
+ };
30287
+ releaseGuestReference = (reference) => {
30288
+ readGuestReference(reference, this);
30289
+ revokeGuestReference(reference, this);
30290
+ this.guestReferences.delete(reference);
30291
+ if (this.active === void 0)
30292
+ reconcileCompiledValues(
30293
+ this.budget,
30294
+ [...this.scope?.retainedValues() ?? [], ...this.retainedRoots()],
30295
+ this.compilation
30296
+ );
30297
+ };
30220
30298
  assertOpen = () => {
30221
30299
  if (this.failure !== void 0) throw this.failure.reason;
30222
30300
  if (this.closed) throw new Error("SafeJS realm is closed; capabilities are revoked.");
@@ -30384,7 +30462,7 @@ var RealmState = class {
30384
30462
  });
30385
30463
  try {
30386
30464
  this.budget.reconcileDataUsage(
30387
- measureSandboxData([...this.scope?.retainedValues() ?? [], ...this.retainedCallbacks()])
30465
+ measureSandboxData([...this.scope?.retainedValues() ?? [], ...this.retainedRoots()])
30388
30466
  );
30389
30467
  } catch (error) {
30390
30468
  this.poison(error);
@@ -30399,7 +30477,7 @@ var RealmState = class {
30399
30477
  if (this.active === void 0)
30400
30478
  reconcileCompiledValues(
30401
30479
  this.budget,
30402
- [...this.scope?.retainedValues() ?? [], ...this.retainedCallbacks()],
30480
+ [...this.scope?.retainedValues() ?? [], ...this.retainedRoots()],
30403
30481
  this.compilation
30404
30482
  );
30405
30483
  };
@@ -30458,7 +30536,7 @@ var RealmState = class {
30458
30536
  if (!this.closed && this.active === void 0)
30459
30537
  reconcileCompiledValues(
30460
30538
  this.budget,
30461
- [...this.scope?.retainedValues() ?? [], ...this.retainedCallbacks()],
30539
+ [...this.scope?.retainedValues() ?? [], ...this.retainedRoots()],
30462
30540
  this.compilation
30463
30541
  );
30464
30542
  }
@@ -30475,6 +30553,23 @@ var RealmState = class {
30475
30553
  createHostObject: this.createHostObject,
30476
30554
  invokeCallback: this.invokeCallback,
30477
30555
  releaseCallback: this.releaseCallback,
30556
+ releaseGuestReference: this.releaseGuestReference,
30557
+ retainGuestArguments: (operation, from) => {
30558
+ this.assertOpen();
30559
+ if (!extension.manifest.capabilities?.includes("guest:retain"))
30560
+ throw new TypeError("Retaining arguments requires the guest:retain grant.");
30561
+ if (typeof operation !== "function")
30562
+ throw new TypeError("Retained operation must be a function.");
30563
+ if (!Number.isSafeInteger(from) || from < 0)
30564
+ throw new TypeError("Argument index must be a non-negative safe integer.");
30565
+ if (this.scope !== void 0)
30566
+ throw new TypeError("Retained operations must be registered during setup.");
30567
+ const previous = this.retainedOperations.get(operation);
30568
+ if (previous !== void 0 && (previous.extension !== extension || previous.from !== from))
30569
+ throw new TypeError("Conflicting retained operation declaration.");
30570
+ this.retainedOperations.set(operation, { extension, from });
30571
+ return operation;
30572
+ },
30478
30573
  nestedOperation: (operation) => {
30479
30574
  this.assertOpen();
30480
30575
  if (!extension.manifest.capabilities?.includes("source:nested"))
@@ -30637,7 +30732,7 @@ var RealmState = class {
30637
30732
  if (!this.closed)
30638
30733
  reconcileCompiledValues(
30639
30734
  this.budget,
30640
- [...this.scope?.retainedValues() ?? [], ...this.retainedCallbacks()],
30735
+ [...this.scope?.retainedValues() ?? [], ...this.retainedRoots()],
30641
30736
  this.compilation
30642
30737
  );
30643
30738
  return result;
@@ -30671,6 +30766,8 @@ var RealmState = class {
30671
30766
  this.callbacks.clear();
30672
30767
  for (const object of this.hostObjects) revokeHostObject(object, this);
30673
30768
  this.hostObjects.clear();
30769
+ for (const reference of this.guestReferences.keys()) revokeGuestReference(reference, this);
30770
+ this.guestReferences.clear();
30674
30771
  this.budget.setRetainedValues(this, void 0);
30675
30772
  this.disposal = (async () => {
30676
30773
  const errors = [];
@@ -30726,6 +30823,7 @@ function createRealm(options = {}) {
30726
30823
  evaluate: state.evaluate,
30727
30824
  invokeCallback: state.invokeCallback,
30728
30825
  releaseCallback: state.releaseCallback,
30826
+ releaseGuestReference: state.releaseGuestReference,
30729
30827
  close: state.close
30730
30828
  });
30731
30829
  }
@@ -31920,4 +32018,4 @@ export {
31920
32018
  FileSnapshotBackend,
31921
32019
  run
31922
32020
  };
31923
- //# sourceMappingURL=chunk-2M6MNQBY.js.map
32021
+ //# sourceMappingURL=chunk-MXUOEOBE.js.map