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.
@@ -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
- **After all agents complete:**
348
- 1. Run full test suite
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. Report results
351
- 4. Continue to Step 8 (verification)
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. **File Size** - No file exceeds 1000 lines (warning at 500+)
716
- 5. **Folder Size** - No folder exceeds 15 files (warning at 8+)
717
- 6. **Strict Typing** - No `any` types in new/changed files
718
- 7. **Return Types** - All exported functions have explicit return types
719
- 8. **Module Structure** - Files grouped by domain entity, not by type
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 [sonnet] (standard)
903
- - Task 2: Add input validation [sonnet] (standard)
904
- - Task 3: Write error handlers [sonnet] (standard)
905
- - Task 4: Add rate limiting config [haiku] (light)
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 [sonnet] - STARTED
920
- [Agent 2] Task 2: Add input validation [sonnet] - STARTED
921
- [Agent 3] Task 3: Write error handlers [sonnet] - STARTED
922
- [Agent 4] Task 4: Add rate limiting config [haiku] - STARTED
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) [haiku - fast]
927
- [Agent 2] ✅ Task 2 complete (3 commits) [sonnet]
928
- [Agent 1] ✅ Task 1 complete (4 commits) [sonnet]
929
- [Agent 3] ✅ Task 3 complete (2 commits) [sonnet]
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 `--model haiku` to force all agents to haiku
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 MODEL` | Force all agents to use a specific model (opus, sonnet, haiku) |
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. Create CLAUDE.md
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
 
@@ -241,25 +241,64 @@ if (status === 'pending') { ... } // TypeScript will validate
241
241
 
242
242
  ---
243
243
 
244
- ## 6. Configuration: No Hardcoded Values
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 = "https://api.stripe.com";
250
- private timeout = 30000;
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
- // ALWAYS
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
- timeout: parseInt(process.env.STRIPE_TIMEOUT || '30000'),
259
- } as const;
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
- **Rule**: If it could differ between environments, it's config.
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, '&amp;')
874
+ .replace(/</g, '&lt;')
875
+ .replace(/>/g, '&gt;')
876
+ .replace(/"/g, '&quot;')
877
+ .replace(/'/g, '&#x27;');
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tlc-claude-code",
3
- "version": "2.2.1",
3
+ "version": "2.3.0",
4
4
  "description": "TLC - Test Led Coding for Claude Code",
5
5
  "bin": {
6
6
  "tlc-claude-code": "./bin/install.js",