Files
zeekayandHanzo Dev e2faccf345 pci: hosted fields are the only card entry path
billing.hanzo.ai shipped two card forms. One is Square Web Payments hosted
fields — the card lives in Square's iframe and we only ever see a single-use
nonce. The other read the PAN, CVC and expiry into React state and POSTed them
as plain JSON from our own origin to `${NEXT_PUBLIC_COMMERCE_API}/billing/card/
tokenize`, defaulting to the banned `/api/v1` prefix. That put this origin in
PCI SAQ-D scope, sitting next to the very control that keeps us in SAQ-A, and it
could only ever fail: the destination is deleted in this same change.

Nobody wrote that form here. `billing-shell.tsx` imported billing components
from the published `@hanzo/ui/billing` barrel, and that barrel is ONE
pre-bundled module carrying the raw-PAN form; a single named import dragged it
in, twice over (card-form + payment-manager), with nothing in this repo to grep
for. The barrel was redundant anyway — every component it supplied already
exists in `lib/billing`, with real types instead of the `FC<any>` ambient shim
`hanzo-ui.d.ts` declared. So the import is gone and `lib/billing` is the ONE
billing surface. The published bundle stays a dependency only for navigation.

Dropping the `any` shim surfaced two errors it had been hiding, both fixed here:
SECTION_TITLES carried four sections the shell does not render (Cost sits inside
usage, Members inside team, Business and Tax inside settings), and commerce
returns `tags` as a scalar or a list where TransactionRecord wants a list.

Also deleted, all of it dead and all of it PAN-shaped:
  - the local CardForm in lib/billing/payment-manager.tsx — unreferenced since
    SquareCardForm took the card tab, but still an unmasked PAN input
  - lib/components/animated-card.tsx — a live card-number preview, whose only
    consumer was that form
  - SquareCardForm's onCvvFocus effect, which reached into Square's iframe via
    contentWindow.document to drive the preview's flip. Cross-origin, so it
    always threw into a swallowed catch — dead code that also undermined the
    exact boundary hosted fields exist to draw. Nothing in first-party JS
    touches the hosted fields now.
  - commerce-client's processCardPayment, which took {number, expMonth, expYear,
    cvc} and posted to a route no server registers

`scripts/pci-scope.mjs` keeps it deleted, and is wired to `pnpm lint` in place
of the `echo 'lint ok'` placeholder. It scans SOURCE for what we write and the
BUILT BUNDLE for what we import — the second is the one that would have caught
this, since the form arrived through a dependency. It refuses a vacuous run: a
scan that examined zero files fails rather than passes.

Verified on the rebuilt bundle: 0 hits for card/tokenize, expiry_month,
api.hanzo.ai/api/v1 and the form's own marker string; 4 for web.squarecdn.com.
The deployed bundle has 2 of each of the first three today.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-01 21:55:19 -07:00
..