wire-mesh-core 3.6.1 → 3.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -780,8 +780,8 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
780
780
  }
781
781
  };
782
782
  }
783
- function createMeshSession(transport, identity, clock = { now: () => Date.now() }, reconnect = null, addresses = [], onPeerAdvert) {
784
- const { session } = createSessionCore(identity, clock, reconnect, async (address) => transport.connect(address), onPeerAdvert, addresses);
783
+ function createMeshSession(transport, identity, clock = { now: () => Date.now() }, reconnect = null, addresses = [], onPeerAdvert, onFrame) {
784
+ const { session } = createSessionCore(identity, clock, reconnect, async (address) => transport.connect(address), onPeerAdvert, addresses, onFrame);
785
785
  return session;
786
786
  }
787
787
  /** Wires an already-accepted Connection up as a full MeshSession, mirroring exactly what createMeshSession's own dial path does once a connection exists (send handshake, send self-advert, negotiate, consume frames) -- the wire-mesh#45 prerequisite agent-comms needs, since its peers both listen and dial rather than only ever dialing the way web-console's own console UI does. Reconnect does not apply here: if this connection drops, only the remote redialing and being accepted again produces a new connection, and therefore a new session -- there is nothing on this side to retry. */
@@ -113,7 +113,9 @@ export declare function createMeshSession(transport: Readonly<Transport>, identi
113
113
  /** This node's own directly-reachable "host:port" candidates (wire-mesh#38), advertised in this session's self-advert so other peers can attempt a direct connection instead of always falling back to a relay. Omit (or pass none) for a caller with nothing to offer, e.g. a browser client. */
114
114
  addresses?: readonly string[],
115
115
  /** Fired for every peer-advert entry as it's applied to the directory, regardless of source -- the hook a gossip-expansion consumer (wire-mesh#187, gossip-expansion.ts's own createGossipExpansion) uses to observe newly-gossiped peers without becoming a second, competing consumer of this session's own single-reader events stream (each emitted SessionEvent wakes at most one waiter, so a second for-await loop over events would silently steal events from whichever consumer already reads it). Omit for a caller with no use for it, exactly today's behaviour. */
116
- onPeerAdvert?: (advert: PeerAdvert) => void): MeshSession;
116
+ onPeerAdvert?: (advert: PeerAdvert) => void,
117
+ /** Fired once per frame received on this session's connection, with the Connection it arrived on: a new Connection after a reconnect is how a caller notices it has to announce itself again, and a core/data reply is how it receives a log it asked a hub for. Omit for a caller with no use for it, exactly today's behaviour. */
118
+ onFrame?: (connection: Readonly<Connection>, frame: Frame) => void | Promise<void>): MeshSession;
117
119
  /** A MeshSession built over a connection that already exists (a Transport's own listen() handed it to onConnection), extended with the one thing a dial-side session can't offer: the device-id of the specific peer at the other end. Unlike createMeshSession, which may end up talking to a relay gossiping about many devices at once, an accepted connection is the agent-comms case -- exactly two peers, directly connected -- so "the peer" is well-defined here in a way it structurally isn't for the dial side. */
118
120
  export interface AcceptedMeshSession extends MeshSession {
119
121
  /** Resolves with the device-id carried by the first peer-advert this connection's remote sends -- the same self-advertisement mechanism createMeshSession's own directory already relies on for every peer, just narrowed to "the one peer this specific connection is with" rather than accumulated into a directory of possibly many. There is no transport-level authentication behind this yet (see wire-mesh#45's own createTlsTransport item): it is only as trustworthy as the remote's own gossip, exactly the same trust level the dial-side directory already has for every entry in it. */
@@ -113,7 +113,9 @@ export declare function createMeshSession(transport: Readonly<Transport>, identi
113
113
  /** This node's own directly-reachable "host:port" candidates (wire-mesh#38), advertised in this session's self-advert so other peers can attempt a direct connection instead of always falling back to a relay. Omit (or pass none) for a caller with nothing to offer, e.g. a browser client. */
114
114
  addresses?: readonly string[],
115
115
  /** Fired for every peer-advert entry as it's applied to the directory, regardless of source -- the hook a gossip-expansion consumer (wire-mesh#187, gossip-expansion.ts's own createGossipExpansion) uses to observe newly-gossiped peers without becoming a second, competing consumer of this session's own single-reader events stream (each emitted SessionEvent wakes at most one waiter, so a second for-await loop over events would silently steal events from whichever consumer already reads it). Omit for a caller with no use for it, exactly today's behaviour. */
116
- onPeerAdvert?: (advert: PeerAdvert) => void): MeshSession;
116
+ onPeerAdvert?: (advert: PeerAdvert) => void,
117
+ /** Fired once per frame received on this session's connection, with the Connection it arrived on: a new Connection after a reconnect is how a caller notices it has to announce itself again, and a core/data reply is how it receives a log it asked a hub for. Omit for a caller with no use for it, exactly today's behaviour. */
118
+ onFrame?: (connection: Readonly<Connection>, frame: Frame) => void | Promise<void>): MeshSession;
117
119
  /** A MeshSession built over a connection that already exists (a Transport's own listen() handed it to onConnection), extended with the one thing a dial-side session can't offer: the device-id of the specific peer at the other end. Unlike createMeshSession, which may end up talking to a relay gossiping about many devices at once, an accepted connection is the agent-comms case -- exactly two peers, directly connected -- so "the peer" is well-defined here in a way it structurally isn't for the dial side. */
118
120
  export interface AcceptedMeshSession extends MeshSession {
119
121
  /** Resolves with the device-id carried by the first peer-advert this connection's remote sends -- the same self-advertisement mechanism createMeshSession's own directory already relies on for every peer, just narrowed to "the one peer this specific connection is with" rather than accumulated into a directory of possibly many. There is no transport-level authentication behind this yet (see wire-mesh#45's own createTlsTransport item): it is only as trustworthy as the remote's own gossip, exactly the same trust level the dial-side directory already has for every entry in it. */
@@ -779,8 +779,8 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
779
779
  }
780
780
  };
781
781
  }
782
- function createMeshSession(transport, identity, clock = { now: () => Date.now() }, reconnect = null, addresses = [], onPeerAdvert) {
783
- const { session } = createSessionCore(identity, clock, reconnect, async (address) => transport.connect(address), onPeerAdvert, addresses);
782
+ function createMeshSession(transport, identity, clock = { now: () => Date.now() }, reconnect = null, addresses = [], onPeerAdvert, onFrame) {
783
+ const { session } = createSessionCore(identity, clock, reconnect, async (address) => transport.connect(address), onPeerAdvert, addresses, onFrame);
784
784
  return session;
785
785
  }
786
786
  /** Wires an already-accepted Connection up as a full MeshSession, mirroring exactly what createMeshSession's own dial path does once a connection exists (send handshake, send self-advert, negotiate, consume frames) -- the wire-mesh#45 prerequisite agent-comms needs, since its peers both listen and dial rather than only ever dialing the way web-console's own console UI does. Reconnect does not apply here: if this connection drops, only the remote redialing and being accepted again produces a new connection, and therefore a new session -- there is nothing on this side to retry. */
@@ -56,13 +56,13 @@ function createNoticeBoard(options) {
56
56
  entries: [entry]
57
57
  })).ok) throw new Error(`gap rejecting entry for peer log (head ${String(fromSeq)})`);
58
58
  }
59
- async function readLog(peer, room) {
59
+ async function readLog(peer, room, dmRootPolicy) {
60
60
  const entries = await require_domain_data_sync.readEntries(storage, peer, 0);
61
61
  const out = [];
62
- for (const entry of entries) out.push(await readOne(entry, room));
62
+ for (const entry of entries) out.push(await readOne(entry, room, dmRootPolicy));
63
63
  return out;
64
64
  }
65
- async function readOne(entry, room) {
65
+ async function readOne(entry, room, dmRootPolicy) {
66
66
  const notice = decodeNotice(entry);
67
67
  if (notice === void 0) return {
68
68
  verified: false,
@@ -72,7 +72,8 @@ function createNoticeBoard(options) {
72
72
  identity,
73
73
  clock,
74
74
  revocation,
75
- expectedRoom: room
75
+ expectedRoom: room,
76
+ dmRootPolicy
76
77
  });
77
78
  if (!verdict.ok) return {
78
79
  verified: false,
@@ -119,8 +120,8 @@ function createNoticeBoard(options) {
119
120
  return {
120
121
  postEncryptedNotice,
121
122
  ingestPeerEntry,
122
- readOwnNotices: async (room) => readLog(identity.deviceId, room),
123
- readPeerNotices: async (peer, room) => readLog(peer, room)
123
+ readOwnNotices: async (room) => readLog(identity.deviceId, room, "either-participant"),
124
+ readPeerNotices: async (peer, room) => readLog(peer, room, "self")
124
125
  };
125
126
  }
126
127
  //#endregion
@@ -55,13 +55,13 @@ function createNoticeBoard(options) {
55
55
  entries: [entry]
56
56
  })).ok) throw new Error(`gap rejecting entry for peer log (head ${String(fromSeq)})`);
57
57
  }
58
- async function readLog(peer, room) {
58
+ async function readLog(peer, room, dmRootPolicy) {
59
59
  const entries = await readEntries(storage, peer, 0);
60
60
  const out = [];
61
- for (const entry of entries) out.push(await readOne(entry, room));
61
+ for (const entry of entries) out.push(await readOne(entry, room, dmRootPolicy));
62
62
  return out;
63
63
  }
64
- async function readOne(entry, room) {
64
+ async function readOne(entry, room, dmRootPolicy) {
65
65
  const notice = decodeNotice(entry);
66
66
  if (notice === void 0) return {
67
67
  verified: false,
@@ -71,7 +71,8 @@ function createNoticeBoard(options) {
71
71
  identity,
72
72
  clock,
73
73
  revocation,
74
- expectedRoom: room
74
+ expectedRoom: room,
75
+ dmRootPolicy
75
76
  });
76
77
  if (!verdict.ok) return {
77
78
  verified: false,
@@ -118,8 +119,8 @@ function createNoticeBoard(options) {
118
119
  return {
119
120
  postEncryptedNotice,
120
121
  ingestPeerEntry,
121
- readOwnNotices: async (room) => readLog(identity.deviceId, room),
122
- readPeerNotices: async (peer, room) => readLog(peer, room)
122
+ readOwnNotices: async (room) => readLog(identity.deviceId, room, "either-participant"),
123
+ readPeerNotices: async (peer, room) => readLog(peer, room, "self")
123
124
  };
124
125
  }
125
126
  //#endregion
@@ -9,7 +9,7 @@ import { a as RevocationCheck } from "../tokens-DqFu7iMX.cjs";
9
9
  */
10
10
  export declare function buildRoomRekeyCommand(keyEpoch: number, wrappedKey: Uint8Array | readonly Uint8Array[]): ManageCommand;
11
11
  /** Sends a room.rekey for roomPath. targetDevice/token forward directly to MeshSession.sendManageRequest's own identically-named parameters, exactly as sendRoomMessage's own doc comment already describes. */
12
- export declare function sendRoomRekey(session: Readonly<MeshSession>, roomPath: string, keyEpoch: number, wrappedKey: Uint8Array | readonly Uint8Array[], targetDevice?: DeviceId, token?: CapabilityToken): Promise<ManageOutcome>;
12
+ export declare function sendRoomRekey(session: Readonly<Pick<MeshSession, "sendManageRequest">>, roomPath: string, keyEpoch: number, wrappedKey: Uint8Array | readonly Uint8Array[], targetDevice?: DeviceId, token?: CapabilityToken): Promise<ManageOutcome>;
13
13
  /** One incoming, fully-verified and unwrapped room.rekey, surfaced for the domain to store (index content keys by epoch for later room-notice decryption). */
14
14
  export interface RoomRekeyEvent {
15
15
  room: string;
@@ -9,7 +9,7 @@ import { a as RevocationCheck } from "../tokens-CBiSYuzp.mjs";
9
9
  */
10
10
  export declare function buildRoomRekeyCommand(keyEpoch: number, wrappedKey: Uint8Array | readonly Uint8Array[]): ManageCommand;
11
11
  /** Sends a room.rekey for roomPath. targetDevice/token forward directly to MeshSession.sendManageRequest's own identically-named parameters, exactly as sendRoomMessage's own doc comment already describes. */
12
- export declare function sendRoomRekey(session: Readonly<MeshSession>, roomPath: string, keyEpoch: number, wrappedKey: Uint8Array | readonly Uint8Array[], targetDevice?: DeviceId, token?: CapabilityToken): Promise<ManageOutcome>;
12
+ export declare function sendRoomRekey(session: Readonly<Pick<MeshSession, "sendManageRequest">>, roomPath: string, keyEpoch: number, wrappedKey: Uint8Array | readonly Uint8Array[], targetDevice?: DeviceId, token?: CapabilityToken): Promise<ManageOutcome>;
13
13
  /** One incoming, fully-verified and unwrapped room.rekey, surfaced for the domain to store (index content keys by epoch for later room-notice decryption). */
14
14
  export interface RoomRekeyEvent {
15
15
  room: string;
@@ -102,7 +102,8 @@ async function verifyRoomNotice(notice, options) {
102
102
  clock: options.clock,
103
103
  revocation: options.revocation,
104
104
  expectedBearer: claims.poster,
105
- roomPath: claims.room
105
+ roomPath: claims.room,
106
+ ...options.dmRootPolicy !== void 0 ? { dmRootPolicy: options.dmRootPolicy } : {}
106
107
  });
107
108
  if (!tokenVerdict.ok) return {
108
109
  ok: false,
@@ -20,6 +20,10 @@ export interface VerifyRoomNoticeOptions {
20
20
  * When given, refuses any notice not claiming exactly this room -- "is this the room I actually asked to read", checked against the notice's own self-declared `room` field before any cryptographic work, so a caller scanning a mixed stream of notices can cheaply skip ones for other rooms. Independent of, and layered on top of, this function's own unconditional internal self-consistency check (the embedded token's scope.path MUST equal the notice's own `room` field regardless of whether expectedRoom is given at all) -- a notice can be internally self-consistent yet still be for a room other than the one a caller expected, and this is the option that catches that case. Mirrors verifyCapabilityToken's own optional expectedBearer for the same "also assert it matches what I expected" shape.
21
21
  */
22
22
  expectedRoom?: RoomPath;
23
+ /**
24
+ * How a DM's embedded token must be rooted, passed to verifyRoomToken (see its own `dmRootPolicy`). The default requires the root to be the verifying identity, which is right for a notice another device posted to me. A device reading its own notice holds a token rooted at the other participant, so only "either-participant" can pass there; it is safe because nothing about an own notice rests on the root rule, the device wrote it.
25
+ */
26
+ dmRootPolicy?: "self" | "either-participant";
23
27
  }
24
28
  /**
25
29
  * Verifies one room-notice (spec/room.cddl) against every obligation its own "Six verifier obligations" comment documents, including `valid-until` (obligation 6): the envelope is a well-formed COSE_Sign1 whose signature verifies against its own embedded poster-key, and poster-key is self-certifying (sha256(poster-key.public-key) equals poster); the embedded token independently passes every ordinary capability-token obligation (signature, expiry, not-before, revocation, delegations- remaining, and -- since the embedded token is itself room-scoped -- the chain-root and scope obligations spec/room.cddl's own six general verifier obligations require of any room:member token) with its bearer pinned to this notice's own `poster` field and its scope pinned to this notice's own `room` field; and, if present, `valid-until` has not yet elapsed as of the injected clock. Checks run cheapest-and-structural first, signature next, the recursive token-chain verification last, mirroring tokens.ts's own ordering discipline.
@@ -20,6 +20,10 @@ export interface VerifyRoomNoticeOptions {
20
20
  * When given, refuses any notice not claiming exactly this room -- "is this the room I actually asked to read", checked against the notice's own self-declared `room` field before any cryptographic work, so a caller scanning a mixed stream of notices can cheaply skip ones for other rooms. Independent of, and layered on top of, this function's own unconditional internal self-consistency check (the embedded token's scope.path MUST equal the notice's own `room` field regardless of whether expectedRoom is given at all) -- a notice can be internally self-consistent yet still be for a room other than the one a caller expected, and this is the option that catches that case. Mirrors verifyCapabilityToken's own optional expectedBearer for the same "also assert it matches what I expected" shape.
21
21
  */
22
22
  expectedRoom?: RoomPath;
23
+ /**
24
+ * How a DM's embedded token must be rooted, passed to verifyRoomToken (see its own `dmRootPolicy`). The default requires the root to be the verifying identity, which is right for a notice another device posted to me. A device reading its own notice holds a token rooted at the other participant, so only "either-participant" can pass there; it is safe because nothing about an own notice rests on the root rule, the device wrote it.
25
+ */
26
+ dmRootPolicy?: "self" | "either-participant";
23
27
  }
24
28
  /**
25
29
  * Verifies one room-notice (spec/room.cddl) against every obligation its own "Six verifier obligations" comment documents, including `valid-until` (obligation 6): the envelope is a well-formed COSE_Sign1 whose signature verifies against its own embedded poster-key, and poster-key is self-certifying (sha256(poster-key.public-key) equals poster); the embedded token independently passes every ordinary capability-token obligation (signature, expiry, not-before, revocation, delegations- remaining, and -- since the embedded token is itself room-scoped -- the chain-root and scope obligations spec/room.cddl's own six general verifier obligations require of any room:member token) with its bearer pinned to this notice's own `poster` field and its scope pinned to this notice's own `room` field; and, if present, `valid-until` has not yet elapsed as of the injected clock. Checks run cheapest-and-structural first, signature next, the recursive token-chain verification last, mirroring tokens.ts's own ordering discipline.
@@ -101,7 +101,8 @@ async function verifyRoomNotice(notice, options) {
101
101
  clock: options.clock,
102
102
  revocation: options.revocation,
103
103
  expectedBearer: claims.poster,
104
- roomPath: claims.room
104
+ roomPath: claims.room,
105
+ ...options.dmRootPolicy !== void 0 ? { dmRootPolicy: options.dmRootPolicy } : {}
105
106
  });
106
107
  if (!tokenVerdict.ok) return {
107
108
  ok: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wire-mesh-core",
3
- "version": "3.6.1",
3
+ "version": "3.7.0",
4
4
  "dependencies": {
5
5
  "cbor2": "2.3.0",
6
6
  "cddl.js": "1.0.1",