@coffre/db 0.10.2 → 0.11.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 (38) hide show
  1. package/dist/connect.d.ts +1 -1
  2. package/dist/{database-DBtn3jqp.d.ts → database-Dh4W2pAj.d.ts} +1 -1
  3. package/dist/dialect.d.ts +1 -1
  4. package/dist/grants.d.ts +12 -5
  5. package/dist/grants.js +13 -8
  6. package/dist/hyperdrive.d.ts +9 -2
  7. package/dist/hyperdrive.js +16 -2
  8. package/dist/index.d.ts +2 -2
  9. package/dist/log.d.ts +1 -1
  10. package/dist/migrations/postgres/0015_workload_access.sql +43 -0
  11. package/dist/migrations/postgres/0016_spent_tokens.sql +12 -0
  12. package/dist/migrations/postgres/meta/0015_snapshot.json +3256 -0
  13. package/dist/migrations/postgres/meta/0016_snapshot.json +3256 -0
  14. package/dist/migrations/postgres/meta/_journal.json +14 -0
  15. package/dist/migrations/sqlite/0015_workload_access.sql +70 -0
  16. package/dist/migrations/sqlite/meta/0015_snapshot.json +2959 -0
  17. package/dist/migrations/sqlite/meta/_journal.json +7 -0
  18. package/dist/portable.d.ts +2 -2
  19. package/dist/{schema-NBVA9q-s.d.ts → schema-DG2t57Kf.d.ts} +327 -2
  20. package/dist/schema-sqlite.d.ts +346 -1
  21. package/dist/schema-sqlite.js +53 -5
  22. package/dist/schema-version.d.ts +1 -1
  23. package/dist/schema-version.js +21 -0
  24. package/dist/schema.d.ts +2 -2
  25. package/dist/schema.js +54 -5
  26. package/package.json +2 -2
  27. package/src/grants.ts +18 -9
  28. package/src/hyperdrive.ts +16 -2
  29. package/src/migrations/postgres/0015_workload_access.sql +43 -0
  30. package/src/migrations/postgres/0016_spent_tokens.sql +12 -0
  31. package/src/migrations/postgres/meta/0015_snapshot.json +3256 -0
  32. package/src/migrations/postgres/meta/0016_snapshot.json +3256 -0
  33. package/src/migrations/postgres/meta/_journal.json +14 -0
  34. package/src/migrations/sqlite/0015_workload_access.sql +70 -0
  35. package/src/migrations/sqlite/meta/0015_snapshot.json +2959 -0
  36. package/src/migrations/sqlite/meta/_journal.json +7 -0
  37. package/src/schema.sqlite.ts +73 -2
  38. package/src/schema.ts +73 -2
@@ -106,6 +106,13 @@
106
106
  "when": 1792345777255,
107
107
  "tag": "0014_custom_roles",
108
108
  "breakpoints": true
109
+ },
110
+ {
111
+ "idx": 15,
112
+ "version": "6",
113
+ "when": 1792803757926,
114
+ "tag": "0015_workload_access",
115
+ "breakpoints": true
109
116
  }
110
117
  ]
111
118
  }
@@ -385,6 +385,14 @@ export const auditLog = sqliteTable(
385
385
  index('audit_log_unbind_idx')
386
386
  .on(sql`json_extract(${table.metadata}, '$.bindingId')`)
387
387
  .where(sql`${table.author} = 'app' AND ${table.action} = 'token.unbind' AND ${table.decision} = 'allow'`),
388
+ // Whether a target is frozen: its newest allowed freeze or lift (docs/design/workload-access.md, section 8.2).
389
+ index('audit_log_freeze_idx')
390
+ .on(sql`json_extract(${table.metadata}, '$.target')`, table.seq)
391
+ .where(sql`${table.author} = 'app' AND ${table.action} IN ('workload.freeze', 'workload.unfreeze') AND ${table.decision} = 'allow'`),
392
+ // An enrollment code's tombstone: the app's successful `enrollment.supersede` naming it.
393
+ index('audit_log_supersede_idx')
394
+ .on(sql`json_extract(${table.metadata}, '$.enrollmentId')`)
395
+ .where(sql`${table.author} = 'app' AND ${table.action} = 'enrollment.supersede' AND ${table.decision} = 'allow'`),
388
396
  // The run a credential was issued for: its exchange's entry, by the credential's ID.
389
397
  index('audit_log_exchange_idx')
390
398
  .on(sql`json_extract(${table.metadata}, '$.credentialId')`)
@@ -514,6 +522,8 @@ export const serviceBindings = sqliteTable(
514
522
  claims: text().notNull(),
515
523
  // What the credentials it issues reach (`CredentialScope`), JSON; null on a binding of 0.4 the app has not sealed yet.
516
524
  scope: text(),
525
+ // What a workload's binding says beyond its claims (`BindingPolicy`), canonical JSON: null on a binding `/api/auth/oidc` takes, never on another (docs/design/workload-access.md, section 11).
526
+ policy: text(),
517
527
  label: text(),
518
528
  createdAt: createdAt(),
519
529
  createdBy: text('created_by').notNull(),
@@ -661,6 +671,46 @@ export const mcpApprovals = sqliteTable(
661
671
  ],
662
672
  );
663
673
 
674
+ /**
675
+ * Enrollment codes (docs/design/workload-access.md, section 4.4): what CI
676
+ * mints for a platform that signs no identity, and the app exchanges for a
677
+ * workload's session at `/api/auth/workload`. Never a credential itself, and
678
+ * only ever added: the log says which code a promote superseded. Its scope,
679
+ * fixed at minting, is the binding's within what the minter held; the MAC
680
+ * covers everything but its hint and creation time (auth-rows.ts in the server).
681
+ */
682
+ export const enrollments = sqliteTable(
683
+ 'enrollments',
684
+ {
685
+ id: text().primaryKey(),
686
+ authMac: bytes('auth_mac').notNull(),
687
+ bindingId: text('binding_id').notNull(),
688
+ // The SHA-256 of the code, `coffre_enr_…`, and enough of it to recognize.
689
+ codeHash: bytes('code_hash').notNull(),
690
+ codeHint: text('code_hint').notNull(),
691
+ // `CredentialScope`, JSON: what its sessions reach, R32's sensitive per place.
692
+ scope: text().notNull(),
693
+ createdAt: createdAt(),
694
+ // Who minted it, and with which of their credentials: a CI run's session.
695
+ createdBy: text('created_by').notNull(),
696
+ createdWith: text('created_with'),
697
+ expiresAt: time('expires_at').notNull(),
698
+ },
699
+ (table) => [
700
+ unique('enrollments_code_hash_key').on(table.codeHash),
701
+ check('enrollments_auth_mac_check', sql`octet_length(${table.authMac}) = 32`),
702
+ check('enrollments_code_hash_check', sql`octet_length(${table.codeHash}) = 32`),
703
+ check('enrollments_scope_check', sql`json_valid(${table.scope})`),
704
+ foreignKey({
705
+ name: 'enrollments_binding_id_fkey',
706
+ columns: [table.bindingId],
707
+ foreignColumns: [serviceBindings.id],
708
+ }).onDelete('restrict'),
709
+ // A binding's codes, newest first, for its page.
710
+ index('enrollments_binding_idx').on(table.bindingId, table.createdAt),
711
+ ],
712
+ );
713
+
664
714
  export const credentials = sqliteTable(
665
715
  'credentials',
666
716
  {
@@ -685,12 +735,24 @@ export const credentials = sqliteTable(
685
735
  lastUsedIp: text('last_used_ip'),
686
736
  revokedAt: time('revoked_at'),
687
737
  revokedBy: text('revoked_by'),
738
+ // A workload's session (docs/design/workload-access.md, section 11), null on every other credential:
739
+ // the thumbprint of the key its proofs are signed by, and its hard end.
740
+ jkt: text(),
741
+ notAfter: time('not_after'),
742
+ // A `narrow` child's: its ancestors' IDs, root first, JSON, and its limits (`Constraints`), canonical JSON.
743
+ ancestors: text(),
744
+ constraints: text(),
745
+ // The enrollment code a session was made from, and, where its binding says `approve`, who approved it.
746
+ enrollmentId: text('enrollment_id'),
747
+ approvedAt: time('approved_at'),
748
+ approvedBy: text('approved_by'),
688
749
  },
689
750
  (table) => [
690
751
  unique('credentials_token_hash_key').on(table.tokenHash),
691
- check('credentials_kind_check', sql`${table.kind} IN ('browser', 'cli', 'service', 'personal', 'handle')`),
752
+ check('credentials_kind_check', sql`${table.kind} IN ('browser', 'cli', 'service', 'personal', 'handle', 'workload')`),
692
753
  check('credentials_principal_check', sql`${table.principal} LIKE 'user:%' OR ${table.principal} LIKE 'token:%'`),
693
- check('credentials_kind_matches_principal', sql`(${table.kind} = 'service') = (${table.principal} LIKE 'token:%')`),
754
+ check('credentials_kind_matches_principal', sql`(${table.kind} IN ('service', 'workload')) = (${table.principal} LIKE 'token:%')`),
755
+ check('credentials_workload_jkt_check', sql`${table.kind} <> 'workload' OR ${table.jkt} IS NOT NULL`),
694
756
  check('credentials_token_hash_check', sql`octet_length(${table.tokenHash}) = 32`),
695
757
  check('credentials_auth_mac_check', sql`octet_length(${table.authMac}) = 32`),
696
758
  foreignKey({
@@ -710,6 +772,15 @@ export const credentials = sqliteTable(
710
772
  index('credentials_live_idx').on(table.principal, table.expiresAt).where(sql`${table.revokedAt} IS NULL`),
711
773
  // What a trust binding issued lately, for its rate.
712
774
  index('credentials_issued_by_idx').on(table.principal, table.createdBy, table.createdAt),
775
+ foreignKey({
776
+ name: 'credentials_enrollment_id_fkey',
777
+ columns: [table.enrollmentId],
778
+ foreignColumns: [enrollments.id],
779
+ }).onDelete('restrict'),
780
+ // A code's sessions made by the exchange, ended ones too: its capacity and its tripwire's last hour, never a child.
781
+ index('credentials_enrollment_roots_idx')
782
+ .on(table.enrollmentId, table.createdAt)
783
+ .where(sql`${table.kind} = 'workload' AND ${table.ancestors} IS NULL`),
713
784
  ],
714
785
  );
715
786
 
package/src/schema.ts CHANGED
@@ -402,6 +402,14 @@ export const auditLog = pgTable(
402
402
  index('audit_log_unbind_idx')
403
403
  .on(sql`((${table.metadata})::jsonb ->> 'bindingId')`)
404
404
  .where(sql`${table.author} = 'app' AND ${table.action} = 'token.unbind' AND ${table.decision} = 'allow'`),
405
+ // Whether a target is frozen: its newest allowed freeze or lift (docs/design/workload-access.md, section 8.2).
406
+ index('audit_log_freeze_idx')
407
+ .on(sql`((${table.metadata})::jsonb ->> 'target')`, table.seq.desc())
408
+ .where(sql`${table.author} = 'app' AND ${table.action} IN ('workload.freeze', 'workload.unfreeze') AND ${table.decision} = 'allow'`),
409
+ // An enrollment code's tombstone: the app's successful `enrollment.supersede` naming it.
410
+ index('audit_log_supersede_idx')
411
+ .on(sql`((${table.metadata})::jsonb ->> 'enrollmentId')`)
412
+ .where(sql`${table.author} = 'app' AND ${table.action} = 'enrollment.supersede' AND ${table.decision} = 'allow'`),
405
413
  // The run a credential was issued for: its exchange's entry, by the credential's ID.
406
414
  index('audit_log_exchange_idx')
407
415
  .on(sql`((${table.metadata})::jsonb ->> 'credentialId')`)
@@ -563,6 +571,8 @@ export const serviceBindings = pgTable(
563
571
  claims: text().notNull(),
564
572
  // What the credentials it issues reach (`CredentialScope`), JSON; null on a binding of 0.4 the app has not sealed yet.
565
573
  scope: text(),
574
+ // What a workload's binding says beyond its claims (`BindingPolicy`), canonical JSON: null on a binding `/api/auth/oidc` takes, never on another (docs/design/workload-access.md, section 11).
575
+ policy: text(),
566
576
  label: text(),
567
577
  createdAt: createdAt(),
568
578
  createdBy: text('created_by').notNull(),
@@ -726,6 +736,46 @@ export const mcpApprovals = pgTable(
726
736
  ],
727
737
  );
728
738
 
739
+ /**
740
+ * Enrollment codes (docs/design/workload-access.md, section 4.4): what CI
741
+ * mints for a platform that signs no identity, and the app exchanges for a
742
+ * workload's session at `/api/auth/workload`. Never a credential itself, and
743
+ * only ever added: the log says which code a promote superseded. Its scope,
744
+ * fixed at minting, is the binding's within what the minter held; the MAC
745
+ * covers everything but its hint and creation time (auth-rows.ts in the server).
746
+ */
747
+ export const enrollments = pgTable(
748
+ 'enrollments',
749
+ {
750
+ id: uuid().primaryKey(),
751
+ authMac: bytea('auth_mac').notNull(),
752
+ bindingId: uuid('binding_id').notNull(),
753
+ // The SHA-256 of the code, `coffre_enr_…`, and enough of it to recognize.
754
+ codeHash: bytea('code_hash').notNull(),
755
+ codeHint: text('code_hint').notNull(),
756
+ // `CredentialScope`, JSON: what its sessions reach, R32's sensitive per place.
757
+ scope: text().notNull(),
758
+ createdAt: createdAt(),
759
+ // Who minted it, and with which of their credentials: a CI run's session.
760
+ createdBy: text('created_by').notNull(),
761
+ createdWith: uuid('created_with'),
762
+ expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),
763
+ },
764
+ (table) => [
765
+ unique('enrollments_code_hash_key').on(table.codeHash),
766
+ check('enrollments_auth_mac_check', sql`octet_length(${table.authMac}) = 32`),
767
+ check('enrollments_code_hash_check', sql`octet_length(${table.codeHash}) = 32`),
768
+ check('enrollments_scope_check', sql`${table.scope}::jsonb IS NOT NULL`),
769
+ foreignKey({
770
+ name: 'enrollments_binding_id_fkey',
771
+ columns: [table.bindingId],
772
+ foreignColumns: [serviceBindings.id],
773
+ }).onDelete('restrict'),
774
+ // A binding's codes, newest first, for its page.
775
+ index('enrollments_binding_idx').on(table.bindingId, table.createdAt.desc()),
776
+ ],
777
+ );
778
+
729
779
  /**
730
780
  * Bearer credentials coffre issues itself: browser sessions, CLI sessions and
731
781
  * service tokens.
@@ -758,12 +808,24 @@ export const credentials = pgTable(
758
808
  lastUsedIp: text('last_used_ip'),
759
809
  revokedAt: timestamp('revoked_at', { withTimezone: true }),
760
810
  revokedBy: text('revoked_by'),
811
+ // A workload's session (docs/design/workload-access.md, section 11), null on every other credential:
812
+ // the thumbprint of the key its proofs are signed by, and its hard end.
813
+ jkt: text(),
814
+ notAfter: timestamp('not_after', { withTimezone: true }),
815
+ // A `narrow` child's: its ancestors' IDs, root first, JSON, and its limits (`Constraints`), canonical JSON.
816
+ ancestors: text(),
817
+ constraints: text(),
818
+ // The enrollment code a session was made from, and, where its binding says `approve`, who approved it.
819
+ enrollmentId: uuid('enrollment_id'),
820
+ approvedAt: timestamp('approved_at', { withTimezone: true }),
821
+ approvedBy: text('approved_by'),
761
822
  },
762
823
  (table) => [
763
824
  unique('credentials_token_hash_key').on(table.tokenHash),
764
- check('credentials_kind_check', sql`${table.kind} IN ('browser', 'cli', 'service', 'personal', 'handle')`),
825
+ check('credentials_kind_check', sql`${table.kind} IN ('browser', 'cli', 'service', 'personal', 'handle', 'workload')`),
765
826
  check('credentials_principal_check', sql`${table.principal} LIKE 'user:%' OR ${table.principal} LIKE 'token:%'`),
766
- check('credentials_kind_matches_principal', sql`(${table.kind} = 'service') = (${table.principal} LIKE 'token:%')`),
827
+ check('credentials_kind_matches_principal', sql`(${table.kind} IN ('service', 'workload')) = (${table.principal} LIKE 'token:%')`),
828
+ check('credentials_workload_jkt_check', sql`${table.kind} <> 'workload' OR ${table.jkt} IS NOT NULL`),
767
829
  check('credentials_token_hash_check', sql`octet_length(${table.tokenHash}) = 32`),
768
830
  check('credentials_auth_mac_check', sql`octet_length(${table.authMac}) = 32`),
769
831
  foreignKey({
@@ -783,6 +845,15 @@ export const credentials = pgTable(
783
845
  index('credentials_live_idx').on(table.principal, table.expiresAt).where(sql`${table.revokedAt} IS NULL`),
784
846
  // What a trust binding issued lately, for its rate.
785
847
  index('credentials_issued_by_idx').on(table.principal, table.createdBy, table.createdAt),
848
+ foreignKey({
849
+ name: 'credentials_enrollment_id_fkey',
850
+ columns: [table.enrollmentId],
851
+ foreignColumns: [enrollments.id],
852
+ }).onDelete('restrict'),
853
+ // A code's sessions made by the exchange, ended ones too: its capacity and its tripwire's last hour, never a child.
854
+ index('credentials_enrollment_roots_idx')
855
+ .on(table.enrollmentId, table.createdAt)
856
+ .where(sql`${table.kind} = 'workload' AND ${table.ancestors} IS NULL`),
786
857
  ],
787
858
  );
788
859