@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
package/src/use.ts ADDED
@@ -0,0 +1,1060 @@
1
+ import { NOTHING } from '@coffre/core/access';
2
+ import { ownerKey, type SecretContext } from '@coffre/core/envelope';
3
+ import { KekBadClaimError, KekUnavailableError, type KekRegistry } from '@coffre/core/kek';
4
+ import { type Access, type CredentialAcceptance, type NamedUseInput, type NamedUseOutcome, type UseBusy, type UseInput, type UseManyInput, type UseManyOutcome, type UseOutcome, type UsedValue } from '@coffre/core/vault';
5
+ import type { Queryable } from '@coffre/db';
6
+ import { engineOf } from '@coffre/db/dialect';
7
+
8
+ import { USE_DEADLINE_MS } from './bulk.ts';
9
+ import type { BulkLimit } from './config.ts';
10
+ import { checkSeals, credentialEnded, decideChecked, readerOf, standingRefusal, useEnvironments, type CheckedView, type Read, type UseCode, type UseRequest, type UseView, type Way, type WayInput } from './decide.ts';
11
+ import { noEdits, objectKey, sealedOnce, type EnvironmentInput, type Input, type InputGrant, type PolicyInput, type RolesInput, type SealKeys, type sealed } from './inputs.ts';
12
+ import { entryMac, MAC_BYTES, TAG_BYTES, type LogMacKey } from './log-entry.ts';
13
+ import { bindingObject, configObject, connectionObject, credentialObject, environmentObject, memberStanding, principalObject, secretObject } from './objects.ts';
14
+ import { PURPOSES, purposeCode, WAYS } from './log-replay.ts';
15
+ import { callBatch as batchPostgres, callUse as callPostgres } from './use.postgres.ts';
16
+ import { callBatch as batchSqlite, callUse as callSqlite } from './use.sqlite.ts';
17
+
18
+ /**
19
+ * 0.15's use (docs/design/0.15-plan.md, PR 5; hot-path-one.md, "A use";
20
+ * formal/HotPathOne.tla): one call, `use_credential`, then the decision on
21
+ * what it read, then the key, opened only once the call has returned,
22
+ * which is once its entry has committed (ReleaseAfterCommit). The log
23
+ * records the request; the outcome is `decideChecked`'s on what the
24
+ * statement read, which the replay of the log recomputes (R53).
25
+ *
26
+ * Around the call, what is per isolate (each a settled value, never work in
27
+ * flight: vault.ts, `PreparedVault`):
28
+ * - **admission:** at most `limit` calls in flight; the next is refused at
29
+ * once, `overloaded`, holding no connection (R44). A call's place is
30
+ * given back when it settles; on Workers, where a call may never settle,
31
+ * at the latest `HELD_PAST_DEADLINE_MS` past its deadline (`UseCache`);
32
+ * - **the deadline:** at most 3 s from the request's time (R53, R54). The
33
+ * call is raced against it and answered `late` when it loses; Postgres
34
+ * holds it too (DeadlineChecked), and a value whose call returns after
35
+ * it never leaves (WorkerHonoursDeadline);
36
+ * - **the seal cache:** each key's inputs' newest seals and the cap, as of
37
+ * the log's newest change (its number and MAC), which the next call
38
+ * names so that its statement probes nothing while that change is still
39
+ * the newest (CacheChecked, CacheSameChange);
40
+ * - **covers:** a repeat of an allowed use rides its entry, writing none,
41
+ * while no input changed since and the key's bulk row is fresh;
42
+ * - **no decision** for a use to take its principal's lock on (T158, D153):
43
+ * a named use that would take it makes a call that locks and writes
44
+ * nothing, decides the use on its rows (`decide`), and only then makes it,
45
+ * in a second call (DecideBeforeLock, DecideEachLock).
46
+ */
47
+
48
+ export type { UseInput, UseManyInput, UseManyOutcome, UseOutcome, UsedValue };
49
+
50
+ /** A use's request id (`newUseRequestId`, core's: the app makes them too). */
51
+ export { newUseRequestId as newRequestId } from '@coffre/core/vault';
52
+
53
+ /** What the vault holds per isolate for its uses (`UseCache`): only settled values. */
54
+ export type CachedSeals = {
55
+ /** The log's newest change when these were read: its number and MAC. */
56
+ newest: string;
57
+ newestMac: Buffer;
58
+ /** The cap its policy's newest change said then (CapSealed). */
59
+ cap: BulkLimit;
60
+ /** The newest change of any input then: a cover after it holds. */
61
+ inputs: string | null;
62
+ /** Which object each seal is of, as the rows named them: an input whose row names another now reads nothing from here. */
63
+ ids: Identities;
64
+ last: Lasts;
65
+ };
66
+
67
+ /** Which object each input's row is of (the credential, its principal, the key, its source, each place): the seals' keys. */
68
+ type Identities = Record<'c' | 'm' | 's' | 'x' | (typeof SLOTS)[number], string | null>;
69
+
70
+ /** Each input's newest change's seal, and the 0.14 entry the principal's accounts for. */
71
+ type Lasts = {
72
+ c: Buffer | null; m: Buffer | null; mOld: string | null; r: Buffer | null; p: Buffer | null; s: Buffer | null; x: Buffer | null;
73
+ e1: Buffer | null; e2: Buffer | null; e3: Buffer | null; e4: Buffer | null;
74
+ };
75
+
76
+ export type Cover = { number: string; at: number; requestId: string; until: number };
77
+
78
+ /**
79
+ * The settings of an isolate's uses: how many calls in flight at once, how long a cover holds, and how many requests'
80
+ * names it maps to ids (`UseCache.names`; 0 maps none, each request then reads its ids: the bench's worst case).
81
+ */
82
+ export type UseLimits = {
83
+ inFlight: number; coverMs: number; names: number;
84
+ /**
85
+ * The most keys a statement of a batch makes uses of (`useMany`): each statement holds the fence for its keys' uses,
86
+ * about 0.4 ms a key, and a change or the checkpointer's barrier waits behind it, every use queued behind that (R44).
87
+ * A batch of more is made in statements of this many, in turn (review r2 of #433): 100 hold the fence about 40 ms.
88
+ */
89
+ batch: number;
90
+ };
91
+
92
+ /**
93
+ * How many uses an isolate has in flight at most, unless set: a pool of
94
+ * `connections` serves that many at once, and a call past it would only
95
+ * queue for a connection, holding its deadline. T112 measures the number
96
+ * for Workers; here it is the pool's.
97
+ */
98
+ export function useLimits(connections: number, set: Partial<UseLimits> = {}): UseLimits {
99
+ return { inFlight: set.inFlight ?? Math.max(1, connections), coverMs: set.coverMs ?? 60_000, names: set.names ?? 10_000, batch: Math.max(1, set.batch ?? 100) };
100
+ }
101
+
102
+ /**
103
+ * How long past its deadline a call keeps its place at most where places lapse (`UseCache`, Workers): the hot login's
104
+ * `statement_timeout` (`coffre setup`'s HOT_SETTINGS, `migrate`). On Workers a call's connection is its own, closed
105
+ * at its deadline at the latest (cloudflare.ts, `HyperdrivePool.end`), or with its request if that ends first: what
106
+ * the call asked and Hyperdrive had not sent goes with it, and what reached Postgres by then Postgres ends within its
107
+ * `statement_timeout`. So by this long past the deadline nothing of the call's holds a connection, whether its
108
+ * promise ever settles or not, which on Workers it may never do: its I/O is gone with its request (AGENTS.md, "What
109
+ * an isolate keeps"). Counted until it settled, each such call kept its place until the isolate was recycled (T170).
110
+ */
111
+ export const HELD_PAST_DEADLINE_MS = 2_000;
112
+
113
+ /**
114
+ * An isolate's admission, seal cache and covers. Bounded: past `max` entries, a map starts over.
115
+ *
116
+ * A call holds its place in admission until it settles. With `callsEndWithRequest` (Workers), at most until `HELD_PAST_DEADLINE_MS`
117
+ * past its deadline, settled or not; without (Node), until it settles, as it always does there, its I/O never torn
118
+ * down under it: a call stuck queued for the hot pool or reading past its deadline (`oldWrite`) holds the pool too,
119
+ * and counts until it lets go, so that no call abandoned piles more on the pool (review r1 of #438).
120
+ */
121
+ export class UseCache {
122
+ /** Each call admitted and not settled, by when its place lapses (`admit`): a time, never work in flight. */
123
+ readonly #admitted = new Set<{ until: number }>();
124
+ readonly #lapses: boolean;
125
+ readonly seals = new Map<string, CachedSeals>();
126
+ readonly covers = new Map<string, Cover>();
127
+ /**
128
+ * What a request names, by its token's hash and place, mapped to ids (`useNamed`): ids only, never an answer, and
129
+ * no key's (`keyIds`). The least recently used goes first past `limits.names` (`named`, `name`). They only let a
130
+ * named use's first call make the use: what it reads, it reads by the request's names (D153).
131
+ */
132
+ readonly names = new Map<string, Resolved>();
133
+ /**
134
+ * Each key's id by its environment and name, whoever asks (`keyed`, `key`), or null for a name the environment held
135
+ * no key of when read: bounded on its own, so that names asked and dropped leave none behind (review r3 of #412).
136
+ */
137
+ readonly keyIds = new Map<string, string | null>();
138
+ readonly limits: UseLimits;
139
+ readonly max: number;
140
+
141
+ constructor(limits: UseLimits, { max = 10_000, callsEndWithRequest = false }: { max?: number; callsEndWithRequest?: boolean } = {}) {
142
+ this.limits = limits;
143
+ this.max = max;
144
+ this.#lapses = callsEndWithRequest;
145
+ }
146
+
147
+ /** How many calls hold a place now: admitted, not settled, and not lapsed. */
148
+ get inFlight(): number {
149
+ const now = Date.now();
150
+ for (const slot of this.#admitted) if (slot.until <= now) this.#admitted.delete(slot);
151
+ return this.#admitted.size;
152
+ }
153
+
154
+ /** Whether the next call is refused, `overloaded`. */
155
+ full(): boolean {
156
+ return this.inFlight >= this.limits.inFlight;
157
+ }
158
+
159
+ /** A place for a call due by `deadline`, given back once it settles (what this answers), unless it lapsed first. */
160
+ admit(deadline: number): () => void {
161
+ const slot = { until: this.#lapses ? deadline + HELD_PAST_DEADLINE_MS : Infinity };
162
+ this.#admitted.add(slot);
163
+ return () => this.#admitted.delete(slot);
164
+ }
165
+
166
+ keep<T>(map: Map<string, T>, key: string, value: T): void {
167
+ if (map.size >= this.max && !map.has(key)) map.clear();
168
+ map.set(key, value);
169
+ }
170
+
171
+ /** The ids `key` names, if mapped, made the most recently used. */
172
+ named(key: string): Resolved | undefined {
173
+ return recall(this.names, key);
174
+ }
175
+
176
+ /** Maps `key` to its ids, the least recently used dropped past the cap. */
177
+ name(key: string, ids: Resolved): void {
178
+ remember(this.names, key, ids, this.limits.names);
179
+ }
180
+
181
+ /** The id of the key named `key` in `environment`, or null for none, if mapped, made the most recently used. */
182
+ keyed(environment: string, key: string): string | null | undefined {
183
+ return recall(this.keyIds, `${environment}\n${key}`);
184
+ }
185
+
186
+ /** Maps a key's name in `environment` to its id, or null for none, the least recently used dropped past the cap. */
187
+ key(environment: string, key: string, id: string | null): void {
188
+ remember(this.keyIds, `${environment}\n${key}`, id, this.limits.names);
189
+ }
190
+
191
+ /** Forgets the key named `key` in `environment`. */
192
+ unkey(environment: string, key: string): void {
193
+ this.keyIds.delete(`${environment}\n${key}`);
194
+ }
195
+ }
196
+
197
+ /** A Map as an LRU: `key`'s value, moved to the end, the most recent; a Map iterates in the order of insertion. */
198
+ function recall<T>(map: Map<string, T>, key: string): T | undefined {
199
+ const found = map.get(key);
200
+ if (found !== undefined) {
201
+ map.delete(key);
202
+ map.set(key, found);
203
+ }
204
+ return found;
205
+ }
206
+
207
+ /** `key` mapped to `value`, the most recent, the least recent dropped past `cap` (0: none kept). */
208
+ function remember<T>(map: Map<string, T>, key: string, value: T, cap: number): void {
209
+ map.delete(key);
210
+ if (cap <= 0) return;
211
+ if (map.size >= cap) map.delete(map.keys().next().value!);
212
+ map.set(key, value);
213
+ }
214
+
215
+ /**
216
+ * One call of `use_credential` as the vault makes it: the request, its MAC, a cover and the cache, if any; `names`,
217
+ * what the request named, which a named call reads by. Its credential none: the isolate's map holds no ids for the
218
+ * names, and the call makes no use, only reads them (D153).
219
+ */
220
+ export type UseCall = {
221
+ credential: string | null; secretId: string | null; requestId: string; at: number; purpose: number; place: string | null; mac: Buffer; deadline: number;
222
+ /** Its way in (`WAYS`'s index: a credential, 0), and behind Access the person, `credential` their object's key (T147). */
223
+ way: number; principal: string | null;
224
+ cover?: Cover;
225
+ known?: Pick<CachedSeals, 'newest' | 'newestMac' | 'cap'>;
226
+ names?: Names;
227
+ /** The call a named use makes once the vault has decided the use on the rows of one that answered `decide` or `moved`. */
228
+ decided?: true;
229
+ };
230
+
231
+ /**
232
+ * What a request named, which a named call reads by (0045, D153), and answers the app's rows for (0044). `handle`: it
233
+ * named no place, and means its handle's own.
234
+ */
235
+ /**
236
+ * What a named call names (0045): its token's hash, its place by slugs, or the handle's own (`handle`), and its key.
237
+ * `unless`, a handle's: the places, `project/environment`, its request does not mean, which the call judges against the
238
+ * slugs it reads (never the map's), answering them as none, the app's.
239
+ */
240
+ export type Names = {
241
+ tokenHash: Buffer; project: string | null; environment: string | null; key: string | null; handle: boolean; unless: readonly string[];
242
+ /** What the app's config accepts of a credential (`NamedUseInput.accepts`), which the call holds the token's to first. */
243
+ accepts: CredentialAcceptance;
244
+ };
245
+
246
+ /**
247
+ * The ids of what a request names, as a named call read them: its credential and its place, and the place's slugs.
248
+ * Kept per token and place without its keys, which `UseCache.keyIds` keeps by environment and name, each map's least
249
+ * recently used dropped past `UseLimits.names`: about 200 and 120 bytes an entry, some 3 MB with both full at 10,000.
250
+ */
251
+ export type Resolved = { credential: string; environment: string; slugs: { project: string; environment: string } };
252
+
253
+ /** What `use_credential` answers (schema 0024's `use_row`), as node-postgres reads it; the SQLite twin answers the same. */
254
+ export type UseRow = {
255
+ state: string | null; number: string | null; covered: string | null; newest: string | null; newest_mac: Buffer | null; inputs: string | null; probed: boolean;
256
+ c_principal: string | null; c_kind: string | null; c_scope: string | null; c_revoked: boolean | null; c_expires: string | null; c_seal: Buffer | null; c_last: Buffer | null;
257
+ m_status: string | null; m_owner: boolean | null; m_role: string | null; m_scope: string | null; m_seal: Buffer | null; m_last: Buffer | null; m_old: string | null; m_access: string | null;
258
+ c_generation: number | null; m_generation: number | null;
259
+ g_project: (string | null)[] | null; g_environment: (string | null)[] | null; g_slug: (string | null)[] | null; g_role: string[] | null; g_expires: (string | null)[] | null;
260
+ r_value: string | null; r_seal: Buffer | null; r_last: Buffer | null;
261
+ p_value: string | null; p_seal: Buffer | null; p_last: Buffer | null; cap_count: number | null; cap_window: string | null;
262
+ s_project: string | null; s_environment: string | null; s_version: string | null; s_source: string | null; s_reference: string | null; s_seal: Buffer | null; s_last: Buffer | null;
263
+ x_project: string | null; x_environment: string | null; x_version: string | null; x_source: string | null; x_reference: string | null; x_seal: Buffer | null; x_last: Buffer | null;
264
+ v_envelope: number | null; v_ciphertext: Buffer | null; v_iv: Buffer | null; v_auth_tag: Buffer | null; v_wrapped_dek: Buffer | null;
265
+ v_kek_provider: string | null; v_kek_id: string | null; v_kek_version: string | null; v_sensitive: boolean | null; v_plain: boolean | null; v_plaintext: string | null;
266
+ in_set: boolean | null; others: number | null;
267
+ /** The credential's token hash, and the rows the app checks, when the call named them (0044). */
268
+ c_token_hash: Buffer | null; app: unknown;
269
+ /** What a named call's names name now: its credential, key and place, by id, and the place's slugs (0045, D153). */
270
+ n_credential: string | null; n_secret: string | null; n_place: string | null; n_project: string | null; n_environment: string | null;
271
+ /**
272
+ * Whether a named call found its caller standing at the place, for its purpose, and so read its key (`use_stands`,
273
+ * W268): false, it read no key and answered on the place alone, as for a key the place does not hold.
274
+ */
275
+ n_standing: boolean | null;
276
+ /** A Cloudflare binding's row, which a use through it read whole, its object (T147): as `to_json` writes it. */
277
+ w_row: Record<string, unknown> | null;
278
+ } & Slot<'e1'> & Slot<'e2'> & Slot<'e3'> & Slot<'e4'>;
279
+
280
+ type Slot<N extends string> = {
281
+ [K in `${N}_id` | `${N}_project` | `${N}_slug` | `${N}_owner` | `${N}_parent`]: string | null;
282
+ } & { [K in `${N}_protected` | `${N}_archived` | `${N}_project_archived`]: boolean | null } & { [K in `${N}_seal` | `${N}_last`]: Buffer | null };
283
+
284
+ const SLOTS = ['e1', 'e2', 'e3', 'e4'] as const;
285
+
286
+ /**
287
+ * What a use needs of its vault: its database, the keys it checks seals
288
+ * with, the log's MAC key (`logMacKey`, the change path's and the
289
+ * checkpointer's: one key, the tree under the MAC, KindInMac), its key
290
+ * registry, and its isolate's cache.
291
+ */
292
+ export type UsePath = {
293
+ db: Queryable;
294
+ sealKeys: SealKeys;
295
+ macKey: LogMacKey;
296
+ keks: KekRegistry;
297
+ cache: UseCache;
298
+ /**
299
+ * What a call waits for first, within its admission: an isolate's first, the log's cutover (vault.ts, `#cutOverYet`).
300
+ * Within it, as on Workers it reads on the call's one connection; however long it waits, its place lapses as any does.
301
+ */
302
+ ready?: () => Promise<void>;
303
+ /**
304
+ * Whether `principal`'s newest 0.14 access entry that carries the vault's
305
+ * MAC, at or below `through`, is past `old`, the newest its newest change
306
+ * accounts for (null: none): asked only when the statement's probe, which
307
+ * reads any entry in the vault's name, found one past it, `through`
308
+ * (UsesSeeOldWrites). An entry forged there is passed over (vault.ts,
309
+ * `#oldWrite`), which writes nothing. Never past `through`: the use's
310
+ * statement committed before this reads, and an entry since is no input it
311
+ * decided on. Absent, the probe's answer stands.
312
+ */
313
+ oldWrite?: (principal: string, old: bigint | null, through: bigint) => Promise<boolean>;
314
+ };
315
+
316
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
317
+ const REQUEST_ID = /^(0|[1-9][0-9]{0,18})$/;
318
+
319
+ /** Why a use's input is not one: a bug of the app's, which fails the call. */
320
+ function checkInput(input: Pick<UseInput, 'requestId' | 'at' | 'deadline' | 'keepsSensitive' | 'purpose'>): void {
321
+ if (!REQUEST_ID.test(input.requestId) || BigInt(input.requestId) >= 2n ** 63n) throw new Error('a use request id is a whole number below 2^63, as text');
322
+ if (!Number.isSafeInteger(input.at) || !Number.isSafeInteger(input.deadline)) throw new Error("a use's time and deadline are whole ms since the epoch");
323
+ if (input.keepsSensitive !== undefined && typeof input.keepsSensitive !== 'boolean') throw new Error('keepsSensitive is true, false or absent');
324
+ if (!(PURPOSES as readonly string[]).includes(input.purpose)) throw new Error(`no purpose ${String(input.purpose)}`);
325
+ }
326
+
327
+ function checkIds(input: { credential: string; secretId: string | null; place: string | null; way?: unknown }): void {
328
+ if (input.way !== undefined && !(WAYS as readonly unknown[]).includes(input.way)) throw new Error(`no way in ${String(input.way)}`);
329
+ // Behind Access, the account: a `user:` or a `token:`, as `vault_members` holds them.
330
+ const named = input.way === 'access' ? /^(user|token):[^\s]{1,320}$/.test(input.credential) : UUID.test(input.credential);
331
+ if (!named || (input.secretId !== null && !UUID.test(input.secretId)) || (input.place !== null && !UUID.test(input.place))) {
332
+ throw new Error('a use names its way in, its key and its place, if any, by lowercase UUID; behind Access, its account');
333
+ }
334
+ }
335
+
336
+ /**
337
+ * A use's way in as its entry names it (T147): a credential's, a connection's or a binding's id; behind Access, the
338
+ * person's object's key, and the person beside it, which the call reads their row by.
339
+ */
340
+ function wayOf(input: Pick<UseInput, 'credential' | 'way'>): { credential: string; way: Way; principal: string | null } {
341
+ const way = input.way ?? 'credential';
342
+ return way === 'access'
343
+ ? { credential: objectKey({ kind: 'principal', id: input.credential }), way, principal: input.credential }
344
+ : { credential: input.credential, way, principal: null };
345
+ }
346
+
347
+ /** Busy answers: nothing was decided, ask again shortly. */
348
+ const busy = (why: UseBusy, retryAfter = 1): Extract<UseOutcome, { busy: UseBusy }> => ({ ok: false, busy: why, retryAfter });
349
+
350
+ /** One use, end to end (the module's comment says how). */
351
+ export async function use(path: UsePath, input: UseInput): Promise<UseOutcome> {
352
+ checkInput(input);
353
+ checkIds(input);
354
+ const asked = { ...input, ...wayOf(input) };
355
+ let used = await useOnce(path, asked);
356
+ // Its key would take its principal's lock: decided first, as a named use is, and made by a second call once allowed;
357
+ // refused, nothing written, which the app logs (`number` null; review r2 of D153, DecideBeforeLock).
358
+ if ('again' in used) used = await useOnce(path, asked, undefined, true);
359
+ if (!('outcome' in used)) throw new Error('use_credential answered a named state to a use that named nothing');
360
+ // A plain value is the app's to hand back on a use by ids, as 0.14's vault never decides one: `decideChecked` refuses
361
+ // it, `bad_claim`, live and replayed alike; only a named use hands one back.
362
+ if ('plain' in used.outcome) throw new Error('a use by ids was handed a plain value');
363
+ return used.outcome;
364
+ }
365
+
366
+ /**
367
+ * A request's several keys, one call (T167, B2; formal/Batching.tla; migration 0048, `use_batch`): each key's use made
368
+ * as `use` makes it, by ids, in the request's order, in one statement (of at most `limits.batch` keys: more are made
369
+ * in statements of that many, in turn), and decided on what it read (`settle`), under one admission and one deadline. Then, only once every key is allowed on those rows (AllDecidedFirst), one more call
370
+ * for the keys that would take the principal's lock, which answered `decide` having written and locked nothing: it
371
+ * takes the lock exclusive before its first key (StrongestFirst), and each key is decided again on what it reads then.
372
+ * Values leave only once every key is allowed, all opened then (AnswerWhole); otherwise the first key in the request's
373
+ * order not allowed is the answer, refused or busy, and no key is opened.
374
+ *
375
+ * a CI run's `coffre export` of 30 keys 1 call, 30 entries (2, its keys entering its cap's set) the 30 values
376
+ * the same, a reference among them ended 1 call, the other keys' entries that key's refusal
377
+ *
378
+ * A refusal on a cover, which a lone use by ids makes again as an entry of its own, is answered as it is (its `number`
379
+ * null, the app logs it): the batch is refused, and makes no other call.
380
+ */
381
+ export async function useMany(path: UsePath, input: UseManyInput): Promise<UseManyOutcome> {
382
+ const { items, ...asked } = input;
383
+ if (!Array.isArray(items) || items.length === 0) throw new Error('a batch of uses names a key at least');
384
+ for (const item of items) {
385
+ checkInput({ ...asked, requestId: item.requestId });
386
+ checkIds({ credential: asked.credential, secretId: item.secretId, place: item.place, way: asked.way });
387
+ }
388
+ if (new Set(items.map((item) => item.requestId)).size !== items.length) throw new Error("a batch's uses each have a request id of their own");
389
+ const way = wayOf(asked);
390
+ const each: Asked[] = items.map((item) => ({ ...asked, ...way, secretId: item.secretId, place: item.place, requestId: item.requestId }));
391
+ const settled: (Used | Allowed | Uncovered | undefined)[] = [];
392
+ // The objects every key shares, their seals checked once for the batch.
393
+ const seals = sealedOnce();
394
+ let pending = each.map((_, index) => index);
395
+ for (let round = 1; pending.length > 0; round++) {
396
+ const decided = round > 1;
397
+ if (round > 2) throw new Error('use_batch answered `decide` to a call of keys it was decided for');
398
+ if (input.deadline > input.at + USE_DEADLINE_MS || Date.now() >= input.deadline) return busy('late');
399
+ if (path.cache.full()) return busy('overloaded');
400
+ const made = pending.map((index) => ready(path, each[index]!, requestOf(each[index]!, undefined), undefined, decided, seals));
401
+ // Round 2's keys, each decided on round 1's rows, take their principal's lock: exclusive, first. Rows that name two
402
+ // principals for one credential were written around the log: no lock first, and Postgres ends a wait cycle (40P01).
403
+ const principals = new Set(decided ? pending.map((index) => (settled[index] as Extract<Used, { again: Ids }>).row.c_principal) : []);
404
+ const lock = principals.size === 1 ? [...principals][0] ?? null : null;
405
+ const answered = await raced(path, input.deadline, (sends) => inStatements(path, made.map((one) => one.call), lock, sends), async (rows) => {
406
+ if (rows.length !== made.length) throw new Error(`use_batch answered ${rows.length} rows for ${made.length} keys`);
407
+ const forged = new Map<string, Promise<boolean>>();
408
+ // One check a principal and entry, as every key's row reads the same (`oldWrite`), in turn on the call's connection.
409
+ const forgedOf = async (row: UseRow, known: CachedSeals | undefined) => {
410
+ const at = `${row.c_principal}\n${row.m_access}\n${(known?.last ?? lastsOf(row)).mOld}`;
411
+ if (!forged.has(at)) forged.set(at, forgedPast(path, row, known));
412
+ return forged.get(at)!;
413
+ };
414
+ const out: { row: UseRow; forged: boolean }[] = [];
415
+ for (const [i, row] of rows.entries()) out.push({ row, forged: await forgedOf(row, row.probed ? undefined : made[i]!.known) });
416
+ return out;
417
+ });
418
+ if ('busy' in answered) return answered;
419
+ for (const [i, index] of pending.entries()) settled[index] = settle(path, each[index]!, undefined, decided, made[i]!, answered[i]!.row, answered[i]!.forged);
420
+ // The first key not allowed, in the request's order, answers for the batch, and no other call is made.
421
+ const first = settled.findIndex((one) => one !== undefined && !('again' in one) && !('allowed' in one));
422
+ if (first >= 0) return refusedAt(first, settled[first]!);
423
+ pending = settled.flatMap((one, index) => (one !== undefined && 'again' in one ? [index] : []));
424
+ }
425
+ // Every key allowed: each opened, within the deadline.
426
+ const opened = await Promise.all(settled.map((one) => opening(path, one as Allowed, input.deadline)));
427
+ const values: Extract<UseManyOutcome, { ok: true }>['values'] = [];
428
+ for (const [index, one] of opened.entries()) {
429
+ const outcome = 'outcome' in one ? one.outcome : null;
430
+ if (outcome === null || !outcome.ok || !('value' in outcome)) return refusedAt(index, one);
431
+ values.push({ number: outcome.number, covered: outcome.covered, value: outcome.value });
432
+ }
433
+ return { ok: true, values };
434
+ }
435
+
436
+ /** A batch's answer for the key at `index` that was not allowed. */
437
+ function refusedAt(index: number, used: Used | Allowed | Uncovered): Extract<UseManyOutcome, { ok: false }> {
438
+ if (!('outcome' in used)) throw new Error('a use by ids answered a named state');
439
+ const { outcome } = used;
440
+ if ('busy' in outcome) return outcome;
441
+ if (outcome.ok) throw new Error('a use allowed is no refusal');
442
+ if (!('code' in outcome)) throw new Error('a use by ids was handed a plain value');
443
+ return { ok: false, index, code: outcome.code, number: outcome.number };
444
+ }
445
+
446
+ /**
447
+ * A round's calls, `limits.batch` keys a statement, in turn, under the round's one admission and deadline: each its own
448
+ * transaction and fence span, a locked round's each taking the principal's lock exclusive before its first key.
449
+ */
450
+ async function inStatements(path: UsePath, calls: readonly UseCall[], lock: string | null, sends: () => void): Promise<UseRow[]> {
451
+ const rows: UseRow[] = [];
452
+ const size = path.cache.limits.batch;
453
+ for (let start = 0; start < calls.length; start += size) {
454
+ // Past the deadline, the round is answered `late` (`raced`): no statement more is sent, and the call lets go of its
455
+ // admission and connection with the one in flight (review r3 of #433). As Postgres's own past-deadline refusal.
456
+ if (start > 0) sends();
457
+ rows.push(...await batchOf(path.db)(path.db, calls.slice(start, start + size), lock));
458
+ }
459
+ return rows;
460
+ }
461
+
462
+ /** The engine's `use_batch`. */
463
+ function batchOf(db: Queryable): (db: Queryable, calls: readonly UseCall[], lock: string | null) => Promise<UseRow[]> {
464
+ return engineOf(db) === 'postgres' ? batchPostgres : batchSqlite;
465
+ }
466
+
467
+ /** A use's request as the vault makes it: by its ids, and its way in's (`wayOf`). */
468
+ type Ids = { credential: string; secretId: string | null; place: string | null; way?: Way; principal?: string | null };
469
+
470
+ /**
471
+ * A use's answer inside the vault: its outcome, its row, the view it decided on, if it got that far. A named one may
472
+ * also answer, having written and locked nothing: `unknown`, a token no credential has; `nowhere`, a place that is
473
+ * none, archived, a fork's or a personal config's, which the app answers itself, decided on the place alone; or, named
474
+ * or by ids, `again`, the use the vault allowed on the rows the call read, to be made by their ids, decided (D153).
475
+ */
476
+ type Used =
477
+ | { unknown: true } | { nowhere: true } | { again: Ids; row: UseRow }
478
+ | { outcome: UseOutcome | { ok: true; plain: string; versionId: string }; row: UseRow | null; checked: CheckedView | null; request: UseRequest };
479
+
480
+ /**
481
+ * One call of `use_credential`, and the decision on its row. `secretId` null with `place`: a key the place does not
482
+ * hold, decided on the caller's standing there (Probing.tla, MissingByStanding); a plain value, decided as any and
483
+ * handed back as its text, which its object's digest binds (PlainByStanding). Neither leaves an entry, nor a cover.
484
+ *
485
+ * `names`: what the request named, which the statement reads by (0045, D153); its ids, the isolate's map's, only let it
486
+ * make the use. A named call whose ids are not the names' answers `moved`, and one whose key would take its principal's
487
+ * lock with no use of it kept answers `decide`, each having written and locked nothing: the vault decides the use on
488
+ * the rows the names read, key and all, and refuses it there, whatever the reason, as for a key never made; only a use
489
+ * it allows is made, in a second call (DecideBeforeLock). A named call whose caller stands nowhere there, for what
490
+ * they ask, reads no key at all (`use_stands`, W268; StandingFirst): its row is a key the place does not hold, which
491
+ * the vault refuses on that standing.
492
+ */
493
+ async function useOnce(path: UsePath, input: Asked, names?: Names, decided = false): Promise<Used> {
494
+ const request = requestOf(input, names);
495
+ // A deadline the database would refuse (deadline_max) is refused here at no cost; one already past, too.
496
+ if (input.deadline > input.at + USE_DEADLINE_MS || Date.now() >= input.deadline) return { outcome: busy('late'), row: null, checked: null, request };
497
+ if (path.cache.full()) return { outcome: busy('overloaded'), row: null, checked: null, request };
498
+ const made = ready(path, input, request, names, decided);
499
+ // The statement, then, within the same admission and deadline, the MAC check of an access entry its probe found that
500
+ // the new log does not account for (`oldWrite`), never past that entry: one forged in the vault's name is passed over.
501
+ const answered = await raced(path, input.deadline, () => callOf(path.db)(path.db, made.call), async (row) => ({ row, forged: await forgedPast(path, row, row.probed ? undefined : made.known) }));
502
+ if ('busy' in answered) return { outcome: answered, row: null, checked: null, request };
503
+ const used = settle(path, input, names, decided, made, answered.row, answered.forged);
504
+ // Refused on a cover, by ids: the request is logged as an entry of its own.
505
+ if ('uncovered' in used) return useOnce(path, input);
506
+ return 'allowed' in used ? opening(path, used, input.deadline) : used;
507
+ }
508
+
509
+ /** What a use asks the vault, by ids: the request's, or a named call's ids as its isolate's map holds them (none: null). */
510
+ type Asked = Pick<UseInput, 'place' | 'purpose' | 'keepsSensitive' | 'requestId' | 'at' | 'deadline'> & {
511
+ credential: string | null; secretId: string | null; way?: Way; principal?: string | null;
512
+ };
513
+
514
+ /** The request a use decides, as the log's replay reads it back. */
515
+ function requestOf(input: Asked, names: Names | undefined): UseRequest {
516
+ const way = input.way ?? 'credential';
517
+ return {
518
+ credential: input.credential ?? '', ...(way === 'credential' ? {} : { way }), secretId: input.secretId ?? '', place: input.place, purpose: input.purpose,
519
+ keepsSensitive: input.keepsSensitive === true, at: input.at, ...(names === undefined ? {} : { plain: true as const }),
520
+ };
521
+ }
522
+
523
+ /** A use's call made ready, with what deciding on its row takes: its cache's keys, and the seals the call passed. */
524
+ type Ready = { call: UseCall; request: UseRequest; key: string; coverKey: string; known: CachedSeals | undefined; seals?: typeof sealed };
525
+
526
+ /** A use's call, its cover and its cached seals as the isolate holds them (`useOnce`, before the statement). */
527
+ function ready(path: UsePath, input: Asked, request: UseRequest, names: Names | undefined, decided: boolean, seals?: typeof sealed): Ready {
528
+ const { cache } = path;
529
+ const way = input.way ?? 'credential';
530
+ const purpose = purposeCode(request);
531
+ const key = `${input.credential ?? ''}\n${input.secretId ?? ''}\n${input.place ?? ''}`;
532
+ // The seals a probe of these ids read, for the cached form: a use by ids only. A named call always probes, so that
533
+ // its work is the same whatever the key and whatever this isolate kept of earlier uses (D153; review r12).
534
+ const known = input.credential === null || names !== undefined ? undefined : cache.seals.get(key);
535
+ // A cover is of the same request but its id and time: the same credential, key, place and purpose.
536
+ const coverKey = `${key}\n${purpose}`;
537
+ const held = input.secretId === null ? undefined : cache.covers.get(coverKey);
538
+ // A cover a change this isolate knows of came after is no cover; one it cannot judge, the statement judges.
539
+ const cover = held !== undefined && input.at < held.until && input.at >= held.at && (known?.inputs == null || BigInt(held.number) > BigInt(known.inputs)) ? held : undefined;
540
+ const call: UseCall = {
541
+ credential: input.credential, secretId: input.secretId, requestId: input.requestId, at: input.at, purpose, place: input.place,
542
+ way: WAYS.indexOf(way), principal: input.principal ?? null,
543
+ // No ids, no use: a call that only reads by names writes nothing, whatever its MAC (0045, `v_right`). Its key's tag
544
+ // all the same, so that every MAC the statement takes names the key the isolate holds (D162; T168 refuses by it).
545
+ mac: input.credential === null
546
+ ? Buffer.concat([path.macKey.tag, Buffer.alloc(MAC_BYTES - TAG_BYTES)])
547
+ : entryMac(path.macKey, { tree: 'uses', row: { at: input.at, requestId: BigInt(input.requestId), credential: input.credential, secret: input.secretId ?? '', purpose, place: input.place } }),
548
+ deadline: input.deadline,
549
+ ...(cover === undefined ? {} : { cover }),
550
+ ...(known === undefined ? {} : { known }),
551
+ ...(names === undefined ? {} : { names }),
552
+ ...(decided ? { decided: true as const } : {}),
553
+ };
554
+ return { call, request, key, coverKey, known, ...(seals === undefined ? {} : { seals }) };
555
+ }
556
+
557
+ /** A use by ids refused on its cover, nothing written of it: made again as an entry of its own, or answered as it is (a batch's). */
558
+ type Uncovered = { uncovered: true; outcome: Extract<UseOutcome, { code: UseCode }> };
559
+
560
+ /**
561
+ * The decision on a call's row (`useOnce`, after the statement): a use allowed with its key yet to open (`Allowed`), or
562
+ * refused on its cover by ids (`Uncovered`), or any other answer.
563
+ */
564
+ function settle(path: UsePath, input: Asked, names: Names | undefined, decided: boolean, made: Ready, row: UseRow, forged: boolean): Used | Allowed | Uncovered {
565
+ const { cache } = path;
566
+ const { request, key, coverKey, known } = made;
567
+ const way = input.way ?? 'credential';
568
+ if (row.state === 'late' || row.state === 'ahead' || row.state === 'unconfigured') return { outcome: busy(row.state, row.state === 'unconfigured' ? 5 : 1), row, checked: null, request };
569
+ if (names !== undefined && row.state === 'unknown') return { unknown: true };
570
+ if (names !== undefined && row.state === 'nowhere') return { nowhere: true };
571
+ if (row.state !== null && !((names !== undefined && (row.state === 'moved' || row.state === 'archived')) || (row.state === 'decide' && !decided))) {
572
+ throw new Error(`use_credential answered ${row.state}, which never leaves it`);
573
+ }
574
+ if (row.state !== null) {
575
+ // The rows the call read, by the names' ids or the use's own (D153): the use decided on them, key and all but the
576
+ // cap, which a use the vault allows weighs under its principal's lock. Refused, as for a key never made, whatever
577
+ // the reason, nothing written; a key that reads an archived place (a reference's source's) too, by name (review r4);
578
+ // allowed, made by those ids, in a second call. A moved call read every seal anew (0045), so the map's never judge
579
+ // the names' rows.
580
+ const ids: Ids = names === undefined
581
+ ? { credential: input.credential!, secretId: input.secretId, place: input.place, way, principal: input.principal ?? null }
582
+ : { credential: row.n_credential!, secretId: row.n_secret, place: row.n_secret === null ? row.n_place : null, way, principal: null };
583
+ const named: UseRequest = { ...request, credential: ids.credential, secretId: ids.secretId ?? '', place: ids.place };
584
+ const view = viewOf(row, named, row.probed ? undefined : known);
585
+ if (view.changed) cache.seals.delete(key);
586
+ const checked = checkedOf(path.sealKeys, view.view, named, view.accounted || forged, made.seals);
587
+ const code = decidedOn(checked, named, row) ?? (names !== undefined && readsArchived(row) ? 'archived' : null);
588
+ if (code !== null) return { outcome: { ok: false, code, number: null }, row, checked, request: named };
589
+ return { again: ids, row };
590
+ }
591
+ const view = viewOf(row, request, row.probed ? undefined : known);
592
+ // An input whose row names another object than the cache's seal is of: `stale`, and the next use probes.
593
+ if (view.changed) cache.seals.delete(key);
594
+ const checked = checkedOf(path.sealKeys, view.view, request, view.accounted || forged, made.seals);
595
+ const code = decidedOn(checked, request, row);
596
+ // The seals a probe read, kept for the next use's cached form: by ids alone. A named call never runs the cached form
597
+ // (above), which an earlier use's seals would choose for a key held and never for one not (Fable's review, r12).
598
+ if (row.probed && row.newest !== null && !view.changed && names === undefined) {
599
+ cache.keep(cache.seals, key, {
600
+ newest: row.newest, newestMac: row.newest_mac!, inputs: row.inputs, ids: view.ids,
601
+ cap: { count: Number(row.cap_count), windowMs: Number(row.cap_window) }, last: lastsOf(row),
602
+ });
603
+ }
604
+ if (row.covered !== null && code !== null) {
605
+ // Refused on a cover (its credential's end, say): by ids, the request is logged as an entry of its own. By name, it
606
+ // is answered as it is, in its one call, as a key the place does not hold is, and the app's entry records it: a
607
+ // second call would tell a key held from one never made (T158; Probing.tla, CoveredRefusalAnswered).
608
+ cache.covers.delete(coverKey);
609
+ if (names === undefined) return { uncovered: true, outcome: { ok: false, code, number: null } };
610
+ }
611
+ if (code !== null) {
612
+ cache.covers.delete(coverKey);
613
+ return { outcome: { ok: false, code, number: row.number }, row, checked, request };
614
+ }
615
+ const v = view.view;
616
+ const secret = v.secret.row!;
617
+ const value = secret.source === null ? secret : v.source!.row!;
618
+ // A plain value, decided: its text, which its object's digest binds; no entry, no cover (it counts toward nothing).
619
+ // Past its deadline, it never leaves, as a value whose key opened late (`open`).
620
+ if (value.key === null) {
621
+ if (Date.now() >= input.deadline) return { outcome: busy('late'), row, checked, request };
622
+ return { outcome: { ok: true, plain: row.v_plaintext!, versionId: value.version }, row, checked, request };
623
+ }
624
+ if (row.covered === null) {
625
+ const expires = checked.credential.row!.expiresAt ?? Infinity;
626
+ cache.keep(cache.covers, coverKey, { number: row.number!, at: input.at, requestId: input.requestId, until: Math.min(input.at + cache.limits.coverMs, expires) });
627
+ }
628
+ return { allowed: { view: v, number: row.covered ?? row.number!, covered: row.covered !== null }, row, checked, request };
629
+ }
630
+
631
+ /** A use allowed, its key yet to open: what `settle` answers of a sealed value, which its caller opens (`opening`). */
632
+ type Allowed = { allowed: { view: UseView; number: string; covered: boolean }; row: UseRow; checked: CheckedView; request: UseRequest };
633
+
634
+ /** A use's answer once its key is opened: the value, or `late` past its deadline, or `bad_claim` for a key not this value's. */
635
+ async function opening(path: UsePath, { allowed: { view, number, covered }, row, checked, request }: Allowed, deadline: number): Promise<Used> {
636
+ const opened = await open(path, view, row, deadline);
637
+ if (opened === 'late') return { outcome: busy('late'), row, checked, request };
638
+ if (opened === 'bad_claim') return { outcome: { ok: false, code: 'bad_claim', number }, row, checked, request };
639
+ return { outcome: { ok: true, number, covered, value: opened }, row, checked, request };
640
+ }
641
+
642
+ /**
643
+ * `decideChecked` on what a call read. A named call that found its caller standing nowhere read no key (`n_standing`,
644
+ * W268): refused as for a key the place does not hold, which the vault's rule refuses too, on the caller's standing
645
+ * (`standingRefusal`). Where that rule finds them standing, a row changed around the log between the gate's statement
646
+ * and the one that read the rest, each with its own snapshot (the fence holds back changes, not such writes): `stale`,
647
+ * which is what happened, never an answer of its own.
648
+ */
649
+ function decidedOn(checked: CheckedView, request: UseRequest, row: UseRow): UseCode | null {
650
+ if (request.plain === true && row.n_standing === false && standingRefusal(checked, request) === null) return 'stale';
651
+ return decideChecked(checked, request);
652
+ }
653
+
654
+ /** Whether a call read its key through an archived place: the place, the key's own, or a reference's source's (0045). */
655
+ function readsArchived(row: UseRow): boolean {
656
+ return (['e1', 'e2', 'e3'] as const).some((slot) => row[`${slot}_archived`] === true || row[`${slot}_project_archived`] === true);
657
+ }
658
+
659
+ /** The ids and slugs a named call read, which the isolate's map keeps for the request's names. */
660
+ function mapped(row: UseRow): Resolved | null {
661
+ if (row.n_credential === null || row.n_place === null || row.n_project === null || row.n_environment === null) return null;
662
+ return { credential: row.n_credential, environment: row.n_place, slugs: { project: row.n_project, environment: row.n_environment } };
663
+ }
664
+
665
+ /**
666
+ * A use as the request names it (T145, PR B; D153; formal/Probing.tla, Credentials.tla Revoking015Caller): one call,
667
+ * which reads what the request names by its names, whatever this isolate's map holds; the ids the map holds, if any,
668
+ * let it make the use in that call, as by ids. A use the vault refuses, whatever the reason, is refused in that one
669
+ * call, whatever the map held (Probing.tla, ResolveByName, NoIsFixed); one it allows on rows the map had no ids for
670
+ * is made by the ids the call read, in a second, and the map keeps them. One admission, checked before anything is
671
+ * read, so that a busy answer never hangs on whether a key is held. The request's route is the credential's and its
672
+ * account's alone (`routeOf`, Probing.tla, OneRoute): the key and the map decide the use, never how the app answers it.
673
+ */
674
+ export async function useNamed(path: UsePath, input: NamedUseInput): Promise<NamedUseOutcome> {
675
+ checkInput(input);
676
+ if ((input.key !== null && (typeof input.key !== 'string' || input.key === '')) || typeof input.tokenHash !== 'string') throw new Error('a named use names its token, by hash, and its key, if any');
677
+ const { accepts } = input;
678
+ if (typeof accepts?.signin !== 'boolean' || !Array.isArray(accepts.issuers) || accepts.issuers.some((issuer) => typeof issuer !== 'string')) {
679
+ throw new Error('a named use says what credentials the app accepts: whether it signs in, and its providers\' issuers');
680
+ }
681
+ const { cache } = path;
682
+ const tokenHash = Buffer.from(input.tokenHash, 'base64');
683
+ const busyFor = (outcome: Extract<UseOutcome, { busy: UseBusy }>) => ({ served: true as const, route: 'busy' as const, outcome });
684
+ if (input.deadline > input.at + USE_DEADLINE_MS || Date.now() >= input.deadline) return busyFor(busy('late'));
685
+ if (cache.full()) return busyFor(busy('overloaded'));
686
+ const at = `${input.tokenHash}\n${input.place?.project ?? ''}\n${input.place?.environment ?? ''}`;
687
+ const found = cache.named(at);
688
+ const keyed = found === undefined || input.key === null ? undefined : cache.keyed(found.environment, input.key);
689
+ // The ids the map holds, if it holds them all; else none, and the first call only reads.
690
+ const held: Ids | null = found === undefined || (input.key !== null && keyed === undefined) ? null
691
+ : { credential: found.credential, secretId: keyed ?? null, place: keyed == null ? found.environment : null };
692
+ const names: Names = {
693
+ tokenHash, project: input.place?.project ?? null, environment: input.place?.environment ?? null, key: input.key, handle: input.place === null,
694
+ // The places a stable URL does not mean, judged by the call against the slugs it reads, never the map's.
695
+ unless: input.place !== null ? [] : (input.unless ?? []).map((other) => `${other.project}/${other.environment}`),
696
+ accepts: { signin: accepts.signin, issuers: [...accepts.issuers] },
697
+ };
698
+ const asked = { purpose: input.purpose, keepsSensitive: input.keepsSensitive, requestId: input.requestId, at: input.at, deadline: input.deadline };
699
+ let used = await useOnce(path, { ...asked, ...(held ?? { credential: null, secretId: null, place: null }) }, names);
700
+ if ('again' in used) {
701
+ // Allowed on the rows its names read: the map keeps their ids, and the use is made by them, decided.
702
+ keep(cache, at, input.key, used.row);
703
+ used = await useOnce(path, { ...asked, ...used.again }, names, true);
704
+ // Out of date again, between its two calls: nothing decided, ask again.
705
+ if ('again' in used) return busyFor(busy('late'));
706
+ }
707
+ if ('unknown' in used) {
708
+ cache.names.delete(at);
709
+ // An unknown token: the credential's own check answers it, whatever it names.
710
+ return { served: true, route: 'credential', outcome: { ok: false, code: 'credential', number: null } };
711
+ }
712
+ // A place the vault serves not by name (none, archived, a fork's, a personal config's, one the URL does not mean):
713
+ // decided on the place, never the key.
714
+ if ('nowhere' in used) return { served: false };
715
+ if ('busy' in used.outcome) return busyFor(used.outcome);
716
+ const row = used.row!;
717
+ const checked = used.checked!;
718
+ keep(cache, at, input.key, row);
719
+ if (routeOf(checked, input.at) === 'credential') {
720
+ const outcome = used.outcome.ok ? null : used.outcome;
721
+ // The credential's seals bear out, ended or not, before any use is allowed (`decideChecked`, the credential first).
722
+ if (outcome === null || !('code' in outcome)) throw new Error('a use was allowed on a credential its seals do not bear out');
723
+ return { served: true, route: 'credential', outcome };
724
+ }
725
+ return {
726
+ served: true,
727
+ route: 'name',
728
+ secretId: used.request.secretId === '' ? null : used.request.secretId,
729
+ outcome: used.outcome,
730
+ app: row.app == null ? null : JSON.stringify(row.app),
731
+ standing: standingOf(checked, used.request),
732
+ place: input.place ?? mapped(row)?.slugs ?? null,
733
+ at: placeOf(checked, row),
734
+ };
735
+ }
736
+
737
+ /**
738
+ * The ids and slugs a named call read, kept in the isolate's map for the request's names and its key: never a key it
739
+ * did not read, its caller standing nowhere there (W268), which would map the name to none for whoever asks next.
740
+ */
741
+ function keep(cache: UseCache, at: string, key: string | null, row: UseRow): void {
742
+ const read = mapped(row);
743
+ if (read === null) return;
744
+ cache.name(at, read);
745
+ if (key !== null && row.n_standing !== false) cache.key(read.environment, key, row.n_secret);
746
+ }
747
+
748
+ /** The place a named call read its names at, by the ids of its row as the call read it: none for a row its seals do not bear out (T159). */
749
+ function placeOf(checked: CheckedView, row: UseRow): { projectId: string; environmentId: string } | null {
750
+ const read = row.n_place === null ? undefined : checked.environments.get(row.n_place);
751
+ const place = read !== undefined && read.valid && read.current ? read.row : null;
752
+ return place === null ? null : { projectId: place.projectId, environmentId: place.id };
753
+ }
754
+
755
+ /** What a named request's route hangs on: its credential and its account, as the use read and checked them. */
756
+ type CallerSeals = Pick<CheckedView, 'credential' | 'principal'>;
757
+
758
+ /**
759
+ * How the app answers a named request (D153, review r2): `name`, on the vault's decision, every refusal of the key
760
+ * alike; or `credential`, by the credential's own check (401, a token of 0.10's sealed first). On the credential and
761
+ * its account alone, as their seals bear them out, and whether the credential has ended: never the roles, a place, the
762
+ * key's rows or the isolate's map, which it cannot see, so that no key, however it is held, and no state of the map,
763
+ * moves a request from one path to the other (formal/Probing.tla, OneRoute).
764
+ */
765
+ export function routeOf(caller: CallerSeals, at: number): 'name' | 'credential' {
766
+ const { credential, principal } = caller;
767
+ if (credential.row === null || !credential.valid || !credential.current) return 'credential';
768
+ if (principal === null || !principal.valid || !principal.current) return 'credential';
769
+ return credentialEnded(credential.row, principal.row, at) ? 'credential' : 'name';
770
+ }
771
+
772
+ /**
773
+ * What the use decided the caller holds, as `access` answers it, from the sealed rows it read (`readerOf`): their
774
+ * standing, role, scope and live grants, and the policy as far as the environments it weighed, which is all the app
775
+ * then checks (the place it reads). Its credential and account bear out, as its route says. Roles, the policy or the
776
+ * place where the key is held or sought not as their seals say (put back, or written around the log): the use was
777
+ * refused on them (`decideChecked`, the seals first), and the caller holds nothing, so that nothing the app checks
778
+ * after reads those rows. Never the key's own rows, which a refusal of it alone says (D153).
779
+ */
780
+ function standingOf(checked: CheckedView, request: UseRequest): Access {
781
+ const credential = checked.credential.row!;
782
+ const principal = checked.principal!.row;
783
+ // Every place the use weighed (where its key is sought or held, read through, its value's, a personal config's parent):
784
+ // one a writer changed refused the use, and tells the app nothing either (T159, Fable on #431).
785
+ const weighed = useEnvironments(checked, request).flatMap((id) => {
786
+ const read = checked.environments.get(id);
787
+ return read === undefined ? [] : [read];
788
+ });
789
+ const sealed = [checked.roles, checked.policy, ...weighed].every((input) => input.valid && input.current);
790
+ // Only the rows their seals bear out: another a writer changed protects or names nothing the app is told (T159).
791
+ const rows = [...checked.environments.values()].flatMap((read) => (read.row === null || !read.valid || !read.current ? [] : [read.row]));
792
+ const reader = sealed
793
+ ? readerOf(principal, credential, checked.roles.row ?? noEdits(), rows, request.at)
794
+ : { status: readerOf(principal, credential, noEdits(), [], request.at).status, live: NOTHING };
795
+ const live = reader.live;
796
+ return {
797
+ principal: credential.principal,
798
+ status: reader.status,
799
+ generation: principal?.generation ?? 0,
800
+ role: live.role,
801
+ scope: live.scope,
802
+ grants: !sealed || reader.status !== 'active' || principal === null
803
+ ? []
804
+ : principal.grants.filter((grant) => grant.expiresAt === null || grant.expiresAt > request.at).map(({ projectId, environmentId, role, expiresAt }) => ({ projectId, environmentId, role, expiresAt })),
805
+ since: null,
806
+ by: null,
807
+ ...(live.policy === undefined ? {} : { policy: live.policy }),
808
+ } as Access;
809
+ }
810
+
811
+ /**
812
+ * The call, then `then` on its row (or a batch's rows), raced against the
813
+ * deadline (WorkerHonoursDeadline): lost, the use is answered `late`, and
814
+ * the call's row, if it ever comes, is dropped. Both hold one place in
815
+ * admission, as they hold the call's connection: what the vault reads
816
+ * after the statement (`oldWrite`) is within the same place and deadline.
817
+ * Past the deadline, or the race lost, the call starts nothing more, neither the statement
818
+ * after `ready` (nor a batch's next, `sends`) nor, the race lost, the reads after it: its
819
+ * answer has gone. One it had started may still reach Postgres (on Node, its bytes leave on
820
+ * the next turn of the loop), which refuses it past the deadline, and its row is dropped.
821
+ * A round of a batch is one call, its statements made in turn within it.
822
+ */
823
+ async function raced<R, T>(path: UsePath, deadline: number, call: (sends: () => void) => Promise<R>, then: (row: R) => Promise<T>): Promise<T | Extract<UseOutcome, { busy: UseBusy }>> {
824
+ const release = path.cache.admit(deadline);
825
+ // Whether the race is lost: once it is, `then` starts nothing. The timer alone says so, never the clock, so that a
826
+ // statement that answered first is decided on, its entry written (Fable's review of #438).
827
+ let lost = false;
828
+ // Before each statement: none past the deadline, nor once the race is lost. Both, as they disagree: a timer can fire
829
+ // a ms or two before `Date.now()` reaches the deadline (Node measures it from the loop's cached time), and a call
830
+ // answered `late` would still send its statement and write its use.
831
+ const sends = () => {
832
+ if (lost || Date.now() >= deadline) throw Object.assign(new Error('a use past its deadline sends nothing more'), { code: '57014' });
833
+ };
834
+ const running = (async () => {
835
+ await path.ready?.();
836
+ sends();
837
+ const row = await call(sends);
838
+ if (lost) throw Object.assign(new Error('a use that lost its race starts nothing more'), { code: '57014' });
839
+ return then(row);
840
+ })().finally(release);
841
+ let timer: ReturnType<typeof setTimeout> | undefined;
842
+ const late = new Promise<'late'>((resolve) => {
843
+ timer = setTimeout(() => {
844
+ lost = true;
845
+ resolve('late');
846
+ }, Math.max(0, deadline - Date.now()));
847
+ });
848
+ try {
849
+ const first = await Promise.race([running, late]);
850
+ if (first === 'late') {
851
+ running.catch(() => {});
852
+ return busy('late');
853
+ }
854
+ return first;
855
+ } catch (error) {
856
+ // node-postgres's code, on the error drizzle wraps it in.
857
+ const code = (error as { code?: string }).code ?? (error as { cause?: { code?: string } }).cause?.code;
858
+ // A replay of a call that wrote: its entry stands, and its value never left. Not decided again.
859
+ if (code === '23505') return busy('replayed');
860
+ // Past its deadline by Postgres's clock, or a lock not had within the hot login's lock_timeout: nothing written. A
861
+ // batch that waited on another for the principal's lock, which Postgres ended (40P01), the same: it never does when
862
+ // each takes the lock exclusive first (`use_batch`, StrongestFirst), unless rows written around the log name two
863
+ // principals for one credential.
864
+ if (code === '57014' || code === '55P03' || code === '40P01') return busy('late');
865
+ throw error;
866
+ } finally {
867
+ clearTimeout(timer);
868
+ }
869
+ }
870
+
871
+ /** The engine's `use_credential`. */
872
+ function callOf(db: Queryable): (db: Queryable, call: UseCall) => Promise<UseRow> {
873
+ return engineOf(db) === 'postgres' ? callPostgres : callSqlite;
874
+ }
875
+
876
+ /**
877
+ * Whether the access entry the statement's probe found past what the
878
+ * principal's newest change accounts for (`viewOf`'s `accounted` false) is
879
+ * one forged in the vault's name, as is every one past it up to the probe's:
880
+ * then it is passed over, as Migration015.tla's UsesSeeOldWrites reads only
881
+ * authentic writes, and the use is decided as its replay decides it.
882
+ */
883
+ async function forgedPast(path: UsePath, row: UseRow, known?: CachedSeals): Promise<boolean> {
884
+ // A row that answers a state (`moved`, `unknown`, `unconfigured`) may name none of these: nothing to check.
885
+ if (path.oldWrite === undefined || row.m_access == null || row.c_principal == null) return false;
886
+ const old = (known?.last ?? lastsOf(row)).mOld ?? null;
887
+ if (old !== null && BigInt(row.m_access) <= BigInt(old)) return false;
888
+ return !(await path.oldWrite(row.c_principal, old === null ? null : BigInt(old), BigInt(row.m_access)));
889
+ }
890
+
891
+ function lastsOf(row: UseRow): Lasts {
892
+ return {
893
+ c: row.c_last, m: row.m_last, mOld: row.m_old, r: row.r_last, p: row.p_last, s: row.s_last, x: row.x_last,
894
+ e1: row.e1_last, e2: row.e2_last, e3: row.e3_last, e4: row.e4_last,
895
+ };
896
+ }
897
+
898
+ /**
899
+ * What the statement read, as PR 3's `UseView` (decide.ts): each input's
900
+ * object, made from its row's columns (objects.ts), its seal, and its
901
+ * newest change's, probed or the cache's. An input whose row names another
902
+ * object than the cache's seal is of (a reference's source repointed, a
903
+ * credential's principal, written around the log) has no newest seal the
904
+ * vault knows: it is not current, and decides `stale` (review r2 of D153).
905
+ * Each input alone: the credential's and its account's judgement never
906
+ * hangs on the key's rows, which a request's route needs (`routeOf`).
907
+ * `changed`: some input's did, and the next use probes. `accounted`: the
908
+ * principal's newest 0.14 access entry is one the new log accounts for
909
+ * (UsesSeeOldWrites).
910
+ */
911
+ export function viewOf(row: UseRow, request: UseRequest, known?: CachedSeals): { view: UseView; ids: Identities; changed: boolean; accounted: boolean } {
912
+ const ids: Identities = {
913
+ c: request.credential, m: row.c_principal, s: request.secretId, x: row.s_source,
914
+ e1: row.e1_id, e2: row.e2_id, e3: row.e3_id, e4: row.e4_id,
915
+ };
916
+ const moved = (Object.keys(ids) as (keyof Identities)[]).filter((input) => known !== undefined && known.ids[input] !== ids[input]);
917
+ const cached = known?.last ?? lastsOf(row);
918
+ // No seal is equal to none: an input moved is current to nothing.
919
+ const last = (input: keyof Identities): Buffer | null => (moved.includes(input) ? UNKNOWN : cached[input]);
920
+ const read = <T extends Input>(object: T | null, seal: Buffer | null, newest: Buffer | null): Read<T> => ({ row: object, seal, newest });
921
+ const credential: Read<WayInput> = read(wayObject(row, request), row.c_seal, last('c'));
922
+ const principal = row.c_principal === null ? null : read(row.m_status === null ? null : principalObject({
923
+ principal: row.c_principal, status: row.m_status, generation: row.m_generation!, ...memberStanding({ owner: row.m_owner === true, role: row.m_role, scope: row.m_scope }),
924
+ }, grantsOf(row)), row.m_seal, last('m'));
925
+ // A plain value's text, which its object's digest binds.
926
+ const version = row.v_wrapped_dek === null ? null : {
927
+ sensitive: row.v_sensitive === true, plaintext: row.v_plaintext,
928
+ kekProvider: row.v_kek_provider!, kekId: row.v_kek_id!, kekVersion: row.v_kek_version!, wrappedDek: row.v_wrapped_dek.toString('base64'),
929
+ };
930
+ const secret = read(row.s_environment === null ? null : secretObject({
931
+ id: request.secretId, projectId: row.s_project!, environmentId: row.s_environment, source: row.s_source, reference: row.s_reference,
932
+ version: row.s_source === null && version !== null ? { id: row.s_version!, ...version } : null,
933
+ }), row.s_seal, last('s'));
934
+ const source = row.s_source === null ? null : read(row.x_environment === null ? null : secretObject({
935
+ id: row.s_source, projectId: row.x_project!, environmentId: row.x_environment, source: row.x_source, reference: row.x_reference,
936
+ version: row.x_source === null && version !== null ? { id: row.x_version!, ...version } : null,
937
+ }), row.x_seal, last('x'));
938
+ const environments = new Map<string, Read<EnvironmentInput>>();
939
+ for (const slot of SLOTS) {
940
+ const id = row[`${slot}_id`];
941
+ if (id === null || environments.has(id)) continue;
942
+ const project = row[`${slot}_project`];
943
+ environments.set(id, read(project === null ? null : environmentObject({
944
+ id, projectId: project, slug: row[`${slot}_slug`]!, owner: row[`${slot}_owner`], parentId: row[`${slot}_parent`],
945
+ protected: row[`${slot}_protected`], archived: row[`${slot}_archived`] === true, projectArchived: row[`${slot}_project_archived`] === true,
946
+ }), row[`${slot}_seal`], last(slot)));
947
+ }
948
+ const view: UseView = {
949
+ credential,
950
+ principal,
951
+ roles: read(row.r_value === null ? null : configObject(row.r_value) as RolesInput, row.r_seal, cached.r),
952
+ policy: read(row.p_value === null ? null : configObject(row.p_value) as PolicyInput, row.p_seal, cached.p),
953
+ secret,
954
+ source,
955
+ environments,
956
+ // Behind Access, the account is the way in: its cap is read with it.
957
+ bulk: (request.way === 'access' ? principal?.row ?? null : credential.row) === null ? null : { inSet: row.in_set === true, others: Number(row.others ?? 0) },
958
+ };
959
+ // UsesSeeOldWrites: 0.14's newest access entry about the principal, at or below the one its newest change accounts for.
960
+ const old = cached.mOld;
961
+ const accounted = row.m_access === null || (old !== null && BigInt(row.m_access) <= BigInt(old));
962
+ return { view, ids, changed: moved.length > 0, accounted };
963
+ }
964
+
965
+ const UNKNOWN = Buffer.alloc(0);
966
+
967
+ /**
968
+ * The way in's object, from its row's columns as the call read them (T147): a credential's or a connection's, its
969
+ * columns; a binding's, its whole row (`w_row`); behind Access none, the account's row being all of it.
970
+ */
971
+ function wayObject(row: UseRow, request: UseRequest): WayInput | null {
972
+ if (row.c_principal === null) return null;
973
+ const expiresAt = row.c_expires === null ? null : Number(row.c_expires);
974
+ switch (request.way ?? 'credential') {
975
+ case 'credential':
976
+ return credentialObject({ id: request.credential, principal: row.c_principal, kind: row.c_kind!, scope: row.c_scope, revoked: row.c_revoked === true, expiresAt, generation: row.c_generation! });
977
+ case 'connection':
978
+ return connectionObject({ id: request.credential, principal: row.c_principal, generation: row.c_generation!, expiresAt: expiresAt!, revoked: row.c_revoked === true });
979
+ case 'binding': {
980
+ const b = row.w_row;
981
+ if (b === null) return null;
982
+ const text = (value: unknown) => (value == null ? null : String(value));
983
+ return bindingObject({
984
+ id: request.credential, principal: String(b.principal), generation: Number(b.generation), profile: String(b.profile), issuer: String(b.issuer),
985
+ // A sealed binding names the instance it was made on (0040): one with none bears out no seal.
986
+ origin: text(b.origin) ?? '', jwksUri: String(b.jwks_uri), claims: String(b.claims), scope: text(b.scope), policy: text(b.policy), revoked: b.revoked_at != null,
987
+ });
988
+ }
989
+ case 'access':
990
+ return null;
991
+ }
992
+ }
993
+
994
+ function grantsOf(row: UseRow): InputGrant[] {
995
+ return (row.g_role ?? []).map((role, i) => ({
996
+ projectId: row.g_project![i] ?? null,
997
+ environmentId: row.g_environment![i] ?? null,
998
+ environmentSlug: row.g_slug![i] ?? null,
999
+ role,
1000
+ expiresAt: row.g_expires![i] == null ? null : Number(row.g_expires![i]),
1001
+ }));
1002
+ }
1003
+
1004
+ /**
1005
+ * The view checked as PR 3's decision takes it, with the principal `stale`
1006
+ * too while 0.14 holds an access entry about it that the new log does not
1007
+ * account for (Migration015.tla, UsesSeeOldWrites): a seal the principal's
1008
+ * row passes could still be one a late 0.14 revoke moved past. Checked with
1009
+ * the seals, before any row (StaleFirst).
1010
+ */
1011
+ function checkedOf(keys: SealKeys, view: UseView, request: UseRequest, accounted: boolean, seals?: typeof sealed): CheckedView {
1012
+ const checked = checkSeals(keys, view, request, seals);
1013
+ const principal = checked.principal === null || accounted ? checked.principal : { ...checked.principal, current: false };
1014
+ return { ...checked, principal };
1015
+ }
1016
+
1017
+ /**
1018
+ * The value's data key, opened as its version's context binds it: its ids,
1019
+ * a personal config's owner, whether it is sensitive. Within the use's
1020
+ * deadline, which the key operation is bound by too: past it, nothing
1021
+ * leaves (`late`). A key that does not open as this value's is a claim the
1022
+ * rows do not bear out (`bad_claim`).
1023
+ */
1024
+ async function open(path: UsePath, view: UseView, row: UseRow, deadline: number): Promise<UsedValue | 'late' | 'bad_claim'> {
1025
+ const secret = view.secret.row!;
1026
+ const value = secret.source === null ? secret : view.source!.row!;
1027
+ const owner = view.environments.get(value.environmentId)?.row?.owner ?? null;
1028
+ const context: SecretContext = {
1029
+ projectId: value.projectId, environmentId: value.environmentId, secretId: value.id,
1030
+ ...(owner === null ? {} : { owner: ownerKey(owner) }),
1031
+ ...(value.nature === 'sensitive' ? { sensitive: true as const } : {}),
1032
+ };
1033
+ const left = deadline - Date.now();
1034
+ if (left <= 0) return 'late';
1035
+ const controller = new AbortController();
1036
+ const timer = setTimeout(() => controller.abort(), left);
1037
+ let dek: Buffer;
1038
+ try {
1039
+ const wrapped = value.key!;
1040
+ dek = await path.keks.unwrap({ ...wrapped, bytes: Buffer.from(wrapped.bytes, 'base64') }, context, { deadline, signal: controller.signal });
1041
+ } catch (error) {
1042
+ if (error instanceof KekBadClaimError) return 'bad_claim';
1043
+ if (error instanceof KekUnavailableError && controller.signal.aborted) return 'late';
1044
+ throw error;
1045
+ } finally {
1046
+ clearTimeout(timer);
1047
+ }
1048
+ try {
1049
+ // Opened after its deadline: it never leaves.
1050
+ if (Date.now() >= deadline) return 'late';
1051
+ return {
1052
+ key: dek.toString('base64'),
1053
+ versionId: value.version,
1054
+ context,
1055
+ envelope: { version: row.v_envelope!, ciphertext: row.v_ciphertext!.toString('base64'), iv: row.v_iv!.toString('base64'), authTag: row.v_auth_tag!.toString('base64') },
1056
+ };
1057
+ } finally {
1058
+ dek.fill(0);
1059
+ }
1060
+ }