@coffre/db 0.14.3 → 0.14.5
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-Ds1Y79h6.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/0020_vault_releases.sql +36 -0
- package/dist/migrations/postgres/0021_prune_vault_releases.sql +19 -0
- package/dist/migrations/postgres/meta/0020_snapshot.json +3334 -0
- package/dist/migrations/postgres/meta/0021_snapshot.json +3334 -0
- package/dist/migrations/postgres/meta/_journal.json +14 -0
- package/dist/migrations/sqlite/0017_vault_releases.sql +26 -0
- package/dist/migrations/sqlite/meta/0017_snapshot.json +3037 -0
- package/dist/migrations/sqlite/meta/_journal.json +7 -0
- package/dist/portable.d.ts +2 -2
- package/dist/{schema-DcVkJhCf.d.ts → schema-BfQEBqFv.d.ts} +73 -2
- package/dist/schema-sqlite.d.ts +75 -1
- package/dist/schema-sqlite.js +21 -1
- package/dist/schema-version.d.ts +1 -1
- package/dist/schema-version.js +21 -0
- package/dist/schema.d.ts +2 -2
- package/dist/schema.js +24 -2
- package/package.json +2 -2
- package/src/log.ts +52 -0
- package/src/migrations/postgres/0020_vault_releases.sql +36 -0
- package/src/migrations/postgres/0021_prune_vault_releases.sql +19 -0
- package/src/migrations/postgres/meta/0020_snapshot.json +3334 -0
- package/src/migrations/postgres/meta/0021_snapshot.json +3334 -0
- package/src/migrations/postgres/meta/_journal.json +14 -0
- package/src/migrations/sqlite/0017_vault_releases.sql +26 -0
- package/src/migrations/sqlite/meta/0017_snapshot.json +3037 -0
- package/src/migrations/sqlite/meta/_journal.json +7 -0
- package/src/schema.sqlite.ts +22 -0
- package/src/schema.ts +25 -0
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
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
declare namespace schema_d_exports {
|
|
2
|
-
export { auditChainHead, auditLog, cappedAlerts, consumedTokens, credentials, deviceAuthorizations, dismissedKeys, enrollments, environments, environmentsRelations, identities, mcpApprovals, mcpConnections, oauthClients, projects, secretReferences, secretVersions, secrets, secretsRelations, serviceBindings, vaultGrants, vaultMembers };
|
|
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 };
|
|
3
3
|
}
|
|
4
4
|
declare const projects: import("drizzle-orm/pg-core").PgTableWithColumns<{
|
|
5
5
|
name: "projects";
|
|
@@ -1238,6 +1238,77 @@ declare const vaultGrants: import("drizzle-orm/pg-core").PgTableWithColumns<{
|
|
|
1238
1238
|
};
|
|
1239
1239
|
dialect: "pg";
|
|
1240
1240
|
}>;
|
|
1241
|
+
/**
|
|
1242
|
+
* Each key the vault released to a principal, and when last: what its bulk
|
|
1243
|
+
* limit counts (R28), the distinct keys released within a window, without
|
|
1244
|
+
* reading the log. The database keeps it from the log itself, in the
|
|
1245
|
+
* transaction of each vault `secret.read` allowed: a trigger on
|
|
1246
|
+
* `audit_log` upserts a row for each key the entry names, as the count
|
|
1247
|
+
* read them from the entries before (an entry that names none, which only
|
|
1248
|
+
* a forged one is, counts as a key of its own, `entry:<seq>`). No login
|
|
1249
|
+
* writes it: the vault and the backup read it. A row a window old counts
|
|
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.
|
|
1253
|
+
*/
|
|
1254
|
+
declare const vaultReleases: import("drizzle-orm/pg-core").PgTableWithColumns<{
|
|
1255
|
+
name: "vault_releases";
|
|
1256
|
+
schema: undefined;
|
|
1257
|
+
columns: {
|
|
1258
|
+
principal: import("drizzle-orm/pg-core").PgColumn<{
|
|
1259
|
+
name: "principal";
|
|
1260
|
+
tableName: "vault_releases";
|
|
1261
|
+
dataType: "string";
|
|
1262
|
+
columnType: "PgText";
|
|
1263
|
+
data: string;
|
|
1264
|
+
driverParam: string;
|
|
1265
|
+
notNull: true;
|
|
1266
|
+
hasDefault: false;
|
|
1267
|
+
isPrimaryKey: false;
|
|
1268
|
+
isAutoincrement: false;
|
|
1269
|
+
hasRuntimeDefault: false;
|
|
1270
|
+
enumValues: [string, ...string[]];
|
|
1271
|
+
baseColumn: never;
|
|
1272
|
+
identity: undefined;
|
|
1273
|
+
generated: undefined;
|
|
1274
|
+
}, {}, {}>;
|
|
1275
|
+
key: import("drizzle-orm/pg-core").PgColumn<{
|
|
1276
|
+
name: "key";
|
|
1277
|
+
tableName: "vault_releases";
|
|
1278
|
+
dataType: "string";
|
|
1279
|
+
columnType: "PgText";
|
|
1280
|
+
data: string;
|
|
1281
|
+
driverParam: string;
|
|
1282
|
+
notNull: true;
|
|
1283
|
+
hasDefault: false;
|
|
1284
|
+
isPrimaryKey: false;
|
|
1285
|
+
isAutoincrement: false;
|
|
1286
|
+
hasRuntimeDefault: false;
|
|
1287
|
+
enumValues: [string, ...string[]];
|
|
1288
|
+
baseColumn: never;
|
|
1289
|
+
identity: undefined;
|
|
1290
|
+
generated: undefined;
|
|
1291
|
+
}, {}, {}>;
|
|
1292
|
+
releasedAt: import("drizzle-orm/pg-core").PgColumn<{
|
|
1293
|
+
name: "released_at";
|
|
1294
|
+
tableName: "vault_releases";
|
|
1295
|
+
dataType: "number";
|
|
1296
|
+
columnType: "PgBigInt53";
|
|
1297
|
+
data: number;
|
|
1298
|
+
driverParam: string | number;
|
|
1299
|
+
notNull: true;
|
|
1300
|
+
hasDefault: false;
|
|
1301
|
+
isPrimaryKey: false;
|
|
1302
|
+
isAutoincrement: false;
|
|
1303
|
+
hasRuntimeDefault: false;
|
|
1304
|
+
enumValues: undefined;
|
|
1305
|
+
baseColumn: never;
|
|
1306
|
+
identity: undefined;
|
|
1307
|
+
generated: undefined;
|
|
1308
|
+
}, {}, {}>;
|
|
1309
|
+
};
|
|
1310
|
+
dialect: "pg";
|
|
1311
|
+
}>;
|
|
1241
1312
|
declare const auditLog: import("drizzle-orm/pg-core").PgTableWithColumns<{
|
|
1242
1313
|
name: "audit_log";
|
|
1243
1314
|
schema: undefined;
|
|
@@ -4125,4 +4196,4 @@ declare const environmentsRelations: import("drizzle-orm").Relations<"environmen
|
|
|
4125
4196
|
environment: import("drizzle-orm").One<"environments", true>;
|
|
4126
4197
|
}>;
|
|
4127
4198
|
//#endregion
|
|
4128
|
-
export { vaultMembers as C, vaultGrants as S, secretReferences as _, credentials as a, secretsRelations as b, enrollments as c, identities as d, mcpApprovals as f, schema_d_exports as g, projects as h, consumedTokens as i, environments as l, oauthClients as m, auditLog as n, deviceAuthorizations as o, mcpConnections as p, cappedAlerts as r, dismissedKeys as s, auditChainHead as t, environmentsRelations as u, secretVersions as v, serviceBindings as x, secrets as y };
|
|
4199
|
+
export { vaultMembers as C, vaultGrants as S, secretReferences as _, credentials as a, secretsRelations as b, enrollments as c, identities as d, mcpApprovals as f, schema_d_exports as g, projects as h, consumedTokens as i, environments as l, oauthClients as m, auditLog as n, deviceAuthorizations as o, mcpConnections as p, cappedAlerts as r, dismissedKeys as s, auditChainHead as t, environmentsRelations as u, secretVersions as v, vaultReleases as w, serviceBindings as x, secrets as y };
|
package/dist/schema-sqlite.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
declare namespace schema_sqlite_d_exports {
|
|
2
|
-
export { auditChainHead, auditLog, cappedAlerts, consumedTokens, credentials, deviceAuthorizations, dismissedKeys, enrollments, environments, environmentsRelations, identities, mcpApprovals, mcpConnections, oauthClients, projects, secretReferences, secretVersions, secrets, secretsRelations, serviceBindings, vaultGrants, vaultMembers };
|
|
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 };
|
|
3
3
|
}
|
|
4
4
|
export declare const projects: import("drizzle-orm/sqlite-core").SQLiteTableWithColumns<{
|
|
5
5
|
name: "projects";
|
|
@@ -1292,6 +1292,80 @@ export declare const vaultGrants: import("drizzle-orm/sqlite-core").SQLiteTableW
|
|
|
1292
1292
|
};
|
|
1293
1293
|
dialect: "sqlite";
|
|
1294
1294
|
}>;
|
|
1295
|
+
/**
|
|
1296
|
+
* Each key the vault released to a principal, and when last: what its bulk
|
|
1297
|
+
* limit counts (R28), the distinct keys released within a window, without
|
|
1298
|
+
* reading the log. The database keeps it from the log itself, in the
|
|
1299
|
+
* transaction of each vault `secret.read` allowed: a trigger on
|
|
1300
|
+
* `audit_log` upserts a row for each key the entry names, as the count
|
|
1301
|
+
* read them from the entries before (an entry that names none, which only
|
|
1302
|
+
* a forged one is, counts as a key of its own, `entry:<seq>`). No login
|
|
1303
|
+
* writes it: the vault and the backup read it. A row a window old counts
|
|
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
|
+
*/
|
|
1307
|
+
export declare const vaultReleases: import("drizzle-orm/sqlite-core").SQLiteTableWithColumns<{
|
|
1308
|
+
name: "vault_releases";
|
|
1309
|
+
schema: undefined;
|
|
1310
|
+
columns: {
|
|
1311
|
+
principal: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
1312
|
+
name: "principal";
|
|
1313
|
+
tableName: "vault_releases";
|
|
1314
|
+
dataType: "string";
|
|
1315
|
+
columnType: "SQLiteText";
|
|
1316
|
+
data: string;
|
|
1317
|
+
driverParam: string;
|
|
1318
|
+
notNull: true;
|
|
1319
|
+
hasDefault: false;
|
|
1320
|
+
isPrimaryKey: false;
|
|
1321
|
+
isAutoincrement: false;
|
|
1322
|
+
hasRuntimeDefault: false;
|
|
1323
|
+
enumValues: [string, ...string[]];
|
|
1324
|
+
baseColumn: never;
|
|
1325
|
+
identity: undefined;
|
|
1326
|
+
generated: undefined;
|
|
1327
|
+
}, {}, {
|
|
1328
|
+
length: undefined;
|
|
1329
|
+
}>;
|
|
1330
|
+
key: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
1331
|
+
name: "key";
|
|
1332
|
+
tableName: "vault_releases";
|
|
1333
|
+
dataType: "string";
|
|
1334
|
+
columnType: "SQLiteText";
|
|
1335
|
+
data: string;
|
|
1336
|
+
driverParam: string;
|
|
1337
|
+
notNull: true;
|
|
1338
|
+
hasDefault: false;
|
|
1339
|
+
isPrimaryKey: false;
|
|
1340
|
+
isAutoincrement: false;
|
|
1341
|
+
hasRuntimeDefault: false;
|
|
1342
|
+
enumValues: [string, ...string[]];
|
|
1343
|
+
baseColumn: never;
|
|
1344
|
+
identity: undefined;
|
|
1345
|
+
generated: undefined;
|
|
1346
|
+
}, {}, {
|
|
1347
|
+
length: undefined;
|
|
1348
|
+
}>;
|
|
1349
|
+
releasedAt: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
1350
|
+
name: "released_at";
|
|
1351
|
+
tableName: "vault_releases";
|
|
1352
|
+
dataType: "number";
|
|
1353
|
+
columnType: "SQLiteInteger";
|
|
1354
|
+
data: number;
|
|
1355
|
+
driverParam: number;
|
|
1356
|
+
notNull: true;
|
|
1357
|
+
hasDefault: false;
|
|
1358
|
+
isPrimaryKey: false;
|
|
1359
|
+
isAutoincrement: false;
|
|
1360
|
+
hasRuntimeDefault: false;
|
|
1361
|
+
enumValues: undefined;
|
|
1362
|
+
baseColumn: never;
|
|
1363
|
+
identity: undefined;
|
|
1364
|
+
generated: undefined;
|
|
1365
|
+
}, {}, {}>;
|
|
1366
|
+
};
|
|
1367
|
+
dialect: "sqlite";
|
|
1368
|
+
}>;
|
|
1295
1369
|
export declare const auditLog: import("drizzle-orm/sqlite-core").SQLiteTableWithColumns<{
|
|
1296
1370
|
name: "audit_log";
|
|
1297
1371
|
schema: undefined;
|
package/dist/schema-sqlite.js
CHANGED
|
@@ -214,6 +214,26 @@ const vaultGrants = sqliteTable("vault_grants", {
|
|
|
214
214
|
foreignColumns: [environments.id]
|
|
215
215
|
}).onDelete("restrict")
|
|
216
216
|
]);
|
|
217
|
+
/**
|
|
218
|
+
* Each key the vault released to a principal, and when last: what its bulk
|
|
219
|
+
* limit counts (R28), the distinct keys released within a window, without
|
|
220
|
+
* reading the log. The database keeps it from the log itself, in the
|
|
221
|
+
* transaction of each vault `secret.read` allowed: a trigger on
|
|
222
|
+
* `audit_log` upserts a row for each key the entry names, as the count
|
|
223
|
+
* read them from the entries before (an entry that names none, which only
|
|
224
|
+
* a forged one is, counts as a key of its own, `entry:<seq>`). No login
|
|
225
|
+
* writes it: the vault and the backup read it. A row a window old counts
|
|
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
|
+
*/
|
|
229
|
+
const vaultReleases = sqliteTable("vault_releases", {
|
|
230
|
+
principal: text().notNull(),
|
|
231
|
+
key: text().notNull(),
|
|
232
|
+
releasedAt: integer("released_at", { mode: "number" }).notNull()
|
|
233
|
+
}, (table) => [primaryKey({
|
|
234
|
+
name: "vault_releases_pkey",
|
|
235
|
+
columns: [table.principal, table.key]
|
|
236
|
+
})]);
|
|
217
237
|
const auditLog = sqliteTable("audit_log", {
|
|
218
238
|
seq: int64("seq").primaryKey(),
|
|
219
239
|
author: text().notNull(),
|
|
@@ -616,4 +636,4 @@ const { environmentsRelations, secretsRelations } = relationsOf(asPostgres({
|
|
|
616
636
|
secretVersions
|
|
617
637
|
}));
|
|
618
638
|
//#endregion
|
|
619
|
-
export { auditChainHead, auditLog, cappedAlerts, consumedTokens, credentials, deviceAuthorizations, dismissedKeys, enrollments, environments, environmentsRelations, identities, mcpApprovals, mcpConnections, oauthClients, projects, secretReferences, secretVersions, secrets, secretsRelations, serviceBindings, vaultGrants, vaultMembers };
|
|
639
|
+
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-version.d.ts
CHANGED
package/dist/schema-version.js
CHANGED
|
@@ -148,6 +148,20 @@ const KNOWN_MIGRATIONS = {
|
|
|
148
148
|
"when": 1792937576477,
|
|
149
149
|
"tag": "0019_backup_role",
|
|
150
150
|
"breakpoints": true
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
"idx": 20,
|
|
154
|
+
"version": "7",
|
|
155
|
+
"when": 1793017780802,
|
|
156
|
+
"tag": "0020_vault_releases",
|
|
157
|
+
"breakpoints": true
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
"idx": 21,
|
|
161
|
+
"version": "7",
|
|
162
|
+
"when": 1793035191911,
|
|
163
|
+
"tag": "0021_prune_vault_releases",
|
|
164
|
+
"breakpoints": true
|
|
151
165
|
}
|
|
152
166
|
].map((entry) => entry.tag),
|
|
153
167
|
sqlite: [
|
|
@@ -269,6 +283,13 @@ const KNOWN_MIGRATIONS = {
|
|
|
269
283
|
"when": 1792835446455,
|
|
270
284
|
"tag": "0016_capped_alerts",
|
|
271
285
|
"breakpoints": true
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
"idx": 17,
|
|
289
|
+
"version": "6",
|
|
290
|
+
"when": 1793017781451,
|
|
291
|
+
"tag": "0017_vault_releases",
|
|
292
|
+
"breakpoints": true
|
|
272
293
|
}
|
|
273
294
|
].map((entry) => entry.tag)
|
|
274
295
|
};
|
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, x as serviceBindings, y as secrets } from "./schema-
|
|
2
|
-
export { auditChainHead, auditLog, cappedAlerts, consumedTokens, credentials, deviceAuthorizations, dismissedKeys, enrollments, environments, environmentsRelations, identities, mcpApprovals, mcpConnections, oauthClients, projects, secretReferences, secretVersions, secrets, secretsRelations, serviceBindings, vaultGrants, vaultMembers };
|
|
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
|
+
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
|
@@ -25,7 +25,8 @@ var schema_exports = /* @__PURE__ */ __exportAll({
|
|
|
25
25
|
secretsRelations: () => secretsRelations,
|
|
26
26
|
serviceBindings: () => serviceBindings,
|
|
27
27
|
vaultGrants: () => vaultGrants,
|
|
28
|
-
vaultMembers: () => vaultMembers
|
|
28
|
+
vaultMembers: () => vaultMembers,
|
|
29
|
+
vaultReleases: () => vaultReleases
|
|
29
30
|
});
|
|
30
31
|
const bytea = customType({ dataType: () => "bytea" });
|
|
31
32
|
const createdAt = () => timestamp("created_at", { withTimezone: true }).notNull().defaultNow();
|
|
@@ -240,6 +241,27 @@ const vaultGrants = pgTable("vault_grants", {
|
|
|
240
241
|
foreignColumns: [environments.id]
|
|
241
242
|
}).onDelete("restrict")
|
|
242
243
|
]);
|
|
244
|
+
/**
|
|
245
|
+
* Each key the vault released to a principal, and when last: what its bulk
|
|
246
|
+
* limit counts (R28), the distinct keys released within a window, without
|
|
247
|
+
* reading the log. The database keeps it from the log itself, in the
|
|
248
|
+
* transaction of each vault `secret.read` allowed: a trigger on
|
|
249
|
+
* `audit_log` upserts a row for each key the entry names, as the count
|
|
250
|
+
* read them from the entries before (an entry that names none, which only
|
|
251
|
+
* a forged one is, counts as a key of its own, `entry:<seq>`). No login
|
|
252
|
+
* writes it: the vault and the backup read it. A row a window old counts
|
|
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.
|
|
256
|
+
*/
|
|
257
|
+
const vaultReleases = pgTable("vault_releases", {
|
|
258
|
+
principal: text().notNull(),
|
|
259
|
+
key: text().notNull(),
|
|
260
|
+
releasedAt: bigint("released_at", { mode: "number" }).notNull()
|
|
261
|
+
}, (table) => [primaryKey({
|
|
262
|
+
name: "vault_releases_pkey",
|
|
263
|
+
columns: [table.principal, table.key]
|
|
264
|
+
})]);
|
|
243
265
|
const auditLog = pgTable("audit_log", {
|
|
244
266
|
seq: bigint({ mode: "bigint" }).primaryKey(),
|
|
245
267
|
author: text().notNull(),
|
|
@@ -698,4 +720,4 @@ const { environmentsRelations, secretsRelations } = relationsOf({
|
|
|
698
720
|
secretVersions
|
|
699
721
|
});
|
|
700
722
|
//#endregion
|
|
701
|
-
export { auditChainHead, auditLog, cappedAlerts, consumedTokens, credentials, deviceAuthorizations, dismissedKeys, enrollments, environments, environmentsRelations, identities, mcpApprovals, mcpConnections, oauthClients, projects, secretReferences, secretVersions, secrets, secretsRelations, serviceBindings, schema_exports as t, vaultGrants, vaultMembers };
|
|
723
|
+
export { auditChainHead, auditLog, cappedAlerts, consumedTokens, credentials, deviceAuthorizations, dismissedKeys, enrollments, environments, environmentsRelations, identities, mcpApprovals, mcpConnections, oauthClients, projects, secretReferences, secretVersions, secrets, secretsRelations, serviceBindings, schema_exports as t, vaultGrants, vaultMembers, vaultReleases };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coffre/db",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.5",
|
|
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.5"
|
|
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,36 @@
|
|
|
1
|
+
-- Releases (W199): each key the vault released to a principal, and when last, so that its bulk limit counts the keys released within a window from a row per key, not from every read the principal ever made. The database keeps it, from each vault `secret.read` allowed, in the append's own transaction; the release before neither reads it nor knows it is kept, and its appends keep it too.
|
|
2
|
+
CREATE TABLE "vault_releases" (
|
|
3
|
+
"principal" text NOT NULL,
|
|
4
|
+
"key" text NOT NULL,
|
|
5
|
+
"released_at" bigint NOT NULL,
|
|
6
|
+
CONSTRAINT "vault_releases_pkey" PRIMARY KEY("principal","key")
|
|
7
|
+
);
|
|
8
|
+
--> statement-breakpoint
|
|
9
|
+
-- Room in each page for the next version of its rows: a read moves its keys' times on in place (a HOT update), so the
|
|
10
|
+
-- table and its key stay a row a key, however often each is read: the page prunes the versions before as it is read.
|
|
11
|
+
ALTER TABLE vault_releases SET (fillfactor = 50);--> statement-breakpoint
|
|
12
|
+
-- Each key the entry names, as the count read them before: the secret, or each item of a read's list at one place, or, for an entry that names none, which only a forged one is, the entry itself. As the owner, so that no login may write the table: the vault's appends reach it only through the log.
|
|
13
|
+
CREATE FUNCTION vault_releases_keep() RETURNS trigger
|
|
14
|
+
LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog, pg_temp AS $$
|
|
15
|
+
BEGIN
|
|
16
|
+
INSERT INTO public.vault_releases AS kept (principal, key, released_at)
|
|
17
|
+
SELECT DISTINCT NEW.actor, coalesce(NEW.secret_id::text, CASE jsonb_typeof(one -> 'secretId') WHEN 'string' THEN one ->> 'secretId' END, 'entry:' || NEW.seq), NEW.occurred_at
|
|
18
|
+
FROM (SELECT) AS entry
|
|
19
|
+
LEFT JOIN LATERAL jsonb_path_query(CASE WHEN NEW.secret_id IS NULL THEN NEW.metadata::jsonb END, 'strict $.secrets ? (@.type() == "array")[*]', '{}', true) AS one ON true
|
|
20
|
+
ON CONFLICT (principal, key) DO UPDATE SET released_at = greatest(kept.released_at, excluded.released_at);
|
|
21
|
+
RETURN NULL;
|
|
22
|
+
END
|
|
23
|
+
$$;--> statement-breakpoint
|
|
24
|
+
-- A trigger runs its function whoever may execute it: no login may call it otherwise.
|
|
25
|
+
REVOKE ALL ON FUNCTION vault_releases_keep() FROM PUBLIC;--> statement-breakpoint
|
|
26
|
+
CREATE TRIGGER audit_log_releases AFTER INSERT ON audit_log FOR EACH ROW
|
|
27
|
+
WHEN (NEW.author = 'vault' AND NEW.action = 'secret.read' AND NEW.decision = 'allow')
|
|
28
|
+
EXECUTE FUNCTION vault_releases_keep();--> statement-breakpoint
|
|
29
|
+
-- Then the releases already in the log, in the same transaction: an append waits from the trigger to the commit, and none is missed.
|
|
30
|
+
INSERT INTO vault_releases (principal, key, released_at)
|
|
31
|
+
SELECT e.actor, coalesce(e.secret_id::text, CASE jsonb_typeof(one -> 'secretId') WHEN 'string' THEN one ->> 'secretId' END, 'entry:' || e.seq), max(e.occurred_at)
|
|
32
|
+
FROM audit_log e
|
|
33
|
+
LEFT JOIN LATERAL jsonb_path_query(CASE WHEN e.secret_id IS NULL THEN e.metadata::jsonb END, 'strict $.secrets ? (@.type() == "array")[*]', '{}', true) AS one ON true
|
|
34
|
+
WHERE e.author = 'vault' AND e.action = 'secret.read' AND e.decision = 'allow'
|
|
35
|
+
GROUP BY 1, 2;--> statement-breakpoint
|
|
36
|
+
GRANT SELECT ON vault_releases TO coffre_vault, coffre_backup;
|
|
@@ -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;
|