@mikeargento/bitgraph 1.10.1 → 1.12.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/README.md +42 -32
- package/dist/export.d.ts +98 -0
- package/dist/export.d.ts.map +1 -0
- package/dist/export.js +366 -0
- package/dist/export.js.map +1 -0
- package/dist/fuse.d.ts +156 -11
- package/dist/fuse.d.ts.map +1 -1
- package/dist/fuse.js +380 -8
- package/dist/fuse.js.map +1 -1
- package/dist/index.d.ts +11 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -1
- package/dist/index.js.map +1 -1
- package/dist/recovery-write.d.ts +58 -0
- package/dist/recovery-write.d.ts.map +1 -0
- package/dist/recovery-write.js +265 -0
- package/dist/recovery-write.js.map +1 -0
- package/dist/recovery.d.ts +490 -0
- package/dist/recovery.d.ts.map +1 -0
- package/dist/recovery.js +1124 -0
- package/dist/recovery.js.map +1 -0
- package/package.json +3 -3
- package/src/export.ts +435 -0
- package/src/fuse.ts +521 -19
- package/src/index.ts +28 -3
- package/src/recovery-write.ts +334 -0
- package/src/recovery.ts +1261 -0
package/src/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Copyright (c) Argento Computing Inc. All rights reserved. See LICENSE.
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* bitgraph-core
|
|
4
|
+
* bitgraph-core: BitGraph
|
|
5
5
|
*
|
|
6
6
|
* Portable cryptographic proof at finalization.
|
|
7
7
|
* Hardware TEE enforcement via AWS Nitro Enclaves.
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* compatibility. Verification of BitGraph proofs is permissionless.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
// Read side
|
|
14
|
+
// Read side: re-exported from the permissive verifier package
|
|
15
15
|
export type {
|
|
16
16
|
BitGraphProof,
|
|
17
17
|
BitGraphPolicy,
|
|
@@ -38,9 +38,20 @@ export type { HostCapabilities } from "./host.js";
|
|
|
38
38
|
// Constructor (write path)
|
|
39
39
|
export { Constructor } from "./constructor.js";
|
|
40
40
|
|
|
41
|
+
// tree/1 (2026-10-03): every new BitGraph is one Merkle tree of 1 to N files
|
|
42
|
+
// under one position; a single file is a tree of one. fuseTree makes it, and
|
|
43
|
+
// the export builders write bitgraph-export/1 for one member or the owner.
|
|
44
|
+
export { fuseTree, treePlacementFor } from "./fuse.js";
|
|
45
|
+
export type { FuseTreeMember, FuseTreeBytesMember, FuseTreeLoadedMember, FuseTreeHashedMember, FuseTreeAsIsMember, TreeMemberPlacement, FuseTreeOptions, FuseTreeProgress, FuseTreeMemberResult, FuseTreeResult, AnchorMark } from "./fuse.js";
|
|
46
|
+
export { buildMemberExport, buildOwnerExport, namesByLeaf, floorFromHeader, fetchFloorHeader, completeExport } from "./export.js";
|
|
47
|
+
export type { ExportFloor, TreeExportSource, ExportParts, ExportFetchOptions, CompleteExportOptions, CompletedExport } from "./export.js";
|
|
48
|
+
export { committedBytesFor, verifyTreeMember, verifyExport, parseExport, EXPORT_FORMAT } from "@mikeargento/bitgraph-verify";
|
|
49
|
+
export type { BitGraphExport, TreeLeaf, TreeMemberEvidence, TreeVerifyResult, ExportClaim, ExportVerifyResult } from "@mikeargento/bitgraph-verify";
|
|
50
|
+
|
|
41
51
|
// The producer profile over the primitive (working name Fuse): allocate a
|
|
42
52
|
// slot, write a commitment to it into the artifact, hash, commit under the
|
|
43
|
-
// same slot. The resulting proof is ordinary bitgraph/1.
|
|
53
|
+
// same slot. The resulting proof is ordinary bitgraph/1. fuse and fuseSet are
|
|
54
|
+
// superseded by fuseTree and kept so code that imports them keeps working.
|
|
44
55
|
export { fuse, fuseSet, MAX_SET_MEMBERS, trailerBytesFor, builderFor, FuseError, digestFromBase64, placementForBytes, fusedNamesFor } from "./fuse.js";
|
|
45
56
|
export { MAX_SET2_MEMBERS, SET2_PLACEMENT_ID, SET_MEMBER_METADATA_KEY } from "@mikeargento/bitgraph-verify";
|
|
46
57
|
export type { FuseBuilder, BuilderInput, FuseOptions, FuseResult, FuseTransport, FuseErrorCode } from "./fuse.js";
|
|
@@ -60,3 +71,17 @@ export type {
|
|
|
60
71
|
PolicyRules,
|
|
61
72
|
ActionValidationResult,
|
|
62
73
|
} from "./policy.js";
|
|
74
|
+
|
|
75
|
+
// Recovery from the file (2026-10-03): the sealed entries a tree's files are
|
|
76
|
+
// found again by, and the writer the CLI, SDK and MCP use after making a tree.
|
|
77
|
+
export { writeRecoveryEntries } from "./recovery-write.js";
|
|
78
|
+
export type { RecoveryWriteInput, RecoveryWriteOptions, RecoveryWriteResult, RecoveryWriteState } from "./recovery-write.js";
|
|
79
|
+
export {
|
|
80
|
+
recoveryAddress, recoveryEntryId, recoverySaltedEntryId, recoveryEntryIdOf, newRecoverySalt, recoveryKeyBytes, recoveryObjectKey,
|
|
81
|
+
sealRecoveryEnvelope, openRecoveryEnvelope, encodeRecoveryPlaintext, parseRecoveryPlaintext, recoveryTreeFrom, sealRecoveryMember, recoveryObjectKeyFor,
|
|
82
|
+
recoveryPlaintextFor, recoveryDigestOf, recoverySidesOf, existingEntryHoldsMember, findOwnRecoveryEntry, proofTrusted,
|
|
83
|
+
recoverFromDigest, recoverFromDigests, fetchRecoveredProof,
|
|
84
|
+
RECOVERY_SIDE_BITS, recoveryMemberStatus, recoverySideState, recoverySideResolved, recoverySaltKey, recoverySaltsFor,
|
|
85
|
+
RECOVERY_FORMAT, RECOVERY_PREFIX, SALT_BYTES, MAX_LOOKUP_ADDRESSES, MAX_RECOVERED_PER_DIGEST, LOOKUP_TIMEOUT_MS, OBJECT_KEY_LENGTH,
|
|
86
|
+
} from "./recovery.js";
|
|
87
|
+
export type { RecoveredEntry, RecoveryPlaintext, RecoveryLookupAnswer, RecoverySide, RecoveryMemberStatus, RecoverySideState, RecoveryTrust } from "./recovery.js";
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
// Copyright (c) 2024-2026 Argento Computing Inc. Licensed under the MIT License. See LICENSE.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Writing a tree's recovery entries from outside the browser (2026-10-03).
|
|
5
|
+
*
|
|
6
|
+
* The drop box keeps its trees recoverable through a persistent queue
|
|
7
|
+
* (website/src/lib/recovery-queue.ts). The CLI, SDK and MCP make trees too,
|
|
8
|
+
* and a file they made should be found again the same way when someone drops
|
|
9
|
+
* it on bitgraph.ing. This posts the same sealed entries (recovery.ts, the
|
|
10
|
+
* Node twin of the site's module) to POST /api/recovery, after the proof is
|
|
11
|
+
* already in hand: it never stands between anyone and a proof.
|
|
12
|
+
*
|
|
13
|
+
* Every write is create-only. "created" counts as kept; "exists" counts
|
|
14
|
+
* only after the stored envelope is opened and holds this same member.
|
|
15
|
+
* A key that holds ANOTHER member's entry (SPEC section 13, squatting) is
|
|
16
|
+
* not the end of it: the same entry is written again under its salted name,
|
|
17
|
+
* with a fresh 32-byte salt sealed inside, and only a salted name that is
|
|
18
|
+
* held too leaves the side blocked. "conflict" and "error" are retried. A
|
|
19
|
+
* site that does not take recovery writes yet (503, or no route at all)
|
|
20
|
+
* leaves the entries pending, and the caller says so: the tree is exactly as
|
|
21
|
+
* made, it is just not recoverable from the file until its entries are
|
|
22
|
+
* written.
|
|
23
|
+
*
|
|
24
|
+
* STATE, FOR A WRITER THAT RESUMES. Progress is one byte per member
|
|
25
|
+
* (recovery.ts, RECOVERY_SIDE_BITS) plus the salts chosen so far, the same
|
|
26
|
+
* shape the browser's queue saves. A caller passes the state of an earlier
|
|
27
|
+
* run to pick up where it stopped, and `onState` hears every change worth
|
|
28
|
+
* keeping: a salt the moment it is chosen (BEFORE its write, so a retry lands
|
|
29
|
+
* on the same name) and progress after every batch. Entries are sealed a
|
|
30
|
+
* batch at a time, never all at once: a 100,000-file tree is 200,000
|
|
31
|
+
* envelopes, and they need not all be in memory together.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { base64ToBytes, bytesToBase64 } from "@mikeargento/bitgraph-verify";
|
|
35
|
+
import {
|
|
36
|
+
MAX_BATCH_ENTRIES,
|
|
37
|
+
RECOVERY_SIDE_BITS,
|
|
38
|
+
existingEntryHoldsMember,
|
|
39
|
+
findOwnRecoveryEntry,
|
|
40
|
+
newRecoverySalt,
|
|
41
|
+
recoverySaltKey,
|
|
42
|
+
recoverySaltsFor,
|
|
43
|
+
recoverySideResolved,
|
|
44
|
+
recoverySideState,
|
|
45
|
+
recoverySidesOf,
|
|
46
|
+
recoveryTreeFrom,
|
|
47
|
+
sealRecoveryMember,
|
|
48
|
+
type FetchLike,
|
|
49
|
+
type RecoveryTreeInput,
|
|
50
|
+
type RecoveryWrite,
|
|
51
|
+
} from "./recovery.js";
|
|
52
|
+
|
|
53
|
+
export interface RecoveryWriteInput extends RecoveryTreeInput {
|
|
54
|
+
/** A file name per leaf, tree order. Advisory, sealed with each entry. */
|
|
55
|
+
names?: ReadonlyArray<string | null | undefined>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* What a writer keeps between runs: one progress byte per member
|
|
60
|
+
* (RECOVERY_SIDE_BITS) and the salts chosen for entries whose deterministic
|
|
61
|
+
* key was held, base64 by recoverySaltKey. The same shape the browser's
|
|
62
|
+
* queue saves for its jobs.
|
|
63
|
+
*/
|
|
64
|
+
export interface RecoveryWriteState {
|
|
65
|
+
progress: Uint8Array;
|
|
66
|
+
salts: Record<string, string>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface RecoveryWriteOptions {
|
|
70
|
+
/** Default https://bitgraph.ing. */
|
|
71
|
+
baseUrl?: string;
|
|
72
|
+
fetch?: FetchLike;
|
|
73
|
+
/** Rounds of retry for conflicts, errors and transient failures. Default 3. */
|
|
74
|
+
retries?: number;
|
|
75
|
+
/** Milliseconds before the first retry; doubles each round. Default 500. */
|
|
76
|
+
backoffMs?: number;
|
|
77
|
+
/** Milliseconds one request may take. Default 60,000. */
|
|
78
|
+
timeoutMs?: number;
|
|
79
|
+
/** Milliseconds the whole call may take; what is not written by then is reported pending. Default: no budget. */
|
|
80
|
+
budgetMs?: number;
|
|
81
|
+
/** An earlier run's state to resume from. */
|
|
82
|
+
state?: RecoveryWriteState;
|
|
83
|
+
/** Hears the state whenever it is worth keeping: a salt before its write, progress after every batch. */
|
|
84
|
+
onState?: (state: RecoveryWriteState) => void | Promise<void>;
|
|
85
|
+
/** False: a held deterministic key leaves the side blocked instead of trying its salted name. Default true. */
|
|
86
|
+
saltedFallback?: boolean;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface RecoveryWriteResult {
|
|
90
|
+
/** Entries this tree needs: two per placed member, one per as-is member. */
|
|
91
|
+
entries: number;
|
|
92
|
+
/** Kept when this call ended: created now, or already stored as this member, in this run or one before it. */
|
|
93
|
+
kept: number;
|
|
94
|
+
/** Created in this call. */
|
|
95
|
+
written: number;
|
|
96
|
+
/** Already stored, and opened to hold this same member, in this call. */
|
|
97
|
+
alreadyThere: number;
|
|
98
|
+
/** Of `kept`, those under a salted name: their deterministic key was held by another member's entry. */
|
|
99
|
+
salted: number;
|
|
100
|
+
/** Both names held by other entries (see SPEC section 13, squatting). */
|
|
101
|
+
blocked: number;
|
|
102
|
+
/** Not written: the site refused writes, the attempts or the budget ran out. */
|
|
103
|
+
pending: number;
|
|
104
|
+
/** Why entries are pending, in words, when any are. */
|
|
105
|
+
reason: string | null;
|
|
106
|
+
/** The state to resume from when `pending` is not zero. */
|
|
107
|
+
state: RecoveryWriteState;
|
|
108
|
+
/** Every entry is kept or blocked: nothing left to write. */
|
|
109
|
+
done: boolean;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
interface Planned {
|
|
113
|
+
index: number;
|
|
114
|
+
w: RecoveryWrite;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
type Answer = { status: string; envelope?: string };
|
|
118
|
+
|
|
119
|
+
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
|
120
|
+
|
|
121
|
+
function hostOf(base: string): string {
|
|
122
|
+
try {
|
|
123
|
+
return new URL(base).host;
|
|
124
|
+
} catch {
|
|
125
|
+
return base;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Base64 salts by recoverySaltKey, with the progress, as a fresh copy for a caller to keep. */
|
|
130
|
+
const snapshot = (progress: Uint8Array, salts: Record<string, string>): RecoveryWriteState => ({ progress: progress.slice(), salts: { ...salts } });
|
|
131
|
+
|
|
132
|
+
export async function writeRecoveryEntries(input: RecoveryWriteInput, opts: RecoveryWriteOptions = {}): Promise<RecoveryWriteResult> {
|
|
133
|
+
const tree = recoveryTreeFrom(input);
|
|
134
|
+
const f: FetchLike = opts.fetch ?? ((url, init) => fetch(url, init));
|
|
135
|
+
const base = opts.baseUrl ?? "https://bitgraph.ing";
|
|
136
|
+
const retries = opts.retries ?? 3;
|
|
137
|
+
const timeoutMs = opts.timeoutMs ?? 60_000;
|
|
138
|
+
const deadline = opts.budgetMs !== undefined ? Date.now() + opts.budgetMs : Infinity;
|
|
139
|
+
let waitMs = opts.backoffMs ?? 500;
|
|
140
|
+
const names = input.names;
|
|
141
|
+
|
|
142
|
+
const progress = new Uint8Array(tree.count);
|
|
143
|
+
if (opts.state?.progress !== undefined) progress.set(opts.state.progress.subarray(0, tree.count));
|
|
144
|
+
const salts: Record<string, string> = { ...(opts.state?.salts ?? {}) };
|
|
145
|
+
let written = 0;
|
|
146
|
+
let alreadyThere = 0;
|
|
147
|
+
let reason: string | null = null;
|
|
148
|
+
// The site takes no writes, or the budget ran out: no more requests this call.
|
|
149
|
+
let stopped = false;
|
|
150
|
+
|
|
151
|
+
const keep = async (): Promise<void> => {
|
|
152
|
+
if (opts.onState !== undefined) await opts.onState(snapshot(progress, salts));
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
/** The pending sides of the members from `cursor.at` on, sealed, up to MAX_BATCH_ENTRIES entries. */
|
|
156
|
+
async function nextBatch(cursor: { at: number }): Promise<Planned[]> {
|
|
157
|
+
const out: Planned[] = [];
|
|
158
|
+
while (cursor.at < tree.count) {
|
|
159
|
+
const i = cursor.at;
|
|
160
|
+
const sides = recoverySidesOf(tree, i).filter((s) => !recoverySideResolved(progress[i]!, s));
|
|
161
|
+
if (sides.length > 0) {
|
|
162
|
+
if (out.length + sides.length > MAX_BATCH_ENTRIES) break;
|
|
163
|
+
const { writes } = await sealRecoveryMember(tree, i, names?.[i] ?? null, sides, recoverySaltsFor(salts, i));
|
|
164
|
+
for (const w of writes) out.push({ index: i, w });
|
|
165
|
+
}
|
|
166
|
+
cursor.at++;
|
|
167
|
+
}
|
|
168
|
+
return out;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** What one operation may still take: its own limit, or what is left of the budget, whichever is less. Zero or less: the budget is spent. */
|
|
172
|
+
const remaining = (): number => Math.min(timeoutMs, deadline - Date.now());
|
|
173
|
+
|
|
174
|
+
/** POST one batch. The answers by key, or null when the request failed as a whole (the reason says how). */
|
|
175
|
+
async function post(batch: Planned[]): Promise<Map<string, Answer> | null> {
|
|
176
|
+
const allowed = remaining();
|
|
177
|
+
if (allowed <= 0) {
|
|
178
|
+
reason = "the time budget ran out";
|
|
179
|
+
stopped = true;
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
let res: Response;
|
|
183
|
+
try {
|
|
184
|
+
res = await f(`${base}/api/recovery`, {
|
|
185
|
+
method: "POST",
|
|
186
|
+
headers: { "content-type": "application/json" },
|
|
187
|
+
body: JSON.stringify({ entries: batch.map((p) => ({ key: p.w.objectKey, envelope: bytesToBase64(p.w.envelope) })) }),
|
|
188
|
+
signal: AbortSignal.timeout(allowed),
|
|
189
|
+
});
|
|
190
|
+
} catch (e) {
|
|
191
|
+
reason = `the site could not be reached (${(e as Error).message})`;
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
if (res.status === 503 || res.status === 404 || res.status === 405 || res.status === 501) {
|
|
195
|
+
// Writes off on the site (503), or a site without the route at all:
|
|
196
|
+
// nothing will land this run. Stop asking.
|
|
197
|
+
stopped = true;
|
|
198
|
+
reason = `${hostOf(base)} is not taking recovery writes yet; the tree is made, but these files cannot be found from the file alone until their entries are written`;
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
if (!res.ok) {
|
|
202
|
+
reason = `POST /api/recovery answered ${res.status}`;
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
let body: unknown;
|
|
206
|
+
try {
|
|
207
|
+
body = await res.json();
|
|
208
|
+
} catch {
|
|
209
|
+
reason = "the site's answer was not JSON";
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
const answers = new Map<string, Answer>();
|
|
213
|
+
const list = (body as { results?: unknown }).results;
|
|
214
|
+
if (Array.isArray(list)) {
|
|
215
|
+
for (const r of list as Array<{ key?: unknown; status?: unknown; envelope?: unknown }>) {
|
|
216
|
+
if (typeof r.key === "string" && typeof r.status === "string") answers.set(r.key, { status: r.status, ...(typeof r.envelope === "string" ? { envelope: r.envelope } : {}) });
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return answers;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Apply one request's answers. A deterministic key held by another member
|
|
224
|
+
* gets a salt, kept first, and its salted write goes out at once; a salted
|
|
225
|
+
* name that is held too is blocked, so this recurses at most once.
|
|
226
|
+
*/
|
|
227
|
+
async function apply(batch: Planned[], answers: Map<string, Answer>): Promise<void> {
|
|
228
|
+
const fallbacks: Planned[] = [];
|
|
229
|
+
for (const p of batch) {
|
|
230
|
+
const a = answers.get(p.w.objectKey);
|
|
231
|
+
const bits = RECOVERY_SIDE_BITS[p.w.side];
|
|
232
|
+
if (a?.status === "created") {
|
|
233
|
+
progress[p.index]! |= bits.kept;
|
|
234
|
+
written++;
|
|
235
|
+
} else if (a?.status === "exists" && a.envelope !== undefined) {
|
|
236
|
+
const stored = base64ToBytes(a.envelope);
|
|
237
|
+
if (stored !== null && (await existingEntryHoldsMember(p.w.digest, p.w.objectKey, stored, p.w.plaintext))) {
|
|
238
|
+
progress[p.index]! |= bits.kept;
|
|
239
|
+
alreadyThere++;
|
|
240
|
+
} else if (p.w.salted || opts.saltedFallback === false) {
|
|
241
|
+
progress[p.index]! |= bits.blocked;
|
|
242
|
+
} else {
|
|
243
|
+
fallbacks.push(p);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
// "conflict", "error", or no answer: pending for the next round.
|
|
247
|
+
}
|
|
248
|
+
if (fallbacks.length === 0 || stopped) return;
|
|
249
|
+
// Another member holds these keys. Before a fresh salt, the address is
|
|
250
|
+
// read: an entry of this member may already sit there under a salted name
|
|
251
|
+
// from an earlier run whose salt was not kept. Found, it counts and its
|
|
252
|
+
// salt is kept now; an address that cannot be read leaves the side
|
|
253
|
+
// pending (nothing is guessed, no second copy is written).
|
|
254
|
+
const fresh: Planned[] = [];
|
|
255
|
+
for (const p of fallbacks) {
|
|
256
|
+
const allowed = remaining();
|
|
257
|
+
if (allowed <= 0) {
|
|
258
|
+
reason = "the time budget ran out";
|
|
259
|
+
stopped = true;
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
let own: Awaited<ReturnType<typeof findOwnRecoveryEntry>>;
|
|
263
|
+
try {
|
|
264
|
+
own = await findOwnRecoveryEntry(p.w.digest, p.w.plaintext, f, { baseUrl: base, timeoutMs: allowed });
|
|
265
|
+
} catch (e) {
|
|
266
|
+
reason = `the address could not be read before a salted write (${(e as Error).message})`;
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
if (own !== null) {
|
|
270
|
+
progress[p.index]! |= RECOVERY_SIDE_BITS[p.w.side].kept;
|
|
271
|
+
alreadyThere++;
|
|
272
|
+
if (own.salt !== null) salts[recoverySaltKey(p.index, p.w.side)] = bytesToBase64(own.salt);
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
fresh.push(p);
|
|
276
|
+
}
|
|
277
|
+
if (fresh.length === 0) return;
|
|
278
|
+
for (const p of fresh) salts[recoverySaltKey(p.index, p.w.side)] ??= bytesToBase64(newRecoverySalt());
|
|
279
|
+
await keep();
|
|
280
|
+
const resealed: Planned[] = [];
|
|
281
|
+
for (const p of fresh) {
|
|
282
|
+
const { writes } = await sealRecoveryMember(tree, p.index, names?.[p.index] ?? null, [p.w.side], recoverySaltsFor(salts, p.index));
|
|
283
|
+
for (const w of writes) resealed.push({ index: p.index, w });
|
|
284
|
+
}
|
|
285
|
+
const again = await post(resealed);
|
|
286
|
+
if (again !== null) await apply(resealed, again);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
for (let round = 0; round <= retries && !stopped; round++) {
|
|
290
|
+
if (round > 0) {
|
|
291
|
+
if (Date.now() + waitMs > deadline) {
|
|
292
|
+
reason = "the time budget ran out";
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
await sleep(waitMs);
|
|
296
|
+
waitMs *= 2;
|
|
297
|
+
}
|
|
298
|
+
const cursor = { at: 0 };
|
|
299
|
+
let sentAny = false;
|
|
300
|
+
while (!stopped) {
|
|
301
|
+
if (Date.now() > deadline) {
|
|
302
|
+
reason = "the time budget ran out";
|
|
303
|
+
stopped = true;
|
|
304
|
+
break;
|
|
305
|
+
}
|
|
306
|
+
const batch = await nextBatch(cursor);
|
|
307
|
+
if (batch.length === 0) break;
|
|
308
|
+
sentAny = true;
|
|
309
|
+
const answers = await post(batch);
|
|
310
|
+
if (answers !== null) await apply(batch, answers);
|
|
311
|
+
await keep();
|
|
312
|
+
}
|
|
313
|
+
if (!sentAny) break;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
let entries = 0;
|
|
317
|
+
let kept = 0;
|
|
318
|
+
let salted = 0;
|
|
319
|
+
let blocked = 0;
|
|
320
|
+
let pending = 0;
|
|
321
|
+
for (let i = 0; i < tree.count; i++) {
|
|
322
|
+
for (const side of recoverySidesOf(tree, i)) {
|
|
323
|
+
entries++;
|
|
324
|
+
const state = recoverySideState(progress[i]!, side);
|
|
325
|
+
if (state === "kept") {
|
|
326
|
+
kept++;
|
|
327
|
+
if (salts[recoverySaltKey(i, side)] !== undefined) salted++;
|
|
328
|
+
} else if (state === "blocked") blocked++;
|
|
329
|
+
else pending++;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
if (pending > 0 && reason === null) reason = "some entries were not written after every retry";
|
|
333
|
+
return { entries, kept, written, alreadyThere, salted, blocked, pending, reason: pending > 0 ? reason : null, state: snapshot(progress, salts), done: pending === 0 };
|
|
334
|
+
}
|