diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8435483..2a6df0f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,20 +10,20 @@ jobs: vet: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 + - uses: actions/checkout@v7 + - uses: actions/setup-go@v7 with: - go-version: "1.25" + go-version-file: "go.mod" cache: false - run: go vet ./... build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 + - uses: actions/checkout@v7 + - uses: actions/setup-go@v7 with: - go-version: "1.25" + go-version-file: "go.mod" cache: false - run: CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /tmp/httphq ./src @@ -34,7 +34,7 @@ jobs: contents: read packages: write steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - id: meta run: | @@ -55,7 +55,7 @@ jobs: - uses: docker/setup-buildx-action@v4 - - uses: docker/build-push-action@v6 + - uses: docker/build-push-action@v7 with: context: . platforms: linux/amd64 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d3e9349..3de0e07 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,10 +13,10 @@ jobs: unit: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 + - uses: actions/checkout@v7 + - uses: actions/setup-go@v7 with: - go-version: "1.25" + go-version-file: "go.mod" cache: false - run: go test ./... e2e: @@ -25,14 +25,14 @@ jobs: run: working-directory: e2e steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 + - uses: actions/checkout@v7 + - uses: actions/setup-go@v7 with: - go-version: "1.25" + go-version-file: "go.mod" cache: false - - uses: actions/setup-node@v6 + - uses: actions/setup-node@v7 with: - node-version: "22" + node-version-file: ".tool-versions" - name: Build server run: CGO_ENABLED=0 go build -o ./bin/httphq ./src working-directory: . diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..5f543b4 --- /dev/null +++ b/.tool-versions @@ -0,0 +1 @@ +nodejs 22.23.1 diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..87cbd14 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,53 @@ +# AGENTS.md + +Guidance for agents and contributors working in this repository. + +## Comments + +Comments describe what the code does now and warn about non-obvious constraints +or regressions. They are not a changelog, bug tracker, or ticket index. Write +them so they still make sense in a year. + +- No ticket or PR references. +- No bug war stories: don't narrate a specific bug and how it was fixed ("this + fixes the flicker when..."). Once fixed, that history is noise. +- No hyper-specific framing: don't tie comments to one-time scenarios; describe + the generic, reusable purpose instead. +- Explain intent and non-obvious behavior: why this branch exists, what + invariant it protects. +- Flag regression risks the next dev must respect (e.g. "excluded by default so + new surfaces are safe"). +- Keep comments generic and reusable, especially in shared helpers and test + fixtures. + +## Client IP is a trust decision + +`PLATFORM` selects which header the real client IP is read from, and setting it +trusts that header unconditionally — httphq cannot tell a platform's header +from one a client forged. Inbound traffic must not be able to reach the process +bypassing that platform, or a client can spoof its IP and evade rate limiting. +Leaving it unset behind a proxy is the opposite failure: every request looks +like it came from the proxy and rate limiting becomes global. + +## Captured data is ephemeral + +SQLite writes to the container's writable layer. Capture history is lost on +restart, by design — nothing here is a durable store, and no migration path +exists for it. + +## Logging + +Structured JSON to stdout via `log/slog`, with OpenTelemetry field names +(`service.name`, `http.request.method`, `url.path`, ...). Every request carries +a `request_id` that is reused from a valid inbound `X-Request-Id` or minted, +echoed on the response, and stamped onto every line emitted while handling it. + +Headers and bodies are never logged and paths are logged without their query +string; a denylist masks sensitive keys as a backstop. Probe traffic to +`/api/health` logs at debug so it stays out of production logs. + +## The listen port is a constant + +`port` in `src/application.go` is not configurable. Anything that needs to run +two instances, or to run alongside something already holding 8080, has to +change the constant. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..8b7cbf4 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +See [AGENTS.md](AGENTS.md). diff --git a/README.md b/README.md index a9b3cc4..9c3faff 100644 --- a/README.md +++ b/README.md @@ -63,10 +63,6 @@ without the noise of whatever provider sits in front of httphq. > behind a proxy but leave `PLATFORM` unset, every request appears to come > from the proxy and rate limiting becomes global. -## Logging - -httphq logs to stdout as structured JSON via the standard library's `log/slog` — one JSON object per line, with no log files or shipping built in, so any collector can pick the logs up. Field names follow OpenTelemetry conventions (`service.name`, `http.request.method`, `url.path`, `http.response.status_code`, ...). Every request gets a correlation `request_id` (a valid inbound `X-Request-Id` is reused, otherwise one is minted) that is echoed back on the response header and stamped onto every log line emitted while handling that request. Each request produces one access-log line; headers and bodies are never logged, paths are logged without their query string, and a denylist masks sensitive keys as a backstop. The level defaults to `info` in production (`debug` elsewhere) and is overridable with `LOG_LEVEL`; Kubernetes probe traffic to `/api/health` logs at `debug` so it stays out of production logs. - ## License [MIT](https://opensource.org/licenses/MIT) diff --git a/e2e/package-lock.json b/e2e/package-lock.json index 436698d..0a51883 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -7,7 +7,8 @@ "name": "httphq-e2e", "devDependencies": { "@playwright/test": "^1.50.0", - "typescript": "^5.6.0" + "@types/node": "^22.20.1", + "typescript": "^6.0.3" } }, "node_modules/@playwright/test": { @@ -26,6 +27,16 @@ "node": ">=18" } }, + "node_modules/@types/node": { + "version": "22.20.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, "node_modules/fsevents": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", @@ -74,9 +85,9 @@ } }, "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -86,6 +97,13 @@ "engines": { "node": ">=14.17" } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" } } } diff --git a/e2e/package.json b/e2e/package.json index 89c9b8d..8d781a5 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -8,6 +8,7 @@ }, "devDependencies": { "@playwright/test": "^1.50.0", - "typescript": "^5.6.0" + "@types/node": "^22.20.1", + "typescript": "^6.0.3" } } diff --git a/e2e/tsconfig.json b/e2e/tsconfig.json new file mode 100644 index 0000000..9fca29a --- /dev/null +++ b/e2e/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "lib": ["ES2022", "DOM"], + "module": "preserve", + "moduleResolution": "bundler", + "noEmit": true, + "skipLibCheck": true, + "strict": true, + "target": "ES2022", + "types": ["node"] + }, + "include": ["tests/**/*.ts", "playwright.config.ts"] +}