#!/usr/bin/env node /** * Add an auth check to every exported async function in a "use server" * file using the TypeScript compiler API. Idempotent. * * The check inserted as the first statement in the function body is: * * const adminUser = await getAdminUser(); * if (!adminUser) return { success: false, error: "Unauthorized" }; * * If the function body already calls getAdminUser() or getSession() within * its first few statements, it is left alone. * * Usage: node scripts/fix-server-auth-ast.js ... */ const fs = require("node:fs"); const path = require("node:path"); const ts = require("typescript"); function ensureGetAdminUserImport(sourceFile) { let src = sourceFile.text; const hasImport = sourceFile.statements.some( (s) => ts.isImportDeclaration(s) && s.moduleSpecifier && s.moduleSpecifier.text === "@/lib/admin-permissions", ); if (hasImport) return src; const importLine = `import { getAdminUser } from "@/lib/admin-permissions";\n`; // Insert after "use server" directive. for (const stmt of sourceFile.statements) { if ( ts.isExpressionStatement(stmt) && ts.isStringLiteral(stmt.expression) && (stmt.expression.text === "use server" || stmt.expression.text === "use client") ) { const pos = stmt.getEnd(); return src.slice(0, pos) + "\n" + importLine + src.slice(pos); } } // Otherwise after the first import. for (const stmt of sourceFile.statements) { if (ts.isImportDeclaration(stmt)) { const pos = stmt.getEnd(); return src.slice(0, pos) + "\n" + importLine + src.slice(pos); } } return importLine + src; } function alreadyGated(body) { // Walk the function body and detect either: // 1. A getAdminUser() / getSession() call (auth gate already in place) // 2. An existing `const adminUser = ...` declaration (would collide) let hasAuthCall = false; let hasAdminUserVar = false; function walk(node) { if ( ts.isCallExpression(node) && ts.isIdentifier(node.expression) && (node.expression.text === "getAdminUser" || node.expression.text === "getSession") ) { hasAuthCall = true; } if ( ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.name.text === "adminUser" ) { hasAdminUserVar = true; } ts.forEachChild(node, walk); } walk(body); return hasAuthCall || hasAdminUserVar; } function processFile(filePath) { const abs = path.resolve(filePath); if (!fs.existsSync(abs)) { console.error(`skip (not found): ${abs}`); return 0; } const original = fs.readFileSync(abs, "utf8"); if (!original.includes('"use server"') && !original.includes("'use server'")) { return 0; } const sourceFile = ts.createSourceFile( abs, original, ts.ScriptTarget.Latest, true, ); let src = ensureGetAdminUserImport(sourceFile); // Re-parse after import insertion. const sf = ts.createSourceFile(abs, src, ts.ScriptTarget.Latest, true); const edits = []; function visit(node) { if ( ts.isFunctionDeclaration(node) && node.modifiers && node.modifiers.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) && node.modifiers.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) && node.body ) { if (!alreadyGated(node.body)) { // Throwing is universal — it works regardless of the function's // return type annotation (Promise, {success, error}, etc.) // and matches how the rest of the app signals "auth gate" errors. const insert = `\n const adminUser = await getAdminUser();\n if (!adminUser) throw new Error("Unauthorized");\n`; const pos = node.body.getStart(sf) + 1; // after `{` edits.push({ pos, insert }); } } ts.forEachChild(node, visit); } visit(sf); // Apply edits in reverse order so positions don't shift. edits.sort((a, b) => b.pos - a.pos); let out = src; for (const e of edits) { out = out.slice(0, e.pos) + e.insert + out.slice(e.pos); } if (edits.length > 0) { fs.writeFileSync(abs, out, "utf8"); console.log(`patched (${edits.length} functions): ${abs}`); } return edits.length; } const files = process.argv.slice(2); if (files.length === 0) { console.error("usage: node scripts/fix-server-auth-ast.js ..."); process.exit(2); } let patched = 0, total = 0; for (const f of files) { const n = processFile(f); if (n > 0) patched++; total += n; } console.log(`done — ${patched} files, ${total} functions`);