@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/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.
|
|
100
|
-
var SDK_VERSION = "
|
|
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(".");
|
|
@@ -148,9 +148,14 @@ var MessageType = /* @__PURE__ */ ((MessageType2) => {
|
|
|
148
148
|
MessageType2[MessageType2["CONTRIBUTE_RESULT"] = 34] = "CONTRIBUTE_RESULT";
|
|
149
149
|
MessageType2[MessageType2["CONTRIBUTE_CANCEL"] = 35] = "CONTRIBUTE_CANCEL";
|
|
150
150
|
MessageType2[MessageType2["TRAINING_DATA"] = 36] = "TRAINING_DATA";
|
|
151
|
-
MessageType2[MessageType2["
|
|
152
|
-
MessageType2[MessageType2["
|
|
153
|
-
MessageType2[MessageType2["
|
|
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";
|
|
154
159
|
return MessageType2;
|
|
155
160
|
})(MessageType || {});
|
|
156
161
|
var RCANMessageError = class extends Error {
|
|
@@ -186,6 +191,10 @@ var RCANMessage = class _RCANMessage {
|
|
|
186
191
|
transportEncoding;
|
|
187
192
|
/** v1.6: GAP-18 multi-modal media chunks */
|
|
188
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;
|
|
189
198
|
constructor(data) {
|
|
190
199
|
if (!data.cmd || data.cmd.trim() === "") {
|
|
191
200
|
throw new RCANMessageError("'cmd' is required");
|
|
@@ -214,6 +223,13 @@ var RCANMessage = class _RCANMessage {
|
|
|
214
223
|
this.loa = data.loa;
|
|
215
224
|
this.transportEncoding = data.transportEncoding;
|
|
216
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
|
+
}
|
|
217
233
|
if (this.confidence !== void 0) {
|
|
218
234
|
if (this.confidence < 0 || this.confidence > 1) {
|
|
219
235
|
throw new RCANMessageError(
|
|
@@ -255,6 +271,8 @@ var RCANMessage = class _RCANMessage {
|
|
|
255
271
|
if (this.loa !== void 0) obj.loa = this.loa;
|
|
256
272
|
if (this.transportEncoding !== void 0) obj.transportEncoding = this.transportEncoding;
|
|
257
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;
|
|
258
276
|
return obj;
|
|
259
277
|
}
|
|
260
278
|
/** Serialize to JSON string */
|
|
@@ -297,7 +315,9 @@ var RCANMessage = class _RCANMessage {
|
|
|
297
315
|
readOnly: obj.readOnly,
|
|
298
316
|
loa: obj.loa,
|
|
299
317
|
transportEncoding: obj.transportEncoding,
|
|
300
|
-
mediaChunks: obj.mediaChunks
|
|
318
|
+
mediaChunks: obj.mediaChunks,
|
|
319
|
+
firmwareHash: obj.firmwareHash,
|
|
320
|
+
attestationRef: obj.attestationRef
|
|
301
321
|
});
|
|
302
322
|
}
|
|
303
323
|
};
|
|
@@ -2051,80 +2071,133 @@ function makeFaultReport(params) {
|
|
|
2051
2071
|
}
|
|
2052
2072
|
|
|
2053
2073
|
// src/identity.ts
|
|
2054
|
-
var
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
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
|
+
};
|
|
2060
2119
|
var DEFAULT_LOA_POLICY = {
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2120
|
+
minRoleForDiscover: 1 /* GUEST */,
|
|
2121
|
+
minRoleForStatus: 1 /* GUEST */,
|
|
2122
|
+
minRoleForChat: 1 /* GUEST */,
|
|
2123
|
+
minRoleForControl: 1 /* GUEST */,
|
|
2124
|
+
minRoleForSafety: 1 /* GUEST */
|
|
2066
2125
|
};
|
|
2067
2126
|
var PRODUCTION_LOA_POLICY = {
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2127
|
+
minRoleForDiscover: 1 /* GUEST */,
|
|
2128
|
+
minRoleForStatus: 1 /* GUEST */,
|
|
2129
|
+
minRoleForChat: 1 /* GUEST */,
|
|
2130
|
+
minRoleForControl: 2 /* OPERATOR */,
|
|
2131
|
+
minRoleForSafety: 6 /* CREATOR */
|
|
2073
2132
|
};
|
|
2074
|
-
function
|
|
2133
|
+
function decodeJwtPayload(token) {
|
|
2075
2134
|
try {
|
|
2076
2135
|
const parts = token.split(".");
|
|
2077
|
-
if (parts.length < 2) return
|
|
2136
|
+
if (parts.length < 2) return null;
|
|
2078
2137
|
const payloadB64 = (parts[1] ?? "").replace(/-/g, "+").replace(/_/g, "/");
|
|
2079
2138
|
const padded = payloadB64 + "=".repeat((4 - payloadB64.length % 4) % 4);
|
|
2080
|
-
|
|
2081
|
-
if (typeof atob !== "undefined") {
|
|
2082
|
-
json = atob(padded);
|
|
2083
|
-
} else {
|
|
2084
|
-
json = Buffer.from(padded, "base64").toString("utf-8");
|
|
2085
|
-
}
|
|
2086
|
-
const claims = JSON.parse(json);
|
|
2087
|
-
const loa = claims["loa"];
|
|
2088
|
-
if (typeof loa === "number" && loa >= 1 && loa <= 3) {
|
|
2089
|
-
return loa;
|
|
2090
|
-
}
|
|
2139
|
+
return JSON.parse(atob(padded));
|
|
2091
2140
|
} catch {
|
|
2141
|
+
return null;
|
|
2092
2142
|
}
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
// v1.7: between chat and control
|
|
2107
|
-
case "control":
|
|
2108
|
-
return policy.minLoaControl;
|
|
2109
|
-
case "safety":
|
|
2110
|
-
return policy.minLoaSafety;
|
|
2111
|
-
default:
|
|
2112
|
-
return null;
|
|
2113
|
-
}
|
|
2114
|
-
}
|
|
2115
|
-
function validateLoaForScope(loa, scope, policy = DEFAULT_LOA_POLICY) {
|
|
2116
|
-
const min = minLoaForScope(scope, policy);
|
|
2117
|
-
if (min === null) {
|
|
2118
|
-
return { valid: true, reason: "unknown scope; allowed by default" };
|
|
2119
|
-
}
|
|
2120
|
-
if (loa >= min) {
|
|
2121
|
-
return { valid: true, reason: "ok" };
|
|
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;
|
|
2122
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"] : [];
|
|
2123
2172
|
return {
|
|
2124
|
-
|
|
2125
|
-
|
|
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
|
|
2126
2181
|
};
|
|
2127
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
|
+
};
|
|
2191
|
+
}
|
|
2192
|
+
if (role >= required) return { ok: true, reason: "" };
|
|
2193
|
+
return {
|
|
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]})`
|
|
2196
|
+
};
|
|
2197
|
+
}
|
|
2198
|
+
function validateLoaForScope(role, scope) {
|
|
2199
|
+
return validateRoleForScope(role, scope);
|
|
2200
|
+
}
|
|
2128
2201
|
|
|
2129
2202
|
// src/federation.ts
|
|
2130
2203
|
var RegistryTier = /* @__PURE__ */ ((RegistryTier2) => {
|
|
@@ -2248,12 +2321,12 @@ function generateId5() {
|
|
|
2248
2321
|
}
|
|
2249
2322
|
function makeFederationSync(source, target, syncType, payload) {
|
|
2250
2323
|
return new RCANMessage({
|
|
2251
|
-
rcan: "1.
|
|
2252
|
-
rcanVersion: "1.
|
|
2324
|
+
rcan: "2.1.0",
|
|
2325
|
+
rcanVersion: "2.1.0",
|
|
2253
2326
|
cmd: "federation_sync",
|
|
2254
2327
|
target,
|
|
2255
2328
|
params: {
|
|
2256
|
-
msg_type: 23 /*
|
|
2329
|
+
msg_type: 23 /* FLEET_COMMAND */,
|
|
2257
2330
|
msg_id: generateId5(),
|
|
2258
2331
|
source_registry: source,
|
|
2259
2332
|
target_registry: target,
|
|
@@ -2280,17 +2353,17 @@ async function validateCrossRegistryCommand(msg, localRegistry, trustCache) {
|
|
|
2280
2353
|
reason: `REGISTRY_UNKNOWN: ${sourceRegistry} is not in the local trust cache`
|
|
2281
2354
|
};
|
|
2282
2355
|
}
|
|
2283
|
-
let loa = 1 /*
|
|
2356
|
+
let loa = 1 /* GUEST */;
|
|
2284
2357
|
const registryJwt = msg.params?.["registry_jwt"];
|
|
2285
2358
|
if (registryJwt) {
|
|
2286
2359
|
loa = extractLoaFromJwt(registryJwt);
|
|
2287
2360
|
} else if (typeof msg.loa === "number") {
|
|
2288
2361
|
loa = msg.loa;
|
|
2289
2362
|
}
|
|
2290
|
-
if (loa < 2 /*
|
|
2363
|
+
if (loa < 2 /* OPERATOR */) {
|
|
2291
2364
|
return {
|
|
2292
2365
|
valid: false,
|
|
2293
|
-
reason: `LOA_INSUFFICIENT: cross-registry commands require LoA>=2 (
|
|
2366
|
+
reason: `LOA_INSUFFICIENT: cross-registry commands require LoA>=2 (OPERATOR), got role=${loa}`
|
|
2294
2367
|
};
|
|
2295
2368
|
}
|
|
2296
2369
|
return { valid: true, reason: "cross-registry command accepted" };
|
|
@@ -2729,24 +2802,312 @@ function isPreemptedBy(scopeLevel) {
|
|
|
2729
2802
|
return scopeLevel >= 3;
|
|
2730
2803
|
}
|
|
2731
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
|
+
|
|
2732
3087
|
// src/index.ts
|
|
2733
3088
|
var VERSION = "0.6.0";
|
|
2734
3089
|
var RCAN_VERSION = "1.6";
|
|
2735
3090
|
export {
|
|
3091
|
+
AUTHORITY_ERROR_CODES,
|
|
2736
3092
|
AuditChain,
|
|
2737
3093
|
AuditError,
|
|
3094
|
+
COMPETITION_SCOPE_LEVEL,
|
|
2738
3095
|
CONTRIBUTE_SCOPE_LEVEL,
|
|
2739
3096
|
ClockDriftError,
|
|
2740
3097
|
CommitmentRecord,
|
|
2741
3098
|
ConfidenceGate,
|
|
2742
3099
|
DEFAULT_LOA_POLICY,
|
|
2743
3100
|
DataCategory,
|
|
3101
|
+
FIRMWARE_MANIFEST_PATH,
|
|
2744
3102
|
FaultCode,
|
|
2745
3103
|
FederationSyncType,
|
|
3104
|
+
FirmwareIntegrityError,
|
|
2746
3105
|
GateError,
|
|
2747
3106
|
HiTLGate,
|
|
2748
3107
|
KeyStore,
|
|
2749
3108
|
LevelOfAssurance,
|
|
3109
|
+
M2MAuthError,
|
|
3110
|
+
M2M_TRUSTED_ISSUER,
|
|
2750
3111
|
MediaEncoding,
|
|
2751
3112
|
MessageType,
|
|
2752
3113
|
NodeClient,
|
|
@@ -2772,13 +3133,18 @@ export {
|
|
|
2772
3133
|
RCANValidationError,
|
|
2773
3134
|
RCANVersionIncompatibleError,
|
|
2774
3135
|
RCAN_VERSION,
|
|
3136
|
+
ROLE_JWT_LEVEL,
|
|
3137
|
+
RRF_REVOCATION_CACHE_TTL_MS,
|
|
3138
|
+
RRF_REVOCATION_URL,
|
|
2775
3139
|
RegistryClient,
|
|
2776
3140
|
RegistryTier,
|
|
2777
3141
|
ReplayCache,
|
|
2778
3142
|
RevocationCache,
|
|
2779
3143
|
RobotURI,
|
|
2780
3144
|
RobotURIError,
|
|
3145
|
+
Role,
|
|
2781
3146
|
SAFETY_MESSAGE_TYPE,
|
|
3147
|
+
SCOPE_MIN_ROLE,
|
|
2782
3148
|
SDK_VERSION,
|
|
2783
3149
|
SPEC_VERSION,
|
|
2784
3150
|
TransportEncoding,
|
|
@@ -2789,6 +3155,9 @@ export {
|
|
|
2789
3155
|
addMediaInline,
|
|
2790
3156
|
addMediaRef,
|
|
2791
3157
|
assertClockSynced,
|
|
3158
|
+
authorityAccessFromWire,
|
|
3159
|
+
authorityAccessToWire,
|
|
3160
|
+
canonicalManifestJson,
|
|
2792
3161
|
checkClockSync,
|
|
2793
3162
|
checkRevocation,
|
|
2794
3163
|
decodeBleFrames,
|
|
@@ -2797,11 +3166,18 @@ export {
|
|
|
2797
3166
|
encodeBleFrames,
|
|
2798
3167
|
encodeCompact,
|
|
2799
3168
|
encodeMinimal,
|
|
3169
|
+
extractIdentityFromJwt,
|
|
2800
3170
|
extractLoaFromJwt,
|
|
3171
|
+
extractRoleFromJwt,
|
|
2801
3172
|
fetchCanonicalSchema,
|
|
3173
|
+
fetchRRFRevocations,
|
|
3174
|
+
isAuthorityRequestValid,
|
|
3175
|
+
isM2mTrustedRevoked,
|
|
2802
3176
|
isPreemptedBy,
|
|
2803
3177
|
isSafetyMessage,
|
|
2804
3178
|
makeCloudRelayMessage,
|
|
3179
|
+
makeCompetitionEnter,
|
|
3180
|
+
makeCompetitionScore,
|
|
2805
3181
|
makeConfigUpdate,
|
|
2806
3182
|
makeConsentDeny,
|
|
2807
3183
|
makeConsentGrant,
|
|
@@ -2814,8 +3190,10 @@ export {
|
|
|
2814
3190
|
makeFaultReport,
|
|
2815
3191
|
makeFederationSync,
|
|
2816
3192
|
makeKeyRotationMessage,
|
|
3193
|
+
makePersonalResearchResult,
|
|
2817
3194
|
makeResumeMessage,
|
|
2818
3195
|
makeRevocationBroadcast,
|
|
3196
|
+
makeSeasonStanding,
|
|
2819
3197
|
makeStopMessage,
|
|
2820
3198
|
makeStreamChunk,
|
|
2821
3199
|
makeTrainingConsentDeny,
|
|
@@ -2823,7 +3201,14 @@ export {
|
|
|
2823
3201
|
makeTrainingConsentRequest,
|
|
2824
3202
|
makeTrainingDataMessage,
|
|
2825
3203
|
makeTransparencyMessage,
|
|
3204
|
+
manifestFromWire,
|
|
3205
|
+
manifestToWire,
|
|
3206
|
+
parseM2mPeerToken,
|
|
3207
|
+
parseM2mTrustedToken,
|
|
3208
|
+
roleFromJwtLevel,
|
|
2826
3209
|
selectTransport,
|
|
3210
|
+
validateAuthorityAccess,
|
|
3211
|
+
validateCompetitionScope,
|
|
2827
3212
|
validateConfig,
|
|
2828
3213
|
validateConfigAgainstSchema,
|
|
2829
3214
|
validateConfigUpdate,
|
|
@@ -2832,13 +3217,17 @@ export {
|
|
|
2832
3217
|
validateCrossRegistryCommand,
|
|
2833
3218
|
validateDelegationChain,
|
|
2834
3219
|
validateLoaForScope,
|
|
3220
|
+
validateManifest,
|
|
2835
3221
|
validateMediaChunks,
|
|
2836
3222
|
validateMessage,
|
|
2837
3223
|
validateNodeAgainstSchema,
|
|
2838
3224
|
validateReplay,
|
|
3225
|
+
validateRoleForScope,
|
|
2839
3226
|
validateSafetyMessage,
|
|
2840
3227
|
validateTrainingDataMessage,
|
|
2841
3228
|
validateURI,
|
|
2842
|
-
validateVersionCompat
|
|
3229
|
+
validateVersionCompat,
|
|
3230
|
+
verifyM2mTrustedToken,
|
|
3231
|
+
verifyM2mTrustedTokenClaims
|
|
2843
3232
|
};
|
|
2844
3233
|
//# sourceMappingURL=index.mjs.map
|