@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 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
- export { type AvarBundle, type AvarEntry, type BundleManifest, type BundlePubKeys, type DecisionStep, GENESIS_PREV_HASH, GENESIS_PREV_STEP_HASH, type PolicyHit, type TextStep, type ToolStep, type TraceStep, type VerificationIssue, type VerificationReport, canonicalize, computeDeviceFingerprint, computeEntryHash, computeStepHash, sha256Hex, signedBodyOf, utf8, verifyBundle, verifySignature };
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.0.0",
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",