@@ -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 | +} |