@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.
- package/README.md +266 -31
- package/dist/chunks/{hub.interfaces-Bf4VfWNX.js → hub.interfaces-Deslgtgr.js} +2 -2
- package/dist/chunks/{hub.interfaces-Bf4VfWNX.js.map → hub.interfaces-Deslgtgr.js.map} +1 -1
- package/dist/chunks/{hubAccess-C06qAKao.d.ts → hubAccess-BmPvNNaM.d.ts} +2 -2
- package/dist/chunks/{hubAccess-C06qAKao.d.ts.map → hubAccess-BmPvNNaM.d.ts.map} +1 -1
- package/dist/chunks/{hubFacade-BIB9CoAv.js → hubFacade-BKTQXCdF.js} +2 -2
- package/dist/chunks/{hubFacade-BIB9CoAv.js.map → hubFacade-BKTQXCdF.js.map} +1 -1
- package/dist/chunks/{hubFacade-DtQpGpUy.d.ts → hubFacade-Bmh-SFWd.d.ts} +2 -2
- package/dist/chunks/{hubFacade-DtQpGpUy.d.ts.map → hubFacade-Bmh-SFWd.d.ts.map} +1 -1
- package/dist/chunks/{boundedTrafficSubscription-XZ1KwXZC.js → inspectionHost-BIkLKvr1.js} +1014 -32
- package/dist/chunks/inspectionHost-BIkLKvr1.js.map +1 -0
- package/dist/chunks/{linkRpcConnection-BadD37ok.d.ts → linkRpcConnection-Bbq20hQG.d.ts} +2215 -1699
- package/dist/chunks/linkRpcConnection-Bbq20hQG.d.ts.map +1 -0
- package/dist/chunks/{src-CAhXne71.js → src-DKjTceek.js} +1975 -2051
- package/dist/chunks/src-DKjTceek.js.map +1 -0
- package/dist/hub/client/index.d.ts +3 -3
- package/dist/hub/client/index.js +3 -3
- package/dist/hub/common/index.d.ts +2 -325
- package/dist/hub/common/index.d.ts.map +1 -1
- package/dist/hub/common/index.js +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +3 -3
- package/dist/inspection/index.d.ts +87 -2
- package/dist/inspection/index.d.ts.map +1 -1
- package/dist/inspection/index.js +2 -2
- package/dist/node.d.ts +3 -3
- package/dist/node.js +4 -4
- package/dist/web.d.ts +2 -2
- package/dist/web.js +3 -3
- package/package.json +7 -2
- package/dist/chunks/boundedTrafficSubscription-XZ1KwXZC.js.map +0 -1
- package/dist/chunks/linkRpcConnection-BadD37ok.d.ts.map +0 -1
- 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 {
|
|
7
|
-
import { array, boolean,
|
|
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
|
|
148
|
+
if (sup === false) return mismatch("No value is allowed");
|
|
124
149
|
if (_isRef(sup)) {
|
|
125
|
-
if (seen.has(sup.$ref)) return
|
|
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)
|
|
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 (
|
|
130
|
-
if (
|
|
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)
|
|
139
|
-
case "object": return v !== null && typeof v === "object" && !Array.isArray(v)
|
|
140
|
-
default: return
|
|
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
|
|
147
|
-
|
|
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
|
|
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
|
|
194
|
+
return valid;
|
|
155
195
|
}
|
|
156
|
-
function _matchesObject(v, sup, c) {
|
|
157
|
-
|
|
158
|
-
for (const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
326
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
}
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
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
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
const
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
const
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
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
|
|
1657
|
+
return all;
|
|
1407
1658
|
}
|
|
1408
|
-
function
|
|
1409
|
-
const
|
|
1410
|
-
|
|
1411
|
-
|
|
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
|
-
|
|
1414
|
-
|
|
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
|
-
/**
|
|
1422
|
-
|
|
1423
|
-
|
|
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
|
-
|
|
1427
|
-
|
|
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
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
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
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
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
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
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
|
-
|
|
1511
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1529
|
-
|
|
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
|
-
|
|
1542
|
-
|
|
1543
|
-
return readSignature(obj, domain)?.keyId;
|
|
1717
|
+
async function walkHubDetailed(channel, opts = {}) {
|
|
1718
|
+
return new HubDirectoryExplorer(channel, opts).explore();
|
|
1544
1719
|
}
|
|
1545
1720
|
/**
|
|
1546
|
-
*
|
|
1547
|
-
*
|
|
1721
|
+
* Backward-compatible wrapper over {@link walkHubDetailed} that returns only the
|
|
1722
|
+
* reachable listings, dropping the inaccessible-directory report.
|
|
1548
1723
|
*/
|
|
1549
|
-
function
|
|
1550
|
-
|
|
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
|
-
*
|
|
1563
|
-
*
|
|
1564
|
-
*
|
|
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
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
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
|
-
|
|
1579
|
-
|
|
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
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
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
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
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
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
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
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
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
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
*
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
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
|
-
|
|
3309
|
-
|
|
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
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
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
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
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
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
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
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
}
|
|
3348
|
-
|
|
3349
|
-
const
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
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
|
-
|
|
3387
|
-
|
|
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
|
-
|
|
3393
|
-
this._clearCorrelationsIfUnused();
|
|
3298
|
+
for (const registration of registrations.reverse()) registration.dispose();
|
|
3394
3299
|
} };
|
|
3395
3300
|
}
|
|
3396
|
-
|
|
3397
|
-
for (const
|
|
3398
|
-
|
|
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
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
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
|
-
*
|
|
3555
|
-
*
|
|
3556
|
-
*
|
|
3557
|
-
*
|
|
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
|
-
|
|
3560
|
-
this.
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
this.
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
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
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
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
|
|
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
|
-
/**
|
|
3592
|
-
|
|
3593
|
-
return
|
|
3368
|
+
/** Number of active endpoint traffic stream subscribers. */
|
|
3369
|
+
get trafficObserverCount() {
|
|
3370
|
+
return this._inspectionHost?.observerCount ?? 0;
|
|
3594
3371
|
}
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
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
|
-
|
|
3600
|
-
const serviceId = opts
|
|
3601
|
-
const
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
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
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
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
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
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
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
const
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
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
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
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)
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
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
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
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
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
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
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
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
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
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
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
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
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
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
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
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
|
-
|
|
3876
|
-
|
|
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
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
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
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
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
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
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
|
-
|
|
3996
|
-
|
|
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:
|
|
4017
|
+
reason: `callBind: unsupported hash alg (${permission.callBind.alg})`
|
|
4013
4018
|
};
|
|
4014
|
-
if (
|
|
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:
|
|
4033
|
-
};
|
|
4034
|
-
return {
|
|
4035
|
-
ok: true,
|
|
4036
|
-
entry,
|
|
4037
|
-
memberName: parsed.member
|
|
4021
|
+
reason: "callBind: payload hash mismatch"
|
|
4038
4022
|
};
|
|
4039
4023
|
}
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
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
|
-
|
|
4085
|
-
|
|
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
|
-
|
|
4088
|
-
|
|
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
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
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
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
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
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
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
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
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
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
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
|
-
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
|
|
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
|
-
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
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 {
|
|
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-
|
|
7681
|
+
//# sourceMappingURL=src-DKjTceek.js.map
|