wire-mesh-core 4.0.8 → 4.1.1
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.
- package/dist/adapters/node-identity.cjs +16 -7
- package/dist/adapters/node-identity.d.cts +1 -1
- package/dist/adapters/node-identity.d.mts +1 -1
- package/dist/adapters/node-identity.mjs +16 -7
- package/dist/adapters/threshold-identity.d.cts +1 -1
- package/dist/adapters/threshold-identity.d.mts +1 -1
- package/dist/domain/capability-grant.d.cts +1 -1
- package/dist/domain/capability-grant.d.mts +1 -1
- package/dist/domain/capability-request.d.cts +1 -1
- package/dist/domain/capability-request.d.mts +1 -1
- package/dist/domain/channel-exhausted-error.cjs +11 -0
- package/dist/domain/channel-exhausted-error.d.cts +6 -0
- package/dist/domain/channel-exhausted-error.d.mts +6 -0
- package/dist/domain/channel-exhausted-error.mjs +10 -0
- package/dist/domain/data-sync.d.cts +1 -1
- package/dist/domain/data-sync.d.mts +1 -1
- package/dist/domain/handshake.cjs +9 -0
- package/dist/domain/handshake.d.cts +2 -0
- package/dist/domain/handshake.d.mts +2 -0
- package/dist/domain/handshake.mjs +9 -1
- package/dist/domain/mesh-session.cjs +112 -48
- package/dist/domain/mesh-session.d.cts +4 -2
- package/dist/domain/mesh-session.d.mts +4 -2
- package/dist/domain/mesh-session.mjs +111 -47
- package/dist/domain/notice-board.d.cts +1 -1
- package/dist/domain/notice-board.d.mts +1 -1
- package/dist/domain/peer-advert.d.cts +1 -1
- package/dist/domain/peer-advert.d.mts +1 -1
- package/dist/domain/room-rekey.d.cts +1 -1
- package/dist/domain/room-rekey.d.mts +1 -1
- package/dist/domain/room.d.cts +1 -1
- package/dist/domain/room.d.mts +1 -1
- package/dist/domain/tokens.cjs +8 -0
- package/dist/domain/tokens.d.cts +4 -2
- package/dist/domain/tokens.d.mts +4 -2
- package/dist/domain/tokens.mjs +9 -1
- package/dist/generated/protocol.cjs +8 -8
- package/dist/generated/protocol.mjs +8 -8
- package/dist/{identity-EV-HHv_D.d.mts → identity-BioGOV9M.d.mts} +10 -1
- package/dist/{identity-B6nhlSOT.d.cts → identity-C5jq53NV.d.cts} +10 -1
- package/dist/ports/identity.cjs +13 -0
- package/dist/ports/identity.d.cts +2 -2
- package/dist/ports/identity.d.mts +2 -2
- package/dist/ports/identity.mjs +12 -1
- package/package.json +5 -1
- package/wasm-dist/wire_mesh_threshold_wasm_bg.wasm +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { D as DeviceId, E as DataRequestFrame, N as Frame, T as DataHaveFrame, U as ManageError, V as ManageCommand, W as ManageOk, bt as RevocationEntry, et as PeerAdvert, f as CapabilityScope, g as CoordinatorFrame, lt as ProtocolVersion, nn as TopologyPeers, p as CapabilityToken, w as DataEntriesFrame } from "../protocol-DzmpxxUr.cjs";
|
|
2
2
|
import { r as Transport, t as Connection } from "../transport-gDbSHO-r.cjs";
|
|
3
|
-
import {
|
|
3
|
+
import { r as IdentityPort } from "../identity-C5jq53NV.cjs";
|
|
4
4
|
import { t as Clock } from "../clock-DhUPRfLh.cjs";
|
|
5
5
|
//#region src/domain/mesh-session.d.ts
|
|
6
6
|
/** How long to wait for the node's handshake before calling it unanswered. A relay-only node never sends one; that is a state to display, not an error. */
|
|
@@ -136,7 +136,9 @@ addresses?: readonly string[],
|
|
|
136
136
|
/** 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. */
|
|
137
137
|
onPeerAdvert?: (advert: PeerAdvert) => void,
|
|
138
138
|
/** 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. */
|
|
139
|
-
onFrame?: (connection: Readonly<Connection>, frame: Frame) => void | Promise<void
|
|
139
|
+
onFrame?: (connection: Readonly<Connection>, frame: Frame) => void | Promise<void>,
|
|
140
|
+
/** Told of a failure that is local to this session and did not end it: an identity port that failed while checking a hello from a peer. The hello is dropped and the peer is told nothing. Omit for a caller with no use for it: the failure is then not reported anywhere. */
|
|
141
|
+
onLocalFault?: (error: unknown) => void): MeshSession;
|
|
140
142
|
/** 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. */
|
|
141
143
|
export interface AcceptedMeshSession extends MeshSession {
|
|
142
144
|
/** 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. */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { D as DeviceId, E as DataRequestFrame, N as Frame, T as DataHaveFrame, U as ManageError, V as ManageCommand, W as ManageOk, bt as RevocationEntry, et as PeerAdvert, f as CapabilityScope, g as CoordinatorFrame, lt as ProtocolVersion, nn as TopologyPeers, p as CapabilityToken, w as DataEntriesFrame } from "../protocol-DzmpxxUr.mjs";
|
|
2
2
|
import { r as Transport, t as Connection } from "../transport-bD3p_TO_.mjs";
|
|
3
|
-
import {
|
|
3
|
+
import { r as IdentityPort } from "../identity-BioGOV9M.mjs";
|
|
4
4
|
import { t as Clock } from "../clock-DhUPRfLh.mjs";
|
|
5
5
|
//#region src/domain/mesh-session.d.ts
|
|
6
6
|
/** How long to wait for the node's handshake before calling it unanswered. A relay-only node never sends one; that is a state to display, not an error. */
|
|
@@ -136,7 +136,9 @@ addresses?: readonly string[],
|
|
|
136
136
|
/** 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. */
|
|
137
137
|
onPeerAdvert?: (advert: PeerAdvert) => void,
|
|
138
138
|
/** 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. */
|
|
139
|
-
onFrame?: (connection: Readonly<Connection>, frame: Frame) => void | Promise<void
|
|
139
|
+
onFrame?: (connection: Readonly<Connection>, frame: Frame) => void | Promise<void>,
|
|
140
|
+
/** Told of a failure that is local to this session and did not end it: an identity port that failed while checking a hello from a peer. The hello is dropped and the peer is told nothing. Omit for a caller with no use for it: the failure is then not reported anywhere. */
|
|
141
|
+
onLocalFault?: (error: unknown) => void): MeshSession;
|
|
140
142
|
/** 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. */
|
|
141
143
|
export interface AcceptedMeshSession extends MeshSession {
|
|
142
144
|
/** 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. */
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { messageFromFrame, tryDecodeFrame, wrapRelayData } from "../adapters/frame-codec.mjs";
|
|
2
|
+
import { IdentityKeyError } from "../ports/identity.mjs";
|
|
2
3
|
import { deviceIdToHex } from "./device-id.mjs";
|
|
3
4
|
import { bytesEqual } from "./token-scope.mjs";
|
|
4
5
|
import { a as OWN_VERSION, i as CORE_VERSION_GOSSIP_KEY, n as TOPOLOGY_PEERS_GOSSIP_KEY, o as isVersionGetCommand, r as validateGossipExtensions, s as versionGetOutcome } from "../gossip-extensions-B6WJjkAr.mjs";
|
|
5
6
|
import { signPeerAdvert, verifyPeerAdvert } from "./peer-advert.mjs";
|
|
6
7
|
import { createAsyncQueue } from "./async-queue.mjs";
|
|
7
|
-
import {
|
|
8
|
+
import { ChannelExhaustedError } from "./channel-exhausted-error.mjs";
|
|
9
|
+
import { localHandshake, negotiate } from "./handshake.mjs";
|
|
8
10
|
import { isDataFrame } from "./hub-mailbox.mjs";
|
|
9
11
|
import { createPingRoundTrips } from "./ping-round-trips.mjs";
|
|
10
12
|
import { createTopologySnapshotTracker } from "./topology-snapshot.mjs";
|
|
@@ -26,6 +28,19 @@ function createRelayPairings() {
|
|
|
26
28
|
};
|
|
27
29
|
}
|
|
28
30
|
//#endregion
|
|
31
|
+
//#region src/domain/bounded-log.ts
|
|
32
|
+
/** A log that keeps only its most recent `window` entries: newest at the end, the oldest dropped once the window is full. Pure and closure-free, split out of mesh-session.ts to keep that file under this repo's max-lines lint budget; nothing here is session-shaped. */
|
|
33
|
+
function createBoundedLog(window) {
|
|
34
|
+
const log = [];
|
|
35
|
+
return {
|
|
36
|
+
record: (entry) => {
|
|
37
|
+
log.push(entry);
|
|
38
|
+
if (log.length > window) log.splice(0, log.length - window);
|
|
39
|
+
},
|
|
40
|
+
entries: () => [...log]
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
//#endregion
|
|
29
44
|
//#region src/domain/secure-channel.ts
|
|
30
45
|
/**
|
|
31
46
|
* The end-to-end channel two devices open between themselves inside a relay pairing (spec/secure-channel.cddl). A relay only forwards the bytes of a `relay-data` frame, and without this it could read them, alter them, and say who sent them. Here the two devices authenticate each other's identity keys over a fresh ECDH exchange, derive one AES-256-GCM key per direction, and seal every frame they exchange so the relay sees ciphertext and every receiver learns the sender's identity from the handshake and nothing a relay asserts.
|
|
@@ -95,16 +110,16 @@ async function beginHandshake(identity, peer) {
|
|
|
95
110
|
};
|
|
96
111
|
}
|
|
97
112
|
/**
|
|
98
|
-
* Checks a received hello:
|
|
113
|
+
* Checks a received hello: it must be addressed to this device, its identity key must be a usable key, and its signature must verify under that key over the input spec/secure-channel.cddl defines. A hostile hello gets a verdict. An error from the identity port that is not an IdentityKeyError is a failure of the port, not a verdict about the peer, and propagates.
|
|
99
114
|
*/
|
|
100
115
|
async function verifyHello(identity, hello) {
|
|
101
116
|
const key = hello["identity-key"];
|
|
117
|
+
const sender = await identity.deriveDeviceId(key["public-key"]);
|
|
118
|
+
if (!bytesEqual(hello["to-device"], identity.deviceId)) return {
|
|
119
|
+
ok: false,
|
|
120
|
+
reason: "wrong_recipient"
|
|
121
|
+
};
|
|
102
122
|
try {
|
|
103
|
-
const sender = await identity.deriveDeviceId(key["public-key"]);
|
|
104
|
-
if (!bytesEqual(hello["to-device"], identity.deviceId)) return {
|
|
105
|
-
ok: false,
|
|
106
|
-
reason: "wrong_recipient"
|
|
107
|
-
};
|
|
108
123
|
return await identity.verify(key, helloSigningInput(hello["ephemeral-key"], hello.nonce, sender, identity.deviceId), hello.signature) ? {
|
|
109
124
|
ok: true,
|
|
110
125
|
peer: {
|
|
@@ -115,13 +130,30 @@ async function verifyHello(identity, hello) {
|
|
|
115
130
|
ok: false,
|
|
116
131
|
reason: "bad_signature"
|
|
117
132
|
};
|
|
118
|
-
} catch {
|
|
133
|
+
} catch (error) {
|
|
134
|
+
if (!(error instanceof IdentityKeyError)) throw error;
|
|
119
135
|
return {
|
|
120
136
|
ok: false,
|
|
121
|
-
reason: "
|
|
137
|
+
reason: error.fault === "unsupported_alg" ? "unsupported_identity_alg" : "invalid_identity_key"
|
|
122
138
|
};
|
|
123
139
|
}
|
|
124
140
|
}
|
|
141
|
+
/** The largest counter a channel uses: the counter is an unsigned 64-bit integer on the wire, and this implementation holds it as a JavaScript number, exact up to this value. A frame above it is refused at decoding, never truncated, and sealing refuses to go past it, never wraps. */
|
|
142
|
+
const MAX_COUNTER = Number.MAX_SAFE_INTEGER;
|
|
143
|
+
/** Throws a RangeError unless `counter` is a counter a channel can start from: a non-negative safe integer. */
|
|
144
|
+
function assertFirstSendCounter(counter) {
|
|
145
|
+
if (!Number.isSafeInteger(counter) || counter < 0) throw new RangeError("firstSendCounter must be a non-negative safe integer");
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Takes the next send counter from `next`, the counter the next seal would use: returns it with the counter that follows. Throws once `next` is past MAX_COUNTER, since a counter that cannot be held exactly would repeat an IV under the same key.
|
|
149
|
+
*/
|
|
150
|
+
function claimSendCounter(next) {
|
|
151
|
+
if (next > MAX_COUNTER) throw new ChannelExhaustedError();
|
|
152
|
+
return {
|
|
153
|
+
counter: next,
|
|
154
|
+
next: next + 1
|
|
155
|
+
};
|
|
156
|
+
}
|
|
125
157
|
function ivFor(counter) {
|
|
126
158
|
const iv = new Uint8Array(GCM_IV_BYTE_LENGTH);
|
|
127
159
|
iv.set(counterBytes(counter), IV_COUNTER_OFFSET);
|
|
@@ -143,11 +175,10 @@ async function deriveDirectionKey({ secret, salt, low, high, label }) {
|
|
|
143
175
|
}, false, ["encrypt", "decrypt"]);
|
|
144
176
|
}
|
|
145
177
|
/**
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
* `peer` is the device verifyHello authenticated for `remote`, and `pending` is this side's own half, whether the handshake was begun here or in answer to that hello.
|
|
178
|
+
* establishChannel with its inputs in one object, and the one extra that tests need: where this end's send counter starts. This module is not a published entry (it is in tsdown.config.ts's INTERNAL_MODULES) and the session never passes `firstSendCounter`, so every real channel starts at 0; a test passes a counter near MAX_COUNTER to reach the bound without sealing 2^53 frames.
|
|
149
179
|
*/
|
|
150
|
-
async function
|
|
180
|
+
async function establishChannelFrom(identity, { pending, remote, peer, firstSendCounter = 0 }) {
|
|
181
|
+
assertFirstSendCounter(firstSendCounter);
|
|
151
182
|
let secret;
|
|
152
183
|
try {
|
|
153
184
|
const remoteKey = await crypto.subtle.importKey("raw", Uint8Array.from(remote["ephemeral-key"]), ECDH_PARAMS, false, []);
|
|
@@ -178,7 +209,7 @@ async function establishChannel(identity, pending, remote, peer) {
|
|
|
178
209
|
});
|
|
179
210
|
const sendKey = localIsLow ? lowToHigh : highToLow;
|
|
180
211
|
const receiveKey = localIsLow ? highToLow : lowToHigh;
|
|
181
|
-
let nextSendCounter =
|
|
212
|
+
let nextSendCounter = firstSendCounter;
|
|
182
213
|
let highestReceived = -1;
|
|
183
214
|
let sealing = Promise.resolve();
|
|
184
215
|
let opening = Promise.resolve();
|
|
@@ -194,7 +225,10 @@ async function establishChannel(identity, pending, remote, peer) {
|
|
|
194
225
|
};
|
|
195
226
|
}
|
|
196
227
|
async function openOne(data) {
|
|
197
|
-
if (data.counter <= highestReceived) return
|
|
228
|
+
if (data.counter <= highestReceived) return {
|
|
229
|
+
ok: false,
|
|
230
|
+
reason: "replayed"
|
|
231
|
+
};
|
|
198
232
|
let plaintext;
|
|
199
233
|
try {
|
|
200
234
|
plaintext = new Uint8Array(await crypto.subtle.decrypt({
|
|
@@ -203,30 +237,47 @@ async function establishChannel(identity, pending, remote, peer) {
|
|
|
203
237
|
additionalData: associatedData(peer.deviceId, identity.deviceId, data.counter)
|
|
204
238
|
}, receiveKey, Uint8Array.from(data.ciphertext)));
|
|
205
239
|
} catch {
|
|
206
|
-
return
|
|
240
|
+
return {
|
|
241
|
+
ok: false,
|
|
242
|
+
reason: "unauthentic"
|
|
243
|
+
};
|
|
207
244
|
}
|
|
208
245
|
highestReceived = Math.max(highestReceived, data.counter);
|
|
209
|
-
|
|
246
|
+
const frame = tryDecodeFrame(plaintext);
|
|
247
|
+
return frame === null ? {
|
|
248
|
+
ok: false,
|
|
249
|
+
reason: "undecodable"
|
|
250
|
+
} : {
|
|
251
|
+
ok: true,
|
|
252
|
+
frame
|
|
253
|
+
};
|
|
210
254
|
}
|
|
255
|
+
const openOutcome = async (data) => {
|
|
256
|
+
const opened = opening.then(async () => openOne(data));
|
|
257
|
+
opening = opened.catch(() => void 0);
|
|
258
|
+
return opened;
|
|
259
|
+
};
|
|
211
260
|
return {
|
|
212
261
|
peer,
|
|
213
262
|
seal: async (frame) => {
|
|
214
|
-
const
|
|
215
|
-
|
|
263
|
+
const claimed = claimSendCounter(nextSendCounter);
|
|
264
|
+
const counter = claimed.counter;
|
|
265
|
+
nextSendCounter = claimed.next;
|
|
216
266
|
const sealed = sealing.then(async () => sealAt(frame, counter));
|
|
217
267
|
sealing = sealed.catch(() => void 0);
|
|
218
268
|
return sealed;
|
|
219
269
|
},
|
|
220
270
|
open: async (data) => {
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
271
|
+
const outcome = await openOutcome(data);
|
|
272
|
+
return outcome.ok ? outcome.frame : void 0;
|
|
273
|
+
},
|
|
274
|
+
openOutcome
|
|
225
275
|
};
|
|
226
276
|
}
|
|
227
277
|
//#endregion
|
|
228
278
|
//#region src/domain/relay-channels.ts
|
|
229
|
-
function createRelayChannels(identity, sendHello) {
|
|
279
|
+
function createRelayChannels(identity, sendHello, options = {}) {
|
|
280
|
+
if (options.firstSendCounter !== void 0) assertFirstSendCounter(options.firstSendCounter);
|
|
230
281
|
const channels = /* @__PURE__ */ new Map();
|
|
231
282
|
/** Registers the peer as awaiting at once, so a hello arriving meanwhile completes this handshake instead of starting a second, then sends this side's hello. */
|
|
232
283
|
function start(peer) {
|
|
@@ -252,17 +303,40 @@ function createRelayChannels(identity, sendHello) {
|
|
|
252
303
|
return (existing ?? start(peer)).settle.promise;
|
|
253
304
|
},
|
|
254
305
|
applyHello: async (hello) => {
|
|
255
|
-
|
|
306
|
+
let verdict;
|
|
307
|
+
try {
|
|
308
|
+
verdict = await verifyHello(identity, hello);
|
|
309
|
+
} catch (error) {
|
|
310
|
+
options.onFault?.(error);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
256
313
|
if (!verdict.ok) return;
|
|
257
314
|
const key = deviceIdToHex(verdict.peer.deviceId);
|
|
258
315
|
const current = channels.get(key);
|
|
259
316
|
const entry = current?.status === "awaiting" ? current : start(verdict.peer.deviceId);
|
|
260
|
-
const
|
|
261
|
-
|
|
317
|
+
const established = await establishChannelFrom(identity, {
|
|
318
|
+
pending: await entry.pending,
|
|
319
|
+
remote: hello,
|
|
320
|
+
peer: verdict.peer,
|
|
321
|
+
...options.firstSendCounter === void 0 ? {} : { firstSendCounter: options.firstSendCounter }
|
|
322
|
+
});
|
|
323
|
+
if (established === void 0) {
|
|
262
324
|
entry.settle.reject(/* @__PURE__ */ new Error("the peer sent an unusable ephemeral key"));
|
|
263
325
|
if (channels.get(key) === entry) channels.delete(key);
|
|
264
326
|
return;
|
|
265
327
|
}
|
|
328
|
+
const channel = {
|
|
329
|
+
...established,
|
|
330
|
+
seal: async (frame) => {
|
|
331
|
+
try {
|
|
332
|
+
return await established.seal(frame);
|
|
333
|
+
} catch (error) {
|
|
334
|
+
const held = channels.get(key);
|
|
335
|
+
if (error instanceof ChannelExhaustedError && held?.status === "established" && held.channel === channel) channels.delete(key);
|
|
336
|
+
throw error;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
};
|
|
266
340
|
channels.set(key, {
|
|
267
341
|
status: "established",
|
|
268
342
|
channel
|
|
@@ -305,27 +379,16 @@ const HANDSHAKE_TIMEOUT_MS = 3e3;
|
|
|
305
379
|
* always-on node grows without limit.
|
|
306
380
|
*/
|
|
307
381
|
const FRAME_LOG_WINDOW = 256;
|
|
308
|
-
function localHandshake(domains) {
|
|
309
|
-
return {
|
|
310
|
-
type: "handshake",
|
|
311
|
-
version: 1,
|
|
312
|
-
domains: [...domains]
|
|
313
|
-
};
|
|
314
|
-
}
|
|
315
382
|
/**
|
|
316
383
|
* Builds the connection-agnostic session state machine and its public MeshSession surface. dial is null for a session that can never (re)connect on its own -- acceptMeshSession's case, where the one connection it will ever have already exists by construction and reconnect therefore cannot apply (only the remote redialing, and being accepted again, produces a fresh connection). createMeshSession supplies dial as transport.connect so its own connect()/reconnect behaviour is unchanged from before this was factored out.
|
|
317
384
|
*/
|
|
318
|
-
function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addresses = [], onFrame, onSessionEnd) {
|
|
385
|
+
function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addresses = [], onFrame, onSessionEnd, onLocalFault) {
|
|
319
386
|
let connection = null;
|
|
320
387
|
let state = { status: "idle" };
|
|
321
388
|
let handshake = { status: "pending" };
|
|
322
389
|
const directory = /* @__PURE__ */ new Map();
|
|
323
|
-
const frameLog =
|
|
324
|
-
|
|
325
|
-
function recordFrame(entry) {
|
|
326
|
-
frameLog.push(entry);
|
|
327
|
-
if (frameLog.length > 256) frameLog.splice(0, frameLog.length - 256);
|
|
328
|
-
}
|
|
390
|
+
const frameLog = createBoundedLog(256);
|
|
391
|
+
const recordFrame = frameLog.record;
|
|
329
392
|
let feedCancelled = false;
|
|
330
393
|
const eventQueue = createAsyncQueue();
|
|
331
394
|
let handshakeTimer = null;
|
|
@@ -340,7 +403,7 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
340
403
|
getRelayedDevices: () => relayPairings.list()
|
|
341
404
|
});
|
|
342
405
|
const pendingManageRequests = /* @__PURE__ */ new Map();
|
|
343
|
-
const
|
|
406
|
+
const sendHello = async (peer, hello) => {
|
|
344
407
|
if (connection === null) throw new Error("not connected");
|
|
345
408
|
recordFrame({
|
|
346
409
|
direction: "sent",
|
|
@@ -348,7 +411,8 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
348
411
|
});
|
|
349
412
|
await connection.send(wrapRelayData(hello, peer));
|
|
350
413
|
emit();
|
|
351
|
-
}
|
|
414
|
+
};
|
|
415
|
+
const relayChannels = createRelayChannels(identity, sendHello, { onFault: onLocalFault });
|
|
352
416
|
const incomingQueue = createAsyncQueue();
|
|
353
417
|
const revocationQueue = createAsyncQueue();
|
|
354
418
|
const coordinatorQueue = createAsyncQueue();
|
|
@@ -358,7 +422,7 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
358
422
|
return {
|
|
359
423
|
state,
|
|
360
424
|
directory: [...directory.values()],
|
|
361
|
-
frameLog:
|
|
425
|
+
frameLog: frameLog.entries()
|
|
362
426
|
};
|
|
363
427
|
}
|
|
364
428
|
function emit() {
|
|
@@ -433,7 +497,7 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
433
497
|
}
|
|
434
498
|
};
|
|
435
499
|
if (isVersionGetCommand(frame.command.params)) {
|
|
436
|
-
incoming.respond(versionGetOutcome());
|
|
500
|
+
incoming.respond(versionGetOutcome()).catch(() => void 0);
|
|
437
501
|
return;
|
|
438
502
|
}
|
|
439
503
|
incomingQueue.push(incoming);
|
|
@@ -827,8 +891,8 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
827
891
|
}
|
|
828
892
|
};
|
|
829
893
|
}
|
|
830
|
-
function createMeshSession(transport, identity, clock = { now: () => Date.now() }, reconnect = null, addresses = [], onPeerAdvert, onFrame) {
|
|
831
|
-
const { session } = createSessionCore(identity, clock, reconnect, async (address) => transport.connect(address), onPeerAdvert, addresses, onFrame);
|
|
894
|
+
function createMeshSession(transport, identity, clock = { now: () => Date.now() }, reconnect = null, addresses = [], onPeerAdvert, onFrame, onLocalFault) {
|
|
895
|
+
const { session } = createSessionCore(identity, clock, reconnect, async (address) => transport.connect(address), onPeerAdvert, addresses, onFrame, void 0, onLocalFault);
|
|
832
896
|
return session;
|
|
833
897
|
}
|
|
834
898
|
/** 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. */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { D as DeviceId, Ot as RoomNotice, T as DataHaveFrame, kt as RoomNoticeClaims, p as CapabilityToken } from "../protocol-DzmpxxUr.cjs";
|
|
2
2
|
import { t as KeyValueStorage } from "../storage-jbGFAYIV.cjs";
|
|
3
|
-
import {
|
|
3
|
+
import { r as IdentityPort } from "../identity-C5jq53NV.cjs";
|
|
4
4
|
import { t as Clock } from "../clock-DhUPRfLh.cjs";
|
|
5
5
|
import { RevocationCheck } from "./tokens.cjs";
|
|
6
6
|
import { RoomNoticeVerdictReason } from "./room.cjs";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { D as DeviceId, Ot as RoomNotice, T as DataHaveFrame, kt as RoomNoticeClaims, p as CapabilityToken } from "../protocol-DzmpxxUr.mjs";
|
|
2
2
|
import { t as KeyValueStorage } from "../storage-jbGFAYIV.mjs";
|
|
3
|
-
import {
|
|
3
|
+
import { r as IdentityPort } from "../identity-BioGOV9M.mjs";
|
|
4
4
|
import { t as Clock } from "../clock-DhUPRfLh.mjs";
|
|
5
5
|
import { RevocationCheck } from "./tokens.mjs";
|
|
6
6
|
import { RoomNoticeVerdictReason } from "./room.mjs";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { B as IdentityKey, D as DeviceId, et as PeerAdvert } from "../protocol-DzmpxxUr.cjs";
|
|
2
|
-
import {
|
|
2
|
+
import { r as IdentityPort } from "../identity-C5jq53NV.cjs";
|
|
3
3
|
//#region src/domain/peer-advert.d.ts
|
|
4
4
|
/**
|
|
5
5
|
* The most a verifier's verdict memo may charge across all its entries, in bytes of key.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { B as IdentityKey, D as DeviceId, et as PeerAdvert } from "../protocol-DzmpxxUr.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { r as IdentityPort } from "../identity-BioGOV9M.mjs";
|
|
3
3
|
//#region src/domain/peer-advert.d.ts
|
|
4
4
|
/**
|
|
5
5
|
* The most a verifier's verdict memo may charge across all its entries, in bytes of key.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { D as DeviceId, V as ManageCommand, p as CapabilityToken } from "../protocol-DzmpxxUr.cjs";
|
|
2
|
-
import {
|
|
2
|
+
import { r as IdentityPort } from "../identity-C5jq53NV.cjs";
|
|
3
3
|
import { t as Clock } from "../clock-DhUPRfLh.cjs";
|
|
4
4
|
import { IncomingManageRequest, ManageOutcome, MeshSession } from "./mesh-session.cjs";
|
|
5
5
|
import { RevocationCheck } from "./tokens.cjs";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { D as DeviceId, V as ManageCommand, p as CapabilityToken } from "../protocol-DzmpxxUr.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { r as IdentityPort } from "../identity-BioGOV9M.mjs";
|
|
3
3
|
import { t as Clock } from "../clock-DhUPRfLh.mjs";
|
|
4
4
|
import { IncomingManageRequest, ManageOutcome, MeshSession } from "./mesh-session.mjs";
|
|
5
5
|
import { RevocationCheck } from "./tokens.mjs";
|
package/dist/domain/room.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { At as RoomPath, Ot as RoomNotice, kt as RoomNoticeClaims, p as CapabilityToken, q as MessageRef } from "../protocol-DzmpxxUr.cjs";
|
|
2
|
-
import {
|
|
2
|
+
import { r as IdentityPort } from "../identity-C5jq53NV.cjs";
|
|
3
3
|
import { t as Clock } from "../clock-DhUPRfLh.cjs";
|
|
4
4
|
import { RevocationCheck } from "./tokens.cjs";
|
|
5
5
|
import { r as RoomTokenVerdictReason } from "../room-token-verification-Dh7SGJfK.cjs";
|
package/dist/domain/room.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { At as RoomPath, Ot as RoomNotice, kt as RoomNoticeClaims, p as CapabilityToken, q as MessageRef } from "../protocol-DzmpxxUr.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { r as IdentityPort } from "../identity-BioGOV9M.mjs";
|
|
3
3
|
import { t as Clock } from "../clock-DhUPRfLh.mjs";
|
|
4
4
|
import { RevocationCheck } from "./tokens.mjs";
|
|
5
5
|
import { r as RoomTokenVerdictReason } from "../room-token-verification-Hb1LkpFh.mjs";
|
package/dist/domain/tokens.cjs
CHANGED
|
@@ -374,6 +374,14 @@ async function canGrantVia(heldGrantCapability, deviceId, candidate, now) {
|
|
|
374
374
|
* When `parent` is given, every one of `tokens.cddl`'s own narrowing obligations is enforced here, at issuance, rather than left for the far end to discover minutes or hours later as a bare `delegation_exceeds_parent` from `verifyCapabilityToken` -- the same "fail loudly, fail early" reasoning that governs every other boundary in this codebase. An issuer minting an invalid delegation is a bug in the caller; this function refuses rather than producing a token indistinguishable from a valid one until someone else verifies it.
|
|
375
375
|
*/
|
|
376
376
|
async function mintCapabilityToken(options) {
|
|
377
|
+
for (const verb of [
|
|
378
|
+
options.capability,
|
|
379
|
+
options.grantsCapability,
|
|
380
|
+
options.requestsCapability
|
|
381
|
+
]) if (verb !== void 0 && !require_generated_protocol.capabilityVerbSchema.safeParse(verb).success) return {
|
|
382
|
+
ok: false,
|
|
383
|
+
reason: "invalid_capability"
|
|
384
|
+
};
|
|
377
385
|
if (options.expires <= options.clock.now()) return {
|
|
378
386
|
ok: false,
|
|
379
387
|
reason: "already_expired"
|
package/dist/domain/tokens.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { B as IdentityKey, D as DeviceId, bt as RevocationEntry, en as TokenClaims, p as CapabilityToken, yt as RevocationClaims } from "../protocol-DzmpxxUr.cjs";
|
|
2
|
-
import {
|
|
2
|
+
import { r as IdentityPort } from "../identity-C5jq53NV.cjs";
|
|
3
3
|
import { t as Clock } from "../clock-DhUPRfLh.cjs";
|
|
4
4
|
import { l as GrantSubject, m as TokenDelegateHandler, r as ConditionsContext, u as NarrowingCandidate } from "../token-predicates-lg4TtUxm.cjs";
|
|
5
5
|
import { PredicateNode } from "trilean";
|
|
@@ -67,7 +67,9 @@ export interface VerifyRevocationEntryOptions {
|
|
|
67
67
|
* Verifies one gossiped revocation-entry (management.cddl): a well-formed COSE_Sign1 whose signature verifies against its own embedded issuer-key, where that issuer-key is self-certifying (sha256(issuer-key.public-key) equals the claimed issuer device-id). A verifier that ingests a revocation-announce frame runs each entry through this before recording it in its revocation view; entries failing here are dropped, not stored. The issuer-match against a specific token's own issuer (only a token's own issuer may revoke it) is deliberately NOT checked here -- it happens at lookup time in RevocationCheck, against whichever token is being verified.
|
|
68
68
|
*/
|
|
69
69
|
export declare function verifyRevocationEntry(entry: RevocationEntry, options: VerifyRevocationEntryOptions): Promise<RevocationEntryVerdict>;
|
|
70
|
-
export type MintRefusalReason = "already_expired" |
|
|
70
|
+
export type MintRefusalReason = "already_expired" |
|
|
71
|
+
/** The capability, grants-capability or requests-capability is not a capability verb of the spec's grammar. */
|
|
72
|
+
"invalid_capability" | "parent_malformed" | "parent_bearer_mismatch" | "expires_exceeds_parent" | "scope_does_not_narrow" | "capability_mismatch" | "delegation_exceeds_parent" |
|
|
71
73
|
/** parent and authorised-by are mutually exclusive link kinds; a manage:grant capability must be minted through authorised-by only. */
|
|
72
74
|
"links_exclusive" |
|
|
73
75
|
/** The cited grant-capability did not decode, or its own conditions bytes did not. */
|
package/dist/domain/tokens.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { B as IdentityKey, D as DeviceId, bt as RevocationEntry, en as TokenClaims, p as CapabilityToken, yt as RevocationClaims } from "../protocol-DzmpxxUr.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { r as IdentityPort } from "../identity-BioGOV9M.mjs";
|
|
3
3
|
import { t as Clock } from "../clock-DhUPRfLh.mjs";
|
|
4
4
|
import { l as GrantSubject, m as TokenDelegateHandler, r as ConditionsContext, u as NarrowingCandidate } from "../token-predicates-CrieP1WI.mjs";
|
|
5
5
|
import { PredicateNode } from "trilean";
|
|
@@ -67,7 +67,9 @@ export interface VerifyRevocationEntryOptions {
|
|
|
67
67
|
* Verifies one gossiped revocation-entry (management.cddl): a well-formed COSE_Sign1 whose signature verifies against its own embedded issuer-key, where that issuer-key is self-certifying (sha256(issuer-key.public-key) equals the claimed issuer device-id). A verifier that ingests a revocation-announce frame runs each entry through this before recording it in its revocation view; entries failing here are dropped, not stored. The issuer-match against a specific token's own issuer (only a token's own issuer may revoke it) is deliberately NOT checked here -- it happens at lookup time in RevocationCheck, against whichever token is being verified.
|
|
68
68
|
*/
|
|
69
69
|
export declare function verifyRevocationEntry(entry: RevocationEntry, options: VerifyRevocationEntryOptions): Promise<RevocationEntryVerdict>;
|
|
70
|
-
export type MintRefusalReason = "already_expired" |
|
|
70
|
+
export type MintRefusalReason = "already_expired" |
|
|
71
|
+
/** The capability, grants-capability or requests-capability is not a capability verb of the spec's grammar. */
|
|
72
|
+
"invalid_capability" | "parent_malformed" | "parent_bearer_mismatch" | "expires_exceeds_parent" | "scope_does_not_narrow" | "capability_mismatch" | "delegation_exceeds_parent" |
|
|
71
73
|
/** parent and authorised-by are mutually exclusive link kinds; a manage:grant capability must be minted through authorised-by only. */
|
|
72
74
|
"links_exclusive" |
|
|
73
75
|
/** The cited grant-capability did not decode, or its own conditions bytes did not. */
|
package/dist/domain/tokens.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { capabilityTokenSchema, revocationClaimsSchema, tokenClaimsSchema } from "../generated/protocol.mjs";
|
|
1
|
+
import { capabilityTokenSchema, capabilityVerbSchema, revocationClaimsSchema, tokenClaimsSchema } from "../generated/protocol.mjs";
|
|
2
2
|
import { bytesToHex } from "./device-id.mjs";
|
|
3
3
|
import { n as firstAsync } from "../sequential-D_W3THly.mjs";
|
|
4
4
|
import { bytesEqual, scopeNarrows } from "./token-scope.mjs";
|
|
@@ -373,6 +373,14 @@ async function canGrantVia(heldGrantCapability, deviceId, candidate, now) {
|
|
|
373
373
|
* When `parent` is given, every one of `tokens.cddl`'s own narrowing obligations is enforced here, at issuance, rather than left for the far end to discover minutes or hours later as a bare `delegation_exceeds_parent` from `verifyCapabilityToken` -- the same "fail loudly, fail early" reasoning that governs every other boundary in this codebase. An issuer minting an invalid delegation is a bug in the caller; this function refuses rather than producing a token indistinguishable from a valid one until someone else verifies it.
|
|
374
374
|
*/
|
|
375
375
|
async function mintCapabilityToken(options) {
|
|
376
|
+
for (const verb of [
|
|
377
|
+
options.capability,
|
|
378
|
+
options.grantsCapability,
|
|
379
|
+
options.requestsCapability
|
|
380
|
+
]) if (verb !== void 0 && !capabilityVerbSchema.safeParse(verb).success) return {
|
|
381
|
+
ok: false,
|
|
382
|
+
reason: "invalid_capability"
|
|
383
|
+
};
|
|
376
384
|
if (options.expires <= options.clock.now()) return {
|
|
377
385
|
ok: false,
|
|
378
386
|
reason: "already_expired"
|
|
@@ -201,8 +201,8 @@ const coreDomainNameSchema = zod.z.lazy(() => zod.z.union([
|
|
|
201
201
|
zod.z.literal("core/bulk"),
|
|
202
202
|
zod.z.literal("core/version")
|
|
203
203
|
]));
|
|
204
|
-
const namespacedDomainIdSchema = zod.z.lazy(() => zod.z.string().regex(/* @__PURE__ */ new RegExp("[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+/[A-Za-z0-9_.-]+")));
|
|
205
|
-
const privateUseDomainIdSchema = zod.z.lazy(() => zod.z.string().regex(/* @__PURE__ */ new RegExp("x-[A-Za-z0-9_.-]+")));
|
|
204
|
+
const namespacedDomainIdSchema = zod.z.lazy(() => zod.z.string().regex(/* @__PURE__ */ new RegExp("^(?:[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+/[A-Za-z0-9_.-]+)$")));
|
|
205
|
+
const privateUseDomainIdSchema = zod.z.lazy(() => zod.z.string().regex(/* @__PURE__ */ new RegExp("^(?:x-[A-Za-z0-9_.-]+)$")));
|
|
206
206
|
const handshakeFrameSchema = zod.z.lazy(() => zod.z.object({
|
|
207
207
|
"type": zod.z.literal("handshake"),
|
|
208
208
|
"version": zod.z.lazy(() => protocolVersionSchema),
|
|
@@ -281,9 +281,9 @@ const topologyPeersSchema = zod.z.lazy(() => zod.z.object({
|
|
|
281
281
|
}));
|
|
282
282
|
const topologyGetSchema = zod.z.lazy(() => zod.z.object({ "verb": zod.z.literal("topology.get") }));
|
|
283
283
|
const roomPathSchema = zod.z.lazy(() => zod.z.union([zod.z.lazy(() => ownerNamedRoomPathSchema), zod.z.lazy(() => dmRoomPathSchema)]));
|
|
284
|
-
const deviceIdHexSchema = zod.z.lazy(() => zod.z.string().regex(/* @__PURE__ */ new RegExp("[0-9a-f]{64}")));
|
|
285
|
-
const ownerNamedRoomPathSchema = zod.z.lazy(() => zod.z.string().regex(/* @__PURE__ */ new RegExp("[0-9a-f]{64}/[A-Za-z0-9_-]+")));
|
|
286
|
-
const dmRoomPathSchema = zod.z.lazy(() => zod.z.string().regex(/* @__PURE__ */ new RegExp("[0-9a-f]{64}\\+[0-9a-f]{64}")));
|
|
284
|
+
const deviceIdHexSchema = zod.z.lazy(() => zod.z.string().regex(/* @__PURE__ */ new RegExp("^(?:[0-9a-f]{64})$")));
|
|
285
|
+
const ownerNamedRoomPathSchema = zod.z.lazy(() => zod.z.string().regex(/* @__PURE__ */ new RegExp("^(?:[0-9a-f]{64}/[A-Za-z0-9_-]+)$")));
|
|
286
|
+
const dmRoomPathSchema = zod.z.lazy(() => zod.z.string().regex(/* @__PURE__ */ new RegExp("^(?:[0-9a-f]{64}\\+[0-9a-f]{64})$")));
|
|
287
287
|
const roomSendSchema = zod.z.lazy(() => zod.z.object({
|
|
288
288
|
"verb": zod.z.literal("room.send"),
|
|
289
289
|
"message-id": zod.z.instanceof(Uint8Array),
|
|
@@ -438,9 +438,9 @@ const capabilityVerbSchema = zod.z.lazy(() => zod.z.union([
|
|
|
438
438
|
zod.z.lazy(() => namespacedCapabilitySchema),
|
|
439
439
|
zod.z.lazy(() => privateUseCapabilitySchema)
|
|
440
440
|
]));
|
|
441
|
-
const coreCapabilitySchema = zod.z.lazy(() => zod.z.string().regex(/* @__PURE__ */ new RegExp("[a-z][a-z0-9-]*:[a-z][a-z0-9-]*")));
|
|
442
|
-
const namespacedCapabilitySchema = zod.z.lazy(() => zod.z.string().regex(/* @__PURE__ */ new RegExp("[a-z0-9.-]+/[A-Za-z0-9_.-]+:[A-Za-z0-9_.-]+")));
|
|
443
|
-
const privateUseCapabilitySchema = zod.z.lazy(() => zod.z.string().regex(/* @__PURE__ */ new RegExp("x-[A-Za-z0-9_.-]+:[A-Za-z0-9_.-]+")));
|
|
441
|
+
const coreCapabilitySchema = zod.z.lazy(() => zod.z.string().regex(/* @__PURE__ */ new RegExp("^(?:[a-z][a-z0-9-]*:[a-z][a-z0-9-]*)$")));
|
|
442
|
+
const namespacedCapabilitySchema = zod.z.lazy(() => zod.z.string().regex(/* @__PURE__ */ new RegExp("^(?:[a-z0-9.-]+/[A-Za-z0-9_.-]+:[A-Za-z0-9_.-]+)$")));
|
|
443
|
+
const privateUseCapabilitySchema = zod.z.lazy(() => zod.z.string().regex(/* @__PURE__ */ new RegExp("^(?:x-[A-Za-z0-9_.-]+:[A-Za-z0-9_.-]+)$")));
|
|
444
444
|
const capabilityScopeSchema = zod.z.lazy(() => zod.z.object({
|
|
445
445
|
"kind": zod.z.string(),
|
|
446
446
|
"path": zod.z.string().optional()
|
|
@@ -200,8 +200,8 @@ const coreDomainNameSchema = z.lazy(() => z.union([
|
|
|
200
200
|
z.literal("core/bulk"),
|
|
201
201
|
z.literal("core/version")
|
|
202
202
|
]));
|
|
203
|
-
const namespacedDomainIdSchema = z.lazy(() => z.string().regex(/* @__PURE__ */ new RegExp("[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+/[A-Za-z0-9_.-]+")));
|
|
204
|
-
const privateUseDomainIdSchema = z.lazy(() => z.string().regex(/* @__PURE__ */ new RegExp("x-[A-Za-z0-9_.-]+")));
|
|
203
|
+
const namespacedDomainIdSchema = z.lazy(() => z.string().regex(/* @__PURE__ */ new RegExp("^(?:[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+/[A-Za-z0-9_.-]+)$")));
|
|
204
|
+
const privateUseDomainIdSchema = z.lazy(() => z.string().regex(/* @__PURE__ */ new RegExp("^(?:x-[A-Za-z0-9_.-]+)$")));
|
|
205
205
|
const handshakeFrameSchema = z.lazy(() => z.object({
|
|
206
206
|
"type": z.literal("handshake"),
|
|
207
207
|
"version": z.lazy(() => protocolVersionSchema),
|
|
@@ -280,9 +280,9 @@ const topologyPeersSchema = z.lazy(() => z.object({
|
|
|
280
280
|
}));
|
|
281
281
|
const topologyGetSchema = z.lazy(() => z.object({ "verb": z.literal("topology.get") }));
|
|
282
282
|
const roomPathSchema = z.lazy(() => z.union([z.lazy(() => ownerNamedRoomPathSchema), z.lazy(() => dmRoomPathSchema)]));
|
|
283
|
-
const deviceIdHexSchema = z.lazy(() => z.string().regex(/* @__PURE__ */ new RegExp("[0-9a-f]{64}")));
|
|
284
|
-
const ownerNamedRoomPathSchema = z.lazy(() => z.string().regex(/* @__PURE__ */ new RegExp("[0-9a-f]{64}/[A-Za-z0-9_-]+")));
|
|
285
|
-
const dmRoomPathSchema = z.lazy(() => z.string().regex(/* @__PURE__ */ new RegExp("[0-9a-f]{64}\\+[0-9a-f]{64}")));
|
|
283
|
+
const deviceIdHexSchema = z.lazy(() => z.string().regex(/* @__PURE__ */ new RegExp("^(?:[0-9a-f]{64})$")));
|
|
284
|
+
const ownerNamedRoomPathSchema = z.lazy(() => z.string().regex(/* @__PURE__ */ new RegExp("^(?:[0-9a-f]{64}/[A-Za-z0-9_-]+)$")));
|
|
285
|
+
const dmRoomPathSchema = z.lazy(() => z.string().regex(/* @__PURE__ */ new RegExp("^(?:[0-9a-f]{64}\\+[0-9a-f]{64})$")));
|
|
286
286
|
const roomSendSchema = z.lazy(() => z.object({
|
|
287
287
|
"verb": z.literal("room.send"),
|
|
288
288
|
"message-id": z.instanceof(Uint8Array),
|
|
@@ -437,9 +437,9 @@ const capabilityVerbSchema = z.lazy(() => z.union([
|
|
|
437
437
|
z.lazy(() => namespacedCapabilitySchema),
|
|
438
438
|
z.lazy(() => privateUseCapabilitySchema)
|
|
439
439
|
]));
|
|
440
|
-
const coreCapabilitySchema = z.lazy(() => z.string().regex(/* @__PURE__ */ new RegExp("[a-z][a-z0-9-]*:[a-z][a-z0-9-]*")));
|
|
441
|
-
const namespacedCapabilitySchema = z.lazy(() => z.string().regex(/* @__PURE__ */ new RegExp("[a-z0-9.-]+/[A-Za-z0-9_.-]+:[A-Za-z0-9_.-]+")));
|
|
442
|
-
const privateUseCapabilitySchema = z.lazy(() => z.string().regex(/* @__PURE__ */ new RegExp("x-[A-Za-z0-9_.-]+:[A-Za-z0-9_.-]+")));
|
|
440
|
+
const coreCapabilitySchema = z.lazy(() => z.string().regex(/* @__PURE__ */ new RegExp("^(?:[a-z][a-z0-9-]*:[a-z][a-z0-9-]*)$")));
|
|
441
|
+
const namespacedCapabilitySchema = z.lazy(() => z.string().regex(/* @__PURE__ */ new RegExp("^(?:[a-z0-9.-]+/[A-Za-z0-9_.-]+:[A-Za-z0-9_.-]+)$")));
|
|
442
|
+
const privateUseCapabilitySchema = z.lazy(() => z.string().regex(/* @__PURE__ */ new RegExp("^(?:x-[A-Za-z0-9_.-]+:[A-Za-z0-9_.-]+)$")));
|
|
443
443
|
const capabilityScopeSchema = z.lazy(() => z.object({
|
|
444
444
|
"kind": z.string(),
|
|
445
445
|
"path": z.string().optional()
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import { B as IdentityKey, D as DeviceId } from "./protocol-DzmpxxUr.mjs";
|
|
2
2
|
//#region src/ports/identity.d.ts
|
|
3
|
+
/** Why an identity-key could not be used: its algorithm is not one the implementation verifies, or its key bytes are not a valid key for that algorithm. */
|
|
4
|
+
type IdentityKeyFault = "unsupported_alg" | "invalid_key";
|
|
5
|
+
/** Thrown by an IdentityPort's verify for an identity-key it cannot use at all, as opposed to a signature that does not verify (which returns false). Any other error from a port is not a fault of the key: it is a failure of the port itself and is never to be read as a verdict about a peer. */
|
|
6
|
+
declare class IdentityKeyError extends Error {
|
|
7
|
+
readonly fault: IdentityKeyFault;
|
|
8
|
+
constructor(fault: IdentityKeyFault, message: string, options?: Readonly<{
|
|
9
|
+
cause: unknown;
|
|
10
|
+
}>);
|
|
11
|
+
}
|
|
3
12
|
/**
|
|
4
13
|
* Wraps the local node's own identity plus the signing/verification primitives capability-token handling needs -- never hardcoded to one crypto library's own API shape (Node's webcrypto, a WASM implementation, a hardware key, etc. can all satisfy this same contract).
|
|
5
14
|
*/
|
|
@@ -16,4 +25,4 @@ interface IdentityPort {
|
|
|
16
25
|
deriveSharedSecret?: (peerKey: IdentityKey) => Promise<Uint8Array<ArrayBuffer>>;
|
|
17
26
|
}
|
|
18
27
|
//#endregion
|
|
19
|
-
export { IdentityPort as t };
|
|
28
|
+
export { IdentityKeyFault as n, IdentityPort as r, IdentityKeyError as t };
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import { B as IdentityKey, D as DeviceId } from "./protocol-DzmpxxUr.cjs";
|
|
2
2
|
//#region src/ports/identity.d.ts
|
|
3
|
+
/** Why an identity-key could not be used: its algorithm is not one the implementation verifies, or its key bytes are not a valid key for that algorithm. */
|
|
4
|
+
type IdentityKeyFault = "unsupported_alg" | "invalid_key";
|
|
5
|
+
/** Thrown by an IdentityPort's verify for an identity-key it cannot use at all, as opposed to a signature that does not verify (which returns false). Any other error from a port is not a fault of the key: it is a failure of the port itself and is never to be read as a verdict about a peer. */
|
|
6
|
+
declare class IdentityKeyError extends Error {
|
|
7
|
+
readonly fault: IdentityKeyFault;
|
|
8
|
+
constructor(fault: IdentityKeyFault, message: string, options?: Readonly<{
|
|
9
|
+
cause: unknown;
|
|
10
|
+
}>);
|
|
11
|
+
}
|
|
3
12
|
/**
|
|
4
13
|
* Wraps the local node's own identity plus the signing/verification primitives capability-token handling needs -- never hardcoded to one crypto library's own API shape (Node's webcrypto, a WASM implementation, a hardware key, etc. can all satisfy this same contract).
|
|
5
14
|
*/
|
|
@@ -16,4 +25,4 @@ interface IdentityPort {
|
|
|
16
25
|
deriveSharedSecret?: (peerKey: IdentityKey) => Promise<Uint8Array<ArrayBuffer>>;
|
|
17
26
|
}
|
|
18
27
|
//#endregion
|
|
19
|
-
export { IdentityPort as t };
|
|
28
|
+
export { IdentityKeyFault as n, IdentityPort as r, IdentityKeyError as t };
|