diff --git a/packages/no-trapping-shim/CHANGELOG.md b/packages/no-trapping-shim/CHANGELOG.md index 436a6104b3..420e6f23d0 100644 --- a/packages/no-trapping-shim/CHANGELOG.md +++ b/packages/no-trapping-shim/CHANGELOG.md @@ -1,143 +1 @@ # Change Log - -All notable changes to this project will be documented in this file. -See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. - -### [1.1.8](https://github.com/endojs/endo/compare/@endo/skel@1.1.7...@endo/skel@1.1.8) (2024-11-13) - -**Note:** Version bump only for package @endo/skel - - - - - -### [1.1.7](https://github.com/endojs/endo/compare/@endo/skel@1.1.6...@endo/skel@1.1.7) (2024-10-22) - -**Note:** Version bump only for package @endo/skel - - - - - -### [1.1.6](https://github.com/endojs/endo/compare/@endo/skel@1.1.5...@endo/skel@1.1.6) (2024-10-10) - -**Note:** Version bump only for package @endo/skel - - - - - -### [1.1.5](https://github.com/endojs/endo/compare/@endo/skel@1.1.4...@endo/skel@1.1.5) (2024-08-27) - -**Note:** Version bump only for package @endo/skel - - - - - -### [1.1.4](https://github.com/endojs/endo/compare/@endo/skel@1.1.3...@endo/skel@1.1.4) (2024-08-01) - -**Note:** Version bump only for package @endo/skel - - - - - -### [1.1.3](https://github.com/endojs/endo/compare/@endo/skel@1.1.2...@endo/skel@1.1.3) (2024-07-30) - -**Note:** Version bump only for package @endo/skel - - - - - -### [1.1.2](https://github.com/endojs/endo/compare/@endo/skel@1.1.1...@endo/skel@1.1.2) (2024-05-07) - -**Note:** Version bump only for package @endo/skel - - - - - -### [1.1.1](https://github.com/endojs/endo/compare/@endo/skel@1.1.0...@endo/skel@1.1.1) (2024-04-04) - -**Note:** Version bump only for package @endo/skel - - - - - -## [1.1.0](https://github.com/endojs/endo/compare/@endo/skel@1.0.4...@endo/skel@1.1.0) (2024-03-20) - - -### Features - -* **ses-ava:** import test from @endo/ses-ava/prepare-endo.js ([#2133](https://github.com/endojs/endo/issues/2133)) ([9d3a7ce](https://github.com/endojs/endo/commit/9d3a7ce150b6fd6fe7c8c4cc43da411e981731ac)) - - - -### [1.0.4](https://github.com/endojs/endo/compare/@endo/skel@1.0.3...@endo/skel@1.0.4) (2024-02-23) - -**Note:** Version bump only for package @endo/skel - - - - - -### [1.0.3](https://github.com/endojs/endo/compare/@endo/skel@1.0.2...@endo/skel@1.0.3) (2024-02-15) - - -### Bug Fixes - -* Add repository directory to all package descriptors ([e5f36e7](https://github.com/endojs/endo/commit/e5f36e7a321c13ee25e74eb74d2a5f3d7517119c)) - - - -### [1.0.2](https://github.com/endojs/endo/compare/@endo/skel@1.0.1...@endo/skel@1.0.2) (2024-01-18) - -**Note:** Version bump only for package @endo/skel - - - - - -### [1.0.1](https://github.com/endojs/endo/compare/@endo/skel@1.0.0...@endo/skel@1.0.1) (2023-12-20) - -**Note:** Version bump only for package @endo/skel - - - - - -## [1.0.0](https://github.com/endojs/endo/compare/@endo/skel@0.1.3...@endo/skel@1.0.0) (2023-12-12) - - -### Bug Fixes - -* Adjust type generation in release process and CI ([9465be3](https://github.com/endojs/endo/commit/9465be369e53167815ca444f6293a8e9eb48501d)) - - - -### [0.1.3](https://github.com/endojs/endo/compare/@endo/skel@0.1.2...@endo/skel@0.1.3) (2023-09-12) - -**Note:** Version bump only for package @endo/skel - - - - - -### 0.1.2 (2023-08-07) - - -### Bug Fixes - -* Fix scaffold and transforms yarn pack ([42439e7](https://github.com/endojs/endo/commit/42439e7d452e839b9856eac0e852766c237219d0)) - - - -### 0.1.1 (2023-08-07) - - -### Bug Fixes - -* Fix scaffold and transforms yarn pack ([42439e7](https://github.com/endojs/endo/commit/42439e7d452e839b9856eac0e852766c237219d0)) diff --git a/packages/no-trapping-shim/index.js b/packages/no-trapping-shim/index.js index e69de29bb2..42a68f007b 100644 --- a/packages/no-trapping-shim/index.js +++ b/packages/no-trapping-shim/index.js @@ -0,0 +1 @@ +export * from './src/no-trapping-pony.js'; diff --git a/packages/no-trapping-shim/package.json b/packages/no-trapping-shim/package.json index c76480f1b2..42c20f99de 100644 --- a/packages/no-trapping-shim/package.json +++ b/packages/no-trapping-shim/package.json @@ -2,7 +2,7 @@ "name": "@endo/no-trapping-shim", "version": "0.1.0", "private": true, - "description": null, + "description": "shim and ponyfill for no-trapping integrity level", "keywords": [], "author": "Endo contributors", "license": "Apache-2.0", @@ -20,6 +20,7 @@ "module": "./index.js", "exports": { ".": "./index.js", + "./shim.js": "./shim.js", "./package.json": "./package.json" }, "scripts": { @@ -35,10 +36,7 @@ "test:c8": "c8 $C8_OPTIONS ava --config=ava-nesm.config.js", "test:xs": "exit 0" }, - "dependencies": {}, "devDependencies": { - "@endo/lockdown": "workspace:^", - "@endo/ses-ava": "workspace:^", "ava": "^6.1.3", "c8": "^7.14.0", "tsd": "^0.31.2", diff --git a/packages/no-trapping-shim/shim.js b/packages/no-trapping-shim/shim.js new file mode 100644 index 0000000000..b20931f038 --- /dev/null +++ b/packages/no-trapping-shim/shim.js @@ -0,0 +1,10 @@ +/* global globalThis */ +import { ReflectPlus, ObjectPlus, ProxyPlus } from './src/no-trapping-pony.js'; + +globalThis.Reflect = ReflectPlus; + +globalThis.Object = ObjectPlus; +// eslint-disable-next-line no-extend-native +Object.prototype.constructor = ObjectPlus; + +globalThis.Proxy = ProxyPlus; diff --git a/packages/no-trapping-shim/src/no-trapping-pony.js b/packages/no-trapping-shim/src/no-trapping-pony.js new file mode 100644 index 0000000000..5b3d356225 --- /dev/null +++ b/packages/no-trapping-shim/src/no-trapping-pony.js @@ -0,0 +1,278 @@ +const OriginalObject = Object; +const OriginalReflect = Reflect; +const OriginalProxy = Proxy; +const { freeze, defineProperty, hasOwn } = OriginalObject; +const { apply, construct, ownKeys } = OriginalReflect; + +const noTrappingSet = new WeakSet(); + +const proxyHandlerMap = new WeakMap(); + +const isPrimitive = specimen => OriginalObject(specimen) !== specimen; + +/** + * Corresponds to the internal function shared by `Object.isNoTrapping` and + * `Reflect.isNoTrapping`. + * + * @param {any} specimen + * @param {boolean} shouldThrow + * @returns {boolean} + */ +const isNoTrappingInternal = (specimen, shouldThrow) => { + if (noTrappingSet.has(specimen)) { + return true; + } + if (!proxyHandlerMap.has(specimen)) { + return false; + } + const [target, handler] = proxyHandlerMap.get(specimen); + if (isNoTrappingInternal(target, shouldThrow)) { + noTrappingSet.add(specimen); + return true; + } + const trap = handler.isNoTrapping; + if (trap === undefined) { + return false; + } + const result = apply(trap, handler, [target]); + const ofTarget = isNoTrappingInternal(target, shouldThrow); + if (result !== ofTarget) { + if (shouldThrow) { + throw TypeError( + `'isNoTrapping' proxy trap does not reflect 'isNoTrapping' of proxy target (which is '${ofTarget}')`, + ); + } + return false; + } + if (result) { + noTrappingSet.add(specimen); + } + return result; +}; + +/** + * Corresponds to the internal function shared by `Object.suppressTrapping` and + * `Reflect.suppressTrapping`. + * + * @param {any} specimen + * @param {boolean} shouldThrow + * @returns {boolean} + */ +const suppressTrappingInternal = (specimen, shouldThrow) => { + if (noTrappingSet.has(specimen)) { + return true; + } + freeze(specimen); + if (!proxyHandlerMap.has(specimen)) { + noTrappingSet.add(specimen); + return true; + } + const [target, handler] = proxyHandlerMap.get(specimen); + if (isNoTrappingInternal(target, shouldThrow)) { + noTrappingSet.add(specimen); + return true; + } + const trap = handler.suppressTrapping; + if (trap === undefined) { + const result = suppressTrappingInternal(target, shouldThrow); + if (result) { + noTrappingSet.add(specimen); + } + return result; + } + const result = apply(trap, handler, [target]); + const ofTarget = isNoTrappingInternal(target, shouldThrow); + if (result !== ofTarget) { + if (shouldThrow) { + throw TypeError( + `'suppressTrapping' proxy trap does not reflect 'isNoTrapping' of proxy target (which is '${ofTarget}')`, + ); + } + return false; + } + if (result) { + noTrappingSet.add(specimen); + } + return result; +}; + +export const extraReflectMethods = freeze({ + isNoTrapping(target) { + if (isPrimitive(target)) { + throw TypeError('Reflect.isNoTrapping called on non-object'); + } + return isNoTrappingInternal(target, false); + }, + suppressTrapping(target) { + if (isPrimitive(target)) { + throw TypeError('Reflect.suppressTrapping called on non-object'); + } + return suppressTrappingInternal(target, false); + }, +}); + +export const extraObjectMethods = freeze({ + isNoTrapping(target) { + if (isPrimitive(target)) { + return true; + } + return isNoTrappingInternal(target, true); + }, + suppressTrapping(target) { + if (isPrimitive(target)) { + return target; + } + if (suppressTrappingInternal(target, true)) { + return target; + } + throw TypeError('preventExtensions trap returned falsy'); + }, +}); + +const addExtras = (base, ...extrasArgs) => { + for (const extras of extrasArgs) { + for (const key of ownKeys(extras)) { + if (base[key] !== extras[key]) { + defineProperty(base, key, { + value: extras[key], + writable: true, + enumerable: false, + configurable: true, + }); + } + } + } +}; + +/** In the shim, `ReflectPlus` replaces the global `Reflect`. */ +const ReflectPlus = {}; +addExtras(ReflectPlus, OriginalReflect, extraReflectMethods); +export { ReflectPlus }; + +/** + * In the shim, `ObjectPlus` replaces the global `Object`. + * + * @type {ObjectConstructor} + */ +// @ts-expect-error TS does not know the rest of the type is added below +const ObjectPlus = function Object(...args) { + if (new.target) { + return construct(OriginalObject, args, new.target); + } else { + return apply(OriginalObject, this, args); + } +}; +// @ts-expect-error We actually can assign to its `.prototype`. +ObjectPlus.prototype = OriginalObject.prototype; +addExtras(ObjectPlus, OriginalObject, extraObjectMethods); +export { ObjectPlus }; + +const metaHandler = freeze({ + get(_, trapName, handlerPlus) { + /** + * The `trapPlus` method is an enhanced version of + * `originalHandler[trapName]`. If the handlerPlus has no own `trapName` + * property, then the `get` of the metaHandler is called, which returns + * the `trapPlus`, which is then called as the trap of the returned + * proxyPlus. When so called, it installs an own `handlerPlus[trapName]` + * which is either `undefined` or this same `trapPlus`, to avoid further + * need to meta-handle that `handlerPlus[trapName]`. + * + * @param {any} target + * @param {any[]} rest + */ + const trapPlus = freeze((target, ...rest) => { + if (isNoTrappingInternal(target, true)) { + defineProperty(handlerPlus, trapName, { + value: undefined, + writable: false, + enumerable: true, + configurable: false, + }); + } else { + if (!hasOwn(handlerPlus, trapName)) { + defineProperty(handlerPlus, trapName, { + value: trapPlus, + writable: false, + enumerable: true, + configurable: true, + }); + } + const { originalHandler } = handlerPlus; + const trap = originalHandler[trapName]; + if (trap !== undefined) { + // Note that whether `trap === undefined` can change dynamically, + // so we do not install an own `handlerPlus[trapName] === undefined` + // for that case. We still install or preserve an own + // `handlerPlus[trapName] === trapPlus` until the target is + // seen to be non-trapping. + return apply(trap, originalHandler, [target, ...rest]); + } + } + return ReflectPlus[trapName](target, ...rest); + }); + return trapPlus; + }, +}); + +/** + * A handlerPlus starts as a fresh empty object that inherits from a proxy + * whose handler is the shared generic metaHandler. + * Thus, the metaHandler's `get` method is called only when the + * `handlerPlus` does not have a property overriding that `trapName`. + * In that case, the metaHandler's `get` is called with its `receiver` + * being the `handlerPlus`. + * + * @param {ProxyHandler} originalHandler + * @returns {ProxyHandler & { + * isNoTrapping: (target: any) => boolean, + * suppressTrapping: (target: any) => boolean, + * originalHandler: ProxyHandler + * }} + */ +const makeHandlerPlus = originalHandler => ({ + // @ts-expect-error TS does not know what this __proto__ is doing + __proto__: new OriginalProxy({}, metaHandler), + // relies on there never being a trap named `originalHandler`. + originalHandler, +}); + +/** + * In the shim, `ProxyPlus` replaces the global `Proxy`. + * + * @type {ProxyConstructor} + */ +// @ts-expect-error We reject non-new calls in the body +const ProxyPlus = function Proxy(target, handler) { + // @ts-expect-error Yes, we mean to compare these. + if (new.target !== ProxyPlus) { + if (new.target === undefined) { + throw TypeError('Proxy constructor requires "new"'); + } + throw TypeError('Safe Proxy shim does not support subclassing'); + } + const handlerPlus = makeHandlerPlus(handler); + const proxy = new OriginalProxy(target, handlerPlus); + proxyHandlerMap.set(proxy, [target, handler]); + return proxy; +}; +// The `OriginalProxy` is both constructible (i.e., responsive to `new`) and +// lacks a `prototype` property. The closest we can come to this is to set +// `ProxyPlus.prototype` to `undefined` +ProxyPlus.prototype = undefined; +ProxyPlus.revocable = (target, handler) => { + const handlerPlus = makeHandlerPlus(handler); + const { proxy, revoke } = OriginalProxy.revocable(target, handlerPlus); + proxyHandlerMap.set(proxy, [target, handler]); + return { + proxy, + revoke() { + if (isNoTrappingInternal(target, true)) { + throw TypeError('Cannot revoke non-trapping proxy'); + } + revoke(); + }, + }; +}; + +export { ProxyPlus }; diff --git a/packages/no-trapping-shim/test/index.test.js b/packages/no-trapping-shim/test/index.test.js deleted file mode 100644 index bf5a26862c..0000000000 --- a/packages/no-trapping-shim/test/index.test.js +++ /dev/null @@ -1,5 +0,0 @@ -import test from '@endo/ses-ava/prepare-endo.js'; - -test('placeholder', async t => { - t.fail('TODO: add tests'); -}); diff --git a/packages/no-trapping-shim/test/no-trapping-pony.test.js b/packages/no-trapping-shim/test/no-trapping-pony.test.js new file mode 100644 index 0000000000..15a2c385ff --- /dev/null +++ b/packages/no-trapping-shim/test/no-trapping-pony.test.js @@ -0,0 +1,29 @@ +// Uses 'ava' rather than @endo/ses-ava to avoid worries about cyclic +// dependencies. We will need similar tests is higher level packages, in order +// to test compat with ses and ses-ava. +import test from 'ava'; +import { ReflectPlus, ProxyPlus } from '../src/no-trapping-pony.js'; + +const { freeze, isFrozen } = Object; + +test('no-trapping-pony', async t => { + const specimen = { foo: 8 }; + + const sillyHandler = freeze({ + get(target, prop, receiver) { + return [target, prop, receiver]; + }, + }); + + const safeProxy = new ProxyPlus(specimen, sillyHandler); + + t.false(ReflectPlus.isNoTrapping(specimen)); + t.false(isFrozen(specimen)); + t.deepEqual(safeProxy.foo, [specimen, 'foo', safeProxy]); + + t.true(ReflectPlus.suppressTrapping(specimen)); + + t.true(ReflectPlus.isNoTrapping(specimen)); + t.true(isFrozen(specimen)); + t.deepEqual(safeProxy.foo, 8); +}); diff --git a/packages/no-trapping-shim/test/no-trapping-shim.test.js b/packages/no-trapping-shim/test/no-trapping-shim.test.js new file mode 100644 index 0000000000..b6ac48c25d --- /dev/null +++ b/packages/no-trapping-shim/test/no-trapping-shim.test.js @@ -0,0 +1,29 @@ +// Uses 'ava' rather than @endo/ses-ava to avoid worries about cyclic +// dependencies. We will need similar tests is higher level packages, in order +// to test compat with ses and ses-ava. +import test from 'ava'; +import '../shim.js'; + +const { freeze, isFrozen } = Object; + +test('no-trapping-pony', async t => { + const specimen = { foo: 8 }; + + const sillyHandler = freeze({ + get(target, prop, receiver) { + return [target, prop, receiver]; + }, + }); + + const safeProxy = new Proxy(specimen, sillyHandler); + + t.false(Reflect.isNoTrapping(specimen)); + t.false(isFrozen(specimen)); + t.deepEqual(safeProxy.foo, [specimen, 'foo', safeProxy]); + + t.true(Reflect.suppressTrapping(specimen)); + + t.true(Reflect.isNoTrapping(specimen)); + t.true(isFrozen(specimen)); + t.deepEqual(safeProxy.foo, 8); +}); diff --git a/yarn.lock b/yarn.lock index a838a69480..9a92741957 100644 --- a/yarn.lock +++ b/yarn.lock @@ -702,6 +702,17 @@ __metadata: languageName: unknown linkType: soft +"@endo/no-trapping-shim@workspace:packages/no-trapping-shim": + version: 0.0.0-use.local + resolution: "@endo/no-trapping-shim@workspace:packages/no-trapping-shim" + dependencies: + ava: "npm:^6.1.3" + c8: "npm:^7.14.0" + tsd: "npm:^0.31.2" + typescript: "npm:~5.6.3" + languageName: unknown + linkType: soft + "@endo/pass-style@workspace:^, @endo/pass-style@workspace:packages/pass-style": version: 0.0.0-use.local resolution: "@endo/pass-style@workspace:packages/pass-style"