@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/README.md +85 -0
- package/bin/rcan-validate.mjs +86 -0
- package/dist/browser.d.mts +107 -2
- package/dist/browser.mjs +285 -1
- package/dist/browser.mjs.map +1 -1
- package/dist/index.d.mts +107 -2
- package/dist/index.d.ts +107 -2
- package/dist/index.js +294 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +285 -1
- package/dist/index.mjs.map +1 -1
- package/dist/rcan.iife.js +2 -2
- package/package.json +3 -3
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.
|
|
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
|