Sign In

@endo/immutable-arraybuffer

Package Overview
Dependencies
Maintainers
9
Versions
5
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@endo/immutable-arraybuffer - npm Package Compare versions

Comparing version
1.1.2
to
2.0.0
+42
CHANGELOG.md
# @endo/immutable-arraybuffer
## 2.0.0
### Major Changes
- [#3308](https://github.com/endojs/endo/pull/3308) [`4da9a99`](https://github.com/endojs/endo/commit/4da9a9959e4376c5760a3232e978a4f8fe4ac6b7) Thanks [@kriskowal](https://github.com/kriskowal)! - Drop the immutable-ArrayBuffer pseudo-prototype.
Emulated immutable `ArrayBuffer`s produced by `@endo/immutable-arraybuffer` now inherit directly from `ArrayBuffer.prototype` rather than from an intermediate prototype. `Object.getPrototypeOf(immuAB) === ArrayBuffer.prototype` for both emulated immutable and genuine buffers; the brand check is the new `immutable` accessor on `ArrayBuffer.prototype` installed by the shim.
The `[Symbol.toStringTag]` slot is preserved as an own property on each emulated immutable buffer (not on the shared prototype), so `Object.prototype.toString.call(immuAB)` continues to return `'[object ImmutableArrayBuffer]'` (as in master) while genuine ArrayBuffers continue to read as `'[object ArrayBuffer]'`. This keeps `concordance` (and any other downstream consumer that sniffs the toStringTag to decide whether the value is a genuine exotic) from misrouting an emulated immutable through Node's `Buffer.from`, which throws because the emulated immutable is not an exotic object.
`@endo/immutable-arraybuffer` is now a side-effect-only package: its sole public export is `./shim.js`. The `index.js` and the package's `.` entry are removed; the previously exported names (`isBufferImmutable`, `sliceBufferToImmutable`, `optTransferBufferToImmutable`) are no longer reachable from outside the package. Callers detect immutability via the `ArrayBuffer.prototype.immutable` accessor (or `Object.prototype.toString.call(buffer) === '[object ImmutableArrayBuffer]'` when the shim has not been loaded) and convert via `buffer.sliceToImmutable(...)` and `buffer.transferToImmutable(...)` on the prototype. The break is a major bump for the `@endo/immutable-arraybuffer` package.
`@endo/bytes`'s `to-immutable.js` imports `@endo/immutable-arraybuffer/shim.js` (triggering the shim install) and calls `buffer.sliceToImmutable(...)` on `ArrayBuffer.prototype` instead of the previously exported `sliceBufferToImmutable` free function.
The shim's install policy is now detect-then-skip rather than warn-and-overwrite: the Immutable ArrayBuffer proposal has reached stage 3, so any prior installation (native or previously loaded shim) wins. If `'sliceToImmutable' in ArrayBuffer.prototype` is already true when the shim loads, the shim does nothing.
`ses` drops the `%ImmutableArrayBufferPrototype%` permits entry, which no longer has a referent. The three permits lines inside `%ArrayBufferPrototype%` that declare the shim-installed methods (`transferToImmutable`, `sliceToImmutable`, `immutable`) stay as-is.
`@endo/pass-style`'s `byteArray` brand check no longer routes through an intermediate prototype; it consults the `immutable` accessor on `ArrayBuffer.prototype` directly. The check also tolerates the `[Symbol.toStringTag]` own-property on emulated immutable buffers and verifies that its value is a non-enumerable data property with a string value.
## [1.1.2](https://github.com/endojs/endo/compare/@endo/immutable-arraybuffer@1.1.1...@endo/immutable-arraybuffer@1.1.2) (2025-07-12)
- Removes `@endo/immutable-arraybufer/shim-hermes.js` and absorbs the necessary features into `@endo/immutable-arraybuffer/shim.js`. We are not qualifying this as a breaking change since the feature did not exist long enough to become relied upon.
## [1.1.1](https://github.com/endojs/endo/compare/@endo/immutable-arraybuffer@1.1.0...@endo/immutable-arraybuffer@1.1.1) (2025-06-17)
- Captures `structuredClone` early so that scuttling all properties of `globalThis`
after initializing `@endo/immutable-arraybuffer` or
`@endo/immutable-arraybuffer/shim.js` does not interfere with this module's
designed behavior.
## [1.1.0](https://github.com/endojs/endo/compare/@endo/immutable-arraybuffer@1.0.0...@endo/immutable-arraybuffer@1.1.0) (2025-06-02)
### Features
- **immutable-arraybuffer:** sliceToImmutable Hermes ponyfill and shim ([dc52bb6](https://github.com/endojs/endo/commit/dc52bb6f027a0a0785095660c0de909c61c40c99))
## 1.0.0 (2025-05-07)
First release.
declare global {
// This syntax extends the global ArrayBuffer interface
interface ArrayBuffer {
/**
* Creates an immutable slice of the given buffer.
*
* @param start The start index.
* @param end The end index.
* @returns The sliced immutable ArrayBuffer.
*/
sliceToImmutable: (start?: number, end?: number) => ArrayBuffer;
/**
* Transfer the contents to a new immutable ArrayBuffer
*
* @param newLength The start index.
* @returns The new immutable ArrayBuffer.
*/
transferToImmutable: (newLength?: number) => ArrayBuffer;
/**
* Whether the buffer is immutable.
*/
immutable: boolean;
}
}
export {};
export namespace immutableArrayBufferLibProperties {
let __proto__: null;
const byteLength: any;
const detached: any;
const maxByteLength: any;
const resizable: any;
const immutable: any;
/**
* @this {ArrayBuffer}
* @param {number} [start]
* @param {number} [end]
*/
function slice(this: ArrayBuffer, start?: number, end?: number): ArrayBuffer;
/**
* @this {ArrayBuffer}
* @param {number} [start]
* @param {number} [end]
*/
function sliceToImmutable(this: ArrayBuffer, start?: number, end?: number): ArrayBuffer;
/**
* @this {ArrayBuffer}
* @param {number} [newByteLength]
*/
function resize(this: ArrayBuffer, newByteLength?: number): void;
/**
* @this {ArrayBuffer}
* @param {number} [newLength]
*/
function transfer(this: ArrayBuffer, newLength?: number): ArrayBuffer;
/**
* @this {ArrayBuffer}
* @param {number} [newLength]
*/
function transferToFixedLength(this: ArrayBuffer, newLength?: number): ArrayBuffer;
/**
* @this {ArrayBuffer}
* @param {number} [newLength]
*/
function transferToImmutable(this: ArrayBuffer, newLength?: number): ArrayBuffer;
}
export { amplifyArrayBuffer as _amplifyArrayBufferForTests };
export function isBufferImmutable(buffer: ArrayBuffer): boolean;
export function sliceBufferToImmutable(buffer: ArrayBuffer, start?: number, end?: number): ArrayBuffer;
export const optTransferBufferToImmutable: ((buffer: ArrayBuffer, newLength?: number) => ArrayBuffer) | undefined;
/**
* Amplifier-with-this-fallthrough: returns the underlying genuine
* `ArrayBuffer` when `arrayBuffer` is an emulated immutable buffer (in the
* brand WeakMap), and returns `arrayBuffer` itself otherwise. This lets the
* methods on the shared `ArrayBuffer.prototype` (after the shim install)
* work as drop-in replacements for the genuine methods when invoked on a
* genuine `ArrayBuffer`, while transparently reaching the underlying buffer
* for the emulated-immutable case. The name aligns with the analogous
* `amplifyTypedArray` on the freezable-TypedArray experiment branch.
*
* @param {ArrayBuffer} arrayBuffer
* @returns {ArrayBuffer}
*/
declare function amplifyArrayBuffer(arrayBuffer: ArrayBuffer): ArrayBuffer;
//# sourceMappingURL=lib.d.ts.map
{"version":3,"file":"lib.d.ts","sourceRoot":"","sources":["lib.js"],"names":[],"mappings":";;;;;;;IAgNE;;;;OAIG;IACH,0CAHW,MAAM,QACN,MAAM,eAIhB;IACD;;;;OAIG;IACH,qDAHW,MAAM,QACN,MAAM,eAKhB;IACD;;;OAGG;IACH,mDAFW,MAAM,QAYhB;IACD;;;OAGG;IACH,iDAFW,MAAM,eAYhB;IACD;;;OAGG;IACH,8DAFW,MAAM,eAYhB;IACD;;;OAGG;IACH,4DAFW,MAAM,eAchB;;;AA8EI,0CAHI,WAAW,GACT,OAAO,CAEkD;AAc/D,+CALI,WAAW,UACX,MAAM,QACN,MAAM,GACJ,WAAW,CAcvB;AAsCD,qDA7Ba,WAAW,cACX,MAAM,KACJ,WAAW,cA2B4C;AAtTtE;;;;;;;;;;;;GAYG;AACH,iDAHW,WAAW,GACT,WAAW,CAQvB"}
const {
ArrayBuffer,
Object,
Reflect,
Symbol,
TypeError,
Uint8Array,
WeakMap,
// Capture structuredClone before it can be scuttled.
structuredClone: optStructuredClone,
// eslint-disable-next-line no-restricted-globals
} = globalThis;
const { freeze, defineProperty, getOwnPropertyDescriptor, getPrototypeOf } =
Object;
const { apply, ownKeys } = Reflect;
// Capture the WeakMap prototype methods up front so we can use them with
// `apply` below, without exposing the `buffers` WeakMap to post-hoc
// prototype lookups or polymorphic dispatch.
const { get: weakmapGet, set: weakmapSet, has: weakmapHas } = WeakMap.prototype;
const { prototype: arrayBufferPrototype } = ArrayBuffer;
const {
slice,
transfer: optTransfer,
resize: optResize,
transferToFixedLength: optTransferToFixedLength,
} = arrayBufferPrototype;
// @ts-expect-error TS doesn't know it'll be there
const { get: arrayBufferByteLength } = getOwnPropertyDescriptor(
arrayBufferPrototype,
'byteLength',
);
// Capture the resizable-ArrayBuffer proposal's accessors when present. On
// platforms without that proposal (Node <= 18, Hermes), these are absent;
// the fallthrough branches in the lib property record short-circuit on
// brand membership and never reach the captured accessor in that case
// (an emulated immutable always has `detached === false`, `resizable ===
// false`, and `maxByteLength === byteLength`).
const optArrayBufferDetached = getOwnPropertyDescriptor(
arrayBufferPrototype,
'detached',
)?.get;
const optArrayBufferResizable = getOwnPropertyDescriptor(
arrayBufferPrototype,
'resizable',
)?.get;
const optArrayBufferMaxByteLength = getOwnPropertyDescriptor(
arrayBufferPrototype,
'maxByteLength',
)?.get;
const typedArrayPrototype = getPrototypeOf(Uint8Array.prototype);
const { set: uint8ArraySet } = typedArrayPrototype;
// @ts-expect-error TS doesn't know it'll be there
const { get: uint8ArrayBuffer } = getOwnPropertyDescriptor(
typedArrayPrototype,
'buffer',
);
/**
* Copy a range of values from a genuine ArrayBuffer exotic object into a new
* ArrayBuffer.
*
* @param {ArrayBuffer} realBuffer
* @param {number} [start]
* @param {number} [end]
* @returns {ArrayBuffer}
*/
const arrayBufferSlice = (realBuffer, start = undefined, end = undefined) =>
apply(slice, realBuffer, [start, end]);
/**
* Move the contents of a genuine ArrayBuffer exotic object into a new fresh
* ArrayBuffer and detach the original source.
* We can only do this on platforms that support `structuredClone` or
* `ArrayBuffer.prototype.transfer`.
* On other platforms, we can still emulate
* `ArrayBuffer.prototoype.sliceToImmutable`, but not
* `ArrayBuffer.prototype.transferToImmutable`.
* See the package README section "Platform support for `transferToImmutable`"
* for the per-engine version thresholds and feature-testing guidance.
*
* @param {ArrayBuffer} arrayBuffer
* @returns {ArrayBuffer}
*/
let optArrayBufferTransfer;
if (optTransfer) {
optArrayBufferTransfer = arrayBuffer => apply(optTransfer, arrayBuffer, []);
} else if (optStructuredClone) {
optArrayBufferTransfer = arrayBuffer => {
// Hopefully, a zero-length slice is cheap, but still enforces that
// `arrayBuffer` is a genuine `ArrayBuffer` exotic object.
arrayBufferSlice(arrayBuffer, 0, 0);
return optStructuredClone(arrayBuffer, {
transfer: [arrayBuffer],
});
};
} else {
// Assignment is redundant, but remains for clarity.
optArrayBufferTransfer = undefined;
}
/**
* If we could use classes with private fields everywhere, this would have
* been a `this.#buffer` private field on an `ImmutableArrayBufferInternal`
* class. But we cannot do so on Hermes. So, instead, we
* emulate the `this.#buffer` private field, including its use as a brand check.
* Maps from all and only emulated Immutable ArrayBuffers to real ArrayBuffers.
*
* @type {WeakMap<ArrayBuffer, ArrayBuffer>}
*/
const buffers = new WeakMap();
const isEmulatedImmutable = buf => apply(weakmapHas, buffers, [buf]);
/**
* Amplifier-with-this-fallthrough: returns the underlying genuine
* `ArrayBuffer` when `arrayBuffer` is an emulated immutable buffer (in the
* brand WeakMap), and returns `arrayBuffer` itself otherwise. This lets the
* methods on the shared `ArrayBuffer.prototype` (after the shim install)
* work as drop-in replacements for the genuine methods when invoked on a
* genuine `ArrayBuffer`, while transparently reaching the underlying buffer
* for the emulated-immutable case. The name aligns with the analogous
* `amplifyTypedArray` on the freezable-TypedArray experiment branch.
*
* @param {ArrayBuffer} arrayBuffer
* @returns {ArrayBuffer}
*/
const amplifyArrayBuffer = arrayBuffer => {
const result = apply(weakmapGet, buffers, [arrayBuffer]);
if (result !== undefined) {
return result;
}
return arrayBuffer;
};
/**
* A plain record of the properties the shim copies onto
* `ArrayBuffer.prototype` to install immutable-ArrayBuffer support. This is
* not a prototype of any object: emulated immutable buffers directly inherit
* from `ArrayBuffer.prototype`, and the methods here become the ones the
* (now shared) prototype dispatches to. Each method either calls
* `amplifyArrayBuffer(this)` to reach the underlying buffer (read accessors,
* `slice`, `sliceToImmutable`) or discriminates on brand WeakMap membership
* and delegates to the captured genuine method on fallthrough (the mutators
* `resize`, `transfer`, `transferToFixedLength`, `transferToImmutable`).
*
* Omits `constructor` so the original `ArrayBuffer.prototype.constructor` is unchanged.
*/
export const immutableArrayBufferLibProperties = {
__proto__: null,
/**
* @this {ArrayBuffer}
*/
get byteLength() {
return apply(arrayBufferByteLength, amplifyArrayBuffer(this), []);
},
/**
* @this {ArrayBuffer}
*/
get detached() {
if (isEmulatedImmutable(this)) {
return false;
}
// Genuine `ArrayBuffer.prototype.detached` is a stage-finished accessor
// on platforms with the resizable-ArrayBuffer proposal. On older
// platforms (Node <= 18, Hermes) it does not exist; the conservative
// answer for a non-detached genuine buffer in that case is false.
if (optArrayBufferDetached === undefined) {
return false;
}
return apply(optArrayBufferDetached, this, []);
},
/**
* @this {ArrayBuffer}
*/
get maxByteLength() {
if (isEmulatedImmutable(this)) {
// For an emulated immutable buffer, maxByteLength is byteLength: it
// cannot grow.
return apply(arrayBufferByteLength, amplifyArrayBuffer(this), []);
}
if (optArrayBufferMaxByteLength === undefined) {
return apply(arrayBufferByteLength, this, []);
}
return apply(optArrayBufferMaxByteLength, this, []);
},
/**
* @this {ArrayBuffer}
*/
get resizable() {
if (isEmulatedImmutable(this)) {
return false;
}
if (optArrayBufferResizable === undefined) {
return false;
}
return apply(optArrayBufferResizable, this, []);
},
/**
* @this {ArrayBuffer}
*/
get immutable() {
return isEmulatedImmutable(this);
},
/**
* @this {ArrayBuffer}
* @param {number} [start]
* @param {number} [end]
*/
slice(start = undefined, end = undefined) {
return arrayBufferSlice(amplifyArrayBuffer(this), start, end);
},
/**
* @this {ArrayBuffer}
* @param {number} [start]
* @param {number} [end]
*/
sliceToImmutable(start = undefined, end = undefined) {
// eslint-disable-next-line no-use-before-define
return sliceBufferToImmutable(amplifyArrayBuffer(this), start, end);
},
/**
* @this {ArrayBuffer}
* @param {number} [newByteLength]
*/
resize(newByteLength = undefined) {
if (isEmulatedImmutable(this)) {
throw TypeError('Cannot resize an immutable ArrayBuffer');
}
if (optResize === undefined) {
throw TypeError(
'Cannot resize ArrayBuffer: underlying platform lacks ArrayBuffer.prototype.resize',
);
}
return apply(optResize, this, [newByteLength]);
},
/**
* @this {ArrayBuffer}
* @param {number} [newLength]
*/
transfer(newLength = undefined) {
if (isEmulatedImmutable(this)) {
throw TypeError('Cannot detach an immutable ArrayBuffer');
}
if (optTransfer === undefined) {
throw TypeError(
'Cannot transfer ArrayBuffer: underlying platform lacks ArrayBuffer.prototype.transfer',
);
}
return apply(optTransfer, this, [newLength]);
},
/**
* @this {ArrayBuffer}
* @param {number} [newLength]
*/
transferToFixedLength(newLength = undefined) {
if (isEmulatedImmutable(this)) {
throw TypeError('Cannot detach an immutable ArrayBuffer');
}
if (optTransferToFixedLength === undefined) {
throw TypeError(
'Cannot transferToFixedLength ArrayBuffer: underlying platform lacks ArrayBuffer.prototype.transferToFixedLength',
);
}
return apply(optTransferToFixedLength, this, [newLength]);
},
/**
* @this {ArrayBuffer}
* @param {number} [newLength]
*/
transferToImmutable(newLength = undefined) {
if (isEmulatedImmutable(this)) {
throw TypeError('Cannot detach an immutable ArrayBuffer');
}
// eslint-disable-next-line no-use-before-define
if (optTransferBufferToImmutable === undefined) {
throw TypeError(
'Cannot transfer to immutable: underlying platform lacks transfer or structuredClone',
);
}
// eslint-disable-next-line no-use-before-define
return optTransferBufferToImmutable(this, newLength);
},
};
// Better fidelity emulation of a class prototype: each property is
// non-enumerable, matching the shape `ArrayBuffer.prototype` itself uses.
for (const key of ownKeys(immutableArrayBufferLibProperties)) {
defineProperty(immutableArrayBufferLibProperties, key, {
enumerable: false,
});
}
freeze(immutableArrayBufferLibProperties);
// Internal-test export. The helper itself is load-bearing for every
// method on `immutableArrayBufferLibProperties`, but the package's
// public export surface intentionally keeps it private (callers either
// touch the helper indirectly through `ArrayBuffer.prototype` methods
// or rely on `isBufferImmutable`). The export exists so the
// adversarial-tests skill can exercise the helper in isolation.
export { amplifyArrayBuffer as _amplifyArrayBufferForTests };
/**
* Emulates what would have been the encapsulated `ImmutableArrayBufferInternal`
* class constructor. This function takes the `realBuffer` which its
* result encapsulates. Security demands that this result has exclusive access
* to the `realBuffer` it is given, which its callers must ensure.
*
* The emulated immutable buffer directly inherits from `ArrayBuffer.prototype`.
* The brand WeakMap is the sole discriminator: `ArrayBuffer.prototype`'s
* methods (after the shim installs the lib properties) check brand membership
* to decide whether to treat the receiver as immutable.
*
* @param {ArrayBuffer} realBuffer
* @returns {ArrayBuffer}
*/
const makeImmutableArrayBufferInternal = realBuffer => {
const result = /** @type {ArrayBuffer} */ (
/** @type {unknown} */ ({
__proto__: arrayBufferPrototype,
})
);
// Install `[Symbol.toStringTag] = 'ImmutableArrayBuffer'` as an own
// property of each emulated immutable buffer (not on the shared prototype,
// which must retain the genuine `'ArrayBuffer'` tag so genuine instances
// continue to read as `[object ArrayBuffer]`). This is the minimum
// departure from DESIGN.md Move 2 paragraph 7 needed to keep
// `concordance` (and any downstream consumer that sniffs the toStringTag)
// from misrouting an emulated immutable through `Buffer.from`, which
// throws because the emulated immutable is not a genuine exotic. With the
// own-property slot in place, `Object.prototype.toString.call(immuAB)`
// returns `'[object ImmutableArrayBuffer]'` and concordance routes the
// value through its unrenderable-value path. Genuine ArrayBuffers
// continue to inherit `'ArrayBuffer'` from the prototype.
defineProperty(result, Symbol.toStringTag, {
value: 'ImmutableArrayBuffer',
writable: false,
enumerable: false,
configurable: false,
});
apply(weakmapSet, buffers, [result, realBuffer]);
return result;
};
// Since `makeImmutableArrayBufferInternal` MUST not escape,
// this `freeze` is just belt-and-suspenders.
freeze(makeImmutableArrayBufferInternal);
/**
* Internal brand check. Returns `true` when `buffer` is an emulated
* immutable buffer (in the lib's brand WeakMap), `false` otherwise. After
* the premise-2 fold-in the package no longer exports this from a public
* entry point; callers use `buffer.immutable` (the accessor installed by
* the shim on `ArrayBuffer.prototype`) or `Object.prototype.toString
* .call(buffer) === '[object ImmutableArrayBuffer]'`. The internal export
* lets the in-package tests (`test/lib-*.test.js`) reach the helper
* directly without round-tripping through the prototype.
*
* @param {ArrayBuffer} buffer
* @returns {boolean}
*/
export const isBufferImmutable = buffer => isEmulatedImmutable(buffer);
/**
* Creates an immutable slice of the given buffer. Internal helper used by
* `immutableArrayBufferLibProperties.sliceToImmutable` and by the shim's
* own install. After the premise-2 fold-in the package no longer exports
* this from a public entry point; the internal export lets the in-package
* tests reach it directly.
*
* @param {ArrayBuffer} buffer The original buffer.
* @param {number} [start] The start index.
* @param {number} [end] The end index.
* @returns {ArrayBuffer} The sliced immutable ArrayBuffer.
*/
export const sliceBufferToImmutable = (
buffer,
start = undefined,
end = undefined,
) => {
let realBuffer = apply(weakmapGet, buffers, [buffer]);
if (realBuffer === undefined) {
realBuffer = buffer;
}
return makeImmutableArrayBufferInternal(
arrayBufferSlice(realBuffer, start, end),
);
};
let transferBufferToImmutable;
if (optArrayBufferTransfer) {
/**
* Transfer the contents to a new Immutable ArrayBuffer. Internal helper
* used by `immutableArrayBufferLibProperties.transferToImmutable` and by
* the shim's own install. Not part of the package's public export surface.
*
* @param {ArrayBuffer} buffer The original buffer.
* @param {number} [newLength] The start index.
* @returns {ArrayBuffer}
*/
transferBufferToImmutable = (buffer, newLength = undefined) => {
if (newLength === undefined) {
buffer = optArrayBufferTransfer(buffer);
} else if (optTransfer) {
buffer = apply(optTransfer, buffer, [newLength]);
} else {
buffer = optArrayBufferTransfer(buffer);
const oldLength = buffer.byteLength;
if (newLength <= oldLength) {
buffer = arrayBufferSlice(buffer, 0, newLength);
} else {
const oldTA = new Uint8Array(buffer);
const newTA = new Uint8Array(newLength);
apply(uint8ArraySet, newTA, [oldTA]);
buffer = apply(uint8ArrayBuffer, newTA, []);
}
}
const result = makeImmutableArrayBufferInternal(buffer);
return /** @type {ArrayBuffer} */ (/** @type {unknown} */ (result));
};
} else {
transferBufferToImmutable = undefined;
}
export const optTransferBufferToImmutable = transferBufferToImmutable;
export {};
//# sourceMappingURL=shim.d.ts.map
{"version":3,"file":"shim.d.ts","sourceRoot":"","sources":["shim.js"],"names":[],"mappings":""}
import { immutableArrayBufferLibProperties } from './lib.js';
// eslint-disable-next-line no-restricted-globals
const { ArrayBuffer, Object } = globalThis;
const { getOwnPropertyDescriptors, defineProperties } = Object;
const { prototype: arrayBufferPrototype } = ArrayBuffer;
// Stage-3 install policy: detect-then-skip.
//
// Both the Immutable ArrayBuffer proposal and the parallel Freezable
// TypedArray proposal are part of the same TC39 proposal, which has
// reached stage 3. At stage 3 or above our policy is detect-then-skip:
// if a prior installation (a native implementation, or a previously
// loaded shim) has already provided `sliceToImmutable` on
// `ArrayBuffer.prototype`, we defer to that installation rather than
// overwriting it. The native implementation always wins.
//
// `sliceToImmutable` is the load-bearing presence check: the proposal
// adds `sliceToImmutable`, `transferToImmutable`, and the `immutable`
// accessor as a unit, and any installer (native or shim) that provides
// one provides all three. Checking only one keeps the detect-then-skip
// branch deterministic.
//
// For proposals prior to stage 3 a warn-and-overwrite policy would be
// appropriate so the shim stays authoritative across partial or
// divergent platform implementations. The Immutable ArrayBuffer proposal
// is past that threshold.
if (!('sliceToImmutable' in arrayBufferPrototype)) {
defineProperties(
arrayBufferPrototype,
getOwnPropertyDescriptors(immutableArrayBufferLibProperties),
);
}
+14
-27
{
"name": "@endo/immutable-arraybuffer",
"version": "1.1.2",
"version": "2.0.0",
"description": "Immutable ArrayBuffer (the shim!)",

@@ -23,7 +23,7 @@ "keywords": [

"type": "module",
"main": "./index.js",
"module": "./index.js",
"exports": {
".": "./index.js",
"./shim.js": "./shim.js",
"./shim.js": {
"types": "./shim.types.d.ts",
"default": "./shim.js"
},
"./package.json": "./package.json"

@@ -35,21 +35,14 @@ },

"lint-fix": "yarn lint:eslint --fix && yarn lint:types",
"lint:eslint": "eslint '**/*.js'",
"lint:eslint": "eslint .",
"lint:types": "tsc",
"postpack": "git clean -fX \"*.d.ts*\" \"*.d.cts*\" \"*.d.mts*\" \"*.tsbuildinfo\"",
"prepack": "tsc --build tsconfig.build.json",
"test": "ava",
"test:c8": "c8 $C8_OPTIONS ava --config=ava-nesm.config.js",
"test:c8": "c8 ${C8_OPTIONS:-} ava",
"test:xs": "exit 0"
},
"devDependencies": {
"ava": "^6.1.3",
"babel-eslint": "^10.1.0",
"c8": "^7.14.0",
"eslint": "^8.57.1",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-eslint-comments": "^3.2.0",
"eslint-plugin-import": "^2.31.0",
"tsd": "^0.31.2",
"typescript": "~5.8.3"
"ava": "^8.0.1",
"c8": "^11.0.0",
"eslint": "^10.4.1",
"tsd": "^0.33.0",
"typescript": "~6.0.3"
},

@@ -70,7 +63,2 @@ "files": [

},
"eslintConfig": {
"extends": [
"plugin:@endo/ses"
]
},
"ava": {

@@ -81,4 +69,3 @@ "files": [

"timeout": "2m"
},
"gitHead": "9815aea9541f241389d2135c6097a7442bdffa17"
}
}
}
+105
-36
# `@endo/immutable-arraybuffer`
This `@endo/immutable-arraybuffer` package provides both a ponyfill and a shim for a proposed new JavaScript feature: *Immutable ArrayBuffers*.
- A ponyfill just defines and exports new things without modifying old things. The `index.js` file implements the ponyfill, providing the exports of the unqualified `@endo/immutable-arraybuffer` package.
- A shim modifies the existing JavaScript primordials as needed to most closely emulate the feature as proposed. The `shim.js` file uses the exports from `index.js` to modify `ArrayBuffer.prototype` to resemble the API being proposed. Importing `@endo/immutable-arraybuffer/shim.js` will cause these changes.
This `@endo/immutable-arraybuffer` package provides a shim for a proposed new JavaScript feature: *Immutable ArrayBuffers*.
A shim modifies the existing JavaScript primordials as needed to most closely emulate the feature as proposed.
Importing `@endo/immutable-arraybuffer/shim.js` will cause these changes.

@@ -11,7 +11,10 @@ Below, we use the term "buffer" to refer informally to an instance of an `ArrayBuffer`, whether immutable or not.

Prior proposals [In-Place Resizable and Growable `ArrayBuffer`s](https://github.com/tc39/proposal-resizablearraybuffer) and [ArrayBuffer.prototype.transfer and friends](https://github.com/tc39/proposal-arraybuffer-transfer) have both reached stage 4, and so are now an official part of JavaScript. Altogether, `ArrayBuffer.prototype` now has the following methods:
- `transfer(newByteLength?: number) :ArrayBuffer` -- move the contents of the original buffer to a new buffer, detach the original buffer, and return the new buffer. The new buffer will be as resizable as the original was.
Prior proposals [In-Place Resizable and Growable `ArrayBuffer`s](https://github.com/tc39/proposal-resizablearraybuffer) and [ArrayBuffer.prototype.transfer and friends](https://github.com/tc39/proposal-arraybuffer-transfer) have both reached stage 4, and so are now an official part of JavaScript.
Altogether, `ArrayBuffer.prototype` now has the following methods:
- `transfer(newByteLength?: number) :ArrayBuffer` -- move the contents of the original buffer to a new buffer, detach the original buffer, and return the new buffer.
The new buffer will be as resizable as the original was.
- `transferToFixedLength(newByteLength?: number) :ArrayBuffer` -- like `transfer` but the new buffer is not resizable.
- `resize(newByteLength: number) :void` -- change the size of this buffer if possible, or throw otherwise.
- `slice(start?: number, end?: number) :ArrayBuffer` -- Return a new buffer whose initial contents are a copy of that region of the original buffer. The original buffer is unmodified.
- `slice(start?: number, end?: number) :ArrayBuffer` -- Return a new buffer whose initial contents are a copy of that region of the original buffer.
The original buffer is unmodified.

@@ -24,13 +27,24 @@ and the following read-only accessor properties

None of the operations above enable the creation of an immutable buffer, i.e., a non-detached buffer whose contents cannot be changed, resized, or detached.
None of the operations above enable the creation of an immutable buffer, that is, a non-detached buffer whose contents cannot be changed, resized, or detached.
Both a `DataView` object and a `TypedArray` object are views into a buffer backing store. For a `TypedArray` object, the contents of the backing store appear as indexed data properties of the `TypeArray` object that reflect the current contents of this backing store. Currently, because there is no way to prevent the contents of the backing store from being changed, `TypedArray`s cannot be frozen.
Both a `DataView` object and a `TypedArray` object are views into a buffer backing store.
For a `TypedArray` object, the contents of the backing store appear as indexed data properties of the `TypeArray` object that reflect the current contents of this backing store.
Currently, because there is no way to prevent the contents of the backing store from being changed, `TypedArray`s cannot be frozen.
Some JavaScript implementations, like Moddable XS, bring JavaScript to embedded systems, like device controllers, where ROM is much more plentiful and cheaper than RAM. These systems need to place voluminous fixed data into ROM, and currently do so using semantics outside the official JavaScript standard.
Some JavaScript implementations, like Moddable XS, bring JavaScript to embedded systems, like device controllers, where ROM is much more plentiful and cheaper than RAM.
These systems need to place voluminous fixed data into ROM, and currently do so using semantics outside the official JavaScript standard.
The [OCapN](https://ocapn.org/) network protocol treats strings and byte-arrays as distinct forms of bulk data to be transmitted by copy. At JavaScript endpoints speaking OCapN such as `@endo/pass-style` + `@endo/marshal`, JavaScript strings represent OCapN strings. The immutability of strings in the JavaScript language reflects their by-copy nature in the protocol. Likewise, to reflect an OCapN byte-array well into the JavaScript language, we need an immutable container of bulk binary data. There currently are none. An Immutable `ArrayBuffer` would provide exactly the low-level machinery we need.
The [OCapN](https://ocapn.org/) network protocol treats strings and byte-arrays as distinct forms of bulk data to be transmitted by copy.
At JavaScript endpoints speaking OCapN such as `@endo/pass-style` + `@endo/marshal`, JavaScript strings represent OCapN strings.
The immutability of strings in the JavaScript language reflects their by-copy nature in the protocol.
Likewise, to reflect an OCapN byte-array well into the JavaScript language, we need an immutable container of bulk binary data.
There currently are none.
A frozen `Uint8Array` would provide exactly the low-level machinery we need.
## Overview of the *Immutable ArrayBuffer* Proposal
The *Immutable ArrayBuffer* proposal introduces additional methods and read-only accessor properties to `ArrayBuffer.prototype` that fit naturally into those explained above. Just as a buffer can be resizable or not, or detached or not, this proposal enables buffers to be immutable or not. Just as `transferToFixedSize` moves the contents of a original buffer into a newly created non-resizable buffer, this proposal provides a transfer operation that moves the contents of an original original buffer into a newly created immutable buffer. Altogether, this proposal only adds to `ArrayBuffer.prototype` one method
The *Immutable ArrayBuffer* proposal introduces additional methods and read-only accessor properties to `ArrayBuffer.prototype` that fit naturally into those explained above.
Just as a buffer can be resizable or not, or detached or not, this proposal enables buffers to be immutable or not.
Just as `transferToFixedSize` moves the contents of a original buffer into a newly created non-resizable buffer, this proposal provides a transfer operation that moves the contents of an original original buffer into a newly created immutable buffer.
Altogether, this proposal only adds to `ArrayBuffer.prototype` one method
- `transferToImmutable() :ArrayBuffer` -- move the contents of the original buffer into a new immutable buffer, detach the original buffer, and return the new buffer.

@@ -41,38 +55,93 @@

An immutable buffer cannot be detached or resized. Its `maxByteLength` is the same as its `byteLength`. A `DataView` or `TypedArray` using an immutable buffer as its backing store can be frozen and immutable. `ArrayBuffer`s, `DataView`s, and `TypedArray`s that are frozen and immutable could be placed in ROM without going beyond JavaScript's official semantics.
An immutable buffer cannot be detached or resized.
Its `maxByteLength` is the same as its `byteLength`.
A `DataView` or `TypedArray` using an immutable buffer as its backing store can be frozen and immutable.
`ArrayBuffer`s, `DataView`s, and `TypedArray`s that are frozen and immutable could be placed in ROM without going beyond JavaScript's official semantics.
## The Ponyfill
## The Shim
The proposal would add methods to `ArrayBuffer.prototype`. But a ponyfill, by definition, cannot do so. Instead, it defines and exports two functions corresponding to the two additions above
- `transferBufferToImmutable(buffer: ArrayBuffer) :ArrayBuffer`
- `isBufferImmutable(buffer: ArrayBuffer) :boolean`
Importing `@endo/immutable-arraybuffer/shim.js` installs the proposed methods (`transferToImmutable`, `sliceToImmutable`) and accessor (`immutable`) onto `ArrayBuffer.prototype`, along with replacements for the genuine `slice`, `resize`, `transfer`, and `transferToFixedLength` methods that discriminate on whether the receiver is an emulated immutable buffer.
For genuine ArrayBuffers, the replacements delegate to the captured genuine methods and behave identically to before.
For emulated immutable buffers, the methods either return the appropriate immutable behaviour (for `slice`) or throw the appropriate "cannot mutate" `TypeError` (for the mutators).
In order for `transferBufferToImmutable` to be able to return something of type `ArrayBuffer` that is actually immutable, that object cannot be an actual `ArrayBuffer` exotic object. Instead, an emulated immutable buffer implements the full proposed `ArrayBuffer` API and ultimately inherits from `ArrayBuffer.prototype`. Thus, `x instanceof ArrayBuffer` will act as proposed.
The shim's install policy is detect-then-skip: if `'sliceToImmutable' in ArrayBuffer.prototype` is already true when the shim loads (a native implementation, or a previously loaded shim), the shim does nothing and the prior installation wins.
The Immutable ArrayBuffer proposal has reached stage 3; at that threshold an earlier installation is presumed authoritative.
The emulated immutable buffers inherit directly from an intermediate prototype we refer to as `immutableArrayBufferPrototype`. This intermediate prototype contains all the methods and read-only accessor properties proposed here, as well as overrides of those inherited from `ArrayBuffer.prototype` as needed to emulate the behavior of an immutable instance. For each emulated immutable buffer, the implementation encapsulates a genuine `ArrayBuffer` that it has exclusive access to, so it can enforce immutability simply by never modifying it.
## The Shim
The immutable-arraybuffer shim additionally adds to `ArrayBuffer.prototype` a
- `transferToImmutable` method trivially derived from the ponyfill's `transferBufferToImmutable`.
- `sliceToImmutable` method trivially derived from the ponyfill's `sliceBufferToImmutable`.
- `immutable` read-only accessor property trivially derived from the ponyfill's `isBufferImmutable`.
## Caveats
The *Immutable ArrayBuffer* shim falls short of the proposal in the following ways
- The ponyfill and shim rely on the underlying platform having either `structuredClone` or `ArrayBuffer.prototype.transfer`. However, Node <= 16 has neither. Node 17 introduces `structuredClone` and Node 21 introduces `ArrayBuffer.prototype.transfer`. Without either, the ponyfill and shim fail to initialize.
- The proposal does not introduce an intermediate prototype, but rather modifies the behavior of the built-in methods on `ArrayBuffer.prototype` itself, to act appropriately on immutable `ArrayBuffer`s. By contrast, the ponyfill's and shim's emulated immutable buffers inherit directly from an intermediate prototype we refer to as `immutableArrayBufferPrototype`. That intermediate prototype directly inherits from `ArrayBuffer.prototype`. All the differential behavior for immutable buffers are provided by overrides found on `immutableArrayBufferPrototype`.
- The `immutableArrayBufferPrototype` intermediate prototype is an artifact of the emulation, but it is not encapsulated. It is trivially discoverable as the object that emulated immutable buffers directly inherit from.
- The shim's emulated immutable buffers are not real `ArrayBuffer` exotic objects. If they were, the shim would not be able to protect them from being written. Even though they implement the full proposed `ArrayBuffer` API, they cannot be plug-compatible -- they cannot be used as the backing stores of `DataView`s or `TypedArray`s. Perhaps follow-on shims might modify `DataView` and `TypedArray` to emulate that as well, but that is hard and beyond the ambition of this ponyfill + shim.
- The shim relies on the underlying platform having either `structuredClone` or `ArrayBuffer.prototype.transfer`.
See [Platform support for `transferToImmutable`](#platform-support-for-transfertoimmutable) below for the per-engine version thresholds and the guidance on when feature-testing is necessary.
Without either, the shim still shims `ArrayBuffer.prototype.sliceToImmutable` but omits `ArrayBuffer.prototype.transferToImmutable`.
- The shim's emulated immutable buffers are not real `ArrayBuffer` exotic objects.
If they were, the shim would not be able to protect them from being written.
Even though they implement the full proposed `ArrayBuffer` API, they cannot be plug-compatible: they cannot be used as the backing stores of `DataView`s or `TypedArray`s.
Perhaps follow-on shims might modify `DataView` and `TypedArray` to emulate that as well, but that is hard and beyond the ambition of this shim.
- Unlike genuine `ArrayBuffer` or `SharedArrayBuffer` exotic objects, the shim's emulated immutable buffers cannot be cloned or transfered between JS threads.
- Even after the *Immutable ArrayBuffer* proposal is implemented by the platform, the current code will still replace it with the shim implementation, in accord with shim best practices. See https://github.com/endojs/endo/pull/2311#discussion_r1632607527 . It will require a later manual step to delete the shim, after manual analysis of the compat implications.
- This is a plain *JavaScript* ponyfill/shim, not by itself a *Hardened JavaScript* polyfill/shim. Thus, the objects and function it creates are not hardened by this ponyfill/shim itself. Rather, the ses-shim is expected to import these, and then treat the resulting objects as if they were additional primordials, to be hardened during `lockdown`'s harden phase.
- This is a plain *JavaScript* shim, not by itself a *Hardened JavaScript* polyfill/shim.
Thus, the objects and function it creates are not hardened by this shim itself.
Rather, the ses-shim is expected to import this, and then treat the resulting objects as if they were additional primordials, to be hardened during `lockdown`'s harden phase.
## Platform support for `transferToImmutable`
The shim's emulation of `ArrayBuffer.prototype.transferToImmutable` requires the underlying platform to provide either `ArrayBuffer.prototype.transfer` (preferred when present) or the global `structuredClone` (used as a fallback to move the buffer's contents into a new backing store).
`sliceToImmutable` and the `immutable` accessor work on every platform; only `transferToImmutable` carries this dependency.
The following table records the first engine version that ships at least one of those primitives.
A cell marked **either** means the platform has both `structuredClone` and `ArrayBuffer.prototype.transfer`; a cell marked **structuredClone only** means the shim uses the structured-clone fallback path.
"Deficient" means neither primitive is present and `ArrayBuffer.prototype.transferToImmutable` is therefore absent after the shim loads.
### Engines
| Engine | First version with `structuredClone` | First version with `ArrayBuffer.prototype.transfer` | Status as of shipping today |
| --- | --- | --- | --- |
| V8 (Chromium) | 9.8 (with Chrome 98, Feb 2022) | 11.4 (with Chrome 114, May 2023) | **either** |
| SpiderMonkey (Firefox) | shipped with Firefox 94 (Nov 2021) | shipped with Firefox 122 (Jan 2024) | **either** |
| JavaScriptCore (WebKit) | shipped with Safari 15.4 (Mar 2022) | shipped with Safari 17.4 (Mar 2024) | **either** |
| Hermes | not implemented | not implemented | **deficient** |
The `structuredClone` global is a Web/HTML platform feature exposed to script through the engine's host environment; the dates above are for the host build that first exposed it.
`ArrayBuffer.prototype.transfer` is a TC39 language feature (ES2024) implemented in the engine itself.
### Runtimes and browsers
| Runtime / browser | First version with `structuredClone` | First version with `ArrayBuffer.prototype.transfer` | Status as of shipping today |
| --- | --- | --- | --- |
| Node.js | 17.0.0 (Oct 2021) | 21.0.0 (Oct 2023) | **either** on Node 21 and later; **structuredClone only** on Node 17 through 20; **deficient** on Node 16 and earlier |
| Deno | 1.14 (Sep 2021) | 1.33 (May 2023) | **either** on Deno 1.33 and later |
| Chrome / Edge | 98 (Feb 2022) | 114 (May 2023) | **either** on Chrome 114 and later; **structuredClone only** on Chrome 98 through 113 |
| Firefox | 94 (Nov 2021) | 122 (Jan 2024) | **either** on Firefox 122 and later; **structuredClone only** on Firefox 94 through 121 |
| Safari | 15.4 (Mar 2022) | 17.4 (Mar 2024) | **either** on Safari 17.4 and later; **structuredClone only** on Safari 15.4 through 17.3 |
| React Native (Hermes) | not implemented | not implemented | **deficient** |
Node 22 (active LTS at the time of writing) and Node 24 (current) both have `ArrayBuffer.prototype.transfer` and use the preferred path.
Node 18 and Node 20 reach the structured-clone fallback path; both are past or near end-of-life under the Node release schedule.
### Feature-testing guidance
Only code that might run on a **deficient** platform needs to feature-test for `ArrayBuffer.prototype.transferToImmutable`:
```js
import '@endo/immutable-arraybuffer/shim.js';
if (typeof ArrayBuffer.prototype.transferToImmutable === 'function') {
// use transferToImmutable
} else {
// fall back to sliceToImmutable (always present once the shim loads)
}
```
Code whose deployment targets are all non-deficient (any modern browser, Node.js 17 and later, Deno 1.14 and later) can rely on `transferToImmutable` being present after `import '@endo/immutable-arraybuffer/shim.js'` and skip the feature test.
React Native on Hermes and pre-Node-17 server environments are the practical cases that still require the test.
## Purposeful Violation
Since the `ImmutableArrayBufferInternal` class is only an artifact of the ponyfill and shim (i.e., is absent both from the real proposal and from native implementations), `ImmutableArrayBufferInternal` should not need its own `Symbol.toStringTag` property. Especially not one that differs from `ArrayBuffer.prototype`. Adding one reduces the fidelity of the ponyfill and shim. Nevertheless, we set `ImmutableArrayBufferInternal.prototype[Symbol.toStringTag]` to `'ImmutableArrayBuffer'`. Why?
This package sets `[Symbol.toStringTag]` to `'ImmutableArrayBuffer'` on each emulated immutable buffer (as an own property of the instance, not on the shared `ArrayBuffer.prototype`).
The rationale: Node's [concordance](https://github.com/concordancejs/concordance/blob/791d2a89b40eb13f2c889ac270dd8be190cf8073/lib/describe.js#L36) (used by ava for diagnostic output) sniffs the result of `Object.prototype.toString.call(value)` to decide whether it can do `Buffer.from(value)` on the object.
`Buffer.from` only works on genuine `ArrayBuffer` exotic objects; passing an emulated immutable buffer to it throws a `TypeError` that concordance does not handle gracefully.
The own-property `[Symbol.toStringTag] = 'ImmutableArrayBuffer'` slot keeps concordance from routing the value through `Buffer.from` and lets it fall through to the unrenderable-value path.
At https://github.com/concordancejs/concordance/blob/791d2a89b40eb13f2c889ac270dd8be190cf8073/lib/describe.js#L36 Node's concordance, in order to render diagnostic output for an object, sniffs the result of `toString()`. If the result seems to indicate that the object is an ArrayBuffer, then concordance assumes it can do things with the object (`Buffer.from`) that can only be done on genuine ArrayBuffers. To avoid this, the ponyfill and shim ensures that the sniff will not match `'ArrayBuffer'`.
Ava also uses Node's concordance for its diagnostic output, which is how we discovered the problem.
The drop-the-pseudo-prototype redesign removed the intermediate prototype that earlier versions hung this slot on; the slot is now installed per-instance via `defineProperty` in `makeImmutableArrayBufferInternal`.
Genuine ArrayBuffers continue to inherit `'ArrayBuffer'` from the prototype: `Object.prototype.toString.call(new ArrayBuffer(0))` reads as `'[object ArrayBuffer]'`.
Only emulated immutable buffers carry the `'ImmutableArrayBuffer'` slot: `Object.prototype.toString.call(new ArrayBuffer(0).sliceToImmutable())` reads as `'[object ImmutableArrayBuffer]'`.
Callers that need to distinguish emulated immutable buffers from genuine ones programmatically should prefer the `immutable` accessor on `ArrayBuffer.prototype` (installed by the shim), which is the canonical brand check.

@@ -15,3 +15,3 @@ # Security Policy

* Sending an email to security at (@) agoric.com., encrypted or unencrypted. To encrypt, please use [@warner](https://github.com/warner)’s personal GPG key [A476E2E6 11880C98 5B3C3A39 0386E81B 11CAA07A](http://www.lothar.com/warner-gpg.html).
* Sending an email to security at (@) agoric.com., encrypted or unencrypted. To encrypt, please use @Warner’s personal GPG key [A476E2E6 11880C98 5B3C3A39 0386E81B 11CAA07A](http://www.lothar.com/warner-gpg.html) .

@@ -37,4 +37,4 @@ * Sending a message on Keybase to `@agoric_security`, or sharing code and other log files via Keybase’s encrypted file system. ((_keybase_private/agoric_security,$YOURNAME).

When a bug patch is included in a software release, the Agoric code maintainers will:
* Confirm the version and date of the software release with the reporter.
* Provide information about the security issue that the software release resolves.
* Credit the bug reporter for discovery by adding thanks in release notes, securing a CVE designation, or adding the researcher’s name to a Hall of Fame.
* Confirm the version and date of the software release with the reporter.
* Provide information about the security issue that the software release resolves.
* Credit the bug reporter for discovery by adding thanks in release notes, securing a CVE designation, or adding the researcher’s name to a Hall of Fame.

@@ -1,1 +0,1 @@

import './src/immutable-arraybuffer-shim.js';
import './src/shim.js';
export * from "./src/immutable-arraybuffer-pony.js";
//# sourceMappingURL=index.d.ts.map
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.js"],"names":[],"mappings":""}
export * from './src/immutable-arraybuffer-pony.js';
export function isBufferImmutable(buffer: ArrayBuffer): boolean;
export function sliceBufferToImmutable(buffer: ArrayBuffer, start?: number, end?: number): ArrayBuffer;
export const optTransferBufferToImmutable: ((buffer: ArrayBuffer, newLength?: number) => ArrayBuffer) | undefined;
//# sourceMappingURL=immutable-arraybuffer-pony.d.ts.map
{"version":3,"file":"immutable-arraybuffer-pony.d.ts","sourceRoot":"","sources":["immutable-arraybuffer-pony.js"],"names":[],"mappings":"AAiMO,0CAJI,WAAW,GACT,OAAO,CAG0C;AASvD,+CALI,WAAW,UACX,MAAM,QACN,MAAM,GACJ,WAAW,CAgBvB;AAoCD,qDA7Ba,WAAW,cACX,MAAM,KACJ,WAAW,cA2B4C"}
/* global globalThis */
const {
ArrayBuffer,
Object,
Reflect,
Symbol,
TypeError,
Uint8Array,
WeakMap,
// Capture structuredClone before it can be scuttled.
structuredClone: optStructuredClone,
// eslint-disable-next-line no-restricted-globals
} = globalThis;
const { freeze, defineProperty, getPrototypeOf, getOwnPropertyDescriptor } =
Object;
const { apply, ownKeys } = Reflect;
const { toStringTag } = Symbol;
const { prototype: arrayBufferPrototype } = ArrayBuffer;
const { slice, transfer: optTransfer } = arrayBufferPrototype;
// @ts-expect-error TS doesn't know it'll be there
const { get: arrayBufferByteLength } = getOwnPropertyDescriptor(
arrayBufferPrototype,
'byteLength',
);
const typedArrayPrototype = getPrototypeOf(Uint8Array.prototype);
const { set: uint8ArraySet } = typedArrayPrototype;
// @ts-expect-error TS doesn't know it'll be there
const { get: uint8ArrayBuffer } = getOwnPropertyDescriptor(
typedArrayPrototype,
'buffer',
);
/**
* Copy a range of values from a genuine ArrayBuffer exotic object into a new
* ArrayBuffer.
*
* @param {ArrayBuffer} realBuffer
* @param {number} [start]
* @param {number} [end]
* @returns {ArrayBuffer}
*/
const arrayBufferSlice = (realBuffer, start = undefined, end = undefined) =>
apply(slice, realBuffer, [start, end]);
/**
* Move the contents of a genuine ArrayBuffer exotic object into a new fresh
* ArrayBuffer and detach the original source.
* We can only do this on platforms that support `structuredClone` or
* `ArrayBuffer.prototype.transfer`.
* On other platforms, we can still emulate
* `ArrayBuffer.prototoype.sliceToImmutable`, but not
* `ArrayBuffer.prototype.transferToImmutable`.
* Currently, these known-deficient platforms are
* - Hermes
* - Node.js <= 16
* - Apparently some versions of JavaScriptCore that are still of concern.
*
* @param {ArrayBuffer} arrayBuffer
* @returns {ArrayBuffer}
*/
let optArrayBufferTransfer;
if (optTransfer) {
optArrayBufferTransfer = arrayBuffer => apply(optTransfer, arrayBuffer, []);
} else if (optStructuredClone) {
optArrayBufferTransfer = arrayBuffer => {
// Hopefully, a zero-length slice is cheap, but still enforces that
// `arrayBuffer` is a genuine `ArrayBuffer` exotic object.
arrayBufferSlice(arrayBuffer, 0, 0);
return optStructuredClone(arrayBuffer, {
transfer: [arrayBuffer],
});
};
} else {
// Assignment is redundant, but remains for clarity.
optArrayBufferTransfer = undefined;
}
/**
* If we could use classes with private fields everywhere, this would have
* been a `this.#buffer` private field on an `ImmutableArrayBufferInternal`
* class. But we cannot do so on Hermes. So, instead, we
* emulate the `this.#buffer` private field, including its use as a brand check.
* Maps from all and only emulated Immutable ArrayBuffers to real ArrayBuffers.
*
* @type {Pick<WeakMap<ArrayBuffer, ArrayBuffer>, 'get' | 'has' | 'set'>}
*/
const buffers = new WeakMap();
// Avoid post-hoc prototype lookups.
for (const methodName of ['get', 'has', 'set']) {
defineProperty(buffers, methodName, { value: buffers[methodName] });
}
const getBuffer = immuAB => {
// Safe because this WeakMap owns its get method.
// eslint-disable-next-line @endo/no-polymorphic-call
const result = buffers.get(immuAB);
if (result) {
return result;
}
throw TypeError('Not an emulated Immutable ArrayBuffer');
};
// Omits `constructor` so `Array.prototype.constructor` is inherited.
const ImmutableArrayBufferInternalPrototype = {
__proto__: arrayBufferPrototype,
get byteLength() {
return apply(arrayBufferByteLength, getBuffer(this), []);
},
get detached() {
getBuffer(this); // shim brand check
return false;
},
get maxByteLength() {
// Not underlying maxByteLength, which is irrelevant
return apply(arrayBufferByteLength, getBuffer(this), []);
},
get resizable() {
getBuffer(this); // shim brand check
return false;
},
get immutable() {
getBuffer(this); // shim brand check
return true;
},
slice(start = undefined, end = undefined) {
return arrayBufferSlice(getBuffer(this), start, end);
},
sliceToImmutable(start = undefined, end = undefined) {
// eslint-disable-next-line no-use-before-define
return sliceBufferToImmutable(getBuffer(this), start, end);
},
resize(_newByteLength = undefined) {
getBuffer(this); // shim brand check
throw TypeError('Cannot resize an immutable ArrayBuffer');
},
transfer(_newLength = undefined) {
getBuffer(this); // shim brand check
throw TypeError('Cannot detach an immutable ArrayBuffer');
},
transferToFixedLength(_newLength = undefined) {
getBuffer(this); // shim brand check
throw TypeError('Cannot detach an immutable ArrayBuffer');
},
transferToImmutable(_newLength = undefined) {
getBuffer(this); // shim brand check
throw TypeError('Cannot detach an immutable ArrayBuffer');
},
/**
* See https://github.com/endojs/endo/tree/master/packages/immutable-arraybuffer#purposeful-violation
*/
[toStringTag]: 'ImmutableArrayBuffer',
};
// Better fidelity emulation of a class prototype
for (const key of ownKeys(ImmutableArrayBufferInternalPrototype)) {
defineProperty(ImmutableArrayBufferInternalPrototype, key, {
enumerable: false,
});
}
/**
* Emulates what would have been the encapsulated `ImmutableArrayBufferInternal`
* class constructor. This function takes the `realBuffer` which its
* result encapsulates. Security demands that this result has exclusive access
* to the `realBuffer` it is given, which its callers must ensure.
*
* @param {ArrayBuffer} realBuffer
* @returns {ArrayBuffer}
*/
const makeImmutableArrayBufferInternal = realBuffer => {
const result = /** @type {ArrayBuffer} */ (
/** @type {unknown} */ ({
__proto__: ImmutableArrayBufferInternalPrototype,
})
);
// Safe because this WeakMap owns its set method.
// eslint-disable-next-line @endo/no-polymorphic-call
buffers.set(result, realBuffer);
return result;
};
// Since `makeImmutableArrayBufferInternal` MUST not escape,
// this `freeze` is just belt-and-suspenders.
freeze(makeImmutableArrayBufferInternal);
/**
* @param {ArrayBuffer} buffer
* @returns {boolean}
*/
// eslint-disable-next-line @endo/no-polymorphic-call
export const isBufferImmutable = buffer => buffers.has(buffer);
/**
* Creates an immutable slice of the given buffer.
* @param {ArrayBuffer} buffer The original buffer.
* @param {number} [start] The start index.
* @param {number} [end] The end index.
* @returns {ArrayBuffer} The sliced immutable ArrayBuffer.
*/
export const sliceBufferToImmutable = (
buffer,
start = undefined,
end = undefined,
) => {
// Safe because this WeakMap owns its get method.
// eslint-disable-next-line @endo/no-polymorphic-call
let realBuffer = buffers.get(buffer);
if (realBuffer === undefined) {
realBuffer = buffer;
}
return makeImmutableArrayBufferInternal(
arrayBufferSlice(realBuffer, start, end),
);
};
let transferBufferToImmutable;
if (optArrayBufferTransfer) {
/**
* Transfer the contents to a new Immutable ArrayBuffer
*
* @param {ArrayBuffer} buffer The original buffer.
* @param {number} [newLength] The start index.
* @returns {ArrayBuffer}
*/
transferBufferToImmutable = (buffer, newLength = undefined) => {
if (newLength === undefined) {
buffer = optArrayBufferTransfer(buffer);
} else if (optTransfer) {
buffer = apply(optTransfer, buffer, [newLength]);
} else {
buffer = optArrayBufferTransfer(buffer);
const oldLength = buffer.byteLength;
// eslint-disable-next-line @endo/restrict-comparison-operands
if (newLength <= oldLength) {
buffer = arrayBufferSlice(buffer, 0, newLength);
} else {
const oldTA = new Uint8Array(buffer);
const newTA = new Uint8Array(newLength);
apply(uint8ArraySet, newTA, [oldTA]);
buffer = apply(uint8ArrayBuffer, newTA, []);
}
}
const result = makeImmutableArrayBufferInternal(buffer);
return /** @type {ArrayBuffer} */ (/** @type {unknown} */ (result));
};
} else {
transferBufferToImmutable = undefined;
}
export const optTransferBufferToImmutable = transferBufferToImmutable;
export {};
//# sourceMappingURL=immutable-arraybuffer-shim.d.ts.map
{"version":3,"file":"immutable-arraybuffer-shim.d.ts","sourceRoot":"","sources":["immutable-arraybuffer-shim.js"],"names":[],"mappings":""}
/* global globalThis */
import {
isBufferImmutable,
sliceBufferToImmutable,
optTransferBufferToImmutable as optXferBuf2Immu,
} from './immutable-arraybuffer-pony.js';
const {
ArrayBuffer,
JSON,
Object,
Reflect,
// eslint-disable-next-line no-restricted-globals
} = globalThis;
// Even though the imported one is not exported by the pony as a live binding,
// TS doesn't know that,
// so it cannot do its normal flow-based inference. By making and using a local
// copy, no problem.
const optTransferBufferToImmutable = optXferBuf2Immu;
const { getOwnPropertyDescriptors, defineProperties, defineProperty } = Object;
const { ownKeys } = Reflect;
const { prototype: arrayBufferPrototype } = ArrayBuffer;
const { stringify } = JSON;
const arrayBufferMethods = {
/**
* Creates an immutable slice of the given buffer.
*
* @this {ArrayBuffer} buffer The original buffer.
* @param {number} [start] The start index.
* @param {number} [end] The end index.
* @returns {ArrayBuffer} The sliced immutable ArrayBuffer.
*/
sliceToImmutable(start = undefined, end = undefined) {
return sliceBufferToImmutable(this, start, end);
},
/**
* @this {ArrayBuffer}
*/
get immutable() {
return isBufferImmutable(this);
},
...(optTransferBufferToImmutable
? {
/**
* Transfer the contents to a new Immutable ArrayBuffer
*
* @this {ArrayBuffer} buffer The original buffer.
* @param {number} [newLength] The start index.
* @returns {ArrayBuffer} The sliced immutable ArrayBuffer.
*/
transferToImmutable(newLength = undefined) {
return optTransferBufferToImmutable(this, newLength);
},
}
: {}),
};
// Better fidelity emulation of a class prototype
for (const key of ownKeys(arrayBufferMethods)) {
defineProperty(arrayBufferMethods, key, {
enumerable: false,
});
}
// Modern shim practice frowns on conditional installation, at least for
// proposals prior to stage 3. This is so changes to the proposal since
// an old shim was distributed don't need to worry about the proposal
// breaking old code depending on the old shim. Thus, if we detect that
// we're about to overwrite a prior installation, we simply issue this
// warning and continue.
//
// TODO, if the primordials are frozen after the prior implementation, such as
// by `lockdown`, then this precludes overwriting as expected. However, for
// this case, the following warning text will be confusing.
//
// Allowing polymorphic calls because these occur during initialization.
// eslint-disable-next-line @endo/no-polymorphic-call
const overwrites = ownKeys(arrayBufferMethods).filter(
key => key in arrayBufferPrototype,
);
if (overwrites.length > 0) {
// eslint-disable-next-line @endo/no-polymorphic-call
console.warn(
`About to overwrite ArrayBuffer.prototype properties ${stringify(overwrites)}`,
);
}
defineProperties(
arrayBufferPrototype,
getOwnPropertyDescriptors(arrayBufferMethods),
);