chore(canary-phase2): generator interface + webbug complete

Phase 2 of the 18-phase plan. Lands the plugin scaffolding (Generator
interface, shared transparent-pixel package, generator registry) and the
first concrete generator (webbug). Phase 2 is intentionally the smallest
phase — subsequent phases add slowredirect, docx, pdf, kubeconfig,
envfile, and mysql generators against the same Generator interface.

Implementation commits (oldest → newest):

  6cc724c1 feat(canary): Generator interface for token-type plugins
  0b6e586e feat(canary): shared 43-byte transparent GIF for image triggers
  9c0ad749 feat(canary): webbug generator (Generate + Trigger)
  bb95f745 feat(canary): generator registry (token.Type → Generator)
  c39b56b9 fix(canary): clear pre-audit lint debt + migrate golangci config to v2
  1a240952 fix(canary-phase2): address audit findings before rollup

Deliverables vs. plan:

- Generator interface (spec §6.2): Type/Generate/Trigger contract with
  ArtifactKind url|file|text|connection_string, Artifact discriminated
  by Kind, TriggerResponse value type so generators stay pure.
- Pixel package: 43-byte canonical GIF89a literal exposed via
  pixel.Clone() (immutable source, independent slice per call) +
  pixel.Len() helper + ContentType const.
- Webbug generator (spec §9.1 + §8.5): Generate returns KindURL
  artifact with baseURL/c/{id} (trailing slash trimmed). Trigger returns
  200 + image/gif + 43-byte pixel + no-store cache headers.
  realIP precedence: CF-Connecting-IP > X-Forwarded-For (rightmost
  non-empty) > X-Real-IP > net.SplitHostPort(RemoteAddr). Handles
  IPv4/IPv6 (including bracketed form). Empty XFF entries (trailing
  comma, all-empty list) fall through to the next source. Nil token
  returns nil event + non-nil GIF response — the contract is explicit
  in the return shape, so the future Phase 9 handler cannot accidentally
  insert an event row with empty TokenID (which would fail the events
  → tokens FK anyway).
- Generator registry: lives in backend/internal/token/generators/registry/
  (sub-package, NOT the generators package itself) to break the
  generators → webbug → generators import cycle the implementation plan
  inadvertently created. registry.Config + registry.Registry +
  registry.Build. Phase 2 registers webbug only; cardinality test
  asserts exactly 1 and enumerates the 6 future types as absent.
- Cross-cutting cleanup (c39b56b9): pre-audit gate found Phase 0/1
  golangci-lint debt the prior phase audits didn't catch. Per
  fix-in-phase rule, cleared all 14 (5 errcheck, 6 golines, 1 funlen
  via run() → run+initTelemetry+mountRouter+gracefulShutdown split,
  1 govet shadow, plus the 1 Phase-2-introduced funlen-on-test that
  the broken .golangci.yml v1-syntax exclude-rules failed to suppress
  under golangci-lint v2). Also migrated .golangci.yml to v2 schema
  (linters.exclusions.{paths,rules}) and added gosec G706 to excludes
  (false-positive log-injection on slog structured-logging call sites).

Phase 2 audit — TWO agents in parallel per standing pattern:

  superpowers:code-reviewer       → PASS (9 findings, all cleared in-phase
                                          by commit 1a240952)
  general-purpose (spec-adherence)→ PASS (every spec contract item
                                          verified; 3 known deltas
                                          accepted: Type() refined to
                                          token.Type, registry moved to
                                          subpackage to break cycle,
                                          optionalHeader bridges
                                          plan-vs-schema pointer types)

Findings cleared in-phase (from code-reviewer):

  MEDIUM — realIP fallback returned "IP:port"; now net.SplitHostPort
  MEDIUM — XFF rightmost empty entry skipped fallback; now walks right-
           to-left and falls through cleanly
  MEDIUM — pixel.TransparentGIF was exported mutable []byte; now
           unexported + pixel.Clone() per call
  LOW    — nil token returned non-nil event with empty TokenID; now
           returns nil event (explicit contract)
  LOW    — no IPv6 coverage in IP precedence tests; added 3 IPv6 cases
           + XFF IPv6 + port-less RemoteAddr
  NIT    — gracefulShutdown swallowed shutdown errors; now errors.Join

Accepted as-is (NIT, Phase 3+ concern):

  NIT — Artifact discriminated-union may want sealed-interface
        refactor once non-URL kinds land
  NIT — Registry exposed bare map (acceptable: read-only post-Build,
        Go memory model permits concurrent map reads with no writes)
  NIT — registry.Build accepts but ignores its Config (signature
        reserved for future stateful generators like mysql)

Forward-looking notes for Phase 3 (NOT deferred items, just heads-up):

  - Phase 3's main.go wire-up cannot copy spec §6.3's
    `generators.BuildRegistry(cfg.Canary)` verbatim; the call is now
    registry.Build(registry.Config{...}) and the cfg.Canary field will
    need to be declared on config.Config.

Deferred items: NONE. BACKLOG.md open section remains empty.

Quality gates (final, post-fix):

  go build ./...                      OK
  go vet ./...                        OK
  go test -race ./...                 PASS (4 packages, 20 unit tests)
  go test -tags=integration -race     PASS (token + event integration,
                                            ~12s testcontainers)
  golangci-lint run ./...             0 issues

Audited: PASS.
This commit is contained in:
CarterPerez-dev 2026-05-12 02:57:29 -04:00
parent 1a240952c7
commit 8207b2747b

Diff Content Not Available