From 25dcb30f832cf3acda44c7a38c55d5945386a81d Mon Sep 17 00:00:00 2001 From: "Mark S. Miller" Date: Wed, 24 Apr 2024 20:21:01 -0400 Subject: [PATCH] types(ses): move comments where typedoc can see them (#2205) closes: #XXXX refs: #XXXX ## Description Before this PR, there was a lot of redundancy between `ses/src/error/types.js` and `ses/types.d.ts` in typing the `ses/src/error/assert.js` module. This cased several problems: - `yarn docs` was picking up only the doc-comments in the `ses/types.d.ts` file, dropping all the useful doc-comments in the `ses/src/error/types.js` file. - redundant expression of the same meaning in different syntaxes is a horrible maintenance burden. They will get out of sync. (This happened to me at first in a recent PR when I added the `sanitize` option.) - The types as exported by the `ses/src/error/types.js` are ambient, which we're trying to reduce and eventually eliminate. The types as exported by `ses/types.d.ts` are not ambient, requiring them to be explicitly imported. Which is finally pleasant with `@import`! - By importing types with `@import` rather than `@typedef {import('...). ...} ...`, the documentation attached to the types is visible in the IDE (at least vscode) and more usage locations. This PR should be a pure refactoring from a runtime semantics POV. No behavior of any code should be any different. All the changes are only to static information: types and doc-comments. ### Security Considerations As mentioned above, redundant expression of the same meaning in different syntaxes ... will get out of sync. This makes code more confusing to review reliably, so this PR helps security. ### Scaling Considerations none ### Documentation Considerations Most of the point! After this PR, `yarn docs` produces much better documentation for the types related to the assert.js module. It is thus also an attempt to get one's feet wet at making this kind of improvement. However, even after this PR, the output of `yarn docs` still leaves plenty of room for further improvement ;) ### Testing Considerations Probably none. Conceivably more type tests could be relevant, but if so it is not apparent to me. ### Compatibility Considerations Because this PR (allegedly) changes no runtime behavior, it cannot cause any incompat runtime behavior. However, client code is also a client of the exported types. Changing types from ambient to non-ambient risks breaking whether old clients continue to pass static type checking. Which brings us back to the oft-repeated question for reviewers: ***Do I need to mark this PR with a `!`?*** ### Upgrade Considerations none. - [ ] Includes `*BREAKING*:` in the commit message with migration instructions for any breaking change. - [ ] Updates `NEWS.md` for user-facing changes. --- packages/ses/src/error/assert.js | 50 +-- packages/ses/src/error/fatal-assert.js | 2 + packages/ses/src/error/stringify-utils.js | 2 + packages/ses/src/error/types.js | 381 +--------------------- packages/ses/src/permits.js | 2 + packages/ses/types.d.ts | 280 +++++++++++++++- 6 files changed, 298 insertions(+), 419 deletions(-) diff --git a/packages/ses/src/error/assert.js b/packages/ses/src/error/assert.js index cb918a57c5..b3cb5d3735 100644 --- a/packages/ses/src/error/assert.js +++ b/packages/ses/src/error/assert.js @@ -46,6 +46,17 @@ import './types.js'; import './internal-types.js'; import { makeNoteLogArgsArrayKit } from './note-log-args.js'; +/** + * @import {BaseAssert, + * Assert, + * AssertionFunctions, + * AssertionUtilities, + * StringablePayload, + * DetailsToken, + * MakeAssert, + * } from '../../types.js' + */ + // For our internal debugging purposes, uncomment // const internalDebugConsole = console; @@ -54,7 +65,7 @@ import { makeNoteLogArgsArrayKit } from './note-log-args.js'; /** @type {WeakMap} */ const declassifiers = new WeakMap(); -/** @type {AssertQuote} */ +/** @type {AssertionUtilities['quote']} */ const quote = (payload, spaces = undefined) => { const result = freeze({ toString: freeze(() => bestEffortStringify(payload, spaces)), @@ -67,19 +78,7 @@ freeze(quote); const canBeBare = freeze(/^[\w:-]( ?[\w:-])*$/); /** - * Embed a string directly into error details without wrapping punctuation. - * To avoid injection attacks that exploit quoting confusion, this must NEVER - * be used with data that is possibly attacker-controlled. - * As a further safeguard, we fall back to quoting any input that is not a - * string of sufficiently word-like parts separated by isolated spaces (rather - * than throwing an exception, which could hide the original problem for which - * explanatory details are being constructed---i.e., ``` assert.details`...` ``` - * should never be the source of a new exception, nor should an attempt to - * render its output, although we _could_ instead decide to handle the latter - * by inline replacement similar to that of `bestEffortStringify` for producing - * rendered messages like `(an object) was tagged "[Unsafe bare string]"`). - * - * @type {AssertQuote} + * @type {AssertionUtilities['bare']} */ const bare = (payload, spaces = undefined) => { if (typeof payload !== 'string' || !regexpTest(canBeBare, payload)) { @@ -165,7 +164,7 @@ freeze(DetailsTokenProto.toString); * of them should be uses where the template literal has no redacted * substitution values. In those cases, the two are equivalent. * - * @type {DetailsTag} + * @type {AssertionUtilities['details']} */ const redactedDetails = (template, ...args) => { // Keep in mind that the vast majority of calls to `details` creates @@ -174,7 +173,7 @@ const redactedDetails = (template, ...args) => { // all the work to happen only if needed, for example, if an assertion fails. const detailsToken = freeze({ __proto__: DetailsTokenProto }); weakmapSet(hiddenDetailsMap, detailsToken, { template, args }); - return detailsToken; + return /** @type {DetailsToken} */ (/** @type {unknown} */ (detailsToken)); }; freeze(redactedDetails); @@ -189,7 +188,7 @@ freeze(redactedDetails); * of safety. `unredactedDetails` also sacrifices the speed of `details`, * which is usually fine in debugging and testing. * - * @type {DetailsTag} + * @type {AssertionUtilities['details']} */ const unredactedDetails = (template, ...args) => { args = arrayMap(args, arg => @@ -286,7 +285,7 @@ const tagError = (err, optErrorName = err.name) => { * * @param {Error} error */ -const sanitizeError = error => { +export const sanitizeError = error => { const descs = getOwnPropertyDescriptors(error); const { name: _nameDesc, @@ -322,7 +321,7 @@ const sanitizeError = error => { }; /** - * @type {AssertMakeError} + * @type {AssertionUtilities['error']} */ const makeError = ( optDetails = redactedDetails`Assert failed`, @@ -397,7 +396,7 @@ const { addLogArgs, takeLogArgsArray } = makeNoteLogArgsArrayKit(); */ const hiddenNoteCallbackArrays = new WeakMap(); -/** @type {AssertNote} */ +/** @type {AssertionUtilities['note']} */ const note = (error, detailsNote) => { if (typeof detailsNote === 'string') { // If it is a string, use it as the literal part of the template so @@ -478,7 +477,7 @@ const makeAssert = (optRaise = undefined, unredacted = false) => { const details = unredacted ? unredactedDetails : redactedDetails; const assertFailedDetails = details`Check failed`; - /** @type {AssertFail} */ + /** @type {AssertionFunctions['fail']} */ const fail = ( optDetails = assertFailedDetails, errConstructor = undefined, @@ -486,13 +485,14 @@ const makeAssert = (optRaise = undefined, unredacted = false) => { ) => { const reason = makeError(optDetails, errConstructor, options); if (optRaise !== undefined) { + // @ts-ignore returns `never` doesn't mean it isn't callable optRaise(reason); } throw reason; }; freeze(fail); - /** @type {FailTag} */ + /** @type {AssertionUtilities['Fail']} */ const Fail = (template, ...args) => fail(details(template, ...args)); // Don't freeze or export `baseAssert` until we add methods. @@ -508,7 +508,7 @@ const makeAssert = (optRaise = undefined, unredacted = false) => { flag || fail(optDetails, errConstructor, options); } - /** @type {AssertEqual} */ + /** @type {AssertionFunctions['equal']} */ const equal = ( actual, expected, @@ -525,7 +525,7 @@ const makeAssert = (optRaise = undefined, unredacted = false) => { }; freeze(equal); - /** @type {AssertTypeof} */ + /** @type {AssertionFunctions['typeof']} */ const assertTypeof = (specimen, typename, optDetails) => { // This will safely fall through if typename is not a string, // which is what we want. @@ -544,7 +544,7 @@ const makeAssert = (optRaise = undefined, unredacted = false) => { }; freeze(assertTypeof); - /** @type {AssertString} */ + /** @type {AssertionFunctions['string']} */ const assertString = (specimen, optDetails = undefined) => assertTypeof(specimen, 'string', optDetails); diff --git a/packages/ses/src/error/fatal-assert.js b/packages/ses/src/error/fatal-assert.js index 29d1d0dc18..5b76544e3a 100644 --- a/packages/ses/src/error/fatal-assert.js +++ b/packages/ses/src/error/fatal-assert.js @@ -5,6 +5,8 @@ import { freeze } from '../commons.js'; import { makeAssert } from './assert.js'; import './types.js'; +/** @import {Assert} from '../../types.js' */ + let abandon; // Sniff for host-provided functions for terminating the enclosing UOPT (see // below). Currently it only checks for the `process.abort` or `process.exit` diff --git a/packages/ses/src/error/stringify-utils.js b/packages/ses/src/error/stringify-utils.js index c63b3a8410..9b0f1ea230 100644 --- a/packages/ses/src/error/stringify-utils.js +++ b/packages/ses/src/error/stringify-utils.js @@ -21,6 +21,8 @@ import { toStringTagSymbol, } from '../commons.js'; +/** @import {StringablePayload} from '../../types.js' */ + /** * Joins English terms with commas and an optional conjunction. * diff --git a/packages/ses/src/error/types.js b/packages/ses/src/error/types.js index 0631c08a1e..80a1c8fd23 100644 --- a/packages/ses/src/error/types.js +++ b/packages/ses/src/error/types.js @@ -1,385 +1,6 @@ // @ts-check -/** - * TypeScript does not treat `AggregateErrorConstructor` as a subtype of - * `ErrorConstructor`, which makes sense because their constructors - * have incompatible signatures. However, we want to parameterize some - * operations by any error constructor, including possible `AggregateError`. - * So we introduce `GenericErrorConstructor` as a common supertype. Any call - * to it to make an instance must therefore first case split on whether the - * constructor is an AggregateErrorConstructor or a normal ErrorConstructor. - * - * @typedef {ErrorConstructor | AggregateErrorConstructor} GenericErrorConstructor - */ - -/** - * @callback BaseAssert - * The `assert` function itself. - * - * @param {any} flag The truthy/falsy value - * @param {Details} [optDetails] The details to throw - * @param {GenericErrorConstructor} [errConstructor] - * An optional alternate error constructor to use. - * @param {AssertMakeErrorOptions} [options] - * @returns {asserts flag} - */ - -/** - * @typedef {object} AssertMakeErrorOptions - * @property {string} [errorName] - * Does not affect the error.name property. That remains determined by - * the constructor. Rather, the `errorName` determines how this error is - * identified in the causal console log's output. - * @property {Error} [cause] - * Discloses the error that caused this one, typically from a lower - * layer of abstraction. This is represented by a public `cause` data property - * on the error, not a hidden annotation. - * @property {Error[]} [errors] - * Normally only used when the ErrorConstuctor is `AggregateError`, to - * represent the set of prior errors aggregated together in this error, - * typically by `Promise.any`. But `makeError` allows it on any error. - * This is represented by a public `errors` data property on the error, - * not a hidden annotation. - * @property {boolean} [sanitize] - * Defaults to true. If true, `makeError` will apply `sanitizeError` - * to the error before returning it. See the comments on `sanitizeError`. - * (TODO what is the proper jsdoc manner to link to another function's - * doc-comment?) - */ - -/** - * @callback AssertMakeError - * - * The `assert.error` method, recording details for the console. - * - * The optional `optDetails` can be a string. - * @param {Details} [optDetails] The details of what was asserted - * @param {GenericErrorConstructor} [errConstructor] - * An optional alternate error constructor to use. - * @param {AssertMakeErrorOptions} [options] - * @returns {Error} - */ - -/** - * @callback AssertFail - * - * The `assert.fail` method. - * - * Fail an assertion, recording full details to the console and - * raising an exception with a message in which `details` substitution values - * have been redacted. - * - * The optional `optDetails` can be a string for backwards compatibility - * with the nodejs assertion library. - * @param {Details} [optDetails] The details of what was asserted - * @param {GenericErrorConstructor} [errConstructor] - * An optional alternate error constructor to use. - * @param {AssertMakeErrorOptions} [options] - * @returns {never} - */ - -/** - * @callback AssertEqual - * The `assert.equal` method - * - * Assert that two values must be `Object.is`. - * @param {any} actual The value we received - * @param {any} expected What we wanted - * @param {Details} [optDetails] The details to throw - * @param {GenericErrorConstructor} [errConstructor] - * An optional alternate error constructor to use. - * @param {AssertMakeErrorOptions} [options] - * @returns {void} - */ - -// Type all the overloads of the assertTypeof function. -// There may eventually be a better way to do this, but -// thems the breaks with Typescript 4.0. -/** - * @callback AssertTypeofBigint - * @param {any} specimen - * @param {'bigint'} typename - * @param {Details} [optDetails] - * @returns {asserts specimen is bigint} - */ - -/** - * @callback AssertTypeofBoolean - * @param {any} specimen - * @param {'boolean'} typename - * @param {Details} [optDetails] - * @returns {asserts specimen is boolean} - */ - -/** - * @callback AssertTypeofFunction - * @param {any} specimen - * @param {'function'} typename - * @param {Details} [optDetails] - * @returns {asserts specimen is Function} - */ - -/** - * @callback AssertTypeofNumber - * @param {any} specimen - * @param {'number'} typename - * @param {Details} [optDetails] - * @returns {asserts specimen is number} - */ - -/** - * @callback AssertTypeofObject - * @param {any} specimen - * @param {'object'} typename - * @param {Details} [optDetails] - * @returns {asserts specimen is Record | null} - */ - -/** - * @callback AssertTypeofString - * @param {any} specimen - * @param {'string'} typename - * @param {Details} [optDetails] - * @returns {asserts specimen is string} - */ - -/** - * @callback AssertTypeofSymbol - * @param {any} specimen - * @param {'symbol'} typename - * @param {Details} [optDetails] - * @returns {asserts specimen is symbol} - */ - -/** - * @callback AssertTypeofUndefined - * @param {any} specimen - * @param {'undefined'} typename - * @param {Details} [optDetails] - * @returns {asserts specimen is undefined} - */ - -/** - * The `assert.typeof` method - * - * @typedef {AssertTypeofBigint & AssertTypeofBoolean & AssertTypeofFunction & AssertTypeofNumber & AssertTypeofObject & AssertTypeofString & AssertTypeofSymbol & AssertTypeofUndefined} AssertTypeof - */ - -/** - * @callback AssertString - * The `assert.string` method. - * - * `assert.string(v)` is equivalent to `assert.typeof(v, 'string')`. We - * special case this one because it is the most frequently used. - * - * Assert an expected typeof result. - * @param {any} specimen The value to get the typeof - * @param {Details} [optDetails] The details to throw - * @returns {asserts specimen is string} - */ - -/** - * @callback AssertNote - * The `assert.note` method. - * - * Annotate an error with details, potentially to be used by an - * augmented console such as the causal console of `console.js`, to - * provide extra information associated with logged errors. - * - * @param {Error} error - * @param {Details} detailsNote - * @returns {void} - */ - -// ///////////////////////////////////////////////////////////////////////////// - -/** - * @typedef {{}} DetailsToken - * A call to the `details` template literal makes and returns a fresh details - * token, which is a frozen empty object associated with the arguments of that - * `details` template literal expression. - */ - -/** - * @typedef {string | DetailsToken} Details - * Either a plain string, or made by the `details` template literal tag. - */ - -/** - * @typedef {object} StringablePayload - * Holds the payload passed to quote so that its printed form is visible. - * @property {() => string} toString How to print the payload - */ - -/** - * To "declassify" and quote a substitution value used in a - * ``` details`...` ``` template literal, enclose that substitution expression - * in a call to `quote`. This makes the value appear quoted - * (as if with `JSON.stringify`) in the message of the thrown error. The - * payload itself is still passed unquoted to the console as it would be - * without `quote`. - * - * For example, the following will reveal the expected sky color, but not the - * actual incorrect sky color, in the thrown error's message: - * ```js - * sky.color === expectedColor || Fail`${sky.color} should be ${quote(expectedColor)}`; - * ``` - * - * // TODO Update SES-shim to new convention, where `details` is - * // renamed to `X` rather than `d`. - * The normal convention is to locally rename `details` to `d` and `quote` to `q` - * like `const { details: d, quote: q } = assert;`, so the above example would then be - * ```js - * sky.color === expectedColor || Fail`${sky.color} should be ${q(expectedColor)}`; - * ``` - * - * @callback AssertQuote - * @param {any} payload What to declassify - * @param {(string|number)} [spaces] - * @returns {StringablePayload} The declassified payload - */ - -/** - * @callback Raise - * - * To make an `assert` which terminates some larger unit of computation - * like a transaction, vat, or process, call `makeAssert` with a `Raise` - * callback, where that callback actually performs that larger termination. - * If possible, the callback should also report its `reason` parameter as - * the alleged reason for the termination. - * - * @param {Error} reason - */ - -/** - * @callback MakeAssert - * - * Makes and returns an `assert` function object that shares the bookkeeping - * state defined by this module with other `assert` function objects made by - * `makeAssert`. This state is per-module-instance and is exposed by the - * `loggedErrorHandler` above. We refer to `assert` as a "function object" - * because it can be called directly as a function, but also has methods that - * can be called. - * - * If `optRaise` is provided, the returned `assert` function object will call - * `optRaise(reason)` before throwing the error. This enables `optRaise` to - * engage in even more violent termination behavior, like terminating the vat, - * that prevents execution from reaching the following throw. However, if - * `optRaise` returns normally, which would be unusual, the throw following - * `optRaise(reason)` would still happen. - * - * @param {Raise} [optRaise] - * @param {boolean} [unredacted] - * @returns {Assert} - */ - -/** - * @typedef {(template: TemplateStringsArray | string[], ...args: any) => DetailsToken} DetailsTag - * - * Use the `details` function as a template literal tag to create - * informative error messages. The assertion functions take such messages - * as optional arguments: - * ```js - * assert(sky.isBlue(), details`${sky.color} should be "blue"`); - * ``` - * // TODO Update SES-shim to new convention, where `details` is - * // renamed to `X` rather than `d`. - * or following the normal convention to locally rename `details` to `d` - * and `quote` to `q` like `const { details: d, quote: q } = assert;`: - * ```js - * assert(sky.isBlue(), d`${sky.color} should be "blue"`); - * ``` - * However, note that in most cases it is preferable to instead use the `Fail` - * template literal tag (which has the same input signature as `details` - * but automatically creates and throws an error): - * ```js - * sky.isBlue() || Fail`${sky.color} should be "blue"`; - * ``` - * - * The details template tag returns a `DetailsToken` object that can print - * itself with the formatted message in two ways. - * It will report full details to the console, but - * mask embedded substitution values with their typeof information in the thrown error - * to prevent revealing secrets up the exceptional path. In the example - * above, the thrown error may reveal only that `sky.color` is a string, - * whereas the same diagnostic printed to the console reveals that the - * sky was green. This masking can be disabled for an individual substitution value - * using `quote`. - * - * The `raw` property of an input template array is ignored, so a simple - * array of strings may be provided directly. - */ - -/** - * @typedef {(template: TemplateStringsArray | string[], ...args: any) => never} FailTag - * - * Use the `Fail` function as a template literal tag to efficiently - * create and throw a `details`-style error only when a condition is not satisfied. - * ```js - * condition || Fail`...complaint...`; - * ``` - * This avoids the overhead of creating usually-unnecessary errors like - * ```js - * assert(condition, details`...complaint...`); - * ``` - * while improving readability over alternatives like - * ```js - * condition || assert.fail(details`...complaint...`); - * ``` - * - * However, due to current weakness in TypeScript, static reasoning - * is less powerful with the `||` patterns than with an `assert` call. - * Until/unless https://github.com/microsoft/TypeScript/issues/51426 is fixed, - * for `||`-style assertions where this loss of static reasoning is a problem, - * instead express the assertion as - * ```js - * if (!condition) { - * Fail`...complaint...`; - * } - * ``` - * or, if needed, - * ```js - * if (!condition) { - * // `throw` is noop since `Fail` throws, but it improves static analysis - * throw Fail`...complaint...`; - * } - * ``` - */ - -/** - * assert that expr is truthy, with an optional details to describe - * the assertion. It is a tagged template literal like - * ```js - * assert(expr, details`....`);` - * ``` - * - * The literal portions of the template are assumed non-sensitive, as - * are the `typeof` types of the substitution values. These are - * assembled into the thrown error message. The actual contents of the - * substitution values are assumed sensitive, to be revealed to - * the console only. We assume only the virtual platform's owner can read - * what is written to the console, where the owner is in a privileged - * position over computation running on that platform. - * - * The optional `optDetails` can be a string for backwards compatibility - * with the nodejs assertion library. - * - * @typedef { BaseAssert & { - * typeof: AssertTypeof, - * error: AssertMakeError, - * fail: AssertFail, - * equal: AssertEqual, - * string: AssertString, - * note: AssertNote, - * details: DetailsTag, - * Fail: FailTag, - * quote: AssertQuote, - * bare: AssertQuote, - * makeAssert: MakeAssert, - * } } Assert - */ - -// ///////////////////////////////////////////////////////////////////////////// +/** @import {GenericErrorConstructor, AssertMakeErrorOptions, DetailsToken, StringablePayload} from '../../types.js' */ /** * @typedef {object} VirtualConsole diff --git a/packages/ses/src/permits.js b/packages/ses/src/permits.js index 611671dd6e..d364501067 100644 --- a/packages/ses/src/permits.js +++ b/packages/ses/src/permits.js @@ -3,6 +3,8 @@ import { arrayPush } from './commons.js'; +/** @import {GenericErrorConstructor} from '../types.js' */ + /** * @file Exports {@code whitelist}, a recursively defined * JSON record enumerating all intrinsics and their properties diff --git a/packages/ses/types.d.ts b/packages/ses/types.d.ts index ff3afd8ab4..d7ccf4d955 100644 --- a/packages/ses/types.d.ts +++ b/packages/ses/types.d.ts @@ -113,17 +113,49 @@ export interface EvaluateOptions { __rejectSomeDirectEvalExpressions__?: boolean; } -// The DetailsToken is an empty object literal. +/** + * A call to the `details` template literal makes and returns a fresh details + * token, which is a frozen empty object associated with the arguments of that + * `details` template literal expression. + */ export type DetailsToken = Record; +/** Either a plain string, or made by the `details` template literal tag. */ export type Details = string | DetailsToken; export interface AssertMakeErrorOptions { + /** + * Does not affect the error.name property. That remains determined by + * the constructor. Rather, the `errorName` determines how this error is + * identified in the causal console log's output. + */ errorName?: string; + + /** + * Discloses the error that caused this one, typically from a lower + * layer of abstraction. This is represented by a public `cause` data property + * on the error, not a hidden annotation. + */ cause?: Error; + + /** + * Normally only used when the ErrorConstuctor is `AggregateError`, to + * represent the set of prior errors aggregated together in this error, + * typically by `Promise.any`. But `makeError` allows it on any error. + * This is represented by a public `errors` data property on the error, + * not a hidden annotation. + */ errors?: Error[]; + + /** + * Defaults to true. If true, `makeError` will apply `sanitizeError` + * to the error before returning it. See the comments on + * {@link sanitizeError}. + */ sanitize?: boolean; } +// TODO inline overloading + type AssertTypeofBigint = ( specimen: any, typeName: 'bigint', @@ -174,62 +206,282 @@ export type AssertTypeof = AssertTypeofBigint & AssertTypeofSymbol & AssertTypeofUndefined; -interface ToStringable { +interface StringablePayload { toString(): string; } +/** + * TypeScript does not treat `AggregateErrorConstructor` as a subtype of + * `ErrorConstructor`, which makes sense because their constructors + * have incompatible signatures. However, we want to parameterize some + * operations by any error constructor, including possible `AggregateError`. + * So we introduce `GenericErrorConstructor` as a common supertype. Any call + * to it to make an instance must therefore first case split on whether the + * constructor is an AggregateErrorConstructor or a normal ErrorConstructor. + */ export type GenericErrorConstructor = | ErrorConstructor | AggregateErrorConstructor; +/** + * To make an `assert` which terminates some larger unit of computation + * like a transaction, vat, or process, call `makeAssert` with a `Raise` + * callback, where that callback actually performs that larger termination. + * If possible, the callback should also report its `reason` parameter as + * the alleged reason for the termination. + */ export type Raise = (reason: Error) => void; + +/** + * Makes and returns an `assert` function object that shares the bookkeeping + * state defined by this module with other `assert` function objects made by + * `makeAssert`. This state is per-module-instance and is exposed by the + * `loggedErrorHandler` above. We refer to `assert` as a "function object" + * because it can be called directly as a function, but also has methods that + * can be called. + * + * If `optRaise` is provided, the returned `assert` function object will call + * `optRaise(reason)` before throwing the error. This enables `optRaise` to + * engage in even more violent termination behavior, like terminating the vat, + * that prevents execution from reaching the following throw. However, if + * `optRaise` returns normally, which would be unusual, the throw following + * `optRaise(reason)` would still happen. + */ // Behold: recursion. // eslint-disable-next-line no-use-before-define export type MakeAssert = (raise?: Raise, unredacted?: boolean) => Assert; -export interface AssertionFunctions { - ( - value: any, - details?: Details, - errConstructor?: GenericErrorConstructor, - options?: AssertMakeErrorOptions, - ): asserts value; +export type BaseAssert = ( + /** The truthy/falsy value we're testing */ + flag: any, + /** The details of what was asserted */ + details?: Details, + /** An optional alternate error constructor to use */ + errConstructor?: GenericErrorConstructor, + options?: AssertMakeErrorOptions, +) => asserts flag; + +export interface AssertionFunctions extends BaseAssert { typeof: AssertTypeof; + + /** + * The `assert.equal` method + * + * Assert that two values must be `Object.is`. + */ equal( - left: any, - right: any, + /** What we received */ + actual: any, + /** What we wanted */ + expected: any, + /** The details of what was asserted */ details?: Details, + /** An optional alternate error constructor to use */ errConstructor?: GenericErrorConstructor, options?: AssertMakeErrorOptions, ): void; - string(specimen: any, details?: Details): asserts specimen is string; + + /** + * The `assert.string` method. + * + * `assert.string(v)` is equivalent to `assert.typeof(v, 'string')`. We + * special case this one because it is the most frequently used. + * + * Assert an expected typeof result. + */ + string( + specimen: any, + /** The details of what was asserted */ + details?: Details, + ): asserts specimen is string; + + /** + * The `assert.fail` method. + * + * Fail an assertion, recording full details to the console and + * raising an exception with a message in which `details` substitution values + * have been redacted. + * + * The optional `optDetails` can be a string for backwards compatibility + * with the nodejs assertion library. + */ fail( + /** The details of what was asserted */ details?: Details, + /** An optional alternate error constructor to use */ errConstructor?: GenericErrorConstructor, options?: AssertMakeErrorOptions, ): never; } export interface AssertionUtilities { + /** + * Aka the `makeError` function as imported from `@endo/errors` + * + * Recording unredacted details for the console. + */ error( + /** The details of what was asserted */ details?: Details, + /** An optional alternate error constructor to use */ errConstructor?: GenericErrorConstructor, options?: AssertMakeErrorOptions, ): Error; + + /** + * Aka the `annotateError` function as imported from `@endo/errors` + * + * Annotate an error with details, potentially to be used by an + * augmented console such as the causal console of `console.js`, to + * provide extra information associated with logged errors. + */ note(error: Error, details: Details): void; + + /** + * Use the `details` function as a template literal tag to create + * informative error messages. The assertion functions take such messages + * as optional arguments: + * ```js + * assert(sky.isBlue(), details`${sky.color} should be "blue"`); + * ``` + * // TODO Update SES-shim to new convention, where `details` is + * // renamed to `X` rather than `d`. + * or following the normal convention to locally rename `details` to `d` + * and `quote` to `q` like `const { details: d, quote: q } = assert;`: + * ```js + * assert(sky.isBlue(), d`${sky.color} should be "blue"`); + * ``` + * However, note that in most cases it is preferable to instead use the `Fail` + * template literal tag (which has the same input signature as `details` + * but automatically creates and throws an error): + * ```js + * sky.isBlue() || Fail`${sky.color} should be "blue"`; + * ``` + * + * The details template tag returns a `DetailsToken` object that can print + * itself with the formatted message in two ways. + * It will report full details to the console, but + * mask embedded substitution values with their typeof information in the thrown error + * to prevent revealing secrets up the exceptional path. In the example + * above, the thrown error may reveal only that `sky.color` is a string, + * whereas the same diagnostic printed to the console reveals that the + * sky was green. This masking can be disabled for an individual substitution value + * using `quote`. + * + * The `raw` property of an input template array is ignored, so a simple + * array of strings may be provided directly. + */ details( template: TemplateStringsArray | string[], ...args: any ): DetailsToken; + + /** + * Use the `Fail` function as a template literal tag to efficiently + * create and throw a `details`-style error only when a condition is not satisfied. + * ```js + * condition || Fail`...complaint...`; + * ``` + * This avoids the overhead of creating usually-unnecessary errors like + * ```js + * assert(condition, details`...complaint...`); + * ``` + * while improving readability over alternatives like + * ```js + * condition || assert.fail(details`...complaint...`); + * ``` + * + * However, due to current weakness in TypeScript, static reasoning + * is less powerful with the `||` patterns than with an `assert` call. + * Until/unless https://github.com/microsoft/TypeScript/issues/51426 is fixed, + * for `||`-style assertions where this loss of static reasoning is a problem, + * instead express the assertion as + * ```js + * if (!condition) { + * Fail`...complaint...`; + * } + * ``` + * or, if needed, + * ```js + * if (!condition) { + * // `throw` is noop since `Fail` throws, but it improves static analysis + * throw Fail`...complaint...`; + * } + * ``` + */ Fail(template: TemplateStringsArray | string[], ...args: any): never; - quote(payload: any, spaces?: string | number): ToStringable; - bare(payload: any, spaces?: string | number): ToStringable; + + /** + * To "declassify" and quote a substitution value used in a + * ``` details`...` ``` template literal, enclose that substitution expression + * in a call to `quote`. This makes the value appear quoted + * (as if with `JSON.stringify`) in the message of the thrown error. The + * payload itself is still passed unquoted to the console as it would be + * without `quote`. + * + * For example, the following will reveal the expected sky color, but not the + * actual incorrect sky color, in the thrown error's message: + * ```js + * sky.color === expectedColor || Fail`${sky.color} should be ${quote(expectedColor)}`; + * ``` + * + * // TODO Update SES-shim to new convention, where `details` is + * // renamed to `X` rather than `d`. + * The normal convention is to locally rename `details` to `d` and `quote` to `q` + * like `const { details: d, quote: q } = assert;`, so the above example would then be + * ```js + * sky.color === expectedColor || Fail`${sky.color} should be ${q(expectedColor)}`; + * ``` + */ + quote( + /** What to declassify */ + payload: any, + spaces?: string | number, + ): /** The declassified and quoted payload */ StringablePayload; + + /** + * Embed a string directly into error details without wrapping punctuation. + * To avoid injection attacks that exploit quoting confusion, this must NEVER + * be used with data that is possibly attacker-controlled. + * As a further safeguard, we fall back to quoting any input that is not a + * string of sufficiently word-like parts separated by isolated spaces (rather + * than throwing an exception, which could hide the original problem for which + * explanatory details are being constructed---i.e., ``` assert.details`...` ``` + * should never be the source of a new exception, nor should an attempt to + * render its output, although we _could_ instead decide to handle the latter + * by inline replacement similar to that of `bestEffortStringify` for producing + * rendered messages like `(an object) was tagged "[Unsafe bare string]"`). + */ + bare( + /** What to declassify */ + payload: any, + spaces?: string | number, + ): /** The declassified payload without quotes (beware confusion hazard) */ + StringablePayload; } export interface DeprecatedAssertionUtilities { makeAssert: MakeAssert; } +/** + * assert that expr is truthy, with an optional details to describe + * the assertion. It is a tagged template literal like + * ```js + * assert(expr, details`....`);` + * ``` + * + * The literal portions of the template are assumed non-sensitive, as + * are the `typeof` types of the substitution values. These are + * assembled into the thrown error message. The actual contents of the + * substitution values are assumed sensitive, to be revealed to + * the console only. We assume only the virtual platform's owner can read + * what is written to the console, where the owner is in a privileged + * position over computation running on that platform. + * + * The optional `optDetails` can be a string for backwards compatibility + * with the nodejs assertion library. + */ export type Assert = AssertionFunctions & AssertionUtilities & DeprecatedAssertionUtilities;