@coffre/db 0.1.15 → 0.1.16
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-CNMfHZwF.d.ts → database-Cq_52uUo.d.ts} +1 -1
- package/dist/dialect.d.ts +1 -1
- package/dist/hyperdrive.d.ts +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +8 -4
- package/dist/log.d.ts +1 -1
- package/dist/migrations/postgres/0002_service_bindings.sql +28 -0
- package/dist/migrations/postgres/0003_exchanges.sql +10 -0
- package/dist/migrations/postgres/0004_live_indexes.sql +3 -0
- package/dist/migrations/postgres/meta/0002_snapshot.json +1870 -0
- package/dist/migrations/postgres/meta/0003_snapshot.json +1917 -0
- package/dist/migrations/postgres/meta/0004_snapshot.json +2006 -0
- package/dist/migrations/postgres/meta/_journal.json +21 -0
- package/dist/migrations/sqlite/0002_service_bindings.sql +23 -0
- package/dist/migrations/sqlite/0003_exchanges.sql +7 -0
- package/dist/migrations/sqlite/0004_live_indexes.sql +3 -0
- package/dist/migrations/sqlite/meta/0002_snapshot.json +1736 -0
- package/dist/migrations/sqlite/meta/0003_snapshot.json +1781 -0
- package/dist/migrations/sqlite/meta/0004_snapshot.json +1811 -0
- package/dist/migrations/sqlite/meta/_journal.json +21 -0
- package/dist/portable.d.ts +2 -2
- package/dist/{schema-AmvM6w0D.d.ts → schema-Wt-p8AOh.d.ts} +305 -2
- package/dist/schema-sqlite.d.ts +305 -1
- package/dist/schema-sqlite.js +38 -3
- package/dist/schema-version.d.ts +1 -1
- package/dist/schema-version.js +74 -26
- package/dist/schema.d.ts +2 -2
- package/dist/schema.js +53 -3
- package/package.json +2 -2
- package/src/database.ts +15 -5
- package/src/migrations/postgres/0002_service_bindings.sql +28 -0
- package/src/migrations/postgres/0003_exchanges.sql +10 -0
- package/src/migrations/postgres/0004_live_indexes.sql +3 -0
- package/src/migrations/postgres/meta/0002_snapshot.json +1870 -0
- package/src/migrations/postgres/meta/0003_snapshot.json +1917 -0
- package/src/migrations/postgres/meta/0004_snapshot.json +2006 -0
- package/src/migrations/postgres/meta/_journal.json +21 -0
- package/src/migrations/sqlite/0002_service_bindings.sql +23 -0
- package/src/migrations/sqlite/0003_exchanges.sql +7 -0
- package/src/migrations/sqlite/0004_live_indexes.sql +3 -0
- package/src/migrations/sqlite/meta/0002_snapshot.json +1736 -0
- package/src/migrations/sqlite/meta/0003_snapshot.json +1781 -0
- package/src/migrations/sqlite/meta/0004_snapshot.json +1811 -0
- package/src/migrations/sqlite/meta/_journal.json +21 -0
- package/src/schema.sqlite.ts +61 -0
- package/src/schema.ts +74 -0
package/dist/schema-sqlite.js
CHANGED
|
@@ -224,7 +224,9 @@ const auditLog = sqliteTable("audit_log", {
|
|
|
224
224
|
index("audit_log_operation_idx").on(table.operationId, table.seq),
|
|
225
225
|
index("audit_log_action_idx").on(table.author, table.action, table.seq),
|
|
226
226
|
index("audit_log_releases_idx").on(table.author, table.actor, table.action, table.decision, table.occurredAt),
|
|
227
|
-
index("audit_log_subject_idx").on(table.author, table.subjectPrincipal, table.seq)
|
|
227
|
+
index("audit_log_subject_idx").on(table.author, table.subjectPrincipal, table.seq),
|
|
228
|
+
index("audit_log_unbind_idx").on(sql`json_extract(${table.metadata}, '$.bindingId')`).where(sql`${table.author} = 'app' AND ${table.action} = 'token.unbind' AND ${table.decision} = 'allow'`),
|
|
229
|
+
index("audit_log_exchange_idx").on(sql`json_extract(${table.metadata}, '$.credentialId')`).where(sql`${table.author} = 'app' AND ${table.action} = 'token.exchange' AND ${table.decision} = 'allow'`)
|
|
228
230
|
]);
|
|
229
231
|
const auditChainHead = sqliteTable("audit_chain_head", {
|
|
230
232
|
onlyRow: flag("only_row").primaryKey().default(true),
|
|
@@ -263,6 +265,37 @@ const identities = sqliteTable("identities", {
|
|
|
263
265
|
uniqueIndex("identities_active_subject").on(table.provider, table.issuerHash, table.activeSubject),
|
|
264
266
|
index("identities_principal_idx").on(table.principal, table.generation)
|
|
265
267
|
]);
|
|
268
|
+
const serviceBindings = sqliteTable("service_bindings", {
|
|
269
|
+
id: text().primaryKey(),
|
|
270
|
+
authMac: bytes("auth_mac").notNull(),
|
|
271
|
+
principal: text().notNull(),
|
|
272
|
+
generation: integer().notNull(),
|
|
273
|
+
profile: text().notNull(),
|
|
274
|
+
issuer: text().notNull(),
|
|
275
|
+
jwksUri: text("jwks_uri").notNull(),
|
|
276
|
+
claims: text().notNull(),
|
|
277
|
+
label: text(),
|
|
278
|
+
createdAt: createdAt(),
|
|
279
|
+
createdBy: text("created_by").notNull(),
|
|
280
|
+
lastUsedAt: time("last_used_at"),
|
|
281
|
+
revokedAt: time("revoked_at"),
|
|
282
|
+
revokedBy: text("revoked_by")
|
|
283
|
+
}, (table) => [
|
|
284
|
+
check("service_bindings_principal_check", sql`${table.principal} LIKE 'token:%'`),
|
|
285
|
+
check("service_bindings_auth_mac_check", sql`octet_length(${table.authMac}) = 32`),
|
|
286
|
+
check("service_bindings_claims_check", sql`json_valid(${table.claims})`),
|
|
287
|
+
foreignKey({
|
|
288
|
+
name: "service_bindings_principal_fkey",
|
|
289
|
+
columns: [table.principal],
|
|
290
|
+
foreignColumns: [vaultMembers.principal]
|
|
291
|
+
}).onDelete("restrict"),
|
|
292
|
+
index("service_bindings_principal_idx").on(table.principal, table.issuer),
|
|
293
|
+
index("service_bindings_live_idx").on(table.principal, table.issuer, table.generation, table.createdAt, table.id).where(sql`${table.revokedAt} IS NULL`)
|
|
294
|
+
]);
|
|
295
|
+
const consumedTokens = sqliteTable("consumed_tokens", {
|
|
296
|
+
hash: bytes("hash").primaryKey(),
|
|
297
|
+
consumedAt: time("consumed_at").notNull().default(now)
|
|
298
|
+
}, (table) => [check("consumed_tokens_hash_check", sql`octet_length(${table.hash}) = 32`)]);
|
|
266
299
|
const credentials = sqliteTable("credentials", {
|
|
267
300
|
id: text().primaryKey(),
|
|
268
301
|
kind: text().notNull(),
|
|
@@ -305,7 +338,9 @@ const credentials = sqliteTable("credentials", {
|
|
|
305
338
|
identities.generation
|
|
306
339
|
]
|
|
307
340
|
}).onDelete("restrict"),
|
|
308
|
-
index("credentials_principal_idx").on(table.principal, table.generation)
|
|
341
|
+
index("credentials_principal_idx").on(table.principal, table.generation),
|
|
342
|
+
index("credentials_live_idx").on(table.principal, table.expiresAt).where(sql`${table.revokedAt} IS NULL`),
|
|
343
|
+
index("credentials_issued_by_idx").on(table.principal, table.createdBy, table.createdAt)
|
|
309
344
|
]);
|
|
310
345
|
const deviceAuthorizations = sqliteTable("device_authorizations", {
|
|
311
346
|
id: text().primaryKey(),
|
|
@@ -344,4 +379,4 @@ const { environmentsRelations, secretsRelations } = relationsOf(asPostgres({
|
|
|
344
379
|
secretVersions
|
|
345
380
|
}));
|
|
346
381
|
//#endregion
|
|
347
|
-
export { auditChainHead, auditLog, credentials, deviceAuthorizations, environments, environmentsRelations, identities, projects, secretVersions, secrets, secretsRelations, vaultGrants, vaultMembers };
|
|
382
|
+
export { auditChainHead, auditLog, consumedTokens, credentials, deviceAuthorizations, environments, environmentsRelations, identities, projects, secretVersions, secrets, secretsRelations, serviceBindings, vaultGrants, vaultMembers };
|
package/dist/schema-version.d.ts
CHANGED
package/dist/schema-version.js
CHANGED
|
@@ -1,33 +1,81 @@
|
|
|
1
1
|
import { engineOf } from "./dialect.js";
|
|
2
2
|
//#region src/migrations/postgres/meta/_journal.json
|
|
3
|
-
var entries$1 = [
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
3
|
+
var entries$1 = [
|
|
4
|
+
{
|
|
5
|
+
"idx": 0,
|
|
6
|
+
"version": "7",
|
|
7
|
+
"when": 1790930587507,
|
|
8
|
+
"tag": "0000_baseline",
|
|
9
|
+
"breakpoints": true
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"idx": 1,
|
|
13
|
+
"version": "7",
|
|
14
|
+
"when": 1791062292678,
|
|
15
|
+
"tag": "0001_remove_syncs",
|
|
16
|
+
"breakpoints": true
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"idx": 2,
|
|
20
|
+
"version": "7",
|
|
21
|
+
"when": 1791069304538,
|
|
22
|
+
"tag": "0002_service_bindings",
|
|
23
|
+
"breakpoints": true
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"idx": 3,
|
|
27
|
+
"version": "7",
|
|
28
|
+
"when": 1791072336889,
|
|
29
|
+
"tag": "0003_exchanges",
|
|
30
|
+
"breakpoints": true
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"idx": 4,
|
|
34
|
+
"version": "7",
|
|
35
|
+
"when": 1791084354462,
|
|
36
|
+
"tag": "0004_live_indexes",
|
|
37
|
+
"breakpoints": true
|
|
38
|
+
}
|
|
39
|
+
];
|
|
16
40
|
//#endregion
|
|
17
41
|
//#region src/migrations/sqlite/meta/_journal.json
|
|
18
|
-
var entries = [
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
42
|
+
var entries = [
|
|
43
|
+
{
|
|
44
|
+
"idx": 0,
|
|
45
|
+
"version": "6",
|
|
46
|
+
"when": 1790874462505,
|
|
47
|
+
"tag": "0000_baseline",
|
|
48
|
+
"breakpoints": true
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"idx": 1,
|
|
52
|
+
"version": "6",
|
|
53
|
+
"when": 1791062293531,
|
|
54
|
+
"tag": "0001_remove_syncs",
|
|
55
|
+
"breakpoints": true
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"idx": 2,
|
|
59
|
+
"version": "6",
|
|
60
|
+
"when": 1791069305749,
|
|
61
|
+
"tag": "0002_service_bindings",
|
|
62
|
+
"breakpoints": true
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"idx": 3,
|
|
66
|
+
"version": "6",
|
|
67
|
+
"when": 1791072337548,
|
|
68
|
+
"tag": "0003_exchanges",
|
|
69
|
+
"breakpoints": true
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"idx": 4,
|
|
73
|
+
"version": "6",
|
|
74
|
+
"when": 1791084355588,
|
|
75
|
+
"tag": "0004_live_indexes",
|
|
76
|
+
"breakpoints": true
|
|
77
|
+
}
|
|
78
|
+
];
|
|
31
79
|
//#endregion
|
|
32
80
|
//#region src/schema-version.ts
|
|
33
81
|
/**
|
package/dist/schema.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
export { auditChainHead, auditLog, credentials, deviceAuthorizations, environments, environmentsRelations, identities, projects, secretVersions, secrets, secretsRelations, vaultGrants, vaultMembers };
|
|
1
|
+
import { a as deviceAuthorizations, c as identities, d as secretVersions, f as secrets, g as vaultMembers, h as vaultGrants, i as credentials, l as projects, m as serviceBindings, n as auditLog, o as environments, p as secretsRelations, r as consumedTokens, s as environmentsRelations, t as auditChainHead } from "./schema-Wt-p8AOh.js";
|
|
2
|
+
export { auditChainHead, auditLog, consumedTokens, credentials, deviceAuthorizations, environments, environmentsRelations, identities, projects, secretVersions, secrets, secretsRelations, serviceBindings, vaultGrants, vaultMembers };
|
package/dist/schema.js
CHANGED
|
@@ -7,6 +7,7 @@ import { bigint, boolean, check, customType, foreignKey, index, integer, pgTable
|
|
|
7
7
|
var schema_exports = /* @__PURE__ */ __exportAll({
|
|
8
8
|
auditChainHead: () => auditChainHead,
|
|
9
9
|
auditLog: () => auditLog,
|
|
10
|
+
consumedTokens: () => consumedTokens,
|
|
10
11
|
credentials: () => credentials,
|
|
11
12
|
deviceAuthorizations: () => deviceAuthorizations,
|
|
12
13
|
environments: () => environments,
|
|
@@ -16,6 +17,7 @@ var schema_exports = /* @__PURE__ */ __exportAll({
|
|
|
16
17
|
secretVersions: () => secretVersions,
|
|
17
18
|
secrets: () => secrets,
|
|
18
19
|
secretsRelations: () => secretsRelations,
|
|
20
|
+
serviceBindings: () => serviceBindings,
|
|
19
21
|
vaultGrants: () => vaultGrants,
|
|
20
22
|
vaultMembers: () => vaultMembers
|
|
21
23
|
});
|
|
@@ -228,7 +230,9 @@ const auditLog = pgTable("audit_log", {
|
|
|
228
230
|
index("audit_log_operation_idx").on(table.operationId, table.seq),
|
|
229
231
|
index("audit_log_action_idx").on(table.author, table.action, table.seq),
|
|
230
232
|
index("audit_log_releases_idx").on(table.author, table.actor, table.action, table.decision, table.occurredAt),
|
|
231
|
-
index("audit_log_subject_idx").on(table.author, table.subjectPrincipal, table.seq)
|
|
233
|
+
index("audit_log_subject_idx").on(table.author, table.subjectPrincipal, table.seq),
|
|
234
|
+
index("audit_log_unbind_idx").on(sql`((${table.metadata})::jsonb ->> 'bindingId')`).where(sql`${table.author} = 'app' AND ${table.action} = 'token.unbind' AND ${table.decision} = 'allow'`),
|
|
235
|
+
index("audit_log_exchange_idx").on(sql`((${table.metadata})::jsonb ->> 'credentialId')`).where(sql`${table.author} = 'app' AND ${table.action} = 'token.exchange' AND ${table.decision} = 'allow'`)
|
|
232
236
|
]);
|
|
233
237
|
const auditChainHead = pgTable("audit_chain_head", {
|
|
234
238
|
onlyRow: boolean("only_row").primaryKey().default(true),
|
|
@@ -276,6 +280,50 @@ const identities = pgTable("identities", {
|
|
|
276
280
|
index("identities_principal_idx").on(table.principal, table.generation)
|
|
277
281
|
]);
|
|
278
282
|
/**
|
|
283
|
+
* A trust binding: which CI runs may sign in as a service, by the ID token
|
|
284
|
+
* their platform signs (`@coffre/core/identity`'s workloads.ts). Immutable
|
|
285
|
+
* but for its label, last use and revocation: a change is a new binding, and
|
|
286
|
+
* the old one's `token.unbind` entry is its tombstone, which outlives any
|
|
287
|
+
* row put back. The MAC covers what decides (auth-rows.ts in the server).
|
|
288
|
+
*/
|
|
289
|
+
const serviceBindings = pgTable("service_bindings", {
|
|
290
|
+
id: uuid().primaryKey(),
|
|
291
|
+
authMac: bytea("auth_mac").notNull(),
|
|
292
|
+
principal: text().notNull(),
|
|
293
|
+
generation: integer().notNull(),
|
|
294
|
+
profile: text().notNull(),
|
|
295
|
+
issuer: text().notNull(),
|
|
296
|
+
jwksUri: text("jwks_uri").notNull(),
|
|
297
|
+
claims: text().notNull(),
|
|
298
|
+
label: text(),
|
|
299
|
+
createdAt: createdAt(),
|
|
300
|
+
createdBy: text("created_by").notNull(),
|
|
301
|
+
lastUsedAt: timestamp("last_used_at", { withTimezone: true }),
|
|
302
|
+
revokedAt: timestamp("revoked_at", { withTimezone: true }),
|
|
303
|
+
revokedBy: text("revoked_by")
|
|
304
|
+
}, (table) => [
|
|
305
|
+
check("service_bindings_principal_check", sql`${table.principal} LIKE 'token:%'`),
|
|
306
|
+
check("service_bindings_auth_mac_check", sql`octet_length(${table.authMac}) = 32`),
|
|
307
|
+
check("service_bindings_claims_check", sql`${table.claims}::jsonb IS NOT NULL`),
|
|
308
|
+
foreignKey({
|
|
309
|
+
name: "service_bindings_principal_fkey",
|
|
310
|
+
columns: [table.principal],
|
|
311
|
+
foreignColumns: [vaultMembers.principal]
|
|
312
|
+
}).onDelete("restrict"),
|
|
313
|
+
index("service_bindings_principal_idx").on(table.principal, table.issuer),
|
|
314
|
+
index("service_bindings_live_idx").on(table.principal, table.issuer, table.generation, table.createdAt, table.id).where(sql`${table.revokedAt} IS NULL`)
|
|
315
|
+
]);
|
|
316
|
+
/**
|
|
317
|
+
* ID tokens exchanged for a credential, each once: the SHA-256 of what its
|
|
318
|
+
* signature covers, `header.payload` as received, so that no other
|
|
319
|
+
* spelling of the same token counts as another. Kept whatever becomes of
|
|
320
|
+
* the credential it bought; the primary key decides a race.
|
|
321
|
+
*/
|
|
322
|
+
const consumedTokens = pgTable("consumed_tokens", {
|
|
323
|
+
hash: bytea("hash").primaryKey(),
|
|
324
|
+
consumedAt: timestamp("consumed_at", { withTimezone: true }).notNull().defaultNow()
|
|
325
|
+
}, (table) => [check("consumed_tokens_hash_check", sql`octet_length(${table.hash}) = 32`)]);
|
|
326
|
+
/**
|
|
279
327
|
* Bearer credentials coffre issues itself: browser sessions, CLI sessions and
|
|
280
328
|
* service tokens.
|
|
281
329
|
*
|
|
@@ -325,7 +373,9 @@ const credentials = pgTable("credentials", {
|
|
|
325
373
|
identities.generation
|
|
326
374
|
]
|
|
327
375
|
}).onDelete("restrict"),
|
|
328
|
-
index("credentials_principal_idx").on(table.principal, table.generation)
|
|
376
|
+
index("credentials_principal_idx").on(table.principal, table.generation),
|
|
377
|
+
index("credentials_live_idx").on(table.principal, table.expiresAt).where(sql`${table.revokedAt} IS NULL`),
|
|
378
|
+
index("credentials_issued_by_idx").on(table.principal, table.createdBy, table.createdAt)
|
|
329
379
|
]);
|
|
330
380
|
/**
|
|
331
381
|
* A CLI asking to be signed in from a browser (RFC 8628, device flow).
|
|
@@ -371,4 +421,4 @@ const { environmentsRelations, secretsRelations } = relationsOf({
|
|
|
371
421
|
secretVersions
|
|
372
422
|
});
|
|
373
423
|
//#endregion
|
|
374
|
-
export { auditChainHead, auditLog, credentials, deviceAuthorizations, environments, environmentsRelations, identities, projects, secretVersions, secrets, secretsRelations, schema_exports as t, vaultGrants, vaultMembers };
|
|
424
|
+
export { auditChainHead, auditLog, consumedTokens, credentials, deviceAuthorizations, environments, environmentsRelations, identities, projects, secretVersions, secrets, secretsRelations, serviceBindings, schema_exports as t, vaultGrants, vaultMembers };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coffre/db",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.16",
|
|
4
4
|
"description": "coffre's database: the schema, its migrations and the connections, on Postgres or SQLite.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
"drizzle-orm": "0.45.2",
|
|
75
75
|
"pg": "8.16.3",
|
|
76
76
|
"pg-connection-string": "2.14.0",
|
|
77
|
-
"@coffre/core": "0.1.
|
|
77
|
+
"@coffre/core": "0.1.16"
|
|
78
78
|
},
|
|
79
79
|
"devDependencies": {
|
|
80
80
|
"@types/node": "26.1.1",
|
package/src/database.ts
CHANGED
|
@@ -49,7 +49,7 @@ const inside = new AsyncLocalStorage<{ pool: PoolLike; open: boolean }>();
|
|
|
49
49
|
/**
|
|
50
50
|
* `pool`, refusing a query or a transaction asked from inside a transaction
|
|
51
51
|
* it is already running. Work from any other flow, a concurrent request's,
|
|
52
|
-
* or what the transaction left running
|
|
52
|
+
* or what the transaction left running once its work was done, goes on as before.
|
|
53
53
|
*/
|
|
54
54
|
function guarded(pool: PoolLike): PoolLike {
|
|
55
55
|
return new Proxy(pool, {
|
|
@@ -76,17 +76,27 @@ function brokenRule(error: unknown): QueryOutsideTransaction | undefined {
|
|
|
76
76
|
|
|
77
77
|
export function createDatabase(pool: PoolLike): Database {
|
|
78
78
|
const db = drizzle(guarded(pool) as pg.Pool, { schema });
|
|
79
|
-
// Each transaction's work runs knowing which one it is in, until
|
|
79
|
+
// Each transaction's work runs knowing which one it is in, until that work is done. Not until
|
|
80
|
+
// the COMMIT comes back: the transaction waits on nothing then, so what the work left running
|
|
81
|
+
// may ask for a query, which waits for the transaction to end rather than for itself.
|
|
80
82
|
const run = db.transaction.bind(db);
|
|
81
83
|
db.transaction = (async (work, config) => {
|
|
82
84
|
const open = { pool, open: true };
|
|
83
85
|
try {
|
|
84
|
-
return await run(
|
|
86
|
+
return await run(
|
|
87
|
+
(tx) =>
|
|
88
|
+
inside.run(open, async () => {
|
|
89
|
+
try {
|
|
90
|
+
return await work(tx);
|
|
91
|
+
} finally {
|
|
92
|
+
open.open = false;
|
|
93
|
+
}
|
|
94
|
+
}),
|
|
95
|
+
config,
|
|
96
|
+
);
|
|
85
97
|
} catch (error) {
|
|
86
98
|
// Drizzle wraps what a query throws: the rule broken is what to say.
|
|
87
99
|
throw brokenRule(error) ?? error;
|
|
88
|
-
} finally {
|
|
89
|
-
open.open = false;
|
|
90
100
|
}
|
|
91
101
|
}) as Database['transaction'];
|
|
92
102
|
return trackCommits(db);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
CREATE TABLE "service_bindings" (
|
|
2
|
+
"id" uuid PRIMARY KEY NOT NULL,
|
|
3
|
+
"auth_mac" "bytea" NOT NULL,
|
|
4
|
+
"principal" text NOT NULL,
|
|
5
|
+
"generation" integer NOT NULL,
|
|
6
|
+
"profile" text NOT NULL,
|
|
7
|
+
"issuer" text NOT NULL,
|
|
8
|
+
"jwks_uri" text NOT NULL,
|
|
9
|
+
"claims" text NOT NULL,
|
|
10
|
+
"label" text,
|
|
11
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
12
|
+
"created_by" text NOT NULL,
|
|
13
|
+
"last_used_at" timestamp with time zone,
|
|
14
|
+
"revoked_at" timestamp with time zone,
|
|
15
|
+
"revoked_by" text,
|
|
16
|
+
CONSTRAINT "service_bindings_principal_check" CHECK ("service_bindings"."principal" LIKE 'token:%'),
|
|
17
|
+
CONSTRAINT "service_bindings_auth_mac_check" CHECK (octet_length("service_bindings"."auth_mac") = 32),
|
|
18
|
+
CONSTRAINT "service_bindings_claims_check" CHECK ("service_bindings"."claims"::jsonb IS NOT NULL)
|
|
19
|
+
);
|
|
20
|
+
--> statement-breakpoint
|
|
21
|
+
ALTER TABLE "service_bindings" ADD CONSTRAINT "service_bindings_principal_fkey" FOREIGN KEY ("principal") REFERENCES "public"."vault_members"("principal") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint
|
|
22
|
+
CREATE INDEX "service_bindings_principal_idx" ON "service_bindings" USING btree ("principal","issuer");--> statement-breakpoint
|
|
23
|
+
CREATE INDEX "audit_log_unbind_idx" ON "audit_log" USING btree ((("metadata")::jsonb ->> 'bindingId')) WHERE "audit_log"."author" = 'app' AND "audit_log"."action" = 'token.unbind' AND "audit_log"."decision" = 'allow';--> statement-breakpoint
|
|
24
|
+
-- The app reads and adds bindings, and changes only what the MAC allows to
|
|
25
|
+
-- change in place: its label, its last use, and its revocation (with the
|
|
26
|
+
-- MAC that covers it). A binding's policy is never updated, nor deleted.
|
|
27
|
+
GRANT SELECT, INSERT ON service_bindings TO coffre_app;--> statement-breakpoint
|
|
28
|
+
GRANT UPDATE (label, last_used_at, revoked_at, revoked_by, auth_mac) ON service_bindings TO coffre_app;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
CREATE TABLE "consumed_tokens" (
|
|
2
|
+
"hash" "bytea" PRIMARY KEY NOT NULL,
|
|
3
|
+
"consumed_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
4
|
+
CONSTRAINT "consumed_tokens_hash_check" CHECK (octet_length("consumed_tokens"."hash") = 32)
|
|
5
|
+
);
|
|
6
|
+
--> statement-breakpoint
|
|
7
|
+
CREATE INDEX "audit_log_exchange_idx" ON "audit_log" USING btree ((("metadata")::jsonb ->> 'credentialId')) WHERE "audit_log"."author" = 'app' AND "audit_log"."action" = 'token.exchange' AND "audit_log"."decision" = 'allow';--> statement-breakpoint
|
|
8
|
+
-- The app records each token it exchanges, and reads whether one was; it
|
|
9
|
+
-- never changes or deletes a record, so a token stays spent.
|
|
10
|
+
GRANT SELECT, INSERT ON consumed_tokens TO coffre_app;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
CREATE INDEX "credentials_live_idx" ON "credentials" USING btree ("principal","expires_at") WHERE "credentials"."revoked_at" IS NULL;--> statement-breakpoint
|
|
2
|
+
CREATE INDEX "credentials_issued_by_idx" ON "credentials" USING btree ("principal","created_by","created_at");--> statement-breakpoint
|
|
3
|
+
CREATE INDEX "service_bindings_live_idx" ON "service_bindings" USING btree ("principal","issuer","generation","created_at","id") WHERE "service_bindings"."revoked_at" IS NULL;
|