wood-fired-tasks 1.18.2 → 2.0.1
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.
- package/CHANGELOG.md +87 -1
- package/README.md +25 -26
- package/SECURITY.md +30 -67
- package/dist/api/plugins/auth/index.d.ts +13 -15
- package/dist/api/plugins/auth/index.js +16 -69
- package/dist/api/plugins/auth/keys.d.ts +13 -11
- package/dist/api/plugins/auth/keys.js +23 -76
- package/dist/api/plugins/auth.d.ts +2 -2
- package/dist/api/plugins/auth.js +2 -2
- package/dist/api/plugins/swagger.js +4 -11
- package/dist/api/start.js +5 -3
- package/dist/cli/api/client.js +3 -6
- package/dist/cli/auth/credentials.d.ts +0 -3
- package/dist/cli/auth/credentials.js +6 -9
- package/dist/cli/bin/tasks-client.js +1 -1
- package/dist/cli/bin/tasks.js +10 -2
- package/dist/cli/cache/count-cache.d.ts +82 -0
- package/dist/cli/cache/count-cache.js +110 -0
- package/dist/cli/cache/paths.d.ts +25 -0
- package/dist/cli/cache/paths.js +71 -0
- package/dist/cli/commands/backup.js +2 -1
- package/dist/cli/commands/completed.js +2 -1
- package/dist/cli/commands/db-check.js +2 -1
- package/dist/cli/commands/db-migrate-identities.js +2 -1
- package/dist/cli/commands/db-mint-token.js +12 -3
- package/dist/cli/commands/doctor.d.ts +137 -0
- package/dist/cli/commands/doctor.js +354 -5
- package/dist/cli/commands/link-project.d.ts +2 -0
- package/dist/cli/commands/link-project.js +126 -0
- package/dist/cli/commands/login.d.ts +40 -0
- package/dist/cli/commands/login.js +64 -40
- package/dist/cli/commands/self-update.js +9 -8
- package/dist/cli/commands/serve.js +8 -0
- package/dist/cli/commands/setup.d.ts +343 -7
- package/dist/cli/commands/setup.js +596 -15
- package/dist/cli/commands/stats.js +2 -1
- package/dist/cli/commands/statusline.d.ts +66 -0
- package/dist/cli/commands/statusline.js +305 -0
- package/dist/cli/commands/topology.js +2 -1
- package/dist/cli/commands/whoami.d.ts +1 -3
- package/dist/cli/commands/whoami.js +1 -11
- package/dist/cli/commands/wsjf.js +4 -3
- package/dist/cli/config/update-check.d.ts +43 -0
- package/dist/cli/config/update-check.js +158 -0
- package/dist/cli/statusline/count-fetcher.d.ts +58 -0
- package/dist/cli/statusline/count-fetcher.js +69 -0
- package/dist/cli/statusline/format-segment.d.ts +70 -0
- package/dist/cli/statusline/format-segment.js +142 -0
- package/dist/cli/statusline/resolve-project.d.ts +60 -0
- package/dist/cli/statusline/resolve-project.js +160 -0
- package/dist/cli/update/check-writer.d.ts +40 -0
- package/dist/cli/update/check-writer.js +106 -0
- package/dist/cli/util/npm-spawn.d.ts +44 -0
- package/dist/cli/util/npm-spawn.js +50 -0
- package/dist/cli/util/prompt.d.ts +84 -0
- package/dist/cli/util/prompt.js +172 -0
- package/dist/config/db-path.d.ts +16 -0
- package/dist/config/db-path.js +72 -0
- package/dist/config/env.d.ts +5 -6
- package/dist/config/env.js +20 -15
- package/dist/config/paths.js +4 -0
- package/dist/db/migrate.d.ts +18 -11
- package/dist/db/migrate.js +22 -16
- package/dist/db/migrations/005-backlogged-status.js +72 -2
- package/dist/index.js +5 -2
- package/dist/mcp/identity-resolution.js +1 -2
- package/dist/mcp/index.js +17 -8
- package/dist/mcp/remote/index.d.ts +7 -13
- package/dist/mcp/remote/index.js +41 -6
- package/dist/mcp/remote/rest-client.d.ts +11 -15
- package/dist/mcp/remote/rest-client.js +12 -26
- package/dist/mcp/resources/events.d.ts +4 -4
- package/dist/mcp/resources/events.js +8 -8
- package/dist/services/identity-seeder.d.ts +14 -11
- package/dist/services/identity-seeder.js +19 -33
- package/dist/skills/tasks/decompose.md +95 -10
- package/dist/skills/tasks/update.md +46 -0
- package/docs/AGENT_CONTEXT.md +1 -1
- package/docs/API.md +59 -61
- package/docs/ARCHITECTURE.md +15 -16
- package/docs/CLI.md +202 -8
- package/docs/INTERFACES.md +12 -10
- package/docs/MCP.md +47 -57
- package/docs/SETUP.md +274 -164
- package/docs/TROUBLESHOOTING.md +1 -1
- package/docs/WORKFLOWS.md +19 -16
- package/package.json +1 -1
- package/packages/wft-router/package.json +1 -1
- package/dist/api/plugins/auth/strategies/legacy.d.ts +0 -46
- package/dist/api/plugins/auth/strategies/legacy.js +0 -87
package/CHANGELOG.md
CHANGED
|
@@ -13,6 +13,91 @@ vulnerabilities, supply-chain pinning) are always called out under `Security`.
|
|
|
13
13
|
|
|
14
14
|
_No changes yet._
|
|
15
15
|
|
|
16
|
+
## [v2.0.1] - 2026-06-08
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- **`tasks setup` interactive "Remote" selection no longer crashes.** Choosing
|
|
20
|
+
option **3) Remote** from the interactive setup menu threw
|
|
21
|
+
`remote onboarding requires a --remote <url> base URL.` because the menu set
|
|
22
|
+
the mode but never captured the server URL — only the `--remote <url>` flag
|
|
23
|
+
did. The interactive remote path now prompts for the base URL when it is not
|
|
24
|
+
supplied as a flag, guarded by a TTY check so non-interactive/CI callers still
|
|
25
|
+
fail fast with the same clear message instead of hanging on stdin.
|
|
26
|
+
- **DB-path legacy-adopt tests are now hermetic.** `migrate-db-path.test.ts` and
|
|
27
|
+
`config-path-e2e.test.ts` exercise the "adopt `./data/tasks.db` when the OS
|
|
28
|
+
app-data DB is absent" precedence, but probed the real filesystem and so
|
|
29
|
+
failed on any dev machine where the `tasks` CLI had already created
|
|
30
|
+
`~/.local/share/wood-fired-tasks/tasks.db` (they passed on clean CI by luck).
|
|
31
|
+
`resolveMigrateDbPath`/`migrateCli` now forward the resolver's existing
|
|
32
|
+
injectable `exists` seam (default `fs.existsSync`, no behavior change), and the
|
|
33
|
+
tests inject a probe that hides the app-data DB — making the precedence-2
|
|
34
|
+
cases deterministic regardless of the developer's local state.
|
|
35
|
+
|
|
36
|
+
## [v2.0.0] - 2026-06-07
|
|
37
|
+
|
|
38
|
+
The **identity auth cutover** — a breaking major. The legacy `X-API-Key`
|
|
39
|
+
shared-secret auth path is **removed**; every API call now authenticates with a
|
|
40
|
+
per-user **Bearer personal access token (PAT)** minted through the identity
|
|
41
|
+
system. Onboarding gains an OIDC device flow and explicit setup modes, and the
|
|
42
|
+
CLI grows `statusline` and `link-project` commands. **No credential migration is
|
|
43
|
+
required** — pre-identity (`is_legacy=1`) rows are left **inert** and read back
|
|
44
|
+
unchanged — and the one schema migration that touches existing data
|
|
45
|
+
(migration 005) was hardened to preserve child rows (see _Fixed_).
|
|
46
|
+
|
|
47
|
+
### Changed
|
|
48
|
+
- **Auth is now Bearer-PAT only.** Every authenticated REST/MCP request carries
|
|
49
|
+
`Authorization: Bearer <pat>`; the previously-supported `X-API-Key` shared-key
|
|
50
|
+
header is no longer accepted. PATs are per-user, revocable, and optionally
|
|
51
|
+
expiring (see [`docs/SETUP.md`](docs/SETUP.md)). This is the breaking change
|
|
52
|
+
that makes v2.0 a major.
|
|
53
|
+
- **`setup` gains explicit Local/Service/Remote modes + an OIDC device flow.**
|
|
54
|
+
Running `wood-fired-tasks setup` with no args on a TTY presents a
|
|
55
|
+
Local/Service/Remote menu; `--local` / `--service` / `--remote <url>` pick a
|
|
56
|
+
path non-interactively. `setup --remote <url>` probes the server's OIDC state
|
|
57
|
+
and picks **device-flow** (OIDC ready) vs **manual-PAT** (OIDC
|
|
58
|
+
disabled/degraded) automatically, replacing the old key-paste onboarding. The
|
|
59
|
+
remote MCP entry is **URL-only** — the PAT is cached in the CLI credentials
|
|
60
|
+
file, never written into `~/.claude.json` (#810).
|
|
61
|
+
- **`API_KEYS` is no longer an auth method.** It only seeds inert `is_legacy=1`
|
|
62
|
+
identity rows; the dead production boot-gate that required it was removed.
|
|
63
|
+
|
|
64
|
+
### Added
|
|
65
|
+
- **OIDC device-flow onboarding** for `setup --remote`, minting a PAT without a
|
|
66
|
+
hand-copied shared key when the server has OIDC enabled.
|
|
67
|
+
- **`statusline` CLI command** for a compact at-a-glance status line, and
|
|
68
|
+
**`link-project`** to write a `.wft/project` marker that pins the working
|
|
69
|
+
directory to a project (consumed by `statusline` and CLI project resolution).
|
|
70
|
+
- **`/tasks:update` slash command** (the packaged `/tasks:*` skill set is now 19).
|
|
71
|
+
- **Update-available notifier** with an opt-out: set `WFT_NO_UPDATE_CHECK=1`
|
|
72
|
+
(env) or `update_check = false` (CLI config) to disable the best-effort
|
|
73
|
+
"newer version available" check.
|
|
74
|
+
|
|
75
|
+
### Fixed
|
|
76
|
+
- **Unified DB-path resolution — fixes silent data loss on upgrade.** `serve`/the
|
|
77
|
+
API defaulted `DATABASE_PATH` to the OS app-data dir while `migrate` and the
|
|
78
|
+
`tasks db*` commands hardcoded `./data/tasks.db`; with `DATABASE_PATH` unset
|
|
79
|
+
they could open **different** databases, silently abandoning an upgrading
|
|
80
|
+
user's `./data/tasks.db`. A single `resolveDbPath()`
|
|
81
|
+
([`src/config/db-path.ts`](src/config/db-path.ts)) is now the source of truth:
|
|
82
|
+
explicit `DATABASE_PATH` (or the `DB_PATH` alias) > adopt an existing legacy
|
|
83
|
+
`./data/tasks.db` > app-data default.
|
|
84
|
+
- **Migration 005 no longer cascade-deletes data.** `PRAGMA foreign_keys = OFF`
|
|
85
|
+
is a no-op inside a transaction, so 005's table rebuild CASCADE-deleted
|
|
86
|
+
`task_comments` / `dependencies` / `tags` when run against a populated pre-005
|
|
87
|
+
database. The migration now snapshots and restores child rows around the
|
|
88
|
+
rebuild (covered by a populated-DB round-trip test).
|
|
89
|
+
|
|
90
|
+
### Security
|
|
91
|
+
- **Removed the `X-API-Key` shared-secret auth path entirely** (the v2.0
|
|
92
|
+
cutover). A single long-lived shared key was the broadest part of the auth
|
|
93
|
+
surface; replacing it with per-user, individually-revocable Bearer PATs scopes
|
|
94
|
+
credentials to a user, makes revocation surgical, and bounds blast radius on
|
|
95
|
+
leak. Legacy `is_legacy=1` rows are left inert — **no credential migration is
|
|
96
|
+
required**.
|
|
97
|
+
- **Closed an upgrade-time data-loss path** (the migration-005 cascade delete;
|
|
98
|
+
see _Fixed_) — a data-integrity fix for anyone upgrading across migration 005
|
|
99
|
+
with populated tables.
|
|
100
|
+
|
|
16
101
|
## [v1.18.2] - 2026-06-06
|
|
17
102
|
|
|
18
103
|
A patch release: a Windows `self-update` fix plus two install-experience
|
|
@@ -561,7 +646,8 @@ and the task/project/dependency/comment/subtask domain model.
|
|
|
561
646
|
- Task hierarchy (subtasks), dependency service, comments, time estimates
|
|
562
647
|
(phase 06).
|
|
563
648
|
|
|
564
|
-
[Unreleased]: https://github.com/Wood-Fired-Games/wood-fired-tasks/compare/
|
|
649
|
+
[Unreleased]: https://github.com/Wood-Fired-Games/wood-fired-tasks/compare/v2.0.0...HEAD
|
|
650
|
+
[v2.0.0]: https://github.com/Wood-Fired-Games/wood-fired-tasks/compare/v1.18.2...v2.0.0
|
|
565
651
|
[v1.15]: https://github.com/Wood-Fired-Games/wood-fired-tasks/compare/v1.14...v1.15
|
|
566
652
|
[v1.14]: https://github.com/Wood-Fired-Games/wood-fired-tasks/compare/v1.13...v1.14
|
|
567
653
|
[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
|
|
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
|
|
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 **
|
|
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
|
|
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
|
|
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`
|
|
174
|
+
### Legacy `X-API-Key` was removed in v2.0
|
|
176
175
|
|
|
177
|
-
The legacy `X-API-Key` strategy
|
|
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/>
|
|
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 +
|
|
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`)
|
|
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 (
|
|
286
|
-
| MCP Server | stdio JSON-RPC (local) or HTTP (remote variant) | MCP client integration | None for stdio (local access); Bearer PAT
|
|
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_…`)
|
|
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
|
|
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 **
|
|
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 "
|
|
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 "
|
|
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 "
|
|
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 "
|
|
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 "
|
|
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 "
|
|
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 |
|
|
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`) |
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
|
151
|
-
server and the CLI HTTP client
|
|
152
|
-
|
|
153
|
-
|
|
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
|
|
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
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
22
|
-
*
|
|
23
|
-
*
|
|
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
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
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'` (
|
|
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
|
-
//
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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.
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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()`
|