@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.
- package/README.md +6 -4
- package/dist/cloudflare.d.ts +343 -9
- package/dist/cloudflare.js +802 -6
- package/dist/{index-CIq4i_KT.d.ts → index-DQ-FKVF3.d.ts} +4 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/log-entry-BYWqj3O5.d.ts +364 -0
- package/dist/node.d.ts +182 -7
- package/dist/node.js +358 -16
- package/dist/{src-NM5NsYFs.js → src-DheG92EM.js} +9 -2
- package/dist/vault-kmtojYnW.js +14623 -0
- package/package.json +4 -5
- package/src/bulk.ts +89 -0
- package/src/changes.ts +478 -0
- package/src/checkpointer-node.ts +342 -0
- package/src/checkpointer-workers.ts +514 -0
- package/src/checkpointer.ts +1165 -0
- package/src/cloudflare.ts +207 -13
- package/src/config.ts +15 -3
- package/src/credentials.postgres.ts +78 -0
- package/src/credentials.sqlite.ts +240 -0
- package/src/credentials.ts +630 -0
- package/src/cutover.ts +275 -0
- package/src/decide.ts +437 -2
- package/src/inputs.ts +499 -0
- package/src/local.ts +20 -4
- package/src/log-checkpointer.ts +116 -0
- package/src/log-entry.ts +113 -0
- package/src/log-replay.ts +188 -0
- package/src/log-store.ts +695 -0
- package/src/node.ts +73 -9
- package/src/objects.ts +141 -0
- package/src/reads.ts +502 -0
- package/src/retention.ts +229 -0
- package/src/snapshot.postgres.ts +39 -9
- package/src/snapshot.sqlite.ts +28 -3
- package/src/snapshot.ts +72 -12
- package/src/store.ts +862 -37
- package/src/use.postgres.ts +111 -0
- package/src/use.sqlite.ts +542 -0
- package/src/use.ts +1060 -0
- package/src/vault.ts +1513 -240
- package/src/verifier.ts +136 -0
- package/src/witness-workers.ts +163 -0
- package/dist/vault-BBdXsny3.js +0 -5077
package/dist/cloudflare.js
CHANGED
|
@@ -1,8 +1,638 @@
|
|
|
1
|
-
import { a as verifyCheckpoint,
|
|
2
|
-
import { n as prepareVault, t as openVault } from "./vault-
|
|
1
|
+
import { a as verifyCheckpoint, c as resolveVaultConfig, i as checkpointMessage, n as KekUnavailableError, r as awsKms, s as derivedSigningKey, t as KekBadClaimError } from "./src-DheG92EM.js";
|
|
2
|
+
import { _ as Unsealed, b as sealingOf, c as openLogCheckpointer, d as dayOf, f as Projector, g as TICK_BOUND_MS, i as logStore, l as reasonOf, m as SEAL_WAIT_MS, n as prepareVault, p as Fence, r as verifierKeys, s as daysAround, t as openVault, u as upkeep, v as WITNESS_DEADLINE_MS, x as within, y as heldIn } from "./vault-kmtojYnW.js";
|
|
3
3
|
import { createDatabase } from "@coffre/db";
|
|
4
4
|
import { HyperdrivePool, postgres } from "@coffre/db/hyperdrive";
|
|
5
5
|
import { WorkerEntrypoint } from "cloudflare:workers";
|
|
6
|
+
import { decodePair, encodePair, noteSigner, openPair, pairVerifiers, parseCheckpoint, splitNote, witnessPair } from "@coffre/core/merkle";
|
|
7
|
+
//#region src/checkpointer-workers.ts
|
|
8
|
+
/**
|
|
9
|
+
* The signer's memory in the object's storage: one key, the pair's record,
|
|
10
|
+
* durable before `remember` returns. The output gate holds a Response and a
|
|
11
|
+
* new connection until the write is, but not a query on a connection
|
|
12
|
+
* already open, as the database write is: `sync()` makes RememberFirst hold
|
|
13
|
+
* without leaning on it (Fable's review).
|
|
14
|
+
*/
|
|
15
|
+
function durableMemory(storage) {
|
|
16
|
+
return {
|
|
17
|
+
last: async () => await storage.get("pair") ?? null,
|
|
18
|
+
async remember(record) {
|
|
19
|
+
await storage.put("pair", record);
|
|
20
|
+
await storage.sync();
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/** How long alarms are kept. */
|
|
25
|
+
const ALARMS_KEPT_MS = 2592e6;
|
|
26
|
+
/**
|
|
27
|
+
* How long the object waits on an open, which reads the newest pair and the trees' edges, and on the next days of
|
|
28
|
+
* uses made without it (T169): as long as a tick.
|
|
29
|
+
*/
|
|
30
|
+
const OPEN_BOUND_MS = TICK_BOUND_MS;
|
|
31
|
+
/** How long it waits on the log's upkeep after a tick: retention drops a day's partition and its tiles, hourly. */
|
|
32
|
+
const UPKEEP_BOUND_MS = 6e4;
|
|
33
|
+
/** How long the vault waits on the object for anything but a seal: a start, its status, the pair it remembers. */
|
|
34
|
+
const ASK_BOUND_MS = 2e3;
|
|
35
|
+
/** How long the vault waits on the object for a seal: its own bound, and the witnesses', and a second more. */
|
|
36
|
+
const SEAL_ASK_BOUND_MS = SEAL_WAIT_MS + WITNESS_DEADLINE_MS + 1e3;
|
|
37
|
+
/** An alarm's key: its time, then random bytes, so that two at one millisecond never share one, evicted or not. */
|
|
38
|
+
const alarmKey = (at, suffix = "") => `alarm:${String(at).padStart(16, "0")}:${suffix}`;
|
|
39
|
+
/** The Durable Object class the deployment binds, configured from `env` as the vault is. */
|
|
40
|
+
function checkpointerObject(settings) {
|
|
41
|
+
return class CoffreCheckpointer {
|
|
42
|
+
#state;
|
|
43
|
+
#env;
|
|
44
|
+
#checkpointer = null;
|
|
45
|
+
#pool = null;
|
|
46
|
+
/** The log's upkeep after each tick (log-checkpointer.ts), with the checkpointer it was opened with. */
|
|
47
|
+
#upkeep = null;
|
|
48
|
+
/** Whether this object is the reads' projector, as its storage says (`startProjector`); asked once. */
|
|
49
|
+
#projects = null;
|
|
50
|
+
/** The projector, on a connection of its own for as long as the object lives, as the checkpointer's. */
|
|
51
|
+
#projector = null;
|
|
52
|
+
/** The day the next days of uses were last made for without the checkpointer (`#makeDays`), when it last tried, and its failure. */
|
|
53
|
+
#daysFor = null;
|
|
54
|
+
#daysTried = -Infinity;
|
|
55
|
+
#daysFailed = null;
|
|
56
|
+
/** Whether the upkeep after a tick that held runs now (`alarm`), which readiness counts as the tick's time. */
|
|
57
|
+
#upkeeping = false;
|
|
58
|
+
/**
|
|
59
|
+
* The last tick as readiness reads it: during the upkeep after it, as if it had just ended. What stood since, a seal's
|
|
60
|
+
* tick that stalled or was refused meanwhile, readiness reads from the checkpointer itself (`standing`).
|
|
61
|
+
*/
|
|
62
|
+
#lastSeen() {
|
|
63
|
+
const last = this.#ticks.at(-1) ?? null;
|
|
64
|
+
return last === null || !this.#upkeeping ? last : {
|
|
65
|
+
at: Date.now(),
|
|
66
|
+
failures: last.failures
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/** The last ticks, newest last, with their wall time in the object: `status`. */
|
|
70
|
+
#ticks = [];
|
|
71
|
+
constructor(state, env) {
|
|
72
|
+
this.#state = state;
|
|
73
|
+
this.#env = env;
|
|
74
|
+
}
|
|
75
|
+
/** The checkpointer, opened once, every other request held meanwhile, so that two never open it side by side. */
|
|
76
|
+
/**
|
|
77
|
+
* Null when it cannot be opened: `open()` refuses the gravest states the log has (entries the signer sealed lost,
|
|
78
|
+
* another pair at its place, time run back), and each is an alarm, kept once while it stands. Never thrown from
|
|
79
|
+
* inside the gate, which would reset the object.
|
|
80
|
+
*/
|
|
81
|
+
async #open() {
|
|
82
|
+
if (this.#checkpointer !== null) return this.#checkpointer;
|
|
83
|
+
return this.#state.blockConcurrencyWhile(async () => {
|
|
84
|
+
if (this.#checkpointer !== null) return this.#checkpointer;
|
|
85
|
+
let pool = null;
|
|
86
|
+
const fence = new Fence();
|
|
87
|
+
try {
|
|
88
|
+
const s = await settings(this.#env);
|
|
89
|
+
const opening = new HyperdrivePool(s.hyperdrive.connectionString, { closeIdleMs: s.closeIdleMs ?? 1e4 });
|
|
90
|
+
pool = opening;
|
|
91
|
+
const store = logStore(createDatabase(pool));
|
|
92
|
+
const checkpointer = await within(openLogCheckpointer({
|
|
93
|
+
store,
|
|
94
|
+
seed: s.seed,
|
|
95
|
+
memory: durableMemory(this.#state.storage),
|
|
96
|
+
witnesses: s.witnesses,
|
|
97
|
+
record: (alarm) => this.#record(alarm),
|
|
98
|
+
fence,
|
|
99
|
+
...s.tickBoundMs === void 0 ? {} : { tickBoundMs: s.tickBoundMs }
|
|
100
|
+
}), s.tickBoundMs ?? OPEN_BOUND_MS, "the open", () => {
|
|
101
|
+
fence.close("its open stalled");
|
|
102
|
+
opening.destroy();
|
|
103
|
+
});
|
|
104
|
+
this.#checkpointer = checkpointer;
|
|
105
|
+
this.#upkeep = upkeep(checkpointer, fence.store(store), {
|
|
106
|
+
keepDays: s.retainDays ?? null,
|
|
107
|
+
seed: s.seed,
|
|
108
|
+
alert: (message) => {
|
|
109
|
+
console.warn(`coffre audit: ${message}`);
|
|
110
|
+
this.#record({
|
|
111
|
+
at: Date.now(),
|
|
112
|
+
message
|
|
113
|
+
}).catch((error) => console.warn(`coffre audit: the alarm could not be kept: ${error.message}`));
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
} catch (error) {
|
|
117
|
+
await fence.close("its open failed");
|
|
118
|
+
pool?.destroy();
|
|
119
|
+
await this.#failure(`the checkpointer cannot open: ${reasonOf(error)}`);
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
this.#pool = pool;
|
|
123
|
+
return this.#checkpointer;
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
/** The failure that stands, alarmed once: a refused open, a tick that threw. */
|
|
127
|
+
#failed = null;
|
|
128
|
+
async #failure(message) {
|
|
129
|
+
if (message === this.#failed) return;
|
|
130
|
+
this.#failed = message;
|
|
131
|
+
console.warn(`coffre audit: ${message}`);
|
|
132
|
+
await this.#record({
|
|
133
|
+
at: Date.now(),
|
|
134
|
+
message
|
|
135
|
+
}).catch((error) => console.warn(`coffre audit: the alarm could not be kept: ${error.message}`));
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Nothing else delivered meanwhile, and the old checkpointer closed, fenced, its tick in flight ended or stalled,
|
|
139
|
+
* before the next request may open another: a change still waiting on the old one can tick it no more, so two
|
|
140
|
+
* never sign at once. Its connection is cut first, so that what it still awaits fails now rather than whenever the
|
|
141
|
+
* database answers (T169); a statement it had sent may still land, which the fence's rules allow.
|
|
142
|
+
*/
|
|
143
|
+
async #reset() {
|
|
144
|
+
await this.#state.blockConcurrencyWhile(async () => {
|
|
145
|
+
const [old, pool] = [this.#checkpointer, this.#pool];
|
|
146
|
+
this.#checkpointer = null;
|
|
147
|
+
this.#pool = null;
|
|
148
|
+
this.#upkeep = null;
|
|
149
|
+
pool?.destroy();
|
|
150
|
+
await old?.close();
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
async #record(alarm) {
|
|
154
|
+
const storage = this.#state.storage;
|
|
155
|
+
await storage.put(alarmKey(alarm.at, crypto.randomUUID()), alarm.message);
|
|
156
|
+
const old = await storage.list({
|
|
157
|
+
prefix: "alarm:",
|
|
158
|
+
end: alarmKey(alarm.at - ALARMS_KEPT_MS),
|
|
159
|
+
limit: 128
|
|
160
|
+
});
|
|
161
|
+
if (old.size > 0) await storage.delete([...old.keys()]);
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* The next days of uses, made once a day when the checkpointer cannot open or tick, over a connection of their own:
|
|
165
|
+
* a use needs its day's partition whatever the audit's state, and making one needs no signer.
|
|
166
|
+
*/
|
|
167
|
+
async #makeDays() {
|
|
168
|
+
const now = Date.now();
|
|
169
|
+
const today = dayOf(now);
|
|
170
|
+
if (this.#daysFor === today || now - this.#daysTried < 36e5) return;
|
|
171
|
+
this.#daysTried = now;
|
|
172
|
+
let pool = null;
|
|
173
|
+
try {
|
|
174
|
+
const s = await settings(this.#env);
|
|
175
|
+
const making = new HyperdrivePool(s.hyperdrive.connectionString, { closeIdleMs: 1e3 });
|
|
176
|
+
pool = making;
|
|
177
|
+
await within(logStore(createDatabase(making)).makeDays(daysAround(now)), s.tickBoundMs ?? OPEN_BOUND_MS, "making them", () => making.destroy());
|
|
178
|
+
this.#daysFor = today;
|
|
179
|
+
this.#daysFailed = null;
|
|
180
|
+
} catch (error) {
|
|
181
|
+
const message = `the next days of uses were not made: ${reasonOf(error)}`;
|
|
182
|
+
if (message !== this.#daysFailed) {
|
|
183
|
+
console.warn(`coffre audit: ${message}`);
|
|
184
|
+
await this.#record({
|
|
185
|
+
at: now,
|
|
186
|
+
message
|
|
187
|
+
}).catch((e) => console.warn(`coffre audit: the alarm could not be kept: ${e.message}`));
|
|
188
|
+
}
|
|
189
|
+
this.#daysFailed = message;
|
|
190
|
+
} finally {
|
|
191
|
+
pool?.destroy();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* The projector's run: some steps of `projectReads`, then the next alarm, a tick later, or at once while a run
|
|
196
|
+
* leaves sealed uses to project. A failure is an alarm, said once while it stands (`Projector`), and the next
|
|
197
|
+
* tick tries again, on the same connection, which postgres.js opens again when it is lost.
|
|
198
|
+
*/
|
|
199
|
+
async #project() {
|
|
200
|
+
let more = false;
|
|
201
|
+
try {
|
|
202
|
+
if (this.#projector === null) {
|
|
203
|
+
const s = await settings(this.#env);
|
|
204
|
+
const pool = new HyperdrivePool(s.hyperdrive.connectionString, { closeIdleMs: s.closeIdleMs ?? 1e4 });
|
|
205
|
+
const store = logStore(createDatabase(pool));
|
|
206
|
+
this.#projector = new Projector(store, await verifierKeys([s.seed]), (message) => {
|
|
207
|
+
console.warn(`coffre audit: ${message}`);
|
|
208
|
+
this.#record({
|
|
209
|
+
at: Date.now(),
|
|
210
|
+
message
|
|
211
|
+
}).catch((error) => console.warn(`coffre audit: the alarm could not be kept: ${error.message}`));
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
more = await this.#projector.run();
|
|
215
|
+
} catch (error) {
|
|
216
|
+
this.#projector = null;
|
|
217
|
+
await this.#failure(`the reads' projection cannot open: ${reasonOf(error)}`);
|
|
218
|
+
} finally {
|
|
219
|
+
await this.#state.storage.setAlarm(Date.now() + (more ? 0 : 500));
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
async #isProjector() {
|
|
223
|
+
return this.#projects ??= await this.#state.storage.get("role") === "reads";
|
|
224
|
+
}
|
|
225
|
+
async alarm() {
|
|
226
|
+
if (await this.#isProjector()) return this.#project();
|
|
227
|
+
let more = false;
|
|
228
|
+
try {
|
|
229
|
+
const checkpointer = await this.#open();
|
|
230
|
+
if (checkpointer === null) return await this.#makeDays();
|
|
231
|
+
const t0 = performance.now();
|
|
232
|
+
const t = await checkpointer.tick();
|
|
233
|
+
this.#ticks.push({
|
|
234
|
+
...t,
|
|
235
|
+
wallMs: performance.now() - t0,
|
|
236
|
+
at: Date.now()
|
|
237
|
+
});
|
|
238
|
+
if (this.#ticks.length > 64) this.#ticks.shift();
|
|
239
|
+
this.#failed = null;
|
|
240
|
+
more = t.more;
|
|
241
|
+
const pool = this.#pool;
|
|
242
|
+
if (this.#upkeep !== null && !more) {
|
|
243
|
+
this.#upkeeping = true;
|
|
244
|
+
try {
|
|
245
|
+
await within(this.#upkeep(), UPKEEP_BOUND_MS, "the log's upkeep", () => pool?.destroy());
|
|
246
|
+
} finally {
|
|
247
|
+
this.#upkeeping = false;
|
|
248
|
+
const last = this.#ticks.at(-1);
|
|
249
|
+
if (last !== void 0) last.at = Date.now();
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
} catch (error) {
|
|
253
|
+
await this.#failure(`a checkpoint failed: ${reasonOf(error)}`);
|
|
254
|
+
await this.#reset();
|
|
255
|
+
await this.#makeDays();
|
|
256
|
+
} finally {
|
|
257
|
+
await this.#state.storage.setAlarm(Date.now() + (more ? 0 : 500));
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
async fetch(request) {
|
|
261
|
+
try {
|
|
262
|
+
const call = await request.json();
|
|
263
|
+
if (call.op === "project") {
|
|
264
|
+
if (!await this.#isProjector()) {
|
|
265
|
+
await this.#state.storage.put("role", "reads");
|
|
266
|
+
this.#projects = true;
|
|
267
|
+
}
|
|
268
|
+
if (await this.#state.storage.getAlarm() === null) await this.#state.storage.setAlarm(Date.now() + 500);
|
|
269
|
+
return Response.json({ ok: true });
|
|
270
|
+
}
|
|
271
|
+
if (await this.#isProjector() && call.op !== "alarms") return new Response("the reads' projector seals nothing", { status: 400 });
|
|
272
|
+
if (call.op === "start") {
|
|
273
|
+
if (await this.#state.storage.getAlarm() === null) await this.#state.storage.setAlarm(Date.now() + 500);
|
|
274
|
+
if (await this.#open() === null) return Response.json({ error: this.#failed }, { status: 503 });
|
|
275
|
+
return Response.json({ ok: true });
|
|
276
|
+
}
|
|
277
|
+
if (call.op === "seal") {
|
|
278
|
+
const checkpointer = await this.#open();
|
|
279
|
+
if (checkpointer === null) return Response.json({ error: new Unsealed(BigInt(call.number), this.#failed ?? "the checkpointer cannot open").message }, { status: 503 });
|
|
280
|
+
try {
|
|
281
|
+
return Response.json(sealedToJson(await checkpointer.sealThrough(BigInt(call.number), call.deadline)));
|
|
282
|
+
} catch (error) {
|
|
283
|
+
if (error instanceof Unsealed) return Response.json({ error: error.message }, { status: 503 });
|
|
284
|
+
throw error;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
if (call.op === "status") {
|
|
288
|
+
const head = this.#checkpointer?.pair.pair;
|
|
289
|
+
return Response.json({
|
|
290
|
+
open: this.#checkpointer !== null,
|
|
291
|
+
failed: this.#failed,
|
|
292
|
+
sealing: sealingOf({
|
|
293
|
+
open: this.#checkpointer !== null,
|
|
294
|
+
failed: this.#failed ?? this.#checkpointer?.standing ?? null,
|
|
295
|
+
last: this.#lastSeen()
|
|
296
|
+
}),
|
|
297
|
+
upto: head === void 0 ? null : String(head.upto),
|
|
298
|
+
sizes: head === void 0 ? null : {
|
|
299
|
+
changes: head.changes.size,
|
|
300
|
+
uses: head.uses.size
|
|
301
|
+
},
|
|
302
|
+
ticks: this.#ticks.map((t) => ({
|
|
303
|
+
...t,
|
|
304
|
+
upto: String(t.upto)
|
|
305
|
+
}))
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
if (call.op === "remembered") return Response.json({ held: heldIn(await durableMemory(this.#state.storage).last()) });
|
|
309
|
+
if (call.op === "alarms") {
|
|
310
|
+
const alarms = [];
|
|
311
|
+
let start = alarmKey(call.since ?? 0);
|
|
312
|
+
let last = null;
|
|
313
|
+
for (;;) {
|
|
314
|
+
const page = await this.#state.storage.list({
|
|
315
|
+
prefix: "alarm:",
|
|
316
|
+
start,
|
|
317
|
+
limit: 1e3
|
|
318
|
+
});
|
|
319
|
+
for (const [key, message] of page) if (key !== last) alarms.push({
|
|
320
|
+
at: Number(key.split(":")[1]),
|
|
321
|
+
message
|
|
322
|
+
});
|
|
323
|
+
if (page.size < 1e3) break;
|
|
324
|
+
last = start = [...page.keys()].at(-1);
|
|
325
|
+
}
|
|
326
|
+
return Response.json(alarms);
|
|
327
|
+
}
|
|
328
|
+
return new Response("unknown op", { status: 400 });
|
|
329
|
+
} catch (error) {
|
|
330
|
+
return Response.json({ error: error.message }, { status: 500 });
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Asks the checkpointer's object, as `durableSealThrough(env.CHECKPOINTER)` and `startCheckpointer(env.CHECKPOINTER)`
|
|
337
|
+
* do, for at most `ms` (T169): an object that does not answer, its gate held or its isolate wedged, fails the ask
|
|
338
|
+
* rather than the call that asked.
|
|
339
|
+
*/
|
|
340
|
+
async function ask(namespace, name, call, ms = ASK_BOUND_MS) {
|
|
341
|
+
if (typeof namespace?.idFromName !== "function") throw new Error("there is no CHECKPOINTER binding: bind the CoffreCheckpointer Durable Object to the vault");
|
|
342
|
+
const asked = (async () => {
|
|
343
|
+
const response = await namespace.get(namespace.idFromName(name)).fetch(new Request("https://checkpointer/", {
|
|
344
|
+
method: "POST",
|
|
345
|
+
body: JSON.stringify(call)
|
|
346
|
+
}));
|
|
347
|
+
return {
|
|
348
|
+
response,
|
|
349
|
+
body: await response.json()
|
|
350
|
+
};
|
|
351
|
+
})();
|
|
352
|
+
const { response, body } = await within(asked, ms, "the checkpointer's object");
|
|
353
|
+
if (!response.ok) throw Object.assign(/* @__PURE__ */ new Error(`the checkpointer failed: ${body.error ?? response.status}`), { status: response.status });
|
|
354
|
+
return body;
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* The change path's seam on Workers (plan PR 4): seal through the checkpointer's object. Its 503, or no answer in
|
|
358
|
+
* time, is `Unsealed` (T169): the change is committed and in force, and the app says 503, try again.
|
|
359
|
+
*/
|
|
360
|
+
function durableSealThrough(namespace, name = "checkpointer") {
|
|
361
|
+
return async (number, deadline) => {
|
|
362
|
+
let sealed;
|
|
363
|
+
try {
|
|
364
|
+
sealed = await ask(namespace, name, {
|
|
365
|
+
op: "seal",
|
|
366
|
+
number: String(number),
|
|
367
|
+
deadline
|
|
368
|
+
}, SEAL_ASK_BOUND_MS);
|
|
369
|
+
} catch (error) {
|
|
370
|
+
if (error.status === void 0) throw new Unsealed(number, `the checkpointer's object did not answer: ${error.message}`, error);
|
|
371
|
+
throw error;
|
|
372
|
+
}
|
|
373
|
+
return sealedFromJson(sealed);
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
/** Starts the object's ticks, once; again is harmless. Throws when it cannot open, its alarm set all the same. */
|
|
377
|
+
async function startCheckpointer(namespace, name = "checkpointer") {
|
|
378
|
+
await ask(namespace, name, { op: "start" });
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Starts the reads' projector (reads.ts, T166): the object of the same class named `name`, which projects from then
|
|
382
|
+
* on, on its own alarm. Again is harmless.
|
|
383
|
+
*/
|
|
384
|
+
async function startProjector(namespace, name = "reads") {
|
|
385
|
+
await ask(namespace, name, { op: "project" });
|
|
386
|
+
}
|
|
387
|
+
/** The object's head and its last ticks, each with its barrier, write and wall time. */
|
|
388
|
+
function checkpointerStatus(namespace, name = "checkpointer") {
|
|
389
|
+
return ask(namespace, name, { op: "status" });
|
|
390
|
+
}
|
|
391
|
+
/** Whether the object seals (`Vault.sealing`, T169): its own view, or, if it does not answer within 2 s, that it does not. */
|
|
392
|
+
function durableSealing(namespace, name = "checkpointer") {
|
|
393
|
+
return async () => {
|
|
394
|
+
try {
|
|
395
|
+
return (await checkpointerStatus(namespace, name)).sealing;
|
|
396
|
+
} catch (error) {
|
|
397
|
+
return {
|
|
398
|
+
ok: false,
|
|
399
|
+
reason: `the checkpointer's object did not answer: ${error.message}`
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* The pair the signer remembers, where no database writer reaches: what the
|
|
406
|
+
* vault's verifier holds its walks to (`VaultOptions.heldOutside`).
|
|
407
|
+
*/
|
|
408
|
+
function durableHeldOutside(namespace, name = "checkpointer") {
|
|
409
|
+
return async () => {
|
|
410
|
+
const { held } = await ask(namespace, name, { op: "remembered" });
|
|
411
|
+
return held;
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
/** The alarms the object kept since `since` (ms). */
|
|
415
|
+
function checkpointerAlarms(namespace, since = 0, name = "checkpointer") {
|
|
416
|
+
return ask(namespace, name, {
|
|
417
|
+
op: "alarms",
|
|
418
|
+
since
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
function sealedToJson(s) {
|
|
422
|
+
const p = s.proof;
|
|
423
|
+
return {
|
|
424
|
+
notes: s.notes,
|
|
425
|
+
witnessed: s.witnessed,
|
|
426
|
+
failures: s.failures,
|
|
427
|
+
cosigned: s.cosigned,
|
|
428
|
+
proof: {
|
|
429
|
+
number: String(p.number),
|
|
430
|
+
tree: p.tree,
|
|
431
|
+
index: p.index,
|
|
432
|
+
size: p.size,
|
|
433
|
+
leaf: p.leaf.toString("base64"),
|
|
434
|
+
proof: p.proof.map((h) => h.toString("base64")),
|
|
435
|
+
notes: p.notes
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
/** The answer as the object sent it. Its notes are the vault's own, signed in the object: read, not verified again. */
|
|
440
|
+
function sealedFromJson(j) {
|
|
441
|
+
const read = (note) => parseCheckpoint(splitNote(note).text);
|
|
442
|
+
const [changes, uses] = [read(j.notes.changes), read(j.notes.uses)];
|
|
443
|
+
return {
|
|
444
|
+
pair: {
|
|
445
|
+
upto: changes.upto,
|
|
446
|
+
at: changes.at,
|
|
447
|
+
floor: changes.floor,
|
|
448
|
+
prev: changes.prev,
|
|
449
|
+
changes,
|
|
450
|
+
uses
|
|
451
|
+
},
|
|
452
|
+
notes: j.notes,
|
|
453
|
+
witnessed: j.witnessed,
|
|
454
|
+
failures: j.failures,
|
|
455
|
+
cosigned: j.cosigned,
|
|
456
|
+
proof: {
|
|
457
|
+
...j.proof,
|
|
458
|
+
number: BigInt(j.proof.number),
|
|
459
|
+
leaf: Buffer.from(j.proof.leaf, "base64"),
|
|
460
|
+
proof: j.proof.proof.map((h) => Buffer.from(h, "base64"))
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
//#endregion
|
|
465
|
+
//#region src/witness-workers.ts
|
|
466
|
+
/** The witness's Durable Object class. A new name for it is a new witness, which takes the next pair it is sent as given. */
|
|
467
|
+
function witnessObject(settings) {
|
|
468
|
+
return class CoffreWitness {
|
|
469
|
+
#state;
|
|
470
|
+
#env;
|
|
471
|
+
constructor(state, env) {
|
|
472
|
+
this.#state = state;
|
|
473
|
+
this.#env = env;
|
|
474
|
+
}
|
|
475
|
+
async fetch(request) {
|
|
476
|
+
try {
|
|
477
|
+
const body = request.method === "GET" ? null : await request.json();
|
|
478
|
+
return await this.#state.blockConcurrencyWhile(() => this.#answer(body));
|
|
479
|
+
} catch (error) {
|
|
480
|
+
return Response.json({
|
|
481
|
+
ok: false,
|
|
482
|
+
reason: `the witness failed: ${error.message}`
|
|
483
|
+
}, { status: 500 });
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
/** Every failure answered from inside the gate, never thrown out of it, which would reset the object. */
|
|
487
|
+
async #answer(p) {
|
|
488
|
+
try {
|
|
489
|
+
return await this.#answerOr(p);
|
|
490
|
+
} catch (error) {
|
|
491
|
+
return Response.json({
|
|
492
|
+
ok: false,
|
|
493
|
+
reason: `the witness failed: ${error.message}`
|
|
494
|
+
}, { status: 500 });
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
async #answerOr(p) {
|
|
498
|
+
const s = await settings(this.#env);
|
|
499
|
+
const log = await pairVerifiers(s.log);
|
|
500
|
+
const self = await noteSigner(s.name, Buffer.from(s.key, "base64"));
|
|
501
|
+
const stored = await this.#state.storage.get("pair");
|
|
502
|
+
const notes = stored === void 0 ? null : decodePair(stored);
|
|
503
|
+
const pair = notes === null ? null : await openPair(notes, log);
|
|
504
|
+
if (stored !== void 0 && pair === null) return Response.json({
|
|
505
|
+
ok: false,
|
|
506
|
+
reason: "the pair this witness holds does not open under the log's keys: name a new witness"
|
|
507
|
+
}, { status: 500 });
|
|
508
|
+
const held = pair === null ? null : {
|
|
509
|
+
pair,
|
|
510
|
+
notes
|
|
511
|
+
};
|
|
512
|
+
const sizes = {
|
|
513
|
+
changes: held?.pair.changes.size ?? 0,
|
|
514
|
+
uses: held?.pair.uses.size ?? 0
|
|
515
|
+
};
|
|
516
|
+
if (p === null) return Response.json({ held: sizes });
|
|
517
|
+
if (p.from.changes !== sizes.changes || p.from.uses !== sizes.uses) return Response.json({ held: sizes }, { status: 409 });
|
|
518
|
+
const proofs = {
|
|
519
|
+
changes: p.proofs.changes.map((h) => Buffer.from(h, "base64")),
|
|
520
|
+
uses: p.proofs.uses.map((h) => Buffer.from(h, "base64"))
|
|
521
|
+
};
|
|
522
|
+
const r = await witnessPair(held, p.notes, proofs, log, self);
|
|
523
|
+
if (!r.answer.ok) return Response.json(r.answer, { status: 422 });
|
|
524
|
+
await this.#state.storage.put("pair", encodePair(p.notes));
|
|
525
|
+
return Response.json(r.answer);
|
|
526
|
+
}
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
/** The witness's object as the checkpointer's `Witness`: the pair and each tree's proof from the sizes the object holds. */
|
|
530
|
+
function durableWitness(namespace, options = {}) {
|
|
531
|
+
const name = options.name ?? "witness";
|
|
532
|
+
let held = null;
|
|
533
|
+
return {
|
|
534
|
+
name,
|
|
535
|
+
async publish(notes, proofs, signal) {
|
|
536
|
+
if (typeof namespace?.idFromName !== "function") return {
|
|
537
|
+
ok: false,
|
|
538
|
+
reason: "there is no WITNESS binding"
|
|
539
|
+
};
|
|
540
|
+
const object = namespace.get(namespace.idFromName(name));
|
|
541
|
+
for (let tries = 0; tries < 2; tries += 1) {
|
|
542
|
+
if (held === null) {
|
|
543
|
+
const response = await object.fetch(new Request("https://witness/", {
|
|
544
|
+
method: "GET",
|
|
545
|
+
signal
|
|
546
|
+
}));
|
|
547
|
+
const body = await response.json().catch(() => ({}));
|
|
548
|
+
const sizes = sizesIn(body.held);
|
|
549
|
+
if (!response.ok || sizes === null) return {
|
|
550
|
+
ok: false,
|
|
551
|
+
reason: body.reason ?? `the witness answered ${response.status}`
|
|
552
|
+
};
|
|
553
|
+
held = sizes;
|
|
554
|
+
}
|
|
555
|
+
const from = held;
|
|
556
|
+
const p = await proofs(from);
|
|
557
|
+
const body = {
|
|
558
|
+
notes,
|
|
559
|
+
from,
|
|
560
|
+
proofs: {
|
|
561
|
+
changes: p.changes.map((h) => h.toString("base64")),
|
|
562
|
+
uses: p.uses.map((h) => h.toString("base64"))
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
const response = await object.fetch(new Request("https://witness/", {
|
|
566
|
+
method: "POST",
|
|
567
|
+
body: JSON.stringify(body),
|
|
568
|
+
signal
|
|
569
|
+
}));
|
|
570
|
+
const answer = await response.json().catch(() => ({ ok: false }));
|
|
571
|
+
if (response.status === 409) {
|
|
572
|
+
held = sizesIn(answer.held);
|
|
573
|
+
if (held === null) return {
|
|
574
|
+
ok: false,
|
|
575
|
+
reason: `the witness answered 409 without sizes it holds`
|
|
576
|
+
};
|
|
577
|
+
continue;
|
|
578
|
+
}
|
|
579
|
+
if (!answer.ok) return {
|
|
580
|
+
ok: false,
|
|
581
|
+
reason: answer.reason ?? `the witness answered ${response.status}`
|
|
582
|
+
};
|
|
583
|
+
held = sizesOf(notes);
|
|
584
|
+
return {
|
|
585
|
+
ok: true,
|
|
586
|
+
cosigned: answer.cosigned
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
return {
|
|
590
|
+
ok: false,
|
|
591
|
+
reason: "the witness kept holding other sizes"
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
/** Sizes a witness says it holds, if they are sizes: whole numbers, none below zero. */
|
|
597
|
+
function sizesIn(value) {
|
|
598
|
+
const sizes = value;
|
|
599
|
+
const size = (n) => Number.isSafeInteger(n) && n >= 0;
|
|
600
|
+
return sizes != null && size(sizes.changes) && size(sizes.uses) ? {
|
|
601
|
+
changes: sizes.changes,
|
|
602
|
+
uses: sizes.uses
|
|
603
|
+
} : null;
|
|
604
|
+
}
|
|
605
|
+
/** Each tree's size, read from its note (the checkpointer's own, just signed). */
|
|
606
|
+
const sizesOf = (notes) => ({
|
|
607
|
+
changes: Number(notes.changes.split("\n")[1]),
|
|
608
|
+
uses: Number(notes.uses.split("\n")[1])
|
|
609
|
+
});
|
|
610
|
+
/**
|
|
611
|
+
* Each pair it is sent, kept in an R2 bucket under `<prefix><upto, 20 digits>.json`.
|
|
612
|
+
* A place already archived with the same pair is fine (a pair sent again);
|
|
613
|
+
* one archived with another is refused, and said.
|
|
614
|
+
*/
|
|
615
|
+
function r2Archive(bucket, options = {}) {
|
|
616
|
+
const prefix = options.prefix ?? "audit/";
|
|
617
|
+
return {
|
|
618
|
+
name: "r2-archive",
|
|
619
|
+
async publish(notes) {
|
|
620
|
+
const upto = notes.changes.split("\n")[3]?.slice(5) ?? "";
|
|
621
|
+
if (!/^\d{1,19}$/.test(upto)) return {
|
|
622
|
+
ok: false,
|
|
623
|
+
reason: "not a checkpoint pair"
|
|
624
|
+
};
|
|
625
|
+
const key = `${prefix}${upto.padStart(20, "0")}.json`;
|
|
626
|
+
const record = encodePair(notes);
|
|
627
|
+
if (await bucket.put(key, record, { onlyIf: { etagDoesNotMatch: "*" } }).catch(() => null) !== null) return { ok: true };
|
|
628
|
+
return await (await bucket.get(key))?.text() === record ? { ok: true } : {
|
|
629
|
+
ok: false,
|
|
630
|
+
reason: `the archive holds another pair up to ${upto}`
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
//#endregion
|
|
6
636
|
//#region src/cloudflare.ts
|
|
7
637
|
/** Set when the Worker's module runs `vault(…)`, before any call comes in. */
|
|
8
638
|
let configure = null;
|
|
@@ -12,6 +642,8 @@ let configure = null;
|
|
|
12
642
|
* would belong to the call that started it (vault.ts, `PreparedVault`).
|
|
13
643
|
*/
|
|
14
644
|
const ready = /* @__PURE__ */ new WeakMap();
|
|
645
|
+
/** How long after a start that failed, the checkpointer's object out of reach, the next call tries again. */
|
|
646
|
+
const START_AGAIN_MS = 1e4;
|
|
15
647
|
/**
|
|
16
648
|
* What the app's `VAULT` service binding calls. Each call gets a database of
|
|
17
649
|
* its own, one client that its queries take turns on, since a Worker's
|
|
@@ -28,16 +660,83 @@ var VaultEntrypoint = class extends WorkerEntrypoint {
|
|
|
28
660
|
this.ctx.waitUntil(pool.end());
|
|
29
661
|
}
|
|
30
662
|
}
|
|
663
|
+
/**
|
|
664
|
+
* 0.15's use: one connection of the hot path's config, for its one statement. Closed once the call is done, and by
|
|
665
|
+
* its deadline at the latest, so that a call answered `late` lets go of Hyperdrive by then, as its place in admission
|
|
666
|
+
* lapses soon after (use.ts, `HELD_PAST_DEADLINE_MS`; T170).
|
|
667
|
+
*/
|
|
668
|
+
async use(input) {
|
|
669
|
+
const { prepared, hotDatabase } = this.#behind(await this.#prepared());
|
|
670
|
+
const pool = new HyperdrivePool(hotDatabase.hyperdrive.connectionString);
|
|
671
|
+
try {
|
|
672
|
+
const hot = createDatabase(pool);
|
|
673
|
+
return await openVault(hot, prepared, hot).use(input);
|
|
674
|
+
} finally {
|
|
675
|
+
this.ctx.waitUntil(pool.end(closeBy(input)));
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
/** A request's several keys (T167): the same one connection, for its one statement, or two. */
|
|
679
|
+
async useMany(input) {
|
|
680
|
+
const { prepared, hotDatabase } = this.#behind(await this.#prepared());
|
|
681
|
+
if (hotDatabase === void 0) throw new Error("0.15's uses need the hot path's Hyperdrive config: set hotDatabase in vault(…)");
|
|
682
|
+
const pool = new HyperdrivePool(hotDatabase.hyperdrive.connectionString);
|
|
683
|
+
try {
|
|
684
|
+
const hot = createDatabase(pool);
|
|
685
|
+
return await openVault(hot, prepared, hot).useMany(input);
|
|
686
|
+
} finally {
|
|
687
|
+
this.ctx.waitUntil(pool.end(closeBy(input)));
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
/** 0.15's use as the request names it: the same one connection, for its read of ids on a miss and its one use. */
|
|
691
|
+
async useNamed(input) {
|
|
692
|
+
const { prepared, hotDatabase } = this.#behind(await this.#prepared());
|
|
693
|
+
const pool = new HyperdrivePool(hotDatabase.hyperdrive.connectionString);
|
|
694
|
+
try {
|
|
695
|
+
const hot = createDatabase(pool);
|
|
696
|
+
return await openVault(hot, prepared, hot).useNamed(input);
|
|
697
|
+
} finally {
|
|
698
|
+
this.ctx.waitUntil(pool.end(closeBy(input)));
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
/** The vault ready, its checkpointer started from this call first, for at most 2 s (`start`). */
|
|
31
702
|
async #ready() {
|
|
703
|
+
const entry = await this.#prepared();
|
|
704
|
+
if (!entry.started && Date.now() - entry.failed >= START_AGAIN_MS) await start(entry);
|
|
705
|
+
return entry;
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* A use waits on no start (T169, R58): it needs no checkpointer, the days of uses the migrations made and the object
|
|
709
|
+
* makes ahead of time aside. Its isolate's start goes on behind it, once each START_AGAIN_MS until one answers.
|
|
710
|
+
*/
|
|
711
|
+
#behind(entry) {
|
|
712
|
+
if (!entry.started && Date.now() - entry.kicked >= START_AGAIN_MS) {
|
|
713
|
+
entry.kicked = Date.now();
|
|
714
|
+
this.ctx.waitUntil(start(entry));
|
|
715
|
+
}
|
|
716
|
+
return entry;
|
|
717
|
+
}
|
|
718
|
+
async #prepared() {
|
|
32
719
|
const env = this.env;
|
|
33
720
|
let entry = ready.get(env);
|
|
34
721
|
if (entry === void 0) {
|
|
35
722
|
if (configure === null) throw new Error("the vault Worker must export default vault(…)");
|
|
36
723
|
const config = configure(env);
|
|
37
|
-
const
|
|
724
|
+
const { checkpointer: namespace, hotDatabase } = checked(config);
|
|
725
|
+
const prepared = await prepareVault(resolveVaultConfig(config), {
|
|
726
|
+
...config.uses === void 0 ? {} : { uses: config.uses },
|
|
727
|
+
log: "0.15",
|
|
728
|
+
sealThrough: durableSealThrough(namespace),
|
|
729
|
+
heldOutside: durableHeldOutside(namespace),
|
|
730
|
+
callsEndWithRequest: true
|
|
731
|
+
});
|
|
38
732
|
entry = ready.get(env) ?? {
|
|
39
733
|
database: config.database,
|
|
40
|
-
|
|
734
|
+
hotDatabase,
|
|
735
|
+
checkpointer: namespace,
|
|
736
|
+
prepared,
|
|
737
|
+
started: false,
|
|
738
|
+
failed: -Infinity,
|
|
739
|
+
kicked: -Infinity
|
|
41
740
|
};
|
|
42
741
|
ready.set(env, entry);
|
|
43
742
|
}
|
|
@@ -85,8 +784,14 @@ var VaultEntrypoint = class extends WorkerEntrypoint {
|
|
|
85
784
|
async checkpoint() {
|
|
86
785
|
return this.#call((vault) => vault.checkpoint());
|
|
87
786
|
}
|
|
787
|
+
/** What the vault says of itself; its uses over the hot path's own config (`use`), which `#call`'s vault has not. */
|
|
88
788
|
async about() {
|
|
89
|
-
|
|
789
|
+
const about = await this.#call((vault) => vault.about());
|
|
790
|
+
return about.log === "0.15" ? {
|
|
791
|
+
...about,
|
|
792
|
+
uses: true,
|
|
793
|
+
batchUses: true
|
|
794
|
+
} : about;
|
|
90
795
|
}
|
|
91
796
|
async keyChecks() {
|
|
92
797
|
return this.#call((vault) => vault.keyChecks());
|
|
@@ -94,15 +799,106 @@ var VaultEntrypoint = class extends WorkerEntrypoint {
|
|
|
94
799
|
async verifyLog(input) {
|
|
95
800
|
return this.#call((vault) => vault.verifyLog(input));
|
|
96
801
|
}
|
|
802
|
+
async prepare(input) {
|
|
803
|
+
return this.#call((vault) => vault.prepare(input));
|
|
804
|
+
}
|
|
805
|
+
async waitSealed(input) {
|
|
806
|
+
return this.#call((vault) => vault.waitSealed(input));
|
|
807
|
+
}
|
|
808
|
+
async signIn(input) {
|
|
809
|
+
return this.#call((vault) => vault.signIn(input));
|
|
810
|
+
}
|
|
811
|
+
async revokeCredential(input) {
|
|
812
|
+
return this.#call((vault) => vault.revokeCredential(input));
|
|
813
|
+
}
|
|
814
|
+
async issueToken(input) {
|
|
815
|
+
return this.#call((vault) => vault.issueToken(input));
|
|
816
|
+
}
|
|
817
|
+
async changeBindings(input) {
|
|
818
|
+
return this.#call((vault) => vault.changeBindings(input));
|
|
819
|
+
}
|
|
820
|
+
async cutover(input) {
|
|
821
|
+
return this.#call((vault) => vault.cutover(input));
|
|
822
|
+
}
|
|
823
|
+
async verifyTrees() {
|
|
824
|
+
return this.#call((vault) => vault.verifyTrees());
|
|
825
|
+
}
|
|
826
|
+
async verifyReads(input) {
|
|
827
|
+
return this.#call((vault) => vault.verifyReads(input));
|
|
828
|
+
}
|
|
829
|
+
async vouch(input) {
|
|
830
|
+
return this.#call((vault) => vault.vouch(input));
|
|
831
|
+
}
|
|
832
|
+
/** Whether the checkpointer seals (T169), asked of its object for at most 2 s; no database, and no start awaited. */
|
|
833
|
+
async sealing() {
|
|
834
|
+
const { checkpointer: namespace } = this.#behind(await this.#prepared());
|
|
835
|
+
return durableSealing(namespace)();
|
|
836
|
+
}
|
|
97
837
|
/** No HTTP surface: only the app's service binding reaches the vault. */
|
|
98
838
|
fetch() {
|
|
99
839
|
return new Response("not found", { status: 404 });
|
|
100
840
|
}
|
|
101
841
|
};
|
|
842
|
+
/** When a use's connection closes at the latest: its deadline, if it names one (`checkInput` refuses it otherwise). */
|
|
843
|
+
function closeBy(input) {
|
|
844
|
+
return typeof input?.deadline === "number" && Number.isSafeInteger(input.deadline) ? input.deadline : void 0;
|
|
845
|
+
}
|
|
846
|
+
/**
|
|
847
|
+
* What 0.15's log needs, configured: a setting left out of `vault(…)` is refused here, by name. A Hyperdrive binding
|
|
848
|
+
* left out of wrangler.jsonc is refused before, by `postgres()`; a Durable Object's, here.
|
|
849
|
+
*/
|
|
850
|
+
function checked(config) {
|
|
851
|
+
if (config.hotDatabase?.hyperdrive?.connectionString === void 0) throw new Error("the vault's uses run through a Hyperdrive config of their own: set hotDatabase in vault(…), postgres(env.HOT_HYPERDRIVE), its login coffre_vault_hot");
|
|
852
|
+
if (typeof config.checkpointer?.idFromName !== "function") throw new Error("the vault answers a change once its checkpointer has sealed it: set checkpointer in vault(…), env.CHECKPOINTER, and bind the CoffreCheckpointer Durable Object beside the vault");
|
|
853
|
+
return {
|
|
854
|
+
hotDatabase: config.hotDatabase,
|
|
855
|
+
checkpointer: config.checkpointer
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
/**
|
|
859
|
+
* The checkpointer's object started, from a call of this isolate, before it
|
|
860
|
+
* goes on: its alarm set, and it open, so that no call meets a log with no
|
|
861
|
+
* checkpoint yet. Started again is harmless: until one start has answered,
|
|
862
|
+
* each call makes its own and waits for it, never for another call's,
|
|
863
|
+
* which a call may not await on Workers (AGENTS.md).
|
|
864
|
+
* An object that refuses to open (a log it cannot take: docs/restore.md) is
|
|
865
|
+
* started all the same, its alarm set, and says why in its alarms; one out
|
|
866
|
+
* of reach, or that does not answer within 2 s (T169), is tried again by a
|
|
867
|
+
* call 10 s later. Neither stops this call: a use needs no checkpointer (and
|
|
868
|
+
* waits on no start: `#behind`), and a change fails on its seal.
|
|
869
|
+
*/
|
|
870
|
+
async function start(entry) {
|
|
871
|
+
const projecting = startProjector(entry.checkpointer).catch((error) => console.warn(`coffre vault: the reads' projector did not start: ${error.message}`));
|
|
872
|
+
try {
|
|
873
|
+
await startCheckpointer(entry.checkpointer);
|
|
874
|
+
entry.started = true;
|
|
875
|
+
} catch (error) {
|
|
876
|
+
if (error.status === 503) entry.started = true;
|
|
877
|
+
else entry.failed = Date.now();
|
|
878
|
+
console.warn(`coffre vault: ${error.message}`);
|
|
879
|
+
}
|
|
880
|
+
await projecting;
|
|
881
|
+
}
|
|
102
882
|
/** The vault Worker's default export, `VaultEntrypoint`, configured from its `env`. */
|
|
103
883
|
function vault(configure_) {
|
|
104
884
|
configure = configure_;
|
|
105
885
|
return VaultEntrypoint;
|
|
106
886
|
}
|
|
887
|
+
/**
|
|
888
|
+
* 0.15's checkpointer, the Durable Object class a deployment binds beside
|
|
889
|
+
* the vault, configured by the same `vault(env => …)`: its Hyperdrive, its
|
|
890
|
+
* signing key, its witnesses. `export const CoffreCheckpointer = checkpointer()`.
|
|
891
|
+
*/
|
|
892
|
+
function checkpointer() {
|
|
893
|
+
return checkpointerObject((env) => {
|
|
894
|
+
if (configure === null) throw new Error("the vault Worker must export default vault(…)");
|
|
895
|
+
const config = configure(env);
|
|
896
|
+
return {
|
|
897
|
+
hyperdrive: config.database.hyperdrive,
|
|
898
|
+
seed: resolveVaultConfig(config).signingKeys[0],
|
|
899
|
+
witnesses: config.witnesses
|
|
900
|
+
};
|
|
901
|
+
});
|
|
902
|
+
}
|
|
107
903
|
//#endregion
|
|
108
|
-
export { KekBadClaimError, KekUnavailableError, VaultEntrypoint, awsKms, checkpointMessage, derivedSigningKey, postgres, vault, verifyCheckpoint };
|
|
904
|
+
export { KekBadClaimError, KekUnavailableError, VaultEntrypoint, awsKms, checkpointMessage, checkpointer, checkpointerAlarms, checkpointerStatus, derivedSigningKey, durableHeldOutside, durableSealThrough, durableSealing, durableWitness, postgres, r2Archive, startCheckpointer, startProjector, vault, verifyCheckpoint, witnessObject as witness };
|