@continuonai/rcan-ts 0.2.0 → 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.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);
@@ -782,6 +791,39 @@ var RCANRegistryError = class extends RCANError {
782
791
  Object.setPrototypeOf(this, new.target.prototype);
783
792
  }
784
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
+ };
785
827
 
786
828
  // src/registry.ts
787
829
  var DEFAULT_BASE_URL = "https://rcan-spec.pages.dev";
@@ -906,8 +948,250 @@ var RegistryClient = class {
906
948
  }
907
949
  };
908
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
+
909
1192
  // src/index.ts
910
- var VERSION = "0.1.0";
1193
+ var VERSION = "0.3.0";
1194
+ var SPEC_VERSION = "1.2";
911
1195
  var RCAN_VERSION = "1.2";
912
1196
  // Annotate the CommonJS export names for ESM import in node:
913
1197
  0 && (module.exports = {
@@ -917,11 +1201,16 @@ var RCAN_VERSION = "1.2";
917
1201
  ConfidenceGate,
918
1202
  GateError,
919
1203
  HiTLGate,
1204
+ NodeClient,
920
1205
  RCANAddressError,
921
1206
  RCANError,
922
1207
  RCANGateError,
923
1208
  RCANMessage,
924
1209
  RCANMessageError,
1210
+ RCANNodeError,
1211
+ RCANNodeNotFoundError,
1212
+ RCANNodeSyncError,
1213
+ RCANNodeTrustError,
925
1214
  RCANRegistryError,
926
1215
  RCANSignatureError,
927
1216
  RCANValidationError,
@@ -929,9 +1218,13 @@ var RCAN_VERSION = "1.2";
929
1218
  RegistryClient,
930
1219
  RobotURI,
931
1220
  RobotURIError,
1221
+ SPEC_VERSION,
932
1222
  VERSION,
1223
+ fetchCanonicalSchema,
933
1224
  validateConfig,
1225
+ validateConfigAgainstSchema,
934
1226
  validateMessage,
1227
+ validateNodeAgainstSchema,
935
1228
  validateURI
936
1229
  });
937
1230
  //# sourceMappingURL=index.js.map