@continuonai/rcan-ts 0.1.1
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 +152 -0
- package/dist/browser.d.mts +353 -0
- package/dist/browser.mjs +888 -0
- package/dist/browser.mjs.map +1 -0
- package/dist/index.d.mts +353 -0
- package/dist/index.d.ts +353 -0
- package/dist/index.js +924 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +883 -0
- package/dist/index.mjs.map +1 -0
- package/dist/rcan-validate.js +3593 -0
- package/package.json +66 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,883 @@
|
|
|
1
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
+
}) : x)(function(x) {
|
|
4
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
5
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
// src/address.ts
|
|
9
|
+
var RobotURIError = class extends Error {
|
|
10
|
+
constructor(message) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = "RobotURIError";
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
var RobotURI = class _RobotURI {
|
|
16
|
+
registry;
|
|
17
|
+
manufacturer;
|
|
18
|
+
model;
|
|
19
|
+
version;
|
|
20
|
+
deviceId;
|
|
21
|
+
constructor(opts) {
|
|
22
|
+
this.registry = opts.registry;
|
|
23
|
+
this.manufacturer = opts.manufacturer;
|
|
24
|
+
this.model = opts.model;
|
|
25
|
+
this.version = opts.version;
|
|
26
|
+
this.deviceId = opts.deviceId;
|
|
27
|
+
}
|
|
28
|
+
/** Parse a RCAN URI string. Throws RobotURIError on invalid input. */
|
|
29
|
+
static parse(uri) {
|
|
30
|
+
if (!uri.startsWith("rcan://")) {
|
|
31
|
+
throw new RobotURIError(`URI must start with 'rcan://' \u2014 got: ${uri}`);
|
|
32
|
+
}
|
|
33
|
+
const withoutScheme = uri.slice("rcan://".length);
|
|
34
|
+
const parts = withoutScheme.split("/");
|
|
35
|
+
if (parts.length !== 5) {
|
|
36
|
+
throw new RobotURIError(
|
|
37
|
+
`URI must have exactly 5 path segments (registry/manufacturer/model/version/device-id) \u2014 got ${parts.length} in: ${uri}`
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
const [registry, manufacturer, model, version, deviceId] = parts;
|
|
41
|
+
for (const [name, value] of [
|
|
42
|
+
["registry", registry],
|
|
43
|
+
["manufacturer", manufacturer],
|
|
44
|
+
["model", model],
|
|
45
|
+
["version", version],
|
|
46
|
+
["device-id", deviceId]
|
|
47
|
+
]) {
|
|
48
|
+
if (!value || value.trim() === "") {
|
|
49
|
+
throw new RobotURIError(`URI segment '${name}' must not be empty`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return new _RobotURI({ registry, manufacturer, model, version, deviceId });
|
|
53
|
+
}
|
|
54
|
+
/** Build a RCAN URI from components. */
|
|
55
|
+
static build(opts) {
|
|
56
|
+
const registry = opts.registry ?? "registry.rcan.dev";
|
|
57
|
+
const { manufacturer, model, version, deviceId } = opts;
|
|
58
|
+
for (const [name, value] of [
|
|
59
|
+
["manufacturer", manufacturer],
|
|
60
|
+
["model", model],
|
|
61
|
+
["version", version],
|
|
62
|
+
["deviceId", deviceId]
|
|
63
|
+
]) {
|
|
64
|
+
if (!value || value.trim() === "") {
|
|
65
|
+
throw new RobotURIError(`'${name}' must not be empty`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return new _RobotURI({ registry, manufacturer, model, version, deviceId });
|
|
69
|
+
}
|
|
70
|
+
/** Full URI string: rcan://registry/manufacturer/model/version/device-id */
|
|
71
|
+
toString() {
|
|
72
|
+
return `rcan://${this.registry}/${this.manufacturer}/${this.model}/${this.version}/${this.deviceId}`;
|
|
73
|
+
}
|
|
74
|
+
/** Short namespace: manufacturer/model */
|
|
75
|
+
get namespace() {
|
|
76
|
+
return `${this.manufacturer}/${this.model}`;
|
|
77
|
+
}
|
|
78
|
+
/** HTTPS registry URL for this robot */
|
|
79
|
+
get registryUrl() {
|
|
80
|
+
return `https://${this.registry}/registry/${this.manufacturer}/${this.model}/${this.version}/${this.deviceId}`;
|
|
81
|
+
}
|
|
82
|
+
/** Check if two URIs refer to the same robot */
|
|
83
|
+
equals(other) {
|
|
84
|
+
return this.toString() === other.toString();
|
|
85
|
+
}
|
|
86
|
+
toJSON() {
|
|
87
|
+
return {
|
|
88
|
+
uri: this.toString(),
|
|
89
|
+
registry: this.registry,
|
|
90
|
+
manufacturer: this.manufacturer,
|
|
91
|
+
model: this.model,
|
|
92
|
+
version: this.version,
|
|
93
|
+
deviceId: this.deviceId
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// src/message.ts
|
|
99
|
+
var RCANMessageError = class extends Error {
|
|
100
|
+
constructor(message) {
|
|
101
|
+
super(message);
|
|
102
|
+
this.name = "RCANMessageError";
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
var RCANMessage = class _RCANMessage {
|
|
106
|
+
rcan;
|
|
107
|
+
cmd;
|
|
108
|
+
target;
|
|
109
|
+
params;
|
|
110
|
+
confidence;
|
|
111
|
+
modelIdentity;
|
|
112
|
+
signature;
|
|
113
|
+
timestamp;
|
|
114
|
+
constructor(data) {
|
|
115
|
+
if (!data.cmd || data.cmd.trim() === "") {
|
|
116
|
+
throw new RCANMessageError("'cmd' is required");
|
|
117
|
+
}
|
|
118
|
+
if (!data.target) {
|
|
119
|
+
throw new RCANMessageError("'target' is required");
|
|
120
|
+
}
|
|
121
|
+
this.rcan = data.rcan ?? "1.2";
|
|
122
|
+
this.cmd = data.cmd;
|
|
123
|
+
this.target = data.target instanceof RobotURI ? data.target.toString() : String(data.target);
|
|
124
|
+
this.params = data.params ?? {};
|
|
125
|
+
this.confidence = data.confidence;
|
|
126
|
+
this.modelIdentity = data.modelIdentity ?? data.model_identity;
|
|
127
|
+
this.signature = data.signature;
|
|
128
|
+
this.timestamp = data.timestamp ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
129
|
+
if (this.confidence !== void 0) {
|
|
130
|
+
if (this.confidence < 0 || this.confidence > 1) {
|
|
131
|
+
throw new RCANMessageError(
|
|
132
|
+
`confidence must be in [0.0, 1.0] \u2014 got ${this.confidence}`
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/** Whether this message has a signature block */
|
|
138
|
+
get isSigned() {
|
|
139
|
+
return this.signature !== void 0 && this.signature.sig !== "";
|
|
140
|
+
}
|
|
141
|
+
/** Whether this message was generated by an AI model (has confidence score) */
|
|
142
|
+
get isAiDriven() {
|
|
143
|
+
return this.confidence !== void 0;
|
|
144
|
+
}
|
|
145
|
+
/** Serialize to a plain object */
|
|
146
|
+
toJSON() {
|
|
147
|
+
const obj = {
|
|
148
|
+
rcan: this.rcan,
|
|
149
|
+
cmd: this.cmd,
|
|
150
|
+
target: this.target,
|
|
151
|
+
timestamp: this.timestamp
|
|
152
|
+
};
|
|
153
|
+
if (Object.keys(this.params).length > 0) obj.params = this.params;
|
|
154
|
+
if (this.confidence !== void 0) obj.confidence = this.confidence;
|
|
155
|
+
if (this.modelIdentity) obj.model_identity = this.modelIdentity;
|
|
156
|
+
if (this.signature) obj.signature = this.signature;
|
|
157
|
+
return obj;
|
|
158
|
+
}
|
|
159
|
+
/** Serialize to JSON string */
|
|
160
|
+
toJSONString(indent) {
|
|
161
|
+
return JSON.stringify(this.toJSON(), null, indent);
|
|
162
|
+
}
|
|
163
|
+
/** Parse from a plain object or JSON string */
|
|
164
|
+
static fromJSON(data) {
|
|
165
|
+
let obj;
|
|
166
|
+
if (typeof data === "string") {
|
|
167
|
+
try {
|
|
168
|
+
obj = JSON.parse(data);
|
|
169
|
+
} catch {
|
|
170
|
+
throw new RCANMessageError("Invalid JSON string");
|
|
171
|
+
}
|
|
172
|
+
} else {
|
|
173
|
+
obj = data;
|
|
174
|
+
}
|
|
175
|
+
if (!obj.cmd) throw new RCANMessageError("Missing required field: 'cmd'");
|
|
176
|
+
if (!obj.target) throw new RCANMessageError("Missing required field: 'target'");
|
|
177
|
+
if (!obj.rcan) throw new RCANMessageError("Missing required field: 'rcan'");
|
|
178
|
+
return new _RCANMessage({
|
|
179
|
+
rcan: obj.rcan,
|
|
180
|
+
cmd: obj.cmd,
|
|
181
|
+
target: obj.target,
|
|
182
|
+
params: obj.params ?? {},
|
|
183
|
+
confidence: obj.confidence,
|
|
184
|
+
modelIdentity: obj.model_identity ?? obj.modelIdentity,
|
|
185
|
+
signature: obj.signature,
|
|
186
|
+
timestamp: obj.timestamp
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
// src/crypto.ts
|
|
192
|
+
function generateUUID() {
|
|
193
|
+
if (typeof globalThis.crypto !== "undefined" && typeof globalThis.crypto.randomUUID === "function") {
|
|
194
|
+
return globalThis.crypto.randomUUID();
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
const { randomUUID } = __require("crypto");
|
|
198
|
+
return randomUUID();
|
|
199
|
+
} catch {
|
|
200
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
201
|
+
const r = Math.random() * 16 | 0;
|
|
202
|
+
const v = c === "x" ? r : r & 3 | 8;
|
|
203
|
+
return v.toString(16);
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
function hmacSha256Sync(secret, data) {
|
|
208
|
+
if (typeof process !== "undefined" && process.versions?.node) {
|
|
209
|
+
try {
|
|
210
|
+
const { createHmac } = __require("crypto");
|
|
211
|
+
return createHmac("sha256", secret).update(data).digest("hex");
|
|
212
|
+
} catch {
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return pureHmacSha256(secret, data);
|
|
216
|
+
}
|
|
217
|
+
function sha256(msg) {
|
|
218
|
+
const K = [
|
|
219
|
+
1116352408,
|
|
220
|
+
1899447441,
|
|
221
|
+
3049323471,
|
|
222
|
+
3921009573,
|
|
223
|
+
961987163,
|
|
224
|
+
1508970993,
|
|
225
|
+
2453635748,
|
|
226
|
+
2870763221,
|
|
227
|
+
3624381080,
|
|
228
|
+
310598401,
|
|
229
|
+
607225278,
|
|
230
|
+
1426881987,
|
|
231
|
+
1925078388,
|
|
232
|
+
2162078206,
|
|
233
|
+
2614888103,
|
|
234
|
+
3248222580,
|
|
235
|
+
3835390401,
|
|
236
|
+
4022224774,
|
|
237
|
+
264347078,
|
|
238
|
+
604807628,
|
|
239
|
+
770255983,
|
|
240
|
+
1249150122,
|
|
241
|
+
1555081692,
|
|
242
|
+
1996064986,
|
|
243
|
+
2554220882,
|
|
244
|
+
2821834349,
|
|
245
|
+
2952996808,
|
|
246
|
+
3210313671,
|
|
247
|
+
3336571891,
|
|
248
|
+
3584528711,
|
|
249
|
+
113926993,
|
|
250
|
+
338241895,
|
|
251
|
+
666307205,
|
|
252
|
+
773529912,
|
|
253
|
+
1294757372,
|
|
254
|
+
1396182291,
|
|
255
|
+
1695183700,
|
|
256
|
+
1986661051,
|
|
257
|
+
2177026350,
|
|
258
|
+
2456956037,
|
|
259
|
+
2730485921,
|
|
260
|
+
2820302411,
|
|
261
|
+
3259730800,
|
|
262
|
+
3345764771,
|
|
263
|
+
3516065817,
|
|
264
|
+
3600352804,
|
|
265
|
+
4094571909,
|
|
266
|
+
275423344,
|
|
267
|
+
430227734,
|
|
268
|
+
506948616,
|
|
269
|
+
659060556,
|
|
270
|
+
883997877,
|
|
271
|
+
958139571,
|
|
272
|
+
1322822218,
|
|
273
|
+
1537002063,
|
|
274
|
+
1747873779,
|
|
275
|
+
1955562222,
|
|
276
|
+
2024104815,
|
|
277
|
+
2227730452,
|
|
278
|
+
2361852424,
|
|
279
|
+
2428436474,
|
|
280
|
+
2756734187,
|
|
281
|
+
3204031479,
|
|
282
|
+
3329325298
|
|
283
|
+
];
|
|
284
|
+
let h0 = 1779033703, h1 = 3144134277, h2 = 1013904242, h3 = 2773480762;
|
|
285
|
+
let h4 = 1359893119, h5 = 2600822924, h6 = 528734635, h7 = 1541459225;
|
|
286
|
+
const msgLen = msg.length;
|
|
287
|
+
const bitLen = msgLen * 8;
|
|
288
|
+
const padded = [...msg];
|
|
289
|
+
padded.push(128);
|
|
290
|
+
while (padded.length % 64 !== 56) padded.push(0);
|
|
291
|
+
for (let i = 7; i >= 0; i--) padded.push(bitLen / Math.pow(2, i * 8) & 255);
|
|
292
|
+
for (let i = 0; i < padded.length; i += 64) {
|
|
293
|
+
const w = [];
|
|
294
|
+
for (let j = 0; j < 16; j++) {
|
|
295
|
+
w[j] = padded[i + j * 4] << 24 | padded[i + j * 4 + 1] << 16 | padded[i + j * 4 + 2] << 8 | padded[i + j * 4 + 3];
|
|
296
|
+
}
|
|
297
|
+
for (let j = 16; j < 64; j++) {
|
|
298
|
+
const s0 = ror(w[j - 15], 7) ^ ror(w[j - 15], 18) ^ w[j - 15] >>> 3;
|
|
299
|
+
const s1 = ror(w[j - 2], 17) ^ ror(w[j - 2], 19) ^ w[j - 2] >>> 10;
|
|
300
|
+
w[j] = w[j - 16] + s0 + w[j - 7] + s1 >>> 0;
|
|
301
|
+
}
|
|
302
|
+
let [a, b, c, d, e, f, g, h] = [h0, h1, h2, h3, h4, h5, h6, h7];
|
|
303
|
+
for (let j = 0; j < 64; j++) {
|
|
304
|
+
const S1 = ror(e, 6) ^ ror(e, 11) ^ ror(e, 25);
|
|
305
|
+
const ch = e & f ^ ~e & g;
|
|
306
|
+
const temp1 = h + S1 + ch + K[j] + w[j] >>> 0;
|
|
307
|
+
const S0 = ror(a, 2) ^ ror(a, 13) ^ ror(a, 22);
|
|
308
|
+
const maj = a & b ^ a & c ^ b & c;
|
|
309
|
+
const temp2 = S0 + maj >>> 0;
|
|
310
|
+
[h, g, f, e, d, c, b, a] = [g, f, e, d + temp1 >>> 0, c, b, a, temp1 + temp2 >>> 0];
|
|
311
|
+
}
|
|
312
|
+
h0 = h0 + a >>> 0;
|
|
313
|
+
h1 = h1 + b >>> 0;
|
|
314
|
+
h2 = h2 + c >>> 0;
|
|
315
|
+
h3 = h3 + d >>> 0;
|
|
316
|
+
h4 = h4 + e >>> 0;
|
|
317
|
+
h5 = h5 + f >>> 0;
|
|
318
|
+
h6 = h6 + g >>> 0;
|
|
319
|
+
h7 = h7 + h >>> 0;
|
|
320
|
+
}
|
|
321
|
+
const out = new Uint8Array(32);
|
|
322
|
+
const view = new DataView(out.buffer);
|
|
323
|
+
[h0, h1, h2, h3, h4, h5, h6, h7].forEach((v, i) => view.setUint32(i * 4, v));
|
|
324
|
+
return out;
|
|
325
|
+
}
|
|
326
|
+
function ror(x, n) {
|
|
327
|
+
return x >>> n | x << 32 - n;
|
|
328
|
+
}
|
|
329
|
+
function toBytes(s) {
|
|
330
|
+
if (typeof TextEncoder !== "undefined") return new TextEncoder().encode(s);
|
|
331
|
+
const out = new Uint8Array(s.length);
|
|
332
|
+
for (let i = 0; i < s.length; i++) out[i] = s.charCodeAt(i) & 255;
|
|
333
|
+
return out;
|
|
334
|
+
}
|
|
335
|
+
function toHex(bytes) {
|
|
336
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
337
|
+
}
|
|
338
|
+
function pureHmacSha256(key, data) {
|
|
339
|
+
const BLOCK = 64;
|
|
340
|
+
let keyBytes = toBytes(key);
|
|
341
|
+
if (keyBytes.length > BLOCK) keyBytes = sha256(keyBytes);
|
|
342
|
+
const ipad = new Uint8Array(BLOCK), opad = new Uint8Array(BLOCK);
|
|
343
|
+
for (let i = 0; i < BLOCK; i++) {
|
|
344
|
+
ipad[i] = (keyBytes[i] ?? 0) ^ 54;
|
|
345
|
+
opad[i] = (keyBytes[i] ?? 0) ^ 92;
|
|
346
|
+
}
|
|
347
|
+
const dataBytes = toBytes(data);
|
|
348
|
+
const inner = new Uint8Array(BLOCK + dataBytes.length);
|
|
349
|
+
inner.set(ipad);
|
|
350
|
+
inner.set(dataBytes, BLOCK);
|
|
351
|
+
const innerHash = sha256(inner);
|
|
352
|
+
const outer = new Uint8Array(BLOCK + 32);
|
|
353
|
+
outer.set(opad);
|
|
354
|
+
outer.set(innerHash, BLOCK);
|
|
355
|
+
return toHex(sha256(outer));
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// src/gates.ts
|
|
359
|
+
var GateError = class extends Error {
|
|
360
|
+
constructor(message) {
|
|
361
|
+
super(message);
|
|
362
|
+
this.name = "GateError";
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
var ConfidenceGate = class {
|
|
366
|
+
threshold;
|
|
367
|
+
constructor(threshold = 0.8) {
|
|
368
|
+
if (threshold < 0 || threshold > 1) {
|
|
369
|
+
throw new GateError(`threshold must be in [0.0, 1.0] \u2014 got ${threshold}`);
|
|
370
|
+
}
|
|
371
|
+
this.threshold = threshold;
|
|
372
|
+
}
|
|
373
|
+
/** Returns true if the confidence score meets the threshold. */
|
|
374
|
+
allows(confidence) {
|
|
375
|
+
return confidence >= this.threshold;
|
|
376
|
+
}
|
|
377
|
+
/** Returns the margin (positive = allowed, negative = blocked). */
|
|
378
|
+
margin(confidence) {
|
|
379
|
+
return confidence - this.threshold;
|
|
380
|
+
}
|
|
381
|
+
/** Throw if confidence is below threshold. */
|
|
382
|
+
assert(confidence, action) {
|
|
383
|
+
if (!this.allows(confidence)) {
|
|
384
|
+
const label = action ? ` for action '${action}'` : "";
|
|
385
|
+
throw new GateError(
|
|
386
|
+
`Confidence ${confidence}${label} is below threshold ${this.threshold}`
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
var HiTLGate = class {
|
|
392
|
+
_pending = /* @__PURE__ */ new Map();
|
|
393
|
+
/**
|
|
394
|
+
* Request human approval for an action.
|
|
395
|
+
* Returns an approval token to poll or pass to approve/deny.
|
|
396
|
+
*/
|
|
397
|
+
request(action, context = {}) {
|
|
398
|
+
const token = generateUUID();
|
|
399
|
+
this._pending.set(token, {
|
|
400
|
+
token,
|
|
401
|
+
action,
|
|
402
|
+
context,
|
|
403
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
404
|
+
status: "pending"
|
|
405
|
+
});
|
|
406
|
+
return token;
|
|
407
|
+
}
|
|
408
|
+
/** Approve a pending request. */
|
|
409
|
+
approve(token) {
|
|
410
|
+
const approval = this._pending.get(token);
|
|
411
|
+
if (!approval) throw new GateError(`Unknown token: ${token}`);
|
|
412
|
+
approval.status = "approved";
|
|
413
|
+
}
|
|
414
|
+
/** Deny a pending request with an optional reason. */
|
|
415
|
+
deny(token, reason) {
|
|
416
|
+
const approval = this._pending.get(token);
|
|
417
|
+
if (!approval) throw new GateError(`Unknown token: ${token}`);
|
|
418
|
+
approval.status = "denied";
|
|
419
|
+
if (reason) approval.reason = reason;
|
|
420
|
+
}
|
|
421
|
+
/** Check the status of a pending approval. */
|
|
422
|
+
check(token) {
|
|
423
|
+
const approval = this._pending.get(token);
|
|
424
|
+
if (!approval) throw new GateError(`Unknown token: ${token}`);
|
|
425
|
+
return approval.status;
|
|
426
|
+
}
|
|
427
|
+
/** Get all pending approvals. */
|
|
428
|
+
get pendingApprovals() {
|
|
429
|
+
return Array.from(this._pending.values()).filter((a) => a.status === "pending");
|
|
430
|
+
}
|
|
431
|
+
/** Get the full approval record. */
|
|
432
|
+
getApproval(token) {
|
|
433
|
+
return this._pending.get(token);
|
|
434
|
+
}
|
|
435
|
+
/** Clear resolved (approved/denied) approvals. */
|
|
436
|
+
clearResolved() {
|
|
437
|
+
for (const [token, approval] of this._pending.entries()) {
|
|
438
|
+
if (approval.status !== "pending") {
|
|
439
|
+
this._pending.delete(token);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
// src/audit.ts
|
|
446
|
+
var AuditError = class extends Error {
|
|
447
|
+
constructor(message) {
|
|
448
|
+
super(message);
|
|
449
|
+
this.name = "AuditError";
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
function computeContentHash(recordId, action, robotUri, timestamp, params) {
|
|
453
|
+
const payload = JSON.stringify(
|
|
454
|
+
{ recordId, action, robotUri, timestamp, params },
|
|
455
|
+
Object.keys({ recordId, action, robotUri, timestamp, params }).sort()
|
|
456
|
+
);
|
|
457
|
+
return hmacSha256Sync("rcan-content-hash", payload);
|
|
458
|
+
}
|
|
459
|
+
function computeHmac(secret, data) {
|
|
460
|
+
const { hmac: _ignored, ...rest } = data;
|
|
461
|
+
const payload = JSON.stringify(rest, Object.keys(rest).sort());
|
|
462
|
+
return hmacSha256Sync(secret, payload);
|
|
463
|
+
}
|
|
464
|
+
var CommitmentRecord = class _CommitmentRecord {
|
|
465
|
+
recordId;
|
|
466
|
+
action;
|
|
467
|
+
robotUri;
|
|
468
|
+
confidence;
|
|
469
|
+
modelIdentity;
|
|
470
|
+
params;
|
|
471
|
+
safetyApproved;
|
|
472
|
+
timestamp;
|
|
473
|
+
contentHash;
|
|
474
|
+
previousHash;
|
|
475
|
+
hmac;
|
|
476
|
+
constructor(data) {
|
|
477
|
+
this.recordId = data.recordId;
|
|
478
|
+
this.action = data.action;
|
|
479
|
+
this.robotUri = data.robotUri;
|
|
480
|
+
this.confidence = data.confidence;
|
|
481
|
+
this.modelIdentity = data.modelIdentity;
|
|
482
|
+
this.params = data.params;
|
|
483
|
+
this.safetyApproved = data.safetyApproved;
|
|
484
|
+
this.timestamp = data.timestamp;
|
|
485
|
+
this.contentHash = data.contentHash;
|
|
486
|
+
this.previousHash = data.previousHash;
|
|
487
|
+
this.hmac = data.hmac;
|
|
488
|
+
}
|
|
489
|
+
static create(data, secret, previousHash = null) {
|
|
490
|
+
const recordId = generateUUID();
|
|
491
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
492
|
+
const params = data.params ?? {};
|
|
493
|
+
const robotUri = data.robotUri ?? "";
|
|
494
|
+
const contentHash = computeContentHash(recordId, data.action, robotUri, timestamp, params);
|
|
495
|
+
const draft = {
|
|
496
|
+
recordId,
|
|
497
|
+
action: data.action,
|
|
498
|
+
robotUri,
|
|
499
|
+
confidence: data.confidence,
|
|
500
|
+
modelIdentity: data.modelIdentity,
|
|
501
|
+
params,
|
|
502
|
+
safetyApproved: data.safetyApproved ?? true,
|
|
503
|
+
timestamp,
|
|
504
|
+
contentHash,
|
|
505
|
+
previousHash,
|
|
506
|
+
hmac: ""
|
|
507
|
+
};
|
|
508
|
+
draft.hmac = computeHmac(secret, draft);
|
|
509
|
+
return new _CommitmentRecord(draft);
|
|
510
|
+
}
|
|
511
|
+
verify(secret) {
|
|
512
|
+
const expected = computeHmac(secret, this.toJSON());
|
|
513
|
+
return expected === this.hmac;
|
|
514
|
+
}
|
|
515
|
+
toJSON() {
|
|
516
|
+
return {
|
|
517
|
+
recordId: this.recordId,
|
|
518
|
+
action: this.action,
|
|
519
|
+
robotUri: this.robotUri,
|
|
520
|
+
confidence: this.confidence,
|
|
521
|
+
modelIdentity: this.modelIdentity,
|
|
522
|
+
params: this.params,
|
|
523
|
+
safetyApproved: this.safetyApproved,
|
|
524
|
+
timestamp: this.timestamp,
|
|
525
|
+
contentHash: this.contentHash,
|
|
526
|
+
previousHash: this.previousHash,
|
|
527
|
+
hmac: this.hmac
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
static fromJSON(obj) {
|
|
531
|
+
return new _CommitmentRecord(obj);
|
|
532
|
+
}
|
|
533
|
+
};
|
|
534
|
+
var AuditChain = class _AuditChain {
|
|
535
|
+
_records = [];
|
|
536
|
+
_secret;
|
|
537
|
+
constructor(secret) {
|
|
538
|
+
this._secret = secret;
|
|
539
|
+
}
|
|
540
|
+
get records() {
|
|
541
|
+
return this._records;
|
|
542
|
+
}
|
|
543
|
+
append(data) {
|
|
544
|
+
const prev = this._records[this._records.length - 1];
|
|
545
|
+
const prevHash = prev?.contentHash ?? null;
|
|
546
|
+
const record = CommitmentRecord.create(data, this._secret, prevHash);
|
|
547
|
+
this._records.push(record);
|
|
548
|
+
return record;
|
|
549
|
+
}
|
|
550
|
+
verifyAll() {
|
|
551
|
+
const errors = [];
|
|
552
|
+
let prevHash = null;
|
|
553
|
+
for (const record of this._records) {
|
|
554
|
+
if (!record.verify(this._secret)) {
|
|
555
|
+
errors.push(`HMAC invalid for record ${record.recordId.slice(0, 8)}`);
|
|
556
|
+
}
|
|
557
|
+
if (prevHash !== null && record.previousHash !== prevHash) {
|
|
558
|
+
errors.push(
|
|
559
|
+
`Chain broken at ${record.recordId.slice(0, 8)}: expected prev=${prevHash.slice(0, 12)}`
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
prevHash = record.contentHash;
|
|
563
|
+
}
|
|
564
|
+
return { valid: errors.length === 0, count: this._records.length, errors };
|
|
565
|
+
}
|
|
566
|
+
toJSONL() {
|
|
567
|
+
return this._records.map((r) => JSON.stringify(r.toJSON())).join("\n") + "\n";
|
|
568
|
+
}
|
|
569
|
+
static fromJSONL(text, secret) {
|
|
570
|
+
const chain = new _AuditChain(secret);
|
|
571
|
+
const lines = text.trim().split("\n").filter((l) => l.trim() !== "");
|
|
572
|
+
for (const line of lines) {
|
|
573
|
+
const obj = JSON.parse(line);
|
|
574
|
+
chain._records.push(CommitmentRecord.fromJSON(obj));
|
|
575
|
+
}
|
|
576
|
+
return chain;
|
|
577
|
+
}
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
// src/validate.ts
|
|
581
|
+
function makeResult() {
|
|
582
|
+
return { ok: true, issues: [], warnings: [], info: [] };
|
|
583
|
+
}
|
|
584
|
+
function fail(result, msg) {
|
|
585
|
+
result.ok = false;
|
|
586
|
+
result.issues.push(msg);
|
|
587
|
+
}
|
|
588
|
+
function warn(result, msg) {
|
|
589
|
+
result.warnings.push(msg);
|
|
590
|
+
}
|
|
591
|
+
function note(result, msg) {
|
|
592
|
+
result.info.push(msg);
|
|
593
|
+
}
|
|
594
|
+
function validateURI(uri) {
|
|
595
|
+
const result = makeResult();
|
|
596
|
+
try {
|
|
597
|
+
const parsed = RobotURI.parse(uri);
|
|
598
|
+
note(result, `\u2705 Valid RCAN URI`);
|
|
599
|
+
note(result, ` Registry: ${parsed.registry}`);
|
|
600
|
+
note(result, ` Manufacturer: ${parsed.manufacturer}`);
|
|
601
|
+
note(result, ` Model: ${parsed.model}`);
|
|
602
|
+
note(result, ` Version: ${parsed.version}`);
|
|
603
|
+
note(result, ` Device ID: ${parsed.deviceId}`);
|
|
604
|
+
} catch (e) {
|
|
605
|
+
fail(result, `Invalid RCAN URI: ${e instanceof Error ? e.message : e}`);
|
|
606
|
+
}
|
|
607
|
+
return result;
|
|
608
|
+
}
|
|
609
|
+
function validateMessage(data) {
|
|
610
|
+
const result = makeResult();
|
|
611
|
+
let obj;
|
|
612
|
+
if (typeof data === "string") {
|
|
613
|
+
try {
|
|
614
|
+
obj = JSON.parse(data);
|
|
615
|
+
} catch {
|
|
616
|
+
fail(result, "Invalid JSON string");
|
|
617
|
+
return result;
|
|
618
|
+
}
|
|
619
|
+
} else if (typeof data === "object" && data !== null) {
|
|
620
|
+
obj = data;
|
|
621
|
+
} else {
|
|
622
|
+
fail(result, "Expected object or JSON string");
|
|
623
|
+
return result;
|
|
624
|
+
}
|
|
625
|
+
for (const field of ["rcan", "cmd", "target"]) {
|
|
626
|
+
if (!(field in obj) || !obj[field]) {
|
|
627
|
+
fail(result, `Missing required field: '${field}'`);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
if (!result.ok) return result;
|
|
631
|
+
try {
|
|
632
|
+
const msg = RCANMessage.fromJSON(obj);
|
|
633
|
+
note(result, `\u2705 RCAN message valid (v${msg.rcan})`);
|
|
634
|
+
note(result, ` cmd: ${msg.cmd}`);
|
|
635
|
+
note(result, ` target: ${msg.target}`);
|
|
636
|
+
if (msg.confidence !== void 0) {
|
|
637
|
+
note(result, ` confidence: ${msg.confidence}`);
|
|
638
|
+
} else {
|
|
639
|
+
warn(result, "No confidence score \u2014 add for RCAN \xA716 AI accountability");
|
|
640
|
+
}
|
|
641
|
+
if (msg.isSigned) {
|
|
642
|
+
note(result, ` signature: alg=${msg.signature?.alg}, kid=${msg.signature?.kid}`);
|
|
643
|
+
} else {
|
|
644
|
+
warn(result, "Message is unsigned (recommended for production)");
|
|
645
|
+
}
|
|
646
|
+
} catch (e) {
|
|
647
|
+
fail(result, `Message validation failed: ${e instanceof Error ? e.message : e}`);
|
|
648
|
+
}
|
|
649
|
+
return result;
|
|
650
|
+
}
|
|
651
|
+
function validateConfig(config) {
|
|
652
|
+
const result = makeResult();
|
|
653
|
+
const meta = config.metadata ?? {};
|
|
654
|
+
const agent = config.agent ?? {};
|
|
655
|
+
const rcanProto = config.rcan_protocol ?? {};
|
|
656
|
+
if (!meta.manufacturer) fail(result, "L1: metadata.manufacturer is required (\xA72)");
|
|
657
|
+
if (!meta.model) fail(result, "L1: metadata.model is required (\xA72)");
|
|
658
|
+
if (!config.rcan_version) warn(result, "L1: rcan_version not declared (recommended)");
|
|
659
|
+
if (!rcanProto.jwt_auth?.enabled) {
|
|
660
|
+
warn(result, "L2: jwt_auth not enabled (required for L2 conformance, \xA78)");
|
|
661
|
+
}
|
|
662
|
+
if (!agent.confidence_gates || agent.confidence_gates.length === 0) {
|
|
663
|
+
warn(result, "L2: confidence_gates not configured (\xA716)");
|
|
664
|
+
}
|
|
665
|
+
if (!agent.hitl_gates || agent.hitl_gates.length === 0) {
|
|
666
|
+
warn(result, "L3: hitl_gates not configured (\xA716)");
|
|
667
|
+
}
|
|
668
|
+
if (!agent.commitment_chain?.enabled) {
|
|
669
|
+
warn(result, "L3: commitment_chain not enabled (\xA716)");
|
|
670
|
+
}
|
|
671
|
+
if (meta.rrn) {
|
|
672
|
+
note(result, `\u2705 RRN registered: ${meta.rrn}`);
|
|
673
|
+
} else {
|
|
674
|
+
warn(result, "Robot not registered \u2014 visit rcan.dev/registry/register");
|
|
675
|
+
}
|
|
676
|
+
if (result.ok && result.issues.length === 0) {
|
|
677
|
+
const l1ok = !result.warnings.some((w) => w.startsWith("L1"));
|
|
678
|
+
const l2ok = l1ok && !result.warnings.some((w) => w.startsWith("L2"));
|
|
679
|
+
const l3ok = l2ok && !result.warnings.some((w) => w.startsWith("L3"));
|
|
680
|
+
const level = l3ok ? "L3" : l2ok ? "L2" : l1ok ? "L1" : "FAIL";
|
|
681
|
+
note(result, `\u2705 Config valid \u2014 conformance level: ${level}`);
|
|
682
|
+
}
|
|
683
|
+
return result;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// src/errors.ts
|
|
687
|
+
var RCANError = class extends Error {
|
|
688
|
+
constructor(message) {
|
|
689
|
+
super(message);
|
|
690
|
+
this.name = "RCANError";
|
|
691
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
692
|
+
}
|
|
693
|
+
};
|
|
694
|
+
var RCANAddressError = class extends RCANError {
|
|
695
|
+
constructor(message) {
|
|
696
|
+
super(message);
|
|
697
|
+
this.name = "RCANAddressError";
|
|
698
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
699
|
+
}
|
|
700
|
+
};
|
|
701
|
+
var RCANValidationError = class extends RCANError {
|
|
702
|
+
constructor(message) {
|
|
703
|
+
super(message);
|
|
704
|
+
this.name = "RCANValidationError";
|
|
705
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
706
|
+
}
|
|
707
|
+
};
|
|
708
|
+
var RCANGateError = class extends RCANError {
|
|
709
|
+
constructor(message, gateType, value, threshold) {
|
|
710
|
+
super(message);
|
|
711
|
+
this.gateType = gateType;
|
|
712
|
+
this.value = value;
|
|
713
|
+
this.threshold = threshold;
|
|
714
|
+
this.name = "RCANGateError";
|
|
715
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
716
|
+
}
|
|
717
|
+
};
|
|
718
|
+
var RCANSignatureError = class extends RCANError {
|
|
719
|
+
constructor(message) {
|
|
720
|
+
super(message);
|
|
721
|
+
this.name = "RCANSignatureError";
|
|
722
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
723
|
+
}
|
|
724
|
+
};
|
|
725
|
+
var RCANRegistryError = class extends RCANError {
|
|
726
|
+
constructor(message) {
|
|
727
|
+
super(message);
|
|
728
|
+
this.name = "RCANRegistryError";
|
|
729
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
730
|
+
}
|
|
731
|
+
};
|
|
732
|
+
|
|
733
|
+
// src/registry.ts
|
|
734
|
+
var DEFAULT_BASE_URL = "https://rcan-spec.pages.dev";
|
|
735
|
+
var RegistryClient = class {
|
|
736
|
+
baseUrl;
|
|
737
|
+
apiKey;
|
|
738
|
+
timeout;
|
|
739
|
+
constructor(opts) {
|
|
740
|
+
this.baseUrl = (opts?.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
741
|
+
this.apiKey = opts?.apiKey;
|
|
742
|
+
this.timeout = opts?.timeout ?? 1e4;
|
|
743
|
+
}
|
|
744
|
+
// ── Private helpers ─────────────────────────────────────────────────────────
|
|
745
|
+
async _fetch(path, init = {}) {
|
|
746
|
+
const url = `${this.baseUrl}${path}`;
|
|
747
|
+
const controller = new AbortController();
|
|
748
|
+
const timer = setTimeout(() => controller.abort(), this.timeout);
|
|
749
|
+
try {
|
|
750
|
+
const response = await fetch(url, {
|
|
751
|
+
...init,
|
|
752
|
+
signal: controller.signal,
|
|
753
|
+
headers: {
|
|
754
|
+
"Content-Type": "application/json",
|
|
755
|
+
...init.headers ?? {}
|
|
756
|
+
}
|
|
757
|
+
});
|
|
758
|
+
return response;
|
|
759
|
+
} finally {
|
|
760
|
+
clearTimeout(timer);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
_authHeaders() {
|
|
764
|
+
if (!this.apiKey) {
|
|
765
|
+
throw new RCANRegistryError(
|
|
766
|
+
"API key required for write operations. Pass apiKey to RegistryClient."
|
|
767
|
+
);
|
|
768
|
+
}
|
|
769
|
+
return { Authorization: `Bearer ${this.apiKey}` };
|
|
770
|
+
}
|
|
771
|
+
async _checkResponse(resp) {
|
|
772
|
+
if (!resp.ok) {
|
|
773
|
+
let msg = `Registry API error: ${resp.status}`;
|
|
774
|
+
try {
|
|
775
|
+
const body = await resp.json();
|
|
776
|
+
if (body?.error) msg = body.error;
|
|
777
|
+
} catch {
|
|
778
|
+
}
|
|
779
|
+
throw new RCANRegistryError(msg);
|
|
780
|
+
}
|
|
781
|
+
return await resp.json();
|
|
782
|
+
}
|
|
783
|
+
// ── Public API ───────────────────────────────────────────────────────────────
|
|
784
|
+
/** Register a new robot. Returns RRN + API key. */
|
|
785
|
+
async register(robot) {
|
|
786
|
+
const resp = await this._fetch("/api/v1/robots", {
|
|
787
|
+
method: "POST",
|
|
788
|
+
body: JSON.stringify(robot)
|
|
789
|
+
});
|
|
790
|
+
return this._checkResponse(resp);
|
|
791
|
+
}
|
|
792
|
+
/** Look up a robot by RRN. */
|
|
793
|
+
async get(rrn) {
|
|
794
|
+
const resp = await this._fetch(`/api/v1/robots/${encodeURIComponent(rrn)}`);
|
|
795
|
+
return this._checkResponse(resp);
|
|
796
|
+
}
|
|
797
|
+
/** List robots with optional pagination and tier filter. */
|
|
798
|
+
async list(opts) {
|
|
799
|
+
const params = new URLSearchParams();
|
|
800
|
+
if (opts?.limit !== void 0) params.set("limit", String(opts.limit));
|
|
801
|
+
if (opts?.offset !== void 0) params.set("offset", String(opts.offset));
|
|
802
|
+
if (opts?.tier) params.set("tier", opts.tier);
|
|
803
|
+
const qs = params.toString() ? `?${params}` : "";
|
|
804
|
+
const resp = await this._fetch(`/api/v1/robots${qs}`);
|
|
805
|
+
return this._checkResponse(resp);
|
|
806
|
+
}
|
|
807
|
+
/** Update robot fields. Requires API key. */
|
|
808
|
+
async patch(rrn, updates) {
|
|
809
|
+
const resp = await this._fetch(
|
|
810
|
+
`/api/v1/robots/${encodeURIComponent(rrn)}`,
|
|
811
|
+
{
|
|
812
|
+
method: "PATCH",
|
|
813
|
+
headers: this._authHeaders(),
|
|
814
|
+
body: JSON.stringify(updates)
|
|
815
|
+
}
|
|
816
|
+
);
|
|
817
|
+
return this._checkResponse(resp);
|
|
818
|
+
}
|
|
819
|
+
/** Delete (soft-delete) a robot. Requires API key. */
|
|
820
|
+
async delete(rrn) {
|
|
821
|
+
const resp = await this._fetch(
|
|
822
|
+
`/api/v1/robots/${encodeURIComponent(rrn)}`,
|
|
823
|
+
{
|
|
824
|
+
method: "DELETE",
|
|
825
|
+
headers: this._authHeaders()
|
|
826
|
+
}
|
|
827
|
+
);
|
|
828
|
+
if (!resp.ok) {
|
|
829
|
+
await this._checkResponse(resp);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
/** Search robots by query, manufacturer, model, or tier. */
|
|
833
|
+
async search(opts) {
|
|
834
|
+
const params = new URLSearchParams();
|
|
835
|
+
if (opts.q) params.set("q", opts.q);
|
|
836
|
+
if (opts.manufacturer) params.set("manufacturer", opts.manufacturer);
|
|
837
|
+
if (opts.model) params.set("model", opts.model);
|
|
838
|
+
if (opts.tier) params.set("tier", opts.tier);
|
|
839
|
+
const qs = params.toString() ? `?${params}` : "";
|
|
840
|
+
const resp = await this._fetch(`/api/v1/robots/search${qs}`);
|
|
841
|
+
if (!resp.ok) {
|
|
842
|
+
const listResp = await this._fetch(`/api/v1/robots${qs}`);
|
|
843
|
+
const data2 = await this._checkResponse(listResp);
|
|
844
|
+
if ("robots" in data2) return data2.robots;
|
|
845
|
+
if ("results" in data2 && data2.results) return data2.results;
|
|
846
|
+
return [];
|
|
847
|
+
}
|
|
848
|
+
const data = await resp.json();
|
|
849
|
+
if (Array.isArray(data)) return data;
|
|
850
|
+
if ("results" in data && data.results) return data.results;
|
|
851
|
+
if ("robots" in data) return data.robots;
|
|
852
|
+
return [];
|
|
853
|
+
}
|
|
854
|
+
};
|
|
855
|
+
|
|
856
|
+
// src/index.ts
|
|
857
|
+
var VERSION = "0.1.0";
|
|
858
|
+
var RCAN_VERSION = "1.2";
|
|
859
|
+
export {
|
|
860
|
+
AuditChain,
|
|
861
|
+
AuditError,
|
|
862
|
+
CommitmentRecord,
|
|
863
|
+
ConfidenceGate,
|
|
864
|
+
GateError,
|
|
865
|
+
HiTLGate,
|
|
866
|
+
RCANAddressError,
|
|
867
|
+
RCANError,
|
|
868
|
+
RCANGateError,
|
|
869
|
+
RCANMessage,
|
|
870
|
+
RCANMessageError,
|
|
871
|
+
RCANRegistryError,
|
|
872
|
+
RCANSignatureError,
|
|
873
|
+
RCANValidationError,
|
|
874
|
+
RCAN_VERSION,
|
|
875
|
+
RegistryClient,
|
|
876
|
+
RobotURI,
|
|
877
|
+
RobotURIError,
|
|
878
|
+
VERSION,
|
|
879
|
+
validateConfig,
|
|
880
|
+
validateMessage,
|
|
881
|
+
validateURI
|
|
882
|
+
};
|
|
883
|
+
//# sourceMappingURL=index.mjs.map
|