test(ses-types-test): Adds basic external types validation for SES
Fixes
MODIFY
package.json
package.json
"packages/ses",
"packages/ses-ava",
"packages/ses-integration-test",
+ "packages/ses-types-test",
"packages/static-module-record",
"packages/syrup",
"packages/test262-runner",

ADD
packages/ses-types-test/README.md
packages/ses-types-test/README.md
ADD
packages/ses-types-test/package.json
+{
+ "name": "@endo/ses-types-test",
+ "version": "0.1.0",
+ "private": true,
+ "description": "TypeScript validation for SES types.",
+ "keywords": [],
+ "author": "Endo contributors",
+ "license": "Apache-2.0",
+ "homepage": "https://github.com/endojs/endo/tree/master/packages/ses-types-test#readme",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/endojs/endo.git"
+ },
+ "bugs": {
+ "url": "https://github.com/endojs/endo/issues"
+ },
+ "type": "module",
+ "exports": {},
+ "scripts": {
+ "build": "exit 0",
+ "lint": "yarn lint:types && yarn lint:ts",
+ "lint-fix": "eslint --fix '**/*.ts'",
+ "lint:ts": "eslint '**/*.ts'",
+ "lint:types": "tsc --build tsconfig.json",
+ "test": "yarn lint:types"
+ },
+ "dependencies": {
+ "ses": "^0.13.2"
+ },
+ "devDependencies": {
+ "@endo/eslint-config": "^0.3.6",
+ "babel-eslint": "^10.0.3",
+ "eslint": "^7.23.0",
+ "eslint-config-airbnb-base": "^14.0.0",
+ "eslint-config-prettier": "^6.9.0",
+ "eslint-plugin-eslint-comments": "^3.1.2",
+ "eslint-plugin-import": "^2.19.1",
+ "eslint-plugin-prettier": "^3.1.2",
+ "prettier": "^1.19.1",
+ "typescript": "^4.0.5"
+ },
+ "files": [],
+ "publishConfig": {
+ "access": "private"
+ },
+ "eslintConfig": {
+ "extends": [
+ "@endo"
+ ]
+ },
+ "prettier": {
+ "trailingComma": "all",
+ "singleQuote": true
+ }
+}

ADD
packages/ses-types-test/tsconfig.json
+{
+ "compilerOptions": {
+ "noEmit": true,
+ "target": "esnext",
+ "module": "esnext",
+ "moduleResolution": "node",
+ "strict": true,
+ "strictNullChecks": true,
+ "noImplicitAny": true,
+ "downlevelIteration": true,
+ "strictNullChecks": true
+ },
+ "include": ["**/*.ts", "**/*.js"]
+}

ADD
packages/ses-types-test/validation.ts
+/* eslint-disable no-unused-vars, no-underscore-dangle */
+/// <reference types="ses"/>
+
+// eslint-disable-next-line prettier/prettier
+import type { __LiveExportsMap__, __FixedExportsMap__ } from 'ses';
+
+lockdown();
+lockdown({});
+lockdown({ errorTaming: 'unsafe' });
+lockdown({
+ mathTaming: 'unsafe',
+ dateTaming: 'unsafe',
+ errorTaming: 'unsafe',
+ localeTaming: 'unsafe',
+ consoleTaming: 'unsafe',
+ stackFiltering: 'verbose',
+ overrideTaming: 'moderate',
+ overrideDebug: ['ponies'],
+ __allowUnsafeMonkeyPatching__: 'unsafe',
+});
+
+// @ts-expect-error
+lockdown({ mode: 'BOSS' });
+
+const { x, y } = harden({ x: 4, y: 3 });
+const h: number = (x ** 2 + y ** 2) ** 0.5;
+
+// @ts-expect-error
+const { z, w } = harden({ x: 3, y: 4 });
+
+const c = new Compartment();
+c.evaluate('10');
+
+// @ts-expect-error
+Compartment();
+
+const transforms = [(source: string) => source];
+const globals = {b: 20};
+const globalLexicals = {a: 10};
+const moduleMap = {
+ 'direct': 'redirect',
+ 'internal': {d: 40},
+};
+const moduleMapHook = (specifier: string) => {
+ if (Math.random() < 0.5) {
+ return 'redirect';
+ } else if (Math.random() < 0.5) {
+ return {c: 30};
+ }
+ return undefined;
+};
+
+const resolveHook = (importSpecifier: string, referrerSpecifier: string) => importSpecifier;
+
+const __liveExportsMap__: __LiveExportsMap__ = {a: ['b', false], c: ['d', true]};
+const __fixedExportsMap__: __FixedExportsMap__ = {d: ['e']};
+
+const importHook = async (specifier: string) => {
+ return {
+ imports: ['x'],
+ exports: ['y'],
+ reexports: ['z'],
+ __syncModuleProgram__: '',
+ __liveExportsMap__,
+ __fixedExportsMap__,
+ };
+};
+
+const d = new Compartment(globals, moduleMap, {
+ name: 'compartment',
+ transforms,
+ globalLexicals,
+ moduleMapHook,
+ importHook,
+ resolveHook,
+ __shimTransforms__: transforms,
+});
+
+d.name === 'compartment';
+
+// @ts-expect-error
+d.name = 'alternate';
+
+d.globalThis.endowment = {};
+
+// @ts-expect-error
+d.globalThis = {};
+
+d.load('x').then(() => {});
+
+d.import('x').then(exports => {});
+
+d.importNow('y');
+
+d.module('z');
+
+const { quote: q, details: X } = assert;
+
+assert.equal('a', 'b');
+assert.equal('a', 'b', 'equality error');
+assert.equal('a', 'b', X`equality error left:${q('a')}, right:${q('b')}`);
+
+assert.typeof(10.1, 'number');
+assert.typeof(10n, 'bigint');
+assert.typeof(false, 'boolean');
+assert.typeof(()=>{}, 'function');
+assert.typeof({}, 'object');
+assert.typeof('Hello, World!\n', 'string');
+assert.typeof(Symbol.for('poke'), 'symbol');
+assert.typeof(undefined, 'undefined');
+
+assert.typeof(10.1, 'number', 'not a number');
+assert.typeof(10n, 'bigint', 'unbigint');
+assert.typeof(false, 'boolean', 'not a boolean');
+assert.typeof(()=>{}, 'function', 'not a function');
+assert.typeof({}, 'object', 'not an object');
+assert.typeof('Hello, World!\n', 'string', 'string error');
+assert.typeof(Symbol.for('poke'), 'symbol', 'symbol error');
+assert.typeof(undefined, 'undefined', 'undefined error');
+
+assert.typeof(10.1, 'number', X`n: ${q(10.1)}`);
+assert.typeof(10n, 'bigint', X`n: ${q(10n)}`);
+assert.typeof(false, 'boolean', X`b: ${q(false)}`);
+assert.typeof(()=>{}, 'function', X`f: ${q(()=>{})}`);
+assert.typeof({}, 'object', X`o: ${q({})}`);
+assert.typeof('Hello, World!\n', 'string', X`s: ${q('Hi')}`);
+assert.typeof(Symbol.for('poke'), 'symbol', X`y: ${q(Symbol.for('boop'))}`);
+assert.typeof(undefined, 'undefined', X`u: ${q(undefined)}`);
+
+// @ts-expect-error
+assert.typeof(null, 'sprocket');
+
+// @ts-expect-error
+assert.typeof(null, 'string', 10);
+
+// Opaque token
+assert.typeof(null, 'string', {});
+
+assert.string('i am a string');
+assert.string(0x535176, 'not a string');
+assert.string(0x535176, X`should have been a string ${10}`);
+
+assert.note(new Error('nothing to see here'), X`except this ${q('detail')}`);
+
+X`canst thou string?`.toString();
+
+const f = (value: any) => {
+ throw assert.error(X`details are ${q(value)}`);
+};
+
+const g = (value: any) => {
+ assert.fail(X`details are ${q(value)}`);
+};
+
+const nonfatal = assert.makeAssert(() => {}, true);
+nonfatal(false, 'definitely nonfatal');

MODIFY
packages/ses/NEWS.md
packages/ses/NEWS.md
@@ -2,6 +2,9 @@ User-visible changes in SES:
# Next version
+- Adds more TypeScript definitions, importable with `/// <reference
+ types="ses"/>`, covering `harden`, `lockdown`, `assert`, and `Compartment`,
+ and many types importable with `import('ses')` notation.
- Adds descriptive detail to module system link error messages and fixes the
reported exports for one.

MODIFY
packages/ses/index.d.ts
packages/ses/index.d.ts
-/* eslint-disable */
-/**
- * Transitively freeze an object.
- */
-import type { Hardener } from './src/make-hardener';
-import type { CompartmentConstructor } from './src/compartment-shim';
-import type { Lockdown } from './src/lockdown-shim';
-import type { StaticModuleRecord } from './module-shim';
-
-// For scripts.
-declare var harden: Hardener;
-declare var lockdown: Lockdown;
-declare var Compartment : ReturnType<CompartmentConstructor>;
-declare var StaticModuleRecord : StaticModuleRecord;
-
-declare type StaticModuleType = RedirectStaticModuleInterface | FinalStaticModuleType;
-declare interface RedirectStaticModuleInterface {
- readonly record: FinalStaticModuleType,
+
+// 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 LockdownOptions {
+ regExpTaming?: 'safe' | 'unsafe';
+ localeTaming?: 'safe' | 'unsafe';
+ consoleTaming?: 'safe' | 'unsafe';
+ errorTaming?: 'safe' | 'unsafe';
+ dateTaming?: 'safe' | 'unsafe';
+ mathTaming?: 'safe' | 'unsafe';
+ stackFiltering?: 'concise' | 'verbose';
+ overrideTaming?: 'moderate' | 'min' | 'severe';
+ overrideDebug?: Array<string>;
+ __allowUnsafeMonkeyPatching__?: 'safe' | 'unsafe';
+}
+
+export type Lockdown = (options?: LockdownOptions) => void;
+
+export type __LiveExportsMap__ = Record<string, [string, boolean]>
+export type __FixedExportsMap__ = Record<string, [string]>
+
+export interface PrecompiledStaticModuleInterface {
+ imports: Array<string>;
+ exports: Array<string>;
+ reexports: Array<string>;
+ __syncModuleProgram__: string;
+ __liveExportsMap__: __LiveExportsMap__;
+ __fixedExportsMap__: __FixedExportsMap__;
+}
+
+export interface ThirdPartyStaticModuleInterface {
+ imports: Array<string>;
+ exports: Array<string>;
+ execute(proxiedExports: Object, compartment: Compartment, resolvedImports: Record<string, string>): void;
+}
+
+export type FinalStaticModuleType = PrecompiledStaticModuleInterface | ThirdPartyStaticModuleInterface
+
+export interface RedirectStaticModuleInterface {
+ record: FinalStaticModuleType,
specifier: string
-};
-declare type FinalStaticModuleType = StaticModuleRecord | ThirdPartyModuleInterface;
-declare interface ThirdPartyStaticModuleInterface {
- readonly imports: Array<string>,
- readonly execute: (exports: Object) => void,
-};
+}
-declare type Transform = (source: string) => string;
-declare type ImportHook = (moduleSpecifier: string) => Promise<Object>;
-declare type ModuleMapHook = (moduleSpecifier: string) => string | Object | void;
+export type StaticModuleType = RedirectStaticModuleInterface | FinalStaticModuleType
+
+export type ModuleExportsNamespace = Record<string, any>
+
+export type Transform = (source: string) => string;
+export type ResolveHook = (importSpecifier: string, referrerSpecifier: string) => string;
+export type ModuleMap = Record<string, string | ModuleExportsNamespace>;
+export type ModuleMapHook = (moduleSpecifier: string) => string | ModuleExportsNamespace | void;
+export type ImportHook = (moduleSpecifier: string) => Promise<StaticModuleType>;
+
+export interface CompartmentOptions {
+ name?: string;
+ transforms?: Array<Transform>;
+ globalLexicals?: Record<string, any>;
+ moduleMapHook?: ModuleMapHook,
+ importHook?: ImportHook,
+ resolveHook?: ResolveHook,
+ __shimTransforms__?: Array<Transform>;
+}
+
+export interface EvaluateOptions {
+ transforms?: Array<Transform>;
+ sloppyGlobalsMode?: boolean;
+ __moduleShimLexicals__?: Record<string, any>;
+ __evadeHtmlCommentTest__?: boolean;
+ __rejectSomeDirectEvalExpressions__?: boolean;
+}
+
+// The DetailsToken is an empty object literal.
+export type DetailsToken = Record<any, never>
+export type Details = string | DetailsToken
+
+export interface AssertMakeErrorOptions {
+}
+
+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;
+
+export type Raise = (reason: Error) => void;
+export type MakeAssert = (raise?: Raise, unredacted?: boolean) => Assert;
+
+interface ToStringable {
+ toString(): string
+}
+
+export interface Assert {
+ (value: any, details?: Details, errorConstructor?:ErrorConstructor): void;
+ typeof: AssertTypeof,
+ error(details?: Details, errorConstructor?:ErrorConstructor): Error;
+ fail(details?: Details, errorConstructor?:ErrorConstructor): never;
+ equal(left: any, right: any, details?: Details, errorConstructor?:ErrorConstructor): void;
+ string(specimen: any, details?: Details): void;
+ note(error: Error, details: Details): void;
+ details(template: TemplateStringsArray | string[], ...args: any): DetailsToken,
+ quote(payload: any, spaces?: string|number): ToStringable,
+ makeAssert: MakeAssert,
+}
declare global {
- // For modules.
- var harden: Hardener;
+ var harden: Harden;
+
var lockdown: Lockdown;
- var Compartment : ReturnType<CompartmentConstructor>;
- var StaticModuleRecord : StaticModuleRecord;
- type StaticModuleType = RedirectStaticModuleInterface | FinalStaticModuleType;
- interface RedirectStaticModuleInterface {
- readonly record: FinalStaticModuleType,
- specifier: string
- };
- type FinalStaticModuleType = StaticModuleRecord | ThirdPartyModuleInterface;
- interface ThirdPartyStaticModuleInterface {
- readonly imports: Array<string>,
- readonly execute: (exports: Object) => void,
- };
-
- type Transform = (source: string) => string;
- type ImportHook = (moduleSpecifier: string) => Promise<Object>;
- type ModuleMapHook = (moduleSpecifier: string) => string | Object | void;
+ 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(globals?: Object, moduleMap?: ModuleMap, options?:CompartmentOptions);
+ get globalThis(): Record<string, any>;
+ get name(): string;
+ evaluate(code: string): any;
+ import(specifier: string): Promise<{namespace: ModuleExportsNamespace}>;
+ load(specifier: string): Promise<void>;
+ importNow(specifier: string): ModuleExportsNamespace;
+ module(specifier: string): ModuleExportsNamespace;
+ }
}

MODIFY
packages/ses/lockdown-options.md
packages/ses/lockdown-options.md
@@ -26,6 +26,8 @@ Each option is explained in its own section below.
| `localeTaming` | `'safe'` | `'unsafe'` | `toLocaleString` |
| `consoleTaming` | `'safe'` | `'unsafe'` | deep stacks |
| `errorTaming` | `'safe'` | `'unsafe'` | `errorInstance.stack` |
+| `mathTaming` | `'safe'` | `'unsafe'` | Math.random side-channel |
+| `dateTaming` | `'safe'` | `'unsafe'` | Date.now and new Date timing attacks |
| `stackFiltering` | `'concise'` | `'verbose'` | deep stacks signal/noise |
| `overrideTaming` | `'moderate'` | `'min'` or `'severe'` | override mistake antidote |
| `overrideDebug` | `[]` | array of property names | detect override mistake |

MODIFY
packages/ses/src/compartment-shim.js
packages/ses/src/compartment-shim.js
// @ts-check
-/* eslint no-underscore-dangle: ["off"] */
+/* eslint-disable no-underscore-dangle */
+/// <reference types="ses">
import {
assign,
@@ -214,26 +215,12 @@ defineProperties(InertCompartment, {
prototype: { value: CompartmentPrototype },
});
-/**
- * @callback CompartmentConstructor
- * 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.
- *
- * @param {Object} endowments
- * @param {Object} _moduleMap
- * @param {Object} [options]
- * @param {string} [options.name]
- * @param {Array<Transform>} [options.transforms]
- * @param {Array<Transform>} [options.__shimTransforms__]
- * @param {Object} [options.globalLexicals]
- */
-
/**
* @callback MakeCompartmentConstructor
* @param {MakeCompartmentConstructor} targetMakeCompartmentConstructor
- * @param {Object} intrinsics
+ * @param {Record<string, any>} intrinsics
* @param {(object: Object) => void} nativeBrander
- * @returns {CompartmentConstructor}
+ * @returns {Compartment['constructor']}
*/
/** @type {MakeCompartmentConstructor} */
@@ -242,7 +229,6 @@ export const makeCompartmentConstructor = (
intrinsics,
nativeBrander,
) => {
- /** @type {CompartmentConstructor} */
function Compartment(endowments = {}, moduleMap = {}, options = {}) {
if (new.target === undefined) {
throw new TypeError(

MODIFY
packages/ses/src/error/types.js
packages/ses/src/error/types.js
*/
// 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.
+// There may eventually be a better way to do this, but they break with
+// Typescript 4.0.
/**
* @callback AssertTypeofBigint
* @param {any} specimen

MODIFY
packages/ses/src/lockdown-shim.js
packages/ses/src/lockdown-shim.js
@@ -33,20 +33,7 @@ import { tameConsole } from './error/tame-console.js';
import tameErrorConstructor from './error/tame-error-constructor.js';
import { assert, makeAssert } from './error/assert.js';
-/**
- * @typedef {{
- * dateTaming?: 'safe' | 'unsafe',
- * errorTaming?: 'safe' | 'unsafe',
- * mathTaming?: 'safe' | 'unsafe',
- * regExpTaming?: 'safe' | 'unsafe',
- * localeTaming?: 'safe' | 'unsafe',
- * consoleTaming?: 'safe' | 'unsafe',
- * overrideTaming?: 'min' | 'moderate' | 'severe',
- * overrideDebug?: Array<string>,
- * stackFiltering?: 'concise' | 'verbose',
- * __allowUnsafeMonkeyPatching__?: 'safe' | 'unsafe',
- * }} LockdownOptions
- */
+/** @typedef {import('../index.js').LockdownOptions} LockdownOptions */
const { details: d, quote: q } = assert;
@@ -324,6 +311,7 @@ export const repairIntrinsics = (
* @param {CompartmentConstructorMaker} makeCompartmentConstructor
* @param {Object} compartmentPrototype
* @param {() => Object} getAnonymousIntrinsics
+ * @returns {import('../index.js').Lockdown}
*/
export const makeLockdown = (
makeCompartmentConstructor,
@@ -344,5 +332,3 @@ export const makeLockdown = (
};
return lockdown;
};
-
-/** @typedef {ReturnType<typeof makeLockdown>} Lockdown */

MODIFY
packages/ses/src/make-hardener.js
packages/ses/src/make-hardener.js
@@ -25,13 +25,13 @@ const { freeze, getOwnPropertyDescriptors, getPrototypeOf } = Object;
const { ownKeys } = Reflect;
/**
- * @typedef {<T>(root: T) => T} Hardener
+ * @typedef {import('../index.js').Harden} Harden
*/
/**
* Create a `harden` function.
*
- * @returns {Hardener}
+ * @returns {Harden}
*/
export const makeHardener = () => {
const hardened = new WeakSet();

test(ses-types-test): Adds basic external types validation for SES

Fixes #777