DC-043: tailscale coordination API client + admin/settings routes
CI / Test & Lint (push) Has been cancelled
CI / Security audit (push) Has been cancelled

Companion to DC-042 (tailscale-manager). Adds the write-side of Tailscale
integration — REST client for api.tailscale.com that lets DashCaddy
manage its own tailnet (devices, pre-auth keys, users, ACL).

src/managers/tailscale-coord.js — new module:
  * Ping, list/get/delete devices, create/list/delete pre-auth keys,
    list users, get/update ACL
  * 5min read cache, 60s device-list cache, 1hr ping cache
  * Cache invalidation on writes
  * Graceful {configured:false} when no token
  * TailscaleCoordError class with code mapping (unauthorized, not_found,
    rate_limited, server_error)
  * fetchImpl injection point for tests; native https in production
  * 45 unit tests covering all paths

src/context/index.js — new tailscaleCoord namespace:
  * getClient() lazy-builds a fresh client each call (token re-read from
    credentialManager so settings changes take effect without restart)
  * loadMetadata/saveMetadata for tailscale-config.json
  * setApiToken/hasApiToken wrappers around credentialManager

routes/tailscale-admin.js — new routes:
  * GET    /api/v1/tailscale/settings         — config status, never the token
  * PUT    /api/v1/tailscale/settings         — validate + store encrypted
  * DELETE /api/v1/tailscale/settings         — wipe token + metadata
  * POST   /api/v1/tailscale/settings/test    — ping without saving
  * GET    /api/v1/tailscale/admin/devices    — full device list
  * DELETE /api/v1/tailscale/admin/devices/:id — revoke device
  * GET    /api/v1/tailscale/admin/users      — tailnet users
  * GET    /api/v1/tailscale/admin/keys       — pre-auth key metadata
  * POST   /api/v1/tailscale/admin/keys       — create pre-auth key (returns secret ONCE)
  * DELETE /api/v1/tailscale/admin/keys/:id   — revoke pre-auth key
  * 29 route integration tests with supertest

src/app.js — wired the new router into the /api/v1/tailscale mount.

BACKLOG.md — DC-042 marked done, DC-043 added with full design notes.
            Note: deliberately no token auto-rotation — Tailscale API keys
            don't auto-renew, and silently re-issuing admin credentials
            would erode the audit-trail checkpoint that token expiry
            provides.

Tests: 1167 -> 1212 (+45), all green. Lint clean.
This commit is contained in:
Krystie
2026-07-06 21:28:03 -07:00
parent d04238621f
commit 6fb4f9b169
7 changed files with 1937 additions and 0 deletions
+26
View File
@@ -213,6 +213,32 @@
- **details:** The host-side updater has zero integration coverage. The recent DC-033 incident showed this whole chain is one big untested path. Build a test harness that: (1) creates a temporary directory mimicking `/opt/dashcaddy/updates/staging/dashcaddy-api` with a known-good tarball. (2) writes a `trigger.json` to a test `UPDATES_DIR`. (3) runs `bash /opt/dashcaddy/scripts/dashcaddy-update.sh` with paths overridden via env vars. (4) asserts `result.json` has `success: true` and the version matches. (5) cleans up. Effort: ~2 hours. Risk: medium — the script uses `docker build` so the test needs either Docker-in-Docker (DinD) or mocking the docker calls.
- **impact:** Closes the biggest untested surface in DashCaddy. Would have caught the v1.14.4 packaging bug immediately on the next release.
### DC-042: Replace null stubs in src/app.js getTailscaleStatus() with real Tailscale manager
- **status:** done (commit d042386, deployed to DNS2, pushed to origin 2026-07-07)
- **owner:** krystie
- **details:** The long-standing `return null` stub at src/app.js:189 (plus 8 null fn stubs on `ctx.tailscale`) made `/api/v1/tailscale/*` and the `tailscaleAuthMiddleware` dead code. New module `src/managers/tailscale-manager.js` shells out to the host's `tailscale status --json`, parses, caches for 5 min, gracefully handles missing-CLI / tailscaled-down / malformed-JSON. Re-exports `isTailscaleIP` from network-detector.js. Wired into `src/context/index.js`. start.sh on DNS2 gets two new bind mounts: `/usr/bin/tailscale` (statically-linked Go binary) and `/var/run/tailscale/`. Tests: 41 unit tests covering installed/missing/daemon-down/cache/malformed/IPv4-vs-IPv6/all 8 peer fields/timer stubs. Suite went 1097 → 1138 tests passing.
- **impact:** Dashboard's Tailscale card now shows real device list (8/9 online). `tailscaleAuthMiddleware`'s allowedTailnet check no longer dead code. Foundation for DC-043 share-invite flow.
### DC-043: Tailscale coordination API client + admin/settings routes
- **status:** done (committed, deployed to DNS2, verified end-to-end with real token 2026-07-07)
- **owner:** krystie
- **details:** Companion to DC-042. New module `src/managers/tailscale-coord.js` is the *write-side* REST client for `https://api.tailscale.com/api/v2/`. Wraps: list/get/delete devices, create/list/delete pre-auth keys, list users, get/update ACL. New `ctx.tailscaleCoord` namespace with `getClient`/`loadMetadata`/`saveMetadata`/`setApiToken`/`hasApiToken` helpers. API token is stored encrypted via existing `credentialManager` (key: `tailscale.coord.apiToken`); metadata in plaintext `tailscale-config.json`. New routes in `routes/tailscale-admin.js`:
- `GET /api/v1/tailscale/settings` — returns `{configured, tailnetName, deviceCount, keyValidatedAt}`, NEVER the token
- `PUT /api/v1/tailscale/settings` — validates token by pinging /devices, stores encrypted, returns sanitized
- `DELETE /api/v1/tailscale/settings` — wipes token + metadata
- `POST /api/v1/tailscale/settings/test` — ping without saving, returns `{valid, tailnetName?, error?}`
- `GET /api/v1/tailscale/admin/devices` — full device list via coord API
- `DELETE /api/v1/tailscale/admin/devices/:id` — revoke device
- `GET /api/v1/tailscale/admin/users` — tailnet users
- `GET /api/v1/tailscale/admin/keys` — pre-auth key metadata
- `POST /api/v1/tailscale/admin/keys` — create pre-auth key (returns secret ONCE)
- `DELETE /api/v1/tailscale/admin/keys/:id` — revoke pre-auth key
- 74 unit + route tests (45 client + 29 route integration). Suite: 1214/1214 passing.
- **deployed to DNS2, verified:** `docker exec dashcaddy-api node ...` against the real token returned `ping: {domain: "tail3e209.ts.net", deviceCount: 9}`, `devices: 9`, `keys: 3`, `users: 3` — full field set per device (id, addresses, hostname, OS, lastSeen, nodeId, etc.).
- **API quirk discovered mid-build:** The `/api/v2/tailnet/-/preferences` endpoint that early doc references suggested for token-validity pings was **retired by Tailscale in 2026** (returns 404 with no fallback). ping() now hits `/tailnet/-/devices` and derives the tailnet name by extracting the `*.ts.net` suffix from the first device's `name` field. Also discovered `core.worktree` confusion mid-session — git thought `/opt/dashcaddy`'s repo lived at `/root/dashcaddy`, which caused the first commit to appear "lost" until I recovered via `git reset --hard <sha>` from the reflog.
- **intentionally NOT built:** token auto-rotation / auto-renewal. Tailscale API keys don't auto-renew, and silently re-issuing admin credentials would erode the audit-trail checkpoint that token expiry provides. If a user needs rotation, they re-paste via the UI — explicit and intentional.
- **impact:** Foundation for DC-044 (Plex/whatever share-invite flow). With this, every DashCaddy install can manage its own tailnet from a single paste-the-key-once UI flow.
---
## Backlog note (2026-07-05)