@coffre/db 0.0.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +19 -3
- package/dist/commits-BuW-NL3n.js +30 -0
- package/dist/connect.d.ts +26 -0
- package/dist/connect.js +156 -0
- package/dist/database-CWpo5aF0.d.ts +39 -0
- package/dist/dialect.d.ts +88 -0
- package/dist/dialect.js +128 -0
- package/dist/hyperdrive.d.ts +32 -0
- package/dist/hyperdrive.js +54 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +34 -0
- package/dist/log.d.ts +72 -0
- package/dist/log.js +133 -0
- package/dist/migrate.d.ts +10 -0
- package/dist/migrate.js +130 -0
- package/dist/migrations/postgres/0000_baseline.sql +505 -0
- package/dist/migrations/postgres/meta/0000_snapshot.json +1939 -0
- package/dist/migrations/postgres/meta/_journal.json +13 -0
- package/dist/migrations/sqlite/0000_baseline.sql +293 -0
- package/dist/migrations/sqlite/meta/0000_snapshot.json +1811 -0
- package/dist/migrations/sqlite/meta/_journal.json +13 -0
- package/dist/portable.d.ts +33 -0
- package/dist/portable.js +12 -0
- package/dist/postgres-Bmar8y0q.js +29 -0
- package/dist/relations-DqgxtlKu.js +58 -0
- package/dist/rolldown-runtime-D7D4PA-g.js +13 -0
- package/dist/schema-Cd4WOnB5.d.ts +2503 -0
- package/dist/schema-sqlite.d.ts +2587 -0
- package/dist/schema-sqlite.js +401 -0
- package/dist/schema-version.d.ts +11 -0
- package/dist/schema-version.js +16 -0
- package/dist/schema.d.ts +2 -0
- package/dist/schema.js +448 -0
- package/package.json +92 -5
- package/src/baseline/postgres.sql +241 -0
- package/src/baseline/sqlite.sql +27 -0
- package/src/baseline.ts +104 -0
- package/src/commits.ts +47 -0
- package/src/connect.ts +171 -0
- package/src/database.ts +63 -0
- package/src/dialect.ts +174 -0
- package/src/drizzle.postgres.config.ts +7 -0
- package/src/drizzle.sqlite.config.ts +7 -0
- package/src/hyperdrive.ts +63 -0
- package/src/log.ts +177 -0
- package/src/migrate-main.ts +8 -0
- package/src/migrate.ts +207 -0
- package/src/migrations/postgres/0000_baseline.sql +505 -0
- package/src/migrations/postgres/meta/0000_snapshot.json +1939 -0
- package/src/migrations/postgres/meta/_journal.json +13 -0
- package/src/migrations/sqlite/0000_baseline.sql +293 -0
- package/src/migrations/sqlite/meta/0000_snapshot.json +1811 -0
- package/src/migrations/sqlite/meta/_journal.json +13 -0
- package/src/portable.ts +54 -0
- package/src/postgres.ts +35 -0
- package/src/relations.ts +53 -0
- package/src/schema-version.ts +13 -0
- package/src/schema.sqlite.ts +511 -0
- package/src/schema.ts +548 -0
- package/src/verify-runtime.ts +122 -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,8 +1,95 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coffre/db",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "coffre's database: the schema, its migrations and the connections, on Postgres or SQLite.",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"
|
|
7
|
-
|
|
8
|
-
|
|
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
|
+
"pg-connection-string": "2.14.0",
|
|
77
|
+
"@coffre/core": "0.1.1"
|
|
78
|
+
},
|
|
79
|
+
"devDependencies": {
|
|
80
|
+
"@types/node": "26.1.1",
|
|
81
|
+
"@types/pg": "8.20.0",
|
|
82
|
+
"drizzle-kit": "0.31.10",
|
|
83
|
+
"tsdown": "0.23.0",
|
|
84
|
+
"typescript": "5.9.3"
|
|
85
|
+
},
|
|
86
|
+
"scripts": {
|
|
87
|
+
"build": "tsdown && cp -r src/migrations dist/migrations",
|
|
88
|
+
"typecheck": "tsc --noEmit",
|
|
89
|
+
"test": "node --conditions=coffre:source --test",
|
|
90
|
+
"db:generate": "node --conditions=coffre:source src/baseline.ts",
|
|
91
|
+
"db:migrate": "node --conditions=coffre:source src/migrate-main.ts",
|
|
92
|
+
"db:check": "cd src && for engine in postgres sqlite; do drizzle-kit check --config drizzle.$engine.config.ts || exit 1; done",
|
|
93
|
+
"db:verify:runtime": "node --conditions=coffre:source src/verify-runtime.ts"
|
|
94
|
+
}
|
|
95
|
+
}
|