tenseleyflow/shithub / 55b6bb7

Browse files

users/sqlc: queries + generated for user_gpg_keys and user_gpg_subkeys

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
55b6bb7316ee69c5458037d98421a38e746fffdd
Parents
d1ae9af
Tree
7f2299f

21 changed files

StatusFile+-
M internal/actions/sqlc/models.go 35 0
M internal/admin/sqlc/models.go 35 0
M internal/auth/policy/sqlc/models.go 70 0
M internal/billing/sqlc/models.go 35 0
M internal/checks/sqlc/models.go 35 0
M internal/issues/sqlc/models.go 35 0
M internal/meta/sqlc/models.go 35 0
M internal/notif/sqlc/models.go 35 0
M internal/orgs/sqlc/models.go 35 0
M internal/pulls/sqlc/models.go 35 0
M internal/ratelimit/sqlc/models.go 35 0
M internal/repos/sqlc/models.go 35 0
M internal/social/sqlc/models.go 35 0
A internal/users/queries/user_gpg_keys.sql 74 0
A internal/users/queries/user_gpg_subkeys.sql 50 0
M internal/users/sqlc/models.go 35 0
M internal/users/sqlc/querier.go 47 0
A internal/users/sqlc/user_gpg_keys.sql.go 279 0
A internal/users/sqlc/user_gpg_subkeys.sql.go 162 0
M internal/webhook/sqlc/models.go 35 0
M internal/worker/sqlc/models.go 35 0
internal/actions/sqlc/models.gomodified
@@ -2545,6 +2545,41 @@ type UserEmail struct {
25452545
 	CreatedAt             pgtype.Timestamptz
25462546
 }
25472547
 
2548
+type UserGpgKey struct {
2549
+	ID                int64
2550
+	UserID            int64
2551
+	Name              string
2552
+	Fingerprint       string
2553
+	KeyID             string
2554
+	Armored           string
2555
+	CanSign           bool
2556
+	CanEncryptComms   bool
2557
+	CanEncryptStorage bool
2558
+	CanCertify        bool
2559
+	CanAuthenticate   bool
2560
+	Uids              []string
2561
+	Subkeys           []byte
2562
+	PrimaryAlgo       string
2563
+	CreatedAt         pgtype.Timestamptz
2564
+	LastUsedAt        pgtype.Timestamptz
2565
+	RevokedAt         pgtype.Timestamptz
2566
+	ExpiresAt         pgtype.Timestamptz
2567
+}
2568
+
2569
+type UserGpgSubkey struct {
2570
+	ID                int64
2571
+	GpgKeyID          int64
2572
+	Fingerprint       string
2573
+	KeyID             string
2574
+	CanSign           bool
2575
+	CanEncryptComms   bool
2576
+	CanEncryptStorage bool
2577
+	CanCertify        bool
2578
+	ExpiresAt         pgtype.Timestamptz
2579
+	RevokedAt         pgtype.Timestamptz
2580
+	CreatedAt         pgtype.Timestamptz
2581
+}
2582
+
25482583
 type UserNotificationPref struct {
25492584
 	UserID    int64
25502585
 	Key       string
internal/admin/sqlc/models.gomodified
@@ -2545,6 +2545,41 @@ type UserEmail struct {
25452545
 	CreatedAt             pgtype.Timestamptz
25462546
 }
25472547
 
2548
+type UserGpgKey struct {
2549
+	ID                int64
2550
+	UserID            int64
2551
+	Name              string
2552
+	Fingerprint       string
2553
+	KeyID             string
2554
+	Armored           string
2555
+	CanSign           bool
2556
+	CanEncryptComms   bool
2557
+	CanEncryptStorage bool
2558
+	CanCertify        bool
2559
+	CanAuthenticate   bool
2560
+	Uids              []string
2561
+	Subkeys           []byte
2562
+	PrimaryAlgo       string
2563
+	CreatedAt         pgtype.Timestamptz
2564
+	LastUsedAt        pgtype.Timestamptz
2565
+	RevokedAt         pgtype.Timestamptz
2566
+	ExpiresAt         pgtype.Timestamptz
2567
+}
2568
+
2569
+type UserGpgSubkey struct {
2570
+	ID                int64
2571
+	GpgKeyID          int64
2572
+	Fingerprint       string
2573
+	KeyID             string
2574
+	CanSign           bool
2575
+	CanEncryptComms   bool
2576
+	CanEncryptStorage bool
2577
+	CanCertify        bool
2578
+	ExpiresAt         pgtype.Timestamptz
2579
+	RevokedAt         pgtype.Timestamptz
2580
+	CreatedAt         pgtype.Timestamptz
2581
+}
2582
+
25482583
 type UserNotificationPref struct {
25492584
 	UserID    int64
25502585
 	Key       string
internal/auth/policy/sqlc/models.gomodified
@@ -1889,6 +1889,22 @@ type CodeSearchPath struct {
18891889
 	Tsv     interface{}
18901890
 }
18911891
 
1892
+type DeviceAuthorization struct {
1893
+	ID              int64
1894
+	DeviceCodeHash  []byte
1895
+	UserCode        string
1896
+	ClientID        string
1897
+	Scopes          []string
1898
+	UserID          pgtype.Int8
1899
+	ApprovedAt      pgtype.Timestamptz
1900
+	DeniedAt        pgtype.Timestamptz
1901
+	IssuedTokenID   pgtype.Int8
1902
+	IntervalSeconds int32
1903
+	ExpiresAt       pgtype.Timestamptz
1904
+	LastPolledAt    pgtype.Timestamptz
1905
+	CreatedAt       pgtype.Timestamptz
1906
+}
1907
+
18921908
 type DomainEvent struct {
18931909
 	ID          int64
18941910
 	ActorUserID pgtype.Int8
@@ -2529,6 +2545,41 @@ type UserEmail struct {
25292545
 	CreatedAt             pgtype.Timestamptz
25302546
 }
25312547
 
2548
+type UserGpgKey struct {
2549
+	ID                int64
2550
+	UserID            int64
2551
+	Name              string
2552
+	Fingerprint       string
2553
+	KeyID             string
2554
+	Armored           string
2555
+	CanSign           bool
2556
+	CanEncryptComms   bool
2557
+	CanEncryptStorage bool
2558
+	CanCertify        bool
2559
+	CanAuthenticate   bool
2560
+	Uids              []string
2561
+	Subkeys           []byte
2562
+	PrimaryAlgo       string
2563
+	CreatedAt         pgtype.Timestamptz
2564
+	LastUsedAt        pgtype.Timestamptz
2565
+	RevokedAt         pgtype.Timestamptz
2566
+	ExpiresAt         pgtype.Timestamptz
2567
+}
2568
+
2569
+type UserGpgSubkey struct {
2570
+	ID                int64
2571
+	GpgKeyID          int64
2572
+	Fingerprint       string
2573
+	KeyID             string
2574
+	CanSign           bool
2575
+	CanEncryptComms   bool
2576
+	CanEncryptStorage bool
2577
+	CanCertify        bool
2578
+	ExpiresAt         pgtype.Timestamptz
2579
+	RevokedAt         pgtype.Timestamptz
2580
+	CreatedAt         pgtype.Timestamptz
2581
+}
2582
+
25322583
 type UserNotificationPref struct {
25332584
 	UserID    int64
25342585
 	Key       string
@@ -2666,6 +2717,25 @@ type WorkflowArtifact struct {
26662717
 	CreatedAt pgtype.Timestamptz
26672718
 }
26682719
 
2720
+type WorkflowCach struct {
2721
+	ID             int64
2722
+	RepoID         int64
2723
+	CacheKey       string
2724
+	CacheVersion   string
2725
+	GitRef         string
2726
+	ObjectKey      string
2727
+	SizeBytes      int64
2728
+	LastAccessedAt pgtype.Timestamptz
2729
+	CreatedAt      pgtype.Timestamptz
2730
+}
2731
+
2732
+type WorkflowDisabled struct {
2733
+	RepoID           int64
2734
+	WorkflowFile     string
2735
+	DisabledByUserID pgtype.Int8
2736
+	DisabledAt       pgtype.Timestamptz
2737
+}
2738
+
26692739
 type WorkflowJob struct {
26702740
 	ID              int64
26712741
 	RunID           int64
internal/billing/sqlc/models.gomodified
@@ -2545,6 +2545,41 @@ type UserEmail struct {
25452545
 	CreatedAt             pgtype.Timestamptz
25462546
 }
25472547
 
2548
+type UserGpgKey struct {
2549
+	ID                int64
2550
+	UserID            int64
2551
+	Name              string
2552
+	Fingerprint       string
2553
+	KeyID             string
2554
+	Armored           string
2555
+	CanSign           bool
2556
+	CanEncryptComms   bool
2557
+	CanEncryptStorage bool
2558
+	CanCertify        bool
2559
+	CanAuthenticate   bool
2560
+	Uids              []string
2561
+	Subkeys           []byte
2562
+	PrimaryAlgo       string
2563
+	CreatedAt         pgtype.Timestamptz
2564
+	LastUsedAt        pgtype.Timestamptz
2565
+	RevokedAt         pgtype.Timestamptz
2566
+	ExpiresAt         pgtype.Timestamptz
2567
+}
2568
+
2569
+type UserGpgSubkey struct {
2570
+	ID                int64
2571
+	GpgKeyID          int64
2572
+	Fingerprint       string
2573
+	KeyID             string
2574
+	CanSign           bool
2575
+	CanEncryptComms   bool
2576
+	CanEncryptStorage bool
2577
+	CanCertify        bool
2578
+	ExpiresAt         pgtype.Timestamptz
2579
+	RevokedAt         pgtype.Timestamptz
2580
+	CreatedAt         pgtype.Timestamptz
2581
+}
2582
+
25482583
 type UserNotificationPref struct {
25492584
 	UserID    int64
25502585
 	Key       string
internal/checks/sqlc/models.gomodified
@@ -2545,6 +2545,41 @@ type UserEmail struct {
25452545
 	CreatedAt             pgtype.Timestamptz
25462546
 }
25472547
 
2548
+type UserGpgKey struct {
2549
+	ID                int64
2550
+	UserID            int64
2551
+	Name              string
2552
+	Fingerprint       string
2553
+	KeyID             string
2554
+	Armored           string
2555
+	CanSign           bool
2556
+	CanEncryptComms   bool
2557
+	CanEncryptStorage bool
2558
+	CanCertify        bool
2559
+	CanAuthenticate   bool
2560
+	Uids              []string
2561
+	Subkeys           []byte
2562
+	PrimaryAlgo       string
2563
+	CreatedAt         pgtype.Timestamptz
2564
+	LastUsedAt        pgtype.Timestamptz
2565
+	RevokedAt         pgtype.Timestamptz
2566
+	ExpiresAt         pgtype.Timestamptz
2567
+}
2568
+
2569
+type UserGpgSubkey struct {
2570
+	ID                int64
2571
+	GpgKeyID          int64
2572
+	Fingerprint       string
2573
+	KeyID             string
2574
+	CanSign           bool
2575
+	CanEncryptComms   bool
2576
+	CanEncryptStorage bool
2577
+	CanCertify        bool
2578
+	ExpiresAt         pgtype.Timestamptz
2579
+	RevokedAt         pgtype.Timestamptz
2580
+	CreatedAt         pgtype.Timestamptz
2581
+}
2582
+
25482583
 type UserNotificationPref struct {
25492584
 	UserID    int64
25502585
 	Key       string
internal/issues/sqlc/models.gomodified
@@ -2545,6 +2545,41 @@ type UserEmail struct {
25452545
 	CreatedAt             pgtype.Timestamptz
25462546
 }
25472547
 
2548
+type UserGpgKey struct {
2549
+	ID                int64
2550
+	UserID            int64
2551
+	Name              string
2552
+	Fingerprint       string
2553
+	KeyID             string
2554
+	Armored           string
2555
+	CanSign           bool
2556
+	CanEncryptComms   bool
2557
+	CanEncryptStorage bool
2558
+	CanCertify        bool
2559
+	CanAuthenticate   bool
2560
+	Uids              []string
2561
+	Subkeys           []byte
2562
+	PrimaryAlgo       string
2563
+	CreatedAt         pgtype.Timestamptz
2564
+	LastUsedAt        pgtype.Timestamptz
2565
+	RevokedAt         pgtype.Timestamptz
2566
+	ExpiresAt         pgtype.Timestamptz
2567
+}
2568
+
2569
+type UserGpgSubkey struct {
2570
+	ID                int64
2571
+	GpgKeyID          int64
2572
+	Fingerprint       string
2573
+	KeyID             string
2574
+	CanSign           bool
2575
+	CanEncryptComms   bool
2576
+	CanEncryptStorage bool
2577
+	CanCertify        bool
2578
+	ExpiresAt         pgtype.Timestamptz
2579
+	RevokedAt         pgtype.Timestamptz
2580
+	CreatedAt         pgtype.Timestamptz
2581
+}
2582
+
25482583
 type UserNotificationPref struct {
25492584
 	UserID    int64
25502585
 	Key       string
internal/meta/sqlc/models.gomodified
@@ -2545,6 +2545,41 @@ type UserEmail struct {
25452545
 	CreatedAt             pgtype.Timestamptz
25462546
 }
25472547
 
2548
+type UserGpgKey struct {
2549
+	ID                int64
2550
+	UserID            int64
2551
+	Name              string
2552
+	Fingerprint       string
2553
+	KeyID             string
2554
+	Armored           string
2555
+	CanSign           bool
2556
+	CanEncryptComms   bool
2557
+	CanEncryptStorage bool
2558
+	CanCertify        bool
2559
+	CanAuthenticate   bool
2560
+	Uids              []string
2561
+	Subkeys           []byte
2562
+	PrimaryAlgo       string
2563
+	CreatedAt         pgtype.Timestamptz
2564
+	LastUsedAt        pgtype.Timestamptz
2565
+	RevokedAt         pgtype.Timestamptz
2566
+	ExpiresAt         pgtype.Timestamptz
2567
+}
2568
+
2569
+type UserGpgSubkey struct {
2570
+	ID                int64
2571
+	GpgKeyID          int64
2572
+	Fingerprint       string
2573
+	KeyID             string
2574
+	CanSign           bool
2575
+	CanEncryptComms   bool
2576
+	CanEncryptStorage bool
2577
+	CanCertify        bool
2578
+	ExpiresAt         pgtype.Timestamptz
2579
+	RevokedAt         pgtype.Timestamptz
2580
+	CreatedAt         pgtype.Timestamptz
2581
+}
2582
+
25482583
 type UserNotificationPref struct {
25492584
 	UserID    int64
25502585
 	Key       string
internal/notif/sqlc/models.gomodified
@@ -2545,6 +2545,41 @@ type UserEmail struct {
25452545
 	CreatedAt             pgtype.Timestamptz
25462546
 }
25472547
 
2548
+type UserGpgKey struct {
2549
+	ID                int64
2550
+	UserID            int64
2551
+	Name              string
2552
+	Fingerprint       string
2553
+	KeyID             string
2554
+	Armored           string
2555
+	CanSign           bool
2556
+	CanEncryptComms   bool
2557
+	CanEncryptStorage bool
2558
+	CanCertify        bool
2559
+	CanAuthenticate   bool
2560
+	Uids              []string
2561
+	Subkeys           []byte
2562
+	PrimaryAlgo       string
2563
+	CreatedAt         pgtype.Timestamptz
2564
+	LastUsedAt        pgtype.Timestamptz
2565
+	RevokedAt         pgtype.Timestamptz
2566
+	ExpiresAt         pgtype.Timestamptz
2567
+}
2568
+
2569
+type UserGpgSubkey struct {
2570
+	ID                int64
2571
+	GpgKeyID          int64
2572
+	Fingerprint       string
2573
+	KeyID             string
2574
+	CanSign           bool
2575
+	CanEncryptComms   bool
2576
+	CanEncryptStorage bool
2577
+	CanCertify        bool
2578
+	ExpiresAt         pgtype.Timestamptz
2579
+	RevokedAt         pgtype.Timestamptz
2580
+	CreatedAt         pgtype.Timestamptz
2581
+}
2582
+
25482583
 type UserNotificationPref struct {
25492584
 	UserID    int64
25502585
 	Key       string
internal/orgs/sqlc/models.gomodified
@@ -2545,6 +2545,41 @@ type UserEmail struct {
25452545
 	CreatedAt             pgtype.Timestamptz
25462546
 }
25472547
 
2548
+type UserGpgKey struct {
2549
+	ID                int64
2550
+	UserID            int64
2551
+	Name              string
2552
+	Fingerprint       string
2553
+	KeyID             string
2554
+	Armored           string
2555
+	CanSign           bool
2556
+	CanEncryptComms   bool
2557
+	CanEncryptStorage bool
2558
+	CanCertify        bool
2559
+	CanAuthenticate   bool
2560
+	Uids              []string
2561
+	Subkeys           []byte
2562
+	PrimaryAlgo       string
2563
+	CreatedAt         pgtype.Timestamptz
2564
+	LastUsedAt        pgtype.Timestamptz
2565
+	RevokedAt         pgtype.Timestamptz
2566
+	ExpiresAt         pgtype.Timestamptz
2567
+}
2568
+
2569
+type UserGpgSubkey struct {
2570
+	ID                int64
2571
+	GpgKeyID          int64
2572
+	Fingerprint       string
2573
+	KeyID             string
2574
+	CanSign           bool
2575
+	CanEncryptComms   bool
2576
+	CanEncryptStorage bool
2577
+	CanCertify        bool
2578
+	ExpiresAt         pgtype.Timestamptz
2579
+	RevokedAt         pgtype.Timestamptz
2580
+	CreatedAt         pgtype.Timestamptz
2581
+}
2582
+
25482583
 type UserNotificationPref struct {
25492584
 	UserID    int64
25502585
 	Key       string
internal/pulls/sqlc/models.gomodified
@@ -2545,6 +2545,41 @@ type UserEmail struct {
25452545
 	CreatedAt             pgtype.Timestamptz
25462546
 }
25472547
 
2548
+type UserGpgKey struct {
2549
+	ID                int64
2550
+	UserID            int64
2551
+	Name              string
2552
+	Fingerprint       string
2553
+	KeyID             string
2554
+	Armored           string
2555
+	CanSign           bool
2556
+	CanEncryptComms   bool
2557
+	CanEncryptStorage bool
2558
+	CanCertify        bool
2559
+	CanAuthenticate   bool
2560
+	Uids              []string
2561
+	Subkeys           []byte
2562
+	PrimaryAlgo       string
2563
+	CreatedAt         pgtype.Timestamptz
2564
+	LastUsedAt        pgtype.Timestamptz
2565
+	RevokedAt         pgtype.Timestamptz
2566
+	ExpiresAt         pgtype.Timestamptz
2567
+}
2568
+
2569
+type UserGpgSubkey struct {
2570
+	ID                int64
2571
+	GpgKeyID          int64
2572
+	Fingerprint       string
2573
+	KeyID             string
2574
+	CanSign           bool
2575
+	CanEncryptComms   bool
2576
+	CanEncryptStorage bool
2577
+	CanCertify        bool
2578
+	ExpiresAt         pgtype.Timestamptz
2579
+	RevokedAt         pgtype.Timestamptz
2580
+	CreatedAt         pgtype.Timestamptz
2581
+}
2582
+
25482583
 type UserNotificationPref struct {
25492584
 	UserID    int64
25502585
 	Key       string
internal/ratelimit/sqlc/models.gomodified
@@ -2545,6 +2545,41 @@ type UserEmail struct {
25452545
 	CreatedAt             pgtype.Timestamptz
25462546
 }
25472547
 
2548
+type UserGpgKey struct {
2549
+	ID                int64
2550
+	UserID            int64
2551
+	Name              string
2552
+	Fingerprint       string
2553
+	KeyID             string
2554
+	Armored           string
2555
+	CanSign           bool
2556
+	CanEncryptComms   bool
2557
+	CanEncryptStorage bool
2558
+	CanCertify        bool
2559
+	CanAuthenticate   bool
2560
+	Uids              []string
2561
+	Subkeys           []byte
2562
+	PrimaryAlgo       string
2563
+	CreatedAt         pgtype.Timestamptz
2564
+	LastUsedAt        pgtype.Timestamptz
2565
+	RevokedAt         pgtype.Timestamptz
2566
+	ExpiresAt         pgtype.Timestamptz
2567
+}
2568
+
2569
+type UserGpgSubkey struct {
2570
+	ID                int64
2571
+	GpgKeyID          int64
2572
+	Fingerprint       string
2573
+	KeyID             string
2574
+	CanSign           bool
2575
+	CanEncryptComms   bool
2576
+	CanEncryptStorage bool
2577
+	CanCertify        bool
2578
+	ExpiresAt         pgtype.Timestamptz
2579
+	RevokedAt         pgtype.Timestamptz
2580
+	CreatedAt         pgtype.Timestamptz
2581
+}
2582
+
25482583
 type UserNotificationPref struct {
25492584
 	UserID    int64
25502585
 	Key       string
internal/repos/sqlc/models.gomodified
@@ -2545,6 +2545,41 @@ type UserEmail struct {
25452545
 	CreatedAt             pgtype.Timestamptz
25462546
 }
25472547
 
2548
+type UserGpgKey struct {
2549
+	ID                int64
2550
+	UserID            int64
2551
+	Name              string
2552
+	Fingerprint       string
2553
+	KeyID             string
2554
+	Armored           string
2555
+	CanSign           bool
2556
+	CanEncryptComms   bool
2557
+	CanEncryptStorage bool
2558
+	CanCertify        bool
2559
+	CanAuthenticate   bool
2560
+	Uids              []string
2561
+	Subkeys           []byte
2562
+	PrimaryAlgo       string
2563
+	CreatedAt         pgtype.Timestamptz
2564
+	LastUsedAt        pgtype.Timestamptz
2565
+	RevokedAt         pgtype.Timestamptz
2566
+	ExpiresAt         pgtype.Timestamptz
2567
+}
2568
+
2569
+type UserGpgSubkey struct {
2570
+	ID                int64
2571
+	GpgKeyID          int64
2572
+	Fingerprint       string
2573
+	KeyID             string
2574
+	CanSign           bool
2575
+	CanEncryptComms   bool
2576
+	CanEncryptStorage bool
2577
+	CanCertify        bool
2578
+	ExpiresAt         pgtype.Timestamptz
2579
+	RevokedAt         pgtype.Timestamptz
2580
+	CreatedAt         pgtype.Timestamptz
2581
+}
2582
+
25482583
 type UserNotificationPref struct {
25492584
 	UserID    int64
25502585
 	Key       string
internal/social/sqlc/models.gomodified
@@ -2545,6 +2545,41 @@ type UserEmail struct {
25452545
 	CreatedAt             pgtype.Timestamptz
25462546
 }
25472547
 
2548
+type UserGpgKey struct {
2549
+	ID                int64
2550
+	UserID            int64
2551
+	Name              string
2552
+	Fingerprint       string
2553
+	KeyID             string
2554
+	Armored           string
2555
+	CanSign           bool
2556
+	CanEncryptComms   bool
2557
+	CanEncryptStorage bool
2558
+	CanCertify        bool
2559
+	CanAuthenticate   bool
2560
+	Uids              []string
2561
+	Subkeys           []byte
2562
+	PrimaryAlgo       string
2563
+	CreatedAt         pgtype.Timestamptz
2564
+	LastUsedAt        pgtype.Timestamptz
2565
+	RevokedAt         pgtype.Timestamptz
2566
+	ExpiresAt         pgtype.Timestamptz
2567
+}
2568
+
2569
+type UserGpgSubkey struct {
2570
+	ID                int64
2571
+	GpgKeyID          int64
2572
+	Fingerprint       string
2573
+	KeyID             string
2574
+	CanSign           bool
2575
+	CanEncryptComms   bool
2576
+	CanEncryptStorage bool
2577
+	CanCertify        bool
2578
+	ExpiresAt         pgtype.Timestamptz
2579
+	RevokedAt         pgtype.Timestamptz
2580
+	CreatedAt         pgtype.Timestamptz
2581
+}
2582
+
25482583
 type UserNotificationPref struct {
25492584
 	UserID    int64
25502585
 	Key       string
internal/users/queries/user_gpg_keys.sqladded
@@ -0,0 +1,74 @@
1
+-- SPDX-License-Identifier: AGPL-3.0-or-later
2
+
3
+-- name: InsertUserGPGKey :one
4
+-- Inserts a parsed primary GPG key. Subkeys land in user_gpg_subkeys
5
+-- in the same transaction (see InsertUserGPGSubkey). expires_at is
6
+-- nullable; many keys have no expiration. revoked_at stays NULL on
7
+-- insert; soft-delete sets it.
8
+INSERT INTO user_gpg_keys (
9
+    user_id, name, fingerprint, key_id, armored,
10
+    can_sign, can_encrypt_comms, can_encrypt_storage, can_certify, can_authenticate,
11
+    uids, subkeys, primary_algo, expires_at
12
+)
13
+VALUES (
14
+    $1, $2, $3, $4, $5,
15
+    $6, $7, $8, $9, $10,
16
+    $11, $12, $13, $14
17
+)
18
+RETURNING id, user_id, name, fingerprint, key_id, armored,
19
+          can_sign, can_encrypt_comms, can_encrypt_storage, can_certify, can_authenticate,
20
+          uids, subkeys, primary_algo,
21
+          created_at, last_used_at, revoked_at, expires_at;
22
+
23
+-- name: ListUserGPGKeys :many
24
+-- Paginated list for the REST surface; HTML settings page reuses with
25
+-- a generous limit and no offset.
26
+SELECT id, user_id, name, fingerprint, key_id, armored,
27
+       can_sign, can_encrypt_comms, can_encrypt_storage, can_certify, can_authenticate,
28
+       uids, subkeys, primary_algo,
29
+       created_at, last_used_at, revoked_at, expires_at
30
+FROM user_gpg_keys
31
+WHERE user_id = $1 AND revoked_at IS NULL
32
+ORDER BY created_at DESC
33
+LIMIT $2 OFFSET $3;
34
+
35
+-- name: CountUserGPGKeys :one
36
+-- Excludes revoked rows so the per-user cap (100) counts live keys.
37
+SELECT count(*) FROM user_gpg_keys WHERE user_id = $1 AND revoked_at IS NULL;
38
+
39
+-- name: GetUserGPGKey :one
40
+-- Scoped single-key lookup for REST GET-by-id. user_id filter prevents
41
+-- cross-user reads (existence-leak-safe: returns no row if the id
42
+-- belongs to another user).
43
+SELECT id, user_id, name, fingerprint, key_id, armored,
44
+       can_sign, can_encrypt_comms, can_encrypt_storage, can_certify, can_authenticate,
45
+       uids, subkeys, primary_algo,
46
+       created_at, last_used_at, revoked_at, expires_at
47
+FROM user_gpg_keys
48
+WHERE id = $1 AND user_id = $2;
49
+
50
+-- name: GetUserGPGKeyByFingerprint :one
51
+-- Uniqueness probe used by the add path to surface a friendly
52
+-- "this key is already registered" error before the unique index
53
+-- violation. Returns any row matching the fingerprint regardless of
54
+-- which user owns it (global uniqueness is the contract).
55
+SELECT id, user_id, name, fingerprint, key_id, armored,
56
+       can_sign, can_encrypt_comms, can_encrypt_storage, can_certify, can_authenticate,
57
+       uids, subkeys, primary_algo,
58
+       created_at, last_used_at, revoked_at, expires_at
59
+FROM user_gpg_keys
60
+WHERE fingerprint = $1 AND revoked_at IS NULL;
61
+
62
+-- name: SoftDeleteUserGPGKey :execrows
63
+-- Scoped soft-delete: stamps revoked_at, preserves the row for audit
64
+-- continuity. Returns the number of rows affected so the handler can
65
+-- distinguish "not found" from "deleted" without a follow-up query.
66
+UPDATE user_gpg_keys
67
+SET revoked_at = now()
68
+WHERE id = $1 AND user_id = $2 AND revoked_at IS NULL;
69
+
70
+-- name: TouchUserGPGKeyLastUsed :exec
71
+-- Best-effort last-used stamp called from the verification path when
72
+-- a signature successfully resolves to this key. No timeout / error
73
+-- propagation; the caller fires-and-forgets via a goroutine.
74
+UPDATE user_gpg_keys SET last_used_at = now() WHERE id = $1;
internal/users/queries/user_gpg_subkeys.sqladded
@@ -0,0 +1,50 @@
1
+-- SPDX-License-Identifier: AGPL-3.0-or-later
2
+
3
+-- name: InsertUserGPGSubkey :one
4
+-- One row per subkey of a primary key. Always inserted in the same
5
+-- transaction as the parent InsertUserGPGKey so the verification
6
+-- hot path's fingerprint lookup is consistent with the REST nested
7
+-- shape.
8
+INSERT INTO user_gpg_subkeys (
9
+    gpg_key_id, fingerprint, key_id,
10
+    can_sign, can_encrypt_comms, can_encrypt_storage, can_certify,
11
+    expires_at
12
+)
13
+VALUES (
14
+    $1, $2, $3,
15
+    $4, $5, $6, $7,
16
+    $8
17
+)
18
+RETURNING id, gpg_key_id, fingerprint, key_id,
19
+          can_sign, can_encrypt_comms, can_encrypt_storage, can_certify,
20
+          expires_at, revoked_at, created_at;
21
+
22
+-- name: GetUserGPGSubkeyByFingerprint :one
23
+-- Hot path for commit/tag signature verification. The signature
24
+-- packet carries the signing subkey's fingerprint; this query
25
+-- resolves it back to the primary key (and via FK to the user).
26
+-- Index lookup via the partial unique index.
27
+SELECT id, gpg_key_id, fingerprint, key_id,
28
+       can_sign, can_encrypt_comms, can_encrypt_storage, can_certify,
29
+       expires_at, revoked_at, created_at
30
+FROM user_gpg_subkeys
31
+WHERE fingerprint = $1 AND revoked_at IS NULL;
32
+
33
+-- name: ListSubkeysForGPGKey :many
34
+-- Reads all live subkeys for one primary; used when invalidating the
35
+-- verification cache on primary soft-delete (every dependent subkey
36
+-- needs its cache rows stamped invalidated too).
37
+SELECT id, gpg_key_id, fingerprint, key_id,
38
+       can_sign, can_encrypt_comms, can_encrypt_storage, can_certify,
39
+       expires_at, revoked_at, created_at
40
+FROM user_gpg_subkeys
41
+WHERE gpg_key_id = $1
42
+ORDER BY id;
43
+
44
+-- name: SoftDeleteSubkeysForGPGKey :exec
45
+-- Stamps revoked_at on every live subkey of a primary. Called in the
46
+-- same transaction as SoftDeleteUserGPGKey so the partial unique index
47
+-- frees up the fingerprint for re-upload if the user rotates.
48
+UPDATE user_gpg_subkeys
49
+SET revoked_at = now()
50
+WHERE gpg_key_id = $1 AND revoked_at IS NULL;
internal/users/sqlc/models.gomodified
@@ -2545,6 +2545,41 @@ type UserEmail struct {
25452545
 	CreatedAt             pgtype.Timestamptz
25462546
 }
25472547
 
2548
+type UserGpgKey struct {
2549
+	ID                int64
2550
+	UserID            int64
2551
+	Name              string
2552
+	Fingerprint       string
2553
+	KeyID             string
2554
+	Armored           string
2555
+	CanSign           bool
2556
+	CanEncryptComms   bool
2557
+	CanEncryptStorage bool
2558
+	CanCertify        bool
2559
+	CanAuthenticate   bool
2560
+	Uids              []string
2561
+	Subkeys           []byte
2562
+	PrimaryAlgo       string
2563
+	CreatedAt         pgtype.Timestamptz
2564
+	LastUsedAt        pgtype.Timestamptz
2565
+	RevokedAt         pgtype.Timestamptz
2566
+	ExpiresAt         pgtype.Timestamptz
2567
+}
2568
+
2569
+type UserGpgSubkey struct {
2570
+	ID                int64
2571
+	GpgKeyID          int64
2572
+	Fingerprint       string
2573
+	KeyID             string
2574
+	CanSign           bool
2575
+	CanEncryptComms   bool
2576
+	CanEncryptStorage bool
2577
+	CanCertify        bool
2578
+	ExpiresAt         pgtype.Timestamptz
2579
+	RevokedAt         pgtype.Timestamptz
2580
+	CreatedAt         pgtype.Timestamptz
2581
+}
2582
+
25482583
 type UserNotificationPref struct {
25492584
 	UserID    int64
25502585
 	Key       string
internal/users/sqlc/querier.gomodified
@@ -39,6 +39,8 @@ type Querier interface {
3939
 	// Drives the 3-changes-per-60d cap.
4040
 	CountRecentUsernameChanges(ctx context.Context, db DBTX, arg CountRecentUsernameChangesParams) (int64, error)
4141
 	CountUnusedRecoveryCodes(ctx context.Context, db DBTX, userID int64) (int64, error)
42
+	// Excludes revoked rows so the per-user cap (100) counts live keys.
43
+	CountUserGPGKeys(ctx context.Context, db DBTX, userID int64) (int64, error)
4244
 	CountUserSSHKeys(ctx context.Context, db DBTX, userID int64) (int64, error)
4345
 	CountUserSSHKeysByKind(ctx context.Context, db DBTX, arg CountUserSSHKeysByKindParams) (int64, error)
4446
 	CountUsers(ctx context.Context, db DBTX) (int64, error)
@@ -81,6 +83,20 @@ type Querier interface {
8183
 	GetUserEmailByAddress(ctx context.Context, db DBTX, email string) (UserEmail, error)
8284
 	GetUserEmailByID(ctx context.Context, db DBTX, id int64) (UserEmail, error)
8385
 	GetUserEmailByVerificationHash(ctx context.Context, db DBTX, verificationTokenHash []byte) (UserEmail, error)
86
+	// Scoped single-key lookup for REST GET-by-id. user_id filter prevents
87
+	// cross-user reads (existence-leak-safe: returns no row if the id
88
+	// belongs to another user).
89
+	GetUserGPGKey(ctx context.Context, db DBTX, arg GetUserGPGKeyParams) (UserGpgKey, error)
90
+	// Uniqueness probe used by the add path to surface a friendly
91
+	// "this key is already registered" error before the unique index
92
+	// violation. Returns any row matching the fingerprint regardless of
93
+	// which user owns it (global uniqueness is the contract).
94
+	GetUserGPGKeyByFingerprint(ctx context.Context, db DBTX, fingerprint string) (UserGpgKey, error)
95
+	// Hot path for commit/tag signature verification. The signature
96
+	// packet carries the signing subkey's fingerprint; this query
97
+	// resolves it back to the primary key (and via FK to the user).
98
+	// Index lookup via the partial unique index.
99
+	GetUserGPGSubkeyByFingerprint(ctx context.Context, db DBTX, fingerprint string) (UserGpgSubkey, error)
84100
 	// Like GetUserByID but returns the row even when deleted_at IS NOT NULL.
85101
 	GetUserIncludingDeleted(ctx context.Context, db DBTX, id int64) (User, error)
86102
 	// Single-key lookup for the REST GET-by-id endpoint. user_id filter so
@@ -102,6 +118,18 @@ type Querier interface {
102118
 	// SPDX-License-Identifier: AGPL-3.0-or-later
103119
 	InsertRecoveryCode(ctx context.Context, db DBTX, arg InsertRecoveryCodeParams) error
104120
 	// SPDX-License-Identifier: AGPL-3.0-or-later
121
+	// Inserts a parsed primary GPG key. Subkeys land in user_gpg_subkeys
122
+	// in the same transaction (see InsertUserGPGSubkey). expires_at is
123
+	// nullable; many keys have no expiration. revoked_at stays NULL on
124
+	// insert; soft-delete sets it.
125
+	InsertUserGPGKey(ctx context.Context, db DBTX, arg InsertUserGPGKeyParams) (UserGpgKey, error)
126
+	// SPDX-License-Identifier: AGPL-3.0-or-later
127
+	// One row per subkey of a primary key. Always inserted in the same
128
+	// transaction as the parent InsertUserGPGKey so the verification
129
+	// hot path's fingerprint lookup is consistent with the REST nested
130
+	// shape.
131
+	InsertUserGPGSubkey(ctx context.Context, db DBTX, arg InsertUserGPGSubkeyParams) (UserGpgSubkey, error)
132
+	// SPDX-License-Identifier: AGPL-3.0-or-later
105133
 	InsertUserSSHKey(ctx context.Context, db DBTX, arg InsertUserSSHKeyParams) (UserSshKey, error)
106134
 	// SPDX-License-Identifier: AGPL-3.0-or-later
107135
 	InsertUserToken(ctx context.Context, db DBTX, arg InsertUserTokenParams) (UserToken, error)
@@ -113,7 +141,14 @@ type Querier interface {
113141
 	// MarkUserEmailPrimaryVerified after the user clicks the verification link.
114142
 	LinkUserPrimaryEmail(ctx context.Context, db DBTX, arg LinkUserPrimaryEmailParams) error
115143
 	ListAuditLogForTarget(ctx context.Context, db DBTX, arg ListAuditLogForTargetParams) ([]AuthAuditLog, error)
144
+	// Reads all live subkeys for one primary; used when invalidating the
145
+	// verification cache on primary soft-delete (every dependent subkey
146
+	// needs its cache rows stamped invalidated too).
147
+	ListSubkeysForGPGKey(ctx context.Context, db DBTX, gpgKeyID int64) ([]UserGpgSubkey, error)
116148
 	ListUserEmailsForUser(ctx context.Context, db DBTX, userID int64) ([]UserEmail, error)
149
+	// Paginated list for the REST surface; HTML settings page reuses with
150
+	// a generous limit and no offset.
151
+	ListUserGPGKeys(ctx context.Context, db DBTX, arg ListUserGPGKeysParams) ([]UserGpgKey, error)
117152
 	// SPDX-License-Identifier: AGPL-3.0-or-later
118153
 	ListUserNotificationPrefs(ctx context.Context, db DBTX, userID int64) ([]UserNotificationPref, error)
119154
 	ListUserSSHKeys(ctx context.Context, db DBTX, userID int64) ([]UserSshKey, error)
@@ -148,10 +183,22 @@ type Querier interface {
148183
 	// user and is verified.
149184
 	SetUserEmailPrimary(ctx context.Context, db DBTX, arg SetUserEmailPrimaryParams) error
150185
 	SetVerificationToken(ctx context.Context, db DBTX, arg SetVerificationTokenParams) error
186
+	// Stamps revoked_at on every live subkey of a primary. Called in the
187
+	// same transaction as SoftDeleteUserGPGKey so the partial unique index
188
+	// frees up the fingerprint for re-upload if the user rotates.
189
+	SoftDeleteSubkeysForGPGKey(ctx context.Context, db DBTX, gpgKeyID int64) error
151190
 	SoftDeleteUser(ctx context.Context, db DBTX, id int64) error
191
+	// Scoped soft-delete: stamps revoked_at, preserves the row for audit
192
+	// continuity. Returns the number of rows affected so the handler can
193
+	// distinguish "not found" from "deleted" without a follow-up query.
194
+	SoftDeleteUserGPGKey(ctx context.Context, db DBTX, arg SoftDeleteUserGPGKeyParams) (int64, error)
152195
 	SuspendUser(ctx context.Context, db DBTX, arg SuspendUserParams) error
153196
 	TouchDeviceAuthorizationPoll(ctx context.Context, db DBTX, id int64) error
154197
 	TouchSSHKeyLastUsed(ctx context.Context, db DBTX, arg TouchSSHKeyLastUsedParams) error
198
+	// Best-effort last-used stamp called from the verification path when
199
+	// a signature successfully resolves to this key. No timeout / error
200
+	// propagation; the caller fires-and-forgets via a goroutine.
201
+	TouchUserGPGKeyLastUsed(ctx context.Context, db DBTX, id int64) error
155202
 	TouchUserLastLogin(ctx context.Context, db DBTX, id int64) error
156203
 	TouchUserTokenLastUsed(ctx context.Context, db DBTX, arg TouchUserTokenLastUsedParams) error
157204
 	// Clears the suspended state. Mirrors SuspendUser; used by the
internal/users/sqlc/user_gpg_keys.sql.goadded
@@ -0,0 +1,279 @@
1
+// Code generated by sqlc. DO NOT EDIT.
2
+// versions:
3
+//   sqlc v1.31.1
4
+// source: user_gpg_keys.sql
5
+
6
+package usersdb
7
+
8
+import (
9
+	"context"
10
+
11
+	"github.com/jackc/pgx/v5/pgtype"
12
+)
13
+
14
+const countUserGPGKeys = `-- name: CountUserGPGKeys :one
15
+SELECT count(*) FROM user_gpg_keys WHERE user_id = $1 AND revoked_at IS NULL
16
+`
17
+
18
+// Excludes revoked rows so the per-user cap (100) counts live keys.
19
+func (q *Queries) CountUserGPGKeys(ctx context.Context, db DBTX, userID int64) (int64, error) {
20
+	row := db.QueryRow(ctx, countUserGPGKeys, userID)
21
+	var count int64
22
+	err := row.Scan(&count)
23
+	return count, err
24
+}
25
+
26
+const getUserGPGKey = `-- name: GetUserGPGKey :one
27
+SELECT id, user_id, name, fingerprint, key_id, armored,
28
+       can_sign, can_encrypt_comms, can_encrypt_storage, can_certify, can_authenticate,
29
+       uids, subkeys, primary_algo,
30
+       created_at, last_used_at, revoked_at, expires_at
31
+FROM user_gpg_keys
32
+WHERE id = $1 AND user_id = $2
33
+`
34
+
35
+type GetUserGPGKeyParams struct {
36
+	ID     int64
37
+	UserID int64
38
+}
39
+
40
+// Scoped single-key lookup for REST GET-by-id. user_id filter prevents
41
+// cross-user reads (existence-leak-safe: returns no row if the id
42
+// belongs to another user).
43
+func (q *Queries) GetUserGPGKey(ctx context.Context, db DBTX, arg GetUserGPGKeyParams) (UserGpgKey, error) {
44
+	row := db.QueryRow(ctx, getUserGPGKey, arg.ID, arg.UserID)
45
+	var i UserGpgKey
46
+	err := row.Scan(
47
+		&i.ID,
48
+		&i.UserID,
49
+		&i.Name,
50
+		&i.Fingerprint,
51
+		&i.KeyID,
52
+		&i.Armored,
53
+		&i.CanSign,
54
+		&i.CanEncryptComms,
55
+		&i.CanEncryptStorage,
56
+		&i.CanCertify,
57
+		&i.CanAuthenticate,
58
+		&i.Uids,
59
+		&i.Subkeys,
60
+		&i.PrimaryAlgo,
61
+		&i.CreatedAt,
62
+		&i.LastUsedAt,
63
+		&i.RevokedAt,
64
+		&i.ExpiresAt,
65
+	)
66
+	return i, err
67
+}
68
+
69
+const getUserGPGKeyByFingerprint = `-- name: GetUserGPGKeyByFingerprint :one
70
+SELECT id, user_id, name, fingerprint, key_id, armored,
71
+       can_sign, can_encrypt_comms, can_encrypt_storage, can_certify, can_authenticate,
72
+       uids, subkeys, primary_algo,
73
+       created_at, last_used_at, revoked_at, expires_at
74
+FROM user_gpg_keys
75
+WHERE fingerprint = $1 AND revoked_at IS NULL
76
+`
77
+
78
+// Uniqueness probe used by the add path to surface a friendly
79
+// "this key is already registered" error before the unique index
80
+// violation. Returns any row matching the fingerprint regardless of
81
+// which user owns it (global uniqueness is the contract).
82
+func (q *Queries) GetUserGPGKeyByFingerprint(ctx context.Context, db DBTX, fingerprint string) (UserGpgKey, error) {
83
+	row := db.QueryRow(ctx, getUserGPGKeyByFingerprint, fingerprint)
84
+	var i UserGpgKey
85
+	err := row.Scan(
86
+		&i.ID,
87
+		&i.UserID,
88
+		&i.Name,
89
+		&i.Fingerprint,
90
+		&i.KeyID,
91
+		&i.Armored,
92
+		&i.CanSign,
93
+		&i.CanEncryptComms,
94
+		&i.CanEncryptStorage,
95
+		&i.CanCertify,
96
+		&i.CanAuthenticate,
97
+		&i.Uids,
98
+		&i.Subkeys,
99
+		&i.PrimaryAlgo,
100
+		&i.CreatedAt,
101
+		&i.LastUsedAt,
102
+		&i.RevokedAt,
103
+		&i.ExpiresAt,
104
+	)
105
+	return i, err
106
+}
107
+
108
+const insertUserGPGKey = `-- name: InsertUserGPGKey :one
109
+
110
+INSERT INTO user_gpg_keys (
111
+    user_id, name, fingerprint, key_id, armored,
112
+    can_sign, can_encrypt_comms, can_encrypt_storage, can_certify, can_authenticate,
113
+    uids, subkeys, primary_algo, expires_at
114
+)
115
+VALUES (
116
+    $1, $2, $3, $4, $5,
117
+    $6, $7, $8, $9, $10,
118
+    $11, $12, $13, $14
119
+)
120
+RETURNING id, user_id, name, fingerprint, key_id, armored,
121
+          can_sign, can_encrypt_comms, can_encrypt_storage, can_certify, can_authenticate,
122
+          uids, subkeys, primary_algo,
123
+          created_at, last_used_at, revoked_at, expires_at
124
+`
125
+
126
+type InsertUserGPGKeyParams struct {
127
+	UserID            int64
128
+	Name              string
129
+	Fingerprint       string
130
+	KeyID             string
131
+	Armored           string
132
+	CanSign           bool
133
+	CanEncryptComms   bool
134
+	CanEncryptStorage bool
135
+	CanCertify        bool
136
+	CanAuthenticate   bool
137
+	Uids              []string
138
+	Subkeys           []byte
139
+	PrimaryAlgo       string
140
+	ExpiresAt         pgtype.Timestamptz
141
+}
142
+
143
+// SPDX-License-Identifier: AGPL-3.0-or-later
144
+// Inserts a parsed primary GPG key. Subkeys land in user_gpg_subkeys
145
+// in the same transaction (see InsertUserGPGSubkey). expires_at is
146
+// nullable; many keys have no expiration. revoked_at stays NULL on
147
+// insert; soft-delete sets it.
148
+func (q *Queries) InsertUserGPGKey(ctx context.Context, db DBTX, arg InsertUserGPGKeyParams) (UserGpgKey, error) {
149
+	row := db.QueryRow(ctx, insertUserGPGKey,
150
+		arg.UserID,
151
+		arg.Name,
152
+		arg.Fingerprint,
153
+		arg.KeyID,
154
+		arg.Armored,
155
+		arg.CanSign,
156
+		arg.CanEncryptComms,
157
+		arg.CanEncryptStorage,
158
+		arg.CanCertify,
159
+		arg.CanAuthenticate,
160
+		arg.Uids,
161
+		arg.Subkeys,
162
+		arg.PrimaryAlgo,
163
+		arg.ExpiresAt,
164
+	)
165
+	var i UserGpgKey
166
+	err := row.Scan(
167
+		&i.ID,
168
+		&i.UserID,
169
+		&i.Name,
170
+		&i.Fingerprint,
171
+		&i.KeyID,
172
+		&i.Armored,
173
+		&i.CanSign,
174
+		&i.CanEncryptComms,
175
+		&i.CanEncryptStorage,
176
+		&i.CanCertify,
177
+		&i.CanAuthenticate,
178
+		&i.Uids,
179
+		&i.Subkeys,
180
+		&i.PrimaryAlgo,
181
+		&i.CreatedAt,
182
+		&i.LastUsedAt,
183
+		&i.RevokedAt,
184
+		&i.ExpiresAt,
185
+	)
186
+	return i, err
187
+}
188
+
189
+const listUserGPGKeys = `-- name: ListUserGPGKeys :many
190
+SELECT id, user_id, name, fingerprint, key_id, armored,
191
+       can_sign, can_encrypt_comms, can_encrypt_storage, can_certify, can_authenticate,
192
+       uids, subkeys, primary_algo,
193
+       created_at, last_used_at, revoked_at, expires_at
194
+FROM user_gpg_keys
195
+WHERE user_id = $1 AND revoked_at IS NULL
196
+ORDER BY created_at DESC
197
+LIMIT $2 OFFSET $3
198
+`
199
+
200
+type ListUserGPGKeysParams struct {
201
+	UserID int64
202
+	Limit  int32
203
+	Offset int32
204
+}
205
+
206
+// Paginated list for the REST surface; HTML settings page reuses with
207
+// a generous limit and no offset.
208
+func (q *Queries) ListUserGPGKeys(ctx context.Context, db DBTX, arg ListUserGPGKeysParams) ([]UserGpgKey, error) {
209
+	rows, err := db.Query(ctx, listUserGPGKeys, arg.UserID, arg.Limit, arg.Offset)
210
+	if err != nil {
211
+		return nil, err
212
+	}
213
+	defer rows.Close()
214
+	items := []UserGpgKey{}
215
+	for rows.Next() {
216
+		var i UserGpgKey
217
+		if err := rows.Scan(
218
+			&i.ID,
219
+			&i.UserID,
220
+			&i.Name,
221
+			&i.Fingerprint,
222
+			&i.KeyID,
223
+			&i.Armored,
224
+			&i.CanSign,
225
+			&i.CanEncryptComms,
226
+			&i.CanEncryptStorage,
227
+			&i.CanCertify,
228
+			&i.CanAuthenticate,
229
+			&i.Uids,
230
+			&i.Subkeys,
231
+			&i.PrimaryAlgo,
232
+			&i.CreatedAt,
233
+			&i.LastUsedAt,
234
+			&i.RevokedAt,
235
+			&i.ExpiresAt,
236
+		); err != nil {
237
+			return nil, err
238
+		}
239
+		items = append(items, i)
240
+	}
241
+	if err := rows.Err(); err != nil {
242
+		return nil, err
243
+	}
244
+	return items, nil
245
+}
246
+
247
+const softDeleteUserGPGKey = `-- name: SoftDeleteUserGPGKey :execrows
248
+UPDATE user_gpg_keys
249
+SET revoked_at = now()
250
+WHERE id = $1 AND user_id = $2 AND revoked_at IS NULL
251
+`
252
+
253
+type SoftDeleteUserGPGKeyParams struct {
254
+	ID     int64
255
+	UserID int64
256
+}
257
+
258
+// Scoped soft-delete: stamps revoked_at, preserves the row for audit
259
+// continuity. Returns the number of rows affected so the handler can
260
+// distinguish "not found" from "deleted" without a follow-up query.
261
+func (q *Queries) SoftDeleteUserGPGKey(ctx context.Context, db DBTX, arg SoftDeleteUserGPGKeyParams) (int64, error) {
262
+	result, err := db.Exec(ctx, softDeleteUserGPGKey, arg.ID, arg.UserID)
263
+	if err != nil {
264
+		return 0, err
265
+	}
266
+	return result.RowsAffected(), nil
267
+}
268
+
269
+const touchUserGPGKeyLastUsed = `-- name: TouchUserGPGKeyLastUsed :exec
270
+UPDATE user_gpg_keys SET last_used_at = now() WHERE id = $1
271
+`
272
+
273
+// Best-effort last-used stamp called from the verification path when
274
+// a signature successfully resolves to this key. No timeout / error
275
+// propagation; the caller fires-and-forgets via a goroutine.
276
+func (q *Queries) TouchUserGPGKeyLastUsed(ctx context.Context, db DBTX, id int64) error {
277
+	_, err := db.Exec(ctx, touchUserGPGKeyLastUsed, id)
278
+	return err
279
+}
internal/users/sqlc/user_gpg_subkeys.sql.goadded
@@ -0,0 +1,162 @@
1
+// Code generated by sqlc. DO NOT EDIT.
2
+// versions:
3
+//   sqlc v1.31.1
4
+// source: user_gpg_subkeys.sql
5
+
6
+package usersdb
7
+
8
+import (
9
+	"context"
10
+
11
+	"github.com/jackc/pgx/v5/pgtype"
12
+)
13
+
14
+const getUserGPGSubkeyByFingerprint = `-- name: GetUserGPGSubkeyByFingerprint :one
15
+SELECT id, gpg_key_id, fingerprint, key_id,
16
+       can_sign, can_encrypt_comms, can_encrypt_storage, can_certify,
17
+       expires_at, revoked_at, created_at
18
+FROM user_gpg_subkeys
19
+WHERE fingerprint = $1 AND revoked_at IS NULL
20
+`
21
+
22
+// Hot path for commit/tag signature verification. The signature
23
+// packet carries the signing subkey's fingerprint; this query
24
+// resolves it back to the primary key (and via FK to the user).
25
+// Index lookup via the partial unique index.
26
+func (q *Queries) GetUserGPGSubkeyByFingerprint(ctx context.Context, db DBTX, fingerprint string) (UserGpgSubkey, error) {
27
+	row := db.QueryRow(ctx, getUserGPGSubkeyByFingerprint, fingerprint)
28
+	var i UserGpgSubkey
29
+	err := row.Scan(
30
+		&i.ID,
31
+		&i.GpgKeyID,
32
+		&i.Fingerprint,
33
+		&i.KeyID,
34
+		&i.CanSign,
35
+		&i.CanEncryptComms,
36
+		&i.CanEncryptStorage,
37
+		&i.CanCertify,
38
+		&i.ExpiresAt,
39
+		&i.RevokedAt,
40
+		&i.CreatedAt,
41
+	)
42
+	return i, err
43
+}
44
+
45
+const insertUserGPGSubkey = `-- name: InsertUserGPGSubkey :one
46
+
47
+INSERT INTO user_gpg_subkeys (
48
+    gpg_key_id, fingerprint, key_id,
49
+    can_sign, can_encrypt_comms, can_encrypt_storage, can_certify,
50
+    expires_at
51
+)
52
+VALUES (
53
+    $1, $2, $3,
54
+    $4, $5, $6, $7,
55
+    $8
56
+)
57
+RETURNING id, gpg_key_id, fingerprint, key_id,
58
+          can_sign, can_encrypt_comms, can_encrypt_storage, can_certify,
59
+          expires_at, revoked_at, created_at
60
+`
61
+
62
+type InsertUserGPGSubkeyParams struct {
63
+	GpgKeyID          int64
64
+	Fingerprint       string
65
+	KeyID             string
66
+	CanSign           bool
67
+	CanEncryptComms   bool
68
+	CanEncryptStorage bool
69
+	CanCertify        bool
70
+	ExpiresAt         pgtype.Timestamptz
71
+}
72
+
73
+// SPDX-License-Identifier: AGPL-3.0-or-later
74
+// One row per subkey of a primary key. Always inserted in the same
75
+// transaction as the parent InsertUserGPGKey so the verification
76
+// hot path's fingerprint lookup is consistent with the REST nested
77
+// shape.
78
+func (q *Queries) InsertUserGPGSubkey(ctx context.Context, db DBTX, arg InsertUserGPGSubkeyParams) (UserGpgSubkey, error) {
79
+	row := db.QueryRow(ctx, insertUserGPGSubkey,
80
+		arg.GpgKeyID,
81
+		arg.Fingerprint,
82
+		arg.KeyID,
83
+		arg.CanSign,
84
+		arg.CanEncryptComms,
85
+		arg.CanEncryptStorage,
86
+		arg.CanCertify,
87
+		arg.ExpiresAt,
88
+	)
89
+	var i UserGpgSubkey
90
+	err := row.Scan(
91
+		&i.ID,
92
+		&i.GpgKeyID,
93
+		&i.Fingerprint,
94
+		&i.KeyID,
95
+		&i.CanSign,
96
+		&i.CanEncryptComms,
97
+		&i.CanEncryptStorage,
98
+		&i.CanCertify,
99
+		&i.ExpiresAt,
100
+		&i.RevokedAt,
101
+		&i.CreatedAt,
102
+	)
103
+	return i, err
104
+}
105
+
106
+const listSubkeysForGPGKey = `-- name: ListSubkeysForGPGKey :many
107
+SELECT id, gpg_key_id, fingerprint, key_id,
108
+       can_sign, can_encrypt_comms, can_encrypt_storage, can_certify,
109
+       expires_at, revoked_at, created_at
110
+FROM user_gpg_subkeys
111
+WHERE gpg_key_id = $1
112
+ORDER BY id
113
+`
114
+
115
+// Reads all live subkeys for one primary; used when invalidating the
116
+// verification cache on primary soft-delete (every dependent subkey
117
+// needs its cache rows stamped invalidated too).
118
+func (q *Queries) ListSubkeysForGPGKey(ctx context.Context, db DBTX, gpgKeyID int64) ([]UserGpgSubkey, error) {
119
+	rows, err := db.Query(ctx, listSubkeysForGPGKey, gpgKeyID)
120
+	if err != nil {
121
+		return nil, err
122
+	}
123
+	defer rows.Close()
124
+	items := []UserGpgSubkey{}
125
+	for rows.Next() {
126
+		var i UserGpgSubkey
127
+		if err := rows.Scan(
128
+			&i.ID,
129
+			&i.GpgKeyID,
130
+			&i.Fingerprint,
131
+			&i.KeyID,
132
+			&i.CanSign,
133
+			&i.CanEncryptComms,
134
+			&i.CanEncryptStorage,
135
+			&i.CanCertify,
136
+			&i.ExpiresAt,
137
+			&i.RevokedAt,
138
+			&i.CreatedAt,
139
+		); err != nil {
140
+			return nil, err
141
+		}
142
+		items = append(items, i)
143
+	}
144
+	if err := rows.Err(); err != nil {
145
+		return nil, err
146
+	}
147
+	return items, nil
148
+}
149
+
150
+const softDeleteSubkeysForGPGKey = `-- name: SoftDeleteSubkeysForGPGKey :exec
151
+UPDATE user_gpg_subkeys
152
+SET revoked_at = now()
153
+WHERE gpg_key_id = $1 AND revoked_at IS NULL
154
+`
155
+
156
+// Stamps revoked_at on every live subkey of a primary. Called in the
157
+// same transaction as SoftDeleteUserGPGKey so the partial unique index
158
+// frees up the fingerprint for re-upload if the user rotates.
159
+func (q *Queries) SoftDeleteSubkeysForGPGKey(ctx context.Context, db DBTX, gpgKeyID int64) error {
160
+	_, err := db.Exec(ctx, softDeleteSubkeysForGPGKey, gpgKeyID)
161
+	return err
162
+}
internal/webhook/sqlc/models.gomodified
@@ -2545,6 +2545,41 @@ type UserEmail struct {
25452545
 	CreatedAt             pgtype.Timestamptz
25462546
 }
25472547
 
2548
+type UserGpgKey struct {
2549
+	ID                int64
2550
+	UserID            int64
2551
+	Name              string
2552
+	Fingerprint       string
2553
+	KeyID             string
2554
+	Armored           string
2555
+	CanSign           bool
2556
+	CanEncryptComms   bool
2557
+	CanEncryptStorage bool
2558
+	CanCertify        bool
2559
+	CanAuthenticate   bool
2560
+	Uids              []string
2561
+	Subkeys           []byte
2562
+	PrimaryAlgo       string
2563
+	CreatedAt         pgtype.Timestamptz
2564
+	LastUsedAt        pgtype.Timestamptz
2565
+	RevokedAt         pgtype.Timestamptz
2566
+	ExpiresAt         pgtype.Timestamptz
2567
+}
2568
+
2569
+type UserGpgSubkey struct {
2570
+	ID                int64
2571
+	GpgKeyID          int64
2572
+	Fingerprint       string
2573
+	KeyID             string
2574
+	CanSign           bool
2575
+	CanEncryptComms   bool
2576
+	CanEncryptStorage bool
2577
+	CanCertify        bool
2578
+	ExpiresAt         pgtype.Timestamptz
2579
+	RevokedAt         pgtype.Timestamptz
2580
+	CreatedAt         pgtype.Timestamptz
2581
+}
2582
+
25482583
 type UserNotificationPref struct {
25492584
 	UserID    int64
25502585
 	Key       string
internal/worker/sqlc/models.gomodified
@@ -2545,6 +2545,41 @@ type UserEmail struct {
25452545
 	CreatedAt             pgtype.Timestamptz
25462546
 }
25472547
 
2548
+type UserGpgKey struct {
2549
+	ID                int64
2550
+	UserID            int64
2551
+	Name              string
2552
+	Fingerprint       string
2553
+	KeyID             string
2554
+	Armored           string
2555
+	CanSign           bool
2556
+	CanEncryptComms   bool
2557
+	CanEncryptStorage bool
2558
+	CanCertify        bool
2559
+	CanAuthenticate   bool
2560
+	Uids              []string
2561
+	Subkeys           []byte
2562
+	PrimaryAlgo       string
2563
+	CreatedAt         pgtype.Timestamptz
2564
+	LastUsedAt        pgtype.Timestamptz
2565
+	RevokedAt         pgtype.Timestamptz
2566
+	ExpiresAt         pgtype.Timestamptz
2567
+}
2568
+
2569
+type UserGpgSubkey struct {
2570
+	ID                int64
2571
+	GpgKeyID          int64
2572
+	Fingerprint       string
2573
+	KeyID             string
2574
+	CanSign           bool
2575
+	CanEncryptComms   bool
2576
+	CanEncryptStorage bool
2577
+	CanCertify        bool
2578
+	ExpiresAt         pgtype.Timestamptz
2579
+	RevokedAt         pgtype.Timestamptz
2580
+	CreatedAt         pgtype.Timestamptz
2581
+}
2582
+
25482583
 type UserNotificationPref struct {
25492584
 	UserID    int64
25502585
 	Key       string