@@ -369,3 +369,79 @@ organization upgrades again. |
| 369 | - Stripe Billing: `https://docs.stripe.com/billing` | 369 | - Stripe Billing: `https://docs.stripe.com/billing` |
| 370 | - Stripe pricing models: | 370 | - Stripe pricing models: |
| 371 | `https://docs.stripe.com/products-prices/pricing-models` | 371 | `https://docs.stripe.com/products-prices/pricing-models` |
| | 372 | + |
| | 373 | +## PRO08 GA audit closure |
| | 374 | + |
| | 375 | +**Audit dates**: 2026-05-13 (run + remediation in a single sprint). |
| | 376 | + |
| | 377 | +**Scope**: PRO04 Stripe adapter + PRO05 entitlements Principal + |
| | 378 | +PRO06 user-tier billing settings + PRO07 Pro v1 feature gates. |
| | 379 | + |
| | 380 | +**Methodology**: four parallel security/correctness audit agents |
| | 381 | +ran read-only inspections against the PRO07 tip plus the deployed |
| | 382 | +host (`shithub.sh`). Findings were triaged per-bug; HIGH and |
| | 383 | +MEDIUM severity gaps were fixed inline on the `payments-pro/08-pro-ga-audit` |
| | 384 | +branch with locking tests. LOW-severity items were either fixed or |
| | 385 | +documented per the user directive "we don't want to take any chances |
| | 386 | +with payments." |
| | 387 | + |
| | 388 | +### Findings + remediation status |
| | 389 | + |
| | 390 | +| # | Severity | Finding | Fix | Test | |
| | 391 | +|---|---|---|---|---| |
| | 392 | +| A1 | MED | `guardPriceKindMatch` bypassable on empty `Items` | refuse empty-items when prices configured | `TestBillingWebhookGuardRefusesEmptyItemsWhenPricesConfigured` | |
| | 393 | +| A2 | HIGH | `(subject_kind, subject_id)` never written to receipts | `SetWebhookEventSubjectForPrincipal` after every resolve; `ListFailedWebhookEvents` operator query | `TestSetWebhookEventSubjectForPrincipalRecordsAuditTrail`, `TestListFailedWebhookEventsReturnsErroredAndStuckEntries` | |
| | 394 | +| A3 | MED | Concurrent dup-replay TOCTOU race | session-scoped advisory lock keyed on event id | `TestBillingWebhookConcurrentReplayAppliesOnce` | |
| | 395 | +| D1 | HIGH | Snapshot CTE wipes lock columns on past_due transitions | conditional preservation in `ApplySubscriptionSnapshot` + user analog | `TestApplySubscriptionSnapshotPreservesGraceLockOnPastDue` (+ recovery + user-side mirror) | |
| | 396 | +| D2 | LOW | No `charge.refunded` handler | new dispatch + `MarkInvoiceRefunded` query + enum + UI surface | `TestBillingWebhookChargeRefundedMarksInvoiceRefunded` (+ unknown invoice + standalone refund) | |
| | 397 | +| D3 | MED | Two subscriptions per customer silently overwrite | `guardSubscriptionOverwrite` rejects different-sub apply | `TestBillingWebhookGuardRefusesSecondSubscriptionForSameCustomer` | |
| | 398 | +| D4 | MED | No stale-event detection (reverse-order corruption) | `last_event_at` column + `IsBillingEventStaleForPrincipal` + handler guard + post-apply touch | `TestBillingWebhookDropsStaleEvent` | |
| | 399 | +| D5 | LOW | `customer.subscription.deleted` for unknown sub 5xx's | log + 200 no-op so Stripe stops retrying | `TestBillingWebhookSubscriptionDeletedForUnknownSubIsNoOp` | |
| | 400 | +| C1 | HIGH | `require_signed_commits` unreachable from UI | form field + template checkbox + sqlc plumbing | covered by `TestSettingsBranches_UserKindEnforceFlipPreventForcePushBlocks` table | |
| | 401 | +| C2 | HIGH | Advanced BP gate fires on wrong inputs | rewire predicate to fire on prevent_*, signing, AND status-checks | table test | |
| | 402 | +| C3 | MED | Multi-reviewer deny copy indistinguishable | thread reviewer count → `required-reviewers-multi-upgrade(-pro)` codes | `TestSettingsBranches_UserKindEnforceFlipMultiReviewerBlocks` | |
| | 403 | +| C4 | MED | Notice copy says "Team" / "organization" on user-tier denies | user-kind variants (`-pro` suffix) with `/settings/billing` href | `TestSettingsBranches_UserKindEnforceFlipRequiredReviewersBlocksSingle` (Pro copy) | |
| | 404 | +| C5 | LOW | `profilePinsRemaining` hard-codes cap; under-counts for Pro | thread entitled cap into the function | covered by existing profile_test suite + no regression | |
| | 405 | + |
| | 406 | +Authorization audit (Agent B) found **no** cross-tenant data leak, |
| | 407 | +no invoice-scoping bleed, and no deny-shape 500 reachable from |
| | 408 | +production paths. Two `err→500` branches in the entitlement gate |
| | 409 | +remain (in `RequirePrincipalFeature` and `evaluateBranchProtectionFeature`) |
| | 410 | +but are defended against by the AFTER-INSERT seed triggers on |
| | 411 | +both billing-state tables — `pgx.ErrNoRows` for a valid principal |
| | 412 | +is dead code in production. |
| | 413 | + |
| | 414 | +### Pre-existing failures NOT introduced by PRO08 |
| | 415 | + |
| | 416 | +These were noted across prior sprints and remain open: |
| | 417 | + |
| | 418 | +- `TestPrivateCollaborationExpansionEnforcesFreeLimitAndTeamUnlimited` |
| | 419 | +- `TestPrivateCollaborationUsageCountsEffectivePrivateAccess` |
| | 420 | +- `TestRepoPrivateVisibilityCountsRepoSpecificGrants` |
| | 421 | + |
| | 422 | +The fourth pre-existing failure |
| | 423 | +(`TestSettingsBranchesAllowsDowngradedOrgToRemoveAdvancedSettings`) |
| | 424 | +was fixed opportunistically while in the branch-protection cluster |
| | 425 | +(seed fixture needed `AllowedPusherUserIds: []int64{}` to satisfy |
| | 426 | +the NOT NULL constraint). |
| | 427 | + |
| | 428 | +### Go/no-go: GA |
| | 429 | + |
| | 430 | +**GO**, conditional on the operator completing the Stripe Dashboard |
| | 431 | +checklist documented in `docs/internal/runbooks/stripe-billing.md` |
| | 432 | +under "Subject resolution chain" and "Per-feature enforcement flags" |
| | 433 | +sections. Production binary needs redeploy to pick up: |
| | 434 | + |
| | 435 | +- Migrations 0077 (`last_event_at`) + 0078 (`refunded` enum + column) |
| | 436 | +- The 13 code fixes above |
| | 437 | + |
| | 438 | +Deploy steps: |
| | 439 | + |
| | 440 | +1. `ssh root@shithub.sh "sudo -u shithub /usr/local/bin/shithubd migrate up"` — applies 0077 + 0078 |
| | 441 | +2. Restart `shithubd-web.service` and `shithubd-worker.service` |
| | 442 | +3. Verify with: `SELECT version_id FROM goose_db_version ORDER BY version_id DESC LIMIT 1` → expect `78` |
| | 443 | +4. Configure Stripe env vars per the runbook (still in test mode until ratified) |
| | 444 | +5. Run the smoke checklist in `runbooks/stripe-billing.md#smoke-test` |
| | 445 | +6. Flip enforce flags per feature after 7-day report-only soak |
| | 446 | + |
| | 447 | +**No production customers exist today** (`SELECT plan, count(*) FROM users WHERE deleted_at IS NULL` → `free: 2`; same for orgs), so the deploy carries near-zero risk to existing customers — there are none on Pro/Team plans. |