wood-fired-tasks 1.12.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.
- package/AGENTS.md +112 -0
- package/CHANGELOG.md +271 -0
- package/CLAUDE.md +21 -0
- package/LICENSE +21 -0
- package/README.md +687 -0
- package/SECURITY.md +299 -0
- package/dist/api/hooks/error-handler.d.ts +6 -0
- package/dist/api/hooks/error-handler.js +128 -0
- package/dist/api/hooks/error-handler.js.map +1 -0
- package/dist/api/plugins/auth/index.d.ts +78 -0
- package/dist/api/plugins/auth/index.js +300 -0
- package/dist/api/plugins/auth/index.js.map +1 -0
- package/dist/api/plugins/auth/keys.d.ts +23 -0
- package/dist/api/plugins/auth/keys.js +100 -0
- package/dist/api/plugins/auth/keys.js.map +1 -0
- package/dist/api/plugins/auth/strategies/legacy.d.ts +46 -0
- package/dist/api/plugins/auth/strategies/legacy.js +87 -0
- package/dist/api/plugins/auth/strategies/legacy.js.map +1 -0
- package/dist/api/plugins/auth/strategies/pat.d.ts +37 -0
- package/dist/api/plugins/auth/strategies/pat.js +99 -0
- package/dist/api/plugins/auth/strategies/pat.js.map +1 -0
- package/dist/api/plugins/auth/strategies/session.d.ts +37 -0
- package/dist/api/plugins/auth/strategies/session.js +30 -0
- package/dist/api/plugins/auth/strategies/session.js.map +1 -0
- package/dist/api/plugins/auth/strategies/types.d.ts +12 -0
- package/dist/api/plugins/auth/strategies/types.js +2 -0
- package/dist/api/plugins/auth/strategies/types.js.map +1 -0
- package/dist/api/plugins/auth.d.ts +29 -0
- package/dist/api/plugins/auth.js +30 -0
- package/dist/api/plugins/auth.js.map +1 -0
- package/dist/api/plugins/swagger.d.ts +31 -0
- package/dist/api/plugins/swagger.js +83 -0
- package/dist/api/plugins/swagger.js.map +1 -0
- package/dist/api/routes/auth/auth-error.d.ts +23 -0
- package/dist/api/routes/auth/auth-error.js +68 -0
- package/dist/api/routes/auth/auth-error.js.map +1 -0
- package/dist/api/routes/auth/callback.d.ts +44 -0
- package/dist/api/routes/auth/callback.js +175 -0
- package/dist/api/routes/auth/callback.js.map +1 -0
- package/dist/api/routes/auth/csrf.d.ts +24 -0
- package/dist/api/routes/auth/csrf.js +75 -0
- package/dist/api/routes/auth/csrf.js.map +1 -0
- package/dist/api/routes/auth/device-code.d.ts +41 -0
- package/dist/api/routes/auth/device-code.js +51 -0
- package/dist/api/routes/auth/device-code.js.map +1 -0
- package/dist/api/routes/auth/device-disabled-stub.d.ts +33 -0
- package/dist/api/routes/auth/device-disabled-stub.js +16 -0
- package/dist/api/routes/auth/device-disabled-stub.js.map +1 -0
- package/dist/api/routes/auth/device-html.d.ts +41 -0
- package/dist/api/routes/auth/device-html.js +243 -0
- package/dist/api/routes/auth/device-html.js.map +1 -0
- package/dist/api/routes/auth/device-token.d.ts +38 -0
- package/dist/api/routes/auth/device-token.js +153 -0
- package/dist/api/routes/auth/device-token.js.map +1 -0
- package/dist/api/routes/auth/disabled-stub.d.ts +31 -0
- package/dist/api/routes/auth/disabled-stub.js +21 -0
- package/dist/api/routes/auth/disabled-stub.js.map +1 -0
- package/dist/api/routes/auth/index.d.ts +65 -0
- package/dist/api/routes/auth/index.js +47 -0
- package/dist/api/routes/auth/index.js.map +1 -0
- package/dist/api/routes/auth/login.d.ts +27 -0
- package/dist/api/routes/auth/login.js +91 -0
- package/dist/api/routes/auth/login.js.map +1 -0
- package/dist/api/routes/auth/logout.d.ts +29 -0
- package/dist/api/routes/auth/logout.js +43 -0
- package/dist/api/routes/auth/logout.js.map +1 -0
- package/dist/api/routes/comments/index.d.ts +3 -0
- package/dist/api/routes/comments/index.js +74 -0
- package/dist/api/routes/comments/index.js.map +1 -0
- package/dist/api/routes/comments/schemas.d.ts +37 -0
- package/dist/api/routes/comments/schemas.js +24 -0
- package/dist/api/routes/comments/schemas.js.map +1 -0
- package/dist/api/routes/dependencies/index.d.ts +3 -0
- package/dist/api/routes/dependencies/index.js +60 -0
- package/dist/api/routes/dependencies/index.js.map +1 -0
- package/dist/api/routes/dependencies/schemas.d.ts +24 -0
- package/dist/api/routes/dependencies/schemas.js +15 -0
- package/dist/api/routes/dependencies/schemas.js.map +1 -0
- package/dist/api/routes/events.d.ts +3 -0
- package/dist/api/routes/events.js +157 -0
- package/dist/api/routes/events.js.map +1 -0
- package/dist/api/routes/health.d.ts +27 -0
- package/dist/api/routes/health.js +231 -0
- package/dist/api/routes/health.js.map +1 -0
- package/dist/api/routes/me/index.d.ts +3 -0
- package/dist/api/routes/me/index.js +8 -0
- package/dist/api/routes/me/index.js.map +1 -0
- package/dist/api/routes/me/profile.d.ts +3 -0
- package/dist/api/routes/me/profile.js +58 -0
- package/dist/api/routes/me/profile.js.map +1 -0
- package/dist/api/routes/me/tokens.d.ts +3 -0
- package/dist/api/routes/me/tokens.js +410 -0
- package/dist/api/routes/me/tokens.js.map +1 -0
- package/dist/api/routes/projects/dependency-graph.d.ts +26 -0
- package/dist/api/routes/projects/dependency-graph.js +56 -0
- package/dist/api/routes/projects/dependency-graph.js.map +1 -0
- package/dist/api/routes/projects/index.d.ts +3 -0
- package/dist/api/routes/projects/index.js +96 -0
- package/dist/api/routes/projects/index.js.map +1 -0
- package/dist/api/routes/projects/schemas.d.ts +37 -0
- package/dist/api/routes/projects/schemas.js +27 -0
- package/dist/api/routes/projects/schemas.js.map +1 -0
- package/dist/api/routes/projects/topology.d.ts +27 -0
- package/dist/api/routes/projects/topology.js +51 -0
- package/dist/api/routes/projects/topology.js.map +1 -0
- package/dist/api/routes/tasks/index.d.ts +3 -0
- package/dist/api/routes/tasks/index.js +330 -0
- package/dist/api/routes/tasks/index.js.map +1 -0
- package/dist/api/routes/tasks/schemas.d.ts +316 -0
- package/dist/api/routes/tasks/schemas.js +129 -0
- package/dist/api/routes/tasks/schemas.js.map +1 -0
- package/dist/api/routes/web/index.d.ts +23 -0
- package/dist/api/routes/web/index.js +30 -0
- package/dist/api/routes/web/index.js.map +1 -0
- package/dist/api/routes/web/login.d.ts +20 -0
- package/dist/api/routes/web/login.js +20 -0
- package/dist/api/routes/web/login.js.map +1 -0
- package/dist/api/routes/web/me.d.ts +23 -0
- package/dist/api/routes/web/me.js +31 -0
- package/dist/api/routes/web/me.js.map +1 -0
- package/dist/api/routes/web/tokens.d.ts +29 -0
- package/dist/api/routes/web/tokens.js +65 -0
- package/dist/api/routes/web/tokens.js.map +1 -0
- package/dist/api/server.d.ts +44 -0
- package/dist/api/server.js +521 -0
- package/dist/api/server.js.map +1 -0
- package/dist/api/start.d.ts +1 -0
- package/dist/api/start.js +79 -0
- package/dist/api/start.js.map +1 -0
- package/dist/cli/api/client.d.ts +126 -0
- package/dist/cli/api/client.js +408 -0
- package/dist/cli/api/client.js.map +1 -0
- package/dist/cli/api/errors.d.ts +16 -0
- package/dist/cli/api/errors.js +20 -0
- package/dist/cli/api/errors.js.map +1 -0
- package/dist/cli/api/types.d.ts +205 -0
- package/dist/cli/api/types.js +15 -0
- package/dist/cli/api/types.js.map +1 -0
- package/dist/cli/auth/browser-open.d.ts +1 -0
- package/dist/cli/auth/browser-open.js +116 -0
- package/dist/cli/auth/browser-open.js.map +1 -0
- package/dist/cli/auth/credentials.d.ts +27 -0
- package/dist/cli/auth/credentials.js +179 -0
- package/dist/cli/auth/credentials.js.map +1 -0
- package/dist/cli/auth/device-flow.d.ts +75 -0
- package/dist/cli/auth/device-flow.js +149 -0
- package/dist/cli/auth/device-flow.js.map +1 -0
- package/dist/cli/bin/tasks-client.d.ts +2 -0
- package/dist/cli/bin/tasks-client.js +86 -0
- package/dist/cli/bin/tasks-client.js.map +1 -0
- package/dist/cli/bin/tasks.d.ts +3 -0
- package/dist/cli/bin/tasks.js +127 -0
- package/dist/cli/bin/tasks.js.map +1 -0
- package/dist/cli/commands/backup.d.ts +3 -0
- package/dist/cli/commands/backup.js +65 -0
- package/dist/cli/commands/backup.js.map +1 -0
- package/dist/cli/commands/claim.d.ts +2 -0
- package/dist/cli/commands/claim.js +37 -0
- package/dist/cli/commands/claim.js.map +1 -0
- package/dist/cli/commands/comment-add.d.ts +2 -0
- package/dist/cli/commands/comment-add.js +45 -0
- package/dist/cli/commands/comment-add.js.map +1 -0
- package/dist/cli/commands/comment-delete.d.ts +2 -0
- package/dist/cli/commands/comment-delete.js +56 -0
- package/dist/cli/commands/comment-delete.js.map +1 -0
- package/dist/cli/commands/comment-list.d.ts +2 -0
- package/dist/cli/commands/comment-list.js +56 -0
- package/dist/cli/commands/comment-list.js.map +1 -0
- package/dist/cli/commands/completed.d.ts +10 -0
- package/dist/cli/commands/completed.js +168 -0
- package/dist/cli/commands/completed.js.map +1 -0
- package/dist/cli/commands/completions.d.ts +10 -0
- package/dist/cli/commands/completions.js +179 -0
- package/dist/cli/commands/completions.js.map +1 -0
- package/dist/cli/commands/create.d.ts +2 -0
- package/dist/cli/commands/create.js +99 -0
- package/dist/cli/commands/create.js.map +1 -0
- package/dist/cli/commands/db-check.d.ts +3 -0
- package/dist/cli/commands/db-check.js +63 -0
- package/dist/cli/commands/db-check.js.map +1 -0
- package/dist/cli/commands/db-migrate-identities.d.ts +37 -0
- package/dist/cli/commands/db-migrate-identities.js +352 -0
- package/dist/cli/commands/db-migrate-identities.js.map +1 -0
- package/dist/cli/commands/db-mint-token.d.ts +33 -0
- package/dist/cli/commands/db-mint-token.js +150 -0
- package/dist/cli/commands/db-mint-token.js.map +1 -0
- package/dist/cli/commands/db.d.ts +10 -0
- package/dist/cli/commands/db.js +16 -0
- package/dist/cli/commands/db.js.map +1 -0
- package/dist/cli/commands/delete.d.ts +2 -0
- package/dist/cli/commands/delete.js +54 -0
- package/dist/cli/commands/delete.js.map +1 -0
- package/dist/cli/commands/dep-add.d.ts +2 -0
- package/dist/cli/commands/dep-add.js +43 -0
- package/dist/cli/commands/dep-add.js.map +1 -0
- package/dist/cli/commands/dep-list.d.ts +2 -0
- package/dist/cli/commands/dep-list.js +36 -0
- package/dist/cli/commands/dep-list.js.map +1 -0
- package/dist/cli/commands/dep-remove.d.ts +2 -0
- package/dist/cli/commands/dep-remove.js +55 -0
- package/dist/cli/commands/dep-remove.js.map +1 -0
- package/dist/cli/commands/doctor.d.ts +3 -0
- package/dist/cli/commands/doctor.js +139 -0
- package/dist/cli/commands/doctor.js.map +1 -0
- package/dist/cli/commands/health.d.ts +2 -0
- package/dist/cli/commands/health.js +28 -0
- package/dist/cli/commands/health.js.map +1 -0
- package/dist/cli/commands/list.d.ts +2 -0
- package/dist/cli/commands/list.js +103 -0
- package/dist/cli/commands/list.js.map +1 -0
- package/dist/cli/commands/login.d.ts +2 -0
- package/dist/cli/commands/login.js +210 -0
- package/dist/cli/commands/login.js.map +1 -0
- package/dist/cli/commands/logout.d.ts +31 -0
- package/dist/cli/commands/logout.js +184 -0
- package/dist/cli/commands/logout.js.map +1 -0
- package/dist/cli/commands/project-create.d.ts +2 -0
- package/dist/cli/commands/project-create.js +44 -0
- package/dist/cli/commands/project-create.js.map +1 -0
- package/dist/cli/commands/project-delete.d.ts +2 -0
- package/dist/cli/commands/project-delete.js +54 -0
- package/dist/cli/commands/project-delete.js.map +1 -0
- package/dist/cli/commands/project-list.d.ts +2 -0
- package/dist/cli/commands/project-list.js +55 -0
- package/dist/cli/commands/project-list.js.map +1 -0
- package/dist/cli/commands/project-show.d.ts +2 -0
- package/dist/cli/commands/project-show.js +38 -0
- package/dist/cli/commands/project-show.js.map +1 -0
- package/dist/cli/commands/project-update.d.ts +2 -0
- package/dist/cli/commands/project-update.js +56 -0
- package/dist/cli/commands/project-update.js.map +1 -0
- package/dist/cli/commands/show.d.ts +2 -0
- package/dist/cli/commands/show.js +38 -0
- package/dist/cli/commands/show.js.map +1 -0
- package/dist/cli/commands/stats.d.ts +3 -0
- package/dist/cli/commands/stats.js +81 -0
- package/dist/cli/commands/stats.js.map +1 -0
- package/dist/cli/commands/subtask-create.d.ts +2 -0
- package/dist/cli/commands/subtask-create.js +85 -0
- package/dist/cli/commands/subtask-create.js.map +1 -0
- package/dist/cli/commands/subtask-list.d.ts +2 -0
- package/dist/cli/commands/subtask-list.js +61 -0
- package/dist/cli/commands/subtask-list.js.map +1 -0
- package/dist/cli/commands/topology.d.ts +16 -0
- package/dist/cli/commands/topology.js +52 -0
- package/dist/cli/commands/topology.js.map +1 -0
- package/dist/cli/commands/update.d.ts +2 -0
- package/dist/cli/commands/update.js +90 -0
- package/dist/cli/commands/update.js.map +1 -0
- package/dist/cli/commands/whoami.d.ts +30 -0
- package/dist/cli/commands/whoami.js +201 -0
- package/dist/cli/commands/whoami.js.map +1 -0
- package/dist/cli/config/env.d.ts +4 -0
- package/dist/cli/config/env.js +25 -0
- package/dist/cli/config/env.js.map +1 -0
- package/dist/cli/output/error-handler.d.ts +5 -0
- package/dist/cli/output/error-handler.js +41 -0
- package/dist/cli/output/error-handler.js.map +1 -0
- package/dist/cli/output/formatters.d.ts +86 -0
- package/dist/cli/output/formatters.js +352 -0
- package/dist/cli/output/formatters.js.map +1 -0
- package/dist/cli/output/json-output.d.ts +39 -0
- package/dist/cli/output/json-output.js +50 -0
- package/dist/cli/output/json-output.js.map +1 -0
- package/dist/cli/output/spinner.d.ts +14 -0
- package/dist/cli/output/spinner.js +48 -0
- package/dist/cli/output/spinner.js.map +1 -0
- package/dist/cli/prompts/interactive.d.ts +25 -0
- package/dist/cli/prompts/interactive.js +80 -0
- package/dist/cli/prompts/interactive.js.map +1 -0
- package/dist/config/env.d.ts +182 -0
- package/dist/config/env.js +311 -0
- package/dist/config/env.js.map +1 -0
- package/dist/db/database.d.ts +11 -0
- package/dist/db/database.js +25 -0
- package/dist/db/database.js.map +1 -0
- package/dist/db/migrate.d.ts +10 -0
- package/dist/db/migrate.js +137 -0
- package/dist/db/migrate.js.map +1 -0
- package/dist/db/migrations/001-initial-schema.d.ts +3 -0
- package/dist/db/migrations/001-initial-schema.js +100 -0
- package/dist/db/migrations/001-initial-schema.js.map +1 -0
- package/dist/db/migrations/002-task-hierarchy-and-dependencies.d.ts +3 -0
- package/dist/db/migrations/002-task-hierarchy-and-dependencies.js +42 -0
- package/dist/db/migrations/002-task-hierarchy-and-dependencies.js.map +1 -0
- package/dist/db/migrations/003-comments-and-estimates.d.ts +3 -0
- package/dist/db/migrations/003-comments-and-estimates.js +36 -0
- package/dist/db/migrations/003-comments-and-estimates.js.map +1 -0
- package/dist/db/migrations/004-claim-protocol.d.ts +3 -0
- package/dist/db/migrations/004-claim-protocol.js +41 -0
- package/dist/db/migrations/004-claim-protocol.js.map +1 -0
- package/dist/db/migrations/005-backlogged-status.d.ts +3 -0
- package/dist/db/migrations/005-backlogged-status.js +156 -0
- package/dist/db/migrations/005-backlogged-status.js.map +1 -0
- package/dist/db/migrations/006-slack-channel-subscriptions.d.ts +3 -0
- package/dist/db/migrations/006-slack-channel-subscriptions.js +23 -0
- package/dist/db/migrations/006-slack-channel-subscriptions.js.map +1 -0
- package/dist/db/migrations/007-completed-at.d.ts +19 -0
- package/dist/db/migrations/007-completed-at.js +36 -0
- package/dist/db/migrations/007-completed-at.js.map +1 -0
- package/dist/db/migrations/008-identity-tables.d.ts +21 -0
- package/dist/db/migrations/008-identity-tables.js +84 -0
- package/dist/db/migrations/008-identity-tables.js.map +1 -0
- package/dist/db/migrations/009-parallel-fk-columns.d.ts +33 -0
- package/dist/db/migrations/009-parallel-fk-columns.js +62 -0
- package/dist/db/migrations/009-parallel-fk-columns.js.map +1 -0
- package/dist/db/migrations/010-identity-uniqueness-indexes.d.ts +47 -0
- package/dist/db/migrations/010-identity-uniqueness-indexes.js +65 -0
- package/dist/db/migrations/010-identity-uniqueness-indexes.js.map +1 -0
- package/dist/db/migrations/011-acceptance-criteria.d.ts +30 -0
- package/dist/db/migrations/011-acceptance-criteria.js +41 -0
- package/dist/db/migrations/011-acceptance-criteria.js.map +1 -0
- package/dist/db/migrations/012-verification-evidence.d.ts +41 -0
- package/dist/db/migrations/012-verification-evidence.js +49 -0
- package/dist/db/migrations/012-verification-evidence.js.map +1 -0
- package/dist/events/event-bus.d.ts +101 -0
- package/dist/events/event-bus.js +184 -0
- package/dist/events/event-bus.js.map +1 -0
- package/dist/events/sse-manager.d.ts +79 -0
- package/dist/events/sse-manager.js +220 -0
- package/dist/events/sse-manager.js.map +1 -0
- package/dist/events/types.d.ts +43 -0
- package/dist/events/types.js +22 -0
- package/dist/events/types.js.map +1 -0
- package/dist/index.d.ts +110 -0
- package/dist/index.js +209 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/audit/schema.d.ts +201 -0
- package/dist/lib/audit/schema.js +141 -0
- package/dist/lib/audit/schema.js.map +1 -0
- package/dist/lib/decompose/schema.d.ts +157 -0
- package/dist/lib/decompose/schema.js +138 -0
- package/dist/lib/decompose/schema.js.map +1 -0
- package/dist/lib/loop-run/integration-audit-schema.d.ts +78 -0
- package/dist/lib/loop-run/integration-audit-schema.js +68 -0
- package/dist/lib/loop-run/integration-audit-schema.js.map +1 -0
- package/dist/lib/loop-run/schema.d.ts +49 -0
- package/dist/lib/loop-run/schema.js +67 -0
- package/dist/lib/loop-run/schema.js.map +1 -0
- package/dist/mcp/errors.d.ts +11 -0
- package/dist/mcp/errors.js +29 -0
- package/dist/mcp/errors.js.map +1 -0
- package/dist/mcp/identity-resolution.d.ts +76 -0
- package/dist/mcp/identity-resolution.js +189 -0
- package/dist/mcp/identity-resolution.js.map +1 -0
- package/dist/mcp/index.d.ts +1 -0
- package/dist/mcp/index.js +126 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/remote/index.d.ts +21 -0
- package/dist/mcp/remote/index.js +95 -0
- package/dist/mcp/remote/index.js.map +1 -0
- package/dist/mcp/remote/register-tools.d.ts +26 -0
- package/dist/mcp/remote/register-tools.js +751 -0
- package/dist/mcp/remote/register-tools.js.map +1 -0
- package/dist/mcp/remote/rest-client.d.ts +66 -0
- package/dist/mcp/remote/rest-client.js +300 -0
- package/dist/mcp/remote/rest-client.js.map +1 -0
- package/dist/mcp/resources/events.d.ts +28 -0
- package/dist/mcp/resources/events.js +98 -0
- package/dist/mcp/resources/events.js.map +1 -0
- package/dist/mcp/server.d.ts +59 -0
- package/dist/mcp/server.js +72 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/tools/comment-tools.d.ts +12 -0
- package/dist/mcp/tools/comment-tools.js +115 -0
- package/dist/mcp/tools/comment-tools.js.map +1 -0
- package/dist/mcp/tools/dependency-tools.d.ts +3 -0
- package/dist/mcp/tools/dependency-tools.js +91 -0
- package/dist/mcp/tools/dependency-tools.js.map +1 -0
- package/dist/mcp/tools/health-tools.d.ts +9 -0
- package/dist/mcp/tools/health-tools.js +82 -0
- package/dist/mcp/tools/health-tools.js.map +1 -0
- package/dist/mcp/tools/project-tools.d.ts +13 -0
- package/dist/mcp/tools/project-tools.js +167 -0
- package/dist/mcp/tools/project-tools.js.map +1 -0
- package/dist/mcp/tools/task-tools.d.ts +41 -0
- package/dist/mcp/tools/task-tools.js +434 -0
- package/dist/mcp/tools/task-tools.js.map +1 -0
- package/dist/mcp/tools/topology-tools.d.ts +14 -0
- package/dist/mcp/tools/topology-tools.js +46 -0
- package/dist/mcp/tools/topology-tools.js.map +1 -0
- package/dist/repositories/api-token.repository.d.ts +40 -0
- package/dist/repositories/api-token.repository.js +63 -0
- package/dist/repositories/api-token.repository.js.map +1 -0
- package/dist/repositories/comment.repository.d.ts +17 -0
- package/dist/repositories/comment.repository.js +73 -0
- package/dist/repositories/comment.repository.js.map +1 -0
- package/dist/repositories/dependency.repository.d.ts +19 -0
- package/dist/repositories/dependency.repository.js +55 -0
- package/dist/repositories/dependency.repository.js.map +1 -0
- package/dist/repositories/errors.d.ts +29 -0
- package/dist/repositories/errors.js +48 -0
- package/dist/repositories/errors.js.map +1 -0
- package/dist/repositories/interfaces.d.ts +200 -0
- package/dist/repositories/interfaces.js +2 -0
- package/dist/repositories/interfaces.js.map +1 -0
- package/dist/repositories/project.repository.d.ts +21 -0
- package/dist/repositories/project.repository.js +97 -0
- package/dist/repositories/project.repository.js.map +1 -0
- package/dist/repositories/row-mapper.d.ts +40 -0
- package/dist/repositories/row-mapper.js +38 -0
- package/dist/repositories/row-mapper.js.map +1 -0
- package/dist/repositories/task.repository.d.ts +45 -0
- package/dist/repositories/task.repository.js +612 -0
- package/dist/repositories/task.repository.js.map +1 -0
- package/dist/repositories/types.d.ts +20 -0
- package/dist/repositories/types.js +11 -0
- package/dist/repositories/types.js.map +1 -0
- package/dist/repositories/user.repository.d.ts +121 -0
- package/dist/repositories/user.repository.js +209 -0
- package/dist/repositories/user.repository.js.map +1 -0
- package/dist/schemas/comment.schema.d.ts +24 -0
- package/dist/schemas/comment.schema.js +27 -0
- package/dist/schemas/comment.schema.js.map +1 -0
- package/dist/schemas/dependency-graph.schema.d.ts +181 -0
- package/dist/schemas/dependency-graph.schema.js +98 -0
- package/dist/schemas/dependency-graph.schema.js.map +1 -0
- package/dist/schemas/dependency.schema.d.ts +9 -0
- package/dist/schemas/dependency.schema.js +16 -0
- package/dist/schemas/dependency.schema.js.map +1 -0
- package/dist/schemas/idempotency.schema.d.ts +18 -0
- package/dist/schemas/idempotency.schema.js +22 -0
- package/dist/schemas/idempotency.schema.js.map +1 -0
- package/dist/schemas/task.schema.d.ts +369 -0
- package/dist/schemas/task.schema.js +276 -0
- package/dist/schemas/task.schema.js.map +1 -0
- package/dist/schemas/topology.schema.d.ts +56 -0
- package/dist/schemas/topology.schema.js +48 -0
- package/dist/schemas/topology.schema.js.map +1 -0
- package/dist/services/auth-audit.d.ts +46 -0
- package/dist/services/auth-audit.js +28 -0
- package/dist/services/auth-audit.js.map +1 -0
- package/dist/services/claim-release.service.d.ts +42 -0
- package/dist/services/claim-release.service.js +90 -0
- package/dist/services/claim-release.service.js.map +1 -0
- package/dist/services/comment.service.d.ts +44 -0
- package/dist/services/comment.service.js +96 -0
- package/dist/services/comment.service.js.map +1 -0
- package/dist/services/dependency-graph.service.d.ts +33 -0
- package/dist/services/dependency-graph.service.js +453 -0
- package/dist/services/dependency-graph.service.js.map +1 -0
- package/dist/services/dependency.service.d.ts +32 -0
- package/dist/services/dependency.service.js +79 -0
- package/dist/services/dependency.service.js.map +1 -0
- package/dist/services/device-flow-store.d.ts +155 -0
- package/dist/services/device-flow-store.js +323 -0
- package/dist/services/device-flow-store.js.map +1 -0
- package/dist/services/errors.d.ts +28 -0
- package/dist/services/errors.js +44 -0
- package/dist/services/errors.js.map +1 -0
- package/dist/services/idempotency.service.d.ts +37 -0
- package/dist/services/idempotency.service.js +54 -0
- package/dist/services/idempotency.service.js.map +1 -0
- package/dist/services/identity-seeder.d.ts +56 -0
- package/dist/services/identity-seeder.js +131 -0
- package/dist/services/identity-seeder.js.map +1 -0
- package/dist/services/oidc-boot.d.ts +73 -0
- package/dist/services/oidc-boot.js +66 -0
- package/dist/services/oidc-boot.js.map +1 -0
- package/dist/services/oidc-client.d.ts +99 -0
- package/dist/services/oidc-client.js +108 -0
- package/dist/services/oidc-client.js.map +1 -0
- package/dist/services/pat-hash.d.ts +23 -0
- package/dist/services/pat-hash.js +73 -0
- package/dist/services/pat-hash.js.map +1 -0
- package/dist/services/pat-touch-debounce.d.ts +65 -0
- package/dist/services/pat-touch-debounce.js +82 -0
- package/dist/services/pat-touch-debounce.js.map +1 -0
- package/dist/services/project.service.d.ts +41 -0
- package/dist/services/project.service.js +133 -0
- package/dist/services/project.service.js.map +1 -0
- package/dist/services/slack.service.d.ts +31 -0
- package/dist/services/slack.service.js +52 -0
- package/dist/services/slack.service.js.map +1 -0
- package/dist/services/task.service.d.ts +151 -0
- package/dist/services/task.service.js +425 -0
- package/dist/services/task.service.js.map +1 -0
- package/dist/services/topology.service.d.ts +65 -0
- package/dist/services/topology.service.js +170 -0
- package/dist/services/topology.service.js.map +1 -0
- package/dist/services/user-upsert.d.ts +43 -0
- package/dist/services/user-upsert.js +53 -0
- package/dist/services/user-upsert.js.map +1 -0
- package/dist/services/workflow-engine.d.ts +93 -0
- package/dist/services/workflow-engine.js +250 -0
- package/dist/services/workflow-engine.js.map +1 -0
- package/dist/slack/commands/tasks-command.d.ts +88 -0
- package/dist/slack/commands/tasks-command.js +920 -0
- package/dist/slack/commands/tasks-command.js.map +1 -0
- package/dist/slack/formatters/project-formatter.d.ts +19 -0
- package/dist/slack/formatters/project-formatter.js +94 -0
- package/dist/slack/formatters/project-formatter.js.map +1 -0
- package/dist/slack/notifier.d.ts +41 -0
- package/dist/slack/notifier.js +111 -0
- package/dist/slack/notifier.js.map +1 -0
- package/dist/slack/repositories/channel-subscription.repository.d.ts +25 -0
- package/dist/slack/repositories/channel-subscription.repository.js +51 -0
- package/dist/slack/repositories/channel-subscription.repository.js.map +1 -0
- package/dist/slack/task-formatter.d.ts +31 -0
- package/dist/slack/task-formatter.js +151 -0
- package/dist/slack/task-formatter.js.map +1 -0
- package/dist/slack/user-identity.d.ts +37 -0
- package/dist/slack/user-identity.js +70 -0
- package/dist/slack/user-identity.js.map +1 -0
- package/dist/types/identity.d.ts +84 -0
- package/dist/types/identity.js +6 -0
- package/dist/types/identity.js.map +1 -0
- package/dist/types/task.d.ts +202 -0
- package/dist/types/task.js +17 -0
- package/dist/types/task.js.map +1 -0
- package/dist/utils/cycle-detector.d.ts +25 -0
- package/dist/utils/cycle-detector.js +86 -0
- package/dist/utils/cycle-detector.js.map +1 -0
- package/dist/utils/exit-codes.d.ts +64 -0
- package/dist/utils/exit-codes.js +57 -0
- package/dist/utils/exit-codes.js.map +1 -0
- package/dist/utils/is-main.d.ts +12 -0
- package/dist/utils/is-main.js +24 -0
- package/dist/utils/is-main.js.map +1 -0
- package/dist/utils/version.d.ts +1 -0
- package/dist/utils/version.js +22 -0
- package/dist/utils/version.js.map +1 -0
- package/dist/web/html.d.ts +73 -0
- package/dist/web/html.js +154 -0
- package/dist/web/html.js.map +1 -0
- package/dist/web/pages/device.d.ts +19 -0
- package/dist/web/pages/device.js +76 -0
- package/dist/web/pages/device.js.map +1 -0
- package/dist/web/pages/error.d.ts +6 -0
- package/dist/web/pages/error.js +23 -0
- package/dist/web/pages/error.js.map +1 -0
- package/dist/web/pages/login.d.ts +5 -0
- package/dist/web/pages/login.js +22 -0
- package/dist/web/pages/login.js.map +1 -0
- package/dist/web/pages/me.d.ts +9 -0
- package/dist/web/pages/me.js +37 -0
- package/dist/web/pages/me.js.map +1 -0
- package/dist/web/pages/tokens.d.ts +20 -0
- package/dist/web/pages/tokens.js +89 -0
- package/dist/web/pages/tokens.js.map +1 -0
- package/dist/web/session-constants.d.ts +21 -0
- package/dist/web/session-constants.js +22 -0
- package/dist/web/session-constants.js.map +1 -0
- package/dist/web/session-flash.d.ts +15 -0
- package/dist/web/session-flash.js +11 -0
- package/dist/web/session-flash.js.map +1 -0
- package/dist/web/session-user.d.ts +59 -0
- package/dist/web/session-user.js +44 -0
- package/dist/web/session-user.js.map +1 -0
- package/docs/AGENT_CONTEXT.md +280 -0
- package/docs/README.md +49 -0
- package/llms.txt +33 -0
- package/package.json +129 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import fp from 'fastify-plugin';
|
|
2
|
+
import { parseApiKeyEntries, config, } from '../../../config/env.js';
|
|
3
|
+
import { validateApiKeysForProduction } from './keys.js';
|
|
4
|
+
import { logAuthFailure, } from '../../../services/auth-audit.js';
|
|
5
|
+
import { tryAuth as tryPat } from './strategies/pat.js';
|
|
6
|
+
import { tryAuth as trySession } from './strategies/session.js';
|
|
7
|
+
import { tryAuth as tryLegacy, precomputeHashedEntries, } from './strategies/legacy.js';
|
|
8
|
+
import { shouldTouchLastUsed } from '../../../services/pat-touch-debounce.js';
|
|
9
|
+
/**
|
|
10
|
+
* Throws if `preHandler` has not run yet (or if `skipAuth` was set). Use in
|
|
11
|
+
* route handlers / tool helpers that need a non-null `request.user`. The
|
|
12
|
+
* type narrowing is the entire point — once `requireUser` returns, the
|
|
13
|
+
* caller has an `AuthenticatedUser` without further null checks.
|
|
14
|
+
*
|
|
15
|
+
* CR-01 (Phase 30 review) — belt-and-suspenders: check for BOTH `null`
|
|
16
|
+
* (the initialized default via `decorateRequest('user', null)`) AND
|
|
17
|
+
* `undefined` (the value the slot holds when the route was registered
|
|
18
|
+
* OUTSIDE any scope that ran the auth-chain plugin — e.g. a top-level
|
|
19
|
+
* device-flow route mounted as a sibling of the `/api/v1` scope). The
|
|
20
|
+
* scope-wiring fix in server.ts addresses the production wiring, but
|
|
21
|
+
* leaving the guard narrow would silently re-open the bug if a future
|
|
22
|
+
* refactor moved a sessionOnly route outside the chain again.
|
|
23
|
+
*/
|
|
24
|
+
export function requireUser(request) {
|
|
25
|
+
if (request.user === null || request.user === undefined) {
|
|
26
|
+
throw new Error('requireUser: request.user is null/undefined — auth preHandler did ' +
|
|
27
|
+
'not run, or the route is registered with config.skipAuth=true, ' +
|
|
28
|
+
'or the route is outside any scope that registered the auth chain.');
|
|
29
|
+
}
|
|
30
|
+
return request.user;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Apply a successful strategy outcome to the request: populate principal
|
|
34
|
+
* slots, then re-child the request logger so every subsequent
|
|
35
|
+
* `request.log.*()` call carries audit fields.
|
|
36
|
+
*
|
|
37
|
+
* `apiKeyLabel` is passed through into the bindings even on non-legacy paths
|
|
38
|
+
* (where it stays `undefined`). This matches the codebase precedent at
|
|
39
|
+
* the pre-split auth.ts:215-216 and the RESEARCH §8 pattern.
|
|
40
|
+
*/
|
|
41
|
+
function applyPrincipal(request, result, apiKeyLabel) {
|
|
42
|
+
request.user = result.user;
|
|
43
|
+
request.authMethod = result.authMethod;
|
|
44
|
+
request.tokenId = result.tokenId;
|
|
45
|
+
if (apiKeyLabel !== undefined) {
|
|
46
|
+
request.apiKeyLabel = apiKeyLabel;
|
|
47
|
+
}
|
|
48
|
+
request.log = request.log.child({
|
|
49
|
+
user_id: result.user.id,
|
|
50
|
+
token_id: result.tokenId,
|
|
51
|
+
auth_method: result.authMethod,
|
|
52
|
+
apiKeyLabel: request.apiKeyLabel,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Post-auth gate: if the matched route declares `config.sessionOnly: true`
|
|
57
|
+
* AND the active auth method is NOT 'session', return 403. This is the only
|
|
58
|
+
* place "PATs cannot mint PATs" is enforced — Plan 5 registers the
|
|
59
|
+
* `/me/tokens` routes with the flag set.
|
|
60
|
+
*
|
|
61
|
+
* Returns `true` when the gate fired and the reply has been sent; the caller
|
|
62
|
+
* MUST stop processing.
|
|
63
|
+
*/
|
|
64
|
+
function enforceSessionOnly(request, reply) {
|
|
65
|
+
if (request.routeOptions.config.sessionOnly === true &&
|
|
66
|
+
request.authMethod !== 'session') {
|
|
67
|
+
reply.code(403).send({
|
|
68
|
+
error: 'session_required',
|
|
69
|
+
message: 'This endpoint cannot be called with a Personal Access Token.',
|
|
70
|
+
});
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Schedule the best-effort `last_used_at` update for a freshly-matched PAT.
|
|
77
|
+
*
|
|
78
|
+
* Plan 28-06 gates the write through the in-process debounce module
|
|
79
|
+
* (`pat-touch-debounce.ts`) so each token id triggers at most one SQL UPDATE
|
|
80
|
+
* per 10-minute window — satisfying REQUIREMENTS.md PAT-03's
|
|
81
|
+
* "≤ 1 write / 10 min / token" cap. No-op for non-PAT matches
|
|
82
|
+
* (tokenId === null) or when the gate returns `false` (recent write within
|
|
83
|
+
* window).
|
|
84
|
+
*
|
|
85
|
+
* better-sqlite3 is synchronous, so `touchLastUsed` returns `void` — wrap in
|
|
86
|
+
* try/catch (no `.catch(...)` on a void return). Errors are warn-logged but
|
|
87
|
+
* NEVER bubble up.
|
|
88
|
+
*/
|
|
89
|
+
function scheduleLastUsedTouch(fastify, tokenId, log) {
|
|
90
|
+
if (tokenId === null)
|
|
91
|
+
return;
|
|
92
|
+
if (!shouldTouchLastUsed(tokenId))
|
|
93
|
+
return;
|
|
94
|
+
setImmediate(() => {
|
|
95
|
+
try {
|
|
96
|
+
fastify.apiTokenRepository.touchLastUsed(tokenId);
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
log.warn({ err, tokenId }, 'touchLastUsed failed');
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Send a uniform 401 response after emitting the categorical audit log.
|
|
105
|
+
*
|
|
106
|
+
* Returns `void` so callers `return sendUnauthorized(...)` cleanly from the
|
|
107
|
+
* preHandler hook. The body is intentionally minimal — Threat T-28-04-02
|
|
108
|
+
* keeps `reasonCode` off the wire so callers can't probe distinct failure
|
|
109
|
+
* modes.
|
|
110
|
+
*/
|
|
111
|
+
function sendUnauthorized(request, reply, strategy, reasonCode) {
|
|
112
|
+
logAuthFailure(request.log, {
|
|
113
|
+
strategy,
|
|
114
|
+
reasonCode,
|
|
115
|
+
requestId: request.id,
|
|
116
|
+
peerIp: request.ip,
|
|
117
|
+
});
|
|
118
|
+
reply.code(401).send({
|
|
119
|
+
error: 'UNAUTHORIZED',
|
|
120
|
+
message: 'Authentication required',
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Strategy chain threw (DB locked, connection lost, prepared-statement
|
|
125
|
+
* compile error from a runtime migration, etc.). Surface as a categorical
|
|
126
|
+
* 500 — explicitly NOT a 401, because pretending auth failed would
|
|
127
|
+
* mis-route operators and make a degraded DB indistinguishable from a
|
|
128
|
+
* brute-force probe. Two log lines:
|
|
129
|
+
*
|
|
130
|
+
* 1. `auth.error` (request.log.error) — carries the underlying `err`
|
|
131
|
+
* object for postmortem.
|
|
132
|
+
* 2. `auth.failure` warn line via logAuthFailure — keeps the audit feed
|
|
133
|
+
* aware that an authentication attempt did NOT succeed during the
|
|
134
|
+
* outage window. Reuses the Phase 27 AuthFailureReason enum
|
|
135
|
+
* ('unknown_token') so we don't widen the enum just to carry an
|
|
136
|
+
* operational signal; the `auth.error` line above is where the
|
|
137
|
+
* diagnostics live.
|
|
138
|
+
*
|
|
139
|
+
* Response body is `{ error: 'INTERNAL_ERROR' }` — no `reasonCode`, no
|
|
140
|
+
* stack, no token-shaped data. Threat T-28-04-02 still applies.
|
|
141
|
+
*
|
|
142
|
+
* WR-01 (Phase 28 review).
|
|
143
|
+
*/
|
|
144
|
+
function sendInternalError(request, reply, err) {
|
|
145
|
+
request.log.error({ err, requestId: request.id }, 'auth.error');
|
|
146
|
+
logAuthFailure(request.log, {
|
|
147
|
+
strategy: 'legacy',
|
|
148
|
+
reasonCode: 'unknown_token',
|
|
149
|
+
requestId: request.id,
|
|
150
|
+
peerIp: request.ip,
|
|
151
|
+
});
|
|
152
|
+
reply.code(500).send({ error: 'INTERNAL_ERROR' });
|
|
153
|
+
}
|
|
154
|
+
const authChainImpl = async (fastify) => {
|
|
155
|
+
// Parse and (in production) validate API_KEYS at register time so a
|
|
156
|
+
// misconfigured prod boot fails fast. Same fail-fast semantics as the
|
|
157
|
+
// pre-split plugin — `validateApiKeysForProduction` throws synchronously
|
|
158
|
+
// and Fastify bubbles the error up to createServer which closes the
|
|
159
|
+
// server and disposes the App (server.ts:345-363 catch).
|
|
160
|
+
const entries = parseApiKeyEntries(process.env.API_KEYS);
|
|
161
|
+
const keys = entries.map((e) => e.key);
|
|
162
|
+
if (process.env.NODE_ENV === 'production') {
|
|
163
|
+
validateApiKeysForProduction(keys);
|
|
164
|
+
}
|
|
165
|
+
else if (entries.length === 0) {
|
|
166
|
+
fastify.log.warn('No API keys configured in API_KEYS env var. All API requests will be rejected.');
|
|
167
|
+
}
|
|
168
|
+
const hashedEntries = precomputeHashedEntries(entries);
|
|
169
|
+
// Decorators MUST land before any route registers in this scope. The fp()
|
|
170
|
+
// wrap below lifts these into the parent scope so sibling /api/v1/* routes
|
|
171
|
+
// see populated slots after a successful preHandler run.
|
|
172
|
+
fastify.decorateRequest('user', null);
|
|
173
|
+
fastify.decorateRequest('authMethod', null);
|
|
174
|
+
fastify.decorateRequest('tokenId', null);
|
|
175
|
+
// MIGR-01 compat: `apiKeyLabel` decoration retained so existing
|
|
176
|
+
// routes/tests (events.ts SSE fingerprinting, auth-logging.test.ts) keep
|
|
177
|
+
// working. Default `undefined` matches the pre-split contract.
|
|
178
|
+
fastify.decorateRequest('apiKeyLabel', undefined);
|
|
179
|
+
const patDeps = {
|
|
180
|
+
apiTokenRepository: fastify.apiTokenRepository,
|
|
181
|
+
userRepository: fastify.userRepository,
|
|
182
|
+
};
|
|
183
|
+
fastify.addHook('preHandler', async (request, reply) => {
|
|
184
|
+
// 0. Route-level skipAuth opt-out (defined but unused in Phase 28 —
|
|
185
|
+
// Plan 4 ships the flag for future Phase 29 /auth/login etc.).
|
|
186
|
+
if (request.routeOptions.config.skipAuth === true) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
// WR-01 (Phase 28 review) — wrap the entire chain walk so that a
|
|
190
|
+
// throwing strategy (e.g. `apiTokenRepository.findByHash` raising
|
|
191
|
+
// because the DB is locked) becomes a categorical 500 with an
|
|
192
|
+
// `auth.error` log AND an `auth.failure` audit line, rather than
|
|
193
|
+
// a generic Fastify 500 that the audit aggregator never sees. We
|
|
194
|
+
// deliberately do NOT downgrade to 401 — a degraded DB should not
|
|
195
|
+
// look like a brute-force probe.
|
|
196
|
+
try {
|
|
197
|
+
// 1. PAT
|
|
198
|
+
const patOutcome = await tryPat(request, patDeps);
|
|
199
|
+
if (patOutcome.kind === 'fail') {
|
|
200
|
+
return sendUnauthorized(request, reply, 'pat', patOutcome.reasonCode);
|
|
201
|
+
}
|
|
202
|
+
if (patOutcome.kind === 'match') {
|
|
203
|
+
applyPrincipal(request, patOutcome.result);
|
|
204
|
+
scheduleLastUsedTouch(fastify, patOutcome.result.tokenId, request.log);
|
|
205
|
+
if (enforceSessionOnly(request, reply))
|
|
206
|
+
return;
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
// 2. Session (Phase 29 — real implementation reads
|
|
210
|
+
// request.session.get('user') and re-validates against
|
|
211
|
+
// userRepository.findById; returns 'skip' when no session backend
|
|
212
|
+
// is registered (OIDC-disabled mode) so the legacy strategy still
|
|
213
|
+
// gets a chance.
|
|
214
|
+
const sessionOutcome = await trySession(request, {
|
|
215
|
+
userRepository: fastify.userRepository,
|
|
216
|
+
});
|
|
217
|
+
if (sessionOutcome.kind === 'fail') {
|
|
218
|
+
// Defensive — Phase 28 stub never returns fail. Keep the branch so
|
|
219
|
+
// Phase 29's swap doesn't need to add it.
|
|
220
|
+
return sendUnauthorized(request, reply, 'session', sessionOutcome.reasonCode);
|
|
221
|
+
}
|
|
222
|
+
if (sessionOutcome.kind === 'match') {
|
|
223
|
+
applyPrincipal(request, sessionOutcome.result);
|
|
224
|
+
if (enforceSessionOnly(request, reply))
|
|
225
|
+
return;
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
// 3. Legacy API_KEYS
|
|
229
|
+
const legacyOutcome = await tryLegacy(request, {
|
|
230
|
+
userRepository: fastify.userRepository,
|
|
231
|
+
hashedEntries,
|
|
232
|
+
});
|
|
233
|
+
if (legacyOutcome.kind === 'fail') {
|
|
234
|
+
return sendUnauthorized(request, reply, 'legacy', legacyOutcome.reasonCode);
|
|
235
|
+
}
|
|
236
|
+
if (legacyOutcome.kind === 'match') {
|
|
237
|
+
applyPrincipal(request, legacyOutcome.result, legacyOutcome.label);
|
|
238
|
+
// Plan 31-05 (MIGR-02): emit one warn log per legacy-authed request
|
|
239
|
+
// so operators can grep their log feed for sunset-readiness reporting
|
|
240
|
+
// (`event: 'legacy_auth_used'`). The onSend hook below stamps the
|
|
241
|
+
// RFC 8594 Deprecation/Sunset headers; this line is the canonical
|
|
242
|
+
// audit signal — the headers are advisory to the client, the log
|
|
243
|
+
// is the operator-side source of truth.
|
|
244
|
+
request.log.warn({
|
|
245
|
+
event: 'legacy_auth_used',
|
|
246
|
+
userId: legacyOutcome.result.user.id,
|
|
247
|
+
apiKeyLabel: legacyOutcome.label,
|
|
248
|
+
requestId: request.id,
|
|
249
|
+
requestUrl: request.url,
|
|
250
|
+
sunset: config.LEGACY_AUTH_SUNSET_DATE,
|
|
251
|
+
}, 'legacy_auth_used');
|
|
252
|
+
if (enforceSessionOnly(request, reply))
|
|
253
|
+
return;
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
// 4. Catch-all — no strategy saw a credential. Per Plan-04 Decision
|
|
257
|
+
// Q6, the audit log records `strategy: 'legacy', reasonCode:
|
|
258
|
+
// 'missing_credential'` so the failure mode matches the pre-split
|
|
259
|
+
// plugin's "missing X-API-Key" branch.
|
|
260
|
+
return sendUnauthorized(request, reply, 'legacy', 'missing_credential');
|
|
261
|
+
}
|
|
262
|
+
catch (err) {
|
|
263
|
+
return sendInternalError(request, reply, err);
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
// Plan 31-05 (MIGR-02): RFC 8594 Deprecation + Sunset response headers
|
|
267
|
+
// for every legacy-X-API-Key-authed request. Gated strictly on
|
|
268
|
+
// `request.authMethod === 'legacy'` so PAT, session, anonymous (skipAuth),
|
|
269
|
+
// and failed-auth responses NEVER carry the headers (Pitfall 4 in
|
|
270
|
+
// 31-RESEARCH §Common Pitfalls).
|
|
271
|
+
//
|
|
272
|
+
// Callback-style (4-arg) signature is used INTENTIONALLY rather than async
|
|
273
|
+
// — registering an async onSend hook inside this fp()-wrapped plugin
|
|
274
|
+
// delays `reply.sent` from becoming true synchronously when the preHandler
|
|
275
|
+
// calls `reply.send()` (e.g. from `enforceSessionOnly`'s 403). The
|
|
276
|
+
// me-tokens session-only tests then see the route handler run after the
|
|
277
|
+
// 403 reply was queued. The synchronous callback form keeps reply.send()
|
|
278
|
+
// synchronous, preserving the Phase 28 sessionOnly invariant.
|
|
279
|
+
fastify.addHook('onSend', (request, reply, payload, done) => {
|
|
280
|
+
if (request.authMethod === 'legacy') {
|
|
281
|
+
reply.header('Deprecation', 'true');
|
|
282
|
+
reply.header('Sunset', config.LEGACY_AUTH_SUNSET_DATE);
|
|
283
|
+
}
|
|
284
|
+
done(null, payload);
|
|
285
|
+
});
|
|
286
|
+
};
|
|
287
|
+
/**
|
|
288
|
+
* Wrap with fastify-plugin to escape the encapsulated scope. Without `fp()`
|
|
289
|
+
* the preHandler hook only fires for routes registered INSIDE this plugin —
|
|
290
|
+
* every sibling under `/api/v1/*` would bypass auth entirely. The
|
|
291
|
+
* `{ name: 'wft-auth', fastify: '5.x' }` options match the pre-split
|
|
292
|
+
* plugin's identity so any downstream `fastify.hasPlugin('wft-auth')`
|
|
293
|
+
* checks keep working.
|
|
294
|
+
*/
|
|
295
|
+
const authChain = fp(authChainImpl, {
|
|
296
|
+
name: 'wft-auth',
|
|
297
|
+
fastify: '5.x',
|
|
298
|
+
});
|
|
299
|
+
export default authChain;
|
|
300
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/api/plugins/auth/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SHA-256 hash of a key, returned as a 32-byte Buffer.
|
|
3
|
+
*
|
|
4
|
+
* Hashing both sides of the comparison guarantees `timingSafeEqual` receives
|
|
5
|
+
* equal-length buffers, eliminating the length-leak that arises from comparing
|
|
6
|
+
* raw keys of different lengths.
|
|
7
|
+
*/
|
|
8
|
+
export declare function hashKey(key: string): Buffer;
|
|
9
|
+
/**
|
|
10
|
+
* Validate API keys for a production environment.
|
|
11
|
+
*
|
|
12
|
+
* Throws an Error listing every failure mode. The error message references
|
|
13
|
+
* keys by their 1-based index — it never includes the key value itself, so
|
|
14
|
+
* the error is safe to log.
|
|
15
|
+
*
|
|
16
|
+
* Rules:
|
|
17
|
+
* - At least one key must be present.
|
|
18
|
+
* - Every key must be at least 32 characters.
|
|
19
|
+
* - No key may contain a known placeholder substring (case-insensitive).
|
|
20
|
+
* - No key may equal a known placeholder value (lowercased).
|
|
21
|
+
* - No key may be a single character repeated (zero entropy).
|
|
22
|
+
*/
|
|
23
|
+
export declare function validateApiKeysForProduction(keys: string[]): void;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API key helpers — hashing + production validation.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from `src/api/plugins/auth.ts` during the Phase 28 (Plan 28-04)
|
|
5
|
+
* chain split. The runtime behaviour of `hashKey` and
|
|
6
|
+
* `validateApiKeysForProduction` is byte-identical to the pre-split
|
|
7
|
+
* implementation; only the file location changed so the new chain plugin at
|
|
8
|
+
* `src/api/plugins/auth/index.ts` and the legacy strategy at
|
|
9
|
+
* `src/api/plugins/auth/strategies/legacy.ts` can import them without a
|
|
10
|
+
* circular reference back through the shim at `src/api/plugins/auth.ts`.
|
|
11
|
+
*
|
|
12
|
+
* The legacy shim re-exports both functions verbatim, so existing
|
|
13
|
+
* `import { hashKey } from '../plugins/auth.js'` callers (e.g. routes/events.ts,
|
|
14
|
+
* sse-caps.test.ts, the legacy strategy module) keep working unchanged.
|
|
15
|
+
*/
|
|
16
|
+
import { createHash } from 'crypto';
|
|
17
|
+
/**
|
|
18
|
+
* Placeholder substrings rejected in production keys (case-insensitive contains check).
|
|
19
|
+
*
|
|
20
|
+
* Substring matches catch keys that embed an obvious placeholder phrase even if
|
|
21
|
+
* padded to satisfy the length floor (e.g. "change-me-to-a-real-keyxxxxxxxxxx").
|
|
22
|
+
*/
|
|
23
|
+
const PLACEHOLDER_SUBSTRINGS = [
|
|
24
|
+
'change-me-to-a-real-key',
|
|
25
|
+
'changeme',
|
|
26
|
+
'placeholder',
|
|
27
|
+
'example',
|
|
28
|
+
];
|
|
29
|
+
/**
|
|
30
|
+
* Placeholder values rejected in production keys (exact lowercase match).
|
|
31
|
+
*
|
|
32
|
+
* Exact matches catch the literal short placeholders the audit named without
|
|
33
|
+
* false-positive on legitimate keys that happen to include the chars "test"
|
|
34
|
+
* or "dev" elsewhere.
|
|
35
|
+
*/
|
|
36
|
+
const PLACEHOLDER_EXACT = new Set(['test', 'dev', 'placeholder']);
|
|
37
|
+
/**
|
|
38
|
+
* Minimum length required for each API key when NODE_ENV=production.
|
|
39
|
+
* 32 characters gives ~190 bits of entropy if generated from a hex/base64 RNG.
|
|
40
|
+
*/
|
|
41
|
+
const MIN_PRODUCTION_KEY_LENGTH = 32;
|
|
42
|
+
/**
|
|
43
|
+
* SHA-256 hash of a key, returned as a 32-byte Buffer.
|
|
44
|
+
*
|
|
45
|
+
* Hashing both sides of the comparison guarantees `timingSafeEqual` receives
|
|
46
|
+
* equal-length buffers, eliminating the length-leak that arises from comparing
|
|
47
|
+
* raw keys of different lengths.
|
|
48
|
+
*/
|
|
49
|
+
export function hashKey(key) {
|
|
50
|
+
return createHash('sha256').update(key, 'utf8').digest();
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Validate API keys for a production environment.
|
|
54
|
+
*
|
|
55
|
+
* Throws an Error listing every failure mode. The error message references
|
|
56
|
+
* keys by their 1-based index — it never includes the key value itself, so
|
|
57
|
+
* the error is safe to log.
|
|
58
|
+
*
|
|
59
|
+
* Rules:
|
|
60
|
+
* - At least one key must be present.
|
|
61
|
+
* - Every key must be at least 32 characters.
|
|
62
|
+
* - No key may contain a known placeholder substring (case-insensitive).
|
|
63
|
+
* - No key may equal a known placeholder value (lowercased).
|
|
64
|
+
* - No key may be a single character repeated (zero entropy).
|
|
65
|
+
*/
|
|
66
|
+
export function validateApiKeysForProduction(keys) {
|
|
67
|
+
const errors = [];
|
|
68
|
+
if (keys.length === 0) {
|
|
69
|
+
errors.push('API_KEYS must contain at least one key (got empty list)');
|
|
70
|
+
}
|
|
71
|
+
for (let i = 0; i < keys.length; i++) {
|
|
72
|
+
const k = keys[i];
|
|
73
|
+
const idx = i + 1;
|
|
74
|
+
if (k.length === 0) {
|
|
75
|
+
errors.push(`key #${idx}: empty value`);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (k.length < MIN_PRODUCTION_KEY_LENGTH) {
|
|
79
|
+
errors.push(`key #${idx}: must be at least ${MIN_PRODUCTION_KEY_LENGTH} characters (got ${k.length})`);
|
|
80
|
+
}
|
|
81
|
+
const lower = k.toLowerCase();
|
|
82
|
+
for (const phrase of PLACEHOLDER_SUBSTRINGS) {
|
|
83
|
+
if (lower.includes(phrase)) {
|
|
84
|
+
errors.push(`key #${idx}: contains placeholder phrase "${phrase}"`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (PLACEHOLDER_EXACT.has(lower)) {
|
|
88
|
+
errors.push(`key #${idx}: matches known placeholder value`);
|
|
89
|
+
}
|
|
90
|
+
if (new Set(k).size === 1) {
|
|
91
|
+
errors.push(`key #${idx}: single character repeated (no entropy)`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (errors.length > 0) {
|
|
95
|
+
throw new Error(`API_KEYS validation failed for production:\n - ${errors.join('\n - ')}\n` +
|
|
96
|
+
`Set API_KEYS to comma-separated keys of at least ${MIN_PRODUCTION_KEY_LENGTH} ` +
|
|
97
|
+
`characters each, with sufficient entropy and no placeholder phrases.`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=keys.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keys.js","sourceRoot":"","sources":["../../../../src/api/plugins/auth/keys.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AAEpC;;;;;GAKG;AACH,MAAM,sBAAsB,GAAG;IAC7B,yBAAyB;IACzB,UAAU;IACV,aAAa;IACb,SAAS;CACV,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,iBAAiB,GAAG,IAAI,GAAG,CAAC,CAAC,MAAM,EAAE,KAAK,EAAE,aAAa,CAAC,CAAC,CAAC;AAElE;;;GAGG;AACH,MAAM,yBAAyB,GAAG,EAAE,CAAC;AAErC;;;;;;GAMG;AACH,MAAM,UAAU,OAAO,CAAC,GAAW;IACjC,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC,MAAM,EAAE,CAAC;AAC3D,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,4BAA4B,CAAC,IAAc;IACzD,MAAM,MAAM,GAAa,EAAE,CAAC;IAE5B,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtB,MAAM,CAAC,IAAI,CAAC,yDAAyD,CAAC,CAAC;IACzE,CAAC;IAED,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACrC,MAAM,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,MAAM,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC;QAElB,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACnB,MAAM,CAAC,IAAI,CAAC,QAAQ,GAAG,eAAe,CAAC,CAAC;YACxC,SAAS;QACX,CAAC;QACD,IAAI,CAAC,CAAC,MAAM,GAAG,yBAAyB,EAAE,CAAC;YACzC,MAAM,CAAC,IAAI,CACT,QAAQ,GAAG,sBAAsB,yBAAyB,oBAAoB,CAAC,CAAC,MAAM,GAAG,CAC1F,CAAC;QACJ,CAAC;QACD,MAAM,KAAK,GAAG,CAAC,CAAC,WAAW,EAAE,CAAC;QAC9B,KAAK,MAAM,MAAM,IAAI,sBAAsB,EAAE,CAAC;YAC5C,IAAI,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;gBAC3B,MAAM,CAAC,IAAI,CAAC,QAAQ,GAAG,kCAAkC,MAAM,GAAG,CAAC,CAAC;YACtE,CAAC;QACH,CAAC;QACD,IAAI,iBAAiB,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;YACjC,MAAM,CAAC,IAAI,CAAC,QAAQ,GAAG,mCAAmC,CAAC,CAAC;QAC9D,CAAC;QACD,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;YAC1B,MAAM,CAAC,IAAI,CAAC,QAAQ,GAAG,0CAA0C,CAAC,CAAC;QACrE,CAAC;IACH,CAAC;IAED,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACtB,MAAM,IAAI,KAAK,CACb,mDAAmD,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI;YAC1E,oDAAoD,yBAAyB,GAAG;YAChF,sEAAsE,CACzE,CAAC;IACJ,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { FastifyRequest } from 'fastify';
|
|
2
|
+
import type { UserRepository } from '../../../../repositories/user.repository.js';
|
|
3
|
+
import type { ApiKeyEntry } from '../../../../config/env.js';
|
|
4
|
+
import type { StrategyOutcome } from './types.js';
|
|
5
|
+
export interface LegacyDeps {
|
|
6
|
+
userRepository: UserRepository;
|
|
7
|
+
/**
|
|
8
|
+
* Pre-computed SHA-256 hashes of every configured API_KEYS entry. The
|
|
9
|
+
* chain plugin computes this once at register time via
|
|
10
|
+
* `precomputeHashedEntries` and passes it in so the strategy never
|
|
11
|
+
* re-hashes per request.
|
|
12
|
+
*/
|
|
13
|
+
hashedEntries: Array<{
|
|
14
|
+
hash: Buffer;
|
|
15
|
+
label: string;
|
|
16
|
+
}>;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Pre-compute the SHA-256 hash + label for each parsed API_KEYS entry.
|
|
20
|
+
*
|
|
21
|
+
* Called once by the chain plugin at register time. The result feeds into
|
|
22
|
+
* `LegacyDeps.hashedEntries` for every subsequent request, avoiding
|
|
23
|
+
* per-request rehash of the configured key list.
|
|
24
|
+
*/
|
|
25
|
+
export declare function precomputeHashedEntries(entries: ApiKeyEntry[]): Array<{
|
|
26
|
+
hash: Buffer;
|
|
27
|
+
label: string;
|
|
28
|
+
}>;
|
|
29
|
+
/**
|
|
30
|
+
* Inspect the request for a legacy `x-api-key` header and return the
|
|
31
|
+
* outcome.
|
|
32
|
+
*
|
|
33
|
+
* Decision tree:
|
|
34
|
+
* 1. No / empty `x-api-key` header → skip
|
|
35
|
+
* (chain catch-all wraps this as `missing_credential` in the audit
|
|
36
|
+
* log if no other strategy matched.)
|
|
37
|
+
* 2. Supplied hash compares equal to a configured one → look up legacy
|
|
38
|
+
* user via `findLegacyByDisplayName(label)`:
|
|
39
|
+
* - user row missing OR user.disabled_at set → fail/user_disabled
|
|
40
|
+
* - otherwise → match
|
|
41
|
+
* 3. Supplied hash equals NONE of the configured hashes → fail/unknown_token
|
|
42
|
+
*
|
|
43
|
+
* The strategy emits NO log lines — the chain owns audit logging via
|
|
44
|
+
* Phase 27's `logAuthFailure` helper.
|
|
45
|
+
*/
|
|
46
|
+
export declare function tryAuth(request: FastifyRequest, deps: LegacyDeps): Promise<StrategyOutcome>;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Legacy API_KEYS auth strategy (MIGR-01 break-glass).
|
|
2
|
+
//
|
|
3
|
+
// Behaviour ported nearly byte-for-byte from `src/api/plugins/auth.ts:170-216`
|
|
4
|
+
// (the existing inline preHandler that this strategy will replace once the
|
|
5
|
+
// chain plugin lands in Plan 28-04). The boundary changes are deliberate:
|
|
6
|
+
//
|
|
7
|
+
// - Returns a categorical `StrategyOutcome` instead of mutating
|
|
8
|
+
// `request.apiKeyLabel` / `request.log` and sending a 401. The chain
|
|
9
|
+
// plugin (Plan 28-04) applies the outcome.
|
|
10
|
+
// - On a successful hash match, performs the additional
|
|
11
|
+
// `userRepository.findLegacyByDisplayName(label)` lookup so the
|
|
12
|
+
// chain can populate `request.user` from a real `users` row (the
|
|
13
|
+
// legacy identity seeded by Phase 27's identity-seeder service).
|
|
14
|
+
//
|
|
15
|
+
// The constant-time comparison loop is preserved verbatim — every
|
|
16
|
+
// configured hash is compared against the supplied hash regardless of
|
|
17
|
+
// where (or whether) a match occurs. This is the timing-attack defence
|
|
18
|
+
// originally documented at `src/api/plugins/auth.ts:198` and re-asserted
|
|
19
|
+
// by the unit test `does NOT short-circuit on first match`.
|
|
20
|
+
import { timingSafeEqual } from 'node:crypto';
|
|
21
|
+
import { hashKey } from '../keys.js';
|
|
22
|
+
import { toAuthenticatedUser } from './pat.js';
|
|
23
|
+
/**
|
|
24
|
+
* Pre-compute the SHA-256 hash + label for each parsed API_KEYS entry.
|
|
25
|
+
*
|
|
26
|
+
* Called once by the chain plugin at register time. The result feeds into
|
|
27
|
+
* `LegacyDeps.hashedEntries` for every subsequent request, avoiding
|
|
28
|
+
* per-request rehash of the configured key list.
|
|
29
|
+
*/
|
|
30
|
+
export function precomputeHashedEntries(entries) {
|
|
31
|
+
return entries.map((e) => ({ hash: hashKey(e.key), label: e.label }));
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Inspect the request for a legacy `x-api-key` header and return the
|
|
35
|
+
* outcome.
|
|
36
|
+
*
|
|
37
|
+
* Decision tree:
|
|
38
|
+
* 1. No / empty `x-api-key` header → skip
|
|
39
|
+
* (chain catch-all wraps this as `missing_credential` in the audit
|
|
40
|
+
* log if no other strategy matched.)
|
|
41
|
+
* 2. Supplied hash compares equal to a configured one → look up legacy
|
|
42
|
+
* user via `findLegacyByDisplayName(label)`:
|
|
43
|
+
* - user row missing OR user.disabled_at set → fail/user_disabled
|
|
44
|
+
* - otherwise → match
|
|
45
|
+
* 3. Supplied hash equals NONE of the configured hashes → fail/unknown_token
|
|
46
|
+
*
|
|
47
|
+
* The strategy emits NO log lines — the chain owns audit logging via
|
|
48
|
+
* Phase 27's `logAuthFailure` helper.
|
|
49
|
+
*/
|
|
50
|
+
export async function tryAuth(request, deps) {
|
|
51
|
+
const supplied = request.headers['x-api-key'];
|
|
52
|
+
if (typeof supplied !== 'string' || supplied.length === 0) {
|
|
53
|
+
return { kind: 'skip' };
|
|
54
|
+
}
|
|
55
|
+
const suppliedHash = hashKey(supplied);
|
|
56
|
+
// Constant-time compare against EVERY configured hash. Do NOT break on
|
|
57
|
+
// first match — keeping the comparison count fixed prevents leaking the
|
|
58
|
+
// match position (or the existence of any match at all) via wall-clock
|
|
59
|
+
// timing. Preserved verbatim from src/api/plugins/auth.ts:194-200.
|
|
60
|
+
let matchedLabel;
|
|
61
|
+
for (const entry of deps.hashedEntries) {
|
|
62
|
+
if (timingSafeEqual(entry.hash, suppliedHash)) {
|
|
63
|
+
matchedLabel = entry.label;
|
|
64
|
+
// Do NOT break — keeps total comparison count fixed.
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (matchedLabel === undefined) {
|
|
68
|
+
return { kind: 'fail', reasonCode: 'unknown_token' };
|
|
69
|
+
}
|
|
70
|
+
// Match — resolve the legacy `users` row Phase 27 seeded so the chain
|
|
71
|
+
// can populate `request.user` with a real principal. `is_legacy=1` is
|
|
72
|
+
// already enforced inside the repository query.
|
|
73
|
+
const user = deps.userRepository.findLegacyByDisplayName(matchedLabel);
|
|
74
|
+
if (user === null || user.disabled_at !== null) {
|
|
75
|
+
return { kind: 'fail', reasonCode: 'user_disabled' };
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
kind: 'match',
|
|
79
|
+
result: {
|
|
80
|
+
user: toAuthenticatedUser(user),
|
|
81
|
+
authMethod: 'legacy',
|
|
82
|
+
tokenId: null,
|
|
83
|
+
},
|
|
84
|
+
label: matchedLabel,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=legacy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"legacy.js","sourceRoot":"","sources":["../../../../../src/api/plugins/auth/strategies/legacy.ts"],"names":[],"mappings":"AAAA,uDAAuD;AACvD,EAAE;AACF,+EAA+E;AAC/E,2EAA2E;AAC3E,0EAA0E;AAC1E,EAAE;AACF,kEAAkE;AAClE,yEAAyE;AACzE,+CAA+C;AAC/C,0DAA0D;AAC1D,oEAAoE;AACpE,qEAAqE;AACrE,qEAAqE;AACrE,EAAE;AACF,kEAAkE;AAClE,sEAAsE;AACtE,uEAAuE;AACvE,yEAAyE;AACzE,4DAA4D;AAC5D,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAG9C,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAErC,OAAO,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAC;AAc/C;;;;;;GAMG;AACH,MAAM,UAAU,uBAAuB,CACrC,OAAsB;IAEtB,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;AACxE,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,KAAK,UAAU,OAAO,CAC3B,OAAuB,EACvB,IAAgB;IAEhB,MAAM,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;IAC9C,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1D,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;IAC1B,CAAC;IAED,MAAM,YAAY,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAEvC,uEAAuE;IACvE,wEAAwE;IACxE,uEAAuE;IACvE,mEAAmE;IACnE,IAAI,YAAgC,CAAC;IACrC,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,aAAa,EAAE,CAAC;QACvC,IAAI,eAAe,CAAC,KAAK,CAAC,IAAI,EAAE,YAAY,CAAC,EAAE,CAAC;YAC9C,YAAY,GAAG,KAAK,CAAC,KAAK,CAAC;YAC3B,qDAAqD;QACvD,CAAC;IACH,CAAC;IAED,IAAI,YAAY,KAAK,SAAS,EAAE,CAAC;QAC/B,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,eAAe,EAAE,CAAC;IACvD,CAAC;IAED,sEAAsE;IACtE,sEAAsE;IACtE,gDAAgD;IAChD,MAAM,IAAI,GAAG,IAAI,CAAC,cAAc,CAAC,uBAAuB,CAAC,YAAY,CAAC,CAAC;IACvE,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,CAAC,WAAW,KAAK,IAAI,EAAE,CAAC;QAC/C,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,eAAe,EAAE,CAAC;IACvD,CAAC;IAED,OAAO;QACL,IAAI,EAAE,OAAO;QACb,MAAM,EAAE;YACN,IAAI,EAAE,mBAAmB,CAAC,IAAI,CAAC;YAC/B,UAAU,EAAE,QAAQ;YACpB,OAAO,EAAE,IAAI;SACd;QACD,KAAK,EAAE,YAAY;KACpB,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { FastifyRequest } from 'fastify';
|
|
2
|
+
import type { ApiTokenRepository } from '../../../../repositories/api-token.repository.js';
|
|
3
|
+
import type { UserRepository } from '../../../../repositories/user.repository.js';
|
|
4
|
+
import type { AuthenticatedUser, User } from '../../../../types/identity.js';
|
|
5
|
+
import type { StrategyOutcome } from './types.js';
|
|
6
|
+
export interface PatDeps {
|
|
7
|
+
apiTokenRepository: ApiTokenRepository;
|
|
8
|
+
userRepository: UserRepository;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Map a snake_case `users` row to the camelCase boundary projection that
|
|
12
|
+
* the auth chain populates on `request.user`.
|
|
13
|
+
*
|
|
14
|
+
* SQLite returns booleans as INTEGER (0|1); the projection normalises to
|
|
15
|
+
* the boolean type the downstream consumers expect. Exported so the legacy
|
|
16
|
+
* strategy can share the conversion in Task 3.
|
|
17
|
+
*/
|
|
18
|
+
export declare function toAuthenticatedUser(user: User): AuthenticatedUser;
|
|
19
|
+
/**
|
|
20
|
+
* Inspect the request for a PAT credential and return the outcome.
|
|
21
|
+
*
|
|
22
|
+
* Decision tree:
|
|
23
|
+
* 1. No Authorization header → skip
|
|
24
|
+
* 2. Authorization doesn't start with `Bearer ` → skip
|
|
25
|
+
* 3. Bearer body doesn't start with `wft_pat_` → skip (legacy may try)
|
|
26
|
+
* 4. PAT body shape wrong (length / charset) → fail/wrong_prefix
|
|
27
|
+
* 5. findByHash returns null → fail/unknown_token
|
|
28
|
+
* 6. row.revoked_at is set → fail/revoked
|
|
29
|
+
* 7. row.expires_at is past wall-clock now → fail/expired
|
|
30
|
+
* 8. user row missing OR user.disabled_at set → fail/user_disabled
|
|
31
|
+
* 9. all checks pass → match
|
|
32
|
+
*
|
|
33
|
+
* No side effects: no log writes, no last_used_at update — the chain (Plan
|
|
34
|
+
* 4) does both centrally after a strategy returns. Keeps this function
|
|
35
|
+
* pure and unit-testable without a Fastify instance.
|
|
36
|
+
*/
|
|
37
|
+
export declare function tryAuth(request: FastifyRequest, deps: PatDeps): Promise<StrategyOutcome>;
|