chore(canary-phase3): slowredirect generator complete

Phase 3 of 18. Adds the second token type: a slowredirect generator
that returns a `KindURL` artifact pointing at `/c/{id}`, serves an
embedded HTML+JS fingerprinting page on trigger, and exposes a
sibling `POST /c/{id}/fingerprint` endpoint that enriches the most
recent matching event's `Extra` JSONB via the existing
event.Repository.AttachFingerprint method. Generator interface
contract from Phase 2 unchanged; registry adds the second entry.

Implementation commits:
  71823f5d feat(canary): slowredirect generator (Generate + Trigger + template)
  6dd520f9 feat(canary): slowredirect fingerprint endpoint
  be13d2f1 feat(canary): register slowredirect generator in registry
  a46fa446 fix(canary-phase3): address audit findings before rollup

Audit outcome — TWO agents (superpowers:code-reviewer + general-purpose
spec adherence) dispatched in parallel. Code-reviewer returned FAIL
with 1 blocker + 4 should-fix + 7 nits; spec-adherence returned PASS-
with-1-violation. Both findings cleared in commit a46fa446 per the
fix-in-phase rule:

  • BLOCKER B1 — template.html used `{{...|js}}` which Go's html/
    template double-escapes in a JS string-literal context. The
    on-the-wire output became `\\u003D` / `\\u0026` (double backslash),
    which JS parses as literal backslash-u-text rather than `=` / `&`.
    Realistic URLs like `?utm_source=newsletter&utm_medium=email`
    were silently corrupted before `window.location.replace`. The
    bug was faithfully transcribed from spec §9.2 lines 1031, 1038,
    1045 — the spec's parenthetical claiming `js` is a custom func
    is factually wrong. Resolution: drop `| js` from both JS-context
    actions; rely on html/template's built-in jsstrescaper.
    Regression test TestTrigger_DestinationRoundtripsThroughJSStringDecode
    asserts the absence of `\\u003D` / `\\u0026` for a real URL.
    Spec doc (local-only) updated to match.

  • SPEC-VIOLATION #1 — nil-token Trigger returned `404 + "Not Found"`,
    contradicting spec §8.5 ("token not found | 200, ... deliberately
    do NOT 404") and §8.5 line 964 ("/c/* endpoints never return JSON
    errors"). 404 is a token-enumeration oracle. Resolution: nil-token
    branch renders the same template with a benign decoy destination
    ("/") and returns 200 + text/html + the full CSP/cache header set.
    Body is byte-shape indistinguishable from a valid response except
    for the embedded destination URL. The `(nil event, &resp, nil
    error)` return shape from the Phase 2 anti-relitigation rule is
    preserved. New test
    TestTrigger_TokenNotFound_HasSameResponseShapeAsValidToken
    asserts the indistinguishability invariant.

  • SHOULD-FIX S1 — extractDestination allowlists `http(s)://`
    (case-insensitive); other schemes (`javascript:`, `data:`,
    `file:`, `vbscript:`, scheme-relative `//host`, no-scheme) now
    return the new exported ErrInvalidDestinationScheme. Phase 9's
    upstream `validate:"url"` is necessary but not sufficient (the
    validator's `url` tag accepts any scheme). 8-case
    TestGenerate_RejectsDangerousDestinationSchemes + 4-case
    TestGenerate_AcceptsHTTPAndHTTPSSchemes added.

  • SHOULD-FIX S3 — fingerprint handler silently 204s any
    Content-Type that doesn't start with `application/json` (the
    embedded template always sends JSON). Rejects before the
    MaxBytesReader read. Test added.

  • SHOULD-FIX S4 — added integration tests for oversize body
    (128 KiB → 204, Extra untouched) and empty token id
    (`/c//fingerprint`).

  • SHOULD-FIX S2 — nonce-based CSP suggested as a senior-bar polish.
    Deferred: this is a spec deviation (§9.2 line 1047 specifies
    `script-src 'unsafe-inline'` verbatim) and crosses into Phase 9
    territory (global SecurityHeaders middleware). Re-evaluate when
    Phase 9 wires the trigger handler.

  • Nits N1-N7 reviewed; the realIP/lastNonEmptyXFF duplication
    is sanctioned per the Phase 2 anti-relitigation note (Phase 9
    middleware promotion is the cleanup point). Other nits either
    correct as-is or deferred to Phase 9+ tasks.

Pre-rollup gate clean: `go build ./...`, `go vet ./...`,
`go test -race -timeout=60s ./...` (unit), `go test -tags=integration
-race -timeout=300s ./internal/token/... ./internal/event/...`, and
`golangci-lint run ./...` (0 issues). BACKLOG.md open section
remains empty.

Registry cardinality: 2 of 7 token types live (webbug + slowredirect).
Five remain: docx (Phase 4), pdf (Phase 5), kubeconfig (Phase 6),
envfile (Phase 7), mysql (Phase 8).
This commit is contained in:
CarterPerez-dev 2026-05-12 06:47:46 -04:00
parent a46fa4465e
commit fe7d641af8

Diff Content Not Available