@continuonai/rcan-ts 0.5.0 → 0.8.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.mjs CHANGED
@@ -96,7 +96,8 @@ var RobotURI = class _RobotURI {
96
96
  };
97
97
 
98
98
  // src/version.ts
99
- var SPEC_VERSION = "1.5";
99
+ var SPEC_VERSION = "1.9.0";
100
+ var SDK_VERSION = "0.8.0";
100
101
  function validateVersionCompat(incomingVersion, localVersion = SPEC_VERSION) {
101
102
  const parseParts = (v) => {
102
103
  const parts = v.split(".");
@@ -117,18 +118,18 @@ var MessageType = /* @__PURE__ */ ((MessageType2) => {
117
118
  MessageType2[MessageType2["HEARTBEAT"] = 4] = "HEARTBEAT";
118
119
  MessageType2[MessageType2["CONFIG"] = 5] = "CONFIG";
119
120
  MessageType2[MessageType2["SAFETY"] = 6] = "SAFETY";
120
- MessageType2[MessageType2["SENSOR_DATA"] = 7] = "SENSOR_DATA";
121
- MessageType2[MessageType2["AUDIT"] = 8] = "AUDIT";
121
+ MessageType2[MessageType2["AUTH"] = 7] = "AUTH";
122
+ MessageType2[MessageType2["ERROR"] = 8] = "ERROR";
122
123
  MessageType2[MessageType2["DISCOVER"] = 9] = "DISCOVER";
123
- MessageType2[MessageType2["TRAINING_DATA"] = 10] = "TRAINING_DATA";
124
- MessageType2[MessageType2["TRANSPARENCY"] = 11] = "TRANSPARENCY";
125
- MessageType2[MessageType2["FEDERATION_SYNC"] = 12] = "FEDERATION_SYNC";
126
- MessageType2[MessageType2["ALERT"] = 13] = "ALERT";
127
- MessageType2[MessageType2["TELEOP"] = 14] = "TELEOP";
128
- MessageType2[MessageType2["CHAT"] = 15] = "CHAT";
129
- MessageType2[MessageType2["ERROR"] = 16] = "ERROR";
124
+ MessageType2[MessageType2["PENDING_AUTH"] = 10] = "PENDING_AUTH";
125
+ MessageType2[MessageType2["INVOKE"] = 11] = "INVOKE";
126
+ MessageType2[MessageType2["INVOKE_RESULT"] = 12] = "INVOKE_RESULT";
127
+ MessageType2[MessageType2["INVOKE_CANCEL"] = 13] = "INVOKE_CANCEL";
128
+ MessageType2[MessageType2["REGISTRY_REGISTER"] = 14] = "REGISTRY_REGISTER";
129
+ MessageType2[MessageType2["REGISTRY_RESOLVE"] = 15] = "REGISTRY_RESOLVE";
130
+ MessageType2[MessageType2["TRANSPARENCY"] = 16] = "TRANSPARENCY";
130
131
  MessageType2[MessageType2["COMMAND_ACK"] = 17] = "COMMAND_ACK";
131
- MessageType2[MessageType2["COMMAND_COMMIT"] = 18] = "COMMAND_COMMIT";
132
+ MessageType2[MessageType2["COMMAND_NACK"] = 18] = "COMMAND_NACK";
132
133
  MessageType2[MessageType2["ROBOT_REVOCATION"] = 19] = "ROBOT_REVOCATION";
133
134
  MessageType2[MessageType2["CONSENT_REQUEST"] = 20] = "CONSENT_REQUEST";
134
135
  MessageType2[MessageType2["CONSENT_GRANT"] = 21] = "CONSENT_GRANT";
@@ -137,7 +138,19 @@ var MessageType = /* @__PURE__ */ ((MessageType2) => {
137
138
  MessageType2[MessageType2["SUBSCRIBE"] = 24] = "SUBSCRIBE";
138
139
  MessageType2[MessageType2["UNSUBSCRIBE"] = 25] = "UNSUBSCRIBE";
139
140
  MessageType2[MessageType2["FAULT_REPORT"] = 26] = "FAULT_REPORT";
140
- MessageType2[MessageType2["COMMAND_NACK"] = 27] = "COMMAND_NACK";
141
+ MessageType2[MessageType2["KEY_ROTATION"] = 27] = "KEY_ROTATION";
142
+ MessageType2[MessageType2["COMMAND_COMMIT"] = 28] = "COMMAND_COMMIT";
143
+ MessageType2[MessageType2["SENSOR_DATA"] = 29] = "SENSOR_DATA";
144
+ MessageType2[MessageType2["TRAINING_CONSENT_REQUEST"] = 30] = "TRAINING_CONSENT_REQUEST";
145
+ MessageType2[MessageType2["TRAINING_CONSENT_GRANT"] = 31] = "TRAINING_CONSENT_GRANT";
146
+ MessageType2[MessageType2["TRAINING_CONSENT_DENY"] = 32] = "TRAINING_CONSENT_DENY";
147
+ MessageType2[MessageType2["CONTRIBUTE_REQUEST"] = 33] = "CONTRIBUTE_REQUEST";
148
+ MessageType2[MessageType2["CONTRIBUTE_RESULT"] = 34] = "CONTRIBUTE_RESULT";
149
+ MessageType2[MessageType2["CONTRIBUTE_CANCEL"] = 35] = "CONTRIBUTE_CANCEL";
150
+ MessageType2[MessageType2["TRAINING_DATA"] = 36] = "TRAINING_DATA";
151
+ MessageType2[MessageType2["FEDERATION_SYNC"] = 23] = "FEDERATION_SYNC";
152
+ MessageType2[MessageType2["ALERT"] = 26] = "ALERT";
153
+ MessageType2[MessageType2["AUDIT"] = 16] = "AUDIT";
141
154
  return MessageType2;
142
155
  })(MessageType || {});
143
156
  var RCANMessageError = class extends Error {
@@ -167,6 +180,12 @@ var RCANMessage = class _RCANMessage {
167
180
  presenceVerified;
168
181
  proximityMeters;
169
182
  readOnly;
183
+ /** v1.6: GAP-14 level of assurance */
184
+ loa;
185
+ /** v1.6: GAP-17 transport encoding hint */
186
+ transportEncoding;
187
+ /** v1.6: GAP-18 multi-modal media chunks */
188
+ mediaChunks;
170
189
  constructor(data) {
171
190
  if (!data.cmd || data.cmd.trim() === "") {
172
191
  throw new RCANMessageError("'cmd' is required");
@@ -192,6 +211,9 @@ var RCANMessage = class _RCANMessage {
192
211
  this.presenceVerified = data.presenceVerified;
193
212
  this.proximityMeters = data.proximityMeters;
194
213
  this.readOnly = data.readOnly;
214
+ this.loa = data.loa;
215
+ this.transportEncoding = data.transportEncoding;
216
+ this.mediaChunks = data.mediaChunks;
195
217
  if (this.confidence !== void 0) {
196
218
  if (this.confidence < 0 || this.confidence > 1) {
197
219
  throw new RCANMessageError(
@@ -230,6 +252,9 @@ var RCANMessage = class _RCANMessage {
230
252
  if (this.presenceVerified !== void 0) obj.presenceVerified = this.presenceVerified;
231
253
  if (this.proximityMeters !== void 0) obj.proximityMeters = this.proximityMeters;
232
254
  if (this.readOnly !== void 0) obj.readOnly = this.readOnly;
255
+ if (this.loa !== void 0) obj.loa = this.loa;
256
+ if (this.transportEncoding !== void 0) obj.transportEncoding = this.transportEncoding;
257
+ if (this.mediaChunks !== void 0) obj.mediaChunks = this.mediaChunks;
233
258
  return obj;
234
259
  }
235
260
  /** Serialize to JSON string */
@@ -269,7 +294,10 @@ var RCANMessage = class _RCANMessage {
269
294
  qos: obj.qos,
270
295
  presenceVerified: obj.presenceVerified,
271
296
  proximityMeters: obj.proximityMeters,
272
- readOnly: obj.readOnly
297
+ readOnly: obj.readOnly,
298
+ loa: obj.loa,
299
+ transportEncoding: obj.transportEncoding,
300
+ mediaChunks: obj.mediaChunks
273
301
  });
274
302
  }
275
303
  };
@@ -1242,6 +1270,7 @@ async function fetchCanonicalSchema(schemaName) {
1242
1270
  try {
1243
1271
  const controller = new AbortController();
1244
1272
  const timer = setTimeout(() => controller.abort(), 5e3);
1273
+ timer.unref?.();
1245
1274
  const res = await fetch(`${SCHEMA_BASE}/${schemaName}`, { signal: controller.signal });
1246
1275
  clearTimeout(timer);
1247
1276
  if (!res.ok) return null;
@@ -1882,7 +1911,7 @@ function makeTrainingConsentDeny(params) {
1882
1911
  return makeConsentDeny(params);
1883
1912
  }
1884
1913
  function validateTrainingDataMessage(msg) {
1885
- if (msg.params.message_type !== 10 /* TRAINING_DATA */) {
1914
+ if (msg.params.message_type !== 36 /* TRAINING_DATA */) {
1886
1915
  return { valid: false, reason: "not a TRAINING_DATA message" };
1887
1916
  }
1888
1917
  const token = msg.params.consent_token;
@@ -2021,23 +2050,708 @@ function makeFaultReport(params) {
2021
2050
  });
2022
2051
  }
2023
2052
 
2053
+ // src/identity.ts
2054
+ var LevelOfAssurance = /* @__PURE__ */ ((LevelOfAssurance2) => {
2055
+ LevelOfAssurance2[LevelOfAssurance2["ANONYMOUS"] = 1] = "ANONYMOUS";
2056
+ LevelOfAssurance2[LevelOfAssurance2["EMAIL_VERIFIED"] = 2] = "EMAIL_VERIFIED";
2057
+ LevelOfAssurance2[LevelOfAssurance2["HARDWARE_TOKEN"] = 3] = "HARDWARE_TOKEN";
2058
+ return LevelOfAssurance2;
2059
+ })(LevelOfAssurance || {});
2060
+ var DEFAULT_LOA_POLICY = {
2061
+ minLoaDiscover: 1 /* ANONYMOUS */,
2062
+ minLoaStatus: 1 /* ANONYMOUS */,
2063
+ minLoaChat: 1 /* ANONYMOUS */,
2064
+ minLoaControl: 1 /* ANONYMOUS */,
2065
+ minLoaSafety: 1 /* ANONYMOUS */
2066
+ };
2067
+ var PRODUCTION_LOA_POLICY = {
2068
+ minLoaDiscover: 1 /* ANONYMOUS */,
2069
+ minLoaStatus: 1 /* ANONYMOUS */,
2070
+ minLoaChat: 1 /* ANONYMOUS */,
2071
+ minLoaControl: 2 /* EMAIL_VERIFIED */,
2072
+ minLoaSafety: 3 /* HARDWARE_TOKEN */
2073
+ };
2074
+ function extractLoaFromJwt(token) {
2075
+ try {
2076
+ const parts = token.split(".");
2077
+ if (parts.length < 2) return 1 /* ANONYMOUS */;
2078
+ const payloadB64 = (parts[1] ?? "").replace(/-/g, "+").replace(/_/g, "/");
2079
+ const padded = payloadB64 + "=".repeat((4 - payloadB64.length % 4) % 4);
2080
+ let json;
2081
+ if (typeof atob !== "undefined") {
2082
+ json = atob(padded);
2083
+ } else {
2084
+ json = Buffer.from(padded, "base64").toString("utf-8");
2085
+ }
2086
+ const claims = JSON.parse(json);
2087
+ const loa = claims["loa"];
2088
+ if (typeof loa === "number" && loa >= 1 && loa <= 3) {
2089
+ return loa;
2090
+ }
2091
+ } catch {
2092
+ }
2093
+ return 1 /* ANONYMOUS */;
2094
+ }
2095
+ function minLoaForScope(scope, policy) {
2096
+ const s = scope.toLowerCase();
2097
+ switch (s) {
2098
+ case "discover":
2099
+ return policy.minLoaDiscover;
2100
+ case "status":
2101
+ return policy.minLoaStatus;
2102
+ case "chat":
2103
+ return policy.minLoaChat;
2104
+ case "contribute":
2105
+ return policy.minLoaChat;
2106
+ // v1.7: between chat and control
2107
+ case "control":
2108
+ return policy.minLoaControl;
2109
+ case "safety":
2110
+ return policy.minLoaSafety;
2111
+ default:
2112
+ return null;
2113
+ }
2114
+ }
2115
+ function validateLoaForScope(loa, scope, policy = DEFAULT_LOA_POLICY) {
2116
+ const min = minLoaForScope(scope, policy);
2117
+ if (min === null) {
2118
+ return { valid: true, reason: "unknown scope; allowed by default" };
2119
+ }
2120
+ if (loa >= min) {
2121
+ return { valid: true, reason: "ok" };
2122
+ }
2123
+ return {
2124
+ valid: false,
2125
+ reason: `LOA_INSUFFICIENT: scope=${scope} requires LoA>=${min}, caller has LoA=${loa}`
2126
+ };
2127
+ }
2128
+
2129
+ // src/federation.ts
2130
+ var RegistryTier = /* @__PURE__ */ ((RegistryTier2) => {
2131
+ RegistryTier2["ROOT"] = "root";
2132
+ RegistryTier2["AUTHORITATIVE"] = "authoritative";
2133
+ RegistryTier2["COMMUNITY"] = "community";
2134
+ return RegistryTier2;
2135
+ })(RegistryTier || {});
2136
+ var FederationSyncType = /* @__PURE__ */ ((FederationSyncType2) => {
2137
+ FederationSyncType2["CONSENT"] = "consent";
2138
+ FederationSyncType2["REVOCATION"] = "revocation";
2139
+ FederationSyncType2["KEY"] = "key";
2140
+ return FederationSyncType2;
2141
+ })(FederationSyncType || {});
2142
+ var CACHE_TTL_MS2 = 24 * 60 * 60 * 1e3;
2143
+ var TrustAnchorCache = class {
2144
+ store = /* @__PURE__ */ new Map();
2145
+ /** Store or refresh a registry identity. */
2146
+ set(identity) {
2147
+ this.store.set(identity.registryUrl, {
2148
+ identity,
2149
+ expiresAt: Date.now() + CACHE_TTL_MS2
2150
+ });
2151
+ }
2152
+ /**
2153
+ * Look up a registry URL.
2154
+ * Returns undefined when absent or when the TTL has expired.
2155
+ */
2156
+ lookup(url) {
2157
+ const entry = this.store.get(url);
2158
+ if (!entry) return void 0;
2159
+ if (Date.now() > entry.expiresAt) {
2160
+ this.store.delete(url);
2161
+ return void 0;
2162
+ }
2163
+ return entry.identity;
2164
+ }
2165
+ /**
2166
+ * Discover a registry via DNS TXT record `_rcan-registry.<domain>`.
2167
+ *
2168
+ * The TXT record is expected to contain a JSON object with the
2169
+ * RegistryIdentity fields. Returns the identity and caches it.
2170
+ *
2171
+ * Node.js only — returns undefined in environments without `dns.promises`.
2172
+ */
2173
+ async discoverViaDns(domain) {
2174
+ const hostname = `_rcan-registry.${domain}`;
2175
+ let records;
2176
+ try {
2177
+ const dnsModule = __require("dns");
2178
+ records = await dnsModule.promises.resolveTxt(hostname);
2179
+ } catch {
2180
+ return void 0;
2181
+ }
2182
+ for (const record of records) {
2183
+ const text = record.join("");
2184
+ try {
2185
+ const parsed = JSON.parse(text);
2186
+ if (parsed.registryUrl && parsed.tier && parsed.publicKeyPem && parsed.domain) {
2187
+ const identity = {
2188
+ registryUrl: parsed.registryUrl,
2189
+ tier: parsed.tier,
2190
+ publicKeyPem: parsed.publicKeyPem,
2191
+ domain: parsed.domain,
2192
+ verifiedAt: (/* @__PURE__ */ new Date()).toISOString()
2193
+ };
2194
+ this.set(identity);
2195
+ return identity;
2196
+ }
2197
+ } catch {
2198
+ }
2199
+ }
2200
+ return void 0;
2201
+ }
2202
+ /**
2203
+ * Verify a JWT was issued by the registry at `url`.
2204
+ *
2205
+ * Validates the `iss` claim and checks the registry is in the trust cache.
2206
+ * Full cryptographic signature verification requires the registry's public
2207
+ * key material — callers should perform additional checks using `publicKeyPem`
2208
+ * from the returned identity.
2209
+ */
2210
+ async verifyRegistryJwt(token, url) {
2211
+ const identity = this.lookup(url);
2212
+ if (!identity) {
2213
+ throw new Error(`REGISTRY_UNKNOWN: ${url} is not in the trust cache`);
2214
+ }
2215
+ let iss;
2216
+ try {
2217
+ const parts = token.split(".");
2218
+ const payloadB64 = (parts[1] ?? "").replace(/-/g, "+").replace(/_/g, "/");
2219
+ const padded = payloadB64 + "=".repeat((4 - payloadB64.length % 4) % 4);
2220
+ let json;
2221
+ if (typeof atob !== "undefined") {
2222
+ json = atob(padded);
2223
+ } else {
2224
+ json = Buffer.from(padded, "base64").toString("utf-8");
2225
+ }
2226
+ const claims = JSON.parse(json);
2227
+ iss = typeof claims["iss"] === "string" ? claims["iss"] : void 0;
2228
+ } catch {
2229
+ throw new Error("REGISTRY_JWT_MALFORMED: cannot decode token payload");
2230
+ }
2231
+ if (iss !== url) {
2232
+ throw new Error(
2233
+ `REGISTRY_JWT_ISS_MISMATCH: expected iss=${url}, got iss=${iss ?? "(none)"}`
2234
+ );
2235
+ }
2236
+ return identity;
2237
+ }
2238
+ };
2239
+ function generateId5() {
2240
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
2241
+ return crypto.randomUUID();
2242
+ }
2243
+ const bytes = Array.from({ length: 16 }, () => Math.floor(Math.random() * 256));
2244
+ bytes[6] = (bytes[6] ?? 0) & 15 | 64;
2245
+ bytes[8] = (bytes[8] ?? 0) & 63 | 128;
2246
+ const hex = bytes.map((b) => b.toString(16).padStart(2, "0"));
2247
+ return `${hex.slice(0, 4).join("")}-${hex.slice(4, 6).join("")}-${hex.slice(6, 8).join("")}-${hex.slice(8, 10).join("")}-${hex.slice(10).join("")}`;
2248
+ }
2249
+ function makeFederationSync(source, target, syncType, payload) {
2250
+ return new RCANMessage({
2251
+ rcan: "1.6",
2252
+ rcanVersion: "1.6",
2253
+ cmd: "federation_sync",
2254
+ target,
2255
+ params: {
2256
+ msg_type: 23 /* FEDERATION_SYNC */,
2257
+ msg_id: generateId5(),
2258
+ source_registry: source,
2259
+ target_registry: target,
2260
+ sync_type: syncType,
2261
+ payload
2262
+ },
2263
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2264
+ });
2265
+ }
2266
+ async function validateCrossRegistryCommand(msg, localRegistry, trustCache) {
2267
+ const msgType = msg.params?.["msg_type"];
2268
+ const isEstop = msgType === 6 /* SAFETY */ || msgType === 6 || msg.cmd === "estop" || msg.cmd === "ESTOP";
2269
+ if (isEstop) {
2270
+ return { valid: true, reason: "ESTOP always permitted (P66 invariant)" };
2271
+ }
2272
+ const sourceRegistry = msg.params?.["source_registry"] ?? msg.params?.["from_registry"];
2273
+ if (!sourceRegistry || sourceRegistry === localRegistry) {
2274
+ return { valid: true, reason: "local registry; no federation check needed" };
2275
+ }
2276
+ const identity = trustCache.lookup(sourceRegistry);
2277
+ if (!identity) {
2278
+ return {
2279
+ valid: false,
2280
+ reason: `REGISTRY_UNKNOWN: ${sourceRegistry} is not in the local trust cache`
2281
+ };
2282
+ }
2283
+ let loa = 1 /* ANONYMOUS */;
2284
+ const registryJwt = msg.params?.["registry_jwt"];
2285
+ if (registryJwt) {
2286
+ loa = extractLoaFromJwt(registryJwt);
2287
+ } else if (typeof msg.loa === "number") {
2288
+ loa = msg.loa;
2289
+ }
2290
+ if (loa < 2 /* EMAIL_VERIFIED */) {
2291
+ return {
2292
+ valid: false,
2293
+ reason: `LOA_INSUFFICIENT: cross-registry commands require LoA>=2 (EMAIL_VERIFIED), got LoA=${loa}`
2294
+ };
2295
+ }
2296
+ return { valid: true, reason: "cross-registry command accepted" };
2297
+ }
2298
+
2299
+ // src/transport.ts
2300
+ var TransportError = class _TransportError extends Error {
2301
+ constructor(message) {
2302
+ super(message);
2303
+ this.name = "TransportError";
2304
+ Object.setPrototypeOf(this, _TransportError.prototype);
2305
+ }
2306
+ };
2307
+ var TransportEncoding = /* @__PURE__ */ ((TransportEncoding2) => {
2308
+ TransportEncoding2["HTTP"] = "http";
2309
+ TransportEncoding2["COMPACT"] = "compact";
2310
+ TransportEncoding2["MINIMAL"] = "minimal";
2311
+ TransportEncoding2["BLE"] = "ble";
2312
+ return TransportEncoding2;
2313
+ })(TransportEncoding || {});
2314
+ var COMPACT_ENCODE = {
2315
+ msg_type: "t",
2316
+ msg_id: "i",
2317
+ timestamp: "ts",
2318
+ from_rrn: "f",
2319
+ to_rrn: "to",
2320
+ scope: "s",
2321
+ payload: "p",
2322
+ signature: "sig"
2323
+ };
2324
+ var COMPACT_DECODE = Object.fromEntries(
2325
+ Object.entries(COMPACT_ENCODE).map(([k, v]) => [v, k])
2326
+ );
2327
+ function encodeCompact(message) {
2328
+ const full = message.toJSON();
2329
+ const compact = {};
2330
+ for (const [key, value] of Object.entries(full)) {
2331
+ const short = COMPACT_ENCODE[key];
2332
+ compact[short ?? key] = value;
2333
+ }
2334
+ if (compact["p"] && typeof compact["p"] === "object") {
2335
+ const params = compact["p"];
2336
+ const compactParams = {};
2337
+ for (const [k, v] of Object.entries(params)) {
2338
+ const short = COMPACT_ENCODE[k];
2339
+ compactParams[short ?? k] = v;
2340
+ }
2341
+ compact["p"] = compactParams;
2342
+ }
2343
+ const json = JSON.stringify(compact);
2344
+ const encoder = new TextEncoder();
2345
+ return encoder.encode(json);
2346
+ }
2347
+ function decodeCompact(data) {
2348
+ const decoder = new TextDecoder();
2349
+ const json = decoder.decode(data);
2350
+ const compact = JSON.parse(json);
2351
+ const full = {};
2352
+ for (const [key, value] of Object.entries(compact)) {
2353
+ const long = COMPACT_DECODE[key];
2354
+ full[long ?? key] = value;
2355
+ }
2356
+ if (full["payload"] && typeof full["payload"] === "object") {
2357
+ const params = full["payload"];
2358
+ const expandedParams = {};
2359
+ for (const [k, v] of Object.entries(params)) {
2360
+ const long = COMPACT_DECODE[k];
2361
+ expandedParams[long ?? k] = v;
2362
+ }
2363
+ full["payload"] = expandedParams;
2364
+ }
2365
+ return new RCANMessage({
2366
+ rcan: full["rcan"] ?? "1.6",
2367
+ rcanVersion: full["rcanVersion"],
2368
+ cmd: full["cmd"],
2369
+ target: full["target"],
2370
+ params: full["params"] ?? full["payload"] ?? {},
2371
+ timestamp: full["timestamp"],
2372
+ confidence: full["confidence"],
2373
+ signature: full["signature"]
2374
+ });
2375
+ }
2376
+ var MINIMAL_SIZE = 32;
2377
+ var SAFETY_TYPE = 6;
2378
+ async function sha256Bytes(input) {
2379
+ const encoded = new TextEncoder().encode(input);
2380
+ const ab = new ArrayBuffer(encoded.byteLength);
2381
+ new Uint8Array(ab).set(encoded);
2382
+ const subtle = globalThis.crypto?.subtle ?? (await import("crypto")).webcrypto.subtle;
2383
+ const hashBuffer = await subtle.digest("SHA-256", ab);
2384
+ return new Uint8Array(hashBuffer);
2385
+ }
2386
+ async function encodeMinimal(message) {
2387
+ const msgType = message.params?.["msg_type"] ?? 0;
2388
+ if (msgType !== SAFETY_TYPE) {
2389
+ throw new TransportError(
2390
+ `encodeMinimal only supports SAFETY (type 6) messages; got type=${msgType}`
2391
+ );
2392
+ }
2393
+ const fromRrn = message.params?.["from_rrn"] ?? message.target ?? "";
2394
+ const toRrn = message.params?.["to_rrn"] ?? message.target ?? "";
2395
+ const fromHash = await sha256Bytes(fromRrn);
2396
+ const toHash = await sha256Bytes(toRrn);
2397
+ const sig = (message.signature?.sig ?? "").replace(/[^A-Za-z0-9+/=]/g, "");
2398
+ let sigBytes;
2399
+ try {
2400
+ if (typeof atob !== "undefined") {
2401
+ const raw = atob(sig.slice(0, 16));
2402
+ sigBytes = new Uint8Array(raw.length);
2403
+ for (let i = 0; i < raw.length; i++) sigBytes[i] = raw.charCodeAt(i);
2404
+ } else {
2405
+ sigBytes = Buffer.from(sig.slice(0, 16), "base64");
2406
+ }
2407
+ } catch {
2408
+ sigBytes = new Uint8Array(8);
2409
+ }
2410
+ const ts = message.timestamp ? Math.floor(new Date(message.timestamp).getTime() / 1e3) : Math.floor(Date.now() / 1e3);
2411
+ const unix32 = ts >>> 0;
2412
+ const out = new Uint8Array(MINIMAL_SIZE);
2413
+ const view = new DataView(out.buffer);
2414
+ view.setUint16(0, SAFETY_TYPE, false);
2415
+ out.set(fromHash.subarray(0, 8), 2);
2416
+ out.set(toHash.subarray(0, 8), 10);
2417
+ view.setUint32(18, unix32, false);
2418
+ const sigSlice = new Uint8Array(8);
2419
+ sigSlice.set(sigBytes.subarray(0, Math.min(8, sigBytes.length)));
2420
+ out.set(sigSlice, 22);
2421
+ let checksum = 0;
2422
+ for (let i = 0; i < 30; i++) checksum ^= out[i] ?? 0;
2423
+ view.setUint16(30, checksum & 65535, false);
2424
+ if (out.length !== MINIMAL_SIZE) {
2425
+ throw new TransportError(
2426
+ `encodeMinimal assertion failed: expected ${MINIMAL_SIZE} bytes, got ${out.length}`
2427
+ );
2428
+ }
2429
+ return out;
2430
+ }
2431
+ function decodeMinimal(data) {
2432
+ if (data.length !== MINIMAL_SIZE) {
2433
+ throw new TransportError(
2434
+ `decodeMinimal: expected ${MINIMAL_SIZE} bytes, got ${data.length}`
2435
+ );
2436
+ }
2437
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
2438
+ const msgType = view.getUint16(0, false);
2439
+ const fromHash = data.subarray(2, 10);
2440
+ const toHash = data.subarray(10, 18);
2441
+ const unix32 = view.getUint32(18, false);
2442
+ const sigTrunc = data.subarray(22, 30);
2443
+ const timestamp = new Date(unix32 * 1e3).toISOString();
2444
+ const toHex2 = (b) => Array.from(b).map((x) => x.toString(16).padStart(2, "0")).join("");
2445
+ return {
2446
+ params: {
2447
+ msg_type: msgType,
2448
+ from_hash: toHex2(fromHash),
2449
+ to_hash: toHex2(toHash),
2450
+ timestamp_s: unix32,
2451
+ sig_truncated: toHex2(sigTrunc)
2452
+ },
2453
+ timestamp
2454
+ };
2455
+ }
2456
+ var DEFAULT_MTU = 251;
2457
+ var BLE_HEADER_SIZE = 3;
2458
+ function encodeBleFrames(message, mtu = DEFAULT_MTU) {
2459
+ const payload = encodeCompact(message);
2460
+ const chunkSize = mtu - BLE_HEADER_SIZE;
2461
+ if (chunkSize <= 0) {
2462
+ throw new TransportError(`MTU ${mtu} is too small (need at least ${BLE_HEADER_SIZE + 1})`);
2463
+ }
2464
+ const totalChunks = Math.ceil(payload.length / chunkSize);
2465
+ const frames = [];
2466
+ for (let i = 0; i < totalChunks; i++) {
2467
+ const chunk = payload.subarray(i * chunkSize, (i + 1) * chunkSize);
2468
+ const frame = new Uint8Array(BLE_HEADER_SIZE + chunk.length);
2469
+ frame[0] = i;
2470
+ frame[1] = totalChunks;
2471
+ frame[2] = i === totalChunks - 1 ? 1 : 0;
2472
+ frame.set(chunk, BLE_HEADER_SIZE);
2473
+ frames.push(frame);
2474
+ }
2475
+ return frames;
2476
+ }
2477
+ function decodeBleFrames(frames) {
2478
+ if (frames.length === 0) {
2479
+ throw new TransportError("decodeBleFrames: no frames provided");
2480
+ }
2481
+ const sorted = [...frames].sort((a, b) => (a[0] ?? 0) - (b[0] ?? 0));
2482
+ const expectedTotal = sorted[0]?.[1] ?? sorted.length;
2483
+ if (sorted.length !== expectedTotal) {
2484
+ throw new TransportError(
2485
+ `decodeBleFrames: expected ${expectedTotal} frames, got ${sorted.length}`
2486
+ );
2487
+ }
2488
+ const payloadChunks = sorted.map((f) => f.subarray(BLE_HEADER_SIZE));
2489
+ const totalLen = payloadChunks.reduce((s, c) => s + c.length, 0);
2490
+ const payload = new Uint8Array(totalLen);
2491
+ let offset = 0;
2492
+ for (const chunk of payloadChunks) {
2493
+ payload.set(chunk, offset);
2494
+ offset += chunk.length;
2495
+ }
2496
+ return decodeCompact(payload);
2497
+ }
2498
+ function selectTransport(available, message) {
2499
+ const msgType = message.params?.["msg_type"] ?? 0;
2500
+ const isSafety = msgType === SAFETY_TYPE;
2501
+ const has = (enc) => available.includes(enc);
2502
+ if (isSafety) {
2503
+ if (has("minimal" /* MINIMAL */)) return "minimal" /* MINIMAL */;
2504
+ if (has("ble" /* BLE */)) return "ble" /* BLE */;
2505
+ if (has("compact" /* COMPACT */)) return "compact" /* COMPACT */;
2506
+ if (has("http" /* HTTP */)) return "http" /* HTTP */;
2507
+ } else {
2508
+ if (has("http" /* HTTP */)) return "http" /* HTTP */;
2509
+ if (has("compact" /* COMPACT */)) return "compact" /* COMPACT */;
2510
+ if (has("ble" /* BLE */)) return "ble" /* BLE */;
2511
+ }
2512
+ throw new TransportError(
2513
+ `No suitable transport available from: [${available.join(", ")}]`
2514
+ );
2515
+ }
2516
+
2517
+ // src/multimodal.ts
2518
+ var MediaEncoding = /* @__PURE__ */ ((MediaEncoding2) => {
2519
+ MediaEncoding2["BASE64"] = "base64";
2520
+ MediaEncoding2["REF"] = "ref";
2521
+ return MediaEncoding2;
2522
+ })(MediaEncoding || {});
2523
+ function generateId6() {
2524
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
2525
+ return crypto.randomUUID();
2526
+ }
2527
+ const bytes = Array.from({ length: 16 }, () => Math.floor(Math.random() * 256));
2528
+ bytes[6] = (bytes[6] ?? 0) & 15 | 64;
2529
+ bytes[8] = (bytes[8] ?? 0) & 63 | 128;
2530
+ const hex = bytes.map((b) => b.toString(16).padStart(2, "0"));
2531
+ return `${hex.slice(0, 4).join("")}-${hex.slice(4, 6).join("")}-${hex.slice(6, 8).join("")}-${hex.slice(8, 10).join("")}-${hex.slice(10).join("")}`;
2532
+ }
2533
+ async function computeSha256Hex(data) {
2534
+ const subtle = globalThis.crypto?.subtle ?? (await import("crypto")).webcrypto.subtle;
2535
+ const ab = new ArrayBuffer(data.byteLength);
2536
+ new Uint8Array(ab).set(data);
2537
+ const hashBuffer = await subtle.digest("SHA-256", ab);
2538
+ const hashArray = new Uint8Array(hashBuffer);
2539
+ return Array.from(hashArray).map((b) => b.toString(16).padStart(2, "0")).join("");
2540
+ }
2541
+ function uint8ToBase64(data) {
2542
+ if (typeof Buffer !== "undefined") {
2543
+ return Buffer.from(data).toString("base64");
2544
+ }
2545
+ let binary = "";
2546
+ for (let i = 0; i < data.length; i++) binary += String.fromCharCode(data[i] ?? 0);
2547
+ return btoa(binary);
2548
+ }
2549
+ function rebuildMessage(base, overrides) {
2550
+ const data = { ...base.toJSON(), ...overrides };
2551
+ return RCANMessage.fromJSON(data);
2552
+ }
2553
+ async function addMediaInline(message, data, mimeType) {
2554
+ const hashSha256 = await computeSha256Hex(data);
2555
+ const dataB64 = uint8ToBase64(data);
2556
+ const chunk = {
2557
+ chunkId: generateId6(),
2558
+ mimeType,
2559
+ encoding: "base64" /* BASE64 */,
2560
+ hashSha256,
2561
+ dataB64,
2562
+ sizeBytes: data.length
2563
+ };
2564
+ const existing = message.mediaChunks ?? [];
2565
+ return rebuildMessage(message, { mediaChunks: [...existing, chunk] });
2566
+ }
2567
+ function addMediaRef(message, refUrl, mimeType, hashSha256, sizeBytes) {
2568
+ const chunk = {
2569
+ chunkId: generateId6(),
2570
+ mimeType,
2571
+ encoding: "ref" /* REF */,
2572
+ hashSha256,
2573
+ refUrl,
2574
+ sizeBytes
2575
+ };
2576
+ const existing = message.mediaChunks ?? [];
2577
+ return rebuildMessage(message, { mediaChunks: [...existing, chunk] });
2578
+ }
2579
+ async function validateMediaChunks(message) {
2580
+ const chunks = message.mediaChunks ?? [];
2581
+ if (chunks.length === 0) {
2582
+ return { valid: true, reason: "no media chunks" };
2583
+ }
2584
+ for (let i = 0; i < chunks.length; i++) {
2585
+ const chunk = chunks[i];
2586
+ if (!chunk.chunkId) return { valid: false, reason: `chunk[${i}]: missing chunkId` };
2587
+ if (!chunk.mimeType) return { valid: false, reason: `chunk[${i}]: missing mimeType` };
2588
+ if (!chunk.hashSha256) return { valid: false, reason: `chunk[${i}]: missing hashSha256` };
2589
+ if (chunk.sizeBytes < 0) return { valid: false, reason: `chunk[${i}]: sizeBytes must be >= 0` };
2590
+ if (chunk.encoding === "base64" /* BASE64 */) {
2591
+ if (!chunk.dataB64) {
2592
+ return { valid: false, reason: `chunk[${i}]: BASE64 encoding requires dataB64` };
2593
+ }
2594
+ let decoded;
2595
+ try {
2596
+ if (typeof Buffer !== "undefined") {
2597
+ decoded = Buffer.from(chunk.dataB64, "base64");
2598
+ } else {
2599
+ const raw = atob(chunk.dataB64);
2600
+ decoded = new Uint8Array(raw.length);
2601
+ for (let j = 0; j < raw.length; j++) decoded[j] = raw.charCodeAt(j);
2602
+ }
2603
+ } catch {
2604
+ return { valid: false, reason: `chunk[${i}]: failed to decode base64 data` };
2605
+ }
2606
+ const actualHash = await computeSha256Hex(decoded);
2607
+ if (actualHash !== chunk.hashSha256) {
2608
+ return {
2609
+ valid: false,
2610
+ reason: `chunk[${i}]: SHA-256 mismatch (expected ${chunk.hashSha256}, got ${actualHash})`
2611
+ };
2612
+ }
2613
+ } else if (chunk.encoding === "ref" /* REF */) {
2614
+ if (!chunk.refUrl) {
2615
+ return { valid: false, reason: `chunk[${i}]: REF encoding requires refUrl` };
2616
+ }
2617
+ } else {
2618
+ return { valid: false, reason: `chunk[${i}]: unknown encoding '${chunk.encoding}'` };
2619
+ }
2620
+ }
2621
+ return { valid: true, reason: "ok" };
2622
+ }
2623
+ async function makeTrainingDataMessage(media) {
2624
+ let msg = new RCANMessage({
2625
+ rcan: "1.6",
2626
+ rcanVersion: "1.6",
2627
+ cmd: "training_data",
2628
+ target: "rcan://training/data",
2629
+ params: {
2630
+ msg_type: 36 /* TRAINING_DATA */,
2631
+ msg_id: generateId6()
2632
+ },
2633
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2634
+ });
2635
+ for (const item of media) {
2636
+ msg = await addMediaInline(msg, item.data, item.mimeType);
2637
+ }
2638
+ return msg;
2639
+ }
2640
+ async function makeStreamChunk(streamId, data, mimeType, chunkIndex, isFinal) {
2641
+ const hashSha256 = await computeSha256Hex(data);
2642
+ const dataB64 = uint8ToBase64(data);
2643
+ const chunk = {
2644
+ chunkId: generateId6(),
2645
+ mimeType,
2646
+ encoding: "base64" /* BASE64 */,
2647
+ hashSha256,
2648
+ dataB64,
2649
+ sizeBytes: data.length
2650
+ };
2651
+ const streamChunkMeta = {
2652
+ streamId,
2653
+ chunkIndex,
2654
+ isFinal,
2655
+ chunk
2656
+ };
2657
+ let msg = new RCANMessage({
2658
+ rcan: "1.6",
2659
+ rcanVersion: "1.6",
2660
+ cmd: "stream_chunk",
2661
+ target: "rcan://streaming/chunk",
2662
+ params: {
2663
+ msg_type: 29 /* SENSOR_DATA */,
2664
+ msg_id: generateId6(),
2665
+ stream_chunk: streamChunkMeta
2666
+ },
2667
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2668
+ });
2669
+ msg = rebuildMessage(msg, { mediaChunks: [chunk] });
2670
+ return msg;
2671
+ }
2672
+
2673
+ // src/contribute.ts
2674
+ var CONTRIBUTE_SCOPE_LEVEL = 2.5;
2675
+ var _idCounter = 0;
2676
+ function _generateId() {
2677
+ return `cr-${Date.now()}-${++_idCounter}`;
2678
+ }
2679
+ function makeContributeRequest(params = {}) {
2680
+ return {
2681
+ type: 33 /* CONTRIBUTE_REQUEST */,
2682
+ request_id: params.request_id ?? _generateId(),
2683
+ project_id: params.project_id ?? "",
2684
+ project_name: params.project_name ?? "",
2685
+ work_unit_id: params.work_unit_id ?? "",
2686
+ resource_type: params.resource_type ?? "cpu",
2687
+ estimated_duration_s: params.estimated_duration_s ?? 0,
2688
+ priority: params.priority ?? 0,
2689
+ payload: params.payload ?? {},
2690
+ timestamp: params.timestamp ?? Date.now() / 1e3
2691
+ };
2692
+ }
2693
+ function makeContributeResult(params = {}) {
2694
+ const result = {
2695
+ type: 34 /* CONTRIBUTE_RESULT */,
2696
+ request_id: params.request_id ?? "",
2697
+ work_unit_id: params.work_unit_id ?? "",
2698
+ status: params.status ?? "completed",
2699
+ resource_type: params.resource_type ?? "cpu",
2700
+ duration_s: params.duration_s ?? 0,
2701
+ compute_units: params.compute_units ?? 0,
2702
+ result_payload: params.result_payload ?? {},
2703
+ timestamp: params.timestamp ?? Date.now() / 1e3
2704
+ };
2705
+ if (params.error_message !== void 0) {
2706
+ result.error_message = params.error_message;
2707
+ }
2708
+ return result;
2709
+ }
2710
+ function makeContributeCancel(params = {}) {
2711
+ return {
2712
+ type: 35 /* CONTRIBUTE_CANCEL */,
2713
+ request_id: params.request_id ?? "",
2714
+ work_unit_id: params.work_unit_id ?? "",
2715
+ reason: params.reason ?? "",
2716
+ timestamp: params.timestamp ?? Date.now() / 1e3
2717
+ };
2718
+ }
2719
+ function validateContributeScope(scopeLevel, action = "request") {
2720
+ if (action === "request" || action === "result") {
2721
+ return scopeLevel >= CONTRIBUTE_SCOPE_LEVEL;
2722
+ }
2723
+ if (action === "cancel") {
2724
+ return scopeLevel >= 2;
2725
+ }
2726
+ return false;
2727
+ }
2728
+ function isPreemptedBy(scopeLevel) {
2729
+ return scopeLevel >= 3;
2730
+ }
2731
+
2024
2732
  // src/index.ts
2025
- var VERSION = "0.5.0";
2026
- var RCAN_VERSION = "1.5";
2733
+ var VERSION = "0.6.0";
2734
+ var RCAN_VERSION = "1.6";
2027
2735
  export {
2028
2736
  AuditChain,
2029
2737
  AuditError,
2738
+ CONTRIBUTE_SCOPE_LEVEL,
2030
2739
  ClockDriftError,
2031
2740
  CommitmentRecord,
2032
2741
  ConfidenceGate,
2742
+ DEFAULT_LOA_POLICY,
2033
2743
  DataCategory,
2034
2744
  FaultCode,
2745
+ FederationSyncType,
2035
2746
  GateError,
2036
2747
  HiTLGate,
2037
2748
  KeyStore,
2749
+ LevelOfAssurance,
2750
+ MediaEncoding,
2038
2751
  MessageType,
2039
2752
  NodeClient,
2040
2753
  OfflineModeManager,
2754
+ PRODUCTION_LOA_POLICY,
2041
2755
  QoSAckTimeoutError,
2042
2756
  QoSLevel,
2043
2757
  QoSManager,
@@ -2059,40 +2773,66 @@ export {
2059
2773
  RCANVersionIncompatibleError,
2060
2774
  RCAN_VERSION,
2061
2775
  RegistryClient,
2776
+ RegistryTier,
2062
2777
  ReplayCache,
2063
2778
  RevocationCache,
2064
2779
  RobotURI,
2065
2780
  RobotURIError,
2066
2781
  SAFETY_MESSAGE_TYPE,
2782
+ SDK_VERSION,
2067
2783
  SPEC_VERSION,
2784
+ TransportEncoding,
2785
+ TransportError,
2786
+ TrustAnchorCache,
2068
2787
  VERSION,
2069
2788
  addDelegationHop,
2789
+ addMediaInline,
2790
+ addMediaRef,
2070
2791
  assertClockSynced,
2071
2792
  checkClockSync,
2072
2793
  checkRevocation,
2794
+ decodeBleFrames,
2795
+ decodeCompact,
2796
+ decodeMinimal,
2797
+ encodeBleFrames,
2798
+ encodeCompact,
2799
+ encodeMinimal,
2800
+ extractLoaFromJwt,
2073
2801
  fetchCanonicalSchema,
2802
+ isPreemptedBy,
2074
2803
  isSafetyMessage,
2075
2804
  makeCloudRelayMessage,
2076
2805
  makeConfigUpdate,
2077
2806
  makeConsentDeny,
2078
2807
  makeConsentGrant,
2079
2808
  makeConsentRequest,
2809
+ makeContributeCancel,
2810
+ makeContributeRequest,
2811
+ makeContributeResult,
2080
2812
  makeEstopMessage,
2081
2813
  makeEstopWithQoS,
2082
2814
  makeFaultReport,
2815
+ makeFederationSync,
2083
2816
  makeKeyRotationMessage,
2084
2817
  makeResumeMessage,
2085
2818
  makeRevocationBroadcast,
2086
2819
  makeStopMessage,
2820
+ makeStreamChunk,
2087
2821
  makeTrainingConsentDeny,
2088
2822
  makeTrainingConsentGrant,
2089
2823
  makeTrainingConsentRequest,
2824
+ makeTrainingDataMessage,
2090
2825
  makeTransparencyMessage,
2826
+ selectTransport,
2091
2827
  validateConfig,
2092
2828
  validateConfigAgainstSchema,
2093
2829
  validateConfigUpdate,
2094
2830
  validateConsentMessage,
2831
+ validateContributeScope,
2832
+ validateCrossRegistryCommand,
2095
2833
  validateDelegationChain,
2834
+ validateLoaForScope,
2835
+ validateMediaChunks,
2096
2836
  validateMessage,
2097
2837
  validateNodeAgainstSchema,
2098
2838
  validateReplay,