wire-mesh-core 4.1.0 → 4.1.2
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/domain/mesh-session.cjs +104 -48
- package/dist/domain/mesh-session.d.cts +2 -2
- package/dist/domain/mesh-session.d.mts +2 -2
- package/dist/domain/mesh-session.mjs +104 -48
- package/dist/domain/tokens.cjs +8 -0
- package/dist/domain/tokens.d.cts +3 -1
- package/dist/domain/tokens.d.mts +3 -1
- package/dist/domain/tokens.mjs +9 -1
- package/dist/generated/protocol.cjs +8 -8
- package/dist/generated/protocol.mjs +8 -8
- package/package.json +1 -1
|
@@ -280,73 +280,129 @@ async function establishChannelFrom(identity, { pending, remote, peer, firstSend
|
|
|
280
280
|
function createRelayChannels(identity, sendHello, options = {}) {
|
|
281
281
|
if (options.firstSendCounter !== void 0) assertFirstSendCounter(options.firstSendCounter);
|
|
282
282
|
const channels = /* @__PURE__ */ new Map();
|
|
283
|
-
/**
|
|
284
|
-
function
|
|
283
|
+
/** Tells the owner of a failure that is local to this side. The callback is the owner's code: its own failure, thrown or as a rejected promise it returns, must not end the session or go unobserved, so it is reported once and swallowed. */
|
|
284
|
+
function reportFault(error) {
|
|
285
|
+
const report = (faultError) => {
|
|
286
|
+
console.error("the local fault callback failed", faultError);
|
|
287
|
+
};
|
|
288
|
+
try {
|
|
289
|
+
const result = options.onFault?.(error);
|
|
290
|
+
if (typeof result === "object" && result !== null && "then" in result && typeof result.then === "function") Promise.resolve(result).catch(report);
|
|
291
|
+
} catch (faultError) {
|
|
292
|
+
report(faultError);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
/** Abandons a handshake that failed: the entry goes if it is still the one held for the peer, and whoever awaits it is told. */
|
|
296
|
+
function abandon(entry, error) {
|
|
297
|
+
const failure = error instanceof Error ? error : new Error(String(error));
|
|
298
|
+
if (channels.get(entry.key) === entry) channels.delete(entry.key);
|
|
299
|
+
entry.settle.reject(failure);
|
|
300
|
+
entry.sent.reject(failure);
|
|
301
|
+
}
|
|
302
|
+
/** Starts a handshake with the peer: sends this side's hello, and registers the peer as awaiting at once, so a hello arriving meanwhile completes this handshake instead of starting a second. `answering` is true when a received hello started this, so nobody awaits the outcome and a failure to send is reported to the owner. `track` is false for a handshake answering a hello while a channel with that peer is established: it is not registered, so the established channel stays in place and in use until the new one is up and this side's hello has been sent. A hello that could not be sent leaves no channel behind: the peer would never learn this side's key. */
|
|
303
|
+
function start(peer, { answering, track } = {
|
|
304
|
+
answering: false,
|
|
305
|
+
track: true
|
|
306
|
+
}) {
|
|
285
307
|
const key = require_domain_device_id.deviceIdToHex(peer);
|
|
286
|
-
const settle = Promise.withResolvers();
|
|
287
308
|
const pending = beginHandshake(identity, peer);
|
|
288
309
|
const entry = {
|
|
289
310
|
status: "awaiting",
|
|
290
311
|
pending,
|
|
291
|
-
settle
|
|
312
|
+
settle: Promise.withResolvers(),
|
|
313
|
+
key,
|
|
314
|
+
sent: Promise.withResolvers()
|
|
292
315
|
};
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
316
|
+
entry.settle.promise.catch(() => void 0);
|
|
317
|
+
entry.sent.promise.catch(() => void 0);
|
|
318
|
+
pending.then(async ({ hello }) => {
|
|
319
|
+
try {
|
|
320
|
+
await sendHello(peer, hello);
|
|
321
|
+
} catch (error) {
|
|
322
|
+
abandon(entry, error);
|
|
323
|
+
entry.settle.promise.then((channel) => {
|
|
324
|
+
const held = channels.get(key);
|
|
325
|
+
if (held?.status === "established" && held.channel === channel) channels.delete(key);
|
|
326
|
+
}, () => void 0);
|
|
327
|
+
if (answering && track) reportFault(error);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
entry.sent.resolve(void 0);
|
|
331
|
+
}, (error) => {
|
|
332
|
+
abandon(entry, error);
|
|
297
333
|
});
|
|
334
|
+
if (track) channels.set(key, entry);
|
|
298
335
|
return entry;
|
|
299
336
|
}
|
|
337
|
+
/** Completes the handshake a verified hello belongs to. Throws on any failure of this side; the caller abandons the handshake. */
|
|
338
|
+
async function complete({ entry, hello, peer, replaces }) {
|
|
339
|
+
const { key } = entry;
|
|
340
|
+
const established = await establishChannelFrom(identity, {
|
|
341
|
+
pending: await entry.pending,
|
|
342
|
+
remote: hello,
|
|
343
|
+
peer,
|
|
344
|
+
...options.firstSendCounter === void 0 ? {} : { firstSendCounter: options.firstSendCounter }
|
|
345
|
+
});
|
|
346
|
+
if (established === void 0) {
|
|
347
|
+
abandon(entry, /* @__PURE__ */ new Error("the peer sent an unusable ephemeral key"));
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
if (replaces !== void 0) await entry.sent.promise;
|
|
351
|
+
if (channels.get(key) !== (replaces ?? entry)) return;
|
|
352
|
+
const channel = {
|
|
353
|
+
...established,
|
|
354
|
+
seal: async (frame) => {
|
|
355
|
+
try {
|
|
356
|
+
return await established.seal(frame);
|
|
357
|
+
} catch (error) {
|
|
358
|
+
const held = channels.get(key);
|
|
359
|
+
if (error instanceof require_domain_channel_exhausted_error.ChannelExhaustedError && held?.status === "established" && held.channel === channel) channels.delete(key);
|
|
360
|
+
throw error;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
channels.set(key, {
|
|
365
|
+
status: "established",
|
|
366
|
+
channel,
|
|
367
|
+
remoteEphemeralKey: hello["ephemeral-key"]
|
|
368
|
+
});
|
|
369
|
+
entry.settle.resolve(channel);
|
|
370
|
+
}
|
|
300
371
|
return {
|
|
301
372
|
ensure: async (peer) => {
|
|
302
|
-
const
|
|
373
|
+
const key = require_domain_device_id.deviceIdToHex(peer);
|
|
374
|
+
const existing = channels.get(key);
|
|
303
375
|
if (existing?.status === "established") return existing.channel;
|
|
304
376
|
return (existing ?? start(peer)).settle.promise;
|
|
305
377
|
},
|
|
306
378
|
applyHello: async (hello) => {
|
|
307
|
-
let
|
|
379
|
+
let entry;
|
|
308
380
|
try {
|
|
309
|
-
verdict = await verifyHello(identity, hello);
|
|
381
|
+
const verdict = await verifyHello(identity, hello);
|
|
382
|
+
if (!verdict.ok) return;
|
|
383
|
+
const key = require_domain_device_id.deviceIdToHex(verdict.peer.deviceId);
|
|
384
|
+
const current = channels.get(key);
|
|
385
|
+
if (current?.status === "established" && require_domain_token_scope.bytesEqual(current.remoteEphemeralKey, hello["ephemeral-key"])) return;
|
|
386
|
+
const replaces = current?.status === "established" ? current : void 0;
|
|
387
|
+
entry = current?.status === "awaiting" ? current : start(verdict.peer.deviceId, {
|
|
388
|
+
answering: true,
|
|
389
|
+
track: replaces === void 0
|
|
390
|
+
});
|
|
391
|
+
await complete({
|
|
392
|
+
entry,
|
|
393
|
+
hello,
|
|
394
|
+
peer: verdict.peer,
|
|
395
|
+
replaces
|
|
396
|
+
});
|
|
310
397
|
} catch (error) {
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
}
|
|
314
|
-
if (!verdict.ok) return;
|
|
315
|
-
const key = require_domain_device_id.deviceIdToHex(verdict.peer.deviceId);
|
|
316
|
-
const current = channels.get(key);
|
|
317
|
-
const entry = current?.status === "awaiting" ? current : start(verdict.peer.deviceId);
|
|
318
|
-
const established = await establishChannelFrom(identity, {
|
|
319
|
-
pending: await entry.pending,
|
|
320
|
-
remote: hello,
|
|
321
|
-
peer: verdict.peer,
|
|
322
|
-
...options.firstSendCounter === void 0 ? {} : { firstSendCounter: options.firstSendCounter }
|
|
323
|
-
});
|
|
324
|
-
if (established === void 0) {
|
|
325
|
-
entry.settle.reject(/* @__PURE__ */ new Error("the peer sent an unusable ephemeral key"));
|
|
326
|
-
if (channels.get(key) === entry) channels.delete(key);
|
|
327
|
-
return;
|
|
398
|
+
if (entry !== void 0) abandon(entry, error);
|
|
399
|
+
reportFault(error);
|
|
328
400
|
}
|
|
329
|
-
const channel = {
|
|
330
|
-
...established,
|
|
331
|
-
seal: async (frame) => {
|
|
332
|
-
try {
|
|
333
|
-
return await established.seal(frame);
|
|
334
|
-
} catch (error) {
|
|
335
|
-
const held = channels.get(key);
|
|
336
|
-
if (error instanceof require_domain_channel_exhausted_error.ChannelExhaustedError && held?.status === "established" && held.channel === channel) channels.delete(key);
|
|
337
|
-
throw error;
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
};
|
|
341
|
-
channels.set(key, {
|
|
342
|
-
status: "established",
|
|
343
|
-
channel
|
|
344
|
-
});
|
|
345
|
-
entry.settle.resolve(channel);
|
|
346
401
|
},
|
|
347
402
|
open: async (data, claimedFrom) => {
|
|
348
403
|
if (claimedFrom === void 0) return;
|
|
349
|
-
const
|
|
404
|
+
const key = require_domain_device_id.deviceIdToHex(claimedFrom);
|
|
405
|
+
const entry = channels.get(key);
|
|
350
406
|
if (entry?.status !== "established") return;
|
|
351
407
|
const frame = await entry.channel.open(data);
|
|
352
408
|
return frame === void 0 ? void 0 : {
|
|
@@ -355,13 +411,13 @@ function createRelayChannels(identity, sendHello, options = {}) {
|
|
|
355
411
|
};
|
|
356
412
|
},
|
|
357
413
|
reset: (reason) => {
|
|
358
|
-
for (const entry of channels.values()) if (entry.status === "awaiting") entry
|
|
414
|
+
for (const entry of [...channels.values()]) if (entry.status === "awaiting") abandon(entry, new Error(reason));
|
|
359
415
|
channels.clear();
|
|
360
416
|
},
|
|
361
417
|
forget: (peer) => {
|
|
362
418
|
const key = require_domain_device_id.deviceIdToHex(peer);
|
|
363
419
|
const entry = channels.get(key);
|
|
364
|
-
if (entry?.status === "awaiting") entry
|
|
420
|
+
if (entry?.status === "awaiting") abandon(entry, /* @__PURE__ */ new Error("the relay pairing was lost"));
|
|
365
421
|
channels.delete(key);
|
|
366
422
|
}
|
|
367
423
|
};
|
|
@@ -137,8 +137,8 @@ addresses?: readonly string[],
|
|
|
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
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
|
|
141
|
-
onLocalFault?: (error: unknown) => void): MeshSession;
|
|
140
|
+
/** Told of a failure that is local to this session and did not end it, while handling a hello from a peer: an identity port that failed to check or sign, an answering hello that could not be sent, a channel that could not be established. The hello is dropped, any half-made handshake for that peer is discarded, and the peer is told nothing, so no failure in handling a hello ends the session. Called once per failing hello; if it throws, or returns a promise that rejects, that is reported once to the console and swallowed. Omit for a caller with no use for it. */
|
|
141
|
+
onLocalFault?: (error: unknown) => void | Promise<void>): MeshSession;
|
|
142
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. */
|
|
143
143
|
export interface AcceptedMeshSession extends MeshSession {
|
|
144
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. */
|
|
@@ -137,8 +137,8 @@ addresses?: readonly string[],
|
|
|
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
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
|
|
141
|
-
onLocalFault?: (error: unknown) => void): MeshSession;
|
|
140
|
+
/** Told of a failure that is local to this session and did not end it, while handling a hello from a peer: an identity port that failed to check or sign, an answering hello that could not be sent, a channel that could not be established. The hello is dropped, any half-made handshake for that peer is discarded, and the peer is told nothing, so no failure in handling a hello ends the session. Called once per failing hello; if it throws, or returns a promise that rejects, that is reported once to the console and swallowed. Omit for a caller with no use for it. */
|
|
141
|
+
onLocalFault?: (error: unknown) => void | Promise<void>): MeshSession;
|
|
142
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. */
|
|
143
143
|
export interface AcceptedMeshSession extends MeshSession {
|
|
144
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. */
|
|
@@ -279,73 +279,129 @@ async function establishChannelFrom(identity, { pending, remote, peer, firstSend
|
|
|
279
279
|
function createRelayChannels(identity, sendHello, options = {}) {
|
|
280
280
|
if (options.firstSendCounter !== void 0) assertFirstSendCounter(options.firstSendCounter);
|
|
281
281
|
const channels = /* @__PURE__ */ new Map();
|
|
282
|
-
/**
|
|
283
|
-
function
|
|
282
|
+
/** Tells the owner of a failure that is local to this side. The callback is the owner's code: its own failure, thrown or as a rejected promise it returns, must not end the session or go unobserved, so it is reported once and swallowed. */
|
|
283
|
+
function reportFault(error) {
|
|
284
|
+
const report = (faultError) => {
|
|
285
|
+
console.error("the local fault callback failed", faultError);
|
|
286
|
+
};
|
|
287
|
+
try {
|
|
288
|
+
const result = options.onFault?.(error);
|
|
289
|
+
if (typeof result === "object" && result !== null && "then" in result && typeof result.then === "function") Promise.resolve(result).catch(report);
|
|
290
|
+
} catch (faultError) {
|
|
291
|
+
report(faultError);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
/** Abandons a handshake that failed: the entry goes if it is still the one held for the peer, and whoever awaits it is told. */
|
|
295
|
+
function abandon(entry, error) {
|
|
296
|
+
const failure = error instanceof Error ? error : new Error(String(error));
|
|
297
|
+
if (channels.get(entry.key) === entry) channels.delete(entry.key);
|
|
298
|
+
entry.settle.reject(failure);
|
|
299
|
+
entry.sent.reject(failure);
|
|
300
|
+
}
|
|
301
|
+
/** Starts a handshake with the peer: sends this side's hello, and registers the peer as awaiting at once, so a hello arriving meanwhile completes this handshake instead of starting a second. `answering` is true when a received hello started this, so nobody awaits the outcome and a failure to send is reported to the owner. `track` is false for a handshake answering a hello while a channel with that peer is established: it is not registered, so the established channel stays in place and in use until the new one is up and this side's hello has been sent. A hello that could not be sent leaves no channel behind: the peer would never learn this side's key. */
|
|
302
|
+
function start(peer, { answering, track } = {
|
|
303
|
+
answering: false,
|
|
304
|
+
track: true
|
|
305
|
+
}) {
|
|
284
306
|
const key = deviceIdToHex(peer);
|
|
285
|
-
const settle = Promise.withResolvers();
|
|
286
307
|
const pending = beginHandshake(identity, peer);
|
|
287
308
|
const entry = {
|
|
288
309
|
status: "awaiting",
|
|
289
310
|
pending,
|
|
290
|
-
settle
|
|
311
|
+
settle: Promise.withResolvers(),
|
|
312
|
+
key,
|
|
313
|
+
sent: Promise.withResolvers()
|
|
291
314
|
};
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
315
|
+
entry.settle.promise.catch(() => void 0);
|
|
316
|
+
entry.sent.promise.catch(() => void 0);
|
|
317
|
+
pending.then(async ({ hello }) => {
|
|
318
|
+
try {
|
|
319
|
+
await sendHello(peer, hello);
|
|
320
|
+
} catch (error) {
|
|
321
|
+
abandon(entry, error);
|
|
322
|
+
entry.settle.promise.then((channel) => {
|
|
323
|
+
const held = channels.get(key);
|
|
324
|
+
if (held?.status === "established" && held.channel === channel) channels.delete(key);
|
|
325
|
+
}, () => void 0);
|
|
326
|
+
if (answering && track) reportFault(error);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
entry.sent.resolve(void 0);
|
|
330
|
+
}, (error) => {
|
|
331
|
+
abandon(entry, error);
|
|
296
332
|
});
|
|
333
|
+
if (track) channels.set(key, entry);
|
|
297
334
|
return entry;
|
|
298
335
|
}
|
|
336
|
+
/** Completes the handshake a verified hello belongs to. Throws on any failure of this side; the caller abandons the handshake. */
|
|
337
|
+
async function complete({ entry, hello, peer, replaces }) {
|
|
338
|
+
const { key } = entry;
|
|
339
|
+
const established = await establishChannelFrom(identity, {
|
|
340
|
+
pending: await entry.pending,
|
|
341
|
+
remote: hello,
|
|
342
|
+
peer,
|
|
343
|
+
...options.firstSendCounter === void 0 ? {} : { firstSendCounter: options.firstSendCounter }
|
|
344
|
+
});
|
|
345
|
+
if (established === void 0) {
|
|
346
|
+
abandon(entry, /* @__PURE__ */ new Error("the peer sent an unusable ephemeral key"));
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
if (replaces !== void 0) await entry.sent.promise;
|
|
350
|
+
if (channels.get(key) !== (replaces ?? entry)) return;
|
|
351
|
+
const channel = {
|
|
352
|
+
...established,
|
|
353
|
+
seal: async (frame) => {
|
|
354
|
+
try {
|
|
355
|
+
return await established.seal(frame);
|
|
356
|
+
} catch (error) {
|
|
357
|
+
const held = channels.get(key);
|
|
358
|
+
if (error instanceof ChannelExhaustedError && held?.status === "established" && held.channel === channel) channels.delete(key);
|
|
359
|
+
throw error;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
channels.set(key, {
|
|
364
|
+
status: "established",
|
|
365
|
+
channel,
|
|
366
|
+
remoteEphemeralKey: hello["ephemeral-key"]
|
|
367
|
+
});
|
|
368
|
+
entry.settle.resolve(channel);
|
|
369
|
+
}
|
|
299
370
|
return {
|
|
300
371
|
ensure: async (peer) => {
|
|
301
|
-
const
|
|
372
|
+
const key = deviceIdToHex(peer);
|
|
373
|
+
const existing = channels.get(key);
|
|
302
374
|
if (existing?.status === "established") return existing.channel;
|
|
303
375
|
return (existing ?? start(peer)).settle.promise;
|
|
304
376
|
},
|
|
305
377
|
applyHello: async (hello) => {
|
|
306
|
-
let
|
|
378
|
+
let entry;
|
|
307
379
|
try {
|
|
308
|
-
verdict = await verifyHello(identity, hello);
|
|
380
|
+
const verdict = await verifyHello(identity, hello);
|
|
381
|
+
if (!verdict.ok) return;
|
|
382
|
+
const key = deviceIdToHex(verdict.peer.deviceId);
|
|
383
|
+
const current = channels.get(key);
|
|
384
|
+
if (current?.status === "established" && bytesEqual(current.remoteEphemeralKey, hello["ephemeral-key"])) return;
|
|
385
|
+
const replaces = current?.status === "established" ? current : void 0;
|
|
386
|
+
entry = current?.status === "awaiting" ? current : start(verdict.peer.deviceId, {
|
|
387
|
+
answering: true,
|
|
388
|
+
track: replaces === void 0
|
|
389
|
+
});
|
|
390
|
+
await complete({
|
|
391
|
+
entry,
|
|
392
|
+
hello,
|
|
393
|
+
peer: verdict.peer,
|
|
394
|
+
replaces
|
|
395
|
+
});
|
|
309
396
|
} catch (error) {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
}
|
|
313
|
-
if (!verdict.ok) return;
|
|
314
|
-
const key = deviceIdToHex(verdict.peer.deviceId);
|
|
315
|
-
const current = channels.get(key);
|
|
316
|
-
const entry = current?.status === "awaiting" ? current : start(verdict.peer.deviceId);
|
|
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) {
|
|
324
|
-
entry.settle.reject(/* @__PURE__ */ new Error("the peer sent an unusable ephemeral key"));
|
|
325
|
-
if (channels.get(key) === entry) channels.delete(key);
|
|
326
|
-
return;
|
|
397
|
+
if (entry !== void 0) abandon(entry, error);
|
|
398
|
+
reportFault(error);
|
|
327
399
|
}
|
|
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
|
-
};
|
|
340
|
-
channels.set(key, {
|
|
341
|
-
status: "established",
|
|
342
|
-
channel
|
|
343
|
-
});
|
|
344
|
-
entry.settle.resolve(channel);
|
|
345
400
|
},
|
|
346
401
|
open: async (data, claimedFrom) => {
|
|
347
402
|
if (claimedFrom === void 0) return;
|
|
348
|
-
const
|
|
403
|
+
const key = deviceIdToHex(claimedFrom);
|
|
404
|
+
const entry = channels.get(key);
|
|
349
405
|
if (entry?.status !== "established") return;
|
|
350
406
|
const frame = await entry.channel.open(data);
|
|
351
407
|
return frame === void 0 ? void 0 : {
|
|
@@ -354,13 +410,13 @@ function createRelayChannels(identity, sendHello, options = {}) {
|
|
|
354
410
|
};
|
|
355
411
|
},
|
|
356
412
|
reset: (reason) => {
|
|
357
|
-
for (const entry of channels.values()) if (entry.status === "awaiting") entry
|
|
413
|
+
for (const entry of [...channels.values()]) if (entry.status === "awaiting") abandon(entry, new Error(reason));
|
|
358
414
|
channels.clear();
|
|
359
415
|
},
|
|
360
416
|
forget: (peer) => {
|
|
361
417
|
const key = deviceIdToHex(peer);
|
|
362
418
|
const entry = channels.get(key);
|
|
363
|
-
if (entry?.status === "awaiting") entry
|
|
419
|
+
if (entry?.status === "awaiting") abandon(entry, /* @__PURE__ */ new Error("the relay pairing was lost"));
|
|
364
420
|
channels.delete(key);
|
|
365
421
|
}
|
|
366
422
|
};
|
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
|
@@ -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
|
@@ -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()
|