vps-health-api/.env.example
le king fu e1224e0df4 feat(claude-usage): add POST and GET /claude-usage behind a third token kind
- checkAuth() reads the expected token from a TOKEN_KINDS table
  (read -> HEALTH_TOKEN, ingest -> HOSTS_INGEST_TOKEN, claude-ingest ->
  CLAUDE_USAGE_INGEST_TOKEN); each ingestion token fails closed to a 503
  naming its own variable. Still one gate, one tokenMatches() call. A route
  naming an unknown kind fails module load.
- readBody() takes its ceiling per route: 4 KiB for /hosts/<id>, 64 KiB for
  /claude-usage.
- claude-usage.js: pure, exported sanitizeClaudeUsage() for contract
  schemaVersion 1 (whitelist rebuild, invalid values dropped, list bounds).
- POST /claude-usage: 400 on invalid JSON or schemaVersion, server-stamped
  receivedAt, atomic write (pid-suffixed temp, mode 600, rename), 204.
- GET /claude-usage: re-sanitized snapshot with ageSeconds, stale and
  staleAfterSeconds (CLAUDE_USAGE_STALE_SECONDS, default 2700), or no_data.
- Dockerfile COPY ships claude-usage.js.
- auth.test.js: the checkAuth() equality allowlist loses the former
  `route.tokenKind === "ingest"` entry, which the table removes. The pin
  only got stricter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 09:16:05 -04:00

41 lines
2.4 KiB
Text

PORT=3001
# HEALTH_TOKEN is read at runtime only (process.env at startup).
# On Coolify: MUST be is_runtime=true, is_buildtime=false.
# Buildtime ARG leaks the secret in clear in application_deployment_queues.logs.
HEALTH_TOKEN=change-me-to-a-strong-secret
LOGTO_HEALTH_URL=https://auth.lacompagniemaximus.com/oidc/.well-known/openid-configuration
# Directory read by GET /reports/scans and GET /defenseurs/findings. On Coolify
# it is its own read-only mount (/home/defenseur/defenseurs/reports ->
# /data/defenseurs/reports:ro), nested inside the read-only /data/defenseurs
# mount that holds status.json.
REPORTS_DIR=/data/defenseurs/reports
# --- Workstation snapshots (POST /hosts/<id>, GET /hosts) --------------------
# Directory the API WRITES workstation snapshots into, one <id>.json per host.
# Unlike the two /data/defenseurs mounts (read-only `:ro`; content owned by
# defenseur:defenseur, uid/gid 1001, group-writable but not by gid 1000), this
# one must be writable by uid 1000 (the `node` user the container runs as) or
# every ingest answers 500.
HOSTS_DIR=/data/hosts
# Comma-separated allowlist of host ids. Each entry must match
# ^[a-z0-9][a-z0-9-]{0,31}$ — anything else is logged and dropped at startup.
HOSTS_ALLOWED_IDS=thinkpad
# Bearer token for POST /hosts/<id>. Separate from HEALTH_TOKEN on purpose: a
# workstation agent should be able to write its own snapshot without gaining
# read access to the Defenseurs reports. Same Coolify rule as HEALTH_TOKEN —
# is_runtime=true, is_buildtime=false. Unset -> POST /hosts/<id> answers 503.
HOSTS_INGEST_TOKEN=change-me-to-a-strong-secret
# Age (seconds) past which GET /hosts reports a host as offline. Default 900.
HOSTS_STALE_SECONDS=900
# --- Claude usage snapshot (POST /claude-usage, GET /claude-usage) ---------
# Bearer token for POST /claude-usage, pushed by the ThinkPad agent. Distinct
# from HOSTS_INGEST_TOKEN and HEALTH_TOKEN: each one is refused on the others'
# routes. Same Coolify rule — is_runtime=true, is_buildtime=false. Unset ->
# POST /claude-usage answers 503.
CLAUDE_USAGE_INGEST_TOKEN=change-me-to-a-strong-secret
# File the API WRITES (atomic rename). Its directory must be its own read-write
# mount owned by uid 1000, like HOSTS_DIR.
CLAUDE_USAGE_PATH=/data/claude-usage/claude-usage.json
# Age (seconds) past which GET /claude-usage reports stale: true. Default 2700.
CLAUDE_USAGE_STALE_SECONDS=2700