Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"@ocap/kernel": "workspace:^",
"@ocap/shims": "workspace:^",
"@ocap/streams": "workspace:^",
"@ocap/utils": "workspace:^",
"ses": "^1.7.0"
},
"devDependencies": {
Expand Down
62 changes: 37 additions & 25 deletions packages/extension/src/background.ts
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -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<void> {
async function sendCommand(method: string, params?: Json): Promise<void> {
await provideOffScreenDocument();

await chrome.runtime.sendMessage({
type,
target: KernelMessageTarget.Offscreen,
data: data ?? null,
target: ExtensionMessageTarget.Offscreen,
payload: {
method,
params: params ?? null,
},
});
}

Expand All @@ -65,26 +71,32 @@ async function provideOffScreenDocument(): Promise<void> {

// 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}"`,
);
}
}),
Expand Down
63 changes: 31 additions & 32 deletions packages/extension/src/iframe.ts
Original file line number Diff line number Diff line change
@@ -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 });

Expand Down Expand Up @@ -49,32 +48,29 @@ async function main(): Promise<void> {
/**
* 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<void> {
switch (message.type) {
case Command.Evaluate: {
if (typeof message.data !== 'string') {
async function handleMessage({ id, payload }: VatMessage): Promise<void> {
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' }),
Expand All @@ -87,16 +83,22 @@ async function main(): Promise<void> {
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}"`,
);
}
}
Expand All @@ -105,13 +107,10 @@ async function main(): Promise<void> {
* 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<void> {
await streams.writer.next(wrapStreamCommand({ id, message }));
async function replyToMessage(id: string, payload: Command): Promise<void> {
await streams.writer.next(wrapStreamCommand({ id, payload }));
}

/**
Expand Down
3 changes: 2 additions & 1 deletion packages/extension/src/makeIframeVatWorker.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down
82 changes: 51 additions & 31 deletions packages/extension/src/offscreen.ts
Original file line number Diff line number Diff line change
@@ -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);

Expand All @@ -23,53 +23,73 @@ async function main(): Promise<void> {

// 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;
}

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<void> {
async function replyToCommand(
method: CommandMethod,
params?: string,
): Promise<void> {
await chrome.runtime.sendMessage({
data: data ?? null,
target: KernelMessageTarget.Background,
type,
target: ExtensionMessageTarget.Background,
payload: {
method,
params: params ?? null,
},
});
}

Expand All @@ -83,8 +103,8 @@ async function main(): Promise<void> {
async function evaluate(vatId: string, source: string): Promise<string> {
try {
const result = await kernel.sendMessage(vatId, {
type: Command.Evaluate,
data: source,
method: CommandMethod.Evaluate,
params: source,
});
return String(result);
} catch (error) {
Expand Down
Loading