diff --git a/package.json b/package.json
index 533f3df743..d90de61229 100644
--- a/package.json
+++ b/package.json
@@ -124,10 +124,10 @@
"vitest>@vitest/browser>webdriverio>@wdio/utils>edgedriver": false,
"vitest>@vitest/browser>webdriverio>@wdio/utils>geckodriver": false,
"vitest>@vitest/mocker>msw": false,
- "@metamask/kernel-cli>@metamask/kernel-node-runtime>@libp2p/webrtc>@ipshipyard/node-datachannel": false,
+ "@metamask/kernel-cli>@metamask/kernel-node-runtime>@libp2p/webrtc>node-datachannel": false,
"@metamask/kernel-cli>@metamask/kernel-node-runtime>@metamask/kernel-store>better-sqlite3": false,
"@metamask/kernel-cli>@metamask/kernel-node-runtime>@metamask/streams": false,
- "@metamask/kernel-cli>@metamask/kernel-shims>@libp2p/webrtc>@ipshipyard/node-datachannel": false,
+ "@metamask/kernel-cli>@metamask/kernel-shims>@libp2p/webrtc>node-datachannel": false,
"@metamask/kernel-cli>@metamask/kernel-utils": false,
"vitest>jsdom>ws>bufferutil": false,
"vitest>jsdom>ws>utf-8-validate": false
@@ -136,10 +136,10 @@
"resolutions": {
"@radix-ui/react-slot@npm:^1.1.0": "1.2.4",
"cookie": "^0.7.0",
- "@chainsafe/libp2p-noise": "16.1.5",
- "@libp2p/interface": "2.11.0",
- "@multiformats/multiaddr": "12.5.1",
- "@multiformats/multiaddr-to-uri": "11.0.2",
+ "@chainsafe/libp2p-noise": "17.0.0",
+ "@libp2p/interface": "3.1.1",
+ "@multiformats/multiaddr": "13.0.1",
+ "@multiformats/multiaddr-to-uri": "12.0.0",
"@multiformats/dns": "1.0.9"
}
}
diff --git a/packages/brow-2-brow/README.md b/packages/brow-2-brow/README.md
deleted file mode 100644
index 1586d4aa3c..0000000000
--- a/packages/brow-2-brow/README.md
+++ /dev/null
@@ -1,5 +0,0 @@
-# Browser-to-browser connectivity demo
-
-A work in progress, for sure.
-
-Derived from https://github.com/libp2p/libp2p-webrtc-guide
diff --git a/packages/brow-2-brow/esbuild.js b/packages/brow-2-brow/esbuild.js
deleted file mode 100644
index d6ff9eaf0d..0000000000
--- a/packages/brow-2-brow/esbuild.js
+++ /dev/null
@@ -1,13 +0,0 @@
-import { build } from 'esbuild';
-
-build({
- entryPoints: ['./src/index.js'],
- outfile: './dist/index.js',
- sourcemap: 'inline',
- minify: false,
- bundle: true,
- define: {
- 'process.env.NODE_DEBUG': 'false',
- global: 'globalThis',
- },
-});
diff --git a/packages/brow-2-brow/package.json b/packages/brow-2-brow/package.json
deleted file mode 100644
index 76d85b0aee..0000000000
--- a/packages/brow-2-brow/package.json
+++ /dev/null
@@ -1,90 +0,0 @@
-{
- "name": "@ocap/brow-2-brow",
- "version": "1.0.0",
- "private": true,
- "description": "Browser-to-browser libp2p communications proof of concept",
- "repository": {
- "type": "git",
- "url": "https://github.com/MetaMask/ocap-kernel.git"
- },
- "type": "module",
- "scripts": {
- "build": "ts-bridge --project tsconfig.build.json --no-references --clean",
- "build:dev": "mkdir -p dist && ln -fs ../src/index.html dist/index.html",
- "build:docs": "typedoc",
- "changelog:validate": "../../scripts/validate-changelog.sh @ocap/brow-2-brow",
- "clean": "rimraf --glob './*.tsbuildinfo' ./.eslintcache ./coverage ./dist ./.turbo ./logs",
- "lint": "yarn lint:eslint && yarn lint:misc --check && yarn constraints && yarn lint:dependencies",
- "lint:dependencies": "depcheck --quiet",
- "lint:eslint": "eslint . --cache",
- "lint:fix": "yarn lint:eslint --fix && yarn lint:misc --write && yarn constraints --fix && yarn lint:dependencies",
- "lint:misc": "prettier --no-error-on-unmatched-pattern '**/*.json' '**/*.md' '**/*.html' '!**/CHANGELOG.old.md' '**/*.yml' '!.yarnrc.yml' '!merged-packages/**' --ignore-path ../../.gitignore --log-level error",
- "test": "vitest run --config vitest.config.ts",
- "test:clean": "yarn test --no-cache --coverage.clean",
- "test:dev": "yarn test --mode development",
- "test:verbose": "yarn test --reporter verbose",
- "test:watch": "vitest --config vitest.config.ts",
- "start": "npm run build:dev && esbuild src/index.js --serve --sourcemap --bundle --outdir=dist --servedir=dist --external:@types/web",
- "start:relay": "node dist/src/relay.mjs",
- "test:dev:quiet": "yarn test:dev --reporter @ocap/repo-tools/vitest-reporters/silent"
- },
- "dependencies": {
- "@chainsafe/libp2p-noise": "^16.1.3",
- "@chainsafe/libp2p-yamux": "7.0.4",
- "@libp2p/autonat": "2.0.38",
- "@libp2p/bootstrap": "11.0.47",
- "@libp2p/circuit-relay-v2": "3.2.24",
- "@libp2p/crypto": "5.1.8",
- "@libp2p/identify": "3.0.39",
- "@libp2p/interface": "2.11.0",
- "@libp2p/peer-id": "5.1.9",
- "@libp2p/ping": "2.0.37",
- "@libp2p/tcp": "10.1.19",
- "@libp2p/webrtc": "5.2.24",
- "@libp2p/websockets": "9.2.19",
- "@libp2p/webtransport": "5.0.51",
- "@multiformats/multiaddr": "^12.4.4",
- "@multiformats/multiaddr-matcher": "^2.0.0",
- "@ts-bridge/cli": "^0.6.3",
- "@ts-bridge/shims": "^0.1.1",
- "it-byte-stream": "^2.0.1",
- "libp2p": "2.10.0",
- "uint8arrays": "^5.1.0"
- },
- "devDependencies": {
- "@ocap/repo-tools": "workspace:^",
- "@types/node": "^22.13.1",
- "@typescript-eslint/eslint-plugin": "^8.29.0",
- "@typescript-eslint/parser": "^8.29.0",
- "@typescript-eslint/utils": "^8.29.0",
- "depcheck": "^1.4.7",
- "esbuild": "^0.25.3",
- "eslint": "^9.23.0",
- "eslint-config-prettier": "^10.1.1",
- "eslint-import-resolver-typescript": "^4.3.1",
- "eslint-plugin-import-x": "^4.10.0",
- "eslint-plugin-jsdoc": "^50.6.9",
- "eslint-plugin-n": "^17.17.0",
- "eslint-plugin-prettier": "^5.2.6",
- "eslint-plugin-promise": "^7.2.1",
- "prettier": "^3.5.3",
- "rimraf": "^6.0.1",
- "ses": "^1.14.0",
- "turbo": "^2.5.6",
- "typescript": "~5.8.2",
- "typescript-eslint": "^8.29.0",
- "vitest": "^4.0.16"
- },
- "eslintConfig": {
- "extends": "ipfs",
- "parserOptions": {
- "sourceType": "module"
- }
- },
- "engines": {
- "node": ">=22"
- },
- "exports": {
- "./package.json": "./package.json"
- }
-}
diff --git a/packages/brow-2-brow/src/constants.ts b/packages/brow-2-brow/src/constants.ts
deleted file mode 100644
index 5e111fb0fb..0000000000
--- a/packages/brow-2-brow/src/constants.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-// peer ids of known bootstrap nodes
-export const bootstrapPeers = [
- 'QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN',
- 'QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa',
- 'QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb',
- 'QmcZf59bWwK5XFi76CZX8cbJ4BhTzzA3gU1ZjYZcYW3dwt',
- 'QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ',
- 'QmZa1sAxajnQjVM8WjWXoMbmPd7NsWhfKsPkErzpm9wGkp',
-];
-
-export const PUBSUB_PEER_DISCOVERY = 'browser-peer-discovery';
diff --git a/packages/brow-2-brow/src/index.html b/packages/brow-2-brow/src/index.html
deleted file mode 100644
index 0cbbdad828..0000000000
--- a/packages/brow-2-brow/src/index.html
+++ /dev/null
@@ -1,105 +0,0 @@
-
-
-
-
-
-
- Browser-to-browser comm test
-
-
-
-
-
-
- Browser-to-browser comm test (TS)
-
-
-
- Node
-
- -
- Peer ID:
- Unknown
-
- - Status: Initialising
- -
- Connections: 0
-
-
- -
- My addresses: 0
-
-
-
-
-
-
-
-
-
- Output
-
-
-
-
-
-
diff --git a/packages/brow-2-brow/src/index.ts b/packages/brow-2-brow/src/index.ts
deleted file mode 100644
index fdbf4064bc..0000000000
--- a/packages/brow-2-brow/src/index.ts
+++ /dev/null
@@ -1,453 +0,0 @@
-import { noise } from '@chainsafe/libp2p-noise';
-import { yamux } from '@chainsafe/libp2p-yamux';
-import { bootstrap } from '@libp2p/bootstrap';
-import { circuitRelayTransport } from '@libp2p/circuit-relay-v2';
-import { identify } from '@libp2p/identify';
-import type {
- PeerId,
- Connection,
- Libp2p,
- Libp2pEvents,
- PrivateKey,
-} from '@libp2p/interface';
-import { peerIdFromPrivateKey } from '@libp2p/peer-id';
-import { webRTC } from '@libp2p/webrtc';
-import { webSockets } from '@libp2p/websockets';
-import { webTransport } from '@libp2p/webtransport';
-import { multiaddr } from '@multiformats/multiaddr';
-import type { ByteStream } from 'it-byte-stream';
-import { byteStream } from 'it-byte-stream';
-import { createLibp2p } from 'libp2p';
-import { toString as bufToString, fromString } from 'uint8arrays';
-
-import { generateKeyPair } from './key-manglage.ts';
-import { update, getPeerTypes, getAddresses, getPeerDetails } from './utils.ts';
-
-// Our eslint rules have a hard time understanding that this is not Node code
-/* eslint-disable n/no-unsupported-features/node-builtins */
-
-const RELAY_ID = 200;
-const RELAY_HOST = '/dns4/troll.fudco.com';
-
-type Channel = {
- msgStream: ByteStream;
- id: number;
-};
-
-declare global {
- // TypeScript requires you to use `var` here for these declarations to work.
- // eslint-disable-next-line no-var
- var libp2p: Libp2p;
- // eslint-disable-next-line no-var
- var location: Location;
- // eslint-disable-next-line no-var
- var document: Document;
-}
-
-const App = async (): Promise => {
- const peerIdList: (PeerId | undefined)[] = []; // id -> peerID
- const keyList: (PrivateKey | undefined)[] = []; // id -> private key
- const idMap = new Map(); // peerID string -> id
- peerIdList[0] = undefined;
- for (let i = 1; i < 256; ++i) {
- const keyPair = await generateKeyPair(i);
- keyList[i] = keyPair;
- const peerId = peerIdFromPrivateKey(keyPair);
- peerIdList[i] = peerId;
- idMap.set(peerId.toString(), i);
- }
-
- const activeChannels = new Map(); // peerID -> channel info
- const queryParams = new URLSearchParams(location.search);
- const idParam = queryParams.get('id');
- // Nullish coalescing does the wrong thing here; the eslint rule is bad
- // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
- let localId = (idParam && Number.parseInt(idParam, 10)) || 0;
- if (localId < 1 || localId > 255) {
- localId = 0;
- }
- const showEvents = queryParams.get('events');
- const showPeerTypes = queryParams.get('peertypes');
- const showAddresses = queryParams.get('addresses');
- const peerId = peerIdList[localId] as unknown as PeerId;
- const privateKey = keyList[localId] as unknown as PrivateKey;
- // TODO(#562): Use logger instead.
- // eslint-disable-next-line no-console
- console.log(`I am id:${localId} peerId:${peerId.toString()}`);
-
- const relayPeerId = peerIdList[RELAY_ID];
- if (!relayPeerId) {
- throw Error(`relay peer ID is undefined`);
- }
- const relayAddr = `${RELAY_HOST}/tcp/9001/ws/p2p/${relayPeerId.toString()}`;
-
- const libp2p = await createLibp2p({
- privateKey,
- addresses: {
- listen: [
- // Both webRTC and p2p-circuit are needed for inbound hole punching to actually work
- '/webrtc',
- '/p2p-circuit',
- ],
- announce: ['/webrtc'],
- },
- transports: [
- webSockets(),
- webTransport(),
- webRTC(),
- // Required to create circuit relay reservations in order to hole punch browser-to-browser WebRTC connections
- circuitRelayTransport(),
- ],
- connectionEncrypters: [noise()],
- streamMuxers: [yamux()],
- connectionGater: {
- // Allow private addresses for local testing
- denyDialMultiaddr: async () => false,
- },
- peerDiscovery: [
- bootstrap({
- list: [relayAddr],
- }),
- ],
- services: {
- identify: identify(),
- },
- });
-
- globalThis.libp2p = libp2p;
-
- const DOM = {
- /* eslint-disable @typescript-eslint/no-non-null-assertion */
- nodePeerId: () => document.getElementById('output-node-peer-id')!,
- nodeStatus: () => document.getElementById('output-node-status')!,
- nodePeerCount: () => document.getElementById('output-peer-count')!,
- nodePeerTypes: () => document.getElementById('output-peer-types')!,
- nodeAddressCount: () => document.getElementById('output-address-count')!,
- nodeAddresses: () => document.getElementById('output-addresses')!,
- nodePeerDetails: () => document.getElementById('output-peer-details')!,
-
- inputMultiaddr: () =>
- document.getElementById('input-multiaddr') as HTMLInputElement,
- inputTarget: () =>
- document.getElementById('input-target') as HTMLInputElement,
- inputMessage: () =>
- document.getElementById('input-message') as HTMLInputElement,
- connectButton: () => document.getElementById('button-connect')!,
- sendButton: () => document.getElementById('button-send')!,
- outputMessages: () => document.getElementById('output-messages')!,
- /* eslint-enable @typescript-eslint/no-non-null-assertion */
- } satisfies Record HTMLElement | HTMLInputElement>;
-
- outputLine(`I am id:${localId} peerId:${peerId.toString()}`);
-
- update(DOM.nodePeerId(), libp2p.peerId.toString());
- update(DOM.nodeStatus(), 'Online');
-
- /**
- * Output a line of text to the display.
- *
- * @param text - The text to output.
- */
- function outputLine(text: string): void {
- const line = document.createElement('div');
- line.setAttribute('class', 'text-sm break-all');
- line.appendChild(document.createTextNode(text));
- DOM.outputMessages().append(line);
- }
-
- /**
- * Output text about an event, if that behavior is turned on.
- *
- * @param desc - Descriptive text to output.
- */
- function outputEvent(desc: string): void {
- if (showEvents) {
- outputLine(`#### ${desc}`);
- }
- }
-
- /**
- * Output a message that was received.
- *
- * @param id - The network node the message came from.
- * @param message - The message itself.
- */
- function outputMsg(id: number, message: string): void {
- outputLine(`${id}:: '${message}'`);
- }
-
- /**
- * Output information about an error that happened.
- *
- * @param id - The network node the error was associated with.
- * @param task - What we were trying to do at the time.
- * @param problem - The error itself.
- */
- function outputError(id: number, task: string, problem: unknown): void {
- if (problem) {
- const realProblem: Error = problem as Error; // to make eslint stfu
- outputLine(`${id}:: error ${task}: ${realProblem}`);
- } else {
- outputLine(`${id}:: error ${task}`);
- }
- }
-
- /**
- * Output information about a libp2p event that happened.
- *
- * @param type - The event type.
- * @param event - The event itself.
- */
- function logEvent(type: string, event: CustomEvent): void {
- switch (type) {
- case 'certificate:provision':
- case 'certificate:renew': {
- const cert = event.detail;
- outputEvent(`${type}: cert=${cert.cert} key=${cert.key}`);
- break;
- }
- case 'connection:close':
- case 'connection:open': {
- const conn = event.detail;
- outputEvent(
- `${type}: id=${conn.id} dir=${conn.direction} remote=${conn.remotePeer} addr=${conn.remoteAddr} status=${conn.status}`,
- );
- break;
- }
- case 'connection:prune': {
- const conns = event.detail;
- outputEvent(
- `${type}: ids=[${conns.map((conn: Connection) => conn.id).join(',')}]`,
- );
- break;
- }
- case 'peer:connect':
- case 'peer:disconnect':
- case 'peer:reconnect-failure':
- outputEvent(`${type}: ${event.detail}`);
- break;
- case 'peer:discovery': {
- const peerInfo = event.detail;
- outputEvent(
- `${type}: ${peerInfo.id} [${peerInfo.multiaddrs.join(',')}]`,
- );
- break;
- }
- case 'peer:identify': {
- const ir = event.detail;
- outputEvent(`${type}: conn=${ir.connection.id} peer=${ir.peerId}`);
- break;
- }
- case 'peer:update':
- case 'self:peer:update': {
- const pu = event.detail;
- outputEvent(`${type}: peer=${pu.peer.id} previous=${pu.previous?.id}`);
- break;
- }
- case 'start':
- case 'stop':
- case 'transport:close':
- case 'transport:listening':
- outputEvent(`${type}`);
- break;
- default:
- outputEvent(`${type}: unknown event ${JSON.stringify(event.detail)}`);
- break;
- }
- }
-
- const eventTypes = [
- 'certificate:provision',
- 'certificate:renew',
- 'connection:close',
- 'connection:open',
- 'connection:prune',
- 'peer:connect',
- 'peer:disconnect',
- 'peer:discovery',
- 'peer:identify',
- 'peer:reconnect-failure',
- 'peer:update',
- 'self:peer:update',
- 'start',
- 'stop',
- 'transport:close',
- 'transport:listening',
- ];
- if (showEvents) {
- for (const et of eventTypes) {
- libp2p.addEventListener(et as keyof Libp2pEvents, (event) =>
- logEvent(et, event),
- );
- }
- }
-
- setInterval(() => {
- update(DOM.nodePeerCount(), String(libp2p.getConnections().length));
- if (showPeerTypes) {
- update(DOM.nodePeerTypes(), getPeerTypes(libp2p));
- }
- update(DOM.nodeAddressCount(), String(libp2p.getMultiaddrs().length));
- if (showAddresses) {
- update(DOM.nodeAddresses(), getAddresses(libp2p));
- }
- update(DOM.nodePeerDetails(), getPeerDetails(libp2p));
- }, 1000);
-
- const connectButton = DOM.connectButton();
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- connectButton.onclick = async (event: any) => {
- event.preventDefault();
- const maddr = multiaddr(DOM.inputMultiaddr().value);
-
- outputLine(`connect to ${maddr.toString()}`);
- try {
- await libp2p.dial(maddr);
- } catch (problem) {
- outputLine(
- `error connecting to ${maddr.toString()}: ${(problem as Error).toString()}`,
- );
- }
- };
-
- const sendButton = DOM.sendButton();
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- sendButton.onclick = async (event: any) => {
- event.preventDefault();
- const target = DOM.inputTarget().value;
- const message = DOM.inputMessage().value;
- // TODO(#562): Use logger instead.
- // eslint-disable-next-line no-console
- console.log(`send to ${target}: '${message}'`);
- await sendMsg(Number(target), message);
- };
-
- /**
- * Act upon a message received from another network node.
- *
- * @param id - The network node received from.
- * @param message - The message that was received.
- */
- function receiveMsg(id: number, message: string): void {
- outputMsg(id, message);
- }
-
- /**
- * Send a message to another network node, opening a Channel to it if necessary.
- *
- * @param id - The network node to send to.
- * @param message - The message to send.
- */
- async function sendMsg(id: number, message: string): Promise {
- let channel: Channel = activeChannels.get(id);
- if (!channel) {
- try {
- channel = await openChannel(id);
- } catch (problem) {
- outputError(id, 'opening connection', problem);
- return;
- }
- readChannel(channel).catch((problem) => {
- outputError(id, 'reading channel', problem);
- });
- }
- try {
- await channel.msgStream.write(fromString(message));
- } catch (problem) {
- outputError(id, 'sending message', problem);
- }
- }
-
- const SCTP_USER_INITIATED_ABORT = 12; // see RFC 4960
-
- /**
- * Start reading (and processing) messages arriving on a channel.
- *
- * @param channel - The Channel to start reading from.
- */
- async function readChannel(channel: Channel): Promise {
- for (;;) {
- let readBuf;
- try {
- readBuf = await channel.msgStream.read();
- } catch (problem) {
- const rtcProblem = problem as RTCError;
- if (
- rtcProblem.errorDetail === 'sctp-failure' &&
- rtcProblem?.sctpCauseCode === SCTP_USER_INITIATED_ABORT
- ) {
- outputLine(`${channel.id}:: remote disconnected`);
- } else {
- outputError(channel.id, 'reading message', problem);
- }
- throw problem;
- }
- if (readBuf) {
- receiveMsg(channel.id, bufToString(readBuf.subarray()));
- }
- }
- }
-
- /**
- * Open a channel to the node with the given id.
- *
- * @param id - The network node to connect to.
- *
- * @returns a Channel to `id`.
- */
- async function openChannel(id: number): Promise {
- const targetPeerId = peerIdList[id];
- if (!targetPeerId) {
- throw Error(`no peer ID for id = ${id}`);
- }
- outputLine(`connecting to id:${id} peerId:${targetPeerId.toString()}`);
- const signal = AbortSignal.timeout(500000);
- const addressString = `${relayAddr}/p2p-circuit/webrtc/p2p/${targetPeerId.toString()}`;
- outputLine(`dial address ${addressString}`);
- const connectToAddr = multiaddr(addressString);
-
- let stream;
- try {
- stream = await libp2p.dialProtocol(connectToAddr, 'whatever', { signal });
- } catch (problem) {
- if (signal.aborted) {
- outputError(
- id,
- `timed out opening channel to ${targetPeerId.toString()}`,
- problem,
- );
- } else {
- outputError(
- id,
- `opening channel to ${targetPeerId.toString()}`,
- problem,
- );
- }
- throw problem;
- }
- const msgStream = byteStream(stream);
- const channel = { msgStream, id };
- activeChannels.set(targetPeerId, channel);
- return channel;
- }
-
- await libp2p.handle('whatever', ({ connection, stream }) => {
- const msgStream = byteStream(stream);
- const remotePeerId = connection.remotePeer;
- const id = idMap.get(peerId.toString()) ?? -1;
- outputLine(
- `inbound connection from id:${id} peerId:${remotePeerId.toString()}`,
- );
- const channel = { msgStream, id };
- activeChannels.set(remotePeerId, channel);
- readChannel(channel).catch(() => {
- /* Nothing to do here. */
- });
- });
-};
-
-App().catch((problem) => {
- // TODO(#562): Use logger instead.
- // eslint-disable-next-line no-console
- console.error(problem);
-});
diff --git a/packages/brow-2-brow/src/key-manglage.ts b/packages/brow-2-brow/src/key-manglage.ts
deleted file mode 100644
index 6ca9348c0f..0000000000
--- a/packages/brow-2-brow/src/key-manglage.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-import { generateKeyPairFromSeed } from '@libp2p/crypto/keys';
-import type { PrivateKey } from '@libp2p/interface';
-
-/**
- * Convert a Uint8Array into a hex string.
- *
- * @param arr - The bytes to convert.
- *
- * @returns `arr` represented as a hex string.
- */
-export function toHex(arr: Uint8Array): string {
- let result = '';
- for (const byte of arr) {
- const byteHex = byte.toString(16);
- result += byteHex.length === 1 ? `0${byteHex}` : byteHex;
- }
- return result;
-}
-
-/**
- * Convert a hex string into a Uint8Array.
- *
- * @param str - The string to convert.
- *
- * @returns the bytes described by `str`.
- */
-export function fromHex(str: string): Uint8Array {
- const len = str.length;
- const resultLen = len / 2;
- const bytes = new Uint8Array(resultLen);
- let inIdx = 0;
- let outIdx = 0;
- while (outIdx < resultLen) {
- const digits = str.slice(inIdx, inIdx + 2);
- bytes[outIdx] = parseInt(digits, 16);
- outIdx += 2;
- inIdx += 2;
- }
- return bytes;
-}
-
-// seed: 1 peerId: 12D3KooWPjceQrSwdWXPyLLeABRXmuqt69Rg3sBYbU1Nft9HyQ6X
-// seed: 2 peerId: 12D3KooWH3uVF6wv47WnArKHk5p6cvgCJEb74UTmxztmQDc298L3
-// seed: 3 peerId: 12D3KooWQYhTNQdmr3ArTeUHRYzFg94BKyTkoWBDWez9kSCVe2Xo
-// seed: 4 peerId: 12D3KooWLJtG8fd2hkQzTn96MrLvThmnNQjTUFZwGEsLRz5EmSzc
-// seed: 5 peerId: 12D3KooWSHj3RRbBjD15g6wekV8y3mm57Pobmps2g2WJm6F67Lay
-
-/**
- * Generate the private key for a given localID.
- *
- * @param localId - The localID whose peerID is sought.
- *
- * @returns the private key for `localID`.
- */
-export async function generateKeyPair(localId: number): Promise {
- let seed;
- if (localId > 0 && localId < 256) {
- seed = new Uint8Array(32);
- seed[0] = localId;
- } else {
- // eslint-disable-next-line n/no-unsupported-features/node-builtins
- seed = globalThis.crypto.getRandomValues(new Uint8Array(32));
- }
- return await generateKeyPairFromSeed('Ed25519', seed);
-}
diff --git a/packages/brow-2-brow/src/relay.ts b/packages/brow-2-brow/src/relay.ts
deleted file mode 100644
index 2b048d095f..0000000000
--- a/packages/brow-2-brow/src/relay.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-import { noise } from '@chainsafe/libp2p-noise';
-import { yamux } from '@chainsafe/libp2p-yamux';
-import { autoNAT } from '@libp2p/autonat';
-import { circuitRelayServer } from '@libp2p/circuit-relay-v2';
-import { identify } from '@libp2p/identify';
-import { ping } from '@libp2p/ping';
-import { tcp } from '@libp2p/tcp';
-import { webSockets } from '@libp2p/websockets';
-import { createLibp2p } from 'libp2p';
-
-import { generateKeyPair } from './key-manglage.ts';
-
-const RELAY_OFFSET = 200; // for historical reasons, don't ask
-
-/**
- * Main.
- */
-async function main(): Promise {
- const localId = process.argv.length > 2 ? Number(process.argv[2]) : 0;
- const privateKey = await generateKeyPair(localId + RELAY_OFFSET);
- const port = 9001 + localId * 2;
-
- const libp2p = await createLibp2p({
- privateKey,
- addresses: {
- listen: [`/ip4/0.0.0.0/tcp/${port}/ws`, `/ip4/0.0.0.0/tcp/${port + 1}`],
- },
- transports: [webSockets(), tcp()],
- connectionEncrypters: [noise()],
- streamMuxers: [yamux()],
- connectionGater: {
- // Allow private addresses for local testing
- denyDialMultiaddr: async () => false,
- },
- services: {
- identify: identify(),
- autoNat: autoNAT(),
- relay: circuitRelayServer(),
- ping: ping(),
- },
- });
-
- // TODO(#562): Use logger instead.
- // eslint-disable-next-line no-console
- console.log('PeerID: ', libp2p.peerId.toString());
- // TODO(#562): Use logger instead.
- // eslint-disable-next-line no-console
- console.log('Multiaddrs: ', libp2p.getMultiaddrs());
- // eslint-disable-next-line no-console
- console.log('Protocols: ', libp2p.getProtocols());
-}
-
-main().catch(() => {
- /* do nothing on error; it's a PoC */
-});
diff --git a/packages/brow-2-brow/src/utils.ts b/packages/brow-2-brow/src/utils.ts
deleted file mode 100644
index 327a77d866..0000000000
--- a/packages/brow-2-brow/src/utils.ts
+++ /dev/null
@@ -1,131 +0,0 @@
-import type { Libp2p } from '@libp2p/interface';
-import { protocols } from '@multiformats/multiaddr';
-import {
- WebRTC,
- WebSockets,
- WebSocketsSecure,
- // eslint-disable-next-line @typescript-eslint/no-shadow
- WebTransport,
- Circuit,
-} from '@multiformats/multiaddr-matcher';
-
-import { bootstrapPeers } from './constants.ts';
-
-/**
- * Fetch info about our own known addresses.
- *
- * @param libp2p - The libp2p object managing all the libp2p stuff.
- *
- * @returns HTML text for displaying our known addresses.
- */
-export function getAddresses(libp2p: Libp2p): string {
- return libp2p
- .getMultiaddrs()
- .map((ma) => {
- return `${ma.toString()}`;
- })
- .join('');
-}
-
-/**
- * Fetch info about known peer types.
- *
- * @param libp2p - The libp2p object managing all the libp2p stuff.
- *
- * @returns HTML text for displaying the known peer types.
- */
-export function getPeerTypes(libp2p: Libp2p): string {
- const types = {
- 'Circuit Relay': 0,
- WebRTC: 0,
- WebSockets: 0,
- 'WebSockets (secure)': 0,
- WebTransport: 0,
- Other: 0,
- };
-
- libp2p
- .getConnections()
- .map((conn) => conn.remoteAddr)
- .forEach((ma) => {
- if (WebRTC.exactMatch(ma) || ma.toString().includes('/webrtc/')) {
- types.WebRTC += 1;
- } else if (WebSockets.exactMatch(ma)) {
- types.WebSockets += 1;
- } else if (WebSocketsSecure.exactMatch(ma)) {
- types['WebSockets (secure)'] += 1;
- } else if (WebTransport.exactMatch(ma)) {
- types.WebTransport += 1;
- } else if (Circuit.exactMatch(ma)) {
- types['Circuit Relay'] += 1;
- } else {
- types.Other += 1;
- // TODO(#562): Use logger instead.
- // eslint-disable-next-line no-console
- console.info('wat', ma.toString());
- }
- });
-
- return Object.entries(types)
- .map(([name, count]) => `${name}: ${count}`)
- .join('');
-}
-
-/**
- * Fetch info about known peers.
- *
- * @param libp2p - The libp2p object managing all the libp2p stuff.
- *
- * @returns HTML text for displaying the known peers.
- */
-export function getPeerDetails(libp2p: Libp2p): string {
- return libp2p
- .getPeers()
- .map((peer) => {
- const peerConnections = libp2p.getConnections(peer);
-
- const nodeType = [];
-
- // detect if this is a bootstrap node
- if (bootstrapPeers.includes(peer.toString())) {
- nodeType.push('bootstrap');
- }
-
- const relayMultiaddrs = libp2p
- .getMultiaddrs()
- .filter((ma) => Circuit.exactMatch(ma));
- const relayPeers = relayMultiaddrs.map((ma) => {
- return ma
- .stringTuples()
- .filter(([name, _]) => name === protocols('p2p').code)
- .map(([_, value]) => value)[0];
- });
-
- // detect if this is a relay we have a reservation on
- if (relayPeers.includes(peer.toString())) {
- nodeType.push('relay');
- }
-
- return `
- ${peer.toString()}${nodeType.length > 0 ? `(${nodeType.join(', ')})` : ''}
- ${peerConnections
- .map((conn) => {
- return `- ${conn.remoteAddr.toString()}
`;
- })
- .join('')}
- `;
- })
- .join('');
-}
-
-/**
- * Update the value of an HTML data element on the page.
- *
- * @param element - The element to update.
- * @param newContent - The new value to update it to.
- */
-export function update(element: HTMLElement, newContent: string): void {
- if (element.innerHTML !== newContent) {
- element.innerHTML = newContent;
- }
-}
diff --git a/packages/brow-2-brow/tsconfig.build.json b/packages/brow-2-brow/tsconfig.build.json
deleted file mode 100644
index 96bb9a4a55..0000000000
--- a/packages/brow-2-brow/tsconfig.build.json
+++ /dev/null
@@ -1,21 +0,0 @@
-{
- "extends": "../../tsconfig.packages.build.json",
- "compilerOptions": {
- "target": "ES2022",
- "module": "node16",
- "lib": ["ES2023", "ES2023.Array", "DOM", "DOM.Iterable", "WebWorker"],
- "moduleResolution": "node16",
- "esModuleInterop": true,
- "verbatimModuleSyntax": true,
- "isolatedModules": true,
- "skipLibCheck": true,
- "resolveJsonModule": true,
- "declaration": true,
- "sourceMap": true,
- "outDir": "dist",
- "strict": true,
- "moduleDetection": "force"
- },
- "files": [],
- "include": ["./src"]
-}
diff --git a/packages/brow-2-brow/tsconfig.json b/packages/brow-2-brow/tsconfig.json
deleted file mode 100644
index c7f0cf795c..0000000000
--- a/packages/brow-2-brow/tsconfig.json
+++ /dev/null
@@ -1,17 +0,0 @@
-{
- "extends": "../../tsconfig.packages.json",
- "compilerOptions": {
- "baseUrl": "./",
- "lib": ["DOM", "ES2022"],
- "types": ["ses", "vitest", "node"],
- "noEmit": true
- },
- "references": [{ "path": "../repo-tools" }, { "path": "../kernel-utils" }],
- "include": [
- "../../vitest.config.ts",
- "./src/**/*.ts",
- "./test",
- "./vitest.config.ts",
- "./vitest.integration.config.ts"
- ]
-}
diff --git a/packages/brow-2-brow/typedoc.json b/packages/brow-2-brow/typedoc.json
deleted file mode 100644
index 9b891fc4ce..0000000000
--- a/packages/brow-2-brow/typedoc.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "entryPoints": [],
- "excludePrivate": true,
- "hideGenerator": true,
- "out": "docs",
- "tsconfig": "./tsconfig.build.json"
-}
diff --git a/packages/brow-2-brow/vitest.config.ts b/packages/brow-2-brow/vitest.config.ts
deleted file mode 100644
index 9ff3a3f75d..0000000000
--- a/packages/brow-2-brow/vitest.config.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import { mergeConfig } from '@ocap/repo-tools/vitest-config';
-import { defineConfig, defineProject } from 'vitest/config';
-
-import defaultConfig from '../../vitest.config.ts';
-
-export default defineConfig((args) => {
- return mergeConfig(
- args,
- defaultConfig,
- defineProject({
- build: {
- ssr: true,
- rollupOptions: {
- output: {
- esModule: true,
- },
- },
- },
- test: {
- name: 'brow2brow',
- exclude: ['**/test/integration/**'],
- passWithNoTests: true,
- },
- }),
- );
-});
diff --git a/packages/kernel-errors/package.json b/packages/kernel-errors/package.json
index b99b01e67b..47fdad4f71 100644
--- a/packages/kernel-errors/package.json
+++ b/packages/kernel-errors/package.json
@@ -57,7 +57,7 @@
"test:dev:quiet": "yarn test:dev --reporter @ocap/repo-tools/vitest-reporters/silent"
},
"dependencies": {
- "@libp2p/interface": "2.11.0",
+ "@libp2p/interface": "3.1.1",
"@metamask/superstruct": "^3.2.1",
"@metamask/utils": "^11.9.0"
},
diff --git a/packages/kernel-node-runtime/package.json b/packages/kernel-node-runtime/package.json
index b800a19618..04f22e0d34 100644
--- a/packages/kernel-node-runtime/package.json
+++ b/packages/kernel-node-runtime/package.json
@@ -69,12 +69,12 @@
"test:dev:quiet": "yarn test:dev --reporter @ocap/repo-tools/vitest-reporters/silent"
},
"dependencies": {
- "@chainsafe/libp2p-quic": "^1.1.8",
+ "@chainsafe/libp2p-quic": "^2.0.1",
"@endo/eventual-send": "^1.3.4",
"@endo/promise-kit": "^1.1.13",
- "@libp2p/interface": "2.11.0",
- "@libp2p/tcp": "10.1.19",
- "@libp2p/webrtc": "5.2.24",
+ "@libp2p/interface": "3.1.1",
+ "@libp2p/tcp": "11.0.14",
+ "@libp2p/webrtc": "6.0.15",
"@metamask/kernel-platforms": "workspace:^",
"@metamask/kernel-rpc-methods": "workspace:^",
"@metamask/kernel-shims": "workspace:^",
diff --git a/packages/kernel-node-runtime/test/e2e/bip39-identity-recovery.test.ts b/packages/kernel-node-runtime/test/e2e/bip39-identity-recovery.test.ts
index a63c4ca4c3..e54c111668 100644
--- a/packages/kernel-node-runtime/test/e2e/bip39-identity-recovery.test.ts
+++ b/packages/kernel-node-runtime/test/e2e/bip39-identity-recovery.test.ts
@@ -1,12 +1,12 @@
import { makeSQLKernelDatabase } from '@metamask/kernel-store/sqlite/nodejs';
import { Kernel } from '@metamask/ocap-kernel';
-import type { KernelStatus } from '@metamask/ocap-kernel';
import { mkdtemp, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { describe, it, expect } from 'vitest';
import { makeTestKernel } from '../helpers/kernel.ts';
+import { getRemoteCommsPeerId } from '../helpers/remote-comms.ts';
// Valid 12-word test mnemonic (DO NOT use in production)
const TEST_MNEMONIC =
@@ -19,22 +19,9 @@ const TEST_TIMEOUT = 30_000;
// Dummy relay addresses - not actually connected to, just needed for bootstrap
// Using localhost addresses that won't conflict with real relay ports
-const DUMMY_RELAYS = ['/ip4/127.0.0.1/tcp/19001/ws/p2p/QmDummyPeerId'];
-
-/**
- * Extract peerId from remoteComms status, returning undefined for disconnected state.
- *
- * @param remoteComms - The remote comms status object.
- * @returns The peer ID string or undefined.
- */
-function getRemoteCommsPeerId(
- remoteComms: KernelStatus['remoteComms'],
-): string | undefined {
- if (remoteComms && remoteComms.state !== 'disconnected') {
- return remoteComms.peerId;
- }
- return undefined;
-}
+const DUMMY_RELAYS = [
+ '/ip4/127.0.0.1/tcp/19001/ws/p2p/12D3KooWBTf3S95YAsh6fi5rhohMsk8qaTy19rrpW8X9G69vn6GP',
+];
// Tests for identity recovery using mnemonic
// Note: These tests verify that the same mnemonic produces the same peer ID
diff --git a/packages/kernel-node-runtime/test/e2e/quic-transport.test.ts b/packages/kernel-node-runtime/test/e2e/quic-transport.test.ts
index 3fec07e3b4..53cca6346e 100644
--- a/packages/kernel-node-runtime/test/e2e/quic-transport.test.ts
+++ b/packages/kernel-node-runtime/test/e2e/quic-transport.test.ts
@@ -10,34 +10,11 @@ import {
makeRemoteVatConfig,
sendRemoteMessage,
} from '../helpers/remote-comms.ts';
+import { stopWithTimeout } from '../helpers/stop-with-timeout.ts';
// Increase timeout for network operations
const NETWORK_TIMEOUT = 30_000;
-/**
- * Stop an operation with a timeout to prevent hangs during cleanup.
- *
- * @param stopFn - The stop function to call.
- * @param timeoutMs - The timeout in milliseconds.
- * @param label - A label for logging.
- */
-async function stopWithTimeout(
- stopFn: () => Promise,
- timeoutMs: number,
- label: string,
-): Promise {
- try {
- await Promise.race([
- stopFn(),
- new Promise((_resolve, reject) =>
- setTimeout(() => reject(new Error(`${label} timed out`)), timeoutMs),
- ),
- ]);
- } catch {
- // Ignore timeout errors during cleanup
- }
-}
-
// Listen addresses for each kernel (port 0 = OS-assigned)
const quicListenAddress = '/ip4/127.0.0.1/udp/0/quic-v1';
const tcpListenAddress = '/ip4/127.0.0.1/tcp/0';
diff --git a/packages/kernel-node-runtime/test/e2e/relay-connectivity.test.ts b/packages/kernel-node-runtime/test/e2e/relay-connectivity.test.ts
new file mode 100644
index 0000000000..13ec963bc2
--- /dev/null
+++ b/packages/kernel-node-runtime/test/e2e/relay-connectivity.test.ts
@@ -0,0 +1,169 @@
+import type { Libp2p } from '@libp2p/interface';
+import { makeSQLKernelDatabase } from '@metamask/kernel-store/sqlite/nodejs';
+import { startRelay } from '@metamask/kernel-utils/libp2p';
+import { Kernel } from '@metamask/ocap-kernel';
+import { createConnection } from 'node:net';
+import { describe, it, expect, beforeAll, afterAll } from 'vitest';
+
+import { makeTestKernel } from '../helpers/kernel.ts';
+import { getRemoteCommsPeerId } from '../helpers/remote-comms.ts';
+import { stopWithTimeout } from '../helpers/stop-with-timeout.ts';
+
+const STOP_TIMEOUT = 5_000;
+const TEST_TIMEOUT = 30_000;
+const RELAY_PEER_ID = '12D3KooWJBDqsyHQF2MWiCdU4kdqx4zTsSTLRdShg7Ui6CRWB4uc';
+const RELAY_WS_PORT = 9001;
+
+/**
+ * Check if a TCP port is already in use.
+ *
+ * @param port - The port to check.
+ * @returns True if the port is in use.
+ */
+async function isPortInUse(port: number): Promise {
+ return new Promise((resolve) => {
+ const socket = createConnection({ port, host: '127.0.0.1' });
+ socket.on('connect', () => {
+ socket.destroy();
+ resolve(true);
+ });
+ socket.on('error', () => {
+ socket.destroy();
+ resolve(false);
+ });
+ });
+}
+
+describe('Relay Connectivity', () => {
+ let relay: Libp2p | undefined;
+ const relayAddr = `/ip4/127.0.0.1/tcp/${RELAY_WS_PORT}/ws/p2p/${RELAY_PEER_ID}`;
+
+ beforeAll(async () => {
+ // In CI, the test-e2e-ci.sh script or another test file may already have
+ // a relay running on port 9001. Only start our own if the port is free.
+ if (!(await isPortInUse(RELAY_WS_PORT))) {
+ relay = await startRelay(console);
+ }
+ }, 15_000);
+
+ afterAll(async () => {
+ if (relay) {
+ await stopWithTimeout(
+ async () => relay!.stop(),
+ STOP_TIMEOUT,
+ 'relay.stop',
+ );
+ }
+ }, 10_000);
+
+ it('relay is reachable on expected port', async () => {
+ expect(await isPortInUse(RELAY_WS_PORT)).toBe(true);
+ });
+
+ it(
+ 'kernel connects to relay and obtains a peer ID',
+ async () => {
+ const kernelDb = await makeSQLKernelDatabase({ dbFilename: ':memory:' });
+ let kernel: Kernel | undefined;
+
+ try {
+ kernel = await makeTestKernel(kernelDb);
+ await kernel.initRemoteComms({ relays: [relayAddr] });
+
+ const status = await kernel.getStatus();
+ expect(status.remoteComms?.state).toBe('connected');
+ const peerId = getRemoteCommsPeerId(status.remoteComms);
+ expect(peerId).toBeDefined();
+ expect(typeof peerId).toBe('string');
+ } finally {
+ if (kernel) {
+ await stopWithTimeout(
+ async () => kernel!.stop(),
+ STOP_TIMEOUT,
+ 'kernel.stop',
+ );
+ }
+ }
+ },
+ TEST_TIMEOUT,
+ );
+
+ it(
+ 'two kernels can discover each other via the relay',
+ async () => {
+ const kernelDb1 = await makeSQLKernelDatabase({ dbFilename: ':memory:' });
+ const kernelDb2 = await makeSQLKernelDatabase({ dbFilename: ':memory:' });
+ let kernel1: Kernel | undefined;
+ let kernel2: Kernel | undefined;
+
+ try {
+ kernel1 = await makeTestKernel(kernelDb1);
+ kernel2 = await makeTestKernel(kernelDb2);
+
+ await kernel1.initRemoteComms({ relays: [relayAddr] });
+ await kernel2.initRemoteComms({ relays: [relayAddr] });
+
+ const status1 = await kernel1.getStatus();
+ const status2 = await kernel2.getStatus();
+
+ expect(status1.remoteComms?.state).toBe('connected');
+ expect(status2.remoteComms?.state).toBe('connected');
+
+ const peerId1 = getRemoteCommsPeerId(status1.remoteComms);
+ const peerId2 = getRemoteCommsPeerId(status2.remoteComms);
+
+ expect(peerId1).toBeDefined();
+ expect(peerId2).toBeDefined();
+ expect(peerId1).not.toBe(peerId2);
+ } finally {
+ await Promise.all([
+ kernel1 &&
+ stopWithTimeout(
+ async () => kernel1!.stop(),
+ STOP_TIMEOUT,
+ 'kernel1.stop',
+ ),
+ kernel2 &&
+ stopWithTimeout(
+ async () => kernel2!.stop(),
+ STOP_TIMEOUT,
+ 'kernel2.stop',
+ ),
+ ]);
+ }
+ },
+ TEST_TIMEOUT,
+ );
+
+ it(
+ 'kernel reports connected state even with unreachable relay',
+ async () => {
+ // Use a valid but unreachable relay address on a port nothing listens on
+ const badRelayAddr =
+ '/ip4/127.0.0.1/tcp/19999/ws/p2p/12D3KooWBTf3S95YAsh6fi5rhohMsk8qaTy19rrpW8X9G69vn6GP';
+ const kernelDb = await makeSQLKernelDatabase({ dbFilename: ':memory:' });
+ let kernel: Kernel | undefined;
+
+ try {
+ kernel = await makeTestKernel(kernelDb);
+ // initRemoteComms should not throw even if the relay is unreachable —
+ // the libp2p node starts locally and relay connection happens async.
+ await kernel.initRemoteComms({ relays: [badRelayAddr] });
+
+ const status = await kernel.getStatus();
+ // The kernel still gets a local peer ID even without relay connectivity
+ expect(status.remoteComms?.state).toBe('connected');
+ expect(getRemoteCommsPeerId(status.remoteComms)).toBeDefined();
+ } finally {
+ if (kernel) {
+ await stopWithTimeout(
+ async () => kernel!.stop(),
+ STOP_TIMEOUT,
+ 'kernel.stop',
+ );
+ }
+ }
+ },
+ TEST_TIMEOUT,
+ );
+});
diff --git a/packages/kernel-node-runtime/test/e2e/remote-comms.test.ts b/packages/kernel-node-runtime/test/e2e/remote-comms.test.ts
index 2ef598a7bd..d2b287263e 100644
--- a/packages/kernel-node-runtime/test/e2e/remote-comms.test.ts
+++ b/packages/kernel-node-runtime/test/e2e/remote-comms.test.ts
@@ -22,33 +22,10 @@ import {
sendRemoteMessage,
setupAliceAndBob,
} from '../helpers/remote-comms.ts';
+import { stopWithTimeout } from '../helpers/stop-with-timeout.ts';
// Increase timeout for network operations
const NETWORK_TIMEOUT = 30_000;
-
-/**
- * Stop an operation with a timeout to prevent hangs during cleanup.
- *
- * @param stopFn - The stop function to call.
- * @param timeoutMs - The timeout in milliseconds.
- * @param label - A label for logging.
- */
-async function stopWithTimeout(
- stopFn: () => Promise,
- timeoutMs: number,
- label: string,
-): Promise {
- try {
- await Promise.race([
- stopFn(),
- new Promise((_resolve, reject) =>
- setTimeout(() => reject(new Error(`${label} timed out`)), timeoutMs),
- ),
- ]);
- } catch {
- // Ignore timeout errors during cleanup
- }
-}
// Test relay configuration
// The relay peer ID is deterministic based on RELAY_LOCAL_ID = 200 in relay.ts
const relayPeerId = '12D3KooWJBDqsyHQF2MWiCdU4kdqx4zTsSTLRdShg7Ui6CRWB4uc';
diff --git a/packages/kernel-node-runtime/test/e2e/setup-suppress-datachannel-error.ts b/packages/kernel-node-runtime/test/e2e/setup-suppress-datachannel-error.ts
new file mode 100644
index 0000000000..70a31fb282
--- /dev/null
+++ b/packages/kernel-node-runtime/test/e2e/setup-suppress-datachannel-error.ts
@@ -0,0 +1,25 @@
+/**
+ * Suppress known uncaught exceptions from node-datachannel native module.
+ *
+ * When a WebRTC DataChannel is closed while a send is in flight,
+ * node-datachannel v0.29.0 (@libp2p/webrtc v6) throws an asynchronous
+ * uncaught exception from the native layer. This is benign — the connection
+ * is already being torn down — but Vitest v4 treats uncaught exceptions as
+ * test failures.
+ *
+ * This setup file installs a handler that suppresses only this specific error.
+ */
+process.on('uncaughtException', (error: Error) => {
+ if (
+ error.message?.includes('DataChannel is closed') ||
+ error.message?.includes('libdatachannel error')
+ ) {
+ // Benign: native WebRTC module fires async error after channel teardown.
+ return;
+ }
+ // For non-datachannel errors, log and set exit code so Vitest reports
+ // the failure. Re-throwing inside uncaughtException would hard-kill the
+ // process without Vitest diagnostics.
+ console.error('Uncaught exception in e2e test:', error);
+ process.exitCode = 1;
+});
diff --git a/packages/kernel-node-runtime/test/helpers/remote-comms.ts b/packages/kernel-node-runtime/test/helpers/remote-comms.ts
index 09feb2f1b2..0323a758ed 100644
--- a/packages/kernel-node-runtime/test/helpers/remote-comms.ts
+++ b/packages/kernel-node-runtime/test/helpers/remote-comms.ts
@@ -4,11 +4,28 @@ import { Kernel, kunser, makeKernelStore } from '@metamask/ocap-kernel';
import type {
ClusterConfig,
KRef,
+ KernelStatus,
RemoteCommsOptions,
} from '@metamask/ocap-kernel';
import { makeTestKernel } from './kernel.ts';
+/**
+ * Extract peerId from remoteComms status, returning undefined for
+ * disconnected state.
+ *
+ * @param remoteComms - The remote comms status object.
+ * @returns The peer ID string or undefined.
+ */
+export function getRemoteCommsPeerId(
+ remoteComms: KernelStatus['remoteComms'],
+): string | undefined {
+ if (remoteComms && remoteComms.state !== 'disconnected') {
+ return remoteComms.peerId;
+ }
+ return undefined;
+}
+
/**
* Helper to create a vat configuration for a remote vat.
*
diff --git a/packages/kernel-node-runtime/test/helpers/stop-with-timeout.ts b/packages/kernel-node-runtime/test/helpers/stop-with-timeout.ts
new file mode 100644
index 0000000000..1621f77d0e
--- /dev/null
+++ b/packages/kernel-node-runtime/test/helpers/stop-with-timeout.ts
@@ -0,0 +1,23 @@
+/**
+ * Stop an operation with a timeout to prevent hangs during cleanup.
+ *
+ * @param stopFn - The stop function to call.
+ * @param timeoutMs - The timeout in milliseconds.
+ * @param label - A label for logging.
+ */
+export async function stopWithTimeout(
+ stopFn: () => Promise,
+ timeoutMs: number,
+ label: string,
+): Promise {
+ try {
+ await Promise.race([
+ stopFn(),
+ new Promise((_resolve, reject) =>
+ setTimeout(() => reject(new Error(`${label} timed out`)), timeoutMs),
+ ),
+ ]);
+ } catch {
+ // Ignore timeout errors during cleanup
+ }
+}
diff --git a/packages/kernel-node-runtime/vitest.config.e2e.ts b/packages/kernel-node-runtime/vitest.config.e2e.ts
index 922508bce0..50fe7c16eb 100644
--- a/packages/kernel-node-runtime/vitest.config.e2e.ts
+++ b/packages/kernel-node-runtime/vitest.config.e2e.ts
@@ -16,6 +16,12 @@ export default defineConfig((args) => {
fileURLToPath(
import.meta.resolve('@metamask/kernel-shims/endoify-node'),
),
+ fileURLToPath(
+ new URL(
+ './test/e2e/setup-suppress-datachannel-error.ts',
+ import.meta.url,
+ ),
+ ),
],
include: ['./test/e2e/**/*.test.ts'],
exclude: ['./src/**/*'],
diff --git a/packages/kernel-shims/package.json b/packages/kernel-shims/package.json
index d4c5d54b1c..dc98aae14d 100644
--- a/packages/kernel-shims/package.json
+++ b/packages/kernel-shims/package.json
@@ -83,7 +83,7 @@
"vitest": "^4.0.16"
},
"peerDependencies": {
- "@libp2p/webrtc": "5.2.24"
+ "@libp2p/webrtc": "6.0.15"
},
"engines": {
"node": ">=22"
diff --git a/packages/kernel-test/package.json b/packages/kernel-test/package.json
index 9d9ebc133f..cd43178408 100644
--- a/packages/kernel-test/package.json
+++ b/packages/kernel-test/package.json
@@ -52,8 +52,8 @@
"@endo/marshal": "^1.8.0",
"@endo/patterns": "^1.7.0",
"@endo/promise-kit": "^1.1.13",
- "@libp2p/crypto": "5.1.8",
- "@libp2p/peer-id": "5.1.9",
+ "@libp2p/crypto": "5.1.14",
+ "@libp2p/peer-id": "6.0.5",
"@metamask/kernel-node-runtime": "workspace:^",
"@metamask/kernel-store": "workspace:^",
"@metamask/kernel-utils": "workspace:^",
diff --git a/packages/kernel-utils/package.json b/packages/kernel-utils/package.json
index c498680f81..be08b1ffdb 100644
--- a/packages/kernel-utils/package.json
+++ b/packages/kernel-utils/package.json
@@ -75,8 +75,7 @@
"module": "./dist/index.mjs",
"types": "./dist/index.d.cts",
"files": [
- "dist/",
- "patches/"
+ "dist/"
],
"scripts": {
"build": "ts-bridge --project tsconfig.build.json --no-references --clean",
@@ -89,7 +88,6 @@
"lint:eslint": "eslint . --cache",
"lint:fix": "yarn lint:eslint --fix && yarn lint:misc --write && yarn constraints --fix && yarn lint:dependencies",
"lint:misc": "prettier --no-error-on-unmatched-pattern '**/*.json' '**/*.md' '**/*.html' '!**/CHANGELOG.old.md' '**/*.yml' '!.yarnrc.yml' '!merged-packages/**' --ignore-path ../../.gitignore --log-level error",
- "postinstall": "[ ! -d patches ] || patch-package --patch-dir patches",
"publish:preview": "yarn npm publish --tag preview",
"test": "vitest run --config vitest.config.ts",
"test:clean": "yarn test --no-cache --coverage.clean",
@@ -99,24 +97,24 @@
"test:dev:quiet": "yarn test:dev --reporter @ocap/repo-tools/vitest-reporters/silent"
},
"dependencies": {
- "@chainsafe/libp2p-noise": "^16.1.3",
- "@chainsafe/libp2p-yamux": "7.0.4",
+ "@chainsafe/libp2p-noise": "^17.0.0",
+ "@chainsafe/libp2p-yamux": "8.0.1",
"@endo/captp": "^4.4.8",
"@endo/errors": "^1.2.13",
"@endo/exo": "^1.5.12",
"@endo/patterns": "^1.7.0",
"@endo/promise-kit": "^1.1.13",
- "@libp2p/autonat": "2.0.38",
- "@libp2p/circuit-relay-v2": "3.2.24",
- "@libp2p/crypto": "5.1.8",
- "@libp2p/identify": "3.0.39",
- "@libp2p/interface": "2.11.0",
- "@libp2p/ping": "2.0.37",
- "@libp2p/tcp": "10.1.19",
- "@libp2p/websockets": "9.2.19",
+ "@libp2p/autonat": "3.0.14",
+ "@libp2p/circuit-relay-v2": "4.1.7",
+ "@libp2p/crypto": "5.1.14",
+ "@libp2p/identify": "4.0.14",
+ "@libp2p/interface": "3.1.1",
+ "@libp2p/ping": "3.0.14",
+ "@libp2p/tcp": "11.0.14",
+ "@libp2p/websockets": "10.1.7",
"@metamask/superstruct": "^3.2.1",
"@metamask/utils": "^11.9.0",
- "libp2p": "2.10.0",
+ "libp2p": "3.1.7",
"setimmediate": "^1.0.5"
},
"devDependencies": {
@@ -144,7 +142,6 @@
"eslint-plugin-prettier": "^5.2.6",
"eslint-plugin-promise": "^7.2.1",
"jsdom": "^27.4.0",
- "patch-package": "^8.0.0",
"prettier": "^3.5.3",
"rimraf": "^6.0.1",
"ses": "^1.14.0",
@@ -156,7 +153,6 @@
},
"peerDependencies": {
"acorn": "^8.15.0",
- "patch-package": "^8.0.0",
"vite": "^7.3.0"
},
"peerDependenciesMeta": {
diff --git a/packages/ocap-kernel/package.json b/packages/ocap-kernel/package.json
index f268891ef2..45436ee100 100644
--- a/packages/ocap-kernel/package.json
+++ b/packages/ocap-kernel/package.json
@@ -69,23 +69,24 @@
},
"dependencies": {
"@agoric/swingset-liveslots": "0.10.3-u21.0.1",
- "@chainsafe/libp2p-noise": "^16.1.3",
- "@chainsafe/libp2p-yamux": "7.0.4",
+ "@chainsafe/libp2p-noise": "^17.0.0",
+ "@chainsafe/libp2p-yamux": "8.0.1",
"@endo/errors": "^1.2.13",
"@endo/eventual-send": "^1.3.4",
"@endo/marshal": "^1.8.0",
"@endo/pass-style": "^1.6.3",
"@endo/promise-kit": "^1.1.13",
- "@libp2p/bootstrap": "11.0.47",
- "@libp2p/circuit-relay-v2": "3.2.24",
- "@libp2p/crypto": "5.1.8",
- "@libp2p/identify": "3.0.39",
- "@libp2p/interface": "2.11.0",
- "@libp2p/peer-id": "5.1.9",
- "@libp2p/ping": "2.0.37",
- "@libp2p/webrtc": "5.2.24",
- "@libp2p/websockets": "9.2.19",
- "@libp2p/webtransport": "5.0.51",
+ "@libp2p/bootstrap": "12.0.15",
+ "@libp2p/circuit-relay-v2": "4.1.7",
+ "@libp2p/crypto": "5.1.14",
+ "@libp2p/identify": "4.0.14",
+ "@libp2p/interface": "3.1.1",
+ "@libp2p/peer-id": "6.0.5",
+ "@libp2p/ping": "3.0.14",
+ "@libp2p/utils": "7.0.14",
+ "@libp2p/webrtc": "6.0.15",
+ "@libp2p/websockets": "10.1.7",
+ "@libp2p/webtransport": "6.0.16",
"@metamask/kernel-errors": "workspace:^",
"@metamask/kernel-platforms": "workspace:^",
"@metamask/kernel-rpc-methods": "workspace:^",
@@ -96,10 +97,9 @@
"@metamask/streams": "workspace:^",
"@metamask/superstruct": "^3.2.1",
"@metamask/utils": "^11.9.0",
- "@multiformats/multiaddr": "^12.4.4",
+ "@multiformats/multiaddr": "^13.0.1",
"@scure/bip39": "^2.0.1",
- "it-byte-stream": "^2.0.1",
- "libp2p": "2.10.0",
+ "libp2p": "3.1.7",
"multiformats": "^13.3.6",
"ses": "^1.14.0",
"setimmediate": "^1.0.5",
diff --git a/packages/ocap-kernel/src/remotes/platform/connection-factory.test.ts b/packages/ocap-kernel/src/remotes/platform/connection-factory.test.ts
index 76aea2f3c8..029352c048 100644
--- a/packages/ocap-kernel/src/remotes/platform/connection-factory.test.ts
+++ b/packages/ocap-kernel/src/remotes/platform/connection-factory.test.ts
@@ -9,10 +9,10 @@ import type { Channel } from '../types.ts';
// Track state shared between mocks and tests
const libp2pState: {
handler?:
- | ((args: {
- connection: { remotePeer: { toString: () => string } };
- stream: object;
- }) => void | Promise)
+ | ((
+ stream: object,
+ connection: { remotePeer: { toString: () => string } },
+ ) => void | Promise)
| undefined;
dials: {
addr: string;
@@ -127,34 +127,69 @@ vi.mock('@metamask/logger', () => ({
},
}));
+/** Protocol codes matching @multiformats/multiaddr v13 */
+const PROTO_CODES: Record = {
+ ip4: 4,
+ ip6: 41,
+ tcp: 6,
+ udp: 273,
+ dns4: 54,
+ dns6: 55,
+ dnsaddr: 56,
+ ws: 477,
+ wss: 478,
+ tls: 448,
+ p2p: 421,
+ 'p2p-circuit': 290,
+ webrtc: 281,
+ 'quic-v1': 461,
+ webtransport: 465,
+};
+
vi.mock('@multiformats/multiaddr', () => ({
+ CODE_P2P: 421,
+ CODE_IP4: 4,
+ CODE_IP6: 41,
+ CODE_DNS4: 54,
+ CODE_DNS6: 55,
+ CODE_DNSADDR: 56,
multiaddr: (addr: string) => {
// Real multiaddr() throws on malformed addresses
if (!addr.startsWith('/')) {
throw new Error(`invalid multiaddr "${addr}"`);
}
- // Extract the last /p2p/ segment if present
- const peerIdMatch = addr.match(/\/p2p\/([^/]+)$/u);
- // Extract protocol names from multiaddr segments (e.g. /dns4/.../tcp/.../ws/...)
+ // Parse segments into components
const segments = addr.split('/').filter(Boolean);
- const protos: string[] = [];
- for (const seg of segments) {
- if (!/^\d+$/u.test(seg) && !peerIdMatch?.[1]?.includes(seg)) {
- protos.push(seg);
+ const components: { code: number; name: string; value?: string }[] = [];
+ const valuedProtos = new Set([
+ 'ip4',
+ 'ip6',
+ 'tcp',
+ 'udp',
+ 'dns4',
+ 'dns6',
+ 'dnsaddr',
+ 'p2p',
+ ]);
+ let idx = 0;
+ while (idx < segments.length) {
+ const name = segments[idx];
+ const code = PROTO_CODES[name];
+ if (code === undefined) {
+ idx += 1;
+ continue;
+ }
+ if (valuedProtos.has(name) && idx + 1 < segments.length) {
+ components.push({ code, name, value: segments[idx + 1] });
+ idx += 2;
+ } else {
+ components.push({ code, name });
+ idx += 1;
}
}
- // Extract host: value after dns4/dns6/ip4/ip6
- const hostMatch = addr.match(/\/(dns[46]?|ip[46])\/([^/]+)/u);
return {
toString: () => addr,
- getPeerId: () => peerIdMatch?.[1] ?? null,
- protoNames: () => protos,
- toOptions: () => ({
- host: hostMatch?.[2] ?? 'unknown',
- port: 0,
- transport: 'tcp' as const,
- family: 4 as const,
- }),
+ getComponents: () => components,
};
},
}));
@@ -167,14 +202,19 @@ vi.mock('@multiformats/multiaddr', () => ({
* @returns A minimal Multiaddr-shaped object.
*/
function makeTestMultiaddr(protoNames: string[], host: string) {
+ // Build components: first component is the host (ip4/ip6/dns4/dns6)
+ const hostProto = protoNames.find((proto) =>
+ ['ip4', 'ip6', 'dns4', 'dns6', 'dnsaddr'].includes(proto),
+ );
+ const components = protoNames.map((name) => {
+ const code = PROTO_CODES[name] ?? 0;
+ if (name === hostProto) {
+ return { code, name, value: host };
+ }
+ return { code, name };
+ });
return {
- protoNames: () => protoNames,
- toOptions: () => ({
- host,
- port: 0,
- transport: 'tcp' as const,
- family: 4 as const,
- }),
+ getComponents: () => components,
};
}
@@ -186,7 +226,7 @@ type MockByteStream = {
};
const streamMap = new WeakMap