workos 0.6.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -15
- package/dist/bin.js +151 -0
- package/dist/bin.js.map +1 -1
- package/dist/cli.config.d.ts +1 -0
- package/dist/cli.config.js +1 -0
- package/dist/cli.config.js.map +1 -1
- package/dist/commands/doctor.d.ts +1 -0
- package/dist/commands/doctor.js +1 -0
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/env.d.ts +9 -0
- package/dist/commands/env.js +188 -0
- package/dist/commands/env.js.map +1 -0
- package/dist/commands/organization.d.ts +18 -0
- package/dist/commands/organization.js +142 -0
- package/dist/commands/organization.js.map +1 -0
- package/dist/commands/user.d.ts +19 -0
- package/dist/commands/user.js +128 -0
- package/dist/commands/user.js.map +1 -0
- package/dist/dashboard/components/CompletionView.js +5 -1
- package/dist/dashboard/components/CompletionView.js.map +1 -1
- package/dist/doctor/agent-prompt.d.ts +10 -0
- package/dist/doctor/agent-prompt.js +190 -0
- package/dist/doctor/agent-prompt.js.map +1 -0
- package/dist/doctor/checks/ai-analysis.d.ts +9 -0
- package/dist/doctor/checks/ai-analysis.js +122 -0
- package/dist/doctor/checks/ai-analysis.js.map +1 -0
- package/dist/doctor/checks/auth-patterns.d.ts +2 -0
- package/dist/doctor/checks/auth-patterns.js +530 -0
- package/dist/doctor/checks/auth-patterns.js.map +1 -0
- package/dist/doctor/checks/environment.js +22 -4
- package/dist/doctor/checks/environment.js.map +1 -1
- package/dist/doctor/checks/framework.js +27 -8
- package/dist/doctor/checks/framework.js.map +1 -1
- package/dist/doctor/checks/language.d.ts +6 -0
- package/dist/doctor/checks/language.js +104 -0
- package/dist/doctor/checks/language.js.map +1 -0
- package/dist/doctor/checks/sdk.js +113 -22
- package/dist/doctor/checks/sdk.js.map +1 -1
- package/dist/doctor/index.js +26 -3
- package/dist/doctor/index.js.map +1 -1
- package/dist/doctor/issues.js +22 -1
- package/dist/doctor/issues.js.map +1 -1
- package/dist/doctor/output.js +85 -18
- package/dist/doctor/output.js.map +1 -1
- package/dist/doctor/types.d.ts +38 -0
- package/dist/doctor/types.js.map +1 -1
- package/dist/lib/adapters/cli-adapter.js +4 -14
- package/dist/lib/adapters/cli-adapter.js.map +1 -1
- package/dist/lib/adapters/dashboard-adapter.js +3 -16
- package/dist/lib/adapters/dashboard-adapter.js.map +1 -1
- package/dist/lib/api-key.d.ts +13 -0
- package/dist/lib/api-key.js +26 -0
- package/dist/lib/api-key.js.map +1 -0
- package/dist/lib/config-store.d.ts +27 -0
- package/dist/lib/config-store.js +142 -0
- package/dist/lib/config-store.js.map +1 -0
- package/dist/lib/credential-store.d.ts +4 -0
- package/dist/lib/credential-store.js +47 -11
- package/dist/lib/credential-store.js.map +1 -1
- package/dist/lib/credentials.d.ts +1 -1
- package/dist/lib/credentials.js +1 -1
- package/dist/lib/credentials.js.map +1 -1
- package/dist/lib/run-with-core.js +23 -2
- package/dist/lib/run-with-core.js.map +1 -1
- package/dist/lib/settings.d.ts +1 -0
- package/dist/lib/settings.js.map +1 -1
- package/dist/lib/validation/build-validator.js +0 -1
- package/dist/lib/validation/build-validator.js.map +1 -1
- package/dist/lib/validation/quick-checks.js +0 -1
- package/dist/lib/validation/quick-checks.js.map +1 -1
- package/dist/lib/workos-api.d.ts +30 -0
- package/dist/lib/workos-api.js +69 -0
- package/dist/lib/workos-api.js.map +1 -0
- package/dist/utils/cli-symbols.d.ts +1 -1
- package/dist/utils/lock-art.d.ts +4 -0
- package/dist/utils/lock-art.js +73 -0
- package/dist/utils/lock-art.js.map +1 -0
- package/dist/utils/package-json.d.ts +1 -0
- package/dist/utils/package-json.js +11 -0
- package/dist/utils/package-json.js.map +1 -1
- package/dist/utils/summary-box.d.ts +18 -0
- package/dist/utils/summary-box.js +148 -0
- package/dist/utils/summary-box.js.map +1 -0
- package/dist/utils/table.d.ts +5 -0
- package/dist/utils/table.js +18 -0
- package/dist/utils/table.js.map +1 -0
- package/package.json +1 -1
- package/skills/workos-authkit-nextjs/SKILL.md +5 -5
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { join, relative } from 'node:path';
|
|
3
|
+
// --- Helpers ---
|
|
4
|
+
/** Return the first path that exists, or null */
|
|
5
|
+
function findFile(paths) {
|
|
6
|
+
for (const p of paths) {
|
|
7
|
+
if (existsSync(p))
|
|
8
|
+
return p;
|
|
9
|
+
}
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
/** Read file content safely, return null on any error */
|
|
13
|
+
function readFileSafe(filePath) {
|
|
14
|
+
try {
|
|
15
|
+
return readFileSync(filePath, 'utf-8');
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/** Test if a file exists and its content matches a regex */
|
|
22
|
+
function fileContains(filePath, pattern) {
|
|
23
|
+
const content = readFileSafe(filePath);
|
|
24
|
+
if (!content)
|
|
25
|
+
return false;
|
|
26
|
+
return pattern.test(content);
|
|
27
|
+
}
|
|
28
|
+
const SKIP_DIRS = new Set(['node_modules', '.next', '.turbo', 'dist', '.git', 'coverage']);
|
|
29
|
+
/**
|
|
30
|
+
* Find files matching a name pattern in a directory tree, limited to maxDepth levels.
|
|
31
|
+
* Skips node_modules, .next, dist, etc.
|
|
32
|
+
*/
|
|
33
|
+
function findFilesShallow(dir, namePattern, maxDepth = 3) {
|
|
34
|
+
const results = [];
|
|
35
|
+
if (!existsSync(dir))
|
|
36
|
+
return results;
|
|
37
|
+
function walk(currentDir, depth) {
|
|
38
|
+
if (depth > maxDepth)
|
|
39
|
+
return;
|
|
40
|
+
try {
|
|
41
|
+
const dirents = readdirSync(currentDir, { withFileTypes: true });
|
|
42
|
+
for (const dirent of dirents) {
|
|
43
|
+
const fullPath = join(currentDir, dirent.name);
|
|
44
|
+
if (dirent.isDirectory()) {
|
|
45
|
+
if (!SKIP_DIRS.has(dirent.name)) {
|
|
46
|
+
walk(fullPath, depth + 1);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
else if (dirent.isFile() && namePattern.test(dirent.name)) {
|
|
50
|
+
results.push(fullPath);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// Directory unreadable
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
walk(dir, 0);
|
|
59
|
+
return results;
|
|
60
|
+
}
|
|
61
|
+
function parseEnvFile(content) {
|
|
62
|
+
const result = {};
|
|
63
|
+
for (const line of content.split('\n')) {
|
|
64
|
+
const trimmed = line.trim();
|
|
65
|
+
if (!trimmed || trimmed.startsWith('#'))
|
|
66
|
+
continue;
|
|
67
|
+
const entry = trimmed.startsWith('export ') ? trimmed.slice(7) : trimmed;
|
|
68
|
+
const eqIndex = entry.indexOf('=');
|
|
69
|
+
if (eqIndex === -1)
|
|
70
|
+
continue;
|
|
71
|
+
const key = entry.slice(0, eqIndex).trim();
|
|
72
|
+
let value = entry.slice(eqIndex + 1).trim();
|
|
73
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
74
|
+
value = value.slice(1, -1);
|
|
75
|
+
}
|
|
76
|
+
result[key] = value;
|
|
77
|
+
}
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
/** Load all env vars from .env and .env.local files */
|
|
81
|
+
function loadProjectEnvRaw(installDir) {
|
|
82
|
+
const env = {};
|
|
83
|
+
for (const file of ['.env', '.env.local']) {
|
|
84
|
+
const content = readFileSafe(join(installDir, file));
|
|
85
|
+
if (content)
|
|
86
|
+
Object.assign(env, parseEnvFile(content));
|
|
87
|
+
}
|
|
88
|
+
return env;
|
|
89
|
+
}
|
|
90
|
+
// --- Individual Checks ---
|
|
91
|
+
/** Resolve the app directory root (app/ or src/app/) for Next.js */
|
|
92
|
+
function resolveAppDir(installDir) {
|
|
93
|
+
const srcApp = join(installDir, 'src', 'app');
|
|
94
|
+
if (existsSync(srcApp))
|
|
95
|
+
return srcApp;
|
|
96
|
+
const app = join(installDir, 'app');
|
|
97
|
+
if (existsSync(app))
|
|
98
|
+
return app;
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
function checkSignoutGetHandler(ctx) {
|
|
102
|
+
if (ctx.framework.name !== 'Next.js')
|
|
103
|
+
return [];
|
|
104
|
+
const appDir = resolveAppDir(ctx.installDir);
|
|
105
|
+
if (!appDir)
|
|
106
|
+
return [];
|
|
107
|
+
const routeFiles = findFilesShallow(appDir, /^route\.(ts|tsx|js|jsx)$/);
|
|
108
|
+
const signoutRoutes = routeFiles.filter((f) => /[/\\](sign-?out|logout)[/\\]/.test(f));
|
|
109
|
+
const findings = [];
|
|
110
|
+
const GET_EXPORT = /export\s+(async\s+)?function\s+GET|export\s+const\s+GET/;
|
|
111
|
+
for (const route of signoutRoutes) {
|
|
112
|
+
if (fileContains(route, GET_EXPORT)) {
|
|
113
|
+
findings.push({
|
|
114
|
+
code: 'SIGNOUT_GET_HANDLER',
|
|
115
|
+
severity: 'error',
|
|
116
|
+
message: 'Signout/logout route uses GET handler — vulnerable to CSRF and prefetch-triggered logouts',
|
|
117
|
+
filePath: relative(ctx.installDir, route),
|
|
118
|
+
remediation: 'Convert to a POST server action. GET routes with side effects are vulnerable to CSRF and will be triggered by Next.js link prefetching.',
|
|
119
|
+
docsUrl: 'https://workos.com/docs/authkit/sign-out',
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return findings;
|
|
124
|
+
}
|
|
125
|
+
function checkSignoutLinkPrefetch(ctx) {
|
|
126
|
+
if (ctx.framework.name !== 'Next.js')
|
|
127
|
+
return [];
|
|
128
|
+
const appDir = resolveAppDir(ctx.installDir);
|
|
129
|
+
if (!appDir)
|
|
130
|
+
return [];
|
|
131
|
+
const tsxFiles = findFilesShallow(appDir, /\.(tsx|jsx)$/);
|
|
132
|
+
// Match <Link>, <NextLink>, or other common aliases
|
|
133
|
+
const LINK_PATTERN = /<(?:Next)?Link\s[^>]*href\s*=\s*["'{`/]*(\/sign-?out|\/logout)/;
|
|
134
|
+
const findings = [];
|
|
135
|
+
for (const file of tsxFiles) {
|
|
136
|
+
if (fileContains(file, LINK_PATTERN)) {
|
|
137
|
+
findings.push({
|
|
138
|
+
code: 'SIGNOUT_LINK_PREFETCH',
|
|
139
|
+
severity: 'warning',
|
|
140
|
+
message: 'Link component points to signout/logout — Next.js will prefetch this in production, potentially triggering logouts',
|
|
141
|
+
filePath: relative(ctx.installDir, file),
|
|
142
|
+
remediation: 'Use a <form> with a server action or <button> with onClick handler instead of <Link> for signout.',
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return findings;
|
|
147
|
+
}
|
|
148
|
+
function checkMissingMiddleware(ctx) {
|
|
149
|
+
if (ctx.framework.name !== 'Next.js')
|
|
150
|
+
return [];
|
|
151
|
+
const middlewarePaths = [
|
|
152
|
+
'middleware.ts',
|
|
153
|
+
'middleware.js',
|
|
154
|
+
'proxy.ts',
|
|
155
|
+
'proxy.js',
|
|
156
|
+
'src/middleware.ts',
|
|
157
|
+
'src/middleware.js',
|
|
158
|
+
'src/proxy.ts',
|
|
159
|
+
'src/proxy.js',
|
|
160
|
+
].map((p) => join(ctx.installDir, p));
|
|
161
|
+
if (findFile(middlewarePaths))
|
|
162
|
+
return [];
|
|
163
|
+
return [
|
|
164
|
+
{
|
|
165
|
+
code: 'MISSING_MIDDLEWARE',
|
|
166
|
+
severity: 'error',
|
|
167
|
+
message: 'No middleware.ts or proxy.ts found — AuthKit session handling requires middleware',
|
|
168
|
+
remediation: 'Create middleware.ts at the project root with authkitMiddleware() from @workos-inc/authkit-nextjs.',
|
|
169
|
+
docsUrl: 'https://workos.com/docs/authkit/nextjs/middleware',
|
|
170
|
+
},
|
|
171
|
+
];
|
|
172
|
+
}
|
|
173
|
+
function checkMiddlewareWrongLocation(ctx) {
|
|
174
|
+
if (ctx.framework.name !== 'Next.js')
|
|
175
|
+
return [];
|
|
176
|
+
const wrongPaths = ['app/middleware.ts', 'app/middleware.js', 'src/app/middleware.ts', 'src/app/middleware.js'].map((p) => join(ctx.installDir, p));
|
|
177
|
+
const found = findFile(wrongPaths);
|
|
178
|
+
if (!found)
|
|
179
|
+
return [];
|
|
180
|
+
return [
|
|
181
|
+
{
|
|
182
|
+
code: 'MIDDLEWARE_WRONG_LOCATION',
|
|
183
|
+
severity: 'warning',
|
|
184
|
+
message: 'middleware.ts found inside app/ directory — must be at project root or src/',
|
|
185
|
+
filePath: relative(ctx.installDir, found),
|
|
186
|
+
remediation: 'Move middleware.ts to the project root (or src/ if using src/ directory).',
|
|
187
|
+
docsUrl: 'https://workos.com/docs/authkit/nextjs/middleware',
|
|
188
|
+
},
|
|
189
|
+
];
|
|
190
|
+
}
|
|
191
|
+
function checkMissingAuthKitProvider(ctx) {
|
|
192
|
+
const layoutPaths = [];
|
|
193
|
+
if (ctx.framework.name === 'Next.js') {
|
|
194
|
+
layoutPaths.push(join(ctx.installDir, 'app', 'layout.tsx'), join(ctx.installDir, 'app', 'layout.jsx'), join(ctx.installDir, 'src', 'app', 'layout.tsx'), join(ctx.installDir, 'src', 'app', 'layout.jsx'));
|
|
195
|
+
}
|
|
196
|
+
else if (ctx.framework.name === 'React Router' && ctx.framework.variant === 'declarative') {
|
|
197
|
+
layoutPaths.push(join(ctx.installDir, 'src', 'App.tsx'), join(ctx.installDir, 'src', 'App.jsx'), join(ctx.installDir, 'app', 'root.tsx'), join(ctx.installDir, 'app', 'root.jsx'));
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
return [];
|
|
201
|
+
}
|
|
202
|
+
const layoutFile = findFile(layoutPaths);
|
|
203
|
+
if (!layoutFile)
|
|
204
|
+
return []; // Can't check if layout doesn't exist
|
|
205
|
+
if (fileContains(layoutFile, /AuthKitProvider/))
|
|
206
|
+
return [];
|
|
207
|
+
return [
|
|
208
|
+
{
|
|
209
|
+
code: 'MISSING_AUTHKIT_PROVIDER',
|
|
210
|
+
severity: 'warning',
|
|
211
|
+
message: 'AuthKitProvider not found in root layout — required for AuthKit session management',
|
|
212
|
+
filePath: relative(ctx.installDir, layoutFile),
|
|
213
|
+
remediation: 'Wrap your app with <AuthKitProvider> in the root layout.',
|
|
214
|
+
docsUrl: 'https://workos.com/docs/authkit/nextjs/setup',
|
|
215
|
+
},
|
|
216
|
+
];
|
|
217
|
+
}
|
|
218
|
+
/** Extract callback path from redirect URI env vars or framework default */
|
|
219
|
+
function resolveCallbackPath(ctx) {
|
|
220
|
+
// Check env vars for actual redirect URI (including NEXT_PUBLIC_ variant)
|
|
221
|
+
const projectEnv = loadProjectEnvRaw(ctx.installDir);
|
|
222
|
+
const redirectUri = projectEnv.WORKOS_REDIRECT_URI ?? projectEnv.NEXT_PUBLIC_WORKOS_REDIRECT_URI ?? ctx.environment.redirectUri;
|
|
223
|
+
if (redirectUri) {
|
|
224
|
+
try {
|
|
225
|
+
return new URL(redirectUri).pathname;
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
// Invalid URL, fall through to framework default
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return ctx.framework.expectedCallbackPath ?? null;
|
|
232
|
+
}
|
|
233
|
+
function checkCallbackRouteMissing(ctx) {
|
|
234
|
+
const callbackPath = resolveCallbackPath(ctx);
|
|
235
|
+
if (!callbackPath)
|
|
236
|
+
return [];
|
|
237
|
+
// Build expected route file paths based on framework
|
|
238
|
+
const possiblePaths = [];
|
|
239
|
+
if (ctx.framework.name === 'Next.js') {
|
|
240
|
+
// app/auth/callback/route.ts (or src/app/)
|
|
241
|
+
const routeDir = callbackPath.replace(/^\//, ''); // 'auth/callback'
|
|
242
|
+
for (const prefix of ['app', 'src/app']) {
|
|
243
|
+
for (const ext of ['ts', 'tsx', 'js', 'jsx']) {
|
|
244
|
+
possiblePaths.push(join(ctx.installDir, prefix, routeDir, `route.${ext}`));
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
else if (ctx.framework.name === 'React Router') {
|
|
249
|
+
// Flat: app/routes/auth.callback.tsx Nested: app/routes/auth/callback.tsx
|
|
250
|
+
const segments = callbackPath.replace(/^\//, '').split('/');
|
|
251
|
+
const flat = segments.join('.');
|
|
252
|
+
const nested = segments.join('/');
|
|
253
|
+
for (const ext of ['tsx', 'jsx', 'ts', 'js']) {
|
|
254
|
+
possiblePaths.push(join(ctx.installDir, 'app', 'routes', `${flat}.${ext}`));
|
|
255
|
+
possiblePaths.push(join(ctx.installDir, 'app', 'routes', nested + `.${ext}`));
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
else if (ctx.framework.name === 'TanStack Start') {
|
|
259
|
+
// Modern flat: src/routes/api.auth.callback.tsx Legacy nested: app/routes/api/auth/callback.tsx
|
|
260
|
+
const segments = callbackPath.replace(/^\//, '').split('/');
|
|
261
|
+
const flat = segments.join('.');
|
|
262
|
+
const nested = segments.join('/');
|
|
263
|
+
for (const ext of ['tsx', 'jsx', 'ts', 'js']) {
|
|
264
|
+
possiblePaths.push(join(ctx.installDir, 'src', 'routes', `${flat}.${ext}`));
|
|
265
|
+
possiblePaths.push(join(ctx.installDir, 'app', 'routes', nested + `.${ext}`));
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (possiblePaths.length === 0)
|
|
269
|
+
return [];
|
|
270
|
+
if (findFile(possiblePaths))
|
|
271
|
+
return [];
|
|
272
|
+
return [
|
|
273
|
+
{
|
|
274
|
+
code: 'CALLBACK_ROUTE_MISSING',
|
|
275
|
+
severity: 'error',
|
|
276
|
+
message: `No callback route found at expected path ${callbackPath}`,
|
|
277
|
+
remediation: `Create the callback route handler at the path matching your WORKOS_REDIRECT_URI.`,
|
|
278
|
+
docsUrl: 'https://workos.com/docs/authkit/redirect-uri',
|
|
279
|
+
},
|
|
280
|
+
];
|
|
281
|
+
}
|
|
282
|
+
const CLIENT_ENV_PREFIXES = ['NEXT_PUBLIC_', 'VITE_', 'REACT_APP_', 'EXPO_PUBLIC_'];
|
|
283
|
+
function checkApiKeyLeakedToClient(ctx) {
|
|
284
|
+
const projectEnv = loadProjectEnvRaw(ctx.installDir);
|
|
285
|
+
const findings = [];
|
|
286
|
+
for (const [key, value] of Object.entries(projectEnv)) {
|
|
287
|
+
const hasClientPrefix = CLIENT_ENV_PREFIXES.some((prefix) => key.startsWith(prefix));
|
|
288
|
+
if (!hasClientPrefix)
|
|
289
|
+
continue;
|
|
290
|
+
const isApiKey = key.includes('WORKOS_API_KEY');
|
|
291
|
+
const isSecretValue = value?.startsWith('sk_test_') || value?.startsWith('sk_live_');
|
|
292
|
+
if (isApiKey || isSecretValue) {
|
|
293
|
+
findings.push({
|
|
294
|
+
code: 'API_KEY_LEAKED_TO_CLIENT',
|
|
295
|
+
severity: 'error',
|
|
296
|
+
message: `Secret API key exposed via client-accessible env var ${key}`,
|
|
297
|
+
remediation: `Remove the client prefix. WORKOS_API_KEY must be server-only (no NEXT_PUBLIC_, VITE_, REACT_APP_, or EXPO_PUBLIC_ prefix).`,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return findings;
|
|
302
|
+
}
|
|
303
|
+
function checkWrongCallbackLoader(ctx) {
|
|
304
|
+
if (ctx.framework.name !== 'React Router')
|
|
305
|
+
return [];
|
|
306
|
+
const callbackPath = ctx.framework.expectedCallbackPath;
|
|
307
|
+
if (!callbackPath)
|
|
308
|
+
return [];
|
|
309
|
+
const segments = callbackPath.replace(/^\//, '').split('/');
|
|
310
|
+
const flat = segments.join('.');
|
|
311
|
+
const nested = segments.join('/');
|
|
312
|
+
const possiblePaths = [];
|
|
313
|
+
for (const ext of ['tsx', 'jsx', 'ts', 'js']) {
|
|
314
|
+
possiblePaths.push(join(ctx.installDir, 'app', 'routes', `${flat}.${ext}`));
|
|
315
|
+
possiblePaths.push(join(ctx.installDir, 'app', 'routes', nested + `.${ext}`));
|
|
316
|
+
}
|
|
317
|
+
const callbackFile = findFile(possiblePaths);
|
|
318
|
+
if (!callbackFile)
|
|
319
|
+
return []; // No callback route to check
|
|
320
|
+
// authkitLoader is for regular routes; authLoader is for the callback
|
|
321
|
+
if (fileContains(callbackFile, /authkitLoader/) && !fileContains(callbackFile, /authLoader/)) {
|
|
322
|
+
return [
|
|
323
|
+
{
|
|
324
|
+
code: 'WRONG_CALLBACK_LOADER',
|
|
325
|
+
severity: 'warning',
|
|
326
|
+
message: 'Callback route uses authkitLoader instead of authLoader',
|
|
327
|
+
filePath: relative(ctx.installDir, callbackFile),
|
|
328
|
+
remediation: 'Use authLoader (not authkitLoader) for the callback route. authkitLoader is for regular routes that need auth context.',
|
|
329
|
+
},
|
|
330
|
+
];
|
|
331
|
+
}
|
|
332
|
+
return [];
|
|
333
|
+
}
|
|
334
|
+
function checkMissingRootAuthLoader(ctx) {
|
|
335
|
+
if (ctx.framework.name !== 'React Router')
|
|
336
|
+
return [];
|
|
337
|
+
const rootPaths = ['app/root.tsx', 'app/root.jsx', 'app/routes/_index.tsx', 'app/routes/_index.jsx'].map((p) => join(ctx.installDir, p));
|
|
338
|
+
const rootFile = findFile(rootPaths);
|
|
339
|
+
if (!rootFile)
|
|
340
|
+
return [];
|
|
341
|
+
if (fileContains(rootFile, /authkitLoader/))
|
|
342
|
+
return [];
|
|
343
|
+
return [
|
|
344
|
+
{
|
|
345
|
+
code: 'MISSING_ROOT_AUTH_LOADER',
|
|
346
|
+
severity: 'warning',
|
|
347
|
+
message: 'Root route does not use authkitLoader — child routes will not have auth context',
|
|
348
|
+
filePath: relative(ctx.installDir, rootFile),
|
|
349
|
+
remediation: 'Add authkitLoader to your root route so child routes can access auth state.',
|
|
350
|
+
},
|
|
351
|
+
];
|
|
352
|
+
}
|
|
353
|
+
function checkMissingAuthkitMiddleware(ctx) {
|
|
354
|
+
if (ctx.framework.name !== 'TanStack Start')
|
|
355
|
+
return [];
|
|
356
|
+
const startPaths = ['src/start.ts', 'src/start.tsx', 'app/start.ts', 'app/start.tsx'].map((p) => join(ctx.installDir, p));
|
|
357
|
+
const startFile = findFile(startPaths);
|
|
358
|
+
if (!startFile)
|
|
359
|
+
return []; // Can't check if start file doesn't exist
|
|
360
|
+
if (fileContains(startFile, /authkitMiddleware/))
|
|
361
|
+
return [];
|
|
362
|
+
return [
|
|
363
|
+
{
|
|
364
|
+
code: 'MISSING_AUTHKIT_MIDDLEWARE',
|
|
365
|
+
severity: 'warning',
|
|
366
|
+
message: 'start.ts does not reference authkitMiddleware — AuthKit session handling requires it',
|
|
367
|
+
filePath: relative(ctx.installDir, startFile),
|
|
368
|
+
remediation: 'Add authkitMiddleware to your start.ts server middleware configuration.',
|
|
369
|
+
},
|
|
370
|
+
];
|
|
371
|
+
}
|
|
372
|
+
function checkCookiePasswordTooShort(ctx) {
|
|
373
|
+
if (ctx.framework.name !== 'React Router' && ctx.framework.name !== 'TanStack Start')
|
|
374
|
+
return [];
|
|
375
|
+
const projectEnv = loadProjectEnvRaw(ctx.installDir);
|
|
376
|
+
const password = projectEnv.WORKOS_COOKIE_PASSWORD;
|
|
377
|
+
// Only warn if password is set but too short; missing password is a separate concern
|
|
378
|
+
if (!password || password.length >= 32)
|
|
379
|
+
return [];
|
|
380
|
+
return [
|
|
381
|
+
{
|
|
382
|
+
code: 'COOKIE_PASSWORD_TOO_SHORT',
|
|
383
|
+
severity: 'warning',
|
|
384
|
+
message: `WORKOS_COOKIE_PASSWORD is ${password.length} characters — minimum 32 required for secure encryption`,
|
|
385
|
+
remediation: 'Set WORKOS_COOKIE_PASSWORD to a random string of at least 32 characters.',
|
|
386
|
+
},
|
|
387
|
+
];
|
|
388
|
+
}
|
|
389
|
+
// --- Cross-language checks (run for ALL projects, not just JS/AuthKit) ---
|
|
390
|
+
const SOURCE_EXTENSIONS = /\.(ts|tsx|js|jsx|py|rb|go|java|kt|php|cs|swift|dart)$/;
|
|
391
|
+
function checkApiKeyInSource(ctx) {
|
|
392
|
+
const API_KEY_PATTERN = /sk_(test|live)_[A-Za-z0-9]{10,}/;
|
|
393
|
+
const sourceFiles = findFilesShallow(ctx.installDir, SOURCE_EXTENSIONS, 4);
|
|
394
|
+
const findings = [];
|
|
395
|
+
for (const file of sourceFiles) {
|
|
396
|
+
const content = readFileSafe(file);
|
|
397
|
+
if (!content)
|
|
398
|
+
continue;
|
|
399
|
+
if (API_KEY_PATTERN.test(content)) {
|
|
400
|
+
findings.push({
|
|
401
|
+
code: 'API_KEY_IN_SOURCE',
|
|
402
|
+
severity: 'error',
|
|
403
|
+
message: `WorkOS API key hardcoded in source file`,
|
|
404
|
+
filePath: relative(ctx.installDir, file),
|
|
405
|
+
remediation: 'Move the API key to an environment variable (WORKOS_API_KEY) and load it from .env or your secret manager.',
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
return findings;
|
|
410
|
+
}
|
|
411
|
+
function checkEnvFileNotGitignored(ctx) {
|
|
412
|
+
const envFiles = ['.env', '.env.local'].filter((f) => existsSync(join(ctx.installDir, f)));
|
|
413
|
+
if (envFiles.length === 0)
|
|
414
|
+
return [];
|
|
415
|
+
const gitignorePath = join(ctx.installDir, '.gitignore');
|
|
416
|
+
const gitignore = readFileSafe(gitignorePath);
|
|
417
|
+
const findings = [];
|
|
418
|
+
for (const envFile of envFiles) {
|
|
419
|
+
const isIgnored = gitignore !== null &&
|
|
420
|
+
gitignore.split('\n').some((line) => {
|
|
421
|
+
const trimmed = line.trim();
|
|
422
|
+
if (trimmed.startsWith('#') || trimmed === '')
|
|
423
|
+
return false;
|
|
424
|
+
return trimmed === envFile || trimmed === '.env*' || trimmed === '.env.*' || trimmed === '.env';
|
|
425
|
+
});
|
|
426
|
+
if (!isIgnored) {
|
|
427
|
+
findings.push({
|
|
428
|
+
code: 'ENV_FILE_NOT_GITIGNORED',
|
|
429
|
+
severity: 'warning',
|
|
430
|
+
message: `${envFile} is not in .gitignore — secrets may be committed to version control`,
|
|
431
|
+
filePath: envFile,
|
|
432
|
+
remediation: `Add ${envFile} to your .gitignore file.`,
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
return findings;
|
|
437
|
+
}
|
|
438
|
+
function checkMixedEnvironmentCredentials(ctx) {
|
|
439
|
+
const projectEnv = loadProjectEnvRaw(ctx.installDir);
|
|
440
|
+
const apiKey = projectEnv.WORKOS_API_KEY;
|
|
441
|
+
const redirectUri = projectEnv.WORKOS_REDIRECT_URI ?? projectEnv.NEXT_PUBLIC_WORKOS_REDIRECT_URI;
|
|
442
|
+
if (!apiKey || !redirectUri)
|
|
443
|
+
return [];
|
|
444
|
+
const isTestKey = apiKey.startsWith('sk_test_');
|
|
445
|
+
const isLiveKey = apiKey.startsWith('sk_live_');
|
|
446
|
+
let isProductionUri = false;
|
|
447
|
+
try {
|
|
448
|
+
const url = new URL(redirectUri);
|
|
449
|
+
isProductionUri = url.hostname !== 'localhost' && !url.hostname.startsWith('127.0.0.');
|
|
450
|
+
}
|
|
451
|
+
catch {
|
|
452
|
+
return [];
|
|
453
|
+
}
|
|
454
|
+
if (isTestKey && isProductionUri) {
|
|
455
|
+
return [
|
|
456
|
+
{
|
|
457
|
+
code: 'MIXED_ENVIRONMENT',
|
|
458
|
+
severity: 'warning',
|
|
459
|
+
message: 'Staging API key (sk_test_) used with a production redirect URI',
|
|
460
|
+
remediation: 'Use sk_live_ API key for production redirect URIs, or change the redirect URI to localhost for staging.',
|
|
461
|
+
},
|
|
462
|
+
];
|
|
463
|
+
}
|
|
464
|
+
if (isLiveKey && !isProductionUri) {
|
|
465
|
+
return [
|
|
466
|
+
{
|
|
467
|
+
code: 'MIXED_ENVIRONMENT',
|
|
468
|
+
severity: 'warning',
|
|
469
|
+
message: 'Production API key (sk_live_) used with a localhost redirect URI',
|
|
470
|
+
remediation: 'Use sk_test_ API key for localhost development, or update the redirect URI to your production domain.',
|
|
471
|
+
},
|
|
472
|
+
];
|
|
473
|
+
}
|
|
474
|
+
return [];
|
|
475
|
+
}
|
|
476
|
+
const CROSS_FRAMEWORK_CHECKS = [
|
|
477
|
+
checkApiKeyLeakedToClient,
|
|
478
|
+
checkApiKeyInSource,
|
|
479
|
+
checkEnvFileNotGitignored,
|
|
480
|
+
checkMixedEnvironmentCredentials,
|
|
481
|
+
];
|
|
482
|
+
const NEXTJS_CHECKS = [
|
|
483
|
+
checkSignoutGetHandler,
|
|
484
|
+
checkSignoutLinkPrefetch,
|
|
485
|
+
checkMissingMiddleware,
|
|
486
|
+
checkMiddlewareWrongLocation,
|
|
487
|
+
checkMissingAuthKitProvider,
|
|
488
|
+
checkCallbackRouteMissing,
|
|
489
|
+
];
|
|
490
|
+
const REACT_ROUTER_CHECKS = [
|
|
491
|
+
checkWrongCallbackLoader,
|
|
492
|
+
checkMissingRootAuthLoader,
|
|
493
|
+
checkMissingAuthKitProvider,
|
|
494
|
+
checkCallbackRouteMissing,
|
|
495
|
+
checkCookiePasswordTooShort,
|
|
496
|
+
];
|
|
497
|
+
const TANSTACK_CHECKS = [
|
|
498
|
+
checkMissingAuthkitMiddleware,
|
|
499
|
+
checkCallbackRouteMissing,
|
|
500
|
+
checkCookiePasswordTooShort,
|
|
501
|
+
];
|
|
502
|
+
export async function checkAuthPatterns(options, framework, environment, sdk) {
|
|
503
|
+
const ctx = {
|
|
504
|
+
framework,
|
|
505
|
+
environment,
|
|
506
|
+
sdk,
|
|
507
|
+
installDir: options.installDir,
|
|
508
|
+
};
|
|
509
|
+
const checks = [...CROSS_FRAMEWORK_CHECKS];
|
|
510
|
+
switch (framework.name) {
|
|
511
|
+
case 'Next.js':
|
|
512
|
+
checks.push(...NEXTJS_CHECKS);
|
|
513
|
+
break;
|
|
514
|
+
case 'React Router':
|
|
515
|
+
checks.push(...REACT_ROUTER_CHECKS);
|
|
516
|
+
break;
|
|
517
|
+
case 'TanStack Start':
|
|
518
|
+
checks.push(...TANSTACK_CHECKS);
|
|
519
|
+
break;
|
|
520
|
+
}
|
|
521
|
+
const findings = [];
|
|
522
|
+
for (const check of checks) {
|
|
523
|
+
findings.push(...check(ctx));
|
|
524
|
+
}
|
|
525
|
+
return {
|
|
526
|
+
checksRun: checks.length,
|
|
527
|
+
findings,
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
//# sourceMappingURL=auth-patterns.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-patterns.js","sourceRoot":"","sources":["../../../src/doctor/checks/auth-patterns.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, readFileSync, readdirSync } from 'node:fs';\nimport { join, relative } from 'node:path';\nimport type {\n AuthPatternFinding,\n AuthPatternInfo,\n FrameworkInfo,\n EnvironmentInfo,\n SdkInfo,\n DoctorOptions,\n} from '../types.js';\n\n// --- Helpers ---\n\n/** Return the first path that exists, or null */\nfunction findFile(paths: string[]): string | null {\n for (const p of paths) {\n if (existsSync(p)) return p;\n }\n return null;\n}\n\n/** Read file content safely, return null on any error */\nfunction readFileSafe(filePath: string): string | null {\n try {\n return readFileSync(filePath, 'utf-8');\n } catch {\n return null;\n }\n}\n\n/** Test if a file exists and its content matches a regex */\nfunction fileContains(filePath: string, pattern: RegExp): boolean {\n const content = readFileSafe(filePath);\n if (!content) return false;\n return pattern.test(content);\n}\n\nconst SKIP_DIRS = new Set(['node_modules', '.next', '.turbo', 'dist', '.git', 'coverage']);\n\n/**\n * Find files matching a name pattern in a directory tree, limited to maxDepth levels.\n * Skips node_modules, .next, dist, etc.\n */\nfunction findFilesShallow(dir: string, namePattern: RegExp, maxDepth = 3): string[] {\n const results: string[] = [];\n if (!existsSync(dir)) return results;\n\n function walk(currentDir: string, depth: number) {\n if (depth > maxDepth) return;\n try {\n const dirents = readdirSync(currentDir, { withFileTypes: true });\n for (const dirent of dirents) {\n const fullPath = join(currentDir, dirent.name);\n if (dirent.isDirectory()) {\n if (!SKIP_DIRS.has(dirent.name)) {\n walk(fullPath, depth + 1);\n }\n } else if (dirent.isFile() && namePattern.test(dirent.name)) {\n results.push(fullPath);\n }\n }\n } catch {\n // Directory unreadable\n }\n }\n\n walk(dir, 0);\n return results;\n}\n\nfunction parseEnvFile(content: string): Record<string, string> {\n const result: Record<string, string> = {};\n for (const line of content.split('\\n')) {\n const trimmed = line.trim();\n if (!trimmed || trimmed.startsWith('#')) continue;\n const entry = trimmed.startsWith('export ') ? trimmed.slice(7) : trimmed;\n const eqIndex = entry.indexOf('=');\n if (eqIndex === -1) continue;\n const key = entry.slice(0, eqIndex).trim();\n let value = entry.slice(eqIndex + 1).trim();\n if ((value.startsWith('\"') && value.endsWith('\"')) || (value.startsWith(\"'\") && value.endsWith(\"'\"))) {\n value = value.slice(1, -1);\n }\n result[key] = value;\n }\n return result;\n}\n\n/** Load all env vars from .env and .env.local files */\nfunction loadProjectEnvRaw(installDir: string): Record<string, string> {\n const env: Record<string, string> = {};\n for (const file of ['.env', '.env.local']) {\n const content = readFileSafe(join(installDir, file));\n if (content) Object.assign(env, parseEnvFile(content));\n }\n return env;\n}\n\n// --- Check Context ---\n\ninterface CheckContext {\n framework: FrameworkInfo;\n environment: EnvironmentInfo;\n sdk: SdkInfo;\n installDir: string;\n}\n\n// --- Individual Checks ---\n\n/** Resolve the app directory root (app/ or src/app/) for Next.js */\nfunction resolveAppDir(installDir: string): string | null {\n const srcApp = join(installDir, 'src', 'app');\n if (existsSync(srcApp)) return srcApp;\n const app = join(installDir, 'app');\n if (existsSync(app)) return app;\n return null;\n}\n\nfunction checkSignoutGetHandler(ctx: CheckContext): AuthPatternFinding[] {\n if (ctx.framework.name !== 'Next.js') return [];\n const appDir = resolveAppDir(ctx.installDir);\n if (!appDir) return [];\n\n const routeFiles = findFilesShallow(appDir, /^route\\.(ts|tsx|js|jsx)$/);\n const signoutRoutes = routeFiles.filter((f) => /[/\\\\](sign-?out|logout)[/\\\\]/.test(f));\n\n const findings: AuthPatternFinding[] = [];\n const GET_EXPORT = /export\\s+(async\\s+)?function\\s+GET|export\\s+const\\s+GET/;\n\n for (const route of signoutRoutes) {\n if (fileContains(route, GET_EXPORT)) {\n findings.push({\n code: 'SIGNOUT_GET_HANDLER',\n severity: 'error',\n message: 'Signout/logout route uses GET handler — vulnerable to CSRF and prefetch-triggered logouts',\n filePath: relative(ctx.installDir, route),\n remediation:\n 'Convert to a POST server action. GET routes with side effects are vulnerable to CSRF and will be triggered by Next.js link prefetching.',\n docsUrl: 'https://workos.com/docs/authkit/sign-out',\n });\n }\n }\n return findings;\n}\n\nfunction checkSignoutLinkPrefetch(ctx: CheckContext): AuthPatternFinding[] {\n if (ctx.framework.name !== 'Next.js') return [];\n const appDir = resolveAppDir(ctx.installDir);\n if (!appDir) return [];\n\n const tsxFiles = findFilesShallow(appDir, /\\.(tsx|jsx)$/);\n // Match <Link>, <NextLink>, or other common aliases\n const LINK_PATTERN = /<(?:Next)?Link\\s[^>]*href\\s*=\\s*[\"'{`/]*(\\/sign-?out|\\/logout)/;\n\n const findings: AuthPatternFinding[] = [];\n for (const file of tsxFiles) {\n if (fileContains(file, LINK_PATTERN)) {\n findings.push({\n code: 'SIGNOUT_LINK_PREFETCH',\n severity: 'warning',\n message:\n 'Link component points to signout/logout — Next.js will prefetch this in production, potentially triggering logouts',\n filePath: relative(ctx.installDir, file),\n remediation:\n 'Use a <form> with a server action or <button> with onClick handler instead of <Link> for signout.',\n });\n }\n }\n return findings;\n}\n\nfunction checkMissingMiddleware(ctx: CheckContext): AuthPatternFinding[] {\n if (ctx.framework.name !== 'Next.js') return [];\n\n const middlewarePaths = [\n 'middleware.ts',\n 'middleware.js',\n 'proxy.ts',\n 'proxy.js',\n 'src/middleware.ts',\n 'src/middleware.js',\n 'src/proxy.ts',\n 'src/proxy.js',\n ].map((p) => join(ctx.installDir, p));\n\n if (findFile(middlewarePaths)) return [];\n\n return [\n {\n code: 'MISSING_MIDDLEWARE',\n severity: 'error',\n message: 'No middleware.ts or proxy.ts found — AuthKit session handling requires middleware',\n remediation: 'Create middleware.ts at the project root with authkitMiddleware() from @workos-inc/authkit-nextjs.',\n docsUrl: 'https://workos.com/docs/authkit/nextjs/middleware',\n },\n ];\n}\n\nfunction checkMiddlewareWrongLocation(ctx: CheckContext): AuthPatternFinding[] {\n if (ctx.framework.name !== 'Next.js') return [];\n\n const wrongPaths = ['app/middleware.ts', 'app/middleware.js', 'src/app/middleware.ts', 'src/app/middleware.js'].map(\n (p) => join(ctx.installDir, p),\n );\n\n const found = findFile(wrongPaths);\n if (!found) return [];\n\n return [\n {\n code: 'MIDDLEWARE_WRONG_LOCATION',\n severity: 'warning',\n message: 'middleware.ts found inside app/ directory — must be at project root or src/',\n filePath: relative(ctx.installDir, found),\n remediation: 'Move middleware.ts to the project root (or src/ if using src/ directory).',\n docsUrl: 'https://workos.com/docs/authkit/nextjs/middleware',\n },\n ];\n}\n\nfunction checkMissingAuthKitProvider(ctx: CheckContext): AuthPatternFinding[] {\n const layoutPaths: string[] = [];\n\n if (ctx.framework.name === 'Next.js') {\n layoutPaths.push(\n join(ctx.installDir, 'app', 'layout.tsx'),\n join(ctx.installDir, 'app', 'layout.jsx'),\n join(ctx.installDir, 'src', 'app', 'layout.tsx'),\n join(ctx.installDir, 'src', 'app', 'layout.jsx'),\n );\n } else if (ctx.framework.name === 'React Router' && ctx.framework.variant === 'declarative') {\n layoutPaths.push(\n join(ctx.installDir, 'src', 'App.tsx'),\n join(ctx.installDir, 'src', 'App.jsx'),\n join(ctx.installDir, 'app', 'root.tsx'),\n join(ctx.installDir, 'app', 'root.jsx'),\n );\n } else {\n return [];\n }\n\n const layoutFile = findFile(layoutPaths);\n if (!layoutFile) return []; // Can't check if layout doesn't exist\n\n if (fileContains(layoutFile, /AuthKitProvider/)) return [];\n\n return [\n {\n code: 'MISSING_AUTHKIT_PROVIDER',\n severity: 'warning',\n message: 'AuthKitProvider not found in root layout — required for AuthKit session management',\n filePath: relative(ctx.installDir, layoutFile),\n remediation: 'Wrap your app with <AuthKitProvider> in the root layout.',\n docsUrl: 'https://workos.com/docs/authkit/nextjs/setup',\n },\n ];\n}\n\n/** Extract callback path from redirect URI env vars or framework default */\nfunction resolveCallbackPath(ctx: CheckContext): string | null {\n // Check env vars for actual redirect URI (including NEXT_PUBLIC_ variant)\n const projectEnv = loadProjectEnvRaw(ctx.installDir);\n const redirectUri =\n projectEnv.WORKOS_REDIRECT_URI ?? projectEnv.NEXT_PUBLIC_WORKOS_REDIRECT_URI ?? ctx.environment.redirectUri;\n\n if (redirectUri) {\n try {\n return new URL(redirectUri).pathname;\n } catch {\n // Invalid URL, fall through to framework default\n }\n }\n\n return ctx.framework.expectedCallbackPath ?? null;\n}\n\nfunction checkCallbackRouteMissing(ctx: CheckContext): AuthPatternFinding[] {\n const callbackPath = resolveCallbackPath(ctx);\n if (!callbackPath) return [];\n\n // Build expected route file paths based on framework\n const possiblePaths: string[] = [];\n\n if (ctx.framework.name === 'Next.js') {\n // app/auth/callback/route.ts (or src/app/)\n const routeDir = callbackPath.replace(/^\\//, ''); // 'auth/callback'\n for (const prefix of ['app', 'src/app']) {\n for (const ext of ['ts', 'tsx', 'js', 'jsx']) {\n possiblePaths.push(join(ctx.installDir, prefix, routeDir, `route.${ext}`));\n }\n }\n } else if (ctx.framework.name === 'React Router') {\n // Flat: app/routes/auth.callback.tsx Nested: app/routes/auth/callback.tsx\n const segments = callbackPath.replace(/^\\//, '').split('/');\n const flat = segments.join('.');\n const nested = segments.join('/');\n for (const ext of ['tsx', 'jsx', 'ts', 'js']) {\n possiblePaths.push(join(ctx.installDir, 'app', 'routes', `${flat}.${ext}`));\n possiblePaths.push(join(ctx.installDir, 'app', 'routes', nested + `.${ext}`));\n }\n } else if (ctx.framework.name === 'TanStack Start') {\n // Modern flat: src/routes/api.auth.callback.tsx Legacy nested: app/routes/api/auth/callback.tsx\n const segments = callbackPath.replace(/^\\//, '').split('/');\n const flat = segments.join('.');\n const nested = segments.join('/');\n for (const ext of ['tsx', 'jsx', 'ts', 'js']) {\n possiblePaths.push(join(ctx.installDir, 'src', 'routes', `${flat}.${ext}`));\n possiblePaths.push(join(ctx.installDir, 'app', 'routes', nested + `.${ext}`));\n }\n }\n\n if (possiblePaths.length === 0) return [];\n if (findFile(possiblePaths)) return [];\n\n return [\n {\n code: 'CALLBACK_ROUTE_MISSING',\n severity: 'error',\n message: `No callback route found at expected path ${callbackPath}`,\n remediation: `Create the callback route handler at the path matching your WORKOS_REDIRECT_URI.`,\n docsUrl: 'https://workos.com/docs/authkit/redirect-uri',\n },\n ];\n}\n\nconst CLIENT_ENV_PREFIXES = ['NEXT_PUBLIC_', 'VITE_', 'REACT_APP_', 'EXPO_PUBLIC_'];\n\nfunction checkApiKeyLeakedToClient(ctx: CheckContext): AuthPatternFinding[] {\n const projectEnv = loadProjectEnvRaw(ctx.installDir);\n const findings: AuthPatternFinding[] = [];\n\n for (const [key, value] of Object.entries(projectEnv)) {\n const hasClientPrefix = CLIENT_ENV_PREFIXES.some((prefix) => key.startsWith(prefix));\n if (!hasClientPrefix) continue;\n\n const isApiKey = key.includes('WORKOS_API_KEY');\n const isSecretValue = value?.startsWith('sk_test_') || value?.startsWith('sk_live_');\n\n if (isApiKey || isSecretValue) {\n findings.push({\n code: 'API_KEY_LEAKED_TO_CLIENT',\n severity: 'error',\n message: `Secret API key exposed via client-accessible env var ${key}`,\n remediation: `Remove the client prefix. WORKOS_API_KEY must be server-only (no NEXT_PUBLIC_, VITE_, REACT_APP_, or EXPO_PUBLIC_ prefix).`,\n });\n }\n }\n return findings;\n}\n\nfunction checkWrongCallbackLoader(ctx: CheckContext): AuthPatternFinding[] {\n if (ctx.framework.name !== 'React Router') return [];\n const callbackPath = ctx.framework.expectedCallbackPath;\n if (!callbackPath) return [];\n\n const segments = callbackPath.replace(/^\\//, '').split('/');\n const flat = segments.join('.');\n const nested = segments.join('/');\n\n const possiblePaths: string[] = [];\n for (const ext of ['tsx', 'jsx', 'ts', 'js']) {\n possiblePaths.push(join(ctx.installDir, 'app', 'routes', `${flat}.${ext}`));\n possiblePaths.push(join(ctx.installDir, 'app', 'routes', nested + `.${ext}`));\n }\n\n const callbackFile = findFile(possiblePaths);\n if (!callbackFile) return []; // No callback route to check\n\n // authkitLoader is for regular routes; authLoader is for the callback\n if (fileContains(callbackFile, /authkitLoader/) && !fileContains(callbackFile, /authLoader/)) {\n return [\n {\n code: 'WRONG_CALLBACK_LOADER',\n severity: 'warning',\n message: 'Callback route uses authkitLoader instead of authLoader',\n filePath: relative(ctx.installDir, callbackFile),\n remediation:\n 'Use authLoader (not authkitLoader) for the callback route. authkitLoader is for regular routes that need auth context.',\n },\n ];\n }\n return [];\n}\n\nfunction checkMissingRootAuthLoader(ctx: CheckContext): AuthPatternFinding[] {\n if (ctx.framework.name !== 'React Router') return [];\n\n const rootPaths = ['app/root.tsx', 'app/root.jsx', 'app/routes/_index.tsx', 'app/routes/_index.jsx'].map((p) =>\n join(ctx.installDir, p),\n );\n\n const rootFile = findFile(rootPaths);\n if (!rootFile) return [];\n\n if (fileContains(rootFile, /authkitLoader/)) return [];\n\n return [\n {\n code: 'MISSING_ROOT_AUTH_LOADER',\n severity: 'warning',\n message: 'Root route does not use authkitLoader — child routes will not have auth context',\n filePath: relative(ctx.installDir, rootFile),\n remediation: 'Add authkitLoader to your root route so child routes can access auth state.',\n },\n ];\n}\n\nfunction checkMissingAuthkitMiddleware(ctx: CheckContext): AuthPatternFinding[] {\n if (ctx.framework.name !== 'TanStack Start') return [];\n\n const startPaths = ['src/start.ts', 'src/start.tsx', 'app/start.ts', 'app/start.tsx'].map((p) =>\n join(ctx.installDir, p),\n );\n\n const startFile = findFile(startPaths);\n if (!startFile) return []; // Can't check if start file doesn't exist\n\n if (fileContains(startFile, /authkitMiddleware/)) return [];\n\n return [\n {\n code: 'MISSING_AUTHKIT_MIDDLEWARE',\n severity: 'warning',\n message: 'start.ts does not reference authkitMiddleware — AuthKit session handling requires it',\n filePath: relative(ctx.installDir, startFile),\n remediation: 'Add authkitMiddleware to your start.ts server middleware configuration.',\n },\n ];\n}\n\nfunction checkCookiePasswordTooShort(ctx: CheckContext): AuthPatternFinding[] {\n if (ctx.framework.name !== 'React Router' && ctx.framework.name !== 'TanStack Start') return [];\n\n const projectEnv = loadProjectEnvRaw(ctx.installDir);\n const password = projectEnv.WORKOS_COOKIE_PASSWORD;\n\n // Only warn if password is set but too short; missing password is a separate concern\n if (!password || password.length >= 32) return [];\n\n return [\n {\n code: 'COOKIE_PASSWORD_TOO_SHORT',\n severity: 'warning',\n message: `WORKOS_COOKIE_PASSWORD is ${password.length} characters — minimum 32 required for secure encryption`,\n remediation: 'Set WORKOS_COOKIE_PASSWORD to a random string of at least 32 characters.',\n },\n ];\n}\n\n// --- Cross-language checks (run for ALL projects, not just JS/AuthKit) ---\n\nconst SOURCE_EXTENSIONS = /\\.(ts|tsx|js|jsx|py|rb|go|java|kt|php|cs|swift|dart)$/;\n\nfunction checkApiKeyInSource(ctx: CheckContext): AuthPatternFinding[] {\n const API_KEY_PATTERN = /sk_(test|live)_[A-Za-z0-9]{10,}/;\n const sourceFiles = findFilesShallow(ctx.installDir, SOURCE_EXTENSIONS, 4);\n const findings: AuthPatternFinding[] = [];\n\n for (const file of sourceFiles) {\n const content = readFileSafe(file);\n if (!content) continue;\n if (API_KEY_PATTERN.test(content)) {\n findings.push({\n code: 'API_KEY_IN_SOURCE',\n severity: 'error',\n message: `WorkOS API key hardcoded in source file`,\n filePath: relative(ctx.installDir, file),\n remediation:\n 'Move the API key to an environment variable (WORKOS_API_KEY) and load it from .env or your secret manager.',\n });\n }\n }\n return findings;\n}\n\nfunction checkEnvFileNotGitignored(ctx: CheckContext): AuthPatternFinding[] {\n const envFiles = ['.env', '.env.local'].filter((f) => existsSync(join(ctx.installDir, f)));\n if (envFiles.length === 0) return [];\n\n const gitignorePath = join(ctx.installDir, '.gitignore');\n const gitignore = readFileSafe(gitignorePath);\n\n const findings: AuthPatternFinding[] = [];\n for (const envFile of envFiles) {\n const isIgnored =\n gitignore !== null &&\n gitignore.split('\\n').some((line) => {\n const trimmed = line.trim();\n if (trimmed.startsWith('#') || trimmed === '') return false;\n return trimmed === envFile || trimmed === '.env*' || trimmed === '.env.*' || trimmed === '.env';\n });\n\n if (!isIgnored) {\n findings.push({\n code: 'ENV_FILE_NOT_GITIGNORED',\n severity: 'warning',\n message: `${envFile} is not in .gitignore — secrets may be committed to version control`,\n filePath: envFile,\n remediation: `Add ${envFile} to your .gitignore file.`,\n });\n }\n }\n return findings;\n}\n\nfunction checkMixedEnvironmentCredentials(ctx: CheckContext): AuthPatternFinding[] {\n const projectEnv = loadProjectEnvRaw(ctx.installDir);\n const apiKey = projectEnv.WORKOS_API_KEY;\n const redirectUri = projectEnv.WORKOS_REDIRECT_URI ?? projectEnv.NEXT_PUBLIC_WORKOS_REDIRECT_URI;\n\n if (!apiKey || !redirectUri) return [];\n\n const isTestKey = apiKey.startsWith('sk_test_');\n const isLiveKey = apiKey.startsWith('sk_live_');\n\n let isProductionUri = false;\n try {\n const url = new URL(redirectUri);\n isProductionUri = url.hostname !== 'localhost' && !url.hostname.startsWith('127.0.0.');\n } catch {\n return [];\n }\n\n if (isTestKey && isProductionUri) {\n return [\n {\n code: 'MIXED_ENVIRONMENT',\n severity: 'warning',\n message: 'Staging API key (sk_test_) used with a production redirect URI',\n remediation:\n 'Use sk_live_ API key for production redirect URIs, or change the redirect URI to localhost for staging.',\n },\n ];\n }\n\n if (isLiveKey && !isProductionUri) {\n return [\n {\n code: 'MIXED_ENVIRONMENT',\n severity: 'warning',\n message: 'Production API key (sk_live_) used with a localhost redirect URI',\n remediation:\n 'Use sk_test_ API key for localhost development, or update the redirect URI to your production domain.',\n },\n ];\n }\n\n return [];\n}\n\n// --- Main Entry Point ---\n\ntype CheckFn = (ctx: CheckContext) => AuthPatternFinding[];\n\nconst CROSS_FRAMEWORK_CHECKS: CheckFn[] = [\n checkApiKeyLeakedToClient,\n checkApiKeyInSource,\n checkEnvFileNotGitignored,\n checkMixedEnvironmentCredentials,\n];\n\nconst NEXTJS_CHECKS: CheckFn[] = [\n checkSignoutGetHandler,\n checkSignoutLinkPrefetch,\n checkMissingMiddleware,\n checkMiddlewareWrongLocation,\n checkMissingAuthKitProvider,\n checkCallbackRouteMissing,\n];\n\nconst REACT_ROUTER_CHECKS: CheckFn[] = [\n checkWrongCallbackLoader,\n checkMissingRootAuthLoader,\n checkMissingAuthKitProvider,\n checkCallbackRouteMissing,\n checkCookiePasswordTooShort,\n];\n\nconst TANSTACK_CHECKS: CheckFn[] = [\n checkMissingAuthkitMiddleware,\n checkCallbackRouteMissing,\n checkCookiePasswordTooShort,\n];\n\nexport async function checkAuthPatterns(\n options: DoctorOptions,\n framework: FrameworkInfo,\n environment: EnvironmentInfo,\n sdk: SdkInfo,\n): Promise<AuthPatternInfo> {\n const ctx: CheckContext = {\n framework,\n environment,\n sdk,\n installDir: options.installDir,\n };\n\n const checks: CheckFn[] = [...CROSS_FRAMEWORK_CHECKS];\n\n switch (framework.name) {\n case 'Next.js':\n checks.push(...NEXTJS_CHECKS);\n break;\n case 'React Router':\n checks.push(...REACT_ROUTER_CHECKS);\n break;\n case 'TanStack Start':\n checks.push(...TANSTACK_CHECKS);\n break;\n }\n\n const findings: AuthPatternFinding[] = [];\n for (const check of checks) {\n findings.push(...check(ctx));\n }\n\n return {\n checksRun: checks.length,\n findings,\n };\n}\n"]}
|
|
@@ -1,10 +1,29 @@
|
|
|
1
1
|
import { readFileSync, existsSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
|
|
3
|
+
function parseEnvFile(content) {
|
|
4
|
+
const result = {};
|
|
5
|
+
for (const line of content.split('\n')) {
|
|
6
|
+
const trimmed = line.trim();
|
|
7
|
+
if (!trimmed || trimmed.startsWith('#'))
|
|
8
|
+
continue;
|
|
9
|
+
// Strip optional `export ` prefix
|
|
10
|
+
const entry = trimmed.startsWith('export ') ? trimmed.slice(7) : trimmed;
|
|
11
|
+
const eqIndex = entry.indexOf('=');
|
|
12
|
+
if (eqIndex === -1)
|
|
13
|
+
continue;
|
|
14
|
+
const key = entry.slice(0, eqIndex).trim();
|
|
15
|
+
let value = entry.slice(eqIndex + 1).trim();
|
|
16
|
+
// Remove surrounding quotes (single or double)
|
|
17
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
18
|
+
value = value.slice(1, -1);
|
|
19
|
+
}
|
|
20
|
+
result[key] = value;
|
|
21
|
+
}
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
4
24
|
/**
|
|
5
25
|
* Load environment variables from project's .env files.
|
|
6
26
|
* Priority: .env.local > .env (matching Next.js/Vite conventions)
|
|
7
|
-
* Uses dotenv parser for proper handling of quotes, multiline, exports, etc.
|
|
8
27
|
*/
|
|
9
28
|
function loadProjectEnv(installDir) {
|
|
10
29
|
const env = {};
|
|
@@ -15,8 +34,7 @@ function loadProjectEnv(installDir) {
|
|
|
15
34
|
if (existsSync(filePath)) {
|
|
16
35
|
try {
|
|
17
36
|
const content = readFileSync(filePath, 'utf-8');
|
|
18
|
-
|
|
19
|
-
Object.assign(env, parsed);
|
|
37
|
+
Object.assign(env, parseEnvFile(content));
|
|
20
38
|
}
|
|
21
39
|
catch {
|
|
22
40
|
// Ignore read errors
|