From ccd003c96f3d969d919104118d8a34b3c1126aef Mon Sep 17 00:00:00 2001 From: "Mark S. Miller" Date: Sat, 14 Jan 2023 14:09:05 -0800 Subject: [PATCH] feat(pass-style): Extract passStyleOf and friends from marshal into the new pass-style package (#1439) --- packages/captp/src/captp.js | 10 ++ packages/far/package.json | 2 +- packages/far/src/index.js | 2 +- packages/marshal/index.js | 38 +---- packages/marshal/package.json | 1 + packages/marshal/src/deeplyFulfilled.js | 4 +- packages/marshal/src/dot-membrane.js | 5 +- packages/marshal/src/encodePassable.js | 17 ++- packages/marshal/src/encodeToCapData.js | 13 +- packages/marshal/src/encodeToSmallcaps.js | 14 +- packages/marshal/src/marshal-justin.js | 12 +- packages/marshal/src/marshal.js | 20 ++- packages/marshal/src/rankOrder.js | 4 +- packages/marshal/src/types.js | 144 ++---------------- packages/marshal/test/prepare-test-env-ava.js | 2 + packages/marshal/test/test-dot-membrane.js | 4 +- packages/marshal/test/test-marshal-capdata.js | 6 +- .../marshal/test/test-marshal-far-function.js | 5 +- packages/marshal/test/test-marshal-far-obj.js | 5 +- packages/marshal/test/test-marshal-justin.js | 4 +- .../marshal/test/test-marshal-smallcaps.js | 5 +- .../marshal/test/test-marshal-stringify.js | 3 +- packages/marshal/test/test-rankOrder.js | 8 +- .../doc}/copyArray-guarantees.md | 0 .../doc}/copyRecord-guarantees.md | 0 packages/pass-style/index.js | 35 +++++ packages/pass-style/package.json | 4 +- .../helpers => pass-style/src}/copyArray.js | 0 .../helpers => pass-style/src}/copyRecord.js | 0 .../src/helpers => pass-style/src}/error.js | 36 +++-- .../src}/internal-types.js | 8 +- .../src}/iter-helpers.js | 2 +- .../{marshal => pass-style}/src/make-far.js | 8 +- .../{marshal => pass-style}/src/makeTagged.js | 2 +- .../src}/passStyle-helpers.js | 4 +- .../src/passStyleOf.js | 28 ++-- .../helpers => pass-style/src}/remotable.js | 8 +- .../src}/safe-promise.js | 2 +- .../src/helpers => pass-style/src}/symbol.js | 2 +- .../src/helpers => pass-style/src}/tagged.js | 0 .../{marshal => pass-style}/src/typeGuards.js | 0 packages/pass-style/src/types.js | 141 +++++++++++++++++ .../test/test-passStyleOf.js | 2 +- 43 files changed, 331 insertions(+), 279 deletions(-) rename packages/{marshal/docs => pass-style/doc}/copyArray-guarantees.md (100%) rename packages/{marshal/docs => pass-style/doc}/copyRecord-guarantees.md (100%) rename packages/{marshal/src/helpers => pass-style/src}/copyArray.js (100%) rename packages/{marshal/src/helpers => pass-style/src}/copyRecord.js (100%) rename packages/{marshal/src/helpers => pass-style/src}/error.js (79%) rename packages/{marshal/src/helpers => pass-style/src}/internal-types.js (82%) rename packages/{marshal/src/helpers => pass-style/src}/iter-helpers.js (97%) rename packages/{marshal => pass-style}/src/make-far.js (97%) rename packages/{marshal => pass-style}/src/makeTagged.js (90%) rename packages/{marshal/src/helpers => pass-style/src}/passStyle-helpers.js (98%) rename packages/{marshal => pass-style}/src/passStyleOf.js (91%) rename packages/{marshal/src/helpers => pass-style/src}/remotable.js (96%) rename packages/{marshal/src/helpers => pass-style/src}/safe-promise.js (98%) rename packages/{marshal/src/helpers => pass-style/src}/symbol.js (98%) rename packages/{marshal/src/helpers => pass-style/src}/tagged.js (100%) rename packages/{marshal => pass-style}/src/typeGuards.js (100%) create mode 100644 packages/pass-style/src/types.js rename packages/{marshal => pass-style}/test/test-passStyleOf.js (99%) diff --git a/packages/captp/src/captp.js b/packages/captp/src/captp.js index c0e57afde7..f2cd176c93 100644 --- a/packages/captp/src/captp.js +++ b/packages/captp/src/captp.js @@ -4,6 +4,12 @@ // This logic was mostly lifted from @agoric/swingset-vat liveSlots.js // Defects in it are mfig's fault. +// +// @ts-ignore We actually mean the function, not the type, +// but TS somehow no longer knows that -- following the extraction +// of @endo/pass-style from @endo/marshal. +// We're using @ts-ignore here because TS is inconsistent about whether this +// line is an error. It is not locally, but it is under CI. import { Remotable, Far, makeMarshal, QCLASS } from '@endo/marshal'; import { E, HandledPromise } from '@endo/eventual-send'; import { isPromise, makePromiseKit } from '@endo/promise-kit'; @@ -326,6 +332,10 @@ export const makeCapTP = ( } // A new remote presence // Use Remotable rather than Far to make a remote from a presence + // + // @ts-expect-error We actually mean the function, not the type, + // but TS somehow no longer knows that -- following the extraction + // of @endo/pass-style from @endo/marshal. val = Remotable(iface, undefined, pr.resPres()); } else { // A new promise diff --git a/packages/far/package.json b/packages/far/package.json index 81fd22b869..11ca2dd227 100644 --- a/packages/far/package.json +++ b/packages/far/package.json @@ -27,7 +27,7 @@ "homepage": "https://github.com/endojs/endo#readme", "dependencies": { "@endo/eventual-send": "^0.16.9", - "@endo/marshal": "^0.8.2" + "@endo/pass-style": "^0.1.0" }, "devDependencies": { "@endo/init": "^0.5.53", diff --git a/packages/far/src/index.js b/packages/far/src/index.js index aa9ee9cb25..dc034c62c1 100644 --- a/packages/far/src/index.js +++ b/packages/far/src/index.js @@ -1,5 +1,5 @@ export { E } from '@endo/eventual-send'; -export { Far, getInterfaceOf, passStyleOf } from '@endo/marshal'; +export { Far, getInterfaceOf, passStyleOf } from '@endo/pass-style'; /** * @template Primary diff --git a/packages/marshal/index.js b/packages/marshal/index.js index 66feb4e782..7d426b729e 100644 --- a/packages/marshal/index.js +++ b/packages/marshal/index.js @@ -1,41 +1,17 @@ -export { mapIterable, filterIterable } from './src/helpers/iter-helpers.js'; -export { - PASS_STYLE, - isObject, - assertChecker, - getTag, - hasOwnPropertyOf, -} from './src/helpers/passStyle-helpers.js'; - -export { getErrorConstructor, toPassableError } from './src/helpers/error.js'; -export { getInterfaceOf } from './src/helpers/remotable.js'; - -export { - nameForPassableSymbol, - passableSymbolForName, -} from './src/helpers/symbol.js'; - -export { passStyleOf, assertPassable } from './src/passStyleOf.js'; - export { deeplyFulfilled } from './src/deeplyFulfilled.js'; -export { makeTagged } from './src/makeTagged.js'; -export { Remotable, Far, ToFarFunction } from './src/make-far.js'; - export { QCLASS } from './src/encodeToCapData.js'; export { makeMarshal } from './src/marshal.js'; export { stringify, parse } from './src/marshal-stringify.js'; export { decodeToJustin } from './src/marshal-justin.js'; -export { - assertRecord, - assertCopyArray, - assertRemotable, - isRemotable, - isRecord, - isCopyArray, -} from './src/typeGuards.js'; - // eslint-disable-next-line import/export export * from './src/types.js'; + +// For compatibility, but importers of these should instead import these +// directly from `@endo/pass-style` or (if applicable) `@endo/far`. +// @ts-expect-error TS only complains about this line when checking other +// packages that depend on this one, like marshal. The complaint is about +// repeatedly exported types. Specifically "Remotable". +export * from '@endo/pass-style'; diff --git a/packages/marshal/package.json b/packages/marshal/package.json index 1eb537973b..8a90431b8a 100644 --- a/packages/marshal/package.json +++ b/packages/marshal/package.json @@ -38,6 +38,7 @@ "dependencies": { "@endo/eventual-send": "^0.16.9", "@endo/nat": "^4.1.24", + "@endo/pass-style": "^0.1.0", "@endo/promise-kit": "^0.2.53" }, "devDependencies": { diff --git a/packages/marshal/src/deeplyFulfilled.js b/packages/marshal/src/deeplyFulfilled.js index 61f2a5a2ef..13b9fc86cc 100644 --- a/packages/marshal/src/deeplyFulfilled.js +++ b/packages/marshal/src/deeplyFulfilled.js @@ -2,9 +2,7 @@ import { E } from '@endo/eventual-send'; import { isPromise } from '@endo/promise-kit'; -import { getTag, isObject } from './helpers/passStyle-helpers.js'; -import { makeTagged } from './makeTagged.js'; -import { passStyleOf } from './passStyleOf.js'; +import { getTag, isObject, makeTagged, passStyleOf } from '@endo/pass-style'; /** @typedef {import('./types.js').Passable} Passable */ /** @template T @typedef {import('@endo/eventual-send').ERef} ERef */ diff --git a/packages/marshal/src/dot-membrane.js b/packages/marshal/src/dot-membrane.js index fa05f0dc6c..d1ad11831b 100644 --- a/packages/marshal/src/dot-membrane.js +++ b/packages/marshal/src/dot-membrane.js @@ -2,11 +2,8 @@ /// import { E } from '@endo/eventual-send'; -import { isObject } from './helpers/passStyle-helpers.js'; -import { getInterfaceOf } from './helpers/remotable.js'; -import { Far } from './make-far.js'; +import { isObject, getInterfaceOf, Far, passStyleOf } from '@endo/pass-style'; import { makeMarshal } from './marshal.js'; -import { passStyleOf } from './passStyleOf.js'; const { fromEntries } = Object; const { ownKeys } = Reflect; diff --git a/packages/marshal/src/encodePassable.js b/packages/marshal/src/encodePassable.js index bba9de05c2..9d0599cf3a 100644 --- a/packages/marshal/src/encodePassable.js +++ b/packages/marshal/src/encodePassable.js @@ -1,13 +1,13 @@ /* eslint-disable no-bitwise */ -import { getTag } from './helpers/passStyle-helpers.js'; -import { makeTagged } from './makeTagged.js'; -import { passStyleOf } from './passStyleOf.js'; -import { assertRecord } from './typeGuards.js'; import { + getTag, + makeTagged, + passStyleOf, + assertRecord, + isErrorLike, nameForPassableSymbol, passableSymbolForName, -} from './helpers/symbol.js'; -import { ErrorHelper } from './helpers/error.js'; +} from '@endo/pass-style'; /** @typedef {import('./types.js').PassStyle} PassStyle */ /** @typedef {import('./types.js').Passable} Passable */ @@ -305,7 +305,7 @@ export const makeEncodePassable = ({ encodeError = (err, _) => Fail`error unexpected: ${err}`, } = {}) => { const encodePassable = passable => { - if (ErrorHelper.canBeValid(passable)) { + if (isErrorLike(passable)) { return encodeError(passable, encodePassable); } const passStyle = passStyleOf(passable); @@ -470,6 +470,9 @@ harden(isEncodedRemotable); * prefix used by any cover so that ordinal mapping keys are always outside * the range of valid collection entry keys. */ +// @ts-expect-error TS does not understand thst `__proto__;` in this position +// is special syntax. Instead, it complains that the `null` is not a string, +// which would only make sense if this were defining a property. export const passStylePrefixes = harden({ __proto__: null, error: '!', diff --git a/packages/marshal/src/encodeToCapData.js b/packages/marshal/src/encodeToCapData.js index 963c4a5940..b2ba597fcd 100644 --- a/packages/marshal/src/encodeToCapData.js +++ b/packages/marshal/src/encodeToCapData.js @@ -6,20 +6,17 @@ // encodes to CapData, a JSON-representable data structure, and leaves it to // the caller (`marshal.js`) to stringify it. -import { passStyleOf } from './passStyleOf.js'; - -import { ErrorHelper } from './helpers/error.js'; -import { makeTagged } from './makeTagged.js'; import { + passStyleOf, + isErrorLike, + makeTagged, isObject, getTag, hasOwnPropertyOf, -} from './helpers/passStyle-helpers.js'; -import { assertPassableSymbol, nameForPassableSymbol, passableSymbolForName, -} from './helpers/symbol.js'; +} from '@endo/pass-style'; /** @typedef {import('./types.js').Passable} Passable */ /** @typedef {import('./types.js').Encoding} Encoding */ @@ -249,7 +246,7 @@ export const makeEncodeToCapData = ({ } }; const encodeToCapData = passable => { - if (ErrorHelper.canBeValid(passable)) { + if (isErrorLike(passable)) { // We pull out this special case to accommodate errors that are not // valid Passables. For example, because they're not frozen. // The special case can only ever apply at the root, and therefore diff --git a/packages/marshal/src/encodeToSmallcaps.js b/packages/marshal/src/encodeToSmallcaps.js index a76e0fb378..a3234aa1da 100644 --- a/packages/marshal/src/encodeToSmallcaps.js +++ b/packages/marshal/src/encodeToSmallcaps.js @@ -6,16 +6,16 @@ // encodes to Smallcaps, a JSON-representable data structure, and leaves it to // the caller (`marshal.js`) to stringify it. -import { passStyleOf } from './passStyleOf.js'; - -import { ErrorHelper } from './helpers/error.js'; -import { makeTagged } from './makeTagged.js'; -import { getTag, hasOwnPropertyOf } from './helpers/passStyle-helpers.js'; import { + passStyleOf, + isErrorLike, + makeTagged, + getTag, + hasOwnPropertyOf, assertPassableSymbol, nameForPassableSymbol, passableSymbolForName, -} from './helpers/symbol.js'; +} from '@endo/pass-style'; /** @typedef {import('./types.js').Passable} Passable */ /** @typedef {import('./types.js').Remotable} Remotable */ @@ -269,7 +269,7 @@ export const makeEncodeToSmallcaps = ({ } }; const encodeToSmallcaps = passable => { - if (ErrorHelper.canBeValid(passable)) { + if (isErrorLike(passable)) { // We pull out this special case to accommodate errors that are not // valid Passables. For example, because they're not frozen. // The special case can only ever apply at the root, and therefore diff --git a/packages/marshal/src/marshal-justin.js b/packages/marshal/src/marshal-justin.js index 697ef6cfd5..f69fdb343f 100644 --- a/packages/marshal/src/marshal-justin.js +++ b/packages/marshal/src/marshal-justin.js @@ -1,12 +1,13 @@ /// import { Nat } from '@endo/nat'; +import { + getErrorConstructor, + isObject, + passableSymbolForName, +} from '@endo/pass-style'; import { QCLASS } from './encodeToCapData.js'; -import { getErrorConstructor } from './helpers/error.js'; -import { isObject } from './helpers/passStyle-helpers.js'; -import { AtAtPrefixPattern, passableSymbolForName } from './helpers/symbol.js'; - /** @typedef {import('./types.js').Encoding} Encoding */ /** @template T @typedef {import('./types.js').CapData} CapData */ @@ -112,6 +113,9 @@ const makeNoIndenter = () => { }; const identPattern = /^[a-zA-Z]\w*$/; +harden(identPattern); +const AtAtPrefixPattern = /^@@(.*)$/; +harden(AtAtPrefixPattern); /** * @param {Encoding} encoding diff --git a/packages/marshal/src/marshal.js b/packages/marshal/src/marshal.js index c7a5cb7c2c..a0acaeee2a 100644 --- a/packages/marshal/src/marshal.js +++ b/packages/marshal/src/marshal.js @@ -1,10 +1,13 @@ /// import { Nat } from '@endo/nat'; -import { assertPassable } from './passStyleOf.js'; +import { + assertPassable, + getInterfaceOf, + getErrorConstructor, + hasOwnPropertyOf, +} from '@endo/pass-style'; -import { getInterfaceOf } from './helpers/remotable.js'; -import { getErrorConstructor } from './helpers/error.js'; import { QCLASS, makeEncodeToCapData, @@ -14,7 +17,6 @@ import { makeDecodeFromSmallcaps, makeEncodeToSmallcaps, } from './encodeToSmallcaps.js'; -import { hasOwnPropertyOf } from './helpers/passStyle-helpers.js'; /** @typedef {import('./types.js').MakeMarshalOptions} MakeMarshalOptions */ /** @template Slot @typedef {import('./types.js').ConvertSlotToVal} ConvertSlotToVal */ @@ -99,7 +101,7 @@ export const makeMarshal = ( * Even if an Error is not actually passable, we'd rather send * it anyway because the diagnostic info carried by the error * is more valuable than diagnosing why the error isn't - * passable. See comments in ErrorHelper. + * passable. See comments in isErrorLike. * * @param {Error} err * @param {(p: Passable) => unknown} encodeRecur @@ -156,7 +158,7 @@ export const makeMarshal = ( * Even if an Error is not actually passable, we'd rather send * it anyway because the diagnostic info carried by the error * is more valuable than diagnosing why the error isn't - * passable. See comments in ErrorHelper. + * passable. See comments in isErrorLike. * * @param {Error} err * @param {(p: Passable) => Encoding} encodeRecur @@ -323,7 +325,13 @@ export const makeMarshal = ( }; const reviveFromSmallcaps = makeDecodeFromSmallcaps({ + // @ts-expect-error This error started after separating pass-style + // out of marshal into its own package. Aside from that, I do not + // understand this error at all. decodeRemotableFromSmallcaps, + // @ts-expect-error This error started after separating pass-style + // out of marshal into its own package. Aside from that, I do not + // understand this error at all. decodePromiseFromSmallcaps, decodeErrorFromSmallcaps, }); diff --git a/packages/marshal/src/rankOrder.js b/packages/marshal/src/rankOrder.js index bd5e45a33e..6f864fcd1e 100644 --- a/packages/marshal/src/rankOrder.js +++ b/packages/marshal/src/rankOrder.js @@ -1,6 +1,4 @@ -import { getTag } from './helpers/passStyle-helpers.js'; -import { passStyleOf } from './passStyleOf.js'; -import { nameForPassableSymbol } from './helpers/symbol.js'; +import { getTag, passStyleOf, nameForPassableSymbol } from '@endo/pass-style'; import { passStylePrefixes, recordNames, diff --git a/packages/marshal/src/types.js b/packages/marshal/src/types.js index 6a9d1c80d5..e79093ce71 100644 --- a/packages/marshal/src/types.js +++ b/packages/marshal/src/types.js @@ -5,101 +5,6 @@ export {}; -/** - * @typedef { "undefined" | "null" | - * "boolean" | "number" | "bigint" | "string" | "symbol" - * } PrimitiveStyle - */ - -/** - * @typedef { PrimitiveStyle | - * "copyRecord" | "copyArray" | "tagged" | - * "remotable" | - * "error" | "promise" - * } PassStyle - */ - -// TODO declare more precise types throughout this file, so the type system -// and IDE can be more helpful. - -/** - * @typedef {*} Passable - * - * A Passable value that may be marshalled. It is classified as one of - * PassStyle. A Passable must be hardened. - * - * A Passable has a pass-by-copy superstructure. This includes - * * the atomic pass-by-copy primitives ("undefined" | "null" | - * "boolean" | "number" | "bigint" | "string" | "symbol"), - * * the pass-by-copy containers - * ("copyRecord" | "copyArray" | "tagged") that - * contain other Passables, - * * and the special cases ("error" | "promise"). - * - * A Passable's pass-by-copy superstructure ends in - * PassableCap leafs ("remotable" | "promise"). Since a - * Passable is hardened, its structure and classification is stable --- its - * structure and classification cannot change even if some of the objects are - * proxies. - */ - -/** - * @callback PassStyleOf - * @param {Passable} passable - * @returns {PassStyle} - */ - -/** - * @typedef {Passable} PureData - * - * A Passable is PureData when its pass-by-copy superstructure whose - * nodes are pass-by-copy composites (CopyArray, CopyRecord, Tagged) leaves are - * primitives or empty composites. No remotables, promises, or errors. - * - * This check assures purity *given* that none of these pass-by-copy composites - * can be a Proxy. TODO SECURITY BUG we plan to enforce this, giving these - * pass-by-copy composites much of the same security properties as the - * proposed Records and Tuples (TODO need link). - * - * Given this (currently counter-factual) assumption, a PureData value cannot - * be used as a communications channel, - * and can therefore be safely shared with subgraphs that should not be able - * to communicate with each other. - */ - -/** - * @typedef {Passable} Remotable - * Might be an object explicitly declared to be `Remotable` using the - * `Far` or `Remotable` functions, or a remote presence of a Remotable. - */ - -/** - * @typedef {Promise | Remotable} PassableCap - * The leaves of a Passable's pass-by-copy superstructure. - */ - -/** - * @template T - * @typedef {T[]} CopyArray - */ - -/** - * @template T - * @typedef {Record} CopyRecord - */ - -/** - * @typedef {{ - * [PASS_STYLE]: 'tagged', - * [Symbol.toStringTag]: string, - * payload: Passable - * }} CopyTagged - * - * The tag is the value of the `[String.toStringTag]` property. - */ - -// ///////////////////////////////////////////////////////////////////////////// - /** * @template Slot * @callback ConvertValToSlot @@ -225,48 +130,19 @@ export {}; * `'#'`. */ -// ///////////////////////////////////////////////////////////////////////////// - -/** - * @typedef {string} InterfaceSpec - * This is an interface specification. - * For now, it is just a string, but will eventually be `PureData`. Either - * way, it must remain pure, so that it can be safely shared by subgraphs that - * are not supposed to be able to communicate. - */ - -/** - * @callback MarshalGetInterfaceOf - * Simple semantics, just tell what interface (or undefined) a remotable has. - * @param {*} maybeRemotable the value to check - * @returns {InterfaceSpec|undefined} the interface specification, or undefined - * if not a deemed to be a Remotable - */ - -/** - * @callback Checker - * Internal to a useful pattern for writing checking logic - * (a "checkFoo" function) that can be used to implement a predicate - * (an "isFoo" function) or a validator (an "assertFoo" function). - * - * * A predicate ideally only returns `true` or `false` and rarely throws. - * * A validator throws an informative diagnostic when the predicate - * would have returned `false`, and simply returns `undefined` normally - * when the predicate would have returned `true`. - * * The internal checking function that they share is parameterized by a - * `Checker` that determines how to proceed with a failure condition. - * Predicates pass in an identity function as checker. Validators - * pass in `assertChecker` which is a trivial wrapper around `assert`. - * - * See the various uses for good examples. - * @param {boolean} cond - * @param {Details=} details - * @returns {boolean} - */ - /** * @typedef {[string, string]} RankCover * RankCover represents the inclusive lower bound and *inclusive* upper bound * of a string-comparison range that covers all possible encodings for * a set of values. */ + +// /////////////////////// Type reexports @endo/pass-style ///////////////////// + +/** @typedef {import('@endo/pass-style').Checker} Checker */ +/** @typedef {import('@endo/pass-style').PassStyle} PassStyle */ +/** @typedef {import('@endo/pass-style').Passable} Passable */ +/** @typedef {import('@endo/pass-style').Remotable} Remotable */ +/** @template T @typedef {import('@endo/pass-style').CopyArray} CopyArray */ +/** @template T @typedef {import('@endo/pass-style').CopyRecord} CopyRecord */ +/** @typedef {import('@endo/pass-style').InterfaceSpec} InterfaceSpec */ diff --git a/packages/marshal/test/prepare-test-env-ava.js b/packages/marshal/test/prepare-test-env-ava.js index 7d0c766f8f..cc67801b3f 100644 --- a/packages/marshal/test/prepare-test-env-ava.js +++ b/packages/marshal/test/prepare-test-env-ava.js @@ -1,6 +1,8 @@ // @ts-nocheck +// eslint-disable-next-line import/no-extraneous-dependencies import '@endo/init/debug.js'; +// eslint-disable-next-line import/no-extraneous-dependencies import { wrapTest } from '@endo/ses-ava'; import rawTest from 'ava'; diff --git a/packages/marshal/test/test-dot-membrane.js b/packages/marshal/test/test-dot-membrane.js index 5c52597977..045b540217 100644 --- a/packages/marshal/test/test-dot-membrane.js +++ b/packages/marshal/test/test-dot-membrane.js @@ -1,6 +1,8 @@ import { test } from './prepare-test-env-ava.js'; + +// eslint-disable-next-line import/order +import { Far } from '@endo/pass-style'; import { makeDotMembraneKit } from '../src/dot-membrane.js'; -import { Far } from '../src/make-far.js'; test('test dot-membrane basics', t => { /** @type {any} */ diff --git a/packages/marshal/test/test-marshal-capdata.js b/packages/marshal/test/test-marshal-capdata.js index cdb402f8f3..71665bb060 100644 --- a/packages/marshal/test/test-marshal-capdata.js +++ b/packages/marshal/test/test-marshal-capdata.js @@ -1,10 +1,8 @@ import { test } from './prepare-test-env-ava.js'; -import { passStyleOf } from '../src/passStyleOf.js'; - +// eslint-disable-next-line import/order +import { passStyleOf, makeTagged, Far } from '@endo/pass-style'; import { makeMarshal } from '../src/marshal.js'; -import { makeTagged } from '../src/makeTagged.js'; -import { Far } from '../src/make-far.js'; const { freeze, isFrozen, create, prototype: objectPrototype } = Object; diff --git a/packages/marshal/test/test-marshal-far-function.js b/packages/marshal/test/test-marshal-far-function.js index 81428bac2a..42ec771353 100644 --- a/packages/marshal/test/test-marshal-far-function.js +++ b/packages/marshal/test/test-marshal-far-function.js @@ -1,8 +1,7 @@ import { test } from './prepare-test-env-ava.js'; -import { getInterfaceOf } from '../src/helpers/remotable.js'; -import { passStyleOf } from '../src/passStyleOf.js'; -import { Far } from '../src/make-far.js'; +// eslint-disable-next-line import/order +import { getInterfaceOf, passStyleOf, Far } from '@endo/pass-style'; const { freeze, setPrototypeOf } = Object; diff --git a/packages/marshal/test/test-marshal-far-obj.js b/packages/marshal/test/test-marshal-far-obj.js index 2dd3613b4f..a85a8d40ac 100644 --- a/packages/marshal/test/test-marshal-far-obj.js +++ b/packages/marshal/test/test-marshal-far-obj.js @@ -1,10 +1,9 @@ import { test } from './prepare-test-env-ava.js'; -import { passStyleOf } from '../src/passStyleOf.js'; +// eslint-disable-next-line import/order +import { passStyleOf, Remotable, Far, getInterfaceOf } from '@endo/pass-style'; -import { Remotable, Far } from '../src/make-far.js'; import { makeMarshal } from '../src/marshal.js'; -import { getInterfaceOf } from '../src/helpers/remotable.js'; const { quote: q } = assert; const { create, getPrototypeOf, prototype: objectPrototype } = Object; diff --git a/packages/marshal/test/test-marshal-justin.js b/packages/marshal/test/test-marshal-justin.js index 042cd839e7..0980037304 100644 --- a/packages/marshal/test/test-marshal-justin.js +++ b/packages/marshal/test/test-marshal-justin.js @@ -1,7 +1,7 @@ import { test } from './prepare-test-env-ava.js'; -import { Remotable } from '../src/make-far.js'; -import { makeTagged } from '../src/makeTagged.js'; +// eslint-disable-next-line import/order +import { Remotable, makeTagged } from '@endo/pass-style'; import { makeMarshal } from '../src/marshal.js'; import { decodeToJustin } from '../src/marshal-justin.js'; diff --git a/packages/marshal/test/test-marshal-smallcaps.js b/packages/marshal/test/test-marshal-smallcaps.js index 41d1cc060b..f450b30ed0 100644 --- a/packages/marshal/test/test-marshal-smallcaps.js +++ b/packages/marshal/test/test-marshal-smallcaps.js @@ -1,11 +1,10 @@ import { test } from './prepare-test-env-ava.js'; -import { Far } from '../src/make-far.js'; +// eslint-disable-next-line import/order +import { Far, makeTagged, passStyleOf } from '@endo/pass-style'; import { makeMarshal } from '../src/marshal.js'; import { roundTripPairs } from './test-marshal-capdata.js'; -import { makeTagged } from '../src/makeTagged.js'; -import { passStyleOf } from '../src/passStyleOf.js'; const { freeze, isFrozen, create, prototype: objectPrototype } = Object; diff --git a/packages/marshal/test/test-marshal-stringify.js b/packages/marshal/test/test-marshal-stringify.js index a0d240c1e0..e10fea157a 100644 --- a/packages/marshal/test/test-marshal-stringify.js +++ b/packages/marshal/test/test-marshal-stringify.js @@ -1,6 +1,7 @@ import { test } from './prepare-test-env-ava.js'; -import { Far } from '../src/make-far.js'; +// eslint-disable-next-line import/order +import { Far } from '@endo/pass-style'; import { stringify, parse } from '../src/marshal-stringify.js'; import { roundTripPairs } from './test-marshal-capdata.js'; diff --git a/packages/marshal/test/test-rankOrder.js b/packages/marshal/test/test-rankOrder.js index d896d502c3..b93abcdfbd 100644 --- a/packages/marshal/test/test-rankOrder.js +++ b/packages/marshal/test/test-rankOrder.js @@ -1,7 +1,10 @@ // @ts-nocheck +// eslint-disable-next-line import/order import { test } from './prepare-test-env-ava.js'; -// eslint-disable-next-line import/order, import/no-extraneous-dependencies + +// eslint-disable-next-line import/no-extraneous-dependencies import { fc } from '@fast-check/ava'; +import { makeTagged, Far } from '@endo/pass-style'; import { FullRankCover, @@ -13,9 +16,6 @@ import { assertRankSorted, } from '../src/rankOrder.js'; -import { makeTagged } from '../src/makeTagged.js'; -import { Far } from '../src/make-far.js'; - const { quote: q } = assert; /** diff --git a/packages/marshal/docs/copyArray-guarantees.md b/packages/pass-style/doc/copyArray-guarantees.md similarity index 100% rename from packages/marshal/docs/copyArray-guarantees.md rename to packages/pass-style/doc/copyArray-guarantees.md diff --git a/packages/marshal/docs/copyRecord-guarantees.md b/packages/pass-style/doc/copyRecord-guarantees.md similarity index 100% rename from packages/marshal/docs/copyRecord-guarantees.md rename to packages/pass-style/doc/copyRecord-guarantees.md diff --git a/packages/pass-style/index.js b/packages/pass-style/index.js index e69de29bb2..af544b7fde 100644 --- a/packages/pass-style/index.js +++ b/packages/pass-style/index.js @@ -0,0 +1,35 @@ +export { mapIterable, filterIterable } from './src/iter-helpers.js'; +export { + PASS_STYLE, + isObject, + assertChecker, + getTag, + hasOwnPropertyOf, +} from './src/passStyle-helpers.js'; + +export { + getErrorConstructor, + toPassableError, + isErrorLike, +} from './src/error.js'; +export { getInterfaceOf } from './src/remotable.js'; + +export { + nameForPassableSymbol, + passableSymbolForName, + assertPassableSymbol, +} from './src/symbol.js'; + +export { passStyleOf, assertPassable } from './src/passStyleOf.js'; + +export { makeTagged } from './src/makeTagged.js'; +export { Remotable, Far, ToFarFunction } from './src/make-far.js'; + +export { + assertRecord, + assertCopyArray, + assertRemotable, + isRemotable, + isRecord, + isCopyArray, +} from './src/typeGuards.js'; diff --git a/packages/pass-style/package.json b/packages/pass-style/package.json index cafdbe5565..04fe6d5a77 100644 --- a/packages/pass-style/package.json +++ b/packages/pass-style/package.json @@ -29,7 +29,9 @@ "lint:types": "tsc -p jsconfig.json", "test": "ava" }, - "dependencies": {}, + "dependencies": { + "@endo/promise-kit": "^0.2.53" + }, "devDependencies": { "@endo/eslint-config": "^0.5.2", "@endo/init": "^0.5.53", diff --git a/packages/marshal/src/helpers/copyArray.js b/packages/pass-style/src/copyArray.js similarity index 100% rename from packages/marshal/src/helpers/copyArray.js rename to packages/pass-style/src/copyArray.js diff --git a/packages/marshal/src/helpers/copyRecord.js b/packages/pass-style/src/copyRecord.js similarity index 100% rename from packages/marshal/src/helpers/copyRecord.js rename to packages/pass-style/src/copyRecord.js diff --git a/packages/marshal/src/helpers/error.js b/packages/pass-style/src/error.js similarity index 79% rename from packages/marshal/src/helpers/error.js rename to packages/pass-style/src/error.js index dcc548007c..2268236c29 100644 --- a/packages/marshal/src/helpers/error.js +++ b/packages/pass-style/src/error.js @@ -3,6 +3,7 @@ import { assertChecker } from './passStyle-helpers.js'; /** @typedef {import('./internal-types.js').PassStyleHelper} PassStyleHelper */ +/** @typedef {import('./types.js').Checker} Checker */ const { details: X, Fail } = assert; const { getPrototypeOf, getOwnPropertyDescriptors } = Object; @@ -24,34 +25,49 @@ const errorConstructors = new Map([ export const getErrorConstructor = name => errorConstructors.get(name); harden(getErrorConstructor); +/** + * @param {unknown} candidate + * @param {Checker} [check] + * @returns {boolean} + */ +const checkErrorLike = (candidate, check = undefined) => { + const reject = !!check && (details => check(false, details)); + // TODO: Need a better test than instanceof + return ( + candidate instanceof Error || + (reject && reject(X`Error expected: ${candidate}`)) + ); +}; +harden(checkErrorLike); + /** * Validating error objects are passable raises a tension between security * vs preserving diagnostic information. For errors, we need to remember * the error itself exists to help us diagnose a bug that's likely more * pressing than a validity bug in the error itself. Thus, whenever it is safe - * to do so, we prefer to let the error test succeed and to couch these + * to do so, we prefer to let the error-like test succeed and to couch these * complaints as notes on the error. * * To resolve this, such a malformed error object will still pass - * `canBeValid` so marshal can use this for top level error to report from, + * `isErrorLike` so marshal can use this for top level error to report from, * even if it would not actually validate. * Instead, the diagnostics that `assertError` would have reported are * attached as notes to the malformed error. Thus, a malformed * error is passable by itself, but not as part of a passable structure. * + * @param {unknown} candidate + * @returns {boolean} + */ +export const isErrorLike = candidate => checkErrorLike(candidate); +harden(isErrorLike); + +/** * @type {PassStyleHelper} */ export const ErrorHelper = harden({ styleName: 'error', - canBeValid: (candidate, check = undefined) => { - const reject = !!check && (details => check(false, details)); - // TODO: Need a better test than instanceof - return ( - candidate instanceof Error || - (reject && reject(X`Error expected: ${candidate}`)) - ); - }, + canBeValid: checkErrorLike, assertValid: candidate => { ErrorHelper.canBeValid(candidate, assertChecker); diff --git a/packages/marshal/src/helpers/internal-types.js b/packages/pass-style/src/internal-types.js similarity index 82% rename from packages/marshal/src/helpers/internal-types.js rename to packages/pass-style/src/internal-types.js index b4e1f39934..899793fe05 100644 --- a/packages/marshal/src/helpers/internal-types.js +++ b/packages/pass-style/src/internal-types.js @@ -1,10 +1,8 @@ -/// - export {}; -/** @typedef {import('../types.js').Checker} Checker */ -/** @typedef {import('../types.js').PassStyle} PassStyle */ -/** @typedef {import('../types.js').PassStyleOf} PassStyleOf */ +/** @typedef {import('./types.js').Checker} Checker */ +/** @typedef {import('./types.js').PassStyle} PassStyle */ +/** @typedef {import('./types.js').PassStyleOf} PassStyleOf */ /** * The PassStyleHelper are only used to make a `passStyleOf` function. diff --git a/packages/marshal/src/helpers/iter-helpers.js b/packages/pass-style/src/iter-helpers.js similarity index 97% rename from packages/marshal/src/helpers/iter-helpers.js rename to packages/pass-style/src/iter-helpers.js index ec930a33b2..15819de211 100644 --- a/packages/marshal/src/helpers/iter-helpers.js +++ b/packages/pass-style/src/iter-helpers.js @@ -1,4 +1,4 @@ -import { Far } from '../make-far.js'; +import { Far } from './make-far.js'; /** * The result iterator has as many elements as the `baseIterator` and diff --git a/packages/marshal/src/make-far.js b/packages/pass-style/src/make-far.js similarity index 97% rename from packages/marshal/src/make-far.js rename to packages/pass-style/src/make-far.js index 70d6e6cb2d..dae9e469d0 100644 --- a/packages/marshal/src/make-far.js +++ b/packages/pass-style/src/make-far.js @@ -1,11 +1,7 @@ /// -import { assertChecker, PASS_STYLE } from './helpers/passStyle-helpers.js'; -import { - assertIface, - getInterfaceOf, - RemotableHelper, -} from './helpers/remotable.js'; +import { assertChecker, PASS_STYLE } from './passStyle-helpers.js'; +import { assertIface, getInterfaceOf, RemotableHelper } from './remotable.js'; /** @typedef {import('./types.js').InterfaceSpec} InterfaceSpec */ /** @template L,R @typedef {import('@endo/eventual-send').RemotableBrand} RemotableBrand */ diff --git a/packages/marshal/src/makeTagged.js b/packages/pass-style/src/makeTagged.js similarity index 90% rename from packages/marshal/src/makeTagged.js rename to packages/pass-style/src/makeTagged.js index 1bfb2ebb09..0721ae07c5 100644 --- a/packages/marshal/src/makeTagged.js +++ b/packages/pass-style/src/makeTagged.js @@ -1,6 +1,6 @@ /// -import { PASS_STYLE } from './helpers/passStyle-helpers.js'; +import { PASS_STYLE } from './passStyle-helpers.js'; import { assertPassable } from './passStyleOf.js'; const { create, prototype: objectPrototype } = Object; diff --git a/packages/marshal/src/helpers/passStyle-helpers.js b/packages/pass-style/src/passStyle-helpers.js similarity index 98% rename from packages/marshal/src/helpers/passStyle-helpers.js rename to packages/pass-style/src/passStyle-helpers.js index 6ac47dd341..91d7786d36 100644 --- a/packages/marshal/src/helpers/passStyle-helpers.js +++ b/packages/pass-style/src/passStyle-helpers.js @@ -1,7 +1,7 @@ /// -/** @typedef {import('../types.js').Checker} Checker */ -/** @typedef {import('../types.js').PassStyle} PassStyle */ +/** @typedef {import('./types.js').Checker} Checker */ +/** @typedef {import('./types.js').PassStyle} PassStyle */ const { details: X, quote: q } = assert; const { isArray } = Array; diff --git a/packages/marshal/src/passStyleOf.js b/packages/pass-style/src/passStyleOf.js similarity index 91% rename from packages/marshal/src/passStyleOf.js rename to packages/pass-style/src/passStyleOf.js index a1f85013e6..ab267a42de 100644 --- a/packages/marshal/src/passStyleOf.js +++ b/packages/pass-style/src/passStyleOf.js @@ -1,22 +1,18 @@ /// import { isPromise } from '@endo/promise-kit'; -import { - isObject, - isTypedArray, - PASS_STYLE, -} from './helpers/passStyle-helpers.js'; - -import { CopyArrayHelper } from './helpers/copyArray.js'; -import { CopyRecordHelper } from './helpers/copyRecord.js'; -import { TaggedHelper } from './helpers/tagged.js'; -import { ErrorHelper } from './helpers/error.js'; -import { RemotableHelper } from './helpers/remotable.js'; - -import { assertPassableSymbol } from './helpers/symbol.js'; -import { assertSafePromise } from './helpers/safe-promise.js'; - -/** @typedef {import('./helpers/internal-types.js').PassStyleHelper} PassStyleHelper */ +import { isObject, isTypedArray, PASS_STYLE } from './passStyle-helpers.js'; + +import { CopyArrayHelper } from './copyArray.js'; +import { CopyRecordHelper } from './copyRecord.js'; +import { TaggedHelper } from './tagged.js'; +import { ErrorHelper } from './error.js'; +import { RemotableHelper } from './remotable.js'; + +import { assertPassableSymbol } from './symbol.js'; +import { assertSafePromise } from './safe-promise.js'; + +/** @typedef {import('./internal-types.js').PassStyleHelper} PassStyleHelper */ /** @typedef {import('./types.js').Passable} Passable */ /** @typedef {import('./types.js').PassStyle} PassStyle */ /** @typedef {import('./types.js').PassStyleOf} PassStyleOf */ diff --git a/packages/marshal/src/helpers/remotable.js b/packages/pass-style/src/remotable.js similarity index 96% rename from packages/marshal/src/helpers/remotable.js rename to packages/pass-style/src/remotable.js index 19ed3a294f..ca082a6664 100644 --- a/packages/marshal/src/helpers/remotable.js +++ b/packages/pass-style/src/remotable.js @@ -11,11 +11,11 @@ import { getTag, } from './passStyle-helpers.js'; -/** @typedef {import('../types.js').Checker} Checker */ -/** @typedef {import('../types.js').InterfaceSpec} InterfaceSpec */ -/** @typedef {import('../types.js').MarshalGetInterfaceOf} MarshalGetInterfaceOf */ +/** @typedef {import('./types.js').Checker} Checker */ +/** @typedef {import('./types.js').InterfaceSpec} InterfaceSpec */ +/** @typedef {import('./types.js').MarshalGetInterfaceOf} MarshalGetInterfaceOf */ /** @typedef {import('./internal-types.js').PassStyleHelper} PassStyleHelper */ -/** @typedef {import('../types.js').Remotable} Remotable */ +/** @typedef {import('./types.js').Remotable} Remotable */ const { details: X, Fail, quote: q } = assert; const { ownKeys } = Reflect; diff --git a/packages/marshal/src/helpers/safe-promise.js b/packages/pass-style/src/safe-promise.js similarity index 98% rename from packages/marshal/src/helpers/safe-promise.js rename to packages/pass-style/src/safe-promise.js index 427c8787c7..73d0760c22 100644 --- a/packages/marshal/src/helpers/safe-promise.js +++ b/packages/pass-style/src/safe-promise.js @@ -3,7 +3,7 @@ import { isPromise } from '@endo/promise-kit'; import { assertChecker, hasOwnPropertyOf } from './passStyle-helpers.js'; -/** @typedef {import('../types.js').Checker} Checker */ +/** @typedef {import('./types.js').Checker} Checker */ const { details: X, quote: q } = assert; const { isFrozen, getPrototypeOf } = Object; diff --git a/packages/marshal/src/helpers/symbol.js b/packages/pass-style/src/symbol.js similarity index 98% rename from packages/marshal/src/helpers/symbol.js rename to packages/pass-style/src/symbol.js index 313c8d8407..f67853a3bb 100644 --- a/packages/marshal/src/helpers/symbol.js +++ b/packages/pass-style/src/symbol.js @@ -72,7 +72,7 @@ export const nameForPassableSymbol = sym => { }; harden(nameForPassableSymbol); -export const AtAtPrefixPattern = /^@@(.*)$/; +const AtAtPrefixPattern = /^@@(.*)$/; harden(AtAtPrefixPattern); /** diff --git a/packages/marshal/src/helpers/tagged.js b/packages/pass-style/src/tagged.js similarity index 100% rename from packages/marshal/src/helpers/tagged.js rename to packages/pass-style/src/tagged.js diff --git a/packages/marshal/src/typeGuards.js b/packages/pass-style/src/typeGuards.js similarity index 100% rename from packages/marshal/src/typeGuards.js rename to packages/pass-style/src/typeGuards.js diff --git a/packages/pass-style/src/types.js b/packages/pass-style/src/types.js new file mode 100644 index 0000000000..bee10e201c --- /dev/null +++ b/packages/pass-style/src/types.js @@ -0,0 +1,141 @@ +export {}; + +/** + * @typedef { "undefined" | "null" | + * "boolean" | "number" | "bigint" | "string" | "symbol" + * } PrimitiveStyle + */ + +/** + * @typedef { PrimitiveStyle | + * "copyRecord" | "copyArray" | "tagged" | + * "remotable" | + * "error" | "promise" + * } PassStyle + */ + +// TODO declare more precise types throughout this file, so the type system +// and IDE can be more helpful. + +/** + * @typedef {*} Passable + * + * A Passable value that may be marshalled. It is classified as one of + * PassStyle. A Passable must be hardened. + * + * A Passable has a pass-by-copy superstructure. This includes + * * the atomic pass-by-copy primitives ("undefined" | "null" | + * "boolean" | "number" | "bigint" | "string" | "symbol"), + * * the pass-by-copy containers + * ("copyRecord" | "copyArray" | "tagged") that + * contain other Passables, + * * and the special cases ("error" | "promise"). + * + * A Passable's pass-by-copy superstructure ends in + * PassableCap leafs ("remotable" | "promise"). Since a + * Passable is hardened, its structure and classification is stable --- its + * structure and classification cannot change even if some of the objects are + * proxies. + */ + +/** + * @callback PassStyleOf + * @param {Passable} passable + * @returns {PassStyle} + */ + +/** + * @typedef {Passable} PureData + * + * A Passable is PureData when its pass-by-copy superstructure whose + * nodes are pass-by-copy composites (CopyArray, CopyRecord, Tagged) leaves are + * primitives or empty composites. No remotables, promises, or errors. + * + * This check assures purity *given* that none of these pass-by-copy composites + * can be a Proxy. TODO SECURITY BUG we plan to enforce this, giving these + * pass-by-copy composites much of the same security properties as the + * proposed Records and Tuples (TODO need link). + * + * Given this (currently counter-factual) assumption, a PureData value cannot + * be used as a communications channel, + * and can therefore be safely shared with subgraphs that should not be able + * to communicate with each other. + */ + +/** + * @typedef {Passable} Remotable + * Might be an object explicitly declared to be `Remotable` using the + * `Far` or `Remotable` functions, or a remote presence of a Remotable. + */ + +/** + * @typedef {Promise | Remotable} PassableCap + * The authority-bearing leaves of a Passable's pass-by-copy superstructure. + */ + +/** + * @template T + * @typedef {T[]} CopyArray + */ + +/** + * @template T + * @typedef {Record} CopyRecord + */ + +/** + * @typedef {{ + * [Symbol.toStringTag]: string, + * payload: Passable + * }} CopyTagged + * + * The tag is the value of the `[String.toStringTag]` property. + * + * We used to also declare + * ```js + * [PASS_STYLE]: 'tagged', + * ``` + * within the CopyTagged type, before we extracted the pass-style package + * from the marshal package. Within pass-style, this additional property + * declaration seemed to be ignored by TS, but at least TS was still not + * complaining. However, TS checking the marshal package complains about + * this line because it does not know what `PASS_STYLE` is. I could not + * figure out how to fix this. + */ + +/** + * @typedef {string} InterfaceSpec + * This is an interface specification. + * For now, it is just a string, but will eventually be `PureData`. Either + * way, it must remain pure, so that it can be safely shared by subgraphs that + * are not supposed to be able to communicate. + */ + +/** + * @callback MarshalGetInterfaceOf + * Simple semantics, just tell what interface (or undefined) a remotable has. + * @param {*} maybeRemotable the value to check + * @returns {InterfaceSpec|undefined} the interface specification, or undefined + * if not a deemed to be a Remotable + */ + +/** + * @callback Checker + * Internal to a useful pattern for writing checking logic + * (a "checkFoo" function) that can be used to implement a predicate + * (an "isFoo" function) or a validator (an "assertFoo" function). + * + * * A predicate ideally only returns `true` or `false` and rarely throws. + * * A validator throws an informative diagnostic when the predicate + * would have returned `false`, and simply returns `undefined` normally + * when the predicate would have returned `true`. + * * The internal checking function that they share is parameterized by a + * `Checker` that determines how to proceed with a failure condition. + * Predicates pass in an identity function as checker. Validators + * pass in `assertChecker` which is a trivial wrapper around `assert`. + * + * See the various uses for good examples. + * @param {boolean} cond + * @param {Details=} details + * @returns {boolean} + */ diff --git a/packages/marshal/test/test-passStyleOf.js b/packages/pass-style/test/test-passStyleOf.js similarity index 99% rename from packages/marshal/test/test-passStyleOf.js rename to packages/pass-style/test/test-passStyleOf.js index 542dd24e74..00a3c5a5b5 100644 --- a/packages/marshal/test/test-passStyleOf.js +++ b/packages/pass-style/test/test-passStyleOf.js @@ -3,7 +3,7 @@ import { test } from './prepare-test-env-ava.js'; import { passStyleOf } from '../src/passStyleOf.js'; import { Far } from '../src/make-far.js'; import { makeTagged } from '../src/makeTagged.js'; -import { PASS_STYLE } from '../src/helpers/passStyle-helpers.js'; +import { PASS_STYLE } from '../src/passStyle-helpers.js'; const { getPrototypeOf } = Object; const { ownKeys } = Reflect;