@poe-platform/safe-js 0.1.37 → 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 +25 -2
- package/dist/safe-js/chunks/{chunk-LBJGHXII.js → chunk-BW4SYRDC.js} +61 -11
- package/dist/safe-js/chunks/chunk-BW4SYRDC.js.map +7 -0
- package/dist/safe-js/chunks/{chunk-ARXBI427.js → chunk-S64MC6ZD.js} +2 -2
- package/dist/safe-js/cli.js +2 -2
- package/dist/safe-js/core.js +1 -1
- package/dist/safe-js/index.js +2 -2
- package/dist/safe-js/interp/host-capabilities.d.ts +4 -0
- package/package.json +2 -2
- package/dist/safe-js/chunks/chunk-LBJGHXII.js.map +0 -7
- /package/dist/safe-js/chunks/{chunk-ARXBI427.js.map → chunk-S64MC6ZD.js.map} +0 -0
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
|
|
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
|
-
|
|
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
|
-
|
|
6387
|
-
|
|
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
|
|
6523
|
-
|
|
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:
|
|
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))
|
|
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
|
|
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-
|
|
32746
|
+
//# sourceMappingURL=chunk-BW4SYRDC.js.map
|