@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/index.mjs CHANGED
@@ -96,8 +96,8 @@ var RobotURI = class _RobotURI {
96
96
  };
97
97
 
98
98
  // src/version.ts
99
- var SPEC_VERSION = "1.6";
100
- var SDK_VERSION = "0.6.0";
99
+ var SPEC_VERSION = "2.1.0";
100
+ var SDK_VERSION = "1.1.0";
101
101
  function validateVersionCompat(incomingVersion, localVersion = SPEC_VERSION) {
102
102
  const parseParts = (v) => {
103
103
  const parts = v.split(".");
@@ -118,18 +118,18 @@ var MessageType = /* @__PURE__ */ ((MessageType2) => {
118
118
  MessageType2[MessageType2["HEARTBEAT"] = 4] = "HEARTBEAT";
119
119
  MessageType2[MessageType2["CONFIG"] = 5] = "CONFIG";
120
120
  MessageType2[MessageType2["SAFETY"] = 6] = "SAFETY";
121
- MessageType2[MessageType2["SENSOR_DATA"] = 7] = "SENSOR_DATA";
122
- MessageType2[MessageType2["AUDIT"] = 8] = "AUDIT";
121
+ MessageType2[MessageType2["AUTH"] = 7] = "AUTH";
122
+ MessageType2[MessageType2["ERROR"] = 8] = "ERROR";
123
123
  MessageType2[MessageType2["DISCOVER"] = 9] = "DISCOVER";
124
- MessageType2[MessageType2["TRAINING_DATA"] = 10] = "TRAINING_DATA";
125
- MessageType2[MessageType2["TRANSPARENCY"] = 11] = "TRANSPARENCY";
126
- MessageType2[MessageType2["FEDERATION_SYNC"] = 12] = "FEDERATION_SYNC";
127
- MessageType2[MessageType2["ALERT"] = 13] = "ALERT";
128
- MessageType2[MessageType2["TELEOP"] = 14] = "TELEOP";
129
- MessageType2[MessageType2["CHAT"] = 15] = "CHAT";
130
- 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";
131
131
  MessageType2[MessageType2["COMMAND_ACK"] = 17] = "COMMAND_ACK";
132
- MessageType2[MessageType2["COMMAND_COMMIT"] = 18] = "COMMAND_COMMIT";
132
+ MessageType2[MessageType2["COMMAND_NACK"] = 18] = "COMMAND_NACK";
133
133
  MessageType2[MessageType2["ROBOT_REVOCATION"] = 19] = "ROBOT_REVOCATION";
134
134
  MessageType2[MessageType2["CONSENT_REQUEST"] = 20] = "CONSENT_REQUEST";
135
135
  MessageType2[MessageType2["CONSENT_GRANT"] = 21] = "CONSENT_GRANT";
@@ -138,7 +138,24 @@ var MessageType = /* @__PURE__ */ ((MessageType2) => {
138
138
  MessageType2[MessageType2["SUBSCRIBE"] = 24] = "SUBSCRIBE";
139
139
  MessageType2[MessageType2["UNSUBSCRIBE"] = 25] = "UNSUBSCRIBE";
140
140
  MessageType2[MessageType2["FAULT_REPORT"] = 26] = "FAULT_REPORT";
141
- 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["COMPETITION_ENTER"] = 37] = "COMPETITION_ENTER";
152
+ MessageType2[MessageType2["COMPETITION_SCORE"] = 38] = "COMPETITION_SCORE";
153
+ MessageType2[MessageType2["SEASON_STANDING"] = 39] = "SEASON_STANDING";
154
+ MessageType2[MessageType2["PERSONAL_RESEARCH_RESULT"] = 40] = "PERSONAL_RESEARCH_RESULT";
155
+ MessageType2[MessageType2["AUTHORITY_ACCESS"] = 41] = "AUTHORITY_ACCESS";
156
+ MessageType2[MessageType2["AUTHORITY_RESPONSE"] = 42] = "AUTHORITY_RESPONSE";
157
+ MessageType2[MessageType2["FIRMWARE_ATTESTATION"] = 43] = "FIRMWARE_ATTESTATION";
158
+ MessageType2[MessageType2["SBOM_UPDATE"] = 44] = "SBOM_UPDATE";
142
159
  return MessageType2;
143
160
  })(MessageType || {});
144
161
  var RCANMessageError = class extends Error {
@@ -174,6 +191,10 @@ var RCANMessage = class _RCANMessage {
174
191
  transportEncoding;
175
192
  /** v1.6: GAP-18 multi-modal media chunks */
176
193
  mediaChunks;
194
+ /** v2.1: SHA-256 of sender's firmware manifest */
195
+ firmwareHash;
196
+ /** v2.1: URI to sender's SBOM attestation endpoint */
197
+ attestationRef;
177
198
  constructor(data) {
178
199
  if (!data.cmd || data.cmd.trim() === "") {
179
200
  throw new RCANMessageError("'cmd' is required");
@@ -202,6 +223,13 @@ var RCANMessage = class _RCANMessage {
202
223
  this.loa = data.loa;
203
224
  this.transportEncoding = data.transportEncoding;
204
225
  this.mediaChunks = data.mediaChunks;
226
+ this.firmwareHash = data.firmwareHash;
227
+ this.attestationRef = data.attestationRef;
228
+ if (this.signature !== void 0 && this.signature["sig"] === "pending") {
229
+ throw new RCANMessageError(
230
+ "signature.sig:'pending' is not valid in RCAN v2.1. Sign the message before sending."
231
+ );
232
+ }
205
233
  if (this.confidence !== void 0) {
206
234
  if (this.confidence < 0 || this.confidence > 1) {
207
235
  throw new RCANMessageError(
@@ -243,6 +271,8 @@ var RCANMessage = class _RCANMessage {
243
271
  if (this.loa !== void 0) obj.loa = this.loa;
244
272
  if (this.transportEncoding !== void 0) obj.transportEncoding = this.transportEncoding;
245
273
  if (this.mediaChunks !== void 0) obj.mediaChunks = this.mediaChunks;
274
+ if (this.firmwareHash !== void 0) obj.firmwareHash = this.firmwareHash;
275
+ if (this.attestationRef !== void 0) obj.attestationRef = this.attestationRef;
246
276
  return obj;
247
277
  }
248
278
  /** Serialize to JSON string */
@@ -285,7 +315,9 @@ var RCANMessage = class _RCANMessage {
285
315
  readOnly: obj.readOnly,
286
316
  loa: obj.loa,
287
317
  transportEncoding: obj.transportEncoding,
288
- mediaChunks: obj.mediaChunks
318
+ mediaChunks: obj.mediaChunks,
319
+ firmwareHash: obj.firmwareHash,
320
+ attestationRef: obj.attestationRef
289
321
  });
290
322
  }
291
323
  };
@@ -1258,6 +1290,7 @@ async function fetchCanonicalSchema(schemaName) {
1258
1290
  try {
1259
1291
  const controller = new AbortController();
1260
1292
  const timer = setTimeout(() => controller.abort(), 5e3);
1293
+ timer.unref?.();
1261
1294
  const res = await fetch(`${SCHEMA_BASE}/${schemaName}`, { signal: controller.signal });
1262
1295
  clearTimeout(timer);
1263
1296
  if (!res.ok) return null;
@@ -1898,7 +1931,7 @@ function makeTrainingConsentDeny(params) {
1898
1931
  return makeConsentDeny(params);
1899
1932
  }
1900
1933
  function validateTrainingDataMessage(msg) {
1901
- if (msg.params.message_type !== 10 /* TRAINING_DATA */) {
1934
+ if (msg.params.message_type !== 36 /* TRAINING_DATA */) {
1902
1935
  return { valid: false, reason: "not a TRAINING_DATA message" };
1903
1936
  }
1904
1937
  const token = msg.params.consent_token;
@@ -2038,77 +2071,133 @@ function makeFaultReport(params) {
2038
2071
  }
2039
2072
 
2040
2073
  // src/identity.ts
2041
- var LevelOfAssurance = /* @__PURE__ */ ((LevelOfAssurance2) => {
2042
- LevelOfAssurance2[LevelOfAssurance2["ANONYMOUS"] = 1] = "ANONYMOUS";
2043
- LevelOfAssurance2[LevelOfAssurance2["EMAIL_VERIFIED"] = 2] = "EMAIL_VERIFIED";
2044
- LevelOfAssurance2[LevelOfAssurance2["HARDWARE_TOKEN"] = 3] = "HARDWARE_TOKEN";
2045
- return LevelOfAssurance2;
2046
- })(LevelOfAssurance || {});
2074
+ var Role = /* @__PURE__ */ ((Role2) => {
2075
+ Role2[Role2["GUEST"] = 1] = "GUEST";
2076
+ Role2[Role2["OPERATOR"] = 2] = "OPERATOR";
2077
+ Role2[Role2["CONTRIBUTOR"] = 3] = "CONTRIBUTOR";
2078
+ Role2[Role2["ADMIN"] = 4] = "ADMIN";
2079
+ Role2[Role2["M2M_PEER"] = 5] = "M2M_PEER";
2080
+ Role2[Role2["CREATOR"] = 6] = "CREATOR";
2081
+ Role2[Role2["M2M_TRUSTED"] = 7] = "M2M_TRUSTED";
2082
+ return Role2;
2083
+ })(Role || {});
2084
+ var LevelOfAssurance = Role;
2085
+ var ROLE_JWT_LEVEL = {
2086
+ [1 /* GUEST */]: 1,
2087
+ [2 /* OPERATOR */]: 2,
2088
+ [3 /* CONTRIBUTOR */]: 2.5,
2089
+ [4 /* ADMIN */]: 3,
2090
+ [5 /* M2M_PEER */]: 4,
2091
+ [6 /* CREATOR */]: 5,
2092
+ [7 /* M2M_TRUSTED */]: 6
2093
+ };
2094
+ var JWT_LEVEL_TO_ROLE = new Map(
2095
+ Object.entries(ROLE_JWT_LEVEL).map(
2096
+ ([role, level]) => [level, Number(role)]
2097
+ )
2098
+ );
2099
+ function roleFromJwtLevel(level) {
2100
+ return JWT_LEVEL_TO_ROLE.get(level);
2101
+ }
2102
+ var SCOPE_MIN_ROLE = {
2103
+ "status": 1 /* GUEST */,
2104
+ "discover": 1 /* GUEST */,
2105
+ "chat": 1 /* GUEST */,
2106
+ "observer": 1 /* GUEST */,
2107
+ "contribute": 3 /* CONTRIBUTOR */,
2108
+ "control": 2 /* OPERATOR */,
2109
+ "teleop": 2 /* OPERATOR */,
2110
+ "training": 4 /* ADMIN */,
2111
+ "training_data": 4 /* ADMIN */,
2112
+ "config": 4 /* ADMIN */,
2113
+ "authority": 4 /* ADMIN */,
2114
+ "admin": 6 /* CREATOR */,
2115
+ "safety": 6 /* CREATOR */,
2116
+ "estop": 6 /* CREATOR */,
2117
+ "fleet.trusted": 7 /* M2M_TRUSTED */
2118
+ };
2047
2119
  var DEFAULT_LOA_POLICY = {
2048
- minLoaDiscover: 1 /* ANONYMOUS */,
2049
- minLoaStatus: 1 /* ANONYMOUS */,
2050
- minLoaChat: 1 /* ANONYMOUS */,
2051
- minLoaControl: 1 /* ANONYMOUS */,
2052
- minLoaSafety: 1 /* ANONYMOUS */
2120
+ minRoleForDiscover: 1 /* GUEST */,
2121
+ minRoleForStatus: 1 /* GUEST */,
2122
+ minRoleForChat: 1 /* GUEST */,
2123
+ minRoleForControl: 1 /* GUEST */,
2124
+ minRoleForSafety: 1 /* GUEST */
2053
2125
  };
2054
2126
  var PRODUCTION_LOA_POLICY = {
2055
- minLoaDiscover: 1 /* ANONYMOUS */,
2056
- minLoaStatus: 1 /* ANONYMOUS */,
2057
- minLoaChat: 1 /* ANONYMOUS */,
2058
- minLoaControl: 2 /* EMAIL_VERIFIED */,
2059
- minLoaSafety: 3 /* HARDWARE_TOKEN */
2127
+ minRoleForDiscover: 1 /* GUEST */,
2128
+ minRoleForStatus: 1 /* GUEST */,
2129
+ minRoleForChat: 1 /* GUEST */,
2130
+ minRoleForControl: 2 /* OPERATOR */,
2131
+ minRoleForSafety: 6 /* CREATOR */
2060
2132
  };
2061
- function extractLoaFromJwt(token) {
2133
+ function decodeJwtPayload(token) {
2062
2134
  try {
2063
2135
  const parts = token.split(".");
2064
- if (parts.length < 2) return 1 /* ANONYMOUS */;
2136
+ if (parts.length < 2) return null;
2065
2137
  const payloadB64 = (parts[1] ?? "").replace(/-/g, "+").replace(/_/g, "/");
2066
2138
  const padded = payloadB64 + "=".repeat((4 - payloadB64.length % 4) % 4);
2067
- let json;
2068
- if (typeof atob !== "undefined") {
2069
- json = atob(padded);
2070
- } else {
2071
- json = Buffer.from(padded, "base64").toString("utf-8");
2072
- }
2073
- const claims = JSON.parse(json);
2074
- const loa = claims["loa"];
2075
- if (typeof loa === "number" && loa >= 1 && loa <= 3) {
2076
- return loa;
2077
- }
2139
+ return JSON.parse(atob(padded));
2078
2140
  } catch {
2141
+ return null;
2079
2142
  }
2080
- return 1 /* ANONYMOUS */;
2081
- }
2082
- function minLoaForScope(scope, policy) {
2083
- const s = scope.toLowerCase();
2084
- switch (s) {
2085
- case "discover":
2086
- return policy.minLoaDiscover;
2087
- case "status":
2088
- return policy.minLoaStatus;
2089
- case "chat":
2090
- return policy.minLoaChat;
2091
- case "control":
2092
- return policy.minLoaControl;
2093
- case "safety":
2094
- return policy.minLoaSafety;
2095
- default:
2096
- return null;
2097
- }
2098
- }
2099
- function validateLoaForScope(loa, scope, policy = DEFAULT_LOA_POLICY) {
2100
- const min = minLoaForScope(scope, policy);
2101
- if (min === null) {
2102
- return { valid: true, reason: "unknown scope; allowed by default" };
2103
- }
2104
- if (loa >= min) {
2105
- return { valid: true, reason: "ok" };
2143
+ }
2144
+ function extractRoleFromJwt(token) {
2145
+ const payload = decodeJwtPayload(token);
2146
+ if (!payload) return 1 /* GUEST */;
2147
+ const rcanRole = payload["rcan_role"];
2148
+ if (rcanRole !== void 0 && rcanRole !== null) {
2149
+ const role = roleFromJwtLevel(Number(rcanRole));
2150
+ if (role !== void 0) return role;
2151
+ }
2152
+ const loa = payload["loa"];
2153
+ if (loa !== void 0 && loa !== null) {
2154
+ const role = roleFromJwtLevel(Number(loa));
2155
+ if (role !== void 0) return role;
2156
+ }
2157
+ return 1 /* GUEST */;
2158
+ }
2159
+ function extractLoaFromJwt(token) {
2160
+ return extractRoleFromJwt(token);
2161
+ }
2162
+ function extractIdentityFromJwt(token) {
2163
+ const payload = decodeJwtPayload(token);
2164
+ if (!payload) {
2165
+ return { sub: "", role: 1 /* GUEST */, jwtLevel: 1, scopes: [] };
2166
+ }
2167
+ const rcanRole = payload["rcan_role"];
2168
+ const loa = payload["loa"];
2169
+ const rawLevel = rcanRole !== void 0 ? Number(rcanRole) : loa !== void 0 ? Number(loa) : 1;
2170
+ const role = roleFromJwtLevel(rawLevel) ?? 1 /* GUEST */;
2171
+ const scopes = Array.isArray(payload["rcan_scopes"]) ? payload["rcan_scopes"] : Array.isArray(payload["scopes"]) ? payload["scopes"] : [];
2172
+ return {
2173
+ sub: String(payload["sub"] ?? ""),
2174
+ role,
2175
+ jwtLevel: ROLE_JWT_LEVEL[role],
2176
+ registryUrl: payload["registry_url"],
2177
+ scopes,
2178
+ verifiedAt: payload["verified_at"],
2179
+ peerRrn: payload["peer_rrn"],
2180
+ fleetRrns: Array.isArray(payload["fleet_rrns"]) ? payload["fleet_rrns"] : void 0
2181
+ };
2182
+ }
2183
+ function validateRoleForScope(role, scope) {
2184
+ const required = SCOPE_MIN_ROLE[scope.toLowerCase()];
2185
+ if (required === void 0) {
2186
+ if (role >= 2 /* OPERATOR */) return { ok: true, reason: "" };
2187
+ return {
2188
+ ok: false,
2189
+ reason: `Unknown scope '${scope}': applying OPERATOR minimum. Caller has ${Role[role]}.`
2190
+ };
2106
2191
  }
2192
+ if (role >= required) return { ok: true, reason: "" };
2107
2193
  return {
2108
- valid: false,
2109
- reason: `LOA_INSUFFICIENT: scope=${scope} requires LoA>=${min}, caller has LoA=${loa}`
2194
+ ok: false,
2195
+ reason: `Scope '${scope}' requires ${Role[required]} (JWT level ${ROLE_JWT_LEVEL[required]}), but caller has ${Role[role]} (JWT level ${ROLE_JWT_LEVEL[role]})`
2110
2196
  };
2111
2197
  }
2198
+ function validateLoaForScope(role, scope) {
2199
+ return validateRoleForScope(role, scope);
2200
+ }
2112
2201
 
2113
2202
  // src/federation.ts
2114
2203
  var RegistryTier = /* @__PURE__ */ ((RegistryTier2) => {
@@ -2232,12 +2321,12 @@ function generateId5() {
2232
2321
  }
2233
2322
  function makeFederationSync(source, target, syncType, payload) {
2234
2323
  return new RCANMessage({
2235
- rcan: "1.6",
2236
- rcanVersion: "1.6",
2324
+ rcan: "2.1.0",
2325
+ rcanVersion: "2.1.0",
2237
2326
  cmd: "federation_sync",
2238
2327
  target,
2239
2328
  params: {
2240
- msg_type: 12 /* FEDERATION_SYNC */,
2329
+ msg_type: 23 /* FLEET_COMMAND */,
2241
2330
  msg_id: generateId5(),
2242
2331
  source_registry: source,
2243
2332
  target_registry: target,
@@ -2264,17 +2353,17 @@ async function validateCrossRegistryCommand(msg, localRegistry, trustCache) {
2264
2353
  reason: `REGISTRY_UNKNOWN: ${sourceRegistry} is not in the local trust cache`
2265
2354
  };
2266
2355
  }
2267
- let loa = 1 /* ANONYMOUS */;
2356
+ let loa = 1 /* GUEST */;
2268
2357
  const registryJwt = msg.params?.["registry_jwt"];
2269
2358
  if (registryJwt) {
2270
2359
  loa = extractLoaFromJwt(registryJwt);
2271
2360
  } else if (typeof msg.loa === "number") {
2272
2361
  loa = msg.loa;
2273
2362
  }
2274
- if (loa < 2 /* EMAIL_VERIFIED */) {
2363
+ if (loa < 2 /* OPERATOR */) {
2275
2364
  return {
2276
2365
  valid: false,
2277
- reason: `LOA_INSUFFICIENT: cross-registry commands require LoA>=2 (EMAIL_VERIFIED), got LoA=${loa}`
2366
+ reason: `LOA_INSUFFICIENT: cross-registry commands require LoA>=2 (OPERATOR), got role=${loa}`
2278
2367
  };
2279
2368
  }
2280
2369
  return { valid: true, reason: "cross-registry command accepted" };
@@ -2363,7 +2452,8 @@ async function sha256Bytes(input) {
2363
2452
  const encoded = new TextEncoder().encode(input);
2364
2453
  const ab = new ArrayBuffer(encoded.byteLength);
2365
2454
  new Uint8Array(ab).set(encoded);
2366
- const hashBuffer = await crypto.subtle.digest("SHA-256", ab);
2455
+ const subtle = globalThis.crypto?.subtle ?? (await import("crypto")).webcrypto.subtle;
2456
+ const hashBuffer = await subtle.digest("SHA-256", ab);
2367
2457
  return new Uint8Array(hashBuffer);
2368
2458
  }
2369
2459
  async function encodeMinimal(message) {
@@ -2514,9 +2604,10 @@ function generateId6() {
2514
2604
  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("")}`;
2515
2605
  }
2516
2606
  async function computeSha256Hex(data) {
2607
+ const subtle = globalThis.crypto?.subtle ?? (await import("crypto")).webcrypto.subtle;
2517
2608
  const ab = new ArrayBuffer(data.byteLength);
2518
2609
  new Uint8Array(ab).set(data);
2519
- const hashBuffer = await crypto.subtle.digest("SHA-256", ab);
2610
+ const hashBuffer = await subtle.digest("SHA-256", ab);
2520
2611
  const hashArray = new Uint8Array(hashBuffer);
2521
2612
  return Array.from(hashArray).map((b) => b.toString(16).padStart(2, "0")).join("");
2522
2613
  }
@@ -2609,7 +2700,7 @@ async function makeTrainingDataMessage(media) {
2609
2700
  cmd: "training_data",
2610
2701
  target: "rcan://training/data",
2611
2702
  params: {
2612
- msg_type: 10 /* TRAINING_DATA */,
2703
+ msg_type: 36 /* TRAINING_DATA */,
2613
2704
  msg_id: generateId6()
2614
2705
  },
2615
2706
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
@@ -2642,7 +2733,7 @@ async function makeStreamChunk(streamId, data, mimeType, chunkIndex, isFinal) {
2642
2733
  cmd: "stream_chunk",
2643
2734
  target: "rcan://streaming/chunk",
2644
2735
  params: {
2645
- msg_type: 7 /* SENSOR_DATA */,
2736
+ msg_type: 29 /* SENSOR_DATA */,
2646
2737
  msg_id: generateId6(),
2647
2738
  stream_chunk: streamChunkMeta
2648
2739
  },
@@ -2652,23 +2743,371 @@ async function makeStreamChunk(streamId, data, mimeType, chunkIndex, isFinal) {
2652
2743
  return msg;
2653
2744
  }
2654
2745
 
2746
+ // src/contribute.ts
2747
+ var CONTRIBUTE_SCOPE_LEVEL = 2.5;
2748
+ var _idCounter = 0;
2749
+ function _generateId() {
2750
+ return `cr-${Date.now()}-${++_idCounter}`;
2751
+ }
2752
+ function makeContributeRequest(params = {}) {
2753
+ return {
2754
+ type: 33 /* CONTRIBUTE_REQUEST */,
2755
+ request_id: params.request_id ?? _generateId(),
2756
+ project_id: params.project_id ?? "",
2757
+ project_name: params.project_name ?? "",
2758
+ work_unit_id: params.work_unit_id ?? "",
2759
+ resource_type: params.resource_type ?? "cpu",
2760
+ estimated_duration_s: params.estimated_duration_s ?? 0,
2761
+ priority: params.priority ?? 0,
2762
+ payload: params.payload ?? {},
2763
+ timestamp: params.timestamp ?? Date.now() / 1e3
2764
+ };
2765
+ }
2766
+ function makeContributeResult(params = {}) {
2767
+ const result = {
2768
+ type: 34 /* CONTRIBUTE_RESULT */,
2769
+ request_id: params.request_id ?? "",
2770
+ work_unit_id: params.work_unit_id ?? "",
2771
+ status: params.status ?? "completed",
2772
+ resource_type: params.resource_type ?? "cpu",
2773
+ duration_s: params.duration_s ?? 0,
2774
+ compute_units: params.compute_units ?? 0,
2775
+ result_payload: params.result_payload ?? {},
2776
+ timestamp: params.timestamp ?? Date.now() / 1e3
2777
+ };
2778
+ if (params.error_message !== void 0) {
2779
+ result.error_message = params.error_message;
2780
+ }
2781
+ return result;
2782
+ }
2783
+ function makeContributeCancel(params = {}) {
2784
+ return {
2785
+ type: 35 /* CONTRIBUTE_CANCEL */,
2786
+ request_id: params.request_id ?? "",
2787
+ work_unit_id: params.work_unit_id ?? "",
2788
+ reason: params.reason ?? "",
2789
+ timestamp: params.timestamp ?? Date.now() / 1e3
2790
+ };
2791
+ }
2792
+ function validateContributeScope(scopeLevel, action = "request") {
2793
+ if (action === "request" || action === "result") {
2794
+ return scopeLevel >= CONTRIBUTE_SCOPE_LEVEL;
2795
+ }
2796
+ if (action === "cancel") {
2797
+ return scopeLevel >= 2;
2798
+ }
2799
+ return false;
2800
+ }
2801
+ function isPreemptedBy(scopeLevel) {
2802
+ return scopeLevel >= 3;
2803
+ }
2804
+
2805
+ // src/competition.ts
2806
+ var COMPETITION_SCOPE_LEVEL = 2;
2807
+ var _idCounter2 = 0;
2808
+ function _generateRunId() {
2809
+ return `run-${Date.now()}-${++_idCounter2}`;
2810
+ }
2811
+ function makeCompetitionEnter(params = {}) {
2812
+ return {
2813
+ type: 37 /* COMPETITION_ENTER */,
2814
+ competition_id: params.competition_id ?? "",
2815
+ competition_format: params.competition_format ?? "sprint",
2816
+ hardware_tier: params.hardware_tier ?? "",
2817
+ model_id: params.model_id ?? "",
2818
+ robot_rrn: params.robot_rrn ?? "",
2819
+ entered_at: params.entered_at ?? Date.now() / 1e3
2820
+ };
2821
+ }
2822
+ function makeCompetitionScore(params = {}) {
2823
+ const score = params.score ?? 0;
2824
+ if (score < 0 || score > 1) {
2825
+ throw new Error(`score must be in [0.0, 1.0], got ${score}`);
2826
+ }
2827
+ return {
2828
+ type: 38 /* COMPETITION_SCORE */,
2829
+ competition_id: params.competition_id ?? "",
2830
+ candidate_id: params.candidate_id ?? "",
2831
+ score,
2832
+ hardware_tier: params.hardware_tier ?? "",
2833
+ verified: params.verified ?? false,
2834
+ submitted_at: params.submitted_at ?? Date.now() / 1e3
2835
+ };
2836
+ }
2837
+ function makeSeasonStanding(params = {}) {
2838
+ return {
2839
+ type: 39 /* SEASON_STANDING */,
2840
+ season_id: params.season_id ?? "",
2841
+ class_id: params.class_id ?? "",
2842
+ standings: params.standings ?? [],
2843
+ days_remaining: params.days_remaining ?? 0,
2844
+ broadcast_at: params.broadcast_at ?? Date.now() / 1e3
2845
+ };
2846
+ }
2847
+ function makePersonalResearchResult(params = {}) {
2848
+ const score = params.score ?? 0;
2849
+ if (score < 0 || score > 1) {
2850
+ throw new Error(`score must be in [0.0, 1.0], got ${score}`);
2851
+ }
2852
+ return {
2853
+ type: 40 /* PERSONAL_RESEARCH_RESULT */,
2854
+ run_id: params.run_id ?? _generateRunId(),
2855
+ run_type: params.run_type ?? "personal",
2856
+ candidate_id: params.candidate_id ?? "",
2857
+ score,
2858
+ hardware_tier: params.hardware_tier ?? "",
2859
+ model_id: params.model_id ?? "",
2860
+ owner_uid: params.owner_uid ?? "",
2861
+ metrics: params.metrics ?? {
2862
+ success_rate: 0,
2863
+ p66_rate: 0,
2864
+ token_efficiency: 0,
2865
+ latency_score: 0
2866
+ },
2867
+ submitted_to_community: params.submitted_to_community ?? false,
2868
+ created_at: params.created_at ?? Date.now() / 1e3
2869
+ };
2870
+ }
2871
+ function validateCompetitionScope(scopeLevel) {
2872
+ return scopeLevel >= COMPETITION_SCOPE_LEVEL;
2873
+ }
2874
+
2875
+ // src/firmware.ts
2876
+ var FIRMWARE_MANIFEST_PATH = "/.well-known/rcan-firmware-manifest.json";
2877
+ function manifestToWire(m) {
2878
+ const wire = {
2879
+ rrn: m.rrn,
2880
+ firmware_version: m.firmwareVersion,
2881
+ build_hash: m.buildHash,
2882
+ components: m.components,
2883
+ signed_at: m.signedAt
2884
+ };
2885
+ if (m.signature) wire.signature = m.signature;
2886
+ return wire;
2887
+ }
2888
+ function manifestFromWire(w) {
2889
+ return {
2890
+ rrn: w.rrn,
2891
+ firmwareVersion: w.firmware_version,
2892
+ buildHash: w.build_hash,
2893
+ components: w.components ?? [],
2894
+ signedAt: w.signed_at ?? "",
2895
+ signature: w.signature
2896
+ };
2897
+ }
2898
+ function canonicalManifestJson(m) {
2899
+ const obj = {
2900
+ build_hash: m.buildHash,
2901
+ components: m.components.map((c) => ({
2902
+ hash: c.hash,
2903
+ name: c.name,
2904
+ version: c.version
2905
+ })),
2906
+ firmware_version: m.firmwareVersion,
2907
+ rrn: m.rrn,
2908
+ signed_at: m.signedAt
2909
+ };
2910
+ return JSON.stringify(obj);
2911
+ }
2912
+ var FirmwareIntegrityError = class extends Error {
2913
+ constructor(message) {
2914
+ super(message);
2915
+ this.name = "FirmwareIntegrityError";
2916
+ }
2917
+ };
2918
+ function validateManifest(m) {
2919
+ const errors = [];
2920
+ if (!m.rrn) errors.push("rrn is required");
2921
+ if (!m.firmwareVersion) errors.push("firmwareVersion is required");
2922
+ if (!m.buildHash) errors.push("buildHash is required");
2923
+ if (!m.buildHash.startsWith("sha256:")) errors.push("buildHash must start with 'sha256:'");
2924
+ if (!m.signedAt) errors.push("signedAt is required");
2925
+ if (!m.signature) errors.push("signature is required (manifest must be signed)");
2926
+ for (const [i, c] of m.components.entries()) {
2927
+ if (!c.name) errors.push(`components[${i}].name is required`);
2928
+ if (!c.version) errors.push(`components[${i}].version is required`);
2929
+ if (!c.hash.startsWith("sha256:")) errors.push(`components[${i}].hash must start with 'sha256:'`);
2930
+ }
2931
+ return errors;
2932
+ }
2933
+
2934
+ // src/authority.ts
2935
+ function authorityAccessToWire(p) {
2936
+ return {
2937
+ request_id: p.requestId,
2938
+ authority_id: p.authorityId,
2939
+ requested_data: p.requestedData,
2940
+ justification: p.justification,
2941
+ expires_at: p.expiresAt
2942
+ };
2943
+ }
2944
+ function authorityAccessFromWire(w) {
2945
+ return {
2946
+ requestId: w.request_id,
2947
+ authorityId: w.authority_id,
2948
+ requestedData: w.requested_data ?? [],
2949
+ justification: w.justification ?? "",
2950
+ expiresAt: w.expires_at ?? 0
2951
+ };
2952
+ }
2953
+ function validateAuthorityAccess(p) {
2954
+ const errors = [];
2955
+ if (!p.requestId) errors.push("requestId is required");
2956
+ if (!p.authorityId) errors.push("authorityId is required");
2957
+ if (!p.requestedData || p.requestedData.length === 0)
2958
+ errors.push("requestedData must include at least one category");
2959
+ if (!p.justification) errors.push("justification is required");
2960
+ if (!p.expiresAt || p.expiresAt <= 0) errors.push("expiresAt must be a positive Unix timestamp");
2961
+ if (p.expiresAt < Date.now() / 1e3) errors.push("expiresAt is in the past \u2014 request has expired");
2962
+ return errors;
2963
+ }
2964
+ function isAuthorityRequestValid(p) {
2965
+ return Date.now() / 1e3 < p.expiresAt && validateAuthorityAccess(p).length === 0;
2966
+ }
2967
+ var AUTHORITY_ERROR_CODES = {
2968
+ NOT_RECOGNIZED: "AUTHORITY_NOT_RECOGNIZED",
2969
+ REQUEST_EXPIRED: "AUTHORITY_REQUEST_EXPIRED",
2970
+ INVALID_TOKEN: "AUTHORITY_INVALID_TOKEN",
2971
+ RATE_LIMITED: "AUTHORITY_RATE_LIMITED"
2972
+ };
2973
+
2974
+ // src/m2m.ts
2975
+ var RRF_REVOCATION_URL = "https://api.rrf.rcan.dev/v2/revocations";
2976
+ var M2M_TRUSTED_ISSUER = "rrf.rcan.dev";
2977
+ var RRF_REVOCATION_CACHE_TTL_MS = 55e3;
2978
+ var M2MAuthError = class extends Error {
2979
+ constructor(message) {
2980
+ super(message);
2981
+ this.name = "M2MAuthError";
2982
+ }
2983
+ };
2984
+ function decodeJwtPayload2(token) {
2985
+ const parts = token.split(".");
2986
+ if (parts.length < 2) throw new M2MAuthError("Invalid JWT structure");
2987
+ const b64 = (parts[1] ?? "").replace(/-/g, "+").replace(/_/g, "/");
2988
+ const padded = b64 + "=".repeat((4 - b64.length % 4) % 4);
2989
+ try {
2990
+ return JSON.parse(atob(padded));
2991
+ } catch (e) {
2992
+ throw new M2MAuthError(`JWT payload decode failed: ${String(e)}`);
2993
+ }
2994
+ }
2995
+ function parseM2mPeerToken(token) {
2996
+ const payload = decodeJwtPayload2(token);
2997
+ const exp = Number(payload["exp"] ?? 0);
2998
+ if (exp > 0 && Date.now() / 1e3 > exp) {
2999
+ throw new M2MAuthError(`M2M_PEER token expired (sub=${String(payload["sub"])})`);
3000
+ }
3001
+ const peerRrn = String(payload["peer_rrn"] ?? "");
3002
+ if (!peerRrn) throw new M2MAuthError("M2M_PEER token missing peer_rrn claim");
3003
+ return {
3004
+ sub: String(payload["sub"] ?? ""),
3005
+ peerRrn,
3006
+ scopes: Array.isArray(payload["rcan_scopes"]) ? payload["rcan_scopes"] : Array.isArray(payload["scopes"]) ? payload["scopes"] : [],
3007
+ exp,
3008
+ iss: String(payload["iss"] ?? "")
3009
+ };
3010
+ }
3011
+ function parseM2mTrustedToken(token) {
3012
+ const payload = decodeJwtPayload2(token);
3013
+ const iss = String(payload["iss"] ?? "");
3014
+ if (iss !== M2M_TRUSTED_ISSUER) {
3015
+ throw new M2MAuthError(
3016
+ `M2M_TRUSTED issuer must be '${M2M_TRUSTED_ISSUER}', got '${iss}'`
3017
+ );
3018
+ }
3019
+ const scopes = Array.isArray(payload["rcan_scopes"]) ? payload["rcan_scopes"] : Array.isArray(payload["scopes"]) ? payload["scopes"] : [];
3020
+ if (!scopes.includes("fleet.trusted")) {
3021
+ throw new M2MAuthError("M2M_TRUSTED token missing required 'fleet.trusted' scope");
3022
+ }
3023
+ const exp = Number(payload["exp"] ?? 0);
3024
+ if (exp > 0 && Date.now() / 1e3 > exp) {
3025
+ throw new M2MAuthError(`M2M_TRUSTED token expired (sub=${String(payload["sub"])})`);
3026
+ }
3027
+ const rrfSig = String(payload["rrf_sig"] ?? "");
3028
+ if (!rrfSig) throw new M2MAuthError("M2M_TRUSTED token missing rrf_sig claim");
3029
+ const fleetRrns = Array.isArray(payload["fleet_rrns"]) ? payload["fleet_rrns"] : [];
3030
+ return {
3031
+ sub: String(payload["sub"] ?? ""),
3032
+ fleetRrns,
3033
+ scopes,
3034
+ exp,
3035
+ iss,
3036
+ rrfSig
3037
+ };
3038
+ }
3039
+ function verifyM2mTrustedTokenClaims(token, targetRrn) {
3040
+ const claims = parseM2mTrustedToken(token);
3041
+ if (!claims.fleetRrns.includes(targetRrn)) {
3042
+ throw new M2MAuthError(
3043
+ `M2M_TRUSTED token does not authorize commanding '${targetRrn}'. Authorized fleet: [${claims.fleetRrns.join(", ")}]`
3044
+ );
3045
+ }
3046
+ return claims;
3047
+ }
3048
+ var _revocationCache = null;
3049
+ async function fetchRRFRevocations(url = RRF_REVOCATION_URL) {
3050
+ const now = Date.now();
3051
+ if (_revocationCache && now - _revocationCache.fetchedAt < RRF_REVOCATION_CACHE_TTL_MS) {
3052
+ return _revocationCache;
3053
+ }
3054
+ try {
3055
+ const resp = await fetch(url, { signal: AbortSignal.timeout?.(5e3) });
3056
+ const data = await resp.json();
3057
+ _revocationCache = {
3058
+ revokedOrchestrators: new Set(data.revoked_orchestrators ?? []),
3059
+ revokedJtis: new Set(data.revoked_jtis ?? []),
3060
+ fetchedAt: now
3061
+ };
3062
+ } catch {
3063
+ if (_revocationCache) return _revocationCache;
3064
+ _revocationCache = { revokedOrchestrators: /* @__PURE__ */ new Set(), revokedJtis: /* @__PURE__ */ new Set(), fetchedAt: now };
3065
+ }
3066
+ return _revocationCache;
3067
+ }
3068
+ async function isM2mTrustedRevoked(claims, jti) {
3069
+ const cache = await fetchRRFRevocations();
3070
+ if (cache.revokedOrchestrators.has(claims.sub)) return true;
3071
+ if (jti && cache.revokedJtis.has(jti)) return true;
3072
+ return false;
3073
+ }
3074
+ async function verifyM2mTrustedToken(token, targetRrn, options) {
3075
+ const claims = verifyM2mTrustedTokenClaims(token, targetRrn);
3076
+ if (!options?.skipRevocationCheck) {
3077
+ const revoked = await isM2mTrustedRevoked(claims);
3078
+ if (revoked) {
3079
+ throw new M2MAuthError(
3080
+ `M2M_TRUSTED orchestrator '${claims.sub}' is on the RRF revocation list`
3081
+ );
3082
+ }
3083
+ }
3084
+ return claims;
3085
+ }
3086
+
2655
3087
  // src/index.ts
2656
3088
  var VERSION = "0.6.0";
2657
3089
  var RCAN_VERSION = "1.6";
2658
3090
  export {
3091
+ AUTHORITY_ERROR_CODES,
2659
3092
  AuditChain,
2660
3093
  AuditError,
3094
+ COMPETITION_SCOPE_LEVEL,
3095
+ CONTRIBUTE_SCOPE_LEVEL,
2661
3096
  ClockDriftError,
2662
3097
  CommitmentRecord,
2663
3098
  ConfidenceGate,
2664
3099
  DEFAULT_LOA_POLICY,
2665
3100
  DataCategory,
3101
+ FIRMWARE_MANIFEST_PATH,
2666
3102
  FaultCode,
2667
3103
  FederationSyncType,
3104
+ FirmwareIntegrityError,
2668
3105
  GateError,
2669
3106
  HiTLGate,
2670
3107
  KeyStore,
2671
3108
  LevelOfAssurance,
3109
+ M2MAuthError,
3110
+ M2M_TRUSTED_ISSUER,
2672
3111
  MediaEncoding,
2673
3112
  MessageType,
2674
3113
  NodeClient,
@@ -2694,13 +3133,18 @@ export {
2694
3133
  RCANValidationError,
2695
3134
  RCANVersionIncompatibleError,
2696
3135
  RCAN_VERSION,
3136
+ ROLE_JWT_LEVEL,
3137
+ RRF_REVOCATION_CACHE_TTL_MS,
3138
+ RRF_REVOCATION_URL,
2697
3139
  RegistryClient,
2698
3140
  RegistryTier,
2699
3141
  ReplayCache,
2700
3142
  RevocationCache,
2701
3143
  RobotURI,
2702
3144
  RobotURIError,
3145
+ Role,
2703
3146
  SAFETY_MESSAGE_TYPE,
3147
+ SCOPE_MIN_ROLE,
2704
3148
  SDK_VERSION,
2705
3149
  SPEC_VERSION,
2706
3150
  TransportEncoding,
@@ -2711,6 +3155,9 @@ export {
2711
3155
  addMediaInline,
2712
3156
  addMediaRef,
2713
3157
  assertClockSynced,
3158
+ authorityAccessFromWire,
3159
+ authorityAccessToWire,
3160
+ canonicalManifestJson,
2714
3161
  checkClockSync,
2715
3162
  checkRevocation,
2716
3163
  decodeBleFrames,
@@ -2719,21 +3166,34 @@ export {
2719
3166
  encodeBleFrames,
2720
3167
  encodeCompact,
2721
3168
  encodeMinimal,
3169
+ extractIdentityFromJwt,
2722
3170
  extractLoaFromJwt,
3171
+ extractRoleFromJwt,
2723
3172
  fetchCanonicalSchema,
3173
+ fetchRRFRevocations,
3174
+ isAuthorityRequestValid,
3175
+ isM2mTrustedRevoked,
3176
+ isPreemptedBy,
2724
3177
  isSafetyMessage,
2725
3178
  makeCloudRelayMessage,
3179
+ makeCompetitionEnter,
3180
+ makeCompetitionScore,
2726
3181
  makeConfigUpdate,
2727
3182
  makeConsentDeny,
2728
3183
  makeConsentGrant,
2729
3184
  makeConsentRequest,
3185
+ makeContributeCancel,
3186
+ makeContributeRequest,
3187
+ makeContributeResult,
2730
3188
  makeEstopMessage,
2731
3189
  makeEstopWithQoS,
2732
3190
  makeFaultReport,
2733
3191
  makeFederationSync,
2734
3192
  makeKeyRotationMessage,
3193
+ makePersonalResearchResult,
2735
3194
  makeResumeMessage,
2736
3195
  makeRevocationBroadcast,
3196
+ makeSeasonStanding,
2737
3197
  makeStopMessage,
2738
3198
  makeStreamChunk,
2739
3199
  makeTrainingConsentDeny,
@@ -2741,21 +3201,33 @@ export {
2741
3201
  makeTrainingConsentRequest,
2742
3202
  makeTrainingDataMessage,
2743
3203
  makeTransparencyMessage,
3204
+ manifestFromWire,
3205
+ manifestToWire,
3206
+ parseM2mPeerToken,
3207
+ parseM2mTrustedToken,
3208
+ roleFromJwtLevel,
2744
3209
  selectTransport,
3210
+ validateAuthorityAccess,
3211
+ validateCompetitionScope,
2745
3212
  validateConfig,
2746
3213
  validateConfigAgainstSchema,
2747
3214
  validateConfigUpdate,
2748
3215
  validateConsentMessage,
3216
+ validateContributeScope,
2749
3217
  validateCrossRegistryCommand,
2750
3218
  validateDelegationChain,
2751
3219
  validateLoaForScope,
3220
+ validateManifest,
2752
3221
  validateMediaChunks,
2753
3222
  validateMessage,
2754
3223
  validateNodeAgainstSchema,
2755
3224
  validateReplay,
3225
+ validateRoleForScope,
2756
3226
  validateSafetyMessage,
2757
3227
  validateTrainingDataMessage,
2758
3228
  validateURI,
2759
- validateVersionCompat
3229
+ validateVersionCompat,
3230
+ verifyM2mTrustedToken,
3231
+ verifyM2mTrustedTokenClaims
2760
3232
  };
2761
3233
  //# sourceMappingURL=index.mjs.map