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/dist/index.js ADDED
@@ -0,0 +1,234 @@
1
+ import {
2
+ VERSION,
3
+ analyzeModule,
4
+ createNodeResolver,
5
+ parseModule,
6
+ resolveImportPath,
7
+ resolveOptions
8
+ } from "./chunk-GQSNZNKN.js";
9
+
10
+ // src/scan.ts
11
+ import { existsSync, readFileSync, readdirSync } from "fs";
12
+ import { join, relative, sep } from "path";
13
+ var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".next", ".git", "dist", "build", "out", "coverage", ".turbo", ".vercel", ".output", ".svelte-kit", ".cache"]);
14
+ var SOURCE = /\.(?:[cm]?[jt]s|[jt]sx)$/;
15
+ function globToRegExp(glob) {
16
+ let pattern = "";
17
+ for (let i = 0; i < glob.length; i++) {
18
+ const char = glob[i];
19
+ if (char === "*" && glob[i + 1] === "*") {
20
+ pattern += glob[i + 2] === "/" ? "(?:.*/)?" : ".*";
21
+ i += glob[i + 2] === "/" ? 2 : 1;
22
+ } else if (char === "*") pattern += "[^/]*";
23
+ else if (char === "?") pattern += "[^/]";
24
+ else pattern += char.replace(/[.+^${}()|[\]\\]/g, "\\$&");
25
+ }
26
+ return new RegExp(`^${pattern}$`);
27
+ }
28
+ function* sourceFiles(dir) {
29
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
30
+ if (entry.isDirectory()) {
31
+ if (!SKIP_DIRS.has(entry.name) && !entry.name.startsWith(".")) yield* sourceFiles(join(dir, entry.name));
32
+ } else if (entry.isFile() && SOURCE.test(entry.name) && !entry.name.endsWith(".d.ts")) {
33
+ yield join(dir, entry.name);
34
+ }
35
+ }
36
+ }
37
+ function loadConfig(root, configPath) {
38
+ if (configPath) return JSON.parse(readFileSync(configPath, "utf8"));
39
+ const file = join(root, "server-action-audit.config.json");
40
+ if (existsSync(file)) return JSON.parse(readFileSync(file, "utf8"));
41
+ const pkg = join(root, "package.json");
42
+ if (existsSync(pkg)) {
43
+ const config = JSON.parse(readFileSync(pkg, "utf8")).serverActionAudit;
44
+ if (config) return config;
45
+ }
46
+ return {};
47
+ }
48
+ function scanProject(root, options = {}) {
49
+ const resolved = resolveOptions(options);
50
+ const ignore = resolved.ignore.map(globToRegExp);
51
+ const resolver = createNodeResolver();
52
+ const report = { root, files: [], errors: [], summary: { actions: 0, protected: 0, unprotected: 0, public: 0, unvalidated: 0 } };
53
+ for (const path of sourceFiles(root)) {
54
+ const file = relative(root, path).split(sep).join("/");
55
+ if (ignore.some((pattern) => pattern.test(file))) continue;
56
+ const text = readFileSync(path, "utf8");
57
+ if (!text.includes("use server")) continue;
58
+ let actions;
59
+ try {
60
+ actions = analyzeModule({ ast: parseModule(text, path), text, filename: path }, resolved, resolver);
61
+ } catch (error) {
62
+ report.errors.push({ file, message: error.message });
63
+ continue;
64
+ }
65
+ if (!actions.length) continue;
66
+ report.files.push({ file, actions });
67
+ for (const action of actions) {
68
+ report.summary.actions++;
69
+ report.summary[action.status]++;
70
+ if (action.validated === false) report.summary.unvalidated++;
71
+ }
72
+ }
73
+ report.files.sort((a, b) => a.file.localeCompare(b.file));
74
+ return report;
75
+ }
76
+
77
+ // src/format.ts
78
+ var useColor = process.env.FORCE_COLOR ? true : !process.env.NO_COLOR && !!process.stdout.isTTY;
79
+ var paint = (code) => (text) => useColor ? `\x1B[${code}m${text}\x1B[0m` : text;
80
+ var red = paint("31");
81
+ var green = paint("32");
82
+ var yellow = paint("33");
83
+ var cyan = paint("36");
84
+ var dim = paint("2");
85
+ var bold = paint("1");
86
+ function describe(action) {
87
+ const parts = [];
88
+ if (action.status === "protected") parts.push(dim(action.reason ?? ""));
89
+ if (action.status === "public") parts.push(cyan(`public (${action.reason})`));
90
+ if (action.status === "unprotected") parts.push(red("no auth check"));
91
+ if (action.validated === false) parts.push(yellow("input not validated"));
92
+ return parts.join(dim(" \xB7 "));
93
+ }
94
+ function symbol(action) {
95
+ if (action.status === "unprotected") return red("\u2717");
96
+ if (action.validated === false) return yellow("!");
97
+ if (action.status === "public") return cyan("\u25CB");
98
+ return green("\u2713");
99
+ }
100
+ function formatPretty(report, options = {}) {
101
+ const { summary } = report;
102
+ const lines = [];
103
+ const fileCount = report.files.length;
104
+ lines.push(bold(`server-action-audit \xB7 ${summary.actions} Server Action${summary.actions === 1 ? "" : "s"} in ${fileCount} file${fileCount === 1 ? "" : "s"}`), "");
105
+ for (const { file, actions } of report.files) {
106
+ const shown = options.quiet ? actions.filter((a) => a.status === "unprotected" || a.validated === false) : actions;
107
+ if (!shown.length) continue;
108
+ lines.push(bold(file));
109
+ const width = Math.min(32, Math.max(...shown.map((a) => a.name.length)));
110
+ for (const action of shown) {
111
+ const where = dim(`${action.line}:${action.column}`.padEnd(7));
112
+ lines.push(` ${symbol(action)} ${action.name.padEnd(width)} ${where} ${describe(action)}`);
113
+ if (action.status === "unprotected" && action.hint) lines.push(` ${dim(action.hint)}`);
114
+ }
115
+ lines.push("");
116
+ }
117
+ for (const error of report.errors) lines.push(yellow(`skipped ${error.file}: ${error.message}`));
118
+ if (report.errors.length) lines.push("");
119
+ if (!summary.actions) {
120
+ lines.push("No Server Actions found.");
121
+ return lines.join("\n");
122
+ }
123
+ lines.push(
124
+ [
125
+ summary.unprotected ? red(`\u2717 ${summary.unprotected} unprotected`) : green("\u2717 0 unprotected"),
126
+ green(`\u2713 ${summary.protected} protected`),
127
+ cyan(`\u25CB ${summary.public} public`),
128
+ summary.unvalidated ? yellow(`! ${summary.unvalidated} with unvalidated input`) : dim("! 0 with unvalidated input")
129
+ ].join(dim(" \xB7 "))
130
+ );
131
+ if (summary.unprotected) {
132
+ lines.push(
133
+ "",
134
+ dim("Every Server Action is a public endpoint: anyone can call it with a POST request."),
135
+ dim("Check the session inside each action, or mark intentional ones with // @public-action."),
136
+ dim("https://nextjs.org/docs/app/guides/data-security")
137
+ );
138
+ }
139
+ return lines.join("\n");
140
+ }
141
+ function formatJson(report) {
142
+ return JSON.stringify(
143
+ {
144
+ summary: report.summary,
145
+ files: report.files.map(({ file, actions }) => ({
146
+ file,
147
+ actions: actions.map(({ node: _node, ...action }) => action)
148
+ })),
149
+ errors: report.errors
150
+ },
151
+ null,
152
+ 2
153
+ );
154
+ }
155
+ var AUTH_MESSAGE = (name) => `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.`;
156
+ var VALIDATION_MESSAGE = (name) => `Server Action "${name}" uses its arguments without validating them. They come straight from the client: parse them with a schema (Zod, Valibot\u2026).`;
157
+ function findings(report) {
158
+ const found = [];
159
+ for (const { file, actions } of report.files) {
160
+ for (const action of actions) {
161
+ if (action.status === "unprotected") {
162
+ found.push({ rule: "require-auth", level: "error", file, action, message: AUTH_MESSAGE(action.name) + (action.hint ? ` ${action.hint}` : "") });
163
+ }
164
+ if (action.validated === false) {
165
+ found.push({ rule: "require-input-validation", level: "warning", file, action, message: VALIDATION_MESSAGE(action.name) });
166
+ }
167
+ }
168
+ }
169
+ return found;
170
+ }
171
+ function formatGithub(report) {
172
+ const data = (text) => text.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
173
+ const property = (text) => data(text).replace(/:/g, "%3A").replace(/,/g, "%2C");
174
+ const lines = findings(report).map(
175
+ ({ rule, level, file, action, message }) => `::${level} file=${property(file)},line=${action.line},col=${action.column},title=${property(`server-action-audit/${rule}`)}::${data(message)}`
176
+ );
177
+ const { summary } = report;
178
+ lines.push(`server-action-audit: ${summary.actions} Server Actions, ${summary.unprotected} unprotected, ${summary.protected} protected, ${summary.public} public, ${summary.unvalidated} with unvalidated input.`);
179
+ return lines.join("\n");
180
+ }
181
+ function formatSarif(report, version) {
182
+ const rules = [
183
+ {
184
+ id: "require-auth",
185
+ name: "RequireAuth",
186
+ shortDescription: { text: "Server Action without an authorization check" },
187
+ fullDescription: { text: "Every Next.js Server Action is a public endpoint. This one does not check who is calling it." },
188
+ helpUri: "https://github.com/i6moons/server-action-audit#readme",
189
+ defaultConfiguration: { level: "error" },
190
+ properties: { tags: ["security", "nextjs"], "security-severity": "7.5", precision: "medium" }
191
+ },
192
+ {
193
+ id: "require-input-validation",
194
+ name: "RequireInputValidation",
195
+ shortDescription: { text: "Server Action without input validation" },
196
+ fullDescription: { text: "The action uses arguments sent by the client without parsing them with a schema." },
197
+ helpUri: "https://github.com/i6moons/server-action-audit#readme",
198
+ defaultConfiguration: { level: "warning" },
199
+ properties: { tags: ["security", "nextjs"], "security-severity": "4.0", precision: "medium" }
200
+ }
201
+ ];
202
+ const results = findings(report).map(({ rule, level, file, action, message }) => ({
203
+ ruleId: rule,
204
+ ruleIndex: rule === "require-auth" ? 0 : 1,
205
+ level,
206
+ message: { text: message },
207
+ locations: [{ physicalLocation: { artifactLocation: { uri: file }, region: { startLine: action.line, startColumn: action.column } } }],
208
+ partialFingerprints: { actionName: `${file}#${action.name}` }
209
+ }));
210
+ return JSON.stringify(
211
+ {
212
+ $schema: "https://json.schemastore.org/sarif-2.1.0.json",
213
+ version: "2.1.0",
214
+ runs: [{ tool: { driver: { name: "server-action-audit", version, informationUri: "https://github.com/i6moons/server-action-audit", rules } }, results }]
215
+ },
216
+ null,
217
+ 2
218
+ );
219
+ }
220
+ export {
221
+ VERSION,
222
+ analyzeModule,
223
+ createNodeResolver,
224
+ formatGithub,
225
+ formatJson,
226
+ formatPretty,
227
+ formatSarif,
228
+ loadConfig,
229
+ parseModule,
230
+ resolveImportPath,
231
+ resolveOptions,
232
+ scanProject
233
+ };
234
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/scan.ts","../src/format.ts"],"sourcesContent":["import { existsSync, readFileSync, readdirSync } from 'node:fs'\nimport { join, relative, sep } from 'node:path'\nimport { analyzeModule, type ServerAction } from './analyze.js'\nimport { resolveOptions, type AuditOptions } from './options.js'\nimport { createNodeResolver, parseModule } from './resolve.js'\n\nexport interface FileReport {\n /** Relative to the project root, with forward slashes. */\n file: string\n actions: ServerAction[]\n}\n\nexport interface ProjectReport {\n root: string\n files: FileReport[]\n /** Files that mention \"use server\" but could not be parsed. */\n errors: { file: string; message: string }[]\n summary: { actions: number; protected: number; unprotected: number; public: number; unvalidated: number }\n}\n\nconst SKIP_DIRS = new Set(['node_modules', '.next', '.git', 'dist', 'build', 'out', 'coverage', '.turbo', '.vercel', '.output', '.svelte-kit', '.cache'])\nconst SOURCE = /\\.(?:[cm]?[jt]s|[jt]sx)$/\n\nfunction globToRegExp(glob: string): RegExp {\n let pattern = ''\n for (let i = 0; i < glob.length; i++) {\n const char = glob[i]!\n if (char === '*' && glob[i + 1] === '*') {\n pattern += glob[i + 2] === '/' ? '(?:.*/)?' : '.*'\n i += glob[i + 2] === '/' ? 2 : 1\n } else if (char === '*') pattern += '[^/]*'\n else if (char === '?') pattern += '[^/]'\n else pattern += char.replace(/[.+^${}()|[\\]\\\\]/g, '\\\\$&')\n }\n return new RegExp(`^${pattern}$`)\n}\n\nfunction* sourceFiles(dir: string): Generator<string> {\n for (const entry of readdirSync(dir, { withFileTypes: true })) {\n if (entry.isDirectory()) {\n if (!SKIP_DIRS.has(entry.name) && !entry.name.startsWith('.')) yield* sourceFiles(join(dir, entry.name))\n } else if (entry.isFile() && SOURCE.test(entry.name) && !entry.name.endsWith('.d.ts')) {\n yield join(dir, entry.name)\n }\n }\n}\n\n/** Options from `server-action-audit.config.json`, or the `serverActionAudit` key of package.json. */\nexport function loadConfig(root: string, configPath?: string): AuditOptions {\n if (configPath) return JSON.parse(readFileSync(configPath, 'utf8')) as AuditOptions\n const file = join(root, 'server-action-audit.config.json')\n if (existsSync(file)) return JSON.parse(readFileSync(file, 'utf8')) as AuditOptions\n const pkg = join(root, 'package.json')\n if (existsSync(pkg)) {\n const config = (JSON.parse(readFileSync(pkg, 'utf8')) as { serverActionAudit?: AuditOptions }).serverActionAudit\n if (config) return config\n }\n return {}\n}\n\nexport function scanProject(root: string, options: AuditOptions = {}): ProjectReport {\n const resolved = resolveOptions(options)\n const ignore = resolved.ignore.map(globToRegExp)\n const resolver = createNodeResolver()\n const report: ProjectReport = { root, files: [], errors: [], summary: { actions: 0, protected: 0, unprotected: 0, public: 0, unvalidated: 0 } }\n\n for (const path of sourceFiles(root)) {\n const file = relative(root, path).split(sep).join('/')\n if (ignore.some((pattern) => pattern.test(file))) continue\n const text = readFileSync(path, 'utf8')\n if (!text.includes('use server')) continue\n let actions: ServerAction[]\n try {\n actions = analyzeModule({ ast: parseModule(text, path), text, filename: path }, resolved, resolver)\n } catch (error) {\n report.errors.push({ file, message: (error as Error).message })\n continue\n }\n if (!actions.length) continue\n report.files.push({ file, actions })\n for (const action of actions) {\n report.summary.actions++\n report.summary[action.status]++\n if (action.validated === false) report.summary.unvalidated++\n }\n }\n report.files.sort((a, b) => a.file.localeCompare(b.file))\n return report\n}\n","import type { ServerAction } from './analyze.js'\nimport type { ProjectReport } from './scan.js'\n\nconst useColor = process.env.FORCE_COLOR ? true : !process.env.NO_COLOR && !!process.stdout.isTTY\nconst paint = (code: string) => (text: string) => (useColor ? `\\x1b[${code}m${text}\\x1b[0m` : text)\nconst red = paint('31')\nconst green = paint('32')\nconst yellow = paint('33')\nconst cyan = paint('36')\nconst dim = paint('2')\nconst bold = paint('1')\n\nfunction describe(action: ServerAction): string {\n const parts: string[] = []\n if (action.status === 'protected') parts.push(dim(action.reason ?? ''))\n if (action.status === 'public') parts.push(cyan(`public (${action.reason})`))\n if (action.status === 'unprotected') parts.push(red('no auth check'))\n if (action.validated === false) parts.push(yellow('input not validated'))\n return parts.join(dim(' · '))\n}\n\nfunction symbol(action: ServerAction): string {\n if (action.status === 'unprotected') return red('✗')\n if (action.validated === false) return yellow('!')\n if (action.status === 'public') return cyan('○')\n return green('✓')\n}\n\nexport function formatPretty(report: ProjectReport, options: { quiet?: boolean } = {}): string {\n const { summary } = report\n const lines: string[] = []\n const fileCount = report.files.length\n lines.push(bold(`server-action-audit · ${summary.actions} Server Action${summary.actions === 1 ? '' : 's'} in ${fileCount} file${fileCount === 1 ? '' : 's'}`), '')\n\n for (const { file, actions } of report.files) {\n const shown = options.quiet ? actions.filter((a) => a.status === 'unprotected' || a.validated === false) : actions\n if (!shown.length) continue\n lines.push(bold(file))\n const width = Math.min(32, Math.max(...shown.map((a) => a.name.length)))\n for (const action of shown) {\n const where = dim(`${action.line}:${action.column}`.padEnd(7))\n lines.push(` ${symbol(action)} ${action.name.padEnd(width)} ${where} ${describe(action)}`)\n if (action.status === 'unprotected' && action.hint) lines.push(` ${dim(action.hint)}`)\n }\n lines.push('')\n }\n\n for (const error of report.errors) lines.push(yellow(`skipped ${error.file}: ${error.message}`))\n if (report.errors.length) lines.push('')\n\n if (!summary.actions) {\n lines.push('No Server Actions found.')\n return lines.join('\\n')\n }\n lines.push(\n [\n summary.unprotected ? red(`✗ ${summary.unprotected} unprotected`) : green('✗ 0 unprotected'),\n green(`✓ ${summary.protected} protected`),\n cyan(`○ ${summary.public} public`),\n summary.unvalidated ? yellow(`! ${summary.unvalidated} with unvalidated input`) : dim('! 0 with unvalidated input'),\n ].join(dim(' · ')),\n )\n if (summary.unprotected) {\n lines.push(\n '',\n dim('Every Server Action is a public endpoint: anyone can call it with a POST request.'),\n dim('Check the session inside each action, or mark intentional ones with // @public-action.'),\n dim('https://nextjs.org/docs/app/guides/data-security'),\n )\n }\n return lines.join('\\n')\n}\n\nexport function formatJson(report: ProjectReport): string {\n return JSON.stringify(\n {\n summary: report.summary,\n files: report.files.map(({ file, actions }) => ({\n file,\n actions: actions.map(({ node: _node, ...action }) => action),\n })),\n errors: report.errors,\n },\n null,\n 2,\n )\n}\n\nconst AUTH_MESSAGE = (name: string) =>\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.`\nconst VALIDATION_MESSAGE = (name: string) =>\n `Server Action \"${name}\" uses its arguments without validating them. They come straight from the client: parse them with a schema (Zod, Valibot…).`\n\ninterface Finding {\n rule: 'require-auth' | 'require-input-validation'\n level: 'error' | 'warning'\n file: string\n action: ServerAction\n message: string\n}\n\nfunction findings(report: ProjectReport): Finding[] {\n const found: Finding[] = []\n for (const { file, actions } of report.files) {\n for (const action of actions) {\n if (action.status === 'unprotected') {\n found.push({ rule: 'require-auth', level: 'error', file, action, message: AUTH_MESSAGE(action.name) + (action.hint ? ` ${action.hint}` : '') })\n }\n if (action.validated === false) {\n found.push({ rule: 'require-input-validation', level: 'warning', file, action, message: VALIDATION_MESSAGE(action.name) })\n }\n }\n }\n return found\n}\n\n/** GitHub Actions workflow commands: the findings show up as annotations on the pull request. */\nexport function formatGithub(report: ProjectReport): string {\n const data = (text: string) => text.replace(/%/g, '%25').replace(/\\r/g, '%0D').replace(/\\n/g, '%0A')\n const property = (text: string) => data(text).replace(/:/g, '%3A').replace(/,/g, '%2C')\n const lines = findings(report).map(\n ({ rule, level, file, action, message }) =>\n `::${level} file=${property(file)},line=${action.line},col=${action.column},title=${property(`server-action-audit/${rule}`)}::${data(message)}`,\n )\n const { summary } = report\n lines.push(`server-action-audit: ${summary.actions} Server Actions, ${summary.unprotected} unprotected, ${summary.protected} protected, ${summary.public} public, ${summary.unvalidated} with unvalidated input.`)\n return lines.join('\\n')\n}\n\n/** SARIF 2.1.0, for GitHub code scanning and other security dashboards. */\nexport function formatSarif(report: ProjectReport, version: string): string {\n const rules = [\n {\n id: 'require-auth',\n name: 'RequireAuth',\n shortDescription: { text: 'Server Action without an authorization check' },\n fullDescription: { text: 'Every Next.js Server Action is a public endpoint. This one does not check who is calling it.' },\n helpUri: 'https://github.com/i6moons/server-action-audit#readme',\n defaultConfiguration: { level: 'error' },\n properties: { tags: ['security', 'nextjs'], 'security-severity': '7.5', precision: 'medium' },\n },\n {\n id: 'require-input-validation',\n name: 'RequireInputValidation',\n shortDescription: { text: 'Server Action without input validation' },\n fullDescription: { text: 'The action uses arguments sent by the client without parsing them with a schema.' },\n helpUri: 'https://github.com/i6moons/server-action-audit#readme',\n defaultConfiguration: { level: 'warning' },\n properties: { tags: ['security', 'nextjs'], 'security-severity': '4.0', precision: 'medium' },\n },\n ]\n const results = findings(report).map(({ rule, level, file, action, message }) => ({\n ruleId: rule,\n ruleIndex: rule === 'require-auth' ? 0 : 1,\n level,\n message: { text: message },\n locations: [{ physicalLocation: { artifactLocation: { uri: file }, region: { startLine: action.line, startColumn: action.column } } }],\n partialFingerprints: { actionName: `${file}#${action.name}` },\n }))\n return JSON.stringify(\n {\n $schema: 'https://json.schemastore.org/sarif-2.1.0.json',\n version: '2.1.0',\n runs: [{ tool: { driver: { name: 'server-action-audit', version, informationUri: 'https://github.com/i6moons/server-action-audit', rules } }, results }],\n },\n null,\n 2,\n )\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,91 @@
1
1
  {
2
2
  "name": "server-action-audit",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Find Next.js Server Actions that anyone can call: a CLI and an ESLint plugin that flag \"use server\" functions without an authorization check or input validation.",
5
+ "keywords": [
6
+ "nextjs",
7
+ "next.js",
8
+ "server-actions",
9
+ "use-server",
10
+ "security",
11
+ "authorization",
12
+ "auth",
13
+ "eslint",
14
+ "eslintplugin",
15
+ "eslint-plugin",
16
+ "audit",
17
+ "react-server-components",
18
+ "next-safe-action",
19
+ "appsec"
20
+ ],
21
+ "author": "Del Prudence (https://github.com/i6moons)",
22
+ "license": "MIT",
23
+ "homepage": "https://github.com/i6moons/server-action-audit#readme",
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/i6moons/server-action-audit.git"
27
+ },
28
+ "bugs": "https://github.com/i6moons/server-action-audit/issues",
29
+ "type": "module",
30
+ "engines": {
31
+ "node": ">=20"
32
+ },
33
+ "bin": {
34
+ "server-action-audit": "dist/cli.js"
35
+ },
36
+ "main": "./dist/index.cjs",
37
+ "module": "./dist/index.js",
38
+ "types": "./dist/index.d.ts",
39
+ "exports": {
40
+ ".": {
41
+ "import": {
42
+ "types": "./dist/index.d.ts",
43
+ "default": "./dist/index.js"
44
+ },
45
+ "require": {
46
+ "types": "./dist/index.d.cts",
47
+ "default": "./dist/index.cjs"
48
+ }
49
+ },
50
+ "./eslint": {
51
+ "import": {
52
+ "types": "./dist/eslint.d.ts",
53
+ "default": "./dist/eslint.js"
54
+ },
55
+ "require": {
56
+ "types": "./dist/eslint.d.cts",
57
+ "default": "./dist/eslint.cjs"
58
+ }
59
+ },
60
+ "./package.json": "./package.json"
61
+ },
62
+ "files": [
63
+ "dist"
64
+ ],
65
+ "scripts": {
66
+ "build": "tsup",
67
+ "test": "vitest run",
68
+ "typecheck": "tsc --noEmit",
69
+ "prepublishOnly": "npm run typecheck && npm test && npm run build"
70
+ },
71
+ "dependencies": {
72
+ "@typescript-eslint/typescript-estree": "^8.0.0"
73
+ },
74
+ "peerDependencies": {
75
+ "eslint": "^9.0.0 || ^10.0.0",
76
+ "typescript": ">=4.8.4"
77
+ },
78
+ "peerDependenciesMeta": {
79
+ "eslint": {
80
+ "optional": true
81
+ }
82
+ },
83
+ "devDependencies": {
84
+ "@types/node": "^22.0.0",
85
+ "@typescript-eslint/parser": "^8.0.0",
86
+ "eslint": "^9.39.0",
87
+ "tsup": "^8.5.1",
88
+ "typescript": "^5.9.3",
89
+ "vitest": "^4.1.0"
90
+ }
91
+ }