@hediet/linkrpc 0.0.2 → 0.0.3-next.20260922.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 (33) hide show
  1. package/README.md +266 -31
  2. package/dist/chunks/{hub.interfaces-Bf4VfWNX.js → hub.interfaces-Deslgtgr.js} +2 -2
  3. package/dist/chunks/{hub.interfaces-Bf4VfWNX.js.map → hub.interfaces-Deslgtgr.js.map} +1 -1
  4. package/dist/chunks/{hubAccess-C06qAKao.d.ts → hubAccess-BmPvNNaM.d.ts} +2 -2
  5. package/dist/chunks/{hubAccess-C06qAKao.d.ts.map → hubAccess-BmPvNNaM.d.ts.map} +1 -1
  6. package/dist/chunks/{hubFacade-BIB9CoAv.js → hubFacade-BKTQXCdF.js} +2 -2
  7. package/dist/chunks/{hubFacade-BIB9CoAv.js.map → hubFacade-BKTQXCdF.js.map} +1 -1
  8. package/dist/chunks/{hubFacade-DtQpGpUy.d.ts → hubFacade-Bmh-SFWd.d.ts} +2 -2
  9. package/dist/chunks/{hubFacade-DtQpGpUy.d.ts.map → hubFacade-Bmh-SFWd.d.ts.map} +1 -1
  10. package/dist/chunks/{boundedTrafficSubscription-XZ1KwXZC.js → inspectionHost-BIkLKvr1.js} +1014 -32
  11. package/dist/chunks/inspectionHost-BIkLKvr1.js.map +1 -0
  12. package/dist/chunks/{linkRpcConnection-BadD37ok.d.ts → linkRpcConnection-Bbq20hQG.d.ts} +2215 -1699
  13. package/dist/chunks/linkRpcConnection-Bbq20hQG.d.ts.map +1 -0
  14. package/dist/chunks/{src-CAhXne71.js → src-DKjTceek.js} +1975 -2051
  15. package/dist/chunks/src-DKjTceek.js.map +1 -0
  16. package/dist/hub/client/index.d.ts +3 -3
  17. package/dist/hub/client/index.js +3 -3
  18. package/dist/hub/common/index.d.ts +2 -325
  19. package/dist/hub/common/index.d.ts.map +1 -1
  20. package/dist/hub/common/index.js +2 -2
  21. package/dist/index.d.ts +2 -2
  22. package/dist/index.js +3 -3
  23. package/dist/inspection/index.d.ts +87 -2
  24. package/dist/inspection/index.d.ts.map +1 -1
  25. package/dist/inspection/index.js +2 -2
  26. package/dist/node.d.ts +3 -3
  27. package/dist/node.js +4 -4
  28. package/dist/web.d.ts +2 -2
  29. package/dist/web.js +3 -3
  30. package/package.json +7 -2
  31. package/dist/chunks/boundedTrafficSubscription-XZ1KwXZC.js.map +0 -1
  32. package/dist/chunks/linkRpcConnection-BadD37ok.d.ts.map +0 -1
  33. package/dist/chunks/src-CAhXne71.js.map +0 -1
@@ -3,9 +3,22 @@
3
3
  * Licensed under the MIT License. See License.txt in the project root for license information.
4
4
  *--------------------------------------------------------------------------------------------*/
5
5
  import { t as __exportAll } from "./rolldown-runtime-4LSo1kEK.js";
6
- import { B as requestType, C as defineInterface, F as NotificationType, I as RequestType, M as sha256$1, N as jcsCanonicalize, P as jcsCanonicalizeBytes, R as isApplicationErrorValue, V as zodToSvcJsonSchema, a as topologyInterface, i as nodeInterface, o as trafficInterface, r as TrafficWatchFlowTracker, t as BoundedTrafficSubscription, z as notificationType } from "./boundedTrafficSubscription-XZ1KwXZC.js";
7
- import { array, boolean, enum as enum$1, int, nonnegative, number, object, optional, positive, string, union, unknown } from "zod/mini";
6
+ import { $ as RequestType, F as principalForPublicKey, G as componentSchemaName, I as publicKeyForKeyId, J as computeInterfaceHash, L as resolveSigningKey, M as bytesToBase64Url, N as keyIdForPrincipal, Q as NotificationType, V as validateInterfaceErrors, X as jcsCanonicalize, Y as sha256$1, Z as jcsCanonicalizeBytes, c as StreamControlType, ct as jsonPointerSegment, d as nodeInterface, dt as isNotification, ft as isRequest, j as base64UrlToBytes$1, l as StreamDir, lt as brandApplicationError, mt as RpcFailure, o as STREAM_METHOD, ot as jsonIssues, pt as isResponse, rt as requestType, st as parseDeclaredErrorBody, t as InspectionHost, tt as isApplicationErrorValue, ut as ErrorCode, z as defineInterface } from "./inspectionHost-BIkLKvr1.js";
7
+ import { array, boolean, int, nonnegative, number, object, optional, positive, string, union, unknown } from "zod/mini";
8
8
  import { safeParse } from "zod/v4/core";
9
+ //#region src/connection/nonCompliantServerError.ts
10
+ var NonCompliantServerError = class extends Error {
11
+ original;
12
+ issues;
13
+ kind = "nonCompliantServer";
14
+ constructor(original, issues) {
15
+ super(`Server error ${original.code} does not match its declared schema: ${issues.map((issue) => `${issue.path || "/"}: ${issue.message}`).join("; ")}`);
16
+ this.original = original;
17
+ this.issues = issues;
18
+ this.name = "NonCompliantServerError";
19
+ }
20
+ };
21
+ //#endregion
9
22
  //#region src/schema/assignability.ts
10
23
  /**
11
24
  * Structural subtype check: returns `true` iff every JSON value matching
@@ -22,6 +35,11 @@ function isAssignable(sub, sup, components = {}) {
22
35
  function matchesJsonSchema(value, schema, components = {}) {
23
36
  return _matches(value, schema, components, /* @__PURE__ */ new Set());
24
37
  }
38
+ function validateJsonSchema(value, schema, components = {}) {
39
+ const issues = [];
40
+ _matches(value, schema, components, /* @__PURE__ */ new Set(), issues);
41
+ return issues;
42
+ }
25
43
  function _check(sub, sup, c, seen) {
26
44
  if (sup === true) return true;
27
45
  if (sub === false) return true;
@@ -118,49 +136,74 @@ function _restOf(s) {
118
136
  }
119
137
  return true;
120
138
  }
121
- function _matches(v, sup, c, seen) {
139
+ function _matches(v, sup, c, seen, issues, path = "") {
140
+ const mismatch = (message) => {
141
+ issues?.push({
142
+ path,
143
+ message
144
+ });
145
+ return false;
146
+ };
122
147
  if (sup === true) return true;
123
- if (sup === false) return false;
148
+ if (sup === false) return mismatch("No value is allowed");
124
149
  if (_isRef(sup)) {
125
- if (seen.has(sup.$ref)) return false;
150
+ if (seen.has(sup.$ref)) return mismatch(`Unguarded recursive reference ${sup.$ref}`);
126
151
  const r = _resolveRef(sup, c);
127
- return r ? _matches(v, r, c, new Set(seen).add(sup.$ref)) : false;
152
+ return r === void 0 ? mismatch(`Unresolved reference ${sup.$ref}`) : _matches(v, r, c, new Set(seen).add(sup.$ref), issues, path);
153
+ }
154
+ const rawType = sup.type;
155
+ if (Array.isArray(rawType)) return _matches(v, { anyOf: rawType.map((type) => ({
156
+ ...sup,
157
+ type
158
+ })) }, c, seen, issues, path);
159
+ if (_isUnion(sup)) {
160
+ const failures = issues === void 0 ? void 0 : [];
161
+ if (_branches(sup).some((branch) => _matches(v, branch, c, seen, failures, path))) return true;
162
+ if (failures?.length) issues?.push(...failures);
163
+ else mismatch("No union branch accepts this value");
164
+ return false;
128
165
  }
129
- if (_isUnion(sup)) return _branches(sup).some((b) => _matches(v, b, c, seen));
130
- if (_isConst(sup)) return _jsonEq(v, sup.const);
131
- if (_isEnum(sup)) return sup.enum.some((e) => _jsonEq(v, e));
166
+ if (_isConst(sup)) return _jsonEq(v, sup.const) || mismatch(`Expected ${JSON.stringify(sup.const)}`);
167
+ if (_isEnum(sup)) return sup.enum.some((e) => _jsonEq(v, e)) || mismatch(`Expected one of ${JSON.stringify(sup.enum)}`);
132
168
  switch (sup.type) {
133
- case "null": return v === null;
134
- case "boolean": return typeof v === "boolean";
135
- case "number": return typeof v === "number";
136
- case "integer": return typeof v === "number" && Number.isInteger(v);
137
- case "string": return typeof v === "string";
138
- case "array": return Array.isArray(v) && _matchesArray(v, sup, c);
139
- case "object": return v !== null && typeof v === "object" && !Array.isArray(v) && _matchesObject(v, sup, c);
140
- default: return false;
169
+ case "null": return v === null || mismatch("Expected null");
170
+ case "boolean": return typeof v === "boolean" || mismatch("Expected boolean");
171
+ case "number": return typeof v === "number" || mismatch("Expected number");
172
+ case "integer": return typeof v === "number" && Number.isInteger(v) || mismatch("Expected integer");
173
+ case "string": return typeof v === "string" || mismatch("Expected string");
174
+ case "array": return Array.isArray(v) ? _matchesArray(v, sup, c, issues, path) : mismatch("Expected array");
175
+ case "object": return v !== null && typeof v === "object" && !Array.isArray(v) ? _matchesObject(v, sup, c, issues, path) : mismatch("Expected object");
176
+ default: return Object.keys(sup).every((key) => key === "title" || key === "description" || key.startsWith("x-")) || mismatch("Unsupported schema shape");
141
177
  }
142
178
  }
143
- function _matchesArray(v, sup, c) {
179
+ function _matchesArray(v, sup, c, issues, path = "") {
144
180
  const prefix = sup.prefixItems ?? [];
145
181
  const rest = _restOf(sup);
146
- if (rest === false && v.length !== prefix.length) {
147
- if (v.length > prefix.length) return false;
182
+ if (v.length < prefix.length || rest === false && v.length > prefix.length) {
183
+ issues?.push({
184
+ path,
185
+ message: rest === false ? `Expected ${prefix.length} items` : `Expected at least ${prefix.length} items`
186
+ });
187
+ return false;
148
188
  }
189
+ let valid = true;
149
190
  for (let i = 0; i < v.length; i++) {
150
191
  const s = prefix[i] ?? (rest === false ? false : rest);
151
- if (s === false) return false;
152
- if (!_matches(v[i], s, c, /* @__PURE__ */ new Set())) return false;
192
+ if (!_matches(v[i], s, c, /* @__PURE__ */ new Set(), issues, `${path}/${i}`)) valid = false;
153
193
  }
154
- return true;
194
+ return valid;
155
195
  }
156
- function _matchesObject(v, sup, c) {
157
- for (const k of sup.required ?? []) if (!Object.hasOwn(v, k)) return false;
158
- for (const [k, val] of Object.entries(v)) {
159
- const s = Object.hasOwn(sup.properties, k) ? sup.properties[k] : sup.additionalProperties;
160
- if (s === false) return false;
161
- if (!_matches(val, s, c, /* @__PURE__ */ new Set())) return false;
196
+ function _matchesObject(v, sup, c, issues, path = "") {
197
+ let valid = true;
198
+ for (const k of sup.required ?? []) if (!Object.hasOwn(v, k)) {
199
+ issues?.push({
200
+ path: path + jsonPointerSegment(k),
201
+ message: "Required property is missing"
202
+ });
203
+ valid = false;
162
204
  }
163
- return true;
205
+ for (const [k, val] of Object.entries(v)) if (!_matches(val, Object.hasOwn(sup.properties, k) ? sup.properties[k] : sup.additionalProperties, c, /* @__PURE__ */ new Set(), issues, path + jsonPointerSegment(k))) valid = false;
206
+ return valid;
164
207
  }
165
208
  function _isBottom(s, c, seen) {
166
209
  if (s === false) return true;
@@ -195,7 +238,7 @@ function _isEnum(s) {
195
238
  }
196
239
  function _resolveRef(r, c) {
197
240
  if (!r.$ref.startsWith("#/components/schemas/")) return void 0;
198
- const name = r.$ref.slice(21);
241
+ const name = componentSchemaName(r.$ref);
199
242
  return c.schemas?.[name];
200
243
  }
201
244
  function _keyOf(s) {
@@ -311,22 +354,20 @@ function isRecord(value) {
311
354
  //#region src/connection/bareInterfaceTarget.ts
312
355
  /**
313
356
  * Bundle an interface definition with metadata-free foreign-protocol
314
- * addressing for use with `connection.get(target)`.
357
+ * addressing for use with `connection.get(target)` or
358
+ * `connection.register(target, handlers)`.
315
359
  */
316
360
  function bareInterfaceTarget(iface, options = {}) {
317
361
  const prefix = options.prefix ?? "";
318
- validateBarePrefix(prefix, "bareInterfaceTarget");
362
+ validateBarePrefix(prefix);
319
363
  return Object.freeze({
320
364
  mode: "bare",
321
365
  interface: iface,
322
366
  prefix
323
367
  });
324
368
  }
325
- function isBareInterfaceTarget(value) {
326
- return typeof value === "object" && value !== null && "mode" in value && value.mode === "bare" && "interface" in value && "prefix" in value;
327
- }
328
- function validateBarePrefix(prefix, api) {
329
- if (prefix.includes("::") || !/^[\x20-\x7e]*$/.test(prefix)) throw new Error(`${api}: prefix must contain only printable ASCII and must not contain "::".`);
369
+ function validateBarePrefix(prefix) {
370
+ if (prefix.includes("::") || !/^[\x20-\x7e]*$/.test(prefix)) throw new Error("Bare interface prefix must contain only printable ASCII and must not contain \"::\".");
330
371
  }
331
372
  //#endregion
332
373
  //#region src/schema/codegen/utils/codeWriter.ts
@@ -395,18 +436,21 @@ var CodeWriter = class {
395
436
  * components receive explicit payload types; other types remain Zod-inferred.
396
437
  */
397
438
  function generateTsInterface(schema, options = {}) {
398
- _validateErrors(schema.methods);
439
+ validateInterfaceErrors(schema);
399
440
  const linkRpcImport = options.linkRpcImport ?? "@hediet/linkrpc";
400
441
  const exportName = options.exportName ?? _deriveExportName(schema.id);
401
442
  const preserveWireSchema = options.preserveWireSchema ?? false;
402
- if (options.bareTarget !== void 0) validateBarePrefix(options.bareTarget.prefix, "bareInterfaceTarget");
443
+ if (options.bareTarget !== void 0) validateBarePrefix(options.bareTarget.prefix);
403
444
  const w = new CodeWriter();
404
445
  const bareTargetImport = options.bareTarget === void 0 ? "" : "bareInterfaceTarget, ";
405
446
  const errorImport = Object.values(schema.methods).some((method) => (method.errors?.length ?? 0) > 0) ? "applicationError, " : "";
447
+ const rawErrorImport = Object.values(schema.methods).some((method) => method.errors?.some((error) => error.schema !== void 0)) ? "rpcError, " : "";
406
448
  const definitionImport = preserveWireSchema ? `${errorImport}${bareTargetImport}InterfaceDefinition, notificationType, requestType, type LinkRpcInterfaceSchema` : `${errorImport}${bareTargetImport}defineInterface, notificationType, requestType`;
407
- w.writeLine(`import { ${definitionImport} } from "${linkRpcImport}";`);
408
- w.writeLine(`import { z } from "zod";`);
409
- w.writeLine();
449
+ if (!options.omitImports) {
450
+ w.writeLine(`import { ${rawErrorImport}${definitionImport} } from "${linkRpcImport}";`);
451
+ w.writeLine(`import { z } from "zod";`);
452
+ w.writeLine();
453
+ }
410
454
  const componentSchemas = schema.components?.schemas ?? {};
411
455
  const cyclicComponents = _findCyclicComponents(componentSchemas);
412
456
  const componentNames = _allocateComponentNames(componentSchemas);
@@ -550,11 +594,18 @@ function _writeMethod(w, method, components, preserveWireSchema) {
550
594
  w.append(".withErrors([").newline();
551
595
  w.indent();
552
596
  for (const error of method.errors) {
553
- w.append(`applicationError(${error.code}, ${JSON.stringify(error.message)}`);
597
+ if (error.schema !== void 0) {
598
+ w.append(`rpcError(${error.code}, `);
599
+ _writeSchema(w, error.schema, components, preserveWireSchema);
600
+ w.append("),").newline();
601
+ continue;
602
+ }
603
+ w.append(error.type === void 0 ? `applicationError(${error.code}, ${JSON.stringify(error.message)}` : `applicationError(${JSON.stringify(error.type)}, { code: ${error.code}, message: ${JSON.stringify(error.message)}`);
554
604
  if (error.data !== void 0) {
555
- w.append(", ");
605
+ w.append(error.type === void 0 ? ", " : ", data: ");
556
606
  _writeSchema(w, error.data, components, preserveWireSchema);
557
607
  }
608
+ if (error.type !== void 0) w.append(" }");
558
609
  w.append("),").newline();
559
610
  }
560
611
  w.dedent();
@@ -577,19 +628,6 @@ function _writeMethod(w, method, components, preserveWireSchema) {
577
628
  w.append("})");
578
629
  }
579
630
  }
580
- function _validateErrors(methods) {
581
- for (const [name, method] of Object.entries(methods)) {
582
- if (method.result === void 0 && method.errors !== void 0) throw new Error(`generateInterface: notification "${name}" cannot declare errors`);
583
- const seen = /* @__PURE__ */ new Set();
584
- for (const error of method.errors ?? []) {
585
- if (typeof error.message !== "string") throw new Error(`generateInterface: error message on "${name}" is not a string`);
586
- if (!Number.isInteger(error.code) || error.code < -2147483648 || error.code > 2147483647) throw new Error(`generateInterface: error code ${error.code} on "${name}" is not a signed i32`);
587
- if (error.code >= -32768 && error.code <= -32e3 || error.code === -32800) throw new Error(`generateInterface: error code ${error.code} on "${name}" is reserved`);
588
- if (seen.has(error.code)) throw new Error(`generateInterface: duplicate error code ${error.code} on "${name}"`);
589
- seen.add(error.code);
590
- }
591
- }
592
- }
593
631
  function _collectMemberDocs(method) {
594
632
  const docs = {};
595
633
  if (method.description !== void 0) docs.description = method.description;
@@ -934,8 +972,7 @@ function _jsonLiteral(value) {
934
972
  return JSON.stringify(value);
935
973
  }
936
974
  function _parseRef(ref) {
937
- if (!ref.startsWith("#/components/schemas/")) throw new Error(`generateInterface: unsupported $ref "${ref}"`);
938
- return ref.slice(21);
975
+ return componentSchemaName(ref);
939
976
  }
940
977
  function _toComponentVar(name) {
941
978
  return _toIdentifier(name) + "Schema";
@@ -1061,6 +1098,372 @@ function _deriveExportName(id) {
1061
1098
  return parts[0].toLowerCase() + parts.slice(1).map((p) => p[0].toUpperCase() + p.slice(1).toLowerCase()).join("") + "Interface";
1062
1099
  }
1063
1100
  //#endregion
1101
+ //#region src/schema/staticHubSchema.ts
1102
+ function parseStaticHubSchema(value) {
1103
+ const root = expectRecord(value, "hub schema");
1104
+ const rawServices = root.services === void 0 ? [] : expectArray(root.services, "services");
1105
+ const interfaceSchemas = expectArray(root.interfaceSchemas, "interfaceSchemas").map((schema, index) => parseInterfaceSchema(schema, `interfaceSchemas[${index}]`));
1106
+ const schemasByKey = /* @__PURE__ */ new Map();
1107
+ for (const schema of interfaceSchemas) {
1108
+ const key = interfaceKey(schema.id, schema.hash);
1109
+ if (schemasByKey.has(key)) throw new Error(`interfaceSchemas contains duplicate ${schema.id}@${schema.hash}`);
1110
+ schemasByKey.set(key, schema);
1111
+ }
1112
+ const services = rawServices.map((service, index) => parseService(service, `services[${index}]`));
1113
+ const serviceIds = /* @__PURE__ */ new Set();
1114
+ for (const [serviceIndex, service] of services.entries()) {
1115
+ if (serviceIds.has(service.serviceId)) throw new Error(`Duplicate serviceId '${service.serviceId}'`);
1116
+ serviceIds.add(service.serviceId);
1117
+ const interfaceIds = /* @__PURE__ */ new Set();
1118
+ for (const [interfaceIndex, ref] of service.interfaces.entries()) {
1119
+ if (interfaceIds.has(ref.interfaceId)) throw new Error(`Duplicate interface '${ref.interfaceId}' in service '${service.serviceId}'`);
1120
+ interfaceIds.add(ref.interfaceId);
1121
+ requireResolvedReference(ref, schemasByKey, `services[${serviceIndex}].interfaces[${interfaceIndex}]`);
1122
+ }
1123
+ }
1124
+ const defaultInterface = root.defaultInterface === void 0 ? void 0 : parseInterfaceReference(root.defaultInterface, "defaultInterface");
1125
+ if (defaultInterface !== void 0) {
1126
+ requireResolvedReference(defaultInterface, schemasByKey, "defaultInterface");
1127
+ const rootInterface = services.find((service) => service.serviceId === "")?.interfaces.find((ref) => ref.interfaceId === defaultInterface.interfaceId);
1128
+ if (rootInterface !== void 0 && rootInterface.interfaceHash !== defaultInterface.interfaceHash) throw new Error(`defaultInterface conflicts with root '${defaultInterface.interfaceId}'`);
1129
+ }
1130
+ const prefixes = /* @__PURE__ */ new Set();
1131
+ const bareInterfaces = root.bareInterfaces === void 0 ? void 0 : expectArray(root.bareInterfaces, "bareInterfaces").map((value, index) => {
1132
+ const path = `bareInterfaces[${index}]`;
1133
+ const binding = expectRecord(value, path);
1134
+ if (typeof binding.prefix !== "string") throw new Error(`${path}.prefix must be a string`);
1135
+ validateBarePrefix(binding.prefix);
1136
+ if (prefixes.has(binding.prefix)) throw new Error(`Duplicate bare prefix '${binding.prefix}'`);
1137
+ prefixes.add(binding.prefix);
1138
+ const ref = parseInterfaceReference(binding.interface, `${path}.interface`);
1139
+ requireResolvedReference(ref, schemasByKey, `${path}.interface`);
1140
+ if (binding.prefix === "" && defaultInterface !== void 0 && interfaceKey(ref.interfaceId, ref.interfaceHash) !== interfaceKey(defaultInterface.interfaceId, defaultInterface.interfaceHash)) throw new Error("Empty bare prefix conflicts with defaultInterface");
1141
+ return {
1142
+ interface: ref,
1143
+ prefix: binding.prefix
1144
+ };
1145
+ });
1146
+ return {
1147
+ ...root.services === void 0 ? {} : { services },
1148
+ ...defaultInterface === void 0 ? {} : { defaultInterface },
1149
+ ...bareInterfaces === void 0 ? {} : { bareInterfaces },
1150
+ interfaceSchemas
1151
+ };
1152
+ }
1153
+ function parseService(value, path) {
1154
+ const service = expectRecord(value, path);
1155
+ if (typeof service.serviceId !== "string") throw new Error(`${path}.serviceId must be a string`);
1156
+ if (service.serviceId.includes("::") || !/^[\x20-\x7e]*$/.test(service.serviceId)) throw new Error(`${path}.serviceId must be printable ASCII without "::"`);
1157
+ return {
1158
+ serviceId: service.serviceId,
1159
+ interfaces: expectArray(service.interfaces, `${path}.interfaces`).map((ref, index) => parseInterfaceReference(ref, `${path}.interfaces[${index}]`))
1160
+ };
1161
+ }
1162
+ function parseInterfaceReference(value, path) {
1163
+ const ref = expectRecord(value, path);
1164
+ if (typeof ref.interfaceId !== "string" || ref.interfaceId.length === 0) throw new Error(`${path}.interfaceId must be a non-empty string`);
1165
+ if (typeof ref.interfaceHash !== "string" || ref.interfaceHash.length === 0) throw new Error(`${path}.interfaceHash must be a non-empty string`);
1166
+ return {
1167
+ interfaceId: ref.interfaceId,
1168
+ interfaceHash: ref.interfaceHash
1169
+ };
1170
+ }
1171
+ function parseInterfaceSchema(value, path) {
1172
+ const schema = expectRecord(value, path);
1173
+ if (typeof schema.id !== "string" || schema.id.length === 0) throw new Error(`${path}.id must be a non-empty string`);
1174
+ if (typeof schema.hash !== "string" || schema.hash.length === 0) throw new Error(`${path}.hash must be a non-empty string`);
1175
+ if (typeof schema.methods !== "object" || schema.methods === null || Array.isArray(schema.methods)) throw new Error(`${path}.methods must be an object`);
1176
+ for (const [name, method] of Object.entries(schema.methods)) {
1177
+ if (name.length === 0) throw new Error(`${path}.methods keys must be non-empty strings`);
1178
+ const parsed = expectRecord(method, `${path}.methods.${name}`);
1179
+ if (!Object.hasOwn(parsed, "params")) throw new Error(`${path}.methods.${name}.params is required`);
1180
+ }
1181
+ const typed = schema;
1182
+ validateInterfaceErrors(typed);
1183
+ const computedHash = computeInterfaceHash(typed);
1184
+ if (computedHash !== typed.hash) throw new Error(`${path} hash mismatch for ${typed.id}: declared ${typed.hash}, computed ${computedHash}`);
1185
+ return typed;
1186
+ }
1187
+ function requireResolvedReference(ref, schemasByKey, path) {
1188
+ if (!schemasByKey.has(interfaceKey(ref.interfaceId, ref.interfaceHash))) throw new Error(`${path} ${formatReference(ref)} does not resolve to an interface schema`);
1189
+ }
1190
+ function formatReference(ref) {
1191
+ return `${ref.interfaceId}@${ref.interfaceHash}`;
1192
+ }
1193
+ function interfaceKey(interfaceId, interfaceHash) {
1194
+ return `${interfaceId}\0${interfaceHash}`;
1195
+ }
1196
+ function expectRecord(value, path) {
1197
+ if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error(`${path} must be an object`);
1198
+ return value;
1199
+ }
1200
+ function expectArray(value, path) {
1201
+ if (!Array.isArray(value)) throw new Error(`${path} must be an array`);
1202
+ return value;
1203
+ }
1204
+ //#endregion
1205
+ //#region src/schema/codegen/generateTsContract.ts
1206
+ /** Generate one reusable definition per schema and immutable, typed route descriptors. */
1207
+ function generateTsContract(document, options = {}) {
1208
+ const contract = parseStaticHubSchema(document);
1209
+ const imports = options.linkRpcImport ?? "@hediet/linkrpc";
1210
+ const chunks = [
1211
+ `import { applicationError, rpcError, bareInterfaceTarget, defaultInterfaceTarget, interfaceTarget, InterfaceDefinition, notificationType, requestType, type BareInterfaceTarget, type DefaultInterfaceTarget, type QualifiedInterfaceTarget, type LinkRpcInterfaceSchema } from ${JSON.stringify(imports)};`,
1212
+ "import { z } from \"zod\";",
1213
+ ""
1214
+ ];
1215
+ const names = /* @__PURE__ */ new Set([
1216
+ "applicationError",
1217
+ "rpcError",
1218
+ "bareInterfaceTarget",
1219
+ "defaultInterfaceTarget",
1220
+ "interfaceTarget",
1221
+ "InterfaceDefinition",
1222
+ "notificationType",
1223
+ "requestType",
1224
+ "LinkRpcInterfaceSchema",
1225
+ "z",
1226
+ "BareInterfaceTarget",
1227
+ "DefaultInterfaceTarget",
1228
+ "QualifiedInterfaceTarget"
1229
+ ]);
1230
+ const usedInterfaceKeys = /* @__PURE__ */ new Set();
1231
+ const usedBindingKeys = /* @__PURE__ */ new Set();
1232
+ const definitionNames = /* @__PURE__ */ new Map();
1233
+ function allocate(name, key) {
1234
+ if (!/^[$A-Z_a-z][$\w]*$/.test(name) || reservedWords.has(name) || name.startsWith("$contract")) throw new Error(`Invalid TypeScript export name '${name}' for '${key}'`);
1235
+ if (names.has(name)) throw new Error(`Generated name '${name}' collides for '${key}'; provide explicit naming overrides`);
1236
+ names.add(name);
1237
+ return name;
1238
+ }
1239
+ const schemas = [...contract.interfaceSchemas].sort((a, b) => compare(`${a.id}@${a.hash}`, `${b.id}@${b.hash}`));
1240
+ for (const [index, schema] of schemas.entries()) {
1241
+ const key = `${schema.id}@${schema.hash}`;
1242
+ usedInterfaceKeys.add(key);
1243
+ const name = allocate(options.interfaceNames?.[key] ?? `${identifier(schema.id)}Interface`, key);
1244
+ definitionNames.set(key, name);
1245
+ const source = generateTsInterface(schema, {
1246
+ exportName: "definition",
1247
+ preserveWireSchema: true,
1248
+ omitImports: true
1249
+ });
1250
+ chunks.push(`namespace $contract${index} {`, source.trimEnd().split("\n").map((line) => ` ${line}`).join("\n"), "}");
1251
+ chunks.push(`export const ${name}: typeof $contract${index}.definition = $contract${index}.definition;`, "");
1252
+ }
1253
+ function binding(ref, key, derived, type, expression) {
1254
+ usedBindingKeys.add(key);
1255
+ const name = allocate(options.bindingNames?.[key] ?? derived, key);
1256
+ const definition = definitionNames.get(`${ref.interfaceId}@${ref.interfaceHash}`);
1257
+ chunks.push(`export const ${name}: ${type}<typeof ${definition}> = ${expression(definition)};`);
1258
+ }
1259
+ for (const service of [...contract.services ?? []].sort((a, b) => compare(a.serviceId, b.serviceId))) for (const ref of [...service.interfaces].sort((a, b) => compare(a.interfaceId, b.interfaceId))) {
1260
+ const key = `${ref.interfaceId}@${ref.interfaceHash}`;
1261
+ const base = identifier(ref.interfaceId);
1262
+ binding(ref, service.serviceId === "" ? `root:${key}` : `service:${service.serviceId}:${key}`, service.serviceId === "" ? `${base}Root` : `${identifier(service.serviceId)}${upper(base)}Service`, "QualifiedInterfaceTarget", (name) => `interfaceTarget(${name}, { serviceId: ${JSON.stringify(service.serviceId)} })`);
1263
+ }
1264
+ if (contract.defaultInterface) binding(contract.defaultInterface, "default", `${identifier(contract.defaultInterface.interfaceId)}Default`, "DefaultInterfaceTarget", (name) => `defaultInterfaceTarget(${name})`);
1265
+ for (const bare of [...contract.bareInterfaces ?? []].sort((a, b) => compare(a.prefix, b.prefix))) binding(bare.interface, `bare:${bare.prefix}`, `${identifier(bare.prefix || bare.interface.interfaceId)}Bare`, "BareInterfaceTarget", (name) => `bareInterfaceTarget(${name}, { prefix: ${JSON.stringify(bare.prefix)} })`);
1266
+ for (const [overrides, used] of [[options.interfaceNames, usedInterfaceKeys], [options.bindingNames, usedBindingKeys]]) for (const key of Object.keys(overrides ?? {})) if (!used.has(key)) throw new Error(`Unknown naming override '${key}'`);
1267
+ return `${chunks.join("\n")}\n`;
1268
+ }
1269
+ function identifier(value) {
1270
+ const name = value.split(/[^A-Za-z0-9_$]+/).filter(Boolean).map((word, index) => index === 0 ? word[0].toLowerCase() + word.slice(1) : upper(word)).join("") || "interface";
1271
+ return /^[0-9]/.test(name) ? `_${name}` : name;
1272
+ }
1273
+ function upper(value) {
1274
+ return value[0].toUpperCase() + value.slice(1);
1275
+ }
1276
+ function compare(a, b) {
1277
+ return a < b ? -1 : a > b ? 1 : 0;
1278
+ }
1279
+ const reservedWords = new Set("break case catch class const continue debugger default delete do else enum export extends false finally for function if import in instanceof new null return super switch this throw true try typeof var void while with implements interface let package private protected public static yield await".split(" "));
1280
+ //#endregion
1281
+ //#region src/protocol/methodName.ts
1282
+ /**
1283
+ * Parse a JSON-RPC method string. Returns `undefined` if any segment is
1284
+ * empty or the segment count is out of range.
1285
+ */
1286
+ function parseMethodName(method) {
1287
+ const parts = method.split("::");
1288
+ if (parts.length === 3 && parts[0].length === 0 && parts[1].length > 0 && parts[2].length > 0) return {
1289
+ kind: "interface",
1290
+ interfaceId: parts[1],
1291
+ member: parts[2]
1292
+ };
1293
+ if (parts.some((p) => p.length === 0)) return void 0;
1294
+ if (parts.length === 1) return {
1295
+ kind: "bare",
1296
+ member: parts[0]
1297
+ };
1298
+ if (parts.length === 2) return {
1299
+ kind: "interface",
1300
+ interfaceId: parts[0],
1301
+ member: parts[1]
1302
+ };
1303
+ if (parts.length === 3) return {
1304
+ kind: "full",
1305
+ serviceId: parts[0],
1306
+ interfaceId: parts[1],
1307
+ member: parts[2]
1308
+ };
1309
+ }
1310
+ /**
1311
+ * Convert a parsed method string to a {@link CallTarget} for capability
1312
+ * matching. Interface-form calls are addressed to the root service —
1313
+ * represented as an empty `serviceId`. Throws on `bare` form (no
1314
+ * interface context) and on malformed input.
1315
+ */
1316
+ function methodNameToTarget(method) {
1317
+ const p = parseMethodName(method);
1318
+ if (!p) throw new Error(`linkrpc: malformed method: ${method}`);
1319
+ if (p.kind === "bare") throw new Error(`linkrpc: bare method has no interface context: ${method}`);
1320
+ if (p.kind === "interface") return {
1321
+ serviceId: "",
1322
+ interfaceId: p.interfaceId,
1323
+ member: p.member
1324
+ };
1325
+ return {
1326
+ serviceId: p.serviceId,
1327
+ interfaceId: p.interfaceId,
1328
+ member: p.member
1329
+ };
1330
+ }
1331
+ //#endregion
1332
+ //#region src/protocol/signedObject.ts
1333
+ /**
1334
+ * The HubRPC **signed-object** standard: one rule for committing a
1335
+ * signature (and a content hash) over any JSON object.
1336
+ *
1337
+ * > The signature `obj.$hubrpcSignature[domain]` covers
1338
+ * > `jcs({ [domainValue(domain)]: obj without $hubrpcSignature and $hubrpcUnsigned })`.
1339
+ *
1340
+ * Both reserved keys are stripped before canonicalization — on **every**
1341
+ * object, call or capability alike:
1342
+ *
1343
+ * - {@link HUBRPC_SIGNATURE_KEY} (`$hubrpcSignature`) holds the signature
1344
+ * itself (a domain-keyed map of `{ keyId, sig }` entries). It can't sign
1345
+ * itself, so it's excluded.
1346
+ * - {@link HUBRPC_UNSIGNED_KEY} (`$hubrpcUnsigned`) holds extrinsic
1347
+ * *attachments* (e.g. the presented capability bag) authored by someone
1348
+ * other than this signer, so it's excluded too.
1349
+ *
1350
+ * Domain separation is intrinsic: the per-domain {@link signingDomainValue}
1351
+ * is the sole key of the canonicalized wrapper object. A `"call"` signature
1352
+ * can therefore never verify as a `"capability"` (or vice-versa). The domain
1353
+ * is *supplied by context* by signer and verifier;
1354
+ * the on-wire `$hubrpcSignature` key is a convenience index, never a trust
1355
+ * claim.
1356
+ *
1357
+ * This module is pure (no private keys, no `crypto.verify`): it only defines
1358
+ * the canonical bytes. Signing/verifying live in `../identity/signedObject`.
1359
+ */
1360
+ /** Reserved wire-key: the signature map `{ [domain]: SignatureEnvelope }`. */
1361
+ const HUBRPC_SIGNATURE_KEY = "$hubrpcSignature";
1362
+ /** Reserved wire-key: extrinsic unsigned attachments (e.g. the capability bag). */
1363
+ const HUBRPC_UNSIGNED_KEY = "$hubrpcUnsigned";
1364
+ /** Reserved wire-key: signed call meta, present only on calls (dodges the JSON-RPC param namespace). */
1365
+ const HUBRPC_META_KEY = "$hubrpc";
1366
+ /** @deprecated Use {@link HUBRPC_SIGNATURE_KEY}. */
1367
+ const LINKRPC_SIGNATURE_KEY = HUBRPC_SIGNATURE_KEY;
1368
+ /** @deprecated Use {@link HUBRPC_UNSIGNED_KEY}. */
1369
+ const LINKRPC_UNSIGNED_KEY = HUBRPC_UNSIGNED_KEY;
1370
+ /** @deprecated Use {@link HUBRPC_META_KEY}. */
1371
+ const LINKRPC_META_KEY = HUBRPC_META_KEY;
1372
+ /**
1373
+ * The domain-separation key. It includes the frozen signature-suite version
1374
+ * and becomes the sole key of the object passed to JCS.
1375
+ */
1376
+ function signingDomainValue(domain) {
1377
+ return `hubrpc-sig/v1/${domain}`;
1378
+ }
1379
+ /**
1380
+ * THE bytes a signature commits to (and a content hash hashes) for `obj` in
1381
+ * `domain`: `jcs({ [domainValue(domain)]: obj minus the two reserved keys })`.
1382
+ */
1383
+ function signingInput(domain, obj) {
1384
+ const signed = { ...obj };
1385
+ delete signed[HUBRPC_SIGNATURE_KEY];
1386
+ delete signed[HUBRPC_UNSIGNED_KEY];
1387
+ return jcsCanonicalizeBytes({ [signingDomainValue(domain)]: signed });
1388
+ }
1389
+ /**
1390
+ * Content identity of a signed object: `base64url(sha256(signingInput(...)))`.
1391
+ * The single operation behind both a capability's `callBind.payloadHash`
1392
+ * (domain `"call"`) and a child capability's `parentHash` (domain
1393
+ * `"capability"`).
1394
+ */
1395
+ function signedHash(domain, obj) {
1396
+ return bytesToBase64Url(sha256$1(signingInput(domain, obj)));
1397
+ }
1398
+ /** Read the signature envelope for `domain` off an object's `$hubrpcSignature` map. */
1399
+ function readSignature(obj, domain) {
1400
+ const sigs = obj[HUBRPC_SIGNATURE_KEY];
1401
+ if (sigs === null || typeof sigs !== "object") return void 0;
1402
+ const entry = sigs[domain];
1403
+ if (entry === null || typeof entry !== "object") return void 0;
1404
+ const { keyId, sig } = entry;
1405
+ if (typeof keyId !== "string" || typeof sig !== "string") return void 0;
1406
+ return {
1407
+ keyId,
1408
+ sig
1409
+ };
1410
+ }
1411
+ /** The {@link KeyId} the signer used for `domain` (the unsigned routing hint), if any. */
1412
+ function getKeyId(obj, domain) {
1413
+ return readSignature(obj, domain)?.keyId;
1414
+ }
1415
+ /**
1416
+ * Return a copy of `obj` with `$hubrpcSignature[domain]` set to `envelope`,
1417
+ * preserving any sibling-domain signatures already present.
1418
+ */
1419
+ function withSignature(obj, domain, envelope) {
1420
+ const prev = obj[HUBRPC_SIGNATURE_KEY];
1421
+ return {
1422
+ ...obj,
1423
+ [HUBRPC_SIGNATURE_KEY]: {
1424
+ ...prev,
1425
+ [domain]: envelope
1426
+ }
1427
+ };
1428
+ }
1429
+ //#endregion
1430
+ //#region src/protocol/linkRpcEnvelope.ts
1431
+ /**
1432
+ * The linkrpc signing/cap system accepts only plain-object user params (or
1433
+ * none). Reject arrays/primitives at the boundary so "strip `$hubrpc*`, the
1434
+ * rest is the signed user params" stays unambiguous.
1435
+ */
1436
+ function requireObjectParams(userParams) {
1437
+ if (userParams === void 0) return {};
1438
+ if (userParams === null || typeof userParams !== "object" || Array.isArray(userParams)) throw new Error("linkrpc: user params must be a plain JSON object or undefined");
1439
+ return userParams;
1440
+ }
1441
+ /**
1442
+ * Return the application-authored portion of a LinkRPC params value.
1443
+ *
1444
+ * Routing and gate layers must continue forwarding the original wire params so
1445
+ * every chained hub can independently verify the envelope. Terminal dispatch
1446
+ * may use this derived value for application-schema validation without mutating
1447
+ * or replacing the wire message.
1448
+ */
1449
+ function stripLinkRpcWireMeta(wireParams) {
1450
+ if (wireParams === void 0) return void 0;
1451
+ if (wireParams === null || typeof wireParams !== "object" || Array.isArray(wireParams)) return wireParams;
1452
+ const object = wireParams;
1453
+ const reserved = [
1454
+ HUBRPC_META_KEY,
1455
+ HUBRPC_SIGNATURE_KEY,
1456
+ HUBRPC_UNSIGNED_KEY
1457
+ ];
1458
+ if (!reserved.some((key) => key in object)) return object;
1459
+ const userParams = {};
1460
+ for (const key of Object.keys(object)) {
1461
+ if (reserved.includes(key)) continue;
1462
+ userParams[key] = object[key];
1463
+ }
1464
+ return userParams;
1465
+ }
1466
+ //#endregion
1064
1467
  //#region src/hub/common/reflection.interfaces.ts
1065
1468
  const defaultsInterface = defineInterface({
1066
1469
  id: "hubrpc.defaults",
@@ -1194,1106 +1597,251 @@ const schemasInterface = defineInterface({
1194
1597
  /** A full `SvcInterfaceSchema`. */
1195
1598
  schema: unknown() })) });
1196
1599
  //#endregion
1197
- //#region src/connection/streaming.ts
1198
- /**
1199
- * Direction a {@link STREAM_METHOD} message travels, relative to the
1200
- * request it is correlated with. Made explicit on the wire so that a
1201
- * middlebox (the hub) can *author* a stream message — e.g. inject a
1202
- * cancel when a caller disconnects — without an inbound message whose
1203
- * arrival link it could infer the direction from.
1204
- */
1205
- const StreamDir = {
1206
- /** callee → caller: progress, partial results. Routes like a response. */
1207
- toCaller: "toCaller",
1208
- /** caller → callee: input, cancellation, keepalive ping. */
1209
- toCallee: "toCallee"
1210
- };
1211
- /**
1212
- * Reserved control verbs. A control message carries no app `payload`; it
1213
- * is interpreted by the runtime / hub itself, independently of the
1214
- * originating method's stream schemas, so *any* request is cancellable
1215
- * and keep-alive-able even when it declares no app stream.
1216
- */
1217
- const StreamControlType = {
1218
- /**
1219
- * Ask the callee to abort the in-flight request. Always {@link
1220
- * StreamDir.toCallee}. The callee surfaces this as an `AbortSignal`
1221
- * and is expected to settle the request (typically with a
1222
- * `cancelled` error response).
1223
- */
1224
- cancel: "cancel",
1225
- /**
1226
- * Keepalive / liveness probe. Resets the hub's per-request idle timer
1227
- * so a long-running call is not reaped, and lets either side actively
1228
- * probe the peer. Carries a `nonce` the peer echoes in its {@link
1229
- * StreamControlType.pong}. May travel in either direction. Emitted
1230
- * automatically (without awaiting a pong) by the channel for streaming
1231
- * calls; see {@link STREAM_METHOD}.
1232
- */
1233
- ping: "ping",
1234
- /**
1235
- * Reply to a {@link StreamControlType.ping}, echoing the ping's
1236
- * `nonce` so the prober can correlate it. Travels opposite the ping.
1237
- * Carries no app effect — purely a liveness acknowledgement.
1238
- */
1239
- pong: "pong"
1240
- };
1241
- /**
1242
- * Well-known `control.reason` strings. The set is **open** — any string
1243
- * is valid on the wire; these are the reasons linkrpc itself emits.
1244
- */
1245
- const StreamControlReason = {
1246
- /**
1247
- * The caller's transport dropped while the request was in flight; the
1248
- * hub cancels the callee's now-orphaned work.
1249
- */
1250
- clientDisconnected: "clientDisconnected",
1251
- /**
1252
- * The request exceeded the hub's idle timeout with no stream activity
1253
- * (no ping, no stream message). See {@link STREAM_METHOD} docs.
1254
- */
1255
- idleTimeout: "idleTimeout"
1256
- };
1257
- /**
1258
- * Streaming sub-protocol. Long-running requests can emit additional
1259
- * notifications correlated with the original call in either direction
1260
- * (callee → caller progress, caller → callee input / cancellation /
1261
- * keepalive).
1262
- *
1263
- * Wire form: a JSON-RPC notification whose method is
1264
- * `${streamInterface.info.id}::send` and whose params match
1265
- * `streamInterface.members.send.paramsSchema`. The hub routes purely on
1266
- * `requestId` (following the originating request's established path, like
1267
- * a response); the originating request's capability authorization covers
1268
- * its full stream lifetime, so stream notifications carry no method /
1269
- * interface namespace of their own. App `payload` contents are typed per
1270
- * request via the originating method's
1271
- * `RequestType.clientStreamSchema` / `serverStreamSchema`; reserved
1272
- * `control` messages are schema-independent.
1273
- *
1274
- * Idle timeout: a request that never streams (no `control` ping, no
1275
- * stream message) is subject to the hub's per-request idle timeout
1276
- * (default 30 minutes) — at which point it is cancelled and its caller
1277
- * gets a `requestTimeout` error. This bounds the hub's pending-request
1278
- * table, which is also what protects it from a slow-loris / DDoS that
1279
- * opens requests and never completes them. Streaming-enabled calls keep
1280
- * themselves alive by emitting a periodic {@link StreamControlType.ping}.
1281
- *
1282
- * The interface is declarative: nobody calls
1283
- * `LinkRpcConnection.register(streamInterface, ...)`. The channel and the
1284
- * hub intercept `$stream::send` directly. The definition exists so
1285
- * reflection / consent UIs can describe the wire shape and so all wire
1286
- * constants derive from one source.
1287
- */
1288
- const streamInterface = defineInterface({
1289
- id: "$stream",
1290
- description: "Reserved sub-protocol for in-flight stream messages correlated to a request by `requestId`."
1291
- }, { send: notificationType(object({
1292
- /**
1293
- * Id of the in-flight request the message belongs to.
1294
- * The hub rewrites this across the forwarding boundary
1295
- * in the same way it rewrites response ids.
1296
- */
1297
- requestId: union([number(), string()]),
1298
- /**
1299
- * Which way this message travels relative to the request.
1300
- * Explicit so the hub can author messages (e.g. cancel on
1301
- * disconnect) without inferring direction from arrival.
1302
- */
1303
- dir: enum$1(["toCaller", "toCallee"]),
1304
- /**
1305
- * Reserved control verb (cancel / ping / pong). Mutually
1306
- * exclusive with `payload` in practice: a control message
1307
- * is interpreted by the runtime, not the application.
1308
- */
1309
- control: optional(object({
1310
- type: enum$1([
1311
- "cancel",
1312
- "ping",
1313
- "pong"
1314
- ]),
1315
- /** Open-set human/diagnostic reason; see {@link StreamControlReason}. */
1316
- reason: optional(string()),
1317
- /**
1318
- * Correlation token. A `ping` carries a fresh nonce;
1319
- * the matching `pong` echoes it back so the prober
1320
- * can resolve the right outstanding probe.
1321
- */
1322
- nonce: optional(string())
1323
- })),
1324
- /** Opaque app payload — typed per call by the request's stream schema. */
1325
- payload: optional(unknown())
1326
- }), { description: "Emit a stream message tied to the request `requestId`." }) });
1327
- /**
1328
- * Full wire method name for stream notifications. Pulled from the
1329
- * interface object so we have a single source of truth.
1330
- */
1331
- const STREAM_METHOD = `${streamInterface.info.id}::send`;
1332
- //#endregion
1333
- //#region src/protocol/jsonRpc.ts
1334
- const ErrorCode = {
1335
- parseError: -32700,
1336
- invalidRequest: -32600,
1337
- methodNotFound: -32601,
1338
- invalidParams: -32602,
1339
- internalError: -32603,
1340
- /** Caller is authenticated but lacks a capability covering the call. */
1341
- permissionRequired: -32401,
1342
- /** The peer a request was routed to detached before it could respond. */
1343
- peerDisconnected: -32402,
1344
- /** The request exceeded the hub's idle timeout with no stream activity. */
1345
- requestTimeout: -32403,
1346
- /** The request was cancelled (by the caller, or by the hub on disconnect). */
1347
- cancelled: -32800
1348
- };
1349
- function isRequest(m) {
1350
- return typeof m.method === "string" && m.id !== void 0;
1351
- }
1352
- function isNotification(m) {
1353
- return typeof m.method === "string" && m.id === void 0;
1354
- }
1355
- function isResponse(m) {
1356
- return typeof m.method !== "string";
1357
- }
1358
- //#endregion
1359
- //#region src/crypto/cryptoProvider.ts
1360
- const _b64UrlAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
1361
- const _b64UrlReverse = (() => {
1362
- const m = {};
1363
- for (let i = 0; i < 64; i++) m[_b64UrlAlphabet[i]] = i;
1364
- return m;
1365
- })();
1366
- function bytesToBase64Url(bytes) {
1367
- let out = "";
1368
- let i = 0;
1369
- for (; i + 3 <= bytes.length; i += 3) {
1370
- const n = bytes[i] << 16 | bytes[i + 1] << 8 | bytes[i + 2];
1371
- out += _b64UrlAlphabet[n >> 18 & 63] + _b64UrlAlphabet[n >> 12 & 63] + _b64UrlAlphabet[n >> 6 & 63] + _b64UrlAlphabet[n & 63];
1372
- }
1373
- const rem = bytes.length - i;
1374
- if (rem === 1) {
1375
- const n = bytes[i] << 16;
1376
- out += _b64UrlAlphabet[n >> 18 & 63] + _b64UrlAlphabet[n >> 12 & 63];
1377
- } else if (rem === 2) {
1378
- const n = bytes[i] << 16 | bytes[i + 1] << 8;
1379
- out += _b64UrlAlphabet[n >> 18 & 63] + _b64UrlAlphabet[n >> 12 & 63] + _b64UrlAlphabet[n >> 6 & 63];
1600
+ //#region src/connection/requestTimeout.ts
1601
+ const DEFAULT_RPC_TIMEOUT_MS = 5e3;
1602
+ async function withRpcTimeout(request, target, timeoutMs = DEFAULT_RPC_TIMEOUT_MS) {
1603
+ if (timeoutMs <= 0) return request;
1604
+ let timer;
1605
+ const timeout = new Promise((_resolve, reject) => {
1606
+ timer = setTimeout(() => {
1607
+ const reason = `${target} timed out after ${timeoutMs}ms`;
1608
+ try {
1609
+ Promise.resolve(request.cancel(reason)).catch(() => {});
1610
+ } finally {
1611
+ try {
1612
+ request.dispose?.(reason);
1613
+ } catch {}
1614
+ }
1615
+ reject(new Error(reason));
1616
+ }, timeoutMs);
1617
+ });
1618
+ try {
1619
+ return await Promise.race([request, timeout]);
1620
+ } finally {
1621
+ if (timer !== void 0) clearTimeout(timer);
1380
1622
  }
1381
- return out;
1382
1623
  }
1383
- function base64UrlToBytes$1(s) {
1384
- const len = s.length;
1385
- const fullGroups = Math.floor(len / 4);
1386
- const rem = len % 4;
1387
- if (rem === 1) throw new Error("base64UrlToBytes: invalid length");
1388
- const outLen = fullGroups * 3 + (rem === 0 ? 0 : rem - 1);
1389
- const out = new Uint8Array(outLen);
1390
- let oi = 0;
1391
- let i = 0;
1392
- for (let g = 0; g < fullGroups; g++, i += 4) {
1393
- const n = _lookup(s, i) << 18 | _lookup(s, i + 1) << 12 | _lookup(s, i + 2) << 6 | _lookup(s, i + 3);
1394
- out[oi++] = n >> 16 & 255;
1395
- out[oi++] = n >> 8 & 255;
1396
- out[oi++] = n & 255;
1397
- }
1398
- if (rem === 2) {
1399
- const n = _lookup(s, i) << 18 | _lookup(s, i + 1) << 12;
1400
- out[oi++] = n >> 16 & 255;
1401
- } else if (rem === 3) {
1402
- const n = _lookup(s, i) << 18 | _lookup(s, i + 1) << 12 | _lookup(s, i + 2) << 6;
1403
- out[oi++] = n >> 16 & 255;
1404
- out[oi++] = n >> 8 & 255;
1624
+ //#endregion
1625
+ //#region src/hub/common/directoryWalk.ts
1626
+ async function fetchDirectory(channel, opts = {}) {
1627
+ const method = opts.target ? `${opts.target}::hubrpc.directory::list` : "hubrpc.directory::list";
1628
+ const all = [];
1629
+ let cursor;
1630
+ while (true) {
1631
+ const params = {};
1632
+ if (opts.interfaceId !== void 0) params.interfaceId = opts.interfaceId;
1633
+ if (opts.interfaceIdPrefix !== void 0) params.interfaceIdPrefix = opts.interfaceIdPrefix;
1634
+ if (opts.serviceId !== void 0) params.serviceId = opts.serviceId;
1635
+ if (opts.serviceIdScopes !== void 0) params.serviceIdScopes = [...opts.serviceIdScopes];
1636
+ if (opts.limit !== void 0) params.limit = opts.limit;
1637
+ if (opts.timeoutMs !== void 0) params.timeoutMs = opts.timeoutMs;
1638
+ if (cursor !== void 0) params.cursor = cursor;
1639
+ const call = channel.sendRequestWithStream(method, params, { interfaceHash: directoryInterface.schemaHash });
1640
+ const target = opts.target ?? "<root>";
1641
+ const page = await withRpcTimeout(Object.assign(call.result, {
1642
+ cancel: (reason) => call.cancel(reason),
1643
+ dispose: (reason) => call.dispose?.(reason)
1644
+ }), `hub directory service '${target}'`, opts.timeoutMs ?? 5e3);
1645
+ for (const it of page.items) all.push({
1646
+ serviceId: it.serviceId,
1647
+ interfaceId: it.interfaceId,
1648
+ hash: it.interfaceHash,
1649
+ ...it.path !== void 0 ? { path: it.path } : {},
1650
+ ...it.serviceDescription !== void 0 ? { serviceDescription: it.serviceDescription } : {},
1651
+ ...Array.isArray(it.rootPrincipalSets) ? { rootPrincipalSets: it.rootPrincipalSets } : {},
1652
+ ...Array.isArray(it.reachableServiceIds) ? { reachableServiceIds: it.reachableServiceIds } : {}
1653
+ });
1654
+ if (!page.nextCursor) break;
1655
+ cursor = page.nextCursor;
1405
1656
  }
1406
- return out;
1657
+ return all;
1407
1658
  }
1408
- function _lookup(s, i) {
1409
- const v = _b64UrlReverse[s[i]];
1410
- if (v === void 0) throw new Error(`base64UrlToBytes: invalid char at ${i}`);
1411
- return v;
1659
+ async function fetchSchema(channel, interfaceId, hash, target) {
1660
+ const method = target ? `${target}::hubrpc.schemas::get` : "hubrpc.schemas::get";
1661
+ const params = { interfaceId };
1662
+ if (hash !== void 0) params.hash = hash;
1663
+ return (await channel.sendRequest(method, params)).schema;
1412
1664
  }
1413
- /** Role-tag prefix on a {@link KeyId}'s inline-key form. */
1414
- const KEY_ID_PREFIX = "key:";
1415
- /** Role-tag prefix that wraps a genesis {@link KeyId} into a {@link PrincipalId}. */
1416
- const PRINCIPAL_PREFIX = "id:";
1417
- /** The inline {@link KeyId} for a raw public key: `"key:" + b64url(pk)`. */
1418
- function keyIdForPublicKey(pk) {
1419
- return KEY_ID_PREFIX + bytesToBase64Url(pk);
1665
+ function findMethodInSchema(schema, methodName) {
1666
+ return schema.methods[methodName];
1420
1667
  }
1421
- /** The perpetual {@link PrincipalId} for a raw public key: `"id:key:" + b64url(pk)`. */
1422
- function principalForPublicKey(pk) {
1423
- return "id:" + keyIdForPublicKey(pk);
1668
+ /** Default recursion depth when walking the bus. */
1669
+ const DEFAULT_WALK_DEPTH = 5;
1670
+ const DIRECTORY_INTERFACE_ID = "hubrpc.directory";
1671
+ const ROOT_NODE_KEY = "\0root";
1672
+ /** Segment-aware service-id pattern matching (`foo` includes `foo/bar`, not `foobar`). */
1673
+ function serviceIdMatchesPattern(serviceId, pattern) {
1674
+ if ("exact" in pattern) return serviceId === pattern.exact;
1675
+ return pattern.prefix === "" || serviceId === pattern.prefix || serviceId.startsWith(`${pattern.prefix}/`);
1424
1676
  }
1425
- /**
1426
- * The genesis {@link KeyId} embedded in a perpetual {@link PrincipalId}
1427
- * (`"id:" + keyId`). Throws if `principal` is not a well-formed `id:` name.
1428
- */
1429
- function keyIdForPrincipal(principal) {
1430
- if (!principal.startsWith("id:")) throw new Error(`malformed principal (missing "id:" prefix): ${principal}`);
1431
- return principal.slice(3);
1677
+ /** Whether a service id is included by a union of patterns. `undefined` means unfiltered. */
1678
+ function serviceIdMatchesScopes(serviceId, scopes) {
1679
+ return scopes === void 0 || scopes.some((pattern) => serviceIdMatchesPattern(serviceId, pattern));
1432
1680
  }
1433
- /**
1434
- * The raw public key carried inline by a `"key:"` {@link KeyId}. Throws on a
1435
- * missing prefix or malformed base64url.
1436
- */
1437
- function publicKeyForKeyId(keyId) {
1438
- if (!keyId.startsWith("key:")) throw new Error(`unsupported keyId (expected "${KEY_ID_PREFIX}" prefix): ${keyId}`);
1439
- return base64UrlToBytes$1(keyId.slice(4));
1681
+ /** Canonicalize a pattern union by removing duplicates and contained patterns. */
1682
+ function normalizeServiceIdScopes(scopes) {
1683
+ const unique = /* @__PURE__ */ new Map();
1684
+ for (const pattern of scopes) unique.set(patternKey(pattern), pattern);
1685
+ const patterns = [...unique.values()];
1686
+ return patterns.filter((candidate, index) => !patterns.some((other, otherIndex) => index !== otherIndex && patternContains(other, candidate))).sort((a, b) => patternKey(a).localeCompare(patternKey(b)));
1440
1687
  }
1441
- /**
1442
- * Resolve the verifying public key for `(principal, keyId)`, or `undefined`
1443
- * to **reject** (fail closed). This is the single seam every verifier goes
1444
- * through, replacing the old "the id *is* the key" decode.
1445
- *
1446
- * Phase 1 supports only inline genesis keys: the `keyId` must be a `"key:"`
1447
- * form and the principal must be exactly `"id:" + keyId` (genesis
1448
- * self-certification by string equality). `keydoc:`/rotation forms resolve
1449
- * through `bindings`/`timeMs` later and are intentionally not handled here.
1450
- */
1451
- function resolveSigningKey(args) {
1452
- const { principal, keyId } = args;
1453
- if (!keyId.startsWith("key:")) return;
1454
- if ("id:" + keyId !== principal) return;
1455
- try {
1456
- return { publicKey: base64UrlToBytes$1(keyId.slice(4)) };
1457
- } catch {
1458
- return;
1688
+ /** Pairwise intersection of two pattern unions. */
1689
+ function intersectServiceIdScopes(left, right) {
1690
+ const intersections = [];
1691
+ for (const a of left) for (const b of right) {
1692
+ const intersection = intersectPattern(a, b);
1693
+ if (intersection !== void 0) intersections.push(intersection);
1459
1694
  }
1695
+ return normalizeServiceIdScopes(intersections);
1460
1696
  }
1461
- //#endregion
1462
- //#region src/protocol/signedObject.ts
1463
- /**
1464
- * The HubRPC **signed-object** standard: one rule for committing a
1465
- * signature (and a content hash) over any JSON object.
1466
- *
1467
- * > The signature `obj.$hubrpcSignature[domain]` covers
1468
- * > `jcs({ [domainValue(domain)]: obj without $hubrpcSignature and $hubrpcUnsigned })`.
1469
- *
1470
- * Both reserved keys are stripped before canonicalization — on **every**
1471
- * object, call or capability alike:
1472
- *
1473
- * - {@link HUBRPC_SIGNATURE_KEY} (`$hubrpcSignature`) holds the signature
1474
- * itself (a domain-keyed map of `{ keyId, sig }` entries). It can't sign
1475
- * itself, so it's excluded.
1476
- * - {@link HUBRPC_UNSIGNED_KEY} (`$hubrpcUnsigned`) holds extrinsic
1477
- * *attachments* (e.g. the presented capability bag) authored by someone
1478
- * other than this signer, so it's excluded too.
1479
- *
1480
- * Domain separation is intrinsic: the per-domain {@link signingDomainValue}
1481
- * is the sole key of the canonicalized wrapper object. A `"call"` signature
1482
- * can therefore never verify as a `"capability"` (or vice-versa). The domain
1483
- * is *supplied by context* by signer and verifier;
1484
- * the on-wire `$hubrpcSignature` key is a convenience index, never a trust
1485
- * claim.
1486
- *
1487
- * This module is pure (no private keys, no `crypto.verify`): it only defines
1488
- * the canonical bytes. Signing/verifying live in `../identity/signedObject`.
1489
- */
1490
- /** Reserved wire-key: the signature map `{ [domain]: SignatureEnvelope }`. */
1491
- const HUBRPC_SIGNATURE_KEY = "$hubrpcSignature";
1492
- /** Reserved wire-key: extrinsic unsigned attachments (e.g. the capability bag). */
1493
- const HUBRPC_UNSIGNED_KEY = "$hubrpcUnsigned";
1494
- /** Reserved wire-key: signed call meta, present only on calls (dodges the JSON-RPC param namespace). */
1495
- const HUBRPC_META_KEY = "$hubrpc";
1496
- /** @deprecated Use {@link HUBRPC_SIGNATURE_KEY}. */
1497
- const LINKRPC_SIGNATURE_KEY = HUBRPC_SIGNATURE_KEY;
1498
- /** @deprecated Use {@link HUBRPC_UNSIGNED_KEY}. */
1499
- const LINKRPC_UNSIGNED_KEY = HUBRPC_UNSIGNED_KEY;
1500
- /** @deprecated Use {@link HUBRPC_META_KEY}. */
1501
- const LINKRPC_META_KEY = HUBRPC_META_KEY;
1502
- /**
1503
- * The domain-separation key. It includes the frozen signature-suite version
1504
- * and becomes the sole key of the object passed to JCS.
1505
- */
1506
- function signingDomainValue(domain) {
1507
- return `hubrpc-sig/v1/${domain}`;
1697
+ function intersectPattern(left, right) {
1698
+ if ("exact" in left) {
1699
+ if ("exact" in right) return left.exact === right.exact ? left : void 0;
1700
+ return serviceIdMatchesPattern(left.exact, right) ? left : void 0;
1701
+ }
1702
+ if ("exact" in right) return serviceIdMatchesPattern(right.exact, left) ? right : void 0;
1703
+ if (serviceIdMatchesPattern(left.prefix, right)) return left;
1704
+ if (serviceIdMatchesPattern(right.prefix, left)) return right;
1508
1705
  }
1509
- /**
1510
- * THE bytes a signature commits to (and a content hash hashes) for `obj` in
1511
- * `domain`: `jcs({ [domainValue(domain)]: obj minus the two reserved keys })`.
1512
- */
1513
- function signingInput(domain, obj) {
1514
- const signed = { ...obj };
1515
- delete signed[HUBRPC_SIGNATURE_KEY];
1516
- delete signed[HUBRPC_UNSIGNED_KEY];
1517
- return jcsCanonicalizeBytes({ [signingDomainValue(domain)]: signed });
1706
+ function patternContains(container, candidate) {
1707
+ if ("exact" in container) return "exact" in candidate && container.exact === candidate.exact;
1708
+ if ("exact" in candidate) return serviceIdMatchesPattern(candidate.exact, container);
1709
+ return serviceIdMatchesPattern(candidate.prefix, container);
1518
1710
  }
1519
- /**
1520
- * Content identity of a signed object: `base64url(sha256(signingInput(...)))`.
1521
- * The single operation behind both a capability's `callBind.payloadHash`
1522
- * (domain `"call"`) and a child capability's `parentHash` (domain
1523
- * `"capability"`).
1524
- */
1525
- function signedHash(domain, obj) {
1526
- return bytesToBase64Url(sha256$1(signingInput(domain, obj)));
1711
+ function patternKey(pattern) {
1712
+ return "exact" in pattern ? `e:${pattern.exact}` : `p:${pattern.prefix}`;
1527
1713
  }
1528
- /** Read the signature envelope for `domain` off an object's `$hubrpcSignature` map. */
1529
- function readSignature(obj, domain) {
1530
- const sigs = obj[HUBRPC_SIGNATURE_KEY];
1531
- if (sigs === null || typeof sigs !== "object") return void 0;
1532
- const entry = sigs[domain];
1533
- if (entry === null || typeof entry !== "object") return void 0;
1534
- const { keyId, sig } = entry;
1535
- if (typeof keyId !== "string" || typeof sig !== "string") return void 0;
1536
- return {
1537
- keyId,
1538
- sig
1539
- };
1714
+ function scopesKey(scopes) {
1715
+ return normalizeServiceIdScopes(scopes).map(patternKey).join("\0");
1540
1716
  }
1541
- /** The {@link KeyId} the signer used for `domain` (the unsigned routing hint), if any. */
1542
- function getKeyId(obj, domain) {
1543
- return readSignature(obj, domain)?.keyId;
1717
+ async function walkHubDetailed(channel, opts = {}) {
1718
+ return new HubDirectoryExplorer(channel, opts).explore();
1544
1719
  }
1545
1720
  /**
1546
- * Return a copy of `obj` with `$hubrpcSignature[domain]` set to `envelope`,
1547
- * preserving any sibling-domain signatures already present.
1721
+ * Backward-compatible wrapper over {@link walkHubDetailed} that returns only the
1722
+ * reachable listings, dropping the inaccessible-directory report.
1548
1723
  */
1549
- function withSignature(obj, domain, envelope) {
1550
- const prev = obj[HUBRPC_SIGNATURE_KEY];
1551
- return {
1552
- ...obj,
1553
- [HUBRPC_SIGNATURE_KEY]: {
1554
- ...prev,
1555
- [domain]: envelope
1556
- }
1557
- };
1724
+ async function walkHub(channel, opts = {}) {
1725
+ return (await walkHubDetailed(channel, opts)).listings;
1558
1726
  }
1559
- //#endregion
1560
- //#region src/protocol/capability.ts
1561
1727
  /**
1562
- * Structural wire-shape check for an untrusted signed capability. This does
1563
- * not establish authenticity; callers must still verify its signature and
1564
- * delegation chain.
1728
+ * Reusable directory graph explorer and watcher.
1729
+ *
1730
+ * The class keeps one native snapshot per directory target. A watch tick
1731
+ * re-lists only its source target; outgoing referral changes then propagate
1732
+ * through affected descendants. Unchanged siblings retain both their snapshot
1733
+ * and watch. Multi-parent targets are queried with the normalized union of all
1734
+ * incoming path scopes.
1565
1735
  */
1566
- function hasSignedCapabilityShape(value) {
1567
- if (!_isRecord(value)) return false;
1568
- if (typeof value.issuer !== "string" || typeof value.audience !== "string" || typeof value.nonce !== "string" || !Array.isArray(value.permissions) || !value.permissions.every(_hasPermissionShape) || value.expiresAtMs !== void 0 && typeof value.expiresAtMs !== "number" || value.parentHash !== void 0 && typeof value.parentHash !== "string") return false;
1569
- return readSignature(value, "capability") !== void 0;
1570
- }
1571
- function _hasPermissionShape(value) {
1572
- if (!_isRecord(value) || !_isRecord(value.target)) return false;
1573
- const target = value.target;
1574
- if (!_hasPatternShape(target.serviceId) || !_hasPatternShape(target.interfaceId) || !Array.isArray(target.members) || !target.members.every(_hasPatternShape) || target.interfaceHash !== void 0 && typeof target.interfaceHash !== "string" || value.canInvoke !== void 0 && typeof value.canInvoke !== "boolean" || value.canDelegate !== void 0 && typeof value.canDelegate !== "boolean") return false;
1575
- if (value.params !== void 0) {
1576
- if (!_isRecord(value.params) || !Object.values(value.params).every(_hasParamMatcherShape)) return false;
1736
+ var HubDirectoryExplorer = class {
1737
+ _channel;
1738
+ _options;
1739
+ _maxDepth;
1740
+ _initialScopes;
1741
+ _nodes = /* @__PURE__ */ new Map();
1742
+ _unlockAttempted = /* @__PURE__ */ new Set();
1743
+ _listeners = /* @__PURE__ */ new Set();
1744
+ _graphListeners = /* @__PURE__ */ new Set();
1745
+ _pendingTicks = /* @__PURE__ */ new Set();
1746
+ _watchRetryTimers = /* @__PURE__ */ new Map();
1747
+ _watchRetryDelays = /* @__PURE__ */ new Map();
1748
+ _initialized = false;
1749
+ _disposed = false;
1750
+ _watching = false;
1751
+ _complete = false;
1752
+ _revision = 0;
1753
+ _work = Promise.resolve();
1754
+ _explorePromise;
1755
+ constructor(_channel, _options = {}) {
1756
+ this._channel = _channel;
1757
+ this._options = _options;
1758
+ this._maxDepth = _options.maxDepth ?? 5;
1759
+ this._initialScopes = normalizeServiceIdScopes(_options.serviceIdScopes ?? [{ prefix: "" }]);
1760
+ this._nodes.set(ROOT_NODE_KEY, this._createRootNode());
1577
1761
  }
1578
- if (value.callBind !== void 0) {
1579
- if (!_isRecord(value.callBind) || value.callBind.alg !== "sha256" || typeof value.callBind.payloadHash !== "string") return false;
1762
+ /** Perform a fresh bounded exploration without starting watches. */
1763
+ explore() {
1764
+ this._throwIfDisposed();
1765
+ if (this._explorePromise !== void 0) return this._explorePromise;
1766
+ const promise = this._explore();
1767
+ this._explorePromise = promise;
1768
+ const clear = () => {
1769
+ if (this._explorePromise === promise) this._explorePromise = void 0;
1770
+ };
1771
+ promise.then(clear, clear);
1772
+ return promise;
1580
1773
  }
1581
- return true;
1582
- }
1583
- function _hasPatternShape(value) {
1584
- if (!_isRecord(value)) return false;
1585
- return "exact" in value && typeof value.exact === "string" || "prefix" in value && typeof value.prefix === "string";
1586
- }
1587
- function _hasParamMatcherShape(value) {
1588
- if (!_isRecord(value)) return false;
1589
- if ("exact" in value) return true;
1590
- if ("enum" in value) return Array.isArray(value.enum);
1591
- if ("prefix" in value) return typeof value.prefix === "string";
1592
- if ("subsetOf" in value) return Array.isArray(value.subsetOf) && value.subsetOf.every((item) => typeof item === "string");
1593
- return value.any === true;
1594
- }
1595
- function _isRecord(value) {
1596
- return value !== null && typeof value === "object" && !Array.isArray(value);
1597
- }
1598
- /**
1599
- * Addressing match for a single permission: do its
1600
- * `serviceId`/`interfaceId`/`interfaceHash`/`members` patterns admit
1601
- * `target`?
1602
- */
1603
- function permissionMatchesTarget(target, perm) {
1604
- return _targetMismatchReason(target, perm) === void 0;
1605
- }
1606
- var TargetMismatchReason = class {
1607
- mismatch;
1608
- constructor(mismatch) {
1609
- this.mismatch = mismatch;
1774
+ async _explore() {
1775
+ if (this._initialized || this._nodes.get(ROOT_NODE_KEY)?.state !== "unexplored") this._resetForExplore();
1776
+ const root = this._nodes.get(ROOT_NODE_KEY);
1777
+ await this._loadNode(root);
1778
+ if (this._disposed) return {
1779
+ listings: [],
1780
+ inaccessible: []
1781
+ };
1782
+ await this._reconcileScopesFromRoot();
1783
+ await this._pruneUnreachable();
1784
+ this._initialized = true;
1785
+ if (this._watching) await this._synchronizeWatches();
1786
+ this._settle();
1787
+ return this.result;
1610
1788
  }
1611
- toString() {
1612
- const mismatch = this.mismatch;
1613
- switch (mismatch.kind) {
1614
- case "serviceId":
1615
- case "interfaceId": return `${mismatch.kind} ${JSON.stringify(mismatch.actual)} does not match ${_formatPattern(mismatch.expected)}`;
1616
- case "interfaceHash": return `interfaceHash ${mismatch.actual === void 0 ? "<none>" : JSON.stringify(mismatch.actual)} does not match required ${JSON.stringify(mismatch.expected)}`;
1617
- case "invalidMembers": return "permission target has invalid members";
1618
- case "member": {
1619
- const allowed = mismatch.expected.map(_formatPattern).join(", ") || "<none>";
1620
- return `member ${JSON.stringify(mismatch.actual)} does not match allowed patterns [${allowed}]`;
1789
+ /** Current deduplicated graph snapshot. */
1790
+ get result() {
1791
+ const best = /* @__PURE__ */ new Map();
1792
+ for (const node of this._nodes.values()) {
1793
+ if (!node.loaded) continue;
1794
+ for (const item of node.listings) {
1795
+ const effective = mergePrincipalSets(item.rootPrincipalSets, node.inherited);
1796
+ const tagged = {
1797
+ ...item,
1798
+ ...effective.length > 0 ? { rootPrincipalSets: effective } : {},
1799
+ discoveredFrom: node.target ?? ""
1800
+ };
1801
+ const key = listingKey(item);
1802
+ const existing = best.get(key);
1803
+ if (!existing || _isAuthoritative(tagged, existing)) best.set(key, tagged);
1621
1804
  }
1622
1805
  }
1623
- }
1624
- };
1625
- function _targetMismatchReason(target, perm) {
1626
- const t = perm.target;
1627
- if (!_patternMatches(target.serviceId, t.serviceId, "/")) return new TargetMismatchReason({
1628
- kind: "serviceId",
1629
- actual: target.serviceId,
1630
- expected: t.serviceId
1631
- });
1632
- if (!_patternMatches(target.interfaceId, t.interfaceId, ".")) return new TargetMismatchReason({
1633
- kind: "interfaceId",
1634
- actual: target.interfaceId,
1635
- expected: t.interfaceId
1636
- });
1637
- if (t.interfaceHash !== void 0 && target.interfaceHash !== t.interfaceHash) return new TargetMismatchReason({
1638
- kind: "interfaceHash",
1639
- actual: target.interfaceHash,
1640
- expected: t.interfaceHash
1641
- });
1642
- if (!Array.isArray(t.members)) return new TargetMismatchReason({ kind: "invalidMembers" });
1643
- if (!t.members.some((m) => _patternMatches(target.member, m, ""))) return new TargetMismatchReason({
1644
- kind: "member",
1645
- actual: target.member,
1646
- expected: t.members
1647
- });
1648
- }
1649
- function _patternMatches(value, pat, delim) {
1650
- if ("exact" in pat) return value === pat.exact;
1651
- const p = pat.prefix;
1652
- if (p === "") return true;
1653
- if (value === p) return true;
1654
- if (delim === "") return value.startsWith(p);
1655
- return value.startsWith(p + delim);
1656
- }
1657
- function _formatPattern(pattern) {
1658
- return "exact" in pattern ? `exact ${JSON.stringify(pattern.exact)}` : `prefix ${JSON.stringify(pattern.prefix)}`;
1659
- }
1660
- /**
1661
- * Strict allowlist match for {@link Permission.params}. The set of
1662
- * top-level keys in the call MUST equal the declared set; values must
1663
- * satisfy their matchers. Nested objects/arrays are compared as opaque
1664
- * JSON values via canonical-JSON equality. Pure (no crypto).
1665
- */
1666
- function matchParams(declared, actualParams) {
1667
- const declaredKeys = Object.keys(declared).sort();
1668
- const actualObj = actualParams !== null && typeof actualParams === "object" && !Array.isArray(actualParams) ? actualParams : void 0;
1669
- const actualKeys = actualObj ? Object.keys(actualObj).sort() : [];
1670
- if (declaredKeys.length !== actualKeys.length || declaredKeys.some((k, i) => k !== actualKeys[i])) return {
1671
- ok: false,
1672
- reason: `param keys mismatch (declared=${declaredKeys.join(",")}, actual=${actualKeys.join(",")})`
1673
- };
1674
- for (const k of declaredKeys) if (!_matchParamValue(declared[k], actualObj[k])) return {
1675
- ok: false,
1676
- reason: `param "${k}" does not match`
1677
- };
1678
- return { ok: true };
1679
- }
1680
- function _matchParamValue(matcher, value) {
1681
- if ("any" in matcher) return true;
1682
- if ("prefix" in matcher) return typeof value === "string" && value.startsWith(matcher.prefix);
1683
- if ("subsetOf" in matcher) return Array.isArray(value) && value.every((v) => typeof v === "string" && matcher.subsetOf.includes(v));
1684
- if ("exact" in matcher) return jcsCanonicalize(matcher.exact) === jcsCanonicalize(value);
1685
- if ("enum" in matcher) {
1686
- const v = jcsCanonicalize(value);
1687
- return matcher.enum.some((opt) => jcsCanonicalize(opt) === v);
1688
- }
1689
- return false;
1690
- }
1691
- /**
1692
- * Does `permission` admit `call` for the given `ability`? Checks, in
1693
- * order: the ability flag (`canInvoke`/`canDelegate`, both default
1694
- * `false`), the target address, the optional `params` allowlist, and the
1695
- * optional `callBind` hash-binding. Pure.
1696
- */
1697
- function permissionPermits(call, permission, ability) {
1698
- if (!(ability === "invoke" ? permission.canInvoke === true : permission.canDelegate === true)) return {
1699
- ok: false,
1700
- reason: `permission does not grant ${ability}`
1701
- };
1702
- const targetMismatch = _targetMismatchReason(call.target, permission);
1703
- if (targetMismatch !== void 0) return {
1704
- ok: false,
1705
- reason: `permission does not address this call: ${targetMismatch.toString()}`
1706
- };
1707
- if (permission.params !== void 0) {
1708
- const r = matchParams(permission.params, call.params);
1709
- if (!r.ok) return {
1710
- ok: false,
1711
- reason: `params: ${r.reason}`
1806
+ return {
1807
+ listings: [...best.values()].filter((item) => this._options.interfaceId === void 0 || item.interfaceId === this._options.interfaceId).filter((item) => this._options.interfaceIdPrefix === void 0 || item.interfaceId.startsWith(this._options.interfaceIdPrefix)).filter((item) => this._options.serviceId === void 0 || item.serviceId === this._options.serviceId),
1808
+ inaccessible: [...this._nodes.values()].filter((node) => node.key !== ROOT_NODE_KEY && node.inaccessibleReason !== void 0).map((node) => ({
1809
+ serviceId: node.target,
1810
+ reason: node.inaccessibleReason
1811
+ }))
1712
1812
  };
1713
1813
  }
1714
- if (permission.callBind !== void 0) {
1715
- if (permission.callBind.alg !== "sha256") return {
1716
- ok: false,
1717
- reason: `callBind: unsupported hash alg (${permission.callBind.alg})`
1718
- };
1719
- if (call.callHash !== permission.callBind.payloadHash) return {
1720
- ok: false,
1721
- reason: "callBind: payload hash mismatch"
1814
+ /** Current complete-or-progressive directory graph report. */
1815
+ get graphSnapshot() {
1816
+ const root = this._nodes.get(ROOT_NODE_KEY) ?? this._createRootNode();
1817
+ const directories = [...this._nodes.values()].filter((node) => node.key !== ROOT_NODE_KEY).sort((a, b) => (a.target ?? "").localeCompare(b.target ?? "")).map((node) => this._nodeReport(node));
1818
+ return {
1819
+ revision: this._revision,
1820
+ complete: this._complete,
1821
+ root: this._nodeReport(root),
1822
+ directories,
1823
+ result: this.result
1722
1824
  };
1723
1825
  }
1724
- return { ok: true };
1725
- }
1726
- /**
1727
- * Does `cap` admit `call` for the given `ability`? True iff **any** of its
1728
- * permissions does (the union over permissions). This is one *link's*
1729
- * judgment; chain intersection is enforced by {@link verifyChain}.
1730
- */
1731
- function capabilityPermits(call, cap, ability) {
1732
- let lastReason = `no permission grants ${ability} for this call`;
1733
- for (const permission of cap.permissions) {
1734
- const r = permissionPermits(call, permission, ability);
1735
- if (r.ok) return { ok: true };
1736
- lastReason = r.reason;
1737
- }
1738
- return {
1739
- ok: false,
1740
- reason: lastReason
1741
- };
1742
- }
1743
- //#endregion
1744
- //#region src/protocol/methodName.ts
1745
- /**
1746
- * Parse a JSON-RPC method string. Returns `undefined` if any segment is
1747
- * empty or the segment count is out of range.
1748
- */
1749
- function parseMethodName(method) {
1750
- const parts = method.split("::");
1751
- if (parts.length === 3 && parts[0].length === 0 && parts[1].length > 0 && parts[2].length > 0) return {
1752
- kind: "interface",
1753
- interfaceId: parts[1],
1754
- member: parts[2]
1755
- };
1756
- if (parts.some((p) => p.length === 0)) return void 0;
1757
- if (parts.length === 1) return {
1758
- kind: "bare",
1759
- member: parts[0]
1760
- };
1761
- if (parts.length === 2) return {
1762
- kind: "interface",
1763
- interfaceId: parts[0],
1764
- member: parts[1]
1765
- };
1766
- if (parts.length === 3) return {
1767
- kind: "full",
1768
- serviceId: parts[0],
1769
- interfaceId: parts[1],
1770
- member: parts[2]
1771
- };
1772
- }
1773
- /**
1774
- * Convert a parsed method string to a {@link CallTarget} for capability
1775
- * matching. Interface-form calls are addressed to the root service —
1776
- * represented as an empty `serviceId`. Throws on `bare` form (no
1777
- * interface context) and on malformed input.
1778
- */
1779
- function methodNameToTarget(method) {
1780
- const p = parseMethodName(method);
1781
- if (!p) throw new Error(`linkrpc: malformed method: ${method}`);
1782
- if (p.kind === "bare") throw new Error(`linkrpc: bare method has no interface context: ${method}`);
1783
- if (p.kind === "interface") return {
1784
- serviceId: "",
1785
- interfaceId: p.interfaceId,
1786
- member: p.member
1787
- };
1788
- return {
1789
- serviceId: p.serviceId,
1790
- interfaceId: p.interfaceId,
1791
- member: p.member
1792
- };
1793
- }
1794
- //#endregion
1795
- //#region src/protocol/linkRpcEnvelope.ts
1796
- /**
1797
- * The linkrpc signing/cap system accepts only plain-object user params (or
1798
- * none). Reject arrays/primitives at the boundary so "strip `$hubrpc*`, the
1799
- * rest is the signed user params" stays unambiguous.
1800
- */
1801
- function requireObjectParams(userParams) {
1802
- if (userParams === void 0) return {};
1803
- if (userParams === null || typeof userParams !== "object" || Array.isArray(userParams)) throw new Error("linkrpc: user params must be a plain JSON object or undefined");
1804
- return userParams;
1805
- }
1806
- /**
1807
- * Return the application-authored portion of a LinkRPC params value.
1808
- *
1809
- * Routing and gate layers must continue forwarding the original wire params so
1810
- * every chained hub can independently verify the envelope. Terminal dispatch
1811
- * may use this derived value for application-schema validation without mutating
1812
- * or replacing the wire message.
1813
- */
1814
- function stripLinkRpcWireMeta(wireParams) {
1815
- if (wireParams === void 0) return void 0;
1816
- if (wireParams === null || typeof wireParams !== "object" || Array.isArray(wireParams)) return wireParams;
1817
- const object = wireParams;
1818
- const reserved = [
1819
- HUBRPC_META_KEY,
1820
- HUBRPC_SIGNATURE_KEY,
1821
- HUBRPC_UNSIGNED_KEY
1822
- ];
1823
- if (!reserved.some((key) => key in object)) return object;
1824
- const userParams = {};
1825
- for (const key of Object.keys(object)) {
1826
- if (reserved.includes(key)) continue;
1827
- userParams[key] = object[key];
1828
- }
1829
- return userParams;
1830
- }
1831
- //#endregion
1832
- //#region src/transport/messageTransport.ts
1833
- /**
1834
- * Observe every message crossing a transport without changing its buffering or
1835
- * lifecycle behavior.
1836
- */
1837
- function traceMessageTransport(transport, trace) {
1838
- return {
1839
- send(message) {
1840
- trace("send", message);
1841
- return transport.send(message);
1842
- },
1843
- setListener(listener) {
1844
- transport.setListener(listener === void 0 ? void 0 : (message) => {
1845
- trace("receive", message);
1846
- listener(message);
1847
- });
1848
- },
1849
- dispose() {
1850
- transport.dispose();
1851
- }
1852
- };
1853
- }
1854
- /**
1855
- * Pipe two transports together: every message one receives is forwarded to
1856
- * the other's `send`. Returns a disposable that detaches both listeners.
1857
- *
1858
- * Neither transport is disposed — the caller owns their lifecycle. This is
1859
- * the building block for relays (e.g. bridging a `WindowMessageTransport`
1860
- * for an iframe to a multiplexer channel).
1861
- */
1862
- function connectTransports(a, b) {
1863
- a.setListener((m) => void b.send(m));
1864
- b.setListener((m) => void a.send(m));
1865
- return { dispose() {
1866
- a.setListener(void 0);
1867
- b.setListener(void 0);
1868
- } };
1869
- }
1870
- /**
1871
- * Two in-memory transports wired back-to-back. Useful for tests and for
1872
- * same-process bridges.
1873
- *
1874
- * Generic in the per-direction payload type so asymmetric pairs (e.g. one
1875
- * side sends plain `JsonRpcMessage`, the other side sends
1876
- * `JsonRpcMessage & { context: Participant }`) are expressible at the type
1877
- * level. At runtime both halves just hand objects through by reference.
1878
- */
1879
- var TransportPair = class {
1880
- a;
1881
- b;
1882
- constructor() {
1883
- const aT = new _InMemoryTransport();
1884
- const bT = new _InMemoryTransport();
1885
- aT._peer = bT;
1886
- bT._peer = aT;
1887
- this.a = aT;
1888
- this.b = bT;
1889
- }
1890
- };
1891
- var _InMemoryTransport = class {
1892
- _peer;
1893
- _listener;
1894
- _buffer = [];
1895
- _closed = false;
1896
- send(message) {
1897
- if (this._closed || this._peer._closed) return;
1898
- this._peer._deliver(message);
1899
- }
1900
- setListener(listener) {
1901
- this._listener = listener;
1902
- if (listener) while (this._buffer.length > 0 && this._listener) {
1903
- const m = this._buffer.shift();
1904
- this._listener(m);
1905
- }
1906
- }
1907
- dispose() {
1908
- this._closed = true;
1909
- }
1910
- _deliver(m) {
1911
- if (this._listener) this._listener(m);
1912
- else this._buffer.push(m);
1913
- }
1914
- };
1915
- //#endregion
1916
- //#region src/transport/multiplexedTransport.ts
1917
- var MultiplexedTransport = class MultiplexedTransport {
1918
- static create(base, channels) {
1919
- return new MultiplexedTransport(base, channels);
1920
- }
1921
- /** Logical transports keyed by the friendly channel names. */
1922
- transports;
1923
- _base;
1924
- _byId = /* @__PURE__ */ new Map();
1925
- _usedIds = /* @__PURE__ */ new Set();
1926
- _disposed = false;
1927
- constructor(base, channels) {
1928
- this._base = base;
1929
- const transports = {};
1930
- for (const name of Object.keys(channels)) {
1931
- const id = channels[name];
1932
- transports[name] = this._createChannel(id);
1933
- }
1934
- this.transports = transports;
1935
- base.setListener((data) => {
1936
- (data == null ? void 0 : this._byId.get(data.ch))?._deliver(data.m);
1937
- });
1938
- }
1939
- /**
1940
- * Add a logical channel after the multiplexer has started.
1941
- *
1942
- * Channel ids are permanently retired when disposed. This prevents a late
1943
- * envelope for an old iframe from being delivered to a replacement iframe.
1944
- */
1945
- addChannel(id) {
1946
- if (this._disposed) throw new Error("Cannot add a channel to a disposed multiplexed transport.");
1947
- return this._createChannel(id);
1948
- }
1949
- dispose() {
1950
- if (this._disposed) return;
1951
- this._disposed = true;
1952
- this._base.setListener(void 0);
1953
- for (const t of [...this._byId.values()]) t.dispose();
1954
- this._byId.clear();
1955
- }
1956
- _createChannel(id) {
1957
- if (id.length === 0) throw new Error("Multiplexed transport channel ids must not be empty.");
1958
- if (this._usedIds.has(id)) throw new Error(`Multiplexed transport channel id "${id}" has already been used.`);
1959
- this._usedIds.add(id);
1960
- const t = new _ChannelTransport(this._base, id, () => {
1961
- if (this._byId.get(id) === t) this._byId.delete(id);
1962
- });
1963
- this._byId.set(id, t);
1964
- return t;
1965
- }
1966
- };
1967
- var _ChannelTransport = class {
1968
- _base;
1969
- _ch;
1970
- _onDispose;
1971
- _listener;
1972
- _buffer = [];
1973
- _closed = false;
1974
- constructor(_base, _ch, _onDispose) {
1975
- this._base = _base;
1976
- this._ch = _ch;
1977
- this._onDispose = _onDispose;
1978
- }
1979
- send(message) {
1980
- if (this._closed) return;
1981
- this._base.send({
1982
- $mux: "v1",
1983
- ch: this._ch,
1984
- m: message
1985
- });
1986
- }
1987
- setListener(listener) {
1988
- this._listener = listener;
1989
- if (listener) while (this._buffer.length > 0 && this._listener) listener(this._buffer.shift());
1990
- }
1991
- dispose() {
1992
- if (this._closed) return;
1993
- this._closed = true;
1994
- this._listener = void 0;
1995
- this._buffer.length = 0;
1996
- this._onDispose();
1997
- }
1998
- _deliver(m) {
1999
- if (this._closed) return;
2000
- if (this._listener) this._listener(m);
2001
- else this._buffer.push(m);
2002
- }
2003
- };
2004
- //#endregion
2005
- //#region src/connection/channel.ts
2006
- var Channel = class Channel {
2007
- sender;
2008
- _setHandler;
2009
- _setWireObserver;
2010
- constructor(sender, _setHandler, _setWireObserver) {
2011
- this.sender = sender;
2012
- this._setHandler = _setHandler;
2013
- this._setWireObserver = _setWireObserver;
2014
- }
2015
- /** Bind the inbound request/notification handler. May be called before or after construction of {@link LinkRpcConnection}. */
2016
- setRequestHandler(handler) {
2017
- this._setHandler(handler);
2018
- }
2019
- /**
2020
- * Install the endpoint inspection observer at the JSON-RPC wire boundary.
2021
- * Intended for {@link LinkRpcConnection}; ordinary consumers should use the
2022
- * public `hubrpc.traffic` service instead.
2023
- */
2024
- setWireMessageObserver(observer) {
2025
- this._setWireObserver?.(observer);
2026
- }
2027
- /**
2028
- * Compose this channel with a sender-side decorator (typically the
2029
- * signing layer). The decorator wraps {@link sender} only; the
2030
- * {@link setRequestHandler} binding is shared with the original channel
2031
- * so the receive side is bound exactly once regardless of decoration depth.
2032
- */
2033
- withSender(decorate) {
2034
- return new Channel(decorate(this.sender), this._setHandler, this._setWireObserver);
2035
- }
2036
- };
2037
- var RpcError = class extends Error {
2038
- code;
2039
- origin;
2040
- data;
2041
- hasData;
2042
- constructor(message, code, data, origin = "local", hasData = data !== void 0) {
2043
- super(message);
2044
- this.code = code;
2045
- this.origin = origin;
2046
- this.name = "RpcError";
2047
- this.hasData = hasData;
2048
- if (hasData) this.data = data;
2049
- }
2050
- };
2051
- //#endregion
2052
- //#region src/connection/requestTimeout.ts
2053
- const DEFAULT_RPC_TIMEOUT_MS = 5e3;
2054
- async function withRpcTimeout(request, target, timeoutMs = DEFAULT_RPC_TIMEOUT_MS) {
2055
- if (timeoutMs <= 0) return request;
2056
- let timer;
2057
- const timeout = new Promise((_resolve, reject) => {
2058
- timer = setTimeout(() => {
2059
- const reason = `${target} timed out after ${timeoutMs}ms`;
2060
- try {
2061
- Promise.resolve(request.cancel(reason)).catch(() => {});
2062
- } finally {
2063
- try {
2064
- request.dispose?.(reason);
2065
- } catch {}
2066
- }
2067
- reject(new Error(reason));
2068
- }, timeoutMs);
2069
- });
2070
- try {
2071
- return await Promise.race([request, timeout]);
2072
- } finally {
2073
- if (timer !== void 0) clearTimeout(timer);
2074
- }
2075
- }
2076
- //#endregion
2077
- //#region src/hub/common/directoryWalk.ts
2078
- async function fetchDirectory(channel, opts = {}) {
2079
- const method = opts.target ? `${opts.target}::hubrpc.directory::list` : "hubrpc.directory::list";
2080
- const all = [];
2081
- let cursor;
2082
- while (true) {
2083
- const params = {};
2084
- if (opts.interfaceId !== void 0) params.interfaceId = opts.interfaceId;
2085
- if (opts.interfaceIdPrefix !== void 0) params.interfaceIdPrefix = opts.interfaceIdPrefix;
2086
- if (opts.serviceId !== void 0) params.serviceId = opts.serviceId;
2087
- if (opts.serviceIdScopes !== void 0) params.serviceIdScopes = [...opts.serviceIdScopes];
2088
- if (opts.limit !== void 0) params.limit = opts.limit;
2089
- if (opts.timeoutMs !== void 0) params.timeoutMs = opts.timeoutMs;
2090
- if (cursor !== void 0) params.cursor = cursor;
2091
- const call = channel.sendRequestWithStream(method, params, { interfaceHash: directoryInterface.schemaHash });
2092
- const target = opts.target ?? "<root>";
2093
- const page = await withRpcTimeout(Object.assign(call.result, {
2094
- cancel: (reason) => call.cancel(reason),
2095
- dispose: (reason) => call.dispose?.(reason)
2096
- }), `hub directory service '${target}'`, opts.timeoutMs ?? 5e3);
2097
- for (const it of page.items) all.push({
2098
- serviceId: it.serviceId,
2099
- interfaceId: it.interfaceId,
2100
- hash: it.interfaceHash,
2101
- ...it.path !== void 0 ? { path: it.path } : {},
2102
- ...it.serviceDescription !== void 0 ? { serviceDescription: it.serviceDescription } : {},
2103
- ...Array.isArray(it.rootPrincipalSets) ? { rootPrincipalSets: it.rootPrincipalSets } : {},
2104
- ...Array.isArray(it.reachableServiceIds) ? { reachableServiceIds: it.reachableServiceIds } : {}
2105
- });
2106
- if (!page.nextCursor) break;
2107
- cursor = page.nextCursor;
2108
- }
2109
- return all;
2110
- }
2111
- async function fetchSchema(channel, interfaceId, hash, target) {
2112
- const method = target ? `${target}::hubrpc.schemas::get` : "hubrpc.schemas::get";
2113
- const params = { interfaceId };
2114
- if (hash !== void 0) params.hash = hash;
2115
- return (await channel.sendRequest(method, params)).schema;
2116
- }
2117
- function findMethodInSchema(schema, methodName) {
2118
- return schema.methods[methodName];
2119
- }
2120
- /** Default recursion depth when walking the bus. */
2121
- const DEFAULT_WALK_DEPTH = 5;
2122
- const DIRECTORY_INTERFACE_ID = "hubrpc.directory";
2123
- const ROOT_NODE_KEY = "\0root";
2124
- /** Segment-aware service-id pattern matching (`foo` includes `foo/bar`, not `foobar`). */
2125
- function serviceIdMatchesPattern(serviceId, pattern) {
2126
- if ("exact" in pattern) return serviceId === pattern.exact;
2127
- return pattern.prefix === "" || serviceId === pattern.prefix || serviceId.startsWith(`${pattern.prefix}/`);
2128
- }
2129
- /** Whether a service id is included by a union of patterns. `undefined` means unfiltered. */
2130
- function serviceIdMatchesScopes(serviceId, scopes) {
2131
- return scopes === void 0 || scopes.some((pattern) => serviceIdMatchesPattern(serviceId, pattern));
2132
- }
2133
- /** Canonicalize a pattern union by removing duplicates and contained patterns. */
2134
- function normalizeServiceIdScopes(scopes) {
2135
- const unique = /* @__PURE__ */ new Map();
2136
- for (const pattern of scopes) unique.set(patternKey(pattern), pattern);
2137
- const patterns = [...unique.values()];
2138
- return patterns.filter((candidate, index) => !patterns.some((other, otherIndex) => index !== otherIndex && patternContains(other, candidate))).sort((a, b) => patternKey(a).localeCompare(patternKey(b)));
2139
- }
2140
- /** Pairwise intersection of two pattern unions. */
2141
- function intersectServiceIdScopes(left, right) {
2142
- const intersections = [];
2143
- for (const a of left) for (const b of right) {
2144
- const intersection = intersectPattern(a, b);
2145
- if (intersection !== void 0) intersections.push(intersection);
2146
- }
2147
- return normalizeServiceIdScopes(intersections);
2148
- }
2149
- function intersectPattern(left, right) {
2150
- if ("exact" in left) {
2151
- if ("exact" in right) return left.exact === right.exact ? left : void 0;
2152
- return serviceIdMatchesPattern(left.exact, right) ? left : void 0;
2153
- }
2154
- if ("exact" in right) return serviceIdMatchesPattern(right.exact, left) ? right : void 0;
2155
- if (serviceIdMatchesPattern(left.prefix, right)) return left;
2156
- if (serviceIdMatchesPattern(right.prefix, left)) return right;
2157
- }
2158
- function patternContains(container, candidate) {
2159
- if ("exact" in container) return "exact" in candidate && container.exact === candidate.exact;
2160
- if ("exact" in candidate) return serviceIdMatchesPattern(candidate.exact, container);
2161
- return serviceIdMatchesPattern(candidate.prefix, container);
2162
- }
2163
- function patternKey(pattern) {
2164
- return "exact" in pattern ? `e:${pattern.exact}` : `p:${pattern.prefix}`;
2165
- }
2166
- function scopesKey(scopes) {
2167
- return normalizeServiceIdScopes(scopes).map(patternKey).join("\0");
2168
- }
2169
- async function walkHubDetailed(channel, opts = {}) {
2170
- return new HubDirectoryExplorer(channel, opts).explore();
2171
- }
2172
- /**
2173
- * Backward-compatible wrapper over {@link walkHubDetailed} that returns only the
2174
- * reachable listings, dropping the inaccessible-directory report.
2175
- */
2176
- async function walkHub(channel, opts = {}) {
2177
- return (await walkHubDetailed(channel, opts)).listings;
2178
- }
2179
- /**
2180
- * Reusable directory graph explorer and watcher.
2181
- *
2182
- * The class keeps one native snapshot per directory target. A watch tick
2183
- * re-lists only its source target; outgoing referral changes then propagate
2184
- * through affected descendants. Unchanged siblings retain both their snapshot
2185
- * and watch. Multi-parent targets are queried with the normalized union of all
2186
- * incoming path scopes.
2187
- */
2188
- var HubDirectoryExplorer = class {
2189
- _channel;
2190
- _options;
2191
- _maxDepth;
2192
- _initialScopes;
2193
- _nodes = /* @__PURE__ */ new Map();
2194
- _unlockAttempted = /* @__PURE__ */ new Set();
2195
- _listeners = /* @__PURE__ */ new Set();
2196
- _graphListeners = /* @__PURE__ */ new Set();
2197
- _pendingTicks = /* @__PURE__ */ new Set();
2198
- _watchRetryTimers = /* @__PURE__ */ new Map();
2199
- _watchRetryDelays = /* @__PURE__ */ new Map();
2200
- _initialized = false;
2201
- _disposed = false;
2202
- _watching = false;
2203
- _complete = false;
2204
- _revision = 0;
2205
- _work = Promise.resolve();
2206
- _explorePromise;
2207
- constructor(_channel, _options = {}) {
2208
- this._channel = _channel;
2209
- this._options = _options;
2210
- this._maxDepth = _options.maxDepth ?? 5;
2211
- this._initialScopes = normalizeServiceIdScopes(_options.serviceIdScopes ?? [{ prefix: "" }]);
2212
- this._nodes.set(ROOT_NODE_KEY, this._createRootNode());
2213
- }
2214
- /** Perform a fresh bounded exploration without starting watches. */
2215
- explore() {
2216
- this._throwIfDisposed();
2217
- if (this._explorePromise !== void 0) return this._explorePromise;
2218
- const promise = this._explore();
2219
- this._explorePromise = promise;
2220
- const clear = () => {
2221
- if (this._explorePromise === promise) this._explorePromise = void 0;
2222
- };
2223
- promise.then(clear, clear);
2224
- return promise;
2225
- }
2226
- async _explore() {
2227
- if (this._initialized || this._nodes.get(ROOT_NODE_KEY)?.state !== "unexplored") this._resetForExplore();
2228
- const root = this._nodes.get(ROOT_NODE_KEY);
2229
- await this._loadNode(root);
2230
- if (this._disposed) return {
2231
- listings: [],
2232
- inaccessible: []
2233
- };
2234
- await this._reconcileScopesFromRoot();
2235
- await this._pruneUnreachable();
2236
- this._initialized = true;
2237
- if (this._watching) await this._synchronizeWatches();
2238
- this._settle();
2239
- return this.result;
2240
- }
2241
- /** Current deduplicated graph snapshot. */
2242
- get result() {
2243
- const best = /* @__PURE__ */ new Map();
2244
- for (const node of this._nodes.values()) {
2245
- if (!node.loaded) continue;
2246
- for (const item of node.listings) {
2247
- const effective = mergePrincipalSets(item.rootPrincipalSets, node.inherited);
2248
- const tagged = {
2249
- ...item,
2250
- ...effective.length > 0 ? { rootPrincipalSets: effective } : {},
2251
- discoveredFrom: node.target ?? ""
2252
- };
2253
- const key = listingKey(item);
2254
- const existing = best.get(key);
2255
- if (!existing || _isAuthoritative(tagged, existing)) best.set(key, tagged);
2256
- }
2257
- }
2258
- return {
2259
- listings: [...best.values()].filter((item) => this._options.interfaceId === void 0 || item.interfaceId === this._options.interfaceId).filter((item) => this._options.interfaceIdPrefix === void 0 || item.interfaceId.startsWith(this._options.interfaceIdPrefix)).filter((item) => this._options.serviceId === void 0 || item.serviceId === this._options.serviceId),
2260
- inaccessible: [...this._nodes.values()].filter((node) => node.key !== ROOT_NODE_KEY && node.inaccessibleReason !== void 0).map((node) => ({
2261
- serviceId: node.target,
2262
- reason: node.inaccessibleReason
2263
- }))
2264
- };
2265
- }
2266
- /** Current complete-or-progressive directory graph report. */
2267
- get graphSnapshot() {
2268
- const root = this._nodes.get(ROOT_NODE_KEY) ?? this._createRootNode();
2269
- const directories = [...this._nodes.values()].filter((node) => node.key !== ROOT_NODE_KEY).sort((a, b) => (a.target ?? "").localeCompare(b.target ?? "")).map((node) => this._nodeReport(node));
2270
- return {
2271
- revision: this._revision,
2272
- complete: this._complete,
2273
- root: this._nodeReport(root),
2274
- directories,
2275
- result: this.result
2276
- };
2277
- }
2278
- /**
2279
- * Subscribe to progressive graph reports. The listener is registered before
2280
- * receiving a synchronous current snapshot, avoiding the subscribe/explore
2281
- * race for both idle and already-running explorers.
2282
- */
2283
- subscribe(listener) {
2284
- this._throwIfDisposed();
2285
- this._graphListeners.add(listener);
2286
- this._deliverGraphEvent(listener, {
2287
- type: "snapshot",
2288
- reason: "subscribed",
2289
- snapshot: this.graphSnapshot
2290
- });
2291
- let active = true;
2292
- return () => {
2293
- if (!active) return;
2294
- active = false;
2295
- this._graphListeners.delete(listener);
2296
- };
1826
+ /**
1827
+ * Subscribe to progressive graph reports. The listener is registered before
1828
+ * receiving a synchronous current snapshot, avoiding the subscribe/explore
1829
+ * race for both idle and already-running explorers.
1830
+ */
1831
+ subscribe(listener) {
1832
+ this._throwIfDisposed();
1833
+ this._graphListeners.add(listener);
1834
+ this._deliverGraphEvent(listener, {
1835
+ type: "snapshot",
1836
+ reason: "subscribed",
1837
+ snapshot: this.graphSnapshot
1838
+ });
1839
+ let active = true;
1840
+ return () => {
1841
+ if (!active) return;
1842
+ active = false;
1843
+ this._graphListeners.delete(listener);
1844
+ };
2297
1845
  }
2298
1846
  /**
2299
1847
  * Explore once, then watch every reachable directory. Resolves after the
@@ -2951,6 +2499,54 @@ function _isAuthoritative(candidate, current) {
2951
2499
  return candidateDirect && !currentDirect;
2952
2500
  }
2953
2501
  //#endregion
2502
+ //#region src/connection/channel.ts
2503
+ var Channel = class Channel {
2504
+ sender;
2505
+ _setHandler;
2506
+ _setWireObserver;
2507
+ constructor(sender, _setHandler, _setWireObserver) {
2508
+ this.sender = sender;
2509
+ this._setHandler = _setHandler;
2510
+ this._setWireObserver = _setWireObserver;
2511
+ }
2512
+ /** Bind the inbound request/notification handler. May be called before or after construction of {@link LinkRpcConnection}. */
2513
+ setRequestHandler(handler) {
2514
+ this._setHandler(handler);
2515
+ }
2516
+ /**
2517
+ * Install the endpoint inspection observer at the JSON-RPC wire boundary.
2518
+ * Intended for {@link LinkRpcConnection}; ordinary consumers should use the
2519
+ * public `hubrpc.traffic` service instead.
2520
+ */
2521
+ setWireMessageObserver(observer) {
2522
+ this._setWireObserver?.(observer);
2523
+ }
2524
+ /**
2525
+ * Compose this channel with a sender-side decorator (typically the
2526
+ * signing layer). The decorator wraps {@link sender} only; the
2527
+ * {@link setRequestHandler} binding is shared with the original channel
2528
+ * so the receive side is bound exactly once regardless of decoration depth.
2529
+ */
2530
+ withSender(decorate) {
2531
+ return new Channel(decorate(this.sender), this._setHandler, this._setWireObserver);
2532
+ }
2533
+ };
2534
+ var RpcError = class extends Error {
2535
+ code;
2536
+ origin;
2537
+ data;
2538
+ hasData;
2539
+ constructor(message, code, data, origin = "local", hasData = data !== void 0) {
2540
+ super(message);
2541
+ this.code = code;
2542
+ this.origin = origin;
2543
+ this.message = message;
2544
+ this.name = "RpcError";
2545
+ this.hasData = hasData;
2546
+ if (hasData) this.data = data;
2547
+ }
2548
+ };
2549
+ //#endregion
2954
2550
  //#region src/connection/jsonRpcChannel.ts
2955
2551
  /**
2956
2552
  * How often a streaming-enabled call emits a `toCallee` keepalive ping so a
@@ -3296,999 +2892,1327 @@ var JsonRpcChannel = class JsonRpcChannel {
3296
2892
  this._observeOutbound(response);
3297
2893
  await this._transport.send(response);
3298
2894
  } else {
3299
- const resp = {
3300
- jsonrpc: "2.0",
3301
- id: m.id,
3302
- error: result.error
3303
- };
3304
- this._observeOutbound(resp);
3305
- await this._transport.send(resp);
2895
+ const resp = {
2896
+ jsonrpc: "2.0",
2897
+ id: m.id,
2898
+ error: result.error
2899
+ };
2900
+ this._observeOutbound(resp);
2901
+ await this._transport.send(resp);
2902
+ }
2903
+ }
2904
+ _handleNotification(m, context) {
2905
+ if (!this._handler) return;
2906
+ try {
2907
+ this._handler.handleNotification({
2908
+ method: m.method,
2909
+ params: m.params,
2910
+ context,
2911
+ stream: _NOOP_STREAM,
2912
+ signal: _NEVER_ABORT
2913
+ });
2914
+ } catch {}
2915
+ }
2916
+ _observeInbound(message) {
2917
+ const observer = this._wireObserver;
2918
+ if (observer === void 0) return;
2919
+ observer("inbound", message);
2920
+ }
2921
+ _observeOutbound(message, opts) {
2922
+ const observer = this._wireObserver;
2923
+ if (observer === void 0) return;
2924
+ observer("outbound", message);
2925
+ }
2926
+ _observeSendFailure(requestId, error) {
2927
+ if (this._wireObserver === void 0) return;
2928
+ this._observeInbound({
2929
+ jsonrpc: "2.0",
2930
+ id: requestId,
2931
+ error: {
2932
+ code: ErrorCode.peerDisconnected,
2933
+ message: error instanceof Error ? error.message : String(error)
2934
+ }
2935
+ });
2936
+ }
2937
+ };
2938
+ /** Stub stream handle for notifications, which carry no request id. */
2939
+ const _NOOP_STREAM = {
2940
+ send: () => Promise.resolve(),
2941
+ onMessage: () => {},
2942
+ ping: () => Promise.resolve()
2943
+ };
2944
+ /** A signal that never aborts — handed to notification handlers. */
2945
+ const _NEVER_ABORT = new AbortController().signal;
2946
+ //#endregion
2947
+ //#region src/connection/interfaceTarget.ts
2948
+ function interfaceTarget(iface, options = {}) {
2949
+ const serviceId = options.serviceId ?? "";
2950
+ if (serviceId.includes("::") || !/^[\x20-\x7e]*$/.test(serviceId)) throw new Error("Service id must contain only printable ASCII and must not contain \"::\".");
2951
+ return Object.freeze({
2952
+ mode: "qualified",
2953
+ interface: iface,
2954
+ serviceId
2955
+ });
2956
+ }
2957
+ function defaultInterfaceTarget(iface) {
2958
+ return Object.freeze({
2959
+ mode: "default",
2960
+ interface: iface
2961
+ });
2962
+ }
2963
+ function isInterfaceTarget(value) {
2964
+ return typeof value === "object" && value !== null && "interface" in value && "mode" in value && (value.mode === "qualified" || value.mode === "default" || value.mode === "bare");
2965
+ }
2966
+ //#endregion
2967
+ //#region src/connection/linkRpcConnection.ts
2968
+ /**
2969
+ * High-level linkrpc connection. Layers method-name routing and zod-driven
2970
+ * validation on top of a plain JSON-RPC channel.
2971
+ *
2972
+ * Typed proxies built by this class merely forward each call's
2973
+ * `interfaceHash` hint to the channel so the signer stamps it into the
2974
+ * `$hubrpc` envelope.
2975
+ *
2976
+ * `TInCtx` is the per-call out-of-band context the connection's transport
2977
+ * carries. Default `undefined` covers cross-process and ordinary in-process
2978
+ * transports. The hub's self/overlay connections instantiate with a
2979
+ * concrete `TInCtx` (e.g. `Participant`) so handlers can see who originated
2980
+ * the call.
2981
+ *
2982
+ * `TOutCtx` is the per-call override / extension bag the outbound sender
2983
+ * understands (see {@link SendOpts.ctx}). It only ever flows into the
2984
+ * sender as input, so it is a contravariant type parameter. Default `any`
2985
+ * keeps the bare `LinkRpcConnection` a valid supertype for holders that do
2986
+ * not care about the outbound ctx; pass a concrete shape (e.g.
2987
+ * `SigningCallCtx`) to get precise {@link get} typing. The connection
2988
+ * itself is agnostic to its contents — it merely forwards the `ctx`
2989
+ * defaults supplied to {@link get}.
2990
+ */
2991
+ var LinkRpcConnection = class LinkRpcConnection {
2992
+ /**
2993
+ * Convenience: build a {@link JsonRpcChannel} `Channel` from the
2994
+ * given transport and wrap it in an `LinkRpcConnection`. Use this
2995
+ * when you have a transport at hand and don't need a decorator
2996
+ * stack (e.g. signing).
2997
+ */
2998
+ static fromTransport(transport, options = {}) {
2999
+ return new LinkRpcConnection(JsonRpcChannel.create(transport), options);
3000
+ }
3001
+ /** Underlying JSON-RPC sender — useful for callers that need raw access (e.g. to call hub-served methods that bypass the interface registry). */
3002
+ channel;
3003
+ /** key = `${serviceId ?? ""}::${interfaceId}` */
3004
+ _registry = /* @__PURE__ */ new Map();
3005
+ /** Descriptions for services that have been registered with a `serviceDescription`. */
3006
+ _serviceDescriptions = /* @__PURE__ */ new Map();
3007
+ /** Root-node-id requirement sets recorded per serviceId. */
3008
+ _serviceRootPrincipalSets = /* @__PURE__ */ new Map();
3009
+ _directoryWatchers = /* @__PURE__ */ new Set();
3010
+ _directoryListeners = /* @__PURE__ */ new Set();
3011
+ _inspection;
3012
+ _inspectionHost;
3013
+ _serviceInspectionRegistrations = /* @__PURE__ */ new Map();
3014
+ _wireObservers = /* @__PURE__ */ new Set();
3015
+ _closeListeners = /* @__PURE__ */ new Set();
3016
+ _closed = false;
3017
+ _wireChannel;
3018
+ _validateOutboundParams;
3019
+ /** Bare-method bindings, keyed by their exact wire prefix. */
3020
+ _bareBindings = /* @__PURE__ */ new Map();
3021
+ _generateTopologyId;
3022
+ /**
3023
+ * Construct from a {@link Channel} (binds the inbound handler and uses
3024
+ * `channel.sender` for outbound calls) or from a bare
3025
+ * {@link IRequestSender} (send-only — no inbound handler is registered,
3026
+ * useful for bootstrap flows like `createManagedIdentity`).
3027
+ */
3028
+ constructor(channel, options = {}) {
3029
+ this._validateOutboundParams = options.validateOutboundParams !== false;
3030
+ this._generateTopologyId = options.generateTopologyId ?? createInspectionId;
3031
+ if ("sender" in channel) {
3032
+ this._wireChannel = channel;
3033
+ this.channel = channel.sender;
3034
+ channel.setRequestHandler({
3035
+ handleRequest: (c) => this._handleRequest(c),
3036
+ handleNotification: (c) => this._handleNotification(c)
3037
+ });
3038
+ } else {
3039
+ this._wireChannel = void 0;
3040
+ this.channel = channel;
3306
3041
  }
3307
3042
  }
3308
- _handleNotification(m, context) {
3309
- if (!this._handler) return;
3310
- try {
3311
- this._handler.handleNotification({
3312
- method: m.method,
3313
- params: m.params,
3314
- context,
3315
- stream: _NOOP_STREAM,
3316
- signal: _NEVER_ABORT
3317
- });
3318
- } catch {}
3043
+ get(ifaceOrTarget, opts) {
3044
+ return this._getClient(ifaceOrTarget, opts, false);
3319
3045
  }
3320
- _observeInbound(message) {
3321
- const observer = this._wireObserver;
3322
- if (observer === void 0) return;
3323
- observer("inbound", message);
3046
+ _getClient(ifaceOrTarget, opts, allFailuresAsValues) {
3047
+ if (isInterfaceTarget(ifaceOrTarget)) {
3048
+ if (opts !== void 0) throw new Error(`${allFailuresAsValues ? "getResultClient" : "get"}: ${ifaceOrTarget.mode === "bare" ? "bare " : ""}interface targets do not accept service or call options.`);
3049
+ if (ifaceOrTarget.mode === "bare") return this._buildBareClient(ifaceOrTarget.interface, ifaceOrTarget.prefix, allFailuresAsValues);
3050
+ return this._buildClient(ifaceOrTarget.interface, ifaceOrTarget.mode === "qualified" ? { serviceId: ifaceOrTarget.serviceId } : {}, void 0, ifaceOrTarget.mode === "default", allFailuresAsValues);
3051
+ }
3052
+ return this._buildClient(ifaceOrTarget, opts ?? {}, void 0, false, allFailuresAsValues);
3324
3053
  }
3325
- _observeOutbound(message, opts) {
3326
- const observer = this._wireObserver;
3327
- if (observer === void 0) return;
3328
- observer("outbound", message);
3054
+ /**
3055
+ * Get a typed client that emits foreign-protocol bare method names.
3056
+ * Unlike {@link get}, these calls carry no LinkRPC interface metadata.
3057
+ */
3058
+ getBare(iface, opts = {}) {
3059
+ return this._buildBareClient(iface, opts.prefix ?? "", false);
3329
3060
  }
3330
- _observeSendFailure(requestId, error) {
3331
- if (this._wireObserver === void 0) return;
3332
- this._observeInbound({
3333
- jsonrpc: "2.0",
3334
- id: requestId,
3335
- error: {
3336
- code: ErrorCode.peerDisconnected,
3337
- message: error instanceof Error ? error.message : String(error)
3061
+ _buildBareClient(iface, prefix, allFailuresAsValues) {
3062
+ validateBarePrefix(prefix);
3063
+ for (const [name, member] of Object.entries(iface.members)) if (member instanceof RequestType && (member.clientStreamSchema !== void 0 || member.serverStreamSchema !== void 0)) throw new Error(`getBare: streaming method "${name}" is not supported on foreign wires.`);
3064
+ return this._buildClient(iface, {}, prefix, false, allFailuresAsValues);
3065
+ }
3066
+ getResultClient(ifaceOrTarget, opts) {
3067
+ return this._getClient(ifaceOrTarget, opts, true);
3068
+ }
3069
+ /** Get a service-scoped handle; all interfaces obtained from it route via `serviceId` (form 3). */
3070
+ service(serviceId) {
3071
+ return new ServiceHandle(this, serviceId);
3072
+ }
3073
+ /** Register handlers, adding preset routing when passed a default or bare target. */
3074
+ register(ifaceOrTarget, handlers, opts = {}) {
3075
+ if (isInterfaceTarget(ifaceOrTarget)) {
3076
+ if (ifaceOrTarget.mode === "qualified") {
3077
+ if (opts.serviceId !== void 0 && opts.serviceId !== ifaceOrTarget.serviceId) throw new Error("register: serviceId conflicts with interface target.");
3078
+ return this._register(ifaceOrTarget.interface, handlers, {
3079
+ ...opts,
3080
+ serviceId: ifaceOrTarget.serviceId || void 0
3081
+ }, false);
3338
3082
  }
3083
+ const prefix = ifaceOrTarget.mode === "bare" ? ifaceOrTarget.prefix : "";
3084
+ validateBarePrefix(prefix);
3085
+ return this._register(ifaceOrTarget.interface, handlers, opts, false, prefix);
3086
+ }
3087
+ return this._register(ifaceOrTarget, handlers, opts, false);
3088
+ }
3089
+ /** Register inspection contracts without recursively enabling service inspection. */
3090
+ registerInspection(iface, handlers, opts = {}) {
3091
+ return this._register(iface, handlers, opts, true);
3092
+ }
3093
+ /** Observe this connection without replacing another inspection scope's observer. */
3094
+ observeWireMessages(observer) {
3095
+ if (this._closed) throw new Error("Connection is closed");
3096
+ if (this._wireChannel === void 0) throw new Error("Connection has no observable wire channel");
3097
+ this._wireObservers.add(observer);
3098
+ this._wireChannel.setWireMessageObserver((direction, message) => {
3099
+ for (const listener of [...this._wireObservers]) try {
3100
+ listener(direction, message);
3101
+ } catch {}
3339
3102
  });
3103
+ return { dispose: () => {
3104
+ this._wireObservers.delete(observer);
3105
+ if (this._wireObservers.size === 0) this._wireChannel?.setWireMessageObserver(void 0);
3106
+ } };
3340
3107
  }
3341
- };
3342
- /** Stub stream handle for notifications, which carry no request id. */
3343
- const _NOOP_STREAM = {
3344
- send: () => Promise.resolve(),
3345
- onMessage: () => {},
3346
- ping: () => Promise.resolve()
3347
- };
3348
- /** A signal that never aborts — handed to notification handlers. */
3349
- const _NEVER_ABORT = new AbortController().signal;
3350
- //#endregion
3351
- //#region src/inspection/endpointTrafficInspector.ts
3352
- const REQUEST_CORRELATION_RETENTION_MS = 18e5;
3353
- const MAX_REQUEST_CORRELATIONS = 4096;
3354
- var EndpointTrafficInspector = class {
3355
- _nodeId;
3356
- _portId;
3357
- _setActive;
3358
- _subscribers = /* @__PURE__ */ new Set();
3359
- _observers = /* @__PURE__ */ new Set();
3360
- _outboundRequests = /* @__PURE__ */ new Map();
3361
- _inboundRequests = /* @__PURE__ */ new Map();
3362
- _trafficWatches = new TrafficWatchFlowTracker();
3363
- observe = (direction, message) => {
3364
- this._onMessage(direction, message);
3365
- };
3366
- constructor(_nodeId, _portId, _setActive) {
3367
- this._nodeId = _nodeId;
3368
- this._portId = _portId;
3369
- this._setActive = _setActive;
3370
- }
3371
- start() {
3372
- this._setActive(true);
3373
- }
3374
- get observerCount() {
3375
- return this._subscribers.size + this._observers.size;
3376
- }
3377
- subscribe(options, send) {
3378
- if (options.trafficIgnoreKey !== void 0 && !this._trafficWatches.claim(options.trafficIgnoreKey)) throw new Error("Traffic watch request was not observed before subscription");
3379
- const subscriber = new BoundedTrafficSubscription(options, send, () => {
3380
- this._subscribers.delete(subscriber);
3381
- this._clearCorrelationsIfUnused();
3108
+ onDidClose(listener) {
3109
+ if (this._closed) throw new Error("Connection is closed");
3110
+ this._closeListeners.add(listener);
3111
+ return { dispose: () => {
3112
+ this._closeListeners.delete(listener);
3113
+ } };
3114
+ }
3115
+ _register(iface, handlers, opts, internalInspection, barePrefix) {
3116
+ const serviceId = opts.serviceId;
3117
+ const key = `${serviceId ?? ""}::${iface.info.id}`;
3118
+ if (this._registry.has(key)) throw new Error(`Interface "${iface.info.id}" already registered${serviceId ? ` under service "${serviceId}"` : ""}.`);
3119
+ if (barePrefix !== void 0 && this._bareBindings.has(barePrefix)) throw new Error(`Bare interface prefix "${barePrefix}" is already registered.`);
3120
+ let descriptionWasAdded = false;
3121
+ if (opts.serviceDescription !== void 0) {
3122
+ if (serviceId === void 0) throw new Error("register: `serviceDescription` requires `serviceId`.");
3123
+ const existing = this._serviceDescriptions.get(serviceId);
3124
+ if (existing !== void 0 && existing !== opts.serviceDescription) throw new Error(`register: conflicting descriptions for service "${serviceId}".`);
3125
+ descriptionWasAdded = existing === void 0;
3126
+ }
3127
+ let rootPrincipalSetsWereAdded = false;
3128
+ if (opts.rootPrincipalSets !== void 0) {
3129
+ if (serviceId === void 0) throw new Error("register: `rootPrincipalSets` requires `serviceId`.");
3130
+ const existing = this._serviceRootPrincipalSets.get(serviceId);
3131
+ if (existing !== void 0 && !rootPrincipalSetsEqual(existing, opts.rootPrincipalSets)) throw new Error(`register: conflicting rootPrincipalSets for service "${serviceId}".`);
3132
+ rootPrincipalSetsWereAdded = existing === void 0;
3133
+ }
3134
+ const entry = {
3135
+ iface,
3136
+ handlers,
3137
+ serviceId,
3138
+ internalInspection
3139
+ };
3140
+ if (descriptionWasAdded) this._serviceDescriptions.set(serviceId, opts.serviceDescription);
3141
+ if (rootPrincipalSetsWereAdded) this._serviceRootPrincipalSets.set(serviceId, opts.rootPrincipalSets);
3142
+ this._registry.set(key, entry);
3143
+ if (barePrefix !== void 0) this._bareBindings.set(barePrefix, {
3144
+ prefix: barePrefix,
3145
+ entry
3146
+ });
3147
+ this._notifyDirectoryWatchers(entry, descriptionWasAdded || rootPrincipalSetsWereAdded);
3148
+ if (!internalInspection && serviceId !== void 0 && this._inspection !== void 0 && !this._serviceInspectionRegistrations.has(serviceId)) try {
3149
+ this._installServiceInspection(serviceId);
3150
+ } catch (error) {
3151
+ this._registry.delete(key);
3152
+ if (barePrefix !== void 0) this._bareBindings.delete(barePrefix);
3153
+ if (descriptionWasAdded) this._serviceDescriptions.delete(serviceId);
3154
+ if (rootPrincipalSetsWereAdded) this._serviceRootPrincipalSets.delete(serviceId);
3155
+ this._notifyDirectoryWatchers(entry);
3156
+ throw error;
3157
+ }
3158
+ let disposed = false;
3159
+ return { dispose: () => {
3160
+ if (disposed) return;
3161
+ disposed = true;
3162
+ if (this._registry.get(key) !== entry) return;
3163
+ this._registry.delete(key);
3164
+ for (const [prefix, binding] of this._bareBindings) if (binding.entry === entry) this._bareBindings.delete(prefix);
3165
+ this._notifyDirectoryWatchers(entry);
3166
+ if (serviceId !== void 0 && !this._hasBusinessServiceRegistration(serviceId)) {
3167
+ this._removeServiceInspection(serviceId);
3168
+ this._serviceDescriptions.delete(serviceId);
3169
+ this._serviceRootPrincipalSets.delete(serviceId);
3170
+ }
3171
+ } };
3172
+ }
3173
+ /** Snapshot of every interface currently registered on this connection. */
3174
+ listRegisteredInterfaces() {
3175
+ return Array.from(this._registry.values()).map((r) => {
3176
+ const sid = r.serviceId ?? "";
3177
+ const item = {
3178
+ serviceId: sid,
3179
+ interfaceId: r.iface.info.id,
3180
+ interfaceHash: r.iface.schemaHash
3181
+ };
3182
+ const desc = sid === "" ? void 0 : this._serviceDescriptions.get(sid);
3183
+ if (desc !== void 0) item.serviceDescription = desc;
3184
+ const sets = sid === "" ? void 0 : this._serviceRootPrincipalSets.get(sid);
3185
+ if (sets !== void 0) item.rootPrincipalSets = sets;
3186
+ return item;
3382
3187
  });
3383
- this._subscribers.add(subscriber);
3384
- return subscriber;
3385
3188
  }
3386
- observeTransits(observer) {
3387
- this._observers.add(observer);
3189
+ /** Subscribe to coarse local directory changes; listeners must re-list. */
3190
+ onDidChangeDirectory(listener) {
3191
+ this._directoryListeners.add(listener);
3192
+ return () => this._directoryListeners.delete(listener);
3193
+ }
3194
+ /**
3195
+ * Look up a registered interface definition by id (and optional content
3196
+ * hash). Returns `undefined` if no registered interface matches.
3197
+ */
3198
+ findRegisteredInterface(interfaceId, hash) {
3199
+ for (const r of this._registry.values()) {
3200
+ if (r.iface.info.id !== interfaceId) continue;
3201
+ if (hash !== void 0 && r.iface.schemaHash !== hash) continue;
3202
+ return r.iface;
3203
+ }
3204
+ }
3205
+ /**
3206
+ * Register the three linkrpc reflection interfaces (`defaults`,
3207
+ * `directory`, `schemas`), backed by this connection's live registry.
3208
+ *
3209
+ * By default they live under the root service (form-2 reachable as
3210
+ * `hubrpc.directory::list`). Pass `serviceId` to additionally mount
3211
+ * them under a specific service — useful for participants that live
3212
+ * behind a hub, so callers can reach reflection via form-3
3213
+ * `<serviceId>::hubrpc.directory::list`.
3214
+ *
3215
+ * Idempotent: re-registering the same `(serviceId, interfaceId)` pair
3216
+ * is a no-op.
3217
+ */
3218
+ enableReflection(opts = {}) {
3219
+ const key = `${opts.serviceId ?? ""}::${defaultsInterface.info.id}`;
3220
+ if (this._registry.has(key)) return { dispose() {} };
3221
+ const prefix = `${opts.serviceId ?? ""}::`;
3222
+ for (const iface of [directoryInterface, schemasInterface]) if (this._registry.has(`${prefix}${iface.info.id}`)) throw new Error(`enableReflection: partial reflection registration for service "${opts.serviceId ?? ""}".`);
3223
+ const regOpts = opts.serviceId !== void 0 ? { serviceId: opts.serviceId } : {};
3224
+ const registrations = [];
3225
+ registrations.push(this.register(defaultsInterface, {
3226
+ get: () => {
3227
+ const binding = this._bareBindings.get("");
3228
+ if (!binding) return {};
3229
+ return {
3230
+ serviceId: binding.entry.serviceId,
3231
+ interfaceId: binding.entry.iface.info.id,
3232
+ interfaceHash: binding.entry.iface.schemaHash
3233
+ };
3234
+ },
3235
+ listBindings: () => ({ bindings: Array.from(this._bareBindings.values()).sort((a, b) => a.prefix < b.prefix ? -1 : a.prefix > b.prefix ? 1 : 0).map((binding) => ({
3236
+ prefix: binding.prefix,
3237
+ serviceId: binding.entry.serviceId,
3238
+ interfaceId: binding.entry.iface.info.id,
3239
+ interfaceHash: binding.entry.iface.schemaHash
3240
+ })) })
3241
+ }, regOpts));
3242
+ registrations.push(this.register(directoryInterface, {
3243
+ list: ({ interfaceId, interfaceIdPrefix, serviceId, serviceIdScopes, cursor, limit }) => {
3244
+ const all = this.listRegisteredInterfaces().filter((r) => opts.serviceId === void 0 || r.serviceId === opts.serviceId).filter((r) => interfaceId === void 0 || r.interfaceId === interfaceId).filter((r) => interfaceIdPrefix === void 0 || r.interfaceId.startsWith(interfaceIdPrefix)).filter((r) => serviceId === void 0 || r.serviceId === serviceId).filter((r) => serviceIdMatchesScopes(r.serviceId, serviceIdScopes));
3245
+ const start = cursor === void 0 ? 0 : Number.parseInt(cursor, 10) || 0;
3246
+ const end = limit === void 0 ? all.length : Math.min(all.length, start + limit);
3247
+ const page = all.slice(start, end).map((r) => ({
3248
+ serviceId: r.serviceId,
3249
+ interfaceId: r.interfaceId,
3250
+ interfaceHash: r.interfaceHash,
3251
+ serviceDescription: r.serviceDescription,
3252
+ rootPrincipalSets: r.rootPrincipalSets?.map((set) => set.map((p) => ({ ...p })))
3253
+ }));
3254
+ return end < all.length ? {
3255
+ items: page,
3256
+ nextCursor: String(end)
3257
+ } : { items: page };
3258
+ },
3259
+ watch: (params, _ctx, stream) => new Promise((resolve) => {
3260
+ const watcher = {
3261
+ interfaceId: params.interfaceId,
3262
+ interfaceIdPrefix: params.interfaceIdPrefix,
3263
+ serviceId: params.serviceId,
3264
+ serviceIdScopes: params.serviceIdScopes,
3265
+ send: () => stream.send({})
3266
+ };
3267
+ this._directoryWatchers.add(watcher);
3268
+ const finish = () => {
3269
+ this._directoryWatchers.delete(watcher);
3270
+ resolve({});
3271
+ };
3272
+ if (stream.signal.aborted) {
3273
+ finish();
3274
+ return;
3275
+ }
3276
+ stream.signal.addEventListener("abort", finish, { once: true });
3277
+ })
3278
+ }, regOpts));
3279
+ registrations.push(this.register(schemasInterface, { get: ({ interfaceId, hash }) => {
3280
+ const iface = this.findRegisteredInterface(interfaceId, hash);
3281
+ if (!iface) throw new RpcError("Interface not found", ErrorCode.methodNotFound, {
3282
+ reason: "unknown-interface",
3283
+ interfaceId,
3284
+ hash
3285
+ });
3286
+ return { schema: iface.toSchema() };
3287
+ } }, regOpts));
3288
+ if (opts.serviceId !== void 0) try {
3289
+ this.enableReflection();
3290
+ } catch (error) {
3291
+ for (const registration of registrations.reverse()) registration.dispose();
3292
+ throw error;
3293
+ }
3388
3294
  let disposed = false;
3389
3295
  return { dispose: () => {
3390
3296
  if (disposed) return;
3391
3297
  disposed = true;
3392
- this._observers.delete(observer);
3393
- this._clearCorrelationsIfUnused();
3298
+ for (const registration of registrations.reverse()) registration.dispose();
3394
3299
  } };
3395
3300
  }
3396
- dispose() {
3397
- for (const subscriber of [...this._subscribers]) subscriber.dispose();
3398
- this._subscribers.clear();
3399
- this._observers.clear();
3400
- this._outboundRequests.clear();
3401
- this._inboundRequests.clear();
3402
- this._trafficWatches.clear();
3403
- this._setActive(false);
3404
- }
3405
- _onMessage(direction, message) {
3406
- const now = Date.now();
3407
- this._pruneRequestCorrelations(now);
3408
- const publiclyObserved = this._subscribers.size !== 0 || this._observers.size !== 0;
3409
- let transit;
3410
- const endpoint = this._endpoint("id" in message ? message.id ?? void 0 : void 0);
3411
- const base = {
3412
- type: "transit",
3413
- timeMs: now,
3414
- nodeId: this._nodeId,
3415
- ...direction === "inbound" ? { in: endpoint } : { out: endpoint },
3416
- disposition: direction === "inbound" ? "consumed" : "forwarded"
3417
- };
3418
- if (isRequest(message)) {
3419
- const correlation = {
3420
- method: message.method,
3421
- timeMs: now
3422
- };
3423
- if (publiclyObserved) {
3424
- const map = this._requestMap(direction);
3425
- const key = requestKey(message.id);
3426
- map.delete(key);
3427
- map.set(key, correlation);
3428
- }
3429
- transit = {
3430
- ...base,
3431
- kind: "request",
3432
- method: message.method,
3433
- params: message.params
3434
- };
3435
- } else if (isResponse(message)) {
3436
- const responseMap = direction === "inbound" ? this._outboundRequests : this._inboundRequests;
3437
- const key = requestKey(message.id);
3438
- const correlation = responseMap.get(key);
3439
- responseMap.delete(key);
3440
- transit = {
3441
- ...base,
3442
- kind: "response",
3443
- method: correlation?.method,
3444
- result: "result" in message ? message.result : void 0,
3445
- error: "error" in message ? message.error : void 0
3446
- };
3447
- } else if (isNotification(message) && message.method === STREAM_METHOD) {
3448
- const params = message.params;
3449
- const streamEndpoint = this._endpoint(params?.requestId);
3450
- transit = {
3451
- ...base,
3452
- ...direction === "inbound" ? { in: streamEndpoint } : { out: streamEndpoint },
3453
- kind: "stream",
3454
- method: STREAM_METHOD,
3455
- params: message.params
3456
- };
3457
- } else transit = {
3458
- ...base,
3459
- kind: "notification",
3460
- method: message.method,
3461
- params: message.params
3462
- };
3463
- function requestKey(requestId) {
3464
- return JSON.stringify([typeof requestId, requestId]);
3465
- }
3466
- if (this._trafficWatches.accept(transit)) return;
3467
- if (!publiclyObserved) return;
3468
- for (const observer of [...this._observers]) try {
3469
- observer(transit);
3301
+ _notifyDirectoryWatchers(entry, serviceMetadataChanged = false) {
3302
+ for (const listener of this._directoryListeners) try {
3303
+ listener();
3470
3304
  } catch {}
3471
- for (const subscriber of this._subscribers) subscriber.enqueue(transit);
3472
- }
3473
- _requestMap(direction) {
3474
- return direction === "outbound" ? this._outboundRequests : this._inboundRequests;
3475
- }
3476
- _endpoint(requestId) {
3477
- return {
3478
- edgeId: this._portId,
3479
- portId: this._portId,
3480
- ...requestId !== void 0 ? { requestId } : {}
3481
- };
3482
- }
3483
- _clearCorrelationsIfUnused() {
3484
- if (this._subscribers.size !== 0 || this._observers.size !== 0) return;
3485
- this._outboundRequests.clear();
3486
- this._inboundRequests.clear();
3487
- }
3488
- _pruneRequestCorrelations(timeMs) {
3489
- this._pruneRequestMap(this._outboundRequests, timeMs);
3490
- this._pruneRequestMap(this._inboundRequests, timeMs);
3491
- }
3492
- _pruneRequestMap(map, timeMs) {
3493
- while (map.size !== 0) {
3494
- const first = map.entries().next().value;
3495
- if (first === void 0 || map.size <= MAX_REQUEST_CORRELATIONS && timeMs - first[1].timeMs <= REQUEST_CORRELATION_RETENTION_MS) return;
3496
- map.delete(first[0]);
3305
+ for (const watcher of this._directoryWatchers) {
3306
+ if (watcher.pending) continue;
3307
+ const directlyMatches = directoryWatcherMatchesEntry(watcher, entry);
3308
+ const metadataMatches = serviceMetadataChanged && entry.serviceId !== void 0 && [...this._registry.values()].some((candidate) => candidate.serviceId === entry.serviceId && directoryWatcherMatchesEntry(watcher, candidate));
3309
+ if (!directlyMatches && !metadataMatches) continue;
3310
+ watcher.pending = true;
3311
+ queueMicrotask(() => {
3312
+ watcher.pending = false;
3313
+ if (this._directoryWatchers.has(watcher)) watcher.send();
3314
+ });
3497
3315
  }
3498
3316
  }
3499
- };
3500
- //#endregion
3501
- //#region src/connection/linkRpcConnection.ts
3502
- /**
3503
- * High-level linkrpc connection. Layers method-name routing and zod-driven
3504
- * validation on top of a plain JSON-RPC channel.
3505
- *
3506
- * Typed proxies built by this class merely forward each call's
3507
- * `interfaceHash` hint to the channel so the signer stamps it into the
3508
- * `$hubrpc` envelope.
3509
- *
3510
- * `TInCtx` is the per-call out-of-band context the connection's transport
3511
- * carries. Default `undefined` covers cross-process and ordinary in-process
3512
- * transports. The hub's self/overlay connections instantiate with a
3513
- * concrete `TInCtx` (e.g. `Participant`) so handlers can see who originated
3514
- * the call.
3515
- *
3516
- * `TOutCtx` is the per-call override / extension bag the outbound sender
3517
- * understands (see {@link SendOpts.ctx}). It only ever flows into the
3518
- * sender as input, so it is a contravariant type parameter. Default `any`
3519
- * keeps the bare `LinkRpcConnection` a valid supertype for holders that do
3520
- * not care about the outbound ctx; pass a concrete shape (e.g.
3521
- * `SigningCallCtx`) to get precise {@link get} typing. The connection
3522
- * itself is agnostic to its contents — it merely forwards the `ctx`
3523
- * defaults supplied to {@link get}.
3524
- */
3525
- var LinkRpcConnection = class LinkRpcConnection {
3526
- /**
3527
- * Convenience: build a {@link JsonRpcChannel} `Channel` from the
3528
- * given transport and wrap it in an `LinkRpcConnection`. Use this
3529
- * when you have a transport at hand and don't need a decorator
3530
- * stack (e.g. signing).
3531
- */
3532
- static fromTransport(transport, options = {}) {
3533
- return new LinkRpcConnection(JsonRpcChannel.create(transport), options);
3534
- }
3535
- /** Underlying JSON-RPC sender — useful for callers that need raw access (e.g. to call hub-served methods that bypass the interface registry). */
3536
- channel;
3537
- /** key = `${serviceId ?? ""}::${interfaceId}` */
3538
- _registry = /* @__PURE__ */ new Map();
3539
- /** Descriptions for services that have been registered with a `serviceDescription`. */
3540
- _serviceDescriptions = /* @__PURE__ */ new Map();
3541
- /** Root-node-id requirement sets recorded per serviceId. */
3542
- _serviceRootPrincipalSets = /* @__PURE__ */ new Map();
3543
- _directoryWatchers = /* @__PURE__ */ new Set();
3544
- _directoryListeners = /* @__PURE__ */ new Set();
3545
- _inspection;
3546
- _trafficInspector;
3547
- _serviceInspectionRegistrations = /* @__PURE__ */ new Map();
3548
- _wireChannel;
3549
- _validateOutboundParams;
3550
- /** Bare-method bindings, keyed by their exact wire prefix. */
3551
- _bareBindings = /* @__PURE__ */ new Map();
3552
- _generateTopologyId;
3553
3317
  /**
3554
- * Construct from a {@link Channel} (binds the inbound handler and uses
3555
- * `channel.sender` for outbound calls) or from a bare
3556
- * {@link IRequestSender} (send-only — no inbound handler is registered,
3557
- * useful for bootstrap flows like `createManagedIdentity`).
3318
+ * Enable the root `hubrpc.node::getNodeId` topology-bootstrap service.
3319
+ *
3320
+ * The generated ids are stable while this registration is active. They are
3321
+ * unauthenticated correlation labels only; callers must not use them for
3322
+ * identity, capability, or authorization decisions.
3558
3323
  */
3559
- constructor(channel, options = {}) {
3560
- this._validateOutboundParams = options.validateOutboundParams !== false;
3561
- this._generateTopologyId = options.generateTopologyId ?? createInspectionId;
3562
- if ("sender" in channel) {
3563
- this._wireChannel = channel;
3564
- this.channel = channel.sender;
3565
- channel.setRequestHandler({
3566
- handleRequest: (c) => this._handleRequest(c),
3567
- handleNotification: (c) => this._handleNotification(c)
3568
- });
3569
- } else {
3570
- this._wireChannel = void 0;
3571
- this.channel = channel;
3324
+ enableInspection(descriptors) {
3325
+ if (this._inspection !== void 0) return this._inspection;
3326
+ const info = {
3327
+ nodeId: this._generateTopologyId("node"),
3328
+ portId: this._generateTopologyId("port"),
3329
+ ...descriptors !== void 0 ? { descriptors: [...descriptors] } : {}
3330
+ };
3331
+ const registration = this._register(nodeInterface, { getNodeId: () => info }, {}, true);
3332
+ const host = new InspectionHost({
3333
+ nodeId: info.nodeId,
3334
+ descriptors
3335
+ });
3336
+ try {
3337
+ if (this._wireChannel !== void 0) host.trackConnection(this, { portId: info.portId });
3338
+ } catch (error) {
3339
+ host.dispose();
3340
+ registration.dispose();
3341
+ throw error;
3572
3342
  }
3573
- }
3574
- get(ifaceOrTarget, opts) {
3575
- if (isBareInterfaceTarget(ifaceOrTarget)) {
3576
- if (opts !== void 0) throw new Error("get: bare interface targets do not accept service or call options.");
3577
- return this.getBare(ifaceOrTarget.interface, { prefix: ifaceOrTarget.prefix });
3343
+ this._inspectionHost = host;
3344
+ let disposed = false;
3345
+ const result = {
3346
+ ...info,
3347
+ observeTraffic: (observer) => host.observeTraffic(observer),
3348
+ dispose: () => {
3349
+ if (disposed) return;
3350
+ disposed = true;
3351
+ for (const serviceId of [...this._serviceInspectionRegistrations.keys()]) this._removeServiceInspection(serviceId);
3352
+ host.dispose();
3353
+ if (this._inspectionHost === host) this._inspectionHost = void 0;
3354
+ registration.dispose();
3355
+ if (this._inspection === result) this._inspection = void 0;
3356
+ }
3357
+ };
3358
+ this._inspection = result;
3359
+ try {
3360
+ const services = new Set(Array.from(this._registry.values()).filter((entry) => entry.serviceId !== void 0 && !entry.internalInspection).map((entry) => entry.serviceId));
3361
+ for (const serviceId of services) this._installServiceInspection(serviceId);
3362
+ } catch (error) {
3363
+ result.dispose();
3364
+ throw error;
3578
3365
  }
3579
- return this._buildClient(ifaceOrTarget, opts ?? {});
3580
- }
3581
- /**
3582
- * Get a typed client that emits foreign-protocol bare method names.
3583
- * Unlike {@link get}, these calls carry no LinkRPC interface metadata.
3584
- */
3585
- getBare(iface, opts = {}) {
3586
- const prefix = opts.prefix ?? "";
3587
- validateBarePrefix(prefix, "getBare");
3588
- for (const [name, member] of Object.entries(iface.members)) if (member instanceof RequestType && (member.clientStreamSchema !== void 0 || member.serverStreamSchema !== void 0)) throw new Error(`getBare: streaming method "${name}" is not supported on foreign wires.`);
3589
- return this._buildClient(iface, {}, prefix);
3366
+ return result;
3590
3367
  }
3591
- /** Get a service-scoped handle; all interfaces obtained from it route via `serviceId` (form 3). */
3592
- service(serviceId) {
3593
- return new ServiceHandle(this, serviceId);
3368
+ /** Number of active endpoint traffic stream subscribers. */
3369
+ get trafficObserverCount() {
3370
+ return this._inspectionHost?.observerCount ?? 0;
3594
3371
  }
3595
- /** Register handlers for an interface on this connection's server side. */
3596
- register(iface, handlers, opts = {}) {
3597
- return this._register(iface, handlers, opts, false);
3372
+ close() {
3373
+ if (this._closed) return;
3374
+ this._closed = true;
3375
+ for (const listener of [...this._closeListeners]) listener();
3376
+ this._closeListeners.clear();
3377
+ this._inspection?.dispose();
3378
+ this._wireObservers.clear();
3379
+ this._wireChannel?.setWireMessageObserver(void 0);
3380
+ this.channel.close();
3598
3381
  }
3599
- _register(iface, handlers, opts, internalInspection) {
3600
- const serviceId = opts.serviceId;
3601
- const key = `${serviceId ?? ""}::${iface.info.id}`;
3602
- if (this._registry.has(key)) throw new Error(`Interface "${iface.info.id}" already registered${serviceId ? ` under service "${serviceId}"` : ""}.`);
3603
- let serviceMetadataChanged = false;
3604
- if (opts.serviceDescription !== void 0) {
3605
- if (serviceId === void 0) throw new Error("register: `serviceDescription` requires `serviceId`.");
3606
- const existing = this._serviceDescriptions.get(serviceId);
3607
- if (existing !== void 0 && existing !== opts.serviceDescription) throw new Error(`register: conflicting descriptions for service "${serviceId}".`);
3608
- this._serviceDescriptions.set(serviceId, opts.serviceDescription);
3609
- serviceMetadataChanged = existing === void 0;
3382
+ _buildClient(iface, opts, barePrefix, defaultRoute = false, allFailuresAsValues = false) {
3383
+ const { serviceId, ...ctxRest } = opts;
3384
+ const sendOpts = barePrefix === void 0 ? {
3385
+ ctx: ctxRest,
3386
+ interfaceHash: iface.schemaHash
3387
+ } : void 0;
3388
+ const prefix = barePrefix ?? (defaultRoute ? "" : serviceId ? `${serviceId}::${iface.info.id}::` : `${iface.info.id}::`);
3389
+ const proxy = {};
3390
+ for (const [name, member] of Object.entries(iface.members)) {
3391
+ const wireMethod = `${prefix}${name}`;
3392
+ if (member.kind === "request") {
3393
+ if (member.clientStreamSchema !== void 0 || member.serverStreamSchema !== void 0) {
3394
+ const serverStreamSchema = member.serverStreamSchema;
3395
+ proxy[name] = (params, callOpts) => {
3396
+ const userListener = callOpts?.onMessage;
3397
+ const onStreamMessage = userListener !== void 0 ? (raw) => {
3398
+ if (serverStreamSchema !== void 0) {
3399
+ const r = safeParse(serverStreamSchema, raw);
3400
+ if (!r.success) return;
3401
+ userListener(r.data);
3402
+ return;
3403
+ }
3404
+ userListener(raw);
3405
+ } : void 0;
3406
+ const channelOpts = onStreamMessage !== void 0 ? {
3407
+ ...sendOpts,
3408
+ onStreamMessage
3409
+ } : sendOpts;
3410
+ let call;
3411
+ try {
3412
+ this._validateOutboundParamsFor(member, wireMethod, params);
3413
+ call = this.channel.sendRequestWithStream(wireMethod, params, channelOpts);
3414
+ } catch (cause) {
3415
+ if (!allFailuresAsValues) throw cause;
3416
+ call = {
3417
+ result: Promise.reject(cause),
3418
+ send: () => {
3419
+ throw cause;
3420
+ },
3421
+ cancel: () => {
3422
+ throw cause;
3423
+ },
3424
+ ping: async () => {
3425
+ throw cause;
3426
+ }
3427
+ };
3428
+ }
3429
+ const checked = addCheckedResult(call.result.then((raw) => validateWireResult(member.resultSchema, raw, wireMethod)), member, allFailuresAsValues);
3430
+ return Object.assign(checked, {
3431
+ send: async (payload) => call.send(payload),
3432
+ cancel: async (reason) => call.cancel(reason),
3433
+ dispose: (reason) => call.dispose?.(reason),
3434
+ ping: () => call.ping()
3435
+ });
3436
+ };
3437
+ } else proxy[name] = (params) => {
3438
+ return addCheckedResult((async () => {
3439
+ this._validateOutboundParamsFor(member, wireMethod, params);
3440
+ const raw = await this.channel.sendRequest(wireMethod, params, sendOpts);
3441
+ return validateWireResult(member.resultSchema, raw, wireMethod);
3442
+ })(), member, allFailuresAsValues);
3443
+ };
3444
+ } else {
3445
+ const notify = (params) => {
3446
+ this._validateOutboundParamsFor(member, wireMethod, params);
3447
+ this.channel.sendNotification(wireMethod, params, sendOpts);
3448
+ };
3449
+ proxy[name] = barePrefix === void 0 ? async (params) => notify(params) : notify;
3450
+ }
3610
3451
  }
3611
- if (opts.rootPrincipalSets !== void 0) {
3612
- if (serviceId === void 0) throw new Error("register: `rootPrincipalSets` requires `serviceId`.");
3613
- const existing = this._serviceRootPrincipalSets.get(serviceId);
3614
- if (existing !== void 0 && !rootPrincipalSetsEqual(existing, opts.rootPrincipalSets)) throw new Error(`register: conflicting rootPrincipalSets for service "${serviceId}".`);
3615
- this._serviceRootPrincipalSets.set(serviceId, opts.rootPrincipalSets);
3616
- serviceMetadataChanged ||= existing === void 0;
3452
+ return proxy;
3453
+ }
3454
+ _validateOutboundParamsFor(member, wireMethod, params) {
3455
+ if (!this._validateOutboundParams) return;
3456
+ validateValue(member.paramsSchema, params, `Invalid params for ${wireMethod}`, ErrorCode.invalidParams);
3457
+ }
3458
+ async _handleRequest(call) {
3459
+ const parsed = this._parseRouted(call.method);
3460
+ if (!parsed.ok) return notFound(parsed.reason, call.method);
3461
+ const { entry, memberName } = parsed;
3462
+ const member = entry.iface.members[memberName];
3463
+ if (!member) return notFound("unknown-method", call.method);
3464
+ const handler = entry.handlers[memberName];
3465
+ if (!handler) return notFound("unknown-method", call.method);
3466
+ const userParams = stripLinkRpcWireMeta(call.params);
3467
+ const parsedParams = safeParse(member.paramsSchema, userParams);
3468
+ if (!parsedParams.success) return { error: {
3469
+ code: ErrorCode.invalidParams,
3470
+ message: "Invalid params",
3471
+ data: { issues: parsedParams.error.issues }
3472
+ } };
3473
+ if (!(member instanceof RequestType)) return notFound("unknown-method", call.method);
3474
+ const stream = this._buildStreamApi(call.stream, call.signal, member);
3475
+ try {
3476
+ const result = await handler(parsedParams.data, call.context, stream);
3477
+ const encodedError = encodeApplicationError(member, result);
3478
+ if (encodedError !== void 0) return { error: encodedError };
3479
+ validateValue(member.resultSchema, result, `Invalid result for ${call.method}`, ErrorCode.internalError);
3480
+ return { result: result === void 0 ? null : result };
3481
+ } catch (e) {
3482
+ if (e instanceof RpcError) return { error: {
3483
+ code: e.code,
3484
+ message: e.message,
3485
+ data: e.data
3486
+ } };
3487
+ return { error: {
3488
+ code: ErrorCode.internalError,
3489
+ message: e instanceof Error ? e.message : String(e)
3490
+ } };
3617
3491
  }
3618
- const entry = {
3619
- iface,
3620
- handlers,
3621
- serviceId,
3622
- internalInspection
3492
+ }
3493
+ _buildStreamApi(callStream, signal, member) {
3494
+ return {
3495
+ send: (payload) => {
3496
+ if (member.serverStreamSchema) {
3497
+ const r = safeParse(member.serverStreamSchema, payload);
3498
+ if (!r.success) throw new Error(`Server stream payload failed schema validation: ${r.error.message}`);
3499
+ }
3500
+ return callStream.send(payload);
3501
+ },
3502
+ onMessage: (listener) => {
3503
+ callStream.onMessage((raw) => {
3504
+ if (member.clientStreamSchema) {
3505
+ const r = safeParse(member.clientStreamSchema, raw);
3506
+ if (!r.success) return;
3507
+ listener(r.data);
3508
+ return;
3509
+ }
3510
+ });
3511
+ },
3512
+ ping: () => callStream.ping(),
3513
+ signal
3623
3514
  };
3624
- this._registry.set(key, entry);
3625
- this._notifyDirectoryWatchers(entry, serviceMetadataChanged);
3626
- if (!internalInspection && serviceId !== void 0 && this._inspection !== void 0 && !this._serviceInspectionRegistrations.has(serviceId)) try {
3627
- this._installServiceInspection(serviceId);
3628
- } catch (error) {
3629
- this._registry.delete(key);
3630
- this._notifyDirectoryWatchers(entry);
3631
- throw error;
3632
- }
3633
- let disposed = false;
3634
- return { dispose: () => {
3635
- if (disposed) return;
3636
- disposed = true;
3637
- if (this._registry.get(key) !== entry) return;
3638
- this._registry.delete(key);
3639
- for (const [prefix, binding] of this._bareBindings) if (binding.entry === entry) this._bareBindings.delete(prefix);
3640
- this._notifyDirectoryWatchers(entry);
3641
- if (serviceId !== void 0 && !this._hasBusinessServiceRegistration(serviceId)) {
3642
- this._removeServiceInspection(serviceId);
3643
- this._serviceDescriptions.delete(serviceId);
3644
- this._serviceRootPrincipalSets.delete(serviceId);
3645
- }
3646
- } };
3647
3515
  }
3648
- /**
3649
- * Declare the preset interface for form-1 (bare-method) dispatch. The
3650
- * interface must already be registered under the root (no serviceId).
3651
- * Surfaced via `hubrpc.defaults::get`.
3652
- */
3653
- setPreset(iface) {
3654
- const key = `::${iface.info.id}`;
3655
- const entry = this._registry.get(key);
3656
- if (!entry) throw new Error(`setPreset: interface "${iface.info.id}" is not registered under the root service.`);
3657
- this._bareBindings.set("", {
3658
- prefix: "",
3659
- entry
3660
- });
3516
+ _handleNotification(call) {
3517
+ const parsed = this._parseRouted(call.method);
3518
+ if (!parsed.ok) return;
3519
+ const { entry, memberName } = parsed;
3520
+ const member = entry.iface.members[memberName];
3521
+ if (!(member instanceof NotificationType)) return;
3522
+ const handler = entry.handlers[memberName];
3523
+ if (!handler) return;
3524
+ const userParams = stripLinkRpcWireMeta(call.params);
3525
+ const parsedParams = safeParse(member.paramsSchema, userParams);
3526
+ if (!parsedParams.success) return;
3527
+ handler(parsedParams.data, call.context);
3661
3528
  }
3662
- /**
3663
- * Bind foreign-protocol bare methods to an already registered interface.
3664
- * Matching uses the longest prefix; once selected, a missing member does
3665
- * not fall through to a shorter binding.
3666
- */
3667
- bindBare(iface, opts) {
3668
- validateBarePrefix(opts.prefix, "bindBare");
3669
- const key = `${opts.serviceId ?? ""}::${iface.info.id}`;
3529
+ _parseRouted(method) {
3530
+ const parsed = parseMethodName(method);
3531
+ if (!parsed) return {
3532
+ ok: false,
3533
+ reason: "bad-method-grammar"
3534
+ };
3535
+ if (parsed.kind === "bare") {
3536
+ let selected;
3537
+ for (const binding of this._bareBindings.values()) if (parsed.member.startsWith(binding.prefix) && (selected === void 0 || binding.prefix.length > selected.prefix.length)) selected = binding;
3538
+ if (!selected) return {
3539
+ ok: false,
3540
+ reason: "no-preset"
3541
+ };
3542
+ return {
3543
+ ok: true,
3544
+ entry: selected.entry,
3545
+ memberName: parsed.member.slice(selected.prefix.length)
3546
+ };
3547
+ }
3548
+ const serviceId = parsed.kind === "full" ? parsed.serviceId : void 0;
3549
+ const key = `${serviceId ?? ""}::${parsed.interfaceId}`;
3670
3550
  const entry = this._registry.get(key);
3671
- if (!entry) throw new Error(`bindBare: interface "${iface.info.id}" is not registered` + (opts.serviceId === void 0 ? " under the root service." : ` under service "${opts.serviceId}".`));
3672
- if (this._bareBindings.has(opts.prefix)) throw new Error(`bindBare: prefix "${opts.prefix}" is already bound.`);
3673
- const binding = {
3674
- prefix: opts.prefix,
3675
- entry
3551
+ if (!entry) return {
3552
+ ok: false,
3553
+ reason: serviceId ? "unknown-service" : "unknown-interface"
3554
+ };
3555
+ return {
3556
+ ok: true,
3557
+ entry,
3558
+ memberName: parsed.member
3676
3559
  };
3677
- this._bareBindings.set(opts.prefix, binding);
3678
- let disposed = false;
3679
- return { dispose: () => {
3680
- if (disposed) return;
3681
- disposed = true;
3682
- if (this._bareBindings.get(opts.prefix) === binding) this._bareBindings.delete(opts.prefix);
3683
- } };
3684
3560
  }
3685
- /** Snapshot of every interface currently registered on this connection. */
3686
- listRegisteredInterfaces() {
3687
- return Array.from(this._registry.values()).map((r) => {
3688
- const sid = r.serviceId ?? "";
3689
- const item = {
3690
- serviceId: sid,
3691
- interfaceId: r.iface.info.id,
3692
- interfaceHash: r.iface.schemaHash
3693
- };
3694
- const desc = sid === "" ? void 0 : this._serviceDescriptions.get(sid);
3695
- if (desc !== void 0) item.serviceDescription = desc;
3696
- const sets = sid === "" ? void 0 : this._serviceRootPrincipalSets.get(sid);
3697
- if (sets !== void 0) item.rootPrincipalSets = sets;
3698
- return item;
3699
- });
3561
+ _installServiceInspection(serviceId) {
3562
+ if (this._serviceInspectionRegistrations.has(serviceId)) return;
3563
+ const info = this._inspection;
3564
+ const host = this._inspectionHost;
3565
+ if (info === void 0 || host === void 0) return;
3566
+ this._serviceInspectionRegistrations.set(serviceId, [host.expose(this, {
3567
+ serviceId,
3568
+ portId: info.portId
3569
+ })]);
3700
3570
  }
3701
- /** Subscribe to coarse local directory changes; listeners must re-list. */
3702
- onDidChangeDirectory(listener) {
3703
- this._directoryListeners.add(listener);
3704
- return () => this._directoryListeners.delete(listener);
3571
+ _removeServiceInspection(serviceId) {
3572
+ const registrations = this._serviceInspectionRegistrations.get(serviceId);
3573
+ if (registrations === void 0) return;
3574
+ this._serviceInspectionRegistrations.delete(serviceId);
3575
+ for (const registration of registrations.reverse()) registration.dispose();
3705
3576
  }
3706
- /**
3707
- * Look up a registered interface definition by id (and optional content
3708
- * hash). Returns `undefined` if no registered interface matches.
3709
- */
3710
- findRegisteredInterface(interfaceId, hash) {
3711
- for (const r of this._registry.values()) {
3712
- if (r.iface.info.id !== interfaceId) continue;
3713
- if (hash !== void 0 && r.iface.schemaHash !== hash) continue;
3714
- return r.iface;
3577
+ _hasBusinessServiceRegistration(serviceId) {
3578
+ return Array.from(this._registry.values()).some((entry) => entry.serviceId === serviceId && !entry.internalInspection);
3579
+ }
3580
+ };
3581
+ function createInspectionId(kind) {
3582
+ const bytes = /* @__PURE__ */ new Uint8Array(16);
3583
+ globalThis.crypto.getRandomValues(bytes);
3584
+ return `${kind}:${bytesToBase64Url(bytes)}`;
3585
+ }
3586
+ /** Structural equality for two CNF root-node-id requirement collections. */
3587
+ function rootPrincipalSetsEqual(a, b) {
3588
+ if (a.length !== b.length) return false;
3589
+ for (let i = 0; i < a.length; i++) {
3590
+ const sa = a[i];
3591
+ const sb = b[i];
3592
+ if (sa.length !== sb.length) return false;
3593
+ for (let j = 0; j < sa.length; j++) {
3594
+ if (sa[j].principal !== sb[j].principal) return false;
3595
+ if (sa[j].transitive === true !== (sb[j].transitive === true)) return false;
3715
3596
  }
3716
3597
  }
3717
- /**
3718
- * Register the three linkrpc reflection interfaces (`defaults`,
3719
- * `directory`, `schemas`), backed by this connection's live registry.
3720
- *
3721
- * By default they live under the root service (form-2 reachable as
3722
- * `hubrpc.directory::list`). Pass `serviceId` to additionally mount
3723
- * them under a specific service — useful for participants that live
3724
- * behind a hub, so callers can reach reflection via form-3
3725
- * `<serviceId>::hubrpc.directory::list`.
3726
- *
3727
- * Idempotent: re-registering the same `(serviceId, interfaceId)` pair
3728
- * is a no-op.
3729
- */
3730
- enableReflection(opts = {}) {
3731
- const key = `${opts.serviceId ?? ""}::${defaultsInterface.info.id}`;
3732
- if (this._registry.has(key)) return { dispose() {} };
3733
- const prefix = `${opts.serviceId ?? ""}::`;
3734
- for (const iface of [directoryInterface, schemasInterface]) if (this._registry.has(`${prefix}${iface.info.id}`)) throw new Error(`enableReflection: partial reflection registration for service "${opts.serviceId ?? ""}".`);
3735
- const regOpts = opts.serviceId !== void 0 ? { serviceId: opts.serviceId } : {};
3736
- const registrations = [];
3737
- registrations.push(this.register(defaultsInterface, {
3738
- get: () => {
3739
- const binding = this._bareBindings.get("");
3740
- if (!binding) return {};
3741
- return {
3742
- serviceId: binding.entry.serviceId,
3743
- interfaceId: binding.entry.iface.info.id,
3744
- interfaceHash: binding.entry.iface.schemaHash
3745
- };
3746
- },
3747
- listBindings: () => ({ bindings: Array.from(this._bareBindings.values()).sort((a, b) => a.prefix < b.prefix ? -1 : a.prefix > b.prefix ? 1 : 0).map((binding) => ({
3748
- prefix: binding.prefix,
3749
- serviceId: binding.entry.serviceId,
3750
- interfaceId: binding.entry.iface.info.id,
3751
- interfaceHash: binding.entry.iface.schemaHash
3752
- })) })
3753
- }, regOpts));
3754
- registrations.push(this.register(directoryInterface, {
3755
- list: ({ interfaceId, interfaceIdPrefix, serviceId, serviceIdScopes, cursor, limit }) => {
3756
- const all = this.listRegisteredInterfaces().filter((r) => opts.serviceId === void 0 || r.serviceId === opts.serviceId).filter((r) => interfaceId === void 0 || r.interfaceId === interfaceId).filter((r) => interfaceIdPrefix === void 0 || r.interfaceId.startsWith(interfaceIdPrefix)).filter((r) => serviceId === void 0 || r.serviceId === serviceId).filter((r) => serviceIdMatchesScopes(r.serviceId, serviceIdScopes));
3757
- const start = cursor === void 0 ? 0 : Number.parseInt(cursor, 10) || 0;
3758
- const end = limit === void 0 ? all.length : Math.min(all.length, start + limit);
3759
- const page = all.slice(start, end).map((r) => ({
3760
- serviceId: r.serviceId,
3761
- interfaceId: r.interfaceId,
3762
- interfaceHash: r.interfaceHash,
3763
- serviceDescription: r.serviceDescription,
3764
- rootPrincipalSets: r.rootPrincipalSets?.map((set) => set.map((p) => ({ ...p })))
3765
- }));
3766
- return end < all.length ? {
3767
- items: page,
3768
- nextCursor: String(end)
3769
- } : { items: page };
3770
- },
3771
- watch: (params, _ctx, stream) => new Promise((resolve) => {
3772
- const watcher = {
3773
- interfaceId: params.interfaceId,
3774
- interfaceIdPrefix: params.interfaceIdPrefix,
3775
- serviceId: params.serviceId,
3776
- serviceIdScopes: params.serviceIdScopes,
3777
- send: () => stream.send({})
3778
- };
3779
- this._directoryWatchers.add(watcher);
3780
- const finish = () => {
3781
- this._directoryWatchers.delete(watcher);
3782
- resolve({});
3783
- };
3784
- if (stream.signal.aborted) {
3785
- finish();
3786
- return;
3787
- }
3788
- stream.signal.addEventListener("abort", finish, { once: true });
3789
- })
3790
- }, regOpts));
3791
- registrations.push(this.register(schemasInterface, { get: ({ interfaceId, hash }) => {
3792
- const iface = this.findRegisteredInterface(interfaceId, hash);
3793
- if (!iface) throw new RpcError("Interface not found", ErrorCode.methodNotFound, {
3794
- reason: "unknown-interface",
3795
- interfaceId,
3796
- hash
3797
- });
3798
- return { schema: iface.toSchema() };
3799
- } }, regOpts));
3800
- if (opts.serviceId !== void 0) try {
3801
- this.enableReflection();
3802
- } catch (error) {
3803
- for (const registration of registrations.reverse()) registration.dispose();
3804
- throw error;
3598
+ return true;
3599
+ }
3600
+ function directoryWatcherMatchesEntry(watcher, entry) {
3601
+ const serviceId = entry.serviceId ?? "";
3602
+ return (watcher.interfaceId === void 0 || watcher.interfaceId === entry.iface.info.id) && (watcher.interfaceIdPrefix === void 0 || entry.iface.info.id.startsWith(watcher.interfaceIdPrefix)) && (watcher.serviceId === void 0 || watcher.serviceId === serviceId) && serviceIdMatchesScopes(serviceId, watcher.serviceIdScopes);
3603
+ }
3604
+ function addCheckedResult(promise, member, allFailuresAsValues = false) {
3605
+ const settled = promise.then((value) => ({
3606
+ ok: true,
3607
+ value
3608
+ }), (cause) => ({
3609
+ ok: false,
3610
+ error: decodeCheckedError(cause, member.applicationErrors),
3611
+ cause
3612
+ }));
3613
+ const result = settled.then((outcome) => {
3614
+ if (outcome.ok) return outcome.value;
3615
+ if (allFailuresAsValues) return new RpcFailure(outcome.error.kind === "application" ? {
3616
+ kind: "application",
3617
+ error: outcome.error
3618
+ } : {
3619
+ kind: "generic",
3620
+ error: outcome.error
3621
+ });
3622
+ if (outcome.error.kind === "application") return new RpcFailure(outcome.error);
3623
+ throw outcome.error.kind === "nonCompliantServer" ? outcome.error : outcome.cause;
3624
+ });
3625
+ return Object.assign(result, { result: async () => {
3626
+ result.catch(() => {});
3627
+ const outcome = await settled;
3628
+ return outcome.ok ? outcome : {
3629
+ ok: false,
3630
+ error: outcome.error
3631
+ };
3632
+ } });
3633
+ }
3634
+ function decodeCheckedError(cause, descriptors) {
3635
+ if (cause instanceof NonCompliantServerError) return cause;
3636
+ if (!(cause instanceof RpcError) || cause.origin !== "remote") return {
3637
+ kind: cause instanceof RpcError && cause.origin === "transport" ? "transport" : "local",
3638
+ cause
3639
+ };
3640
+ const original = {
3641
+ code: cause.code,
3642
+ message: cause.message,
3643
+ ...cause.hasData ? { data: cause.data } : {}
3644
+ };
3645
+ const candidates = descriptors.filter((candidate) => candidate.code === cause.code).sort((a, b) => Number(b.type !== void 0) - Number(a.type !== void 0));
3646
+ const body = {
3647
+ message: cause.message,
3648
+ ...cause.hasData ? { data: cause.data } : {}
3649
+ };
3650
+ const issues = [];
3651
+ for (const descriptor of candidates) {
3652
+ const parsed = parseDeclaredErrorBody(descriptor, body);
3653
+ if (!parsed.success) {
3654
+ issues.push(...parsed.issues);
3655
+ continue;
3805
3656
  }
3806
- let disposed = false;
3807
- return { dispose: () => {
3808
- if (disposed) return;
3809
- disposed = true;
3810
- for (const registration of registrations.reverse()) registration.dispose();
3811
- } };
3657
+ if (descriptor.type === void 0) return brandApplicationError({
3658
+ ...parsed.data,
3659
+ kind: "application",
3660
+ code: descriptor.code
3661
+ });
3662
+ const envelope = parsed.data.data;
3663
+ return brandApplicationError({
3664
+ kind: "application",
3665
+ code: descriptor.code,
3666
+ message: parsed.data.message,
3667
+ type: descriptor.type,
3668
+ ...typeof envelope === "object" && envelope !== null && "data" in envelope ? { data: envelope.data } : {}
3669
+ });
3812
3670
  }
3813
- _notifyDirectoryWatchers(entry, serviceMetadataChanged = false) {
3814
- for (const listener of this._directoryListeners) try {
3815
- listener();
3816
- } catch {}
3817
- for (const watcher of this._directoryWatchers) {
3818
- if (watcher.pending) continue;
3819
- const directlyMatches = directoryWatcherMatchesEntry(watcher, entry);
3820
- const metadataMatches = serviceMetadataChanged && entry.serviceId !== void 0 && [...this._registry.values()].some((candidate) => candidate.serviceId === entry.serviceId && directoryWatcherMatchesEntry(watcher, candidate));
3821
- if (!directlyMatches && !metadataMatches) continue;
3822
- watcher.pending = true;
3823
- queueMicrotask(() => {
3824
- watcher.pending = false;
3825
- if (this._directoryWatchers.has(watcher)) watcher.send();
3671
+ if (candidates.length > 0) return new NonCompliantServerError(original, issues);
3672
+ return {
3673
+ kind: "remote",
3674
+ ...original
3675
+ };
3676
+ }
3677
+ function encodeApplicationError(member, value) {
3678
+ if (!isApplicationErrorValue(value)) return void 0;
3679
+ const candidate = value;
3680
+ const descriptor = member.applicationErrors.find((error) => error.code === candidate.code && error.type === candidate.type && (error.bodySchema !== void 0 || error.type !== void 0 || error.message === candidate.message));
3681
+ if (descriptor === void 0) throw new RpcError(`Undeclared application error ${String(candidate.code)}`, ErrorCode.internalError);
3682
+ const data = Object.hasOwn(value, "data") ? { data: value.data } : {};
3683
+ const body = {
3684
+ message: value.message,
3685
+ ...descriptor.type === void 0 ? data : { data: {
3686
+ type: descriptor.type,
3687
+ ...data
3688
+ } }
3689
+ };
3690
+ const parsed = parseDeclaredErrorBody(descriptor, body);
3691
+ const issues = parsed.success ? jsonIssues(body) : parsed.issues;
3692
+ if (issues.length > 0) throw new RpcError(`Invalid data for application error ${descriptor.code}`, ErrorCode.internalError, { issues: issues.map((issue) => ({ ...issue })) });
3693
+ return {
3694
+ code: descriptor.code,
3695
+ ...body
3696
+ };
3697
+ }
3698
+ function validateWireResult(schema, raw, wireMethod) {
3699
+ const value = normalizeWireResult(schema, raw);
3700
+ validateValue(schema, value, `Invalid result for ${wireMethod}`, ErrorCode.internalError);
3701
+ return value;
3702
+ }
3703
+ /** Validate at both ends without stripping extra keys or applying schema transforms. */
3704
+ function validateValue(schema, value, message, code) {
3705
+ const checked = safeParse(schema, value);
3706
+ if (!checked.success) throw new RpcError(message, code, { issues: checked.error.issues });
3707
+ }
3708
+ /** Undo the server's undefined → null convention only when the schema rejects null. */
3709
+ function normalizeWireResult(schema, raw) {
3710
+ if (raw !== null) return raw;
3711
+ if (safeParse(schema, null).success) return null;
3712
+ if (safeParse(schema, void 0).success) return void 0;
3713
+ return null;
3714
+ }
3715
+ /** Service-scoped handle returned by `connection.service(id)`. */
3716
+ var ServiceHandle = class {
3717
+ _connection;
3718
+ _serviceId;
3719
+ constructor(_connection, _serviceId) {
3720
+ this._connection = _connection;
3721
+ this._serviceId = _serviceId;
3722
+ }
3723
+ getResultClient(iface, opts = {}) {
3724
+ return this._connection.getResultClient(iface, {
3725
+ ...opts,
3726
+ serviceId: this._serviceId
3727
+ });
3728
+ }
3729
+ get(iface, opts = {}) {
3730
+ return this._connection.get(iface, {
3731
+ ...opts,
3732
+ serviceId: this._serviceId
3733
+ });
3734
+ }
3735
+ register(ifaceOrTarget, handlers, opts = {}) {
3736
+ return this._connection.register(ifaceOrTarget, handlers, {
3737
+ ...opts,
3738
+ serviceId: this._serviceId
3739
+ });
3740
+ }
3741
+ };
3742
+ function notFound(reason, method) {
3743
+ return { error: {
3744
+ code: ErrorCode.methodNotFound,
3745
+ message: `Method not found: ${method}`,
3746
+ data: {
3747
+ reason,
3748
+ method
3749
+ }
3750
+ } };
3751
+ }
3752
+ //#endregion
3753
+ //#region src/schema/exportStaticHubSchema.ts
3754
+ /** Export a complete static contract, failing on missing reflection, schemas, or truncated discovery. */
3755
+ async function exportStaticHubSchema(channel, options = {}) {
3756
+ if (options.maxDepth !== void 0 && (!Number.isSafeInteger(options.maxDepth) || options.maxDepth < 0)) throw new Error("maxDepth must be a non-negative integer");
3757
+ const cursors = /* @__PURE__ */ new Map();
3758
+ const timed = {
3759
+ close: () => {},
3760
+ sendRequest: (method, params, opts) => {
3761
+ const call = channel.sendRequestWithStream(method, params, opts);
3762
+ return withRpcTimeout(Object.assign(call.result, {
3763
+ cancel: (reason) => call.cancel(reason),
3764
+ dispose: (reason) => call.dispose?.(reason)
3765
+ }), `contract export ${method}`, options.timeoutMs);
3766
+ },
3767
+ sendNotification: (method, params, opts) => channel.sendNotification(method, params, opts),
3768
+ sendRequestWithStream: (method, params, opts) => {
3769
+ const call = channel.sendRequestWithStream(method, params, opts);
3770
+ if (!method.endsWith("hubrpc.directory::list")) return call;
3771
+ return {
3772
+ send: (payload) => call.send(payload),
3773
+ cancel: (reason) => call.cancel(reason),
3774
+ dispose: (reason) => call.dispose?.(reason),
3775
+ ping: () => call.ping(),
3776
+ result: call.result.then((raw) => {
3777
+ const parsed = safeParse(directoryInterface.members.list.resultSchema, raw);
3778
+ if (!parsed.success) throw new Error(`Invalid directory response from ${method}`);
3779
+ const page = parsed.data;
3780
+ if (page.truncated) throw new Error(`Truncated directory response from ${method}`);
3781
+ if (!params || typeof params !== "object" || !("cursor" in params)) cursors.set(method, /* @__PURE__ */ new Set());
3782
+ if (page.nextCursor !== void 0) {
3783
+ const seen = cursors.get(method);
3784
+ if (seen.has(page.nextCursor)) throw new Error(`Repeated directory cursor from ${method}`);
3785
+ seen.add(page.nextCursor);
3786
+ }
3787
+ return raw;
3788
+ })
3789
+ };
3790
+ }
3791
+ };
3792
+ const connection = new LinkRpcConnection(timed);
3793
+ const explorer = new HubDirectoryExplorer(timed, {
3794
+ rootTarget: options.serviceId,
3795
+ maxDepth: options.maxDepth,
3796
+ timeoutMs: options.timeoutMs
3797
+ });
3798
+ try {
3799
+ await explorer.explore();
3800
+ const snapshot = explorer.graphSnapshot;
3801
+ const incomplete = [snapshot.root, ...snapshot.directories].filter((node) => node.state !== "loaded");
3802
+ if (!snapshot.complete || incomplete.length !== 0 || snapshot.result.inaccessible.length !== 0) throw new Error(`Incomplete contract directory discovery: ${incomplete.map((node) => `${node.target.serviceId ?? "<root>"}: ${node.inaccessibleReason ?? node.state}`).join(", ")}`);
3803
+ const explored = new Set([snapshot.root, ...snapshot.directories].map((node) => node.target.serviceId ?? ""));
3804
+ for (const listing of snapshot.result.listings) if (listing.interfaceId === directoryInterface.info.id && !explored.has(listing.serviceId)) throw new Error(`Incomplete contract directory discovery: '${listing.serviceId}' exceeds maxDepth`);
3805
+ const defaults = connection.get(defaultsInterface, { serviceId: options.serviceId });
3806
+ const preset = await defaults.get({});
3807
+ const { bindings } = await defaults.listBindings({});
3808
+ if (preset.interfaceId === void 0 !== (preset.interfaceHash === void 0)) throw new Error("Incomplete default interface reference: both interfaceId and interfaceHash are required");
3809
+ if (preset.serviceId !== void 0 && preset.interfaceId === void 0) throw new Error("A service-only preset cannot be represented by defaultInterface");
3810
+ const services = /* @__PURE__ */ new Map();
3811
+ const schemas = /* @__PURE__ */ new Map();
3812
+ async function resolve(ref, serviceId) {
3813
+ const key = `${ref.interfaceId}@${ref.interfaceHash}`;
3814
+ if (schemas.has(key)) return;
3815
+ const { schema } = await connection.get(schemasInterface, { serviceId }).get({
3816
+ interfaceId: ref.interfaceId,
3817
+ hash: ref.interfaceHash
3826
3818
  });
3819
+ const validated = parseStaticHubSchema({ interfaceSchemas: [schema] }).interfaceSchemas[0];
3820
+ if (validated.id !== ref.interfaceId || validated.hash !== ref.interfaceHash) throw new Error(`Schema response does not match ${key}`);
3821
+ schemas.set(key, validated);
3827
3822
  }
3828
- }
3829
- /**
3830
- * Enable the root `hubrpc.node::getNodeId` topology-bootstrap service.
3831
- *
3832
- * The generated ids are stable while this registration is active. They are
3833
- * unauthenticated correlation labels only; callers must not use them for
3834
- * identity, capability, or authorization decisions.
3835
- */
3836
- enableInspection(descriptors) {
3837
- if (this._inspection !== void 0) return this._inspection;
3838
- const info = {
3839
- nodeId: this._generateTopologyId("node"),
3840
- portId: this._generateTopologyId("port"),
3841
- ...descriptors !== void 0 ? { descriptors: [...descriptors] } : {}
3842
- };
3843
- const registration = this._register(nodeInterface, { getNodeId: () => info }, {}, true);
3844
- const trafficInspector = new EndpointTrafficInspector(info.nodeId, info.portId, (active) => this._wireChannel?.setWireMessageObserver(active ? trafficInspector.observe : void 0));
3845
- this._trafficInspector = trafficInspector;
3846
- trafficInspector.start();
3847
- let disposed = false;
3848
- const result = {
3849
- ...info,
3850
- observeTraffic: (observer) => trafficInspector.observeTransits(observer),
3851
- dispose: () => {
3852
- if (disposed) return;
3853
- disposed = true;
3854
- for (const serviceId of [...this._serviceInspectionRegistrations.keys()]) this._removeServiceInspection(serviceId);
3855
- trafficInspector.dispose();
3856
- if (this._trafficInspector === trafficInspector) this._trafficInspector = void 0;
3857
- registration.dispose();
3858
- if (this._inspection === result) this._inspection = void 0;
3859
- }
3860
- };
3861
- this._inspection = result;
3862
- try {
3863
- const services = new Set(Array.from(this._registry.values()).filter((entry) => entry.serviceId !== void 0 && !entry.internalInspection).map((entry) => entry.serviceId));
3864
- for (const serviceId of services) this._installServiceInspection(serviceId);
3865
- } catch (error) {
3866
- result.dispose();
3867
- throw error;
3823
+ for (const listing of snapshot.result.listings) {
3824
+ const ref = {
3825
+ interfaceId: listing.interfaceId,
3826
+ interfaceHash: listing.hash
3827
+ };
3828
+ const interfaces = services.get(listing.serviceId) ?? [];
3829
+ interfaces.push(ref);
3830
+ services.set(listing.serviceId, interfaces);
3831
+ await resolve(ref, listing.discoveredFrom || void 0);
3868
3832
  }
3869
- return result;
3833
+ const defaultInterface = preset.interfaceId === void 0 ? void 0 : {
3834
+ interfaceId: preset.interfaceId,
3835
+ interfaceHash: preset.interfaceHash
3836
+ };
3837
+ if (defaultInterface) await resolve(defaultInterface, options.serviceId ?? preset.serviceId);
3838
+ for (const binding of bindings) await resolve(binding, options.serviceId ?? binding.serviceId);
3839
+ const compare = (a, b) => a < b ? -1 : a > b ? 1 : 0;
3840
+ return parseStaticHubSchema({
3841
+ interfaceSchemas: [...schemas.values()].sort((a, b) => compare(`${a.id}@${a.hash}`, `${b.id}@${b.hash}`)),
3842
+ services: [...services].sort(([a], [b]) => compare(a, b)).map(([serviceId, interfaces]) => ({
3843
+ serviceId,
3844
+ interfaces: interfaces.sort((a, b) => compare(a.interfaceId, b.interfaceId))
3845
+ })),
3846
+ ...defaultInterface === void 0 ? {} : { defaultInterface },
3847
+ bareInterfaces: bindings.map(({ prefix, interfaceId, interfaceHash }) => ({
3848
+ prefix,
3849
+ interface: {
3850
+ interfaceId,
3851
+ interfaceHash
3852
+ }
3853
+ })).sort((a, b) => compare(a.prefix, b.prefix))
3854
+ });
3855
+ } finally {
3856
+ explorer.dispose();
3870
3857
  }
3871
- /** Number of active endpoint traffic stream subscribers. */
3872
- get trafficObserverCount() {
3873
- return this._trafficInspector?.observerCount ?? 0;
3858
+ }
3859
+ //#endregion
3860
+ //#region src/protocol/capability.ts
3861
+ /**
3862
+ * Structural wire-shape check for an untrusted signed capability. This does
3863
+ * not establish authenticity; callers must still verify its signature and
3864
+ * delegation chain.
3865
+ */
3866
+ function hasSignedCapabilityShape(value) {
3867
+ if (!_isRecord(value)) return false;
3868
+ if (typeof value.issuer !== "string" || typeof value.audience !== "string" || typeof value.nonce !== "string" || !Array.isArray(value.permissions) || !value.permissions.every(_hasPermissionShape) || value.expiresAtMs !== void 0 && typeof value.expiresAtMs !== "number" || value.parentHash !== void 0 && typeof value.parentHash !== "string") return false;
3869
+ return readSignature(value, "capability") !== void 0;
3870
+ }
3871
+ function _hasPermissionShape(value) {
3872
+ if (!_isRecord(value) || !_isRecord(value.target)) return false;
3873
+ const target = value.target;
3874
+ if (!_hasPatternShape(target.serviceId) || !_hasPatternShape(target.interfaceId) || !Array.isArray(target.members) || !target.members.every(_hasPatternShape) || target.interfaceHash !== void 0 && typeof target.interfaceHash !== "string" || value.canInvoke !== void 0 && typeof value.canInvoke !== "boolean" || value.canDelegate !== void 0 && typeof value.canDelegate !== "boolean") return false;
3875
+ if (value.params !== void 0) {
3876
+ if (!_isRecord(value.params) || !Object.values(value.params).every(_hasParamMatcherShape)) return false;
3874
3877
  }
3875
- close() {
3876
- this.channel.close();
3878
+ if (value.callBind !== void 0) {
3879
+ if (!_isRecord(value.callBind) || value.callBind.alg !== "sha256" || typeof value.callBind.payloadHash !== "string") return false;
3877
3880
  }
3878
- _buildClient(iface, opts, barePrefix) {
3879
- const { serviceId, ...ctxRest } = opts;
3880
- const sendOpts = barePrefix === void 0 ? {
3881
- ctx: ctxRest,
3882
- interfaceHash: iface.schemaHash
3883
- } : void 0;
3884
- const prefix = barePrefix ?? (serviceId ? `${serviceId}::${iface.info.id}::` : `${iface.info.id}::`);
3885
- const proxy = {};
3886
- for (const [name, member] of Object.entries(iface.members)) {
3887
- const wireMethod = `${prefix}${name}`;
3888
- if (member.kind === "request") {
3889
- if (member.clientStreamSchema !== void 0 || member.serverStreamSchema !== void 0) {
3890
- const serverStreamSchema = member.serverStreamSchema;
3891
- proxy[name] = (params, callOpts) => {
3892
- const userListener = callOpts?.onMessage;
3893
- const onStreamMessage = userListener !== void 0 ? (raw) => {
3894
- if (serverStreamSchema !== void 0) {
3895
- const r = safeParse(serverStreamSchema, raw);
3896
- if (!r.success) return;
3897
- userListener(r.data);
3898
- return;
3899
- }
3900
- userListener(raw);
3901
- } : void 0;
3902
- const channelOpts = onStreamMessage !== void 0 ? {
3903
- ...sendOpts,
3904
- onStreamMessage
3905
- } : sendOpts;
3906
- this._validateOutboundParamsFor(member, wireMethod, params);
3907
- const call = this.channel.sendRequestWithStream(wireMethod, params, channelOpts);
3908
- const checked = addCheckedResult(call.result.then((raw) => validateWireResult(member.resultSchema, raw, wireMethod)), member);
3909
- return Object.assign(checked, {
3910
- send: async (payload) => call.send(payload),
3911
- cancel: async (reason) => call.cancel(reason),
3912
- dispose: (reason) => call.dispose?.(reason),
3913
- ping: () => call.ping()
3914
- });
3915
- };
3916
- } else proxy[name] = (params) => {
3917
- return addCheckedResult((async () => {
3918
- this._validateOutboundParamsFor(member, wireMethod, params);
3919
- const raw = await this.channel.sendRequest(wireMethod, params, sendOpts);
3920
- return validateWireResult(member.resultSchema, raw, wireMethod);
3921
- })(), member);
3922
- };
3923
- } else {
3924
- const notify = (params) => {
3925
- this._validateOutboundParamsFor(member, wireMethod, params);
3926
- this.channel.sendNotification(wireMethod, params, sendOpts);
3927
- };
3928
- proxy[name] = barePrefix === void 0 ? async (params) => notify(params) : notify;
3881
+ return true;
3882
+ }
3883
+ function _hasPatternShape(value) {
3884
+ if (!_isRecord(value)) return false;
3885
+ return "exact" in value && typeof value.exact === "string" || "prefix" in value && typeof value.prefix === "string";
3886
+ }
3887
+ function _hasParamMatcherShape(value) {
3888
+ if (!_isRecord(value)) return false;
3889
+ if ("exact" in value) return true;
3890
+ if ("enum" in value) return Array.isArray(value.enum);
3891
+ if ("prefix" in value) return typeof value.prefix === "string";
3892
+ if ("subsetOf" in value) return Array.isArray(value.subsetOf) && value.subsetOf.every((item) => typeof item === "string");
3893
+ return value.any === true;
3894
+ }
3895
+ function _isRecord(value) {
3896
+ return value !== null && typeof value === "object" && !Array.isArray(value);
3897
+ }
3898
+ /**
3899
+ * Addressing match for a single permission: do its
3900
+ * `serviceId`/`interfaceId`/`interfaceHash`/`members` patterns admit
3901
+ * `target`?
3902
+ */
3903
+ function permissionMatchesTarget(target, perm) {
3904
+ return _targetMismatchReason(target, perm) === void 0;
3905
+ }
3906
+ var TargetMismatchReason = class {
3907
+ mismatch;
3908
+ constructor(mismatch) {
3909
+ this.mismatch = mismatch;
3910
+ }
3911
+ toString() {
3912
+ const mismatch = this.mismatch;
3913
+ switch (mismatch.kind) {
3914
+ case "serviceId":
3915
+ case "interfaceId": return `${mismatch.kind} ${JSON.stringify(mismatch.actual)} does not match ${_formatPattern(mismatch.expected)}`;
3916
+ case "interfaceHash": return `interfaceHash ${mismatch.actual === void 0 ? "<none>" : JSON.stringify(mismatch.actual)} does not match required ${JSON.stringify(mismatch.expected)}`;
3917
+ case "invalidMembers": return "permission target has invalid members";
3918
+ case "member": {
3919
+ const allowed = mismatch.expected.map(_formatPattern).join(", ") || "<none>";
3920
+ return `member ${JSON.stringify(mismatch.actual)} does not match allowed patterns [${allowed}]`;
3929
3921
  }
3930
3922
  }
3931
- return proxy;
3932
- }
3933
- _validateOutboundParamsFor(member, wireMethod, params) {
3934
- if (!this._validateOutboundParams) return;
3935
- validateValue(member.paramsSchema, params, `Invalid params for ${wireMethod}`, ErrorCode.invalidParams);
3936
3923
  }
3937
- async _handleRequest(call) {
3938
- const parsed = this._parseRouted(call.method);
3939
- if (!parsed.ok) return notFound(parsed.reason, call.method);
3940
- const { entry, memberName } = parsed;
3941
- const member = entry.iface.members[memberName];
3942
- if (!member) return notFound("unknown-method", call.method);
3943
- const handler = entry.handlers[memberName];
3944
- if (!handler) return notFound("unknown-method", call.method);
3945
- const userParams = stripLinkRpcWireMeta(call.params);
3946
- const parsedParams = safeParse(member.paramsSchema, userParams);
3947
- if (!parsedParams.success) return { error: {
3948
- code: ErrorCode.invalidParams,
3949
- message: "Invalid params",
3950
- data: { issues: parsedParams.error.issues }
3951
- } };
3952
- if (!(member instanceof RequestType)) return notFound("unknown-method", call.method);
3953
- const stream = this._buildStreamApi(call.stream, call.signal, member);
3954
- try {
3955
- const result = await handler(parsedParams.data, call.context, stream);
3956
- const encodedError = encodeApplicationError(member, result);
3957
- if (encodedError !== void 0) return { error: encodedError };
3958
- validateValue(member.resultSchema, result, `Invalid result for ${call.method}`, ErrorCode.internalError);
3959
- return { result: result === void 0 ? null : result };
3960
- } catch (e) {
3961
- if (e instanceof RpcError) return { error: {
3962
- code: e.code,
3963
- message: e.message,
3964
- data: e.data
3965
- } };
3966
- return { error: {
3967
- code: ErrorCode.internalError,
3968
- message: e instanceof Error ? e.message : String(e)
3969
- } };
3970
- }
3924
+ };
3925
+ function _targetMismatchReason(target, perm) {
3926
+ const t = perm.target;
3927
+ if (!_patternMatches(target.serviceId, t.serviceId, "/")) return new TargetMismatchReason({
3928
+ kind: "serviceId",
3929
+ actual: target.serviceId,
3930
+ expected: t.serviceId
3931
+ });
3932
+ if (!_patternMatches(target.interfaceId, t.interfaceId, ".")) return new TargetMismatchReason({
3933
+ kind: "interfaceId",
3934
+ actual: target.interfaceId,
3935
+ expected: t.interfaceId
3936
+ });
3937
+ if (t.interfaceHash !== void 0 && target.interfaceHash !== t.interfaceHash) return new TargetMismatchReason({
3938
+ kind: "interfaceHash",
3939
+ actual: target.interfaceHash,
3940
+ expected: t.interfaceHash
3941
+ });
3942
+ if (!Array.isArray(t.members)) return new TargetMismatchReason({ kind: "invalidMembers" });
3943
+ if (!t.members.some((m) => _patternMatches(target.member, m, ""))) return new TargetMismatchReason({
3944
+ kind: "member",
3945
+ actual: target.member,
3946
+ expected: t.members
3947
+ });
3948
+ }
3949
+ function _patternMatches(value, pat, delim) {
3950
+ if ("exact" in pat) return value === pat.exact;
3951
+ const p = pat.prefix;
3952
+ if (p === "") return true;
3953
+ if (value === p) return true;
3954
+ if (delim === "") return value.startsWith(p);
3955
+ return value.startsWith(p + delim);
3956
+ }
3957
+ function _formatPattern(pattern) {
3958
+ return "exact" in pattern ? `exact ${JSON.stringify(pattern.exact)}` : `prefix ${JSON.stringify(pattern.prefix)}`;
3959
+ }
3960
+ /**
3961
+ * Strict allowlist match for {@link Permission.params}. The set of
3962
+ * top-level keys in the call MUST equal the declared set; values must
3963
+ * satisfy their matchers. Nested objects/arrays are compared as opaque
3964
+ * JSON values via canonical-JSON equality. Pure (no crypto).
3965
+ */
3966
+ function matchParams(declared, actualParams) {
3967
+ const declaredKeys = Object.keys(declared).sort();
3968
+ const actualObj = actualParams !== null && typeof actualParams === "object" && !Array.isArray(actualParams) ? actualParams : void 0;
3969
+ const actualKeys = actualObj ? Object.keys(actualObj).sort() : [];
3970
+ if (declaredKeys.length !== actualKeys.length || declaredKeys.some((k, i) => k !== actualKeys[i])) return {
3971
+ ok: false,
3972
+ reason: `param keys mismatch (declared=${declaredKeys.join(",")}, actual=${actualKeys.join(",")})`
3973
+ };
3974
+ for (const k of declaredKeys) if (!_matchParamValue(declared[k], actualObj[k])) return {
3975
+ ok: false,
3976
+ reason: `param "${k}" does not match`
3977
+ };
3978
+ return { ok: true };
3979
+ }
3980
+ function _matchParamValue(matcher, value) {
3981
+ if ("any" in matcher) return true;
3982
+ if ("prefix" in matcher) return typeof value === "string" && value.startsWith(matcher.prefix);
3983
+ if ("subsetOf" in matcher) return Array.isArray(value) && value.every((v) => typeof v === "string" && matcher.subsetOf.includes(v));
3984
+ if ("exact" in matcher) return jcsCanonicalize(matcher.exact) === jcsCanonicalize(value);
3985
+ if ("enum" in matcher) {
3986
+ const v = jcsCanonicalize(value);
3987
+ return matcher.enum.some((opt) => jcsCanonicalize(opt) === v);
3971
3988
  }
3972
- _buildStreamApi(callStream, signal, member) {
3973
- return {
3974
- send: (payload) => {
3975
- if (member.serverStreamSchema) {
3976
- const r = safeParse(member.serverStreamSchema, payload);
3977
- if (!r.success) throw new Error(`Server stream payload failed schema validation: ${r.error.message}`);
3978
- }
3979
- return callStream.send(payload);
3980
- },
3981
- onMessage: (listener) => {
3982
- callStream.onMessage((raw) => {
3983
- if (member.clientStreamSchema) {
3984
- const r = safeParse(member.clientStreamSchema, raw);
3985
- if (!r.success) return;
3986
- listener(r.data);
3987
- return;
3988
- }
3989
- });
3990
- },
3991
- ping: () => callStream.ping(),
3992
- signal
3989
+ return false;
3990
+ }
3991
+ /**
3992
+ * Does `permission` admit `call` for the given `ability`? Checks, in
3993
+ * order: the ability flag (`canInvoke`/`canDelegate`, both default
3994
+ * `false`), the target address, the optional `params` allowlist, and the
3995
+ * optional `callBind` hash-binding. Pure.
3996
+ */
3997
+ function permissionPermits(call, permission, ability) {
3998
+ if (!(ability === "invoke" ? permission.canInvoke === true : permission.canDelegate === true)) return {
3999
+ ok: false,
4000
+ reason: `permission does not grant ${ability}`
4001
+ };
4002
+ const targetMismatch = _targetMismatchReason(call.target, permission);
4003
+ if (targetMismatch !== void 0) return {
4004
+ ok: false,
4005
+ reason: `permission does not address this call: ${targetMismatch.toString()}`
4006
+ };
4007
+ if (permission.params !== void 0) {
4008
+ const r = matchParams(permission.params, call.params);
4009
+ if (!r.ok) return {
4010
+ ok: false,
4011
+ reason: `params: ${r.reason}`
3993
4012
  };
3994
4013
  }
3995
- _handleNotification(call) {
3996
- const parsed = this._parseRouted(call.method);
3997
- if (!parsed.ok) return;
3998
- const { entry, memberName } = parsed;
3999
- const member = entry.iface.members[memberName];
4000
- if (!(member instanceof NotificationType)) return;
4001
- const handler = entry.handlers[memberName];
4002
- if (!handler) return;
4003
- const userParams = stripLinkRpcWireMeta(call.params);
4004
- const parsedParams = safeParse(member.paramsSchema, userParams);
4005
- if (!parsedParams.success) return;
4006
- handler(parsedParams.data, call.context);
4007
- }
4008
- _parseRouted(method) {
4009
- const parsed = parseMethodName(method);
4010
- if (!parsed) return {
4014
+ if (permission.callBind !== void 0) {
4015
+ if (permission.callBind.alg !== "sha256") return {
4011
4016
  ok: false,
4012
- reason: "bad-method-grammar"
4017
+ reason: `callBind: unsupported hash alg (${permission.callBind.alg})`
4013
4018
  };
4014
- if (parsed.kind === "bare") {
4015
- let selected;
4016
- for (const binding of this._bareBindings.values()) if (parsed.member.startsWith(binding.prefix) && (selected === void 0 || binding.prefix.length > selected.prefix.length)) selected = binding;
4017
- if (!selected) return {
4018
- ok: false,
4019
- reason: "no-preset"
4020
- };
4021
- return {
4022
- ok: true,
4023
- entry: selected.entry,
4024
- memberName: parsed.member.slice(selected.prefix.length)
4025
- };
4026
- }
4027
- const serviceId = parsed.kind === "full" ? parsed.serviceId : void 0;
4028
- const key = `${serviceId ?? ""}::${parsed.interfaceId}`;
4029
- const entry = this._registry.get(key);
4030
- if (!entry) return {
4019
+ if (call.callHash !== permission.callBind.payloadHash) return {
4031
4020
  ok: false,
4032
- reason: serviceId ? "unknown-service" : "unknown-interface"
4033
- };
4034
- return {
4035
- ok: true,
4036
- entry,
4037
- memberName: parsed.member
4021
+ reason: "callBind: payload hash mismatch"
4038
4022
  };
4039
4023
  }
4040
- _installServiceInspection(serviceId) {
4041
- if (this._serviceInspectionRegistrations.has(serviceId)) return;
4042
- const info = this._inspection;
4043
- const trafficInspector = this._trafficInspector;
4044
- if (info === void 0 || trafficInspector === void 0) return;
4045
- const registrations = [];
4046
- const opts = { serviceId };
4047
- try {
4048
- registrations.push(this._register(nodeInterface, { getNodeId: () => ({
4049
- nodeId: info.nodeId,
4050
- portId: info.portId
4051
- }) }, opts, true));
4052
- registrations.push(this._register(topologyInterface, {
4053
- getGraph: () => this._endpointGraph(serviceId, info),
4054
- watchGraph: async (_params, _ctx, stream) => {
4055
- await waitForAbort(stream.signal);
4056
- return {};
4057
- }
4058
- }, opts, true));
4059
- registrations.push(this._register(trafficInterface, {
4060
- watch: ({ methodPrefix, trafficIgnoreKey, focusRequest }, _ctx, stream) => this._watchTraffic(trafficInspector, {
4061
- methodPrefix,
4062
- trafficIgnoreKey,
4063
- focusRequest
4064
- }, stream),
4065
- watchWithPayloads: ({ methodPrefix, maxPayloadBytes, trafficIgnoreKey, focusRequest }, _ctx, stream) => this._watchTraffic(trafficInspector, {
4066
- methodPrefix,
4067
- maxPayloadBytes,
4068
- trafficIgnoreKey,
4069
- focusRequest
4070
- }, stream)
4071
- }, opts, true));
4072
- } catch (error) {
4073
- for (const registration of registrations.reverse()) registration.dispose();
4074
- throw error;
4024
+ return { ok: true };
4025
+ }
4026
+ /**
4027
+ * Does `cap` admit `call` for the given `ability`? True iff **any** of its
4028
+ * permissions does (the union over permissions). This is one *link's*
4029
+ * judgment; chain intersection is enforced by {@link verifyChain}.
4030
+ */
4031
+ function capabilityPermits(call, cap, ability) {
4032
+ let lastReason = `no permission grants ${ability} for this call`;
4033
+ for (const permission of cap.permissions) {
4034
+ const r = permissionPermits(call, permission, ability);
4035
+ if (r.ok) return { ok: true };
4036
+ lastReason = r.reason;
4037
+ }
4038
+ return {
4039
+ ok: false,
4040
+ reason: lastReason
4041
+ };
4042
+ }
4043
+ //#endregion
4044
+ //#region src/transport/messageTransport.ts
4045
+ /**
4046
+ * Observe every message crossing a transport without changing its buffering or
4047
+ * lifecycle behavior.
4048
+ */
4049
+ function traceMessageTransport(transport, trace) {
4050
+ return {
4051
+ send(message) {
4052
+ trace("send", message);
4053
+ return transport.send(message);
4054
+ },
4055
+ setListener(listener) {
4056
+ transport.setListener(listener === void 0 ? void 0 : (message) => {
4057
+ trace("receive", message);
4058
+ listener(message);
4059
+ });
4060
+ },
4061
+ dispose() {
4062
+ transport.dispose();
4075
4063
  }
4076
- this._serviceInspectionRegistrations.set(serviceId, registrations);
4064
+ };
4065
+ }
4066
+ /**
4067
+ * Pipe two transports together: every message one receives is forwarded to
4068
+ * the other's `send`. Returns a disposable that detaches both listeners.
4069
+ *
4070
+ * Neither transport is disposed — the caller owns their lifecycle. This is
4071
+ * the building block for relays (e.g. bridging a `WindowMessageTransport`
4072
+ * for an iframe to a multiplexer channel).
4073
+ */
4074
+ function connectTransports(a, b) {
4075
+ a.setListener((m) => void b.send(m));
4076
+ b.setListener((m) => void a.send(m));
4077
+ return { dispose() {
4078
+ a.setListener(void 0);
4079
+ b.setListener(void 0);
4080
+ } };
4081
+ }
4082
+ /**
4083
+ * Two in-memory transports wired back-to-back. Useful for tests and for
4084
+ * same-process bridges.
4085
+ *
4086
+ * Generic in the per-direction payload type so asymmetric pairs (e.g. one
4087
+ * side sends plain `JsonRpcMessage`, the other side sends
4088
+ * `JsonRpcMessage & { context: Participant }`) are expressible at the type
4089
+ * level. At runtime both halves just hand objects through by reference.
4090
+ */
4091
+ var TransportPair = class {
4092
+ a;
4093
+ b;
4094
+ constructor() {
4095
+ const aT = new _InMemoryTransport();
4096
+ const bT = new _InMemoryTransport();
4097
+ aT._peer = bT;
4098
+ bT._peer = aT;
4099
+ this.a = aT;
4100
+ this.b = bT;
4077
4101
  }
4078
- _removeServiceInspection(serviceId) {
4079
- const registrations = this._serviceInspectionRegistrations.get(serviceId);
4080
- if (registrations === void 0) return;
4081
- this._serviceInspectionRegistrations.delete(serviceId);
4082
- for (const registration of registrations.reverse()) registration.dispose();
4102
+ };
4103
+ var _InMemoryTransport = class {
4104
+ _peer;
4105
+ _listener;
4106
+ _buffer = [];
4107
+ _closed = false;
4108
+ send(message) {
4109
+ if (this._closed || this._peer._closed) return;
4110
+ this._peer._deliver(message);
4083
4111
  }
4084
- _hasBusinessServiceRegistration(serviceId) {
4085
- return Array.from(this._registry.values()).some((entry) => entry.serviceId === serviceId && !entry.internalInspection);
4112
+ setListener(listener) {
4113
+ this._listener = listener;
4114
+ if (listener) while (this._buffer.length > 0 && this._listener) {
4115
+ const m = this._buffer.shift();
4116
+ this._listener(m);
4117
+ }
4086
4118
  }
4087
- _endpointGraph(serviceId, info) {
4088
- return {
4089
- observerServiceId: serviceId,
4090
- entryNodeId: info.nodeId,
4091
- nodes: [{
4092
- nodeId: info.nodeId,
4093
- kind: "endpoint",
4094
- ...info.descriptors !== void 0 ? { descriptors: info.descriptors } : {},
4095
- ports: [{ portId: info.portId }]
4096
- }],
4097
- links: [],
4098
- routes: [{
4099
- serviceId,
4100
- nodeId: info.nodeId,
4101
- portId: info.portId,
4102
- match: "exact"
4103
- }]
4104
- };
4119
+ dispose() {
4120
+ this._closed = true;
4105
4121
  }
4106
- async _watchTraffic(inspector, options, stream) {
4107
- const subscription = inspector.subscribe(options, (event) => stream.send(event));
4108
- const dispose = () => subscription.dispose();
4109
- if (stream.signal.aborted) dispose();
4110
- else stream.signal.addEventListener("abort", dispose, { once: true });
4111
- try {
4112
- return await subscription.closed;
4113
- } finally {
4114
- stream.signal.removeEventListener("abort", dispose);
4115
- subscription.dispose();
4116
- }
4122
+ _deliver(m) {
4123
+ if (this._listener) this._listener(m);
4124
+ else this._buffer.push(m);
4117
4125
  }
4118
4126
  };
4119
- function createInspectionId(kind) {
4120
- const bytes = /* @__PURE__ */ new Uint8Array(16);
4121
- globalThis.crypto.getRandomValues(bytes);
4122
- return `${kind}:${bytesToBase64Url(bytes)}`;
4123
- }
4124
- /** Structural equality for two CNF root-node-id requirement collections. */
4125
- function rootPrincipalSetsEqual(a, b) {
4126
- if (a.length !== b.length) return false;
4127
- for (let i = 0; i < a.length; i++) {
4128
- const sa = a[i];
4129
- const sb = b[i];
4130
- if (sa.length !== sb.length) return false;
4131
- for (let j = 0; j < sa.length; j++) {
4132
- if (sa[j].principal !== sb[j].principal) return false;
4133
- if (sa[j].transitive === true !== (sb[j].transitive === true)) return false;
4134
- }
4127
+ //#endregion
4128
+ //#region src/transport/multiplexedTransport.ts
4129
+ var MultiplexedTransport = class MultiplexedTransport {
4130
+ static create(base, channels) {
4131
+ return new MultiplexedTransport(base, channels);
4135
4132
  }
4136
- return true;
4137
- }
4138
- function directoryWatcherMatchesEntry(watcher, entry) {
4139
- const serviceId = entry.serviceId ?? "";
4140
- return (watcher.interfaceId === void 0 || watcher.interfaceId === entry.iface.info.id) && (watcher.interfaceIdPrefix === void 0 || entry.iface.info.id.startsWith(watcher.interfaceIdPrefix)) && (watcher.serviceId === void 0 || watcher.serviceId === serviceId) && serviceIdMatchesScopes(serviceId, watcher.serviceIdScopes);
4141
- }
4142
- function addCheckedResult(promise, member) {
4143
- return Object.assign(promise, { result: async () => {
4144
- try {
4145
- return {
4146
- ok: true,
4147
- value: await promise
4148
- };
4149
- } catch (cause) {
4150
- return {
4151
- ok: false,
4152
- error: decodeCheckedError(cause, member.applicationErrors)
4153
- };
4154
- }
4155
- } });
4156
- }
4157
- function decodeCheckedError(cause, descriptors) {
4158
- if (!(cause instanceof RpcError) || cause.origin !== "remote") return {
4159
- kind: "local",
4160
- cause
4161
- };
4162
- const descriptor = descriptors.find((candidate) => candidate.code === cause.code && candidate.message === cause.message);
4163
- if (descriptor !== void 0) {
4164
- if (descriptor.dataSchema === void 0 && !cause.hasData) return {
4165
- kind: "application",
4166
- code: descriptor.code,
4167
- message: descriptor.message
4168
- };
4169
- if (descriptor.dataSchema !== void 0 && cause.hasData && matchesApplicationData(descriptor.dataSchema, cause.data)) {
4170
- if (safeParse(descriptor.dataSchema, cause.data).success) return {
4171
- kind: "application",
4172
- code: descriptor.code,
4173
- message: descriptor.message,
4174
- data: cause.data
4175
- };
4133
+ /** Logical transports keyed by the friendly channel names. */
4134
+ transports;
4135
+ _base;
4136
+ _byId = /* @__PURE__ */ new Map();
4137
+ _usedIds = /* @__PURE__ */ new Set();
4138
+ _disposed = false;
4139
+ constructor(base, channels) {
4140
+ this._base = base;
4141
+ const transports = {};
4142
+ for (const name of Object.keys(channels)) {
4143
+ const id = channels[name];
4144
+ transports[name] = this._createChannel(id);
4176
4145
  }
4146
+ this.transports = transports;
4147
+ base.setListener((data) => {
4148
+ (data == null ? void 0 : this._byId.get(data.ch))?._deliver(data.m);
4149
+ });
4177
4150
  }
4178
- return {
4179
- kind: "remote",
4180
- code: cause.code,
4181
- message: cause.message,
4182
- ...cause.hasData ? { data: cause.data } : {}
4183
- };
4184
- }
4185
- function encodeApplicationError(member, value) {
4186
- if (!isApplicationErrorValue(value)) return void 0;
4187
- const candidate = value;
4188
- const descriptor = member.applicationErrors.find((error) => error.code === candidate.code && error.message === candidate.message);
4189
- if (descriptor === void 0) throw new RpcError(`Undeclared application error ${String(candidate.code)}`, ErrorCode.internalError);
4190
- const hasData = Object.hasOwn(candidate, "data");
4191
- if (descriptor.dataSchema === void 0) {
4192
- if (hasData) throw new RpcError(`Application error ${descriptor.code} must not contain data`, ErrorCode.internalError);
4193
- return {
4194
- code: descriptor.code,
4195
- message: descriptor.message
4196
- };
4197
- }
4198
- if (!hasData) throw new RpcError(`Application error ${descriptor.code} requires data`, ErrorCode.internalError);
4199
- if (!matchesApplicationData(descriptor.dataSchema, candidate.data)) throw new RpcError(`Invalid data for application error ${descriptor.code}`, ErrorCode.internalError, { issues: [{ message: "Data must be JSON matching the declared wire schema" }] });
4200
- const parsed = safeParse(descriptor.dataSchema, candidate.data);
4201
- if (!parsed.success) throw new RpcError(`Invalid data for application error ${descriptor.code}`, ErrorCode.internalError, { issues: parsed.error.issues });
4202
- return {
4203
- code: descriptor.code,
4204
- message: descriptor.message,
4205
- data: candidate.data
4206
- };
4207
- }
4208
- /** Reject values JSON serialization would silently omit, coerce, or replace via toJSON. */
4209
- function isWireJsonValue(value, ancestors = /* @__PURE__ */ new Set()) {
4210
- if (value === null || typeof value === "string" || typeof value === "boolean") return true;
4211
- if (typeof value === "number") return Number.isFinite(value);
4212
- if (typeof value !== "object" || ancestors.has(value)) return false;
4213
- if (!Array.isArray(value) && Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) return false;
4214
- ancestors.add(value);
4215
- const children = Array.isArray(value) ? value.values() : Object.values(value);
4216
- for (const child of children) if (!isWireJsonValue(child, ancestors)) return false;
4217
- ancestors.delete(value);
4218
- return true;
4219
- }
4220
- const applicationDataSchemas = /* @__PURE__ */ new WeakMap();
4221
- function matchesApplicationData(schema, value) {
4222
- if (!isWireJsonValue(value)) return false;
4223
- let wire = applicationDataSchemas.get(schema);
4224
- if (wire === void 0) {
4225
- const components = {};
4226
- wire = {
4227
- schema: zodToSvcJsonSchema(schema, {
4228
- methodName: "applicationError",
4229
- schemaPosition: "data",
4230
- components
4231
- }),
4232
- components
4233
- };
4234
- applicationDataSchemas.set(schema, wire);
4151
+ /**
4152
+ * Add a logical channel after the multiplexer has started.
4153
+ *
4154
+ * Channel ids are permanently retired when disposed. This prevents a late
4155
+ * envelope for an old iframe from being delivered to a replacement iframe.
4156
+ */
4157
+ addChannel(id) {
4158
+ if (this._disposed) throw new Error("Cannot add a channel to a disposed multiplexed transport.");
4159
+ return this._createChannel(id);
4235
4160
  }
4236
- return matchesJsonSchema(value, wire.schema, { schemas: wire.components });
4237
- }
4238
- function validateWireResult(schema, raw, wireMethod) {
4239
- const value = normalizeWireResult(schema, raw);
4240
- validateValue(schema, value, `Invalid result for ${wireMethod}`, ErrorCode.internalError);
4241
- return value;
4242
- }
4243
- /** Validate at both ends without stripping extra keys or applying schema transforms. */
4244
- function validateValue(schema, value, message, code) {
4245
- const checked = safeParse(schema, value);
4246
- if (!checked.success) throw new RpcError(message, code, { issues: checked.error.issues });
4247
- }
4248
- /** Undo the server's undefined → null convention only when the schema rejects null. */
4249
- function normalizeWireResult(schema, raw) {
4250
- if (raw !== null) return raw;
4251
- if (safeParse(schema, null).success) return null;
4252
- if (safeParse(schema, void 0).success) return void 0;
4253
- return null;
4254
- }
4255
- /** Service-scoped handle returned by `connection.service(id)`. */
4256
- var ServiceHandle = class {
4257
- _connection;
4258
- _serviceId;
4259
- constructor(_connection, _serviceId) {
4260
- this._connection = _connection;
4261
- this._serviceId = _serviceId;
4161
+ dispose() {
4162
+ if (this._disposed) return;
4163
+ this._disposed = true;
4164
+ this._base.setListener(void 0);
4165
+ for (const t of [...this._byId.values()]) t.dispose();
4166
+ this._byId.clear();
4262
4167
  }
4263
- get(iface, opts = {}) {
4264
- return this._connection.get(iface, {
4265
- ...opts,
4266
- serviceId: this._serviceId
4168
+ _createChannel(id) {
4169
+ if (id.length === 0) throw new Error("Multiplexed transport channel ids must not be empty.");
4170
+ if (this._usedIds.has(id)) throw new Error(`Multiplexed transport channel id "${id}" has already been used.`);
4171
+ this._usedIds.add(id);
4172
+ const t = new _ChannelTransport(this._base, id, () => {
4173
+ if (this._byId.get(id) === t) this._byId.delete(id);
4267
4174
  });
4175
+ this._byId.set(id, t);
4176
+ return t;
4268
4177
  }
4269
- register(iface, handlers, opts = {}) {
4270
- return this._connection.register(iface, handlers, {
4271
- ...opts,
4272
- serviceId: this._serviceId
4178
+ };
4179
+ var _ChannelTransport = class {
4180
+ _base;
4181
+ _ch;
4182
+ _onDispose;
4183
+ _listener;
4184
+ _buffer = [];
4185
+ _closed = false;
4186
+ constructor(_base, _ch, _onDispose) {
4187
+ this._base = _base;
4188
+ this._ch = _ch;
4189
+ this._onDispose = _onDispose;
4190
+ }
4191
+ send(message) {
4192
+ if (this._closed) return;
4193
+ this._base.send({
4194
+ $mux: "v1",
4195
+ ch: this._ch,
4196
+ m: message
4273
4197
  });
4274
4198
  }
4199
+ setListener(listener) {
4200
+ this._listener = listener;
4201
+ if (listener) while (this._buffer.length > 0 && this._listener) listener(this._buffer.shift());
4202
+ }
4203
+ dispose() {
4204
+ if (this._closed) return;
4205
+ this._closed = true;
4206
+ this._listener = void 0;
4207
+ this._buffer.length = 0;
4208
+ this._onDispose();
4209
+ }
4210
+ _deliver(m) {
4211
+ if (this._closed) return;
4212
+ if (this._listener) this._listener(m);
4213
+ else this._buffer.push(m);
4214
+ }
4275
4215
  };
4276
- function notFound(reason, method) {
4277
- return { error: {
4278
- code: ErrorCode.methodNotFound,
4279
- message: `Method not found: ${method}`,
4280
- data: {
4281
- reason,
4282
- method
4283
- }
4284
- } };
4285
- }
4286
- function waitForAbort(signal) {
4287
- if (signal.aborted) return Promise.resolve();
4288
- return new Promise((resolve) => {
4289
- signal.addEventListener("abort", () => resolve(), { once: true });
4290
- });
4291
- }
4292
4216
  //#endregion
4293
4217
  //#region src/connection/channelConnector.ts
4294
4218
  const DEFAULT_INITIAL_BACKOFF_MS = 250;
@@ -7752,6 +7676,6 @@ async function createSeededSigningIdentity(opts) {
7752
7676
  return KeypairSigningIdentity.fromKeypair(ed);
7753
7677
  }
7754
7678
  //#endregion
7755
- export { walkHubDetailed as $, bareInterfaceTarget as $t, attachCapabilities as A, PRINCIPAL_PREFIX as At, LinkRpcConnection as B, isRequest as Bt, identityStorageInterface as C, getKeyId as Ct, PublicWrappingIdentity as D, signingInput as Dt, PublicSigningIdentity as E, signingDomainValue as Et, generateX25519Keypair as F, principalForPublicKey as Ft, fetchDirectory as G, StreamDir as Gt, JsonRpcChannel as H, STREAM_METHOD as Ht, formatEndpointUri as I, publicKeyForKeyId as It, intersectServiceIdScopes as J, directoryInterface as Jt, fetchSchema as K, streamInterface as Kt, isHubEndpoint as L, resolveSigningKey as Lt, verifyRpcCall as M, bytesToBase64Url as Mt, crypto_exports as N, keyIdForPrincipal as Nt, Principal as O, withSignature as Ot, generateKeypair as P, keyIdForPublicKey as Pt, walkHub as Q, generateTsInterface as Qt, parseEndpointUri as R, ErrorCode as Rt, identityInterface as S, LINKRPC_UNSIGNED_KEY as St, KeypairSigningIdentity as T, signedHash as Tt, DEFAULT_WALK_DEPTH as U, StreamControlReason as Ut, ServiceHandle as V, isResponse as Vt, HubDirectoryExplorer as W, StreamControlType as Wt, serviceIdMatchesPattern as X, schemasInterface as Xt, normalizeServiceIdScopes as Y, directoryWatchNever as Yt, serviceIdMatchesScopes as Z, zServiceIdPattern as Zt, InMemoryManagedIdentity as _, HUBRPC_META_KEY as _t, verifyCall as a, TransportPair as at, registerIdentityOnOverlay as b, LINKRPC_META_KEY as bt, invoke as c, requireObjectParams as ct, prefix as d, parseMethodName as dt, materializeJsonSchema as en, DEFAULT_RPC_TIMEOUT_MS as et, signCapability as f, capabilityPermits as ft, createManagedPrincipal as g, permissionPermits as gt, PrincipalWithStore as h, permissionMatchesTarget as ht, signParams as i, MultiplexedTransport as it, signRpcCall as j, base64UrlToBytes$1 as jt, CapBag as k, KEY_ID_PREFIX as kt, issueCapability as l, stripLinkRpcWireMeta as lt, SigningSender as m, matchParams as mt, createSeededMemoryPrincipal as n, matchesJsonSchema as nn, Channel as nt, capBagFreshAt as o, connectTransports as ot, OneShotCapStaging as p, hasSignedCapabilityShape as pt, findMethodInSchema as q, defaultsInterface as qt, createSeededSigningIdentity as r, RpcError as rt, capabilityFreshAt as s, traceMessageTransport as st, createMemoryPrincipal as t, isAssignable as tn, withRpcTimeout as tt, permits as u, methodNameToTarget as ut, InMemoryManagedIdentityStorage as v, HUBRPC_SIGNATURE_KEY as vt, KeypairIdentity as w, readSignature as wt, registerLazyIdentityOnOverlay as x, LINKRPC_SIGNATURE_KEY as xt, createManagedIdentity as y, HUBRPC_UNSIGNED_KEY as yt, ChannelConnector as z, isNotification as zt };
7679
+ export { interfaceTarget as $, attachCapabilities as A, getKeyId as At, MultiplexedTransport as B, generateTsInterface as Bt, identityStorageInterface as C, stripLinkRpcWireMeta as Ct, PublicWrappingIdentity as D, LINKRPC_META_KEY as Dt, PublicSigningIdentity as E, HUBRPC_UNSIGNED_KEY as Et, generateX25519Keypair as F, withSignature as Ft, hasSignedCapabilityShape as G, validateJsonSchema as Gt, connectTransports as H, materializeJsonSchema as Ht, formatEndpointUri as I, methodNameToTarget as It, permissionPermits as J, matchParams as K, NonCompliantServerError as Kt, isHubEndpoint as L, parseMethodName as Lt, verifyRpcCall as M, signedHash as Mt, crypto_exports as N, signingDomainValue as Nt, Principal as O, LINKRPC_SIGNATURE_KEY as Ot, generateKeypair as P, signingInput as Pt, defaultInterfaceTarget as Q, parseEndpointUri as R, generateTsContract as Rt, identityInterface as S, requireObjectParams as St, KeypairSigningIdentity as T, HUBRPC_SIGNATURE_KEY as Tt, traceMessageTransport as U, isAssignable as Ut, TransportPair as V, bareInterfaceTarget as Vt, capabilityPermits as W, matchesJsonSchema as Wt, LinkRpcConnection as X, exportStaticHubSchema as Y, ServiceHandle as Z, InMemoryManagedIdentity as _, defaultsInterface as _t, verifyCall as a, HubDirectoryExplorer as at, registerIdentityOnOverlay as b, schemasInterface as bt, invoke as c, findMethodInSchema as ct, prefix as d, serviceIdMatchesPattern as dt, isInterfaceTarget as et, signCapability as f, serviceIdMatchesScopes as ft, createManagedPrincipal as g, withRpcTimeout as gt, PrincipalWithStore as h, DEFAULT_RPC_TIMEOUT_MS as ht, signParams as i, DEFAULT_WALK_DEPTH as it, signRpcCall as j, readSignature as jt, CapBag as k, LINKRPC_UNSIGNED_KEY as kt, issueCapability as l, intersectServiceIdScopes as lt, SigningSender as m, walkHubDetailed as mt, createSeededMemoryPrincipal as n, Channel as nt, capBagFreshAt as o, fetchDirectory as ot, OneShotCapStaging as p, walkHub as pt, permissionMatchesTarget as q, createSeededSigningIdentity as r, RpcError as rt, capabilityFreshAt as s, fetchSchema as st, createMemoryPrincipal as t, JsonRpcChannel as tt, permits as u, normalizeServiceIdScopes as ut, InMemoryManagedIdentityStorage as v, directoryInterface as vt, KeypairIdentity as w, HUBRPC_META_KEY as wt, registerLazyIdentityOnOverlay as x, zServiceIdPattern as xt, createManagedIdentity as y, directoryWatchNever as yt, ChannelConnector as z, parseStaticHubSchema as zt };
7756
7680
 
7757
- //# sourceMappingURL=src-CAhXne71.js.map
7681
+ //# sourceMappingURL=src-DKjTceek.js.map