@mikeargento/bitgraph 1.3.0 → 1.4.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-set-sdk.test.d.ts +2 -0
- package/dist/__tests__/fuse-set-sdk.test.d.ts.map +1 -0
- package/dist/__tests__/fuse-set-sdk.test.js +513 -0
- package/dist/__tests__/fuse-set-sdk.test.js.map +1 -0
- package/dist/__tests__/fuse-set.test.d.ts +2 -0
- package/dist/__tests__/fuse-set.test.d.ts.map +1 -0
- package/dist/__tests__/fuse-set.test.js +863 -0
- package/dist/__tests__/fuse-set.test.js.map +1 -0
- package/dist/fuse-cli.js +111 -32
- package/dist/fuse-cli.js.map +1 -1
- package/dist/fuse.d.ts +77 -3
- package/dist/fuse.d.ts.map +1 -1
- package/dist/fuse.js +244 -48
- package/dist/fuse.js.map +1 -1
- package/dist/index.d.ts +3 -1
- 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-set-sdk.test.ts +638 -0
- package/src/__tests__/fuse-set.test.ts +1000 -0
- package/src/fuse-cli.ts +97 -27
- package/src/fuse.ts +322 -46
- package/src/index.ts +4 -1
package/src/fuse-cli.ts
CHANGED
|
@@ -5,8 +5,11 @@
|
|
|
5
5
|
* bitgraph-fuse: the internal harness (spec 9.2), as a command rather than a
|
|
6
6
|
* page. Exercises Forms A, B and C end to end through fuse(), writes the
|
|
7
7
|
* Frame (and the fused bytes when they must be kept), and prints the bounded
|
|
8
|
-
* copy of spec 9.3. `
|
|
9
|
-
*
|
|
8
|
+
* copy of spec 9.3. `set` runs fuseSet() over N files under one slot and
|
|
9
|
+
* writes the proof beside the manifest bytes. `check` runs verifyFuse over a
|
|
10
|
+
* Frame (or bare proof) and a file, or verifyFuseMember when the proof is a
|
|
11
|
+
* set proof and the file is not its manifest, so every verification path can
|
|
12
|
+
* be exercised from a shell.
|
|
10
13
|
*
|
|
11
14
|
* Not a product surface. The site has no /fuse page: a page would make the
|
|
12
15
|
* website build depend on packages that are not published, which is a deploy
|
|
@@ -18,9 +21,9 @@
|
|
|
18
21
|
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
19
22
|
import { basename, extname, join, resolve } from "node:path";
|
|
20
23
|
import { sha256 } from "@noble/hashes/sha256";
|
|
21
|
-
import { parseFrame, verifyFuse, bytesToBase64 } from "@mikeargento/bitgraph-verify";
|
|
22
|
-
import type { BitGraphProof, PlacementId } from "@mikeargento/bitgraph-verify";
|
|
23
|
-
import { fuse, builderFor, FuseError } from "./fuse.js";
|
|
24
|
+
import { parseFrame, verifyFuse, verifyFuseMember, bytesToBase64 } from "@mikeargento/bitgraph-verify";
|
|
25
|
+
import type { BitGraphProof, PlacementId, FuseVerifyResult, FuseMemberResult } from "@mikeargento/bitgraph-verify";
|
|
26
|
+
import { fuse, fuseSet, builderFor, placementForBytes, fusedNamesFor, FuseError } from "./fuse.js";
|
|
24
27
|
import type { FuseTransport } from "./fuse.js";
|
|
25
28
|
|
|
26
29
|
const USAGE = `bitgraph-fuse: BitGraph producer harness (profile bitgraph-fuse/1, working name)
|
|
@@ -29,12 +32,14 @@ const USAGE = `bitgraph-fuse: BitGraph producer harness (profile bitgraph-fuse/1
|
|
|
29
32
|
Form A or B over an existing file. The file is never modified.
|
|
30
33
|
bitgraph-fuse produce [--origin <file>] [options]
|
|
31
34
|
Form C: a canonical payload naming an optional source.
|
|
32
|
-
bitgraph-fuse
|
|
33
|
-
|
|
35
|
+
bitgraph-fuse set <file>... [options]
|
|
36
|
+
N files under one slot; the committed artifact is the set manifest. The files are never modified.
|
|
37
|
+
bitgraph-fuse check <frame-or-proof.json> <file> [--manifest <set.manifest.json>] [--max-positions N]
|
|
38
|
+
Verify a Frame (or bare proof) against the fused bytes or the original; a set proof against a member or its original.
|
|
34
39
|
|
|
35
|
-
Options for fuse and
|
|
36
|
-
--out <dir> where to write the Frame and fused bytes (default: .)
|
|
37
|
-
--keep also write the fused bytes for byte-exact placements
|
|
40
|
+
Options for fuse, produce and set:
|
|
41
|
+
--out <dir> where to write the Frame (for set: set.proof.json and set.manifest.json) and fused bytes (default: .)
|
|
42
|
+
--keep also write the fused bytes for byte-exact placements (set: inputs need distinct names)
|
|
38
43
|
--base-url <url> commit surface (default https://bitgraph.ing)
|
|
39
44
|
--allocate-path <p> default /api/fuse/allocate (parent-direct: /allocate-slot)
|
|
40
45
|
--commit-path <p> default /api/fuse/commit (parent-direct: /commit)
|
|
@@ -78,6 +83,10 @@ function transportFrom(flags: Map<string, string | true>): FuseTransport {
|
|
|
78
83
|
return t;
|
|
79
84
|
}
|
|
80
85
|
|
|
86
|
+
function outDirOf(args: Args): string {
|
|
87
|
+
return resolve(typeof args.flags.get("out") === "string" ? (args.flags.get("out") as string) : ".");
|
|
88
|
+
}
|
|
89
|
+
|
|
81
90
|
async function writeOutputs(outDir: string, label: string, frame: unknown, fusedName: string | null, fused: Uint8Array | undefined): Promise<string[]> {
|
|
82
91
|
await mkdir(outDir, { recursive: true });
|
|
83
92
|
const written: string[] = [];
|
|
@@ -102,8 +111,7 @@ async function runFuse(args: Args): Promise<number> {
|
|
|
102
111
|
const fusedName = `${label.slice(0, label.length - extname(label).length)}.fused${ext}`;
|
|
103
112
|
const keep = args.flags.get("keep") === true;
|
|
104
113
|
const r = await fuse(builderFor(placement as PlacementId, original), { placement: placement as PlacementId, original, fusedFile: fusedName, keepFused: keep, transport: transportFrom(args.flags) });
|
|
105
|
-
const
|
|
106
|
-
const written = await writeOutputs(outDir, label, r.frame, keep ? fusedName : null, r.fusedBytes);
|
|
114
|
+
const written = await writeOutputs(outDirOf(args), label, r.frame, keep ? fusedName : null, r.fusedBytes);
|
|
107
115
|
report(r.proof, r.verification.category, r.recovered, true);
|
|
108
116
|
process.stdout.write(`\nwrote:\n${written.map((w) => " " + w).join("\n")}\n`);
|
|
109
117
|
if (!keep) process.stdout.write(`\nThe fused bytes were not kept (${placement} is byte-exact: any verifier rebuilds them from the original and the proof). Pass --keep to write them.\n`);
|
|
@@ -114,13 +122,56 @@ async function runProduce(args: Args): Promise<number> {
|
|
|
114
122
|
const originPath = args.flags.get("origin");
|
|
115
123
|
const originDigest = typeof originPath === "string" ? sha256(new Uint8Array(await readFile(resolve(originPath)))) : undefined;
|
|
116
124
|
const r = await fuse(builderFor("produced/1"), { placement: "produced/1", ...(originDigest !== undefined ? { originDigest } : {}), fusedFile: "produced.json", transport: transportFrom(args.flags) });
|
|
117
|
-
const
|
|
118
|
-
const written = await writeOutputs(outDir, "produced", r.frame, "produced.json", r.fusedBytes);
|
|
125
|
+
const written = await writeOutputs(outDirOf(args), "produced", r.frame, "produced.json", r.fusedBytes);
|
|
119
126
|
report(r.proof, r.verification.category, r.recovered, originDigest !== undefined);
|
|
120
127
|
process.stdout.write(`\nwrote:\n${written.map((w) => " " + w).join("\n")}\n`);
|
|
121
128
|
return 0;
|
|
122
129
|
}
|
|
123
130
|
|
|
131
|
+
/** N files under one slot. Writes the proof and the manifest bytes exactly (they are the committed artifact, so `check` can hash them as it). */
|
|
132
|
+
async function runSet(args: Args): Promise<number> {
|
|
133
|
+
if (args.positional.length === 0) { process.stderr.write(USAGE); return 64; }
|
|
134
|
+
const keep = args.flags.get("keep") === true;
|
|
135
|
+
const members = [];
|
|
136
|
+
for (const file of args.positional) {
|
|
137
|
+
const original = new Uint8Array(await readFile(resolve(file)));
|
|
138
|
+
members.push({ original, placement: placementForBytes(original), name: sanitize(basename(file)) });
|
|
139
|
+
}
|
|
140
|
+
// Under --keep every member's fused bytes go to a file named from its own
|
|
141
|
+
// name and placement; two inputs that would share that name are refused
|
|
142
|
+
// here, before any allocation, rather than one silently overwriting the other.
|
|
143
|
+
if (keep) {
|
|
144
|
+
const seen = new Map<string, number>();
|
|
145
|
+
for (const [i, m] of members.entries()) {
|
|
146
|
+
const { fusedName } = fusedNamesFor(m.name, m.placement);
|
|
147
|
+
const j = seen.get(fusedName);
|
|
148
|
+
if (j !== undefined) { process.stderr.write(`set --keep: members ${j} and ${i} would both be written as ${fusedName}; give the files distinct names\n`); return 64; }
|
|
149
|
+
seen.set(fusedName, i);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const r = await fuseSet(members, { keepFused: keep, transport: transportFrom(args.flags) });
|
|
153
|
+
const outDir = outDirOf(args);
|
|
154
|
+
await mkdir(outDir, { recursive: true });
|
|
155
|
+
const written: string[] = [];
|
|
156
|
+
const proofPath = join(outDir, "set.proof.json");
|
|
157
|
+
await writeFile(proofPath, JSON.stringify(r.proof, null, 2) + "\n");
|
|
158
|
+
written.push(proofPath);
|
|
159
|
+
const manifestPath = join(outDir, "set.manifest.json");
|
|
160
|
+
await writeFile(manifestPath, r.manifestBytes);
|
|
161
|
+
written.push(manifestPath);
|
|
162
|
+
for (const m of r.members) {
|
|
163
|
+
if (m.fusedBytes === undefined || m.fusedName === null) continue;
|
|
164
|
+
const p = join(outDir, m.fusedName);
|
|
165
|
+
await writeFile(p, m.fusedBytes);
|
|
166
|
+
written.push(p);
|
|
167
|
+
}
|
|
168
|
+
report(r.proof, r.verification.category, r.recovered, false);
|
|
169
|
+
for (const m of r.members) process.stdout.write(`member ${m.index} row ${m.manifestIndex} ${m.placement} ${m.verification.category} ${m.fusedName ?? ""}\n`);
|
|
170
|
+
if (!r.manifestEchoed) process.stdout.write(`note the proof does not carry the manifest; keep set.manifest.json beside it\n`);
|
|
171
|
+
process.stdout.write(`\nwrote:\n${written.map((w) => " " + w).join("\n")}\n`);
|
|
172
|
+
return 0;
|
|
173
|
+
}
|
|
174
|
+
|
|
124
175
|
function report(proof: BitGraphProof, category: string, recovered: boolean, hasOrigin: boolean): void {
|
|
125
176
|
const c = proof.commit;
|
|
126
177
|
if (hasOrigin) process.stdout.write(COPY_ORIGINAL + "\n\n");
|
|
@@ -134,6 +185,22 @@ function report(proof: BitGraphProof, category: string, recovered: boolean, hasO
|
|
|
134
185
|
if (recovered) process.stdout.write(`note the commit response was lost; the proof was read back by digest and matched on the held slot\n`);
|
|
135
186
|
}
|
|
136
187
|
|
|
188
|
+
/** The verdict lines shared by both verifiers; the `set` line appears only for a member verdict. */
|
|
189
|
+
function printVerdict(r: FuseVerifyResult | FuseMemberResult): void {
|
|
190
|
+
process.stdout.write(`category ${r.category}\n`);
|
|
191
|
+
process.stdout.write(`proof ${r.proof.valid ? "valid" : `invalid: ${r.proof.reason ?? ""}`}\n`);
|
|
192
|
+
process.stdout.write(`file digest ${r.fileDigestB64}\n`);
|
|
193
|
+
process.stdout.write(`artifact ${r.artifactDigestB64}\n`);
|
|
194
|
+
if (r.originDigestB64) process.stdout.write(`origin ${r.originDigestB64}\n`);
|
|
195
|
+
if (r.placement) process.stdout.write(`placement ${r.placement}\n`);
|
|
196
|
+
if (r.span) process.stdout.write(`span slot ${r.span.slotCounter} to commit ${r.span.commitCounter} (${r.span.positions} positions)\n`);
|
|
197
|
+
if (r.policy.maxPositions !== null) process.stdout.write(`span policy ${r.policy.spanExceeded ? "EXCEEDED" : "within"} ${r.policy.maxPositions} positions\n`);
|
|
198
|
+
if ("set" in r && r.set?.member) process.stdout.write(`set member ${r.set.member.index + 1} of ${r.set.memberCount}\n`);
|
|
199
|
+
if (r.reason) process.stdout.write(`reason ${r.reason}\n`);
|
|
200
|
+
for (const s of r.statements) process.stdout.write(`\n${s}\n`);
|
|
201
|
+
process.stdout.write(`\nfloor computed by the Player from anchors in a bundle (bitgraph-play check); not available here\n`);
|
|
202
|
+
}
|
|
203
|
+
|
|
137
204
|
async function runCheck(args: Args): Promise<number> {
|
|
138
205
|
const [frameArg, fileArg] = args.positional;
|
|
139
206
|
if (frameArg === undefined || fileArg === undefined) { process.stderr.write(USAGE); return 64; }
|
|
@@ -148,18 +215,20 @@ async function runCheck(args: Args): Promise<number> {
|
|
|
148
215
|
}
|
|
149
216
|
const bytes = new Uint8Array(await readFile(resolve(fileArg)));
|
|
150
217
|
const max = args.flags.get("max-positions");
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
218
|
+
const policy = typeof max === "string" ? { maxPositions: BigInt(max) } : {};
|
|
219
|
+
// A set proof and a file that is not its manifest: the member verifier answers.
|
|
220
|
+
const a = proof.attribution;
|
|
221
|
+
if (a?.name === "bitgraph-fuse/1" && a.title === "set/1" && bytesToBase64(sha256(bytes)) !== proof.artifact.digestB64) {
|
|
222
|
+
const manifestArg = args.flags.get("manifest");
|
|
223
|
+
const manifest = typeof manifestArg === "string" ? new Uint8Array(await readFile(resolve(manifestArg))) : undefined;
|
|
224
|
+
const r = await verifyFuseMember({ proof, bytes, ...(manifest !== undefined ? { manifest } : {}), ...policy });
|
|
225
|
+
printVerdict(r);
|
|
226
|
+
if (r.category === "SET_MEMBER_DIRECT" || r.category === "SET_MEMBER_FROM_ORIGIN") return r.policy.spanExceeded ? 1 : 0;
|
|
227
|
+
if (r.category === "UNDETERMINED_PLACEMENT" || r.category === "NO_MATCH") return 2;
|
|
228
|
+
return 1;
|
|
229
|
+
}
|
|
230
|
+
const r = await verifyFuse({ proof, bytes, frame, ...policy });
|
|
231
|
+
printVerdict(r);
|
|
163
232
|
if (r.category === "RECORDED" || r.category === "FUSED_DIRECT" || r.category === "FUSED_FROM_ORIGIN") return r.policy.spanExceeded ? 1 : 0;
|
|
164
233
|
if (r.category === "UNDETERMINED_PLACEMENT" || r.category === "NO_MATCH") return 2;
|
|
165
234
|
return 1;
|
|
@@ -171,12 +240,13 @@ async function main(): Promise<number> {
|
|
|
171
240
|
switch (args.command) {
|
|
172
241
|
case "fuse": return await runFuse(args);
|
|
173
242
|
case "produce": return await runProduce(args);
|
|
243
|
+
case "set": return await runSet(args);
|
|
174
244
|
case "check": return await runCheck(args);
|
|
175
245
|
default: process.stderr.write(USAGE); return 64;
|
|
176
246
|
}
|
|
177
247
|
} catch (err) {
|
|
178
248
|
if (err instanceof FuseError) {
|
|
179
|
-
process.stderr.write(`no fused proof was completed (${err.code}${err.status !== null ? `, ${err.status}` : ""}): ${err.message}\n`);
|
|
249
|
+
process.stderr.write(`no fused proof was completed (${err.code}${err.status !== null ? `, ${err.status}` : ""}${err.member !== null ? `, member ${err.member}` : ""}): ${err.message}\n`);
|
|
180
250
|
return err.code === "tee-restarting" ? 2 : 1;
|
|
181
251
|
}
|
|
182
252
|
process.stderr.write(`error: ${err instanceof Error ? err.message : String(err)}\n`);
|
package/src/fuse.ts
CHANGED
|
@@ -15,6 +15,16 @@
|
|
|
15
15
|
* 4. fill: commit that digest under the same slot, with the placement id
|
|
16
16
|
* and the origin digest in the signed attribution.
|
|
17
17
|
*
|
|
18
|
+
* fuseSet(members, options): the same four beats for N files under ONE slot.
|
|
19
|
+
* The commitment is computed once and written into every member by that
|
|
20
|
+
* member's own placement; the committed artifact is the canonical set
|
|
21
|
+
* manifest (placement set/1, built by the verify package); the signed title
|
|
22
|
+
* is "set/1" with no origin, because a set has no single origin; the parsed
|
|
23
|
+
* manifest rides along as unsigned metadata. Nothing is committed unless
|
|
24
|
+
* every member's bytes carry the commitment and embed its own origin, and no
|
|
25
|
+
* proof is returned unless the manifest verifies FUSED_DIRECT and every
|
|
26
|
+
* member SET_MEMBER_DIRECT against the explicit manifest bytes.
|
|
27
|
+
*
|
|
18
28
|
* What this module never does: write the nonce anywhere but process memory,
|
|
19
29
|
* put it in a message, or fall back to an ordinary recording when the fused
|
|
20
30
|
* commit fails. A failure is reported as a failure and the slot expires on
|
|
@@ -28,18 +38,34 @@
|
|
|
28
38
|
import { sha256 } from "@noble/hashes/sha256";
|
|
29
39
|
import {
|
|
30
40
|
buildFrame,
|
|
41
|
+
buildSetManifest,
|
|
42
|
+
bytesEqual,
|
|
31
43
|
bytesToBase64,
|
|
32
44
|
bytesToHex,
|
|
33
45
|
computeSlotCommitment,
|
|
34
46
|
computeSlotRecordHash,
|
|
35
47
|
fuseAttribution,
|
|
36
48
|
getPlacement,
|
|
49
|
+
readSetMetadata,
|
|
50
|
+
SET_METADATA_KEY,
|
|
37
51
|
verifyFuse,
|
|
52
|
+
verifyFuseMember,
|
|
38
53
|
base64ToBytes,
|
|
39
54
|
} from "@mikeargento/bitgraph-verify";
|
|
40
|
-
import type {
|
|
55
|
+
import type {
|
|
56
|
+
BitGraphProof,
|
|
57
|
+
FuseFrame,
|
|
58
|
+
FuseMemberResult,
|
|
59
|
+
FuseVerifyResult,
|
|
60
|
+
Located,
|
|
61
|
+
Placement,
|
|
62
|
+
PlacementId,
|
|
63
|
+
SetManifest,
|
|
64
|
+
SetMember,
|
|
65
|
+
SlotAllocation,
|
|
66
|
+
} from "@mikeargento/bitgraph-verify";
|
|
41
67
|
|
|
42
|
-
export type { FuseFrame, PlacementId, SlotAllocation, BitGraphProof } from "@mikeargento/bitgraph-verify";
|
|
68
|
+
export type { FuseFrame, PlacementId, SlotAllocation, BitGraphProof, SetManifest, FuseMemberResult, FuseVerifyResult } from "@mikeargento/bitgraph-verify";
|
|
43
69
|
|
|
44
70
|
/** What the builder receives. The raw nonce is deliberately absent. */
|
|
45
71
|
export interface BuilderInput {
|
|
@@ -119,11 +145,14 @@ export type FuseErrorCode =
|
|
|
119
145
|
export class FuseError extends Error {
|
|
120
146
|
readonly code: FuseErrorCode;
|
|
121
147
|
readonly status: number | null;
|
|
122
|
-
|
|
148
|
+
/** The caller's 0-based index into a set's members when the failure is one member's; null otherwise. fuse() never sets it. */
|
|
149
|
+
readonly member: number | null;
|
|
150
|
+
constructor(code: FuseErrorCode, message: string, status: number | null = null, member: number | null = null) {
|
|
123
151
|
super(message);
|
|
124
152
|
this.name = "FuseError";
|
|
125
153
|
this.code = code;
|
|
126
154
|
this.status = status;
|
|
155
|
+
this.member = member;
|
|
127
156
|
}
|
|
128
157
|
}
|
|
129
158
|
|
|
@@ -181,6 +210,9 @@ const DEFAULTS = {
|
|
|
181
210
|
recoveryDelayMs: 1_500,
|
|
182
211
|
} as const;
|
|
183
212
|
|
|
213
|
+
/** A transport with every default filled in: what the beats below take. */
|
|
214
|
+
type BoundTransport = Required<Pick<FuseTransport, keyof typeof DEFAULTS>> & FuseTransport;
|
|
215
|
+
|
|
184
216
|
const B64_32 = /^[A-Za-z0-9+/]{43}=$/;
|
|
185
217
|
const B64_64 = /^[A-Za-z0-9+/]{86}==$/;
|
|
186
218
|
|
|
@@ -267,23 +299,8 @@ async function recover(
|
|
|
267
299
|
return null;
|
|
268
300
|
}
|
|
269
301
|
|
|
270
|
-
/**
|
|
271
|
-
|
|
272
|
-
* throws a FuseError; it never returns an ordinary recording in place of a
|
|
273
|
-
* fused one.
|
|
274
|
-
*/
|
|
275
|
-
export async function fuse(builder: FuseBuilder, options: FuseOptions): Promise<FuseResult> {
|
|
276
|
-
const placement = getPlacement(options.placement);
|
|
277
|
-
if (placement === undefined) throw new FuseError("bad-placement", `placement "${options.placement}" is not registered`);
|
|
278
|
-
if (placement.form !== "C" && options.original === undefined) throw new FuseError("bad-input", `${placement.id} needs the original bytes`);
|
|
279
|
-
if (placement.form === "C" && options.original !== undefined) throw new FuseError("bad-input", "produced/1 takes no original; pass originDigest to name a source");
|
|
280
|
-
if (options.originDigest !== undefined && options.originDigest.length !== 32) throw new FuseError("bad-input", "originDigest must be 32 bytes");
|
|
281
|
-
|
|
282
|
-
const t = { ...DEFAULTS, ...(options.transport ?? {}) };
|
|
283
|
-
const originDigest = options.original !== undefined ? sha256(options.original) : options.originDigest;
|
|
284
|
-
const originDigestB64 = originDigest !== undefined ? bytesToBase64(originDigest) : null;
|
|
285
|
-
|
|
286
|
-
// 1. nonce
|
|
302
|
+
/** 1. nonce. The signed slot record from the boundary; it must sit on the anchored chain. */
|
|
303
|
+
async function allocateSlot(t: BoundTransport): Promise<SlotAllocation> {
|
|
287
304
|
const alloc = await request(t, t.allocatePath, { method: "POST", body: {} });
|
|
288
305
|
if (alloc.status === 503 && codeOf(alloc.json) === "tee-restarting") throw new FuseError("tee-restarting", messageOf(alloc.json, "the boundary is restarting"), 503);
|
|
289
306
|
if (alloc.status !== 200) throw new FuseError("allocate-failed", messageOf(alloc.json, `allocation failed (${alloc.status})`), alloc.status);
|
|
@@ -291,37 +308,29 @@ export async function fuse(builder: FuseBuilder, options: FuseOptions): Promise<
|
|
|
291
308
|
const slot = (alloc.json as { slot?: unknown } | null)?.slot;
|
|
292
309
|
if (!isSlotRecord(slot) || slotId !== slot.nonceB64) throw new FuseError("allocate-failed", "the allocation response is not a slot record", alloc.status);
|
|
293
310
|
if (slot.chainId !== "bitgraph:main") throw new FuseError("allocate-failed", "the slot is not on the anchored chain; a fused floor needs bitgraph:main");
|
|
311
|
+
return slot;
|
|
312
|
+
}
|
|
294
313
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
throw new FuseError("builder-failed", `the builder threw: ${err instanceof Error ? err.message : String(err)}`);
|
|
302
|
-
}
|
|
303
|
-
if (!(fused instanceof Uint8Array)) throw new FuseError("builder-failed", "the builder must return a Uint8Array");
|
|
304
|
-
// Fail closed: never commit bytes that do not carry the commitment.
|
|
314
|
+
/**
|
|
315
|
+
* Fail closed: never commit bytes that do not carry the commitment. Returns
|
|
316
|
+
* what the placement located, for any further check. `member` names the
|
|
317
|
+
* set member the bytes belong to; null for a single fused artifact.
|
|
318
|
+
*/
|
|
319
|
+
function requireCommitment(placement: Placement, fused: Uint8Array, commitment: Uint8Array, member: number | null = null): Located {
|
|
305
320
|
const located = placement.locate(fused);
|
|
306
321
|
if (located === null || bytesToHex(located.commitment) !== bytesToHex(commitment)) {
|
|
307
|
-
|
|
322
|
+
const label = member !== null ? `member ${member}: ` : "";
|
|
323
|
+
throw new FuseError("commitment-missing", `${label}the fused bytes do not carry the ${placement.id} commitment; nothing was committed and the slot will expire`, null, member);
|
|
308
324
|
}
|
|
325
|
+
return located;
|
|
326
|
+
}
|
|
309
327
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
const body: Record<string, unknown> = {
|
|
317
|
-
digests: [{ digestB64: artifactDigestB64, hashAlg: "sha256" }],
|
|
318
|
-
slotId: slot.nonceB64,
|
|
319
|
-
slot,
|
|
320
|
-
chainId: "bitgraph:main",
|
|
321
|
-
attribution,
|
|
322
|
-
};
|
|
323
|
-
if (options.agency !== undefined) body.agency = options.agency;
|
|
324
|
-
|
|
328
|
+
/**
|
|
329
|
+
* 4. fill. Commit under the held slot; on a lost or refused response read
|
|
330
|
+
* back by digest and match the slot record; never allocate again; refuse a
|
|
331
|
+
* proof under any other slot.
|
|
332
|
+
*/
|
|
333
|
+
async function commitUnderSlot(t: BoundTransport, body: Record<string, unknown>, artifactDigestB64: string, slot: SlotAllocation): Promise<{ proof: BitGraphProof; recovered: boolean }> {
|
|
325
334
|
let proof: BitGraphProof | null = null;
|
|
326
335
|
let recovered = false;
|
|
327
336
|
let commit: { status: number; json: unknown } | null = null;
|
|
@@ -356,6 +365,55 @@ export async function fuse(builder: FuseBuilder, options: FuseOptions): Promise<
|
|
|
356
365
|
if (proof.slotAllocation?.nonceB64 !== slot.nonceB64 || proof.commit?.nonceB64 !== slot.nonceB64) {
|
|
357
366
|
throw new FuseError("slot-mismatch", "the boundary returned a proof under a different slot; nothing is labelled fused");
|
|
358
367
|
}
|
|
368
|
+
return { proof, recovered };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Allocate, fuse, hash, fill. Returns the Frame with the unchanged proof, or
|
|
373
|
+
* throws a FuseError; it never returns an ordinary recording in place of a
|
|
374
|
+
* fused one.
|
|
375
|
+
*/
|
|
376
|
+
export async function fuse(builder: FuseBuilder, options: FuseOptions): Promise<FuseResult> {
|
|
377
|
+
const placement = getPlacement(options.placement);
|
|
378
|
+
if (placement === undefined) throw new FuseError("bad-placement", `placement "${options.placement}" is not registered`);
|
|
379
|
+
if (placement.form !== "C" && options.original === undefined) throw new FuseError("bad-input", `${placement.id} needs the original bytes`);
|
|
380
|
+
if (placement.form === "C" && options.original !== undefined) throw new FuseError("bad-input", "produced/1 takes no original; pass originDigest to name a source");
|
|
381
|
+
if (options.originDigest !== undefined && options.originDigest.length !== 32) throw new FuseError("bad-input", "originDigest must be 32 bytes");
|
|
382
|
+
|
|
383
|
+
const t: BoundTransport = { ...DEFAULTS, ...(options.transport ?? {}) };
|
|
384
|
+
const originDigest = options.original !== undefined ? sha256(options.original) : options.originDigest;
|
|
385
|
+
const originDigestB64 = originDigest !== undefined ? bytesToBase64(originDigest) : null;
|
|
386
|
+
|
|
387
|
+
// 1. nonce
|
|
388
|
+
const slot = await allocateSlot(t);
|
|
389
|
+
|
|
390
|
+
// 2. fuse
|
|
391
|
+
const commitment = computeSlotCommitment(slot);
|
|
392
|
+
let fused: Uint8Array;
|
|
393
|
+
try {
|
|
394
|
+
fused = await builder({ commitment, commitmentHex: bytesToHex(commitment), ...(originDigest !== undefined ? { originDigest } : {}), slot });
|
|
395
|
+
} catch (err) {
|
|
396
|
+
throw new FuseError("builder-failed", `the builder threw: ${err instanceof Error ? err.message : String(err)}`);
|
|
397
|
+
}
|
|
398
|
+
if (!(fused instanceof Uint8Array)) throw new FuseError("builder-failed", "the builder must return a Uint8Array");
|
|
399
|
+
requireCommitment(placement, fused, commitment);
|
|
400
|
+
|
|
401
|
+
// 3. hash
|
|
402
|
+
const artifactDigest = sha256(fused);
|
|
403
|
+
const artifactDigestB64 = bytesToBase64(artifactDigest);
|
|
404
|
+
|
|
405
|
+
// 4. fill
|
|
406
|
+
const attribution = fuseAttribution(placement.id, originDigest);
|
|
407
|
+
const body: Record<string, unknown> = {
|
|
408
|
+
digests: [{ digestB64: artifactDigestB64, hashAlg: "sha256" }],
|
|
409
|
+
slotId: slot.nonceB64,
|
|
410
|
+
slot,
|
|
411
|
+
chainId: "bitgraph:main",
|
|
412
|
+
attribution,
|
|
413
|
+
};
|
|
414
|
+
if (options.agency !== undefined) body.agency = options.agency;
|
|
415
|
+
const { proof, recovered } = await commitUnderSlot(t, body, artifactDigestB64, slot);
|
|
416
|
+
|
|
359
417
|
// A minted proof is verified by a reader before it is called a proof.
|
|
360
418
|
const verification = await verifyFuse({ proof, bytes: fused });
|
|
361
419
|
if (verification.category !== "FUSED_DIRECT") {
|
|
@@ -382,6 +440,224 @@ export async function fuse(builder: FuseBuilder, options: FuseOptions): Promise<
|
|
|
382
440
|
};
|
|
383
441
|
}
|
|
384
442
|
|
|
443
|
+
// ---------------------------------------------------------------------------
|
|
444
|
+
// Sets: N files fused under ONE slot
|
|
445
|
+
// ---------------------------------------------------------------------------
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* The most members one set takes. Measured: one canonical row is 246 bytes
|
|
449
|
+
* (container/1, the longest id this phase), so 2000 rows is 492,174 bytes.
|
|
450
|
+
* The parent refuses raw bodies over 1 MB (server.ts:249), which would land
|
|
451
|
+
* AFTER allocation and burn the slot. Half the cap is left for the slot
|
|
452
|
+
* record, an agency envelope, and future longer placement ids. 4000 rows
|
|
453
|
+
* (984 KB) leaves 63 KB and is refused; 10000 rows (2.4 MB) cannot pass at
|
|
454
|
+
* all. A test pins the budget.
|
|
455
|
+
*/
|
|
456
|
+
export const MAX_SET_MEMBERS = 2000;
|
|
457
|
+
|
|
458
|
+
/** The placements a set member takes: Forms A and B, one original per member. */
|
|
459
|
+
export type SetMemberPlacement = "trailer/1" | "container/1";
|
|
460
|
+
|
|
461
|
+
export interface FuseSetMember {
|
|
462
|
+
/** The original bytes. Never modified. */
|
|
463
|
+
original: Uint8Array;
|
|
464
|
+
/** Default: placementForBytes(original). */
|
|
465
|
+
placement?: SetMemberPlacement;
|
|
466
|
+
/** Advisory; feeds fusedNamesFor. */
|
|
467
|
+
name?: string;
|
|
468
|
+
/** Default: builderFor(placement, original). The locate and origin guards run regardless. */
|
|
469
|
+
builder?: FuseBuilder;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
export interface FuseSetOptions {
|
|
473
|
+
/** Return each member's fused bytes. Default false: they are virtual, rebuilt from the original and the proof. */
|
|
474
|
+
keepFused?: boolean;
|
|
475
|
+
/** Actor-bound commits: an agency envelope passed through untouched. */
|
|
476
|
+
agency?: unknown;
|
|
477
|
+
transport?: FuseTransport;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
export interface FuseSetMemberResult {
|
|
481
|
+
/** The caller's index into members. */
|
|
482
|
+
index: number;
|
|
483
|
+
/** The row's position in the sorted manifest; equals verification.set.member.index. */
|
|
484
|
+
manifestIndex: number;
|
|
485
|
+
placement: SetMemberPlacement;
|
|
486
|
+
originDigestB64: string;
|
|
487
|
+
/** SHA-256 of the member's fused bytes; the row's artifact. */
|
|
488
|
+
artifactDigestB64: string;
|
|
489
|
+
/** fusedNamesFor(name, placement); null when the member has no name. */
|
|
490
|
+
fusedName: string | null;
|
|
491
|
+
/** Advisory; no Frame is written for a set member this phase. */
|
|
492
|
+
frameName: string | null;
|
|
493
|
+
/** Present only when keepFused is true. */
|
|
494
|
+
fusedBytes?: Uint8Array;
|
|
495
|
+
/** The local verification of the returned proof against this member's fused bytes. Always SET_MEMBER_DIRECT on success, with set.manifestSource "argument". */
|
|
496
|
+
verification: FuseMemberResult;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
export interface FuseSetResult {
|
|
500
|
+
proof: BitGraphProof;
|
|
501
|
+
/** The committed artifact. Keep it beside the proof. */
|
|
502
|
+
manifestBytes: Uint8Array;
|
|
503
|
+
/** JSON.parse of manifestBytes; the exact object sent under metadata. */
|
|
504
|
+
manifest: SetManifest;
|
|
505
|
+
/** SHA-256 of manifestBytes; equals proof.artifact.digestB64. */
|
|
506
|
+
artifactDigestB64: string;
|
|
507
|
+
slotCommitmentB64: string;
|
|
508
|
+
/** In the caller's order. */
|
|
509
|
+
members: FuseSetMemberResult[];
|
|
510
|
+
/** True when the commit response was lost and the proof was read back by the manifest digest. */
|
|
511
|
+
recovered: boolean;
|
|
512
|
+
/** True only when readSetMetadata(proof) is byte-equal to manifestBytes. */
|
|
513
|
+
manifestEchoed: boolean;
|
|
514
|
+
/** verifyFuse over manifestBytes. Always FUSED_DIRECT under "set/1" on success. */
|
|
515
|
+
verification: FuseVerifyResult;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Allocate once, fuse every member with the one commitment, hash the set
|
|
520
|
+
* manifest, fill the slot with it. Returns the proof with the manifest bytes
|
|
521
|
+
* beside it, or throws a FuseError; it never commits a partial set and never
|
|
522
|
+
* allocates a second slot.
|
|
523
|
+
*/
|
|
524
|
+
export async function fuseSet(members: readonly FuseSetMember[], options: FuseSetOptions = {}): Promise<FuseSetResult> {
|
|
525
|
+
// 0. validate, before any request. A refusal here burns nothing.
|
|
526
|
+
if (!Array.isArray(members) || members.length === 0) throw new FuseError("bad-input", "a set lists at least one member");
|
|
527
|
+
if (members.length > MAX_SET_MEMBERS) throw new FuseError("bad-input", `a set lists at most ${MAX_SET_MEMBERS} members (got ${members.length})`);
|
|
528
|
+
interface Checked { placement: Placement; id: SetMemberPlacement; original: Uint8Array; originDigest: Uint8Array; name: string | null; builder: FuseBuilder }
|
|
529
|
+
const checked: Checked[] = [];
|
|
530
|
+
const seen = new Map<string, number>();
|
|
531
|
+
for (let i = 0; i < members.length; i++) {
|
|
532
|
+
const m = members[i];
|
|
533
|
+
// A null, undefined or missing element is refused like any other member without original bytes.
|
|
534
|
+
if (m === null || typeof m !== "object" || !(m.original instanceof Uint8Array)) throw new FuseError("bad-input", `member ${i}: original must be a Uint8Array`, null, i);
|
|
535
|
+
const id = m.placement ?? placementForBytes(m.original);
|
|
536
|
+
const placement = getPlacement(id);
|
|
537
|
+
if (placement === undefined) throw new FuseError("bad-placement", `member ${i}: placement "${id}" is not registered`, null, i);
|
|
538
|
+
if (placement.form === "C") throw new FuseError("bad-input", `member ${i}: ${id} takes no original; a set holds trailer/1 and container/1 members only`, null, i);
|
|
539
|
+
if (m.name !== undefined && typeof m.name !== "string") throw new FuseError("bad-input", `member ${i}: name must be a string`, null, i);
|
|
540
|
+
if (m.builder !== undefined && typeof m.builder !== "function") throw new FuseError("bad-input", `member ${i}: builder must be a function`, null, i);
|
|
541
|
+
const originDigest = sha256(m.original);
|
|
542
|
+
// The same original under the same placement fuses to the same bytes, which one manifest lists once.
|
|
543
|
+
const key = `${id}:${bytesToHex(originDigest)}`;
|
|
544
|
+
const j = seen.get(key);
|
|
545
|
+
if (j !== undefined) {
|
|
546
|
+
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);
|
|
547
|
+
}
|
|
548
|
+
seen.set(key, i);
|
|
549
|
+
checked.push({ placement, id, original: m.original, originDigest, name: m.name ?? null, builder: m.builder ?? builderFor(id, m.original) });
|
|
550
|
+
}
|
|
551
|
+
const t: BoundTransport = { ...DEFAULTS, ...(options.transport ?? {}) };
|
|
552
|
+
|
|
553
|
+
// 1. nonce: one slot for the whole set
|
|
554
|
+
const slot = await allocateSlot(t);
|
|
555
|
+
|
|
556
|
+
// 2. fuse: the commitment once, every member's bytes carrying it. The slot
|
|
557
|
+
// is held and its TTL is running; a throw here burns it but commits nothing.
|
|
558
|
+
const commitment = computeSlotCommitment(slot);
|
|
559
|
+
const commitmentHex = bytesToHex(commitment);
|
|
560
|
+
const fusedBytes: Uint8Array[] = [];
|
|
561
|
+
const rows: SetMember[] = [];
|
|
562
|
+
for (let i = 0; i < checked.length; i++) {
|
|
563
|
+
const c = checked[i]!;
|
|
564
|
+
let fused: Uint8Array;
|
|
565
|
+
try {
|
|
566
|
+
fused = await c.builder({ commitment, commitmentHex, originDigest: c.originDigest, slot });
|
|
567
|
+
} catch (err) {
|
|
568
|
+
throw new FuseError("builder-failed", `member ${i}: the builder threw: ${err instanceof Error ? err.message : String(err)}`, null, i);
|
|
569
|
+
}
|
|
570
|
+
if (!(fused instanceof Uint8Array)) throw new FuseError("builder-failed", `member ${i}: the builder must return a Uint8Array`, null, i);
|
|
571
|
+
const located = requireCommitment(c.placement, fused, commitment, i);
|
|
572
|
+
// The row's origin must be the origin the bytes embed, else the member
|
|
573
|
+
// would verify INVALID_ORIGIN_ATTRIBUTION after the slot is spent. Both
|
|
574
|
+
// facts are checked when both are present: the digest the bytes declare
|
|
575
|
+
// (container/1's payload) and the bytes they carry, so a builder cannot
|
|
576
|
+
// pack other bytes under the member's digest and leave a member no
|
|
577
|
+
// original rebuilds.
|
|
578
|
+
const declared = located.originDigest;
|
|
579
|
+
const carried = located.originalBytes !== undefined ? sha256(located.originalBytes) : undefined;
|
|
580
|
+
if ((declared !== undefined && !bytesEqual(declared, c.originDigest)) || (carried !== undefined && !bytesEqual(carried, c.originDigest))) {
|
|
581
|
+
throw new FuseError("builder-failed", `member ${i}: the fused bytes embed an origin that is not the member's original; nothing was committed and the slot will expire`, null, i);
|
|
582
|
+
}
|
|
583
|
+
fusedBytes.push(fused);
|
|
584
|
+
rows.push({ artifact: sha256(fused), origin: c.originDigest, placement: c.id });
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// 3. hash: the canonical manifest is the artifact
|
|
588
|
+
let manifestBytes: Uint8Array;
|
|
589
|
+
try {
|
|
590
|
+
manifestBytes = buildSetManifest(commitment, rows);
|
|
591
|
+
} catch (err) {
|
|
592
|
+
throw new FuseError("bad-input", `the set manifest could not be built: ${err instanceof Error ? err.message : String(err)}; nothing was committed and the slot will expire`);
|
|
593
|
+
}
|
|
594
|
+
const artifactDigestB64 = bytesToBase64(sha256(manifestBytes));
|
|
595
|
+
const manifest = JSON.parse(new TextDecoder().decode(manifestBytes)) as SetManifest;
|
|
596
|
+
|
|
597
|
+
// 4. fill: one commit, the parsed manifest riding along as unsigned metadata
|
|
598
|
+
const body: Record<string, unknown> = {
|
|
599
|
+
digests: [{ digestB64: artifactDigestB64, hashAlg: "sha256" }],
|
|
600
|
+
slotId: slot.nonceB64,
|
|
601
|
+
slot,
|
|
602
|
+
chainId: "bitgraph:main",
|
|
603
|
+
attribution: fuseAttribution("set/1"),
|
|
604
|
+
metadata: { [SET_METADATA_KEY]: manifest },
|
|
605
|
+
};
|
|
606
|
+
if (options.agency !== undefined) body.agency = options.agency;
|
|
607
|
+
const { proof, recovered } = await commitUnderSlot(t, body, artifactDigestB64, slot);
|
|
608
|
+
|
|
609
|
+
// The manifest is verified by a reader before the proof is called a set proof.
|
|
610
|
+
const verification = await verifyFuse({ proof, bytes: manifestBytes });
|
|
611
|
+
if (verification.category !== "FUSED_DIRECT" || verification.placement !== "set/1") {
|
|
612
|
+
throw new FuseError("verification-failed", `the returned proof does not verify as a set: ${verification.category}${verification.reason ? ` (${verification.reason})` : ""}`);
|
|
613
|
+
}
|
|
614
|
+
// The echo is unsigned and advisory. Absent is normal: no production
|
|
615
|
+
// boundary returns it today (the site proxy does not forward metadata and
|
|
616
|
+
// the enclave's commitDigest action drops it); differing means a boundary
|
|
617
|
+
// rewrote the response.
|
|
618
|
+
const echoed = readSetMetadata(proof);
|
|
619
|
+
if (echoed !== null && !bytesEqual(echoed, manifestBytes)) {
|
|
620
|
+
throw new FuseError("verification-failed", `the returned proof echoes a set manifest under metadata["${SET_METADATA_KEY}"] that differs from the committed one`);
|
|
621
|
+
}
|
|
622
|
+
const manifestEchoed = echoed !== null;
|
|
623
|
+
// Every member against the explicit manifest bytes, so no verdict depends on the echo.
|
|
624
|
+
const keep = options.keepFused === true;
|
|
625
|
+
const results: FuseSetMemberResult[] = [];
|
|
626
|
+
for (let i = 0; i < checked.length; i++) {
|
|
627
|
+
const c = checked[i]!;
|
|
628
|
+
const fused = fusedBytes[i]!;
|
|
629
|
+
const memberArtifactB64 = bytesToBase64(rows[i]!.artifact);
|
|
630
|
+
const v = await verifyFuseMember({ proof, bytes: fused, manifest: manifestBytes });
|
|
631
|
+
const member = v.set?.member ?? null;
|
|
632
|
+
if (v.category !== "SET_MEMBER_DIRECT" || member === null || member.fusedDigestB64 !== memberArtifactB64) {
|
|
633
|
+
throw new FuseError("verification-failed", `member ${i}: the returned proof does not verify this member: ${v.category}${v.reason ? ` (${v.reason})` : ""}`, null, i);
|
|
634
|
+
}
|
|
635
|
+
const names = c.name !== null ? fusedNamesFor(c.name, c.id) : null;
|
|
636
|
+
results.push({
|
|
637
|
+
index: i,
|
|
638
|
+
manifestIndex: member.index,
|
|
639
|
+
placement: c.id,
|
|
640
|
+
originDigestB64: bytesToBase64(c.originDigest),
|
|
641
|
+
artifactDigestB64: memberArtifactB64,
|
|
642
|
+
fusedName: names?.fusedName ?? null,
|
|
643
|
+
frameName: names?.frameName ?? null,
|
|
644
|
+
...(keep ? { fusedBytes: fused } : {}),
|
|
645
|
+
verification: v,
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
return {
|
|
649
|
+
proof,
|
|
650
|
+
manifestBytes,
|
|
651
|
+
manifest,
|
|
652
|
+
artifactDigestB64,
|
|
653
|
+
slotCommitmentB64: bytesToBase64(commitment),
|
|
654
|
+
members: results,
|
|
655
|
+
recovered,
|
|
656
|
+
manifestEchoed,
|
|
657
|
+
verification,
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
|
|
385
661
|
/** Decode a standard-base64 digest, for callers holding one as text. */
|
|
386
662
|
export function digestFromBase64(b64: string): Uint8Array {
|
|
387
663
|
const d = base64ToBytes(b64);
|
package/src/index.ts
CHANGED
|
@@ -40,8 +40,11 @@ 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, builderFor, FuseError, digestFromBase64, placementForBytes, fusedNamesFor } from "./fuse.js";
|
|
43
|
+
export { fuse, fuseSet, MAX_SET_MEMBERS, 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, FuseSetMemberResult, FuseSetResult } from "./fuse.js";
|
|
46
|
+
// The verify-package types those results are made of, so the core entry names everything it returns.
|
|
47
|
+
export type { FuseFrame, PlacementId, SetManifest, FuseMemberResult, FuseVerifyResult } from "@mikeargento/bitgraph-verify";
|
|
45
48
|
|
|
46
49
|
// Policy parsing, hashing, and validation
|
|
47
50
|
export {
|