@coffre/vault 0.14.4 → 0.15.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.
Files changed (45) hide show
  1. package/README.md +6 -4
  2. package/dist/cloudflare.d.ts +343 -9
  3. package/dist/cloudflare.js +802 -6
  4. package/dist/{index-CIq4i_KT.d.ts → index-DQ-FKVF3.d.ts} +4 -1
  5. package/dist/index.d.ts +1 -1
  6. package/dist/index.js +1 -1
  7. package/dist/log-entry-BYWqj3O5.d.ts +364 -0
  8. package/dist/node.d.ts +182 -7
  9. package/dist/node.js +358 -16
  10. package/dist/{src-NM5NsYFs.js → src-DheG92EM.js} +9 -2
  11. package/dist/vault-kmtojYnW.js +14623 -0
  12. package/package.json +4 -5
  13. package/src/bulk.ts +89 -0
  14. package/src/changes.ts +478 -0
  15. package/src/checkpointer-node.ts +342 -0
  16. package/src/checkpointer-workers.ts +514 -0
  17. package/src/checkpointer.ts +1165 -0
  18. package/src/cloudflare.ts +207 -13
  19. package/src/config.ts +15 -3
  20. package/src/credentials.postgres.ts +78 -0
  21. package/src/credentials.sqlite.ts +240 -0
  22. package/src/credentials.ts +630 -0
  23. package/src/cutover.ts +275 -0
  24. package/src/decide.ts +437 -2
  25. package/src/inputs.ts +499 -0
  26. package/src/local.ts +20 -4
  27. package/src/log-checkpointer.ts +116 -0
  28. package/src/log-entry.ts +113 -0
  29. package/src/log-replay.ts +188 -0
  30. package/src/log-store.ts +695 -0
  31. package/src/node.ts +73 -9
  32. package/src/objects.ts +141 -0
  33. package/src/reads.ts +502 -0
  34. package/src/retention.ts +229 -0
  35. package/src/snapshot.postgres.ts +39 -9
  36. package/src/snapshot.sqlite.ts +28 -3
  37. package/src/snapshot.ts +72 -12
  38. package/src/store.ts +862 -37
  39. package/src/use.postgres.ts +111 -0
  40. package/src/use.sqlite.ts +542 -0
  41. package/src/use.ts +1060 -0
  42. package/src/vault.ts +1513 -240
  43. package/src/verifier.ts +136 -0
  44. package/src/witness-workers.ts +163 -0
  45. package/dist/vault-BBdXsny3.js +0 -5077
@@ -0,0 +1,514 @@
1
+ // The 0.15 checkpointer on Workers (docs/design/audit-tree.md): one Durable
2
+ // Object for the instance, which seals the log on its alarm every 500 ms
3
+ // and keeps the signer's memory in its own storage. A write to that storage
4
+ // is held by the object's output gate until it is durable, so no message
5
+ // leaves it, the database write included, before the pair it signed is
6
+ // remembered (RememberFirst). It holds one connection to the database
7
+ // through the vault's Hyperdrive for as long as it lives: its ticks run one
8
+ // at a time, and a Durable Object may keep a connection across requests.
9
+ // Nothing it awaits is unbounded (T169): a tick past its bound stalls the
10
+ // checkpointer, its connection is cut, and the next alarm opens another; a
11
+ // change waits at most `SEAL_WAIT_MS` for its seal, then answers 503.
12
+ //
13
+ // The deployment binds it and exports it (`export const CoffreCheckpointer =
14
+ // checkpointer()`, beside `vault(…)`); the vault starts it, from each
15
+ // isolate's first call (cloudflare.ts). Asked with `fetch`, as the anchor
16
+ // and the gates are.
17
+ //
18
+ // The reads' projector (reads.ts, T166) is another object of the same class,
19
+ // named `reads` (`startProjector`), which its storage says it is: its own
20
+ // alarm, its own invocation and its own connection, so that no invocation
21
+ // ever holds two connections (a Worker that opened several at once was
22
+ // cancelled as hung, AGENTS.md), and a projection behind, or failing, delays
23
+ // no tick, no seal, no change and no use (formal/Reads.tla).
24
+ import { parseCheckpoint, splitNote, type PairNotes } from '@coffre/core/merkle';
25
+ import { createDatabase } from '@coffre/db';
26
+ import { HyperdrivePool } from '@coffre/db/hyperdrive';
27
+
28
+ import type { Sealing } from '@coffre/core/vault';
29
+
30
+ import {
31
+ Fence,
32
+ heldIn,
33
+ SEAL_WAIT_MS,
34
+ sealingOf,
35
+ TICK_BOUND_MS,
36
+ Unsealed,
37
+ WITNESS_DEADLINE_MS,
38
+ within,
39
+ type Alarm,
40
+ type Checkpointer,
41
+ type InclusionProof,
42
+ type Memory,
43
+ type SealThrough,
44
+ type Sealed,
45
+ type Tick,
46
+ type Witness,
47
+ } from './checkpointer.ts';
48
+ import { daysAround, openLogCheckpointer, reasonOf, RETAIN_EVERY_MS, TICK_MS, upkeep } from './log-checkpointer.ts';
49
+ import { Projector } from './reads.ts';
50
+ import { verifierKeys } from './verifier.ts';
51
+ import { logStore } from './log-store.ts';
52
+ import { dayOf } from './retention.ts';
53
+
54
+ /** The part of a Durable Object's state the checkpointer uses. */
55
+ export type ObjectState = {
56
+ storage: {
57
+ get<T>(key: string): Promise<T | undefined>;
58
+ put<T>(key: string, value: T): Promise<void>;
59
+ sync(): Promise<void>;
60
+ delete(keys: string[]): Promise<number>;
61
+ list<T>(options: { prefix: string; start?: string; end?: string; limit?: number }): Promise<Map<string, T>>;
62
+ getAlarm(): Promise<number | null>;
63
+ setAlarm(scheduledTime: number): Promise<void>;
64
+ };
65
+ blockConcurrencyWhile<T>(work: () => Promise<T>): Promise<T>;
66
+ };
67
+
68
+ /** A Durable Object binding: `env.CHECKPOINTER`, `env.WITNESS`. */
69
+ export type ObjectNamespace = {
70
+ idFromName(name: string): unknown;
71
+ get(id: unknown): { fetch(request: Request): Promise<Response> };
72
+ };
73
+
74
+ export type CheckpointerSettings = {
75
+ /** The vault's Hyperdrive, its own login. */
76
+ hyperdrive: { readonly connectionString: string };
77
+ /** The vault's signing seed. */
78
+ seed: Uint8Array;
79
+ witnesses?: readonly Witness[];
80
+ /** How long its connection may sit idle before it closes; the next query opens it again. Ticking, it never is. */
81
+ closeIdleMs?: number;
82
+ /** Full days of uses kept before today's; null keeps every day (retention.ts). */
83
+ retainDays?: number | null;
84
+ /** `TICK_BOUND_MS`, which also bounds an open and the next days made without one: for the tests. */
85
+ tickBoundMs?: number;
86
+ };
87
+
88
+ /**
89
+ * The signer's memory in the object's storage: one key, the pair's record,
90
+ * durable before `remember` returns. The output gate holds a Response and a
91
+ * new connection until the write is, but not a query on a connection
92
+ * already open, as the database write is: `sync()` makes RememberFirst hold
93
+ * without leaning on it (Fable's review).
94
+ */
95
+ export function durableMemory(storage: ObjectState['storage']): Memory {
96
+ return {
97
+ last: async () => (await storage.get<string>('pair')) ?? null,
98
+ async remember(record) {
99
+ await storage.put('pair', record);
100
+ await storage.sync();
101
+ },
102
+ };
103
+ }
104
+
105
+ /** How long alarms are kept. */
106
+ const ALARMS_KEPT_MS = 30 * 86_400_000;
107
+ /**
108
+ * How long the object waits on an open, which reads the newest pair and the trees' edges, and on the next days of
109
+ * uses made without it (T169): as long as a tick.
110
+ */
111
+ const OPEN_BOUND_MS = TICK_BOUND_MS;
112
+ /** How long it waits on the log's upkeep after a tick: retention drops a day's partition and its tiles, hourly. */
113
+ const UPKEEP_BOUND_MS = 60_000;
114
+ /** How long the vault waits on the object for anything but a seal: a start, its status, the pair it remembers. */
115
+ const ASK_BOUND_MS = 2000;
116
+ /** How long the vault waits on the object for a seal: its own bound, and the witnesses', and a second more. */
117
+ const SEAL_ASK_BOUND_MS = SEAL_WAIT_MS + WITNESS_DEADLINE_MS + 1000;
118
+ /** An alarm's key: its time, then random bytes, so that two at one millisecond never share one, evicted or not. */
119
+ const alarmKey = (at: number, suffix = '') => `alarm:${String(at).padStart(16, '0')}:${suffix}`;
120
+
121
+ /** The Durable Object class the deployment binds, configured from `env` as the vault is. */
122
+ export function checkpointerObject<Env>(settings: (env: Env) => CheckpointerSettings | Promise<CheckpointerSettings>) {
123
+ return class CoffreCheckpointer {
124
+ readonly #state: ObjectState;
125
+ readonly #env: Env;
126
+ #checkpointer: Checkpointer | null = null;
127
+ #pool: HyperdrivePool | null = null;
128
+ /** The log's upkeep after each tick (log-checkpointer.ts), with the checkpointer it was opened with. */
129
+ #upkeep: (() => Promise<void>) | null = null;
130
+ /** Whether this object is the reads' projector, as its storage says (`startProjector`); asked once. */
131
+ #projects: boolean | null = null;
132
+ /** The projector, on a connection of its own for as long as the object lives, as the checkpointer's. */
133
+ #projector: Projector | null = null;
134
+ /** The day the next days of uses were last made for without the checkpointer (`#makeDays`), when it last tried, and its failure. */
135
+ #daysFor: string | null = null;
136
+ #daysTried = -Infinity;
137
+ #daysFailed: string | null = null;
138
+ /** Whether the upkeep after a tick that held runs now (`alarm`), which readiness counts as the tick's time. */
139
+ #upkeeping = false;
140
+
141
+ /**
142
+ * The last tick as readiness reads it: during the upkeep after it, as if it had just ended. What stood since, a seal's
143
+ * tick that stalled or was refused meanwhile, readiness reads from the checkpointer itself (`standing`).
144
+ */
145
+ #lastSeen(): { at: number; failures: readonly string[] } | null {
146
+ const last = this.#ticks.at(-1) ?? null;
147
+ return last === null || !this.#upkeeping ? last : { at: Date.now(), failures: last.failures };
148
+ }
149
+
150
+ /** The last ticks, newest last, with their wall time in the object: `status`. */
151
+ readonly #ticks: (Tick & { wallMs: number; at: number })[] = [];
152
+
153
+ constructor(state: ObjectState, env: Env) {
154
+ this.#state = state;
155
+ this.#env = env;
156
+ }
157
+
158
+ /** The checkpointer, opened once, every other request held meanwhile, so that two never open it side by side. */
159
+ /**
160
+ * Null when it cannot be opened: `open()` refuses the gravest states the log has (entries the signer sealed lost,
161
+ * another pair at its place, time run back), and each is an alarm, kept once while it stands. Never thrown from
162
+ * inside the gate, which would reset the object.
163
+ */
164
+ async #open(): Promise<Checkpointer | null> {
165
+ if (this.#checkpointer !== null) return this.#checkpointer;
166
+ return this.#state.blockConcurrencyWhile(async () => {
167
+ if (this.#checkpointer !== null) return this.#checkpointer;
168
+ let pool: HyperdrivePool | null = null;
169
+ // Bounded (T169): an open that stalls is fenced and its connection cut, so that the gate lets go well before the
170
+ // platform's 30 s would reset the object.
171
+ const fence = new Fence();
172
+ try {
173
+ const s = await settings(this.#env);
174
+ const opening = new HyperdrivePool(s.hyperdrive.connectionString, { closeIdleMs: s.closeIdleMs ?? 10_000 });
175
+ pool = opening;
176
+ const store = logStore(createDatabase(pool));
177
+ const checkpointer = await within(openLogCheckpointer({
178
+ store,
179
+ seed: s.seed,
180
+ memory: durableMemory(this.#state.storage),
181
+ witnesses: s.witnesses,
182
+ record: (alarm) => this.#record(alarm),
183
+ fence,
184
+ ...(s.tickBoundMs === undefined ? {} : { tickBoundMs: s.tickBoundMs }),
185
+ }), s.tickBoundMs ?? OPEN_BOUND_MS, 'the open', () => {
186
+ void fence.close('its open stalled');
187
+ opening.destroy();
188
+ });
189
+ this.#checkpointer = checkpointer;
190
+ // Behind the checkpointer's fence, as everything it does: nothing of it runs once another opens.
191
+ this.#upkeep = upkeep(checkpointer, fence.store(store), {
192
+ keepDays: s.retainDays ?? null,
193
+ seed: s.seed,
194
+ alert: (message) => {
195
+ console.warn(`coffre audit: ${message}`);
196
+ void this.#record({ at: Date.now(), message }).catch((error: Error) => console.warn(`coffre audit: the alarm could not be kept: ${error.message}`));
197
+ },
198
+ });
199
+ } catch (error) {
200
+ await fence.close('its open failed');
201
+ pool?.destroy();
202
+ await this.#failure(`the checkpointer cannot open: ${reasonOf(error)}`);
203
+ return null;
204
+ }
205
+ this.#pool = pool;
206
+ return this.#checkpointer;
207
+ });
208
+ }
209
+
210
+ /** The failure that stands, alarmed once: a refused open, a tick that threw. */
211
+ #failed: string | null = null;
212
+
213
+ async #failure(message: string): Promise<void> {
214
+ if (message === this.#failed) return;
215
+ this.#failed = message;
216
+ console.warn(`coffre audit: ${message}`);
217
+ await this.#record({ at: Date.now(), message }).catch((error: Error) => console.warn(`coffre audit: the alarm could not be kept: ${error.message}`));
218
+ }
219
+
220
+ /**
221
+ * Nothing else delivered meanwhile, and the old checkpointer closed, fenced, its tick in flight ended or stalled,
222
+ * before the next request may open another: a change still waiting on the old one can tick it no more, so two
223
+ * never sign at once. Its connection is cut first, so that what it still awaits fails now rather than whenever the
224
+ * database answers (T169); a statement it had sent may still land, which the fence's rules allow.
225
+ */
226
+ async #reset(): Promise<void> {
227
+ await this.#state.blockConcurrencyWhile(async () => {
228
+ const [old, pool] = [this.#checkpointer, this.#pool];
229
+ this.#checkpointer = null;
230
+ this.#pool = null;
231
+ this.#upkeep = null;
232
+ pool?.destroy();
233
+ await old?.close();
234
+ });
235
+ }
236
+
237
+ async #record(alarm: Alarm): Promise<void> {
238
+ const storage = this.#state.storage;
239
+ await storage.put(alarmKey(alarm.at, crypto.randomUUID()), alarm.message);
240
+ const old = await storage.list<string>({ prefix: 'alarm:', end: alarmKey(alarm.at - ALARMS_KEPT_MS), limit: 128 });
241
+ if (old.size > 0) await storage.delete([...old.keys()]);
242
+ }
243
+
244
+ /**
245
+ * The next days of uses, made once a day when the checkpointer cannot open or tick, over a connection of their own:
246
+ * a use needs its day's partition whatever the audit's state, and making one needs no signer.
247
+ */
248
+ async #makeDays(): Promise<void> {
249
+ const now = Date.now();
250
+ const today = dayOf(now);
251
+ // Once a day when it works; when it fails, an hour later (DAYS_AHEAD days of slack), alarmed once while it stands
252
+ // apart from the checkpointer's own failures, so that neither hides the other.
253
+ if (this.#daysFor === today || now - this.#daysTried < RETAIN_EVERY_MS) return;
254
+ this.#daysTried = now;
255
+ let pool: HyperdrivePool | null = null;
256
+ try {
257
+ const s = await settings(this.#env);
258
+ const making = new HyperdrivePool(s.hyperdrive.connectionString, { closeIdleMs: 1000 });
259
+ pool = making;
260
+ await within(logStore(createDatabase(making)).makeDays(daysAround(now)), s.tickBoundMs ?? OPEN_BOUND_MS, 'making them', () => making.destroy());
261
+ this.#daysFor = today;
262
+ this.#daysFailed = null;
263
+ } catch (error) {
264
+ const message = `the next days of uses were not made: ${reasonOf(error)}`;
265
+ if (message !== this.#daysFailed) {
266
+ console.warn(`coffre audit: ${message}`);
267
+ await this.#record({ at: now, message }).catch((e: Error) => console.warn(`coffre audit: the alarm could not be kept: ${e.message}`));
268
+ }
269
+ this.#daysFailed = message;
270
+ } finally {
271
+ pool?.destroy();
272
+ }
273
+ }
274
+
275
+ /**
276
+ * The projector's run: some steps of `projectReads`, then the next alarm, a tick later, or at once while a run
277
+ * leaves sealed uses to project. A failure is an alarm, said once while it stands (`Projector`), and the next
278
+ * tick tries again, on the same connection, which postgres.js opens again when it is lost.
279
+ */
280
+ async #project(): Promise<void> {
281
+ let more = false;
282
+ try {
283
+ if (this.#projector === null) {
284
+ const s = await settings(this.#env);
285
+ const pool = new HyperdrivePool(s.hyperdrive.connectionString, { closeIdleMs: s.closeIdleMs ?? 10_000 });
286
+ const store = logStore(createDatabase(pool));
287
+ this.#projector = new Projector(store, await verifierKeys([s.seed]), (message) => {
288
+ console.warn(`coffre audit: ${message}`);
289
+ void this.#record({ at: Date.now(), message }).catch((error: Error) => console.warn(`coffre audit: the alarm could not be kept: ${error.message}`));
290
+ });
291
+ }
292
+ more = await this.#projector.run();
293
+ } catch (error) {
294
+ this.#projector = null;
295
+ await this.#failure(`the reads' projection cannot open: ${reasonOf(error)}`);
296
+ } finally {
297
+ await this.#state.storage.setAlarm(Date.now() + (more ? 0 : TICK_MS));
298
+ }
299
+ }
300
+
301
+ async #isProjector(): Promise<boolean> {
302
+ return (this.#projects ??= (await this.#state.storage.get<string>('role')) === 'reads');
303
+ }
304
+
305
+ async alarm(): Promise<void> {
306
+ if (await this.#isProjector()) return this.#project();
307
+ // A window sealed only in part (T169, WINDOW_MAX): the next alarm at once, its upkeep once it has caught up.
308
+ let more = false;
309
+ try {
310
+ const checkpointer = await this.#open();
311
+ if (checkpointer === null) return await this.#makeDays();
312
+ const t0 = performance.now();
313
+ // Bounded by the checkpointer itself (TICK_BOUND_MS): one that stalled throws, and is replaced below.
314
+ const t = await checkpointer.tick();
315
+ this.#ticks.push({ ...t, wallMs: performance.now() - t0, at: Date.now() });
316
+ if (this.#ticks.length > 64) this.#ticks.shift();
317
+ this.#failed = null;
318
+ more = t.more;
319
+ // On the same connection, cut past its bound: the checkpointer goes with it. Meanwhile readiness counts the
320
+ // checkpointer as sealing (review by Fable): its last tick was good, and the upkeep is bounded.
321
+ const pool = this.#pool;
322
+ if (this.#upkeep !== null && !more) {
323
+ this.#upkeeping = true;
324
+ try {
325
+ await within(this.#upkeep(), UPKEEP_BOUND_MS, "the log's upkeep", () => pool?.destroy());
326
+ } finally {
327
+ this.#upkeeping = false;
328
+ const last = this.#ticks.at(-1);
329
+ if (last !== undefined) last.at = Date.now();
330
+ }
331
+ }
332
+ } catch (error) {
333
+ // A write that failed may be a tamper signal (a tile changed under the signer): an alarm, kept once.
334
+ await this.#failure(`a checkpoint failed: ${reasonOf(error)}`);
335
+ await this.#reset();
336
+ await this.#makeDays();
337
+ } finally {
338
+ await this.#state.storage.setAlarm(Date.now() + (more ? 0 : TICK_MS));
339
+ }
340
+ }
341
+
342
+ async fetch(request: Request): Promise<Response> {
343
+ try {
344
+ const call = (await request.json()) as { op: 'start' } | { op: 'project' } | { op: 'seal'; number: string; deadline?: number } | { op: 'alarms'; since?: number } | { op: 'status' } | { op: 'remembered' };
345
+ if (call.op === 'project') {
346
+ // The reads' projector from now on, its role kept for when it wakes evicted, and its alarm set.
347
+ if (!(await this.#isProjector())) {
348
+ await this.#state.storage.put('role', 'reads');
349
+ this.#projects = true;
350
+ }
351
+ if ((await this.#state.storage.getAlarm()) === null) await this.#state.storage.setAlarm(Date.now() + TICK_MS);
352
+ return Response.json({ ok: true });
353
+ }
354
+ if (await this.#isProjector() && call.op !== 'alarms') return new Response('the reads\' projector seals nothing', { status: 400 });
355
+ if (call.op === 'start') {
356
+ // The alarm first, whether it opens or not: refused, each tick tries again and makes the next days of uses meanwhile.
357
+ if ((await this.#state.storage.getAlarm()) === null) await this.#state.storage.setAlarm(Date.now() + TICK_MS);
358
+ if ((await this.#open()) === null) return Response.json({ error: this.#failed }, { status: 503 });
359
+ return Response.json({ ok: true });
360
+ }
361
+ if (call.op === 'seal') {
362
+ // A 503 is a seal that did not come in time (T169): the change is in force, and asked again, sealed.
363
+ const checkpointer = await this.#open();
364
+ if (checkpointer === null) return Response.json({ error: new Unsealed(BigInt(call.number), this.#failed ?? 'the checkpointer cannot open').message }, { status: 503 });
365
+ try {
366
+ return Response.json(sealedToJson(await checkpointer.sealThrough(BigInt(call.number), call.deadline)));
367
+ } catch (error) {
368
+ if (error instanceof Unsealed) return Response.json({ error: error.message }, { status: 503 });
369
+ throw error;
370
+ }
371
+ }
372
+ if (call.op === 'status') {
373
+ // The object's own view: after an eviction, nothing until its next tick opens it again (`open: false`); the
374
+ // failure that stands, if any, which readiness reads with the last tick's time (`sealingOf`).
375
+ const head = this.#checkpointer?.pair.pair;
376
+ return Response.json({
377
+ open: this.#checkpointer !== null,
378
+ failed: this.#failed,
379
+ // By the object's clock, as its ticks' times are.
380
+ sealing: sealingOf({ open: this.#checkpointer !== null, failed: this.#failed ?? this.#checkpointer?.standing ?? null, last: this.#lastSeen() }),
381
+ upto: head === undefined ? null : String(head.upto), sizes: head === undefined ? null : { changes: head.changes.size, uses: head.uses.size },
382
+ ticks: this.#ticks.map((t) => ({ ...t, upto: String(t.upto) })),
383
+ });
384
+ }
385
+ if (call.op === 'remembered') {
386
+ // The pair the signer remembers, read from the object's storage, opened or not; a record that is no pair, refused.
387
+ return Response.json({ held: heldIn(await durableMemory(this.#state.storage).last()) });
388
+ }
389
+ if (call.op === 'alarms') {
390
+ // A page at a time, each from the last key of the one before (which it lists again): a list is 1,000 keys at most.
391
+ const alarms: Alarm[] = [];
392
+ let start = alarmKey(call.since ?? 0);
393
+ let last: string | null = null;
394
+ for (;;) {
395
+ const page = await this.#state.storage.list<string>({ prefix: 'alarm:', start, limit: 1000 });
396
+ for (const [key, message] of page) if (key !== last) alarms.push({ at: Number(key.split(':')[1]), message });
397
+ if (page.size < 1000) break;
398
+ last = start = [...page.keys()].at(-1)!;
399
+ }
400
+ return Response.json(alarms);
401
+ }
402
+ return new Response('unknown op', { status: 400 });
403
+ } catch (error) {
404
+ return Response.json({ error: (error as Error).message }, { status: 500 });
405
+ }
406
+ }
407
+ };
408
+ }
409
+
410
+ /**
411
+ * Asks the checkpointer's object, as `durableSealThrough(env.CHECKPOINTER)` and `startCheckpointer(env.CHECKPOINTER)`
412
+ * do, for at most `ms` (T169): an object that does not answer, its gate held or its isolate wedged, fails the ask
413
+ * rather than the call that asked.
414
+ */
415
+ async function ask<T>(namespace: ObjectNamespace, name: string, call: object, ms = ASK_BOUND_MS): Promise<T> {
416
+ if (typeof namespace?.idFromName !== 'function') throw new Error('there is no CHECKPOINTER binding: bind the CoffreCheckpointer Durable Object to the vault');
417
+ const asked = (async () => {
418
+ const response = await namespace.get(namespace.idFromName(name)).fetch(new Request('https://checkpointer/', { method: 'POST', body: JSON.stringify(call) }));
419
+ return { response, body: (await response.json()) as T & { error?: string } };
420
+ })();
421
+ const { response, body } = await within(asked, ms, "the checkpointer's object");
422
+ // Its status kept: a 503 is an object that refused to open, its alarm set (`start`).
423
+ if (!response.ok) throw Object.assign(new Error(`the checkpointer failed: ${body.error ?? response.status}`), { status: response.status });
424
+ return body;
425
+ }
426
+
427
+ /**
428
+ * The change path's seam on Workers (plan PR 4): seal through the checkpointer's object. Its 503, or no answer in
429
+ * time, is `Unsealed` (T169): the change is committed and in force, and the app says 503, try again.
430
+ */
431
+ export function durableSealThrough(namespace: ObjectNamespace, name = 'checkpointer'): SealThrough {
432
+ return async (number, deadline) => {
433
+ let sealed: SealedJson;
434
+ try {
435
+ sealed = await ask<SealedJson>(namespace, name, { op: 'seal', number: String(number), deadline }, SEAL_ASK_BOUND_MS);
436
+ } catch (error) {
437
+ // The object's 503 says so in core's words; an object that did not answer, or not in time, is the same. Its 500 is
438
+ // a seal refused (an entry that fails its MAC, a database at another pair): an alarm, kept by the object.
439
+ if ((error as { status?: number }).status === undefined) throw new Unsealed(number, `the checkpointer's object did not answer: ${(error as Error).message}`, error);
440
+ throw error;
441
+ }
442
+ return sealedFromJson(sealed);
443
+ };
444
+ }
445
+
446
+ /** Starts the object's ticks, once; again is harmless. Throws when it cannot open, its alarm set all the same. */
447
+ export async function startCheckpointer(namespace: ObjectNamespace, name = 'checkpointer'): Promise<void> {
448
+ await ask(namespace, name, { op: 'start' });
449
+ }
450
+
451
+ /**
452
+ * Starts the reads' projector (reads.ts, T166): the object of the same class named `name`, which projects from then
453
+ * on, on its own alarm. Again is harmless.
454
+ */
455
+ export async function startProjector(namespace: ObjectNamespace, name = 'reads'): Promise<void> {
456
+ await ask(namespace, name, { op: 'project' });
457
+ }
458
+
459
+ /** The object's head and its last ticks, each with its barrier, write and wall time. */
460
+ export function checkpointerStatus(namespace: ObjectNamespace, name = 'checkpointer'): Promise<{ open: boolean; failed: string | null; sealing: Sealing; upto: string | null; sizes: Record<'changes' | 'uses', number> | null; ticks: (Omit<Tick, 'upto'> & { upto: string; wallMs: number; at: number })[] }> {
461
+ return ask(namespace, name, { op: 'status' });
462
+ }
463
+
464
+ /** Whether the object seals (`Vault.sealing`, T169): its own view, or, if it does not answer within 2 s, that it does not. */
465
+ export function durableSealing(namespace: ObjectNamespace, name = 'checkpointer'): () => Promise<Sealing> {
466
+ return async () => {
467
+ try {
468
+ return (await checkpointerStatus(namespace, name)).sealing;
469
+ } catch (error) {
470
+ return { ok: false, reason: `the checkpointer's object did not answer: ${(error as Error).message}` };
471
+ }
472
+ };
473
+ }
474
+
475
+ /**
476
+ * The pair the signer remembers, where no database writer reaches: what the
477
+ * vault's verifier holds its walks to (`VaultOptions.heldOutside`).
478
+ */
479
+ export function durableHeldOutside(namespace: ObjectNamespace, name = 'checkpointer'): () => Promise<PairNotes[]> {
480
+ return async () => {
481
+ const { held } = await ask<{ held: PairNotes[] }>(namespace, name, { op: 'remembered' });
482
+ return held;
483
+ };
484
+ }
485
+
486
+ /** The alarms the object kept since `since` (ms). */
487
+ export function checkpointerAlarms(namespace: ObjectNamespace, since = 0, name = 'checkpointer'): Promise<Alarm[]> {
488
+ return ask<Alarm[]>(namespace, name, { op: 'alarms', since });
489
+ }
490
+
491
+ type SealedJson = {
492
+ notes: PairNotes; witnessed: boolean | null; failures: string[]; cosigned: PairNotes[];
493
+ proof: { number: string; tree: InclusionProof['tree']; index: number; size: number; leaf: string; proof: string[]; notes: PairNotes };
494
+ };
495
+
496
+ function sealedToJson(s: Sealed): SealedJson {
497
+ const p = s.proof;
498
+ return {
499
+ notes: s.notes, witnessed: s.witnessed, failures: s.failures, cosigned: s.cosigned,
500
+ proof: { number: String(p.number), tree: p.tree, index: p.index, size: p.size, leaf: p.leaf.toString('base64'), proof: p.proof.map((h) => h.toString('base64')), notes: p.notes },
501
+ };
502
+ }
503
+
504
+ /** The answer as the object sent it. Its notes are the vault's own, signed in the object: read, not verified again. */
505
+ function sealedFromJson(j: SealedJson): Sealed {
506
+ const read = (note: string) => parseCheckpoint(splitNote(note).text);
507
+ const [changes, uses] = [read(j.notes.changes), read(j.notes.uses)];
508
+ return {
509
+ pair: { upto: changes.upto, at: changes.at, floor: changes.floor, prev: changes.prev, changes, uses },
510
+ notes: j.notes, witnessed: j.witnessed, failures: j.failures, cosigned: j.cosigned,
511
+ proof: { ...j.proof, number: BigInt(j.proof.number), leaf: Buffer.from(j.proof.leaf, 'base64'), proof: j.proof.proof.map((h) => Buffer.from(h, 'base64')) },
512
+ };
513
+ }
514
+