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