@coffre/db 0.10.2 → 0.12.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 (46) hide show
  1. package/dist/connect.d.ts +1 -1
  2. package/dist/{database-DBtn3jqp.d.ts → database-Ds1Y79h6.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/0017_capped_alerts.sql +10 -0
  13. package/dist/migrations/postgres/meta/0015_snapshot.json +3256 -0
  14. package/dist/migrations/postgres/meta/0016_snapshot.json +3256 -0
  15. package/dist/migrations/postgres/meta/0017_snapshot.json +3295 -0
  16. package/dist/migrations/postgres/meta/_journal.json +21 -0
  17. package/dist/migrations/sqlite/0015_workload_access.sql +70 -0
  18. package/dist/migrations/sqlite/0016_capped_alerts.sql +7 -0
  19. package/dist/migrations/sqlite/meta/0015_snapshot.json +2959 -0
  20. package/dist/migrations/sqlite/meta/0016_snapshot.json +2998 -0
  21. package/dist/migrations/sqlite/meta/_journal.json +14 -0
  22. package/dist/portable.d.ts +2 -2
  23. package/dist/{schema-NBVA9q-s.d.ts → schema-DcVkJhCf.d.ts} +393 -2
  24. package/dist/schema-sqlite.d.ts +408 -1
  25. package/dist/schema-sqlite.js +61 -5
  26. package/dist/schema-version.d.ts +1 -1
  27. package/dist/schema-version.js +35 -0
  28. package/dist/schema.d.ts +2 -2
  29. package/dist/schema.js +71 -5
  30. package/package.json +2 -2
  31. package/src/grants.ts +18 -9
  32. package/src/hyperdrive.ts +16 -2
  33. package/src/migrations/postgres/0015_workload_access.sql +43 -0
  34. package/src/migrations/postgres/0016_spent_tokens.sql +12 -0
  35. package/src/migrations/postgres/0017_capped_alerts.sql +10 -0
  36. package/src/migrations/postgres/meta/0015_snapshot.json +3256 -0
  37. package/src/migrations/postgres/meta/0016_snapshot.json +3256 -0
  38. package/src/migrations/postgres/meta/0017_snapshot.json +3295 -0
  39. package/src/migrations/postgres/meta/_journal.json +21 -0
  40. package/src/migrations/sqlite/0015_workload_access.sql +70 -0
  41. package/src/migrations/sqlite/0016_capped_alerts.sql +7 -0
  42. package/src/migrations/sqlite/meta/0015_snapshot.json +2959 -0
  43. package/src/migrations/sqlite/meta/0016_snapshot.json +2998 -0
  44. package/src/migrations/sqlite/meta/_journal.json +14 -0
  45. package/src/schema.sqlite.ts +83 -2
  46. package/src/schema.ts +91 -2
@@ -106,6 +106,20 @@
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
116
+ },
117
+ {
118
+ "idx": 16,
119
+ "version": "6",
120
+ "when": 1792835446455,
121
+ "tag": "0016_capped_alerts",
122
+ "breakpoints": true
109
123
  }
110
124
  ]
111
125
  }
@@ -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(),
@@ -548,6 +558,16 @@ export const consumedTokens = sqliteTable(
548
558
  (table) => [check('consumed_tokens_hash_check', sql`octet_length(${table.hash}) = 32`)],
549
559
  );
550
560
 
561
+ export const cappedAlerts = sqliteTable(
562
+ 'capped_alerts',
563
+ {
564
+ service: text().notNull(),
565
+ kind: text().notNull(),
566
+ raisedAt: time('raised_at').notNull(),
567
+ },
568
+ (table) => [primaryKey({ name: 'capped_alerts_pkey', columns: [table.service, table.kind] })],
569
+ );
570
+
551
571
  /**
552
572
  * MCP clients that registered themselves (RFC 7591), the fallback to Client
553
573
  * ID Metadata Documents, which need no row. The ID is the client's
@@ -661,6 +681,46 @@ export const mcpApprovals = sqliteTable(
661
681
  ],
662
682
  );
663
683
 
684
+ /**
685
+ * Enrollment codes (docs/design/workload-access.md, section 4.4): what CI
686
+ * mints for a platform that signs no identity, and the app exchanges for a
687
+ * workload's session at `/api/auth/workload`. Never a credential itself, and
688
+ * only ever added: the log says which code a promote superseded. Its scope,
689
+ * fixed at minting, is the binding's within what the minter held; the MAC
690
+ * covers everything but its hint and creation time (auth-rows.ts in the server).
691
+ */
692
+ export const enrollments = sqliteTable(
693
+ 'enrollments',
694
+ {
695
+ id: text().primaryKey(),
696
+ authMac: bytes('auth_mac').notNull(),
697
+ bindingId: text('binding_id').notNull(),
698
+ // The SHA-256 of the code, `coffre_enr_…`, and enough of it to recognize.
699
+ codeHash: bytes('code_hash').notNull(),
700
+ codeHint: text('code_hint').notNull(),
701
+ // `CredentialScope`, JSON: what its sessions reach, R32's sensitive per place.
702
+ scope: text().notNull(),
703
+ createdAt: createdAt(),
704
+ // Who minted it, and with which of their credentials: a CI run's session.
705
+ createdBy: text('created_by').notNull(),
706
+ createdWith: text('created_with'),
707
+ expiresAt: time('expires_at').notNull(),
708
+ },
709
+ (table) => [
710
+ unique('enrollments_code_hash_key').on(table.codeHash),
711
+ check('enrollments_auth_mac_check', sql`octet_length(${table.authMac}) = 32`),
712
+ check('enrollments_code_hash_check', sql`octet_length(${table.codeHash}) = 32`),
713
+ check('enrollments_scope_check', sql`json_valid(${table.scope})`),
714
+ foreignKey({
715
+ name: 'enrollments_binding_id_fkey',
716
+ columns: [table.bindingId],
717
+ foreignColumns: [serviceBindings.id],
718
+ }).onDelete('restrict'),
719
+ // A binding's codes, newest first, for its page.
720
+ index('enrollments_binding_idx').on(table.bindingId, table.createdAt),
721
+ ],
722
+ );
723
+
664
724
  export const credentials = sqliteTable(
665
725
  'credentials',
666
726
  {
@@ -685,12 +745,24 @@ export const credentials = sqliteTable(
685
745
  lastUsedIp: text('last_used_ip'),
686
746
  revokedAt: time('revoked_at'),
687
747
  revokedBy: text('revoked_by'),
748
+ // A workload's session (docs/design/workload-access.md, section 11), null on every other credential:
749
+ // the thumbprint of the key its proofs are signed by, and its hard end.
750
+ jkt: text(),
751
+ notAfter: time('not_after'),
752
+ // A `narrow` child's: its ancestors' IDs, root first, JSON, and its limits (`Constraints`), canonical JSON.
753
+ ancestors: text(),
754
+ constraints: text(),
755
+ // The enrollment code a session was made from, and, where its binding says `approve`, who approved it.
756
+ enrollmentId: text('enrollment_id'),
757
+ approvedAt: time('approved_at'),
758
+ approvedBy: text('approved_by'),
688
759
  },
689
760
  (table) => [
690
761
  unique('credentials_token_hash_key').on(table.tokenHash),
691
- check('credentials_kind_check', sql`${table.kind} IN ('browser', 'cli', 'service', 'personal', 'handle')`),
762
+ check('credentials_kind_check', sql`${table.kind} IN ('browser', 'cli', 'service', 'personal', 'handle', 'workload')`),
692
763
  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:%')`),
764
+ check('credentials_kind_matches_principal', sql`(${table.kind} IN ('service', 'workload')) = (${table.principal} LIKE 'token:%')`),
765
+ check('credentials_workload_jkt_check', sql`${table.kind} <> 'workload' OR ${table.jkt} IS NOT NULL`),
694
766
  check('credentials_token_hash_check', sql`octet_length(${table.tokenHash}) = 32`),
695
767
  check('credentials_auth_mac_check', sql`octet_length(${table.authMac}) = 32`),
696
768
  foreignKey({
@@ -710,6 +782,15 @@ export const credentials = sqliteTable(
710
782
  index('credentials_live_idx').on(table.principal, table.expiresAt).where(sql`${table.revokedAt} IS NULL`),
711
783
  // What a trust binding issued lately, for its rate.
712
784
  index('credentials_issued_by_idx').on(table.principal, table.createdBy, table.createdAt),
785
+ foreignKey({
786
+ name: 'credentials_enrollment_id_fkey',
787
+ columns: [table.enrollmentId],
788
+ foreignColumns: [enrollments.id],
789
+ }).onDelete('restrict'),
790
+ // A code's sessions made by the exchange, ended ones too: its capacity and its tripwire's last hour, never a child.
791
+ index('credentials_enrollment_roots_idx')
792
+ .on(table.enrollmentId, table.createdAt)
793
+ .where(sql`${table.kind} = 'workload' AND ${table.ancestors} IS NULL`),
713
794
  ],
714
795
  );
715
796
 
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(),
@@ -603,6 +613,24 @@ export const consumedTokens = pgTable(
603
613
  (table) => [check('consumed_tokens_hash_check', sql`octet_length(${table.hash}) = 32`)],
604
614
  );
605
615
 
616
+ /**
617
+ * When a service's capped alert was last raised, by kind: the refusal of a
618
+ * token signed by a key its uploaded keys lack (`unknown_key`) alerts once
619
+ * an hour, however many isolates see it. One conditional write per refusal
620
+ * decides (`claimAlert`), so that the many it caps never reach the log's
621
+ * head. Only a request that a binding of the service on the token's issuer
622
+ * gets as far as its keys writes here: a row per service, at most, per kind.
623
+ */
624
+ export const cappedAlerts = pgTable(
625
+ 'capped_alerts',
626
+ {
627
+ service: text().notNull(),
628
+ kind: text().notNull(),
629
+ raisedAt: timestamp('raised_at', { withTimezone: true }).notNull(),
630
+ },
631
+ (table) => [primaryKey({ name: 'capped_alerts_pkey', columns: [table.service, table.kind] })],
632
+ );
633
+
606
634
  /**
607
635
  * MCP clients that registered themselves (RFC 7591), the fallback to Client
608
636
  * ID Metadata Documents, which need no row. The ID is the client's
@@ -726,6 +754,46 @@ export const mcpApprovals = pgTable(
726
754
  ],
727
755
  );
728
756
 
757
+ /**
758
+ * Enrollment codes (docs/design/workload-access.md, section 4.4): what CI
759
+ * mints for a platform that signs no identity, and the app exchanges for a
760
+ * workload's session at `/api/auth/workload`. Never a credential itself, and
761
+ * only ever added: the log says which code a promote superseded. Its scope,
762
+ * fixed at minting, is the binding's within what the minter held; the MAC
763
+ * covers everything but its hint and creation time (auth-rows.ts in the server).
764
+ */
765
+ export const enrollments = pgTable(
766
+ 'enrollments',
767
+ {
768
+ id: uuid().primaryKey(),
769
+ authMac: bytea('auth_mac').notNull(),
770
+ bindingId: uuid('binding_id').notNull(),
771
+ // The SHA-256 of the code, `coffre_enr_…`, and enough of it to recognize.
772
+ codeHash: bytea('code_hash').notNull(),
773
+ codeHint: text('code_hint').notNull(),
774
+ // `CredentialScope`, JSON: what its sessions reach, R32's sensitive per place.
775
+ scope: text().notNull(),
776
+ createdAt: createdAt(),
777
+ // Who minted it, and with which of their credentials: a CI run's session.
778
+ createdBy: text('created_by').notNull(),
779
+ createdWith: uuid('created_with'),
780
+ expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),
781
+ },
782
+ (table) => [
783
+ unique('enrollments_code_hash_key').on(table.codeHash),
784
+ check('enrollments_auth_mac_check', sql`octet_length(${table.authMac}) = 32`),
785
+ check('enrollments_code_hash_check', sql`octet_length(${table.codeHash}) = 32`),
786
+ check('enrollments_scope_check', sql`${table.scope}::jsonb IS NOT NULL`),
787
+ foreignKey({
788
+ name: 'enrollments_binding_id_fkey',
789
+ columns: [table.bindingId],
790
+ foreignColumns: [serviceBindings.id],
791
+ }).onDelete('restrict'),
792
+ // A binding's codes, newest first, for its page.
793
+ index('enrollments_binding_idx').on(table.bindingId, table.createdAt.desc()),
794
+ ],
795
+ );
796
+
729
797
  /**
730
798
  * Bearer credentials coffre issues itself: browser sessions, CLI sessions and
731
799
  * service tokens.
@@ -758,12 +826,24 @@ export const credentials = pgTable(
758
826
  lastUsedIp: text('last_used_ip'),
759
827
  revokedAt: timestamp('revoked_at', { withTimezone: true }),
760
828
  revokedBy: text('revoked_by'),
829
+ // A workload's session (docs/design/workload-access.md, section 11), null on every other credential:
830
+ // the thumbprint of the key its proofs are signed by, and its hard end.
831
+ jkt: text(),
832
+ notAfter: timestamp('not_after', { withTimezone: true }),
833
+ // A `narrow` child's: its ancestors' IDs, root first, JSON, and its limits (`Constraints`), canonical JSON.
834
+ ancestors: text(),
835
+ constraints: text(),
836
+ // The enrollment code a session was made from, and, where its binding says `approve`, who approved it.
837
+ enrollmentId: uuid('enrollment_id'),
838
+ approvedAt: timestamp('approved_at', { withTimezone: true }),
839
+ approvedBy: text('approved_by'),
761
840
  },
762
841
  (table) => [
763
842
  unique('credentials_token_hash_key').on(table.tokenHash),
764
- check('credentials_kind_check', sql`${table.kind} IN ('browser', 'cli', 'service', 'personal', 'handle')`),
843
+ check('credentials_kind_check', sql`${table.kind} IN ('browser', 'cli', 'service', 'personal', 'handle', 'workload')`),
765
844
  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:%')`),
845
+ check('credentials_kind_matches_principal', sql`(${table.kind} IN ('service', 'workload')) = (${table.principal} LIKE 'token:%')`),
846
+ check('credentials_workload_jkt_check', sql`${table.kind} <> 'workload' OR ${table.jkt} IS NOT NULL`),
767
847
  check('credentials_token_hash_check', sql`octet_length(${table.tokenHash}) = 32`),
768
848
  check('credentials_auth_mac_check', sql`octet_length(${table.authMac}) = 32`),
769
849
  foreignKey({
@@ -783,6 +863,15 @@ export const credentials = pgTable(
783
863
  index('credentials_live_idx').on(table.principal, table.expiresAt).where(sql`${table.revokedAt} IS NULL`),
784
864
  // What a trust binding issued lately, for its rate.
785
865
  index('credentials_issued_by_idx').on(table.principal, table.createdBy, table.createdAt),
866
+ foreignKey({
867
+ name: 'credentials_enrollment_id_fkey',
868
+ columns: [table.enrollmentId],
869
+ foreignColumns: [enrollments.id],
870
+ }).onDelete('restrict'),
871
+ // A code's sessions made by the exchange, ended ones too: its capacity and its tripwire's last hour, never a child.
872
+ index('credentials_enrollment_roots_idx')
873
+ .on(table.enrollmentId, table.createdAt)
874
+ .where(sql`${table.kind} = 'workload' AND ${table.ancestors} IS NULL`),
786
875
  ],
787
876
  );
788
877