@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
|
@@ -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
|
+
}
|