@hediet/linkrpc-hub 0.0.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.
Files changed (50) hide show
  1. package/README.md +101 -0
  2. package/dist/chunks/config-BCvkg7jv.d.ts +566 -0
  3. package/dist/chunks/configFile-y6Phntqu.d.ts +9 -0
  4. package/dist/chunks/connectionTokenBinder.interfaces-B-beCg06.js +40 -0
  5. package/dist/chunks/connectionTokenBinder.interfaces-B-beCg06.js.map +1 -0
  6. package/dist/chunks/connectionTokenBinder.interfaces-BhzQ1DTS.d.ts +36 -0
  7. package/dist/chunks/hubConnectionAcceptor-B5-8JsFY.js +2768 -0
  8. package/dist/chunks/hubConnectionAcceptor-B5-8JsFY.js.map +1 -0
  9. package/dist/chunks/hubConnectionAcceptor-BwydvFa4.d.ts +1560 -0
  10. package/dist/chunks/index-CLIUrV88.d.ts +481 -0
  11. package/dist/chunks/node-CTXsQ6oa.js +460 -0
  12. package/dist/chunks/node-CTXsQ6oa.js.map +1 -0
  13. package/dist/chunks/nodeTransit-CWeFnbwt.js +444 -0
  14. package/dist/chunks/nodeTransit-CWeFnbwt.js.map +1 -0
  15. package/dist/chunks/nodeTransit-cmtZgdpW.d.ts +226 -0
  16. package/dist/chunks/runHub-P3YUwwdv.js +1797 -0
  17. package/dist/chunks/runHub-P3YUwwdv.js.map +1 -0
  18. package/dist/chunks/server-BAxchQhy.js +1368 -0
  19. package/dist/chunks/server-BAxchQhy.js.map +1 -0
  20. package/dist/cli.d.ts +1 -0
  21. package/dist/cli.js +38 -0
  22. package/dist/cli.js.map +1 -0
  23. package/dist/config.d.ts +2 -0
  24. package/dist/config.js +305 -0
  25. package/dist/config.js.map +1 -0
  26. package/dist/configFile.d.ts +2 -0
  27. package/dist/configFile.js +27 -0
  28. package/dist/configFile.js.map +1 -0
  29. package/dist/engine/runHub.d.ts +42 -0
  30. package/dist/engine/runHub.js +2 -0
  31. package/dist/hub/server/client.d.ts +2 -0
  32. package/dist/hub/server/client.js +2 -0
  33. package/dist/hub/server/connectionTokenBinder.d.ts +2 -0
  34. package/dist/hub/server/connectionTokenBinder.js +2 -0
  35. package/dist/hub/server/index.d.ts +5 -0
  36. package/dist/hub/server/index.js +5 -0
  37. package/dist/hub/server/node/index.d.ts +218 -0
  38. package/dist/hub/server/node/index.js +2 -0
  39. package/dist/hub/server/transit.d.ts +2 -0
  40. package/dist/hub/server/transit.js +2 -0
  41. package/dist/index.d.ts +512 -0
  42. package/dist/index.js +309 -0
  43. package/dist/index.js.map +1 -0
  44. package/dist/serve.d.ts +14 -0
  45. package/dist/serve.js +31 -0
  46. package/dist/serve.js.map +1 -0
  47. package/dist/spawn.d.ts +12 -0
  48. package/dist/spawn.js +25 -0
  49. package/dist/spawn.js.map +1 -0
  50. package/package.json +83 -0
@@ -0,0 +1,2768 @@
1
+ import { t as connectionTokenBinderInterface } from "./connectionTokenBinder.interfaces-B-beCg06.js";
2
+ import { ErrorCode, LinkRpcConnection, RpcError, STREAM_METHOD, StreamControlReason, StreamControlType, StreamDir, TransportPair, directoryInterface, directoryWatchNever, isNotification, isRequest, isResponse, methodNameToTarget, nodeInterface, parseMethodName, permits, registerLazyIdentityOnOverlay, schemasInterface, verifyCall, verifyRpcCall } from "@hediet/linkrpc";
3
+ import { ROOT_SERVICE_ID, SERVICE_ID_SEPARATOR, hubGrantedServiceIdInterface, hubServiceIdRegistryInterface, isServiceIdUnder, isValidServiceId, normalizeServiceIdScopes, serviceIdMatchesScopes } from "@hediet/linkrpc/hub/common";
4
+ import { randomUUID } from "node:crypto";
5
+ import { BoundedTrafficSubscription, TrafficWatchFlowTracker, nodeInterface as nodeInterface$1, topologyInterface, trafficInterface } from "@hediet/linkrpc/inspection";
6
+ import { safeParse } from "zod/v4/core";
7
+ //#region src/hub/server/routing/forwardingTable.ts
8
+ /**
9
+ * A longest-prefix routing table keyed by {@link ServiceId} prefixes — the
10
+ * hub's equivalent of an IP forwarding table.
11
+ *
12
+ * A prefix `"a/b"` owns every serviceId equal to it or beneath it
13
+ * (`"a/b"`, `"a/b/c"`, …) *unless* a longer claimed prefix matches first.
14
+ * Matching is segment-aware: `"a/bc"` is **not** under `"a/b"`.
15
+ *
16
+ * The table is intentionally a plain stateful container with no opinion on
17
+ * *who* may claim what — that policy lives in {@link Hub.claimPrefix}. This
18
+ * keeps the data structure trivially testable in isolation.
19
+ */
20
+ var ForwardingTable = class {
21
+ _entries = /* @__PURE__ */ new Map();
22
+ /**
23
+ * Reverse index: value → the set of prefixes claimed for it. Kept in
24
+ * lock-step with {@link _entries} so {@link deleteByValue} (used when a
25
+ * link detaches) is O(claims-for-that-value) instead of an O(size) scan.
26
+ */
27
+ _prefixesByValue = /* @__PURE__ */ new Map();
28
+ /** Number of claimed prefixes. */
29
+ get size() {
30
+ return this._entries.size;
31
+ }
32
+ has(prefix) {
33
+ return this._entries.has(prefix);
34
+ }
35
+ get(prefix) {
36
+ return this._entries.get(prefix);
37
+ }
38
+ /**
39
+ * Claim `prefix` for `value`. Overwrites any existing claim — callers
40
+ * that want claim-once semantics must check {@link has} first (the hub
41
+ * does).
42
+ */
43
+ set(prefix, value) {
44
+ const prev = this._entries.get(prefix);
45
+ if (prev !== void 0 && prev !== value) this._dropFromIndex(prev, prefix);
46
+ this._entries.set(prefix, value);
47
+ let prefixes = this._prefixesByValue.get(value);
48
+ if (!prefixes) {
49
+ prefixes = /* @__PURE__ */ new Set();
50
+ this._prefixesByValue.set(value, prefixes);
51
+ }
52
+ prefixes.add(prefix);
53
+ }
54
+ delete(prefix) {
55
+ const value = this._entries.get(prefix);
56
+ if (value === void 0) return false;
57
+ this._entries.delete(prefix);
58
+ this._dropFromIndex(value, prefix);
59
+ return true;
60
+ }
61
+ /**
62
+ * Remove every claim bound to `value` (matched by reference/equality).
63
+ * O(number of prefixes that value owns). Returns the removed prefixes.
64
+ */
65
+ deleteByValue(value) {
66
+ const prefixes = this._prefixesByValue.get(value);
67
+ if (!prefixes) return [];
68
+ this._prefixesByValue.delete(value);
69
+ for (const prefix of prefixes) this._entries.delete(prefix);
70
+ return [...prefixes];
71
+ }
72
+ _dropFromIndex(value, prefix) {
73
+ const prefixes = this._prefixesByValue.get(value);
74
+ if (!prefixes) return;
75
+ prefixes.delete(prefix);
76
+ if (prefixes.size === 0) this._prefixesByValue.delete(value);
77
+ }
78
+ entries() {
79
+ return this._entries.entries();
80
+ }
81
+ prefixes() {
82
+ return [...this._entries.keys()];
83
+ }
84
+ /**
85
+ * Find the value owning the longest claimed prefix of `serviceId`.
86
+ * Walks up the `'/'` segments — `"a/b/c"` tries `"a/b/c"`, `"a/b"`,
87
+ * `"a"` in that order — and returns the first hit, or `undefined` if
88
+ * none of its ancestors are claimed.
89
+ */
90
+ longestPrefixMatch(serviceId) {
91
+ let candidate = serviceId;
92
+ while (candidate.length > 0) {
93
+ const value = this._entries.get(candidate);
94
+ if (value !== void 0) return {
95
+ prefix: candidate,
96
+ value
97
+ };
98
+ const slash = candidate.lastIndexOf(SERVICE_ID_SEPARATOR);
99
+ if (slash === -1) break;
100
+ candidate = candidate.slice(0, slash);
101
+ }
102
+ }
103
+ };
104
+ /**
105
+ * Validate a forwarding-table **prefix**: a non-root {@link ServiceId}. The
106
+ * root (`""`) is a valid service id but cannot be claimed as a prefix (it
107
+ * would capture every address); the uplink is the route of last resort
108
+ * instead. Returns an error string, or `undefined` when well-formed.
109
+ */
110
+ function validatePrefix(prefix) {
111
+ if (typeof prefix !== "string" || prefix === ROOT_SERVICE_ID) return "prefix required";
112
+ if (!isValidServiceId(prefix)) return "prefix must be a valid serviceId (no leading/trailing/empty '/' segments)";
113
+ }
114
+ //#endregion
115
+ //#region src/hub/server/routing/participantRoots.ts
116
+ /** Reconciles participant-root queries and watches over the Hub's routing links. */
117
+ var ParticipantRoots = class {
118
+ _options;
119
+ constructor(_options) {
120
+ this._options = _options;
121
+ }
122
+ async query(method, params, excludePrefix) {
123
+ return (await Promise.all([...this._participantLinks(excludePrefix)].map((link) => this._options.requestOnLink(link, method, params).then((result) => result, () => void 0)))).filter((result) => result !== void 0);
124
+ }
125
+ watch(method, params, onTick, excludePrefix) {
126
+ const watches = /* @__PURE__ */ new Map();
127
+ let disposed = false;
128
+ const stopState = (state) => {
129
+ if (state.retry !== void 0) clearTimeout(state.retry);
130
+ state.retry = void 0;
131
+ state.watch?.dispose();
132
+ state.watch = void 0;
133
+ };
134
+ const start = (link, state) => {
135
+ if (disposed || !this._participantLinks(excludePrefix).has(link)) return;
136
+ state.watch = this._options.watchOnLink(link, method, params, onTick, () => {
137
+ queueMicrotask(() => {
138
+ if (disposed || watches.get(link) !== state) return;
139
+ state.watch = void 0;
140
+ state.retry = setTimeout(() => {
141
+ state.retry = void 0;
142
+ start(link, state);
143
+ }, state.retryDelayMs);
144
+ state.retryDelayMs = Math.min(5e3, state.retryDelayMs * 2);
145
+ });
146
+ }, () => {
147
+ state.retryDelayMs = 200;
148
+ onTick();
149
+ });
150
+ };
151
+ const reconcile = () => {
152
+ const current = this._participantLinks(excludePrefix);
153
+ for (const [link, state] of watches) {
154
+ if (current.has(link)) continue;
155
+ stopState(state);
156
+ watches.delete(link);
157
+ }
158
+ for (const link of current) {
159
+ if (watches.has(link)) continue;
160
+ const state = { retryDelayMs: 200 };
161
+ watches.set(link, state);
162
+ start(link, state);
163
+ }
164
+ };
165
+ reconcile();
166
+ const unsubscribe = this._options.onDidChangeRouting(reconcile);
167
+ return { dispose: () => {
168
+ if (disposed) return;
169
+ disposed = true;
170
+ unsubscribe();
171
+ for (const state of watches.values()) stopState(state);
172
+ watches.clear();
173
+ } };
174
+ }
175
+ _participantLinks(excludePrefix) {
176
+ const exclude = excludePrefix !== void 0 ? this._options.prefixOwner(excludePrefix) : void 0;
177
+ const uplink = this._options.uplink();
178
+ const links = /* @__PURE__ */ new Set();
179
+ for (const [, link] of this._options.forwardingEntries()) {
180
+ if (link === exclude || link === uplink) continue;
181
+ links.add(link);
182
+ }
183
+ return links;
184
+ }
185
+ };
186
+ //#endregion
187
+ //#region src/hub/server/routing/peerDiscovery.ts
188
+ const GET_NODE_ID_METHOD = `${nodeInterface$1.info.id}::getNodeId`;
189
+ /** Background policy for direct-peer identity; forwarding never waits for it. */
190
+ var PeerDiscovery = class {
191
+ _options;
192
+ _peers = /* @__PURE__ */ new WeakMap();
193
+ _timeoutMs;
194
+ constructor(_options) {
195
+ this._options = _options;
196
+ this._timeoutMs = _options.timeoutMs ?? 1e3;
197
+ if (!Number.isFinite(this._timeoutMs) || this._timeoutMs <= 0) throw new Error("peerIdentificationTimeoutMs must be a positive finite number");
198
+ }
199
+ attach(link) {
200
+ if (this._peers.has(link)) return;
201
+ const state = {
202
+ status: "pending",
203
+ retryDelayMs: 200
204
+ };
205
+ this._peers.set(link, state);
206
+ this._schedule(link, state, 0);
207
+ }
208
+ detach(link) {
209
+ const state = this._peers.get(link);
210
+ if (state?.timer !== void 0) clearTimeout(state.timer);
211
+ this._peers.delete(link);
212
+ }
213
+ info(link) {
214
+ return this._peers.get(link)?.info;
215
+ }
216
+ status(link) {
217
+ return this._peers.get(link)?.status ?? "pending";
218
+ }
219
+ identify(link) {
220
+ const state = this._peers.get(link);
221
+ if (state === void 0) return Promise.reject(/* @__PURE__ */ new Error("identifyPeer: link is detached"));
222
+ if (state.info !== void 0) return Promise.resolve(state.info);
223
+ if (state.attempt !== void 0) return state.attempt;
224
+ if (state.timer !== void 0) clearTimeout(state.timer);
225
+ state.timer = void 0;
226
+ const attempt = Promise.resolve().then(async () => {
227
+ if (this._peers.get(link) !== state) throw new Error("identifyPeer: link is detached");
228
+ const raw = await this._options.request(link, this._timeoutMs);
229
+ const parsed = safeParse(nodeInterface$1.members.getNodeId.resultSchema, raw);
230
+ if (!parsed.success || parsed.data.nodeId.length === 0 || parsed.data.portId.length === 0) throw new Error(`identifyPeer: invalid result from ${GET_NODE_ID_METHOD}`);
231
+ const info = parsed.data;
232
+ if (this._peers.get(link) !== state) throw new Error("identifyPeer: link is detached");
233
+ state.info = info;
234
+ state.status = "identified";
235
+ this._options.onDidChange();
236
+ return info;
237
+ }).catch((error) => {
238
+ if (this._peers.get(link) === state) {
239
+ const status = error instanceof RpcError && error.code === ErrorCode.methodNotFound ? "unsupported" : "error";
240
+ if (state.status !== status) {
241
+ state.status = status;
242
+ this._options.onDidChange();
243
+ }
244
+ const delay = state.status === "unsupported" ? 3e4 : state.retryDelayMs;
245
+ state.retryDelayMs = Math.min(5e3, state.retryDelayMs * 2);
246
+ this._schedule(link, state, delay * (.8 + Math.random() * .4));
247
+ }
248
+ throw new Error(`identifyPeer: ${GET_NODE_ID_METHOD} failed: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
249
+ }).finally(() => {
250
+ state.attempt = void 0;
251
+ });
252
+ state.attempt = attempt;
253
+ return attempt;
254
+ }
255
+ _schedule(link, state, delayMs) {
256
+ if (this._peers.get(link) !== state) return;
257
+ state.timer = setTimeout(() => {
258
+ state.timer = void 0;
259
+ if (this._peers.get(link) !== state) return;
260
+ this.identify(link).catch(() => {});
261
+ }, delayMs);
262
+ state.timer.unref();
263
+ }
264
+ };
265
+ //#endregion
266
+ //#region src/hub/server/routing/routingTopology.ts
267
+ /**
268
+ * Owns the Hub's inspection-facing topology state. Routing remains in Hub;
269
+ * this class only consumes snapshots and low-level request callbacks.
270
+ */
271
+ var RoutingTopology = class {
272
+ _options;
273
+ _portIds = /* @__PURE__ */ new WeakMap();
274
+ _handleLinks = /* @__PURE__ */ new WeakMap();
275
+ _peers;
276
+ _inspectionServices = /* @__PURE__ */ new WeakMap();
277
+ _managedRoutingTopologies = /* @__PURE__ */ new WeakMap();
278
+ _managedTopologyFragments = /* @__PURE__ */ new Set();
279
+ constructor(_options) {
280
+ this._options = _options;
281
+ this._peers = new PeerDiscovery({
282
+ timeoutMs: _options.peerIdentificationTimeoutMs,
283
+ request: (link, timeoutMs) => _options.requestOnLink(link, GET_NODE_ID_METHOD, {}, timeoutMs),
284
+ onDidChange: _options.onDidChange
285
+ });
286
+ }
287
+ attachLink(link) {
288
+ this._peers.attach(link);
289
+ }
290
+ portId(link) {
291
+ let id = this._portIds.get(link);
292
+ if (id === void 0) {
293
+ id = this._options.generateTopologyId("port");
294
+ this._portIds.set(link, id);
295
+ }
296
+ return id;
297
+ }
298
+ registerHandle(handle, link) {
299
+ this._handleLinks.set(handle, link);
300
+ }
301
+ identifyPeer(link) {
302
+ return this._peers.identify(link);
303
+ }
304
+ registerManagedRoutingTopology(attached, topology) {
305
+ const link = this._attachedLink(attached, "registerManagedRoutingTopology");
306
+ this._managedRoutingTopologies.set(link, topology);
307
+ this._options.onDidChange();
308
+ let disposed = false;
309
+ return { dispose: () => {
310
+ if (disposed) return;
311
+ disposed = true;
312
+ if (this._managedRoutingTopologies.get(link) !== topology) return;
313
+ this._managedRoutingTopologies.delete(link);
314
+ this._options.onDidChange();
315
+ } };
316
+ }
317
+ registerManagedTopologyFragment(fragment) {
318
+ this._managedTopologyFragments.add(fragment);
319
+ this._options.onDidChange();
320
+ let disposed = false;
321
+ return { dispose: () => {
322
+ if (disposed) return;
323
+ disposed = true;
324
+ this._managedTopologyFragments.delete(fragment);
325
+ this._options.onDidChange();
326
+ } };
327
+ }
328
+ markInspectionService(attached, serviceId) {
329
+ const link = this._attachedLink(attached, "markInspectionService");
330
+ let services = this._inspectionServices.get(link);
331
+ if (services === void 0) {
332
+ services = /* @__PURE__ */ new Set();
333
+ this._inspectionServices.set(link, services);
334
+ }
335
+ services.add(serviceId);
336
+ }
337
+ isInspectionService(link, serviceId) {
338
+ return this._inspectionServices.get(link)?.has(serviceId) === true;
339
+ }
340
+ getTopologyGraph(observerServiceId) {
341
+ const allLinks = [...this._options.links()];
342
+ const visibleLinks = allLinks.filter((link) => !this._inspectionServices.has(link));
343
+ const hubPorts = allLinks.map((link) => ({
344
+ portId: this.portId(link),
345
+ label: this._options.edgeId(link)
346
+ }));
347
+ for (const link of visibleLinks) for (const hubLink of this._managedRoutingTopologies.get(link)?.hubLinks ?? []) if (!hubPorts.some((port) => port.portId === hubLink.hubPortId)) hubPorts.push({
348
+ portId: hubLink.hubPortId,
349
+ label: hubLink.label ?? hubLink.hubPortId
350
+ });
351
+ const nodes = [{
352
+ nodeId: this._options.nodeId,
353
+ kind: "hub",
354
+ ...this._options.debugName !== void 0 ? { label: this._options.debugName } : {},
355
+ ...this._options.descriptors !== void 0 ? { descriptors: [...this._options.descriptors] } : {},
356
+ ports: hubPorts
357
+ }];
358
+ const peerNodes = /* @__PURE__ */ new Map();
359
+ const links = [];
360
+ for (const link of visibleLinks) {
361
+ const peer = this._topologyPeer(link);
362
+ const managed = this._managedRoutingTopologies.get(link);
363
+ let peerNode = peerNodes.get(peer.nodeId);
364
+ if (peerNode === void 0) {
365
+ peerNode = {
366
+ nodeId: peer.nodeId,
367
+ ports: []
368
+ };
369
+ peerNodes.set(peer.nodeId, peerNode);
370
+ nodes.push(peerNode);
371
+ }
372
+ if (!peerNode.ports.some((port) => port.portId === peer.portId)) peerNode.ports.push({ portId: peer.portId });
373
+ if (managed !== void 0) {
374
+ nodes.push({
375
+ ...managed.node,
376
+ ports: [...managed.node.ports]
377
+ });
378
+ for (const adjacentNode of managed.adjacentNodes ?? []) nodes.push({
379
+ ...adjacentNode,
380
+ ports: [...adjacentNode.ports]
381
+ });
382
+ for (const hubLink of managed.hubLinks) links.push({
383
+ from: {
384
+ nodeId: this._options.nodeId,
385
+ portId: hubLink.hubPortId
386
+ },
387
+ to: {
388
+ nodeId: managed.node.nodeId,
389
+ portId: hubLink.nodePortId
390
+ },
391
+ ...hubLink.label !== void 0 ? { label: hubLink.label } : {}
392
+ });
393
+ links.push({
394
+ from: {
395
+ nodeId: managed.node.nodeId,
396
+ portId: managed.peerPortId
397
+ },
398
+ to: peer,
399
+ ...managed.peerLinkLabel !== void 0 ? { label: managed.peerLinkLabel } : {},
400
+ ...managed.peerTransport !== void 0 ? { transport: managed.peerTransport } : {},
401
+ peerState: this._peers.status(link)
402
+ });
403
+ links.push(...managed.adjacentLinks ?? []);
404
+ continue;
405
+ }
406
+ const transport = this._options.transportInfo(link);
407
+ links.push({
408
+ from: {
409
+ nodeId: this._options.nodeId,
410
+ portId: this.portId(link)
411
+ },
412
+ to: peer,
413
+ label: this._options.edgeId(link),
414
+ peerState: this._peers.status(link),
415
+ ...transport !== void 0 ? { transport } : {}
416
+ });
417
+ }
418
+ const routes = [];
419
+ for (const [serviceId, link] of this._options.forwardingEntries()) {
420
+ if (this.isInspectionService(link, serviceId)) {
421
+ routes.push({
422
+ serviceId,
423
+ nodeId: this._options.nodeId,
424
+ portId: this.portId(link),
425
+ match: "prefix"
426
+ });
427
+ continue;
428
+ }
429
+ const peer = this._topologyPeer(link);
430
+ routes.push({
431
+ serviceId,
432
+ nodeId: peer.nodeId,
433
+ portId: peer.portId,
434
+ match: "prefix"
435
+ });
436
+ }
437
+ for (const fragment of this._managedTopologyFragments) {
438
+ nodes.push(...fragment.nodes.map((node) => ({
439
+ ...node,
440
+ ports: [...node.ports]
441
+ })));
442
+ links.push(...fragment.links ?? []);
443
+ routes.push(...fragment.routes ?? []);
444
+ }
445
+ return {
446
+ observerServiceId,
447
+ entryNodeId: this._options.nodeId,
448
+ nodes,
449
+ links,
450
+ routes
451
+ };
452
+ }
453
+ cleanupLink(link) {
454
+ this._inspectionServices.delete(link);
455
+ this._peers.detach(link);
456
+ this._managedRoutingTopologies.delete(link);
457
+ }
458
+ _attachedLink(attached, operation) {
459
+ const link = this._handleLinks.get(attached);
460
+ if (link === void 0 || !this._options.isAttached(link)) throw new Error(`${operation}: link is not attached to this hub`);
461
+ return link;
462
+ }
463
+ _topologyPeer(link) {
464
+ const identified = this._peers.info(link);
465
+ if (identified !== void 0) return identified;
466
+ const localPortId = this.portId(link);
467
+ return {
468
+ nodeId: `${this._options.nodeId}:unidentified:${localPortId}`,
469
+ portId: `unidentified:${localPortId}`
470
+ };
471
+ }
472
+ };
473
+ //#endregion
474
+ //#region src/hub/server/routing/routingHub.ts
475
+ const DEFAULT_IDLE_TIMEOUT_MS = 18e5;
476
+ /**
477
+ * A **Hub** is the entire routing concept of linkrpc v2 — the only noun.
478
+ * It is a tiny message router with an unauthenticated topology-correlation
479
+ * identity and exactly three moving
480
+ * parts, all expressed as {@link IMessageTransport} links:
481
+ *
482
+ * - **forwarding table** — longest-prefix serviceId → link map (the
483
+ * downstream neighbours / claimed subnets).
484
+ * - **loopback** — an optional link to the local root services (`bare`/
485
+ * `interface`-form calls land here and *never* leave the hub). This is
486
+ * the routing equivalent of `127.0.0.1`.
487
+ * - **uplink** — an optional default route. A hub *with* an uplink is also
488
+ * a participant of its parent hub, which is how hubs **nest** (a VS Code
489
+ * window hub uplinks to an OS hub uplinks to a home-server hub).
490
+ *
491
+ * Routing is pure longest-prefix forwarding with JSON-RPC id rewriting:
492
+ * a forwarded request keeps its full method name verbatim; only its `id`
493
+ * is swapped for a hub-local one so responses can be demultiplexed back to
494
+ * the originating link. There is no notion of identity, capabilities, or
495
+ * trust at this layer — those are policies layered on top.
496
+ *
497
+ * The single privileged write seam is {@link claimPrefix}; everything else
498
+ * is mechanical message shuffling.
499
+ */
500
+ var Hub = class {
501
+ _options;
502
+ /** Unauthenticated topology-correlation identity for this hub. */
503
+ nodeId;
504
+ _table = new ForwardingTable();
505
+ _links = /* @__PURE__ */ new Set();
506
+ _pending = /* @__PURE__ */ new Map();
507
+ _uplink;
508
+ _loopback;
509
+ _nextId = 1;
510
+ _log;
511
+ _transitObservers = /* @__PURE__ */ new Set();
512
+ /**
513
+ * Listeners notified whenever the forwarding table changes (a prefix is
514
+ * claimed or released, or a link detaches dropping its claims). Coarse, by
515
+ * design: it carries no payload — consumers re-query the directory. This is
516
+ * the "poll now" nudge that backs `hubrpc.directory::watch`.
517
+ */
518
+ _routingListeners = /* @__PURE__ */ new Set();
519
+ _topologyListeners = /* @__PURE__ */ new Set();
520
+ /** Stable inspection label per link; assigned lazily / by prefix claim. */
521
+ _edgeIds = /* @__PURE__ */ new WeakMap();
522
+ _transportInfo = /* @__PURE__ */ new WeakMap();
523
+ _nextEdgeId = 1;
524
+ _topology;
525
+ _participantRoots;
526
+ /** Per-request idle timeout in ms; `<= 0` disables. See {@link HubOptions.idleTimeoutMs}. */
527
+ _idleMs;
528
+ constructor(_options = {}) {
529
+ this._options = _options;
530
+ const generateTopologyId = _options.generateTopologyId ?? (() => randomUUID());
531
+ this.nodeId = _options.nodeId ?? generateTopologyId("node");
532
+ this._log = _options.logger;
533
+ if (_options.onTransit !== void 0) this._transitObservers.add(_options.onTransit);
534
+ this._idleMs = _options.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS;
535
+ this._topology = new RoutingTopology({
536
+ nodeId: this.nodeId,
537
+ generateTopologyId,
538
+ debugName: _options.debugName,
539
+ descriptors: _options.descriptors,
540
+ peerIdentificationTimeoutMs: _options.peerIdentificationTimeoutMs,
541
+ links: () => this._links,
542
+ forwardingEntries: () => this._table.entries(),
543
+ edgeId: (link) => this._edgeId(link),
544
+ transportInfo: (link) => this._transportInfo.get(link),
545
+ isAttached: (link) => this._links.has(link),
546
+ requestOnLink: (link, method, params, timeoutMs) => this._requestOnLink(link, method, params, timeoutMs),
547
+ onDidChange: () => this._fireTopologyChanged()
548
+ });
549
+ this._participantRoots = new ParticipantRoots({
550
+ forwardingEntries: () => this._table.entries(),
551
+ prefixOwner: (prefix) => this._table.get(prefix),
552
+ uplink: () => this._uplink,
553
+ requestOnLink: (link, method, params) => this._requestOnLink(link, method, params),
554
+ watchOnLink: (link, method, params, onTick, onSettled, onEstablished) => this._watchOnLink(link, method, params, onTick, onSettled, onEstablished),
555
+ onDidChangeRouting: (listener) => this.onDidChangeRouting(listener)
556
+ });
557
+ }
558
+ /** Prefix for log lines; identifies this hub when nested. */
559
+ _tag() {
560
+ return this._options.debugName !== void 0 ? `[${this._options.debugName}] ` : "";
561
+ }
562
+ /** Stable inspection label for a link; auto-assigned on first use. */
563
+ _edgeId(link) {
564
+ let id = this._edgeIds.get(link);
565
+ if (id === void 0) {
566
+ id = `e${this._nextEdgeId++}`;
567
+ this._edgeIds.set(link, id);
568
+ }
569
+ return id;
570
+ }
571
+ /** Give `link` a friendly inspection label (prefix / loopback / uplink). */
572
+ _labelEdge(link, label) {
573
+ const cur = this._edgeIds.get(link);
574
+ if (cur === void 0 || /^e\d+$/.test(cur)) this._edgeIds.set(link, label);
575
+ }
576
+ /** Emit a pre-built node transit. Callers guard construction by observer count. */
577
+ _emit(t) {
578
+ for (const observer of [...this._transitObservers]) try {
579
+ observer(t);
580
+ } catch (err) {
581
+ this._log?.warn(`${this._tag()}transit observer threw: ${String(err)}`);
582
+ }
583
+ }
584
+ _transitEndpoint(link, requestId) {
585
+ return {
586
+ edgeId: this._edgeId(link),
587
+ portId: this._topology.portId(link),
588
+ ...requestId !== void 0 ? { requestId } : {}
589
+ };
590
+ }
591
+ /** Number of live transit observers. */
592
+ get transitObserverCount() {
593
+ return this._transitObservers.size;
594
+ }
595
+ /** Observe routed messages until the returned handle is disposed. */
596
+ observeTransits(observer) {
597
+ this._transitObservers.add(observer);
598
+ let disposed = false;
599
+ return { dispose: () => {
600
+ if (disposed) return;
601
+ disposed = true;
602
+ this._transitObservers.delete(observer);
603
+ } };
604
+ }
605
+ /**
606
+ * Publish a transit from a routing node managed by this Hub. Callers should
607
+ * check {@link transitObserverCount} before constructing the transit.
608
+ */
609
+ publishManagedTransit(transit) {
610
+ if (this._transitObservers.size === 0) return;
611
+ this._emit(transit);
612
+ }
613
+ /** Replace an attached link's direct topology edge with a managed routing node. */
614
+ registerManagedRoutingTopology(attached, topology) {
615
+ return this._topology.registerManagedRoutingTopology(attached, topology);
616
+ }
617
+ /** Add an in-process endpoint or routing fragment to this Hub's topology. */
618
+ registerManagedTopologyFragment(fragment) {
619
+ return this._topology.registerManagedTopologyFragment(fragment);
620
+ }
621
+ /**
622
+ * Subscribe to coarse routing-table changes (prefix claim/release/detach).
623
+ * The listener receives no arguments — it is a "something changed; re-query"
624
+ * nudge. Returns an unsubscribe function.
625
+ */
626
+ onDidChangeRouting(listener) {
627
+ this._routingListeners.add(listener);
628
+ return () => {
629
+ this._routingListeners.delete(listener);
630
+ };
631
+ }
632
+ /** Subscribe to topology changes, including routing and background peer discovery. */
633
+ onDidChangeTopology(listener) {
634
+ this._topologyListeners.add(listener);
635
+ return () => {
636
+ this._topologyListeners.delete(listener);
637
+ };
638
+ }
639
+ /** Number of active routing invalidation observers. */
640
+ get routingObserverCount() {
641
+ return this._routingListeners.size;
642
+ }
643
+ get topologyObserverCount() {
644
+ return this._topologyListeners.size;
645
+ }
646
+ /** Fire all routing-change listeners. Best-effort; one throwing listener does not block the rest. */
647
+ _fireRoutingChanged() {
648
+ this._notifyListeners(this._routingListeners);
649
+ this._fireTopologyChanged();
650
+ }
651
+ _fireTopologyChanged() {
652
+ this._notifyListeners(this._topologyListeners);
653
+ }
654
+ _notifyListeners(listeners) {
655
+ for (const listener of [...listeners]) try {
656
+ listener();
657
+ } catch (err) {
658
+ this._log?.warn(`${this._tag()}change listener threw: ${String(err)}`);
659
+ }
660
+ }
661
+ /** Emit a stream transit with the owning request's correlated endpoints. */
662
+ _emitStream(note, inEp, outEp) {
663
+ if (this._transitObservers.size === 0) return;
664
+ this._emit({
665
+ timeMs: Date.now(),
666
+ nodeId: this.nodeId,
667
+ in: inEp,
668
+ out: outEp,
669
+ disposition: "forwarded",
670
+ kind: "stream",
671
+ method: STREAM_METHOD,
672
+ params: note.params
673
+ });
674
+ }
675
+ /**
676
+ * Snapshot the requests currently in flight through this hub. Each row is
677
+ * one {@link _pending} entry — the authoritative in-flight set — annotated
678
+ * with its method, the edges it bridges, and how long it has waited. Useful
679
+ * for surfacing stuck/slow requests in an inspector.
680
+ */
681
+ pendingRequests() {
682
+ const now = Date.now();
683
+ const out = [];
684
+ for (const [hubId, p] of this._pending) out.push({
685
+ hubId,
686
+ originalId: p.originalId,
687
+ method: p.method,
688
+ originEdgeId: this._edgeId(p.origin),
689
+ targetEdgeId: this._edgeId(p.target),
690
+ ageMs: now - p.startedAtMs
691
+ });
692
+ return out;
693
+ }
694
+ /**
695
+ * Begin routing the messages of `link`. The hub installs itself as the
696
+ * link's listener; do not also attach another listener. Returns a
697
+ * disposable that stops routing the link (detaching the listener and
698
+ * dropping any forwarding entries, uplink/loopback slots, and pending
699
+ * responses bound to it) when disposed. Peer discovery starts in the
700
+ * background; unresolved or unsupported peers never block routing.
701
+ */
702
+ attach(link, opts) {
703
+ const prefixes = opts?.routePrefixes ?? [];
704
+ const seen = /* @__PURE__ */ new Set();
705
+ for (const prefix of prefixes) {
706
+ this._validatePrefixClaim(prefix);
707
+ if (seen.has(prefix)) throw new Error(`claimPrefix: duplicate prefix '${prefix}'`);
708
+ seen.add(prefix);
709
+ }
710
+ if (opts?.exclusiveHubRootHandler === true && this._loopback !== void 0 && this._loopback !== link) throw new Error("exclusiveHubRootHandler: another link is already the hub-root handler");
711
+ if (opts?.edgeId !== void 0) this._edgeIds.set(link, opts.edgeId);
712
+ if (opts?.transport !== void 0) this._transportInfo.set(link, opts.transport);
713
+ let disposed = false;
714
+ const handle = {
715
+ dispose: () => {
716
+ if (disposed) return;
717
+ disposed = true;
718
+ this._detach(link);
719
+ },
720
+ setAsLoopback: () => {
721
+ if (disposed || !this._links.has(link)) throw new Error("setAsLoopback: link is detached");
722
+ this.setLoopback(link);
723
+ },
724
+ addPrefixRoute: (prefix) => this.claimPrefix(link, prefix),
725
+ releasePrefix: (prefix) => this.releasePrefix(prefix),
726
+ edgeId: this._edgeId(link),
727
+ portId: this._topology.portId(link),
728
+ identifyPeer: () => this._topology.identifyPeer(link),
729
+ request: (method, params, timeoutMs) => this._requestOnLink(link, method, params, timeoutMs)
730
+ };
731
+ this._topology.registerHandle(handle, link);
732
+ const isNew = !this._links.has(link);
733
+ const loopbackChanged = opts?.exclusiveHubRootHandler === true && this._loopback !== link;
734
+ if (isNew) {
735
+ this._links.add(link);
736
+ this._topology.attachLink(link);
737
+ }
738
+ if (opts?.exclusiveHubRootHandler === true) {
739
+ this._loopback = link;
740
+ this._labelEdge(link, "loopback");
741
+ }
742
+ for (const prefix of prefixes) this._claimPrefix(link, prefix);
743
+ if (isNew) link.setListener((m) => this._onMessage(link, m));
744
+ if (isNew || loopbackChanged || prefixes.length !== 0) this._fireRoutingChanged();
745
+ return handle;
746
+ }
747
+ /**
748
+ * Attach a fresh in-memory link and hand back its far end alongside the
749
+ * {@link AttachedLink} handle. Creates a {@link TransportPair} internally,
750
+ * {@link attach}es the near side to the hub, and returns the attach handle
751
+ * augmented with `transport` — the far side for a peer (a consumer
752
+ * connection, an in-process participant, …) to drive. Equivalent to
753
+ * `const p = new TransportPair(); const link = hub.attach(p.a);
754
+ * return { ...link, transport: p.b };`. Disposing the returned link also
755
+ * disposes `transport`.
756
+ */
757
+ attachOut(opts) {
758
+ const pair = new TransportPair();
759
+ const link = this.attach(pair.a, opts);
760
+ const result = {
761
+ ...link,
762
+ transport: pair.b,
763
+ dispose: () => {
764
+ link.dispose();
765
+ pair.b.dispose();
766
+ }
767
+ };
768
+ this._topology.registerHandle(result, pair.a);
769
+ return result;
770
+ }
771
+ _detach(link) {
772
+ if (!this._links.has(link)) return;
773
+ link.setListener(void 0);
774
+ this._links.delete(link);
775
+ this._topology.cleanupLink(link);
776
+ this._table.deleteByValue(link);
777
+ if (this._uplink === link) this._uplink = void 0;
778
+ if (this._loopback === link) this._loopback = void 0;
779
+ for (const [id, p] of [...this._pending]) if (p.origin === link) {
780
+ this._injectCancel(p.target, id, StreamControlReason.clientDisconnected);
781
+ if (this._transitObservers.size !== 0) this._emit({
782
+ timeMs: Date.now(),
783
+ nodeId: this.nodeId,
784
+ in: this._transitEndpoint(p.target, id),
785
+ disposition: "dropped",
786
+ kind: "response",
787
+ method: p.method,
788
+ error: {
789
+ code: ErrorCode.peerDisconnected,
790
+ message: "origin disconnected"
791
+ }
792
+ });
793
+ this._deletePending(id);
794
+ } else if (p.target === link) {
795
+ this._deletePending(id);
796
+ this._log?.warn(`${this._tag()}target detached mid-request (hubId=${id}); failing origin`);
797
+ if (this._transitObservers.size !== 0) this._emit({
798
+ timeMs: Date.now(),
799
+ nodeId: this.nodeId,
800
+ in: this._transitEndpoint(p.target, id),
801
+ out: this._transitEndpoint(p.origin, p.originalId),
802
+ disposition: "forwarded",
803
+ kind: "response",
804
+ method: p.method,
805
+ error: {
806
+ code: ErrorCode.peerDisconnected,
807
+ message: "target detached before responding"
808
+ }
809
+ });
810
+ this._replyError(p.origin, p.originalId, ErrorCode.peerDisconnected, "target detached before responding");
811
+ }
812
+ this._fireRoutingChanged();
813
+ }
814
+ /** Set (or clear) the default route. Automatically attaches the link. */
815
+ setUplink(link) {
816
+ const changed = this._uplink !== link;
817
+ this._uplink = link;
818
+ if (link) {
819
+ this.attach(link);
820
+ this._labelEdge(link, "uplink");
821
+ }
822
+ if (changed) this._fireRoutingChanged();
823
+ }
824
+ /** Set (or clear) the root handler. Clear the existing handler before assigning a different link. */
825
+ setLoopback(link) {
826
+ if (link !== void 0) this.attach(link, { exclusiveHubRootHandler: true });
827
+ else if (this._loopback !== void 0) {
828
+ this._loopback = void 0;
829
+ this._fireRoutingChanged();
830
+ }
831
+ }
832
+ /**
833
+ * Bind a serviceId `prefix` to `link`. Like initial attachment routes,
834
+ * this is a privileged operation; higher layers enforce who may claim.
835
+ *
836
+ * Claim-once: throws if the prefix is malformed or already owned.
837
+ */
838
+ claimPrefix(link, prefix) {
839
+ this.attach(link, { routePrefixes: [prefix] });
840
+ }
841
+ _validatePrefixClaim(prefix) {
842
+ const err = validatePrefix(prefix);
843
+ if (err) throw new Error(`claimPrefix: ${err}`);
844
+ if (this._table.has(prefix)) throw new Error(`claimPrefix: prefix '${prefix}' is already claimed`);
845
+ }
846
+ _claimPrefix(link, prefix) {
847
+ this._table.set(prefix, link);
848
+ this._labelEdge(link, prefix);
849
+ this._log?.debug(`${this._tag()}claim prefix '${prefix}'`);
850
+ }
851
+ releasePrefix(prefix) {
852
+ const released = this._table.delete(prefix);
853
+ if (released) {
854
+ this._log?.debug(`${this._tag()}release prefix '${prefix}'`);
855
+ this._fireRoutingChanged();
856
+ }
857
+ return released;
858
+ }
859
+ /** Snapshot of currently-claimed prefixes. */
860
+ claimedPrefixes() {
861
+ return this._table.prefixes();
862
+ }
863
+ /**
864
+ * Trust the inspection interfaces mounted for `serviceId` on this attached
865
+ * in-process link. Only exact reserved inspection interface calls are hidden.
866
+ */
867
+ markInspectionService(attached, serviceId) {
868
+ this._topology.markInspectionService(attached, serviceId);
869
+ }
870
+ /** Build a topology snapshot on demand for the addressed observer service. */
871
+ getTopologyGraph(observerServiceId) {
872
+ return this._topology.getTopologyGraph(observerServiceId);
873
+ }
874
+ /**
875
+ * Fan a **root-form** request out to every distinct downstream participant
876
+ * link (the claimed-prefix owners), skipping the link that owns
877
+ * `excludePrefix` (typically the hub's own services prefix) and the uplink.
878
+ * Each participant is queried **once** even if it owns several prefixes.
879
+ * Failures (unreachable participant, timeout, no root handler) are dropped,
880
+ * so the result holds one entry per link that answered.
881
+ *
882
+ * The request is delivered to each participant's **root** services — on an
883
+ * overlay uplink the splitter routes a root-form method to the participant
884
+ * (`H·root → P`), which is never forwarded and never gated. That is what
885
+ * lets the hub's global directory gather participant self-listings without
886
+ * signing or capabilities.
887
+ */
888
+ async queryParticipantRoots(method, params, excludePrefix) {
889
+ return this._participantRoots.query(method, params, excludePrefix);
890
+ }
891
+ /**
892
+ * Keep a root-form streaming request open on every current participant.
893
+ * Routing changes reconcile the participant set; application stream payloads
894
+ * are collapsed into a coarse callback so callers can re-query.
895
+ */
896
+ watchParticipantRoots(method, params, onTick, excludePrefix) {
897
+ return this._participantRoots.watch(method, params, onTick, excludePrefix);
898
+ }
899
+ /**
900
+ * Send a single request down `link` and resolve with its result — the
901
+ * hub-originated analogue of {@link _routeRequest}. It allocates a hub id,
902
+ * parks a {@link _pending} entry whose `origin` is a tiny in-memory sink
903
+ * that settles this promise on the matching response, and writes the
904
+ * request to `link`. It therefore reuses the full demux / idle-timeout /
905
+ * detach machinery: a response routes back through {@link _demux} into the
906
+ * sink, an idle target fails it via {@link _onIdleTimeout}, and a detaching
907
+ * target fails it via {@link _detach}.
908
+ */
909
+ _requestOnLink(link, method, params, timeoutMs) {
910
+ return new Promise((resolve, reject) => {
911
+ const hubId = this._nextId++;
912
+ const key = String(hubId);
913
+ let settled = false;
914
+ let requestTimeout;
915
+ const finish = (error, result) => {
916
+ if (settled) return;
917
+ settled = true;
918
+ if (requestTimeout !== void 0) clearTimeout(requestTimeout);
919
+ if (error !== void 0) reject(error);
920
+ else resolve(result);
921
+ };
922
+ const origin = {
923
+ send: (m) => {
924
+ const r = m;
925
+ const err = r.error;
926
+ if (err) finish(new RpcError(err.message ?? "request failed", err.code ?? ErrorCode.internalError, err.data));
927
+ else finish(void 0, r.result);
928
+ },
929
+ setListener: () => {},
930
+ dispose: () => {}
931
+ };
932
+ this._pending.set(key, {
933
+ origin,
934
+ originalId: hubId,
935
+ target: link,
936
+ method,
937
+ startedAtMs: Date.now()
938
+ });
939
+ this._armIdle(key);
940
+ if (timeoutMs !== void 0 && timeoutMs > 0) {
941
+ requestTimeout = setTimeout(() => {
942
+ if (this._pending.get(key)?.origin !== origin) return;
943
+ this._injectCancel(link, key, StreamControlReason.clientDisconnected);
944
+ this._deletePending(key);
945
+ finish(/* @__PURE__ */ new Error(`${method} timed out after ${timeoutMs}ms`));
946
+ }, timeoutMs);
947
+ requestTimeout.unref();
948
+ }
949
+ const onSendError = (error) => {
950
+ if (this._pending.get(key)?.origin !== origin) return;
951
+ this._deletePending(key);
952
+ finish(error instanceof Error ? error : new Error(String(error)));
953
+ };
954
+ try {
955
+ Promise.resolve(link.send({
956
+ jsonrpc: "2.0",
957
+ id: hubId,
958
+ method,
959
+ params
960
+ })).catch(onSendError);
961
+ } catch (error) {
962
+ onSendError(error);
963
+ }
964
+ });
965
+ }
966
+ _watchOnLink(link, method, params, onTick, onSettled, onEstablished) {
967
+ const hubId = this._nextId++;
968
+ const key = String(hubId);
969
+ let active = true;
970
+ let pingTimer;
971
+ let establishedTimer;
972
+ const stop = (cancel) => {
973
+ if (!active) return;
974
+ active = false;
975
+ if (pingTimer !== void 0) clearInterval(pingTimer);
976
+ if (establishedTimer !== void 0) clearTimeout(establishedTimer);
977
+ if (this._pending.get(key)?.origin === origin) {
978
+ if (cancel) this._injectCancel(link, key, StreamControlReason.clientDisconnected);
979
+ this._deletePending(key);
980
+ }
981
+ if (!cancel) onSettled();
982
+ };
983
+ const sendControl = (control, dir) => {
984
+ const message = {
985
+ jsonrpc: "2.0",
986
+ method: STREAM_METHOD,
987
+ params: {
988
+ requestId: hubId,
989
+ dir,
990
+ control
991
+ }
992
+ };
993
+ try {
994
+ Promise.resolve(link.send(message)).catch(() => stop(false));
995
+ } catch {
996
+ stop(false);
997
+ }
998
+ };
999
+ const origin = {
1000
+ send: (message) => {
1001
+ if (isNotification(message) && message.method === STREAM_METHOD) {
1002
+ const stream = _streamParams(message);
1003
+ if (stream?.dir !== StreamDir.toCaller) return;
1004
+ if (stream.control?.type === StreamControlType.ping) sendControl({
1005
+ type: StreamControlType.pong,
1006
+ nonce: stream.control.nonce
1007
+ }, StreamDir.toCallee);
1008
+ if (stream.payload !== void 0) onTick();
1009
+ } else if (isResponse(message)) stop(false);
1010
+ },
1011
+ setListener: () => {},
1012
+ dispose: () => stop(true)
1013
+ };
1014
+ this._pending.set(key, {
1015
+ origin,
1016
+ originalId: hubId,
1017
+ target: link,
1018
+ method,
1019
+ startedAtMs: Date.now()
1020
+ });
1021
+ this._armIdle(key);
1022
+ establishedTimer = setTimeout(() => {
1023
+ establishedTimer = void 0;
1024
+ if (active && this._pending.get(key)?.origin === origin) onEstablished();
1025
+ }, 10);
1026
+ if (this._idleMs > 0) pingTimer = setInterval(() => {
1027
+ if (!active || this._pending.get(key)?.origin !== origin) {
1028
+ stop(false);
1029
+ return;
1030
+ }
1031
+ this._resetIdle(key);
1032
+ sendControl({
1033
+ type: StreamControlType.ping,
1034
+ nonce: randomUUID()
1035
+ }, StreamDir.toCallee);
1036
+ }, Math.max(10, Math.floor(this._idleMs / 3)));
1037
+ const onSendError = () => stop(false);
1038
+ try {
1039
+ Promise.resolve(link.send({
1040
+ jsonrpc: "2.0",
1041
+ id: hubId,
1042
+ method,
1043
+ params
1044
+ })).catch(onSendError);
1045
+ } catch {
1046
+ onSendError();
1047
+ }
1048
+ return { dispose: () => stop(true) };
1049
+ }
1050
+ /**
1051
+ * The link a still-in-flight forwarded request originally arrived on,
1052
+ * keyed by the **hub-rewritten** id the request now carries (i.e. the
1053
+ * `id` a target sees on its delivered message). Valid from the moment the
1054
+ * hub forwards the request until its response is demultiplexed; returns
1055
+ * `undefined` for unknown / already-answered ids.
1056
+ *
1057
+ * This is the seam a hub-hosted participant uses to learn *which* link a
1058
+ * call it is handling came from, so it can bind a prefix claim to that
1059
+ * exact link via {@link claimPrefix} without the routing core ever
1060
+ * growing a notion of authenticated identity.
1061
+ */
1062
+ getSourceTransport(requestId) {
1063
+ return this._pending.get(String(requestId))?.origin;
1064
+ }
1065
+ _onMessage(from, m) {
1066
+ if (isResponse(m)) this._demux(from, m);
1067
+ else if (isRequest(m)) {
1068
+ if (m.method === GET_NODE_ID_METHOD) {
1069
+ from.send({
1070
+ jsonrpc: "2.0",
1071
+ id: m.id,
1072
+ result: {
1073
+ nodeId: this.nodeId,
1074
+ portId: this._topology.portId(from)
1075
+ }
1076
+ });
1077
+ return;
1078
+ }
1079
+ this._routeRequest(from, m);
1080
+ } else if (isNotification(m)) {
1081
+ if (m.method === STREAM_METHOD) this._routeStream(from, m);
1082
+ else this._routeNotification(from, m);
1083
+ }
1084
+ }
1085
+ _resolve(method, from) {
1086
+ const parsed = parseMethodName(method);
1087
+ if (!parsed) return { kind: "malformed" };
1088
+ if (parsed.kind === "bare" || parsed.kind === "interface") return {
1089
+ kind: "loopback",
1090
+ link: this._loopback
1091
+ };
1092
+ const match = this._table.longestPrefixMatch(parsed.serviceId);
1093
+ if (match) return {
1094
+ kind: "prefix",
1095
+ link: match.value,
1096
+ prefix: match.prefix
1097
+ };
1098
+ if (this._uplink !== void 0 && this._uplink === from) return {
1099
+ kind: "uplink",
1100
+ link: void 0
1101
+ };
1102
+ return {
1103
+ kind: "uplink",
1104
+ link: this._uplink
1105
+ };
1106
+ }
1107
+ _routeRequest(from, req) {
1108
+ const res = this._resolve(req.method, from);
1109
+ const target = res.kind === "malformed" ? void 0 : res.link;
1110
+ if (!target) {
1111
+ const message = this._noTargetMessage(res, req.method);
1112
+ this._log?.warn(`${this._tag()}unroutable request '${req.method}' (id=${String(req.id)}): ${message}`);
1113
+ if (this._transitObservers.size !== 0) this._emit({
1114
+ timeMs: Date.now(),
1115
+ nodeId: this.nodeId,
1116
+ in: this._transitEndpoint(from, req.id),
1117
+ disposition: "unroutable",
1118
+ kind: "request",
1119
+ method: req.method,
1120
+ params: req.params
1121
+ });
1122
+ this._replyError(from, req.id, this._noTargetCode(res), message);
1123
+ return;
1124
+ }
1125
+ const hubId = this._nextId++;
1126
+ const key = String(hubId);
1127
+ this._pending.set(key, {
1128
+ origin: from,
1129
+ originalId: req.id,
1130
+ target,
1131
+ method: req.method,
1132
+ startedAtMs: Date.now()
1133
+ });
1134
+ this._armIdle(key);
1135
+ this._log?.trace(`${this._tag()}route '${req.method}' (${res.kind}${res.kind === "prefix" ? ` ${res.prefix}` : ""}) id=${String(req.id)}\u2192${hubId}`);
1136
+ if (this._transitObservers.size !== 0) this._emit({
1137
+ timeMs: Date.now(),
1138
+ nodeId: this.nodeId,
1139
+ in: this._transitEndpoint(from, req.id),
1140
+ out: this._transitEndpoint(target, hubId),
1141
+ disposition: "forwarded",
1142
+ kind: "request",
1143
+ method: req.method,
1144
+ params: req.params
1145
+ });
1146
+ const forwarded = {
1147
+ ...req,
1148
+ id: hubId
1149
+ };
1150
+ const onSendError = (error) => {
1151
+ const pending = this._pending.get(key);
1152
+ if (pending === void 0 || pending.target !== target) return;
1153
+ this._deletePending(key);
1154
+ const message = `target send failed: ${error instanceof Error ? error.message : String(error)}`;
1155
+ this._log?.warn(`${this._tag()}${message} (hubId=${key})`);
1156
+ if (this._transitObservers.size !== 0) this._emit({
1157
+ timeMs: Date.now(),
1158
+ nodeId: this.nodeId,
1159
+ in: this._transitEndpoint(pending.target, hubId),
1160
+ out: this._transitEndpoint(pending.origin, pending.originalId),
1161
+ disposition: "dropped",
1162
+ kind: "response",
1163
+ method: pending.method,
1164
+ error: {
1165
+ code: ErrorCode.peerDisconnected,
1166
+ message
1167
+ }
1168
+ });
1169
+ this._replyError(pending.origin, pending.originalId, ErrorCode.peerDisconnected, message);
1170
+ };
1171
+ try {
1172
+ Promise.resolve(target.send(forwarded)).catch(onSendError);
1173
+ } catch (error) {
1174
+ onSendError(error);
1175
+ }
1176
+ }
1177
+ _routeNotification(from, note) {
1178
+ const method = note.method;
1179
+ const params = note.params;
1180
+ const res = this._resolve(method, from);
1181
+ const target = res.kind === "malformed" ? void 0 : res.link;
1182
+ if (target) {
1183
+ if (this._transitObservers.size !== 0) this._emit({
1184
+ timeMs: Date.now(),
1185
+ nodeId: this.nodeId,
1186
+ in: this._transitEndpoint(from),
1187
+ out: this._transitEndpoint(target),
1188
+ disposition: "forwarded",
1189
+ kind: "notification",
1190
+ method,
1191
+ params
1192
+ });
1193
+ target.send(note);
1194
+ } else {
1195
+ if (this._transitObservers.size !== 0) this._emit({
1196
+ timeMs: Date.now(),
1197
+ nodeId: this.nodeId,
1198
+ in: this._transitEndpoint(from),
1199
+ disposition: res.kind === "malformed" ? "unroutable" : "dropped",
1200
+ kind: "notification",
1201
+ method,
1202
+ params
1203
+ });
1204
+ this._log?.trace(`${this._tag()}drop unroutable notification '${method}'`);
1205
+ }
1206
+ }
1207
+ _demux(from, res) {
1208
+ if (res.id === null) return;
1209
+ const key = String(res.id);
1210
+ const pending = this._pending.get(key);
1211
+ if (!pending) return;
1212
+ if (from !== pending.target) return;
1213
+ this._deletePending(key);
1214
+ if (this._transitObservers.size !== 0) this._emit({
1215
+ timeMs: Date.now(),
1216
+ nodeId: this.nodeId,
1217
+ in: this._transitEndpoint(pending.target, res.id),
1218
+ out: this._transitEndpoint(pending.origin, pending.originalId),
1219
+ disposition: "forwarded",
1220
+ kind: "response",
1221
+ method: pending.method,
1222
+ result: res.result,
1223
+ error: res.error
1224
+ });
1225
+ pending.origin.send({
1226
+ ...res,
1227
+ id: pending.originalId
1228
+ });
1229
+ }
1230
+ /**
1231
+ * Route a `$stream::send` notification. Unlike an ordinary notification, a
1232
+ * stream message is **correlated to an in-flight request by `requestId`**,
1233
+ * not addressed by method name — so it follows the same path the request
1234
+ * established (recorded in {@link _pending}), exactly like a
1235
+ * {@link _demux | response}, and the hub rewrites `requestId` across the
1236
+ * forwarding boundary the same way it rewrites a response `id`.
1237
+ *
1238
+ * The message's explicit `dir` picks the traversal — no inference from the
1239
+ * arriving link:
1240
+ *
1241
+ * - **`toCaller`** (callee → caller progress): routed like a response. The
1242
+ * streamer used the hub-rewritten id it *received*, so `requestId` is a
1243
+ * pending hub id; only the link the request was forwarded to may stream
1244
+ * on it. Restore the origin's original id and forward to the origin.
1245
+ * - **`toCallee`** (caller → callee input / cancel / ping): the reverse
1246
+ * traversal of the same entry. The streamer used the original id it
1247
+ * *sent*; rewrite `requestId` to the hub id the target was handed and
1248
+ * forward to the target. Only the origin may stream this direction.
1249
+ *
1250
+ * Every routed stream message resets the request's idle timer (a keepalive
1251
+ * ping is exactly a `toCallee` control with no payload). A `$stream::send`
1252
+ * matching no in-flight request is dropped (late / unknown correlation).
1253
+ */
1254
+ _routeStream(from, note) {
1255
+ const p = _streamParams(note);
1256
+ if (!p || p.requestId === void 0) {
1257
+ this._log?.trace(`${this._tag()}drop malformed '${STREAM_METHOD}' (no requestId)`);
1258
+ return;
1259
+ }
1260
+ if (p.dir === StreamDir.toCaller) {
1261
+ const key = String(p.requestId);
1262
+ const pending = this._pending.get(key);
1263
+ if (!pending || from !== pending.target) {
1264
+ this._log?.trace(`${this._tag()}drop unroutable toCaller '${STREAM_METHOD}' requestId=${String(p.requestId)}`);
1265
+ return;
1266
+ }
1267
+ this._resetIdle(key);
1268
+ pending.origin.send(_withRequestId$1(note, pending.originalId));
1269
+ this._emitStream(note, this._transitEndpoint(pending.target, p.requestId), this._transitEndpoint(pending.origin, pending.originalId));
1270
+ return;
1271
+ }
1272
+ for (const [hubId, pending] of this._pending) if (pending.origin === from && String(pending.originalId) === String(p.requestId)) {
1273
+ this._resetIdle(hubId);
1274
+ pending.target.send(_withRequestId$1(note, Number(hubId)));
1275
+ this._emitStream(note, this._transitEndpoint(pending.origin, p.requestId), this._transitEndpoint(pending.target, Number(hubId)));
1276
+ return;
1277
+ }
1278
+ this._log?.trace(`${this._tag()}drop unroutable toCallee '${STREAM_METHOD}' requestId=${String(p.requestId)}`);
1279
+ }
1280
+ /** Delete a pending entry and clear its idle timer. */
1281
+ _deletePending(key) {
1282
+ const p = this._pending.get(key);
1283
+ if (!p) return;
1284
+ if (p.timer !== void 0) clearTimeout(p.timer);
1285
+ this._pending.delete(key);
1286
+ }
1287
+ /** Arm (or re-arm) the idle timer for a pending entry. No-op when disabled. */
1288
+ _armIdle(key) {
1289
+ if (this._idleMs <= 0) return;
1290
+ const t = setTimeout(() => this._onIdleTimeout(key), this._idleMs);
1291
+ t.unref?.();
1292
+ const p = this._pending.get(key);
1293
+ if (p) p.timer = t;
1294
+ else clearTimeout(t);
1295
+ }
1296
+ /** Reset the idle timer on stream activity. */
1297
+ _resetIdle(key) {
1298
+ const p = this._pending.get(key);
1299
+ if (!p) return;
1300
+ if (p.timer !== void 0) clearTimeout(p.timer);
1301
+ this._armIdle(key);
1302
+ }
1303
+ /**
1304
+ * A request went idle past {@link _idleMs}. Cancel the (presumably hung)
1305
+ * callee, fail the caller with a `requestTimeout`, and drop the entry —
1306
+ * the bound that keeps `_pending` from growing without limit.
1307
+ */
1308
+ _onIdleTimeout(key) {
1309
+ const p = this._pending.get(key);
1310
+ if (!p) return;
1311
+ this._log?.warn(`${this._tag()}request idle-timed-out (hubId=${key}); cancelling + failing origin`);
1312
+ this._injectCancel(p.target, key, StreamControlReason.idleTimeout);
1313
+ this._deletePending(key);
1314
+ if (this._transitObservers.size !== 0) this._emit({
1315
+ timeMs: Date.now(),
1316
+ nodeId: this.nodeId,
1317
+ in: this._transitEndpoint(p.target, key),
1318
+ out: this._transitEndpoint(p.origin, p.originalId),
1319
+ disposition: "forwarded",
1320
+ kind: "response",
1321
+ method: p.method,
1322
+ error: {
1323
+ code: ErrorCode.requestTimeout,
1324
+ message: "request idle-timed-out"
1325
+ }
1326
+ });
1327
+ this._replyError(p.origin, p.originalId, ErrorCode.requestTimeout, "request idle-timed-out");
1328
+ }
1329
+ /**
1330
+ * Author a `toCallee` cancel toward `target` for the request the target
1331
+ * knows as `hubId`. Used on caller-disconnect and idle-timeout — the hub
1332
+ * itself originates the message, which is why `dir` is explicit on the
1333
+ * wire (there is no inbound link to infer it from).
1334
+ */
1335
+ _injectCancel(target, hubId, reason) {
1336
+ const params = {
1337
+ requestId: Number(hubId),
1338
+ dir: StreamDir.toCallee,
1339
+ control: {
1340
+ type: StreamControlType.cancel,
1341
+ reason
1342
+ }
1343
+ };
1344
+ try {
1345
+ Promise.resolve(target.send({
1346
+ jsonrpc: "2.0",
1347
+ method: STREAM_METHOD,
1348
+ params
1349
+ })).catch(() => void 0);
1350
+ } catch {}
1351
+ }
1352
+ _noTargetCode(res) {
1353
+ return res.kind === "malformed" ? ErrorCode.invalidRequest : ErrorCode.methodNotFound;
1354
+ }
1355
+ _noTargetMessage(res, method) {
1356
+ switch (res.kind) {
1357
+ case "malformed": return `malformed method name: ${method}`;
1358
+ case "loopback": return `no local root services to handle: ${method}`;
1359
+ case "uplink": return `no route for: ${method}`;
1360
+ default: return `unroutable: ${method}`;
1361
+ }
1362
+ }
1363
+ _replyError(to, id, code, message) {
1364
+ const error = {
1365
+ jsonrpc: "2.0",
1366
+ id,
1367
+ error: this._options.debugName !== void 0 ? {
1368
+ code,
1369
+ message,
1370
+ data: { hub: this._options.debugName }
1371
+ } : {
1372
+ code,
1373
+ message
1374
+ }
1375
+ };
1376
+ to.send(error);
1377
+ }
1378
+ };
1379
+ /** Read the params off a {@link STREAM_METHOD} notification. */
1380
+ function _streamParams(m) {
1381
+ const params = m.params;
1382
+ if (params === null || typeof params !== "object") return void 0;
1383
+ return params;
1384
+ }
1385
+ /** Clone a {@link STREAM_METHOD} notification with `requestId` replaced. */
1386
+ function _withRequestId$1(m, requestId) {
1387
+ const params = {
1388
+ ...m.params,
1389
+ requestId
1390
+ };
1391
+ return {
1392
+ ...m,
1393
+ params
1394
+ };
1395
+ }
1396
+ //#endregion
1397
+ //#region src/hub/server/hubInspector.ts
1398
+ /** Publishes dynamically observed raw node transits through bounded traffic streams. */
1399
+ var HubInspector = class {
1400
+ _hub;
1401
+ _subscribers = /* @__PURE__ */ new Set();
1402
+ _trafficSources = /* @__PURE__ */ new Set();
1403
+ _trafficWatches = new TrafficWatchFlowTracker();
1404
+ _observation;
1405
+ constructor(_hub) {
1406
+ this._hub = _hub;
1407
+ this._observation = this._hub.observeTransits((transit) => {
1408
+ this._onTransit(transit);
1409
+ });
1410
+ }
1411
+ get observerCount() {
1412
+ return this._subscribers.size;
1413
+ }
1414
+ subscribe(options, send) {
1415
+ if (options.trafficIgnoreKey !== void 0 && !this._trafficWatches.claim(options.trafficIgnoreKey)) throw new Error("Traffic watch request was not observed before subscription");
1416
+ const subscriber = new BoundedTrafficSubscription(options, send, () => {
1417
+ this._subscribers.delete(subscriber);
1418
+ if (this._subscribers.size === 0) this._stopTrafficSources();
1419
+ });
1420
+ if (this._subscribers.size === 0) this._startTrafficSources();
1421
+ this._subscribers.add(subscriber);
1422
+ return subscriber;
1423
+ }
1424
+ dispose() {
1425
+ for (const subscriber of [...this._subscribers]) subscriber.dispose();
1426
+ this._subscribers.clear();
1427
+ this._observation.dispose();
1428
+ this._trafficWatches.clear();
1429
+ this._stopTrafficSources();
1430
+ this._trafficSources.clear();
1431
+ }
1432
+ /** Add an endpoint traffic source managed by this Hub inspection service. */
1433
+ addTrafficSource(source) {
1434
+ const registered = { source };
1435
+ this._trafficSources.add(registered);
1436
+ if (this._subscribers.size !== 0) this._startTrafficSource(registered);
1437
+ let disposed = false;
1438
+ return { dispose: () => {
1439
+ if (disposed) return;
1440
+ disposed = true;
1441
+ this._trafficSources.delete(registered);
1442
+ registered.observation?.dispose();
1443
+ registered.observation = void 0;
1444
+ } };
1445
+ }
1446
+ _startTrafficSources() {
1447
+ for (const source of this._trafficSources) this._startTrafficSource(source);
1448
+ }
1449
+ _startTrafficSource(source) {
1450
+ source.observation ??= source.source.observe((transit) => this._emitTransit(transit));
1451
+ }
1452
+ _stopTrafficSources() {
1453
+ for (const source of this._trafficSources) {
1454
+ source.observation?.dispose();
1455
+ source.observation = void 0;
1456
+ }
1457
+ }
1458
+ _onTransit(transit) {
1459
+ const event = {
1460
+ type: "transit",
1461
+ timeMs: transit.timeMs,
1462
+ nodeId: transit.nodeId,
1463
+ ...transit.in !== void 0 ? { in: normalizeEndpoint(transit.in) } : {},
1464
+ ...transit.out !== void 0 ? { out: normalizeEndpoint(transit.out) } : {},
1465
+ disposition: transit.disposition,
1466
+ kind: transit.kind,
1467
+ method: transit.method,
1468
+ params: transit.params,
1469
+ result: transit.result,
1470
+ error: transit.error
1471
+ };
1472
+ if (this._trafficWatches.accept(event)) return;
1473
+ if (this._subscribers.size !== 0) this._emitTransit(event);
1474
+ }
1475
+ _emitTransit(transit) {
1476
+ for (const subscriber of this._subscribers) subscriber.enqueue(transit);
1477
+ }
1478
+ };
1479
+ function normalizeEndpoint(endpoint) {
1480
+ return {
1481
+ edgeId: endpoint.edgeId,
1482
+ portId: endpoint.portId ?? endpoint.edgeId,
1483
+ ...endpoint.requestId !== void 0 ? { requestId: endpoint.requestId } : {}
1484
+ };
1485
+ }
1486
+ //#endregion
1487
+ //#region src/hub/server/routing/overlaySplitter.ts
1488
+ /** Separator for return-path tags. NUL never appears in JSON-RPC ids. */
1489
+ const SEP = "\0";
1490
+ /**
1491
+ * A fixed three-port message splitter — the per-participant primitive that
1492
+ * replaces a nested {@link import('./routingHub').Hub} inside a
1493
+ * {@link import('./rootOverlay').RootOverlay}.
1494
+ *
1495
+ * Ports:
1496
+ * - **P** — the participant (downstream connection),
1497
+ * - **C** — the local root-services connection (`hubServiceIdRegistry::registerServiceId`,
1498
+ * `hubrpc.directory`, `identity::*`, …),
1499
+ * - **H** — the uplink to the parent hub.
1500
+ *
1501
+ * Routing rules (verbatim to the design):
1502
+ * ```
1503
+ * P·root → C P·* → H
1504
+ * H·* → P H·root → P
1505
+ * C·* → P
1506
+ * ```
1507
+ * where `root` means a bare/interface-form method and `*` a fully-qualified
1508
+ * (`serviceId::…`) method. Responses follow the reverse path. `H·root → P`
1509
+ * lets the parent hub reach the participant's *own* root services (e.g. the
1510
+ * participant-served root directory) — distinct from `P·root → C`, which is
1511
+ * the participant *consuming* the overlay's root services; the two are
1512
+ * opposite directions served by different connections, so they never collide.
1513
+ *
1514
+ * ### `$stream::send` — correlated by `requestId`, not by method form
1515
+ *
1516
+ * A {@link STREAM_METHOD} (`$stream::send`) notification is *interface-form*
1517
+ * (`$stream::send`), so the method-form rules above would naively treat it as
1518
+ * `root` and send a participant's stream to **C** (and route an inbound one as
1519
+ * a root call to **P**). That is wrong: a stream message belongs to an in-flight
1520
+ * request's lifetime and must follow **that request's** path, exactly like a
1521
+ * response does. So `$stream::send` bypasses the method-form table and is
1522
+ * routed like a response instead:
1523
+ *
1524
+ * - **P → ?**: the participant streams using the `requestId` it *observed*,
1525
+ * which for a request it serves is the tag the splitter stamped on the
1526
+ * delivered request id (`H\0…` / `C\0…`). We {@link _decodeId | decode} that
1527
+ * tag, restore the original id, and forward to the tagged origin (H or C) —
1528
+ * the same demux responses get. An **untagged** `requestId` means the
1529
+ * participant *initiated* the request (P→H or P→C); we cannot tell which
1530
+ * statelessly, so we default to the **uplink** (the common case: streaming
1531
+ * to a fully-qualified service via the parent).
1532
+ * - **H/C → P**: forwarded verbatim to the participant (never dropped), as a
1533
+ * response would be — the parent has already restored P's original
1534
+ * `requestId`, symmetric to response-id rewriting.
1535
+ *
1536
+ * PARTICIPANT-INITIATED STREAMS — the one stateful case: because `requestId`
1537
+ * lives in `params` (not a structural top-level id the splitter rewrites in
1538
+ * lock-step), a stream frame for a request the participant *initiated* (`P→C`
1539
+ * or `P→H`) carries P's *own untagged* id, which alone cannot say whether the
1540
+ * call went to the root (C) or the uplink (H). The splitter therefore keeps a
1541
+ * small `requestId → target` map ({@link _pInitiated}), populated when P sends
1542
+ * such a request and cleared when its response returns, and consults it to
1543
+ * route those frames. This is the splitter's *only* per-request state; every
1544
+ * other path still routes statelessly via id-tagging.
1545
+ *
1546
+ * ### Stateless demux via id-tagging
1547
+ *
1548
+ * P is the only port that receives requests from *two* sources (H and C),
1549
+ * so a response from P is ambiguous and ids can collide (`H` and `C` may
1550
+ * each send `id:5`). Rather than a pending-request map (with its attendant
1551
+ * timeout/leak problem), the splitter rewrites **only requests destined for
1552
+ * P**, prepending the origin and original id type to the id:
1553
+ *
1554
+ * ```
1555
+ * H→P: id' = "H\0n\042" (push origin tag)
1556
+ * C→P: id' = "C\0s\0abc"
1557
+ * P→?: decode id' → route to H or C, restore the original id
1558
+ * ```
1559
+ *
1560
+ * Every other edge (P→H, P→C and the responses coming back from H/C to P)
1561
+ * passes **verbatim** — those targets each have a single peer, so there is no
1562
+ * response ambiguity. The sole exception is *stream* frames for
1563
+ * participant-initiated requests, whose target is recovered from the small
1564
+ * {@link _pInitiated} map (see above); apart from that the id encodes its own
1565
+ * return path.
1566
+ *
1567
+ * NOTE: unlike the central {@link import('./routingHub').Hub}, the tag is
1568
+ * not authenticated. A misbehaving participant could emit a forged tagged
1569
+ * response toward H or C; each endpoint still drops ids it has no pending
1570
+ * request for, and identity/signing live in a higher layer. For the
1571
+ * single-participant overlay this is an acceptable trade for being fully
1572
+ * stateless. If unforgeable return paths are ever required here, HMAC the
1573
+ * tag with a splitter-private secret.
1574
+ */
1575
+ var OverlaySplitter = class {
1576
+ _participant;
1577
+ _root;
1578
+ _uplink;
1579
+ _disposed = false;
1580
+ _inspect;
1581
+ /**
1582
+ * Per-request routing target (`C` or `H`) for requests the participant
1583
+ * *initiated*, keyed by P's own request id. Streams for those requests
1584
+ * carry P's untagged id, which alone can't say whether the call went to the
1585
+ * root (C) or the uplink (H); this map recovers it. Populated when P sends
1586
+ * the request, cleared when its response returns. The splitter's only
1587
+ * per-request state — every other path routes statelessly via id-tagging.
1588
+ */
1589
+ _pInitiated = /* @__PURE__ */ new Map();
1590
+ constructor(_participant, _root, _uplink, inspection) {
1591
+ this._participant = _participant;
1592
+ this._root = _root;
1593
+ this._uplink = _uplink;
1594
+ this._inspect = inspection;
1595
+ this._participant.setListener((m) => this._onFromParticipant(m));
1596
+ this._root.setListener((m) => this._onFromUpstream(m, "C"));
1597
+ this._uplink.setListener((m) => this._onFromUpstream(m, "H"));
1598
+ }
1599
+ /** Detach all three ports. Does not dispose the transports themselves. */
1600
+ dispose() {
1601
+ if (this._disposed) return;
1602
+ this._disposed = true;
1603
+ this._participant.setListener(void 0);
1604
+ this._root.setListener(void 0);
1605
+ this._uplink.setListener(void 0);
1606
+ this._pInitiated.clear();
1607
+ }
1608
+ /** Edge id of an upstream origin port. */
1609
+ _originEdge(origin) {
1610
+ return origin === "H" ? this._inspect.edges.h : this._inspect.edges.c;
1611
+ }
1612
+ /** Emit a node transit if inspection is enabled. Zero-cost otherwise. */
1613
+ _emit(t) {
1614
+ if (this._inspect) this._inspect.onTransit(t);
1615
+ }
1616
+ _onFromParticipant(m) {
1617
+ if (isResponse(m)) {
1618
+ if (m.id === null) return;
1619
+ const dec = _decodeId(m.id);
1620
+ if (!dec) return;
1621
+ if (dec.origin === "H") {
1622
+ if (this._inspect) this._emit(this._mkResponse(this._inspect.edges.p, m.id, this._inspect.edges.h, dec.id, m));
1623
+ this._uplink.send({
1624
+ ...m,
1625
+ id: dec.id
1626
+ });
1627
+ } else {
1628
+ if (this._inspect) this._emit(this._mkResponse(this._inspect.edges.p, m.id, this._inspect.edges.c, dec.id, m));
1629
+ this._root.send({
1630
+ ...m,
1631
+ id: dec.id
1632
+ });
1633
+ }
1634
+ return;
1635
+ }
1636
+ const method = _methodOf(m);
1637
+ if (method === void 0) return;
1638
+ if (method === STREAM_METHOD) {
1639
+ this._routeParticipantStream(m);
1640
+ return;
1641
+ }
1642
+ const rootForm = _isRootForm(method);
1643
+ if (rootForm === void 0) {
1644
+ if (this._inspect) this._emit(this._mkForward(this._inspect.edges.p, m, void 0, method, "dropped"));
1645
+ return;
1646
+ }
1647
+ if (rootForm) {
1648
+ if (this._inspect) this._emit(this._mkForward(this._inspect.edges.p, m, this._inspect.edges.c, method, "forwarded"));
1649
+ if (isRequest(m)) this._pInitiated.set(m.id, { target: "C" });
1650
+ this._root.send(m);
1651
+ } else {
1652
+ if (this._inspect) this._emit(this._mkForward(this._inspect.edges.p, m, this._inspect.edges.h, method, "forwarded"));
1653
+ if (isRequest(m)) this._pInitiated.set(m.id, { target: "H" });
1654
+ this._uplink.send(m);
1655
+ }
1656
+ }
1657
+ /**
1658
+ * Route a participant-emitted `$stream::send` by its `requestId` tag,
1659
+ * mirroring how a {@link _onFromParticipant | response} is demuxed. The
1660
+ * participant streams with the id it *observed*; for a request it serves
1661
+ * that id carries the origin tag the splitter stamped, so decoding it
1662
+ * yields the return port and the original id. Untagged → a request the
1663
+ * participant *initiated*; recover its target (C or H) from {@link
1664
+ * _pInitiated}, defaulting to the uplink.
1665
+ */
1666
+ _routeParticipantStream(m) {
1667
+ const requestId = _streamRequestId(m);
1668
+ const dec = requestId !== void 0 ? _decodeId(requestId) : void 0;
1669
+ if (dec) {
1670
+ const restored = _withRequestId(m, dec.id);
1671
+ if (dec.origin === "H") this._uplink.send(restored);
1672
+ else this._root.send(restored);
1673
+ return;
1674
+ }
1675
+ if ((requestId !== void 0 ? this._pInitiated.get(requestId) : void 0)?.target === "C") {
1676
+ this._root.send(m);
1677
+ return;
1678
+ }
1679
+ this._uplink.send(m);
1680
+ }
1681
+ _onFromUpstream(m, origin) {
1682
+ if (isResponse(m)) {
1683
+ m.id === null || this._pInitiated.get(m.id);
1684
+ if (m.id !== null) this._pInitiated.delete(m.id);
1685
+ if (this._inspect) this._emit(this._mkResponse(this._originEdge(origin), m.id, this._inspect.edges.p, m.id, m));
1686
+ this._participant.send(m);
1687
+ return;
1688
+ }
1689
+ const method = _methodOf(m);
1690
+ if (method === void 0) return;
1691
+ if (method === STREAM_METHOD) {
1692
+ if (_streamDir(m) === StreamDir.toCallee) {
1693
+ const requestId = _streamRequestId(m);
1694
+ if (requestId !== void 0) {
1695
+ this._participant.send(_withRequestId(m, _encodeId(origin, requestId)));
1696
+ return;
1697
+ }
1698
+ }
1699
+ this._participant.send(m);
1700
+ return;
1701
+ }
1702
+ if (_isRootForm(method) === void 0) {
1703
+ if (this._inspect) this._emit(this._mkForward(this._originEdge(origin), m, void 0, method, "dropped"));
1704
+ return;
1705
+ }
1706
+ if (isRequest(m)) {
1707
+ const encodedId = _encodeId(origin, m.id);
1708
+ if (this._inspect) this._emit(this._mkForward(this._originEdge(origin), m, this._inspect.edges.p, method, "forwarded", encodedId));
1709
+ this._participant.send({
1710
+ ...m,
1711
+ id: encodedId
1712
+ });
1713
+ } else if (isNotification(m)) {
1714
+ if (this._inspect) this._emit(this._mkForward(this._originEdge(origin), m, this._inspect.edges.p, method, "forwarded"));
1715
+ this._participant.send(m);
1716
+ }
1717
+ }
1718
+ /** Build a request/notification transit (`out` absent ⇒ dropped here). */
1719
+ _mkForward(inEdge, m, outEdge, method, disposition, outRequestId) {
1720
+ const id = m.id;
1721
+ const isReq = isRequest(m);
1722
+ const inEp = {
1723
+ edgeId: inEdge,
1724
+ ...isReq && id !== void 0 ? { requestId: id } : {}
1725
+ };
1726
+ const outEp = outEdge === void 0 ? void 0 : {
1727
+ edgeId: outEdge,
1728
+ ...isReq ? { requestId: outRequestId ?? id } : {}
1729
+ };
1730
+ return {
1731
+ timeMs: Date.now(),
1732
+ nodeId: this._inspect.nodeId,
1733
+ in: inEp,
1734
+ out: outEp,
1735
+ disposition,
1736
+ kind: isReq ? "request" : "notification",
1737
+ method,
1738
+ params: m.params
1739
+ };
1740
+ }
1741
+ /** Build a response transit crossing from `inEdge`/`inId` to `outEdge`/`outId`. */
1742
+ _mkResponse(inEdge, inId, outEdge, outId, m) {
1743
+ return {
1744
+ timeMs: Date.now(),
1745
+ nodeId: this._inspect.nodeId,
1746
+ in: {
1747
+ edgeId: inEdge,
1748
+ ...inId !== null ? { requestId: inId } : {}
1749
+ },
1750
+ out: {
1751
+ edgeId: outEdge,
1752
+ ...outId !== null ? { requestId: outId } : {}
1753
+ },
1754
+ disposition: "forwarded",
1755
+ kind: "response",
1756
+ result: m.result,
1757
+ error: m.error
1758
+ };
1759
+ }
1760
+ };
1761
+ function _methodOf(m) {
1762
+ const method = m.method;
1763
+ return typeof method === "string" ? method : void 0;
1764
+ }
1765
+ /** Read the `requestId` correlator off a `$stream::send` notification. */
1766
+ function _streamRequestId(m) {
1767
+ const params = m.params;
1768
+ if (params === null || typeof params !== "object") return void 0;
1769
+ const requestId = params.requestId;
1770
+ return typeof requestId === "number" || typeof requestId === "string" ? requestId : void 0;
1771
+ }
1772
+ /** Read the `dir` discriminator off a `$stream::send` notification. */
1773
+ function _streamDir(m) {
1774
+ const params = m.params;
1775
+ if (params === null || typeof params !== "object") return void 0;
1776
+ const dir = params.dir;
1777
+ return dir === StreamDir.toCaller || dir === StreamDir.toCallee ? dir : void 0;
1778
+ }
1779
+ /** Clone a `$stream::send` notification with `requestId` replaced (id restore). */
1780
+ function _withRequestId(m, requestId) {
1781
+ const params = {
1782
+ ...m.params,
1783
+ requestId
1784
+ };
1785
+ return {
1786
+ ...m,
1787
+ params
1788
+ };
1789
+ }
1790
+ /** `true` for bare/interface form, `false` for fully-qualified, `undefined` if malformed. */
1791
+ function _isRootForm(method) {
1792
+ const p = parseMethodName(method);
1793
+ if (!p) return void 0;
1794
+ return p.kind === "bare" || p.kind === "interface";
1795
+ }
1796
+ function _encodeId(origin, id) {
1797
+ return `${origin}${SEP}${typeof id === "number" ? "n" : "s"}${SEP}${String(id)}`;
1798
+ }
1799
+ function _decodeId(id) {
1800
+ if (typeof id !== "string") return void 0;
1801
+ const i1 = id.indexOf(SEP);
1802
+ if (i1 < 0) return void 0;
1803
+ const origin = id.slice(0, i1);
1804
+ if (origin !== "H" && origin !== "C") return void 0;
1805
+ const i2 = id.indexOf(SEP, i1 + 1);
1806
+ if (i2 < 0) return void 0;
1807
+ const type = id.slice(i1 + 1, i2);
1808
+ const raw = id.slice(i2 + 1);
1809
+ return {
1810
+ origin,
1811
+ id: type === "n" ? Number(raw) : raw
1812
+ };
1813
+ }
1814
+ //#endregion
1815
+ //#region src/hub/server/routing/rootOverlay.ts
1816
+ /**
1817
+ * A **RootOverlay** is the per-participant front door. It is a pure
1818
+ * {@link OverlaySplitter} wiring with no routing table, no nested hub, and —
1819
+ * deliberately — **no knowledge of which services it serves**. It exposes:
1820
+ *
1821
+ * - {@link root} — the connection serving this participant's root services.
1822
+ * Install bootstrap/identity modules onto it with `registerHubServices` /
1823
+ * `registerIdentityServices` *before or after* connecting the participant.
1824
+ * - {@link connectParticipant} — wires the downstream participant through the
1825
+ * splitter (`P·root → root`, `P·* → uplink`, `uplink·* → P`).
1826
+ *
1827
+ * Because an overlay represents exactly one participant, prefix claims happen
1828
+ * only on the parent (via the {@link import('./routingHub').AttachedLink} that
1829
+ * `registerHubServices` was given); there is no local table to write.
1830
+ *
1831
+ * The overlay is generic over the per-call `TContext` its {@link root}
1832
+ * connection sees. Pass a context-stamping {@link ChannelTransport} to
1833
+ * {@link connectParticipant} to attach per-call context. Most callers connect a
1834
+ * plain (or signature-verifying) transport and leave `TContext` as `undefined`
1835
+ * (context-free).
1836
+ */
1837
+ var RootOverlay = class {
1838
+ /**
1839
+ * The per-participant root connection. Register the overlay's root
1840
+ * services here — e.g. `registerHubServices(overlay.root, upstream)` and
1841
+ * `registerIdentityServices(overlay.root, { resolveIdentity })`. Reachable
1842
+ * only by this overlay's participant (root-addressed calls land here).
1843
+ *
1844
+ * Its inbound transport carries the overlay's `TContext`, so identity-aware
1845
+ * front doors — notably the consent `hubAccess` service — can be served here
1846
+ * for the interface-form calls a participant addresses to its own root.
1847
+ */
1848
+ root;
1849
+ nodeId = `overlay:${randomUUID()}`;
1850
+ participantPortId = `port:${randomUUID()}`;
1851
+ rootPortId = `port:${randomUUID()}`;
1852
+ rootNodeId = `endpoint:${randomUUID()}`;
1853
+ uplinkPortId;
1854
+ _uplink;
1855
+ _rootPair;
1856
+ _inspection;
1857
+ _splitter;
1858
+ _disposed = false;
1859
+ constructor(options) {
1860
+ this._uplink = options.uplink;
1861
+ this._inspection = options.inspection;
1862
+ this.uplinkPortId = options.uplinkPortId ?? `port:${randomUUID()}`;
1863
+ this._rootPair = new TransportPair();
1864
+ this.root = LinkRpcConnection.fromTransport(this._rootPair.b);
1865
+ this.root.register(nodeInterface$1, { getNodeId: () => ({
1866
+ nodeId: this.nodeId,
1867
+ portId: this.participantPortId
1868
+ }) });
1869
+ }
1870
+ managedTopology(edgeLabel, peerTransport) {
1871
+ return {
1872
+ node: {
1873
+ nodeId: this.nodeId,
1874
+ kind: "hub",
1875
+ label: "Root overlay",
1876
+ ports: [
1877
+ {
1878
+ portId: this.participantPortId,
1879
+ label: "participant"
1880
+ },
1881
+ {
1882
+ portId: this.rootPortId,
1883
+ label: "root"
1884
+ },
1885
+ {
1886
+ portId: this.uplinkPortId,
1887
+ label: "uplink"
1888
+ }
1889
+ ]
1890
+ },
1891
+ hubLinks: [{
1892
+ hubPortId: this.uplinkPortId,
1893
+ nodePortId: this.uplinkPortId,
1894
+ ...edgeLabel !== void 0 ? { label: edgeLabel } : {}
1895
+ }],
1896
+ peerPortId: this.participantPortId,
1897
+ ...peerTransport !== void 0 ? { peerTransport } : {},
1898
+ adjacentNodes: [{
1899
+ nodeId: this.rootNodeId,
1900
+ kind: "endpoint",
1901
+ label: "Root services",
1902
+ ports: [{ portId: this.rootPortId }]
1903
+ }],
1904
+ adjacentLinks: [{
1905
+ from: {
1906
+ nodeId: this.nodeId,
1907
+ portId: this.rootPortId
1908
+ },
1909
+ to: {
1910
+ nodeId: this.rootNodeId,
1911
+ portId: this.rootPortId
1912
+ }
1913
+ }]
1914
+ };
1915
+ }
1916
+ /**
1917
+ * Connect the downstream participant. Root-addressed calls from it hit the
1918
+ * {@link root} services; prefixed calls forward via the uplink. May be
1919
+ * called once.
1920
+ *
1921
+ * Connect a context-stamping {@link ChannelTransport} when `TContext` is
1922
+ * concrete; otherwise connect the raw participant transport directly.
1923
+ */
1924
+ connectParticipant(participant) {
1925
+ if (this._splitter) throw new Error("RootOverlay: participant already connected");
1926
+ this._splitter = new OverlaySplitter(participant, this._rootPair.a, this._uplink, this._inspection);
1927
+ }
1928
+ /**
1929
+ * Tear the overlay down: detach the splitter and dispose the internal root
1930
+ * transport pair. The caller owns the uplink and the participant transport
1931
+ * and disposes them separately (disposing the uplink's
1932
+ * {@link import('./routingHub').AttachedLink} releases any claimed prefixes
1933
+ * on the parent). Idempotent.
1934
+ */
1935
+ dispose() {
1936
+ if (this._disposed) return;
1937
+ this._disposed = true;
1938
+ this._splitter?.dispose();
1939
+ this._rootPair.a.dispose();
1940
+ this._rootPair.b.dispose();
1941
+ }
1942
+ };
1943
+ //#endregion
1944
+ //#region src/hub/server/rootServices.ts
1945
+ /**
1946
+ * Install a participant's **connection root services** onto its overlay root
1947
+ * connection `root`:
1948
+ *
1949
+ * - `hubGrantedServiceId::get` — the connection round-trip: reports the
1950
+ * `grantedServiceIdNamespace` this connection may claim freely.
1951
+ * - `hubGrantedServiceId::register` — claims a prefix on
1952
+ * `upstream` (the parent hub link) within the connection's granted namespace,
1953
+ * gated by `authorizeClaim` (default: namespace membership). Because an
1954
+ * overlay represents a single participant, *one* upstream claim suffices: all
1955
+ * prefixed traffic the parent routes to this overlay is for this participant,
1956
+ * so the {@link OverlaySplitter} delivers it without any local table.
1957
+ * - `hubrpc.directory::list` — by default a **referral** (own root interfaces
1958
+ * plus one row pointing at `<hubServiceId>::hubrpc.directory`); an
1959
+ * implementation is free to aggregate the parent's listing instead.
1960
+ * - `hubrpc.schemas::get` — schemas for this overlay's own root interfaces.
1961
+ *
1962
+ * The hub's consent front door (`hubAccess::*`) is installed separately at the
1963
+ * overlay root by {@link registerHubAccessService}; it needs no capability of
1964
+ * its own because the root is never forwarded.
1965
+ *
1966
+ * The overlay itself stays unaware of which services it serves; this is the
1967
+ * routing module that gives it its front door.
1968
+ */
1969
+ function registerHubServices(root, upstream, options = {}) {
1970
+ const hubServiceId = options.hubServiceId ?? "hub";
1971
+ const grantedServiceIdNamespace = options.grantedServiceIdNamespace ?? "";
1972
+ const authorizeClaim = options.authorizeClaim;
1973
+ root.register(hubGrantedServiceIdInterface, {
1974
+ get: () => {
1975
+ return { grantedServiceIdNamespace };
1976
+ },
1977
+ getHubServiceId: () => {
1978
+ return { hubServiceId };
1979
+ },
1980
+ register: ({ serviceId }) => {
1981
+ const verdict = authorizeClaim ? authorizeClaim(serviceId) : grantedServiceIdNamespace !== "" && isServiceIdUnder(serviceId, grantedServiceIdNamespace) ? { ok: true } : {
1982
+ ok: false,
1983
+ reason: `"${serviceId}" is outside this connection's granted namespace "${grantedServiceIdNamespace}"`
1984
+ };
1985
+ if (!verdict.ok) throw new RpcError(`claim denied: ${verdict.reason}`, ErrorCode.invalidRequest);
1986
+ upstream.addPrefixRoute(serviceId);
1987
+ return {};
1988
+ }
1989
+ });
1990
+ root.register(directoryInterface, {
1991
+ list: ({ interfaceId, interfaceIdPrefix, serviceId, serviceIdScopes }) => {
1992
+ const referral = {
1993
+ serviceId: hubServiceId,
1994
+ interfaceId: directoryInterface.info.id,
1995
+ interfaceHash: directoryInterface.schemaHash,
1996
+ reachableServiceIds: [{ prefix: "" }]
1997
+ };
1998
+ return { items: [...root.listRegisteredInterfaces(), referral].filter((it) => interfaceId === void 0 || it.interfaceId === interfaceId).filter((it) => interfaceIdPrefix === void 0 || it.interfaceId.startsWith(interfaceIdPrefix)).filter((it) => serviceId === void 0 || it.serviceId === serviceId).filter((it) => serviceIdMatchesScopes(it.serviceId, serviceIdScopes)).map((it) => {
1999
+ const entry = {
2000
+ serviceId: it.serviceId,
2001
+ interfaceId: it.interfaceId,
2002
+ interfaceHash: it.interfaceHash
2003
+ };
2004
+ const desc = it.serviceDescription;
2005
+ if (desc !== void 0) entry.serviceDescription = desc;
2006
+ const sets = it.rootPrincipalSets;
2007
+ if (sets !== void 0) entry.rootPrincipalSets = sets.map((s) => [...s]);
2008
+ const reachable = it.reachableServiceIds;
2009
+ if (reachable !== void 0) entry.reachableServiceIds = [...reachable];
2010
+ return entry;
2011
+ }) };
2012
+ },
2013
+ watch: (_params, _ctx, stream) => new Promise((resolve) => {
2014
+ if (stream.signal.aborted) {
2015
+ resolve({});
2016
+ return;
2017
+ }
2018
+ let pending = false;
2019
+ const flush = () => {
2020
+ pending = false;
2021
+ if (!stream.signal.aborted) stream.send({}).catch(() => void 0);
2022
+ };
2023
+ const unsubscribe = root.onDidChangeDirectory(() => {
2024
+ if (pending) return;
2025
+ pending = true;
2026
+ queueMicrotask(flush);
2027
+ });
2028
+ stream.signal.addEventListener("abort", () => {
2029
+ unsubscribe();
2030
+ resolve({});
2031
+ }, { once: true });
2032
+ })
2033
+ });
2034
+ root.register(schemasInterface, { get: ({ interfaceId, hash }) => {
2035
+ const iface = root.findRegisteredInterface(interfaceId, hash);
2036
+ if (!iface) throw new RpcError("Interface not found", ErrorCode.methodNotFound, {
2037
+ reason: "unknown-interface",
2038
+ interfaceId,
2039
+ hash
2040
+ });
2041
+ return { schema: iface.toSchema() };
2042
+ } });
2043
+ }
2044
+ /**
2045
+ * Install `identity::*` (and optionally `identity.storage::*`) onto a
2046
+ * participant's overlay root connection. The identity is resolved lazily on
2047
+ * first use — see {@link registerLazyIdentityOnOverlay}.
2048
+ *
2049
+ * This is the identity counterpart to {@link registerHubServices}: a small,
2050
+ * composable module the overlay does not need to know about.
2051
+ */
2052
+ function registerIdentityServices(root, options) {
2053
+ registerLazyIdentityOnOverlay(root, options.resolveIdentity, options.storage);
2054
+ }
2055
+ //#endregion
2056
+ //#region src/hub/server/hubRegister.ts
2057
+ /**
2058
+ * Annotate an inbound transport so every request carries its wire id in a
2059
+ * {@link RegisterCallContext}. Synchronous and signature-agnostic: it neither
2060
+ * verifies signatures nor checks capabilities (that has already happened on the
2061
+ * forwarded path — see {@link registerHubServiceIdRegistry}). It exists only so
2062
+ * a hub-hosted handler can recover the request id the typed channel would
2063
+ * otherwise strip.
2064
+ */
2065
+ function withRequestIdContext(link) {
2066
+ return {
2067
+ send: (message) => link.send(message),
2068
+ setListener: (listener) => {
2069
+ if (listener === void 0) {
2070
+ link.setListener(void 0);
2071
+ return;
2072
+ }
2073
+ link.setListener((message) => {
2074
+ const requestId = isRequest(message) ? message.id : void 0;
2075
+ const contextual = {
2076
+ ...message,
2077
+ context: { requestId }
2078
+ };
2079
+ Object.defineProperty(contextual, "context", {
2080
+ enumerable: false,
2081
+ writable: false
2082
+ });
2083
+ listener(contextual);
2084
+ });
2085
+ },
2086
+ dispose: () => link.dispose()
2087
+ };
2088
+ }
2089
+ /**
2090
+ * Install the hub's privileged **claim front door** — the typed
2091
+ * `hubServiceIdRegistry::registerServiceId` handler — onto the hub services
2092
+ * `connection` (mounted under `hubServiceId`).
2093
+ *
2094
+ * Unlike the per-overlay `registerHubServices` front door — which only lets a
2095
+ * participant claim *within its provenance-granted namespace* — this endpoint
2096
+ * is the path for claiming a prefix **outside** that namespace.
2097
+ *
2098
+ * The handler is a **pure side effect**. Authentication (signature) and
2099
+ * authorization (an admin-rooted capability that permits
2100
+ * `hub::hubServiceIdRegistry::registerServiceId` for the requested
2101
+ * `requestedPrefix`) are enforced *before* the call ever reaches here, by the
2102
+ * {@link import('./forwardedCallGate').withForwardedCallGate forwarded-call
2103
+ * gate} every untrusted participant sits behind. By the time the request lands,
2104
+ * it is already known to be authentic and authorized, so the handler only:
2105
+ *
2106
+ * 1. validates the prefix is well-formed;
2107
+ * 2. resolves the link the request was forwarded from via
2108
+ * {@link Hub.getSourceTransport} (keyed by {@link RegisterCallContext.requestId});
2109
+ * and
2110
+ * 3. binds the prefix to that link via {@link Hub.claimPrefix}.
2111
+ *
2112
+ * The source transport answers only *where* to route the prefix, never
2113
+ * *whether* the claim is allowed.
2114
+ */
2115
+ function registerHubServiceIdRegistry(connection, options) {
2116
+ const { hub, hubServiceId } = options;
2117
+ connection.register(hubServiceIdRegistryInterface, { registerServiceId: async ({ requestedPrefix }, ctx) => {
2118
+ const prefixError = validatePrefix(requestedPrefix);
2119
+ if (prefixError) throw new RpcError(prefixError, ErrorCode.invalidParams);
2120
+ const origin = ctx.requestId !== void 0 ? hub.getSourceTransport(ctx.requestId) : void 0;
2121
+ if (origin === void 0) throw new RpcError("no source link for claim", ErrorCode.invalidRequest);
2122
+ try {
2123
+ hub.claimPrefix(origin, requestedPrefix);
2124
+ } catch (e) {
2125
+ throw new RpcError(e instanceof Error ? e.message : String(e), ErrorCode.invalidRequest, { prefix: requestedPrefix });
2126
+ }
2127
+ return {};
2128
+ } }, { serviceId: hubServiceId });
2129
+ }
2130
+ //#endregion
2131
+ //#region src/hub/server/hubServices.ts
2132
+ function createHubServiceInterfaces(hub, options = {}) {
2133
+ const hubServiceId = options.hubServiceId ?? "hub";
2134
+ const link = hub.attachOut({
2135
+ exclusiveHubRootHandler: true,
2136
+ routePrefixes: [hubServiceId]
2137
+ });
2138
+ hub.markInspectionService(link, hubServiceId);
2139
+ const connection = LinkRpcConnection.fromTransport(withRequestIdContext(link.transport));
2140
+ const inspector = new HubInspector(hub);
2141
+ registerInspectionInterfaces(connection, hub, inspector, hubServiceId, link.portId, options.descriptors);
2142
+ registerHubReflection(connection, hub, hubServiceId, options.beforeDirectoryQuery);
2143
+ registerHubServiceIdRegistry(connection, {
2144
+ hub,
2145
+ hubServiceId
2146
+ });
2147
+ return {
2148
+ connection,
2149
+ hubServiceId,
2150
+ inspector,
2151
+ dispose: () => {
2152
+ inspector.dispose();
2153
+ connection.close();
2154
+ link.dispose();
2155
+ }
2156
+ };
2157
+ }
2158
+ function registerInspectionInterfaces(connection, hub, inspector, hubServiceId, portId, descriptors) {
2159
+ connection.register(nodeInterface, { getNodeId: () => ({
2160
+ nodeId: hub.nodeId,
2161
+ portId,
2162
+ ...descriptors !== void 0 ? { descriptors: [...descriptors] } : {}
2163
+ }) }, { serviceId: hubServiceId });
2164
+ connection.register(topologyInterface, {
2165
+ getGraph: () => hub.getTopologyGraph(hubServiceId),
2166
+ watchGraph: (_params, _ctx, stream) => new Promise((resolve) => {
2167
+ if (stream.signal.aborted) {
2168
+ resolve({});
2169
+ return;
2170
+ }
2171
+ let pending = false;
2172
+ const flush = () => {
2173
+ pending = false;
2174
+ if (!stream.signal.aborted) stream.send({}).catch(() => void 0);
2175
+ };
2176
+ const unsubscribe = hub.onDidChangeTopology(() => {
2177
+ if (pending) return;
2178
+ pending = true;
2179
+ queueMicrotask(flush);
2180
+ });
2181
+ stream.signal.addEventListener("abort", () => {
2182
+ unsubscribe();
2183
+ resolve({});
2184
+ }, { once: true });
2185
+ })
2186
+ }, { serviceId: hubServiceId });
2187
+ connection.register(trafficInterface, {
2188
+ watch: ({ methodPrefix, trafficIgnoreKey, focusRequest }, _ctx, stream) => runTrafficWatch(inspector, {
2189
+ methodPrefix,
2190
+ trafficIgnoreKey,
2191
+ focusRequest
2192
+ }, stream),
2193
+ watchWithPayloads: ({ methodPrefix, maxPayloadBytes, trafficIgnoreKey, focusRequest }, _ctx, stream) => runTrafficWatch(inspector, {
2194
+ methodPrefix,
2195
+ maxPayloadBytes,
2196
+ trafficIgnoreKey,
2197
+ focusRequest
2198
+ }, stream)
2199
+ }, { serviceId: hubServiceId });
2200
+ }
2201
+ async function runTrafficWatch(inspector, options, stream) {
2202
+ const subscription = inspector.subscribe(options, (event) => stream.send(event));
2203
+ const dispose = () => subscription.dispose();
2204
+ if (stream.signal.aborted) dispose();
2205
+ else stream.signal.addEventListener("abort", dispose, { once: true });
2206
+ try {
2207
+ return await subscription.closed;
2208
+ } finally {
2209
+ stream.signal.removeEventListener("abort", dispose);
2210
+ subscription.dispose();
2211
+ }
2212
+ }
2213
+ /**
2214
+ * Register the hub's **reflection** surface — `hubrpc.directory::list` /
2215
+ * `hubrpc.schemas::get` — on `connection`, mounted under `hubServiceId`.
2216
+ *
2217
+ * This is the always-on half of {@link createHubServiceInterfaces}: the
2218
+ * destination every {@link RootOverlay} directory *refers* world-view queries
2219
+ * to, independent of whether the signed, capability-gated claim front door
2220
+ * ({@link registerHubServiceIdRegistry}) is enabled.
2221
+ *
2222
+ * `directory.list` returns the hub's own services plus the referrals explicitly
2223
+ * returned by participant connection-root directories. Routing claims do not
2224
+ * create directory entries: a routable service that is not explicitly listed
2225
+ * remains callable when already known but is not discoverable.
2226
+ */
2227
+ function registerHubReflection(connection, hub, hubServiceId, beforeDirectoryQuery) {
2228
+ connection.register(directoryInterface, {
2229
+ list: ({ interfaceId, interfaceIdPrefix, serviceId, serviceIdScopes }) => {
2230
+ return { items: [{
2231
+ serviceId: hubServiceId,
2232
+ interfaceId: directoryInterface.info.id,
2233
+ interfaceHash: directoryInterface.schemaHash,
2234
+ reachableServiceIds: [{ prefix: hubServiceId }]
2235
+ }].filter((it) => interfaceId === void 0 || it.interfaceId === interfaceId).filter((it) => interfaceIdPrefix === void 0 || it.interfaceId.startsWith(interfaceIdPrefix)).filter((it) => serviceId === void 0 || it.serviceId === serviceId).filter((it) => serviceIdMatchesScopes(it.serviceId, serviceIdScopes)) };
2236
+ },
2237
+ watch: directoryWatchNever
2238
+ });
2239
+ connection.register(schemasInterface, { get: ({ interfaceId, hash }) => {
2240
+ const iface = connection.findRegisteredInterface(interfaceId, hash);
2241
+ if (!iface) throw new RpcError("Interface not found", ErrorCode.methodNotFound, {
2242
+ reason: "unknown-interface",
2243
+ interfaceId,
2244
+ hash
2245
+ });
2246
+ return { schema: iface.toSchema() };
2247
+ } });
2248
+ connection.register(directoryInterface, {
2249
+ list: async ({ interfaceId, interfaceIdPrefix, serviceId, serviceIdScopes }) => {
2250
+ await beforeDirectoryQuery?.();
2251
+ const items = [];
2252
+ for (const r of connection.listRegisteredInterfaces()) items.push(r);
2253
+ const rootListings = await hub.queryParticipantRoots(`${directoryInterface.info.id}::list`, { interfaceId: directoryInterface.info.id }, hubServiceId);
2254
+ for (const raw of rootListings) {
2255
+ const page = raw;
2256
+ if (!page || !Array.isArray(page.items)) continue;
2257
+ for (const it of page.items) {
2258
+ if (it.serviceId === "") continue;
2259
+ items.push(it);
2260
+ }
2261
+ }
2262
+ const deduplicated = /* @__PURE__ */ new Map();
2263
+ for (const item of items) {
2264
+ const key = `${item.serviceId}\0${item.interfaceId}\0${item.interfaceHash}`;
2265
+ const previous = deduplicated.get(key);
2266
+ if (previous === void 0) {
2267
+ deduplicated.set(key, item);
2268
+ continue;
2269
+ }
2270
+ const preferred = item.serviceDescription !== void 0 || item.rootPrincipalSets !== void 0 ? item : previous;
2271
+ if (item.interfaceId !== directoryInterface.info.id) {
2272
+ deduplicated.set(key, preferred);
2273
+ continue;
2274
+ }
2275
+ const previousScopes = previous.reachableServiceIds ?? [{ prefix: previous.serviceId }];
2276
+ const itemScopes = item.reachableServiceIds ?? [{ prefix: item.serviceId }];
2277
+ deduplicated.set(key, {
2278
+ ...preferred,
2279
+ reachableServiceIds: normalizeServiceIdScopes([...previousScopes, ...itemScopes])
2280
+ });
2281
+ }
2282
+ return { items: [...deduplicated.values()].filter((it) => interfaceId === void 0 || it.interfaceId === interfaceId).filter((it) => interfaceIdPrefix === void 0 || it.interfaceId.startsWith(interfaceIdPrefix)).filter((it) => serviceId === void 0 || it.serviceId === serviceId).filter((it) => serviceIdMatchesScopes(it.serviceId, serviceIdScopes)).map((it) => ({
2283
+ serviceId: it.serviceId,
2284
+ interfaceId: it.interfaceId,
2285
+ interfaceHash: it.interfaceHash,
2286
+ ...it.serviceDescription !== void 0 ? { serviceDescription: it.serviceDescription } : {},
2287
+ ...it.rootPrincipalSets !== void 0 ? { rootPrincipalSets: it.rootPrincipalSets.map((s) => [...s]) } : {},
2288
+ ...it.reachableServiceIds !== void 0 ? { reachableServiceIds: [...it.reachableServiceIds] } : {}
2289
+ })) };
2290
+ },
2291
+ watch: async (params, _ctx, stream) => {
2292
+ await beforeDirectoryQuery?.();
2293
+ return new Promise((resolve) => {
2294
+ if (stream.signal.aborted) {
2295
+ resolve({});
2296
+ return;
2297
+ }
2298
+ let pending = false;
2299
+ const flush = () => {
2300
+ pending = false;
2301
+ if (stream.signal.aborted) return;
2302
+ stream.send({}).catch(() => void 0);
2303
+ };
2304
+ const notify = () => {
2305
+ if (pending) return;
2306
+ pending = true;
2307
+ queueMicrotask(flush);
2308
+ };
2309
+ const unsubscribeRouting = hub.onDidChangeRouting(notify);
2310
+ const unsubscribeLocal = connection.onDidChangeDirectory(notify);
2311
+ const participantWatches = hub.watchParticipantRoots(`${directoryInterface.info.id}::watch`, params, notify, hubServiceId);
2312
+ stream.signal.addEventListener("abort", () => {
2313
+ unsubscribeRouting();
2314
+ unsubscribeLocal();
2315
+ participantWatches.dispose();
2316
+ resolve({});
2317
+ }, { once: true });
2318
+ });
2319
+ }
2320
+ }, { serviceId: hubServiceId });
2321
+ connection.register(schemasInterface, { get: ({ interfaceId, hash }) => {
2322
+ const iface = connection.findRegisteredInterface(interfaceId, hash);
2323
+ if (!iface) throw new RpcError("Interface not found", ErrorCode.methodNotFound, {
2324
+ reason: "unknown-interface",
2325
+ interfaceId,
2326
+ hash
2327
+ });
2328
+ return { schema: iface.toSchema() };
2329
+ } }, { serviceId: hubServiceId });
2330
+ }
2331
+ //#endregion
2332
+ //#region src/hub/server/connectionTokenBinderService.ts
2333
+ function assertUnderPrefix(value, prefixes, field, configuredPrefix) {
2334
+ if (!prefixes.some((p) => value.startsWith(p))) throw new RpcError(`${field} "${value}" is outside this binder's granted ${configuredPrefix} ${JSON.stringify(prefixes)}`, ErrorCode.invalidRequest);
2335
+ }
2336
+ /**
2337
+ * Install `connectionTokenBinder::bindConnectionToken` on `connection`.
2338
+ *
2339
+ * Two forms, selected by {@link RegisterConnectionTokenBinderOptions.serviceId}:
2340
+ *
2341
+ * - **root form** (no `serviceId`): installed at a participant's overlay root
2342
+ * (root form, never forwarded → never gated). Safe because only the specific
2343
+ * trusted connection the acceptor installed it on can reach it.
2344
+ * - **forwarded form** (`serviceId` set): mounted as a routable hub service so
2345
+ * any participant can discover and call it. Safe only because forwarded calls
2346
+ * go through the hub's forwarded-call gate; the same prefix checks still bound
2347
+ * what any caller may mint.
2348
+ *
2349
+ * Each call mints a single-use token binding the requested `identitySlot` and/or
2350
+ * `serviceIdNamespace`, provided each requested field falls under the matching
2351
+ * configured prefix. Omitting a field leaves that axis unbound.
2352
+ */
2353
+ function registerConnectionTokenBinderService(connection, options) {
2354
+ const { store, identitySlotPrefixes, serviceIdPrefixes, ttlMs, serviceId } = options;
2355
+ connection.register(connectionTokenBinderInterface, { bindConnectionToken: ({ identitySlot, serviceIdNamespace }) => {
2356
+ const binding = {};
2357
+ if (identitySlot !== void 0) {
2358
+ assertUnderPrefix(identitySlot, identitySlotPrefixes, "identitySlot", "identitySlotPrefix");
2359
+ binding.identitySlot = identitySlot;
2360
+ }
2361
+ if (serviceIdNamespace !== void 0) {
2362
+ assertUnderPrefix(serviceIdNamespace, serviceIdPrefixes, "serviceIdNamespace", "serviceIdPrefix");
2363
+ binding.grantedServiceIdNamespace = serviceIdNamespace;
2364
+ }
2365
+ const minted = ttlMs !== void 0 ? store.mint(binding, ttlMs) : store.mint(binding);
2366
+ return {
2367
+ token: minted.token,
2368
+ expiresAt: minted.expiresAt
2369
+ };
2370
+ } }, serviceId !== void 0 ? { serviceId } : {});
2371
+ }
2372
+ //#endregion
2373
+ //#region src/hub/server/connectionHandler.ts
2374
+ /**
2375
+ * Walk `factories` in order; return the first handler that claims `token`, or
2376
+ * `undefined` if none do (the caller drops the connection).
2377
+ */
2378
+ function resolveConnectionHandler(factories, token) {
2379
+ for (const factory of factories) {
2380
+ const handler = factory.handle(token);
2381
+ if (handler !== void 0) return handler;
2382
+ }
2383
+ }
2384
+ /**
2385
+ * Install a participant's root services from a resolved {@link RootProvision}:
2386
+ * the always-on claim/directory front door and consent surface, plus (when
2387
+ * present) the minting front door and lazy managed identity. This is the single
2388
+ * shared installer every built-in handler funnels through.
2389
+ */
2390
+ function provisionRoot(ctx, provision) {
2391
+ registerHubServices(ctx.root, ctx.upstream, {
2392
+ hubServiceId: ctx.hubServiceId,
2393
+ ...provision.grantedServiceIdNamespace !== void 0 ? { grantedServiceIdNamespace: provision.grantedServiceIdNamespace } : {},
2394
+ ...ctx.authorizeClaim !== void 0 ? { authorizeClaim: ctx.authorizeClaim } : {}
2395
+ });
2396
+ ctx.installHubAccess?.(ctx.root);
2397
+ if (provision.connectionTokenBinder !== void 0) registerConnectionTokenBinderService(ctx.root, provision.connectionTokenBinder);
2398
+ if (provision.resolveIdentity !== void 0) registerIdentityServices(ctx.root, {
2399
+ resolveIdentity: provision.resolveIdentity,
2400
+ ...provision.storage !== void 0 ? { storage: provision.storage } : {}
2401
+ });
2402
+ }
2403
+ /**
2404
+ * A factory that claims **any** connection (optionally gated by `claims`) and
2405
+ * provisions a fixed root. Used for the `anonymous` handler (claims all), the
2406
+ * `static` handler (claims a matching token), and dial-in endpoints (claims all,
2407
+ * no token).
2408
+ */
2409
+ function fixedProvisionHandler(provision, claims = () => true) {
2410
+ return { handle: (token) => claims(token) ? (ctx) => provisionRoot(ctx, provision) : void 0 };
2411
+ }
2412
+ /** The `anonymous` handler: claims every connection, provisioning `provision`. */
2413
+ function anonymousHandler(provision) {
2414
+ return fixedProvisionHandler(provision);
2415
+ }
2416
+ /** The `static` handler: claims connections whose token equals `value`. */
2417
+ function staticTokenHandler(value, provision) {
2418
+ return fixedProvisionHandler(provision, (token) => token === value);
2419
+ }
2420
+ /**
2421
+ * The `bound` handler: claims connections whose token is live in `store`, and on
2422
+ * claim **redeems** it (single-use) to derive the provisioned identity slot
2423
+ * and/or granted serviceId namespace. `resolveSlot` turns a redeemed slot into
2424
+ * a lazy identity resolver plus its per-identity storage (injected so this stays
2425
+ * node-agnostic).
2426
+ */
2427
+ function boundTokenHandler(store, resolveSlot) {
2428
+ return { handle: (token) => {
2429
+ if (!store.peek(token)) return void 0;
2430
+ return (ctx) => {
2431
+ const binding = store.redeem(token);
2432
+ if (binding === void 0) throw new Error("bound token was consumed between accept and redeem");
2433
+ provisionRoot(ctx, {
2434
+ ...binding.grantedServiceIdNamespace !== void 0 ? { grantedServiceIdNamespace: binding.grantedServiceIdNamespace } : {},
2435
+ ...binding.identitySlot !== void 0 ? resolveSlot(binding.identitySlot) : {}
2436
+ });
2437
+ };
2438
+ } };
2439
+ }
2440
+ //#endregion
2441
+ //#region src/hub/server/verifiedSignature.ts
2442
+ /**
2443
+ * Wrap an inbound transport so every request's `$hubrpc` signature is verified
2444
+ * **eagerly, at the door** — a pure authenticity gate with no capability check:
2445
+ *
2446
+ * - a **valid** signature → the request is delivered unchanged;
2447
+ * - an **invalid** signature → the request is **rejected** here (an error
2448
+ * response is sent and the message is never delivered);
2449
+ * - an **unsigned** call (no envelope) → delivered unchanged, so keyless
2450
+ * connections still work.
2451
+ *
2452
+ * This is the signature half of the hub's trust model. *Authorization*
2453
+ * (capability chains) is **not** this wrapper's job — that is enforced
2454
+ * separately by {@link import('./forwardedCallGate').withForwardedCallGate} on
2455
+ * the hub-facing (forwarded) path. The root overlay uses this wrapper on its own
2456
+ * root-form calls (consent / identity front doors), which never pass through the
2457
+ * forwarded-call gate and so need their authenticity established here.
2458
+ *
2459
+ * When `verifySignatures` is not `true`, the transport is returned unchanged (no
2460
+ * verification, every message passes verbatim).
2461
+ *
2462
+ * Verification is async but in-order delivery is preserved via a per-link
2463
+ * promise chain.
2464
+ */
2465
+ function withVerifiedSignature(link, options = {}) {
2466
+ if (options.verifySignatures !== true) return link;
2467
+ return {
2468
+ send: (message) => link.send(message),
2469
+ setListener: (listener) => {
2470
+ if (listener === void 0) {
2471
+ link.setListener(void 0);
2472
+ return;
2473
+ }
2474
+ let chain = Promise.resolve();
2475
+ link.setListener((message) => {
2476
+ chain = chain.then(async () => {
2477
+ if (await _rejectedBadSignature(link, message)) return;
2478
+ listener(message);
2479
+ }).catch((e) => {
2480
+ console.error("Error in inbound signature verification:", e);
2481
+ });
2482
+ });
2483
+ },
2484
+ dispose: () => link.dispose()
2485
+ };
2486
+ }
2487
+ /**
2488
+ * Verify a single inbound request's signature. Returns `true` (and sends an
2489
+ * error response over `link`) when the signature is present but invalid;
2490
+ * returns `false` — the message should be delivered — for a valid signature, an
2491
+ * unsigned call, or a non-request.
2492
+ */
2493
+ async function _rejectedBadSignature(link, message) {
2494
+ if (!isRequest(message)) return false;
2495
+ const res = await verifyRpcCall({
2496
+ wireMethod: message.method,
2497
+ wireParams: message.params
2498
+ });
2499
+ if (!res.ok) {
2500
+ link.send({
2501
+ jsonrpc: "2.0",
2502
+ id: message.id,
2503
+ error: {
2504
+ code: ErrorCode.invalidRequest,
2505
+ message: res.reason
2506
+ }
2507
+ });
2508
+ return true;
2509
+ }
2510
+ return false;
2511
+ }
2512
+ //#endregion
2513
+ //#region src/hub/server/forwardedCallGate.ts
2514
+ /**
2515
+ * A **signature front door** for fully-qualified calls. Wrap an incoming
2516
+ * transport with this before attaching it to a hub or constructing the serving
2517
+ * connection, so every `serviceId::interfaceId::member` call must carry a
2518
+ * valid `$hubrpc` signature.
2519
+ * Unsigned or tampered calls are rejected with an error response and never
2520
+ * reach the downstream hub or connection.
2521
+ *
2522
+ * What passes **verbatim** (ungated):
2523
+ * - responses;
2524
+ * - root-addressed requests (`interfaceId::member`) and bare requests
2525
+ * (`member`) — these always terminate at the connection root and are never
2526
+ * forwarded; and
2527
+ * - requests targeting an {@link ForwardedCallGateOptions.exemptPrefixes exempt
2528
+ * prefix} (the hub's own services, which gate themselves).
2529
+ *
2530
+ * The gate **does not strip** the envelope: signed wire params keep the real
2531
+ * call params at the top level alongside `$hubrpc`/`$hubrpcUnsigned`, so the
2532
+ * target's typed handler recovers them via schema-stripping while a target that
2533
+ * cares may re-verify as defense-in-depth. Authorization is *not* this gate's
2534
+ * job (unless {@link ForwardedCallGateOptions.requireCapability} is set): it
2535
+ * proves *who* is calling, leaving *whether they may* to the capability layer.
2536
+ *
2537
+ * In-order delivery toward the hub is preserved across the async verification:
2538
+ * inbound messages are processed through a single serial queue.
2539
+ */
2540
+ function withForwardedCallGate(inner, options) {
2541
+ return withFullyQualifiedCallGate(inner, options);
2542
+ }
2543
+ /** Explicitly named alias for {@link withForwardedCallGate}. */
2544
+ function withFullyQualifiedCallGate(inner, options) {
2545
+ return new ForwardedCallGate(inner, options);
2546
+ }
2547
+ var ForwardedCallGate = class {
2548
+ _inner;
2549
+ _options;
2550
+ _exempt;
2551
+ /**
2552
+ * Per-call nonces already admitted on this link. A call replaying a
2553
+ * nonce is rejected — this is the single replay ledger, and it makes any
2554
+ * `callBind`-scoped capability single-use for free.
2555
+ */
2556
+ _seenNonces = /* @__PURE__ */ new Set();
2557
+ _downstream;
2558
+ /** Serializes inbound processing so async verification never reorders the stream. */
2559
+ _tail = Promise.resolve();
2560
+ constructor(_inner, _options) {
2561
+ this._inner = _inner;
2562
+ this._options = _options;
2563
+ this._exempt = new Set(_options.exemptPrefixes ?? []);
2564
+ }
2565
+ /** hub → participant: verbatim, never gated. */
2566
+ send(message) {
2567
+ this._inner.send(message);
2568
+ }
2569
+ setListener(listener) {
2570
+ this._downstream = listener;
2571
+ this._inner.setListener(listener === void 0 ? void 0 : (message) => {
2572
+ this._tail = this._tail.then(() => this._process(message));
2573
+ });
2574
+ }
2575
+ dispose() {
2576
+ this._inner.dispose();
2577
+ }
2578
+ async _process(m) {
2579
+ if (!isRequest(m) && !isNotification(m)) {
2580
+ this._downstream?.(m);
2581
+ return;
2582
+ }
2583
+ const parsed = parseMethodName(m.method);
2584
+ if (!parsed || parsed.kind !== "full" || this._exempt.has(parsed.serviceId)) {
2585
+ this._downstream?.(m);
2586
+ return;
2587
+ }
2588
+ await this._gate(m);
2589
+ }
2590
+ async _gate(call) {
2591
+ const nowMs = this._options.nowMs?.();
2592
+ const res = await verifyCall({
2593
+ method: call.method,
2594
+ params: call.params,
2595
+ parseMethod: methodNameToTarget,
2596
+ requireCapability: this._options.requireCapability === true,
2597
+ ...nowMs !== void 0 ? { nowMs } : {}
2598
+ });
2599
+ if (!res.ok) {
2600
+ const code = res.kind === "capability" ? ErrorCode.permissionRequired : ErrorCode.invalidRequest;
2601
+ this._reject(call, code, res.reason);
2602
+ return;
2603
+ }
2604
+ if (this._seenNonces.has(res.nonce)) {
2605
+ this._reject(call, ErrorCode.invalidRequest, "replayed request nonce");
2606
+ return;
2607
+ }
2608
+ if (this._options.requireCapability === true) {
2609
+ const verdict = await permits(res.call, res.capabilities, (serviceId) => this._acceptedRootIssuers(serviceId), nowMs ?? Date.now());
2610
+ if (!verdict.ok) {
2611
+ this._reject(call, ErrorCode.permissionRequired, verdict.reason);
2612
+ return;
2613
+ }
2614
+ }
2615
+ this._seenNonces.add(res.nonce);
2616
+ this._downstream?.(call);
2617
+ }
2618
+ _reject(call, code, message) {
2619
+ if (!isRequest(call)) return;
2620
+ this._inner.send({
2621
+ jsonrpc: "2.0",
2622
+ id: call.id,
2623
+ error: {
2624
+ code,
2625
+ message
2626
+ }
2627
+ });
2628
+ }
2629
+ _acceptedRootIssuers(serviceId) {
2630
+ if (this._options.requireCapability !== true) return [];
2631
+ if (this._options.trustedRoots !== void 0) return this._options.trustedRoots.map(({ principal }) => ({
2632
+ principal,
2633
+ isPublic: true
2634
+ }));
2635
+ return this._options.acceptedRootIssuers(serviceId);
2636
+ }
2637
+ };
2638
+ //#endregion
2639
+ //#region src/hub/server/hubConnectionAcceptor.ts
2640
+ /**
2641
+ * Bridges accepted transports onto the hubv2 graph via a pluggable chain of
2642
+ * {@link ConnectionHandlerFactory connection handlers}. For each transport it:
2643
+ *
2644
+ * 1. attaches an uplink to the central hub and builds a {@link RootOverlay};
2645
+ * 2. resolves the first handler whose token check claims the connection and
2646
+ * lets it install the participant's root services (claim/directory front
2647
+ * door + consent, and optionally identity, granted namespace, minting) —
2648
+ * dropping the connection if none claim; and
2649
+ * 3. connects the participant, disposing the overlay and detaching the uplink
2650
+ * (which releases its claimed prefixes) when the transport closes.
2651
+ *
2652
+ * It never names a backend transport type — hand it any {@link ITransportServer}.
2653
+ */
2654
+ var HubConnectionAcceptor = class {
2655
+ _options;
2656
+ _accepted = /* @__PURE__ */ new Set();
2657
+ _disposed = false;
2658
+ constructor(_options) {
2659
+ this._options = _options;
2660
+ this._options.server.setConnectionHandler((t) => {
2661
+ this._accept(t);
2662
+ });
2663
+ }
2664
+ _accept(transport) {
2665
+ if (this._disposed) {
2666
+ transport.dispose();
2667
+ return;
2668
+ }
2669
+ let accepted;
2670
+ try {
2671
+ accepted = this._wire(transport);
2672
+ } catch (e) {
2673
+ this._options.onError?.(e instanceof Error ? e : new Error(String(e)));
2674
+ transport.dispose();
2675
+ return;
2676
+ }
2677
+ this._accepted.add(accepted);
2678
+ transport.onDidClose(() => {
2679
+ accepted.overlay.dispose();
2680
+ accepted.topology.dispose();
2681
+ accepted.upstream.dispose();
2682
+ this._accepted.delete(accepted);
2683
+ });
2684
+ this._options.onAttached?.({ overlay: accepted.overlay });
2685
+ }
2686
+ /**
2687
+ * Wrap the hub-facing link per the {@link ForwardCheckingPolicy}. The
2688
+ * option union guarantees that capability mode always carries its
2689
+ * `adminIds` trust anchors, so this can never silently fail closed by
2690
+ * forgetting them.
2691
+ */
2692
+ _gateHubFacing(inner) {
2693
+ if (this._options.verifyForwardedCalls !== true) return inner;
2694
+ if (this._options.requireForwardedCapability === true) {
2695
+ const adminIds = [...this._options.adminIds];
2696
+ return withForwardedCallGate(inner, {
2697
+ requireCapability: true,
2698
+ acceptedRootIssuers: () => adminIds.map((nodeId) => ({
2699
+ principal: nodeId,
2700
+ isPublic: true
2701
+ }))
2702
+ });
2703
+ }
2704
+ const adminIds = this._options.adminIds !== void 0 ? [...this._options.adminIds] : void 0;
2705
+ return withForwardedCallGate(inner, { ...adminIds !== void 0 ? { acceptedRootIssuers: () => adminIds.map((nodeId) => ({
2706
+ principal: nodeId,
2707
+ isPublic: true
2708
+ })) } : {} });
2709
+ }
2710
+ _wire(transport) {
2711
+ const token = transport.initializeToken;
2712
+ const handler = resolveConnectionHandler(this._options.handlers, token);
2713
+ if (handler === void 0) throw new Error("no connection handler accepted the presented token");
2714
+ const pair = new TransportPair();
2715
+ const hubFacing = this._gateHubFacing(pair.b);
2716
+ const upstream = this._options.hub.attach(hubFacing);
2717
+ const overlay = new RootOverlay({
2718
+ uplink: pair.a,
2719
+ uplinkPortId: upstream.portId
2720
+ });
2721
+ const topology = this._options.hub.registerManagedRoutingTopology(upstream, overlay.managedTopology(upstream.edgeId, transport.topologyInfo));
2722
+ const policy = this._options.policy;
2723
+ const ctx = {
2724
+ root: overlay.root,
2725
+ upstream,
2726
+ hub: this._options.hub,
2727
+ transport,
2728
+ token,
2729
+ hubServiceId: this._options.hubServiceId ?? "hub",
2730
+ ...policy !== void 0 ? { authorizeClaim: (requestedPrefix) => policy.authorizeClaim({
2731
+ principal: void 0,
2732
+ transport,
2733
+ requestedPrefix
2734
+ }) } : {},
2735
+ ...this._options.installHubAccess !== void 0 ? { installHubAccess: this._options.installHubAccess } : {}
2736
+ };
2737
+ try {
2738
+ handler(ctx);
2739
+ const verifySignatures = this._options.verifyForwardedCalls === true;
2740
+ overlay.connectParticipant(withVerifiedSignature(transport, { verifySignatures }));
2741
+ return {
2742
+ overlay,
2743
+ upstream,
2744
+ topology
2745
+ };
2746
+ } catch (error) {
2747
+ overlay.dispose();
2748
+ topology.dispose();
2749
+ upstream.dispose();
2750
+ throw error;
2751
+ }
2752
+ }
2753
+ dispose() {
2754
+ if (this._disposed) return;
2755
+ this._disposed = true;
2756
+ for (const accepted of this._accepted) {
2757
+ accepted.overlay.dispose();
2758
+ accepted.topology.dispose();
2759
+ accepted.upstream.dispose();
2760
+ }
2761
+ this._accepted.clear();
2762
+ this._options.server.dispose();
2763
+ }
2764
+ };
2765
+ //#endregion
2766
+ export { validatePrefix as S, RootOverlay as _, anonymousHandler as a, Hub as b, provisionRoot as c, registerConnectionTokenBinderService as d, createHubServiceInterfaces as f, registerIdentityServices as g, registerHubServices as h, withVerifiedSignature as i, resolveConnectionHandler as l, withRequestIdContext as m, withForwardedCallGate as n, boundTokenHandler as o, registerHubServiceIdRegistry as p, withFullyQualifiedCallGate as r, fixedProvisionHandler as s, HubConnectionAcceptor as t, staticTokenHandler as u, OverlaySplitter as v, ForwardingTable as x, HubInspector as y };
2767
+
2768
+ //# sourceMappingURL=hubConnectionAcceptor-B5-8JsFY.js.map