@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,308 @@
|
|
|
1
|
+
schema_version: "2.0.0"
|
|
2
|
+
id: nextauth
|
|
3
|
+
name: "NextAuth.js / Auth.js"
|
|
4
|
+
version: "2.0.0"
|
|
5
|
+
description: "Self-hosted authentication with NextAuth.js v4 / Auth.js v5 — provider configuration, JWT/database session strategies, role-based access, and middleware route protection."
|
|
6
|
+
category: pattern
|
|
7
|
+
language: javascript
|
|
8
|
+
frameworks:
|
|
9
|
+
- next-auth
|
|
10
|
+
dependencies:
|
|
11
|
+
none:
|
|
12
|
+
- "@clerk/nextjs"
|
|
13
|
+
- "@supabase/ssr"
|
|
14
|
+
- lucia
|
|
15
|
+
detection:
|
|
16
|
+
dependencies:
|
|
17
|
+
any:
|
|
18
|
+
- next-auth
|
|
19
|
+
- "@auth/nextjs"
|
|
20
|
+
- "@auth/core"
|
|
21
|
+
source_indicators:
|
|
22
|
+
- "NextAuth("
|
|
23
|
+
- "authOptions"
|
|
24
|
+
- "getServerSession"
|
|
25
|
+
- "useSession"
|
|
26
|
+
- "SessionProvider"
|
|
27
|
+
- "auth()"
|
|
28
|
+
structure:
|
|
29
|
+
required_dirs:
|
|
30
|
+
- path: app/api/auth
|
|
31
|
+
purpose: "NextAuth.js catch-all route directory — must contain [...nextauth]/route.ts which exports both GET and POST handlers. All sign-in, sign-out, OAuth callbacks, CSRF protection, and provider redirects run through this single route. Never add business logic here."
|
|
32
|
+
recommended_dirs:
|
|
33
|
+
- path: src/lib
|
|
34
|
+
purpose: "Single authOptions export in auth.ts — provider configuration, session strategy, adapter wiring, jwt and session callbacks. Imported by the route handler, getServerSession calls, and middleware. Keeping authOptions in one file ensures all tokens are signed with the same secret."
|
|
35
|
+
- path: components
|
|
36
|
+
purpose: "Client Component wrappers that depend on next-auth/react hooks — primarily a Providers.tsx that wraps SessionProvider so the root layout stays a Server Component."
|
|
37
|
+
separation:
|
|
38
|
+
rules:
|
|
39
|
+
- concern: route_handler
|
|
40
|
+
belongs_in: app/api/auth
|
|
41
|
+
rule_text: "Create the NextAuth route handler at app/api/auth/[...nextauth]/route.ts. Import authOptions from src/lib/auth.ts — never inline provider config in the route file. The handler must export both GET and POST or sign-in and CSRF flows will break."
|
|
42
|
+
example: |
|
|
43
|
+
// app/api/auth/[...nextauth]/route.ts
|
|
44
|
+
import NextAuth from 'next-auth';
|
|
45
|
+
import { authOptions } from '@/lib/auth';
|
|
46
|
+
|
|
47
|
+
const handler = NextAuth(authOptions);
|
|
48
|
+
export { handler as GET, handler as POST };
|
|
49
|
+
indicators:
|
|
50
|
+
- "NextAuth("
|
|
51
|
+
- "[...nextauth]"
|
|
52
|
+
- "handler as GET"
|
|
53
|
+
- "handler as POST"
|
|
54
|
+
- concern: auth_config
|
|
55
|
+
belongs_in: src/lib
|
|
56
|
+
rule_text: "Define all providers, callbacks, adapter, and session strategy in src/lib/auth.ts as a single exported NextAuthOptions object. Every file that needs session data imports authOptions from this one location. The jwt and session callbacks must both be present to persist custom fields like user.id or user.role."
|
|
57
|
+
example: |
|
|
58
|
+
// src/lib/auth.ts — single authoritative NextAuth configuration
|
|
59
|
+
import { type NextAuthOptions } from 'next-auth';
|
|
60
|
+
import GithubProvider from 'next-auth/providers/github';
|
|
61
|
+
import { PrismaAdapter } from '@auth/prisma-adapter';
|
|
62
|
+
import { prisma } from '@/lib/db';
|
|
63
|
+
|
|
64
|
+
export const authOptions: NextAuthOptions = {
|
|
65
|
+
adapter: PrismaAdapter(prisma),
|
|
66
|
+
providers: [
|
|
67
|
+
GithubProvider({
|
|
68
|
+
clientId: process.env.GITHUB_ID!,
|
|
69
|
+
clientSecret: process.env.GITHUB_SECRET!,
|
|
70
|
+
}),
|
|
71
|
+
],
|
|
72
|
+
session: { strategy: 'database' },
|
|
73
|
+
secret: process.env.NEXTAUTH_SECRET,
|
|
74
|
+
callbacks: {
|
|
75
|
+
// Thread custom fields from DB user into session
|
|
76
|
+
session({ session, user }) {
|
|
77
|
+
if (session.user) {
|
|
78
|
+
session.user.id = user.id;
|
|
79
|
+
(session.user as any).role = (user as any).role;
|
|
80
|
+
}
|
|
81
|
+
return session;
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
indicators:
|
|
86
|
+
- "authOptions"
|
|
87
|
+
- "NextAuthOptions"
|
|
88
|
+
- "providers:"
|
|
89
|
+
- "callbacks:"
|
|
90
|
+
- "NEXTAUTH_SECRET"
|
|
91
|
+
- concern: session_access
|
|
92
|
+
belongs_in: app
|
|
93
|
+
rule_text: "In Server Components and API routes, use getServerSession(authOptions) to read the session. In Client Components, use useSession() from next-auth/react. Never read session cookies or JWT tokens manually — getServerSession reads the cookie but handles decryption and expiry."
|
|
94
|
+
example: |
|
|
95
|
+
// Server Component — no loading state, data available on first render
|
|
96
|
+
import { getServerSession } from 'next-auth';
|
|
97
|
+
import { authOptions } from '@/lib/auth';
|
|
98
|
+
import { redirect } from 'next/navigation';
|
|
99
|
+
|
|
100
|
+
export default async function ProtectedPage() {
|
|
101
|
+
const session = await getServerSession(authOptions);
|
|
102
|
+
if (!session) redirect('/api/auth/signin');
|
|
103
|
+
return <Dashboard user={session.user} />;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Client Component — shows loading state during hydration
|
|
107
|
+
'use client';
|
|
108
|
+
import { useSession } from 'next-auth/react';
|
|
109
|
+
export function UserMenu() {
|
|
110
|
+
const { data: session, status } = useSession();
|
|
111
|
+
if (status === 'loading') return <Spinner />;
|
|
112
|
+
if (!session) return <SignInButton />;
|
|
113
|
+
return <UserAvatar user={session.user} />;
|
|
114
|
+
}
|
|
115
|
+
indicators:
|
|
116
|
+
- "getServerSession"
|
|
117
|
+
- "useSession()"
|
|
118
|
+
- "from 'next-auth/react'"
|
|
119
|
+
- "authOptions"
|
|
120
|
+
- concern: middleware_protection
|
|
121
|
+
belongs_in: middleware.ts
|
|
122
|
+
rule_text: "Export next-auth/middleware as the default export to protect matching routes. Configure the matcher pattern to exclude /api/auth (the NextAuth handler itself), static assets, and public pages. Use the authorized callback in authOptions for role-based access control."
|
|
123
|
+
example: |
|
|
124
|
+
// middleware.ts — blanket protection for dashboard and protected API routes
|
|
125
|
+
export { default } from 'next-auth/middleware';
|
|
126
|
+
|
|
127
|
+
export const config = {
|
|
128
|
+
matcher: [
|
|
129
|
+
'/dashboard/:path*',
|
|
130
|
+
'/admin/:path*',
|
|
131
|
+
'/api/protected/:path*',
|
|
132
|
+
// Does NOT match: /api/auth (NextAuth handler), /sign-in, static files
|
|
133
|
+
],
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// For role-based access, add authorized callback in authOptions:
|
|
137
|
+
// callbacks: {
|
|
138
|
+
// authorized({ token, req }) {
|
|
139
|
+
// if (req.nextUrl.pathname.startsWith('/admin')) return token?.role === 'admin';
|
|
140
|
+
// return !!token;
|
|
141
|
+
// },
|
|
142
|
+
// }
|
|
143
|
+
indicators:
|
|
144
|
+
- "next-auth/middleware"
|
|
145
|
+
- "matcher:"
|
|
146
|
+
- "authorized("
|
|
147
|
+
- concern: session_provider
|
|
148
|
+
belongs_in: components
|
|
149
|
+
rule_text: "Wrap SessionProvider in a dedicated 'use client' component so the root layout stays a Server Component. Pass the server-fetched session as a prop to SessionProvider to eliminate the loading flicker that occurs when SessionProvider fetches the session client-side."
|
|
150
|
+
example: |
|
|
151
|
+
// components/providers.tsx — isolates the 'use client' boundary
|
|
152
|
+
'use client';
|
|
153
|
+
import { SessionProvider } from 'next-auth/react';
|
|
154
|
+
import type { Session } from 'next-auth';
|
|
155
|
+
|
|
156
|
+
export function Providers({
|
|
157
|
+
children,
|
|
158
|
+
session,
|
|
159
|
+
}: {
|
|
160
|
+
children: React.ReactNode;
|
|
161
|
+
session: Session | null;
|
|
162
|
+
}) {
|
|
163
|
+
return <SessionProvider session={session}>{children}</SessionProvider>;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// app/layout.tsx — Server Component, fetches session for SSR hydration
|
|
167
|
+
import { getServerSession } from 'next-auth';
|
|
168
|
+
import { authOptions } from '@/lib/auth';
|
|
169
|
+
import { Providers } from '@/components/providers';
|
|
170
|
+
|
|
171
|
+
export default async function RootLayout({ children }: { children: React.ReactNode }) {
|
|
172
|
+
const session = await getServerSession(authOptions);
|
|
173
|
+
return (
|
|
174
|
+
<html lang="en">
|
|
175
|
+
<body>
|
|
176
|
+
<Providers session={session}>{children}</Providers>
|
|
177
|
+
</body>
|
|
178
|
+
</html>
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
indicators:
|
|
182
|
+
- "SessionProvider"
|
|
183
|
+
- "session={session}"
|
|
184
|
+
- "from 'next-auth/react'"
|
|
185
|
+
patterns:
|
|
186
|
+
data_flow:
|
|
187
|
+
direction: "Request → Middleware (withAuth) → Server Component (getServerSession) → Service → DB"
|
|
188
|
+
rules:
|
|
189
|
+
- "Middleware protects routes before any rendering — pages don't need to re-check for middleware-covered paths."
|
|
190
|
+
- "Server Components call getServerSession(authOptions) — reads the encrypted cookie without an extra network call."
|
|
191
|
+
- "Client Components call useSession() — reads from SessionProvider context already in the page, zero extra requests."
|
|
192
|
+
- "authOptions is defined once in src/lib/auth.ts — imported by route handler, middleware, and all session accessors so they all use the same secret and config."
|
|
193
|
+
- "Custom session fields (user.id, user.role) must flow through both jwt callback (token enrichment) and session callback (token → session mapping)."
|
|
194
|
+
- "Database sessions (strategy: 'database') store the session in your DB via the adapter — JWT sessions store everything in the encrypted cookie."
|
|
195
|
+
error_handling:
|
|
196
|
+
recommended: "Check if session is null before accessing session.user properties — getServerSession() returns null for unauthenticated requests, not an error. Redirect to /api/auth/signin to let NextAuth handle the sign-in flow rather than a custom page."
|
|
197
|
+
naming:
|
|
198
|
+
config: "src/lib/auth.ts — single authOptions export, imported by all session consumers"
|
|
199
|
+
route_handler: "app/api/auth/[...nextauth]/route.ts — exports GET and POST"
|
|
200
|
+
session_provider: "components/providers.tsx — 'use client' wrapper for SessionProvider"
|
|
201
|
+
session_helper: "getServerSession(authOptions) in Server Components; useSession() in Client Components"
|
|
202
|
+
anti_patterns:
|
|
203
|
+
- id: missing_secret
|
|
204
|
+
severity: critical
|
|
205
|
+
description: "Running NextAuth without NEXTAUTH_SECRET set to a strong random value. In development, NextAuth uses an insecure default and issues a warning. In production, it throws a hard startup error. Without a strong secret, session tokens can be forged by brute-force."
|
|
206
|
+
bad_example: |
|
|
207
|
+
# .env — weak or missing secret
|
|
208
|
+
NEXTAUTH_SECRET=secret # too short, too predictable
|
|
209
|
+
# Or: NEXTAUTH_SECRET not set at all — uses insecure default in dev
|
|
210
|
+
good_example: |
|
|
211
|
+
# .env.local — strong random secret, 32+ characters
|
|
212
|
+
NEXTAUTH_SECRET=your-32-char-or-longer-random-secret-here
|
|
213
|
+
# Generate with: openssl rand -base64 32
|
|
214
|
+
# Or: node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
|
|
215
|
+
- id: session_not_checked_in_api
|
|
216
|
+
severity: warning
|
|
217
|
+
description: "API routes outside the middleware matcher that return sensitive data without calling getServerSession(). The middleware only protects matched paths — any API route not in the matcher must check the session itself."
|
|
218
|
+
bad_example: |
|
|
219
|
+
// ❌ No session check — any request (including unauthenticated) gets the data
|
|
220
|
+
export async function GET() {
|
|
221
|
+
const users = await db.user.findMany({ select: { email: true, salary: true } });
|
|
222
|
+
return Response.json(users);
|
|
223
|
+
}
|
|
224
|
+
good_example: |
|
|
225
|
+
// ✓ Explicit session check before accessing protected data
|
|
226
|
+
import { getServerSession } from 'next-auth';
|
|
227
|
+
import { authOptions } from '@/lib/auth';
|
|
228
|
+
export async function GET() {
|
|
229
|
+
const session = await getServerSession(authOptions);
|
|
230
|
+
if (!session) return new Response('Unauthorized', { status: 401 });
|
|
231
|
+
const users = await db.user.findMany({ select: { email: true } });
|
|
232
|
+
return Response.json(users);
|
|
233
|
+
}
|
|
234
|
+
- id: duplicated_auth_options
|
|
235
|
+
severity: warning
|
|
236
|
+
description: "Defining authOptions inline in multiple API routes instead of importing from src/lib/auth.ts. Each definition can have a different secret or provider config — tokens signed by one definition are rejected by another, causing intermittent auth failures that are hard to debug."
|
|
237
|
+
bad_example: |
|
|
238
|
+
// ❌ authOptions defined inline in the route file
|
|
239
|
+
// app/api/auth/[...nextauth]/route.ts
|
|
240
|
+
const handler = NextAuth({
|
|
241
|
+
providers: [GithubProvider({ clientId: '...', clientSecret: '...' })],
|
|
242
|
+
secret: process.env.NEXTAUTH_SECRET,
|
|
243
|
+
// Different callbacks or session strategy in another file → token mismatch
|
|
244
|
+
});
|
|
245
|
+
good_example: |
|
|
246
|
+
// ✓ Import from the single source of truth
|
|
247
|
+
import { authOptions } from '@/lib/auth';
|
|
248
|
+
const handler = NextAuth(authOptions);
|
|
249
|
+
export { handler as GET, handler as POST };
|
|
250
|
+
- id: custom_fields_not_in_callbacks
|
|
251
|
+
severity: warning
|
|
252
|
+
description: "Extending the Session type with custom fields (user.id, user.role) but not adding both a jwt callback and a session callback to populate them. The TypeScript types compile, but the fields are undefined at runtime because the values are never written into the token or session object."
|
|
253
|
+
bad_example: |
|
|
254
|
+
// next-auth.d.ts: Session.user.role declared as string
|
|
255
|
+
// authOptions — callbacks are missing entirely
|
|
256
|
+
export const authOptions: NextAuthOptions = {
|
|
257
|
+
providers: [...],
|
|
258
|
+
// session.user.role is always undefined at runtime
|
|
259
|
+
};
|
|
260
|
+
good_example: |
|
|
261
|
+
// ✓ Both callbacks required: jwt enriches the token, session maps token → session
|
|
262
|
+
callbacks: {
|
|
263
|
+
async jwt({ token, user }) {
|
|
264
|
+
if (user) token.role = (user as any).role; // runs on sign-in only
|
|
265
|
+
return token;
|
|
266
|
+
},
|
|
267
|
+
async session({ session, token }) {
|
|
268
|
+
if (session.user) session.user.role = token.role as string; // every request
|
|
269
|
+
return session;
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
- id: credentials_provider_plain_text
|
|
273
|
+
severity: critical
|
|
274
|
+
description: "Using CredentialsProvider and comparing passwords as plain text. If the database is breached, every user's password is immediately exposed. Passwords must always be stored as bcrypt or argon2 hashes."
|
|
275
|
+
bad_example: |
|
|
276
|
+
// ❌ Plain text comparison — catastrophic on any database breach
|
|
277
|
+
CredentialsProvider({
|
|
278
|
+
authorize: async (credentials) => {
|
|
279
|
+
const user = await db.user.findFirst({ where: { email: credentials?.email } });
|
|
280
|
+
if (user?.password === credentials?.password) return user; // raw string compare
|
|
281
|
+
return null;
|
|
282
|
+
},
|
|
283
|
+
})
|
|
284
|
+
good_example: |
|
|
285
|
+
// ✓ bcrypt — one-way hash, breach-safe
|
|
286
|
+
import bcrypt from 'bcryptjs';
|
|
287
|
+
CredentialsProvider({
|
|
288
|
+
authorize: async (credentials) => {
|
|
289
|
+
const user = await db.user.findFirst({ where: { email: credentials?.email } });
|
|
290
|
+
if (!user?.passwordHash) return null;
|
|
291
|
+
const valid = await bcrypt.compare(credentials?.password ?? '', user.passwordHash);
|
|
292
|
+
return valid ? { id: user.id, email: user.email } : null;
|
|
293
|
+
},
|
|
294
|
+
})
|
|
295
|
+
- id: adapter_with_jwt_strategy
|
|
296
|
+
severity: warning
|
|
297
|
+
description: "Configuring a database adapter (e.g., PrismaAdapter) with session: { strategy: 'jwt' }. With JWT strategy, NextAuth does not write sessions to the database — the adapter is wired but ignored for session storage, causing confusion and wasted DB connections."
|
|
298
|
+
bad_example: |
|
|
299
|
+
// ❌ Adapter installed but JWT strategy means sessions never hit the DB
|
|
300
|
+
export const authOptions: NextAuthOptions = {
|
|
301
|
+
adapter: PrismaAdapter(prisma),
|
|
302
|
+
session: { strategy: 'jwt' }, // adapter's Session table is never written
|
|
303
|
+
};
|
|
304
|
+
good_example: |
|
|
305
|
+
// ✓ Use database strategy when you have an adapter
|
|
306
|
+
adapter: PrismaAdapter(prisma),
|
|
307
|
+
session: { strategy: 'database' },
|
|
308
|
+
// — OR — remove the adapter entirely for JWT-only sessions (no DB table needed)
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
schema_version: "2.0.0"
|
|
2
|
+
id: playwright-e2e
|
|
3
|
+
name: "Playwright E2E"
|
|
4
|
+
version: "2.0.0"
|
|
5
|
+
description: "End-to-end testing with Playwright — Page Object Models, storageState-based auth fixtures, semantic selectors, baseURL from config, globalSetup for one-time login, and CI-optimized retries."
|
|
6
|
+
category: pattern
|
|
7
|
+
language: javascript
|
|
8
|
+
frameworks:
|
|
9
|
+
- playwright
|
|
10
|
+
dependencies:
|
|
11
|
+
none:
|
|
12
|
+
- "@jest/globals"
|
|
13
|
+
detection:
|
|
14
|
+
dependencies:
|
|
15
|
+
any:
|
|
16
|
+
- "@playwright/test"
|
|
17
|
+
source_indicators:
|
|
18
|
+
- "from '@playwright/test'"
|
|
19
|
+
- "test.describe("
|
|
20
|
+
- "page.goto("
|
|
21
|
+
- "expect(page)"
|
|
22
|
+
- "storageState"
|
|
23
|
+
structure:
|
|
24
|
+
required_dirs:
|
|
25
|
+
- path: tests/e2e
|
|
26
|
+
purpose: "E2E test specs organized by user flow or feature. Each spec file covers one major feature (e.g., auth.spec.ts, checkout.spec.ts). Test specs import Page Objects and fixtures — they never contain raw selectors or setup logic."
|
|
27
|
+
recommended_dirs:
|
|
28
|
+
- path: tests/e2e/pages
|
|
29
|
+
purpose: "Page Object Model classes — one class per page or major UI component. The POM encapsulates all selectors and user-facing actions. Test specs call POM methods (loginPage.login(), checkoutPage.placeOrder()) — they never call page.locator() or page.getByRole() directly."
|
|
30
|
+
- path: tests/e2e/fixtures
|
|
31
|
+
purpose: "Custom Playwright fixtures extending the base `test` — authenticated page fixtures that load storageState from globalSetup. Import this extended `test` in all specs that need auth instead of the default @playwright/test export."
|
|
32
|
+
- path: tests/e2e/global-setup
|
|
33
|
+
purpose: "globalSetup.ts — one-time setup that runs before all specs. Performs the login flow once, saves storageState to a file (e.g., .auth/user.json), and reuses it across all tests via fixtures. Eliminates repeated login in every spec file."
|
|
34
|
+
separation:
|
|
35
|
+
rules:
|
|
36
|
+
- concern: page_object_models
|
|
37
|
+
belongs_in: tests/e2e/pages
|
|
38
|
+
rule_text: "Encapsulate all page selectors and user-action sequences in Page Object Model classes. Each POM class receives a Playwright Page object in its constructor. Methods represent user intentions (login(), submitForm(), navigateToSettings()) — not individual click/fill operations. Tests call POM methods and assert on outcomes."
|
|
39
|
+
example: |
|
|
40
|
+
// tests/e2e/pages/login.page.ts
|
|
41
|
+
import type { Page } from '@playwright/test';
|
|
42
|
+
|
|
43
|
+
export class LoginPage {
|
|
44
|
+
constructor(private readonly page: Page) {}
|
|
45
|
+
|
|
46
|
+
async goto() {
|
|
47
|
+
await this.page.goto('/login');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async login(email: string, password: string) {
|
|
51
|
+
await this.page.getByLabel('Email').fill(email);
|
|
52
|
+
await this.page.getByLabel('Password').fill(password);
|
|
53
|
+
await this.page.getByRole('button', { name: 'Sign in' }).click();
|
|
54
|
+
// Wait for navigation to complete
|
|
55
|
+
await this.page.waitForURL('**/dashboard');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async getErrorMessage() {
|
|
59
|
+
return this.page.getByRole('alert').textContent();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// tests/e2e/auth.spec.ts — test calls POM, not selectors
|
|
64
|
+
const loginPage = new LoginPage(page);
|
|
65
|
+
await loginPage.goto();
|
|
66
|
+
await loginPage.login('user@test.com', 'password');
|
|
67
|
+
await expect(page).toHaveURL('/dashboard');
|
|
68
|
+
indicators:
|
|
69
|
+
- "class LoginPage"
|
|
70
|
+
- "class DashboardPage"
|
|
71
|
+
- "new LoginPage(page)"
|
|
72
|
+
- concern: shared_fixtures
|
|
73
|
+
belongs_in: tests/e2e/fixtures
|
|
74
|
+
rule_text: "Create custom Playwright fixtures that extend the base test with authenticated page contexts. Load storageState (saved by globalSetup) into the browser context — this skips the login UI entirely for specs that need auth. All authenticated specs import the custom test, not the default one from @playwright/test."
|
|
75
|
+
example: |
|
|
76
|
+
// tests/e2e/fixtures/auth.fixture.ts
|
|
77
|
+
import { test as base, type Page } from '@playwright/test';
|
|
78
|
+
import path from 'path';
|
|
79
|
+
|
|
80
|
+
type AuthFixtures = { authedPage: Page };
|
|
81
|
+
|
|
82
|
+
export const test = base.extend<AuthFixtures>({
|
|
83
|
+
authedPage: async ({ browser }, use) => {
|
|
84
|
+
const context = await browser.newContext({
|
|
85
|
+
storageState: path.resolve('.auth/user.json'), // saved by globalSetup
|
|
86
|
+
});
|
|
87
|
+
const page = await context.newPage();
|
|
88
|
+
await use(page);
|
|
89
|
+
await context.close();
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
export { expect } from '@playwright/test';
|
|
94
|
+
|
|
95
|
+
// tests/e2e/dashboard.spec.ts — uses auth fixture
|
|
96
|
+
import { test, expect } from '../fixtures/auth.fixture';
|
|
97
|
+
test('shows user data', async ({ authedPage }) => {
|
|
98
|
+
await authedPage.goto('/dashboard');
|
|
99
|
+
await expect(authedPage.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
|
|
100
|
+
});
|
|
101
|
+
indicators:
|
|
102
|
+
- "base.extend"
|
|
103
|
+
- "storageState"
|
|
104
|
+
- "authedPage"
|
|
105
|
+
- concern: selectors
|
|
106
|
+
belongs_in: tests/e2e/pages
|
|
107
|
+
rule_text: "Use semantic, user-facing selectors in this priority order: (1) getByRole with name — most resilient, matches what screen readers see; (2) getByLabel — for form inputs; (3) getByTestId — for complex interactive components where role/label is impractical; (4) getByText — for links and non-interactive text. Never use CSS class selectors or XPath."
|
|
108
|
+
example: |
|
|
109
|
+
// ✓ Role-based — matches semantic HTML, resilient to style changes
|
|
110
|
+
await page.getByRole('button', { name: 'Submit' }).click();
|
|
111
|
+
await page.getByRole('link', { name: 'View profile' }).click();
|
|
112
|
+
|
|
113
|
+
// ✓ Label-based — for form inputs
|
|
114
|
+
await page.getByLabel('Email address').fill('user@test.com');
|
|
115
|
+
|
|
116
|
+
// ✓ data-testid — for complex interactive widgets without clear role
|
|
117
|
+
await page.getByTestId('date-picker-input').click();
|
|
118
|
+
|
|
119
|
+
// ❌ CSS class — breaks on class rename
|
|
120
|
+
// await page.locator('.btn-primary').click();
|
|
121
|
+
// ❌ XPath — brittle to DOM structure changes
|
|
122
|
+
// await page.locator('//div[@class="form"]/button[1]').click();
|
|
123
|
+
indicators:
|
|
124
|
+
- "getByRole"
|
|
125
|
+
- "getByLabel"
|
|
126
|
+
- "getByTestId"
|
|
127
|
+
- concern: config
|
|
128
|
+
belongs_in: playwright.config.ts
|
|
129
|
+
rule_text: "Configure baseURL, retries, timeout, and reporter in playwright.config.ts. Never hardcode localhost URLs in test files — use page.goto('/path') which Playwright prepends with baseURL. Use environment variables for baseURL to support testing in different environments (local, staging, CI)."
|
|
130
|
+
example: |
|
|
131
|
+
// playwright.config.ts
|
|
132
|
+
import { defineConfig, devices } from '@playwright/test';
|
|
133
|
+
|
|
134
|
+
export default defineConfig({
|
|
135
|
+
testDir: './tests/e2e',
|
|
136
|
+
globalSetup: './tests/e2e/global-setup/auth.setup.ts',
|
|
137
|
+
fullyParallel: true,
|
|
138
|
+
forbidOnly: !!process.env.CI,
|
|
139
|
+
retries: process.env.CI ? 2 : 0,
|
|
140
|
+
reporter: process.env.CI ? 'github' : 'list',
|
|
141
|
+
use: {
|
|
142
|
+
baseURL: process.env.BASE_URL ?? 'http://localhost:3000',
|
|
143
|
+
trace: 'on-first-retry', // captures trace on flaky tests in CI
|
|
144
|
+
screenshot: 'only-on-failure',
|
|
145
|
+
},
|
|
146
|
+
projects: [
|
|
147
|
+
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
|
|
148
|
+
],
|
|
149
|
+
});
|
|
150
|
+
indicators:
|
|
151
|
+
- "playwright.config.ts"
|
|
152
|
+
- "baseURL"
|
|
153
|
+
- "retries"
|
|
154
|
+
patterns:
|
|
155
|
+
data_flow:
|
|
156
|
+
direction: "globalSetup (login once) → storageState file → authedPage fixture → Test spec → POM methods → Browser → Application"
|
|
157
|
+
rules:
|
|
158
|
+
- "globalSetup logs in once and saves cookies/localStorage to .auth/user.json."
|
|
159
|
+
- "Auth fixture loads .auth/user.json — test specs get authenticated page with no login overhead."
|
|
160
|
+
- "Test specs call POM methods — never raw Playwright selectors."
|
|
161
|
+
- "baseURL in playwright.config.ts — page.goto('/path') works in all environments."
|
|
162
|
+
- "Retries and trace collection in CI — flaky tests get 2 retries before failing."
|
|
163
|
+
error_handling:
|
|
164
|
+
recommended: "Use trace: 'on-first-retry' to capture full browser trace on flaky tests. Use screenshot: 'only-on-failure' to get a visual snapshot when a test fails in CI."
|
|
165
|
+
naming:
|
|
166
|
+
specs: "tests/e2e/[feature].spec.ts"
|
|
167
|
+
page_objects: "tests/e2e/pages/[page].page.ts"
|
|
168
|
+
fixtures: "tests/e2e/fixtures/auth.fixture.ts"
|
|
169
|
+
global_setup: "tests/e2e/global-setup/auth.setup.ts"
|
|
170
|
+
storage_state: ".auth/user.json"
|
|
171
|
+
anti_patterns:
|
|
172
|
+
- id: raw_selectors_in_tests
|
|
173
|
+
severity: warning
|
|
174
|
+
description: "Using CSS selectors or XPath directly in test spec files instead of Page Object Model methods. When the UI changes (class rename, DOM restructure), every test that uses that selector breaks — with POMs, only one file needs updating."
|
|
175
|
+
bad_example: |
|
|
176
|
+
// ❌ Raw selectors in test spec — brittle, hard to maintain
|
|
177
|
+
test('user can log in', async ({ page }) => {
|
|
178
|
+
await page.goto('/login');
|
|
179
|
+
await page.locator('.login-form input[type="email"]').fill('user@test.com');
|
|
180
|
+
await page.locator('#submit-btn').click();
|
|
181
|
+
await expect(page.locator('.dashboard-header')).toBeVisible();
|
|
182
|
+
});
|
|
183
|
+
good_example: |
|
|
184
|
+
// ✓ POM method — one place to update when UI changes
|
|
185
|
+
test('user can log in', async ({ page }) => {
|
|
186
|
+
const loginPage = new LoginPage(page);
|
|
187
|
+
await loginPage.goto();
|
|
188
|
+
await loginPage.login('user@test.com', 'password');
|
|
189
|
+
await expect(page).toHaveURL('/dashboard');
|
|
190
|
+
});
|
|
191
|
+
- id: auth_in_every_test
|
|
192
|
+
severity: warning
|
|
193
|
+
description: "Performing the full login flow (goto /login, fill email, fill password, click submit) at the start of every authenticated test. A full login takes 1-3 seconds per test — 100 authenticated tests = 100-300 extra seconds. Use globalSetup + storageState to log in once."
|
|
194
|
+
bad_example: |
|
|
195
|
+
// ❌ Full login flow in every test — multiplies test execution time
|
|
196
|
+
test('view dashboard', async ({ page }) => {
|
|
197
|
+
await page.goto('/login');
|
|
198
|
+
await page.fill('#email', 'user@test.com');
|
|
199
|
+
await page.fill('#password', 'Password123');
|
|
200
|
+
await page.click('[type="submit"]');
|
|
201
|
+
await page.waitForURL('/dashboard');
|
|
202
|
+
// Now the actual test starts...
|
|
203
|
+
});
|
|
204
|
+
good_example: |
|
|
205
|
+
// ✓ Auth fixture provides pre-authenticated page — login ran once in globalSetup
|
|
206
|
+
test('view dashboard', async ({ authedPage }) => {
|
|
207
|
+
await authedPage.goto('/dashboard');
|
|
208
|
+
await expect(authedPage.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
|
|
209
|
+
});
|
|
210
|
+
- id: no_retries_in_ci
|
|
211
|
+
severity: warning
|
|
212
|
+
description: "Running Playwright in CI without retries — a single transient network condition, animation timing issue, or server cold start causes the entire suite to fail. Tests that pass locally may fail in CI due to slower execution."
|
|
213
|
+
bad_example: |
|
|
214
|
+
// playwright.config.ts — no CI configuration
|
|
215
|
+
export default defineConfig({
|
|
216
|
+
use: { baseURL: 'http://localhost:3000' },
|
|
217
|
+
// No retries, no CI reporter, no trace collection
|
|
218
|
+
});
|
|
219
|
+
good_example: |
|
|
220
|
+
export default defineConfig({
|
|
221
|
+
retries: process.env.CI ? 2 : 0,
|
|
222
|
+
reporter: process.env.CI ? 'github' : 'list',
|
|
223
|
+
use: {
|
|
224
|
+
baseURL: process.env.BASE_URL ?? 'http://localhost:3000',
|
|
225
|
+
trace: 'on-first-retry',
|
|
226
|
+
screenshot: 'only-on-failure',
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
- id: hardcoded_base_url_in_goto
|
|
230
|
+
severity: warning
|
|
231
|
+
description: "Using hardcoded localhost URLs in page.goto() calls instead of relative paths. Hardcoded URLs break when running tests against staging, CI, or any non-localhost environment. Configure baseURL once in playwright.config.ts."
|
|
232
|
+
bad_example: |
|
|
233
|
+
// ❌ Hardcoded URL — only works on localhost:3000
|
|
234
|
+
await page.goto('http://localhost:3000/dashboard');
|
|
235
|
+
await page.goto('http://localhost:3000/login');
|
|
236
|
+
good_example: |
|
|
237
|
+
// ✓ Relative path — Playwright prepends baseURL from playwright.config.ts
|
|
238
|
+
await page.goto('/dashboard');
|
|
239
|
+
await page.goto('/login');
|
|
240
|
+
// baseURL is configured per-environment: local=localhost:3000, CI=$BASE_URL
|
|
241
|
+
- id: missing_global_setup
|
|
242
|
+
severity: warning
|
|
243
|
+
description: "Re-logging in across every spec file instead of using globalSetup to create a storageState file once. For a test suite with 50 specs all needing authentication, this means 50 complete login flows instead of 1."
|
|
244
|
+
bad_example: |
|
|
245
|
+
// ❌ Each spec file logs in separately — N login flows for N specs
|
|
246
|
+
// auth.spec.ts: logs in at the top
|
|
247
|
+
// dashboard.spec.ts: logs in again
|
|
248
|
+
// settings.spec.ts: logs in again
|
|
249
|
+
// 50 spec files × 2 seconds login = 100 seconds wasted on login
|
|
250
|
+
good_example: |
|
|
251
|
+
// ✓ globalSetup logs in once, saves cookies to .auth/user.json
|
|
252
|
+
// tests/e2e/global-setup/auth.setup.ts
|
|
253
|
+
async function globalSetup() {
|
|
254
|
+
const { chromium } = require('@playwright/test');
|
|
255
|
+
const browser = await chromium.launch();
|
|
256
|
+
const page = await browser.newPage();
|
|
257
|
+
await page.goto('/login');
|
|
258
|
+
await page.getByLabel('Email').fill(process.env.TEST_USER_EMAIL!);
|
|
259
|
+
await page.getByLabel('Password').fill(process.env.TEST_USER_PASSWORD!);
|
|
260
|
+
await page.getByRole('button', { name: 'Sign in' }).click();
|
|
261
|
+
await page.waitForURL('**/dashboard');
|
|
262
|
+
await page.context().storageState({ path: '.auth/user.json' });
|
|
263
|
+
await browser.close();
|
|
264
|
+
}
|
|
265
|
+
export default globalSetup;
|