Commit Graph

825 Commits

Author SHA1 Message Date
Carter Perez 46231c15ed
Merge pull request #252 from CarterPerez-dev/dependabot/uv/PROJECTS/advanced/encrypted-p2p-chat/backend/idna-3.15
chore(deps): bump idna from 3.11 to 3.15 in /PROJECTS/advanced/encrypted-p2p-chat/backend
2026-05-22 10:13:38 -04:00
Carter Perez c1c4241638
Merge pull request #254 from CarterPerez-dev/dependabot/uv/PROJECTS/beginner/c2-beacon/backend/idna-3.15
chore(deps): bump idna from 3.11 to 3.15 in /PROJECTS/beginner/c2-beacon/backend
2026-05-22 10:13:20 -04:00
dependabot[bot] 3474d7711b
chore(deps): bump mlflow
Bumps [mlflow](https://github.com/mlflow/mlflow) from 3.9.0 to 3.11.1.
- [Release notes](https://github.com/mlflow/mlflow/releases)
- [Changelog](https://github.com/mlflow/mlflow/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mlflow/mlflow/compare/v3.9.0...v3.11.1)

---
updated-dependencies:
- dependency-name: mlflow
  dependency-version: 3.11.1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-21 20:33:21 +00:00
dependabot[bot] 15a9429d0f
chore(deps): bump idna in /PROJECTS/beginner/c2-beacon/backend
Bumps [idna](https://github.com/kjd/idna) from 3.11 to 3.15.
- [Release notes](https://github.com/kjd/idna/releases)
- [Changelog](https://github.com/kjd/idna/blob/master/HISTORY.md)
- [Commits](https://github.com/kjd/idna/compare/v3.11...v3.15)

---
updated-dependencies:
- dependency-name: idna
  dependency-version: '3.15'
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-21 20:32:07 +00:00
dependabot[bot] 1e44bf6384
chore(deps): bump idna in /PROJECTS/advanced/encrypted-p2p-chat/backend
Bumps [idna](https://github.com/kjd/idna) from 3.11 to 3.15.
- [Release notes](https://github.com/kjd/idna/releases)
- [Changelog](https://github.com/kjd/idna/blob/master/HISTORY.md)
- [Commits](https://github.com/kjd/idna/compare/v3.11...v3.15)

---
updated-dependencies:
- dependency-name: idna
  dependency-version: '3.15'
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-19 16:30:52 +00:00
dependabot[bot] d43054c523
chore(deps): bump idna in /PROJECTS/foundations/http-headers-scanner
Bumps [idna](https://github.com/kjd/idna) from 3.13 to 3.15.
- [Release notes](https://github.com/kjd/idna/releases)
- [Changelog](https://github.com/kjd/idna/blob/master/HISTORY.md)
- [Commits](https://github.com/kjd/idna/compare/v3.13...v3.15)

---
updated-dependencies:
- dependency-name: idna
  dependency-version: '3.15'
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-19 16:23:33 +00:00
dependabot[bot] 3897a542e6
chore(deps): bump idna in /PROJECTS/advanced/bug-bounty-platform/backend
Bumps [idna](https://github.com/kjd/idna) from 3.11 to 3.15.
- [Release notes](https://github.com/kjd/idna/releases)
- [Changelog](https://github.com/kjd/idna/blob/master/HISTORY.md)
- [Commits](https://github.com/kjd/idna/compare/v3.11...v3.15)

---
updated-dependencies:
- dependency-name: idna
  dependency-version: '3.15'
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-19 16:19:13 +00:00
dependabot[bot] 341d04a160
chore(deps): bump idna from 3.11 to 3.15 in /PROJECTS/beginner/keylogger
Bumps [idna](https://github.com/kjd/idna) from 3.11 to 3.15.
- [Release notes](https://github.com/kjd/idna/releases)
- [Changelog](https://github.com/kjd/idna/blob/master/HISTORY.md)
- [Commits](https://github.com/kjd/idna/compare/v3.11...v3.15)

---
updated-dependencies:
- dependency-name: idna
  dependency-version: '3.15'
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-19 16:08:43 +00:00
dependabot[bot] 360df54954
chore(deps): bump idna in /PROJECTS/advanced/api-rate-limiter
Bumps [idna](https://github.com/kjd/idna) from 3.11 to 3.15.
- [Release notes](https://github.com/kjd/idna/releases)
- [Changelog](https://github.com/kjd/idna/blob/master/HISTORY.md)
- [Commits](https://github.com/kjd/idna/compare/v3.11...v3.15)

---
updated-dependencies:
- dependency-name: idna
  dependency-version: '3.15'
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-19 15:59:27 +00:00
dependabot[bot] 9b4b6f7772
chore(deps): bump idna in /PROJECTS/advanced/ai-threat-detection/backend
Bumps [idna](https://github.com/kjd/idna) from 3.11 to 3.15.
- [Release notes](https://github.com/kjd/idna/releases)
- [Changelog](https://github.com/kjd/idna/blob/master/HISTORY.md)
- [Commits](https://github.com/kjd/idna/compare/v3.11...v3.15)

---
updated-dependencies:
- dependency-name: idna
  dependency-version: '3.15'
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-19 15:56:13 +00:00
dependabot[bot] c9fda34256
chore(deps): bump github.com/go-git/go-git/v5
Bumps [github.com/go-git/go-git/v5](https://github.com/go-git/go-git) from 5.19.0 to 5.19.1.
- [Release notes](https://github.com/go-git/go-git/releases)
- [Changelog](https://github.com/go-git/go-git/blob/main/HISTORY.md)
- [Commits](https://github.com/go-git/go-git/compare/v5.19.0...v5.19.1)

---
updated-dependencies:
- dependency-name: github.com/go-git/go-git/v5
  dependency-version: 5.19.1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-19 15:38:06 +00:00
dependabot[bot] 3250ddbbe3
chore(deps): bump github.com/redis/go-redis/v9
Bumps [github.com/redis/go-redis/v9](https://github.com/redis/go-redis) from 9.7.0 to 9.7.3.
- [Release notes](https://github.com/redis/go-redis/releases)
- [Changelog](https://github.com/redis/go-redis/blob/master/RELEASE-NOTES.md)
- [Commits](https://github.com/redis/go-redis/compare/v9.7.0...v9.7.3)

---
updated-dependencies:
- dependency-name: github.com/redis/go-redis/v9
  dependency-version: 9.7.3
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-19 14:18:33 +00:00
dependabot[bot] 05ef94cc93
chore(deps): bump ws (via audit fix)
Bumps [ws](https://github.com/websockets/ws) from 8.18.3 to 8.20.1.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/8.18.3...8.20.1)

---
updated-dependencies:
- dependency-name: ws
  dependency-version: 8.20.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-18 21:10:02 +00:00
Carter Perez cad4abf19a
Merge pull request #241 from CarterPerez-dev/dependabot/go_modules/PROJECTS/beginner/canary-token-generator/backend/github.com/go-viper/mapstructure/v2-2.4.0
chore(deps): bump github.com/go-viper/mapstructure/v2 from 2.2.1 to 2.4.0 in /PROJECTS/beginner/canary-token-generator/backend
2026-05-18 15:34:04 -04:00
dependabot[bot] 7623b8f557
chore(deps): bump github.com/go-viper/mapstructure/v2
Bumps [github.com/go-viper/mapstructure/v2](https://github.com/go-viper/mapstructure) from 2.2.1 to 2.4.0.
- [Release notes](https://github.com/go-viper/mapstructure/releases)
- [Changelog](https://github.com/go-viper/mapstructure/blob/main/CHANGELOG.md)
- [Commits](https://github.com/go-viper/mapstructure/compare/v2.2.1...v2.4.0)

---
updated-dependencies:
- dependency-name: github.com/go-viper/mapstructure/v2
  dependency-version: 2.4.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-18 14:03:28 +00:00
dependabot[bot] adf26769a9
chore(deps): bump flatted
Bumps [flatted](https://github.com/WebReflection/flatted) from 3.3.3 to 3.4.2.
- [Commits](https://github.com/WebReflection/flatted/compare/v3.3.3...v3.4.2)

---
updated-dependencies:
- dependency-name: flatted
  dependency-version: 3.4.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-18 14:02:41 +00:00
Carter Perez c1f5322ea2
Merge branch 'main' into dependabot/npm_and_yarn/PROJECTS/beginner/canary-token-generator/frontend/axios-1.15.2 2026-05-18 10:00:49 -04:00
dependabot[bot] a71926ca09
chore(deps): bump fast-uri
Bumps [fast-uri](https://github.com/fastify/fast-uri) from 3.1.0 to 3.1.2.
- [Release notes](https://github.com/fastify/fast-uri/releases)
- [Commits](https://github.com/fastify/fast-uri/compare/v3.1.0...v3.1.2)

---
updated-dependencies:
- dependency-name: fast-uri
  dependency-version: 3.1.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-18 04:48:05 +00:00
dependabot[bot] edf66fb576
chore(deps): bump axios
Bumps [axios](https://github.com/axios/axios) from 1.13.2 to 1.15.2.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.13.2...v1.15.2)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.15.2
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-18 04:47:56 +00:00
Carter Perez fa70d35345
Update README.md 2026-05-18 00:46:55 -04:00
Carter Perez b247504c35
Update README.md 2026-05-18 00:46:31 -04:00
Carter Perez 79ca7006b3
Merge pull request #235 from CarterPerez-dev/project/canary-token-generator
Project/canary token generator
2026-05-18 00:44:30 -04:00
CarterPerez-dev 615efe051d update 2026-05-18 00:41:25 -04:00
CarterPerez-dev de6ac71954 chore(canary): drop stray /healthcheck binary + ignore cmd build artifacts
go build ./cmd/healthcheck (run for verification, not via the bin/ recipe)
drops the binary in the working dir. Untracked previously; landed in the
last commit by accident. Gitignore now lists each cmd binary explicitly so
the same mistake can't recur for canary, healthcheck, or the two pdf/docx
template builders.
2026-05-18 00:03:20 -04:00
CarterPerez-dev 10321a9a61 fix(canary): prod compose can actually start
Three things that all blocked tunnel-start:

1. justfile: dropped set dotenv-load / set export — they pulled
   .env.development into every recipe shell, where empty values
   beat docker compose --env-file .env per shell-env precedence
   (POSTGRES_PASSWORD was the visible casualty). Dev recipes now
   pass --env-file .env.development explicitly so they're not
   affected by the change.

2. canary image is distroless/static, so the wget-based healthcheck
   had no binary to run and every check failed → nginx/tunnel never
   started. Added cmd/healthcheck (tiny Go HTTP probe of /healthz),
   built alongside canary, swapped compose healthcheck to /healthcheck.

3. Added tunnel-build and tunnel-rebuild recipes so the next person
   doesn't have to remember which compose files the tunnel overlay
   needs.
2026-05-18 00:02:51 -04:00
CarterPerez-dev 9655120ebe chore(canary): phase B audit fixes + frontend asset rename + gitignore
Audit Phase B (backend): F5 trusted-proxy gate wiring, F7 ErrValidation
sentinel, F8 dedup unique-IP semantic via SCard, F9 notify worker pool +
graceful Shutdown, F12 token ID collision retry.

Frontend: public/asset/ → public/assets/ rename + index.html paths.

Housekeeping: .npmrc strict-dep-builds=false (pnpm fix), .gitignore now
excludes all .env.* except .env.example.
2026-05-17 23:34:42 -04:00
CarterPerez-dev d96de872b0 chore(canary): halve webbug pixel.jpg dimensions (736→368) 2026-05-17 22:58:53 -04:00
CarterPerez-dev 159c942551 feat(canary): webbug serves a real (visible) image instead of 1x1 GIF
Operator request: give the webbug some personality. Trigger response is now
a small JPEG embedded into the binary via //go:embed, served from
backend/internal/token/generators/webbug/asset/pixel.jpg. Default is a
545-byte 32x32 yellow placeholder I generated with imagemagick — replace
the file with any image (same path, same filename, doesn't matter what
size or content) and the next backend rebuild picks it up. No other code
needs to change.

Scope: ONLY webbug. The docx, pdf, and envfile generators keep their own
Trigger functions that still call pixel.Clone() for the canonical
invisible 1x1 GIF — those types embed the trigger URL inside their
respective artifact bytes (a Word footer, a PDF /AA action, a fake .env
INTERNAL_METRICS_ENDPOINT line) and need the response to stay invisible
so the attacker doesn't notice they pinged us. Stealth preserved where
it matters.

The pixel package itself is untouched (still has the 43-byte GIF
constant); only webbug stops importing it.

Tradeoff baked in: a webbug whose response is a visible image is
*technically* less stealthy than the canonical 1x1 — the attacker can
see "huh there's a small image here." But the visible mechanism is real
too (think Mailchimp / SendGrid email-open tracking, which use sized
"social proof" images, not just 1x1s), so this is a legitimate variant.
For an operator who wants strict 1x1-invisible behavior in prod, the
revert is a 3-line change in webbug/generator.go (swap embed for
pixel.Clone() again).

Tests updated: webbug/generator_test.go no longer asserts gifByteLength
or pixel.Clone() equality; it now asserts the body starts with the JPEG
SOI marker (0xff 0xd8) + ContentType is image/jpeg. The
"independent-copy-per-call" invariant test still passes (bytes.Clone in
the Trigger ensures each Body slice is a fresh allocation).

Cache headers unchanged (no-store / no-cache / must-revalidate) so every
fetch hits the trigger handler.
2026-05-17 22:55:47 -04:00
CarterPerez-dev 4b54c1b825 chore(canary): align e2e + handler tests with mysqlEnabled signature
Upstream 8d2599c0 added a mysqlEnabled parameter to token.NewHandler and
took a long line over the limit in handler_test. Pass the parameter from
the e2e stack (true: integration test exercises mysql token creation) and
reflow the long handler_test return so golines is happy.
2026-05-17 22:55:28 -04:00
CarterPerez-dev 8d2599c01a feat(canary): mark MySQL type as disabled when mysql.enabled=false
Operator request: when MYSQL_FAKE_ENABLED=false (the dev default + the only
viable setting behind Cloudflare Tunnel since CF Tunnel doesn't carry raw
TCP), surface that in the UI so users can see the species but understand
they can't deploy it on this instance. Avoids the silent-broken case where
someone creates a mysql token, gets a connection_string artifact, points
their MySQL client at it, and just times out with no clue why.

Backend (3-line change):
- TypeDescriptor gains `Enabled bool` + optional `DisabledReason string`
- TypeDescriptors() now takes `mysqlEnabled bool`; mysql is the one whose
  Enabled tracks the flag, all others are always true (they don't need
  any deployment-level infra beyond HTTP)
- Handler stores mysqlEnabled, passes to descriptors call; main.go wires
  cfg.MySQL.Enabled in. Handler tests updated to pass false explicitly.

Frontend (matching schema + visual treatment):
- typeDescriptorSchema picks up `enabled` (default true for backward compat)
  and `disabled_reason` (optional string)
- TypeCard reads descriptor.enabled, sets `data-disabled` on the label,
  disables the radio input, replaces the blurb with the disabled_reason
  when present (so the user sees WHY rather than guessing), and renders
  a small "disabled" pill in the top-right corner of the card
- SCSS: dashed border, muted palette, opacity 0.55, glyph faded, cursor
  not-allowed, hover effect suppressed — reads as "shelf is here but the
  specimen is checked out" rather than a broken button

In the operator UI: mysql card renders ghosted with the explanatory text
"Requires direct TCP exposure (port 3306). Not reachable via Cloudflare
Tunnel — only enable on a VPS with raw TCP access." Selecting it is
mechanically impossible (input is disabled), no form-level guard needed.

Flip MYSQL_FAKE_ENABLED=true in env when you stand up a VPS later and the
card re-enables with no other code changes.

Backend builds clean + token-package tests pass.
Frontend gates green: typecheck + biome + lint:scss + build (431ms).
2026-05-17 22:45:53 -04:00
CarterPerez-dev 22f1daf431 test(canary): e2e suite — create→trigger→notify→manage across all 7 types
Audit F23: integration tests covered repositories and individual handlers
but no test exercised the full chain. The PDF padding bug (F2) had passing
unit tests for years because the substring-only assertion masked the
embedded underscore padding.

New //go:build integration test in internal/server spins up Postgres via
testcontainers + miniredis, walks each of webbug, slowredirect, docx, pdf,
kubeconfig, envfile, mysql: POSTs /api/tokens, extracts the trigger URL
from the artifact bytes the way a victim would (regex for non-binary, ZIP
walk for docx, base64 decode for pdf), GETs the URL, then asserts the
manage view sees the event and the notifier fired. PDF subtest asserts the
byte after the embedded /c/<id> is not '_' — the structural check that
catches F2-style regressions.
2026-05-17 19:08:12 -04:00
CarterPerez-dev 2c033c58f2 fix(canary): webhook sender blocks SSRF to private/loopback/IMDS hosts
Audit F3: validateURL checked scheme/host/userinfo but never resolved the
host, so an operator-supplied webhook_url could point at the canary's own
Redis (redis:6379), Postgres, link-local IMDS (169.254.169.254), or any
RFC1918 host on the docker network or VPS subnet — a classic confused-deputy
SSRF triggered by self-triggering a token after creation.

validateURL now resolves the hostname (or parses a literal IP) and rejects
loopback, RFC1918, CGNAT, link-local, multicast, unspecified, IMDS, and
IPv6 unique-local. The default HTTP client also installs a DialContext that
re-checks the dialed IP for defense-in-depth against DNS rebinding. The
Config gains an AllowPrivateHosts flag (default false) that test code opts
into when targeting httptest.NewServer.
2026-05-17 19:08:01 -04:00
CarterPerez-dev 30ebda39d5 fix(canary): PDF trigger URL uses ?p= query pad; handler trims trailing _
Audit F2: the PDF placeholder was padded with underscores embedded directly
in the /URI path, so Acrobat fetched /c/<id>____________ and the chi route
extracted the padded id. Lookup returned ErrNotFound, the webbug fallback
served a 1x1 GIF, and event recording skipped because tok was nil. PDF
tokens silently never recorded events.

Part B (durable): pad inside a ?p= query string so the trigger path stays
canonical and chi extracts the real id. Part A (defense in depth): handler
TrimRight on _ so PDFs already in the field also resolve correctly. Added
two regression tests: byte-after-id assertion in the PDF artifact, and the
handler's trim path.
2026-05-17 19:07:50 -04:00
CarterPerez-dev 4382ba5e17 fix(canary): CSP allows challenges.cloudflare.com for Turnstile
Audit F10: script-src 'self' blocked the Turnstile bootstrap script
(https://challenges.cloudflare.com/turnstile/v0/api.js) and its widget
iframe. Added challenges.cloudflare.com to script-src, connect-src, and a
new frame-src directive so the widget can render under prod CSP.
2026-05-17 19:07:41 -04:00
CarterPerez-dev e78c33ae18 fix(canary): declare VITE_TURNSTILE_SITE_KEY ARG in prod Dockerfile
Audit F4: compose.yml already passed VITE_TURNSTILE_SITE_KEY as a build arg,
but vite.prod never declared an ARG/ENV for it, so Vite's import.meta.env
resolved to undefined. The Turnstile widget never rendered in prod, causing
the backend to reject every create-token request with TURNSTILE_FAILED.
2026-05-17 19:07:39 -04:00
CarterPerez-dev 20b6fc86ce fix(canary): prod nginx proxy_pass /api/, /c/, /k/ to canary upstream
Audit F1: prod.nginx had only the SPA fallback so all API requests, trigger
requests, and kubeconfig requests returned index.html, breaking the prod
deployment end-to-end. Added an upstream canary block to nginx.prod.conf and
three proxy_pass location blocks mirroring dev.nginx (plus CF-Connecting-IP
header since prod sits behind Cloudflare Tunnel).
2026-05-17 19:07:32 -04:00
CarterPerez-dev 5884349336 fix(canary): dev nginx routes /c/, /k/, /api/ to canary + manage_url defaults to base_url
Two tightly-coupled bugs visible after the PUBLIC_BASE_URL fix landed:

1. Trigger URLs now correctly show :22784 (the host-exposed nginx port),
   but opening one in the browser rendered the SPA's NotFound page instead
   of hitting the backend trigger handler.

   Root cause: dev.nginx only had `location /` → frontend_dev. No location
   block for `/c/`, `/k/`, or even `/api/`. Every request went to the SPA;
   only `/api/*` happened to work because vite's internal proxy caught it
   on its way through frontend_dev → canary. /c/* and /k/* (the trigger
   paths) just fell through to the SPA fallback.

   Fix: add `upstream canary { server canary:8080 }` to nginx.conf and
   three new location blocks in dev.nginx for /api/, /c/, /k/. Each one
   proxy_passes directly to the canary upstream with X-Real-IP +
   X-Forwarded-For + X-Forwarded-Proto set so middleware.RealIP() resolves
   the actual visitor IP rather than the docker bridge. Mirrors what
   spec §12.3 prescribes for prod nginx.

2. Manage URL in responses still showed :8080 even after PUBLIC_BASE_URL
   was set to :22784. Two separate config keys: canary.base_url
   (PUBLIC_BASE_URL → trigger_url) and canary.manage_url (CANARY_MANAGE_URL
   → manage_url). The latter has no env-name overlap with the spec's
   PUBLIC_BASE_URL, so it kept its default literal "http://localhost:8080".

   Fix: in config.Load(), after unmarshal, fall manage_url back to base_url
   when it's empty or still equals the bare default. Operators setting one
   env var (PUBLIC_BASE_URL) now get both URLs computed off it. Explicit
   CANARY_MANAGE_URL still wins if set — preserves the prod ability to
   serve manage at a different domain than triggers. Extracted the literal
   to a `defaultCanaryBaseURL` const so the default and the fallback
   sentinel reference the same string.

After this + the prior PUBLIC_BASE_URL alias, a fresh token from the form
should produce trigger_url + manage_url both pointing at
http://localhost:${NGINX_HOST_PORT}/... — clickable, reachable, and triggers
hitting /c/ now route to canary and record events.

Operator action: just dev-down && just dev-up to rebuild nginx + canary
containers with the new config.
2026-05-17 18:12:09 -04:00
CarterPerez-dev 2d395d0691 fix(canary): map PUBLIC_BASE_URL env var to canary.base_url
Spec §12.4, .env.example, .env.development, and dev.compose.yml all use the
name PUBLIC_BASE_URL for the externally-reachable URL stamped into trigger/
manage links. But the koanf env-key map in config.go only routed
CANARY_BASE_URL → canary.base_url. So setting PUBLIC_BASE_URL had no effect,
the default fallback "http://localhost:8080" silently won, and every issued
token came back with trigger_url/manage_url like
"http://localhost:8080/c/<id>" regardless of where canary was actually reachable.

Visible to the operator as: trigger/manage URLs in the SPECIMEN ISSUED dossier
pointing at :8080 (where canary doesn't even listen externally — host port is
nginx ${NGINX_HOST_PORT}, defaulting to randomised 22784).

Fix is one line: add PUBLIC_BASE_URL alongside CANARY_BASE_URL in the env-key
map. Both env names now route to canary.base_url. Backwards-compatible —
existing CANARY_BASE_URL deployments keep working.

Operator action: set `PUBLIC_BASE_URL=http://localhost:${NGINX_HOST_PORT}` in
.env.development (e.g. http://localhost:22784) and restart canary. URLs will
then reflect the actual reachable address.
2026-05-17 18:02:48 -04:00
CarterPerez-dev 44927b245c fix(canary): accept null events from manage response
Backend serializes `Events []Event` as JSON `null` when the slice is nil
(Go's zero value for slices). Spec §8.4 shows it as an array — stale spec,
verified actual behavior with `curl http://localhost:22784/api/m/<uuid>`
returning `"events": null` on a freshly-created token with no triggers.

Zod schema was `events: z.array(eventResponseSchema)` — failed to parse the
null, threw PARSE_ERROR, manage page rendered "CANNOT REACH ARCHIVE" instead
of the empty dossier.

Fixed: `.nullish().transform(v => v ?? [])` — accepts null/undefined and
normalizes to []. Manage page's `events.length === 0` empty-state check
("No events recorded yet") now reachable; consumers always see an array.

Caught by memory rule feedback_verify_data_shapes.md: trust reality > spec.
I trusted §8.4's example object during Phase 14 schema design instead of
hitting the live endpoint. Same kind of drift as the four Phase-14 spec-vs-code
divergences cleared at the time — this one slipped through because
TokenManagePage was never end-to-end exercised pre-UI.
2026-05-17 17:58:13 -04:00
CarterPerez-dev 915d471b8d fix(canary): vite dev proxy — correct backend port + stop stripping /api
Two latent template-leftover bugs in vite.config.ts proxy that 404'd every
useTokenTypes/useCreateToken/useManageToken/useDeleteToken call:

- Default target was http://localhost:8000. Backend listens on :8080
  (backend/config.yaml `server.port: 8080`, default registered in
  internal/config/config.go). Off-by-one-port silent for the whole
  Phase 14 cycle because Phase 14 didn't end-to-end-test.

- `rewrite: (p) => p.replace(/^\/api/, '')` stripped /api before forwarding.
  But backend mounts routes UNDER /api via `r.Route("/api", ...)`
  (cmd/canary/main.go:322). So a request for /api/tokens/types got rewritten
  to /tokens/types, which backend doesn't serve — 404 page not found.

Frontend axios uses baseURL `/api` and path `/tokens/types` → full URL
`/api/tokens/types`. With these fixes the proxy now passes that through
verbatim to http://localhost:8080/api/tokens/types, which is where chi
actually mounts the route.

dev.compose.yml: added VITE_API_TARGET=http://canary:8080 to the frontend
container's env so the dev-compose path also works (inside the container
"localhost" is the vite container, not the canary container — has to use
docker DNS).

Surfaced today when the operator manually eyeballed the landing page and
SpeciesSection rendered "Could not load species catalog. Try again in a
moment." (the descriptorsError branch in landing/index.tsx SpeciesSection).
2026-05-17 17:37:36 -04:00
CarterPerez-dev 6ef5f68b59 feat(canary): web fonts — Archivo Black / Space Grotesk / Departure Mono
System stack was the floor; this is the intended pairing. Three roles, three
voices:

- Archivo Black (display) — the big confident archive voice. CANARIES,
  TRAP TRIPPED, DOSSIER NOT FOUND, SPECIMEN ISSUED, NO SPECIMEN HERE. Single
  weight 900 black grotesque, wide letterforms, built for huge display. Pairs
  visually with the PARADISE-style pixelated typography in the
  vector-minimalism reference folder.
- Space Grotesk (sans) — speaking voice. Body, section titles, blurbs,
  button labels, error text. Geometric joints + open apertures read as
  "designed" where system extra-bold read as "just heavy." Weights 400-700
  cover the range; section titles dropped 800→700 since Space Grotesk caps
  at 700 (no perceptible difference in the design).
- Departure Mono (mono) — technical scribblings. SerialBar, timestamps,
  field labels, strip pills, ID codes, all the spec-sheet micro-text. Its
  pixelated character is the deliberate echo of image 1 (PARADISE) that
  ui-monospace can't make.

Loaded via Google Fonts <link> in index.html (preconnect + display=swap),
no npm dep. ~50KB cold cache; cached-warm thereafter. System sans / system
mono remain in the fallback chain at the end of each stack so a CDN miss
degrades to the previous design without breaking.

$font-display token added to _fonts.scss for the giant headlines; $font-sans
and $font-mono updated to lead with the new families. Headlines previously at
font-weight: 900 dropped the weight (Archivo Black is single-weight), and
letter-spacing/line-height tightened slightly to match the new metrics.

Gates green: typecheck + biome + lint:scss + build (419ms; no chunk size
delta — fonts load from CDN, not the bundle).
2026-05-17 17:27:35 -04:00
CarterPerez-dev da126ad889 fix(canary-phase-ui-design): single-source form fields + URL revoke + eventExtra schema
Parallel audit (superpowers:code-reviewer + general-purpose spec adherence)
returned PASS-WITH-NOTES + PASS. All actionable SHOULD-FIX and NIT items
addressed in this commit per the fix-in-phase rule.

SHOULD-FIX (code review):
- Dead ternary at manage/index.tsx:253 (`cursor === undefined ? idx+1 : idx+1`)
  collapsed to `idx + 1`. EventLogSection prop renamed `cursor` → `cursorIsSet`
  (boolean) since the value itself was never consumed past the dead branch.
- URL.createObjectURL leak in artifact.tsx — both file (base64) and text
  variants now run inside useEffect with cleanup that calls
  URL.revokeObjectURL on dependency-change AND on unmount. Extracted into
  two co-located hooks: useBase64ObjectUrl, useTextObjectUrl.
- Text inputs lacked <label htmlFor=…> association. Refactored the landing
  form to use the existing Field primitives (TextField, TextareaField) which
  wire htmlFor↔id via useId. This single change also closed the next finding.
- Dead infrastructure: Field primitives existed but no consumer used them.
  Landing form's local FieldLabel removed; TextField/TextareaField now have
  consumers. Net diff: −181 LOC.
- LeaderLabel: truly unused primitive, no foreseeable consumer in this scope.
  Deleted (`frontend/src/components/LeaderLabel/`) and removed from barrel.

SHOULD-FIX (spec adherence):
- §8.6 row 410 → TOKEN_DISABLED: spec is stale. Backend Go ground truth
  (verified via `grep StatusGone\|TOKEN_DISABLED backend/internal/`) emits
  neither — disabled tokens come back as 200 with `token.enabled=false`,
  which the DossierCard already renders with alarm-pill + HEADLINE_DISABLED.
  Per feedback_verify_data_shapes.md: backend Go beats spec when they diverge.
  No code change; documented here so the next AI doesn't re-open it.
- eventResponseSchema.extra: tightened from `z.unknown()` to
  `z.record(z.string(), z.unknown())` to match backend's `json.RawMessage`
  default of `{}` (event/repository.go:47). Added EventExtra type alias;
  event-row.tsx now uses it instead of casting through unknown.

NIT:
- SerialBar: extracted `BAR_COUNT = 22` const; `FALLBACK_BARS` constant
  string replaces the BAR_CHARS array that had length-9 / length-22
  inconsistency between the empty-input and computed branches.
- Glyph: removed `default` branch — switch is now exhaustive over the
  closed TokenType enum, so a future 8th type would be a compile error
  rather than silently rendering WebbugGlyph.
- Dead `export { TOKEN_BLURB }` + `export { ArtifactDisplay }` from
  landing/index.tsx — no observed consumer, dropped.
- App.tsx and main.tsx header banners normalized to the project-canonical
  19 = signs (matches all other files in repo).

NOT addressed in this commit (intentional):
- CreateTokenRequest `metadata` as `z.unknown()`. Refactor to a discriminated
  request schema keyed on `type` would tighten compile-time safety for any
  future caller bypassing buildPayload — defer because (a) the form is the
  only caller and validates via Zod on submit, (b) the change touches the
  Phase 14 contract that was just stabilized two commits before this UI phase.
- NotFound (frontend route fallback) vs ManageNotFound (manage 404) live
  separately. Both work; the two paths are intentional — off-path vs
  off-dossier are semantically different events.

Gates green: typecheck + biome check + lint:scss + build (414ms; landing
chunk dropped 21KB → 19.93KB after dead style removal).
2026-05-17 15:07:50 -04:00
CarterPerez-dev 4a4f465d24 feat(canary): manage dossier page + /m/:manageId route + 404 fallback
Manage route renders the per-token dossier — the "specimen catalog entry"
twin of the intake page. Same vector-minimalism mind: archive strip,
big-blocky headline, hairline-rule cards, mono labels, alarm-red only
when the trap is alive (eventsTotal > 0) or disabled.

Page composition:
- Top Strip (FIELD STATION / SECTION / SPECIES pill)
- Hero block — headline state-shifts: QUIET (no events, ink), TRAP TRIPPED
  (alarm red), TRAP MUTED (ink-mute). Memo rendered as printer-block quote.
- Halftone separator
- SpecimenCard with armed status / trigger count / silenced (15m window) /
  last seen / created / alert channel / filename — plus Trigger URL CopyField
- EventLogSection — cursor-paginated list of EventRow cards (timestamp +
  source_ip + geo + ASN + UA + referer + notify_status pill + expandable
  fingerprint extra). "Load older →" + "← back to most recent" controls.
- DeleteSection — two-step arm/confirm pattern (no native confirm() dialog),
  alarm-tone Button on confirm, success → toast + navigate('/')
- Bottom Strip with "new specimen ↗" link

States: useManageToken loading | ApiError(NOT_FOUND) → ManageNotFound |
other error → ManageError with retry | success → ManageView.

Pagination policy per plan anti-relit: singular useQuery, NO useInfiniteQuery.
Each cursor change replaces the visible page (operator-friendly default).
Auto-refresh every 5s via QUERY_STRATEGIES.frequent (Phase 14 contract).

Routes:
- /  → landing
- /m/:manageId → manage (new)
- *  → notfound (new — replaces previous fallback-to-landing)

config.ts: ROUTES.MANAGE added; manageRoute(id) helper centralizes the
URI-encoded path so result.tsx and any future caller cannot drift.

notfound page: same ethos in miniature. 404 strip pill, big OFF-ROUTE
headline, link back to intake.

Gates green: typecheck + biome + stylelint + build (414ms, manage chunk
10.9KB / 3.7KB gzip, notfound chunk 1.0KB / 0.5KB gzip).
2026-05-17 14:54:27 -04:00
CarterPerez-dev 5562f82f34 feat(canary): landing — specimen requisition page + Turnstile widget
Landing route (/) is the token-creation page, framed as a "specimen
requisition" intake form per the vector-minimalism ethos. Mind: cataloging
a thing that resists cataloging — a trap baited for an unseen visitor.

Page composition:
- Top archive Strip (FIELD STATION / VOL / ISSUE pill)
- Hero block: large blocky "CANARIES" headline, Latin subtitle, purpose line
- Halftone separator
- Five sections (01 species / 02 annotate / 03 route / 04 species config / 05 verify)
- Bottom Strip with routing/license metadata

Form sub-sections (each a small component to keep complexity under biome's
maxAllowedComplexity=25 cap on FormView):
- SpeciesSection — 7-card grid via TypePicker (radio fieldset, sr-only inputs)
- AnnotateSection — memo textarea + conditional filename (when file-kind)
- RouteSection — Telegram | Webhook radio fieldset + conditional credentials
- ConfigSection — slowredirect destination_url or envfile include_keys
- VerifySection — Turnstile widget if VITE_TURNSTILE_SITE_KEY present
- SubmitFooter — release button + privacy note

Validation: client uses createTokenRequestSchema.safeParse, surfaces field
errors inline (mono caps in alarm color); server-side ApiError.fields are
mapped back into the same FieldErrors shape via buildSubmissionState (Phase 14
single-source envelope contract preserved).

Result view: SpecimenCard with full token dossier (DataRows), kind-discriminated
artifact display (url → CopyField; file/text → download Blob via object-URL;
connection_string → CopyField with note), Manage + Trigger CopyFields, link to
the dossier route /m/:manage_id (route wired in next chunk).

Turnstile: explicit-render widget, lazy script-load, registers token via
setTurnstileTokenProvider on success; cleans up on unmount; no key = no widget
(matches backend dev-bypass at internal/turnstile/verifier.go).

Gates green: typecheck + biome + stylelint + build (406ms, landing chunk
123KB / 38KB gzip).
2026-05-17 14:49:37 -04:00
CarterPerez-dev f5d893ab8c feat(canary): UI design layer — vector-minimalism tokens + primitives
Operator (Carter) redirected from infra Phase 15 to UI design layer; design ethos
"vector-minimalism" (specimen-cataloging mind, off-white paper, deep ink, alarm-red
accent, hairline rules, halftone/grain overlays). Foundation only — pages follow.

Tokens: 7-step paper/ink/alarm/signal palette + paper-grain SVG noise + halftone
dot data-uri + hairline width. Stripped color palette in white-canvas commit is
deliberately NOT re-introduced; this is a project-owned palette, not the template's.

Primitives (each in own folder with module SCSS):
- Strip / StripItem    — archive header (vol/issue/serial pills)
- LeaderLabel          — index + rule + caption annotation
- SpecimenCard         — tag + body + serial container (paper|ink|alarm tone)
- DataRow              — field=value with dotted leader (mono|emphasize|alarm)
- Pill                 — paper|ink|alarm|signal label
- CopyField            — labelled mono code with clipboard button
- Halftone             — repeating dot strip (sparse|normal|dense)
- SerialBar            — fingerprint bar + code two-row mono display
- Glyph                — 7 token-type SVG icons (currentColor, 24×24)
- Field (Text/Textarea/Select/Wrap) — labelled form inputs with leader rule
- Button               — primary|ghost|alarm × sm|md|lg

Gates green: pnpm typecheck + pnpm biome check + pnpm lint:scss + pnpm build (294ms).

No backend changes. No page-level routing changes yet.
2026-05-17 14:39:30 -04:00
Carter Perez 0817997585
Update hash_identifier.py 2026-05-17 14:11:54 -04:00
CarterPerez-dev 1d205c1791 chore(canary): white-canvas frontend design layer pre-operator UI
Strip the inherited template aesthetic so the next AI/designer starts
from a true blank canvas. AI tunnel-vision pattern: when there's an
existing layout/sidebar/color palette, the model tends to redesign
in place rather than truly start fresh. Removing the visual scaffold
forces fresh-canvas thinking when operator-driven UI work begins.

Removed:
- pages/dashboard, pages/settings — template "Welcome / Available
  Stores / Suggested Features" stubs that have no canary purpose.
- pages/landing/landing.module.scss, core/app/{shell,toast}.module.scss
  — empty header stubs imported only for side effects.
- core/lib/shell.ui.store.ts — zustand store for sidebar open/
  collapsed/theme state; entirely template-coupled. core/lib/index.ts
  reduced to an empty barrel ready for canary stores.
- core/app/shell.tsx sidebar+nav+header layout; replaced with bare
  ErrorBoundary > Suspense > Outlet. Keeps robustness pattern,
  drops the navbar template a designer would otherwise inherit.
- App.tsx hardcoded dark Toaster theme + wrapping .app div.
- styles/_tokens.scss color palette ($bg-page, $text-default, etc.)
  — biased toward a specific dark aesthetic. Spacing, typography,
  weights, line heights, letter spacing, radius, z-index, transitions,
  breakpoints, container widths all KEPT — neutral design primitives,
  not aesthetic choices.
- styles/_reset.scss body bg-color: #fff / color: #000 defaults.
  Designer chooses the palette; reset stays purely structural.
- styles.scss .app class wrapper.
- config.ts USERS endpoints, USERS query keys, STORAGE_KEYS,
  HTTP_STATUS, PAGINATION — template constants with no canary
  consumer. Reduced to ROUTES.HOME and QUERY_CONFIG (consumed by
  query.config.ts). Designer adds back what they actually need.

Kept (logic / infrastructure / Phase 14 work):
- Full api/ and core/api/ — Zod schemas, axios client, hooks,
  ApiError, QueryClient, retry strategies.
- main.tsx, index.html (already canary-titled with favicons in
  public/asset).
- QueryClientProvider + RouterProvider + bare Toaster + ReactQuery
  Devtools provider stack.
- Lazy-route + Suspense + ErrorBoundary pattern in shell.
- styles/_index.scss, _fonts.scss (system font stacks), _mixins.scss
  (breakpoint utilities, flex helpers, sr-only, truncate, etc.) —
  all neutral.

Gate: pnpm typecheck, pnpm biome check, pnpm lint:scss, pnpm build
all clean. Production bundle: 140-byte landing chunk (the <div />),
~37 KB index chunk (api/core/router/zod machinery), no leftover
template CSS.
2026-05-17 13:12:24 -04:00
CarterPerez-dev 7c0a504d0b fix(canary-phase14): single-source envelope parsing + query-key splay
Address audit findings S-1, S-3, S-4, S-7 pre-rollup (fix-in-phase).

S-1: Eliminate hand-rolled envelope error parser duplication.
  Moved transformAxiosError out of core/api/errors.ts (where it
  required a private parseEnvelopeError mirror of the Zod schema)
  into api/client.ts where it now uses apiErrorEnvelopeSchema directly
  via safeParse. core/api/errors.ts is now ApiError class + code
  constants + module augmentation only, with no canary-envelope
  knowledge — keeps layering core <- universal, api <- canary clean.

S-3: useManageToken query key now splays cursor/limit individually
  instead of carrying the params object reference. Without this,
  callers passing a freshly-allocated { cursor } each render would
  miss the cache even with identical values.

S-4: unwrapEnvelope's PARSE_ERROR now includes the first Zod issue
  (path + message) in the ApiError.message so operators see WHICH
  field shape failed, not just that something did.

S-7: Add request interceptor rejection handler so request-phase
  errors propagate via Promise.reject instead of throwing
  synchronously.
2026-05-17 13:03:27 -04:00
CarterPerez-dev f6194d87b9 feat(canary): TanStack Query hooks for token lifecycle
Four hooks wired through the canary api/client envelope helpers.
Module-augmented defaultError surfaces ApiError everywhere.

- useTokenTypes: GET /tokens/types, parsed via typeDescriptorListSchema.
  Uses QUERY_STRATEGIES.static (Infinity stale, no refetch on focus)
  since the discoverable types list is build-time stable.
- useCreateToken: POST /tokens (CreateTokenInput -> CreateTokenResponse).
  On success invalidates ['canary','manage', manage_id] so a freshly
  created token's manage page hydrates with the new row on first hit.
- useManageToken: GET /m/{id} with cursor pagination. URLSearchParams
  builds ?cursor=&limit= only for defined values; manageId is
  URI-encoded. QUERY_STRATEGIES.frequent enables 30s polling so the
  events list feels live. enabled=false when manageId is empty.
- useDeleteToken: DELETE /m/{id}. On success removes the cached
  manage data entirely (no point keeping it after a delete).
2026-05-17 12:56:03 -04:00
CarterPerez-dev 2d13e649a6 feat(canary): axios client with Turnstile header interceptor
- api/client.ts: axios instance with baseURL from VITE_API_URL env
  (falls back to /api) and 15s timeout.
- Request interceptor adds CF-Turnstile-Response header from a
  pluggable provider (setTurnstileTokenProvider). Backend's
  middleware/turnstile.go reads header first then body field; using
  the header keeps the request body shape clean and matches the plan.
- Response interceptor routes axios errors through transformAxiosError
  (canary envelope-aware) so callers see typed ApiError everywhere.
- Envelope-aware helpers apiGet / apiPost / apiDelete:
  - apiGet/apiPost unwrap {success:true,data:T} via successEnvelope
    factory; on shape mismatch throw ApiError(PARSE_ERROR).
  - apiDelete returns void for 204 responses; envelope error path
    handled by the interceptor.
- Plumbed into api/index.ts barrel.
2026-05-17 12:55:42 -04:00
CarterPerez-dev db98a0d497 feat(canary): Zod schemas + canary envelope error normalization
- Add Zod 4 schemas mirroring backend handler shapes:
  - api/types/token.ts: tokenResponseSchema, manageTokenViewSchema,
    artifactSchema (discriminatedUnion on kind), typeDescriptorSchema,
    createTokenRequestSchema (with superRefine for telegram/webhook
    conditional required), per-type metadata helpers.
  - api/types/event.ts: eventResponseSchema with GeoView, NotifyStatus,
    manageResponseSchema with cursor pagination.
  - api/types/error.ts: apiErrorEnvelopeSchema (canary {success,error}
    envelope) + successEnvelope<T> factory for typed unwrap.
- Refactor core/api/errors.ts: ApiError now carries canary error codes
  (VALIDATION_ERROR, TURNSTILE_FAILED, BAD_CURSOR, etc.) and a
  fields: Record<string,string> map. transformAxiosError parses the
  canary envelope first, falls back to HTTP status code if envelope
  shape is missing.
- Update core/api/query.config.ts no-retry list for canary codes
  (user-error codes plus RATE_LIMITED to avoid burning rate budget).
- Remove obsolete template apiClient at core/api/api.config.ts; the
  new canary client lives at api/client.ts (Phase 14 next commit).
2026-05-17 12:55:10 -04:00
CarterPerez-dev 32e9c0a30e fix(canary-phase13): direct unit coverage for firstSubdivisionName
Code-review audit flagged firstSubdivisionName as exercised only
transitively through extractLookup. Adds five direct unit tests
covering the documented branches (nil/empty slice, English-name
prefers, ISOCode fallback, skip-empty-then-find-populated,
all-empty returns ""). Fix-in-phase per project convention; no
backlog rot.
2026-05-17 05:30:13 -04:00
CarterPerez-dev 0e5ce63dc8 feat(canary): wire geoip into main with NopService fallback
openGeoIP opens cfg.GeoIP.Path and returns a (Lookuper, closer)
pair; on open failure (missing/unreadable mmdb) it returns
geoip.NopService() and a no-op closer so the rest of the wire-up
sees a consistent non-nil interface and the binary stays healthy
in environments without a MaxMind license. buildEventStack now
accepts a geoip.Lookuper and threads it into event.ServiceConfig.

Close is deferred in run() so the mmdb file handle releases on
graceful shutdown; a close-error is logged at warn (not fatal).
2026-05-17 05:24:29 -04:00
CarterPerez-dev b3fd75b680 feat(canary): Event.AttachGeoIP + event.Service geo enrichment
- entity: (*Event).AttachGeoIP(geoip.Lookup) sets Geo* nullable
  pointer fields with overwrite semantics (empty/zero collapses to
  nil so DB column stays NULL). Single-arg signature matches the
  call shape in design spec §9 (line 1437) and keeps the helper
  symmetric with the geoip.Lookup value type.
- service: ServiceConfig.GeoIP geoip.Lookuper plumbed through to
  the Service.geo field. Record now calls enrichGeo(evt) BEFORE
  s.repo.Insert so geo_country/region/city/asn/asn_org persist in
  the same row. enrichGeo is nil-safe on three axes (geo, evt,
  evt.SourceIP) — geo enrichment is best-effort, never an error
  path.
- service_test: fakeLookuper + newSvcWithGeo helper; five new
  tests (enriches-before-insert, no-geo-leaves-nil, empty-IP-
  skips-lookup, NopService-leaves-nil, best-effort-when-insert-
  fails). All pre-existing tests pass unchanged because GeoIP
  defaults to nil.
- entity_test: seven AttachGeoIP unit tests covering populate-all,
  empty Lookup, partial fields, zero/negative ASN guards, overwrite
  semantics, and ASN-pointer independence from the input value.
- integration_test: two real-Postgres tests (stub Lookuper writes
  populated geo_* columns; NopService leaves them NULL) verify
  the wiring round-trips through the Insert SQL.
2026-05-17 05:24:17 -04:00
CarterPerez-dev ac334b14f9 feat(canary): geoip wrapper package + config wiring
- internal/geoip: Service over oschwald/geoip2-golang v2 with City db
  enrichment (Country/Region/Subdivision/City) and a NopService
  factory for environments without the mmdb file. Nil-safe Lookup
  returns a zero geoip.Lookup on any failure path (nil receiver,
  nil reader, empty/malformed IP, reader error, !rec.HasData).
- internal/geoip: unexported cityReader interface allows whitebox
  fake-based unit testing without bundling a real .mmdb fixture;
  extractLookup is a pure function for synthetic-record coverage.
- config: GeoIPConfig{Path} koanf section; GEOLITE_PATH env binding
  matching compose.yml; default /data/GeoLite2-City.mmdb.
- deps: github.com/oschwald/geoip2-golang/v2 v2.1.0.

ASN/ASNOrg fields are reserved on geoip.Lookup but stay zero with a
City-only mmdb; populating them is a future-extension concern (ASN
db is not fetched by scripts/init.sh per design §12.5).
2026-05-17 05:23:53 -04:00
CarterPerez-dev 3cf198657b fix(canary-phase12): address audit findings before phase rollup
- Rename admin.Handler.Register → RegisterRoutes for parity with
  health.Handler.RegisterRoutes and token.Handler.RegisterAPIRoutes /
  RegisterManageRoutes / RegisterTriggerRoutes. Spec §6.3 line 558
  also illustrates the canonical wire-up as
  `adminH.RegisterRoutes(api)`.
- Drop the local strItoa helper in handler_test.go; use strconv.Itoa.
  The helper was a code-reviewer nit — equivalent stdlib already
  exists; fewer lines, less surface to drift.

Both findings are non-blocking but covered by the fix-in-phase rule
(no backlog rot). Phase 12 audits otherwise PASS on first dispatch:
- superpowers:code-reviewer: PASS
- general-purpose spec-adherence: PASS
2026-05-14 07:02:39 -04:00
CarterPerez-dev 9adb3d2eb8 feat(canary): wire /api/admin mount + integration tests
- cmd/canary/main.go: mountAdminRoutes helper keeps run() and
  mountRouter under funlen 100. Admin routes mounted INSIDE the /api
  subrouter so they inherit read-tier rate limit + CORS; gated by
  OperatorBearer(cfg.Operator.Token). If OPERATOR_TOKEN is unset, the
  entire mount is skipped (no Bearer middleware, no routes — admin
  endpoints simply do not exist), and a structured warn log records
  the reason. This is the production safety: misconfiguration leaves
  the endpoint nonexistent rather than reachable.
- internal/admin/integration_test.go: testcontainers-backed end-to-end
  tests exercising the full /api/admin/* stack:
    * 404 on missing Authorization (no WWW-Authenticate leak)
    * 404 on wrong token
    * 200 on /stats with seeded mixed tokens + events
    * 200 on /tokens?limit=2 with HasMore/NextOffset accuracy
    * 204 on disable + DB-level verification token.Enabled flipped
    * 404 on disable for a missing id
  All requests go through the OperatorBearer middleware to verify the
  wire-up matches the unit behavior.
2026-05-14 06:57:39 -04:00
CarterPerez-dev 9e82f70841 feat(canary): admin handler (stats + tokens list + disable) + repo methods
- internal/admin/handler.go: replaces unused template stats handler
  with the canonical Phase 12 endpoints from spec §8.1:
    GET  /stats                — tokens_count, events_count, by_type,
                                 by_alert_channel
    GET  /tokens               — offset-based pagination, default 50,
                                 cap 100; returns full token.Response
                                 list with trigger/manage URLs via
                                 injected URLBuilder; 400 BAD_PARAM on
                                 negative/non-int offset
    POST /tokens/{id}/disable  — 204 on success; 404 NOT_FOUND envelope
                                 when token.ErrNotFound; 500 envelope
                                 on other repo errors
  Handler takes TokenRepository + EventRepository + URLBuilder
  interfaces (test seam); wire-up supplies *token.Repository,
  *event.Repository, *token.Service (which already implements
  TriggerURL/ManageURL).
- internal/admin/dto.go: Stats, TokenListPage, TokenListResponse.
- token.Repository.CountByType / CountByAlertChannel: GROUP BY type
  and GROUP BY alert_channel; returned shapes typed as TypeCount /
  ChannelCount with json+db tags for direct JSON emission.
- event.Repository.CountAll: global event count.
- Handler tests cover happy path + 500 propagation for each repo
  dependency; pagination (default/limit-cap/offset paging); 400 on
  bad offset; 404 on disable miss; 405 on GET to disable endpoint
  (sanity check that POST-only routing is intentional).
2026-05-14 06:57:28 -04:00
CarterPerez-dev 426ed54594 feat(canary): OperatorBearer middleware + operator token config
- internal/middleware/operator_bearer.go: returns 404 on miss (NOT 401)
  per spec §11.5; subtle.ConstantTimeCompare for timing safety; rejects
  when configured token is empty (defense-in-depth even though wire-up
  skips mount in that case).
- Table-driven test covers: missing header, empty header, wrong scheme
  (Basic), case-sensitive scheme (lowercase bearer), no payload,
  no-space-after-Bearer, equal-length wrong token, different-length
  wrong token, correct token, empty-config rejects both empty and
  populated headers, extra-whitespace rejection. Also asserts no
  WWW-Authenticate header on 404 (endpoint-hiding intent).
- config.OperatorConfig + OPERATOR_TOKEN env mapping; default empty
  (mount is skipped when empty).
2026-05-14 06:57:10 -04:00
CarterPerez-dev ca67cf6313 fix(canary-phase11): use repo-supplied HasMore/NextCursor before rollup
Both audits flagged buildPage as a non-blocking but worth-cleaning nit:
it re-derived next_cursor from len(events) and the last event's ID
rather than using the HasMore + NextCursor flags that
event.Repository.ListByToken already computes via the LIMIT+1 peek
trick. Both calculations agreed today, but the duplication would
mask a divergence if the repo's pagination logic ever evolved
(e.g. switched to opaque token cursors).

Inline buildPage into gatherManageData and use list.HasMore +
list.NextCursor as the authoritative signals. Drops the buildPage
helper. Behavior identical; tests still pass.
2026-05-14 01:12:07 -04:00
CarterPerez-dev 884215288e feat(canary): wire manage routes + extract buildHTTPDeps + integration tests
- cmd/canary/main.go: tokenH.RegisterManageRoutes(api) mounts GET +
  DELETE /m/{manage_id} inside the /api subrouter (read-tier rate
  limit applies; no Turnstile, no create-tier limits — read-style
  endpoints by spec §8.1).
- Extract buildHTTPDeps to keep run() under the funlen ceiling (was
  102, now under 100). Pulls out registry build + tokenSvc + verifier
  + healthH + tokenH construction. Returns the four handles run()
  needs to wire the router and shutdown path.

Integration tests (internal/event/integration_test.go,
testcontainers-Postgres + miniredis):
- TestIntegration_ManagePageReturnsTokenAndEventsAndSilencedCount:
  POST creates a webbug, three triggers from the same CF-Connecting-IP
  fire (one notification, two deduped per the Phase 10 dedup gate);
  GET /api/m/{manage_id} returns token with TriggerCount=3,
  EventsTotal=3, EventsSilencedActive=2, three event records with
  exactly one NotifySent and two NotifyDeduped, no next page.
- TestIntegration_ManagePageReturns404OnUnknownManageID
- TestIntegration_ManageDeleteCascadesEvents: delete via DELETE
  /api/m/{manage_id}, verify CountByToken == 0 (FK ON DELETE CASCADE)
  and the token is gone (token.ErrNotFound).
2026-05-14 01:07:38 -04:00
CarterPerez-dev 593433fc03 feat(canary): manage page handlers + DTOs (GET + DELETE /m/{manage_id})
Adds the HTTP layer for spec §8.4. The UUID in the URL is the
capability — no other auth required, by design.

- token/dto.go: ManageTokenView (slimmer than Response — no manage_id,
  no manage_url, no metadata, since the user is already on the manage
  page and those would be redundant), ManagePage{NextCursor, HasMore},
  ManageResponse{Token, Events, EventsTotal, EventsSilencedActive, Page}.
  Token.ToManageView(triggerURL) builder. Events use the existing
  event.Response (already matches spec shape exactly).
- token/handler.go: EventQuery + DedupCounter interfaces declared near
  the existing EventRecorder + FingerprintRecorder; same dependency-
  injection style. NewHandler signature gains the two new args (5 → 6;
  test sites + main.go updated).
- GetManage(w, r): parses ?cursor= (int64, must be >= 0) and ?limit=
  (defaults 20, capped at 100). Returns 404 envelope for unknown
  manage_id (envelope, not bare http.NotFound, so frontend gets a
  parseable error code). 400 BAD_CURSOR for non-numeric or negative.
  Calls svc.GetByManageID + eventQuery.ListByToken + CountByToken +
  dedupCounter.CountActiveDedup; assembles the manage payload.
- DeleteManage(w, r): 204 on success, 404 on miss. Cascade-delete is
  via the existing tokens-events FK ON DELETE CASCADE.
- buildPage helper: NextCursor is the string of the last event's ID
  iff len(events) == limit (matches spec example "next_cursor": "41").

Tests cover happy path, 404 on unknown id, 400 on bad/negative cursor,
?limit= respected, ?limit=999 capped at 100, DELETE happy + 404.
2026-05-14 01:07:09 -04:00
CarterPerez-dev e3f9ff48f9 feat(canary): event.CountActiveDedup + token.DeleteByManageID for manage page
Adds the two service primitives Phase 11's manage handler needs.

- event.Service.CountActiveDedup(ctx, tokenID): Redis SCAN over
  dedup:trigger:{tokenID}:* in batches of 100, sums (value-1) for each
  key (value=1 = first trigger fired notification, value=N>1 = N-1
  silenced). Returns 0 when no rdb is configured.
- token.Service.DeleteByManageID: thin wrapper around
  Repository.DeleteByManageID; ServiceRepository interface gains the
  method so test fakes can implement it (fakeRepo updated). Returns
  ErrNotFound on no-rows.

Tests cover: empty pattern returns 0, first-trigger-only returns 0
(notify fired, nothing silenced), aggregation across IPs sums correctly,
other tokens' keys are excluded by the prefix match, nil rdb returns 0.
2026-05-14 01:06:45 -04:00
CarterPerez-dev 41b9d2c911 fix(canary-phase10): escape geo wrapping parens for MarkdownV2 before rollup
Audit catch (code-reviewer agent). formatGeo emitted literal `(` and `)`
around the geo block (e.g. `(Toronto, CA)`), but `(` and `)` are in the
MarkdownV2 reserved set when used outside `[text](url)` link delimiter
positions. Telegram would reject the entire message with
`Bad Request: can't parse entities: Character '(' is reserved`.

Fix: write the wrapping parens as `\(` and `\)` literals in the three
parens-construction switch arms. The dynamic field values inside still
flow through EscapeMD as before. The `[View full event timeline](url)`
inline link is left alone — its `(` `)` ARE link delimiters and per V2
rules must NOT be escaped (only `)` and `\` inside the URL portion need
escapes, and our manage URL has neither).

Regression test: TestSender_Send_MessageContainsKeyFields now asserts
`require.Contains(text, \\(Toronto, CA\\))` so substring matching can't
hide future regressions on the wrapping parens.
2026-05-14 00:47:08 -04:00
CarterPerez-dev 82085711b6 test(canary): testcontainers integration test for trigger + dedup + retention
Closes deferred Phase-9 item 1 (testcontainers full-flow test) and
exercises the Phase 10 dedup gate, retention loop, and notify writeback
end to end.

internal/event/integration_test.go (build tag integration):

- TestIntegration_FullCreateAndTriggerFlow: spins up testcontainers
  Postgres + miniredis, builds the same chi router shape main.go uses
  (RequestID + Recovery + tokenH.RegisterTriggerRoutes + /api subrouter
  with /tokens POST). POSTs a webbug create, then GETs /c/{id} twice
  from the same CF-Connecting-IP. Asserts: only one notification fires
  (capturingSender callCount == 1), both events recorded (CountByToken
  == 2), one event marked NotifySent + one NotifyDeduped, dedup INCR
  bumped the Redis counter to "2", trigger_count == 2 on the token.

- TestIntegration_DifferentIPsBothNotify: same setup, three different
  IPs each fire their own notification (no cross-IP dedup).

- TestIntegration_DedupTTLExpiry: builds an event.Service with 50ms
  DedupTTL, records two events (second deduped), miniredis.FastForward
  past TTL, third record notifies again — proves the SetNX TTL is the
  authoritative window boundary.

- TestIntegration_RetentionLoopPrunes: inserts 7 events, runs
  RunRetentionLoop with limit=3, asserts the table converges to 3
  rows (newest kept) and the loop exits cleanly on context cancel.

The capturingSender is a notify.Sender impl; this lets us assert
on whether the dispatch goroutine actually called Send() without
mocking the HTTP layer of the real telegram/webhook senders.
2026-05-14 00:32:28 -04:00
CarterPerez-dev 0524d84c89 chore(canary): scope gosec G101/G107/G704 to outbound HTTP packages
Closes deferred Phase-9 item 4. Phase 9's rollup added G107 + G704 to
the global gosec.excludes as a stop-gap so the Turnstile siteverify
call wouldn't flag SSRF taint. With Phase 10 introducing webhook
sender that takes user-supplied URLs (validated at the call site via
webhook.validateURL before the request is built), the global excludes
hide the very kind of issue we want flagged anywhere new outbound HTTP
lands.

- gosec.excludes: drop G107 + G704 globally; keep only G104 + G706
  (the codebase-wide errcheck/subprocess policies).
- exclusions.rules path-scope G107|G704 to:
  * internal/notify/telegram/sender.go (operator-config bot URL)
  * internal/notify/webhook/sender.go (user URL with validateURL gate)
  * internal/turnstile/verifier.go (operator-config siteverify URL)
- exclusions.rules path-scope G101 to internal/config/config.go where
  the env-var-name map (WEBHOOK_HMAC_SECRET, TURNSTILE_SECRET_KEY,
  OPERATOR_TOKEN, etc.) trips the hardcoded-credentials regex on
  literal env var names — false positives, not actual secrets.

Any new client.Do(req) outside these three packages will surface
G107 at the call site and force the author to add URL validation.
2026-05-14 00:32:07 -04:00
CarterPerez-dev 2799045ee7 refactor(canary): consolidate realIP into middleware across 5 generators
Closes deferred Phase-9 item 8. Phase 9 promoted realIP / lastNonEmptyXFF /
OptionalHeader to internal/middleware and refactored webbug only; the
other generators kept their byte-identical local copies. Sweeping them
all over now while the touch surface is already open for Phase 10.

- docx, envfile, kubeconfig, pdf, slowredirect: drop the local
  realIP / lastNonEmptyXFF / optionalHeader functions plus the
  CF-Connecting-IP / X-Forwarded-For / X-Real-IP header constants.
  Trigger paths now call middleware.RealIP / middleware.OptionalHeader
  directly. Drops the unused "net" import from each.
- slowredirect/fingerprint_handler.go: same treatment for the
  AttachFingerprint call site.

No behavior change — middleware.RealIP is byte-identical to what was
previously inlined per generator; verified by re-running the per-package
trigger tests post-refactor.
2026-05-14 00:31:49 -04:00
CarterPerez-dev f9b012699a feat(canary): wire event + notify into runtime + create-tier rate limits
Swaps the Phase-9 directEventRecorder/mysqlEventRecorder shims for
real event.Service.Record via an eventRecorderAdapter. Constructs
notify.Service with telegram + webhook senders. Spawns the retention
loop goroutine off the main wait group; gracefulShutdown now also
notifySvc.Wait()s for in-flight notifications to drain.

Closes deferred Phase-9 items 2 (real event recorder), 3 (nested
create-tier rate limits), 5 (FingerprintRecorder wiring), 7 (drop
required tag on TurnstileResp).

- cmd/canary/main.go:
  * buildEventStack constructs telegram + webhook senders and registers
    them on notify.Service. event.Service wired with eventRepo (Store +
    StatusWriter), tokenRepo (TokenIncrementer), redis client, notifier.
  * eventRecorderAdapter and a single mysqlEventRecorder both delegate
    to event.Service.Record(ctx, t.NotifyInfo(), evt) — one path,
    same dedup + notify semantics for HTTP triggers and mysql triggers.
  * fingerprintRecorderAdapter wraps event.Repository.AttachFingerprint
    with the configured window (defaults to 5 min) so the slow-redirect
    fingerprint POST writes through.
  * spawnRetentionLoop runs event.Service.RunRetentionLoop on the
    shared wait group; configured interval + per-token limit gate it.
  * /api/tokens POST gains nested create-tier limits per spec §11.3:
    LimitCreateMin (5/min) + LimitCreateHour (20/hr) chained ahead of
    Turnstile, with their own fingerprint-keyed Redis namespaces so
    they don't share buckets with the read-tier limiter.
  * Shutdown order: server → telemetry → redis → db, then
    notifySvc.Wait() to drain in-flight goroutines, then wg.Wait()
    for retention loop + mysql listener.

- internal/config/config.go:
  * NotifyConfig: dedup_ttl, send_timeout, max_tries, max_elapsed,
    initial_interval, retention_interval, retention_limit,
    webhook_hmac_secret, telegram_api_base, fingerprint_window.
  * RateLimitConfig: create_min_rate / burst, create_hour_rate / burst.
  * Defaults + env mappings (NOTIFY_*, RATE_LIMIT_CREATE_*, plus the
    spec'd WEBHOOK_HMAC_SECRET).

- internal/token/dto.go: drop the `validate:"required"` tag on
  cf_turnstile_response. The middleware (TurnstileVerify) is the sole
  authority — it bypasses on empty SecretKey for dev mode and would
  reject empty-token submissions in prod via the Verifier's own
  ErrEmptyToken path. The validator tag was forcing tests to pass
  a placeholder string in dev mode which conflicted with the bypass.
2026-05-14 00:31:24 -04:00
CarterPerez-dev 0757d9f196 feat(canary): event + notify domain — contracts, senders, services
Lays the domain layer for Phase 10 notification dispatch.

- event/contract.go: NotifyInfo (token-shape DTO that breaks the would-be
  event→notify→token cycle), Notifier interface, TokenIncrementer interface,
  Store interface (Insert+UpdateNotifyStatus+PruneToLimit).
- token.Token.NotifyInfo() helper bridges *token.Token → event.NotifyInfo
  for the main.go adapter.
- notify/types.go: Sender + StatusWriter interfaces.
- notify.Service: per-channel sender registry, async fire-and-forget
  Notify with bounded sendTimeout, WaitGroup for graceful drain, status
  writeback (sent / failed) on the configured StatusWriter.
- telegram.Sender: POST /bot{TOKEN}/sendMessage with MarkdownV2-escaped
  body, cenkalti/backoff/v5 retry (3 tries / 30s window), permanent on
  4xx, 10s overall + 5s connect timeouts. Note: spec body said
  parse_mode=Markdown but escape table is the V2 set; using MarkdownV2
  keeps the escape rules consistent.
- webhook.Sender: POST user URL with versioned JSON envelope (§10.4),
  optional HMAC-SHA256 signing via X-Canary-Signature, URL validation
  at the call site (rejects non-http(s), missing host, userinfo). Same
  retry/timeout policy as telegram.
- event.Service: Record(ctx, info, evt) inserts → IncrementTriggerCount
  → Redis SetNX dedup gate (15m TTL, fail-open on Redis error). First
  trigger calls notifier; duplicates INCR + UpdateNotifyStatus(deduped).
- event.Service.RunRetentionLoop(ctx, interval, limit) tickered prune.

Adds cenkalti/backoff/v5 + miniredis/v2 to go.mod for backoff API and
unit-test Redis fakes.
2026-05-14 00:30:42 -04:00
CarterPerez-dev 6c37082598 fix(canary-phase9): plumb mysql public host/port through to generator
Audit found that registry.Build was calling mysql.New() with hardcoded
localhost:3306 defaults, ignoring cfg.MySQL.PublicHost and PublicPort.
Result: every emitted mysql:// connection string said
"mysql://canary_<id>@localhost:3306/internal_db" regardless of actual
deployment address — useless for any non-localhost deployment.

Fix:
  - registry.Config: adds MySQLPublicHost + MySQLPublicPort fields
  - registry.Build: switches between mysql.New() and
    mysql.NewWithAddress(host, port) based on config
  - cmd/canary/main.go: passes cfg.MySQL.PublicHost/PublicPort
    through to registry.Build
  - registry_test.go: new TestBuild_MySQLUsesConfiguredPublicAddress
    asserts the connection string reflects configured host/port

Other Phase 9 audit observations DEFERRED to Phase 10 (not blocking
rollup):
  - §11.3 create-tier rate limit on POST /api/tokens (Turnstile
    already provides spam protection; tiered limits are
    belt-and-suspenders)
  - scoping the gosec G107/G704 exclude to specific packages
    (revisit when Phase 10's webhook sender lands and needs
    case-by-case URL validation)
  - TurnstileResp dto required-tag clashes with dev-mode bypass
    (low severity — current behavior is sound)
  - /api/m/{manage_id} GET/DELETE (spec §8.1 lines 740-741) — Phase 11
  - FingerprintRecorder is nil at startup (slowredirect fingerprint
    handler wiring) — Phase 10
  - mysql_username metadata write in token.Service (mysql trigger
    lookup goes by token ID, not username — low impact)

Pre-rollup gate clean at HEAD:
  - go build ./... + go vet ./... clean
  - go test -race -timeout=60s ./... all pass (~50 new test cases)
  - go test -tags=integration -race -timeout=300s ./internal/token/...
    ./internal/event/... all pass
  - golangci-lint run ./... → 0 issues
  - grep -rn "//nolint" --include="*.go" returns nothing
2026-05-13 15:27:21 -04:00
CarterPerez-dev 8d1016b87d feat(canary): full wire-up + webbug refactor + mysql goroutine
Phase 9 tasks 9.7 + 9.8 + closes Phase 8 task 8.5 deferral.

config.Config additions:
  - Canary    {BaseURL, ManageURL}        — public URLs for trigger
                                            + manage redirects
  - Turnstile {SecretKey, SiteKey}        — Cloudflare Turnstile
                                            (empty SecretKey = dev bypass)
  - MySQL     {Enabled, Addr, PublicHost, — fake TCP listener config
               PublicPort}

cmd/canary/main.go full wire-up per spec §6.3 + §11.1:
  - Global middleware: RequestID → Logger → Recovery → SecurityHeaders
  - /healthz: no further middleware (router-mounted directly)
  - /c/{id}, /c/{id}/fingerprint, /k/{id}, /k/{id}/*: mounted at
    router root, OUTSIDE /api subrouter, so they skip CORS + rate
    limit (spec §11.1 line 1552 — we want to record EVERY hit)
  - /api subrouter: CORS → RateLimit (KeyByFingerprint, FailOpen=true)
  - /api/tokens/types: no further middleware (read-only public list)
  - /api/tokens POST: + TurnstileVerify middleware
  - mysql goroutine: spawned via spawnMySQLListener iff
    cfg.MySQL.Enabled; uses mysqlTokenLookup + mysqlEventRecorder
    adapters bridging mysql.{TokenLookup,EventRecorder} to
    token.Service + event.Repository

Adapter types in cmd/canary/main.go:
  - registryAdapter: bridges registry.Registry (map type) to
    token.Registry interface (Get method)
  - directEventRecorder: bridges token.EventRecorder to
    event.Repository.Insert + token.Repository.IncrementTriggerCount
    (synchronous; Phase 10 swaps in event.Service for dedup + async
    notify)
  - mysqlTokenLookup, mysqlEventRecorder: same shape for the TCP
    mysql handler

webbug refactor (task 9.8):
  - Delete local realIP/lastNonEmptyXFF/optionalHeader copies (~30 LOC)
  - Use middleware.RealIP and middleware.OptionalHeader instead
  - Tests still pass (the IP-precedence suite in webbug_test.go
    exercises middleware.RealIP behavior end-to-end via the Trigger
    contract)

Pre-rollup gate clean at HEAD:
  - go build ./... + go vet ./... clean
  - go test -race -timeout=60s ./... all pass
  - go test -tags=integration -race -timeout=300s ./internal/token/...
    ./internal/event/... all pass
  - golangci-lint run ./... → 0 issues
  - grep -rn "//nolint" --include="*.go" returns nothing

DEFERRED (not in Phase 9 scope):
  - Phase 9 task 9.9 integration test (full POST /api/tokens against
    testcontainers) — defer to Phase 10's broader integration suite
    along with event.Service + notify.Service end-to-end coverage
  - Phase 10 brings: event.Service with dedup + async notify, swaps
    in for the directEventRecorder/mysqlEventRecorder adapters
  - Phase 13 brings: geoip.Service wired into event recording
2026-05-13 15:20:01 -04:00
CarterPerez-dev 0a456f97ae feat(canary): token HTTP handlers (API + trigger routes)
Phase 9 task 9.6.

Routes:
  RegisterAPIRoutes(r):
    GET  /tokens/types  → static list of 7 type descriptors
    POST /tokens        → CreateToken (validation + service.Create)

  RegisterTriggerRoutes(r):
    GET  /c/{id}                 → HandleTrigger (webbug, slowredirect,
                                    docx, pdf, envfile use this path)
    POST /c/{id}/fingerprint     → HandleFingerprint (slowredirect)
    HandleFunc /k/{id}           → HandleTrigger (kubeconfig, bare path)
    HandleFunc /k/{id}/*         → HandleTrigger (kubeconfig wildcard,
                                    any HTTP method)

HandleTrigger flow:
  - extract token id from chi.URLParam
  - service.GetByID (returns nil-nil for not-found)
  - if token exists and !enabled, treat as unknown
  - resolve generator (known token → its type; unknown + path
    prefix /k/ → kubeconfig; unknown otherwise → webbug)
  - generator.Trigger(ctx, tok, r) → event + response
  - record event via EventRecorder interface if present (Phase 10
    wires real event.Service)
  - write response: headers + body OR 302 + Location for redirect

CreateToken flow:
  - 64KB body cap via http.MaxBytesReader
  - JSON decode (bad → 400 BAD_JSON)
  - extract fingerprint + real IP via middleware helpers
  - service.Create
  - error switch: validation/unknown-type/destination/include-keys
    map to 400; generator-failure to 500
  - happy path: 201 with {token, artifact} envelope per spec §8.2

ArtifactJSON discriminator pattern:
  - kind=url     → url + destination_url
  - kind=file    → filename + content_type + content_b64 (base64)
  - kind=text    → filename + content_type + content (raw)
  - kind=conn... → connection_string

Tests (~10 cases): 7-type list, happy path with envelope assertion,
bad JSON, validation failure, known-token records event + writes
response, unknown-token still returns shape (defense-in-depth),
disabled-token treated as unknown, fingerprint without recorder
returns 204, ArtifactJSON discriminator for all 4 kinds.

Phase 9 task 9.6 discharged. Wire-up + config + webbug refactor +
integration test next.
2026-05-13 15:16:41 -04:00
CarterPerez-dev 019bfda437 feat(canary): token service + contract relocation
Phase 9 task 9.5.

Resolved a long-standing import-cycle constraint: the Generator
interface + Artifact + ArtifactKind + TriggerResponse types now live
at internal/token/contract.go (the token package, which both the
service and concrete generators can import). The
internal/token/generators/generator.go file now re-exports these
as Go type aliases for backwards compatibility — concrete generators
(webbug/slowredirect/docx/pdf/kubeconfig/envfile/mysql) keep their
existing `generators.Artifact` / `generators.KindFile` references
unchanged.

This unlocks token.Service.Create, which would otherwise be unable
to type-reference Artifact (since generators imports token for
token.Type/token.Token).

token.Service:
  - Create(ctx, req, fp, ip) (*Token, Artifact, error):
      - go-playground/validator on the request DTO
      - type-specific metadata validation:
        - slowredirect → metadata.destination_url required, must be
          http:// or https://
        - envfile → metadata.include_keys (if present) must be a
          subset of {aws,stripe,github,db}; malformed JSON rejected
        - others → no extra validation
      - registry.Get(type) → ErrUnknownGeneratorType on miss
      - generateTokenID: 12-char [a-z0-9] via crypto/rand
      - uuid.NewString for manage_id
      - call Generator.Generate (which may mutate t.Metadata for
        mysql's mysql_username persist)
      - repo.Insert; rollback semantics: if generator fails, no
        persistence happens
  - GetByID / GetByManageID: proxy to repo, return (nil, nil) on
    not-found (so handlers can distinguish 404 from real errors)
  - IncrementTriggerCount: proxy to repo
  - TriggerURL / ManageURL: URL builders so handlers don't duplicate
    path joining
  - Generator(t Type): registry lookup for handler dispatch

Tests (~15 cases): ID + manage_id format regexes, repo persistence,
generator call, unknown type, validation failure, slowredirect
metadata variants (missing/invalid-scheme/valid), envfile include_keys
(invalid/valid), generator error path skips persistence, distinct IDs
across 30 calls, GetByID nil-nil semantics, URL builders.
2026-05-13 15:13:43 -04:00
CarterPerez-dev 537b0e3fd8 feat(canary): turnstile verifier + middleware
Phase 9 task 9.2 + 9.3. Cloudflare Turnstile integration for
POST /api/tokens spam protection.

turnstile/verifier.go:
  - Verifier{secret, verifyURL, client, rdb}
  - Verify(ctx, token, fingerprint) error:
      - empty secret → dev-mode bypass (return nil)
      - empty token → ErrEmptyToken
      - cache hit on "turnstile:fp:<fp>" → return nil (5-min TTL)
      - else: POST to challenges.cloudflare.com/turnstile/v0/siteverify
        with secret + response, decode, return ErrVerifyFailed on
        false or wrap network err
      - on success, write cache entry (best-effort, logged on error)
  - RedisClient interface (Get + SetEx) so tests can use a fake
    without miniredis dep
  - HTTP client timeout 10s, defaults to DefaultVerifyURL constant

middleware/turnstile.go:
  - TurnstileVerifier interface (Verify) decouples middleware from
    the concrete verifier — lets tests inject a fake
  - TurnstileVerify(v) middleware: extracts token from
    CF-Turnstile-Response header OR JSON body's cf_turnstile_response
    field (header wins); preserves request body for downstream
    handlers by re-wrapping io.NopCloser; on verify failure returns
    400 with {success:false, error:{code:"TURNSTILE_FAILED"}}

.golangci.yml: adds G104, G706, G107, G704 to gosec excludes.
G704/G107 are SSRF taint-analysis rules that fire on any HTTP client
.Do call where the URL came from a struct field. In this codebase
all outbound HTTP calls go to operator-configured endpoints (Turnstile
siteverify, future Phase 10 Telegram), with user-supplied webhook
URLs validated at the call site before reaching client.Do. The
global excludes are policy-level (matches the G104/G706 precedent
of "this codebase doesn't do X").

Tests (~14 cases):
  - verifier: empty-secret bypass, empty-token error, success cached
    by fingerprint, cache miss for different fingerprint, failed
    siteverify returns ErrVerifyFailed, nil-redis works without
    caching, network error wrapped (and NOT ErrVerifyFailed)
  - middleware: header token passes, body token passes, body
    preserved for downstream, failure returns 400 with code, header
    wins over body when both present, empty-token still calls
    verifier
2026-05-13 15:08:37 -04:00
CarterPerez-dev c988e4b902 feat(canary): middleware additions (realip, fingerprint, recovery)
Phase 9 first commit. Three middleware additions that the token
handler + service will consume.

  - realip.go: RealIP(r) + OptionalHeader(v) + lastNonEmptyXFF —
    promotion of the helpers currently duplicated across webbug/
    slowredirect/docx/pdf/kubeconfig/envfile/mysql generators. The
    bodies are byte-identical to those copies. Phase 9 task 9.8
    refactors webbug to use this shared helper next; the other
    generators' duplication remains until Phase 10's Trigger plumbing
    refactor (Phase 9 keeps the scope to webbug per the plan)
  - fingerprint.go: ExtractFingerprint(r) = hex(sha256(realIP|UA))[:16]
    + KeyByFingerprint(r) = "ratelimit:fp:" + fp, per spec §11.2
    lines 1568-1591. The 16-char prefix is a deliberate space/
    collision trade-off documented in the spec — full sha256 is 64
    chars, 16 chars = 64 bits ≈ 1e19 keyspace, fine for fingerprint
    rate-limiting where collisions are caught by the rate limit
    granularity anyway
  - recovery.go: Recovery(logger) middleware — defer recover(),
    logs panic + request_id + path + stack, writes a 500 with the
    project's standard {success:false, error:{code,message}}
    envelope. Per spec §11.1 line 1543

Tests (~16 cases): 11 source-IP precedence cases matching the
generator suites + OptionalHeader empty/whitespace/value + fingerprint
determinism/length/hex-format/different-IPs-distinct + KeyByFingerprint
prefix + recovery panic-returns-500 + recovery passes-through-non-panic
+ recovery logs request ID through the RequestID middleware chain.

Phase 9 task 9.1 + 9.4 discharged.
2026-05-13 15:04:29 -04:00
CarterPerez-dev fe80c94a73 feat(canary): mysql generator + register in registry
Phase 8 third commit. Closes the generator set — all 7 token types are
now registered.

mysql/generator.go:
  - Generator with publicHost / publicPort / database fields. New()
    returns localhost:3306/internal_db defaults; NewWithAddress(host,
    port) overrides for Phase 9 wire-up
  - Generate writes mysql_username into t.Metadata (json.RawMessage
    merge via setMySQLUsername helper — same defensive map[string]
    json.RawMessage pattern as envfile's extractIncludeKeys: malformed
    existing metadata is replaced rather than erroring, existing
    fields preserved, prior mysql_username overwritten)
  - Returns Artifact{Kind: KindConnectionString, ConnectionString:
    "mysql://canary_<id>@<host>:<port>/internal_db"} per spec §9.7
    lines 1345-1352. baseURL ignored (mysql connection strings don't
    use it)
  - Trigger(ctx, t, r) returns (nil, nil, ErrHTTPTriggerNotSupported)
    sentinel error — mysql triggers fire over the TCP listener (see
    server.go/handler.go), not via the HTTP router. Phase 9's router
    will not mount any HTTP route to mysql.Trigger; this is defense
    for accidental programmatic calls
  - Note on spec §9.7 line 1343: `t.Metadata["mysql_username"] =
    username` doesn't compile against the real Token.Metadata
    (json.RawMessage). The setMySQLUsername helper is the sanctioned
    *string-shape adaptation, matching the slowredirect/envfile
    metadata patterns

mysql/generator_test.go (~11 cases):
  - Type / KindConnectionString / default connection string format /
    custom address / canary_ prefix invariant / metadata persistence
    on empty token / preservation of existing metadata fields /
    malformed-metadata fallback / stale-mysql_username overwrite /
    baseURL ignored / Trigger returns ErrHTTPTriggerNotSupported with
    nil event + nil response

registry update: cardinality 6 → 7; pending list now empty.
TestBuild_AllSevenGeneratorsRegistered replaces the
TypesPresentInPhaseN test pattern from prior phases, asserting all 7
generator types are present (closes the generator set; future phases
add services/handlers, not generators).

DEFERRED TO PHASE 9 (task 8.5): wiring `go mysql.Run(ctx, addr,
handler)` into cmd/canary/main.go. The handoff anti-relitigation set
documents "Phase 9 wires the registry into main.go" — the mysql
listener goroutine fits naturally into Phase 9's main.go work
alongside config.Config.MySQL.Enabled, config.Config.MySQL.Addr,
the token.Service (for TokenLookup), and the event.Service (for
EventRecorder). Wiring it in Phase 8 would require partial config
additions that Phase 9 then re-touches.

Pre-rollup acceptance gate clean at HEAD:
  - go build ./... + go vet ./... clean
  - go test -race -timeout=60s ./... all packages pass (~115 tests
    added across Phase 8 alone)
  - go test -tags=integration -race -timeout=300s pass
  - golangci-lint run ./... → 0 issues.
  - zero //nolint pragmas anywhere
2026-05-13 14:55:37 -04:00
CarterPerez-dev 4d4d4951ba feat(canary): mysql server + connection handler
Phase 8 second commit. TCP listener orchestration + per-connection
business logic.

server.go (~70 LOC):
  - mysql.Run(ctx, addr, ConnectionHandler) error
    Listens via net.ListenConfig (context-aware), accepts in a loop,
    dispatches each connection to handler.HandleConnection in its own
    goroutine, waits on context cancellation to close the listener
    and drain in-flight connections via WaitGroup
  - ConnectionHandler interface (single HandleConnection method) so
    tests can substitute a stub without standing up real handler deps
  - Logs listener bind + accept errors via slog; suppresses net.ErrClosed
    on graceful shutdown so the shutdown path is silent

handler.go (~170 LOC):
  - TokenLookup interface (GetByID) and EventRecorder interface (Record)
    decouple the handler from Phase 9's token.Service and Phase 10's
    event.Service. Production wire-up in Phase 9 (deferred — see Phase
    8 rollup notes on task 8.5 deferral)
  - HandleConnection writes HandshakeV10 → reads HandshakeResponse41 →
    extracts username → strips canary_ prefix → looks up token →
    records event with mysql_username + mysql_client_capabilities +
    mysql_client_charset in event.Extra (mirrors kubeconfig's
    kubectl_* forensic capture) → writes ERR_Packet 1045 → defers
    close. 10-second connection deadline.
  - Defense-in-depth: usernames without canary_ prefix dropped
    silently, unknown tokens dropped silently, lookup errors dropped
    silently — no behavior signal to a probing attacker
  - Nil EventRecorder tolerated (writes ERR but skips event) — Phase 9
    can wire the handler before event.Service exists if needed

Tests (~14 cases):
  - server: nil-handler rejected, basic dispatch, 10 concurrent
    connections, context cancellation closes listener cleanly,
    invalid address rejected
  - handler: handshake sent first, non-canary username drops silently,
    known token records event AND sends ERR, event.Extra carries
    all three kubectl-equivalent fields with correct hex formatting,
    unknown token records nothing, lookup error silent, nil
    EventRecorder tolerated (still sends ERR), bad auth packet
    silent
  - Uses real net.Listener + net.Conn pairs (not mocks) for
    behavioral fidelity — TCP wire-protocol tests should hit real
    sockets

go test -race -timeout=60s passes. golangci-lint clean.
2026-05-13 14:53:22 -04:00
CarterPerez-dev 712202abc0 feat(canary): mysql wire protocol (handshake + auth + ERR packets)
Phase 8 first commit. Pure encoding/decoding layer for the MySQL
wire protocol — no I/O orchestration, no DB lookups, no business
logic. Three primary functions plus utility helpers, all driven by
the byte-exact spec §9.7 lines 1363-1403:

  - BuildHandshakeV10(connID, authData[20]) []byte
    Server's initial greeting packet (0x0a protocol version,
    "5.7.40-canary" server version, random auth-plugin-data split
    into 8+12 byte parts, capability flags 0xf7ff / 0x81ff,
    utf8mb4_unicode_ci charset, mysql_native_password plugin name).
    Wraps in 3-byte LE length + 1-byte sequence ID (0x00).

  - ReadClientAuth(r io.Reader) (*ClientAuth, error)
    Parses HandshakeResponse41. Returns the username (null-terminated,
    extracted starting at byte offset 32 after the 4+4+1+23-byte
    fixed header) plus the client's capabilities, max packet size,
    and charset for forensic richness. Defensive bounds checking
    against ErrInvalidPayload / ErrUsernameMissing / ErrShortPacket /
    ErrPacketTooLarge sentinels. 64KB max packet size cap.

  - BuildAccessDeniedErr(username, sourceHost string) []byte
    Standard MySQL ERR_Packet 1045 with SQL state "28000" and the
    exact "Access denied for user '%s'@'%s' (using password: YES)"
    message kubectl and the mysql client both display verbatim.
    Sequence ID 0x02 (after handshake=0 and client auth=1).

Plus crypto/rand-backed helpers NewRandomAuthData (20 bytes for the
challenge) and NewRandomConnectionID (uint32). Consistent with the
crypto/rand discipline from Phase 0 supplement, Phase 5 (pdf), Phase 7
(envfile).

Tests (~24 cases): byte-exact assertions on packet headers, payload
layout, sequence IDs; round-trip Build/Read for 5 username variants;
error paths (short packet, missing username terminator, empty reader,
oversize packet rejected mid-stream); randomness distinctness (50
calls → near-50 unique values).

Handler + server land in next commit; generator + registry in the
one after. Trigger interface conformance for the mysql Generator will
be a sentinel-error no-op (mysql triggers over TCP, not HTTP — Phase
9 router doesn't mount any HTTP path to mysql.Trigger).

go test -race -timeout=60s ./internal/token/generators/mysql/...
passes, golangci-lint clean.
2026-05-13 14:49:03 -04:00
CarterPerez-dev 1cde61efbb feat(canary): register envfile generator in registry
Sixth entry in the registry map. Cardinality bumps 5 → 6; pending list
shrinks to {mysql} (1 remaining).

  - registry.go: adds the import + token.TypeEnvfile: envfile.New()
  - registry_test.go:
      - TestBuild_RegistersEnvfile added (mirrors RegistersKubeconfig)
      - TypeEnvfile removed from TestBuild_PendingTypesNotYetRegistered
      - TestBuild_OnlyExpectedTypesPresentInPhase6 renamed → InPhase7,
        cardinality assertion bumped 5 → 6

Pre-audit gate clean at HEAD: go build / vet / test -race / integration
tests / golangci-lint run all pass with 0 issues.
2026-05-13 14:37:02 -04:00
CarterPerez-dev 2a2a3c4d8f 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.
2026-05-13 14:36:06 -04:00
CarterPerez-dev 003aa9970c feat(canary): envfile bait recipes (aws/stripe/github/db)
Phase 7 first commit. Four recipes for generating realistic-looking
credentials that pass syntactic validation by tools like gitleaks but
are NOT real credentials — they're decoration that makes the env file
look like a legitimate prod config dump. The detection mechanism is
the canary URL embedded by the generator (next commit); the bait keys
themselves don't trigger anything.

Package layout (sub-package under envfile/):
  - recipes.go: EnvLine, Recipe interface, internal registry, shared
    crypto/rand helpers (RandomAlnumUpper / RandomAlnumMixed /
    RandomHexLower / RandomBase64 / RandomChoice). All randomness
    uses crypto/rand — no math/rand or gosec G404 fight, matches the
    Phase 0 supplement's discipline.
  - aws.go: AKIA + 16 [A-Z0-9] body (gitleaks regex
    `AKIA[0-9A-Z]{16}`) + 40+ base64-encoded secret + canonical
    region (us-east-1 etc.) + stable S3 bucket name
  - stripe.go: sk_live_/pk_live_/whsec_ prefixes with 24/24/32-char
    [A-Za-z0-9] bodies (matches Stripe's documented live-key format)
  - github.go: ghp_ + 36 base62 body + 6 base62 "checksum" = 42 chars
    after the prefix (matches gitleaks
    `ghp_[0-9a-zA-Z]{36,255}`); plus base64 deploy-key, stable owner +
    repo names (acme-corp / internal-platform)
  - db.go: postgres://app_writer:<24-char pass>@db.internal:5432/
    app_prod?sslmode=require + redis://default:<32-char pass>@
    cache-prod.internal:6379/0 — realistic internal-DNS-style
    hostnames per spec §9.6 "Bait realism principles"

Public API:
  - recipes.Get(key) (Recipe, bool) — registry lookup
  - recipes.AvailableKeys() []string — sorted snapshot
  - Recipe types (AWS, Stripe, GitHub, DB) exported so callers can
    construct them directly if needed (registry is the normal path)

Tests (~30 cases): one test file per recipe plus shared helpers.
  - Format-regex assertions: AKIA + base32-upper, sk_live_/pk_live_/
    whsec_ + base62, ghp_ + base62, region xx-name-n
  - URL parsing for postgres + redis (real net/url validation, not
    string matching)
  - Distinct-invocations checks (20 calls → near-20 unique secrets)
  - Helper coverage: RandomAlnumUpper/Mixed/HexLower/Base64/Choice
    length + alphabet + empty-input handling

Recipes package compiles + tests pass in isolation. Generator and
registry wire-up land in next commits.

go test -race -timeout=60s ./internal/token/generators/envfile/recipes/...
passes with 0 lint issues.
2026-05-13 14:33:16 -04:00
CarterPerez-dev 982beafdc9 fix(canary-phase6): address audit nit before rollup
Phase 6 audit (2 agents in parallel) returned PASS. The
general-purpose spec-adherence agent flagged one actionable NIT (the
superpowers:code-reviewer flagged the same item as defensible — fixing
it per the fix-in-phase rule regardless).

N1 — template.yaml.tmpl line 2 file-header comment read
"# template.yaml" but the on-disk filename is template.yaml.tmpl
(renamed in commit 6f994f37 to avoid the repo's check-yaml pre-commit
hook). Project file-header rule is "©AngelaMos | 2026 + filename only
(NOT path)" — the comment must match the actual filename. Updated to
"# template.yaml.tmpl".

Other audit observations marked NOT-ACTIONABLE:
- NIT (verb table): verbFromMethod handles GET/POST/PUT/PATCH/DELETE
  + default-to-list. OPTIONS/CONNECT/TRACE fall through to "list"
  which is the sanctioned ceiling per the verb-derivation enhancement
  scope. Phase 14 may revisit if scan-OPTIONS forensics matters.
- NIT (defaultResource stutter): "resource is forbidden: ... cannot
  list resource resource ..." for empty-path edge cases. Real kubectl
  never emits an empty path; cosmetic only.
- NIT (empty baseURL): TrimRight on empty string produces empty +
  "/k/" + id which is relative. Registry wire-up guarantees non-empty
  baseURL; defensive guard would be belt-and-suspenders only.
- INFO (Bearer extraction): plan task 6.2 mentions "extracts Bearer
  (or path id)" — Phase 6's generator receives *token.Token
  already-resolved per the §6.2 Generator interface, so the path/Bearer
  dispatch lands at the routing layer in Phase 9. Phase 6 punt is
  legitimate.
- POSITIVE observations from both agents are retained as-is.

Pre-rollup gate clean: go build / vet / test -race / test -tags=
integration / golangci-lint run all pass.
2026-05-13 14:26:15 -04:00
Carter Perez 972729135d
Merge pull request #233 from CarterPerez-dev/project/foundations-tier
feat(foundations): add foundations tier — three teaching-first Python projects
2026-05-13 14:24:37 -04:00
CarterPerez-dev ceec4d37d1 feat(canary): register kubeconfig generator in registry
Fifth entry in the registry map. Cardinality bumps 4 → 5; pending list
shrinks to {envfile, mysql} (2 remaining).

  - registry.go: adds the import + token.TypeKubeconfig: kubeconfig.New()
  - registry_test.go:
      - TestBuild_RegistersKubeconfig added (mirrors RegistersPDF)
      - TypeKubeconfig removed from TestBuild_PendingTypesNotYetRegistered
      - TestBuild_OnlyExpectedTypesPresentInPhase5 renamed → InPhase6,
        cardinality assertion bumped 4 → 5

Pre-audit gate clean at HEAD: go build ./... + go vet ./... + go test
-race -timeout=60s ./... + go test -tags=integration -race -timeout=300s
./internal/token/... ./internal/event/... + golangci-lint run ./...
all pass with 0 issues.
2026-05-13 14:22:32 -04:00
CarterPerez-dev bd0c664602 feat(canary): kubeconfig fake K8s API handler (forbidden response)
Trigger implementation completes the kubeconfig Generator's interface
conformance. Behavior matches spec §9.5 + §8.5 defense-in-depth:

  - ALWAYS returns 403 + Kubernetes-shaped Status JSON (whether the
    token is valid or not). Attackers cannot distinguish "valid
    token, no permission" from "no such token" by status code or
    response shape — both are 403 with byte-identical structure modulo
    the resource/verb message slot.
  - Status JSON conforms to k8s.io Status object: kind=Status,
    apiVersion=v1, metadata={}, status=Failure, reason=Forbidden,
    code=403, message=verisimilar real-kubectl error string.
  - Message format mirrors kubectl's actual output:
      <resource> is forbidden: User "system:anonymous" cannot <verb>
      resource "<resource>" in API group "" in the namespace "default"
    where <resource> is the last segment of r.URL.Path and <verb> is
    derived from the HTTP method (GET/HEAD → list, POST → create, PUT
    → update, PATCH → patch, DELETE → delete, others → list). This is
    richer than spec §9.5's hardcoded "list" — kubectl ships the verb
    based on the action it's attempting, so a /apis/.../pods DELETE
    response saying "cannot list" would tip the attacker off. Method
    mapping is forensic gold for the operator (which kubectl action
    actually fired) and verisimilar to the attacker.
  - Event.Extra captures the kubectl_path / kubectl_method /
    kubectl_query / kubectl_ua fields per spec §9.5 line 1234. UA
    capture is the forensic prize — kubectl ships its version + arch
    in the UA string ("kubectl/v1.30.0 (linux/amd64) kubernetes/...").
  - Source-IP triplet (realIP / lastNonEmptyXFF / optionalHeader) is
    a content-copy of webbug/docx/pdf — sanctioned duplication per
    Phase 2/3/4 anti-relitigation set. Phase 9 middleware extraction
    collapses these into one shared helper.
  - Nil-token path returns the same 403+Status with verisimilar
    message + headers, and nil event so the handler cannot persist
    a row with empty TokenID (FK violation) — spec §8.5.

Tests cover (~36 cases including subtests):
  - 403 + JSON content-type + cache headers
  - Status JSON shape (Kind/APIVersion/metadata={}/Status/Reason/Code)
  - Message resource extraction: pods, secrets, single-resource get,
    API version probe, trailing slash, healthz probe (6 paths)
  - Verb derivation from HTTP method (7 methods, 5 distinct verbs)
  - User "system:anonymous" impersonation present
  - kubectl_path/method/query/ua in Extra JSON
  - Empty query string handled
  - Source-IP precedence (11 subcases mirroring docx/pdf for parity)
  - Missing UA/Referer → nil pointers
  - Nil-token defense-in-depth: still returns 403 + parseable Status,
    nil event, response-shape parity with valid-token case (HTTP code
    + JSON kind/version/status/reason/code all identical so an
    attacker cannot probe token validity by diffing responses)

Generator now satisfies generators.Generator interface. Registry wire-up
in next commit.
2026-05-13 14:22:19 -04:00
CarterPerez-dev 6f994f3756 feat(canary): kubeconfig generator + YAML template
Phase 6 first commit. Generator produces a real-looking kubeconfig
artifact that points kubectl at the canary's /k/{id} endpoint:

  - text/template embedded YAML following the spec §9.5 layout (one
    cluster + one context + one user, all referencing the same
    prod-cluster + svc-backup-reader names for verisimilitude)
  - APIServerURL = baseURL + "/k/" + t.ID (trailing-slash safe via
    strings.TrimRight)
  - Token = t.ID embedded as the user's bearer token — when kubectl
    fires a request, the bearer in the Authorization header is exactly
    this id, giving us a second lookup path beyond the URL path
  - ClusterName = "prod-cluster", UserName = "svc-backup-reader" —
    hardcoded per spec §9.5 lines 1193-1194 (tempting bait names that
    look like a real prod service-account kubeconfig)
  - Artifact.Kind = KindText (different from docx/pdf KindFile — YAML
    is text)
  - ContentType "application/yaml"
  - Filename default "kubeconfig" with the standard *string deref+trim
    pattern from docx/pdf

The template file is named template.yaml.tmpl (not .yaml) so the
repo's pre-commit check-yaml hook does not parse it as pure YAML —
the {{.X}} Go-template tokens are not valid YAML flow mappings and
would fail the parser. .tmpl is the conventional extension for
Go-templated source files; identify-cli (which check-yaml uses to
match files) does not classify .yaml.tmpl as YAML. Future phases
needing templated YAML (envfile recipes, etc.) should follow the
same convention.

Tests parse the rendered output through gopkg.in/yaml.v3 against a
typed schema struct, asserting every field flows through correctly:
APIVersion, Kind, current-context, Clusters[0].name + .cluster.server,
Contexts[0].name + .context.cluster + .context.user, Users[0].name +
.user.token. Plus Filename defaulting subtests, base-URL
trailing-slash trim, subpath preserve, distinct-ids → distinct
outputs, bearer-token-embedded check, ends-with-newline check.

Handler (Trigger) ships in the next commit per spec §9.5 file split
({generator, handler, template}). At this commit kubeconfig.Generator
has Type() + Generate() but no Trigger() — the package compiles in
isolation but does not yet satisfy generators.Generator interface.
The registry wires it in commit 3.

go test -race -timeout=60s ./internal/token/generators/kubeconfig/...
passes.
2026-05-13 14:21:57 -04:00
CarterPerez-dev d97662ee61 feat(foundations): add foundations tier — three teaching-first Python projects
Adds the foundations tier: three Python projects pitched at someone
who has never written Python before, ramping from a single-file hash
identifier up to the cryptographic boundary of the beginner tier.

Projects
- hash-identifier: identify ~30 hash formats by prefix, length, and
  charset (one file, ~30 tests).
- http-headers-scanner: fetch a URL and grade its security headers
  A–F (httpx + rich + respx, single-file scanner, 13 tests).
- password-manager: Argon2id + AES-256-GCM encrypted CLI vault with
  atomic durable writes and advisory file locking (the hardest
  foundations project — stepping stone into the beginner tier).

Each project ships with
- Heavily-commented source as a teaching aid (foundations-tier override
  of the no-comments rule — explanations targeted at first-time readers).
- A full learn/ folder: Overview, Concepts, Architecture, line-by-line
  Implementation walkthrough, Challenges.
- README with ASCII banner, badges (incl. tier + difficulty), demo
  sessions, command tables, learn-folder index, see-also links.
- install.sh + justfile + uv-managed dependencies.
- pytest + ruff + mypy + pylint config, all linters at 10.00/10.

Also centralizes the docs/ exclude in .gitignore (was a per-project
list of advanced/beginner docs paths) so dev-only audit reports and
plan documents stay local across the whole repo without needing
individual entries.
2026-05-13 13:57:33 -04:00
CarterPerez-dev 73dd837362 feat(canary): register pdf generator in registry
Fourth entry in the registry map. Cardinality bumps 3 → 4; pending list
shrinks to {kubeconfig, envfile, mysql} (3 remaining).

  - registry.go: adds the import + token.TypePDF: pdf.New() entry
  - registry_test.go:
      - TestBuild_RegistersPDF added (mirrors RegistersDocx)
      - TypePDF removed from TestBuild_PendingTypesNotYetRegistered
      - TestBuild_OnlyExpectedTypesPresentInPhase4 renamed → InPhase5,
        cardinality assertion bumped 3 → 4

Pre-audit gate clean at HEAD: go build ./... + go vet ./... + go test
-race -timeout=60s ./... + go test -tags=integration -race -timeout=300s
./internal/token/... ./internal/event/... + golangci-lint run ./... all
pass with 0 issues.
2026-05-13 13:54:33 -04:00
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
CarterPerez-dev c9c9333294 feat(canary): pdf template (binary blob + build helper)
Phase 5 first commit. Adds the committed template.pdf binary blob plus
the pure-Go one-shot utility that produced it, matching the docx Phase
4 pattern (cmd/build<format>template/main.go excluded from production).

cmd/buildpdftemplate/main.go assembles a minimal PDF-1.4 by hand:

  - Header: %PDF-1.4 + 4 high-bit bytes to mark as binary
  - Object 1: Catalog → Pages root
  - Object 2: Pages → single Kids ref + Count 1
  - Object 3: Page with /AA << /O << /Type /Action /S /URI /URI (...) >> >>
    where the URI value is the 76-char placeholder
    HONEY_TRACK_URL_PADDED_TO_FIXED_WIDTH______________________________________
  - xref table with computed byte offsets
  - trailer + startxref + %%EOF

The placeholder is a direct dictionary value (NOT inside a stream), so
substitution can be a plain byte-replace at runtime without breaking the
cross-reference table — spec §9.4 line 1133. No FlateDecode anywhere.

pdfcpu (v0.12.1) is used for validation only — the builder calls
api.Validate before writing, and tests will call it again on substituted
output to confirm the PDF stays well-formed.

Verification at HEAD:
  - 477-byte template.pdf
  - sha256: 70c6359016ceb780539f8c4497991b98ff82201e35a38d36090d88856027e103
  - Reproducible: rebuild produces byte-identical output
  - grep -aob HONEY_TRACK_URL → one offset (240)
  - file: PDF document, version 1.4, 1 page(s)
  - api.Validate passes

go.mod adds pdfcpu as a direct dep with its transitive deps via go mod
tidy. golang.org/x/crypto returns to direct (pdfcpu uses it for AES
encryption support).
2026-05-13 13:54:06 -04:00
CarterPerez-dev 13b2604f18 fix(canary): replace //nolint pragmas with explicit error handling
The standing "no //nolint anywhere" rule (handoff anti-relitigation
set) was honored by Phases 2-4 but inherited template code in
core/database.go, middleware/ratelimit.go, and health/handler.go
carried 6 pragmas from the original template import. Replacing each
with proper error handling rather than silencing the linter:

core/database.go
  NewDatabase ping-failure path: propagates db.Close() error via
  multi-%w fmt.Errorf rather than discarding it via _ = db.Close().
  Pattern matches the existing rollback error wrap on line 102.

  InTx + InTxWithOptions panic-recovery defer: rollback failure now
  logs via slog.Error before re-panicking, rather than discarding
  the error via _ = tx.Rollback(). The original error context is
  preserved by the panic; the rollback failure is observable.

  jitteredDuration: switches from math/rand/v2 to crypto/rand +
  math/big. The function runs once per pool init at startup so the
  per-call cost (~microseconds) is irrelevant. Eliminates the gosec
  G404 trigger without a global exclude (handoff anti-relitigation:
  "don't add new excludes for one-off issues"). Adds the missing
  maxJitter <= 0 guard that the rand.Int64N call would have panicked
  on with a base smaller than 7ns.

middleware/ratelimit.go
  writeRateLimitExceeded: json.NewEncoder().Encode error is now
  logged via slog.Error rather than discarded. slog is already
  imported and used elsewhere in the file (line 60).

health/handler.go
  writeStatus: same pattern; adds log/slog import. Brings the file
  in line with the rest of the codebase's slog-everywhere discipline.

After: zero //nolint pragmas anywhere in backend Go code
(grep -rn "//nolint" returns nothing). golangci-lint run ./... reports
0 issues. All unit tests pass under -race; all integration tests pass
under -race -tags=integration with the testcontainer Postgres.

Surfaced by the pre-Phase-5 cross-phase alignment audit. Cleared as
part of finishing the Phase 0 lint-discipline alignment so Phases 5+
inherit a fully consistent codebase.
2026-05-13 13:28:44 -04:00
CarterPerez-dev fdc4144cbc chore(canary): drop unused auth error helpers from core
Auth-template residue in core/errors.go + core/response.go:

  - UnauthorizedError() / ForbiddenError() + Err{Unauthorized,Forbidden}
    sentinels
  - TokenExpiredError() / TokenInvalidError() / TokenRevokedError() +
    Err{TokenExpired,TokenInvalid,TokenRevoked} sentinels
  - core.Unauthorized() / core.Forbidden() response wrappers

Zero call sites anywhere in the codebase. Worse, the Token*Error names
semantically collide with canary tokens (same package, completely
unrelated meaning) — a future-reader trap that would compound once
Phase 9 wires real operator-bearer auth gates on the admin handler.

Generic AppError plumbing (NewAppError, NotFoundError, DuplicateError,
ValidationError, InternalError, RateLimitError, IsAppError, GetAppError)
is preserved — all in active use.

When Phase 9 needs operator-bearer or turnstile-gated responses, helpers
can be reintroduced with names appropriate to the actual auth design
rather than carrying generic JWT-shaped vocabulary forward into a
canary-token namespace.

Surfaced by the pre-Phase-5 cross-phase alignment audit. Cleared per
the fix-in-phase rule. BACKLOG closed section records the resolution.
2026-05-13 13:28:30 -04:00
CarterPerez-dev 31f1fff343 chore(canary): drop unused argon2 password machinery from core
internal/core/security.go was preserved through Phase 0's auth prune
untouched. Audit: zero callers anywhere in the codebase, yet the file
kept golang.org/x/crypto as a direct dep and ran a full Argon2id KDF
on every process start via a package-level init() that pre-computed
a dummy hash "to prevent timing attacks" — wasted boot CPU for an
unused codepath.

Deleted symbols:
  - HashPassword, VerifyPassword, VerifyPasswordWithRehash
  - VerifyPasswordTimingSafe + dummyHash init()
  - decodeHash, needsRehash internal helpers
  - GenerateSecureToken, GenerateRefreshToken
  - HashToken, CompareTokenHash

go mod tidy demotes golang.org/x/crypto from direct to indirect
(pgx/v5 still pulls it transitively).

Surfaced by the pre-Phase-5 cross-phase alignment audit. Cleared as
part of finishing Phase 0's auth prune rather than logged as deferred
work. BACKLOG closed section records the item + resolution.
2026-05-13 13:28:17 -04:00
CarterPerez-dev 10ae118d53 fix(canary-phase4): address audit nits before rollup
Phase 4 audit (2 agents) returned PASS with three NITs. N1 + N2 cleared
in-phase per the standing fix-in-phase rule. N3 was stylistic-consistency
positive and not actionable.

N1 — generator_test.go IP-precedence subtests: added two missing cases
present in webbug's identical suite (the realIP helper triplet is byte-
identical to webbug, so this is symmetry rather than coverage):
- "RemoteAddr loopback IPv6 strips brackets and port" → "[::1]:9999"
- "XFF IPv6 rightmost" → "198.51.100.1, 2001:db8::dead"

N2 — generator.go patchTemplate loop: the second `cErr := ...` shadowed
nothing but reused the name from the close-error block earlier in the
same iteration. Renamed the header-create variant to `hErr` for clarity.

Audit verdicts:
- superpowers:code-reviewer: PASS (0 B / 0 S / 3 N)
- general-purpose spec-adherence vs §9.3 / §8.5: PASS (0 violations,
  6/6 invariants MATCH, BACKLOG.md still empty)
2026-05-13 03:19:13 -04:00
CarterPerez-dev 5884f98616 feat(canary): register docx generator in registry
Phase 4 Task 4.5 — adds token.TypeDocx: docx.New() to registry.Build.

Test updates:
- TestBuild_RegistersDocx: new, mirrors the webbug + slowredirect
  registration tests
- TestBuild_PendingTypesNotYetRegistered: drops TypeDocx from the
  pending list (4 remain: pdf, kubeconfig, envfile, mysql)
- TestBuild_OnlyExpectedTypesPresentInPhase3 →
  TestBuild_OnlyExpectedTypesPresentInPhase4: cardinality assertion
  bumped from 2 to 3
2026-05-13 03:15:00 -04:00
CarterPerez-dev 82bb3a9f14 feat(canary): docx generator (zip-patch INCLUDEPICTURE)
Phase 4 Task 4.3+4.4 — docx Generator that produces a tracked Word
document by embedding template.docx and runtime-substituting the
HONEY_TRACK_URL placeholder in word/footer2.xml with the per-token
trigger URL. Preserves per-entry zip Method (STORE/DEFLATE) so
Word/LibreOffice still opens the result; non-footer entry bodies are
byte-identical to the template.

Generate returns {Kind: KindFile, Filename: t.Filename ?? "Document.docx",
Content: <patched zip>, ContentType: wordprocessingml MIME}. The
spec §9.3 reference snippet uses `t.Filename == ""` which won't
compile against the real schema — Token.Filename is *string. The
generator dereferences safely via resolveFilename, trimming and
falling back to the default for nil/empty/whitespace pointers.

Trigger mirrors webbug exactly per spec §9.3 line 1118: 200 + 43-byte
transparent GIF via pixel.Clone() + cache-control no-store + pragma
no-cache. Nil-token returns the same response with nil event
(spec §8.5 — no token-existence enumeration). realIP /
lastNonEmptyXFF / optionalHeader helpers are copied from webbug per
the standing rule (sanctioned duplication until Phase 9 middleware
extraction).

24 test cases including the six prescribed by implementation plan
§4.3 (OutputIsValidZip, FooterContainsTriggerURL,
FooterDoesNotContainPlaceholder, OtherEntriesUnchanged,
PreservesCompressionMethods, ReturnsGIFLikeWebbug) plus surrounding
correctness/regression coverage (type, kind, content type, filename
defaulting incl. whitespace, trigger-URL trailing-slash + subpath +
uniqueness, source-IP precedence with 9 subtests mirroring webbug,
GIF body independence per Trigger call, nil-token defense).

Also fixes pre-existing lint debt in cmd/builddocxtemplate from
commit f36cce9a: errcheck on the deferred f.Close (now propagates
via named return) and gosec G304 on os.OpenFile (now filepath.Clean
on the operator-supplied -out path). Rebuilding template.docx after
this change produces byte-identical output (verified via sha256sum).
2026-05-13 03:14:12 -04:00
CarterPerez-dev f36cce9a01 feat(canary): docx template (binary blob + build helper)
Phase 4 Task 4.1+4.2 — pure-Go OOXML template builder under
backend/cmd/builddocxtemplate/. Run:

  go run ./cmd/builddocxtemplate \
    -out ./internal/token/generators/docx/template/template.docx

produces a minimal valid .docx (5 zip entries, mixed STORE/DEFLATE
compression) whose word/footer2.xml carries an INCLUDEPICTURE field
referencing the literal placeholder string HONEY_TRACK_URL with the
\\d switch (forces fetch-on-open, no local cache).

The committed template.docx is the embedded source for the Phase 4
docx generator (next commit). Mixed compression methods in the template
make Task 4.3's TestGenerate_PreservesCompressionMethods a meaningful
regression guard — a hardcode-DEFLATE generator would now mismatch on
[Content_Types].xml + word/_rels/document.xml.rels (both STORE).

cmd/builddocxtemplate is excluded from the production binary (separate
cmd/ subdir from cmd/canary).
2026-05-13 03:04:36 -04:00
Carter Perez e9533cdd4d
Merge pull request #229 from CarterPerez-dev/dependabot/uv/PROJECTS/advanced/api-rate-limiter/urllib3-2.7.0
chore(deps): bump urllib3 from 2.6.3 to 2.7.0 in /PROJECTS/advanced/api-rate-limiter
2026-05-13 02:42:27 -04:00
Carter Perez f700aac1ad
Merge pull request #227 from CarterPerez-dev/dependabot/go_modules/PROJECTS/intermediate/secrets-scanner/github.com/go-git/go-git/v5-5.19.0
chore(deps): bump github.com/go-git/go-git/v5 from 5.18.0 to 5.19.0 in /PROJECTS/intermediate/secrets-scanner
2026-05-13 02:42:15 -04:00
Carter Perez 07f7b3e07f
Merge pull request #225 from CarterPerez-dev/dependabot/uv/PROJECTS/advanced/ai-threat-detection/backend/gitpython-3.1.50
chore(deps): bump gitpython from 3.1.47 to 3.1.50 in /PROJECTS/advanced/ai-threat-detection/backend
2026-05-13 02:42:01 -04:00
Carter Perez 6de5a3b83d
Merge pull request #224 from CarterPerez-dev/dependabot/uv/PROJECTS/advanced/api-rate-limiter/python-multipart-0.0.27
chore(deps): bump python-multipart from 0.0.26 to 0.0.27 in /PROJECTS/advanced/api-rate-limiter
2026-05-13 02:41:38 -04:00
Carter Perez d0783c481e
Merge pull request #223 from CarterPerez-dev/dependabot/uv/PROJECTS/advanced/bug-bounty-platform/backend/mako-1.3.12
chore(deps): bump mako from 1.3.11 to 1.3.12 in /PROJECTS/advanced/bug-bounty-platform/backend
2026-05-13 02:41:25 -04:00
Carter Perez 95d9cd4202
Merge pull request #222 from CarterPerez-dev/dependabot/uv/PROJECTS/beginner/c2-beacon/backend/python-multipart-0.0.27
chore(deps): bump python-multipart from 0.0.26 to 0.0.27 in /PROJECTS/beginner/c2-beacon/backend
2026-05-13 02:41:07 -04:00
Carter Perez 3b076ab4ba
Merge pull request #221 from CarterPerez-dev/dependabot/uv/PROJECTS/advanced/bug-bounty-platform/backend/python-multipart-0.0.27
chore(deps): bump python-multipart from 0.0.26 to 0.0.27 in /PROJECTS/advanced/bug-bounty-platform/backend
2026-05-13 02:40:54 -04:00
Carter Perez fb33c8e810
Merge pull request #220 from CarterPerez-dev/dependabot/uv/PROJECTS/advanced/encrypted-p2p-chat/backend/mako-1.3.12
chore(deps): bump mako from 1.3.10 to 1.3.12 in /PROJECTS/advanced/encrypted-p2p-chat/backend
2026-05-13 02:40:47 -04:00
Carter Perez db3cf3d2f1
Merge pull request #219 from CarterPerez-dev/dependabot/uv/PROJECTS/advanced/encrypted-p2p-chat/backend/python-multipart-0.0.27
chore(deps): bump python-multipart from 0.0.26 to 0.0.27 in /PROJECTS/advanced/encrypted-p2p-chat/backend
2026-05-13 02:40:39 -04:00
Carter Perez 430c16d63e
Merge pull request #230 from CarterPerez-dev/dependabot/uv/PROJECTS/beginner/c2-beacon/backend/urllib3-2.7.0
chore(deps): bump urllib3 from 2.6.3 to 2.7.0 in /PROJECTS/beginner/c2-beacon/backend
2026-05-13 02:40:31 -04:00
Carter Perez bacc629091
Merge pull request #226 from CarterPerez-dev/dependabot/uv/PROJECTS/advanced/encrypted-p2p-chat/backend/urllib3-2.7.0
chore(deps): bump urllib3 from 2.6.2 to 2.7.0 in /PROJECTS/advanced/encrypted-p2p-chat/backend
2026-05-13 02:40:19 -04:00
Carter Perez 6f9ce8df4a
Merge pull request #228 from CarterPerez-dev/dependabot/uv/PROJECTS/advanced/bug-bounty-platform/backend/urllib3-2.7.0
chore(deps): bump urllib3 from 2.6.2 to 2.7.0 in /PROJECTS/advanced/bug-bounty-platform/backend
2026-05-13 02:40:11 -04:00
Carter Perez 5b8032754a
Merge pull request #231 from CarterPerez-dev/dependabot/uv/PROJECTS/advanced/ai-threat-detection/backend/urllib3-2.7.0
chore(deps): bump urllib3 from 2.6.3 to 2.7.0 in /PROJECTS/advanced/ai-threat-detection/backend
2026-05-13 02:40:02 -04:00
Carter Perez ae8d15ab95
Merge pull request #232 from CarterPerez-dev/dependabot/uv/PROJECTS/beginner/keylogger/urllib3-2.7.0
chore(deps): bump urllib3 from 2.6.3 to 2.7.0 in /PROJECTS/beginner/keylogger
2026-05-13 02:39:54 -04:00
Carter Perez 4b8875016b
Merge pull request #217 from CarterPerez-dev/dependabot/uv/PROJECTS/advanced/ai-threat-detection/backend/mako-1.3.12
chore(deps): bump mako from 1.3.11 to 1.3.12 in /PROJECTS/advanced/ai-threat-detection/backend
2026-05-13 02:39:47 -04:00
dependabot[bot] 413e9fd40e
chore(deps): bump urllib3 in /PROJECTS/beginner/keylogger
Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.6.3 to 2.7.0.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/2.6.3...2.7.0)

---
updated-dependencies:
- dependency-name: urllib3
  dependency-version: 2.7.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-12 17:31:33 +00:00
CarterPerez-dev a46fa4465e fix(canary-phase3): address audit findings before rollup
Phase 3 audits returned one BLOCKER and one SPEC-VIOLATION plus two
should-fix items; all cleared in-phase per the no-rot rule.

BLOCKER — template.html double-escaped JS-context interpolations.
Go's html/template auto-escapes {{.X}} in <script> string-literal
position via its built-in jsstrescaper. Piping through the explicit
`| js` (JSEscaper) re-ran the escaper, producing `\\u003D` /
`\\u0026` on the wire (double backslash). JS parses `\\u003D` as a
literal backslash followed by "u003D" text rather than `=`, so any
realistic destination URL — e.g.
https://news.example.com/article?utm_source=newsletter&utm_medium=email
— was corrupted into
https://news.example.com/article?utm_source=newsletter&utm_medium=email
before being passed to window.location.replace. The unit tests
previously missed it because every test destination was an
escape-free string. Fix: drop `| js` from both JS-context actions
in template.html; the contextual auto-escaper alone produces single-
backslash `=` which JS correctly decodes. Spec §9.2 line 1031
and 1038 (and the explanatory paragraph 1045) are factually wrong
about `| js` being a custom template func — it is `JSEscaper`, and
adding it on top of the built-in contextual JS string escaper is
pure double-escape harm. Spec docs (local-only, not committed)
amended accordingly. Added
TestTrigger_DestinationRoundtripsThroughJSStringDecode as a
regression guard against re-introducing the pipeline.

SPEC-VIOLATION — nil-token Trigger returned `404 + "Not Found"`,
breaking spec §8.5 ("token not found | 200, ... deliberately do NOT
404") and §8.5 line 964 ("/c/* endpoints never return JSON errors").
A 404 on a non-existent token ID lets scanners enumerate which IDs
are real. Fix: nil-token branch now renders the same template with a
benign decoy destination ("/") and returns 200 + text/html with the
full CSP override and cache headers. Response is byte-shape
indistinguishable from a valid-token response except for the
destination URL embedded in the script. Added
TestTrigger_TokenNotFound_HasSameResponseShapeAsValidToken to
assert the indistinguishability invariant. The `(nil event, &resp,
nil error)` return shape from the handoff anti-relitigation is
preserved.

SHOULD-FIX S1 — extractDestination now allowlists `http://` and
`https://` (case-insensitive) and returns the new
ErrInvalidDestinationScheme sentinel otherwise. The noscript
meta-refresh sits in an HTML-attribute context where html/template
does NOT recognize `url=...` as a URL context, so `javascript:`,
`data:`, `file:`, `vbscript:` and scheme-relative `//example.com`
would otherwise render verbatim and follow on JS-disabled clients.
Phase 9's `validate:"url"` on input is necessary but not sufficient
(the validator's `url` tag accepts any scheme). Added
TestGenerate_RejectsDangerousDestinationSchemes (8 cases) and
TestGenerate_AcceptsHTTPAndHTTPSSchemes (4 case-variants).

SHOULD-FIX S3 — fingerprint handler now silently 204s on any
Content-Type that does not start with "application/json". The
embedded template is the only legitimate caller and always sends
JSON; this rejects adversarial probes before the MaxBytesReader
read. Added
TestFingerprintHandler_WrongContentType_Returns204AndDoesNotTouchEvent.

SHOULD-FIX S4 — added integration tests for the oversize-body cap
(128 KiB body returns 204, Extra untouched) and the empty-token-id
guard (`/c//fingerprint`); the latter accepts chi's natural routing
behavior alongside the in-handler 204.

Non-functional cleanups: extracted shared template-render path
into renderWith / renderResponse / renderDecoyResponse to keep the
nil-token + happy-path bodies definitionally identical;
ErrMissingDestination and ErrInvalidDestinationScheme exported so
callers (and tests) can ErrorIs them; TestGenerate_RejectsMissing*
tightened to use require.ErrorIs(tc.wantErr) instead of bare
require.Error.

Pre-rollup gate (go build / vet / test -race unit+integration /
golangci-lint) is clean.
2026-05-12 06:46:45 -04:00
CarterPerez-dev be13d2f1c4 feat(canary): register slowredirect generator in registry
Phase 3 wiring: registry.Build now maps token.TypeSlowRedirect to
slowredirect.New() alongside the existing webbug entry. The pending-
types regression guard sheds SlowRedirect from its list (5 remain:
docx, pdf, kubeconfig, envfile, mysql) and the cardinality assertion
moves from "exactly one generator" to "exactly two".
2026-05-12 06:34:12 -04:00
CarterPerez-dev 6dd520f966 feat(canary): slowredirect fingerprint endpoint
POST /c/{id}/fingerprint handler that decodes a JSON fingerprint body
and merges it into the most recent event's Extra JSONB for the same
(token_id, source_ip) tuple within a 30s window via the existing
event.Repository.AttachFingerprint method. Decoupled from the concrete
repository through a small FingerprintAttacher interface so future
event.Service can swap in without touching this package.

Per spec §9.2 the fingerprint POST is enrichment-only: a missing match
(event.ErrNotFound), an invalid JSON body, an empty body, and a body
exceeding 64 KiB all silently return 204. Only unexpected repository
errors are logged via slog. The token id is read from chi.URLParam("id")
so the route mounts naturally under the existing trigger router in
Phase 9.

Three integration tests against testcontainers cover the happy-path
JSONB merge, the no-matching-event path (silent 204), and the
invalid-JSON path (204 plus stored Extra left untouched).
2026-05-12 06:34:04 -04:00
CarterPerez-dev 71823f5de2 feat(canary): slowredirect generator (Generate + Trigger + template)
HTML+JS browser-fingerprint redirect page rendered via html/template:
Generate reads metadata.destination_url off the token row and returns
KindURL with the trigger URL + persisted destination; Trigger renders
the embedded template (noscript meta-refresh + inline fetch posting
the fingerprint, then window.location.replace), returns the page with
Content-Security-Policy override allowing inline script + connect-src
'self', and emits the event capturing token id / source IP / UA /
Referer.

Nil-token Trigger returns nil event (FK guard) plus a 404 HTML body,
matching the established defensive-rendering pattern from webbug.
Destination_url is extracted defensively (missing/empty/whitespace
all surface ErrMissingDestination). realIP precedence copied from
webbug pending the Phase 9 middleware promotion.

19 unit-test assertions cover Type, Generate's URL+destination output,
five missing-destination edge cases, XSS neutralization in two
injection contexts (attribute escape + script-closing escape), CSP
override + cache headers, event-recording metadata, nil-token contract,
and per-call response independence.
2026-05-12 06:33:52 -04:00
CarterPerez-dev 1a240952c7 fix(canary-phase2): address audit findings before rollup
Two audit agents (code-reviewer + spec-adherence) both returned PASS but
flagged substantive findings. Per fix-in-phase / no-backlog-rot, clearing
every MEDIUM + LOW in-phase.

Findings addressed:

MEDIUM — realIP RemoteAddr fallback returned "IP:port" verbatim, where
the spec wants the bare IP (geoip + downstream parsing assume host-only).
Now uses net.SplitHostPort with raw-string fallback if not host:port.
Adds 4 RemoteAddr cases: IPv4 strips port, IPv6 bracket form strips
brackets+port, loopback IPv6, and the port-less raw fallback.

MEDIUM — realIP XFF branch accepted whatever rightmost-comma-split
produced, so headers like "198.51.100.1, " (trailing comma) yielded
"" and skipped XRI/RemoteAddr entirely. Extracted into lastNonEmptyXFF
which walks right-to-left and falls through cleanly. Adds two cases:
trailing-comma falls through, all-empty entries fall through.

MEDIUM — pixel.TransparentGIF was an exported mutable []byte; any caller
could clobber it process-wide (mutating one response's body would affect
every subsequent webbug trigger). Renamed to unexported transparentGIF
+ exposed pixel.Clone() and pixel.Len(). Webbug now calls pixel.Clone()
per trigger; new test TestTrigger_ResponseBodyIsIndependentCopyPerCall
verifies two triggers return independent slices.

LOW (reviewer-escalated to HIGH-for-Phase-3) — Trigger on nil token
returned a non-nil event with empty TokenID. Since events.token_id is a
NOT NULL FK to tokens.id, the Phase 3 handler could not have persisted
that event anyway, and the contract was implicit (would have required
an inline comment, which the no-comments rule forbids). Now: nil token
in → nil event, non-nil response out. Contract is explicit in the
return shape. Test asserts evt is nil for nil-token path.

LOW — no IPv6 coverage in IP-precedence tests. Added IPv6 cases for
XFF rightmost and three RemoteAddr forms (bracketed, loopback, no-port).

NIT — gracefulShutdown swallowed every shutdown error, returning nil
unconditionally. Now collects with errors.Join so callers can detect
partial-shutdown for telemetry/exit-code purposes.

Audits accepted as-is (NIT, not blocking):
- Artifact discriminated-union shape (Phase 3+ concern when other Kinds
  are produced)
- Registry returning bare map (read-only post-Build; concurrent reads
  are safe by Go's memory model)
- Build(_ Config) ignoring its arg (signature reserved for future
  stateful generators)

Quality verified post-fix: build/vet/lint clean (0 issues),
14 webbug tests + 6 pixel tests + 4 registry tests PASS under -race,
integration tests unchanged.
2026-05-12 02:56:29 -04:00
CarterPerez-dev c39b56b9af fix(canary): clear pre-audit lint debt + migrate golangci config to v2
Pre-audit gate (`golangci-lint run`) at the start of Phase 2 surfaced 15
issues that should have been zeroed before Phase 1 rollup. Per the
fix-in-phase rule (no backlog rot), clearing everything before Phase 2
audit agents run on a green tree.

Config:
- .golangci.yml: migrate `issues.exclude-rules` and `issues.exclude-dirs`
  to v2 syntax (`linters.exclusions.{rules,paths}`); test-file funlen/
  dupl/goconst exclusion now actually applies under golangci-lint v2.10
- .golangci.yml: add G706 to gosec excludes — false-positive log-injection
  reports on slog structured-logging call sites (slog separates message
  from kv args, immune to log-line injection by construction)

errcheck (5):
- cmd/canary/main.go: `_ = telemetry.Shutdown(...)` → log on error
- internal/token/repository.go: `defer stmt.Close()` → log on close error
- internal/event/repository.go: same as token
- internal/testutil/postgres.go: `_ = pgContainer.Terminate(...)` and
  `_ = db.Close()` → t.Logf on cleanup error (use distinct err names to
  avoid govet shadow)

funlen (1):
- cmd/canary/main.go: split `run` into `run` + `initTelemetry` +
  `mountRouter` + `gracefulShutdown` (was 55 statements, now under
  the 50 cap; helpers are individually well under)

govet shadow (1):
- cmd/canary/main.go: migrations check switched from `if err :=` to
  `if err =` (reuses outer err whose value was already consumed) — the
  inner short-decl was lexically shadowing without intent
- cmd/canary/main.go: select-case `err` renamed to `startErr` to avoid
  the same shadow pattern

golines (6, all auto-fixed by `golangci-lint run --fix`):
- main.go, config.go, telemetry.go, event/repository.go, token/dto.go,
  token/repository.go — long lines wrapped to 80-col

Verified post-fix: build OK, vet OK, unit tests PASS under -race,
integration tests PASS under -tags=integration -race (12s testcontainers),
golangci-lint reports 0 issues.
2026-05-12 02:49:56 -04:00
CarterPerez-dev bb95f74579 feat(canary): generator registry (token.Type → Generator)
Lives in its own subpackage to break the otherwise-cyclic import
(generators interface → webbug impl → generators interface): the registry
must import concrete implementations, which themselves import the
generators package for Artifact/TriggerResponse types.

- registry.Config { BaseURL string } (reserved for future stateful generators)
- registry.Registry = map[token.Type]generators.Generator
- registry.Build registers webbug only; subsequent phases append
  slowredirect, docx, pdf, kubeconfig, envfile, mysql

Tests assert webbug present, unknown type returns nil, pending types
are not yet claimed, Phase 2 cardinality is exactly 1.
2026-05-12 02:43:53 -04:00
CarterPerez-dev 9c0ad74973 feat(canary): webbug generator (Generate + Trigger)
- Generate returns KindURL artifact: baseURL/c/{id} (trailing slash trimmed)
- Trigger builds event from CF-Connecting-IP|XFF(rightmost)|XRI|RemoteAddr precedence,
  captures UA + Referer (nil when absent), returns 43-byte GIF response with
  no-store cache headers
- Nil-token path still returns GIF (spec §8.5 defense-in-depth)
- Tests: artifact shape, request metadata capture, IP precedence chain (5 cases),
  nil-pointer mapping for absent headers, GIF response shape + cache headers,
  missing-token path

realIP() lives here temporarily; promotes to middleware in Phase 9.
2026-05-12 02:42:17 -04:00
CarterPerez-dev 0b6e586eb1 feat(canary): shared 43-byte transparent GIF for image triggers
Used by webbug, docx, pdf, envfile triggers. Single source of truth.
Test asserts length, magic bytes, GIF89a trailer, and decodes via image/gif.
2026-05-12 02:40:24 -04:00
CarterPerez-dev 6cc724c19f feat(canary): Generator interface for token-type plugins
- ArtifactKind enum: url, file, text, connection_string
- Artifact struct discriminated by Kind
- TriggerResponse value-typed (handler writes; generator stays pure)
- Generator interface: Type, Generate, Trigger
2026-05-12 02:40:00 -04:00
dependabot[bot] 606b8d93d8
chore(deps): bump urllib3
Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.6.3 to 2.7.0.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/2.6.3...2.7.0)

---
updated-dependencies:
- dependency-name: urllib3
  dependency-version: 2.7.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-11 16:59:10 +00:00
dependabot[bot] cd1dac6240
chore(deps): bump urllib3 in /PROJECTS/beginner/c2-beacon/backend
Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.6.3 to 2.7.0.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/2.6.3...2.7.0)

---
updated-dependencies:
- dependency-name: urllib3
  dependency-version: 2.7.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-11 16:55:52 +00:00
dependabot[bot] 0a33fa9735
chore(deps): bump urllib3 in /PROJECTS/advanced/api-rate-limiter
Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.6.3 to 2.7.0.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/2.6.3...2.7.0)

---
updated-dependencies:
- dependency-name: urllib3
  dependency-version: 2.7.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-11 16:55:49 +00:00
dependabot[bot] 1ddc8acf39
chore(deps): bump urllib3
Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.6.2 to 2.7.0.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/2.6.2...2.7.0)

---
updated-dependencies:
- dependency-name: urllib3
  dependency-version: 2.7.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-11 16:30:23 +00:00
dependabot[bot] 5ba40b27dc
chore(deps): bump github.com/go-git/go-git/v5
Bumps [github.com/go-git/go-git/v5](https://github.com/go-git/go-git) from 5.18.0 to 5.19.0.
- [Release notes](https://github.com/go-git/go-git/releases)
- [Changelog](https://github.com/go-git/go-git/blob/main/HISTORY.md)
- [Commits](https://github.com/go-git/go-git/compare/v5.18.0...v5.19.0)

---
updated-dependencies:
- dependency-name: github.com/go-git/go-git/v5
  dependency-version: 5.19.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-11 16:03:40 +00:00
dependabot[bot] 373c7ba487
chore(deps): bump urllib3
Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.6.2 to 2.7.0.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/2.6.2...2.7.0)

---
updated-dependencies:
- dependency-name: urllib3
  dependency-version: 2.7.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-11 16:00:17 +00:00
CarterPerez-dev 697f4909d7 fix(canary-phase1): clear all post-phase-1 audit observations + header normalization
No 'logged for later' — every non-blocking finding from the Phase 1 audits
is fixed now, in this commit, before declaring the phase truly closed.

Code cleanups (5 items):
1. Database.SQLDB() *sql.DB accessor on core.Database; main.go uses
   db.SQLDB() instead of the awkward db.DB.DB triple-chain.
2. Hoisted list-default literals (50, 20) to package-level
   defaultListLimit consts in token + event repositories. MEMORY.md
   'no magic numbers' rule honored.
3. Moved ptr[T any] helper to internal/testutil/ptr.go as testutil.Ptr;
   removed local copies from token + event repository_test.go.
4. Renamed CHECK constraints in 0001_create_tokens.sql to match spec
   text exactly: chk_token_type → chk_type, chk_alert_channel → chk_channel.
   Spec was the original contract; impl now aligns.
5. Resolved APP_ENVIRONMENT vs ENVIRONMENT divergence per spec §12.1:
   - compose.yml: ENVIRONMENT=production → APP_ENVIRONMENT=production
   - dev.compose.yml: ENVIRONMENT=development → APP_ENVIRONMENT=development
   - config.go envKeyMap: ENVIRONMENT → APP_ENVIRONMENT mapping

Concurrency bug fix (real, not just polish):
- core/migrations.go: added sync.Mutex around goose calls. goose's
  package-level state (SetBaseFS, SetDialect) raced under t.Parallel()
  testcontainers tests. Race detector caught it under -race after
  parallel test counts climbed. Mutex serializes goose invocation
  globally; testcontainer-parallelism otherwise unaffected.

Header normalization (50 files):
- Bulk-normalized every project file's header to canonical
  '©AngelaMos | 2026' (no space, with © glyph, year 2026) per MEMORY.md
  style rule. Eliminated 4 distinct non-canonical variants: '// AngelaMos',
  '// ©AngelaMos | 2025', '// © AngelaMos | 202X', '# AngelaMos'.
- Verified clean: grep returns zero non-canonical headers across the
  whole project tree (excluding gitignored docs/ and node_modules/).

Backlog discipline:
- Created docs/plans/BACKLOG.md with strict format: open items only,
  HIGH/MEDIUM/LOW severity, must-clear-before-ship contract.
- BACKLOG currently has zero open items — every observation was fixed
  here, not deferred. Closed-items section logs what was cleared.

Verification:
- go build ./...                                     clean
- go vet ./...                                       clean
- go test -tags=integration -race ./internal/token/...  11/11 PASS
- go test -tags=integration -race ./internal/event/...   9/9 PASS
- docker compose -f compose.yml config              parses
- docker compose -f dev.compose.yml config          parses
- grep for non-canonical headers                     zero matches
2026-05-10 06:15:26 -04:00
CarterPerez-dev b2558e2e41 feat(canary): Phase 1 — Postgres schema + token & event repositories
Schema (3 goose migrations under internal/core/migrations/):
- 0001_create_tokens.sql: tokens table per spec §7.1
  Columns: id (varchar 12, PK), manage_id (UUID unique), type, memo,
           filename, alert_channel, telegram_bot/chat, webhook_url,
           created_at/ip/fp, enabled, trigger_count, last_triggered, metadata (jsonb)
  CHECK constraints: chk_token_type (7-token enum), chk_alert_channel (telegram|webhook),
                     chk_telegram_complete (when channel=telegram, bot+chat required),
                     chk_webhook_complete (when channel=webhook, url required)

- 0002_create_events.sql: events table
  Columns: id (bigserial PK), token_id (FK with ON DELETE CASCADE),
           triggered_at, source_ip (inet), user_agent, referer,
           geo_country/region/city/asn/asn_org, extra (jsonb),
           notify_status, notified_at
  CHECK chk_notify_status (pending|sent|failed|deduped)

- 0003_indexes.sql: full index set per spec §7.1
  tokens(created_ip), tokens(created_fp), tokens(created_at DESC),
  tokens(type), partial idx_tokens_trigger_count WHERE trigger_count > 0,
  events(token_id, triggered_at DESC), events(source_ip),
  partial idx_events_notify_pending WHERE notify_status = 'pending'

Migration runner (internal/core/migrations.go):
- go:embed migrations/*.sql baked into binary
- core.RunMigrations(*sql.DB) called from main.go after DB connect
- pressly/goose v3.27.1 (research-recommended; no dirty-state bug)

Token domain (internal/token/):
- entity.go: Token struct + typed Type/AlertChannel enums with Valid() guards
- dto.go: CreateRequest with validator/v10 tags (oneof, required_if, url, max),
          Response shape with ToResponse(triggerURL, manageURL) helper
- repository.go: Insert (RETURNING created_at + counters), GetByID,
                 GetByManageID, DeleteByManageID (FK cascade), IncrementTriggerCount,
                 SetEnabled, ListAll + CountAll. ErrNotFound on sql.ErrNoRows.
                 Named-parameter binding via sqlx.NamedExecContext.
- repository_test.go (//go:build integration): 11 tests covering insert,
  get-by-id, get-by-manage-id, not-found paths, delete with cascade,
  trigger count increment, enable toggle, list pagination, type CHECK

Event domain (internal/event/):
- entity.go: Event struct + NotifyStatus enum
- dto.go: GeoView + Response with ToResponse() flattens geo into nested object
- repository.go: Insert, GetByID, ListByToken (cursor pagination via
                 LIMIT N+1, returns NextCursor + HasMore), CountByToken,
                 AttachFingerprint (UPDATE most-recent within window using
                 jsonb || merge), UpdateNotifyStatus, PruneToLimit
                 (window function row_number() PARTITION BY token_id)
- repository_test.go (//go:build integration): 9 tests covering insert,
  cursor-paginated listing, FK cascade from token deletion, fingerprint
  merge into existing extra jsonb, notify status update, prune-to-N
  preserves newest-first, prune rejects zero limit

Test infrastructure:
- internal/testutil/postgres.go: testcontainers-go helper that spins up
  postgres:18-alpine, applies migrations via core.RunMigrations, returns
  ready *sql.DB with t.Cleanup-registered teardown. ~5-7s per test;
  20 integration tests run in ~13s end-to-end.

Wired into main.go:
- core.RunMigrations(db.DB.DB) called after NewDatabase
- token + event repos instantiated; blank-assigned for now (services
  consume them in Phase 9/10)

Verification:
- go build ./...                                      clean
- go vet ./...                                        clean
- go test -tags=integration ./internal/token/...      11 of 11 PASS
- go test -tags=integration ./internal/event/...      9 of 9 PASS
2026-05-10 05:47:24 -04:00
CarterPerez-dev 98c006897d fix(canary-phase0): address audit findings before phase rollup
Phase 0 audit (superpowers:code-reviewer + general-purpose) returned FAIL
on shared findings. This commit resolves the blockers + important items.

Blockers:
- infra/docker/vite.prod: replace 'react-scss/' COPY paths with 'frontend/'
  (would have broken 'docker compose build nginx')
- Delete backend/Dockerfile (broken template debris referencing deleted
  cmd/api, missing migrations/, deleted keys/)
- Delete backend/Justfile (docker compose recipes pointed at deleted
  backend/compose.yml + backend/dev.compose.yml; project-root justfile
  [backend] group covers everything operationally)
- backend/internal/admin/handler.go: strip the residual JWT/auth scaffolding
  (AuthService interface, authSvc field, authenticator+adminOnly mw params).
  Phase 12 will gate /admin under OperatorBearer; for now RegisterRoutes
  takes only chi.Router.
- Delete backend/.env + backend/.env.example: stale template duplicates
  containing only template keys; project-root .env.example is the source
  of truth per spec §12.4.

Important:
- Add © glyph to file headers in cloudflared.compose.yml, infra/docker/
  vite.{dev,prod}, backend/.gitignore (operator's standing rule:
  '©AngelaMos | 2026', not 'AngelaMos | 2026')
- backend/.gitignore: drop stale 'keys/*.pem' / 'keys/*.key' entries
  (backend/keys/ no longer exists)
- scripts/init.sh: rename 'local_tmp' variable to 'tmp_dir' to remove
  visual ambiguity with the 'local' bash keyword (both audit agents
  briefly misread it as 'local local_tmp=...')
- .env.example: add inline comment explaining the dual NGINX_HOST_PORT
  defaults (22784 prod compose, 58495 dev compose; do not override in
  shared .env)

Deferred (logged but not blocking Phase 0):
- Pre-existing template Go files (internal/{config,core,server,health,
  middleware/{request_id,logging,headers,ratelimit},admin}/*.go) carry
  '// AngelaMos | 2026' without ©. Bulk-fixing these belongs to a
  hygiene pass; not Phase 0 scope.
- frontend/vite.config.ts and frontend/stylelint.config.js reference
  '2025'; operator-imported template files, separate concern.
- APP_ENVIRONMENT vs ENVIRONMENT spec/impl mismatch: spec §12.1 sample
  uses APP_ENVIRONMENT; envKeyMap in config.go uses ENVIRONMENT (template
  default). Implementation is internally consistent. Logged in plan
  Appendix C as a non-blocking spec amendment candidate.

Verification: go build ./... + go vet ./... clean. grep returns no
hits for react-scss / carterperez-dev/templates / JWTConfig /
InvalidateAllSessions across backend/, infra/, scripts/, compose files.
2026-05-10 05:37:32 -04:00
CarterPerez-dev a4811d1c04 feat(canary): import operator's React + Vite frontend template
Frontend stack (pre-rebranded by operator before Phase 0):
- React 19 + react-router-dom v7
- TypeScript 5.9 with project references
- Vite 7 (rolldown-vite — Rust port)
- TanStack Query v5 + axios for data
- Zustand v5 for client state
- Zod v4 for runtime validation
- SCSS (Sass), not Tailwind — operator stylistic choice
- Biome 2 (lint + format), Stylelint for SCSS
- react-error-boundary, sonner (toasts)
- react-icons

Source layout (operator-authored skeleton):
- src/App.tsx, main.tsx, config.ts, styles.scss
- src/api/{hooks,types,index.ts}        TanStack Query hooks
- src/components/                        UI primitives (operator-designed later)
- src/core/{api,app,lib}                 axios client, routers, utilities
- src/pages/                             page-level containers
- src/styles/                            SCSS partials

Per implementation plan §16.3, Phase 14 stops at API client only —
operator owns visual/component design from there.

Pre-commit hooks fixed:
- site.webmanifest: missing trailing newline
- index.html: trailing whitespace stripped
- .stylelintignore + stylelint.config.js: chmod -x (no shebang, not executable)

node_modules/ is gitignored.
2026-05-10 05:28:09 -04:00
CarterPerez-dev a2d6f09ab3 feat(canary): add project-root .env.example + idempotent init.sh
.env.example covers every env var the canary stack reads:
- Public: APP_NAME, NGINX_HOST_PORT, PUBLIC_BASE_URL, VITE_APP_TITLE, VITE_API_URL
- Anti-bot: TURNSTILE_SITE_KEY/SECRET + VITE_TURNSTILE_SITE_KEY (frontend mirror)
- Operator: OPERATOR_TOKEN (admin endpoints)
- DB: POSTGRES_PASSWORD + POSTGRES_DEV_PORT
- Cache: REDIS_DEV_PORT
- GeoIP: MAXMIND_ACCOUNT_ID/LICENSE_KEY (optional)
- Webhooks: WEBHOOK_HMAC_SECRET (optional)
- Fake MySQL: MYSQL_FAKE_ENABLED + MYSQL_HOST_PORT (optional, off by default)
- Logging: LOG_LEVEL, LOG_FORMAT
- Telemetry: OTEL_ENABLED, OTEL_EXPORTER_OTLP_ENDPOINT + Jaeger ports
- Tunnel: CLOUDFLARE_TUNNEL_TOKEN (for cloudflared.compose.yml)

scripts/init.sh rewritten as idempotent setup helper:
- Copies .env.example -> .env on first run
- Generates POSTGRES_PASSWORD + OPERATOR_TOKEN via openssl rand -hex 32 if blank
- Skips already-set values (idempotent)
- Conditionally fetches GeoLite2-City.mmdb when MAXMIND creds present
- All sed usage is operator-side (script run-time), not Claude tool-time

scripts/randomize-ports.sh kept as-is (operator helper for spinning up
sibling projects on non-conflicting ports).
2026-05-10 05:27:30 -04:00
CarterPerez-dev 369c954892 feat(canary): unified Justfile with frontend + backend + compose groups
Replaces template Justfile (which referenced ./react-scss/). Groups:

[frontend]   pnpm install/dev/build/preview, biome, stylelint, tsc
             Targets at fe-install/fe-dev/fe-build/fe-preview prefixed for clarity
[backend]    go mod tidy, vet, golangci-lint, test (unit + integration tags),
             coverage HTML, run, static build, air hot reload
[lint]       lint = be-lint + biome + stylelint + tsc;  ci = lint + test
[compose]    docker compose lifecycle for prod (up/start/down/stop/build/logs/ps)
[tunnel]     overlay with cloudflared.compose.yml for prod+tunnel
[dev]        docker compose lifecycle for dev.compose.yml
[util]       init (gen secrets + fetch GeoLite2), ports, info, clean

Justfile parses cleanly via 'just --list'. Frontend dir reference fixed
(now ./frontend, not ./react-scss).
2026-05-10 05:25:27 -04:00
CarterPerez-dev a5c8d17134 feat(canary): add canary.dev + canary.prod Dockerfiles + import infra/
Production Dockerfile (infra/docker/canary.prod):
- Multi-stage: golang:1.25-alpine builder → distroless/static:nonroot final
- CGO_ENABLED=0, -trimpath, -ldflags='-s -w' → minimal static binary
- Embeds config.yaml; runs as nonroot user
- ENTRYPOINT /canary, CMD -config /config.yaml
- EXPOSE 8080

Development Dockerfile (infra/docker/canary.dev):
- golang:1.25-alpine + air-verse/air for hot reload
- Source bind-mounted by dev.compose.yml; .air.toml drives rebuilds
- go mod download cached at image-build time, dep refresh at runtime if changed
- EXPOSE 8080

Also imports the rest of the template's infra/ that was untracked:
- infra/docker/vite.dev + vite.prod (existing, unchanged)
- infra/nginx/{nginx.conf, dev.nginx, prod.nginx, nginx.prod.conf}
  These will be extended in Phase 15 with /api, /c, /k upstream blocks.
2026-05-10 05:24:42 -04:00
CarterPerez-dev 8b70bfbba9 chore(canary): merge backend compose into project root
Single project-root compose.yml + dev.compose.yml manage all services.

Production stack (compose.yml):
- nginx          public ingress on \${NGINX_HOST_PORT:-22784}:80
- canary         Go backend, NO host port (reachable only via nginx)
                 distroless/static:nonroot Dockerfile
                 healthcheck → /healthz, depends on postgres + redis
- postgres       postgres:18-alpine, no host port, named volume pgdata
- redis          redis:7-alpine, no host port, named volume redisdata
- (geolite vol)  read-only mount /data for GeoLite2-City.mmdb

Dev stack (dev.compose.yml):
- nginx          dev ingress on \${NGINX_HOST_PORT:-58495}:80
- frontend       Vite HMR on \${FRONTEND_HOST_PORT:-15723}:5173
- canary         Air hot-reload (canary.dev Dockerfile, bind-mounts ./backend)
                 OTel exports to jaeger:4317
- postgres       host port \${POSTGRES_DEV_PORT:-5447}:5432 for psql access
- redis          host port \${REDIS_DEV_PORT:-6022}:6379 for redis-cli access
- jaeger         UI 16686, OTLP gRPC 4317, OTLP HTTP 4318
- volumes        gocache + gomodcache speed up rebuilds

All randomized host ports preserved exactly:
  22784 (prod nginx), 58495 (dev nginx), 15723 (vite), 5447 (pg dev),
  6022 (redis dev), 16686/4317/4318 (jaeger).

Backend has no host port in production: nginx is the single entrypoint.
Cloudflare Tunnel sidecar terminates at nginx:80 via cloudflared.compose.yml.

backend/compose.yml + backend/dev.compose.yml deleted (merged here).

Validation:
- docker compose -f compose.yml config           — OK
- docker compose -f dev.compose.yml config       — OK
- docker compose -f compose.yml -f cloudflared.compose.yml config — OK
2026-05-10 05:23:55 -04:00
CarterPerez-dev 7fa2861e7a feat(canary): backend bootstrap — strip JWT/users, rename module, rewrite main
Removes the template's JWT auth + user domain (Phase 0 §0.4):
- Deleted backend/internal/auth/ (entire JWT auth domain)
- Deleted backend/internal/user/ (user CRUD domain)
- Deleted backend/keys/ (JWT signing keys directory)
- Deleted backend/internal/middleware/auth.go (Authenticator + RequireAdmin)
- Deleted Justfile generate-keys recipe (no JWT keys to generate)
- Removed lestrrat-go/jwx/v3 + transitive deps via go mod tidy
- Stripped JWTConfig type, defaults, env mappings, validators from config.go
- Removed jwt: section from config.yaml
- Removed JWT_* lines from backend/.env and .env.example

Renames Go module to project-local path (Phase 0 §0.5):
  github.com/carterperez-dev/templates/go-backend
  → github.com/CarterPerez-dev/cybersecurity-projects/canary-token-generator/backend
- Rewrote imports in 6 remaining .go files using Edit tool (NEVER sed per repo rule)
- Updated .golangci.yml local-prefixes + gci section ordering

Renames cmd/api → cmd/canary and rewrites main.go (Phase 0 §0.6):
- mv cmd/api cmd/canary
- Rewrote cmd/canary/main.go as canary bootstrap (config, telemetry, db,
  redis, middleware chain, health, /api stub) — no auth wiring
- Updated .air.toml cmd path
- Updated backend/Justfile run/build targets to cmd/canary + bin/canary
- Renamed docker-build image tag to canary-token-generator:latest

Rebrands defaults:
- config.go: app.name "Go Backend" → "Canary Token Generator"
- config.go: otel.service_name "go-backend" → "canary-token-generator"
- config.yaml: app.name "Go Backend Template" → "Canary Token Generator"
- backend/.env(.example): OTEL_SERVICE_NAME → canary-token-generator

Drops user-aware rate-limit helpers from middleware/ratelimit.go:
- Removed KeyByUser, KeyByUserAndEndpoint, normalizeEndpoint, isUUID, isNumeric
- Removed TierConfig, DefaultTiers, TieredRateLimiter (referenced GetUserID)
- KeyByIP, NewRateLimiter, PerMinute/PerSecond/PerHour preserved

Verification:
- go build ./...    — clean
- go vet ./...      — clean
- go mod tidy       — silent (deps consolidated)
- grep -r "carterperez-dev/templates|JWTConfig|JWT_PRIVATE|lestrrat" → empty

backend/compose.yml + backend/dev.compose.yml are still present here; they
get merged into project-root compose files in Task 0.7 (next commit).
2026-05-10 05:22:08 -04:00
CarterPerez-dev 382890cb56 chore(canary): rebrand from no-auth-template to canary-token-generator
- compose.yml: APP_NAME default → canary-token-generator
- dev.compose.yml: APP_NAME default → canary-token-generator
                   ./react-scss bind paths → ./frontend (template flatten)
- cloudflared.compose.yml: APP_NAME default → canary-token-generator
- VITE_APP_TITLE default in both compose files → "Canary Token Generator"

Frontend package.json and index.html were already rebranded by operator
between sessions. All three compose stacks now validate via docker compose
config (prod, dev, prod+tunnel overlay).
2026-05-10 05:15:09 -04:00
CarterPerez-dev 5ccabe00b5 chore(canary): expand project .gitignore for Go + Node + secrets
- Ignore docs/ (dev-only planning material)
- Ignore env files except .env.example
- Ignore backend Go build/test artifacts (bin, tmp, coverage)
- Ignore frontend Node + Vite caches and dist
- Ignore local data volumes (geoip mmdb, sqlite databases)
- Ignore OS/editor cruft and lint caches
2026-05-10 05:13:19 -04:00
dependabot[bot] ca3d45bf63
chore(deps): bump gitpython
Bumps [gitpython](https://github.com/gitpython-developers/GitPython) from 3.1.47 to 3.1.50.
- [Release notes](https://github.com/gitpython-developers/GitPython/releases)
- [Changelog](https://github.com/gitpython-developers/GitPython/blob/main/CHANGES)
- [Commits](https://github.com/gitpython-developers/GitPython/compare/3.1.47...3.1.50)

---
updated-dependencies:
- dependency-name: gitpython
  dependency-version: 3.1.50
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-09 03:15:11 +00:00
dependabot[bot] 5b01af8277
chore(deps): bump python-multipart
Bumps [python-multipart](https://github.com/Kludex/python-multipart) from 0.0.26 to 0.0.27.
- [Release notes](https://github.com/Kludex/python-multipart/releases)
- [Changelog](https://github.com/Kludex/python-multipart/blob/main/CHANGELOG.md)
- [Commits](https://github.com/Kludex/python-multipart/compare/0.0.26...0.0.27)

---
updated-dependencies:
- dependency-name: python-multipart
  dependency-version: 0.0.27
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-08 12:23:40 +00:00
dependabot[bot] 27cbd67d01
chore(deps): bump mako in /PROJECTS/advanced/bug-bounty-platform/backend
Bumps [mako](https://github.com/sqlalchemy/mako) from 1.3.11 to 1.3.12.
- [Release notes](https://github.com/sqlalchemy/mako/releases)
- [Changelog](https://github.com/sqlalchemy/mako/blob/main/CHANGES)
- [Commits](https://github.com/sqlalchemy/mako/commits)

---
updated-dependencies:
- dependency-name: mako
  dependency-version: 1.3.12
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-08 12:21:25 +00:00
dependabot[bot] 606034ee2c
chore(deps): bump python-multipart
Bumps [python-multipart](https://github.com/Kludex/python-multipart) from 0.0.26 to 0.0.27.
- [Release notes](https://github.com/Kludex/python-multipart/releases)
- [Changelog](https://github.com/Kludex/python-multipart/blob/main/CHANGELOG.md)
- [Commits](https://github.com/Kludex/python-multipart/compare/0.0.26...0.0.27)

---
updated-dependencies:
- dependency-name: python-multipart
  dependency-version: 0.0.27
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-08 12:15:41 +00:00
dependabot[bot] 3d6eee8627
chore(deps): bump python-multipart
Bumps [python-multipart](https://github.com/Kludex/python-multipart) from 0.0.26 to 0.0.27.
- [Release notes](https://github.com/Kludex/python-multipart/releases)
- [Changelog](https://github.com/Kludex/python-multipart/blob/main/CHANGELOG.md)
- [Commits](https://github.com/Kludex/python-multipart/compare/0.0.26...0.0.27)

---
updated-dependencies:
- dependency-name: python-multipart
  dependency-version: 0.0.27
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-08 12:00:07 +00:00
dependabot[bot] a3f438d786
chore(deps): bump mako in /PROJECTS/advanced/encrypted-p2p-chat/backend
Bumps [mako](https://github.com/sqlalchemy/mako) from 1.3.10 to 1.3.12.
- [Release notes](https://github.com/sqlalchemy/mako/releases)
- [Changelog](https://github.com/sqlalchemy/mako/blob/main/CHANGES)
- [Commits](https://github.com/sqlalchemy/mako/commits)

---
updated-dependencies:
- dependency-name: mako
  dependency-version: 1.3.12
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-08 11:57:37 +00:00
dependabot[bot] 8e8e2ab987
chore(deps): bump python-multipart
Bumps [python-multipart](https://github.com/Kludex/python-multipart) from 0.0.26 to 0.0.27.
- [Release notes](https://github.com/Kludex/python-multipart/releases)
- [Changelog](https://github.com/Kludex/python-multipart/blob/main/CHANGELOG.md)
- [Commits](https://github.com/Kludex/python-multipart/compare/0.0.26...0.0.27)

---
updated-dependencies:
- dependency-name: python-multipart
  dependency-version: 0.0.27
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-08 11:47:03 +00:00
Carter Perez d6c4fb7b4b
Merge pull request #211 from CarterPerez-dev/fix/lints
fix: resolve CI lint failures across frontend and Go projects
2026-05-08 06:45:00 -04:00
CarterPerez-dev 7d69339413 fix: extract osv package string literals to constants
goconst counts occurrences across the whole package (including test files)
when deciding whether to flag a non-test file — the exclusion only suppresses
reports FROM test files. Add unexported constants for severity levels, CVSS
types, ecosystem, and reference types in client.go, and update client_test.go
to use them so the total raw-string count per literal drops below threshold.
2026-05-08 06:30:08 -04:00
dependabot[bot] 063a1ca222
chore(deps): bump mako in /PROJECTS/advanced/ai-threat-detection/backend
Bumps [mako](https://github.com/sqlalchemy/mako) from 1.3.11 to 1.3.12.
- [Release notes](https://github.com/sqlalchemy/mako/releases)
- [Changelog](https://github.com/sqlalchemy/mako/blob/main/CHANGES)
- [Commits](https://github.com/sqlalchemy/mako/commits)

---
updated-dependencies:
- dependency-name: mako
  dependency-version: 1.3.12
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-08 10:29:08 +00:00
Carter Perez 5b3dfaf443
Merge pull request #214 from CarterPerez-dev/dependabot/npm_and_yarn/PROJECTS/advanced/ai-threat-detection/frontend/axios-1.15.2
chore(deps): bump axios from 1.15.0 to 1.15.2 in /PROJECTS/advanced/ai-threat-detection/frontend
2026-05-08 06:26:05 -04:00
Carter Perez 0044be7b51
Merge pull request #215 from CarterPerez-dev/dependabot/npm_and_yarn/PROJECTS/intermediate/api-security-scanner/frontend/axios-1.15.2
chore(deps): bump axios from 1.15.0 to 1.15.2 in /PROJECTS/intermediate/api-security-scanner/frontend
2026-05-08 06:25:57 -04:00
Carter Perez c51e7c20e8
Merge pull request #216 from CarterPerez-dev/dependabot/npm_and_yarn/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/axios-1.15.2
chore(deps): bump axios from 1.15.0 to 1.15.2 in /PROJECTS/advanced/monitor-the-situation-dashboard/frontend
2026-05-08 06:25:49 -04:00
CarterPerez-dev 25671b4f9d fix: resolve simple-vulnerability-scanner goconst failures
Migrate .golangci.yml from v1 issues.exclude-rules to v2
linters.exclusions.rules — the v1 key was silently ignored by golangci-lint
v2, so test-file goconst exclusions weren't applied. With the config fixed,
all test-file violations disappear. Extract severity constants in output.go
to fix the 4 remaining non-test goconst violations (CRITICAL/HIGH/MODERATE/
LOW each appear 3x in severityRank, severityBreakdown, and severityColorFn).
2026-05-08 06:21:26 -04:00
dependabot[bot] d5fc3a8818
chore(deps): bump axios
Bumps [axios](https://github.com/axios/axios) from 1.15.0 to 1.15.2.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.15.0...v1.15.2)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.15.2
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-08 10:08:36 +00:00
dependabot[bot] 39c6df50df
chore(deps): bump axios
Bumps [axios](https://github.com/axios/axios) from 1.15.0 to 1.15.2.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.15.0...v1.15.2)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.15.2
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-08 10:02:21 +00:00
dependabot[bot] 45d4bc9993
chore(deps): bump axios
Bumps [axios](https://github.com/axios/axios) from 1.15.0 to 1.15.2.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.15.0...v1.15.2)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.15.2
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-08 09:54:42 +00:00
dependabot[bot] 369d227380
chore(deps): bump python-multipart
Bumps [python-multipart](https://github.com/Kludex/python-multipart) from 0.0.26 to 0.0.27.
- [Release notes](https://github.com/Kludex/python-multipart/releases)
- [Changelog](https://github.com/Kludex/python-multipart/blob/main/CHANGELOG.md)
- [Commits](https://github.com/Kludex/python-multipart/compare/0.0.26...0.0.27)

---
updated-dependencies:
- dependency-name: python-multipart
  dependency-version: 0.0.27
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-08 09:54:40 +00:00
Carter Perez 89d6d671d2
Merge pull request #206 from CarterPerez-dev/dependabot/go_modules/PROJECTS/advanced/monitor-the-situation-dashboard/backend/github.com/jackc/pgx/v5-5.9.2
chore(deps): bump github.com/jackc/pgx/v5 from 5.7.2 to 5.9.2 in /PROJECTS/advanced/monitor-the-situation-dashboard/backend
2026-05-08 05:48:35 -04:00
Carter Perez e51ef4b857
Merge pull request #209 from CarterPerez-dev/dependabot/npm_and_yarn/PROJECTS/advanced/bug-bounty-platform/frontend/axios-1.15.2
chore(deps): bump axios from 1.15.0 to 1.15.2 in /PROJECTS/advanced/bug-bounty-platform/frontend
2026-05-08 05:48:28 -04:00
Carter Perez c205152638
Merge pull request #210 from CarterPerez-dev/dependabot/npm_and_yarn/PROJECTS/intermediate/binary-analysis-tool/frontend/axios-1.15.2
chore(deps): bump axios from 1.15.0 to 1.15.2 in /PROJECTS/intermediate/binary-analysis-tool/frontend
2026-05-08 05:48:21 -04:00
Carter Perez 12eef419dc
Merge pull request #212 from CarterPerez-dev/dependabot/npm_and_yarn/PROJECTS/intermediate/siem-dashboard/frontend/axios-1.15.2
chore(deps): bump axios from 1.15.0 to 1.15.2 in /PROJECTS/intermediate/siem-dashboard/frontend
2026-05-08 05:48:10 -04:00
Carter Perez a05c3f1467
Merge pull request #207 from CarterPerez-dev/dependabot/npm_and_yarn/PROJECTS/advanced/honeypot-network/frontend/axios-1.15.2
chore(deps): bump axios from 1.15.0 to 1.15.2 in /PROJECTS/advanced/honeypot-network/frontend
2026-05-08 05:47:09 -04:00
CarterPerez-dev 5863be7dac fix: pin pnpm to v10 in CI and add .npmrc to all frontend projects
pnpm latest on Node 22 resolves to pnpm 11 which rejects lockfileVersion
9.0 lockfiles with ERR_PNPM_LOCKFILE_CONFIG_MISMATCH. Pinning to pnpm 10
keeps compatibility with existing lockfiles. .npmrc sets
strict-dep-builds=false so build-script warnings don't hard-fail the
frozen install.
2026-05-08 05:46:38 -04:00
dependabot[bot] 651997aa3a
chore(deps): bump axios
Bumps [axios](https://github.com/axios/axios) from 1.15.0 to 1.15.2.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.15.0...v1.15.2)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.15.2
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-08 09:45:39 +00:00
CarterPerez-dev adeec9b36d fix: resolve CI lint failures across frontend and Go projects
Node.js bumped to 22 in lint workflow (pnpm 11.x requires >=22.13).
Extract goconst-flagged string literals in secrets-scanner to named
constants. Carry along monitor-dashboard docker/package fixes.
2026-05-08 05:40:04 -04:00
dependabot[bot] e51e0ad05d
chore(deps): bump axios
Bumps [axios](https://github.com/axios/axios) from 1.15.0 to 1.15.2.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.15.0...v1.15.2)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.15.2
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-08 09:05:26 +00:00
dependabot[bot] 75b0cac000
chore(deps): bump axios
Bumps [axios](https://github.com/axios/axios) from 1.15.0 to 1.15.2.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.15.0...v1.15.2)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.15.2
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-08 09:04:07 +00:00
Carter Perez ca8382482b
Merge pull request #208 from CarterPerez-dev/project/monitor-the-situation-dashboard
add learn folder to IM MONITORING THE SITUATION DASHBOARD project
2026-05-08 04:55:09 -04:00
dependabot[bot] fb7e00f90e
chore(deps): bump axios in /PROJECTS/advanced/honeypot-network/frontend
Bumps [axios](https://github.com/axios/axios) from 1.15.0 to 1.15.2.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.15.0...v1.15.2)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.15.2
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-08 08:51:09 +00:00
CarterPerez-dev 1b9fcbac13 add learn folder to IM MONITORING THE SITUATION DASHBOARD project 2026-05-08 04:43:29 -04:00
dependabot[bot] 06cb5f654c
chore(deps): bump github.com/jackc/pgx/v5
Bumps [github.com/jackc/pgx/v5](https://github.com/jackc/pgx) from 5.7.2 to 5.9.2.
- [Changelog](https://github.com/jackc/pgx/blob/master/CHANGELOG.md)
- [Commits](https://github.com/jackc/pgx/compare/v5.7.2...v5.9.2)

---
updated-dependencies:
- dependency-name: github.com/jackc/pgx/v5
  dependency-version: 5.9.2
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-04 23:53:39 +00:00
dependabot[bot] 509c79a3d1
chore(deps): bump axios
Bumps [axios](https://github.com/axios/axios) from 1.13.2 to 1.15.0.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.13.2...v1.15.0)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.15.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-04 22:59:57 +00:00
Carter Perez 1a04003ce1
Merge pull request #204 from CarterPerez-dev/project/monitor-the-situation-dashboard
Project/monitor the situation dashboard
2026-05-04 18:56:41 -04:00
CarterPerez-dev 5e7f2c36de I'm being gangstalked by the CIA, they put a chip in my brain 2026-05-04 18:53:54 -04:00
Carter Perez 480503dbfa
Update README.md 2026-05-04 03:48:28 -04:00
CarterPerez-dev 6e35f49d2a feat(monitor/backend): swpc emits rich space_weather payload (kp + bz_gsm + speed/density + xray flux/class) 2026-05-03 19:39:41 -04:00
CarterPerez-dev 6f062bf49b feat(monitor/frontend): earthquake + outage panels (left + right column, USGS / CF Radar) 2026-05-03 19:25:03 -04:00
CarterPerez-dev 887dc217d2 fix(monitor/frontend): drop --type-body 13px → 12px (more density for table data; KPI/hero numbers unchanged) 2026-05-03 19:18:03 -04:00
CarterPerez-dev 0695f5d9a4 fix(monitor/frontend): bgp panel CC column wider (HK was truncating to H...) 2026-05-03 19:15:26 -04:00
CarterPerez-dev cf38f88efd fix(monitor/frontend): supplementary centroids (HK et al) + bgp schema lenient (victimAsns nullable, dedup geo/threat types) 2026-05-03 19:11:12 -04:00
CarterPerez-dev 0f53e67236 feat(monitor/frontend): bgp hijack panel + globe plot via abuseipdb-enriched country 2026-05-03 19:06:11 -04:00
CarterPerez-dev c25fd57eaf feat(monitor/frontend): dashboard layout — CVE Velocity to left under ISS, BGP HIJACKS to right top 2026-05-03 19:05:45 -04:00
CarterPerez-dev 0705f37330 fix(monitor/frontend): dshield panel (topports record has date/limit siblings of port entries) 2026-05-03 18:54:34 -04:00
CarterPerez-dev 038de64605 fix(monitor/backend): gdelt switch to timelinevol mode (article-snippet source of malformed escapes is gone) 2026-05-03 18:42:27 -04:00
CarterPerez-dev e216331aeb feat(monitor/backend): abuseipdb enrichment for bgp hijacks (country + abuse_score in payload) 2026-05-03 18:20:15 -04:00
CarterPerez-dev e690b30e92 feat(monitor/frontend): country-centroid plotting for ransomware + internet_outage on globe 2026-05-03 18:11:15 -04:00
CarterPerez-dev 0a41102e75 fix(monitor/frontend): globe snapshot seed + pushPoint dedup-by-id (instant eq+iss dots, single moving iss) 2026-05-03 17:56:50 -04:00
CarterPerez-dev a6dcaed7dc feat(monitor/frontend): globe country outlines (thin --fg-4 stroke, local 110m geojson) 2026-05-03 17:49:55 -04:00
CarterPerez-dev aa9a24bdd0 feat(monitor/frontend): dashboard lifecycle hook (snapshot bootstrap → WS → typed routing)
useDashboardLifecycle() orchestrates the three pieces that make the
dashboard alive:

1. Audio gesture unlock — unlockOnFirstGesture() on mount so playChime()
   is ready when Phase 6 wires alert triggers.
2. Snapshot-then-WS — useSnapshot()'s isSuccess gates createDashboardWS
   so we never open the WS before initial state is in TanStack cache
   (per spec §10.2 race resolution). After connect+setReady the server
   stops buffering and starts streaming deltas.
3. Globe ring eviction — every 5min calls useGlobeEvents.evict(now)
   so expired pulse rings drop off.

Topic routing (typed switch, no `as any` — every payload narrows to a
local interface that matches the verified backend shape):
  cve_new          → useCveStore.push
  kev_added        → useKevStore.push
  ransomware_victim → useRansomwareStore.push
  coinbase_price   → usePrices.pushTick (ISO ts → number, snake_case
                     volume_24h → camelCase)
  earthquake       → globe point + ring pulse (4s TTL)
  iss_position     → globe point (id='iss-current' so each tick replaces
                     the previous) + setQueryData(iss_position) so the
                     ISS panel sees the latest
  wiki_itn         → ticker push (source 'Wikipedia')
  gdelt_spike      → ticker push (source 'GDELT', headline includes
                     z-score and count)
  space_weather/internet_outage/bgp_hijack/scan_firehose → setQueryData
                     to merge into snapshot (panels read from there)
  heartbeat        → no-op (connection liveness only)

Small bug fix in browserDriver: WebSocket.send() throws when readyState
is CONNECTING. createDashboardWS.setReady() can be called before the
socket opens (we call it immediately after snapshot resolves), so the
inner sock.send is now guarded by a readyState===OPEN check. The
onOpen handler in createDashboardWS resends init when the socket
actually opens — belt-and-suspenders.

This is the load-bearing wiring per the plan. Panels stop rendering
single snapshot rows and start growing as WS events arrive.
2026-05-03 09:47:40 -04:00
CarterPerez-dev 71e0529960 feat(monitor/frontend): about modal (meme + project explainer, ethos-minimal)
Native <dialog> opened from the TopStrip ? icon via useUIStore.aboutOpen.
Ref-driven showModal()/close() in useEffect, onClose synced to the store
so Escape closes both the dialog and the store flag in one round trip
(no listener fight). Three short paragraphs: the meme origin, the data
sources, and the keyboard shortcuts. The kbd-styled spans (mono +
1px --fg-4 outline) call out F and Esc.

No animation on open/close beyond the browser's default <dialog>
showModal transition (which is OS UI, not decoration). No backdrop-click
to close — Escape and the close button are enough; backdrop-click adds
a fragile target===dialog comparison for marginal value. ::backdrop
gets a 60% black tint so it reads as a modal, not a layer cake.

prose line-height 1.4 (looser than the 1.25 table tightness because
prose needs readability — operator-density still applies but Tufte
data-ink isn't violated by paragraph leading).

Plan 5 Task 23 Design QA: no illustration, no confetti, no tooltip
arrows, monochrome border, native ::backdrop only.
2026-05-03 09:43:27 -04:00
CarterPerez-dev 21f54390df feat(monitor/frontend): audio chime infrastructure (load/unlock/play, phase 6 wires the trigger)
Per spec §10.5 — three pure functions sitting on top of the existing
useAudioStore (so React surfaces can subscribe to .unlocked if a
"click anywhere to enable audio" toast is needed later):

  loadChime(file)              fetch / decode arraybuffer into AudioBuffer
  unlockOnFirstGesture()       attach one-shot pointerdown listener that
                               resumes a suspended AudioContext, sets
                               unlocked=true. Idempotent across re-mounts
                               via module-level listenerAttached guard
                               (StrictMode double-mount safe).
  playChime() → boolean        returns false if the audio context isn't
                               ready / buffer not loaded / not unlocked,
                               so callers can fall back to a toast.

Phase 6 calls playChime() on alert match; Phase 5 only wires the
unlockOnFirstGesture() call into the dashboard lifecycle (Task 24)
so the audio is ready when Phase 6 lands.
2026-05-03 09:41:22 -04:00
CarterPerez-dev 43df53f845 feat(monitor/frontend): ISS panel (live position + next-pass placeholder for phase 6)
Four-row layout: Lat/Lon (degree symbol, mono), Alt (km, mono tabular),
Vel (km/h with thousands separator, mono tabular), Next Pass (— stub).

Reads snapshot.iss_position directly (no per-panel store — current
position is the only thing the panel cares about, and the wheretheiss.at
collector pushes a complete state every 10s, so latest-event semantics
match what the panel needs). Real shape verified: latitude/longitude/
altitude/velocity/timestamp/fetched_at — all numeric except fetched_at
ISO string. Stale threshold 30s (3x the 10s poll cadence).

Next Pass row stays "—" muted --fg-3 — Phase 6 wires the SGP4-driven
user-observer-location pass prediction per spec §10.4. No "sign in to
add observer" CTA: operator UI doesn't onboard.

Plan 5 Task 21 Design QA: degree symbol + mono coords, alt/vel mono
tabular, no decorative ISS-tracing widget (the centerpiece globe owns
that), next-pass row visible-but-muted (never hidden).
2026-05-03 09:40:02 -04:00
CarterPerez-dev f0bb0dcb8d feat(monitor/frontend): space weather panel (Kp 9-bar, solar wind, X-ray flux)
Three-row layout (label | value | secondary or 9-bar). Kp index renders as
a 9-segment grid bar — segments below kp filled --fg-2, at-or-above outlined
--fg-4. When kp >= 7 (G3+ geomagnetic storm) the entire filled portion goes
amber AND the panel's stale dot fires amber — one of the few legitimate
amber-data uses per ethos. X-ray class M or X also flips amber.

Backend snapshot caveat: the SWPC collector currently emits only
{ts, pushed} as the WS event payload — the actual kp / bz_gsm / speed_kms
/ density / xray_class / xray_flux readings live in Redis ring buffers
(per spec §6.2) but aren't yet projected into snapshot.space_weather.
The TS interface defines all fields optimistically; the panel renders
"—" / empty 9-bar for missing fields. When the backend extends the SWPC
event payload (or a separate snapshot enrichment lands), the panel
auto-fills with no frontend changes.

KP_SEGMENT_KEYS is an explicit string array instead of Array.from(...,
i) to avoid biome's noArrayIndexKey warning while preserving the
positional segment semantics.

Plan 5 Task 20 Design QA: Kp bar uses ONLY --fg-2 / --fg-4 / --amber
(no other colors), values mono tabular, X-ray class letter sans, no
sun glyph / aurora photo / weather-forecast iconography.
2026-05-03 09:38:52 -04:00
CarterPerez-dev b67729ae13 feat(monitor/frontend): ETH panel (mirror of BTC, separate file pre-abstraction)
Mirrors BTCPanel structure 1:1 with SYMBOL='ETH-USD' and the Coinbase
ethereum price page as the raw link. Per Plan 5 Task 19: don't refactor
into a shared <PricePanel symbol="..." /> until Phase 6 — 50 lines of
duplication beats locking ourselves into a shared contract before BTC
and ETH might diverge in display (gas-fee tile for ETH only, etc.).

Same monochrome discipline: --fg-1 hero price, △/▽ glyph + sign for
direction, no green/red, no animated count-up. 24H change deferred for
the same reason as BTC (60-min history cap can't carry 24h yet).
2026-05-03 09:35:52 -04:00
CarterPerez-dev 71ea453b82 feat(monitor/frontend): BTC panel (hero price + glyph-based change indicator, no color direction)
Reads from the existing usePrices store (latest tick + 60-min history).
Seeds latest from snapshot.coinbase_price on mount, converting the ISO ts
string and snake_case volume_24h to the store's number-ts and camelCase
volume24h shapes (verified against live snapshot).

Hero price uses --type-num-xl mono with tabular-nums — the dashboard's
biggest single number by design. Color stays --fg-1 regardless of
direction. The 1H change row uses △ / ▽ glyph + sign for direction
(not green/red) per the ethos no-branded-color-per-metric rule.
Compute is (last - first) / first * 100 over the 60 minute-bar closes.

Stale indicator goes amber when last tick > 60s ago (Coinbase WS expects
sub-second updates; 60s of silence = real staleness).

24H change is deferred — would need backend snapshot to send 24h of
minute bars; the store caps at 60. Plan said both 1H and 24H but 24H
isn't computable from the available data yet. Showing only 1H is
honest — the missing row would be permanent "—" otherwise.

Sparkline is the 60-minute close ladder. Renders null while history is
shorter than 2 points (Sparkline primitive's own contract). Empty
panel surface is fine — operator UI doesn't onboard.

Plan 5 Task 18 Design QA: hero is the largest single number, price
color stays --fg-1, change row uses glyph + sign (no color), sparkline
is single-stroke currentColor with no fill or axes, no animated
count-up on price change.
2026-05-03 09:34:58 -04:00
CarterPerez-dev 9d391434e0 feat(monitor/frontend): ransomware panel (victim feed + 600ms data-changed flash)
RansomwareStore (zustand) accumulates RansomwareVictim items dedup-by
composite key (post_title|group_name|discovered) — the JSON payload has
no id field (the backend computes one via Row.ID() but doesn't include
it in the WS payload), so the composite key is what we have. Capped 200.

6-row tight table — victim mono primary, group sans --fg-2, country
sans --fg-3, ago mono muted right-aligned. table-layout fixed +
ellipsis. Hover --bg-row-hover.

Flash on new arrival: a useRef<Set> tracks seen composite keys; new
items not in the set get added to a flashKeys state for 600ms, which
applies a row-flash keyframe animation that fades from --fg-4
background to transparent. Per ethos motion=meaning rule — the only
panel motion allowed because new ransomware victims literally are
"data changed." Initial seed (when seenKeys is empty) does NOT trigger
a flash — the first batch is "what's there now," not "new arrivals."

No skull / lock / "RANSOMWARE!!" iconography (per Plan 5 Task 17 Design
QA). No colored severity. After flash, row visually identical to others.

Real shape verified against live snapshot.ransomware_victim: snake_case
post_title/group_name/discovered/country/activity/website/description.
TS interface matches.
2026-05-03 09:32:20 -04:00
CarterPerez-dev 4724f7fe92 feat(monitor/frontend): KEV panel (recent CISA additions, 6-row tight table)
KevStore (zustand) accumulates KevEntry items dedup-by-cveID, capped at
200. Panel seeds the store from snapshot.kev_added on mount.

6-row table — CVE-ID mono / vendor·product sans (--fg-2) / dateAdded mono
muted (--fg-3). table-layout: fixed + per-column widths (38% / 40% / 22%)
+ ellipsis on overflow. Hover --bg-row-hover (subtle, no transition).

No severity column, no fluff per the plan: every row in this catalog is
a known-exploited vulnerability, the existence of the row carries the
weight. No "ransomware-use" badge — that's the separate Ransomware panel's
job. No CTA — operator clicks the catalog via the raw link.

Real KevEntry shape verified against live snapshot.kev_added: camelCase
keys cveID/vendorProject/product/vulnerabilityName/dateAdded/dueDate/
knownRansomwareCampaignUse/shortDescription/requiredAction. Mirrored
exactly in the TS interface (no transform layer where bugs hide).

Plan 5 Task 16 Design QA: six rows max, CVE-ID mono primary text, vendor
· product body sans secondary, date mono muted, no severity badge.
2026-05-03 09:22:02 -04:00
CarterPerez-dev 79cbdaaa51 feat(monitor/frontend): CVE velocity panel (1h/6h/24h KPIs + 24h sparkline + recent table)
CveStore (zustand) accumulates CveEvent items dedup-by-CveID, capped at
500. The panel seeds the store from snapshot.cve_new on mount via
useEffect — Task 24's WS routing will extend this with live deltas.

Three KPIs derived in-component from the store (1h/6h/24h counts, mono
tabular --type-num-l). 24h sparkline computed from per-hour bucketing
of items[].Published (one of the few places a chart is justified —
shape-of-CVE-velocity IS the insight per ethos rule on charts). Bottom
table shows the 5 most recent: CVE-ID mono, severity sans plain (NOT a
colored badge — severity stays monochrome per ethos), EPSS percentile
mono right-aligned, relative-time mono muted right-aligned. table-layout:
fixed + per-column widths + ellipsis to keep rows on a single line in
the 320px column.

Real CveEvent shape matches the backend Row struct (verified against
live snapshot): PascalCase keys CveID/Published/LastModified/Severity/
CVSS/EPSSScore/EPSSPercentile/InKEV. Earlier plan-author assumption of
{recent_24h:[]} was wrong — snapshot delivers a single latest event,
the running list builds in the store over time.

Plan 5 Task 15 Design QA: KPIs mono tabular, sparkline pure stroke
(currentColor, no fill/axes), severity NOT a colored badge, EPSS mono %,
ago muted, no "view all" CTA — operator clicks NVD via the raw link.
2026-05-03 09:20:58 -04:00
CarterPerez-dev 9ea40198d0 fix(monitor/frontend): stack DShield tables vertically (no horizontal overflow in 320px column)
Plan 5 said "two compact tables side-by-side" but didn't account for the
320px right-column width — tables overflowed and the source-IP table's
Reports/Tgt columns scrolled off-screen. Replaces grid 1fr 1.2fr with a
flex column so each table gets the full panel width: top ports on top,
top sources below.
2026-05-03 09:16:58 -04:00