import { parse } from "@babel/parser"; import fs from "node:fs/promises"; import path from "node:path"; /** Enforce the authored bundled-code contract, not a security sandbox. */ export function bundlePolicyErrors(source: string, filename: string): string[] { const ast = parse(source, { sourceType: "module", plugins: ["typescript", "jsx"] }); const errors: string[] = []; const visit = (value: unknown) => { if (Array.isArray(value)) { value.forEach(visit); return; } if (!value || typeof value !== "object") return; const n = value as Record; if (typeof n.type !== "string") return; const fail = (message: string) => errors.push(`${filename}:${n.loc?.start.line ?? 1}: ${message}`); if (n.type === "MetaProperty" && n.meta.name === "import") fail("Bundled package code cannot use import.meta; import packaged assets statically"); if (n.type === "Identifier" && ["__dirname", "__filename"].includes(n.name)) fail("Bundled package code cannot depend on module filesystem locations"); if (["CallExpression", "NewExpression"].includes(n.type)) { if (n.callee?.type === "Identifier" && ["eval", "Function"].includes(n.callee.name)) fail("Dynamic code generation is unsupported in bundled package code"); if ( (n.callee?.type === "Import" || (n.callee?.type === "Identifier" && n.callee.name === "require")) && (n.arguments.length !== 1 || n.arguments[0].type !== "StringLiteral") ) fail("Module imports must have a static string specifier"); } if (n.type === "ImportExpression" && n.source.type !== "StringLiteral") fail("Module imports must have a static string specifier"); Object.values(n).forEach(visit); }; visit(ast); return errors; } export async function checkBundleSources(directory: string): Promise { const errors: string[] = []; async function walk(current: string) { for (const entry of await fs.readdir(current, { withFileTypes: true })) { if (["gen", "node_modules"].includes(entry.name)) continue; const file = path.join(current, entry.name); if (entry.isSymbolicLink()) throw new Error(`Authored source symlinks are unsupported: ${file}`); if (entry.isDirectory()) await walk(file); else if (/\.[cm]?[jt]sx?$/.test(entry.name) && !entry.name.endsWith(".d.ts")) errors.push(...bundlePolicyErrors(await fs.readFile(file, "utf8"), file)); } } await walk(directory); if (errors.length) throw new Error(errors.join("\n")); }