@continuonai/rcan-ts 0.3.0 → 0.6.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 +5 -3
- package/dist/browser.d.mts +956 -7
- package/dist/browser.mjs +1593 -6
- package/dist/browser.mjs.map +1 -1
- package/dist/index.d.mts +956 -7
- package/dist/index.d.ts +956 -7
- package/dist/index.js +1658 -7
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1587 -6
- package/dist/index.mjs.map +1 -1
- package/dist/rcan-validate.js +62 -2
- package/dist/rcan.iife.js +3 -3
- package/package.json +3 -3
package/dist/index.mjs
CHANGED
|
@@ -95,7 +95,52 @@ var RobotURI = class _RobotURI {
|
|
|
95
95
|
}
|
|
96
96
|
};
|
|
97
97
|
|
|
98
|
+
// src/version.ts
|
|
99
|
+
var SPEC_VERSION = "1.6";
|
|
100
|
+
var SDK_VERSION = "0.6.0";
|
|
101
|
+
function validateVersionCompat(incomingVersion, localVersion = SPEC_VERSION) {
|
|
102
|
+
const parseParts = (v) => {
|
|
103
|
+
const parts = v.split(".");
|
|
104
|
+
const major = parseInt(parts[0] ?? "0", 10);
|
|
105
|
+
const minor = parseInt(parts[1] ?? "0", 10);
|
|
106
|
+
return [isNaN(major) ? 0 : major, isNaN(minor) ? 0 : minor];
|
|
107
|
+
};
|
|
108
|
+
const [inMajor] = parseParts(incomingVersion);
|
|
109
|
+
const [localMajor] = parseParts(localVersion);
|
|
110
|
+
return inMajor === localMajor;
|
|
111
|
+
}
|
|
112
|
+
|
|
98
113
|
// src/message.ts
|
|
114
|
+
var MessageType = /* @__PURE__ */ ((MessageType2) => {
|
|
115
|
+
MessageType2[MessageType2["COMMAND"] = 1] = "COMMAND";
|
|
116
|
+
MessageType2[MessageType2["RESPONSE"] = 2] = "RESPONSE";
|
|
117
|
+
MessageType2[MessageType2["STATUS"] = 3] = "STATUS";
|
|
118
|
+
MessageType2[MessageType2["HEARTBEAT"] = 4] = "HEARTBEAT";
|
|
119
|
+
MessageType2[MessageType2["CONFIG"] = 5] = "CONFIG";
|
|
120
|
+
MessageType2[MessageType2["SAFETY"] = 6] = "SAFETY";
|
|
121
|
+
MessageType2[MessageType2["SENSOR_DATA"] = 7] = "SENSOR_DATA";
|
|
122
|
+
MessageType2[MessageType2["AUDIT"] = 8] = "AUDIT";
|
|
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";
|
|
131
|
+
MessageType2[MessageType2["COMMAND_ACK"] = 17] = "COMMAND_ACK";
|
|
132
|
+
MessageType2[MessageType2["COMMAND_COMMIT"] = 18] = "COMMAND_COMMIT";
|
|
133
|
+
MessageType2[MessageType2["ROBOT_REVOCATION"] = 19] = "ROBOT_REVOCATION";
|
|
134
|
+
MessageType2[MessageType2["CONSENT_REQUEST"] = 20] = "CONSENT_REQUEST";
|
|
135
|
+
MessageType2[MessageType2["CONSENT_GRANT"] = 21] = "CONSENT_GRANT";
|
|
136
|
+
MessageType2[MessageType2["CONSENT_DENY"] = 22] = "CONSENT_DENY";
|
|
137
|
+
MessageType2[MessageType2["FLEET_COMMAND"] = 23] = "FLEET_COMMAND";
|
|
138
|
+
MessageType2[MessageType2["SUBSCRIBE"] = 24] = "SUBSCRIBE";
|
|
139
|
+
MessageType2[MessageType2["UNSUBSCRIBE"] = 25] = "UNSUBSCRIBE";
|
|
140
|
+
MessageType2[MessageType2["FAULT_REPORT"] = 26] = "FAULT_REPORT";
|
|
141
|
+
MessageType2[MessageType2["COMMAND_NACK"] = 27] = "COMMAND_NACK";
|
|
142
|
+
return MessageType2;
|
|
143
|
+
})(MessageType || {});
|
|
99
144
|
var RCANMessageError = class extends Error {
|
|
100
145
|
constructor(message) {
|
|
101
146
|
super(message);
|
|
@@ -104,6 +149,8 @@ var RCANMessageError = class extends Error {
|
|
|
104
149
|
};
|
|
105
150
|
var RCANMessage = class _RCANMessage {
|
|
106
151
|
rcan;
|
|
152
|
+
/** v1.5: rcanVersion field — defaults to SPEC_VERSION */
|
|
153
|
+
rcanVersion;
|
|
107
154
|
cmd;
|
|
108
155
|
target;
|
|
109
156
|
params;
|
|
@@ -111,6 +158,22 @@ var RCANMessage = class _RCANMessage {
|
|
|
111
158
|
modelIdentity;
|
|
112
159
|
signature;
|
|
113
160
|
timestamp;
|
|
161
|
+
/** v1.5 fields */
|
|
162
|
+
senderType;
|
|
163
|
+
cloudProvider;
|
|
164
|
+
keyId;
|
|
165
|
+
delegationChain;
|
|
166
|
+
groupId;
|
|
167
|
+
qos;
|
|
168
|
+
presenceVerified;
|
|
169
|
+
proximityMeters;
|
|
170
|
+
readOnly;
|
|
171
|
+
/** v1.6: GAP-14 level of assurance */
|
|
172
|
+
loa;
|
|
173
|
+
/** v1.6: GAP-17 transport encoding hint */
|
|
174
|
+
transportEncoding;
|
|
175
|
+
/** v1.6: GAP-18 multi-modal media chunks */
|
|
176
|
+
mediaChunks;
|
|
114
177
|
constructor(data) {
|
|
115
178
|
if (!data.cmd || data.cmd.trim() === "") {
|
|
116
179
|
throw new RCANMessageError("'cmd' is required");
|
|
@@ -118,7 +181,8 @@ var RCANMessage = class _RCANMessage {
|
|
|
118
181
|
if (!data.target) {
|
|
119
182
|
throw new RCANMessageError("'target' is required");
|
|
120
183
|
}
|
|
121
|
-
this.rcan = data.rcan ??
|
|
184
|
+
this.rcan = data.rcan ?? SPEC_VERSION;
|
|
185
|
+
this.rcanVersion = data.rcanVersion ?? SPEC_VERSION;
|
|
122
186
|
this.cmd = data.cmd;
|
|
123
187
|
this.target = data.target instanceof RobotURI ? data.target.toString() : String(data.target);
|
|
124
188
|
this.params = data.params ?? {};
|
|
@@ -126,6 +190,18 @@ var RCANMessage = class _RCANMessage {
|
|
|
126
190
|
this.modelIdentity = data.modelIdentity ?? data.model_identity;
|
|
127
191
|
this.signature = data.signature;
|
|
128
192
|
this.timestamp = data.timestamp ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
193
|
+
this.senderType = data.senderType;
|
|
194
|
+
this.cloudProvider = data.cloudProvider;
|
|
195
|
+
this.keyId = data.keyId;
|
|
196
|
+
this.delegationChain = data.delegationChain;
|
|
197
|
+
this.groupId = data.groupId;
|
|
198
|
+
this.qos = data.qos;
|
|
199
|
+
this.presenceVerified = data.presenceVerified;
|
|
200
|
+
this.proximityMeters = data.proximityMeters;
|
|
201
|
+
this.readOnly = data.readOnly;
|
|
202
|
+
this.loa = data.loa;
|
|
203
|
+
this.transportEncoding = data.transportEncoding;
|
|
204
|
+
this.mediaChunks = data.mediaChunks;
|
|
129
205
|
if (this.confidence !== void 0) {
|
|
130
206
|
if (this.confidence < 0 || this.confidence > 1) {
|
|
131
207
|
throw new RCANMessageError(
|
|
@@ -146,6 +222,7 @@ var RCANMessage = class _RCANMessage {
|
|
|
146
222
|
toJSON() {
|
|
147
223
|
const obj = {
|
|
148
224
|
rcan: this.rcan,
|
|
225
|
+
rcanVersion: this.rcanVersion,
|
|
149
226
|
cmd: this.cmd,
|
|
150
227
|
target: this.target,
|
|
151
228
|
timestamp: this.timestamp
|
|
@@ -154,6 +231,18 @@ var RCANMessage = class _RCANMessage {
|
|
|
154
231
|
if (this.confidence !== void 0) obj.confidence = this.confidence;
|
|
155
232
|
if (this.modelIdentity) obj.model_identity = this.modelIdentity;
|
|
156
233
|
if (this.signature) obj.signature = this.signature;
|
|
234
|
+
if (this.senderType !== void 0) obj.senderType = this.senderType;
|
|
235
|
+
if (this.cloudProvider !== void 0) obj.cloudProvider = this.cloudProvider;
|
|
236
|
+
if (this.keyId !== void 0) obj.keyId = this.keyId;
|
|
237
|
+
if (this.delegationChain !== void 0) obj.delegationChain = this.delegationChain;
|
|
238
|
+
if (this.groupId !== void 0) obj.groupId = this.groupId;
|
|
239
|
+
if (this.qos !== void 0) obj.qos = this.qos;
|
|
240
|
+
if (this.presenceVerified !== void 0) obj.presenceVerified = this.presenceVerified;
|
|
241
|
+
if (this.proximityMeters !== void 0) obj.proximityMeters = this.proximityMeters;
|
|
242
|
+
if (this.readOnly !== void 0) obj.readOnly = this.readOnly;
|
|
243
|
+
if (this.loa !== void 0) obj.loa = this.loa;
|
|
244
|
+
if (this.transportEncoding !== void 0) obj.transportEncoding = this.transportEncoding;
|
|
245
|
+
if (this.mediaChunks !== void 0) obj.mediaChunks = this.mediaChunks;
|
|
157
246
|
return obj;
|
|
158
247
|
}
|
|
159
248
|
/** Serialize to JSON string */
|
|
@@ -177,16 +266,56 @@ var RCANMessage = class _RCANMessage {
|
|
|
177
266
|
if (!obj.rcan) throw new RCANMessageError("Missing required field: 'rcan'");
|
|
178
267
|
return new _RCANMessage({
|
|
179
268
|
rcan: obj.rcan,
|
|
269
|
+
rcanVersion: obj.rcanVersion,
|
|
180
270
|
cmd: obj.cmd,
|
|
181
271
|
target: obj.target,
|
|
182
272
|
params: obj.params ?? {},
|
|
183
273
|
confidence: obj.confidence,
|
|
184
274
|
modelIdentity: obj.model_identity ?? obj.modelIdentity,
|
|
185
275
|
signature: obj.signature,
|
|
186
|
-
timestamp: obj.timestamp
|
|
276
|
+
timestamp: obj.timestamp,
|
|
277
|
+
senderType: obj.senderType,
|
|
278
|
+
cloudProvider: obj.cloudProvider,
|
|
279
|
+
keyId: obj.keyId,
|
|
280
|
+
delegationChain: obj.delegationChain,
|
|
281
|
+
groupId: obj.groupId,
|
|
282
|
+
qos: obj.qos,
|
|
283
|
+
presenceVerified: obj.presenceVerified,
|
|
284
|
+
proximityMeters: obj.proximityMeters,
|
|
285
|
+
readOnly: obj.readOnly,
|
|
286
|
+
loa: obj.loa,
|
|
287
|
+
transportEncoding: obj.transportEncoding,
|
|
288
|
+
mediaChunks: obj.mediaChunks
|
|
187
289
|
});
|
|
188
290
|
}
|
|
189
291
|
};
|
|
292
|
+
function makeCloudRelayMessage(base, provider) {
|
|
293
|
+
const data = base.toJSON();
|
|
294
|
+
data.senderType = "cloud_function";
|
|
295
|
+
data.cloudProvider = provider;
|
|
296
|
+
return new RCANMessage(data);
|
|
297
|
+
}
|
|
298
|
+
function addDelegationHop(msg, hop) {
|
|
299
|
+
const chain = msg.delegationChain ? [...msg.delegationChain, hop] : [hop];
|
|
300
|
+
const data = msg.toJSON();
|
|
301
|
+
data.delegationChain = chain;
|
|
302
|
+
return new RCANMessage(data);
|
|
303
|
+
}
|
|
304
|
+
function validateDelegationChain(chain) {
|
|
305
|
+
if (chain.length > 4) {
|
|
306
|
+
return { valid: false, reason: "DELEGATION_CHAIN_EXCEEDED: max depth is 4 hops" };
|
|
307
|
+
}
|
|
308
|
+
for (let i = 0; i < chain.length; i++) {
|
|
309
|
+
const hop = chain[i];
|
|
310
|
+
if (!hop) return { valid: false, reason: `hop ${i} is undefined` };
|
|
311
|
+
if (!hop.issuerRuri) return { valid: false, reason: `hop ${i}: missing issuerRuri` };
|
|
312
|
+
if (!hop.humanSubject) return { valid: false, reason: `hop ${i}: missing humanSubject` };
|
|
313
|
+
if (!hop.timestamp) return { valid: false, reason: `hop ${i}: missing timestamp` };
|
|
314
|
+
if (!hop.scope) return { valid: false, reason: `hop ${i}: missing scope` };
|
|
315
|
+
if (!hop.signature) return { valid: false, reason: `hop ${i}: missing signature` };
|
|
316
|
+
}
|
|
317
|
+
return { valid: true, reason: "ok" };
|
|
318
|
+
}
|
|
190
319
|
|
|
191
320
|
// src/crypto.ts
|
|
192
321
|
function generateUUID() {
|
|
@@ -775,6 +904,34 @@ var RCANNodeTrustError = class _RCANNodeTrustError extends RCANNodeError {
|
|
|
775
904
|
Object.setPrototypeOf(this, _RCANNodeTrustError.prototype);
|
|
776
905
|
}
|
|
777
906
|
};
|
|
907
|
+
var RCANVersionIncompatibleError = class _RCANVersionIncompatibleError extends RCANError {
|
|
908
|
+
constructor(incomingVersion, localVersion) {
|
|
909
|
+
super(`VERSION_INCOMPATIBLE: incoming=${incomingVersion}, local=${localVersion}`);
|
|
910
|
+
this.name = "RCANVersionIncompatibleError";
|
|
911
|
+
Object.setPrototypeOf(this, _RCANVersionIncompatibleError.prototype);
|
|
912
|
+
}
|
|
913
|
+
};
|
|
914
|
+
var RCANReplayAttackError = class _RCANReplayAttackError extends RCANError {
|
|
915
|
+
constructor(reason) {
|
|
916
|
+
super(`REPLAY_DETECTED: ${reason}`);
|
|
917
|
+
this.name = "RCANReplayAttackError";
|
|
918
|
+
Object.setPrototypeOf(this, _RCANReplayAttackError.prototype);
|
|
919
|
+
}
|
|
920
|
+
};
|
|
921
|
+
var RCANDelegationChainError = class _RCANDelegationChainError extends RCANError {
|
|
922
|
+
constructor(reason) {
|
|
923
|
+
super(`DELEGATION_CHAIN_ERROR: ${reason}`);
|
|
924
|
+
this.name = "RCANDelegationChainError";
|
|
925
|
+
Object.setPrototypeOf(this, _RCANDelegationChainError.prototype);
|
|
926
|
+
}
|
|
927
|
+
};
|
|
928
|
+
var RCANConfigAuthorizationError = class _RCANConfigAuthorizationError extends RCANError {
|
|
929
|
+
constructor(reason) {
|
|
930
|
+
super(`CONFIG_AUTH_ERROR: ${reason}`);
|
|
931
|
+
this.name = "RCANConfigAuthorizationError";
|
|
932
|
+
Object.setPrototypeOf(this, _RCANConfigAuthorizationError.prototype);
|
|
933
|
+
}
|
|
934
|
+
};
|
|
778
935
|
|
|
779
936
|
// src/registry.ts
|
|
780
937
|
var DEFAULT_BASE_URL = "https://rcan-spec.pages.dev";
|
|
@@ -1140,19 +1297,1389 @@ async function validateNodeAgainstSchema(manifest) {
|
|
|
1140
1297
|
return errors.length === 0 ? { valid: true } : { valid: false, errors };
|
|
1141
1298
|
}
|
|
1142
1299
|
|
|
1300
|
+
// src/qos.ts
|
|
1301
|
+
var QoSLevel = /* @__PURE__ */ ((QoSLevel2) => {
|
|
1302
|
+
QoSLevel2[QoSLevel2["FIRE_AND_FORGET"] = 0] = "FIRE_AND_FORGET";
|
|
1303
|
+
QoSLevel2[QoSLevel2["ACKNOWLEDGED"] = 1] = "ACKNOWLEDGED";
|
|
1304
|
+
QoSLevel2[QoSLevel2["EXACTLY_ONCE"] = 2] = "EXACTLY_ONCE";
|
|
1305
|
+
return QoSLevel2;
|
|
1306
|
+
})(QoSLevel || {});
|
|
1307
|
+
var QoSAckTimeoutError = class _QoSAckTimeoutError extends Error {
|
|
1308
|
+
constructor(msgId) {
|
|
1309
|
+
super(`ACK timeout for message ${msgId} \u2014 safety halt required`);
|
|
1310
|
+
this.name = "QoSAckTimeoutError";
|
|
1311
|
+
Object.setPrototypeOf(this, _QoSAckTimeoutError.prototype);
|
|
1312
|
+
}
|
|
1313
|
+
};
|
|
1314
|
+
var QoSManager = class {
|
|
1315
|
+
_send;
|
|
1316
|
+
_waitForAck;
|
|
1317
|
+
constructor(send, waitForAck) {
|
|
1318
|
+
this._send = send;
|
|
1319
|
+
this._waitForAck = waitForAck;
|
|
1320
|
+
}
|
|
1321
|
+
/**
|
|
1322
|
+
* Send a message with the specified QoS level.
|
|
1323
|
+
*
|
|
1324
|
+
* For QoS 0: fire and forget.
|
|
1325
|
+
* For QoS 1: retry until ACK received or maxRetries exhausted.
|
|
1326
|
+
* For QoS 2: same as QoS 1 but sends a COMMAND_COMMIT after ACK.
|
|
1327
|
+
*/
|
|
1328
|
+
async sendWithQoS(msg, opts = {}) {
|
|
1329
|
+
const qos = opts.qos ?? 0 /* FIRE_AND_FORGET */;
|
|
1330
|
+
const maxRetries = opts.maxRetries ?? 3;
|
|
1331
|
+
const initialBackoffMs = opts.initialBackoffMs ?? 100;
|
|
1332
|
+
const ackTimeoutMs = opts.ackTimeoutMs ?? 500;
|
|
1333
|
+
if (qos === 0 /* FIRE_AND_FORGET */) {
|
|
1334
|
+
await this._send(msg);
|
|
1335
|
+
return { delivered: true, attempts: 1, reason: "fire-and-forget" };
|
|
1336
|
+
}
|
|
1337
|
+
const msgId = msg.message_id ?? msg.msg_id ?? "unknown";
|
|
1338
|
+
let attempts = 0;
|
|
1339
|
+
let backoffMs = initialBackoffMs;
|
|
1340
|
+
while (attempts <= maxRetries) {
|
|
1341
|
+
await this._send(msg);
|
|
1342
|
+
attempts++;
|
|
1343
|
+
const acked = await this._waitForAck(msgId, ackTimeoutMs);
|
|
1344
|
+
if (acked) {
|
|
1345
|
+
return { delivered: true, attempts, reason: qos === 2 /* EXACTLY_ONCE */ ? "exactly-once" : "acknowledged" };
|
|
1346
|
+
}
|
|
1347
|
+
if (attempts > maxRetries) break;
|
|
1348
|
+
await sleep(backoffMs);
|
|
1349
|
+
backoffMs = Math.min(backoffMs * 2, 5e3);
|
|
1350
|
+
}
|
|
1351
|
+
return { delivered: false, attempts, reason: `ACK not received after ${maxRetries} retries` };
|
|
1352
|
+
}
|
|
1353
|
+
};
|
|
1354
|
+
function makeEstopWithQoS(ruri, reason) {
|
|
1355
|
+
const msg = {
|
|
1356
|
+
message_type: 6,
|
|
1357
|
+
ruri,
|
|
1358
|
+
safety_event: "ESTOP",
|
|
1359
|
+
reason: reason.slice(0, 512),
|
|
1360
|
+
timestamp_ms: Date.now(),
|
|
1361
|
+
message_id: generateId(),
|
|
1362
|
+
qos: 2 /* EXACTLY_ONCE */
|
|
1363
|
+
};
|
|
1364
|
+
return msg;
|
|
1365
|
+
}
|
|
1366
|
+
function generateId() {
|
|
1367
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
1368
|
+
return crypto.randomUUID();
|
|
1369
|
+
}
|
|
1370
|
+
const bytes = Array.from({ length: 16 }, () => Math.floor(Math.random() * 256));
|
|
1371
|
+
bytes[6] = bytes[6] & 15 | 64;
|
|
1372
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
1373
|
+
const hex = bytes.map((b) => b.toString(16).padStart(2, "0"));
|
|
1374
|
+
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("")}`;
|
|
1375
|
+
}
|
|
1376
|
+
function sleep(ms) {
|
|
1377
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
// src/safety.ts
|
|
1381
|
+
var SAFETY_MESSAGE_TYPE = 6;
|
|
1382
|
+
function generateId2() {
|
|
1383
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
1384
|
+
return crypto.randomUUID();
|
|
1385
|
+
}
|
|
1386
|
+
const bytes = Array.from({ length: 16 }, () => Math.floor(Math.random() * 256));
|
|
1387
|
+
bytes[6] = bytes[6] & 15 | 64;
|
|
1388
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
1389
|
+
const hex = bytes.map((b) => b.toString(16).padStart(2, "0"));
|
|
1390
|
+
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("")}`;
|
|
1391
|
+
}
|
|
1392
|
+
function makeEstopMessage(ruri, reason) {
|
|
1393
|
+
return {
|
|
1394
|
+
message_type: 6,
|
|
1395
|
+
ruri,
|
|
1396
|
+
safety_event: "ESTOP",
|
|
1397
|
+
reason: reason.slice(0, 512),
|
|
1398
|
+
timestamp_ms: Date.now(),
|
|
1399
|
+
message_id: generateId2(),
|
|
1400
|
+
qos: 2 /* EXACTLY_ONCE */
|
|
1401
|
+
};
|
|
1402
|
+
}
|
|
1403
|
+
function makeStopMessage(ruri, reason) {
|
|
1404
|
+
return {
|
|
1405
|
+
message_type: 6,
|
|
1406
|
+
ruri,
|
|
1407
|
+
safety_event: "STOP",
|
|
1408
|
+
reason: reason.slice(0, 512),
|
|
1409
|
+
timestamp_ms: Date.now(),
|
|
1410
|
+
message_id: generateId2()
|
|
1411
|
+
};
|
|
1412
|
+
}
|
|
1413
|
+
function makeResumeMessage(ruri, reason) {
|
|
1414
|
+
return {
|
|
1415
|
+
message_type: 6,
|
|
1416
|
+
ruri,
|
|
1417
|
+
safety_event: "RESUME",
|
|
1418
|
+
reason: reason.slice(0, 512),
|
|
1419
|
+
timestamp_ms: Date.now(),
|
|
1420
|
+
message_id: generateId2()
|
|
1421
|
+
};
|
|
1422
|
+
}
|
|
1423
|
+
function isSafetyMessage(msg) {
|
|
1424
|
+
return typeof msg === "object" && msg !== null && msg.message_type === SAFETY_MESSAGE_TYPE;
|
|
1425
|
+
}
|
|
1426
|
+
function validateSafetyMessage(msg) {
|
|
1427
|
+
const errors = [];
|
|
1428
|
+
if (msg.message_type !== 6) errors.push("message_type must be 6");
|
|
1429
|
+
if (!msg.ruri) errors.push("ruri is required");
|
|
1430
|
+
if (!["ESTOP", "STOP", "RESUME"].includes(msg.safety_event ?? "")) {
|
|
1431
|
+
errors.push("safety_event must be ESTOP, STOP, or RESUME");
|
|
1432
|
+
}
|
|
1433
|
+
if (!msg.reason || msg.reason.length === 0) errors.push("reason is required");
|
|
1434
|
+
if (!msg.message_id) errors.push("message_id is required");
|
|
1435
|
+
if (!msg.timestamp_ms || msg.timestamp_ms <= 0) errors.push("timestamp_ms must be positive");
|
|
1436
|
+
return errors;
|
|
1437
|
+
}
|
|
1438
|
+
function makeTransparencyMessage(ruri, disclosure, delegationChain) {
|
|
1439
|
+
return {
|
|
1440
|
+
message_type: 11,
|
|
1441
|
+
ruri,
|
|
1442
|
+
disclosure,
|
|
1443
|
+
timestamp_ms: Date.now(),
|
|
1444
|
+
message_id: generateId2(),
|
|
1445
|
+
delegation_chain: delegationChain
|
|
1446
|
+
};
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
// src/replay.ts
|
|
1450
|
+
var SAFETY_MESSAGE_TYPE2 = 6;
|
|
1451
|
+
var SAFETY_MAX_WINDOW_S = 10;
|
|
1452
|
+
var DEFAULT_WINDOW_S = 30;
|
|
1453
|
+
var DEFAULT_MAX_SIZE = 1e4;
|
|
1454
|
+
var ReplayCache = class {
|
|
1455
|
+
windowSeconds;
|
|
1456
|
+
maxSize;
|
|
1457
|
+
/** Map<msgId, expiresAtMs> */
|
|
1458
|
+
_seen;
|
|
1459
|
+
constructor(windowSeconds = DEFAULT_WINDOW_S, maxSize = DEFAULT_MAX_SIZE) {
|
|
1460
|
+
this.windowSeconds = windowSeconds;
|
|
1461
|
+
this.maxSize = maxSize;
|
|
1462
|
+
this._seen = /* @__PURE__ */ new Map();
|
|
1463
|
+
}
|
|
1464
|
+
/**
|
|
1465
|
+
* Check a message id + timestamp and record it if allowed.
|
|
1466
|
+
*
|
|
1467
|
+
* @param msgId - unique message identifier
|
|
1468
|
+
* @param timestamp - ISO 8601 string or Unix seconds (float)
|
|
1469
|
+
* @param isSafety - if true, enforce the 10s safety window cap
|
|
1470
|
+
*/
|
|
1471
|
+
checkAndRecord(msgId, timestamp, isSafety = false) {
|
|
1472
|
+
const now = Date.now();
|
|
1473
|
+
this._evict(now);
|
|
1474
|
+
const window = isSafety ? Math.min(this.windowSeconds, SAFETY_MAX_WINDOW_S) : this.windowSeconds;
|
|
1475
|
+
const tMs = parseTimestampMs(timestamp);
|
|
1476
|
+
if (tMs === null) {
|
|
1477
|
+
return { allowed: false, reason: `invalid timestamp format: ${timestamp}` };
|
|
1478
|
+
}
|
|
1479
|
+
const ageMs = now - tMs;
|
|
1480
|
+
const windowMs = window * 1e3;
|
|
1481
|
+
if (ageMs > windowMs) {
|
|
1482
|
+
return {
|
|
1483
|
+
allowed: false,
|
|
1484
|
+
reason: `message too old: age=${Math.round(ageMs / 1e3)}s > window=${window}s`
|
|
1485
|
+
};
|
|
1486
|
+
}
|
|
1487
|
+
if (tMs > now + 5e3) {
|
|
1488
|
+
return {
|
|
1489
|
+
allowed: false,
|
|
1490
|
+
reason: `message timestamp is in the future`
|
|
1491
|
+
};
|
|
1492
|
+
}
|
|
1493
|
+
if (this._seen.has(msgId)) {
|
|
1494
|
+
return { allowed: false, reason: `replay detected: msg_id ${msgId} already seen` };
|
|
1495
|
+
}
|
|
1496
|
+
if (this._seen.size >= this.maxSize) {
|
|
1497
|
+
const firstKey = this._seen.keys().next().value;
|
|
1498
|
+
this._seen.delete(firstKey);
|
|
1499
|
+
}
|
|
1500
|
+
const expiresAt = now + windowMs;
|
|
1501
|
+
this._seen.set(msgId, expiresAt);
|
|
1502
|
+
return { allowed: true, reason: "ok" };
|
|
1503
|
+
}
|
|
1504
|
+
/** Evict entries whose window has passed */
|
|
1505
|
+
_evict(now) {
|
|
1506
|
+
for (const [id, expiresAt] of this._seen) {
|
|
1507
|
+
if (expiresAt <= now) {
|
|
1508
|
+
this._seen.delete(id);
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
/** Number of entries currently tracked */
|
|
1513
|
+
get size() {
|
|
1514
|
+
return this._seen.size;
|
|
1515
|
+
}
|
|
1516
|
+
};
|
|
1517
|
+
function validateReplay(message, cache) {
|
|
1518
|
+
const msg = message;
|
|
1519
|
+
const msgId = msg.message_id ?? msg.msg_id;
|
|
1520
|
+
if (!msgId) {
|
|
1521
|
+
return { valid: false, reason: "missing message_id / msg_id" };
|
|
1522
|
+
}
|
|
1523
|
+
let timestamp;
|
|
1524
|
+
if (typeof msg.timestamp_ms === "number") {
|
|
1525
|
+
timestamp = String(msg.timestamp_ms / 1e3);
|
|
1526
|
+
} else if (msg.timestamp !== void 0) {
|
|
1527
|
+
timestamp = String(msg.timestamp);
|
|
1528
|
+
}
|
|
1529
|
+
if (!timestamp) {
|
|
1530
|
+
return { valid: false, reason: "missing timestamp" };
|
|
1531
|
+
}
|
|
1532
|
+
const isSafety = msg.message_type === SAFETY_MESSAGE_TYPE2 || msg.message_type === SAFETY_MESSAGE_TYPE2;
|
|
1533
|
+
const result = cache.checkAndRecord(msgId, timestamp, isSafety);
|
|
1534
|
+
return { valid: result.allowed, reason: result.reason };
|
|
1535
|
+
}
|
|
1536
|
+
function parseTimestampMs(ts) {
|
|
1537
|
+
if (ts.includes("T") || ts.includes("-")) {
|
|
1538
|
+
const d = new Date(ts);
|
|
1539
|
+
if (!isNaN(d.getTime())) return d.getTime();
|
|
1540
|
+
}
|
|
1541
|
+
const n = parseFloat(ts);
|
|
1542
|
+
if (isNaN(n)) return null;
|
|
1543
|
+
if (n > 1e12) return n;
|
|
1544
|
+
return n * 1e3;
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
// src/clock.ts
|
|
1548
|
+
var ClockDriftError = class _ClockDriftError extends Error {
|
|
1549
|
+
offsetSeconds;
|
|
1550
|
+
constructor(offsetSeconds, maxDriftSeconds) {
|
|
1551
|
+
super(
|
|
1552
|
+
`Clock drift too large: offset=${offsetSeconds.toFixed(3)}s > max=${maxDriftSeconds}s`
|
|
1553
|
+
);
|
|
1554
|
+
this.name = "ClockDriftError";
|
|
1555
|
+
this.offsetSeconds = offsetSeconds;
|
|
1556
|
+
Object.setPrototypeOf(this, _ClockDriftError.prototype);
|
|
1557
|
+
}
|
|
1558
|
+
};
|
|
1559
|
+
async function checkClockSync(timeServer) {
|
|
1560
|
+
const server = timeServer ?? "https://worldtimeapi.org/api/ip";
|
|
1561
|
+
try {
|
|
1562
|
+
const before = Date.now();
|
|
1563
|
+
const resp = await fetch(server, { method: "HEAD", signal: AbortSignal.timeout(3e3) });
|
|
1564
|
+
const after = Date.now();
|
|
1565
|
+
const dateHeader = resp.headers.get("Date") ?? resp.headers.get("date");
|
|
1566
|
+
if (!dateHeader) {
|
|
1567
|
+
return {
|
|
1568
|
+
synchronized: true,
|
|
1569
|
+
offsetSeconds: 0,
|
|
1570
|
+
source: "assumed (no Date header)"
|
|
1571
|
+
};
|
|
1572
|
+
}
|
|
1573
|
+
const serverMs = new Date(dateHeader).getTime();
|
|
1574
|
+
if (isNaN(serverMs)) {
|
|
1575
|
+
return { synchronized: true, offsetSeconds: 0, source: "assumed (unparseable Date header)" };
|
|
1576
|
+
}
|
|
1577
|
+
const localMidpoint = (before + after) / 2;
|
|
1578
|
+
const offsetSeconds = (localMidpoint - serverMs) / 1e3;
|
|
1579
|
+
const synchronized = Math.abs(offsetSeconds) <= 5;
|
|
1580
|
+
return { synchronized, offsetSeconds, source: server };
|
|
1581
|
+
} catch {
|
|
1582
|
+
return { synchronized: true, offsetSeconds: 0, source: "assumed (network unavailable)" };
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
async function assertClockSynced(maxDriftSeconds = 5) {
|
|
1586
|
+
const status = await checkClockSync();
|
|
1587
|
+
if (!status.synchronized || Math.abs(status.offsetSeconds) > maxDriftSeconds) {
|
|
1588
|
+
throw new ClockDriftError(status.offsetSeconds, maxDriftSeconds);
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
// src/configUpdate.ts
|
|
1593
|
+
async function hashDiff(diff) {
|
|
1594
|
+
const text = JSON.stringify(diff, Object.keys(diff).sort());
|
|
1595
|
+
if (typeof crypto !== "undefined" && crypto.subtle) {
|
|
1596
|
+
const encoded = new TextEncoder().encode(text);
|
|
1597
|
+
const hashBuf = await crypto.subtle.digest("SHA-256", encoded);
|
|
1598
|
+
return Array.from(new Uint8Array(hashBuf)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1599
|
+
}
|
|
1600
|
+
let hash = 2166136261;
|
|
1601
|
+
for (let i = 0; i < text.length; i++) {
|
|
1602
|
+
hash ^= text.charCodeAt(i);
|
|
1603
|
+
hash = hash * 16777619 >>> 0;
|
|
1604
|
+
}
|
|
1605
|
+
return hash.toString(16).padStart(8, "0");
|
|
1606
|
+
}
|
|
1607
|
+
async function makeConfigUpdate(diff, scope, rollback, target = "rcan://local/config", safetyOverrides = false) {
|
|
1608
|
+
const configHash = await hashDiff(diff);
|
|
1609
|
+
return new RCANMessage({
|
|
1610
|
+
rcan: SPEC_VERSION,
|
|
1611
|
+
cmd: "CONFIG_UPDATE",
|
|
1612
|
+
target,
|
|
1613
|
+
params: {
|
|
1614
|
+
message_type: 5 /* CONFIG */,
|
|
1615
|
+
diff,
|
|
1616
|
+
rollback,
|
|
1617
|
+
scope,
|
|
1618
|
+
config_hash: configHash,
|
|
1619
|
+
safety_overrides: safetyOverrides
|
|
1620
|
+
}
|
|
1621
|
+
});
|
|
1622
|
+
}
|
|
1623
|
+
function validateConfigUpdate(msg) {
|
|
1624
|
+
const p = msg.params;
|
|
1625
|
+
if (!p.diff || typeof p.diff !== "object") {
|
|
1626
|
+
return { valid: false, reason: "missing required field: params.diff" };
|
|
1627
|
+
}
|
|
1628
|
+
if (!p.config_hash || typeof p.config_hash !== "string") {
|
|
1629
|
+
return { valid: false, reason: "missing required field: params.config_hash" };
|
|
1630
|
+
}
|
|
1631
|
+
if (!("rollback" in p)) {
|
|
1632
|
+
return { valid: false, reason: "missing required field: params.rollback" };
|
|
1633
|
+
}
|
|
1634
|
+
if (p.safety_overrides === true && p.scope !== "creator") {
|
|
1635
|
+
return {
|
|
1636
|
+
valid: false,
|
|
1637
|
+
reason: "safety_overrides=true requires scope=creator (owner is insufficient)"
|
|
1638
|
+
};
|
|
1639
|
+
}
|
|
1640
|
+
return { valid: true, reason: "ok" };
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
// src/keys.ts
|
|
1644
|
+
function generateId3() {
|
|
1645
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
1646
|
+
return crypto.randomUUID();
|
|
1647
|
+
}
|
|
1648
|
+
return `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
1649
|
+
}
|
|
1650
|
+
var KeyStore = class {
|
|
1651
|
+
_keys = [];
|
|
1652
|
+
/** Add a key to the store */
|
|
1653
|
+
addKey(entry) {
|
|
1654
|
+
this._keys.push(entry);
|
|
1655
|
+
}
|
|
1656
|
+
/** Get the current JWKS document (all non-revoked keys) */
|
|
1657
|
+
getJWKS() {
|
|
1658
|
+
return { keys: [...this._keys] };
|
|
1659
|
+
}
|
|
1660
|
+
/** Find a key by kid */
|
|
1661
|
+
findKey(kid) {
|
|
1662
|
+
return this._keys.find((k) => k.kid === kid);
|
|
1663
|
+
}
|
|
1664
|
+
/** Check if a key is valid (not expired, not revoked) */
|
|
1665
|
+
isKeyValid(kid, nowMs) {
|
|
1666
|
+
const entry = this.findKey(kid);
|
|
1667
|
+
if (!entry) return false;
|
|
1668
|
+
const now = (nowMs ?? Date.now()) / 1e3;
|
|
1669
|
+
if (entry.revoked_at !== void 0 && entry.revoked_at <= now) return false;
|
|
1670
|
+
if (entry.exp !== void 0 && entry.exp < now) return false;
|
|
1671
|
+
return true;
|
|
1672
|
+
}
|
|
1673
|
+
/** Mark a key as expired (rotate out) */
|
|
1674
|
+
expireKey(kid, expiresAt) {
|
|
1675
|
+
const entry = this.findKey(kid);
|
|
1676
|
+
if (entry) {
|
|
1677
|
+
entry.exp = expiresAt ?? Math.floor(Date.now() / 1e3);
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
/** Revoke a key immediately */
|
|
1681
|
+
revokeKey(kid) {
|
|
1682
|
+
const entry = this.findKey(kid);
|
|
1683
|
+
if (entry) {
|
|
1684
|
+
entry.revoked_at = Math.floor(Date.now() / 1e3);
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
/** All valid keys (not expired, not revoked) */
|
|
1688
|
+
validKeys(nowMs) {
|
|
1689
|
+
return this._keys.filter((k) => this.isKeyValid(k.kid, nowMs));
|
|
1690
|
+
}
|
|
1691
|
+
};
|
|
1692
|
+
function makeKeyRotationMessage(newPublicKey, oldKeyId, overlapSeconds = 120, target = "rcan://local/keys") {
|
|
1693
|
+
const newKid = generateId3().slice(0, 8);
|
|
1694
|
+
return new RCANMessage({
|
|
1695
|
+
rcan: SPEC_VERSION,
|
|
1696
|
+
cmd: "KEY_ROTATION",
|
|
1697
|
+
target,
|
|
1698
|
+
params: {
|
|
1699
|
+
message_type: 5 /* CONFIG */,
|
|
1700
|
+
new_public_key: newPublicKey,
|
|
1701
|
+
new_kid: newKid,
|
|
1702
|
+
old_kid: oldKeyId,
|
|
1703
|
+
overlap_seconds: overlapSeconds,
|
|
1704
|
+
initiated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1705
|
+
},
|
|
1706
|
+
keyId: newKid
|
|
1707
|
+
});
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
// src/consent.ts
|
|
1711
|
+
function generateId4() {
|
|
1712
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
1713
|
+
return crypto.randomUUID();
|
|
1714
|
+
}
|
|
1715
|
+
return `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
1716
|
+
}
|
|
1717
|
+
function makeConsentRequest(params) {
|
|
1718
|
+
const requestId = params.requestId ?? generateId4();
|
|
1719
|
+
return new RCANMessage({
|
|
1720
|
+
rcan: SPEC_VERSION,
|
|
1721
|
+
cmd: "CONSENT_REQUEST",
|
|
1722
|
+
target: params.targetRuri,
|
|
1723
|
+
params: {
|
|
1724
|
+
message_type: 20 /* CONSENT_REQUEST */,
|
|
1725
|
+
requester_ruri: params.requesterRuri,
|
|
1726
|
+
requester_owner: params.requesterOwner,
|
|
1727
|
+
target_ruri: params.targetRuri,
|
|
1728
|
+
requested_scopes: params.requestedScopes,
|
|
1729
|
+
duration_hours: params.durationHours,
|
|
1730
|
+
justification: params.justification,
|
|
1731
|
+
request_id: requestId,
|
|
1732
|
+
consent_type: params.consentType ?? "cross_robot",
|
|
1733
|
+
data_categories: params.dataCategories ?? []
|
|
1734
|
+
}
|
|
1735
|
+
});
|
|
1736
|
+
}
|
|
1737
|
+
function makeConsentGrant(params) {
|
|
1738
|
+
const expiresAt = params.expiresAt ?? new Date(Date.now() + 24 * 3600 * 1e3).toISOString();
|
|
1739
|
+
return new RCANMessage({
|
|
1740
|
+
rcan: SPEC_VERSION,
|
|
1741
|
+
cmd: "CONSENT_GRANT",
|
|
1742
|
+
target: "rcan://local/consent",
|
|
1743
|
+
params: {
|
|
1744
|
+
message_type: 21 /* CONSENT_GRANT */,
|
|
1745
|
+
request_id: params.requestId,
|
|
1746
|
+
granted_scopes: params.grantedScopes ?? [],
|
|
1747
|
+
expires_at: expiresAt,
|
|
1748
|
+
reason: params.reason ?? "approved"
|
|
1749
|
+
}
|
|
1750
|
+
});
|
|
1751
|
+
}
|
|
1752
|
+
function makeConsentDeny(params) {
|
|
1753
|
+
return new RCANMessage({
|
|
1754
|
+
rcan: SPEC_VERSION,
|
|
1755
|
+
cmd: "CONSENT_DENY",
|
|
1756
|
+
target: "rcan://local/consent",
|
|
1757
|
+
params: {
|
|
1758
|
+
message_type: 22 /* CONSENT_DENY */,
|
|
1759
|
+
request_id: params.requestId,
|
|
1760
|
+
reason: params.reason ?? "denied"
|
|
1761
|
+
}
|
|
1762
|
+
});
|
|
1763
|
+
}
|
|
1764
|
+
function validateConsentMessage(msg) {
|
|
1765
|
+
const cmd = msg.cmd;
|
|
1766
|
+
const p = msg.params;
|
|
1767
|
+
const msgType = p.message_type;
|
|
1768
|
+
if (cmd === "CONSENT_REQUEST") {
|
|
1769
|
+
if (msgType !== 20 /* CONSENT_REQUEST */) {
|
|
1770
|
+
return { valid: false, reason: "message_type must be CONSENT_REQUEST (20)" };
|
|
1771
|
+
}
|
|
1772
|
+
if (!p.requester_ruri) return { valid: false, reason: "missing requester_ruri" };
|
|
1773
|
+
if (!p.target_ruri) return { valid: false, reason: "missing target_ruri" };
|
|
1774
|
+
if (!p.requested_scopes || !Array.isArray(p.requested_scopes) || p.requested_scopes.length === 0) {
|
|
1775
|
+
return { valid: false, reason: "requested_scopes must be a non-empty array" };
|
|
1776
|
+
}
|
|
1777
|
+
if (!p.request_id) return { valid: false, reason: "missing request_id" };
|
|
1778
|
+
if (!p.justification) return { valid: false, reason: "missing justification" };
|
|
1779
|
+
return { valid: true, reason: "ok" };
|
|
1780
|
+
}
|
|
1781
|
+
if (cmd === "CONSENT_GRANT") {
|
|
1782
|
+
if (msgType !== 21 /* CONSENT_GRANT */) {
|
|
1783
|
+
return { valid: false, reason: "message_type must be CONSENT_GRANT (21)" };
|
|
1784
|
+
}
|
|
1785
|
+
if (!p.request_id) return { valid: false, reason: "missing request_id" };
|
|
1786
|
+
if (!p.expires_at) return { valid: false, reason: "missing expires_at" };
|
|
1787
|
+
return { valid: true, reason: "ok" };
|
|
1788
|
+
}
|
|
1789
|
+
if (cmd === "CONSENT_DENY") {
|
|
1790
|
+
if (msgType !== 22 /* CONSENT_DENY */) {
|
|
1791
|
+
return { valid: false, reason: "message_type must be CONSENT_DENY (22)" };
|
|
1792
|
+
}
|
|
1793
|
+
if (!p.request_id) return { valid: false, reason: "missing request_id" };
|
|
1794
|
+
return { valid: true, reason: "ok" };
|
|
1795
|
+
}
|
|
1796
|
+
return { valid: false, reason: `unknown consent command: ${cmd}` };
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
// src/revocation.ts
|
|
1800
|
+
var CACHE_TTL_MS = 60 * 60 * 1e3;
|
|
1801
|
+
var RevocationCache = class {
|
|
1802
|
+
_cache = /* @__PURE__ */ new Map();
|
|
1803
|
+
/** Get a cached status if still fresh */
|
|
1804
|
+
get(rrn, nowMs) {
|
|
1805
|
+
const entry = this._cache.get(rrn);
|
|
1806
|
+
if (!entry) return void 0;
|
|
1807
|
+
const now = nowMs ?? Date.now();
|
|
1808
|
+
if (entry.cachedUntil !== void 0 && entry.cachedUntil < now) {
|
|
1809
|
+
this._cache.delete(rrn);
|
|
1810
|
+
return void 0;
|
|
1811
|
+
}
|
|
1812
|
+
return entry;
|
|
1813
|
+
}
|
|
1814
|
+
/** Store a status record with a 1h TTL */
|
|
1815
|
+
set(status, nowMs) {
|
|
1816
|
+
const now = nowMs ?? Date.now();
|
|
1817
|
+
this._cache.set(status.rrn, {
|
|
1818
|
+
...status,
|
|
1819
|
+
cachedUntil: now + CACHE_TTL_MS
|
|
1820
|
+
});
|
|
1821
|
+
}
|
|
1822
|
+
/** Invalidate a cached entry immediately */
|
|
1823
|
+
invalidate(rrn) {
|
|
1824
|
+
this._cache.delete(rrn);
|
|
1825
|
+
}
|
|
1826
|
+
/** Number of cached entries */
|
|
1827
|
+
get size() {
|
|
1828
|
+
return this._cache.size;
|
|
1829
|
+
}
|
|
1830
|
+
};
|
|
1831
|
+
async function checkRevocation(rrn, registryUrl, cache) {
|
|
1832
|
+
const c = cache ?? new RevocationCache();
|
|
1833
|
+
const cached = c.get(rrn);
|
|
1834
|
+
if (cached) return cached;
|
|
1835
|
+
const url = `${registryUrl.replace(/\/$/, "")}/api/v1/robots/${encodeURIComponent(rrn)}/revocation-status`;
|
|
1836
|
+
try {
|
|
1837
|
+
const resp = await fetch(url, { signal: AbortSignal.timeout(5e3) });
|
|
1838
|
+
if (!resp.ok) {
|
|
1839
|
+
const status2 = { rrn, status: "active", reason: `registry returned ${resp.status}` };
|
|
1840
|
+
c.set(status2);
|
|
1841
|
+
return status2;
|
|
1842
|
+
}
|
|
1843
|
+
const data = await resp.json();
|
|
1844
|
+
const status = {
|
|
1845
|
+
rrn,
|
|
1846
|
+
status: data.status ?? "active",
|
|
1847
|
+
revokedAt: data.revokedAt,
|
|
1848
|
+
reason: data.reason,
|
|
1849
|
+
authority: data.authority
|
|
1850
|
+
};
|
|
1851
|
+
c.set(status);
|
|
1852
|
+
return status;
|
|
1853
|
+
} catch {
|
|
1854
|
+
const status = { rrn, status: "active", reason: "network unavailable" };
|
|
1855
|
+
return status;
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
function makeRevocationBroadcast(rrn, reason) {
|
|
1859
|
+
return new RCANMessage({
|
|
1860
|
+
rcan: SPEC_VERSION,
|
|
1861
|
+
cmd: "ROBOT_REVOCATION",
|
|
1862
|
+
target: "rcan://broadcast/revocation",
|
|
1863
|
+
params: {
|
|
1864
|
+
message_type: 19 /* ROBOT_REVOCATION */,
|
|
1865
|
+
rrn,
|
|
1866
|
+
reason,
|
|
1867
|
+
revoked_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1868
|
+
}
|
|
1869
|
+
});
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
// src/trainingConsent.ts
|
|
1873
|
+
var DataCategory = /* @__PURE__ */ ((DataCategory2) => {
|
|
1874
|
+
DataCategory2["VIDEO"] = "video";
|
|
1875
|
+
DataCategory2["AUDIO"] = "audio";
|
|
1876
|
+
DataCategory2["LOCATION"] = "location";
|
|
1877
|
+
DataCategory2["BIOMETRIC"] = "biometric";
|
|
1878
|
+
DataCategory2["TELEMETRY"] = "telemetry";
|
|
1879
|
+
return DataCategory2;
|
|
1880
|
+
})(DataCategory || {});
|
|
1881
|
+
function makeTrainingConsentRequest(params) {
|
|
1882
|
+
return makeConsentRequest({
|
|
1883
|
+
requesterRuri: params.requesterRuri,
|
|
1884
|
+
requesterOwner: params.requesterOwner,
|
|
1885
|
+
targetRuri: params.targetRuri,
|
|
1886
|
+
requestedScopes: ["training_data"],
|
|
1887
|
+
durationHours: params.durationHours,
|
|
1888
|
+
justification: params.justification,
|
|
1889
|
+
requestId: params.requestId,
|
|
1890
|
+
consentType: "training_data",
|
|
1891
|
+
dataCategories: params.dataCategories
|
|
1892
|
+
});
|
|
1893
|
+
}
|
|
1894
|
+
function makeTrainingConsentGrant(params) {
|
|
1895
|
+
return makeConsentGrant(params);
|
|
1896
|
+
}
|
|
1897
|
+
function makeTrainingConsentDeny(params) {
|
|
1898
|
+
return makeConsentDeny(params);
|
|
1899
|
+
}
|
|
1900
|
+
function validateTrainingDataMessage(msg) {
|
|
1901
|
+
if (msg.params.message_type !== 10 /* TRAINING_DATA */) {
|
|
1902
|
+
return { valid: false, reason: "not a TRAINING_DATA message" };
|
|
1903
|
+
}
|
|
1904
|
+
const token = msg.params.consent_token;
|
|
1905
|
+
if (!token || typeof token !== "string" || token.trim() === "") {
|
|
1906
|
+
return {
|
|
1907
|
+
valid: false,
|
|
1908
|
+
reason: "TRAINING_DATA message missing consent_token (\xA717)"
|
|
1909
|
+
};
|
|
1910
|
+
}
|
|
1911
|
+
return { valid: true, reason: "ok" };
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
// src/offline.ts
|
|
1915
|
+
var SAFETY_MESSAGE_TYPE3 = 6;
|
|
1916
|
+
var DEFAULT_CROSS_OWNER_GRACE_S = 3600;
|
|
1917
|
+
var DEFAULT_KEY_TTL_S = 86400;
|
|
1918
|
+
var OfflineModeManager = class {
|
|
1919
|
+
crossOwnerGraceS;
|
|
1920
|
+
keyTtlS;
|
|
1921
|
+
_cachedKeys = [];
|
|
1922
|
+
constructor(crossOwnerGraceS = DEFAULT_CROSS_OWNER_GRACE_S, keyTtlS = DEFAULT_KEY_TTL_S) {
|
|
1923
|
+
this.crossOwnerGraceS = crossOwnerGraceS;
|
|
1924
|
+
this.keyTtlS = keyTtlS;
|
|
1925
|
+
}
|
|
1926
|
+
/**
|
|
1927
|
+
* Determine whether a command can be accepted in the current offline state.
|
|
1928
|
+
*
|
|
1929
|
+
* @param msg - The incoming message (SafetyMessage or plain object with message_type)
|
|
1930
|
+
* @param isOffline - Whether we are currently in offline mode
|
|
1931
|
+
* @param localNetwork - Whether the sender is on the local network
|
|
1932
|
+
* @param isOwner - Whether the sender has owner-level role (validated locally)
|
|
1933
|
+
* @param isCrossOwner - Whether this is a cross-owner (non-same-owner) command
|
|
1934
|
+
* @param nowMs - Optional current time in ms (for testing)
|
|
1935
|
+
*/
|
|
1936
|
+
canAcceptCommand(msg, isOffline, localNetwork, isOwner = true, isCrossOwner = false, offlineSinceMs, nowMs) {
|
|
1937
|
+
if (msg && msg.message_type === SAFETY_MESSAGE_TYPE3 && msg.safety_event === "ESTOP") {
|
|
1938
|
+
return { allowed: true, reason: "ESTOP always accepted (Protocol 66)" };
|
|
1939
|
+
}
|
|
1940
|
+
if (!isOffline) {
|
|
1941
|
+
return { allowed: true, reason: "online mode" };
|
|
1942
|
+
}
|
|
1943
|
+
if (!localNetwork) {
|
|
1944
|
+
return {
|
|
1945
|
+
allowed: false,
|
|
1946
|
+
reason: "offline mode: cross-network commands blocked"
|
|
1947
|
+
};
|
|
1948
|
+
}
|
|
1949
|
+
if (!isOwner) {
|
|
1950
|
+
return {
|
|
1951
|
+
allowed: false,
|
|
1952
|
+
reason: "offline mode: only owner-role commands accepted from local network"
|
|
1953
|
+
};
|
|
1954
|
+
}
|
|
1955
|
+
if (isCrossOwner && offlineSinceMs !== void 0) {
|
|
1956
|
+
const now = nowMs ?? Date.now();
|
|
1957
|
+
const offlineSeconds = (now - offlineSinceMs) / 1e3;
|
|
1958
|
+
if (offlineSeconds > this.crossOwnerGraceS) {
|
|
1959
|
+
return {
|
|
1960
|
+
allowed: false,
|
|
1961
|
+
reason: `offline mode: cross-owner grace period expired (${Math.round(offlineSeconds)}s > ${this.crossOwnerGraceS}s)`
|
|
1962
|
+
};
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
return {
|
|
1966
|
+
allowed: true,
|
|
1967
|
+
reason: "offline mode: owner command on local network accepted"
|
|
1968
|
+
};
|
|
1969
|
+
}
|
|
1970
|
+
/** Cache a public key for offline use */
|
|
1971
|
+
cacheKey(entry, nowMs) {
|
|
1972
|
+
const now = nowMs ?? Date.now();
|
|
1973
|
+
this._cachedKeys = this._cachedKeys.filter((k) => k.kid !== entry.kid);
|
|
1974
|
+
this._cachedKeys.push({
|
|
1975
|
+
...entry,
|
|
1976
|
+
cachedAtMs: now,
|
|
1977
|
+
ttlSeconds: this.keyTtlS
|
|
1978
|
+
});
|
|
1979
|
+
}
|
|
1980
|
+
/** Get a cached public key if still valid */
|
|
1981
|
+
getCachedKey(kid, nowMs) {
|
|
1982
|
+
const now = nowMs ?? Date.now();
|
|
1983
|
+
const entry = this._cachedKeys.find((k) => k.kid === kid);
|
|
1984
|
+
if (!entry) return void 0;
|
|
1985
|
+
const age = (now - entry.cachedAtMs) / 1e3;
|
|
1986
|
+
if (age > entry.ttlSeconds) {
|
|
1987
|
+
this._cachedKeys = this._cachedKeys.filter((k) => k.kid !== kid);
|
|
1988
|
+
return void 0;
|
|
1989
|
+
}
|
|
1990
|
+
return entry;
|
|
1991
|
+
}
|
|
1992
|
+
/** Protocol 66 manifest fields for offline mode */
|
|
1993
|
+
getManifestFields(offlineSinceMs, nowMs) {
|
|
1994
|
+
if (offlineSinceMs === void 0) {
|
|
1995
|
+
return { offline_mode: false, offline_since_s: 0 };
|
|
1996
|
+
}
|
|
1997
|
+
const now = nowMs ?? Date.now();
|
|
1998
|
+
return {
|
|
1999
|
+
offline_mode: true,
|
|
2000
|
+
offline_since_s: Math.round((now - offlineSinceMs) / 1e3)
|
|
2001
|
+
};
|
|
2002
|
+
}
|
|
2003
|
+
};
|
|
2004
|
+
|
|
2005
|
+
// src/faultReport.ts
|
|
2006
|
+
var FaultCode = /* @__PURE__ */ ((FaultCode2) => {
|
|
2007
|
+
FaultCode2["SENSOR_PROXIMITY_FAILURE"] = "SENSOR_PROXIMITY_FAILURE";
|
|
2008
|
+
FaultCode2["SENSOR_CAMERA_FAILURE"] = "SENSOR_CAMERA_FAILURE";
|
|
2009
|
+
FaultCode2["SENSOR_IMU_FAILURE"] = "SENSOR_IMU_FAILURE";
|
|
2010
|
+
FaultCode2["MOTOR_OVERCURRENT"] = "MOTOR_OVERCURRENT";
|
|
2011
|
+
FaultCode2["MOTOR_OVERTEMP"] = "MOTOR_OVERTEMP";
|
|
2012
|
+
FaultCode2["MOTOR_STALL"] = "MOTOR_STALL";
|
|
2013
|
+
FaultCode2["BATTERY_CRITICAL"] = "BATTERY_CRITICAL";
|
|
2014
|
+
FaultCode2["BATTERY_LOW"] = "BATTERY_LOW";
|
|
2015
|
+
FaultCode2["NETWORK_TIMEOUT"] = "NETWORK_TIMEOUT";
|
|
2016
|
+
FaultCode2["NETWORK_REGISTRY_UNREACHABLE"] = "NETWORK_REGISTRY_UNREACHABLE";
|
|
2017
|
+
FaultCode2["SAFETY_ESTOP_STUCK"] = "SAFETY_ESTOP_STUCK";
|
|
2018
|
+
FaultCode2["SAFETY_WATCHDOG_TIMEOUT"] = "SAFETY_WATCHDOG_TIMEOUT";
|
|
2019
|
+
FaultCode2["UNKNOWN"] = "UNKNOWN";
|
|
2020
|
+
return FaultCode2;
|
|
2021
|
+
})(FaultCode || {});
|
|
2022
|
+
function makeFaultReport(params) {
|
|
2023
|
+
return new RCANMessage({
|
|
2024
|
+
rcan: SPEC_VERSION,
|
|
2025
|
+
cmd: "FAULT_REPORT",
|
|
2026
|
+
target: params.target ?? "rcan://local/fault",
|
|
2027
|
+
params: {
|
|
2028
|
+
message_type: 26 /* FAULT_REPORT */,
|
|
2029
|
+
fault_code: params.faultCode,
|
|
2030
|
+
severity: params.severity,
|
|
2031
|
+
subsystem: params.subsystem,
|
|
2032
|
+
affects_safety: params.affectsSafety,
|
|
2033
|
+
safe_to_continue: params.safeToContinue,
|
|
2034
|
+
description: params.description ?? "",
|
|
2035
|
+
reported_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
2036
|
+
}
|
|
2037
|
+
});
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
// 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 || {});
|
|
2047
|
+
var DEFAULT_LOA_POLICY = {
|
|
2048
|
+
minLoaDiscover: 1 /* ANONYMOUS */,
|
|
2049
|
+
minLoaStatus: 1 /* ANONYMOUS */,
|
|
2050
|
+
minLoaChat: 1 /* ANONYMOUS */,
|
|
2051
|
+
minLoaControl: 1 /* ANONYMOUS */,
|
|
2052
|
+
minLoaSafety: 1 /* ANONYMOUS */
|
|
2053
|
+
};
|
|
2054
|
+
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 */
|
|
2060
|
+
};
|
|
2061
|
+
function extractLoaFromJwt(token) {
|
|
2062
|
+
try {
|
|
2063
|
+
const parts = token.split(".");
|
|
2064
|
+
if (parts.length < 2) return 1 /* ANONYMOUS */;
|
|
2065
|
+
const payloadB64 = (parts[1] ?? "").replace(/-/g, "+").replace(/_/g, "/");
|
|
2066
|
+
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
|
+
}
|
|
2078
|
+
} catch {
|
|
2079
|
+
}
|
|
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" };
|
|
2106
|
+
}
|
|
2107
|
+
return {
|
|
2108
|
+
valid: false,
|
|
2109
|
+
reason: `LOA_INSUFFICIENT: scope=${scope} requires LoA>=${min}, caller has LoA=${loa}`
|
|
2110
|
+
};
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
// src/federation.ts
|
|
2114
|
+
var RegistryTier = /* @__PURE__ */ ((RegistryTier2) => {
|
|
2115
|
+
RegistryTier2["ROOT"] = "root";
|
|
2116
|
+
RegistryTier2["AUTHORITATIVE"] = "authoritative";
|
|
2117
|
+
RegistryTier2["COMMUNITY"] = "community";
|
|
2118
|
+
return RegistryTier2;
|
|
2119
|
+
})(RegistryTier || {});
|
|
2120
|
+
var FederationSyncType = /* @__PURE__ */ ((FederationSyncType2) => {
|
|
2121
|
+
FederationSyncType2["CONSENT"] = "consent";
|
|
2122
|
+
FederationSyncType2["REVOCATION"] = "revocation";
|
|
2123
|
+
FederationSyncType2["KEY"] = "key";
|
|
2124
|
+
return FederationSyncType2;
|
|
2125
|
+
})(FederationSyncType || {});
|
|
2126
|
+
var CACHE_TTL_MS2 = 24 * 60 * 60 * 1e3;
|
|
2127
|
+
var TrustAnchorCache = class {
|
|
2128
|
+
store = /* @__PURE__ */ new Map();
|
|
2129
|
+
/** Store or refresh a registry identity. */
|
|
2130
|
+
set(identity) {
|
|
2131
|
+
this.store.set(identity.registryUrl, {
|
|
2132
|
+
identity,
|
|
2133
|
+
expiresAt: Date.now() + CACHE_TTL_MS2
|
|
2134
|
+
});
|
|
2135
|
+
}
|
|
2136
|
+
/**
|
|
2137
|
+
* Look up a registry URL.
|
|
2138
|
+
* Returns undefined when absent or when the TTL has expired.
|
|
2139
|
+
*/
|
|
2140
|
+
lookup(url) {
|
|
2141
|
+
const entry = this.store.get(url);
|
|
2142
|
+
if (!entry) return void 0;
|
|
2143
|
+
if (Date.now() > entry.expiresAt) {
|
|
2144
|
+
this.store.delete(url);
|
|
2145
|
+
return void 0;
|
|
2146
|
+
}
|
|
2147
|
+
return entry.identity;
|
|
2148
|
+
}
|
|
2149
|
+
/**
|
|
2150
|
+
* Discover a registry via DNS TXT record `_rcan-registry.<domain>`.
|
|
2151
|
+
*
|
|
2152
|
+
* The TXT record is expected to contain a JSON object with the
|
|
2153
|
+
* RegistryIdentity fields. Returns the identity and caches it.
|
|
2154
|
+
*
|
|
2155
|
+
* Node.js only — returns undefined in environments without `dns.promises`.
|
|
2156
|
+
*/
|
|
2157
|
+
async discoverViaDns(domain) {
|
|
2158
|
+
const hostname = `_rcan-registry.${domain}`;
|
|
2159
|
+
let records;
|
|
2160
|
+
try {
|
|
2161
|
+
const dnsModule = __require("dns");
|
|
2162
|
+
records = await dnsModule.promises.resolveTxt(hostname);
|
|
2163
|
+
} catch {
|
|
2164
|
+
return void 0;
|
|
2165
|
+
}
|
|
2166
|
+
for (const record of records) {
|
|
2167
|
+
const text = record.join("");
|
|
2168
|
+
try {
|
|
2169
|
+
const parsed = JSON.parse(text);
|
|
2170
|
+
if (parsed.registryUrl && parsed.tier && parsed.publicKeyPem && parsed.domain) {
|
|
2171
|
+
const identity = {
|
|
2172
|
+
registryUrl: parsed.registryUrl,
|
|
2173
|
+
tier: parsed.tier,
|
|
2174
|
+
publicKeyPem: parsed.publicKeyPem,
|
|
2175
|
+
domain: parsed.domain,
|
|
2176
|
+
verifiedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2177
|
+
};
|
|
2178
|
+
this.set(identity);
|
|
2179
|
+
return identity;
|
|
2180
|
+
}
|
|
2181
|
+
} catch {
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
return void 0;
|
|
2185
|
+
}
|
|
2186
|
+
/**
|
|
2187
|
+
* Verify a JWT was issued by the registry at `url`.
|
|
2188
|
+
*
|
|
2189
|
+
* Validates the `iss` claim and checks the registry is in the trust cache.
|
|
2190
|
+
* Full cryptographic signature verification requires the registry's public
|
|
2191
|
+
* key material — callers should perform additional checks using `publicKeyPem`
|
|
2192
|
+
* from the returned identity.
|
|
2193
|
+
*/
|
|
2194
|
+
async verifyRegistryJwt(token, url) {
|
|
2195
|
+
const identity = this.lookup(url);
|
|
2196
|
+
if (!identity) {
|
|
2197
|
+
throw new Error(`REGISTRY_UNKNOWN: ${url} is not in the trust cache`);
|
|
2198
|
+
}
|
|
2199
|
+
let iss;
|
|
2200
|
+
try {
|
|
2201
|
+
const parts = token.split(".");
|
|
2202
|
+
const payloadB64 = (parts[1] ?? "").replace(/-/g, "+").replace(/_/g, "/");
|
|
2203
|
+
const padded = payloadB64 + "=".repeat((4 - payloadB64.length % 4) % 4);
|
|
2204
|
+
let json;
|
|
2205
|
+
if (typeof atob !== "undefined") {
|
|
2206
|
+
json = atob(padded);
|
|
2207
|
+
} else {
|
|
2208
|
+
json = Buffer.from(padded, "base64").toString("utf-8");
|
|
2209
|
+
}
|
|
2210
|
+
const claims = JSON.parse(json);
|
|
2211
|
+
iss = typeof claims["iss"] === "string" ? claims["iss"] : void 0;
|
|
2212
|
+
} catch {
|
|
2213
|
+
throw new Error("REGISTRY_JWT_MALFORMED: cannot decode token payload");
|
|
2214
|
+
}
|
|
2215
|
+
if (iss !== url) {
|
|
2216
|
+
throw new Error(
|
|
2217
|
+
`REGISTRY_JWT_ISS_MISMATCH: expected iss=${url}, got iss=${iss ?? "(none)"}`
|
|
2218
|
+
);
|
|
2219
|
+
}
|
|
2220
|
+
return identity;
|
|
2221
|
+
}
|
|
2222
|
+
};
|
|
2223
|
+
function generateId5() {
|
|
2224
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
2225
|
+
return crypto.randomUUID();
|
|
2226
|
+
}
|
|
2227
|
+
const bytes = Array.from({ length: 16 }, () => Math.floor(Math.random() * 256));
|
|
2228
|
+
bytes[6] = (bytes[6] ?? 0) & 15 | 64;
|
|
2229
|
+
bytes[8] = (bytes[8] ?? 0) & 63 | 128;
|
|
2230
|
+
const hex = bytes.map((b) => b.toString(16).padStart(2, "0"));
|
|
2231
|
+
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("")}`;
|
|
2232
|
+
}
|
|
2233
|
+
function makeFederationSync(source, target, syncType, payload) {
|
|
2234
|
+
return new RCANMessage({
|
|
2235
|
+
rcan: "1.6",
|
|
2236
|
+
rcanVersion: "1.6",
|
|
2237
|
+
cmd: "federation_sync",
|
|
2238
|
+
target,
|
|
2239
|
+
params: {
|
|
2240
|
+
msg_type: 12 /* FEDERATION_SYNC */,
|
|
2241
|
+
msg_id: generateId5(),
|
|
2242
|
+
source_registry: source,
|
|
2243
|
+
target_registry: target,
|
|
2244
|
+
sync_type: syncType,
|
|
2245
|
+
payload
|
|
2246
|
+
},
|
|
2247
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2248
|
+
});
|
|
2249
|
+
}
|
|
2250
|
+
async function validateCrossRegistryCommand(msg, localRegistry, trustCache) {
|
|
2251
|
+
const msgType = msg.params?.["msg_type"];
|
|
2252
|
+
const isEstop = msgType === 6 /* SAFETY */ || msgType === 6 || msg.cmd === "estop" || msg.cmd === "ESTOP";
|
|
2253
|
+
if (isEstop) {
|
|
2254
|
+
return { valid: true, reason: "ESTOP always permitted (P66 invariant)" };
|
|
2255
|
+
}
|
|
2256
|
+
const sourceRegistry = msg.params?.["source_registry"] ?? msg.params?.["from_registry"];
|
|
2257
|
+
if (!sourceRegistry || sourceRegistry === localRegistry) {
|
|
2258
|
+
return { valid: true, reason: "local registry; no federation check needed" };
|
|
2259
|
+
}
|
|
2260
|
+
const identity = trustCache.lookup(sourceRegistry);
|
|
2261
|
+
if (!identity) {
|
|
2262
|
+
return {
|
|
2263
|
+
valid: false,
|
|
2264
|
+
reason: `REGISTRY_UNKNOWN: ${sourceRegistry} is not in the local trust cache`
|
|
2265
|
+
};
|
|
2266
|
+
}
|
|
2267
|
+
let loa = 1 /* ANONYMOUS */;
|
|
2268
|
+
const registryJwt = msg.params?.["registry_jwt"];
|
|
2269
|
+
if (registryJwt) {
|
|
2270
|
+
loa = extractLoaFromJwt(registryJwt);
|
|
2271
|
+
} else if (typeof msg.loa === "number") {
|
|
2272
|
+
loa = msg.loa;
|
|
2273
|
+
}
|
|
2274
|
+
if (loa < 2 /* EMAIL_VERIFIED */) {
|
|
2275
|
+
return {
|
|
2276
|
+
valid: false,
|
|
2277
|
+
reason: `LOA_INSUFFICIENT: cross-registry commands require LoA>=2 (EMAIL_VERIFIED), got LoA=${loa}`
|
|
2278
|
+
};
|
|
2279
|
+
}
|
|
2280
|
+
return { valid: true, reason: "cross-registry command accepted" };
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2283
|
+
// src/transport.ts
|
|
2284
|
+
var TransportError = class _TransportError extends Error {
|
|
2285
|
+
constructor(message) {
|
|
2286
|
+
super(message);
|
|
2287
|
+
this.name = "TransportError";
|
|
2288
|
+
Object.setPrototypeOf(this, _TransportError.prototype);
|
|
2289
|
+
}
|
|
2290
|
+
};
|
|
2291
|
+
var TransportEncoding = /* @__PURE__ */ ((TransportEncoding2) => {
|
|
2292
|
+
TransportEncoding2["HTTP"] = "http";
|
|
2293
|
+
TransportEncoding2["COMPACT"] = "compact";
|
|
2294
|
+
TransportEncoding2["MINIMAL"] = "minimal";
|
|
2295
|
+
TransportEncoding2["BLE"] = "ble";
|
|
2296
|
+
return TransportEncoding2;
|
|
2297
|
+
})(TransportEncoding || {});
|
|
2298
|
+
var COMPACT_ENCODE = {
|
|
2299
|
+
msg_type: "t",
|
|
2300
|
+
msg_id: "i",
|
|
2301
|
+
timestamp: "ts",
|
|
2302
|
+
from_rrn: "f",
|
|
2303
|
+
to_rrn: "to",
|
|
2304
|
+
scope: "s",
|
|
2305
|
+
payload: "p",
|
|
2306
|
+
signature: "sig"
|
|
2307
|
+
};
|
|
2308
|
+
var COMPACT_DECODE = Object.fromEntries(
|
|
2309
|
+
Object.entries(COMPACT_ENCODE).map(([k, v]) => [v, k])
|
|
2310
|
+
);
|
|
2311
|
+
function encodeCompact(message) {
|
|
2312
|
+
const full = message.toJSON();
|
|
2313
|
+
const compact = {};
|
|
2314
|
+
for (const [key, value] of Object.entries(full)) {
|
|
2315
|
+
const short = COMPACT_ENCODE[key];
|
|
2316
|
+
compact[short ?? key] = value;
|
|
2317
|
+
}
|
|
2318
|
+
if (compact["p"] && typeof compact["p"] === "object") {
|
|
2319
|
+
const params = compact["p"];
|
|
2320
|
+
const compactParams = {};
|
|
2321
|
+
for (const [k, v] of Object.entries(params)) {
|
|
2322
|
+
const short = COMPACT_ENCODE[k];
|
|
2323
|
+
compactParams[short ?? k] = v;
|
|
2324
|
+
}
|
|
2325
|
+
compact["p"] = compactParams;
|
|
2326
|
+
}
|
|
2327
|
+
const json = JSON.stringify(compact);
|
|
2328
|
+
const encoder = new TextEncoder();
|
|
2329
|
+
return encoder.encode(json);
|
|
2330
|
+
}
|
|
2331
|
+
function decodeCompact(data) {
|
|
2332
|
+
const decoder = new TextDecoder();
|
|
2333
|
+
const json = decoder.decode(data);
|
|
2334
|
+
const compact = JSON.parse(json);
|
|
2335
|
+
const full = {};
|
|
2336
|
+
for (const [key, value] of Object.entries(compact)) {
|
|
2337
|
+
const long = COMPACT_DECODE[key];
|
|
2338
|
+
full[long ?? key] = value;
|
|
2339
|
+
}
|
|
2340
|
+
if (full["payload"] && typeof full["payload"] === "object") {
|
|
2341
|
+
const params = full["payload"];
|
|
2342
|
+
const expandedParams = {};
|
|
2343
|
+
for (const [k, v] of Object.entries(params)) {
|
|
2344
|
+
const long = COMPACT_DECODE[k];
|
|
2345
|
+
expandedParams[long ?? k] = v;
|
|
2346
|
+
}
|
|
2347
|
+
full["payload"] = expandedParams;
|
|
2348
|
+
}
|
|
2349
|
+
return new RCANMessage({
|
|
2350
|
+
rcan: full["rcan"] ?? "1.6",
|
|
2351
|
+
rcanVersion: full["rcanVersion"],
|
|
2352
|
+
cmd: full["cmd"],
|
|
2353
|
+
target: full["target"],
|
|
2354
|
+
params: full["params"] ?? full["payload"] ?? {},
|
|
2355
|
+
timestamp: full["timestamp"],
|
|
2356
|
+
confidence: full["confidence"],
|
|
2357
|
+
signature: full["signature"]
|
|
2358
|
+
});
|
|
2359
|
+
}
|
|
2360
|
+
var MINIMAL_SIZE = 32;
|
|
2361
|
+
var SAFETY_TYPE = 6;
|
|
2362
|
+
async function sha256Bytes(input) {
|
|
2363
|
+
const encoded = new TextEncoder().encode(input);
|
|
2364
|
+
const ab = new ArrayBuffer(encoded.byteLength);
|
|
2365
|
+
new Uint8Array(ab).set(encoded);
|
|
2366
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", ab);
|
|
2367
|
+
return new Uint8Array(hashBuffer);
|
|
2368
|
+
}
|
|
2369
|
+
async function encodeMinimal(message) {
|
|
2370
|
+
const msgType = message.params?.["msg_type"] ?? 0;
|
|
2371
|
+
if (msgType !== SAFETY_TYPE) {
|
|
2372
|
+
throw new TransportError(
|
|
2373
|
+
`encodeMinimal only supports SAFETY (type 6) messages; got type=${msgType}`
|
|
2374
|
+
);
|
|
2375
|
+
}
|
|
2376
|
+
const fromRrn = message.params?.["from_rrn"] ?? message.target ?? "";
|
|
2377
|
+
const toRrn = message.params?.["to_rrn"] ?? message.target ?? "";
|
|
2378
|
+
const fromHash = await sha256Bytes(fromRrn);
|
|
2379
|
+
const toHash = await sha256Bytes(toRrn);
|
|
2380
|
+
const sig = (message.signature?.sig ?? "").replace(/[^A-Za-z0-9+/=]/g, "");
|
|
2381
|
+
let sigBytes;
|
|
2382
|
+
try {
|
|
2383
|
+
if (typeof atob !== "undefined") {
|
|
2384
|
+
const raw = atob(sig.slice(0, 16));
|
|
2385
|
+
sigBytes = new Uint8Array(raw.length);
|
|
2386
|
+
for (let i = 0; i < raw.length; i++) sigBytes[i] = raw.charCodeAt(i);
|
|
2387
|
+
} else {
|
|
2388
|
+
sigBytes = Buffer.from(sig.slice(0, 16), "base64");
|
|
2389
|
+
}
|
|
2390
|
+
} catch {
|
|
2391
|
+
sigBytes = new Uint8Array(8);
|
|
2392
|
+
}
|
|
2393
|
+
const ts = message.timestamp ? Math.floor(new Date(message.timestamp).getTime() / 1e3) : Math.floor(Date.now() / 1e3);
|
|
2394
|
+
const unix32 = ts >>> 0;
|
|
2395
|
+
const out = new Uint8Array(MINIMAL_SIZE);
|
|
2396
|
+
const view = new DataView(out.buffer);
|
|
2397
|
+
view.setUint16(0, SAFETY_TYPE, false);
|
|
2398
|
+
out.set(fromHash.subarray(0, 8), 2);
|
|
2399
|
+
out.set(toHash.subarray(0, 8), 10);
|
|
2400
|
+
view.setUint32(18, unix32, false);
|
|
2401
|
+
const sigSlice = new Uint8Array(8);
|
|
2402
|
+
sigSlice.set(sigBytes.subarray(0, Math.min(8, sigBytes.length)));
|
|
2403
|
+
out.set(sigSlice, 22);
|
|
2404
|
+
let checksum = 0;
|
|
2405
|
+
for (let i = 0; i < 30; i++) checksum ^= out[i] ?? 0;
|
|
2406
|
+
view.setUint16(30, checksum & 65535, false);
|
|
2407
|
+
if (out.length !== MINIMAL_SIZE) {
|
|
2408
|
+
throw new TransportError(
|
|
2409
|
+
`encodeMinimal assertion failed: expected ${MINIMAL_SIZE} bytes, got ${out.length}`
|
|
2410
|
+
);
|
|
2411
|
+
}
|
|
2412
|
+
return out;
|
|
2413
|
+
}
|
|
2414
|
+
function decodeMinimal(data) {
|
|
2415
|
+
if (data.length !== MINIMAL_SIZE) {
|
|
2416
|
+
throw new TransportError(
|
|
2417
|
+
`decodeMinimal: expected ${MINIMAL_SIZE} bytes, got ${data.length}`
|
|
2418
|
+
);
|
|
2419
|
+
}
|
|
2420
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
2421
|
+
const msgType = view.getUint16(0, false);
|
|
2422
|
+
const fromHash = data.subarray(2, 10);
|
|
2423
|
+
const toHash = data.subarray(10, 18);
|
|
2424
|
+
const unix32 = view.getUint32(18, false);
|
|
2425
|
+
const sigTrunc = data.subarray(22, 30);
|
|
2426
|
+
const timestamp = new Date(unix32 * 1e3).toISOString();
|
|
2427
|
+
const toHex2 = (b) => Array.from(b).map((x) => x.toString(16).padStart(2, "0")).join("");
|
|
2428
|
+
return {
|
|
2429
|
+
params: {
|
|
2430
|
+
msg_type: msgType,
|
|
2431
|
+
from_hash: toHex2(fromHash),
|
|
2432
|
+
to_hash: toHex2(toHash),
|
|
2433
|
+
timestamp_s: unix32,
|
|
2434
|
+
sig_truncated: toHex2(sigTrunc)
|
|
2435
|
+
},
|
|
2436
|
+
timestamp
|
|
2437
|
+
};
|
|
2438
|
+
}
|
|
2439
|
+
var DEFAULT_MTU = 251;
|
|
2440
|
+
var BLE_HEADER_SIZE = 3;
|
|
2441
|
+
function encodeBleFrames(message, mtu = DEFAULT_MTU) {
|
|
2442
|
+
const payload = encodeCompact(message);
|
|
2443
|
+
const chunkSize = mtu - BLE_HEADER_SIZE;
|
|
2444
|
+
if (chunkSize <= 0) {
|
|
2445
|
+
throw new TransportError(`MTU ${mtu} is too small (need at least ${BLE_HEADER_SIZE + 1})`);
|
|
2446
|
+
}
|
|
2447
|
+
const totalChunks = Math.ceil(payload.length / chunkSize);
|
|
2448
|
+
const frames = [];
|
|
2449
|
+
for (let i = 0; i < totalChunks; i++) {
|
|
2450
|
+
const chunk = payload.subarray(i * chunkSize, (i + 1) * chunkSize);
|
|
2451
|
+
const frame = new Uint8Array(BLE_HEADER_SIZE + chunk.length);
|
|
2452
|
+
frame[0] = i;
|
|
2453
|
+
frame[1] = totalChunks;
|
|
2454
|
+
frame[2] = i === totalChunks - 1 ? 1 : 0;
|
|
2455
|
+
frame.set(chunk, BLE_HEADER_SIZE);
|
|
2456
|
+
frames.push(frame);
|
|
2457
|
+
}
|
|
2458
|
+
return frames;
|
|
2459
|
+
}
|
|
2460
|
+
function decodeBleFrames(frames) {
|
|
2461
|
+
if (frames.length === 0) {
|
|
2462
|
+
throw new TransportError("decodeBleFrames: no frames provided");
|
|
2463
|
+
}
|
|
2464
|
+
const sorted = [...frames].sort((a, b) => (a[0] ?? 0) - (b[0] ?? 0));
|
|
2465
|
+
const expectedTotal = sorted[0]?.[1] ?? sorted.length;
|
|
2466
|
+
if (sorted.length !== expectedTotal) {
|
|
2467
|
+
throw new TransportError(
|
|
2468
|
+
`decodeBleFrames: expected ${expectedTotal} frames, got ${sorted.length}`
|
|
2469
|
+
);
|
|
2470
|
+
}
|
|
2471
|
+
const payloadChunks = sorted.map((f) => f.subarray(BLE_HEADER_SIZE));
|
|
2472
|
+
const totalLen = payloadChunks.reduce((s, c) => s + c.length, 0);
|
|
2473
|
+
const payload = new Uint8Array(totalLen);
|
|
2474
|
+
let offset = 0;
|
|
2475
|
+
for (const chunk of payloadChunks) {
|
|
2476
|
+
payload.set(chunk, offset);
|
|
2477
|
+
offset += chunk.length;
|
|
2478
|
+
}
|
|
2479
|
+
return decodeCompact(payload);
|
|
2480
|
+
}
|
|
2481
|
+
function selectTransport(available, message) {
|
|
2482
|
+
const msgType = message.params?.["msg_type"] ?? 0;
|
|
2483
|
+
const isSafety = msgType === SAFETY_TYPE;
|
|
2484
|
+
const has = (enc) => available.includes(enc);
|
|
2485
|
+
if (isSafety) {
|
|
2486
|
+
if (has("minimal" /* MINIMAL */)) return "minimal" /* MINIMAL */;
|
|
2487
|
+
if (has("ble" /* BLE */)) return "ble" /* BLE */;
|
|
2488
|
+
if (has("compact" /* COMPACT */)) return "compact" /* COMPACT */;
|
|
2489
|
+
if (has("http" /* HTTP */)) return "http" /* HTTP */;
|
|
2490
|
+
} else {
|
|
2491
|
+
if (has("http" /* HTTP */)) return "http" /* HTTP */;
|
|
2492
|
+
if (has("compact" /* COMPACT */)) return "compact" /* COMPACT */;
|
|
2493
|
+
if (has("ble" /* BLE */)) return "ble" /* BLE */;
|
|
2494
|
+
}
|
|
2495
|
+
throw new TransportError(
|
|
2496
|
+
`No suitable transport available from: [${available.join(", ")}]`
|
|
2497
|
+
);
|
|
2498
|
+
}
|
|
2499
|
+
|
|
2500
|
+
// src/multimodal.ts
|
|
2501
|
+
var MediaEncoding = /* @__PURE__ */ ((MediaEncoding2) => {
|
|
2502
|
+
MediaEncoding2["BASE64"] = "base64";
|
|
2503
|
+
MediaEncoding2["REF"] = "ref";
|
|
2504
|
+
return MediaEncoding2;
|
|
2505
|
+
})(MediaEncoding || {});
|
|
2506
|
+
function generateId6() {
|
|
2507
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
2508
|
+
return crypto.randomUUID();
|
|
2509
|
+
}
|
|
2510
|
+
const bytes = Array.from({ length: 16 }, () => Math.floor(Math.random() * 256));
|
|
2511
|
+
bytes[6] = (bytes[6] ?? 0) & 15 | 64;
|
|
2512
|
+
bytes[8] = (bytes[8] ?? 0) & 63 | 128;
|
|
2513
|
+
const hex = bytes.map((b) => b.toString(16).padStart(2, "0"));
|
|
2514
|
+
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
|
+
}
|
|
2516
|
+
async function computeSha256Hex(data) {
|
|
2517
|
+
const ab = new ArrayBuffer(data.byteLength);
|
|
2518
|
+
new Uint8Array(ab).set(data);
|
|
2519
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", ab);
|
|
2520
|
+
const hashArray = new Uint8Array(hashBuffer);
|
|
2521
|
+
return Array.from(hashArray).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
2522
|
+
}
|
|
2523
|
+
function uint8ToBase64(data) {
|
|
2524
|
+
if (typeof Buffer !== "undefined") {
|
|
2525
|
+
return Buffer.from(data).toString("base64");
|
|
2526
|
+
}
|
|
2527
|
+
let binary = "";
|
|
2528
|
+
for (let i = 0; i < data.length; i++) binary += String.fromCharCode(data[i] ?? 0);
|
|
2529
|
+
return btoa(binary);
|
|
2530
|
+
}
|
|
2531
|
+
function rebuildMessage(base, overrides) {
|
|
2532
|
+
const data = { ...base.toJSON(), ...overrides };
|
|
2533
|
+
return RCANMessage.fromJSON(data);
|
|
2534
|
+
}
|
|
2535
|
+
async function addMediaInline(message, data, mimeType) {
|
|
2536
|
+
const hashSha256 = await computeSha256Hex(data);
|
|
2537
|
+
const dataB64 = uint8ToBase64(data);
|
|
2538
|
+
const chunk = {
|
|
2539
|
+
chunkId: generateId6(),
|
|
2540
|
+
mimeType,
|
|
2541
|
+
encoding: "base64" /* BASE64 */,
|
|
2542
|
+
hashSha256,
|
|
2543
|
+
dataB64,
|
|
2544
|
+
sizeBytes: data.length
|
|
2545
|
+
};
|
|
2546
|
+
const existing = message.mediaChunks ?? [];
|
|
2547
|
+
return rebuildMessage(message, { mediaChunks: [...existing, chunk] });
|
|
2548
|
+
}
|
|
2549
|
+
function addMediaRef(message, refUrl, mimeType, hashSha256, sizeBytes) {
|
|
2550
|
+
const chunk = {
|
|
2551
|
+
chunkId: generateId6(),
|
|
2552
|
+
mimeType,
|
|
2553
|
+
encoding: "ref" /* REF */,
|
|
2554
|
+
hashSha256,
|
|
2555
|
+
refUrl,
|
|
2556
|
+
sizeBytes
|
|
2557
|
+
};
|
|
2558
|
+
const existing = message.mediaChunks ?? [];
|
|
2559
|
+
return rebuildMessage(message, { mediaChunks: [...existing, chunk] });
|
|
2560
|
+
}
|
|
2561
|
+
async function validateMediaChunks(message) {
|
|
2562
|
+
const chunks = message.mediaChunks ?? [];
|
|
2563
|
+
if (chunks.length === 0) {
|
|
2564
|
+
return { valid: true, reason: "no media chunks" };
|
|
2565
|
+
}
|
|
2566
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
2567
|
+
const chunk = chunks[i];
|
|
2568
|
+
if (!chunk.chunkId) return { valid: false, reason: `chunk[${i}]: missing chunkId` };
|
|
2569
|
+
if (!chunk.mimeType) return { valid: false, reason: `chunk[${i}]: missing mimeType` };
|
|
2570
|
+
if (!chunk.hashSha256) return { valid: false, reason: `chunk[${i}]: missing hashSha256` };
|
|
2571
|
+
if (chunk.sizeBytes < 0) return { valid: false, reason: `chunk[${i}]: sizeBytes must be >= 0` };
|
|
2572
|
+
if (chunk.encoding === "base64" /* BASE64 */) {
|
|
2573
|
+
if (!chunk.dataB64) {
|
|
2574
|
+
return { valid: false, reason: `chunk[${i}]: BASE64 encoding requires dataB64` };
|
|
2575
|
+
}
|
|
2576
|
+
let decoded;
|
|
2577
|
+
try {
|
|
2578
|
+
if (typeof Buffer !== "undefined") {
|
|
2579
|
+
decoded = Buffer.from(chunk.dataB64, "base64");
|
|
2580
|
+
} else {
|
|
2581
|
+
const raw = atob(chunk.dataB64);
|
|
2582
|
+
decoded = new Uint8Array(raw.length);
|
|
2583
|
+
for (let j = 0; j < raw.length; j++) decoded[j] = raw.charCodeAt(j);
|
|
2584
|
+
}
|
|
2585
|
+
} catch {
|
|
2586
|
+
return { valid: false, reason: `chunk[${i}]: failed to decode base64 data` };
|
|
2587
|
+
}
|
|
2588
|
+
const actualHash = await computeSha256Hex(decoded);
|
|
2589
|
+
if (actualHash !== chunk.hashSha256) {
|
|
2590
|
+
return {
|
|
2591
|
+
valid: false,
|
|
2592
|
+
reason: `chunk[${i}]: SHA-256 mismatch (expected ${chunk.hashSha256}, got ${actualHash})`
|
|
2593
|
+
};
|
|
2594
|
+
}
|
|
2595
|
+
} else if (chunk.encoding === "ref" /* REF */) {
|
|
2596
|
+
if (!chunk.refUrl) {
|
|
2597
|
+
return { valid: false, reason: `chunk[${i}]: REF encoding requires refUrl` };
|
|
2598
|
+
}
|
|
2599
|
+
} else {
|
|
2600
|
+
return { valid: false, reason: `chunk[${i}]: unknown encoding '${chunk.encoding}'` };
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
return { valid: true, reason: "ok" };
|
|
2604
|
+
}
|
|
2605
|
+
async function makeTrainingDataMessage(media) {
|
|
2606
|
+
let msg = new RCANMessage({
|
|
2607
|
+
rcan: "1.6",
|
|
2608
|
+
rcanVersion: "1.6",
|
|
2609
|
+
cmd: "training_data",
|
|
2610
|
+
target: "rcan://training/data",
|
|
2611
|
+
params: {
|
|
2612
|
+
msg_type: 10 /* TRAINING_DATA */,
|
|
2613
|
+
msg_id: generateId6()
|
|
2614
|
+
},
|
|
2615
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2616
|
+
});
|
|
2617
|
+
for (const item of media) {
|
|
2618
|
+
msg = await addMediaInline(msg, item.data, item.mimeType);
|
|
2619
|
+
}
|
|
2620
|
+
return msg;
|
|
2621
|
+
}
|
|
2622
|
+
async function makeStreamChunk(streamId, data, mimeType, chunkIndex, isFinal) {
|
|
2623
|
+
const hashSha256 = await computeSha256Hex(data);
|
|
2624
|
+
const dataB64 = uint8ToBase64(data);
|
|
2625
|
+
const chunk = {
|
|
2626
|
+
chunkId: generateId6(),
|
|
2627
|
+
mimeType,
|
|
2628
|
+
encoding: "base64" /* BASE64 */,
|
|
2629
|
+
hashSha256,
|
|
2630
|
+
dataB64,
|
|
2631
|
+
sizeBytes: data.length
|
|
2632
|
+
};
|
|
2633
|
+
const streamChunkMeta = {
|
|
2634
|
+
streamId,
|
|
2635
|
+
chunkIndex,
|
|
2636
|
+
isFinal,
|
|
2637
|
+
chunk
|
|
2638
|
+
};
|
|
2639
|
+
let msg = new RCANMessage({
|
|
2640
|
+
rcan: "1.6",
|
|
2641
|
+
rcanVersion: "1.6",
|
|
2642
|
+
cmd: "stream_chunk",
|
|
2643
|
+
target: "rcan://streaming/chunk",
|
|
2644
|
+
params: {
|
|
2645
|
+
msg_type: 7 /* SENSOR_DATA */,
|
|
2646
|
+
msg_id: generateId6(),
|
|
2647
|
+
stream_chunk: streamChunkMeta
|
|
2648
|
+
},
|
|
2649
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2650
|
+
});
|
|
2651
|
+
msg = rebuildMessage(msg, { mediaChunks: [chunk] });
|
|
2652
|
+
return msg;
|
|
2653
|
+
}
|
|
2654
|
+
|
|
1143
2655
|
// src/index.ts
|
|
1144
|
-
var VERSION = "0.
|
|
1145
|
-
var
|
|
1146
|
-
var RCAN_VERSION = "1.2";
|
|
2656
|
+
var VERSION = "0.6.0";
|
|
2657
|
+
var RCAN_VERSION = "1.6";
|
|
1147
2658
|
export {
|
|
1148
2659
|
AuditChain,
|
|
1149
2660
|
AuditError,
|
|
2661
|
+
ClockDriftError,
|
|
1150
2662
|
CommitmentRecord,
|
|
1151
2663
|
ConfidenceGate,
|
|
2664
|
+
DEFAULT_LOA_POLICY,
|
|
2665
|
+
DataCategory,
|
|
2666
|
+
FaultCode,
|
|
2667
|
+
FederationSyncType,
|
|
1152
2668
|
GateError,
|
|
1153
2669
|
HiTLGate,
|
|
2670
|
+
KeyStore,
|
|
2671
|
+
LevelOfAssurance,
|
|
2672
|
+
MediaEncoding,
|
|
2673
|
+
MessageType,
|
|
1154
2674
|
NodeClient,
|
|
2675
|
+
OfflineModeManager,
|
|
2676
|
+
PRODUCTION_LOA_POLICY,
|
|
2677
|
+
QoSAckTimeoutError,
|
|
2678
|
+
QoSLevel,
|
|
2679
|
+
QoSManager,
|
|
1155
2680
|
RCANAddressError,
|
|
2681
|
+
RCANConfigAuthorizationError,
|
|
2682
|
+
RCANDelegationChainError,
|
|
1156
2683
|
RCANError,
|
|
1157
2684
|
RCANGateError,
|
|
1158
2685
|
RCANMessage,
|
|
@@ -1162,19 +2689,73 @@ export {
|
|
|
1162
2689
|
RCANNodeSyncError,
|
|
1163
2690
|
RCANNodeTrustError,
|
|
1164
2691
|
RCANRegistryError,
|
|
2692
|
+
RCANReplayAttackError,
|
|
1165
2693
|
RCANSignatureError,
|
|
1166
2694
|
RCANValidationError,
|
|
2695
|
+
RCANVersionIncompatibleError,
|
|
1167
2696
|
RCAN_VERSION,
|
|
1168
2697
|
RegistryClient,
|
|
2698
|
+
RegistryTier,
|
|
2699
|
+
ReplayCache,
|
|
2700
|
+
RevocationCache,
|
|
1169
2701
|
RobotURI,
|
|
1170
2702
|
RobotURIError,
|
|
2703
|
+
SAFETY_MESSAGE_TYPE,
|
|
2704
|
+
SDK_VERSION,
|
|
1171
2705
|
SPEC_VERSION,
|
|
2706
|
+
TransportEncoding,
|
|
2707
|
+
TransportError,
|
|
2708
|
+
TrustAnchorCache,
|
|
1172
2709
|
VERSION,
|
|
2710
|
+
addDelegationHop,
|
|
2711
|
+
addMediaInline,
|
|
2712
|
+
addMediaRef,
|
|
2713
|
+
assertClockSynced,
|
|
2714
|
+
checkClockSync,
|
|
2715
|
+
checkRevocation,
|
|
2716
|
+
decodeBleFrames,
|
|
2717
|
+
decodeCompact,
|
|
2718
|
+
decodeMinimal,
|
|
2719
|
+
encodeBleFrames,
|
|
2720
|
+
encodeCompact,
|
|
2721
|
+
encodeMinimal,
|
|
2722
|
+
extractLoaFromJwt,
|
|
1173
2723
|
fetchCanonicalSchema,
|
|
2724
|
+
isSafetyMessage,
|
|
2725
|
+
makeCloudRelayMessage,
|
|
2726
|
+
makeConfigUpdate,
|
|
2727
|
+
makeConsentDeny,
|
|
2728
|
+
makeConsentGrant,
|
|
2729
|
+
makeConsentRequest,
|
|
2730
|
+
makeEstopMessage,
|
|
2731
|
+
makeEstopWithQoS,
|
|
2732
|
+
makeFaultReport,
|
|
2733
|
+
makeFederationSync,
|
|
2734
|
+
makeKeyRotationMessage,
|
|
2735
|
+
makeResumeMessage,
|
|
2736
|
+
makeRevocationBroadcast,
|
|
2737
|
+
makeStopMessage,
|
|
2738
|
+
makeStreamChunk,
|
|
2739
|
+
makeTrainingConsentDeny,
|
|
2740
|
+
makeTrainingConsentGrant,
|
|
2741
|
+
makeTrainingConsentRequest,
|
|
2742
|
+
makeTrainingDataMessage,
|
|
2743
|
+
makeTransparencyMessage,
|
|
2744
|
+
selectTransport,
|
|
1174
2745
|
validateConfig,
|
|
1175
2746
|
validateConfigAgainstSchema,
|
|
2747
|
+
validateConfigUpdate,
|
|
2748
|
+
validateConsentMessage,
|
|
2749
|
+
validateCrossRegistryCommand,
|
|
2750
|
+
validateDelegationChain,
|
|
2751
|
+
validateLoaForScope,
|
|
2752
|
+
validateMediaChunks,
|
|
1176
2753
|
validateMessage,
|
|
1177
2754
|
validateNodeAgainstSchema,
|
|
1178
|
-
|
|
2755
|
+
validateReplay,
|
|
2756
|
+
validateSafetyMessage,
|
|
2757
|
+
validateTrainingDataMessage,
|
|
2758
|
+
validateURI,
|
|
2759
|
+
validateVersionCompat
|
|
1179
2760
|
};
|
|
1180
2761
|
//# sourceMappingURL=index.mjs.map
|