From b0717f617710e2b5fd132e60d09b5118b209a7c9 Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Sat, 12 Sep 2026 09:29:29 +0100 Subject: [PATCH 1/6] refactor(core): generalise token-id.ts into random-id.ts core/room's own fields (token-id, message-id) are both defined as an arbitrary-length bstr needing the same unguessable-identifier randomness, so the helper is renamed to reflect that it now backs both, not just token minting. --- src/core/random-id.ts | 6 ++++++ src/core/token-id.ts | 6 ------ src/test/{token-id.test.ts => random-id.test.ts} | 10 +++++----- 3 files changed, 11 insertions(+), 11 deletions(-) create mode 100644 src/core/random-id.ts delete mode 100644 src/core/token-id.ts rename src/test/{token-id.test.ts => random-id.test.ts} (58%) diff --git a/src/core/random-id.ts b/src/core/random-id.ts new file mode 100644 index 0000000..4824052 --- /dev/null +++ b/src/core/random-id.ts @@ -0,0 +1,6 @@ +/** Generates a random identifier: several of core/room's own fields (token-id, message-id) are defined as an arbitrary-length bstr, and 16 random bytes (128 bits) is the conventional size for an unguessable identifier, matching a UUIDv4's own random payload. */ +export function randomId(): Uint8Array { + const bytes = new Uint8Array(16); + crypto.getRandomValues(bytes); + return bytes; +} diff --git a/src/core/token-id.ts b/src/core/token-id.ts deleted file mode 100644 index c5d0227..0000000 --- a/src/core/token-id.ts +++ /dev/null @@ -1,6 +0,0 @@ -/** Generates a random token-id: tokens.cddl defines token-id as an arbitrary-length bstr, and 16 random bytes (128 bits) is the conventional size for an unguessable identifier, matching a UUIDv4's own random payload. */ -export function randomTokenId(): Uint8Array { - const bytes = new Uint8Array(16); - crypto.getRandomValues(bytes); - return bytes; -} diff --git a/src/test/token-id.test.ts b/src/test/random-id.test.ts similarity index 58% rename from src/test/token-id.test.ts rename to src/test/random-id.test.ts index a943ccc..5ff1dfd 100644 --- a/src/test/token-id.test.ts +++ b/src/test/random-id.test.ts @@ -1,16 +1,16 @@ import { describe, it } from "node:test"; import assert from "node:assert/strict"; -import { randomTokenId } from "../core/token-id.js"; +import { randomId } from "../core/random-id.js"; -describe("randomTokenId", () => { +describe("randomId", () => { it("returns 16 bytes", () => { - const id = randomTokenId(); + const id = randomId(); assert.equal(id.length, 16); }); it("returns a different value on each call", () => { - const a = randomTokenId(); - const b = randomTokenId(); + const a = randomId(); + const b = randomId(); assert.notDeepEqual(a, b); }); }); From 168fd2c1935c94c2a9b30a9d7f9b5ae30fcd3034 Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Sat, 12 Sep 2026 09:31:41 +0100 Subject: [PATCH 2/6] feat(core): verify and deliver a directed room.send handleRoomSend verifies the presented room:member token against all six of core/room's obligations, then delivers the message locally once -- the manage-response itself is the delivery receipt, so there is no separate "delivered" event to emit the way the legacy broadcastPatch path needs. sendRoomMessageDirected is the client-side counterpart: loads the sender's own persisted token for the room, throwing NOT_A_MEMBER if absent, and sends it as a real wire-authenticated room.send via sendRoomRequest. MeshStoreIdentity grows a revocation: RevocationCheck field so verifyRoomToken has a real revocation view to consult, threaded through every setIdentity() call site. --- src/core/bridge-mesh.ts | 3 + src/core/mesh-store.ts | 108 +++++++++++++++++- src/test/downtime-replay.integration.test.ts | 2 + src/test/identity-restart.integration.test.ts | 2 + src/test/room-join-admission.test.ts | 2 + src/test/test-transport.ts | 2 + 6 files changed, 113 insertions(+), 6 deletions(-) diff --git a/src/core/bridge-mesh.ts b/src/core/bridge-mesh.ts index f7ce547..0417b2c 100644 --- a/src/core/bridge-mesh.ts +++ b/src/core/bridge-mesh.ts @@ -8,6 +8,7 @@ import { deviceIdToHex } from "wire-mesh-core/domain/device-id"; import { createSystemClock } from "wire-mesh-core/adapters/system-clock"; +import { createRevocationView } from "wire-mesh-core/domain/revocation-view"; import { MeshStore } from "./mesh-store.js"; import { CommsTool } from "./tool.js"; import { WireMeshTransport } from "./wire-mesh-transport.js"; @@ -37,6 +38,7 @@ export function createBridgeMeshSync( new WireMeshTransport(store.events, identity, store.roomVerbHandlers), ); const tool = new CommsTool(store, store.discovery); + const revocation = createRevocationView(); return { store, tool, @@ -45,6 +47,7 @@ export function createBridgeMeshSync( identity: await toIdentityPort(identity), clock: createSystemClock(), slot, + revocation, }); }, }; diff --git a/src/core/mesh-store.ts b/src/core/mesh-store.ts index ba60296..04cf0bd 100644 --- a/src/core/mesh-store.ts +++ b/src/core/mesh-store.ts @@ -32,22 +32,27 @@ import { FederationManager } from "./federation.js"; import type { FedLink } from "./federation.js"; import { getCertificateFingerprint } from "./identity.js"; import { + bytesToHex, deviceIdFromHex, deviceIdToHex, } from "wire-mesh-core/domain/device-id"; import { mintCapabilityToken } from "wire-mesh-core/domain/tokens"; +import type { RevocationCheck } from "wire-mesh-core/domain/tokens"; import type { IdentityPort } from "wire-mesh-core/ports/identity"; import type { Clock } from "wire-mesh-core/ports/clock"; import type { IncomingManageRequest, ManageOutcome, } from "wire-mesh-core/domain/mesh-session"; -import { roomJoinOkSchema } from "wire-mesh-core/generated/protocol"; +import { roomJoinOkSchema, roomSendSchema } from "wire-mesh-core/generated/protocol"; import type { RoomVerbHandler } from "./room-router.js"; -import { ROOM_MEMBER_CAPABILITY } from "./room-token-verification.js"; -import { saveRoomToken } from "./identity-store.js"; +import { + ROOM_MEMBER_CAPABILITY, + verifyRoomToken, +} from "./room-token-verification.js"; +import { loadRoomTokens, saveRoomToken } from "./identity-store.js"; import type { IdentitySlot } from "./identity-store.js"; -import { randomTokenId } from "./token-id.js"; +import { randomId } from "./random-id.js"; import type { MeshMessage, MeshStatePatch, PeerInfo } from "./wire-protocol.js"; import type { ConnectionHandle, @@ -83,6 +88,7 @@ export interface MeshStoreIdentity { identity: IdentityPort; clock: Clock; slot: IdentitySlot; + revocation: RevocationCheck; } /** @@ -1221,7 +1227,7 @@ export class MeshStore implements CommsStore { const verdict = await mintCapabilityToken({ identity, clock, - tokenId: randomTokenId(), + tokenId: randomId(), bearer: deviceIdFromHex(owner), capability: "room:member", scope: { kind: "room", path: roomPath }, @@ -1242,7 +1248,97 @@ export class MeshStore implements CommsStore { get roomVerbHandlers(): Partial> { return { "room.join": (request, handle) => this.handleRoomJoin(request, handle), + "room.send": (request, handle) => this.handleRoomSend(request, handle), + }; + } + + /** + * Receiving side of a directed room.send (P3.5): verifies the presented token against all six of core/room's own obligations, then delivers the message locally exactly once -- the manage-response this returns IS the delivery receipt, so there is no separate "delivered" event to emit the way the legacy broadcastPatch path needed one. + */ + private async handleRoomSend( + request: IncomingManageRequest, + handle: ConnectionHandle, + ): Promise { + const roomPath = request.scope.path; + if (roomPath === undefined) { + return { result: "error", code: "missing_scope_path" }; + } + if (request.token === undefined) { + return { result: "error", code: "unauthorized" }; + } + const { identity, clock, revocation } = this.requireIdentity(); + const verdict = await verifyRoomToken(request.token, { + identity, + clock, + revocation, + expectedBearer: deviceIdFromHex(handle.id), + roomPath, + }); + if (!verdict.ok) { + return { result: "error", code: "unauthorized" }; + } + + const parsedParams = roomSendSchema.safeParse(request.command.params); + if (!parsedParams.success) { + return { result: "error", code: "malformed_params" }; + } + const params = parsedParams.data; + + const message: RoomMessage = { + id: bytesToHex(params["message-id"]), + from: handle.id, + room: roomPath, + content: params.text, + timestamp: new Date(params["sent-at"]).toISOString(), + readBy: [handle.id], }; + const history = this.messages.get(roomPath) ?? []; + history.push(message); + this.messages.set(roomPath, history); + + const event: DeliveryEvent = { type: "room_message", message }; + this.queueDelivery(this.peerId, event); + this.fireLocalDelivery(this.peerId, event); + + return { result: "ok" }; + } + + /** + * Sends one directed room.send to a single member's own session, attaching this store's own persisted room:member token for the given room path -- the primitive P3.5's own directed fan-out (deliverToRoom) will loop over per member once it replaces the legacy broadcastPatch path this store still uses for message delivery today. Throws if this store holds no token for the room: never a member, or a token that expired or was revoked with nothing fresh persisted in its place. + */ + async sendRoomMessageDirected( + roomPath: string, + memberId: string, + text: string, + ): Promise { + const { slot, clock } = this.requireIdentity(); + const token = loadRoomTokens(slot)[roomPath]; + if (token === undefined) { + throw new CommsError( + `No room:member token for ${roomPath}`, + "NOT_A_MEMBER", + ); + } + const outcome = await this.requireTransport().sendRoomRequest( + memberId, + { + verb: ROOM_MEMBER_CAPABILITY, + params: { + verb: "room.send", + "message-id": randomId(), + "sent-at": clock.now(), + text, + }, + }, + { kind: "room", path: roomPath }, + token, + ); + if (outcome.result !== "ok") { + throw new CommsError( + `room.send to ${memberId} for ${roomPath} failed (${outcome.code})`, + "SEND_FAILED", + ); + } } /** @@ -1302,7 +1398,7 @@ export class MeshStore implements CommsStore { const verdict = await mintCapabilityToken({ identity, clock, - tokenId: randomTokenId(), + tokenId: randomId(), bearer: deviceIdFromHex(handle.id), capability: "room:member", scope: { kind: "room", path: roomPath }, diff --git a/src/test/downtime-replay.integration.test.ts b/src/test/downtime-replay.integration.test.ts index b77cf00..08841c6 100644 --- a/src/test/downtime-replay.integration.test.ts +++ b/src/test/downtime-replay.integration.test.ts @@ -10,6 +10,7 @@ import { tmpdir } from "node:os"; import path from "node:path"; import { deviceIdToHex } from "wire-mesh-core/domain/device-id"; import { createSystemClock } from "wire-mesh-core/adapters/system-clock"; +import { createRevocationView } from "wire-mesh-core/domain/revocation-view"; import { MeshStore } from "../core/mesh-store.js"; import { WireMeshTransport } from "../core/wire-mesh-transport.js"; import { @@ -44,6 +45,7 @@ async function makePeer( identity: await toIdentityPort(identity), clock: createSystemClock(), slot, + revocation: createRevocationView(), }); const deliveries: DeliveryEvent[] = []; return { store, deliveries }; diff --git a/src/test/identity-restart.integration.test.ts b/src/test/identity-restart.integration.test.ts index b7282e1..0101326 100644 --- a/src/test/identity-restart.integration.test.ts +++ b/src/test/identity-restart.integration.test.ts @@ -10,6 +10,7 @@ import { tmpdir } from "node:os"; import path from "node:path"; import { deviceIdToHex } from "wire-mesh-core/domain/device-id"; import { createSystemClock } from "wire-mesh-core/adapters/system-clock"; +import { createRevocationView } from "wire-mesh-core/domain/revocation-view"; import { MeshStore } from "../core/mesh-store.js"; import { WireMeshTransport } from "../core/wire-mesh-transport.js"; import type { PeerIdentity } from "../core/identity.js"; @@ -44,6 +45,7 @@ async function makePeer( identity: await toIdentityPort(identity), clock: createSystemClock(), slot, + revocation: createRevocationView(), }); const deliveries: DeliveryEvent[] = []; return { store, deliveries }; diff --git a/src/test/room-join-admission.test.ts b/src/test/room-join-admission.test.ts index d5524d0..825febe 100644 --- a/src/test/room-join-admission.test.ts +++ b/src/test/room-join-admission.test.ts @@ -18,6 +18,7 @@ import { deviceIdToHex, } from "wire-mesh-core/domain/device-id"; import { createSystemClock } from "wire-mesh-core/adapters/system-clock"; +import { createRevocationView } from "wire-mesh-core/domain/revocation-view"; import { mintCapabilityToken } from "wire-mesh-core/domain/tokens"; import { MeshStore } from "../core/mesh-store.js"; import { ownerNamedRoomPath } from "../core/room-path.js"; @@ -174,6 +175,7 @@ describe("joinRoom (requester side, remote path)", () => { identity: identityPort, clock: createSystemClock(), slot, + revocation: createRevocationView(), }); const ownerId = "f".repeat(64); diff --git a/src/test/test-transport.ts b/src/test/test-transport.ts index 9f47fe9..1d655a0 100644 --- a/src/test/test-transport.ts +++ b/src/test/test-transport.ts @@ -6,6 +6,7 @@ import { tmpdir } from "node:os"; import { WireMeshTransport } from "../core/wire-mesh-transport.js"; import { deviceIdToHex } from "wire-mesh-core/domain/device-id"; import { createSystemClock } from "wire-mesh-core/adapters/system-clock"; +import { createRevocationView } from "wire-mesh-core/domain/revocation-view"; import { loadOrCreateIdentity } from "../core/identity-store.js"; import type { IdentitySlot } from "../core/identity-store.js"; import { toIdentityPort } from "../core/wire-mesh-identity.js"; @@ -32,6 +33,7 @@ export async function wireTestTransport( identity: await toIdentityPort(identity), clock: createSystemClock(), slot: resolvedSlot, + revocation: createRevocationView(), }); // Surface transport-level errors instead of leaving them silent — a genuine socket failure during a test run is signal worth seeing even when the test's own assertions still pass, since it can point at a real race the assertions don't happen to catch. store.onError = (e) => { From a2699f6f7c17ba40fff8fda320d0fed97bb62ad5 Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Sat, 12 Sep 2026 09:32:04 +0100 Subject: [PATCH 3/6] test: wire the smoke test's generated script for real room-verb dispatch The generated child-process script never passed store.roomVerbHandlers to WireMeshTransport's constructor, so the smoke test's own peers could never dispatch any room verb over the wire -- missed until now since it's a dynamically-generated string tsc never checks. Also threads the revocation view MeshStoreIdentity now requires. --- src/test/mesh-smoke.runner.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/test/mesh-smoke.runner.ts b/src/test/mesh-smoke.runner.ts index 973d185..0f3c3a8 100644 --- a/src/test/mesh-smoke.runner.ts +++ b/src/test/mesh-smoke.runner.ts @@ -114,6 +114,7 @@ function buildScript(name: string, actions: string): string { `const { toIdentityPort } = require("./dist/core/wire-mesh-identity.js");`, `const { deviceIdToHex } = require("wire-mesh-core/domain/device-id");`, `const { createSystemClock } = require("wire-mesh-core/adapters/system-clock");`, + `const { createRevocationView } = require("wire-mesh-core/domain/revocation-view");`, `const os = require("node:os");`, `const path = require("node:path");`, `const fs = require("node:fs");`, @@ -123,8 +124,8 @@ function buildScript(name: string, actions: string): string { ` const slot = { harness: "smoke-${name}", cwd: "/test/${name}", dir: fs.mkdtempSync(path.join(os.tmpdir(), "agent-comms-smoke-${name}-")) };`, ` const identity = loadOrCreateIdentity(slot);`, ` store.peerId = deviceIdToHex(Uint8Array.from(identity.deviceId));`, - ` store.setTransport(new WireMeshTransport(store.events, identity));`, - ` store.setIdentity({ identity: await toIdentityPort(identity), clock: createSystemClock(), slot });`, + ` store.setTransport(new WireMeshTransport(store.events, identity, store.roomVerbHandlers));`, + ` store.setIdentity({ identity: await toIdentityPort(identity), clock: createSystemClock(), slot, revocation: createRevocationView() });`, ` const tool = new CommsTool(store);`, ` const deliveries = [];`, ` store.onDelivery = (_id, event) => {`, From 2f9c91b85bd3fbc16a3f23eaa30f817e80678c16 Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Sat, 12 Sep 2026 09:32:21 +0100 Subject: [PATCH 4/6] test(core): cover directed room.send delivery end to end Three real, two-peer integration tests: a directed send from the owner delivers to the member's own onDelivery, the same works in reverse from a member back to the owner, and sendRoomMessageDirected throws when this store holds no persisted token for the room. Membership is set up by minting and persisting the member's own token directly rather than driving a real room.join round trip: MeshStore's legacy full-state-sync makes two mesh-connected peers instantly aware of any room the moment it's created, so a genuine join round trip always takes the already-known-locally branch here, irrelevant to what this file actually exercises. --- .../room-send-directed.integration.test.ts | 175 ++++++++++++++++++ 1 file changed, 175 insertions(+) create mode 100644 src/test/room-send-directed.integration.test.ts diff --git a/src/test/room-send-directed.integration.test.ts b/src/test/room-send-directed.integration.test.ts new file mode 100644 index 0000000..a7ae2be --- /dev/null +++ b/src/test/room-send-directed.integration.test.ts @@ -0,0 +1,175 @@ +/** + * Integration test for room.send's directed delivery (P3.5's own foundational primitive): a real, wire-authenticated room:member token authorises exactly one message to exactly one member's own session, verified against all six of core/room's obligations on the receiving end, delivered locally with no separate "delivered" event since the manage-response itself is the receipt. + * + * Membership is set up by minting and persisting the member's own token directly, not by driving a real room.join round trip: MeshStore's legacy full-state-sync (still active per the "both paths coexist" transition) makes two mesh-connected peers instantly aware of any room the moment it's created, so member.joinRoom(room.id, ...) always takes the already-known-locally branch rather than the wire-level remote-join path -- the same race #73's own tests had to route around, and irrelevant to what this file actually tests (room.send, not room.join). + */ + +import * as assert from "node:assert/strict"; +import { test } from "node:test"; +import { mintCapabilityToken } from "wire-mesh-core/domain/tokens"; +import { createSystemClock } from "wire-mesh-core/adapters/system-clock"; +import { deviceIdFromHex } from "wire-mesh-core/domain/device-id"; +import { MeshStore } from "../core/mesh-store.js"; +import type { DeliveryEvent } from "../core/types.js"; +import { loadOrCreateIdentity, saveRoomToken } from "../core/identity-store.js"; +import type { IdentitySlot } from "../core/identity-store.js"; +import { toIdentityPort } from "../core/wire-mesh-identity.js"; +import { waitFor, wireTestTransport } from "./test-transport.js"; + +let nextPort = 20_970; +function freshPort(): number { + nextPort += 1; + return nextPort; +} + +async function makeConnectedPair(port: number): Promise<{ + owner: MeshStore; + ownerSlot: IdentitySlot; + member: MeshStore; + memberSlot: IdentitySlot; +}> { + const owner = new MeshStore(port); + const ownerSlot = await wireTestTransport(owner); + await owner.init(); + await owner.registerAgent({ + name: "owner", + harness: "test", + cwd: "/test/owner", + pid: process.pid, + visibility: "visible", + tags: [], + }); + + const member = new MeshStore(port); + const memberSlot = await wireTestTransport(member); + await member.init(); + await member.registerAgent({ + name: "member", + harness: "test", + cwd: "/test/member", + pid: process.pid, + visibility: "visible", + tags: [], + }); + + await waitFor( + () => owner.serialise().agents[member.peerId] !== undefined, + "owner sees the member agent", + ); + return { owner, ownerSlot, member, memberSlot }; +} + +/** Mints a member grant directly, under the owner's own persisted identity, and persists it into the member's own slot -- bypassing the wire-level room.join round trip entirely, per this file's own header comment on why that round trip can't be exercised in a live two-peer test here. */ +async function grantMembership( + ownerSlot: IdentitySlot, + memberSlot: IdentitySlot, + roomPath: string, + memberDeviceHex: string, +): Promise { + const ownerIdentity = await toIdentityPort(loadOrCreateIdentity(ownerSlot)); + const clock = createSystemClock(); + const verdict = await mintCapabilityToken({ + identity: ownerIdentity, + clock, + tokenId: new Uint8Array([1]), + bearer: deviceIdFromHex(memberDeviceHex), + capability: "room:member", + scope: { kind: "room", path: roomPath }, + expires: clock.now() + 60_000, + delegationsRemaining: 0, + }); + assert.ok(verdict.ok, "expected the fixture grant to mint successfully"); + if (!verdict.ok) return; + saveRoomToken(memberSlot, roomPath, verdict.token); +} + +void test("a directed room.send delivers to the recipient's own onDelivery", async () => { + const { owner, ownerSlot, member, memberSlot } = await makeConnectedPair( + freshPort(), + ); + + try { + const room = await owner.createRoom({ + name: "general", + type: "public", + owner: owner.peerId, + description: "", + }); + await grantMembership(ownerSlot, memberSlot, room.id, member.peerId); + + const deliveries: DeliveryEvent[] = []; + member.onDelivery = (_agentId, event) => { + deliveries.push(event); + }; + + await owner.sendRoomMessageDirected(room.id, member.peerId, "hello there"); + + await waitFor( + () => deliveries.some((event) => event.type === "room_message"), + "member receives the directed room.send", + ); + const delivered = deliveries.find( + (event) => event.type === "room_message", + ); + assert.ok(delivered); + if (delivered.type !== "room_message") return; + assert.equal(delivered.message.content, "hello there"); + assert.equal(delivered.message.from, owner.peerId); + assert.equal(delivered.message.room, room.id); + } finally { + await member.shutdown(); + await owner.shutdown(); + } +}); + +void test("a directed room.send from a member (not just the owner) also delivers", async () => { + const { owner, ownerSlot, member, memberSlot } = await makeConnectedPair( + freshPort(), + ); + + try { + const room = await owner.createRoom({ + name: "general", + type: "public", + owner: owner.peerId, + description: "", + }); + await grantMembership(ownerSlot, memberSlot, room.id, member.peerId); + + const deliveries: DeliveryEvent[] = []; + owner.onDelivery = (_agentId, event) => { + deliveries.push(event); + }; + + await member.sendRoomMessageDirected(room.id, owner.peerId, "hi back"); + + await waitFor( + () => deliveries.some((event) => event.type === "room_message"), + "owner receives the directed room.send", + ); + } finally { + await member.shutdown(); + await owner.shutdown(); + } +}); + +void test("sendRoomMessageDirected throws when this store holds no token for the room", async () => { + const { owner, member } = await makeConnectedPair(freshPort()); + + try { + const room = await owner.createRoom({ + name: "general", + type: "public", + owner: owner.peerId, + description: "", + }); + // Deliberately not granted -- member has no persisted token for this room. + await assert.rejects( + member.sendRoomMessageDirected(room.id, owner.peerId, "uninvited"), + /No room:member token/, + ); + } finally { + await member.shutdown(); + await owner.shutdown(); + } +}); From cfea7ea8c4991efba81519b626f0b3c352f1a359 Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Sat, 12 Sep 2026 09:35:16 +0100 Subject: [PATCH 5/6] style: apply eslint --fix formatting --- src/core/mesh-store.ts | 5 ++++- src/test/room-send-directed.integration.test.ts | 14 +++++--------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/core/mesh-store.ts b/src/core/mesh-store.ts index 04cf0bd..878b3d7 100644 --- a/src/core/mesh-store.ts +++ b/src/core/mesh-store.ts @@ -44,7 +44,10 @@ import type { IncomingManageRequest, ManageOutcome, } from "wire-mesh-core/domain/mesh-session"; -import { roomJoinOkSchema, roomSendSchema } from "wire-mesh-core/generated/protocol"; +import { + roomJoinOkSchema, + roomSendSchema, +} from "wire-mesh-core/generated/protocol"; import type { RoomVerbHandler } from "./room-router.js"; import { ROOM_MEMBER_CAPABILITY, diff --git a/src/test/room-send-directed.integration.test.ts b/src/test/room-send-directed.integration.test.ts index a7ae2be..374958f 100644 --- a/src/test/room-send-directed.integration.test.ts +++ b/src/test/room-send-directed.integration.test.ts @@ -84,9 +84,8 @@ async function grantMembership( } void test("a directed room.send delivers to the recipient's own onDelivery", async () => { - const { owner, ownerSlot, member, memberSlot } = await makeConnectedPair( - freshPort(), - ); + const { owner, ownerSlot, member, memberSlot } = + await makeConnectedPair(freshPort()); try { const room = await owner.createRoom({ @@ -108,9 +107,7 @@ void test("a directed room.send delivers to the recipient's own onDelivery", asy () => deliveries.some((event) => event.type === "room_message"), "member receives the directed room.send", ); - const delivered = deliveries.find( - (event) => event.type === "room_message", - ); + const delivered = deliveries.find((event) => event.type === "room_message"); assert.ok(delivered); if (delivered.type !== "room_message") return; assert.equal(delivered.message.content, "hello there"); @@ -123,9 +120,8 @@ void test("a directed room.send delivers to the recipient's own onDelivery", asy }); void test("a directed room.send from a member (not just the owner) also delivers", async () => { - const { owner, ownerSlot, member, memberSlot } = await makeConnectedPair( - freshPort(), - ); + const { owner, ownerSlot, member, memberSlot } = + await makeConnectedPair(freshPort()); try { const room = await owner.createRoom({ From 02a0ba2f611a4a2c18d504387d5e9753ac97665b Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Sat, 12 Sep 2026 09:49:22 +0100 Subject: [PATCH 6/6] fix(deps): bump wire-mesh-core to 1.0.3 for the real-wire signature fix Every capability token received over a genuine TCP/TLS connection failed signature verification despite its bytes crossing the wire perfectly intact -- a cbor2 Buffer-vs-Uint8Array mismatch, fixed upstream in ExaDev/wire-mesh#88. This dependency's own project-level .npmrc gains a matching minimum-release-age exclusion, since it's a first-party package this repo depends on. --- .npmrc | 1 + package.json | 2 +- pnpm-lock.yaml | 10 +++++----- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.npmrc b/.npmrc index 04b657d..773d1e0 100644 --- a/.npmrc +++ b/.npmrc @@ -1,2 +1,3 @@ minimum-release-age=10080 minimum-release-age-exclude[]=@mariozechner/* +minimum-release-age-exclude[]=wire-mesh-core diff --git a/package.json b/package.json index 1a85a59..3e9691b 100644 --- a/package.json +++ b/package.json @@ -97,7 +97,7 @@ "cbor2": "2.3.0", "preact": "10.29.7", "typebox": "1.3.6", - "wire-mesh-core": "1.0.2", + "wire-mesh-core": "1.0.3", "ws": "8.21.1", "zod": "4.4.3" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9c48da2..7190d36 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -36,8 +36,8 @@ importers: specifier: 1.3.6 version: 1.3.6 wire-mesh-core: - specifier: 1.0.2 - version: 1.0.2 + specifier: 1.0.3 + version: 1.0.3 ws: specifier: 8.21.1 version: 8.21.1 @@ -3235,8 +3235,8 @@ packages: engines: {node: '>= 8'} hasBin: true - wire-mesh-core@1.0.2: - resolution: {integrity: sha512-MV+odiI9DQXsGWJ9FiOs8hbZxseLEua2JfosRlPdPIByOMcM8/PYkgtbPt8hyPuYyYl21QHTQVputzGkyL69Zw==} + wire-mesh-core@1.0.3: + resolution: {integrity: sha512-jauVnnL5c9EJynIvvztXa0V4eRXZbMz2a6zGKCTh1ZHB9srwTRg03rE49FltC+2MXtOT//dpOIZFTshEAHwKhg==} word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} @@ -6780,7 +6780,7 @@ snapshots: dependencies: isexe: 2.0.0 - wire-mesh-core@1.0.2: + wire-mesh-core@1.0.3: dependencies: cbor2: 2.3.0 cddl.js: 1.0.1