feat(canary): envfile generator (shuffled bait + embedded canary URL)
Generator builds the artifact per spec §9.6:
- extractIncludeKeys(t.Metadata) reads include_keys from JSON
metadata, defaults to ["aws", "db"] on missing/malformed metadata
(defensive fallback covers empty raw, invalid JSON, wrong type,
null value, empty array, missing field, all-whitespace entries)
- buildSections iterates recipe keys, appending the canary section
last: INTERNAL_METRICS_ENDPOINT=<baseURL>/c/<t.ID> +
INTERNAL_METRICS_TOKEN=tok_live_<32-char alnum>
- shuffleSections uses crypto/rand-driven Fisher-Yates so the canary
section can land at any position. Spec §9.6 line 1301 uses
math/rand.Shuffle; we use crypto/rand to stay consistent with the
Phase 0 supplement choice and the recipes package
- renderSections writes the fixed "# Production environment / NODE_ENV
/ PORT" header followed by each section's comment + key=value lines
- Artifact: KindText, ContentType "text/plain; charset=utf-8",
Filename default ".env"
Trigger is a byte-identical content-copy of webbug/docx/pdf: 200 +
pixel.Clone() GIF + cache headers + realIP triplet + nil-token-returns-
GIF defense-in-depth.
Tests (~30 cases):
- Generator: Type, KindText, ContentType, Filename defaulting (4),
header presence, canary URL embedded, canary token format
(tok_live_+32 alnum regex), trailing-slash trim, default
include_keys vs metadata-driven include_keys, unknown keys
skipped, 6 malformed-metadata fallback subcases, empty-string
filtering in keys list, shuffle variability (30 invocations →
canary at multiple positions), distinct outputs (random bait +
random shuffle), token id appears exactly once in output
- Trigger: GIF response shape, source-IP precedence (11 subcases
matching docx/pdf/kubeconfig), missing UA/Referer → nil pointers,
response body independence (mutate one, other unchanged),
nil-token still returns GIF + nil event
Registry wire-up next.