Precedence: the closest AGENTS.md to the files you're changing wins. This root holds global defaults only; scoped files override.
| Path | Scope |
|---|---|
| internal/AGENTS.md | Go backend packages — LDAP, rate limit, RPC, tokens |
| internal/web/AGENTS.md | Frontend — TypeScript, Tailwind CSS, WCAG 2.2 AAA |
LDAP self-service password changer (hybrid Go + TypeScript web app). Email-based reset, rate limiting, WCAG 2.2 AAA compliance. Single binary deployment with embedded assets.
Stack: Go 1.26 + Fiber v3, TypeScript (ultra-strict), Tailwind CSS 4, Docker multi-stage, Bun (migrated from pnpm in 8f69f47).
Source: package.json scripts + go test. Run from repo root.
| Task | Command |
|---|---|
| Install deps | bun install (CI; adds --frozen-lockfile for reproducible release builds) |
| Dev (all watch) | bun run dev (runs build:assets then TS/CSS watchers + air for Go hot-reload) |
| Build all | bun run build |
| Build assets | bun run build:assets (Tailwind CSS + tsc) |
| TS watch | bun run js:dev |
| TS type-check / compile | bun run js:build (runs tsc; no minifier configured) |
| CSS watch | bun run css:dev |
| CSS build | bun run css:build |
| Go test | go test -v -race ./... |
| Go build | go build -v ./... |
| Format | bunx prettier --write . |
| Format check | bunx prettier --check . |
| Lint TS | bun run lint (or bun run lint:fix) |
| Lint Go | CI runs via golangci/golangci-lint-action. Locally: install golangci-lint then golangci-lint run. |
Toolchain install: Bun and Go are installed separately (no packageManager/engines pins in package.json). Neither air nor golangci-lint is a bun dependency — bun run dev invokes air as a bare command, so install both yourself (go install github.com/air-verse/air@latest, and golangci-lint via go install or your package manager).
Git hooks: make hooks copies githooks/pre-commit and githooks/commit-msg into .git/hooks/. Husky is no longer used.
Docker-first: docker compose --profile dev up is the canonical dev path; native Bun/Go is optional convenience.
Running it locally: full runbook in docs/development-guide.md. The Dockerfile is a binary-selector (COPY bin/…-linux-<arch>), so docker compose --build — including the dev/test profiles — fails on a clean checkout until you build binaries into bin/ first.
- Before coding, read the nearest
AGENTS.mdfor the area you're touching. - After a change: smallest relevant check (
bun run js:buildfor TS,go test ./pkg/...for Go). - Before committing:
bunx prettier --write .+ fullgo test ./...if ≥2 files or shared code. - Before claiming done: run verification, show output as evidence — never "try again" or "should work now" without proof.
- No secrets in git. Use
.env.local(gitignored). Never commit LDAP/SMTP credentials. - LDAPS required in production (
ldaps://URLs). - No PII logging — passwords, tokens, session IDs never reach logs.
- Rate limiting — two separate in-memory limiters, don't conflate them:
- Per-IP: 10 requests / 60 minutes, max 1000 tracked IPs. Hardcoded in
internal/ratelimit/ip_limiter.go(NewLimiterWithCapacity(10, 60*time.Minute, 1000)); no environment variable configures it. Applies tochange-passwordandrequest-password-reset. - Per-identifier (reset only):
RESET_RATE_LIMIT_REQUESTS(default 3) perRESET_RATE_LIMIT_WINDOW_MINUTES(default 60), keyed by the typed identifier and again by the resolved account — not by IP. - There is no
RATE_LIMIT_*variable prefix.
- Per-IP: 10 requests / 60 minutes, max 1000 tracked IPs. Hardcoded in
- Cryptographic random tokens with configurable expiry; single-use, server-side.
- Strict input validation at boundaries — see
internal/validators/. - Container runs as UID 65534 (nobody), not root.
- Renovate handles dependency updates; review major-version changelogs.
Before commit:
-
bunx prettier --write . -
bun run js:build(TypeScript strict) -
go test ./... -
go build - No secrets in staged files
- Docs updated if behavior changed
- WCAG 2.2 AAA maintained (if UI changed — see docs/accessibility.md)
Commit format: Conventional Commits. Examples: feat(auth): add reset via email, fix(validators): correct regex, chore(deps): bump bun. No AI attribution in messages.
PR:
- CI green (types, formatting, tests, security scans)
- Keep small (~≤300 net LOC when possible)
- Prefix with ticket ID when applicable
- Updated docs land in same PR
- Docker-first. Native setup is convenience, not requirement. Use compose profiles:
dev,test. - YAGNI. Build only what's requested. No speculative features.
- Type safety. TS: no
any, all strict flags on. Go:any(notinterface{}),errors.AsType[T], wrap errors with context. - Accessibility non-negotiable. 7:1 contrast, full keyboard nav, screen-reader tested. See docs/accessibility.md.
- Dependencies. Keep
bun.lockandgo.sumcommitted. Use top-leveloverridesinpackage.jsonfor transitive CVE fixes when upstream hasn't patched. - CI/CD.
pr-quality.ymlauto-approves collaborator PRs.auto-merge-deps.ymlhandles Dependabot/Renovate. See docs/development-guide.md for bootstrap.
Unified with the org's release pipeline (netresearch/.github reusables).
git tag -s vX.Y.Z -m "vX.Y.Z" # annotated + signed tag (required)
git push origin vX.Y.Z # triggers release.ymlPipeline (see .github/workflows/release.yml):
create-release— creates GitHub Release; computesmake_latestfrom semver vs existing releases (backfills and older-major bugfixes don't steal the Latest badge).goreleaser— builds binaries, archives, per-archive Syft SBOMs, cosign-signedchecksums.txt; attests archives+SBOMs viaactions/attest-build-provenance.container— multi-arch ghcr.io image, cosign keyless-signed + SLSA provenance.verify-notes— appends standardized "Verify your download" block.
Backfill: gh workflow run release.yml --ref main -f tag=vX.Y.Z.
GoReleaser config notes (.goreleaser.yml):
mode: keep-existing(preserves create-release's notes; don't change toreplace)changelog.use: git(notgithub—githubignores filters)- No 32-bit ARM: Fiber v3
math.MaxUint32overflowsinton 32-bit →linux/armexcluded
- Scoped
AGENTS.mdfor the area → code ininternal/→ docs/. - Similar patterns: search git history / existing tests.
go test -v ./...surfaces many problems.- Docker weirdness:
docker compose down -v && docker compose --profile dev up --build. If the build dies onCOPY bin/…: lstat /bin: no such file or directory, that's the binary-selector — see docs/development-guide.md. - Env config:
.env.localvs.env.local.example.