@clerk/eslint-plugin 0.0.1 → 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/dist/next.js ADDED
@@ -0,0 +1,833 @@
1
+ const require_chunk = require('./chunk-C_NdSu1c.js');
2
+ let node_path = require("node:path");
3
+ node_path = require_chunk.__toESM(node_path);
4
+ let node_fs = require("node:fs");
5
+
6
+ //#region src/next/lib/exports.ts
7
+ function unwrapFunction(node) {
8
+ if (!node) return null;
9
+ if (node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression") return node;
10
+ return null;
11
+ }
12
+ function resolveLocalIdentifierTarget(programNode, name) {
13
+ for (const stmt of programNode.body) {
14
+ if (stmt.type === "FunctionDeclaration" && stmt.id && stmt.id.name === name) return {
15
+ kind: "function",
16
+ node: stmt
17
+ };
18
+ if (stmt.type === "VariableDeclaration") for (const declarator of stmt.declarations) {
19
+ if (declarator.id.type !== "Identifier" || declarator.id.name !== name) continue;
20
+ const initFn = unwrapFunction(declarator.init ?? void 0);
21
+ if (initFn) return {
22
+ kind: "function",
23
+ node: initFn
24
+ };
25
+ return { kind: "unknown" };
26
+ }
27
+ if (stmt.type === "ImportDeclaration") {
28
+ for (const spec of stmt.specifiers) if (spec.local && spec.local.name === name) return {
29
+ kind: "imported",
30
+ source: stmt.source.value
31
+ };
32
+ }
33
+ }
34
+ return { kind: "unknown" };
35
+ }
36
+ function resolveDefaultExportTarget(programNode, declaration) {
37
+ const direct = unwrapFunction(declaration);
38
+ if (direct) return {
39
+ kind: "function",
40
+ node: direct
41
+ };
42
+ if (declaration.type !== "Identifier") return { kind: "unknown" };
43
+ return resolveLocalIdentifierTarget(programNode, declaration.name);
44
+ }
45
+ function getExportedName(spec) {
46
+ const node = spec.exported;
47
+ if (node.type === "Identifier") return node.name;
48
+ if (node.type === "Literal" && typeof node.value === "string") return node.value;
49
+ return null;
50
+ }
51
+ /**
52
+ * Resolve the module's default export, regardless of whether it's declared with
53
+ * `export default ...` or via a specifier such as `export { Page as default }`
54
+ * or `export { default } from './Page'`.
55
+ */
56
+ function resolveDefaultExport(programNode) {
57
+ for (const stmt of programNode.body) {
58
+ if (stmt.type === "ExportDefaultDeclaration") return {
59
+ target: resolveDefaultExportTarget(programNode, stmt.declaration),
60
+ reportNode: stmt
61
+ };
62
+ if (stmt.type !== "ExportNamedDeclaration") continue;
63
+ if (stmt.exportKind === "type") continue;
64
+ for (const spec of stmt.specifiers) {
65
+ if (spec.type !== "ExportSpecifier") continue;
66
+ if (spec.exportKind === "type") continue;
67
+ if (getExportedName(spec) !== "default") continue;
68
+ if (stmt.source) return {
69
+ target: {
70
+ kind: "imported",
71
+ source: stmt.source.value
72
+ },
73
+ reportNode: stmt
74
+ };
75
+ if (spec.local.type !== "Identifier") return {
76
+ target: { kind: "unknown" },
77
+ reportNode: stmt
78
+ };
79
+ return {
80
+ target: resolveLocalIdentifierTarget(programNode, spec.local.name),
81
+ reportNode: stmt
82
+ };
83
+ }
84
+ }
85
+ return null;
86
+ }
87
+ /**
88
+ * Yield value-level `export * from '...'` declarations. The rule cannot follow
89
+ * these across files, so callers treat them conservatively as unverifiable.
90
+ */
91
+ function* iterateExportAllDeclarations(programNode) {
92
+ for (const stmt of programNode.body) {
93
+ if (stmt.type !== "ExportAllDeclaration") continue;
94
+ if (stmt.exportKind === "type") continue;
95
+ if (stmt.exported) continue;
96
+ yield {
97
+ source: stmt.source.value,
98
+ reportNode: stmt
99
+ };
100
+ }
101
+ }
102
+ function* iterateNamedExports(programNode) {
103
+ for (const stmt of programNode.body) {
104
+ if (stmt.type !== "ExportNamedDeclaration") continue;
105
+ if (stmt.exportKind === "type") continue;
106
+ if (stmt.declaration) {
107
+ const decl = stmt.declaration;
108
+ if (decl.type === "FunctionDeclaration" && decl.id) yield {
109
+ name: decl.id.name,
110
+ target: {
111
+ kind: "function",
112
+ node: decl
113
+ },
114
+ reportNode: stmt
115
+ };
116
+ else if (decl.type === "VariableDeclaration") for (const declarator of decl.declarations) {
117
+ if (declarator.id.type !== "Identifier") continue;
118
+ const fn = unwrapFunction(declarator.init ?? void 0);
119
+ yield {
120
+ name: declarator.id.name,
121
+ target: fn ? {
122
+ kind: "function",
123
+ node: fn
124
+ } : { kind: "unknown" },
125
+ reportNode: stmt
126
+ };
127
+ }
128
+ continue;
129
+ }
130
+ for (const spec of stmt.specifiers) {
131
+ if (spec.type !== "ExportSpecifier") continue;
132
+ if (spec.exportKind === "type") continue;
133
+ const exportedName = getExportedName(spec);
134
+ if (!exportedName) continue;
135
+ if (stmt.source) {
136
+ yield {
137
+ name: exportedName,
138
+ target: {
139
+ kind: "imported",
140
+ source: stmt.source.value
141
+ },
142
+ reportNode: stmt
143
+ };
144
+ continue;
145
+ }
146
+ if (spec.local.type !== "Identifier") continue;
147
+ yield {
148
+ name: exportedName,
149
+ target: resolveLocalIdentifierTarget(programNode, spec.local.name),
150
+ reportNode: stmt
151
+ };
152
+ }
153
+ }
154
+ }
155
+
156
+ //#endregion
157
+ //#region src/next/lib/file-info.ts
158
+ /**
159
+ * Utilities for classifying a file by path, kind, and module-level directives.
160
+ */
161
+ const RESOURCE_FILES = new Set([
162
+ "page",
163
+ "layout",
164
+ "template",
165
+ "default",
166
+ "route"
167
+ ]);
168
+ const RESOURCE_EXTENSIONS = /\.(ts|tsx|js|jsx|mjs|cjs)$/;
169
+ /**
170
+ * @param rootDir Project root to relativize against — typically from
171
+ * `resolveProjectRoot()` (explicit option, nearest `eslint.config.*`, or ESLint
172
+ * `cwd`). When omitted or when the file lies outside `rootDir`, the absolute
173
+ * path is scanned for an `app` segment instead.
174
+ */
175
+ function getRelativeFolder(filename, rootDir) {
176
+ if (!filename) return null;
177
+ const normalizedFile = filename.replaceAll("\\", "/");
178
+ let candidate = normalizedFile;
179
+ if (rootDir) {
180
+ const normalizedRoot = rootDir.replaceAll("\\", "/");
181
+ const rel = node_path.default.posix.relative(normalizedRoot, normalizedFile);
182
+ if (rel && !rel.startsWith("..")) candidate = rel;
183
+ }
184
+ const segments = candidate.split("/");
185
+ const appIdx = segments.findIndex((seg) => seg === "app");
186
+ if (appIdx !== -1) return node_path.default.posix.dirname(segments.slice(appIdx).join("/"));
187
+ if (candidate !== normalizedFile) return node_path.default.posix.dirname(candidate);
188
+ return null;
189
+ }
190
+ function getFileKind(filename) {
191
+ if (!filename) return null;
192
+ const base = node_path.default.basename(filename).replace(RESOURCE_EXTENSIONS, "");
193
+ return RESOURCE_FILES.has(base) ? base : null;
194
+ }
195
+ function hasTopLevelDirective(programNode, name) {
196
+ for (const stmt of programNode.body) {
197
+ if (stmt.type !== "ExpressionStatement") break;
198
+ if (!("directive" in stmt)) break;
199
+ if (stmt.directive === name) return true;
200
+ }
201
+ return false;
202
+ }
203
+ function isServerFunctionModule(programNode) {
204
+ return hasTopLevelDirective(programNode, "use server");
205
+ }
206
+ function isClientModule(programNode) {
207
+ return hasTopLevelDirective(programNode, "use client");
208
+ }
209
+
210
+ //#endregion
211
+ //#region src/next/lib/fixers.ts
212
+ const CLERK_AUTH_SOURCE$1 = "@clerk/nextjs/server";
213
+ /**
214
+ * The local name to call `.protect()` on. Reuses an existing alias (e.g.
215
+ * `import { auth as clerkAuth }`) when present, otherwise defaults to `auth`.
216
+ */
217
+ function resolveAuthName(authNames) {
218
+ for (const name of authNames) return name;
219
+ return "auth";
220
+ }
221
+ /**
222
+ * Build the ordered, non-overlapping set of edits that protect `fn`. All edits
223
+ * belong to a single suggestion and are applied atomically.
224
+ */
225
+ function buildAuthProtectFixes({ fixer, sourceCode, fn, authNames }) {
226
+ const program = sourceCode.ast;
227
+ const authName = resolveAuthName(authNames);
228
+ const fixes = [];
229
+ const importFix = ensureAuthImportFix(fixer, sourceCode, program, authNames);
230
+ if (importFix) fixes.push(importFix);
231
+ const asyncFix = ensureAsyncFix(fixer, sourceCode, fn);
232
+ if (asyncFix) fixes.push(asyncFix);
233
+ fixes.push(insertProtectCallFix(fixer, sourceCode, fn, authName, authNames));
234
+ return fixes;
235
+ }
236
+ /**
237
+ * If `node` is `await <auth>()` (a zero-argument call to one of the imported
238
+ * `auth` names), return the callee identifier so it can be rewritten to
239
+ * `<auth>.protect`. Returns `null` otherwise.
240
+ *
241
+ * Used to merge protection into an existing call (`const { userId } = await
242
+ * auth()` -> `const { userId } = await auth.protect()`) instead of prepending a
243
+ * duplicate `await auth.protect();`.
244
+ */
245
+ function mergeableAuthCallee(node, authNames) {
246
+ if (!node || node.type !== "AwaitExpression") return null;
247
+ const arg = node.argument;
248
+ if (arg.type !== "CallExpression" || arg.arguments.length > 0) return null;
249
+ if (arg.callee.type !== "Identifier" || !authNames.has(arg.callee.name)) return null;
250
+ return arg.callee;
251
+ }
252
+ /**
253
+ * Look for a mergeable `await <auth>()` in the first executable statement of a
254
+ * block body — either a bare `await auth();` or the initializer of the first
255
+ * declarator (`const { userId } = await auth();`).
256
+ */
257
+ function firstStatementAuthCallee(stmt, authNames) {
258
+ if (stmt.type === "ExpressionStatement") return mergeableAuthCallee(stmt.expression, authNames);
259
+ if (stmt.type === "VariableDeclaration") {
260
+ const first = stmt.declarations[0];
261
+ return first ? mergeableAuthCallee(first.init, authNames) : null;
262
+ }
263
+ return null;
264
+ }
265
+ function getLineIndent(sourceCode, node) {
266
+ const line = sourceCode.lines[node.loc.start.line - 1] ?? "";
267
+ const match = /^\s*/.exec(line);
268
+ return match ? match[0] : "";
269
+ }
270
+ function ensureAsyncFix(fixer, sourceCode, fn) {
271
+ if (fn.async) return null;
272
+ const firstToken = sourceCode.getFirstToken(fn);
273
+ if (!firstToken) return null;
274
+ return fixer.insertTextBefore(firstToken, "async ");
275
+ }
276
+ function insertProtectCallFix(fixer, sourceCode, fn, authName, authNames) {
277
+ const call = `await ${authName}.protect();`;
278
+ const body = fn.body;
279
+ if (body.type !== "BlockStatement") {
280
+ const conciseCallee = mergeableAuthCallee(body, authNames);
281
+ if (conciseCallee) return fixer.replaceText(conciseCallee, `${conciseCallee.name}.protect`);
282
+ const fnIndent = getLineIndent(sourceCode, fn);
283
+ const inner = `${fnIndent} `;
284
+ const blockBody = `{\n${inner}${call}\n${inner}return ${sourceCode.getText(body)};\n${fnIndent}}`;
285
+ const arrowToken = sourceCode.getTokenBefore(body, { filter: (token) => token.type === "Punctuator" && token.value === "=>" });
286
+ if (arrowToken) return fixer.replaceTextRange([arrowToken.range[1], fn.range[1]], ` ${blockBody}`);
287
+ return fixer.replaceText(body, blockBody);
288
+ }
289
+ const stmts = body.body;
290
+ let lastDirective = null;
291
+ let firstExecIdx = 0;
292
+ for (const stmt of stmts) if (stmt.type === "ExpressionStatement" && stmt.directive) {
293
+ lastDirective = stmt;
294
+ firstExecIdx++;
295
+ } else break;
296
+ const firstExec = stmts[firstExecIdx];
297
+ if (firstExec) {
298
+ const mergeCallee = firstStatementAuthCallee(firstExec, authNames);
299
+ if (mergeCallee) return fixer.replaceText(mergeCallee, `${mergeCallee.name}.protect`);
300
+ }
301
+ if (lastDirective) {
302
+ const indent = getLineIndent(sourceCode, lastDirective);
303
+ return fixer.insertTextAfter(lastDirective, `\n${indent}${call}`);
304
+ }
305
+ const firstStmt = stmts[0];
306
+ if (firstStmt) {
307
+ const indent = getLineIndent(sourceCode, firstStmt);
308
+ return fixer.insertTextBefore(firstStmt, `${call}\n${indent}`);
309
+ }
310
+ const openBrace = sourceCode.getFirstToken(body);
311
+ const indent = `${getLineIndent(sourceCode, fn)} `;
312
+ if (openBrace) return fixer.insertTextAfter(openBrace, `\n${indent}${call}`);
313
+ return fixer.insertTextBefore(body, `${call}\n`);
314
+ }
315
+ function ensureAuthImportFix(fixer, sourceCode, program, authNames) {
316
+ if (authNames.size > 0) return null;
317
+ for (const stmt of program.body) {
318
+ if (stmt.type !== "ImportDeclaration") continue;
319
+ if (stmt.source.value !== CLERK_AUTH_SOURCE$1 || stmt.importKind === "type") continue;
320
+ const named = stmt.specifiers.filter((spec) => spec.type === "ImportSpecifier");
321
+ const last = named[named.length - 1];
322
+ if (last) return fixer.insertTextAfter(last, ", auth");
323
+ }
324
+ const importText = `import { auth } from '${CLERK_AUTH_SOURCE$1}';`;
325
+ const stmts = program.body;
326
+ let lastDirective = null;
327
+ for (const stmt of stmts) if (stmt.type === "ExpressionStatement" && stmt.directive) lastDirective = stmt;
328
+ else break;
329
+ if (lastDirective) return fixer.insertTextAfter(lastDirective, `\n${importText}`);
330
+ const firstStmt = stmts[0];
331
+ if (firstStmt) return fixer.insertTextBefore(firstStmt, `${importText}\n`);
332
+ return fixer.insertTextAfterRange([0, 0], `${importText}\n`);
333
+ }
334
+
335
+ //#endregion
336
+ //#region src/next/lib/match-folders.ts
337
+ const REGEX_SPECIAL = /[.+^${}()|[\]\\]/g;
338
+ function segmentToRegex(seg) {
339
+ let out = "";
340
+ for (let i = 0; i < seg.length; i++) {
341
+ const ch = seg[i];
342
+ if (ch === "*") out += "[^/]*";
343
+ else if (REGEX_SPECIAL.test(ch)) out += "\\" + ch;
344
+ else out += ch;
345
+ REGEX_SPECIAL.lastIndex = 0;
346
+ }
347
+ return out;
348
+ }
349
+ function segmentMatches(patternSeg, pathSeg) {
350
+ if (patternSeg === pathSeg) return true;
351
+ if (!patternSeg.includes("*")) return false;
352
+ return new RegExp("^" + segmentToRegex(patternSeg) + "$").test(pathSeg);
353
+ }
354
+ function matchSegments(patternSegs, pi, pathSegs, si) {
355
+ while (pi < patternSegs.length) {
356
+ const seg = patternSegs[pi];
357
+ if (seg === "**") {
358
+ if (pi === patternSegs.length - 1) return true;
359
+ for (let k = si; k <= pathSegs.length; k++) if (matchSegments(patternSegs, pi + 1, pathSegs, k)) return true;
360
+ return false;
361
+ }
362
+ if (si >= pathSegs.length) return false;
363
+ if (!segmentMatches(seg, pathSegs[si])) return false;
364
+ pi++;
365
+ si++;
366
+ }
367
+ return si === pathSegs.length;
368
+ }
369
+ function matchPath(pattern, path) {
370
+ return matchSegments(pattern.split("/"), 0, path.split("/"), 0);
371
+ }
372
+ function specificity(pattern) {
373
+ return pattern.split("/").filter((seg) => seg.length > 0 && seg !== "**" && !seg.includes("*")).length;
374
+ }
375
+ function literalPrefix(pattern) {
376
+ const segs = pattern.split("/");
377
+ const result = [];
378
+ for (const seg of segs) {
379
+ if (seg === "**" || seg.includes("*")) break;
380
+ result.push(seg);
381
+ }
382
+ return result.join("/");
383
+ }
384
+ function hasDescendantsMatching(folder, patterns) {
385
+ if (!patterns || patterns.length === 0) return false;
386
+ for (const pattern of patterns) {
387
+ const prefix = literalPrefix(pattern);
388
+ if (!prefix) continue;
389
+ if (prefix === folder) return true;
390
+ if (prefix.startsWith(folder + "/")) return true;
391
+ }
392
+ return false;
393
+ }
394
+ function classifyFolder(folderPath, options) {
395
+ const protectPatterns = options.protected ?? [];
396
+ const publicPatterns = options.public ?? [];
397
+ const protectMatches = protectPatterns.filter((p) => matchPath(p, folderPath));
398
+ const publicMatches = publicPatterns.filter((p) => matchPath(p, folderPath));
399
+ if (protectMatches.length === 0 && publicMatches.length === 0) return "unmatched";
400
+ if (publicMatches.length === 0) return "protected";
401
+ if (protectMatches.length === 0) return "public";
402
+ return Math.max(...protectMatches.map(specificity)) >= Math.max(...publicMatches.map(specificity)) ? "protected" : "public";
403
+ }
404
+
405
+ //#endregion
406
+ //#region src/next/lib/project-root.ts
407
+ /**
408
+ * Resolve the directory paths should be relativized against when classifying
409
+ * App Router folders.
410
+ */
411
+ /** Filenames ESLint searches for when resolving flat config (precedence order). */
412
+ const ESLINT_CONFIG_FILENAMES = [
413
+ "eslint.config.js",
414
+ "eslint.config.mjs",
415
+ "eslint.config.cjs",
416
+ "eslint.config.ts",
417
+ "eslint.config.mts",
418
+ "eslint.config.cts"
419
+ ];
420
+ function normalizeDir(dir) {
421
+ return dir.replaceAll("\\", "/");
422
+ }
423
+ /**
424
+ * Walk up from `filePath` and return the directory of the nearest
425
+ * `eslint.config.*`, mirroring ESLint's per-file config lookup.
426
+ */
427
+ function findEslintProjectRoot(filePath) {
428
+ let dir = node_path.default.resolve(node_path.default.dirname(filePath));
429
+ const { root } = node_path.default.parse(dir);
430
+ while (true) {
431
+ for (const name of ESLINT_CONFIG_FILENAMES) if ((0, node_fs.existsSync)(node_path.default.join(dir, name))) return normalizeDir(dir);
432
+ if (dir === root) return null;
433
+ dir = node_path.default.dirname(dir);
434
+ }
435
+ }
436
+ /**
437
+ * Pick the directory to relativize linted file paths against.
438
+ *
439
+ * Precedence: explicit `rootDir` → nearest `eslint.config.*` ancestor → `cwd`.
440
+ */
441
+ function resolveProjectRoot(filename, { rootDir, cwd } = {}) {
442
+ if (rootDir) return normalizeDir(rootDir);
443
+ if (filename) {
444
+ const fromConfig = findEslintProjectRoot(filename);
445
+ if (fromConfig) return fromConfig;
446
+ }
447
+ return cwd ? normalizeDir(cwd) : void 0;
448
+ }
449
+
450
+ //#endregion
451
+ //#region src/next/lib/protection-checks.ts
452
+ const CLERK_AUTH_SOURCE = "@clerk/nextjs/server";
453
+ /**
454
+ * Collect the local names that `auth` is imported as from `@clerk/nextjs/server`
455
+ * in the given module. Usually returns `{'auth'}`, but handles aliased imports
456
+ * like `import { auth as clerkAuth } from '@clerk/nextjs/server'` correctly.
457
+ * Type-only imports (`import type { auth }` / `import { type auth }`) are
458
+ * excluded — they are erased at compile time and do not provide a runtime binding.
459
+ */
460
+ function findAuthLocalNames(programNode) {
461
+ const names = /* @__PURE__ */ new Set();
462
+ for (const stmt of programNode.body) {
463
+ if (stmt.type !== "ImportDeclaration") continue;
464
+ if (stmt.source.value !== CLERK_AUTH_SOURCE) continue;
465
+ if (stmt.importKind === "type") continue;
466
+ for (const spec of stmt.specifiers) {
467
+ if (spec.type !== "ImportSpecifier") continue;
468
+ if (spec.importKind === "type") continue;
469
+ if ((spec.imported.type === "Identifier" ? spec.imported.name : spec.imported.value) === "auth") names.add(spec.local.name);
470
+ }
471
+ }
472
+ return names;
473
+ }
474
+ const AUTH_FIELDS = new Set([
475
+ "userId",
476
+ "sessionId",
477
+ "isAuthenticated"
478
+ ]);
479
+ const EXIT_FUNCTIONS = new Set([
480
+ "redirect",
481
+ "permanentRedirect",
482
+ "notFound",
483
+ "unauthorized",
484
+ "forbidden",
485
+ "redirectToSignIn",
486
+ "redirectToSignUp"
487
+ ]);
488
+ function isProtectCall(node, authNames) {
489
+ if (!node || node.type !== "CallExpression") return false;
490
+ const callee = node.callee;
491
+ if (callee.type !== "MemberExpression") return false;
492
+ if (callee.property.type !== "Identifier" || callee.property.name !== "protect") return false;
493
+ if (callee.object.type === "Identifier" && authNames.has(callee.object.name)) return true;
494
+ if (callee.object.type === "AwaitExpression" && callee.object.argument.type === "CallExpression" && callee.object.argument.callee.type === "Identifier" && authNames.has(callee.object.argument.callee.name)) return true;
495
+ return false;
496
+ }
497
+ function isProtectAwait(node, authNames) {
498
+ return !!node && node.type === "AwaitExpression" && isProtectCall(node.argument, authNames);
499
+ }
500
+ function isProtectAwaitStatement(stmt, authNames) {
501
+ if (stmt.type === "ExpressionStatement") return isProtectAwait(stmt.expression, authNames);
502
+ if (stmt.type === "VariableDeclaration") {
503
+ const first = stmt.declarations[0];
504
+ return first != null && isProtectAwait(first.init ?? void 0, authNames);
505
+ }
506
+ return false;
507
+ }
508
+ function capturedAuthBindings(stmt, authNames) {
509
+ if (stmt.type !== "VariableDeclaration") return null;
510
+ if (stmt.declarations.length !== 1) return null;
511
+ const decl = stmt.declarations[0];
512
+ if (!decl) return null;
513
+ if (decl.id.type !== "ObjectPattern") return null;
514
+ if (!decl.init || decl.init.type !== "AwaitExpression") return null;
515
+ const arg = decl.init.argument;
516
+ if (arg.type !== "CallExpression") return null;
517
+ if (arg.callee.type !== "Identifier" || !authNames.has(arg.callee.name)) return null;
518
+ const bindings = /* @__PURE__ */ new Set();
519
+ for (const prop of decl.id.properties) {
520
+ if (prop.type !== "Property") continue;
521
+ if (prop.key.type !== "Identifier") continue;
522
+ const fieldName = prop.key.name;
523
+ if (!AUTH_FIELDS.has(fieldName)) continue;
524
+ if (prop.value.type !== "Identifier" || prop.value.name !== fieldName) continue;
525
+ bindings.add(fieldName);
526
+ }
527
+ return bindings.size > 0 ? bindings : null;
528
+ }
529
+ function isRecognizedAuthCheck(test, bindings) {
530
+ if (test.type === "UnaryExpression" && test.operator === "!") return test.argument.type === "Identifier" && AUTH_FIELDS.has(test.argument.name) && bindings.has(test.argument.name);
531
+ if (test.type !== "BinaryExpression") return false;
532
+ if (test.operator !== "===" && test.operator !== "==") return false;
533
+ let id;
534
+ let other;
535
+ if (test.left.type === "Identifier" && bindings.has(test.left.name)) {
536
+ id = test.left;
537
+ other = test.right;
538
+ } else if (test.right.type === "Identifier" && bindings.has(test.right.name)) {
539
+ id = test.right;
540
+ other = test.left;
541
+ } else return false;
542
+ const name = id.name;
543
+ if (name === "userId" || name === "sessionId") return other.type === "Literal" && other.value === null;
544
+ if (name === "isAuthenticated") return test.operator === "===" && other.type === "Literal" && other.value === false;
545
+ return false;
546
+ }
547
+ function isExitCall(expr) {
548
+ if (!expr || expr.type !== "CallExpression") return false;
549
+ if (expr.callee.type !== "Identifier") return false;
550
+ return EXIT_FUNCTIONS.has(expr.callee.name);
551
+ }
552
+ function statementExits(stmt) {
553
+ if (!stmt) return false;
554
+ if (stmt.type === "ReturnStatement") return true;
555
+ if (stmt.type === "ThrowStatement") return true;
556
+ if (stmt.type === "ExpressionStatement") return isExitCall(stmt.expression);
557
+ return false;
558
+ }
559
+ function consequentExits(consequent) {
560
+ if (!consequent) return false;
561
+ if (statementExits(consequent)) return true;
562
+ if (consequent.type === "BlockStatement") return consequent.body.some(statementExits);
563
+ return false;
564
+ }
565
+ function isAuthGuardWithExit(stmt, bindings) {
566
+ if (stmt.type !== "IfStatement") return false;
567
+ if (!isRecognizedAuthCheck(stmt.test, bindings)) return false;
568
+ return consequentExits(stmt.consequent);
569
+ }
570
+ function isNonRuntimeStatement(stmt) {
571
+ if (stmt.type === "ExpressionStatement" && stmt.directive) return true;
572
+ if (stmt.type === "TSTypeAliasDeclaration") return true;
573
+ if (stmt.type === "TSInterfaceDeclaration") return true;
574
+ return false;
575
+ }
576
+ function nextExecutable(stmts, from) {
577
+ let i = from;
578
+ while (i < stmts.length && isNonRuntimeStatement(stmts[i])) i++;
579
+ return i;
580
+ }
581
+ function hasProtectAtTop(fn, authNames) {
582
+ if (!fn || !fn.async) return false;
583
+ const body = fn.body;
584
+ if (body && body.type !== "BlockStatement") return body.type === "AwaitExpression" && isProtectCall(body.argument, authNames);
585
+ if (!body || body.type !== "BlockStatement") return false;
586
+ const stmts = body.body;
587
+ const first = nextExecutable(stmts, 0);
588
+ if (first >= stmts.length) return false;
589
+ if (isProtectAwaitStatement(stmts[first], authNames)) return true;
590
+ const captured = capturedAuthBindings(stmts[first], authNames);
591
+ if (captured) {
592
+ const second = nextExecutable(stmts, first + 1);
593
+ if (second < stmts.length && isAuthGuardWithExit(stmts[second], captured)) return true;
594
+ }
595
+ return false;
596
+ }
597
+
598
+ //#endregion
599
+ //#region src/next/require-auth-protection.ts
600
+ const HTTP_METHODS = new Set([
601
+ "GET",
602
+ "POST",
603
+ "PUT",
604
+ "PATCH",
605
+ "DELETE",
606
+ "OPTIONS",
607
+ "HEAD"
608
+ ]);
609
+ const DEFAULT_RESOURCES = {
610
+ routeHandlers: true,
611
+ serverFunctions: true,
612
+ serverComponentEntrypoints: true
613
+ };
614
+ const rule = {
615
+ meta: {
616
+ type: "problem",
617
+ hasSuggestions: true,
618
+ docs: { description: "Require `await auth.protect()` in App Router resources under protected folders" },
619
+ schema: [{
620
+ type: "object",
621
+ properties: {
622
+ protected: {
623
+ type: "array",
624
+ items: { type: "string" },
625
+ minItems: 1
626
+ },
627
+ public: {
628
+ type: "array",
629
+ items: { type: "string" }
630
+ },
631
+ resources: {
632
+ type: "object",
633
+ properties: {
634
+ routeHandlers: { type: "boolean" },
635
+ serverFunctions: { type: "boolean" },
636
+ serverComponentEntrypoints: { type: "boolean" }
637
+ },
638
+ additionalProperties: false
639
+ },
640
+ mixedScopeLayouts: { oneOf: [{
641
+ type: "string",
642
+ enum: ["auto"]
643
+ }, {
644
+ type: "array",
645
+ items: { type: "string" }
646
+ }] },
647
+ rootDir: { type: "string" }
648
+ },
649
+ required: ["protected"],
650
+ additionalProperties: false
651
+ }],
652
+ messages: {
653
+ missingProtect: "Expected `await auth.protect()` at the top of {{subject}} in a folder configured as protected. Add the call to the top of the function, move the file into a public folder, or configure this folder as public.",
654
+ addAuthProtect: "Add `await auth.protect()` to the top of this {{subject}}.",
655
+ exportImported: "This {{subject}} is exported from '{{source}}'. The rule cannot follow imports across files. Add a wrapper with `await auth.protect()`, or ensure the imported function calls it and add an eslint-disable comment with a reason.",
656
+ unverifiableExport: "This {{subject}} could not be verified as being protected, likely because it is assigned from a call expression (e.g. `const handler = withAuth(impl)`). Inline a function literal that calls `await auth.protect()`, or add an eslint-disable comment with a reason.",
657
+ unlistedMixedScopeLayout: "This {{fileKind}} at '{{folder}}/' wraps both protected and public descendants but is not listed in `mixedScopeLayouts`. Either add '{{folder}}' to the list to acknowledge the mixed scope, or restructure so the {{fileKind}} wraps only public or protected descendants."
658
+ }
659
+ },
660
+ create(context) {
661
+ const filename = context.physicalFilename ?? context.filename ?? context.getFilename?.();
662
+ const cwd = context.cwd || context.getCwd?.();
663
+ const options = context.options[0] ?? {};
664
+ const config = {
665
+ protected: options.protected,
666
+ public: options.public ?? []
667
+ };
668
+ const resources = normalizeResources(options.resources);
669
+ const mixedScopeLayoutsOption = options.mixedScopeLayouts === void 0 ? "auto" : options.mixedScopeLayouts;
670
+ const folder = getRelativeFolder(filename, resolveProjectRoot(filename, {
671
+ rootDir: options.rootDir,
672
+ cwd
673
+ }));
674
+ if (!folder) return {};
675
+ const fileKind = getFileKind(filename);
676
+ let authNames = /* @__PURE__ */ new Set();
677
+ let shouldCheckInlineServerFunctions = false;
678
+ const checkedFunctions = /* @__PURE__ */ new WeakSet();
679
+ return {
680
+ Program(programNode) {
681
+ const ast = programNode;
682
+ const isServerFunction = isServerFunctionModule(ast);
683
+ const isClient = isClientModule(ast);
684
+ if (classifyFolder(folder, config) !== "protected") return;
685
+ authNames = findAuthLocalNames(ast);
686
+ shouldCheckInlineServerFunctions = resources.serverFunctions && !isClient;
687
+ if (!fileKind && !isServerFunction) return;
688
+ if (isClient && (fileKind === "page" || fileKind === "layout" || fileKind === "template" || fileKind === "default")) return;
689
+ if (resources.serverComponentEntrypoints && (fileKind === "layout" || fileKind === "template") && hasDescendantsMatching(folder, config.public)) {
690
+ checkUnacknowledgedMixedScope(context, ast, fileKind, folder, mixedScopeLayoutsOption);
691
+ return;
692
+ }
693
+ if (resources.serverComponentEntrypoints && (fileKind === "page" || fileKind === "layout" || fileKind === "template" || fileKind === "default")) checkDefaultExport(context, ast, fileKind, authNames, checkedFunctions);
694
+ else if (resources.routeHandlers && fileKind === "route") checkRouteHandlers(context, ast, authNames, checkedFunctions);
695
+ else if (resources.serverFunctions && isServerFunction) checkServerFunctions(context, ast, authNames, checkedFunctions);
696
+ },
697
+ FunctionDeclaration(node) {
698
+ checkInlineServerFunction(context, node, authNames, shouldCheckInlineServerFunctions, checkedFunctions);
699
+ },
700
+ FunctionExpression(node) {
701
+ checkInlineServerFunction(context, node, authNames, shouldCheckInlineServerFunctions, checkedFunctions);
702
+ },
703
+ ArrowFunctionExpression(node) {
704
+ checkInlineServerFunction(context, node, authNames, shouldCheckInlineServerFunctions, checkedFunctions);
705
+ }
706
+ };
707
+ }
708
+ };
709
+ function normalizeResources(resources) {
710
+ return {
711
+ ...DEFAULT_RESOURCES,
712
+ ...resources
713
+ };
714
+ }
715
+ function checkUnacknowledgedMixedScope(context, programNode, fileKind, folder, mixedScopeLayoutsOption) {
716
+ if (mixedScopeLayoutsOption === "auto") return;
717
+ if (mixedScopeLayoutsOption.includes(folder)) return;
718
+ const defaultExport = programNode.body.find((n) => n.type === "ExportDefaultDeclaration");
719
+ context.report({
720
+ node: defaultExport ?? programNode,
721
+ messageId: "unlistedMixedScopeLayout",
722
+ data: {
723
+ folder,
724
+ fileKind
725
+ }
726
+ });
727
+ }
728
+ function getMissingProtectReportNode(fn, fallback) {
729
+ return fn.id ?? fn.body ?? fallback;
730
+ }
731
+ function checkMissingProtect(context, reportNode, target, subject, authNames, checkedFunctions) {
732
+ if (target.kind === "imported") {
733
+ context.report({
734
+ node: reportNode,
735
+ messageId: "exportImported",
736
+ data: {
737
+ subject,
738
+ source: target.source
739
+ }
740
+ });
741
+ return;
742
+ }
743
+ if (target.kind === "function") {
744
+ checkedFunctions?.add(target.node);
745
+ if (!hasProtectAtTop(target.node, authNames)) context.report({
746
+ node: getMissingProtectReportNode(target.node, reportNode),
747
+ messageId: "missingProtect",
748
+ data: { subject },
749
+ suggest: buildAddAuthProtectSuggestion(context, target.node, subject, authNames)
750
+ });
751
+ return;
752
+ }
753
+ context.report({
754
+ node: reportNode,
755
+ messageId: "unverifiableExport",
756
+ data: { subject }
757
+ });
758
+ }
759
+ function checkRouteHandlers(context, programNode, authNames, checkedFunctions) {
760
+ for (const { name, target, reportNode } of iterateNamedExports(programNode)) {
761
+ if (!HTTP_METHODS.has(name)) continue;
762
+ checkMissingProtect(context, reportNode, target, `${name} handler`, authNames, checkedFunctions);
763
+ }
764
+ for (const { source, reportNode } of iterateExportAllDeclarations(programNode)) checkMissingProtect(context, reportNode, {
765
+ kind: "imported",
766
+ source
767
+ }, "route handlers", authNames);
768
+ }
769
+ function checkServerFunctions(context, programNode, authNames, checkedFunctions) {
770
+ for (const { name, target, reportNode } of iterateNamedExports(programNode)) {
771
+ if (name === "default") continue;
772
+ checkMissingProtect(context, reportNode, target, `Server Function '${name}'`, authNames, checkedFunctions);
773
+ }
774
+ const defaultExport = resolveDefaultExport(programNode);
775
+ if (defaultExport) checkMissingProtect(context, defaultExport.reportNode, defaultExport.target, "Server Function", authNames, checkedFunctions);
776
+ for (const { source, reportNode } of iterateExportAllDeclarations(programNode)) checkMissingProtect(context, reportNode, {
777
+ kind: "imported",
778
+ source
779
+ }, "Server Functions", authNames);
780
+ }
781
+ function checkDefaultExport(context, programNode, fileKind, authNames, checkedFunctions) {
782
+ const defaultExport = resolveDefaultExport(programNode);
783
+ if (!defaultExport) return;
784
+ checkMissingProtect(context, defaultExport.reportNode, defaultExport.target, fileKind, authNames, checkedFunctions);
785
+ }
786
+ function hasUseServerDirective(fn) {
787
+ const body = fn.body;
788
+ if (!body || body.type !== "BlockStatement") return false;
789
+ for (const stmt of body.body) {
790
+ if (stmt.type !== "ExpressionStatement") return false;
791
+ if (typeof stmt.directive !== "string") return false;
792
+ if (stmt.directive === "use server") return true;
793
+ }
794
+ return false;
795
+ }
796
+ function checkInlineServerFunction(context, fn, authNames, shouldCheckInlineServerFunctions, checkedFunctions) {
797
+ if (!shouldCheckInlineServerFunctions || checkedFunctions.has(fn) || !hasUseServerDirective(fn)) return;
798
+ if (!hasProtectAtTop(fn, authNames)) context.report({
799
+ node: getMissingProtectReportNode(fn, fn),
800
+ messageId: "missingProtect",
801
+ data: { subject: "Inline Server Function" },
802
+ suggest: buildAddAuthProtectSuggestion(context, fn, "Inline Server Function", authNames)
803
+ });
804
+ }
805
+ function buildAddAuthProtectSuggestion(context, fn, subject, authNames) {
806
+ const sourceCode = context.sourceCode;
807
+ return [{
808
+ messageId: "addAuthProtect",
809
+ data: { subject },
810
+ fix(fixer) {
811
+ return buildAuthProtectFixes({
812
+ fixer,
813
+ sourceCode,
814
+ fn,
815
+ authNames
816
+ });
817
+ }
818
+ }];
819
+ }
820
+
821
+ //#endregion
822
+ //#region src/next/index.ts
823
+ const plugin = {
824
+ meta: {
825
+ name: "@clerk/eslint-plugin/next",
826
+ version: "0.1.0"
827
+ },
828
+ rules: { "require-auth-protection": rule }
829
+ };
830
+
831
+ //#endregion
832
+ module.exports = plugin;
833
+ //# sourceMappingURL=next.js.map