@aarmos/avar-core 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +77 -1
- package/dist/index.js +163 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -194,4 +194,80 @@ declare function signedBodyOf(entry: AvarEntry): Omit<AvarEntry, "signature" | "
|
|
|
194
194
|
|
|
195
195
|
declare function verifyBundle(bundle: AvarBundle): Promise<VerificationReport>;
|
|
196
196
|
|
|
197
|
-
|
|
197
|
+
type DiffOp = {
|
|
198
|
+
op: "add";
|
|
199
|
+
path: string;
|
|
200
|
+
value: unknown;
|
|
201
|
+
} | {
|
|
202
|
+
op: "remove";
|
|
203
|
+
path: string;
|
|
204
|
+
oldValue: unknown;
|
|
205
|
+
} | {
|
|
206
|
+
op: "replace";
|
|
207
|
+
path: string;
|
|
208
|
+
oldValue: unknown;
|
|
209
|
+
value: unknown;
|
|
210
|
+
};
|
|
211
|
+
type CanonicalDiff = {
|
|
212
|
+
equal: boolean;
|
|
213
|
+
ops: DiffOp[];
|
|
214
|
+
};
|
|
215
|
+
/** Structural, canonical diff of two JSON values. */
|
|
216
|
+
declare function diffCanonical(a: unknown, b: unknown): CanonicalDiff;
|
|
217
|
+
type ReceiptEntryChange = {
|
|
218
|
+
id: string;
|
|
219
|
+
indexA: number;
|
|
220
|
+
indexB: number;
|
|
221
|
+
ops: DiffOp[];
|
|
222
|
+
signatureChanged: boolean;
|
|
223
|
+
entryHashChanged: boolean;
|
|
224
|
+
stepsChanged: boolean;
|
|
225
|
+
};
|
|
226
|
+
type ReceiptDiff = {
|
|
227
|
+
equal: boolean;
|
|
228
|
+
kind: "bundle" | "entry";
|
|
229
|
+
specVersion: {
|
|
230
|
+
from: string;
|
|
231
|
+
to: string;
|
|
232
|
+
} | null;
|
|
233
|
+
chainHead: {
|
|
234
|
+
from: {
|
|
235
|
+
entryHash: string;
|
|
236
|
+
index: number;
|
|
237
|
+
};
|
|
238
|
+
to: {
|
|
239
|
+
entryHash: string;
|
|
240
|
+
index: number;
|
|
241
|
+
};
|
|
242
|
+
extended: boolean;
|
|
243
|
+
} | null;
|
|
244
|
+
devicePublicKeys: {
|
|
245
|
+
added: string[];
|
|
246
|
+
removed: string[];
|
|
247
|
+
};
|
|
248
|
+
entries: {
|
|
249
|
+
added: Array<{
|
|
250
|
+
id: string;
|
|
251
|
+
index: number;
|
|
252
|
+
}>;
|
|
253
|
+
removed: Array<{
|
|
254
|
+
id: string;
|
|
255
|
+
index: number;
|
|
256
|
+
}>;
|
|
257
|
+
modified: ReceiptEntryChange[];
|
|
258
|
+
unchanged: number;
|
|
259
|
+
};
|
|
260
|
+
/** Raw canonical diff of manifest + pubkeys (entries handled above). */
|
|
261
|
+
ops: DiffOp[];
|
|
262
|
+
};
|
|
263
|
+
/**
|
|
264
|
+
* Compare two AVAR receipts. Accepts full bundles OR single entries.
|
|
265
|
+
* Entries are matched by `id`, not by index — reordering is not a diff.
|
|
266
|
+
*/
|
|
267
|
+
declare function diffReceipts(a: unknown, b: unknown): ReceiptDiff;
|
|
268
|
+
/** Canonical diff of two policy documents. Schema-agnostic. */
|
|
269
|
+
declare function diffPolicies(a: unknown, b: unknown): CanonicalDiff;
|
|
270
|
+
/** Canonical diff of two tool manifests. Schema-agnostic. */
|
|
271
|
+
declare function diffToolManifests(a: unknown, b: unknown): CanonicalDiff;
|
|
272
|
+
|
|
273
|
+
export { type AvarBundle, type AvarEntry, type BundleManifest, type BundlePubKeys, type CanonicalDiff, type DecisionStep, type DiffOp, GENESIS_PREV_HASH, GENESIS_PREV_STEP_HASH, type PolicyHit, type ReceiptDiff, type ReceiptEntryChange, type TextStep, type ToolStep, type TraceStep, type VerificationIssue, type VerificationReport, canonicalize, computeDeviceFingerprint, computeEntryHash, computeStepHash, diffCanonical, diffPolicies, diffReceipts, diffToolManifests, sha256Hex, signedBodyOf, utf8, verifyBundle, verifySignature };
|
package/dist/index.js
CHANGED
|
@@ -294,6 +294,165 @@ function computeChainHead(entries) {
|
|
|
294
294
|
}
|
|
295
295
|
return { entryHash: "", index: -1 };
|
|
296
296
|
}
|
|
297
|
+
|
|
298
|
+
// src/diff.ts
|
|
299
|
+
function isPlainObject(v) {
|
|
300
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
301
|
+
}
|
|
302
|
+
function escapePointer(seg) {
|
|
303
|
+
return seg.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
304
|
+
}
|
|
305
|
+
function join(base, seg) {
|
|
306
|
+
return `${base}/${typeof seg === "number" ? seg : escapePointer(seg)}`;
|
|
307
|
+
}
|
|
308
|
+
function diffCanonical(a, b) {
|
|
309
|
+
const ops = [];
|
|
310
|
+
walk(a, b, "", ops);
|
|
311
|
+
return { equal: ops.length === 0, ops };
|
|
312
|
+
}
|
|
313
|
+
function walk(a, b, path, ops) {
|
|
314
|
+
if (a === b) return;
|
|
315
|
+
if ((a === null || typeof a !== "object") && (b === null || typeof b !== "object")) {
|
|
316
|
+
if (canonicalize(a) !== canonicalize(b)) {
|
|
317
|
+
ops.push({ op: "replace", path: path || "/", oldValue: a, value: b });
|
|
318
|
+
}
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
322
|
+
const min = Math.min(a.length, b.length);
|
|
323
|
+
for (let i = 0; i < min; i++) walk(a[i], b[i], join(path, i), ops);
|
|
324
|
+
if (b.length > a.length) {
|
|
325
|
+
for (let i = a.length; i < b.length; i++) {
|
|
326
|
+
ops.push({ op: "add", path: join(path, i), value: b[i] });
|
|
327
|
+
}
|
|
328
|
+
} else if (a.length > b.length) {
|
|
329
|
+
for (let i = b.length; i < a.length; i++) {
|
|
330
|
+
ops.push({ op: "remove", path: join(path, i), oldValue: a[i] });
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
if (isPlainObject(a) && isPlainObject(b)) {
|
|
336
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
337
|
+
const sorted = [...keys].sort();
|
|
338
|
+
for (const k of sorted) {
|
|
339
|
+
const inA = Object.prototype.hasOwnProperty.call(a, k);
|
|
340
|
+
const inB = Object.prototype.hasOwnProperty.call(b, k);
|
|
341
|
+
if (inA && !inB) ops.push({ op: "remove", path: join(path, k), oldValue: a[k] });
|
|
342
|
+
else if (!inA && inB) ops.push({ op: "add", path: join(path, k), value: b[k] });
|
|
343
|
+
else walk(a[k], b[k], join(path, k), ops);
|
|
344
|
+
}
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
ops.push({ op: "replace", path: path || "/", oldValue: a, value: b });
|
|
348
|
+
}
|
|
349
|
+
function isBundle(v) {
|
|
350
|
+
return isPlainObject(v) && typeof v.specVersion === "string" && isPlainObject(v.manifest) && Array.isArray(v.entries);
|
|
351
|
+
}
|
|
352
|
+
function isEntry(v) {
|
|
353
|
+
return isPlainObject(v) && typeof v.id === "string" && Array.isArray(v.steps);
|
|
354
|
+
}
|
|
355
|
+
function diffEntryPair(a, b, indexA, indexB) {
|
|
356
|
+
const { entriesNdjsonBytes: _skip1, ...aClean } = a;
|
|
357
|
+
const { entriesNdjsonBytes: _skip2, ...bClean } = b;
|
|
358
|
+
void _skip1;
|
|
359
|
+
void _skip2;
|
|
360
|
+
const { ops } = diffCanonical(aClean, bClean);
|
|
361
|
+
const signatureChanged = ops.some((o) => o.path === "/signature" || o.path.startsWith("/signature/"));
|
|
362
|
+
const entryHashChanged = ops.some((o) => o.path === "/entryHash");
|
|
363
|
+
const stepsChanged = ops.some((o) => o.path === "/steps" || o.path.startsWith("/steps/"));
|
|
364
|
+
return {
|
|
365
|
+
id: a.id,
|
|
366
|
+
indexA,
|
|
367
|
+
indexB,
|
|
368
|
+
ops,
|
|
369
|
+
signatureChanged,
|
|
370
|
+
entryHashChanged,
|
|
371
|
+
stepsChanged
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
function diffReceipts(a, b) {
|
|
375
|
+
if (isEntry(a) && isEntry(b)) {
|
|
376
|
+
const change = diffEntryPair(a, b, 0, 0);
|
|
377
|
+
return {
|
|
378
|
+
equal: change.ops.length === 0,
|
|
379
|
+
kind: "entry",
|
|
380
|
+
specVersion: null,
|
|
381
|
+
chainHead: null,
|
|
382
|
+
devicePublicKeys: { added: [], removed: [] },
|
|
383
|
+
entries: {
|
|
384
|
+
added: [],
|
|
385
|
+
removed: [],
|
|
386
|
+
modified: change.ops.length === 0 ? [] : [change],
|
|
387
|
+
unchanged: change.ops.length === 0 ? 1 : 0
|
|
388
|
+
},
|
|
389
|
+
ops: change.ops
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
if (!isBundle(a) || !isBundle(b)) {
|
|
393
|
+
throw new Error("diffReceipts: inputs must be AvarBundle or AvarEntry values.");
|
|
394
|
+
}
|
|
395
|
+
const specVersion = a.specVersion === b.specVersion ? null : { from: a.specVersion, to: b.specVersion };
|
|
396
|
+
const headA = a.manifest.chainHead;
|
|
397
|
+
const headB = b.manifest.chainHead;
|
|
398
|
+
const chainHead = headA.entryHash === headB.entryHash && headA.index === headB.index ? null : {
|
|
399
|
+
from: headA,
|
|
400
|
+
to: headB,
|
|
401
|
+
extended: headB.index > headA.index
|
|
402
|
+
};
|
|
403
|
+
const keysA = new Set(a.manifest.devicePublicKeys ?? []);
|
|
404
|
+
const keysB = new Set(b.manifest.devicePublicKeys ?? []);
|
|
405
|
+
const devicePublicKeys = {
|
|
406
|
+
added: [...keysB].filter((k) => !keysA.has(k)).sort(),
|
|
407
|
+
removed: [...keysA].filter((k) => !keysB.has(k)).sort()
|
|
408
|
+
};
|
|
409
|
+
const byIdA = /* @__PURE__ */ new Map();
|
|
410
|
+
a.entries.forEach((e, i) => byIdA.set(e.id, { entry: e, index: i }));
|
|
411
|
+
const byIdB = /* @__PURE__ */ new Map();
|
|
412
|
+
b.entries.forEach((e, i) => byIdB.set(e.id, { entry: e, index: i }));
|
|
413
|
+
const added = [];
|
|
414
|
+
const removed = [];
|
|
415
|
+
const modified = [];
|
|
416
|
+
let unchanged = 0;
|
|
417
|
+
for (const [id, { entry, index }] of byIdB) {
|
|
418
|
+
const prior = byIdA.get(id);
|
|
419
|
+
if (!prior) {
|
|
420
|
+
added.push({ id, index });
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
const change = diffEntryPair(prior.entry, entry, prior.index, index);
|
|
424
|
+
if (change.ops.length === 0) unchanged++;
|
|
425
|
+
else modified.push(change);
|
|
426
|
+
}
|
|
427
|
+
for (const [id, { index }] of byIdA) {
|
|
428
|
+
if (!byIdB.has(id)) removed.push({ id, index });
|
|
429
|
+
}
|
|
430
|
+
added.sort((x, y) => x.index - y.index);
|
|
431
|
+
removed.sort((x, y) => x.index - y.index);
|
|
432
|
+
modified.sort((x, y) => x.indexA - y.indexA);
|
|
433
|
+
const strip = (bundle) => ({
|
|
434
|
+
specVersion: bundle.specVersion,
|
|
435
|
+
manifest: bundle.manifest,
|
|
436
|
+
pubkeys: bundle.pubkeys
|
|
437
|
+
});
|
|
438
|
+
const headerDiff = diffCanonical(strip(a), strip(b));
|
|
439
|
+
const equal = added.length === 0 && removed.length === 0 && modified.length === 0 && headerDiff.equal;
|
|
440
|
+
return {
|
|
441
|
+
equal,
|
|
442
|
+
kind: "bundle",
|
|
443
|
+
specVersion,
|
|
444
|
+
chainHead,
|
|
445
|
+
devicePublicKeys,
|
|
446
|
+
entries: { added, removed, modified, unchanged },
|
|
447
|
+
ops: headerDiff.ops
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
function diffPolicies(a, b) {
|
|
451
|
+
return diffCanonical(a, b);
|
|
452
|
+
}
|
|
453
|
+
function diffToolManifests(a, b) {
|
|
454
|
+
return diffCanonical(a, b);
|
|
455
|
+
}
|
|
297
456
|
export {
|
|
298
457
|
GENESIS_PREV_HASH,
|
|
299
458
|
GENESIS_PREV_STEP_HASH,
|
|
@@ -301,6 +460,10 @@ export {
|
|
|
301
460
|
computeDeviceFingerprint,
|
|
302
461
|
computeEntryHash,
|
|
303
462
|
computeStepHash,
|
|
463
|
+
diffCanonical,
|
|
464
|
+
diffPolicies,
|
|
465
|
+
diffReceipts,
|
|
466
|
+
diffToolManifests,
|
|
304
467
|
sha256Hex,
|
|
305
468
|
signedBodyOf,
|
|
306
469
|
utf8,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aarmos/avar-core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Reference implementation of the AVAR (Aarmos Verifiable Action Record) spec — canonical JSON, Ed25519 signature verification, hash-chain math, and bundle verification. Pure, zero-dep, browser + Node.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|