@levironexe/architect 0.1.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 (210) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/CONTRIBUTING.md +55 -0
  3. package/README.md +341 -0
  4. package/dist/analyzers/ast-parser.d.ts +3 -0
  5. package/dist/analyzers/ast-parser.js +305 -0
  6. package/dist/analyzers/ast-parser.js.map +1 -0
  7. package/dist/analyzers/dependency-graph.d.ts +2 -0
  8. package/dist/analyzers/dependency-graph.js +67 -0
  9. package/dist/analyzers/dependency-graph.js.map +1 -0
  10. package/dist/analyzers/duplication.d.ts +2 -0
  11. package/dist/analyzers/duplication.js +56 -0
  12. package/dist/analyzers/duplication.js.map +1 -0
  13. package/dist/analyzers/file-walker.d.ts +3 -0
  14. package/dist/analyzers/file-walker.js +80 -0
  15. package/dist/analyzers/file-walker.js.map +1 -0
  16. package/dist/cli/context-runner.d.ts +1 -0
  17. package/dist/cli/context-runner.js +16 -0
  18. package/dist/cli/context-runner.js.map +1 -0
  19. package/dist/cli/index.d.ts +24 -0
  20. package/dist/cli/index.js +217 -0
  21. package/dist/cli/index.js.map +1 -0
  22. package/dist/cli/init-runner.d.ts +25 -0
  23. package/dist/cli/init-runner.js +152 -0
  24. package/dist/cli/init-runner.js.map +1 -0
  25. package/dist/cli/scan-runner.d.ts +8 -0
  26. package/dist/cli/scan-runner.js +133 -0
  27. package/dist/cli/scan-runner.js.map +1 -0
  28. package/dist/formatters/plan-json.d.ts +2 -0
  29. package/dist/formatters/plan-json.js +4 -0
  30. package/dist/formatters/plan-json.js.map +1 -0
  31. package/dist/formatters/plan-markdown.d.ts +2 -0
  32. package/dist/formatters/plan-markdown.js +42 -0
  33. package/dist/formatters/plan-markdown.js.map +1 -0
  34. package/dist/formatters/plan-prompt.d.ts +4 -0
  35. package/dist/formatters/plan-prompt.js +5 -0
  36. package/dist/formatters/plan-prompt.js.map +1 -0
  37. package/dist/formatters/plan-terminal.d.ts +5 -0
  38. package/dist/formatters/plan-terminal.js +62 -0
  39. package/dist/formatters/plan-terminal.js.map +1 -0
  40. package/dist/generators/blueprint-renderer.d.ts +3 -0
  41. package/dist/generators/blueprint-renderer.js +27 -0
  42. package/dist/generators/blueprint-renderer.js.map +1 -0
  43. package/dist/generators/claudeWriter.d.ts +3 -0
  44. package/dist/generators/claudeWriter.js +9 -0
  45. package/dist/generators/claudeWriter.js.map +1 -0
  46. package/dist/generators/copilotWriter.d.ts +3 -0
  47. package/dist/generators/copilotWriter.js +11 -0
  48. package/dist/generators/copilotWriter.js.map +1 -0
  49. package/dist/generators/cursorWriter.d.ts +3 -0
  50. package/dist/generators/cursorWriter.js +14 -0
  51. package/dist/generators/cursorWriter.js.map +1 -0
  52. package/dist/generators/genericWriter.d.ts +3 -0
  53. package/dist/generators/genericWriter.js +9 -0
  54. package/dist/generators/genericWriter.js.map +1 -0
  55. package/dist/generators/template-context.d.ts +18 -0
  56. package/dist/generators/template-context.js +126 -0
  57. package/dist/generators/template-context.js.map +1 -0
  58. package/dist/generators/templateRenderer.d.ts +2 -0
  59. package/dist/generators/templateRenderer.js +19 -0
  60. package/dist/generators/templateRenderer.js.map +1 -0
  61. package/dist/generators/windsurfWriter.d.ts +3 -0
  62. package/dist/generators/windsurfWriter.js +14 -0
  63. package/dist/generators/windsurfWriter.js.map +1 -0
  64. package/dist/generators/writer-types.d.ts +11 -0
  65. package/dist/generators/writer-types.js +40 -0
  66. package/dist/generators/writer-types.js.map +1 -0
  67. package/dist/llm/claude-provider.d.ts +8 -0
  68. package/dist/llm/claude-provider.js +22 -0
  69. package/dist/llm/claude-provider.js.map +1 -0
  70. package/dist/llm/concern-classifier.d.ts +15 -0
  71. package/dist/llm/concern-classifier.js +61 -0
  72. package/dist/llm/concern-classifier.js.map +1 -0
  73. package/dist/llm/config.d.ts +11 -0
  74. package/dist/llm/config.js +120 -0
  75. package/dist/llm/config.js.map +1 -0
  76. package/dist/llm/ollama-provider.d.ts +8 -0
  77. package/dist/llm/ollama-provider.js +27 -0
  78. package/dist/llm/ollama-provider.js.map +1 -0
  79. package/dist/llm/openai-provider.d.ts +8 -0
  80. package/dist/llm/openai-provider.js +19 -0
  81. package/dist/llm/openai-provider.js.map +1 -0
  82. package/dist/llm/prompt-builder.d.ts +12 -0
  83. package/dist/llm/prompt-builder.js +132 -0
  84. package/dist/llm/prompt-builder.js.map +1 -0
  85. package/dist/llm/provider.d.ts +17 -0
  86. package/dist/llm/provider.js +2 -0
  87. package/dist/llm/provider.js.map +1 -0
  88. package/dist/llm/response-parser.d.ts +6 -0
  89. package/dist/llm/response-parser.js +128 -0
  90. package/dist/llm/response-parser.js.map +1 -0
  91. package/dist/planner/plan-generator.d.ts +7 -0
  92. package/dist/planner/plan-generator.js +275 -0
  93. package/dist/planner/plan-generator.js.map +1 -0
  94. package/dist/planner/plan-prompt-builder.d.ts +9 -0
  95. package/dist/planner/plan-prompt-builder.js +92 -0
  96. package/dist/planner/plan-prompt-builder.js.map +1 -0
  97. package/dist/planner/plan-response-parser.d.ts +7 -0
  98. package/dist/planner/plan-response-parser.js +21 -0
  99. package/dist/planner/plan-response-parser.js.map +1 -0
  100. package/dist/planner/plan-validator.d.ts +3 -0
  101. package/dist/planner/plan-validator.js +49 -0
  102. package/dist/planner/plan-validator.js.map +1 -0
  103. package/dist/reporters/scan-json.d.ts +13 -0
  104. package/dist/reporters/scan-json.js +26 -0
  105. package/dist/reporters/scan-json.js.map +1 -0
  106. package/dist/reporters/terminal.d.ts +6 -0
  107. package/dist/reporters/terminal.js +224 -0
  108. package/dist/reporters/terminal.js.map +1 -0
  109. package/dist/scoring/consistency-score.d.ts +3 -0
  110. package/dist/scoring/consistency-score.js +23 -0
  111. package/dist/scoring/consistency-score.js.map +1 -0
  112. package/dist/scoring/duplication-score.d.ts +3 -0
  113. package/dist/scoring/duplication-score.js +16 -0
  114. package/dist/scoring/duplication-score.js.map +1 -0
  115. package/dist/scoring/health-score.d.ts +4 -0
  116. package/dist/scoring/health-score.js +20 -0
  117. package/dist/scoring/health-score.js.map +1 -0
  118. package/dist/scoring/issue-builder.d.ts +4 -0
  119. package/dist/scoring/issue-builder.js +62 -0
  120. package/dist/scoring/issue-builder.js.map +1 -0
  121. package/dist/scoring/modularity-score.d.ts +3 -0
  122. package/dist/scoring/modularity-score.js +56 -0
  123. package/dist/scoring/modularity-score.js.map +1 -0
  124. package/dist/scoring/pattern-analysis.d.ts +3 -0
  125. package/dist/scoring/pattern-analysis.js +74 -0
  126. package/dist/scoring/pattern-analysis.js.map +1 -0
  127. package/dist/scoring/separation-score.d.ts +3 -0
  128. package/dist/scoring/separation-score.js +35 -0
  129. package/dist/scoring/separation-score.js.map +1 -0
  130. package/dist/skills/detector.d.ts +4 -0
  131. package/dist/skills/detector.js +104 -0
  132. package/dist/skills/detector.js.map +1 -0
  133. package/dist/skills/lister.d.ts +9 -0
  134. package/dist/skills/lister.js +35 -0
  135. package/dist/skills/lister.js.map +1 -0
  136. package/dist/skills/loader.d.ts +6 -0
  137. package/dist/skills/loader.js +76 -0
  138. package/dist/skills/loader.js.map +1 -0
  139. package/dist/skills/structure-check.d.ts +2 -0
  140. package/dist/skills/structure-check.js +37 -0
  141. package/dist/skills/structure-check.js.map +1 -0
  142. package/dist/skills/validator.d.ts +6 -0
  143. package/dist/skills/validator.js +229 -0
  144. package/dist/skills/validator.js.map +1 -0
  145. package/dist/types/analysis.d.ts +130 -0
  146. package/dist/types/analysis.js +41 -0
  147. package/dist/types/analysis.js.map +1 -0
  148. package/dist/types/concern.d.ts +48 -0
  149. package/dist/types/concern.js +16 -0
  150. package/dist/types/concern.js.map +1 -0
  151. package/dist/types/generation.d.ts +32 -0
  152. package/dist/types/generation.js +2 -0
  153. package/dist/types/generation.js.map +1 -0
  154. package/dist/types/issue.d.ts +12 -0
  155. package/dist/types/issue.js +2 -0
  156. package/dist/types/issue.js.map +1 -0
  157. package/dist/types/pattern.d.ts +15 -0
  158. package/dist/types/pattern.js +2 -0
  159. package/dist/types/pattern.js.map +1 -0
  160. package/dist/types/plan.d.ts +56 -0
  161. package/dist/types/plan.js +2 -0
  162. package/dist/types/plan.js.map +1 -0
  163. package/dist/types/scan-output.d.ts +84 -0
  164. package/dist/types/scan-output.js +2 -0
  165. package/dist/types/scan-output.js.map +1 -0
  166. package/dist/types/scoring.d.ts +15 -0
  167. package/dist/types/scoring.js +2 -0
  168. package/dist/types/scoring.js.map +1 -0
  169. package/dist/types/skill.d.ts +97 -0
  170. package/dist/types/skill.js +2 -0
  171. package/dist/types/skill.js.map +1 -0
  172. package/dist/utils/agent-detector.d.ts +2 -0
  173. package/dist/utils/agent-detector.js +22 -0
  174. package/dist/utils/agent-detector.js.map +1 -0
  175. package/dist/utils/interactive.d.ts +6 -0
  176. package/dist/utils/interactive.js +15 -0
  177. package/dist/utils/interactive.js.map +1 -0
  178. package/dist/utils/path.d.ts +5 -0
  179. package/dist/utils/path.js +31 -0
  180. package/dist/utils/path.js.map +1 -0
  181. package/dist/utils/progress.d.ts +17 -0
  182. package/dist/utils/progress.js +48 -0
  183. package/dist/utils/progress.js.map +1 -0
  184. package/dist/utils/thresholds.d.ts +6 -0
  185. package/dist/utils/thresholds.js +48 -0
  186. package/dist/utils/thresholds.js.map +1 -0
  187. package/package.json +63 -0
  188. package/skills/meta/general-js.skill.yaml +131 -0
  189. package/skills/patterns/clerk-auth.skill.yaml +349 -0
  190. package/skills/patterns/docker-deploy.skill.yaml +214 -0
  191. package/skills/patterns/drizzle.skill.yaml +277 -0
  192. package/skills/patterns/mongoose.skill.yaml +290 -0
  193. package/skills/patterns/nextauth.skill.yaml +308 -0
  194. package/skills/patterns/playwright-e2e.skill.yaml +265 -0
  195. package/skills/patterns/prisma.skill.yaml +255 -0
  196. package/skills/patterns/s3-storage.skill.yaml +235 -0
  197. package/skills/patterns/selenium-e2e.skill.yaml +276 -0
  198. package/skills/patterns/supabase-auth.skill.yaml +298 -0
  199. package/skills/patterns/supabase.skill.yaml +304 -0
  200. package/skills/patterns/vercel-deploy.skill.yaml +219 -0
  201. package/skills/patterns/vitest-testing.skill.yaml +262 -0
  202. package/skills/stacks/express-api.skill.yaml +155 -0
  203. package/skills/stacks/fastify-api.skill.yaml +119 -0
  204. package/skills/stacks/hono-api.skill.yaml +130 -0
  205. package/skills/stacks/nestjs.skill.yaml +135 -0
  206. package/skills/stacks/nextjs-app-router.skill.yaml +176 -0
  207. package/skills/stacks/react-spa.skill.yaml +153 -0
  208. package/skills/stacks/vue-nuxt.skill.yaml +115 -0
  209. package/templates/architect-plan.md +139 -0
  210. package/templates/architect-refactor.md +119 -0
@@ -0,0 +1,255 @@
1
+ schema_version: "2.0.0"
2
+ id: prisma
3
+ name: "Prisma ORM"
4
+ version: "2.0.0"
5
+ description: "Schema-first Prisma ORM with singleton client, repository/model pattern, migration-only schema changes, error handling for known request errors, and N+1 prevention via include/select."
6
+ category: pattern
7
+ language: javascript
8
+ frameworks:
9
+ - prisma
10
+ dependencies:
11
+ none:
12
+ - drizzle-orm
13
+ - mongoose
14
+ - sequelize
15
+ detection:
16
+ dependencies:
17
+ any:
18
+ - prisma
19
+ - "@prisma/client"
20
+ source_indicators:
21
+ - "PrismaClient"
22
+ - "prisma.$"
23
+ - "from '@prisma/client'"
24
+ - "prisma.user."
25
+ - "prisma.post."
26
+ structure:
27
+ required_dirs:
28
+ - path: prisma
29
+ purpose: "Prisma schema file (schema.prisma) and migration history. The only place where the database schema is defined — all table structure, relations, and enums live here. Any schema change must go through `npx prisma migrate dev` to generate a migration file and update the generated Prisma Client."
30
+ - path: prisma/migrations
31
+ purpose: "Auto-generated migration SQL files created by `npx prisma migrate dev`. Never edit these files manually — Prisma tracks their checksums and will refuse to apply migrations if they are modified after generation."
32
+ - path: src/lib
33
+ purpose: "Prisma Client singleton in db.ts — the one and only place that calls `new PrismaClient()`. All repositories and services import the prisma instance from here. Prevents connection pool exhaustion in serverless environments where each cold start could otherwise create a new pool."
34
+ recommended_dirs:
35
+ - path: src/repositories
36
+ purpose: "Data access layer — one file per Prisma model (e.g., user.repository.ts). All prisma.user.*(), prisma.post.*() calls belong here. Services call repository functions; they never import prisma directly. This makes the data layer mockable in unit tests."
37
+ - path: src/services
38
+ purpose: "Business logic layer — orchestrates calls to one or more repositories, validates input, applies rules, and returns domain objects. Never calls prisma directly."
39
+ separation:
40
+ rules:
41
+ - concern: singleton_client
42
+ belongs_in: src/lib
43
+ rule_text: "Create exactly one PrismaClient instance using the global singleton pattern in src/lib/db.ts. In serverless/Next.js environments, Hot Module Replacement recreates modules on every file save — without the global pattern, each save creates a new PrismaClient and a new connection pool until connections are exhausted."
44
+ example: |
45
+ // src/lib/db.ts — the only file that calls new PrismaClient()
46
+ import { PrismaClient } from '@prisma/client';
47
+
48
+ const globalForPrisma = globalThis as unknown as {
49
+ prisma: PrismaClient | undefined;
50
+ };
51
+
52
+ export const prisma =
53
+ globalForPrisma.prisma ??
54
+ new PrismaClient({
55
+ log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
56
+ });
57
+
58
+ if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;
59
+ anti_indicators:
60
+ - "new PrismaClient()"
61
+ - concern: migrations
62
+ belongs_in: prisma/migrations
63
+ rule_text: "All schema changes must go through the Prisma migration system. Use `npx prisma migrate dev --name description` in development and `npx prisma migrate deploy` in production CI/CD. Never run raw `ALTER TABLE` or `CREATE TABLE` SQL manually — it breaks migration history and Prisma Client type generation."
64
+ example: |
65
+ # Development: creates migration file + applies it + regenerates Prisma Client
66
+ npx prisma migrate dev --name add-phone-to-users
67
+
68
+ # Production CI/CD: applies pending migrations without interactive prompts
69
+ npx prisma migrate deploy
70
+
71
+ # After pulling schema changes: regenerate Prisma Client types locally
72
+ npx prisma generate
73
+ indicators:
74
+ - "prisma migrate dev"
75
+ - "prisma migrate deploy"
76
+ - "prisma generate"
77
+ - concern: data_access
78
+ belongs_in: src/repositories
79
+ rule_text: "Prisma queries live in src/repositories/ or src/models/. Services and API routes call repository functions — they never import prisma directly. Repository functions own the select/include shape of queries, handle errors, and return typed domain objects."
80
+ example: |
81
+ // src/repositories/user.repository.ts
82
+ import { prisma } from '@/lib/db';
83
+ import type { Prisma } from '@prisma/client';
84
+
85
+ export async function findUserById(id: string) {
86
+ return prisma.user.findUnique({
87
+ where: { id },
88
+ select: { id: true, email: true, name: true, createdAt: true },
89
+ // Exclude: password hash, internal fields
90
+ });
91
+ }
92
+
93
+ export async function createUser(data: Prisma.UserCreateInput) {
94
+ return prisma.user.create({ data });
95
+ }
96
+
97
+ export async function listUsersWithPosts(limit = 20) {
98
+ return prisma.user.findMany({
99
+ take: limit,
100
+ include: { posts: { take: 5, orderBy: { createdAt: 'desc' } } },
101
+ orderBy: { createdAt: 'desc' },
102
+ });
103
+ }
104
+ indicators:
105
+ - "prisma.user."
106
+ - "prisma.post."
107
+ - ".findMany("
108
+ - ".findUnique("
109
+ - "from '@/lib/db'"
110
+ - concern: transactions
111
+ belongs_in: src/repositories
112
+ rule_text: "Use prisma.$transaction() for operations that must succeed or fail together. Interactive transactions (callback form) support dependent queries where the second query uses the result of the first. Batch transactions (array form) are faster for independent mutations."
113
+ example: |
114
+ // src/repositories/order.repository.ts
115
+ export async function createOrderWithInventoryUpdate(
116
+ userId: string,
117
+ productId: string,
118
+ quantity: number
119
+ ) {
120
+ return prisma.$transaction(async (tx) => {
121
+ // Check and decrement inventory atomically
122
+ const product = await tx.product.update({
123
+ where: { id: productId, stock: { gte: quantity } },
124
+ data: { stock: { decrement: quantity } },
125
+ });
126
+
127
+ // Create the order using updated product data
128
+ return tx.order.create({
129
+ data: { userId, productId, quantity, total: product.price * quantity },
130
+ });
131
+ });
132
+ }
133
+ indicators:
134
+ - "prisma.$transaction"
135
+ - "$transaction("
136
+ patterns:
137
+ data_flow:
138
+ direction: "API Route/Server Action → Service → Repository → Prisma Client → Database"
139
+ rules:
140
+ - "API routes and Server Actions call service functions only — never prisma directly."
141
+ - "Services contain business logic and call repository functions — never prisma directly."
142
+ - "Repositories are the only files that import prisma from src/lib/db.ts."
143
+ - "Migrations run via `prisma migrate` — never raw SQL."
144
+ - "Prisma Client types are regenerated after every schema change with `prisma generate`."
145
+ - "For N+1 prevention: use include/select in repositories rather than loading relations in loops."
146
+ error_handling:
147
+ recommended: "Catch PrismaClientKnownRequestError in repositories. P2002 = unique constraint violation (duplicate email), P2025 = record not found (delete/update of non-existent row), P2003 = foreign key constraint violation."
148
+ naming:
149
+ schema: "prisma/schema.prisma — single data model source of truth"
150
+ client: "src/lib/db.ts — exports the prisma singleton"
151
+ repositories: "src/repositories/[model].repository.ts — e.g. user.repository.ts, post.repository.ts"
152
+ models: "src/models/[model].model.ts — domain model classes wrapping repository calls"
153
+ anti_patterns:
154
+ - id: multiple_prisma_clients
155
+ severity: critical
156
+ description: "Calling `new PrismaClient()` in multiple files instead of importing the singleton. Each PrismaClient creates its own connection pool. In serverless environments (Vercel, Lambda), each function invocation can create a new pool — leading to database 'too many connections' errors under moderate traffic."
157
+ bad_example: |
158
+ // ❌ New PrismaClient in every file — separate connection pool per module
159
+ // src/repositories/user.repository.ts
160
+ import { PrismaClient } from '@prisma/client';
161
+ const prisma = new PrismaClient(); // pool #1
162
+
163
+ // src/repositories/post.repository.ts
164
+ import { PrismaClient } from '@prisma/client';
165
+ const prisma = new PrismaClient(); // pool #2 — now two pools competing
166
+ good_example: |
167
+ // ✓ Import the singleton everywhere — one pool for the entire process
168
+ // src/lib/db.ts: export const prisma = globalThis.prisma ?? new PrismaClient();
169
+ import { prisma } from '@/lib/db';
170
+ - id: prisma_in_route_handler
171
+ severity: critical
172
+ description: "Calling prisma directly in API route handlers, Server Actions, or React components. This bypasses the service/repository layer, makes the code untestable (can't mock prisma calls in unit tests), and scatters query logic across the codebase."
173
+ bad_example: |
174
+ // ❌ Direct prisma call in API route — no service layer, untestable
175
+ import { prisma } from '@/lib/db';
176
+ export async function GET() {
177
+ const users = await prisma.user.findMany({
178
+ include: { posts: true },
179
+ });
180
+ return Response.json(users);
181
+ }
182
+ good_example: |
183
+ // ✓ API route calls service, service calls repository
184
+ import { userService } from '@/services/user.service';
185
+ export async function GET() {
186
+ const users = await userService.listUsersWithPosts();
187
+ return Response.json(users);
188
+ }
189
+ // src/services/user.service.ts calls userRepository.listWithPosts()
190
+ // src/repositories/user.repository.ts calls prisma.user.findMany(...)
191
+ - id: manual_sql_migration
192
+ severity: warning
193
+ description: "Modifying the database schema with raw SQL (ALTER TABLE, CREATE TABLE) instead of Prisma's migration system. The migration history becomes out of sync with the schema.prisma file — `prisma migrate deploy` in production applies migrations that have already been applied or skips needed ones."
194
+ bad_example: |
195
+ -- ❌ Manual SQL bypasses Prisma migration tracking
196
+ -- Run directly in DB console:
197
+ ALTER TABLE users ADD COLUMN phone VARCHAR(20);
198
+ -- schema.prisma still has no phone field — Prisma Client has no phone type
199
+ good_example: |
200
+ // ✓ 1. Add field in schema.prisma
201
+ // model User { phone String? }
202
+ // 2. Generate + apply migration:
203
+ // npx prisma migrate dev --name add-phone-to-users
204
+ // 3. Prisma Client regenerated automatically — phone field is now typed
205
+ - id: n_plus_one_query
206
+ severity: warning
207
+ description: "Loading a list of records and then fetching their relations in a loop — creates N+1 database queries (1 for the list + N for each record's relations). Use Prisma's include or nested select to fetch everything in a single query."
208
+ bad_example: |
209
+ // ❌ N+1: 1 query for posts + 1 query per post for the author
210
+ const posts = await prisma.post.findMany(); // query 1
211
+ for (const post of posts) {
212
+ post.author = await prisma.user.findUnique({ where: { id: post.userId } }); // N queries
213
+ }
214
+ good_example: |
215
+ // ✓ Single query: posts JOIN users via include
216
+ const posts = await prisma.post.findMany({
217
+ include: { author: { select: { id: true, name: true, email: true } } },
218
+ });
219
+ - id: unhandled_known_request_error
220
+ severity: warning
221
+ description: "Not catching PrismaClientKnownRequestError — unique constraint violations (P2002) and not-found errors (P2025) crash with an unhandled error instead of returning a user-friendly message. These errors are predictable and must be handled explicitly."
222
+ bad_example: |
223
+ // ❌ Unique constraint crash surfaces as a 500 Internal Server Error
224
+ export async function createUser(email: string) {
225
+ return prisma.user.create({ data: { email } });
226
+ // If email already exists: Unhandled PrismaClientKnownRequestError P2002
227
+ }
228
+ good_example: |
229
+ // ✓ Catch known errors and return domain-appropriate responses
230
+ import { Prisma } from '@prisma/client';
231
+ export async function createUser(email: string) {
232
+ try {
233
+ return await prisma.user.create({ data: { email } });
234
+ } catch (e) {
235
+ if (e instanceof Prisma.PrismaClientKnownRequestError) {
236
+ if (e.code === 'P2002') throw new Error('Email already in use');
237
+ }
238
+ throw e;
239
+ }
240
+ }
241
+ - id: select_star_in_production
242
+ severity: warning
243
+ description: "Using findMany() or findUnique() without a select clause — fetches all columns including password hashes, internal flags, and large text fields. Always specify exactly what fields you need, especially in API responses."
244
+ bad_example: |
245
+ // ❌ Returns all fields including passwordHash, internalFlags, etc.
246
+ export async function GET() {
247
+ const users = await prisma.user.findMany(); // returns passwordHash!
248
+ return Response.json(users);
249
+ }
250
+ good_example: |
251
+ // ✓ Explicit select — only the fields the client needs
252
+ const users = await prisma.user.findMany({
253
+ select: { id: true, email: true, name: true, createdAt: true },
254
+ // passwordHash, internalFlags, etc. never leave the DB layer
255
+ });
@@ -0,0 +1,235 @@
1
+ schema_version: "2.0.0"
2
+ id: s3-storage
3
+ name: "AWS S3 / R2 Storage"
4
+ version: "2.0.0"
5
+ description: "Object storage with pre-signed URLs for direct browser uploads, key-based DB references, server-side S3 client singleton, file type validation before signing, and secure download URLs."
6
+ category: pattern
7
+ language: javascript
8
+ frameworks: []
9
+ dependencies:
10
+ none: []
11
+ detection:
12
+ dependencies:
13
+ any:
14
+ - "@aws-sdk/client-s3"
15
+ - "@aws-sdk/s3-request-presigner"
16
+ source_indicators:
17
+ - "S3Client"
18
+ - "PutObjectCommand"
19
+ - "getSignedUrl"
20
+ - "from '@aws-sdk/client-s3'"
21
+ - "s3-request-presigner"
22
+ structure:
23
+ required_dirs: []
24
+ recommended_dirs:
25
+ - path: src/lib
26
+ purpose: "S3 client singleton in s3.ts — the only file that calls `new S3Client()`. Reads credentials from environment variables (or IAM role when deployed on EC2/ECS). Imported by the upload service only — never by route handlers or components directly."
27
+ - path: src/services
28
+ purpose: "Upload and download service logic in upload.service.ts — createUploadUrl() generates a pre-signed PUT URL, createDownloadUrl() generates a pre-signed GET URL. Validates allowed MIME types before generating the URL to prevent arbitrary file uploads."
29
+ separation:
30
+ rules:
31
+ - concern: presigned_urls
32
+ belongs_in: src/services
33
+ rule_text: "Never proxy file bytes through your server. The upload flow is: (1) client calls your API to request a pre-signed PUT URL, (2) client uploads directly to S3 using that URL, (3) client notifies your API with the S3 key after upload completes. Server memory is never touched by file bytes."
34
+ example: |
35
+ // src/services/upload.service.ts
36
+ import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
37
+ import { PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3';
38
+ import { s3 } from '@/lib/s3';
39
+ import { randomUUID } from 'crypto';
40
+
41
+ const ALLOWED_MIME_TYPES = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'];
42
+
43
+ export async function createUploadUrl(originalFilename: string, contentType: string) {
44
+ // Validate MIME type server-side before signing
45
+ if (!ALLOWED_MIME_TYPES.includes(contentType)) {
46
+ throw new Error(`File type ${contentType} is not allowed`);
47
+ }
48
+ // Randomize key to prevent path traversal and name collisions
49
+ const ext = originalFilename.split('.').pop();
50
+ const key = `uploads/${randomUUID()}.${ext}`;
51
+ const url = await getSignedUrl(
52
+ s3,
53
+ new PutObjectCommand({
54
+ Bucket: process.env.S3_BUCKET!,
55
+ Key: key,
56
+ ContentType: contentType, // required: browser upload rejects 403 without this
57
+ }),
58
+ { expiresIn: 300 } // 5 minutes — enough for large files, short enough to limit misuse
59
+ );
60
+ return { url, key };
61
+ }
62
+
63
+ export async function createDownloadUrl(key: string) {
64
+ return getSignedUrl(
65
+ s3,
66
+ new GetObjectCommand({ Bucket: process.env.S3_BUCKET!, Key: key }),
67
+ { expiresIn: 3600 } // 1 hour
68
+ );
69
+ }
70
+ indicators:
71
+ - "getSignedUrl"
72
+ - "PutObjectCommand"
73
+ - "expiresIn"
74
+ - concern: key_storage
75
+ belongs_in: src/models
76
+ rule_text: "Store the S3 object key (e.g. `uploads/abc123.jpg`) in your database — NOT the full S3 URL. Generate signed download URLs on-demand when serving the file to clients. This allows bucket migration, CDN changes, and URL policy changes without data migrations."
77
+ example: |
78
+ // ✓ Store key in DB — not the full URL
79
+ await db.user.update({
80
+ where: { id: userId },
81
+ data: { avatarKey: 'uploads/abc123-abc.jpg' }, // key only
82
+ });
83
+
84
+ // ✓ Generate download URL on-demand in an API response
85
+ const user = await db.user.findUnique({ where: { id: userId } });
86
+ const avatarUrl = user.avatarKey
87
+ ? await createDownloadUrl(user.avatarKey)
88
+ : null;
89
+ return { ...user, avatarUrl }; // URL expires — never cached in DB
90
+ indicators:
91
+ - "avatarKey"
92
+ - "fileKey"
93
+ - "imageKey"
94
+ - "objectKey"
95
+ - concern: client_singleton
96
+ belongs_in: src/lib
97
+ rule_text: "Create the S3 client as a module-level singleton in src/lib/s3.ts. Never call `new S3Client()` outside this file. Credentials must come from environment variables or an IAM instance role — never hardcoded."
98
+ example: |
99
+ // src/lib/s3.ts
100
+ import { S3Client } from '@aws-sdk/client-s3';
101
+
102
+ // Singleton: AWS SDK reuses HTTP connections for performance
103
+ export const s3 = new S3Client({
104
+ region: process.env.AWS_REGION ?? 'us-east-1',
105
+ // No credentials field: uses AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY env vars
106
+ // or IAM instance role automatically
107
+ });
108
+ anti_indicators:
109
+ - "new S3Client({"
110
+ - concern: upload_confirmation
111
+ belongs_in: src/api
112
+ rule_text: "After the client uploads directly to S3, it must call your API with the key to confirm the upload. Your API then validates that the key actually exists in S3 before storing it in the database. Without confirmation, an attacker can store arbitrary S3 keys pointing to non-existent or malicious objects."
113
+ example: |
114
+ // app/api/upload/confirm/route.ts
115
+ import { HeadObjectCommand } from '@aws-sdk/client-s3';
116
+ import { s3 } from '@/lib/s3';
117
+
118
+ export async function POST(req: Request) {
119
+ const { key } = await req.json();
120
+ // Validate that the object actually exists in S3 before saving key to DB
121
+ await s3.send(new HeadObjectCommand({ Bucket: process.env.S3_BUCKET!, Key: key }));
122
+ // Only now save to DB
123
+ await db.user.update({ where: { id: userId }, data: { avatarKey: key } });
124
+ return Response.json({ success: true });
125
+ }
126
+ indicators:
127
+ - "HeadObjectCommand"
128
+ - "confirm"
129
+ patterns:
130
+ data_flow:
131
+ direction: "Client → POST /upload/presign (server validates MIME) → S3 pre-signed PUT URL → Client uploads directly to S3 → Client POST /upload/confirm → Server verifies object exists → DB stores key"
132
+ rules:
133
+ - "File bytes never touch your server — client uploads directly to S3."
134
+ - "Validate MIME type in createUploadUrl() before generating the signed URL."
135
+ - "Store S3 object keys in DB — generate signed download URLs on-demand."
136
+ - "Confirm uploads server-side with HeadObjectCommand before persisting the key."
137
+ - "Set ContentType on PutObjectCommand — browsers reject pre-signed uploads without it."
138
+ error_handling:
139
+ recommended: "Catch NoSuchKey (S3ServiceException code NoSuchKey) when generating download URLs — return null or 404 if the object was deleted. Set maxFileSize on the client and validate server-side in the presign endpoint."
140
+ naming:
141
+ client: "src/lib/s3.ts — singleton S3Client; credentials from environment variables only"
142
+ upload_service: "src/services/upload.service.ts — createUploadUrl() and createDownloadUrl() live here"
143
+ key_pattern: "[folder]/[uuid].[ext] — e.g. uploads/a1b2c3d4-ef56-7890-abcd-ef1234567890.jpg"
144
+ anti_patterns:
145
+ - id: proxy_upload
146
+ severity: warning
147
+ description: "Receiving uploaded files in your server and re-uploading to S3 — doubles bandwidth cost, risks memory exhaustion on large files, and adds significant latency. A 50 MB file received by your server means 100 MB of total bandwidth usage."
148
+ bad_example: |
149
+ // ❌ File received by server, then uploaded to S3
150
+ app.post('/upload', upload.single('file'), async (req, res) => {
151
+ await s3.send(new PutObjectCommand({
152
+ Body: req.file.buffer, // 50 MB in server RAM
153
+ Bucket: process.env.S3_BUCKET,
154
+ Key: `uploads/${req.file.originalname}`,
155
+ }));
156
+ res.json({ key: `uploads/${req.file.originalname}` });
157
+ });
158
+ good_example: |
159
+ // ✓ Server generates presigned URL — client uploads directly
160
+ app.post('/upload/presign', async (req, res) => {
161
+ const { url, key } = await createUploadUrl(req.body.filename, req.body.contentType);
162
+ res.json({ url, key }); // file never touches server
163
+ });
164
+ - id: store_full_url
165
+ severity: warning
166
+ description: "Storing full S3 URLs in the database — breaks when bucket, region, CDN domain, or URL signing policy changes. Migrating data is expensive; migrating a key column is trivial."
167
+ bad_example: |
168
+ // ❌ Full URL stored — breaks on bucket migration or CDN change
169
+ await db.user.update({
170
+ where: { id: userId },
171
+ data: { avatarUrl: 'https://my-bucket.s3.us-east-1.amazonaws.com/uploads/photo.jpg' },
172
+ });
173
+ good_example: |
174
+ // ✓ Store key only — generate signed URL on every read
175
+ await db.user.update({
176
+ where: { id: userId },
177
+ data: { avatarKey: 'uploads/abc123.jpg' },
178
+ });
179
+ - id: hardcoded_credentials
180
+ severity: critical
181
+ description: "Embedding AWS access keys directly in source code or committing them in .env files. Secrets in git history are permanent — even if removed in a later commit, they remain in git log and forks."
182
+ bad_example: |
183
+ // ❌ Hardcoded credentials — leaked in git history forever
184
+ new S3Client({
185
+ region: 'us-east-1',
186
+ credentials: {
187
+ accessKeyId: 'AKIAIOSFODNN7EXAMPLE',
188
+ secretAccessKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
189
+ },
190
+ });
191
+ good_example: |
192
+ // ✓ AWS SDK reads credentials from environment automatically
193
+ // Set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY in .env (never committed)
194
+ // or use IAM role on EC2/ECS — no credentials needed in code
195
+ new S3Client({ region: process.env.AWS_REGION });
196
+ - id: missing_content_type_on_presign
197
+ severity: warning
198
+ description: "Generating a pre-signed PutObjectCommand without setting ContentType. S3 pre-signed URLs are signed against specific request parameters — if ContentType is in the signature but the browser upload omits or changes it, S3 returns a 403 Forbidden."
199
+ bad_example: |
200
+ // ❌ No ContentType — browser upload fails with 403
201
+ const url = await getSignedUrl(s3, new PutObjectCommand({
202
+ Bucket: process.env.S3_BUCKET,
203
+ Key: key,
204
+ // Missing: ContentType
205
+ }), { expiresIn: 300 });
206
+ good_example: |
207
+ // ✓ ContentType in the command matches what the browser will send
208
+ const url = await getSignedUrl(s3, new PutObjectCommand({
209
+ Bucket: process.env.S3_BUCKET,
210
+ Key: key,
211
+ ContentType: contentType, // e.g. 'image/jpeg'
212
+ }), { expiresIn: 300 });
213
+ // Client must set 'Content-Type': contentType header when PUT-ing to this URL
214
+ - id: no_file_type_validation
215
+ severity: critical
216
+ description: "Generating a pre-signed upload URL without validating the file's MIME type server-side. An attacker can request a signed URL for a .exe, .html (phishing), or .svg (XSS) file and upload it to your bucket — it will be served from your domain."
217
+ bad_example: |
218
+ // ❌ No MIME type validation — attacker uploads malware.exe to your S3 bucket
219
+ export async function POST(req: Request) {
220
+ const { filename, contentType } = await req.json();
221
+ // contentType can be anything the client sends
222
+ const url = await getSignedUrl(s3, new PutObjectCommand({
223
+ Bucket: process.env.S3_BUCKET, Key: `uploads/${filename}`, ContentType: contentType,
224
+ }), { expiresIn: 300 });
225
+ return Response.json({ url });
226
+ }
227
+ good_example: |
228
+ // ✓ Validate against allowlist before signing
229
+ const ALLOWED = new Set(['image/jpeg', 'image/png', 'image/webp', 'image/gif']);
230
+ export async function POST(req: Request) {
231
+ const { filename, contentType } = await req.json();
232
+ if (!ALLOWED.has(contentType)) return Response.json({ error: 'File type not allowed' }, { status: 400 });
233
+ const { url, key } = await createUploadUrl(filename, contentType);
234
+ return Response.json({ url, key });
235
+ }