Files
certctl/web
shankar0123 f502da306f feat(gui/approvals): payload preview with profile-edit diff + cert-issuance preview (A-5)
The MED-10 closure claim in `cowork/auth-bundles-audit-2026-05-10.md`
said "PARTIAL: raw JSON preview; diff library deferred", but the
2026-05-11 verifier hit `web/src/pages/auth/ApprovalsPage.tsx` and
found ZERO payload rendering — only a doc-comment mention. Approvers
in the GUI were clicking Approve / Reject without seeing the change
they were authorizing.

That defeats the entire two-person-approval primitive. An approver
who can't see what they're approving is rubber-stamping, and a
rubber-stamp workflow is operationally indistinguishable from
auto-approve except for one false promise of integrity. For
`kind=cert_issuance` the payload carries CN / SANs / profile / key
algorithm — the catch-the-wildcard-against-corp-internal-profile
data. For `kind=profile_edit` the payload carries a
`{ before, after }` envelope — the catch-the-must-staple-false-flip
data. Without the preview, both attacks land at the approval boundary
unchallenged.

Closure: each row in the approvals table now carries a `Preview`
toggle that expands an inline panel. Dispatch by `kind`:

  - profile_edit → ProfileEditDiff. Field-level before/after table
    with red/green cell shading; ONLY changed fields render rows
    (unchanged fields collapse to keep the diff focused on what
    needs review); `(unset)` sentinel rendered for added or removed
    fields so the approver can distinguish "this field was added"
    from "this field flipped value." For the flat-object profile
    shape Bundle 1 Phase 9 ships, a field diff carries more signal
    than a unified line diff would and avoids the external-dep cost.

  - cert_issuance → IssuanceRequestPreview. Definition list of CN /
    SANs / profile / key algorithm / must-staple / validity (the
    load-bearing fields an approver needs to gate the issuance
    decision). Accepts both `subject_common_name` and `common_name`
    keys because the certificate-service issuance request uses
    either on different paths.

  - any other kind → generic <pre> JSON dump. Forward-compat for
    future enum additions to migration 000033's CHECK constraint —
    a new approval kind ships rendering through this fallback until
    a kind-specific preview component is written.

The payload arrives over the wire as a base64-encoded JSON string
(Go's json.Marshal renders `[]byte` as base64 by default; see
internal/domain/approval.go:41 where `Payload []byte`). The new
exported `decodePayload(payload)` helper atob()s + JSON.parse()s,
returning null on any failure. Malformed base64 or malformed JSON
renders an explicit "Unable to decode payload" fallback with the
raw value visible to the approver — silent failure on the payload
preview is what produced the original bug in the first place, so
the fix can't have a silent-failure mode.

Component dispatch and base64 decode are also exposed for testing:

  decodePayload(undefined) → null
  decodePayload('') → null
  decodePayload(btoa(JSON.stringify(x))) → x
  decodePayload('!!!not-base64!!!') → null (atob throws)
  decodePayload(btoa('not a json document')) → null (JSON.parse throws)

Each interactive element carries a data-testid so future E2E
coverage can exercise the contract without brittle CSS selectors —
same pattern as Bundle 1's RolesPage.

Tests (13 total, all passing under vitest):

Page-level (8):
  A-5 Preview button toggles the payload panel
  A-5 ProfileEdit kind renders field diff with changed-only rows
  A-5 ProfileEdit before/after values are visible in the diff cells
  A-5 ProfileEdit with no changes renders empty-state
  A-5 CertIssuance renders definition list with SANs + profile + key algo
  A-5 Unknown kind falls back to generic JSON pre block
  A-5 Empty payload renders the "No payload attached" sentinel
  A-5 Malformed base64 payload renders the decode-error fallback

decodePayload pure-function suite (5):
  returns null for undefined input
  returns null for empty string
  round-trips base64-encoded JSON
  returns null on malformed base64
  returns null on valid base64 of non-JSON content

Verify gate green: tsc --noEmit clean; vitest passes all 17 tests
in ApprovalsPage.test.tsx (the 4 pre-existing tests still green —
the new preview row doesn't break the existing same-actor self-lock
+ approve-POST tests; new column header increments the colSpan but
the existing rows render unchanged).

Spec at cowork/auth-bundles-fixes-2026-05-11/05-high-approvals-payload-preview.md.
Audit doc: MED-10 row in `cowork/auth-bundles-audit-2026-05-10.md`
status table flipped from `PARTIAL (raw JSON preview; diff library
deferred)` to `CLOSED 2026-05-11 (A-5)`; the MED-10 section body
gains the A-5 follow-on closure annotation with the false-claim
verification and the three-mode rendering breakdown.
Operator-visible CHANGELOG.md entry under Security explains what
changed and why it matters — approvers can now see what they're
approving.
2026-05-11 10:57:07 +00:00
..