wire-mesh-core 1.21.0 → 1.23.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.
- package/dist/domain/mesh-session.cjs +4 -2
- package/dist/domain/mesh-session.d.cts +4 -0
- package/dist/domain/mesh-session.d.mts +4 -0
- package/dist/domain/mesh-session.mjs +4 -2
- package/dist/domain/relay-hub.cjs +5 -0
- package/dist/domain/relay-hub.d.cts +8 -1
- package/dist/domain/relay-hub.d.mts +8 -1
- package/dist/domain/relay-hub.mjs +5 -0
- package/dist/domain/relay-use-gate.cjs +27 -0
- package/dist/domain/relay-use-gate.d.cts +23 -0
- package/dist/domain/relay-use-gate.d.mts +23 -0
- package/dist/domain/relay-use-gate.mjs +25 -0
- package/package.json +5 -1
|
@@ -31,7 +31,7 @@ function localHandshake(domains) {
|
|
|
31
31
|
/**
|
|
32
32
|
* 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.
|
|
33
33
|
*/
|
|
34
|
-
function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addresses = []) {
|
|
34
|
+
function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addresses = [], onFrame, onSessionEnd) {
|
|
35
35
|
let connection = null;
|
|
36
36
|
let state = { status: "idle" };
|
|
37
37
|
let handshake = { status: "pending" };
|
|
@@ -239,8 +239,10 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
239
239
|
for await (const frame of link.receive()) {
|
|
240
240
|
if (feedCancelled) return;
|
|
241
241
|
applyFrame(frame);
|
|
242
|
+
await onFrame?.(link, frame);
|
|
242
243
|
emit();
|
|
243
244
|
}
|
|
245
|
+
onSessionEnd?.(link);
|
|
244
246
|
if (state.status === "connected") handleDisconnect("node closed the connection", address, localDomains);
|
|
245
247
|
}
|
|
246
248
|
/** Builds this side's own self-advert: this node's own directly-reachable addresses (wire-mesh#38), or none for a caller with nothing to offer (a browser client, which cannot accept inbound connections) -- either is an honest advert, not a stopgap. extensions merge onto peer-advert's own open `* tstr => any` tail -- the mechanism sendGossipUpdate uses to keep a gossiped fact (presence status, an accept/refuse policy, or any future domain's own) live over the connection's lifetime. Extensions are spread before the three mandatory fields (never after) so a caller-supplied key of the same name can never shadow them on the wire -- validateGossipExtensions already rejects that case loudly, but the field order is kept safe in its own right rather than relying solely on the guard staying in sync. */
|
|
@@ -466,7 +468,7 @@ async function acceptMeshSession(connection, identity, localDomains, options = {
|
|
|
466
468
|
if (peerDeviceIdResolved) return;
|
|
467
469
|
peerDeviceIdResolved = true;
|
|
468
470
|
resolvePeerDeviceId?.(advert.device);
|
|
469
|
-
}, options.addresses);
|
|
471
|
+
}, options.addresses, options.onFrame, options.onSessionEnd);
|
|
470
472
|
await wireUpConnection(connection, options.label ?? "accepted", localDomains);
|
|
471
473
|
return {
|
|
472
474
|
...session,
|
|
@@ -101,6 +101,10 @@ export interface AcceptedMeshSessionOptions {
|
|
|
101
101
|
clock?: Readonly<Clock>;
|
|
102
102
|
/** This node's own directly-reachable "host:port" candidates (wire-mesh#38), advertised in this session's self-advert. Omit (or pass none) for a caller with nothing to offer. */
|
|
103
103
|
addresses?: readonly string[];
|
|
104
|
+
/** Observes every frame this session receives, alongside (never instead of) its own manage-request/gossip/handshake handling (wire-mesh#102) -- the integration point an "ordinary opted-in node relays for peers it's already talking to" uses: pass a shared RelayHub instance's own handleFrame, bound to this connection, and it sees the identical frame stream this session's own applyFrame already consumes, with no second for-await loop racing over the same connection.receive(). A dedicated wire-mesh-node/cloudflare-hub deployment, which never runs a MeshSession at all, is unaffected -- this is additive, opt-in, and irrelevant to that case. */
|
|
105
|
+
onFrame?: (connection: Readonly<Connection>, frame: Frame) => void | Promise<void>;
|
|
106
|
+
/** Fired once this session's own connection.receive() stream ends -- the counterpart a RelayHub's own forgetConnection needs, since its registry is keyed by this exact Connection and must be cleaned up when it specifically ends. */
|
|
107
|
+
onSessionEnd?: (connection: Readonly<Connection>) => void;
|
|
104
108
|
}
|
|
105
109
|
/** 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. */
|
|
106
110
|
export declare function acceptMeshSession(connection: Readonly<Connection>, identity: Readonly<IdentityPort>, localDomains: readonly string[], options?: Readonly<AcceptedMeshSessionOptions>): Promise<AcceptedMeshSession>;
|
|
@@ -101,6 +101,10 @@ export interface AcceptedMeshSessionOptions {
|
|
|
101
101
|
clock?: Readonly<Clock>;
|
|
102
102
|
/** This node's own directly-reachable "host:port" candidates (wire-mesh#38), advertised in this session's self-advert. Omit (or pass none) for a caller with nothing to offer. */
|
|
103
103
|
addresses?: readonly string[];
|
|
104
|
+
/** Observes every frame this session receives, alongside (never instead of) its own manage-request/gossip/handshake handling (wire-mesh#102) -- the integration point an "ordinary opted-in node relays for peers it's already talking to" uses: pass a shared RelayHub instance's own handleFrame, bound to this connection, and it sees the identical frame stream this session's own applyFrame already consumes, with no second for-await loop racing over the same connection.receive(). A dedicated wire-mesh-node/cloudflare-hub deployment, which never runs a MeshSession at all, is unaffected -- this is additive, opt-in, and irrelevant to that case. */
|
|
105
|
+
onFrame?: (connection: Readonly<Connection>, frame: Frame) => void | Promise<void>;
|
|
106
|
+
/** Fired once this session's own connection.receive() stream ends -- the counterpart a RelayHub's own forgetConnection needs, since its registry is keyed by this exact Connection and must be cleaned up when it specifically ends. */
|
|
107
|
+
onSessionEnd?: (connection: Readonly<Connection>) => void;
|
|
104
108
|
}
|
|
105
109
|
/** 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. */
|
|
106
110
|
export declare function acceptMeshSession(connection: Readonly<Connection>, identity: Readonly<IdentityPort>, localDomains: readonly string[], options?: Readonly<AcceptedMeshSessionOptions>): Promise<AcceptedMeshSession>;
|
|
@@ -30,7 +30,7 @@ function localHandshake(domains) {
|
|
|
30
30
|
/**
|
|
31
31
|
* 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.
|
|
32
32
|
*/
|
|
33
|
-
function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addresses = []) {
|
|
33
|
+
function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addresses = [], onFrame, onSessionEnd) {
|
|
34
34
|
let connection = null;
|
|
35
35
|
let state = { status: "idle" };
|
|
36
36
|
let handshake = { status: "pending" };
|
|
@@ -238,8 +238,10 @@ function createSessionCore(identity, clock, reconnect, dial, onPeerAdvert, addre
|
|
|
238
238
|
for await (const frame of link.receive()) {
|
|
239
239
|
if (feedCancelled) return;
|
|
240
240
|
applyFrame(frame);
|
|
241
|
+
await onFrame?.(link, frame);
|
|
241
242
|
emit();
|
|
242
243
|
}
|
|
244
|
+
onSessionEnd?.(link);
|
|
243
245
|
if (state.status === "connected") handleDisconnect("node closed the connection", address, localDomains);
|
|
244
246
|
}
|
|
245
247
|
/** Builds this side's own self-advert: this node's own directly-reachable addresses (wire-mesh#38), or none for a caller with nothing to offer (a browser client, which cannot accept inbound connections) -- either is an honest advert, not a stopgap. extensions merge onto peer-advert's own open `* tstr => any` tail -- the mechanism sendGossipUpdate uses to keep a gossiped fact (presence status, an accept/refuse policy, or any future domain's own) live over the connection's lifetime. Extensions are spread before the three mandatory fields (never after) so a caller-supplied key of the same name can never shadow them on the wire -- validateGossipExtensions already rejects that case loudly, but the field order is kept safe in its own right rather than relying solely on the guard staying in sync. */
|
|
@@ -465,7 +467,7 @@ async function acceptMeshSession(connection, identity, localDomains, options = {
|
|
|
465
467
|
if (peerDeviceIdResolved) return;
|
|
466
468
|
peerDeviceIdResolved = true;
|
|
467
469
|
resolvePeerDeviceId?.(advert.device);
|
|
468
|
-
}, options.addresses);
|
|
470
|
+
}, options.addresses, options.onFrame, options.onSessionEnd);
|
|
469
471
|
await wireUpConnection(connection, options.label ?? "accepted", localDomains);
|
|
470
472
|
return {
|
|
471
473
|
...session,
|
|
@@ -104,6 +104,11 @@ function createRelayHub() {
|
|
|
104
104
|
forgetConnection(connection);
|
|
105
105
|
}
|
|
106
106
|
},
|
|
107
|
+
registerConnection(connection) {
|
|
108
|
+
connections.add(connection);
|
|
109
|
+
},
|
|
110
|
+
onFrame: handleFrame,
|
|
111
|
+
onDisconnect: forgetConnection,
|
|
107
112
|
stop() {
|
|
108
113
|
connections.clear();
|
|
109
114
|
devices.clear();
|
|
@@ -1,8 +1,15 @@
|
|
|
1
|
+
import { D as Frame } from "../protocol-l9RMR3s5.cjs";
|
|
1
2
|
import { t as Connection } from "../transport-Baipikk7.cjs";
|
|
2
3
|
//#region src/domain/relay-hub.d.ts
|
|
3
4
|
export interface RelayHub {
|
|
4
|
-
/** Drives one accepted connection until it closes: registers gossip-advertised devices, re-broadcasts each gossip frame to every other connected client and replies to the gossiping connection with a catch-up frame of every other already-known device, answers relay-connect by pairing and notifying the target, and forwards relay-data within established pairings. Resolves when the connection's frame stream ends. */
|
|
5
|
+
/** Drives one accepted connection until it closes: registers gossip-advertised devices, re-broadcasts each gossip frame to every other connected client and replies to the gossiping connection with a catch-up frame of every other already-known device, answers relay-connect by pairing and notifying the target, and forwards relay-data within established pairings. Resolves when the connection's frame stream ends. Internally just registerConnection followed by a loop of onFrame calls and a final onDisconnect -- kept as its own method since a dedicated hub deployment (wire-mesh-node/cloudflare-hub) has nothing else driving the connection and wants the whole lifecycle in one call. */
|
|
5
6
|
handleConnection: (connection: Readonly<Connection>) => Promise<void>;
|
|
7
|
+
/** Registers a connection with this hub without taking over its own frame-consumption loop (wire-mesh#102) -- the entry point an "ordinary opted-in node relays for peers it's already talking to" uses, alongside onFrame/onDisconnect below, to let a MeshSession's own single connection.receive() loop drive this hub rather than running a second, competing one. Must be called once, before the first onFrame call for this connection, so gossip fan-out (which forwards to every OTHER registered connection) already sees it. */
|
|
8
|
+
registerConnection: (connection: Readonly<Connection>) => void;
|
|
9
|
+
/** Observes one frame on an already-registered connection -- the same per-frame logic handleConnection's own loop calls internally, exposed directly so a caller with its own frame-consumption loop can drive it without a second for-await over the same connection.receive(). */
|
|
10
|
+
onFrame: (connection: Readonly<Connection>, frame: Frame) => Promise<void>;
|
|
11
|
+
/** Cleans up all registry and pairing state for one connection once its own frame stream has ended -- the onSessionEnd counterpart registerConnection/onFrame needs for the shared-consumption case, since this hub's registry is keyed by Connection and has no other way to learn a connection is gone. */
|
|
12
|
+
onDisconnect: (connection: Readonly<Connection>) => void;
|
|
6
13
|
/** Drops all registry and pairing state -- used by tests and by transport teardown. */
|
|
7
14
|
stop: () => void;
|
|
8
15
|
}
|
|
@@ -1,8 +1,15 @@
|
|
|
1
|
+
import { D as Frame } from "../protocol-l9RMR3s5.mjs";
|
|
1
2
|
import { t as Connection } from "../transport-DWbvUc3L.mjs";
|
|
2
3
|
//#region src/domain/relay-hub.d.ts
|
|
3
4
|
export interface RelayHub {
|
|
4
|
-
/** Drives one accepted connection until it closes: registers gossip-advertised devices, re-broadcasts each gossip frame to every other connected client and replies to the gossiping connection with a catch-up frame of every other already-known device, answers relay-connect by pairing and notifying the target, and forwards relay-data within established pairings. Resolves when the connection's frame stream ends. */
|
|
5
|
+
/** Drives one accepted connection until it closes: registers gossip-advertised devices, re-broadcasts each gossip frame to every other connected client and replies to the gossiping connection with a catch-up frame of every other already-known device, answers relay-connect by pairing and notifying the target, and forwards relay-data within established pairings. Resolves when the connection's frame stream ends. Internally just registerConnection followed by a loop of onFrame calls and a final onDisconnect -- kept as its own method since a dedicated hub deployment (wire-mesh-node/cloudflare-hub) has nothing else driving the connection and wants the whole lifecycle in one call. */
|
|
5
6
|
handleConnection: (connection: Readonly<Connection>) => Promise<void>;
|
|
7
|
+
/** Registers a connection with this hub without taking over its own frame-consumption loop (wire-mesh#102) -- the entry point an "ordinary opted-in node relays for peers it's already talking to" uses, alongside onFrame/onDisconnect below, to let a MeshSession's own single connection.receive() loop drive this hub rather than running a second, competing one. Must be called once, before the first onFrame call for this connection, so gossip fan-out (which forwards to every OTHER registered connection) already sees it. */
|
|
8
|
+
registerConnection: (connection: Readonly<Connection>) => void;
|
|
9
|
+
/** Observes one frame on an already-registered connection -- the same per-frame logic handleConnection's own loop calls internally, exposed directly so a caller with its own frame-consumption loop can drive it without a second for-await over the same connection.receive(). */
|
|
10
|
+
onFrame: (connection: Readonly<Connection>, frame: Frame) => Promise<void>;
|
|
11
|
+
/** Cleans up all registry and pairing state for one connection once its own frame stream has ended -- the onSessionEnd counterpart registerConnection/onFrame needs for the shared-consumption case, since this hub's registry is keyed by Connection and has no other way to learn a connection is gone. */
|
|
12
|
+
onDisconnect: (connection: Readonly<Connection>) => void;
|
|
6
13
|
/** Drops all registry and pairing state -- used by tests and by transport teardown. */
|
|
7
14
|
stop: () => void;
|
|
8
15
|
}
|
|
@@ -103,6 +103,11 @@ function createRelayHub() {
|
|
|
103
103
|
forgetConnection(connection);
|
|
104
104
|
}
|
|
105
105
|
},
|
|
106
|
+
registerConnection(connection) {
|
|
107
|
+
connections.add(connection);
|
|
108
|
+
},
|
|
109
|
+
onFrame: handleFrame,
|
|
110
|
+
onDisconnect: forgetConnection,
|
|
106
111
|
stop() {
|
|
107
112
|
connections.clear();
|
|
108
113
|
devices.clear();
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region src/domain/relay-use-gate.ts
|
|
3
|
+
/** An in-memory, per-connection authorization set. Keyed by Connection identity (reference equality), matching RelayHub's own registry -- a connection that reconnects is a new object and starts unauthorized again, exactly as a fresh relay-hub registration would. */
|
|
4
|
+
function createRelayUseAuthorizationTracker() {
|
|
5
|
+
const authorized = /* @__PURE__ */ new Set();
|
|
6
|
+
return {
|
|
7
|
+
isAuthorized: (connection) => authorized.has(connection),
|
|
8
|
+
authorize: (connection) => {
|
|
9
|
+
authorized.add(connection);
|
|
10
|
+
},
|
|
11
|
+
revoke: (connection) => {
|
|
12
|
+
authorized.delete(connection);
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Wraps a RelayHub's own onFrame with the relay:use check: a relay-connect from an unauthorized connection is silently dropped before ever reaching the hub, matching relay-connect-frame's own existing "no error frame defined for this" precedent for an unresolvable request (spec/transport.cddl, relay-hub.ts's own handleFrame comment for an unknown target-device). Every other frame type passes through unconditionally -- gossip needs no gate (it carries no relay effect on its own), and relay-data needs none either, since it can only ever act within a pairing that itself required an authorized relay-connect to establish in the first place; gating it again would be redundant, not additionally safe.
|
|
18
|
+
*/
|
|
19
|
+
function createGatedRelayFrameHandler(options) {
|
|
20
|
+
return async function gatedOnFrame(connection, frame) {
|
|
21
|
+
if (frame.type === "relay-connect" && !options.isAuthorized(connection)) return;
|
|
22
|
+
await options.onFrame(connection, frame);
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
//#endregion
|
|
26
|
+
exports.createGatedRelayFrameHandler = createGatedRelayFrameHandler;
|
|
27
|
+
exports.createRelayUseAuthorizationTracker = createRelayUseAuthorizationTracker;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { D as Frame } from "../protocol-l9RMR3s5.cjs";
|
|
2
|
+
import { t as Connection } from "../transport-Baipikk7.cjs";
|
|
3
|
+
//#region src/domain/relay-use-gate.d.ts
|
|
4
|
+
export interface RelayUseAuthorizationTracker {
|
|
5
|
+
/** True once authorize() has been called for this exact connection and revoke() has not been called since. */
|
|
6
|
+
isAuthorized: (connection: Readonly<Connection>) => boolean;
|
|
7
|
+
/** Marks a connection as holding a currently-valid relay:use grant -- called from the accepting side of a capability-request/capability-grant-ok exchange once it decides to grant. */
|
|
8
|
+
authorize: (connection: Readonly<Connection>) => void;
|
|
9
|
+
/** Withdraws a connection's own authorization, e.g. once its granted token's own expiry has passed or it was revoked. Tracking here is a live boolean, not the token itself -- a caller enforcing expiry calls this when it decides the grant is no longer current. */
|
|
10
|
+
revoke: (connection: Readonly<Connection>) => void;
|
|
11
|
+
}
|
|
12
|
+
/** An in-memory, per-connection authorization set. Keyed by Connection identity (reference equality), matching RelayHub's own registry -- a connection that reconnects is a new object and starts unauthorized again, exactly as a fresh relay-hub registration would. */
|
|
13
|
+
export declare function createRelayUseAuthorizationTracker(): RelayUseAuthorizationTracker;
|
|
14
|
+
export interface CreateGatedRelayFrameHandlerOptions {
|
|
15
|
+
/** The RelayHub method this gate wraps -- typically a real RelayHub's own onFrame (wire-mesh#102), but accepting just the one method it needs keeps this module decoupled from RelayHub's full interface. */
|
|
16
|
+
onFrame: (connection: Readonly<Connection>, frame: Frame) => Promise<void>;
|
|
17
|
+
isAuthorized: (connection: Readonly<Connection>) => boolean;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Wraps a RelayHub's own onFrame with the relay:use check: a relay-connect from an unauthorized connection is silently dropped before ever reaching the hub, matching relay-connect-frame's own existing "no error frame defined for this" precedent for an unresolvable request (spec/transport.cddl, relay-hub.ts's own handleFrame comment for an unknown target-device). Every other frame type passes through unconditionally -- gossip needs no gate (it carries no relay effect on its own), and relay-data needs none either, since it can only ever act within a pairing that itself required an authorized relay-connect to establish in the first place; gating it again would be redundant, not additionally safe.
|
|
21
|
+
*/
|
|
22
|
+
export declare function createGatedRelayFrameHandler(options: Readonly<CreateGatedRelayFrameHandlerOptions>): (connection: Readonly<Connection>, frame: Frame) => Promise<void>;
|
|
23
|
+
//#endregion
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { D as Frame } from "../protocol-l9RMR3s5.mjs";
|
|
2
|
+
import { t as Connection } from "../transport-DWbvUc3L.mjs";
|
|
3
|
+
//#region src/domain/relay-use-gate.d.ts
|
|
4
|
+
export interface RelayUseAuthorizationTracker {
|
|
5
|
+
/** True once authorize() has been called for this exact connection and revoke() has not been called since. */
|
|
6
|
+
isAuthorized: (connection: Readonly<Connection>) => boolean;
|
|
7
|
+
/** Marks a connection as holding a currently-valid relay:use grant -- called from the accepting side of a capability-request/capability-grant-ok exchange once it decides to grant. */
|
|
8
|
+
authorize: (connection: Readonly<Connection>) => void;
|
|
9
|
+
/** Withdraws a connection's own authorization, e.g. once its granted token's own expiry has passed or it was revoked. Tracking here is a live boolean, not the token itself -- a caller enforcing expiry calls this when it decides the grant is no longer current. */
|
|
10
|
+
revoke: (connection: Readonly<Connection>) => void;
|
|
11
|
+
}
|
|
12
|
+
/** An in-memory, per-connection authorization set. Keyed by Connection identity (reference equality), matching RelayHub's own registry -- a connection that reconnects is a new object and starts unauthorized again, exactly as a fresh relay-hub registration would. */
|
|
13
|
+
export declare function createRelayUseAuthorizationTracker(): RelayUseAuthorizationTracker;
|
|
14
|
+
export interface CreateGatedRelayFrameHandlerOptions {
|
|
15
|
+
/** The RelayHub method this gate wraps -- typically a real RelayHub's own onFrame (wire-mesh#102), but accepting just the one method it needs keeps this module decoupled from RelayHub's full interface. */
|
|
16
|
+
onFrame: (connection: Readonly<Connection>, frame: Frame) => Promise<void>;
|
|
17
|
+
isAuthorized: (connection: Readonly<Connection>) => boolean;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Wraps a RelayHub's own onFrame with the relay:use check: a relay-connect from an unauthorized connection is silently dropped before ever reaching the hub, matching relay-connect-frame's own existing "no error frame defined for this" precedent for an unresolvable request (spec/transport.cddl, relay-hub.ts's own handleFrame comment for an unknown target-device). Every other frame type passes through unconditionally -- gossip needs no gate (it carries no relay effect on its own), and relay-data needs none either, since it can only ever act within a pairing that itself required an authorized relay-connect to establish in the first place; gating it again would be redundant, not additionally safe.
|
|
21
|
+
*/
|
|
22
|
+
export declare function createGatedRelayFrameHandler(options: Readonly<CreateGatedRelayFrameHandlerOptions>): (connection: Readonly<Connection>, frame: Frame) => Promise<void>;
|
|
23
|
+
//#endregion
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
//#region src/domain/relay-use-gate.ts
|
|
2
|
+
/** An in-memory, per-connection authorization set. Keyed by Connection identity (reference equality), matching RelayHub's own registry -- a connection that reconnects is a new object and starts unauthorized again, exactly as a fresh relay-hub registration would. */
|
|
3
|
+
function createRelayUseAuthorizationTracker() {
|
|
4
|
+
const authorized = /* @__PURE__ */ new Set();
|
|
5
|
+
return {
|
|
6
|
+
isAuthorized: (connection) => authorized.has(connection),
|
|
7
|
+
authorize: (connection) => {
|
|
8
|
+
authorized.add(connection);
|
|
9
|
+
},
|
|
10
|
+
revoke: (connection) => {
|
|
11
|
+
authorized.delete(connection);
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Wraps a RelayHub's own onFrame with the relay:use check: a relay-connect from an unauthorized connection is silently dropped before ever reaching the hub, matching relay-connect-frame's own existing "no error frame defined for this" precedent for an unresolvable request (spec/transport.cddl, relay-hub.ts's own handleFrame comment for an unknown target-device). Every other frame type passes through unconditionally -- gossip needs no gate (it carries no relay effect on its own), and relay-data needs none either, since it can only ever act within a pairing that itself required an authorized relay-connect to establish in the first place; gating it again would be redundant, not additionally safe.
|
|
17
|
+
*/
|
|
18
|
+
function createGatedRelayFrameHandler(options) {
|
|
19
|
+
return async function gatedOnFrame(connection, frame) {
|
|
20
|
+
if (frame.type === "relay-connect" && !options.isAuthorized(connection)) return;
|
|
21
|
+
await options.onFrame(connection, frame);
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
//#endregion
|
|
25
|
+
export { createGatedRelayFrameHandler, createRelayUseAuthorizationTracker };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wire-mesh-core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.23.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"packageManager": "pnpm@12.4.1+sha512.2e81e399d73fe8390dab25e06aa788ab7a5908248d2f5a370f82b481147a6a7a367bf8048f9a6fdb6460f21a66f0542dedb8b94ca2c8723596741920b1656d4c",
|
|
6
6
|
"repository": {
|
|
@@ -114,6 +114,10 @@
|
|
|
114
114
|
"import": "./dist/domain/relay-hub.mjs",
|
|
115
115
|
"require": "./dist/domain/relay-hub.cjs"
|
|
116
116
|
},
|
|
117
|
+
"./domain/relay-use-gate": {
|
|
118
|
+
"import": "./dist/domain/relay-use-gate.mjs",
|
|
119
|
+
"require": "./dist/domain/relay-use-gate.cjs"
|
|
120
|
+
},
|
|
117
121
|
"./domain/revocation-view": {
|
|
118
122
|
"import": "./dist/domain/revocation-view.mjs",
|
|
119
123
|
"require": "./dist/domain/revocation-view.cjs"
|