UNPKG

run402-mcp

Version:

MCP server for Run402 — full-stack backend infrastructure for AI agents: Postgres, auth, storage, serverless functions and atomic deploys. Paid with x402/MPP. Includes $0.03 image generation.

573 lines • 28.9 kB
import { collectLocalFileReferences } from "./deploy-manifest.js"; /** * `run402 doctor` source-scan module (auth-aware-ssr Section 9). * * Walks the project's `src/` directory and reports patterns the * auth-aware-ssr design specifies as deploy-failing OR runtime warnings: * * - **Hallucinated SDK names.** `getUser`, `getSession`, `currentUser`, * `getCurrentUser`, `getServerSession`, `auth.protect`, `auth.signIn`, * `auth.logout`, `auth.middleware`, etc. Each hit emits * `R402_AUTH_UNKNOWN_EXPORT` with a structured fix-it (attempted name, * canonical replacement, import line, docs URL). * * - **State-changing GET handlers.** Astro pages that export a GET * handler containing DB-mutation patterns (`db().insert`, `db().update`, * `db().delete`, `adminDb().sql("UPDATE"`, etc.). Emit * `R402_AUTH_STATE_CHANGING_GET`. * * - **`auth.*` calls in prerendered pages.** Astro pages declaring * `export const prerender = true` that also call `auth.*` helpers. * Emit `R402_AUTH_PRERENDERED`. * * - **Direct `internal.sessions.authz_version` mutation.** Consumer * migrations that try to `UPDATE internal.sessions SET authz_version` * manually. Emit `R402_AUTH_AUTHZ_VERSION_PROHIBITED`. * * The scanner is regex-based — fast, dependency-free, and good enough * for the canonical patterns. The `run402 doctor --json` mode emits the * structured envelope; the default mode prints a readable per-finding * summary. Wired into `run402 deploy` pre-flight as a deploy-failing * gate for the error severities; non-blocking for warning severities. * * @see the auth-aware-ssr OpenSpec change (functions-sdk-auth-model) */ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"; import { extname, join, relative } from "node:path"; /** Severity ladder for scanner findings. `error` blocks deploy; `warn` * reports but doesn't block. The `run402 doctor` exit code is non-zero * whenever any `error`-severity finding is present. */ export const SCAN_SEVERITY = Object.freeze({ ERROR: "error", WARN: "warn", }); /** File extensions scanned. Astro frontmatter + TS/JS are the primary * surface; the regex matchers fire equally on all of them. `.astro` * matters because consumers write SSR pages there. */ const SCANNED_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".astro"]); /** Directories the scanner refuses to descend into. We never report * on platform-generated code or vendored dependencies. */ const SKIPPED_DIRECTORIES = new Set([ "node_modules", ".git", ".vscode", ".idea", "dist", "build", "out", ".astro", ".next", ".vercel", ".netlify", "coverage", ]); /** Hallucinated-name registry from the auth-aware-ssr spec. Each entry * carries the canonical replacement so the fix-it is actionable. The * list is intentionally exhaustive — every name flagged here came up * in pre-launch LLM hallucination samples. */ const HALLUCINATED_NAMES = [ // ESM named imports — caught by regex on import lines AND by call-site // matching when consumers paste from training-data examples. { name: "getUser", canonical: "auth.user() / auth.requireUser()", origin: "supabase / clerk / nextauth legacy" }, { name: "getUserId", canonical: "(await auth.user())?.id", origin: "run402 v0.x" }, { name: "getRole", canonical: "auth.requireRole(role)", origin: "run402 v0.x" }, { name: "getSession", canonical: "auth.user()", origin: "next-auth / nextauth" }, { name: "currentUser", canonical: "auth.user()", origin: "clerk" }, { name: "currentSession", canonical: "auth.user()", origin: "clerk" }, { name: "getCurrentUser", canonical: "auth.user()", origin: "generic" }, { name: "getCurrentSession", canonical: "auth.user()", origin: "generic" }, { name: "getServerSession", canonical: "auth.user()", origin: "next-auth" }, { name: "getAuth", canonical: "auth.user() / auth.requireUser()", origin: "clerk" }, { name: "requireAuth", canonical: "auth.requireUser()", origin: "generic" }, { name: "withAuth", canonical: "auth.requireUser() inside the handler", origin: "next-auth-style HOC" }, { name: "protectRoute", canonical: "auth.requireUser() inside the handler", origin: "generic" }, { name: "useUser", canonical: "auth.user() (note: server-only; not a React hook)", origin: "clerk / supabase" }, { name: "useSession", canonical: "auth.user() (note: server-only)", origin: "next-auth / clerk" }, { name: "createServerClient", canonical: "db() (use the bundled SDK; no client setup needed)", origin: "supabase" }, { name: "clerkClient", canonical: "auth.user() + db()", origin: "clerk" }, ]; /** Property-access hallucinations on the `auth` object. The SDK's * Proxy catches these at runtime, but the source scanner fires * earlier so the deploy fails before the bundle ships. */ const HALLUCINATED_AUTH_PROPERTIES = [ { name: "auth.session", canonical: "auth.user() then read .sessionId" }, { name: "auth.getSession", canonical: "auth.user()" }, { name: "auth.currentUser", canonical: "auth.user()" }, { name: "auth.currentSession", canonical: "auth.user()" }, { name: "auth.requireAuth", canonical: "auth.requireUser()" }, { name: "auth.middleware", canonical: "auth.csrfField() / @run402/astro middleware" }, { name: "auth.signIn", canonical: "POST /auth/sign-in or auth.sessions.createResponseFromIdentity({...})" }, { name: "auth.signOut", canonical: "auth.sessions.endResponse()" }, { name: "auth.signout", canonical: "auth.sessions.endResponse()" }, { name: "auth.logout", canonical: "auth.sessions.endResponse()" }, { name: "auth.login", canonical: "auth.sessions.createResponseFromIdentity({...})" }, { name: "auth.redirectToSignIn", canonical: "auth.requireUser() — platform handles redirect" }, { name: "auth.getUser", canonical: "auth.user()" }, { name: "auth.getToken", canonical: "auth.requireUser() then read .sessionId (tokens not exposed)" }, { name: "auth.protect", canonical: "auth.requireUser() / auth.requireRole(...)" }, ]; /** Unsupported legacy browser-session patterns. Bearer headers alone are valid * in SPA token clients and server integrations, so are not evidence of misuse. * These are caught at scan time because the SDK doesn't ship a * shim — the line just fails to execute. */ const BROWSER_ONLY_PATTERNS = [ { pattern: /localStorage\.getItem\(\s*['"]wl_session['"]\s*\)/g, name: "localStorage.wl_session", canonical: "auth.user() (browser sessions are HttpOnly cookies; no localStorage)", }, ]; /** Scan a single file's content. Returns the array of findings (zero * or more). Pure / no I/O — tests pass strings directly. */ export function scanFileContent(content, opts = {}) { const filePath = opts.filePath ?? "<inline>"; const findings = []; // 1) Hallucinated bare names — import { getSession } from ... OR // bare call sites `await getSession(...)`. for (const entry of HALLUCINATED_NAMES) { // Match in an `import { … }` statement OR a bare function-call site. // Negative-lookahead: don't fire on `auth.getSession` etc. (caught // separately by the auth-property scanner below). const importRegex = new RegExp(`import\\s*\\{[^}]*\\b${escapeRegex(entry.name)}\\b[^}]*\\}\\s*from\\s*['"]@run402/functions['"]`, "g"); const callRegex = new RegExp(`(?<![.\\w])${escapeRegex(entry.name)}\\s*\\(`, "g"); let match; while ((match = importRegex.exec(content)) !== null) { findings.push({ code: "R402_AUTH_UNKNOWN_EXPORT", severity: SCAN_SEVERITY.ERROR, file: filePath, line: lineNumberFor(content, match.index), attempted_name: entry.name, canonical_name: entry.canonical, import_line: 'import { auth } from "@run402/functions"', docs: "https://docs.run402.com/auth/sdk", message: `Import '${entry.name}' from @run402/functions is not a working export (origin: ${entry.origin}). Use ${entry.canonical}.`, }); } while ((match = callRegex.exec(content)) !== null) { findings.push({ code: "R402_AUTH_UNKNOWN_EXPORT", severity: SCAN_SEVERITY.ERROR, file: filePath, line: lineNumberFor(content, match.index), attempted_name: entry.name, canonical_name: entry.canonical, import_line: 'import { auth } from "@run402/functions"', docs: "https://docs.run402.com/auth/sdk", message: `Call to '${entry.name}()' will throw R402_AUTH_UNKNOWN_EXPORT at runtime. Use ${entry.canonical}.`, }); } } // 2) Hallucinated property access on `auth.*`. The SDK Proxy catches // these at runtime; we catch earlier at deploy. for (const entry of HALLUCINATED_AUTH_PROPERTIES) { const regex = new RegExp(`(?<![\\w.])${escapeRegex(entry.name)}\\b`, "g"); let match; while ((match = regex.exec(content)) !== null) { findings.push({ code: "R402_AUTH_UNKNOWN_EXPORT", severity: SCAN_SEVERITY.ERROR, file: filePath, line: lineNumberFor(content, match.index), attempted_name: entry.name, canonical_name: entry.canonical, import_line: 'import { auth } from "@run402/functions"', docs: "https://docs.run402.com/auth/sdk", message: `'${entry.name}' is not a valid auth.* helper. Use ${entry.canonical}.`, }); } } // 3) Browser-only / wrong-environment patterns. for (const entry of BROWSER_ONLY_PATTERNS) { let match; while ((match = entry.pattern.exec(content)) !== null) { findings.push({ code: "R402_AUTH_UNKNOWN_EXPORT", severity: SCAN_SEVERITY.ERROR, file: filePath, line: lineNumberFor(content, match.index), attempted_name: entry.name, canonical_name: entry.canonical, message: `'${entry.name}' is not supported. ${entry.canonical}.`, }); } } // 4) Prerendered pages calling auth.*. The Astro adapter throws // R402_AUTH_PRERENDERED at build time; this catches earlier. if (filePath.endsWith(".astro") || filePath.endsWith(".ts") || filePath.endsWith(".tsx")) { const declaresPrerender = /export\s+const\s+prerender\s*=\s*true/.test(content); if (declaresPrerender) { const authCallRegex = /\bauth\.(user|requireUser|requireRole|requireMembership|requireFresh|fetch|csrfToken|csrfField|sessions|identities)\b/g; let match; while ((match = authCallRegex.exec(content)) !== null) { findings.push({ code: "R402_AUTH_PRERENDERED", severity: SCAN_SEVERITY.ERROR, file: filePath, line: lineNumberFor(content, match.index), message: `auth.${match[1]} called from a prerendered page. Convert to SSR (\`export const prerender = false\`) or use a server island.`, docs: "https://docs.run402.com/auth/rendering-modes", }); } } } // 5) State-changing GET handlers. Heuristic: an Astro `export const GET` // or a `GET` Web-handler containing db-mutation patterns. const getHandlerRegex = /export\s+(?:async\s+)?(?:const\s+|function\s+)GET\s*[=(]/g; const mutationInGetRegex = /\b(?:db|adminDb)\s*\(\s*\)?[^)]*\)?\s*\.(?:insert|update|delete)\s*\(/g; const sqlMutationRegex = /\.sql\s*\(\s*['"`]\s*(?:UPDATE|INSERT|DELETE)\b/gi; if (getHandlerRegex.test(content)) { let match; while ((match = mutationInGetRegex.exec(content)) !== null) { findings.push({ code: "R402_AUTH_STATE_CHANGING_GET", severity: SCAN_SEVERITY.ERROR, file: filePath, line: lineNumberFor(content, match.index), message: "GET handler mutates state. Move the mutation to POST.", docs: "https://docs.run402.com/auth/hosted-ui#post-only", }); } while ((match = sqlMutationRegex.exec(content)) !== null) { findings.push({ code: "R402_AUTH_STATE_CHANGING_GET", severity: SCAN_SEVERITY.ERROR, file: filePath, line: lineNumberFor(content, match.index), message: "GET handler runs UPDATE/INSERT/DELETE SQL. Move the mutation to POST.", docs: "https://docs.run402.com/auth/hosted-ui#post-only", }); } } // 6) Direct mutation of internal.sessions.authz_version in consumer // migrations or SQL strings. The platform is the sole writer. const authzVersionRegex = /UPDATE\s+internal\.sessions\s+SET\s+authz_version\b/gi; let m; while ((m = authzVersionRegex.exec(content)) !== null) { findings.push({ code: "R402_AUTH_AUTHZ_VERSION_PROHIBITED", severity: SCAN_SEVERITY.ERROR, file: filePath, line: lineNumberFor(content, m.index), message: "Consumer code may not mutate internal.sessions.authz_version directly. Register your grants table in the authz manifest so the platform installs the bump trigger.", docs: "https://docs.run402.com/auth/db-actor-context#authz-version", }); } // 7) Redundant `.eq("user_id", user.id)` against RLS-bound tables. // db() propagates the actor via the run402.actor.* settings — PostgREST // enforces ownership in RLS. Filtering on user_id again is at best a // no-op and at worst a code smell that suggests the developer doesn't // trust RLS. Catch the most common shapes: // // .eq("user_id", user.id) // .eq('user_id', actor.id) // .eq("user_id", await auth.user()).id (rare; covered by the suffix) // // Opt-out via inline annotation comment on the preceding or same line: // // run402-allow-user-filter: <reason> // .eq("user_id", joinedRowOwner) // // Pattern is intentionally narrow (`user_id` literal column name + a // value expression matching `<ident>.id`) to keep the false-positive // rate low. Heuristic — RLS-binding is unknown at scan time; the rule // fires on the shape, and the author either annotates or fixes. const redundantFilterRegex = /\.eq\s*\(\s*['"]user_id['"]\s*,\s*([a-zA-Z_$][\w$]*)\.id\s*\)/g; const lines = content.split(/\r?\n/); const cumulativeOffsets = (() => { const offsets = [0]; for (let i = 0; i < lines.length; i++) { // +1 for the trailing newline we split on offsets.push(offsets[i] + lines[i].length + 1); } return offsets; })(); function lineIndexFor(charIndex) { // Binary search would be faster; lines are typically <2k. for (let i = 0; i < cumulativeOffsets.length - 1; i++) { if (charIndex < cumulativeOffsets[i + 1]) return i; } return cumulativeOffsets.length - 2; } let f; while ((f = redundantFilterRegex.exec(content)) !== null) { const lineIdx = lineIndexFor(f.index); const thisLine = lines[lineIdx] ?? ""; const prevLine = lineIdx > 0 ? (lines[lineIdx - 1] ?? "") : ""; const annotated = /\/\/\s*run402-allow-user-filter/i.test(thisLine) || /\/\/\s*run402-allow-user-filter/i.test(prevLine); if (annotated) continue; findings.push({ code: "R402_AUTH_REDUNDANT_USER_FILTER", severity: SCAN_SEVERITY.WARN, file: filePath, line: lineIdx + 1, message: `Redundant '.eq(\"user_id\", ${f[1]}.id)'. db() propagates the actor — PostgREST RLS enforces ownership server-side. ` + `If this is intentional (e.g., the table's RLS scopes on something else and you want to filter additionally), ` + `silence with: // run402-allow-user-filter: <reason>`, docs: "https://run402.com/errors/#R402_AUTH_REDUNDANT_USER_FILTER", }); } // 8) Tenant-assertion session-mint call without the declared capability. // `auth.sessions.createResponseFromTenantAssertion(...)` mints a browser // session from a tenant's vouching. It works ONLY in a function whose // deploy/apply spec declares `capabilities: ["auth.sessionMint"]` // (FunctionSpec.capabilities — sibling to `config`, since the platform // has no code-export metadata channel). Service-key presence is NOT // sufficient. Without the capability the gateway returns // R402_AUTH_UNTRUSTED_CONTEXT at runtime and mints no session. // // The pure file scanner can't see the per-function spec, so the caller // threads `opts.declaredCapabilities` (the union of capabilities declared // across run402.config.json function entries — see readDeclaredCapabilities). // We suppress the finding when "auth.sessionMint" is present anywhere in // that union. Global-union (not per-file) is a deliberate precision // trade-off: the file→function-entry mapping isn't reliable from source, // and the runtime gate catches the rare "function A declared it, function // B forgot" case. WARN severity (never block deploy): an inline/SDK spec // the doctor can't read might declare the capability. const declaredCaps = opts.declaredCapabilities instanceof Set ? opts.declaredCapabilities : new Set(Array.isArray(opts.declaredCapabilities) ? opts.declaredCapabilities : []); if (!declaredCaps.has("auth.sessionMint")) { const mintCallRegex = /\bcreateResponseFromTenantAssertion\s*\(/g; let mintMatch; while ((mintMatch = mintCallRegex.exec(content)) !== null) { findings.push({ code: "R402_DOCTOR_AUTH_SESSION_MINT_CAPABILITY_MISSING", severity: SCAN_SEVERITY.WARN, file: filePath, line: lineNumberFor(content, mintMatch.index), message: "createResponseFromTenantAssertion (tenant-assertion session mint) requires the " + '"auth.sessionMint" capability, which no function declares in run402.config.json. ' + "Without it the gateway returns R402_AUTH_UNTRUSTED_CONTEXT at runtime and mints no session.", fix: 'Add "capabilities": ["auth.sessionMint"] to this function\'s entry in run402.config.json ' + '(under functions.replace.<name>, a sibling to "config"). A service key is NOT sufficient.', docs: "https://docs.run402.com/auth/tenant-assertion#capability", }); } } // 9) The service key inside a user's request. A handler that reads the // current user (`auth.user()`, `auth.requireUser()`, …) and then calls // `adminDb()` answers that user's request with a key that bypasses row- // level security, so a bug in the handler's own checks exposes every row. // `db(req)` runs as the user and lets RLS decide. Advisory: some handlers // do need the service key after an explicit check (an admin action), and // say so with: // run402-allow-admin-db: <reason> if (/\bauth\s*\.\s*(?:user|requireUser|requireRole|requireMembership)\s*\(|\brequireUser\s*\(/.test(content)) { const adminDbRegex = /\badminDb\s*\(\s*\)/g; let adminMatch; while ((adminMatch = adminDbRegex.exec(content)) !== null) { const lineIdx = lineIndexFor(adminMatch.index); const thisLine = lines[lineIdx] ?? ""; const prevLine = lineIdx > 0 ? (lines[lineIdx - 1] ?? "") : ""; if (/\/\/\s*run402-allow-admin-db/i.test(thisLine) || /\/\/\s*run402-allow-admin-db/i.test(prevLine)) continue; findings.push({ code: "SERVICE_KEY_IN_USER_REQUEST", severity: SCAN_SEVERITY.WARN, file: filePath, line: lineIdx + 1, message: "adminDb() uses the project service key, which bypasses row-level security, in a file that reads the current user. " + "A request made for that user should run as that user: db(req) lets RLS decide which rows it sees.", fix: "Use db(req) for the user's reads and writes. If this handler needs the service key after its own check (an admin action), " + "silence with: // run402-allow-admin-db: <reason>", docs: "https://run402.com/errors/#SERVICE_KEY_IN_USER_REQUEST", }); } } return findings; } /** Recursively walk `srcDir` and scan every file with a relevant * extension. Returns the combined findings list, sorted by file + * line for stable output. */ export function scanSourceTree(srcDir, opts = {}) { const findings = []; // Capability picture for the tenant-assertion mint check (#8). Read from // run402.config.json unless the caller passed it explicitly (tests do). const declaredCapabilities = opts.declaredCapabilities ?? readDeclaredCapabilities(opts.cwd ?? srcDir); walk(srcDir, (filePath) => { if (!SCANNED_EXTENSIONS.has(extname(filePath)) || opts.excludeFiles?.has(filePath)) return; let content; try { content = readFileSync(filePath, "utf8"); } catch (err) { findings.push({ code: "R402_AUTH_SOURCE_SCAN_ERROR", severity: SCAN_SEVERITY.WARN, file: relative(opts.cwd ?? srcDir, filePath), message: `failed to read file: ${err instanceof Error ? err.message : String(err)}`, }); return; } findings.push(...scanFileContent(content, { filePath: relative(opts.cwd ?? srcDir, filePath), declaredCapabilities, })); }); findings.sort((a, b) => { if (a.file !== b.file) return a.file < b.file ? -1 : 1; return (a.line ?? 0) - (b.line ?? 0); }); return findings; } /** Scan an explicit list of on-disk file paths — no directory walk. * Used by `run402 deploy` for manifest/spec/stdin deploys, where * the artifact is exactly the set of files the manifest references, NOT * whatever happens to live under cwd/src. Files without a * scannable extension are ignored; unreadable files become a WARN * finding (never throw). Returns the combined findings list, sorted by * file + line for stable output, exactly like `scanSourceTree`. */ export function scanSourceFiles(filePaths, opts = {}) { const findings = []; const cwd = opts.cwd ?? process.cwd(); // Same capability picture as scanSourceTree (#8 mint check). const declaredCapabilities = opts.declaredCapabilities ?? readDeclaredCapabilities(cwd); for (const filePath of filePaths) { if (!SCANNED_EXTENSIONS.has(extname(filePath))) continue; let content; try { content = readFileSync(filePath, "utf8"); } catch (err) { findings.push({ code: "R402_AUTH_SOURCE_SCAN_ERROR", severity: SCAN_SEVERITY.WARN, file: relative(cwd, filePath), message: `failed to read file: ${err instanceof Error ? err.message : String(err)}`, }); continue; } findings.push(...scanFileContent(content, { filePath: relative(cwd, filePath), declaredCapabilities, })); } findings.sort((a, b) => { if (a.file !== b.file) return a.file < b.file ? -1 : 1; return (a.line ?? 0) - (b.line ?? 0); }); return findings; } function walk(dir, visitor) { let entries; try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return; } for (const entry of entries) { if (entry.isDirectory()) { if (SKIPPED_DIRECTORIES.has(entry.name)) continue; const child = join(dir, entry.name); if (existsSync(join(child, ".git")) || ["run402.json", "run402.deploy.json", "app.json"].some((name) => existsSync(join(child, name)))) continue; walk(child, visitor); } else if (entry.isFile()) { visitor(join(dir, entry.name)); } } } function lineNumberFor(content, index) { let line = 1; for (let i = 0; i < index; i++) { if (content.charCodeAt(i) === 10) line++; } return line; } function escapeRegex(s) { return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } /** Convenience for tests: synchronous, no FS access. */ export function _testOnly_hallucinatedNames() { return HALLUCINATED_NAMES.slice(); } export function _testOnly_authProperties() { return HALLUCINATED_AUTH_PROPERTIES.slice(); } /** Read the union of `capabilities` declared across all function entries in * `run402.config.json` (the apply spec). Used by the tenant-assertion mint * check (#8) to suppress the warning when "auth.sessionMint" is declared. * * Functions live under `functions.replace.<name>` / `functions.set.<name>` * with `capabilities?: string[]` as a sibling to `config`. Best-effort: * a missing or malformed config returns an empty set (the scanner then * warns, which is the safe default — the runtime gate is the hard * enforcement). Returns a `Set<string>`. */ export function readDeclaredCapabilities(cwd = process.cwd()) { const caps = new Set(); let parsed; try { parsed = JSON.parse(readFileSync(join(cwd, "run402.config.json"), "utf8")); } catch { return caps; // no config / unreadable / malformed → nothing declared } const fns = parsed?.functions; if (!fns || typeof fns !== "object") return caps; for (const bucket of ["replace", "set", "patch"]) { const entries = fns[bucket]; if (!entries || typeof entries !== "object") continue; for (const entry of Object.values(entries)) { const declared = entry?.capabilities; if (Array.isArray(declared)) { for (const cap of declared) if (typeof cap === "string") caps.add(cap); } } } return caps; } /** Resolve the project's src/ directory. Astro convention is `<root>/src`; * bare Node projects use `<root>/src` or `<root>`. We prefer `src/` if * it exists. */ export function resolveScanRoot(cwd = process.cwd()) { const srcDir = join(cwd, "src"); try { if (statSync(srcDir).isDirectory()) return srcDir; } catch { // Fall through. } return cwd; } /** The same selected-app scope gates up/apply and is reported by doctor. */ export function scanDeploymentSources(spec, appRoot, sourceRoot) { const scanRoot = sourceRoot ?? resolveScanRoot(appRoot); const generated = new Set(); for (const bucket of [spec.functions?.replace, spec.functions?.patch?.set]) { for (const fn of Object.values(bucket ?? {})) { if (fn.capabilities?.includes("astro.ssr.v1") && fn.source && typeof fn.source === "object" && "path" in fn.source) generated.add(fn.source.path); } } const references = collectLocalFileReferences(spec).filter((ref) => ref.expects === "file" && !generated.has(ref.path)); // Generated SSR bundles must not be re-scanned as consumer source. Their // original source remains in the selected application's source tree. const files = new Set(); if (sourceRoot || scanRoot !== appRoot) walk(scanRoot, (path) => { if (SCANNED_EXTENSIONS.has(extname(path)) && !generated.has(path)) files.add(path); }); for (const reference of references) if (SCANNED_EXTENSIONS.has(extname(reference.path))) files.add(reference.path); const findings = scanSourceFiles([...files], { cwd: appRoot }); return { app_root: appRoot, scan_root: scanRoot, checked_files: files.size, explicit_references: references.length, findings, errors: findings.filter((finding) => finding.severity === SCAN_SEVERITY.ERROR) }; } //# sourceMappingURL=source-scan.js.map