@coffre/db 0.14.5 → 0.15.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/README.md +2 -1
- package/dist/change-records-_fKvahvs.js +55 -0
- package/dist/connect.d.ts +22 -10
- package/dist/connect.js +74 -12
- package/dist/database-CVteLhp1.d.ts +410 -0
- package/dist/{database-BadNeeN-.js → database-D55ENPX-.js} +46 -25
- package/dist/dialect.d.ts +4 -3
- package/dist/dialect.js +14 -5
- package/dist/grants.d.ts +4 -4
- package/dist/grants.js +4 -4
- package/dist/hyperdrive.d.ts +48 -23
- package/dist/hyperdrive.js +168 -91
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/log-DaD7fyUq.d.ts +722 -0
- package/dist/log.d.ts +2 -622
- package/dist/log.js +178 -13
- package/dist/migrate.d.ts +43 -3
- package/dist/migrate.js +149 -29
- package/dist/migrations/postgres/0022_log_015.sql +672 -0
- package/dist/migrations/postgres/0023_change_path.sql +153 -0
- package/dist/migrations/postgres/0024_use_015.sql +556 -0
- package/dist/migrations/postgres/0025_app_change.sql +221 -0
- package/dist/migrations/postgres/0026_issue_lock.sql +106 -0
- package/dist/migrations/postgres/0027_retention.sql +118 -0
- package/dist/migrations/postgres/0028_use_stalls.sql +594 -0
- package/dist/migrations/postgres/0029_use_objects.sql +887 -0
- package/dist/migrations/postgres/0030_unarchive_restores.sql +259 -0
- package/dist/migrations/postgres/0031_app_read.sql +65 -0
- package/dist/migrations/postgres/0032_secrets_live_index.sql +4 -0
- package/dist/migrations/postgres/0033_use_generations.sql +582 -0
- package/dist/migrations/postgres/0034_vault_credentials.sql +153 -0
- package/dist/migrations/postgres/0035_first_days.sql +3 -0
- package/dist/migrations/postgres/0036_vault_issues.sql +91 -0
- package/dist/migrations/postgres/0037_vault_bindings.sql +271 -0
- package/dist/migrations/postgres/0040_binding_origin.sql +159 -0
- package/dist/migrations/postgres/0041_cutover.sql +132 -0
- package/dist/migrations/postgres/0042_sign_in_index.sql +7 -0
- package/dist/migrations/postgres/0043_issue_revoke_index.sql +4 -0
- package/dist/migrations/postgres/0044_use_by_name.sql +705 -0
- package/dist/migrations/postgres/0045_decide_first.sql +878 -0
- package/dist/migrations/postgres/0046_way_in_uses.sql +1149 -0
- package/dist/migrations/postgres/0047_cutover_finish.sql +268 -0
- package/dist/migrations/postgres/0048_use_batch.sql +59 -0
- package/dist/migrations/postgres/0049_reads.sql +171 -0
- package/dist/migrations/postgres/meta/0022_snapshot.json +3831 -0
- package/dist/migrations/postgres/meta/0023_snapshot.json +3831 -0
- package/dist/migrations/postgres/meta/0024_snapshot.json +3831 -0
- package/dist/migrations/postgres/meta/0025_snapshot.json +3831 -0
- package/dist/migrations/postgres/meta/0026_snapshot.json +3831 -0
- package/dist/migrations/postgres/meta/0027_snapshot.json +3853 -0
- package/dist/migrations/postgres/meta/0028_snapshot.json +3853 -0
- package/dist/migrations/postgres/meta/0029_snapshot.json +3859 -0
- package/dist/migrations/postgres/meta/0030_snapshot.json +3859 -0
- package/dist/migrations/postgres/meta/0031_snapshot.json +3859 -0
- package/dist/migrations/postgres/meta/0032_snapshot.json +3875 -0
- package/dist/migrations/postgres/meta/0033_snapshot.json +3875 -0
- package/dist/migrations/postgres/meta/0034_snapshot.json +3875 -0
- package/dist/migrations/postgres/meta/0035_snapshot.json +3875 -0
- package/dist/migrations/postgres/meta/0036_snapshot.json +3875 -0
- package/dist/migrations/postgres/meta/0037_snapshot.json +3875 -0
- package/dist/migrations/postgres/meta/0038_snapshot.json +3881 -0
- package/dist/migrations/postgres/meta/0039_snapshot.json +3881 -0
- package/dist/migrations/postgres/meta/0040_snapshot.json +3931 -0
- package/dist/migrations/postgres/meta/0041_snapshot.json +3981 -0
- package/dist/migrations/postgres/meta/0042_snapshot.json +3981 -0
- package/dist/migrations/postgres/meta/0043_snapshot.json +3981 -0
- package/dist/migrations/postgres/meta/0044_snapshot.json +3991 -0
- package/dist/migrations/postgres/meta/0045_snapshot.json +3997 -0
- package/dist/migrations/postgres/meta/0046_snapshot.json +3997 -0
- package/dist/migrations/postgres/meta/0047_snapshot.json +4234 -0
- package/dist/migrations/postgres/meta/_journal.json +182 -0
- package/dist/migrations/sqlite/0018_log_015.sql +84 -0
- package/dist/migrations/sqlite/0019_retention.sql +2 -0
- package/dist/migrations/sqlite/0020_ended_references.sql +1 -0
- package/dist/migrations/sqlite/0021_secrets_live_index.sql +1 -0
- package/dist/migrations/sqlite/0022_binding_origin.sql +1 -0
- package/dist/migrations/sqlite/0023_sign_in_index.sql +2 -0
- package/dist/migrations/sqlite/0024_issue_revoke_index.sql +2 -0
- package/dist/migrations/sqlite/0025_way_in_uses.sql +42 -0
- package/dist/migrations/sqlite/0026_cutover_finish.sql +11 -0
- package/dist/migrations/sqlite/0027_reads.sql +38 -0
- package/dist/migrations/sqlite/meta/0018_snapshot.json +3529 -0
- package/dist/migrations/sqlite/meta/0019_snapshot.json +3538 -0
- package/dist/migrations/sqlite/meta/0020_snapshot.json +3545 -0
- package/dist/migrations/sqlite/meta/0021_snapshot.json +3553 -0
- package/dist/migrations/sqlite/meta/0022_snapshot.json +3560 -0
- package/dist/migrations/sqlite/meta/0023_snapshot.json +3579 -0
- package/dist/migrations/sqlite/meta/0024_snapshot.json +3598 -0
- package/dist/migrations/sqlite/meta/0025_snapshot.json +3609 -0
- package/dist/migrations/sqlite/meta/0026_snapshot.json +3616 -0
- package/dist/migrations/sqlite/meta/0027_snapshot.json +3834 -0
- package/dist/migrations/sqlite/meta/_journal.json +70 -0
- package/dist/platform-Y3gICK6n.d.ts +58 -0
- package/dist/portable.d.ts +2 -2
- package/dist/postgres-BI3xTnyw.js +1718 -0
- package/dist/postgres-platform-node.d.ts +14 -0
- package/dist/postgres-platform-node.js +21 -0
- package/dist/postgres-platform-workerd.d.ts +61 -0
- package/dist/postgres-platform-workerd.js +163 -0
- package/dist/{schema-BfQEBqFv.d.ts → schema-DgoPqqsI.d.ts} +1326 -7
- package/dist/schema-sqlite.d.ts +1306 -6
- package/dist/schema-sqlite.js +154 -18
- package/dist/schema-version.d.ts +1 -1
- package/dist/schema-version.js +254 -2
- package/dist/schema.d.ts +2 -2
- package/dist/schema.js +226 -20
- package/package.json +13 -5
- package/src/active-subject.ts +9 -0
- package/src/change-records.ts +38 -0
- package/src/connect.ts +67 -13
- package/src/database.ts +67 -51
- package/src/dialect.ts +17 -8
- package/src/grants.ts +3 -3
- package/src/hyperdrive.ts +181 -104
- package/src/log.ts +196 -15
- package/src/migrate.ts +157 -30
- package/src/migrations/postgres/0022_log_015.sql +672 -0
- package/src/migrations/postgres/0023_change_path.sql +153 -0
- package/src/migrations/postgres/0024_use_015.sql +556 -0
- package/src/migrations/postgres/0025_app_change.sql +221 -0
- package/src/migrations/postgres/0026_issue_lock.sql +106 -0
- package/src/migrations/postgres/0027_retention.sql +118 -0
- package/src/migrations/postgres/0028_use_stalls.sql +594 -0
- package/src/migrations/postgres/0029_use_objects.sql +887 -0
- package/src/migrations/postgres/0030_unarchive_restores.sql +259 -0
- package/src/migrations/postgres/0031_app_read.sql +65 -0
- package/src/migrations/postgres/0032_secrets_live_index.sql +4 -0
- package/src/migrations/postgres/0033_use_generations.sql +582 -0
- package/src/migrations/postgres/0034_vault_credentials.sql +153 -0
- package/src/migrations/postgres/0035_first_days.sql +3 -0
- package/src/migrations/postgres/0036_vault_issues.sql +91 -0
- package/src/migrations/postgres/0037_vault_bindings.sql +271 -0
- package/src/migrations/postgres/0040_binding_origin.sql +159 -0
- package/src/migrations/postgres/0041_cutover.sql +132 -0
- package/src/migrations/postgres/0042_sign_in_index.sql +7 -0
- package/src/migrations/postgres/0043_issue_revoke_index.sql +4 -0
- package/src/migrations/postgres/0044_use_by_name.sql +705 -0
- package/src/migrations/postgres/0045_decide_first.sql +878 -0
- package/src/migrations/postgres/0046_way_in_uses.sql +1149 -0
- package/src/migrations/postgres/0047_cutover_finish.sql +268 -0
- package/src/migrations/postgres/0048_use_batch.sql +59 -0
- package/src/migrations/postgres/0049_reads.sql +171 -0
- package/src/migrations/postgres/meta/0022_snapshot.json +3831 -0
- package/src/migrations/postgres/meta/0023_snapshot.json +3831 -0
- package/src/migrations/postgres/meta/0024_snapshot.json +3831 -0
- package/src/migrations/postgres/meta/0025_snapshot.json +3831 -0
- package/src/migrations/postgres/meta/0026_snapshot.json +3831 -0
- package/src/migrations/postgres/meta/0027_snapshot.json +3853 -0
- package/src/migrations/postgres/meta/0028_snapshot.json +3853 -0
- package/src/migrations/postgres/meta/0029_snapshot.json +3859 -0
- package/src/migrations/postgres/meta/0030_snapshot.json +3859 -0
- package/src/migrations/postgres/meta/0031_snapshot.json +3859 -0
- package/src/migrations/postgres/meta/0032_snapshot.json +3875 -0
- package/src/migrations/postgres/meta/0033_snapshot.json +3875 -0
- package/src/migrations/postgres/meta/0034_snapshot.json +3875 -0
- package/src/migrations/postgres/meta/0035_snapshot.json +3875 -0
- package/src/migrations/postgres/meta/0036_snapshot.json +3875 -0
- package/src/migrations/postgres/meta/0037_snapshot.json +3875 -0
- package/src/migrations/postgres/meta/0038_snapshot.json +3881 -0
- package/src/migrations/postgres/meta/0039_snapshot.json +3881 -0
- package/src/migrations/postgres/meta/0040_snapshot.json +3931 -0
- package/src/migrations/postgres/meta/0041_snapshot.json +3981 -0
- package/src/migrations/postgres/meta/0042_snapshot.json +3981 -0
- package/src/migrations/postgres/meta/0043_snapshot.json +3981 -0
- package/src/migrations/postgres/meta/0044_snapshot.json +3991 -0
- package/src/migrations/postgres/meta/0045_snapshot.json +3997 -0
- package/src/migrations/postgres/meta/0046_snapshot.json +3997 -0
- package/src/migrations/postgres/meta/0047_snapshot.json +4234 -0
- package/src/migrations/postgres/meta/_journal.json +182 -0
- package/src/migrations/sqlite/0018_log_015.sql +84 -0
- package/src/migrations/sqlite/0019_retention.sql +2 -0
- package/src/migrations/sqlite/0020_ended_references.sql +1 -0
- package/src/migrations/sqlite/0021_secrets_live_index.sql +1 -0
- package/src/migrations/sqlite/0022_binding_origin.sql +1 -0
- package/src/migrations/sqlite/0023_sign_in_index.sql +2 -0
- package/src/migrations/sqlite/0024_issue_revoke_index.sql +2 -0
- package/src/migrations/sqlite/0025_way_in_uses.sql +42 -0
- package/src/migrations/sqlite/0026_cutover_finish.sql +11 -0
- package/src/migrations/sqlite/0027_reads.sql +38 -0
- package/src/migrations/sqlite/meta/0018_snapshot.json +3529 -0
- package/src/migrations/sqlite/meta/0019_snapshot.json +3538 -0
- package/src/migrations/sqlite/meta/0020_snapshot.json +3545 -0
- package/src/migrations/sqlite/meta/0021_snapshot.json +3553 -0
- package/src/migrations/sqlite/meta/0022_snapshot.json +3560 -0
- package/src/migrations/sqlite/meta/0023_snapshot.json +3579 -0
- package/src/migrations/sqlite/meta/0024_snapshot.json +3598 -0
- package/src/migrations/sqlite/meta/0025_snapshot.json +3609 -0
- package/src/migrations/sqlite/meta/0026_snapshot.json +3616 -0
- package/src/migrations/sqlite/meta/0027_snapshot.json +3834 -0
- package/src/migrations/sqlite/meta/_journal.json +70 -0
- package/src/postgres-js/README.md +183 -0
- package/src/postgres-js/UNLICENSE +24 -0
- package/src/postgres-js/bytes.ts +89 -0
- package/src/postgres-js/connection.ts +882 -0
- package/src/postgres-js/errors.ts +84 -0
- package/src/postgres-js/index.ts +516 -0
- package/src/postgres-js/platform.node.ts +29 -0
- package/src/postgres-js/platform.ts +60 -0
- package/src/postgres-js/platform.workerd.ts +225 -0
- package/src/postgres-js/query.ts +123 -0
- package/src/postgres-js/queue.ts +42 -0
- package/src/postgres-js/result.ts +46 -0
- package/src/postgres-js/types.ts +52 -0
- package/src/postgres.ts +375 -26
- package/src/schema-version.ts +2 -2
- package/src/schema.sqlite.ts +198 -7
- package/src/schema.ts +291 -6
- package/src/verify-runtime.ts +18 -29
- package/LICENSE +0 -21
- package/dist/database-wq1AVRAP.d.ts +0 -73
- package/dist/postgres-Bmar8y0q.js +0 -29
- package/dist/relations-BfR3JjT1.js +0 -38
- package/src/relations.ts +0 -40
package/dist/schema.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
|
|
2
|
-
import { n as
|
|
2
|
+
import { n as recorded, r as ACTIVE_SUBJECT } from "./change-records-_fKvahvs.js";
|
|
3
3
|
import { sql } from "drizzle-orm";
|
|
4
|
-
import { bigint, boolean, check, customType, foreignKey, index, integer, pgTable, primaryKey, text, timestamp, unique, uniqueIndex, uuid } from "drizzle-orm/pg-core";
|
|
4
|
+
import { bigint, boolean, check, customType, foreignKey, index, integer, pgTable, primaryKey, smallint, text, timestamp, unique, uniqueIndex, uuid } from "drizzle-orm/pg-core";
|
|
5
5
|
//#region src/schema.ts
|
|
6
6
|
var schema_exports = /* @__PURE__ */ __exportAll({
|
|
7
7
|
auditChainHead: () => auditChainHead,
|
|
@@ -13,8 +13,17 @@ var schema_exports = /* @__PURE__ */ __exportAll({
|
|
|
13
13
|
dismissedKeys: () => dismissedKeys,
|
|
14
14
|
enrollments: () => enrollments,
|
|
15
15
|
environments: () => environments,
|
|
16
|
-
environmentsRelations: () => environmentsRelations,
|
|
17
16
|
identities: () => identities,
|
|
17
|
+
logBulk: () => logBulk,
|
|
18
|
+
logChanges: () => logChanges,
|
|
19
|
+
logCheckpoints: () => logCheckpoints,
|
|
20
|
+
logConfig: () => logConfig,
|
|
21
|
+
logFence: () => logFence,
|
|
22
|
+
logReadDays: () => logReadDays,
|
|
23
|
+
logReads: () => logReads,
|
|
24
|
+
logReadsCursor: () => logReadsCursor,
|
|
25
|
+
logTiles: () => logTiles,
|
|
26
|
+
logUses: () => logUses,
|
|
18
27
|
mcpApprovals: () => mcpApprovals,
|
|
19
28
|
mcpConnections: () => mcpConnections,
|
|
20
29
|
oauthClients: () => oauthClients,
|
|
@@ -22,7 +31,6 @@ var schema_exports = /* @__PURE__ */ __exportAll({
|
|
|
22
31
|
secretReferences: () => secretReferences,
|
|
23
32
|
secretVersions: () => secretVersions,
|
|
24
33
|
secrets: () => secrets,
|
|
25
|
-
secretsRelations: () => secretsRelations,
|
|
26
34
|
serviceBindings: () => serviceBindings,
|
|
27
35
|
vaultGrants: () => vaultGrants,
|
|
28
36
|
vaultMembers: () => vaultMembers,
|
|
@@ -63,8 +71,11 @@ const environments = pgTable("environments", {
|
|
|
63
71
|
createdAt: createdAt(),
|
|
64
72
|
archivedAt: timestamp("archived_at", { withTimezone: true }),
|
|
65
73
|
parentId: uuid("parent_id"),
|
|
66
|
-
owner: text()
|
|
74
|
+
owner: text(),
|
|
75
|
+
protected: boolean(),
|
|
76
|
+
seal: bytea()
|
|
67
77
|
}, (table) => [
|
|
78
|
+
check("environments_seal_check", sql`${table.seal} IS NULL OR octet_length(${table.seal}) = 16`),
|
|
68
79
|
unique("environments_project_id_slug_key").on(table.projectId, table.slug),
|
|
69
80
|
unique("environments_project_scoped").on(table.id, table.projectId),
|
|
70
81
|
check("environments_slug_check", sql`${table.slug} ~ ${PLACE_SLUG}`),
|
|
@@ -93,8 +104,10 @@ const secrets = pgTable("secrets", {
|
|
|
93
104
|
currentVersionId: uuid("current_version_id").references(() => secretVersions.id, { onDelete: "no action" }),
|
|
94
105
|
currentVersion: integer("current_version").notNull().default(0),
|
|
95
106
|
archivedAt: timestamp("archived_at", { withTimezone: true }),
|
|
96
|
-
folder: text()
|
|
107
|
+
folder: text(),
|
|
108
|
+
seal: bytea()
|
|
97
109
|
}, (table) => [
|
|
110
|
+
check("secrets_seal_check", sql`${table.seal} IS NULL OR octet_length(${table.seal}) = 16`),
|
|
98
111
|
unique("secrets_project_id_environment_id_key_key").on(table.projectId, table.environmentId, table.key),
|
|
99
112
|
check("secrets_key_check", sql`${table.key} ~ '^[A-Za-z_][A-Za-z0-9_]{0,127}$'`),
|
|
100
113
|
folderCheck("secrets_folder_check", table.folder),
|
|
@@ -113,7 +126,8 @@ const secrets = pgTable("secrets", {
|
|
|
113
126
|
columns: [table.environmentId, table.projectId],
|
|
114
127
|
foreignColumns: [environments.id, environments.projectId]
|
|
115
128
|
}),
|
|
116
|
-
index("secrets_lookup_idx").on(table.projectId, table.environmentId, table.key)
|
|
129
|
+
index("secrets_lookup_idx").on(table.projectId, table.environmentId, table.key),
|
|
130
|
+
index("secrets_live_idx").on(table.environmentId).where(sql`${table.archivedAt} IS NULL`)
|
|
117
131
|
]);
|
|
118
132
|
const secretVersions = pgTable("secret_versions", {
|
|
119
133
|
id: uuid().primaryKey().defaultRandom(),
|
|
@@ -192,8 +206,10 @@ const vaultMembers = pgTable("vault_members", {
|
|
|
192
206
|
statusChangedAt: bigint("status_changed_at", { mode: "number" }).notNull(),
|
|
193
207
|
statusChangedBy: text("status_changed_by").notNull(),
|
|
194
208
|
accessSeq: bigint("access_seq", { mode: "bigint" }).notNull(),
|
|
195
|
-
mac: bytea().notNull()
|
|
209
|
+
mac: bytea().notNull(),
|
|
210
|
+
seal: bytea()
|
|
196
211
|
}, (table) => [
|
|
212
|
+
check("vault_members_seal_check", sql`${table.seal} IS NULL OR octet_length(${table.seal}) = 16`),
|
|
197
213
|
check("vault_members_mac_check", sql`octet_length(${table.mac}) = 32`),
|
|
198
214
|
check("vault_members_principal_check", sql`${table.principal} ~ '^(user|token):[^[:space:]:][^[:space:]]*$'`),
|
|
199
215
|
check("vault_members_user_lowercase", sql`${table.principal} NOT LIKE 'user:%' OR ${table.principal} = lower(${table.principal})`),
|
|
@@ -283,7 +299,8 @@ const auditLog = pgTable("audit_log", {
|
|
|
283
299
|
metadata: text().notNull().default("{}"),
|
|
284
300
|
prevHash: bytea("prev_hash").notNull(),
|
|
285
301
|
mac: bytea().notNull(),
|
|
286
|
-
hash: bytea().notNull()
|
|
302
|
+
hash: bytea().notNull(),
|
|
303
|
+
log015: boolean("log015")
|
|
287
304
|
}, (table) => [
|
|
288
305
|
check("audit_log_author_check", sql`${table.author} IN ('app', 'vault')`),
|
|
289
306
|
check("audit_log_actor_check", sql`${table.actor} ~ '^(user|token|system):.+$'`),
|
|
@@ -353,7 +370,8 @@ const secretReferences = pgTable("secret_references", {
|
|
|
353
370
|
sourceSecretId: uuid("source_secret_id").notNull(),
|
|
354
371
|
createdSeq: bigint("created_seq", { mode: "bigint" }).notNull(),
|
|
355
372
|
createdAt: createdAt(),
|
|
356
|
-
createdBy: text("created_by").notNull()
|
|
373
|
+
createdBy: text("created_by").notNull(),
|
|
374
|
+
endedAt: timestamp("ended_at", { withTimezone: true })
|
|
357
375
|
}, (table) => [
|
|
358
376
|
unique("secret_references_created_seq_key").on(table.createdSeq),
|
|
359
377
|
foreignKey({
|
|
@@ -441,13 +459,16 @@ const serviceBindings = pgTable("service_bindings", {
|
|
|
441
459
|
claims: text().notNull(),
|
|
442
460
|
scope: text(),
|
|
443
461
|
policy: text(),
|
|
462
|
+
origin: text(),
|
|
444
463
|
label: text(),
|
|
445
464
|
createdAt: createdAt(),
|
|
446
465
|
createdBy: text("created_by").notNull(),
|
|
447
466
|
lastUsedAt: timestamp("last_used_at", { withTimezone: true }),
|
|
448
467
|
revokedAt: timestamp("revoked_at", { withTimezone: true }),
|
|
449
|
-
revokedBy: text("revoked_by")
|
|
468
|
+
revokedBy: text("revoked_by"),
|
|
469
|
+
seal: bytea()
|
|
450
470
|
}, (table) => [
|
|
471
|
+
check("service_bindings_seal_check", sql`${table.seal} IS NULL OR octet_length(${table.seal}) = 16`),
|
|
451
472
|
check("service_bindings_principal_check", sql`${table.principal} LIKE 'token:%'`),
|
|
452
473
|
check("service_bindings_auth_mac_check", sql`octet_length(${table.authMac}) = 32`),
|
|
453
474
|
check("service_bindings_claims_check", sql`${table.claims}::jsonb IS NOT NULL`),
|
|
@@ -527,8 +548,10 @@ const mcpConnections = pgTable("mcp_connections", {
|
|
|
527
548
|
lastUsedAt: timestamp("last_used_at", { withTimezone: true }),
|
|
528
549
|
lastUsedIp: text("last_used_ip"),
|
|
529
550
|
revokedAt: timestamp("revoked_at", { withTimezone: true }),
|
|
530
|
-
revokedBy: text("revoked_by")
|
|
551
|
+
revokedBy: text("revoked_by"),
|
|
552
|
+
seal: bytea()
|
|
531
553
|
}, (table) => [
|
|
554
|
+
check("mcp_connections_seal_check", sql`${table.seal} IS NULL OR octet_length(${table.seal}) = 16`),
|
|
532
555
|
check("mcp_connections_principal_check", sql`${table.principal} LIKE 'user:%'`),
|
|
533
556
|
check("mcp_connections_registration_check", sql`${table.registration} IN ('cimd', 'dcr')`),
|
|
534
557
|
check("mcp_connections_auth_mac_check", sql`octet_length(${table.authMac}) = 32`),
|
|
@@ -639,8 +662,10 @@ const credentials = pgTable("credentials", {
|
|
|
639
662
|
constraints: text(),
|
|
640
663
|
enrollmentId: uuid("enrollment_id"),
|
|
641
664
|
approvedAt: timestamp("approved_at", { withTimezone: true }),
|
|
642
|
-
approvedBy: text("approved_by")
|
|
665
|
+
approvedBy: text("approved_by"),
|
|
666
|
+
seal: bytea()
|
|
643
667
|
}, (table) => [
|
|
668
|
+
check("credentials_seal_check", sql`${table.seal} IS NULL OR octet_length(${table.seal}) = 16`),
|
|
644
669
|
unique("credentials_token_hash_key").on(table.tokenHash),
|
|
645
670
|
check("credentials_kind_check", sql`${table.kind} IN ('browser', 'cli', 'service', 'personal', 'handle', 'workload')`),
|
|
646
671
|
check("credentials_principal_check", sql`${table.principal} LIKE 'user:%' OR ${table.principal} LIKE 'token:%'`),
|
|
@@ -713,11 +738,192 @@ const deviceAuthorizations = pgTable("device_authorizations", {
|
|
|
713
738
|
foreignColumns: [vaultMembers.principal]
|
|
714
739
|
}).onDelete("restrict")
|
|
715
740
|
]);
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
741
|
+
/**
|
|
742
|
+
* 0.15's configuration objects (0022_log_015): decision inputs no other row
|
|
743
|
+
* holds, one row each, sealed as every input is. `policy` holds the bulk
|
|
744
|
+
* cap's count and window, which a use reads in its own statement; `roles`
|
|
745
|
+
* the role tables. JSON, as the vault writes it. Written only through
|
|
746
|
+
* `change_input`.
|
|
747
|
+
*/
|
|
748
|
+
const logConfig = pgTable("log_config", {
|
|
749
|
+
name: text().primaryKey(),
|
|
750
|
+
value: text().notNull(),
|
|
751
|
+
seal: bytea()
|
|
752
|
+
}, (table) => [
|
|
753
|
+
check("log_config_name_check", sql`${table.name} IN ('policy', 'roles')`),
|
|
754
|
+
check("log_config_value_check", sql`${table.value}::jsonb IS NOT NULL`),
|
|
755
|
+
check("log_config_seal_check", sql`${table.seal} IS NULL OR octet_length(${table.seal}) = 16`)
|
|
756
|
+
]);
|
|
757
|
+
/**
|
|
758
|
+
* 0.15's log (0022_log_015, docs/design/hot-path-one.md): two tables numbered
|
|
759
|
+
* from one sequence, `log_number`, each number taken under the fence's lock
|
|
760
|
+
* (a credential's issue: under the issue lock, which the barrier waits for;
|
|
761
|
+
* 0026), so that the number alone orders uses against changes. Nothing reads or
|
|
762
|
+
* writes them before 0.15's vault; the vault login reads them and writes
|
|
763
|
+
* them only through the migration's functions.
|
|
764
|
+
*
|
|
765
|
+
* A change: any write to a decision input, or to configuration, kept for
|
|
766
|
+
* good. `object` is the input it changes (`log_object(kind, id)`), `seal`
|
|
767
|
+
* that object's seal after it, `payload` what it does. The MAC is the
|
|
768
|
+
* vault's, 16 bytes, its key's tag then HMAC-SHA256 cut to 12 (D162), over everything but the number,
|
|
769
|
+
* which the vault does not know when it asks. The vault login reads them,
|
|
770
|
+
* and the app's reads the changes too, for the audit pages: a sign-in
|
|
771
|
+
* through a trust binding, and the vault's token issues, revokes and
|
|
772
|
+
* sign-outs, are recorded there alone (D147, T156, T164).
|
|
773
|
+
*/
|
|
774
|
+
const logChanges = pgTable("log_changes", {
|
|
775
|
+
number: bigint({ mode: "bigint" }).primaryKey(),
|
|
776
|
+
at: bigint({ mode: "number" }).notNull(),
|
|
777
|
+
requestId: text("request_id").notNull(),
|
|
778
|
+
object: uuid().notNull(),
|
|
779
|
+
actor: text().notNull(),
|
|
780
|
+
payload: text().notNull(),
|
|
781
|
+
seal: bytea(),
|
|
782
|
+
mac: bytea().notNull()
|
|
783
|
+
}, (table) => [
|
|
784
|
+
unique("log_changes_request_id_key").on(table.requestId),
|
|
785
|
+
check("log_changes_seal_check", sql`${table.seal} IS NULL OR octet_length(${table.seal}) = 16`),
|
|
786
|
+
check("log_changes_mac_check", sql`octet_length(${table.mac}) = 16`),
|
|
787
|
+
index("log_changes_object_idx").on(table.object, table.number),
|
|
788
|
+
index("log_changes_sign_in_idx").on(table.at, table.number).where(recorded("postgres", table.payload, "signIn")),
|
|
789
|
+
index("log_changes_sign_in_actor_idx").on(table.actor, table.at, table.number).where(recorded("postgres", table.payload, "signIn")),
|
|
790
|
+
index("log_changes_issue_revoke_idx").on(table.at, table.number).where(recorded("postgres", table.payload, "issue", "revoke", "signOut")),
|
|
791
|
+
index("log_changes_issue_revoke_actor_idx").on(table.actor, table.at, table.number).where(recorded("postgres", table.payload, "issue", "revoke", "signOut"))
|
|
792
|
+
]);
|
|
793
|
+
/**
|
|
794
|
+
* A use, the slim row (R53): no principal (the credential's), no
|
|
795
|
+
* environment (the secret's, or `place`), no kind (the table says it).
|
|
796
|
+
* Partitioned by the day of `at` (`make_day`), which retention drops whole;
|
|
797
|
+
* fixed-width columns first, and eight of them, so the row pads to nothing. One index, the
|
|
798
|
+
* replay's: a retried call repeats its `at` and request id, both under its
|
|
799
|
+
* MAC, and lands in the same day.
|
|
800
|
+
*/
|
|
801
|
+
const logUses = pgTable("log_uses", {
|
|
802
|
+
number: bigint({ mode: "bigint" }).notNull(),
|
|
803
|
+
at: bigint({ mode: "number" }).notNull(),
|
|
804
|
+
requestId: bigint("request_id", { mode: "bigint" }).notNull(),
|
|
805
|
+
credential: uuid().notNull(),
|
|
806
|
+
secret: uuid().notNull(),
|
|
807
|
+
purpose: smallint().notNull(),
|
|
808
|
+
mac: bytea().notNull(),
|
|
809
|
+
place: uuid()
|
|
810
|
+
}, (table) => [check("log_uses_mac_check", sql`octet_length(${table.mac}) = 16`), uniqueIndex("log_uses_request_idx").on(table.at, table.requestId)]);
|
|
811
|
+
/**
|
|
812
|
+
* 0.15's reads as the audit pages list them (T166; formal/Reads.tla): a
|
|
813
|
+
* projection of each use a signed pair seals, written by the vault's
|
|
814
|
+
* projector apart from the checkpointer, from the sealed objects at the
|
|
815
|
+
* use's number: its account, its key's place, the fork it was read in,
|
|
816
|
+
* through a reference the source and its place, the version read; under the
|
|
817
|
+
* vault's MAC over all of it, the number included. Partitioned by the day of
|
|
818
|
+
* `at`, as the uses are (`make_reads_day`), and dropped with them.
|
|
819
|
+
*/
|
|
820
|
+
const logReads = pgTable("log_reads", {
|
|
821
|
+
number: bigint({ mode: "bigint" }).notNull(),
|
|
822
|
+
at: bigint({ mode: "number" }).notNull(),
|
|
823
|
+
credential: uuid().notNull(),
|
|
824
|
+
purpose: smallint().notNull(),
|
|
825
|
+
principal: text().notNull(),
|
|
826
|
+
secret: uuid().notNull(),
|
|
827
|
+
environment: uuid().notNull(),
|
|
828
|
+
project: uuid().notNull(),
|
|
829
|
+
place: uuid(),
|
|
830
|
+
source: uuid(),
|
|
831
|
+
sourceEnvironment: uuid("source_environment"),
|
|
832
|
+
sourceProject: uuid("source_project"),
|
|
833
|
+
version: uuid(),
|
|
834
|
+
mac: bytea().notNull()
|
|
835
|
+
}, (table) => [
|
|
836
|
+
check("log_reads_mac_check", sql`octet_length(${table.mac}) = 16`),
|
|
837
|
+
uniqueIndex("log_reads_number_idx").on(table.at, table.number),
|
|
838
|
+
index("log_reads_principal_idx").on(table.principal, table.at, table.number)
|
|
839
|
+
]);
|
|
840
|
+
/** The days each member's reads are in (`log_reads`), so that a page of theirs reads those days alone: the projector writes them with the reads. */
|
|
841
|
+
const logReadDays = pgTable("log_read_days", {
|
|
842
|
+
principal: text().notNull(),
|
|
843
|
+
day: integer().notNull()
|
|
844
|
+
}, (table) => [primaryKey({
|
|
845
|
+
name: "log_read_days_pkey",
|
|
846
|
+
columns: [table.principal, table.day]
|
|
847
|
+
})]);
|
|
848
|
+
/** How far the projector has gone: the number every use at or below it is projected, and when. */
|
|
849
|
+
const logReadsCursor = pgTable("log_reads_cursor", {
|
|
850
|
+
onlyRow: boolean("only_row").primaryKey().default(true),
|
|
851
|
+
through: bigint({ mode: "bigint" }).notNull(),
|
|
852
|
+
pair: bigint({ mode: "bigint" }).notNull(),
|
|
853
|
+
at: bigint({ mode: "number" }).notNull(),
|
|
854
|
+
mac: bytea()
|
|
855
|
+
}, (table) => [check("log_reads_cursor_only_row_check", sql`${table.onlyRow}`), check("log_reads_cursor_mac_check", sql`${table.mac} IS NULL OR octet_length(${table.mac}) = 16`)]);
|
|
856
|
+
/**
|
|
857
|
+
* The bulk cap (R28, R52): each principal's last logged use of each key, as
|
|
858
|
+
* the start of that use's refresh period, written only by a use that holds
|
|
859
|
+
* the principal's lock (exclusive to enter a key, shared to move it on:
|
|
860
|
+
* 0028), at most once per refresh per key. A row older than the window, a
|
|
861
|
+
* refresh, the deadline and a minute counts for nothing; `prune_bulk`
|
|
862
|
+
* forgets it an hour after that.
|
|
863
|
+
*/
|
|
864
|
+
const logBulk = pgTable("log_bulk", {
|
|
865
|
+
principal: text().notNull(),
|
|
866
|
+
secret: uuid().notNull(),
|
|
867
|
+
last: bigint({ mode: "number" }).notNull()
|
|
868
|
+
}, (table) => [primaryKey({
|
|
869
|
+
name: "log_bulk_pkey",
|
|
870
|
+
columns: [table.principal, table.secret]
|
|
871
|
+
})]);
|
|
872
|
+
/**
|
|
873
|
+
* The fence's one row: the lowest `at` a use may still be written with,
|
|
874
|
+
* raised by each checkpoint's barrier (`close_one`), never lowered. Made
|
|
875
|
+
* by the migration; without it every use is refused.
|
|
876
|
+
*/
|
|
877
|
+
const logFence = pgTable("log_fence", {
|
|
878
|
+
onlyRow: boolean("only_row").primaryKey().default(true),
|
|
879
|
+
atFloor: bigint("at_floor", { mode: "number" }).notNull()
|
|
880
|
+
}, (table) => [check("log_fence_only_row_check", sql`${table.onlyRow}`)]);
|
|
881
|
+
/**
|
|
882
|
+
* A signed checkpoint: the pair of tree heads (W212), changes and uses, at
|
|
883
|
+
* one `upto`, every number at or below which is final. One row, so that no
|
|
884
|
+
* state holds one head without the other. `at_floor` is the floor its
|
|
885
|
+
* barrier set: the next window's uses have an `at` at or above it.
|
|
886
|
+
*/
|
|
887
|
+
const logCheckpoints = pgTable("log_checkpoints", {
|
|
888
|
+
upto: bigint({ mode: "bigint" }).primaryKey(),
|
|
889
|
+
at: bigint({ mode: "number" }).notNull(),
|
|
890
|
+
atFloor: bigint("at_floor", { mode: "number" }).notNull(),
|
|
891
|
+
changesSize: bigint("changes_size", { mode: "bigint" }).notNull(),
|
|
892
|
+
changesRoot: bytea("changes_root").notNull(),
|
|
893
|
+
changesNote: text("changes_note").notNull(),
|
|
894
|
+
usesSize: bigint("uses_size", { mode: "bigint" }).notNull(),
|
|
895
|
+
usesRoot: bytea("uses_root").notNull(),
|
|
896
|
+
usesNote: text("uses_note").notNull()
|
|
897
|
+
}, (table) => [
|
|
898
|
+
check("log_checkpoints_changes_root_check", sql`octet_length(${table.changesRoot}) = 32`),
|
|
899
|
+
check("log_checkpoints_uses_root_check", sql`octet_length(${table.usesRoot}) = 32`),
|
|
900
|
+
index("log_checkpoints_floor_idx").on(table.atFloor, table.upto)
|
|
901
|
+
]);
|
|
902
|
+
/**
|
|
903
|
+
* The two trees' tiles (C2SP tlog-tiles: height 8, up to 256 hashes of one
|
|
904
|
+
* level). Partitioned by tree; the uses tree's leaf tiles (level 0) by
|
|
905
|
+
* `leaf`, the first leaf a tile covers (`make_tiles`), so that retention
|
|
906
|
+
* drops a table rather than deleting a day's rows (W212); its levels above,
|
|
907
|
+
* which span far past their first leaf, in one table (W217).
|
|
908
|
+
*/
|
|
909
|
+
const logTiles = pgTable("log_tiles", {
|
|
910
|
+
tree: text().notNull(),
|
|
911
|
+
level: smallint().notNull(),
|
|
912
|
+
leaf: bigint({ mode: "bigint" }).notNull(),
|
|
913
|
+
hashes: bytea().notNull()
|
|
914
|
+
}, (table) => [
|
|
915
|
+
primaryKey({
|
|
916
|
+
name: "log_tiles_pkey",
|
|
917
|
+
columns: [
|
|
918
|
+
table.tree,
|
|
919
|
+
table.level,
|
|
920
|
+
table.leaf
|
|
921
|
+
]
|
|
922
|
+
}),
|
|
923
|
+
check("log_tiles_tree_check", sql`${table.tree} IN ('changes', 'uses')`),
|
|
924
|
+
check("log_tiles_level_check", sql`${table.level} BETWEEN 0 AND 7`),
|
|
925
|
+
check("log_tiles_leaf_check", sql`${table.leaf} >= 0 AND ${table.leaf} % (256::numeric ^ (${table.level} + 1)) = 0`),
|
|
926
|
+
check("log_tiles_hashes_check", sql`octet_length(${table.hashes}) BETWEEN 32 AND 8192 AND octet_length(${table.hashes}) % 32 = 0`)
|
|
927
|
+
]);
|
|
722
928
|
//#endregion
|
|
723
|
-
export { auditChainHead, auditLog, cappedAlerts, consumedTokens, credentials, deviceAuthorizations, dismissedKeys, enrollments, environments,
|
|
929
|
+
export { auditChainHead, auditLog, cappedAlerts, consumedTokens, credentials, deviceAuthorizations, dismissedKeys, enrollments, environments, identities, logBulk, logChanges, logCheckpoints, logConfig, logFence, logReadDays, logReads, logReadsCursor, logTiles, logUses, mcpApprovals, mcpConnections, oauthClients, projects, secretReferences, secretVersions, secrets, serviceBindings, schema_exports as t, vaultGrants, vaultMembers, vaultReleases };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coffre/db",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
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,16 +74,24 @@
|
|
|
74
74
|
},
|
|
75
75
|
"./package.json": "./package.json"
|
|
76
76
|
},
|
|
77
|
+
"imports": {
|
|
78
|
+
"#postgres/platform": {
|
|
79
|
+
"coffre:source": {
|
|
80
|
+
"workerd": "./src/postgres-js/platform.workerd.ts",
|
|
81
|
+
"default": "./src/postgres-js/platform.node.ts"
|
|
82
|
+
},
|
|
83
|
+
"types": "./dist/postgres-platform-node.d.ts",
|
|
84
|
+
"workerd": "./dist/postgres-platform-workerd.js",
|
|
85
|
+
"default": "./dist/postgres-platform-node.js"
|
|
86
|
+
}
|
|
87
|
+
},
|
|
77
88
|
"dependencies": {
|
|
78
89
|
"@libsql/client": "0.18.0",
|
|
79
90
|
"drizzle-orm": "0.45.2",
|
|
80
|
-
"
|
|
81
|
-
"pg-connection-string": "2.14.0",
|
|
82
|
-
"@coffre/core": "0.14.5"
|
|
91
|
+
"@coffre/core": "0.15.0"
|
|
83
92
|
},
|
|
84
93
|
"devDependencies": {
|
|
85
94
|
"@types/node": "26.1.1",
|
|
86
|
-
"@types/pg": "8.20.0",
|
|
87
95
|
"drizzle-kit": "0.31.10",
|
|
88
96
|
"tsdown": "0.23.0",
|
|
89
97
|
"typescript": "5.9.3"
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { sql } from 'drizzle-orm';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `identities.active_subject`: the subject while the account is bound, null
|
|
5
|
+
* once revoked. A unique index on (provider, issuer_hash, active_subject)
|
|
6
|
+
* binds an account to one person at a time. Plain column names, so the
|
|
7
|
+
* expression reads the same on both databases.
|
|
8
|
+
*/
|
|
9
|
+
export const ACTIVE_SUBJECT = sql`CASE WHEN revoked_at IS NULL THEN subject END`;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { sql, type SQL, type SQLWrapper } from 'drizzle-orm';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* What a change of 0.15's log records besides its op, which the audit pages
|
|
5
|
+
* list where 0.14's entry stood (D147): a sign-in through a trust binding
|
|
6
|
+
* (`signIn`, T156); a token the vault issued (`issue`); a credential it
|
|
7
|
+
* revoked by request, or that signed itself out (a revoke's `reason`,
|
|
8
|
+
* T164). Each is known by text its payload holds (`holds`), and by how the
|
|
9
|
+
* payload starts (`starts`): the vault writes it canonical, its keys in
|
|
10
|
+
* order, so an issue's is `{"caller":…` and a revoke's
|
|
11
|
+
* `{"op":"credential.revoke",…`. A string the payload holds has its quotes
|
|
12
|
+
* escaped, but an object's keys need not be the vault's own (a role's
|
|
13
|
+
* name, in the roles' change: review r1), so where the text alone could be
|
|
14
|
+
* one, the start says whose payload it is. The partial indexes of
|
|
15
|
+
* `log_changes` hold each set, and a read that is to use one repeats its
|
|
16
|
+
* condition (`recorded`), word for word and with no parameter.
|
|
17
|
+
*/
|
|
18
|
+
export const RECORDS = {
|
|
19
|
+
signIn: { holds: '"signIn":{' },
|
|
20
|
+
issue: { starts: '{"caller":"', holds: '"issue":{"by":"' },
|
|
21
|
+
revoke: { starts: '{"op":"credential.revoke",', holds: '"reason":"revoke"' },
|
|
22
|
+
signOut: { starts: '{"op":"credential.revoke",', holds: '"reason":"sign_out"' },
|
|
23
|
+
} as const satisfies Record<string, { starts?: string; holds: string }>;
|
|
24
|
+
|
|
25
|
+
export type ChangeRecord = keyof typeof RECORDS;
|
|
26
|
+
|
|
27
|
+
/** Whether `payload` holds one of `records`, on Postgres or SQLite: an index's condition, and the reads' that use it. */
|
|
28
|
+
export function recorded(engine: 'postgres' | 'sqlite', payload: SQLWrapper, ...records: ChangeRecord[]): SQL {
|
|
29
|
+
const at = (text: string) => {
|
|
30
|
+
const marker = sql.raw(`'${text}'`);
|
|
31
|
+
return engine === 'postgres' ? sql`strpos(${payload}, ${marker})` : sql`instr(${payload}, ${marker})`;
|
|
32
|
+
};
|
|
33
|
+
const each = records.map((record) => {
|
|
34
|
+
const { starts, holds }: { starts?: string; holds: string } = RECORDS[record];
|
|
35
|
+
return starts === undefined ? sql`${at(holds)} > 0` : sql`(${at(starts)} = 1 AND ${at(holds)} > 0)`;
|
|
36
|
+
});
|
|
37
|
+
return each.length === 1 ? each[0]! : sql`(${sql.join(each, sql` OR `)})`;
|
|
38
|
+
}
|
package/src/connect.ts
CHANGED
|
@@ -1,30 +1,30 @@
|
|
|
1
1
|
import { resolve } from 'node:path';
|
|
2
2
|
|
|
3
|
-
import type { Client, ResultSet, Transaction as LibsqlTransaction, TransactionMode } from '@libsql/client';
|
|
4
|
-
import pg from 'pg';
|
|
5
|
-
|
|
3
|
+
import type { Client, InStatement, ResultSet, Transaction as LibsqlTransaction, TransactionMode } from '@libsql/client';
|
|
6
4
|
import { createDatabase, type Database } from './database.ts';
|
|
7
5
|
import type { Engine } from './dialect.ts';
|
|
8
6
|
import { asPostgresDatabase } from './portable.ts';
|
|
9
|
-
import {
|
|
7
|
+
import { postgresClient } from './postgres.ts';
|
|
10
8
|
|
|
11
|
-
export {
|
|
9
|
+
export { postgresClient, postgresOptions, postgresSession, quoteIdentifier, quoteLiteral, SessionLost, type ClientOptions, type PostgresClient } from './postgres.ts';
|
|
12
10
|
|
|
13
11
|
/**
|
|
14
12
|
* Open a database from its URL, on a Node server:
|
|
15
13
|
*
|
|
16
|
-
* postgres://… or postgresql://…
|
|
14
|
+
* postgres://… or postgresql://… postgres.js, a pool of up to 10 connections
|
|
17
15
|
* file:… or libsql:… @libsql/client (SQLite)
|
|
18
16
|
*
|
|
19
17
|
* The SQLite driver loads only when asked for. The Worker does not
|
|
20
18
|
* come through here; it builds its Postgres database from the Hyperdrive
|
|
21
|
-
* pool with createDatabase.
|
|
19
|
+
* pool with createDatabase. `max`, for a pool of its own beside another,
|
|
20
|
+
* as the vault's checkpointer has (T169).
|
|
22
21
|
*/
|
|
23
|
-
export async function openDatabase(url: string): Promise<OpenDatabase> {
|
|
22
|
+
export async function openDatabase(url: string, options: { max?: number } = {}): Promise<OpenDatabase> {
|
|
24
23
|
switch (engineOfUrl(url)) {
|
|
25
24
|
case 'postgres': {
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
// As node-postgres's pool was: 10 connections, each closed after 10 s idle.
|
|
26
|
+
const client = postgresClient(url, { max: options.max ?? 10, idle_timeout: 10 });
|
|
27
|
+
return { engine: 'postgres', db: createDatabase(client), close: () => client.end(), cut: () => client.end({ timeout: 0 }) };
|
|
28
28
|
}
|
|
29
29
|
case 'sqlite': {
|
|
30
30
|
const [{ createClient }, { drizzle }, schema] = await Promise.all([
|
|
@@ -36,12 +36,13 @@ export async function openDatabase(url: string): Promise<OpenDatabase> {
|
|
|
36
36
|
const client = createClient({ url, timeout: 5_000, intMode: 'bigint' });
|
|
37
37
|
if (url.startsWith('file:')) await client.execute('PRAGMA journal_mode = WAL');
|
|
38
38
|
const db = drizzle({ client: oneTransactionAtATime(client, writeQueueFor(url)), schema });
|
|
39
|
-
return { engine: 'sqlite', db: asPostgresDatabase(db), close: async () => client.close() };
|
|
39
|
+
return { engine: 'sqlite', db: asPostgresDatabase(db), close: async () => client.close(), cut: async () => client.close() };
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
/** `close` once what was asked has answered; `cut` at once, what is in flight failing (T169). */
|
|
45
|
+
export type OpenDatabase = { engine: Engine; db: Database; close: () => Promise<void>; cut: () => Promise<void> };
|
|
45
46
|
|
|
46
47
|
export function engineOfUrl(url: string): Engine {
|
|
47
48
|
const scheme = url.slice(0, url.indexOf(':'));
|
|
@@ -61,6 +62,16 @@ export function engineOfUrl(url: string): Engine {
|
|
|
61
62
|
* connections reach it. Each begins IMMEDIATE (libsql's default), taking the
|
|
62
63
|
* write lock with its first statement, which is what makes a read inside it
|
|
63
64
|
* as good as a locked one (see forUpdate).
|
|
65
|
+
*
|
|
66
|
+
* A lone statement that writes waits its turn too. It cannot wait for the
|
|
67
|
+
* lock itself: libsql's busy wait blocks the thread, so the transaction
|
|
68
|
+
* holding the lock, in this process, cannot go on to commit, and the write
|
|
69
|
+
* fails after `timeout`. A connection whose write failed that way still
|
|
70
|
+
* reads the file as it was then, until its statement is collected:
|
|
71
|
+
*
|
|
72
|
+
* six MCP calls at once the first's approval, in a transaction BEGIN IMMEDIATE, then awaits
|
|
73
|
+
* another's last_used_at a lone UPDATE, outside the queue the thread blocked 5 s, SQLITE_BUSY
|
|
74
|
+
* it reads the approval on that connection no row
|
|
64
75
|
*/
|
|
65
76
|
class WriteQueue {
|
|
66
77
|
#tail: Promise<void> = Promise.resolve();
|
|
@@ -89,7 +100,17 @@ function oneTransactionAtATime(client: Client, queue: WriteQueue): Client {
|
|
|
89
100
|
return new Proxy(client, {
|
|
90
101
|
get(target, key) {
|
|
91
102
|
if (key === 'execute') {
|
|
92
|
-
return async (...args: Parameters<Client['execute']>) =>
|
|
103
|
+
return async (...args: Parameters<Client['execute']>) => {
|
|
104
|
+
// Parameters<> sees execute's last overload only, (sql, args); Drizzle calls the other, ({ sql, args }).
|
|
105
|
+
const [statement] = args as unknown as [InStatement];
|
|
106
|
+
if (!writes(typeof statement === 'string' ? statement : statement.sql)) return numbers(await target.execute(...args));
|
|
107
|
+
const release = await queue.acquire();
|
|
108
|
+
try {
|
|
109
|
+
return numbers(await target.execute(...args));
|
|
110
|
+
} finally {
|
|
111
|
+
release();
|
|
112
|
+
}
|
|
113
|
+
};
|
|
93
114
|
}
|
|
94
115
|
if (key === 'transaction') {
|
|
95
116
|
return async (mode?: TransactionMode) => {
|
|
@@ -154,6 +175,39 @@ function settling(tx: LibsqlTransaction, release: () => void): LibsqlTransaction
|
|
|
154
175
|
return tx;
|
|
155
176
|
}
|
|
156
177
|
|
|
178
|
+
/**
|
|
179
|
+
* Whether a lone statement may write: all but a query (SELECT or VALUES, or
|
|
180
|
+
* a WITH whose statement is one) and EXPLAIN, read from its words outside
|
|
181
|
+
* parentheses, strings, quoted names and comments.
|
|
182
|
+
*
|
|
183
|
+
* select … from "mcp_approvals" where … reads
|
|
184
|
+
* WITH policy AS (SELECT …) SELECT json_object(…) reads
|
|
185
|
+
* WITH old AS (SELECT …) DELETE FROM … writes
|
|
186
|
+
* update "mcp_connections" set … writes
|
|
187
|
+
*/
|
|
188
|
+
export function writes(statement: string): boolean {
|
|
189
|
+
let depth = 0;
|
|
190
|
+
let first: string | null = null;
|
|
191
|
+
for (const [token] of statement.matchAll(/--[^\n]*|\/\*[\s\S]*?(?:\*\/|$)|'(?:[^']|'')*'?|"(?:[^"]|"")*"?|`[^`]*`?|\[[^\]]*\]?|[()]|[A-Za-z_][\w$]*/g)) {
|
|
192
|
+
if (token === '(') depth += 1;
|
|
193
|
+
else if (token === ')') depth -= 1;
|
|
194
|
+
else if (depth === 0 && /^[A-Za-z_]/.test(token)) {
|
|
195
|
+
const word = token.toLowerCase();
|
|
196
|
+
if (first === null) {
|
|
197
|
+
first = word;
|
|
198
|
+
if (word !== 'with') return !QUERIES.has(word);
|
|
199
|
+
} else if (STATEMENTS.has(word)) {
|
|
200
|
+
return !QUERIES.has(word);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const QUERIES = new Set(['select', 'values', 'explain']);
|
|
208
|
+
/** The words a WITH's own statement starts with. */
|
|
209
|
+
const STATEMENTS = new Set(['select', 'values', 'insert', 'update', 'delete', 'replace']);
|
|
210
|
+
|
|
157
211
|
/** Keep large counters exact; Drizzle's booleans and dates still need safe numbers. */
|
|
158
212
|
function numbers(result: ResultSet): ResultSet {
|
|
159
213
|
result.rows = result.rows.map((row) => {
|