@coffre/db 0.2.0 → 0.4.0
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-Cq_52uUo.d.ts → database-edkMkG02.d.ts} +1 -1
- package/dist/dialect.d.ts +10 -2
- package/dist/dialect.js +34 -1
- package/dist/grants.d.ts +32 -0
- package/dist/grants.js +27 -0
- package/dist/hyperdrive.d.ts +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/log.d.ts +1 -1
- package/dist/migrate.js +6 -1
- package/dist/migrations/postgres/0000_baseline.sql +174 -46
- package/dist/migrations/postgres/0001_mcp_approvals.sql +24 -0
- package/dist/migrations/postgres/meta/0000_snapshot.json +963 -185
- package/dist/migrations/postgres/meta/0001_snapshot.json +1171 -30
- package/dist/migrations/postgres/meta/_journal.json +3 -24
- package/dist/migrations/sqlite/0000_baseline.sql +142 -35
- package/dist/migrations/sqlite/0001_mcp_approvals.sql +19 -0
- package/dist/migrations/sqlite/meta/0000_snapshot.json +878 -235
- package/dist/migrations/sqlite/meta/0001_snapshot.json +1062 -57
- package/dist/migrations/sqlite/meta/_journal.json +3 -24
- package/dist/portable.d.ts +2 -2
- package/dist/{schema-Wt-p8AOh.d.ts → schema-BqCB3XFm.d.ts} +1153 -83
- package/dist/schema-sqlite.d.ts +1171 -60
- package/dist/schema-sqlite.js +173 -11
- package/dist/schema-version.d.ts +9 -13
- package/dist/schema-version.js +38 -90
- package/dist/schema.d.ts +2 -2
- package/dist/schema.js +208 -15
- package/package.json +7 -2
- package/src/baseline/postgres.sql +46 -10
- package/src/baseline/sqlite.sql +15 -0
- package/src/baseline.ts +7 -3
- package/src/dialect.ts +25 -1
- package/src/grants.ts +51 -0
- package/src/migrate.ts +5 -0
- package/src/migrations/postgres/0000_baseline.sql +174 -46
- package/src/migrations/postgres/0001_mcp_approvals.sql +24 -0
- package/src/migrations/postgres/meta/0000_snapshot.json +963 -185
- package/src/migrations/postgres/meta/0001_snapshot.json +1171 -30
- package/src/migrations/postgres/meta/_journal.json +3 -24
- package/src/migrations/sqlite/0000_baseline.sql +142 -35
- package/src/migrations/sqlite/0001_mcp_approvals.sql +19 -0
- package/src/migrations/sqlite/meta/0000_snapshot.json +878 -235
- package/src/migrations/sqlite/meta/0001_snapshot.json +1062 -57
- package/src/migrations/sqlite/meta/_journal.json +3 -24
- package/src/schema-version.ts +26 -18
- package/src/schema.sqlite.ts +223 -8
- package/src/schema.ts +263 -11
- package/dist/migrations/postgres/0001_remove_syncs.sql +0 -13
- package/dist/migrations/postgres/0002_service_bindings.sql +0 -28
- package/dist/migrations/postgres/0003_exchanges.sql +0 -10
- package/dist/migrations/postgres/0004_live_indexes.sql +0 -3
- package/dist/migrations/postgres/meta/0002_snapshot.json +0 -1870
- package/dist/migrations/postgres/meta/0003_snapshot.json +0 -1917
- package/dist/migrations/postgres/meta/0004_snapshot.json +0 -2006
- package/dist/migrations/sqlite/0001_remove_syncs.sql +0 -13
- package/dist/migrations/sqlite/0002_service_bindings.sql +0 -23
- package/dist/migrations/sqlite/0003_exchanges.sql +0 -7
- package/dist/migrations/sqlite/0004_live_indexes.sql +0 -3
- package/dist/migrations/sqlite/meta/0002_snapshot.json +0 -1736
- package/dist/migrations/sqlite/meta/0003_snapshot.json +0 -1781
- package/dist/migrations/sqlite/meta/0004_snapshot.json +0 -1811
- package/src/migrations/postgres/0001_remove_syncs.sql +0 -13
- package/src/migrations/postgres/0002_service_bindings.sql +0 -28
- package/src/migrations/postgres/0003_exchanges.sql +0 -10
- package/src/migrations/postgres/0004_live_indexes.sql +0 -3
- package/src/migrations/postgres/meta/0002_snapshot.json +0 -1870
- package/src/migrations/postgres/meta/0003_snapshot.json +0 -1917
- package/src/migrations/postgres/meta/0004_snapshot.json +0 -2006
- package/src/migrations/sqlite/0001_remove_syncs.sql +0 -13
- package/src/migrations/sqlite/0002_service_bindings.sql +0 -23
- package/src/migrations/sqlite/0003_exchanges.sql +0 -7
- package/src/migrations/sqlite/0004_live_indexes.sql +0 -3
- package/src/migrations/sqlite/meta/0002_snapshot.json +0 -1736
- package/src/migrations/sqlite/meta/0003_snapshot.json +0 -1781
- package/src/migrations/sqlite/meta/0004_snapshot.json +0 -1811
|
@@ -5,36 +5,15 @@
|
|
|
5
5
|
{
|
|
6
6
|
"idx": 0,
|
|
7
7
|
"version": "6",
|
|
8
|
-
"when":
|
|
8
|
+
"when": 1791237724735,
|
|
9
9
|
"tag": "0000_baseline",
|
|
10
10
|
"breakpoints": true
|
|
11
11
|
},
|
|
12
12
|
{
|
|
13
13
|
"idx": 1,
|
|
14
14
|
"version": "6",
|
|
15
|
-
"when":
|
|
16
|
-
"tag": "
|
|
17
|
-
"breakpoints": true
|
|
18
|
-
},
|
|
19
|
-
{
|
|
20
|
-
"idx": 2,
|
|
21
|
-
"version": "6",
|
|
22
|
-
"when": 1791069305749,
|
|
23
|
-
"tag": "0002_service_bindings",
|
|
24
|
-
"breakpoints": true
|
|
25
|
-
},
|
|
26
|
-
{
|
|
27
|
-
"idx": 3,
|
|
28
|
-
"version": "6",
|
|
29
|
-
"when": 1791072337548,
|
|
30
|
-
"tag": "0003_exchanges",
|
|
31
|
-
"breakpoints": true
|
|
32
|
-
},
|
|
33
|
-
{
|
|
34
|
-
"idx": 4,
|
|
35
|
-
"version": "6",
|
|
36
|
-
"when": 1791084355588,
|
|
37
|
-
"tag": "0004_live_indexes",
|
|
15
|
+
"when": 1791242675074,
|
|
16
|
+
"tag": "0001_mcp_approvals",
|
|
38
17
|
"breakpoints": true
|
|
39
18
|
}
|
|
40
19
|
]
|
package/src/schema-version.ts
CHANGED
|
@@ -1,32 +1,40 @@
|
|
|
1
|
+
import { count, sql, type SQL } from 'drizzle-orm';
|
|
2
|
+
|
|
1
3
|
import type { Queryable } from './database.ts';
|
|
2
|
-
import { engineOf, type Engine } from './dialect.ts';
|
|
4
|
+
import { engineOf, migrationLedger, type Engine } from './dialect.ts';
|
|
3
5
|
import postgresJournal from './migrations/postgres/meta/_journal.json' with { type: 'json' };
|
|
4
6
|
import sqliteJournal from './migrations/sqlite/meta/_journal.json' with { type: 'json' };
|
|
5
7
|
|
|
6
|
-
/**
|
|
7
|
-
* Minimum migration prefix required by this application image, per engine:
|
|
8
|
-
* Advance this only when the runtime needs the new schema. The sync-removal
|
|
9
|
-
* migration only drops unused tables: new code works with either prefix,
|
|
10
|
-
* so Workers Builds can deploy before an owner runs the migration.
|
|
11
|
-
* Applied migrations remain immutable (see baseline.ts).
|
|
12
|
-
*/
|
|
13
|
-
export const REQUIRED_MIGRATIONS: Record<Engine, number> = { postgres: 1, sqlite: 1 };
|
|
14
|
-
|
|
15
|
-
export function requiredMigrations(db: Queryable): number {
|
|
16
|
-
return REQUIRED_MIGRATIONS[engineOf(db)];
|
|
17
|
-
}
|
|
18
|
-
|
|
19
8
|
/**
|
|
20
9
|
* Every migration this version ships, oldest first, per engine: read from
|
|
21
10
|
* the journals at build time, so a Worker, which has no files, knows them
|
|
22
|
-
* too.
|
|
23
|
-
* `REQUIRED_MIGRATIONS` lets it run on.
|
|
11
|
+
* too.
|
|
24
12
|
*/
|
|
25
13
|
export const KNOWN_MIGRATIONS: Record<Engine, readonly string[]> = {
|
|
26
14
|
postgres: postgresJournal.entries.map((entry) => entry.tag),
|
|
27
15
|
sqlite: sqliteJournal.entries.map((entry) => entry.tag),
|
|
28
16
|
};
|
|
29
17
|
|
|
30
|
-
|
|
31
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Whether the database has applied every migration this version ships:
|
|
20
|
+
* below that, the app serves nothing but its readiness, which says so
|
|
21
|
+
* (`coffre migrate` runs before each deploy). A database never loses a
|
|
22
|
+
* migration, so once true it stays true.
|
|
23
|
+
*/
|
|
24
|
+
export async function migrated(db: Queryable): Promise<boolean> {
|
|
25
|
+
// A database never migrated has no ledger at all, which is no migration, not an error.
|
|
26
|
+
if (!(await hasLedger(db))) return false;
|
|
27
|
+
const [ledger] = await db.select({ n: count() }).from(migrationLedger(db));
|
|
28
|
+
return (ledger?.n ?? 0) >= KNOWN_MIGRATIONS[engineOf(db)].length;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function hasLedger(db: Queryable): Promise<boolean> {
|
|
32
|
+
if (engineOf(db) === 'postgres') {
|
|
33
|
+
const { rows } = await db.execute<{ present: boolean }>(sql`SELECT to_regclass('drizzle.__drizzle_migrations') IS NOT NULL AS present`);
|
|
34
|
+
return rows[0]?.present === true;
|
|
35
|
+
}
|
|
36
|
+
const rows = await (db as unknown as { all<R>(query: SQL): Promise<R[]> }).all<{ present: number }>(
|
|
37
|
+
sql`SELECT 1 AS present FROM sqlite_master WHERE type = 'table' AND name = '__drizzle_migrations'`,
|
|
38
|
+
);
|
|
39
|
+
return rows.length > 0;
|
|
32
40
|
}
|
package/src/schema.sqlite.ts
CHANGED
|
@@ -48,6 +48,17 @@ const flag = (name: string) => integer(name, { mode: 'boolean' });
|
|
|
48
48
|
const isSlug = (column: AnySQLiteColumn, max: number) =>
|
|
49
49
|
sql`length(${column}) BETWEEN 1 AND ${sql.raw(String(max))} AND ${column} GLOB '[a-z0-9]*' AND ${column} NOT GLOB '*[^a-z0-9-]*'`;
|
|
50
50
|
|
|
51
|
+
/** A slug, or a tombstone's, `market~deleted-2026-10-05`: schema.ts's `PLACE_SLUG`, in GLOB. */
|
|
52
|
+
const isPlaceSlug = (column: AnySQLiteColumn) => {
|
|
53
|
+
const base = sql`substr(${column}, 1, instr(${column}, '~') - 1)`;
|
|
54
|
+
const suffix = sql`substr(${column}, instr(${column}, '~') + 1)`;
|
|
55
|
+
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-]*')`;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/** A folder's name; the app also refuses control characters, which GLOB has no class for. */
|
|
59
|
+
const folderCheck = (name: string, column: AnySQLiteColumn) =>
|
|
60
|
+
check(name, sql`${column} IS NULL OR (length(${column}) BETWEEN 1 AND 64 AND ${column} NOT GLOB '*/*' AND ${column} = trim(${column}))`);
|
|
61
|
+
|
|
51
62
|
export const projects = sqliteTable(
|
|
52
63
|
'projects',
|
|
53
64
|
{
|
|
@@ -56,10 +67,12 @@ export const projects = sqliteTable(
|
|
|
56
67
|
name: text().notNull(),
|
|
57
68
|
createdAt: createdAt(),
|
|
58
69
|
archivedAt: time('archived_at'),
|
|
70
|
+
folder: text(),
|
|
59
71
|
},
|
|
60
72
|
(table) => [
|
|
61
73
|
unique('projects_slug_key').on(table.slug),
|
|
62
|
-
check('projects_slug_check',
|
|
74
|
+
check('projects_slug_check', isPlaceSlug(table.slug)),
|
|
75
|
+
folderCheck('projects_folder_check', table.folder),
|
|
63
76
|
],
|
|
64
77
|
);
|
|
65
78
|
|
|
@@ -76,7 +89,7 @@ export const environments = sqliteTable(
|
|
|
76
89
|
(table) => [
|
|
77
90
|
unique('environments_project_id_slug_key').on(table.projectId, table.slug),
|
|
78
91
|
unique('environments_project_scoped').on(table.id, table.projectId),
|
|
79
|
-
check('environments_slug_check',
|
|
92
|
+
check('environments_slug_check', isPlaceSlug(table.slug)),
|
|
80
93
|
foreignKey({
|
|
81
94
|
name: 'environments_project_id_fkey',
|
|
82
95
|
columns: [table.projectId],
|
|
@@ -99,6 +112,7 @@ export const secrets = sqliteTable(
|
|
|
99
112
|
}),
|
|
100
113
|
currentVersion: integer('current_version').notNull().default(0),
|
|
101
114
|
archivedAt: time('archived_at'),
|
|
115
|
+
folder: text(),
|
|
102
116
|
},
|
|
103
117
|
(table) => [
|
|
104
118
|
unique('secrets_project_id_environment_id_key_key').on(table.projectId, table.environmentId, table.key),
|
|
@@ -106,6 +120,7 @@ export const secrets = sqliteTable(
|
|
|
106
120
|
'secrets_key_check',
|
|
107
121
|
sql`length(${table.key}) BETWEEN 1 AND 128 AND ${table.key} GLOB '[A-Za-z_]*' AND ${table.key} NOT GLOB '*[^A-Za-z0-9_]*'`,
|
|
108
122
|
),
|
|
123
|
+
folderCheck('secrets_folder_check', table.folder),
|
|
109
124
|
foreignKey({
|
|
110
125
|
name: 'secrets_project_id_fkey',
|
|
111
126
|
columns: [table.projectId],
|
|
@@ -156,6 +171,30 @@ export const secretVersions = sqliteTable(
|
|
|
156
171
|
],
|
|
157
172
|
);
|
|
158
173
|
|
|
174
|
+
export const dismissedKeys = sqliteTable(
|
|
175
|
+
'dismissed_keys',
|
|
176
|
+
{
|
|
177
|
+
environmentId: text('environment_id').notNull(),
|
|
178
|
+
key: text().notNull(),
|
|
179
|
+
dismissedAt: time('dismissed_at').notNull().default(now),
|
|
180
|
+
dismissedBy: text('dismissed_by').notNull(),
|
|
181
|
+
restoredAt: time('restored_at'),
|
|
182
|
+
restoredBy: text('restored_by'),
|
|
183
|
+
},
|
|
184
|
+
(table) => [
|
|
185
|
+
primaryKey({ name: 'dismissed_keys_pkey', columns: [table.environmentId, table.key] }),
|
|
186
|
+
check(
|
|
187
|
+
'dismissed_keys_key_check',
|
|
188
|
+
sql`length(${table.key}) BETWEEN 1 AND 128 AND ${table.key} GLOB '[A-Za-z_]*' AND ${table.key} NOT GLOB '*[^A-Za-z0-9_]*'`,
|
|
189
|
+
),
|
|
190
|
+
foreignKey({
|
|
191
|
+
name: 'dismissed_keys_environment_id_fkey',
|
|
192
|
+
columns: [table.environmentId],
|
|
193
|
+
foreignColumns: [environments.id],
|
|
194
|
+
}).onDelete('restrict'),
|
|
195
|
+
],
|
|
196
|
+
);
|
|
197
|
+
|
|
159
198
|
const ROLES = sql.raw(ROLE_NAMES.map((role) => `'${role}'`).join(', '));
|
|
160
199
|
const ENVIRONMENT_ROLES = sql.raw(ROLE_NAMES.filter(assignableToEnvironment).map((role) => `'${role}'`).join(', '));
|
|
161
200
|
|
|
@@ -175,10 +214,10 @@ export const vaultMembers = sqliteTable(
|
|
|
175
214
|
},
|
|
176
215
|
(table) => [
|
|
177
216
|
check('vault_members_mac_check', sql`octet_length(${table.mac}) = 32`),
|
|
178
|
-
// `^(user|token
|
|
217
|
+
// `^(user|token):[^[:space:]:][^[:space:]]*$`, without regular expressions.
|
|
179
218
|
check(
|
|
180
219
|
'vault_members_principal_check',
|
|
181
|
-
sql`(${table.principal} GLOB 'user:?*' OR ${table.principal} GLOB 'token:?*'
|
|
220
|
+
sql`(${table.principal} GLOB 'user:?*' OR ${table.principal} GLOB 'token:?*')
|
|
182
221
|
AND substr(${table.principal}, instr(${table.principal}, ':') + 1, 1) <> ':'
|
|
183
222
|
AND instr(${table.principal}, ' ') = 0 AND instr(${table.principal}, char(9)) = 0
|
|
184
223
|
AND instr(${table.principal}, char(10)) = 0 AND instr(${table.principal}, char(13)) = 0`,
|
|
@@ -196,17 +235,29 @@ export const vaultGrants = sqliteTable(
|
|
|
196
235
|
principal: text().notNull(),
|
|
197
236
|
projectId: text('project_id'),
|
|
198
237
|
environmentId: text('environment_id'),
|
|
238
|
+
environmentSlug: text('environment_slug'),
|
|
199
239
|
role: text().notNull(),
|
|
200
240
|
expiresAt: integer('expires_at', { mode: 'number' }),
|
|
201
241
|
grantedAt: integer('granted_at', { mode: 'number' }).notNull(),
|
|
202
242
|
grantedBy: text('granted_by').notNull(),
|
|
203
243
|
},
|
|
204
244
|
(table) => [
|
|
205
|
-
check(
|
|
245
|
+
check(
|
|
246
|
+
'vault_grants_one_place',
|
|
247
|
+
sql`((${table.projectId} IS NULL) <> (${table.environmentId} IS NULL) AND ${table.environmentSlug} IS NULL) OR (${table.projectId} IS NULL AND ${table.environmentId} IS NULL)`,
|
|
248
|
+
),
|
|
249
|
+
check('vault_grants_environment_slug_check', sql`${table.environmentSlug} IS NULL OR (${isSlug(table.environmentSlug, 63)})`),
|
|
206
250
|
check('vault_grants_role_check', sql`${table.role} IN (${ROLES})`),
|
|
207
|
-
check(
|
|
251
|
+
check(
|
|
252
|
+
'vault_grants_environment_role_check',
|
|
253
|
+
sql`(${table.environmentId} IS NULL AND ${table.environmentSlug} IS NULL) OR ${table.role} IN (${ENVIRONMENT_ROLES})`,
|
|
254
|
+
),
|
|
208
255
|
unique('vault_grants_on_project').on(table.principal, table.projectId),
|
|
209
256
|
unique('vault_grants_on_environment').on(table.principal, table.environmentId),
|
|
257
|
+
unique('vault_grants_on_environment_slug').on(table.principal, table.environmentSlug),
|
|
258
|
+
uniqueIndex('vault_grants_on_every_project')
|
|
259
|
+
.on(table.principal)
|
|
260
|
+
.where(sql`${table.projectId} IS NULL AND ${table.environmentId} IS NULL AND ${table.environmentSlug} IS NULL`),
|
|
210
261
|
foreignKey({
|
|
211
262
|
name: 'vault_grants_principal_fkey',
|
|
212
263
|
columns: [table.principal],
|
|
@@ -236,7 +287,7 @@ export const auditLog = sqliteTable(
|
|
|
236
287
|
// Milliseconds since the epoch, from the database's clock, read after the
|
|
237
288
|
// append took the head's lock.
|
|
238
289
|
occurredAt: integer('occurred_at', { mode: 'number' }).notNull(),
|
|
239
|
-
// Who acted: `user:<email>`, `token:<id
|
|
290
|
+
// Who acted: `user:<email>`, `token:<id>` or `system:<name>`.
|
|
240
291
|
actor: text().notNull(),
|
|
241
292
|
action: text().notNull(),
|
|
242
293
|
decision: text().notNull(),
|
|
@@ -262,7 +313,7 @@ export const auditLog = sqliteTable(
|
|
|
262
313
|
check('audit_log_author_check', sql`${table.author} IN ('app', 'vault')`),
|
|
263
314
|
check(
|
|
264
315
|
'audit_log_actor_check',
|
|
265
|
-
sql`${table.actor} GLOB 'user:?*' OR ${table.actor} GLOB 'token:?*' OR ${table.actor} GLOB '
|
|
316
|
+
sql`${table.actor} GLOB 'user:?*' OR ${table.actor} GLOB 'token:?*' OR ${table.actor} GLOB 'system:?*'`,
|
|
266
317
|
),
|
|
267
318
|
check('audit_log_decision_check', sql`${table.decision} IN ('allow', 'deny')`),
|
|
268
319
|
check('audit_log_metadata_check', sql`json_valid(${table.metadata})`),
|
|
@@ -311,6 +362,57 @@ export const auditLog = sqliteTable(
|
|
|
311
362
|
index('audit_log_exchange_idx')
|
|
312
363
|
.on(sql`json_extract(${table.metadata}, '$.credentialId')`)
|
|
313
364
|
.where(sql`${table.author} = 'app' AND ${table.action} = 'token.exchange' AND ${table.decision} = 'allow'`),
|
|
365
|
+
index('audit_log_reference_end_idx')
|
|
366
|
+
.on(table.relatedSeq)
|
|
367
|
+
.where(sql`${table.author} = 'vault' AND ${table.action} = 'reference.end' AND ${table.decision} = 'allow'`),
|
|
368
|
+
index('audit_log_also_project_idx')
|
|
369
|
+
.on(sql`json_extract(${table.metadata}, '$.also.projectId')`, table.seq)
|
|
370
|
+
.where(sql`json_extract(${table.metadata}, '$.also') IS NOT NULL`),
|
|
371
|
+
index('audit_log_also_environment_idx')
|
|
372
|
+
.on(sql`json_extract(${table.metadata}, '$.also.environmentId')`, table.seq)
|
|
373
|
+
.where(sql`json_extract(${table.metadata}, '$.also') IS NOT NULL`),
|
|
374
|
+
index('audit_log_also_secret_idx')
|
|
375
|
+
.on(sql`json_extract(${table.metadata}, '$.also.secretId')`, table.seq)
|
|
376
|
+
.where(sql`json_extract(${table.metadata}, '$.also') IS NOT NULL`),
|
|
377
|
+
],
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
export const secretReferences = sqliteTable(
|
|
381
|
+
'secret_references',
|
|
382
|
+
{
|
|
383
|
+
id: text().primaryKey(),
|
|
384
|
+
projectId: text('project_id').notNull(),
|
|
385
|
+
environmentId: text('environment_id').notNull(),
|
|
386
|
+
secretId: text('secret_id').notNull(),
|
|
387
|
+
sourceProjectId: text('source_project_id').notNull(),
|
|
388
|
+
sourceEnvironmentId: text('source_environment_id').notNull(),
|
|
389
|
+
sourceSecretId: text('source_secret_id').notNull(),
|
|
390
|
+
createdSeq: int64('created_seq').notNull(),
|
|
391
|
+
createdAt: createdAt(),
|
|
392
|
+
createdBy: text('created_by').notNull(),
|
|
393
|
+
},
|
|
394
|
+
(table) => [
|
|
395
|
+
unique('secret_references_created_seq_key').on(table.createdSeq),
|
|
396
|
+
foreignKey({
|
|
397
|
+
name: 'secret_references_secret_id_fkey',
|
|
398
|
+
columns: [table.secretId],
|
|
399
|
+
foreignColumns: [secrets.id],
|
|
400
|
+
}).onDelete('restrict'),
|
|
401
|
+
foreignKey({
|
|
402
|
+
name: 'secret_references_source_secret_id_fkey',
|
|
403
|
+
columns: [table.sourceSecretId],
|
|
404
|
+
foreignColumns: [secrets.id],
|
|
405
|
+
}).onDelete('restrict'),
|
|
406
|
+
foreignKey({
|
|
407
|
+
name: 'secret_references_created_seq_fkey',
|
|
408
|
+
columns: [table.createdSeq],
|
|
409
|
+
foreignColumns: [auditLog.seq],
|
|
410
|
+
}).onDelete('restrict'),
|
|
411
|
+
index('secret_references_holder_idx').on(table.secretId, table.createdSeq),
|
|
412
|
+
index('secret_references_source_idx').on(table.sourceSecretId),
|
|
413
|
+
index('secret_references_project_idx').on(table.projectId),
|
|
414
|
+
index('secret_references_source_project_idx').on(table.sourceProjectId),
|
|
415
|
+
index('secret_references_created_by_idx').on(table.createdBy),
|
|
314
416
|
],
|
|
315
417
|
);
|
|
316
418
|
|
|
@@ -410,6 +512,119 @@ export const consumedTokens = sqliteTable(
|
|
|
410
512
|
(table) => [check('consumed_tokens_hash_check', sql`octet_length(${table.hash}) = 32`)],
|
|
411
513
|
);
|
|
412
514
|
|
|
515
|
+
/**
|
|
516
|
+
* MCP clients that registered themselves (RFC 7591), the fallback to Client
|
|
517
|
+
* ID Metadata Documents, which need no row. The ID is the client's
|
|
518
|
+
* `client_id`. Its redirects are under its MAC: a database writer cannot add
|
|
519
|
+
* one of their own to a client people have approved.
|
|
520
|
+
*/
|
|
521
|
+
export const oauthClients = sqliteTable(
|
|
522
|
+
'oauth_clients',
|
|
523
|
+
{
|
|
524
|
+
id: text().primaryKey(),
|
|
525
|
+
authMac: bytes('auth_mac').notNull(),
|
|
526
|
+
// Its own claim, shown as unverified.
|
|
527
|
+
name: text().notNull(),
|
|
528
|
+
// JSON: a list of HTTPS or loopback URLs.
|
|
529
|
+
redirectUris: text('redirect_uris').notNull(),
|
|
530
|
+
createdAt: createdAt(),
|
|
531
|
+
createdIp: text('created_ip'),
|
|
532
|
+
revokedAt: time('revoked_at'),
|
|
533
|
+
},
|
|
534
|
+
(table) => [
|
|
535
|
+
check('oauth_clients_auth_mac_check', sql`octet_length(${table.authMac}) = 32`),
|
|
536
|
+
check('oauth_clients_redirect_uris_check', sql`json_valid(${table.redirectUris})`),
|
|
537
|
+
],
|
|
538
|
+
);
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* One MCP client a person connected: one approved authorization, its
|
|
542
|
+
* scopes, its code until the client redeems it, and the refresh token it
|
|
543
|
+
* holds since. Access tokens have no row: each names its connection under a
|
|
544
|
+
* MAC, and every request checks this row. Revoking it ends them all.
|
|
545
|
+
*/
|
|
546
|
+
export const mcpConnections = sqliteTable(
|
|
547
|
+
'mcp_connections',
|
|
548
|
+
{
|
|
549
|
+
id: text().primaryKey(),
|
|
550
|
+
authMac: bytes('auth_mac').notNull(),
|
|
551
|
+
// The person it acts as, `user:<email>`, under the generation they approved it in.
|
|
552
|
+
principal: text().notNull(),
|
|
553
|
+
generation: integer().notNull(),
|
|
554
|
+
// A metadata document's URL, or a registration's ID.
|
|
555
|
+
clientId: text('client_id').notNull(),
|
|
556
|
+
// As the consent page showed them: display only.
|
|
557
|
+
clientName: text('client_name').notNull(),
|
|
558
|
+
clientHost: text('client_host'),
|
|
559
|
+
registration: text().notNull(),
|
|
560
|
+
// Space-separated, in catalogue order: `browse write`.
|
|
561
|
+
scopes: text().notNull(),
|
|
562
|
+
redirectUri: text('redirect_uri').notNull(),
|
|
563
|
+
// Until the client redeems it: its SHA-256, its PKCE challenge, and 60 seconds.
|
|
564
|
+
codeHash: bytes('code_hash'),
|
|
565
|
+
codeChallenge: text('code_challenge'),
|
|
566
|
+
codeExpiresAt: time('code_expires_at'),
|
|
567
|
+
// From then on: the refresh token's SHA-256, and the one it replaced, whose reuse ends the connection.
|
|
568
|
+
refreshHash: bytes('refresh_hash'),
|
|
569
|
+
refreshPreviousHash: bytes('refresh_previous_hash'),
|
|
570
|
+
expiresAt: time('expires_at').notNull(),
|
|
571
|
+
createdAt: createdAt(),
|
|
572
|
+
lastUsedAt: time('last_used_at'),
|
|
573
|
+
lastUsedIp: text('last_used_ip'),
|
|
574
|
+
revokedAt: time('revoked_at'),
|
|
575
|
+
revokedBy: text('revoked_by'),
|
|
576
|
+
},
|
|
577
|
+
(table) => [
|
|
578
|
+
check('mcp_connections_principal_check', sql`${table.principal} LIKE 'user:%'`),
|
|
579
|
+
check('mcp_connections_registration_check', sql`${table.registration} IN ('cimd', 'dcr')`),
|
|
580
|
+
check('mcp_connections_auth_mac_check', sql`octet_length(${table.authMac}) = 32`),
|
|
581
|
+
check('mcp_connections_code_hash_check', sql`${table.codeHash} IS NULL OR octet_length(${table.codeHash}) = 32`),
|
|
582
|
+
check('mcp_connections_refresh_hash_check', sql`${table.refreshHash} IS NULL OR octet_length(${table.refreshHash}) = 32`),
|
|
583
|
+
foreignKey({
|
|
584
|
+
name: 'mcp_connections_principal_fkey',
|
|
585
|
+
columns: [table.principal],
|
|
586
|
+
foreignColumns: [vaultMembers.principal],
|
|
587
|
+
}).onDelete('restrict'),
|
|
588
|
+
unique('mcp_connections_code_hash_key').on(table.codeHash),
|
|
589
|
+
unique('mcp_connections_refresh_hash_key').on(table.refreshHash),
|
|
590
|
+
index('mcp_connections_refresh_previous_idx').on(table.refreshPreviousHash),
|
|
591
|
+
// A person's live connections: Connected apps, and the limit on how many.
|
|
592
|
+
index('mcp_connections_live_idx').on(table.principal, table.expiresAt).where(sql`${table.revokedAt} IS NULL`),
|
|
593
|
+
],
|
|
594
|
+
);
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* A change an MCP client asked for, waiting for its person on coffre's own
|
|
598
|
+
* page: see schema.ts.
|
|
599
|
+
*/
|
|
600
|
+
export const mcpApprovals = sqliteTable(
|
|
601
|
+
'mcp_approvals',
|
|
602
|
+
{
|
|
603
|
+
id: text().primaryKey(),
|
|
604
|
+
connectionId: text('connection_id').notNull(),
|
|
605
|
+
tool: text().notNull(),
|
|
606
|
+
arguments: text().notNull(),
|
|
607
|
+
digest: bytes('digest').notNull(),
|
|
608
|
+
status: text().notNull().default('pending'),
|
|
609
|
+
outcome: text(),
|
|
610
|
+
createdAt: createdAt(),
|
|
611
|
+
expiresAt: time('expires_at').notNull(),
|
|
612
|
+
decidedAt: time('decided_at'),
|
|
613
|
+
reportedAt: time('reported_at'),
|
|
614
|
+
},
|
|
615
|
+
(table) => [
|
|
616
|
+
check('mcp_approvals_status_check', sql`${table.status} IN ('pending', 'approved', 'denied', 'cancelled', 'failed')`),
|
|
617
|
+
check('mcp_approvals_digest_check', sql`octet_length(${table.digest}) = 32`),
|
|
618
|
+
check('mcp_approvals_arguments_check', sql`json_valid(${table.arguments})`),
|
|
619
|
+
foreignKey({
|
|
620
|
+
name: 'mcp_approvals_connection_fkey',
|
|
621
|
+
columns: [table.connectionId],
|
|
622
|
+
foreignColumns: [mcpConnections.id],
|
|
623
|
+
}).onDelete('restrict'),
|
|
624
|
+
index('mcp_approvals_open_idx').on(table.connectionId, table.digest).where(sql`${table.reportedAt} IS NULL`),
|
|
625
|
+
],
|
|
626
|
+
);
|
|
627
|
+
|
|
413
628
|
export const credentials = sqliteTable(
|
|
414
629
|
'credentials',
|
|
415
630
|
{
|