feat: add workspace isolation and administration UI
CI / macos-latest / Node 20 (push) Has been cancelled
CI / ubuntu-latest / Node 20 (push) Has been cancelled
CI / windows-latest / Node 20 (push) Has been cancelled

This commit is contained in:
m4
2026-07-19 12:17:18 +08:00
parent b9ed81745e
commit fe982f7f95
150 changed files with 20503 additions and 1868 deletions
+32 -1
View File
@@ -4,7 +4,7 @@
// .next/static/ (NOT inside standalone — must be copied)
// We copy everything into dist/ so the published package ships one folder that
// `node dist/server.js` can run.
import { cp, rm, readdir } from "fs/promises";
import { cp, mkdir, readdir, rm, writeFile } from "fs/promises";
import { existsSync } from "fs";
const STANDALONE = ".next/standalone";
@@ -26,6 +26,37 @@ if (existsSync(PUBLIC)) {
await cp(PUBLIC, `${OUT}/public`, { recursive: true });
}
// Turbopack represents serverExternalPackages with generated package aliases
// under .next/node_modules. The standalone tree uses absolute symlinks for
// those aliases, which npm omits from tarballs. Replace better-sqlite3 aliases
// with portable shims and let npm install the real native dependency for the
// target platform from the package root.
const NEXT_NODE_MODULES = `${OUT}/.next/node_modules`;
if (existsSync(NEXT_NODE_MODULES)) {
for (const entry of await readdir(NEXT_NODE_MODULES)) {
if (!entry.startsWith("better-sqlite3-")) continue;
const alias = `${NEXT_NODE_MODULES}/${entry}`;
await rm(alias, { recursive: true, force: true });
await mkdir(alias, { recursive: true });
await writeFile(
`${alias}/package.json`,
`${JSON.stringify(
{ name: entry, private: true, main: "index.js" },
null,
2
)}\n`
);
await writeFile(
`${alias}/index.js`,
'module.exports = require("better-sqlite3");\n'
);
}
}
await rm(`${OUT}/node_modules/better-sqlite3`, {
recursive: true,
force: true,
});
// Next copies the whole project root into the standalone bundle. Prune it down
// to just the runtime essentials (drops src/, configs, and local notes so
// they never get published).
+32
View File
@@ -0,0 +1,32 @@
import { createRequire } from "node:module";
import { existsSync } from "node:fs";
import { lstat, readdir } from "node:fs/promises";
import path from "node:path";
const aliasesRoot = path.resolve("dist/.next/node_modules");
const aliases = (await readdir(aliasesRoot)).filter((entry) =>
entry.startsWith("better-sqlite3-")
);
if (aliases.length === 0) {
throw new Error("standalone build contains no better-sqlite3 external alias");
}
if (existsSync(path.resolve("dist/node_modules/better-sqlite3"))) {
throw new Error("standalone build contains a build-platform SQLite binary");
}
const require = createRequire(import.meta.url);
for (const alias of aliases) {
const aliasPath = path.join(aliasesRoot, alias);
if ((await lstat(aliasPath)).isSymbolicLink()) {
throw new Error(`${alias} is an npm-incompatible symbolic link`);
}
const Database = require(aliasPath);
const database = new Database(":memory:");
try {
database.prepare("SELECT 1 value").get();
} finally {
database.close();
}
}
console.log(`Verified ${aliases.length} portable better-sqlite3 alias(es).`);
+48
View File
@@ -0,0 +1,48 @@
import { readFile, readdir } from "node:fs/promises";
import path from "node:path";
const root = path.resolve("src");
const forbiddenSdk = /\b(?:new\s+Client|useClient|ClientProvider|useStream)\b/;
const forbiddenBrowserConfig =
/\b(?:deploymentUrl|langsmithApiKey|NEXT_PUBLIC_LANGSMITH_API_KEY)\b/;
const workspaceRoutes = [
"app/api/workspace/route.ts",
"app/api/workspace/file/route.ts",
"app/api/workspace/upload/route.ts",
"app/api/workspace/download/route.ts",
];
async function sourceFiles(directory) {
const entries = await readdir(directory, { withFileTypes: true });
const files = [];
for (const entry of entries) {
const absolute = path.join(directory, entry.name);
if (entry.isDirectory()) files.push(...(await sourceFiles(absolute)));
else if (/\.(?:ts|tsx)$/.test(entry.name)) files.push(absolute);
}
return files;
}
const violations = [];
for (const file of await sourceFiles(root)) {
const relative = path.relative(root, file).replaceAll(path.sep, "/");
// Server-side BFF clients are intentionally the only LangGraph SDK clients.
if (relative.startsWith("lib/server/") || relative.startsWith("app/api/")) continue;
const source = await readFile(file, "utf8");
if (forbiddenSdk.test(source)) violations.push(`${relative} (LangGraph SDK)`);
if (forbiddenBrowserConfig.test(source)) {
violations.push(`${relative} (deployment credential or URL)`);
}
}
if (violations.length) {
throw new Error(`Browser BFF boundary violation: ${violations.join(", ")}`);
}
for (const route of workspaceRoutes) {
const source = await readFile(path.join(root, route), "utf8");
if (!source.includes("resolveConversationWorkspace")) {
throw new Error(`Workspace route is not scope-resolved: ${route}`);
}
}
console.log("Verified browser BFF boundary and conversation-scoped workspace routes.");