@continuonai/rcan-ts 0.1.2 → 0.3.0
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 +85 -0
- package/bin/rcan-validate.mjs +86 -0
- package/dist/browser.d.mts +165 -32
- package/dist/browser.mjs +299 -2
- package/dist/browser.mjs.map +1 -1
- package/dist/index.d.mts +165 -32
- package/dist/index.d.ts +165 -32
- package/dist/index.js +308 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +299 -2
- package/dist/index.mjs.map +1 -1
- package/dist/rcan-validate.js +14 -1
- package/dist/rcan.iife.js +2 -2
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -201,48 +201,101 @@ declare class AuditChain {
|
|
|
201
201
|
}
|
|
202
202
|
|
|
203
203
|
/**
|
|
204
|
-
*
|
|
204
|
+
* rcan-ts types — TypeScript interfaces for RCAN config and message shapes.
|
|
205
205
|
*
|
|
206
|
-
*
|
|
206
|
+
* These provide parity with the rcan-py TypedDicts and enable
|
|
207
|
+
* static type checking with tsc/ts-jest/pyright.
|
|
207
208
|
*/
|
|
208
|
-
interface
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
209
|
+
interface RCANMetadata {
|
|
210
|
+
manufacturer?: string;
|
|
211
|
+
model?: string;
|
|
212
|
+
version?: string;
|
|
213
|
+
device_id?: string;
|
|
214
|
+
robot_name?: string;
|
|
215
|
+
rrn?: string;
|
|
216
|
+
rcan_uri?: string;
|
|
213
217
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
218
|
+
interface RCANAgentConfig {
|
|
219
|
+
provider?: string;
|
|
220
|
+
model?: string;
|
|
221
|
+
temperature?: number;
|
|
222
|
+
confidence_gates?: Array<{
|
|
223
|
+
threshold?: number;
|
|
224
|
+
[key: string]: unknown;
|
|
225
|
+
}>;
|
|
226
|
+
hitl_gates?: Array<Record<string, unknown>>;
|
|
227
|
+
commitment_chain?: {
|
|
228
|
+
enabled?: boolean;
|
|
229
|
+
[key: string]: unknown;
|
|
224
230
|
};
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
confidence_gates?: Array<{
|
|
229
|
-
threshold?: number;
|
|
230
|
-
}>;
|
|
231
|
-
hitl_gates?: Array<Record<string, unknown>>;
|
|
232
|
-
commitment_chain?: {
|
|
233
|
-
enabled?: boolean;
|
|
234
|
-
};
|
|
235
|
-
signing?: {
|
|
236
|
-
enabled?: boolean;
|
|
237
|
-
};
|
|
231
|
+
signing?: {
|
|
232
|
+
enabled?: boolean;
|
|
233
|
+
[key: string]: unknown;
|
|
238
234
|
};
|
|
235
|
+
}
|
|
236
|
+
interface RCANConfig {
|
|
237
|
+
rcan_version?: string;
|
|
238
|
+
metadata?: RCANMetadata;
|
|
239
|
+
agent?: RCANAgentConfig;
|
|
240
|
+
channels?: Record<string, unknown>;
|
|
239
241
|
rcan_protocol?: {
|
|
240
242
|
jwt_auth?: {
|
|
241
243
|
enabled?: boolean;
|
|
242
244
|
};
|
|
245
|
+
[key: string]: unknown;
|
|
243
246
|
};
|
|
244
247
|
[key: string]: unknown;
|
|
245
248
|
}
|
|
249
|
+
interface RCANMessageEnvelope {
|
|
250
|
+
cmd: string;
|
|
251
|
+
target: string;
|
|
252
|
+
rcan_version?: string;
|
|
253
|
+
confidence?: number;
|
|
254
|
+
timestamp_ns?: number;
|
|
255
|
+
params?: Record<string, unknown>;
|
|
256
|
+
signature?: string;
|
|
257
|
+
[key: string]: unknown;
|
|
258
|
+
}
|
|
259
|
+
interface RCANRegistryNode {
|
|
260
|
+
rcan_node_version: string;
|
|
261
|
+
node_type: "root" | "authoritative" | "resolver" | "cache";
|
|
262
|
+
operator: string;
|
|
263
|
+
namespace_prefix: string;
|
|
264
|
+
public_key: string;
|
|
265
|
+
api_base: string;
|
|
266
|
+
registry_ui?: string;
|
|
267
|
+
spec_version?: string;
|
|
268
|
+
capabilities?: string[];
|
|
269
|
+
sync_endpoint?: string;
|
|
270
|
+
last_sync?: string;
|
|
271
|
+
ttl_seconds?: number;
|
|
272
|
+
contact?: string;
|
|
273
|
+
governance?: string;
|
|
274
|
+
federation_protocol?: string;
|
|
275
|
+
}
|
|
276
|
+
interface RCANResolveResult {
|
|
277
|
+
rrn: string;
|
|
278
|
+
resolved_by: string;
|
|
279
|
+
namespace: string;
|
|
280
|
+
record: Record<string, unknown>;
|
|
281
|
+
cache_status: "HIT" | "MISS" | "STALE";
|
|
282
|
+
resolved_at: string;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* RCAN Validation — validate messages, configs, and URIs.
|
|
287
|
+
*
|
|
288
|
+
* Each function returns a ValidationResult with ok, issues, warnings, info.
|
|
289
|
+
*/
|
|
290
|
+
|
|
291
|
+
interface ValidationResult {
|
|
292
|
+
ok: boolean;
|
|
293
|
+
issues: string[];
|
|
294
|
+
warnings: string[];
|
|
295
|
+
info: string[];
|
|
296
|
+
}
|
|
297
|
+
declare function validateURI(uri: string): ValidationResult;
|
|
298
|
+
declare function validateMessage(data: unknown): ValidationResult;
|
|
246
299
|
declare function validateConfig(config: RCANConfig): ValidationResult;
|
|
247
300
|
|
|
248
301
|
/**
|
|
@@ -270,6 +323,22 @@ declare class RCANSignatureError extends RCANError {
|
|
|
270
323
|
declare class RCANRegistryError extends RCANError {
|
|
271
324
|
constructor(message: string);
|
|
272
325
|
}
|
|
326
|
+
declare class RCANNodeError extends RCANError {
|
|
327
|
+
readonly nodeUrl?: string | undefined;
|
|
328
|
+
constructor(message: string, nodeUrl?: string | undefined);
|
|
329
|
+
}
|
|
330
|
+
declare class RCANNodeNotFoundError extends RCANNodeError {
|
|
331
|
+
readonly rrn: string;
|
|
332
|
+
constructor(rrn: string, nodeUrl?: string);
|
|
333
|
+
}
|
|
334
|
+
declare class RCANNodeSyncError extends RCANNodeError {
|
|
335
|
+
readonly cause?: Error | undefined;
|
|
336
|
+
constructor(message: string, nodeUrl?: string, cause?: Error | undefined);
|
|
337
|
+
}
|
|
338
|
+
declare class RCANNodeTrustError extends RCANNodeError {
|
|
339
|
+
readonly reason: "invalid_signature" | "expired_cert" | "unknown_issuer" | "missing_pubkey";
|
|
340
|
+
constructor(reason: RCANNodeTrustError["reason"], nodeUrl?: string);
|
|
341
|
+
}
|
|
273
342
|
|
|
274
343
|
/**
|
|
275
344
|
* rcan-ts — RegistryClient
|
|
@@ -339,6 +408,68 @@ declare class RegistryClient {
|
|
|
339
408
|
}): Promise<Robot[]>;
|
|
340
409
|
}
|
|
341
410
|
|
|
411
|
+
/**
|
|
412
|
+
* rcan-ts — NodeClient
|
|
413
|
+
* Federation-aware node discovery and RRN resolution.
|
|
414
|
+
*
|
|
415
|
+
* Zero runtime dependencies — uses globalThis.fetch (Node 18+, browsers, CF Workers).
|
|
416
|
+
*
|
|
417
|
+
* @see https://rcan.dev/spec#section-federation
|
|
418
|
+
*/
|
|
419
|
+
|
|
420
|
+
declare class NodeClient {
|
|
421
|
+
private readonly rootUrl;
|
|
422
|
+
private readonly timeoutMs;
|
|
423
|
+
constructor(rootUrl?: string, timeoutMs?: number);
|
|
424
|
+
private _fetch;
|
|
425
|
+
/**
|
|
426
|
+
* Fetch /.well-known/rcan-node.json from any node URL.
|
|
427
|
+
* Throws RCANNodeTrustError if the manifest is malformed.
|
|
428
|
+
* Throws RCANNodeSyncError on network failure.
|
|
429
|
+
*/
|
|
430
|
+
getNodeManifest(nodeUrl: string): Promise<RCANRegistryNode>;
|
|
431
|
+
/**
|
|
432
|
+
* Get list of known registry nodes from root /api/v1/nodes.
|
|
433
|
+
* Optionally filter by namespace prefix (e.g. "BD").
|
|
434
|
+
*/
|
|
435
|
+
listNodes(prefix?: string): Promise<RCANRegistryNode[]>;
|
|
436
|
+
/**
|
|
437
|
+
* Find the authoritative node for an RRN.
|
|
438
|
+
* - Delegated RRN (RRN-BD-00000001): GET /api/v1/nodes?prefix=BD, return first match.
|
|
439
|
+
* - Root RRN (RRN-00000042): return root node manifest.
|
|
440
|
+
* - Unknown format: throws RCANNodeNotFoundError.
|
|
441
|
+
*/
|
|
442
|
+
discover(rrn: string): Promise<RCANRegistryNode>;
|
|
443
|
+
/**
|
|
444
|
+
* Resolve an RRN across the federation.
|
|
445
|
+
* First tries {rootUrl}/api/v1/resolve/{rrn}.
|
|
446
|
+
* On 404, discovers the authoritative node and tries {node.api_base}/robots/{rrn}.
|
|
447
|
+
*/
|
|
448
|
+
resolve(rrn: string): Promise<RCANResolveResult>;
|
|
449
|
+
/**
|
|
450
|
+
* Verify a node manifest is well-formed.
|
|
451
|
+
* Checks required fields, ed25519: public_key prefix, valid node_type, https:// api_base.
|
|
452
|
+
*/
|
|
453
|
+
verifyNode(manifest: unknown): manifest is RCANRegistryNode;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
declare function fetchCanonicalSchema(schemaName: string): Promise<object | null>;
|
|
457
|
+
/**
|
|
458
|
+
* Validate a config object against the canonical rcan-config schema from rcan.dev.
|
|
459
|
+
* Returns { valid: true } or { valid: false, errors: string[] }.
|
|
460
|
+
* Gracefully returns { valid: true, skipped: true } if schema unreachable.
|
|
461
|
+
*/
|
|
462
|
+
declare function validateConfigAgainstSchema(config: unknown): Promise<{
|
|
463
|
+
valid: boolean;
|
|
464
|
+
errors?: string[];
|
|
465
|
+
skipped?: boolean;
|
|
466
|
+
}>;
|
|
467
|
+
declare function validateNodeAgainstSchema(manifest: unknown): Promise<{
|
|
468
|
+
valid: boolean;
|
|
469
|
+
errors?: string[];
|
|
470
|
+
skipped?: boolean;
|
|
471
|
+
}>;
|
|
472
|
+
|
|
342
473
|
/**
|
|
343
474
|
* rcan-ts — Official TypeScript SDK for RCAN v1.2
|
|
344
475
|
* Robot Communication and Accountability Network
|
|
@@ -347,7 +478,9 @@ declare class RegistryClient {
|
|
|
347
478
|
* @see https://github.com/continuonai/rcan-ts
|
|
348
479
|
*/
|
|
349
480
|
|
|
350
|
-
declare const VERSION = "0.
|
|
481
|
+
declare const VERSION = "0.3.0";
|
|
482
|
+
declare const SPEC_VERSION = "1.2";
|
|
483
|
+
/** @deprecated Use SPEC_VERSION instead */
|
|
351
484
|
declare const RCAN_VERSION = "1.2";
|
|
352
485
|
|
|
353
|
-
export { type ApprovalStatus, AuditChain, AuditError, type ChainVerifyResult, CommitmentRecord, type CommitmentRecordData, type CommitmentRecordJSON, ConfidenceGate, GateError, HiTLGate, type ListResult, type PendingApproval, RCANAddressError, type RCANConfig, RCANError, RCANGateError, RCANMessage, type RCANMessageData, RCANMessageError, RCANRegistryError, RCANSignatureError, RCANValidationError, RCAN_VERSION, type RegistrationResult, RegistryClient, type Robot, type RobotRegistration, RobotURI, RobotURIError, type RobotURIOptions, type SignatureBlock, VERSION, type ValidationResult, validateConfig, validateMessage, validateURI };
|
|
486
|
+
export { type ApprovalStatus, AuditChain, AuditError, type ChainVerifyResult, CommitmentRecord, type CommitmentRecordData, type CommitmentRecordJSON, ConfidenceGate, GateError, HiTLGate, type ListResult, NodeClient, type PendingApproval, RCANAddressError, type RCANAgentConfig, type RCANConfig, RCANError, RCANGateError, RCANMessage, type RCANMessageData, type RCANMessageEnvelope, RCANMessageError, type RCANMetadata, RCANNodeError, RCANNodeNotFoundError, RCANNodeSyncError, RCANNodeTrustError, RCANRegistryError, type RCANRegistryNode, type RCANResolveResult, RCANSignatureError, RCANValidationError, RCAN_VERSION, type RegistrationResult, RegistryClient, type Robot, type RobotRegistration, RobotURI, RobotURIError, type RobotURIOptions, SPEC_VERSION, type SignatureBlock, VERSION, type ValidationResult, fetchCanonicalSchema, validateConfig, validateConfigAgainstSchema, validateMessage, validateNodeAgainstSchema, validateURI };
|
package/dist/index.js
CHANGED
|
@@ -26,11 +26,16 @@ __export(index_exports, {
|
|
|
26
26
|
ConfidenceGate: () => ConfidenceGate,
|
|
27
27
|
GateError: () => GateError,
|
|
28
28
|
HiTLGate: () => HiTLGate,
|
|
29
|
+
NodeClient: () => NodeClient,
|
|
29
30
|
RCANAddressError: () => RCANAddressError,
|
|
30
31
|
RCANError: () => RCANError,
|
|
31
32
|
RCANGateError: () => RCANGateError,
|
|
32
33
|
RCANMessage: () => RCANMessage,
|
|
33
34
|
RCANMessageError: () => RCANMessageError,
|
|
35
|
+
RCANNodeError: () => RCANNodeError,
|
|
36
|
+
RCANNodeNotFoundError: () => RCANNodeNotFoundError,
|
|
37
|
+
RCANNodeSyncError: () => RCANNodeSyncError,
|
|
38
|
+
RCANNodeTrustError: () => RCANNodeTrustError,
|
|
34
39
|
RCANRegistryError: () => RCANRegistryError,
|
|
35
40
|
RCANSignatureError: () => RCANSignatureError,
|
|
36
41
|
RCANValidationError: () => RCANValidationError,
|
|
@@ -38,9 +43,13 @@ __export(index_exports, {
|
|
|
38
43
|
RegistryClient: () => RegistryClient,
|
|
39
44
|
RobotURI: () => RobotURI,
|
|
40
45
|
RobotURIError: () => RobotURIError,
|
|
46
|
+
SPEC_VERSION: () => SPEC_VERSION,
|
|
41
47
|
VERSION: () => VERSION,
|
|
48
|
+
fetchCanonicalSchema: () => fetchCanonicalSchema,
|
|
42
49
|
validateConfig: () => validateConfig,
|
|
50
|
+
validateConfigAgainstSchema: () => validateConfigAgainstSchema,
|
|
43
51
|
validateMessage: () => validateMessage,
|
|
52
|
+
validateNodeAgainstSchema: () => validateNodeAgainstSchema,
|
|
44
53
|
validateURI: () => validateURI
|
|
45
54
|
});
|
|
46
55
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -693,9 +702,22 @@ function validateConfig(config) {
|
|
|
693
702
|
const meta = config.metadata ?? {};
|
|
694
703
|
const agent = config.agent ?? {};
|
|
695
704
|
const rcanProto = config.rcan_protocol ?? {};
|
|
705
|
+
for (const key of ["rcan_version", "metadata", "agent"]) {
|
|
706
|
+
if (!(key in config) || config[key] === void 0 || config[key] === null) {
|
|
707
|
+
fail(result, `Missing required key: '${key}'`);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
const rv = config.rcan_version;
|
|
711
|
+
if (rv) {
|
|
712
|
+
if (!/^\d+\.\d+$/.test(rv)) {
|
|
713
|
+
fail(result, `rcan_version '${rv}' must match pattern N.N (e.g. '1.2')`);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
696
716
|
if (!meta.manufacturer) fail(result, "L1: metadata.manufacturer is required (\xA72)");
|
|
697
717
|
if (!meta.model) fail(result, "L1: metadata.model is required (\xA72)");
|
|
698
|
-
if (!
|
|
718
|
+
if (!meta.device_id && !meta.robot_name) {
|
|
719
|
+
fail(result, "L1: metadata.device_id (or robot_name) is required (\xA72)");
|
|
720
|
+
}
|
|
699
721
|
if (!rcanProto.jwt_auth?.enabled) {
|
|
700
722
|
warn(result, "L2: jwt_auth not enabled (required for L2 conformance, \xA78)");
|
|
701
723
|
}
|
|
@@ -769,6 +791,39 @@ var RCANRegistryError = class extends RCANError {
|
|
|
769
791
|
Object.setPrototypeOf(this, new.target.prototype);
|
|
770
792
|
}
|
|
771
793
|
};
|
|
794
|
+
var RCANNodeError = class _RCANNodeError extends RCANError {
|
|
795
|
+
constructor(message, nodeUrl) {
|
|
796
|
+
super(message);
|
|
797
|
+
this.nodeUrl = nodeUrl;
|
|
798
|
+
this.name = "RCANNodeError";
|
|
799
|
+
Object.setPrototypeOf(this, _RCANNodeError.prototype);
|
|
800
|
+
}
|
|
801
|
+
};
|
|
802
|
+
var RCANNodeNotFoundError = class _RCANNodeNotFoundError extends RCANNodeError {
|
|
803
|
+
constructor(rrn, nodeUrl) {
|
|
804
|
+
super(`RRN not found in federation: ${rrn}`, nodeUrl);
|
|
805
|
+
this.rrn = rrn;
|
|
806
|
+
this.name = "RCANNodeNotFoundError";
|
|
807
|
+
Object.setPrototypeOf(this, _RCANNodeNotFoundError.prototype);
|
|
808
|
+
}
|
|
809
|
+
};
|
|
810
|
+
var RCANNodeSyncError = class _RCANNodeSyncError extends RCANNodeError {
|
|
811
|
+
constructor(message, nodeUrl, cause) {
|
|
812
|
+
super(message, nodeUrl);
|
|
813
|
+
this.cause = cause;
|
|
814
|
+
this.name = "RCANNodeSyncError";
|
|
815
|
+
Object.setPrototypeOf(this, _RCANNodeSyncError.prototype);
|
|
816
|
+
}
|
|
817
|
+
};
|
|
818
|
+
var RCANNodeTrustError = class _RCANNodeTrustError extends RCANNodeError {
|
|
819
|
+
reason;
|
|
820
|
+
constructor(reason, nodeUrl) {
|
|
821
|
+
super(`Node trust verification failed: ${reason}`, nodeUrl);
|
|
822
|
+
this.name = "RCANNodeTrustError";
|
|
823
|
+
this.reason = reason;
|
|
824
|
+
Object.setPrototypeOf(this, _RCANNodeTrustError.prototype);
|
|
825
|
+
}
|
|
826
|
+
};
|
|
772
827
|
|
|
773
828
|
// src/registry.ts
|
|
774
829
|
var DEFAULT_BASE_URL = "https://rcan-spec.pages.dev";
|
|
@@ -893,8 +948,250 @@ var RegistryClient = class {
|
|
|
893
948
|
}
|
|
894
949
|
};
|
|
895
950
|
|
|
951
|
+
// src/node.ts
|
|
952
|
+
var ROOT_NODE_URL = "https://rcan.dev";
|
|
953
|
+
var NODE_MANIFEST_PATH = "/.well-known/rcan-node.json";
|
|
954
|
+
var VALID_NODE_TYPES = /* @__PURE__ */ new Set([
|
|
955
|
+
"root",
|
|
956
|
+
"authoritative",
|
|
957
|
+
"resolver",
|
|
958
|
+
"cache"
|
|
959
|
+
]);
|
|
960
|
+
function parseRRNNamespace(rrn) {
|
|
961
|
+
const delegated = rrn.match(/^RRN-([A-Z0-9]{2,8})-(\d{8,16})$/);
|
|
962
|
+
if (delegated) return { type: "delegated", prefix: delegated[1], serial: delegated[2] };
|
|
963
|
+
const root = rrn.match(/^RRN-(\d{8,16})$/);
|
|
964
|
+
if (root) return { type: "root", serial: root[1] };
|
|
965
|
+
return null;
|
|
966
|
+
}
|
|
967
|
+
var NodeClient = class {
|
|
968
|
+
rootUrl;
|
|
969
|
+
timeoutMs;
|
|
970
|
+
constructor(rootUrl = ROOT_NODE_URL, timeoutMs = 1e4) {
|
|
971
|
+
this.rootUrl = rootUrl.replace(/\/$/, "");
|
|
972
|
+
this.timeoutMs = timeoutMs;
|
|
973
|
+
}
|
|
974
|
+
// ── Private helpers ─────────────────────────────────────────────────────────
|
|
975
|
+
async _fetch(url) {
|
|
976
|
+
const controller = new AbortController();
|
|
977
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
978
|
+
try {
|
|
979
|
+
return await globalThis.fetch(url, { signal: controller.signal });
|
|
980
|
+
} catch (err) {
|
|
981
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
982
|
+
throw new RCANNodeSyncError(`Request timed out: ${url}`, url, err);
|
|
983
|
+
}
|
|
984
|
+
throw new RCANNodeSyncError(
|
|
985
|
+
`Network error fetching ${url}: ${err.message}`,
|
|
986
|
+
url,
|
|
987
|
+
err instanceof Error ? err : void 0
|
|
988
|
+
);
|
|
989
|
+
} finally {
|
|
990
|
+
clearTimeout(timer);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
// ── Public API ───────────────────────────────────────────────────────────────
|
|
994
|
+
/**
|
|
995
|
+
* Fetch /.well-known/rcan-node.json from any node URL.
|
|
996
|
+
* Throws RCANNodeTrustError if the manifest is malformed.
|
|
997
|
+
* Throws RCANNodeSyncError on network failure.
|
|
998
|
+
*/
|
|
999
|
+
async getNodeManifest(nodeUrl) {
|
|
1000
|
+
const url = `${nodeUrl.replace(/\/$/, "")}${NODE_MANIFEST_PATH}`;
|
|
1001
|
+
const resp = await this._fetch(url);
|
|
1002
|
+
if (!resp.ok) {
|
|
1003
|
+
if (resp.status === 404) {
|
|
1004
|
+
throw new RCANNodeNotFoundError(url, nodeUrl);
|
|
1005
|
+
}
|
|
1006
|
+
throw new RCANNodeSyncError(
|
|
1007
|
+
`Failed to fetch node manifest from ${nodeUrl}: HTTP ${resp.status}`,
|
|
1008
|
+
nodeUrl
|
|
1009
|
+
);
|
|
1010
|
+
}
|
|
1011
|
+
let data;
|
|
1012
|
+
try {
|
|
1013
|
+
data = await resp.json();
|
|
1014
|
+
} catch (err) {
|
|
1015
|
+
throw new RCANNodeSyncError(
|
|
1016
|
+
`Invalid JSON in node manifest from ${nodeUrl}`,
|
|
1017
|
+
nodeUrl,
|
|
1018
|
+
err instanceof Error ? err : void 0
|
|
1019
|
+
);
|
|
1020
|
+
}
|
|
1021
|
+
if (!this.verifyNode(data)) {
|
|
1022
|
+
throw new RCANNodeTrustError("missing_pubkey", nodeUrl);
|
|
1023
|
+
}
|
|
1024
|
+
return data;
|
|
1025
|
+
}
|
|
1026
|
+
/**
|
|
1027
|
+
* Get list of known registry nodes from root /api/v1/nodes.
|
|
1028
|
+
* Optionally filter by namespace prefix (e.g. "BD").
|
|
1029
|
+
*/
|
|
1030
|
+
async listNodes(prefix) {
|
|
1031
|
+
const qs = prefix ? `?prefix=${encodeURIComponent(prefix)}` : "";
|
|
1032
|
+
const url = `${this.rootUrl}/api/v1/nodes${qs}`;
|
|
1033
|
+
const resp = await this._fetch(url);
|
|
1034
|
+
if (!resp.ok) {
|
|
1035
|
+
throw new RCANNodeSyncError(
|
|
1036
|
+
`Failed to list nodes from ${url}: HTTP ${resp.status}`,
|
|
1037
|
+
url
|
|
1038
|
+
);
|
|
1039
|
+
}
|
|
1040
|
+
let data;
|
|
1041
|
+
try {
|
|
1042
|
+
data = await resp.json();
|
|
1043
|
+
} catch (err) {
|
|
1044
|
+
throw new RCANNodeSyncError(
|
|
1045
|
+
`Invalid JSON in nodes list from ${url}`,
|
|
1046
|
+
url,
|
|
1047
|
+
err instanceof Error ? err : void 0
|
|
1048
|
+
);
|
|
1049
|
+
}
|
|
1050
|
+
if (Array.isArray(data)) return data;
|
|
1051
|
+
if (data && typeof data === "object" && "nodes" in data) {
|
|
1052
|
+
return data.nodes;
|
|
1053
|
+
}
|
|
1054
|
+
return [];
|
|
1055
|
+
}
|
|
1056
|
+
/**
|
|
1057
|
+
* Find the authoritative node for an RRN.
|
|
1058
|
+
* - Delegated RRN (RRN-BD-00000001): GET /api/v1/nodes?prefix=BD, return first match.
|
|
1059
|
+
* - Root RRN (RRN-00000042): return root node manifest.
|
|
1060
|
+
* - Unknown format: throws RCANNodeNotFoundError.
|
|
1061
|
+
*/
|
|
1062
|
+
async discover(rrn) {
|
|
1063
|
+
const parsed = parseRRNNamespace(rrn);
|
|
1064
|
+
if (!parsed) {
|
|
1065
|
+
throw new RCANNodeNotFoundError(rrn, this.rootUrl);
|
|
1066
|
+
}
|
|
1067
|
+
if (parsed.type === "root") {
|
|
1068
|
+
return this.getNodeManifest(this.rootUrl);
|
|
1069
|
+
}
|
|
1070
|
+
const nodes = await this.listNodes(parsed.prefix);
|
|
1071
|
+
if (nodes.length === 0) {
|
|
1072
|
+
throw new RCANNodeNotFoundError(rrn, this.rootUrl);
|
|
1073
|
+
}
|
|
1074
|
+
return nodes[0];
|
|
1075
|
+
}
|
|
1076
|
+
/**
|
|
1077
|
+
* Resolve an RRN across the federation.
|
|
1078
|
+
* First tries {rootUrl}/api/v1/resolve/{rrn}.
|
|
1079
|
+
* On 404, discovers the authoritative node and tries {node.api_base}/robots/{rrn}.
|
|
1080
|
+
*/
|
|
1081
|
+
async resolve(rrn) {
|
|
1082
|
+
const primaryUrl = `${this.rootUrl}/api/v1/resolve/${encodeURIComponent(rrn)}`;
|
|
1083
|
+
let resp;
|
|
1084
|
+
try {
|
|
1085
|
+
resp = await this._fetch(primaryUrl);
|
|
1086
|
+
} catch (err) {
|
|
1087
|
+
throw err;
|
|
1088
|
+
}
|
|
1089
|
+
if (resp.ok) {
|
|
1090
|
+
try {
|
|
1091
|
+
return await resp.json();
|
|
1092
|
+
} catch (err) {
|
|
1093
|
+
throw new RCANNodeSyncError(
|
|
1094
|
+
`Invalid JSON in resolve response for ${rrn}`,
|
|
1095
|
+
this.rootUrl,
|
|
1096
|
+
err instanceof Error ? err : void 0
|
|
1097
|
+
);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
if (resp.status !== 404) {
|
|
1101
|
+
throw new RCANNodeSyncError(
|
|
1102
|
+
`Unexpected HTTP ${resp.status} resolving ${rrn}`,
|
|
1103
|
+
this.rootUrl
|
|
1104
|
+
);
|
|
1105
|
+
}
|
|
1106
|
+
const node = await this.discover(rrn);
|
|
1107
|
+
const fallbackUrl = `${node.api_base.replace(/\/$/, "")}/robots/${encodeURIComponent(rrn)}`;
|
|
1108
|
+
const fallbackResp = await this._fetch(fallbackUrl);
|
|
1109
|
+
if (!fallbackResp.ok) {
|
|
1110
|
+
if (fallbackResp.status === 404) {
|
|
1111
|
+
throw new RCANNodeNotFoundError(rrn, node.api_base);
|
|
1112
|
+
}
|
|
1113
|
+
throw new RCANNodeSyncError(
|
|
1114
|
+
`HTTP ${fallbackResp.status} from authoritative node for ${rrn}`,
|
|
1115
|
+
node.api_base
|
|
1116
|
+
);
|
|
1117
|
+
}
|
|
1118
|
+
try {
|
|
1119
|
+
return await fallbackResp.json();
|
|
1120
|
+
} catch (err) {
|
|
1121
|
+
throw new RCANNodeSyncError(
|
|
1122
|
+
`Invalid JSON in fallback resolve response for ${rrn}`,
|
|
1123
|
+
node.api_base,
|
|
1124
|
+
err instanceof Error ? err : void 0
|
|
1125
|
+
);
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
/**
|
|
1129
|
+
* Verify a node manifest is well-formed.
|
|
1130
|
+
* Checks required fields, ed25519: public_key prefix, valid node_type, https:// api_base.
|
|
1131
|
+
*/
|
|
1132
|
+
verifyNode(manifest) {
|
|
1133
|
+
if (!manifest || typeof manifest !== "object") return false;
|
|
1134
|
+
const m = manifest;
|
|
1135
|
+
if (typeof m.rcan_node_version !== "string" || !m.rcan_node_version) return false;
|
|
1136
|
+
if (typeof m.node_type !== "string" || !VALID_NODE_TYPES.has(m.node_type)) return false;
|
|
1137
|
+
if (typeof m.operator !== "string" || !m.operator) return false;
|
|
1138
|
+
if (typeof m.namespace_prefix !== "string" || !m.namespace_prefix) return false;
|
|
1139
|
+
if (typeof m.public_key !== "string" || !m.public_key.startsWith("ed25519:")) return false;
|
|
1140
|
+
if (typeof m.api_base !== "string" || !m.api_base.startsWith("https://")) return false;
|
|
1141
|
+
return true;
|
|
1142
|
+
}
|
|
1143
|
+
};
|
|
1144
|
+
|
|
1145
|
+
// src/schema.ts
|
|
1146
|
+
var SCHEMA_BASE = "https://rcan.dev/schemas";
|
|
1147
|
+
var schemaCache = /* @__PURE__ */ new Map();
|
|
1148
|
+
async function fetchCanonicalSchema(schemaName) {
|
|
1149
|
+
if (schemaCache.has(schemaName)) return schemaCache.get(schemaName);
|
|
1150
|
+
try {
|
|
1151
|
+
const controller = new AbortController();
|
|
1152
|
+
const timer = setTimeout(() => controller.abort(), 5e3);
|
|
1153
|
+
const res = await fetch(`${SCHEMA_BASE}/${schemaName}`, { signal: controller.signal });
|
|
1154
|
+
clearTimeout(timer);
|
|
1155
|
+
if (!res.ok) return null;
|
|
1156
|
+
const schema = await res.json();
|
|
1157
|
+
schemaCache.set(schemaName, schema);
|
|
1158
|
+
return schema;
|
|
1159
|
+
} catch {
|
|
1160
|
+
return null;
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
async function validateConfigAgainstSchema(config) {
|
|
1164
|
+
const schema = await fetchCanonicalSchema("rcan-config.schema.json");
|
|
1165
|
+
if (!schema) return { valid: true, skipped: true };
|
|
1166
|
+
const errors = [];
|
|
1167
|
+
if (typeof config !== "object" || config === null) {
|
|
1168
|
+
return { valid: false, errors: ["Config must be an object"] };
|
|
1169
|
+
}
|
|
1170
|
+
const cfg = config;
|
|
1171
|
+
const required = schema.required ?? [];
|
|
1172
|
+
for (const key of required) {
|
|
1173
|
+
if (!(key in cfg)) errors.push(`Missing required field: ${key}`);
|
|
1174
|
+
}
|
|
1175
|
+
return errors.length === 0 ? { valid: true } : { valid: false, errors };
|
|
1176
|
+
}
|
|
1177
|
+
async function validateNodeAgainstSchema(manifest) {
|
|
1178
|
+
const schema = await fetchCanonicalSchema("rcan-node.schema.json");
|
|
1179
|
+
if (!schema) return { valid: true, skipped: true };
|
|
1180
|
+
const errors = [];
|
|
1181
|
+
if (typeof manifest !== "object" || manifest === null) {
|
|
1182
|
+
return { valid: false, errors: ["Manifest must be an object"] };
|
|
1183
|
+
}
|
|
1184
|
+
const m = manifest;
|
|
1185
|
+
const required = schema.required ?? [];
|
|
1186
|
+
for (const key of required) {
|
|
1187
|
+
if (!(key in m)) errors.push(`Missing required field: ${key}`);
|
|
1188
|
+
}
|
|
1189
|
+
return errors.length === 0 ? { valid: true } : { valid: false, errors };
|
|
1190
|
+
}
|
|
1191
|
+
|
|
896
1192
|
// src/index.ts
|
|
897
|
-
var VERSION = "0.
|
|
1193
|
+
var VERSION = "0.3.0";
|
|
1194
|
+
var SPEC_VERSION = "1.2";
|
|
898
1195
|
var RCAN_VERSION = "1.2";
|
|
899
1196
|
// Annotate the CommonJS export names for ESM import in node:
|
|
900
1197
|
0 && (module.exports = {
|
|
@@ -904,11 +1201,16 @@ var RCAN_VERSION = "1.2";
|
|
|
904
1201
|
ConfidenceGate,
|
|
905
1202
|
GateError,
|
|
906
1203
|
HiTLGate,
|
|
1204
|
+
NodeClient,
|
|
907
1205
|
RCANAddressError,
|
|
908
1206
|
RCANError,
|
|
909
1207
|
RCANGateError,
|
|
910
1208
|
RCANMessage,
|
|
911
1209
|
RCANMessageError,
|
|
1210
|
+
RCANNodeError,
|
|
1211
|
+
RCANNodeNotFoundError,
|
|
1212
|
+
RCANNodeSyncError,
|
|
1213
|
+
RCANNodeTrustError,
|
|
912
1214
|
RCANRegistryError,
|
|
913
1215
|
RCANSignatureError,
|
|
914
1216
|
RCANValidationError,
|
|
@@ -916,9 +1218,13 @@ var RCAN_VERSION = "1.2";
|
|
|
916
1218
|
RegistryClient,
|
|
917
1219
|
RobotURI,
|
|
918
1220
|
RobotURIError,
|
|
1221
|
+
SPEC_VERSION,
|
|
919
1222
|
VERSION,
|
|
1223
|
+
fetchCanonicalSchema,
|
|
920
1224
|
validateConfig,
|
|
1225
|
+
validateConfigAgainstSchema,
|
|
921
1226
|
validateMessage,
|
|
1227
|
+
validateNodeAgainstSchema,
|
|
922
1228
|
validateURI
|
|
923
1229
|
});
|
|
924
1230
|
//# sourceMappingURL=index.js.map
|