thevoidforge 21.0.10 → 21.0.12

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 (108) hide show
  1. package/dist/.claude/commands/ai.md +69 -0
  2. package/dist/.claude/commands/architect.md +121 -0
  3. package/dist/.claude/commands/assemble.md +201 -0
  4. package/dist/.claude/commands/assess.md +75 -0
  5. package/dist/.claude/commands/blueprint.md +135 -0
  6. package/dist/.claude/commands/build.md +116 -0
  7. package/dist/.claude/commands/campaign.md +201 -0
  8. package/dist/.claude/commands/cultivation.md +166 -0
  9. package/dist/.claude/commands/current.md +128 -0
  10. package/dist/.claude/commands/dangerroom.md +74 -0
  11. package/dist/.claude/commands/debrief.md +178 -0
  12. package/dist/.claude/commands/deploy.md +99 -0
  13. package/dist/.claude/commands/devops.md +143 -0
  14. package/dist/.claude/commands/gauntlet.md +140 -0
  15. package/dist/.claude/commands/git.md +104 -0
  16. package/dist/.claude/commands/grow.md +146 -0
  17. package/dist/.claude/commands/imagine.md +126 -0
  18. package/dist/.claude/commands/portfolio.md +50 -0
  19. package/dist/.claude/commands/prd.md +113 -0
  20. package/dist/.claude/commands/qa.md +107 -0
  21. package/dist/.claude/commands/review.md +151 -0
  22. package/dist/.claude/commands/security.md +100 -0
  23. package/dist/.claude/commands/test.md +96 -0
  24. package/dist/.claude/commands/thumper.md +116 -0
  25. package/dist/.claude/commands/treasury.md +100 -0
  26. package/dist/.claude/commands/ux.md +118 -0
  27. package/dist/.claude/commands/vault.md +189 -0
  28. package/dist/.claude/commands/void.md +108 -0
  29. package/dist/CHANGELOG.md +1918 -0
  30. package/dist/CLAUDE.md +250 -0
  31. package/dist/HOLOCRON.md +856 -0
  32. package/dist/VERSION.md +123 -0
  33. package/dist/docs/NAMING_REGISTRY.md +478 -0
  34. package/dist/docs/methods/AI_INTELLIGENCE.md +276 -0
  35. package/dist/docs/methods/ASSEMBLER.md +142 -0
  36. package/dist/docs/methods/BACKEND_ENGINEER.md +165 -0
  37. package/dist/docs/methods/BUILD_JOURNAL.md +185 -0
  38. package/dist/docs/methods/BUILD_PROTOCOL.md +426 -0
  39. package/dist/docs/methods/CAMPAIGN.md +568 -0
  40. package/dist/docs/methods/CONTEXT_MANAGEMENT.md +189 -0
  41. package/dist/docs/methods/DEEP_CURRENT.md +184 -0
  42. package/dist/docs/methods/DEVOPS_ENGINEER.md +295 -0
  43. package/dist/docs/methods/FIELD_MEDIC.md +261 -0
  44. package/dist/docs/methods/FORGE_ARTIST.md +108 -0
  45. package/dist/docs/methods/FORGE_KEEPER.md +268 -0
  46. package/dist/docs/methods/GAUNTLET.md +344 -0
  47. package/dist/docs/methods/GROWTH_STRATEGIST.md +466 -0
  48. package/dist/docs/methods/HEARTBEAT.md +168 -0
  49. package/dist/docs/methods/MCP_INTEGRATION.md +139 -0
  50. package/dist/docs/methods/MUSTER.md +148 -0
  51. package/dist/docs/methods/PRD_GENERATOR.md +186 -0
  52. package/dist/docs/methods/PRODUCT_DESIGN_FRONTEND.md +250 -0
  53. package/dist/docs/methods/QA_ENGINEER.md +337 -0
  54. package/dist/docs/methods/RELEASE_MANAGER.md +145 -0
  55. package/dist/docs/methods/SECURITY_AUDITOR.md +320 -0
  56. package/dist/docs/methods/SUB_AGENTS.md +335 -0
  57. package/dist/docs/methods/SYSTEMS_ARCHITECT.md +171 -0
  58. package/dist/docs/methods/TESTING.md +359 -0
  59. package/dist/docs/methods/THUMPER.md +175 -0
  60. package/dist/docs/methods/TIME_VAULT.md +120 -0
  61. package/dist/docs/methods/TREASURY.md +184 -0
  62. package/dist/docs/methods/TROUBLESHOOTING.md +265 -0
  63. package/dist/docs/patterns/README.md +52 -0
  64. package/dist/docs/patterns/ad-billing-adapter.ts +537 -0
  65. package/dist/docs/patterns/ad-platform-adapter.ts +421 -0
  66. package/dist/docs/patterns/ai-classifier.ts +195 -0
  67. package/dist/docs/patterns/ai-eval.ts +272 -0
  68. package/dist/docs/patterns/ai-orchestrator.ts +341 -0
  69. package/dist/docs/patterns/ai-router.ts +194 -0
  70. package/dist/docs/patterns/ai-tool-schema.ts +237 -0
  71. package/dist/docs/patterns/api-route.ts +241 -0
  72. package/dist/docs/patterns/backtest-engine.ts +499 -0
  73. package/dist/docs/patterns/browser-review.ts +292 -0
  74. package/dist/docs/patterns/combobox.tsx +300 -0
  75. package/dist/docs/patterns/component.tsx +262 -0
  76. package/dist/docs/patterns/daemon-process.ts +338 -0
  77. package/dist/docs/patterns/data-pipeline.ts +297 -0
  78. package/dist/docs/patterns/database-migration.ts +466 -0
  79. package/dist/docs/patterns/e2e-test.ts +629 -0
  80. package/dist/docs/patterns/error-handling.ts +312 -0
  81. package/dist/docs/patterns/execution-safety.ts +601 -0
  82. package/dist/docs/patterns/financial-transaction.ts +342 -0
  83. package/dist/docs/patterns/funding-plan.ts +462 -0
  84. package/dist/docs/patterns/game-entity.ts +137 -0
  85. package/dist/docs/patterns/game-loop.ts +113 -0
  86. package/dist/docs/patterns/game-state.ts +143 -0
  87. package/dist/docs/patterns/job-queue.ts +225 -0
  88. package/dist/docs/patterns/kongo-integration.ts +164 -0
  89. package/dist/docs/patterns/middleware.ts +363 -0
  90. package/dist/docs/patterns/mobile-screen.tsx +139 -0
  91. package/dist/docs/patterns/mobile-service.ts +167 -0
  92. package/dist/docs/patterns/multi-tenant.ts +382 -0
  93. package/dist/docs/patterns/oauth-token-lifecycle.ts +223 -0
  94. package/dist/docs/patterns/outbound-rate-limiter.ts +260 -0
  95. package/dist/docs/patterns/prompt-template.ts +195 -0
  96. package/dist/docs/patterns/revenue-source-adapter.ts +311 -0
  97. package/dist/docs/patterns/service.ts +224 -0
  98. package/dist/docs/patterns/sse-endpoint.ts +118 -0
  99. package/dist/docs/patterns/stablecoin-adapter.ts +511 -0
  100. package/dist/docs/patterns/third-party-script.ts +68 -0
  101. package/dist/scripts/thumper/gom-jabbar.sh +241 -0
  102. package/dist/scripts/thumper/relay.sh +610 -0
  103. package/dist/scripts/thumper/scan.sh +359 -0
  104. package/dist/scripts/thumper/thumper.sh +190 -0
  105. package/dist/scripts/thumper/water-rings.sh +76 -0
  106. package/dist/scripts/voidforge.js +1 -1
  107. package/package.json +1 -1
  108. package/dist/tsconfig.tsbuildinfo +0 -1
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Pattern: AI Router
3
+ *
4
+ * Maps natural language input to typed intents and dispatches to handlers.
5
+ * Think of this as a programmable switch statement where AI does the matching.
6
+ *
7
+ * Key principles:
8
+ * - Intents are a closed set — define them upfront, not discovered at runtime
9
+ * - Ambiguity is an explicit state — route to clarification, not a random handler
10
+ * - Default fallback prevents unhandled inputs from failing silently
11
+ * - Emit metrics on every route decision for observability
12
+ * - Handler functions are pure business logic — no AI inside handlers
13
+ *
14
+ * Agents: Picard (routing architecture), Stark (handler dispatch), Batman (edge cases)
15
+ *
16
+ * Provider note: Uses Anthropic for intent classification. Swap classify() for any provider.
17
+ */
18
+
19
+ import Anthropic from '@anthropic-ai/sdk'
20
+ import { z } from 'zod'
21
+
22
+ // --- Intent definitions — closed set, exhaustive ---
23
+
24
+ export const INTENTS = [
25
+ 'check_order_status',
26
+ 'request_refund',
27
+ 'update_account',
28
+ 'product_question',
29
+ 'speak_to_human',
30
+ 'ambiguous',
31
+ ] as const
32
+
33
+ export type Intent = (typeof INTENTS)[number]
34
+
35
+ // --- Router types ---
36
+
37
+ interface RouteResult {
38
+ intent: Intent
39
+ confidence: number
40
+ params: Record<string, string> // Extracted entities (orderId, productName, etc.)
41
+ handlerResponse: unknown
42
+ }
43
+
44
+ interface RouterMetrics {
45
+ intent: Intent
46
+ confidence: number
47
+ latencyMs: number
48
+ routed: boolean // false if fell through to default
49
+ }
50
+
51
+ type MetricsEmitter = (metrics: RouterMetrics) => void
52
+
53
+ type IntentHandler = (
54
+ input: string,
55
+ params: Record<string, string>
56
+ ) => Promise<unknown>
57
+
58
+ // --- Intent classification ---
59
+
60
+ const IntentOutputSchema = z.object({
61
+ intent: z.enum(INTENTS),
62
+ confidence: z.number().min(0).max(1),
63
+ params: z.record(z.string()),
64
+ })
65
+
66
+ async function classifyIntent(
67
+ client: Anthropic,
68
+ input: string
69
+ ): Promise<z.infer<typeof IntentOutputSchema>> {
70
+ const response = await client.messages.create({
71
+ model: 'claude-sonnet-4-20250514',
72
+ max_tokens: 256,
73
+ system: [
74
+ 'You are an intent classifier. Classify the user message into exactly one intent.',
75
+ `Valid intents: ${INTENTS.join(', ')}`,
76
+ 'If the intent is unclear, use "ambiguous".',
77
+ 'Extract relevant parameters (orderId, productName, etc.) from the message.',
78
+ 'Respond with JSON: { "intent": "<intent>", "confidence": <0-1>, "params": {} }',
79
+ ].join('\n'),
80
+ messages: [{ role: 'user', content: input }],
81
+ })
82
+
83
+ const content = response.content[0]
84
+ if (content.type !== 'text') throw new Error('Expected text response')
85
+ return IntentOutputSchema.parse(JSON.parse(content.text))
86
+ }
87
+
88
+ // OpenAI adaptation:
89
+ // Use openai.chat.completions.create() with response_format: { type: 'json_object' }.
90
+ // Same schema parsing. For very high throughput, consider fine-tuned gpt-4o-mini.
91
+
92
+ // --- IntentRouter class ---
93
+
94
+ const AMBIGUITY_THRESHOLD = 0.50 // Below this, treat as ambiguous regardless of model label
95
+
96
+ export class IntentRouter {
97
+ private handlers = new Map<Intent, IntentHandler>()
98
+ private defaultHandler: IntentHandler
99
+ private emitMetrics: MetricsEmitter
100
+
101
+ constructor(
102
+ private client: Anthropic,
103
+ options: {
104
+ defaultHandler: IntentHandler
105
+ emitMetrics?: MetricsEmitter
106
+ }
107
+ ) {
108
+ this.defaultHandler = options.defaultHandler
109
+ this.emitMetrics = options.emitMetrics ?? (() => {}) // No-op if not provided
110
+ }
111
+
112
+ /** Register a handler for an intent. One handler per intent. */
113
+ on(intent: Intent, handler: IntentHandler): this {
114
+ this.handlers.set(intent, handler)
115
+ return this
116
+ }
117
+
118
+ /** Classify intent from natural language and dispatch to the registered handler. */
119
+ async routeRequest(input: string): Promise<RouteResult> {
120
+ const start = Date.now()
121
+ const classification = await classifyIntent(this.client, input)
122
+ const latencyMs = Date.now() - start
123
+
124
+ // Override to ambiguous if confidence is too low, regardless of model's label
125
+ const effectiveIntent: Intent =
126
+ classification.confidence < AMBIGUITY_THRESHOLD ? 'ambiguous' : classification.intent
127
+
128
+ const handler = this.handlers.get(effectiveIntent) ?? this.defaultHandler
129
+ const routed = this.handlers.has(effectiveIntent)
130
+
131
+ // Emit metrics for observability — track routing decisions, not content
132
+ this.emitMetrics({
133
+ intent: effectiveIntent,
134
+ confidence: classification.confidence,
135
+ latencyMs,
136
+ routed,
137
+ })
138
+
139
+ const handlerResponse = await handler(input, classification.params)
140
+
141
+ return {
142
+ intent: effectiveIntent,
143
+ confidence: classification.confidence,
144
+ params: classification.params,
145
+ handlerResponse,
146
+ }
147
+ }
148
+ }
149
+
150
+ // --- Usage example ---
151
+
152
+ // const router = new IntentRouter(anthropicClient, {
153
+ // defaultHandler: async (input) => ({
154
+ // message: "I'm not sure how to help with that. Let me connect you with someone.",
155
+ // }),
156
+ // emitMetrics: (m) => statsd.increment('ai.router.intent', { intent: m.intent }),
157
+ // })
158
+ //
159
+ // router
160
+ // .on('check_order_status', async (_input, params) => {
161
+ // return orderService.getStatus(params.orderId)
162
+ // })
163
+ // .on('request_refund', async (_input, params) => {
164
+ // return refundService.initiate(params.orderId)
165
+ // })
166
+ // .on('ambiguous', async (input) => {
167
+ // return { message: 'Could you clarify? I can help with orders, refunds, or account changes.' }
168
+ // })
169
+ //
170
+ // const result = await router.routeRequest("Where's my order #12345?")
171
+
172
+ /**
173
+ * Framework adaptations:
174
+ *
175
+ * Express:
176
+ * - Mount router as middleware: app.post('/api/chat', async (req, res) => {
177
+ * const result = await router.routeRequest(req.body.message)
178
+ * res.json(result)
179
+ * })
180
+ * - Metrics via StatsD/Prometheus middleware
181
+ * - Rate limit the classification endpoint (express-rate-limit)
182
+ *
183
+ * FastAPI:
184
+ * - IntentRouter as a dependency: router = Depends(get_intent_router)
185
+ * - Pydantic models for RouteResult and RouterMetrics
186
+ * - Metrics via prometheus-fastapi-instrumentator or custom middleware
187
+ * - Async handlers: all handlers are async def by default in Python
188
+ *
189
+ * Django:
190
+ * - IntentRouter instantiated in services.py, called from views
191
+ * - Store routing decisions in a RoutingLog model for analytics
192
+ * - Metrics via django-prometheus or statsd
193
+ * - For real-time: combine with SSE pattern (sse-endpoint.ts) via Django Channels
194
+ */
@@ -0,0 +1,237 @@
1
+ /**
2
+ * Pattern: AI Tool Schema
3
+ *
4
+ * Key principles:
5
+ * - Define tools once, convert to any provider format (Anthropic, OpenAI)
6
+ * - Zod schemas for parameter validation — model output is untrusted input
7
+ * - Tool execution has error boundaries — one bad tool call can't crash the agent
8
+ * - Registry pattern for managing available tools per context/user/role
9
+ * - Tool results are always strings — structured data is JSON-serialized
10
+ *
11
+ * Agents: Stark (backend), Picard (architecture), Kenobi (input validation)
12
+ *
13
+ * Provider note: defineTool() creates a provider-agnostic definition.
14
+ * toAnthropicFormat() and toOpenAIFormat() convert for each SDK.
15
+ */
16
+
17
+ import { z, ZodType, ZodObject, ZodRawShape } from 'zod'
18
+ import type Anthropic from '@anthropic-ai/sdk'
19
+
20
+ // --- Core types ---
21
+
22
+ /** Provider-agnostic tool definition. */
23
+ export interface ToolDefinition<TInput extends ZodRawShape = ZodRawShape> {
24
+ name: string
25
+ description: string
26
+ parameters: ZodObject<TInput>
27
+ execute: (input: z.infer<ZodObject<TInput>>) => Promise<string>
28
+ }
29
+
30
+ /** Result of a tool execution. */
31
+ export interface ToolResult {
32
+ toolName: string
33
+ success: boolean
34
+ output: string
35
+ durationMs: number
36
+ error?: string
37
+ }
38
+
39
+ // --- defineTool() — type-safe tool creation ---
40
+
41
+ /** Create a tool definition with Zod-validated parameters. */
42
+ export function defineTool<T extends ZodRawShape>(config: {
43
+ name: string
44
+ description: string
45
+ parameters: ZodObject<T>
46
+ execute: (input: z.infer<ZodObject<T>>) => Promise<string>
47
+ }): ToolDefinition<T> {
48
+ return config
49
+ }
50
+
51
+ // Example tool definitions:
52
+ //
53
+ // const getWeatherTool = defineTool({
54
+ // name: 'get_weather',
55
+ // description: 'Get current weather for a city',
56
+ // parameters: z.object({
57
+ // city: z.string().describe('City name'),
58
+ // units: z.enum(['celsius', 'fahrenheit']).default('celsius').describe('Temperature units'),
59
+ // }),
60
+ // execute: async (input) => {
61
+ // const weather = await weatherService.getCurrent(input.city, input.units)
62
+ // return JSON.stringify(weather) // Always return string
63
+ // },
64
+ // })
65
+
66
+ // --- Tool execution with error boundary ---
67
+
68
+ /** Execute a tool safely. Validates input, catches errors, measures duration. */
69
+ export async function executeTool<T extends ZodRawShape>(
70
+ tool: ToolDefinition<T>,
71
+ rawInput: unknown
72
+ ): Promise<ToolResult> {
73
+ const start = Date.now()
74
+
75
+ // 1. Validate input with Zod — model output is untrusted
76
+ const parseResult = tool.parameters.safeParse(rawInput)
77
+ if (!parseResult.success) {
78
+ return {
79
+ toolName: tool.name,
80
+ success: false,
81
+ output: '',
82
+ durationMs: Date.now() - start,
83
+ error: `Invalid parameters: ${parseResult.error.issues.map((i) => i.message).join(', ')}`,
84
+ }
85
+ }
86
+
87
+ // 2. Execute with error boundary
88
+ try {
89
+ const output = await tool.execute(parseResult.data)
90
+ return {
91
+ toolName: tool.name,
92
+ success: true,
93
+ output,
94
+ durationMs: Date.now() - start,
95
+ }
96
+ } catch (error) {
97
+ return {
98
+ toolName: tool.name,
99
+ success: false,
100
+ output: '',
101
+ durationMs: Date.now() - start,
102
+ error: error instanceof Error ? error.message : 'Tool execution failed',
103
+ }
104
+ }
105
+ }
106
+
107
+ // --- ToolRegistry — manage available tools ---
108
+
109
+ export class ToolRegistry {
110
+ private tools = new Map<string, ToolDefinition>()
111
+
112
+ /** Register a tool. Throws if name already taken. */
113
+ register(tool: ToolDefinition): this {
114
+ if (this.tools.has(tool.name)) {
115
+ throw new Error(`Tool already registered: ${tool.name}`)
116
+ }
117
+ this.tools.set(tool.name, tool)
118
+ return this
119
+ }
120
+
121
+ /** Get a tool by name. Returns undefined if not found. */
122
+ get(name: string): ToolDefinition | undefined {
123
+ return this.tools.get(name)
124
+ }
125
+
126
+ /** Execute a tool by name with raw input from the model. */
127
+ async execute(name: string, rawInput: unknown): Promise<ToolResult> {
128
+ const tool = this.tools.get(name)
129
+ if (!tool) {
130
+ return {
131
+ toolName: name,
132
+ success: false,
133
+ output: '',
134
+ durationMs: 0,
135
+ error: `Unknown tool: ${name}`,
136
+ }
137
+ }
138
+ return executeTool(tool, rawInput)
139
+ }
140
+
141
+ /** Convert all registered tools to Anthropic format. */
142
+ toAnthropicFormat(): Anthropic.Tool[] {
143
+ return Array.from(this.tools.values()).map((tool) => ({
144
+ name: tool.name,
145
+ description: tool.description,
146
+ input_schema: zodToJsonSchema(tool.parameters),
147
+ }))
148
+ }
149
+
150
+ /** Convert all registered tools to OpenAI format. */
151
+ toOpenAIFormat(): Array<{
152
+ type: 'function'
153
+ function: { name: string; description: string; parameters: Record<string, unknown> }
154
+ }> {
155
+ return Array.from(this.tools.values()).map((tool) => ({
156
+ type: 'function' as const,
157
+ function: {
158
+ name: tool.name,
159
+ description: tool.description,
160
+ parameters: zodToJsonSchema(tool.parameters),
161
+ },
162
+ }))
163
+ }
164
+
165
+ /** List all registered tool names. */
166
+ listNames(): string[] {
167
+ return Array.from(this.tools.keys())
168
+ }
169
+ }
170
+
171
+ // --- Zod to JSON Schema conversion (simplified) ---
172
+ // In production, use zod-to-json-schema package. This is a minimal reference.
173
+
174
+ function zodToJsonSchema(schema: ZodType): Record<string, unknown> {
175
+ // Use zod-to-json-schema in production:
176
+ // import { zodToJsonSchema } from 'zod-to-json-schema'
177
+ // return zodToJsonSchema(schema)
178
+ //
179
+ // Simplified version for the pattern reference:
180
+ if (schema instanceof ZodObject) {
181
+ const shape = schema.shape
182
+ const properties: Record<string, unknown> = {}
183
+ const required: string[] = []
184
+
185
+ for (const [key, value] of Object.entries(shape)) {
186
+ const zodField = value as ZodType
187
+ properties[key] = { type: 'string', description: zodField.description ?? '' }
188
+ if (!zodField.isOptional()) required.push(key)
189
+ }
190
+
191
+ return { type: 'object', properties, required }
192
+ }
193
+ return { type: 'object', properties: {} }
194
+ }
195
+
196
+ // --- Usage example ---
197
+
198
+ // const registry = new ToolRegistry()
199
+ // .register(getWeatherTool)
200
+ // .register(lookupOrderTool)
201
+ // .register(createTicketTool)
202
+ //
203
+ // // Pass to Anthropic agent loop (see ai-orchestrator.ts):
204
+ // const response = await client.messages.create({
205
+ // model: 'claude-sonnet-4-20250514',
206
+ // tools: registry.toAnthropicFormat(),
207
+ // messages: [{ role: 'user', content: 'What is the weather in Tokyo?' }],
208
+ // })
209
+ //
210
+ // // When model returns tool_use, execute via registry:
211
+ // for (const block of response.content) {
212
+ // if (block.type === 'tool_use') {
213
+ // const result = await registry.execute(block.name, block.input)
214
+ // // Feed result back to model as tool_result
215
+ // }
216
+ // }
217
+
218
+ /**
219
+ * Framework adaptations:
220
+ *
221
+ * Express:
222
+ * - ToolRegistry as singleton, initialized at startup
223
+ * - Register tools from service layer — tools call services, not DB directly
224
+ * - Admin endpoint to list available tools: GET /api/admin/tools
225
+ *
226
+ * FastAPI:
227
+ * - defineTool equivalent: dataclass with Pydantic model for parameters
228
+ * - ToolRegistry as dependency: registry = Depends(get_tool_registry)
229
+ * - Pydantic replaces Zod for parameter validation
230
+ * - Same provider adapter pattern: to_anthropic_format(), to_openai_format()
231
+ *
232
+ * Django:
233
+ * - Tools defined in tools.py, registered in apps.py ready() method
234
+ * - Tool permissions tied to Django auth: tool.requires_perm('orders.view')
235
+ * - ToolResult logged to ToolExecutionLog model for audit
236
+ * - Admin interface to enable/disable tools per tenant (multi-tenant.ts)
237
+ */
@@ -0,0 +1,241 @@
1
+ /**
2
+ * Pattern: API Route Handler
3
+ *
4
+ * Key principles:
5
+ * - Validate input at the boundary (Zod schema)
6
+ * - Auth check before business logic
7
+ * - Business logic in service layer, not the route
8
+ * - Consistent response shape
9
+ * - Structured error handling — never leak internals
10
+ * - Ownership verification for user-scoped resources
11
+ *
12
+ * Agents: Rogers (API design), Barton (error handling), Kenobi (security)
13
+ *
14
+ * Framework adaptations:
15
+ * Next.js: This file (NextRequest/NextResponse, App Router)
16
+ * Express: router.post('/api/projects', validate(schema), auth, async (req, res, next) => { ... })
17
+ * Django: DRF ViewSet with serializer validation, permission_classes, service call
18
+ * Rails: Controller action with strong_params, before_action :authenticate, service call
19
+ *
20
+ * === Django Deep Dive (DRF) ===
21
+ *
22
+ * from rest_framework import viewsets, serializers, permissions, status
23
+ * from rest_framework.response import Response
24
+ * from .services import ProjectService
25
+ *
26
+ * class ProjectSerializer(serializers.Serializer):
27
+ * name = serializers.CharField(max_length=200)
28
+ * description = serializers.CharField(required=False, allow_blank=True)
29
+ *
30
+ * class ProjectViewSet(viewsets.ViewSet):
31
+ * permission_classes = [permissions.IsAuthenticated]
32
+ *
33
+ * def create(self, request):
34
+ * serializer = ProjectSerializer(data=request.data)
35
+ * serializer.is_valid(raise_exception=True) # DRF handles 400 automatically
36
+ * project = ProjectService.create(
37
+ * user=request.user,
38
+ * **serializer.validated_data
39
+ * )
40
+ * return Response(ProjectSerializer(project).data, status=status.HTTP_201_CREATED)
41
+ *
42
+ * # Key differences from TypeScript pattern:
43
+ * # - Validation: DRF serializers replace Zod schemas
44
+ * # - Auth: permission_classes replaces getSession() — declarative, not imperative
45
+ * # - Ownership: service receives request.user, enforces ownership internally
46
+ * # - Errors: DRF exception handler maps service exceptions to HTTP responses
47
+ * # - SELECT control: Use serializer fields to control response shape, not Prisma select
48
+ * # For mutations, always SELECT only the fields you need (serializer.fields controls this)
49
+ *
50
+ * === FastAPI Deep Dive ===
51
+ *
52
+ * from fastapi import APIRouter, Depends, HTTPException
53
+ * from pydantic import BaseModel
54
+ * from .services import ProjectService
55
+ * from .auth import get_current_user, User
56
+ *
57
+ * router = APIRouter(prefix="/api/projects")
58
+ *
59
+ * class CreateProjectRequest(BaseModel):
60
+ * name: str
61
+ * description: str | None = None
62
+ *
63
+ * @router.post("/", status_code=201)
64
+ * async def create_project(
65
+ * body: CreateProjectRequest,
66
+ * user: User = Depends(get_current_user),
67
+ * ):
68
+ * project = await ProjectService.create(user=user, **body.model_dump())
69
+ * return project
70
+ *
71
+ * # Key differences from TypeScript pattern:
72
+ * # - Validation: Pydantic models replace Zod schemas (validated before handler runs)
73
+ * # - Auth: Depends(get_current_user) replaces getSession() — dependency injection
74
+ * # - Ownership: service receives user, enforces ownership internally
75
+ * # - Errors: HTTPException or custom exception handlers map to HTTP responses
76
+ * # - Fire-and-forget: Use BackgroundTasks for sendBeacon-style endpoints
77
+ *
78
+ * See /docs/patterns/error-handling.ts for the canonical error strategy.
79
+ */
80
+
81
+ import { NextRequest, NextResponse } from 'next/server'
82
+ import { z } from 'zod'
83
+ import { getSession } from '@/lib/auth'
84
+ import { projectService } from '@/lib/services/projects'
85
+ import { ApiError, handleApiError } from '@/lib/errors'
86
+
87
+ // --- Input validation at the boundary ---
88
+ const createProjectSchema = z.object({
89
+ name: z.string().min(1).max(100).trim(),
90
+ description: z.string().max(500).optional(),
91
+ })
92
+
93
+ // --- POST /api/projects ---
94
+ export async function POST(req: NextRequest) {
95
+ try {
96
+ // 1. Authenticate
97
+ const session = await getSession(req)
98
+ if (!session) {
99
+ return NextResponse.json(
100
+ { error: { code: 'UNAUTHORIZED', message: 'Authentication required' } },
101
+ { status: 401 }
102
+ )
103
+ }
104
+
105
+ // 2. Validate input
106
+ const body = await req.json()
107
+ const parsed = createProjectSchema.safeParse(body)
108
+ if (!parsed.success) {
109
+ return NextResponse.json(
110
+ {
111
+ error: {
112
+ code: 'VALIDATION_ERROR',
113
+ message: 'Invalid input',
114
+ details: parsed.error.flatten().fieldErrors,
115
+ },
116
+ },
117
+ { status: 400 }
118
+ )
119
+ }
120
+
121
+ // 3. Business logic in service layer
122
+ const project = await projectService.create({
123
+ ...parsed.data,
124
+ ownerId: session.userId,
125
+ })
126
+
127
+ // 4. Consistent success response
128
+ return NextResponse.json({ project }, { status: 201 })
129
+ } catch (error) {
130
+ return handleApiError(error)
131
+ }
132
+ }
133
+
134
+ // --- GET /api/projects ---
135
+ export async function GET(req: NextRequest) {
136
+ try {
137
+ const session = await getSession(req)
138
+ if (!session) {
139
+ return NextResponse.json(
140
+ { error: { code: 'UNAUTHORIZED', message: 'Authentication required' } },
141
+ { status: 401 }
142
+ )
143
+ }
144
+
145
+ const { searchParams } = new URL(req.url)
146
+ const page = Math.max(1, Number(searchParams.get('page')) || 1)
147
+ const limit = Math.min(50, Math.max(1, Number(searchParams.get('limit')) || 20))
148
+
149
+ // Service handles pagination, filtering, ownership scoping
150
+ const result = await projectService.list({
151
+ ownerId: session.userId,
152
+ page,
153
+ limit,
154
+ })
155
+
156
+ return NextResponse.json({
157
+ projects: result.items,
158
+ pagination: {
159
+ page: result.page,
160
+ limit: result.limit,
161
+ total: result.total,
162
+ hasMore: result.hasMore,
163
+ },
164
+ })
165
+ } catch (error) {
166
+ return handleApiError(error)
167
+ }
168
+ }
169
+
170
+ // --- Prisma Select on Mutations ---
171
+ // RULE: Always use `select` on Prisma create/update responses. Never return
172
+ // raw Prisma results from mutations — they include ALL columns by default,
173
+ // silently leaking sensitive fields (phoneHash, email, tokens, internal IDs).
174
+ //
175
+ // BAD: const user = await db.user.update({ where: { id }, data: { name } })
176
+ // return NextResponse.json({ user }) // leaks phoneHash, email, etc.
177
+ //
178
+ // GOOD: const user = await db.user.update({
179
+ // where: { id }, data: { name },
180
+ // select: { id: true, name: true, avatar: true }
181
+ // })
182
+ // return NextResponse.json({ user }) // only selected fields
183
+ //
184
+ // This applies to create(), update(), upsert(), and any mutation that returns data.
185
+ // findMany/findUnique already use select in the service pattern — mutations must too.
186
+ // (Field report #36: Prisma update() without select leaked phoneHash and whatsappId.)
187
+
188
+ // --- Fire-and-Forget Endpoints (sendBeacon) ---
189
+ // navigator.sendBeacon() cannot set custom headers — it sends a plain POST with
190
+ // Content-Type: text/plain. This makes it incompatible with header-based CSRF
191
+ // enforcement (X-CSRF-Token). For analytics/telemetry endpoints that use sendBeacon:
192
+ //
193
+ // 1. Exempt the endpoint from CSRF header checks
194
+ // 2. Add compensating controls: per-IP rate limiting, session cookie validation,
195
+ // origin check via Referer/Origin header (browsers always send these on sendBeacon)
196
+ // 3. Ensure the endpoint only WRITES (append-only) — never reads or mutates user data
197
+ // 4. Log the exemption in the security audit
198
+ //
199
+ // Pattern:
200
+ // POST /api/analytics/event — exempt from X-CSRF-Token
201
+ // Validation: session cookie + origin header + rate limit (100/min/IP)
202
+ // Data: append-only event log, no user data returned in response
203
+ //
204
+ // (Field report #36: analytics endpoint failed silently because sendBeacon
205
+ // couldn't attach the CSRF header.)
206
+
207
+ // ── Anti-pattern: Raw Prisma spread ──────────────────
208
+ // NEVER spread raw ORM/Prisma objects into API responses:
209
+ // ❌ res.json({ ...record }) — leaks internal fields (userId, rawMessageId, FK columns)
210
+ // ❌ res.json({ ...record, extra: 1 }) — still leaks everything from the spread
211
+ // ✅ res.json({ id: record.id, name: record.name, context: record.context })
212
+ //
213
+ // Prisma `include` without `select` returns ALL columns.
214
+ // Spreading passes them to the client, exposing internal schema.
215
+ // Always whitelist fields explicitly.
216
+ // (Field report #77: Dialog Travel leaked 12 internal fields via ...rec spread)
217
+
218
+ // --- Consistent error handler (shared across all routes) ---
219
+ // Located in /lib/errors.ts — shown here for reference
220
+ /*
221
+ export function handleApiError(error: unknown) {
222
+ if (error instanceof ApiError) {
223
+ return NextResponse.json(
224
+ { error: { code: error.code, message: error.message } },
225
+ { status: error.status }
226
+ )
227
+ }
228
+
229
+ // Log unexpected errors with context, never expose to client
230
+ console.error('[API Error]', {
231
+ message: error instanceof Error ? error.message : 'Unknown error',
232
+ stack: error instanceof Error ? error.stack : undefined,
233
+ timestamp: new Date().toISOString(),
234
+ })
235
+
236
+ return NextResponse.json(
237
+ { error: { code: 'INTERNAL_ERROR', message: 'Something went wrong' } },
238
+ { status: 500 }
239
+ )
240
+ }
241
+ */