server-action-audit 0.0.0-stage → 0.2.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.
@@ -0,0 +1,677 @@
1
+ // src/ast.ts
2
+ var SKIP = /* @__PURE__ */ new Set(["parent", "loc", "range", "tokens", "comments", "typeAnnotation", "returnType", "typeParameters"]);
3
+ function walk(root, visit) {
4
+ if (!root || typeof root.type !== "string") return;
5
+ if (visit(root) === false) return;
6
+ for (const key of Object.keys(root)) {
7
+ if (SKIP.has(key)) continue;
8
+ const value = root[key];
9
+ if (Array.isArray(value)) {
10
+ for (const child of value) if (child && typeof child.type === "string") walk(child, visit);
11
+ } else if (value && typeof value.type === "string") {
12
+ walk(value, visit);
13
+ }
14
+ }
15
+ }
16
+ function isFunction(node) {
17
+ return !!node && (node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression");
18
+ }
19
+ function directives(body) {
20
+ const found = [];
21
+ for (const statement of body ?? []) {
22
+ if (statement.type !== "ExpressionStatement") break;
23
+ if (typeof statement.directive === "string") found.push(statement.directive);
24
+ else if (statement.expression?.type === "Literal" && typeof statement.expression.value === "string") found.push(statement.expression.value);
25
+ else break;
26
+ }
27
+ return found;
28
+ }
29
+ function functionDirectives(fn) {
30
+ return fn.body?.type === "BlockStatement" ? directives(fn.body.body) : [];
31
+ }
32
+ function unwrap(node) {
33
+ let current = node;
34
+ while (current.type === "ChainExpression" || current.type === "TSNonNullExpression" || current.type === "TSAsExpression" || current.type === "ParenthesizedExpression") {
35
+ current = current.expression;
36
+ }
37
+ return current;
38
+ }
39
+ function calleePath(callee) {
40
+ const node = unwrap(callee);
41
+ if (node.type === "Identifier") return node.name;
42
+ if (node.type === "MemberExpression") {
43
+ const property = !node.computed && node.property?.type === "Identifier" ? node.property.name : "*";
44
+ return `${calleePath(node.object)}.${property}`;
45
+ }
46
+ if (node.type === "Super") return "super";
47
+ if (node.type === "ThisExpression") return "this";
48
+ return "*";
49
+ }
50
+ function lastSegment(path) {
51
+ const index = path.lastIndexOf(".");
52
+ return index === -1 ? path : path.slice(index + 1);
53
+ }
54
+ function callsIn(root) {
55
+ const calls = [];
56
+ walk(root, (node) => {
57
+ if (node.type === "CallExpression") calls.push(node);
58
+ });
59
+ return calls;
60
+ }
61
+ function unconditionalCalls(fn, followReturned = false) {
62
+ const calls = [];
63
+ const expression = (node) => {
64
+ if (!node || typeof node.type !== "string" || isFunction(node)) return;
65
+ if (node.type === "CallExpression") calls.push(node);
66
+ for (const key of Object.keys(node)) {
67
+ if (SKIP.has(key)) continue;
68
+ const value = node[key];
69
+ if (Array.isArray(value)) value.forEach((child) => child && expression(child));
70
+ else if (value && typeof value.type === "string") expression(value);
71
+ }
72
+ };
73
+ const returned = (node) => {
74
+ if (followReturned && node && isFunction(node)) calls.push(...unconditionalCalls(node));
75
+ else expression(node);
76
+ };
77
+ const statement = (node) => {
78
+ if (!node) return;
79
+ switch (node.type) {
80
+ case "BlockStatement":
81
+ for (const child of node.body ?? []) statement(child);
82
+ break;
83
+ case "ExpressionStatement":
84
+ expression(node.expression);
85
+ break;
86
+ case "VariableDeclaration":
87
+ for (const declarator of node.declarations ?? []) expression(declarator.init);
88
+ break;
89
+ case "ReturnStatement":
90
+ returned(node.argument);
91
+ break;
92
+ case "ThrowStatement":
93
+ expression(node.argument);
94
+ break;
95
+ case "IfStatement":
96
+ expression(node.test);
97
+ break;
98
+ case "TryStatement":
99
+ statement(node.block);
100
+ break;
101
+ case "SwitchStatement":
102
+ expression(node.discriminant);
103
+ break;
104
+ case "DoWhileStatement":
105
+ statement(node.body);
106
+ break;
107
+ case "LabeledStatement":
108
+ statement(node.body);
109
+ break;
110
+ }
111
+ };
112
+ if (fn.body?.type === "BlockStatement") statement(fn.body);
113
+ else returned(fn.body);
114
+ return calls;
115
+ }
116
+ function callChain(call) {
117
+ const methods = [];
118
+ const functions = [];
119
+ const identifiers = [];
120
+ let current = unwrap(call);
121
+ for (; ; ) {
122
+ if (current.type === "CallExpression") {
123
+ for (const argument of current.arguments ?? []) {
124
+ if (isFunction(argument)) functions.unshift(argument);
125
+ else if (argument.type === "Identifier") identifiers.unshift(argument.name);
126
+ }
127
+ const callee = unwrap(current.callee);
128
+ if (callee.type === "MemberExpression") {
129
+ methods.unshift(!callee.computed && callee.property?.type === "Identifier" ? callee.property.name : "*");
130
+ current = unwrap(callee.object);
131
+ continue;
132
+ }
133
+ if (callee.type === "Identifier") return { root: callee.name, methods, functions, identifiers };
134
+ return { root: null, methods, functions, identifiers };
135
+ }
136
+ if (current.type === "MemberExpression") {
137
+ methods.unshift(!current.computed && current.property?.type === "Identifier" ? current.property.name : "*");
138
+ current = unwrap(current.object);
139
+ continue;
140
+ }
141
+ if (current.type === "AwaitExpression") {
142
+ current = unwrap(current.argument);
143
+ continue;
144
+ }
145
+ return { root: current.type === "Identifier" ? current.name : null, methods, functions, identifiers };
146
+ }
147
+ }
148
+ function position(node) {
149
+ return node.loc ? { line: node.loc.start.line, column: node.loc.start.column + 1 } : { line: 0, column: 0 };
150
+ }
151
+
152
+ // src/options.ts
153
+ var EXACT_PATHS = ["auth"];
154
+ var NAMES = [
155
+ "currentUser",
156
+ // Clerk
157
+ "getServerSession",
158
+ // NextAuth v4
159
+ "getKindeServerSession",
160
+ // Kinde
161
+ "validateRequest",
162
+ // Lucia
163
+ "getIronSession",
164
+ // iron-session
165
+ "withAuth",
166
+ // WorkOS AuthKit
167
+ "verifyIdToken",
168
+ // Firebase Admin
169
+ "verifySessionCookie",
170
+ // Firebase Admin
171
+ "jwtVerify",
172
+ // jose
173
+ "verifyToken",
174
+ "verifyJwt",
175
+ "verifyAccessToken",
176
+ "getSession",
177
+ "verifySession",
178
+ "getCurrentUser",
179
+ "getAuthUser",
180
+ "getAuthenticatedUser",
181
+ "getSessionUser",
182
+ "getUserOrThrow",
183
+ "requireAuth",
184
+ "requireUser",
185
+ "requireSession",
186
+ "requireAdmin",
187
+ "requireRole",
188
+ "requirePermission",
189
+ "checkAuth",
190
+ "ensureAuth",
191
+ "ensureUser",
192
+ "assertAuth",
193
+ "assertUser",
194
+ "isAuthenticated",
195
+ "authenticate",
196
+ "authorize"
197
+ ];
198
+ var SUFFIXES = [
199
+ "auth.getUser",
200
+ // Supabase
201
+ "auth.getClaims",
202
+ // Supabase
203
+ "auth.getSession",
204
+ // Supabase
205
+ "auth.protect",
206
+ // Clerk
207
+ "api.getSession"
208
+ // Better Auth
209
+ ];
210
+ var AUTH_NAME = /auth|protect|guard|session|user|admin|private|secure|logged/i;
211
+ var 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;
212
+ var 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;
213
+ function resolveOptions(options = {}) {
214
+ const extra = options.authFunctions ?? [];
215
+ return {
216
+ exactPaths: new Set(EXACT_PATHS),
217
+ names: /* @__PURE__ */ new Set([...NAMES, ...extra.filter((name) => !name.includes("."))]),
218
+ suffixes: [...SUFFIXES, ...extra.filter((name) => name.includes("."))],
219
+ authWrappers: new Set(options.authWrappers ?? []),
220
+ authClients: new Set(options.authClients ?? []),
221
+ publicActions: new Set(options.publicActions ?? []),
222
+ ignore: options.ignore ?? []
223
+ };
224
+ }
225
+ function isAuthCall(path, options) {
226
+ if (options.exactPaths.has(path)) return true;
227
+ if (options.names.has(lastSegment(path))) return true;
228
+ return options.suffixes.some((suffix) => path === suffix || path.endsWith(`.${suffix}`));
229
+ }
230
+ var VALIDATION_METHODS = /* @__PURE__ */ new Set(["parse", "safeParse", "parseAsync", "safeParseAsync", "validate", "validateSync", "validateAsync", "assert"]);
231
+ var NOT_VALIDATION = /* @__PURE__ */ new Set(["JSON", "Date", "Number", "URL", "path", "Math", "BigInt", "console", "assert"]);
232
+ function isValidationCall(path) {
233
+ const method = lastSegment(path);
234
+ if (!VALIDATION_METHODS.has(method)) return false;
235
+ const owner = path.includes(".") ? path.slice(0, path.lastIndexOf(".")) : "";
236
+ return !NOT_VALIDATION.has(lastSegment(owner) || owner);
237
+ }
238
+
239
+ // src/analyze.ts
240
+ var PUBLIC_MARKER = /@public-action\b/;
241
+ var CLIENT_METHODS = /* @__PURE__ */ new Set(["action", "stateAction", "handler", "createServerAction", "createAction"]);
242
+ var SCHEMA_METHODS = /* @__PURE__ */ new Set(["schema", "inputSchema", "input", "bindArgsSchemas"]);
243
+ var MAX_DEPTH = 4;
244
+ var ModuleScope = class {
245
+ constructor(ast, filename) {
246
+ this.ast = ast;
247
+ this.filename = filename;
248
+ for (const statement of ast.body ?? []) this.index(statement);
249
+ }
250
+ ast;
251
+ filename;
252
+ functions = /* @__PURE__ */ new Map();
253
+ imports = /* @__PURE__ */ new Map();
254
+ exports = /* @__PURE__ */ new Map();
255
+ /** Top-level declarations by name, with the statement that holds them (for comments). */
256
+ declarations = /* @__PURE__ */ new Map();
257
+ declare(name, init, statement) {
258
+ this.declarations.set(name, { init, statement });
259
+ if (isFunction(init)) this.functions.set(name, init);
260
+ else if (init.type === "CallExpression") {
261
+ const wrapped = (init.arguments ?? []).find((argument) => isFunction(argument));
262
+ if (wrapped) this.functions.set(name, wrapped);
263
+ }
264
+ }
265
+ index(statement) {
266
+ const declaration = statement.type === "ExportNamedDeclaration" ? statement.declaration : statement;
267
+ if (statement.type === "ImportDeclaration" && statement.importKind !== "type") {
268
+ for (const specifier of statement.specifiers ?? []) {
269
+ const imported = specifier.type === "ImportDefaultSpecifier" ? "default" : specifier.type === "ImportNamespaceSpecifier" ? "*" : specifier.imported?.name ?? specifier.imported?.value;
270
+ this.imports.set(specifier.local.name, { source: statement.source.value, imported });
271
+ }
272
+ return;
273
+ }
274
+ if (declaration?.type === "FunctionDeclaration" && declaration.id) {
275
+ this.declare(declaration.id.name, declaration, statement);
276
+ if (statement.type === "ExportNamedDeclaration") this.exports.set(declaration.id.name, declaration.id.name);
277
+ }
278
+ if (declaration?.type === "VariableDeclaration") {
279
+ for (const declarator of declaration.declarations ?? []) {
280
+ if (declarator.id?.type !== "Identifier" || !declarator.init) continue;
281
+ this.declare(declarator.id.name, declarator.init, statement);
282
+ if (statement.type === "ExportNamedDeclaration") this.exports.set(declarator.id.name, declarator.id.name);
283
+ }
284
+ }
285
+ if (statement.type === "ExportNamedDeclaration" && !statement.declaration) {
286
+ for (const specifier of statement.specifiers ?? []) {
287
+ const exported = specifier.exported?.name ?? specifier.exported?.value;
288
+ const local = specifier.local?.name ?? specifier.local?.value;
289
+ this.exports.set(exported, statement.source ? { source: statement.source.value, imported: local } : local);
290
+ }
291
+ }
292
+ if (statement.type === "ExportDefaultDeclaration") {
293
+ const value = statement.declaration;
294
+ if (value?.type === "Identifier") this.exports.set("default", value.name);
295
+ else if (value) {
296
+ this.declare("default", value, statement);
297
+ this.exports.set("default", "default");
298
+ }
299
+ }
300
+ }
301
+ };
302
+ function scopeOf(module, context) {
303
+ let scope = context.scopes.get(module.filename);
304
+ if (!scope) {
305
+ scope = new ModuleScope(module.ast, module.filename);
306
+ context.scopes.set(module.filename, scope);
307
+ }
308
+ return scope;
309
+ }
310
+ function resolveFunction(name, scope, context, depth) {
311
+ const local = scope.functions.get(name);
312
+ if (local) return { fn: local, scope };
313
+ const binding = scope.imports.get(name);
314
+ if (!binding || !context.resolver || depth > MAX_DEPTH) return null;
315
+ return exportedFunction(binding, scope, context, depth + 1);
316
+ }
317
+ function exportedFunction(binding, from, context, depth) {
318
+ if (depth > MAX_DEPTH || binding.imported === "*") return null;
319
+ const module = context.resolver?.(binding.source, from.filename);
320
+ if (!module) return null;
321
+ const scope = scopeOf(module, context);
322
+ const entry = scope.exports.get(binding.imported);
323
+ if (entry === void 0) return null;
324
+ if (typeof entry === "string") {
325
+ const fn = scope.functions.get(entry);
326
+ return fn ? { fn, scope } : resolveFunction(entry, scope, context, depth);
327
+ }
328
+ return exportedFunction(entry, scope, context, depth + 1);
329
+ }
330
+ function authEvidence(fn, scope, context, depth = 0, seen = /* @__PURE__ */ new Set(), followReturned = false) {
331
+ if (depth > MAX_DEPTH || seen.has(fn)) return null;
332
+ seen.add(fn);
333
+ const calls = unconditionalCalls(fn, followReturned);
334
+ for (const call of calls) {
335
+ const path = calleePath(call.callee);
336
+ if (isAuthCall(path, context.options)) return `${path}()`;
337
+ }
338
+ for (const call of calls) {
339
+ const path = calleePath(call.callee);
340
+ if (path.includes(".") || path === "*" || GUEST_GUARD.test(path) || !HELPER_NAME.test(path)) continue;
341
+ const helper = resolveFunction(path, scope, context, depth);
342
+ if (!helper) continue;
343
+ const evidence = authEvidence(helper.fn, helper.scope, context, depth + 1, seen);
344
+ if (evidence) return `${path}() \u2192 ${evidence}`;
345
+ }
346
+ return null;
347
+ }
348
+ function boundNames(pattern, names = []) {
349
+ if (!pattern) return names;
350
+ if (pattern.type === "Identifier") names.push(pattern.name);
351
+ else if (pattern.type === "ObjectPattern") for (const property of pattern.properties ?? []) boundNames(property.type === "RestElement" ? property.argument : property.value, names);
352
+ else if (pattern.type === "ArrayPattern") for (const element of pattern.elements ?? []) boundNames(element, names);
353
+ else if (pattern.type === "AssignmentPattern") boundNames(pattern.left, names);
354
+ else if (pattern.type === "RestElement") boundNames(pattern.argument, names);
355
+ return names;
356
+ }
357
+ function closureEvidence(action, enclosing, scope, context) {
358
+ if (enclosing.body?.type !== "BlockStatement") return null;
359
+ const fromAuth = /* @__PURE__ */ new Map();
360
+ for (const statement of enclosing.body.body ?? []) {
361
+ if (statement.type !== "VariableDeclaration") continue;
362
+ for (const declarator of statement.declarations ?? []) {
363
+ const evidence = authEvidence({ type: "ArrowFunctionExpression", params: [], body: declarator.init }, scope, context);
364
+ if (evidence) for (const name of boundNames(declarator.id)) fromAuth.set(name, evidence);
365
+ }
366
+ }
367
+ if (!fromAuth.size) return null;
368
+ let found = null;
369
+ walk(action.body, (node) => {
370
+ if (found) return false;
371
+ if (node.type === "Identifier" && fromAuth.has(node.name)) found = `${node.name} from ${fromAuth.get(node.name)} in the enclosing component`;
372
+ });
373
+ return found;
374
+ }
375
+ function declarationFrom(binding, from, context, depth) {
376
+ if (depth > MAX_DEPTH || binding.imported === "*" || !context.resolver) return null;
377
+ const module = context.resolver(binding.source, from.filename);
378
+ if (!module) return null;
379
+ const scope = scopeOf(module, context);
380
+ const entry = scope.exports.get(binding.imported);
381
+ if (typeof entry === "string") return resolveDeclaration(entry, scope, context, depth + 1);
382
+ return entry ? declarationFrom(entry, scope, context, depth + 1) : null;
383
+ }
384
+ function resolveDeclaration(name, scope, context, depth) {
385
+ const local = scope.declarations.get(name);
386
+ if (local) return { init: local.init, scope };
387
+ const binding = scope.imports.get(name);
388
+ return binding ? declarationFrom(binding, scope, context, depth) : null;
389
+ }
390
+ function definitionEvidence(name, scope, context, depth = 0) {
391
+ if (depth > MAX_DEPTH || GUEST_GUARD.test(name)) return null;
392
+ if (depth > 0 && (context.options.authClients.has(name) || context.options.authWrappers.has(name) || AUTH_NAME.test(name))) return name;
393
+ const definition = resolveFunction(name, scope, context, 0);
394
+ const evidence = definition && authEvidence(definition.fn, definition.scope, context, 1, /* @__PURE__ */ new Set(), true);
395
+ if (evidence) return `${name} \u2192 ${evidence}`;
396
+ const declaration = resolveDeclaration(name, scope, context, 0);
397
+ if (declaration?.init.type === "CallExpression") {
398
+ const chain = callChain(declaration.init);
399
+ for (const base of [chain.root, ...chain.identifiers]) {
400
+ if (!base || base === name) continue;
401
+ const inherited = definitionEvidence(base, declaration.scope, context, depth + 1);
402
+ if (inherited) return `${name} \u2192 ${inherited}`;
403
+ }
404
+ }
405
+ return null;
406
+ }
407
+ function validatesDirectly(fn) {
408
+ return callsIn(fn.body).some((call) => isValidationCall(calleePath(call.callee)));
409
+ }
410
+ function parameterCount(fn) {
411
+ return (fn.params ?? []).length;
412
+ }
413
+ function leadingComments(node, comments, text) {
414
+ const start = node.range?.[0];
415
+ if (start === void 0) return [];
416
+ const found = [];
417
+ let cursor = start;
418
+ for (const comment of [...comments].reverse()) {
419
+ const [from, to] = comment.range ?? [Infinity, Infinity];
420
+ if (to > cursor) continue;
421
+ if (text.slice(to, cursor).trim() !== "") break;
422
+ found.push(comment);
423
+ cursor = from;
424
+ }
425
+ return found;
426
+ }
427
+ function bodyHeadComments(fn, comments) {
428
+ if (fn.body?.type !== "BlockStatement" || !fn.body.range) return [];
429
+ const [bodyStart, bodyEnd] = fn.body.range;
430
+ const statements = fn.body.body ?? [];
431
+ const firstReal = statements.find((statement) => !(statement.type === "ExpressionStatement" && typeof statement.directive === "string"));
432
+ const limit = firstReal?.range?.[0] ?? bodyEnd;
433
+ return comments.filter((comment) => comment.range && comment.range[0] > bodyStart && comment.range[1] <= limit);
434
+ }
435
+ function moduleCandidates(scope) {
436
+ const candidates = [];
437
+ for (const [exported, entry] of scope.exports) {
438
+ if (typeof entry !== "string") continue;
439
+ const declaration = scope.declarations.get(entry);
440
+ if (!declaration) continue;
441
+ const { init, statement } = declaration;
442
+ const target = init.type === "AwaitExpression" ? init.argument : init;
443
+ if (!isFunction(target) && target.type !== "CallExpression") continue;
444
+ const name = exported === "default" && target.id?.type === "Identifier" ? target.id.name : exported;
445
+ candidates.push({ name, kind: "module", target, anchors: [statement, init] });
446
+ }
447
+ return candidates;
448
+ }
449
+ function inlineCandidates(ast, skip) {
450
+ const candidates = [];
451
+ const visit = (node, parent, grandparent, enclosing) => {
452
+ if (isFunction(node) && !skip.has(node) && functionDirectives(node).includes("use server")) {
453
+ let name = node.id?.name;
454
+ if (!name && parent?.type === "VariableDeclarator" && parent.id?.type === "Identifier") name = parent.id.name;
455
+ if (!name && (parent?.type === "Property" || parent?.type === "MethodDefinition") && parent.key?.type === "Identifier") name = parent.key.name;
456
+ if (!name && parent?.type === "JSXExpressionContainer" && grandparent?.type === "JSXAttribute") name = `(inline ${grandparent.name?.name ?? "action"})`;
457
+ candidates.push({ name: name ?? "(inline)", kind: "inline", target: node, anchors: [parent?.type === "VariableDeclarator" ? grandparent ?? node : node], enclosing });
458
+ }
459
+ const inner = isFunction(node) ? node : enclosing;
460
+ for (const key of Object.keys(node)) {
461
+ if (key === "parent" || key === "loc" || key === "range") continue;
462
+ const value = node[key];
463
+ if (Array.isArray(value)) {
464
+ for (const child of value) if (child && typeof child.type === "string") visit(child, node, parent, inner);
465
+ } else if (value && typeof value.type === "string") {
466
+ visit(value, node, parent, inner);
467
+ }
468
+ }
469
+ };
470
+ visit(ast, null, null, null);
471
+ return candidates;
472
+ }
473
+ function analyzeCandidate(candidate, scope, context, comments, text) {
474
+ const { target } = candidate;
475
+ let reason = null;
476
+ let validated = null;
477
+ let hint = null;
478
+ const handlers = [];
479
+ if (isFunction(target)) {
480
+ handlers.push(target);
481
+ reason = authEvidence(target, scope, context);
482
+ if (!reason && candidate.enclosing) reason = closureEvidence(target, candidate.enclosing, scope, context);
483
+ validated = parameterCount(target) === 0 ? null : validatesDirectly(target);
484
+ } else {
485
+ const chain = callChain(target);
486
+ handlers.push(...chain.functions);
487
+ const isClient = chain.methods.some((method) => CLIENT_METHODS.has(method));
488
+ const root = chain.root;
489
+ const trusted = root !== null && (context.options.authClients.has(root) || context.options.authWrappers.has(root) || AUTH_NAME.test(root) && !GUEST_GUARD.test(root));
490
+ if (trusted) reason = isClient ? `${root} client` : `${root}() wrapper`;
491
+ for (const handler of chain.functions) reason ??= authEvidence(handler, scope, context);
492
+ for (const name of [root, ...chain.identifiers]) {
493
+ if (!reason && name) reason = definitionEvidence(name, scope, context);
494
+ }
495
+ if (isClient) {
496
+ validated = chain.methods.some((method) => SCHEMA_METHODS.has(method)) ? true : null;
497
+ } else if (root && /valid|schema|zod|parse/i.test(root)) {
498
+ validated = true;
499
+ } else {
500
+ const handler = chain.functions[0];
501
+ validated = !handler || parameterCount(handler) === 0 ? null : validatesDirectly(handler);
502
+ }
503
+ if (!reason && root) {
504
+ hint = isClient ? `If "${root}" has auth middleware, add it to "authClients".` : `If ${root}() checks the session, add it to "authWrappers".`;
505
+ }
506
+ }
507
+ const markerComments = [
508
+ ...candidate.anchors.flatMap((anchor) => leadingComments(anchor, comments, text)),
509
+ ...handlers.flatMap((handler) => bodyHeadComments(handler, comments))
510
+ ];
511
+ const markedPublic = markerComments.some((comment) => PUBLIC_MARKER.test(comment.value));
512
+ const listedPublic = context.options.publicActions.has(candidate.name);
513
+ const status = reason ? "protected" : markedPublic || listedPublic ? "public" : "unprotected";
514
+ if (status === "public") {
515
+ reason = markedPublic ? "@public-action" : "publicActions";
516
+ hint = null;
517
+ }
518
+ const { line, column } = position(candidate.anchors[0] ?? target);
519
+ return { name: candidate.name, kind: candidate.kind, line, column, status, reason, validated, hint, node: candidate.anchors[0] ?? target };
520
+ }
521
+ function analyzeModule(input, options, resolver) {
522
+ if (!input.text.includes("use server")) return [];
523
+ const context = { options, resolver, scopes: /* @__PURE__ */ new Map() };
524
+ const scope = scopeOf({ ast: input.ast, filename: input.filename }, context);
525
+ const comments = input.comments ?? input.ast.comments ?? [];
526
+ const candidates = [];
527
+ if (directives(input.ast.body).includes("use server")) candidates.push(...moduleCandidates(scope));
528
+ const covered = new Set(candidates.map((candidate) => candidate.target));
529
+ candidates.push(...inlineCandidates(input.ast, covered));
530
+ return candidates.map((candidate) => analyzeCandidate(candidate, scope, context, comments, input.text)).sort((a, b) => a.line - b.line || a.column - b.column);
531
+ }
532
+
533
+ // src/resolve.ts
534
+ import { existsSync, readFileSync, statSync } from "fs";
535
+ import { dirname, join, resolve } from "path";
536
+ import { parse } from "@typescript-eslint/typescript-estree";
537
+ var EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"];
538
+ function parseModule(text, filename) {
539
+ return parse(text, {
540
+ jsx: !/\.[cm]?ts$/.test(filename),
541
+ loc: true,
542
+ range: true,
543
+ comment: true
544
+ });
545
+ }
546
+ function isFile(path) {
547
+ try {
548
+ return statSync(path).isFile();
549
+ } catch {
550
+ return false;
551
+ }
552
+ }
553
+ var TS_SOURCES = { ".js": [".ts", ".tsx"], ".jsx": [".tsx"], ".mjs": [".mts"], ".cjs": [".cts"] };
554
+ function tryFile(base) {
555
+ if (isFile(base)) return base;
556
+ const jsExtension = base.match(/\.(js|jsx|mjs|cjs)$/)?.[0];
557
+ if (jsExtension) {
558
+ for (const extension of TS_SOURCES[jsExtension] ?? []) {
559
+ const candidate = base.slice(0, -jsExtension.length) + extension;
560
+ if (isFile(candidate)) return candidate;
561
+ }
562
+ }
563
+ for (const extension of EXTENSIONS) if (isFile(base + extension)) return base + extension;
564
+ for (const extension of EXTENSIONS) if (isFile(join(base, `index${extension}`))) return join(base, `index${extension}`);
565
+ return null;
566
+ }
567
+ function parseJsonc(text) {
568
+ let out = "";
569
+ let inString = false;
570
+ for (let i = 0; i < text.length; i++) {
571
+ const char = text[i];
572
+ if (inString) {
573
+ out += char;
574
+ if (char === "\\") out += text[++i] ?? "";
575
+ else if (char === '"') inString = false;
576
+ } else if (char === '"') {
577
+ inString = true;
578
+ out += char;
579
+ } else if (char === "/" && text[i + 1] === "/") {
580
+ while (i < text.length && text[i] !== "\n") i++;
581
+ out += "\n";
582
+ } else if (char === "/" && text[i + 1] === "*") {
583
+ i = text.indexOf("*/", i + 2);
584
+ if (i === -1) break;
585
+ i++;
586
+ } else {
587
+ out += char;
588
+ }
589
+ }
590
+ return JSON.parse(out.replace(/,(\s*[}\]])/g, "$1"));
591
+ }
592
+ var mappings = /* @__PURE__ */ new Map();
593
+ function pathMapping(fromDir) {
594
+ if (mappings.has(fromDir)) return mappings.get(fromDir);
595
+ let mapping = null;
596
+ for (const name of ["tsconfig.json", "jsconfig.json"]) {
597
+ const file = join(fromDir, name);
598
+ if (!existsSync(file)) continue;
599
+ try {
600
+ const config = parseJsonc(readFileSync(file, "utf8"));
601
+ const options = config.compilerOptions ?? {};
602
+ if (options.paths) mapping = { base: resolve(fromDir, options.baseUrl ?? "."), paths: options.paths };
603
+ } catch {
604
+ mapping = null;
605
+ }
606
+ break;
607
+ }
608
+ if (!mapping) {
609
+ const parent = dirname(fromDir);
610
+ mapping = parent !== fromDir && !existsSync(join(fromDir, "package.json")) ? pathMapping(parent) : null;
611
+ }
612
+ mappings.set(fromDir, mapping);
613
+ return mapping;
614
+ }
615
+ function resolveAlias(specifier, fromFile) {
616
+ const mapping = pathMapping(dirname(fromFile));
617
+ if (!mapping) return null;
618
+ for (const [pattern, targets] of Object.entries(mapping.paths)) {
619
+ const star = pattern.indexOf("*");
620
+ let captured = null;
621
+ if (star === -1) captured = specifier === pattern ? "" : null;
622
+ else {
623
+ const prefix = pattern.slice(0, star);
624
+ const suffix = pattern.slice(star + 1);
625
+ if (specifier.startsWith(prefix) && specifier.endsWith(suffix) && specifier.length >= prefix.length + suffix.length) {
626
+ captured = specifier.slice(prefix.length, specifier.length - suffix.length);
627
+ }
628
+ }
629
+ if (captured === null) continue;
630
+ for (const target of targets) {
631
+ const file = tryFile(resolve(mapping.base, target.replace("*", captured)));
632
+ if (file) return file;
633
+ }
634
+ }
635
+ return null;
636
+ }
637
+ function resolveImportPath(specifier, fromFile) {
638
+ if (specifier.startsWith(".")) return tryFile(resolve(dirname(fromFile), specifier));
639
+ if (specifier.startsWith("/")) return tryFile(specifier);
640
+ return resolveAlias(specifier, fromFile);
641
+ }
642
+ function createNodeResolver() {
643
+ const cache = /* @__PURE__ */ new Map();
644
+ return (specifier, fromFile) => {
645
+ const file = resolveImportPath(specifier, fromFile);
646
+ if (!file) return null;
647
+ let mtime = 0;
648
+ try {
649
+ mtime = statSync(file).mtimeMs;
650
+ } catch {
651
+ return null;
652
+ }
653
+ const cached = cache.get(file);
654
+ if (cached && cached.mtime === mtime) return cached.module;
655
+ let module = null;
656
+ try {
657
+ module = { ast: parseModule(readFileSync(file, "utf8"), file), filename: file };
658
+ } catch {
659
+ module = null;
660
+ }
661
+ cache.set(file, { mtime, module });
662
+ return module;
663
+ };
664
+ }
665
+
666
+ // src/version.ts
667
+ var VERSION = "0.2.0";
668
+
669
+ export {
670
+ resolveOptions,
671
+ analyzeModule,
672
+ parseModule,
673
+ resolveImportPath,
674
+ createNodeResolver,
675
+ VERSION
676
+ };
677
+ //# sourceMappingURL=chunk-RCEEE6TI.js.map