@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.
- package/CHANGELOG.md +32 -0
- package/CONTRIBUTING.md +55 -0
- package/README.md +341 -0
- package/dist/analyzers/ast-parser.d.ts +3 -0
- package/dist/analyzers/ast-parser.js +305 -0
- package/dist/analyzers/ast-parser.js.map +1 -0
- package/dist/analyzers/dependency-graph.d.ts +2 -0
- package/dist/analyzers/dependency-graph.js +67 -0
- package/dist/analyzers/dependency-graph.js.map +1 -0
- package/dist/analyzers/duplication.d.ts +2 -0
- package/dist/analyzers/duplication.js +56 -0
- package/dist/analyzers/duplication.js.map +1 -0
- package/dist/analyzers/file-walker.d.ts +3 -0
- package/dist/analyzers/file-walker.js +80 -0
- package/dist/analyzers/file-walker.js.map +1 -0
- package/dist/cli/context-runner.d.ts +1 -0
- package/dist/cli/context-runner.js +16 -0
- package/dist/cli/context-runner.js.map +1 -0
- package/dist/cli/index.d.ts +24 -0
- package/dist/cli/index.js +217 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/init-runner.d.ts +25 -0
- package/dist/cli/init-runner.js +152 -0
- package/dist/cli/init-runner.js.map +1 -0
- package/dist/cli/scan-runner.d.ts +8 -0
- package/dist/cli/scan-runner.js +133 -0
- package/dist/cli/scan-runner.js.map +1 -0
- package/dist/formatters/plan-json.d.ts +2 -0
- package/dist/formatters/plan-json.js +4 -0
- package/dist/formatters/plan-json.js.map +1 -0
- package/dist/formatters/plan-markdown.d.ts +2 -0
- package/dist/formatters/plan-markdown.js +42 -0
- package/dist/formatters/plan-markdown.js.map +1 -0
- package/dist/formatters/plan-prompt.d.ts +4 -0
- package/dist/formatters/plan-prompt.js +5 -0
- package/dist/formatters/plan-prompt.js.map +1 -0
- package/dist/formatters/plan-terminal.d.ts +5 -0
- package/dist/formatters/plan-terminal.js +62 -0
- package/dist/formatters/plan-terminal.js.map +1 -0
- package/dist/generators/blueprint-renderer.d.ts +3 -0
- package/dist/generators/blueprint-renderer.js +27 -0
- package/dist/generators/blueprint-renderer.js.map +1 -0
- package/dist/generators/claudeWriter.d.ts +3 -0
- package/dist/generators/claudeWriter.js +9 -0
- package/dist/generators/claudeWriter.js.map +1 -0
- package/dist/generators/copilotWriter.d.ts +3 -0
- package/dist/generators/copilotWriter.js +11 -0
- package/dist/generators/copilotWriter.js.map +1 -0
- package/dist/generators/cursorWriter.d.ts +3 -0
- package/dist/generators/cursorWriter.js +14 -0
- package/dist/generators/cursorWriter.js.map +1 -0
- package/dist/generators/genericWriter.d.ts +3 -0
- package/dist/generators/genericWriter.js +9 -0
- package/dist/generators/genericWriter.js.map +1 -0
- package/dist/generators/template-context.d.ts +18 -0
- package/dist/generators/template-context.js +126 -0
- package/dist/generators/template-context.js.map +1 -0
- package/dist/generators/templateRenderer.d.ts +2 -0
- package/dist/generators/templateRenderer.js +19 -0
- package/dist/generators/templateRenderer.js.map +1 -0
- package/dist/generators/windsurfWriter.d.ts +3 -0
- package/dist/generators/windsurfWriter.js +14 -0
- package/dist/generators/windsurfWriter.js.map +1 -0
- package/dist/generators/writer-types.d.ts +11 -0
- package/dist/generators/writer-types.js +40 -0
- package/dist/generators/writer-types.js.map +1 -0
- package/dist/llm/claude-provider.d.ts +8 -0
- package/dist/llm/claude-provider.js +22 -0
- package/dist/llm/claude-provider.js.map +1 -0
- package/dist/llm/concern-classifier.d.ts +15 -0
- package/dist/llm/concern-classifier.js +61 -0
- package/dist/llm/concern-classifier.js.map +1 -0
- package/dist/llm/config.d.ts +11 -0
- package/dist/llm/config.js +120 -0
- package/dist/llm/config.js.map +1 -0
- package/dist/llm/ollama-provider.d.ts +8 -0
- package/dist/llm/ollama-provider.js +27 -0
- package/dist/llm/ollama-provider.js.map +1 -0
- package/dist/llm/openai-provider.d.ts +8 -0
- package/dist/llm/openai-provider.js +19 -0
- package/dist/llm/openai-provider.js.map +1 -0
- package/dist/llm/prompt-builder.d.ts +12 -0
- package/dist/llm/prompt-builder.js +132 -0
- package/dist/llm/prompt-builder.js.map +1 -0
- package/dist/llm/provider.d.ts +17 -0
- package/dist/llm/provider.js +2 -0
- package/dist/llm/provider.js.map +1 -0
- package/dist/llm/response-parser.d.ts +6 -0
- package/dist/llm/response-parser.js +128 -0
- package/dist/llm/response-parser.js.map +1 -0
- package/dist/planner/plan-generator.d.ts +7 -0
- package/dist/planner/plan-generator.js +275 -0
- package/dist/planner/plan-generator.js.map +1 -0
- package/dist/planner/plan-prompt-builder.d.ts +9 -0
- package/dist/planner/plan-prompt-builder.js +92 -0
- package/dist/planner/plan-prompt-builder.js.map +1 -0
- package/dist/planner/plan-response-parser.d.ts +7 -0
- package/dist/planner/plan-response-parser.js +21 -0
- package/dist/planner/plan-response-parser.js.map +1 -0
- package/dist/planner/plan-validator.d.ts +3 -0
- package/dist/planner/plan-validator.js +49 -0
- package/dist/planner/plan-validator.js.map +1 -0
- package/dist/reporters/scan-json.d.ts +13 -0
- package/dist/reporters/scan-json.js +26 -0
- package/dist/reporters/scan-json.js.map +1 -0
- package/dist/reporters/terminal.d.ts +6 -0
- package/dist/reporters/terminal.js +224 -0
- package/dist/reporters/terminal.js.map +1 -0
- package/dist/scoring/consistency-score.d.ts +3 -0
- package/dist/scoring/consistency-score.js +23 -0
- package/dist/scoring/consistency-score.js.map +1 -0
- package/dist/scoring/duplication-score.d.ts +3 -0
- package/dist/scoring/duplication-score.js +16 -0
- package/dist/scoring/duplication-score.js.map +1 -0
- package/dist/scoring/health-score.d.ts +4 -0
- package/dist/scoring/health-score.js +20 -0
- package/dist/scoring/health-score.js.map +1 -0
- package/dist/scoring/issue-builder.d.ts +4 -0
- package/dist/scoring/issue-builder.js +62 -0
- package/dist/scoring/issue-builder.js.map +1 -0
- package/dist/scoring/modularity-score.d.ts +3 -0
- package/dist/scoring/modularity-score.js +56 -0
- package/dist/scoring/modularity-score.js.map +1 -0
- package/dist/scoring/pattern-analysis.d.ts +3 -0
- package/dist/scoring/pattern-analysis.js +74 -0
- package/dist/scoring/pattern-analysis.js.map +1 -0
- package/dist/scoring/separation-score.d.ts +3 -0
- package/dist/scoring/separation-score.js +35 -0
- package/dist/scoring/separation-score.js.map +1 -0
- package/dist/skills/detector.d.ts +4 -0
- package/dist/skills/detector.js +104 -0
- package/dist/skills/detector.js.map +1 -0
- package/dist/skills/lister.d.ts +9 -0
- package/dist/skills/lister.js +35 -0
- package/dist/skills/lister.js.map +1 -0
- package/dist/skills/loader.d.ts +6 -0
- package/dist/skills/loader.js +76 -0
- package/dist/skills/loader.js.map +1 -0
- package/dist/skills/structure-check.d.ts +2 -0
- package/dist/skills/structure-check.js +37 -0
- package/dist/skills/structure-check.js.map +1 -0
- package/dist/skills/validator.d.ts +6 -0
- package/dist/skills/validator.js +229 -0
- package/dist/skills/validator.js.map +1 -0
- package/dist/types/analysis.d.ts +130 -0
- package/dist/types/analysis.js +41 -0
- package/dist/types/analysis.js.map +1 -0
- package/dist/types/concern.d.ts +48 -0
- package/dist/types/concern.js +16 -0
- package/dist/types/concern.js.map +1 -0
- package/dist/types/generation.d.ts +32 -0
- package/dist/types/generation.js +2 -0
- package/dist/types/generation.js.map +1 -0
- package/dist/types/issue.d.ts +12 -0
- package/dist/types/issue.js +2 -0
- package/dist/types/issue.js.map +1 -0
- package/dist/types/pattern.d.ts +15 -0
- package/dist/types/pattern.js +2 -0
- package/dist/types/pattern.js.map +1 -0
- package/dist/types/plan.d.ts +56 -0
- package/dist/types/plan.js +2 -0
- package/dist/types/plan.js.map +1 -0
- package/dist/types/scan-output.d.ts +84 -0
- package/dist/types/scan-output.js +2 -0
- package/dist/types/scan-output.js.map +1 -0
- package/dist/types/scoring.d.ts +15 -0
- package/dist/types/scoring.js +2 -0
- package/dist/types/scoring.js.map +1 -0
- package/dist/types/skill.d.ts +97 -0
- package/dist/types/skill.js +2 -0
- package/dist/types/skill.js.map +1 -0
- package/dist/utils/agent-detector.d.ts +2 -0
- package/dist/utils/agent-detector.js +22 -0
- package/dist/utils/agent-detector.js.map +1 -0
- package/dist/utils/interactive.d.ts +6 -0
- package/dist/utils/interactive.js +15 -0
- package/dist/utils/interactive.js.map +1 -0
- package/dist/utils/path.d.ts +5 -0
- package/dist/utils/path.js +31 -0
- package/dist/utils/path.js.map +1 -0
- package/dist/utils/progress.d.ts +17 -0
- package/dist/utils/progress.js +48 -0
- package/dist/utils/progress.js.map +1 -0
- package/dist/utils/thresholds.d.ts +6 -0
- package/dist/utils/thresholds.js +48 -0
- package/dist/utils/thresholds.js.map +1 -0
- package/package.json +63 -0
- package/skills/meta/general-js.skill.yaml +131 -0
- package/skills/patterns/clerk-auth.skill.yaml +349 -0
- package/skills/patterns/docker-deploy.skill.yaml +214 -0
- package/skills/patterns/drizzle.skill.yaml +277 -0
- package/skills/patterns/mongoose.skill.yaml +290 -0
- package/skills/patterns/nextauth.skill.yaml +308 -0
- package/skills/patterns/playwright-e2e.skill.yaml +265 -0
- package/skills/patterns/prisma.skill.yaml +255 -0
- package/skills/patterns/s3-storage.skill.yaml +235 -0
- package/skills/patterns/selenium-e2e.skill.yaml +276 -0
- package/skills/patterns/supabase-auth.skill.yaml +298 -0
- package/skills/patterns/supabase.skill.yaml +304 -0
- package/skills/patterns/vercel-deploy.skill.yaml +219 -0
- package/skills/patterns/vitest-testing.skill.yaml +262 -0
- package/skills/stacks/express-api.skill.yaml +155 -0
- package/skills/stacks/fastify-api.skill.yaml +119 -0
- package/skills/stacks/hono-api.skill.yaml +130 -0
- package/skills/stacks/nestjs.skill.yaml +135 -0
- package/skills/stacks/nextjs-app-router.skill.yaml +176 -0
- package/skills/stacks/react-spa.skill.yaml +153 -0
- package/skills/stacks/vue-nuxt.skill.yaml +115 -0
- package/templates/architect-plan.md +139 -0
- package/templates/architect-refactor.md +119 -0
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
schema_version: "2.0.0"
|
|
2
|
+
id: clerk-auth
|
|
3
|
+
name: "Clerk Auth"
|
|
4
|
+
version: "2.0.0"
|
|
5
|
+
description: "Managed authentication with Clerk — middleware route protection, server/client user access patterns, webhook database sync, and organization-based multi-tenancy."
|
|
6
|
+
category: pattern
|
|
7
|
+
language: javascript
|
|
8
|
+
frameworks:
|
|
9
|
+
- clerk
|
|
10
|
+
dependencies:
|
|
11
|
+
none:
|
|
12
|
+
- next-auth
|
|
13
|
+
- "@supabase/ssr"
|
|
14
|
+
- lucia
|
|
15
|
+
detection:
|
|
16
|
+
dependencies:
|
|
17
|
+
any:
|
|
18
|
+
- "@clerk/nextjs"
|
|
19
|
+
- "@clerk/clerk-sdk-node"
|
|
20
|
+
- "@clerk/clerk-react"
|
|
21
|
+
source_indicators:
|
|
22
|
+
- "ClerkProvider"
|
|
23
|
+
- "clerkMiddleware"
|
|
24
|
+
- "auth()"
|
|
25
|
+
- "currentUser()"
|
|
26
|
+
- "useUser()"
|
|
27
|
+
- "useAuth()"
|
|
28
|
+
structure:
|
|
29
|
+
required_dirs:
|
|
30
|
+
- path: app/api/webhooks/clerk
|
|
31
|
+
purpose: "Clerk webhook handler route — receives user.created, user.updated, and user.deleted events pushed by Clerk's servers. Must verify the Svix signature on every incoming request before touching the database — no signature check means attackers can forge any user lifecycle event."
|
|
32
|
+
recommended_dirs:
|
|
33
|
+
- path: src/lib
|
|
34
|
+
purpose: "Server-only Clerk helpers — auth() call wrappers, organization guard functions, and backend Clerk SDK calls (e.g., clerkClient().users.updateUserMetadata). Never import from here in Client Components ('use client' files)."
|
|
35
|
+
- path: middleware.ts
|
|
36
|
+
purpose: "Project root middleware file — clerkMiddleware() runs before every page render and API call, enforcing public vs. protected route separation. Only one middleware.ts is allowed per Next.js project; it must be at the root alongside app/."
|
|
37
|
+
separation:
|
|
38
|
+
rules:
|
|
39
|
+
- concern: route_protection
|
|
40
|
+
belongs_in: middleware.ts
|
|
41
|
+
rule_text: "Use clerkMiddleware() in middleware.ts to protect routes. Define public routes with createRouteMatcher — every other route is protected by default and redirects to sign-in. Never add per-page auth checks in layout.tsx or page.tsx as a substitute for middleware."
|
|
42
|
+
example: |
|
|
43
|
+
// middleware.ts — runs before every request
|
|
44
|
+
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';
|
|
45
|
+
|
|
46
|
+
const isPublicRoute = createRouteMatcher([
|
|
47
|
+
'/sign-in(.*)',
|
|
48
|
+
'/sign-up(.*)',
|
|
49
|
+
'/api/webhooks/(.*)',
|
|
50
|
+
'/',
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
export default clerkMiddleware(async (auth, req) => {
|
|
54
|
+
if (!isPublicRoute(req)) await auth.protect();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
export const config = {
|
|
58
|
+
matcher: [
|
|
59
|
+
'/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
|
|
60
|
+
'/(api|trpc)(.*)',
|
|
61
|
+
],
|
|
62
|
+
};
|
|
63
|
+
indicators:
|
|
64
|
+
- "clerkMiddleware"
|
|
65
|
+
- "createRouteMatcher"
|
|
66
|
+
- "auth.protect()"
|
|
67
|
+
- "isPublicRoute"
|
|
68
|
+
- concern: server_user_access
|
|
69
|
+
belongs_in: app
|
|
70
|
+
rule_text: "In Server Components and API route handlers, use auth() from @clerk/nextjs/server to get the current session (userId, orgId). For full user profile data (name, email, imageUrl), use currentUser() — it makes a network call to Clerk's API so only call it when the profile data is actually needed."
|
|
71
|
+
example: |
|
|
72
|
+
// app/dashboard/page.tsx — Server Component
|
|
73
|
+
import { auth, currentUser } from '@clerk/nextjs/server';
|
|
74
|
+
import { redirect } from 'next/navigation';
|
|
75
|
+
|
|
76
|
+
export default async function DashboardPage() {
|
|
77
|
+
const { userId } = await auth();
|
|
78
|
+
if (!userId) redirect('/sign-in');
|
|
79
|
+
|
|
80
|
+
// Only call currentUser() when you need profile fields beyond userId
|
|
81
|
+
const user = await currentUser();
|
|
82
|
+
return <h1>Welcome, {user?.firstName}</h1>;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// app/api/user/route.ts — API route
|
|
86
|
+
import { auth } from '@clerk/nextjs/server';
|
|
87
|
+
export async function GET() {
|
|
88
|
+
const { userId } = await auth();
|
|
89
|
+
if (!userId) return new Response('Unauthorized', { status: 401 });
|
|
90
|
+
const userData = await db.user.findUnique({ where: { clerkId: userId } });
|
|
91
|
+
return Response.json(userData);
|
|
92
|
+
}
|
|
93
|
+
indicators:
|
|
94
|
+
- "from '@clerk/nextjs/server'"
|
|
95
|
+
- "auth()"
|
|
96
|
+
- "currentUser()"
|
|
97
|
+
- "userId"
|
|
98
|
+
- concern: client_user_access
|
|
99
|
+
belongs_in: components
|
|
100
|
+
rule_text: "In Client Components, use useUser(), useAuth(), or useClerk() hooks from @clerk/nextjs. These hooks read from the ClerkProvider context — they work only inside components rendered below <ClerkProvider> in the tree. Use for display-only UI (avatars, names, role badges) — never for access control decisions."
|
|
101
|
+
example: |
|
|
102
|
+
// components/user-avatar.tsx — Client Component
|
|
103
|
+
'use client';
|
|
104
|
+
import { useUser } from '@clerk/nextjs';
|
|
105
|
+
|
|
106
|
+
export function UserAvatar() {
|
|
107
|
+
const { user, isLoaded, isSignedIn } = useUser();
|
|
108
|
+
|
|
109
|
+
// isLoaded prevents hydration mismatch
|
|
110
|
+
if (!isLoaded) return <div className="h-8 w-8 rounded-full bg-gray-200 animate-pulse" />;
|
|
111
|
+
if (!isSignedIn) return null;
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<img
|
|
115
|
+
src={user.imageUrl}
|
|
116
|
+
alt={user.fullName ?? 'User avatar'}
|
|
117
|
+
className="h-8 w-8 rounded-full"
|
|
118
|
+
/>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
indicators:
|
|
122
|
+
- "useUser()"
|
|
123
|
+
- "useAuth()"
|
|
124
|
+
- "useClerk()"
|
|
125
|
+
- "from '@clerk/nextjs'"
|
|
126
|
+
- concern: webhook_sync
|
|
127
|
+
belongs_in: app/api/webhooks/clerk
|
|
128
|
+
rule_text: "Verify the Svix signature before processing any webhook event. Sync user lifecycle events (user.created, user.updated, user.deleted) to your database. Store Clerk's userId as a NOT NULL UNIQUE 'clerkId' column in your users table — never store email alone because users can change their email in Clerk's dashboard."
|
|
129
|
+
example: |
|
|
130
|
+
// app/api/webhooks/clerk/route.ts
|
|
131
|
+
import { Webhook } from 'svix';
|
|
132
|
+
import { headers } from 'next/headers';
|
|
133
|
+
import type { WebhookEvent } from '@clerk/nextjs/server';
|
|
134
|
+
|
|
135
|
+
export async function POST(req: Request) {
|
|
136
|
+
const body = await req.text();
|
|
137
|
+
const h = await headers();
|
|
138
|
+
|
|
139
|
+
const wh = new Webhook(process.env.CLERK_WEBHOOK_SECRET!);
|
|
140
|
+
// wh.verify() throws WebhookVerificationError on invalid signature
|
|
141
|
+
const event = wh.verify(body, {
|
|
142
|
+
'svix-id': h.get('svix-id')!,
|
|
143
|
+
'svix-timestamp': h.get('svix-timestamp')!,
|
|
144
|
+
'svix-signature': h.get('svix-signature')!,
|
|
145
|
+
}) as WebhookEvent;
|
|
146
|
+
|
|
147
|
+
switch (event.type) {
|
|
148
|
+
case 'user.created':
|
|
149
|
+
await db.user.create({
|
|
150
|
+
data: {
|
|
151
|
+
clerkId: event.data.id,
|
|
152
|
+
email: event.data.email_addresses[0].email_address,
|
|
153
|
+
name: `${event.data.first_name} ${event.data.last_name}`.trim(),
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
break;
|
|
157
|
+
case 'user.updated':
|
|
158
|
+
await db.user.update({
|
|
159
|
+
where: { clerkId: event.data.id },
|
|
160
|
+
data: { email: event.data.email_addresses[0].email_address },
|
|
161
|
+
});
|
|
162
|
+
break;
|
|
163
|
+
case 'user.deleted':
|
|
164
|
+
await db.user.delete({ where: { clerkId: event.data.id! } });
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return new Response('OK', { status: 200 });
|
|
169
|
+
}
|
|
170
|
+
indicators:
|
|
171
|
+
- "Webhook"
|
|
172
|
+
- "svix"
|
|
173
|
+
- "CLERK_WEBHOOK_SECRET"
|
|
174
|
+
- "WebhookEvent"
|
|
175
|
+
- "svix-signature"
|
|
176
|
+
- concern: organization_access
|
|
177
|
+
belongs_in: app
|
|
178
|
+
rule_text: "For multi-tenant apps, read orgId from auth() and filter every database query by it. Switching organizations in Clerk's UI rotates the orgId in the session — your database queries automatically see only the new org's data. Never derive the organization from user metadata or a custom header."
|
|
179
|
+
example: |
|
|
180
|
+
// app/projects/page.tsx — scoped to active organization
|
|
181
|
+
import { auth } from '@clerk/nextjs/server';
|
|
182
|
+
import { redirect } from 'next/navigation';
|
|
183
|
+
|
|
184
|
+
export default async function ProjectsPage() {
|
|
185
|
+
const { userId, orgId } = await auth();
|
|
186
|
+
if (!userId) redirect('/sign-in');
|
|
187
|
+
if (!orgId) redirect('/select-org'); // user hasn't selected an org yet
|
|
188
|
+
|
|
189
|
+
// Every query filtered by orgId — no cross-tenant data leaks
|
|
190
|
+
const projects = await db.project.findMany({
|
|
191
|
+
where: { orgId },
|
|
192
|
+
orderBy: { createdAt: 'desc' },
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
return <ProjectList projects={projects} />;
|
|
196
|
+
}
|
|
197
|
+
indicators:
|
|
198
|
+
- "orgId"
|
|
199
|
+
- "orgRole"
|
|
200
|
+
- "useOrganization()"
|
|
201
|
+
- "useOrganizationList()"
|
|
202
|
+
- "orgSlug"
|
|
203
|
+
patterns:
|
|
204
|
+
data_flow:
|
|
205
|
+
direction: "HTTP Request → clerkMiddleware (protect or allow) → Server Component (auth()) → Service/DB"
|
|
206
|
+
rules:
|
|
207
|
+
- "Middleware runs first — protected routes redirect to sign-in before any page code executes."
|
|
208
|
+
- "Server Components call auth() for session (userId, orgId) and currentUser() for profile data — currentUser() costs an extra network call so only call it when needed."
|
|
209
|
+
- "Client Components use useUser()/useAuth() hooks — they read from ClerkProvider context already in the page, zero extra network call."
|
|
210
|
+
- "Webhooks are the bridge from Clerk's managed user store to your relational database — sync on user.created/updated/deleted events."
|
|
211
|
+
- "For multi-tenant apps: auth() → { userId, orgId } → every DB query includes orgId in WHERE clause."
|
|
212
|
+
- "publicMetadata and privateMetadata must only be mutated from Server Actions or API routes, never from Client Components."
|
|
213
|
+
error_handling:
|
|
214
|
+
recommended: "Use auth.protect() in middleware for route-level protection. For API routes not covered by middleware, check auth().userId and return 401 when null. For org-scoped routes, check auth().orgId and redirect to /select-org when null."
|
|
215
|
+
naming:
|
|
216
|
+
middleware: "middleware.ts at project root — clerkMiddleware() must be the outermost and only middleware"
|
|
217
|
+
webhook_handler: "app/api/webhooks/clerk/route.ts — POST handler with Svix signature verification"
|
|
218
|
+
server_helper: "src/lib/auth.ts — reusable server-side auth wrappers (requireAuth, requireOrg)"
|
|
219
|
+
db_field: "clerkId — NOT NULL UNIQUE column in users table; stores Clerk's userId string (format: user_XXXX)"
|
|
220
|
+
org_db_field: "orgId — NOT NULL column on organization-scoped tables; stores Clerk's orgId string (format: org_XXXX)"
|
|
221
|
+
anti_patterns:
|
|
222
|
+
- id: manual_token_handling
|
|
223
|
+
severity: critical
|
|
224
|
+
description: "Manually reading, parsing, or verifying Clerk JWTs or __session cookies instead of using Clerk's auth() helper. This breaks when Clerk rotates signing keys (which happens automatically), and bypasses session freshness checks, leaving stale or forged tokens accepted."
|
|
225
|
+
bad_example: |
|
|
226
|
+
// ❌ Manual JWT parsing — breaks on key rotation, bypasses Clerk's checks
|
|
227
|
+
import jwt from 'jsonwebtoken';
|
|
228
|
+
const token = req.headers.authorization?.split(' ')[1];
|
|
229
|
+
const decoded = jwt.verify(token, process.env.CLERK_PEM_KEY!);
|
|
230
|
+
const userId = (decoded as any).sub;
|
|
231
|
+
// This will silently break the next time Clerk rotates its signing key
|
|
232
|
+
good_example: |
|
|
233
|
+
// ✓ Clerk's auth() handles verification, key rotation, and session refresh automatically
|
|
234
|
+
import { auth } from '@clerk/nextjs/server';
|
|
235
|
+
const { userId } = await auth();
|
|
236
|
+
if (!userId) return new Response('Unauthorized', { status: 401 });
|
|
237
|
+
- id: unverified_webhook
|
|
238
|
+
severity: critical
|
|
239
|
+
description: "Processing Clerk webhook payloads without verifying the Svix HMAC signature. Any attacker who discovers your webhook endpoint URL can POST fabricated user.created or user.deleted events — creating phantom users, deleting real users, or granting admin roles without ever touching Clerk's dashboard."
|
|
240
|
+
bad_example: |
|
|
241
|
+
// ❌ No signature verification — endpoint is open to forgery
|
|
242
|
+
export async function POST(req: Request) {
|
|
243
|
+
const payload = await req.json();
|
|
244
|
+
// Blindly trusting the payload type and data
|
|
245
|
+
if (payload.type === 'user.created') {
|
|
246
|
+
await db.user.create({
|
|
247
|
+
data: { clerkId: payload.data.id, role: 'admin' }, // forged!
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
return new Response('OK');
|
|
251
|
+
}
|
|
252
|
+
good_example: |
|
|
253
|
+
// ✓ Svix verification throws WebhookVerificationError on tampered requests
|
|
254
|
+
import { Webhook } from 'svix';
|
|
255
|
+
const wh = new Webhook(process.env.CLERK_WEBHOOK_SECRET!);
|
|
256
|
+
const event = wh.verify(body, svixHeaders) as WebhookEvent;
|
|
257
|
+
// Only reaches here if signature is valid
|
|
258
|
+
if (event.type === 'user.created') {
|
|
259
|
+
await db.user.create({ data: { clerkId: event.data.id } });
|
|
260
|
+
}
|
|
261
|
+
- id: client_component_auth_check
|
|
262
|
+
severity: warning
|
|
263
|
+
description: "Using useUser() or useAuth() to gate protected content or navigation in Client Components. Client-side auth can be bypassed by disabling JavaScript, using browser DevTools to modify React state, or navigating directly via URL. Always enforce access control in middleware or Server Components."
|
|
264
|
+
bad_example: |
|
|
265
|
+
// ❌ Client-side guard — content flashes and is bypassable with DevTools
|
|
266
|
+
'use client';
|
|
267
|
+
import { useAuth } from '@clerk/nextjs';
|
|
268
|
+
export function AdminPanel() {
|
|
269
|
+
const { isSignedIn } = useAuth();
|
|
270
|
+
if (!isSignedIn) return null; // briefly visible during hydration; bypassable
|
|
271
|
+
return <DangerousAdminActions />;
|
|
272
|
+
}
|
|
273
|
+
good_example: |
|
|
274
|
+
// ✓ Server Component — auth check happens before any HTML is sent
|
|
275
|
+
import { auth } from '@clerk/nextjs/server';
|
|
276
|
+
import { redirect } from 'next/navigation';
|
|
277
|
+
export default async function AdminPage() {
|
|
278
|
+
const { userId } = await auth();
|
|
279
|
+
if (!userId) redirect('/sign-in');
|
|
280
|
+
return <DangerousAdminActions />;
|
|
281
|
+
}
|
|
282
|
+
// OR: add /admin to isPublicRoute exclusion in middleware.ts (simplest)
|
|
283
|
+
- id: missing_clerk_provider
|
|
284
|
+
severity: critical
|
|
285
|
+
description: "Not wrapping the root layout with <ClerkProvider>. All Clerk client hooks (useUser, useAuth, useClerk, SignInButton, UserButton) throw a React context error at runtime because they expect to find ClerkProvider as an ancestor in the component tree."
|
|
286
|
+
bad_example: |
|
|
287
|
+
// app/layout.tsx — missing ClerkProvider
|
|
288
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
289
|
+
return (
|
|
290
|
+
<html lang="en">
|
|
291
|
+
<body>{children}</body>
|
|
292
|
+
{/* Any child that calls useUser() crashes:
|
|
293
|
+
Error: useUser() called outside <ClerkProvider> */}
|
|
294
|
+
</html>
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
good_example: |
|
|
298
|
+
// app/layout.tsx — ClerkProvider wraps the entire app
|
|
299
|
+
import { ClerkProvider } from '@clerk/nextjs';
|
|
300
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
301
|
+
return (
|
|
302
|
+
<ClerkProvider>
|
|
303
|
+
<html lang="en">
|
|
304
|
+
<body>{children}</body>
|
|
305
|
+
</html>
|
|
306
|
+
</ClerkProvider>
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
- id: org_id_not_enforced
|
|
310
|
+
severity: warning
|
|
311
|
+
description: "In multi-tenant apps, querying the database without filtering by orgId. Users authenticated to organization A can read or modify organization B's data because the tenant boundary is not applied to the query."
|
|
312
|
+
bad_example: |
|
|
313
|
+
// ❌ Returns ALL projects regardless of the user's current organization
|
|
314
|
+
const { userId } = await auth();
|
|
315
|
+
// orgId is available but ignored — org B members see org A's data
|
|
316
|
+
const projects = await db.project.findMany({ where: { createdBy: userId } });
|
|
317
|
+
good_example: |
|
|
318
|
+
// ✓ Scope every query to the authenticated user's active organization
|
|
319
|
+
const { userId, orgId } = await auth();
|
|
320
|
+
if (!orgId) redirect('/select-organization');
|
|
321
|
+
const projects = await db.project.findMany({
|
|
322
|
+
where: { orgId }, // org boundary enforced at query level
|
|
323
|
+
});
|
|
324
|
+
- id: metadata_mutation_on_client
|
|
325
|
+
severity: warning
|
|
326
|
+
description: "Updating Clerk publicMetadata from a Client Component via the browser SDK. The client-side user.update() call is made with the user's own session, meaning any user can call it with arbitrary values — including promoting themselves to 'admin'. All metadata writes must go through a Server Action that validates permissions first."
|
|
327
|
+
bad_example: |
|
|
328
|
+
// ❌ Client-side metadata write — user can call update() with any role value
|
|
329
|
+
'use client';
|
|
330
|
+
import { useUser } from '@clerk/nextjs';
|
|
331
|
+
export function UpgradeButton() {
|
|
332
|
+
const { user } = useUser();
|
|
333
|
+
const upgrade = () => user?.update({ unsafeMetadata: { plan: 'enterprise' } });
|
|
334
|
+
return <button onClick={upgrade}>Upgrade</button>;
|
|
335
|
+
}
|
|
336
|
+
good_example: |
|
|
337
|
+
// ✓ Server Action validates the request before mutating metadata
|
|
338
|
+
'use server';
|
|
339
|
+
import { auth, clerkClient } from '@clerk/nextjs/server';
|
|
340
|
+
export async function upgradePlan(targetUserId: string) {
|
|
341
|
+
const { sessionClaims } = await auth();
|
|
342
|
+
// Only admins can upgrade other users
|
|
343
|
+
if (sessionClaims?.publicMetadata?.role !== 'admin') {
|
|
344
|
+
throw new Error('Forbidden');
|
|
345
|
+
}
|
|
346
|
+
await (await clerkClient()).users.updateUserMetadata(targetUserId, {
|
|
347
|
+
publicMetadata: { plan: 'enterprise' },
|
|
348
|
+
});
|
|
349
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
schema_version: "2.0.0"
|
|
2
|
+
id: docker-deploy
|
|
3
|
+
name: "Docker"
|
|
4
|
+
version: "2.0.0"
|
|
5
|
+
description: "Docker containerization with multi-stage builds, non-root user, runtime-injected secrets, HEALTHCHECK, pinned base image tags, and correct COPY order to maximize layer cache."
|
|
6
|
+
category: pattern
|
|
7
|
+
language: javascript
|
|
8
|
+
frameworks: []
|
|
9
|
+
dependencies:
|
|
10
|
+
none: []
|
|
11
|
+
detection:
|
|
12
|
+
files:
|
|
13
|
+
- Dockerfile
|
|
14
|
+
- docker-compose.yml
|
|
15
|
+
- docker-compose.yaml
|
|
16
|
+
source_indicators:
|
|
17
|
+
- "FROM node:"
|
|
18
|
+
- "COPY --from=builder"
|
|
19
|
+
- "docker-compose"
|
|
20
|
+
- "AS builder"
|
|
21
|
+
- "AS production"
|
|
22
|
+
structure:
|
|
23
|
+
required_dirs: []
|
|
24
|
+
recommended_dirs:
|
|
25
|
+
- path: docker
|
|
26
|
+
purpose: "Docker support files — entrypoint.sh (startup script that runs migrations before the app), healthcheck.sh (custom health probe script), and any docker-compose overrides. Keep these out of the project root to avoid clutter."
|
|
27
|
+
- path: .docker
|
|
28
|
+
purpose: "Alternative location for Docker configuration scripts — entrypoints, init scripts, and environment templates. Use one consistent location across the project."
|
|
29
|
+
separation:
|
|
30
|
+
rules:
|
|
31
|
+
- concern: multi_stage_build
|
|
32
|
+
belongs_in: Dockerfile
|
|
33
|
+
rule_text: "Use a two-stage Dockerfile — a `builder` stage installs all dependencies (including devDependencies) and compiles the TypeScript source, and a `production` stage copies only the compiled output. Never copy the entire source or node_modules from the builder if you can reinstall with --omit=dev."
|
|
34
|
+
example: |
|
|
35
|
+
# Dockerfile
|
|
36
|
+
# Stage 1: build
|
|
37
|
+
FROM node:20-alpine AS builder
|
|
38
|
+
WORKDIR /app
|
|
39
|
+
# Copy package files first — layer is cached until package.json changes
|
|
40
|
+
COPY package*.json ./
|
|
41
|
+
RUN npm ci
|
|
42
|
+
COPY . .
|
|
43
|
+
RUN npm run build # produces dist/
|
|
44
|
+
|
|
45
|
+
# Stage 2: production — only compiled output, no devDependencies
|
|
46
|
+
FROM node:20-alpine AS production
|
|
47
|
+
WORKDIR /app
|
|
48
|
+
COPY package*.json ./
|
|
49
|
+
RUN npm ci --omit=dev # install only runtime deps — ~70% smaller image
|
|
50
|
+
COPY --from=builder /app/dist ./dist
|
|
51
|
+
# Run health checks, switch user, start app
|
|
52
|
+
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s \
|
|
53
|
+
CMD node -e "require('http').get('http://localhost:3000/health', r => process.exit(r.statusCode === 200 ? 0 : 1)).on('error', () => process.exit(1))"
|
|
54
|
+
USER node
|
|
55
|
+
CMD ["node", "dist/index.js"]
|
|
56
|
+
indicators:
|
|
57
|
+
- "FROM node:"
|
|
58
|
+
- "AS builder"
|
|
59
|
+
- "AS production"
|
|
60
|
+
- "COPY --from=builder"
|
|
61
|
+
- concern: non_root_user
|
|
62
|
+
belongs_in: Dockerfile
|
|
63
|
+
rule_text: "Switch to the built-in `node` user before the CMD or ENTRYPOINT instruction. Running as root in a container gives an attacker full host access if the container runtime is misconfigured or the app has a code execution vulnerability."
|
|
64
|
+
example: |
|
|
65
|
+
# ✓ Switch to non-root user — applied in the production stage
|
|
66
|
+
# Ensure the app directory is owned by node before switching
|
|
67
|
+
COPY --chown=node:node --from=builder /app/dist ./dist
|
|
68
|
+
USER node
|
|
69
|
+
CMD ["node", "dist/index.js"]
|
|
70
|
+
anti_indicators:
|
|
71
|
+
- "USER root"
|
|
72
|
+
- concern: env_injection
|
|
73
|
+
belongs_in: docker-compose.yml
|
|
74
|
+
rule_text: "Inject all secrets via environment variables at container runtime — never bake secrets into the image with ENV or ARG instructions. Use docker-compose's env_file directive to load from .env. Commit .env.example with placeholder values; add .env to .gitignore."
|
|
75
|
+
example: |
|
|
76
|
+
# docker-compose.yml — runtime environment injection
|
|
77
|
+
services:
|
|
78
|
+
app:
|
|
79
|
+
build: .
|
|
80
|
+
ports:
|
|
81
|
+
- "3000:3000"
|
|
82
|
+
env_file: .env # loads DATABASE_URL, JWT_SECRET, etc. from .env
|
|
83
|
+
environment:
|
|
84
|
+
NODE_ENV: production # non-secret config can be inline
|
|
85
|
+
restart: unless-stopped
|
|
86
|
+
indicators:
|
|
87
|
+
- "env_file:"
|
|
88
|
+
- "environment:"
|
|
89
|
+
- concern: layer_cache_order
|
|
90
|
+
belongs_in: Dockerfile
|
|
91
|
+
rule_text: "Order COPY instructions to maximize Docker's layer cache — copy package files (package.json, package-lock.json) and run npm ci BEFORE copying application source. Since package files change rarely, the npm ci layer is cached for the majority of builds."
|
|
92
|
+
example: |
|
|
93
|
+
# ✓ Package files first — npm ci layer cached unless package.json changes
|
|
94
|
+
COPY package*.json ./
|
|
95
|
+
RUN npm ci # cached on most builds
|
|
96
|
+
COPY . . # application source last — invalidates only subsequent layers
|
|
97
|
+
RUN npm run build
|
|
98
|
+
|
|
99
|
+
# ❌ WRONG ORDER — invalidates npm ci on every source file change:
|
|
100
|
+
# COPY . . # copies everything including src/
|
|
101
|
+
# RUN npm ci # re-runs on every code change — no cache benefit
|
|
102
|
+
# RUN npm run build
|
|
103
|
+
indicators:
|
|
104
|
+
- "COPY package*.json"
|
|
105
|
+
- concern: healthcheck
|
|
106
|
+
belongs_in: Dockerfile
|
|
107
|
+
rule_text: "Add a HEALTHCHECK instruction to every production Dockerfile. Without it, Docker and Kubernetes cannot distinguish between a starting container and a broken one — they keep routing traffic to unhealthy instances."
|
|
108
|
+
example: |
|
|
109
|
+
# Option 1: HTTP health check via node inline script (no extra dependencies)
|
|
110
|
+
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
|
|
111
|
+
CMD node -e "require('http').get('http://localhost:3000/health', r => process.exit(r.statusCode===200?0:1)).on('error',()=>process.exit(1))"
|
|
112
|
+
|
|
113
|
+
# Option 2: wget (available in alpine)
|
|
114
|
+
HEALTHCHECK --interval=30s --timeout=5s CMD wget --quiet --tries=1 --spider http://localhost:3000/health || exit 1
|
|
115
|
+
|
|
116
|
+
# In docker-compose, override start-period for slow-starting apps:
|
|
117
|
+
# healthcheck:
|
|
118
|
+
# test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3000/health"]
|
|
119
|
+
# start_period: 30s
|
|
120
|
+
indicators:
|
|
121
|
+
- "HEALTHCHECK"
|
|
122
|
+
- "/health"
|
|
123
|
+
patterns:
|
|
124
|
+
data_flow:
|
|
125
|
+
direction: "Source → Builder Stage (npm ci + tsc) → Production Stage (npm ci --omit=dev + dist copy) → Container Runtime (env vars injected)"
|
|
126
|
+
rules:
|
|
127
|
+
- "Builder stage: install all deps + compile TypeScript to dist/."
|
|
128
|
+
- "Production stage: reinstall only runtime deps (--omit=dev) + copy dist/ from builder."
|
|
129
|
+
- "Secrets injected at runtime via env_file — never in Dockerfile ENV or ARG."
|
|
130
|
+
- "COPY package files before COPY . . to maximize layer cache reuse."
|
|
131
|
+
- "HEALTHCHECK enables orchestrators to detect unhealthy containers automatically."
|
|
132
|
+
error_handling:
|
|
133
|
+
recommended: "Add a /health endpoint that returns 200 when the app is ready (DB connected, etc.) and 503 during startup or degraded state. HEALTHCHECK uses this to report container health to Docker/Kubernetes."
|
|
134
|
+
naming:
|
|
135
|
+
dockerfile: "Dockerfile at project root — multi-stage: AS builder then AS production"
|
|
136
|
+
compose: "docker-compose.yml (development) + docker-compose.prod.yml (production overrides)"
|
|
137
|
+
env_template: ".env.example committed to git with placeholder values; .env in .gitignore"
|
|
138
|
+
health_endpoint: "GET /health — returns { status: 'ok', uptime: number } with 200, or 503 during degraded state"
|
|
139
|
+
anti_patterns:
|
|
140
|
+
- id: secrets_in_image
|
|
141
|
+
severity: critical
|
|
142
|
+
description: "Baking secrets into Docker images with ENV or ARG instructions. Secrets persist in image layers and are visible via `docker history` — anyone with pull access to the image registry can read them."
|
|
143
|
+
bad_example: |
|
|
144
|
+
# ❌ Secrets baked into the image — visible in docker history
|
|
145
|
+
ARG DATABASE_URL
|
|
146
|
+
ENV DATABASE_URL=${DATABASE_URL}
|
|
147
|
+
ENV JWT_SECRET=hardcoded-secret-key
|
|
148
|
+
# docker history myapp → shows JWT_SECRET value in the layer
|
|
149
|
+
good_example: |
|
|
150
|
+
# ✓ Inject secrets at runtime, not build time
|
|
151
|
+
# docker-compose.yml: env_file: .env
|
|
152
|
+
# Or: docker run --env-file .env myapp
|
|
153
|
+
# Image contains no secrets — safe to push to any registry
|
|
154
|
+
- id: single_stage_build
|
|
155
|
+
severity: warning
|
|
156
|
+
description: "Using a single-stage Dockerfile that includes all devDependencies, TypeScript source, and build tools in the production image. A typical Next.js project: single-stage = ~1.5 GB image, multi-stage = ~300 MB. Larger images mean slower deployments and a larger attack surface."
|
|
157
|
+
bad_example: |
|
|
158
|
+
# ❌ Single stage — devDependencies + source + build tools in production
|
|
159
|
+
FROM node:20
|
|
160
|
+
WORKDIR /app
|
|
161
|
+
COPY . . # includes src/, tests/, .env.example
|
|
162
|
+
RUN npm install # installs ALL deps including jest, ts-node, etc.
|
|
163
|
+
RUN npm run build
|
|
164
|
+
CMD ["node", "dist/index.js"]
|
|
165
|
+
good_example: |
|
|
166
|
+
# ✓ Multi-stage — production image has only what's needed to run
|
|
167
|
+
FROM node:20-alpine AS builder
|
|
168
|
+
RUN npm ci && npm run build
|
|
169
|
+
FROM node:20-alpine AS production
|
|
170
|
+
COPY package*.json ./
|
|
171
|
+
RUN npm ci --omit=dev # runtime deps only
|
|
172
|
+
COPY --from=builder /app/dist ./dist
|
|
173
|
+
- id: running_as_root
|
|
174
|
+
severity: warning
|
|
175
|
+
description: "Not switching to a non-root user before CMD — Docker containers run as root by default. If the application has an RCE vulnerability, the attacker has root-level access inside the container."
|
|
176
|
+
bad_example: |
|
|
177
|
+
# ❌ No USER instruction — process runs as root inside container
|
|
178
|
+
COPY --from=builder /app/dist ./dist
|
|
179
|
+
CMD ["node", "dist/index.js"]
|
|
180
|
+
good_example: |
|
|
181
|
+
# ✓ Switch to the built-in non-root node user
|
|
182
|
+
COPY --chown=node:node --from=builder /app/dist ./dist
|
|
183
|
+
USER node
|
|
184
|
+
CMD ["node", "dist/index.js"]
|
|
185
|
+
- id: using_latest_tag
|
|
186
|
+
severity: warning
|
|
187
|
+
description: "Using `FROM node:latest` — the `latest` tag is a moving target that resolves to a different version on each build. When a new Node.js major is released, `latest` jumps to it and your build may fail with breaking changes. Pin to a specific LTS version."
|
|
188
|
+
bad_example: |
|
|
189
|
+
# ❌ Non-deterministic — breaks when node:latest jumps to a new major
|
|
190
|
+
FROM node:latest
|
|
191
|
+
# Today: node 22. After next LTS release: could be node 24 — breaking
|
|
192
|
+
good_example: |
|
|
193
|
+
# ✓ Pinned to a specific LTS version — predictable builds
|
|
194
|
+
FROM node:20-alpine AS builder
|
|
195
|
+
FROM node:20-alpine AS production
|
|
196
|
+
# To upgrade, explicitly change the version and test
|
|
197
|
+
- id: copying_node_modules
|
|
198
|
+
severity: warning
|
|
199
|
+
description: "Running COPY . . before npm ci causes local node_modules to be copied into the image. node_modules built on macOS are not compatible with the Linux container — native addons fail, symlinks break, and the image includes dev dependencies."
|
|
200
|
+
bad_example: |
|
|
201
|
+
# ❌ COPY . . copies local node_modules into the image
|
|
202
|
+
FROM node:20-alpine
|
|
203
|
+
WORKDIR /app
|
|
204
|
+
COPY . . # copies node_modules from macOS host!
|
|
205
|
+
RUN npm ci # may use or be confused by the already-copied modules
|
|
206
|
+
CMD ["node", "dist/index.js"]
|
|
207
|
+
good_example: |
|
|
208
|
+
# ✓ Add node_modules to .dockerignore, copy package files first
|
|
209
|
+
# .dockerignore: node_modules, dist, .env
|
|
210
|
+
FROM node:20-alpine
|
|
211
|
+
WORKDIR /app
|
|
212
|
+
COPY package*.json ./ # package files only — no local node_modules
|
|
213
|
+
RUN npm ci # fresh install in the Linux container
|
|
214
|
+
COPY . . # source files only (node_modules excluded by .dockerignore)
|