@mikeargento/bitgraph-verify 1.4.0 → 1.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/dist/fuse-member.d.ts +34 -0
- package/dist/fuse-member.d.ts.map +1 -0
- package/dist/fuse-member.js +306 -0
- package/dist/fuse-member.js.map +1 -0
- package/dist/fuse-verify.d.ts +3 -0
- package/dist/fuse-verify.d.ts.map +1 -1
- package/dist/fuse-verify.js +6 -2
- package/dist/fuse-verify.js.map +1 -1
- package/dist/fuse.d.ts +111 -2
- package/dist/fuse.d.ts.map +1 -1
- package/dist/fuse.js +258 -5
- package/dist/fuse.js.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/fuse-member.ts +432 -0
- package/src/fuse-verify.ts +7 -4
- package/src/fuse.ts +285 -6
- package/src/index.ts +8 -1
package/src/fuse.ts
CHANGED
|
@@ -247,7 +247,7 @@ export function parseFusePayload(bytes: Uint8Array): { commitment: Uint8Array; o
|
|
|
247
247
|
}
|
|
248
248
|
|
|
249
249
|
// ---------------------------------------------------------------------------
|
|
250
|
-
// Minimal deterministic ustar (POSIX.1-1988) for container/1
|
|
250
|
+
// Minimal deterministic ustar (POSIX.1-1988) for container/1 and container/2
|
|
251
251
|
// ---------------------------------------------------------------------------
|
|
252
252
|
|
|
253
253
|
const BLOCK = 512;
|
|
@@ -260,7 +260,7 @@ function octal(n: number, width: number): Uint8Array {
|
|
|
260
260
|
}
|
|
261
261
|
|
|
262
262
|
function ustarHeader(name: string, size: number): Uint8Array {
|
|
263
|
-
if (size > MAX_ENTRY) throw new RangeError("container
|
|
263
|
+
if (size > MAX_ENTRY) throw new RangeError("container entries are limited to 8 GiB");
|
|
264
264
|
const h = new Uint8Array(BLOCK);
|
|
265
265
|
const nameBytes = utf8(name);
|
|
266
266
|
if (nameBytes.length > 100) throw new RangeError("ustar name too long");
|
|
@@ -325,7 +325,7 @@ function parseTar(bytes: Uint8Array): TarEntry[] | null {
|
|
|
325
325
|
// Placement registry
|
|
326
326
|
// ---------------------------------------------------------------------------
|
|
327
327
|
|
|
328
|
-
export type PlacementId = "trailer/1" | "container/1" | "produced/1";
|
|
328
|
+
export type PlacementId = "trailer/1" | "container/1" | "container/2" | "produced/1" | "set/1";
|
|
329
329
|
|
|
330
330
|
export interface Located {
|
|
331
331
|
/** The commitment found in the fused bytes. */
|
|
@@ -336,6 +336,12 @@ export interface Located {
|
|
|
336
336
|
originalBytes?: Uint8Array;
|
|
337
337
|
}
|
|
338
338
|
|
|
339
|
+
/** What a Form A or B placement puts around the original: fused = prefix, original, suffix. */
|
|
340
|
+
export interface FusedFrame {
|
|
341
|
+
prefix: Uint8Array;
|
|
342
|
+
suffix: Uint8Array;
|
|
343
|
+
}
|
|
344
|
+
|
|
339
345
|
export interface Placement {
|
|
340
346
|
readonly id: PlacementId;
|
|
341
347
|
/** A: in-file placement of an existing original; B: container; C: produced artifact. */
|
|
@@ -346,6 +352,19 @@ export interface Placement {
|
|
|
346
352
|
build(input: { original?: Uint8Array; originDigest?: Uint8Array; commitment: Uint8Array }): Uint8Array;
|
|
347
353
|
/** Find the commitment in fused bytes, or null when the marker is absent or malformed. */
|
|
348
354
|
locate(fused: Uint8Array): Located | null;
|
|
355
|
+
/**
|
|
356
|
+
* Forms A and B: the bytes around the original, such that
|
|
357
|
+
* build({original, originDigest, commitment}) is exactly prefix, original,
|
|
358
|
+
* suffix. A producer that streams the original once can hash the prefix and
|
|
359
|
+
* the original as they pass and finish with the suffix later.
|
|
360
|
+
*/
|
|
361
|
+
frame?(input: { originalSize: number; originDigest: Uint8Array; commitment: Uint8Array }): FusedFrame;
|
|
362
|
+
/**
|
|
363
|
+
* The prefix when it depends on the original's size alone, so a scanner can
|
|
364
|
+
* hash it before any slot exists; null when the prefix carries the
|
|
365
|
+
* commitment (container/1) and the fused digest needs the bytes again.
|
|
366
|
+
*/
|
|
367
|
+
scanPrefix?(originalSize: number): Uint8Array | null;
|
|
349
368
|
}
|
|
350
369
|
|
|
351
370
|
const trailer1: Placement = {
|
|
@@ -364,6 +383,13 @@ const trailer1: Placement = {
|
|
|
364
383
|
if (!t.subarray(8, 16).every((b) => b === 0)) return null;
|
|
365
384
|
return { commitment: new Uint8Array(t.subarray(16, 48)), originalBytes: fused.subarray(0, fused.length - TRAILER_LENGTH) };
|
|
366
385
|
},
|
|
386
|
+
frame({ commitment }) {
|
|
387
|
+
if (commitment.length !== 32) throw new TypeError("commitment must be 32 bytes");
|
|
388
|
+
return { prefix: new Uint8Array(0), suffix: concat(utf8(TRAILER_MAGIC), new Uint8Array(8), commitment) };
|
|
389
|
+
},
|
|
390
|
+
scanPrefix() {
|
|
391
|
+
return new Uint8Array(0);
|
|
392
|
+
},
|
|
367
393
|
};
|
|
368
394
|
|
|
369
395
|
const container1: Placement = {
|
|
@@ -387,6 +413,66 @@ const container1: Placement = {
|
|
|
387
413
|
if (!bytesEqual(rebuilt, fused)) return null;
|
|
388
414
|
return { commitment: payload.commitment, originDigest: payload.originDigest, originalBytes: o.data };
|
|
389
415
|
},
|
|
416
|
+
frame({ originalSize, originDigest, commitment }) {
|
|
417
|
+
const manifest = buildFusePayload(commitment, originDigest);
|
|
418
|
+
return {
|
|
419
|
+
prefix: concat(ustarHeader(CONTAINER_MANIFEST_PATH, manifest.length), manifest, new Uint8Array(padTo(manifest.length)), ustarHeader(CONTAINER_ORIGINAL_PATH, originalSize)),
|
|
420
|
+
suffix: concat(new Uint8Array(padTo(originalSize)), new Uint8Array(BLOCK * 2)),
|
|
421
|
+
};
|
|
422
|
+
},
|
|
423
|
+
scanPrefix() {
|
|
424
|
+
// The manifest, and so the commitment, comes before the original.
|
|
425
|
+
return null;
|
|
426
|
+
},
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* container/2: the same archive with the original FIRST. Everything before
|
|
431
|
+
* the original's bytes is its ustar header, which depends on the size alone,
|
|
432
|
+
* so a scanner that hashes header and original as the file streams by can
|
|
433
|
+
* finish the fused digest later with the manifest for whatever slot the
|
|
434
|
+
* set is made under, without reading the file again. Any bytes fit: the
|
|
435
|
+
* original stays byte-exact inside, and the archive is a plain tar.
|
|
436
|
+
*/
|
|
437
|
+
function buildContainer2(original: Uint8Array, manifest: Uint8Array): Uint8Array {
|
|
438
|
+
return concat(
|
|
439
|
+
ustarHeader(CONTAINER_ORIGINAL_PATH, original.length), original, new Uint8Array(padTo(original.length)),
|
|
440
|
+
ustarHeader(CONTAINER_MANIFEST_PATH, manifest.length), manifest, new Uint8Array(padTo(manifest.length)),
|
|
441
|
+
new Uint8Array(BLOCK * 2),
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const container2: Placement = {
|
|
446
|
+
id: "container/2",
|
|
447
|
+
form: "B",
|
|
448
|
+
byteExact: true,
|
|
449
|
+
build({ original, originDigest, commitment }) {
|
|
450
|
+
if (original === undefined) throw new TypeError("container/2 requires the original bytes");
|
|
451
|
+
const digest = originDigest ?? sha256(original);
|
|
452
|
+
return buildContainer2(original, buildFusePayload(commitment, digest));
|
|
453
|
+
},
|
|
454
|
+
locate(fused) {
|
|
455
|
+
const entries = parseTar(fused);
|
|
456
|
+
if (entries === null || entries.length !== 2) return null;
|
|
457
|
+
const [o, m] = entries as [TarEntry, TarEntry];
|
|
458
|
+
if (o.name !== CONTAINER_ORIGINAL_PATH || m.name !== CONTAINER_MANIFEST_PATH) return null;
|
|
459
|
+
const payload = parseFusePayload(m.data);
|
|
460
|
+
if (payload === null || payload.originDigest === undefined) return null;
|
|
461
|
+
// The archive must be the one this module would build: headers included.
|
|
462
|
+
const rebuilt = buildContainer2(o.data, m.data);
|
|
463
|
+
if (!bytesEqual(rebuilt, fused)) return null;
|
|
464
|
+
return { commitment: payload.commitment, originDigest: payload.originDigest, originalBytes: o.data };
|
|
465
|
+
},
|
|
466
|
+
frame({ originalSize, originDigest, commitment }) {
|
|
467
|
+
const manifest = buildFusePayload(commitment, originDigest);
|
|
468
|
+
return {
|
|
469
|
+
prefix: ustarHeader(CONTAINER_ORIGINAL_PATH, originalSize),
|
|
470
|
+
suffix: concat(new Uint8Array(padTo(originalSize)), ustarHeader(CONTAINER_MANIFEST_PATH, manifest.length), manifest, new Uint8Array(padTo(manifest.length)), new Uint8Array(BLOCK * 2)),
|
|
471
|
+
};
|
|
472
|
+
},
|
|
473
|
+
scanPrefix(originalSize) {
|
|
474
|
+
return ustarHeader(CONTAINER_ORIGINAL_PATH, originalSize);
|
|
475
|
+
},
|
|
390
476
|
};
|
|
391
477
|
|
|
392
478
|
const produced1: Placement = {
|
|
@@ -407,19 +493,212 @@ const produced1: Placement = {
|
|
|
407
493
|
};
|
|
408
494
|
|
|
409
495
|
/** Registered placements in the fixed order a verifier tries them when none is declared. */
|
|
410
|
-
export const PLACEMENTS: readonly Placement[] = Object.freeze([trailer1, container1, produced1]);
|
|
496
|
+
export const PLACEMENTS: readonly Placement[] = Object.freeze([trailer1, container1, container2, produced1]);
|
|
497
|
+
|
|
498
|
+
// ---------------------------------------------------------------------------
|
|
499
|
+
// Set manifest (placement set/1): N files fused under ONE slot
|
|
500
|
+
// ---------------------------------------------------------------------------
|
|
501
|
+
//
|
|
502
|
+
// A set is N files fused under one slot. The commitment c is computed once
|
|
503
|
+
// from the one slot record; every member's fused bytes carry c via that
|
|
504
|
+
// member's own placement (trailer/1 or container/1, chosen per file as
|
|
505
|
+
// today); and the COMMITTED ARTIFACT is a canonical manifest listing the
|
|
506
|
+
// members' fused digests, origin digests and placement ids, plus c itself.
|
|
507
|
+
// The manifest is a Form C artifact under the placement id "set/1".
|
|
508
|
+
//
|
|
509
|
+
// Its canonical encoding is load-bearing: one committed hash must stand for
|
|
510
|
+
// exactly one member list. buildSetManifest is the single source of the byte
|
|
511
|
+
// layout (rows strictly ascending by artifact digest, lowercase hex, sorted
|
|
512
|
+
// keys, no whitespace) and parseSetManifest accepts nothing that is not byte
|
|
513
|
+
// for byte equal to its own rebuild. Anything outside that domain is refused,
|
|
514
|
+
// never normalized.
|
|
515
|
+
|
|
516
|
+
export const SET_PLACEMENT_ID = "set/1" as const;
|
|
411
517
|
|
|
518
|
+
/**
|
|
519
|
+
* The proof.metadata key under which a set proof carries its manifest as a
|
|
520
|
+
* parsed plain object: the profile id itself, namespaced so it cannot collide
|
|
521
|
+
* with a site's own metadata keys. metadata is UNSIGNED and advisory. The
|
|
522
|
+
* manifest is protected only because its canonical bytes must hash to the
|
|
523
|
+
* signed artifact digest, which verifyFuseMember checks before reading a row.
|
|
524
|
+
*/
|
|
525
|
+
export const SET_METADATA_KEY = FUSE_PROFILE;
|
|
526
|
+
|
|
527
|
+
/** A placement id is a lowercase name, a slash, and a positive version: "trailer/1". */
|
|
528
|
+
const PLACEMENT_ID_PATTERN = /^[a-z0-9][a-z0-9-]*\/[1-9][0-9]*$/;
|
|
529
|
+
|
|
530
|
+
/** One member of a set, bytes view: the build input and the parse output. */
|
|
531
|
+
export interface SetMember {
|
|
532
|
+
/** SHA-256 of the member's FUSED bytes. */
|
|
533
|
+
artifact: Uint8Array;
|
|
534
|
+
/** SHA-256 of the member's ORIGINAL bytes; selects the rebuild path and feeds lookup by original. */
|
|
535
|
+
origin: Uint8Array;
|
|
536
|
+
/** The placement that carries the commitment inside this member's fused bytes. */
|
|
537
|
+
placement: string;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/** The manifest as JSON: the type of the value under proof.metadata[SET_METADATA_KEY]. */
|
|
541
|
+
export interface SetManifest {
|
|
542
|
+
members: Array<{
|
|
543
|
+
artifact: { algorithm: "sha256"; digest: string };
|
|
544
|
+
origin: { algorithm: "sha256"; digest: string };
|
|
545
|
+
placement: string;
|
|
546
|
+
}>;
|
|
547
|
+
placement: typeof SET_PLACEMENT_ID;
|
|
548
|
+
slotCommitment: { algorithm: "sha256"; digest: string };
|
|
549
|
+
type: typeof FUSE_PROFILE;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Build the canonical set manifest bytes. Rows are sorted strictly ascending
|
|
554
|
+
* by artifact digest (byte order, which is the lexicographic order of the
|
|
555
|
+
* lowercase hex), so the same members in any input order give the same bytes.
|
|
556
|
+
* Throws on a commitment or digest that is not 32 bytes, an empty list, a
|
|
557
|
+
* malformed placement id, a member placement of "set/1" (no nesting in v1),
|
|
558
|
+
* or a duplicate artifact digest. Duplicate ORIGIN digests are permitted: one
|
|
559
|
+
* original fused two ways is two members with two artifact digests.
|
|
560
|
+
*/
|
|
561
|
+
export function buildSetManifest(commitment: Uint8Array, members: readonly SetMember[]): Uint8Array {
|
|
562
|
+
if (commitment.length !== 32) throw new TypeError("commitment must be 32 bytes");
|
|
563
|
+
if (members.length === 0) throw new TypeError("a set lists at least one member");
|
|
564
|
+
const rows: SetManifest["members"] = [];
|
|
565
|
+
const seen = new Set<string>();
|
|
566
|
+
for (const m of members) {
|
|
567
|
+
if (m.artifact.length !== 32) throw new TypeError("member artifact digest must be 32 bytes");
|
|
568
|
+
if (m.origin.length !== 32) throw new TypeError("member origin digest must be 32 bytes");
|
|
569
|
+
if (!PLACEMENT_ID_PATTERN.test(m.placement)) throw new TypeError(`member placement "${m.placement}" is not a placement id`);
|
|
570
|
+
if (m.placement === SET_PLACEMENT_ID) throw new TypeError("a set cannot list a set as a member");
|
|
571
|
+
const artifact = bytesToHex(m.artifact);
|
|
572
|
+
if (seen.has(artifact)) throw new TypeError(`duplicate member artifact digest ${artifact}`);
|
|
573
|
+
seen.add(artifact);
|
|
574
|
+
rows.push({
|
|
575
|
+
artifact: { algorithm: "sha256", digest: artifact },
|
|
576
|
+
origin: { algorithm: "sha256", digest: bytesToHex(m.origin) },
|
|
577
|
+
placement: m.placement,
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
rows.sort((a, b) => (a.artifact.digest < b.artifact.digest ? -1 : 1));
|
|
581
|
+
const manifest: SetManifest = {
|
|
582
|
+
members: rows,
|
|
583
|
+
placement: SET_PLACEMENT_ID,
|
|
584
|
+
slotCommitment: { algorithm: "sha256", digest: bytesToHex(commitment) },
|
|
585
|
+
type: FUSE_PROFILE,
|
|
586
|
+
};
|
|
587
|
+
return canonicalize(manifest);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Strict parse of set manifest bytes, in the style of parseFusePayload. The
|
|
592
|
+
* bytes must be valid UTF-8 JSON, a plain object with exactly the keys
|
|
593
|
+
* {members, placement, slotCommitment, type}, the profile type, placement
|
|
594
|
+
* "set/1", a lowercase-hex 32-byte commitment, at least one row, every row
|
|
595
|
+
* exactly {artifact, origin, placement} with 32-byte digests and a
|
|
596
|
+
* well-formed placement id other than "set/1", and finally must equal
|
|
597
|
+
* buildSetManifest over what was read byte for byte. That one comparison
|
|
598
|
+
* rejects whitespace, key reordering, duplicate JSON keys (a duplicate cannot
|
|
599
|
+
* survive a round trip), unsorted or duplicated rows, non-canonical escapes
|
|
600
|
+
* and trailing bytes. Registration of a row's placement is NOT checked here:
|
|
601
|
+
* a v2 placement must not poison v1 readers, and it surfaces per row as
|
|
602
|
+
* UNDETERMINED_PLACEMENT at verify time. Returns null on any deviation.
|
|
603
|
+
*/
|
|
604
|
+
export function parseSetManifest(bytes: Uint8Array): { commitment: Uint8Array; members: SetMember[] } | null {
|
|
605
|
+
let text: string;
|
|
606
|
+
try {
|
|
607
|
+
// ignoreBOM keeps a leading BOM in the text, where JSON.parse refuses it,
|
|
608
|
+
// rather than stripping it as though it were whitespace.
|
|
609
|
+
text = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
|
|
610
|
+
} catch {
|
|
611
|
+
return null;
|
|
612
|
+
}
|
|
613
|
+
let parsed: unknown;
|
|
614
|
+
try {
|
|
615
|
+
parsed = JSON.parse(text);
|
|
616
|
+
} catch {
|
|
617
|
+
return null;
|
|
618
|
+
}
|
|
619
|
+
if (!isPlainObject(parsed)) return null;
|
|
620
|
+
if (Object.keys(parsed).sort().join(",") !== "members,placement,slotCommitment,type") return null;
|
|
621
|
+
if (parsed["type"] !== FUSE_PROFILE || parsed["placement"] !== SET_PLACEMENT_ID) return null;
|
|
622
|
+
const commitment = readDigestField(parsed["slotCommitment"]);
|
|
623
|
+
if (commitment === null) return null;
|
|
624
|
+
const list = parsed["members"];
|
|
625
|
+
if (!Array.isArray(list) || list.length === 0) return null;
|
|
626
|
+
const members: SetMember[] = [];
|
|
627
|
+
for (const row of list as unknown[]) {
|
|
628
|
+
if (!isPlainObject(row)) return null;
|
|
629
|
+
if (Object.keys(row).sort().join(",") !== "artifact,origin,placement") return null;
|
|
630
|
+
const artifact = readDigestField(row["artifact"]);
|
|
631
|
+
const origin = readDigestField(row["origin"]);
|
|
632
|
+
const placement = row["placement"];
|
|
633
|
+
if (artifact === null || origin === null || typeof placement !== "string") return null;
|
|
634
|
+
if (!PLACEMENT_ID_PATTERN.test(placement) || placement === SET_PLACEMENT_ID) return null;
|
|
635
|
+
members.push({ artifact, origin, placement });
|
|
636
|
+
}
|
|
637
|
+
let rebuilt: Uint8Array;
|
|
638
|
+
try {
|
|
639
|
+
rebuilt = buildSetManifest(commitment, members);
|
|
640
|
+
} catch {
|
|
641
|
+
return null;
|
|
642
|
+
}
|
|
643
|
+
if (!bytesEqual(rebuilt, bytes)) return null;
|
|
644
|
+
return { commitment, members };
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* The manifest bytes a proof carries under proof.metadata[SET_METADATA_KEY],
|
|
649
|
+
* re-canonicalized from the parsed object, or null when there is none or it
|
|
650
|
+
* is not a plain object. UNBOUND and UNVALIDATED: metadata is unsigned, so
|
|
651
|
+
* this returns bytes only, never rows. Nothing reads a member from it except
|
|
652
|
+
* through verifyFuseMember, which first requires these bytes to parse
|
|
653
|
+
* strictly and to hash to the signed artifact digest.
|
|
654
|
+
*/
|
|
655
|
+
export function readSetMetadata(proof: BitGraphProof): Uint8Array | null {
|
|
656
|
+
const value = proof.metadata?.[SET_METADATA_KEY];
|
|
657
|
+
if (!isPlainObject(value)) return null;
|
|
658
|
+
try {
|
|
659
|
+
return canonicalize(value);
|
|
660
|
+
} catch {
|
|
661
|
+
return null;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
const set1: Placement = {
|
|
666
|
+
id: SET_PLACEMENT_ID,
|
|
667
|
+
form: "C",
|
|
668
|
+
byteExact: false,
|
|
669
|
+
build() {
|
|
670
|
+
throw new TypeError("set/1 is built with buildSetManifest(commitment, members)");
|
|
671
|
+
},
|
|
672
|
+
locate(fused) {
|
|
673
|
+
const manifest = parseSetManifest(fused);
|
|
674
|
+
return manifest === null ? null : { commitment: manifest.commitment };
|
|
675
|
+
},
|
|
676
|
+
};
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* Resolve a placement by id. set/1 resolves here but is NOT in PLACEMENTS:
|
|
680
|
+
* the undeclared scan is for bytes whose placement was not declared, whereas
|
|
681
|
+
* a set manifest is identified by hashing to the signed artifact digest and
|
|
682
|
+
* by its signed title, so the scan order of every existing fixture is
|
|
683
|
+
* literally unchanged.
|
|
684
|
+
*/
|
|
412
685
|
export function getPlacement(id: string): Placement | undefined {
|
|
413
|
-
return PLACEMENTS.find((p) => p.id === id);
|
|
686
|
+
return [...PLACEMENTS, set1].find((p) => p.id === id);
|
|
414
687
|
}
|
|
415
688
|
|
|
416
689
|
// ---------------------------------------------------------------------------
|
|
417
690
|
// Attribution (the signed carrier of placement and origin, spec 6.5)
|
|
418
691
|
// ---------------------------------------------------------------------------
|
|
419
692
|
|
|
420
|
-
/**
|
|
693
|
+
/**
|
|
694
|
+
* attribution.name = "bitgraph-fuse/1" (the profile id), title = placement id,
|
|
695
|
+
* message = origin digest in standard base64. A set has no single origin, so
|
|
696
|
+
* set/1 refuses an origin digest: a set/1 marker carrying one is out of
|
|
697
|
+
* profile and verifyFuseMember refuses it.
|
|
698
|
+
*/
|
|
421
699
|
export function fuseAttribution(placement: PlacementId, originDigest?: Uint8Array): Attribution {
|
|
422
700
|
if (originDigest !== undefined && originDigest.length !== 32) throw new TypeError("originDigest must be 32 bytes");
|
|
701
|
+
if (placement === SET_PLACEMENT_ID && originDigest !== undefined) throw new TypeError("set/1 has no single origin; a set marker carries no origin digest");
|
|
423
702
|
return {
|
|
424
703
|
name: FUSE_ATTRIBUTION_NAME,
|
|
425
704
|
title: placement,
|
package/src/index.ts
CHANGED
|
@@ -41,6 +41,11 @@ export {
|
|
|
41
41
|
CONTAINER_ORIGINAL_PATH,
|
|
42
42
|
PLACEMENTS,
|
|
43
43
|
getPlacement,
|
|
44
|
+
SET_PLACEMENT_ID,
|
|
45
|
+
SET_METADATA_KEY,
|
|
46
|
+
buildSetManifest,
|
|
47
|
+
parseSetManifest,
|
|
48
|
+
readSetMetadata,
|
|
44
49
|
canonicalSlotBody,
|
|
45
50
|
computeSlotRecordHash,
|
|
46
51
|
slotCommitmentPreimage,
|
|
@@ -59,6 +64,8 @@ export {
|
|
|
59
64
|
hexToBytes,
|
|
60
65
|
bytesEqual,
|
|
61
66
|
} from "./fuse.js";
|
|
62
|
-
export type { PlacementId, Placement, Located, FusePayload, FuseFrame, FuseMarker, MarkerSource } from "./fuse.js";
|
|
67
|
+
export type { PlacementId, Placement, Located, FusePayload, FuseFrame, FuseMarker, MarkerSource, SetMember, SetManifest, FusedFrame } from "./fuse.js";
|
|
63
68
|
export { verifyFuse, assembledAfterCommit } from "./fuse-verify.js";
|
|
64
69
|
export type { FuseCategory, FuseSpan, FuseVerifyResult, FuseVerifyOptions } from "./fuse-verify.js";
|
|
70
|
+
export { verifyFuseMember } from "./fuse-member.js";
|
|
71
|
+
export type { FuseMemberCategory, FuseSetEvidence, FuseMemberOptions, FuseMemberResult } from "./fuse-member.js";
|