diff --git a/packages/extension/package.json b/packages/extension/package.json index 7bfe5ec19..58df43657 100644 --- a/packages/extension/package.json +++ b/packages/extension/package.json @@ -41,6 +41,7 @@ "@ocap/kernel": "workspace:^", "@ocap/shims": "workspace:^", "@ocap/streams": "workspace:^", + "@ocap/utils": "workspace:^", "ses": "^1.7.0" }, "devDependencies": { diff --git a/packages/extension/src/background.ts b/packages/extension/src/background.ts index 2e2704b34..40345b842 100644 --- a/packages/extension/src/background.ts +++ b/packages/extension/src/background.ts @@ -1,27 +1,31 @@ import type { Json } from '@metamask/utils'; import './background-trusted-prelude.js'; -import type { KernelMessage } from '@ocap/streams'; -import { Command, KernelMessageTarget } from '@ocap/streams'; +import { CommandMethod } from '@ocap/utils'; -import { makeHandledCallback } from './shared.js'; +import { + ExtensionMessageTarget, + isExtensionRuntimeMessage, + makeHandledCallback, +} from './shared.js'; // globalThis.kernel will exist due to dev-console.js in background-trusted-prelude.js Object.defineProperties(globalThis.kernel, { capTpCall: { value: async (method: string, params: Json[]) => - sendMessage(Command.CapTpCall, { method, params }), + sendCommand(CommandMethod.CapTpCall, { method, params }), }, capTpInit: { - value: async () => sendMessage(Command.CapTpInit), + value: async () => sendCommand(CommandMethod.CapTpInit), }, evaluate: { - value: async (source: string) => sendMessage(Command.Evaluate, source), + value: async (source: string) => + sendCommand(CommandMethod.Evaluate, source), }, ping: { - value: async () => sendMessage(Command.Ping), + value: async () => sendCommand(CommandMethod.Ping), }, sendMessage: { - value: sendMessage, + value: sendCommand, }, }); harden(globalThis.kernel); @@ -30,23 +34,25 @@ const OFFSCREEN_DOCUMENT_PATH = '/offscreen.html'; // With this we can click the extension action button to wake up the service worker. chrome.action.onClicked.addListener(() => { - sendMessage(Command.Ping).catch(console.error); + sendCommand(CommandMethod.Ping).catch(console.error); }); /** * Send a message to the offscreen document. * - * @param type - The message type. - * @param data - The message data. - * @param data.name - The name to include in the message. + * @param method - The message type. + * @param params - The message data. + * @param params.name - The name to include in the message. */ -async function sendMessage(type: string, data?: Json): Promise { +async function sendCommand(method: string, params?: Json): Promise { await provideOffScreenDocument(); await chrome.runtime.sendMessage({ - type, - target: KernelMessageTarget.Offscreen, - data: data ?? null, + target: ExtensionMessageTarget.Offscreen, + payload: { + method, + params: params ?? null, + }, }); } @@ -65,26 +71,32 @@ async function provideOffScreenDocument(): Promise { // Handle replies from the offscreen document chrome.runtime.onMessage.addListener( - makeHandledCallback(async (message: KernelMessage) => { - if (message.target !== KernelMessageTarget.Background) { + makeHandledCallback(async (message: unknown) => { + if (!isExtensionRuntimeMessage(message)) { + console.error('Background received unexpected message', message); + return; + } + if (message.target !== ExtensionMessageTarget.Background) { console.warn( `Background received message with unexpected target: "${message.target}"`, ); return; } - switch (message.type) { - case Command.Evaluate: - case Command.CapTpCall: - case Command.CapTpInit: - case Command.Ping: - console.log(message.data); + const { payload } = message; + + switch (payload.method) { + case CommandMethod.Evaluate: + case CommandMethod.CapTpCall: + case CommandMethod.CapTpInit: + case CommandMethod.Ping: + console.log(payload.params); 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}"`, + `Background received unexpected command method: "${payload.method}"`, ); } }), diff --git a/packages/extension/src/iframe.ts b/packages/extension/src/iframe.ts index fe23a7516..629b91bb3 100644 --- a/packages/extension/src/iframe.ts +++ b/packages/extension/src/iframe.ts @@ -1,20 +1,19 @@ import { makeCapTP } from '@endo/captp'; import { makeExo } from '@endo/exo'; import { M } from '@endo/patterns'; +import { receiveMessagePort, makeMessagePortStreamPair } from '@ocap/streams'; import type { StreamEnvelope, CapTpMessage, + Command, VatMessage, - WrappedVatMessage, -} from '@ocap/streams'; +} from '@ocap/utils'; import { - receiveMessagePort, - makeMessagePortStreamPair, + CommandMethod, makeStreamEnvelopeHandler, - Command, wrapCapTp, wrapStreamCommand, -} from '@ocap/streams'; +} from '@ocap/utils'; const defaultCompartment = new Compartment({ URL }); @@ -49,32 +48,29 @@ async function main(): Promise { /** * 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. + * @param vatMessage - The vat message to handle. + * @param vatMessage.id - The id of the message. + * @param vatMessage.payload - The payload to handle. */ - async function handleMessage({ - id, - message, - }: WrappedVatMessage): Promise { - switch (message.type) { - case Command.Evaluate: { - if (typeof message.data !== 'string') { + async function handleMessage({ id, payload }: VatMessage): Promise { + switch (payload.method) { + case CommandMethod.Evaluate: { + if (typeof payload.params !== 'string') { console.error( - 'iframe received message with unexpected data type', + 'iframe received command with unexpected params', // @ts-expect-error The type of `message.data` is `never`, but this could happen at runtime. - stringifyResult(message.data), + stringifyResult(payload.params), ); return; } - const result = safelyEvaluate(message.data); + const result = safelyEvaluate(payload.params); await replyToMessage(id, { - type: Command.Evaluate, - data: stringifyResult(result), + method: CommandMethod.Evaluate, + params: stringifyResult(result), }); break; } - case Command.CapTpInit: { + case CommandMethod.CapTpInit: { const bootstrap = makeExo( 'TheGreatFrangooly', M.interface('TheGreatFrangooly', {}, { defaultGuards: 'passable' }), @@ -87,16 +83,22 @@ async function main(): Promise { streams.writer.next(wrapCapTp(content as CapTpMessage)), bootstrap, ); - await replyToMessage(id, { type: Command.CapTpInit, data: null }); + await replyToMessage(id, { + method: CommandMethod.CapTpInit, + params: null, + }); break; } - case Command.Ping: - await replyToMessage(id, { type: Command.Ping, data: 'pong' }); + case CommandMethod.Ping: + await replyToMessage(id, { + method: CommandMethod.Ping, + params: 'pong', + }); break; default: console.error( // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - `iframe received unexpected message type: "${message.type}"`, + `iframe received unexpected command method: "${payload.method}"`, ); } } @@ -105,13 +107,10 @@ async function main(): Promise { * Reply to a message from the parent window. * * @param id - The id of the message to reply to. - * @param message - The message to reply with. + * @param payload - The payload to reply with. */ - async function replyToMessage( - id: string, - message: VatMessage, - ): Promise { - await streams.writer.next(wrapStreamCommand({ id, message })); + async function replyToMessage(id: string, payload: Command): Promise { + await streams.writer.next(wrapStreamCommand({ id, payload })); } /** diff --git a/packages/extension/src/makeIframeVatWorker.ts b/packages/extension/src/makeIframeVatWorker.ts index 3282a2c22..0d6ac1a2c 100644 --- a/packages/extension/src/makeIframeVatWorker.ts +++ b/packages/extension/src/makeIframeVatWorker.ts @@ -1,7 +1,8 @@ import { createWindow } from '@metamask/snaps-utils'; import type { VatId, VatWorker } from '@ocap/kernel'; -import type { initializeMessageChannel, StreamEnvelope } from '@ocap/streams'; +import type { initializeMessageChannel } from '@ocap/streams'; import { makeMessagePortStreamPair } from '@ocap/streams'; +import type { StreamEnvelope } from '@ocap/utils'; const IFRAME_URI = 'iframe.html'; diff --git a/packages/extension/src/offscreen.ts b/packages/extension/src/offscreen.ts index e904a2d79..4f06e41a6 100644 --- a/packages/extension/src/offscreen.ts +++ b/packages/extension/src/offscreen.ts @@ -1,13 +1,13 @@ import { Kernel } from '@ocap/kernel'; -import { - initializeMessageChannel, - Command, - KernelMessageTarget, -} from '@ocap/streams'; -import type { KernelMessage } from '@ocap/streams'; +import { initializeMessageChannel } from '@ocap/streams'; +import { CommandMethod } from '@ocap/utils'; import { makeIframeVatWorker } from './makeIframeVatWorker.js'; -import { makeHandledCallback } from './shared.js'; +import { + ExtensionMessageTarget, + isExtensionRuntimeMessage, + makeHandledCallback, +} from './shared.js'; main().catch(console.error); @@ -23,9 +23,13 @@ async function main(): Promise { // Handle messages from the background service worker chrome.runtime.onMessage.addListener( - makeHandledCallback(async (message: KernelMessage) => { - if (message.target !== KernelMessageTarget.Offscreen) { - console.warn( + makeHandledCallback(async (message: unknown) => { + if (!isExtensionRuntimeMessage(message)) { + console.error('Offscreen received unexpected message', message); + return; + } + if (message.target !== ExtensionMessageTarget.Offscreen) { + console.error( `Offscreen received message with unexpected target: "${message.target}"`, ); return; @@ -33,43 +37,59 @@ async function main(): Promise { const vat = await iframeReadyP; - switch (message.type) { - case Command.Evaluate: - await reply(Command.Evaluate, await evaluate(vat.id, message.data)); + const { payload } = message; + + switch (payload.method) { + case CommandMethod.Evaluate: + await replyToCommand( + CommandMethod.Evaluate, + await evaluate(vat.id, payload.params), + ); break; - case Command.CapTpCall: { - const result = await vat.callCapTp(message.data); - await reply(Command.CapTpCall, JSON.stringify(result, null, 2)); + case CommandMethod.CapTpCall: { + const result = await vat.callCapTp(payload.params); + await replyToCommand( + CommandMethod.CapTpCall, + JSON.stringify(result, null, 2), + ); break; } - case Command.CapTpInit: + case CommandMethod.CapTpInit: await vat.makeCapTp(); - await reply(Command.CapTpInit, '~~~ CapTP Initialized ~~~'); + await replyToCommand( + CommandMethod.CapTpInit, + '~~~ CapTP Initialized ~~~', + ); break; - case Command.Ping: - await reply(Command.Ping, 'pong'); + case CommandMethod.Ping: + await replyToCommand(CommandMethod.Ping, 'pong'); break; default: console.error( - // @ts-expect-error The type of `message` is `never`, but this could happen at runtime. + // @ts-expect-error The type of `payload` is `never`, but this could happen at runtime. // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - `Offscreen received unexpected message type: "${message.type}"`, + `Offscreen received unexpected command method: "${payload.method}"`, ); } }), ); /** - * Reply to the background script. + * Reply to a command from the background script. * - * @param type - The message type. - * @param data - The message data. + * @param method - The command method. + * @param params - The command parameters. */ - async function reply(type: Command, data?: string): Promise { + async function replyToCommand( + method: CommandMethod, + params?: string, + ): Promise { await chrome.runtime.sendMessage({ - data: data ?? null, - target: KernelMessageTarget.Background, - type, + target: ExtensionMessageTarget.Background, + payload: { + method, + params: params ?? null, + }, }); } @@ -83,8 +103,8 @@ async function main(): Promise { async function evaluate(vatId: string, source: string): Promise { try { const result = await kernel.sendMessage(vatId, { - type: Command.Evaluate, - data: source, + method: CommandMethod.Evaluate, + params: source, }); return String(result); } catch (error) { diff --git a/packages/extension/src/shared.ts b/packages/extension/src/shared.ts index 2bc7a4e10..6e1758ded 100644 --- a/packages/extension/src/shared.ts +++ b/packages/extension/src/shared.ts @@ -1,5 +1,29 @@ +import { isObject } from '@metamask/utils'; +import type { Command } from '@ocap/utils'; +import { isCommand } from '@ocap/utils'; + export type VatId = string; +export enum ExtensionMessageTarget { + Background = 'background', + Offscreen = 'offscreen', +} + +export type ExtensionRuntimeMessage = { + payload: Command; + target: ExtensionMessageTarget; +}; + +export const isExtensionRuntimeMessage = ( + message: unknown, +): message is ExtensionRuntimeMessage => + isObject(message) && + typeof message.target === 'string' && + Object.values(ExtensionMessageTarget).includes( + message.target as ExtensionMessageTarget, + ) && + isCommand(message.payload); + /** * Wrap an async callback to ensure any errors are at least logged. * diff --git a/packages/extension/tsconfig.build.json b/packages/extension/tsconfig.build.json index 9ce11ecb2..68addf9a0 100644 --- a/packages/extension/tsconfig.build.json +++ b/packages/extension/tsconfig.build.json @@ -11,7 +11,8 @@ }, "references": [ { "path": "../shims/tsconfig.build.json" }, - { "path": "../streams/tsconfig.build.json" } + { "path": "../streams/tsconfig.build.json" }, + { "path": "../utils/tsconfig.build.json" } ], "include": [ "./src/**/*.ts", diff --git a/packages/extension/tsconfig.json b/packages/extension/tsconfig.json index 449869536..903959299 100644 --- a/packages/extension/tsconfig.json +++ b/packages/extension/tsconfig.json @@ -10,9 +10,10 @@ "types": ["chrome", "ses", "vitest", "vitest/jsdom"] }, "references": [ + { "path": "../shims" }, { "path": "../streams" }, { "path": "../test-utils" }, - { "path": "../shims" } + { "path": "../utils" } ], "include": [ "./src/**/*.ts", diff --git a/packages/kernel/package.json b/packages/kernel/package.json index 32d2d4245..b847ce45b 100644 --- a/packages/kernel/package.json +++ b/packages/kernel/package.json @@ -47,6 +47,7 @@ "@endo/promise-kit": "^1.1.4", "@ocap/shims": "workspace:^", "@ocap/streams": "workspace:^", + "@ocap/utils": "workspace:^", "ses": "^1.7.0" }, "devDependencies": { diff --git a/packages/kernel/src/Kernel.test.ts b/packages/kernel/src/Kernel.test.ts index 36971c827..7bf30df17 100644 --- a/packages/kernel/src/Kernel.test.ts +++ b/packages/kernel/src/Kernel.test.ts @@ -1,4 +1,4 @@ -import type { VatMessage } from '@ocap/streams'; +import type { Command } from '@ocap/utils'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import { Kernel } from './Kernel.js'; @@ -102,14 +102,14 @@ describe('Kernel', () => { await kernel.launchVat({ id: 'vat-id', worker: mockWorker }); vi.spyOn(Vat.prototype, 'sendMessage').mockResolvedValueOnce('test'); expect( - await kernel.sendMessage('vat-id', 'test' as unknown as VatMessage), + await kernel.sendMessage('vat-id', 'test' as unknown as Command), ).toBe('test'); }); it('throws an error when sending a message to the vat that does not exist in the kernel', async () => { const kernel = new Kernel(); await expect(async () => - kernel.sendMessage('non-existent-vat-id', {} as VatMessage), + kernel.sendMessage('non-existent-vat-id', {} as Command), ).rejects.toThrow('Vat with ID non-existent-vat-id does not exist.'); }); @@ -118,7 +118,7 @@ describe('Kernel', () => { await kernel.launchVat({ id: 'vat-id', worker: mockWorker }); vi.spyOn(Vat.prototype, 'sendMessage').mockRejectedValueOnce('error'); await expect(async () => - kernel.sendMessage('vat-id', {} as VatMessage), + kernel.sendMessage('vat-id', {} as Command), ).rejects.toThrow('error'); }); }); diff --git a/packages/kernel/src/Kernel.ts b/packages/kernel/src/Kernel.ts index 0d540919e..2875ab707 100644 --- a/packages/kernel/src/Kernel.ts +++ b/packages/kernel/src/Kernel.ts @@ -1,5 +1,5 @@ import '@ocap/shims/endoify'; -import type { VatMessage } from '@ocap/streams'; +import type { Command } from '@ocap/utils'; import type { VatId, VatWorker } from './types.js'; import { Vat } from './Vat.js'; @@ -62,12 +62,12 @@ export class Kernel { * Send a message to a vat. * * @param id - The id of the vat to send the message to. - * @param message - The message to send. + * @param command - The command to send. * @returns A promise that resolves the response to the message. */ - async sendMessage(id: VatId, message: VatMessage): Promise { + async sendMessage(id: VatId, command: Command): Promise { const { vat } = this.#getVatRecord(id); - return vat.sendMessage(message); + return vat.sendMessage(command); } /** diff --git a/packages/kernel/src/Vat.test.ts b/packages/kernel/src/Vat.test.ts index 867d69766..b6599a88c 100644 --- a/packages/kernel/src/Vat.test.ts +++ b/packages/kernel/src/Vat.test.ts @@ -1,11 +1,12 @@ import '@ocap/shims/endoify'; +import { makeMessagePortStreamPair } from '@ocap/streams'; +import { makeCapTpMock, makePromiseKitMock } from '@ocap/test-utils'; import { - makeMessagePortStreamPair, + CommandMethod, makeStreamEnvelopeHandler, - Command, -} from '@ocap/streams'; -import type { StreamEnvelope, VatMessage } from '@ocap/streams'; -import { makeCapTpMock, makePromiseKitMock } from '@ocap/test-utils'; + type Command, + type StreamEnvelope, +} from '@ocap/utils'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import { Vat } from './Vat.js'; @@ -42,8 +43,8 @@ describe('Vat', () => { await vat.init(); expect(sendMessageMock).toHaveBeenCalledWith({ - type: Command.Ping, - data: null, + method: CommandMethod.Ping, + params: null, }); expect(capTpMock).toHaveBeenCalled(); }); @@ -51,7 +52,7 @@ describe('Vat', () => { describe('sendMessage', () => { it('sends a message and resolves the promise', async () => { - const mockMessage = { type: 'makeCapTp', data: null } as VatMessage; + const mockMessage = { method: 'makeCapTp', params: null } as Command; const sendMessagePromise = vat.sendMessage(mockMessage); vat.unresolvedMessages.get('test-vat-1')?.resolve('test-response'); const result = await sendMessagePromise; @@ -87,8 +88,8 @@ describe('Vat', () => { await vat.makeCapTp(); expect(vat.streamEnvelopeHandler.contentHandlers.capTp).toBeDefined(); expect(sendMessageMock).toHaveBeenCalledWith({ - type: Command.CapTpInit, - data: null, + method: CommandMethod.CapTpInit, + params: null, }); }); }); diff --git a/packages/kernel/src/Vat.ts b/packages/kernel/src/Vat.ts index ae2439028..a751ae1c0 100644 --- a/packages/kernel/src/Vat.ts +++ b/packages/kernel/src/Vat.ts @@ -1,24 +1,22 @@ import { makeCapTP } from '@endo/captp'; import { E } from '@endo/eventual-send'; import { makePromiseKit } from '@endo/promise-kit'; +import type { StreamPair, Reader } from '@ocap/streams'; import type { - StreamPair, StreamEnvelope, StreamEnvelopeHandler, - Reader, CapTpMessage, CapTpPayload, - VatMessage, - MessageId, -} from '@ocap/streams'; + Command, +} from '@ocap/utils'; import { wrapCapTp, wrapStreamCommand, - Command, makeStreamEnvelopeHandler, -} from '@ocap/streams'; + CommandMethod, +} from '@ocap/utils'; -import type { UnresolvedMessages, VatId } from './types.js'; +import type { MessageId, UnresolvedMessages, VatId } from './types.js'; import { makeCounter } from './utils/makeCounter.js'; type VatConstructorProps = { @@ -45,13 +43,19 @@ export class Vat { this.#messageCounter = makeCounter(); this.streamEnvelopeHandler = makeStreamEnvelopeHandler( { - command: async ({ id: messageId, message }) => { + command: async ({ + id: messageId, + payload, + }: { + id: MessageId; + payload: Command; + }) => { const promiseCallbacks = this.unresolvedMessages.get(messageId); if (promiseCallbacks === undefined) { console.error(`No unresolved message with id "${messageId}".`); } else { this.unresolvedMessages.delete(messageId); - promiseCallbacks.resolve(message.data); + promiseCallbacks.resolve(payload.params); } }, }, @@ -71,7 +75,7 @@ export class Vat { throw error; }); - await this.sendMessage({ type: Command.Ping, data: null }); + await this.sendMessage({ method: CommandMethod.Ping, params: null }); console.debug(`Created vat with id "${this.id}"`); return await this.makeCapTp(); @@ -116,7 +120,7 @@ export class Vat { ctp.dispatch(content); }; - return this.sendMessage({ type: Command.CapTpInit, data: null }); + return this.sendMessage({ method: CommandMethod.CapTpInit, params: null }); } /** @@ -150,16 +154,16 @@ export class Vat { /** * Send a message to a vat. * - * @param message - The message to send. + * @param payload - The message to send. * @returns A promise that resolves the response to the message. */ - async sendMessage(message: VatMessage): Promise { - console.debug(`Sending message to vat "${this.id}"`, message); + async sendMessage(payload: Command): Promise { + console.debug(`Sending message to vat "${this.id}"`, payload); const { promise, reject, resolve } = makePromiseKit(); const messageId = this.#nextMessageId(); this.unresolvedMessages.set(messageId, { reject, resolve }); await this.streams.writer.next( - wrapStreamCommand({ id: messageId, message }), + wrapStreamCommand({ id: messageId, payload }), ); return promise; } diff --git a/packages/kernel/src/types.ts b/packages/kernel/src/types.ts index 5b8d304f6..58aa20b18 100644 --- a/packages/kernel/src/types.ts +++ b/packages/kernel/src/types.ts @@ -1,5 +1,8 @@ import type { PromiseKit } from '@endo/promise-kit'; -import type { StreamPair, MessageId, StreamEnvelope } from '@ocap/streams'; +import type { StreamPair } from '@ocap/streams'; +import type { StreamEnvelope } from '@ocap/utils'; + +export type MessageId = string; export type VatId = string; diff --git a/packages/kernel/tsconfig.build.json b/packages/kernel/tsconfig.build.json index a406f5f5a..560397410 100644 --- a/packages/kernel/tsconfig.build.json +++ b/packages/kernel/tsconfig.build.json @@ -7,6 +7,6 @@ "lib": ["DOM", "ES2022"], "types": ["ses"] }, - "references": [], + "references": [{ "path": "../utils/tsconfig.build.json" }], "include": ["./src"] } diff --git a/packages/kernel/tsconfig.json b/packages/kernel/tsconfig.json index 6f1d89de4..fbcad7aa9 100644 --- a/packages/kernel/tsconfig.json +++ b/packages/kernel/tsconfig.json @@ -3,6 +3,6 @@ "compilerOptions": { "baseUrl": "./" }, - "references": [], + "references": [{ "path": "../utils" }], "include": ["./src"] } diff --git a/packages/streams/package.json b/packages/streams/package.json index 6ba009be2..d2e4b164a 100644 --- a/packages/streams/package.json +++ b/packages/streams/package.json @@ -46,7 +46,6 @@ "test:watch": "vitest --config vitest.config.ts" }, "dependencies": { - "@endo/captp": "^4.2.2", "@endo/promise-kit": "^1.1.4", "@endo/stream": "^1.2.2", "@metamask/utils": "^9.1.0" diff --git a/packages/streams/src/index.test.ts b/packages/streams/src/index.test.ts index 625957abd..bb4d6ca0a 100644 --- a/packages/streams/src/index.test.ts +++ b/packages/streams/src/index.test.ts @@ -9,12 +9,6 @@ describe('index', () => { 'initializeMessageChannel', 'receiveMessagePort', 'makeMessagePortStreamPair', - 'makeStreamEnvelopeKit', - 'KernelMessageTarget', - 'Command', - 'wrapStreamCommand', - 'wrapCapTp', - 'makeStreamEnvelopeHandler', ]), ); }); diff --git a/packages/streams/src/index.ts b/packages/streams/src/index.ts index b794edc38..321406b0b 100644 --- a/packages/streams/src/index.ts +++ b/packages/streams/src/index.ts @@ -5,19 +5,10 @@ export { export type { StreamPair, Reader, Writer } from './streams.js'; export { makeMessagePortStreamPair } from './streams.js'; export { makeStreamEnvelopeKit } from './envelope-kit.js'; +export type { StreamEnveloper } from './enveloper.js'; +export type { Envelope } from './envelope.js'; +export type { StreamEnvelopeHandler } from './envelope-handler.js'; export type { - CapTpMessage, - CapTpPayload, - MessageId, - VatMessage, - KernelMessage, - WrappedVatMessage, -} from './types.js'; -export { KernelMessageTarget, Command } from './types.js'; -export { - wrapStreamCommand, - wrapCapTp, - makeStreamEnvelopeHandler, - type StreamEnvelope, - type StreamEnvelopeHandler, -} from './stream-envelope.js'; + MakeStreamEnvelopeHandler, + StreamEnvelopeKit, +} from './envelope-kit.js'; diff --git a/packages/streams/src/type-guards.test.ts b/packages/streams/src/type-guards.test.ts deleted file mode 100644 index 96175bde1..000000000 --- a/packages/streams/src/type-guards.test.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { describe, it, expect } from 'vitest'; - -import { isWrappedVatMessage, isCapTpMessage } from './type-guards.js'; -import { Command } from './types.js'; - -describe('type-guards', () => { - describe('isWrappedVatMessage', () => { - it.each` - value | expectedResult | description - ${{ id: 'some-id', message: { type: Command.Ping, data: null } }} | ${true} | ${'valid wrapped vat message'} - ${123} | ${false} | ${'invalid wrapped vat message: primitive number'} - ${{ id: true, message: {} }} | ${false} | ${'invalid wrapped vat message: invalid id and empty message'} - ${{ id: 'some-id', message: null }} | ${false} | ${'invalid wrapped vat message: message is null'} - ${{ id: 123, message: { type: Command.Ping, data: null } }} | ${false} | ${'invalid wrapped vat message: invalid id type'} - ${{ id: 'some-id' }} | ${false} | ${'invalid wrapped vat message: missing message'} - ${{ id: 'some-id', message: 123 }} | ${false} | ${'invalid wrapped vat message: message is a primitive number'} - ${{ id: 'some-id', message: { type: 123, data: null } }} | ${false} | ${'invalid wrapped vat message: invalid type in message'} - `( - 'returns $expectedResult for $description', - ({ value, expectedResult }) => { - expect(isWrappedVatMessage(value)).toBe(expectedResult); - }, - ); - }); - - describe('isCapTpMessage', () => { - it.each` - value | expectedResult | description - ${{ type: 'CTP_some-type', epoch: 123 }} | ${true} | ${'valid cap tp message'} - ${{ type: true, epoch: null }} | ${false} | ${'invalid cap tp message: invalid type and epoch'} - ${{ type: 'some-type' }} | ${false} | ${'invalid cap tp message: missing epoch'} - ${{ type: 123, epoch: null }} | ${false} | ${'invalid cap tp message: invalid type'} - ${{ type: 'CTP_some-type' }} | ${false} | ${'invalid cap tp message: missing epoch'} - ${{ type: 'CTP_some-type', epoch: true }} | ${false} | ${'invalid cap tp message: invalid epoch type'} - `( - 'returns $expectedResult for $description', - ({ value, expectedResult }) => { - expect(isCapTpMessage(value)).toBe(expectedResult); - }, - ); - }); -}); diff --git a/packages/streams/src/type-guards.ts b/packages/streams/src/type-guards.ts deleted file mode 100644 index cec6426df..000000000 --- a/packages/streams/src/type-guards.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { isObject } from '@metamask/utils'; - -import type { CapTpMessage, WrappedVatMessage } from './types.js'; - -export const isWrappedVatMessage = ( - value: unknown, -): value is WrappedVatMessage => - isObject(value) && - typeof value.id === 'string' && - isObject(value.message) && - typeof value.message.type === 'string' && - (typeof value.message.data === 'string' || value.message.data === null); - -export const isCapTpMessage = (value: unknown): value is CapTpMessage => - isObject(value) && - typeof value.type === 'string' && - value.type.startsWith('CTP_') && - typeof value.epoch === 'number'; diff --git a/packages/streams/src/types.ts b/packages/streams/src/types.ts deleted file mode 100644 index 18ec5d4c7..000000000 --- a/packages/streams/src/types.ts +++ /dev/null @@ -1,58 +0,0 @@ -import type { Primitive } from '@endo/captp'; - -export type MessageId = string; - -export enum KernelMessageTarget { - Background = 'background', - Offscreen = 'offscreen', - WebWorker = 'webWorker', - Node = 'node', -} - -export type DataObject = - | Primitive - | Promise - | DataObject[] - | { [key: string]: DataObject }; - -type CommandLike< - CommandType extends Command, - Data extends DataObject, - TargetType extends KernelMessageTarget, -> = { - 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 = - | CommandLike - | CommandLike - | CommandLike - | CommandLike; - -export type KernelMessage = CommandMessage; -export type VatMessage = CommandMessage; - -export type WrappedVatMessage = { - id: MessageId; - message: VatMessage; -}; - -export type CapTpMessage = { - type: Type; - epoch: number; - [key: string]: unknown; -}; diff --git a/packages/utils/.eslintrc.cjs b/packages/utils/.eslintrc.cjs new file mode 100644 index 000000000..165e7042e --- /dev/null +++ b/packages/utils/.eslintrc.cjs @@ -0,0 +1,3 @@ +module.exports = { + extends: ['../../.eslintrc.cjs'], +}; diff --git a/packages/utils/CHANGELOG.md b/packages/utils/CHANGELOG.md new file mode 100644 index 000000000..0c82cb1ed --- /dev/null +++ b/packages/utils/CHANGELOG.md @@ -0,0 +1,10 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +[Unreleased]: https://github.com/MetaMask/ocap-kernel/ diff --git a/packages/utils/README.md b/packages/utils/README.md new file mode 100644 index 000000000..17ed83e69 --- /dev/null +++ b/packages/utils/README.md @@ -0,0 +1,7 @@ +# `utils` + +A kitchen drawer of Ocap Kernel utilities. + +## Contributing + +This package is part of a monorepo. Instructions for contributing can be found in the [monorepo README](https://github.com/MetaMask/ocap-kernel#readme). diff --git a/packages/utils/package.json b/packages/utils/package.json new file mode 100644 index 000000000..d4069513a --- /dev/null +++ b/packages/utils/package.json @@ -0,0 +1,80 @@ +{ + "name": "@ocap/utils", + "version": "0.0.0", + "private": true, + "description": "A kitchen drawer of Ocap Kernel utilities", + "repository": { + "type": "git", + "url": "https://github.com/MetaMask/ocap-kernel.git" + }, + "type": "module", + "exports": { + ".": { + "import": { + "types": "./dist/index.d.mts", + "default": "./dist/index.mjs" + }, + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + } + }, + "./package.json": "./package.json" + }, + "files": [ + "dist/" + ], + "scripts": { + "build": "ts-bridge --project tsconfig.build.json --clean", + "build:docs": "typedoc", + "changelog:validate": "../../scripts/validate-changelog.sh utils", + "clean": "rimraf --glob ./dist './*.tsbuildinfo'", + "lint": "yarn lint:eslint && yarn lint:misc --check && yarn constraints && yarn lint:dependencies", + "lint:dependencies": "depcheck", + "lint:eslint": "eslint . --cache --ext js,mjs,cjs,ts,mts,cts", + "lint:fix": "yarn constraints --fix && yarn lint:eslint --fix && yarn lint:misc --write", + "lint:misc": "prettier --no-error-on-unmatched-pattern '**/*.json' '**/*.md' '**/*.html' '!**/CHANGELOG.old.md' '**/*.yml' '!.yarnrc.yml' '!merged-packages/**' --ignore-path ../../.gitignore", + "publish:preview": "yarn npm publish --tag preview", + "test": "vitest run --config vitest.config.ts", + "test:clean": "yarn test --no-cache --coverage.clean", + "test:dev": "yarn test --coverage false", + "test:verbose": "yarn test --reporter verbose", + "test:watch": "vitest --config vitest.config.ts" + }, + "dependencies": { + "@endo/captp": "^4.2.2", + "@metamask/utils": "^9.1.0", + "@ocap/streams": "workspace:^" + }, + "devDependencies": { + "@arethetypeswrong/cli": "^0.15.3", + "@metamask/auto-changelog": "^3.4.4", + "@metamask/eslint-config": "^13.0.0", + "@metamask/eslint-config-nodejs": "^13.0.0", + "@metamask/eslint-config-typescript": "^13.0.0", + "@ts-bridge/cli": "^0.5.1", + "@ts-bridge/shims": "^0.1.1", + "@typescript-eslint/eslint-plugin": "^8.1.0", + "@typescript-eslint/parser": "^8.1.0", + "depcheck": "^1.4.7", + "eslint": "^8.57.0", + "eslint-config-prettier": "^8.8.0", + "eslint-plugin-import-x": "^0.5.1", + "eslint-plugin-jsdoc": "^47.0.2", + "eslint-plugin-n": "^16.6.2", + "eslint-plugin-prettier": "^4.2.1", + "eslint-plugin-promise": "^6.1.1", + "eslint-plugin-vitest": "^0.4.1", + "jsdom": "^24.1.1", + "prettier": "^2.7.1", + "rimraf": "^6.0.1", + "ses": "^1.7.0", + "typedoc": "^0.24.8", + "typescript": "~5.5.4", + "vite": "^5.3.5", + "vitest": "^2.0.5" + }, + "engines": { + "node": "^18.18 || >=20" + } +} diff --git a/packages/utils/src/index.test.ts b/packages/utils/src/index.test.ts new file mode 100644 index 000000000..5c22ce18e --- /dev/null +++ b/packages/utils/src/index.test.ts @@ -0,0 +1,17 @@ +import { describe, it, expect } from 'vitest'; + +import * as indexModule from './index.js'; + +describe('index', () => { + it('has the expected exports', () => { + expect(Object.keys(indexModule)).toStrictEqual( + expect.arrayContaining([ + 'CommandMethod', + 'isCommand', + 'makeStreamEnvelopeHandler', + 'wrapStreamCommand', + 'wrapCapTp', + ]), + ); + }); +}); diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts new file mode 100644 index 000000000..df0933e7b --- /dev/null +++ b/packages/utils/src/index.ts @@ -0,0 +1,15 @@ +export type { + CapTpMessage, + CapTpPayload, + Command, + VatMessage, +} from './types.js'; +export { CommandMethod } from './types.js'; +export { isCommand } from './type-guards.js'; +export { + wrapStreamCommand, + wrapCapTp, + makeStreamEnvelopeHandler, + type StreamEnvelope, + type StreamEnvelopeHandler, +} from './stream-envelope.js'; diff --git a/packages/streams/src/stream-envelope.test.ts b/packages/utils/src/stream-envelope.test.ts similarity index 91% rename from packages/streams/src/stream-envelope.test.ts rename to packages/utils/src/stream-envelope.test.ts index 50256a55f..4ba116c74 100644 --- a/packages/streams/src/stream-envelope.test.ts +++ b/packages/utils/src/stream-envelope.test.ts @@ -5,13 +5,13 @@ import { wrapStreamCommand, makeStreamEnvelopeHandler, } from './stream-envelope.js'; -import type { CapTpMessage, WrappedVatMessage } from './types.js'; -import { Command } from './types.js'; +import type { CapTpMessage, VatMessage } from './types.js'; +import { CommandMethod } from './types.js'; describe('StreamEnvelopeHandler', () => { - const commandContent: WrappedVatMessage = { + const commandContent: VatMessage = { id: '1', - message: { type: Command.Evaluate, data: '1 + 1' }, + payload: { method: CommandMethod.Evaluate, params: '1 + 1' }, }; const capTpContent: CapTpMessage = { type: 'CTP_CALL', diff --git a/packages/streams/src/stream-envelope.ts b/packages/utils/src/stream-envelope.ts similarity index 62% rename from packages/streams/src/stream-envelope.ts rename to packages/utils/src/stream-envelope.ts index a53b3fef4..3df0e539c 100644 --- a/packages/streams/src/stream-envelope.ts +++ b/packages/utils/src/stream-envelope.ts @@ -1,6 +1,7 @@ -import { makeStreamEnvelopeKit } from './envelope-kit.js'; -import { isCapTpMessage, isWrappedVatMessage } from './type-guards.js'; -import type { CapTpMessage, WrappedVatMessage } from './types.js'; +import { makeStreamEnvelopeKit } from '@ocap/streams'; + +import { isCapTpMessage, isVatMessage } from './type-guards.js'; +import type { CapTpMessage, VatMessage } from './types.js'; type GuardType = TypeGuard extends ( value: unknown, @@ -27,21 +28,19 @@ const envelopeLabels = Object.values(EnvelopeLabel); const envelopeKit = makeStreamEnvelopeKit< typeof envelopeLabels, { - command: WrappedVatMessage; + command: VatMessage; capTp: CapTpMessage; } >({ - command: isWrappedVatMessage, + command: isVatMessage, capTp: isCapTpMessage, }); -const { streamEnveloper, makeStreamEnvelopeHandler } = envelopeKit; - export type StreamEnvelope = GuardType; export type StreamEnvelopeHandler = ReturnType< - typeof makeStreamEnvelopeHandler + typeof envelopeKit.makeStreamEnvelopeHandler >; -export const wrapStreamCommand = streamEnveloper.command.wrap; -export const wrapCapTp = streamEnveloper.capTp.wrap; -export { makeStreamEnvelopeHandler }; +export const wrapStreamCommand = envelopeKit.streamEnveloper.command.wrap; +export const wrapCapTp = envelopeKit.streamEnveloper.capTp.wrap; +export const { makeStreamEnvelopeHandler } = envelopeKit; diff --git a/packages/utils/src/type-guards.test.ts b/packages/utils/src/type-guards.test.ts new file mode 100644 index 000000000..5d2adee10 --- /dev/null +++ b/packages/utils/src/type-guards.test.ts @@ -0,0 +1,85 @@ +import { describe, it, expect } from 'vitest'; + +import { + isVatMessage, + isCapTpMessage, + isCommand, + isCapTpPayload, +} from './type-guards.js'; +import { CommandMethod } from './types.js'; + +describe('type-guards', () => { + describe('isCapTpPayload', () => { + it.each` + value | expectedResult | description + ${{ method: 'someMethod', params: [] }} | ${true} | ${'valid cap tp payload with empty params'} + ${{ method: 'someMethod', params: ['param1'] }} | ${true} | ${'valid cap tp payload with non-empty params'} + ${123} | ${false} | ${'invalid cap tp payload: primitive number'} + ${{ method: true, params: [] }} | ${false} | ${'invalid cap tp payload: invalid method type'} + ${{ method: 'someMethod' }} | ${false} | ${'invalid cap tp payload: missing params'} + ${{ method: 'someMethod', params: 'param1' }} | ${false} | ${'invalid cap tp payload: params is a primitive string'} + ${{ method: 123, params: [] }} | ${false} | ${'invalid cap tp payload: invalid method type and valid params'} + ${{ method: 'someMethod', params: true }} | ${false} | ${'invalid cap tp payload: valid method and invalid params'} + `( + 'returns $expectedResult for $description', + ({ value, expectedResult }) => { + expect(isCapTpPayload(value)).toBe(expectedResult); + }, + ); + }); + + describe('isCommand', () => { + it.each` + value | expectedResult | description + ${{ method: CommandMethod.Ping, params: null }} | ${true} | ${'valid command with null data'} + ${{ method: CommandMethod.Ping, params: 'data' }} | ${true} | ${'valid command with string data'} + ${123} | ${false} | ${'invalid command: primitive number'} + ${{ method: true, params: 'data' }} | ${false} | ${'invalid command: invalid type'} + ${{ method: CommandMethod.Ping }} | ${false} | ${'invalid command: missing data'} + ${{ method: CommandMethod.Ping, params: 123 }} | ${false} | ${'invalid command: data is a primitive number'} + ${{ method: 123, params: null }} | ${false} | ${'invalid command: invalid type and valid data'} + ${{ method: 'some-type', params: true }} | ${false} | ${'invalid command: valid type and invalid data'} + `( + 'returns $expectedResult for $description', + ({ value, expectedResult }) => { + expect(isCommand(value)).toBe(expectedResult); + }, + ); + }); + + describe('isVatMessage', () => { + it.each` + value | expectedResult | description + ${{ id: 'some-id', payload: { method: CommandMethod.Ping, params: null } }} | ${true} | ${'valid vat message'} + ${123} | ${false} | ${'invalid vat message: primitive number'} + ${{ id: true, payload: {} }} | ${false} | ${'invalid vat message: invalid id and empty payload'} + ${{ id: 'some-id', payload: null }} | ${false} | ${'invalid vat message: payload is null'} + ${{ id: 123, payload: { method: CommandMethod.Ping, params: null } }} | ${false} | ${'invalid vat message: invalid id type'} + ${{ id: 'some-id' }} | ${false} | ${'invalid vat message: missing payload'} + ${{ id: 'some-id', payload: 123 }} | ${false} | ${'invalid vat message: payload is a primitive number'} + ${{ id: 'some-id', payload: { method: 123, params: null } }} | ${false} | ${'invalid vat message: invalid type in payload'} + `( + 'returns $expectedResult for $description', + ({ value, expectedResult }) => { + expect(isVatMessage(value)).toBe(expectedResult); + }, + ); + }); + + describe('isCapTpMessage', () => { + it.each` + value | expectedResult | description + ${{ type: 'CTP_some-type', epoch: 123 }} | ${true} | ${'valid cap tp message'} + ${{ type: true, epoch: null }} | ${false} | ${'invalid cap tp message: invalid type and epoch'} + ${{ type: 'some-type' }} | ${false} | ${'invalid cap tp message: missing epoch'} + ${{ type: 123, epoch: null }} | ${false} | ${'invalid cap tp message: invalid type'} + ${{ type: 'CTP_some-type' }} | ${false} | ${'invalid cap tp message: missing epoch'} + ${{ type: 'CTP_some-type', epoch: true }} | ${false} | ${'invalid cap tp message: invalid epoch type'} + `( + 'returns $expectedResult for $description', + ({ value, expectedResult }) => { + expect(isCapTpMessage(value)).toBe(expectedResult); + }, + ); + }); +}); diff --git a/packages/utils/src/type-guards.ts b/packages/utils/src/type-guards.ts new file mode 100644 index 000000000..2483fbc57 --- /dev/null +++ b/packages/utils/src/type-guards.ts @@ -0,0 +1,29 @@ +import { isObject } from '@metamask/utils'; + +import type { + Command, + CapTpMessage, + VatMessage, + CapTpPayload, +} from './types.js'; + +export const isCapTpPayload = (value: unknown): value is CapTpPayload => + isObject(value) && + typeof value.method === 'string' && + Array.isArray(value.params); + +export const isCommand = (value: unknown): value is Command => + isObject(value) && + typeof value.method === 'string' && + (typeof value.params === 'string' || + value.params === null || + isCapTpPayload(value.params)); + +export const isVatMessage = (value: unknown): value is VatMessage => + isObject(value) && typeof value.id === 'string' && isCommand(value.payload); + +export const isCapTpMessage = (value: unknown): value is CapTpMessage => + isObject(value) && + typeof value.type === 'string' && + value.type.startsWith('CTP_') && + typeof value.epoch === 'number'; diff --git a/packages/utils/src/types.ts b/packages/utils/src/types.ts new file mode 100644 index 000000000..140909cac --- /dev/null +++ b/packages/utils/src/types.ts @@ -0,0 +1,41 @@ +import type { Primitive } from '@endo/captp'; + +export enum CommandMethod { + CapTpCall = 'callCapTp', + CapTpInit = 'makeCapTp', + Evaluate = 'evaluate', + Ping = 'ping', +} + +type CommandParams = + | Primitive + | Promise + | CommandParams[] + | { [key: string]: CommandParams }; + +export type CapTpPayload = { + method: string; + params: CommandParams[]; +}; + +type CommandLike = { + method: Method; + params: Data; +}; + +export type Command = + | CommandLike + | CommandLike + | CommandLike + | CommandLike; + +export type VatMessage = { + id: string; + payload: Command; +}; + +export type CapTpMessage = { + type: Type; + epoch: number; + [key: string]: unknown; +}; diff --git a/packages/utils/tsconfig.build.json b/packages/utils/tsconfig.build.json new file mode 100644 index 000000000..6134d645c --- /dev/null +++ b/packages/utils/tsconfig.build.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.packages.build.json", + "compilerOptions": { + "baseUrl": "./", + "lib": ["DOM", "ES2022"], + "outDir": "./dist", + "rootDir": "./src", + "types": ["ses"] + }, + "references": [{ "path": "../streams/tsconfig.build.json" }], + "include": ["./src"] +} diff --git a/packages/utils/tsconfig.json b/packages/utils/tsconfig.json new file mode 100644 index 000000000..e9f1c081c --- /dev/null +++ b/packages/utils/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.packages.json", + "compilerOptions": { + "baseUrl": "./", + "lib": ["DOM", "ES2022"], + "types": ["ses", "vitest", "vitest/jsdom"] + }, + "references": [{ "path": "../streams" }], + "include": ["./src"] +} diff --git a/packages/utils/typedoc.json b/packages/utils/typedoc.json new file mode 100644 index 000000000..f8eb78ae1 --- /dev/null +++ b/packages/utils/typedoc.json @@ -0,0 +1,8 @@ +{ + "entryPoints": [], + "excludePrivate": true, + "hideGenerator": true, + "out": "docs", + "tsconfig": "./tsconfig.build.json", + "projectDocuments": ["documents/*.md"] +} diff --git a/packages/utils/vitest.config.ts b/packages/utils/vitest.config.ts new file mode 100644 index 000000000..68aa3a1b5 --- /dev/null +++ b/packages/utils/vitest.config.ts @@ -0,0 +1,17 @@ +// eslint-disable-next-line spaced-comment +/// + +import { defineConfig, mergeConfig } from 'vite'; + +import { getDefaultConfig } from '../../vitest.config.packages.js'; + +const defaultConfig = getDefaultConfig(); + +export default mergeConfig( + defaultConfig, + defineConfig({ + test: { + setupFiles: '../test-utils/src/env/mock-endo.ts', + }, + }), +); diff --git a/tsconfig.build.json b/tsconfig.build.json index 34acb2854..bfec97504 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -4,6 +4,7 @@ "references": [ { "path": "./packages/extension/tsconfig.build.json" }, { "path": "./packages/shims/tsconfig.build.json" }, - { "path": "./packages/streams/tsconfig.build.json" } + { "path": "./packages/streams/tsconfig.build.json" }, + { "path": "./packages/utils/tsconfig.build.json" } ] } diff --git a/tsconfig.json b/tsconfig.json index 2cd76ded6..78fd67721 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,6 +12,7 @@ { "path": "./packages/extension" }, { "path": "./packages/shims" }, { "path": "./packages/streams" }, - { "path": "./packages/test-utils" } + { "path": "./packages/test-utils" }, + { "path": "./packages/utils" } ] } diff --git a/yarn.lock b/yarn.lock index 879db5dad..758d9a503 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1382,6 +1382,7 @@ __metadata: "@ocap/shims": "workspace:^" "@ocap/streams": "workspace:^" "@ocap/test-utils": "workspace:^" + "@ocap/utils": "workspace:^" "@types/chrome": "npm:^0.0.268" "@typescript-eslint/eslint-plugin": "npm:^8.1.0" "@typescript-eslint/parser": "npm:^8.1.0" @@ -1422,6 +1423,7 @@ __metadata: "@ocap/shims": "workspace:^" "@ocap/streams": "workspace:^" "@ocap/test-utils": "workspace:^" + "@ocap/utils": "workspace:^" "@ts-bridge/cli": "npm:^0.5.1" "@ts-bridge/shims": "npm:^0.1.1" "@typescript-eslint/eslint-plugin": "npm:^8.1.0" @@ -1521,7 +1523,6 @@ __metadata: resolution: "@ocap/streams@workspace:packages/streams" dependencies: "@arethetypeswrong/cli": "npm:^0.15.3" - "@endo/captp": "npm:^4.2.2" "@endo/promise-kit": "npm:^1.1.4" "@endo/stream": "npm:^1.2.2" "@metamask/auto-changelog": "npm:^3.4.4" @@ -1580,6 +1581,42 @@ __metadata: languageName: unknown linkType: soft +"@ocap/utils@workspace:^, @ocap/utils@workspace:packages/utils": + version: 0.0.0-use.local + resolution: "@ocap/utils@workspace:packages/utils" + dependencies: + "@arethetypeswrong/cli": "npm:^0.15.3" + "@endo/captp": "npm:^4.2.2" + "@metamask/auto-changelog": "npm:^3.4.4" + "@metamask/eslint-config": "npm:^13.0.0" + "@metamask/eslint-config-nodejs": "npm:^13.0.0" + "@metamask/eslint-config-typescript": "npm:^13.0.0" + "@metamask/utils": "npm:^9.1.0" + "@ocap/streams": "workspace:^" + "@ts-bridge/cli": "npm:^0.5.1" + "@ts-bridge/shims": "npm:^0.1.1" + "@typescript-eslint/eslint-plugin": "npm:^8.1.0" + "@typescript-eslint/parser": "npm:^8.1.0" + depcheck: "npm:^1.4.7" + eslint: "npm:^8.57.0" + eslint-config-prettier: "npm:^8.8.0" + eslint-plugin-import-x: "npm:^0.5.1" + eslint-plugin-jsdoc: "npm:^47.0.2" + eslint-plugin-n: "npm:^16.6.2" + eslint-plugin-prettier: "npm:^4.2.1" + eslint-plugin-promise: "npm:^6.1.1" + eslint-plugin-vitest: "npm:^0.4.1" + jsdom: "npm:^24.1.1" + prettier: "npm:^2.7.1" + rimraf: "npm:^6.0.1" + ses: "npm:^1.7.0" + typedoc: "npm:^0.24.8" + typescript: "npm:~5.5.4" + vite: "npm:^5.3.5" + vitest: "npm:^2.0.5" + languageName: unknown + linkType: soft + "@pkgjs/parseargs@npm:^0.11.0": version: 0.11.0 resolution: "@pkgjs/parseargs@npm:0.11.0"