@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/dist/index.d.ts CHANGED
@@ -201,48 +201,101 @@ declare class AuditChain {
201
201
  }
202
202
 
203
203
  /**
204
- * RCAN Validationvalidate messages, configs, and URIs.
204
+ * rcan-ts typesTypeScript interfaces for RCAN config and message shapes.
205
205
  *
206
- * Each function returns a ValidationResult with ok, issues, warnings, info.
206
+ * These provide parity with the rcan-py TypedDicts and enable
207
+ * static type checking with tsc/ts-jest/pyright.
207
208
  */
208
- interface ValidationResult {
209
- ok: boolean;
210
- issues: string[];
211
- warnings: string[];
212
- info: string[];
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
- declare function validateURI(uri: string): ValidationResult;
215
- declare function validateMessage(data: unknown): ValidationResult;
216
- interface RCANConfig {
217
- rcan_version?: string;
218
- metadata?: {
219
- manufacturer?: string;
220
- model?: string;
221
- version?: string;
222
- rrn?: string;
223
- rcan_uri?: string;
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
- agent?: {
226
- provider?: string;
227
- model?: string;
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.1.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 (!config.rcan_version) warn(result, "L1: rcan_version not declared (recommended)");
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.1.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