diff --git a/.env.example b/.env.example index 5d890dc..92011c7 100644 --- a/.env.example +++ b/.env.example @@ -37,6 +37,10 @@ REDIRECT_PORT=3000 # 307 (Temporary Redirect), 308 (Permanent Redirect - modern default) # REDIRECT_STATUS_CODE=308 +# Enable diagnostic timing headers in redirect responses (default: false) +# When true, adds X-Lynx-Cache-Hit, X-Lynx-Timing-Total-Ms, etc. to redirect responses +# ENABLE_TIMING_HEADERS=false + # Short code configuration # Maximum length for custom short codes (default: 50) # SHORT_CODE_MAX_LENGTH=50 diff --git a/.github/workflows/README.md b/.github/workflows/README.md index fca8f69..b83ebde 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -103,7 +103,7 @@ The Docker images support the following environment variables (see `.env.example - `API_PORT` - API server port (default: `8080`) - `REDIRECT_HOST` - Redirect server host (default: `0.0.0.0`) - `REDIRECT_PORT` - Redirect server port (default: `3000`) -- `AUTH_MODE` - Authentication mode (`none` or `oauth`) +- `AUTH_MODE` - Authentication mode (`none`, `oauth`, or `cloudflare`) ## Development diff --git a/.jules/atlas.md b/.jules/atlas.md new file mode 100644 index 0000000..9af9447 --- /dev/null +++ b/.jules/atlas.md @@ -0,0 +1,3 @@ +## 2026-02-28 - API pagination and redirect code drift +**Learning:** The README API examples used `offset=0` (traditional pagination) but the actual `ListQuery` struct in `src/api/handlers.rs` accepts `limit` + `cursor` (cursor-based pagination). The redirect default was documented as 301 but the `RedirectMode::default()` in `src/config/mod.rs` is 308. Additionally, `ENABLE_TIMING_HEADERS` was read in `src/main.rs` but missing from both README and `.env.example`. +**Action:** Always verify pagination parameters by checking the handler's query struct, not just the route definition. Check `src/main.rs` for env vars beyond `src/config/mod.rs` since some config (like `ENABLE_TIMING_HEADERS`) is parsed directly in main. Run `scripts/check-docs-drift.sh` to catch route and env var drift. diff --git a/README.md b/README.md index df027f1..1bd1942 100644 --- a/README.md +++ b/README.md @@ -338,6 +338,8 @@ All configuration is done via environment variables. See `.env.example` for a co | Variable | Description | Default | |----------|-------------|---------| | `CACHE_MAX_ENTRIES` | Maximum entries in read cache | `500000` (~100MB) | +| `REDIRECT_STATUS_CODE` | HTTP status code for redirects: `301`, `302`, `303`, `307`, `308` | `308` | +| `ENABLE_TIMING_HEADERS` | Include diagnostic timing headers in redirect responses | `false` | ### Frontend @@ -366,30 +368,49 @@ For custom frontend deployment or development, see the [Frontend README](fronten The API server exposes RESTful endpoints at `/api/*`. Authentication is required unless `AUTH_MODE=none`. -### Quick Examples +### Public Endpoints (no auth required) + +```bash +GET /api/health # Health check +GET /api/auth/mode # Returns the configured authentication mode +``` + +### Protected Endpoints (auth required unless AUTH_MODE=none) ```bash -# Health check -GET /api/health +POST /api/urls # Create short URL +GET /api/urls # List URLs (cursor-based pagination) +GET /api/urls/search # Search URLs by query string +GET /api/urls/{code} # Get URL details +PUT /api/urls/{code}/deactivate # Deactivate URL (admin only) +PUT /api/urls/{code}/reactivate # Reactivate URL (admin only) +GET /api/user/info # Get current user info +GET /api/analytics/{code} # Get analytics for a URL (admin only) +GET /api/analytics/{code}/aggregate # Get aggregated analytics (admin only) +``` + +### Quick Examples +```bash # Create short URL -POST /api/urls -{ - "url": "https://example.com/very/long/url", - "custom_code": "mycode" // optional -} +curl -X POST http://localhost:8080/api/urls \ + -H "Content-Type: application/json" \ + -d '{"url": "https://example.com/very/long/url", "custom_code": "mycode"}' -# Get URL details -GET /api/urls/:code +# List URLs (cursor-based pagination, default limit=50) +curl http://localhost:8080/api/urls?limit=20 + +# Paginate using the next_cursor from the previous response +curl http://localhost:8080/api/urls?limit=20&cursor= -# List URLs (paginated) -GET /api/urls?limit=50&offset=0 +# Get URL details +curl http://localhost:8080/api/urls/mycode -# Deactivate URL -PUT /api/urls/:code/deactivate +# Deactivate URL (admin only) +curl -X PUT http://localhost:8080/api/urls/mycode/deactivate -# Reactivate URL -PUT /api/urls/:code/reactivate +# Reactivate URL (admin only) +curl -X PUT http://localhost:8080/api/urls/mycode/reactivate ``` ### Redirect Server @@ -397,21 +418,19 @@ PUT /api/urls/:code/reactivate The redirect server handles public-facing redirects: ```bash -# Redirect to original URL -GET /:code -→ 301 Redirect to original URL +GET /{code} +→ 308 Permanent Redirect to original URL (configurable via REDIRECT_STATUS_CODE) # Returns 410 Gone for deactivated URLs # Returns 404 Not Found for non-existent codes ``` -The redirect endpoint includes performance tracing headers: +When `ENABLE_TIMING_HEADERS=true`, the redirect endpoint includes performance tracing headers: - `X-Lynx-Cache-Hit`: Whether served from cache (`true`/`false`) - `X-Lynx-Timing-Total-Ms`: Total request time in milliseconds - `X-Lynx-Timing-Cache-Ms`: Cache lookup time - `X-Lynx-Timing-Db-Ms`: Database lookup time (0 if cache hit) - -For complete API documentation, see the API section in the [old README](README.old.md) or explore the OpenAPI schema (if available). +- `X-Lynx-Timing-Handler-Ms`: Handler processing time ## Admin Management diff --git a/scripts/check-docs-drift.sh b/scripts/check-docs-drift.sh new file mode 100755 index 0000000..fc8b784 --- /dev/null +++ b/scripts/check-docs-drift.sh @@ -0,0 +1,105 @@ +#!/usr/bin/env bash +# Drift-prevention: checks that README.md documents all API routes and +# key environment variables that are defined in the source code. +# +# Usage: +# bash scripts/check-docs-drift.sh +# +# Exit code 0 = all checks pass, 1 = drift detected. + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +README="$REPO_ROOT/README.md" +ENV_EXAMPLE="$REPO_ROOT/.env.example" +DRIFT=0 + +# ── 1. API route coverage ─────────────────────────────────────────── +# Extract route paths from Rust source and verify each appears in README. +echo "=== Checking API route coverage in README ===" + +# Extract route strings from api/routes.rs and redirect/routes.rs +ROUTE_FILES=( + "$REPO_ROOT/src/api/routes.rs" + "$REPO_ROOT/src/redirect/routes.rs" +) + +for rf in "${ROUTE_FILES[@]}"; do + if [[ ! -f "$rf" ]]; then + echo "WARN: Route file not found: $rf" + continue + fi + # Match .route("/path", ...) and extract the path + while IFS= read -r route_path; do + if ! grep -qF "$route_path" "$README"; then + # Try with {param} converted to a grep-compatible regex + alt_path=$(echo "$route_path" | sed 's/{[^}]*}/[^/]*/g') + if ! grep -qP "$alt_path" "$README" 2>/dev/null; then + echo "DRIFT: Route '$route_path' (from $(basename "$rf")) not found in README.md" + DRIFT=1 + fi + fi + done < <(grep -oP '\.route\(\s*"\K[^"]+(?=")' "$rf") +done + +# ── 2. Environment variable coverage ──────────────────────────────── +# Check that key env vars read in config/mod.rs and main.rs appear in +# README.md or .env.example. +echo "=== Checking env var coverage in README + .env.example ===" + +# Collect env var names from config source and main.rs +ENV_SOURCES=( + "$REPO_ROOT/src/config/mod.rs" + "$REPO_ROOT/src/main.rs" +) + +KNOWN_VARS=() +for src in "${ENV_SOURCES[@]}"; do + if [[ ! -f "$src" ]]; then continue; fi + while IFS= read -r var; do + KNOWN_VARS+=("$var") + done < <(grep -oP 'std::env::var\(\s*"\K[^"]+(?=")' "$src") +done + +# De-duplicate +UNIQUE_VARS=($(printf '%s\n' "${KNOWN_VARS[@]}" | sort -u)) + +for var in "${UNIQUE_VARS[@]}"; do + found=0 + grep -qF "$var" "$README" && found=1 + if [[ $found -eq 0 ]]; then + grep -qF "$var" "$ENV_EXAMPLE" && found=1 + fi + if [[ $found -eq 0 ]]; then + echo "DRIFT: Env var '$var' (from source) not found in README.md or .env.example" + DRIFT=1 + fi +done + +# ── 3. Redirect status code default ──────────────────────────────── +echo "=== Checking redirect status code documentation ===" +# The default redirect code is 308 (RedirectMode::default() = Permanent). +# README must not claim 301 as the default. +if grep -qP '→\s*301\s+Redirect' "$README"; then + echo "DRIFT: README claims default redirect is 301 but code defaults to 308" + DRIFT=1 +fi + +# ── 4. Pagination parameter ──────────────────────────────────────── +echo "=== Checking pagination documentation ===" +# The list endpoint uses cursor-based pagination, not offset-based. +if grep -qP 'offset=\d+' "$README"; then + echo "DRIFT: README references offset-based pagination but API uses cursor-based" + DRIFT=1 +fi + +# ── Summary ───────────────────────────────────────────────────────── +if [[ $DRIFT -eq 0 ]]; then + echo "" + echo "✅ All documentation drift checks passed." + exit 0 +else + echo "" + echo "❌ Documentation drift detected. Please update docs to match source." + exit 1 +fi