@coffre/db 0.6.2 → 0.8.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 (72) hide show
  1. package/dist/connect.d.ts +1 -1
  2. package/dist/{database-C63WjCrC.d.ts → database-BEnexh-3.d.ts} +1 -1
  3. package/dist/dialect.d.ts +1 -1
  4. package/dist/grants.d.ts +1 -1
  5. package/dist/hyperdrive.d.ts +1 -1
  6. package/dist/index.d.ts +2 -2
  7. package/dist/log.d.ts +22 -1
  8. package/dist/log.js +37 -2
  9. package/dist/migrations/postgres/0004_environment_parents.sql +5 -0
  10. package/dist/migrations/postgres/0005_personal_tokens.sql +5 -0
  11. package/dist/migrations/postgres/0006_uploaded_keys.sql +2 -0
  12. package/dist/migrations/postgres/0007_personal_configs.sql +4 -0
  13. package/dist/migrations/postgres/0008_recipes.sql +19 -0
  14. package/dist/migrations/postgres/0009_handles.sql +3 -0
  15. package/dist/migrations/postgres/meta/0004_snapshot.json +2913 -0
  16. package/dist/migrations/postgres/meta/0005_snapshot.json +2929 -0
  17. package/dist/migrations/postgres/meta/0006_snapshot.json +2935 -0
  18. package/dist/migrations/postgres/meta/0007_snapshot.json +2967 -0
  19. package/dist/migrations/postgres/meta/0008_snapshot.json +2985 -0
  20. package/dist/migrations/postgres/meta/0009_snapshot.json +2985 -0
  21. package/dist/migrations/postgres/meta/_journal.json +42 -0
  22. package/dist/migrations/sqlite/0004_environment_parents.sql +3 -0
  23. package/dist/migrations/sqlite/0005_personal_tokens.sql +37 -0
  24. package/dist/migrations/sqlite/0006_uploaded_keys.sql +1 -0
  25. package/dist/migrations/sqlite/0007_personal_configs.sql +3 -0
  26. package/dist/migrations/sqlite/0008_recipes.sql +15 -0
  27. package/dist/migrations/sqlite/0009_handles.sql +36 -0
  28. package/dist/migrations/sqlite/meta/0004_snapshot.json +2641 -0
  29. package/dist/migrations/sqlite/meta/0005_snapshot.json +2656 -0
  30. package/dist/migrations/sqlite/meta/0006_snapshot.json +2663 -0
  31. package/dist/migrations/sqlite/meta/0007_snapshot.json +2683 -0
  32. package/dist/migrations/sqlite/meta/0008_snapshot.json +2704 -0
  33. package/dist/migrations/sqlite/meta/0009_snapshot.json +2704 -0
  34. package/dist/migrations/sqlite/meta/_journal.json +42 -0
  35. package/dist/portable.d.ts +2 -2
  36. package/dist/{schema-a7PkFgea.d.ts → schema-BZRkBJQZ.d.ts} +102 -0
  37. package/dist/schema-sqlite.d.ts +114 -0
  38. package/dist/schema-sqlite.js +20 -4
  39. package/dist/schema-version.d.ts +1 -1
  40. package/dist/schema-version.js +84 -0
  41. package/dist/schema.d.ts +1 -1
  42. package/dist/schema.js +20 -4
  43. package/package.json +2 -2
  44. package/src/log.ts +40 -1
  45. package/src/migrations/postgres/0004_environment_parents.sql +5 -0
  46. package/src/migrations/postgres/0005_personal_tokens.sql +5 -0
  47. package/src/migrations/postgres/0006_uploaded_keys.sql +2 -0
  48. package/src/migrations/postgres/0007_personal_configs.sql +4 -0
  49. package/src/migrations/postgres/0008_recipes.sql +19 -0
  50. package/src/migrations/postgres/0009_handles.sql +3 -0
  51. package/src/migrations/postgres/meta/0004_snapshot.json +2913 -0
  52. package/src/migrations/postgres/meta/0005_snapshot.json +2929 -0
  53. package/src/migrations/postgres/meta/0006_snapshot.json +2935 -0
  54. package/src/migrations/postgres/meta/0007_snapshot.json +2967 -0
  55. package/src/migrations/postgres/meta/0008_snapshot.json +2985 -0
  56. package/src/migrations/postgres/meta/0009_snapshot.json +2985 -0
  57. package/src/migrations/postgres/meta/_journal.json +42 -0
  58. package/src/migrations/sqlite/0004_environment_parents.sql +3 -0
  59. package/src/migrations/sqlite/0005_personal_tokens.sql +37 -0
  60. package/src/migrations/sqlite/0006_uploaded_keys.sql +1 -0
  61. package/src/migrations/sqlite/0007_personal_configs.sql +3 -0
  62. package/src/migrations/sqlite/0008_recipes.sql +15 -0
  63. package/src/migrations/sqlite/0009_handles.sql +36 -0
  64. package/src/migrations/sqlite/meta/0004_snapshot.json +2641 -0
  65. package/src/migrations/sqlite/meta/0005_snapshot.json +2656 -0
  66. package/src/migrations/sqlite/meta/0006_snapshot.json +2663 -0
  67. package/src/migrations/sqlite/meta/0007_snapshot.json +2683 -0
  68. package/src/migrations/sqlite/meta/0008_snapshot.json +2704 -0
  69. package/src/migrations/sqlite/meta/0009_snapshot.json +2704 -0
  70. package/src/migrations/sqlite/meta/_journal.json +42 -0
  71. package/src/schema.sqlite.ts +31 -3
  72. package/src/schema.ts +37 -3
@@ -29,6 +29,48 @@
29
29
  "when": 1791676487593,
30
30
  "tag": "0003_credential_scopes",
31
31
  "breakpoints": true
32
+ },
33
+ {
34
+ "idx": 4,
35
+ "version": "6",
36
+ "when": 1792244020959,
37
+ "tag": "0004_environment_parents",
38
+ "breakpoints": true
39
+ },
40
+ {
41
+ "idx": 5,
42
+ "version": "6",
43
+ "when": 1792250910230,
44
+ "tag": "0005_personal_tokens",
45
+ "breakpoints": true
46
+ },
47
+ {
48
+ "idx": 6,
49
+ "version": "6",
50
+ "when": 1792252693912,
51
+ "tag": "0006_uploaded_keys",
52
+ "breakpoints": true
53
+ },
54
+ {
55
+ "idx": 7,
56
+ "version": "6",
57
+ "when": 1792253795070,
58
+ "tag": "0007_personal_configs",
59
+ "breakpoints": true
60
+ },
61
+ {
62
+ "idx": 8,
63
+ "version": "6",
64
+ "when": 1792255560312,
65
+ "tag": "0008_recipes",
66
+ "breakpoints": true
67
+ },
68
+ {
69
+ "idx": 9,
70
+ "version": "6",
71
+ "when": 1792281880190,
72
+ "tag": "0009_handles",
73
+ "breakpoints": true
32
74
  }
33
75
  ]
34
76
  }
@@ -85,16 +85,29 @@ export const environments = sqliteTable(
85
85
  name: text().notNull(),
86
86
  createdAt: createdAt(),
87
87
  archivedAt: time('archived_at'),
88
+ parentId: text('parent_id'),
89
+ owner: text(),
88
90
  },
89
91
  (table) => [
90
92
  unique('environments_project_id_slug_key').on(table.projectId, table.slug),
91
93
  unique('environments_project_scoped').on(table.id, table.projectId),
92
94
  check('environments_slug_check', isPlaceSlug(table.slug)),
95
+ // An environment is never its own parent. Longer loops, written around the app, are refused at read (`parentsOf`).
96
+ check('environments_parent_check', sql`${table.parentId} IS NULL OR ${table.parentId} <> ${table.id}`),
97
+ check('environments_owner_check', sql`${table.owner} IS NULL OR (${table.parentId} IS NOT NULL AND ${table.owner} LIKE 'user:_%')`),
98
+ uniqueIndex('environments_parent_id_owner_key').on(table.parentId, table.owner).where(sql`instr(${table.slug}, '~') = 0`),
93
99
  foreignKey({
94
100
  name: 'environments_project_id_fkey',
95
101
  columns: [table.projectId],
96
102
  foreignColumns: [projects.id],
97
103
  }).onDelete('restrict'),
104
+ // Checked at the end of a statement, not at each row: one that deletes a fork and its parent together is no orphan.
105
+ foreignKey({
106
+ name: 'environments_parent_in_project',
107
+ columns: [table.parentId, table.projectId],
108
+ foreignColumns: [table.id, table.projectId],
109
+ }).onDelete('no action'),
110
+ index('environments_parent_idx').on(table.parentId),
98
111
  ],
99
112
  );
100
113
 
@@ -156,6 +169,14 @@ export const secretVersions = sqliteTable(
156
169
  kekVersion: text('kek_version').notNull(),
157
170
  createdAt: createdAt(),
158
171
  createdBy: text('created_by').notNull(),
172
+ // How a client made this value, `bytes:32:hex` (`@coffre/core/recipe`),
173
+ // when it was generated rather than typed: Regenerate makes the next one
174
+ // the same way. Null for a typed value, so writing one by hand clears it.
175
+ recipe: text(),
176
+ // A generated key pair's two versions, written together, share an id,
177
+ // and each says which half it holds: 'private' or 'public'.
178
+ pairId: text('pair_id'),
179
+ pairHalf: text('pair_half'),
159
180
  },
160
181
  (table) => [
161
182
  unique('secret_versions_secret_id_version_key').on(table.secretId, table.version),
@@ -364,6 +385,10 @@ export const auditLog = sqliteTable(
364
385
  index('audit_log_exchange_idx')
365
386
  .on(sql`json_extract(${table.metadata}, '$.credentialId')`)
366
387
  .where(sql`${table.author} = 'app' AND ${table.action} = 'token.exchange' AND ${table.decision} = 'allow'`),
388
+ // The token a credential is, when one was issued: its creation's entry, by the credential's ID, whose label names a personal token's entries.
389
+ index('audit_log_token_create_idx')
390
+ .on(sql`json_extract(${table.metadata}, '$.targetCredentialId')`)
391
+ .where(sql`${table.author} = 'app' AND ${table.action} = 'token.create' AND ${table.decision} = 'allow'`),
367
392
  index('audit_log_reference_end_idx')
368
393
  .on(table.relatedSeq)
369
394
  .where(sql`${table.author} = 'vault' AND ${table.action} = 'reference.end' AND ${table.decision} = 'allow'`),
@@ -476,8 +501,11 @@ export const serviceBindings = sqliteTable(
476
501
  generation: integer().notNull(),
477
502
  profile: text().notNull(),
478
503
  issuer: text().notNull(),
479
- // From the issuer's discovery when the binding was made, never refetched.
504
+ // From the issuer's discovery when the binding was made, never refetched;
505
+ // or, for keys uploaded with the binding, `sha256:<hex>` of `jwks`.
480
506
  jwksUri: text('jwks_uri').notNull(),
507
+ // The issuer's public keys, uploaded with the binding (`uploadedKeys`), JSON; null when they are fetched.
508
+ jwks: text(),
481
509
  // JSON, sorted by claim name.
482
510
  claims: text().notNull(),
483
511
  // What the credentials it issues reach (`CredentialScope`), JSON; null on a binding of 0.4 the app has not sealed yet.
@@ -641,7 +669,7 @@ export const credentials = sqliteTable(
641
669
  // The member it acts as, `user:<email>` or `token:<id>`, under the generation it was issued in.
642
670
  principal: text().notNull(),
643
671
  identityId: text('identity_id'),
644
- // What a service token reaches (`CredentialScope`), JSON: null for a session, one a binding issued (its binding's), or one of 0.4 the app has not sealed yet.
672
+ // What a service or personal token reaches (`CredentialScope`), JSON: null for a session, one a binding issued (its binding's), or one of 0.4 the app has not sealed yet.
645
673
  scope: text(),
646
674
  label: text(),
647
675
  createdAt: createdAt(),
@@ -654,7 +682,7 @@ export const credentials = sqliteTable(
654
682
  },
655
683
  (table) => [
656
684
  unique('credentials_token_hash_key').on(table.tokenHash),
657
- check('credentials_kind_check', sql`${table.kind} IN ('browser', 'cli', 'service')`),
685
+ check('credentials_kind_check', sql`${table.kind} IN ('browser', 'cli', 'service', 'personal', 'handle')`),
658
686
  check('credentials_principal_check', sql`${table.principal} LIKE 'user:%' OR ${table.principal} LIKE 'token:%'`),
659
687
  check('credentials_kind_matches_principal', sql`(${table.kind} = 'service') = (${table.principal} LIKE 'token:%')`),
660
688
  check('credentials_token_hash_check', sql`octet_length(${table.tokenHash}) = 32`),
package/src/schema.ts CHANGED
@@ -68,16 +68,35 @@ export const environments = pgTable(
68
68
  name: text().notNull(),
69
69
  createdAt: createdAt(),
70
70
  archivedAt: timestamp('archived_at', { withTimezone: true }),
71
+ // The environment it inherits from, in the same project: its keys are
72
+ // this one's, but for those it sets or removes (docs/design/forking.md).
73
+ // Set when it is made and never after, so no chain can loop.
74
+ parentId: uuid('parent_id'),
75
+ // A personal config's owner, `user:<email>`: a fork whose values only they
76
+ // read, the vault binding each to them. Set when it is made and never after.
77
+ owner: text(),
71
78
  },
72
79
  (table) => [
73
80
  unique('environments_project_id_slug_key').on(table.projectId, table.slug),
74
81
  unique('environments_project_scoped').on(table.id, table.projectId),
75
82
  check('environments_slug_check', sql`${table.slug} ~ ${PLACE_SLUG}`),
83
+ // An environment is never its own parent. Longer loops, written around the app, are refused at read (`parentsOf`).
84
+ check('environments_parent_check', sql`${table.parentId} IS NULL OR ${table.parentId} <> ${table.id}`),
85
+ check('environments_owner_check', sql`${table.owner} IS NULL OR (${table.parentId} IS NOT NULL AND ${table.owner} LIKE 'user:_%')`),
86
+ // One personal config per person per environment; a deleted one, a tombstone, holds no place.
87
+ uniqueIndex('environments_parent_id_owner_key').on(table.parentId, table.owner).where(sql`strpos(${table.slug}, '~') = 0`),
76
88
  foreignKey({
77
89
  name: 'environments_project_id_fkey',
78
90
  columns: [table.projectId],
79
91
  foreignColumns: [projects.id],
80
92
  }).onDelete('restrict'),
93
+ // Checked at the end of a statement, not at each row: one that deletes a fork and its parent together is no orphan.
94
+ foreignKey({
95
+ name: 'environments_parent_in_project',
96
+ columns: [table.parentId, table.projectId],
97
+ foreignColumns: [table.id, table.projectId],
98
+ }).onDelete('no action'),
99
+ index('environments_parent_idx').on(table.parentId),
81
100
  ],
82
101
  );
83
102
 
@@ -144,6 +163,14 @@ export const secretVersions = pgTable(
144
163
  kekVersion: text('kek_version').notNull(),
145
164
  createdAt: createdAt(),
146
165
  createdBy: text('created_by').notNull(),
166
+ // How a client made this value, `bytes:32:hex` (`@coffre/core/recipe`),
167
+ // when it was generated rather than typed: Regenerate makes the next one
168
+ // the same way. Null for a typed value, so writing one by hand clears it.
169
+ recipe: text(),
170
+ // A generated key pair's two versions, written together, share an id,
171
+ // and each says which half it holds: 'private' or 'public'.
172
+ pairId: uuid('pair_id'),
173
+ pairHalf: text('pair_half'),
147
174
  },
148
175
  (table) => [
149
176
  unique('secret_versions_secret_id_version_key').on(table.secretId, table.version),
@@ -376,6 +403,10 @@ export const auditLog = pgTable(
376
403
  index('audit_log_exchange_idx')
377
404
  .on(sql`((${table.metadata})::jsonb ->> 'credentialId')`)
378
405
  .where(sql`${table.author} = 'app' AND ${table.action} = 'token.exchange' AND ${table.decision} = 'allow'`),
406
+ // The token a credential is, when one was issued: its creation's entry, by the credential's ID, whose label names a personal token's entries.
407
+ index('audit_log_token_create_idx')
408
+ .on(sql`((${table.metadata})::jsonb ->> 'targetCredentialId')`)
409
+ .where(sql`${table.author} = 'app' AND ${table.action} = 'token.create' AND ${table.decision} = 'allow'`),
379
410
  // Whether a reference has ended: the vault's `reference.end` naming its creation.
380
411
  index('audit_log_reference_end_idx')
381
412
  .on(table.relatedSeq)
@@ -520,8 +551,11 @@ export const serviceBindings = pgTable(
520
551
  generation: integer().notNull(),
521
552
  profile: text().notNull(),
522
553
  issuer: text().notNull(),
523
- // From the issuer's discovery when the binding was made, never refetched.
554
+ // From the issuer's discovery when the binding was made, never refetched;
555
+ // or, for keys uploaded with the binding, `sha256:<hex>` of `jwks`.
524
556
  jwksUri: text('jwks_uri').notNull(),
557
+ // The issuer's public keys, uploaded with the binding (`uploadedKeys`), JSON; null when they are fetched.
558
+ jwks: text(),
525
559
  // JSON, sorted by claim name.
526
560
  claims: text().notNull(),
527
561
  // What the credentials it issues reach (`CredentialScope`), JSON; null on a binding of 0.4 the app has not sealed yet.
@@ -709,7 +743,7 @@ export const credentials = pgTable(
709
743
  // The member it acts as, `user:<email>` or `token:<id>`, under the generation it was issued in.
710
744
  principal: text().notNull(),
711
745
  identityId: uuid('identity_id'),
712
- // What a service token reaches (`CredentialScope`), JSON: null for a session, one a binding issued (its binding's), or one of 0.4 the app has not sealed yet.
746
+ // What a service or personal token reaches (`CredentialScope`), JSON: null for a session, one a binding issued (its binding's), or one of 0.4 the app has not sealed yet.
713
747
  scope: text(),
714
748
  label: text(),
715
749
  createdAt: createdAt(),
@@ -722,7 +756,7 @@ export const credentials = pgTable(
722
756
  },
723
757
  (table) => [
724
758
  unique('credentials_token_hash_key').on(table.tokenHash),
725
- check('credentials_kind_check', sql`${table.kind} IN ('browser', 'cli', 'service')`),
759
+ check('credentials_kind_check', sql`${table.kind} IN ('browser', 'cli', 'service', 'personal', 'handle')`),
726
760
  check('credentials_principal_check', sql`${table.principal} LIKE 'user:%' OR ${table.principal} LIKE 'token:%'`),
727
761
  check('credentials_kind_matches_principal', sql`(${table.kind} = 'service') = (${table.principal} LIKE 'token:%')`),
728
762
  check('credentials_token_hash_check', sql`octet_length(${table.tokenHash}) = 32`),