@coffre/db 0.1.14 → 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.
Files changed (47) hide show
  1. package/dist/connect.d.ts +1 -1
  2. package/dist/{database-CNMfHZwF.d.ts → database-Cq_52uUo.d.ts} +1 -1
  3. package/dist/dialect.d.ts +1 -1
  4. package/dist/hyperdrive.d.ts +1 -1
  5. package/dist/index.d.ts +2 -2
  6. package/dist/index.js +8 -4
  7. package/dist/log.d.ts +1 -1
  8. package/dist/migrations/postgres/0002_service_bindings.sql +28 -0
  9. package/dist/migrations/postgres/0003_exchanges.sql +10 -0
  10. package/dist/migrations/postgres/0004_live_indexes.sql +3 -0
  11. package/dist/migrations/postgres/meta/0002_snapshot.json +1870 -0
  12. package/dist/migrations/postgres/meta/0003_snapshot.json +1917 -0
  13. package/dist/migrations/postgres/meta/0004_snapshot.json +2006 -0
  14. package/dist/migrations/postgres/meta/_journal.json +21 -0
  15. package/dist/migrations/sqlite/0002_service_bindings.sql +23 -0
  16. package/dist/migrations/sqlite/0003_exchanges.sql +7 -0
  17. package/dist/migrations/sqlite/0004_live_indexes.sql +3 -0
  18. package/dist/migrations/sqlite/meta/0002_snapshot.json +1736 -0
  19. package/dist/migrations/sqlite/meta/0003_snapshot.json +1781 -0
  20. package/dist/migrations/sqlite/meta/0004_snapshot.json +1811 -0
  21. package/dist/migrations/sqlite/meta/_journal.json +21 -0
  22. package/dist/portable.d.ts +2 -2
  23. package/dist/{schema-AmvM6w0D.d.ts → schema-Wt-p8AOh.d.ts} +305 -2
  24. package/dist/schema-sqlite.d.ts +305 -1
  25. package/dist/schema-sqlite.js +38 -3
  26. package/dist/schema-version.d.ts +1 -1
  27. package/dist/schema-version.js +74 -26
  28. package/dist/schema.d.ts +2 -2
  29. package/dist/schema.js +53 -3
  30. package/package.json +2 -2
  31. package/src/database.ts +15 -5
  32. package/src/migrations/postgres/0002_service_bindings.sql +28 -0
  33. package/src/migrations/postgres/0003_exchanges.sql +10 -0
  34. package/src/migrations/postgres/0004_live_indexes.sql +3 -0
  35. package/src/migrations/postgres/meta/0002_snapshot.json +1870 -0
  36. package/src/migrations/postgres/meta/0003_snapshot.json +1917 -0
  37. package/src/migrations/postgres/meta/0004_snapshot.json +2006 -0
  38. package/src/migrations/postgres/meta/_journal.json +21 -0
  39. package/src/migrations/sqlite/0002_service_bindings.sql +23 -0
  40. package/src/migrations/sqlite/0003_exchanges.sql +7 -0
  41. package/src/migrations/sqlite/0004_live_indexes.sql +3 -0
  42. package/src/migrations/sqlite/meta/0002_snapshot.json +1736 -0
  43. package/src/migrations/sqlite/meta/0003_snapshot.json +1781 -0
  44. package/src/migrations/sqlite/meta/0004_snapshot.json +1811 -0
  45. package/src/migrations/sqlite/meta/_journal.json +21 -0
  46. package/src/schema.sqlite.ts +61 -0
  47. package/src/schema.ts +74 -0
@@ -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 };
@@ -1,4 +1,4 @@
1
- import { i as Queryable } from "./database-CNMfHZwF.js";
1
+ import { i as Queryable } from "./database-Cq_52uUo.js";
2
2
  import { Engine } from "./dialect.js";
3
3
  //#region src/schema-version.d.ts
4
4
  /**
@@ -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
- "idx": 0,
5
- "version": "7",
6
- "when": 1790930587507,
7
- "tag": "0000_baseline",
8
- "breakpoints": true
9
- }, {
10
- "idx": 1,
11
- "version": "7",
12
- "when": 1791062292678,
13
- "tag": "0001_remove_syncs",
14
- "breakpoints": true
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
- "idx": 0,
20
- "version": "6",
21
- "when": 1790874462505,
22
- "tag": "0000_baseline",
23
- "breakpoints": true
24
- }, {
25
- "idx": 1,
26
- "version": "6",
27
- "when": 1791062293531,
28
- "tag": "0001_remove_syncs",
29
- "breakpoints": true
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 environments, c as projects, d as secrets, f as secretsRelations, i as deviceAuthorizations, m as vaultMembers, n as auditLog, o as environmentsRelations, p as vaultGrants, r as credentials, s as identities, t as auditChainHead, u as secretVersions } from "./schema-AmvM6w0D.js";
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.14",
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.14"
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 after it ended, goes on as before.
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 the transaction ends.
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((tx) => inside.run(open, () => work(tx)), config);
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;