@continuonai/rcan-ts 0.8.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/README.md +1 -0
- package/dist/browser.d.mts +395 -39
- package/dist/browser.mjs +462 -73
- package/dist/browser.mjs.map +1 -1
- package/dist/index.d.mts +395 -39
- package/dist/index.d.ts +395 -39
- package/dist/index.js +497 -74
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +462 -73
- package/dist/index.mjs.map +1 -1
- package/dist/rcan-validate.js +17 -2
- package/dist/rcan.iife.js +3 -3
- package/package.json +1 -1
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.
|
|
103
|
-
var SDK_VERSION = "
|
|
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(".");
|
|
@@ -151,9 +151,14 @@ var MessageType = /* @__PURE__ */ ((MessageType2) => {
|
|
|
151
151
|
MessageType2[MessageType2["CONTRIBUTE_RESULT"] = 34] = "CONTRIBUTE_RESULT";
|
|
152
152
|
MessageType2[MessageType2["CONTRIBUTE_CANCEL"] = 35] = "CONTRIBUTE_CANCEL";
|
|
153
153
|
MessageType2[MessageType2["TRAINING_DATA"] = 36] = "TRAINING_DATA";
|
|
154
|
-
MessageType2[MessageType2["
|
|
155
|
-
MessageType2[MessageType2["
|
|
156
|
-
MessageType2[MessageType2["
|
|
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";
|
|
157
162
|
return MessageType2;
|
|
158
163
|
})(MessageType || {});
|
|
159
164
|
var RCANMessageError = class extends Error {
|
|
@@ -190,6 +195,10 @@ var RCANMessage = class _RCANMessage {
|
|
|
190
195
|
__publicField(this, "transportEncoding");
|
|
191
196
|
/** v1.6: GAP-18 multi-modal media chunks */
|
|
192
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");
|
|
193
202
|
if (!data.cmd || data.cmd.trim() === "") {
|
|
194
203
|
throw new RCANMessageError("'cmd' is required");
|
|
195
204
|
}
|
|
@@ -217,6 +226,13 @@ var RCANMessage = class _RCANMessage {
|
|
|
217
226
|
this.loa = data.loa;
|
|
218
227
|
this.transportEncoding = data.transportEncoding;
|
|
219
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
|
+
}
|
|
220
236
|
if (this.confidence !== void 0) {
|
|
221
237
|
if (this.confidence < 0 || this.confidence > 1) {
|
|
222
238
|
throw new RCANMessageError(
|
|
@@ -258,6 +274,8 @@ var RCANMessage = class _RCANMessage {
|
|
|
258
274
|
if (this.loa !== void 0) obj.loa = this.loa;
|
|
259
275
|
if (this.transportEncoding !== void 0) obj.transportEncoding = this.transportEncoding;
|
|
260
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;
|
|
261
279
|
return obj;
|
|
262
280
|
}
|
|
263
281
|
/** Serialize to JSON string */
|
|
@@ -300,7 +318,9 @@ var RCANMessage = class _RCANMessage {
|
|
|
300
318
|
readOnly: obj.readOnly,
|
|
301
319
|
loa: obj.loa,
|
|
302
320
|
transportEncoding: obj.transportEncoding,
|
|
303
|
-
mediaChunks: obj.mediaChunks
|
|
321
|
+
mediaChunks: obj.mediaChunks,
|
|
322
|
+
firmwareHash: obj.firmwareHash,
|
|
323
|
+
attestationRef: obj.attestationRef
|
|
304
324
|
});
|
|
305
325
|
}
|
|
306
326
|
};
|
|
@@ -2060,80 +2080,133 @@ function makeFaultReport(params) {
|
|
|
2060
2080
|
}
|
|
2061
2081
|
|
|
2062
2082
|
// src/identity.ts
|
|
2063
|
-
var
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
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
|
+
};
|
|
2069
2128
|
var DEFAULT_LOA_POLICY = {
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2129
|
+
minRoleForDiscover: 1 /* GUEST */,
|
|
2130
|
+
minRoleForStatus: 1 /* GUEST */,
|
|
2131
|
+
minRoleForChat: 1 /* GUEST */,
|
|
2132
|
+
minRoleForControl: 1 /* GUEST */,
|
|
2133
|
+
minRoleForSafety: 1 /* GUEST */
|
|
2075
2134
|
};
|
|
2076
2135
|
var PRODUCTION_LOA_POLICY = {
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2136
|
+
minRoleForDiscover: 1 /* GUEST */,
|
|
2137
|
+
minRoleForStatus: 1 /* GUEST */,
|
|
2138
|
+
minRoleForChat: 1 /* GUEST */,
|
|
2139
|
+
minRoleForControl: 2 /* OPERATOR */,
|
|
2140
|
+
minRoleForSafety: 6 /* CREATOR */
|
|
2082
2141
|
};
|
|
2083
|
-
function
|
|
2142
|
+
function decodeJwtPayload(token) {
|
|
2084
2143
|
try {
|
|
2085
2144
|
const parts = token.split(".");
|
|
2086
|
-
if (parts.length < 2) return
|
|
2145
|
+
if (parts.length < 2) return null;
|
|
2087
2146
|
const payloadB64 = (parts[1] ?? "").replace(/-/g, "+").replace(/_/g, "/");
|
|
2088
2147
|
const padded = payloadB64 + "=".repeat((4 - payloadB64.length % 4) % 4);
|
|
2089
|
-
|
|
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
|
-
}
|
|
2148
|
+
return JSON.parse(atob(padded));
|
|
2100
2149
|
} catch {
|
|
2150
|
+
return null;
|
|
2101
2151
|
}
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
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" };
|
|
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;
|
|
2131
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"] : [];
|
|
2132
2181
|
return {
|
|
2133
|
-
|
|
2134
|
-
|
|
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
|
|
2135
2190
|
};
|
|
2136
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
|
+
};
|
|
2200
|
+
}
|
|
2201
|
+
if (role >= required) return { ok: true, reason: "" };
|
|
2202
|
+
return {
|
|
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]})`
|
|
2205
|
+
};
|
|
2206
|
+
}
|
|
2207
|
+
function validateLoaForScope(role, scope) {
|
|
2208
|
+
return validateRoleForScope(role, scope);
|
|
2209
|
+
}
|
|
2137
2210
|
|
|
2138
2211
|
// src/federation.ts
|
|
2139
2212
|
var RegistryTier = /* @__PURE__ */ ((RegistryTier2) => {
|
|
@@ -2259,12 +2332,12 @@ function generateId5() {
|
|
|
2259
2332
|
}
|
|
2260
2333
|
function makeFederationSync(source, target, syncType, payload) {
|
|
2261
2334
|
return new RCANMessage({
|
|
2262
|
-
rcan: "1.
|
|
2263
|
-
rcanVersion: "1.
|
|
2335
|
+
rcan: "2.1.0",
|
|
2336
|
+
rcanVersion: "2.1.0",
|
|
2264
2337
|
cmd: "federation_sync",
|
|
2265
2338
|
target,
|
|
2266
2339
|
params: {
|
|
2267
|
-
msg_type: 23 /*
|
|
2340
|
+
msg_type: 23 /* FLEET_COMMAND */,
|
|
2268
2341
|
msg_id: generateId5(),
|
|
2269
2342
|
source_registry: source,
|
|
2270
2343
|
target_registry: target,
|
|
@@ -2291,17 +2364,17 @@ async function validateCrossRegistryCommand(msg, localRegistry, trustCache) {
|
|
|
2291
2364
|
reason: `REGISTRY_UNKNOWN: ${sourceRegistry} is not in the local trust cache`
|
|
2292
2365
|
};
|
|
2293
2366
|
}
|
|
2294
|
-
let loa = 1 /*
|
|
2367
|
+
let loa = 1 /* GUEST */;
|
|
2295
2368
|
const registryJwt = msg.params?.["registry_jwt"];
|
|
2296
2369
|
if (registryJwt) {
|
|
2297
2370
|
loa = extractLoaFromJwt(registryJwt);
|
|
2298
2371
|
} else if (typeof msg.loa === "number") {
|
|
2299
2372
|
loa = msg.loa;
|
|
2300
2373
|
}
|
|
2301
|
-
if (loa < 2 /*
|
|
2374
|
+
if (loa < 2 /* OPERATOR */) {
|
|
2302
2375
|
return {
|
|
2303
2376
|
valid: false,
|
|
2304
|
-
reason: `LOA_INSUFFICIENT: cross-registry commands require LoA>=2 (
|
|
2377
|
+
reason: `LOA_INSUFFICIENT: cross-registry commands require LoA>=2 (OPERATOR), got role=${loa}`
|
|
2305
2378
|
};
|
|
2306
2379
|
}
|
|
2307
2380
|
return { valid: true, reason: "cross-registry command accepted" };
|
|
@@ -2740,24 +2813,312 @@ function isPreemptedBy(scopeLevel) {
|
|
|
2740
2813
|
return scopeLevel >= 3;
|
|
2741
2814
|
}
|
|
2742
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
|
+
|
|
2743
3098
|
// src/index.ts
|
|
2744
3099
|
var VERSION = "0.6.0";
|
|
2745
3100
|
var RCAN_VERSION = "1.6";
|
|
2746
3101
|
export {
|
|
3102
|
+
AUTHORITY_ERROR_CODES,
|
|
2747
3103
|
AuditChain,
|
|
2748
3104
|
AuditError,
|
|
3105
|
+
COMPETITION_SCOPE_LEVEL,
|
|
2749
3106
|
CONTRIBUTE_SCOPE_LEVEL,
|
|
2750
3107
|
ClockDriftError,
|
|
2751
3108
|
CommitmentRecord,
|
|
2752
3109
|
ConfidenceGate,
|
|
2753
3110
|
DEFAULT_LOA_POLICY,
|
|
2754
3111
|
DataCategory,
|
|
3112
|
+
FIRMWARE_MANIFEST_PATH,
|
|
2755
3113
|
FaultCode,
|
|
2756
3114
|
FederationSyncType,
|
|
3115
|
+
FirmwareIntegrityError,
|
|
2757
3116
|
GateError,
|
|
2758
3117
|
HiTLGate,
|
|
2759
3118
|
KeyStore,
|
|
2760
3119
|
LevelOfAssurance,
|
|
3120
|
+
M2MAuthError,
|
|
3121
|
+
M2M_TRUSTED_ISSUER,
|
|
2761
3122
|
MediaEncoding,
|
|
2762
3123
|
MessageType,
|
|
2763
3124
|
NodeClient,
|
|
@@ -2783,13 +3144,18 @@ export {
|
|
|
2783
3144
|
RCANValidationError,
|
|
2784
3145
|
RCANVersionIncompatibleError,
|
|
2785
3146
|
RCAN_VERSION,
|
|
3147
|
+
ROLE_JWT_LEVEL,
|
|
3148
|
+
RRF_REVOCATION_CACHE_TTL_MS,
|
|
3149
|
+
RRF_REVOCATION_URL,
|
|
2786
3150
|
RegistryClient,
|
|
2787
3151
|
RegistryTier,
|
|
2788
3152
|
ReplayCache,
|
|
2789
3153
|
RevocationCache,
|
|
2790
3154
|
RobotURI,
|
|
2791
3155
|
RobotURIError,
|
|
3156
|
+
Role,
|
|
2792
3157
|
SAFETY_MESSAGE_TYPE,
|
|
3158
|
+
SCOPE_MIN_ROLE,
|
|
2793
3159
|
SDK_VERSION,
|
|
2794
3160
|
SPEC_VERSION,
|
|
2795
3161
|
TransportEncoding,
|
|
@@ -2800,6 +3166,9 @@ export {
|
|
|
2800
3166
|
addMediaInline,
|
|
2801
3167
|
addMediaRef,
|
|
2802
3168
|
assertClockSynced,
|
|
3169
|
+
authorityAccessFromWire,
|
|
3170
|
+
authorityAccessToWire,
|
|
3171
|
+
canonicalManifestJson,
|
|
2803
3172
|
checkClockSync,
|
|
2804
3173
|
checkRevocation,
|
|
2805
3174
|
decodeBleFrames,
|
|
@@ -2808,11 +3177,18 @@ export {
|
|
|
2808
3177
|
encodeBleFrames,
|
|
2809
3178
|
encodeCompact,
|
|
2810
3179
|
encodeMinimal,
|
|
3180
|
+
extractIdentityFromJwt,
|
|
2811
3181
|
extractLoaFromJwt,
|
|
3182
|
+
extractRoleFromJwt,
|
|
2812
3183
|
fetchCanonicalSchema,
|
|
3184
|
+
fetchRRFRevocations,
|
|
3185
|
+
isAuthorityRequestValid,
|
|
3186
|
+
isM2mTrustedRevoked,
|
|
2813
3187
|
isPreemptedBy,
|
|
2814
3188
|
isSafetyMessage,
|
|
2815
3189
|
makeCloudRelayMessage,
|
|
3190
|
+
makeCompetitionEnter,
|
|
3191
|
+
makeCompetitionScore,
|
|
2816
3192
|
makeConfigUpdate,
|
|
2817
3193
|
makeConsentDeny,
|
|
2818
3194
|
makeConsentGrant,
|
|
@@ -2825,8 +3201,10 @@ export {
|
|
|
2825
3201
|
makeFaultReport,
|
|
2826
3202
|
makeFederationSync,
|
|
2827
3203
|
makeKeyRotationMessage,
|
|
3204
|
+
makePersonalResearchResult,
|
|
2828
3205
|
makeResumeMessage,
|
|
2829
3206
|
makeRevocationBroadcast,
|
|
3207
|
+
makeSeasonStanding,
|
|
2830
3208
|
makeStopMessage,
|
|
2831
3209
|
makeStreamChunk,
|
|
2832
3210
|
makeTrainingConsentDeny,
|
|
@@ -2834,7 +3212,14 @@ export {
|
|
|
2834
3212
|
makeTrainingConsentRequest,
|
|
2835
3213
|
makeTrainingDataMessage,
|
|
2836
3214
|
makeTransparencyMessage,
|
|
3215
|
+
manifestFromWire,
|
|
3216
|
+
manifestToWire,
|
|
3217
|
+
parseM2mPeerToken,
|
|
3218
|
+
parseM2mTrustedToken,
|
|
3219
|
+
roleFromJwtLevel,
|
|
2837
3220
|
selectTransport,
|
|
3221
|
+
validateAuthorityAccess,
|
|
3222
|
+
validateCompetitionScope,
|
|
2838
3223
|
validateConfig,
|
|
2839
3224
|
validateConfigAgainstSchema,
|
|
2840
3225
|
validateConfigUpdate,
|
|
@@ -2843,13 +3228,17 @@ export {
|
|
|
2843
3228
|
validateCrossRegistryCommand,
|
|
2844
3229
|
validateDelegationChain,
|
|
2845
3230
|
validateLoaForScope,
|
|
3231
|
+
validateManifest,
|
|
2846
3232
|
validateMediaChunks,
|
|
2847
3233
|
validateMessage,
|
|
2848
3234
|
validateNodeAgainstSchema,
|
|
2849
3235
|
validateReplay,
|
|
3236
|
+
validateRoleForScope,
|
|
2850
3237
|
validateSafetyMessage,
|
|
2851
3238
|
validateTrainingDataMessage,
|
|
2852
3239
|
validateURI,
|
|
2853
|
-
validateVersionCompat
|
|
3240
|
+
validateVersionCompat,
|
|
3241
|
+
verifyM2mTrustedToken,
|
|
3242
|
+
verifyM2mTrustedTokenClaims
|
|
2854
3243
|
};
|
|
2855
3244
|
//# sourceMappingURL=browser.mjs.map
|