diff --git a/packages/extension/package.json b/packages/extension/package.json index 68ca6c89d..1a3559723 100644 --- a/packages/extension/package.json +++ b/packages/extension/package.json @@ -31,7 +31,10 @@ "test:watch": "vitest --config vitest.config.ts" }, "dependencies": { + "@endo/captp": "^4.2.2", "@endo/eventual-send": "^1.2.4", + "@endo/exo": "^1.5.2", + "@endo/patterns": "^1.4.2", "@endo/promise-kit": "^1.1.4", "@metamask/snaps-utils": "^7.8.0", "@metamask/utils": "^9.1.0", diff --git a/packages/extension/src/background-trusted-prelude.js b/packages/extension/src/background-trusted-prelude.js index 19405f97b..fd661e49f 100644 --- a/packages/extension/src/background-trusted-prelude.js +++ b/packages/extension/src/background-trusted-prelude.js @@ -1,2 +1,2 @@ -import './dev-console.js'; import './endoify.js'; +import './dev-console.js'; diff --git a/packages/extension/src/background.ts b/packages/extension/src/background.ts index c22a61d42..3182291dc 100644 --- a/packages/extension/src/background.ts +++ b/packages/extension/src/background.ts @@ -1,11 +1,18 @@ +import type { Json } from '@metamask/utils'; + import './background-trusted-prelude.js'; -import type { ExtensionMessage } from './shared.js'; -import { Command, makeHandledCallback } from './shared.js'; +import type { ExtensionMessage } from './message.js'; +import { Command, ExtensionMessageTarget } from './message.js'; +import { makeHandledCallback } from './shared.js'; -// globalThis.kernel will exist due to dev-console.js +// globalThis.kernel will exist due to dev-console.js in background-trusted-prelude.js Object.defineProperties(globalThis.kernel, { - sendMessage: { - value: sendMessage, + capTpCall: { + value: async (method: string, params: Json[]) => + sendMessage(Command.CapTpCall, { method, params }), + }, + capTpInit: { + value: async () => sendMessage(Command.CapTpInit), }, evaluate: { value: async (source: string) => sendMessage(Command.Evaluate, source), @@ -13,7 +20,11 @@ Object.defineProperties(globalThis.kernel, { ping: { value: async () => sendMessage(Command.Ping), }, + sendMessage: { + value: sendMessage, + }, }); +harden(globalThis.kernel); const OFFSCREEN_DOCUMENT_PATH = '/offscreen.html'; @@ -29,12 +40,12 @@ chrome.action.onClicked.addListener(() => { * @param data - The message data. * @param data.name - The name to include in the message. */ -async function sendMessage(type: string, data?: string): Promise { +async function sendMessage(type: string, data?: Json): Promise { await provideOffScreenDocument(); await chrome.runtime.sendMessage({ type, - target: 'offscreen', + target: ExtensionMessageTarget.Offscreen, data: data ?? null, }); } @@ -54,8 +65,8 @@ async function provideOffScreenDocument(): Promise { // Handle replies from the offscreen document chrome.runtime.onMessage.addListener( - makeHandledCallback(async (message: ExtensionMessage) => { - if (message.target !== 'background') { + makeHandledCallback(async (message: ExtensionMessage) => { + if (message.target !== ExtensionMessageTarget.Background) { console.warn( `Background received message with unexpected target: "${message.target}"`, ); @@ -64,25 +75,17 @@ chrome.runtime.onMessage.addListener( switch (message.type) { case Command.Evaluate: + case Command.CapTpCall: + case Command.CapTpInit: case Command.Ping: console.log(message.data); - await closeOffscreenDocument(); break; default: console.error( + // @ts-expect-error The type of `message` is `never`, but this could happen at runtime. // eslint-disable-next-line @typescript-eslint/restrict-template-expressions `Background received unexpected message type: "${message.type}"`, ); } }), ); - -/** - * Close the offscreen document if it exists. - */ -async function closeOffscreenDocument(): Promise { - if (!(await chrome.offscreen.hasDocument())) { - return; - } - await chrome.offscreen.closeDocument(); -} diff --git a/packages/extension/src/dev-console.test.ts b/packages/extension/src/dev-console.test.ts new file mode 100644 index 000000000..c3c6dfb76 --- /dev/null +++ b/packages/extension/src/dev-console.test.ts @@ -0,0 +1,21 @@ +import { describe, it, expect } from 'vitest'; +import '@ocap/shims/endoify'; +import './dev-console.js'; + +describe('vat-console', () => { + describe('kernel', () => { + it('is available on globalThis', async () => { + expect(kernel).toBeDefined(); + }); + + it('has expected property descriptors', async () => { + expect( + Object.getOwnPropertyDescriptor(globalThis, 'kernel'), + ).toMatchObject({ + configurable: false, + enumerable: true, + writable: false, + }); + }); + }); +}); diff --git a/packages/extension/src/envelope.ts b/packages/extension/src/envelope.ts new file mode 100644 index 000000000..cc5e72e37 --- /dev/null +++ b/packages/extension/src/envelope.ts @@ -0,0 +1,22 @@ +import { isObject } from '@metamask/utils'; + +import type { WrappedIframeMessage } from './message.js'; +import { isWrappedIframeMessage } from './message.js'; + +export enum EnvelopeLabel { + Command = 'command', + CapTp = 'capTp', +} + +export type StreamEnvelope = + | { + label: EnvelopeLabel.Command; + content: WrappedIframeMessage; + } + | { label: EnvelopeLabel.CapTp; content: unknown }; + +export const isStreamEnvelope = (value: unknown): value is StreamEnvelope => + isObject(value) && + (value.label === EnvelopeLabel.CapTp || + (value.label === EnvelopeLabel.Command && + isWrappedIframeMessage(value.content))); diff --git a/packages/extension/src/iframe-manager.test.ts b/packages/extension/src/iframe-manager.test.ts index b8dd6ef16..45bb5b191 100644 --- a/packages/extension/src/iframe-manager.test.ts +++ b/packages/extension/src/iframe-manager.test.ts @@ -3,8 +3,10 @@ import * as snapsUtils from '@metamask/snaps-utils'; import { delay, makePromiseKitMock } from '@ocap/test-utils'; import { vi, describe, it, expect } from 'vitest'; +import { EnvelopeLabel } from './envelope.js'; import { IframeManager } from './iframe-manager.js'; -import { Command } from './shared.js'; +import type { IframeMessage } from './message.js'; +import { Command } from './message.js'; vi.mock('@endo/promise-kit', () => makePromiseKitMock()); @@ -34,7 +36,7 @@ describe('IframeManager', () => { expect(id).toBeTypeOf('string'); expect(sendMessageSpy).toHaveBeenCalledOnce(); expect(sendMessageSpy).toHaveBeenCalledWith(id, { - type: 'ping', + type: Command.Ping, data: null, }); }); @@ -59,7 +61,7 @@ describe('IframeManager', () => { expect(returnedId).toBe(id); expect(sendMessageSpy).toHaveBeenCalledOnce(); expect(sendMessageSpy).toHaveBeenCalledWith(id, { - type: 'ping', + type: Command.Ping, data: null, }); }); @@ -89,7 +91,7 @@ describe('IframeManager', () => { expect(id).toBeTypeOf('string'); expect(sendMessageSpy).toHaveBeenCalledOnce(); expect(sendMessageSpy).toHaveBeenCalledWith(id, { - type: 'ping', + type: Command.Ping, data: null, }); }); @@ -126,6 +128,297 @@ describe('IframeManager', () => { expect(removeSpy).not.toHaveBeenCalled(); }); + + it('warns of unresolved messages', async () => { + const id = 'foo'; + const messageCount = 7; + const awaitCount = 2; + + vi.mocked(snapsUtils.createWindow).mockImplementationOnce(vi.fn()); + + const manager = new IframeManager(); + + vi.spyOn(manager, 'sendMessage').mockImplementationOnce(vi.fn()); + + const { port1, port2 } = new MessageChannel(); + + await manager.create({ id, getPort: makeGetPort(port1) }); + + const warnSpy = vi.spyOn(console, 'warn'); + + const messagePromises = Array(messageCount) + .fill(0) + .map(async (_, i) => + manager.sendMessage(id, { type: Command.Evaluate, data: `${i}+1` }), + ); + + // resolve the first `awaitCount` promises + for (let i = 0; i < awaitCount; i++) { + port2.postMessage({ + done: false, + value: { + label: EnvelopeLabel.Command, + content: { + id: `foo-${i + 1}`, + message: { + type: Command.Evaluate, + data: `${i + 1}`, + }, + }, + }, + }); + await messagePromises[i]; + } + + await manager.delete(id); + expect(warnSpy).toHaveBeenCalledTimes(messageCount - awaitCount); + // This test assumes messageIds begin at 1, not 0 + expect(warnSpy).toHaveBeenLastCalledWith( + `Unhandled orphaned message: ${id}-${messageCount}`, + ); + }); + }); + + describe('capTp', () => { + it('throws if called before initialization', async () => { + const mockWindow = {}; + vi.mocked(snapsUtils.createWindow).mockResolvedValueOnce( + mockWindow as Window, + ); + const manager = new IframeManager(); + vi.spyOn(manager, 'sendMessage').mockImplementation(vi.fn()); + const [, id] = await manager.create({ getPort: makeGetPort() }); + + await expect( + async () => + await manager.callCapTp(id, { + method: 'whatIsTheGreatFrangooly', + params: [], + }), + ).rejects.toThrow(/does not have a CapTP connection\.$/u); + }); + + it('throws if initialization is called twice on the same vat', async () => { + const id = 'frangooly'; + + const capTpInit = { + query: { + label: EnvelopeLabel.Command, + content: { + id: `${id}-1`, + message: { + data: null, + type: 'makeCapTp', + }, + }, + }, + response: { + label: EnvelopeLabel.Command, + content: { + id: `${id}-1`, + message: { + type: 'makeCapTp', + data: null, + }, + }, + }, + }; + + vi.mocked(snapsUtils.createWindow).mockImplementationOnce(vi.fn()); + + const { port1, port2 } = new MessageChannel(); + const port1PostMessageSpy = vi + .spyOn(port1, 'postMessage') + .mockImplementation(vi.fn()); + + let port1PostMessageCallCounter: number = 0; + const expectSendMessageToHaveBeenCalledOnceMoreWith = ( + expectation: unknown, + ): void => { + port1PostMessageCallCounter += 1; + expect(port1PostMessageSpy).toHaveBeenCalledTimes( + port1PostMessageCallCounter, + ); + expect(port1PostMessageSpy).toHaveBeenLastCalledWith({ + done: false, + value: expectation, + }); + }; + + const mockReplyWith = (message: unknown): void => + port2.postMessage({ + done: false, + value: message, + }); + + const manager = new IframeManager(); + + vi.spyOn(manager, 'sendMessage').mockImplementationOnce(vi.fn()); + + await manager.create({ id, getPort: makeGetPort(port1) }); + + // Init CapTP connection + const initCapTpPromise = manager.makeCapTp(id); + + expectSendMessageToHaveBeenCalledOnceMoreWith(capTpInit.query); + mockReplyWith(capTpInit.response); + + await initCapTpPromise.then((resolvedValue) => + console.debug(`CapTp initialized: ${JSON.stringify(resolvedValue)}`), + ); + + await expect(async () => await manager.makeCapTp(id)).rejects.toThrow( + /already has a CapTP connection\./u, + ); + }); + + it('does TheGreatFrangooly', async () => { + const id = 'frangooly'; + + const capTpInit = { + query: { + label: EnvelopeLabel.Command, + content: { + id: `${id}-1`, + message: { + data: null, + type: 'makeCapTp', + }, + }, + }, + response: { + label: EnvelopeLabel.Command, + content: { + id: `${id}-1`, + message: { + type: 'makeCapTp', + data: null, + }, + }, + }, + }; + + const greatFrangoolyBootstrap = { + query: { + label: 'capTp', + content: { + epoch: 0, + questionID: 'q-1', + type: 'CTP_BOOTSTRAP', + }, + }, + response: { + label: 'capTp', + content: { + type: 'CTP_RETURN', + epoch: 0, + answerID: 'q-1', + result: { + body: '{"@qclass":"slot","iface":"Alleged: TheGreatFrangooly","index":0}', + slots: ['o+1'], + }, + }, + }, + }; + + const greatFrangoolyCall = { + query: { + label: 'capTp', + content: { + type: 'CTP_CALL', + epoch: 0, + method: { + body: '["whatIsTheGreatFrangooly",[]]', + slots: [], + }, + questionID: 'q-2', + target: 'o-1', + }, + }, + response: { + label: 'capTp', + content: { + type: 'CTP_RETURN', + epoch: 0, + answerID: 'q-2', + result: { + body: '"Crowned with Chaos"', + slots: [], + }, + }, + }, + }; + + vi.mocked(snapsUtils.createWindow).mockImplementationOnce(vi.fn()); + + const { port1, port2 } = new MessageChannel(); + const port1PostMessageSpy = vi + .spyOn(port1, 'postMessage') + .mockImplementation(vi.fn()); + + let port1PostMessageCallCounter: number = 0; + const expectSendMessageToHaveBeenCalledOnceMoreWith = ( + expectation: unknown, + ): void => { + port1PostMessageCallCounter += 1; + expect(port1PostMessageSpy).toHaveBeenCalledTimes( + port1PostMessageCallCounter, + ); + expect(port1PostMessageSpy).toHaveBeenLastCalledWith({ + done: false, + value: expectation, + }); + }; + + const mockReplyWith = (message: unknown): void => + port2.postMessage({ + done: false, + value: message, + }); + + const manager = new IframeManager(); + + vi.spyOn(manager, 'sendMessage').mockImplementationOnce(vi.fn()); + + await manager.create({ id, getPort: makeGetPort(port1) }); + + // Init CapTP connection + const initCapTpPromise = manager.makeCapTp(id); + + expectSendMessageToHaveBeenCalledOnceMoreWith(capTpInit.query); + mockReplyWith(capTpInit.response); + + await initCapTpPromise.then((resolvedValue) => + console.debug(`CapTp initialized: ${JSON.stringify(resolvedValue)}`), + ); + + // Bootstrap TheGreatFrangooly... + const callCapTpResponse = manager.callCapTp(id, { + method: 'whatIsTheGreatFrangooly', + params: [], + }); + + expectSendMessageToHaveBeenCalledOnceMoreWith( + greatFrangoolyBootstrap.query, + ); + mockReplyWith(greatFrangoolyBootstrap.response); + + await delay().then(() => + console.debug('TheGreatFrangooly bootstrapped...'), + ); + + // ...and call it. + expectSendMessageToHaveBeenCalledOnceMoreWith(greatFrangoolyCall.query); + mockReplyWith(greatFrangoolyCall.response); + + await callCapTpResponse.then((resolvedValue) => + console.debug( + `TheGreatFrangooly called: ${JSON.stringify(resolvedValue)}`, + ), + ); + + expect(await callCapTpResponse).equals('Crowned with Chaos'); + }); }); describe('sendMessage', () => { @@ -142,39 +435,48 @@ describe('IframeManager', () => { const id = 'foo'; await manager.create({ id, getPort: makeGetPort(port1) }); - const message = { type: Command.Evaluate, data: '2+2' }; + const message: IframeMessage = { type: Command.Evaluate, data: '2+2' }; + const response: IframeMessage = { type: Command.Evaluate, data: '4' }; + // sendMessage wraps the content in a EnvelopeLabel.Command envelope const messagePromise = manager.sendMessage(id, message); const messageId: string | undefined = - portPostMessageSpy.mock.lastCall?.[0]?.value?.id; + portPostMessageSpy.mock.lastCall?.[0]?.value?.content?.id; expect(messageId).toBeTypeOf('string'); + // postMessage sends the json directly, so we have to wrap it in an envelope here port2.postMessage({ done: false, value: { - id: messageId, - message: { - type: Command.Evaluate, - data: '4', + label: EnvelopeLabel.Command, + content: { + id: messageId, + message: response, }, }, }); + // awaiting event loop should resolve the messagePromise + expect(await messagePromise).toBe(response.data); + + // messagePromise doesn't resolve until message was posted expect(portPostMessageSpy).toHaveBeenCalledOnce(); expect(portPostMessageSpy).toHaveBeenCalledWith({ done: false, value: { - id: messageId, - message, + label: EnvelopeLabel.Command, + content: { + id: messageId, + message, + }, }, }); - expect(await messagePromise).toBe('4'); }); it('throws if iframe not found', async () => { const manager = new IframeManager(); const id = 'foo'; - const message = { type: Command.Ping, data: null }; + const message: IframeMessage = { type: Command.Ping, data: null }; await expect(manager.sendMessage(id, message)).rejects.toThrow( `No vat with id "${id}"`, @@ -219,10 +521,13 @@ describe('IframeManager', () => { port2.postMessage({ done: false, value: { - id: 'foo', - message: { - type: Command.Evaluate, - data: '"bar"', + label: EnvelopeLabel.Command, + content: { + id: 'foo', + message: { + type: Command.Evaluate, + data: '"bar"', + }, }, }, }); diff --git a/packages/extension/src/iframe-manager.ts b/packages/extension/src/iframe-manager.ts index 5e5c1866d..abd994263 100644 --- a/packages/extension/src/iframe-manager.ts +++ b/packages/extension/src/iframe-manager.ts @@ -1,46 +1,55 @@ +import { makeCapTP } from '@endo/captp'; +import { E } from '@endo/eventual-send'; import type { PromiseKit } from '@endo/promise-kit'; import { makePromiseKit } from '@endo/promise-kit'; import { createWindow } from '@metamask/snaps-utils'; -import type { MessagePortReader, MessagePortStreamPair } from '@ocap/streams'; +import type { StreamPair, Reader } from '@ocap/streams'; import { initializeMessageChannel, makeMessagePortStreamPair, } from '@ocap/streams'; -import type { IframeMessage, WrappedIframeMessage } from './shared.js'; -import { Command, isWrappedIframeMessage } from './shared.js'; +import type { StreamEnvelope } from './envelope.js'; +import { EnvelopeLabel, isStreamEnvelope } from './envelope.js'; +import type { CapTpPayload, IframeMessage, MessageId } from './message.js'; +import { Command } from './message.js'; +import { makeCounter, type VatId } from './shared.js'; const IFRAME_URI = 'iframe.html'; /** * Get a DOM id for our iframes, for greater collision resistance. * - * @param id - The id to base the DOM id on. + * @param id - The vat id to base the DOM id on. * @returns The DOM id. */ -const getHtmlId = (id: string): string => `ocap-iframe-${id}`; +const getHtmlId = (id: VatId): string => `ocap-iframe-${id}`; type PromiseCallbacks = Omit, 'promise'>; type GetPort = (targetWindow: Window) => Promise; +type VatRecord = { + streams: StreamPair; + messageCounter: () => number; + unresolvedMessages: Map; + capTp?: ReturnType; +}; + /** * A singleton class to manage and message iframes. */ export class IframeManager { - #currentId: number; - - readonly #unresolvedMessages: Map; + readonly #vats: Map; - readonly #vats: Map>; + readonly #vatIdCounter: () => number; /** * Create a new IframeManager. */ constructor() { - this.#currentId = 0; this.#vats = new Map(); - this.#unresolvedMessages = new Map(); + this.#vatIdCounter = makeCounter(); } /** @@ -49,20 +58,24 @@ export class IframeManager { * @param args - Options bag. * @param args.id - The id of the vat to create. * @param args.getPort - A function to get the message port for the iframe. - * @returns The iframe's content window, and its internal id. + * @returns The iframe's content window, and the id of the associated vat. */ async create( - args: { id?: string; getPort?: GetPort } = {}, - ): Promise { - const id = args.id ?? this.#nextId(); + args: { id?: VatId; getPort?: GetPort } = {}, + ): Promise { + const id = args.id ?? this.#nextVatId(); const getPort = args.getPort ?? initializeMessageChannel; const newWindow = await createWindow(IFRAME_URI, getHtmlId(id)); const port = await getPort(newWindow); - const streams = makeMessagePortStreamPair(port); - this.#vats.set(id, streams); + const streams = makeMessagePortStreamPair(port); + this.#vats.set(id, { + streams, + messageCounter: makeCounter(), + unresolvedMessages: new Map(), + }); /* v8 ignore next 4: Not known to be possible. */ - this.#receiveMessages(streams.reader).catch((error) => { + this.#receiveMessages(id, streams.reader).catch((error) => { console.error(`Unexpected read error from vat "${id}"`, error); this.delete(id).catch(() => undefined); }); @@ -73,19 +86,22 @@ export class IframeManager { } /** - * Delete an iframe. + * Delete a vat and its associated iframe. * - * @param id - The id of the iframe to delete. + * @param id - The id of the vat to delete. * @returns A promise that resolves when the iframe is deleted. */ - async delete(id: string): Promise { - const streams = this.#vats.get(id); - if (streams === undefined) { + async delete(id: VatId): Promise { + const vat = this.#vats.get(id); + if (vat === undefined) { return undefined; } - const closeP = streams.return(); + const closeP = vat.streams.return(); // TODO: Handle orphaned messages + for (const [messageId] of vat.unresolvedMessages) { + console.warn(`Unhandled orphaned message: ${messageId}`); + } this.#vats.delete(id); const iframe = document.getElementById(getHtmlId(id)); @@ -100,35 +116,61 @@ export class IframeManager { } /** - * Send a message to an iframe. + * Send a message to a vat. * - * @param id - The id of the iframe to send the message to. + * @param id - The id of the vat to send the message to. * @param message - The message to send. * @returns A promise that resolves the response to the message. */ - async sendMessage( - id: string, - message: IframeMessage, - ): Promise { - const streams = this.#vats.get(id); - if (streams === undefined) { - throw new Error(`No vat with id "${id}"`); - } - + async sendMessage(id: VatId, message: IframeMessage): Promise { + const vat = this.#expectGetVat(id); const { promise, reject, resolve } = makePromiseKit(); - const messageId = this.#nextId(); - this.#unresolvedMessages.set(messageId, { reject, resolve }); - await streams.writer.next({ id: messageId, message }); + const messageId = this.#nextMessageId(id); + + vat.unresolvedMessages.set(messageId, { reject, resolve }); + await vat.streams.writer.next({ + label: EnvelopeLabel.Command, + content: { id: messageId, message }, + }); return promise; } + async callCapTp(id: VatId, payload: CapTpPayload): Promise { + const { capTp } = this.#expectGetVat(id); + if (capTp === undefined) { + throw new Error(`Vat with id "${id}" does not have a CapTP connection.`); + } + return E(capTp.getBootstrap())[payload.method](...payload.params); + } + + async makeCapTp(id: VatId): Promise { + const vat = this.#expectGetVat(id); + if (vat.capTp !== undefined) { + throw new Error(`Vat with id "${id}" already has a CapTP connection.`); + } + + // Handle writes here. #receiveMessages() handles reads. + const { writer } = vat.streams; + // https://github.com/endojs/endo/issues/2412 + // eslint-disable-next-line @typescript-eslint/no-misused-promises + const ctp = makeCapTP(id, async (content: unknown) => { + console.log('CapTP to vat', JSON.stringify(content, null, 2)); + await writer.next({ label: EnvelopeLabel.CapTp, content }); + }); + + vat.capTp = ctp; + return this.sendMessage(id, { type: Command.CapTpInit, data: null }); + } + async #receiveMessages( - reader: MessagePortReader, + vatId: VatId, + reader: Reader, ): Promise { + const vat = this.#expectGetVat(vatId); for await (const rawMessage of reader) { console.debug('Offscreen received message', rawMessage); - if (!isWrappedIframeMessage(rawMessage)) { + if (!isStreamEnvelope(rawMessage)) { console.warn( 'Offscreen received message with unexpected format', rawMessage, @@ -136,20 +178,56 @@ export class IframeManager { return; } - const { id, message } = rawMessage; - const promiseCallbacks = this.#unresolvedMessages.get(id); - if (promiseCallbacks === undefined) { - console.error(`No unresolved message with id "${id}".`); - continue; + switch (rawMessage.label) { + case EnvelopeLabel.CapTp: { + console.log( + 'CapTP from vat', + JSON.stringify(rawMessage.content, null, 2), + ); + const { capTp } = this.#expectGetVat(vatId); + if (capTp !== undefined) { + capTp.dispatch(rawMessage.content); + } + break; + } + case EnvelopeLabel.Command: { + const { id, message } = rawMessage.content; + const promiseCallbacks = vat.unresolvedMessages.get(id); + if (promiseCallbacks === undefined) { + console.error(`No unresolved message with id "${id}".`); + } else { + vat.unresolvedMessages.delete(id); + promiseCallbacks.resolve(message.data); + } + break; + } + /* v8 ignore next 3: Exhaustiveness check */ + default: + // @ts-expect-error The type of `rawMessage` is `never`, but this could happen at runtime. + throw new Error(`Unexpected message label "${rawMessage.label}".`); } - - promiseCallbacks.resolve(message.data); } } - #nextId(): string { - const id = this.#currentId; - this.#currentId += 1; - return String(id); + /** + * Get a vat record by id, or throw an error if it doesn't exist. + * + * @param id - The id of the vat to get. + * @returns The vat record. + */ + #expectGetVat(id: VatId): VatRecord { + const vat = this.#vats.get(id); + if (vat === undefined) { + throw new Error(`No vat with id "${id}"`); + } + return vat; } + + readonly #nextMessageId = (id: VatId): MessageId => { + return `${id}-${this.#expectGetVat(id).messageCounter()}`; + }; + + readonly #nextVatId = (): MessageId => { + return `${this.#vatIdCounter()}`; + }; } diff --git a/packages/extension/src/iframe.ts b/packages/extension/src/iframe.ts index e5940f27c..bcd25f1ef 100644 --- a/packages/extension/src/iframe.ts +++ b/packages/extension/src/iframe.ts @@ -1,7 +1,12 @@ +import { makeCapTP } from '@endo/captp'; +import { makeExo } from '@endo/exo'; +import { M } from '@endo/patterns'; import { receiveMessagePort, makeMessagePortStreamPair } from '@ocap/streams'; -import type { WrappedIframeMessage } from './shared.js'; -import { Command, isWrappedIframeMessage } from './shared.js'; +import type { StreamEnvelope } from './envelope.js'; +import { EnvelopeLabel, isStreamEnvelope } from './envelope.js'; +import type { IframeMessage, WrappedIframeMessage } from './message.js'; +import { Command } from './message.js'; const defaultCompartment = new Compartment({ URL }); @@ -12,36 +17,85 @@ main().catch(console.error); */ async function main(): Promise { const port = await receiveMessagePort(); - const streams = makeMessagePortStreamPair(port); + const streams = makeMessagePortStreamPair(port); + let capTp: ReturnType | undefined; - for await (const wrappedMessage of streams.reader) { - console.debug('iframe received message', wrappedMessage); + for await (const rawMessage of streams.reader) { + console.debug('iframe received message', rawMessage); - if (!isWrappedIframeMessage(wrappedMessage)) { + if (!isStreamEnvelope(rawMessage)) { console.error( 'iframe received message with unexpected format', - wrappedMessage, + rawMessage, ); return; } - const { id, message } = wrappedMessage; + switch (rawMessage.label) { + case EnvelopeLabel.CapTp: + if (capTp !== undefined) { + capTp.dispatch(rawMessage.content); + } + break; + case EnvelopeLabel.Command: + await handleMessage(rawMessage.content); + break; + /* v8 ignore next 3: Exhaustiveness check */ + default: + // @ts-expect-error The type of `rawMessage` is `never`, but this could happen at runtime. + throw new Error(`Unexpected message label "${rawMessage.label}".`); + } + } + await streams.return(); + throw new Error('MessagePortReader ended unexpectedly.'); + + /** + * Handle a message from the parent window. + * + * @param wrappedMessage - The wrapped message to handle. + * @param wrappedMessage.id - The id of the message. + * @param wrappedMessage.message - The message to handle. + */ + async function handleMessage({ + id, + message, + }: WrappedIframeMessage): Promise { switch (message.type) { case Command.Evaluate: { if (typeof message.data !== 'string') { console.error( 'iframe received message with unexpected data type', - message.data, + // @ts-expect-error The type of `message.data` is `never`, but this could happen at runtime. + stringifyResult(message.data), ); return; } const result = safelyEvaluate(message.data); - await reply(id, Command.Evaluate, stringifyResult(result)); + await replyToMessage(id, { + type: Command.Evaluate, + data: stringifyResult(result), + }); + break; + } + case Command.CapTpInit: { + const bootstrap = makeExo( + 'TheGreatFrangooly', + M.interface('TheGreatFrangooly', {}, { defaultGuards: 'passable' }), + { whatIsTheGreatFrangooly: () => 'Crowned with Chaos' }, + ); + + capTp = makeCapTP( + 'iframe', + async (content: unknown) => + streams.writer.next({ label: EnvelopeLabel.CapTp, content }), + bootstrap, + ); + await replyToMessage(id, { type: Command.CapTpInit, data: null }); break; } case Command.Ping: - await reply(id, Command.Ping, 'pong'); + await replyToMessage(id, { type: Command.Ping, data: 'pong' }); break; default: console.error( @@ -51,22 +105,20 @@ async function main(): Promise { } } - await streams.return(); - throw new Error('MessagePortReader ended unexpectedly.'); - /** - * Reply to the parent window. + * Reply to a message from the parent window. * * @param id - The id of the message to reply to. - * @param messageType - The message type. - * @param data - The message data. + * @param message - The message to reply with. */ - async function reply( + async function replyToMessage( id: string, - messageType: Command, - data: string, + message: IframeMessage, ): Promise { - await streams.writer.next({ id, message: { type: messageType, data } }); + await streams.writer.next({ + label: EnvelopeLabel.Command, + content: { id, message }, + }); } /** diff --git a/packages/extension/src/message.test.ts b/packages/extension/src/message.test.ts new file mode 100644 index 000000000..8fdec3480 --- /dev/null +++ b/packages/extension/src/message.test.ts @@ -0,0 +1,26 @@ +import { describe, it, expect } from 'vitest'; + +import { isWrappedIframeMessage } from './message.js'; + +describe('message', () => { + describe('isWrappedIframeMessage', () => { + it('returns true for valid messages', () => { + expect( + isWrappedIframeMessage({ + id: '1', + message: { type: 'evaluate', data: '1 + 1' }, + }), + ).toBe(true); + }); + + it.each([ + [{}], + [{ id: '1' }], + [{ message: { type: 'evaluate' } }], + [{ id: '1', message: { type: 'evaluate' } }], + [{ id: '1', message: { type: 'evaluate', data: 1 } }], + ])('returns false for invalid messages: %j', (message) => { + expect(isWrappedIframeMessage(message)).toBe(false); + }); + }); +}); diff --git a/packages/extension/src/message.ts b/packages/extension/src/message.ts new file mode 100644 index 000000000..c9e26b614 --- /dev/null +++ b/packages/extension/src/message.ts @@ -0,0 +1,60 @@ +import type { Primitive } from '@endo/captp'; +import { isObject } from '@metamask/utils'; + +export type MessageId = string; + +type DataObject = + | Primitive + | Promise + | DataObject[] + | { [key: string]: DataObject }; + +export enum ExtensionMessageTarget { + Background = 'background', + Offscreen = 'offscreen', +} + +type CommandForm< + CommandType extends Command, + Data extends DataObject, + TargetType extends ExtensionMessageTarget, +> = { + type: CommandType; + target?: TargetType; + data: Data; +}; + +export enum Command { + CapTpCall = 'callCapTp', + CapTpInit = 'makeCapTp', + Evaluate = 'evaluate', + Ping = 'ping', +} + +export type CapTpPayload = { + method: string; + params: DataObject[]; +}; + +type CommandMessage = + | CommandForm + | CommandForm + | CommandForm + | CommandForm; + +export type ExtensionMessage = CommandMessage; +export type IframeMessage = CommandMessage; + +export type WrappedIframeMessage = { + id: MessageId; + message: IframeMessage; +}; + +export const isWrappedIframeMessage = ( + value: unknown, +): value is WrappedIframeMessage => + isObject(value) && + typeof value.id === 'string' && + isObject(value.message) && + typeof value.message.type === 'string' && + (typeof value.message.data === 'string' || value.message.data === null); diff --git a/packages/extension/src/offscreen.ts b/packages/extension/src/offscreen.ts index 5c48b8d13..c379f3c85 100644 --- a/packages/extension/src/offscreen.ts +++ b/packages/extension/src/offscreen.ts @@ -1,6 +1,7 @@ import { IframeManager } from './iframe-manager.js'; -import type { ExtensionMessage } from './shared.js'; -import { Command, makeHandledCallback } from './shared.js'; +import type { ExtensionMessage } from './message.js'; +import { Command, ExtensionMessageTarget } from './message.js'; +import { makeHandledCallback } from './shared.js'; main().catch(console.error); @@ -11,12 +12,14 @@ async function main(): Promise { // Hard-code a single iframe for now. const IFRAME_ID = 'default'; const iframeManager = new IframeManager(); - const iframeReadyP = iframeManager.create({ id: IFRAME_ID }); + const iframeReadyP = iframeManager + .create({ id: IFRAME_ID }) + .then(async () => iframeManager.makeCapTp(IFRAME_ID)); // Handle messages from the background service worker chrome.runtime.onMessage.addListener( - makeHandledCallback(async (message: ExtensionMessage) => { - if (message.target !== 'offscreen') { + makeHandledCallback(async (message: ExtensionMessage) => { + if (message.target !== ExtensionMessageTarget.Offscreen) { console.warn( `Offscreen received message with unexpected target: "${message.target}"`, ); @@ -29,11 +32,21 @@ async function main(): Promise { case Command.Evaluate: await reply(Command.Evaluate, await evaluate(message.data)); break; + case Command.CapTpCall: { + const result = await iframeManager.callCapTp(IFRAME_ID, message.data); + await reply(Command.CapTpCall, JSON.stringify(result, null, 2)); + break; + } + case Command.CapTpInit: + await iframeManager.makeCapTp(IFRAME_ID); + await reply(Command.CapTpInit, '~~~ CapTP Initialized ~~~'); + break; case Command.Ping: await reply(Command.Ping, 'pong'); break; default: console.error( + // @ts-expect-error The type of `message` is `never`, but this could happen at runtime. // eslint-disable-next-line @typescript-eslint/restrict-template-expressions `Offscreen received unexpected message type: "${message.type}"`, ); @@ -50,7 +63,7 @@ async function main(): Promise { async function reply(type: Command, data?: string): Promise { await chrome.runtime.sendMessage({ data: data ?? null, - target: 'background', + target: ExtensionMessageTarget.Background, type, }); } diff --git a/packages/extension/src/shared.test.ts b/packages/extension/src/shared.test.ts index d10870d46..54c4ff8aa 100644 --- a/packages/extension/src/shared.test.ts +++ b/packages/extension/src/shared.test.ts @@ -2,34 +2,9 @@ import './endoify.js'; import { delay } from '@ocap/test-utils'; import { vi, describe, it, expect } from 'vitest'; -import { isWrappedIframeMessage, makeHandledCallback } from './shared.js'; +import { makeCounter, makeHandledCallback } from './shared.js'; describe('shared', () => { - describe('isWrappedIframeMessage', () => { - it('returns true for valid messages', () => { - expect( - isWrappedIframeMessage({ - id: '1', - message: { type: 'evaluate', data: '1 + 1' }, - }), - ).toBe(true); - }); - - it('returns false for invalid messages', () => { - const invalidMessages = [ - {}, - { id: '1' }, - { message: { type: 'evaluate' } }, - { id: '1', message: { type: 'evaluate' } }, - { id: '1', message: { type: 'evaluate', data: 1 } }, - ]; - - invalidMessages.forEach((message) => { - expect(isWrappedIframeMessage(message)).toBe(false); - }); - }); - }); - describe('makeHandledCallback', () => { it('returns a function', () => { const callback = makeHandledCallback(async () => Promise.resolve()); @@ -62,4 +37,25 @@ describe('shared', () => { ); }); }); + + describe('makeCounter', () => { + it('starts at 1 by default', () => { + const counter = makeCounter(); + expect(counter()).toBe(1); + }); + + it('starts counting from the supplied argument', () => { + const start = 50; + const counter = makeCounter(start); + expect(counter()).toStrictEqual(start + 1); + }); + + it('increments convincingly', () => { + const counter = makeCounter(); + const first = counter(); + expect(counter()).toStrictEqual(first + 1); + expect(counter()).toStrictEqual(first + 2); + expect(counter()).toStrictEqual(first + 3); + }); + }); }); diff --git a/packages/extension/src/shared.ts b/packages/extension/src/shared.ts index 99c82f6b9..c41aa7abc 100644 --- a/packages/extension/src/shared.ts +++ b/packages/extension/src/shared.ts @@ -1,40 +1,4 @@ -import { isObject } from '@metamask/utils'; - -export enum Command { - Evaluate = 'evaluate', - Ping = 'ping', -} - -export type ExtensionMessage< - Type extends Command, - Data extends null | string | unknown[] | Record, -> = { - type: Type; - target: 'background' | 'offscreen'; - data: Data; -}; - -export type IframeMessage< - Type extends Command, - Data extends null | string | unknown[] | Record, -> = { - type: Type; - data: Data; -}; - -export type WrappedIframeMessage = { - id: string; - message: IframeMessage; -}; - -export const isWrappedIframeMessage = ( - value: unknown, -): value is WrappedIframeMessage => - isObject(value) && - typeof value.id === 'string' && - isObject(value.message) && - typeof value.message.type === 'string' && - (typeof value.message.data === 'string' || value.message.data === null); +export type VatId = string; /** * Wrap an async callback to ensure any errors are at least logged. @@ -50,3 +14,17 @@ export const makeHandledCallback = ( callback(...args).catch(console.error); }; }; + +/** + * A simple counter which increments and returns when called. + * + * @param start - One less than the first returned number. + * @returns A counter. + */ +export const makeCounter = (start: number = 0) => { + let counter: number = start; + return () => { + counter += 1; + return counter; + }; +}; diff --git a/packages/streams/src/index.ts b/packages/streams/src/index.ts index 7fbe64309..aacd60335 100644 --- a/packages/streams/src/index.ts +++ b/packages/streams/src/index.ts @@ -2,7 +2,7 @@ export { initializeMessageChannel, receiveMessagePort, } from './message-channel.js'; -export type { MessagePortStreamPair } from './streams.js'; +export type { StreamPair, Reader, Writer } from './streams.js'; export { makeMessagePortStreamPair, MessagePortReader, diff --git a/packages/streams/src/streams.test.ts b/packages/streams/src/streams.test.ts index f2375fe25..a609a1578 100644 --- a/packages/streams/src/streams.test.ts +++ b/packages/streams/src/streams.test.ts @@ -313,7 +313,7 @@ describe('makeMessagePortStreamPair', () => { const { port1, port2 } = new MessageChannel(); const streamPair = makeMessagePortStreamPair(port1); const remoteReader = new MessagePortReader(port2); - const localReadP = streamPair.reader.next(); + const localReadP = (streamPair.reader as MessagePortReader).next(); const remoteReadP = remoteReader.next(); expect(port1.onmessage).toBeDefined(); diff --git a/packages/streams/src/streams.ts b/packages/streams/src/streams.ts index f7a13109a..3433e86aa 100644 --- a/packages/streams/src/streams.ts +++ b/packages/streams/src/streams.ts @@ -22,6 +22,8 @@ import { makePromiseKit } from '@endo/promise-kit'; import type { Reader, Writer } from '@endo/stream'; import { hasProperty, isObject } from '@metamask/utils'; +export type { Reader, Writer }; + type PromiseCallbacks = { resolve: (value: unknown) => void; reject: (reason: unknown) => void; @@ -318,9 +320,9 @@ export class MessagePortWriter implements Writer { } harden(MessagePortWriter); -export type MessagePortStreamPair = Readonly<{ - reader: MessagePortReader; - writer: MessagePortWriter; +export type StreamPair = Readonly<{ + reader: Reader; + writer: Writer; /** * Calls `.return()` on both streams. */ @@ -343,7 +345,7 @@ export type MessagePortStreamPair = Readonly<{ */ export const makeMessagePortStreamPair = ( port: MessagePort, -): MessagePortStreamPair => { +): StreamPair => { const reader = new MessagePortReader(port); const writer = new MessagePortWriter(port); diff --git a/yarn.lock b/yarn.lock index f788ec267..10c624eb7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -296,6 +296,19 @@ __metadata: languageName: node linkType: hard +"@endo/captp@npm:^4.2.2": + version: 4.3.0 + resolution: "@endo/captp@npm:4.3.0" + dependencies: + "@endo/errors": "npm:^1.2.5" + "@endo/eventual-send": "npm:^1.2.5" + "@endo/marshal": "npm:^1.5.3" + "@endo/nat": "npm:^5.0.10" + "@endo/promise-kit": "npm:^1.1.5" + checksum: 10/496ae0f7f46160680efb213e2043b33ce6347eb05d07d05dd70ca3ff873f2215a28d6bc346159ba8d9075d246a46ae4778ba5003490641ab53572c1a5fe9bcf0 + languageName: node + linkType: hard + "@endo/cjs-module-analyzer@npm:^1.0.6": version: 1.0.6 resolution: "@endo/cjs-module-analyzer@npm:1.0.6" @@ -303,6 +316,17 @@ __metadata: languageName: node linkType: hard +"@endo/common@npm:^1.2.5": + version: 1.2.5 + resolution: "@endo/common@npm:1.2.5" + dependencies: + "@endo/errors": "npm:^1.2.5" + "@endo/eventual-send": "npm:^1.2.5" + "@endo/promise-kit": "npm:^1.1.5" + checksum: 10/11c026e09d716bb3d8384a0090e6b938e05616d88f99ef126d8e3dc5f4e5f343fb93874d6bf5b1ca61d85048c4a9c8d9317ee6ee1a2adf249aa4da6f4d31635c + languageName: node + linkType: hard + "@endo/compartment-mapper@npm:^1.2.0": version: 1.2.1 resolution: "@endo/compartment-mapper@npm:1.2.1" @@ -315,10 +339,19 @@ __metadata: languageName: node linkType: hard -"@endo/env-options@npm:^1.1.5": - version: 1.1.5 - resolution: "@endo/env-options@npm:1.1.5" - checksum: 10/ce4cb29ecf387f52f7d1c9e7e43b0a1064326587ebac62e7c239bf2df71aa4c3296d2a05cf169d1efcd8c1ddf73aeede8afd86e7b5c9387b80e8e0939d1af0f6 +"@endo/env-options@npm:^1.1.6": + version: 1.1.6 + resolution: "@endo/env-options@npm:1.1.6" + checksum: 10/c16675a18e70caf92a2cc35bab40e8a34a761b3bd355ea480b4092d1e0e15dfdeec48246e470b5f4dfe40d2a140d07c7926dd8b76df5fcc294bc21ebb18d5d3d + languageName: node + linkType: hard + +"@endo/errors@npm:^1.2.5": + version: 1.2.5 + resolution: "@endo/errors@npm:1.2.5" + dependencies: + ses: "npm:^1.8.0" + checksum: 10/50ffbd939cf5d2b6388e9f362c92b0b0c4be3d0e14fa79c0a57d67adbf5b657f76a3999eb0b89a94528c5b96420f8981caf93a820790b3ae41cb02db58553af5 languageName: node linkType: hard @@ -334,12 +367,38 @@ __metadata: languageName: node linkType: hard -"@endo/eventual-send@npm:^1.2.2, @endo/eventual-send@npm:^1.2.3, @endo/eventual-send@npm:^1.2.4": - version: 1.2.4 - resolution: "@endo/eventual-send@npm:1.2.4" +"@endo/eventual-send@npm:^1.2.2, @endo/eventual-send@npm:^1.2.3, @endo/eventual-send@npm:^1.2.4, @endo/eventual-send@npm:^1.2.5": + version: 1.2.5 + resolution: "@endo/eventual-send@npm:1.2.5" + dependencies: + "@endo/env-options": "npm:^1.1.6" + checksum: 10/3d3c8eb48fc48a96f8ca8c96baeccd8a8cbc14d4dd64583e44034e0aab77be4a54ad80e87992d63a003c105889df5dbbd75f21271598f1ca6b0dfb36c2e081c8 + languageName: node + linkType: hard + +"@endo/exo@npm:^1.5.2": + version: 1.5.3 + resolution: "@endo/exo@npm:1.5.3" dependencies: - "@endo/env-options": "npm:^1.1.5" - checksum: 10/5b46f7987af609dd52e6c65fd828ca29e36cbf88128e435ccf9cadfb72457c4342d5b6b49a3dc977d2203cefc6c956d6aa0280e086c3bca6f4b1d7228c50810e + "@endo/common": "npm:^1.2.5" + "@endo/env-options": "npm:^1.1.6" + "@endo/errors": "npm:^1.2.5" + "@endo/eventual-send": "npm:^1.2.5" + "@endo/far": "npm:^1.1.5" + "@endo/pass-style": "npm:^1.4.3" + "@endo/patterns": "npm:^1.4.3" + checksum: 10/1106370db7efa55b790bad645321d31b4969cc1b8f98e2af80d93e64da3cdb7e2e52e855ccd1951f23606383716ac673333471705896addf3ae84b6a12880cae + languageName: node + linkType: hard + +"@endo/far@npm:^1.1.5": + version: 1.1.5 + resolution: "@endo/far@npm:1.1.5" + dependencies: + "@endo/errors": "npm:^1.2.5" + "@endo/eventual-send": "npm:^1.2.5" + "@endo/pass-style": "npm:^1.4.3" + checksum: 10/c420c13a204ec2891b60d13d7af428fb2969c098519430e991dafdeb7868343b0cc160e2b1b0872587f112c423638254d8c4ffefee20f2725f1d29b0dabbd8e7 languageName: node linkType: hard @@ -364,6 +423,20 @@ __metadata: languageName: node linkType: hard +"@endo/marshal@npm:^1.5.3": + version: 1.5.3 + resolution: "@endo/marshal@npm:1.5.3" + dependencies: + "@endo/common": "npm:^1.2.5" + "@endo/errors": "npm:^1.2.5" + "@endo/eventual-send": "npm:^1.2.5" + "@endo/nat": "npm:^5.0.10" + "@endo/pass-style": "npm:^1.4.3" + "@endo/promise-kit": "npm:^1.1.5" + checksum: 10/51e96383ce8de2ad834804f8154bb121482afe5739b22a84759b31240498b920f691667fb5a5cc5a49327a3f4ec7d0679e4e05d7f9be5b27acc8d761809d92aa + languageName: node + linkType: hard + "@endo/module-source@npm:^1.0.1": version: 1.0.1 resolution: "@endo/module-source@npm:1.0.1" @@ -377,12 +450,45 @@ __metadata: languageName: node linkType: hard -"@endo/promise-kit@npm:^1.1.2, @endo/promise-kit@npm:^1.1.3, @endo/promise-kit@npm:^1.1.4": - version: 1.1.4 - resolution: "@endo/promise-kit@npm:1.1.4" +"@endo/nat@npm:^5.0.10": + version: 5.0.10 + resolution: "@endo/nat@npm:5.0.10" + checksum: 10/4c582a96a96f3413de7945dc5be92eb3bc63692440a541ee42607ebdaaf6af3083777344d8e6b8056915d065a32485d00bc3b6047bc205af586d421c453be494 + languageName: node + linkType: hard + +"@endo/pass-style@npm:^1.4.3": + version: 1.4.3 + resolution: "@endo/pass-style@npm:1.4.3" dependencies: - ses: "npm:^1.7.0" - checksum: 10/794c38d2105597dfec999fb55e0865edd5ce24c86e42eb58561514b5ee68dedae423a7e79e9d75fa66447a88d7c4755d28f848ee841cc531c0ced9253b121fc6 + "@endo/env-options": "npm:^1.1.6" + "@endo/errors": "npm:^1.2.5" + "@endo/eventual-send": "npm:^1.2.5" + "@endo/promise-kit": "npm:^1.1.5" + "@fast-check/ava": "npm:^1.1.5" + checksum: 10/3358431007927cef4fad8a6c9d7f0035ab0d17a243867635cf5e279097803dc4e226acb44e221bf12faeca9799cd0db897435849b4eacdfce6c3c0c7ad9bbbd1 + languageName: node + linkType: hard + +"@endo/patterns@npm:^1.4.2, @endo/patterns@npm:^1.4.3": + version: 1.4.3 + resolution: "@endo/patterns@npm:1.4.3" + dependencies: + "@endo/common": "npm:^1.2.5" + "@endo/errors": "npm:^1.2.5" + "@endo/eventual-send": "npm:^1.2.5" + "@endo/marshal": "npm:^1.5.3" + "@endo/promise-kit": "npm:^1.1.5" + checksum: 10/34581909ecf1283ed4ab3179643db47ba0f32621b39879766f1053395903b1cac462fb81b32a81334c17a642cff17349b2f8d5f13c711555dee09f2c1bb444b3 + languageName: node + linkType: hard + +"@endo/promise-kit@npm:^1.1.2, @endo/promise-kit@npm:^1.1.3, @endo/promise-kit@npm:^1.1.4, @endo/promise-kit@npm:^1.1.5": + version: 1.1.5 + resolution: "@endo/promise-kit@npm:1.1.5" + dependencies: + ses: "npm:^1.8.0" + checksum: 10/a0483335f36c0614066906afd246e98e185dfe1079cc949b2031db5fdf727096ca0cccb21e294ba2bff3a82b54c8ff4e53b6e13506b325c56774374d8b90eb5b languageName: node linkType: hard @@ -667,6 +773,17 @@ __metadata: languageName: node linkType: hard +"@fast-check/ava@npm:^1.1.5": + version: 1.2.1 + resolution: "@fast-check/ava@npm:1.2.1" + dependencies: + fast-check: "npm:^3.0.0" + peerDependencies: + ava: ^4 || ^5 || ^6 + checksum: 10/816ac43e5fb0c2a101bc7e2307f67b68ede4dd64d029cecc78795113580c90f4120ccc3ea6931ad757c465dff06a79c24f45cbe399b8ab6d74494d72f9f19736 + languageName: node + linkType: hard + "@humanwhocodes/config-array@npm:^0.11.14": version: 0.11.14 resolution: "@humanwhocodes/config-array@npm:0.11.14" @@ -1251,7 +1368,10 @@ __metadata: resolution: "@ocap/extension@workspace:packages/extension" dependencies: "@arethetypeswrong/cli": "npm:^0.15.3" + "@endo/captp": "npm:^4.2.2" "@endo/eventual-send": "npm:^1.2.4" + "@endo/exo": "npm:^1.5.2" + "@endo/patterns": "npm:^1.4.2" "@endo/promise-kit": "npm:^1.1.4" "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/eslint-config": "npm:^13.0.0" @@ -3663,6 +3783,15 @@ __metadata: languageName: node linkType: hard +"fast-check@npm:^3.0.0": + version: 3.22.0 + resolution: "fast-check@npm:3.22.0" + dependencies: + pure-rand: "npm:^6.1.0" + checksum: 10/26ae7cc228fcd9759124db10cbbc01efff730bcdc848544ec7c3a533b9d88dec88d2a4a79da0ea4eb1ec78611dc6576f06f3fa5f8ff7126ad2eecf5ce3da57c6 + languageName: node + linkType: hard + "fast-deep-equal@npm:^3.1.1, fast-deep-equal@npm:^3.1.3": version: 3.1.3 resolution: "fast-deep-equal@npm:3.1.3" @@ -5649,6 +5778,13 @@ __metadata: languageName: node linkType: hard +"pure-rand@npm:^6.1.0": + version: 6.1.0 + resolution: "pure-rand@npm:6.1.0" + checksum: 10/256aa4bcaf9297256f552914e03cbdb0039c8fe1db11fa1e6d3f80790e16e563eb0a859a1e61082a95e224fc0c608661839439f8ecc6a3db4e48d46d99216ee4 + languageName: node + linkType: hard + "querystringify@npm:^2.1.1": version: 2.2.0 resolution: "querystringify@npm:2.2.0" @@ -5973,12 +6109,12 @@ __metadata: languageName: node linkType: hard -"ses@npm:^1.1.0, ses@npm:^1.5.0, ses@npm:^1.7.0": - version: 1.7.0 - resolution: "ses@npm:1.7.0" +"ses@npm:^1.1.0, ses@npm:^1.5.0, ses@npm:^1.7.0, ses@npm:^1.8.0": + version: 1.8.0 + resolution: "ses@npm:1.8.0" dependencies: - "@endo/env-options": "npm:^1.1.5" - checksum: 10/8d1227fadcd06653d1b49083c067ae07e55164af984c9e8b393238fbbd315f47216472e3ac65a78638955f3f1a2537e9c9865f0ab142639a6862b902cb1cf6f2 + "@endo/env-options": "npm:^1.1.6" + checksum: 10/ce1cb7f85147ce8c83f63b6d7cfb0a38bc4ca31a85fe9c7d86547595fb2ea0503ab1bacf7c9eb1dd5c7796638d4fbac51608a8f4493d71b56a584262837819ba languageName: node linkType: hard