@poe-platform/safe-js 0.1.36 → 0.1.38

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
@@ -241,7 +241,7 @@ const collection = context.createHostObject({ indexed: {
241
241
 
242
242
  Saved collections observe current host contents. Index reads, `Object.keys`/`values`/`entries`, `Object.hasOwn`, `in`, `for...in`, `for...of`, array/object spread and `Array.from` use the live view. Enumerable keys include current indices and fixed members, but not `length`. `Array.from` preserves element identity and interleaves mapping with reads. Noncanonical and out-of-range indices never call `get`; fixed members cannot reuse `length` or canonical index names. Enumeration and traversal consume execution budgets, without eagerly allocating virtual properties.
243
243
 
244
- For changing named properties, add `named` to the same definition:
244
+ For a live set of named properties, add `named` to the same definition:
245
245
 
246
246
  ```js
247
247
  const named = {
@@ -258,13 +258,36 @@ const attributesObject = context.createHostObject({ named });
258
258
  | --- | --- |
259
259
  | `keys()` | Synchronous dense own-data array of distinct strings. Proxies, accessors, sparse arrays and reserved `constructor`/`prototype`/`__proto__` names reject. |
260
260
  | `get(name)` | Synchronous value provider, called only for a currently present name. Existing host conversion and identity rules apply. |
261
+ | `set(name, value)` | Optional synchronous setter, including new names. Receives the normally converted host value; assignment returns the original guest RHS. Omit to keep named writes disabled. |
262
+ | `delete(name)` | Optional synchronous deleter returning a boolean. Absent names return `true` without calling it; existing names return its result. Omit to keep deletion disabled. |
261
263
  | `maxKeys` | Required positive integer, at most 65,536. |
262
264
  | `maxKeyCodeUnits` | Required positive aggregate key-length cap, at most 1,048,576 UTF-16 code units. Execution, array, string and data budgets also apply. |
263
265
  | `enumerable` | Defaults to `true`. Set `false` to keep names readable and visible to `in`/`Object.hasOwn`, but omit them from keys/values/entries, object spread and `for...in`. |
264
266
 
265
267
  Fixed properties/methods take precedence over names. With `indexed`, numeric indices and `length` remain indexed members. Enumeration deduplicates collisions; names removed by an earlier getter are skipped. Named-only objects are not iterable—combine `named` with `indexed` when you need numeric collection access and `for...of`.
266
268
 
267
- Named properties, indexed members and indexed `length` are read-only. Live objects reject deletion, freezing, native prototype access, property-descriptor manipulation and 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.
269
+ To opt into dynamic writes and deletion, supply the hooks explicitly:
270
+
271
+ ```js
272
+ const storage = context.createHostObject({
273
+ named: {
274
+ keys: () => [...values.keys()],
275
+ get: name => values.get(name),
276
+ set: (name, value) => { values.set(name, value); },
277
+ delete: name => values.delete(name),
278
+ maxKeys: 256,
279
+ maxKeyCodeUnits: 8192
280
+ }
281
+ });
282
+ ```
283
+
284
+ Guest code can now use `storage.theme = "dark"` and `delete storage.theme`.
285
+
286
+ Both hooks must be synchronous; async/generator functions and proxies reject, and promises returned by ordinary functions are rejected and observed. Fixed members still use only their declared setters. Named hooks cannot overwrite or delete fixed members, indexed slots (including out-of-range indices), indexed `length`, or reserved prototype names. Saved objects remain live across native changes and are revoked on realm close.
287
+
288
+ SafeJS validates current keys and prospective new-key count/UTF-16/data limits before calling a mutator, then validates keys again afterward. Work, conversion and cancellation budgets still apply. Providers must enforce atomic storage quotas themselves: a post-write failure cannot roll back native side effects. Values are normally copied or passed as explicit realm-owned capabilities, not retained as arbitrary guest objects. Browser Storage coercion, persistence, origin policy and events belong in the consumer.
289
+
290
+ Named properties are read-only unless opted in; indexed members and indexed `length` remain read-only. Live objects reject other deletion, freezing, native prototype access, property-descriptor manipulation and 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.
268
291
 
269
292
  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 random generators and telemetry are rejected in this mode rather than silently ignored.
270
293
 
@@ -6380,11 +6380,15 @@ function createLiveHostObject(definition, controller) {
6380
6380
  if (input.named !== void 0) {
6381
6381
  const data = readDataRecord(input.named, "Named host capability");
6382
6382
  if (Object.keys(data).some(
6383
- (key) => !["keys", "get", "maxKeys", "maxKeyCodeUnits", "enumerable"].includes(key)
6383
+ (key) => !["keys", "get", "set", "delete", "maxKeys", "maxKeyCodeUnits", "enumerable"].includes(key)
6384
6384
  ))
6385
6385
  throw new TypeError("Unknown named host capability field.");
6386
- if (typeof data.keys !== "function" || typeof data.get !== "function")
6387
- throw new TypeError("Named keys and get must be synchronous functions.");
6386
+ for (const name of ["keys", "get", "set", "delete"]) {
6387
+ const operation = data[name];
6388
+ if (operation === void 0 && (name === "set" || name === "delete")) continue;
6389
+ if (typeof operation !== "function" || types3.isProxy(operation) || types3.isAsyncFunction(operation) || types3.isGeneratorFunction(operation))
6390
+ throw new TypeError(`Named ${name} must be a synchronous non-generator function, not a proxy.`);
6391
+ }
6388
6392
  if (typeof data.maxKeys !== "number" || !Number.isInteger(data.maxKeys) || data.maxKeys < 1 || data.maxKeys > MAX_NAMED_KEYS)
6389
6393
  throw new RangeError(`Named maxKeys must be an integer from 1 to ${MAX_NAMED_KEYS}.`);
6390
6394
  if (typeof data.maxKeyCodeUnits !== "number" || !Number.isInteger(data.maxKeyCodeUnits) || data.maxKeyCodeUnits < 1 || data.maxKeyCodeUnits > MAX_NAMED_KEY_CODE_UNITS)
@@ -6396,6 +6400,8 @@ function createLiveHostObject(definition, controller) {
6396
6400
  named = {
6397
6401
  keys: data.keys,
6398
6402
  get: data.get,
6403
+ set: data.set,
6404
+ delete: data.delete,
6399
6405
  maxKeys: data.maxKeys,
6400
6406
  maxKeyCodeUnits: data.maxKeyCodeUnits,
6401
6407
  enumerable: data.enumerable
@@ -6519,8 +6525,28 @@ function setHostObjectMember(value, key, entry) {
6519
6525
  state.controller.assertActive();
6520
6526
  state.controller.chargeWork();
6521
6527
  const property = state.properties.get(key);
6522
- if (property?.set === void 0) throw new TypeError(`Host property '${key}' is not writable.`);
6523
- state.controller.write(property.set, entry);
6528
+ if (property !== void 0) {
6529
+ if (property.set === void 0) throw new TypeError(`Host property '${key}' is not writable.`);
6530
+ state.controller.write(property.set, entry);
6531
+ return;
6532
+ }
6533
+ if (state.named?.set === void 0) throw new TypeError(`Host property '${key}' is not writable.`);
6534
+ namedMutationKeys(state, key, true);
6535
+ state.controller.write((value2) => state.named.set(key, value2), entry);
6536
+ namedKeys(state);
6537
+ }
6538
+ function deleteHostObjectMember(value, key) {
6539
+ const state = guestObjects.get(value);
6540
+ state.controller.assertActive();
6541
+ state.controller.chargeWork();
6542
+ if (state.named?.delete === void 0) throw new TypeError("Live host properties cannot be deleted.");
6543
+ if (!namedMutationKeys(state, key, false).includes(key)) return true;
6544
+ const deleted = state.controller.read(() => state.named.delete(key), (result) => {
6545
+ if (typeof result !== "boolean") throw new TypeError("Named delete must return a boolean.");
6546
+ return result;
6547
+ });
6548
+ namedKeys(state);
6549
+ return deleted;
6524
6550
  }
6525
6551
  function getHostObjectKeys(value) {
6526
6552
  const state = guestObjects.get(value);
@@ -6591,6 +6617,27 @@ function canonicalIndex(key) {
6591
6617
  const index = Number(key);
6592
6618
  return Number.isInteger(index) && index >= 0 && index < 4294967295 && String(index) === key ? index : void 0;
6593
6619
  }
6620
+ function namedMutationKeys(state, key, create) {
6621
+ state.controller.chargeWork(key.length);
6622
+ state.controller.checkString(key);
6623
+ if (["constructor", "prototype", "__proto__"].includes(key) || state.properties.has(key) || state.methods.has(key) || state.indexed !== void 0 && (key === "length" || canonicalIndex(key) !== void 0))
6624
+ throw new TypeError(`Host member '${key}' is protected from named mutation.`);
6625
+ const named = state.named;
6626
+ if (key.length > named.maxKeyCodeUnits)
6627
+ throw new RangeError("Named key exceeds maximum UTF-16 code units.");
6628
+ const keys = namedKeys(state);
6629
+ if (create && !keys.includes(key)) {
6630
+ const length = keys.length + 1;
6631
+ if (length > named.maxKeys) throw new RangeError("Named keys exceed maxKeys.");
6632
+ state.controller.checkLength(length);
6633
+ let units = key.length;
6634
+ for (const existing of keys) units += existing.length;
6635
+ if (units > named.maxKeyCodeUnits)
6636
+ throw new RangeError("Named keys exceed maximum UTF-16 code units.");
6637
+ state.controller.checkTemporaryDataSize(1 + length + units);
6638
+ }
6639
+ return keys;
6640
+ }
6594
6641
  function namedKeys(state) {
6595
6642
  const named = state.named;
6596
6643
  return state.controller.read(named.keys, (value) => {
@@ -27141,11 +27188,11 @@ async function evaluateDeleteExpression(node, context) {
27141
27188
  if (!isIndexableSandboxValue(member.object)) {
27142
27189
  throw new TypeError("Unary operator 'delete' requires a sandbox object property.");
27143
27190
  }
27144
- deleteSandboxProperty(member.object, member.property);
27191
+ const deleted = deleteSandboxProperty(member.object, member.property);
27145
27192
  return {
27146
27193
  kind: "normal",
27147
27194
  hasValue: true,
27148
- value: true
27195
+ value: deleted
27149
27196
  };
27150
27197
  }
27151
27198
  async function evaluateUpdateExpression(node, context) {
@@ -28007,12 +28054,12 @@ function setSandboxProperty(target, property, value, budget) {
28007
28054
  }
28008
28055
  }
28009
28056
  function deleteSandboxProperty(target, property) {
28010
- if (isGuestHostObject(target)) throw new TypeError("Live host properties cannot be deleted.");
28057
+ if (isGuestHostObject(target)) return deleteHostObjectMember(target, String(property));
28011
28058
  if (isGuestClosure(target)) target = materializeFunctionProperties(target);
28012
28059
  if (Array.isArray(target)) {
28013
28060
  assertCollectionMutable(target);
28014
28061
  }
28015
- delete target[String(property)];
28062
+ return delete target[String(property)];
28016
28063
  }
28017
28064
  function getClosureMemberValue(target, property, context) {
28018
28065
  return getFunctionMember(target, property, createFunctionMethodOptions(context));
@@ -31052,6 +31099,9 @@ var RealmState = class {
31052
31099
  assertActive: this.assertOpen,
31053
31100
  chargeWork: this.chargeWork,
31054
31101
  checkLength: (length) => this.budget.allocateArrayLength(length),
31102
+ checkString: (value) => {
31103
+ this.budget.allocateString(value);
31104
+ },
31055
31105
  checkTemporaryDataSize: (size) => {
31056
31106
  const temporary = {};
31057
31107
  try {
@@ -31064,7 +31114,7 @@ var RealmState = class {
31064
31114
  const value = this.invokeHost(operation, operation);
31065
31115
  if (types5.isPromise(value)) {
31066
31116
  void Promise.resolve(value).catch(() => void 0);
31067
- throw new TypeError("Live property getters must be synchronous.");
31117
+ throw new TypeError("Live property operations must be synchronous.");
31068
31118
  }
31069
31119
  return this.importValue(validate === void 0 ? value : validate(value));
31070
31120
  },
@@ -32693,4 +32743,4 @@ export {
32693
32743
  FileSnapshotBackend,
32694
32744
  run
32695
32745
  };
32696
- //# sourceMappingURL=chunk-LBJGHXII.js.map
32746
+ //# sourceMappingURL=chunk-BW4SYRDC.js.map