@coffre/db 0.9.0 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/dist/connect.d.ts +1 -1
  2. package/dist/connect.js +1 -1
  3. package/dist/database-BadNeeN-.js +146 -0
  4. package/dist/{database-hd1hAll-.d.ts → database-DBtn3jqp.d.ts} +27 -3
  5. package/dist/dialect.d.ts +1 -1
  6. package/dist/grants.d.ts +2 -2
  7. package/dist/grants.js +26 -10
  8. package/dist/hyperdrive.d.ts +1 -1
  9. package/dist/index.d.ts +3 -3
  10. package/dist/index.js +2 -89
  11. package/dist/log.d.ts +26 -6
  12. package/dist/log.js +86 -21
  13. package/dist/migrations/postgres/0012_sensitive_values.sql +18 -0
  14. package/dist/migrations/postgres/0013_contributor_role.sql +5 -0
  15. package/dist/migrations/postgres/0014_custom_roles.sql +4 -0
  16. package/dist/migrations/postgres/meta/0012_snapshot.json +3004 -0
  17. package/dist/migrations/postgres/meta/0013_snapshot.json +3004 -0
  18. package/dist/migrations/postgres/meta/0014_snapshot.json +3000 -0
  19. package/dist/migrations/postgres/meta/_journal.json +21 -0
  20. package/dist/migrations/sqlite/0012_sensitive_values.sql +15 -0
  21. package/dist/migrations/sqlite/0013_contributor_role.sql +28 -0
  22. package/dist/migrations/sqlite/0014_custom_roles.sql +27 -0
  23. package/dist/migrations/sqlite/meta/0012_snapshot.json +2726 -0
  24. package/dist/migrations/sqlite/meta/0013_snapshot.json +2726 -0
  25. package/dist/migrations/sqlite/meta/0014_snapshot.json +2722 -0
  26. package/dist/migrations/sqlite/meta/_journal.json +21 -0
  27. package/dist/portable.d.ts +2 -2
  28. package/dist/{schema-CCYKTG-E.d.ts → schema-NBVA9q-s.d.ts} +17 -0
  29. package/dist/schema-sqlite.d.ts +17 -0
  30. package/dist/schema-sqlite.js +3 -6
  31. package/dist/schema-version.d.ts +1 -1
  32. package/dist/schema-version.js +42 -0
  33. package/dist/schema.d.ts +1 -1
  34. package/dist/schema.js +3 -7
  35. package/package.json +2 -2
  36. package/src/database.ts +35 -3
  37. package/src/grants.ts +32 -18
  38. package/src/log.ts +90 -34
  39. package/src/migrations/postgres/0012_sensitive_values.sql +18 -0
  40. package/src/migrations/postgres/0013_contributor_role.sql +5 -0
  41. package/src/migrations/postgres/0014_custom_roles.sql +4 -0
  42. package/src/migrations/postgres/meta/0012_snapshot.json +3004 -0
  43. package/src/migrations/postgres/meta/0013_snapshot.json +3004 -0
  44. package/src/migrations/postgres/meta/0014_snapshot.json +3000 -0
  45. package/src/migrations/postgres/meta/_journal.json +21 -0
  46. package/src/migrations/sqlite/0012_sensitive_values.sql +15 -0
  47. package/src/migrations/sqlite/0013_contributor_role.sql +28 -0
  48. package/src/migrations/sqlite/0014_custom_roles.sql +27 -0
  49. package/src/migrations/sqlite/meta/0012_snapshot.json +2726 -0
  50. package/src/migrations/sqlite/meta/0013_snapshot.json +2726 -0
  51. package/src/migrations/sqlite/meta/0014_snapshot.json +2722 -0
  52. package/src/migrations/sqlite/meta/_journal.json +21 -0
  53. package/src/prepared.ts +60 -0
  54. package/src/schema.sqlite.ts +8 -9
  55. package/src/schema.ts +8 -10
@@ -85,6 +85,27 @@
85
85
  "when": 1792317461299,
86
86
  "tag": "0011_plain_values",
87
87
  "breakpoints": true
88
+ },
89
+ {
90
+ "idx": 12,
91
+ "version": "6",
92
+ "when": 1792340920098,
93
+ "tag": "0012_sensitive_values",
94
+ "breakpoints": true
95
+ },
96
+ {
97
+ "idx": 13,
98
+ "version": "6",
99
+ "when": 1792343374795,
100
+ "tag": "0013_contributor_role",
101
+ "breakpoints": true
102
+ },
103
+ {
104
+ "idx": 14,
105
+ "version": "6",
106
+ "when": 1792345777255,
107
+ "tag": "0014_custom_roles",
108
+ "breakpoints": true
88
109
  }
89
110
  ]
90
111
  }
@@ -0,0 +1,60 @@
1
+ import type { Query } from 'drizzle-orm';
2
+
3
+ import { tablesOf, type Queryable } from './database.ts';
4
+ import { engineOf } from './dialect.ts';
5
+
6
+ /** What Drizzle works out from a select builder before it can run it, once per dialect. */
7
+ type Built = { query: Query; fields: unknown; joinsNotNullableMap: unknown; engine: 'postgres' | 'sqlite' };
8
+
9
+ /** The part of a select builder `prepared` reads, once. */
10
+ type Select = PromiseLike<unknown> & { toSQL(): Query; prepare(...name: never[]): unknown };
11
+
12
+ /**
13
+ * A select built once per process, or once per isolate on Workers, and run
14
+ * with new values each time. Drizzle rebuilds a query's SQL from its
15
+ * builder at every run, and that was most of a short query's CPU; this
16
+ * builds it from `build` the first time a database of its dialect asks,
17
+ * then hands the SQL and its columns to that call's own session
18
+ * (`session.prepareQuery`), which binds `values` to the placeholders
19
+ * (`sql.placeholder`) and maps the rows as the builder would.
20
+ *
21
+ * `build` must not depend on anything but the database's dialect, and the
22
+ * values go to the driver as they are, not through a column's encoding:
23
+ * strings, numbers, bigints and bytes.
24
+ *
25
+ * const member = prepared((db) => db.select().from(tablesOf(db).vaultMembers).where(eq(…, sql.placeholder('principal'))));
26
+ * const [row] = await member(db, { principal });
27
+ */
28
+ export function prepared<Q extends Select>(
29
+ build: (db: Queryable) => Q,
30
+ ): (db: Queryable, values?: Record<string, unknown>) => Promise<Awaited<Q>> {
31
+ // By the dialect's tables: one Postgres build, one SQLite build.
32
+ const builds = new WeakMap<object, Built>();
33
+ return (db, values = {}) => {
34
+ const tables = tablesOf(db);
35
+ let built = builds.get(tables);
36
+ if (built === undefined) {
37
+ const builder = build(db);
38
+ // Drizzle's own preparation, which runs nothing, says which columns come back and which joins may be null.
39
+ // Unnamed on Postgres: no statement is prepared on the server, whose connections a call does not keep.
40
+ const { fields, joinsNotNullableMap } = (builder as unknown as { prepare(): { fields: unknown; joinsNotNullableMap: unknown } }).prepare();
41
+ built = { query: builder.toSQL(), fields, joinsNotNullableMap, engine: engineOf(db) };
42
+ builds.set(tables, built);
43
+ }
44
+ const session = (db as unknown as { session: Session }).session;
45
+ const query = built.engine === 'sqlite'
46
+ ? session.prepareQuery(built.query, built.fields, 'all', true)
47
+ : session.prepareQuery(built.query, built.fields, undefined, true);
48
+ query.joinsNotNullableMap = built.joinsNotNullableMap;
49
+ return (built.engine === 'sqlite' ? query.all(values) : query.execute(values)) as Promise<Awaited<Q>>;
50
+ };
51
+ }
52
+
53
+ /** Drizzle's session, Postgres's or SQLite's, as `prepared` calls it. */
54
+ type Session = {
55
+ prepareQuery(query: Query, fields: unknown, nameOrMethod: string | undefined, isResponseInArrayMode: boolean): {
56
+ joinsNotNullableMap: unknown;
57
+ execute(values: Record<string, unknown>): Promise<unknown>;
58
+ all(values: Record<string, unknown>): Promise<unknown>;
59
+ };
60
+ };
@@ -1,4 +1,3 @@
1
- import { assignableToEnvironment, ROLE_NAMES } from '@coffre/core/access';
2
1
  import { sql } from 'drizzle-orm';
3
2
  import {
4
3
  blob,
@@ -182,6 +181,11 @@ export const secretVersions = sqliteTable(
182
181
  // the vault. Null for a secret. Its ciphertext is kept as any version's,
183
182
  // for what opens values through the vault (a reference, a fork's copy).
184
183
  plaintext: text(),
184
+ // A sensitive value: never handed back to a person, Owners included, only
185
+ // used for them through the credential proxy, or read by a service
186
+ // account. Its data key's wrap binds it, so the vault opens a version
187
+ // flipped back as nothing. Never plain too.
188
+ sensitive: flag('sensitive').notNull().default(false),
185
189
  },
186
190
  (table) => [
187
191
  unique('secret_versions_secret_id_version_key').on(table.secretId, table.version),
@@ -221,9 +225,6 @@ export const dismissedKeys = sqliteTable(
221
225
  ],
222
226
  );
223
227
 
224
- const ROLES = sql.raw(ROLE_NAMES.map((role) => `'${role}'`).join(', '));
225
- const ENVIRONMENT_ROLES = sql.raw(ROLE_NAMES.filter(assignableToEnvironment).map((role) => `'${role}'`).join(', '));
226
-
227
228
  export const vaultMembers = sqliteTable(
228
229
  'vault_members',
229
230
  {
@@ -275,11 +276,9 @@ export const vaultGrants = sqliteTable(
275
276
  sql`((${table.projectId} IS NULL) <> (${table.environmentId} IS NULL) AND ${table.environmentSlug} IS NULL) OR (${table.projectId} IS NULL AND ${table.environmentId} IS NULL)`,
276
277
  ),
277
278
  check('vault_grants_environment_slug_check', sql`${table.environmentSlug} IS NULL OR (${isSlug(table.environmentSlug, 63)})`),
278
- check('vault_grants_role_check', sql`${table.role} IN (${ROLES})`),
279
- check(
280
- 'vault_grants_environment_role_check',
281
- sql`(${table.environmentId} IS NULL AND ${table.environmentSlug} IS NULL) OR ${table.role} IN (${ENVIRONMENT_ROLES})`,
282
- ),
279
+ // A role's id, one that ships or one an Owner made (`ROLE_ID`): which the instance's policy defines, and which of them an
280
+ // environment may be granted, the vault decides, as the policy is now (docs/design/roles.md).
281
+ check('vault_grants_role_check', sql`length(${table.role}) BETWEEN 2 AND 32 AND ${table.role} GLOB '[a-z]*' AND ${table.role} NOT GLOB '*[^a-z0-9-]*'`),
283
282
  unique('vault_grants_on_project').on(table.principal, table.projectId),
284
283
  unique('vault_grants_on_environment').on(table.principal, table.environmentId),
285
284
  unique('vault_grants_on_environment_slug').on(table.principal, table.environmentSlug),
package/src/schema.ts CHANGED
@@ -1,4 +1,3 @@
1
- import { assignableToEnvironment, ROLE_NAMES } from '@coffre/core/access';
2
1
  import { sql } from 'drizzle-orm';
3
2
  import {
4
3
  bigint,
@@ -176,6 +175,11 @@ export const secretVersions = pgTable(
176
175
  // the vault. Null for a secret. Its ciphertext is kept as any version's,
177
176
  // for what opens values through the vault (a reference, a fork's copy).
178
177
  plaintext: text(),
178
+ // A sensitive value: never handed back to a person, Owners included, only
179
+ // used for them through the credential proxy, or read by a service
180
+ // account. Its data key's wrap binds it, so the vault opens a version
181
+ // flipped back as nothing. Never plain too.
182
+ sensitive: boolean().notNull().default(false),
179
183
  },
180
184
  (table) => [
181
185
  unique('secret_versions_secret_id_version_key').on(table.secretId, table.version),
@@ -219,10 +223,6 @@ export const dismissedKeys = pgTable(
219
223
  ],
220
224
  );
221
225
 
222
- /** The role catalogue, as SQL: every role, and those an environment may be granted. */
223
- const ROLES = sql.raw(ROLE_NAMES.map((role) => `'${role}'`).join(', '));
224
- const ENVIRONMENT_ROLES = sql.raw(ROLE_NAMES.filter(assignableToEnvironment).map((role) => `'${role}'`).join(', '));
225
-
226
226
  /**
227
227
  * The vault's member directory: everyone it has admitted, written by the
228
228
  * vault alone, read by both. A principal with no row is no member.
@@ -294,11 +294,9 @@ export const vaultGrants = pgTable(
294
294
  sql`((${table.projectId} IS NULL) <> (${table.environmentId} IS NULL) AND ${table.environmentSlug} IS NULL) OR (${table.projectId} IS NULL AND ${table.environmentId} IS NULL)`,
295
295
  ),
296
296
  check('vault_grants_environment_slug_check', sql`${table.environmentSlug} IS NULL OR ${table.environmentSlug} ~ '^[a-z0-9][a-z0-9-]{0,62}$'`),
297
- check('vault_grants_role_check', sql`${table.role} IN (${ROLES})`),
298
- check(
299
- 'vault_grants_environment_role_check',
300
- sql`(${table.environmentId} IS NULL AND ${table.environmentSlug} IS NULL) OR ${table.role} IN (${ENVIRONMENT_ROLES})`,
301
- ),
297
+ // A role's id, one that ships or one an Owner made (`ROLE_ID`): which the instance's policy defines, and which of them an
298
+ // environment may be granted, the vault decides, as the policy is now (docs/design/roles.md).
299
+ check('vault_grants_role_check', sql`${table.role} ~ '^[a-z][a-z0-9-]{1,31}$'`),
302
300
  unique('vault_grants_on_project').on(table.principal, table.projectId),
303
301
  unique('vault_grants_on_environment').on(table.principal, table.environmentId),
304
302
  unique('vault_grants_on_environment_slug').on(table.principal, table.environmentSlug),