Skip to content

fix(security): require auth for detailed health metadata#4715

Merged
koala73 merged 2 commits into
mainfrom
codex/health-details-require-api-key
Jul 4, 2026
Merged

fix(security): require auth for detailed health metadata#4715
koala73 merged 2 commits into
mainfrom
codex/health-details-require-api-key

Conversation

@koala73

@koala73 koala73 commented Jul 4, 2026

Copy link
Copy Markdown
Owner

Summary

Unauthenticated health probes can still check compact status, but they can no longer retrieve the full operational inventory of cache keys, record counts, and freshness thresholds. Detailed /api/health responses and ?history=1 now require an operator/enterprise API key, while ?compact=1 remains public for UptimeRobot and keyword monitors.

The endpoint also normalizes user-key gateway fallback failures to Invalid API key, so standalone detailed-health auth does not expose internal validation plumbing. The docs now point public monitors at the compact URL and describe the keyed operator view explicitly.

Validation

  • node --test tests/health-redis-down-status.test.mjs api/health-cors.test.mjs
  • npm run typecheck:api
  • npm run lint:mintlify-slugs
  • npm run lint:unicode:staged
  • git push --force-with-lease pre-push guardrails passed, including full typecheck, API typecheck, Convex/CJS/unicode/boundary checks, safe HTML, rate-limit policy, premium-fetch parity, edge function bundle/tests, markdown lint, MDX lint, and version sync

Compound Engineering
GPT--5_Codex

@mintlify

mintlify Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
WorldMonitor 🟢 Ready View Preview Jul 4, 2026, 5:33 AM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@vercel

vercel Bot commented Jul 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
worldmonitor Ignored Ignored Preview Jul 4, 2026 7:01am

Request Review

@greptile-apps

greptile-apps Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR gates the detailed /api/health and ?history=1 responses behind a valid API key while keeping ?compact=1 public for uptime monitors. The auth check is placed before any Redis I/O, OPTIONS preflights are unaffected, and the four new test cases cover the major auth paths cleanly.

  • api/health.js: URL parameter parsing is moved earlier, a new validateApiKey(req, { forceKey: true }) call blocks unauthenticated requests for non-compact and history views, and the redundant inner new URL(req.url) block is removed.
  • Docs: All public-monitor examples are updated to ?compact=1; the keyed operator curl example and the new 401 row in the status table are added.

Confidence Score: 4/5

Safe to merge for the primary use case (enterprise operator keys and public uptime monitors), but user-facing wm_-prefixed API keys will hit a confusing 401 containing internal implementation language.

The auth gate, compact bypass, and OPTIONS handling are all correct. The one real defect is that wm_-prefixed user API keys are rejected with 'User API key requires gateway validation' — an internal message surfaced verbatim to callers — because the gateway validation path that would actually honour those keys never runs in this Edge Function context. Anyone holding a user-tier key who reads the updated docs and tries the endpoint gets an opaque, actionless error.

api/health.js — specifically the interaction between the new auth gate and wm_-prefixed user API keys, and whether the docs' 'valid API key' phrasing should be narrowed to 'operator API key'.

Important Files Changed

Filename Overview
api/health.js Adds auth gate before detailed health and history responses; compact mode remains public. Auth check correctly positions before any Redis I/O, and OPTIONS handling is correctly unaffected. However, wm_-prefixed user API keys are silently rejected with an internal-jargon error message because the gateway validation path never executes in this Edge Function context — only enterprise keys from WORLDMONITOR_VALID_KEYS actually work.
tests/health-redis-down-status.test.mjs Expands the original single REDIS_DOWN test into four focused cases: unauthenticated detailed health → 401, unauthenticated history → 401, compact public access → 503 REDIS_DOWN, authenticated enterprise key → 503 REDIS_DOWN. Coverage is good for the enterprise-key path; no test for wm_-prefixed user keys.
docs/health-endpoints.mdx Updates authentication description, adds 401 to the status table, and updates all example curl commands to use compact or keyed variants. The note about operator-view access is clear for enterprise key holders, but the generic phrasing "valid API key" may mislead users holding wm_-prefixed keys.
docs/api-platform.mdx Single-line update directing public monitors to ?compact=1 and noting the key requirement for detailed view. Change is accurate and consistent with the implementation.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant C as Caller
    participant H as /api/health
    participant AK as validateApiKey
    participant R as Redis

    C->>H: OPTIONS
    H-->>C: 204 (CORS preflight, no auth)

    C->>H: "GET ?compact=1"
    Note over H: compact=true, wantsHistory=false<br/>!compact||wantsHistory = false → skip auth
    H->>R: full pipeline
    R-->>H: results
    H-->>C: 200/503 compact body

    C->>H: GET /api/health (no key)
    Note over H: compact=false → auth required
    H->>AK: validateApiKey(forceKey:true)
    AK-->>H: "{valid:false, required:true, error:'API key required'}"
    H-->>C: "401 {error:'API key required'}"

    C->>H: GET /api/health (enterprise key)
    H->>AK: validateApiKey(forceKey:true)
    AK-->>H: "{valid:true, required:true, kind:'enterprise'}"
    H->>R: full pipeline
    R-->>H: results
    H-->>C: 200/503 full body

    C->>H: "GET ?history=1 (no key)"
    Note over H: wantsHistory=true → auth required
    H->>AK: validateApiKey(forceKey:true)
    AK-->>H: "{valid:false, required:true, error:'API key required'}"
    H-->>C: "401 {error:'API key required'}"

    C->>H: "GET /api/health (wm_* user key)"
    H->>AK: validateApiKey(forceKey:true)
    AK-->>H: "{valid:false, required:true, error:'User API key requires gateway validation'}"
    H-->>C: 401 (internal jargon leaked)
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant C as Caller
    participant H as /api/health
    participant AK as validateApiKey
    participant R as Redis

    C->>H: OPTIONS
    H-->>C: 204 (CORS preflight, no auth)

    C->>H: "GET ?compact=1"
    Note over H: compact=true, wantsHistory=false<br/>!compact||wantsHistory = false → skip auth
    H->>R: full pipeline
    R-->>H: results
    H-->>C: 200/503 compact body

    C->>H: GET /api/health (no key)
    Note over H: compact=false → auth required
    H->>AK: validateApiKey(forceKey:true)
    AK-->>H: "{valid:false, required:true, error:'API key required'}"
    H-->>C: "401 {error:'API key required'}"

    C->>H: GET /api/health (enterprise key)
    H->>AK: validateApiKey(forceKey:true)
    AK-->>H: "{valid:true, required:true, kind:'enterprise'}"
    H->>R: full pipeline
    R-->>H: results
    H-->>C: 200/503 full body

    C->>H: "GET ?history=1 (no key)"
    Note over H: wantsHistory=true → auth required
    H->>AK: validateApiKey(forceKey:true)
    AK-->>H: "{valid:false, required:true, error:'API key required'}"
    H-->>C: "401 {error:'API key required'}"

    C->>H: "GET /api/health (wm_* user key)"
    H->>AK: validateApiKey(forceKey:true)
    AK-->>H: "{valid:false, required:true, error:'User API key requires gateway validation'}"
    H-->>C: 401 (internal jargon leaked)
Loading

Reviews (1): Last reviewed commit: "fix(security): require auth for detailed..." | Re-trigger Greptile

Comment thread api/health.js
@koala73
koala73 merged commit 59e34e6 into main Jul 4, 2026
24 checks passed
@koala73
koala73 deleted the codex/health-details-require-api-key branch July 4, 2026 07:06
koala73 added a commit that referenced this pull request Jul 5, 2026
…support discovery + status advertisement fixes (#4867)

Five external agent-journey runs (signup, pricing, developer-auth, support,
integration intents) showed a clean split: intents answerable via the
Link-header → .well-known → auth.md chain scored 90-100%, while pricing,
signup, and support — discoverable only via llms.txt or nowhere — forced
agents into slug-guessing, UA 403s, and misleading-200 SPA shells.

Closes #4850 (signup journey):
- llms.txt/llms-full.txt + auth.md: document the descriptive User-Agent
  policy (edge WAF challenges curl/*, python-requests/*, short UAs — and
  masks 404s as 403s for those UAs)
- docs/accounts.mdx (+ nav): agent-facing account-creation guide — free tier
  needs no account; Clerk modal flow; MCP OAuth path for agents
- vercel.json: 301 /blog/posts → /blog (posts live at /blog/posts/<slug>/,
  the parent had no index)

Closes #4854 (pricing journey):
- .well-known/api-catalog: RFC 9727 service-meta advertising pricing.md,
  the live product-catalog JSON, and support.md on the api context object
- middleware.ts: /api/product-catalog joins PUBLIC_API_PATHS — the keyless
  read-only pricing catalog was UA-403'd for the exact agents it serves;
  robots.txt gets the matching Allow carve-out (llms.txt precedent)
- docs/pricing.mdx (+ nav) with a drift guard test extracting prices from
  convex/config/productCatalog.ts source text (no generator import)
- docs/openapi/CommerceService.openapi.yaml wired into the api-reference
  nav — placed OUTSIDE docs/api/ (that directory is generated: proto-check
  regen diff + injector scripts would clobber a hand-authored spec); kept
  out of the root openapi.json to respect the #4853 ~1MB size budget
- docs.json redirects for the guessed slugs: plans/tiers → pricing,
  rate-limit(s) → usage-rate-limits, mcp-api-key → usage-auth
- pricing.md: Limits & Overage section (hard 429 + Retry-After, no silent
  overage billing) + live JSON catalog pointer + UA note

Closes #4856 (status advertisement, stale since #4715):
- api-catalog status href + 3x vercel.json Link rel="status" now point at
  the keyless /api/health?compact=1 form
- /api/health 401 carries WWW-Authenticate with RFC 9728 resource_metadata
  (RFC 7235 §3.1 made it mandatory; it was absent) and a body hint naming
  the public compact form
- deploy-config guards: every advertised status URL must be the keyless
  compact form; service-meta entries pinned

Closes #4857 (support journey):
- public/support.md (root, sitemap-listed) + docs/support.mdx (+ nav):
  consolidated channels (support@/enterprise@, GitHub issues, Discord,
  status page, Turnstile form marked human-only) + explicit no-SLA
  statement per tier
- vercel.json: /contact, /support, /help 301 → /docs/support (they served
  the misleading-200 dashboard SPA shell via the catch-all rewrite)
- server-card.json documentation block: support repointed to /docs/support
  (was getting-started), pricing + productCatalog entries added

Out of scope, noted: the Cloudflare-side WAF UA rule (dashboard config, not
repo) and the root openapi info.description (generated + size-budgeted).

Claude-Session: https://claude.ai/code/session_01N3xTXNUrCiy1xqy3VbkMRx
koala73 added a commit that referenced this pull request Jul 5, 2026
…p the anon 401-per-minute flood (#4902) (#4905)

#4715 key-gated detailed /api/health (operator/enterprise only) but the
dashboard client kept fetching it bare: every anonymous tab 401'd once a
minute and the seed-health → data-freshness pipeline went silently dead.

- Default the client to /api/health?compact=1 (public). `problems` carries
  the same per-check shape; mapped checks absent from it were evaluated
  server-side and found within budget, so synthesize OK-as-of-checkedAt
  (seedAgeMin 0 — an age-less update would leave lastUpdate null and
  calculateStatus reports no_data).
- 401/403 → 15-min suppression window: one visible error per window per tab
  instead of one per tick if the endpoint ever gets re-gated (same
  caller-sweep flood class as #4865).
- REDIS_DOWN 503 body-first handling unchanged (short-circuits before the
  compact branch server-side).

Verified against live prod: 24 mapped sources hydrate, stale gdeltIntel
maps very_stale, absent checks read fresh.

Fixes #4902

Claude-Session: https://claude.ai/code/session_01YKrVoDp8TfEKLYXo83kPFV
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant