@coffre/vault 0.14.5 → 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 +341 -4
- package/dist/cloudflare.js +802 -6
- package/dist/index.js +1 -1
- package/dist/log-entry-BYWqj3O5.d.ts +364 -0
- package/dist/node.d.ts +181 -6
- package/dist/node.js +358 -16
- package/dist/{src-Bg0ZeMJW.js → src-DheG92EM.js} +1 -1
- 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 +3 -2
- 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 +64 -10
- package/src/store.ts +832 -36
- package/src/use.postgres.ts +111 -0
- package/src/use.sqlite.ts +542 -0
- package/src/use.ts +1060 -0
- package/src/vault.ts +1366 -220
- package/src/verifier.ts +136 -0
- package/src/witness-workers.ts +163 -0
- package/dist/vault-Cbi45kLB.js +0 -5197
package/dist/node.js
CHANGED
|
@@ -1,9 +1,291 @@
|
|
|
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-
|
|
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, a as replayStored, b as sealingOf, c as openLogCheckpointer, f as Projector, g as TICK_BOUND_MS, h as Stalled, i as logStore, l as reasonOf, n as prepareVault, p as Fence, r as verifierKeys, s as daysAround, t as openVault, u as upkeep, x as within, y as heldIn } from "./vault-kmtojYnW.js";
|
|
3
3
|
import { isUnavailable } from "@coffre/db/dialect";
|
|
4
4
|
import { chmodSync, existsSync, lstatSync, rmSync } from "node:fs";
|
|
5
5
|
import { Agent, createServer, request } from "node:http";
|
|
6
|
-
import { openDatabase } from "@coffre/db/connect";
|
|
6
|
+
import { engineOfUrl, openDatabase } from "@coffre/db/connect";
|
|
7
|
+
import { appendFile, open, readFile, rename, stat, writeFile } from "node:fs/promises";
|
|
8
|
+
import { dirname } from "node:path";
|
|
9
|
+
//#region src/checkpointer-node.ts
|
|
10
|
+
/**
|
|
11
|
+
* The 0.15 checkpointer on Node (docs/design/audit-tree.md): one process
|
|
12
|
+
* seals the log every 500 ms, its memory a file beside the vault, synced
|
|
13
|
+
* before the database has the pair it holds (RememberFirst). The vault's
|
|
14
|
+
* process runs it (`keepCheckpointer`, node.ts). Nothing it awaits is
|
|
15
|
+
* unbounded (T169): a tick past its bound stalls the checkpointer, and the
|
|
16
|
+
* vault's process opens another.
|
|
17
|
+
*/
|
|
18
|
+
/** The signer's memory in a file: written whole, synced, renamed over the last, and the directory synced, so a power loss keeps it. */
|
|
19
|
+
function fileMemory(path) {
|
|
20
|
+
return {
|
|
21
|
+
last: () => readFile(path, "utf8").catch((error) => {
|
|
22
|
+
if (error.code === "ENOENT") return null;
|
|
23
|
+
throw error;
|
|
24
|
+
}),
|
|
25
|
+
async remember(record) {
|
|
26
|
+
const file = await open(`${path}.part`, "w");
|
|
27
|
+
try {
|
|
28
|
+
await file.writeFile(record);
|
|
29
|
+
await file.sync();
|
|
30
|
+
} finally {
|
|
31
|
+
await file.close();
|
|
32
|
+
}
|
|
33
|
+
await rename(`${path}.part`, path);
|
|
34
|
+
const dir = await open(dirname(path), "r");
|
|
35
|
+
try {
|
|
36
|
+
await dir.sync();
|
|
37
|
+
} finally {
|
|
38
|
+
await dir.close();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/** How long the runner waits on an open, the next days of uses, or the log's upkeep after a tick (T169). */
|
|
44
|
+
const OPEN_BOUND_MS = TICK_BOUND_MS;
|
|
45
|
+
const UPKEEP_BOUND_MS = 6e4;
|
|
46
|
+
/**
|
|
47
|
+
* Opens the checkpointer over `database` (the vault's login) and ticks it
|
|
48
|
+
* every `intervalMs`, a tick after the last one ends, each followed by the
|
|
49
|
+
* log's upkeep (days made ahead, retention past `retainDays`, the bulk
|
|
50
|
+
* cap's rows pruned). Each alarm is appended to `<memory>.alarms`, one JSON
|
|
51
|
+
* line each. Given `connect` instead, over a session of its own, cut once
|
|
52
|
+
* it stalls or stops, so that what it still awaits holds no connection of
|
|
53
|
+
* the vault's (review r1).
|
|
54
|
+
*/
|
|
55
|
+
async function runCheckpointer(options) {
|
|
56
|
+
const alarmsPath = `${options.memory}.alarms`;
|
|
57
|
+
let appending = Promise.resolve();
|
|
58
|
+
if (options.connect === void 0 === (options.database === void 0)) throw new Error("the checkpointer runs over its database or a session it connects, one of the two");
|
|
59
|
+
const session = options.connect === void 0 ? null : await within(options.connect(), options.tickBoundMs ?? OPEN_BOUND_MS, "connecting");
|
|
60
|
+
const store = logStore(session?.db ?? options.database, { tileRange: options.tileRange });
|
|
61
|
+
const fence = new Fence();
|
|
62
|
+
const checkpointer = await within(store.makeDays(daysAround(Date.now())), options.tickBoundMs ?? OPEN_BOUND_MS, "making the next days of uses").then(() => within(openLogCheckpointer({
|
|
63
|
+
store,
|
|
64
|
+
seed: options.seed,
|
|
65
|
+
memory: (options.memoryOf ?? fileMemory)(options.memory),
|
|
66
|
+
witnesses: options.witnesses,
|
|
67
|
+
record: (alarm) => record(alarm),
|
|
68
|
+
fence,
|
|
69
|
+
...options.tickBoundMs === void 0 ? {} : { tickBoundMs: options.tickBoundMs }
|
|
70
|
+
}), options.tickBoundMs ?? OPEN_BOUND_MS, "the open")).catch(async (error) => {
|
|
71
|
+
await session?.cut().catch(() => {});
|
|
72
|
+
await fence.close("its open failed");
|
|
73
|
+
throw error;
|
|
74
|
+
});
|
|
75
|
+
/** In order, one after the other; a file grown past a megabyte keeps its last 30 days. */
|
|
76
|
+
function record(alarm) {
|
|
77
|
+
appending = appending.then(async () => {
|
|
78
|
+
await appendFile(alarmsPath, `${JSON.stringify(alarm)}\n`);
|
|
79
|
+
if ((await stat(alarmsPath)).size > 1e6) {
|
|
80
|
+
const kept = (await readAlarms(alarmsPath)).filter((a) => a.at >= alarm.at - 2592e6);
|
|
81
|
+
await writeFile(`${alarmsPath}.part`, kept.map((a) => `${JSON.stringify(a)}\n`).join(""));
|
|
82
|
+
await rename(`${alarmsPath}.part`, alarmsPath);
|
|
83
|
+
}
|
|
84
|
+
}).catch((error) => console.warn(`coffre audit: an alarm could not be kept: ${error.message}`));
|
|
85
|
+
return appending;
|
|
86
|
+
}
|
|
87
|
+
const keep = upkeep(checkpointer, fence.store(store), {
|
|
88
|
+
keepDays: options.retainDays ?? null,
|
|
89
|
+
seed: options.seed,
|
|
90
|
+
alert: (message) => {
|
|
91
|
+
console.warn(`coffre audit: ${message}`);
|
|
92
|
+
record({
|
|
93
|
+
at: Date.now(),
|
|
94
|
+
message
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
const keys = await verifierKeys([options.seed]);
|
|
99
|
+
const projector = new Projector(store, keys, (message) => {
|
|
100
|
+
console.warn(`coffre audit: ${message}`);
|
|
101
|
+
record({
|
|
102
|
+
at: Date.now(),
|
|
103
|
+
message
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
const interval = options.intervalMs ?? 500;
|
|
107
|
+
let stopped = false;
|
|
108
|
+
let timer;
|
|
109
|
+
let ticking = Promise.resolve();
|
|
110
|
+
let failed = null;
|
|
111
|
+
let last = null;
|
|
112
|
+
let stalled = false;
|
|
113
|
+
let upkeeping = null;
|
|
114
|
+
let retired = false;
|
|
115
|
+
const retire = (why) => {
|
|
116
|
+
if (retired) return;
|
|
117
|
+
retired = true;
|
|
118
|
+
options.stalled?.(why);
|
|
119
|
+
};
|
|
120
|
+
const upkeepOnce = async () => {
|
|
121
|
+
if (upkeeping !== null) return;
|
|
122
|
+
const run = upkeeping = keep().finally(() => {
|
|
123
|
+
if (upkeeping === run) upkeeping = null;
|
|
124
|
+
});
|
|
125
|
+
await within(run, options.upkeepBoundMs ?? UPKEEP_BOUND_MS, "the log's upkeep").catch(async (error) => {
|
|
126
|
+
const message = `a checkpoint failed: ${error.message}`;
|
|
127
|
+
console.warn(`coffre audit: ${message}`);
|
|
128
|
+
failed = message;
|
|
129
|
+
stalled = true;
|
|
130
|
+
clearTimeout(timer);
|
|
131
|
+
await record({
|
|
132
|
+
at: Date.now(),
|
|
133
|
+
message
|
|
134
|
+
});
|
|
135
|
+
ticking.finally(() => retire(message));
|
|
136
|
+
});
|
|
137
|
+
};
|
|
138
|
+
let more = false;
|
|
139
|
+
const loop = () => {
|
|
140
|
+
ticking = checkpointer.tick().then((t) => {
|
|
141
|
+
failed = null;
|
|
142
|
+
last = {
|
|
143
|
+
at: Date.now(),
|
|
144
|
+
failures: t.failures
|
|
145
|
+
};
|
|
146
|
+
projector.kick();
|
|
147
|
+
more = t.more;
|
|
148
|
+
if (!more) upkeepOnce();
|
|
149
|
+
}, async (error) => {
|
|
150
|
+
more = false;
|
|
151
|
+
const message = `a checkpoint failed: ${reasonOf(error)}`;
|
|
152
|
+
if (message !== failed) {
|
|
153
|
+
failed = message;
|
|
154
|
+
console.warn(`coffre audit: ${message}`);
|
|
155
|
+
await record({
|
|
156
|
+
at: Date.now(),
|
|
157
|
+
message
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
if (error instanceof Stalled) stalled = true;
|
|
161
|
+
await within(store.makeDays(daysAround(Date.now())), options.tickBoundMs ?? OPEN_BOUND_MS, "making the next days of uses").catch(() => {});
|
|
162
|
+
}).finally(() => {
|
|
163
|
+
if (stalled) retire(failed ?? "the log's upkeep did not end in time");
|
|
164
|
+
else if (!stopped) timer = setTimeout(loop, more ? 0 : interval);
|
|
165
|
+
});
|
|
166
|
+
};
|
|
167
|
+
loop();
|
|
168
|
+
return {
|
|
169
|
+
checkpointer,
|
|
170
|
+
sealThrough: (number, deadline) => checkpointer.sealThrough(number, deadline),
|
|
171
|
+
async heldOutside() {
|
|
172
|
+
return heldIn(await fileMemory(options.memory).last());
|
|
173
|
+
},
|
|
174
|
+
async projected() {
|
|
175
|
+
for (let run = 0; run < 100; run += 1) {
|
|
176
|
+
await projector.settled();
|
|
177
|
+
projector.kick();
|
|
178
|
+
await projector.settled();
|
|
179
|
+
const [cursor, newest] = await Promise.all([store.readsCursor(), store.newest()]);
|
|
180
|
+
if (newest === null || cursor.through >= newest.upto) return;
|
|
181
|
+
}
|
|
182
|
+
throw new Error("the reads' projection did not catch up with the newest pair");
|
|
183
|
+
},
|
|
184
|
+
sealing: () => sealingOf({
|
|
185
|
+
open: !stalled && !stopped,
|
|
186
|
+
failed: failed ?? checkpointer.standing,
|
|
187
|
+
last
|
|
188
|
+
}),
|
|
189
|
+
async alarms(since = 0) {
|
|
190
|
+
await appending;
|
|
191
|
+
return (await readAlarms(alarmsPath)).filter((a) => a.at >= since);
|
|
192
|
+
},
|
|
193
|
+
async stop() {
|
|
194
|
+
stopped = true;
|
|
195
|
+
clearTimeout(timer);
|
|
196
|
+
if (stalled) await session?.cut().catch(() => {});
|
|
197
|
+
await ticking;
|
|
198
|
+
const projecting = projector.stop();
|
|
199
|
+
if (!stalled) await within(projecting, options.tickBoundMs ?? OPEN_BOUND_MS, "the reads' projection").catch(() => {});
|
|
200
|
+
await checkpointer.close();
|
|
201
|
+
await session?.cut().catch(() => {});
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
/** The alarms in the file, a line each; a line a crash left torn is skipped. */
|
|
206
|
+
async function readAlarms(path) {
|
|
207
|
+
return (await readFile(path, "utf8").catch((error) => {
|
|
208
|
+
if (error.code === "ENOENT") return "";
|
|
209
|
+
throw error;
|
|
210
|
+
})).split("\n").flatMap((line) => {
|
|
211
|
+
try {
|
|
212
|
+
return line.length > 0 ? [JSON.parse(line)] : [];
|
|
213
|
+
} catch {
|
|
214
|
+
return [];
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
/** How long after a refused open the vault's checkpointer tries again (`keepCheckpointer`). */
|
|
219
|
+
const OPEN_AGAIN_MS = 1e4;
|
|
220
|
+
/**
|
|
221
|
+
* The checkpointer as the vault's process keeps it: `runCheckpointer`, tried
|
|
222
|
+
* again every `retryMs` while it cannot open (a log it refuses, as after a
|
|
223
|
+
* restore, docs/restore.md; the database out of reach), each try making the
|
|
224
|
+
* next days of uses first, so that uses go on; its failure an alarm, kept
|
|
225
|
+
* once while it stands. Meanwhile a change's seal fails with why, as on
|
|
226
|
+
* Workers, and the pair held outside is read from the file as ever. One
|
|
227
|
+
* that stalls (T169) is stopped, fenced, and another opened a tick later.
|
|
228
|
+
*/
|
|
229
|
+
async function keepCheckpointer(options) {
|
|
230
|
+
let running = null;
|
|
231
|
+
let failed = null;
|
|
232
|
+
let stopped = false;
|
|
233
|
+
let timer;
|
|
234
|
+
const alarmsPath = `${options.memory}.alarms`;
|
|
235
|
+
let retiring = Promise.resolve();
|
|
236
|
+
const tryOpen = async () => {
|
|
237
|
+
try {
|
|
238
|
+
const opened = await runCheckpointer({
|
|
239
|
+
...options,
|
|
240
|
+
stalled: (why) => {
|
|
241
|
+
if (running === opened) running = null;
|
|
242
|
+
failed = why;
|
|
243
|
+
const retired = opened.stop().catch(() => {});
|
|
244
|
+
retiring = Promise.all([retiring, retired]).then(() => {});
|
|
245
|
+
retired.finally(() => {
|
|
246
|
+
if (!stopped) timer = setTimeout(() => void (trying = tryOpen()), 500);
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
running = opened;
|
|
251
|
+
failed = null;
|
|
252
|
+
} catch (error) {
|
|
253
|
+
const message = `the checkpointer cannot open: ${reasonOf(error)}`;
|
|
254
|
+
if (message !== failed) {
|
|
255
|
+
failed = message;
|
|
256
|
+
console.warn(`coffre audit: ${message}`);
|
|
257
|
+
await appendFile(alarmsPath, `${JSON.stringify({
|
|
258
|
+
at: Date.now(),
|
|
259
|
+
message
|
|
260
|
+
})}\n`).catch((e) => console.warn(`coffre audit: an alarm could not be kept: ${e.message}`));
|
|
261
|
+
}
|
|
262
|
+
if (!stopped) timer = setTimeout(() => void (trying = tryOpen()), options.retryMs ?? OPEN_AGAIN_MS);
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
let trying = tryOpen();
|
|
266
|
+
await trying;
|
|
267
|
+
return {
|
|
268
|
+
sealThrough: (number, deadline) => running === null ? Promise.reject(new Unsealed(number, failed ?? "the checkpointer is not open")) : running.sealThrough(number, deadline),
|
|
269
|
+
sealing: () => running === null ? {
|
|
270
|
+
ok: false,
|
|
271
|
+
reason: failed ?? "the checkpointer is not open"
|
|
272
|
+
} : running.sealing(),
|
|
273
|
+
async heldOutside() {
|
|
274
|
+
return heldIn(await fileMemory(options.memory).last());
|
|
275
|
+
},
|
|
276
|
+
async alarms(since = 0) {
|
|
277
|
+
return running === null ? (await readAlarms(alarmsPath)).filter((a) => a.at >= since) : running.alarms(since);
|
|
278
|
+
},
|
|
279
|
+
async stop() {
|
|
280
|
+
stopped = true;
|
|
281
|
+
clearTimeout(timer);
|
|
282
|
+
await trying;
|
|
283
|
+
await running?.stop();
|
|
284
|
+
await retiring;
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
//#endregion
|
|
7
289
|
//#region src/failure.ts
|
|
8
290
|
/**
|
|
9
291
|
* A vault call's failure, as `serveVault` answers it: its message, and the
|
|
@@ -55,16 +337,31 @@ const METHODS = [
|
|
|
55
337
|
"checkpoint",
|
|
56
338
|
"about",
|
|
57
339
|
"keyChecks",
|
|
58
|
-
"verifyLog"
|
|
340
|
+
"verifyLog",
|
|
341
|
+
"use",
|
|
342
|
+
"useNamed",
|
|
343
|
+
"useMany",
|
|
344
|
+
"prepare",
|
|
345
|
+
"waitSealed",
|
|
346
|
+
"signIn",
|
|
347
|
+
"revokeCredential",
|
|
348
|
+
"issueToken",
|
|
349
|
+
"changeBindings",
|
|
350
|
+
"cutover",
|
|
351
|
+
"verifyTrees",
|
|
352
|
+
"verifyReads",
|
|
353
|
+
"vouch",
|
|
354
|
+
"sealing"
|
|
59
355
|
];
|
|
60
356
|
/**
|
|
61
|
-
* The vault in this process, over `db
|
|
357
|
+
* The vault in this process, over `db`, and `hot` for 0.15's uses (vault.ts,
|
|
358
|
+
* `openVault`). Every argument and result goes
|
|
62
359
|
* through JSON on the way, as it would over RPC or a socket, so what works
|
|
63
360
|
* here does not rely on sharing objects with the caller. `close` is
|
|
64
361
|
* whatever lets go of the database, when the vault opened it.
|
|
65
362
|
*/
|
|
66
|
-
async function openLocalVault(db, config, options = {}, close = async () => {}) {
|
|
67
|
-
const vault = openVault(db, await prepareVault(config, options));
|
|
363
|
+
async function openLocalVault(db, config, options = {}, close = async () => {}, hot) {
|
|
364
|
+
const vault = openVault(db, await prepareVault(config, options), hot);
|
|
68
365
|
const local = Object.fromEntries(METHODS.map((name) => [name, async (...args) => json(await vault[name](...json(args)))]));
|
|
69
366
|
return Object.assign(local, { close });
|
|
70
367
|
}
|
|
@@ -75,11 +372,12 @@ function json(value) {
|
|
|
75
372
|
//#region src/node.ts
|
|
76
373
|
/**
|
|
77
374
|
* The vault on Node, one of two ways, each over the database the server
|
|
78
|
-
* uses, through the vault's own login
|
|
375
|
+
* uses, through the vault's own login and its uses' (`hotDatabase`), with
|
|
376
|
+
* the checkpointer that seals its log in the same process (`checkpointer`):
|
|
79
377
|
*
|
|
80
|
-
* - in the server's own process: `vault: await localVault({ database, ...keys })`;
|
|
378
|
+
* - in the server's own process: `vault: await localVault({ database, hotDatabase, checkpointer, ...keys })`;
|
|
81
379
|
* - as a process of its own, which the server reaches over a Unix socket:
|
|
82
|
-
* `serveVault({ socket, database, ...keys })` there, and
|
|
380
|
+
* `serveVault({ socket, database, hotDatabase, checkpointer, ...keys })` there, and
|
|
83
381
|
* `vault: connectVault(socket)` in the server.
|
|
84
382
|
*
|
|
85
383
|
* The second keeps every key out of the process that faces the network. The
|
|
@@ -89,15 +387,59 @@ function json(value) {
|
|
|
89
387
|
* with the arguments as a JSON array, and refusals come back as values, as
|
|
90
388
|
* from any vault; only a failure is an error.
|
|
91
389
|
*/
|
|
92
|
-
/**
|
|
390
|
+
/**
|
|
391
|
+
* The vault in this process, and under 0.15's log, the default, its
|
|
392
|
+
* checkpointer, opened before the vault serves, so that no call meets a log
|
|
393
|
+
* with no checkpoint yet. A configuration without what 0.15 needs is refused.
|
|
394
|
+
*/
|
|
93
395
|
async function localVault(config, options = {}) {
|
|
94
396
|
const resolved = resolveVaultConfig(config);
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
|
|
397
|
+
const log015 = (options.log ?? "0.15") === "0.15";
|
|
398
|
+
const own = log015 && options.sealThrough === void 0;
|
|
399
|
+
if (log015 && config.hotDatabase === void 0) throw new Error("the vault's uses run as a login of their own: set hotDatabase in the vault's configuration, the database as coffre_vault_hot (HOT_DATABASE_URL)");
|
|
400
|
+
if (own && config.checkpointer?.memory === void 0) throw new Error("the vault answers a change once its checkpointer has sealed it: set checkpointer: { memory } in the vault's configuration, the file its signer remembers in (CHECKPOINTER_MEMORY)");
|
|
401
|
+
const opened = [];
|
|
402
|
+
const open = async (database) => {
|
|
403
|
+
if (typeof database !== "string") return database;
|
|
404
|
+
const { db, close } = await openDatabase(database);
|
|
405
|
+
opened.push(close);
|
|
406
|
+
return db;
|
|
407
|
+
};
|
|
408
|
+
let checkpointer = null;
|
|
409
|
+
const close = async () => {
|
|
410
|
+
await checkpointer?.stop();
|
|
411
|
+
await Promise.all(opened.map((done) => done()));
|
|
412
|
+
};
|
|
413
|
+
try {
|
|
414
|
+
const db = await open(config.database);
|
|
415
|
+
const hot = config.hotDatabase === void 0 ? void 0 : config.hotDatabase === config.database ? db : await open(config.hotDatabase);
|
|
416
|
+
const connections = hot?.$client?.options?.max;
|
|
417
|
+
const uses = {
|
|
418
|
+
...connections === void 0 ? {} : { connections },
|
|
419
|
+
...options.uses
|
|
420
|
+
};
|
|
421
|
+
if (own) {
|
|
422
|
+
const url = typeof config.database === "string" && engineOfUrl(config.database) === "postgres" ? config.database : null;
|
|
423
|
+
const kept = checkpointer = await keepCheckpointer({
|
|
424
|
+
...url === null ? { database: db } : { connect: () => openDatabase(url, { max: 2 }) },
|
|
425
|
+
seed: resolved.signingKeys[0],
|
|
426
|
+
memory: config.checkpointer.memory
|
|
427
|
+
});
|
|
428
|
+
options = {
|
|
429
|
+
...options,
|
|
430
|
+
sealThrough: kept.sealThrough,
|
|
431
|
+
heldOutside: kept.heldOutside,
|
|
432
|
+
sealing: kept.sealing
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
return await openLocalVault(db, resolved, {
|
|
436
|
+
...options,
|
|
437
|
+
uses
|
|
438
|
+
}, close, hot);
|
|
439
|
+
} catch (error) {
|
|
98
440
|
await close();
|
|
99
441
|
throw error;
|
|
100
|
-
}
|
|
442
|
+
}
|
|
101
443
|
}
|
|
102
444
|
const METHOD_NAMES = new Set(METHODS);
|
|
103
445
|
const MAX_BODY = 8388608;
|
|
@@ -182,4 +524,4 @@ async function readBody(stream) {
|
|
|
182
524
|
return Buffer.concat(chunks).toString("utf8");
|
|
183
525
|
}
|
|
184
526
|
//#endregion
|
|
185
|
-
export { KekBadClaimError, KekUnavailableError, awsKms, checkpointMessage, connectVault, derivedSigningKey, localVault, serveVault, verifyCheckpoint };
|
|
527
|
+
export { KekBadClaimError, KekUnavailableError, awsKms, checkpointMessage, connectVault, derivedSigningKey, fileMemory, keepCheckpointer, localVault, replayStored, runCheckpointer, serveVault, verifyCheckpoint };
|
|
@@ -83,7 +83,7 @@ function signingKeys(given, keks) {
|
|
|
83
83
|
return [signing, ...derived].filter((key, i, all) => all.findIndex((other) => other.equals(key)) === i);
|
|
84
84
|
}
|
|
85
85
|
function checkBulkLimit({ count, windowMinutes }) {
|
|
86
|
-
if (!Number.isInteger(count) || count < 1 || !(windowMinutes > 0) || windowMinutes > 1440) throw new Error(`bulkLimit needs a whole count of at least 1 and a window above 0 minutes, a day (${MAX_BULK_WINDOW_MINUTES}) at most`);
|
|
86
|
+
if (!Number.isInteger(count) || count < 1 || !(windowMinutes > 0) || windowMinutes > 1440 || !Number.isInteger(windowMinutes * 6e4)) throw new Error(`bulkLimit needs a whole count of at least 1 and a window above 0 minutes, a day (${MAX_BULK_WINDOW_MINUTES}) at most, a whole number of milliseconds`);
|
|
87
87
|
return {
|
|
88
88
|
count,
|
|
89
89
|
windowMs: windowMinutes * 6e4
|