@coffre/db 0.3.0 → 0.4.1

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 (76) hide show
  1. package/dist/connect.d.ts +1 -1
  2. package/dist/{database-Cq_52uUo.d.ts → database-edkMkG02.d.ts} +1 -1
  3. package/dist/dialect.d.ts +10 -2
  4. package/dist/dialect.js +34 -1
  5. package/dist/grants.d.ts +32 -0
  6. package/dist/grants.js +27 -0
  7. package/dist/hyperdrive.d.ts +1 -1
  8. package/dist/index.d.ts +2 -2
  9. package/dist/log.d.ts +1 -1
  10. package/dist/migrate.js +6 -1
  11. package/dist/migrations/postgres/0000_baseline.sql +174 -46
  12. package/dist/migrations/postgres/0001_mcp_approvals.sql +24 -0
  13. package/dist/migrations/postgres/meta/0000_snapshot.json +963 -185
  14. package/dist/migrations/postgres/meta/0001_snapshot.json +1171 -30
  15. package/dist/migrations/postgres/meta/_journal.json +3 -24
  16. package/dist/migrations/sqlite/0000_baseline.sql +142 -35
  17. package/dist/migrations/sqlite/0001_mcp_approvals.sql +19 -0
  18. package/dist/migrations/sqlite/meta/0000_snapshot.json +878 -235
  19. package/dist/migrations/sqlite/meta/0001_snapshot.json +1062 -57
  20. package/dist/migrations/sqlite/meta/_journal.json +3 -24
  21. package/dist/portable.d.ts +2 -2
  22. package/dist/{schema-Wt-p8AOh.d.ts → schema-BqCB3XFm.d.ts} +1153 -83
  23. package/dist/schema-sqlite.d.ts +1171 -60
  24. package/dist/schema-sqlite.js +173 -11
  25. package/dist/schema-version.d.ts +9 -13
  26. package/dist/schema-version.js +38 -90
  27. package/dist/schema.d.ts +2 -2
  28. package/dist/schema.js +208 -15
  29. package/package.json +7 -2
  30. package/src/baseline/postgres.sql +46 -10
  31. package/src/baseline/sqlite.sql +15 -0
  32. package/src/baseline.ts +7 -3
  33. package/src/dialect.ts +25 -1
  34. package/src/grants.ts +51 -0
  35. package/src/migrate.ts +5 -0
  36. package/src/migrations/postgres/0000_baseline.sql +174 -46
  37. package/src/migrations/postgres/0001_mcp_approvals.sql +24 -0
  38. package/src/migrations/postgres/meta/0000_snapshot.json +963 -185
  39. package/src/migrations/postgres/meta/0001_snapshot.json +1171 -30
  40. package/src/migrations/postgres/meta/_journal.json +3 -24
  41. package/src/migrations/sqlite/0000_baseline.sql +142 -35
  42. package/src/migrations/sqlite/0001_mcp_approvals.sql +19 -0
  43. package/src/migrations/sqlite/meta/0000_snapshot.json +878 -235
  44. package/src/migrations/sqlite/meta/0001_snapshot.json +1062 -57
  45. package/src/migrations/sqlite/meta/_journal.json +3 -24
  46. package/src/schema-version.ts +26 -18
  47. package/src/schema.sqlite.ts +223 -8
  48. package/src/schema.ts +263 -11
  49. package/dist/migrations/postgres/0001_remove_syncs.sql +0 -13
  50. package/dist/migrations/postgres/0002_service_bindings.sql +0 -28
  51. package/dist/migrations/postgres/0003_exchanges.sql +0 -10
  52. package/dist/migrations/postgres/0004_live_indexes.sql +0 -3
  53. package/dist/migrations/postgres/meta/0002_snapshot.json +0 -1870
  54. package/dist/migrations/postgres/meta/0003_snapshot.json +0 -1917
  55. package/dist/migrations/postgres/meta/0004_snapshot.json +0 -2006
  56. package/dist/migrations/sqlite/0001_remove_syncs.sql +0 -13
  57. package/dist/migrations/sqlite/0002_service_bindings.sql +0 -23
  58. package/dist/migrations/sqlite/0003_exchanges.sql +0 -7
  59. package/dist/migrations/sqlite/0004_live_indexes.sql +0 -3
  60. package/dist/migrations/sqlite/meta/0002_snapshot.json +0 -1736
  61. package/dist/migrations/sqlite/meta/0003_snapshot.json +0 -1781
  62. package/dist/migrations/sqlite/meta/0004_snapshot.json +0 -1811
  63. package/src/migrations/postgres/0001_remove_syncs.sql +0 -13
  64. package/src/migrations/postgres/0002_service_bindings.sql +0 -28
  65. package/src/migrations/postgres/0003_exchanges.sql +0 -10
  66. package/src/migrations/postgres/0004_live_indexes.sql +0 -3
  67. package/src/migrations/postgres/meta/0002_snapshot.json +0 -1870
  68. package/src/migrations/postgres/meta/0003_snapshot.json +0 -1917
  69. package/src/migrations/postgres/meta/0004_snapshot.json +0 -2006
  70. package/src/migrations/sqlite/0001_remove_syncs.sql +0 -13
  71. package/src/migrations/sqlite/0002_service_bindings.sql +0 -23
  72. package/src/migrations/sqlite/0003_exchanges.sql +0 -7
  73. package/src/migrations/sqlite/0004_live_indexes.sql +0 -3
  74. package/src/migrations/sqlite/meta/0002_snapshot.json +0 -1736
  75. package/src/migrations/sqlite/meta/0003_snapshot.json +0 -1781
  76. package/src/migrations/sqlite/meta/0004_snapshot.json +0 -1811
@@ -2,7 +2,7 @@ import { n as relationsOf, t as ACTIVE_SUBJECT } from "./relations-BfR3JjT1.js";
2
2
  import { asPostgres } from "./portable.js";
3
3
  import { sql } from "drizzle-orm";
4
4
  import { ROLE_NAMES, assignableToEnvironment } from "@coffre/core/access";
5
- import { blob, check, customType, foreignKey, index, integer, sqliteTable, text, unique, uniqueIndex } from "drizzle-orm/sqlite-core";
5
+ import { blob, check, customType, foreignKey, index, integer, primaryKey, sqliteTable, text, unique, uniqueIndex } from "drizzle-orm/sqlite-core";
6
6
  //#region src/schema.sqlite.ts
7
7
  /**
8
8
  * The schema in schema.ts, for SQLite (through libsql). Same tables,
@@ -28,13 +28,26 @@ const createdAt = () => time("created_at").notNull().default(now);
28
28
  const flag = (name) => integer(name, { mode: "boolean" });
29
29
  /** `^[a-z0-9][a-z0-9-]{0,max-1}$`, in GLOB. */
30
30
  const isSlug = (column, max) => sql`length(${column}) BETWEEN 1 AND ${sql.raw(String(max))} AND ${column} GLOB '[a-z0-9]*' AND ${column} NOT GLOB '*[^a-z0-9-]*'`;
31
+ /** A slug, or a tombstone's, `market~deleted-2026-10-05`: schema.ts's `PLACE_SLUG`, in GLOB. */
32
+ const isPlaceSlug = (column) => {
33
+ const base = sql`substr(${column}, 1, instr(${column}, '~') - 1)`;
34
+ const suffix = sql`substr(${column}, instr(${column}, '~') + 1)`;
35
+ return sql`(${isSlug(column, 63)}) OR (instr(${column}, '~') BETWEEN 2 AND 64 AND ${base} GLOB '[a-z0-9]*' AND ${base} NOT GLOB '*[^a-z0-9-]*' AND length(${suffix}) BETWEEN 1 AND 40 AND ${suffix} NOT GLOB '*[^a-z0-9-]*')`;
36
+ };
37
+ /** A folder's name; the app also refuses control characters, which GLOB has no class for. */
38
+ const folderCheck = (name, column) => check(name, sql`${column} IS NULL OR (length(${column}) BETWEEN 1 AND 64 AND ${column} NOT GLOB '*/*' AND ${column} = trim(${column}))`);
31
39
  const projects = sqliteTable("projects", {
32
40
  id: text().primaryKey(),
33
41
  slug: text().notNull(),
34
42
  name: text().notNull(),
35
43
  createdAt: createdAt(),
36
- archivedAt: time("archived_at")
37
- }, (table) => [unique("projects_slug_key").on(table.slug), check("projects_slug_check", isSlug(table.slug, 63))]);
44
+ archivedAt: time("archived_at"),
45
+ folder: text()
46
+ }, (table) => [
47
+ unique("projects_slug_key").on(table.slug),
48
+ check("projects_slug_check", isPlaceSlug(table.slug)),
49
+ folderCheck("projects_folder_check", table.folder)
50
+ ]);
38
51
  const environments = sqliteTable("environments", {
39
52
  id: text().primaryKey(),
40
53
  projectId: text("project_id").notNull(),
@@ -45,7 +58,7 @@ const environments = sqliteTable("environments", {
45
58
  }, (table) => [
46
59
  unique("environments_project_id_slug_key").on(table.projectId, table.slug),
47
60
  unique("environments_project_scoped").on(table.id, table.projectId),
48
- check("environments_slug_check", isSlug(table.slug, 63)),
61
+ check("environments_slug_check", isPlaceSlug(table.slug)),
49
62
  foreignKey({
50
63
  name: "environments_project_id_fkey",
51
64
  columns: [table.projectId],
@@ -61,10 +74,12 @@ const secrets = sqliteTable("secrets", {
61
74
  updatedAt: time("updated_at").notNull().default(now),
62
75
  currentVersionId: text("current_version_id").references(() => secretVersions.id, { onDelete: "no action" }),
63
76
  currentVersion: integer("current_version").notNull().default(0),
64
- archivedAt: time("archived_at")
77
+ archivedAt: time("archived_at"),
78
+ folder: text()
65
79
  }, (table) => [
66
80
  unique("secrets_project_id_environment_id_key_key").on(table.projectId, table.environmentId, table.key),
67
81
  check("secrets_key_check", sql`length(${table.key}) BETWEEN 1 AND 128 AND ${table.key} GLOB '[A-Za-z_]*' AND ${table.key} NOT GLOB '*[^A-Za-z0-9_]*'`),
82
+ folderCheck("secrets_folder_check", table.folder),
68
83
  foreignKey({
69
84
  name: "secrets_project_id_fkey",
70
85
  columns: [table.projectId],
@@ -108,6 +123,25 @@ const secretVersions = sqliteTable("secret_versions", {
108
123
  }).onDelete("restrict"),
109
124
  index("secret_versions_secret_idx").on(table.secretId, table.version)
110
125
  ]);
126
+ const dismissedKeys = sqliteTable("dismissed_keys", {
127
+ environmentId: text("environment_id").notNull(),
128
+ key: text().notNull(),
129
+ dismissedAt: time("dismissed_at").notNull().default(now),
130
+ dismissedBy: text("dismissed_by").notNull(),
131
+ restoredAt: time("restored_at"),
132
+ restoredBy: text("restored_by")
133
+ }, (table) => [
134
+ primaryKey({
135
+ name: "dismissed_keys_pkey",
136
+ columns: [table.environmentId, table.key]
137
+ }),
138
+ check("dismissed_keys_key_check", sql`length(${table.key}) BETWEEN 1 AND 128 AND ${table.key} GLOB '[A-Za-z_]*' AND ${table.key} NOT GLOB '*[^A-Za-z0-9_]*'`),
139
+ foreignKey({
140
+ name: "dismissed_keys_environment_id_fkey",
141
+ columns: [table.environmentId],
142
+ foreignColumns: [environments.id]
143
+ }).onDelete("restrict")
144
+ ]);
111
145
  const ROLES = sql.raw(ROLE_NAMES.map((role) => `'${role}'`).join(", "));
112
146
  const ENVIRONMENT_ROLES = sql.raw(ROLE_NAMES.filter(assignableToEnvironment).map((role) => `'${role}'`).join(", "));
113
147
  const vaultMembers = sqliteTable("vault_members", {
@@ -123,7 +157,7 @@ const vaultMembers = sqliteTable("vault_members", {
123
157
  mac: bytes("mac").notNull()
124
158
  }, (table) => [
125
159
  check("vault_members_mac_check", sql`octet_length(${table.mac}) = 32`),
126
- check("vault_members_principal_check", sql`(${table.principal} GLOB 'user:?*' OR ${table.principal} GLOB 'token:?*' OR ${table.principal} GLOB 'sync:?*')
160
+ check("vault_members_principal_check", sql`(${table.principal} GLOB 'user:?*' OR ${table.principal} GLOB 'token:?*')
127
161
  AND substr(${table.principal}, instr(${table.principal}, ':') + 1, 1) <> ':'
128
162
  AND instr(${table.principal}, ' ') = 0 AND instr(${table.principal}, char(9)) = 0
129
163
  AND instr(${table.principal}, char(10)) = 0 AND instr(${table.principal}, char(13)) = 0`),
@@ -136,16 +170,20 @@ const vaultGrants = sqliteTable("vault_grants", {
136
170
  principal: text().notNull(),
137
171
  projectId: text("project_id"),
138
172
  environmentId: text("environment_id"),
173
+ environmentSlug: text("environment_slug"),
139
174
  role: text().notNull(),
140
175
  expiresAt: integer("expires_at", { mode: "number" }),
141
176
  grantedAt: integer("granted_at", { mode: "number" }).notNull(),
142
177
  grantedBy: text("granted_by").notNull()
143
178
  }, (table) => [
144
- check("vault_grants_one_place", sql`(${table.projectId} IS NULL) <> (${table.environmentId} IS NULL)`),
179
+ check("vault_grants_one_place", sql`((${table.projectId} IS NULL) <> (${table.environmentId} IS NULL) AND ${table.environmentSlug} IS NULL) OR (${table.projectId} IS NULL AND ${table.environmentId} IS NULL)`),
180
+ check("vault_grants_environment_slug_check", sql`${table.environmentSlug} IS NULL OR (${isSlug(table.environmentSlug, 63)})`),
145
181
  check("vault_grants_role_check", sql`${table.role} IN (${ROLES})`),
146
- check("vault_grants_environment_role_check", sql`${table.environmentId} IS NULL OR ${table.role} IN (${ENVIRONMENT_ROLES})`),
182
+ check("vault_grants_environment_role_check", sql`(${table.environmentId} IS NULL AND ${table.environmentSlug} IS NULL) OR ${table.role} IN (${ENVIRONMENT_ROLES})`),
147
183
  unique("vault_grants_on_project").on(table.principal, table.projectId),
148
184
  unique("vault_grants_on_environment").on(table.principal, table.environmentId),
185
+ unique("vault_grants_on_environment_slug").on(table.principal, table.environmentSlug),
186
+ uniqueIndex("vault_grants_on_every_project").on(table.principal).where(sql`${table.projectId} IS NULL AND ${table.environmentId} IS NULL AND ${table.environmentSlug} IS NULL`),
149
187
  foreignKey({
150
188
  name: "vault_grants_principal_fkey",
151
189
  columns: [table.principal],
@@ -186,7 +224,7 @@ const auditLog = sqliteTable("audit_log", {
186
224
  hash: bytes("hash").notNull()
187
225
  }, (table) => [
188
226
  check("audit_log_author_check", sql`${table.author} IN ('app', 'vault')`),
189
- check("audit_log_actor_check", sql`${table.actor} GLOB 'user:?*' OR ${table.actor} GLOB 'token:?*' OR ${table.actor} GLOB 'sync:?*' OR ${table.actor} GLOB 'system:?*'`),
227
+ check("audit_log_actor_check", sql`${table.actor} GLOB 'user:?*' OR ${table.actor} GLOB 'token:?*' OR ${table.actor} GLOB 'system:?*'`),
190
228
  check("audit_log_decision_check", sql`${table.decision} IN ('allow', 'deny')`),
191
229
  check("audit_log_metadata_check", sql`json_valid(${table.metadata})`),
192
230
  check("audit_log_prev_hash_check", sql`octet_length(${table.prevHash}) = 32`),
@@ -226,7 +264,45 @@ const auditLog = sqliteTable("audit_log", {
226
264
  index("audit_log_releases_idx").on(table.author, table.actor, table.action, table.decision, table.occurredAt),
227
265
  index("audit_log_subject_idx").on(table.author, table.subjectPrincipal, table.seq),
228
266
  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'`)
267
+ 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'`),
268
+ index("audit_log_reference_end_idx").on(table.relatedSeq).where(sql`${table.author} = 'vault' AND ${table.action} = 'reference.end' AND ${table.decision} = 'allow'`),
269
+ index("audit_log_also_project_idx").on(sql`json_extract(${table.metadata}, '$.also.projectId')`, table.seq).where(sql`json_extract(${table.metadata}, '$.also') IS NOT NULL`),
270
+ index("audit_log_also_environment_idx").on(sql`json_extract(${table.metadata}, '$.also.environmentId')`, table.seq).where(sql`json_extract(${table.metadata}, '$.also') IS NOT NULL`),
271
+ index("audit_log_also_secret_idx").on(sql`json_extract(${table.metadata}, '$.also.secretId')`, table.seq).where(sql`json_extract(${table.metadata}, '$.also') IS NOT NULL`)
272
+ ]);
273
+ const secretReferences = sqliteTable("secret_references", {
274
+ id: text().primaryKey(),
275
+ projectId: text("project_id").notNull(),
276
+ environmentId: text("environment_id").notNull(),
277
+ secretId: text("secret_id").notNull(),
278
+ sourceProjectId: text("source_project_id").notNull(),
279
+ sourceEnvironmentId: text("source_environment_id").notNull(),
280
+ sourceSecretId: text("source_secret_id").notNull(),
281
+ createdSeq: int64("created_seq").notNull(),
282
+ createdAt: createdAt(),
283
+ createdBy: text("created_by").notNull()
284
+ }, (table) => [
285
+ unique("secret_references_created_seq_key").on(table.createdSeq),
286
+ foreignKey({
287
+ name: "secret_references_secret_id_fkey",
288
+ columns: [table.secretId],
289
+ foreignColumns: [secrets.id]
290
+ }).onDelete("restrict"),
291
+ foreignKey({
292
+ name: "secret_references_source_secret_id_fkey",
293
+ columns: [table.sourceSecretId],
294
+ foreignColumns: [secrets.id]
295
+ }).onDelete("restrict"),
296
+ foreignKey({
297
+ name: "secret_references_created_seq_fkey",
298
+ columns: [table.createdSeq],
299
+ foreignColumns: [auditLog.seq]
300
+ }).onDelete("restrict"),
301
+ index("secret_references_holder_idx").on(table.secretId, table.createdSeq),
302
+ index("secret_references_source_idx").on(table.sourceSecretId),
303
+ index("secret_references_project_idx").on(table.projectId),
304
+ index("secret_references_source_project_idx").on(table.sourceProjectId),
305
+ index("secret_references_created_by_idx").on(table.createdBy)
230
306
  ]);
231
307
  const auditChainHead = sqliteTable("audit_chain_head", {
232
308
  onlyRow: flag("only_row").primaryKey().default(true),
@@ -296,6 +372,92 @@ const consumedTokens = sqliteTable("consumed_tokens", {
296
372
  hash: bytes("hash").primaryKey(),
297
373
  consumedAt: time("consumed_at").notNull().default(now)
298
374
  }, (table) => [check("consumed_tokens_hash_check", sql`octet_length(${table.hash}) = 32`)]);
375
+ /**
376
+ * MCP clients that registered themselves (RFC 7591), the fallback to Client
377
+ * ID Metadata Documents, which need no row. The ID is the client's
378
+ * `client_id`. Its redirects are under its MAC: a database writer cannot add
379
+ * one of their own to a client people have approved.
380
+ */
381
+ const oauthClients = sqliteTable("oauth_clients", {
382
+ id: text().primaryKey(),
383
+ authMac: bytes("auth_mac").notNull(),
384
+ name: text().notNull(),
385
+ redirectUris: text("redirect_uris").notNull(),
386
+ createdAt: createdAt(),
387
+ createdIp: text("created_ip"),
388
+ revokedAt: time("revoked_at")
389
+ }, (table) => [check("oauth_clients_auth_mac_check", sql`octet_length(${table.authMac}) = 32`), check("oauth_clients_redirect_uris_check", sql`json_valid(${table.redirectUris})`)]);
390
+ /**
391
+ * One MCP client a person connected: one approved authorization, its
392
+ * scopes, its code until the client redeems it, and the refresh token it
393
+ * holds since. Access tokens have no row: each names its connection under a
394
+ * MAC, and every request checks this row. Revoking it ends them all.
395
+ */
396
+ const mcpConnections = sqliteTable("mcp_connections", {
397
+ id: text().primaryKey(),
398
+ authMac: bytes("auth_mac").notNull(),
399
+ principal: text().notNull(),
400
+ generation: integer().notNull(),
401
+ clientId: text("client_id").notNull(),
402
+ clientName: text("client_name").notNull(),
403
+ clientHost: text("client_host"),
404
+ registration: text().notNull(),
405
+ scopes: text().notNull(),
406
+ redirectUri: text("redirect_uri").notNull(),
407
+ codeHash: bytes("code_hash"),
408
+ codeChallenge: text("code_challenge"),
409
+ codeExpiresAt: time("code_expires_at"),
410
+ refreshHash: bytes("refresh_hash"),
411
+ refreshPreviousHash: bytes("refresh_previous_hash"),
412
+ expiresAt: time("expires_at").notNull(),
413
+ createdAt: createdAt(),
414
+ lastUsedAt: time("last_used_at"),
415
+ lastUsedIp: text("last_used_ip"),
416
+ revokedAt: time("revoked_at"),
417
+ revokedBy: text("revoked_by")
418
+ }, (table) => [
419
+ check("mcp_connections_principal_check", sql`${table.principal} LIKE 'user:%'`),
420
+ check("mcp_connections_registration_check", sql`${table.registration} IN ('cimd', 'dcr')`),
421
+ check("mcp_connections_auth_mac_check", sql`octet_length(${table.authMac}) = 32`),
422
+ check("mcp_connections_code_hash_check", sql`${table.codeHash} IS NULL OR octet_length(${table.codeHash}) = 32`),
423
+ check("mcp_connections_refresh_hash_check", sql`${table.refreshHash} IS NULL OR octet_length(${table.refreshHash}) = 32`),
424
+ foreignKey({
425
+ name: "mcp_connections_principal_fkey",
426
+ columns: [table.principal],
427
+ foreignColumns: [vaultMembers.principal]
428
+ }).onDelete("restrict"),
429
+ unique("mcp_connections_code_hash_key").on(table.codeHash),
430
+ unique("mcp_connections_refresh_hash_key").on(table.refreshHash),
431
+ index("mcp_connections_refresh_previous_idx").on(table.refreshPreviousHash),
432
+ index("mcp_connections_live_idx").on(table.principal, table.expiresAt).where(sql`${table.revokedAt} IS NULL`)
433
+ ]);
434
+ /**
435
+ * A change an MCP client asked for, waiting for its person on coffre's own
436
+ * page: see schema.ts.
437
+ */
438
+ const mcpApprovals = sqliteTable("mcp_approvals", {
439
+ id: text().primaryKey(),
440
+ connectionId: text("connection_id").notNull(),
441
+ tool: text().notNull(),
442
+ arguments: text().notNull(),
443
+ digest: bytes("digest").notNull(),
444
+ status: text().notNull().default("pending"),
445
+ outcome: text(),
446
+ createdAt: createdAt(),
447
+ expiresAt: time("expires_at").notNull(),
448
+ decidedAt: time("decided_at"),
449
+ reportedAt: time("reported_at")
450
+ }, (table) => [
451
+ check("mcp_approvals_status_check", sql`${table.status} IN ('pending', 'approved', 'denied', 'cancelled', 'failed')`),
452
+ check("mcp_approvals_digest_check", sql`octet_length(${table.digest}) = 32`),
453
+ check("mcp_approvals_arguments_check", sql`json_valid(${table.arguments})`),
454
+ foreignKey({
455
+ name: "mcp_approvals_connection_fkey",
456
+ columns: [table.connectionId],
457
+ foreignColumns: [mcpConnections.id]
458
+ }).onDelete("restrict"),
459
+ index("mcp_approvals_open_idx").on(table.connectionId, table.digest).where(sql`${table.reportedAt} IS NULL`)
460
+ ]);
299
461
  const credentials = sqliteTable("credentials", {
300
462
  id: text().primaryKey(),
301
463
  kind: text().notNull(),
@@ -379,4 +541,4 @@ const { environmentsRelations, secretsRelations } = relationsOf(asPostgres({
379
541
  secretVersions
380
542
  }));
381
543
  //#endregion
382
- export { auditChainHead, auditLog, consumedTokens, credentials, deviceAuthorizations, environments, environmentsRelations, identities, projects, secretVersions, secrets, secretsRelations, serviceBindings, vaultGrants, vaultMembers };
544
+ export { auditChainHead, auditLog, consumedTokens, credentials, deviceAuthorizations, dismissedKeys, environments, environmentsRelations, identities, mcpApprovals, mcpConnections, oauthClients, projects, secretReferences, secretVersions, secrets, secretsRelations, serviceBindings, vaultGrants, vaultMembers };
@@ -1,21 +1,17 @@
1
- import { i as Queryable } from "./database-Cq_52uUo.js";
1
+ import { i as Queryable } from "./database-edkMkG02.js";
2
2
  import { Engine } from "./dialect.js";
3
3
  //#region src/schema-version.d.ts
4
- /**
5
- * Minimum migration prefix required by this application image, per engine:
6
- * Advance this only when the runtime needs the new schema. The sync-removal
7
- * migration only drops unused tables: new code works with either prefix,
8
- * so Workers Builds can deploy before an owner runs the migration.
9
- * Applied migrations remain immutable (see baseline.ts).
10
- */
11
- export declare const REQUIRED_MIGRATIONS: Record<Engine, number>;
12
- export declare function requiredMigrations(db: Queryable): number;
13
4
  /**
14
5
  * Every migration this version ships, oldest first, per engine: read from
15
6
  * the journals at build time, so a Worker, which has no files, knows them
16
- * too. A database that has applied fewer is behind this code, whatever
17
- * `REQUIRED_MIGRATIONS` lets it run on.
7
+ * too.
18
8
  */
19
9
  export declare const KNOWN_MIGRATIONS: Record<Engine, readonly string[]>;
20
- export declare function knownMigrations(db: Queryable): readonly string[];
10
+ /**
11
+ * Whether the database has applied every migration this version ships:
12
+ * below that, the app serves nothing but its readiness, which says so
13
+ * (`coffre migrate` runs before each deploy). A database never loses a
14
+ * migration, so once true it stays true.
15
+ */
16
+ export declare function migrated(db: Queryable): Promise<boolean>;
21
17
  //#endregion
@@ -1,109 +1,57 @@
1
- import { engineOf } from "./dialect.js";
2
- //#region src/migrations/postgres/meta/_journal.json
3
- var entries$1 = [
4
- {
1
+ import { engineOf, migrationLedger } from "./dialect.js";
2
+ import { count, sql } from "drizzle-orm";
3
+ //#endregion
4
+ //#region src/schema-version.ts
5
+ /**
6
+ * Every migration this version ships, oldest first, per engine: read from
7
+ * the journals at build time, so a Worker, which has no files, knows them
8
+ * too.
9
+ */
10
+ const KNOWN_MIGRATIONS = {
11
+ postgres: [{
5
12
  "idx": 0,
6
13
  "version": "7",
7
- "when": 1790930587507,
14
+ "when": 1791237720902,
8
15
  "tag": "0000_baseline",
9
16
  "breakpoints": true
10
- },
11
- {
17
+ }, {
12
18
  "idx": 1,
13
19
  "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",
20
+ "when": 1791242674488,
21
+ "tag": "0001_mcp_approvals",
23
22
  "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
- ];
40
- //#endregion
41
- //#region src/migrations/sqlite/meta/_journal.json
42
- var entries = [
43
- {
23
+ }].map((entry) => entry.tag),
24
+ sqlite: [{
44
25
  "idx": 0,
45
26
  "version": "6",
46
- "when": 1790874462505,
27
+ "when": 1791237724735,
47
28
  "tag": "0000_baseline",
48
29
  "breakpoints": true
49
- },
50
- {
30
+ }, {
51
31
  "idx": 1,
52
32
  "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",
33
+ "when": 1791242675074,
34
+ "tag": "0001_mcp_approvals",
62
35
  "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
- ];
79
- //#endregion
80
- //#region src/schema-version.ts
81
- /**
82
- * Minimum migration prefix required by this application image, per engine:
83
- * Advance this only when the runtime needs the new schema. The sync-removal
84
- * migration only drops unused tables: new code works with either prefix,
85
- * so Workers Builds can deploy before an owner runs the migration.
86
- * Applied migrations remain immutable (see baseline.ts).
87
- */
88
- const REQUIRED_MIGRATIONS = {
89
- postgres: 1,
90
- sqlite: 1
36
+ }].map((entry) => entry.tag)
91
37
  };
92
- function requiredMigrations(db) {
93
- return REQUIRED_MIGRATIONS[engineOf(db)];
94
- }
95
38
  /**
96
- * Every migration this version ships, oldest first, per engine: read from
97
- * the journals at build time, so a Worker, which has no files, knows them
98
- * too. A database that has applied fewer is behind this code, whatever
99
- * `REQUIRED_MIGRATIONS` lets it run on.
39
+ * Whether the database has applied every migration this version ships:
40
+ * below that, the app serves nothing but its readiness, which says so
41
+ * (`coffre migrate` runs before each deploy). A database never loses a
42
+ * migration, so once true it stays true.
100
43
  */
101
- const KNOWN_MIGRATIONS = {
102
- postgres: entries$1.map((entry) => entry.tag),
103
- sqlite: entries.map((entry) => entry.tag)
104
- };
105
- function knownMigrations(db) {
106
- return KNOWN_MIGRATIONS[engineOf(db)];
44
+ async function migrated(db) {
45
+ if (!await hasLedger(db)) return false;
46
+ const [ledger] = await db.select({ n: count() }).from(migrationLedger(db));
47
+ return (ledger?.n ?? 0) >= KNOWN_MIGRATIONS[engineOf(db)].length;
48
+ }
49
+ async function hasLedger(db) {
50
+ if (engineOf(db) === "postgres") {
51
+ const { rows } = await db.execute(sql`SELECT to_regclass('drizzle.__drizzle_migrations') IS NOT NULL AS present`);
52
+ return rows[0]?.present === true;
53
+ }
54
+ return (await db.all(sql`SELECT 1 AS present FROM sqlite_master WHERE type = 'table' AND name = '__drizzle_migrations'`)).length > 0;
107
55
  }
108
56
  //#endregion
109
- export { KNOWN_MIGRATIONS, REQUIRED_MIGRATIONS, knownMigrations, requiredMigrations };
57
+ export { KNOWN_MIGRATIONS, migrated };
package/dist/schema.d.ts CHANGED
@@ -1,2 +1,2 @@
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 };
1
+ import { _ as secrets, a as deviceAuthorizations, b as vaultGrants, c as environmentsRelations, d as mcpConnections, f as oauthClients, g as secretVersions, h as secretReferences, i as credentials, l as identities, n as auditLog, o as dismissedKeys, p as projects, r as consumedTokens, s as environments, t as auditChainHead, u as mcpApprovals, v as secretsRelations, x as vaultMembers, y as serviceBindings } from "./schema-BqCB3XFm.js";
2
+ export { auditChainHead, auditLog, consumedTokens, credentials, deviceAuthorizations, dismissedKeys, environments, environmentsRelations, identities, mcpApprovals, mcpConnections, oauthClients, projects, secretReferences, secretVersions, secrets, secretsRelations, serviceBindings, vaultGrants, vaultMembers };