@continuonai/rcan-ts 0.6.0 → 1.1.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,8 +99,8 @@ var RobotURI = class _RobotURI {
99
99
  };
100
100
 
101
101
  // src/version.ts
102
- var SPEC_VERSION = "1.6";
103
- var SDK_VERSION = "0.6.0";
102
+ var SPEC_VERSION = "2.1.0";
103
+ var SDK_VERSION = "1.1.0";
104
104
  function validateVersionCompat(incomingVersion, localVersion = SPEC_VERSION) {
105
105
  const parseParts = (v) => {
106
106
  const parts = v.split(".");
@@ -121,18 +121,18 @@ var MessageType = /* @__PURE__ */ ((MessageType2) => {
121
121
  MessageType2[MessageType2["HEARTBEAT"] = 4] = "HEARTBEAT";
122
122
  MessageType2[MessageType2["CONFIG"] = 5] = "CONFIG";
123
123
  MessageType2[MessageType2["SAFETY"] = 6] = "SAFETY";
124
- MessageType2[MessageType2["SENSOR_DATA"] = 7] = "SENSOR_DATA";
125
- MessageType2[MessageType2["AUDIT"] = 8] = "AUDIT";
124
+ MessageType2[MessageType2["AUTH"] = 7] = "AUTH";
125
+ MessageType2[MessageType2["ERROR"] = 8] = "ERROR";
126
126
  MessageType2[MessageType2["DISCOVER"] = 9] = "DISCOVER";
127
- MessageType2[MessageType2["TRAINING_DATA"] = 10] = "TRAINING_DATA";
128
- MessageType2[MessageType2["TRANSPARENCY"] = 11] = "TRANSPARENCY";
129
- MessageType2[MessageType2["FEDERATION_SYNC"] = 12] = "FEDERATION_SYNC";
130
- MessageType2[MessageType2["ALERT"] = 13] = "ALERT";
131
- MessageType2[MessageType2["TELEOP"] = 14] = "TELEOP";
132
- MessageType2[MessageType2["CHAT"] = 15] = "CHAT";
133
- 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";
134
134
  MessageType2[MessageType2["COMMAND_ACK"] = 17] = "COMMAND_ACK";
135
- MessageType2[MessageType2["COMMAND_COMMIT"] = 18] = "COMMAND_COMMIT";
135
+ MessageType2[MessageType2["COMMAND_NACK"] = 18] = "COMMAND_NACK";
136
136
  MessageType2[MessageType2["ROBOT_REVOCATION"] = 19] = "ROBOT_REVOCATION";
137
137
  MessageType2[MessageType2["CONSENT_REQUEST"] = 20] = "CONSENT_REQUEST";
138
138
  MessageType2[MessageType2["CONSENT_GRANT"] = 21] = "CONSENT_GRANT";
@@ -141,7 +141,24 @@ var MessageType = /* @__PURE__ */ ((MessageType2) => {
141
141
  MessageType2[MessageType2["SUBSCRIBE"] = 24] = "SUBSCRIBE";
142
142
  MessageType2[MessageType2["UNSUBSCRIBE"] = 25] = "UNSUBSCRIBE";
143
143
  MessageType2[MessageType2["FAULT_REPORT"] = 26] = "FAULT_REPORT";
144
- 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["COMPETITION_ENTER"] = 37] = "COMPETITION_ENTER";
155
+ MessageType2[MessageType2["COMPETITION_SCORE"] = 38] = "COMPETITION_SCORE";
156
+ MessageType2[MessageType2["SEASON_STANDING"] = 39] = "SEASON_STANDING";
157
+ MessageType2[MessageType2["PERSONAL_RESEARCH_RESULT"] = 40] = "PERSONAL_RESEARCH_RESULT";
158
+ MessageType2[MessageType2["AUTHORITY_ACCESS"] = 41] = "AUTHORITY_ACCESS";
159
+ MessageType2[MessageType2["AUTHORITY_RESPONSE"] = 42] = "AUTHORITY_RESPONSE";
160
+ MessageType2[MessageType2["FIRMWARE_ATTESTATION"] = 43] = "FIRMWARE_ATTESTATION";
161
+ MessageType2[MessageType2["SBOM_UPDATE"] = 44] = "SBOM_UPDATE";
145
162
  return MessageType2;
146
163
  })(MessageType || {});
147
164
  var RCANMessageError = class extends Error {
@@ -178,6 +195,10 @@ var RCANMessage = class _RCANMessage {
178
195
  __publicField(this, "transportEncoding");
179
196
  /** v1.6: GAP-18 multi-modal media chunks */
180
197
  __publicField(this, "mediaChunks");
198
+ /** v2.1: SHA-256 of sender's firmware manifest */
199
+ __publicField(this, "firmwareHash");
200
+ /** v2.1: URI to sender's SBOM attestation endpoint */
201
+ __publicField(this, "attestationRef");
181
202
  if (!data.cmd || data.cmd.trim() === "") {
182
203
  throw new RCANMessageError("'cmd' is required");
183
204
  }
@@ -205,6 +226,13 @@ var RCANMessage = class _RCANMessage {
205
226
  this.loa = data.loa;
206
227
  this.transportEncoding = data.transportEncoding;
207
228
  this.mediaChunks = data.mediaChunks;
229
+ this.firmwareHash = data.firmwareHash;
230
+ this.attestationRef = data.attestationRef;
231
+ if (this.signature !== void 0 && this.signature["sig"] === "pending") {
232
+ throw new RCANMessageError(
233
+ "signature.sig:'pending' is not valid in RCAN v2.1. Sign the message before sending."
234
+ );
235
+ }
208
236
  if (this.confidence !== void 0) {
209
237
  if (this.confidence < 0 || this.confidence > 1) {
210
238
  throw new RCANMessageError(
@@ -246,6 +274,8 @@ var RCANMessage = class _RCANMessage {
246
274
  if (this.loa !== void 0) obj.loa = this.loa;
247
275
  if (this.transportEncoding !== void 0) obj.transportEncoding = this.transportEncoding;
248
276
  if (this.mediaChunks !== void 0) obj.mediaChunks = this.mediaChunks;
277
+ if (this.firmwareHash !== void 0) obj.firmwareHash = this.firmwareHash;
278
+ if (this.attestationRef !== void 0) obj.attestationRef = this.attestationRef;
249
279
  return obj;
250
280
  }
251
281
  /** Serialize to JSON string */
@@ -288,7 +318,9 @@ var RCANMessage = class _RCANMessage {
288
318
  readOnly: obj.readOnly,
289
319
  loa: obj.loa,
290
320
  transportEncoding: obj.transportEncoding,
291
- mediaChunks: obj.mediaChunks
321
+ mediaChunks: obj.mediaChunks,
322
+ firmwareHash: obj.firmwareHash,
323
+ attestationRef: obj.attestationRef
292
324
  });
293
325
  }
294
326
  };
@@ -1263,6 +1295,7 @@ async function fetchCanonicalSchema(schemaName) {
1263
1295
  try {
1264
1296
  const controller = new AbortController();
1265
1297
  const timer = setTimeout(() => controller.abort(), 5e3);
1298
+ timer.unref?.();
1266
1299
  const res = await fetch(`${SCHEMA_BASE}/${schemaName}`, { signal: controller.signal });
1267
1300
  clearTimeout(timer);
1268
1301
  if (!res.ok) return null;
@@ -1907,7 +1940,7 @@ function makeTrainingConsentDeny(params) {
1907
1940
  return makeConsentDeny(params);
1908
1941
  }
1909
1942
  function validateTrainingDataMessage(msg) {
1910
- if (msg.params.message_type !== 10 /* TRAINING_DATA */) {
1943
+ if (msg.params.message_type !== 36 /* TRAINING_DATA */) {
1911
1944
  return { valid: false, reason: "not a TRAINING_DATA message" };
1912
1945
  }
1913
1946
  const token = msg.params.consent_token;
@@ -2047,77 +2080,133 @@ function makeFaultReport(params) {
2047
2080
  }
2048
2081
 
2049
2082
  // src/identity.ts
2050
- var LevelOfAssurance = /* @__PURE__ */ ((LevelOfAssurance2) => {
2051
- LevelOfAssurance2[LevelOfAssurance2["ANONYMOUS"] = 1] = "ANONYMOUS";
2052
- LevelOfAssurance2[LevelOfAssurance2["EMAIL_VERIFIED"] = 2] = "EMAIL_VERIFIED";
2053
- LevelOfAssurance2[LevelOfAssurance2["HARDWARE_TOKEN"] = 3] = "HARDWARE_TOKEN";
2054
- return LevelOfAssurance2;
2055
- })(LevelOfAssurance || {});
2083
+ var Role = /* @__PURE__ */ ((Role2) => {
2084
+ Role2[Role2["GUEST"] = 1] = "GUEST";
2085
+ Role2[Role2["OPERATOR"] = 2] = "OPERATOR";
2086
+ Role2[Role2["CONTRIBUTOR"] = 3] = "CONTRIBUTOR";
2087
+ Role2[Role2["ADMIN"] = 4] = "ADMIN";
2088
+ Role2[Role2["M2M_PEER"] = 5] = "M2M_PEER";
2089
+ Role2[Role2["CREATOR"] = 6] = "CREATOR";
2090
+ Role2[Role2["M2M_TRUSTED"] = 7] = "M2M_TRUSTED";
2091
+ return Role2;
2092
+ })(Role || {});
2093
+ var LevelOfAssurance = Role;
2094
+ var ROLE_JWT_LEVEL = {
2095
+ [1 /* GUEST */]: 1,
2096
+ [2 /* OPERATOR */]: 2,
2097
+ [3 /* CONTRIBUTOR */]: 2.5,
2098
+ [4 /* ADMIN */]: 3,
2099
+ [5 /* M2M_PEER */]: 4,
2100
+ [6 /* CREATOR */]: 5,
2101
+ [7 /* M2M_TRUSTED */]: 6
2102
+ };
2103
+ var JWT_LEVEL_TO_ROLE = new Map(
2104
+ Object.entries(ROLE_JWT_LEVEL).map(
2105
+ ([role, level]) => [level, Number(role)]
2106
+ )
2107
+ );
2108
+ function roleFromJwtLevel(level) {
2109
+ return JWT_LEVEL_TO_ROLE.get(level);
2110
+ }
2111
+ var SCOPE_MIN_ROLE = {
2112
+ "status": 1 /* GUEST */,
2113
+ "discover": 1 /* GUEST */,
2114
+ "chat": 1 /* GUEST */,
2115
+ "observer": 1 /* GUEST */,
2116
+ "contribute": 3 /* CONTRIBUTOR */,
2117
+ "control": 2 /* OPERATOR */,
2118
+ "teleop": 2 /* OPERATOR */,
2119
+ "training": 4 /* ADMIN */,
2120
+ "training_data": 4 /* ADMIN */,
2121
+ "config": 4 /* ADMIN */,
2122
+ "authority": 4 /* ADMIN */,
2123
+ "admin": 6 /* CREATOR */,
2124
+ "safety": 6 /* CREATOR */,
2125
+ "estop": 6 /* CREATOR */,
2126
+ "fleet.trusted": 7 /* M2M_TRUSTED */
2127
+ };
2056
2128
  var DEFAULT_LOA_POLICY = {
2057
- minLoaDiscover: 1 /* ANONYMOUS */,
2058
- minLoaStatus: 1 /* ANONYMOUS */,
2059
- minLoaChat: 1 /* ANONYMOUS */,
2060
- minLoaControl: 1 /* ANONYMOUS */,
2061
- minLoaSafety: 1 /* ANONYMOUS */
2129
+ minRoleForDiscover: 1 /* GUEST */,
2130
+ minRoleForStatus: 1 /* GUEST */,
2131
+ minRoleForChat: 1 /* GUEST */,
2132
+ minRoleForControl: 1 /* GUEST */,
2133
+ minRoleForSafety: 1 /* GUEST */
2062
2134
  };
2063
2135
  var PRODUCTION_LOA_POLICY = {
2064
- minLoaDiscover: 1 /* ANONYMOUS */,
2065
- minLoaStatus: 1 /* ANONYMOUS */,
2066
- minLoaChat: 1 /* ANONYMOUS */,
2067
- minLoaControl: 2 /* EMAIL_VERIFIED */,
2068
- minLoaSafety: 3 /* HARDWARE_TOKEN */
2136
+ minRoleForDiscover: 1 /* GUEST */,
2137
+ minRoleForStatus: 1 /* GUEST */,
2138
+ minRoleForChat: 1 /* GUEST */,
2139
+ minRoleForControl: 2 /* OPERATOR */,
2140
+ minRoleForSafety: 6 /* CREATOR */
2069
2141
  };
2070
- function extractLoaFromJwt(token) {
2142
+ function decodeJwtPayload(token) {
2071
2143
  try {
2072
2144
  const parts = token.split(".");
2073
- if (parts.length < 2) return 1 /* ANONYMOUS */;
2145
+ if (parts.length < 2) return null;
2074
2146
  const payloadB64 = (parts[1] ?? "").replace(/-/g, "+").replace(/_/g, "/");
2075
2147
  const padded = payloadB64 + "=".repeat((4 - payloadB64.length % 4) % 4);
2076
- let json;
2077
- if (typeof atob !== "undefined") {
2078
- json = atob(padded);
2079
- } else {
2080
- json = Buffer.from(padded, "base64").toString("utf-8");
2081
- }
2082
- const claims = JSON.parse(json);
2083
- const loa = claims["loa"];
2084
- if (typeof loa === "number" && loa >= 1 && loa <= 3) {
2085
- return loa;
2086
- }
2148
+ return JSON.parse(atob(padded));
2087
2149
  } catch {
2150
+ return null;
2088
2151
  }
2089
- return 1 /* ANONYMOUS */;
2090
- }
2091
- function minLoaForScope(scope, policy) {
2092
- const s = scope.toLowerCase();
2093
- switch (s) {
2094
- case "discover":
2095
- return policy.minLoaDiscover;
2096
- case "status":
2097
- return policy.minLoaStatus;
2098
- case "chat":
2099
- return policy.minLoaChat;
2100
- case "control":
2101
- return policy.minLoaControl;
2102
- case "safety":
2103
- return policy.minLoaSafety;
2104
- default:
2105
- return null;
2106
- }
2107
- }
2108
- function validateLoaForScope(loa, scope, policy = DEFAULT_LOA_POLICY) {
2109
- const min = minLoaForScope(scope, policy);
2110
- if (min === null) {
2111
- return { valid: true, reason: "unknown scope; allowed by default" };
2112
- }
2113
- if (loa >= min) {
2114
- return { valid: true, reason: "ok" };
2152
+ }
2153
+ function extractRoleFromJwt(token) {
2154
+ const payload = decodeJwtPayload(token);
2155
+ if (!payload) return 1 /* GUEST */;
2156
+ const rcanRole = payload["rcan_role"];
2157
+ if (rcanRole !== void 0 && rcanRole !== null) {
2158
+ const role = roleFromJwtLevel(Number(rcanRole));
2159
+ if (role !== void 0) return role;
2160
+ }
2161
+ const loa = payload["loa"];
2162
+ if (loa !== void 0 && loa !== null) {
2163
+ const role = roleFromJwtLevel(Number(loa));
2164
+ if (role !== void 0) return role;
2165
+ }
2166
+ return 1 /* GUEST */;
2167
+ }
2168
+ function extractLoaFromJwt(token) {
2169
+ return extractRoleFromJwt(token);
2170
+ }
2171
+ function extractIdentityFromJwt(token) {
2172
+ const payload = decodeJwtPayload(token);
2173
+ if (!payload) {
2174
+ return { sub: "", role: 1 /* GUEST */, jwtLevel: 1, scopes: [] };
2175
+ }
2176
+ const rcanRole = payload["rcan_role"];
2177
+ const loa = payload["loa"];
2178
+ const rawLevel = rcanRole !== void 0 ? Number(rcanRole) : loa !== void 0 ? Number(loa) : 1;
2179
+ const role = roleFromJwtLevel(rawLevel) ?? 1 /* GUEST */;
2180
+ const scopes = Array.isArray(payload["rcan_scopes"]) ? payload["rcan_scopes"] : Array.isArray(payload["scopes"]) ? payload["scopes"] : [];
2181
+ return {
2182
+ sub: String(payload["sub"] ?? ""),
2183
+ role,
2184
+ jwtLevel: ROLE_JWT_LEVEL[role],
2185
+ registryUrl: payload["registry_url"],
2186
+ scopes,
2187
+ verifiedAt: payload["verified_at"],
2188
+ peerRrn: payload["peer_rrn"],
2189
+ fleetRrns: Array.isArray(payload["fleet_rrns"]) ? payload["fleet_rrns"] : void 0
2190
+ };
2191
+ }
2192
+ function validateRoleForScope(role, scope) {
2193
+ const required = SCOPE_MIN_ROLE[scope.toLowerCase()];
2194
+ if (required === void 0) {
2195
+ if (role >= 2 /* OPERATOR */) return { ok: true, reason: "" };
2196
+ return {
2197
+ ok: false,
2198
+ reason: `Unknown scope '${scope}': applying OPERATOR minimum. Caller has ${Role[role]}.`
2199
+ };
2115
2200
  }
2201
+ if (role >= required) return { ok: true, reason: "" };
2116
2202
  return {
2117
- valid: false,
2118
- reason: `LOA_INSUFFICIENT: scope=${scope} requires LoA>=${min}, caller has LoA=${loa}`
2203
+ ok: false,
2204
+ reason: `Scope '${scope}' requires ${Role[required]} (JWT level ${ROLE_JWT_LEVEL[required]}), but caller has ${Role[role]} (JWT level ${ROLE_JWT_LEVEL[role]})`
2119
2205
  };
2120
2206
  }
2207
+ function validateLoaForScope(role, scope) {
2208
+ return validateRoleForScope(role, scope);
2209
+ }
2121
2210
 
2122
2211
  // src/federation.ts
2123
2212
  var RegistryTier = /* @__PURE__ */ ((RegistryTier2) => {
@@ -2243,12 +2332,12 @@ function generateId5() {
2243
2332
  }
2244
2333
  function makeFederationSync(source, target, syncType, payload) {
2245
2334
  return new RCANMessage({
2246
- rcan: "1.6",
2247
- rcanVersion: "1.6",
2335
+ rcan: "2.1.0",
2336
+ rcanVersion: "2.1.0",
2248
2337
  cmd: "federation_sync",
2249
2338
  target,
2250
2339
  params: {
2251
- msg_type: 12 /* FEDERATION_SYNC */,
2340
+ msg_type: 23 /* FLEET_COMMAND */,
2252
2341
  msg_id: generateId5(),
2253
2342
  source_registry: source,
2254
2343
  target_registry: target,
@@ -2275,17 +2364,17 @@ async function validateCrossRegistryCommand(msg, localRegistry, trustCache) {
2275
2364
  reason: `REGISTRY_UNKNOWN: ${sourceRegistry} is not in the local trust cache`
2276
2365
  };
2277
2366
  }
2278
- let loa = 1 /* ANONYMOUS */;
2367
+ let loa = 1 /* GUEST */;
2279
2368
  const registryJwt = msg.params?.["registry_jwt"];
2280
2369
  if (registryJwt) {
2281
2370
  loa = extractLoaFromJwt(registryJwt);
2282
2371
  } else if (typeof msg.loa === "number") {
2283
2372
  loa = msg.loa;
2284
2373
  }
2285
- if (loa < 2 /* EMAIL_VERIFIED */) {
2374
+ if (loa < 2 /* OPERATOR */) {
2286
2375
  return {
2287
2376
  valid: false,
2288
- reason: `LOA_INSUFFICIENT: cross-registry commands require LoA>=2 (EMAIL_VERIFIED), got LoA=${loa}`
2377
+ reason: `LOA_INSUFFICIENT: cross-registry commands require LoA>=2 (OPERATOR), got role=${loa}`
2289
2378
  };
2290
2379
  }
2291
2380
  return { valid: true, reason: "cross-registry command accepted" };
@@ -2374,7 +2463,8 @@ async function sha256Bytes(input) {
2374
2463
  const encoded = new TextEncoder().encode(input);
2375
2464
  const ab = new ArrayBuffer(encoded.byteLength);
2376
2465
  new Uint8Array(ab).set(encoded);
2377
- const hashBuffer = await crypto.subtle.digest("SHA-256", ab);
2466
+ const subtle = globalThis.crypto?.subtle ?? (await import("crypto")).webcrypto.subtle;
2467
+ const hashBuffer = await subtle.digest("SHA-256", ab);
2378
2468
  return new Uint8Array(hashBuffer);
2379
2469
  }
2380
2470
  async function encodeMinimal(message) {
@@ -2525,9 +2615,10 @@ function generateId6() {
2525
2615
  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("")}`;
2526
2616
  }
2527
2617
  async function computeSha256Hex(data) {
2618
+ const subtle = globalThis.crypto?.subtle ?? (await import("crypto")).webcrypto.subtle;
2528
2619
  const ab = new ArrayBuffer(data.byteLength);
2529
2620
  new Uint8Array(ab).set(data);
2530
- const hashBuffer = await crypto.subtle.digest("SHA-256", ab);
2621
+ const hashBuffer = await subtle.digest("SHA-256", ab);
2531
2622
  const hashArray = new Uint8Array(hashBuffer);
2532
2623
  return Array.from(hashArray).map((b) => b.toString(16).padStart(2, "0")).join("");
2533
2624
  }
@@ -2620,7 +2711,7 @@ async function makeTrainingDataMessage(media) {
2620
2711
  cmd: "training_data",
2621
2712
  target: "rcan://training/data",
2622
2713
  params: {
2623
- msg_type: 10 /* TRAINING_DATA */,
2714
+ msg_type: 36 /* TRAINING_DATA */,
2624
2715
  msg_id: generateId6()
2625
2716
  },
2626
2717
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
@@ -2653,7 +2744,7 @@ async function makeStreamChunk(streamId, data, mimeType, chunkIndex, isFinal) {
2653
2744
  cmd: "stream_chunk",
2654
2745
  target: "rcan://streaming/chunk",
2655
2746
  params: {
2656
- msg_type: 7 /* SENSOR_DATA */,
2747
+ msg_type: 29 /* SENSOR_DATA */,
2657
2748
  msg_id: generateId6(),
2658
2749
  stream_chunk: streamChunkMeta
2659
2750
  },
@@ -2663,23 +2754,371 @@ async function makeStreamChunk(streamId, data, mimeType, chunkIndex, isFinal) {
2663
2754
  return msg;
2664
2755
  }
2665
2756
 
2757
+ // src/contribute.ts
2758
+ var CONTRIBUTE_SCOPE_LEVEL = 2.5;
2759
+ var _idCounter = 0;
2760
+ function _generateId() {
2761
+ return `cr-${Date.now()}-${++_idCounter}`;
2762
+ }
2763
+ function makeContributeRequest(params = {}) {
2764
+ return {
2765
+ type: 33 /* CONTRIBUTE_REQUEST */,
2766
+ request_id: params.request_id ?? _generateId(),
2767
+ project_id: params.project_id ?? "",
2768
+ project_name: params.project_name ?? "",
2769
+ work_unit_id: params.work_unit_id ?? "",
2770
+ resource_type: params.resource_type ?? "cpu",
2771
+ estimated_duration_s: params.estimated_duration_s ?? 0,
2772
+ priority: params.priority ?? 0,
2773
+ payload: params.payload ?? {},
2774
+ timestamp: params.timestamp ?? Date.now() / 1e3
2775
+ };
2776
+ }
2777
+ function makeContributeResult(params = {}) {
2778
+ const result = {
2779
+ type: 34 /* CONTRIBUTE_RESULT */,
2780
+ request_id: params.request_id ?? "",
2781
+ work_unit_id: params.work_unit_id ?? "",
2782
+ status: params.status ?? "completed",
2783
+ resource_type: params.resource_type ?? "cpu",
2784
+ duration_s: params.duration_s ?? 0,
2785
+ compute_units: params.compute_units ?? 0,
2786
+ result_payload: params.result_payload ?? {},
2787
+ timestamp: params.timestamp ?? Date.now() / 1e3
2788
+ };
2789
+ if (params.error_message !== void 0) {
2790
+ result.error_message = params.error_message;
2791
+ }
2792
+ return result;
2793
+ }
2794
+ function makeContributeCancel(params = {}) {
2795
+ return {
2796
+ type: 35 /* CONTRIBUTE_CANCEL */,
2797
+ request_id: params.request_id ?? "",
2798
+ work_unit_id: params.work_unit_id ?? "",
2799
+ reason: params.reason ?? "",
2800
+ timestamp: params.timestamp ?? Date.now() / 1e3
2801
+ };
2802
+ }
2803
+ function validateContributeScope(scopeLevel, action = "request") {
2804
+ if (action === "request" || action === "result") {
2805
+ return scopeLevel >= CONTRIBUTE_SCOPE_LEVEL;
2806
+ }
2807
+ if (action === "cancel") {
2808
+ return scopeLevel >= 2;
2809
+ }
2810
+ return false;
2811
+ }
2812
+ function isPreemptedBy(scopeLevel) {
2813
+ return scopeLevel >= 3;
2814
+ }
2815
+
2816
+ // src/competition.ts
2817
+ var COMPETITION_SCOPE_LEVEL = 2;
2818
+ var _idCounter2 = 0;
2819
+ function _generateRunId() {
2820
+ return `run-${Date.now()}-${++_idCounter2}`;
2821
+ }
2822
+ function makeCompetitionEnter(params = {}) {
2823
+ return {
2824
+ type: 37 /* COMPETITION_ENTER */,
2825
+ competition_id: params.competition_id ?? "",
2826
+ competition_format: params.competition_format ?? "sprint",
2827
+ hardware_tier: params.hardware_tier ?? "",
2828
+ model_id: params.model_id ?? "",
2829
+ robot_rrn: params.robot_rrn ?? "",
2830
+ entered_at: params.entered_at ?? Date.now() / 1e3
2831
+ };
2832
+ }
2833
+ function makeCompetitionScore(params = {}) {
2834
+ const score = params.score ?? 0;
2835
+ if (score < 0 || score > 1) {
2836
+ throw new Error(`score must be in [0.0, 1.0], got ${score}`);
2837
+ }
2838
+ return {
2839
+ type: 38 /* COMPETITION_SCORE */,
2840
+ competition_id: params.competition_id ?? "",
2841
+ candidate_id: params.candidate_id ?? "",
2842
+ score,
2843
+ hardware_tier: params.hardware_tier ?? "",
2844
+ verified: params.verified ?? false,
2845
+ submitted_at: params.submitted_at ?? Date.now() / 1e3
2846
+ };
2847
+ }
2848
+ function makeSeasonStanding(params = {}) {
2849
+ return {
2850
+ type: 39 /* SEASON_STANDING */,
2851
+ season_id: params.season_id ?? "",
2852
+ class_id: params.class_id ?? "",
2853
+ standings: params.standings ?? [],
2854
+ days_remaining: params.days_remaining ?? 0,
2855
+ broadcast_at: params.broadcast_at ?? Date.now() / 1e3
2856
+ };
2857
+ }
2858
+ function makePersonalResearchResult(params = {}) {
2859
+ const score = params.score ?? 0;
2860
+ if (score < 0 || score > 1) {
2861
+ throw new Error(`score must be in [0.0, 1.0], got ${score}`);
2862
+ }
2863
+ return {
2864
+ type: 40 /* PERSONAL_RESEARCH_RESULT */,
2865
+ run_id: params.run_id ?? _generateRunId(),
2866
+ run_type: params.run_type ?? "personal",
2867
+ candidate_id: params.candidate_id ?? "",
2868
+ score,
2869
+ hardware_tier: params.hardware_tier ?? "",
2870
+ model_id: params.model_id ?? "",
2871
+ owner_uid: params.owner_uid ?? "",
2872
+ metrics: params.metrics ?? {
2873
+ success_rate: 0,
2874
+ p66_rate: 0,
2875
+ token_efficiency: 0,
2876
+ latency_score: 0
2877
+ },
2878
+ submitted_to_community: params.submitted_to_community ?? false,
2879
+ created_at: params.created_at ?? Date.now() / 1e3
2880
+ };
2881
+ }
2882
+ function validateCompetitionScope(scopeLevel) {
2883
+ return scopeLevel >= COMPETITION_SCOPE_LEVEL;
2884
+ }
2885
+
2886
+ // src/firmware.ts
2887
+ var FIRMWARE_MANIFEST_PATH = "/.well-known/rcan-firmware-manifest.json";
2888
+ function manifestToWire(m) {
2889
+ const wire = {
2890
+ rrn: m.rrn,
2891
+ firmware_version: m.firmwareVersion,
2892
+ build_hash: m.buildHash,
2893
+ components: m.components,
2894
+ signed_at: m.signedAt
2895
+ };
2896
+ if (m.signature) wire.signature = m.signature;
2897
+ return wire;
2898
+ }
2899
+ function manifestFromWire(w) {
2900
+ return {
2901
+ rrn: w.rrn,
2902
+ firmwareVersion: w.firmware_version,
2903
+ buildHash: w.build_hash,
2904
+ components: w.components ?? [],
2905
+ signedAt: w.signed_at ?? "",
2906
+ signature: w.signature
2907
+ };
2908
+ }
2909
+ function canonicalManifestJson(m) {
2910
+ const obj = {
2911
+ build_hash: m.buildHash,
2912
+ components: m.components.map((c) => ({
2913
+ hash: c.hash,
2914
+ name: c.name,
2915
+ version: c.version
2916
+ })),
2917
+ firmware_version: m.firmwareVersion,
2918
+ rrn: m.rrn,
2919
+ signed_at: m.signedAt
2920
+ };
2921
+ return JSON.stringify(obj);
2922
+ }
2923
+ var FirmwareIntegrityError = class extends Error {
2924
+ constructor(message) {
2925
+ super(message);
2926
+ this.name = "FirmwareIntegrityError";
2927
+ }
2928
+ };
2929
+ function validateManifest(m) {
2930
+ const errors = [];
2931
+ if (!m.rrn) errors.push("rrn is required");
2932
+ if (!m.firmwareVersion) errors.push("firmwareVersion is required");
2933
+ if (!m.buildHash) errors.push("buildHash is required");
2934
+ if (!m.buildHash.startsWith("sha256:")) errors.push("buildHash must start with 'sha256:'");
2935
+ if (!m.signedAt) errors.push("signedAt is required");
2936
+ if (!m.signature) errors.push("signature is required (manifest must be signed)");
2937
+ for (const [i, c] of m.components.entries()) {
2938
+ if (!c.name) errors.push(`components[${i}].name is required`);
2939
+ if (!c.version) errors.push(`components[${i}].version is required`);
2940
+ if (!c.hash.startsWith("sha256:")) errors.push(`components[${i}].hash must start with 'sha256:'`);
2941
+ }
2942
+ return errors;
2943
+ }
2944
+
2945
+ // src/authority.ts
2946
+ function authorityAccessToWire(p) {
2947
+ return {
2948
+ request_id: p.requestId,
2949
+ authority_id: p.authorityId,
2950
+ requested_data: p.requestedData,
2951
+ justification: p.justification,
2952
+ expires_at: p.expiresAt
2953
+ };
2954
+ }
2955
+ function authorityAccessFromWire(w) {
2956
+ return {
2957
+ requestId: w.request_id,
2958
+ authorityId: w.authority_id,
2959
+ requestedData: w.requested_data ?? [],
2960
+ justification: w.justification ?? "",
2961
+ expiresAt: w.expires_at ?? 0
2962
+ };
2963
+ }
2964
+ function validateAuthorityAccess(p) {
2965
+ const errors = [];
2966
+ if (!p.requestId) errors.push("requestId is required");
2967
+ if (!p.authorityId) errors.push("authorityId is required");
2968
+ if (!p.requestedData || p.requestedData.length === 0)
2969
+ errors.push("requestedData must include at least one category");
2970
+ if (!p.justification) errors.push("justification is required");
2971
+ if (!p.expiresAt || p.expiresAt <= 0) errors.push("expiresAt must be a positive Unix timestamp");
2972
+ if (p.expiresAt < Date.now() / 1e3) errors.push("expiresAt is in the past \u2014 request has expired");
2973
+ return errors;
2974
+ }
2975
+ function isAuthorityRequestValid(p) {
2976
+ return Date.now() / 1e3 < p.expiresAt && validateAuthorityAccess(p).length === 0;
2977
+ }
2978
+ var AUTHORITY_ERROR_CODES = {
2979
+ NOT_RECOGNIZED: "AUTHORITY_NOT_RECOGNIZED",
2980
+ REQUEST_EXPIRED: "AUTHORITY_REQUEST_EXPIRED",
2981
+ INVALID_TOKEN: "AUTHORITY_INVALID_TOKEN",
2982
+ RATE_LIMITED: "AUTHORITY_RATE_LIMITED"
2983
+ };
2984
+
2985
+ // src/m2m.ts
2986
+ var RRF_REVOCATION_URL = "https://api.rrf.rcan.dev/v2/revocations";
2987
+ var M2M_TRUSTED_ISSUER = "rrf.rcan.dev";
2988
+ var RRF_REVOCATION_CACHE_TTL_MS = 55e3;
2989
+ var M2MAuthError = class extends Error {
2990
+ constructor(message) {
2991
+ super(message);
2992
+ this.name = "M2MAuthError";
2993
+ }
2994
+ };
2995
+ function decodeJwtPayload2(token) {
2996
+ const parts = token.split(".");
2997
+ if (parts.length < 2) throw new M2MAuthError("Invalid JWT structure");
2998
+ const b64 = (parts[1] ?? "").replace(/-/g, "+").replace(/_/g, "/");
2999
+ const padded = b64 + "=".repeat((4 - b64.length % 4) % 4);
3000
+ try {
3001
+ return JSON.parse(atob(padded));
3002
+ } catch (e) {
3003
+ throw new M2MAuthError(`JWT payload decode failed: ${String(e)}`);
3004
+ }
3005
+ }
3006
+ function parseM2mPeerToken(token) {
3007
+ const payload = decodeJwtPayload2(token);
3008
+ const exp = Number(payload["exp"] ?? 0);
3009
+ if (exp > 0 && Date.now() / 1e3 > exp) {
3010
+ throw new M2MAuthError(`M2M_PEER token expired (sub=${String(payload["sub"])})`);
3011
+ }
3012
+ const peerRrn = String(payload["peer_rrn"] ?? "");
3013
+ if (!peerRrn) throw new M2MAuthError("M2M_PEER token missing peer_rrn claim");
3014
+ return {
3015
+ sub: String(payload["sub"] ?? ""),
3016
+ peerRrn,
3017
+ scopes: Array.isArray(payload["rcan_scopes"]) ? payload["rcan_scopes"] : Array.isArray(payload["scopes"]) ? payload["scopes"] : [],
3018
+ exp,
3019
+ iss: String(payload["iss"] ?? "")
3020
+ };
3021
+ }
3022
+ function parseM2mTrustedToken(token) {
3023
+ const payload = decodeJwtPayload2(token);
3024
+ const iss = String(payload["iss"] ?? "");
3025
+ if (iss !== M2M_TRUSTED_ISSUER) {
3026
+ throw new M2MAuthError(
3027
+ `M2M_TRUSTED issuer must be '${M2M_TRUSTED_ISSUER}', got '${iss}'`
3028
+ );
3029
+ }
3030
+ const scopes = Array.isArray(payload["rcan_scopes"]) ? payload["rcan_scopes"] : Array.isArray(payload["scopes"]) ? payload["scopes"] : [];
3031
+ if (!scopes.includes("fleet.trusted")) {
3032
+ throw new M2MAuthError("M2M_TRUSTED token missing required 'fleet.trusted' scope");
3033
+ }
3034
+ const exp = Number(payload["exp"] ?? 0);
3035
+ if (exp > 0 && Date.now() / 1e3 > exp) {
3036
+ throw new M2MAuthError(`M2M_TRUSTED token expired (sub=${String(payload["sub"])})`);
3037
+ }
3038
+ const rrfSig = String(payload["rrf_sig"] ?? "");
3039
+ if (!rrfSig) throw new M2MAuthError("M2M_TRUSTED token missing rrf_sig claim");
3040
+ const fleetRrns = Array.isArray(payload["fleet_rrns"]) ? payload["fleet_rrns"] : [];
3041
+ return {
3042
+ sub: String(payload["sub"] ?? ""),
3043
+ fleetRrns,
3044
+ scopes,
3045
+ exp,
3046
+ iss,
3047
+ rrfSig
3048
+ };
3049
+ }
3050
+ function verifyM2mTrustedTokenClaims(token, targetRrn) {
3051
+ const claims = parseM2mTrustedToken(token);
3052
+ if (!claims.fleetRrns.includes(targetRrn)) {
3053
+ throw new M2MAuthError(
3054
+ `M2M_TRUSTED token does not authorize commanding '${targetRrn}'. Authorized fleet: [${claims.fleetRrns.join(", ")}]`
3055
+ );
3056
+ }
3057
+ return claims;
3058
+ }
3059
+ var _revocationCache = null;
3060
+ async function fetchRRFRevocations(url = RRF_REVOCATION_URL) {
3061
+ const now = Date.now();
3062
+ if (_revocationCache && now - _revocationCache.fetchedAt < RRF_REVOCATION_CACHE_TTL_MS) {
3063
+ return _revocationCache;
3064
+ }
3065
+ try {
3066
+ const resp = await fetch(url, { signal: AbortSignal.timeout?.(5e3) });
3067
+ const data = await resp.json();
3068
+ _revocationCache = {
3069
+ revokedOrchestrators: new Set(data.revoked_orchestrators ?? []),
3070
+ revokedJtis: new Set(data.revoked_jtis ?? []),
3071
+ fetchedAt: now
3072
+ };
3073
+ } catch {
3074
+ if (_revocationCache) return _revocationCache;
3075
+ _revocationCache = { revokedOrchestrators: /* @__PURE__ */ new Set(), revokedJtis: /* @__PURE__ */ new Set(), fetchedAt: now };
3076
+ }
3077
+ return _revocationCache;
3078
+ }
3079
+ async function isM2mTrustedRevoked(claims, jti) {
3080
+ const cache = await fetchRRFRevocations();
3081
+ if (cache.revokedOrchestrators.has(claims.sub)) return true;
3082
+ if (jti && cache.revokedJtis.has(jti)) return true;
3083
+ return false;
3084
+ }
3085
+ async function verifyM2mTrustedToken(token, targetRrn, options) {
3086
+ const claims = verifyM2mTrustedTokenClaims(token, targetRrn);
3087
+ if (!options?.skipRevocationCheck) {
3088
+ const revoked = await isM2mTrustedRevoked(claims);
3089
+ if (revoked) {
3090
+ throw new M2MAuthError(
3091
+ `M2M_TRUSTED orchestrator '${claims.sub}' is on the RRF revocation list`
3092
+ );
3093
+ }
3094
+ }
3095
+ return claims;
3096
+ }
3097
+
2666
3098
  // src/index.ts
2667
3099
  var VERSION = "0.6.0";
2668
3100
  var RCAN_VERSION = "1.6";
2669
3101
  export {
3102
+ AUTHORITY_ERROR_CODES,
2670
3103
  AuditChain,
2671
3104
  AuditError,
3105
+ COMPETITION_SCOPE_LEVEL,
3106
+ CONTRIBUTE_SCOPE_LEVEL,
2672
3107
  ClockDriftError,
2673
3108
  CommitmentRecord,
2674
3109
  ConfidenceGate,
2675
3110
  DEFAULT_LOA_POLICY,
2676
3111
  DataCategory,
3112
+ FIRMWARE_MANIFEST_PATH,
2677
3113
  FaultCode,
2678
3114
  FederationSyncType,
3115
+ FirmwareIntegrityError,
2679
3116
  GateError,
2680
3117
  HiTLGate,
2681
3118
  KeyStore,
2682
3119
  LevelOfAssurance,
3120
+ M2MAuthError,
3121
+ M2M_TRUSTED_ISSUER,
2683
3122
  MediaEncoding,
2684
3123
  MessageType,
2685
3124
  NodeClient,
@@ -2705,13 +3144,18 @@ export {
2705
3144
  RCANValidationError,
2706
3145
  RCANVersionIncompatibleError,
2707
3146
  RCAN_VERSION,
3147
+ ROLE_JWT_LEVEL,
3148
+ RRF_REVOCATION_CACHE_TTL_MS,
3149
+ RRF_REVOCATION_URL,
2708
3150
  RegistryClient,
2709
3151
  RegistryTier,
2710
3152
  ReplayCache,
2711
3153
  RevocationCache,
2712
3154
  RobotURI,
2713
3155
  RobotURIError,
3156
+ Role,
2714
3157
  SAFETY_MESSAGE_TYPE,
3158
+ SCOPE_MIN_ROLE,
2715
3159
  SDK_VERSION,
2716
3160
  SPEC_VERSION,
2717
3161
  TransportEncoding,
@@ -2722,6 +3166,9 @@ export {
2722
3166
  addMediaInline,
2723
3167
  addMediaRef,
2724
3168
  assertClockSynced,
3169
+ authorityAccessFromWire,
3170
+ authorityAccessToWire,
3171
+ canonicalManifestJson,
2725
3172
  checkClockSync,
2726
3173
  checkRevocation,
2727
3174
  decodeBleFrames,
@@ -2730,21 +3177,34 @@ export {
2730
3177
  encodeBleFrames,
2731
3178
  encodeCompact,
2732
3179
  encodeMinimal,
3180
+ extractIdentityFromJwt,
2733
3181
  extractLoaFromJwt,
3182
+ extractRoleFromJwt,
2734
3183
  fetchCanonicalSchema,
3184
+ fetchRRFRevocations,
3185
+ isAuthorityRequestValid,
3186
+ isM2mTrustedRevoked,
3187
+ isPreemptedBy,
2735
3188
  isSafetyMessage,
2736
3189
  makeCloudRelayMessage,
3190
+ makeCompetitionEnter,
3191
+ makeCompetitionScore,
2737
3192
  makeConfigUpdate,
2738
3193
  makeConsentDeny,
2739
3194
  makeConsentGrant,
2740
3195
  makeConsentRequest,
3196
+ makeContributeCancel,
3197
+ makeContributeRequest,
3198
+ makeContributeResult,
2741
3199
  makeEstopMessage,
2742
3200
  makeEstopWithQoS,
2743
3201
  makeFaultReport,
2744
3202
  makeFederationSync,
2745
3203
  makeKeyRotationMessage,
3204
+ makePersonalResearchResult,
2746
3205
  makeResumeMessage,
2747
3206
  makeRevocationBroadcast,
3207
+ makeSeasonStanding,
2748
3208
  makeStopMessage,
2749
3209
  makeStreamChunk,
2750
3210
  makeTrainingConsentDeny,
@@ -2752,21 +3212,33 @@ export {
2752
3212
  makeTrainingConsentRequest,
2753
3213
  makeTrainingDataMessage,
2754
3214
  makeTransparencyMessage,
3215
+ manifestFromWire,
3216
+ manifestToWire,
3217
+ parseM2mPeerToken,
3218
+ parseM2mTrustedToken,
3219
+ roleFromJwtLevel,
2755
3220
  selectTransport,
3221
+ validateAuthorityAccess,
3222
+ validateCompetitionScope,
2756
3223
  validateConfig,
2757
3224
  validateConfigAgainstSchema,
2758
3225
  validateConfigUpdate,
2759
3226
  validateConsentMessage,
3227
+ validateContributeScope,
2760
3228
  validateCrossRegistryCommand,
2761
3229
  validateDelegationChain,
2762
3230
  validateLoaForScope,
3231
+ validateManifest,
2763
3232
  validateMediaChunks,
2764
3233
  validateMessage,
2765
3234
  validateNodeAgainstSchema,
2766
3235
  validateReplay,
3236
+ validateRoleForScope,
2767
3237
  validateSafetyMessage,
2768
3238
  validateTrainingDataMessage,
2769
3239
  validateURI,
2770
- validateVersionCompat
3240
+ validateVersionCompat,
3241
+ verifyM2mTrustedToken,
3242
+ verifyM2mTrustedTokenClaims
2771
3243
  };
2772
3244
  //# sourceMappingURL=browser.mjs.map