Cybersecurity-Projects/PROJECTS
CarterPerez-dev 13cd6cd1a3 feat(canary): pdf generator (byte-replace fixed-width URI action)
Generator runs at Token-create time. Substitutes the 76-byte placeholder
in the embedded template.pdf with the canary trigger URL padded to the
same byte length. Length preservation is critical: the PDF's xref table
indexes objects by byte offset, so anything that shifts bytes after the
catalog corrupts the file.

Algorithm (spec §9.4 lines 1145-1165):

  triggerURL := strings.TrimRight(baseURL, "/") + "/c/" + t.ID
  if len(triggerURL) > 76 → ErrTriggerURLTooLong
  padded := triggerURL + strings.Repeat("_", 76 - len(triggerURL))
  out := bytes.Replace(pdfTemplate, placeholder, padded, 1)

Defensive checks after substitution:
  - len(out) == len(pdfTemplate) (xref offsets stay valid)
  - bytes.Contains(out, triggerURL) (substitution actually happened)

Trigger is a content-copy of webbug's response (sanctioned per Phase
2/3/4 anti-relitigation set): 200 + 43-byte pixel.Clone() + Cache-Control
+ Pragma headers. Nil-token path returns the same artifact shape with no
event (spec §8.5 defense-in-depth). realIP / lastNonEmptyXFF /
optionalHeader are duplicated byte-identically with docx and slowredirect
— Phase 9 collapses this into a middleware helper.

Test suite (~32 cases, mirrors the docx Phase 4 shape plus PDF-specific
structural guards):

  Template invariants (5):
    - placeholder root byte-locatable exactly once (literal, not encoded)
    - full 76-byte placeholder byte-locatable exactly once
    - starts with %PDF- header
    - ends with %%EOF marker
    - pdfcpu.api.Validate passes on the raw template

  Generator behavior (10):
    - Type() is TypePDF, Artifact.Kind is KindFile
    - ContentType is application/pdf
    - Filename defaulting (nil / empty / whitespace / set)
    - trigger URL precedence (trailing-slash trim, subpath preserve,
      distinct ids → distinct outputs)
    - output length == template length (xref preservation)
    - output contains trigger URL
    - placeholder root removed from output
    - URL > 76 chars returns ErrTriggerURLTooLong (errors.Is)
    - boundary URL at exactly 76 chars succeeds (zero padding)
    - api.Validate still passes on substituted output
    - substitution preserves byte offset (URL occupies the same position
      the placeholder did)

  Trigger behavior (~16 incl subtests):
    - response shape identical to webbug (200 + pixel.Clone + headers)
    - records event with token id / source IP / UA / Referer
    - source-IP precedence (CF > XFF rightmost > XRI > RemoteAddr),
      11 subcases mirroring docx + webbug for full symmetry
    - missing UA/Referer → nil pointers
    - per-call body is an independent slice (mutate one, other unchanged)
    - nil-token path returns GIF + nil event (FK-safe)

go test -race -timeout=60s ./internal/token/generators/pdf/... passes.
2026-05-13 13:54:24 -04:00
..
advanced fix: pin pnpm to v10 in CI and add .npmrc to all frontend projects 2026-05-08 05:46:38 -04:00
beginner feat(canary): pdf generator (byte-replace fixed-width URI action) 2026-05-13 13:54:24 -04:00
intermediate fix: pin pnpm to v10 in CI and add .npmrc to all frontend projects 2026-05-08 05:46:38 -04:00