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:
parent
a46fa4465e
commit
fe7d641af8