Finishes the half-implemented MFA enforcement work and ships the SLO
monitoring rules at the same time.
MFA grace period (auth):
- New `mfa-policy.ts` central source of truth: `MFA_REQUIRED_ROLES = [ADMIN]`,
`MFA_GRACE_PERIOD_DAYS = 14`, `MFA_REAUTH_WINDOW_MINUTES = 15`.
- New columns `User.mfaGraceStartedAt` + `User.mfaLastVerifiedAt`
(migration `20260429000000_add_mfa_grace_columns`).
- `JwtPayload.mfa: 'none' | 'grace' | 'enrollment_required'` claim now
carried in every access token so the FE + admin guards can react.
- `LoginUserHandler.resolveMfaGraceClaim()`:
* If role requires MFA and user has not enrolled, lazy-stamp
`mfaGraceStartedAt` on first login (returns `mfa: 'grace'`,
`remainingDays: 14`).
* After window expires → `mfa: 'enrollment_required'`, `remainingDays: 0`
(callers must force enrolment on sensitive routes).
* Otherwise → `mfa: 'none'`.
- `LocalStrategy` now passes `totpEnabled` + `mfaGraceStartedAt` through
to the command so the handler can branch without an extra query.
- `IUserRepository` + `PrismaUserRepository` get
`updateMfaGraceStartedAt` / `updateMfaLastVerifiedAt`.
- `UserEntity` carries the two new fields end-to-end (props, getters,
`createNew` + `createPasswordless` factories). Fixed an orphan-property
syntax bug in `createPasswordless` that was breaking typecheck.
- `oauth.service.ts` `UserEntity` construction now includes `deletedAt`
+ the two MFA fields (was missing required props).
- Add missing `jsonwebtoken` + `@types/jsonwebtoken` to `apps/api`
(transitively pulled in via `jwt-rotation.ts` from commit 3705193 but
never declared, so `tsc --noEmit` was failing).
- Update `login-user.handler.spec.ts` + `local.strategy.spec.ts` to cover
grace-window + enrolment-required branches. 338/338 auth tests pass.
Ops monitoring:
- New `monitoring/prometheus/slo-rules.yml` with recording + alerting
rules for the agreed SLOs.
- Wire it into `prometheus.yml` + alertmanager routing.
- Capture the SLO soak-test results in
`docs/audits/slo-soak-test-log.md`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
98 lines
3.0 KiB
JSON
98 lines
3.0 KiB
JSON
{
|
|
"name": "@goodgo/api",
|
|
"version": "0.0.1",
|
|
"private": true,
|
|
"scripts": {
|
|
"dev": "nest start --watch",
|
|
"build": "nest build",
|
|
"start": "node dist/main",
|
|
"start:prod": "node dist/main",
|
|
"lint": "eslint src/",
|
|
"test": "vitest run",
|
|
"test:integration": "vitest run --config vitest.integration.config.ts",
|
|
"typecheck": "tsc --noEmit"
|
|
},
|
|
"dependencies": {
|
|
"@anthropic-ai/sdk": "^0.89.0",
|
|
"@aws-sdk/client-s3": "^3.1026.0",
|
|
"@aws-sdk/s3-request-presigner": "^3.1026.0",
|
|
"@bull-board/api": "^7.0.0",
|
|
"@bull-board/express": "^7.0.0",
|
|
"@bull-board/nestjs": "^7.0.0",
|
|
"@goodgo/mcp-servers": "workspace:*",
|
|
"@nest-lab/throttler-storage-redis": "^1.2.0",
|
|
"@nestjs/bullmq": "^11.0.4",
|
|
"@nestjs/common": "^11.0.0",
|
|
"@nestjs/config": "^4.0.4",
|
|
"@nestjs/core": "^11.0.0",
|
|
"@nestjs/cqrs": "^11.0.0",
|
|
"@nestjs/event-emitter": "^3.0.0",
|
|
"@nestjs/jwt": "^11.0.2",
|
|
"@nestjs/passport": "^11.0.5",
|
|
"@nestjs/platform-express": "^11.0.0",
|
|
"@nestjs/platform-socket.io": "^11.1.19",
|
|
"@nestjs/schedule": "^6.1.1",
|
|
"@nestjs/swagger": "^11.2.7",
|
|
"@nestjs/terminus": "^11.1.1",
|
|
"@nestjs/throttler": "^6.5.0",
|
|
"@nestjs/websockets": "^11.1.19",
|
|
"@paralleldrive/cuid2": "^3.3.0",
|
|
"@prisma/adapter-pg": "^7.7.0",
|
|
"@prisma/client": "^7.7.0",
|
|
"@sentry/nestjs": "^10.47.0",
|
|
"@sentry/profiling-node": "^10.47.0",
|
|
"@socket.io/redis-adapter": "^8.3.0",
|
|
"@willsoto/nestjs-prometheus": "^6.1.0",
|
|
"bcrypt": "^6.0.0",
|
|
"bullmq": "^5.74.1",
|
|
"class-transformer": "^0.5.1",
|
|
"class-validator": "^0.15.1",
|
|
"cookie-parser": "^1.4.7",
|
|
"firebase-admin": "^13.7.0",
|
|
"handlebars": "^4.7.9",
|
|
"helmet": "^8.1.0",
|
|
"ioredis": "^5.4.0",
|
|
"jsonwebtoken": "^9.0.3",
|
|
"nodemailer": "^8.0.5",
|
|
"otplib": "^13.4.0",
|
|
"passport": "^0.7.0",
|
|
"passport-google-oauth20": "^2.0.0",
|
|
"passport-jwt": "^4.0.1",
|
|
"passport-local": "^1.0.0",
|
|
"pg": "^8.20.0",
|
|
"pino": "^10.3.1",
|
|
"pino-pretty": "^13.0.0",
|
|
"prom-client": "^15.1.3",
|
|
"puppeteer": "^24.41.0",
|
|
"qrcode": "^1.5.4",
|
|
"reflect-metadata": "^0.2.0",
|
|
"rxjs": "^7.8.0",
|
|
"sanitize-html": "^2.17.2",
|
|
"socket.io": "^4.8.3",
|
|
"swagger-ui-express": "^5.0.1",
|
|
"typesense": "^3.0.5"
|
|
},
|
|
"devDependencies": {
|
|
"@nestjs/cli": "^11.0.0",
|
|
"@nestjs/schematics": "^11.0.0",
|
|
"@nestjs/testing": "^11.0.0",
|
|
"@types/bcrypt": "^6.0.0",
|
|
"@types/cookie-parser": "^1.4.10",
|
|
"@types/express": "^5.0.0",
|
|
"@types/jsonwebtoken": "^9.0.10",
|
|
"@types/node": "^25.5.2",
|
|
"@types/nodemailer": "^8.0.0",
|
|
"@types/passport-google-oauth20": "^2.0.17",
|
|
"@types/passport-jwt": "^4.0.1",
|
|
"@types/passport-local": "^1.0.38",
|
|
"@types/pg": "^8.20.0",
|
|
"@types/qrcode": "^1.5.6",
|
|
"@types/sanitize-html": "^2.16.1",
|
|
"@types/supertest": "^7.2.0",
|
|
"prisma": "^7.7.0",
|
|
"supertest": "^7.2.2",
|
|
"typescript": "^6.0.2",
|
|
"vitest": "^4.1.3"
|
|
}
|
|
}
|