UNPKG

auth

Version:
1,224 lines (1,217 loc) 242 kB
#!/usr/bin/env node import { Command } from "commander"; import { exec, execSync, spawn } from "node:child_process"; import * as fs$2 from "node:fs"; import fs, { existsSync, readFileSync, readdirSync } from "node:fs"; import * as os$1 from "node:os"; import os from "node:os"; import * as path$1 from "node:path"; import path, { join } from "node:path"; import chalk from "chalk"; import prompts from "prompts"; import yoctoSpinner from "yocto-spinner"; import fs$1 from "node:fs/promises"; import { createTelemetry, getTelemetryAuthConfig } from "@better-auth/telemetry"; import { getAdapter } from "better-auth/db/adapter"; import * as z from "zod"; import { capitalizeFirstLetter, toSnakeCase } from "@better-auth/core/utils/string"; import { initGetFieldName, initGetModelName } from "better-auth/adapters"; import { getAuthTables } from "better-auth/db"; import prettier, { format } from "prettier"; import { getMigrations } from "better-auth/db/migration"; import { produceSchema } from "@mrleebo/prisma-ast"; import Crypto from "node:crypto"; import babelPresetReact from "@babel/preset-react"; import babelPresetTypeScript from "@babel/preset-typescript"; import { BetterAuthError } from "@better-auth/core/error"; import { loadConfig } from "c12"; import { createPathsMatcher, getTsconfig, parseTsconfig } from "get-tsconfig"; import open from "open"; import { env } from "@better-auth/core/env"; import { log } from "@clack/prompts"; import { base64 } from "@better-auth/utils/base64"; import * as semver from "semver"; import "dotenv/config"; //#region src/commands/ai.ts const PROTOCOL_URL = "https://agent-auth-protocol.com"; const AGENT_CLI_PKG = "@auth/agent-cli"; const AGENT_PLUGIN_PKG = "@better-auth/agent-auth"; const DEFAULT_REGISTRY = "https://agent-auth.directory"; const SKILLS_REPO = "better-auth/agent-auth"; function cancelled() { console.log(chalk.yellow("\n✋ Setup cancelled.")); process.exit(0); } function check(value) { if (value === void 0 || value === null) cancelled(); return value; } async function aiAction() { console.log("\n" + [ ` ██ ████`, ` ████ ██ ${chalk.bold("Agent Auth")} ${chalk.dim("Setup")}`, ` ██ ████ ${chalk.gray("AI agent authentication & capability-based authorization.")}` ].join("\n")); console.log(); const { setup } = await prompts({ type: "select", name: "setup", message: "What would you like to do?", choices: [{ title: "Integrate Agent Auth client", value: "client", description: "MCP server, CLI, or SDK for your agents" }, { title: "Create an Agent Auth server", value: "server", description: "expose capabilities from your service to AI agents" }] }); check(setup); if (setup === "client") await setupClient(); else await setupServerSelection(); } async function setupClient() { const { method } = await prompts({ type: "select", name: "method", message: "How do you want to integrate?", choices: [{ title: "MCP Server", value: "mcp", description: "for AI tools — Claude, Cursor, Windsurf, etc." }, { title: "CLI", value: "cli", description: "command-line tool for agent workflows" }] }); check(method); if (method === "mcp") await setupMcp(); else await setupCli(); } async function setupServerSelection() { const { implementation } = await prompts({ type: "select", name: "implementation", message: "Choose an implementation", choices: [{ title: "Better Auth + Agent Auth", value: "better-auth", description: "TypeScript" }] }); check(implementation); await setupServer(); } async function setupMcp() { const { tool } = await prompts({ type: "select", name: "tool", message: "Which AI tool?", choices: [ { title: "Cursor", value: "cursor" }, { title: "Claude Code", value: "claude-code" }, { title: "Claude Desktop", value: "claude-desktop" }, { title: "Windsurf", value: "windsurf" }, { title: "VS Code / Copilot", value: "vscode" }, { title: "Open Code", value: "opencode" }, { title: "Other", value: "other" } ] }); check(tool); let scope = "global"; if (tool === "cursor" || tool === "vscode") { const { s } = await prompts({ type: "select", name: "s", message: "Where should it be configured?", choices: [{ title: "This project", value: "project", description: tool === "cursor" ? ".cursor/mcp.json" : ".vscode/mcp.json" }, { title: "Global (all projects)", value: "global", description: tool === "cursor" ? "~/.cursor/mcp.json" : "user settings" }] }); check(s); scope = s; } const { registryUrl } = await prompts({ type: "text", name: "registryUrl", message: "Registry URL", initial: DEFAULT_REGISTRY }); const mcpArgs = buildMcpArgs(registryUrl?.trim() || DEFAULT_REGISTRY); if (tool === "claude-code") await setupClaudeCode(mcpArgs); else if (tool === "opencode") await setupOpenCode(mcpArgs); else if (tool === "other") showJsonConfig({ command: "npx", args: mcpArgs }); else await writeMcpConfigInteractive(tool, scope, mcpArgs); await offerSkillInstall("agent-auth-mcp"); showNextSteps([`${chalk.cyan("Docs")} ${PROTOCOL_URL}/docs/integrate-client`, `${chalk.cyan("GitHub")} https://github.com/better-auth/agent-auth`]); console.log(chalk.green("\n✔ ") + chalk.bold("Done! ") + "Restart your AI tool to connect.\n"); } async function setupClaudeCode(args) { const { scope } = await prompts({ type: "select", name: "scope", message: "Where should it be configured?", choices: [{ title: "This project", value: "project", description: "--scope project" }, { title: "Global (all projects)", value: "user", description: "--scope user" }] }); check(scope); const cmd = [ "claude", "mcp", "add", "agent-auth", "--scope", scope, "--", "npx", ...args ].join(" "); console.log(chalk.bold.white("\nRun this command:")); console.log(chalk.cyan(` ${cmd}\n`)); const { run } = await prompts({ type: "confirm", name: "run", message: "Run it now?", initial: true }); if (run) { const s = yoctoSpinner({ text: "Adding MCP server to Claude Code…", color: "white" }); s.start(); try { execSync(cmd, { stdio: "pipe" }); s.success("Added to Claude Code."); } catch { s.stop(); console.log(chalk.yellow("⚠ Could not run the command automatically.")); console.log(chalk.gray(" Run the command above manually.")); } } } async function setupOpenCode(args) { const configPath = path$1.join(process.cwd(), "opencode.json"); const display = "opencode.json"; const openCodeEntry = { type: "stdio", command: "npx", args, enabled: true }; const { write } = await prompts({ type: "confirm", name: "write", message: `Write config to ${chalk.cyan(display)}?`, initial: true }); if (write) { writeOpenCodeConfig(configPath, openCodeEntry); console.log(chalk.green(`\n✓ Written to ${display}`)); } else { const json = JSON.stringify({ $schema: "https://opencode.ai/config.json", mcp: { "agent-auth": openCodeEntry } }, null, 2); console.log(chalk.bold.white("\nAdd to your opencode.json:\n")); console.log(json.split("\n").map((line) => chalk.cyan(` ${line}`)).join("\n")); console.log(); } } function writeOpenCodeConfig(configPath, entry) { let config = {}; if (fs$2.existsSync(configPath)) try { config = JSON.parse(fs$2.readFileSync(configPath, "utf-8")); } catch {} const mcp = config.mcp ?? {}; mcp["agent-auth"] = entry; config.$schema = "https://opencode.ai/config.json"; config.mcp = mcp; fs$2.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n"); } async function writeMcpConfigInteractive(tool, scope, args) { const entry = { command: "npx", args }; const configPath = getMcpConfigPath(tool, scope); if (!configPath) { showJsonConfig(entry); return; } const display = displayPath(configPath, scope); const { write } = await prompts({ type: "confirm", name: "write", message: `Write config to ${chalk.cyan(display)}?`, initial: true }); if (write) { writeMcpConfig(configPath, entry); console.log(chalk.green(`\n✓ Written to ${display}`)); } else showJsonConfig(entry); } async function setupCli() { const { installCli } = await prompts({ type: "confirm", name: "installCli", message: `Install ${chalk.cyan(AGENT_CLI_PKG)} globally?`, initial: true }); if (installCli) { const s = yoctoSpinner({ text: `Installing ${AGENT_CLI_PKG}…`, color: "white" }); s.start(); try { execSync(`npm install -g ${AGENT_CLI_PKG}`, { stdio: "pipe" }); s.success(`${AGENT_CLI_PKG} installed globally.`); } catch { s.stop(); console.log(chalk.yellow("⚠ Could not install automatically. Run manually:")); console.log(chalk.cyan(` npm install -g ${AGENT_CLI_PKG}\n`)); } } else console.log(chalk.dim(`\n To install later: npm install -g ${AGENT_CLI_PKG}\n`)); await offerSkillInstall("agent-auth-cli"); console.log(chalk.bold.white("\nUsage:")); console.log(chalk.gray(" # Discover a provider")); console.log(chalk.cyan(" auth-agent discover https://api.example.com")); console.log(chalk.gray("\n # Search the registry for providers")); console.log(chalk.cyan(` auth-agent search "send email"`)); console.log(chalk.gray("\n # Connect an agent with capabilities")); console.log(chalk.cyan(" auth-agent connect --provider <url> --capabilities <cap1> <cap2>")); console.log(chalk.gray("\n # Execute a capability")); console.log(chalk.cyan(` auth-agent execute <agent-id> <capability> --args '{"key":"value"}'`)); console.log(chalk.gray("\n # Run as MCP server")); console.log(chalk.cyan(` auth-agent mcp`)); showNextSteps([`${chalk.cyan("Docs")} ${PROTOCOL_URL}/docs/integrate-client`, `${chalk.cyan("GitHub")} https://github.com/better-auth/agent-auth`]); console.log(chalk.green("\n✔ ") + chalk.bold("Ready. ") + "Run auth-agent --help to see all commands.\n"); } async function setupServer() { const { source } = await prompts({ type: "select", name: "source", message: "How do you want to define capabilities?", choices: [ { title: "Default", value: "manual", description: "define capabilities in code" }, { title: "From an OpenAPI spec", value: "openapi", description: "derive capabilities from an OpenAPI document" }, { title: "From an MCP server", value: "mcp", description: "proxy an existing MCP server's tools" } ] }); check(source); const { name } = await prompts({ type: "text", name: "name", message: "What's your service called?", validate: (v) => v?.trim() ? true : "Name is required." }); check(name); const { description } = await prompts({ type: "text", name: "description", message: `Short description ${chalk.dim("(press Enter to skip)")}` }); const desc = description?.trim() || void 0; let sourceUrl; if (source === "openapi") { const { url } = await prompts({ type: "text", name: "url", message: `OpenAPI spec URL ${chalk.dim("(e.g. https://api.example.com/openapi.json)")}`, validate: (v) => v?.trim() ? true : "URL is required." }); check(url); sourceUrl = url.trim(); } else if (source === "mcp") { const { url } = await prompts({ type: "text", name: "url", message: `MCP server URL ${chalk.dim("(e.g. https://api.example.com/mcp)")}`, validate: (v) => v?.trim() ? true : "URL is required." }); check(url); sourceUrl = url.trim(); } const code = generateServerCode(name.trim(), desc, source, sourceUrl); const { write } = await prompts({ type: "confirm", name: "write", message: "Generate an auth config file?", initial: true }); if (write) { const { filePath } = await prompts({ type: "text", name: "filePath", message: "File path", initial: "lib/auth.ts" }); const target = filePath?.trim() || "lib/auth.ts"; if (fs$2.existsSync(target)) { const { overwrite } = await prompts({ type: "confirm", name: "overwrite", message: `${chalk.yellow(target)} already exists. Overwrite?`, initial: false }); if (!overwrite) { showCodeBlock(code, "auth config"); showServerOutro(); return; } } const dir = path$1.dirname(target); if (dir && dir !== "." && !fs$2.existsSync(dir)) fs$2.mkdirSync(dir, { recursive: true }); fs$2.writeFileSync(target, code); console.log(chalk.green(`\n✓ Created ${target}`)); } else showCodeBlock(code, "auth config"); showServerOutro(); } function generateServerCode(name, description, source, sourceUrl) { const descLine = description ? `\n\t\t\tproviderDescription: ${JSON.stringify(description)},` : ""; if (source === "openapi" && sourceUrl) return `import { betterAuth } from "better-auth"; import { agentAuth } from "${AGENT_PLUGIN_PKG}"; import { createFromOpenAPI } from "${AGENT_PLUGIN_PKG}/openapi"; const spec = await fetch(${JSON.stringify(sourceUrl)}).then(r => r.json()); const openapi = createFromOpenAPI(spec, { \tbaseUrl: ${JSON.stringify(sourceUrl.replace(/\/openapi\.json$|\/openapi\.yaml$|\/swagger\.json$|\/docs\/openapi$/, ""))}, }); export const auth = betterAuth({ \tplugins: [ \t\tagentAuth({ \t\t\tproviderName: ${JSON.stringify(name)},${descLine} \t\t\t...openapi, \t\t}), \t], }); `; if (source === "mcp" && sourceUrl) return `import { betterAuth } from "better-auth"; import { agentAuth } from "${AGENT_PLUGIN_PKG}"; export const auth = betterAuth({ \tplugins: [ \t\tagentAuth({ \t\t\tproviderName: ${JSON.stringify(name)},${descLine} \t\t\tmcpServer: ${JSON.stringify(sourceUrl)}, \t\t}), \t], }); `; return `import { betterAuth } from "better-auth"; import { agentAuth } from "${AGENT_PLUGIN_PKG}"; export const auth = betterAuth({ \tplugins: [ \t\tagentAuth({ \t\t\tproviderName: ${JSON.stringify(name)},${descLine} \t\t\tcapabilities: [ \t\t\t\t{ \t\t\t\t\tname: "example", \t\t\t\t\tdescription: "An example capability — replace with your own", \t\t\t\t\tinput: { \t\t\t\t\t\ttype: "object", \t\t\t\t\t\tproperties: { \t\t\t\t\t\t\tmessage: { type: "string", description: "Input message" }, \t\t\t\t\t\t}, \t\t\t\t\t}, \t\t\t\t}, \t\t\t], \t\t\tasync onExecute({ capability, arguments: args }) { \t\t\t\tswitch (capability) { \t\t\t\t\tcase "example": \t\t\t\t\t\treturn { message: \`Hello from \${(args as Record<string, string>).message}\` }; \t\t\t\t\tdefault: \t\t\t\t\t\tthrow new Error(\`Unknown capability: \${capability}\`); \t\t\t\t} \t\t\t}, \t\t}), \t], }); `; } function showServerOutro() { console.log(chalk.bold.white("\nNext steps:\n")); console.log(chalk.white(" 1. Install dependencies:")); console.log(chalk.cyan(` npm install better-auth ${AGENT_PLUGIN_PKG}\n`)); console.log(chalk.white(" 2. Configure your database:")); console.log(chalk.gray(" Better Auth needs a database to store agents, hosts, and grants.")); console.log(chalk.cyan(" https://www.better-auth.com/docs/concepts/database\n")); console.log(chalk.white(" 3. Run database migrations:")); console.log(chalk.cyan(" npx auth migrate\n")); console.log(chalk.white(" 4. Expose the discovery endpoint at your app root:")); console.log(chalk.gray(" GET /.well-known/agent-configuration")); console.log(chalk.gray(" → return auth.api.getAgentConfiguration({ headers })\n")); console.log(` ${chalk.cyan("Docs")} ${PROTOCOL_URL}/docs/build-server`); console.log(` ${chalk.cyan("GitHub")} https://github.com/better-auth/agent-auth`); console.log(chalk.green("\n✔ ") + chalk.bold("Server scaffolded. ") + "Follow the steps above to finish setup.\n"); } async function offerSkillInstall(skillName) { const { installSkill } = await prompts({ type: "confirm", name: "installSkill", message: `Install the ${chalk.cyan(skillName)} skill for your coding agents?`, initial: true }); if (!installSkill) return; const cmd = `npx -y skills add ${SKILLS_REPO} --skill ${skillName}`; const s = yoctoSpinner({ text: `Installing ${skillName} skill…`, color: "white" }); s.start(); try { execSync(cmd, { stdio: "pipe" }); s.success(`${skillName} skill installed.`); } catch { s.stop(); console.log(chalk.yellow("⚠ Could not install automatically. Run manually:")); console.log(chalk.cyan(` ${cmd}\n`)); } } function buildMcpArgs(registry) { const args = [ "-y", AGENT_CLI_PKG, "mcp" ]; if (registry && registry !== DEFAULT_REGISTRY) args.push("--registry-url", registry); return args; } function getMcpConfigPath(tool, scope) { const home = os$1.homedir(); switch (tool) { case "cursor": return scope === "global" ? path$1.join(home, ".cursor", "mcp.json") : path$1.join(process.cwd(), ".cursor", "mcp.json"); case "claude-desktop": if (process.platform === "win32") return path$1.join(process.env.APPDATA || home, "Claude", "claude_desktop_config.json"); if (process.platform === "darwin") return path$1.join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json"); return path$1.join(home, ".config", "Claude", "claude_desktop_config.json"); case "windsurf": return path$1.join(home, ".codeium", "windsurf", "mcp_config.json"); case "vscode": return scope === "global" ? null : path$1.join(process.cwd(), ".vscode", "mcp.json"); default: return null; } } function writeMcpConfig(configPath, entry) { let config = {}; if (fs$2.existsSync(configPath)) try { config = JSON.parse(fs$2.readFileSync(configPath, "utf-8")); } catch {} const servers = config.mcpServers ?? {}; servers["agent-auth"] = entry; config.mcpServers = servers; const dir = path$1.dirname(configPath); if (!fs$2.existsSync(dir)) fs$2.mkdirSync(dir, { recursive: true }); fs$2.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n"); } function displayPath(filePath, scope) { if (scope === "project") return path$1.relative(process.cwd(), filePath) || filePath; return filePath.replace(os$1.homedir(), "~"); } function showJsonConfig(entry) { const json = JSON.stringify({ mcpServers: { "agent-auth": entry } }, null, 2); console.log(chalk.bold.white("\nAdd to your MCP configuration:\n")); console.log(json.split("\n").map((line) => chalk.cyan(` ${line}`)).join("\n")); console.log(); } function showCodeBlock(code, title) { console.log(chalk.bold.white(`\n${title}:\n`)); console.log(code.split("\n").map((line) => chalk.dim(` ${line}`)).join("\n")); } function showNextSteps(lines) { console.log(chalk.bold.white("\nLearn more:\n")); for (const line of lines) console.log(` ${line}`); } const ai = new Command("ai").description("Interactive setup for Agent Auth — AI agent authentication").action(aiAction); //#endregion //#region src/generators/drizzle.ts function convertToSnakeCase(str, camelCase) { return camelCase ? str : toSnakeCase(str); } const generateDrizzleSchema = async ({ options, file, adapter }) => { const tables = getAuthTables(options); const filePath = file || "./auth-schema.ts"; const databaseType = adapter.options?.provider; if (!databaseType) throw new Error(`Database provider type is undefined during Drizzle schema generation. Please define a \`provider\` in the Drizzle adapter config. Read more at https://better-auth.com/docs/adapters/drizzle`); const fileExist = existsSync(filePath); let code = generateImport({ databaseType, tables, options }); const getModelName = initGetModelName({ schema: tables, usePlural: adapter.options?.adapterConfig?.usePlural }); const getFieldName = initGetFieldName({ schema: tables, usePlural: adapter.options?.adapterConfig?.usePlural }); for (const tableKey in tables) { const table = tables[tableKey]; if (table.disableMigrations) continue; const modelName = getModelName(tableKey); const fields = table.fields; function getType(name, field) { if (!databaseType) throw new Error(`Database provider type is undefined during Drizzle schema generation. Please define a \`provider\` in the Drizzle adapter config. Read more at https://better-auth.com/docs/adapters/drizzle`); name = convertToSnakeCase(name, adapter.options?.camelCase); if (field.references?.field === "id") { const useNumberId = options.advanced?.database?.generateId === "serial"; const useUUIDs = options.advanced?.database?.generateId === "uuid"; if (useNumberId) if (databaseType === "pg") return `integer('${name}')`; else if (databaseType === "mysql") return `int('${name}')`; else return `integer('${name}')`; if (useUUIDs && databaseType === "pg") return `uuid('${name}')`; if (field.references.field) { if (databaseType === "mysql") return `varchar('${name}', { length: 36 })`; } return `text('${name}')`; } const type = field.type; if (typeof type !== "string") if (Array.isArray(type) && type.every((x) => typeof x === "string")) return { sqlite: `text({ enum: [${type.map((x) => `'${x}'`).join(", ")}] })`, pg: `text('${name}', { enum: [${type.map((x) => `'${x}'`).join(", ")}] })`, mysql: `mysqlEnum([${type.map((x) => `'${x}'`).join(", ")}])` }[databaseType]; else throw new TypeError(`Invalid field type for field ${name} in model ${modelName}`); const dbTypeMap = { string: { sqlite: `text('${name}')`, pg: `text('${name}')`, mysql: field.unique ? `varchar('${name}', { length: 255 })` : field.references ? `varchar('${name}', { length: 36 })` : field.sortable ? `varchar('${name}', { length: 255 })` : field.index ? `varchar('${name}', { length: 255 })` : `text('${name}')` }, boolean: { sqlite: `integer('${name}', { mode: 'boolean' })`, pg: `boolean('${name}')`, mysql: `boolean('${name}')` }, number: { sqlite: `integer('${name}')`, pg: field.bigint ? `bigint('${name}', { mode: 'number' })` : `integer('${name}')`, mysql: field.bigint ? `bigint('${name}', { mode: 'number' })` : `int('${name}')` }, date: { sqlite: `integer('${name}', { mode: 'timestamp_ms' })`, pg: `timestamp('${name}')`, mysql: `timestamp('${name}', { fsp: 3 })` }, "number[]": { sqlite: `text('${name}', { mode: "json" })`, pg: field.bigint ? `bigint('${name}', { mode: 'number' }).array()` : `integer('${name}').array()`, mysql: `text('${name}', { mode: 'json' })` }, "string[]": { sqlite: `text('${name}', { mode: "json" })`, pg: `text('${name}').array()`, mysql: `text('${name}', { mode: "json" })` }, json: { sqlite: `text('${name}', { mode: "json" })`, pg: `jsonb('${name}')`, mysql: `json('${name}', { mode: "json" })` } }[type]; if (!dbTypeMap) throw new Error(`Unsupported field type '${field.type}' for field '${name}'.`); return dbTypeMap[databaseType]; } let id = ""; const useNumberId = options.advanced?.database?.generateId === "serial"; if (options.advanced?.database?.generateId === "uuid" && databaseType === "pg") id = `uuid("id").default(sql\`pg_catalog.gen_random_uuid()\`).primaryKey()`; else if (useNumberId) if (databaseType === "pg") id = `integer("id").generatedByDefaultAsIdentity().primaryKey()`; else if (databaseType === "sqlite") id = `integer("id", { mode: "number" }).primaryKey({ autoIncrement: true })`; else id = `int("id").autoincrement().primaryKey()`; else if (databaseType === "mysql") id = `varchar('id', { length: 36 }).primaryKey()`; else if (databaseType === "pg") id = `text('id').primaryKey()`; else id = `text('id').primaryKey()`; const indexes = []; const assignIndexes = (indexes) => { if (!indexes.length) return ""; const code = [`, (table) => [`]; for (const index of indexes) code.push(` ${index.type}("${index.name}").on(table.${index.on}),`); code.push(`]`); return code.join("\n"); }; const schema = `export const ${modelName} = ${databaseType}Table("${convertToSnakeCase(modelName, adapter.options?.camelCase)}", { id: ${id}, ${Object.keys(fields).map((field) => { const attr = fields[field]; const fieldName = attr.fieldName || field; let type = getType(fieldName, attr); if (attr.index && !attr.unique) indexes.push({ type: "index", name: `${modelName}_${fieldName}_idx`, on: fieldName }); if (attr.defaultValue !== null && typeof attr.defaultValue !== "undefined") if (typeof attr.defaultValue === "function") { if (attr.type === "date" && attr.defaultValue.toString().includes("new Date()")) if (databaseType === "sqlite") type += `.default(sql\`(cast(unixepoch('subsecond') * 1000 as integer))\`)`; else type += `.defaultNow()`; } else if (typeof attr.defaultValue === "string") type += `.default(${JSON.stringify(attr.defaultValue)})`; else if (Array.isArray(attr.defaultValue)) { const elements = attr.defaultValue.map((value) => JSON.stringify(value)).join(", "); type += `.default([${elements}])`; } else if (typeof attr.defaultValue === "object" && attr.defaultValue !== null) type += `.default(${JSON.stringify(attr.defaultValue)})`; else type += `.default(${attr.defaultValue})`; if (attr.onUpdate && attr.type === "date") { if (typeof attr.onUpdate === "function") type += `.$onUpdate(${attr.onUpdate})`; } return `${fieldName}: ${type}${attr.required !== false ? ".notNull()" : ""}${attr.unique ? ".unique()" : ""}${attr.references ? `.references(()=> ${getModelName(attr.references.model)}.${getFieldName({ model: attr.references.model, field: attr.references.field })}, { onDelete: '${attr.references.onDelete || "cascade"}' })` : ""}`; }).join(",\n ")} }${assignIndexes(indexes)});`; code += `\n${schema}\n`; } let relationsString = ""; for (const tableKey in tables) { const table = tables[tableKey]; if (table.disableMigrations) continue; const modelName = getModelName(tableKey); const oneRelations = []; const manyRelations = []; const foreignFields = Object.entries(table.fields).filter(([_, field]) => field.references); const foreignFieldCounts = /* @__PURE__ */ new Map(); for (const [_, field] of foreignFields) { const referencedModel = getModelName(field.references.model); foreignFieldCounts.set(referencedModel, (foreignFieldCounts.get(referencedModel) ?? 0) + 1); } const usedOneRelationKeys = /* @__PURE__ */ new Set(); for (const [fieldName, field] of foreignFields) { const referencedModel = field.references.model; const hasMultipleRelations = (foreignFieldCounts.get(getModelName(referencedModel)) ?? 0) > 1; let relationKey = hasMultipleRelations ? fieldName.replace(/Id$/, "") : getModelName(referencedModel); if (usedOneRelationKeys.has(relationKey)) relationKey = fieldName; if (usedOneRelationKeys.has(relationKey)) { let suffix = 2; while (usedOneRelationKeys.has(`${relationKey}_${suffix}`)) suffix++; relationKey = `${relationKey}_${suffix}`; } usedOneRelationKeys.add(relationKey); const fieldRef = `${getModelName(tableKey)}.${getFieldName({ model: tableKey, field: fieldName })}`; const referenceRef = `${getModelName(referencedModel)}.${getFieldName({ model: referencedModel, field: field.references.field || "id" })}`; oneRelations.push({ key: relationKey, model: getModelName(referencedModel), type: "one", relationName: hasMultipleRelations ? `${getModelName(tableKey)}_${fieldName}` : void 0, reference: { field: fieldRef, references: referenceRef } }); } const otherModels = Object.entries(tables).filter(([modelName]) => modelName !== tableKey); for (const [modelName, otherTable] of otherModels) { const foreignKeysPointingHere = Object.entries(otherTable.fields).filter(([_, field]) => field.references?.model === tableKey || field.references?.model === getModelName(tableKey)); if (foreignKeysPointingHere.length === 0) continue; for (const [fieldName, field] of foreignKeysPointingHere) { const relationType = field.unique ? "one" : "many"; let relationKey = getModelName(modelName); if (!adapter.options?.adapterConfig?.usePlural && relationType === "many") relationKey = `${relationKey}s`; const hasMultipleRelations = foreignKeysPointingHere.length > 1; if (hasMultipleRelations) relationKey = `${relationKey}By${fieldName.charAt(0).toUpperCase()}${fieldName.slice(1)}`; manyRelations.push({ key: relationKey, model: getModelName(modelName), type: relationType, relationName: hasMultipleRelations ? `${getModelName(modelName)}_${fieldName}` : void 0 }); } } const hasForwardOne = oneRelations.length > 0; const hasReverseOne = manyRelations.some((relation) => relation.type === "one"); const hasReverseMany = manyRelations.some((relation) => relation.type === "many"); const hasOne = hasForwardOne || hasReverseOne; const hasMany = hasReverseMany; const renderOneRelation = (relation) => relation.reference ? ` ${relation.key}: one(${relation.model}, { fields: [${relation.reference.field}], references: [${relation.reference.references}], ${relation.relationName ? `relationName: "${relation.relationName}",` : ""} })` : ""; const renderReverseRelation = ({ key, model, type, relationName }) => { return ` ${key}: ${type === "one" ? "one" : "many"}(${model}${relationName ? `, { relationName: "${relationName}" }` : ""})`; }; if (hasOne || hasMany) { const helpers = [hasOne ? "one" : null, hasMany ? "many" : null].filter(Boolean).join(", "); const relationEntries = [...oneRelations.map(renderOneRelation).filter((x) => x !== ""), ...manyRelations.map(renderReverseRelation)].join(",\n "); const tableRelation = `export const ${modelName}Relations = relations(${getModelName(table.modelName)}, ({ ${helpers} }) => ({ ${relationEntries} }))`; relationsString += `\n${tableRelation}\n`; } } code += `\n${relationsString}`; return { code: await prettier.format(code, { parser: "typescript" }), fileName: filePath, overwrite: fileExist }; }; function generateImport({ databaseType, tables, options }) { const rootImports = ["relations"]; const coreImports = []; let hasBigint = false; let hasJson = false; for (const table of Object.values(tables)) { for (const field of Object.values(table.fields)) { if (field.bigint) hasBigint = true; if (field.type === "json") hasJson = true; } if (hasJson && hasBigint) break; } const useNumberId = options.advanced?.database?.generateId === "serial"; const useUUIDs = options.advanced?.database?.generateId === "uuid"; coreImports.push(`${databaseType}Table`); coreImports.push(databaseType === "mysql" ? "varchar, text" : databaseType === "pg" ? "text" : "text"); coreImports.push(hasBigint ? databaseType !== "sqlite" ? "bigint" : "" : ""); coreImports.push(databaseType !== "sqlite" ? "timestamp, boolean" : ""); if (databaseType === "mysql") { const hasNonBigintNumber = Object.values(tables).some((table) => Object.values(table.fields).some((field) => (field.type === "number" || field.type === "number[]") && !field.bigint)); if (useNumberId || hasNonBigintNumber) coreImports.push("int"); if (Object.values(tables).some((table) => Object.values(table.fields).some((field) => typeof field.type !== "string" && Array.isArray(field.type) && field.type.every((x) => typeof x === "string")))) coreImports.push("mysqlEnum"); } else if (databaseType === "pg") { if (useUUIDs) rootImports.push("sql"); const hasNonBigintNumber = Object.values(tables).some((table) => Object.values(table.fields).some((field) => (field.type === "number" || field.type === "number[]") && !field.bigint)); const hasFkToId = Object.values(tables).some((table) => Object.values(table.fields).some((field) => field.references?.field === "id")); if (hasNonBigintNumber || options.advanced?.database?.generateId === "serial" && hasFkToId) coreImports.push("integer"); } else coreImports.push("integer"); if (databaseType === "pg" && useUUIDs) coreImports.push("uuid"); if (hasJson) { if (databaseType === "pg") coreImports.push("jsonb"); if (databaseType === "mysql") coreImports.push("json"); } if (databaseType === "sqlite" && Object.values(tables).some((table) => Object.values(table.fields).some((field) => field.type === "date" && field.defaultValue && typeof field.defaultValue === "function" && field.defaultValue.toString().includes("new Date()")))) rootImports.push("sql"); if (Object.values(tables).some((table) => Object.values(table.fields).some((field) => field.index && !field.unique))) coreImports.push("index"); return `${rootImports.length > 0 ? `import { ${rootImports.join(", ")} } from "drizzle-orm";\n` : ""}import { ${coreImports.map((x) => x.trim()).filter((x) => x !== "").join(", ")} } from "drizzle-orm/${databaseType}-core";\n`; } //#endregion //#region src/generators/kysely.ts const generateKyselySchema = async ({ options, file }) => { const { compileMigrations } = await getMigrations(options); const migrations = await compileMigrations(); return { code: migrations.trim() === ";" ? "" : migrations, fileName: file || `./better-auth_migrations/${(/* @__PURE__ */ new Date()).toISOString().replace(/:/g, "-")}.sql` }; }; //#endregion //#region src/utils/helper.ts async function tryCatch(promise) { try { return { data: await promise, error: null }; } catch (error) { return { data: null, error }; } } const generateSecretHash = () => { return Crypto.randomBytes(32).toString("hex"); }; const spawnCommand = (cmd, cwd = process.cwd()) => new Promise((resolve, reject) => { const child = spawn(cmd, { cwd, stdio: "inherit", shell: true }); child.on("close", (code, signal) => { if (code !== 0 && code !== null) reject(/* @__PURE__ */ new Error(`Exited with code ${code}`)); else if (signal) reject(/* @__PURE__ */ new Error(`Killed with signal ${signal}`)); else resolve(); }); child.on("error", reject); }); //#endregion //#region src/utils/get-package-info.ts function getPackageInfo(cwd) { const packageJsonPath = cwd ? path.join(cwd, "package.json") : path.join("package.json"); return JSON.parse(readFileSync(packageJsonPath, "utf-8")); } function getPrismaVersion(cwd) { try { const packageInfo = getPackageInfo(cwd); const prismaVersion = packageInfo.dependencies?.prisma || packageInfo.devDependencies?.prisma || packageInfo.dependencies?.["@prisma/client"] || packageInfo.devDependencies?.["@prisma/client"]; if (!prismaVersion) return null; const match = prismaVersion.match(/(\d+)/); return match ? parseInt(match[1], 10) : null; } catch { return null; } } /** * Checks if a package has a specific dependency. * * @param packageJson The package.json object * @param dependency The dependency to check for * @returns true if the package has the dependency */ function hasDependency(packageJson, dependency) { let hasDependency = false; if (packageJson.dependencies?.[dependency] || packageJson.devDependencies?.[dependency] || packageJson.peerDependencies?.[dependency] || packageJson.optionalDependencies?.[dependency]) hasDependency = true; return hasDependency; } /** * Checks if a directory is a monorepo root by looking for common monorepo indicators. * * @param dir Directory to check * @returns true if the directory appears to be a monorepo root */ async function isMonorepoRoot(dir) { const { data: files } = await tryCatch(fs$1.readdir(dir, "utf-8")); if (!files) return false; if (files.includes("pnpm-workspace.yaml")) return true; if (files.includes("package.json")) { const packageJsonPath = path.join(dir, "package.json"); const { data } = await tryCatch(fs$1.readFile(packageJsonPath, "utf-8")); if (data) try { const packageJson = JSON.parse(data); if (packageJson.workspaces && (Array.isArray(packageJson.workspaces) || typeof packageJson.workspaces === "object")) return true; } catch {} } return [ "lerna.json", "turbo.json", "nx.json", "rush.json" ].some((indicator) => files.includes(indicator)); } /** * Finds the monorepo root by walking up the directory tree. * * @param startDir Starting directory * @returns Path to monorepo root, or null if not found */ async function findMonorepoRoot(startDir) { let currentDir = path.resolve(startDir); const root = path.parse(currentDir).root; while (currentDir !== root) { if (await isMonorepoRoot(currentDir)) return currentDir; const parentDir = path.dirname(currentDir); if (parentDir === currentDir) break; currentDir = parentDir; } return null; } //#endregion //#region src/generators/prisma.ts const generatePrismaSchema = async ({ adapter, options, file }) => { const provider = adapter.options?.provider || "postgresql"; const tables = getAuthTables(options); const filePath = file || "./prisma/schema.prisma"; const resolvedFilePath = path.isAbsolute(filePath) ? filePath : path.join(process.cwd(), filePath); const schemaPrismaExist = existsSync(resolvedFilePath); const getModelName = initGetModelName({ schema: getAuthTables(options), usePlural: adapter.options?.adapterConfig?.usePlural }); const getFieldName = initGetFieldName({ schema: getAuthTables(options), usePlural: false }); let schemaPrisma = ""; if (schemaPrismaExist) schemaPrisma = await fs$1.readFile(resolvedFilePath, "utf-8"); else schemaPrisma = getNewPrisma(provider, process.cwd()); const prismaVersion = getPrismaVersion(process.cwd()); if (prismaVersion && prismaVersion >= 7 && schemaPrismaExist) schemaPrisma = produceSchema(schemaPrisma, (builder) => { const generator = builder.findByType("generator", { name: "client" }); if (generator && generator.properties) { const providerProp = generator.properties.find((prop) => prop.type === "assignment" && prop.key === "provider"); if (providerProp && providerProp.value === "\"prisma-client-js\"") providerProp.value = "\"prisma-client\""; } const datasource = builder.findByType("datasource", { name: "db" }); if (datasource && datasource.properties) { const urlIndex = datasource.properties.findIndex((prop) => prop.type === "assignment" && prop.key === "url"); if (urlIndex !== -1) datasource.properties.splice(urlIndex, 1); } }); const manyToManyRelations = /* @__PURE__ */ new Map(); for (const table in tables) { const fields = tables[table]?.fields; for (const field in fields) { const attr = fields[field]; if (attr.references) { const referencedOriginalModel = attr.references.model; const referencedModelNameCap = capitalizeFirstLetter(getModelName(tables[referencedOriginalModel]?.modelName || referencedOriginalModel)); if (!manyToManyRelations.has(referencedModelNameCap)) manyToManyRelations.set(referencedModelNameCap, /* @__PURE__ */ new Set()); const currentModelNameCap = capitalizeFirstLetter(getModelName(tables[table]?.modelName || table)); manyToManyRelations.get(referencedModelNameCap).add(currentModelNameCap); } } } const indexedFields = /* @__PURE__ */ new Map(); for (const table in tables) { const fields = tables[table]?.fields; const modelName = capitalizeFirstLetter(getModelName(tables[table]?.modelName || table)); indexedFields.set(modelName, []); for (const field in fields) { const attr = fields[field]; if (attr.index && !attr.unique) { const fieldName = attr.fieldName || field; indexedFields.get(modelName).push(fieldName); } } } const schema = produceSchema(schemaPrisma, (builder) => { for (const table in tables) { if (tables[table]?.disableMigrations) continue; const originalTableName = table; const customModelName = tables[table]?.modelName || table; const modelName = capitalizeFirstLetter(getModelName(customModelName)); const fields = tables[table]?.fields; function getType({ isBigint, isOptional, type }) { if (type === "string") return isOptional ? "String?" : "String"; if (type === "number" && isBigint) return isOptional ? "BigInt?" : "BigInt"; if (type === "number") return isOptional ? "Int?" : "Int"; if (type === "boolean") return isOptional ? "Boolean?" : "Boolean"; if (type === "date") return isOptional ? "DateTime?" : "DateTime"; if (type === "json") { if (provider === "sqlite" || provider === "mysql") return isOptional ? "String?" : "String"; return isOptional ? "Json?" : "Json"; } if (type === "string[]") { if (provider === "sqlite" || provider === "mysql") return isOptional ? "String?" : "String"; return "String[]"; } if (type === "number[]") { if (provider === "sqlite" || provider === "mysql") return "String"; return "Int[]"; } } function getFieldTypeParts(type) { const isArray = type.endsWith("[]"); const typeWithoutArray = isArray ? type.slice(0, -2) : type; const isOptional = typeWithoutArray.endsWith("?"); return { fieldType: isOptional ? typeWithoutArray.slice(0, -1) : typeWithoutArray, isArray, isOptional }; } const prismaModel = builder.findByType("model", { name: modelName }); if (!prismaModel) if (provider === "mongodb") builder.model(modelName).field("id", "String").attribute("id").attribute(`map("_id")`); else { const useNumberId = options.advanced?.database?.generateId === "serial"; const useUUIDs = options.advanced?.database?.generateId === "uuid"; if (useNumberId) builder.model(modelName).field("id", "Int").attribute("id").attribute("default(autoincrement())"); else if (useUUIDs && provider === "postgresql") builder.model(modelName).field("id", "String").attribute("id").attribute("default(dbgenerated(\"pg_catalog.gen_random_uuid()\"))").attribute("db.Uuid"); else builder.model(modelName).field("id", "String").attribute("id"); } for (const field in fields) { const attr = fields[field]; const fieldName = attr.fieldName || field; const useUUIDs = options.advanced?.database?.generateId === "uuid"; const useNumberId = options.advanced?.database?.generateId === "serial"; const fieldType = field === "id" && useNumberId ? getType({ isBigint: false, isOptional: false, type: "number" }) : getType({ isBigint: attr?.bigint || false, isOptional: attr?.required === false, type: attr.references?.field === "id" ? useNumberId ? "number" : "string" : attr.type }); if (prismaModel) { const isAlreadyExist = builder.findByType("field", { name: fieldName, within: prismaModel.properties }); if (isAlreadyExist) { if (fieldType && typeof isAlreadyExist.fieldType === "string") { const fieldTypeParts = getFieldTypeParts(fieldType); const existingFieldTypeParts = getFieldTypeParts(isAlreadyExist.fieldType); if ((existingFieldTypeParts.fieldType === "Int" || existingFieldTypeParts.fieldType === "BigInt") && (fieldTypeParts.fieldType === "Int" || fieldTypeParts.fieldType === "BigInt")) { isAlreadyExist.fieldType = fieldTypeParts.fieldType; isAlreadyExist.optional = fieldTypeParts.isOptional || void 0; isAlreadyExist.array = fieldTypeParts.isArray || void 0; } } continue; } } if (!fieldType) throw new Error(`Unsupported Prisma field type for model "${modelName}", field "${fieldName}"${attr.type ? ` (source type: "${attr.type}")` : ""}.`); const fieldBuilder = builder.model(modelName).field(fieldName, fieldType); if (field === "id") { fieldBuilder.attribute("id"); if (provider === "mongodb") fieldBuilder.attribute(`map("_id")`); } if (attr.unique) builder.model(modelName).blockAttribute(`unique([${fieldName}])`); if (attr.defaultValue !== void 0) { if (Array.isArray(attr.defaultValue)) { if (attr.type === "json") { if (Object.prototype.toString.call(attr.defaultValue[0]) === "[object Object]") { fieldBuilder.attribute(`default("${JSON.stringify(attr.defaultValue).replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}")`); continue; } const jsonArray = []; for (const value of attr.defaultValue) jsonArray.push(value); fieldBuilder.attribute(`default("${JSON.stringify(jsonArray).replace(/"/g, "\\\"")}")`); continue; } if (attr.defaultValue.length === 0) { fieldBuilder.attribute(`default([])`); continue; } else if (typeof attr.defaultValue[0] === "string" && attr.type === "string[]") { const valueArray = []; for (const value of attr.defaultValue) valueArray.push(JSON.stringify(value)); fieldBuilder.attribute(`default([${valueArray}])`); } else if (typeof attr.defaultValue[0] === "number") { const valueArray = []; for (const value of attr.defaultValue) valueArray.push(`${value}`); fieldBuilder.attribute(`default([${valueArray}])`); } } else if (typeof attr.defaultValue === "object" && !Array.isArray(attr.defaultValue) && attr.defaultValue !== null) { if (Object.entries(attr.defaultValue).length === 0) { fieldBuilder.attribute(`default("{}")`); continue; } fieldBuilder.attribute(`default("${JSON.stringify(attr.defaultValue).replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}")`); } if (field === "createdAt") fieldBuilder.attribute("default(now())"); else if (typeof attr.defaultValue === "string" && provider !== "mysql") fieldBuilder.attribute(`default("${attr.defaultValue}")`); else if (typeof attr.defaultValue === "boolean" || typeof attr.defaultValue === "number") fieldBuilder.attribute(`default(${attr.defaultValue})`); else if (typeof attr.defaultValue === "function") {} } if (field === "updatedAt" && attr.onUpdate) fieldBuilder.attribute("updatedAt"); else if (attr.onUpdate) {} if (attr.references) { if (useUUIDs && provider === "postgresql" && attr.references?.field === "id") builder.model(modelName).field(fieldName).attribute(`db.Uuid`); const referencedOriginalModelName = getModelName(attr.references.model); const referencedCustomModelName = tables[referencedOriginalModelName]?.modelName || referencedOriginalModelName; let action = "Cascade"; if (attr.references.onDelete === "no action") action = "NoAction"; else if (attr.references.onDelete === "set null") action = "SetNull"; else if (attr.references.onDelete === "set default") action = "SetDefault"; else if (attr.references.onDelete === "restrict") action = "Restrict"; const relationField = `relation(fields: [${getFieldName({ model: originalTableName, field: fieldName })}], references: [${getFieldName({ model: attr.references.model, field: attr.references.field })}], onDelete: ${action})`; builder.model(modelName).field(referencedCustomModelName.toLowerCase(), `${capitalizeFirstLetter(referencedCustomModelName)}${attr.required === false ? "?" : ""}`).attribute(relationField); } if (!attr.unique && !attr.references && provider === "mysql" && attr.type === "string") builder.model(modelName).field(fieldName).attribute("db.Text"); } if (manyToManyRelations.has(modelName)) for (const relatedModel of manyToManyRelations.get(modelName)) { const relatedTableName = Object.keys(tables).find((key) => capitalizeFirstLetter(tables[key]?.modelName || key) === relatedModel); const relatedFields = relatedTableName ? tables[relatedTableName]?.fields : {}; const [_fieldKey, fkFieldAttr] = Object.entries(relatedFields || {}).find(([_fieldName, fieldAttr]) => fieldAttr.references && getModelName(fieldAttr.references.model) === getModelName(originalTableName)) || []; const isUnique = fkFieldAttr?.unique === true; const fieldName = isUnique || adapter.options?.usePlural === true ? `${relatedModel.toLowerCase()}` : `${relatedModel.toLowerCase()}s`; if (!builder.findByType("field", { name: fieldName, within: prismaModel?.properties })) builder.model(modelName).field(fieldName, `${relatedModel}${isUnique ? "?" : "[]"}`); } const indexedFieldsForModel = indexedFields.get(modelName); if (indexedFieldsForModel && indexedFieldsForModel.length > 0) for (const fieldName of indexedFieldsForModel) { if (prismaModel) { if (prismaModel.properties.some((v) => v.type === "attribute" && v.name === "index" && JSON.stringify(v.args[0]?.value).includes(fieldName))) continue; } const field = Object.entries(fields).find(([key, attr]) => (attr.fieldName || key) === fieldName)?.[1]; let indexField = fieldName; if (provider === "mysql" && field && field.type === "string") { const useNumberId = options.advanced?.database?.generateId === "serial"; const useUUIDs = options.advanced?.database?.generateId === "uuid"; if (field.references?.field === "id" && (useNumberId || useUUIDs)) indexField = `${fieldName}`; else indexField = `${fieldName}(length: 191)`; } builder.model(modelName).blockAttribute(`index([${indexField}])`); } const hasAttribute = builder.findByType("attribute", { name: "map", within: prismaModel?.properties }); const hasChanged = customModelName !== originalTableName; if (!hasAttribute) builder.model(modelName).blockAttribute("map", `${getModelName(hasChanged ? customModelName : originalTableName)}`); } }); const schemaChanged = schema.trim() !== schemaPrisma.trim(); return { code: schemaChanged ? schema : "", fileName: filePath, overwrite: schemaPrismaExist && schemaChanged }; }; const getNewPrisma = (provider, cwd) => { const prismaVersion = getPrismaVersion(cwd); const isV7 = prismaVersion && prismaVersion >= 7; const clientProvider = isV7 ? "prisma-client" : "prisma-client-js"; if (isV7) return `generator client { provider = "${clientProvider}" } datasource db { provider = "${provider}" }`; return `generator client { provider = "${clientProvider}" } datasource db { provider = "${provider}" url = ${provider === "sqlite" ? `"file:./dev.db"` : `env("DATABASE_URL")`} }`; }; //#endregion //#region src/generators/index.ts const adapters = { prisma: generatePrismaSchema, drizzle: gen