From f405186eb850a8f11c82292bf84f9d427f0f67f7 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Mon, 13 Jul 2026 00:59:22 -0700 Subject: [PATCH] =?UTF-8?q?Add=20API-SURFACE.md=20=E2=80=94=20full=20route?= =?UTF-8?q?=20inventory=20with=20auth=20+=20rate-limit=20classification?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Generated from static analysis of router.*() registrations across 47 route files. Covers 285 routes grouped into 29 feature areas. Each entry includes method, full path, auth classification (public/protected per PUBLIC_ROUTES allowlist), rate-limit bucket (GENERAL/STRICT/TOTP), and source file:line. Also cross-checks against openapi.yaml: 142 routes undocumented, 18 stale paths in spec. This is a real gap that should be fixed before v1.0 public release. --- API-SURFACE.md | 706 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 706 insertions(+) create mode 100644 API-SURFACE.md diff --git a/API-SURFACE.md b/API-SURFACE.md new file mode 100644 index 0000000..e86bf1d --- /dev/null +++ b/API-SURFACE.md @@ -0,0 +1,706 @@ +# DashCaddy API Surface + +> **Generated:** 2026-07-13 +> **Total routes:** 285 +> **Files scanned:** 47 +> **Source of truth:** router.* registrations in `dashcaddy-api/routes/` + root paths in `src/app.js` + +## Auth & Rate Limit Model + +**Auth classification:** +- `public` = in `PUBLIC_ROUTES` allowlist (`src/utilities/middleware.js:310-364`), bypasses TOTP +- `protected` = requires valid TOTP session cookie (`dashcaddy_session`) OR API key/JWT token + +**Rate limits** (from `RATE_LIMITS` in `src/utilities/constants.js:69`): +- `GENERAL` = 1000 req / 15 min / IP — default for all `/api/v1/*` +- `STRICT` = 20 req / 15 min / IP — auth key endpoints (`/auth/keys`, `/auth/jwt`, `/auth/gate`, `/auth/app-token`) +- `TOTP` = 10 req / 15 min / IP — TOTP verify/setup + +**CSRF:** TOTP session uses double-submit cookie pattern. State-changing requests (POST/PUT/DELETE/PATCH) require `X-CSRF-Token` header matching the `csrf_token` cookie. + +--- + +## Summary by Area + +| Area | Routes | Public | Protected | +|---|---:|---:|---:| +| App catalog | 28 | 0 | 28 | +| Tailscale | 20 | 20 | 0 | +| Backups | 19 | 0 | 19 | +| DNS | 19 | 0 | 19 | +| Monitoring | 19 | 3 | 16 | +| Updates | 16 | 6 | 10 | +| Authentication | 15 | 10 | 5 | +| Logs | 15 | 0 | 15 | +| Configuration | 13 | 9 | 4 | +| Health | 12 | 2 | 10 | +| Services | 12 | 4 | 8 | +| Containers (lifecycle) | 10 | 0 | 10 | +| Core / system | 9 | 6 | 3 | +| Dependencies | 8 | 0 | 8 | +| Notifications | 8 | 0 | 8 | +| App recipes | 8 | 0 | 8 | +| Docker resources | 7 | 0 | 7 | +| Caddy / sites | 7 | 0 | 7 | +| Updates / workflows | 6 | 0 | 6 | +| Auto-restart | 5 | 0 | 5 | +| Certificate authority | 5 | 5 | 0 | +| OpenClaw integration | 5 | 0 | 5 | +| Config drift | 4 | 0 | 4 | +| Licensing | 4 | 2 | 2 | +| File browser | 3 | 0 | 3 | +| Theming | 3 | 1 | 2 | +| Service credentials | 2 | 0 | 2 | +| Events | 2 | 0 | 2 | +| Internal helpers | 1 | 0 | 1 | +| **TOTAL** | **285** | **68** | **217** | + +## App catalog + +_28 routes_ + +| Method | Full path | Auth | Rate limit | Defined in | +|---|---|---|---|---| +| DELETE | `/api/v1/:appId` | protected | GENERAL (1000/15m) | `routes/apps/removal.js:39` | +| GET | `/api/v1/:appId/backup-points` | protected | GENERAL (1000/15m) | `routes/apps/restore.js:131` | +| POST | `/api/v1/:appId/restore` | protected | GENERAL (1000/15m) | `routes/apps/restore.js:38` | +| POST | `/api/v1/:appId/revert/:filename` | protected | GENERAL (1000/15m) | `routes/apps/restore.js:185` | +| POST | `/api/v1/arr/auto-setup` | protected | GENERAL (1000/15m) | `routes/arr/config.js:282` | +| POST | `/api/v1/arr/configure-overseerr` | protected | GENERAL (1000/15m) | `routes/arr/config.js:27` | +| GET | `/api/v1/arr/credentials` | protected | GENERAL (1000/15m) | `routes/arr/credentials.js:109` | +| POST | `/api/v1/arr/credentials` | protected | GENERAL (1000/15m) | `routes/arr/credentials.js:21` | +| DELETE | `/api/v1/arr/credentials/:service` | protected | GENERAL (1000/15m) | `routes/arr/credentials.js:134` | +| GET | `/api/v1/arr/detect` | protected | GENERAL (1000/15m) | `routes/arr/detect.js:20` | +| GET | `/api/v1/arr/quality-profiles` | protected | GENERAL (1000/15m) | `routes/arr/config.js:497` | +| POST | `/api/v1/arr/quality-profiles` | protected | GENERAL (1000/15m) | `routes/arr/config.js:566` | +| POST | `/api/v1/arr/smart-connect` | protected | GENERAL (1000/15m) | `routes/arr/smart-connect.js:26` | +| GET | `/api/v1/arr/smart-detect` | protected | GENERAL (1000/15m) | `routes/arr/detect.js:78` | +| POST | `/api/v1/arr/test-connection` | protected | GENERAL (1000/15m) | `routes/arr/config.js:208` | +| POST | `/api/v1/check-existing` | protected | GENERAL (1000/15m) | `routes/apps/deploy.js:241` | +| DELETE | `/api/v1/compose-stack/:stackName` | protected | GENERAL (1000/15m) | `routes/apps/compose.js:308` | +| POST | `/api/v1/deploy` | protected | GENERAL (1000/15m) | `routes/apps/deploy.js:254` | +| POST | `/api/v1/deploy-compose` | protected | GENERAL (1000/15m) | `routes/apps/compose.js:170` | +| POST | `/api/v1/import-compose` | protected | GENERAL (1000/15m) | `routes/apps/compose.js:159` | +| GET | `/api/v1/plex/libraries` | protected | GENERAL (1000/15m) | `routes/arr/plex.js:26` | +| GET | `/api/v1/ports/:basePort/suggest` | protected | GENERAL (1000/15m) | `routes/apps/templates.js:77` | +| GET | `/api/v1/ports/:port/check` | protected | GENERAL (1000/15m) | `routes/apps/templates.js:65` | +| POST | `/api/v1/restore-all` | protected | GENERAL (1000/15m) | `routes/apps/restore.js:58` | +| GET | `/api/v1/restore-status` | protected | GENERAL (1000/15m) | `routes/apps/restore.js:97` | +| GET | `/api/v1/templates` | protected | GENERAL (1000/15m) | `routes/apps/templates.js:45` | +| GET | `/api/v1/templates/:appId` | protected | GENERAL (1000/15m) | `routes/apps/templates.js:54` | +| POST | `/api/v1/update-subdomain` | protected | GENERAL (1000/15m) | `routes/apps/templates.js:91` | + +## Tailscale + +_20 routes_ + +| Method | Full path | Auth | Rate limit | Defined in | +|---|---|---|---|---| +| GET | `/api/v1/tailscale/acl` | public | GENERAL (1000/15m) | `routes/tailscale.js:301` | +| GET | `/api/v1/tailscale/admin/devices` | public | GENERAL (1000/15m) | `routes/tailscale-admin.js:154` | +| DELETE | `/api/v1/tailscale/admin/devices/:id` | public | GENERAL (1000/15m) | `routes/tailscale-admin.js:170` | +| GET | `/api/v1/tailscale/admin/keys` | public | GENERAL (1000/15m) | `routes/tailscale-admin.js:202` | +| POST | `/api/v1/tailscale/admin/keys` | public | GENERAL (1000/15m) | `routes/tailscale-admin.js:211` | +| DELETE | `/api/v1/tailscale/admin/keys/:id` | public | GENERAL (1000/15m) | `routes/tailscale-admin.js:237` | +| GET | `/api/v1/tailscale/admin/users` | public | GENERAL (1000/15m) | `routes/tailscale-admin.js:191` | +| GET | `/api/v1/tailscale/api-devices` | public | GENERAL (1000/15m) | `routes/tailscale.js:274` | +| GET | `/api/v1/tailscale/check-connection` | public | GENERAL (1000/15m) | `routes/tailscale.js:96` | +| POST | `/api/v1/tailscale/config` | public | GENERAL (1000/15m) | `routes/tailscale.js:80` | +| GET | `/api/v1/tailscale/devices` | public | GENERAL (1000/15m) | `routes/tailscale.js:113` | +| DELETE | `/api/v1/tailscale/oauth-config` | public | GENERAL (1000/15m) | `routes/tailscale.js:259` | +| POST | `/api/v1/tailscale/oauth-config` | public | GENERAL (1000/15m) | `routes/tailscale.js:201` | +| POST | `/api/v1/tailscale/protect-service` | public | GENERAL (1000/15m) | `routes/tailscale.js:147` | +| DELETE | `/api/v1/tailscale/settings` | public | GENERAL (1000/15m) | `routes/tailscale-admin.js:124` | +| GET | `/api/v1/tailscale/settings` | public | GENERAL (1000/15m) | `routes/tailscale-admin.js:60` | +| PUT | `/api/v1/tailscale/settings` | public | GENERAL (1000/15m) | `routes/tailscale-admin.js:76` | +| POST | `/api/v1/tailscale/settings/test` | public | GENERAL (1000/15m) | `routes/tailscale-admin.js:131` | +| GET | `/api/v1/tailscale/status` | public | GENERAL (1000/15m) | `routes/tailscale.js:36` | +| POST | `/api/v1/tailscale/sync` | public | GENERAL (1000/15m) | `routes/tailscale.js:287` | + +## Backups + +_19 routes_ + +| Method | Full path | Auth | Rate limit | Defined in | +|---|---|---|---|---| +| POST | `/api/v1/backups/backup/:appId` | protected | GENERAL (1000/15m) | `routes/backups.js:161` | +| POST | `/api/v1/backups/compare/:filename` | protected | GENERAL (1000/15m) | `routes/backups.js:373` | +| GET | `/api/v1/backups/config` | protected | GENERAL (1000/15m) | `routes/backups.js:480` | +| POST | `/api/v1/backups/config` | protected | GENERAL (1000/15m) | `routes/backups.js:486` | +| DELETE | `/api/v1/backups/credentials/:provider` | protected | GENERAL (1000/15m) | `routes/backups.js:631` | +| GET | `/api/v1/backups/credentials/:provider` | protected | GENERAL (1000/15m) | `routes/backups.js:558` | +| POST | `/api/v1/backups/credentials/:provider` | protected | GENERAL (1000/15m) | `routes/backups.js:590` | +| POST | `/api/v1/backups/execute` | protected | GENERAL (1000/15m) | `routes/backups.js:492` | +| GET | `/api/v1/backups/files` | protected | GENERAL (1000/15m) | `routes/backups.js:118` | +| GET | `/api/v1/backups/files/:appId` | protected | GENERAL (1000/15m) | `routes/backups.js:189` | +| GET | `/api/v1/backups/history` | protected | GENERAL (1000/15m) | `routes/backups.js:498` | +| POST | `/api/v1/backups/restore-file/:filename` | protected | GENERAL (1000/15m) | `routes/backups.js:237` | +| POST | `/api/v1/backups/restore/:backupId` | protected | GENERAL (1000/15m) | `routes/backups.js:538` | +| GET | `/api/v1/backups/schedule` | protected | GENERAL (1000/15m) | `routes/backups.js:29` | +| POST | `/api/v1/backups/schedule` | protected | GENERAL (1000/15m) | `routes/backups.js:59` | +| POST | `/api/v1/backups/schedule` | protected | GENERAL (1000/15m) | `routes/backups.js:511` | +| DELETE | `/api/v1/backups/schedule/:appId` | protected | GENERAL (1000/15m) | `routes/backups.js:102` | +| GET | `/api/v1/backups/storage-info` | protected | GENERAL (1000/15m) | `routes/backups.js:505` | +| POST | `/api/v1/backups/test-destination` | protected | GENERAL (1000/15m) | `routes/backups.js:546` | + +## DNS + +_19 routes_ + +| Method | Full path | Auth | Rate limit | Defined in | +|---|---|---|---|---| +| GET | `/api/v1/dns/check-update` | protected | GENERAL (1000/15m) | `routes/dns.js:669` | +| DELETE | `/api/v1/dns/credentials` | protected | GENERAL (1000/15m) | `routes/dns.js:597` | +| POST | `/api/v1/dns/credentials` | protected | GENERAL (1000/15m) | `routes/dns.js:490` | +| GET | `/api/v1/dns/logs` | protected | GENERAL (1000/15m) | `routes/dns.js:337` | +| GET | `/api/v1/dns/propagation` | protected | GENERAL (1000/15m) | `routes/dns.js:802` | +| GET | `/api/v1/dns/propagation/:domain` | protected | GENERAL (1000/15m) | `routes/dns.js:847` | +| POST | `/api/v1/dns/propagation/verify` | protected | GENERAL (1000/15m) | `routes/dns.js:815` | +| GET | `/api/v1/dns/provider/status` | protected | GENERAL (1000/15m) | `routes/dns.js:55` | +| GET | `/api/v1/dns/providers` | protected | GENERAL (1000/15m) | `routes/dns.js:48` | +| DELETE | `/api/v1/dns/record` | protected | GENERAL (1000/15m) | `routes/dns.js:176` | +| POST | `/api/v1/dns/record` | protected | GENERAL (1000/15m) | `routes/dns.js:225` | +| POST | `/api/v1/dns/refresh-token` | protected | GENERAL (1000/15m) | `routes/dns.js:655` | +| GET | `/api/v1/dns/resolve` | protected | GENERAL (1000/15m) | `routes/dns.js:292` | +| POST | `/api/v1/dns/restart/:dnsId` | protected | GENERAL (1000/15m) | `routes/dns.js:621` | +| GET | `/api/v1/dns/token-status` | protected | GENERAL (1000/15m) | `routes/dns.js:474` | +| DELETE | `/api/v1/dns/universal/record` | protected | GENERAL (1000/15m) | `routes/dns.js:119` | +| POST | `/api/v1/dns/universal/record` | protected | GENERAL (1000/15m) | `routes/dns.js:71` | +| GET | `/api/v1/dns/universal/resolve` | protected | GENERAL (1000/15m) | `routes/dns.js:145` | +| POST | `/api/v1/dns/update` | protected | GENERAL (1000/15m) | `routes/dns.js:732` | + +## Monitoring + +_19 routes_ + +| Method | Full path | Auth | Rate limit | Defined in | +|---|---|---|---|---| +| GET | `/api/v1/certificates` | protected | GENERAL (1000/15m) | `routes/ssl-monitor.js:26` | +| GET | `/api/v1/certificates/:serviceId` | protected | GENERAL (1000/15m) | `routes/ssl-monitor.js:35` | +| POST | `/api/v1/check` | protected | GENERAL (1000/15m) | `routes/ssl-monitor.js:50` | +| POST | `/api/v1/check/:serviceId` | protected | GENERAL (1000/15m) | `routes/ssl-monitor.js:59` | +| GET | `/api/v1/config` | public | GENERAL (1000/15m) | `routes/ssl-monitor.js:80` | +| POST | `/api/v1/config` | public | GENERAL (1000/15m) | `routes/ssl-monitor.js:90` | +| GET | `/api/v1/monitoring/aggregated/:containerId` | protected | GENERAL (1000/15m) | `routes/monitoring.js:72` | +| GET | `/api/v1/monitoring/alerts` | protected | GENERAL (1000/15m) | `routes/monitoring.js:104` | +| DELETE | `/api/v1/monitoring/alerts/:containerId` | protected | GENERAL (1000/15m) | `routes/monitoring.js:174` | +| GET | `/api/v1/monitoring/alerts/:containerId` | protected | GENERAL (1000/15m) | `routes/monitoring.js:168` | +| POST | `/api/v1/monitoring/alerts/:containerId` | protected | GENERAL (1000/15m) | `routes/monitoring.js:162` | +| POST | `/api/v1/monitoring/alerts/:containerId/test` | protected | GENERAL (1000/15m) | `routes/monitoring.js:111` | +| GET | `/api/v1/monitoring/alerts/config` | protected | GENERAL (1000/15m) | `routes/monitoring.js:85` | +| POST | `/api/v1/monitoring/alerts/config` | protected | GENERAL (1000/15m) | `routes/monitoring.js:91` | +| GET | `/api/v1/monitoring/history/:containerId` | protected | GENERAL (1000/15m) | `routes/monitoring.js:49` | +| GET | `/api/v1/monitoring/stats` | public | GENERAL (1000/15m) | `routes/monitoring.js:20` | +| GET | `/api/v1/monitoring/stats/:containerId` | protected | GENERAL (1000/15m) | `routes/monitoring.js:38` | +| GET | `/api/v1/stats/container/:id` | protected | GENERAL (1000/15m) | `routes/monitoring.js:240` | +| GET | `/api/v1/stats/containers` | protected | GENERAL (1000/15m) | `routes/monitoring.js:182` | + +## Updates + +_16 routes_ + +| Method | Full path | Auth | Rate limit | Defined in | +|---|---|---|---|---| +| POST | `/api/v1/system/rollback` | protected | GENERAL (1000/15m) | `routes/updates.js:164` | +| GET | `/api/v1/system/rollback-versions` | protected | GENERAL (1000/15m) | `routes/updates.js:158` | +| POST | `/api/v1/system/update-apply` | protected | GENERAL (1000/15m) | `routes/updates.js:95` | +| GET | `/api/v1/system/update-check` | public | GENERAL (1000/15m) | `routes/updates.js:89` | +| GET | `/api/v1/system/update-history` | public | GENERAL (1000/15m) | `routes/updates.js:152` | +| POST | `/api/v1/system/update-notify` | public | GENERAL (1000/15m) | `routes/updates.js:126` | +| GET | `/api/v1/system/update-status` | public | GENERAL (1000/15m) | `routes/updates.js:143` | +| GET | `/api/v1/system/version` | public | GENERAL (1000/15m) | `routes/updates.js:83` | +| GET | `/api/v1/updates/auto-update` | protected | GENERAL (1000/15m) | `routes/updates.js:65` | +| POST | `/api/v1/updates/auto-update/:containerId` | protected | GENERAL (1000/15m) | `routes/updates.js:59` | +| GET | `/api/v1/updates/available` | public | GENERAL (1000/15m) | `routes/updates.js:29` | +| POST | `/api/v1/updates/check` | protected | GENERAL (1000/15m) | `routes/updates.js:22` | +| GET | `/api/v1/updates/history` | protected | GENERAL (1000/15m) | `routes/updates.js:49` | +| POST | `/api/v1/updates/rollback/:containerId` | protected | GENERAL (1000/15m) | `routes/updates.js:43` | +| POST | `/api/v1/updates/schedule/:containerId` | protected | GENERAL (1000/15m) | `routes/updates.js:71` | +| POST | `/api/v1/updates/update/:containerId` | protected | GENERAL (1000/15m) | `routes/updates.js:37` | + +## Authentication + +_15 routes_ + +| Method | Full path | Auth | Rate limit | Defined in | +|---|---|---|---|---| +| GET | `/api/v1/auth/app-token/:serviceId` | public | STRICT (20/15m) | `routes/auth/sso-gate.js:104` | +| GET | `/api/v1/auth/gate/:serviceId` | public | STRICT (20/15m) | `routes/auth/sso-gate.js:26` | +| POST | `/api/v1/auth/jwt` | protected | STRICT (20/15m) | `routes/auth/keys.js:103` | +| GET | `/api/v1/auth/keys` | protected | STRICT (20/15m) | `routes/auth/keys.js:36` | +| POST | `/api/v1/auth/keys` | protected | STRICT (20/15m) | `routes/auth/keys.js:47` | +| DELETE | `/api/v1/auth/keys/:keyId` | protected | STRICT (20/15m) | `routes/auth/keys.js:81` | +| GET | `/api/v1/auth/login-page` | public | GENERAL (1000/15m) | `routes/auth/sso-gate.js:206` | +| GET | `/api/v1/totp/check-session` | public | TOTP (10/15m) | `routes/auth/totp.js:228` | +| GET | `/api/v1/totp/config` | public | TOTP (10/15m) | `routes/auth/totp.js:30` | +| POST | `/api/v1/totp/config` | public | TOTP (10/15m) | `routes/auth/totp.js:286` | +| POST | `/api/v1/totp/disable` | protected | TOTP (10/15m) | `routes/auth/totp.js:253` | +| GET | `/api/v1/totp/recovery-info` | public | TOTP (10/15m) | `routes/auth/totp.js:56` | +| POST | `/api/v1/totp/setup` | public | TOTP (10/15m) | `routes/auth/totp.js:116` | +| POST | `/api/v1/totp/verify` | public | TOTP (10/15m) | `routes/auth/totp.js:194` | +| POST | `/api/v1/totp/verify-setup` | public | TOTP (10/15m) | `routes/auth/totp.js:157` | + +## Logs + +_15 routes_ + +| Method | Full path | Auth | Rate limit | Defined in | +|---|---|---|---|---| +| DELETE | `/api/v1/audit-logs` | protected | GENERAL (1000/15m) | `routes/errorlogs.js:71` | +| GET | `/api/v1/audit-logs` | protected | GENERAL (1000/15m) | `routes/errorlogs.js:55` | +| DELETE | `/api/v1/error-logs` | protected | GENERAL (1000/15m) | `routes/errorlogs.js:47` | +| GET | `/api/v1/error-logs` | protected | GENERAL (1000/15m) | `routes/errorlogs.js:20` | +| GET | `/api/v1/logs/container/:id` | protected | GENERAL (1000/15m) | `routes/logs.js:39` | +| GET | `/api/v1/logs/containers` | protected | GENERAL (1000/15m) | `routes/logs.js:23` | +| GET | `/api/v1/logs/digest/:date` | protected | GENERAL (1000/15m) | `routes/logs.js:184` | +| POST | `/api/v1/logs/digest/generate` | protected | GENERAL (1000/15m) | `routes/logs.js:176` | +| GET | `/api/v1/logs/digest/history` | protected | GENERAL (1000/15m) | `routes/logs.js:169` | +| GET | `/api/v1/logs/digest/latest` | protected | GENERAL (1000/15m) | `routes/logs.js:152` | +| GET | `/api/v1/logs/digest/live` | protected | GENERAL (1000/15m) | `routes/logs.js:162` | +| GET | `/api/v1/logs/docker-disk` | protected | GENERAL (1000/15m) | `routes/logs.js:203` | +| POST | `/api/v1/logs/docker-maintenance` | protected | GENERAL (1000/15m) | `routes/logs.js:211` | +| GET | `/api/v1/logs/file` | protected | GENERAL (1000/15m) | `routes/logs.js:218` | +| GET | `/api/v1/logs/stream/:id` | protected | GENERAL (1000/15m) | `routes/logs.js:93` | + +## Configuration + +_13 routes_ + +| Method | Full path | Auth | Rate limit | Defined in | +|---|---|---|---|---| +| POST | `/api/v1/assets/upload` | protected | GENERAL (1000/15m) | `routes/config/assets.js:33` | +| GET | `/api/v1/backup/export` | protected | GENERAL (1000/15m) | `routes/config/backup.js:51` | +| POST | `/api/v1/backup/preview` | protected | GENERAL (1000/15m) | `routes/config/backup.js:153` | +| POST | `/api/v1/backup/restore` | protected | GENERAL (1000/15m) | `routes/config/backup.js:218` | +| DELETE | `/api/v1/config` | public | GENERAL (1000/15m) | `routes/config/settings.js:78` | +| GET | `/api/v1/config` | public | GENERAL (1000/15m) | `routes/config/settings.js:26` | +| POST | `/api/v1/config` | public | GENERAL (1000/15m) | `routes/config/settings.js:35` | +| DELETE | `/api/v1/favicon` | public | GENERAL (1000/15m) | `routes/config/assets.js:272` | +| GET | `/api/v1/favicon` | public | GENERAL (1000/15m) | `routes/config/assets.js:203` | +| POST | `/api/v1/favicon` | public | GENERAL (1000/15m) | `routes/config/assets.js:212` | +| DELETE | `/api/v1/logo` | public | GENERAL (1000/15m) | `routes/config/assets.js:170` | +| GET | `/api/v1/logo` | public | GENERAL (1000/15m) | `routes/config/assets.js:77` | +| POST | `/api/v1/logo` | public | GENERAL (1000/15m) | `routes/config/assets.js:112` | + +## Health + +_12 routes_ + +| Method | Full path | Auth | Rate limit | Defined in | +|---|---|---|---|---| +| DELETE | `/api/v1/health-checks/:serviceId/configure` | protected | GENERAL (1000/15m) | `routes/health.js:357` | +| POST | `/api/v1/health-checks/:serviceId/configure` | protected | GENERAL (1000/15m) | `routes/health.js:351` | +| GET | `/api/v1/health-checks/:serviceId/stats` | protected | GENERAL (1000/15m) | `routes/health.js:340` | +| GET | `/api/v1/health-checks/incidents` | protected | GENERAL (1000/15m) | `routes/health.js:363` | +| GET | `/api/v1/health-checks/incidents/history` | protected | GENERAL (1000/15m) | `routes/health.js:371` | +| GET | `/api/v1/health-checks/status` | public | GENERAL (1000/15m) | `routes/health.js:319` | +| GET | `/api/v1/health/ca` | public | GENERAL (1000/15m) | `routes/health.js:267` | +| GET | `/api/v1/health/cached` | protected | GENERAL (1000/15m) | `routes/health.js:179` | +| GET | `/api/v1/health/probe` | protected | GENERAL (1000/15m) | `routes/health.js:230` | +| GET | `/api/v1/health/pylon` | protected | GENERAL (1000/15m) | `routes/health.js:245` | +| GET | `/api/v1/health/service/:id` | protected | GENERAL (1000/15m) | `routes/health.js:188` | +| GET | `/api/v1/health/services` | protected | GENERAL (1000/15m) | `routes/health.js:109` | + +## Services + +_12 routes_ + +| Method | Full path | Auth | Rate limit | Defined in | +|---|---|---|---|---| +| DELETE | `/api/v1/seedhost-creds` | protected | GENERAL (1000/15m) | `routes/services.js:310` | +| GET | `/api/v1/seedhost-creds` | protected | GENERAL (1000/15m) | `routes/services.js:289` | +| POST | `/api/v1/seedhost-creds` | protected | GENERAL (1000/15m) | `routes/services.js:272` | +| GET | `/api/v1/services` | public | GENERAL (1000/15m) | `routes/services.js:374` | +| POST | `/api/v1/services` | public | GENERAL (1000/15m) | `routes/services.js:389` | +| PUT | `/api/v1/services` | public | GENERAL (1000/15m) | `routes/services.js:434` | +| DELETE | `/api/v1/services/:id` | protected | GENERAL (1000/15m) | `routes/services.js:462` | +| DELETE | `/api/v1/services/:serviceId/credentials` | protected | GENERAL (1000/15m) | `routes/services.js:237` | +| GET | `/api/v1/services/:serviceId/credentials` | protected | GENERAL (1000/15m) | `routes/services.js:252` | +| POST | `/api/v1/services/:serviceId/credentials` | protected | GENERAL (1000/15m) | `routes/services.js:213` | +| GET | `/api/v1/services/status` | public | GENERAL (1000/15m) | `routes/services.js:327` | +| POST | `/api/v1/services/update` | protected | GENERAL (1000/15m) | `routes/services.js:486` | + +## Containers (lifecycle) + +_10 routes_ + +| Method | Full path | Auth | Rate limit | Defined in | +|---|---|---|---|---| +| DELETE | `/api/v1/containers/:id` | protected | GENERAL (1000/15m) | `routes/containers.js:235` | +| GET | `/api/v1/containers/:id/check-update` | protected | GENERAL (1000/15m) | `routes/containers.js:155` | +| GET | `/api/v1/containers/:id/logs` | protected | GENERAL (1000/15m) | `routes/containers.js:193` | +| GET | `/api/v1/containers/:id/resources` | protected | GENERAL (1000/15m) | `routes/containers.js:223` | +| PUT | `/api/v1/containers/:id/resources` | protected | GENERAL (1000/15m) | `routes/containers.js:205` | +| POST | `/api/v1/containers/:id/restart` | protected | GENERAL (1000/15m) | `routes/containers.js:48` | +| POST | `/api/v1/containers/:id/start` | protected | GENERAL (1000/15m) | `routes/containers.js:34` | +| POST | `/api/v1/containers/:id/stop` | protected | GENERAL (1000/15m) | `routes/containers.js:41` | +| POST | `/api/v1/containers/:id/update` | protected | GENERAL (1000/15m) | `routes/containers.js:55` | +| GET | `/api/v1/containers/discover` | protected | GENERAL (1000/15m) | `routes/containers.js:242` | + +## Core / system + +_9 routes_ + +| Method | Full path | Auth | Rate limit | Defined in | +|---|---|---|---|---| +| GET | `/api/v1/docs` | protected | GENERAL (1000/15m) | `src/app.js:925` | +| GET | `/api/v1/docs/spec` | protected | GENERAL (1000/15m) | `src/app.js:943` | +| GET | `/api/v1/network/ips` | protected | GENERAL (1000/15m) | `src/app.js:899` | +| GET | `/health` | public | GENERAL (1000/15m) | `src/app.js:777` | +| GET | `/health/live` | public | GENERAL (1000/15m) | `src/app.js:778` | +| GET | `/health/ready` | public | GENERAL (1000/15m) | `src/app.js:782` | +| GET | `/healthz` | public | GENERAL (1000/15m) | `src/app.js:779` | +| GET | `/probe/:id` | public | GENERAL (1000/15m) | `src/app.js:786` | +| GET | `/readyz` | public | GENERAL (1000/15m) | `src/app.js:783` | + +## Dependencies + +_8 routes_ + +| Method | Full path | Auth | Rate limit | Defined in | +|---|---|---|---|---| +| DELETE | `/api/v1/dependencies/:serviceId` | protected | GENERAL (1000/15m) | `routes/dependencies.js:166` | +| GET | `/api/v1/dependencies/:serviceId` | protected | GENERAL (1000/15m) | `routes/dependencies.js:80` | +| POST | `/api/v1/dependencies/:serviceId` | protected | GENERAL (1000/15m) | `routes/dependencies.js:123` | +| GET | `/api/v1/dependencies/:serviceId/chain` | protected | GENERAL (1000/15m) | `routes/dependencies.js:105` | +| POST | `/api/v1/dependencies/:serviceId/restart` | protected | GENERAL (1000/15m) | `routes/dependencies.js:198` | +| GET | `/api/v1/dependencies/:serviceId/status` | protected | GENERAL (1000/15m) | `routes/dependencies.js:114` | +| GET | `/api/v1/dependencies/graph` | protected | GENERAL (1000/15m) | `routes/dependencies.js:48` | +| GET | `/api/v1/dependencies/validate` | protected | GENERAL (1000/15m) | `routes/dependencies.js:56` | + +## Notifications + +_8 routes_ + +| Method | Full path | Auth | Rate limit | Defined in | +|---|---|---|---|---| +| GET | `/api/v1/notifications/config` | protected | GENERAL (1000/15m) | `routes/notifications.js:20` | +| POST | `/api/v1/notifications/config` | protected | GENERAL (1000/15m) | `routes/notifications.js:53` | +| POST | `/api/v1/notifications/health-check` | protected | GENERAL (1000/15m) | `routes/notifications.js:214` | +| DELETE | `/api/v1/notifications/history` | protected | GENERAL (1000/15m) | `routes/notifications.js:208` | +| GET | `/api/v1/notifications/history` | protected | GENERAL (1000/15m) | `routes/notifications.js:192` | +| POST | `/api/v1/notifications/send` | protected | GENERAL (1000/15m) | `routes/notifications.js:246` | +| GET | `/api/v1/notifications/status` | protected | GENERAL (1000/15m) | `routes/notifications.js:224` | +| POST | `/api/v1/notifications/test` | protected | GENERAL (1000/15m) | `routes/notifications.js:159` | + +## App recipes + +_8 routes_ + +| Method | Full path | Auth | Rate limit | Defined in | +|---|---|---|---|---| +| DELETE | `/api/v1/:recipeId` | protected | GENERAL (1000/15m) | `routes/recipes/manage.js:197` | +| POST | `/api/v1/:recipeId/restart` | protected | GENERAL (1000/15m) | `routes/recipes/manage.js:171` | +| POST | `/api/v1/:recipeId/start` | protected | GENERAL (1000/15m) | `routes/recipes/manage.js:108` | +| POST | `/api/v1/:recipeId/stop` | protected | GENERAL (1000/15m) | `routes/recipes/manage.js:139` | +| POST | `/api/v1/deploy` | protected | GENERAL (1000/15m) | `routes/recipes/deploy.js:29` | +| GET | `/api/v1/deployed` | protected | GENERAL (1000/15m) | `routes/recipes/manage.js:24` | +| GET | `/api/v1/templates` | protected | GENERAL (1000/15m) | `routes/recipes/index.js:34` | +| GET | `/api/v1/templates/:recipeId` | protected | GENERAL (1000/15m) | `routes/recipes/index.js:63` | + +## Docker resources + +_7 routes_ + +| Method | Full path | Auth | Rate limit | Defined in | +|---|---|---|---|---| +| GET | `/api/v1/docker/disk-usage` | protected | GENERAL (1000/15m) | `routes/docker-resources.js:84` | +| GET | `/api/v1/docker/networks` | protected | GENERAL (1000/15m) | `routes/docker-resources.js:50` | +| POST | `/api/v1/docker/networks` | protected | GENERAL (1000/15m) | `routes/docker-resources.js:64` | +| DELETE | `/api/v1/docker/networks/:id` | protected | GENERAL (1000/15m) | `routes/docker-resources.js:76` | +| GET | `/api/v1/docker/volumes` | protected | GENERAL (1000/15m) | `routes/docker-resources.js:17` | +| POST | `/api/v1/docker/volumes` | protected | GENERAL (1000/15m) | `routes/docker-resources.js:30` | +| DELETE | `/api/v1/docker/volumes/:name` | protected | GENERAL (1000/15m) | `routes/docker-resources.js:42` | + +## Caddy / sites + +_7 routes_ + +| Method | Full path | Auth | Rate limit | Defined in | +|---|---|---|---|---| +| GET | `/api/v1/caddy/cas` | protected | GENERAL (1000/15m) | `routes/sites.js:57` | +| GET | `/api/v1/caddy/config` | protected | GENERAL (1000/15m) | `routes/sites.js:31` | +| POST | `/api/v1/caddy/reload` | protected | GENERAL (1000/15m) | `routes/sites.js:38` | +| GET | `/api/v1/caddyfile` | protected | GENERAL (1000/15m) | `routes/sites.js:25` | +| POST | `/api/v1/site` | protected | GENERAL (1000/15m) | `routes/sites.js:160` | +| DELETE | `/api/v1/site/:domain` | protected | GENERAL (1000/15m) | `routes/sites.js:135` | +| POST | `/api/v1/site/external` | protected | GENERAL (1000/15m) | `routes/sites.js:188` | + +## Updates / workflows + +_6 routes_ + +| Method | Full path | Auth | Rate limit | Defined in | +|---|---|---|---|---| +| GET | `/api/v1/workflows/workflows` | protected | GENERAL (1000/15m) | `routes/workflows.js:22` | +| POST | `/api/v1/workflows/workflows/:workflowId/disable` | protected | GENERAL (1000/15m) | `routes/workflows.js:35` | +| POST | `/api/v1/workflows/workflows/:workflowId/enable` | protected | GENERAL (1000/15m) | `routes/workflows.js:28` | +| GET | `/api/v1/workflows/workflows/:workflowId/history` | protected | GENERAL (1000/15m) | `routes/workflows.js:52` | +| POST | `/api/v1/workflows/workflows/:workflowId/run` | protected | GENERAL (1000/15m) | `routes/workflows.js:42` | +| GET | `/api/v1/workflows/workflows/history` | protected | GENERAL (1000/15m) | `routes/workflows.js:60` | + +## Auto-restart + +_5 routes_ + +| Method | Full path | Auth | Rate limit | Defined in | +|---|---|---|---|---| +| GET | `/api/v1/policies` | protected | GENERAL (1000/15m) | `routes/auto-restart.js:30` | +| DELETE | `/api/v1/policies/:serviceId` | protected | GENERAL (1000/15m) | `routes/auto-restart.js:103` | +| GET | `/api/v1/policies/:serviceId` | protected | GENERAL (1000/15m) | `routes/auto-restart.js:39` | +| POST | `/api/v1/policies/:serviceId` | protected | GENERAL (1000/15m) | `routes/auto-restart.js:60` | +| POST | `/api/v1/policies/:serviceId/test` | protected | GENERAL (1000/15m) | `routes/auto-restart.js:123` | + +## Certificate authority + +_5 routes_ + +| Method | Full path | Auth | Rate limit | Defined in | +|---|---|---|---|---| +| GET | `/api/v1/ca/cert/:domain` | public | GENERAL (1000/15m) | `routes/ca.js:127` | +| GET | `/api/v1/ca/certs` | public | GENERAL (1000/15m) | `routes/ca.js:242` | +| GET | `/api/v1/ca/info` | public | GENERAL (1000/15m) | `routes/ca.js:15` | +| GET | `/api/v1/ca/install-script` | public | GENERAL (1000/15m) | `routes/ca.js:63` | +| GET | `/api/v1/ca/root.crt` | public | GENERAL (1000/15m) | `routes/ca.js:45` | + +## OpenClaw integration + +_5 routes_ + +| Method | Full path | Auth | Rate limit | Defined in | +|---|---|---|---|---| +| DELETE | `/api/v1/openclaw/` | protected | GENERAL (1000/15m) | `routes/openclaw.js:244` | +| POST | `/api/v1/openclaw/deploy` | protected | GENERAL (1000/15m) | `routes/openclaw.js:150` | +| GET | `/api/v1/openclaw/proxy/*` | protected | GENERAL (1000/15m) | `routes/openclaw.js:216` | +| POST | `/api/v1/openclaw/proxy/*` | protected | GENERAL (1000/15m) | `routes/openclaw.js:230` | +| GET | `/api/v1/openclaw/status` | protected | GENERAL (1000/15m) | `routes/openclaw.js:116` | + +## Config drift + +_4 routes_ + +| Method | Full path | Auth | Rate limit | Defined in | +|---|---|---|---|---| +| POST | `/api/v1/fix` | protected | GENERAL (1000/15m) | `routes/config-drift.js:51` | +| GET | `/api/v1/last` | protected | GENERAL (1000/15m) | `routes/config-drift.js:39` | +| POST | `/api/v1/polling` | protected | GENERAL (1000/15m) | `routes/config-drift.js:66` | +| GET | `/api/v1/report` | protected | GENERAL (1000/15m) | `routes/config-drift.js:30` | + +## Licensing + +_4 routes_ + +| Method | Full path | Auth | Rate limit | Defined in | +|---|---|---|---|---| +| POST | `/api/v1/license/activate` | protected | GENERAL (1000/15m) | `routes/license.js:16` | +| POST | `/api/v1/license/deactivate` | protected | GENERAL (1000/15m) | `routes/license.js:41` | +| GET | `/api/v1/license/feature/:feature` | public | GENERAL (1000/15m) | `routes/license.js:52` | +| GET | `/api/v1/license/status` | public | GENERAL (1000/15m) | `routes/license.js:35` | + +## File browser + +_3 routes_ + +| Method | Full path | Auth | Rate limit | Defined in | +|---|---|---|---|---| +| GET | `/api/v1/browse/directories` | protected | GENERAL (1000/15m) | `routes/browse.js:52` | +| GET | `/api/v1/browse/roots` | protected | GENERAL (1000/15m) | `routes/browse.js:34` | +| GET | `/api/v1/media/detected-mounts` | protected | GENERAL (1000/15m) | `routes/browse.js:137` | + +## Theming + +_3 routes_ + +| Method | Full path | Auth | Rate limit | Defined in | +|---|---|---|---|---| +| GET | `/api/v1/themes` | public | GENERAL (1000/15m) | `routes/themes.js:40` | +| DELETE | `/api/v1/themes/:slug` | protected | GENERAL (1000/15m) | `routes/themes.js:65` | +| POST | `/api/v1/themes/:slug` | protected | GENERAL (1000/15m) | `routes/themes.js:45` | + +## Service credentials + +_2 routes_ + +| Method | Full path | Auth | Rate limit | Defined in | +|---|---|---|---|---| +| GET | `/api/v1/credentials/list` | protected | GENERAL (1000/15m) | `routes/credentials.js:15` | +| POST | `/api/v1/credentials/rotate-key` | protected | GENERAL (1000/15m) | `routes/credentials.js:21` | + +## Events + +_2 routes_ + +| Method | Full path | Auth | Rate limit | Defined in | +|---|---|---|---|---| +| GET | `/api/v1/events/clients` | protected | GENERAL (1000/15m) | `routes/events.js:154` | +| GET | `/api/v1/events/stream` | protected | GENERAL (1000/15m) | `routes/events.js:126` | + +## Internal helpers + +_1 routes_ + +| Method | Full path | Auth | Rate limit | Defined in | +|---|---|---|---|---| +| GET | `/api/v1/status` | protected | GENERAL (1000/15m) | `routes/context.js:8` | + +## Mount Point Map + +How `src/app.js` wires route files to URL prefixes (via `apiRouter.use`): + +| Route file(s) | Mounted at | +|---|---| +| `routes/ca` | `/api/v1/ca` | +| `routes/containers` | `/api/v1/containers` | +| `routes/dependencies` | `/api/v1/dependencies` | +| `routes/dns` | `/api/v1/dns` | +| `routes/docker-resources` | `/api/v1/docker` | +| `routes/events` | `/api/v1/events` | +| `routes/license` | `/api/v1/license` | +| `routes/notifications` | `/api/v1/notifications` | +| `routes/openclaw` | `/api/v1/openclaw` | +| `routes/recipes/` | `/api/v1/recipes` | +| `routes/tailscale` | `/api/v1/tailscale` | +| `routes/tailscale-admin` | `/api/v1/tailscale` | +| `routes/workflows` | `/api/v1/workflows` | +| `routes/auth/` | `/api/v1 (root)` | +| `routes/config/` | `/api/v1 (root)` | +| `routes/services` | `/api/v1 (root)` | +| `routes/health` | `/api/v1 (root)` | +| `routes/monitoring` | `/api/v1 (root)` | +| `routes/updates` | `/api/v1 (root)` | +| `routes/sites` | `/api/v1 (root)` | +| `routes/credentials` | `/api/v1 (root)` | +| `routes/arr/` | `/api/v1 (root)` | +| `routes/apps/` | `/api/v1 (root)` | +| `routes/logs` | `/api/v1 (root)` | +| `routes/backups` | `/api/v1 (root)` | +| `routes/browse` | `/api/v1 (root)` | +| `routes/errorlogs` | `/api/v1 (root)` | +| `routes/themes` | `/api/v1 (root)` | +| `routes/auto-restart` | `/api/v1 (root)` | +| `routes/config-drift` | `/api/v1 (root)` | +| `routes/ssl-monitor` | `/api/v1 (root)` | + +## PUBLIC_ROUTES Allowlist + +Source: `src/utilities/middleware.js:310-364` (42 entries) + +| Method | Path | Match | +|---|---|---| +| ANY | `/health` | exact | +| ANY | `/health/live` | exact | +| ANY | `/health/ready` | exact | +| ANY | `/healthz` | exact | +| ANY | `/readyz` | exact | +| ANY | `/probe/` | prefix | +| ANY | `/api/v1/tailscale/` | prefix | +| ANY | `/api/v1/totp/config` | exact | +| ANY | `/api/v1/totp/recovery-info` | exact | +| ANY | `/api/v1/totp/verify` | exact | +| ANY | `/api/v1/totp/setup` | exact | +| ANY | `/api/v1/totp/verify-setup` | exact | +| ANY | `/api/v1/totp/check-session` | exact | +| ANY | `/api/v1/auth/gate/` | prefix | +| ANY | `/api/v1/auth/app-token/` | prefix | +| ANY | `/api/v1/auth/login-page` | exact | +| ANY | `/api/v1/services` | exact | +| ANY | `/api/v1/ca/info` | exact | +| ANY | `/api/v1/ca/root.crt` | exact | +| ANY | `/api/v1/ca/install-script` | exact | +| ANY | `/api/v1/health/ca` | exact | +| GET | `/api/v1/ca/cert/` | prefix | +| ANY | `/api/v1/ca/certs` | exact | +| ANY | `/api/v1/csrf-token` | exact | +| ANY | `/api/v1/logo` | exact | +| ANY | `/api/v1/favicon` | exact | +| ANY | `/api/v1/themes` | exact | +| ANY | `/api/v1/license/status` | exact | +| GET | `/api/v1/license/feature/` | prefix | +| ANY | `/api/v1/config` | exact | +| ANY | `/api/v1/services/status` | exact | +| ANY | `/api/v1/health-checks/status` | exact | +| ANY | `/api/v1/monitoring/stats` | exact | +| ANY | `/api/v1/system/version` | exact | +| ANY | `/api/v1/system/update-status` | exact | +| ANY | `/api/v1/system/update-history` | exact | +| ANY | `/api/v1/system/update-check` | exact | +| ANY | `/api/v1/updates/available` | exact | +| ANY | `/api/v1/system/update-notify` | exact | +| ANY | `/api/v1/monitoring/stats` | exact | +| ANY | `/api/v1/health-checks/status` | exact | +| ANY | `/api/v1/version` | exact | + +## Root-Level Endpoints (defined directly in src/app.js) + +| Method | Path | Auth | Purpose | +|---|---|---|---| +| GET | `/health` | public | Liveness (alias for `/health/live`) | +| GET | `/health/live` | public | Process-only check, no I/O | +| GET | `/health/ready` | public | Checks config + services + Docker + Caddy-admin (3s timeout each) | +| GET | `/healthz` | public | k8s alias for `/health/live` | +| GET | `/readyz` | public | k8s alias for `/health/ready` | +| GET | `/probe/:id` | public | Per-service health probe, sets `X-DashCaddy-HealthCheck: 1` | +| GET | `/api/v1/network/ips` | protected | Detected network interfaces + IPs (cached) | +| GET | `/api/v1/docs` | protected | Interactive Swagger UI | +| GET | `/api/v1/docs/spec` | protected | Raw OpenAPI 3.0.3 spec | +| GET | `/api/v1/version` | public (per PUBLIC_ROUTES) | API version | + +## OpenAPI Spec Cross-Check + +- Routes defined in code: **236** +- Paths in `openapi.yaml`: **112** + +### In code but NOT documented in OpenAPI (142) + +- `/api/v1/:appId` +- `/api/v1/:appId/backup-points` +- `/api/v1/:appId/restore` +- `/api/v1/:appId/revert/:filename` +- `/api/v1/:recipeId` +- `/api/v1/:recipeId/restart` +- `/api/v1/:recipeId/start` +- `/api/v1/:recipeId/stop` +- `/api/v1/arr/quality-profiles` +- `/api/v1/audit-logs` +- `/api/v1/auth/jwt` +- `/api/v1/auth/keys` +- `/api/v1/auth/keys/:keyId` +- `/api/v1/auth/login-page` +- `/api/v1/backups/backup/:appId` +- `/api/v1/backups/compare/:filename` +- `/api/v1/backups/credentials/:provider` +- `/api/v1/backups/files` +- `/api/v1/backups/files/:appId` +- `/api/v1/backups/restore-file/:filename` +- `/api/v1/backups/schedule` +- `/api/v1/backups/schedule/:appId` +- `/api/v1/backups/storage-info` +- `/api/v1/backups/test-destination` +- `/api/v1/browse/directories` +- `/api/v1/ca/cert/:domain` +- `/api/v1/ca/certs` +- `/api/v1/ca/info` +- `/api/v1/ca/install-script` +- `/api/v1/ca/root.crt` +- ... and 112 more + +### Documented but NOT in code (18) + +- `/api/v1/apps/:appId` +- `/api/v1/apps/check-existing` +- `/api/v1/apps/check-port/:port` +- `/api/v1/apps/deploy` +- `/api/v1/apps/suggest-port/:basePort` +- `/api/v1/apps/templates` +- `/api/v1/apps/templates/:appId` +- `/api/v1/apps/update-subdomain` +- `/api/v1/audit-log` +- `/api/v1/browse/dir` +- `/api/v1/caddy/get-cas` +- `/api/v1/health` +- `/api/v1/health-check/configure/:serviceId` +- `/api/v1/health-check/incidents` +- `/api/v1/health-check/incidents/history` +- `/api/v1/health-check/stats/:serviceId` +- `/api/v1/health-check/status` +- `/api/v1/service-creds/:serviceId`