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.
- package/README.md +58 -10
- package/bin/chainproof.js +126 -0
- package/bin/devforge.js +2 -1
- package/package.json +33 -7
- package/src/chainproof-bridge.js +330 -0
- package/src/ci-mode.js +85 -0
- package/src/claude-configurator.js +87 -49
- package/src/cli.js +35 -12
- package/src/composer.js +159 -34
- package/src/doctor-checks-chainproof.js +106 -0
- package/src/doctor-checks.js +39 -20
- package/src/doctor-prompts.js +9 -9
- package/src/doctor.js +37 -4
- package/src/guided.js +3 -3
- package/src/index.js +31 -10
- package/src/init-mode.js +64 -11
- package/src/menu.js +178 -0
- package/src/prompts.js +5 -12
- package/src/recommender.js +134 -10
- package/src/scanner.js +57 -2
- package/src/uat-generator.js +204 -189
- package/src/update-check.js +9 -4
- package/src/update.js +1 -1
- package/src/utils.js +65 -6
- package/templates/ai/guardrails-py/backend/app/ai/__init__.py +29 -0
- package/templates/ai/guardrails-py/backend/app/ai/audit_log.py +133 -0
- package/templates/ai/guardrails-py/backend/app/ai/client.py.template +323 -0
- package/templates/ai/guardrails-py/backend/app/ai/health.py.template +157 -0
- package/templates/ai/guardrails-py/backend/app/ai/input_guard.py +98 -0
- package/templates/ai/guardrails-ts/src/lib/ai/audit-log.ts.template +164 -0
- package/templates/ai/guardrails-ts/src/lib/ai/client.ts.template +403 -0
- package/templates/ai/guardrails-ts/src/lib/ai/health.ts.template +165 -0
- package/templates/ai/guardrails-ts/src/lib/ai/index.ts.template +17 -0
- package/templates/ai/guardrails-ts/src/lib/ai/input-guard.ts.template +124 -0
- package/templates/auth/nextauth/src/lib/auth.ts.template +12 -7
- package/templates/backend/express/Dockerfile.template +18 -0
- package/templates/backend/express/package.json.template +33 -0
- package/templates/backend/express/src/index.ts.template +34 -0
- package/templates/backend/express/src/routes/health.ts.template +27 -0
- package/templates/backend/express/tsconfig.json +17 -0
- package/templates/backend/fastapi/backend/Dockerfile.template +5 -0
- package/templates/backend/fastapi/backend/app/api/health.py.template +1 -1
- package/templates/backend/fastapi/backend/app/core/config.py.template +1 -1
- package/templates/backend/fastapi/backend/app/core/errors.py +1 -1
- package/templates/backend/fastapi/backend/app/main.py.template +3 -1
- package/templates/backend/fastapi/backend/requirements.txt.template +2 -0
- package/templates/backend/hono/Dockerfile.template +18 -0
- package/templates/backend/hono/package.json.template +31 -0
- package/templates/backend/hono/src/index.ts.template +32 -0
- package/templates/backend/hono/src/routes/health.ts.template +27 -0
- package/templates/backend/hono/tsconfig.json +18 -0
- package/templates/base/docs/plans/.gitkeep +0 -0
- package/templates/base/docs/uat/UAT_CHECKLIST.csv.template +2 -0
- package/templates/base/docs/uat/UAT_TEMPLATE.md.template +22 -0
- package/templates/chainproof/base/.chainproof/config.json.template +11 -0
- package/templates/chainproof/base/.chainproof/mcp-server.mjs +310 -0
- package/templates/chainproof/base/.mcp.json +9 -0
- package/templates/chainproof/fastapi/.chainproof/middleware.json.template +14 -0
- package/templates/chainproof/nextjs/.chainproof/hooks.json.template +19 -0
- package/templates/chainproof/polyglot/.chainproof/config.json.template +21 -0
- package/templates/claude-code/agents/architect.md +25 -11
- package/templates/claude-code/agents/build-error-resolver.md +22 -7
- package/templates/claude-code/agents/chief-of-staff.md +42 -8
- package/templates/claude-code/agents/code-quality-reviewer.md +15 -1
- package/templates/claude-code/agents/database-reviewer.md +16 -2
- package/templates/claude-code/agents/deep-reviewer.md +191 -0
- package/templates/claude-code/agents/doc-updater.md +19 -5
- package/templates/claude-code/agents/docs-lookup.md +19 -5
- package/templates/claude-code/agents/e2e-runner.md +26 -12
- package/templates/claude-code/agents/enforcement-gate.md +102 -0
- package/templates/claude-code/agents/frontend-builder.md +188 -0
- package/templates/claude-code/agents/harness-optimizer.md +61 -0
- package/templates/claude-code/agents/loop-operator.md +27 -12
- package/templates/claude-code/agents/planner.md +21 -7
- package/templates/claude-code/agents/product-strategist.md +138 -0
- package/templates/claude-code/agents/production-readiness.md +14 -0
- package/templates/claude-code/agents/prompt-auditor.md +115 -0
- package/templates/claude-code/agents/refactor-cleaner.md +22 -8
- package/templates/claude-code/agents/security-reviewer.md +15 -0
- package/templates/claude-code/agents/spec-validator.md +45 -1
- package/templates/claude-code/agents/tdd-guide.md +21 -7
- package/templates/claude-code/agents/uat-validator.md +18 -0
- package/templates/claude-code/claude-md/base.md +15 -7
- package/templates/claude-code/claude-md/fastapi.md +8 -8
- package/templates/claude-code/claude-md/fullstack.md +6 -6
- package/templates/claude-code/claude-md/hono.md +18 -0
- package/templates/claude-code/claude-md/nextjs.md +5 -5
- package/templates/claude-code/claude-md/remix.md +18 -0
- package/templates/claude-code/commands/audit-security.md +14 -0
- package/templates/claude-code/commands/audit-spec.md +14 -0
- package/templates/claude-code/commands/audit-wiring.md +14 -0
- package/templates/claude-code/commands/build-fix.md +28 -0
- package/templates/claude-code/commands/build-ui.md +59 -0
- package/templates/claude-code/commands/code-review.md +54 -26
- package/templates/claude-code/commands/fix-loop.md +211 -0
- package/templates/claude-code/commands/full-audit.md +37 -8
- package/templates/claude-code/commands/generate-prd.md +1 -1
- package/templates/claude-code/commands/generate-sdd.md +74 -0
- package/templates/claude-code/commands/generate-uat.md +107 -35
- package/templates/claude-code/commands/help.md +68 -0
- package/templates/claude-code/commands/live-uat.md +268 -0
- package/templates/claude-code/commands/optimize-claude-md.md +15 -1
- package/templates/claude-code/commands/plan.md +3 -3
- package/templates/claude-code/commands/pre-pr.md +57 -19
- package/templates/claude-code/commands/product-strategist.md +21 -0
- package/templates/claude-code/commands/resume-session.md +10 -10
- package/templates/claude-code/commands/run-uat.md +59 -2
- package/templates/claude-code/commands/save-session.md +10 -10
- package/templates/claude-code/commands/simplify.md +36 -0
- package/templates/claude-code/commands/tdd.md +17 -18
- package/templates/claude-code/commands/verify-all.md +24 -0
- package/templates/claude-code/commands/verify-intent.md +55 -0
- package/templates/claude-code/commands/workflows.md +52 -37
- package/templates/claude-code/hooks/polyglot.json +10 -1
- package/templates/claude-code/hooks/python.json +10 -1
- package/templates/claude-code/hooks/scripts/autofix-polyglot.mjs +20 -10
- package/templates/claude-code/hooks/scripts/autofix-python.mjs +4 -5
- package/templates/claude-code/hooks/scripts/autofix-typescript.mjs +4 -4
- package/templates/claude-code/hooks/scripts/code-hygiene.mjs +293 -0
- package/templates/claude-code/hooks/scripts/guard-protected-files.mjs +2 -2
- package/templates/claude-code/hooks/scripts/pre-commit-gate.mjs +207 -0
- package/templates/claude-code/hooks/typescript.json +10 -1
- package/templates/claude-code/skills/ai-prompts/SKILL.md +119 -41
- package/templates/claude-code/skills/git-workflow/SKILL.md +6 -6
- package/templates/claude-code/skills/nextjs/SKILL.md +1 -1
- package/templates/claude-code/skills/playwright/SKILL.md +6 -5
- package/templates/claude-code/skills/security-api/SKILL.md +1 -1
- package/templates/claude-code/skills/security-web/SKILL.md +2 -1
- package/templates/claude-code/skills/testing-patterns/SKILL.md +9 -9
- package/templates/database/prisma-postgres/{.env.example → .env.example.template} +1 -0
- package/templates/database/sqlalchemy-postgres/{.env.example → .env.example.template} +1 -0
- package/templates/docs-portal/fastapi/backend/app/portal/__init__.py +0 -0
- package/templates/docs-portal/fastapi/backend/app/portal/__pycache__/docs_reader.cpython-314.pyc +0 -0
- package/templates/docs-portal/fastapi/backend/app/portal/docs_reader.py +201 -0
- package/templates/docs-portal/fastapi/backend/app/portal/html_renderer.py +229 -0
- package/templates/docs-portal/fastapi/backend/app/portal/router.py.template +35 -0
- package/templates/docs-portal/nextjs/src/app/portal/[category]/[slug]/page.tsx +81 -0
- package/templates/docs-portal/nextjs/src/app/portal/[category]/page.tsx +65 -0
- package/templates/docs-portal/nextjs/src/app/portal/layout.tsx.template +54 -0
- package/templates/docs-portal/nextjs/src/app/portal/page.tsx +85 -0
- package/templates/docs-portal/nextjs/src/components/portal/markdown-renderer.tsx +101 -0
- package/templates/docs-portal/nextjs/src/components/portal/mobile-portal-nav.tsx +81 -0
- package/templates/docs-portal/nextjs/src/components/portal/portal-nav.tsx +86 -0
- package/templates/docs-portal/nextjs/src/lib/docs.ts +139 -0
- package/templates/frontend/nextjs/package.json.template +3 -1
- package/templates/frontend/react/index.html.template +12 -0
- package/templates/frontend/react/package.json.template +34 -0
- package/templates/frontend/react/src/App.tsx.template +10 -0
- package/templates/frontend/react/src/index.css +1 -0
- package/templates/frontend/react/src/main.tsx +10 -0
- package/templates/frontend/react/tsconfig.json +17 -0
- package/templates/frontend/react/vite.config.ts.template +15 -0
- package/templates/frontend/react/vitest.config.ts +9 -0
- package/templates/frontend/remix/app/root.tsx.template +31 -0
- package/templates/frontend/remix/app/routes/_index.tsx.template +19 -0
- package/templates/frontend/remix/app/routes/api.health.ts.template +10 -0
- package/templates/frontend/remix/app/tailwind.css +1 -0
- package/templates/frontend/remix/package.json.template +39 -0
- package/templates/frontend/remix/tsconfig.json +18 -0
- package/templates/frontend/remix/vite.config.ts.template +7 -0
- package/templates/infra/github-actions/.github/workflows/ci.yml.template +52 -0
- package/templates/testing/pytest/backend/tests/__init__.py +0 -0
- package/templates/testing/pytest/backend/tests/conftest.py.template +11 -0
- package/templates/testing/pytest/backend/tests/test_health.py.template +10 -0
- package/templates/testing/vitest/vitest.config.ts.template +18 -0
- package/CLAUDE.md +0 -38
- 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
|
-
//
|
|
14
|
-
//
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
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"))
|
|
@@ -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
|
|
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(
|
|
@@ -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
|
+
}
|