DC-043: tailscale coordination API client + admin/settings routes
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:
+26
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user