forgedev 1.1.3 → 1.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.
Files changed (167) hide show
  1. package/README.md +58 -10
  2. package/bin/chainproof.js +126 -0
  3. package/bin/devforge.js +2 -1
  4. package/package.json +33 -7
  5. package/src/chainproof-bridge.js +330 -0
  6. package/src/ci-mode.js +85 -0
  7. package/src/claude-configurator.js +87 -49
  8. package/src/cli.js +35 -12
  9. package/src/composer.js +159 -34
  10. package/src/doctor-checks-chainproof.js +106 -0
  11. package/src/doctor-checks.js +39 -20
  12. package/src/doctor-prompts.js +9 -9
  13. package/src/doctor.js +37 -4
  14. package/src/guided.js +3 -3
  15. package/src/index.js +31 -10
  16. package/src/init-mode.js +64 -11
  17. package/src/menu.js +178 -0
  18. package/src/prompts.js +5 -12
  19. package/src/recommender.js +134 -10
  20. package/src/scanner.js +57 -2
  21. package/src/uat-generator.js +204 -189
  22. package/src/update-check.js +9 -4
  23. package/src/update.js +1 -1
  24. package/src/utils.js +65 -6
  25. package/templates/ai/guardrails-py/backend/app/ai/__init__.py +29 -0
  26. package/templates/ai/guardrails-py/backend/app/ai/audit_log.py +133 -0
  27. package/templates/ai/guardrails-py/backend/app/ai/client.py.template +323 -0
  28. package/templates/ai/guardrails-py/backend/app/ai/health.py.template +157 -0
  29. package/templates/ai/guardrails-py/backend/app/ai/input_guard.py +98 -0
  30. package/templates/ai/guardrails-ts/src/lib/ai/audit-log.ts.template +164 -0
  31. package/templates/ai/guardrails-ts/src/lib/ai/client.ts.template +403 -0
  32. package/templates/ai/guardrails-ts/src/lib/ai/health.ts.template +165 -0
  33. package/templates/ai/guardrails-ts/src/lib/ai/index.ts.template +17 -0
  34. package/templates/ai/guardrails-ts/src/lib/ai/input-guard.ts.template +124 -0
  35. package/templates/auth/nextauth/src/lib/auth.ts.template +12 -7
  36. package/templates/backend/express/Dockerfile.template +18 -0
  37. package/templates/backend/express/package.json.template +33 -0
  38. package/templates/backend/express/src/index.ts.template +34 -0
  39. package/templates/backend/express/src/routes/health.ts.template +27 -0
  40. package/templates/backend/express/tsconfig.json +17 -0
  41. package/templates/backend/fastapi/backend/Dockerfile.template +5 -0
  42. package/templates/backend/fastapi/backend/app/api/health.py.template +1 -1
  43. package/templates/backend/fastapi/backend/app/core/config.py.template +1 -1
  44. package/templates/backend/fastapi/backend/app/core/errors.py +1 -1
  45. package/templates/backend/fastapi/backend/app/main.py.template +3 -1
  46. package/templates/backend/fastapi/backend/requirements.txt.template +2 -0
  47. package/templates/backend/hono/Dockerfile.template +18 -0
  48. package/templates/backend/hono/package.json.template +31 -0
  49. package/templates/backend/hono/src/index.ts.template +32 -0
  50. package/templates/backend/hono/src/routes/health.ts.template +27 -0
  51. package/templates/backend/hono/tsconfig.json +18 -0
  52. package/templates/base/docs/plans/.gitkeep +0 -0
  53. package/templates/base/docs/uat/UAT_CHECKLIST.csv.template +2 -0
  54. package/templates/base/docs/uat/UAT_TEMPLATE.md.template +22 -0
  55. package/templates/chainproof/base/.chainproof/config.json.template +11 -0
  56. package/templates/chainproof/base/.chainproof/mcp-server.mjs +310 -0
  57. package/templates/chainproof/base/.mcp.json +9 -0
  58. package/templates/chainproof/fastapi/.chainproof/middleware.json.template +14 -0
  59. package/templates/chainproof/nextjs/.chainproof/hooks.json.template +19 -0
  60. package/templates/chainproof/polyglot/.chainproof/config.json.template +21 -0
  61. package/templates/claude-code/agents/architect.md +25 -11
  62. package/templates/claude-code/agents/build-error-resolver.md +22 -7
  63. package/templates/claude-code/agents/chief-of-staff.md +42 -8
  64. package/templates/claude-code/agents/code-quality-reviewer.md +15 -1
  65. package/templates/claude-code/agents/database-reviewer.md +16 -2
  66. package/templates/claude-code/agents/deep-reviewer.md +191 -0
  67. package/templates/claude-code/agents/doc-updater.md +19 -5
  68. package/templates/claude-code/agents/docs-lookup.md +19 -5
  69. package/templates/claude-code/agents/e2e-runner.md +26 -12
  70. package/templates/claude-code/agents/enforcement-gate.md +102 -0
  71. package/templates/claude-code/agents/frontend-builder.md +188 -0
  72. package/templates/claude-code/agents/harness-optimizer.md +61 -0
  73. package/templates/claude-code/agents/loop-operator.md +27 -12
  74. package/templates/claude-code/agents/planner.md +21 -7
  75. package/templates/claude-code/agents/product-strategist.md +138 -0
  76. package/templates/claude-code/agents/production-readiness.md +14 -0
  77. package/templates/claude-code/agents/prompt-auditor.md +115 -0
  78. package/templates/claude-code/agents/refactor-cleaner.md +22 -8
  79. package/templates/claude-code/agents/security-reviewer.md +15 -0
  80. package/templates/claude-code/agents/spec-validator.md +45 -1
  81. package/templates/claude-code/agents/tdd-guide.md +21 -7
  82. package/templates/claude-code/agents/uat-validator.md +18 -0
  83. package/templates/claude-code/claude-md/base.md +15 -7
  84. package/templates/claude-code/claude-md/fastapi.md +8 -8
  85. package/templates/claude-code/claude-md/fullstack.md +6 -6
  86. package/templates/claude-code/claude-md/hono.md +18 -0
  87. package/templates/claude-code/claude-md/nextjs.md +5 -5
  88. package/templates/claude-code/claude-md/remix.md +18 -0
  89. package/templates/claude-code/commands/audit-security.md +14 -0
  90. package/templates/claude-code/commands/audit-spec.md +14 -0
  91. package/templates/claude-code/commands/audit-wiring.md +14 -0
  92. package/templates/claude-code/commands/build-fix.md +28 -0
  93. package/templates/claude-code/commands/build-ui.md +59 -0
  94. package/templates/claude-code/commands/code-review.md +54 -26
  95. package/templates/claude-code/commands/fix-loop.md +211 -0
  96. package/templates/claude-code/commands/full-audit.md +37 -8
  97. package/templates/claude-code/commands/generate-prd.md +1 -1
  98. package/templates/claude-code/commands/generate-sdd.md +74 -0
  99. package/templates/claude-code/commands/generate-uat.md +107 -35
  100. package/templates/claude-code/commands/help.md +68 -0
  101. package/templates/claude-code/commands/live-uat.md +268 -0
  102. package/templates/claude-code/commands/optimize-claude-md.md +15 -1
  103. package/templates/claude-code/commands/plan.md +3 -3
  104. package/templates/claude-code/commands/pre-pr.md +57 -19
  105. package/templates/claude-code/commands/product-strategist.md +21 -0
  106. package/templates/claude-code/commands/resume-session.md +10 -10
  107. package/templates/claude-code/commands/run-uat.md +59 -2
  108. package/templates/claude-code/commands/save-session.md +10 -10
  109. package/templates/claude-code/commands/simplify.md +36 -0
  110. package/templates/claude-code/commands/tdd.md +17 -18
  111. package/templates/claude-code/commands/verify-all.md +24 -0
  112. package/templates/claude-code/commands/verify-intent.md +55 -0
  113. package/templates/claude-code/commands/workflows.md +52 -37
  114. package/templates/claude-code/hooks/polyglot.json +10 -1
  115. package/templates/claude-code/hooks/python.json +10 -1
  116. package/templates/claude-code/hooks/scripts/autofix-polyglot.mjs +20 -10
  117. package/templates/claude-code/hooks/scripts/autofix-python.mjs +4 -5
  118. package/templates/claude-code/hooks/scripts/autofix-typescript.mjs +4 -4
  119. package/templates/claude-code/hooks/scripts/code-hygiene.mjs +293 -0
  120. package/templates/claude-code/hooks/scripts/guard-protected-files.mjs +2 -2
  121. package/templates/claude-code/hooks/scripts/pre-commit-gate.mjs +207 -0
  122. package/templates/claude-code/hooks/typescript.json +10 -1
  123. package/templates/claude-code/skills/ai-prompts/SKILL.md +119 -41
  124. package/templates/claude-code/skills/git-workflow/SKILL.md +6 -6
  125. package/templates/claude-code/skills/nextjs/SKILL.md +1 -1
  126. package/templates/claude-code/skills/playwright/SKILL.md +6 -5
  127. package/templates/claude-code/skills/security-api/SKILL.md +1 -1
  128. package/templates/claude-code/skills/security-web/SKILL.md +2 -1
  129. package/templates/claude-code/skills/testing-patterns/SKILL.md +9 -9
  130. package/templates/database/prisma-postgres/{.env.example → .env.example.template} +1 -0
  131. package/templates/database/sqlalchemy-postgres/{.env.example → .env.example.template} +1 -0
  132. package/templates/docs-portal/fastapi/backend/app/portal/__init__.py +0 -0
  133. package/templates/docs-portal/fastapi/backend/app/portal/__pycache__/docs_reader.cpython-314.pyc +0 -0
  134. package/templates/docs-portal/fastapi/backend/app/portal/docs_reader.py +201 -0
  135. package/templates/docs-portal/fastapi/backend/app/portal/html_renderer.py +229 -0
  136. package/templates/docs-portal/fastapi/backend/app/portal/router.py.template +35 -0
  137. package/templates/docs-portal/nextjs/src/app/portal/[category]/[slug]/page.tsx +81 -0
  138. package/templates/docs-portal/nextjs/src/app/portal/[category]/page.tsx +65 -0
  139. package/templates/docs-portal/nextjs/src/app/portal/layout.tsx.template +54 -0
  140. package/templates/docs-portal/nextjs/src/app/portal/page.tsx +85 -0
  141. package/templates/docs-portal/nextjs/src/components/portal/markdown-renderer.tsx +101 -0
  142. package/templates/docs-portal/nextjs/src/components/portal/mobile-portal-nav.tsx +81 -0
  143. package/templates/docs-portal/nextjs/src/components/portal/portal-nav.tsx +86 -0
  144. package/templates/docs-portal/nextjs/src/lib/docs.ts +139 -0
  145. package/templates/frontend/nextjs/package.json.template +3 -1
  146. package/templates/frontend/react/index.html.template +12 -0
  147. package/templates/frontend/react/package.json.template +34 -0
  148. package/templates/frontend/react/src/App.tsx.template +10 -0
  149. package/templates/frontend/react/src/index.css +1 -0
  150. package/templates/frontend/react/src/main.tsx +10 -0
  151. package/templates/frontend/react/tsconfig.json +17 -0
  152. package/templates/frontend/react/vite.config.ts.template +15 -0
  153. package/templates/frontend/react/vitest.config.ts +9 -0
  154. package/templates/frontend/remix/app/root.tsx.template +31 -0
  155. package/templates/frontend/remix/app/routes/_index.tsx.template +19 -0
  156. package/templates/frontend/remix/app/routes/api.health.ts.template +10 -0
  157. package/templates/frontend/remix/app/tailwind.css +1 -0
  158. package/templates/frontend/remix/package.json.template +39 -0
  159. package/templates/frontend/remix/tsconfig.json +18 -0
  160. package/templates/frontend/remix/vite.config.ts.template +7 -0
  161. package/templates/infra/github-actions/.github/workflows/ci.yml.template +52 -0
  162. package/templates/testing/pytest/backend/tests/__init__.py +0 -0
  163. package/templates/testing/pytest/backend/tests/conftest.py.template +11 -0
  164. package/templates/testing/pytest/backend/tests/test_health.py.template +10 -0
  165. package/templates/testing/vitest/vitest.config.ts.template +18 -0
  166. package/CLAUDE.md +0 -38
  167. package/templates/claude-code/commands/done.md +0 -19
@@ -0,0 +1,165 @@
1
+ /**
2
+ * AI Health Check — Observability endpoint for AI system metrics.
3
+ *
4
+ * Compliance: NIST AI RMF Manage 3.2 (monitoring),
5
+ * EU AI Act Art. 9 (risk management)
6
+ *
7
+ * Exposes metrics for monitoring dashboards and alerting:
8
+ * - Model availability and latency
9
+ * - Confidence score distribution
10
+ * - Human review trigger rate
11
+ * - Error rate and types
12
+ * - Token usage and cost estimation
13
+ *
14
+ * Mount this at /api/ai/health or similar endpoint.
15
+ */
16
+
17
+ interface CallMetric {
18
+ timestamp: number;
19
+ model: string;
20
+ latencyMs: number;
21
+ confidence: number;
22
+ success: boolean;
23
+ tokenUsage?: { inputTokens: number; outputTokens: number };
24
+ }
25
+
26
+ class AIHealthMetrics {
27
+ private metrics: CallMetric[] = [];
28
+ private maxMetrics = 5000;
29
+
30
+ recordCall(metric: Omit<CallMetric, 'timestamp'>): void {
31
+ this.metrics.push({ ...metric, timestamp: Date.now() });
32
+ if (this.metrics.length > this.maxMetrics) {
33
+ this.metrics = this.metrics.slice(-this.maxMetrics);
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Get AI health status for the health check endpoint.
39
+ */
40
+ getHealthStatus(windowMs = 3600_000): AIHealthStatus {
41
+ const cutoff = Date.now() - windowMs;
42
+ const recent = this.metrics.filter(m => m.timestamp >= cutoff);
43
+
44
+ if (recent.length === 0) {
45
+ return {
46
+ status: 'ok',
47
+ aiAvailable: true,
48
+ message: 'No AI calls in the monitoring window',
49
+ window: `${windowMs / 60_000}m`,
50
+ metrics: this.emptyMetrics(),
51
+ models: {},
52
+ };
53
+ }
54
+
55
+ const successes = recent.filter(m => m.success);
56
+ const errorRate = 1 - (successes.length / recent.length);
57
+ const avgConfidence = successes.length > 0
58
+ ? successes.reduce((s, m) => s + m.confidence, 0) / successes.length
59
+ : 0;
60
+ const avgLatency = recent.reduce((s, m) => s + m.latencyMs, 0) / recent.length;
61
+ const lowConfidenceRate = successes.filter(m => m.confidence < 0.7).length / Math.max(successes.length, 1);
62
+
63
+ // Determine overall status
64
+ let status: 'ok' | 'degraded' | 'unhealthy' = 'ok';
65
+ const warnings: string[] = [];
66
+
67
+ if (errorRate > 0.5) {
68
+ status = 'unhealthy';
69
+ warnings.push(`High error rate: ${(errorRate * 100).toFixed(1)}%`);
70
+ } else if (errorRate > 0.1) {
71
+ status = 'degraded';
72
+ warnings.push(`Elevated error rate: ${(errorRate * 100).toFixed(1)}%`);
73
+ }
74
+
75
+ if (avgConfidence < 0.5) {
76
+ status = status === 'ok' ? 'degraded' : status;
77
+ warnings.push(`Low average confidence: ${(avgConfidence * 100).toFixed(1)}%`);
78
+ }
79
+
80
+ if (avgLatency > 10_000) {
81
+ status = status === 'ok' ? 'degraded' : status;
82
+ warnings.push(`High average latency: ${avgLatency.toFixed(0)}ms`);
83
+ }
84
+
85
+ if (lowConfidenceRate > 0.3) {
86
+ warnings.push(`${(lowConfidenceRate * 100).toFixed(1)}% of calls below confidence threshold`);
87
+ }
88
+
89
+ // Per-model breakdown
90
+ const models: Record<string, ModelMetrics> = {};
91
+ const modelNames = [...new Set(recent.map(m => m.model))];
92
+ for (const model of modelNames) {
93
+ const modelCalls = recent.filter(m => m.model === model);
94
+ const modelSuccesses = modelCalls.filter(m => m.success);
95
+ const totalTokens = modelCalls.reduce((s, m) => s + (m.tokenUsage?.inputTokens ?? 0) + (m.tokenUsage?.outputTokens ?? 0), 0);
96
+
97
+ models[model] = {
98
+ calls: modelCalls.length,
99
+ successRate: modelSuccesses.length / modelCalls.length,
100
+ avgLatencyMs: modelCalls.reduce((s, m) => s + m.latencyMs, 0) / modelCalls.length,
101
+ avgConfidence: modelSuccesses.length > 0
102
+ ? modelSuccesses.reduce((s, m) => s + m.confidence, 0) / modelSuccesses.length
103
+ : 0,
104
+ totalTokens,
105
+ };
106
+ }
107
+
108
+ return {
109
+ status,
110
+ aiAvailable: errorRate < 1,
111
+ message: warnings.length > 0 ? warnings.join('; ') : 'All AI systems operating normally',
112
+ window: `${windowMs / 60_000}m`,
113
+ metrics: {
114
+ totalCalls: recent.length,
115
+ successRate: 1 - errorRate,
116
+ avgConfidence,
117
+ avgLatencyMs: avgLatency,
118
+ lowConfidenceRate,
119
+ errorRate,
120
+ },
121
+ models,
122
+ };
123
+ }
124
+
125
+ private emptyMetrics() {
126
+ return {
127
+ totalCalls: 0,
128
+ successRate: 1,
129
+ avgConfidence: 0,
130
+ avgLatencyMs: 0,
131
+ lowConfidenceRate: 0,
132
+ errorRate: 0,
133
+ };
134
+ }
135
+ }
136
+
137
+ // --- Types ---
138
+
139
+ export interface AIHealthStatus {
140
+ status: 'ok' | 'degraded' | 'unhealthy';
141
+ aiAvailable: boolean;
142
+ message: string;
143
+ window: string;
144
+ metrics: {
145
+ totalCalls: number;
146
+ successRate: number;
147
+ avgConfidence: number;
148
+ avgLatencyMs: number;
149
+ lowConfidenceRate: number;
150
+ errorRate: number;
151
+ };
152
+ models: Record<string, ModelMetrics>;
153
+ }
154
+
155
+ interface ModelMetrics {
156
+ calls: number;
157
+ successRate: number;
158
+ avgLatencyMs: number;
159
+ avgConfidence: number;
160
+ totalTokens: number;
161
+ }
162
+
163
+ // --- Singleton ---
164
+
165
+ export const aiHealthMetrics = new AIHealthMetrics();
@@ -0,0 +1,17 @@
1
+ /**
2
+ * AI Guardrails — {{PROJECT_NAME_PASCAL}}
3
+ *
4
+ * Central export for all AI safety infrastructure.
5
+ *
6
+ * Usage:
7
+ * import { getAIClient } from '@/lib/ai';
8
+ * const ai = getAIClient();
9
+ * const result = await ai.generate({ prompt: '...', schema: MySchema });
10
+ *
11
+ * Compliance: EU AI Act (2024/1689), NIST AI RMF 1.0
12
+ */
13
+
14
+ export { AIClient, getAIClient, type AIResponse, type AIClientConfig } from './client.js';
15
+ export { validateInput, sanitizeInput, type InputValidationResult } from './input-guard.js';
16
+ export { aiAuditLog, type AuditEntry, type AuditLogConfig } from './audit-log.js';
17
+ export { aiHealthMetrics, type AIHealthStatus } from './health.js';
@@ -0,0 +1,124 @@
1
+ /**
2
+ * AI Input Guard — Prompt injection detection and input sanitization.
3
+ *
4
+ * Compliance: EU AI Act Art. 15 (robustness), NIST AI RMF Manage 2.2
5
+ *
6
+ * Detects common prompt injection patterns before they reach the model.
7
+ * This is a defense-in-depth layer — the model itself has safety training,
8
+ * but catching obvious attacks early is cheaper and more auditable.
9
+ */
10
+
11
+ export interface InputValidationResult {
12
+ blocked: boolean;
13
+ reason?: string;
14
+ /** Detected injection patterns (for audit logging) */
15
+ detectedPatterns?: string[];
16
+ }
17
+
18
+ // --- Injection Detection Patterns ---
19
+
20
+ const INJECTION_PATTERNS: Array<{ pattern: RegExp; name: string }> = [
21
+ // Direct instruction override attempts
22
+ { pattern: /ignore\s+(all\s+)?(previous|prior|above)\s+(instructions|prompts|rules)/i, name: 'instruction-override' },
23
+ { pattern: /disregard\s+(all\s+)?(previous|prior|above|your)\s+(instructions|prompts|rules|training)/i, name: 'instruction-override' },
24
+ { pattern: /forget\s+(all\s+)?(previous|prior|your)\s+(instructions|context|rules)/i, name: 'instruction-override' },
25
+
26
+ // Role manipulation
27
+ { pattern: /you\s+are\s+now\s+(a|an|the)\s+/i, name: 'role-manipulation' },
28
+ { pattern: /act\s+as\s+(if\s+you\s+are|a|an)\s+/i, name: 'role-manipulation' },
29
+ { pattern: /pretend\s+(to\s+be|you\s+are)\s+/i, name: 'role-manipulation' },
30
+ { pattern: /from\s+now\s+on\s+(you|your)\s+/i, name: 'role-manipulation' },
31
+
32
+ // System prompt extraction
33
+ { pattern: /what\s+(is|are)\s+your\s+(system\s+)?(prompt|instructions|rules)/i, name: 'prompt-extraction' },
34
+ { pattern: /show\s+me\s+your\s+(system\s+)?(prompt|instructions)/i, name: 'prompt-extraction' },
35
+ { pattern: /repeat\s+(your|the)\s+(system\s+)?(prompt|instructions)/i, name: 'prompt-extraction' },
36
+ { pattern: /print\s+(your|the)\s+(system\s+)?(prompt|instructions)/i, name: 'prompt-extraction' },
37
+
38
+ // Delimiter injection
39
+ { pattern: /\<\/?system\>/i, name: 'delimiter-injection' },
40
+ { pattern: /\[INST\]/i, name: 'delimiter-injection' },
41
+ { pattern: /\<\|im_start\|/i, name: 'delimiter-injection' },
42
+ { pattern: /###\s*(system|instruction|human|assistant)/i, name: 'delimiter-injection' },
43
+
44
+ // Data exfiltration
45
+ { pattern: /send\s+(this|the|all)\s+(data|info|conversation)\s+to/i, name: 'data-exfiltration' },
46
+ { pattern: /forward\s+(this|everything)\s+to/i, name: 'data-exfiltration' },
47
+ ];
48
+
49
+ // --- Suspicious content patterns ---
50
+
51
+ const SUSPICIOUS_PATTERNS: Array<{ pattern: RegExp; name: string }> = [
52
+ // Encoded payloads
53
+ { pattern: /eval\s*\(/i, name: 'code-execution' },
54
+ { pattern: /exec\s*\(/i, name: 'code-execution' },
55
+ { pattern: /import\s+(?:os|subprocess)|subprocess\./i, name: 'code-execution' },
56
+
57
+ // Base64 encoded content (potential hidden instructions)
58
+ { pattern: /[A-Za-z0-9+/]{100,}={0,2}/i, name: 'encoded-payload' },
59
+ ];
60
+
61
+ /**
62
+ * Validate input for prompt injection and other safety concerns.
63
+ *
64
+ * Returns blocked: true if injection detected, with details for audit logging.
65
+ * Returns blocked: false if input is clean.
66
+ */
67
+ export function validateInput(input: string): InputValidationResult {
68
+ // Normalize Unicode to defeat homoglyph attacks (e.g., Cyrillic "а" for Latin "a")
69
+ const normalized = input.normalize('NFKC');
70
+ const detectedPatterns: string[] = [];
71
+
72
+ // Check injection patterns (always block)
73
+ for (const { pattern, name } of INJECTION_PATTERNS) {
74
+ if (pattern.test(normalized)) {
75
+ detectedPatterns.push(name);
76
+ }
77
+ }
78
+
79
+ if (detectedPatterns.length > 0) {
80
+ return {
81
+ blocked: true,
82
+ reason: `Potential prompt injection detected: ${[...new Set(detectedPatterns)].join(', ')}`,
83
+ detectedPatterns,
84
+ };
85
+ }
86
+
87
+ // Check suspicious patterns (warn but don't block by default)
88
+ const suspicious: string[] = [];
89
+ for (const { pattern, name } of SUSPICIOUS_PATTERNS) {
90
+ if (pattern.test(normalized)) {
91
+ suspicious.push(name);
92
+ }
93
+ }
94
+
95
+ if (suspicious.length > 0) {
96
+ return {
97
+ blocked: false,
98
+ reason: `Suspicious patterns detected (not blocked): ${suspicious.join(', ')}`,
99
+ detectedPatterns: suspicious,
100
+ };
101
+ }
102
+
103
+ return { blocked: false };
104
+ }
105
+
106
+ /**
107
+ * Sanitize input by removing known dangerous patterns.
108
+ * Use this when you want to clean input rather than reject it.
109
+ */
110
+ export function sanitizeInput(input: string): string {
111
+ let sanitized = input;
112
+
113
+ // Remove system/instruction delimiters
114
+ sanitized = sanitized.replace(/<\/?system>/gi, '');
115
+ sanitized = sanitized.replace(/\[INST\]/gi, '');
116
+ sanitized = sanitized.replace(/<\|im_start\|[^>]*>?/gi, '');
117
+ sanitized = sanitized.replace(/<\|im_end\|>?/gi, '');
118
+ sanitized = sanitized.replace(/###\s*(system|instruction|human|assistant)/gi, '');
119
+
120
+ // Remove null bytes
121
+ sanitized = sanitized.replace(/\0/g, '');
122
+
123
+ return sanitized;
124
+ }
@@ -10,16 +10,21 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
10
10
  password: { label: 'Password', type: 'password' },
11
11
  },
12
12
  async authorize(credentials) {
13
- // TODO: Implement actual authentication logic
14
- // Replace with database lookup and password verification
13
+ // IMPORTANT: This is a placeholder. Replace this with your actual
14
+ // authentication logic (database lookup + password hash comparison)
15
+ // before going live. Returning null here means all logins are
16
+ // rejected until you wire up real verification.
15
17
  if (!credentials?.email || !credentials?.password) {
16
18
  return null;
17
19
  }
18
- return {
19
- id: '1',
20
- email: credentials.email as string,
21
- name: 'User',
22
- };
20
+
21
+ // Uncomment and modify once your user table is ready:
22
+ // const user = await db.user.findUnique({ where: { email: credentials.email } });
23
+ // if (user && await bcrypt.compare(credentials.password, user.passwordHash)) {
24
+ // return { id: user.id, email: user.email, name: user.name };
25
+ // }
26
+
27
+ return null;
23
28
  },
24
29
  }),
25
30
  ],
@@ -0,0 +1,18 @@
1
+ FROM node:22-alpine AS builder
2
+
3
+ WORKDIR /app
4
+ COPY package*.json ./
5
+ RUN npm ci
6
+ COPY . .
7
+ RUN npm run build
8
+
9
+ FROM node:22-alpine
10
+ WORKDIR /app
11
+ RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001
12
+ COPY package*.json ./
13
+ RUN npm ci --omit=dev
14
+ COPY --from=builder /app/dist ./dist
15
+ RUN chown -R nodejs:nodejs /app
16
+ USER nodejs
17
+ EXPOSE 3001
18
+ CMD ["node", "dist/index.js"]
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "{{PROJECT_NAME}}-server",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "tsx watch src/index.ts",
8
+ "build": "tsc",
9
+ "start": "node dist/index.js",
10
+ "lint": "eslint .",
11
+ "typecheck": "tsc --noEmit",
12
+ "test": "vitest run",
13
+ "test:watch": "vitest",
14
+ "db:push": "prisma db push",
15
+ "db:studio": "prisma studio",
16
+ "db:generate": "prisma generate"
17
+ },
18
+ "dependencies": {
19
+ "express": "^5.1.0",
20
+ "cors": "^2.8.5",
21
+ "@prisma/client": "^6.6.0"
22
+ },
23
+ "devDependencies": {
24
+ "typescript": "^5.8.3",
25
+ "@types/node": "^22.14.0",
26
+ "@types/express": "^5.0.0",
27
+ "@types/cors": "^2.8.17",
28
+ "tsx": "^4.19.0",
29
+ "eslint": "^9.25.0",
30
+ "prisma": "^6.6.0",
31
+ "vitest": "^3.1.1"
32
+ }
33
+ }
@@ -0,0 +1,34 @@
1
+ import express from 'express';
2
+ import cors from 'cors';
3
+ import { healthRouter } from './routes/health.js';
4
+
5
+ const app = express();
6
+ const PORT = Number(process.env.PORT) || 3001;
7
+
8
+ app.use(cors({ origin: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000'] }));
9
+ app.use(express.json());
10
+
11
+ app.use(healthRouter);
12
+
13
+ // Global error handler — never leak stack traces
14
+ app.use((err: Error, _req: express.Request, res: express.Response, next: express.NextFunction) => {
15
+ console.error('Unhandled error:', err);
16
+ if (res.headersSent) {
17
+ return next(err);
18
+ }
19
+ res.status(500).json({ error: { code: 'INTERNAL_ERROR', message: 'An unexpected error occurred' } });
20
+ });
21
+
22
+ const server = app.listen(PORT, () => {
23
+ console.log(`{{PROJECT_NAME_PASCAL}} server listening on port ${PORT}`);
24
+ });
25
+
26
+ // Graceful shutdown
27
+ function shutdown() {
28
+ console.log('Shutting down gracefully...');
29
+ server.close(() => process.exit(0));
30
+ setTimeout(() => process.exit(1), 10_000);
31
+ }
32
+
33
+ process.on('SIGINT', shutdown);
34
+ process.on('SIGTERM', shutdown);
@@ -0,0 +1,27 @@
1
+ import { Router } from 'express';
2
+
3
+ export const healthRouter = Router();
4
+
5
+ // Liveness probe — always returns ok if the process is running
6
+ healthRouter.get('/health', (_req, res) => {
7
+ res.json({
8
+ status: 'ok',
9
+ timestamp: new Date().toISOString(),
10
+ uptime: process.uptime(),
11
+ service: '{{PROJECT_NAME}}',
12
+ });
13
+ });
14
+
15
+ // Readiness probe — add real dependency checks (database, cache, etc.)
16
+ // before using this as a Kubernetes readiness probe
17
+ healthRouter.get('/healthz', async (_req, res) => {
18
+ // TODO: Replace with actual checks, e.g.:
19
+ // const db = await prisma.$queryRaw`SELECT 1`;
20
+ const checks: Record<string, string> = {};
21
+
22
+ res.json({
23
+ status: 'ok',
24
+ service: '{{PROJECT_NAME}}',
25
+ checks,
26
+ });
27
+ });
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "Node16",
5
+ "moduleResolution": "Node16",
6
+ "outDir": "dist",
7
+ "rootDir": "src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "resolveJsonModule": true,
13
+ "declaration": true
14
+ },
15
+ "include": ["src"],
16
+ "exclude": ["node_modules", "dist"]
17
+ }
@@ -2,11 +2,16 @@ FROM python:3.11-slim
2
2
 
3
3
  WORKDIR /app
4
4
 
5
+ RUN groupadd -r appuser && useradd -r -g appuser appuser
6
+
5
7
  COPY requirements.txt .
6
8
  RUN pip install --no-cache-dir -r requirements.txt
7
9
 
8
10
  COPY . .
9
11
 
12
+ RUN chown -R appuser:appuser /app
13
+ USER appuser
14
+
10
15
  EXPOSE 8000
11
16
 
12
17
  CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
@@ -16,7 +16,7 @@ async def health_check():
16
16
 
17
17
  @router.get("/healthz")
18
18
  async def health_check_deep():
19
- """Deep health check verifies database connectivity."""
19
+ """Deep health check. Verifies database connectivity."""
20
20
  try:
21
21
  async with async_session() as session:
22
22
  await session.execute(text("SELECT 1"))
@@ -6,7 +6,7 @@ class Settings(BaseSettings):
6
6
  app_name: str = "{{PROJECT_NAME_PASCAL}}"
7
7
  debug: bool = False
8
8
 
9
- # Database no default: must be set via .env or environment
9
+ # Database (no default: must be set via .env or environment)
10
10
  database_url: str
11
11
 
12
12
  # CORS
@@ -1,5 +1,5 @@
1
1
  class AppError(Exception):
2
- """Structured application error never leaks stack traces to clients."""
2
+ """Structured application error. Never leaks stack traces to clients."""
3
3
 
4
4
  def __init__(
5
5
  self,
@@ -4,6 +4,7 @@ from fastapi.middleware.cors import CORSMiddleware
4
4
  from fastapi.responses import JSONResponse
5
5
 
6
6
  from app.api.health import router as health_router
7
+ from app.portal.router import router as portal_router
7
8
  from app.core.config import settings
8
9
  from app.core.errors import AppError
9
10
 
@@ -36,9 +37,10 @@ app.add_middleware(
36
37
 
37
38
  # Routes
38
39
  app.include_router(health_router, tags=["health"])
40
+ app.include_router(portal_router)
39
41
 
40
42
 
41
- # Global exception handler never leak stack traces
43
+ # Global exception handler - never leak stack traces
42
44
  @app.exception_handler(AppError)
43
45
  async def app_error_handler(request: Request, exc: AppError):
44
46
  return JSONResponse(
@@ -10,5 +10,7 @@ passlib[bcrypt]>=1.7.4
10
10
  httpx>=0.28.0
11
11
  pytest>=8.3.0
12
12
  pytest-asyncio>=0.25.0
13
+ markdown>=3.7
14
+ bleach>=6.2.0
13
15
  ruff>=0.11.0
14
16
  pyright>=1.1.400
@@ -0,0 +1,18 @@
1
+ FROM node:22-alpine AS builder
2
+
3
+ WORKDIR /app
4
+ COPY package*.json ./
5
+ RUN npm ci
6
+ COPY . .
7
+ RUN npm run build
8
+
9
+ FROM node:22-alpine
10
+ WORKDIR /app
11
+ RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001
12
+ COPY package*.json ./
13
+ RUN npm ci --omit=dev
14
+ COPY --from=builder /app/dist ./dist
15
+ RUN chown -R nodejs:nodejs /app
16
+ USER nodejs
17
+ EXPOSE 3000
18
+ CMD ["node", "dist/index.js"]
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "{{PROJECT_NAME}}",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "tsx watch src/index.ts",
8
+ "build": "tsc",
9
+ "start": "node dist/index.js",
10
+ "lint": "eslint .",
11
+ "typecheck": "tsc --noEmit",
12
+ "test": "vitest run",
13
+ "test:watch": "vitest",
14
+ "db:push": "prisma db push",
15
+ "db:studio": "prisma studio",
16
+ "db:generate": "prisma generate"
17
+ },
18
+ "dependencies": {
19
+ "hono": "^4.7.0",
20
+ "@hono/node-server": "^1.14.0",
21
+ "@prisma/client": "^6.6.0"
22
+ },
23
+ "devDependencies": {
24
+ "typescript": "^5.8.3",
25
+ "@types/node": "^22.14.0",
26
+ "tsx": "^4.19.0",
27
+ "eslint": "^9.25.0",
28
+ "prisma": "^6.6.0",
29
+ "vitest": "^3.1.1"
30
+ }
31
+ }
@@ -0,0 +1,32 @@
1
+ import { Hono } from 'hono';
2
+ import { cors } from 'hono/cors';
3
+ import { serve } from '@hono/node-server';
4
+ import { healthRoutes } from './routes/health.js';
5
+
6
+ const app = new Hono();
7
+
8
+ app.use('*', cors());
9
+
10
+ app.route('/', healthRoutes);
11
+
12
+ // Global error handler — never leak stack traces
13
+ app.onError((err, c) => {
14
+ console.error('Unhandled error:', err);
15
+ return c.json({ error: { code: 'INTERNAL_ERROR', message: 'An unexpected error occurred' } }, 500);
16
+ });
17
+
18
+ const PORT = Number(process.env.PORT) || 3000;
19
+
20
+ const server = serve({ fetch: app.fetch, port: PORT }, () => {
21
+ console.log(`{{PROJECT_NAME_PASCAL}} server listening on port ${PORT}`);
22
+ });
23
+
24
+ // Graceful shutdown
25
+ function shutdown() {
26
+ console.log('Shutting down gracefully...');
27
+ server.close(() => process.exit(0));
28
+ setTimeout(() => process.exit(1), 10_000);
29
+ }
30
+
31
+ process.on('SIGINT', shutdown);
32
+ process.on('SIGTERM', shutdown);
@@ -0,0 +1,27 @@
1
+ import { Hono } from 'hono';
2
+
3
+ export const healthRoutes = new Hono();
4
+
5
+ // Liveness probe — always returns ok if the process is running
6
+ healthRoutes.get('/health', (c) => {
7
+ return c.json({
8
+ status: 'ok',
9
+ timestamp: new Date().toISOString(),
10
+ uptime: process.uptime(),
11
+ service: '{{PROJECT_NAME}}',
12
+ });
13
+ });
14
+
15
+ // Readiness probe — add real dependency checks (database, cache, etc.)
16
+ // before using this as a Kubernetes readiness probe
17
+ healthRoutes.get('/healthz', async (c) => {
18
+ // TODO: Replace with actual checks, e.g.:
19
+ // const db = await prisma.$queryRaw`SELECT 1`;
20
+ const checks: Record<string, string> = {};
21
+
22
+ return c.json({
23
+ status: 'ok',
24
+ service: '{{PROJECT_NAME}}',
25
+ checks,
26
+ });
27
+ });
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "Node16",
5
+ "moduleResolution": "Node16",
6
+ "isolatedModules": true,
7
+ "outDir": "dist",
8
+ "rootDir": "src",
9
+ "strict": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "resolveJsonModule": true,
14
+ "declaration": true
15
+ },
16
+ "include": ["src"],
17
+ "exclude": ["node_modules", "dist"]
18
+ }