Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 3 additions & 0 deletions .jules/atlas.md
Original file line number Diff line number Diff line change
@@ -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.
63 changes: 41 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -366,52 +368,69 @@ 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=<next_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

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

Expand Down
105 changes: 105 additions & 0 deletions scripts/check-docs-drift.sh
Original file line number Diff line number Diff line change
@@ -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
Loading