@coffre/db 0.0.0-stage → 0.1.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.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +14 -2
  3. package/dist/commits-BuW-NL3n.js +30 -0
  4. package/dist/connect.d.ts +21 -0
  5. package/dist/connect.js +155 -0
  6. package/dist/database-CWpo5aF0.d.ts +39 -0
  7. package/dist/dialect.d.ts +88 -0
  8. package/dist/dialect.js +128 -0
  9. package/dist/hyperdrive.d.ts +32 -0
  10. package/dist/hyperdrive.js +53 -0
  11. package/dist/index.d.ts +3 -0
  12. package/dist/index.js +34 -0
  13. package/dist/log.d.ts +72 -0
  14. package/dist/log.js +133 -0
  15. package/dist/migrate.d.ts +10 -0
  16. package/dist/migrate.js +129 -0
  17. package/dist/migrations/postgres/0000_baseline.sql +502 -0
  18. package/dist/migrations/postgres/meta/0000_snapshot.json +1939 -0
  19. package/dist/migrations/postgres/meta/_journal.json +13 -0
  20. package/dist/migrations/sqlite/0000_baseline.sql +293 -0
  21. package/dist/migrations/sqlite/meta/0000_snapshot.json +1811 -0
  22. package/dist/migrations/sqlite/meta/_journal.json +13 -0
  23. package/dist/portable.d.ts +33 -0
  24. package/dist/portable.js +12 -0
  25. package/dist/relations-DqgxtlKu.js +58 -0
  26. package/dist/rolldown-runtime-D7D4PA-g.js +13 -0
  27. package/dist/schema-Cd4WOnB5.d.ts +2503 -0
  28. package/dist/schema-sqlite.d.ts +2587 -0
  29. package/dist/schema-sqlite.js +401 -0
  30. package/dist/schema-version.d.ts +11 -0
  31. package/dist/schema-version.js +16 -0
  32. package/dist/schema.d.ts +2 -0
  33. package/dist/schema.js +448 -0
  34. package/package.json +91 -3
  35. package/src/baseline/postgres.sql +238 -0
  36. package/src/baseline/sqlite.sql +27 -0
  37. package/src/baseline.ts +104 -0
  38. package/src/commits.ts +47 -0
  39. package/src/connect.ts +168 -0
  40. package/src/database.ts +63 -0
  41. package/src/dialect.ts +174 -0
  42. package/src/drizzle.postgres.config.ts +7 -0
  43. package/src/drizzle.sqlite.config.ts +7 -0
  44. package/src/hyperdrive.ts +62 -0
  45. package/src/log.ts +177 -0
  46. package/src/migrate-main.ts +8 -0
  47. package/src/migrate.ts +206 -0
  48. package/src/migrations/postgres/0000_baseline.sql +502 -0
  49. package/src/migrations/postgres/meta/0000_snapshot.json +1939 -0
  50. package/src/migrations/postgres/meta/_journal.json +13 -0
  51. package/src/migrations/sqlite/0000_baseline.sql +293 -0
  52. package/src/migrations/sqlite/meta/0000_snapshot.json +1811 -0
  53. package/src/migrations/sqlite/meta/_journal.json +13 -0
  54. package/src/portable.ts +54 -0
  55. package/src/relations.ts +53 -0
  56. package/src/schema-version.ts +13 -0
  57. package/src/schema.sqlite.ts +511 -0
  58. package/src/schema.ts +548 -0
  59. package/src/verify-runtime.ts +120 -0
package/dist/schema.js ADDED
@@ -0,0 +1,448 @@
1
+ import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
+ import { n as relationsOf, t as ACTIVE_SUBJECT } from "./relations-DqgxtlKu.js";
3
+ import { sql } from "drizzle-orm";
4
+ import { ROLE_NAMES, assignableToEnvironment } from "@coffre/core/access";
5
+ import { bigint, boolean, check, customType, foreignKey, index, integer, pgTable, primaryKey, text, timestamp, unique, uniqueIndex, uuid } from "drizzle-orm/pg-core";
6
+ //#region src/schema.ts
7
+ var schema_exports = /* @__PURE__ */ __exportAll({
8
+ auditChainHead: () => auditChainHead,
9
+ auditLog: () => auditLog,
10
+ credentials: () => credentials,
11
+ deviceAuthorizations: () => deviceAuthorizations,
12
+ environments: () => environments,
13
+ environmentsRelations: () => environmentsRelations,
14
+ identities: () => identities,
15
+ projects: () => projects,
16
+ secretVersions: () => secretVersions,
17
+ secrets: () => secrets,
18
+ secretsRelations: () => secretsRelations,
19
+ syncKeys: () => syncKeys,
20
+ syncKeysRelations: () => syncKeysRelations,
21
+ syncs: () => syncs,
22
+ syncsRelations: () => syncsRelations,
23
+ vaultGrants: () => vaultGrants,
24
+ vaultMembers: () => vaultMembers
25
+ });
26
+ const bytea = customType({ dataType: () => "bytea" });
27
+ const createdAt = () => timestamp("created_at", { withTimezone: true }).notNull().defaultNow();
28
+ const projects = pgTable("projects", {
29
+ id: uuid().primaryKey().defaultRandom(),
30
+ slug: text().notNull(),
31
+ name: text().notNull(),
32
+ createdAt: createdAt(),
33
+ archivedAt: timestamp("archived_at", { withTimezone: true })
34
+ }, (table) => [unique("projects_slug_key").on(table.slug), check("projects_slug_check", sql`${table.slug} ~ '^[a-z0-9][a-z0-9-]{0,62}$'`)]);
35
+ const environments = pgTable("environments", {
36
+ id: uuid().primaryKey().defaultRandom(),
37
+ projectId: uuid("project_id").notNull(),
38
+ slug: text().notNull(),
39
+ name: text().notNull(),
40
+ createdAt: createdAt(),
41
+ archivedAt: timestamp("archived_at", { withTimezone: true })
42
+ }, (table) => [
43
+ unique("environments_project_id_slug_key").on(table.projectId, table.slug),
44
+ unique("environments_project_scoped").on(table.id, table.projectId),
45
+ check("environments_slug_check", sql`${table.slug} ~ '^[a-z0-9][a-z0-9-]{0,62}$'`),
46
+ foreignKey({
47
+ name: "environments_project_id_fkey",
48
+ columns: [table.projectId],
49
+ foreignColumns: [projects.id]
50
+ }).onDelete("restrict")
51
+ ]);
52
+ const secrets = pgTable("secrets", {
53
+ id: uuid().primaryKey().defaultRandom(),
54
+ projectId: uuid("project_id").notNull(),
55
+ environmentId: uuid("environment_id").notNull(),
56
+ key: text().notNull(),
57
+ createdAt: createdAt(),
58
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
59
+ currentVersionId: uuid("current_version_id").references(() => secretVersions.id, { onDelete: "no action" }),
60
+ currentVersion: integer("current_version").notNull().default(0),
61
+ archivedAt: timestamp("archived_at", { withTimezone: true })
62
+ }, (table) => [
63
+ unique("secrets_project_id_environment_id_key_key").on(table.projectId, table.environmentId, table.key),
64
+ check("secrets_key_check", sql`${table.key} ~ '^[A-Za-z_][A-Za-z0-9_]{0,127}$'`),
65
+ foreignKey({
66
+ name: "secrets_project_id_fkey",
67
+ columns: [table.projectId],
68
+ foreignColumns: [projects.id]
69
+ }).onDelete("restrict"),
70
+ foreignKey({
71
+ name: "secrets_environment_id_fkey",
72
+ columns: [table.environmentId],
73
+ foreignColumns: [environments.id]
74
+ }).onDelete("restrict"),
75
+ foreignKey({
76
+ name: "secrets_environment_in_project",
77
+ columns: [table.environmentId, table.projectId],
78
+ foreignColumns: [environments.id, environments.projectId]
79
+ }),
80
+ index("secrets_lookup_idx").on(table.projectId, table.environmentId, table.key)
81
+ ]);
82
+ const secretVersions = pgTable("secret_versions", {
83
+ id: uuid().primaryKey().defaultRandom(),
84
+ secretId: uuid("secret_id").notNull(),
85
+ version: integer().notNull(),
86
+ envelopeVersion: integer("envelope_version").notNull(),
87
+ ciphertext: bytea().notNull(),
88
+ iv: bytea().notNull(),
89
+ authTag: bytea("auth_tag").notNull(),
90
+ wrappedDek: bytea("wrapped_dek").notNull(),
91
+ kekProvider: text("kek_provider").notNull(),
92
+ kekId: text("kek_id").notNull(),
93
+ kekVersion: text("kek_version").notNull(),
94
+ createdAt: createdAt(),
95
+ createdBy: text("created_by").notNull()
96
+ }, (table) => [
97
+ unique("secret_versions_secret_id_version_key").on(table.secretId, table.version),
98
+ check("secret_versions_version_check", sql`${table.version} > 0`),
99
+ check("secret_versions_iv_check", sql`octet_length(${table.iv}) = 12`),
100
+ check("secret_versions_auth_tag_check", sql`octet_length(${table.authTag}) = 16`),
101
+ foreignKey({
102
+ name: "secret_versions_secret_id_fkey",
103
+ columns: [table.secretId],
104
+ foreignColumns: [secrets.id]
105
+ }).onDelete("restrict"),
106
+ index("secret_versions_secret_idx").on(table.secretId, table.version.desc())
107
+ ]);
108
+ /** The role catalogue, as SQL: every role, and those an environment may be granted. */
109
+ const ROLES = sql.raw(ROLE_NAMES.map((role) => `'${role}'`).join(", "));
110
+ const ENVIRONMENT_ROLES = sql.raw(ROLE_NAMES.filter(assignableToEnvironment).map((role) => `'${role}'`).join(", "));
111
+ /**
112
+ * The vault's member directory: everyone it has admitted, written by the
113
+ * vault alone, read by both. A principal with no row is no member.
114
+ * Principals are the strings coffre uses at its edges: `user:<email>`,
115
+ * `token:<id>`, `sync:<id>`. Times are milliseconds since the epoch, each
116
+ * the time of the vault's log entry that set it, so the log replays to
117
+ * these rows exactly.
118
+ */
119
+ const vaultMembers = pgTable("vault_members", {
120
+ principal: text().primaryKey(),
121
+ status: text().notNull(),
122
+ owner: boolean().notNull().default(false),
123
+ generation: integer().notNull().default(0),
124
+ createdAt: bigint("created_at", { mode: "number" }).notNull(),
125
+ createdBy: text("created_by").notNull(),
126
+ statusChangedAt: bigint("status_changed_at", { mode: "number" }).notNull(),
127
+ statusChangedBy: text("status_changed_by").notNull(),
128
+ accessSeq: bigint("access_seq", { mode: "bigint" }).notNull(),
129
+ mac: bytea().notNull()
130
+ }, (table) => [
131
+ check("vault_members_mac_check", sql`octet_length(${table.mac}) = 32`),
132
+ check("vault_members_principal_check", sql`${table.principal} ~ '^(user|token|sync):[^[:space:]:][^[:space:]]*$'`),
133
+ check("vault_members_user_lowercase", sql`${table.principal} NOT LIKE 'user:%' OR ${table.principal} = lower(${table.principal})`),
134
+ check("vault_members_status_check", sql`${table.status} IN ('active', 'removed')`),
135
+ check("vault_members_owner_check", sql`NOT ${table.owner} OR (${table.status} = 'active' AND ${table.principal} LIKE 'user:%')`),
136
+ check("vault_members_generation_check", sql`${table.generation} >= 0`)
137
+ ]);
138
+ /**
139
+ * One role per member per place: a project, or one of its environments,
140
+ * never both. A revoked grant is deleted; an expired one stays until its
141
+ * place is granted again, so the members page can say it lapsed.
142
+ */
143
+ const vaultGrants = pgTable("vault_grants", {
144
+ principal: text().notNull(),
145
+ projectId: uuid("project_id"),
146
+ environmentId: uuid("environment_id"),
147
+ role: text().notNull(),
148
+ expiresAt: bigint("expires_at", { mode: "number" }),
149
+ grantedAt: bigint("granted_at", { mode: "number" }).notNull(),
150
+ grantedBy: text("granted_by").notNull()
151
+ }, (table) => [
152
+ check("vault_grants_one_place", sql`(${table.projectId} IS NULL) <> (${table.environmentId} IS NULL)`),
153
+ check("vault_grants_role_check", sql`${table.role} IN (${ROLES})`),
154
+ check("vault_grants_environment_role_check", sql`${table.environmentId} IS NULL OR ${table.role} IN (${ENVIRONMENT_ROLES})`),
155
+ unique("vault_grants_on_project").on(table.principal, table.projectId),
156
+ unique("vault_grants_on_environment").on(table.principal, table.environmentId),
157
+ foreignKey({
158
+ name: "vault_grants_principal_fkey",
159
+ columns: [table.principal],
160
+ foreignColumns: [vaultMembers.principal]
161
+ }).onDelete("restrict"),
162
+ foreignKey({
163
+ name: "vault_grants_project_id_fkey",
164
+ columns: [table.projectId],
165
+ foreignColumns: [projects.id]
166
+ }).onDelete("restrict"),
167
+ foreignKey({
168
+ name: "vault_grants_environment_id_fkey",
169
+ columns: [table.environmentId],
170
+ foreignColumns: [environments.id]
171
+ }).onDelete("restrict")
172
+ ]);
173
+ const auditLog = pgTable("audit_log", {
174
+ seq: bigint({ mode: "bigint" }).primaryKey(),
175
+ author: text().notNull(),
176
+ keyId: text("key_id").notNull(),
177
+ occurredAt: bigint("occurred_at", { mode: "number" }).notNull(),
178
+ actor: text().notNull(),
179
+ action: text().notNull(),
180
+ decision: text().notNull(),
181
+ code: text(),
182
+ subjectPrincipal: text("subject_principal"),
183
+ projectId: uuid("project_id"),
184
+ environmentId: uuid("environment_id"),
185
+ secretId: uuid("secret_id"),
186
+ secretVersionId: uuid("secret_version_id"),
187
+ operationId: uuid("operation_id"),
188
+ requestId: text("request_id"),
189
+ sourceIp: text("source_ip"),
190
+ relatedSeq: bigint("related_seq", { mode: "bigint" }),
191
+ metadata: text().notNull().default("{}"),
192
+ prevHash: bytea("prev_hash").notNull(),
193
+ mac: bytea().notNull(),
194
+ hash: bytea().notNull()
195
+ }, (table) => [
196
+ check("audit_log_author_check", sql`${table.author} IN ('app', 'vault')`),
197
+ check("audit_log_actor_check", sql`${table.actor} ~ '^(user|token|sync|system):.+$'`),
198
+ check("audit_log_decision_check", sql`${table.decision} IN ('allow', 'deny')`),
199
+ check("audit_log_metadata_check", sql`${table.metadata}::jsonb IS NOT NULL`),
200
+ check("audit_log_prev_hash_check", sql`octet_length(${table.prevHash}) = 32`),
201
+ check("audit_log_mac_check", sql`octet_length(${table.mac}) = 32`),
202
+ check("audit_log_hash_check", sql`octet_length(${table.hash}) = 32`),
203
+ foreignKey({
204
+ name: "audit_log_project_id_fkey",
205
+ columns: [table.projectId],
206
+ foreignColumns: [projects.id]
207
+ }).onDelete("restrict"),
208
+ foreignKey({
209
+ name: "audit_log_environment_id_fkey",
210
+ columns: [table.environmentId],
211
+ foreignColumns: [environments.id]
212
+ }).onDelete("restrict"),
213
+ foreignKey({
214
+ name: "audit_log_secret_id_fkey",
215
+ columns: [table.secretId],
216
+ foreignColumns: [secrets.id]
217
+ }).onDelete("restrict"),
218
+ foreignKey({
219
+ name: "audit_log_secret_version_id_fkey",
220
+ columns: [table.secretVersionId],
221
+ foreignColumns: [secretVersions.id]
222
+ }).onDelete("restrict"),
223
+ foreignKey({
224
+ name: "audit_log_related_seq_fkey",
225
+ columns: [table.relatedSeq],
226
+ foreignColumns: [table.seq]
227
+ }).onDelete("restrict"),
228
+ index("audit_log_project_idx").on(table.projectId, table.seq),
229
+ index("audit_log_environment_idx").on(table.environmentId, table.seq),
230
+ index("audit_log_secret_idx").on(table.secretId, table.seq),
231
+ index("audit_log_actor_idx").on(table.actor, table.seq),
232
+ index("audit_log_operation_idx").on(table.operationId, table.seq),
233
+ index("audit_log_action_idx").on(table.author, table.action, table.seq),
234
+ index("audit_log_releases_idx").on(table.author, table.actor, table.action, table.decision, table.occurredAt),
235
+ index("audit_log_subject_idx").on(table.author, table.subjectPrincipal, table.seq)
236
+ ]);
237
+ const auditChainHead = pgTable("audit_chain_head", {
238
+ onlyRow: boolean("only_row").primaryKey().default(true),
239
+ nextSeq: bigint("next_seq", { mode: "bigint" }).notNull().default(sql`0`),
240
+ headHash: bytea("head_hash").notNull()
241
+ }, (table) => [
242
+ check("audit_chain_head_only_row_check", sql`${table.onlyRow}`),
243
+ check("audit_chain_head_next_seq_check", sql`${table.nextSeq} >= 0`),
244
+ check("audit_chain_head_head_hash_check", sql`octet_length(${table.headHash}) = 32`)
245
+ ]);
246
+ /**
247
+ * An account at a sign-in provider, bound to one principal.
248
+ *
249
+ * Looked up by (provider, subject), never by email. An email address is
250
+ * recycled when someone leaves; the provider's subject is not, so binding to
251
+ * it is what stops a new holder of an old address from inheriting its access.
252
+ * The email is kept for display only.
253
+ */
254
+ const identities = pgTable("identities", {
255
+ id: uuid().primaryKey().defaultRandom(),
256
+ provider: text().notNull(),
257
+ authMac: bytea("auth_mac").notNull(),
258
+ subject: text().notNull(),
259
+ issuerHash: text("issuer_hash").notNull(),
260
+ generation: integer().notNull(),
261
+ principal: text().notNull(),
262
+ email: text(),
263
+ createdAt: createdAt(),
264
+ createdBy: text("created_by").notNull(),
265
+ lastSignInAt: timestamp("last_sign_in_at", { withTimezone: true }),
266
+ revokedAt: timestamp("revoked_at", { withTimezone: true }),
267
+ revokedBy: text("revoked_by"),
268
+ activeSubject: text("active_subject").generatedAlwaysAs(ACTIVE_SUBJECT)
269
+ }, (table) => [
270
+ check("identities_principal_check", sql`${table.principal} LIKE 'user:%'`),
271
+ check("identities_provider_check", sql`${table.provider} ~ '^[a-z0-9][a-z0-9-]{0,31}$'`),
272
+ check("identities_auth_mac_check", sql`octet_length(${table.authMac}) = 32`),
273
+ unique("identities_member_generation_key").on(table.id, table.principal, table.generation),
274
+ foreignKey({
275
+ name: "identities_principal_fkey",
276
+ columns: [table.principal],
277
+ foreignColumns: [vaultMembers.principal]
278
+ }).onDelete("restrict"),
279
+ uniqueIndex("identities_active_subject").on(table.provider, table.issuerHash, table.activeSubject),
280
+ index("identities_principal_idx").on(table.principal, table.generation)
281
+ ]);
282
+ /**
283
+ * Bearer credentials coffre issues itself: browser sessions, CLI sessions and
284
+ * service tokens.
285
+ *
286
+ * One table, so that revoking everything a principal holds is one statement.
287
+ * Only a SHA-256 of each token is stored. Tokens carry 256 bits of entropy, so
288
+ * a fast hash is enough; a database leak yields nothing that authenticates.
289
+ */
290
+ const credentials = pgTable("credentials", {
291
+ id: uuid().primaryKey().defaultRandom(),
292
+ kind: text().notNull(),
293
+ authMac: bytea("auth_mac").notNull(),
294
+ tokenHash: bytea("token_hash").notNull(),
295
+ tokenHint: text("token_hint").notNull(),
296
+ generation: integer().notNull(),
297
+ principal: text().notNull(),
298
+ identityId: uuid("identity_id"),
299
+ label: text(),
300
+ createdAt: createdAt(),
301
+ createdBy: text("created_by").notNull(),
302
+ expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
303
+ lastUsedAt: timestamp("last_used_at", { withTimezone: true }),
304
+ lastUsedIp: text("last_used_ip"),
305
+ revokedAt: timestamp("revoked_at", { withTimezone: true }),
306
+ revokedBy: text("revoked_by")
307
+ }, (table) => [
308
+ unique("credentials_token_hash_key").on(table.tokenHash),
309
+ check("credentials_kind_check", sql`${table.kind} IN ('browser', 'cli', 'service')`),
310
+ check("credentials_principal_check", sql`${table.principal} LIKE 'user:%' OR ${table.principal} LIKE 'token:%'`),
311
+ check("credentials_kind_matches_principal", sql`(${table.kind} = 'service') = (${table.principal} LIKE 'token:%')`),
312
+ check("credentials_token_hash_check", sql`octet_length(${table.tokenHash}) = 32`),
313
+ check("credentials_auth_mac_check", sql`octet_length(${table.authMac}) = 32`),
314
+ foreignKey({
315
+ name: "credentials_principal_fkey",
316
+ columns: [table.principal],
317
+ foreignColumns: [vaultMembers.principal]
318
+ }).onDelete("restrict"),
319
+ foreignKey({
320
+ name: "credentials_identity_id_fkey",
321
+ columns: [
322
+ table.identityId,
323
+ table.principal,
324
+ table.generation
325
+ ],
326
+ foreignColumns: [
327
+ identities.id,
328
+ identities.principal,
329
+ identities.generation
330
+ ]
331
+ }).onDelete("restrict"),
332
+ index("credentials_principal_idx").on(table.principal, table.generation)
333
+ ]);
334
+ /**
335
+ * A CLI asking to be signed in from a browser (RFC 8628, device flow).
336
+ *
337
+ * The CLI holds the device code and polls with it; a signed-in person approves
338
+ * the short user code in their browser. It works the same on a laptop and on
339
+ * a server reached over SSH, which a localhost redirect does not.
340
+ */
341
+ const deviceAuthorizations = pgTable("device_authorizations", {
342
+ id: uuid().primaryKey().defaultRandom(),
343
+ deviceCodeHash: bytea("device_code_hash").notNull(),
344
+ authMac: bytea("auth_mac").notNull(),
345
+ userCode: text("user_code").notNull(),
346
+ clientLabel: text("client_label"),
347
+ clientIp: text("client_ip"),
348
+ createdAt: createdAt(),
349
+ expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
350
+ decidedAt: timestamp("decided_at", { withTimezone: true }),
351
+ decision: text(),
352
+ generation: integer().notNull().default(0),
353
+ principal: text(),
354
+ consumedAt: timestamp("consumed_at", { withTimezone: true })
355
+ }, (table) => [
356
+ unique("device_authorizations_device_code_hash_key").on(table.deviceCodeHash),
357
+ unique("device_authorizations_user_code_key").on(table.userCode),
358
+ check("device_authorizations_auth_mac_check", sql`octet_length(${table.authMac}) = 32`),
359
+ check("device_authorizations_decision_check", sql`${table.decision} IS NULL OR ${table.decision} IN ('approved', 'denied')`),
360
+ check("device_authorizations_approval_names_principal", sql`(${table.decision} IS NULL AND ${table.decidedAt} IS NULL AND ${table.principal} IS NULL AND ${table.generation} = 0 AND ${table.consumedAt} IS NULL)
361
+ OR (${table.decision} IS NOT NULL AND ${table.decidedAt} IS NOT NULL AND (
362
+ (${table.decision} = 'denied' AND ${table.principal} IS NULL AND ${table.generation} = 0 AND ${table.consumedAt} IS NULL)
363
+ OR (${table.decision} = 'approved' AND ${table.principal} IS NOT NULL AND ${table.principal} LIKE 'user:%')
364
+ ))`),
365
+ foreignKey({
366
+ name: "device_authorizations_principal_fkey",
367
+ columns: [table.principal],
368
+ foreignColumns: [vaultMembers.principal]
369
+ }).onDelete("restrict")
370
+ ]);
371
+ /**
372
+ * An environment kept in step with a third-party service: GitHub Actions
373
+ * secrets, Vercel or Railway variables, Worker secrets.
374
+ *
375
+ * The destination's API token is itself a coffre secret, referenced by id, so
376
+ * it is encrypted, versioned and audited like everything else and never sits
377
+ * in this table.
378
+ */
379
+ const syncs = pgTable("syncs", {
380
+ id: uuid().primaryKey().defaultRandom(),
381
+ projectId: uuid("project_id").notNull(),
382
+ environmentId: uuid("environment_id").notNull(),
383
+ provider: text().notNull(),
384
+ config: text().notNull(),
385
+ credentialSecretId: uuid("credential_secret_id").notNull(),
386
+ createdAt: createdAt(),
387
+ createdBy: text("created_by").notNull(),
388
+ pausedAt: timestamp("paused_at", { withTimezone: true }),
389
+ archivedAt: timestamp("archived_at", { withTimezone: true }),
390
+ leaseUntil: timestamp("lease_until", { withTimezone: true }),
391
+ lastRunAt: timestamp("last_run_at", { withTimezone: true }),
392
+ lastStatus: text("last_status"),
393
+ lastError: text("last_error")
394
+ }, (table) => [
395
+ check("syncs_config_check", sql`${table.config}::jsonb IS NOT NULL`),
396
+ check("syncs_last_status_check", sql`${table.lastStatus} IS NULL OR ${table.lastStatus} IN ('ok', 'partial', 'failed')`),
397
+ foreignKey({
398
+ name: "syncs_environment_in_project",
399
+ columns: [table.environmentId, table.projectId],
400
+ foreignColumns: [environments.id, environments.projectId]
401
+ }).onDelete("restrict"),
402
+ foreignKey({
403
+ name: "syncs_credential_secret_id_fkey",
404
+ columns: [table.credentialSecretId],
405
+ foreignColumns: [secrets.id]
406
+ }).onDelete("restrict"),
407
+ index("syncs_environment_idx").on(table.environmentId)
408
+ ]);
409
+ /**
410
+ * What a sync last pushed, one row per key.
411
+ *
412
+ * Most destinations are write-only, so coffre cannot diff against them. It
413
+ * diffs against this instead: a key is stale when its secret has moved past
414
+ * the version recorded here. It is also the list of keys coffre may delete at
415
+ * the destination; a key it never pushed is never removed.
416
+ */
417
+ const syncKeys = pgTable("sync_keys", {
418
+ syncId: uuid("sync_id").notNull(),
419
+ key: text().notNull(),
420
+ secretVersionId: uuid("secret_version_id"),
421
+ pushedAt: timestamp("pushed_at", { withTimezone: true }).notNull().defaultNow(),
422
+ removedAt: timestamp("removed_at", { withTimezone: true })
423
+ }, (table) => [
424
+ primaryKey({
425
+ name: "sync_keys_pkey",
426
+ columns: [table.syncId, table.key]
427
+ }),
428
+ foreignKey({
429
+ name: "sync_keys_sync_id_fkey",
430
+ columns: [table.syncId],
431
+ foreignColumns: [syncs.id]
432
+ }).onDelete("restrict"),
433
+ foreignKey({
434
+ name: "sync_keys_secret_version_id_fkey",
435
+ columns: [table.secretVersionId],
436
+ foreignColumns: [secretVersions.id]
437
+ }).onDelete("restrict")
438
+ ]);
439
+ const { environmentsRelations, secretsRelations, syncsRelations, syncKeysRelations } = relationsOf({
440
+ projects,
441
+ environments,
442
+ secrets,
443
+ secretVersions,
444
+ syncs,
445
+ syncKeys
446
+ });
447
+ //#endregion
448
+ export { auditChainHead, auditLog, credentials, deviceAuthorizations, environments, environmentsRelations, identities, projects, secretVersions, secrets, secretsRelations, syncKeys, syncKeysRelations, syncs, syncsRelations, schema_exports as t, vaultGrants, vaultMembers };
package/package.json CHANGED
@@ -1,6 +1,94 @@
1
1
  {
2
2
  "name": "@coffre/db",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "description": "coffre's database: the schema, its migrations and the connections, on Postgres or SQLite.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/erwinkn/coffre.git",
9
+ "directory": "packages/db"
10
+ },
11
+ "type": "module",
12
+ "engines": {
13
+ "node": ">=24"
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "src"
18
+ ],
19
+ "exports": {
20
+ ".": {
21
+ "coffre:source": "./src/database.ts",
22
+ "types": "./dist/index.d.ts",
23
+ "default": "./dist/index.js"
24
+ },
25
+ "./schema": {
26
+ "coffre:source": "./src/schema.ts",
27
+ "types": "./dist/schema.d.ts",
28
+ "default": "./dist/schema.js"
29
+ },
30
+ "./schema-sqlite": {
31
+ "coffre:source": "./src/schema.sqlite.ts",
32
+ "types": "./dist/schema-sqlite.d.ts",
33
+ "default": "./dist/schema-sqlite.js"
34
+ },
35
+ "./dialect": {
36
+ "coffre:source": "./src/dialect.ts",
37
+ "types": "./dist/dialect.d.ts",
38
+ "default": "./dist/dialect.js"
39
+ },
40
+ "./portable": {
41
+ "coffre:source": "./src/portable.ts",
42
+ "types": "./dist/portable.d.ts",
43
+ "default": "./dist/portable.js"
44
+ },
45
+ "./schema-version": {
46
+ "coffre:source": "./src/schema-version.ts",
47
+ "types": "./dist/schema-version.d.ts",
48
+ "default": "./dist/schema-version.js"
49
+ },
50
+ "./hyperdrive": {
51
+ "coffre:source": "./src/hyperdrive.ts",
52
+ "types": "./dist/hyperdrive.d.ts",
53
+ "default": "./dist/hyperdrive.js"
54
+ },
55
+ "./connect": {
56
+ "coffre:source": "./src/connect.ts",
57
+ "types": "./dist/connect.d.ts",
58
+ "default": "./dist/connect.js"
59
+ },
60
+ "./migrate": {
61
+ "coffre:source": "./src/migrate.ts",
62
+ "types": "./dist/migrate.d.ts",
63
+ "default": "./dist/migrate.js"
64
+ },
65
+ "./log": {
66
+ "coffre:source": "./src/log.ts",
67
+ "types": "./dist/log.d.ts",
68
+ "default": "./dist/log.js"
69
+ },
70
+ "./package.json": "./package.json"
71
+ },
72
+ "dependencies": {
73
+ "@libsql/client": "0.18.0",
74
+ "drizzle-orm": "0.45.2",
75
+ "pg": "8.16.3",
76
+ "@coffre/core": "0.1.0"
77
+ },
78
+ "devDependencies": {
79
+ "@types/node": "26.1.1",
80
+ "@types/pg": "8.20.0",
81
+ "drizzle-kit": "0.31.10",
82
+ "tsdown": "0.23.0",
83
+ "typescript": "5.9.3"
84
+ },
85
+ "scripts": {
86
+ "build": "tsdown && cp -r src/migrations dist/migrations",
87
+ "typecheck": "tsc --noEmit",
88
+ "test": "node --conditions=coffre:source --test",
89
+ "db:generate": "node --conditions=coffre:source src/baseline.ts",
90
+ "db:migrate": "node --conditions=coffre:source src/migrate-main.ts",
91
+ "db:check": "cd src && for engine in postgres sqlite; do drizzle-kit check --config drizzle.$engine.config.ts || exit 1; done",
92
+ "db:verify:runtime": "node --conditions=coffre:source src/verify-runtime.ts"
93
+ }
6
94
  }