@mikeargento/bitgraph 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/__tests__/fuse-placement.test.js +10 -10
- package/dist/__tests__/fuse-roundtrip.test.js +71 -2
- package/dist/__tests__/fuse-roundtrip.test.js.map +1 -1
- package/dist/__tests__/fuse-set-sdk.test.js +105 -2
- package/dist/__tests__/fuse-set-sdk.test.js.map +1 -1
- package/dist/__tests__/fuse-set.test.js +1 -1
- package/dist/__tests__/fuse-set.test.js.map +1 -1
- package/dist/fuse-cli.js +2 -2
- package/dist/fuse-cli.js.map +1 -1
- package/dist/fuse.d.ts +70 -13
- package/dist/fuse.d.ts.map +1 -1
- package/dist/fuse.js +127 -46
- package/dist/fuse.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/fuse-placement.test.ts +10 -10
- package/src/__tests__/fuse-roundtrip.test.ts +71 -2
- package/src/__tests__/fuse-set-sdk.test.ts +117 -9
- package/src/__tests__/fuse-set.test.ts +1 -1
- package/src/fuse-cli.ts +2 -2
- package/src/fuse.ts +186 -54
- package/src/index.ts +2 -2
|
@@ -32,8 +32,8 @@ import {
|
|
|
32
32
|
import type { BitGraphProof, SlotAllocation, Attribution, SetMember, SetManifest } from "@mikeargento/bitgraph-verify";
|
|
33
33
|
import { makeKey, signBody, b64, utf8 } from "./audit-fixtures.js";
|
|
34
34
|
import type { ManualKey } from "./audit-fixtures.js";
|
|
35
|
-
import { fuse, fuseSet, builderFor, placementForBytes, fusedNamesFor, FuseError, MAX_SET_MEMBERS, digest } from "../fuse.js";
|
|
36
|
-
import type { FuseSetProgress } from "../fuse.js";
|
|
35
|
+
import { fuse, fuseSet, builderFor, placementForBytes, fusedNamesFor, FuseError, MAX_SET_MEMBERS, digest, trailerBytesFor } from "../fuse.js";
|
|
36
|
+
import type { FuseSetProgress, FuseSetBytesMember, FuseSetLoadedMember, FuseSetHashedMember, FusedDigestInput } from "../fuse.js";
|
|
37
37
|
import type { FuseSetMember } from "../fuse.js";
|
|
38
38
|
|
|
39
39
|
const FIX = fileURLToPath(new URL("../../src/__tests__/fuse-fixtures/", import.meta.url));
|
|
@@ -48,7 +48,7 @@ const unrelated = utf8("a file nobody recorded\n");
|
|
|
48
48
|
// The phase-1 fixtures fuse original.txt as trailer/1 and image.png as
|
|
49
49
|
// container/1, so these members declare those placements; the by-bytes
|
|
50
50
|
// default (placementForBytes) is asserted on its own in test 3.
|
|
51
|
-
const two = ():
|
|
51
|
+
const two = (): FuseSetBytesMember[] => [{ original, placement: "trailer/1" }, { original: png, placement: "container/1" }];
|
|
52
52
|
|
|
53
53
|
// ---------------------------------------------------------------------------
|
|
54
54
|
// Local signer: slots and proofs minted from a received commit body
|
|
@@ -160,9 +160,9 @@ const toUrlSafe = (s: string) => s.replace(/\+/g, "-").replace(/\//g, "_").repla
|
|
|
160
160
|
interface Oracle { commitment: Uint8Array; fused: Uint8Array[]; rows: SetMember[]; manifest: Uint8Array; digestB64: string; sorted: SetMember[] }
|
|
161
161
|
|
|
162
162
|
/** The commitment from the slot, each member built by its placement, the canonical manifest and its digest. */
|
|
163
|
-
function oracle(slot: SlotAllocation, members: readonly
|
|
163
|
+
function oracle(slot: SlotAllocation, members: readonly FuseSetBytesMember[]): Oracle {
|
|
164
164
|
const commitment = computeSlotCommitment(slot);
|
|
165
|
-
const placementOf = (m:
|
|
165
|
+
const placementOf = (m: FuseSetBytesMember) => m.placement ?? placementForBytes(m.original);
|
|
166
166
|
const fused = members.map((m) => getPlacement(placementOf(m))!.build({ original: m.original, commitment }));
|
|
167
167
|
const rows = members.map((m, i) => ({ artifact: sha256(fused[i]!), origin: sha256(m.original), placement: placementOf(m) }));
|
|
168
168
|
const manifest = buildSetManifest(commitment, rows);
|
|
@@ -259,7 +259,7 @@ describe("fuseSet(): one slot, N files, the manifest as the artifact", () => {
|
|
|
259
259
|
// The by-bytes default: PNG is trailer-safe, plain text goes into a container.
|
|
260
260
|
const d = await fuseSet([{ original: png }, { original }], { transport: honest(key, slot).transport });
|
|
261
261
|
assert.deepEqual(d.members.map((m) => m.placement), [placementForBytes(png), placementForBytes(original)]);
|
|
262
|
-
assert.deepEqual(d.members.map((m) => m.placement), ["trailer/1", "container/
|
|
262
|
+
assert.deepEqual(d.members.map((m) => m.placement), ["trailer/1", "container/2"]);
|
|
263
263
|
});
|
|
264
264
|
|
|
265
265
|
test("4. each original rebuilds its member (SET_MEMBER_FROM_ORIGIN); with the echo stripped and no explicit bytes nothing is bound", async () => {
|
|
@@ -336,7 +336,7 @@ describe("fuseSet(): a bad member burns the slot and commits nothing", () => {
|
|
|
336
336
|
test("9. bytes that carry the commitment but embed another member's origin are builder-failed naming the member and the origin", async () => {
|
|
337
337
|
const { calls, transport } = honest(key, slot);
|
|
338
338
|
// Two trailer/1 members; member 1's builder hands back member 0's fused bytes, which carry c but embed member 0's original.
|
|
339
|
-
const members:
|
|
339
|
+
const members: FuseSetBytesMember[] = [{ original, placement: "trailer/1" }, { original: note, placement: "trailer/1" }];
|
|
340
340
|
members[1]!.builder = ({ commitment }) => getPlacement("trailer/1")!.build({ original, commitment });
|
|
341
341
|
await assert.rejects(fuseSet(members, { transport }), (e: FuseError) => e.code === "builder-failed" && /member 1/.test(e.message) && /origin/.test(e.message) && e.member === 1);
|
|
342
342
|
assert.equal(commits(calls).length, 0);
|
|
@@ -345,14 +345,14 @@ describe("fuseSet(): a bad member burns the slot and commits nothing", () => {
|
|
|
345
345
|
|
|
346
346
|
test("9b. container/1: bytes that declare the member's origin digest but carry other bytes are builder-failed naming the member; no commit", async () => {
|
|
347
347
|
const { calls, transport } = honest(key, slot);
|
|
348
|
-
const members:
|
|
348
|
+
const members: FuseSetBytesMember[] = [{ original, placement: "trailer/1" }, { original: png, placement: "container/1" }];
|
|
349
349
|
// The payload names png's digest (so the declared origin matches) while the tar carries other bytes.
|
|
350
350
|
members[1]!.builder = ({ commitment, originDigest }) => getPlacement("container/1")!.build({ original: utf8("not the member's original\n"), originDigest: originDigest!, commitment });
|
|
351
351
|
await assert.rejects(fuseSet(members, { transport }), (e: FuseError) => e.code === "builder-failed" && /^member 1/.test(e.message) && /origin/.test(e.message) && e.member === 1);
|
|
352
352
|
assert.equal(commits(calls).length, 0, "nothing was committed");
|
|
353
353
|
assert.equal(allocates(calls).length, 1);
|
|
354
354
|
// The mirror: a payload declaring another digest over the member's own bytes is refused the same way.
|
|
355
|
-
const declared:
|
|
355
|
+
const declared: FuseSetBytesMember[] = [{ original, placement: "trailer/1" }, { original: png, placement: "container/1" }];
|
|
356
356
|
declared[1]!.builder = ({ commitment }) => getPlacement("container/1")!.build({ original: png, originDigest: sha256(unrelated), commitment });
|
|
357
357
|
const b = honest(key, slot);
|
|
358
358
|
await assert.rejects(fuseSet(declared, { transport: b.transport }), (e: FuseError) => e.code === "builder-failed" && e.member === 1);
|
|
@@ -698,3 +698,111 @@ describe("fuseSet(): hashing and progress", () => {
|
|
|
698
698
|
assert.equal(calls, 8, "every report was attempted");
|
|
699
699
|
});
|
|
700
700
|
});
|
|
701
|
+
|
|
702
|
+
describe("fuseSet(): loaded and hashed members", () => {
|
|
703
|
+
/** The trailer/1 member finished from a hasher state, as a scanner does: hash the original once, keep the state, add the trailer later. */
|
|
704
|
+
const savedState = (bytes: Uint8Array) => { const h = sha256.create(); h.update(bytes); return h; };
|
|
705
|
+
const finish = (h: ReturnType<typeof sha256.create>, trailer: Uint8Array) => { const c = h.clone(); c.update(trailer); return c.digest(); };
|
|
706
|
+
|
|
707
|
+
test("30. trailerBytesFor is the placement's own suffix: original followed by it hashes to the trailer/1 build, and a saved state finished with it agrees", () => {
|
|
708
|
+
const commitment = computeSlotCommitment(slot);
|
|
709
|
+
const t = trailerBytesFor(commitment);
|
|
710
|
+
assert.equal(t.length, 48);
|
|
711
|
+
assert.deepEqual(t.subarray(0, 8), utf8("BGFUSE01"));
|
|
712
|
+
assert.deepEqual(t.subarray(8, 16), new Uint8Array(8));
|
|
713
|
+
assert.deepEqual(t.subarray(16), commitment);
|
|
714
|
+
for (const o of [original, png, note, new Uint8Array(0), new Uint8Array(55).fill(1), new Uint8Array(64).fill(2), new Uint8Array(1_000_003).fill(3)]) {
|
|
715
|
+
const built = getPlacement("trailer/1")!.build({ original: o, commitment });
|
|
716
|
+
assert.deepEqual(built.subarray(o.length), t, `suffix for ${o.length} bytes`);
|
|
717
|
+
assert.deepEqual(finish(savedState(o), t), sha256(built), `finished state for ${o.length} bytes`);
|
|
718
|
+
}
|
|
719
|
+
assert.throws(() => trailerBytesFor(new Uint8Array(31)), (e: FuseError) => e.code === "bad-input");
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
test("31. a loaded member is read once, after the slot is held, checked against its named digest, and released unless kept", async () => {
|
|
723
|
+
const { calls, transport } = honest(key, slot);
|
|
724
|
+
const order: string[] = [];
|
|
725
|
+
const wrapped = { ...transport, fetch: async (url: string, init?: RequestInit) => { order.push(new URL(url).pathname); return (transport.fetch as (u: string, i?: RequestInit) => Promise<Response>)(url, init); } } as unknown as typeof transport;
|
|
726
|
+
let loads = 0;
|
|
727
|
+
const loaded: FuseSetLoadedMember = { load: async () => { loads++; order.push("load"); return original; }, originDigest: sha256(original), placement: "trailer/1", name: "photo.jpg" };
|
|
728
|
+
const r = await fuseSet([loaded, { original: png, placement: "container/1" }], { transport: wrapped });
|
|
729
|
+
assert.equal(loads, 1);
|
|
730
|
+
assert.ok(order.indexOf("load") > order.indexOf("/api/fuse/allocate"), "read after the slot is held");
|
|
731
|
+
assert.ok(order.indexOf("load") < order.indexOf("/api/fuse/commit"), "read before the commit");
|
|
732
|
+
const o = oracle(slot, [{ original, placement: "trailer/1" }, { original: png, placement: "container/1" }]);
|
|
733
|
+
assert.equal(r.members[0]!.artifactDigestB64, bytesToBase64(o.rows[0]!.artifact));
|
|
734
|
+
assert.equal(r.members[0]!.originDigestB64, bytesToBase64(sha256(original)));
|
|
735
|
+
assert.equal(r.members[0]!.fusedName, fusedNamesFor("photo.jpg", "trailer/1").fusedName);
|
|
736
|
+
assert.ok(!("fusedBytes" in r.members[0]!));
|
|
737
|
+
assert.equal(commits(calls).length, 1);
|
|
738
|
+
const kept = await fuseSet([{ ...loaded }], { transport: honest(key, slot).transport, keepFused: true });
|
|
739
|
+
assert.deepEqual(kept.members[0]!.fusedBytes, getPlacement("trailer/1")!.build({ original, commitment: computeSlotCommitment(slot) }));
|
|
740
|
+
// A wrong named digest burns the slot and commits nothing.
|
|
741
|
+
const bad = honest(key, slot);
|
|
742
|
+
await assert.rejects(fuseSet([{ load: () => original, originDigest: sha256(unrelated), placement: "trailer/1" }], { transport: bad.transport }), (e: FuseError) => e.code === "bad-input" && /^member 0: originDigest is not the SHA-256 of the loaded bytes/.test(e.message) && e.member === 0);
|
|
743
|
+
assert.equal(commits(bad.calls).length, 0);
|
|
744
|
+
assert.equal(allocates(bad.calls).length, 1);
|
|
745
|
+
// A loader that throws, or returns something other than bytes, is load-failed naming the member.
|
|
746
|
+
const thrown = honest(key, slot);
|
|
747
|
+
await assert.rejects(fuseSet([{ load: () => { throw new Error("gone"); }, originDigest: sha256(original), placement: "trailer/1" }], { transport: thrown.transport }), (e: FuseError) => e.code === "load-failed" && /^member 0: load threw: gone/.test(e.message) && e.member === 0);
|
|
748
|
+
assert.equal(commits(thrown.calls).length, 0);
|
|
749
|
+
await assert.rejects(fuseSet([{ original, placement: "trailer/1" }, { load: (() => "text") as never, originDigest: sha256(original), placement: "container/1" }], { transport: honest(key, slot).transport }), (e: FuseError) => e.code === "load-failed" && /^member 1: load must return a Uint8Array/.test(e.message) && e.member === 1);
|
|
750
|
+
// Without a placement or a digest nothing is requested.
|
|
751
|
+
const quiet = honest(key, slot);
|
|
752
|
+
await assert.rejects(fuseSet([{ load: () => original, originDigest: sha256(original) } as never], { transport: quiet.transport }), (e: FuseError) => e.code === "bad-input" && /^member 0: a loaded member names its placement/.test(e.message));
|
|
753
|
+
await assert.rejects(fuseSet([{ load: () => original, placement: "trailer/1" } as never], { transport: quiet.transport }), (e: FuseError) => e.code === "bad-input" && /^member 0: a loaded member names its originDigest/.test(e.message));
|
|
754
|
+
assert.equal(quiet.calls.length, 0);
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
test("32. a hashed member answers its fused digest for the held commitment and never shows its bytes; the row is what the real verifier finds", async () => {
|
|
758
|
+
const { calls, transport } = honest(key, slot);
|
|
759
|
+
const inputs: FusedDigestInput[] = [];
|
|
760
|
+
const state = savedState(original);
|
|
761
|
+
const hashed: FuseSetHashedMember = {
|
|
762
|
+
originDigest: sha256(original),
|
|
763
|
+
placement: "trailer/1",
|
|
764
|
+
name: "photo.jpg",
|
|
765
|
+
fusedDigest: (input) => { inputs.push(input); return finish(state, trailerBytesFor(input.commitment)); },
|
|
766
|
+
};
|
|
767
|
+
const r = await fuseSet([hashed, { original: png, placement: "container/1" }, { load: () => note, originDigest: sha256(note), placement: "container/1" }], { transport, keepFused: true });
|
|
768
|
+
assert.equal(inputs.length, 1);
|
|
769
|
+
const commitment = computeSlotCommitment(slot);
|
|
770
|
+
assert.deepEqual(inputs[0]!.commitment, commitment);
|
|
771
|
+
assert.equal(inputs[0]!.commitmentHex, bytesToHex(commitment));
|
|
772
|
+
assert.equal(inputs[0]!.slot.nonceB64, slot.nonceB64);
|
|
773
|
+
const built = getPlacement("trailer/1")!.build({ original, commitment });
|
|
774
|
+
assert.equal(r.members[0]!.artifactDigestB64, bytesToBase64(sha256(built)), "the row is the hash of the placement's build");
|
|
775
|
+
assert.ok(!("fusedBytes" in r.members[0]!), "nothing to keep for a hashed member");
|
|
776
|
+
assert.ok("fusedBytes" in r.members[1]! && "fusedBytes" in r.members[2]!, "bytes and loaded members are kept");
|
|
777
|
+
assert.equal(r.members.length, 3);
|
|
778
|
+
assert.equal(r.manifest.members.length, 3);
|
|
779
|
+
assert.equal(commits(calls).length, 1);
|
|
780
|
+
// The real verifier reads the member from the bytes a reader would build, and from its original.
|
|
781
|
+
const direct = await verifyFuseMember({ proof: r.proof, bytes: built, manifest: r.manifestBytes });
|
|
782
|
+
assert.equal(direct.category, "SET_MEMBER_DIRECT", direct.reason ?? "");
|
|
783
|
+
assert.equal(direct.set!.member!.index, r.members[0]!.manifestIndex);
|
|
784
|
+
const fromOrigin = await verifyFuseMember({ proof: r.proof, bytes: original, manifest: r.manifestBytes });
|
|
785
|
+
assert.equal(fromOrigin.category, "SET_MEMBER_FROM_ORIGIN", fromOrigin.reason ?? "");
|
|
786
|
+
// verifyMembers refuses a hashed member before any request; a throwing or short answer is builder-failed naming the member.
|
|
787
|
+
const quiet = honest(key, slot);
|
|
788
|
+
await assert.rejects(fuseSet([hashed], { transport: quiet.transport, verifyMembers: true }), (e: FuseError) => e.code === "bad-input" && /^member 0: a hashed member cannot be verified in full/.test(e.message) && e.member === 0);
|
|
789
|
+
assert.equal(quiet.calls.length, 0);
|
|
790
|
+
const throwing = honest(key, slot);
|
|
791
|
+
await assert.rejects(fuseSet([{ ...hashed, fusedDigest: () => { throw new Error("no state"); } }], { transport: throwing.transport }), (e: FuseError) => e.code === "builder-failed" && /^member 0: fusedDigest threw: no state/.test(e.message) && e.member === 0);
|
|
792
|
+
assert.equal(commits(throwing.calls).length, 0);
|
|
793
|
+
await assert.rejects(fuseSet([{ ...hashed, fusedDigest: () => new Uint8Array(31) }], { transport: honest(key, slot).transport }), (e: FuseError) => e.code === "builder-failed" && /^member 0: fusedDigest must return a 32-byte digest/.test(e.message));
|
|
794
|
+
// The same original as a hashed member and as a bytes member under one placement is the duplicate it always was.
|
|
795
|
+
await assert.rejects(fuseSet([hashed, { original, placement: "trailer/1" }], { transport: honest(key, slot).transport }), (e: FuseError) => e.code === "bad-input" && /^members 0 and 1 are the same original/.test(e.message));
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
test("33. progress counts every shape: hash before the slot for all three, fuse after it, one commit", async () => {
|
|
799
|
+
const seen: FuseSetProgress[] = [];
|
|
800
|
+
const state = savedState(original);
|
|
801
|
+
await fuseSet([
|
|
802
|
+
{ originDigest: sha256(original), placement: "trailer/1", fusedDigest: ({ commitment }) => finish(state, trailerBytesFor(commitment)) },
|
|
803
|
+
{ load: () => note, originDigest: sha256(note), placement: "container/1" },
|
|
804
|
+
{ original: png, placement: "container/1" },
|
|
805
|
+
], { transport: honest(key, slot).transport, onProgress: (p) => seen.push({ ...p }) });
|
|
806
|
+
assert.deepEqual(seen.map((p) => `${p.phase} ${p.done}/${p.total}`), ["hash 1/3", "hash 2/3", "hash 3/3", "fuse 1/3", "fuse 2/3", "fuse 3/3", "commit 0/1", "commit 1/1"]);
|
|
807
|
+
});
|
|
808
|
+
});
|
|
@@ -352,7 +352,7 @@ describe("codec: parseSetManifest", () => {
|
|
|
352
352
|
|
|
353
353
|
describe("registry and metadata", () => {
|
|
354
354
|
test("13. PLACEMENTS is unchanged; set/1 resolves through getPlacement as Form C, not byte-exact, locate-only", () => {
|
|
355
|
-
assert.deepEqual(PLACEMENTS.map((p) => p.id), ["trailer/1", "container/1", "produced/1"]);
|
|
355
|
+
assert.deepEqual(PLACEMENTS.map((p) => p.id), ["trailer/1", "container/1", "container/2", "produced/1"]);
|
|
356
356
|
const set1 = getPlacement("set/1")!;
|
|
357
357
|
assert.ok(set1);
|
|
358
358
|
assert.equal(set1.id, SET_PLACEMENT_ID);
|
package/src/fuse-cli.ts
CHANGED
|
@@ -28,7 +28,7 @@ import type { FuseTransport } from "./fuse.js";
|
|
|
28
28
|
|
|
29
29
|
const USAGE = `bitgraph-fuse: BitGraph producer harness (profile bitgraph-fuse/1, working name)
|
|
30
30
|
|
|
31
|
-
bitgraph-fuse fuse <file> --placement trailer/1|container/1 [options]
|
|
31
|
+
bitgraph-fuse fuse <file> --placement trailer/1|container/1|container/2 [options]
|
|
32
32
|
Form A or B over an existing file. The file is never modified.
|
|
33
33
|
bitgraph-fuse produce [--origin <file>] [options]
|
|
34
34
|
Form C: a canonical payload naming an optional source.
|
|
@@ -104,7 +104,7 @@ async function writeOutputs(outDir: string, label: string, frame: unknown, fused
|
|
|
104
104
|
async function runFuse(args: Args): Promise<number> {
|
|
105
105
|
const file = args.positional[0];
|
|
106
106
|
const placement = args.flags.get("placement");
|
|
107
|
-
if (file === undefined || (placement !== "trailer/1" && placement !== "container/1")) { process.stderr.write(USAGE); return 64; }
|
|
107
|
+
if (file === undefined || (placement !== "trailer/1" && placement !== "container/1" && placement !== "container/2")) { process.stderr.write(USAGE); return 64; }
|
|
108
108
|
const original = new Uint8Array(await readFile(resolve(file)));
|
|
109
109
|
const label = sanitize(basename(file));
|
|
110
110
|
const ext = placement === "trailer/1" ? extname(label) : ".tar";
|
package/src/fuse.ts
CHANGED
|
@@ -49,6 +49,8 @@ import {
|
|
|
49
49
|
parseSetManifest,
|
|
50
50
|
readSetMetadata,
|
|
51
51
|
SET_METADATA_KEY,
|
|
52
|
+
TRAILER_LENGTH,
|
|
53
|
+
TRAILER_MAGIC,
|
|
52
54
|
verifyFuse,
|
|
53
55
|
verifyFuseMember,
|
|
54
56
|
base64ToBytes,
|
|
@@ -153,6 +155,7 @@ export type FuseErrorCode =
|
|
|
153
155
|
| "bad-input"
|
|
154
156
|
| "allocate-failed"
|
|
155
157
|
| "builder-failed"
|
|
158
|
+
| "load-failed"
|
|
156
159
|
| "commitment-missing"
|
|
157
160
|
| "commit-refused"
|
|
158
161
|
| "slot-unavailable"
|
|
@@ -183,12 +186,14 @@ export class FuseError extends Error {
|
|
|
183
186
|
* safe only where decoders stop at an end marker or read by declared sizes:
|
|
184
187
|
* JPEG (EOI), PNG (IEND), GIF (0x3B), TIFF and the TIFF-based raws such as
|
|
185
188
|
* DNG, CR2, NEF, ARW (offset tables), BMP and RIFF containers such as WebP,
|
|
186
|
-
* WAV, AVI (declared sizes). Everything else goes into `container/
|
|
187
|
-
* that carries the original untouched
|
|
189
|
+
* WAV, AVI (declared sizes). Everything else goes into `container/2`, a tar
|
|
190
|
+
* that carries the original untouched and FIRST, so a scanner can hash it
|
|
191
|
+
* once and finish the fused digest later: PDF, ZIP-based documents, ISO base
|
|
188
192
|
* media video and images, Matroska, MP3, structured and plain text, and any
|
|
189
|
-
* format not recognised here.
|
|
193
|
+
* format not recognised here. Artifacts made under `container/1` (the
|
|
194
|
+
* manifest first) stay readable; nothing new is made under it.
|
|
190
195
|
*/
|
|
191
|
-
export function placementForBytes(bytes: Uint8Array): "trailer/1" | "container/
|
|
196
|
+
export function placementForBytes(bytes: Uint8Array): "trailer/1" | "container/2" {
|
|
192
197
|
const at = (sig: number[], offset = 0): boolean => bytes.length >= offset + sig.length && sig.every((v, i) => bytes[offset + i] === v);
|
|
193
198
|
const trailerSafe =
|
|
194
199
|
at([0xff, 0xd8, 0xff]) || // JPEG
|
|
@@ -197,7 +202,7 @@ export function placementForBytes(bytes: Uint8Array): "trailer/1" | "container/1
|
|
|
197
202
|
at([0x49, 0x49, 0x2a, 0x00]) || at([0x4d, 0x4d, 0x00, 0x2a]) || // TIFF, DNG, CR2, NEF, ARW
|
|
198
203
|
(at([0x42, 0x4d]) && bytes.length >= 14) || // BMP
|
|
199
204
|
(at([0x52, 0x49, 0x46, 0x46]) && bytes.length >= 12); // RIFF: WebP, WAV, AVI
|
|
200
|
-
return trailerSafe ? "trailer/1" : "container/
|
|
205
|
+
return trailerSafe ? "trailer/1" : "container/2";
|
|
201
206
|
}
|
|
202
207
|
|
|
203
208
|
/** Names for a fused artifact and its Frame, from the original's name. */
|
|
@@ -206,7 +211,7 @@ export function fusedNamesFor(originalName: string, placement: PlacementId): { f
|
|
|
206
211
|
const stem = dot > 0 ? originalName.slice(0, dot) : originalName;
|
|
207
212
|
const ext = dot > 0 ? originalName.slice(dot) : "";
|
|
208
213
|
return {
|
|
209
|
-
fusedName: placement === "trailer/1" ? `${stem}.fused${ext}` : placement
|
|
214
|
+
fusedName: placement === "trailer/1" ? `${stem}.fused${ext}` : placement.startsWith("container/") ? `${stem}.fused.tar` : `${stem}.produced.json`,
|
|
210
215
|
frameName: `${originalName}.bitgraph-fuse.json`,
|
|
211
216
|
};
|
|
212
217
|
}
|
|
@@ -476,9 +481,20 @@ export async function fuse(builder: FuseBuilder, options: FuseOptions): Promise<
|
|
|
476
481
|
export const MAX_SET_MEMBERS = 2000;
|
|
477
482
|
|
|
478
483
|
/** The placements a set member takes: Forms A and B, one original per member. */
|
|
479
|
-
export type SetMemberPlacement = "trailer/1" | "container/1";
|
|
484
|
+
export type SetMemberPlacement = "trailer/1" | "container/1" | "container/2";
|
|
480
485
|
|
|
481
|
-
|
|
486
|
+
/** What a hashed member's fused digest is computed for: the held slot and its commitment. */
|
|
487
|
+
export interface FusedDigestInput {
|
|
488
|
+
commitment: Uint8Array;
|
|
489
|
+
commitmentHex: string;
|
|
490
|
+
slot: SlotAllocation;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* A member given as bytes. The core hashes the original, builds the fused
|
|
495
|
+
* bytes under the slot's commitment, checks them, and hashes them.
|
|
496
|
+
*/
|
|
497
|
+
export interface FuseSetBytesMember {
|
|
482
498
|
/** The original bytes. Never modified. */
|
|
483
499
|
original: Uint8Array;
|
|
484
500
|
/** Default: placementForBytes(original). */
|
|
@@ -489,10 +505,62 @@ export interface FuseSetMember {
|
|
|
489
505
|
builder?: FuseBuilder;
|
|
490
506
|
}
|
|
491
507
|
|
|
508
|
+
/**
|
|
509
|
+
* A member whose bytes are read only when it is that member's turn, after
|
|
510
|
+
* the slot is held, and released once hashed: one member's bytes in memory
|
|
511
|
+
* at a time, however large the set. Nothing can be read before allocation
|
|
512
|
+
* without reading twice, so the caller names the placement and the origin
|
|
513
|
+
* digest up front; the digest is checked against the loaded bytes, and the
|
|
514
|
+
* byte guards run as for a bytes member.
|
|
515
|
+
*/
|
|
516
|
+
export interface FuseSetLoadedMember {
|
|
517
|
+
load: () => Promise<Uint8Array> | Uint8Array;
|
|
518
|
+
originDigest: Uint8Array;
|
|
519
|
+
placement: SetMemberPlacement;
|
|
520
|
+
/** Advisory; feeds fusedNamesFor. */
|
|
521
|
+
name?: string;
|
|
522
|
+
/** Default: builderFor(placement, bytes). The locate and origin guards run regardless. */
|
|
523
|
+
builder?: FuseBuilder;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* A member the caller hashes itself: it answers the fused digest for the
|
|
528
|
+
* held slot's commitment. For trailer/1 that is a hash state saved after
|
|
529
|
+
* the original and finished with trailerBytesFor(commitment), so the bytes
|
|
530
|
+
* are read once, when they are scanned, and never again. The core never
|
|
531
|
+
* sees this member's bytes: no byte guard runs, keepFused returns nothing
|
|
532
|
+
* for it, and verifyMembers refuses it before any request. Its row is bound
|
|
533
|
+
* to the committed manifest by digest like every other.
|
|
534
|
+
*/
|
|
535
|
+
export interface FuseSetHashedMember {
|
|
536
|
+
originDigest: Uint8Array;
|
|
537
|
+
placement: SetMemberPlacement;
|
|
538
|
+
fusedDigest: (input: FusedDigestInput) => Promise<Uint8Array> | Uint8Array;
|
|
539
|
+
/** Advisory; feeds fusedNamesFor. */
|
|
540
|
+
name?: string;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
export type FuseSetMember = FuseSetBytesMember | FuseSetLoadedMember | FuseSetHashedMember;
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* The 48 bytes trailer/1 appends after the original: the magic, eight
|
|
547
|
+
* reserved zero bytes, the commitment. A hasher whose state was saved after
|
|
548
|
+
* the original finishes with these and holds the member's fused digest
|
|
549
|
+
* without reading the original again. A test pins them against the
|
|
550
|
+
* placement's own build.
|
|
551
|
+
*/
|
|
552
|
+
export function trailerBytesFor(commitment: Uint8Array): Uint8Array {
|
|
553
|
+
if (!(commitment instanceof Uint8Array) || commitment.length !== 32) throw new FuseError("bad-input", "a slot commitment is 32 bytes");
|
|
554
|
+
const out = new Uint8Array(TRAILER_LENGTH);
|
|
555
|
+
out.set(new TextEncoder().encode(TRAILER_MAGIC), 0);
|
|
556
|
+
out.set(commitment, TRAILER_LENGTH - 32);
|
|
557
|
+
return out;
|
|
558
|
+
}
|
|
559
|
+
|
|
492
560
|
export interface FuseSetProgress {
|
|
493
561
|
/**
|
|
494
|
-
* "hash":
|
|
495
|
-
* "fuse": each member
|
|
562
|
+
* "hash": each member checked before any request (a bytes member's origin digest is taken here).
|
|
563
|
+
* "fuse": each member's fused digest taken, after the slot is held.
|
|
496
564
|
* "commit": 0 of 1 before the request, 1 of 1 when the proof is back.
|
|
497
565
|
* "verify": only with verifyMembers, one per member.
|
|
498
566
|
*/
|
|
@@ -502,7 +570,7 @@ export interface FuseSetProgress {
|
|
|
502
570
|
}
|
|
503
571
|
|
|
504
572
|
export interface FuseSetOptions {
|
|
505
|
-
/** Return each member's fused bytes. Default false: they are virtual, rebuilt from the original and the proof. */
|
|
573
|
+
/** Return each member's fused bytes. Default false: they are virtual, rebuilt from the original and the proof. A hashed member has none to return. */
|
|
506
574
|
keepFused?: boolean;
|
|
507
575
|
/**
|
|
508
576
|
* Run the full verifier (verifyFuseMember) over every member's fused bytes
|
|
@@ -510,7 +578,8 @@ export interface FuseSetOptions {
|
|
|
510
578
|
* false: every member is bound to the returned proof by digest, its row in
|
|
511
579
|
* the committed manifest, which is itself verified FUSED_DIRECT; that is
|
|
512
580
|
* linear and reads no bytes. The full pass re-hashes every member with the
|
|
513
|
-
* verifier's own hasher and grows with the square of the member count.
|
|
581
|
+
* verifier's own hasher and grows with the square of the member count. A
|
|
582
|
+
* set with a hashed member refuses it before any request.
|
|
514
583
|
*/
|
|
515
584
|
verifyMembers?: boolean;
|
|
516
585
|
/** Called as the set advances. A throw inside it is ignored: a progress hook never changes the outcome. */
|
|
@@ -533,7 +602,7 @@ export interface FuseSetMemberResult {
|
|
|
533
602
|
fusedName: string | null;
|
|
534
603
|
/** Advisory; no Frame is written for a set member this phase. */
|
|
535
604
|
frameName: string | null;
|
|
536
|
-
/** Present only when keepFused is true. */
|
|
605
|
+
/** Present only when keepFused is true and the member's bytes passed through the core (never for a hashed member). */
|
|
537
606
|
fusedBytes?: Uint8Array;
|
|
538
607
|
/** Present only with verifyMembers: the verifier's own verdict against this member's fused bytes. Always SET_MEMBER_DIRECT on success, with set.manifestSource "argument". */
|
|
539
608
|
verification?: FuseMemberResult;
|
|
@@ -562,13 +631,28 @@ export interface FuseSetResult {
|
|
|
562
631
|
* Allocate once, fuse every member with the one commitment, hash the set
|
|
563
632
|
* manifest, fill the slot with it. Returns the proof with the manifest bytes
|
|
564
633
|
* beside it, or throws a FuseError; it never commits a partial set and never
|
|
565
|
-
* allocates a second slot.
|
|
634
|
+
* allocates a second slot. Members may be given as bytes, as a loader read
|
|
635
|
+
* one at a time after the slot is held, or as a digest the caller finishes
|
|
636
|
+
* itself; one set may mix them.
|
|
566
637
|
*/
|
|
567
638
|
export async function fuseSet(members: readonly FuseSetMember[], options: FuseSetOptions = {}): Promise<FuseSetResult> {
|
|
568
639
|
// 0. validate, before any request. A refusal here burns nothing.
|
|
569
640
|
if (!Array.isArray(members) || members.length === 0) throw new FuseError("bad-input", "a set lists at least one member");
|
|
570
641
|
if (members.length > MAX_SET_MEMBERS) throw new FuseError("bad-input", `a set lists at most ${MAX_SET_MEMBERS} members (got ${members.length})`);
|
|
571
|
-
|
|
642
|
+
const keep = options.keepFused === true;
|
|
643
|
+
const verifyMembers = options.verifyMembers === true;
|
|
644
|
+
type Kind = "bytes" | "loaded" | "hashed";
|
|
645
|
+
interface Checked {
|
|
646
|
+
kind: Kind;
|
|
647
|
+
placement: Placement;
|
|
648
|
+
id: SetMemberPlacement;
|
|
649
|
+
originDigest: Uint8Array;
|
|
650
|
+
name: string | null;
|
|
651
|
+
original: Uint8Array | null;
|
|
652
|
+
load: (() => Promise<Uint8Array> | Uint8Array) | null;
|
|
653
|
+
builder: FuseBuilder | null;
|
|
654
|
+
fusedDigest: ((input: FusedDigestInput) => Promise<Uint8Array> | Uint8Array) | null;
|
|
655
|
+
}
|
|
572
656
|
const checked: Checked[] = [];
|
|
573
657
|
const seen = new Map<string, number>();
|
|
574
658
|
const report = (phase: FuseSetProgress["phase"], done: number, total: number) => {
|
|
@@ -579,17 +663,23 @@ export async function fuseSet(members: readonly FuseSetMember[], options: FuseSe
|
|
|
579
663
|
// a progress hook never changes the outcome
|
|
580
664
|
}
|
|
581
665
|
};
|
|
666
|
+
const bad = (i: number, message: string) => new FuseError("bad-input", `member ${i}: ${message}`, null, i);
|
|
582
667
|
for (let i = 0; i < members.length; i++) {
|
|
583
|
-
const m = members[i];
|
|
584
|
-
// A null, undefined or missing element is refused like any other member without
|
|
585
|
-
if (m === null || typeof m !== "object"
|
|
586
|
-
const
|
|
668
|
+
const m = members[i] as Partial<FuseSetBytesMember & FuseSetLoadedMember & FuseSetHashedMember> | null | undefined;
|
|
669
|
+
// A null, undefined or missing element is refused like any other member without bytes, a loader or a digest.
|
|
670
|
+
if (m === null || typeof m !== "object") throw bad(i, "original must be a Uint8Array, or load or fusedDigest a function");
|
|
671
|
+
const kind: Kind | null = m.original instanceof Uint8Array ? "bytes" : typeof m.load === "function" ? "loaded" : typeof m.fusedDigest === "function" ? "hashed" : null;
|
|
672
|
+
if (kind === null) throw bad(i, "original must be a Uint8Array, or load or fusedDigest a function");
|
|
673
|
+
if (kind !== "bytes" && m.placement === undefined) throw bad(i, `a ${kind} member names its placement`);
|
|
674
|
+
const id = m.placement ?? placementForBytes(m.original as Uint8Array);
|
|
587
675
|
const placement = getPlacement(id);
|
|
588
676
|
if (placement === undefined) throw new FuseError("bad-placement", `member ${i}: placement "${id}" is not registered`, null, i);
|
|
589
|
-
if (placement.form === "C") throw
|
|
590
|
-
if (m.name !== undefined && typeof m.name !== "string") throw
|
|
591
|
-
if (m.builder !== undefined && typeof m.builder !== "function") throw
|
|
592
|
-
|
|
677
|
+
if (placement.form === "C") throw bad(i, `${id} takes no original; a set holds trailer/1, container/1 and container/2 members only`);
|
|
678
|
+
if (m.name !== undefined && typeof m.name !== "string") throw bad(i, "name must be a string");
|
|
679
|
+
if (m.builder !== undefined && typeof m.builder !== "function") throw bad(i, "builder must be a function");
|
|
680
|
+
if (kind !== "bytes" && !(m.originDigest instanceof Uint8Array && m.originDigest.length === 32)) throw bad(i, `a ${kind} member names its originDigest, 32 bytes`);
|
|
681
|
+
if (kind === "hashed" && verifyMembers) throw bad(i, "a hashed member cannot be verified in full; pass its bytes or drop verifyMembers");
|
|
682
|
+
const originDigest = kind === "bytes" ? await digest(m.original as Uint8Array) : (m.originDigest as Uint8Array);
|
|
593
683
|
// The same original under the same placement fuses to the same bytes, which one manifest lists once.
|
|
594
684
|
const key = `${id}:${bytesToHex(originDigest)}`;
|
|
595
685
|
const j = seen.get(key);
|
|
@@ -597,7 +687,17 @@ export async function fuseSet(members: readonly FuseSetMember[], options: FuseSe
|
|
|
597
687
|
throw new FuseError("bad-input", `members ${j} and ${i} are the same original under the same placement (${id}) and would fuse to the same bytes; a set lists each fused artifact once`, null, i);
|
|
598
688
|
}
|
|
599
689
|
seen.set(key, i);
|
|
600
|
-
checked.push({
|
|
690
|
+
checked.push({
|
|
691
|
+
kind,
|
|
692
|
+
placement,
|
|
693
|
+
id,
|
|
694
|
+
originDigest,
|
|
695
|
+
name: m.name ?? null,
|
|
696
|
+
original: kind === "bytes" ? (m.original as Uint8Array) : null,
|
|
697
|
+
load: kind === "loaded" ? (m.load as Checked["load"]) : null,
|
|
698
|
+
builder: kind !== "hashed" && m.builder !== undefined ? (m.builder as FuseBuilder) : null,
|
|
699
|
+
fusedDigest: kind === "hashed" ? (m.fusedDigest as Checked["fusedDigest"]) : null,
|
|
700
|
+
});
|
|
601
701
|
report("hash", i + 1, members.length);
|
|
602
702
|
}
|
|
603
703
|
const t: BoundTransport = { ...DEFAULTS, ...(options.transport ?? {}) };
|
|
@@ -605,41 +705,72 @@ export async function fuseSet(members: readonly FuseSetMember[], options: FuseSe
|
|
|
605
705
|
// 1. nonce: one slot for the whole set
|
|
606
706
|
const slot = await allocateSlot(t);
|
|
607
707
|
|
|
608
|
-
// 2. fuse: the commitment once, every member's
|
|
609
|
-
// is held and its TTL is running; a throw here burns it but commits
|
|
708
|
+
// 2. fuse: the commitment once, every member's digest under it. The slot
|
|
709
|
+
// is held and its TTL is running; a throw here burns it but commits
|
|
710
|
+
// nothing. A member's fused bytes are virtual: each is built, hashed
|
|
711
|
+
// and released in turn, so memory holds one member's bytes at a time.
|
|
712
|
+
// They are held only for a caller who keeps them or asks the full
|
|
713
|
+
// verifier to read them.
|
|
610
714
|
const commitment = computeSlotCommitment(slot);
|
|
611
715
|
const commitmentHex = bytesToHex(commitment);
|
|
612
|
-
const keep = options.keepFused === true;
|
|
613
|
-
const verifyMembers = options.verifyMembers === true;
|
|
614
|
-
// A member's fused bytes are virtual: each is built, hashed and released in
|
|
615
|
-
// turn, so memory holds the originals and one fused copy. They are held only
|
|
616
|
-
// for a caller who keeps them or asks the full verifier to read them.
|
|
617
716
|
const fusedBytes: (Uint8Array | null)[] = [];
|
|
618
717
|
const rows: SetMember[] = [];
|
|
718
|
+
const expiring = "nothing was committed and the slot will expire";
|
|
619
719
|
for (let i = 0; i < checked.length; i++) {
|
|
620
720
|
const c = checked[i]!;
|
|
621
|
-
let
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
721
|
+
let artifact: Uint8Array;
|
|
722
|
+
let held: Uint8Array | null = null;
|
|
723
|
+
if (c.kind === "hashed") {
|
|
724
|
+
let d: unknown;
|
|
725
|
+
try {
|
|
726
|
+
d = await c.fusedDigest!({ commitment, commitmentHex, slot });
|
|
727
|
+
} catch (err) {
|
|
728
|
+
throw new FuseError("builder-failed", `member ${i}: fusedDigest threw: ${err instanceof Error ? err.message : String(err)}; ${expiring}`, null, i);
|
|
729
|
+
}
|
|
730
|
+
if (!(d instanceof Uint8Array) || d.length !== 32) throw new FuseError("builder-failed", `member ${i}: fusedDigest must return a 32-byte digest; ${expiring}`, null, i);
|
|
731
|
+
artifact = d;
|
|
732
|
+
} else {
|
|
733
|
+
let original: Uint8Array;
|
|
734
|
+
if (c.kind === "loaded") {
|
|
735
|
+
let loaded: unknown;
|
|
736
|
+
try {
|
|
737
|
+
loaded = await c.load!();
|
|
738
|
+
} catch (err) {
|
|
739
|
+
throw new FuseError("load-failed", `member ${i}: load threw: ${err instanceof Error ? err.message : String(err)}; ${expiring}`, null, i);
|
|
740
|
+
}
|
|
741
|
+
if (!(loaded instanceof Uint8Array)) throw new FuseError("load-failed", `member ${i}: load must return a Uint8Array; ${expiring}`, null, i);
|
|
742
|
+
original = loaded;
|
|
743
|
+
// The digest the caller named is the row's origin; it must be these bytes' own.
|
|
744
|
+
if (!bytesEqual(await digest(original), c.originDigest)) throw new FuseError("bad-input", `member ${i}: originDigest is not the SHA-256 of the loaded bytes; ${expiring}`, null, i);
|
|
745
|
+
} else {
|
|
746
|
+
original = c.original!;
|
|
747
|
+
}
|
|
748
|
+
const builder = c.builder ?? builderFor(c.id, original);
|
|
749
|
+
let fused: Uint8Array;
|
|
750
|
+
try {
|
|
751
|
+
fused = await builder({ commitment, commitmentHex, originDigest: c.originDigest, slot });
|
|
752
|
+
} catch (err) {
|
|
753
|
+
throw new FuseError("builder-failed", `member ${i}: the builder threw: ${err instanceof Error ? err.message : String(err)}`, null, i);
|
|
754
|
+
}
|
|
755
|
+
if (!(fused instanceof Uint8Array)) throw new FuseError("builder-failed", `member ${i}: the builder must return a Uint8Array`, null, i);
|
|
756
|
+
const located = requireCommitment(c.placement, fused, commitment, i);
|
|
757
|
+
// The row's origin must be the origin the bytes embed, else the member
|
|
758
|
+
// would verify INVALID_ORIGIN_ATTRIBUTION after the slot is spent. Both
|
|
759
|
+
// facts are checked when both are present: the digest the bytes declare
|
|
760
|
+
// (container/1's payload) and the bytes they carry, compared byte for
|
|
761
|
+
// byte with the member's original rather than hashed again, so a builder
|
|
762
|
+
// cannot pack other bytes under the member's digest and leave a member no
|
|
763
|
+
// original rebuilds.
|
|
764
|
+
const declared = located.originDigest;
|
|
765
|
+
const carried = located.originalBytes;
|
|
766
|
+
if ((declared !== undefined && !bytesEqual(declared, c.originDigest)) || (carried !== undefined && !bytesEqual(carried, original))) {
|
|
767
|
+
throw new FuseError("builder-failed", `member ${i}: the fused bytes embed an origin that is not the member's original; ${expiring}`, null, i);
|
|
768
|
+
}
|
|
769
|
+
artifact = await digest(fused);
|
|
770
|
+
if (keep || verifyMembers) held = fused;
|
|
640
771
|
}
|
|
641
|
-
rows.push({ artifact
|
|
642
|
-
fusedBytes.push(
|
|
772
|
+
rows.push({ artifact, origin: c.originDigest, placement: c.id });
|
|
773
|
+
fusedBytes.push(held);
|
|
643
774
|
report("fuse", i + 1, checked.length);
|
|
644
775
|
}
|
|
645
776
|
|
|
@@ -648,7 +779,7 @@ export async function fuseSet(members: readonly FuseSetMember[], options: FuseSe
|
|
|
648
779
|
try {
|
|
649
780
|
manifestBytes = buildSetManifest(commitment, rows);
|
|
650
781
|
} catch (err) {
|
|
651
|
-
throw new FuseError("bad-input", `the set manifest could not be built: ${err instanceof Error ? err.message : String(err)};
|
|
782
|
+
throw new FuseError("bad-input", `the set manifest could not be built: ${err instanceof Error ? err.message : String(err)}; ${expiring}`);
|
|
652
783
|
}
|
|
653
784
|
const artifactDigestB64 = bytesToBase64(await digest(manifestBytes));
|
|
654
785
|
const manifest = JSON.parse(new TextDecoder().decode(manifestBytes)) as SetManifest;
|
|
@@ -712,6 +843,7 @@ export async function fuseSet(members: readonly FuseSetMember[], options: FuseSe
|
|
|
712
843
|
report("verify", i + 1, checked.length);
|
|
713
844
|
}
|
|
714
845
|
const names = c.name !== null ? fusedNamesFor(c.name, c.id) : null;
|
|
846
|
+
const held = fusedBytes[i];
|
|
715
847
|
results.push({
|
|
716
848
|
index: i,
|
|
717
849
|
manifestIndex: k,
|
|
@@ -720,7 +852,7 @@ export async function fuseSet(members: readonly FuseSetMember[], options: FuseSe
|
|
|
720
852
|
artifactDigestB64: memberArtifactB64,
|
|
721
853
|
fusedName: names?.fusedName ?? null,
|
|
722
854
|
frameName: names?.frameName ?? null,
|
|
723
|
-
...(keep ? { fusedBytes:
|
|
855
|
+
...(keep && held !== null ? { fusedBytes: held } : {}),
|
|
724
856
|
...(verification !== undefined ? { verification } : {}),
|
|
725
857
|
});
|
|
726
858
|
}
|
package/src/index.ts
CHANGED
|
@@ -40,9 +40,9 @@ export { Constructor } from "./constructor.js";
|
|
|
40
40
|
// The producer profile over the primitive (working name Fuse): allocate a
|
|
41
41
|
// slot, write a commitment to it into the artifact, hash, commit under the
|
|
42
42
|
// same slot. The resulting proof is ordinary bitgraph/1.
|
|
43
|
-
export { fuse, fuseSet, MAX_SET_MEMBERS, builderFor, FuseError, digestFromBase64, placementForBytes, fusedNamesFor } from "./fuse.js";
|
|
43
|
+
export { fuse, fuseSet, MAX_SET_MEMBERS, trailerBytesFor, builderFor, FuseError, digestFromBase64, placementForBytes, fusedNamesFor } from "./fuse.js";
|
|
44
44
|
export type { FuseBuilder, BuilderInput, FuseOptions, FuseResult, FuseTransport, FuseErrorCode } from "./fuse.js";
|
|
45
|
-
export type { FuseSetMember, SetMemberPlacement, FuseSetOptions, FuseSetProgress, FuseSetMemberResult, FuseSetResult } from "./fuse.js";
|
|
45
|
+
export type { FuseSetMember, FuseSetBytesMember, FuseSetLoadedMember, FuseSetHashedMember, FusedDigestInput, SetMemberPlacement, FuseSetOptions, FuseSetProgress, FuseSetMemberResult, FuseSetResult } from "./fuse.js";
|
|
46
46
|
// The verify-package types those results are made of, so the core entry names everything it returns.
|
|
47
47
|
export type { FuseFrame, PlacementId, SetManifest, FuseMemberResult, FuseVerifyResult } from "@mikeargento/bitgraph-verify";
|
|
48
48
|
|