@hediet/linkrpc 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +383 -0
  2. package/dist/chunks/_empty-crypto-Bi0tGx5K.js +8 -0
  3. package/dist/chunks/boundedTrafficSubscription-1L592xc7.js +1126 -0
  4. package/dist/chunks/boundedTrafficSubscription-1L592xc7.js.map +1 -0
  5. package/dist/chunks/hub.interfaces-BzWfsVT2.js +526 -0
  6. package/dist/chunks/hub.interfaces-BzWfsVT2.js.map +1 -0
  7. package/dist/chunks/hubAccess-DwTZPiI8.d.ts +79 -0
  8. package/dist/chunks/hubAccess-DwTZPiI8.d.ts.map +1 -0
  9. package/dist/chunks/hubFacade-CQflVkVC.js +85 -0
  10. package/dist/chunks/hubFacade-CQflVkVC.js.map +1 -0
  11. package/dist/chunks/hubFacade-Dkgw2pTi.d.ts +101 -0
  12. package/dist/chunks/hubFacade-Dkgw2pTi.d.ts.map +1 -0
  13. package/dist/chunks/linkRpcConnection-CtlQmetO.d.ts +3623 -0
  14. package/dist/chunks/linkRpcConnection-CtlQmetO.d.ts.map +1 -0
  15. package/dist/chunks/rolldown-runtime-4LSo1kEK.js +17 -0
  16. package/dist/chunks/src-D3NUIwyo.js +7795 -0
  17. package/dist/chunks/src-D3NUIwyo.js.map +1 -0
  18. package/dist/hub/client/index.d.ts +50 -0
  19. package/dist/hub/client/index.d.ts.map +1 -0
  20. package/dist/hub/client/index.js +97 -0
  21. package/dist/hub/client/index.js.map +1 -0
  22. package/dist/hub/common/index.d.ts +1189 -0
  23. package/dist/hub/common/index.d.ts.map +1 -0
  24. package/dist/hub/common/index.js +267 -0
  25. package/dist/hub/common/index.js.map +1 -0
  26. package/dist/index.d.ts +6 -0
  27. package/dist/index.js +7 -0
  28. package/dist/inspection/index.d.ts +66 -0
  29. package/dist/inspection/index.d.ts.map +1 -0
  30. package/dist/inspection/index.js +6 -0
  31. package/dist/node.d.ts +548 -0
  32. package/dist/node.d.ts.map +1 -0
  33. package/dist/node.js +1087 -0
  34. package/dist/node.js.map +1 -0
  35. package/dist/web.d.ts +44 -0
  36. package/dist/web.d.ts.map +1 -0
  37. package/dist/web.js +58 -0
  38. package/dist/web.js.map +1 -0
  39. package/package.json +59 -0
package/dist/node.js ADDED
@@ -0,0 +1,1087 @@
1
+ /*---------------------------------------------------------------------------------------------
2
+ * Copyright (c) Microsoft Corporation. All rights reserved.
3
+ * Licensed under the MIT License. See License.txt in the project root for license information.
4
+ *--------------------------------------------------------------------------------------------*/
5
+ import { A as NotificationType, C as defineInterface, D as sha256, E as computeInterfaceHash, F as normalizeJsonSchema, M as notificationType, N as requestType, O as jcsCanonicalize, P as zodToSvcJsonSchema, S as InterfaceDefinition, T as EXTENSION_PREFIX, _ as zTrafficEvent, a as topologyInterface, b as zTrafficTransitEvent, c as zParticipantDescriptorSource, d as zTopologyLink, f as zTopologyLinkEndpoint, g as zTopologyTransportInfo, h as zTopologyTransportEndpoint, i as nodeInterface, j as RequestType, k as jcsCanonicalizeBytes, l as zRouteClaim, m as zTopologyPort, o as trafficInterface, p as zTopologyNode, s as zParticipantDescriptor, u as zTopologyGraph, v as zTrafficOverflowEvent, w as interfaceFromSchema, x as zTrafficWatchResult, y as zTrafficTransitEndpoint } from "./chunks/boundedTrafficSubscription-1L592xc7.js";
6
+ import { $t as bareInterfaceTarget, A as attachCapabilities, At as PRINCIPAL_PREFIX, B as LinkRpcConnection, Bt as isRequest, C as identityStorageInterface, Ct as getKeyId, D as PublicWrappingIdentity, Dt as signingInput, E as PublicSigningIdentity, Et as signingDomainValue, F as generateX25519Keypair, Ft as principalForPublicKey, Gt as StreamDir, H as JsonRpcChannel, Ht as STREAM_METHOD, I as formatEndpointUri, It as publicKeyForKeyId, Jt as directoryInterface, Kt as streamInterface, L as isHubEndpoint, Lt as resolveSigningKey, M as verifyRpcCall, Mt as bytesToBase64Url, N as crypto_exports, Nt as keyIdForPrincipal, O as Principal, Ot as withSignature, P as generateKeypair, Pt as keyIdForPublicKey, Qt as generateTsInterface, R as parseEndpointUri, Rt as ErrorCode, S as identityInterface, St as LINKRPC_UNSIGNED_KEY, T as KeypairSigningIdentity, Tt as signedHash, Ut as StreamControlReason, V as ServiceHandle, Vt as isResponse, Wt as StreamControlType, Xt as schemasInterface, Yt as directoryWatchNever, Zt as zServiceIdPattern, _ as InMemoryManagedIdentity, _t as HUBRPC_META_KEY, a as verifyCall, an as componentSchemaRef, at as TransportPair, b as registerIdentityOnOverlay, bt as LINKRPC_META_KEY, c as invoke, ct as requireObjectParams, d as prefix, dt as parseMethodName, en as createSchemaToZod, et as DEFAULT_RPC_TIMEOUT_MS, f as signCapability, ft as capabilityPermits, g as createManagedPrincipal, gt as permissionPermits, h as PrincipalWithStore, ht as permissionMatchesTarget, i as signParams, in as componentSchemaName, it as MultiplexedTransport, j as signRpcCall, jt as base64UrlToBytes, k as CapBag, kt as KEY_ID_PREFIX, l as issueCapability, lt as stripLinkRpcWireMeta, m as SigningSender, mt as matchParams, n as createSeededMemoryPrincipal, nn as materializeJsonSchema, nt as Channel, o as capBagFreshAt, on as isAssignable, ot as connectTransports, p as OneShotCapStaging, pt as hasSignedCapabilityShape, qt as defaultsInterface, r as createSeededSigningIdentity, rn as assertSchemaReferences, rt as RpcError, s as capabilityFreshAt, st as traceMessageTransport, t as createMemoryPrincipal, tn as schemaToZod, tt as withRpcTimeout, u as permits, ut as methodNameToTarget, v as InMemoryManagedIdentityStorage, vt as HUBRPC_SIGNATURE_KEY, w as KeypairIdentity, wt as readSignature, x as registerLazyIdentityOnOverlay, xt as LINKRPC_SIGNATURE_KEY, y as createManagedIdentity, yt as HUBRPC_UNSIGNED_KEY, z as ChannelConnector, zt as isNotification } from "./chunks/src-D3NUIwyo.js";
7
+ import { n as hubFromConnection } from "./chunks/hubFacade-CQflVkVC.js";
8
+ import { spawn } from "node:child_process";
9
+ import * as fs from "node:fs/promises";
10
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
11
+ import * as net from "node:net";
12
+ import * as os from "node:os";
13
+ import * as path from "node:path";
14
+ import { dirname } from "node:path";
15
+ //#region src/node/ndjsonTransport.ts
16
+ /**
17
+ * Newline-delimited JSON transport over a Node Readable/Writable pair.
18
+ * One JSON value per line, both directions.
19
+ *
20
+ * Not LSP-style headers — those add no value for a private link between two
21
+ * cooperating processes and require a length-prefixed framer.
22
+ */
23
+ var NdjsonTransport = class {
24
+ _input;
25
+ _output;
26
+ _onClose;
27
+ _listener;
28
+ _buffer = [];
29
+ _residual = "";
30
+ _closed = false;
31
+ constructor(_input, _output, _onClose) {
32
+ this._input = _input;
33
+ this._output = _output;
34
+ this._onClose = _onClose;
35
+ this._input.setEncoding?.("utf8");
36
+ this._input.on("data", (chunk) => this._onData(chunk));
37
+ this._input.on("end", () => this._onEnd());
38
+ this._input.on("close", () => this._onEnd());
39
+ this._input.on("error", () => this._onEnd());
40
+ if (!Object.is(this._output, this._input)) this._output.on("error", () => this._onEnd());
41
+ }
42
+ send(message) {
43
+ if (this._closed) return;
44
+ this._output.write(JSON.stringify(message) + "\n");
45
+ }
46
+ setListener(listener) {
47
+ this._listener = listener;
48
+ if (listener) while (this._buffer.length > 0 && this._listener) {
49
+ const m = this._buffer.shift();
50
+ this._listener(m);
51
+ }
52
+ }
53
+ dispose() {
54
+ if (this._closed) return;
55
+ this._closed = true;
56
+ this._onClose?.();
57
+ }
58
+ _onData(chunk) {
59
+ this._residual += chunk;
60
+ let nl;
61
+ while ((nl = this._residual.indexOf("\n")) >= 0) {
62
+ const line = this._residual.slice(0, nl).trim();
63
+ this._residual = this._residual.slice(nl + 1);
64
+ if (!line) continue;
65
+ let parsed;
66
+ try {
67
+ parsed = JSON.parse(line);
68
+ } catch {
69
+ continue;
70
+ }
71
+ this._deliver(parsed);
72
+ }
73
+ }
74
+ _onEnd() {
75
+ if (!this._closed) {
76
+ this._closed = true;
77
+ this._onClose?.();
78
+ }
79
+ }
80
+ _deliver(m) {
81
+ if (this._listener) this._listener(m);
82
+ else this._buffer.push(m);
83
+ }
84
+ };
85
+ //#endregion
86
+ //#region src/node/initialize.ts
87
+ /**
88
+ * Reserved transport-handshake method. Sent as the very first message on a
89
+ * connection that authenticates and/or negotiates transport details. It is
90
+ * handled entirely by the transport layer (via {@link connectNdjson}) and is
91
+ * never forwarded to the hub or any service — it is not a routed call.
92
+ */
93
+ const INITIALIZE_METHOD = "hubrpc::initialize";
94
+ /** Transitional alias accepted by servers created during the LinkRPC wire-name migration. */
95
+ const LINKRPC_INITIALIZE_METHOD_ALIAS = "linkrpc::initialize";
96
+ /** Current transport protocol version. */
97
+ const INITIALIZE_PROTOCOL_VERSION = 1;
98
+ /** Default time (ms) to wait for the handshake message before giving up. */
99
+ const DEFAULT_HANDSHAKE_TIMEOUT_MS = 1e4;
100
+ /** Fixed request id for the initiator's `hubrpc::initialize` request. */
101
+ const INITIALIZE_REQUEST_ID = 0;
102
+ /**
103
+ * The single supported way to build an ndjson transport. Constructs the
104
+ * transport over `input`/`output` and, when {@link ConnectNdjsonOptions.initialize}
105
+ * is set, runs the `hubrpc::initialize` handshake internally before resolving.
106
+ *
107
+ * Rejects (after disposing the transport) when the handshake fails — a bad or
108
+ * missing token, a wrong/absent first message, or a timeout. The accepting side
109
+ * should treat a rejection as "drop this connection".
110
+ */
111
+ async function connectNdjson(opts) {
112
+ const role = opts.initialize;
113
+ let rejectClosedHandshake;
114
+ const closedDuringHandshake = role ? new Promise((_resolve, reject) => {
115
+ rejectClosedHandshake = reject;
116
+ }) : void 0;
117
+ const baseTransport = new NdjsonTransport(opts.input, opts.output, () => {
118
+ try {
119
+ opts.onClose?.();
120
+ } finally {
121
+ rejectClosedHandshake?.(/* @__PURE__ */ new Error("hubrpc::initialize: transport closed during handshake"));
122
+ }
123
+ });
124
+ const transport = opts.trace === void 0 ? baseTransport : traceMessageTransport(baseTransport, opts.trace);
125
+ if (!role) return { transport };
126
+ try {
127
+ const { token } = await Promise.race([runInitializeHandshake(transport, role, { ...opts.handshakeTimeoutMs !== void 0 ? { handshakeTimeoutMs: opts.handshakeTimeoutMs } : {} }), closedDuringHandshake]);
128
+ rejectClosedHandshake = void 0;
129
+ return {
130
+ transport,
131
+ ...token !== void 0 ? { token } : {}
132
+ };
133
+ } catch (err) {
134
+ rejectClosedHandshake = void 0;
135
+ transport.dispose();
136
+ throw err;
137
+ }
138
+ }
139
+ /**
140
+ * Run the `hubrpc::initialize` handshake on an already-constructed transport.
141
+ * Used for transports that the caller builds itself (e.g. WebSocket), where
142
+ * {@link connectNdjson} does not apply. On failure it throws but does NOT
143
+ * dispose the transport — the caller owns it and decides how to tear down.
144
+ *
145
+ * For ndjson links, prefer {@link connectNdjson}, which constructs the
146
+ * transport and runs this handshake atomically.
147
+ */
148
+ async function runInitializeHandshake(transport, role, options = {}) {
149
+ const timeoutMs = options.handshakeTimeoutMs ?? DEFAULT_HANDSHAKE_TIMEOUT_MS;
150
+ if (role.kind === "client") {
151
+ await _runClientHandshake(transport, role.token, timeoutMs);
152
+ return {};
153
+ }
154
+ return { token: await _runServerHandshake(transport, role.isTokenAccepted, timeoutMs) };
155
+ }
156
+ async function _runClientHandshake(transport, token, timeoutMs) {
157
+ const reply = _nextMessage(transport, timeoutMs);
158
+ const params = {
159
+ protocolVersion: 1,
160
+ ...token !== void 0 ? { token } : {}
161
+ };
162
+ transport.send({
163
+ jsonrpc: "2.0",
164
+ id: INITIALIZE_REQUEST_ID,
165
+ method: INITIALIZE_METHOD,
166
+ params
167
+ });
168
+ const message = await reply;
169
+ if (isRequest(message) || !("id" in message) || message.id !== INITIALIZE_REQUEST_ID) throw new Error("hubrpc::initialize: unexpected reply to handshake");
170
+ if ("error" in message) throw new Error(`hubrpc::initialize rejected: ${message.error.message}`);
171
+ }
172
+ async function _runServerHandshake(transport, isTokenAccepted, timeoutMs) {
173
+ const first = await _nextMessage(transport, timeoutMs);
174
+ if (!isRequest(first) || first.method !== "hubrpc::initialize" && first.method !== "linkrpc::initialize") throw new Error("hubrpc::initialize: expected initialize as the first message");
175
+ const params = first.params ?? {};
176
+ if (!await isTokenAccepted(params.token)) {
177
+ transport.send({
178
+ jsonrpc: "2.0",
179
+ id: first.id,
180
+ error: {
181
+ code: ErrorCode.invalidRequest,
182
+ message: "unauthenticated"
183
+ }
184
+ });
185
+ throw new Error("hubrpc::initialize: unauthenticated");
186
+ }
187
+ transport.send({
188
+ jsonrpc: "2.0",
189
+ id: first.id,
190
+ result: { protocolVersion: 1 }
191
+ });
192
+ return params.token;
193
+ }
194
+ /**
195
+ * Consume exactly one incoming message, then detach. Relies on
196
+ * {@link NdjsonTransport}'s buffering: setting the listener to `undefined` from
197
+ * inside the callback stops the buffer drain, leaving any later messages queued
198
+ * for the real listener the channel attaches afterwards.
199
+ */
200
+ function _nextMessage(transport, timeoutMs) {
201
+ return new Promise((resolve, reject) => {
202
+ const timer = setTimeout(() => {
203
+ transport.setListener(void 0);
204
+ reject(/* @__PURE__ */ new Error("hubrpc::initialize: handshake timed out"));
205
+ }, timeoutMs);
206
+ transport.setListener((message) => {
207
+ clearTimeout(timer);
208
+ transport.setListener(void 0);
209
+ resolve(message);
210
+ });
211
+ });
212
+ }
213
+ //#endregion
214
+ //#region src/node/stdio.ts
215
+ /**
216
+ * Spawn a child process and connect to its stdio as a linkrpc connection.
217
+ *
218
+ * Pass a single command line (split on whitespace) or an array `[cmd, ...args]`.
219
+ * The child's stderr is piped to the parent's stderr so logs surface naturally.
220
+ *
221
+ * @example
222
+ * const c = await connectToCmdStdio("node ./ghConnector.js");
223
+ * const gh = c.get(ghInterface);
224
+ * await gh.createIssue({ title: "..." });
225
+ */
226
+ async function connectToCmdStdio(cmd) {
227
+ const parts = typeof cmd === "string" ? cmd.split(/\s+/).filter(Boolean) : [...cmd];
228
+ if (parts.length === 0) throw new Error("connectToCmdStdio: empty command");
229
+ const [bin, ...args] = parts;
230
+ const child = spawn(bin, args, { stdio: [
231
+ "pipe",
232
+ "pipe",
233
+ "inherit"
234
+ ] });
235
+ if (!child.stdin || !child.stdout) throw new Error("connectToCmdStdio: child has no stdio");
236
+ const { transport } = await connectNdjson({
237
+ input: child.stdout,
238
+ output: child.stdin,
239
+ onClose: () => {
240
+ if (!child.killed) child.kill();
241
+ }
242
+ });
243
+ return LinkRpcConnection.fromTransport(transport);
244
+ }
245
+ /**
246
+ * For the child side: a linkrpc connection over this process's stdin/stdout.
247
+ * The child MUST NOT write user output to stdout — only RPC messages — or
248
+ * the parent's parser will choke.
249
+ */
250
+ async function serveOnStdio() {
251
+ const { transport } = await connectNdjson({
252
+ input: process.stdin,
253
+ output: process.stdout
254
+ });
255
+ return LinkRpcConnection.fromTransport(transport);
256
+ }
257
+ async function openStdioChannel() {
258
+ const { transport } = await connectNdjson({
259
+ input: process.stdin,
260
+ output: process.stdout
261
+ });
262
+ const channel = JsonRpcChannel.create(transport);
263
+ const closeListeners = /* @__PURE__ */ new Set();
264
+ let closed = false;
265
+ const fireClose = () => {
266
+ if (closed) return;
267
+ closed = true;
268
+ for (const l of closeListeners) try {
269
+ l();
270
+ } catch {}
271
+ closeListeners.clear();
272
+ };
273
+ process.stdin.on("end", fireClose);
274
+ process.stdin.on("close", fireClose);
275
+ return Object.assign(channel, { onClose: (listener) => {
276
+ if (closed) {
277
+ queueMicrotask(listener);
278
+ return { dispose: () => {} };
279
+ }
280
+ closeListeners.add(listener);
281
+ return { dispose: () => closeListeners.delete(listener) };
282
+ } });
283
+ }
284
+ //#endregion
285
+ //#region src/node/webSocketClientTransport.ts
286
+ /**
287
+ * Client-side WebSocket transport: one JSON-RPC message per text frame.
288
+ * Mirror of the hub's server-side `WebSocketTransport`, kept here so the
289
+ * core node entry can reach a `ws://` / `wss://` hub without depending on
290
+ * `@hediet/linkrpc-hub`.
291
+ *
292
+ * Uses the platform-global `WebSocket` (Node 22+, browsers), so no `ws`
293
+ * package dependency is required on the client side.
294
+ *
295
+ * Takes ownership of the socket: {@link dispose} closes it, and a peer
296
+ * close / error fires `onClose` exactly once.
297
+ */
298
+ var WebSocketTransport = class {
299
+ _ws;
300
+ _onClose;
301
+ _listener;
302
+ _buffer = [];
303
+ _closed = false;
304
+ constructor(_ws, _onClose) {
305
+ this._ws = _ws;
306
+ this._onClose = _onClose;
307
+ _ws.binaryType = "arraybuffer";
308
+ _ws.addEventListener("message", (event) => {
309
+ const data = event.data;
310
+ const text = typeof data === "string" ? data : data instanceof ArrayBuffer ? new TextDecoder().decode(data) : ArrayBuffer.isView(data) ? new TextDecoder().decode(data) : String(data);
311
+ for (const line of text.split("\n")) {
312
+ const trimmed = line.trim();
313
+ if (!trimmed) continue;
314
+ let parsed;
315
+ try {
316
+ parsed = JSON.parse(trimmed);
317
+ } catch {
318
+ continue;
319
+ }
320
+ this._deliver(parsed);
321
+ }
322
+ });
323
+ const onEnd = () => {
324
+ if (this._closed) return;
325
+ this._closed = true;
326
+ this._onClose?.();
327
+ };
328
+ _ws.addEventListener("close", onEnd);
329
+ _ws.addEventListener("error", onEnd);
330
+ }
331
+ send(message) {
332
+ if (this._closed) return;
333
+ if (this._ws.readyState !== WebSocket.OPEN) return;
334
+ this._ws.send(JSON.stringify(message));
335
+ }
336
+ setListener(listener) {
337
+ this._listener = listener;
338
+ if (listener) while (this._buffer.length > 0 && this._listener) {
339
+ const m = this._buffer.shift();
340
+ this._listener(m);
341
+ }
342
+ }
343
+ dispose() {
344
+ if (this._closed) return;
345
+ this._closed = true;
346
+ try {
347
+ this._ws.close();
348
+ } catch {}
349
+ this._onClose?.();
350
+ }
351
+ _deliver(m) {
352
+ if (this._listener) this._listener(m);
353
+ else this._buffer.push(m);
354
+ }
355
+ };
356
+ /**
357
+ * Open a WebSocket to `url` and resolve once it is connected. Rejects if the
358
+ * socket errors before open.
359
+ *
360
+ * Authentication is NOT done here: the hub's WebSocket server authenticates
361
+ * over the in-band `hubrpc::initialize` handshake (so browsers, which cannot
362
+ * set request headers on the `WebSocket` constructor, work too). Run
363
+ * {@link runInitializeHandshake} on the resulting {@link WebSocketTransport}.
364
+ *
365
+ * Headers ride on the upgrade request via the non-standard `headers` option
366
+ * supported by Node's global `WebSocket` (undici); browsers ignore it.
367
+ */
368
+ function openWebSocket(url, opts = {}) {
369
+ const headers = { ...opts.headers };
370
+ return new Promise((resolve, reject) => {
371
+ const ws = new WebSocket(url, { headers });
372
+ const onOpen = () => {
373
+ ws.removeEventListener("error", onError);
374
+ resolve(ws);
375
+ };
376
+ const onError = (event) => {
377
+ ws.removeEventListener("open", onOpen);
378
+ const message = event.message;
379
+ reject(/* @__PURE__ */ new Error(message ? `WebSocket error: ${message}` : "WebSocket connection failed"));
380
+ };
381
+ ws.addEventListener("open", onOpen, { once: true });
382
+ ws.addEventListener("error", onError, { once: true });
383
+ });
384
+ }
385
+ //#endregion
386
+ //#region src/node/hubClient.ts
387
+ /** Env var the hub publishes for its endpoint (named pipe / UDS path). */
388
+ const LINKRPC_ENDPOINT_VAR = "LINKRPC_ENDPOINT";
389
+ /** Env var the hub publishes for its shared attribution token. */
390
+ const LINKRPC_TOKEN_VAR = "LINKRPC_TOKEN";
391
+ /**
392
+ * Open a connection to a running linkrpc hub. Reads `LINKRPC_ENDPOINT` and
393
+ * `LINKRPC_TOKEN` from the environment unless overridden. Sends the single-
394
+ * line `{"hello":1,"token":"..."}` preamble that the hub's socket server
395
+ * requires, then wraps the socket in an `LinkRpcConnection`.
396
+ *
397
+ * Throws if env vars are missing, if the socket cannot be opened, or if the
398
+ * hub refuses the preamble (the socket simply closes — callers should treat
399
+ * an immediate close of the channel as an auth failure).
400
+ */
401
+ async function connectToHub(options = {}) {
402
+ const hubChannel = await openHubChannel(options);
403
+ const signing = {
404
+ principal: options.principal,
405
+ oneShotCaps: options.oneShotCaps
406
+ };
407
+ const channel = SigningSender.wrapChannel(hubChannel, signing);
408
+ const connection = new LinkRpcConnection(channel);
409
+ return {
410
+ connection,
411
+ hub: hubFromConnection(connection),
412
+ endpoint: hubChannel.endpoint,
413
+ token: hubChannel.token,
414
+ signing,
415
+ onClose: hubChannel.onClose,
416
+ close: () => {
417
+ connection.close();
418
+ hubChannel.close();
419
+ }
420
+ };
421
+ }
422
+ /**
423
+ * Open the hub socket and send the preamble, returning the raw
424
+ * {@link HubChannel} without any signing. Use this when you want to
425
+ * compose identity yourself; otherwise prefer {@link connectToHub}.
426
+ */
427
+ async function openHubChannel(options = {}) {
428
+ const endpoint = options.endpoint ?? process.env["LINKRPC_ENDPOINT"];
429
+ const token = options.token ?? process.env["LINKRPC_TOKEN"] ?? "";
430
+ if (!endpoint) throw new Error(`${LINKRPC_ENDPOINT_VAR} is not set; cannot connect to linkrpc hub.`);
431
+ const closeListeners = /* @__PURE__ */ new Set();
432
+ let closed = false;
433
+ let closeChannel;
434
+ const fireClose = () => {
435
+ if (closed) return;
436
+ closed = true;
437
+ closeChannel?.();
438
+ for (const l of closeListeners) try {
439
+ l();
440
+ } catch {}
441
+ closeListeners.clear();
442
+ };
443
+ const { transport, destroy } = await _openHubTransport(endpoint, token, fireClose);
444
+ const rpc = JsonRpcChannel.createWithClose(transport);
445
+ closeChannel = rpc.close;
446
+ if (closed) rpc.close();
447
+ return Object.assign(rpc.channel, {
448
+ endpoint,
449
+ token,
450
+ onClose: (listener) => {
451
+ if (closed) {
452
+ queueMicrotask(listener);
453
+ return { dispose: () => {} };
454
+ }
455
+ closeListeners.add(listener);
456
+ return { dispose: () => closeListeners.delete(listener) };
457
+ },
458
+ close: () => {
459
+ destroy();
460
+ fireClose();
461
+ }
462
+ });
463
+ }
464
+ /** True for `ws://` / `wss://` endpoints (case-insensitive). */
465
+ function _isWebSocketEndpoint(endpoint) {
466
+ return /^wss?:\/\//i.test(endpoint);
467
+ }
468
+ /**
469
+ * Open the underlying transport for {@link openHubChannel}, picking the
470
+ * scheme from the endpoint:
471
+ * - `ws://` / `wss://` → WebSocket; the token is presented via the
472
+ * `hubrpc::initialize` handshake (browsers cannot set request headers).
473
+ * - anything else → named pipe / UDS; the token is presented via the
474
+ * `hubrpc::initialize` handshake the socket server requires.
475
+ *
476
+ * `onClose` fires once when the peer ends the transport.
477
+ */
478
+ async function _openHubTransport(endpoint, token, onClose) {
479
+ if (_isWebSocketEndpoint(endpoint)) {
480
+ const ws = await openWebSocket(endpoint);
481
+ const transport = new WebSocketTransport(ws, onClose);
482
+ const destroy = () => {
483
+ try {
484
+ ws.close();
485
+ } catch {}
486
+ };
487
+ try {
488
+ await runInitializeHandshake(transport, {
489
+ kind: "client",
490
+ token
491
+ });
492
+ } catch (err) {
493
+ transport.dispose();
494
+ destroy();
495
+ throw err;
496
+ }
497
+ return {
498
+ transport,
499
+ destroy
500
+ };
501
+ }
502
+ const socket = await _openSocket(endpoint);
503
+ const { transport } = await connectNdjson({
504
+ input: socket,
505
+ output: socket,
506
+ onClose: () => {
507
+ socket.destroy();
508
+ onClose();
509
+ },
510
+ initialize: {
511
+ kind: "client",
512
+ token
513
+ }
514
+ });
515
+ return {
516
+ transport,
517
+ destroy: () => socket.destroy()
518
+ };
519
+ }
520
+ async function registerHubPrefix(opts) {
521
+ let params;
522
+ if (opts.identity) {
523
+ const signer = new KeypairSigningIdentity(opts.identity.principal, opts.identity.keypair.privateKey);
524
+ params = await signParams({
525
+ method: "hubServiceIdRegistry::registerServiceId",
526
+ params: { requestedPrefix: opts.prefix },
527
+ signingIdentity: signer,
528
+ ...opts.capabilities && opts.capabilities.length > 0 ? { capabilities: [...opts.capabilities] } : {}
529
+ });
530
+ } else {
531
+ if (opts.capabilities && opts.capabilities.length > 0) throw new Error("registerHubPrefix: capabilities require identity to be set");
532
+ params = { requestedPrefix: opts.prefix };
533
+ }
534
+ await opts.handle.connection.channel.sendRequest("hubServiceIdRegistry::registerServiceId", params);
535
+ }
536
+ /**
537
+ * Load the Ed25519 + X25519 keypairs for `id` from disk, or
538
+ * generate-and-persist them on first call. The slot is keyed by SHA-256 of
539
+ * `id` so callers can pass any stable string (e.g. `import.meta.filename`)
540
+ * without worrying about filesystem-safe characters.
541
+ *
542
+ * Identity files written before wrapping support carried only the Ed25519
543
+ * keypair; on load they are transparently migrated — the Ed25519 keypair
544
+ * (and therefore the `nodeId`) is preserved and a fresh X25519 wrap keypair
545
+ * is generated and written back.
546
+ *
547
+ * NOTE: keys are stored unencrypted. Only suitable for local development
548
+ * identities — not for anything that must resist file-system attackers.
549
+ */
550
+ async function loadOrCreateIdentity(opts) {
551
+ const file = opts.file ?? path.join(opts.storeDir ?? _defaultStoreDir(), `${_slotName(opts.id)}.json`);
552
+ await fs.mkdir(path.dirname(file), { recursive: true });
553
+ try {
554
+ const raw = await fs.readFile(file, "utf8");
555
+ const parsed = JSON.parse(raw);
556
+ if (typeof parsed.privateKey !== "string" || typeof parsed.publicKey !== "string") throw new Error("malformed identity file");
557
+ const privateKey = base64UrlToBytes(parsed.privateKey);
558
+ const publicKey = base64UrlToBytes(parsed.publicKey);
559
+ const keypair = {
560
+ privateKey,
561
+ publicKey
562
+ };
563
+ let wrapKeypair;
564
+ if (typeof parsed.wrapPrivateKey === "string" && typeof parsed.wrapPublicKey === "string") wrapKeypair = {
565
+ privateKey: base64UrlToBytes(parsed.wrapPrivateKey),
566
+ publicKey: base64UrlToBytes(parsed.wrapPublicKey)
567
+ };
568
+ else {
569
+ wrapKeypair = await generateX25519Keypair();
570
+ await _writeIdentityFile(file, keypair, wrapKeypair, opts.id);
571
+ }
572
+ return {
573
+ keypair,
574
+ wrapKeypair,
575
+ principal: principalForPublicKey(publicKey),
576
+ file
577
+ };
578
+ } catch (e) {
579
+ if (e.code !== "ENOENT") throw e;
580
+ }
581
+ const keypair = await generateKeypair();
582
+ const wrapKeypair = await generateX25519Keypair();
583
+ await _writeIdentityFile(file, keypair, wrapKeypair, opts.id);
584
+ return {
585
+ keypair,
586
+ wrapKeypair,
587
+ principal: principalForPublicKey(keypair.publicKey),
588
+ file
589
+ };
590
+ }
591
+ async function _writeIdentityFile(file, keypair, wrapKeypair, id) {
592
+ const payload = {
593
+ privateKey: bytesToBase64Url(keypair.privateKey),
594
+ publicKey: bytesToBase64Url(keypair.publicKey),
595
+ wrapPrivateKey: bytesToBase64Url(wrapKeypair.privateKey),
596
+ wrapPublicKey: bytesToBase64Url(wrapKeypair.publicKey),
597
+ id
598
+ };
599
+ await fs.writeFile(file, JSON.stringify(payload, null, 2), { mode: 384 });
600
+ }
601
+ function _defaultStoreDir() {
602
+ const home = os.homedir();
603
+ if (process.platform === "win32") {
604
+ const appdata = process.env.APPDATA;
605
+ if (appdata) return path.join(appdata, "hubrpc", "identities");
606
+ return path.join(home, "AppData", "Roaming", "hubrpc", "identities");
607
+ }
608
+ if (process.platform === "darwin") return path.join(home, "Library", "Application Support", "hubrpc", "identities");
609
+ const xdg = process.env.XDG_CONFIG_HOME;
610
+ if (xdg) return path.join(xdg, "hubrpc", "identities");
611
+ return path.join(home, ".config", "hubrpc", "identities");
612
+ }
613
+ function _slotName(id) {
614
+ const digest = sha256(new TextEncoder().encode(id));
615
+ let out = "";
616
+ for (let i = 0; i < 16; i++) out += digest[i].toString(16).padStart(2, "0");
617
+ return out;
618
+ }
619
+ async function loadPersistedRegisterCap(args) {
620
+ const file = _capFilePath(args.identityId, args.prefix, args.storeDir);
621
+ try {
622
+ const raw = await fs.readFile(file, "utf8");
623
+ const parsed = JSON.parse(raw);
624
+ if (!parsed.capability) return void 0;
625
+ if (parsed.capability.expiresAtMs !== void 0 && parsed.capability.expiresAtMs < Date.now()) return;
626
+ return {
627
+ capability: parsed.capability,
628
+ file
629
+ };
630
+ } catch (e) {
631
+ if (e.code !== "ENOENT") throw e;
632
+ return;
633
+ }
634
+ }
635
+ async function persistRegisterCap(args) {
636
+ const file = _capFilePath(args.identityId, args.prefix, args.storeDir);
637
+ await fs.mkdir(path.dirname(file), { recursive: true });
638
+ await fs.writeFile(file, JSON.stringify({ capability: args.capability }, null, 2), { mode: 384 });
639
+ return file;
640
+ }
641
+ function _capFilePath(identityId, prefix, storeDir) {
642
+ const dir = storeDir ?? _defaultStoreDir();
643
+ const slot = _slotName(identityId);
644
+ return path.join(dir, `${slot}-cap-${_slotName(prefix)}.json`);
645
+ }
646
+ function _openSocket(endpoint) {
647
+ return new Promise((resolve, reject) => {
648
+ const socket = net.createConnection(endpoint);
649
+ const onError = (err) => {
650
+ socket.removeListener("connect", onConnect);
651
+ reject(err);
652
+ };
653
+ const onConnect = () => {
654
+ socket.removeListener("error", onError);
655
+ resolve(socket);
656
+ };
657
+ socket.once("error", onError);
658
+ socket.once("connect", onConnect);
659
+ });
660
+ }
661
+ //#endregion
662
+ //#region src/node/hubReconnect.ts
663
+ /**
664
+ * Connect to the hub, register `prefix`, run `setup`, and keep the
665
+ * connection alive across hub restarts. When the socket closes, retries
666
+ * with exponential backoff (re-reading `LINKRPC_ENDPOINT` / `LINKRPC_TOKEN`
667
+ * from `process.env` on each attempt so rotated tokens published by the
668
+ * hub host get picked up if the parent shell re-exports them).
669
+ *
670
+ * The hub keeps recently-rotated tokens accepted for a grace window, so
671
+ * even processes that captured the env at spawn time can usually reconnect
672
+ * after a host reload without the parent re-exporting anything.
673
+ */
674
+ function serveOverHubWithReconnect(options) {
675
+ const log = _makeLog(options.log, options.name ?? options.prefix);
676
+ const initial = options.initialBackoffMs ?? 200;
677
+ const max = options.maxBackoffMs ?? 5e3;
678
+ let stopped = false;
679
+ let current;
680
+ const stop = () => {
681
+ if (stopped) return;
682
+ stopped = true;
683
+ current?.close();
684
+ current = void 0;
685
+ };
686
+ options.signal?.addEventListener("abort", stop, { once: true });
687
+ const claimer = options.identity ? createHubPrefixClaimer({
688
+ prefix: options.prefix,
689
+ identity: options.identity,
690
+ log
691
+ }) : void 0;
692
+ return {
693
+ stop,
694
+ done: (async () => {
695
+ let attempt = 0;
696
+ let backoff = initial;
697
+ while (!stopped) try {
698
+ if (attempt > 0) log(`reconnecting (attempt ${attempt + 1})…`);
699
+ const handle = await connectToHub();
700
+ current = handle;
701
+ if (claimer) await claimer.claim(handle);
702
+ else await registerHubPrefix({
703
+ handle,
704
+ prefix: options.prefix
705
+ });
706
+ await options.setup(handle.connection);
707
+ log(attempt === 0 ? `connected to hub at ${handle.endpoint} (prefix "${options.prefix}")` : `reconnected to hub (prefix "${options.prefix}")`);
708
+ attempt = 0;
709
+ backoff = initial;
710
+ await new Promise((resolve) => {
711
+ handle.onClose(() => resolve());
712
+ });
713
+ if (stopped) break;
714
+ log(`disconnected from hub`);
715
+ current = void 0;
716
+ } catch (err) {
717
+ if (stopped) break;
718
+ current?.close();
719
+ current = void 0;
720
+ const msg = err instanceof Error ? err.message : String(err);
721
+ log(`connect failed: ${msg}; retrying in ${backoff} ms`);
722
+ await _delay(backoff, options.signal);
723
+ attempt++;
724
+ backoff = Math.min(max, backoff * 2);
725
+ }
726
+ })()
727
+ };
728
+ }
729
+ /**
730
+ * Build a reusable, stateful claimer for the signed register + capability
731
+ * flow. Use this with {@link connectService}'s `reconnecting` connector to
732
+ * claim a prefix on each (re)connect:
733
+ *
734
+ * ```ts
735
+ * const claimer = createHubPrefixClaimer({ prefix, identity: { slot } });
736
+ * connectService({
737
+ * connector: reconnecting(async () => {
738
+ * const handle = await connectToHub();
739
+ * try { await claimer.claim(handle); } catch (e) { handle.close(); throw e; }
740
+ * return handle;
741
+ * }),
742
+ * onConnect: ({ connection }) => registerService(connection),
743
+ * });
744
+ * ```
745
+ */
746
+ function createHubPrefixClaimer(opts) {
747
+ const log = opts.log ?? (() => {});
748
+ let identity;
749
+ let cachedCap;
750
+ let capLoaded = false;
751
+ return { async claim(handle) {
752
+ if (!identity) {
753
+ const loaded = await loadOrCreateIdentity({
754
+ id: opts.identity.slot,
755
+ ...opts.identity.storeDir !== void 0 ? { storeDir: opts.identity.storeDir } : {}
756
+ });
757
+ identity = {
758
+ principal: loaded.principal,
759
+ keypair: loaded.keypair
760
+ };
761
+ }
762
+ if (!capLoaded) {
763
+ cachedCap = (await loadPersistedRegisterCap({
764
+ identityId: opts.identity.slot,
765
+ prefix: opts.prefix,
766
+ ...opts.identity.storeDir !== void 0 ? { storeDir: opts.identity.storeDir } : {}
767
+ }))?.capability;
768
+ capLoaded = true;
769
+ }
770
+ cachedCap = await _registerSigned(handle, opts.prefix, identity, cachedCap, opts.identity, log);
771
+ } };
772
+ }
773
+ const DEFAULT_ISSUER_METHOD = "hub-admin::hub-admin.capabilities::issueRegisterCap";
774
+ /**
775
+ * Try `registerHubPrefix` with `cachedCap`. On `permissionRequired`, call
776
+ * the hub-admin issuer to mint a fresh cap, persist it, retry. Returns the
777
+ * cap that actually worked (caller stores it for the next reconnect).
778
+ */
779
+ async function _registerSigned(handle, prefix, identity, cachedCap, identityOpts, log) {
780
+ try {
781
+ await registerHubPrefix({
782
+ handle,
783
+ prefix,
784
+ identity,
785
+ ...cachedCap ? { capabilities: [cachedCap] } : {}
786
+ });
787
+ return cachedCap;
788
+ } catch (e) {
789
+ const msg = e instanceof Error ? e.message : String(e);
790
+ if (!_isPermissionRequired(e) || identityOpts.issuerMethod === null) throw e;
791
+ log(`register denied (${msg}); requesting capability from hub-admin issuer…`);
792
+ }
793
+ const issuerMethod = identityOpts.issuerMethod ?? DEFAULT_ISSUER_METHOD;
794
+ const signer = new KeypairSigningIdentity(identity.principal, identity.keypair.privateKey);
795
+ const signedIssueParams = await signParams({
796
+ method: issuerMethod,
797
+ params: {
798
+ audience: identity.principal,
799
+ prefix,
800
+ ...identityOpts.purpose !== void 0 ? { purpose: identityOpts.purpose } : {}
801
+ },
802
+ signingIdentity: signer
803
+ });
804
+ const issueResp = await handle.connection.channel.sendRequest(issuerMethod, signedIssueParams);
805
+ if (!issueResp?.capability) throw new Error("hub-admin issuer returned no capability");
806
+ const fresh = issueResp.capability;
807
+ await persistRegisterCap({
808
+ identityId: identityOpts.slot,
809
+ prefix,
810
+ capability: fresh,
811
+ ...identityOpts.storeDir !== void 0 ? { storeDir: identityOpts.storeDir } : {}
812
+ });
813
+ await registerHubPrefix({
814
+ handle,
815
+ prefix,
816
+ identity,
817
+ capabilities: [fresh]
818
+ });
819
+ return fresh;
820
+ }
821
+ function _isPermissionRequired(e) {
822
+ if (e === null || typeof e !== "object") return false;
823
+ const rec = e;
824
+ if (typeof rec.code === "number") return rec.code === ErrorCode.permissionRequired;
825
+ const msg = e.message ?? "";
826
+ return /permission required/i.test(msg);
827
+ }
828
+ function _makeLog(sink, name) {
829
+ if (!sink) return () => {};
830
+ const write = sink === true ? (line) => {
831
+ process.stderr.write(line + "\n");
832
+ } : sink;
833
+ return (line) => write(`[linkrpc:${name}] ${line}`);
834
+ }
835
+ function _delay(ms, signal) {
836
+ return new Promise((resolve) => {
837
+ if (signal?.aborted) {
838
+ resolve();
839
+ return;
840
+ }
841
+ const t = setTimeout(() => {
842
+ signal?.removeEventListener("abort", onAbort);
843
+ resolve();
844
+ }, ms);
845
+ const onAbort = () => {
846
+ clearTimeout(t);
847
+ resolve();
848
+ };
849
+ signal?.addEventListener("abort", onAbort, { once: true });
850
+ });
851
+ }
852
+ //#endregion
853
+ //#region src/node/fileManagedIdentityStorage.ts
854
+ /**
855
+ * A {@link ManagedIdentityStorage} backed by a single JSON file (one object
856
+ * keyed by the storage keys). Suitable for self-managed identities that
857
+ * persist their capability bag locally — e.g. pass it to `CapBag.load`.
858
+ *
859
+ * The file is created (mode 0o600) on first write; reads of a missing file
860
+ * yield an empty store. Not concurrency-safe across processes.
861
+ */
862
+ function fileManagedIdentityStorage(file) {
863
+ const read = async () => {
864
+ try {
865
+ return JSON.parse(await readFile(file, "utf8"));
866
+ } catch (e) {
867
+ if (e.code === "ENOENT") return {};
868
+ throw e;
869
+ }
870
+ };
871
+ const write = async (data) => {
872
+ await mkdir(dirname(file), { recursive: true });
873
+ await writeFile(file, JSON.stringify(data, null, 2), { mode: 384 });
874
+ };
875
+ return {
876
+ get: async (key) => (await read())[key],
877
+ set: async (key, value) => {
878
+ const data = await read();
879
+ data[key] = value;
880
+ await write(data);
881
+ },
882
+ delete: async (key) => {
883
+ const data = await read();
884
+ const existed = key in data;
885
+ delete data[key];
886
+ await write(data);
887
+ return existed;
888
+ },
889
+ list: async (prefix = "") => Object.keys(await read()).filter((k) => k.startsWith(prefix))
890
+ };
891
+ }
892
+ //#endregion
893
+ //#region src/node/principal.ts
894
+ /**
895
+ * Self-managed principal: a local Ed25519 + X25519 keypair (created/loaded on
896
+ * disk) signs and wraps/unwraps for outbound calls, and granted caps are
897
+ * cached in a sibling file. Purely local — no transport needed — and
898
+ * cheap/idempotent, so it can be re-derived on each reconnect. Used with
899
+ * `--use-non-managed-identity`.
900
+ */
901
+ async function createSelfManagedPrincipal(identityKey) {
902
+ return _selfManagedPrincipal({ id: identityKey });
903
+ }
904
+ /**
905
+ * Self-managed principal whose keypair lives at an explicit file path (rather
906
+ * than a slot derived from an id). Caps are cached in a sibling `.caps.json`.
907
+ * Used by callers that want to pin the identity to a specific file, e.g. the
908
+ * CLI's `--principal file:<path>`.
909
+ */
910
+ async function createSelfManagedPrincipalFromFile(file) {
911
+ return _selfManagedPrincipal({
912
+ id: `file:${file}`,
913
+ file
914
+ });
915
+ }
916
+ async function _selfManagedPrincipal(opts) {
917
+ const id = await loadOrCreateIdentity({
918
+ id: opts.id,
919
+ file: opts.file
920
+ });
921
+ const capsFile = id.file.endsWith(".json") ? id.file.replace(/\.json$/, ".caps.json") : `${id.file}.caps.json`;
922
+ const capBag = await CapBag.load({ storage: fileManagedIdentityStorage(capsFile) });
923
+ const identity = new KeypairIdentity(id.principal, id.keypair.privateKey, id.wrapKeypair);
924
+ return new Principal(identity, capBag);
925
+ }
926
+ //#endregion
927
+ //#region src/node/headerDelimitedTransport.ts
928
+ const HEADER_SEPARATOR = Buffer.from("\r\n\r\n");
929
+ const DEFAULT_MAX_HEADER_BYTES = 65536;
930
+ const DEFAULT_MAX_CONTENT_LENGTH = 67108864;
931
+ const DEFAULT_MAX_PENDING_MESSAGES = 1024;
932
+ /**
933
+ * LSP "base protocol" framing over a Node Readable/Writable pair.
934
+ *
935
+ * The streams remain owned by the caller: close/dispose detach listeners but
936
+ * never end, destroy, or otherwise take ownership of either stream.
937
+ */
938
+ var HeaderDelimitedTransport = class {
939
+ _input;
940
+ _output;
941
+ _onClose;
942
+ _listener;
943
+ _pending = [];
944
+ _residual = Buffer.alloc(0);
945
+ _expectedLength;
946
+ _closed = false;
947
+ _closeReason;
948
+ _maxHeaderBytes;
949
+ _maxContentLength;
950
+ _maxPendingMessages;
951
+ _onDataListener = (chunk) => this._onData(chunk);
952
+ _onEndListener = () => this._onEnd();
953
+ _onInputErrorListener = (error) => this.close(`Header-delimited input error: ${error.message}`);
954
+ _onOutputErrorListener = (error) => this.close(`Header-delimited output error: ${error.message}`);
955
+ constructor(_input, _output, _onClose, options = {}) {
956
+ this._input = _input;
957
+ this._output = _output;
958
+ this._onClose = _onClose;
959
+ this._maxHeaderBytes = positiveLimit(options.maxHeaderBytes, DEFAULT_MAX_HEADER_BYTES, "maxHeaderBytes");
960
+ this._maxContentLength = positiveLimit(options.maxContentLength, DEFAULT_MAX_CONTENT_LENGTH, "maxContentLength");
961
+ this._maxPendingMessages = positiveLimit(options.maxPendingMessages, DEFAULT_MAX_PENDING_MESSAGES, "maxPendingMessages");
962
+ _input.on("data", this._onDataListener);
963
+ _input.on("end", this._onEndListener);
964
+ _input.on("close", this._onEndListener);
965
+ _input.on("error", this._onInputErrorListener);
966
+ _output.on("error", this._onOutputErrorListener);
967
+ }
968
+ get closed() {
969
+ return this._closed;
970
+ }
971
+ get closeReason() {
972
+ return this._closeReason;
973
+ }
974
+ send(message) {
975
+ if (this._closed) return Promise.reject(new Error(this._closeReason ?? "Header-delimited transport is closed"));
976
+ const body = Buffer.from(JSON.stringify(message), "utf8");
977
+ if (body.length > this._maxContentLength) return Promise.reject(/* @__PURE__ */ new Error(`Header-delimited payload exceeds ${this._maxContentLength} bytes`));
978
+ const wire = Buffer.concat([Buffer.from(`Content-Length: ${body.length}\r\n\r\n`, "ascii"), body]);
979
+ return new Promise((resolve, reject) => {
980
+ this._output.write(wire, (error) => {
981
+ if (error) reject(error);
982
+ else resolve();
983
+ });
984
+ });
985
+ }
986
+ setListener(listener) {
987
+ this._listener = listener;
988
+ while (this._listener && this._pending.length > 0) this._listener(this._pending.shift());
989
+ }
990
+ dispose() {
991
+ this.close("Header-delimited transport disposed");
992
+ }
993
+ close(reason = "Header-delimited transport closed") {
994
+ if (this._closed) return;
995
+ this._closed = true;
996
+ this._closeReason = reason;
997
+ this._input.off("data", this._onDataListener);
998
+ this._input.off("end", this._onEndListener);
999
+ this._input.off("close", this._onEndListener);
1000
+ this._input.off("error", this._onInputErrorListener);
1001
+ this._output.off("error", this._onOutputErrorListener);
1002
+ this._onClose?.(reason);
1003
+ }
1004
+ _onData(chunk) {
1005
+ if (this._closed) return;
1006
+ const bytes = typeof chunk === "string" ? Buffer.from(chunk, "utf8") : chunk;
1007
+ this._residual = this._residual.length === 0 ? Buffer.from(bytes) : Buffer.concat([this._residual, bytes]);
1008
+ while (!this._closed) {
1009
+ if (this._expectedLength === void 0) {
1010
+ const headerEnd = this._residual.indexOf(HEADER_SEPARATOR);
1011
+ if (headerEnd < 0) {
1012
+ if (this._residual.length > this._maxHeaderBytes) this.close(`Invalid header-delimited frame: header exceeds ${this._maxHeaderBytes} bytes`);
1013
+ return;
1014
+ }
1015
+ if (headerEnd + HEADER_SEPARATOR.length > this._maxHeaderBytes) {
1016
+ this.close(`Invalid header-delimited frame: header exceeds ${this._maxHeaderBytes} bytes`);
1017
+ return;
1018
+ }
1019
+ try {
1020
+ this._expectedLength = parseHeaders(this._residual.subarray(0, headerEnd), this._maxContentLength);
1021
+ } catch (error) {
1022
+ this.close(`Invalid header-delimited frame: ${error instanceof Error ? error.message : String(error)}`);
1023
+ return;
1024
+ }
1025
+ this._residual = this._residual.subarray(headerEnd + HEADER_SEPARATOR.length);
1026
+ }
1027
+ if (this._residual.length < this._expectedLength) return;
1028
+ const body = this._residual.subarray(0, this._expectedLength);
1029
+ this._residual = this._residual.subarray(this._expectedLength);
1030
+ this._expectedLength = void 0;
1031
+ let parsed;
1032
+ try {
1033
+ parsed = JSON.parse(body.toString("utf8"));
1034
+ } catch (error) {
1035
+ this.close(`Invalid header-delimited JSON payload: ${error instanceof Error ? error.message : String(error)}`);
1036
+ return;
1037
+ }
1038
+ this._deliver(parsed);
1039
+ }
1040
+ }
1041
+ _onEnd() {
1042
+ if (this._closed) return;
1043
+ this.close(this._expectedLength !== void 0 || this._residual.length > 0 ? "Truncated header-delimited frame" : "Header-delimited input closed");
1044
+ }
1045
+ _deliver(message) {
1046
+ if (this._listener) {
1047
+ this._listener(message);
1048
+ return;
1049
+ }
1050
+ if (this._pending.length >= this._maxPendingMessages) {
1051
+ this.close(`Header-delimited pending message limit ${this._maxPendingMessages} exceeded`);
1052
+ return;
1053
+ }
1054
+ this._pending.push(message);
1055
+ }
1056
+ };
1057
+ function parseHeaders(headerBytes, maxContentLength) {
1058
+ const values = /* @__PURE__ */ new Map();
1059
+ for (const line of headerBytes.toString("ascii").split("\r\n")) {
1060
+ const colon = line.indexOf(":");
1061
+ if (colon <= 0) throw new Error(`malformed header ${JSON.stringify(line)}`);
1062
+ const name = line.slice(0, colon).trim().toLowerCase();
1063
+ const value = line.slice(colon + 1).trim();
1064
+ if (values.has(name)) throw new Error(`duplicate ${name} header`);
1065
+ values.set(name, value);
1066
+ }
1067
+ const rawLength = values.get("content-length");
1068
+ if (rawLength === void 0 || !/^(0|[1-9]\d*)$/.test(rawLength)) throw new Error("missing or invalid Content-Length header");
1069
+ const length = Number(rawLength);
1070
+ if (!Number.isSafeInteger(length) || length > maxContentLength) throw new Error(`Content-Length exceeds ${maxContentLength} bytes`);
1071
+ const contentType = values.get("content-type");
1072
+ if (contentType !== void 0 && !isSupportedContentType(contentType)) throw new Error(`unsupported Content-Type ${JSON.stringify(contentType)}`);
1073
+ return length;
1074
+ }
1075
+ function isSupportedContentType(value) {
1076
+ const parts = value.split(";").map((part) => part.trim().toLowerCase());
1077
+ return parts[0] === "application/vscode-jsonrpc" && parts.slice(1).every((part) => /^charset\s*=\s*"?utf-?8"?$/.test(part));
1078
+ }
1079
+ function positiveLimit(value, fallback, name) {
1080
+ const result = value ?? fallback;
1081
+ if (!Number.isSafeInteger(result) || result <= 0) throw new Error(`${name} must be a positive integer`);
1082
+ return result;
1083
+ }
1084
+ //#endregion
1085
+ export { CapBag, Channel, ChannelConnector, DEFAULT_RPC_TIMEOUT_MS, EXTENSION_PREFIX, ErrorCode, HUBRPC_META_KEY, HUBRPC_SIGNATURE_KEY, HUBRPC_UNSIGNED_KEY, HeaderDelimitedTransport, INITIALIZE_METHOD, INITIALIZE_PROTOCOL_VERSION, InMemoryManagedIdentity, InMemoryManagedIdentityStorage, InterfaceDefinition, JsonRpcChannel, KEY_ID_PREFIX, KeypairIdentity, KeypairSigningIdentity, LINKRPC_ENDPOINT_VAR, LINKRPC_INITIALIZE_METHOD_ALIAS, LINKRPC_META_KEY, LINKRPC_SIGNATURE_KEY, LINKRPC_TOKEN_VAR, LINKRPC_UNSIGNED_KEY, LinkRpcConnection, MultiplexedTransport, NotificationType, OneShotCapStaging, PRINCIPAL_PREFIX, Principal, PrincipalWithStore, PublicSigningIdentity, PublicWrappingIdentity, RequestType, RpcError, STREAM_METHOD, ServiceHandle, SigningSender, StreamControlReason, StreamControlType, StreamDir, TransportPair, WebSocketTransport, assertSchemaReferences, attachCapabilities, bareInterfaceTarget, base64UrlToBytes, bytesToBase64Url, capBagFreshAt, capabilityFreshAt, capabilityPermits, componentSchemaName, componentSchemaRef, computeInterfaceHash, connectNdjson, connectToCmdStdio, connectToHub, connectTransports, createHubPrefixClaimer, createManagedIdentity, createManagedPrincipal, createMemoryPrincipal, createSchemaToZod, createSeededMemoryPrincipal, createSeededSigningIdentity, createSelfManagedPrincipal, createSelfManagedPrincipalFromFile, crypto_exports as crypto, defaultsInterface, defineInterface, directoryInterface, directoryWatchNever, fileManagedIdentityStorage, formatEndpointUri, generateTsInterface, getKeyId, hasSignedCapabilityShape, identityInterface, identityStorageInterface, interfaceFromSchema, invoke, isAssignable, isHubEndpoint, isNotification, isRequest, isResponse, issueCapability, jcsCanonicalize, jcsCanonicalizeBytes, keyIdForPrincipal, keyIdForPublicKey, loadOrCreateIdentity, matchParams, materializeJsonSchema, methodNameToTarget, nodeInterface, normalizeJsonSchema, notificationType, openHubChannel, openStdioChannel, openWebSocket, parseEndpointUri, parseMethodName, permissionMatchesTarget, permissionPermits, permits, prefix, principalForPublicKey, publicKeyForKeyId, readSignature, registerHubPrefix, registerIdentityOnOverlay, registerLazyIdentityOnOverlay, requestType, requireObjectParams, resolveSigningKey, runInitializeHandshake, schemaToZod, schemasInterface, serveOnStdio, serveOverHubWithReconnect, signCapability, signParams, signRpcCall, signedHash, signingDomainValue, signingInput, streamInterface, stripLinkRpcWireMeta, topologyInterface, traceMessageTransport, trafficInterface, verifyCall, verifyRpcCall, withRpcTimeout, withSignature, zParticipantDescriptor, zParticipantDescriptorSource, zRouteClaim, zServiceIdPattern, zTopologyGraph, zTopologyLink, zTopologyLinkEndpoint, zTopologyNode, zTopologyPort, zTopologyTransportEndpoint, zTopologyTransportInfo, zTrafficEvent, zTrafficOverflowEvent, zTrafficTransitEndpoint, zTrafficTransitEvent, zTrafficWatchResult, zodToSvcJsonSchema };
1086
+
1087
+ //# sourceMappingURL=node.js.map