wood-fired-tasks 1.18.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/CHANGELOG.md +67 -1
  2. package/README.md +25 -26
  3. package/SECURITY.md +30 -67
  4. package/dist/api/plugins/auth/index.d.ts +13 -15
  5. package/dist/api/plugins/auth/index.js +16 -69
  6. package/dist/api/plugins/auth/keys.d.ts +13 -11
  7. package/dist/api/plugins/auth/keys.js +23 -76
  8. package/dist/api/plugins/auth.d.ts +2 -2
  9. package/dist/api/plugins/auth.js +2 -2
  10. package/dist/api/plugins/swagger.js +4 -11
  11. package/dist/api/start.js +5 -3
  12. package/dist/cli/api/client.js +3 -6
  13. package/dist/cli/auth/credentials.d.ts +0 -3
  14. package/dist/cli/auth/credentials.js +6 -9
  15. package/dist/cli/bin/tasks-client.js +1 -1
  16. package/dist/cli/bin/tasks.js +10 -2
  17. package/dist/cli/cache/count-cache.d.ts +82 -0
  18. package/dist/cli/cache/count-cache.js +110 -0
  19. package/dist/cli/cache/paths.d.ts +25 -0
  20. package/dist/cli/cache/paths.js +71 -0
  21. package/dist/cli/commands/backup.js +2 -1
  22. package/dist/cli/commands/completed.js +2 -1
  23. package/dist/cli/commands/db-check.js +2 -1
  24. package/dist/cli/commands/db-migrate-identities.js +2 -1
  25. package/dist/cli/commands/db-mint-token.js +12 -3
  26. package/dist/cli/commands/doctor.d.ts +137 -0
  27. package/dist/cli/commands/doctor.js +354 -5
  28. package/dist/cli/commands/link-project.d.ts +2 -0
  29. package/dist/cli/commands/link-project.js +126 -0
  30. package/dist/cli/commands/login.d.ts +40 -0
  31. package/dist/cli/commands/login.js +64 -40
  32. package/dist/cli/commands/self-update.js +9 -8
  33. package/dist/cli/commands/serve.js +8 -0
  34. package/dist/cli/commands/setup.d.ts +343 -7
  35. package/dist/cli/commands/setup.js +583 -15
  36. package/dist/cli/commands/stats.js +2 -1
  37. package/dist/cli/commands/statusline.d.ts +66 -0
  38. package/dist/cli/commands/statusline.js +305 -0
  39. package/dist/cli/commands/topology.js +2 -1
  40. package/dist/cli/commands/whoami.d.ts +1 -3
  41. package/dist/cli/commands/whoami.js +1 -11
  42. package/dist/cli/commands/wsjf.js +4 -3
  43. package/dist/cli/config/update-check.d.ts +43 -0
  44. package/dist/cli/config/update-check.js +158 -0
  45. package/dist/cli/statusline/count-fetcher.d.ts +58 -0
  46. package/dist/cli/statusline/count-fetcher.js +69 -0
  47. package/dist/cli/statusline/format-segment.d.ts +70 -0
  48. package/dist/cli/statusline/format-segment.js +142 -0
  49. package/dist/cli/statusline/resolve-project.d.ts +60 -0
  50. package/dist/cli/statusline/resolve-project.js +160 -0
  51. package/dist/cli/update/check-writer.d.ts +40 -0
  52. package/dist/cli/update/check-writer.js +106 -0
  53. package/dist/cli/util/npm-spawn.d.ts +44 -0
  54. package/dist/cli/util/npm-spawn.js +50 -0
  55. package/dist/cli/util/prompt.d.ts +84 -0
  56. package/dist/cli/util/prompt.js +172 -0
  57. package/dist/config/db-path.d.ts +16 -0
  58. package/dist/config/db-path.js +72 -0
  59. package/dist/config/env.d.ts +5 -6
  60. package/dist/config/env.js +20 -15
  61. package/dist/config/paths.js +4 -0
  62. package/dist/db/migrate.d.ts +12 -9
  63. package/dist/db/migrate.js +14 -13
  64. package/dist/db/migrations/005-backlogged-status.js +72 -2
  65. package/dist/index.js +5 -2
  66. package/dist/mcp/identity-resolution.js +1 -2
  67. package/dist/mcp/index.js +17 -8
  68. package/dist/mcp/remote/index.d.ts +7 -13
  69. package/dist/mcp/remote/index.js +41 -6
  70. package/dist/mcp/remote/rest-client.d.ts +11 -15
  71. package/dist/mcp/remote/rest-client.js +12 -26
  72. package/dist/mcp/resources/events.d.ts +4 -4
  73. package/dist/mcp/resources/events.js +8 -8
  74. package/dist/services/identity-seeder.d.ts +14 -11
  75. package/dist/services/identity-seeder.js +19 -33
  76. package/dist/skills/tasks/decompose.md +95 -10
  77. package/dist/skills/tasks/update.md +46 -0
  78. package/docs/AGENT_CONTEXT.md +1 -1
  79. package/docs/API.md +59 -61
  80. package/docs/ARCHITECTURE.md +15 -16
  81. package/docs/CLI.md +202 -8
  82. package/docs/INTERFACES.md +12 -10
  83. package/docs/MCP.md +47 -57
  84. package/docs/SETUP.md +274 -164
  85. package/docs/TROUBLESHOOTING.md +1 -1
  86. package/docs/WORKFLOWS.md +19 -16
  87. package/package.json +1 -1
  88. package/packages/wft-router/package.json +1 -1
  89. package/dist/api/plugins/auth/strategies/legacy.d.ts +0 -46
  90. package/dist/api/plugins/auth/strategies/legacy.js +0 -87
package/CHANGELOG.md CHANGED
@@ -13,6 +13,71 @@ vulnerabilities, supply-chain pinning) are always called out under `Security`.
13
13
 
14
14
  _No changes yet._
15
15
 
16
+ ## [v2.0.0] - 2026-06-07
17
+
18
+ The **identity auth cutover** — a breaking major. The legacy `X-API-Key`
19
+ shared-secret auth path is **removed**; every API call now authenticates with a
20
+ per-user **Bearer personal access token (PAT)** minted through the identity
21
+ system. Onboarding gains an OIDC device flow and explicit setup modes, and the
22
+ CLI grows `statusline` and `link-project` commands. **No credential migration is
23
+ required** — pre-identity (`is_legacy=1`) rows are left **inert** and read back
24
+ unchanged — and the one schema migration that touches existing data
25
+ (migration 005) was hardened to preserve child rows (see _Fixed_).
26
+
27
+ ### Changed
28
+ - **Auth is now Bearer-PAT only.** Every authenticated REST/MCP request carries
29
+ `Authorization: Bearer <pat>`; the previously-supported `X-API-Key` shared-key
30
+ header is no longer accepted. PATs are per-user, revocable, and optionally
31
+ expiring (see [`docs/SETUP.md`](docs/SETUP.md)). This is the breaking change
32
+ that makes v2.0 a major.
33
+ - **`setup` gains explicit Local/Service/Remote modes + an OIDC device flow.**
34
+ Running `wood-fired-tasks setup` with no args on a TTY presents a
35
+ Local/Service/Remote menu; `--local` / `--service` / `--remote <url>` pick a
36
+ path non-interactively. `setup --remote <url>` probes the server's OIDC state
37
+ and picks **device-flow** (OIDC ready) vs **manual-PAT** (OIDC
38
+ disabled/degraded) automatically, replacing the old key-paste onboarding. The
39
+ remote MCP entry is **URL-only** — the PAT is cached in the CLI credentials
40
+ file, never written into `~/.claude.json` (#810).
41
+ - **`API_KEYS` is no longer an auth method.** It only seeds inert `is_legacy=1`
42
+ identity rows; the dead production boot-gate that required it was removed.
43
+
44
+ ### Added
45
+ - **OIDC device-flow onboarding** for `setup --remote`, minting a PAT without a
46
+ hand-copied shared key when the server has OIDC enabled.
47
+ - **`statusline` CLI command** for a compact at-a-glance status line, and
48
+ **`link-project`** to write a `.wft/project` marker that pins the working
49
+ directory to a project (consumed by `statusline` and CLI project resolution).
50
+ - **`/tasks:update` slash command** (the packaged `/tasks:*` skill set is now 19).
51
+ - **Update-available notifier** with an opt-out: set `WFT_NO_UPDATE_CHECK=1`
52
+ (env) or `update_check = false` (CLI config) to disable the best-effort
53
+ "newer version available" check.
54
+
55
+ ### Fixed
56
+ - **Unified DB-path resolution — fixes silent data loss on upgrade.** `serve`/the
57
+ API defaulted `DATABASE_PATH` to the OS app-data dir while `migrate` and the
58
+ `tasks db*` commands hardcoded `./data/tasks.db`; with `DATABASE_PATH` unset
59
+ they could open **different** databases, silently abandoning an upgrading
60
+ user's `./data/tasks.db`. A single `resolveDbPath()`
61
+ ([`src/config/db-path.ts`](src/config/db-path.ts)) is now the source of truth:
62
+ explicit `DATABASE_PATH` (or the `DB_PATH` alias) > adopt an existing legacy
63
+ `./data/tasks.db` > app-data default.
64
+ - **Migration 005 no longer cascade-deletes data.** `PRAGMA foreign_keys = OFF`
65
+ is a no-op inside a transaction, so 005's table rebuild CASCADE-deleted
66
+ `task_comments` / `dependencies` / `tags` when run against a populated pre-005
67
+ database. The migration now snapshots and restores child rows around the
68
+ rebuild (covered by a populated-DB round-trip test).
69
+
70
+ ### Security
71
+ - **Removed the `X-API-Key` shared-secret auth path entirely** (the v2.0
72
+ cutover). A single long-lived shared key was the broadest part of the auth
73
+ surface; replacing it with per-user, individually-revocable Bearer PATs scopes
74
+ credentials to a user, makes revocation surgical, and bounds blast radius on
75
+ leak. Legacy `is_legacy=1` rows are left inert — **no credential migration is
76
+ required**.
77
+ - **Closed an upgrade-time data-loss path** (the migration-005 cascade delete;
78
+ see _Fixed_) — a data-integrity fix for anyone upgrading across migration 005
79
+ with populated tables.
80
+
16
81
  ## [v1.18.2] - 2026-06-06
17
82
 
18
83
  A patch release: a Windows `self-update` fix plus two install-experience
@@ -561,7 +626,8 @@ and the task/project/dependency/comment/subtask domain model.
561
626
  - Task hierarchy (subtasks), dependency service, comments, time estimates
562
627
  (phase 06).
563
628
 
564
- [Unreleased]: https://github.com/Wood-Fired-Games/wood-fired-tasks/compare/v1.15...HEAD
629
+ [Unreleased]: https://github.com/Wood-Fired-Games/wood-fired-tasks/compare/v2.0.0...HEAD
630
+ [v2.0.0]: https://github.com/Wood-Fired-Games/wood-fired-tasks/compare/v1.18.2...v2.0.0
565
631
  [v1.15]: https://github.com/Wood-Fired-Games/wood-fired-tasks/compare/v1.14...v1.15
566
632
  [v1.14]: https://github.com/Wood-Fired-Games/wood-fired-tasks/compare/v1.13...v1.14
567
633
  [v1.13]: https://github.com/Wood-Fired-Games/wood-fired-tasks/compare/v1.12...v1.13
package/README.md CHANGED
@@ -16,7 +16,7 @@ Wood Fired Tasks is open-source coordination infrastructure for fleets of AI cod
16
16
 
17
17
  - `/tasks:*` skill files implementing the plan→decompose→loop→audit lifecycle (ship as Claude Code slash commands; the recipes are vendor-neutral)
18
18
  - MCP server with 27 tools for native agent integration (local SQLite or remote HTTP modes) + cross-platform installers (Linux/macOS and Windows)
19
- - REST API with 47 route handlers across `src/api/routes/` (1 public `/health`; the rest authenticated; a single instance serves up to 40 — OIDC-disabled stubs are mutually exclusive with the live OIDC routes) and a `tasks` CLI with 34 commands
19
+ - REST API with 52 route handlers across `src/api/routes/` (1 public `/health`; the rest authenticated; a single instance serves up to 45 — OIDC-disabled stubs are mutually exclusive with the live OIDC routes) and a `tasks` CLI with 42 commands
20
20
  - Atomic task claiming with optimistic locking + workflow automation (parent auto-complete, dependency auto-unblock) for multi-agent coordination
21
21
  - Real-time Server-Sent Events (SSE) for task/project change notifications
22
22
  - SQLite database with WAL mode, FTS5 full-text search, and automatic migrations
@@ -133,7 +133,7 @@ development instead? See [docs/SETUP.md → Development Setup](docs/SETUP.md#dev
133
133
 
134
134
  For self-hosted production deploys (including the fork-and-deploy workflow for OSS operators): provision a host once with `deploy/install.sh`, then ship every subsequent release in place with `deploy/upgrade.sh` (atomic backup, migrate, restart, `/health` probe, manual rollback recipe on failure). The full walkthrough — first-time install, in-place upgrades, deploying your fork, manual rollback, and the migration safety contract — lives at [Self-hosting and upgrades](docs/SETUP.md#self-hosting-and-upgrades). When a deploy or a reboot goes sideways, the [Troubleshooting & Recovery runbook](docs/TROUBLESHOOTING.md) covers boot failures (`exit 78`), wrong/stale-database symptoms, and safe backup/restore.
135
135
 
136
- **Sharing one server across a team.** The common shape is a single on-prem server with a fleet of Windows, Linux, and macOS workstations all pointed at it in remote mode (each client proxies its MCP tool calls to the shared REST API, so everyone sees one backlog). The end-to-end recipe — make the server reachable behind TLS, mint one revocable PAT per machine, and run the installer in `--mode remote` on each OS — is the [Multi-OS client fleet](docs/SETUP.md#multi-os-client-fleet-one-shared-on-prem-server) section.
136
+ **Sharing one server across a team.** The common shape is a single on-prem server with a fleet of Windows, Linux, and macOS workstations all pointed at it in remote mode (each client proxies its MCP tool calls to the shared REST API, so everyone sees one backlog). The end-to-end recipe — make the server reachable behind TLS, mint one revocable PAT per machine, and run `wood-fired-tasks setup --remote <url> --token wft_pat_…` on each OS — is the [Multi-OS client fleet](docs/SETUP.md#multi-os-client-fleet-one-shared-on-prem-server) section.
137
137
 
138
138
  ## Automation (event-driven)
139
139
 
@@ -153,28 +153,27 @@ a single task unblock, the MCP server also exposes a `wait_for_unblock` tool
153
153
 
154
154
  ## Security Model
155
155
 
156
- **Read this before deploying.** Wood Fired Tasks is built for trusted multi-agent coordination. As of **v1.6** the REST API authenticates every `/api/v1` request through a three-strategy chain (`src/api/plugins/auth/index.ts`), tried in order — the first strategy that produces a valid user wins, and that user's id is stamped onto every write (`created_by_user_id`, `assignee_user_id`, …) and the per-request audit log (`user_id`, `token_id`, `auth_method`):
156
+ **Read this before deploying.** Wood Fired Tasks is built for trusted multi-agent coordination. As of **v2.0** the REST API authenticates every `/api/v1` request through a two-strategy chain (`src/api/plugins/auth/index.ts`), tried in order — the first strategy that produces a valid user wins, and that user's id is stamped onto every write (`created_by_user_id`, `assignee_user_id`, …) and the per-request audit log (`user_id`, `token_id`, `auth_method`):
157
157
 
158
158
  | Order | Strategy | Credential | Wire format |
159
159
  |-------|----------|------------|-------------|
160
160
  | 1 | **PAT** — recommended for machines/agents | row in `api_tokens` (SHA-256 hash stored) | `Authorization: Bearer wft_pat_<…>` |
161
161
  | 2 | **Session** — recommended for humans | OIDC sign-in → sealed-box cookie | `Cookie: wft_session=<…>` |
162
- | 3 | **Legacy** — deprecated but still supported (see below) | entry in `API_KEYS` env list | `X-API-Key: <…>` |
163
162
 
164
- PATs are minted from a logged-in `/me` web session or offline via `tasks db mint-token`; the raw value is shown **once** at mint time (only a hash is stored) and revoked via the `/me` UI, `DELETE /me/tokens/:id`, or `tasks logout`. Sessions come from OIDC (`/auth/login` → provider → `/auth/callback`, protected by PKCE + state), are sealed-box-encrypted with `SESSION_COOKIE_SECRET`, and expire after 8h. The CLI and remote MCP client auto-select the header from the `wft_pat_` prefix, so the same env var accepts a PAT or a legacy key. Full detail: [SECURITY.md → Authentication Architecture](SECURITY.md#authentication-architecture).
163
+ PATs are minted from a logged-in `/me` web session or offline via `tasks db mint-token`; the raw value is shown **once** at mint time (only a hash is stored) and revoked via the `/me` UI, `DELETE /me/tokens/:id`, or `tasks logout`. Sessions come from OIDC (`/auth/login` → provider → `/auth/callback`, protected by PKCE + state), are sealed-box-encrypted with `SESSION_COOKIE_SECRET`, and expire after 8h. The CLI and remote MCP client send the PAT as `Authorization: Bearer`. Full detail: [SECURITY.md → Authentication Architecture](SECURITY.md#authentication-architecture).
165
164
 
166
165
  ### ⚠️ Authentication is NOT authorization — every identity is admin
167
166
 
168
167
  **Read this before exposing the service to anything but trusted callers.**
169
168
 
170
169
  - **Authentication ≠ authorization.** The auth chain only *identifies* the caller; it does **not** scope what they may do.
171
- - **Every authenticated identity is effectively an admin.** Any valid credential — PAT, OIDC session, or legacy `X-API-Key` — can read, write, and delete **every** task, project, comment, and dependency across **every** project in the database.
170
+ - **Every authenticated identity is effectively an admin.** Any valid credential — PAT or OIDC session — can read, write, and delete **every** task, project, comment, and dependency across **every** project in the database.
172
171
  - **There is NO RBAC, NO ACL, and NO per-project / per-tenant isolation.** These are not implemented; scoped/role-based permissions are tracked only as future work.
173
172
  - **Do NOT expose this service on a public network, and do NOT run it multi-tenant, without an external authorization layer** (e.g. an authenticating reverse proxy that enforces its own per-tenant access control in front of the API). Treat any issued credential as full admin access to all data.
174
173
 
175
- ### Legacy `X-API-Key` is still supported (PAT / OIDC preferred)
174
+ ### Legacy `X-API-Key` was removed in v2.0
176
175
 
177
- The legacy `X-API-Key` strategy **still works today** it is strategy #3 in the live auth chain (`src/api/plugins/auth/index.ts`), and `API_KEYS` is currently a **required** env var (see Configuration), so every deployment has at least one working key. It is marked **deprecated as of v1.6**: PAT and OIDC sessions are the preferred credentials going forward, but legacy keys have **not** been removed. Legacy-authed responses carry advisory RFC 8594 `Deprecation: true` + `Sunset: <LEGACY_AUTH_SUNSET_DATE>` headers (operator-controlled, default `2026-12-31`) and emit a `legacy_auth_used` warn log so operators can track migration progress. New deployments should issue **PATs** — one per machine/agent, so you can revoke an individual token without disturbing others — or use OIDC sessions. The `API_KEYS` env accepts a comma-separated list of `key` or `key:label` entries (the label surfaces in audit logs as `apiKeyLabel`; the raw key is never logged). See [SECURITY.md → Legacy Auth Sunset Timeline](SECURITY.md#legacy-auth-sunset-timeline) and `tasks db migrate-identities` for the planned migration path.
176
+ The legacy `X-API-Key` shared-secret strategy was **removed entirely in v2.0** (`src/api/plugins/auth/index.ts` no longer accepts it). A request carrying only `X-API-Key` now gets **401**. `API_KEYS` is no longer an auth method and is **not** a required env var it is not in the Zod env schema. If set, it only (optionally) seeds inert legacy `users` rows (`is_legacy=1`) for display/back-reference; those rows hold no usable credential. Every deployment must now issue **PATs** — one per machine/agent, so you can revoke an individual token without disturbing others — or use OIDC sessions. PATs are minted from a `/me` web session or offline via `tasks db mint-token`. See [SECURITY.md → Authentication Architecture](SECURITY.md#authentication-architecture) and `tasks db migrate-identities` for the migration path off legacy keys.
178
177
 
179
178
  ### Defense in depth
180
179
 
@@ -210,7 +209,7 @@ flowchart TB
210
209
  end
211
210
 
212
211
  subgraph guards[Auth and validation]
213
- Auth[auth plugin<br/>X-API-Key]
212
+ Auth[auth plugin<br/>Bearer PAT / session]
214
213
  RL[Rate limiter<br/>RATE_LIMIT_*]
215
214
  Zod[Zod schemas<br/>request/response]
216
215
  end
@@ -237,7 +236,7 @@ flowchart TB
237
236
  end
238
237
 
239
238
  HumanCLI --> CLI
240
- HTTPAgent -->|HTTP + X-API-Key| REST
239
+ HTTPAgent -->|HTTP + Bearer PAT| REST
241
240
  ClaudeMCP -->|stdio JSON-RPC| MCP
242
241
  SlackUser -->|Socket Mode| Slack
243
242
 
@@ -281,9 +280,9 @@ flowchart TB
281
280
 
282
281
  | Interface | Access Method | Transport | Auth |
283
282
  |-----------|--------------|-----------|------|
284
- | REST API | HTTP endpoints | Port 3000 (configurable) | PAT (`Authorization: Bearer`), session cookie, or legacy `X-API-Key` |
285
- | CLI | `tasks` command | HTTP to API server (most cmds); direct SQLite for offline ops (`backup`, `doctor`, `stats`, `db-check`, `completed`) | `API_KEY` env var (accepts a PAT or legacy key) |
286
- | MCP Server | stdio JSON-RPC (local) or HTTP (remote variant) | MCP client integration | None for stdio (local access); Bearer PAT or `X-API-Key` for remote |
283
+ | REST API | HTTP endpoints | Port 3000 (configurable) | PAT (`Authorization: Bearer`) or OIDC session cookie |
284
+ | CLI | `tasks` command | HTTP to API server (most cmds); direct SQLite for offline ops (`backup`, `doctor`, `stats`, `db-check`, `completed`) | `API_KEY` env var (holds a PAT, sent as `Authorization: Bearer`) |
285
+ | MCP Server | stdio JSON-RPC (local) or HTTP (remote variant) | MCP client integration | None for stdio (local access); Bearer PAT for remote |
287
286
  | Slack subprocess | Slack Socket Mode | WebSocket to Slack | Slack signing secret + bot token |
288
287
 
289
288
  All entry points share the same TypeScript services
@@ -341,7 +340,7 @@ The `priority` enum is **augmented, not replaced**, by WSJF: once a project has
341
340
 
342
341
  ## API Summary
343
342
 
344
- All `/api/v1` endpoints require authentication — a PAT (`Authorization: Bearer wft_pat_…`), an OIDC session cookie, or a legacy `X-API-Key` header (deprecated). `GET /health` is public; the OIDC sign-in flow lives under `/auth/*` (outside `/api/v1`).
343
+ All `/api/v1` endpoints require authentication — a PAT (`Authorization: Bearer wft_pat_…`) or an OIDC session cookie. `GET /health` is public; the OIDC sign-in flow lives under `/auth/*` (outside `/api/v1`).
345
344
 
346
345
  Base URL: `http://localhost:3000`
347
346
 
@@ -417,14 +416,14 @@ The OIDC/session/PAT surface backing the auth model lives partly outside the tas
417
416
  | GET | /auth/callback | OIDC redirect callback → sets the session cookie |
418
417
  | POST | /auth/logout | Revoke the active PAT and clear the session |
419
418
  | GET | /auth/error | OIDC/session error landing page (session-expiry, 403 destinations) |
420
- | GET | /api/v1/me | Current authenticated user's profile (accepts session, PAT, or legacy auth) |
419
+ | GET | /api/v1/me | Current authenticated user's profile (accepts PAT or session) |
421
420
  | GET | /api/v1/me/tokens | List the caller's personal access tokens |
422
421
  | DELETE | /api/v1/me/tokens/active | Revoke the caller's currently-active token |
423
422
  | DELETE | /api/v1/me/tokens/:id | Revoke a personal access token by ID |
424
423
 
425
424
  A device-authorization flow under `/auth/device*` (`GET /auth/device`, `POST /auth/device/code`, `POST /auth/device/token`, `POST /auth/device/verify`) supports headless PAT minting. When OIDC is **not** configured, the `/auth/*` and `/auth/device/*` routes are replaced by disabled-stub handlers (HTTP 501), so they exist in both modes but only one set is live per instance. When `SESSION_COOKIE_SECRET` is set, top-level HTML web routes (`GET /login`, `GET /me`, `GET /me/tokens`, `POST /me/tokens/:id/revoke`) are also served for the browser sign-in UI.
426
425
 
427
- This brings the full registered surface to **47 route handlers** under `src/api/routes/` — derived by counting `fastify.<verb>(` / `server.<verb>(` registrations across the route files (excluding tests). A single running instance serves up to **40** of them: the 7 OIDC-disabled stub handlers are mutually exclusive with the 8 live OIDC `/auth/*` routes.
426
+ This brings the full registered surface to **52 route handlers** under `src/api/routes/` — derived by counting `fastify.<verb>(` / `server.<verb>(` registrations across the route files (excluding tests). A single running instance serves up to **45** of them: the 7 OIDC-disabled stub handlers are mutually exclusive with the live OIDC `/auth/*` routes.
428
427
 
429
428
  For detailed API documentation including request/response schemas, see [docs/API.md](docs/API.md).
430
429
 
@@ -627,15 +626,15 @@ Wood Fired Tasks streams real-time task and project change notifications via Ser
627
626
 
628
627
  ```bash
629
628
  # Subscribe to all events
630
- curl -N -H "X-API-Key: your-key" \
629
+ curl -N -H "Authorization: Bearer wft_pat_<your-pat>" \
631
630
  http://localhost:3000/api/v1/events
632
631
 
633
632
  # Filter by project
634
- curl -N -H "X-API-Key: your-key" \
633
+ curl -N -H "Authorization: Bearer wft_pat_<your-pat>" \
635
634
  "http://localhost:3000/api/v1/events?project_id=1"
636
635
 
637
636
  # Filter by event type
638
- curl -N -H "X-API-Key: your-key" \
637
+ curl -N -H "Authorization: Bearer wft_pat_<your-pat>" \
639
638
  "http://localhost:3000/api/v1/events?event_types=task.created,task.claimed"
640
639
  ```
641
640
 
@@ -644,7 +643,7 @@ curl -N -H "X-API-Key: your-key" \
644
643
  Include `Last-Event-ID` header to resume from where you left off:
645
644
 
646
645
  ```bash
647
- curl -N -H "X-API-Key: your-key" \
646
+ curl -N -H "Authorization: Bearer wft_pat_<your-pat>" \
648
647
  -H "Last-Event-ID: 42" \
649
648
  http://localhost:3000/api/v1/events
650
649
  ```
@@ -658,13 +657,13 @@ Multiple agents can race to claim the same task. Exactly one wins; the rest rece
658
657
  ```bash
659
658
  # Claim a task
660
659
  curl -X POST http://localhost:3000/api/v1/tasks/42/claim \
661
- -H "X-API-Key: your-key" \
660
+ -H "Authorization: Bearer wft_pat_<your-pat>" \
662
661
  -H "Content-Type: application/json" \
663
662
  -d '{"assignee": "agent-1"}'
664
663
 
665
664
  # With idempotency key (safe to retry)
666
665
  curl -X POST http://localhost:3000/api/v1/tasks/42/claim \
667
- -H "X-API-Key: your-key" \
666
+ -H "Authorization: Bearer wft_pat_<your-pat>" \
668
667
  -H "X-Idempotency-Key: unique-key-123" \
669
668
  -H "Content-Type: application/json" \
670
669
  -d '{"assignee": "agent-1"}'
@@ -710,10 +709,10 @@ The four WSJF MCP tools (`wsjf_ranking`, `wsjf_history`, `rescore_project`, `wsj
710
709
  |----------|-------------|---------|
711
710
  | PORT | HTTP server port | 3000 |
712
711
  | HOST | HTTP server host. Defaults to loopback only; set to `0.0.0.0` (or a specific LAN IP) to expose on the network. | 127.0.0.1 |
713
- | API_KEYS | Comma-separated legacy API keys (`key` or `key:label`). **Required** — the Zod config schema rejects an empty/unset value (`src/config/env.ts`), so the server exits with code 78 (`EX_CONFIG`) at startup when it is missing. There is no "auth-disabled" mode. | (required — no default) |
712
+ | API_KEYS | Optional, legacy-only. **Not an auth method and not required** — it is not in the Zod config schema. If set (comma-separated `key` or `key:label`), it only seeds inert legacy `users` rows (`is_legacy=1`) for display/back-reference; those rows carry no usable credential. Auth is PAT (Bearer) or OIDC session. | (optional — no default) |
714
713
  | LOG_LEVEL | Logging level (debug, info, warn, error) | info |
715
714
  | NODE_ENV | Environment (development, production) | (none) |
716
- | DATABASE_PATH | Path to SQLite database file (canonical; MCP server also accepts legacy `DB_PATH`) | ./data/tasks.db |
715
+ | DATABASE_PATH | Path to SQLite database file (canonical; MCP server also accepts legacy `DB_PATH`). Resolution precedence: explicit `DATABASE_PATH` > legacy `./data/tasks.db` auto-adopt (used with a one-time warning when `DATABASE_PATH` is unset, a legacy `./data/tasks.db` exists, and the app-data DB does not) > OS app-data default. | OS app-data dir — `~/.local/share/wood-fired-tasks/tasks.db` (Linux), `~/Library/Application Support/wood-fired-tasks/tasks.db` (macOS), `%APPDATA%\wood-fired-tasks\tasks.db` (Windows) |
717
716
  | API_BASE_URL | Base URL for CLI API calls | http://localhost:3000 |
718
717
  | API_KEY | API key for CLI authentication | (none) |
719
718
  | SLACK_BOT_TOKEN / SLACK_APP_TOKEN / SLACK_SIGNING_SECRET | Optional Slack integration (all three required together) — see [docs/SLACK.md](docs/SLACK.md) | (none) |
@@ -733,7 +732,7 @@ variables) lives in [docs/SETUP.md → Environment Variables](docs/SETUP.md#envi
733
732
  # Development mode with hot reload
734
733
  npm run dev
735
734
 
736
- # Run tests (2640 tests across 204 files)
735
+ # Run tests (~2600+ tests)
737
736
  npm test
738
737
 
739
738
  # Watch mode for tests
@@ -771,7 +770,7 @@ SQLite with better-sqlite3 driver, WAL mode, and automatic migrations via Umzug.
771
770
 
772
771
  ### Testing
773
772
 
774
- 2640 tests across 204 test files covering:
773
+ ~2600+ tests covering:
775
774
  - Service layer unit tests
776
775
  - API route integration tests (all endpoints)
777
776
  - MCP tool tests (all tools)
package/SECURITY.md CHANGED
@@ -76,7 +76,7 @@ We will:
76
76
  Issues we will prioritize include, but are not limited to:
77
77
 
78
78
  - Authentication bypass on any endpoint — reaching a `/api/v1` route
79
- without a valid PAT, session, or `X-API-Key` credential, or bypassing
79
+ without a valid PAT or session credential, or bypassing
80
80
  the SSE auth path. (Note: there is no separate authorization layer to
81
81
  bypass — see "Authentication Is Not Authorization" below. Any valid
82
82
  credential is already full-access.)
@@ -102,7 +102,7 @@ Thank you for helping keep wood-fired-tasks and its users safe.
102
102
 
103
103
  ## Authentication Architecture
104
104
 
105
- As of v1.6, the REST API supports three authentication strategies, tried
105
+ As of v2.0, the REST API supports two authentication strategies, tried
106
106
  in order by a Fastify chain plugin (`src/api/plugins/auth/index.ts`). The
107
107
  first strategy that produces a valid `request.user` wins; the request
108
108
  proceeds with that user's id stamped onto every write (`created_by_user_id`,
@@ -113,11 +113,10 @@ log (`user_id`, `token_id`, `auth_method`).
113
113
  |-------|----------|------------|-------------|
114
114
  | 1 | **PAT (Personal Access Token)** | A token row in `api_tokens` | `Authorization: Bearer wft_pat_<…>` |
115
115
  | 2 | **Session** | An OIDC-derived sealed-box session cookie | `Cookie: wft_session=<…>` |
116
- | 3 | **Legacy** | An entry in the `API_KEYS` env list | `X-API-Key: <…>` |
117
116
 
118
- The three strategies coexist intentionally legacy keeps existing
119
- deployments running while operators migrate; PAT is the recommended
120
- machine credential; session is the recommended user credential.
117
+ PAT is the recommended machine credential; session is the recommended
118
+ user credential. (The legacy `X-API-Key` shared-secret strategy was
119
+ **removed in v2.0** see [Legacy `X-API-Key` Status — Removed in v2.0](#legacy-x-api-key-status--removed-in-v20) below.)
121
120
 
122
121
  ### PAT lifecycle
123
122
 
@@ -147,10 +146,10 @@ for hygiene:
147
146
  the PAT auth strategy: once `expires_at` is in the past the token fails
148
147
  with `reasonCode: expired`.
149
148
 
150
- The PAT prefix (`wft_pat_`) is part of the wire format: the remote MCP
151
- server and the CLI HTTP client switch their auth header based on the
152
- prefix, so the same env var (`WFT_API_KEY` for MCP, `API_KEY` for CLI)
153
- transparently accepts a PAT or a legacy key.
149
+ The PAT prefix (`wft_pat_`) is part of the wire format. The remote MCP
150
+ server and the CLI HTTP client read the PAT from their respective env var
151
+ (`WFT_API_KEY` for MCP, `API_KEY` for CLI) and send it as
152
+ `Authorization: Bearer <pat>`.
154
153
 
155
154
  ### Session lifecycle
156
155
 
@@ -192,9 +191,7 @@ Every authenticated request emits a structured pino log line carrying:
192
191
  guarantees they exist).
193
192
  - `token_id` — the `api_tokens.id` when strategy=PAT; NULL
194
193
  otherwise.
195
- - `auth_method` — one of `pat`, `session`, `legacy`.
196
- - `apiKeyLabel` — the human-friendly label for legacy keys, e.g.
197
- `key_alice-laptop`. Absent for PAT / session.
194
+ - `auth_method` — one of `pat`, `session`.
198
195
 
199
196
  Failures emit a counterpart `tag: auth.failure` line with a coarse
200
197
  `reasonCode` (`missing_credential`, `unknown_token`, `revoked_token`, …)
@@ -202,66 +199,32 @@ so secret values never appear in logs. The `auth-audit` helper enforces
202
199
  this — it is the **only** sanctioned way for the auth plugin to
203
200
  log into the request.
204
201
 
205
- ## Legacy `X-API-Key` Status
206
-
207
- The legacy `X-API-Key` strategy is **deprecated but still fully
208
- supported as of v1.11.** It remains the third link in the auth chain
209
- (`src/api/plugins/auth/index.ts` walks PAT → session legacy), so a
210
- request carrying a valid `API_KEYS` entry still authenticates and
211
- mutates data. PAT and OIDC session are the preferred credentials; legacy
212
- keys exist to keep older deployments running while operators migrate.
213
-
214
- There is **no scheduled removal version.** Earlier drafts of this
215
- document described a "v1.7 sunset" that would drop `API_KEYS` support —
216
- that never happened. v1.7 through v1.11 shipped with the legacy strategy
217
- intact, and no removal date is currently committed.
218
-
219
- Legacy authentication is surfaced so operators can track migration
220
- progress, not blocked:
221
-
222
- - Every legacy-authed REST response carries two RFC 8594 headers:
223
-
224
- ```
225
- Deprecation: true
226
- Sunset: 2026-12-31
227
- ```
228
-
229
- The `Sunset` value comes from the `LEGACY_AUTH_SUNSET_DATE` env var
230
- (default `2026-12-31`, must be `YYYY-MM-DD`). It is an advisory
231
- migration target, **not** an enforced cutoff — the strategy keeps
232
- working past that date. PAT-authed and session-authed requests carry
233
- **neither** header.
234
-
235
- - Every legacy-authed request also emits a `warn`-level log line:
236
-
237
- ```json
238
- {
239
- "level": 40,
240
- "event": "legacy_auth_used",
241
- "userId": 1,
242
- "apiKeyLabel": "key_alice-laptop",
243
- "requestId": "…",
244
- "requestUrl": "/api/v1/tasks",
245
- "sunset": "2026-12-31"
246
- }
247
- ```
248
-
249
- Aggregate `legacy_auth_used` over a rolling window to gauge migration
250
- readiness — a steady decline to zero means clients have all moved to
251
- PAT or session. New deployments should issue PATs (one per
252
- machine/agent) or use OIDC sessions rather than `API_KEYS`.
253
-
254
- If a future release does remove the legacy strategy, the
202
+ ## Legacy `X-API-Key` Status — Removed in v2.0
203
+
204
+ The legacy `X-API-Key` shared-secret strategy was **removed entirely in
205
+ v2.0.** The auth chain (`src/api/plugins/auth/index.ts`) now walks only
206
+ PAT → session; a request carrying only an `X-API-Key` header gets **401**.
207
+ There is no "auth-disabled" mode and no shared-secret fallback.
208
+
209
+ `API_KEYS` is **no longer an auth method and is no longer a required env
210
+ var** — it is not in the Zod config schema (`src/config/env.ts`). If set,
211
+ it is read only as an optional seed for inert legacy `users` rows
212
+ (`is_legacy=1`) so historical identities still render; those rows carry
213
+ **no usable credential** and cannot authenticate a request.
214
+
215
+ Every deployment must now authenticate with a per-user **PAT** (one per
216
+ machine/agent, individually revocable) or an **OIDC session**. The
255
217
  `tasks db migrate-identities` tool (idempotent; backfills identity FKs
256
- for historical rows that carry only the legacy TEXT identity columns)
257
- is the supported pre-upgrade step. It is safe to run today.
218
+ for historical rows that carry only the legacy TEXT identity columns) is
219
+ the supported step to fold pre-identity data forward; it is safe to run
220
+ at any time.
258
221
 
259
222
  ## CORS
260
223
 
261
224
  The REST API **does not register a CORS plugin** — there is no
262
225
  `@fastify/cors` (or equivalent) registration anywhere in `src/api/`, and
263
226
  `cors` is not a project dependency. This is intentional: the API is built
264
- for server-to-server and agent traffic (PAT / `X-API-Key` in headers),
227
+ for server-to-server and agent traffic (PAT in the `Authorization: Bearer` header),
265
228
  plus a same-origin browser surface (`/auth/*`, `/me`, `/login`) that does
266
229
  not need cross-origin access. With no `Access-Control-Allow-Origin`
267
230
  header emitted, browsers block cross-origin reads of API responses by
@@ -283,7 +246,7 @@ default.
283
246
  Authentication identifies the caller; it does **not** scope what the
284
247
  caller may do. Wood Fired Tasks has **no RBAC, no ACL, and no tenant /
285
248
  project isolation.** Every authenticated identity — whether it arrived
286
- via PAT, OIDC session, or a legacy `X-API-Key` — is effectively an
249
+ via PAT or OIDC session — is effectively an
287
250
  admin: it can read, write, and delete **every** task, project, comment,
288
251
  dependency, and Slack subscription across **every** project in the
289
252
  database. The `--scopes` minted onto a PAT are advisory metadata only and
@@ -2,7 +2,9 @@
2
2
  * Phase 28 (Plan 28-04) — unified auth chain plugin.
3
3
  *
4
4
  * Replaces the legacy single-strategy plugin at `src/api/plugins/auth.ts`
5
- * with a three-strategy chain (PAT → session-stub legacy API_KEYS). The
5
+ * with a two-strategy chain (PAT → session). The legacy X-API-Key strategy
6
+ * was removed in the v2.0 auth cutover (Phase 0, task #799); a request that
7
+ * bears only an X-API-Key header now falls through to the catch-all 401. The
6
8
  * file at `src/api/plugins/auth.ts` survives as a thin re-export shim so
7
9
  * existing `import authPlugin from './plugins/auth.js'` callers (server.ts,
8
10
  * auth-logging.test.ts) keep working without churn.
@@ -11,28 +13,24 @@
11
13
  * 1. Decorate `FastifyRequest` with `user`, `authMethod`, `tokenId`,
12
14
  * `apiKeyLabel` at plugin load (Fastify requires decoration before any
13
15
  * route registers).
14
- * 2. Validate `process.env.API_KEYS` in production (throws synchronously,
15
- * which Fastify bubbles up to createServer → exits with non-zero).
16
- * 3. Pre-compute SHA-256 hashes of every configured API_KEYS entry once
17
- * at register time; feed the result into the legacy strategy on every
18
- * request so it never re-hashes.
19
- * 4. Register a `preHandler` hook that:
16
+ * 2. Register a `preHandler` hook that:
20
17
  * a. Short-circuits when `request.routeOptions.config.skipAuth === true`.
21
- * b. Walks PAT → session-stub → legacy. First match wins. PAT failure
22
- * does NOT fall through to legacy — see `enforceSessionOnly` /
23
- * strategy-fail short-circuit below.
18
+ * b. Walks PAT → session. First match wins. PAT failure does NOT fall
19
+ * through to session — see `enforceSessionOnly` / strategy-fail
20
+ * short-circuit below.
24
21
  * c. On a successful match, populates `request.user`, `request.authMethod`,
25
- * `request.tokenId` (and `request.apiKeyLabel` for legacy), re-childs
26
- * the request logger with `{ user_id, token_id, auth_method,
27
- * apiKeyLabel }` so every downstream log line carries audit fields,
28
- * and enforces `config.sessionOnly` post-auth.
22
+ * `request.tokenId`, re-childs the request logger with
23
+ * `{ user_id, token_id, auth_method, apiKeyLabel }` so every
24
+ * downstream log line carries audit fields, and enforces
25
+ * `config.sessionOnly` post-auth.
29
26
  * d. On a strategy `fail` outcome, emits one `auth.failure` warn log via
30
27
  * the Phase 27 `logAuthFailure` helper and returns a uniform 401
31
28
  * (the distinct `reasonCode` lives ONLY in the audit log — never in
32
29
  * the response body).
33
30
  * e. On total fall-through (every strategy returned `skip`), emits a
34
31
  * catch-all `auth.failure` log tagged `strategy: 'legacy'`,
35
- * `reasonCode: 'missing_credential'` (per Plan-04 Decision Q6).
32
+ * `reasonCode: 'missing_credential'` (the tag name is retained for
33
+ * audit-feed continuity even though the legacy strategy is gone).
36
34
  *
37
35
  * Side-effect contracts:
38
36
  * - PAT match schedules `setImmediate(() => apiTokenRepository.touchLastUsed(
@@ -1,10 +1,7 @@
1
1
  import fp from 'fastify-plugin';
2
- import { parseApiKeyEntries, config } from '../../../config/env.js';
3
- import { validateApiKeysForProduction } from './keys.js';
4
2
  import { logAuthFailure } from '../../../services/auth-audit.js';
5
3
  import { tryAuth as tryPat } from './strategies/pat.js';
6
4
  import { tryAuth as trySession } from './strategies/session.js';
7
- import { tryAuth as tryLegacy, precomputeHashedEntries } from './strategies/legacy.js';
8
5
  import { shouldTouchLastUsed } from '../../../services/pat-touch-debounce.js';
9
6
  /**
10
7
  * Throws if `preHandler` has not run yet (or if `skipAuth` was set). Use in
@@ -151,20 +148,16 @@ function sendInternalError(request, reply, err) {
151
148
  reply.code(500).send({ error: 'INTERNAL_ERROR' });
152
149
  }
153
150
  const authChainImpl = async (fastify) => {
154
- // Parse and (in production) validate API_KEYS at register time so a
155
- // misconfigured prod boot fails fast. Same fail-fast semantics as the
156
- // pre-split plugin `validateApiKeysForProduction` throws synchronously
157
- // and Fastify bubbles the error up to createServer which closes the
158
- // server and disposes the App (server.ts:345-363 catch).
159
- const entries = parseApiKeyEntries(process.env['API_KEYS']);
160
- const keys = entries.map((e) => e.key);
161
- if (process.env['NODE_ENV'] === 'production') {
162
- validateApiKeysForProduction(keys);
163
- }
164
- else if (entries.length === 0) {
165
- fastify.log.warn('No API keys configured in API_KEYS env var. All API requests will be rejected.');
166
- }
167
- const hashedEntries = precomputeHashedEntries(entries);
151
+ // v2.0 auth cutover (#799/#801): the legacy X-API-Key REST strategy was
152
+ // removed REST authenticates via PAT session only. `API_KEYS` no longer
153
+ // gates REST access, so the old production fatal gate
154
+ // (`validateApiKeysForProduction`, which threw on an empty/unset API_KEYS and
155
+ // aborted boot) is dead and was actively hostile to upgraders who correctly
156
+ // dropped API_KEYS. It is removed here. A server with no users still boots
157
+ // and serves OIDC/PAT auth. (`parseApiKeyEntries` survives because the MCP
158
+ // stdio actor-resolution path still matches a legacy WFT_API_KEY against
159
+ // API_KEYS — see src/mcp/identity-resolution.ts — but that is an MCP concern,
160
+ // not a REST boot gate.)
168
161
  // Decorators MUST land before any route registers in this scope. The fp()
169
162
  // wrap below lifts these into the parent scope so sibling /api/v1/* routes
170
163
  // see populated slots after a successful preHandler run.
@@ -224,64 +217,18 @@ const authChainImpl = async (fastify) => {
224
217
  return;
225
218
  return;
226
219
  }
227
- // 3. Legacy API_KEYS
228
- const legacyOutcome = await tryLegacy(request, {
229
- userRepository: fastify.userRepository,
230
- hashedEntries,
231
- });
232
- if (legacyOutcome.kind === 'fail') {
233
- return sendUnauthorized(request, reply, 'legacy', legacyOutcome.reasonCode);
234
- }
235
- if (legacyOutcome.kind === 'match') {
236
- applyPrincipal(request, legacyOutcome.result, legacyOutcome.label);
237
- // Plan 31-05 (MIGR-02): emit one warn log per legacy-authed request
238
- // so operators can grep their log feed for sunset-readiness reporting
239
- // (`event: 'legacy_auth_used'`). The onSend hook below stamps the
240
- // RFC 8594 Deprecation/Sunset headers; this line is the canonical
241
- // audit signal — the headers are advisory to the client, the log
242
- // is the operator-side source of truth.
243
- request.log.warn({
244
- event: 'legacy_auth_used',
245
- userId: legacyOutcome.result.user.id,
246
- apiKeyLabel: legacyOutcome.label,
247
- requestId: request.id,
248
- requestUrl: request.url,
249
- sunset: config.LEGACY_AUTH_SUNSET_DATE,
250
- }, 'legacy_auth_used');
251
- if (enforceSessionOnly(request, reply))
252
- return;
253
- return;
254
- }
255
- // 4. Catch-all — no strategy saw a credential. Per Plan-04 Decision
256
- // Q6, the audit log records `strategy: 'legacy', reasonCode:
257
- // 'missing_credential'` so the failure mode matches the pre-split
258
- // plugin's "missing X-API-Key" branch.
220
+ // 3. Catch-all — no strategy saw a credential (the legacy X-API-Key
221
+ // strategy was removed in the v2.0 auth cutover, Phase 0). A request
222
+ // bearing only an X-API-Key header and no PAT/session now falls
223
+ // through to here and receives a uniform 401. The audit log records
224
+ // `strategy: 'legacy', reasonCode: 'missing_credential'` so the
225
+ // failure mode still classifies as a missing-credential event.
259
226
  return sendUnauthorized(request, reply, 'legacy', 'missing_credential');
260
227
  }
261
228
  catch (err) {
262
229
  return sendInternalError(request, reply, err);
263
230
  }
264
231
  });
265
- // Plan 31-05 (MIGR-02): RFC 8594 Deprecation + Sunset response headers
266
- // for every legacy-X-API-Key-authed request. Gated strictly on
267
- // `request.authMethod === 'legacy'` so PAT, session, anonymous (skipAuth),
268
- // and failed-auth responses NEVER carry the headers (Pitfall 4 in
269
- // 31-RESEARCH §Common Pitfalls).
270
- //
271
- // Callback-style (4-arg) signature is used INTENTIONALLY rather than async
272
- // — registering an async onSend hook inside this fp()-wrapped plugin
273
- // delays `reply.sent` from becoming true synchronously when the preHandler
274
- // calls `reply.send()` (e.g. from `enforceSessionOnly`'s 403). The
275
- // me-tokens session-only tests then see the route handler run after the
276
- // 403 reply was queued. The synchronous callback form keeps reply.send()
277
- // synchronous, preserving the Phase 28 sessionOnly invariant.
278
- fastify.addHook('onSend', (request, reply, payload, done) => {
279
- if (request.authMethod === 'legacy') {
280
- reply.header('Deprecation', 'true');
281
- reply.header('Sunset', config.LEGACY_AUTH_SUNSET_DATE);
282
- }
283
- done(null, payload);
284
- });
285
232
  };
286
233
  /**
287
234
  * Wrap with fastify-plugin to escape the encapsulated scope. Without `fp()`