server-action-audit 0.0.0-stage → 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/LICENSE +21 -0
- package/README.fr.md +54 -0
- package/README.md +163 -2
- package/dist/chunk-GQSNZNKN.js +677 -0
- package/dist/chunk-GQSNZNKN.js.map +1 -0
- package/dist/cli.js +964 -0
- package/dist/cli.js.map +1 -0
- package/dist/eslint.cjs +796 -0
- package/dist/eslint.cjs.map +1 -0
- package/dist/eslint.d.cts +15 -0
- package/dist/eslint.d.ts +15 -0
- package/dist/eslint.js +112 -0
- package/dist/eslint.js.map +1 -0
- package/dist/index.cjs +933 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +123 -0
- package/dist/index.d.ts +123 -0
- package/dist/index.js +234 -0
- package/dist/index.js.map +1 -0
- package/package.json +89 -4
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/eslint.ts","../src/ast.ts","../src/options.ts","../src/analyze.ts","../src/resolve.ts","../src/version.ts"],"sourcesContent":["import type { ESLint, Linter, Rule } from 'eslint'\nimport { analyzeModule, type ServerAction } from './analyze.js'\nimport type { Comment, Node } from './ast.js'\nimport { resolveOptions, type AuditOptions } from './options.js'\nimport { createNodeResolver } from './resolve.js'\nimport { VERSION } from './version.js'\n\nconst resolver = createNodeResolver()\n\nconst optionsSchema = {\n type: 'object',\n properties: {\n authFunctions: { type: 'array', items: { type: 'string' } },\n authWrappers: { type: 'array', items: { type: 'string' } },\n authClients: { type: 'array', items: { type: 'string' } },\n publicActions: { type: 'array', items: { type: 'string' } },\n },\n additionalProperties: false,\n} as const\n\n/** Options from the rule, merged over `settings.serverActionAudit` (shared by both rules). */\nfunction actionsOf(context: Rule.RuleContext): ServerAction[] {\n const text = context.sourceCode.text\n if (!text.includes('use server')) return []\n const shared = (context.settings?.serverActionAudit ?? {}) as AuditOptions\n const own = (context.options[0] ?? {}) as AuditOptions\n const options = resolveOptions({\n authFunctions: [...(shared.authFunctions ?? []), ...(own.authFunctions ?? [])],\n authWrappers: [...(shared.authWrappers ?? []), ...(own.authWrappers ?? [])],\n authClients: [...(shared.authClients ?? []), ...(own.authClients ?? [])],\n publicActions: [...(shared.publicActions ?? []), ...(own.publicActions ?? [])],\n })\n return analyzeModule(\n {\n ast: context.sourceCode.ast as unknown as Node,\n text,\n filename: context.physicalFilename ?? context.filename,\n comments: context.sourceCode.getAllComments() as unknown as Comment[],\n },\n options,\n resolver,\n )\n}\n\nconst requireAuth: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Require an authorization check in every Next.js Server Action',\n url: 'https://github.com/i6moons/server-action-audit#require-auth',\n },\n schema: [optionsSchema],\n messages: {\n missingAuth:\n 'Server Action \"{{name}}\" has no authorization check: anyone can call it with a POST request. Check the session inside the action, or mark it with // @public-action if it is public on purpose.{{hint}}',\n },\n },\n create(context) {\n return {\n 'Program:exit'() {\n for (const action of actionsOf(context)) {\n if (action.status !== 'unprotected') continue\n context.report({\n node: action.node as unknown as Rule.Node,\n messageId: 'missingAuth',\n data: { name: action.name, hint: action.hint ? ` ${action.hint}` : '' },\n })\n }\n },\n }\n },\n}\n\nconst requireInputValidation: Rule.RuleModule = {\n meta: {\n type: 'suggestion',\n docs: {\n description: 'Require Server Actions to validate their arguments (Zod, Valibot, a next-safe-action schema…)',\n url: 'https://github.com/i6moons/server-action-audit#require-input-validation',\n },\n schema: [optionsSchema],\n messages: {\n missingValidation:\n 'Server Action \"{{name}}\" uses its arguments without validating them. They come straight from the client: parse them with a schema (Zod, Valibot…).',\n },\n },\n create(context) {\n return {\n 'Program:exit'() {\n for (const action of actionsOf(context)) {\n if (action.validated !== false) continue\n context.report({ node: action.node as unknown as Rule.Node, messageId: 'missingValidation', data: { name: action.name } })\n }\n },\n }\n },\n}\n\nconst plugin = {\n meta: { name: 'server-action-audit', version: VERSION },\n rules: {\n 'require-auth': requireAuth,\n 'require-input-validation': requireInputValidation,\n },\n configs: {} as Record<string, Linter.Config>,\n} satisfies ESLint.Plugin & { configs: Record<string, Linter.Config> }\n\nplugin.configs.recommended = {\n name: 'server-action-audit/recommended',\n plugins: { 'server-action-audit': plugin },\n rules: {\n 'server-action-audit/require-auth': 'error',\n 'server-action-audit/require-input-validation': 'warn',\n },\n}\n\nexport default plugin\n","/** Minimal ESTree helpers, shared by the CLI (typescript-estree) and ESLint (espree or typescript-eslint). */\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type Node = { type: string; range?: [number, number]; loc?: { start: { line: number; column: number } }; [key: string]: any }\n\nexport interface Comment {\n type: string\n value: string\n range?: [number, number]\n}\n\nconst SKIP = new Set(['parent', 'loc', 'range', 'tokens', 'comments', 'typeAnnotation', 'returnType', 'typeParameters'])\n\n/** Calls `visit` on every node under `root`. Return `false` from `visit` to skip a node's children. */\nexport function walk(root: Node | null | undefined, visit: (node: Node) => boolean | void): void {\n if (!root || typeof root.type !== 'string') return\n if (visit(root) === false) return\n for (const key of Object.keys(root)) {\n if (SKIP.has(key)) continue\n const value = root[key]\n if (Array.isArray(value)) {\n for (const child of value) if (child && typeof child.type === 'string') walk(child, visit)\n } else if (value && typeof value.type === 'string') {\n walk(value, visit)\n }\n }\n}\n\nexport function isFunction(node: Node | null | undefined): boolean {\n return !!node && (node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression')\n}\n\n/** Leading string-literal statements of a program or function body. */\nexport function directives(body: Node[] | undefined): string[] {\n const found: string[] = []\n for (const statement of body ?? []) {\n if (statement.type !== 'ExpressionStatement') break\n if (typeof statement.directive === 'string') found.push(statement.directive)\n else if (statement.expression?.type === 'Literal' && typeof statement.expression.value === 'string') found.push(statement.expression.value)\n else break\n }\n return found\n}\n\nexport function functionDirectives(fn: Node): string[] {\n return fn.body?.type === 'BlockStatement' ? directives(fn.body.body) : []\n}\n\nfunction unwrap(node: Node): Node {\n let current = node\n while (current.type === 'ChainExpression' || current.type === 'TSNonNullExpression' || current.type === 'TSAsExpression' || current.type === 'ParenthesizedExpression') {\n current = current.expression\n }\n return current\n}\n\n/**\n * Dotted path of a call's callee: `auth` → \"auth\", `supabase.auth.getUser` → \"supabase.auth.getUser\",\n * `(await createClient()).auth.getUser` → \"*.auth.getUser\". Computed members become \"*\".\n */\nexport function calleePath(callee: Node): string {\n const node = unwrap(callee)\n if (node.type === 'Identifier') return node.name\n if (node.type === 'MemberExpression') {\n const property = !node.computed && node.property?.type === 'Identifier' ? node.property.name : '*'\n return `${calleePath(node.object)}.${property}`\n }\n if (node.type === 'Super') return 'super'\n if (node.type === 'ThisExpression') return 'this'\n return '*'\n}\n\nexport function lastSegment(path: string): string {\n const index = path.lastIndexOf('.')\n return index === -1 ? path : path.slice(index + 1)\n}\n\n/** Every call expression inside `root`, nested functions included. */\nexport function callsIn(root: Node | null | undefined): Node[] {\n const calls: Node[] = []\n walk(root, (node) => {\n if (node.type === 'CallExpression') calls.push(node)\n })\n return calls\n}\n\n/**\n * Calls that run on the main path of `fn`: top-level statements, the condition of an `if`, the\n * body of a `try`. Calls in an `if` branch, a loop body or a nested callback may never run, so\n * they do not count as a guard. Inside an expression everything counts, so helper idioms like\n * `return token ? verify(token) : null` still do.\n *\n * With `followReturned`, a function returned at the top level is included too: that is the\n * function a wrapper like `withTeam(action)` actually runs on each request.\n */\nexport function unconditionalCalls(fn: Node, followReturned = false): Node[] {\n const calls: Node[] = []\n const expression = (node: Node | null | undefined): void => {\n if (!node || typeof node.type !== 'string' || isFunction(node)) return\n if (node.type === 'CallExpression') calls.push(node)\n for (const key of Object.keys(node)) {\n if (SKIP.has(key)) continue\n const value = node[key]\n if (Array.isArray(value)) value.forEach((child) => child && expression(child))\n else if (value && typeof value.type === 'string') expression(value)\n }\n }\n const returned = (node: Node | null | undefined): void => {\n if (followReturned && node && isFunction(node)) calls.push(...unconditionalCalls(node))\n else expression(node)\n }\n const statement = (node: Node | null | undefined): void => {\n if (!node) return\n switch (node.type) {\n case 'BlockStatement':\n for (const child of node.body ?? []) statement(child)\n break\n case 'ExpressionStatement':\n expression(node.expression)\n break\n case 'VariableDeclaration':\n for (const declarator of node.declarations ?? []) expression(declarator.init)\n break\n case 'ReturnStatement':\n returned(node.argument)\n break\n case 'ThrowStatement':\n expression(node.argument)\n break\n case 'IfStatement':\n expression(node.test)\n break\n case 'TryStatement':\n statement(node.block)\n break\n case 'SwitchStatement':\n expression(node.discriminant)\n break\n case 'DoWhileStatement':\n statement(node.body)\n break\n case 'LabeledStatement':\n statement(node.body)\n break\n }\n }\n if (fn.body?.type === 'BlockStatement') statement(fn.body)\n else returned(fn.body)\n return calls\n}\n\n/**\n * For `a.b(x).c(y).d(fn)`: the root identifier (\"a\"), the method names in call order ([\"b\", \"c\", \"d\"])\n * and the function arguments passed anywhere in the chain.\n */\nexport function callChain(call: Node): { root: string | null; methods: string[]; functions: Node[]; identifiers: string[] } {\n const methods: string[] = []\n const functions: Node[] = []\n /** Identifiers passed as arguments: `.use(authMiddleware)`. */\n const identifiers: string[] = []\n let current: Node = unwrap(call)\n for (;;) {\n if (current.type === 'CallExpression') {\n for (const argument of current.arguments ?? []) {\n if (isFunction(argument)) functions.unshift(argument)\n else if (argument.type === 'Identifier') identifiers.unshift(argument.name)\n }\n const callee = unwrap(current.callee)\n if (callee.type === 'MemberExpression') {\n methods.unshift(!callee.computed && callee.property?.type === 'Identifier' ? callee.property.name : '*')\n current = unwrap(callee.object)\n continue\n }\n if (callee.type === 'Identifier') return { root: callee.name, methods, functions, identifiers }\n return { root: null, methods, functions, identifiers }\n }\n if (current.type === 'MemberExpression') {\n methods.unshift(!current.computed && current.property?.type === 'Identifier' ? current.property.name : '*')\n current = unwrap(current.object)\n continue\n }\n if (current.type === 'AwaitExpression') {\n current = unwrap(current.argument)\n continue\n }\n return { root: current.type === 'Identifier' ? current.name : null, methods, functions, identifiers }\n }\n}\n\nexport function position(node: Node): { line: number; column: number } {\n return node.loc ? { line: node.loc.start.line, column: node.loc.start.column + 1 } : { line: 0, column: 0 }\n}\n","import { lastSegment } from './ast.js'\n\nexport interface AuditOptions {\n /**\n * Extra functions that check who is calling. A plain name matches the last part of the call\n * (`getMe` matches `getMe()` and `lib.getMe()`); a dotted name matches the end of the path\n * (`session.require` matches `ctx.session.require()`).\n */\n authFunctions?: string[]\n /** Extra higher-order functions that wrap an action with an auth check: `export const a = withTeam(async () => …)`. */\n authWrappers?: string[]\n /** Extra action clients with auth middleware (next-safe-action, zsa): `teamClient.action(…)`. */\n authClients?: string[]\n /** Actions that are public on purpose (login, newsletter…). Same effect as a `// @public-action` comment. */\n publicActions?: string[]\n /** Files to skip (CLI only), as globs relative to the project root: `[\"app/legacy/**\"]`. */\n ignore?: string[]\n}\n\n/** Auth.js / NextAuth v5 and Clerk: a bare `auth()` call. Other libraries' `x.auth()` are not checks. */\nconst EXACT_PATHS = ['auth']\n\n/** Matched against the last part of the call. */\nconst NAMES = [\n 'currentUser', // Clerk\n 'getServerSession', // NextAuth v4\n 'getKindeServerSession', // Kinde\n 'validateRequest', // Lucia\n 'getIronSession', // iron-session\n 'withAuth', // WorkOS AuthKit\n 'verifyIdToken', // Firebase Admin\n 'verifySessionCookie', // Firebase Admin\n 'jwtVerify', // jose\n 'verifyToken',\n 'verifyJwt',\n 'verifyAccessToken',\n 'getSession',\n 'verifySession',\n 'getCurrentUser',\n 'getAuthUser',\n 'getAuthenticatedUser',\n 'getSessionUser',\n 'getUserOrThrow',\n 'requireAuth',\n 'requireUser',\n 'requireSession',\n 'requireAdmin',\n 'requireRole',\n 'requirePermission',\n 'checkAuth',\n 'ensureAuth',\n 'ensureUser',\n 'assertAuth',\n 'assertUser',\n 'isAuthenticated',\n 'authenticate',\n 'authorize',\n]\n\n/** Matched against the end of the call path. */\nconst SUFFIXES = [\n 'auth.getUser', // Supabase\n 'auth.getClaims', // Supabase\n 'auth.getSession', // Supabase\n 'auth.protect', // Clerk\n 'api.getSession', // Better Auth\n]\n\n/** Wrapper or client names that suggest an auth check: withAuth, validatedActionWithUser, authActionClient… */\nexport const AUTH_NAME = /auth|protect|guard|session|user|admin|private|secure|logged/i\n\n/**\n * Helpers worth following when looking for an auth check: getCurrentUser, requireOwner, getViewer,\n * verifySession… A helper with an unrelated name (sendEmail, getLocale) may read the session for\n * other reasons, such as the user's language, so it is not followed.\n */\nexport const HELPER_NAME = /auth|session|user|viewer|account|member|owner|admin|role|permission|access|identity|token|login|guard|protect|secure|private|require|ensure|assert|verify|check|current|^me$|^whoami$/i\n\n/**\n * Guards that read the session to keep logged-in users OUT (sign-up, login, password reset):\n * `throwIfAuthenticated`, `redirectIfLoggedIn`, `requireGuest`. They call `getSession()` but\n * protect nothing, so they are never followed or trusted.\n */\nexport const GUEST_GUARD = /(?:throw|redirect|fail|reject|error)If(?:Authenticated|Authed|LoggedIn|SignedIn|Session)|requireGuest|guestOnly|onlyGuests?|(?:require|ensure|assert)(?:Not|No)(?:Authenticated|LoggedIn|SignedIn|Session|User)|unauthenticated|anonymous/i\n\nexport interface ResolvedOptions {\n exactPaths: Set<string>\n names: Set<string>\n suffixes: string[]\n authWrappers: Set<string>\n authClients: Set<string>\n publicActions: Set<string>\n ignore: string[]\n}\n\nexport function resolveOptions(options: AuditOptions = {}): ResolvedOptions {\n const extra = options.authFunctions ?? []\n return {\n exactPaths: new Set(EXACT_PATHS),\n names: new Set([...NAMES, ...extra.filter((name) => !name.includes('.'))]),\n suffixes: [...SUFFIXES, ...extra.filter((name) => name.includes('.'))],\n authWrappers: new Set(options.authWrappers ?? []),\n authClients: new Set(options.authClients ?? []),\n publicActions: new Set(options.publicActions ?? []),\n ignore: options.ignore ?? [],\n }\n}\n\nexport function isAuthCall(path: string, options: ResolvedOptions): boolean {\n if (options.exactPaths.has(path)) return true\n if (options.names.has(lastSegment(path))) return true\n return options.suffixes.some((suffix) => path === suffix || path.endsWith(`.${suffix}`))\n}\n\n/** `schema.parse(x)`, `v.safeParse(schema, x)`… but not `JSON.parse` or `Date.parse`. */\nconst VALIDATION_METHODS = new Set(['parse', 'safeParse', 'parseAsync', 'safeParseAsync', 'validate', 'validateSync', 'validateAsync', 'assert'])\nconst NOT_VALIDATION = new Set(['JSON', 'Date', 'Number', 'URL', 'path', 'Math', 'BigInt', 'console', 'assert'])\n\nexport function isValidationCall(path: string): boolean {\n const method = lastSegment(path)\n if (!VALIDATION_METHODS.has(method)) return false\n const owner = path.includes('.') ? path.slice(0, path.lastIndexOf('.')) : ''\n return !NOT_VALIDATION.has(lastSegment(owner) || owner)\n}\n","import { callChain, calleePath, callsIn, directives, functionDirectives, isFunction, position, unconditionalCalls, walk, type Comment, type Node } from './ast.js'\nimport { AUTH_NAME, GUEST_GUARD, HELPER_NAME, isAuthCall, isValidationCall, type ResolvedOptions } from './options.js'\n\nexport type ActionStatus = 'protected' | 'unprotected' | 'public'\n\nexport interface ServerAction {\n /** Exported name, function name, or `(inline)` for an anonymous inline action. */\n name: string\n /** `module`: exported from a \"use server\" file. `inline`: a function with its own \"use server\" directive. */\n kind: 'module' | 'inline'\n line: number\n column: number\n status: ActionStatus\n /** Why it counts as protected or public: `auth()`, `getUser() → supabase.auth.getUser()`, `authActionClient client`… */\n reason: string | null\n /** `false` when the action takes arguments and nothing validates them; `null` when it takes none. */\n validated: boolean | null\n /** Advice when the tool could not see an auth check. */\n hint: string | null\n node: Node\n}\n\nexport interface ModuleSource {\n ast: Node\n filename: string\n}\n\n/** Turns an import specifier into the parsed module, or `null` (package, missing file…). */\nexport type ImportResolver = (specifier: string, fromFile: string) => ModuleSource | null\n\nexport interface AnalyzeInput {\n ast: Node\n text: string\n filename: string\n comments?: Comment[]\n}\n\nconst PUBLIC_MARKER = /@public-action\\b/\nconst CLIENT_METHODS = new Set(['action', 'stateAction', 'handler', 'createServerAction', 'createAction'])\nconst SCHEMA_METHODS = new Set(['schema', 'inputSchema', 'input', 'bindArgsSchemas'])\nconst MAX_DEPTH = 4\n\ntype Reexport = { source: string; imported: string }\n\n/** What a module declares at the top level: functions, imports and exports. */\nclass ModuleScope {\n readonly functions = new Map<string, Node>()\n readonly imports = new Map<string, Reexport>()\n readonly exports = new Map<string, string | Reexport>()\n /** Top-level declarations by name, with the statement that holds them (for comments). */\n readonly declarations = new Map<string, { init: Node; statement: Node }>()\n\n constructor(readonly ast: Node, readonly filename: string) {\n for (const statement of ast.body ?? []) this.index(statement)\n }\n\n private declare(name: string, init: Node, statement: Node): void {\n this.declarations.set(name, { init, statement })\n if (isFunction(init)) this.functions.set(name, init)\n // `export const getUser = cache(async () => …)`: the wrapped function is what runs.\n else if (init.type === 'CallExpression') {\n const wrapped = (init.arguments ?? []).find((argument: Node) => isFunction(argument))\n if (wrapped) this.functions.set(name, wrapped)\n }\n }\n\n private index(statement: Node): void {\n const declaration = statement.type === 'ExportNamedDeclaration' ? statement.declaration : statement\n if (statement.type === 'ImportDeclaration' && statement.importKind !== 'type') {\n for (const specifier of statement.specifiers ?? []) {\n const imported =\n specifier.type === 'ImportDefaultSpecifier' ? 'default' : specifier.type === 'ImportNamespaceSpecifier' ? '*' : (specifier.imported?.name ?? specifier.imported?.value)\n this.imports.set(specifier.local.name, { source: statement.source.value, imported })\n }\n return\n }\n if (declaration?.type === 'FunctionDeclaration' && declaration.id) {\n this.declare(declaration.id.name, declaration, statement)\n if (statement.type === 'ExportNamedDeclaration') this.exports.set(declaration.id.name, declaration.id.name)\n }\n if (declaration?.type === 'VariableDeclaration') {\n for (const declarator of declaration.declarations ?? []) {\n if (declarator.id?.type !== 'Identifier' || !declarator.init) continue\n this.declare(declarator.id.name, declarator.init, statement)\n if (statement.type === 'ExportNamedDeclaration') this.exports.set(declarator.id.name, declarator.id.name)\n }\n }\n if (statement.type === 'ExportNamedDeclaration' && !statement.declaration) {\n for (const specifier of statement.specifiers ?? []) {\n const exported = specifier.exported?.name ?? specifier.exported?.value\n const local = specifier.local?.name ?? specifier.local?.value\n this.exports.set(exported, statement.source ? { source: statement.source.value, imported: local } : local)\n }\n }\n if (statement.type === 'ExportDefaultDeclaration') {\n const value = statement.declaration\n if (value?.type === 'Identifier') this.exports.set('default', value.name)\n else if (value) {\n this.declare('default', value, statement)\n this.exports.set('default', 'default')\n }\n }\n }\n}\n\ninterface Context {\n options: ResolvedOptions\n resolver: ImportResolver | undefined\n scopes: Map<string, ModuleScope>\n}\n\nfunction scopeOf(module: ModuleSource, context: Context): ModuleScope {\n let scope = context.scopes.get(module.filename)\n if (!scope) {\n scope = new ModuleScope(module.ast, module.filename)\n context.scopes.set(module.filename, scope)\n }\n return scope\n}\n\n/** The function a name refers to in `scope`: a local function, or an imported one (followed across files). */\nfunction resolveFunction(name: string, scope: ModuleScope, context: Context, depth: number): { fn: Node; scope: ModuleScope } | null {\n const local = scope.functions.get(name)\n if (local) return { fn: local, scope }\n const binding = scope.imports.get(name)\n if (!binding || !context.resolver || depth > MAX_DEPTH) return null\n return exportedFunction(binding, scope, context, depth + 1)\n}\n\nfunction exportedFunction(binding: Reexport, from: ModuleScope, context: Context, depth: number): { fn: Node; scope: ModuleScope } | null {\n if (depth > MAX_DEPTH || binding.imported === '*') return null\n const module = context.resolver?.(binding.source, from.filename)\n if (!module) return null\n const scope = scopeOf(module, context)\n const entry = scope.exports.get(binding.imported)\n if (entry === undefined) return null\n if (typeof entry === 'string') {\n const fn = scope.functions.get(entry)\n return fn ? { fn, scope } : resolveFunction(entry, scope, context, depth)\n }\n return exportedFunction(entry, scope, context, depth + 1)\n}\n\n/**\n * First auth check that always runs when `fn` runs, directly or through helpers it calls:\n * \"auth()\", \"getUser() → auth()\". A check inside a branch or a callback does not count.\n */\nfunction authEvidence(fn: Node, scope: ModuleScope, context: Context, depth = 0, seen = new Set<Node>(), followReturned = false): string | null {\n if (depth > MAX_DEPTH || seen.has(fn)) return null\n seen.add(fn)\n const calls = unconditionalCalls(fn, followReturned)\n for (const call of calls) {\n const path = calleePath(call.callee)\n if (isAuthCall(path, context.options)) return `${path}()`\n }\n for (const call of calls) {\n const path = calleePath(call.callee)\n if (path.includes('.') || path === '*' || GUEST_GUARD.test(path) || !HELPER_NAME.test(path)) continue\n const helper = resolveFunction(path, scope, context, depth)\n if (!helper) continue\n const evidence = authEvidence(helper.fn, helper.scope, context, depth + 1, seen)\n if (evidence) return `${path}() → ${evidence}`\n }\n return null\n}\n\nfunction boundNames(pattern: Node | null | undefined, names: string[] = []): string[] {\n if (!pattern) return names\n if (pattern.type === 'Identifier') names.push(pattern.name)\n else if (pattern.type === 'ObjectPattern') for (const property of pattern.properties ?? []) boundNames(property.type === 'RestElement' ? property.argument : property.value, names)\n else if (pattern.type === 'ArrayPattern') for (const element of pattern.elements ?? []) boundNames(element, names)\n else if (pattern.type === 'AssignmentPattern') boundNames(pattern.left, names)\n else if (pattern.type === 'RestElement') boundNames(pattern.argument, names)\n return names\n}\n\n/**\n * An inline action that uses a value the enclosing component got from an auth check:\n * `const session = await auth()` in the page, then `if (!session) return` in the action.\n * Next.js encrypts the variables an inline action closes over, so the action sees the\n * session of the render that created it.\n */\nfunction closureEvidence(action: Node, enclosing: Node, scope: ModuleScope, context: Context): string | null {\n if (enclosing.body?.type !== 'BlockStatement') return null\n const fromAuth = new Map<string, string>()\n for (const statement of enclosing.body.body ?? []) {\n if (statement.type !== 'VariableDeclaration') continue\n for (const declarator of statement.declarations ?? []) {\n const evidence = authEvidence({ type: 'ArrowFunctionExpression', params: [], body: declarator.init } as Node, scope, context)\n if (evidence) for (const name of boundNames(declarator.id)) fromAuth.set(name, evidence)\n }\n }\n if (!fromAuth.size) return null\n let found: string | null = null\n walk(action.body, (node) => {\n if (found) return false\n if (node.type === 'Identifier' && fromAuth.has(node.name)) found = `${node.name} from ${fromAuth.get(node.name)} in the enclosing component`\n })\n return found\n}\n\nfunction declarationFrom(binding: Reexport, from: ModuleScope, context: Context, depth: number): { init: Node; scope: ModuleScope } | null {\n if (depth > MAX_DEPTH || binding.imported === '*' || !context.resolver) return null\n const module = context.resolver(binding.source, from.filename)\n if (!module) return null\n const scope = scopeOf(module, context)\n const entry = scope.exports.get(binding.imported)\n if (typeof entry === 'string') return resolveDeclaration(entry, scope, context, depth + 1)\n return entry ? declarationFrom(entry, scope, context, depth + 1) : null\n}\n\n/** What a top-level name is initialized with, following imports: `actionClient.use(…)`, `cache(…)`. */\nfunction resolveDeclaration(name: string, scope: ModuleScope, context: Context, depth: number): { init: Node; scope: ModuleScope } | null {\n const local = scope.declarations.get(name)\n if (local) return { init: local.init, scope }\n const binding = scope.imports.get(name)\n return binding ? declarationFrom(binding, scope, context, depth) : null\n}\n\n/**\n * Auth check in the definition of a wrapper, a client or a middleware, following clients derived\n * from other clients: `apiKeyClient = actionClient.use(…)`, where `actionClient` checks the session.\n */\nfunction definitionEvidence(name: string, scope: ModuleScope, context: Context, depth = 0): string | null {\n if (depth > MAX_DEPTH || GUEST_GUARD.test(name)) return null\n if (depth > 0 && (context.options.authClients.has(name) || context.options.authWrappers.has(name) || AUTH_NAME.test(name))) return name\n const definition = resolveFunction(name, scope, context, 0)\n const evidence = definition && authEvidence(definition.fn, definition.scope, context, 1, new Set(), true)\n if (evidence) return `${name} → ${evidence}`\n const declaration = resolveDeclaration(name, scope, context, 0)\n if (declaration?.init.type === 'CallExpression') {\n const chain = callChain(declaration.init)\n for (const base of [chain.root, ...chain.identifiers]) {\n if (!base || base === name) continue\n const inherited = definitionEvidence(base, declaration.scope, context, depth + 1)\n if (inherited) return `${name} → ${inherited}`\n }\n }\n return null\n}\n\nfunction validatesDirectly(fn: Node): boolean {\n return callsIn(fn.body).some((call) => isValidationCall(calleePath(call.callee)))\n}\n\nfunction parameterCount(fn: Node): number {\n return (fn.params ?? []).length\n}\n\n/** Comments right before `node` (only whitespace or other comments in between). */\nfunction leadingComments(node: Node, comments: Comment[], text: string): Comment[] {\n const start = node.range?.[0]\n if (start === undefined) return []\n const found: Comment[] = []\n let cursor = start\n for (const comment of [...comments].reverse()) {\n const [from, to] = comment.range ?? [Infinity, Infinity]\n if (to > cursor) continue\n if (text.slice(to, cursor).trim() !== '') break\n found.push(comment)\n cursor = from\n }\n return found\n}\n\n/** Comments at the top of a function body, before its first real statement. */\nfunction bodyHeadComments(fn: Node, comments: Comment[]): Comment[] {\n if (fn.body?.type !== 'BlockStatement' || !fn.body.range) return []\n const [bodyStart, bodyEnd] = fn.body.range as [number, number]\n const statements: Node[] = fn.body.body ?? []\n const firstReal = statements.find((statement) => !(statement.type === 'ExpressionStatement' && typeof statement.directive === 'string'))\n const limit = firstReal?.range?.[0] ?? bodyEnd\n return comments.filter((comment) => comment.range && comment.range[0] > bodyStart && comment.range[1] <= limit)\n}\n\ninterface Candidate {\n name: string\n kind: 'module' | 'inline'\n /** The function itself, or the wrapper call (`withAuth(fn)`, `client.action(fn)`). */\n target: Node\n /** Nodes whose leading comments may mark the action public. */\n anchors: Node[]\n /** For an inline action: the component or function it is declared in. */\n enclosing?: Node | null\n}\n\nfunction moduleCandidates(scope: ModuleScope): Candidate[] {\n const candidates: Candidate[] = []\n for (const [exported, entry] of scope.exports) {\n if (typeof entry !== 'string') continue\n const declaration = scope.declarations.get(entry)\n if (!declaration) continue\n const { init, statement } = declaration\n const target = init.type === 'AwaitExpression' ? init.argument : init\n if (!isFunction(target) && target.type !== 'CallExpression') continue\n // `export default async function createPost()` reads better as \"createPost\" than \"default\".\n const name = exported === 'default' && target.id?.type === 'Identifier' ? target.id.name : exported\n candidates.push({ name, kind: 'module', target, anchors: [statement, init] })\n }\n return candidates\n}\n\n/** Functions with their own \"use server\" directive, named from where they appear. */\nfunction inlineCandidates(ast: Node, skip: Set<Node>): Candidate[] {\n const candidates: Candidate[] = []\n const visit = (node: Node, parent: Node | null, grandparent: Node | null, enclosing: Node | null): void => {\n if (isFunction(node) && !skip.has(node) && functionDirectives(node).includes('use server')) {\n let name = node.id?.name as string | undefined\n if (!name && parent?.type === 'VariableDeclarator' && parent.id?.type === 'Identifier') name = parent.id.name\n if (!name && (parent?.type === 'Property' || parent?.type === 'MethodDefinition') && parent.key?.type === 'Identifier') name = parent.key.name\n if (!name && parent?.type === 'JSXExpressionContainer' && grandparent?.type === 'JSXAttribute') name = `(inline ${grandparent.name?.name ?? 'action'})`\n candidates.push({ name: name ?? '(inline)', kind: 'inline', target: node, anchors: [parent?.type === 'VariableDeclarator' ? grandparent ?? node : node], enclosing })\n }\n const inner = isFunction(node) ? node : enclosing\n for (const key of Object.keys(node)) {\n if (key === 'parent' || key === 'loc' || key === 'range') continue\n const value = node[key]\n if (Array.isArray(value)) {\n for (const child of value) if (child && typeof child.type === 'string') visit(child, node, parent, inner)\n } else if (value && typeof value.type === 'string') {\n visit(value, node, parent, inner)\n }\n }\n }\n visit(ast, null, null, null)\n return candidates\n}\n\nfunction analyzeCandidate(candidate: Candidate, scope: ModuleScope, context: Context, comments: Comment[], text: string): ServerAction {\n const { target } = candidate\n let reason: string | null = null\n let validated: boolean | null = null\n let hint: string | null = null\n const handlers: Node[] = []\n\n if (isFunction(target)) {\n handlers.push(target)\n reason = authEvidence(target, scope, context)\n if (!reason && candidate.enclosing) reason = closureEvidence(target, candidate.enclosing, scope, context)\n validated = parameterCount(target) === 0 ? null : validatesDirectly(target)\n } else {\n const chain = callChain(target)\n handlers.push(...chain.functions)\n const isClient = chain.methods.some((method) => CLIENT_METHODS.has(method))\n const root = chain.root\n const trusted = root !== null && (context.options.authClients.has(root) || context.options.authWrappers.has(root) || (AUTH_NAME.test(root) && !GUEST_GUARD.test(root)))\n if (trusted) reason = isClient ? `${root} client` : `${root}() wrapper`\n for (const handler of chain.functions) reason ??= authEvidence(handler, scope, context)\n // Read the definition of the wrapper, the client and its `.use(middleware)`:\n // `withTeam` → getUser() → verifyToken(), `myClient = base.use(async () => getSession())`.\n for (const name of [root, ...chain.identifiers]) {\n if (!reason && name) reason = definitionEvidence(name, scope, context)\n }\n if (isClient) {\n validated = chain.methods.some((method) => SCHEMA_METHODS.has(method)) ? true : null\n } else if (root && /valid|schema|zod|parse/i.test(root)) {\n validated = true\n } else {\n const handler = chain.functions[0]\n validated = !handler || parameterCount(handler) === 0 ? null : validatesDirectly(handler)\n }\n if (!reason && root) {\n hint = isClient\n ? `If \"${root}\" has auth middleware, add it to \"authClients\".`\n : `If ${root}() checks the session, add it to \"authWrappers\".`\n }\n }\n\n const markerComments = [\n ...candidate.anchors.flatMap((anchor) => leadingComments(anchor, comments, text)),\n ...handlers.flatMap((handler) => bodyHeadComments(handler, comments)),\n ]\n const markedPublic = markerComments.some((comment) => PUBLIC_MARKER.test(comment.value))\n const listedPublic = context.options.publicActions.has(candidate.name)\n const status: ActionStatus = reason ? 'protected' : markedPublic || listedPublic ? 'public' : 'unprotected'\n if (status === 'public') {\n reason = markedPublic ? '@public-action' : 'publicActions'\n hint = null\n }\n const { line, column } = position(candidate.anchors[0] ?? target)\n return { name: candidate.name, kind: candidate.kind, line, column, status, reason, validated, hint, node: candidate.anchors[0] ?? target }\n}\n\n/** Every Server Action declared in one module, with its protection status. */\nexport function analyzeModule(input: AnalyzeInput, options: ResolvedOptions, resolver?: ImportResolver): ServerAction[] {\n if (!input.text.includes('use server')) return []\n const context: Context = { options, resolver, scopes: new Map() }\n const scope = scopeOf({ ast: input.ast, filename: input.filename }, context)\n const comments = input.comments ?? (input.ast.comments as Comment[] | undefined) ?? []\n const candidates: Candidate[] = []\n if (directives(input.ast.body).includes('use server')) candidates.push(...moduleCandidates(scope))\n const covered = new Set(candidates.map((candidate) => candidate.target))\n candidates.push(...inlineCandidates(input.ast, covered))\n return candidates.map((candidate) => analyzeCandidate(candidate, scope, context, comments, input.text)).sort((a, b) => a.line - b.line || a.column - b.column)\n}\n\n","import { existsSync, readFileSync, statSync } from 'node:fs'\nimport { dirname, join, resolve } from 'node:path'\nimport { parse } from '@typescript-eslint/typescript-estree'\nimport type { Node } from './ast.js'\nimport type { ImportResolver, ModuleSource } from './analyze.js'\n\nconst EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.mts', '.cts']\n\nexport function parseModule(text: string, filename: string): Node {\n return parse(text, {\n jsx: !/\\.[cm]?ts$/.test(filename),\n loc: true,\n range: true,\n comment: true,\n }) as unknown as Node\n}\n\nfunction isFile(path: string): boolean {\n try {\n return statSync(path).isFile()\n } catch {\n return false\n }\n}\n\n/** In TypeScript ESM, `import './session.js'` points at session.ts. */\nconst TS_SOURCES: Record<string, string[]> = { '.js': ['.ts', '.tsx'], '.jsx': ['.tsx'], '.mjs': ['.mts'], '.cjs': ['.cts'] }\n\nfunction tryFile(base: string): string | null {\n if (isFile(base)) return base\n const jsExtension = base.match(/\\.(js|jsx|mjs|cjs)$/)?.[0]\n if (jsExtension) {\n for (const extension of TS_SOURCES[jsExtension] ?? []) {\n const candidate = base.slice(0, -jsExtension.length) + extension\n if (isFile(candidate)) return candidate\n }\n }\n for (const extension of EXTENSIONS) if (isFile(base + extension)) return base + extension\n for (const extension of EXTENSIONS) if (isFile(join(base, `index${extension}`))) return join(base, `index${extension}`)\n return null\n}\n\n/** JSON with comments and trailing commas, as tsconfig.json allows. */\nfunction parseJsonc(text: string): unknown {\n let out = ''\n let inString = false\n for (let i = 0; i < text.length; i++) {\n const char = text[i]!\n if (inString) {\n out += char\n if (char === '\\\\') out += text[++i] ?? ''\n else if (char === '\"') inString = false\n } else if (char === '\"') {\n inString = true\n out += char\n } else if (char === '/' && text[i + 1] === '/') {\n while (i < text.length && text[i] !== '\\n') i++\n out += '\\n'\n } else if (char === '/' && text[i + 1] === '*') {\n i = text.indexOf('*/', i + 2)\n if (i === -1) break\n i++\n } else {\n out += char\n }\n }\n return JSON.parse(out.replace(/,(\\s*[}\\]])/g, '$1'))\n}\n\ninterface PathMapping {\n base: string\n paths: Record<string, string[]>\n}\n\nconst mappings = new Map<string, PathMapping | null>()\n\n/** `compilerOptions.paths` of the nearest tsconfig.json or jsconfig.json. */\nfunction pathMapping(fromDir: string): PathMapping | null {\n if (mappings.has(fromDir)) return mappings.get(fromDir)!\n let mapping: PathMapping | null = null\n for (const name of ['tsconfig.json', 'jsconfig.json']) {\n const file = join(fromDir, name)\n if (!existsSync(file)) continue\n try {\n const config = parseJsonc(readFileSync(file, 'utf8')) as { compilerOptions?: { baseUrl?: string; paths?: Record<string, string[]> } }\n const options = config.compilerOptions ?? {}\n if (options.paths) mapping = { base: resolve(fromDir, options.baseUrl ?? '.'), paths: options.paths }\n } catch {\n mapping = null\n }\n break\n }\n if (!mapping) {\n const parent = dirname(fromDir)\n mapping = parent !== fromDir && !existsSync(join(fromDir, 'package.json')) ? pathMapping(parent) : null\n }\n mappings.set(fromDir, mapping)\n return mapping\n}\n\nfunction resolveAlias(specifier: string, fromFile: string): string | null {\n const mapping = pathMapping(dirname(fromFile))\n if (!mapping) return null\n for (const [pattern, targets] of Object.entries(mapping.paths)) {\n const star = pattern.indexOf('*')\n let captured: string | null = null\n if (star === -1) captured = specifier === pattern ? '' : null\n else {\n const prefix = pattern.slice(0, star)\n const suffix = pattern.slice(star + 1)\n if (specifier.startsWith(prefix) && specifier.endsWith(suffix) && specifier.length >= prefix.length + suffix.length) {\n captured = specifier.slice(prefix.length, specifier.length - suffix.length)\n }\n }\n if (captured === null) continue\n for (const target of targets) {\n const file = tryFile(resolve(mapping.base, target.replace('*', captured)))\n if (file) return file\n }\n }\n return null\n}\n\nexport function resolveImportPath(specifier: string, fromFile: string): string | null {\n if (specifier.startsWith('.')) return tryFile(resolve(dirname(fromFile), specifier))\n if (specifier.startsWith('/')) return tryFile(specifier)\n return resolveAlias(specifier, fromFile)\n}\n\n/**\n * Resolves relative imports and tsconfig `paths` aliases (`@/lib/session`), then parses the file.\n * Packages from node_modules are not followed. Parsed files are cached until they change on disk.\n */\nexport function createNodeResolver(): ImportResolver {\n const cache = new Map<string, { mtime: number; module: ModuleSource | null }>()\n return (specifier, fromFile) => {\n const file = resolveImportPath(specifier, fromFile)\n if (!file) return null\n let mtime = 0\n try {\n mtime = statSync(file).mtimeMs\n } catch {\n return null\n }\n const cached = cache.get(file)\n if (cached && cached.mtime === mtime) return cached.module\n let module: ModuleSource | null = null\n try {\n module = { ast: parseModule(readFileSync(file, 'utf8'), file), filename: file }\n } catch {\n module = null\n }\n cache.set(file, { mtime, module })\n return module\n }\n}\n","/** Kept in sync with package.json by test/version.test.ts. */\nexport const VERSION = '0.1.0'\n"],"mappings":"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","names":["module","resolver","module"]}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Rule, Linter } from 'eslint';
|
|
2
|
+
|
|
3
|
+
declare const plugin: {
|
|
4
|
+
meta: {
|
|
5
|
+
name: string;
|
|
6
|
+
version: string;
|
|
7
|
+
};
|
|
8
|
+
rules: {
|
|
9
|
+
'require-auth': Rule.RuleModule;
|
|
10
|
+
'require-input-validation': Rule.RuleModule;
|
|
11
|
+
};
|
|
12
|
+
configs: Record<string, Linter.Config>;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export { plugin as default };
|
package/dist/eslint.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Rule, Linter } from 'eslint';
|
|
2
|
+
|
|
3
|
+
declare const plugin: {
|
|
4
|
+
meta: {
|
|
5
|
+
name: string;
|
|
6
|
+
version: string;
|
|
7
|
+
};
|
|
8
|
+
rules: {
|
|
9
|
+
'require-auth': Rule.RuleModule;
|
|
10
|
+
'require-input-validation': Rule.RuleModule;
|
|
11
|
+
};
|
|
12
|
+
configs: Record<string, Linter.Config>;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export { plugin as default };
|
package/dist/eslint.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import {
|
|
2
|
+
VERSION,
|
|
3
|
+
analyzeModule,
|
|
4
|
+
createNodeResolver,
|
|
5
|
+
resolveOptions
|
|
6
|
+
} from "./chunk-GQSNZNKN.js";
|
|
7
|
+
|
|
8
|
+
// src/eslint.ts
|
|
9
|
+
var resolver = createNodeResolver();
|
|
10
|
+
var optionsSchema = {
|
|
11
|
+
type: "object",
|
|
12
|
+
properties: {
|
|
13
|
+
authFunctions: { type: "array", items: { type: "string" } },
|
|
14
|
+
authWrappers: { type: "array", items: { type: "string" } },
|
|
15
|
+
authClients: { type: "array", items: { type: "string" } },
|
|
16
|
+
publicActions: { type: "array", items: { type: "string" } }
|
|
17
|
+
},
|
|
18
|
+
additionalProperties: false
|
|
19
|
+
};
|
|
20
|
+
function actionsOf(context) {
|
|
21
|
+
const text = context.sourceCode.text;
|
|
22
|
+
if (!text.includes("use server")) return [];
|
|
23
|
+
const shared = context.settings?.serverActionAudit ?? {};
|
|
24
|
+
const own = context.options[0] ?? {};
|
|
25
|
+
const options = resolveOptions({
|
|
26
|
+
authFunctions: [...shared.authFunctions ?? [], ...own.authFunctions ?? []],
|
|
27
|
+
authWrappers: [...shared.authWrappers ?? [], ...own.authWrappers ?? []],
|
|
28
|
+
authClients: [...shared.authClients ?? [], ...own.authClients ?? []],
|
|
29
|
+
publicActions: [...shared.publicActions ?? [], ...own.publicActions ?? []]
|
|
30
|
+
});
|
|
31
|
+
return analyzeModule(
|
|
32
|
+
{
|
|
33
|
+
ast: context.sourceCode.ast,
|
|
34
|
+
text,
|
|
35
|
+
filename: context.physicalFilename ?? context.filename,
|
|
36
|
+
comments: context.sourceCode.getAllComments()
|
|
37
|
+
},
|
|
38
|
+
options,
|
|
39
|
+
resolver
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
var requireAuth = {
|
|
43
|
+
meta: {
|
|
44
|
+
type: "problem",
|
|
45
|
+
docs: {
|
|
46
|
+
description: "Require an authorization check in every Next.js Server Action",
|
|
47
|
+
url: "https://github.com/i6moons/server-action-audit#require-auth"
|
|
48
|
+
},
|
|
49
|
+
schema: [optionsSchema],
|
|
50
|
+
messages: {
|
|
51
|
+
missingAuth: 'Server Action "{{name}}" has no authorization check: anyone can call it with a POST request. Check the session inside the action, or mark it with // @public-action if it is public on purpose.{{hint}}'
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
create(context) {
|
|
55
|
+
return {
|
|
56
|
+
"Program:exit"() {
|
|
57
|
+
for (const action of actionsOf(context)) {
|
|
58
|
+
if (action.status !== "unprotected") continue;
|
|
59
|
+
context.report({
|
|
60
|
+
node: action.node,
|
|
61
|
+
messageId: "missingAuth",
|
|
62
|
+
data: { name: action.name, hint: action.hint ? ` ${action.hint}` : "" }
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
var requireInputValidation = {
|
|
70
|
+
meta: {
|
|
71
|
+
type: "suggestion",
|
|
72
|
+
docs: {
|
|
73
|
+
description: "Require Server Actions to validate their arguments (Zod, Valibot, a next-safe-action schema\u2026)",
|
|
74
|
+
url: "https://github.com/i6moons/server-action-audit#require-input-validation"
|
|
75
|
+
},
|
|
76
|
+
schema: [optionsSchema],
|
|
77
|
+
messages: {
|
|
78
|
+
missingValidation: 'Server Action "{{name}}" uses its arguments without validating them. They come straight from the client: parse them with a schema (Zod, Valibot\u2026).'
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
create(context) {
|
|
82
|
+
return {
|
|
83
|
+
"Program:exit"() {
|
|
84
|
+
for (const action of actionsOf(context)) {
|
|
85
|
+
if (action.validated !== false) continue;
|
|
86
|
+
context.report({ node: action.node, messageId: "missingValidation", data: { name: action.name } });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
var plugin = {
|
|
93
|
+
meta: { name: "server-action-audit", version: VERSION },
|
|
94
|
+
rules: {
|
|
95
|
+
"require-auth": requireAuth,
|
|
96
|
+
"require-input-validation": requireInputValidation
|
|
97
|
+
},
|
|
98
|
+
configs: {}
|
|
99
|
+
};
|
|
100
|
+
plugin.configs.recommended = {
|
|
101
|
+
name: "server-action-audit/recommended",
|
|
102
|
+
plugins: { "server-action-audit": plugin },
|
|
103
|
+
rules: {
|
|
104
|
+
"server-action-audit/require-auth": "error",
|
|
105
|
+
"server-action-audit/require-input-validation": "warn"
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
var eslint_default = plugin;
|
|
109
|
+
export {
|
|
110
|
+
eslint_default as default
|
|
111
|
+
};
|
|
112
|
+
//# sourceMappingURL=eslint.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/eslint.ts"],"sourcesContent":["import type { ESLint, Linter, Rule } from 'eslint'\nimport { analyzeModule, type ServerAction } from './analyze.js'\nimport type { Comment, Node } from './ast.js'\nimport { resolveOptions, type AuditOptions } from './options.js'\nimport { createNodeResolver } from './resolve.js'\nimport { VERSION } from './version.js'\n\nconst resolver = createNodeResolver()\n\nconst optionsSchema = {\n type: 'object',\n properties: {\n authFunctions: { type: 'array', items: { type: 'string' } },\n authWrappers: { type: 'array', items: { type: 'string' } },\n authClients: { type: 'array', items: { type: 'string' } },\n publicActions: { type: 'array', items: { type: 'string' } },\n },\n additionalProperties: false,\n} as const\n\n/** Options from the rule, merged over `settings.serverActionAudit` (shared by both rules). */\nfunction actionsOf(context: Rule.RuleContext): ServerAction[] {\n const text = context.sourceCode.text\n if (!text.includes('use server')) return []\n const shared = (context.settings?.serverActionAudit ?? {}) as AuditOptions\n const own = (context.options[0] ?? {}) as AuditOptions\n const options = resolveOptions({\n authFunctions: [...(shared.authFunctions ?? []), ...(own.authFunctions ?? [])],\n authWrappers: [...(shared.authWrappers ?? []), ...(own.authWrappers ?? [])],\n authClients: [...(shared.authClients ?? []), ...(own.authClients ?? [])],\n publicActions: [...(shared.publicActions ?? []), ...(own.publicActions ?? [])],\n })\n return analyzeModule(\n {\n ast: context.sourceCode.ast as unknown as Node,\n text,\n filename: context.physicalFilename ?? context.filename,\n comments: context.sourceCode.getAllComments() as unknown as Comment[],\n },\n options,\n resolver,\n )\n}\n\nconst requireAuth: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Require an authorization check in every Next.js Server Action',\n url: 'https://github.com/i6moons/server-action-audit#require-auth',\n },\n schema: [optionsSchema],\n messages: {\n missingAuth:\n 'Server Action \"{{name}}\" has no authorization check: anyone can call it with a POST request. Check the session inside the action, or mark it with // @public-action if it is public on purpose.{{hint}}',\n },\n },\n create(context) {\n return {\n 'Program:exit'() {\n for (const action of actionsOf(context)) {\n if (action.status !== 'unprotected') continue\n context.report({\n node: action.node as unknown as Rule.Node,\n messageId: 'missingAuth',\n data: { name: action.name, hint: action.hint ? ` ${action.hint}` : '' },\n })\n }\n },\n }\n },\n}\n\nconst requireInputValidation: Rule.RuleModule = {\n meta: {\n type: 'suggestion',\n docs: {\n description: 'Require Server Actions to validate their arguments (Zod, Valibot, a next-safe-action schema…)',\n url: 'https://github.com/i6moons/server-action-audit#require-input-validation',\n },\n schema: [optionsSchema],\n messages: {\n missingValidation:\n 'Server Action \"{{name}}\" uses its arguments without validating them. They come straight from the client: parse them with a schema (Zod, Valibot…).',\n },\n },\n create(context) {\n return {\n 'Program:exit'() {\n for (const action of actionsOf(context)) {\n if (action.validated !== false) continue\n context.report({ node: action.node as unknown as Rule.Node, messageId: 'missingValidation', data: { name: action.name } })\n }\n },\n }\n },\n}\n\nconst plugin = {\n meta: { name: 'server-action-audit', version: VERSION },\n rules: {\n 'require-auth': requireAuth,\n 'require-input-validation': requireInputValidation,\n },\n configs: {} as Record<string, Linter.Config>,\n} satisfies ESLint.Plugin & { configs: Record<string, Linter.Config> }\n\nplugin.configs.recommended = {\n name: 'server-action-audit/recommended',\n plugins: { 'server-action-audit': plugin },\n rules: {\n 'server-action-audit/require-auth': 'error',\n 'server-action-audit/require-input-validation': 'warn',\n },\n}\n\nexport default plugin\n"],"mappings":"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","names":[]}
|