@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.
- package/README.md +101 -0
- package/dist/chunks/config-BCvkg7jv.d.ts +566 -0
- package/dist/chunks/configFile-y6Phntqu.d.ts +9 -0
- package/dist/chunks/connectionTokenBinder.interfaces-B-beCg06.js +40 -0
- package/dist/chunks/connectionTokenBinder.interfaces-B-beCg06.js.map +1 -0
- package/dist/chunks/connectionTokenBinder.interfaces-BhzQ1DTS.d.ts +36 -0
- package/dist/chunks/hubConnectionAcceptor-B5-8JsFY.js +2768 -0
- package/dist/chunks/hubConnectionAcceptor-B5-8JsFY.js.map +1 -0
- package/dist/chunks/hubConnectionAcceptor-BwydvFa4.d.ts +1560 -0
- package/dist/chunks/index-CLIUrV88.d.ts +481 -0
- package/dist/chunks/node-CTXsQ6oa.js +460 -0
- package/dist/chunks/node-CTXsQ6oa.js.map +1 -0
- package/dist/chunks/nodeTransit-CWeFnbwt.js +444 -0
- package/dist/chunks/nodeTransit-CWeFnbwt.js.map +1 -0
- package/dist/chunks/nodeTransit-cmtZgdpW.d.ts +226 -0
- package/dist/chunks/runHub-P3YUwwdv.js +1797 -0
- package/dist/chunks/runHub-P3YUwwdv.js.map +1 -0
- package/dist/chunks/server-BAxchQhy.js +1368 -0
- package/dist/chunks/server-BAxchQhy.js.map +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +38 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.js +305 -0
- package/dist/config.js.map +1 -0
- package/dist/configFile.d.ts +2 -0
- package/dist/configFile.js +27 -0
- package/dist/configFile.js.map +1 -0
- package/dist/engine/runHub.d.ts +42 -0
- package/dist/engine/runHub.js +2 -0
- package/dist/hub/server/client.d.ts +2 -0
- package/dist/hub/server/client.js +2 -0
- package/dist/hub/server/connectionTokenBinder.d.ts +2 -0
- package/dist/hub/server/connectionTokenBinder.js +2 -0
- package/dist/hub/server/index.d.ts +5 -0
- package/dist/hub/server/index.js +5 -0
- package/dist/hub/server/node/index.d.ts +218 -0
- package/dist/hub/server/node/index.js +2 -0
- package/dist/hub/server/transit.d.ts +2 -0
- package/dist/hub/server/transit.js +2 -0
- package/dist/index.d.ts +512 -0
- package/dist/index.js +309 -0
- package/dist/index.js.map +1 -0
- package/dist/serve.d.ts +14 -0
- package/dist/serve.js +31 -0
- package/dist/serve.js.map +1 -0
- package/dist/spawn.d.ts +12 -0
- package/dist/spawn.js +25 -0
- package/dist/spawn.js.map +1 -0
- package/package.json +83 -0
|
@@ -0,0 +1,1560 @@
|
|
|
1
|
+
import { o as NodeTransit, s as NodeTransitObserver } from "./nodeTransit-cmtZgdpW.js";
|
|
2
|
+
import { Capability, ChannelTransport, IMessageTransport, Identity, JsonRpcMessage, JsonValue, LinkRpcConnection, ManagedIdentityStorageBackend, MessageWithCtx, Pattern, Permission, PrincipalId, RequestId, RootPrincipalSet, SignedCapability, SigningIdentity } from "@hediet/linkrpc";
|
|
3
|
+
import { ITransportServer, ServiceId, Transport } from "@hediet/linkrpc/hub/common";
|
|
4
|
+
import { NodeInfo, ParticipantDescriptorSource, TopologyGraph, TopologyIdGenerator, TopologyTransportInfo, TrafficEvent, TrafficSubscription, TrafficSubscriptionOptions, TrafficTransitEvent } from "@hediet/linkrpc/inspection";
|
|
5
|
+
//#region src/hub/server/routing/routingTopology.d.ts
|
|
6
|
+
interface ManagedRoutingTopology {
|
|
7
|
+
readonly node: TopologyGraph['nodes'][number];
|
|
8
|
+
readonly hubLinks: readonly {
|
|
9
|
+
readonly hubPortId: string;
|
|
10
|
+
readonly nodePortId: string;
|
|
11
|
+
readonly label?: string;
|
|
12
|
+
}[];
|
|
13
|
+
readonly peerPortId: string;
|
|
14
|
+
readonly peerLinkLabel?: string;
|
|
15
|
+
readonly peerTransport?: TopologyTransportInfo;
|
|
16
|
+
readonly adjacentNodes?: TopologyGraph['nodes'];
|
|
17
|
+
readonly adjacentLinks?: TopologyGraph['links'];
|
|
18
|
+
}
|
|
19
|
+
interface ManagedTopologyFragment {
|
|
20
|
+
readonly nodes: TopologyGraph['nodes'];
|
|
21
|
+
readonly links?: TopologyGraph['links'];
|
|
22
|
+
readonly routes?: TopologyGraph['routes'];
|
|
23
|
+
}
|
|
24
|
+
//#endregion
|
|
25
|
+
//#region src/hub/server/routing/routingHub.d.ts
|
|
26
|
+
/**
|
|
27
|
+
* Diagnostics sink for a {@link Hub}. Deliberately structurally compatible
|
|
28
|
+
* with VS Code's `LogOutputChannel`, so an extension can pass its log channel
|
|
29
|
+
* straight in without an adapter.
|
|
30
|
+
*
|
|
31
|
+
* The hub only ever logs *routing decisions* — method names, the resolution
|
|
32
|
+
* kind, claimed prefixes and error reasons — never message **payloads**
|
|
33
|
+
* (params/results), so attaching a logger never leaks call arguments.
|
|
34
|
+
*/
|
|
35
|
+
interface IHubLogger {
|
|
36
|
+
trace(message: string): void;
|
|
37
|
+
debug(message: string): void;
|
|
38
|
+
info(message: string): void;
|
|
39
|
+
warn(message: string): void;
|
|
40
|
+
error(message: string): void;
|
|
41
|
+
}
|
|
42
|
+
interface HubOptions {
|
|
43
|
+
/** Name used in error payloads / diagnostics. */
|
|
44
|
+
readonly debugName?: string;
|
|
45
|
+
/** Optional diagnostic descriptor for this routing node. */
|
|
46
|
+
readonly descriptors?: readonly ParticipantDescriptorSource[];
|
|
47
|
+
/** Stable topology identity for this hub. A random id is generated by default. */
|
|
48
|
+
readonly nodeId?: string;
|
|
49
|
+
/** Topology node/port ID generator. Defaults to random UUIDs; never used for security nonces. */
|
|
50
|
+
readonly generateTopologyId?: TopologyIdGenerator;
|
|
51
|
+
/**
|
|
52
|
+
* Optional diagnostics sink. When omitted the hub logs nothing and pays no
|
|
53
|
+
* string-formatting cost (log calls short-circuit on the missing logger).
|
|
54
|
+
*/
|
|
55
|
+
readonly logger?: IHubLogger;
|
|
56
|
+
/**
|
|
57
|
+
* Per-request idle timeout in milliseconds. A forwarded request whose
|
|
58
|
+
* stream is idle for this long (no response, no stream message, no
|
|
59
|
+
* keepalive ping) is cancelled toward its target and failed back to its
|
|
60
|
+
* origin with a `requestTimeout` error. Defaults to 30 minutes. Set to
|
|
61
|
+
* `0` (or a negative value) to disable — only sensible in tests, since
|
|
62
|
+
* it removes the hub's bound on `_pending` growth.
|
|
63
|
+
*/
|
|
64
|
+
readonly idleTimeoutMs?: number;
|
|
65
|
+
/** Positive deadline per automatic peer probe, in ms (default 1000). Failures retry in the background. */
|
|
66
|
+
readonly peerIdentificationTimeoutMs?: number;
|
|
67
|
+
/**
|
|
68
|
+
* Optional initial node-transit observer. Additional observers can be
|
|
69
|
+
* attached dynamically with {@link Hub.observeTransits}. When set, the hub emits a
|
|
70
|
+
* {@link NodeTransit} for every message it routes (requests, responses,
|
|
71
|
+
* notifications, stream frames). Off by default — when omitted the emit
|
|
72
|
+
* sites short-circuit and allocate nothing. Never carries more than the
|
|
73
|
+
* method/params/result the hub already handles.
|
|
74
|
+
*/
|
|
75
|
+
readonly onTransit?: NodeTransitObserver;
|
|
76
|
+
}
|
|
77
|
+
/** Minimal disposable handle; calling `dispose` detaches the link. */
|
|
78
|
+
interface IDisposable {
|
|
79
|
+
dispose(): void;
|
|
80
|
+
}
|
|
81
|
+
/** A snapshot row from {@link Hub.pendingRequests}: one in-flight forward. */
|
|
82
|
+
interface PendingRequestInfo {
|
|
83
|
+
/** Hub-rewritten id the target sees. */
|
|
84
|
+
readonly hubId: string;
|
|
85
|
+
/** Original id the origin used. */
|
|
86
|
+
readonly originalId: RequestId;
|
|
87
|
+
/** Fully-qualified method. */
|
|
88
|
+
readonly method: string;
|
|
89
|
+
/** Edge the request arrived on. */
|
|
90
|
+
readonly originEdgeId: string;
|
|
91
|
+
/** Edge the request was forwarded to. */
|
|
92
|
+
readonly targetEdgeId: string;
|
|
93
|
+
/** Milliseconds the request has been in flight. */
|
|
94
|
+
readonly ageMs: number;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Handle returned by {@link Hub.attach}. Beyond detaching the link, it lets
|
|
98
|
+
* the caller claim/release prefixes *bound to this specific link* without
|
|
99
|
+
* holding a reference to the link itself — the seam a {@link RootOverlay}
|
|
100
|
+
* uses to let its root services claim upstream on the participant's behalf.
|
|
101
|
+
*/
|
|
102
|
+
interface AttachedLink extends IDisposable {
|
|
103
|
+
/** Make this link the hub's root handler. Throws if another link already holds the role. */
|
|
104
|
+
setAsLoopback(): void;
|
|
105
|
+
/** Bind `prefix` to this link (see {@link Hub.claimPrefix}). */
|
|
106
|
+
addPrefixRoute(prefix: ServiceId): void;
|
|
107
|
+
/** Release `prefix` from the table. Returns `true` if it existed. */
|
|
108
|
+
releasePrefix(prefix: ServiceId): boolean;
|
|
109
|
+
/**
|
|
110
|
+
* The stable inspection id the hub uses for this link in its
|
|
111
|
+
* {@link NodeTransit} events. A neighbouring node (e.g. an
|
|
112
|
+
* {@link import('./overlaySplitter').OverlaySplitter}) should label the
|
|
113
|
+
* *same physical edge* with this id so a {@link TransitAggregator} chains
|
|
114
|
+
* the two nodes' transits across the boundary.
|
|
115
|
+
*/
|
|
116
|
+
readonly edgeId: string;
|
|
117
|
+
/** Stable topology port identity, independent of the mutable friendly edge label. */
|
|
118
|
+
readonly portId: string;
|
|
119
|
+
/** Await or retry peer discovery. Discovery also runs automatically after attachment. */
|
|
120
|
+
identifyPeer(): Promise<NodeInfo>;
|
|
121
|
+
/** Send a bounded root request to the far end while the Hub owns dispatch. */
|
|
122
|
+
request(method: string, params: JsonValue, timeoutMs?: number): Promise<JsonValue | undefined>;
|
|
123
|
+
}
|
|
124
|
+
interface AttachLinkOptions {
|
|
125
|
+
readonly edgeId?: string;
|
|
126
|
+
readonly transport?: TopologyTransportInfo;
|
|
127
|
+
/** Make this link the sole hub-root handler. Throws if another link already holds the role. */
|
|
128
|
+
readonly exclusiveHubRootHandler?: boolean;
|
|
129
|
+
/** Initial prefix routes. All must be valid, distinct, and unclaimed. */
|
|
130
|
+
readonly routePrefixes?: readonly ServiceId[];
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* A **Hub** is the entire routing concept of linkrpc v2 — the only noun.
|
|
134
|
+
* It is a tiny message router with an unauthenticated topology-correlation
|
|
135
|
+
* identity and exactly three moving
|
|
136
|
+
* parts, all expressed as {@link IMessageTransport} links:
|
|
137
|
+
*
|
|
138
|
+
* - **forwarding table** — longest-prefix serviceId → link map (the
|
|
139
|
+
* downstream neighbours / claimed subnets).
|
|
140
|
+
* - **loopback** — an optional link to the local root services (`bare`/
|
|
141
|
+
* `interface`-form calls land here and *never* leave the hub). This is
|
|
142
|
+
* the routing equivalent of `127.0.0.1`.
|
|
143
|
+
* - **uplink** — an optional default route. A hub *with* an uplink is also
|
|
144
|
+
* a participant of its parent hub, which is how hubs **nest** (a VS Code
|
|
145
|
+
* window hub uplinks to an OS hub uplinks to a home-server hub).
|
|
146
|
+
*
|
|
147
|
+
* Routing is pure longest-prefix forwarding with JSON-RPC id rewriting:
|
|
148
|
+
* a forwarded request keeps its full method name verbatim; only its `id`
|
|
149
|
+
* is swapped for a hub-local one so responses can be demultiplexed back to
|
|
150
|
+
* the originating link. There is no notion of identity, capabilities, or
|
|
151
|
+
* trust at this layer — those are policies layered on top.
|
|
152
|
+
*
|
|
153
|
+
* The single privileged write seam is {@link claimPrefix}; everything else
|
|
154
|
+
* is mechanical message shuffling.
|
|
155
|
+
*/
|
|
156
|
+
declare class Hub {
|
|
157
|
+
private readonly _options;
|
|
158
|
+
/** Unauthenticated topology-correlation identity for this hub. */
|
|
159
|
+
readonly nodeId: string;
|
|
160
|
+
private readonly _table;
|
|
161
|
+
private readonly _links;
|
|
162
|
+
private readonly _pending;
|
|
163
|
+
private _uplink;
|
|
164
|
+
private _loopback;
|
|
165
|
+
private _nextId;
|
|
166
|
+
private readonly _log;
|
|
167
|
+
private readonly _transitObservers;
|
|
168
|
+
/**
|
|
169
|
+
* Listeners notified whenever the forwarding table changes (a prefix is
|
|
170
|
+
* claimed or released, or a link detaches dropping its claims). Coarse, by
|
|
171
|
+
* design: it carries no payload — consumers re-query the directory. This is
|
|
172
|
+
* the "poll now" nudge that backs `hubrpc.directory::watch`.
|
|
173
|
+
*/
|
|
174
|
+
private readonly _routingListeners;
|
|
175
|
+
private readonly _topologyListeners;
|
|
176
|
+
/** Stable inspection label per link; assigned lazily / by prefix claim. */
|
|
177
|
+
private readonly _edgeIds;
|
|
178
|
+
private readonly _transportInfo;
|
|
179
|
+
private _nextEdgeId;
|
|
180
|
+
private readonly _topology;
|
|
181
|
+
private readonly _participantRoots;
|
|
182
|
+
/** Per-request idle timeout in ms; `<= 0` disables. See {@link HubOptions.idleTimeoutMs}. */
|
|
183
|
+
private readonly _idleMs;
|
|
184
|
+
constructor(_options?: HubOptions);
|
|
185
|
+
/** Prefix for log lines; identifies this hub when nested. */
|
|
186
|
+
private _tag;
|
|
187
|
+
/** Stable inspection label for a link; auto-assigned on first use. */
|
|
188
|
+
private _edgeId;
|
|
189
|
+
/** Give `link` a friendly inspection label (prefix / loopback / uplink). */
|
|
190
|
+
private _labelEdge;
|
|
191
|
+
/** Emit a pre-built node transit. Callers guard construction by observer count. */
|
|
192
|
+
private _emit;
|
|
193
|
+
private _transitEndpoint;
|
|
194
|
+
/** Number of live transit observers. */
|
|
195
|
+
get transitObserverCount(): number;
|
|
196
|
+
/** Observe routed messages until the returned handle is disposed. */
|
|
197
|
+
observeTransits(observer: NodeTransitObserver): IDisposable;
|
|
198
|
+
/**
|
|
199
|
+
* Publish a transit from a routing node managed by this Hub. Callers should
|
|
200
|
+
* check {@link transitObserverCount} before constructing the transit.
|
|
201
|
+
*/
|
|
202
|
+
publishManagedTransit(transit: NodeTransit): void;
|
|
203
|
+
/** Replace an attached link's direct topology edge with a managed routing node. */
|
|
204
|
+
registerManagedRoutingTopology(attached: AttachedLink, topology: ManagedRoutingTopology): IDisposable;
|
|
205
|
+
/** Add an in-process endpoint or routing fragment to this Hub's topology. */
|
|
206
|
+
registerManagedTopologyFragment(fragment: ManagedTopologyFragment): IDisposable;
|
|
207
|
+
/**
|
|
208
|
+
* Subscribe to coarse routing-table changes (prefix claim/release/detach).
|
|
209
|
+
* The listener receives no arguments — it is a "something changed; re-query"
|
|
210
|
+
* nudge. Returns an unsubscribe function.
|
|
211
|
+
*/
|
|
212
|
+
onDidChangeRouting(listener: () => void): () => void;
|
|
213
|
+
/** Subscribe to topology changes, including routing and background peer discovery. */
|
|
214
|
+
onDidChangeTopology(listener: () => void): () => void;
|
|
215
|
+
/** Number of active routing invalidation observers. */
|
|
216
|
+
get routingObserverCount(): number;
|
|
217
|
+
get topologyObserverCount(): number;
|
|
218
|
+
/** Fire all routing-change listeners. Best-effort; one throwing listener does not block the rest. */
|
|
219
|
+
private _fireRoutingChanged;
|
|
220
|
+
private _fireTopologyChanged;
|
|
221
|
+
private _notifyListeners;
|
|
222
|
+
/** Emit a stream transit with the owning request's correlated endpoints. */
|
|
223
|
+
private _emitStream;
|
|
224
|
+
/**
|
|
225
|
+
* Snapshot the requests currently in flight through this hub. Each row is
|
|
226
|
+
* one {@link _pending} entry — the authoritative in-flight set — annotated
|
|
227
|
+
* with its method, the edges it bridges, and how long it has waited. Useful
|
|
228
|
+
* for surfacing stuck/slow requests in an inspector.
|
|
229
|
+
*/
|
|
230
|
+
pendingRequests(): PendingRequestInfo[];
|
|
231
|
+
/**
|
|
232
|
+
* Begin routing the messages of `link`. The hub installs itself as the
|
|
233
|
+
* link's listener; do not also attach another listener. Returns a
|
|
234
|
+
* disposable that stops routing the link (detaching the listener and
|
|
235
|
+
* dropping any forwarding entries, uplink/loopback slots, and pending
|
|
236
|
+
* responses bound to it) when disposed. Peer discovery starts in the
|
|
237
|
+
* background; unresolved or unsupported peers never block routing.
|
|
238
|
+
*/
|
|
239
|
+
attach(link: IMessageTransport, opts?: AttachLinkOptions): AttachedLink;
|
|
240
|
+
/**
|
|
241
|
+
* Attach a fresh in-memory link and hand back its far end alongside the
|
|
242
|
+
* {@link AttachedLink} handle. Creates a {@link TransportPair} internally,
|
|
243
|
+
* {@link attach}es the near side to the hub, and returns the attach handle
|
|
244
|
+
* augmented with `transport` — the far side for a peer (a consumer
|
|
245
|
+
* connection, an in-process participant, …) to drive. Equivalent to
|
|
246
|
+
* `const p = new TransportPair(); const link = hub.attach(p.a);
|
|
247
|
+
* return { ...link, transport: p.b };`. Disposing the returned link also
|
|
248
|
+
* disposes `transport`.
|
|
249
|
+
*/
|
|
250
|
+
attachOut(opts?: AttachLinkOptions): AttachedLink & {
|
|
251
|
+
readonly transport: IMessageTransport;
|
|
252
|
+
};
|
|
253
|
+
private _detach;
|
|
254
|
+
/** Set (or clear) the default route. Automatically attaches the link. */
|
|
255
|
+
setUplink(link: IMessageTransport | undefined): void;
|
|
256
|
+
/** Set (or clear) the root handler. Clear the existing handler before assigning a different link. */
|
|
257
|
+
setLoopback(link: IMessageTransport | undefined): void;
|
|
258
|
+
/**
|
|
259
|
+
* Bind a serviceId `prefix` to `link`. Like initial attachment routes,
|
|
260
|
+
* this is a privileged operation; higher layers enforce who may claim.
|
|
261
|
+
*
|
|
262
|
+
* Claim-once: throws if the prefix is malformed or already owned.
|
|
263
|
+
*/
|
|
264
|
+
claimPrefix(link: IMessageTransport, prefix: ServiceId): void;
|
|
265
|
+
private _validatePrefixClaim;
|
|
266
|
+
private _claimPrefix;
|
|
267
|
+
releasePrefix(prefix: ServiceId): boolean;
|
|
268
|
+
/** Snapshot of currently-claimed prefixes. */
|
|
269
|
+
claimedPrefixes(): ServiceId[];
|
|
270
|
+
/**
|
|
271
|
+
* Trust the inspection interfaces mounted for `serviceId` on this attached
|
|
272
|
+
* in-process link. Only exact reserved inspection interface calls are hidden.
|
|
273
|
+
*/
|
|
274
|
+
markInspectionService(attached: AttachedLink, serviceId: ServiceId): void;
|
|
275
|
+
/** Build a topology snapshot on demand for the addressed observer service. */
|
|
276
|
+
getTopologyGraph(observerServiceId: string): TopologyGraph;
|
|
277
|
+
/**
|
|
278
|
+
* Fan a **root-form** request out to every distinct downstream participant
|
|
279
|
+
* link (the claimed-prefix owners), skipping the link that owns
|
|
280
|
+
* `excludePrefix` (typically the hub's own services prefix) and the uplink.
|
|
281
|
+
* Each participant is queried **once** even if it owns several prefixes.
|
|
282
|
+
* Failures (unreachable participant, timeout, no root handler) are dropped,
|
|
283
|
+
* so the result holds one entry per link that answered.
|
|
284
|
+
*
|
|
285
|
+
* The request is delivered to each participant's **root** services — on an
|
|
286
|
+
* overlay uplink the splitter routes a root-form method to the participant
|
|
287
|
+
* (`H·root → P`), which is never forwarded and never gated. That is what
|
|
288
|
+
* lets the hub's global directory gather participant self-listings without
|
|
289
|
+
* signing or capabilities.
|
|
290
|
+
*/
|
|
291
|
+
queryParticipantRoots(method: string, params: JsonValue, excludePrefix?: ServiceId): Promise<JsonValue[]>;
|
|
292
|
+
/**
|
|
293
|
+
* Keep a root-form streaming request open on every current participant.
|
|
294
|
+
* Routing changes reconcile the participant set; application stream payloads
|
|
295
|
+
* are collapsed into a coarse callback so callers can re-query.
|
|
296
|
+
*/
|
|
297
|
+
watchParticipantRoots(method: string, params: JsonValue, onTick: () => void, excludePrefix?: ServiceId): IDisposable;
|
|
298
|
+
/**
|
|
299
|
+
* Send a single request down `link` and resolve with its result — the
|
|
300
|
+
* hub-originated analogue of {@link _routeRequest}. It allocates a hub id,
|
|
301
|
+
* parks a {@link _pending} entry whose `origin` is a tiny in-memory sink
|
|
302
|
+
* that settles this promise on the matching response, and writes the
|
|
303
|
+
* request to `link`. It therefore reuses the full demux / idle-timeout /
|
|
304
|
+
* detach machinery: a response routes back through {@link _demux} into the
|
|
305
|
+
* sink, an idle target fails it via {@link _onIdleTimeout}, and a detaching
|
|
306
|
+
* target fails it via {@link _detach}.
|
|
307
|
+
*/
|
|
308
|
+
private _requestOnLink;
|
|
309
|
+
private _watchOnLink;
|
|
310
|
+
/**
|
|
311
|
+
* The link a still-in-flight forwarded request originally arrived on,
|
|
312
|
+
* keyed by the **hub-rewritten** id the request now carries (i.e. the
|
|
313
|
+
* `id` a target sees on its delivered message). Valid from the moment the
|
|
314
|
+
* hub forwards the request until its response is demultiplexed; returns
|
|
315
|
+
* `undefined` for unknown / already-answered ids.
|
|
316
|
+
*
|
|
317
|
+
* This is the seam a hub-hosted participant uses to learn *which* link a
|
|
318
|
+
* call it is handling came from, so it can bind a prefix claim to that
|
|
319
|
+
* exact link via {@link claimPrefix} without the routing core ever
|
|
320
|
+
* growing a notion of authenticated identity.
|
|
321
|
+
*/
|
|
322
|
+
getSourceTransport(requestId: RequestId): IMessageTransport | undefined;
|
|
323
|
+
private _onMessage;
|
|
324
|
+
private _resolve;
|
|
325
|
+
private _routeRequest;
|
|
326
|
+
private _routeNotification;
|
|
327
|
+
private _demux;
|
|
328
|
+
/**
|
|
329
|
+
* Route a `$stream::send` notification. Unlike an ordinary notification, a
|
|
330
|
+
* stream message is **correlated to an in-flight request by `requestId`**,
|
|
331
|
+
* not addressed by method name — so it follows the same path the request
|
|
332
|
+
* established (recorded in {@link _pending}), exactly like a
|
|
333
|
+
* {@link _demux | response}, and the hub rewrites `requestId` across the
|
|
334
|
+
* forwarding boundary the same way it rewrites a response `id`.
|
|
335
|
+
*
|
|
336
|
+
* The message's explicit `dir` picks the traversal — no inference from the
|
|
337
|
+
* arriving link:
|
|
338
|
+
*
|
|
339
|
+
* - **`toCaller`** (callee → caller progress): routed like a response. The
|
|
340
|
+
* streamer used the hub-rewritten id it *received*, so `requestId` is a
|
|
341
|
+
* pending hub id; only the link the request was forwarded to may stream
|
|
342
|
+
* on it. Restore the origin's original id and forward to the origin.
|
|
343
|
+
* - **`toCallee`** (caller → callee input / cancel / ping): the reverse
|
|
344
|
+
* traversal of the same entry. The streamer used the original id it
|
|
345
|
+
* *sent*; rewrite `requestId` to the hub id the target was handed and
|
|
346
|
+
* forward to the target. Only the origin may stream this direction.
|
|
347
|
+
*
|
|
348
|
+
* Every routed stream message resets the request's idle timer (a keepalive
|
|
349
|
+
* ping is exactly a `toCallee` control with no payload). A `$stream::send`
|
|
350
|
+
* matching no in-flight request is dropped (late / unknown correlation).
|
|
351
|
+
*/
|
|
352
|
+
private _routeStream;
|
|
353
|
+
/** Delete a pending entry and clear its idle timer. */
|
|
354
|
+
private _deletePending;
|
|
355
|
+
/** Arm (or re-arm) the idle timer for a pending entry. No-op when disabled. */
|
|
356
|
+
private _armIdle;
|
|
357
|
+
/** Reset the idle timer on stream activity. */
|
|
358
|
+
private _resetIdle;
|
|
359
|
+
/**
|
|
360
|
+
* A request went idle past {@link _idleMs}. Cancel the (presumably hung)
|
|
361
|
+
* callee, fail the caller with a `requestTimeout`, and drop the entry —
|
|
362
|
+
* the bound that keeps `_pending` from growing without limit.
|
|
363
|
+
*/
|
|
364
|
+
private _onIdleTimeout;
|
|
365
|
+
/**
|
|
366
|
+
* Author a `toCallee` cancel toward `target` for the request the target
|
|
367
|
+
* knows as `hubId`. Used on caller-disconnect and idle-timeout — the hub
|
|
368
|
+
* itself originates the message, which is why `dir` is explicit on the
|
|
369
|
+
* wire (there is no inbound link to infer it from).
|
|
370
|
+
*/
|
|
371
|
+
private _injectCancel;
|
|
372
|
+
private _noTargetCode;
|
|
373
|
+
private _noTargetMessage;
|
|
374
|
+
private _replyError;
|
|
375
|
+
}
|
|
376
|
+
//#endregion
|
|
377
|
+
//#region src/hub/server/hubInspector.d.ts
|
|
378
|
+
interface HubTrafficWatchOptions extends TrafficSubscriptionOptions {}
|
|
379
|
+
interface HubTrafficSubscription extends IDisposable, TrafficSubscription {}
|
|
380
|
+
interface HubTrafficSource {
|
|
381
|
+
observe(observer: (transit: TrafficTransitEvent) => void): IDisposable;
|
|
382
|
+
}
|
|
383
|
+
/** Publishes dynamically observed raw node transits through bounded traffic streams. */
|
|
384
|
+
declare class HubInspector implements IDisposable {
|
|
385
|
+
private readonly _hub;
|
|
386
|
+
private readonly _subscribers;
|
|
387
|
+
private readonly _trafficSources;
|
|
388
|
+
private readonly _trafficWatches;
|
|
389
|
+
private readonly _observation;
|
|
390
|
+
constructor(_hub: Hub);
|
|
391
|
+
get observerCount(): number;
|
|
392
|
+
subscribe(options: HubTrafficWatchOptions, send: (event: TrafficEvent) => Promise<void>): HubTrafficSubscription;
|
|
393
|
+
dispose(): void;
|
|
394
|
+
/** Add an endpoint traffic source managed by this Hub inspection service. */
|
|
395
|
+
addTrafficSource(source: HubTrafficSource): IDisposable;
|
|
396
|
+
private _startTrafficSources;
|
|
397
|
+
private _startTrafficSource;
|
|
398
|
+
private _stopTrafficSources;
|
|
399
|
+
private _onTransit;
|
|
400
|
+
private _emitTransit;
|
|
401
|
+
}
|
|
402
|
+
//#endregion
|
|
403
|
+
//#region src/hub/server/routing/overlaySplitter.d.ts
|
|
404
|
+
/**
|
|
405
|
+
* Inspection wiring for an {@link OverlaySplitter}. When supplied, the splitter
|
|
406
|
+
* emits a {@link NodeTransit} for every message it routes. The three port edge
|
|
407
|
+
* ids name the splitter's own edges; **`edges.h` must equal the parent hub
|
|
408
|
+
* link's `edgeId`** so a {@link TransitAggregator} chains the splitter's and
|
|
409
|
+
* hub's transits across the shared boundary (the splitter forwards request ids
|
|
410
|
+
* verbatim there, so the `(edgeId, requestId)` endpoints line up).
|
|
411
|
+
*/
|
|
412
|
+
interface OverlaySplitterInspection {
|
|
413
|
+
readonly nodeId: string;
|
|
414
|
+
readonly onTransit: NodeTransitObserver;
|
|
415
|
+
readonly edges: {
|
|
416
|
+
readonly p: string;
|
|
417
|
+
readonly c: string;
|
|
418
|
+
readonly h: string;
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* A fixed three-port message splitter — the per-participant primitive that
|
|
423
|
+
* replaces a nested {@link import('./routingHub').Hub} inside a
|
|
424
|
+
* {@link import('./rootOverlay').RootOverlay}.
|
|
425
|
+
*
|
|
426
|
+
* Ports:
|
|
427
|
+
* - **P** — the participant (downstream connection),
|
|
428
|
+
* - **C** — the local root-services connection (`hubServiceIdRegistry::registerServiceId`,
|
|
429
|
+
* `hubrpc.directory`, `identity::*`, …),
|
|
430
|
+
* - **H** — the uplink to the parent hub.
|
|
431
|
+
*
|
|
432
|
+
* Routing rules (verbatim to the design):
|
|
433
|
+
* ```
|
|
434
|
+
* P·root → C P·* → H
|
|
435
|
+
* H·* → P H·root → P
|
|
436
|
+
* C·* → P
|
|
437
|
+
* ```
|
|
438
|
+
* where `root` means a bare/interface-form method and `*` a fully-qualified
|
|
439
|
+
* (`serviceId::…`) method. Responses follow the reverse path. `H·root → P`
|
|
440
|
+
* lets the parent hub reach the participant's *own* root services (e.g. the
|
|
441
|
+
* participant-served root directory) — distinct from `P·root → C`, which is
|
|
442
|
+
* the participant *consuming* the overlay's root services; the two are
|
|
443
|
+
* opposite directions served by different connections, so they never collide.
|
|
444
|
+
*
|
|
445
|
+
* ### `$stream::send` — correlated by `requestId`, not by method form
|
|
446
|
+
*
|
|
447
|
+
* A {@link STREAM_METHOD} (`$stream::send`) notification is *interface-form*
|
|
448
|
+
* (`$stream::send`), so the method-form rules above would naively treat it as
|
|
449
|
+
* `root` and send a participant's stream to **C** (and route an inbound one as
|
|
450
|
+
* a root call to **P**). That is wrong: a stream message belongs to an in-flight
|
|
451
|
+
* request's lifetime and must follow **that request's** path, exactly like a
|
|
452
|
+
* response does. So `$stream::send` bypasses the method-form table and is
|
|
453
|
+
* routed like a response instead:
|
|
454
|
+
*
|
|
455
|
+
* - **P → ?**: the participant streams using the `requestId` it *observed*,
|
|
456
|
+
* which for a request it serves is the tag the splitter stamped on the
|
|
457
|
+
* delivered request id (`H\0…` / `C\0…`). We {@link _decodeId | decode} that
|
|
458
|
+
* tag, restore the original id, and forward to the tagged origin (H or C) —
|
|
459
|
+
* the same demux responses get. An **untagged** `requestId` means the
|
|
460
|
+
* participant *initiated* the request (P→H or P→C); we cannot tell which
|
|
461
|
+
* statelessly, so we default to the **uplink** (the common case: streaming
|
|
462
|
+
* to a fully-qualified service via the parent).
|
|
463
|
+
* - **H/C → P**: forwarded verbatim to the participant (never dropped), as a
|
|
464
|
+
* response would be — the parent has already restored P's original
|
|
465
|
+
* `requestId`, symmetric to response-id rewriting.
|
|
466
|
+
*
|
|
467
|
+
* PARTICIPANT-INITIATED STREAMS — the one stateful case: because `requestId`
|
|
468
|
+
* lives in `params` (not a structural top-level id the splitter rewrites in
|
|
469
|
+
* lock-step), a stream frame for a request the participant *initiated* (`P→C`
|
|
470
|
+
* or `P→H`) carries P's *own untagged* id, which alone cannot say whether the
|
|
471
|
+
* call went to the root (C) or the uplink (H). The splitter therefore keeps a
|
|
472
|
+
* small `requestId → target` map ({@link _pInitiated}), populated when P sends
|
|
473
|
+
* such a request and cleared when its response returns, and consults it to
|
|
474
|
+
* route those frames. This is the splitter's *only* per-request state; every
|
|
475
|
+
* other path still routes statelessly via id-tagging.
|
|
476
|
+
*
|
|
477
|
+
* ### Stateless demux via id-tagging
|
|
478
|
+
*
|
|
479
|
+
* P is the only port that receives requests from *two* sources (H and C),
|
|
480
|
+
* so a response from P is ambiguous and ids can collide (`H` and `C` may
|
|
481
|
+
* each send `id:5`). Rather than a pending-request map (with its attendant
|
|
482
|
+
* timeout/leak problem), the splitter rewrites **only requests destined for
|
|
483
|
+
* P**, prepending the origin and original id type to the id:
|
|
484
|
+
*
|
|
485
|
+
* ```
|
|
486
|
+
* H→P: id' = "H\0n\042" (push origin tag)
|
|
487
|
+
* C→P: id' = "C\0s\0abc"
|
|
488
|
+
* P→?: decode id' → route to H or C, restore the original id
|
|
489
|
+
* ```
|
|
490
|
+
*
|
|
491
|
+
* Every other edge (P→H, P→C and the responses coming back from H/C to P)
|
|
492
|
+
* passes **verbatim** — those targets each have a single peer, so there is no
|
|
493
|
+
* response ambiguity. The sole exception is *stream* frames for
|
|
494
|
+
* participant-initiated requests, whose target is recovered from the small
|
|
495
|
+
* {@link _pInitiated} map (see above); apart from that the id encodes its own
|
|
496
|
+
* return path.
|
|
497
|
+
*
|
|
498
|
+
* NOTE: unlike the central {@link import('./routingHub').Hub}, the tag is
|
|
499
|
+
* not authenticated. A misbehaving participant could emit a forged tagged
|
|
500
|
+
* response toward H or C; each endpoint still drops ids it has no pending
|
|
501
|
+
* request for, and identity/signing live in a higher layer. For the
|
|
502
|
+
* single-participant overlay this is an acceptable trade for being fully
|
|
503
|
+
* stateless. If unforgeable return paths are ever required here, HMAC the
|
|
504
|
+
* tag with a splitter-private secret.
|
|
505
|
+
*/
|
|
506
|
+
declare class OverlaySplitter<TContext = undefined> {
|
|
507
|
+
private readonly _participant;
|
|
508
|
+
private readonly _root;
|
|
509
|
+
private readonly _uplink;
|
|
510
|
+
private _disposed;
|
|
511
|
+
private readonly _inspect;
|
|
512
|
+
/**
|
|
513
|
+
* Per-request routing target (`C` or `H`) for requests the participant
|
|
514
|
+
* *initiated*, keyed by P's own request id. Streams for those requests
|
|
515
|
+
* carry P's untagged id, which alone can't say whether the call went to the
|
|
516
|
+
* root (C) or the uplink (H); this map recovers it. Populated when P sends
|
|
517
|
+
* the request, cleared when its response returns. The splitter's only
|
|
518
|
+
* per-request state — every other path routes statelessly via id-tagging.
|
|
519
|
+
*/
|
|
520
|
+
private readonly _pInitiated;
|
|
521
|
+
constructor(_participant: IMessageTransport<MessageWithCtx<TContext>>, _root: IMessageTransport<JsonRpcMessage, MessageWithCtx<TContext>>, _uplink: IMessageTransport, inspection?: OverlaySplitterInspection);
|
|
522
|
+
/** Detach all three ports. Does not dispose the transports themselves. */
|
|
523
|
+
dispose(): void;
|
|
524
|
+
/** Edge id of an upstream origin port. */
|
|
525
|
+
private _originEdge;
|
|
526
|
+
/** Emit a node transit if inspection is enabled. Zero-cost otherwise. */
|
|
527
|
+
private _emit;
|
|
528
|
+
private _onFromParticipant;
|
|
529
|
+
/**
|
|
530
|
+
* Route a participant-emitted `$stream::send` by its `requestId` tag,
|
|
531
|
+
* mirroring how a {@link _onFromParticipant | response} is demuxed. The
|
|
532
|
+
* participant streams with the id it *observed*; for a request it serves
|
|
533
|
+
* that id carries the origin tag the splitter stamped, so decoding it
|
|
534
|
+
* yields the return port and the original id. Untagged → a request the
|
|
535
|
+
* participant *initiated*; recover its target (C or H) from {@link
|
|
536
|
+
* _pInitiated}, defaulting to the uplink.
|
|
537
|
+
*/
|
|
538
|
+
private _routeParticipantStream;
|
|
539
|
+
private _onFromUpstream;
|
|
540
|
+
/** Build a request/notification transit (`out` absent ⇒ dropped here). */
|
|
541
|
+
private _mkForward;
|
|
542
|
+
/** Build a response transit crossing from `inEdge`/`inId` to `outEdge`/`outId`. */
|
|
543
|
+
private _mkResponse;
|
|
544
|
+
}
|
|
545
|
+
//#endregion
|
|
546
|
+
//#region src/hub/server/routing/rootOverlay.d.ts
|
|
547
|
+
interface RootOverlayOptions {
|
|
548
|
+
/**
|
|
549
|
+
* The uplink transport to the parent hub. The overlay forwards all
|
|
550
|
+
* prefixed (`serviceId::…`) participant traffic here and receives the
|
|
551
|
+
* parent's routed traffic back. Obtain it from `parentHub.attach(pair.b)`
|
|
552
|
+
* and pass `pair.a`.
|
|
553
|
+
*/
|
|
554
|
+
readonly uplink: IMessageTransport;
|
|
555
|
+
/**
|
|
556
|
+
* Optional inspection wiring forwarded to the underlying
|
|
557
|
+
* {@link OverlaySplitter}. Set `edges.h` to the parent hub link's `edgeId`
|
|
558
|
+
* (from `parentHub.attach(...).edgeId`) so transits chain across the
|
|
559
|
+
* boundary.
|
|
560
|
+
*/
|
|
561
|
+
readonly inspection?: OverlaySplitterInspection;
|
|
562
|
+
/** Parent-facing topology port. Defaults to a generated connection-lifetime id. */
|
|
563
|
+
readonly uplinkPortId?: string;
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* A **RootOverlay** is the per-participant front door. It is a pure
|
|
567
|
+
* {@link OverlaySplitter} wiring with no routing table, no nested hub, and —
|
|
568
|
+
* deliberately — **no knowledge of which services it serves**. It exposes:
|
|
569
|
+
*
|
|
570
|
+
* - {@link root} — the connection serving this participant's root services.
|
|
571
|
+
* Install bootstrap/identity modules onto it with `registerHubServices` /
|
|
572
|
+
* `registerIdentityServices` *before or after* connecting the participant.
|
|
573
|
+
* - {@link connectParticipant} — wires the downstream participant through the
|
|
574
|
+
* splitter (`P·root → root`, `P·* → uplink`, `uplink·* → P`).
|
|
575
|
+
*
|
|
576
|
+
* Because an overlay represents exactly one participant, prefix claims happen
|
|
577
|
+
* only on the parent (via the {@link import('./routingHub').AttachedLink} that
|
|
578
|
+
* `registerHubServices` was given); there is no local table to write.
|
|
579
|
+
*
|
|
580
|
+
* The overlay is generic over the per-call `TContext` its {@link root}
|
|
581
|
+
* connection sees. Pass a context-stamping {@link ChannelTransport} to
|
|
582
|
+
* {@link connectParticipant} to attach per-call context. Most callers connect a
|
|
583
|
+
* plain (or signature-verifying) transport and leave `TContext` as `undefined`
|
|
584
|
+
* (context-free).
|
|
585
|
+
*/
|
|
586
|
+
declare class RootOverlay<TContext = undefined> {
|
|
587
|
+
/**
|
|
588
|
+
* The per-participant root connection. Register the overlay's root
|
|
589
|
+
* services here — e.g. `registerHubServices(overlay.root, upstream)` and
|
|
590
|
+
* `registerIdentityServices(overlay.root, { resolveIdentity })`. Reachable
|
|
591
|
+
* only by this overlay's participant (root-addressed calls land here).
|
|
592
|
+
*
|
|
593
|
+
* Its inbound transport carries the overlay's `TContext`, so identity-aware
|
|
594
|
+
* front doors — notably the consent `hubAccess` service — can be served here
|
|
595
|
+
* for the interface-form calls a participant addresses to its own root.
|
|
596
|
+
*/
|
|
597
|
+
readonly root: LinkRpcConnection<TContext>;
|
|
598
|
+
readonly nodeId: string;
|
|
599
|
+
readonly participantPortId: string;
|
|
600
|
+
readonly rootPortId: string;
|
|
601
|
+
readonly rootNodeId: string;
|
|
602
|
+
readonly uplinkPortId: string;
|
|
603
|
+
private readonly _uplink;
|
|
604
|
+
private readonly _rootPair;
|
|
605
|
+
private readonly _inspection;
|
|
606
|
+
private _splitter;
|
|
607
|
+
private _disposed;
|
|
608
|
+
constructor(options: RootOverlayOptions);
|
|
609
|
+
managedTopology(edgeLabel?: string, peerTransport?: TopologyTransportInfo): ManagedRoutingTopology;
|
|
610
|
+
/**
|
|
611
|
+
* Connect the downstream participant. Root-addressed calls from it hit the
|
|
612
|
+
* {@link root} services; prefixed calls forward via the uplink. May be
|
|
613
|
+
* called once.
|
|
614
|
+
*
|
|
615
|
+
* Connect a context-stamping {@link ChannelTransport} when `TContext` is
|
|
616
|
+
* concrete; otherwise connect the raw participant transport directly.
|
|
617
|
+
*/
|
|
618
|
+
connectParticipant(participant: IMessageTransport<MessageWithCtx<TContext>>): void;
|
|
619
|
+
/**
|
|
620
|
+
* Tear the overlay down: detach the splitter and dispose the internal root
|
|
621
|
+
* transport pair. The caller owns the uplink and the participant transport
|
|
622
|
+
* and disposes them separately (disposing the uplink's
|
|
623
|
+
* {@link import('./routingHub').AttachedLink} releases any claimed prefixes
|
|
624
|
+
* on the parent). Idempotent.
|
|
625
|
+
*/
|
|
626
|
+
dispose(): void;
|
|
627
|
+
}
|
|
628
|
+
//#endregion
|
|
629
|
+
//#region src/hub/server/rootServices.d.ts
|
|
630
|
+
interface RegisterHubServicesOptions {
|
|
631
|
+
/**
|
|
632
|
+
* ServiceId under which the parent serves its global reflection services.
|
|
633
|
+
* The overlay's directory listing *refers* world-view queries here.
|
|
634
|
+
* Defaults to `'hub'`.
|
|
635
|
+
*/
|
|
636
|
+
readonly hubServiceId?: string;
|
|
637
|
+
/**
|
|
638
|
+
* Absolute serviceId region this connection may claim freely (anything
|
|
639
|
+
* at/under it, no capability needed), surfaced verbatim through
|
|
640
|
+
* `hubGrantedServiceId::get` as `grantedServiceIdNamespace`. Typically the
|
|
641
|
+
* connection's attested identity key. Defaults to `''` — "claim nothing
|
|
642
|
+
* freely" — which still lets `authorizeClaim` admit claims.
|
|
643
|
+
*/
|
|
644
|
+
readonly grantedServiceIdNamespace?: string;
|
|
645
|
+
/**
|
|
646
|
+
* Consulted in `hubGrantedServiceId::register` before the prefix is
|
|
647
|
+
* claimed. Return `{ ok: false, reason }` to deny (surfaced to the
|
|
648
|
+
* participant as an error). When omitted, claims default to "must be within
|
|
649
|
+
* `grantedServiceIdNamespace`".
|
|
650
|
+
*/
|
|
651
|
+
authorizeClaim?(requestedPrefix: ServiceId): {
|
|
652
|
+
ok: true;
|
|
653
|
+
} | {
|
|
654
|
+
ok: false;
|
|
655
|
+
reason: string;
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* Install a participant's **connection root services** onto its overlay root
|
|
660
|
+
* connection `root`:
|
|
661
|
+
*
|
|
662
|
+
* - `hubGrantedServiceId::get` — the connection round-trip: reports the
|
|
663
|
+
* `grantedServiceIdNamespace` this connection may claim freely.
|
|
664
|
+
* - `hubGrantedServiceId::register` — claims a prefix on
|
|
665
|
+
* `upstream` (the parent hub link) within the connection's granted namespace,
|
|
666
|
+
* gated by `authorizeClaim` (default: namespace membership). Because an
|
|
667
|
+
* overlay represents a single participant, *one* upstream claim suffices: all
|
|
668
|
+
* prefixed traffic the parent routes to this overlay is for this participant,
|
|
669
|
+
* so the {@link OverlaySplitter} delivers it without any local table.
|
|
670
|
+
* - `hubrpc.directory::list` — by default a **referral** (own root interfaces
|
|
671
|
+
* plus one row pointing at `<hubServiceId>::hubrpc.directory`); an
|
|
672
|
+
* implementation is free to aggregate the parent's listing instead.
|
|
673
|
+
* - `hubrpc.schemas::get` — schemas for this overlay's own root interfaces.
|
|
674
|
+
*
|
|
675
|
+
* The hub's consent front door (`hubAccess::*`) is installed separately at the
|
|
676
|
+
* overlay root by {@link registerHubAccessService}; it needs no capability of
|
|
677
|
+
* its own because the root is never forwarded.
|
|
678
|
+
*
|
|
679
|
+
* The overlay itself stays unaware of which services it serves; this is the
|
|
680
|
+
* routing module that gives it its front door.
|
|
681
|
+
*/
|
|
682
|
+
declare function registerHubServices(root: LinkRpcConnection<unknown>, upstream: AttachedLink, options?: RegisterHubServicesOptions): void;
|
|
683
|
+
interface RegisterIdentityServicesOptions {
|
|
684
|
+
/**
|
|
685
|
+
* Resolve the managed identity for this participant. Called **lazily** —
|
|
686
|
+
* only when the participant first invokes `identity::*` — so registering
|
|
687
|
+
* the service does not mint or load any key material (matches the consent
|
|
688
|
+
* model: serving `identity::*` costs nothing until the participant opts in).
|
|
689
|
+
*/
|
|
690
|
+
resolveIdentity(): Promise<Identity>;
|
|
691
|
+
/**
|
|
692
|
+
* Optional per-identity persistent key/value store, registered as
|
|
693
|
+
* `identity.storage::*` alongside `identity::*`.
|
|
694
|
+
*/
|
|
695
|
+
storage?: ManagedIdentityStorageBackend;
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* Install `identity::*` (and optionally `identity.storage::*`) onto a
|
|
699
|
+
* participant's overlay root connection. The identity is resolved lazily on
|
|
700
|
+
* first use — see {@link registerLazyIdentityOnOverlay}.
|
|
701
|
+
*
|
|
702
|
+
* This is the identity counterpart to {@link registerHubServices}: a small,
|
|
703
|
+
* composable module the overlay does not need to know about.
|
|
704
|
+
*/
|
|
705
|
+
declare function registerIdentityServices(root: LinkRpcConnection<unknown>, options: RegisterIdentityServicesOptions): void;
|
|
706
|
+
//#endregion
|
|
707
|
+
//#region src/hub/server/hubRegister.d.ts
|
|
708
|
+
/**
|
|
709
|
+
* Out-of-band, per-call context produced by {@link withRequestIdContext}. A
|
|
710
|
+
* typed handler only ever sees the zod-stripped params (the `$hubrpc` envelope
|
|
711
|
+
* and the wire id are gone), so the originating request's wire id is smuggled
|
|
712
|
+
* alongside the message via the channel's `context` slot — never over a wire.
|
|
713
|
+
*
|
|
714
|
+
* The register handler uses it to resolve the link a forwarded claim arrived on
|
|
715
|
+
* via {@link Hub.getSourceTransport}.
|
|
716
|
+
*/
|
|
717
|
+
interface RegisterCallContext {
|
|
718
|
+
/**
|
|
719
|
+
* The hub-rewritten wire id of the inbound request, or `undefined` for
|
|
720
|
+
* responses / notifications. Keyed lookups into
|
|
721
|
+
* {@link Hub.getSourceTransport} use exactly this id.
|
|
722
|
+
*/
|
|
723
|
+
readonly requestId: RequestId | undefined;
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Annotate an inbound transport so every request carries its wire id in a
|
|
727
|
+
* {@link RegisterCallContext}. Synchronous and signature-agnostic: it neither
|
|
728
|
+
* verifies signatures nor checks capabilities (that has already happened on the
|
|
729
|
+
* forwarded path — see {@link registerHubServiceIdRegistry}). It exists only so
|
|
730
|
+
* a hub-hosted handler can recover the request id the typed channel would
|
|
731
|
+
* otherwise strip.
|
|
732
|
+
*/
|
|
733
|
+
declare function withRequestIdContext(link: IMessageTransport): ChannelTransport<RegisterCallContext>;
|
|
734
|
+
interface HubRegisterOptions {
|
|
735
|
+
/** The central hub whose forwarding-table claims are written. */
|
|
736
|
+
readonly hub: Hub;
|
|
737
|
+
/** ServiceId the register endpoint (and the rest of the hub services) is mounted under. */
|
|
738
|
+
readonly hubServiceId: string;
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Install the hub's privileged **claim front door** — the typed
|
|
742
|
+
* `hubServiceIdRegistry::registerServiceId` handler — onto the hub services
|
|
743
|
+
* `connection` (mounted under `hubServiceId`).
|
|
744
|
+
*
|
|
745
|
+
* Unlike the per-overlay `registerHubServices` front door — which only lets a
|
|
746
|
+
* participant claim *within its provenance-granted namespace* — this endpoint
|
|
747
|
+
* is the path for claiming a prefix **outside** that namespace.
|
|
748
|
+
*
|
|
749
|
+
* The handler is a **pure side effect**. Authentication (signature) and
|
|
750
|
+
* authorization (an admin-rooted capability that permits
|
|
751
|
+
* `hub::hubServiceIdRegistry::registerServiceId` for the requested
|
|
752
|
+
* `requestedPrefix`) are enforced *before* the call ever reaches here, by the
|
|
753
|
+
* {@link import('./forwardedCallGate').withForwardedCallGate forwarded-call
|
|
754
|
+
* gate} every untrusted participant sits behind. By the time the request lands,
|
|
755
|
+
* it is already known to be authentic and authorized, so the handler only:
|
|
756
|
+
*
|
|
757
|
+
* 1. validates the prefix is well-formed;
|
|
758
|
+
* 2. resolves the link the request was forwarded from via
|
|
759
|
+
* {@link Hub.getSourceTransport} (keyed by {@link RegisterCallContext.requestId});
|
|
760
|
+
* and
|
|
761
|
+
* 3. binds the prefix to that link via {@link Hub.claimPrefix}.
|
|
762
|
+
*
|
|
763
|
+
* The source transport answers only *where* to route the prefix, never
|
|
764
|
+
* *whether* the claim is allowed.
|
|
765
|
+
*/
|
|
766
|
+
declare function registerHubServiceIdRegistry(connection: LinkRpcConnection<RegisterCallContext>, options: HubRegisterOptions): void;
|
|
767
|
+
//#endregion
|
|
768
|
+
//#region src/hub/server/hubServices.d.ts
|
|
769
|
+
interface HubServicesOptions {
|
|
770
|
+
/** ServiceId the global services are mounted under. Defaults to `'hub'`. */
|
|
771
|
+
readonly hubServiceId?: string;
|
|
772
|
+
/** Optional diagnostic descriptor for the hub participant. */
|
|
773
|
+
readonly descriptors?: readonly ParticipantDescriptorSource[];
|
|
774
|
+
/** Runs before serving a global directory list or starting a directory watch. */
|
|
775
|
+
readonly beforeDirectoryQuery?: () => void | Promise<void>;
|
|
776
|
+
}
|
|
777
|
+
interface HubServices {
|
|
778
|
+
/** The in-process connection backing the hub's global services. */
|
|
779
|
+
readonly connection: LinkRpcConnection<RegisterCallContext>;
|
|
780
|
+
/** ServiceId the services are mounted under. */
|
|
781
|
+
readonly hubServiceId: string;
|
|
782
|
+
/** Traffic/topology inspector backing the hub's inspection services. */
|
|
783
|
+
readonly inspector: HubInspector;
|
|
784
|
+
/** Close the connection and detach the in-process link from the hub. */
|
|
785
|
+
dispose(): void;
|
|
786
|
+
}
|
|
787
|
+
declare function createHubServiceInterfaces(hub: Hub, options?: HubServicesOptions): HubServices;
|
|
788
|
+
//#endregion
|
|
789
|
+
//#region src/hub/server/accessCandidates.d.ts
|
|
790
|
+
/** A single entry of the aggregated interface directory. */
|
|
791
|
+
interface DirectoryEntry {
|
|
792
|
+
readonly serviceId: string;
|
|
793
|
+
readonly interfaceId: string;
|
|
794
|
+
readonly hash: string;
|
|
795
|
+
readonly serviceDescription?: string;
|
|
796
|
+
readonly rootPrincipalSets?: readonly RootPrincipalSet[];
|
|
797
|
+
}
|
|
798
|
+
interface AccessInterfaceRequirement {
|
|
799
|
+
readonly id: string;
|
|
800
|
+
/** Optional schema-version pin; when set the candidate's hash must match. */
|
|
801
|
+
readonly hash?: string;
|
|
802
|
+
/** Defaults to true. */
|
|
803
|
+
readonly required: boolean;
|
|
804
|
+
}
|
|
805
|
+
interface AccessMemberRequirement {
|
|
806
|
+
readonly interfaceId: string;
|
|
807
|
+
/** Member name matcher (`{ exact }` or `{ prefix }`). */
|
|
808
|
+
readonly member: Pattern;
|
|
809
|
+
/** Defaults to true. */
|
|
810
|
+
readonly required: boolean;
|
|
811
|
+
}
|
|
812
|
+
interface AccessSlotRequest {
|
|
813
|
+
readonly interfaces: readonly AccessInterfaceRequirement[];
|
|
814
|
+
readonly members: readonly AccessMemberRequirement[];
|
|
815
|
+
}
|
|
816
|
+
interface AccessSlotCandidate {
|
|
817
|
+
readonly serviceId: string;
|
|
818
|
+
readonly serviceDescription?: string;
|
|
819
|
+
/** Subset of the slot's requested interfaces actually present on this candidate. */
|
|
820
|
+
readonly satisfiedInterfaces: readonly AccessInterfaceRequirement[];
|
|
821
|
+
/**
|
|
822
|
+
* Requested interfaces the candidate does NOT satisfy. By construction
|
|
823
|
+
* every entry here is `required: false` — a required-but-missing
|
|
824
|
+
* interface filters the candidate out before it is emitted.
|
|
825
|
+
*/
|
|
826
|
+
readonly unsatisfiedInterfaces: readonly AccessInterfaceRequirement[];
|
|
827
|
+
}
|
|
828
|
+
interface ResolvedAccessSlot {
|
|
829
|
+
readonly request: AccessSlotRequest;
|
|
830
|
+
/** Empty = no service satisfies every required interface. */
|
|
831
|
+
readonly candidates: readonly AccessSlotCandidate[];
|
|
832
|
+
}
|
|
833
|
+
interface ResolvedCandidates {
|
|
834
|
+
readonly dependencies: Readonly<Record<string, ResolvedAccessSlot>>;
|
|
835
|
+
/** Slot ids for which no candidate satisfied every required interface. */
|
|
836
|
+
readonly noCandidateSlots: readonly string[];
|
|
837
|
+
}
|
|
838
|
+
/**
|
|
839
|
+
* Fetch the full interface directory by recursively walking the hub's
|
|
840
|
+
* **referral** directory graph. {@link walkHubDetailed} follows explicit
|
|
841
|
+
* `hubrpc.directory` listings to collect leaf interfaces (folding transitive
|
|
842
|
+
* root-node-id requirements onto descendants along the way). Routing claims do
|
|
843
|
+
* not create referrals. This is the v2 replacement for the hub-internal
|
|
844
|
+
* `_fullDirectory()`.
|
|
845
|
+
*/
|
|
846
|
+
declare function fetchFullDirectory(connection: LinkRpcConnection, hubServiceId?: string): Promise<DirectoryEntry[]>;
|
|
847
|
+
/**
|
|
848
|
+
* Resolve every dependency slot against a directory snapshot. Pure port of
|
|
849
|
+
* the v1 hub's per-request candidate resolution loop + `_candidatesForSlot`.
|
|
850
|
+
*/
|
|
851
|
+
declare function resolveAccessCandidates(slots: Readonly<Record<string, AccessSlotRequest>>, directory: readonly DirectoryEntry[]): ResolvedCandidates;
|
|
852
|
+
/**
|
|
853
|
+
* Match one slot against the directory snapshot. A service is a candidate iff
|
|
854
|
+
* it satisfies every `required` interface (by id, and by hash when pinned).
|
|
855
|
+
*/
|
|
856
|
+
declare function candidatesForSlot(slot: AccessSlotRequest, directory: readonly DirectoryEntry[]): AccessSlotCandidate[];
|
|
857
|
+
//#endregion
|
|
858
|
+
//#region src/hub/server/capabilityProposal.d.ts
|
|
859
|
+
/**
|
|
860
|
+
* Hub-minted "proposal" of a capability the user is about to approve.
|
|
861
|
+
* `capability` is the readable JSON the consent UI renders; `$hubData` is an
|
|
862
|
+
* opaque blob the UI must round-trip verbatim — only the issuer inspects it.
|
|
863
|
+
*/
|
|
864
|
+
interface CapabilityProposal {
|
|
865
|
+
readonly capability: Capability;
|
|
866
|
+
/** Opaque issuer-private signed blob; round-trip verbatim. */
|
|
867
|
+
readonly $hubData: unknown;
|
|
868
|
+
}
|
|
869
|
+
/**
|
|
870
|
+
* Access-grant duration vocabulary.
|
|
871
|
+
*
|
|
872
|
+
* `once` and `shortLived` share the same 5-minute TTL — they differ only on
|
|
873
|
+
* the *use* axis: `once` is the single-use "Allow once" choice (the consent
|
|
874
|
+
* host additionally pins a `callBind`, making it intrinsically single-use),
|
|
875
|
+
* while `shortLived` is an ordinary multi-use grant that simply expires soon.
|
|
876
|
+
* `persistent` never expires (no `expiresAtMs`); revocation is the only way to
|
|
877
|
+
* end it.
|
|
878
|
+
*/
|
|
879
|
+
type AccessDurationName = 'once' | 'shortLived' | 'longLived' | 'persistent';
|
|
880
|
+
/**
|
|
881
|
+
* Convert an access duration into a Unix-milliseconds expiration timestamp, so
|
|
882
|
+
* handlers compute the same `expiresAtMs` the issuer mints with. Returns
|
|
883
|
+
* `undefined` for `persistent` (and any future never-expiring duration), which
|
|
884
|
+
* callers pass straight to {@link CapabilityProposalIssuer.propose}/`mint` —
|
|
885
|
+
* both omit `expiresAtMs` when it is `undefined`, yielding a non-expiring cap.
|
|
886
|
+
* Defaults to the safe short TTL (`shortLived`) when no duration is supplied.
|
|
887
|
+
*/
|
|
888
|
+
declare function durationToExp(duration: AccessDurationName | undefined): number | undefined;
|
|
889
|
+
interface ProposeArgs {
|
|
890
|
+
readonly audience: PrincipalId;
|
|
891
|
+
readonly permissions: readonly Permission[];
|
|
892
|
+
/** Unix milliseconds. Absent = never expires. */
|
|
893
|
+
readonly expiresAtMs?: number;
|
|
894
|
+
}
|
|
895
|
+
/**
|
|
896
|
+
* Issues capabilities and capability proposals signed by an admin
|
|
897
|
+
* {@link SigningIdentity}. One instance per hub host; tracks redeemed nonces
|
|
898
|
+
* for one-shot proposal redemption.
|
|
899
|
+
*/
|
|
900
|
+
declare class CapabilityProposalIssuer {
|
|
901
|
+
private readonly _issuer;
|
|
902
|
+
private readonly _redeemedNonces;
|
|
903
|
+
private _nonceCounter;
|
|
904
|
+
constructor(_issuer: SigningIdentity);
|
|
905
|
+
get issuerPrincipalId(): PrincipalId;
|
|
906
|
+
/**
|
|
907
|
+
* Build an unsigned `Capability` the issuer is willing to sign, tagged with
|
|
908
|
+
* an internal marker. {@link signProposal} refuses to sign anything lacking
|
|
909
|
+
* the marker — a guard against a handler fabricating arbitrary caps. The
|
|
910
|
+
* marker is non-enumerable, so it never affects serialization or signing.
|
|
911
|
+
*/
|
|
912
|
+
propose(args: ProposeArgs): Capability;
|
|
913
|
+
/**
|
|
914
|
+
* Mint a final `SignedCapability` directly from a proposed cap, bypassing
|
|
915
|
+
* the preview round-trip. Use when no UI needs to render the unsigned form.
|
|
916
|
+
*/
|
|
917
|
+
mint(args: ProposeArgs): Promise<SignedCapability>;
|
|
918
|
+
/**
|
|
919
|
+
* Pair a proposed `Capability` with a domain-separated signature over
|
|
920
|
+
* `canonicalJson({ preview: cap, bind? })`. The result is **not** a bearer
|
|
921
|
+
* token — only this issuer can redeem it.
|
|
922
|
+
*/
|
|
923
|
+
signProposal(cap: Capability, opts?: {
|
|
924
|
+
readonly bind?: JsonValue;
|
|
925
|
+
}): Promise<CapabilityProposal>;
|
|
926
|
+
/**
|
|
927
|
+
* Verify and redeem a {@link CapabilityProposal}, returning a real
|
|
928
|
+
* {@link SignedCapability}. Checks: `$hubData` signature, preview deep-equal
|
|
929
|
+
* to `proposal.capability`, optional `bind` match, and one-shot nonce.
|
|
930
|
+
*/
|
|
931
|
+
redeemProposal(proposal: CapabilityProposal, opts?: {
|
|
932
|
+
readonly expectedBind?: JsonValue;
|
|
933
|
+
}): Promise<SignedCapability>;
|
|
934
|
+
private _sign;
|
|
935
|
+
private _nextNonce;
|
|
936
|
+
}
|
|
937
|
+
//#endregion
|
|
938
|
+
//#region src/hub/server/hubAccessService.d.ts
|
|
939
|
+
interface AccessConsumer {
|
|
940
|
+
readonly name: string;
|
|
941
|
+
readonly origin?: string;
|
|
942
|
+
readonly purpose?: string;
|
|
943
|
+
}
|
|
944
|
+
type AccessDuration = AccessDurationName;
|
|
945
|
+
interface AccessSlotBinding {
|
|
946
|
+
readonly serviceId: string;
|
|
947
|
+
/** Interface ids (of the slot's requested interfaces) the grant covers. */
|
|
948
|
+
readonly interfaces: readonly string[];
|
|
949
|
+
}
|
|
950
|
+
interface AccessRequestArgs {
|
|
951
|
+
readonly consumer: AccessConsumer;
|
|
952
|
+
/** Verified signer of the `hubAccess::request` call; the capability audience. */
|
|
953
|
+
readonly consumerPrincipalId: PrincipalId;
|
|
954
|
+
readonly dependencies: Readonly<Record<string, ResolvedAccessSlot>>;
|
|
955
|
+
readonly duration: AccessDuration | undefined;
|
|
956
|
+
}
|
|
957
|
+
type AccessDecision = {
|
|
958
|
+
readonly granted: true;
|
|
959
|
+
readonly resolvedSlots: Readonly<Record<string, AccessSlotBinding>>;
|
|
960
|
+
readonly capabilities: readonly SignedCapability[];
|
|
961
|
+
} | {
|
|
962
|
+
readonly granted: false;
|
|
963
|
+
readonly reason?: string;
|
|
964
|
+
};
|
|
965
|
+
interface AccessExtendMember {
|
|
966
|
+
readonly interfaceId: string;
|
|
967
|
+
readonly member: Pattern;
|
|
968
|
+
/** Defaults to true. */
|
|
969
|
+
readonly required: boolean;
|
|
970
|
+
}
|
|
971
|
+
interface AccessExtendArgs {
|
|
972
|
+
readonly consumer: AccessConsumer;
|
|
973
|
+
readonly consumerPrincipalId: PrincipalId;
|
|
974
|
+
readonly serviceId: string;
|
|
975
|
+
readonly added: readonly AccessExtendMember[];
|
|
976
|
+
readonly duration: AccessDuration | undefined;
|
|
977
|
+
}
|
|
978
|
+
type AccessExtendDecision = {
|
|
979
|
+
readonly granted: true;
|
|
980
|
+
readonly serviceId: string;
|
|
981
|
+
readonly grantedMembers: readonly {
|
|
982
|
+
interfaceId: string;
|
|
983
|
+
member: Pattern;
|
|
984
|
+
}[];
|
|
985
|
+
readonly capabilities?: readonly SignedCapability[];
|
|
986
|
+
} | {
|
|
987
|
+
readonly granted: false;
|
|
988
|
+
readonly reason?: string;
|
|
989
|
+
};
|
|
990
|
+
/**
|
|
991
|
+
* Optional per-permission invocation preview the consumer attaches to a
|
|
992
|
+
* direct ({@link HubAccessHandlers.onAccessRequestDirect}) request. It is
|
|
993
|
+
* **not** a signed authority constraint — it is the data the consent host
|
|
994
|
+
* needs to render an honest "Allow once" prompt and to pre-compute the
|
|
995
|
+
* `callBind.payloadHash` that binds a one-shot capability to exactly this
|
|
996
|
+
* call. `nonce`/`signedAtMs`/`interfaceHash` are the values the consumer
|
|
997
|
+
* will sign with when it actually issues the call.
|
|
998
|
+
*/
|
|
999
|
+
interface AccessCallIntent {
|
|
1000
|
+
/** Fully-qualified method name the consumer intends to call. */
|
|
1001
|
+
readonly method: string;
|
|
1002
|
+
/** Params the consumer intends to send (shown to the user, hashed into callBind). */
|
|
1003
|
+
readonly params?: unknown;
|
|
1004
|
+
/** Schema-version assertion the call will carry. */
|
|
1005
|
+
readonly interfaceHash?: string;
|
|
1006
|
+
/** base64url nonce bytes the consumer will sign with. */
|
|
1007
|
+
readonly nonce: string;
|
|
1008
|
+
/** Unix milliseconds the consumer will sign with. */
|
|
1009
|
+
readonly signedAtMs: number;
|
|
1010
|
+
/** One-line summary for the prompt. */
|
|
1011
|
+
readonly summary?: string;
|
|
1012
|
+
/** Consumer's suggested default consent action. */
|
|
1013
|
+
readonly suggestion?: AccessDuration;
|
|
1014
|
+
}
|
|
1015
|
+
/**
|
|
1016
|
+
* An {@link AccessDirectArgs} permission, carrying the verbatim signed
|
|
1017
|
+
* {@link Permission} the consumer asked for plus the optional
|
|
1018
|
+
* {@link AccessCallIntent} that enables "Allow once" byte-binding.
|
|
1019
|
+
*/
|
|
1020
|
+
interface AccessDirectPermission extends Permission {
|
|
1021
|
+
readonly callIntent?: AccessCallIntent;
|
|
1022
|
+
}
|
|
1023
|
+
interface AccessDirectArgs {
|
|
1024
|
+
readonly consumer: AccessConsumer;
|
|
1025
|
+
readonly consumerPrincipalId: PrincipalId;
|
|
1026
|
+
readonly permissions: readonly AccessDirectPermission[];
|
|
1027
|
+
readonly duration: AccessDuration | undefined;
|
|
1028
|
+
}
|
|
1029
|
+
type AccessDirectDecision = {
|
|
1030
|
+
readonly granted: true;
|
|
1031
|
+
readonly capabilities: readonly SignedCapability[];
|
|
1032
|
+
} | {
|
|
1033
|
+
readonly granted: false;
|
|
1034
|
+
readonly reason?: string;
|
|
1035
|
+
};
|
|
1036
|
+
interface HubAccessHandlers {
|
|
1037
|
+
/** Service-discovery grant: pick a service per slot and mint scoped caps. */
|
|
1038
|
+
onAccessRequest(args: AccessRequestArgs): Promise<AccessDecision>;
|
|
1039
|
+
/** Service-pinned widening of an existing grant. */
|
|
1040
|
+
onAccessExtend(args: AccessExtendArgs): Promise<AccessExtendDecision>;
|
|
1041
|
+
/** Verbatim attenuation grant (no discovery). */
|
|
1042
|
+
onAccessRequestDirect(args: AccessDirectArgs): Promise<AccessDirectDecision>;
|
|
1043
|
+
}
|
|
1044
|
+
interface RegisterHubAccessOptions {
|
|
1045
|
+
/** Consent + capability-issuance callbacks. */
|
|
1046
|
+
readonly handlers: HubAccessHandlers;
|
|
1047
|
+
/** Provides the directory snapshot for candidate resolution. */
|
|
1048
|
+
fetchDirectory(): Promise<readonly DirectoryEntry[]>;
|
|
1049
|
+
}
|
|
1050
|
+
/**
|
|
1051
|
+
* Install `hubAccess::{request,extend,requestAccess}` at the **connection
|
|
1052
|
+
* root** of a participant's overlay (root form, no serviceId prefix). The root
|
|
1053
|
+
* is never forwarded, so the consent front door is reached directly and needs
|
|
1054
|
+
* no capability of its own.
|
|
1055
|
+
*
|
|
1056
|
+
* The capability `audience` is taken **directly from `params.consumer.principal`**
|
|
1057
|
+
* — no longer derived from a verified call signer. A cap minted for a nodeId is
|
|
1058
|
+
* only usable by the holder of that node's key (enforced at call time by
|
|
1059
|
+
* `permits`, which checks `audience === call.signer`), so a self-asserted
|
|
1060
|
+
* audience grants no usable authority to a caller who does not hold the key.
|
|
1061
|
+
*/
|
|
1062
|
+
declare function registerHubAccessService(connection: LinkRpcConnection<unknown>, options: RegisterHubAccessOptions): void;
|
|
1063
|
+
//#endregion
|
|
1064
|
+
//#region src/hub/server/tokenIdentityStore.d.ts
|
|
1065
|
+
/**
|
|
1066
|
+
* What a connection token resolves to once redeemed. The token is a hub-minted,
|
|
1067
|
+
* single-use bearer credential; its presence on a connection attests that the
|
|
1068
|
+
* minting authority (a `connectionTokenBinder`-granted endpoint) vouched for the
|
|
1069
|
+
* bound slot(s).
|
|
1070
|
+
*
|
|
1071
|
+
* The two bindings are **independent axes**, each optional:
|
|
1072
|
+
*
|
|
1073
|
+
* - {@link identitySlot} drives the redeemer's *managed identity* (`identity::*`).
|
|
1074
|
+
* - {@link grantedServiceIdNamespace} drives the redeemer's *freely-claimable
|
|
1075
|
+
* serviceId namespace* (`hubGrantedServiceId::get`).
|
|
1076
|
+
*
|
|
1077
|
+
* A binder typically sets both to the same value (the historical convention),
|
|
1078
|
+
* but they no longer have to agree — and either may be omitted. A token with
|
|
1079
|
+
* neither is still admitted, but the connection routes anonymously (no
|
|
1080
|
+
* `identity::*`, no freely-claimable namespace).
|
|
1081
|
+
*/
|
|
1082
|
+
interface TokenIdentityBinding {
|
|
1083
|
+
/**
|
|
1084
|
+
* Managed-identity slot the redeeming connection inherits. Omit to admit the
|
|
1085
|
+
* connection without a managed identity.
|
|
1086
|
+
*/
|
|
1087
|
+
readonly identitySlot?: string;
|
|
1088
|
+
/**
|
|
1089
|
+
* ServiceId namespace the redeeming connection may claim freely (reported
|
|
1090
|
+
* via `hubGrantedServiceId::get`). Omit to grant no freely-claimable
|
|
1091
|
+
* namespace.
|
|
1092
|
+
*/
|
|
1093
|
+
readonly grantedServiceIdNamespace?: string;
|
|
1094
|
+
}
|
|
1095
|
+
/** A freshly minted token plus its absolute expiry (epoch ms). */
|
|
1096
|
+
interface MintedToken {
|
|
1097
|
+
readonly token: string;
|
|
1098
|
+
readonly expiresAt: number;
|
|
1099
|
+
}
|
|
1100
|
+
interface TokenIdentityStoreOptions {
|
|
1101
|
+
/** Default lifetime applied to {@link TokenIdentityStore.mint}. Default 30s. */
|
|
1102
|
+
readonly defaultTtlMs?: number;
|
|
1103
|
+
/** Clock seam (tests). Default `Date.now`. */
|
|
1104
|
+
readonly now?: () => number;
|
|
1105
|
+
/** Token generator seam (tests). Default 32 random bytes, base64url. */
|
|
1106
|
+
readonly generateToken?: () => string;
|
|
1107
|
+
}
|
|
1108
|
+
/**
|
|
1109
|
+
* The hub's connection-token mint + redemption store, shared between the
|
|
1110
|
+
* `connectionTokenBinder::bindConnectionToken` front door (mint) and a
|
|
1111
|
+
* `managedIdentity: { mode: "fromToken" }` / `grantedServiceId: { mode:
|
|
1112
|
+
* "fromToken" }` listener (redeem).
|
|
1113
|
+
*
|
|
1114
|
+
* Tokens are random, time-boxed and **single-use**: {@link redeem} consumes the
|
|
1115
|
+
* entry, so a token can bind exactly one connection and cannot be replayed once
|
|
1116
|
+
* used or expired. Expired entries are pruned lazily on access.
|
|
1117
|
+
*/
|
|
1118
|
+
declare class TokenIdentityStore {
|
|
1119
|
+
private readonly _byToken;
|
|
1120
|
+
private readonly _defaultTtlMs;
|
|
1121
|
+
private readonly _now;
|
|
1122
|
+
private readonly _generate;
|
|
1123
|
+
constructor(options?: TokenIdentityStoreOptions);
|
|
1124
|
+
/** Mint a fresh single-use token bound to `binding`, valid for `ttlMs`. */
|
|
1125
|
+
mint(binding?: TokenIdentityBinding, ttlMs?: number): MintedToken;
|
|
1126
|
+
/**
|
|
1127
|
+
* Non-consuming existence check for the `hubrpc::initialize` token gate.
|
|
1128
|
+
* Returns `true` iff the token is live (known and unexpired). Single-use is
|
|
1129
|
+
* enforced separately by {@link redeem}.
|
|
1130
|
+
*/
|
|
1131
|
+
peek(token: string | undefined): boolean;
|
|
1132
|
+
/**
|
|
1133
|
+
* Consume `token`, returning its binding exactly once. Returns `undefined`
|
|
1134
|
+
* for an unknown, expired or already-redeemed token (the caller should drop
|
|
1135
|
+
* the connection in that case).
|
|
1136
|
+
*/
|
|
1137
|
+
redeem(token: string | undefined): TokenIdentityBinding | undefined;
|
|
1138
|
+
/** Live (unexpired, unredeemed) token count — primarily for tests/metrics. */
|
|
1139
|
+
get size(): number;
|
|
1140
|
+
private _prune;
|
|
1141
|
+
}
|
|
1142
|
+
//#endregion
|
|
1143
|
+
//#region src/hub/server/connectionTokenBinderService.d.ts
|
|
1144
|
+
interface RegisterConnectionTokenBinderOptions {
|
|
1145
|
+
/** Shared mint/redeem store (the same instance the redeem-side listener reads). */
|
|
1146
|
+
readonly store: TokenIdentityStore;
|
|
1147
|
+
/**
|
|
1148
|
+
* Literal `identitySlot` prefixes this binder may mint identities for (e.g.
|
|
1149
|
+
* `["docker/"]`). A `bindConnectionToken({ identitySlot })` call is rejected
|
|
1150
|
+
* unless `identitySlot` starts with one of these. Empty → identity binding
|
|
1151
|
+
* is disabled (any `identitySlot` request is rejected).
|
|
1152
|
+
*/
|
|
1153
|
+
readonly identitySlotPrefixes: readonly string[];
|
|
1154
|
+
/**
|
|
1155
|
+
* Literal `serviceIdNamespace` prefixes this binder may grant (e.g.
|
|
1156
|
+
* `["docker/"]`). A `bindConnectionToken({ serviceIdNamespace })` call is
|
|
1157
|
+
* rejected unless `serviceIdNamespace` starts with one of these. Empty →
|
|
1158
|
+
* serviceId granting is disabled (any `serviceIdNamespace` request is
|
|
1159
|
+
* rejected).
|
|
1160
|
+
*/
|
|
1161
|
+
readonly serviceIdPrefixes: readonly string[];
|
|
1162
|
+
/** Optional per-token TTL (ms); falls back to the store's default. */
|
|
1163
|
+
readonly ttlMs?: number;
|
|
1164
|
+
/**
|
|
1165
|
+
* Mount point when registering as a **forwarded hub service** (reachable via
|
|
1166
|
+
* routing as `<serviceId>::connectionTokenBinder`). Omit for the **root
|
|
1167
|
+
* form**: an interface-form registration on an overlay root, never forwarded
|
|
1168
|
+
* → never gated.
|
|
1169
|
+
*/
|
|
1170
|
+
readonly serviceId?: string;
|
|
1171
|
+
}
|
|
1172
|
+
/**
|
|
1173
|
+
* Install `connectionTokenBinder::bindConnectionToken` on `connection`.
|
|
1174
|
+
*
|
|
1175
|
+
* Two forms, selected by {@link RegisterConnectionTokenBinderOptions.serviceId}:
|
|
1176
|
+
*
|
|
1177
|
+
* - **root form** (no `serviceId`): installed at a participant's overlay root
|
|
1178
|
+
* (root form, never forwarded → never gated). Safe because only the specific
|
|
1179
|
+
* trusted connection the acceptor installed it on can reach it.
|
|
1180
|
+
* - **forwarded form** (`serviceId` set): mounted as a routable hub service so
|
|
1181
|
+
* any participant can discover and call it. Safe only because forwarded calls
|
|
1182
|
+
* go through the hub's forwarded-call gate; the same prefix checks still bound
|
|
1183
|
+
* what any caller may mint.
|
|
1184
|
+
*
|
|
1185
|
+
* Each call mints a single-use token binding the requested `identitySlot` and/or
|
|
1186
|
+
* `serviceIdNamespace`, provided each requested field falls under the matching
|
|
1187
|
+
* configured prefix. Omitting a field leaves that axis unbound.
|
|
1188
|
+
*/
|
|
1189
|
+
declare function registerConnectionTokenBinderService(connection: LinkRpcConnection<unknown>, options: RegisterConnectionTokenBinderOptions): void;
|
|
1190
|
+
//#endregion
|
|
1191
|
+
//#region src/hub/server/connectionHandler.d.ts
|
|
1192
|
+
/**
|
|
1193
|
+
* Everything a {@link ConnectionHandler} needs to install a participant's root
|
|
1194
|
+
* services. Built by {@link import('./hubConnectionAcceptor').HubConnectionAcceptor}
|
|
1195
|
+
* once per accepted connection: the connection-specific bits (`root`,
|
|
1196
|
+
* `upstream`, `transport`, `token`) plus the connection-independent policy the
|
|
1197
|
+
* acceptor was configured with (`hubServiceId`, `authorizeClaim`,
|
|
1198
|
+
* `installHubAccess`).
|
|
1199
|
+
*/
|
|
1200
|
+
interface ConnectionContext {
|
|
1201
|
+
/** The overlay root to install this participant's front doors on. */
|
|
1202
|
+
readonly root: LinkRpcConnection<unknown>;
|
|
1203
|
+
/** The participant's link to the central hub (for `hubGrantedServiceId::register`). */
|
|
1204
|
+
readonly upstream: AttachedLink;
|
|
1205
|
+
/** The central hub. */
|
|
1206
|
+
readonly hub: Hub;
|
|
1207
|
+
/** The accepted transport (custom handlers may read attestation off it). */
|
|
1208
|
+
readonly transport: IMessageTransport;
|
|
1209
|
+
/** The `hubrpc::initialize` token the connection presented (if any). */
|
|
1210
|
+
readonly token: string | undefined;
|
|
1211
|
+
/** ServiceId the global reflection services are mounted under. */
|
|
1212
|
+
readonly hubServiceId: string;
|
|
1213
|
+
/** Per-connection claim authorizer (from the acceptor's policy), if any. */
|
|
1214
|
+
readonly authorizeClaim?: (requestedPrefix: ServiceId) => {
|
|
1215
|
+
ok: true;
|
|
1216
|
+
} | {
|
|
1217
|
+
ok: false;
|
|
1218
|
+
reason: string;
|
|
1219
|
+
};
|
|
1220
|
+
/** Installs the consent front door at the overlay root, if configured. */
|
|
1221
|
+
readonly installHubAccess?: (root: LinkRpcConnection<unknown>) => void;
|
|
1222
|
+
}
|
|
1223
|
+
/**
|
|
1224
|
+
* The resolved root services a claiming handler provisions. Every field is
|
|
1225
|
+
* optional; {@link provisionRoot} installs the always-on parts (claim/directory
|
|
1226
|
+
* front door + consent) unconditionally and the rest only when present.
|
|
1227
|
+
*/
|
|
1228
|
+
interface RootProvision {
|
|
1229
|
+
/** Freely-claimable serviceId namespace (`hubGrantedServiceId::get`). */
|
|
1230
|
+
readonly grantedServiceIdNamespace?: string;
|
|
1231
|
+
/** Lazily-resolved managed identity (`identity::*`). Omit for no identity. */
|
|
1232
|
+
readonly resolveIdentity?: () => Promise<Identity>;
|
|
1233
|
+
/** Optional per-identity persistent storage (`identity.storage::*`). */
|
|
1234
|
+
readonly storage?: ManagedIdentityStorageBackend;
|
|
1235
|
+
/** Token-minting front door (`connectionTokenBinder::*`). Omit for none. */
|
|
1236
|
+
readonly connectionTokenBinder?: RegisterConnectionTokenBinderOptions;
|
|
1237
|
+
}
|
|
1238
|
+
/**
|
|
1239
|
+
* A claiming connection handler: installs the participant's root services and
|
|
1240
|
+
* returns. Built-ins delegate the actual registration to {@link provisionRoot}.
|
|
1241
|
+
*/
|
|
1242
|
+
type ConnectionHandler = (ctx: ConnectionContext) => void;
|
|
1243
|
+
/**
|
|
1244
|
+
* Selects and (on claim) provisions a connection by its `hubrpc::initialize`
|
|
1245
|
+
* token. `handle` is **non-consuming** — it may be called at the pre-handshake
|
|
1246
|
+
* gate to answer "would you accept this token?" — and returns a
|
|
1247
|
+
* {@link ConnectionHandler} that does the (possibly consuming) installation, or
|
|
1248
|
+
* `undefined` to pass to the next factory.
|
|
1249
|
+
*/
|
|
1250
|
+
interface ConnectionHandlerFactory {
|
|
1251
|
+
handle(token: string | undefined): ConnectionHandler | undefined;
|
|
1252
|
+
}
|
|
1253
|
+
/**
|
|
1254
|
+
* Walk `factories` in order; return the first handler that claims `token`, or
|
|
1255
|
+
* `undefined` if none do (the caller drops the connection).
|
|
1256
|
+
*/
|
|
1257
|
+
declare function resolveConnectionHandler(factories: readonly ConnectionHandlerFactory[], token: string | undefined): ConnectionHandler | undefined;
|
|
1258
|
+
/**
|
|
1259
|
+
* Install a participant's root services from a resolved {@link RootProvision}:
|
|
1260
|
+
* the always-on claim/directory front door and consent surface, plus (when
|
|
1261
|
+
* present) the minting front door and lazy managed identity. This is the single
|
|
1262
|
+
* shared installer every built-in handler funnels through.
|
|
1263
|
+
*/
|
|
1264
|
+
declare function provisionRoot(ctx: ConnectionContext, provision: RootProvision): void;
|
|
1265
|
+
/**
|
|
1266
|
+
* A factory that claims **any** connection (optionally gated by `claims`) and
|
|
1267
|
+
* provisions a fixed root. Used for the `anonymous` handler (claims all), the
|
|
1268
|
+
* `static` handler (claims a matching token), and dial-in endpoints (claims all,
|
|
1269
|
+
* no token).
|
|
1270
|
+
*/
|
|
1271
|
+
declare function fixedProvisionHandler(provision: RootProvision, claims?: (token: string | undefined) => boolean): ConnectionHandlerFactory;
|
|
1272
|
+
/** The `anonymous` handler: claims every connection, provisioning `provision`. */
|
|
1273
|
+
declare function anonymousHandler(provision: RootProvision): ConnectionHandlerFactory;
|
|
1274
|
+
/** The `static` handler: claims connections whose token equals `value`. */
|
|
1275
|
+
declare function staticTokenHandler(value: string, provision: RootProvision): ConnectionHandlerFactory;
|
|
1276
|
+
/**
|
|
1277
|
+
* The `bound` handler: claims connections whose token is live in `store`, and on
|
|
1278
|
+
* claim **redeems** it (single-use) to derive the provisioned identity slot
|
|
1279
|
+
* and/or granted serviceId namespace. `resolveSlot` turns a redeemed slot into
|
|
1280
|
+
* a lazy identity resolver plus its per-identity storage (injected so this stays
|
|
1281
|
+
* node-agnostic).
|
|
1282
|
+
*/
|
|
1283
|
+
declare function boundTokenHandler(store: TokenIdentityStore, resolveSlot: (slot: string) => {
|
|
1284
|
+
resolveIdentity: () => Promise<Identity>;
|
|
1285
|
+
storage: ManagedIdentityStorageBackend;
|
|
1286
|
+
}): ConnectionHandlerFactory;
|
|
1287
|
+
//#endregion
|
|
1288
|
+
//#region src/hub/server/provenance.d.ts
|
|
1289
|
+
/**
|
|
1290
|
+
* Attested origin of an incoming connection. Produced by a
|
|
1291
|
+
* {@link ConnectionProvenanceProvider} from a freshly accepted transport,
|
|
1292
|
+
* *before* any RPC flows. Everything here is, by contract, already verified:
|
|
1293
|
+
* there is no "unverified" provenance — a provider that cannot fully attest
|
|
1294
|
+
* the peer returns `{ error }` instead of a partial result.
|
|
1295
|
+
*/
|
|
1296
|
+
interface ConnectionProvenance {
|
|
1297
|
+
/**
|
|
1298
|
+
* Stable identity key for the peer. INVARIANT: must begin with
|
|
1299
|
+
* `` `${provider.identityNamespace}/` `` (e.g. `"docker/echo-provider"`),
|
|
1300
|
+
* which {@link withProvenance} enforces — a value that doesn't is treated
|
|
1301
|
+
* as un-attested. This keeps keys from different provenance sources from
|
|
1302
|
+
* colliding and makes the namespace self-describing.
|
|
1303
|
+
*/
|
|
1304
|
+
readonly identityKey: string;
|
|
1305
|
+
/**
|
|
1306
|
+
* Free-form, already-verified attestation detail for logging and policy
|
|
1307
|
+
* (container id, image, pid, uid, …). Never used for routing or trust.
|
|
1308
|
+
*/
|
|
1309
|
+
readonly attributes: Readonly<Record<string, string | number>>;
|
|
1310
|
+
}
|
|
1311
|
+
/**
|
|
1312
|
+
* Resolves the {@link ConnectionProvenance} of an accepted transport.
|
|
1313
|
+
*
|
|
1314
|
+
* Generic over the transport type so the provider can read whatever the
|
|
1315
|
+
* concrete transport exposes (e.g. a `NodeSocketTransport`'s `socket` for
|
|
1316
|
+
* peercred) — the hub core stays free of those types. Implementations live
|
|
1317
|
+
* outside the hub (e.g. `@hediet/linkrpc-extra`).
|
|
1318
|
+
*/
|
|
1319
|
+
interface ConnectionProvenanceProvider<TTransport extends Transport> {
|
|
1320
|
+
/**
|
|
1321
|
+
* Namespace every {@link ConnectionProvenance.identityKey} this provider
|
|
1322
|
+
* emits is prefixed with (e.g. `"docker"`).
|
|
1323
|
+
*/
|
|
1324
|
+
readonly identityNamespace: string;
|
|
1325
|
+
/**
|
|
1326
|
+
* Resolve provenance for `transport` before any RPC is exchanged. Honour
|
|
1327
|
+
* `signal` to abort if the transport closes or a deadline elapses.
|
|
1328
|
+
*
|
|
1329
|
+
* @returns verified provenance, or `{ error }` to refuse attestation.
|
|
1330
|
+
*/
|
|
1331
|
+
resolve(transport: TTransport, signal: AbortSignal): Promise<ConnectionProvenance | {
|
|
1332
|
+
error: string;
|
|
1333
|
+
}>;
|
|
1334
|
+
}
|
|
1335
|
+
/**
|
|
1336
|
+
* A {@link Transport} annotated with its (possibly absent) attestation —
|
|
1337
|
+
* the narrow shape provenance-aware consumers (e.g. {@link PrefixPolicy})
|
|
1338
|
+
* depend on, without naming a concrete backend transport type.
|
|
1339
|
+
*/
|
|
1340
|
+
interface ITransportWithProvenance extends Transport {
|
|
1341
|
+
readonly provenance: ConnectionProvenance | undefined;
|
|
1342
|
+
}
|
|
1343
|
+
/** A concrete transport `T` annotated with its (possibly absent) attestation. */
|
|
1344
|
+
type WithProvenance<T extends Transport> = T & {
|
|
1345
|
+
readonly provenance: ConnectionProvenance | undefined;
|
|
1346
|
+
};
|
|
1347
|
+
interface WithProvenanceOptions {
|
|
1348
|
+
/**
|
|
1349
|
+
* Abort the provider's `resolve` after this many ms (counts as
|
|
1350
|
+
* un-attested). Omit for no deadline.
|
|
1351
|
+
*/
|
|
1352
|
+
readonly resolveTimeoutMs?: number;
|
|
1353
|
+
/**
|
|
1354
|
+
* Drop connections that could not be attested instead of forwarding them
|
|
1355
|
+
* with `provenance: undefined`. Default `false`.
|
|
1356
|
+
*/
|
|
1357
|
+
readonly requireProvenance?: boolean;
|
|
1358
|
+
}
|
|
1359
|
+
/**
|
|
1360
|
+
* Annotate every transport from `source` with the provenance `provider`
|
|
1361
|
+
* resolves for it. On provider error/timeout — or when the returned
|
|
1362
|
+
* `identityKey` does not start with `` `${provider.identityNamespace}/` `` —
|
|
1363
|
+
* the transport is forwarded with `provenance: undefined`, unless
|
|
1364
|
+
* {@link WithProvenanceOptions.requireProvenance} is set, in which case it is
|
|
1365
|
+
* disposed and dropped.
|
|
1366
|
+
*
|
|
1367
|
+
* This is the one concrete {@link mapTransport} the hub ships: it is where
|
|
1368
|
+
* `net.Socket` (or any backend handle) is read for attestation and nowhere
|
|
1369
|
+
* else.
|
|
1370
|
+
*/
|
|
1371
|
+
declare function withProvenance<T extends Transport>(source: ITransportServer<T>, provider: ConnectionProvenanceProvider<T>, options?: WithProvenanceOptions): ITransportServer<WithProvenance<T>>;
|
|
1372
|
+
//#endregion
|
|
1373
|
+
//#region src/hub/server/prefixPolicy.d.ts
|
|
1374
|
+
/** What a prefix-claim is judged against. */
|
|
1375
|
+
interface ClaimContext<TTransport extends Transport> {
|
|
1376
|
+
/**
|
|
1377
|
+
* Node id of the participant's hub-minted managed identity, or `undefined`
|
|
1378
|
+
* if the connection has no identity (anonymous routing).
|
|
1379
|
+
*/
|
|
1380
|
+
readonly principal: PrincipalId | undefined;
|
|
1381
|
+
/** The connecting transport (may carry provenance, depending on `T`). */
|
|
1382
|
+
readonly transport: TTransport;
|
|
1383
|
+
/** ServiceId prefix the participant is trying to claim. */
|
|
1384
|
+
readonly requestedPrefix: ServiceId;
|
|
1385
|
+
}
|
|
1386
|
+
/**
|
|
1387
|
+
* Decides whether a participant may own a routing prefix. This is the policy
|
|
1388
|
+
* wrapped around the hub's single privileged write seam (`claimPrefix`) — the
|
|
1389
|
+
* only place identity/provenance gates routing.
|
|
1390
|
+
*/
|
|
1391
|
+
interface PrefixPolicy<TTransport extends Transport> {
|
|
1392
|
+
authorizeClaim(ctx: ClaimContext<TTransport>): {
|
|
1393
|
+
ok: true;
|
|
1394
|
+
} | {
|
|
1395
|
+
ok: false;
|
|
1396
|
+
reason: string;
|
|
1397
|
+
};
|
|
1398
|
+
}
|
|
1399
|
+
interface PrincipalIdPrefixPolicyOptions<TTransport extends ITransportWithProvenance> {
|
|
1400
|
+
/**
|
|
1401
|
+
* Prefixes a participant is allowed to claim. Default: the transport's
|
|
1402
|
+
* attested {@link ConnectionProvenance.identityKey} used **verbatim** as
|
|
1403
|
+
* the sole claimable prefix (e.g. `"docker/echo-provider"`). The full,
|
|
1404
|
+
* namespace-scoped key is the prefix — it is never split or rewritten. A
|
|
1405
|
+
* connection with no provenance may claim nothing.
|
|
1406
|
+
*/
|
|
1407
|
+
derivePrefixes?(ctx: ClaimContext<TTransport>): readonly ServiceId[];
|
|
1408
|
+
}
|
|
1409
|
+
/**
|
|
1410
|
+
* Default {@link PrefixPolicy}: a participant may claim only the prefix(es)
|
|
1411
|
+
* {@link PrincipalIdPrefixPolicyOptions.derivePrefixes} grants it. The out-of-box
|
|
1412
|
+
* derivation ties the claimable prefix to the peer's attested provenance, so
|
|
1413
|
+
* `docker/echo-provider` can serve `docker/echo-provider/*` and nothing can
|
|
1414
|
+
* impersonate it.
|
|
1415
|
+
*/
|
|
1416
|
+
declare class PrincipalIdPrefixPolicy<TTransport extends ITransportWithProvenance> implements PrefixPolicy<TTransport> {
|
|
1417
|
+
private readonly _derive;
|
|
1418
|
+
constructor(options?: PrincipalIdPrefixPolicyOptions<TTransport>);
|
|
1419
|
+
authorizeClaim(ctx: ClaimContext<TTransport>): {
|
|
1420
|
+
ok: true;
|
|
1421
|
+
} | {
|
|
1422
|
+
ok: false;
|
|
1423
|
+
reason: string;
|
|
1424
|
+
};
|
|
1425
|
+
}
|
|
1426
|
+
//#endregion
|
|
1427
|
+
//#region src/hub/server/hubConnectionAcceptor.d.ts
|
|
1428
|
+
/**
|
|
1429
|
+
* A data shape describing an imperative consent front door — the handlers plus
|
|
1430
|
+
* the directory source `registerHubAccessService` needs. Retained as a
|
|
1431
|
+
* convenience type; the acceptor installs consent via the more general
|
|
1432
|
+
* {@link HubConnectionAcceptorBaseOptions.installHubAccess} callback.
|
|
1433
|
+
*/
|
|
1434
|
+
interface HubAccessConfig {
|
|
1435
|
+
readonly handlers: HubAccessHandlers;
|
|
1436
|
+
fetchDirectory(): Promise<readonly DirectoryEntry[]>;
|
|
1437
|
+
}
|
|
1438
|
+
interface HubConnectionAcceptorBaseOptions<TTransport extends Transport> {
|
|
1439
|
+
/**
|
|
1440
|
+
* Source of inbound transports. Often a provenance-annotated server
|
|
1441
|
+
* (`withProvenance(socketServer, provider)`), but any
|
|
1442
|
+
* {@link ITransportServer} works — provenance is read via `resolveIdentity`
|
|
1443
|
+
* and the policy, not required by the acceptor's types.
|
|
1444
|
+
*/
|
|
1445
|
+
readonly server: ITransportServer<TTransport>;
|
|
1446
|
+
/** The central hub all accepted participants attach under. */
|
|
1447
|
+
readonly hub: Hub;
|
|
1448
|
+
/**
|
|
1449
|
+
* Ordered connection handlers. For each accepted transport the acceptor
|
|
1450
|
+
* resolves the first handler whose {@link ConnectionHandlerFactory.handle}
|
|
1451
|
+
* claims the presented `hubrpc::initialize` token; that handler installs the
|
|
1452
|
+
* participant's root services (identity, granted namespace, minting, …). If
|
|
1453
|
+
* no handler claims, the connection is dropped. An empty array accepts
|
|
1454
|
+
* nothing.
|
|
1455
|
+
*/
|
|
1456
|
+
readonly handlers: readonly ConnectionHandlerFactory[];
|
|
1457
|
+
/**
|
|
1458
|
+
* Authorizes each prefix claim a participant makes. Omit to allow every
|
|
1459
|
+
* well-formed claim.
|
|
1460
|
+
*/
|
|
1461
|
+
readonly policy?: PrefixPolicy<TTransport>;
|
|
1462
|
+
/** ServiceId of the global reflection services. Defaults to `'hub'`. */
|
|
1463
|
+
readonly hubServiceId?: string;
|
|
1464
|
+
/**
|
|
1465
|
+
* Install the consent front door at each accepted participant's overlay
|
|
1466
|
+
* root (root form, never forwarded → never gated). The callback receives the
|
|
1467
|
+
* root connection and registers `hubAccess::*` on it however it likes — the
|
|
1468
|
+
* imperative `registerHubAccessService(root, …)` or the keyless
|
|
1469
|
+
* `HubAccessManifestHost.registerHubAccessAtRoot(root)`. Omit on open hubs;
|
|
1470
|
+
* then no consent surface is served.
|
|
1471
|
+
*/
|
|
1472
|
+
installHubAccess?(root: LinkRpcConnection<unknown>): void;
|
|
1473
|
+
/** Fired after a participant's overlay is attached. */
|
|
1474
|
+
onAttached?(info: {
|
|
1475
|
+
overlay: RootOverlay;
|
|
1476
|
+
}): void;
|
|
1477
|
+
/** Fired when accepting a connection throws. */
|
|
1478
|
+
onError?(error: Error): void;
|
|
1479
|
+
}
|
|
1480
|
+
/**
|
|
1481
|
+
* Forwarded-call gating policy — a **discriminated union** so the type system
|
|
1482
|
+
* enforces the safe combinations and rules out the dangerous "ask for
|
|
1483
|
+
* capability enforcement but forget the trust anchors" misconfiguration:
|
|
1484
|
+
*
|
|
1485
|
+
* - **off** (default): forwarded calls reach the hub unverified.
|
|
1486
|
+
* - **authenticity only** (`verifyForwardedCalls: true`): forwarded calls must
|
|
1487
|
+
* carry a valid `$hubrpc` signature; `adminIds` is optional.
|
|
1488
|
+
* - **capability** (`verifyForwardedCalls: true` + `requireForwardedCapability:
|
|
1489
|
+
* true`): forwarded calls must additionally present a capability rooted at one
|
|
1490
|
+
* of `adminIds`. Because an empty/absent anchor set fails closed (rejecting
|
|
1491
|
+
* *every* capability, including validly granted ones), `adminIds` is
|
|
1492
|
+
* **required at compile time** in this arm.
|
|
1493
|
+
*/
|
|
1494
|
+
type ForwardCheckingPolicy = {
|
|
1495
|
+
readonly verifyForwardedCalls?: false;
|
|
1496
|
+
readonly requireForwardedCapability?: false;
|
|
1497
|
+
readonly adminIds?: undefined;
|
|
1498
|
+
} | {
|
|
1499
|
+
/**
|
|
1500
|
+
* Every accepted participant's hub-facing link is wrapped in a
|
|
1501
|
+
* {@link withForwardedCallGate signature front door}: forwarded
|
|
1502
|
+
* (fully-qualified) requests must carry a valid `$hubrpc` signature to
|
|
1503
|
+
* reach the hub. Requests to the hub's own `hubServiceId` prefix are
|
|
1504
|
+
* exempt (those services self-gate).
|
|
1505
|
+
*/
|
|
1506
|
+
readonly verifyForwardedCalls: true;
|
|
1507
|
+
readonly requireForwardedCapability?: false;
|
|
1508
|
+
/**
|
|
1509
|
+
* Optional root trust anchors. With authenticity-only verification a
|
|
1510
|
+
* capability is not required, but when present a call's chain is still
|
|
1511
|
+
* checked against these anchors.
|
|
1512
|
+
*/
|
|
1513
|
+
readonly adminIds?: Iterable<PrincipalId>;
|
|
1514
|
+
} | {
|
|
1515
|
+
readonly verifyForwardedCalls: true;
|
|
1516
|
+
/**
|
|
1517
|
+
* The forwarded-call gate also requires a capability, not just a
|
|
1518
|
+
* signature: a forwarded call authorises only if its capability chain
|
|
1519
|
+
* roots at one of {@link adminIds} (accepted for every service). The
|
|
1520
|
+
* gate also dedups each request nonce, so `callBind` grants are
|
|
1521
|
+
* single-use for free.
|
|
1522
|
+
*/
|
|
1523
|
+
readonly requireForwardedCapability: true;
|
|
1524
|
+
/** Root trust anchors. Typically the hub admin's PrincipalId. Required (fail-closed otherwise). */
|
|
1525
|
+
readonly adminIds: Iterable<PrincipalId>;
|
|
1526
|
+
};
|
|
1527
|
+
type HubConnectionAcceptorOptions<TTransport extends Transport> = HubConnectionAcceptorBaseOptions<TTransport> & ForwardCheckingPolicy;
|
|
1528
|
+
/**
|
|
1529
|
+
* Bridges accepted transports onto the hubv2 graph via a pluggable chain of
|
|
1530
|
+
* {@link ConnectionHandlerFactory connection handlers}. For each transport it:
|
|
1531
|
+
*
|
|
1532
|
+
* 1. attaches an uplink to the central hub and builds a {@link RootOverlay};
|
|
1533
|
+
* 2. resolves the first handler whose token check claims the connection and
|
|
1534
|
+
* lets it install the participant's root services (claim/directory front
|
|
1535
|
+
* door + consent, and optionally identity, granted namespace, minting) —
|
|
1536
|
+
* dropping the connection if none claim; and
|
|
1537
|
+
* 3. connects the participant, disposing the overlay and detaching the uplink
|
|
1538
|
+
* (which releases its claimed prefixes) when the transport closes.
|
|
1539
|
+
*
|
|
1540
|
+
* It never names a backend transport type — hand it any {@link ITransportServer}.
|
|
1541
|
+
*/
|
|
1542
|
+
declare class HubConnectionAcceptor<TTransport extends Transport> {
|
|
1543
|
+
private readonly _options;
|
|
1544
|
+
private readonly _accepted;
|
|
1545
|
+
private _disposed;
|
|
1546
|
+
constructor(_options: HubConnectionAcceptorOptions<TTransport>);
|
|
1547
|
+
private _accept;
|
|
1548
|
+
/**
|
|
1549
|
+
* Wrap the hub-facing link per the {@link ForwardCheckingPolicy}. The
|
|
1550
|
+
* option union guarantees that capability mode always carries its
|
|
1551
|
+
* `adminIds` trust anchors, so this can never silently fail closed by
|
|
1552
|
+
* forgetting them.
|
|
1553
|
+
*/
|
|
1554
|
+
private _gateHubFacing;
|
|
1555
|
+
private _wire;
|
|
1556
|
+
dispose(): void;
|
|
1557
|
+
}
|
|
1558
|
+
//#endregion
|
|
1559
|
+
export { AccessSlotRequest as $, AccessCallIntent as A, PendingRequestInfo as At, AccessRequestArgs as B, staticTokenHandler as C, HubTrafficSubscription as Ct, TokenIdentityBinding as D, HubOptions as Dt, MintedToken as E, Hub as Et, AccessDirectPermission as F, AccessDurationName as G, HubAccessHandlers as H, AccessDuration as I, ProposeArgs as J, CapabilityProposal as K, AccessExtendArgs as L, AccessDecision as M, AccessDirectArgs as N, TokenIdentityStore as O, IDisposable as Ot, AccessDirectDecision as P, AccessSlotCandidate as Q, AccessExtendDecision as R, resolveConnectionHandler as S, HubTrafficSource as St, registerConnectionTokenBinderService as T, AttachedLink as Tt, RegisterHubAccessOptions as U, AccessSlotBinding as V, registerHubAccessService as W, AccessInterfaceRequirement as X, durationToExp as Y, AccessMemberRequirement as Z, RootProvision as _, RootOverlay as _t, PrefixPolicy as a, resolveAccessCandidates as at, fixedProvisionHandler as b, OverlaySplitterInspection as bt, ConnectionProvenance as c, createHubServiceInterfaces as ct, WithProvenance as d, registerHubServiceIdRegistry as dt, DirectoryEntry as et, WithProvenanceOptions as f, withRequestIdContext as ft, ConnectionHandlerFactory as g, registerIdentityServices as gt, ConnectionHandler as h, registerHubServices as ht, ClaimContext as i, fetchFullDirectory as it, AccessConsumer as j, TokenIdentityStoreOptions as k, IHubLogger as kt, ConnectionProvenanceProvider as l, HubRegisterOptions as lt, ConnectionContext as m, RegisterIdentityServicesOptions as mt, HubConnectionAcceptor as n, ResolvedCandidates as nt, PrincipalIdPrefixPolicy as o, HubServices as ot, withProvenance as p, RegisterHubServicesOptions as pt, CapabilityProposalIssuer as q, HubConnectionAcceptorOptions as r, candidatesForSlot as rt, PrincipalIdPrefixPolicyOptions as s, HubServicesOptions as st, HubAccessConfig as t, ResolvedAccessSlot as tt, ITransportWithProvenance as u, RegisterCallContext as ut, anonymousHandler as v, RootOverlayOptions as vt, RegisterConnectionTokenBinderOptions as w, HubTrafficWatchOptions as wt, provisionRoot as x, HubInspector as xt, boundTokenHandler as y, OverlaySplitter as yt, AccessExtendMember as z };
|
|
1560
|
+
//# sourceMappingURL=hubConnectionAcceptor-BwydvFa4.d.ts.map
|