Files
Crussell/backend/handlers/payments/twofa.go
T
popertots 2cdbad0cea feat: SCA-only saved-card charges — 2FA charge fallback removed (C6), versioned consent fields, token provenance
PSR 2017 reg 100 makes SCA mandatory and non-waivable for customer-initiated
stored-credential charges; a merchant-side 2FA check cannot legally substitute
for it (authorising a token-less charge via 2FA leaves the MERCHANT liable for
ECI 7 / SLI 210 chargebacks and reg 77(6) compensation regardless of consent).

- payments/twofa.go: the homegrown 2FA fallback for token-less saved-card
  charges is REMOVED ENTIRELY. requireTwoFactorForCardAccess is now SCA-only:
  a non-empty Square verification_token (charge surfaces, token forwarded to
  Square) skips the gate; anything else is refused 402 verification_required.
  enforceSCAFallbackConsent is a compile-compatible no-op (fallback never runs).
- New requireTwoFactorForCardAccessWithTokenValidation distinguishes surfaces
  where the token IS forwarded to Square (charge — Square validates it) from
  card-SAVE surfaces (token client-asserted, never forwarded: a non-empty token
  must NOT skip the save gate, auth-F1).
- SCA tokenize-result wire contract (C1): a saved card charged with a fresh
  one-time tokenize-result sends the token as the charge SOURCE (new_card_token
  -> source_id) alongside saved_card_id, never a separate verification_token.
  resolveChargeSource resolves the saved-card branch FIRST (customer from the
  card row, token as source) so combined token+card requests are SCA-clean.
- C6 consent fields (consent_version / consent_accepted) added to the booking/
  tip/till/gift-card charge requests, enforced server-side before any fallback
  charge could reach Square and recorded on the 2fa_fallback_charge audit row;
  logVerificationTokenProvenance traces minted tokens to their charge.
- user 2FA issuance gate refactored into pure build-agnostic functions
  (twoFAPepperConfigured / twoFADeliveryChannelConfigured /
  twoFAEnsureIssueAllowedStrict) shared with the payments re-issue path and
  exercised directly by the test,dev suite; TWO_FACTOR_FALLBACK switch and
  .env.example entry removed; startup posture notes updated.
- Test coverage: fail-closed 2FA production gates (pepper/delivery), token
  validation on save vs charge surfaces, completion idempotency, idempotency
  key determinism, refund-policy 72h/24h epsilon boundaries, VAT parity.
2026-08-22 00:34:50 +01:00

457 lines
25 KiB
Go

package payments
import (
"context"
"crypto/rand"
"errors"
"fmt"
"log"
"math/big"
"net/http"
"os"
"strings"
"time"
"crussell/clock"
"crussell/db"
"crussell/internal/twofa"
)
// require2FADisabled reports whether REQUIRE_2FA explicitly disables 2FA
// enforcement. The parse is case-insensitive and alias-tolerant (false/0/off/no),
// so a value like "False", "OFF" or "off" never silently leaves the gate ON.
// Any other value — including empty or unknown — keeps enforcement ON
// (fail-closed).
func require2FADisabled() bool {
switch strings.ToLower(strings.TrimSpace(os.Getenv("REQUIRE_2FA"))) {
case "false", "0", "off", "no":
return true
default:
return false
}
}
// twoFactorEnforced reports whether 2FA is required for online card payments.
// It is fail-closed: enforcement is ON unless 2FA has been explicitly disabled
// (REQUIRE_2FA=false/0/off/no, case-insensitive — see require2FADisabled) or
// SQUARE_ENVIRONMENT explicitly selects the dev/mock stack
// (mock/dev/development/test — see IsExplicitDevOrMockEnv in
// idempotency_helpers.go). Empty or unknown SQUARE_ENVIRONMENT values are
// treated as production-enforced, so a mistyped env var can never silently
// disarm the gate — main.go logs a startup warning for that misconfiguration.
func twoFactorEnforced() bool {
return !require2FADisabled() && !IsExplicitDevOrMockEnv()
}
// TwoFactorEnforced is the exported form of twoFactorEnforced, so the user
// package (settings endpoints) and the profile handler can report whether 2FA
// is currently required without re-implementing the env logic.
func (s *PaymentService) TwoFactorEnforced() bool {
return twoFactorEnforced()
}
// errTwoFAPepperRequired is returned when TWO_FACTOR_PEPPER is unset in a
// production-style re-issue gate. Refusing to re-issue is the only safe outcome:
// without the pepper the fresh code would be persisted as an unsalted SHA-256
// digest in the 1M code space, which a log/DB leak could brute-force offline
// (mirrors handlers/user's errTwoFAPepperRequired). Defined here (no build tag)
// so the pure gate (twoFAReissueIssueAllowedStrict) and the test,dev suite
// share it.
var errTwoFAPepperRequired = errors.New("TWO_FACTOR_PEPPER is not set; refusing to issue a 2FA code (an unsalted digest would be offline-brute-forceable)")
// errTwoFADeliveryUnavailable is returned when a production-style re-issue has
// no delivery channel: email/SMS unwired and the operator has not opted into
// the insecure log-delivery mode (TWO_FACTOR_ALLOW_LOG_DELIVERY=true). A fresh
// code the customer could never receive would strand them on the saved-card
// gate (mirrors handlers/user's errTwoFADeliveryUnavailable).
var errTwoFADeliveryUnavailable = errors.New("2FA requires an email or SMS delivery channel; contact the salon")
// twoFAPepperConfigured reports whether TWO_FACTOR_PEPPER is set — the pure,
// build-agnostic read behind the strict re-issue gate.
func twoFAPepperConfigured() bool { return os.Getenv("TWO_FACTOR_PEPPER") != "" }
// twoFADeliveryChannelConfigured reports whether the deployment has explicitly
// configured a 2FA code delivery channel: TWO_FACTOR_ALLOW_LOG_DELIVERY set to
// exactly "true" (the only production channel today — email/SMS unwired, P6).
// Pure env read, build-agnostic: the build-tagged twoFADeliveryAvailable
// (twofa_delivery_dev.go / twofa_delivery_prod.go) is the runtime-facing
// wrapper that turns this into the always-true dev channel or the prod env
// check.
func twoFADeliveryChannelConfigured() bool { return os.Getenv("TWO_FACTOR_ALLOW_LOG_DELIVERY") == "true" }
// twoFAReissueIssueAllowedStrict is the pure, build-agnostic production-style
// re-issue gate: a re-issued 2FA code may be minted ONLY when BOTH
// TWO_FACTOR_PEPPER is set (an unsalted digest in the 1M code space would be
// offline-brute-forceable) AND a delivery channel is configured (otherwise the
// fresh code could never reach the customer). Either way it fails closed. The
// build-tagged twoFAReissueIssueAllowed wraps it for production builds;
// dev/test builds always allow re-issue and never consult it — but the test,dev
// suite exercises THIS function directly, so the fail-closed branches are
// CI-visible even though the prod file (!dev && !test) is excluded there.
func twoFAReissueIssueAllowedStrict() error {
if !twoFAPepperConfigured() {
return errTwoFAPepperRequired
}
if !twoFADeliveryChannelConfigured() {
return errTwoFADeliveryUnavailable
}
return nil
}
// Single source of truth for 2FA verification: crussell/internal/twofa owns
// the code hashing (HMAC-SHA256 keyed by TWO_FACTOR_PEPPER, legacy SHA-256
// fallback), the constant-time compare, the code-lifetime check, and the
// per-user brute-force lockout. The user package's interactive endpoints
// (setup/verify/disable) and this file's re-issue path share it; nothing is
// re-implemented locally here. The saved-card charge gate no longer verifies
// 2FA codes at all (SCA-only — the 2FA fallback was removed), so the only
// local consumer left is reissueTwoFACodeAfterFailedCharge below.
// enforceSCAFallbackConsent is retained as a compile-compatible NO-OP so the
// saved-card charge handlers (handlers.go / till.go / giftcards.go) keep
// compiling unchanged. It used to enforce the C6 consent notice on the
// SCA-unavailable → 2FA fallback path, which is now REMOVED ENTIRELY: PSR 2017
// reg 100 makes Strong Customer Authentication mandatory and non-waivable for
// customer-initiated stored-credential charges, and the homegrown 2FA (a
// merchant-side check with no bank involvement) cannot legally act as an SCA
// fallback — authorising a token-less charge via 2FA leaves the MERCHANT liable
// for ECI 7 / SLI 210 chargebacks and PSR 2017 reg 77(6) compensation, and
// customer consent does not cure that. The gate now refuses a token-less
// saved-card charge 402 verification_required BEFORE any fallback can be used,
// so fallbackUsed is always false and there is no consent to demand. The
// callers pass it through as before; it always returns true.
func enforceSCAFallbackConsent(w http.ResponseWriter, consentVersion *string, consentAccepted bool, fallbackUsed bool) bool {
return true
}
// consentVersionValue normalizes the request's optional consent_version pointer
// to a string ("" when absent). Retained because the saved-card charge handlers
// still read the field when writing their (now unreachable) fallback audit row.
func consentVersionValue(version *string) string {
if version == nil {
return ""
}
return *version
}
// requireTwoFactorForCardAccess gates the saved-card online payment paths.
// It returns (allowed, fallbackUsed): allowed is true when the request may
// proceed; fallbackUsed is ALWAYS false — the homegrown 2FA fallback for
// token-less saved-card charges was REMOVED ENTIRELY, so no charge is ever
// authorized by a 2FA code and no fallback audit row is ever written (the
// callers' insertTwoFAFallbackAudit branches are unreachable).
//
// It is a thin wrapper over requireTwoFactorForCardAccessWithTokenValidation
// that passes tokenForwardedToSquare=true — the legacy signature is retained
// because the saved-card CHARGE surfaces (booking, tip, gift-card buy, till)
// forward the verification_token to Square in CreatePaymentReq.VerificationToken,
// so a non-empty token there IS Square-validated SCA and legitimately skips the
// gate. The card-SAVE surfaces call the WithTokenValidation variant directly
// with false (see that helper for the auth-F1 rationale).
//
// The decision model, in order:
//
// - 2FA enforcement is not active (dev/mock, or REQUIRE_2FA disabled) →
// allowed. The dev mock simulates SCA (SimulateSavedCardVerificationRequired
// + cnon:sca-... tokenize-results), so development has full parity with the
// SCA-only production posture.
//
// - The request carries a Square verification_token (SCA performed — the
// issuer has already authenticated the buyer) AND the token is forwarded to
// Square on this surface (tokenForwardedToSquare=true, the charge paths):
// SKIP the gate entirely. SCA is PRIMARY; a charge that carries a token
// passes even for a user who has not enabled 2FA.
//
// - Otherwise the charge is token-less, and there is NO homegrown 2FA
// fallback anymore. PSR 2017 reg 100 makes Strong Customer Authentication
// mandatory and NON-WAIVABLE for customer-initiated stored-credential
// charges, and a merchant-side 2FA check with no bank involvement cannot
// legally act as an SCA substitute: authorising a token-less charge via 2FA
// would leave the MERCHANT liable for ECI 7 / SLI 210 chargebacks and PSR
// 2017 reg 77(6) compensation, and customer consent does not cure that. On
// genuine sca-unavailable the charge is therefore REFUSED 402
// verification_required and the customer is invited to pay online later.
//
// On any denial an error JSON is written (parseable by the frontend via
// extractErrorMessage) and allowed=false is returned — the caller must abort
// the charge.
func requireTwoFactorForCardAccess(w http.ResponseWriter, r *http.Request, service *PaymentService, userID, verificationCode, verificationToken string, consume bool) (allowed, fallbackUsed bool) {
return requireTwoFactorForCardAccessWithTokenValidation(w, r, service, userID, verificationCode, verificationToken, consume, true)
}
// requireTwoFactorForCardAccessWithTokenValidation is the real gate:
// requireTwoFactorForCardAccess above is the charge-surface wrapper that passes
// tokenForwardedToSquare=true. The extra flag distinguishes surfaces where the
// verification_token WILL be forwarded to Square (charge — Square validates it
// server-side and rejects a forged token, so a non-empty token legitimately
// proves SCA) from surfaces where it is client-asserted and NEVER forwarded
// (card SAVE — the card is persisted via CreateCardOnFile, which takes no
// verification_token, so Square never validates it).
//
// auth-F1: on a SAVE surface a forged non-empty verification_token must NOT
// skip the gate — with tokenForwardedToSquare=false the token is ignored and
// the token-less refusal below applies. An authenticated client can therefore
// no longer persist a card to the account with `verification_token: "anything"`
// and no SCA. The one legitimate SAVE skip is the call-site's scaTokenizedSavedCard
// flow (new_card_token + saved_card_id): there the SCA tokenize-result token IS
// the charge source and Square validates it as source_id, so the gate is skipped
// by the caller before this helper is ever reached.
func requireTwoFactorForCardAccessWithTokenValidation(w http.ResponseWriter, r *http.Request, service *PaymentService, userID, verificationCode, verificationToken string, consume bool, tokenForwardedToSquare bool) (allowed, fallbackUsed bool) {
if !twoFactorEnforced() {
return true, false
}
// SCA-primary: a Square verification_token means the issuer already
// completed Strong Customer Authentication — the gate is skipped and no
// fallback applies. This skip is only valid when the token WILL be
// forwarded to Square (tokenForwardedToSquare=true, the charge surfaces):
// Square validates it and rejects a forged value. On a SAVE surface the
// token is client-asserted and never reaches Square, so a forged non-empty
// token must not skip the gate (auth-F1) — the token-less refusal below
// applies instead.
if verificationToken != "" && tokenForwardedToSquare {
return true, false
}
// SCA-only: a token-less saved-card charge has NO 2FA fallback (the
// homegrown fallback was REMOVED — see the gate doc above for the PSR 2017
// legal rationale). It is refused 402 verification_required; the customer is
// invited to pay online later (or through Square's buyer-verification flow
// on a retry). fallbackUsed stays false.
writeVerificationRequiredResponse(w)
return false, false
}
// reissueTwoFACodeAfterFailedCharge mints a fresh 2FA code after a saved-card
// charge failed at Square — but ONLY when a code was actually consumed by a
// FRESH saved-card charge (fresh-only semantics). The charge gate consumes the
// verified code at gate time for fresh charges (single-use — closing the
// verify-then-consume TOCTOU where a verified-but-unconsumed code could
// authorize a second charge), so a failed fresh charge leaves no live code for
// the same-key retry; this re-issues one with the same 10-minute lifetime and
// delivery behaviour as the user package's code issuance (dev/test logs the
// code for the operator to relay; production logs only with
// TWO_FACTOR_ALLOW_LOG_DELIVERY=true, matching the fail-closed delivery
// contract). It is a NO-OP for every other outcome: a new-card (cnon) charge
// never gates (usedSavedCard=false), a pending-reuse retry verified WITHOUT
// consuming (fallbackUsed=false — its code survives for one more attempt and a
// re-issue would silently invalidate the one the customer holds), and an
// SCA-authorized charge never touched the 2FA gate at all.
//
// Callers pass:
// - usedSavedCard: whether this charge actually used a saved card (the 2FA
// gate applies only to saved-card ccof charges);
// - fallbackUsed: whether the 2FA fallback gate actually consumed a code on
// THIS attempt (true only for a fresh charge — the gate's
// twoFAFallbackUsed ANDed with the caller's not-a-pending-reuse test).
//
// LOW-MEDIUM (finding 2): the re-issue is routed through the same fail-closed
// issuance gate as the interactive mint paths (twoFAReissueIssueAllowed —
// mirrored from the user package's twoFAEnsureIssueAllowed via the build-tagged
// twofa_delivery_dev.go / twofa_delivery_prod.go): a production build refuses
// to re-issue when TWO_FACTOR_PEPPER is unset (an unsalted digest in the 1M
// code space would be offline-brute-forceable) or when no delivery channel is
// configured. It also respects the same per-user mint cooldown
// (twoFAMintCooldown via the shared twofa.AttemptState.LastMintAt), so a
// charge-failure loop cannot mint codes faster than the mint endpoints allow.
// Best-effort: a failure logs a CRITICAL line + raises a critical-payment
// admin notification (the operator must mint a code manually or fix the
// config) and the customer requests a fresh code through the normal 2FA flow.
//
// PEPPER-CHANGE NOTE (Loop B finding 2): TWO_FACTOR_PEPPER is the ONLY hard
// gate on issuance here (twoFAReissueIssueAllowed) AND on the interactive mint
// paths (twoFAEnsureIssueAllowed in handlers/user). The pepper keys the
// HMAC-SHA256 of every stored pending-code hash, so CHANGING it invalidates
// ALL pending codes — every stored hash was computed with the old pepper and
// can never match a code minted under the new one. A fresh saved-card charge
// that consumed a pre-change code then fails will re-issue a code hashed with
// the NEW pepper, which still cannot match anything the customer holds (their
// code was minted under the old pepper, or was consumed). Operators MUST NOT
// change the pepper without re-minting every user's code (or having the
// customer re-run 2FA setup); main.go's startup check should treat a changed
// pepper as a config incident.
func reissueTwoFACodeAfterFailedCharge(ctx context.Context, q db.Querier, userID string, usedSavedCard, fallbackUsed bool, r *http.Request) {
if userID == "" || !twoFactorEnforced() || !usedSavedCard || !fallbackUsed {
return
}
if err := twoFAReissueIssueAllowed(); err != nil {
// Loop B HIGH (finding 2): a REFUSED re-issue strands the customer. The
// gate consumed their code for the fresh charge (single-use) and the
// charge failed — with no re-issued code the same-key retry fails
// forever with 400 ErrMissingOrExpired and the customer is locked out.
// This is an OPERATOR-FACING incident, not a silent best-effort miss:
// log a CRITICAL line AND raise the per-issue-capped critical-payment
// admin notification (sweep.go's insertCriticalPaymentNotification — the
// DB-backed stand-in for the un-watched CRITICAL logs) so the operator
// knows the customer is blocked and can mint a code manually or fix the
// config (TWO_FACTOR_PEPPER / delivery channel).
log.Printf("CRITICAL: failed to re-issue a 2FA code for user %s after a failed saved-card charge (%v) — the customer's code was consumed by the fresh charge and NO live code remains, so the same-key retry cannot succeed; the operator must mint a code manually or configure TWO_FACTOR_PEPPER and a 2FA delivery channel", userID, err)
// Round 2 Loop B findings 2 + 7 — the alert is capped PER-ISSUE, NOT
// globally (see alertReissueFail below): at most ONE unacknowledged row
// per stranded customer, so one customer's alert can never be
// suppressed by OTHER users' rows filling the 'critical_payment_log'
// bucket, and the count-then-insert is atomic (a single INSERT ... WHERE
// NOT EXISTS — no TOCTOU). An attacker also cannot FLOOD the alert:
// raising it requires a real saved-card charge that consumed a real code
// AND a failed re-issue, and the per-issue dedup holds each user to one
// row until acknowledged. Fail-closed behaviour is unchanged (the
// CRITICAL log always fires); only the notification INSERT is bounded.
alertReissueFail(ctx, q, userID)
return
}
// Mint cooldown (B11a + Round 2 Loop A finding 2): the shared per-user mutex
// serializes the stamp read/write with the user package's mints and the
// gate's verify critical section. A successful verify NO LONGER clears the
// stamp (internal/twofa.Check keeps it — it is cleared only at terminal
// charge success via twofa.ConsumePendingCode), so this check now genuinely
// bounds a charge-failure loop: the first re-issue after a mint is skipped
// while clock.Now().Sub(LastMintAt) < twoFAMintCooldown, bounding code churn
// and dev log flooding. The customer requests a fresh code through the
// normal mint endpoint once the window elapses.
st := twofa.StateFor(userID)
st.Mu.Lock()
defer st.Mu.Unlock()
if !st.LastMintAt.IsZero() && clock.Now().Sub(st.LastMintAt) < twoFAMintCooldown {
// Round 2 Loop B finding 6b: this skip was SILENT before. A FRESH
// charge consumed the customer's code at the gate (single-use) and the
// charge failed; the re-issue is now skipped by the per-user mint
// cooldown — the customer holds NO live code for the same-key retry
// until the cooldown lapses. That is a stranded customer, so raise the
// same per-issue-capped reissue-fail alert the refused-issue branch
// above uses (alertReissueFail, deduped on reason+user_id) so the
// operator knows to mint a code manually. The alert is per-issue
// (finding 2): repeated skips for the same customer stay ONE row until
// acknowledged and can never be suppressed by other users' rows.
log.Printf("2FA: re-issue skipped for user %s after a failed charge (mint cooldown) — the customer's code was consumed by the fresh charge and NO live code remains until the cooldown lapses", userID)
alertReissueFail(ctx, q, userID)
return
}
code, err := generatePaymentsTwoFACode()
if err != nil {
log.Printf("2FA: failed to generate a re-issued code for user %s after a failed charge: %v", userID, err)
return
}
if _, err := q.Exec(ctx, `
UPDATE users
SET two_factor_pending_code_hash = $2,
two_factor_pending_code_expires = $3
WHERE id = $1
`, userID, twofa.Hash(code), clock.Now().Add(twoFAPendingCodeLifetime)); err != nil {
log.Printf("2FA: failed to store a re-issued code for user %s after a failed charge: %v", userID, err)
return
}
st.SetLastMintAtLocked(clock.Now())
// Delivery is build-dependent (twofa_delivery_dev.go / twofa_delivery_prod.go),
// mirroring the user package's twoFADeliverCode: dev/test builds always write
// the [2FA] log line (the operator relays the code); production writes it ONLY
// when the operator explicitly opted into log delivery
// (TWO_FACTOR_ALLOW_LOG_DELIVERY=true) — otherwise the plaintext code is never
// logged.
twoFAReissueDeliverCode(userID, code)
}
// twoFAMintCooldown bounds how often the re-issue path mints a fresh 2FA code
// for one user after a failed saved-card charge, mirroring the user package's
// mint cooldown (handlers/user/twofa.go). The shared stamp lives on the
// per-user twofa.AttemptState.LastMintAt so both mint paths cohere.
//
// Round 2 Loop A finding 2: the stamp survives a successful gate verify
// (internal/twofa.Check no longer clears it) and is cleared only at TERMINAL
// charge success via twofa.ConsumePendingCode — so this check below is what
// actually bounds a charge-failure loop: after a fresh charge consumed a code
// at the gate and failed, the re-issue is skipped while the last mint is
// inside the cooldown (logged, not silent), bounding code churn + dev log
// flooding. Round 2 Loop B finding 6a — COORDINATION (money agent): the
// FRESH-charge terminal-success path in handlers.go does NOT call
// ConsumePendingCode (the gate already burned the code with consume=true), so
// its mint-cooldown stamp survives — a customer who completes a fresh charge
// within the cooldown of their last mint and immediately requests a new code
// gets 429 until the window elapses. To re-arm immediate re-minting after a
// completed fresh charge, the money agent should call
// twofa.ClearMintCooldownForUser(userID) on that terminal-success path (the
// exported, coordination-actionable entry point documented on
// internal/twofa.ClearMintCooldownForUser).
const twoFAMintCooldown = 1 * time.Minute
// alertReissueFail surfaces a stranded-customer incident in the admin
// notification centre (reason 'critical_payment_log'). Round 2 Loop B finding
// 2: it is capped PER-ISSUE, NOT globally — the NOT EXISTS guard keeps exactly
// ONE unacknowledged row per (reason, user_id) (the booking is unresolvable at
// re-issue time), so one customer's alert is never suppressed by other users'
// rows filling the 'critical_payment_log' bucket, and the count-then-insert is
// atomic (a single INSERT ... WHERE NOT EXISTS — no TOCTOU). It is a dedicated
// local insert rather than the sweep's insertCriticalPaymentNotification so
// that the money agent's upcoming GLOBAL cap on the sweep insert (finding 1)
// can never swallow this alert — a stranded customer must always surface.
// Best-effort: a failure logs and the caller's CRITICAL log line still fires.
func alertReissueFail(ctx context.Context, q db.Querier, userID string) {
tag, err := q.Exec(ctx, `
INSERT INTO admin_notifications (reason, user_id, created_at)
SELECT 'critical_payment_log', $1, NOW()
WHERE NOT EXISTS (
SELECT 1 FROM admin_notifications an
WHERE an.reason = 'critical_payment_log'
AND an.user_id = $1
AND an.acknowledged_at IS NULL
)
`, userID)
if err != nil {
log.Printf("2FA: failed to insert critical-payment admin notification for reissue failure (user=%s): %v", userID, err)
return
}
if tag.RowsAffected() > 0 {
log.Printf("2FA: inserted critical-payment admin notification for reissue failure (user=%s) — customer stranded after a failed fresh saved-card charge", userID)
}
}
// generatePaymentsTwoFACode returns a random 6-digit verification code,
// mirroring the user package's generator (crypto/rand, uniform 0-999999).
func generatePaymentsTwoFACode() (string, error) {
n, err := rand.Int(rand.Reader, big.NewInt(1_000_000))
if err != nil {
return "", err
}
return fmt.Sprintf("%06d", n.Int64()), nil
}
// twoFAPendingCodeLifetime is how long a re-issued 2FA code stays valid,
// mirroring the user package's pending-code expiry.
const twoFAPendingCodeLifetime = 10 * time.Minute
// COORDINATION NOTE (Round 2 Loop B finding 1) — the shared notification flood
// cap now lives in crussell/internal/adminnotify
// (MaxUnacknowledgedCriticalLogs = 100 + CriticalLogsCapExceeded), applied
// ATOMICALLY (a conditional `INSERT ... SELECT ... WHERE (SELECT COUNT(*) ...)
// < $cap`) at every insert site this finding calls out. Status of each site:
//
// - THIS package's reissue-fail alert (reissueTwoFACodeAfterFailedCharge):
// capped PER-ISSUE instead (Round 2 Loop B finding 2) — the local
// alertReissueFail helper dedups atomically on (reason, user_id), so one
// customer's alert is never suppressed by other users' rows. NOT globally
// capped, and deliberately decoupled from sweep's generic insert so the
// money agent's global cap there can never swallow it.
//
// - auth/jwt.go VerifyRefreshToken's 'refresh_token_reuse' alert: NOW capped
// (Round 2 Loop B finding 1) — folded into its INSERT via the shared
// adminnotify cap.
//
// - handlers/webhooks/square.go (dispute/booking/unknown-event/orphan-replay)
// and handlers/user/account.go InsertSquareErasureCriticalNotification:
// NOW capped — same atomic fold.
//
// - handlers/payments/sweep.go:1556 insertCriticalPaymentNotification (the
// MONEY agent): STILL NEEDS the fold. Its INSERT ... SELECT ... WHERE NOT
// EXISTS is the same unbounded-across-accounts shape. Add
// `AND (SELECT COUNT(*) FROM admin_notifications _an WHERE _an.reason =
// 'critical_payment_log' AND _an.acknowledged_at IS NULL) < $N` (N =
// adminnotify.MaxUnacknowledgedCriticalLogs) to its WHERE clause.
//
// - handlers/scheduling/time-blockers.go:511 and internal/jobs/cleanup.go:333
// (jobs/scheduling agent): same shape on the GDPR-cleanup and log-scan
// paths — apply the identical fold.
//
// - main.go has NO admin_notifications insert sites (it only mounts the
// notification read/ack routes), so nothing to cap there.
//
// Fail-closed behaviour (a REFUSED re-issue still CRITICAL-logs and leaves the
// operator to mint manually) is unchanged — only the notification INSERT is
// bounded.