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
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
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