@hediet/linkrpc-hub 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +101 -0
  2. package/dist/chunks/config-BCvkg7jv.d.ts +566 -0
  3. package/dist/chunks/configFile-y6Phntqu.d.ts +9 -0
  4. package/dist/chunks/connectionTokenBinder.interfaces-B-beCg06.js +40 -0
  5. package/dist/chunks/connectionTokenBinder.interfaces-B-beCg06.js.map +1 -0
  6. package/dist/chunks/connectionTokenBinder.interfaces-BhzQ1DTS.d.ts +36 -0
  7. package/dist/chunks/hubConnectionAcceptor-B5-8JsFY.js +2768 -0
  8. package/dist/chunks/hubConnectionAcceptor-B5-8JsFY.js.map +1 -0
  9. package/dist/chunks/hubConnectionAcceptor-BwydvFa4.d.ts +1560 -0
  10. package/dist/chunks/index-CLIUrV88.d.ts +481 -0
  11. package/dist/chunks/node-CTXsQ6oa.js +460 -0
  12. package/dist/chunks/node-CTXsQ6oa.js.map +1 -0
  13. package/dist/chunks/nodeTransit-CWeFnbwt.js +444 -0
  14. package/dist/chunks/nodeTransit-CWeFnbwt.js.map +1 -0
  15. package/dist/chunks/nodeTransit-cmtZgdpW.d.ts +226 -0
  16. package/dist/chunks/runHub-P3YUwwdv.js +1797 -0
  17. package/dist/chunks/runHub-P3YUwwdv.js.map +1 -0
  18. package/dist/chunks/server-BAxchQhy.js +1368 -0
  19. package/dist/chunks/server-BAxchQhy.js.map +1 -0
  20. package/dist/cli.d.ts +1 -0
  21. package/dist/cli.js +38 -0
  22. package/dist/cli.js.map +1 -0
  23. package/dist/config.d.ts +2 -0
  24. package/dist/config.js +305 -0
  25. package/dist/config.js.map +1 -0
  26. package/dist/configFile.d.ts +2 -0
  27. package/dist/configFile.js +27 -0
  28. package/dist/configFile.js.map +1 -0
  29. package/dist/engine/runHub.d.ts +42 -0
  30. package/dist/engine/runHub.js +2 -0
  31. package/dist/hub/server/client.d.ts +2 -0
  32. package/dist/hub/server/client.js +2 -0
  33. package/dist/hub/server/connectionTokenBinder.d.ts +2 -0
  34. package/dist/hub/server/connectionTokenBinder.js +2 -0
  35. package/dist/hub/server/index.d.ts +5 -0
  36. package/dist/hub/server/index.js +5 -0
  37. package/dist/hub/server/node/index.d.ts +218 -0
  38. package/dist/hub/server/node/index.js +2 -0
  39. package/dist/hub/server/transit.d.ts +2 -0
  40. package/dist/hub/server/transit.js +2 -0
  41. package/dist/index.d.ts +512 -0
  42. package/dist/index.js +309 -0
  43. package/dist/index.js.map +1 -0
  44. package/dist/serve.d.ts +14 -0
  45. package/dist/serve.js +31 -0
  46. package/dist/serve.js.map +1 -0
  47. package/dist/spawn.d.ts +12 -0
  48. package/dist/spawn.js +25 -0
  49. package/dist/spawn.js.map +1 -0
  50. package/package.json +83 -0
package/dist/index.js ADDED
@@ -0,0 +1,309 @@
1
+ import { ConnectionHandlerSchema, ConnectionTokenBinderSchema, ConsentApproverSchema, EndpointConfigSchema, ForwardCheckingSchema, HubConfigSchema, ListenerConfigSchema, ParticipantConnectorConfigSchema, hubConfigJsonSchema, parseHubConfig } from "./config.js";
2
+ import { a as ApproveClient, c as buildSlotPermissions, d as computeCallBindHash, f as createTerminalConsentPrompt, h as toSignedPermissions, i as runManifestApprover, l as createHubAccessConfig, m as raceConsent, n as configureParticipants, o as HubAccessManifestHost, p as describePermissions, r as validateConfiguredParticipants, s as registerHubAccessManifest, t as runHub, u as describeRequest } from "./chunks/runHub-P3YUwwdv.js";
3
+ import { i as formatFlowSummary, t as TrafficFlowAggregator } from "./chunks/nodeTransit-CWeFnbwt.js";
4
+ import { c as durationToExp, s as CapabilityProposalIssuer, t as createIdentityKeystore } from "./chunks/server-BAxchQhy.js";
5
+ import { spawnCommand } from "./spawn.js";
6
+ import { loadHubConfig, printHubConfigSchema } from "./configFile.js";
7
+ import { serveHub } from "./serve.js";
8
+ import { DEFAULT_RPC_TIMEOUT_MS, withRpcTimeout } from "@hediet/linkrpc";
9
+ import { HubDirectoryExplorer, hubAccessManifestInterface } from "@hediet/linkrpc/hub/common";
10
+ import { safeParse } from "zod/v4/core";
11
+ //#region src/engine/hubAccessGrantSigner.ts
12
+ var HubAccessGrantSigner = class {
13
+ _grant;
14
+ _issuer;
15
+ constructor(signingIdentity, _grant) {
16
+ this._grant = _grant;
17
+ this._issuer = new CapabilityProposalIssuer(signingIdentity);
18
+ }
19
+ /**
20
+ * The {@link AccessDecider}: consult the grant policy and, on `permitted`,
21
+ * mint a capability with the held identity (audience = the consumer,
22
+ * honoring "Allow once" `callBind` byte-binding for one-shot durations).
23
+ */
24
+ decide = async (ctx) => {
25
+ const verdict = await this._grant(ctx);
26
+ if (verdict.result === "rejected") return verdict.reason !== void 0 ? {
27
+ grant: false,
28
+ reason: verdict.reason
29
+ } : { grant: false };
30
+ return {
31
+ grant: true,
32
+ capabilities: [await this._mint(ctx)]
33
+ };
34
+ };
35
+ async _mint(ctx) {
36
+ return this._issuer.mint({
37
+ audience: ctx.consumerPrincipalId,
38
+ permissions: await toSignedPermissions(ctx.permissions, ctx.consumerPrincipalId, ctx.duration),
39
+ expiresAtMs: durationToExp(ctx.duration)
40
+ });
41
+ }
42
+ };
43
+ //#endregion
44
+ //#region src/engine/aggregatingManifest.ts
45
+ /**
46
+ * `AggregatingHubAccessManifest` — a `manifest* → manifest` combinator.
47
+ *
48
+ * It implements the same {@link IHubAccessManifest} surface the approver/UI
49
+ * consumes, but backs it by **many** source manifests: a trusted *local* source
50
+ * (the participant's own {@link HubAccessManifestHost}, reached over an in-memory
51
+ * loopback) plus zero or more *remote* sources it discovers by scanning the hub
52
+ * directory for `hubAccessManifest` interfaces. So the pipeline composes:
53
+ *
54
+ * hubAccess ─(HubAccessManifestHost)→ manifest_local ─┐
55
+ * discovered ───────────────────────→ manifest_remote*┤─(aggregator)→ manifest → UI
56
+ *
57
+ * Two invariants make aggregation safe:
58
+ *
59
+ * 1. **Namespaced entry ids.** Each source's entryIds are prefixed with the
60
+ * source's `tag`, so the merged id space has no collisions and a `setCurrent`
61
+ * decision can be routed back to exactly the source that parked it.
62
+ *
63
+ * 2. **Origin is preserved only for trusted-local sources.** An entry's
64
+ * host-authored `origin` bag (e.g. `{ sourceTransportId }`) routes a *local*
65
+ * consent UI. A remote source's `origin` refers to a transport on *another*
66
+ * hub — passing it through as-if-local would let a remote address a local
67
+ * iframe. So `preserveOrigin` is opt-in per source (true for the local
68
+ * loopback, false for everything discovered); a stripped entry has no
69
+ * `sourceTransportId`, so the UI falls back to its panel — correct by
70
+ * construction.
71
+ *
72
+ * Discovery + reads happen over the caller-supplied connection; reaching a
73
+ * remote `hubAccessManifest::{getDesired,setCurrent}` is a forwarded (form-3)
74
+ * call, so that connection must carry an admin-rooted **capability** for
75
+ * `*::hubAccessManifest::*` (and the directory) — without it the gate rejects the
76
+ * scan. This is the aggregator's whole reason to hold the approver's identity.
77
+ */
78
+ /** The declared result schema for `hubAccessManifest::getDesired` — used to validate each source. */
79
+ const GET_DESIRED_RESULT_SCHEMA = hubAccessManifestInterface.members.getDesired.resultSchema;
80
+ /** Delimiter between a source `tag` and the source-local entryId. Absent from serviceIds/UUIDs. */
81
+ const SEP = "\0";
82
+ var AggregatingHubAccessManifest = class {
83
+ _options;
84
+ _listeners = /* @__PURE__ */ new Set();
85
+ _revision = 0;
86
+ /** tag → source, refreshed on every {@link _refresh}. Used to route `setCurrent`. */
87
+ _byTag = /* @__PURE__ */ new Map();
88
+ /** tag → resilient watch for that exact source incarnation. */
89
+ _watches = /* @__PURE__ */ new Map();
90
+ /** Teardown for the source-set watch (e.g. directory watch), started lazily. */
91
+ _sourcesWatchOff;
92
+ _refreshPromise;
93
+ _disposed = false;
94
+ constructor(_options) {
95
+ this._options = _options;
96
+ }
97
+ /** Fire whenever any source's desired set may have changed (drives `watchDesired`). */
98
+ onDidChange(cb) {
99
+ this._listeners.add(cb);
100
+ if (this._options.watchSources !== void 0 && this._sourcesWatchOff === void 0) this._sourcesWatchOff = this._options.watchSources(() => {
101
+ if (!this._disposed) this._emit();
102
+ });
103
+ this._refresh().catch(() => {});
104
+ return () => this._listeners.delete(cb);
105
+ }
106
+ _emit() {
107
+ this._revision++;
108
+ for (const l of this._listeners) try {
109
+ l();
110
+ } catch {}
111
+ }
112
+ /** Resolve the source set, (un)subscribing watches to match, and cache it by tag. */
113
+ _refresh() {
114
+ if (!this._refreshPromise) this._refreshPromise = this._doRefresh().finally(() => {
115
+ this._refreshPromise = void 0;
116
+ });
117
+ return this._refreshPromise;
118
+ }
119
+ async _doRefresh() {
120
+ const sources = await this._options.resolveSources();
121
+ const next = new Map(sources.map((s) => [s.tag, s]));
122
+ for (const [tag, watch] of this._watches) {
123
+ const nextSource = next.get(tag);
124
+ if (!nextSource || nextSource.manifest !== watch.source.manifest) {
125
+ this._closeWatch(watch);
126
+ this._watches.delete(tag);
127
+ }
128
+ }
129
+ this._byTag = next;
130
+ for (const s of sources) if (!this._watches.has(s.tag)) this._openWatch(s);
131
+ return sources;
132
+ }
133
+ _openWatch(s) {
134
+ const watch = { source: s };
135
+ this._watches.set(s.tag, watch);
136
+ this._startWatch(watch);
137
+ }
138
+ _startWatch(watch) {
139
+ if (this._disposed || this._watches.get(watch.source.tag) !== watch) return;
140
+ try {
141
+ const call = watch.source.manifest.watchDesired({}, { onMessage: () => {
142
+ if (!this._disposed) this._emit();
143
+ } });
144
+ watch.call = call;
145
+ call.then(() => this._watchEnded(watch), () => this._watchEnded(watch));
146
+ } catch {
147
+ this._watchEnded(watch);
148
+ }
149
+ }
150
+ _watchEnded(watch) {
151
+ if (this._disposed || this._watches.get(watch.source.tag) !== watch) return;
152
+ watch.call = void 0;
153
+ if (watch.retryTimer !== void 0) return;
154
+ watch.retryTimer = setTimeout(() => {
155
+ watch.retryTimer = void 0;
156
+ if (this._disposed || this._watches.get(watch.source.tag) !== watch) return;
157
+ this._startWatch(watch);
158
+ this._emit();
159
+ }, 250);
160
+ }
161
+ _closeWatch(watch) {
162
+ if (watch.retryTimer !== void 0) clearTimeout(watch.retryTimer);
163
+ watch.retryTimer = void 0;
164
+ const call = watch.call;
165
+ watch.call = void 0;
166
+ if (call) Promise.resolve(call.cancel("aggregator source removed")).catch(() => {});
167
+ }
168
+ /** Merge every source's desired document into one, namespacing ids by source tag. */
169
+ async getDesired() {
170
+ const sources = await this._refresh();
171
+ const requested = {};
172
+ for (const s of sources) {
173
+ let doc;
174
+ try {
175
+ doc = await withRpcTimeout(s.manifest.getDesired({}), `hubAccessManifest service '${s.tag || "<root>"}'`, this._options.timeoutMs ?? DEFAULT_RPC_TIMEOUT_MS);
176
+ } catch (e) {
177
+ this._options.log?.(`aggregator: getDesired(${s.tag}) failed: ${e.message}`);
178
+ continue;
179
+ }
180
+ const parsed = safeParse(GET_DESIRED_RESULT_SCHEMA, doc);
181
+ if (!parsed.success) {
182
+ const issue = parsed.error.issues[0];
183
+ const where = issue?.path?.join(".") ?? "";
184
+ this._options.log?.(`aggregator: source '${s.tag}' returned an invalid hubAccessManifest::getDesired document; skipping. ${issue?.message ?? "schema mismatch"}${where ? ` (at ${where})` : ""}`);
185
+ continue;
186
+ }
187
+ for (const [id, entry] of Object.entries(parsed.data.requested)) requested[encodeKey(s.tag, id)] = s.preserveOrigin ? entry : stripOrigin(entry);
188
+ }
189
+ return {
190
+ requested,
191
+ revision: ++this._revision
192
+ };
193
+ }
194
+ /** Route each decision patch back to the source that parked the entry. */
195
+ async setCurrent(patches) {
196
+ const bySource = /* @__PURE__ */ new Map();
197
+ for (const p of patches) {
198
+ const decoded = decodeCurrentPath(p.path);
199
+ if (!decoded) continue;
200
+ const list = bySource.get(decoded.tag) ?? [];
201
+ list.push({
202
+ ...p,
203
+ path: `/current/${escapePointer(decoded.innerId)}`
204
+ });
205
+ bySource.set(decoded.tag, list);
206
+ }
207
+ await Promise.all([...bySource].map(async ([tag, ps]) => {
208
+ const src = this._byTag.get(tag);
209
+ if (!src) throw new Error(`aggregator: setCurrent for unknown source '${tag}'`);
210
+ await withRpcTimeout(src.manifest.setCurrent({ patches: ps }), `hubAccessManifest service '${tag || "<root>"}'`, this._options.timeoutMs ?? DEFAULT_RPC_TIMEOUT_MS);
211
+ }));
212
+ }
213
+ dispose() {
214
+ this._disposed = true;
215
+ for (const watch of this._watches.values()) this._closeWatch(watch);
216
+ this._watches.clear();
217
+ this._listeners.clear();
218
+ }
219
+ };
220
+ /**
221
+ * Serve an {@link AggregatingHubAccessManifest} over `connection` as an ordinary
222
+ * `hubAccessManifest` — so an approver consumes it via
223
+ * `connection.service(serviceId).get(hubAccessManifestInterface)` with no idea it
224
+ * is an aggregate. `current` is never read back (decisions flow through
225
+ * `setCurrent` to the sources), so `getCurrent`/`watchCurrent` are served empty.
226
+ */
227
+ function registerAggregatingManifest(connection, aggregator, options) {
228
+ connection.register(hubAccessManifestInterface, {
229
+ getDesired: () => aggregator.getDesired(),
230
+ watchDesired: (_params, _ctx, stream) => new Promise((resolve) => {
231
+ if (stream.signal.aborted) {
232
+ resolve({});
233
+ return;
234
+ }
235
+ const off = aggregator.onDidChange(() => {
236
+ if (!stream.signal.aborted) stream.send({});
237
+ });
238
+ stream.signal.addEventListener("abort", () => {
239
+ off();
240
+ resolve({});
241
+ }, { once: true });
242
+ }),
243
+ getCurrent: () => ({
244
+ current: {},
245
+ revision: 0
246
+ }),
247
+ setCurrent: async (params) => {
248
+ await aggregator.setCurrent(params.patches);
249
+ return { revision: 0 };
250
+ },
251
+ watchCurrent: (_params, _ctx, stream) => new Promise((resolve) => {
252
+ stream.signal.addEventListener("abort", () => resolve({}), { once: true });
253
+ })
254
+ }, { serviceId: options.serviceId });
255
+ }
256
+ function encodeKey(tag, innerId) {
257
+ return `${tag}${SEP}${innerId}`;
258
+ }
259
+ function decodeCurrentPath(path) {
260
+ if (!path.startsWith("/current/")) return void 0;
261
+ const key = unescapePointer(path.slice(9));
262
+ const i = key.indexOf(SEP);
263
+ if (i < 0) return void 0;
264
+ return {
265
+ tag: key.slice(0, i),
266
+ innerId: key.slice(i + 1)
267
+ };
268
+ }
269
+ /** Drop the host-authored `origin` bag from a remote entry (see class docs). */
270
+ function stripOrigin(entry) {
271
+ if (entry.origin === void 0) return entry;
272
+ const clone = { ...entry };
273
+ delete clone.origin;
274
+ return clone;
275
+ }
276
+ function escapePointer(segment) {
277
+ return segment.replace(/~/g, "~0").replace(/\//g, "~1");
278
+ }
279
+ function unescapePointer(segment) {
280
+ return segment.replace(/~1/g, "/").replace(/~0/g, "~");
281
+ }
282
+ //#endregion
283
+ //#region src/engine/watchHubDirectoryTree.ts
284
+ function watchHubDirectoryTree(connection, rootTarget, onChange, options = {}) {
285
+ const explorer = new HubDirectoryExplorer(connection.channel, {
286
+ rootTarget,
287
+ ...options.maxDepth !== void 0 ? { maxDepth: options.maxDepth } : {},
288
+ ...options.log !== void 0 ? { log: options.log } : {}
289
+ });
290
+ let disposed = false;
291
+ let stop;
292
+ explorer.watch(() => {
293
+ if (!disposed) onChange();
294
+ }).then((teardown) => {
295
+ if (disposed) teardown();
296
+ else stop = teardown;
297
+ }).catch((error) => {
298
+ options.log?.(`watchHubDirectoryTree: start failed: ${error.message}`);
299
+ });
300
+ return () => {
301
+ disposed = true;
302
+ stop?.();
303
+ explorer.dispose();
304
+ };
305
+ }
306
+ //#endregion
307
+ export { AggregatingHubAccessManifest, ApproveClient, ConnectionHandlerSchema, ConnectionTokenBinderSchema, ConsentApproverSchema, EndpointConfigSchema, ForwardCheckingSchema, HubAccessGrantSigner, HubAccessManifestHost, HubConfigSchema, ListenerConfigSchema, ParticipantConnectorConfigSchema, TrafficFlowAggregator, buildSlotPermissions, computeCallBindHash, configureParticipants, createHubAccessConfig, createIdentityKeystore, createTerminalConsentPrompt, describePermissions, describeRequest, formatFlowSummary, hubConfigJsonSchema, loadHubConfig, parseHubConfig, printHubConfigSchema, raceConsent, registerAggregatingManifest, registerHubAccessManifest, runHub, runManifestApprover, serveHub, spawnCommand, toSignedPermissions, validateConfiguredParticipants, watchHubDirectoryTree };
308
+
309
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/engine/hubAccessGrantSigner.ts","../src/engine/aggregatingManifest.ts","../src/engine/watchHubDirectoryTree.ts"],"sourcesContent":["/**\n * `HubAccessGrantSigner` — a standalone, in-process consent signer for the\n * `hubAccess` flow. It bundles two things and nothing else:\n *\n * - a {@link SigningIdentity} it mints capabilities with, and\n * - a {@link GrantPolicy} that says, per request, permit or reject.\n *\n * It implements **no interface** — it is plumbing you adapt onto the hubAccess\n * front door via {@link createHubAccessConfig}:\n *\n * ```ts\n * const signer = new HubAccessGrantSigner(adminIdentity, () => ({ result: 'permitted' }));\n * registerHubAccessService(root, createHubAccessConfig(signer.decide));\n * ```\n *\n * This is the simple \"the hub grants locally with its own key\" path (tests, a\n * trusted local operator). The distributed, keyless path uses\n * {@link HubAccessManifestHost} instead, where a *remote* approver signs.\n */\nimport { type SignedCapability, type SigningIdentity } from '@hediet/linkrpc';\nimport { CapabilityProposalIssuer, durationToExp } from '../hub/server';\nimport { toSignedPermissions } from './consent';\nimport type { AccessDecider, AccessRequestContext } from './hubAccessConfig';\n\n/** A grant policy's verdict on one request. */\nexport type GrantVerdict =\n | { readonly result: 'permitted' }\n | { readonly result: 'rejected'; readonly reason?: string };\n\n/** Decides whether to grant a normalized request. May be sync or async. */\nexport type GrantPolicy = (request: AccessRequestContext) => GrantVerdict | Promise<GrantVerdict>;\n\nexport class HubAccessGrantSigner {\n private readonly _issuer: CapabilityProposalIssuer;\n\n constructor(\n signingIdentity: SigningIdentity,\n private readonly _grant: GrantPolicy,\n ) {\n this._issuer = new CapabilityProposalIssuer(signingIdentity);\n }\n\n /**\n * The {@link AccessDecider}: consult the grant policy and, on `permitted`,\n * mint a capability with the held identity (audience = the consumer,\n * honoring \"Allow once\" `callBind` byte-binding for one-shot durations).\n */\n public readonly decide: AccessDecider = async (ctx) => {\n const verdict = await this._grant(ctx);\n if (verdict.result === 'rejected') {\n return verdict.reason !== undefined ? { grant: false, reason: verdict.reason } : { grant: false };\n }\n const capability = await this._mint(ctx);\n return { grant: true, capabilities: [capability] };\n };\n\n private async _mint(ctx: AccessRequestContext): Promise<SignedCapability> {\n return this._issuer.mint({\n audience: ctx.consumerPrincipalId,\n permissions: await toSignedPermissions(ctx.permissions, ctx.consumerPrincipalId, ctx.duration),\n expiresAtMs: durationToExp(ctx.duration),\n });\n }\n}\n","/**\n * `AggregatingHubAccessManifest` — a `manifest* → manifest` combinator.\n *\n * It implements the same {@link IHubAccessManifest} surface the approver/UI\n * consumes, but backs it by **many** source manifests: a trusted *local* source\n * (the participant's own {@link HubAccessManifestHost}, reached over an in-memory\n * loopback) plus zero or more *remote* sources it discovers by scanning the hub\n * directory for `hubAccessManifest` interfaces. So the pipeline composes:\n *\n * hubAccess ─(HubAccessManifestHost)→ manifest_local ─┐\n * discovered ───────────────────────→ manifest_remote*┤─(aggregator)→ manifest → UI\n *\n * Two invariants make aggregation safe:\n *\n * 1. **Namespaced entry ids.** Each source's entryIds are prefixed with the\n * source's `tag`, so the merged id space has no collisions and a `setCurrent`\n * decision can be routed back to exactly the source that parked it.\n *\n * 2. **Origin is preserved only for trusted-local sources.** An entry's\n * host-authored `origin` bag (e.g. `{ sourceTransportId }`) routes a *local*\n * consent UI. A remote source's `origin` refers to a transport on *another*\n * hub — passing it through as-if-local would let a remote address a local\n * iframe. So `preserveOrigin` is opt-in per source (true for the local\n * loopback, false for everything discovered); a stripped entry has no\n * `sourceTransportId`, so the UI falls back to its panel — correct by\n * construction.\n *\n * Discovery + reads happen over the caller-supplied connection; reaching a\n * remote `hubAccessManifest::{getDesired,setCurrent}` is a forwarded (form-3)\n * call, so that connection must carry an admin-rooted **capability** for\n * `*::hubAccessManifest::*` (and the directory) — without it the gate rejects the\n * scan. This is the aggregator's whole reason to hold the approver's identity.\n */\nimport {\n DEFAULT_RPC_TIMEOUT_MS,\n type LinkRpcConnection,\n withRpcTimeout,\n} from '@hediet/linkrpc';\nimport { hubAccessManifestInterface, type IHubAccessManifest } from '@hediet/linkrpc/hub/common';\nimport { safeParse } from 'zod/v4/core';\n\n/** The declared result schema for `hubAccessManifest::getDesired` — used to validate each source. */\nconst GET_DESIRED_RESULT_SCHEMA = hubAccessManifestInterface.members.getDesired.resultSchema;\n\n/** The `{ requested, revision }` document `hubAccessManifest::getDesired` returns. */\ntype GetDesiredResult = Awaited<ReturnType<IHubAccessManifest['getDesired']>>;\n/** One desired entry (a `discover | direct` manifest request). Treated opaquely except `origin`. */\ntype ManifestEntry = GetDesiredResult['requested'][string];\n/** A single `hubAccessManifest::setCurrent` patch. */\ntype ManifestPatch = Parameters<IHubAccessManifest['setCurrent']>[0]['patches'][number];\n\n/** A manifest the aggregator merges, tagged and origin-policied. */\nexport interface AggregatorSource {\n /**\n * Stable id that namespaces this source's entryIds in the merged document\n * (and routes `setCurrent` back). For a discovered source, its serviceId.\n */\n readonly tag: string;\n /** The source manifest client. */\n readonly manifest: IHubAccessManifest;\n /**\n * Pass each entry's `origin` bag through verbatim (true) or strip it\n * (false, default). Set true ONLY for a trusted-local source whose\n * `sourceTransportId` addresses a local UI; a remote source's origin must be\n * stripped so it can never address a local iframe.\n */\n readonly preserveOrigin?: boolean;\n}\n\nexport interface AggregatingManifestOptions {\n /**\n * Resolve the current set of source manifests (the local loopback plus\n * whatever the caller discovers by scanning the hub). Re-invoked on every\n * {@link AggregatingHubAccessManifest.getDesired}, so newly-appeared sources\n * are picked up.\n */\n readonly resolveSources: () => Promise<readonly AggregatorSource[]> | readonly AggregatorSource[];\n /**\n * Optional: subscribe to \"the *set* of sources may have changed\" — e.g. the\n * hub's `hubrpc.directory::watch`, which ticks when a service appears or\n * disappears. Each tick re-emits a change (so a watching approver re-`getDesired`,\n * which re-runs {@link resolveSources} and discovers the new/removed source).\n * Without it, a brand-new manifest is only noticed when some *already-known*\n * source ticks or the approver polls. Returns a teardown.\n */\n readonly watchSources?: (onChange: () => void) => () => void;\n /** Human log sink. */\n readonly log?: (line: string) => void;\n /** Hard deadline for each source request. Defaults to 5 seconds. */\n readonly timeoutMs?: number;\n}\n\n/** Delimiter between a source `tag` and the source-local entryId. Absent from serviceIds/UUIDs. */\nconst SEP = '\\u0000';\n\nexport class AggregatingHubAccessManifest {\n private readonly _listeners = new Set<() => void>();\n private _revision = 0;\n /** tag → source, refreshed on every {@link _refresh}. Used to route `setCurrent`. */\n private _byTag = new Map<string, AggregatorSource>();\n /** tag → resilient watch for that exact source incarnation. */\n private readonly _watches = new Map<string, SourceWatch>();\n /** Teardown for the source-set watch (e.g. directory watch), started lazily. */\n private _sourcesWatchOff: (() => void) | undefined;\n private _refreshPromise: Promise<readonly AggregatorSource[]> | undefined;\n private _disposed = false;\n\n constructor(private readonly _options: AggregatingManifestOptions) { }\n\n /** Fire whenever any source's desired set may have changed (drives `watchDesired`). */\n public onDidChange(cb: () => void): () => void {\n this._listeners.add(cb);\n // Watch the source *set* (directory) so newly-appeared manifests are\n // discovered, not just changes within already-known sources.\n if (this._options.watchSources !== undefined && this._sourcesWatchOff === undefined) {\n this._sourcesWatchOff = this._options.watchSources(() => {\n if (!this._disposed) this._emit();\n });\n }\n // Ensure per-source watches are live so their ticks propagate to `cb`.\n void this._refresh().catch(() => { /* refresh logs its own errors */ });\n return () => this._listeners.delete(cb);\n }\n\n private _emit(): void {\n this._revision++;\n for (const l of this._listeners) {\n try { l(); } catch { /* a listener throwing must not break the others */ }\n }\n }\n\n /** Resolve the source set, (un)subscribing watches to match, and cache it by tag. */\n private _refresh(): Promise<readonly AggregatorSource[]> {\n if (!this._refreshPromise) {\n this._refreshPromise = this._doRefresh().finally(() => {\n this._refreshPromise = undefined;\n });\n }\n return this._refreshPromise;\n }\n\n private async _doRefresh(): Promise<readonly AggregatorSource[]> {\n const sources = await this._options.resolveSources();\n const next = new Map(sources.map((s) => [s.tag, s] as const));\n for (const [tag, watch] of this._watches) {\n const nextSource = next.get(tag);\n if (!nextSource || nextSource.manifest !== watch.source.manifest) {\n this._closeWatch(watch);\n this._watches.delete(tag);\n }\n }\n this._byTag = next;\n for (const s of sources) {\n if (!this._watches.has(s.tag)) this._openWatch(s);\n }\n return sources;\n }\n\n private _openWatch(s: AggregatorSource): void {\n const watch: SourceWatch = { source: s };\n this._watches.set(s.tag, watch);\n this._startWatch(watch);\n }\n\n private _startWatch(watch: SourceWatch): void {\n if (this._disposed || this._watches.get(watch.source.tag) !== watch) return;\n try {\n const call = watch.source.manifest.watchDesired({}, {\n onMessage: () => { if (!this._disposed) this._emit(); },\n });\n watch.call = call;\n call.then(\n () => this._watchEnded(watch),\n () => this._watchEnded(watch),\n );\n } catch {\n // A source that can't be watched still contributes via getDesired.\n this._watchEnded(watch);\n }\n }\n\n private _watchEnded(watch: SourceWatch): void {\n if (this._disposed || this._watches.get(watch.source.tag) !== watch) return;\n watch.call = undefined;\n if (watch.retryTimer !== undefined) return;\n watch.retryTimer = setTimeout(() => {\n watch.retryTimer = undefined;\n if (this._disposed || this._watches.get(watch.source.tag) !== watch) return;\n this._startWatch(watch);\n this._emit();\n }, 250);\n }\n\n private _closeWatch(watch: SourceWatch): void {\n if (watch.retryTimer !== undefined) clearTimeout(watch.retryTimer);\n watch.retryTimer = undefined;\n const call = watch.call;\n watch.call = undefined;\n if (call) {\n void Promise.resolve(call.cancel('aggregator source removed')).catch(() => { /* closing */ });\n }\n }\n\n /** Merge every source's desired document into one, namespacing ids by source tag. */\n public async getDesired(): Promise<GetDesiredResult> {\n const sources = await this._refresh();\n const requested: Record<string, ManifestEntry> = {};\n for (const s of sources) {\n let doc: GetDesiredResult;\n try {\n doc = await withRpcTimeout(\n s.manifest.getDesired({}),\n `hubAccessManifest service '${s.tag || '<root>'}'`,\n this._options.timeoutMs ?? DEFAULT_RPC_TIMEOUT_MS,\n );\n } catch (e) {\n this._options.log?.(`aggregator: getDesired(${s.tag}) failed: ${(e as Error).message}`);\n continue;\n }\n // Result validation at the consumption boundary: `hubAccessManifest`\n // result schemas are compile-time only (the RPC layer validates\n // inbound params + streams, but returns results verbatim), so a\n // source that serves a malformed desired document (e.g. entries\n // missing their required `consumer`) would otherwise blow up deep in\n // the approver with an opaque `undefined` read. Fail loudly here:\n // name the bad source + the first issue, and skip it.\n const parsed = safeParse(GET_DESIRED_RESULT_SCHEMA, doc);\n if (!parsed.success) {\n const issue = parsed.error.issues[0];\n const where = issue?.path?.join('.') ?? '';\n this._options.log?.(\n `aggregator: source '${s.tag}' returned an invalid hubAccessManifest::getDesired document; skipping. `\n + `${issue?.message ?? 'schema mismatch'}${where ? ` (at ${where})` : ''}`,\n );\n continue;\n }\n for (const [id, entry] of Object.entries(parsed.data.requested)) {\n requested[encodeKey(s.tag, id)] = s.preserveOrigin ? entry : stripOrigin(entry);\n }\n }\n return { requested, revision: ++this._revision };\n }\n\n /** Route each decision patch back to the source that parked the entry. */\n public async setCurrent(patches: readonly ManifestPatch[]): Promise<void> {\n const bySource = new Map<string, ManifestPatch[]>();\n for (const p of patches) {\n const decoded = decodeCurrentPath(p.path);\n if (!decoded) continue; // whole-doc / malformed patch — nothing to route\n const list = bySource.get(decoded.tag) ?? [];\n list.push({ ...p, path: `/current/${escapePointer(decoded.innerId)}` });\n bySource.set(decoded.tag, list);\n }\n await Promise.all([...bySource].map(async ([tag, ps]) => {\n const src = this._byTag.get(tag);\n if (!src) {\n throw new Error(`aggregator: setCurrent for unknown source '${tag}'`);\n }\n await withRpcTimeout(\n src.manifest.setCurrent({ patches: ps }),\n `hubAccessManifest service '${tag || '<root>'}'`,\n this._options.timeoutMs ?? DEFAULT_RPC_TIMEOUT_MS,\n );\n }));\n }\n\n public dispose(): void {\n this._disposed = true;\n for (const watch of this._watches.values()) this._closeWatch(watch);\n this._watches.clear();\n this._listeners.clear();\n }\n\n}\n\ninterface SourceWatch {\n readonly source: AggregatorSource;\n call?: ReturnType<IHubAccessManifest['watchDesired']>;\n retryTimer?: ReturnType<typeof setTimeout>;\n}\n\n/**\n * Serve an {@link AggregatingHubAccessManifest} over `connection` as an ordinary\n * `hubAccessManifest` — so an approver consumes it via\n * `connection.service(serviceId).get(hubAccessManifestInterface)` with no idea it\n * is an aggregate. `current` is never read back (decisions flow through\n * `setCurrent` to the sources), so `getCurrent`/`watchCurrent` are served empty.\n */\nexport function registerAggregatingManifest(\n connection: LinkRpcConnection<unknown>,\n aggregator: AggregatingHubAccessManifest,\n options: { readonly serviceId: string; },\n): void {\n connection.register(hubAccessManifestInterface, {\n getDesired: () => aggregator.getDesired(),\n watchDesired: (_params, _ctx, stream) =>\n new Promise<Record<string, never>>((resolve) => {\n if (stream.signal.aborted) { resolve({}); return; }\n const off = aggregator.onDidChange(() => {\n if (!stream.signal.aborted) stream.send({});\n });\n stream.signal.addEventListener('abort', () => { off(); resolve({}); }, { once: true });\n }),\n getCurrent: () => ({ current: {}, revision: 0 }),\n setCurrent: async (params) => { await aggregator.setCurrent(params.patches); return { revision: 0 }; },\n watchCurrent: (_params, _ctx, stream) =>\n new Promise<Record<string, never>>((resolve) => {\n stream.signal.addEventListener('abort', () => resolve({}), { once: true });\n }),\n }, { serviceId: options.serviceId });\n}\n\nfunction encodeKey(tag: string, innerId: string): string {\n return `${tag}${SEP}${innerId}`;\n}\n\nfunction decodeCurrentPath(path: string): { tag: string; innerId: string; } | undefined {\n const prefix = '/current/';\n if (!path.startsWith(prefix)) return undefined;\n const key = unescapePointer(path.slice(prefix.length));\n const i = key.indexOf(SEP);\n if (i < 0) return undefined;\n return { tag: key.slice(0, i), innerId: key.slice(i + 1) };\n}\n\n/** Drop the host-authored `origin` bag from a remote entry (see class docs). */\nfunction stripOrigin(entry: ManifestEntry): ManifestEntry {\n if (entry.origin === undefined) return entry;\n const clone = { ...entry };\n delete clone.origin;\n return clone;\n}\n\nfunction escapePointer(segment: string): string {\n return segment.replace(/~/g, '~0').replace(/\\//g, '~1');\n}\n\nfunction unescapePointer(segment: string): string {\n return segment.replace(/~1/g, '/').replace(/~0/g, '~');\n}\n","/**\n * `watchHubDirectoryTree` — a recursive \"the directory tree may have changed\"\n * subscription.\n *\n * The hub directory is a **referral graph**: the root\n * (`<rootTarget>::hubrpc.directory`) lists sub-directories, each of which may\n * list more. A single `hubrpc.directory::watch` only reflects *its own* node —\n * the hub's global directory aggregates local, participant-root, and routing\n * invalidations, while every referred directory remains a separate node with\n * its own watch. To react to a manifest appearing *anywhere* in the tree, a\n * consumer must watch **every** node the walk traverses, and re-subscribe as the\n * set of nodes itself changes.\n *\n * This uses {@link HubDirectoryExplorer} to open\n * `hubrpc.directory::watch` on the root and every discovered sub-directory.\n * A tick re-lists only its source and reconciles affected descendant branches;\n * unchanged siblings are not re-listed or re-watched. Nodes that\n * serve the static {@link directoryWatchNever} handler simply never tick — the\n * watch is a harmless no-op there — while live nodes (the routing-backed root,\n * nested sub-hubs) drive discovery. Reads/watches are forwarded (form-3) calls,\n * so `connection` must carry a capability for `*::hubrpc.directory::*` when it is\n * gated.\n *\n * Returns a teardown that cancels every open watch.\n */\nimport { type LinkRpcConnection } from '@hediet/linkrpc';\nimport { HubDirectoryExplorer } from '@hediet/linkrpc/hub/common';\n\nexport interface WatchHubDirectoryTreeOptions {\n /** Bound the referral recursion (forwarded to {@link walkHubDetailed}). */\n readonly maxDepth?: number;\n /** Human log sink for watch/reconciliation failures. */\n readonly log?: (line: string) => void;\n}\n\nexport function watchHubDirectoryTree(\n connection: LinkRpcConnection,\n rootTarget: string,\n onChange: () => void,\n options: WatchHubDirectoryTreeOptions = {},\n): () => void {\n const explorer = new HubDirectoryExplorer(connection.channel, {\n rootTarget,\n ...(options.maxDepth !== undefined ? { maxDepth: options.maxDepth } : {}),\n ...(options.log !== undefined ? { log: options.log } : {}),\n });\n let disposed = false;\n let stop: (() => void) | undefined;\n void explorer.watch(() => {\n if (!disposed) onChange();\n }).then((teardown) => {\n if (disposed) teardown();\n else stop = teardown;\n }).catch((error) => {\n options.log?.(`watchHubDirectoryTree: start failed: ${(error as Error).message}`);\n });\n\n return () => {\n disposed = true;\n stop?.();\n explorer.dispose();\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ import { n as printHubConfigSchema, t as loadHubConfig } from "./chunks/configFile-y6Phntqu.js";
2
+ //#region src/serve.d.ts
3
+ interface ServeHubOptions {
4
+ readonly configPath: string;
5
+ readonly cmdInteractive?: boolean;
6
+ readonly log?: (line: string) => void;
7
+ /** Log every JSON-RPC message routed through the hub to {@link log}. */
8
+ readonly logMessages?: boolean;
9
+ }
10
+ /** Run a hub from `configPath`; resolves only on SIGINT/SIGTERM. */
11
+ declare function serveHub(opts: ServeHubOptions): Promise<void>;
12
+ //#endregion
13
+ export { ServeHubOptions, loadHubConfig, printHubConfigSchema, serveHub };
14
+ //# sourceMappingURL=serve.d.ts.map
package/dist/serve.js ADDED
@@ -0,0 +1,31 @@
1
+ import { t as runHub } from "./chunks/runHub-P3YUwwdv.js";
2
+ import { loadHubConfig, printHubConfigSchema } from "./configFile.js";
3
+ //#region src/serve.ts
4
+ /**
5
+ * `serve` — run a hub from a declarative {@link HubConfig} file and block until
6
+ * interrupted. Shared by the `linkrpc-hub` binary and (re-exported) the CLI.
7
+ */
8
+ /** Run a hub from `configPath`; resolves only on SIGINT/SIGTERM. */
9
+ async function serveHub(opts) {
10
+ const log = opts.log ?? ((line) => process.stderr.write(line + "\n"));
11
+ const config = loadHubConfig(opts.configPath);
12
+ const running = await runHub({
13
+ config,
14
+ log,
15
+ cmdInteractive: opts.cmdInteractive,
16
+ logMessages: opts.logMessages
17
+ });
18
+ log(`hub running (${running.listeners.length} listener(s)). Ctrl-C to stop.`);
19
+ await new Promise((resolve) => {
20
+ const stop = () => {
21
+ running.dispose();
22
+ resolve();
23
+ };
24
+ process.once("SIGINT", stop);
25
+ process.once("SIGTERM", stop);
26
+ });
27
+ }
28
+ //#endregion
29
+ export { loadHubConfig, printHubConfigSchema, serveHub };
30
+
31
+ //# sourceMappingURL=serve.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serve.js","names":[],"sources":["../src/serve.ts"],"sourcesContent":["/**\n * `serve` — run a hub from a declarative {@link HubConfig} file and block until\n * interrupted. Shared by the `linkrpc-hub` binary and (re-exported) the CLI.\n */\nimport { runHub } from './engine/runHub';\nimport { loadHubConfig } from './configFile';\n\nexport { loadHubConfig, printHubConfigSchema } from './configFile';\n\nexport interface ServeHubOptions {\n readonly configPath: string;\n readonly cmdInteractive?: boolean;\n readonly log?: (line: string) => void;\n /** Log every JSON-RPC message routed through the hub to {@link log}. */\n readonly logMessages?: boolean;\n}\n\n/** Run a hub from `configPath`; resolves only on SIGINT/SIGTERM. */\nexport async function serveHub(opts: ServeHubOptions): Promise<void> {\n const log = opts.log ?? ((line: string) => process.stderr.write(line + '\\n'));\n const config = loadHubConfig(opts.configPath);\n const running = await runHub({\n config,\n log,\n cmdInteractive: opts.cmdInteractive,\n logMessages: opts.logMessages,\n });\n log(`hub running (${running.listeners.length} listener(s)). Ctrl-C to stop.`);\n\n await new Promise<void>((resolve) => {\n const stop = (): void => {\n running.dispose();\n resolve();\n };\n process.once('SIGINT', stop);\n process.once('SIGTERM', stop);\n });\n}\n"],"mappings":";;;;;;;;AAkBA,eAAsB,SAAS,MAAsC;CACjE,MAAM,MAAM,KAAK,SAAS,SAAiB,QAAQ,OAAO,MAAM,OAAO,IAAI;CAC3E,MAAM,SAAS,cAAc,KAAK,UAAU;CAC5C,MAAM,UAAU,MAAM,OAAO;EACzB;EACA;EACA,gBAAgB,KAAK;EACrB,aAAa,KAAK;CACtB,CAAC;CACD,IAAI,gBAAgB,QAAQ,UAAU,OAAO,+BAA+B;CAE5E,MAAM,IAAI,SAAe,YAAY;EACjC,MAAM,aAAmB;GACrB,QAAQ,QAAQ;GAChB,QAAQ;EACZ;EACA,QAAQ,KAAK,UAAU,IAAI;EAC3B,QAAQ,KAAK,WAAW,IAAI;CAChC,CAAC;AACL"}
@@ -0,0 +1,12 @@
1
+ import { EndpointCommand } from "@hediet/linkrpc/node";
2
+ import { ChildProcess, SpawnOptions } from "node:child_process";
3
+ //#region src/spawn.d.ts
4
+ /**
5
+ * Spawn a child from a command spec. `{ command }` is run through the OS shell
6
+ * (so quoting / splitting follows the shell's rules); `{ argv }` is run
7
+ * directly (no shell), except on Windows where `.cmd` shims need one.
8
+ */
9
+ declare function spawnCommand(command: EndpointCommand, options: SpawnOptions): ChildProcess;
10
+ //#endregion
11
+ export { spawnCommand };
12
+ //# sourceMappingURL=spawn.d.ts.map
package/dist/spawn.js ADDED
@@ -0,0 +1,25 @@
1
+ import { spawn } from "node:child_process";
2
+ //#region src/spawn.ts
3
+ /**
4
+ * Spawn a child from a command spec. `{ command }` is run through the OS shell
5
+ * (so quoting / splitting follows the shell's rules); `{ argv }` is run
6
+ * directly (no shell), except on Windows where `.cmd` shims need one.
7
+ */
8
+ function spawnCommand(command, options) {
9
+ if ("argv" in command) {
10
+ const [bin, ...args] = command.argv;
11
+ if (!bin) throw new Error("spawnCommand: empty argv command");
12
+ return spawn(bin, args, {
13
+ ...options,
14
+ shell: process.platform === "win32"
15
+ });
16
+ }
17
+ return spawn(command.command, {
18
+ ...options,
19
+ shell: true
20
+ });
21
+ }
22
+ //#endregion
23
+ export { spawnCommand };
24
+
25
+ //# sourceMappingURL=spawn.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spawn.js","names":[],"sources":["../src/spawn.ts"],"sourcesContent":["/**\n * Minimal child-spawn helper used by the hub engine. Kept free of the CLI's\n * connection machinery so the engine has no heavy dependencies. (Socket-path\n * allocation lives on {@link SocketServer.allocSocketPath}.)\n */\nimport { type EndpointCommand } from '@hediet/linkrpc/node';\nimport { type ChildProcess, spawn, type SpawnOptions } from 'node:child_process';\n\n/**\n * Spawn a child from a command spec. `{ command }` is run through the OS shell\n * (so quoting / splitting follows the shell's rules); `{ argv }` is run\n * directly (no shell), except on Windows where `.cmd` shims need one.\n */\nexport function spawnCommand(command: EndpointCommand, options: SpawnOptions): ChildProcess {\n if ('argv' in command) {\n const [bin, ...args] = command.argv;\n if (!bin) throw new Error('spawnCommand: empty argv command');\n return spawn(bin, args, { ...options, shell: process.platform === 'win32' });\n }\n return spawn(command.command, { ...options, shell: true });\n}\n"],"mappings":";;;;;;;AAaA,SAAgB,aAAa,SAA0B,SAAqC;CACxF,IAAI,UAAU,SAAS;EACnB,MAAM,CAAC,KAAK,GAAG,QAAQ,QAAQ;EAC/B,IAAI,CAAC,KAAK,MAAM,IAAI,MAAM,kCAAkC;EAC5D,OAAO,MAAM,KAAK,MAAM;GAAE,GAAG;GAAS,OAAO,QAAQ,aAAa;EAAQ,CAAC;CAC/E;CACA,OAAO,MAAM,QAAQ,SAAS;EAAE,GAAG;EAAS,OAAO;CAAK,CAAC;AAC7D"}
package/package.json ADDED
@@ -0,0 +1,83 @@
1
+ {
2
+ "name": "@hediet/linkrpc-hub",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "import": "./dist/index.js",
10
+ "types": "./dist/index.d.ts"
11
+ },
12
+ "./hub/server": {
13
+ "import": "./dist/hub/server/index.js",
14
+ "types": "./dist/hub/server/index.d.ts"
15
+ },
16
+ "./hub/server/client": {
17
+ "import": "./dist/hub/server/client.js",
18
+ "types": "./dist/hub/server/client.d.ts"
19
+ },
20
+ "./hub/server/transit": {
21
+ "import": "./dist/hub/server/transit.js",
22
+ "types": "./dist/hub/server/transit.d.ts"
23
+ },
24
+ "./hub/server/connection-token-binder": {
25
+ "import": "./dist/hub/server/connectionTokenBinder.js",
26
+ "types": "./dist/hub/server/connectionTokenBinder.d.ts"
27
+ },
28
+ "./hub/server/node": {
29
+ "import": "./dist/hub/server/node/index.js",
30
+ "types": "./dist/hub/server/node/index.d.ts"
31
+ },
32
+ "./config": {
33
+ "import": "./dist/config.js",
34
+ "types": "./dist/config.d.ts"
35
+ },
36
+ "./config-file": {
37
+ "import": "./dist/configFile.js",
38
+ "types": "./dist/configFile.d.ts"
39
+ },
40
+ "./spawn": {
41
+ "import": "./dist/spawn.js",
42
+ "types": "./dist/spawn.d.ts"
43
+ },
44
+ "./serve": {
45
+ "import": "./dist/serve.js",
46
+ "types": "./dist/serve.d.ts"
47
+ },
48
+ "./engine/runHub": {
49
+ "import": "./dist/engine/runHub.js",
50
+ "types": "./dist/engine/runHub.d.ts"
51
+ }
52
+ },
53
+ "bin": {
54
+ "linkrpc-hub": "./dist/cli.js"
55
+ },
56
+ "files": [
57
+ "dist"
58
+ ],
59
+ "dependencies": {
60
+ "@vscode/observables": "0.1.1-0",
61
+ "commander": "^12.1.0",
62
+ "ws": "^8.18.0",
63
+ "zod": "^4.4.3",
64
+ "@hediet/linkrpc": "0.0.1"
65
+ },
66
+ "devDependencies": {
67
+ "@types/node": "^22.0.0",
68
+ "@types/ws": "^8.5.13",
69
+ "tsdown": "^0.22.3",
70
+ "tsx": "4.22.4",
71
+ "typescript": "^6.0.3",
72
+ "vitest": "^3.0.0",
73
+ "@hediet/linkrpc-infra": "0.0.1"
74
+ },
75
+ "scripts": {
76
+ "start": "tsx src/cli.ts",
77
+ "build": "tsdown",
78
+ "pack": "pnpm build && pnpm pack",
79
+ "dev": "tsdown --watch",
80
+ "test": "vitest --run",
81
+ "test:watch": "vitest"
82
+ }
83
+ }