ses
Version:
Hardened JavaScript for Fearless Cooperation
581 lines (523 loc) • 20 kB
TypeScript
/**
* @module Types of the SES environment
*/
/* eslint-disable no-restricted-globals, vars-on-top, no-var */
// It's academically tempting to define a hardened type, but TypeScript doesn't
// strike a good balance in distinguishing "readonly" in the sense that you
// promise not to change vs "readonly" in the sense that you depend on a thing
// not changing.
// type Hardened<T> =
// T extends number | bigint | string | null | undefined | Function ? T :
// { readonly [P in keyof T]: Hardened<T[P]> };
// So Harden just passes the type through without modification.
// This will occasionally conflict with the type of Object.freeze.
// In those cases, we recommend casting the result of Object.freeze to the
// original thawn type, as if the signature of freeze were identical to this
// version of harden.
export type Harden = <T>(value: T) => T; // not Hardened<T>;
export interface RepairOptions {
regExpTaming?: 'safe' | 'unsafe';
localeTaming?: 'safe' | 'unsafe';
consoleTaming?: 'safe' | 'unsafe';
errorTrapping?: 'platform' | 'exit' | 'abort' | 'report' | 'none';
reporting?: 'platform' | 'console' | 'none';
unhandledRejectionTrapping?: 'report' | 'none';
errorTaming?: 'safe' | 'unsafe' | 'unsafe-debug';
/**
* @deprecated Deprecated and does nothing. In the future specifying it will be an error.
*/
dateTaming?: 'safe' | 'unsafe';
/**
* @deprecated Deprecated and does nothing. In the future specifying it will be an error.
*/
mathTaming?: 'safe' | 'unsafe';
evalTaming?: 'safeEval' | 'unsafeEval' | 'noEval';
stackFiltering?: 'concise' | 'verbose';
overrideTaming?: 'moderate' | 'min' | 'severe';
overrideDebug?: Array<string>;
domainTaming?: 'safe' | 'unsafe';
/**
* safe (default): do nothing.
*
* unsafe-ignore: make %IteratorPrototype%[@@iterator] to a funky accessor which ignores all assignments.
*/
legacyRegeneratorRuntimeTaming?: 'safe' | 'unsafe-ignore';
__hardenTaming__?: 'safe' | 'unsafe';
}
// Deprecated in favor of the more specific RepairOptions
export type LockdownOptions = RepairOptions;
export type RepairIntrinsics = (options?: LockdownOptions) => void;
export type HardenIntrinsics = () => void;
export type Lockdown = (options?: LockdownOptions) => void;
export type ModuleExportsNamespace = Record<string, any>;
export type __LiveExportMap__ = Record<string, [string, boolean]>;
export type __FixedExportMap__ = Record<string, [string]>;
export type __ReexportMap__ = Record<string, Array<[string, string]>>;
export interface PrecompiledModuleSource {
imports: Array<string>;
exports: Array<string>;
reexports: Array<string>;
__syncModuleProgram__: string;
__liveExportMap__: __LiveExportMap__;
__fixedExportMap__: __FixedExportMap__;
__reexportMap__: __ReexportMap__;
}
export interface VirtualModuleSource {
imports: Array<string>;
exports: Array<string>;
/**
* Note that this value does _not_ contain any numeric or symbol property keys, which can theoretically be members of `exports` in a CommonJS module.
*/
execute(
exportsTarget: Record<string, any>,
compartment: Compartment,
resolvedImports: Record<string, string>,
): void;
}
export type ModuleSource = PrecompiledModuleSource | VirtualModuleSource;
export interface SourceModuleDescriptor {
source: string | ModuleSource;
specifier?: string;
importMeta?: any;
compartment?: Compartment; // defaults to parent
}
export interface NamespaceModuleDescriptor {
namespace: string | ModuleExportsNamespace;
compartment?: Compartment;
}
// Deprecated in favor of SourceModuleDescriptor,
// but beware the change in default compartment.
export interface RecordModuleDescriptor {
specifier: string;
record?: ModuleSource;
importMeta?: any;
compartment?: Compartment; // defaults to self
}
export type ModuleDescriptor =
| SourceModuleDescriptor
| NamespaceModuleDescriptor
// To be deprecated:
| RecordModuleDescriptor
| ModuleExportsNamespace
| VirtualModuleSource
| PrecompiledModuleSource
| string;
// Deprecated type aliases:
export type PrecompiledStaticModuleInterface = PrecompiledModuleSource;
export type ThirdPartyStaticModuleInterface = VirtualModuleSource;
export type RedirectStaticModuleInterface = RecordModuleDescriptor;
export type FinalStaticModuleType = ModuleSource;
export type StaticModuleType = RedirectStaticModuleInterface | ModuleSource;
export type Transform = (source: string) => string;
export type ResolveHook = (
importSpecifier: string,
referrerSpecifier: string,
) => string;
export type ModuleMap = Record<string, string | ModuleDescriptor>;
export type ModuleMapHook = (
moduleSpecifier: string,
) => ModuleDescriptor | undefined;
export type ImportHook = (moduleSpecifier: string) => Promise<ModuleDescriptor>;
export type ImportNowHook = (moduleSpecifier: string) => ModuleDescriptor;
export type ImportMetaHook = (
moduleSpecifier: string,
importMeta: Object,
) => void;
export interface CompartmentOptions {
name?: string;
transforms?: Array<Transform>;
moduleMapHook?: ModuleMapHook;
importHook?: ImportHook;
importNowHook?: ImportNowHook;
importMetaHook?: ImportMetaHook;
resolveHook?: ResolveHook;
globals?: Map<string, any>;
modules?: Map<string, ModuleDescriptor>;
__shimTransforms__?: Array<Transform>;
__noNamespaceBox__?: boolean;
}
export interface EvaluateOptions {
transforms?: Array<Transform>;
sloppyGlobalsMode?: boolean;
__moduleShimLexicals__?: Record<string, any>;
__evadeHtmlCommentTest__?: boolean;
__rejectSomeDirectEvalExpressions__?: boolean;
}
/**
* 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<any, never>;
/** 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',
details?: Details,
) => asserts specimen is bigint;
type AssertTypeofBoolean = (
specimen: any,
typeName: 'boolean',
details?: Details,
) => asserts specimen is boolean;
type AssertTypeofFunction = (
specimen: any,
typeName: 'function',
details?: Details,
) => asserts specimen is Function;
type AssertTypeofNumber = (
specimen: any,
typeName: 'number',
details?: Details,
) => asserts specimen is number;
type AssertTypeofObject = (
specimen: any,
typeName: 'object',
details?: Details,
) => asserts specimen is Record<any, any> | null;
type AssertTypeofString = (
specimen: any,
typeName: 'string',
details?: Details,
) => asserts specimen is string;
type AssertTypeofSymbol = (
specimen: any,
typeName: 'symbol',
details?: Details,
) => asserts specimen is symbol;
type AssertTypeofUndefined = (
specimen: any,
typeName: 'undefined',
details?: Details,
) => asserts specimen is undefined;
export type AssertTypeof = AssertTypeofBigint &
AssertTypeofBoolean &
AssertTypeofFunction &
AssertTypeofNumber &
AssertTypeofObject &
AssertTypeofString &
AssertTypeofSymbol &
AssertTypeofUndefined;
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 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<T>(
/** What we received */
actual: unknown,
/** What we wanted */
expected: T,
/** The details of what was asserted */
details?: Details,
/** An optional alternate error constructor to use */
errConstructor?: GenericErrorConstructor,
options?: AssertMakeErrorOptions,
): asserts actual is T;
/**
* 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"`);
* ```
* or following the normal convention to locally rename `details` to `X`
* and `quote` to `q` like `const { details: X, quote: q } = assert;`:
* ```js
* assert(sky.isBlue(), X`${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;
/**
* 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)}`;
* ```
*
* The normal convention is to locally rename `details` to `X` and `quote` to `q`
* like `const { details: X, 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;
interface CompartmentEvaluateOptions {
sloppyGlobalsMode?: boolean;
__moduleShimLexicals__?: Object;
__evadeHtmlCommentTest__?: boolean;
__evadeImportExpressionTest__?: boolean;
__rejectSomeDirectEvalExpressions__?: boolean;
}
declare global {
var harden: Harden;
var repairIntrinsics: RepairIntrinsics;
var hardenIntrinsics: HardenIntrinsics;
var lockdown: Lockdown;
var assert: Assert;
/**
* Each Compartment constructor is a global. A host that wants to execute
* code in a context bound to a new global creates a new compartment.
*/
export class Compartment {
constructor(options?: CompartmentOptions & { __options__: true });
// Deprecated:
constructor(
globals?: Record<PropertyKey, any> | undefined,
modules?: Record<string, ModuleDescriptor>,
options?: CompartmentOptions,
);
get globalThis(): Record<PropertyKey, any>;
get name(): string;
evaluate(code: string, options?: CompartmentEvaluateOptions): any;
import(specifier: string): Promise<{ namespace: ModuleExportsNamespace }>;
load(specifier: string): Promise<void>;
importNow(specifier: string): ModuleExportsNamespace;
module(specifier: string): ModuleExportsNamespace;
}
}