@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.
Files changed (43) hide show
  1. package/README.md +6 -4
  2. package/dist/cloudflare.d.ts +341 -4
  3. package/dist/cloudflare.js +802 -6
  4. package/dist/index.js +1 -1
  5. package/dist/log-entry-BYWqj3O5.d.ts +364 -0
  6. package/dist/node.d.ts +181 -6
  7. package/dist/node.js +358 -16
  8. package/dist/{src-Bg0ZeMJW.js → src-DheG92EM.js} +1 -1
  9. package/dist/vault-kmtojYnW.js +14623 -0
  10. package/package.json +4 -5
  11. package/src/bulk.ts +89 -0
  12. package/src/changes.ts +478 -0
  13. package/src/checkpointer-node.ts +342 -0
  14. package/src/checkpointer-workers.ts +514 -0
  15. package/src/checkpointer.ts +1165 -0
  16. package/src/cloudflare.ts +207 -13
  17. package/src/config.ts +3 -2
  18. package/src/credentials.postgres.ts +78 -0
  19. package/src/credentials.sqlite.ts +240 -0
  20. package/src/credentials.ts +630 -0
  21. package/src/cutover.ts +275 -0
  22. package/src/decide.ts +437 -2
  23. package/src/inputs.ts +499 -0
  24. package/src/local.ts +20 -4
  25. package/src/log-checkpointer.ts +116 -0
  26. package/src/log-entry.ts +113 -0
  27. package/src/log-replay.ts +188 -0
  28. package/src/log-store.ts +695 -0
  29. package/src/node.ts +73 -9
  30. package/src/objects.ts +141 -0
  31. package/src/reads.ts +502 -0
  32. package/src/retention.ts +229 -0
  33. package/src/snapshot.postgres.ts +39 -9
  34. package/src/snapshot.sqlite.ts +28 -3
  35. package/src/snapshot.ts +64 -10
  36. package/src/store.ts +832 -36
  37. package/src/use.postgres.ts +111 -0
  38. package/src/use.sqlite.ts +542 -0
  39. package/src/use.ts +1060 -0
  40. package/src/vault.ts +1366 -220
  41. package/src/verifier.ts +136 -0
  42. package/src/witness-workers.ts +163 -0
  43. package/dist/vault-Cbi45kLB.js +0 -5197
@@ -0,0 +1,111 @@
1
+ import type { Queryable } from '@coffre/db';
2
+ import { rowsOf } from '@coffre/db/dialect';
3
+ import { sql } from 'drizzle-orm';
4
+
5
+ import { GATE_ROLES } from './decide.ts';
6
+ import type { UseCall, UseRow } from './use.ts';
7
+
8
+ /**
9
+ * A use on Postgres: one call of `use_credential` (0024's, as 0046 has it), one
10
+ * statement and one round trip, its own transaction. It answers its row as
11
+ * one JSON value (0024 says why), read back here as `UseRow`.
12
+ */
13
+ export async function callUse(db: Queryable, call: UseCall): Promise<UseRow> {
14
+ const known = call.known;
15
+ const [row] = await rowsOf<{ use: string }>(db, sql`SELECT use_credential(
16
+ ${call.credential}::uuid, ${call.secretId}::uuid, ${call.requestId}::bigint, ${String(call.at)}::bigint, ${call.purpose}::smallint, ${call.place}::uuid,
17
+ ${call.mac}::bytea, ${String(call.deadline)}::bigint,
18
+ ${call.cover?.number ?? null}::bigint, ${call.cover === undefined ? null : String(call.cover.at)}::bigint, ${call.cover?.requestId ?? null}::bigint,
19
+ ${known?.newest ?? null}::bigint, ${known?.newestMac ?? null}::bytea,
20
+ ${known?.cap.count ?? null}::integer, ${known === undefined ? null : String(known.cap.windowMs)}::bigint,
21
+ ${call.names?.tokenHash ?? null}::bytea, ${call.names?.project ?? null}::text, ${call.names?.environment ?? null}::text, ${call.names?.key ?? null}::text,
22
+ ${call.decided === true}::boolean,
23
+ ${call.names?.handle === true}::boolean,
24
+ ${call.names === undefined ? null : sql.param([...call.names.unless])}::text[],
25
+ ${call.names?.accepts.signin ?? null}::boolean,
26
+ ${call.names === undefined ? null : sql.param([...call.names.accepts.issuers])}::text[],
27
+ ${call.names === undefined ? null : GATE_ROLES}::jsonb,
28
+ ${call.way}::smallint, ${call.principal}::text
29
+ )::text AS use`);
30
+ return rowOf(row!.use);
31
+ }
32
+
33
+ /**
34
+ * A batch of uses by ids on Postgres (T167, B2): one call of `use_batch` (0048), one statement and one round trip, its
35
+ * own transaction, each key's use `use_credential`'s, in order. `lock`: the principal whose lock the batch takes
36
+ * exclusive before its first key (StrongestFirst), its keys decided. It answers a JSON array of `use_credential`'s rows.
37
+ */
38
+ export async function callBatch(db: Queryable, calls: readonly UseCall[], lock: string | null): Promise<UseRow[]> {
39
+ const [first] = calls;
40
+ if (first === undefined) throw new Error('a batch of uses names a key at least');
41
+ if (calls.some((call) => call.names !== undefined || call.credential !== first.credential || call.at !== first.at || call.purpose !== first.purpose
42
+ || call.deadline !== first.deadline || call.way !== first.way || call.principal !== first.principal)) {
43
+ throw new Error("a batch's uses are one request's, by ids: one way in, time, purpose and deadline");
44
+ }
45
+ const list = <T>(of: (call: UseCall) => T) => sql.param(calls.map(of));
46
+ const [row] = await rowsOf<{ uses: string }>(db, sql`SELECT use_batch(
47
+ ${first.credential}::uuid, ${list((call) => call.secretId)}::uuid[], ${list((call) => call.requestId)}::bigint[], ${String(first.at)}::bigint, ${first.purpose}::smallint,
48
+ ${list((call) => call.place)}::uuid[], ${list((call) => call.mac)}::bytea[], ${String(first.deadline)}::bigint,
49
+ ${list((call) => call.cover?.number ?? null)}::bigint[], ${list((call) => (call.cover === undefined ? null : String(call.cover.at)))}::bigint[], ${list((call) => call.cover?.requestId ?? null)}::bigint[],
50
+ ${list((call) => call.known?.newest ?? null)}::bigint[], ${list((call) => call.known?.newestMac ?? null)}::bytea[],
51
+ ${list((call) => call.known?.cap.count ?? null)}::integer[], ${list((call) => (call.known === undefined ? null : String(call.known.cap.windowMs)))}::bigint[],
52
+ ${list((call) => call.decided === true)}::boolean[],
53
+ ${first.way}::smallint, ${first.principal}::text, ${lock}::text
54
+ )::text AS uses`);
55
+ return rowsOfBatch(row!.uses);
56
+ }
57
+
58
+ /** `use_batch`'s JSON array, each element as `rowOf` reads one, the array read again exactly if any needs it. */
59
+ export function rowsOfBatch(text: string): UseRow[] {
60
+ let rows = JSON.parse(text) as Record<string, unknown>[];
61
+ if (!Array.isArray(rows)) throw new Error('use_batch answered no array');
62
+ if (rows.some(inexact)) rows = exactly(text) as Record<string, unknown>[];
63
+ return rows.map(rowFrom);
64
+ }
65
+
66
+ /** The row's bigints: as text, exactly. */
67
+ const WHOLE = ['number', 'covered', 'newest', 'inputs', 'c_expires', 'm_old', 'm_access', 'cap_window'] as const;
68
+ /** Its bytes, which JSON carries as `\x` and hex. */
69
+ const BYTES = [
70
+ 'newest_mac', 'c_token_hash', 'c_seal', 'c_last', 'm_seal', 'm_last', 'r_seal', 'r_last', 'p_seal', 'p_last', 's_seal', 's_last', 'x_seal', 'x_last',
71
+ 'v_ciphertext', 'v_iv', 'v_auth_tag', 'v_wrapped_dek', ...['e1', 'e2', 'e3', 'e4'].flatMap((slot) => [`${slot}_seal`, `${slot}_last`]),
72
+ ] as const;
73
+
74
+ /**
75
+ * `use_credential`'s JSON as `UseRow`. A JSON number past 2^53 would lose
76
+ * digits in `JSON.parse`: none of the row's is (entry numbers, times in
77
+ * ms), but one that were is read again with each number's own text, rather
78
+ * than misread (a reviver on every value costs 30 µs a use: measured).
79
+ */
80
+ export function rowOf(text: string): UseRow {
81
+ const row = JSON.parse(text) as Record<string, unknown>;
82
+ return rowFrom(inexact(row) ? exactly(text) as Record<string, unknown> : row);
83
+ }
84
+
85
+ const unsafe = (value: unknown) => typeof value === 'number' && !Number.isSafeInteger(value);
86
+
87
+ /** Whether a row as `JSON.parse` read it has a whole number it could not read exactly. */
88
+ function inexact(row: Record<string, unknown>): boolean {
89
+ return WHOLE.some((name) => unsafe(row[name])) || ((row.g_expires as unknown[] | null)?.some(unsafe) ?? false);
90
+ }
91
+
92
+ /** JSON read with each number past 2^53 as its own text. */
93
+ function exactly(text: string): unknown {
94
+ return JSON.parse(text, (_key, value: unknown, context?: { source?: string }) => {
95
+ if (!unsafe(value)) return value;
96
+ // A runtime without JSON.parse's source text: never a number misread.
97
+ if (context?.source === undefined) throw new Error('use_credential answered a whole number past 2^53, which this runtime cannot read exactly');
98
+ return context.source;
99
+ });
100
+ }
101
+
102
+ /** A row read, its whole numbers as text and its bytes as bytes. */
103
+ function rowFrom(row: Record<string, unknown>): UseRow {
104
+ for (const name of WHOLE) if (row[name] != null) row[name] = String(row[name]);
105
+ if (Array.isArray(row.g_expires)) row.g_expires = row.g_expires.map((value) => (value == null ? null : String(value)));
106
+ for (const name of BYTES) {
107
+ const value = row[name];
108
+ if (typeof value === 'string') row[name] = Buffer.from(value.slice(2), 'hex');
109
+ }
110
+ return row as UseRow;
111
+ }