@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.
- package/dist/connect.d.ts +1 -1
- package/dist/{database-DBtn3jqp.d.ts → database-Ds1Y79h6.d.ts} +1 -1
- package/dist/dialect.d.ts +1 -1
- package/dist/grants.d.ts +12 -5
- package/dist/grants.js +13 -8
- package/dist/hyperdrive.d.ts +9 -2
- package/dist/hyperdrive.js +16 -2
- package/dist/index.d.ts +2 -2
- package/dist/log.d.ts +1 -1
- package/dist/migrations/postgres/0015_workload_access.sql +43 -0
- package/dist/migrations/postgres/0016_spent_tokens.sql +12 -0
- package/dist/migrations/postgres/0017_capped_alerts.sql +10 -0
- package/dist/migrations/postgres/meta/0015_snapshot.json +3256 -0
- package/dist/migrations/postgres/meta/0016_snapshot.json +3256 -0
- package/dist/migrations/postgres/meta/0017_snapshot.json +3295 -0
- package/dist/migrations/postgres/meta/_journal.json +21 -0
- package/dist/migrations/sqlite/0015_workload_access.sql +70 -0
- package/dist/migrations/sqlite/0016_capped_alerts.sql +7 -0
- package/dist/migrations/sqlite/meta/0015_snapshot.json +2959 -0
- package/dist/migrations/sqlite/meta/0016_snapshot.json +2998 -0
- package/dist/migrations/sqlite/meta/_journal.json +14 -0
- package/dist/portable.d.ts +2 -2
- package/dist/{schema-NBVA9q-s.d.ts → schema-DcVkJhCf.d.ts} +393 -2
- package/dist/schema-sqlite.d.ts +408 -1
- package/dist/schema-sqlite.js +61 -5
- package/dist/schema-version.d.ts +1 -1
- package/dist/schema-version.js +35 -0
- package/dist/schema.d.ts +2 -2
- package/dist/schema.js +71 -5
- package/package.json +2 -2
- package/src/grants.ts +18 -9
- package/src/hyperdrive.ts +16 -2
- package/src/migrations/postgres/0015_workload_access.sql +43 -0
- package/src/migrations/postgres/0016_spent_tokens.sql +12 -0
- package/src/migrations/postgres/0017_capped_alerts.sql +10 -0
- package/src/migrations/postgres/meta/0015_snapshot.json +3256 -0
- package/src/migrations/postgres/meta/0016_snapshot.json +3256 -0
- package/src/migrations/postgres/meta/0017_snapshot.json +3295 -0
- package/src/migrations/postgres/meta/_journal.json +21 -0
- package/src/migrations/sqlite/0015_workload_access.sql +70 -0
- package/src/migrations/sqlite/0016_capped_alerts.sql +7 -0
- package/src/migrations/sqlite/meta/0015_snapshot.json +2959 -0
- package/src/migrations/sqlite/meta/0016_snapshot.json +2998 -0
- package/src/migrations/sqlite/meta/_journal.json +14 -0
- package/src/schema.sqlite.ts +83 -2
- 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
|
}
|
package/src/schema.sqlite.ts
CHANGED
|
@@ -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}
|
|
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}
|
|
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
|
|