create-rigel 0.1.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/LICENSE +21 -0
- package/README.md +156 -0
- package/cli.js +122 -0
- package/package.json +47 -0
- package/templates/express/.claude/CLAUDE.md +114 -0
- package/templates/express/.claude/agents/arch-validator.md +85 -0
- package/templates/express/.claude/agents/db-optimizer.md +72 -0
- package/templates/express/.claude/agents/doc-gardener.md +67 -0
- package/templates/express/.claude/agents/garbage-collector.md +94 -0
- package/templates/express/.claude/agents/gate-checker.md +176 -0
- package/templates/express/.claude/agents/reviewer.md +84 -0
- package/templates/express/.claude/agents/security-auditor.md +82 -0
- package/templates/express/.claude/hooks/post-write.sh +93 -0
- package/templates/express/.claude/rules/api.md +149 -0
- package/templates/express/.claude/rules/architecture.md +78 -0
- package/templates/express/.claude/rules/database.md +104 -0
- package/templates/express/.claude/rules/jobs.md +81 -0
- package/templates/express/.claude/rules/security.md +92 -0
- package/templates/express/.claude/rules/testing.md +97 -0
- package/templates/express/.claude/settings.json +35 -0
- package/templates/express/.claude/skills/00-infra-setup/SKILL.md +479 -0
- package/templates/express/.claude/skills/01-write-roadmap/SKILL.md +72 -0
- package/templates/express/.claude/skills/02-write-spec/SKILL.md +119 -0
- package/templates/express/.claude/skills/03-write-plan/SKILL.md +124 -0
- package/templates/express/.claude/skills/04-build-layer/SKILL.md +216 -0
- package/templates/express/.claude/skills/05-validate-layer/SKILL.md +43 -0
- package/templates/express/.claude/skills/06-push-layer/SKILL.md +59 -0
- package/templates/express/.claude/skills/07-layer-check/SKILL.md +62 -0
- package/templates/express/.claude/skills/08-garbage-collect/SKILL.md +86 -0
- package/templates/express/.claude/skills/09-doc-garden/SKILL.md +64 -0
- package/templates/express/.claude/skills/10-db-optimize/SKILL.md +96 -0
- package/templates/express/.claude/skills/11-load-test/SKILL.md +77 -0
- package/templates/express/.dockerignore +43 -0
- package/templates/express/.env.example +42 -0
- package/templates/express/.github/CODEOWNERS +22 -0
- package/templates/express/.github/dependabot.yml +48 -0
- package/templates/express/.github/pull_request_template.md +58 -0
- package/templates/express/.github/workflows/ci.yml +278 -0
- package/templates/express/.github/workflows/load-test.yml +41 -0
- package/templates/express/.gitleaks.toml +18 -0
- package/templates/express/.lintstagedrc.json +3 -0
- package/templates/express/.nvmrc +1 -0
- package/templates/express/.prettierrc +8 -0
- package/templates/express/.sequelizerc +8 -0
- package/templates/express/AGENTS.md +94 -0
- package/templates/express/ARCHITECTURE.md +161 -0
- package/templates/express/Dockerfile +40 -0
- package/templates/express/Makefile +79 -0
- package/templates/express/QUICKSTART.md +131 -0
- package/templates/express/docker-compose.yml +114 -0
- package/templates/express/docs/PLANS.md +111 -0
- package/templates/express/docs/QUALITY_SCORE.md +66 -0
- package/templates/express/docs/design-docs/core-beliefs.md +89 -0
- package/templates/express/docs/design-docs/decisions/ADR-000-infrastructure.md +70 -0
- package/templates/express/docs/design-docs/decisions/ADR-001-observability.md +82 -0
- package/templates/express/docs/design-docs/decisions/index.md +23 -0
- package/templates/express/docs/design-docs/index.md +11 -0
- package/templates/express/docs/design-docs/observability.md +194 -0
- package/templates/express/docs/design-docs/team-workflow.md +64 -0
- package/templates/express/docs/exec-plans/active/.gitkeep +0 -0
- package/templates/express/docs/exec-plans/completed/.gitkeep +0 -0
- package/templates/express/docs/exec-plans/tech-debt-tracker.md +31 -0
- package/templates/express/docs/generated/.gitkeep +0 -0
- package/templates/express/docs/product-specs/ROADMAP.md +49 -0
- package/templates/express/docs/product-specs/draft/.gitkeep +0 -0
- package/templates/express/docs/product-specs/index.md +21 -0
- package/templates/express/docs/product-specs/ready/.gitkeep +0 -0
- package/templates/express/eslint.config.mjs +94 -0
- package/templates/express/gitignore +36 -0
- package/templates/express/infra/monitoring/alloy/config.alloy +59 -0
- package/templates/express/jest.config.ts +61 -0
- package/templates/express/package.json +45 -0
- package/templates/express/scripts/openapi.export.ts +77 -0
- package/templates/express/tests/architecture/isolation.test.ts +51 -0
- package/templates/express/tests/architecture/layers.test.ts +83 -0
- package/templates/express/tests/integration/health.test.ts +40 -0
- package/templates/express/tests/integration/isolation.test.template.ts +88 -0
- package/templates/express/tests/load/smoke.js +46 -0
- package/templates/express/tests/load/soak.js +45 -0
- package/templates/express/tests/load/stress.js +44 -0
- package/templates/express/tests/unit/utils/response.util.test.ts +53 -0
- package/templates/express/tsconfig.build.json +8 -0
- package/templates/express/tsconfig.json +20 -0
- package/templates/fastapi/.claude/CLAUDE.md +122 -0
- package/templates/fastapi/.claude/agents/arch-validator.md +64 -0
- package/templates/fastapi/.claude/agents/db-optimizer.md +59 -0
- package/templates/fastapi/.claude/agents/doc-gardener.md +55 -0
- package/templates/fastapi/.claude/agents/garbage-collector.md +74 -0
- package/templates/fastapi/.claude/agents/gate-checker.md +133 -0
- package/templates/fastapi/.claude/agents/reviewer.md +74 -0
- package/templates/fastapi/.claude/agents/security-auditor.md +69 -0
- package/templates/fastapi/.claude/hooks/post-write.sh +65 -0
- package/templates/fastapi/.claude/rules/api.md +119 -0
- package/templates/fastapi/.claude/rules/architecture.md +84 -0
- package/templates/fastapi/.claude/rules/database.md +153 -0
- package/templates/fastapi/.claude/rules/jobs.md +85 -0
- package/templates/fastapi/.claude/rules/security.md +102 -0
- package/templates/fastapi/.claude/rules/testing.md +207 -0
- package/templates/fastapi/.claude/settings.json +50 -0
- package/templates/fastapi/.claude/skills/00-infra-setup/SKILL.md +311 -0
- package/templates/fastapi/.claude/skills/01-write-roadmap/SKILL.md +61 -0
- package/templates/fastapi/.claude/skills/02-write-spec/SKILL.md +82 -0
- package/templates/fastapi/.claude/skills/03-write-plan/SKILL.md +83 -0
- package/templates/fastapi/.claude/skills/04-build-layer/SKILL.md +170 -0
- package/templates/fastapi/.claude/skills/05-validate-layer/SKILL.md +20 -0
- package/templates/fastapi/.claude/skills/06-push-layer/SKILL.md +31 -0
- package/templates/fastapi/.claude/skills/07-layer-check/SKILL.md +51 -0
- package/templates/fastapi/.claude/skills/08-garbage-collect/SKILL.md +47 -0
- package/templates/fastapi/.claude/skills/09-doc-garden/SKILL.md +9 -0
- package/templates/fastapi/.claude/skills/10-db-optimize/SKILL.md +53 -0
- package/templates/fastapi/.claude/skills/11-load-test/SKILL.md +81 -0
- package/templates/fastapi/.dockerignore +30 -0
- package/templates/fastapi/.github/dependabot.yml +24 -0
- package/templates/fastapi/AGENTS.md +87 -0
- package/templates/fastapi/ARCHITECTURE.md +141 -0
- package/templates/fastapi/Makefile +42 -0
- package/templates/fastapi/README.md +179 -0
- package/templates/fastapi/docs/PLANS.md +26 -0
- package/templates/fastapi/docs/QUALITY_SCORE.md +26 -0
- package/templates/fastapi/docs/design-docs/core-beliefs.md +46 -0
- package/templates/fastapi/docs/design-docs/decisions/ADR-000-infrastructure.md +79 -0
- package/templates/fastapi/docs/design-docs/decisions/index.md +7 -0
- package/templates/fastapi/docs/design-docs/index.md +9 -0
- package/templates/fastapi/docs/exec-plans/active/.gitkeep +0 -0
- package/templates/fastapi/docs/exec-plans/completed/.gitkeep +0 -0
- package/templates/fastapi/docs/exec-plans/tech-debt-tracker.md +19 -0
- package/templates/fastapi/docs/generated/.gitkeep +0 -0
- package/templates/fastapi/docs/product-specs/ROADMAP.md +49 -0
- package/templates/fastapi/docs/product-specs/draft/.gitkeep +0 -0
- package/templates/fastapi/docs/product-specs/index.md +18 -0
- package/templates/fastapi/docs/product-specs/ready/.gitkeep +0 -0
- package/templates/fastapi/gitignore +46 -0
- package/templates/fastapi/scripts/gate.sh +68 -0
- package/templates/nestjs/.claude/CLAUDE.md +121 -0
- package/templates/nestjs/.claude/agents/db-optimizer.md +51 -0
- package/templates/nestjs/.claude/agents/doc-gardener.md +45 -0
- package/templates/nestjs/.claude/agents/garbage-collector.md +50 -0
- package/templates/nestjs/.claude/agents/gate-checker.md +127 -0
- package/templates/nestjs/.claude/agents/reviewer.md +68 -0
- package/templates/nestjs/.claude/agents/security-auditor.md +72 -0
- package/templates/nestjs/.claude/hooks/post-write.sh +91 -0
- package/templates/nestjs/.claude/rules/api.md +87 -0
- package/templates/nestjs/.claude/rules/architecture.md +104 -0
- package/templates/nestjs/.claude/rules/database.md +111 -0
- package/templates/nestjs/.claude/rules/security.md +110 -0
- package/templates/nestjs/.claude/rules/testing.md +145 -0
- package/templates/nestjs/.claude/settings.json +33 -0
- package/templates/nestjs/.claude/skills/00-infra-setup/SKILL.md +222 -0
- package/templates/nestjs/.claude/skills/01-write-spec/SKILL.md +25 -0
- package/templates/nestjs/.claude/skills/02-write-plan/SKILL.md +18 -0
- package/templates/nestjs/.claude/skills/03-build-layer/SKILL.md +242 -0
- package/templates/nestjs/.claude/skills/04-validate-layer/SKILL.md +4 -0
- package/templates/nestjs/.claude/skills/05-push-layer/SKILL.md +6 -0
- package/templates/nestjs/.claude/skills/06-layer-check/SKILL.md +12 -0
- package/templates/nestjs/.claude/skills/07-garbage-collect/SKILL.md +2 -0
- package/templates/nestjs/.claude/skills/08-doc-garden/SKILL.md +4 -0
- package/templates/nestjs/.claude/skills/09-api-sync/SKILL.md +36 -0
- package/templates/nestjs/.claude/skills/10-load-test/SKILL.md +29 -0
- package/templates/nestjs/.env.example +11 -0
- package/templates/nestjs/AGENTS.md +88 -0
- package/templates/nestjs/ARCHITECTURE.md +192 -0
- package/templates/nestjs/docs/PLANS.md +22 -0
- package/templates/nestjs/docs/QUALITY_SCORE.md +27 -0
- package/templates/nestjs/docs/design-docs/core-beliefs.md +18 -0
- package/templates/nestjs/docs/design-docs/decisions/ADR-000-infrastructure.md +58 -0
- package/templates/nestjs/docs/design-docs/decisions/index.md +7 -0
- package/templates/nestjs/docs/design-docs/index.md +7 -0
- package/templates/nestjs/docs/exec-plans/active/.gitkeep +0 -0
- package/templates/nestjs/docs/exec-plans/completed/.gitkeep +0 -0
- package/templates/nestjs/docs/exec-plans/tech-debt-tracker.md +16 -0
- package/templates/nestjs/docs/generated/.gitkeep +0 -0
- package/templates/nestjs/docs/product-specs/draft/.gitkeep +0 -0
- package/templates/nestjs/docs/product-specs/index.md +7 -0
- package/templates/nestjs/docs/product-specs/ready/.gitkeep +0 -0
- package/templates/nestjs/gitignore +8 -0
- package/templates/nextjs/.claude/CLAUDE.md +139 -0
- package/templates/nextjs/.claude/agents/arch-validator.md +66 -0
- package/templates/nextjs/.claude/agents/contract-checker.md +78 -0
- package/templates/nextjs/.claude/agents/doc-gardener.md +52 -0
- package/templates/nextjs/.claude/agents/garbage-collector.md +61 -0
- package/templates/nextjs/.claude/agents/gate-checker.md +129 -0
- package/templates/nextjs/.claude/agents/perf-auditor.md +109 -0
- package/templates/nextjs/.claude/agents/reviewer.md +79 -0
- package/templates/nextjs/.claude/agents/security-auditor.md +50 -0
- package/templates/nextjs/.claude/hooks/post-write.sh +121 -0
- package/templates/nextjs/.claude/rules/api-contract.md +166 -0
- package/templates/nextjs/.claude/rules/architecture.md +99 -0
- package/templates/nextjs/.claude/rules/components.md +122 -0
- package/templates/nextjs/.claude/rules/security.md +122 -0
- package/templates/nextjs/.claude/rules/testing.md +187 -0
- package/templates/nextjs/.claude/scripts/infra-setup.sh +550 -0
- package/templates/nextjs/.claude/settings.json +33 -0
- package/templates/nextjs/.claude/skills/00-infra-setup/SKILL.md +471 -0
- package/templates/nextjs/.claude/skills/01-write-roadmap/SKILL.md +77 -0
- package/templates/nextjs/.claude/skills/02-write-spec/SKILL.md +78 -0
- package/templates/nextjs/.claude/skills/03-write-plan/SKILL.md +91 -0
- package/templates/nextjs/.claude/skills/04-build-layer/SKILL.md +141 -0
- package/templates/nextjs/.claude/skills/05-validate-layer/SKILL.md +13 -0
- package/templates/nextjs/.claude/skills/06-push-layer/SKILL.md +20 -0
- package/templates/nextjs/.claude/skills/07-layer-check/SKILL.md +28 -0
- package/templates/nextjs/.claude/skills/08-garbage-collect/SKILL.md +14 -0
- package/templates/nextjs/.claude/skills/09-doc-garden/SKILL.md +12 -0
- package/templates/nextjs/.claude/skills/10-api-sync/SKILL.md +50 -0
- package/templates/nextjs/.claude/skills/11-perf-budget/SKILL.md +51 -0
- package/templates/nextjs/.dockerignore +25 -0
- package/templates/nextjs/.gitattributes +10 -0
- package/templates/nextjs/.github/CODEOWNERS +24 -0
- package/templates/nextjs/.github/dependabot.yml +31 -0
- package/templates/nextjs/.github/pull_request_template.md +59 -0
- package/templates/nextjs/.github/workflows/ci.yml +143 -0
- package/templates/nextjs/.github/workflows/lighthouse.yml +54 -0
- package/templates/nextjs/.github/workflows/load-test.yml +54 -0
- package/templates/nextjs/.husky/pre-commit +1 -0
- package/templates/nextjs/.lintstagedrc.json +4 -0
- package/templates/nextjs/.mcp.json +8 -0
- package/templates/nextjs/.prettierignore +13 -0
- package/templates/nextjs/.prettierrc +9 -0
- package/templates/nextjs/AGENTS.md +88 -0
- package/templates/nextjs/ARCHITECTURE.md +165 -0
- package/templates/nextjs/Dockerfile +38 -0
- package/templates/nextjs/Makefile +50 -0
- package/templates/nextjs/QUICKSTART.md +128 -0
- package/templates/nextjs/docs/PLANS.md +23 -0
- package/templates/nextjs/docs/QUALITY_SCORE.md +27 -0
- package/templates/nextjs/docs/design-docs/core-beliefs.md +43 -0
- package/templates/nextjs/docs/design-docs/decisions/ADR-000-infrastructure.md +72 -0
- package/templates/nextjs/docs/design-docs/decisions/index.md +7 -0
- package/templates/nextjs/docs/design-docs/index.md +7 -0
- package/templates/nextjs/docs/design-docs/team-workflow.md +66 -0
- package/templates/nextjs/docs/exec-plans/active/.gitkeep +0 -0
- package/templates/nextjs/docs/exec-plans/completed/.gitkeep +0 -0
- package/templates/nextjs/docs/exec-plans/tech-debt-tracker.md +19 -0
- package/templates/nextjs/docs/generated/.gitkeep +0 -0
- package/templates/nextjs/docs/product-specs/ROADMAP.md +59 -0
- package/templates/nextjs/docs/product-specs/draft/.gitkeep +0 -0
- package/templates/nextjs/docs/product-specs/index.md +18 -0
- package/templates/nextjs/docs/product-specs/ready/.gitkeep +0 -0
- package/templates/nextjs/eslint.config.mjs +150 -0
- package/templates/nextjs/gitignore +35 -0
- package/templates/nextjs/lighthouserc.js +37 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: security-auditor
|
|
3
|
+
description: OWASP Top 10 security review for NestJS. Run before any PR touching auth, guards, or sensitive data.
|
|
4
|
+
model: claude-sonnet-4-5
|
|
5
|
+
tools: [Read, Bash]
|
|
6
|
+
color: red
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
You are a senior application security engineer reviewing NestJS code against OWASP Top 10.
|
|
10
|
+
|
|
11
|
+
## Pre-Review
|
|
12
|
+
```bash
|
|
13
|
+
git diff main --name-only
|
|
14
|
+
```
|
|
15
|
+
Read each changed file.
|
|
16
|
+
|
|
17
|
+
## Checklist
|
|
18
|
+
|
|
19
|
+
### A01 — Broken Access Control
|
|
20
|
+
- [ ] Global `JwtAuthGuard` is registered in `app.module.ts` (APP_GUARD)
|
|
21
|
+
- [ ] All public routes have `@Public()` decorator
|
|
22
|
+
- [ ] Every repository query has `userId` in WHERE clause
|
|
23
|
+
- [ ] Admin routes have `@Roles('ADMIN')` + `RolesGuard`
|
|
24
|
+
- [ ] No horizontal escalation (user A can't access user B's data)
|
|
25
|
+
|
|
26
|
+
### A02 — Cryptographic Failures
|
|
27
|
+
- [ ] Passwords: `argon2.hash()` — not bcrypt, not MD5
|
|
28
|
+
- [ ] JWT secret: `config.getOrThrow('JWT_SECRET')` — min 32 chars
|
|
29
|
+
- [ ] No secrets hardcoded anywhere
|
|
30
|
+
- [ ] HTTPS enforced (helmet HSTS header)
|
|
31
|
+
|
|
32
|
+
### A03 — Injection
|
|
33
|
+
- [ ] All DB via Sequelize ORM or named replacements
|
|
34
|
+
- [ ] No string-interpolated `sequelize.query()`
|
|
35
|
+
- [ ] All inputs through ValidationPipe + class-validator DTOs
|
|
36
|
+
|
|
37
|
+
### A04 — Insecure Design
|
|
38
|
+
- [ ] Auth endpoints rate-limited (10/min via @nestjs/throttler)
|
|
39
|
+
- [ ] UUIDs (not sequential ints) on user-facing IDs
|
|
40
|
+
- [ ] Duplicate detection before creation
|
|
41
|
+
|
|
42
|
+
### A05 — Security Misconfiguration
|
|
43
|
+
- [ ] Helmet middleware applied globally
|
|
44
|
+
- [ ] CORS configured centrally with explicit origins
|
|
45
|
+
- [ ] No stack traces in error responses (AllExceptionsFilter)
|
|
46
|
+
- [ ] Swagger UI disabled in production
|
|
47
|
+
|
|
48
|
+
### A07 — Auth Failures
|
|
49
|
+
- [ ] Access token: max 15 min (ConfigService value)
|
|
50
|
+
- [ ] Refresh token: single-use, revoked after use
|
|
51
|
+
- [ ] Token revocation: Redis `revoked:{jti}` checked in JwtStrategy
|
|
52
|
+
- [ ] Login: same error for bad user vs bad password
|
|
53
|
+
|
|
54
|
+
### A09 — Logging Failures
|
|
55
|
+
- [ ] Auth events logged: login, logout, failed auth
|
|
56
|
+
- [ ] 401/403 logged via LoggingInterceptor
|
|
57
|
+
- [ ] No PII in log values (nestjs-pino redaction)
|
|
58
|
+
|
|
59
|
+
## Verdict
|
|
60
|
+
```
|
|
61
|
+
APPROVED — all checks pass.
|
|
62
|
+
|
|
63
|
+
OR
|
|
64
|
+
|
|
65
|
+
CHANGES REQUIRED:
|
|
66
|
+
CRITICAL (block merge):
|
|
67
|
+
1. [file:line] [problem]
|
|
68
|
+
Fix: [exact code]
|
|
69
|
+
|
|
70
|
+
HIGH (fix before production):
|
|
71
|
+
2. [file:line] [problem]
|
|
72
|
+
```
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Fires after every Write/Edit on TypeScript files.
|
|
3
|
+
# Catches NestJS-specific violations immediately.
|
|
4
|
+
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
FILE=$(cat | python3 -c "
|
|
8
|
+
import sys, json
|
|
9
|
+
try:
|
|
10
|
+
data = json.load(sys.stdin)
|
|
11
|
+
print(data.get('file_path') or data.get('path') or '')
|
|
12
|
+
except:
|
|
13
|
+
print('')
|
|
14
|
+
" 2>/dev/null || echo "")
|
|
15
|
+
|
|
16
|
+
[[ -z "$FILE" ]] && exit 0
|
|
17
|
+
[[ ! -f "$FILE" ]] && exit 0
|
|
18
|
+
[[ ! "$FILE" =~ \.ts$ ]] && exit 0
|
|
19
|
+
|
|
20
|
+
WARNINGS=()
|
|
21
|
+
|
|
22
|
+
# 1. console.log — use Logger from @nestjs/common
|
|
23
|
+
if [[ "$FILE" =~ ^src/ ]]; then
|
|
24
|
+
if grep -qn "console\.\(log\|error\|warn\|info\)" "$FILE" 2>/dev/null; then
|
|
25
|
+
LINE=$(grep -n "console\.\(log\|error\|warn\|info\)" "$FILE" | head -1 | cut -d: -f1)
|
|
26
|
+
WARNINGS+=("⚠ [HOOK] console.log at $FILE:$LINE — use Logger from @nestjs/common or nestjs-pino")
|
|
27
|
+
fi
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
# 2. process.env — use ConfigService
|
|
31
|
+
if [[ "$FILE" =~ ^src/ ]] && [[ ! "$FILE" =~ src/config/ ]]; then
|
|
32
|
+
if grep -qn "process\.env" "$FILE" 2>/dev/null; then
|
|
33
|
+
LINE=$(grep -n "process\.env" "$FILE" | head -1 | cut -d: -f1)
|
|
34
|
+
WARNINGS+=("⚠ [HOOK] process.env at $FILE:$LINE — inject ConfigService instead")
|
|
35
|
+
fi
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
# 3. HttpException in service files — use NestJS semantic exceptions
|
|
39
|
+
if [[ "$FILE" =~ \.service\.ts$ ]]; then
|
|
40
|
+
if grep -qn "throw new HttpException\b" "$FILE" 2>/dev/null; then
|
|
41
|
+
LINE=$(grep -n "throw new HttpException" "$FILE" | head -1 | cut -d: -f1)
|
|
42
|
+
WARNINGS+=("⚠ [HOOK] HttpException in service at $FILE:$LINE — use NotFoundException, ConflictException etc.")
|
|
43
|
+
fi
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
# 4. @InjectModel in service files — inject repository instead
|
|
47
|
+
if [[ "$FILE" =~ \.service\.ts$ ]]; then
|
|
48
|
+
if grep -qn "@InjectModel\b" "$FILE" 2>/dev/null; then
|
|
49
|
+
LINE=$(grep -n "@InjectModel" "$FILE" | head -1 | cut -d: -f1)
|
|
50
|
+
WARNINGS+=("⚠ [HOOK] @InjectModel in service at $FILE:$LINE — inject the repository class, not the model directly")
|
|
51
|
+
fi
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
# 5. Raw .toJSON() without Zod parse in repository files
|
|
55
|
+
if [[ "$FILE" =~ \.repository\.ts$ ]]; then
|
|
56
|
+
if grep -qn "\.toJSON()" "$FILE" 2>/dev/null; then
|
|
57
|
+
# Check if .parse( is on the same or next line
|
|
58
|
+
if ! grep -qn "Schema\.parse\|\.parse(" "$FILE" 2>/dev/null; then
|
|
59
|
+
WARNINGS+=("⚠ [HOOK] .toJSON() without ZodSchema.parse() in $FILE — validate every DB result")
|
|
60
|
+
fi
|
|
61
|
+
fi
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
# 6. Business logic in controller files (if/else, calculations)
|
|
65
|
+
if [[ "$FILE" =~ \.controller\.ts$ ]]; then
|
|
66
|
+
if grep -qnE "^\s+(if|else|switch|for|while|const .* = .* \? )" "$FILE" 2>/dev/null; then
|
|
67
|
+
LINE=$(grep -nE "^\s+(if|else|switch|for|while)" "$FILE" | head -1 | cut -d: -f1)
|
|
68
|
+
WARNINGS+=("⚠ [HOOK] Possible business logic in controller at $FILE:$LINE — controllers should delegate to service only")
|
|
69
|
+
fi
|
|
70
|
+
fi
|
|
71
|
+
|
|
72
|
+
# 7. File size > 400 lines
|
|
73
|
+
LINE_COUNT=$(wc -l < "$FILE" 2>/dev/null || echo 0)
|
|
74
|
+
if [[ $LINE_COUNT -gt 400 ]]; then
|
|
75
|
+
WARNINGS+=("⚠ [HOOK] $FILE has $LINE_COUNT lines — exceeds 400-line limit. Split into focused modules.")
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
# 8. Missing @ApiProperty in DTO files
|
|
79
|
+
if [[ "$FILE" =~ \.dto\.ts$ ]]; then
|
|
80
|
+
PROP_COUNT=$(grep -c "^\s*\(readonly\s\+\)\?[a-zA-Z]" "$FILE" 2>/dev/null || echo 0)
|
|
81
|
+
API_PROP_COUNT=$(grep -c "@ApiProperty" "$FILE" 2>/dev/null || echo 0)
|
|
82
|
+
if [[ $PROP_COUNT -gt 0 ]] && [[ $API_PROP_COUNT -eq 0 ]]; then
|
|
83
|
+
WARNINGS+=("⚠ [HOOK] DTO $FILE has properties without @ApiProperty() — Swagger will be incomplete")
|
|
84
|
+
fi
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
for w in "${WARNINGS[@]}"; do
|
|
88
|
+
echo "$w"
|
|
89
|
+
done
|
|
90
|
+
|
|
91
|
+
exit 0
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "src/**/*.controller.ts"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# API Rules — Auto-injected on controller file edits
|
|
7
|
+
|
|
8
|
+
## Every Controller Checklist
|
|
9
|
+
|
|
10
|
+
### Required Class Decorators
|
|
11
|
+
```typescript
|
|
12
|
+
@ApiTags('Applications') // ← group in Swagger UI
|
|
13
|
+
@ApiBearerAuth() // ← shows auth in Swagger
|
|
14
|
+
@Controller('v1/applications') // ← always under v1/ prefix
|
|
15
|
+
export class ApplicationController {
|
|
16
|
+
constructor(private readonly appService: ApplicationService) {}
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### Required Method Decorators (every route)
|
|
20
|
+
```typescript
|
|
21
|
+
@Get(':id')
|
|
22
|
+
@ApiOperation({ summary: 'Get application by ID' })
|
|
23
|
+
@ApiOkResponse({ type: ApplicationResponseDto })
|
|
24
|
+
@ApiNotFoundResponse({ description: 'Application not found' })
|
|
25
|
+
findOne(
|
|
26
|
+
@Param('id', ParseUUIDPipe) id: string,
|
|
27
|
+
@CurrentUser() user: JwtPayload,
|
|
28
|
+
) {
|
|
29
|
+
return this.appService.findOne(user.sub, id) // ← one-liner
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Rate Limiting Tiers
|
|
34
|
+
```typescript
|
|
35
|
+
// On controller or method level:
|
|
36
|
+
@Throttle({ default: { limit: 10, ttl: 60000 } }) // Auth: 10/min
|
|
37
|
+
@Throttle({ default: { limit: 60, ttl: 60000 } }) // Public: 60/min
|
|
38
|
+
@Throttle({ default: { limit: 300, ttl: 60000 } }) // User: 300/min (default)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Route Versioning (all routes under v1/)
|
|
42
|
+
```typescript
|
|
43
|
+
// ✅ v1 prefix in controller
|
|
44
|
+
@Controller('v1/applications')
|
|
45
|
+
|
|
46
|
+
// OR use versioning module:
|
|
47
|
+
// main.ts: app.enableVersioning({ type: VersioningType.URI })
|
|
48
|
+
// @Controller({ path: 'applications', version: '1' })
|
|
49
|
+
|
|
50
|
+
// ❌ Never unversioned
|
|
51
|
+
@Controller('applications')
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Pagination Response
|
|
55
|
+
```typescript
|
|
56
|
+
@Get()
|
|
57
|
+
@ApiOkResponse({ description: 'Paginated list' })
|
|
58
|
+
list(
|
|
59
|
+
@Query() query: PaginationDto,
|
|
60
|
+
@CurrentUser() user: JwtPayload,
|
|
61
|
+
) {
|
|
62
|
+
return this.appService.list(user.sub, query.cursor, query.limit)
|
|
63
|
+
}
|
|
64
|
+
// Response: { items: [...], nextCursor: string | null, hasMore: boolean }
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Public Routes
|
|
68
|
+
```typescript
|
|
69
|
+
@Public() // ← bypass JWT guard
|
|
70
|
+
@Throttle({ default: { limit: 10, ttl: 60000 } }) // ← strict rate limit on auth
|
|
71
|
+
@Post('register')
|
|
72
|
+
@ApiOperation({ summary: 'Register new account' })
|
|
73
|
+
@ApiCreatedResponse({ type: AuthResultDto })
|
|
74
|
+
register(@Body() dto: RegisterDto) {
|
|
75
|
+
return this.authService.register(dto)
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Admin Routes
|
|
80
|
+
```typescript
|
|
81
|
+
@Roles('ADMIN') // ← requires ADMIN role
|
|
82
|
+
@Delete(':id')
|
|
83
|
+
@ApiOperation({ summary: 'Admin: delete any resource' })
|
|
84
|
+
adminDelete(@Param('id', ParseUUIDPipe) id: string) {
|
|
85
|
+
return this.appService.adminDelete(id)
|
|
86
|
+
}
|
|
87
|
+
```
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "src/**/*.ts"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Architecture Rules — Auto-injected on every src/ file edit
|
|
7
|
+
|
|
8
|
+
## Import Hierarchy (one-way, enforced)
|
|
9
|
+
|
|
10
|
+
| Your file | May import from |
|
|
11
|
+
|---|---|
|
|
12
|
+
| `*.model.ts` | Types, sequelize-typescript decorators only |
|
|
13
|
+
| `*.dto.ts` | class-validator, @nestjs/swagger, Types only |
|
|
14
|
+
| `*.repository.ts` | Model, Config (ConfigService), Types, Zod |
|
|
15
|
+
| `*.service.ts` | Repository, Config, Types — NO HTTP imports |
|
|
16
|
+
| `*.controller.ts` | Service, DTOs, Common decorators, @nestjs/common |
|
|
17
|
+
| `*.module.ts` | All of the above |
|
|
18
|
+
| `src/common/**` | Config, Types only |
|
|
19
|
+
|
|
20
|
+
## Model Rules
|
|
21
|
+
```typescript
|
|
22
|
+
// ✅ Model = schema only
|
|
23
|
+
@Table({ tableName: 'applications', paranoid: true })
|
|
24
|
+
export class Application extends Model {
|
|
25
|
+
@Default(() => newId())
|
|
26
|
+
@Column({ type: DataType.UUID, primaryKey: true })
|
|
27
|
+
id!: string
|
|
28
|
+
|
|
29
|
+
// ❌ Never add methods with business logic to models
|
|
30
|
+
// getActiveStages() { ... } ← WRONG, belongs in service
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Repository Rules
|
|
35
|
+
```typescript
|
|
36
|
+
// ✅ Always validate output with Zod
|
|
37
|
+
async findById(id: string, userId: string): Promise<ApplicationRecord> {
|
|
38
|
+
const raw = await this.appModel.findOne({ where: { id, userId } })
|
|
39
|
+
if (!raw) throw new NotFoundException(`Application ${id} not found`)
|
|
40
|
+
return ApplicationSchema.parse(raw.toJSON()) // REQUIRED
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ❌ Never return raw Sequelize instances
|
|
44
|
+
async findById(id: string): Promise<Application> {
|
|
45
|
+
return this.appModel.findByPk(id) // WRONG — unvalidated, exposes ORM internals
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ✅ Ownership in every query
|
|
49
|
+
where: { id, userId } // userId always from auth context, never from user input
|
|
50
|
+
|
|
51
|
+
// ✅ Cursor pagination on all list methods
|
|
52
|
+
async list(userId: string, cursor?: PageCursor, limit = 20) {
|
|
53
|
+
const where: WhereOptions = { userId }
|
|
54
|
+
if (cursor) {
|
|
55
|
+
where[Op.or] = [
|
|
56
|
+
{ createdAt: { [Op.lt]: cursor.createdAt } },
|
|
57
|
+
{ createdAt: cursor.createdAt, id: { [Op.lt]: cursor.id } },
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
// ...
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Service Rules
|
|
65
|
+
```typescript
|
|
66
|
+
// ✅ Takes typed inputs, returns typed outputs, throws semantic exceptions
|
|
67
|
+
async createApplication(userId: string, dto: CreateApplicationDto): Promise<ApplicationRecord> {
|
|
68
|
+
return this.applicationRepository.create(userId, dto)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ❌ No HTTP types in service
|
|
72
|
+
import { Request } from 'express' // FORBIDDEN
|
|
73
|
+
throw new HttpException('error', 404) // FORBIDDEN — use NotFoundException
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Controller Rules
|
|
77
|
+
```typescript
|
|
78
|
+
// ✅ One-liner handlers only
|
|
79
|
+
@Post()
|
|
80
|
+
@ApiOperation({ summary: 'Create application' })
|
|
81
|
+
@ApiCreatedResponse({ type: ApplicationResponseDto })
|
|
82
|
+
create(@Body() dto: CreateApplicationDto, @CurrentUser() user: JwtPayload) {
|
|
83
|
+
return this.applicationService.createApplication(user.sub, dto)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ✅ Swagger on every route — no exceptions
|
|
87
|
+
// Every controller class: @ApiTags('Applications') @ApiBearerAuth()
|
|
88
|
+
// Every handler: @ApiOperation() + @ApiResponse() or @ApiOkResponse()
|
|
89
|
+
|
|
90
|
+
// ❌ Logic in controllers
|
|
91
|
+
create(@Body() dto: CreateApplicationDto) {
|
|
92
|
+
if (dto.stage === 'APPLIED') { ... } // WRONG — belongs in service
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## ConfigService Pattern
|
|
97
|
+
```typescript
|
|
98
|
+
// ✅ Always inject ConfigService
|
|
99
|
+
constructor(private config: ConfigService) {}
|
|
100
|
+
const jwtSecret = this.config.getOrThrow<string>('JWT_SECRET')
|
|
101
|
+
|
|
102
|
+
// ❌ Never
|
|
103
|
+
process.env.JWT_SECRET
|
|
104
|
+
```
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "src/**/*.repository.ts"
|
|
4
|
+
- "src/**/*.model.ts"
|
|
5
|
+
- "db/migrations/**/*.js"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Database Rules — Auto-injected on repository/model/migration edits
|
|
9
|
+
|
|
10
|
+
## The 5 Checks Before Every Repository Method Ships
|
|
11
|
+
|
|
12
|
+
### 1. Zod Parse on Every Result
|
|
13
|
+
```typescript
|
|
14
|
+
// ✅ REQUIRED
|
|
15
|
+
const raw = await this.userModel.findOne({ where: { email } })
|
|
16
|
+
if (!raw) return null
|
|
17
|
+
return UserSchema.parse(raw.toJSON())
|
|
18
|
+
|
|
19
|
+
// ❌ FORBIDDEN
|
|
20
|
+
return raw // Sequelize instance — unvalidated
|
|
21
|
+
return raw.toJSON() as UserRecord // cast without validation
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### 2. No N+1 — Eager Load Always
|
|
25
|
+
```typescript
|
|
26
|
+
// ✅ Single query with include
|
|
27
|
+
const apps = await this.appModel.findAll({
|
|
28
|
+
where: { userId },
|
|
29
|
+
include: [
|
|
30
|
+
{ model: Note },
|
|
31
|
+
{ model: Contact },
|
|
32
|
+
],
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
// ❌ N+1
|
|
36
|
+
const apps = await this.appModel.findAll({ where: { userId } })
|
|
37
|
+
for (const app of apps) {
|
|
38
|
+
app.notes = await this.noteModel.findAll({ where: { applicationId: app.id } })
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### 3. Cursor Pagination on All List Methods
|
|
43
|
+
```typescript
|
|
44
|
+
// ✅ Cursor-based
|
|
45
|
+
async list(userId: string, cursor?: PageCursor, limit = 20) {
|
|
46
|
+
const where: WhereOptions = { userId }
|
|
47
|
+
if (cursor) {
|
|
48
|
+
where[Op.or as symbol] = [
|
|
49
|
+
{ createdAt: { [Op.lt]: cursor.createdAt } },
|
|
50
|
+
{ createdAt: cursor.createdAt, id: { [Op.lt]: cursor.id } },
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
const rows = await this.appModel.findAll({
|
|
54
|
+
where, include: [...],
|
|
55
|
+
order: [['createdAt', 'DESC'], ['id', 'DESC']],
|
|
56
|
+
limit: limit + 1,
|
|
57
|
+
})
|
|
58
|
+
const hasMore = rows.length > limit
|
|
59
|
+
const items = hasMore ? rows.slice(0, limit) : rows
|
|
60
|
+
return {
|
|
61
|
+
items: items.map(r => ApplicationSchema.parse(r.toJSON())),
|
|
62
|
+
nextCursor: hasMore ? { id: items.at(-1)!.id, createdAt: items.at(-1)!.createdAt } : null,
|
|
63
|
+
hasMore,
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ❌ FORBIDDEN on growing tables
|
|
68
|
+
findAll({ offset: page * limit, limit })
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### 4. Transactions for Multi-Table Writes
|
|
72
|
+
```typescript
|
|
73
|
+
// ✅ Inject Sequelize and use transaction
|
|
74
|
+
constructor(
|
|
75
|
+
@InjectModel(Application) private appModel: typeof Application,
|
|
76
|
+
@InjectConnection() private sequelize: Sequelize,
|
|
77
|
+
) {}
|
|
78
|
+
|
|
79
|
+
async updateStage(id: string, stage: string, note: string) {
|
|
80
|
+
return this.sequelize.transaction(async (t) => {
|
|
81
|
+
await Application.update({ stage }, { where: { id }, transaction: t })
|
|
82
|
+
await Note.create({ applicationId: id, body: note, type: 'SYSTEM' }, { transaction: t })
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### 5. Soft Delete — paranoid: true
|
|
88
|
+
```typescript
|
|
89
|
+
// ✅ @Table decorator
|
|
90
|
+
@Table({ tableName: 'applications', paranoid: true })
|
|
91
|
+
export class Application extends Model {
|
|
92
|
+
@DeletedAt deletedAt?: Date
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// findAll automatically excludes soft-deleted — no manual WHERE needed
|
|
96
|
+
// To include deleted: { paranoid: false }
|
|
97
|
+
// Hard delete: { force: true } — only for GDPR
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Model Checklist
|
|
101
|
+
- [ ] `@Table({ paranoid: true })` — soft delete
|
|
102
|
+
- [ ] `@Default(() => newId())` on id — UUIDv7
|
|
103
|
+
- [ ] `indexes` array in `@Table` — FK columns + ORDER BY columns
|
|
104
|
+
- [ ] Partial index for `WHERE deleted_at IS NULL` if high-volume
|
|
105
|
+
- [ ] `@CreatedAt`, `@UpdatedAt`, `@DeletedAt` decorators
|
|
106
|
+
|
|
107
|
+
## Migration Checklist
|
|
108
|
+
- [ ] Both `up()` and `down()` implemented
|
|
109
|
+
- [ ] New indexes use `CONCURRENTLY` to avoid table locks
|
|
110
|
+
- [ ] FK constraints with explicit `ON DELETE` behaviour
|
|
111
|
+
- [ ] Run `npm run db:migrate` to verify before committing
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "src/**/*.controller.ts"
|
|
4
|
+
- "src/**/*.repository.ts"
|
|
5
|
+
- "src/auth/**/*.ts"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Security Rules — Auto-injected on controller/repository/auth edits
|
|
9
|
+
|
|
10
|
+
## Auth — Global Guard, Opt-Out Model
|
|
11
|
+
|
|
12
|
+
```typescript
|
|
13
|
+
// ✅ Public routes must have @Public()
|
|
14
|
+
@Public()
|
|
15
|
+
@Post('login')
|
|
16
|
+
login(@Body() dto: LoginDto) { ... }
|
|
17
|
+
|
|
18
|
+
// ✅ Protected routes have NOTHING — global guard handles it
|
|
19
|
+
@Get('me')
|
|
20
|
+
getProfile(@CurrentUser() user: JwtPayload) { ... }
|
|
21
|
+
|
|
22
|
+
// ❌ Never add @UseGuards(JwtAuthGuard) manually — it's global
|
|
23
|
+
@UseGuards(JwtAuthGuard) // redundant and misleading
|
|
24
|
+
@Get('me')
|
|
25
|
+
getProfile() { ... }
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Ownership — Enforced in Repository
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
// ✅ userId ALWAYS in WHERE clause
|
|
32
|
+
const raw = await this.appModel.findOne({
|
|
33
|
+
where: { id, userId } // userId from auth context — never from user input
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
// ❌ No ownership check — any user can access any record
|
|
37
|
+
const raw = await this.appModel.findByPk(id)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Input Validation — DTOs + ValidationPipe
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
// ✅ ValidationPipe global (main.ts) + class-validator on DTO
|
|
44
|
+
export class CreateApplicationDto {
|
|
45
|
+
@IsString() @MinLength(1) @MaxLength(200)
|
|
46
|
+
@ApiProperty({ example: 'Acme Corp' })
|
|
47
|
+
company: string
|
|
48
|
+
|
|
49
|
+
@IsOptional() @IsUrl()
|
|
50
|
+
@ApiProperty({ required: false })
|
|
51
|
+
postingUrl?: string
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ❌ Never accept raw body without DTO
|
|
55
|
+
@Post() create(@Body() body: any) { ... } // FORBIDDEN
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## SQL Injection Prevention
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
// ✅ Sequelize ORM parameterises automatically
|
|
62
|
+
await this.appModel.findAll({ where: { userId, stage } })
|
|
63
|
+
|
|
64
|
+
// ✅ Raw queries with named replacements
|
|
65
|
+
await sequelize.query(
|
|
66
|
+
'SELECT * FROM applications WHERE user_id = :userId',
|
|
67
|
+
{ replacements: { userId }, type: QueryTypes.SELECT }
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
// ❌ String interpolation — FORBIDDEN
|
|
71
|
+
await sequelize.query(`SELECT * FROM applications WHERE user_id = '${userId}'`)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Password Hashing — argon2 Only
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
import * as argon2 from 'argon2'
|
|
78
|
+
|
|
79
|
+
// Hash
|
|
80
|
+
const hash = await argon2.hash(password)
|
|
81
|
+
|
|
82
|
+
// Verify
|
|
83
|
+
const valid = await argon2.verify(hash, password)
|
|
84
|
+
|
|
85
|
+
// ❌ Never
|
|
86
|
+
import * as bcrypt from 'bcrypt' // GPU-vulnerable
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Error Responses — Never Expose Internals
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
// ✅ AllExceptionsFilter in common/ catches all — sanitised output
|
|
93
|
+
// { statusCode, message, timestamp, path } — no stack traces
|
|
94
|
+
|
|
95
|
+
// ✅ Throw semantic NestJS exceptions
|
|
96
|
+
throw new NotFoundException('Application not found')
|
|
97
|
+
throw new ConflictException('Stage transition invalid')
|
|
98
|
+
|
|
99
|
+
// ❌ Never expose internal errors
|
|
100
|
+
res.json({ error: err.message, stack: err.stack })
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## JWT Tokens
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
// Access token: 15 minutes — from ConfigService
|
|
107
|
+
// Refresh token: 7 days — single use, rotated on refresh
|
|
108
|
+
// Revocation: Redis key `revoked:{jti}` — checked in JwtStrategy.validate()
|
|
109
|
+
// Tokens in response body — never in cookies (frontend handles storage)
|
|
110
|
+
```
|