@@ -0,0 +1,123 @@ |
| | 1 | +// SPDX-License-Identifier: AGPL-3.0-or-later |
| | 2 | + |
| | 3 | +package sigverify |
| | 4 | + |
| | 5 | +import ( |
| | 6 | + "context" |
| | 7 | + "time" |
| | 8 | + |
| | 9 | + reposdb "github.com/tenseleyFlow/shithub/internal/repos/sqlc" |
| | 10 | +) |
| | 11 | + |
| | 12 | +// View is a render-friendly projection of a commit_verification_cache |
| | 13 | +// row. It carries everything the HTML badge partial and the REST |
| | 14 | +// `verification` object need; both consumers translate to their own |
| | 15 | +// surface shapes from here. |
| | 16 | +// |
| | 17 | +// "No cache row" maps to View{Verified: false, Reason: ReasonUnsigned} |
| | 18 | +// — gh's documented behavior for commits we haven't verified yet. |
| | 19 | +// |
| | 20 | +// "Cache row exists but invalidated_at != null" also maps to the |
| | 21 | +// unsigned-shaped default. Rationale: a stale row means the signing |
| | 22 | +// key was revoked between verification and now; pretending it's |
| | 23 | +// verified would be wrong. Treating it as "needs re-verify, render |
| | 24 | +// as unsigned for now" lets the backfill worker catch up |
| | 25 | +// asynchronously without the render path doing crypto work inline. |
| | 26 | +type View struct { |
| | 27 | + Verified bool |
| | 28 | + Reason Reason |
| | 29 | + Signature string |
| | 30 | + Payload []byte |
| | 31 | + VerifiedAt *time.Time |
| | 32 | + SignerUserID int64 |
| | 33 | + SignerSubkeyID int64 |
| | 34 | +} |
| | 35 | + |
| | 36 | +// UnsignedView is the canonical "no cache row" / "row invalidated" |
| | 37 | +// shape. Mirrors gh's "unsigned" rendering. |
| | 38 | +func UnsignedView() View { |
| | 39 | + return View{Verified: false, Reason: ReasonUnsigned} |
| | 40 | +} |
| | 41 | + |
| | 42 | +// ViewFromRow translates a sqlc cache row into the View shape. |
| | 43 | +// Honors the invalidated_at flag by returning UnsignedView for |
| | 44 | +// stale rows. |
| | 45 | +func ViewFromRow(row reposdb.CommitVerificationCache) View { |
| | 46 | + if row.InvalidatedAt.Valid { |
| | 47 | + return UnsignedView() |
| | 48 | + } |
| | 49 | + view := View{ |
| | 50 | + Verified: row.Verified, |
| | 51 | + Reason: Reason(row.Reason), |
| | 52 | + Payload: row.Payload, |
| | 53 | + } |
| | 54 | + if row.SignatureArmored.Valid { |
| | 55 | + view.Signature = row.SignatureArmored.String |
| | 56 | + } |
| | 57 | + if row.VerifiedAt.Valid { |
| | 58 | + t := row.VerifiedAt.Time |
| | 59 | + view.VerifiedAt = &t |
| | 60 | + } |
| | 61 | + if row.SignerUserID.Valid { |
| | 62 | + view.SignerUserID = row.SignerUserID.Int64 |
| | 63 | + } |
| | 64 | + if row.SignerSubkeyID.Valid { |
| | 65 | + view.SignerSubkeyID = row.SignerSubkeyID.Int64 |
| | 66 | + } |
| | 67 | + return view |
| | 68 | +} |
| | 69 | + |
| | 70 | +// LoadViewsForOIDs batch-reads verification rows for a set of commit |
| | 71 | +// OIDs and returns a map keyed by OID. OIDs without a cache row are |
| | 72 | +// absent from the map — the renderer treats absence as |
| | 73 | +// UnsignedView() via the LookupView helper. |
| | 74 | +// |
| | 75 | +// Used by the commit-list render path (both HTML and REST). |
| | 76 | +func LoadViewsForOIDs(ctx context.Context, db reposdb.DBTX, repoID int64, oids []string) (map[string]View, error) { |
| | 77 | + if len(oids) == 0 { |
| | 78 | + return map[string]View{}, nil |
| | 79 | + } |
| | 80 | + q := reposdb.New() |
| | 81 | + rows, err := q.GetCommitVerificationsForOIDs(ctx, db, reposdb.GetCommitVerificationsForOIDsParams{ |
| | 82 | + RepoID: repoID, |
| | 83 | + Oids: oids, |
| | 84 | + }) |
| | 85 | + if err != nil { |
| | 86 | + return nil, err |
| | 87 | + } |
| | 88 | + out := make(map[string]View, len(rows)) |
| | 89 | + for _, row := range rows { |
| | 90 | + out[row.CommitOid] = ViewFromRow(row) |
| | 91 | + } |
| | 92 | + return out, nil |
| | 93 | +} |
| | 94 | + |
| | 95 | +// LoadView reads a single commit's verification row. Returns |
| | 96 | +// UnsignedView() when no row exists or the row is invalidated. |
| | 97 | +func LoadView(ctx context.Context, db reposdb.DBTX, repoID int64, oid string) (View, error) { |
| | 98 | + q := reposdb.New() |
| | 99 | + row, err := q.GetCommitVerification(ctx, db, reposdb.GetCommitVerificationParams{ |
| | 100 | + RepoID: repoID, |
| | 101 | + CommitOid: oid, |
| | 102 | + }) |
| | 103 | + if err != nil { |
| | 104 | + // Distinguishing "not found" from "DB error" matters: not- |
| | 105 | + // found is the unsigned-default fast path; DB error should |
| | 106 | + // propagate so the caller can decide between fail-open |
| | 107 | + // (render anyway) and fail-closed (5xx). Caller handles via |
| | 108 | + // errors.Is(err, pgx.ErrNoRows). |
| | 109 | + return UnsignedView(), err |
| | 110 | + } |
| | 111 | + return ViewFromRow(row), nil |
| | 112 | +} |
| | 113 | + |
| | 114 | +// LookupView returns the View for the given OID from a map, falling |
| | 115 | +// back to UnsignedView() on miss. Template-friendly helper so the |
| | 116 | +// partial doesn't have to do nil-checks inline. |
| | 117 | +func LookupView(m map[string]View, oid string) View { |
| | 118 | + v, ok := m[oid] |
| | 119 | + if !ok { |
| | 120 | + return UnsignedView() |
| | 121 | + } |
| | 122 | + return v |
| | 123 | +} |