@coffre/db 0.14.4 → 0.14.6
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/dist/connect.d.ts +1 -1
- package/dist/{database-CFhTnnzM.d.ts → database-wq1AVRAP.d.ts} +1 -1
- package/dist/dialect.d.ts +1 -1
- package/dist/grants.d.ts +1 -1
- package/dist/hyperdrive.d.ts +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/log.d.ts +27 -1
- package/dist/log.js +50 -1
- package/dist/migrations/postgres/0021_prune_vault_releases.sql +19 -0
- package/dist/migrations/postgres/meta/0021_snapshot.json +3334 -0
- package/dist/migrations/postgres/meta/_journal.json +7 -0
- package/dist/portable.d.ts +2 -2
- package/dist/{schema-z8GcN_Xu.d.ts → schema-BfQEBqFv.d.ts} +3 -2
- package/dist/schema-sqlite.d.ts +2 -2
- package/dist/schema-sqlite.js +2 -2
- package/dist/schema-version.d.ts +1 -1
- package/dist/schema-version.js +7 -0
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +3 -2
- package/package.json +2 -2
- package/src/log.ts +52 -0
- package/src/migrations/postgres/0021_prune_vault_releases.sql +19 -0
- package/src/migrations/postgres/meta/0021_snapshot.json +3334 -0
- package/src/migrations/postgres/meta/_journal.json +7 -0
- package/src/schema.sqlite.ts +2 -2
- package/src/schema.ts +3 -2
|
@@ -148,6 +148,13 @@
|
|
|
148
148
|
"when": 1793017780802,
|
|
149
149
|
"tag": "0020_vault_releases",
|
|
150
150
|
"breakpoints": true
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
"idx": 21,
|
|
154
|
+
"version": "7",
|
|
155
|
+
"when": 1793035191911,
|
|
156
|
+
"tag": "0021_prune_vault_releases",
|
|
157
|
+
"breakpoints": true
|
|
151
158
|
}
|
|
152
159
|
]
|
|
153
160
|
}
|
package/dist/portable.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { g as schema_d_exports } from "./schema-
|
|
2
|
-
import { t as Database } from "./database-
|
|
1
|
+
import { g as schema_d_exports } from "./schema-BfQEBqFv.js";
|
|
2
|
+
import { t as Database } from "./database-wq1AVRAP.js";
|
|
3
3
|
import { t as schema_sqlite_d_exports } from "./schema-sqlite.js";
|
|
4
4
|
import { LibSQLDatabase } from "drizzle-orm/libsql";
|
|
5
5
|
//#region src/portable.d.ts
|
|
@@ -1247,8 +1247,9 @@ declare const vaultGrants: import("drizzle-orm/pg-core").PgTableWithColumns<{
|
|
|
1247
1247
|
* read them from the entries before (an entry that names none, which only
|
|
1248
1248
|
* a forged one is, counts as a key of its own, `entry:<seq>`). No login
|
|
1249
1249
|
* writes it: the vault and the backup read it. A row a window old counts
|
|
1250
|
-
* for nothing
|
|
1251
|
-
*
|
|
1250
|
+
* for nothing: the vault's checkpoint forgets those a day and an hour old,
|
|
1251
|
+
* longer than any window (`prune_vault_releases`), so a count reads about a
|
|
1252
|
+
* day of keys.
|
|
1252
1253
|
*/
|
|
1253
1254
|
declare const vaultReleases: import("drizzle-orm/pg-core").PgTableWithColumns<{
|
|
1254
1255
|
name: "vault_releases";
|
package/dist/schema-sqlite.d.ts
CHANGED
|
@@ -1301,8 +1301,8 @@ export declare const vaultGrants: import("drizzle-orm/sqlite-core").SQLiteTableW
|
|
|
1301
1301
|
* read them from the entries before (an entry that names none, which only
|
|
1302
1302
|
* a forged one is, counts as a key of its own, `entry:<seq>`). No login
|
|
1303
1303
|
* writes it: the vault and the backup read it. A row a window old counts
|
|
1304
|
-
* for nothing
|
|
1305
|
-
*
|
|
1304
|
+
* for nothing: the vault's checkpoint forgets those a day and an hour old,
|
|
1305
|
+
* longer than any window, so a count reads about a day of keys.
|
|
1306
1306
|
*/
|
|
1307
1307
|
export declare const vaultReleases: import("drizzle-orm/sqlite-core").SQLiteTableWithColumns<{
|
|
1308
1308
|
name: "vault_releases";
|
package/dist/schema-sqlite.js
CHANGED
|
@@ -223,8 +223,8 @@ const vaultGrants = sqliteTable("vault_grants", {
|
|
|
223
223
|
* read them from the entries before (an entry that names none, which only
|
|
224
224
|
* a forged one is, counts as a key of its own, `entry:<seq>`). No login
|
|
225
225
|
* writes it: the vault and the backup read it. A row a window old counts
|
|
226
|
-
* for nothing
|
|
227
|
-
*
|
|
226
|
+
* for nothing: the vault's checkpoint forgets those a day and an hour old,
|
|
227
|
+
* longer than any window, so a count reads about a day of keys.
|
|
228
228
|
*/
|
|
229
229
|
const vaultReleases = sqliteTable("vault_releases", {
|
|
230
230
|
principal: text().notNull(),
|
package/dist/schema-version.d.ts
CHANGED
package/dist/schema-version.js
CHANGED
|
@@ -155,6 +155,13 @@ const KNOWN_MIGRATIONS = {
|
|
|
155
155
|
"when": 1793017780802,
|
|
156
156
|
"tag": "0020_vault_releases",
|
|
157
157
|
"breakpoints": true
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
"idx": 21,
|
|
161
|
+
"version": "7",
|
|
162
|
+
"when": 1793035191911,
|
|
163
|
+
"tag": "0021_prune_vault_releases",
|
|
164
|
+
"breakpoints": true
|
|
158
165
|
}
|
|
159
166
|
].map((entry) => entry.tag),
|
|
160
167
|
sqlite: [
|
package/dist/schema.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { C as vaultMembers, S as vaultGrants, _ as secretReferences, a as credentials, b as secretsRelations, c as enrollments, d as identities, f as mcpApprovals, h as projects, i as consumedTokens, l as environments, m as oauthClients, n as auditLog, o as deviceAuthorizations, p as mcpConnections, r as cappedAlerts, s as dismissedKeys, t as auditChainHead, u as environmentsRelations, v as secretVersions, w as vaultReleases, x as serviceBindings, y as secrets } from "./schema-
|
|
1
|
+
import { C as vaultMembers, S as vaultGrants, _ as secretReferences, a as credentials, b as secretsRelations, c as enrollments, d as identities, f as mcpApprovals, h as projects, i as consumedTokens, l as environments, m as oauthClients, n as auditLog, o as deviceAuthorizations, p as mcpConnections, r as cappedAlerts, s as dismissedKeys, t as auditChainHead, u as environmentsRelations, v as secretVersions, w as vaultReleases, x as serviceBindings, y as secrets } from "./schema-BfQEBqFv.js";
|
|
2
2
|
export { auditChainHead, auditLog, cappedAlerts, consumedTokens, credentials, deviceAuthorizations, dismissedKeys, enrollments, environments, environmentsRelations, identities, mcpApprovals, mcpConnections, oauthClients, projects, secretReferences, secretVersions, secrets, secretsRelations, serviceBindings, vaultGrants, vaultMembers, vaultReleases };
|
package/dist/schema.js
CHANGED
|
@@ -250,8 +250,9 @@ const vaultGrants = pgTable("vault_grants", {
|
|
|
250
250
|
* read them from the entries before (an entry that names none, which only
|
|
251
251
|
* a forged one is, counts as a key of its own, `entry:<seq>`). No login
|
|
252
252
|
* writes it: the vault and the backup read it. A row a window old counts
|
|
253
|
-
* for nothing
|
|
254
|
-
*
|
|
253
|
+
* for nothing: the vault's checkpoint forgets those a day and an hour old,
|
|
254
|
+
* longer than any window (`prune_vault_releases`), so a count reads about a
|
|
255
|
+
* day of keys.
|
|
255
256
|
*/
|
|
256
257
|
const vaultReleases = pgTable("vault_releases", {
|
|
257
258
|
principal: text().notNull(),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coffre/db",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.6",
|
|
4
4
|
"description": "coffre's database: the schema, its migrations and the connections, on Postgres or SQLite.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
"drizzle-orm": "0.45.2",
|
|
80
80
|
"pg": "8.16.3",
|
|
81
81
|
"pg-connection-string": "2.14.0",
|
|
82
|
-
"@coffre/core": "0.14.
|
|
82
|
+
"@coffre/core": "0.14.6"
|
|
83
83
|
},
|
|
84
84
|
"devDependencies": {
|
|
85
85
|
"@types/node": "26.1.1",
|
package/src/log.ts
CHANGED
|
@@ -59,6 +59,20 @@ export class LogHeadMismatch extends Error {
|
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
/**
|
|
63
|
+
* As many transactions as may already wait for the log's head do
|
|
64
|
+
* (`lockLogHeadQueued`): this one waits for nothing, and holds nothing.
|
|
65
|
+
* Its code is Postgres's for a lock not available, so every caller already
|
|
66
|
+
* answers it as one that gave up waiting: unavailable, ask again shortly.
|
|
67
|
+
*/
|
|
68
|
+
export class HeadBusy extends Error {
|
|
69
|
+
readonly code = '55P03';
|
|
70
|
+
constructor() {
|
|
71
|
+
super("too many requests wait for the audit log's head: ask again shortly");
|
|
72
|
+
this.name = 'HeadBusy';
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
62
76
|
/**
|
|
63
77
|
* The log's head is behind one this process wrote: the database was rolled
|
|
64
78
|
* back, or entries were cut off its end. That cannot be told from a restore,
|
|
@@ -155,6 +169,44 @@ export async function lockLogHeadIfFree(tx: Transaction): Promise<boolean> {
|
|
|
155
169
|
return rows.length > 0;
|
|
156
170
|
}
|
|
157
171
|
|
|
172
|
+
/**
|
|
173
|
+
* Lock the log's head, waiting for it as `lockLogHead` does, if fewer than
|
|
174
|
+
* `waiters` other transactions hold one of its places in line; otherwise
|
|
175
|
+
* refuse at once (`HeadBusy`), having waited for nothing. A place is an
|
|
176
|
+
* advisory lock, tried, never waited for, and held to the end of the
|
|
177
|
+
* transaction, which holds the head by then: so at most `waiters`
|
|
178
|
+
* connections wait for the head, or hold it, through here, and a burst
|
|
179
|
+
* beyond them answers in one round trip rather than seconds later, holding
|
|
180
|
+
* a connection all the while. One statement: the place, then, only if one
|
|
181
|
+
* was free, the head; refused, one more says whether the head is there at
|
|
182
|
+
* all. SQLite's transactions hold the whole file.
|
|
183
|
+
*/
|
|
184
|
+
export async function lockLogHeadQueued(tx: Transaction, waiters: number): Promise<{ nextSeq: bigint; headHash: Buffer }> {
|
|
185
|
+
if (engineOf(tx) === 'sqlite') return lockLogHead(tx);
|
|
186
|
+
const { rows } = await (tx as unknown as { execute(query: SQL): Promise<{ rows: { next_seq: string; head_hash: Buffer }[] }> }).execute(sql`
|
|
187
|
+
SELECT next_seq, head_hash FROM audit_chain_head
|
|
188
|
+
WHERE only_row AND (SELECT true FROM generate_series(1, ${waiters}::int) AS place WHERE pg_try_advisory_xact_lock(${HEAD_LINE}, place) LIMIT 1)
|
|
189
|
+
FOR UPDATE`);
|
|
190
|
+
const [head] = rows;
|
|
191
|
+
if (head === undefined) {
|
|
192
|
+
// No place, or no head at all: the second, which no one may write past, said as lockLogHead says it.
|
|
193
|
+
const { rows: [found] } = await (tx as unknown as { execute(query: SQL): Promise<{ rows: { found: boolean }[] }> }).execute(
|
|
194
|
+
sql`SELECT EXISTS (SELECT 1 FROM audit_chain_head WHERE only_row) AS found`,
|
|
195
|
+
);
|
|
196
|
+
if (found?.found !== true) throw new Error('audit_chain_head is missing; refusing to write an unchained entry');
|
|
197
|
+
throw new HeadBusy();
|
|
198
|
+
}
|
|
199
|
+
holding.add(tx);
|
|
200
|
+
return { nextSeq: BigInt(head.next_seq), headHash: head.head_hash };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* The first key of the head's places in line, Postgres's advisory locks:
|
|
205
|
+
* the second is the place. A space apart from the members' queues and the
|
|
206
|
+
* migrator's key.
|
|
207
|
+
*/
|
|
208
|
+
const HEAD_LINE = 0x636f6668;
|
|
209
|
+
|
|
158
210
|
const lockedHead = prepared((db) => {
|
|
159
211
|
const { auditChainHead } = tablesOf(db);
|
|
160
212
|
return forUpdate(
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
-- Releases pruned (W199, T111): the vault's checkpoint forgets the releases no window can count again, those over a day and an hour old by the database's clock, at most at_most a call. A bulk limit's window is a day at most (the vault's config), and the hour is a margin for the clock stepping back: no caller chooses what is kept, so the vault's login, which still deletes no row itself, cannot forget a release a window counts. The release before never calls it.
|
|
2
|
+
CREATE FUNCTION prune_vault_releases(at_most integer) RETURNS integer
|
|
3
|
+
LANGUAGE sql SECURITY DEFINER SET search_path = pg_catalog, pg_temp SET lock_timeout = '3s' AS $$
|
|
4
|
+
-- The bound in the DELETE's own WHERE too: a row a read moved on since the subquery's snapshot is checked again,
|
|
5
|
+
-- as it is now, and kept.
|
|
6
|
+
WITH bound AS (SELECT (extract(epoch from clock_timestamp()) * 1000)::bigint - 25 * 3600000 AS before),
|
|
7
|
+
pruned AS (
|
|
8
|
+
DELETE FROM public.vault_releases
|
|
9
|
+
WHERE released_at < (SELECT before FROM bound)
|
|
10
|
+
AND ctid IN (
|
|
11
|
+
-- Any of them, each past every window; a row an append holds is left to the next run, rather than waited for.
|
|
12
|
+
SELECT ctid FROM public.vault_releases WHERE released_at < (SELECT before FROM bound) LIMIT at_most FOR UPDATE SKIP LOCKED
|
|
13
|
+
)
|
|
14
|
+
RETURNING 1
|
|
15
|
+
)
|
|
16
|
+
SELECT count(*)::integer FROM pruned;
|
|
17
|
+
$$;--> statement-breakpoint
|
|
18
|
+
REVOKE ALL ON FUNCTION prune_vault_releases(integer) FROM PUBLIC;--> statement-breakpoint
|
|
19
|
+
GRANT EXECUTE ON FUNCTION prune_vault_releases(integer) TO coffre_vault;
|