@mikeargento/bitgraph-verify 1.5.0 → 1.7.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.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { MerkleTree } from "./fuse-merkle.js";
1
2
  import type { Attribution, BitGraphProof, SlotAllocation } from "./types.js";
2
3
  export declare const FUSE_PROFILE: "bitgraph-fuse/1";
3
4
  /** Domain separation prefix: the 15 profile bytes followed by one zero byte. */
@@ -59,7 +60,7 @@ export declare function parseFusePayload(bytes: Uint8Array): {
59
60
  commitment: Uint8Array;
60
61
  originDigest?: Uint8Array;
61
62
  } | null;
62
- export type PlacementId = "trailer/1" | "container/1" | "produced/1" | "set/1";
63
+ export type PlacementId = "trailer/1" | "container/1" | "container/2" | "produced/1" | "set/1" | "set/2";
63
64
  export interface Located {
64
65
  /** The commitment found in the fused bytes. */
65
66
  commitment: Uint8Array;
@@ -68,6 +69,11 @@ export interface Located {
68
69
  /** The original's bytes as carried inside the fused bytes, for placements that carry them. */
69
70
  originalBytes?: Uint8Array;
70
71
  }
72
+ /** What a Form A or B placement puts around the original: fused = prefix, original, suffix. */
73
+ export interface FusedFrame {
74
+ prefix: Uint8Array;
75
+ suffix: Uint8Array;
76
+ }
71
77
  export interface Placement {
72
78
  readonly id: PlacementId;
73
79
  /** A: in-file placement of an existing original; B: container; C: produced artifact. */
@@ -82,6 +88,23 @@ export interface Placement {
82
88
  }): Uint8Array;
83
89
  /** Find the commitment in fused bytes, or null when the marker is absent or malformed. */
84
90
  locate(fused: Uint8Array): Located | null;
91
+ /**
92
+ * Forms A and B: the bytes around the original, such that
93
+ * build({original, originDigest, commitment}) is exactly prefix, original,
94
+ * suffix. A producer that streams the original once can hash the prefix and
95
+ * the original as they pass and finish with the suffix later.
96
+ */
97
+ frame?(input: {
98
+ originalSize: number;
99
+ originDigest: Uint8Array;
100
+ commitment: Uint8Array;
101
+ }): FusedFrame;
102
+ /**
103
+ * The prefix when it depends on the original's size alone, so a scanner can
104
+ * hash it before any slot exists; null when the prefix carries the
105
+ * commitment (container/1) and the fused digest needs the bytes again.
106
+ */
107
+ scanPrefix?(originalSize: number): Uint8Array | null;
85
108
  }
86
109
  /** Registered placements in the fixed order a verifier tries them when none is declared. */
87
110
  export declare const PLACEMENTS: readonly Placement[];
@@ -160,8 +183,74 @@ export declare function parseSetManifest(bytes: Uint8Array): {
160
183
  * strictly and to hash to the signed artifact digest.
161
184
  */
162
185
  export declare function readSetMetadata(proof: BitGraphProof): Uint8Array | null;
186
+ export declare const SET2_PLACEMENT_ID: "set/2";
187
+ /** The proof.metadata key under which a member's own evidence (row, index, count, path) may ride, UNSIGNED, beside the root document. */
188
+ export declare const SET_MEMBER_METADATA_KEY: "bitgraph-fuse/1/member";
189
+ /** The most members one set/2 tree lists. A limit stated plainly, not a design constant: the tree and the paths are fine far beyond it. */
190
+ export declare const MAX_SET2_MEMBERS = 1000000;
191
+ /** The root document as JSON: the type of the value under proof.metadata[SET_METADATA_KEY] for a set/2 proof. */
192
+ export interface SetRoot {
193
+ count: number;
194
+ placement: typeof SET2_PLACEMENT_ID;
195
+ root: {
196
+ algorithm: "sha256";
197
+ digest: string;
198
+ };
199
+ slotCommitment: {
200
+ algorithm: "sha256";
201
+ digest: string;
202
+ };
203
+ type: typeof FUSE_PROFILE;
204
+ }
205
+ /** One member's evidence as JSON: its row, its leaf index, the tree size, and the sibling path from the leaf up. */
206
+ export interface SetMemberProof {
207
+ count: number;
208
+ index: number;
209
+ member: SetManifest["members"][number];
210
+ path: string[];
211
+ placement: typeof SET2_PLACEMENT_ID;
212
+ type: typeof FUSE_PROFILE;
213
+ }
214
+ /** The canonical bytes of one row, exactly as a set/1 manifest lists it; the leaf a set/2 tree hashes. */
215
+ export declare function canonicalSetRow(m: SetMember): Uint8Array;
216
+ /** The leaf hash of one row: SHA-256(0x00, canonical row bytes). */
217
+ export declare function setLeaf(m: SetMember): Uint8Array;
218
+ /**
219
+ * Order members as a set/2 tree lists them: strictly ascending by artifact
220
+ * digest, no duplicate artifact. Throws on a duplicate, an empty list, or a
221
+ * malformed row, like buildSetManifest.
222
+ */
223
+ export declare function sortSetMembers(members: readonly SetMember[]): SetMember[];
224
+ /** The tree over a member list, in tree order (sortSetMembers): the sorted rows, their leaf hashes, the root, and every member's path on demand. */
225
+ export declare function buildSetTree(members: readonly SetMember[]): {
226
+ sorted: SetMember[];
227
+ leaves: Uint8Array[];
228
+ root: Uint8Array;
229
+ tree: MerkleTree;
230
+ };
231
+ /** The inclusion path of the member at `index` in tree order. */
232
+ export declare function setMemberPath(tree: MerkleTree, index: number): Uint8Array[];
233
+ /** Build the canonical set/2 root document bytes: the committed artifact. */
234
+ export declare function buildSetRoot(commitment: Uint8Array, count: number, root: Uint8Array): Uint8Array;
235
+ /** Strict parse of set/2 root document bytes: exactly the five keys, the literals, 32-byte digests, a count in range, byte-equal to its own rebuild. */
236
+ export declare function parseSetRoot(bytes: Uint8Array): {
237
+ commitment: Uint8Array;
238
+ count: number;
239
+ root: Uint8Array;
240
+ } | null;
241
+ /** Build a member's evidence object (JSON shape) from tree order. */
242
+ export declare function buildSetMemberProof(member: SetMember, index: number, count: number, path: readonly Uint8Array[]): SetMemberProof;
243
+ /** Strict read of a member's evidence from a parsed JSON value: shape, literals, 32-byte hex digests and path nodes, index within count. Null on any deviation. UNBOUND: nothing here touches a root. */
244
+ export declare function parseSetMemberProof(value: unknown): {
245
+ member: SetMember;
246
+ index: number;
247
+ count: number;
248
+ path: Uint8Array[];
249
+ } | null;
250
+ /** Recompute a root from a member's leaf and path; null when the path does not fit. */
251
+ export declare function setRootFromMember(member: SetMember, index: number, count: number, path: readonly Uint8Array[]): Uint8Array | null;
163
252
  /**
164
- * Resolve a placement by id. set/1 resolves here but is NOT in PLACEMENTS:
253
+ * Resolve a placement by id. set/1 and set/2 resolve here but are NOT in PLACEMENTS:
165
254
  * the undeclared scan is for bytes whose placement was not declared, whereas
166
255
  * a set manifest is identified by hashing to the signed artifact digest and
167
256
  * by its signed title, so the scan order of every existing fixture is
@@ -1 +1 @@
1
- {"version":3,"file":"fuse.d.ts","sourceRoot":"","sources":["../src/fuse.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"fuse.d.ts","sourceRoot":"","sources":["../src/fuse.ts"],"names":[],"mappings":"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"}
package/dist/fuse.js CHANGED
@@ -24,6 +24,7 @@
24
24
  * claim the slot.
25
25
  */
26
26
  import { sha256 } from "@noble/hashes/sha256";
27
+ import { merkleLeafHash, merkleRootFromPath, MerkleTree } from "./fuse-merkle.js";
27
28
  import { canonicalize } from "./canonical.js";
28
29
  // ---------------------------------------------------------------------------
29
30
  // Constants
@@ -234,7 +235,7 @@ export function parseFusePayload(bytes) {
234
235
  return originDigest !== undefined ? { commitment, originDigest } : { commitment };
235
236
  }
236
237
  // ---------------------------------------------------------------------------
237
- // Minimal deterministic ustar (POSIX.1-1988) for container/1
238
+ // Minimal deterministic ustar (POSIX.1-1988) for container/1 and container/2
238
239
  // ---------------------------------------------------------------------------
239
240
  const BLOCK = 512;
240
241
  const MAX_ENTRY = 0o77777777777; // 8 GiB - 1, the 11-digit octal size field
@@ -246,7 +247,7 @@ function octal(n, width) {
246
247
  }
247
248
  function ustarHeader(name, size) {
248
249
  if (size > MAX_ENTRY)
249
- throw new RangeError("container/1 entries are limited to 8 GiB");
250
+ throw new RangeError("container entries are limited to 8 GiB");
250
251
  const h = new Uint8Array(BLOCK);
251
252
  const nameBytes = utf8(name);
252
253
  if (nameBytes.length > 100)
@@ -323,6 +324,14 @@ const trailer1 = {
323
324
  return null;
324
325
  return { commitment: new Uint8Array(t.subarray(16, 48)), originalBytes: fused.subarray(0, fused.length - TRAILER_LENGTH) };
325
326
  },
327
+ frame({ commitment }) {
328
+ if (commitment.length !== 32)
329
+ throw new TypeError("commitment must be 32 bytes");
330
+ return { prefix: new Uint8Array(0), suffix: concat(utf8(TRAILER_MAGIC), new Uint8Array(8), commitment) };
331
+ },
332
+ scanPrefix() {
333
+ return new Uint8Array(0);
334
+ },
326
335
  };
327
336
  const container1 = {
328
337
  id: "container/1",
@@ -350,6 +359,65 @@ const container1 = {
350
359
  return null;
351
360
  return { commitment: payload.commitment, originDigest: payload.originDigest, originalBytes: o.data };
352
361
  },
362
+ frame({ originalSize, originDigest, commitment }) {
363
+ const manifest = buildFusePayload(commitment, originDigest);
364
+ return {
365
+ prefix: concat(ustarHeader(CONTAINER_MANIFEST_PATH, manifest.length), manifest, new Uint8Array(padTo(manifest.length)), ustarHeader(CONTAINER_ORIGINAL_PATH, originalSize)),
366
+ suffix: concat(new Uint8Array(padTo(originalSize)), new Uint8Array(BLOCK * 2)),
367
+ };
368
+ },
369
+ scanPrefix() {
370
+ // The manifest, and so the commitment, comes before the original.
371
+ return null;
372
+ },
373
+ };
374
+ /**
375
+ * container/2: the same archive with the original FIRST. Everything before
376
+ * the original's bytes is its ustar header, which depends on the size alone,
377
+ * so a scanner that hashes header and original as the file streams by can
378
+ * finish the fused digest later with the manifest for whatever slot the
379
+ * set is made under, without reading the file again. Any bytes fit: the
380
+ * original stays byte-exact inside, and the archive is a plain tar.
381
+ */
382
+ function buildContainer2(original, manifest) {
383
+ return concat(ustarHeader(CONTAINER_ORIGINAL_PATH, original.length), original, new Uint8Array(padTo(original.length)), ustarHeader(CONTAINER_MANIFEST_PATH, manifest.length), manifest, new Uint8Array(padTo(manifest.length)), new Uint8Array(BLOCK * 2));
384
+ }
385
+ const container2 = {
386
+ id: "container/2",
387
+ form: "B",
388
+ byteExact: true,
389
+ build({ original, originDigest, commitment }) {
390
+ if (original === undefined)
391
+ throw new TypeError("container/2 requires the original bytes");
392
+ const digest = originDigest ?? sha256(original);
393
+ return buildContainer2(original, buildFusePayload(commitment, digest));
394
+ },
395
+ locate(fused) {
396
+ const entries = parseTar(fused);
397
+ if (entries === null || entries.length !== 2)
398
+ return null;
399
+ const [o, m] = entries;
400
+ if (o.name !== CONTAINER_ORIGINAL_PATH || m.name !== CONTAINER_MANIFEST_PATH)
401
+ return null;
402
+ const payload = parseFusePayload(m.data);
403
+ if (payload === null || payload.originDigest === undefined)
404
+ return null;
405
+ // The archive must be the one this module would build: headers included.
406
+ const rebuilt = buildContainer2(o.data, m.data);
407
+ if (!bytesEqual(rebuilt, fused))
408
+ return null;
409
+ return { commitment: payload.commitment, originDigest: payload.originDigest, originalBytes: o.data };
410
+ },
411
+ frame({ originalSize, originDigest, commitment }) {
412
+ const manifest = buildFusePayload(commitment, originDigest);
413
+ return {
414
+ prefix: ustarHeader(CONTAINER_ORIGINAL_PATH, originalSize),
415
+ suffix: concat(new Uint8Array(padTo(originalSize)), ustarHeader(CONTAINER_MANIFEST_PATH, manifest.length), manifest, new Uint8Array(padTo(manifest.length)), new Uint8Array(BLOCK * 2)),
416
+ };
417
+ },
418
+ scanPrefix(originalSize) {
419
+ return ustarHeader(CONTAINER_ORIGINAL_PATH, originalSize);
420
+ },
353
421
  };
354
422
  const produced1 = {
355
423
  id: "produced/1",
@@ -370,7 +438,7 @@ const produced1 = {
370
438
  },
371
439
  };
372
440
  /** Registered placements in the fixed order a verifier tries them when none is declared. */
373
- export const PLACEMENTS = Object.freeze([trailer1, container1, produced1]);
441
+ export const PLACEMENTS = Object.freeze([trailer1, container1, container2, produced1]);
374
442
  // ---------------------------------------------------------------------------
375
443
  // Set manifest (placement set/1): N files fused under ONE slot
376
444
  // ---------------------------------------------------------------------------
@@ -422,7 +490,7 @@ export function buildSetManifest(commitment, members) {
422
490
  throw new TypeError("member origin digest must be 32 bytes");
423
491
  if (!PLACEMENT_ID_PATTERN.test(m.placement))
424
492
  throw new TypeError(`member placement "${m.placement}" is not a placement id`);
425
- if (m.placement === SET_PLACEMENT_ID)
493
+ if (m.placement === SET_PLACEMENT_ID || m.placement === SET2_PLACEMENT_ID)
426
494
  throw new TypeError("a set cannot list a set as a member");
427
495
  const artifact = bytesToHex(m.artifact);
428
496
  if (seen.has(artifact))
@@ -497,7 +565,7 @@ export function parseSetManifest(bytes) {
497
565
  const placement = row["placement"];
498
566
  if (artifact === null || origin === null || typeof placement !== "string")
499
567
  return null;
500
- if (!PLACEMENT_ID_PATTERN.test(placement) || placement === SET_PLACEMENT_ID)
568
+ if (!PLACEMENT_ID_PATTERN.test(placement) || placement === SET_PLACEMENT_ID || placement === SET2_PLACEMENT_ID)
501
569
  return null;
502
570
  members.push({ artifact, origin, placement });
503
571
  }
@@ -543,15 +611,221 @@ const set1 = {
543
611
  return manifest === null ? null : { commitment: manifest.commitment };
544
612
  },
545
613
  };
614
+ // ---------------------------------------------------------------------------
615
+ // Merkle set (placement set/2): N files under ONE slot, any N
616
+ // ---------------------------------------------------------------------------
617
+ //
618
+ // set/1 commits the whole member list, which is what caps it: the list rides
619
+ // in the commit body and in every copy of the proof. set/2 commits the ROOT
620
+ // of a Merkle tree over the same rows, so the committed artifact is a few
621
+ // hundred bytes whatever N is, and a member proves its place with a path of
622
+ // ceil(log2 N) siblings. The floor is unchanged: every member's fused bytes
623
+ // still carry the slot's commitment through its own placement. Membership
624
+ // and floor stay inseparable in verifyFuseMember; what changes is where the
625
+ // list lives (with the producer and the reader that serves it) and what a
626
+ // member carries (its row, its index and its path, see SetMemberProof).
627
+ //
628
+ // Leaves are the canonical row bytes (the same {artifact, origin, placement}
629
+ // row set/1 lists), hashed with the RFC 6962 leaf prefix, in the same strict
630
+ // order as a set/1 manifest: ascending by artifact digest, no duplicates.
631
+ // So one root stands for exactly one member list, and a reader holding the
632
+ // list can rebuild the tree; a reader holding one member needs its path.
633
+ export const SET2_PLACEMENT_ID = "set/2";
634
+ /** The proof.metadata key under which a member's own evidence (row, index, count, path) may ride, UNSIGNED, beside the root document. */
635
+ export const SET_MEMBER_METADATA_KEY = `${FUSE_PROFILE}/member`;
636
+ /** The most members one set/2 tree lists. A limit stated plainly, not a design constant: the tree and the paths are fine far beyond it. */
637
+ export const MAX_SET2_MEMBERS = 1_000_000;
638
+ /** The canonical bytes of one row, exactly as a set/1 manifest lists it; the leaf a set/2 tree hashes. */
639
+ export function canonicalSetRow(m) {
640
+ if (m.artifact.length !== 32)
641
+ throw new TypeError("member artifact digest must be 32 bytes");
642
+ if (m.origin.length !== 32)
643
+ throw new TypeError("member origin digest must be 32 bytes");
644
+ if (!PLACEMENT_ID_PATTERN.test(m.placement) || m.placement === SET_PLACEMENT_ID || m.placement === SET2_PLACEMENT_ID)
645
+ throw new TypeError(`member placement "${m.placement}" is not a member placement id`);
646
+ return canonicalize({
647
+ artifact: { algorithm: "sha256", digest: bytesToHex(m.artifact) },
648
+ origin: { algorithm: "sha256", digest: bytesToHex(m.origin) },
649
+ placement: m.placement,
650
+ });
651
+ }
652
+ /** The leaf hash of one row: SHA-256(0x00, canonical row bytes). */
653
+ export function setLeaf(m) {
654
+ return merkleLeafHash(canonicalSetRow(m));
655
+ }
656
+ /**
657
+ * Order members as a set/2 tree lists them: strictly ascending by artifact
658
+ * digest, no duplicate artifact. Throws on a duplicate, an empty list, or a
659
+ * malformed row, like buildSetManifest.
660
+ */
661
+ export function sortSetMembers(members) {
662
+ if (members.length === 0)
663
+ throw new TypeError("a set lists at least one member");
664
+ const sorted = [...members].sort((a, b) => {
665
+ const x = bytesToHex(a.artifact);
666
+ const y = bytesToHex(b.artifact);
667
+ return x < y ? -1 : x > y ? 1 : 0;
668
+ });
669
+ for (let i = 0; i < sorted.length; i++) {
670
+ canonicalSetRow(sorted[i]);
671
+ if (i > 0 && bytesEqual(sorted[i].artifact, sorted[i - 1].artifact))
672
+ throw new TypeError(`duplicate member artifact digest ${bytesToHex(sorted[i].artifact)}`);
673
+ }
674
+ return sorted;
675
+ }
676
+ /** The tree over a member list, in tree order (sortSetMembers): the sorted rows, their leaf hashes, the root, and every member's path on demand. */
677
+ export function buildSetTree(members) {
678
+ const sorted = sortSetMembers(members);
679
+ const leaves = sorted.map(setLeaf);
680
+ const tree = new MerkleTree(leaves);
681
+ return { sorted, leaves, root: tree.root, tree };
682
+ }
683
+ /** The inclusion path of the member at `index` in tree order. */
684
+ export function setMemberPath(tree, index) {
685
+ return tree.path(index);
686
+ }
687
+ /** Build the canonical set/2 root document bytes: the committed artifact. */
688
+ export function buildSetRoot(commitment, count, root) {
689
+ if (commitment.length !== 32)
690
+ throw new TypeError("commitment must be 32 bytes");
691
+ if (root.length !== 32)
692
+ throw new TypeError("root must be 32 bytes");
693
+ if (!Number.isInteger(count) || count < 1 || count > MAX_SET2_MEMBERS)
694
+ throw new TypeError(`count must be an integer from 1 to ${MAX_SET2_MEMBERS}`);
695
+ const doc = {
696
+ count,
697
+ placement: SET2_PLACEMENT_ID,
698
+ root: { algorithm: "sha256", digest: bytesToHex(root) },
699
+ slotCommitment: { algorithm: "sha256", digest: bytesToHex(commitment) },
700
+ type: FUSE_PROFILE,
701
+ };
702
+ return canonicalize(doc);
703
+ }
704
+ /** Strict parse of set/2 root document bytes: exactly the five keys, the literals, 32-byte digests, a count in range, byte-equal to its own rebuild. */
705
+ export function parseSetRoot(bytes) {
706
+ let text;
707
+ try {
708
+ text = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
709
+ }
710
+ catch {
711
+ return null;
712
+ }
713
+ let parsed;
714
+ try {
715
+ parsed = JSON.parse(text);
716
+ }
717
+ catch {
718
+ return null;
719
+ }
720
+ if (!isPlainObject(parsed))
721
+ return null;
722
+ if (Object.keys(parsed).sort().join(",") !== "count,placement,root,slotCommitment,type")
723
+ return null;
724
+ if (parsed["type"] !== FUSE_PROFILE || parsed["placement"] !== SET2_PLACEMENT_ID)
725
+ return null;
726
+ const count = parsed["count"];
727
+ if (typeof count !== "number" || !Number.isInteger(count) || count < 1 || count > MAX_SET2_MEMBERS)
728
+ return null;
729
+ const commitment = readDigestField(parsed["slotCommitment"]);
730
+ const root = readDigestField(parsed["root"]);
731
+ if (commitment === null || root === null)
732
+ return null;
733
+ let rebuilt;
734
+ try {
735
+ rebuilt = buildSetRoot(commitment, count, root);
736
+ }
737
+ catch {
738
+ return null;
739
+ }
740
+ if (!bytesEqual(rebuilt, bytes))
741
+ return null;
742
+ return { commitment, count, root };
743
+ }
744
+ /** Build a member's evidence object (JSON shape) from tree order. */
745
+ export function buildSetMemberProof(member, index, count, path) {
746
+ if (!Number.isInteger(index) || !Number.isInteger(count) || count < 1 || index < 0 || index >= count)
747
+ throw new RangeError("member index out of range");
748
+ canonicalSetRow(member);
749
+ return {
750
+ count,
751
+ index,
752
+ member: {
753
+ artifact: { algorithm: "sha256", digest: bytesToHex(member.artifact) },
754
+ origin: { algorithm: "sha256", digest: bytesToHex(member.origin) },
755
+ placement: member.placement,
756
+ },
757
+ path: path.map((p) => {
758
+ if (p.length !== 32)
759
+ throw new TypeError("a path node is 32 bytes");
760
+ return bytesToHex(p);
761
+ }),
762
+ placement: SET2_PLACEMENT_ID,
763
+ type: FUSE_PROFILE,
764
+ };
765
+ }
766
+ /** Strict read of a member's evidence from a parsed JSON value: shape, literals, 32-byte hex digests and path nodes, index within count. Null on any deviation. UNBOUND: nothing here touches a root. */
767
+ export function parseSetMemberProof(value) {
768
+ if (!isPlainObject(value))
769
+ return null;
770
+ if (Object.keys(value).sort().join(",") !== "count,index,member,path,placement,type")
771
+ return null;
772
+ if (value["type"] !== FUSE_PROFILE || value["placement"] !== SET2_PLACEMENT_ID)
773
+ return null;
774
+ const count = value["count"];
775
+ const index = value["index"];
776
+ if (typeof count !== "number" || !Number.isInteger(count) || count < 1 || count > MAX_SET2_MEMBERS)
777
+ return null;
778
+ if (typeof index !== "number" || !Number.isInteger(index) || index < 0 || index >= count)
779
+ return null;
780
+ const row = value["member"];
781
+ if (!isPlainObject(row) || Object.keys(row).sort().join(",") !== "artifact,origin,placement")
782
+ return null;
783
+ const artifact = readDigestField(row["artifact"]);
784
+ const origin = readDigestField(row["origin"]);
785
+ const placement = row["placement"];
786
+ if (artifact === null || origin === null || typeof placement !== "string")
787
+ return null;
788
+ if (!PLACEMENT_ID_PATTERN.test(placement) || placement === SET_PLACEMENT_ID || placement === SET2_PLACEMENT_ID)
789
+ return null;
790
+ const list = value["path"];
791
+ if (!Array.isArray(list))
792
+ return null;
793
+ const path = [];
794
+ for (const node of list) {
795
+ if (typeof node !== "string" || !/^[0-9a-f]{64}$/.test(node))
796
+ return null;
797
+ const bytes = hexToBytes(node);
798
+ if (bytes === null)
799
+ return null;
800
+ path.push(bytes);
801
+ }
802
+ return { member: { artifact, origin, placement }, index, count, path };
803
+ }
804
+ /** Recompute a root from a member's leaf and path; null when the path does not fit. */
805
+ export function setRootFromMember(member, index, count, path) {
806
+ return merkleRootFromPath(setLeaf(member), index, count, path);
807
+ }
808
+ const set2 = {
809
+ id: SET2_PLACEMENT_ID,
810
+ form: "C",
811
+ byteExact: false,
812
+ build() {
813
+ throw new TypeError("set/2 is built with buildSetRoot(commitment, count, root)");
814
+ },
815
+ locate(fused) {
816
+ const doc = parseSetRoot(fused);
817
+ return doc === null ? null : { commitment: doc.commitment };
818
+ },
819
+ };
546
820
  /**
547
- * Resolve a placement by id. set/1 resolves here but is NOT in PLACEMENTS:
821
+ * Resolve a placement by id. set/1 and set/2 resolve here but are NOT in PLACEMENTS:
548
822
  * the undeclared scan is for bytes whose placement was not declared, whereas
549
823
  * a set manifest is identified by hashing to the signed artifact digest and
550
824
  * by its signed title, so the scan order of every existing fixture is
551
825
  * literally unchanged.
552
826
  */
553
827
  export function getPlacement(id) {
554
- return [...PLACEMENTS, set1].find((p) => p.id === id);
828
+ return [...PLACEMENTS, set1, set2].find((p) => p.id === id);
555
829
  }
556
830
  // ---------------------------------------------------------------------------
557
831
  // Attribution (the signed carrier of placement and origin, spec 6.5)
@@ -565,8 +839,8 @@ export function getPlacement(id) {
565
839
  export function fuseAttribution(placement, originDigest) {
566
840
  if (originDigest !== undefined && originDigest.length !== 32)
567
841
  throw new TypeError("originDigest must be 32 bytes");
568
- if (placement === SET_PLACEMENT_ID && originDigest !== undefined)
569
- throw new TypeError("set/1 has no single origin; a set marker carries no origin digest");
842
+ if ((placement === SET_PLACEMENT_ID || placement === SET2_PLACEMENT_ID) && originDigest !== undefined)
843
+ throw new TypeError(`${placement} has no single origin; a set marker carries no origin digest`);
570
844
  return {
571
845
  name: FUSE_ATTRIBUTION_NAME,
572
846
  title: placement,