tlc-claude-code 2.2.1 → 2.3.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/.claude/agents/builder.md +17 -0
- package/.claude/commands/tlc/audit.md +12 -0
- package/.claude/commands/tlc/build.md +67 -24
- package/.claude/commands/tlc/guard.md +9 -0
- package/.claude/commands/tlc/init.md +12 -1
- package/.claude/commands/tlc/review.md +19 -0
- package/CODING-STANDARDS.md +217 -10
- package/package.json +1 -1
- package/server/lib/careful-patterns.js +142 -0
- package/server/lib/careful-patterns.test.js +164 -0
- package/server/lib/field-report.js +92 -0
- package/server/lib/field-report.test.js +195 -0
- package/server/lib/orchestration/worktree-manager.js +133 -0
- package/server/lib/orchestration/worktree-manager.test.js +198 -0
- package/server/lib/overdrive-command.js +31 -9
- package/server/lib/overdrive-command.test.js +25 -26
- package/server/lib/review-fixer.js +107 -0
- package/server/lib/review-fixer.test.js +152 -0
- package/server/lib/scope-checker.js +127 -0
- package/server/lib/scope-checker.test.js +175 -0
- package/server/lib/skill-validator.js +165 -0
- package/server/lib/skill-validator.test.js +289 -0
- package/server/lib/standards/standards-injector.js +6 -0
- package/server/lib/test-selector.js +127 -0
- package/server/lib/test-selector.test.js +172 -0
- package/server/templates/CLAUDE.md +6 -0
- package/server/templates/CODING-STANDARDS.md +356 -10
|
@@ -122,6 +122,23 @@ src/modules/{entity}/
|
|
|
122
122
|
- No credentials — use environment variables.
|
|
123
123
|
- No magic numbers or strings — use named constants or enums.
|
|
124
124
|
|
|
125
|
+
### Configuration
|
|
126
|
+
- All `process.env` access MUST be in a config module — never in services or controllers.
|
|
127
|
+
- Validate all required env vars at startup with a schema (Zod/Joi).
|
|
128
|
+
- No silent empty-string fallbacks for secrets or connection strings.
|
|
129
|
+
|
|
130
|
+
### Security
|
|
131
|
+
- Every data-access endpoint MUST check resource ownership, not just authentication.
|
|
132
|
+
- Never return secrets (API keys, tokens, passwords) in API responses or HTML.
|
|
133
|
+
- Hash OTPs, reset tokens, and session secrets before storing — never plaintext.
|
|
134
|
+
- Escape all dynamic values in HTML output — no raw interpolation.
|
|
135
|
+
- No inline HTML string builders for new pages — use a proper frontend or templating engine.
|
|
136
|
+
- Tests MUST prove that user A cannot read/modify user B's data.
|
|
137
|
+
|
|
138
|
+
### Dependency Injection
|
|
139
|
+
- Never manually instantiate services/providers with `new` — use DI.
|
|
140
|
+
- Register all providers in the DI container.
|
|
141
|
+
|
|
125
142
|
### File and Folder Limits
|
|
126
143
|
- Files: warn at 500 lines, error at 1000 lines.
|
|
127
144
|
- Folders: warn at 8 files, error at 15 files.
|
|
@@ -52,6 +52,11 @@ Run `auditProject(projectPath)` which executes:
|
|
|
52
52
|
| **Missing Return Types** | Exported functions without explicit return type | warning |
|
|
53
53
|
| **Missing Parameter Types** | Function parameters without type annotations | error |
|
|
54
54
|
| **Weak tsconfig** | `strict: true` not enabled in tsconfig.json | warning |
|
|
55
|
+
| **Direct `process.env`** | `process.env.` usage outside config module files | error |
|
|
56
|
+
| **Unescaped HTML** | Template literals containing HTML tags with `${` interpolation | error |
|
|
57
|
+
| **Secrets in Responses** | Response objects containing fields named `apiKey`, `secret`, `token`, `password` | error |
|
|
58
|
+
| **Manual Instantiation** | `new .*Provider(` or `new .*Service(` in application code | warning |
|
|
59
|
+
| **Missing Ownership Check** | Controller methods with `@Param('id')` but no ownership/authorization guard | warning |
|
|
55
60
|
|
|
56
61
|
### Step 3: Generate Report
|
|
57
62
|
|
|
@@ -109,6 +114,13 @@ Status: FAILED (18 issues found)
|
|
|
109
114
|
JSDoc Coverage: 8 issues (42% of exports undocumented)
|
|
110
115
|
Import Style: PASSED
|
|
111
116
|
|
|
117
|
+
SECURITY & CONFIG
|
|
118
|
+
Direct process.env: 3 issues (outside config module)
|
|
119
|
+
Unescaped HTML: 2 issues
|
|
120
|
+
Secrets in Responses: 1 issue
|
|
121
|
+
Manual Instantiation: 1 issue
|
|
122
|
+
Missing Ownership: 2 issues
|
|
123
|
+
|
|
112
124
|
Report saved to: .planning/AUDIT-REPORT.md
|
|
113
125
|
|
|
114
126
|
Fix automatically? Run /tlc:cleanup
|
|
@@ -344,11 +344,45 @@ Task(resume="AGENT_ID", prompt="Continue from where you left off. Fix any errors
|
|
|
344
344
|
- When user asks "check on agents"
|
|
345
345
|
- After each agent completes
|
|
346
346
|
|
|
347
|
-
**
|
|
348
|
-
|
|
347
|
+
**Before spawning agents (integration branch):**
|
|
348
|
+
|
|
349
|
+
Create an integration branch so all worktrees merge into one place, producing a single clean PR:
|
|
350
|
+
|
|
351
|
+
```javascript
|
|
352
|
+
const { createIntegrationBranch } = require('./server/lib/orchestration/worktree-manager');
|
|
353
|
+
const { branch } = createIntegrationBranch(phaseNumber, { exec, baseBranch: 'main' });
|
|
354
|
+
// branch = 'phase/42'
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
All worktrees branch off `phase/{N}`, not `main`. This means each worktree's diff is small and relative to the integration branch, not the entire main history.
|
|
358
|
+
|
|
359
|
+
**After all agents complete — sequential merge-back:**
|
|
360
|
+
|
|
361
|
+
```javascript
|
|
362
|
+
const { mergeAllWorktrees, listWorktrees } = require('./server/lib/orchestration/worktree-manager');
|
|
363
|
+
|
|
364
|
+
// Filter to only THIS phase's worktrees — don't merge unrelated work
|
|
365
|
+
const allWorktrees = listWorktrees({ exec });
|
|
366
|
+
const worktrees = allWorktrees.filter(wt => wt.name.startsWith(`phase-${phaseNumber}-`));
|
|
367
|
+
const result = mergeAllWorktrees(worktrees, `phase/${phaseNumber}`, { exec });
|
|
368
|
+
|
|
369
|
+
// result.merged = ['task-1', 'task-3', 'task-4'] — successfully merged
|
|
370
|
+
// result.conflicts = ['task-2'] — needs manual resolution
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
`mergeAllWorktrees` does the following automatically:
|
|
374
|
+
1. **Analyzes file overlap** — worktrees touching disjoint files merge first
|
|
375
|
+
2. **Merges one at a time** into `phase/{N}`
|
|
376
|
+
3. **Rebases remaining worktrees** onto the updated `phase/{N}` after each merge
|
|
377
|
+
4. **Skips conflicting worktrees** — preserves them for manual resolution, continues with others
|
|
378
|
+
5. **Cleans up** merged worktrees (removes worktree dir + branch)
|
|
379
|
+
|
|
380
|
+
After merge-back:
|
|
381
|
+
1. Run full test suite on the integration branch
|
|
349
382
|
2. Verify all tasks pass
|
|
350
|
-
3.
|
|
351
|
-
4.
|
|
383
|
+
3. If conflicts exist, report them with file lists
|
|
384
|
+
4. If clean, the integration branch is ready for a single PR to main
|
|
385
|
+
5. Continue to Step 8 (verification)
|
|
352
386
|
|
|
353
387
|
### Step 1c: Sync and Claim (Multi-User, Sequential Only)
|
|
354
388
|
|
|
@@ -712,11 +746,17 @@ git diff --name-status main...HEAD
|
|
|
712
746
|
1. **Test Coverage** - Every implementation file has a test file
|
|
713
747
|
2. **TDD Compliance** - Commits show test-first pattern (score ≥ 50%)
|
|
714
748
|
3. **Security Scan** - No hardcoded secrets, eval(), innerHTML, etc.
|
|
715
|
-
4. **
|
|
716
|
-
5. **
|
|
717
|
-
6. **
|
|
718
|
-
7. **
|
|
719
|
-
8. **
|
|
749
|
+
4. **Authorization** - Every data-access endpoint has ownership checks, not just auth guards
|
|
750
|
+
5. **Secrets Exposure** - No API keys, tokens, or passwords returned in responses/HTML
|
|
751
|
+
6. **Config Hygiene** - No `process.env` outside config module; config validated at startup
|
|
752
|
+
7. **Output Encoding** - No unescaped `${...}` interpolation in HTML template strings
|
|
753
|
+
8. **Sensitive Data** - OTPs, reset tokens, session secrets are hashed before storage
|
|
754
|
+
9. **DI Compliance** - No manual `new Service()` / `new Provider()` in application code
|
|
755
|
+
10. **File Size** - No file exceeds 1000 lines (warning at 500+)
|
|
756
|
+
11. **Folder Size** - No folder exceeds 15 files (warning at 8+)
|
|
757
|
+
12. **Strict Typing** - No `any` types in new/changed files
|
|
758
|
+
13. **Return Types** - All exported functions have explicit return types
|
|
759
|
+
14. **Module Structure** - Files grouped by domain entity, not by type
|
|
720
760
|
|
|
721
761
|
**Review output:**
|
|
722
762
|
|
|
@@ -727,8 +767,11 @@ git diff --name-status main...HEAD
|
|
|
727
767
|
Test Coverage: ✅ 5/5 files covered
|
|
728
768
|
TDD Score: 75% ✅
|
|
729
769
|
Security: ✅ No issues
|
|
770
|
+
Authorization: ✅ All endpoints have ownership checks
|
|
771
|
+
Secrets Exposure: ✅ No secrets in responses
|
|
772
|
+
Config Hygiene: ✅ No process.env outside config
|
|
773
|
+
Output Encoding: ✅ All HTML output escaped
|
|
730
774
|
File Sizes: ✅ All under 1000 lines
|
|
731
|
-
Folder Sizes: ✅ All under 15 files
|
|
732
775
|
Strict Typing: ✅ No `any` found
|
|
733
776
|
Return Types: ✅ All exports typed
|
|
734
777
|
|
|
@@ -899,10 +942,10 @@ Found: 2-PLAN.md (4 tasks)
|
|
|
899
942
|
🚀 Overdrive Mode Available (Opus 4.6)
|
|
900
943
|
|
|
901
944
|
Phase 2 has 4 independent tasks:
|
|
902
|
-
- Task 1: Create API routes [
|
|
903
|
-
- Task 2: Add input validation [
|
|
904
|
-
- Task 3: Write error handlers [
|
|
905
|
-
- Task 4: Add rate limiting config [
|
|
945
|
+
- Task 1: Create API routes [opus]
|
|
946
|
+
- Task 2: Add input validation [opus]
|
|
947
|
+
- Task 3: Write error handlers [opus]
|
|
948
|
+
- Task 4: Add rate limiting config [opus]
|
|
906
949
|
|
|
907
950
|
Recommended: 4 agents (one per task)
|
|
908
951
|
|
|
@@ -916,17 +959,17 @@ User: 1
|
|
|
916
959
|
Claude: 🚀 Launching Overdrive Mode (Opus 4.6)
|
|
917
960
|
|
|
918
961
|
Spawning 4 agents...
|
|
919
|
-
[Agent 1] Task 1: Create API routes [
|
|
920
|
-
[Agent 2] Task 2: Add input validation [
|
|
921
|
-
[Agent 3] Task 3: Write error handlers [
|
|
922
|
-
[Agent 4] Task 4: Add rate limiting config [
|
|
962
|
+
[Agent 1] Task 1: Create API routes [opus] - STARTED
|
|
963
|
+
[Agent 2] Task 2: Add input validation [opus] - STARTED
|
|
964
|
+
[Agent 3] Task 3: Write error handlers [opus] - STARTED
|
|
965
|
+
[Agent 4] Task 4: Add rate limiting config [opus] - STARTED
|
|
923
966
|
|
|
924
967
|
... agents working in background ...
|
|
925
968
|
|
|
926
|
-
[Agent 4] ✅ Task 4 complete (1 commit) [
|
|
927
|
-
[Agent 2] ✅ Task 2 complete (3 commits) [
|
|
928
|
-
[Agent 1] ✅ Task 1 complete (4 commits) [
|
|
929
|
-
[Agent 3] ✅ Task 3 complete (2 commits) [
|
|
969
|
+
[Agent 4] ✅ Task 4 complete (1 commit) [opus]
|
|
970
|
+
[Agent 2] ✅ Task 2 complete (3 commits) [opus]
|
|
971
|
+
[Agent 1] ✅ Task 1 complete (4 commits) [opus]
|
|
972
|
+
[Agent 3] ✅ Task 3 complete (2 commits) [opus]
|
|
930
973
|
|
|
931
974
|
All agents complete. Running full test suite...
|
|
932
975
|
✅ 24 tests passing
|
|
@@ -957,7 +1000,7 @@ Phase 2 complete. Ready for /tlc:verify 2
|
|
|
957
1000
|
- Merge conflicts → Agents working on same files (rare if tasks are truly independent)
|
|
958
1001
|
- One agent failed → Other agents continue; resume failed agent or fix manually
|
|
959
1002
|
- Want sequential instead → Use `--sequential` flag: `/tlc:build 2 --sequential`
|
|
960
|
-
- Cost too high → Use `--
|
|
1003
|
+
- Cost too high → Use `--agents 2` to limit parallelism
|
|
961
1004
|
- Agent running too long → Use `--max-turns 30` to limit execution
|
|
962
1005
|
|
|
963
1006
|
## Flags
|
|
@@ -966,7 +1009,7 @@ Phase 2 complete. Ready for /tlc:verify 2
|
|
|
966
1009
|
|------|-------------|
|
|
967
1010
|
| `--sequential` | Force sequential execution even if tasks are independent |
|
|
968
1011
|
| `--agents N` | Limit parallel agents to N (default: one per independent task) |
|
|
969
|
-
| `--model
|
|
1012
|
+
| `--model opus` | Force all agents to opus (default — all tiers use opus) |
|
|
970
1013
|
| `--max-turns N` | Limit each agent's execution to N turns (default: 50) |
|
|
971
1014
|
| `--providers claude,codex` | Force specific providers (default: auto-detect from router state) |
|
|
972
1015
|
| `--no-tmux` | Skip tmux panes, run worktree agents in background |
|
|
@@ -89,6 +89,15 @@ For a phase that's been (partially) built, verify TLC compliance:
|
|
|
89
89
|
- [ ] No hardcoded secrets, URLs, or credentials (check for patterns)
|
|
90
90
|
- [ ] No skipped tests (`.skip`, `xit`, `xdescribe`)
|
|
91
91
|
|
|
92
|
+
**Security & Config (CODING-STANDARDS §6, §21, §22):**
|
|
93
|
+
- [ ] Every data-access endpoint has ownership check (not just auth — verify user owns the resource)
|
|
94
|
+
- [ ] No secrets (API keys, tokens, passwords) returned in API responses or rendered in HTML
|
|
95
|
+
- [ ] No `process.env` in services/controllers — all config through validated config module
|
|
96
|
+
- [ ] No unescaped `${...}` interpolation in HTML template strings
|
|
97
|
+
- [ ] OTPs, reset tokens, session secrets are hashed before storage (not plaintext)
|
|
98
|
+
- [ ] No `new ServiceClass()` / `new ProviderClass()` — use DI container
|
|
99
|
+
- [ ] Tests prove user A cannot read/modify user B's data (ownership test)
|
|
100
|
+
|
|
92
101
|
**Process:**
|
|
93
102
|
- [ ] Phase plan exists and tasks are checked off
|
|
94
103
|
- [ ] Implementation matches what was planned (no scope creep)
|
|
@@ -207,7 +207,18 @@ Start writing tests now, or save backlog for later?
|
|
|
207
207
|
|
|
208
208
|
**If "Start now":** Begin writing tests for the first critical path item using Red-Green-Refactor (but code already exists, so focus on capturing current behavior).
|
|
209
209
|
|
|
210
|
-
### 9.
|
|
210
|
+
### 9. Inject Standards (CLAUDE.md + CODING-STANDARDS.md)
|
|
211
|
+
|
|
212
|
+
**First, inject both standards files using the standards-injector module:**
|
|
213
|
+
|
|
214
|
+
```javascript
|
|
215
|
+
const { injectStandards } = require('./lib/standards/standards-injector');
|
|
216
|
+
const results = await injectStandards(projectPath);
|
|
217
|
+
// Creates CLAUDE.md and CODING-STANDARDS.md from templates if missing
|
|
218
|
+
// Appends TLC section to existing CLAUDE.md if present
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
**If the module is not available** (e.g., TLC server not installed locally), create `CLAUDE.md` manually and copy `CODING-STANDARDS.md` from the TLC templates directory.
|
|
211
222
|
|
|
212
223
|
Create `CLAUDE.md` to enforce TLC workflow over Claude's default behaviors:
|
|
213
224
|
|
|
@@ -116,6 +116,8 @@ git log --oneline --name-status main..HEAD
|
|
|
116
116
|
|
|
117
117
|
Scan diff for common security issues:
|
|
118
118
|
|
|
119
|
+
**Pattern-Based Checks:**
|
|
120
|
+
|
|
119
121
|
| Pattern | Issue | Severity |
|
|
120
122
|
|---------|-------|----------|
|
|
121
123
|
| `password = "..."` | Hardcoded password | HIGH |
|
|
@@ -126,6 +128,17 @@ Scan diff for common security issues:
|
|
|
126
128
|
| `exec("..." + var)` | Command injection | HIGH |
|
|
127
129
|
| `SELECT...WHERE...+` | SQL injection | HIGH |
|
|
128
130
|
|
|
131
|
+
**Semantic Security Checks (read the code, not just grep):**
|
|
132
|
+
|
|
133
|
+
| Check | What to Look For | Severity |
|
|
134
|
+
|-------|-----------------|----------|
|
|
135
|
+
| **Ownership/IDOR** | Controller methods that take an ID param (`req.params.id`, `@Param('id')`) and query data without checking the requesting user owns the resource. Every data-access endpoint MUST verify ownership, not just authentication. | HIGH |
|
|
136
|
+
| **Secrets in responses** | Response objects, DTOs, or `res.json()`/`res.send()` calls that include fields like `apiKey`, `secret`, `token`, `password`, `webhookSecret`. Secrets are write-only — never return them. | HIGH |
|
|
137
|
+
| **Direct `process.env`** | `process.env.` usage in service, controller, or middleware files. All env access MUST go through a validated config module. Grep: `process\.env\.` in non-config files. | MEDIUM |
|
|
138
|
+
| **Unescaped HTML** | Template literals that build HTML with `${...}` interpolation of variables (e.g., `` `<h1>${user.name}</h1>` ``). All dynamic values in HTML MUST be escaped. | HIGH |
|
|
139
|
+
| **Plaintext sensitive data** | OTP codes, reset tokens, or session secrets stored without hashing. Look for database inserts/updates of these values without a hash step. | HIGH |
|
|
140
|
+
| **Manual instantiation** | `new SomeProvider(...)` or `new SomeService(...)` in application code instead of using DI. | MEDIUM |
|
|
141
|
+
|
|
129
142
|
**Fail if:** Any HIGH severity issues found.
|
|
130
143
|
|
|
131
144
|
### Step 5b: Coding Standards Check
|
|
@@ -311,6 +324,12 @@ After Steps 2-6 complete, if ANY issues were found:
|
|
|
311
324
|
| `any` type | Replace with proper interface | If domain type unclear |
|
|
312
325
|
| File >1000 lines | Split into sub-modules | If split strategy unclear |
|
|
313
326
|
| Security vulnerability | Patch it | If fix might break behavior |
|
|
327
|
+
| Missing ownership check | Add guard/check | If ownership model unclear |
|
|
328
|
+
| Secrets in response | Remove or mask fields | If field is needed by client |
|
|
329
|
+
| Direct `process.env` | Move to config module | If config module doesn't exist yet |
|
|
330
|
+
| Unescaped HTML | Add escapeHtml() | If templating engine preferred |
|
|
331
|
+
| Plaintext sensitive data | Add hash step | - |
|
|
332
|
+
| Manual `new Service()` | Convert to DI | If DI container not set up |
|
|
314
333
|
| Codex-flagged bug | Apply suggestion | If suggestion conflicts with Claude |
|
|
315
334
|
| Merge conflict | - | Always human |
|
|
316
335
|
|
package/CODING-STANDARDS.md
CHANGED
|
@@ -241,25 +241,64 @@ if (status === 'pending') { ... } // TypeScript will validate
|
|
|
241
241
|
|
|
242
242
|
---
|
|
243
243
|
|
|
244
|
-
## 6. Configuration: No
|
|
244
|
+
## 6. Configuration: Validated, Centralized, No Direct `process.env`
|
|
245
|
+
|
|
246
|
+
### Centralized Config with Validation
|
|
247
|
+
|
|
248
|
+
All configuration MUST go through a validated config module. Never read `process.env` directly in application code.
|
|
245
249
|
|
|
246
250
|
```typescript
|
|
247
|
-
// ❌ NEVER
|
|
251
|
+
// ❌ NEVER: Direct process.env in services/controllers
|
|
248
252
|
class PaymentService {
|
|
249
|
-
private baseUrl =
|
|
250
|
-
private
|
|
253
|
+
private baseUrl = process.env.STRIPE_BASE_URL || 'https://api.stripe.com';
|
|
254
|
+
private apiKey = process.env.STRIPE_API_KEY; // silently undefined if missing
|
|
251
255
|
}
|
|
252
256
|
|
|
253
|
-
//
|
|
254
|
-
// lib/configuration.ts or shared/config/stripe.config.ts
|
|
257
|
+
// ❌ NEVER: Config without validation
|
|
255
258
|
export const stripeConfig = {
|
|
256
259
|
baseUrl: process.env.STRIPE_BASE_URL || 'https://api.stripe.com',
|
|
257
|
-
apiKey: process.env.STRIPE_API_KEY,
|
|
258
|
-
|
|
259
|
-
|
|
260
|
+
apiKey: process.env.STRIPE_API_KEY, // no validation, no fail-fast
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
// ✅ ALWAYS: Validated config module with fail-fast
|
|
264
|
+
// src/config/config.ts
|
|
265
|
+
import { z } from 'zod';
|
|
266
|
+
|
|
267
|
+
const configSchema = z.object({
|
|
268
|
+
stripe: z.object({
|
|
269
|
+
baseUrl: z.string().url().default('https://api.stripe.com'),
|
|
270
|
+
apiKey: z.string().min(1, 'STRIPE_API_KEY is required'),
|
|
271
|
+
timeout: z.coerce.number().int().positive().default(30000),
|
|
272
|
+
}),
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
export type AppConfig = z.infer<typeof configSchema>;
|
|
276
|
+
|
|
277
|
+
export function loadConfig(): AppConfig {
|
|
278
|
+
const result = configSchema.safeParse({
|
|
279
|
+
stripe: {
|
|
280
|
+
baseUrl: process.env.STRIPE_BASE_URL,
|
|
281
|
+
apiKey: process.env.STRIPE_API_KEY,
|
|
282
|
+
timeout: process.env.STRIPE_TIMEOUT,
|
|
283
|
+
},
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
if (!result.success) {
|
|
287
|
+
throw new Error(`Config validation failed:\n${result.error.format()}`);
|
|
288
|
+
}
|
|
289
|
+
return result.data;
|
|
290
|
+
}
|
|
260
291
|
```
|
|
261
292
|
|
|
262
|
-
|
|
293
|
+
### Rules
|
|
294
|
+
|
|
295
|
+
1. **All `process.env` access MUST be in the config module** — never in services, controllers, or middleware.
|
|
296
|
+
2. **All required env vars MUST be validated at startup** — fail fast, not at first request.
|
|
297
|
+
3. **Distinguish required vs optional** — required vars throw on boot; optional have explicit defaults.
|
|
298
|
+
4. **No silent empty-string fallbacks** for secrets or connection strings.
|
|
299
|
+
5. **Config is injected via DI or imported from the config module** — never read from env directly.
|
|
300
|
+
|
|
301
|
+
**Rule**: If it could differ between environments, it's config. If it's required, validate it at boot.
|
|
263
302
|
|
|
264
303
|
---
|
|
265
304
|
|
|
@@ -432,6 +471,13 @@ Before committing any code:
|
|
|
432
471
|
- [ ] Typed errors, not generic throws
|
|
433
472
|
- [ ] Tests co-located with module
|
|
434
473
|
- [ ] Build passes (`npm run build`)
|
|
474
|
+
- [ ] **No direct `process.env`** in services/controllers — config module only
|
|
475
|
+
- [ ] **Config validated at startup** with schema (Zod/Joi)
|
|
476
|
+
- [ ] **Ownership checks** on every data-access endpoint
|
|
477
|
+
- [ ] **No secrets in responses** — API keys, tokens are write-only
|
|
478
|
+
- [ ] **Sensitive data hashed** at rest (OTPs, reset tokens)
|
|
479
|
+
- [ ] **All HTML output escaped** — no raw interpolation of user values
|
|
480
|
+
- [ ] **No manual `new Service()`** — use DI container
|
|
435
481
|
|
|
436
482
|
---
|
|
437
483
|
|
|
@@ -723,6 +769,160 @@ function handleResponse<T>(response: ApiResponse<T>): T {
|
|
|
723
769
|
|
|
724
770
|
---
|
|
725
771
|
|
|
772
|
+
## 21. Security: Authorization, Secrets, and Output Encoding
|
|
773
|
+
|
|
774
|
+
### Resource Ownership Checks
|
|
775
|
+
|
|
776
|
+
Every endpoint that accesses a resource MUST verify the requesting user owns or has permission to access that resource. Authentication alone is not enough.
|
|
777
|
+
|
|
778
|
+
```typescript
|
|
779
|
+
// ❌ WRONG: Only checks authentication, not ownership
|
|
780
|
+
@Get('settings/:merchantId')
|
|
781
|
+
@UseGuards(AuthGuard)
|
|
782
|
+
async getSettings(@Param('merchantId') merchantId: string): Promise<Settings> {
|
|
783
|
+
return this.settingsService.findByMerchant(merchantId); // any authed user can read any merchant
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// ✅ CORRECT: Verifies ownership
|
|
787
|
+
@Get('settings/:merchantId')
|
|
788
|
+
@UseGuards(AuthGuard)
|
|
789
|
+
async getSettings(
|
|
790
|
+
@Param('merchantId') merchantId: string,
|
|
791
|
+
@CurrentUser() user: User,
|
|
792
|
+
): Promise<Settings> {
|
|
793
|
+
if (user.merchantId !== merchantId && user.role !== 'admin') {
|
|
794
|
+
throw new ForbiddenError('Cannot access another merchant\'s settings');
|
|
795
|
+
}
|
|
796
|
+
return this.settingsService.findByMerchant(merchantId);
|
|
797
|
+
}
|
|
798
|
+
```
|
|
799
|
+
|
|
800
|
+
**Rules:**
|
|
801
|
+
1. Every data-access endpoint MUST check that the requesting user owns the resource.
|
|
802
|
+
2. Tests MUST prove that user A cannot read/modify user B's data.
|
|
803
|
+
3. Prefer a reusable ownership guard over per-endpoint checks.
|
|
804
|
+
|
|
805
|
+
### Never Expose Secrets in Responses
|
|
806
|
+
|
|
807
|
+
API keys, webhook secrets, tokens, and credentials MUST never appear in API responses or rendered HTML.
|
|
808
|
+
|
|
809
|
+
```typescript
|
|
810
|
+
// ❌ WRONG: Returning secrets to the client
|
|
811
|
+
return {
|
|
812
|
+
merchantId: merchant.id,
|
|
813
|
+
apiKey: merchant.apiKey, // NEVER
|
|
814
|
+
webhookSecret: merchant.webhookSecret, // NEVER
|
|
815
|
+
};
|
|
816
|
+
|
|
817
|
+
// ✅ CORRECT: Mask or omit secrets
|
|
818
|
+
return {
|
|
819
|
+
merchantId: merchant.id,
|
|
820
|
+
apiKey: mask(merchant.apiKey), // "sk_live_...4x7f"
|
|
821
|
+
webhookSecret: '••••••••', // write-only, never read back
|
|
822
|
+
};
|
|
823
|
+
```
|
|
824
|
+
|
|
825
|
+
**Rules:**
|
|
826
|
+
1. Secrets are **write-only** — accept on create/update, never return in GET responses.
|
|
827
|
+
2. If display is needed, return masked values (first 4 + last 4 chars).
|
|
828
|
+
3. Audit every response DTO and HTML template for leaked credentials.
|
|
829
|
+
|
|
830
|
+
### Hash Sensitive Data at Rest
|
|
831
|
+
|
|
832
|
+
OTP codes, reset tokens, and session secrets MUST be stored as one-way hashes, never plaintext.
|
|
833
|
+
|
|
834
|
+
```typescript
|
|
835
|
+
// ❌ WRONG: Plaintext OTP
|
|
836
|
+
await db.otpSessions.insert({ code: '123456', expiresAt });
|
|
837
|
+
|
|
838
|
+
// ✅ CORRECT: Hashed OTP
|
|
839
|
+
import { createHash } from 'crypto';
|
|
840
|
+
const hashedCode = createHash('sha256').update(code).digest('hex');
|
|
841
|
+
await db.otpSessions.insert({ codeHash: hashedCode, expiresAt });
|
|
842
|
+
|
|
843
|
+
// Verification: hash the input and compare
|
|
844
|
+
function verifyOtp(input: string, stored: string): boolean {
|
|
845
|
+
const inputHash = createHash('sha256').update(input).digest('hex');
|
|
846
|
+
return timingSafeEqual(Buffer.from(inputHash), Buffer.from(stored));
|
|
847
|
+
}
|
|
848
|
+
```
|
|
849
|
+
|
|
850
|
+
### Output Encoding (XSS Prevention)
|
|
851
|
+
|
|
852
|
+
Never interpolate user-controlled values into HTML without escaping. This applies to inline HTML generation, template strings, and server-rendered pages.
|
|
853
|
+
|
|
854
|
+
```typescript
|
|
855
|
+
// ❌ WRONG: Raw interpolation — XSS risk
|
|
856
|
+
const html = `<h1>Welcome, ${user.name}</h1>`;
|
|
857
|
+
const html = `<a href="${redirectUrl}">Continue</a>`;
|
|
858
|
+
|
|
859
|
+
// ✅ CORRECT: Always escape
|
|
860
|
+
import { escapeHtml } from '@/shared/utils/escape';
|
|
861
|
+
|
|
862
|
+
const html = `<h1>Welcome, ${escapeHtml(user.name)}</h1>`;
|
|
863
|
+
const html = `<a href="${escapeHtml(redirectUrl)}">Continue</a>`;
|
|
864
|
+
```
|
|
865
|
+
|
|
866
|
+
```typescript
|
|
867
|
+
// shared/utils/escape.ts
|
|
868
|
+
/**
|
|
869
|
+
* Escapes HTML special characters to prevent XSS.
|
|
870
|
+
*/
|
|
871
|
+
export function escapeHtml(str: string): string {
|
|
872
|
+
return str
|
|
873
|
+
.replace(/&/g, '&')
|
|
874
|
+
.replace(/</g, '<')
|
|
875
|
+
.replace(/>/g, '>')
|
|
876
|
+
.replace(/"/g, '"')
|
|
877
|
+
.replace(/'/g, ''');
|
|
878
|
+
}
|
|
879
|
+
```
|
|
880
|
+
|
|
881
|
+
**Rules:**
|
|
882
|
+
1. **Every** dynamic value in HTML output MUST be escaped.
|
|
883
|
+
2. Prefer a real templating engine over string concatenation for HTML.
|
|
884
|
+
3. Audit merchant-controlled, query-string, and user-input values first.
|
|
885
|
+
4. Do not add new pages using inline HTML string builders — use a proper frontend.
|
|
886
|
+
|
|
887
|
+
---
|
|
888
|
+
|
|
889
|
+
## 22. Dependency Injection: No Manual Instantiation
|
|
890
|
+
|
|
891
|
+
Never manually instantiate services or providers that should be managed by the DI container.
|
|
892
|
+
|
|
893
|
+
```typescript
|
|
894
|
+
// ❌ WRONG: Bypassing DI
|
|
895
|
+
class PaymentService {
|
|
896
|
+
async processPayment(method: string): Promise<void> {
|
|
897
|
+
const provider = method === 'cyberpay'
|
|
898
|
+
? new CyberpayProvider(process.env.CYBERPAY_KEY) // untestable, unmanaged
|
|
899
|
+
: new CodProvider();
|
|
900
|
+
await provider.charge(amount);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// ✅ CORRECT: Provider registry via DI
|
|
905
|
+
@Injectable()
|
|
906
|
+
class PaymentService {
|
|
907
|
+
constructor(
|
|
908
|
+
@Inject('PAYMENT_PROVIDERS') private providers: Map<string, PaymentProvider>,
|
|
909
|
+
) {}
|
|
910
|
+
|
|
911
|
+
async processPayment(method: string): Promise<void> {
|
|
912
|
+
const provider = this.providers.get(method);
|
|
913
|
+
if (!provider) throw new BadRequestError(`Unknown payment method: ${method}`);
|
|
914
|
+
await provider.charge(amount);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
```
|
|
918
|
+
|
|
919
|
+
**Rules:**
|
|
920
|
+
1. All providers/services MUST be registered in the DI container.
|
|
921
|
+
2. Never use `new ServiceClass()` in application code — let the framework manage lifecycle.
|
|
922
|
+
3. Use factory providers or provider registries for dynamic selection.
|
|
923
|
+
|
|
924
|
+
---
|
|
925
|
+
|
|
726
926
|
## AI Instructions
|
|
727
927
|
|
|
728
928
|
When generating code:
|
|
@@ -744,6 +944,13 @@ When generating code:
|
|
|
744
944
|
15. **Never** let folders exceed 15 files - organize into subfolders
|
|
745
945
|
16. **Never** use `any` type - use `unknown` or proper interfaces
|
|
746
946
|
17. **Always** add explicit return types to functions
|
|
947
|
+
18. **Never** read `process.env` outside the config module
|
|
948
|
+
19. **Always** validate config at startup with a schema
|
|
949
|
+
20. **Always** add ownership/authorization checks on data-access endpoints
|
|
950
|
+
21. **Never** return secrets (API keys, tokens) in API responses or HTML
|
|
951
|
+
22. **Always** hash OTPs, reset tokens, and session secrets before storing
|
|
952
|
+
23. **Always** escape dynamic values in HTML output
|
|
953
|
+
24. **Never** use `new ServiceClass()` — register in DI and inject
|
|
747
954
|
|
|
748
955
|
### Cleanup Tasks
|
|
749
956
|
|