@better-auth/cli 1.4.0-beta.1 â 1.4.0-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +2823 -0
- package/package.json +16 -12
- package/dist/index.mjs +0 -3482
package/dist/index.js
ADDED
|
@@ -0,0 +1,2823 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { parse } from "dotenv";
|
|
4
|
+
import semver from "semver";
|
|
5
|
+
import prettier, { format } from "prettier";
|
|
6
|
+
import * as z from "zod/v4";
|
|
7
|
+
import * as fs$2 from "fs";
|
|
8
|
+
import fs, { existsSync, readFileSync } from "fs";
|
|
9
|
+
import * as path$1 from "path";
|
|
10
|
+
import path from "path";
|
|
11
|
+
import fs$1 from "fs/promises";
|
|
12
|
+
import chalk from "chalk";
|
|
13
|
+
import { cancel, confirm, intro, isCancel, log, multiselect, outro, select, spinner, text } from "@clack/prompts";
|
|
14
|
+
import { exec, execSync } from "child_process";
|
|
15
|
+
import { BetterAuthError, capitalizeFirstLetter, createTelemetry, getTelemetryAuthConfig, logger } from "better-auth";
|
|
16
|
+
import Crypto from "crypto";
|
|
17
|
+
import yoctoSpinner from "yocto-spinner";
|
|
18
|
+
import prompts from "prompts";
|
|
19
|
+
import { getAdapter, getAuthTables, getMigrations } from "better-auth/db";
|
|
20
|
+
import { loadConfig } from "c12";
|
|
21
|
+
import babelPresetTypeScript from "@babel/preset-typescript";
|
|
22
|
+
import babelPresetReact from "@babel/preset-react";
|
|
23
|
+
import { produceSchema } from "@mrleebo/prisma-ast";
|
|
24
|
+
import { createAuthClient } from "better-auth/client";
|
|
25
|
+
import { deviceAuthorizationClient } from "better-auth/client/plugins";
|
|
26
|
+
import open from "open";
|
|
27
|
+
import * as os$1 from "os";
|
|
28
|
+
import os from "os";
|
|
29
|
+
import { base64 } from "@better-auth/utils/base64";
|
|
30
|
+
import "dotenv/config";
|
|
31
|
+
|
|
32
|
+
//#region src/utils/get-package-info.ts
|
|
33
|
+
function getPackageInfo(cwd) {
|
|
34
|
+
const packageJsonPath = cwd ? path.join(cwd, "package.json") : path.join("package.json");
|
|
35
|
+
return JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
//#endregion
|
|
39
|
+
//#region src/utils/install-dependencies.ts
|
|
40
|
+
function installDependencies({ dependencies, packageManager, cwd }) {
|
|
41
|
+
let installCommand;
|
|
42
|
+
switch (packageManager) {
|
|
43
|
+
case "npm":
|
|
44
|
+
installCommand = "npm install --force";
|
|
45
|
+
break;
|
|
46
|
+
case "pnpm":
|
|
47
|
+
installCommand = "pnpm install";
|
|
48
|
+
break;
|
|
49
|
+
case "bun":
|
|
50
|
+
installCommand = "bun install";
|
|
51
|
+
break;
|
|
52
|
+
case "yarn":
|
|
53
|
+
installCommand = "yarn install";
|
|
54
|
+
break;
|
|
55
|
+
default: throw new Error("Invalid package manager");
|
|
56
|
+
}
|
|
57
|
+
const command = `${installCommand} ${dependencies.join(" ")}`;
|
|
58
|
+
return new Promise((resolve, reject) => {
|
|
59
|
+
exec(command, { cwd }, (error, stdout, stderr) => {
|
|
60
|
+
if (error) {
|
|
61
|
+
reject(new Error(stderr));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
resolve(true);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
//#endregion
|
|
70
|
+
//#region src/utils/check-package-managers.ts
|
|
71
|
+
function checkCommand(command) {
|
|
72
|
+
return new Promise((resolve) => {
|
|
73
|
+
exec(`${command} --version`, (error) => {
|
|
74
|
+
if (error) resolve(false);
|
|
75
|
+
else resolve(true);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
async function checkPackageManagers() {
|
|
80
|
+
return {
|
|
81
|
+
hasPnpm: await checkCommand("pnpm"),
|
|
82
|
+
hasBun: await checkCommand("bun")
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
//#endregion
|
|
87
|
+
//#region src/utils/format-ms.ts
|
|
88
|
+
/**
|
|
89
|
+
* Only supports up to seconds.
|
|
90
|
+
*/
|
|
91
|
+
function formatMilliseconds(ms) {
|
|
92
|
+
if (ms < 0) throw new Error("Milliseconds cannot be negative");
|
|
93
|
+
if (ms < 1e3) return `${ms}ms`;
|
|
94
|
+
return `${Math.floor(ms / 1e3)}s ${ms % 1e3}ms`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
//#endregion
|
|
98
|
+
//#region src/commands/secret.ts
|
|
99
|
+
const generateSecret = new Command("secret").action(() => {
|
|
100
|
+
const secret = generateSecretHash();
|
|
101
|
+
logger.info(`\nAdd the following to your .env file:
|
|
102
|
+
${chalk.gray("# Auth Secret") + chalk.green(`\nBETTER_AUTH_SECRET=${secret}`)}`);
|
|
103
|
+
});
|
|
104
|
+
const generateSecretHash = () => {
|
|
105
|
+
return Crypto.randomBytes(32).toString("hex");
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
//#endregion
|
|
109
|
+
//#region src/generators/auth-config.ts
|
|
110
|
+
async function generateAuthConfig({ format: format$1, current_user_config, spinner: spinner$1, plugins, database }) {
|
|
111
|
+
const common_indexes = {
|
|
112
|
+
START_OF_PLUGINS: { START_OF_PLUGINS: {
|
|
113
|
+
type: "regex",
|
|
114
|
+
regex: /betterAuth\([\w\W]*plugins:[\W]*\[()/m,
|
|
115
|
+
getIndex: ({ matchIndex, match }) => {
|
|
116
|
+
return matchIndex + match[0].length;
|
|
117
|
+
}
|
|
118
|
+
} }.START_OF_PLUGINS,
|
|
119
|
+
END_OF_PLUGINS: {
|
|
120
|
+
type: "manual",
|
|
121
|
+
getIndex: ({ content, additionalFields }) => {
|
|
122
|
+
return findClosingBracket(content, additionalFields.start_of_plugins, "[", "]");
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
START_OF_BETTERAUTH: {
|
|
126
|
+
type: "regex",
|
|
127
|
+
regex: /betterAuth\({()/m,
|
|
128
|
+
getIndex: ({ matchIndex }) => {
|
|
129
|
+
return matchIndex + 12;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
const config_generation = {
|
|
134
|
+
add_plugin: async (opts) => {
|
|
135
|
+
let start_of_plugins = getGroupInfo(opts.config, common_indexes.START_OF_PLUGINS, {});
|
|
136
|
+
if (!start_of_plugins) throw new Error("Couldn't find start of your plugins array in your auth config file.");
|
|
137
|
+
let end_of_plugins = getGroupInfo(opts.config, common_indexes.END_OF_PLUGINS, { start_of_plugins: start_of_plugins.index });
|
|
138
|
+
if (!end_of_plugins) throw new Error("Couldn't find end of your plugins array in your auth config file.");
|
|
139
|
+
let new_content;
|
|
140
|
+
if (opts.direction_in_plugins_array === "prepend") new_content = insertContent({
|
|
141
|
+
line: start_of_plugins.line,
|
|
142
|
+
character: start_of_plugins.character,
|
|
143
|
+
content: opts.config,
|
|
144
|
+
insert_content: `${opts.pluginFunctionName}(${opts.pluginContents}),`
|
|
145
|
+
});
|
|
146
|
+
else {
|
|
147
|
+
const pluginArrayContent = opts.config.slice(start_of_plugins.index, end_of_plugins.index).trim();
|
|
148
|
+
const isPluginArrayEmpty = pluginArrayContent === "";
|
|
149
|
+
const isPluginArrayEndsWithComma = pluginArrayContent.endsWith(",");
|
|
150
|
+
const needsComma = !isPluginArrayEmpty && !isPluginArrayEndsWithComma;
|
|
151
|
+
new_content = insertContent({
|
|
152
|
+
line: end_of_plugins.line,
|
|
153
|
+
character: end_of_plugins.character,
|
|
154
|
+
content: opts.config,
|
|
155
|
+
insert_content: `${needsComma ? "," : ""}${opts.pluginFunctionName}(${opts.pluginContents})`
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
new_content = await format$1(new_content);
|
|
160
|
+
} catch (error) {
|
|
161
|
+
console.error(error);
|
|
162
|
+
throw new Error(`Failed to generate new auth config during plugin addition phase.`);
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
code: new_content,
|
|
166
|
+
dependencies: [],
|
|
167
|
+
envs: []
|
|
168
|
+
};
|
|
169
|
+
},
|
|
170
|
+
add_import: async (opts) => {
|
|
171
|
+
let importString = "";
|
|
172
|
+
for (const import_ of opts.imports) if (Array.isArray(import_.variables)) importString += `import { ${import_.variables.map((x) => `${x.asType ? "type " : ""}${x.name}${x.as ? ` as ${x.as}` : ""}`).join(", ")} } from "${import_.path}";\n`;
|
|
173
|
+
else importString += `import ${import_.variables.asType ? "type " : ""}${import_.variables.name}${import_.variables.as ? ` as ${import_.variables.as}` : ""} from "${import_.path}";\n`;
|
|
174
|
+
try {
|
|
175
|
+
return {
|
|
176
|
+
code: await format$1(importString + opts.config),
|
|
177
|
+
dependencies: [],
|
|
178
|
+
envs: []
|
|
179
|
+
};
|
|
180
|
+
} catch (error) {
|
|
181
|
+
console.error(error);
|
|
182
|
+
throw new Error(`Failed to generate new auth config during import addition phase.`);
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
add_database: async (opts) => {
|
|
186
|
+
const required_envs = [];
|
|
187
|
+
const required_deps = [];
|
|
188
|
+
let database_code_str = "";
|
|
189
|
+
async function add_db({ db_code, dependencies, envs, imports, code_before_betterAuth }) {
|
|
190
|
+
if (code_before_betterAuth) {
|
|
191
|
+
let start_of_betterauth$1 = getGroupInfo(opts.config, common_indexes.START_OF_BETTERAUTH, {});
|
|
192
|
+
if (!start_of_betterauth$1) throw new Error("Couldn't find start of betterAuth() function.");
|
|
193
|
+
opts.config = insertContent({
|
|
194
|
+
line: start_of_betterauth$1.line - 1,
|
|
195
|
+
character: 0,
|
|
196
|
+
content: opts.config,
|
|
197
|
+
insert_content: `\n${code_before_betterAuth}\n`
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
const code_gen = await config_generation.add_import({
|
|
201
|
+
config: opts.config,
|
|
202
|
+
imports
|
|
203
|
+
});
|
|
204
|
+
opts.config = code_gen.code;
|
|
205
|
+
database_code_str = db_code;
|
|
206
|
+
required_envs.push(...envs, ...code_gen.envs);
|
|
207
|
+
required_deps.push(...dependencies, ...code_gen.dependencies);
|
|
208
|
+
}
|
|
209
|
+
if (opts.database === "sqlite") await add_db({
|
|
210
|
+
db_code: `new Database(process.env.DATABASE_URL || "database.sqlite")`,
|
|
211
|
+
dependencies: ["better-sqlite3"],
|
|
212
|
+
envs: ["DATABASE_URL"],
|
|
213
|
+
imports: [{
|
|
214
|
+
path: "better-sqlite3",
|
|
215
|
+
variables: {
|
|
216
|
+
asType: false,
|
|
217
|
+
name: "Database"
|
|
218
|
+
}
|
|
219
|
+
}]
|
|
220
|
+
});
|
|
221
|
+
else if (opts.database === "postgres") await add_db({
|
|
222
|
+
db_code: `new Pool({\nconnectionString: process.env.DATABASE_URL || "postgresql://postgres:password@localhost:5432/database"\n})`,
|
|
223
|
+
dependencies: ["pg"],
|
|
224
|
+
envs: ["DATABASE_URL"],
|
|
225
|
+
imports: [{
|
|
226
|
+
path: "pg",
|
|
227
|
+
variables: [{
|
|
228
|
+
asType: false,
|
|
229
|
+
name: "Pool"
|
|
230
|
+
}]
|
|
231
|
+
}]
|
|
232
|
+
});
|
|
233
|
+
else if (opts.database === "mysql") await add_db({
|
|
234
|
+
db_code: `createPool(process.env.DATABASE_URL!)`,
|
|
235
|
+
dependencies: ["mysql2"],
|
|
236
|
+
envs: ["DATABASE_URL"],
|
|
237
|
+
imports: [{
|
|
238
|
+
path: "mysql2/promise",
|
|
239
|
+
variables: [{
|
|
240
|
+
asType: false,
|
|
241
|
+
name: "createPool"
|
|
242
|
+
}]
|
|
243
|
+
}]
|
|
244
|
+
});
|
|
245
|
+
else if (opts.database === "mssql") await add_db({
|
|
246
|
+
code_before_betterAuth: `new MssqlDialect({
|
|
247
|
+
tarn: {
|
|
248
|
+
...Tarn,
|
|
249
|
+
options: {
|
|
250
|
+
min: 0,
|
|
251
|
+
max: 10,
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
tedious: {
|
|
255
|
+
...Tedious,
|
|
256
|
+
connectionFactory: () => new Tedious.Connection({
|
|
257
|
+
authentication: {
|
|
258
|
+
options: {
|
|
259
|
+
password: 'password',
|
|
260
|
+
userName: 'username',
|
|
261
|
+
},
|
|
262
|
+
type: 'default',
|
|
263
|
+
},
|
|
264
|
+
options: {
|
|
265
|
+
database: 'some_db',
|
|
266
|
+
port: 1433,
|
|
267
|
+
trustServerCertificate: true,
|
|
268
|
+
},
|
|
269
|
+
server: 'localhost',
|
|
270
|
+
}),
|
|
271
|
+
},
|
|
272
|
+
})`,
|
|
273
|
+
db_code: `dialect`,
|
|
274
|
+
dependencies: [
|
|
275
|
+
"tedious",
|
|
276
|
+
"tarn",
|
|
277
|
+
"kysely"
|
|
278
|
+
],
|
|
279
|
+
envs: ["DATABASE_URL"],
|
|
280
|
+
imports: [
|
|
281
|
+
{
|
|
282
|
+
path: "tedious",
|
|
283
|
+
variables: {
|
|
284
|
+
name: "*",
|
|
285
|
+
as: "Tedious"
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
path: "tarn",
|
|
290
|
+
variables: {
|
|
291
|
+
name: "*",
|
|
292
|
+
as: "Tarn"
|
|
293
|
+
}
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
path: "kysely",
|
|
297
|
+
variables: [{ name: "MssqlDialect" }]
|
|
298
|
+
}
|
|
299
|
+
]
|
|
300
|
+
});
|
|
301
|
+
else if (opts.database === "drizzle:mysql" || opts.database === "drizzle:sqlite" || opts.database === "drizzle:pg") await add_db({
|
|
302
|
+
db_code: `drizzleAdapter(db, {\nprovider: "${opts.database.replace("drizzle:", "")}",\n})`,
|
|
303
|
+
dependencies: [""],
|
|
304
|
+
envs: [],
|
|
305
|
+
imports: [{
|
|
306
|
+
path: "better-auth/adapters/drizzle",
|
|
307
|
+
variables: [{ name: "drizzleAdapter" }]
|
|
308
|
+
}, {
|
|
309
|
+
path: "./database.ts",
|
|
310
|
+
variables: [{ name: "db" }]
|
|
311
|
+
}]
|
|
312
|
+
});
|
|
313
|
+
else if (opts.database === "prisma:mysql" || opts.database === "prisma:sqlite" || opts.database === "prisma:postgresql") await add_db({
|
|
314
|
+
db_code: `prismaAdapter(client, {\nprovider: "${opts.database.replace("prisma:", "")}",\n})`,
|
|
315
|
+
dependencies: [`@prisma/client`],
|
|
316
|
+
envs: [],
|
|
317
|
+
code_before_betterAuth: "const client = new PrismaClient();",
|
|
318
|
+
imports: [{
|
|
319
|
+
path: "better-auth/adapters/prisma",
|
|
320
|
+
variables: [{ name: "prismaAdapter" }]
|
|
321
|
+
}, {
|
|
322
|
+
path: "@prisma/client",
|
|
323
|
+
variables: [{ name: "PrismaClient" }]
|
|
324
|
+
}]
|
|
325
|
+
});
|
|
326
|
+
else if (opts.database === "mongodb") await add_db({
|
|
327
|
+
db_code: `mongodbAdapter(db)`,
|
|
328
|
+
dependencies: ["mongodb"],
|
|
329
|
+
envs: [`DATABASE_URL`],
|
|
330
|
+
code_before_betterAuth: [`const client = new MongoClient(process.env.DATABASE_URL || "mongodb://localhost:27017/database");`, `const db = client.db();`].join("\n"),
|
|
331
|
+
imports: [{
|
|
332
|
+
path: "better-auth/adapters/mongodb",
|
|
333
|
+
variables: [{ name: "mongodbAdapter" }]
|
|
334
|
+
}, {
|
|
335
|
+
path: "mongodb",
|
|
336
|
+
variables: [{ name: "MongoClient" }]
|
|
337
|
+
}]
|
|
338
|
+
});
|
|
339
|
+
let start_of_betterauth = getGroupInfo(opts.config, common_indexes.START_OF_BETTERAUTH, {});
|
|
340
|
+
if (!start_of_betterauth) throw new Error("Couldn't find start of betterAuth() function.");
|
|
341
|
+
let new_content;
|
|
342
|
+
new_content = insertContent({
|
|
343
|
+
line: start_of_betterauth.line,
|
|
344
|
+
character: start_of_betterauth.character,
|
|
345
|
+
content: opts.config,
|
|
346
|
+
insert_content: `database: ${database_code_str},`
|
|
347
|
+
});
|
|
348
|
+
try {
|
|
349
|
+
new_content = await format$1(new_content);
|
|
350
|
+
return {
|
|
351
|
+
code: new_content,
|
|
352
|
+
dependencies: required_deps,
|
|
353
|
+
envs: required_envs
|
|
354
|
+
};
|
|
355
|
+
} catch (error) {
|
|
356
|
+
console.error(error);
|
|
357
|
+
throw new Error(`Failed to generate new auth config during database addition phase.`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
let new_user_config = await format$1(current_user_config);
|
|
362
|
+
let total_dependencies = [];
|
|
363
|
+
let total_envs = [];
|
|
364
|
+
if (plugins.length !== 0) {
|
|
365
|
+
const imports = [];
|
|
366
|
+
for await (const plugin of plugins) {
|
|
367
|
+
const existingIndex = imports.findIndex((x) => x.path === plugin.path);
|
|
368
|
+
if (existingIndex !== -1) imports[existingIndex].variables.push({
|
|
369
|
+
name: plugin.name,
|
|
370
|
+
asType: false
|
|
371
|
+
});
|
|
372
|
+
else imports.push({
|
|
373
|
+
path: plugin.path,
|
|
374
|
+
variables: [{
|
|
375
|
+
name: plugin.name,
|
|
376
|
+
asType: false
|
|
377
|
+
}]
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
if (imports.length !== 0) {
|
|
381
|
+
const { code, envs, dependencies } = await config_generation.add_import({
|
|
382
|
+
config: new_user_config,
|
|
383
|
+
imports
|
|
384
|
+
});
|
|
385
|
+
total_dependencies.push(...dependencies);
|
|
386
|
+
total_envs.push(...envs);
|
|
387
|
+
new_user_config = code;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
for await (const plugin of plugins) try {
|
|
391
|
+
let pluginContents = "";
|
|
392
|
+
if (plugin.id === "magic-link") pluginContents = `{\nsendMagicLink({ email, token, url }, request) {\n// Send email with magic link\n},\n}`;
|
|
393
|
+
else if (plugin.id === "email-otp") pluginContents = `{\nasync sendVerificationOTP({ email, otp, type }, request) {\n// Send email with OTP\n},\n}`;
|
|
394
|
+
else if (plugin.id === "generic-oauth") pluginContents = `{\nconfig: [],\n}`;
|
|
395
|
+
else if (plugin.id === "oidc") pluginContents = `{\nloginPage: "/sign-in",\n}`;
|
|
396
|
+
const { code, dependencies, envs } = await config_generation.add_plugin({
|
|
397
|
+
config: new_user_config,
|
|
398
|
+
direction_in_plugins_array: plugin.id === "next-cookies" ? "append" : "prepend",
|
|
399
|
+
pluginFunctionName: plugin.name,
|
|
400
|
+
pluginContents
|
|
401
|
+
});
|
|
402
|
+
new_user_config = code;
|
|
403
|
+
total_envs.push(...envs);
|
|
404
|
+
total_dependencies.push(...dependencies);
|
|
405
|
+
} catch (error) {
|
|
406
|
+
spinner$1.stop(`Something went wrong while generating/updating your new auth config file.`, 1);
|
|
407
|
+
logger.error(error.message);
|
|
408
|
+
process.exit(1);
|
|
409
|
+
}
|
|
410
|
+
if (database) try {
|
|
411
|
+
const { code, dependencies, envs } = await config_generation.add_database({
|
|
412
|
+
config: new_user_config,
|
|
413
|
+
database
|
|
414
|
+
});
|
|
415
|
+
new_user_config = code;
|
|
416
|
+
total_dependencies.push(...dependencies);
|
|
417
|
+
total_envs.push(...envs);
|
|
418
|
+
} catch (error) {
|
|
419
|
+
spinner$1.stop(`Something went wrong while generating/updating your new auth config file.`, 1);
|
|
420
|
+
logger.error(error.message);
|
|
421
|
+
process.exit(1);
|
|
422
|
+
}
|
|
423
|
+
return {
|
|
424
|
+
generatedCode: new_user_config,
|
|
425
|
+
dependencies: total_dependencies,
|
|
426
|
+
envs: total_envs
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
function findClosingBracket(content, startIndex, openingBracket, closingBracket) {
|
|
430
|
+
let stack = 0;
|
|
431
|
+
let inString = false;
|
|
432
|
+
let quoteChar = null;
|
|
433
|
+
for (let i = startIndex; i < content.length; i++) {
|
|
434
|
+
const char = content[i];
|
|
435
|
+
if (char === "\"" || char === "'" || char === "`") {
|
|
436
|
+
if (!inString) {
|
|
437
|
+
inString = true;
|
|
438
|
+
quoteChar = char;
|
|
439
|
+
} else if (char === quoteChar) {
|
|
440
|
+
inString = false;
|
|
441
|
+
quoteChar = null;
|
|
442
|
+
}
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
if (!inString) {
|
|
446
|
+
if (char === openingBracket) stack++;
|
|
447
|
+
else if (char === closingBracket) {
|
|
448
|
+
if (stack === 0) return i;
|
|
449
|
+
stack--;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
return null;
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Helper function to insert content at a specific line and character position in a string.
|
|
457
|
+
*/
|
|
458
|
+
function insertContent(params) {
|
|
459
|
+
const { line, character, content, insert_content } = params;
|
|
460
|
+
const lines = content.split("\n");
|
|
461
|
+
if (line < 1 || line > lines.length) throw new Error("Invalid line number");
|
|
462
|
+
const targetLineIndex = line - 1;
|
|
463
|
+
if (character < 0 || character > lines[targetLineIndex].length) throw new Error("Invalid character index");
|
|
464
|
+
const targetLine = lines[targetLineIndex];
|
|
465
|
+
lines[targetLineIndex] = targetLine.slice(0, character) + insert_content + targetLine.slice(character);
|
|
466
|
+
return lines.join("\n");
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Helper function to get the line and character position of a specific group in a string using a CommonIndexConfig.
|
|
470
|
+
*/
|
|
471
|
+
function getGroupInfo(content, commonIndexConfig, additionalFields) {
|
|
472
|
+
if (commonIndexConfig.type === "regex") {
|
|
473
|
+
const { regex, getIndex } = commonIndexConfig;
|
|
474
|
+
const match = regex.exec(content);
|
|
475
|
+
if (match) {
|
|
476
|
+
const matchIndex = match.index;
|
|
477
|
+
const groupIndex = getIndex({
|
|
478
|
+
matchIndex,
|
|
479
|
+
match,
|
|
480
|
+
additionalFields
|
|
481
|
+
});
|
|
482
|
+
if (groupIndex === null) return null;
|
|
483
|
+
const position = getPosition(content, groupIndex);
|
|
484
|
+
return {
|
|
485
|
+
line: position.line,
|
|
486
|
+
character: position.character,
|
|
487
|
+
index: groupIndex
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
return null;
|
|
491
|
+
} else {
|
|
492
|
+
const { getIndex } = commonIndexConfig;
|
|
493
|
+
const index = getIndex({
|
|
494
|
+
content,
|
|
495
|
+
additionalFields
|
|
496
|
+
});
|
|
497
|
+
if (index === null) return null;
|
|
498
|
+
const { line, character } = getPosition(content, index);
|
|
499
|
+
return {
|
|
500
|
+
line,
|
|
501
|
+
character,
|
|
502
|
+
index
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* Helper function to calculate line and character position based on an index
|
|
508
|
+
*/
|
|
509
|
+
const getPosition = (str, index) => {
|
|
510
|
+
const lines = str.slice(0, index).split("\n");
|
|
511
|
+
return {
|
|
512
|
+
line: lines.length,
|
|
513
|
+
character: lines[lines.length - 1].length
|
|
514
|
+
};
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
//#endregion
|
|
518
|
+
//#region src/utils/get-tsconfig-info.ts
|
|
519
|
+
function stripJsonComments(jsonString) {
|
|
520
|
+
return jsonString.replace(/\\"|"(?:\\"|[^"])*"|(\/\/.*|\/\*[\s\S]*?\*\/)/g, (m, g) => g ? "" : m).replace(/,(?=\s*[}\]])/g, "");
|
|
521
|
+
}
|
|
522
|
+
function getTsconfigInfo(cwd, flatPath) {
|
|
523
|
+
let tsConfigPath;
|
|
524
|
+
if (flatPath) tsConfigPath = flatPath;
|
|
525
|
+
else tsConfigPath = cwd ? path.join(cwd, "tsconfig.json") : path.join("tsconfig.json");
|
|
526
|
+
try {
|
|
527
|
+
const text$1 = fs.readFileSync(tsConfigPath, "utf-8");
|
|
528
|
+
return JSON.parse(stripJsonComments(text$1));
|
|
529
|
+
} catch (error) {
|
|
530
|
+
throw error;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
//#endregion
|
|
535
|
+
//#region src/commands/init.ts
|
|
536
|
+
/**
|
|
537
|
+
* Should only use any database that is core DBs, and supports the Better Auth CLI generate functionality.
|
|
538
|
+
*/
|
|
539
|
+
const supportedDatabases = [
|
|
540
|
+
"sqlite",
|
|
541
|
+
"mysql",
|
|
542
|
+
"mssql",
|
|
543
|
+
"postgres",
|
|
544
|
+
"drizzle:pg",
|
|
545
|
+
"drizzle:mysql",
|
|
546
|
+
"drizzle:sqlite",
|
|
547
|
+
"prisma:postgresql",
|
|
548
|
+
"prisma:mysql",
|
|
549
|
+
"prisma:sqlite",
|
|
550
|
+
"mongodb"
|
|
551
|
+
];
|
|
552
|
+
const supportedPlugins = [
|
|
553
|
+
{
|
|
554
|
+
id: "two-factor",
|
|
555
|
+
name: "twoFactor",
|
|
556
|
+
path: `better-auth/plugins`,
|
|
557
|
+
clientName: "twoFactorClient",
|
|
558
|
+
clientPath: "better-auth/client/plugins"
|
|
559
|
+
},
|
|
560
|
+
{
|
|
561
|
+
id: "username",
|
|
562
|
+
name: "username",
|
|
563
|
+
clientName: "usernameClient",
|
|
564
|
+
path: `better-auth/plugins`,
|
|
565
|
+
clientPath: "better-auth/client/plugins"
|
|
566
|
+
},
|
|
567
|
+
{
|
|
568
|
+
id: "anonymous",
|
|
569
|
+
name: "anonymous",
|
|
570
|
+
clientName: "anonymousClient",
|
|
571
|
+
path: `better-auth/plugins`,
|
|
572
|
+
clientPath: "better-auth/client/plugins"
|
|
573
|
+
},
|
|
574
|
+
{
|
|
575
|
+
id: "phone-number",
|
|
576
|
+
name: "phoneNumber",
|
|
577
|
+
clientName: "phoneNumberClient",
|
|
578
|
+
path: `better-auth/plugins`,
|
|
579
|
+
clientPath: "better-auth/client/plugins"
|
|
580
|
+
},
|
|
581
|
+
{
|
|
582
|
+
id: "magic-link",
|
|
583
|
+
name: "magicLink",
|
|
584
|
+
clientName: "magicLinkClient",
|
|
585
|
+
clientPath: "better-auth/client/plugins",
|
|
586
|
+
path: `better-auth/plugins`
|
|
587
|
+
},
|
|
588
|
+
{
|
|
589
|
+
id: "email-otp",
|
|
590
|
+
name: "emailOTP",
|
|
591
|
+
clientName: "emailOTPClient",
|
|
592
|
+
path: `better-auth/plugins`,
|
|
593
|
+
clientPath: "better-auth/client/plugins"
|
|
594
|
+
},
|
|
595
|
+
{
|
|
596
|
+
id: "passkey",
|
|
597
|
+
name: "passkey",
|
|
598
|
+
clientName: "passkeyClient",
|
|
599
|
+
path: `better-auth/plugins/passkey`,
|
|
600
|
+
clientPath: "better-auth/client/plugins"
|
|
601
|
+
},
|
|
602
|
+
{
|
|
603
|
+
id: "generic-oauth",
|
|
604
|
+
name: "genericOAuth",
|
|
605
|
+
clientName: "genericOAuthClient",
|
|
606
|
+
path: `better-auth/plugins`,
|
|
607
|
+
clientPath: "better-auth/client/plugins"
|
|
608
|
+
},
|
|
609
|
+
{
|
|
610
|
+
id: "one-tap",
|
|
611
|
+
name: "oneTap",
|
|
612
|
+
clientName: "oneTapClient",
|
|
613
|
+
path: `better-auth/plugins`,
|
|
614
|
+
clientPath: "better-auth/client/plugins"
|
|
615
|
+
},
|
|
616
|
+
{
|
|
617
|
+
id: "api-key",
|
|
618
|
+
name: "apiKey",
|
|
619
|
+
clientName: "apiKeyClient",
|
|
620
|
+
path: `better-auth/plugins`,
|
|
621
|
+
clientPath: "better-auth/client/plugins"
|
|
622
|
+
},
|
|
623
|
+
{
|
|
624
|
+
id: "admin",
|
|
625
|
+
name: "admin",
|
|
626
|
+
clientName: "adminClient",
|
|
627
|
+
path: `better-auth/plugins`,
|
|
628
|
+
clientPath: "better-auth/client/plugins"
|
|
629
|
+
},
|
|
630
|
+
{
|
|
631
|
+
id: "organization",
|
|
632
|
+
name: "organization",
|
|
633
|
+
clientName: "organizationClient",
|
|
634
|
+
path: `better-auth/plugins`,
|
|
635
|
+
clientPath: "better-auth/client/plugins"
|
|
636
|
+
},
|
|
637
|
+
{
|
|
638
|
+
id: "oidc",
|
|
639
|
+
name: "oidcProvider",
|
|
640
|
+
clientName: "oidcClient",
|
|
641
|
+
path: `better-auth/plugins`,
|
|
642
|
+
clientPath: "better-auth/client/plugins"
|
|
643
|
+
},
|
|
644
|
+
{
|
|
645
|
+
id: "sso",
|
|
646
|
+
name: "sso",
|
|
647
|
+
clientName: "ssoClient",
|
|
648
|
+
path: `better-auth/plugins/sso`,
|
|
649
|
+
clientPath: "better-auth/client/plugins"
|
|
650
|
+
},
|
|
651
|
+
{
|
|
652
|
+
id: "bearer",
|
|
653
|
+
name: "bearer",
|
|
654
|
+
clientName: void 0,
|
|
655
|
+
path: `better-auth/plugins`,
|
|
656
|
+
clientPath: void 0
|
|
657
|
+
},
|
|
658
|
+
{
|
|
659
|
+
id: "multi-session",
|
|
660
|
+
name: "multiSession",
|
|
661
|
+
clientName: "multiSessionClient",
|
|
662
|
+
path: `better-auth/plugins`,
|
|
663
|
+
clientPath: "better-auth/client/plugins"
|
|
664
|
+
},
|
|
665
|
+
{
|
|
666
|
+
id: "oauth-proxy",
|
|
667
|
+
name: "oAuthProxy",
|
|
668
|
+
clientName: void 0,
|
|
669
|
+
path: `better-auth/plugins`,
|
|
670
|
+
clientPath: void 0
|
|
671
|
+
},
|
|
672
|
+
{
|
|
673
|
+
id: "open-api",
|
|
674
|
+
name: "openAPI",
|
|
675
|
+
clientName: void 0,
|
|
676
|
+
path: `better-auth/plugins`,
|
|
677
|
+
clientPath: void 0
|
|
678
|
+
},
|
|
679
|
+
{
|
|
680
|
+
id: "jwt",
|
|
681
|
+
name: "jwt",
|
|
682
|
+
clientName: void 0,
|
|
683
|
+
clientPath: void 0,
|
|
684
|
+
path: `better-auth/plugins`
|
|
685
|
+
},
|
|
686
|
+
{
|
|
687
|
+
id: "next-cookies",
|
|
688
|
+
name: "nextCookies",
|
|
689
|
+
clientPath: void 0,
|
|
690
|
+
clientName: void 0,
|
|
691
|
+
path: `better-auth/next-js`
|
|
692
|
+
}
|
|
693
|
+
];
|
|
694
|
+
const defaultFormatOptions = {
|
|
695
|
+
trailingComma: "all",
|
|
696
|
+
useTabs: false,
|
|
697
|
+
tabWidth: 4
|
|
698
|
+
};
|
|
699
|
+
const getDefaultAuthConfig = async ({ appName }) => await format([
|
|
700
|
+
"import { betterAuth } from 'better-auth';",
|
|
701
|
+
"",
|
|
702
|
+
"export const auth = betterAuth({",
|
|
703
|
+
appName ? `appName: "${appName}",` : "",
|
|
704
|
+
"plugins: [],",
|
|
705
|
+
"});"
|
|
706
|
+
].join("\n"), {
|
|
707
|
+
filepath: "auth.ts",
|
|
708
|
+
...defaultFormatOptions
|
|
709
|
+
});
|
|
710
|
+
const getDefaultAuthClientConfig = async ({ auth_config_path, framework, clientPlugins }) => {
|
|
711
|
+
function groupImportVariables() {
|
|
712
|
+
const result = [{
|
|
713
|
+
path: "better-auth/client/plugins",
|
|
714
|
+
variables: [{ name: "inferAdditionalFields" }]
|
|
715
|
+
}];
|
|
716
|
+
for (const plugin of clientPlugins) for (const import_ of plugin.imports) if (Array.isArray(import_.variables)) for (const variable of import_.variables) {
|
|
717
|
+
const existingIndex = result.findIndex((x) => x.path === import_.path);
|
|
718
|
+
if (existingIndex !== -1) {
|
|
719
|
+
const vars = result[existingIndex].variables;
|
|
720
|
+
if (Array.isArray(vars)) vars.push(variable);
|
|
721
|
+
else result[existingIndex].variables = [vars, variable];
|
|
722
|
+
} else result.push({
|
|
723
|
+
path: import_.path,
|
|
724
|
+
variables: [variable]
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
else {
|
|
728
|
+
const existingIndex = result.findIndex((x) => x.path === import_.path);
|
|
729
|
+
if (existingIndex !== -1) {
|
|
730
|
+
const vars = result[existingIndex].variables;
|
|
731
|
+
if (Array.isArray(vars)) vars.push(import_.variables);
|
|
732
|
+
else result[existingIndex].variables = [vars, import_.variables];
|
|
733
|
+
} else result.push({
|
|
734
|
+
path: import_.path,
|
|
735
|
+
variables: [import_.variables]
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
return result;
|
|
739
|
+
}
|
|
740
|
+
let imports = groupImportVariables();
|
|
741
|
+
let importString = "";
|
|
742
|
+
for (const import_ of imports) if (Array.isArray(import_.variables)) importString += `import { ${import_.variables.map((x) => `${x.asType ? "type " : ""}${x.name}${x.as ? ` as ${x.as}` : ""}`).join(", ")} } from "${import_.path}";\n`;
|
|
743
|
+
else importString += `import ${import_.variables.asType ? "type " : ""}${import_.variables.name}${import_.variables.as ? ` as ${import_.variables.as}` : ""} from "${import_.path}";\n`;
|
|
744
|
+
return await format([
|
|
745
|
+
`import { createAuthClient } from "better-auth/${framework === "nextjs" ? "react" : framework === "vanilla" ? "client" : framework}";`,
|
|
746
|
+
`import type { auth } from "${auth_config_path}";`,
|
|
747
|
+
importString,
|
|
748
|
+
``,
|
|
749
|
+
`export const authClient = createAuthClient({`,
|
|
750
|
+
`baseURL: "http://localhost:3000",`,
|
|
751
|
+
`plugins: [inferAdditionalFields<typeof auth>(),${clientPlugins.map((x) => `${x.name}(${x.contents})`).join(", ")}],`,
|
|
752
|
+
`});`
|
|
753
|
+
].join("\n"), {
|
|
754
|
+
filepath: "auth-client.ts",
|
|
755
|
+
...defaultFormatOptions
|
|
756
|
+
});
|
|
757
|
+
};
|
|
758
|
+
const optionsSchema = z.object({
|
|
759
|
+
cwd: z.string(),
|
|
760
|
+
config: z.string().optional(),
|
|
761
|
+
database: z.enum(supportedDatabases).optional(),
|
|
762
|
+
"skip-db": z.boolean().optional(),
|
|
763
|
+
"skip-plugins": z.boolean().optional(),
|
|
764
|
+
"package-manager": z.string().optional(),
|
|
765
|
+
tsconfig: z.string().optional()
|
|
766
|
+
});
|
|
767
|
+
const outroText = `𼳠All Done, Happy Hacking!`;
|
|
768
|
+
async function initAction(opts) {
|
|
769
|
+
console.log();
|
|
770
|
+
intro("đ Initializing Better Auth");
|
|
771
|
+
const options = optionsSchema.parse(opts);
|
|
772
|
+
const cwd = path.resolve(options.cwd);
|
|
773
|
+
let packageManagerPreference = void 0;
|
|
774
|
+
let config_path = "";
|
|
775
|
+
let framework = "vanilla";
|
|
776
|
+
const format$1 = async (code) => await format(code, {
|
|
777
|
+
filepath: config_path,
|
|
778
|
+
...defaultFormatOptions
|
|
779
|
+
});
|
|
780
|
+
let packageInfo;
|
|
781
|
+
try {
|
|
782
|
+
packageInfo = getPackageInfo(cwd);
|
|
783
|
+
} catch (error) {
|
|
784
|
+
log.error(`â Couldn't read your package.json file. (dir: ${cwd})`);
|
|
785
|
+
log.error(JSON.stringify(error, null, 2));
|
|
786
|
+
process.exit(1);
|
|
787
|
+
}
|
|
788
|
+
const envFiles = await getEnvFiles(cwd);
|
|
789
|
+
if (!envFiles.length) {
|
|
790
|
+
outro("â No .env files found. Please create an env file first.");
|
|
791
|
+
process.exit(0);
|
|
792
|
+
}
|
|
793
|
+
let targetEnvFile;
|
|
794
|
+
if (envFiles.includes(".env")) targetEnvFile = ".env";
|
|
795
|
+
else if (envFiles.includes(".env.local")) targetEnvFile = ".env.local";
|
|
796
|
+
else if (envFiles.includes(".env.development")) targetEnvFile = ".env.development";
|
|
797
|
+
else if (envFiles.length === 1) targetEnvFile = envFiles[0];
|
|
798
|
+
else targetEnvFile = "none";
|
|
799
|
+
let tsconfigInfo;
|
|
800
|
+
try {
|
|
801
|
+
tsconfigInfo = await getTsconfigInfo(cwd, options.tsconfig !== void 0 ? path.resolve(cwd, options.tsconfig) : path.join(cwd, "tsconfig.json"));
|
|
802
|
+
} catch (error) {
|
|
803
|
+
log.error(`â Couldn't read your tsconfig.json file. (dir: ${cwd})`);
|
|
804
|
+
console.error(error);
|
|
805
|
+
process.exit(1);
|
|
806
|
+
}
|
|
807
|
+
if (!("compilerOptions" in tsconfigInfo && "strict" in tsconfigInfo.compilerOptions && tsconfigInfo.compilerOptions.strict === true)) {
|
|
808
|
+
log.warn(`Better Auth requires your tsconfig.json to have "compilerOptions.strict" set to true.`);
|
|
809
|
+
const shouldAdd = await confirm({ message: `Would you like us to set ${chalk.bold(`strict`)} to ${chalk.bold(`true`)}?` });
|
|
810
|
+
if (isCancel(shouldAdd)) {
|
|
811
|
+
cancel(`â Operation cancelled.`);
|
|
812
|
+
process.exit(0);
|
|
813
|
+
}
|
|
814
|
+
if (shouldAdd) try {
|
|
815
|
+
await fs$1.writeFile(path.join(cwd, "tsconfig.json"), await format(JSON.stringify(Object.assign(tsconfigInfo, { compilerOptions: { strict: true } })), {
|
|
816
|
+
filepath: "tsconfig.json",
|
|
817
|
+
...defaultFormatOptions
|
|
818
|
+
}), "utf-8");
|
|
819
|
+
log.success(`đ tsconfig.json successfully updated!`);
|
|
820
|
+
} catch (error) {
|
|
821
|
+
log.error(`Failed to add "compilerOptions.strict" to your tsconfig.json file.`);
|
|
822
|
+
console.error(error);
|
|
823
|
+
process.exit(1);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
const s = spinner({ indicator: "dots" });
|
|
827
|
+
s.start(`Checking better-auth installation`);
|
|
828
|
+
let latest_betterauth_version;
|
|
829
|
+
try {
|
|
830
|
+
latest_betterauth_version = await getLatestNpmVersion("better-auth");
|
|
831
|
+
} catch (error) {
|
|
832
|
+
log.error(`â Couldn't get latest version of better-auth.`);
|
|
833
|
+
console.error(error);
|
|
834
|
+
process.exit(1);
|
|
835
|
+
}
|
|
836
|
+
if (!packageInfo.dependencies || !Object.keys(packageInfo.dependencies).includes("better-auth")) {
|
|
837
|
+
s.stop("Finished fetching latest version of better-auth.");
|
|
838
|
+
const s2 = spinner({ indicator: "dots" });
|
|
839
|
+
const shouldInstallBetterAuthDep = await confirm({ message: `Would you like to install Better Auth?` });
|
|
840
|
+
if (isCancel(shouldInstallBetterAuthDep)) {
|
|
841
|
+
cancel(`â Operation cancelled.`);
|
|
842
|
+
process.exit(0);
|
|
843
|
+
}
|
|
844
|
+
if (packageManagerPreference === void 0) packageManagerPreference = await getPackageManager$1();
|
|
845
|
+
if (shouldInstallBetterAuthDep) {
|
|
846
|
+
s2.start(`Installing Better Auth using ${chalk.bold(packageManagerPreference)}`);
|
|
847
|
+
try {
|
|
848
|
+
const start = Date.now();
|
|
849
|
+
await installDependencies({
|
|
850
|
+
dependencies: ["better-auth@latest"],
|
|
851
|
+
packageManager: packageManagerPreference,
|
|
852
|
+
cwd
|
|
853
|
+
});
|
|
854
|
+
s2.stop(`Better Auth installed ${chalk.greenBright(`successfully`)}! ${chalk.gray(`(${formatMilliseconds(Date.now() - start)})`)}`);
|
|
855
|
+
} catch (error) {
|
|
856
|
+
s2.stop(`Failed to install Better Auth:`);
|
|
857
|
+
console.error(error);
|
|
858
|
+
process.exit(1);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
} else if (packageInfo.dependencies["better-auth"] !== "workspace:*" && semver.lt(semver.coerce(packageInfo.dependencies["better-auth"])?.toString(), semver.clean(latest_betterauth_version))) {
|
|
862
|
+
s.stop("Finished fetching latest version of better-auth.");
|
|
863
|
+
const shouldInstallBetterAuthDep = await confirm({ message: `Your current Better Auth dependency is out-of-date. Would you like to update it? (${chalk.bold(packageInfo.dependencies["better-auth"])} â ${chalk.bold(`v${latest_betterauth_version}`)})` });
|
|
864
|
+
if (isCancel(shouldInstallBetterAuthDep)) {
|
|
865
|
+
cancel(`â Operation cancelled.`);
|
|
866
|
+
process.exit(0);
|
|
867
|
+
}
|
|
868
|
+
if (shouldInstallBetterAuthDep) {
|
|
869
|
+
if (packageManagerPreference === void 0) packageManagerPreference = await getPackageManager$1();
|
|
870
|
+
const s$1 = spinner({ indicator: "dots" });
|
|
871
|
+
s$1.start(`Updating Better Auth using ${chalk.bold(packageManagerPreference)}`);
|
|
872
|
+
try {
|
|
873
|
+
const start = Date.now();
|
|
874
|
+
await installDependencies({
|
|
875
|
+
dependencies: ["better-auth@latest"],
|
|
876
|
+
packageManager: packageManagerPreference,
|
|
877
|
+
cwd
|
|
878
|
+
});
|
|
879
|
+
s$1.stop(`Better Auth updated ${chalk.greenBright(`successfully`)}! ${chalk.gray(`(${formatMilliseconds(Date.now() - start)})`)}`);
|
|
880
|
+
} catch (error) {
|
|
881
|
+
s$1.stop(`Failed to update Better Auth:`);
|
|
882
|
+
log.error(error.message);
|
|
883
|
+
process.exit(1);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
} else s.stop(`Better Auth dependencies are ${chalk.greenBright(`up to date`)}!`);
|
|
887
|
+
const packageJson = getPackageInfo(cwd);
|
|
888
|
+
let appName;
|
|
889
|
+
if (!packageJson.name) {
|
|
890
|
+
const newAppName = await text({ message: "What is the name of your application?" });
|
|
891
|
+
if (isCancel(newAppName)) {
|
|
892
|
+
cancel("â Operation cancelled.");
|
|
893
|
+
process.exit(0);
|
|
894
|
+
}
|
|
895
|
+
appName = newAppName;
|
|
896
|
+
} else appName = packageJson.name;
|
|
897
|
+
let possiblePaths$1 = [
|
|
898
|
+
"auth.ts",
|
|
899
|
+
"auth.tsx",
|
|
900
|
+
"auth.js",
|
|
901
|
+
"auth.jsx"
|
|
902
|
+
];
|
|
903
|
+
possiblePaths$1 = [
|
|
904
|
+
...possiblePaths$1,
|
|
905
|
+
...possiblePaths$1.map((it) => `lib/server/${it}`),
|
|
906
|
+
...possiblePaths$1.map((it) => `server/${it}`),
|
|
907
|
+
...possiblePaths$1.map((it) => `lib/${it}`),
|
|
908
|
+
...possiblePaths$1.map((it) => `utils/${it}`)
|
|
909
|
+
];
|
|
910
|
+
possiblePaths$1 = [
|
|
911
|
+
...possiblePaths$1,
|
|
912
|
+
...possiblePaths$1.map((it) => `src/${it}`),
|
|
913
|
+
...possiblePaths$1.map((it) => `app/${it}`)
|
|
914
|
+
];
|
|
915
|
+
if (options.config) config_path = path.join(cwd, options.config);
|
|
916
|
+
else for (const possiblePath of possiblePaths$1) if (existsSync(path.join(cwd, possiblePath))) {
|
|
917
|
+
config_path = path.join(cwd, possiblePath);
|
|
918
|
+
break;
|
|
919
|
+
}
|
|
920
|
+
let current_user_config = "";
|
|
921
|
+
let database = null;
|
|
922
|
+
let add_plugins = [];
|
|
923
|
+
if (!config_path) {
|
|
924
|
+
const shouldCreateAuthConfig = await select({
|
|
925
|
+
message: `Would you like to create an auth config file?`,
|
|
926
|
+
options: [{
|
|
927
|
+
label: "Yes",
|
|
928
|
+
value: "yes"
|
|
929
|
+
}, {
|
|
930
|
+
label: "No",
|
|
931
|
+
value: "no"
|
|
932
|
+
}]
|
|
933
|
+
});
|
|
934
|
+
if (isCancel(shouldCreateAuthConfig)) {
|
|
935
|
+
cancel(`â Operation cancelled.`);
|
|
936
|
+
process.exit(0);
|
|
937
|
+
}
|
|
938
|
+
if (shouldCreateAuthConfig === "yes") {
|
|
939
|
+
const shouldSetupDb = await confirm({
|
|
940
|
+
message: `Would you like to set up your ${chalk.bold(`database`)}?`,
|
|
941
|
+
initialValue: true
|
|
942
|
+
});
|
|
943
|
+
if (isCancel(shouldSetupDb)) {
|
|
944
|
+
cancel(`â Operating cancelled.`);
|
|
945
|
+
process.exit(0);
|
|
946
|
+
}
|
|
947
|
+
if (shouldSetupDb) {
|
|
948
|
+
const prompted_database = await select({
|
|
949
|
+
message: "Choose a Database Dialect",
|
|
950
|
+
options: supportedDatabases.map((it) => ({
|
|
951
|
+
value: it,
|
|
952
|
+
label: it
|
|
953
|
+
}))
|
|
954
|
+
});
|
|
955
|
+
if (isCancel(prompted_database)) {
|
|
956
|
+
cancel(`â Operating cancelled.`);
|
|
957
|
+
process.exit(0);
|
|
958
|
+
}
|
|
959
|
+
database = prompted_database;
|
|
960
|
+
}
|
|
961
|
+
if (options["skip-plugins"] !== false) {
|
|
962
|
+
const shouldSetupPlugins = await confirm({ message: `Would you like to set up ${chalk.bold(`plugins`)}?` });
|
|
963
|
+
if (isCancel(shouldSetupPlugins)) {
|
|
964
|
+
cancel(`â Operating cancelled.`);
|
|
965
|
+
process.exit(0);
|
|
966
|
+
}
|
|
967
|
+
if (shouldSetupPlugins) {
|
|
968
|
+
const prompted_plugins = await multiselect({
|
|
969
|
+
message: "Select your new plugins",
|
|
970
|
+
options: supportedPlugins.filter((x) => x.id !== "next-cookies").map((x) => ({
|
|
971
|
+
value: x.id,
|
|
972
|
+
label: x.id
|
|
973
|
+
})),
|
|
974
|
+
required: false
|
|
975
|
+
});
|
|
976
|
+
if (isCancel(prompted_plugins)) {
|
|
977
|
+
cancel(`â Operating cancelled.`);
|
|
978
|
+
process.exit(0);
|
|
979
|
+
}
|
|
980
|
+
add_plugins = prompted_plugins.map((x) => supportedPlugins.find((y) => y.id === x));
|
|
981
|
+
for (const possible_next_config_path of [
|
|
982
|
+
"next.config.js",
|
|
983
|
+
"next.config.ts",
|
|
984
|
+
"next.config.mjs",
|
|
985
|
+
".next/server/next.config.js",
|
|
986
|
+
".next/server/next.config.ts",
|
|
987
|
+
".next/server/next.config.mjs"
|
|
988
|
+
]) if (existsSync(path.join(cwd, possible_next_config_path))) {
|
|
989
|
+
framework = "nextjs";
|
|
990
|
+
break;
|
|
991
|
+
}
|
|
992
|
+
if (framework === "nextjs") {
|
|
993
|
+
const result = await confirm({ message: `It looks like you're using NextJS. Do you want to add the next-cookies plugin? ${chalk.bold(`(Recommended)`)}` });
|
|
994
|
+
if (isCancel(result)) {
|
|
995
|
+
cancel(`â Operating cancelled.`);
|
|
996
|
+
process.exit(0);
|
|
997
|
+
}
|
|
998
|
+
if (result) add_plugins.push(supportedPlugins.find((x) => x.id === "next-cookies"));
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
const filePath = path.join(cwd, "auth.ts");
|
|
1003
|
+
config_path = filePath;
|
|
1004
|
+
log.info(`Creating auth config file: ${filePath}`);
|
|
1005
|
+
try {
|
|
1006
|
+
current_user_config = await getDefaultAuthConfig({ appName });
|
|
1007
|
+
const { dependencies, envs, generatedCode } = await generateAuthConfig({
|
|
1008
|
+
current_user_config,
|
|
1009
|
+
format: format$1,
|
|
1010
|
+
s,
|
|
1011
|
+
plugins: add_plugins,
|
|
1012
|
+
database
|
|
1013
|
+
});
|
|
1014
|
+
current_user_config = generatedCode;
|
|
1015
|
+
await fs$1.writeFile(filePath, current_user_config);
|
|
1016
|
+
config_path = filePath;
|
|
1017
|
+
log.success(`đ Auth config file successfully created!`);
|
|
1018
|
+
if (envs.length !== 0) {
|
|
1019
|
+
log.info(`There are ${envs.length} environment variables for your database of choice.`);
|
|
1020
|
+
const shouldUpdateEnvs = await confirm({ message: `Would you like us to update your ENV files?` });
|
|
1021
|
+
if (isCancel(shouldUpdateEnvs)) {
|
|
1022
|
+
cancel("â Operation cancelled.");
|
|
1023
|
+
process.exit(0);
|
|
1024
|
+
}
|
|
1025
|
+
if (shouldUpdateEnvs) {
|
|
1026
|
+
const filesToUpdate = await multiselect({
|
|
1027
|
+
message: "Select the .env files you want to update",
|
|
1028
|
+
options: envFiles.map((x) => ({
|
|
1029
|
+
value: path.join(cwd, x),
|
|
1030
|
+
label: x
|
|
1031
|
+
})),
|
|
1032
|
+
required: false
|
|
1033
|
+
});
|
|
1034
|
+
if (isCancel(filesToUpdate)) {
|
|
1035
|
+
cancel("â Operation cancelled.");
|
|
1036
|
+
process.exit(0);
|
|
1037
|
+
}
|
|
1038
|
+
if (filesToUpdate.length === 0) log.info("No .env files to update. Skipping...");
|
|
1039
|
+
else {
|
|
1040
|
+
try {
|
|
1041
|
+
await updateEnvs({
|
|
1042
|
+
files: filesToUpdate,
|
|
1043
|
+
envs,
|
|
1044
|
+
isCommented: true
|
|
1045
|
+
});
|
|
1046
|
+
} catch (error) {
|
|
1047
|
+
log.error(`Failed to update .env files:`);
|
|
1048
|
+
log.error(JSON.stringify(error, null, 2));
|
|
1049
|
+
process.exit(1);
|
|
1050
|
+
}
|
|
1051
|
+
log.success(`đ ENV files successfully updated!`);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
if (dependencies.length !== 0) {
|
|
1056
|
+
log.info(`There are ${dependencies.length} dependencies to install. (${dependencies.map((x) => chalk.green(x)).join(", ")})`);
|
|
1057
|
+
const shouldInstallDeps = await confirm({ message: `Would you like us to install dependencies?` });
|
|
1058
|
+
if (isCancel(shouldInstallDeps)) {
|
|
1059
|
+
cancel("â Operation cancelled.");
|
|
1060
|
+
process.exit(0);
|
|
1061
|
+
}
|
|
1062
|
+
if (shouldInstallDeps) {
|
|
1063
|
+
const s$1 = spinner({ indicator: "dots" });
|
|
1064
|
+
if (packageManagerPreference === void 0) packageManagerPreference = await getPackageManager$1();
|
|
1065
|
+
s$1.start(`Installing dependencies using ${chalk.bold(packageManagerPreference)}...`);
|
|
1066
|
+
try {
|
|
1067
|
+
const start = Date.now();
|
|
1068
|
+
await installDependencies({
|
|
1069
|
+
dependencies,
|
|
1070
|
+
packageManager: packageManagerPreference,
|
|
1071
|
+
cwd
|
|
1072
|
+
});
|
|
1073
|
+
s$1.stop(`Dependencies installed ${chalk.greenBright(`successfully`)} ${chalk.gray(`(${formatMilliseconds(Date.now() - start)})`)}`);
|
|
1074
|
+
} catch (error) {
|
|
1075
|
+
s$1.stop(`Failed to install dependencies using ${packageManagerPreference}:`);
|
|
1076
|
+
log.error(error.message);
|
|
1077
|
+
process.exit(1);
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
} catch (error) {
|
|
1082
|
+
log.error(`Failed to create auth config file: ${filePath}`);
|
|
1083
|
+
console.error(error);
|
|
1084
|
+
process.exit(1);
|
|
1085
|
+
}
|
|
1086
|
+
} else if (shouldCreateAuthConfig === "no") log.info(`Skipping auth config file creation.`);
|
|
1087
|
+
} else {
|
|
1088
|
+
log.message();
|
|
1089
|
+
log.success(`Found auth config file. ${chalk.gray(`(${config_path})`)}`);
|
|
1090
|
+
log.message();
|
|
1091
|
+
}
|
|
1092
|
+
let possibleClientPaths = [
|
|
1093
|
+
"auth-client.ts",
|
|
1094
|
+
"auth-client.tsx",
|
|
1095
|
+
"auth-client.js",
|
|
1096
|
+
"auth-client.jsx",
|
|
1097
|
+
"client.ts",
|
|
1098
|
+
"client.tsx",
|
|
1099
|
+
"client.js",
|
|
1100
|
+
"client.jsx"
|
|
1101
|
+
];
|
|
1102
|
+
possibleClientPaths = [
|
|
1103
|
+
...possibleClientPaths,
|
|
1104
|
+
...possibleClientPaths.map((it) => `lib/server/${it}`),
|
|
1105
|
+
...possibleClientPaths.map((it) => `server/${it}`),
|
|
1106
|
+
...possibleClientPaths.map((it) => `lib/${it}`),
|
|
1107
|
+
...possibleClientPaths.map((it) => `utils/${it}`)
|
|
1108
|
+
];
|
|
1109
|
+
possibleClientPaths = [
|
|
1110
|
+
...possibleClientPaths,
|
|
1111
|
+
...possibleClientPaths.map((it) => `src/${it}`),
|
|
1112
|
+
...possibleClientPaths.map((it) => `app/${it}`)
|
|
1113
|
+
];
|
|
1114
|
+
let authClientConfigPath = null;
|
|
1115
|
+
for (const possiblePath of possibleClientPaths) if (existsSync(path.join(cwd, possiblePath))) {
|
|
1116
|
+
authClientConfigPath = path.join(cwd, possiblePath);
|
|
1117
|
+
break;
|
|
1118
|
+
}
|
|
1119
|
+
if (!authClientConfigPath) {
|
|
1120
|
+
const choice = await select({
|
|
1121
|
+
message: `Would you like to create an auth client config file?`,
|
|
1122
|
+
options: [{
|
|
1123
|
+
label: "Yes",
|
|
1124
|
+
value: "yes"
|
|
1125
|
+
}, {
|
|
1126
|
+
label: "No",
|
|
1127
|
+
value: "no"
|
|
1128
|
+
}]
|
|
1129
|
+
});
|
|
1130
|
+
if (isCancel(choice)) {
|
|
1131
|
+
cancel(`â Operation cancelled.`);
|
|
1132
|
+
process.exit(0);
|
|
1133
|
+
}
|
|
1134
|
+
if (choice === "yes") {
|
|
1135
|
+
authClientConfigPath = path.join(cwd, "auth-client.ts");
|
|
1136
|
+
log.info(`Creating auth client config file: ${authClientConfigPath}`);
|
|
1137
|
+
try {
|
|
1138
|
+
let contents = await getDefaultAuthClientConfig({
|
|
1139
|
+
auth_config_path: ("./" + path.join(config_path.replace(cwd, ""))).replace(".//", "./"),
|
|
1140
|
+
clientPlugins: add_plugins.filter((x) => x.clientName).map((plugin) => {
|
|
1141
|
+
let contents$1 = "";
|
|
1142
|
+
if (plugin.id === "one-tap") contents$1 = `{ clientId: "MY_CLIENT_ID" }`;
|
|
1143
|
+
return {
|
|
1144
|
+
contents: contents$1,
|
|
1145
|
+
id: plugin.id,
|
|
1146
|
+
name: plugin.clientName,
|
|
1147
|
+
imports: [{
|
|
1148
|
+
path: "better-auth/client/plugins",
|
|
1149
|
+
variables: [{ name: plugin.clientName }]
|
|
1150
|
+
}]
|
|
1151
|
+
};
|
|
1152
|
+
}),
|
|
1153
|
+
framework
|
|
1154
|
+
});
|
|
1155
|
+
await fs$1.writeFile(authClientConfigPath, contents);
|
|
1156
|
+
log.success(`đ Auth client config file successfully created!`);
|
|
1157
|
+
} catch (error) {
|
|
1158
|
+
log.error(`Failed to create auth client config file: ${authClientConfigPath}`);
|
|
1159
|
+
log.error(JSON.stringify(error, null, 2));
|
|
1160
|
+
process.exit(1);
|
|
1161
|
+
}
|
|
1162
|
+
} else if (choice === "no") log.info(`Skipping auth client config file creation.`);
|
|
1163
|
+
} else log.success(`Found auth client config file. ${chalk.gray(`(${authClientConfigPath})`)}`);
|
|
1164
|
+
if (targetEnvFile !== "none") try {
|
|
1165
|
+
const parsed = parse(await fs$1.readFile(path.join(cwd, targetEnvFile), "utf8"));
|
|
1166
|
+
let isMissingSecret = false;
|
|
1167
|
+
let isMissingUrl = false;
|
|
1168
|
+
if (parsed.BETTER_AUTH_SECRET === void 0) isMissingSecret = true;
|
|
1169
|
+
if (parsed.BETTER_AUTH_URL === void 0) isMissingUrl = true;
|
|
1170
|
+
if (isMissingSecret || isMissingUrl) {
|
|
1171
|
+
let txt = "";
|
|
1172
|
+
if (isMissingSecret && !isMissingUrl) txt = chalk.bold(`BETTER_AUTH_SECRET`);
|
|
1173
|
+
else if (!isMissingSecret && isMissingUrl) txt = chalk.bold(`BETTER_AUTH_URL`);
|
|
1174
|
+
else txt = chalk.bold.underline(`BETTER_AUTH_SECRET`) + ` and ` + chalk.bold.underline(`BETTER_AUTH_URL`);
|
|
1175
|
+
log.warn(`Missing ${txt} in ${targetEnvFile}`);
|
|
1176
|
+
const shouldAdd = await select({
|
|
1177
|
+
message: `Do you want to add ${txt} to ${targetEnvFile}?`,
|
|
1178
|
+
options: [
|
|
1179
|
+
{
|
|
1180
|
+
label: "Yes",
|
|
1181
|
+
value: "yes"
|
|
1182
|
+
},
|
|
1183
|
+
{
|
|
1184
|
+
label: "No",
|
|
1185
|
+
value: "no"
|
|
1186
|
+
},
|
|
1187
|
+
{
|
|
1188
|
+
label: "Choose other file(s)",
|
|
1189
|
+
value: "other"
|
|
1190
|
+
}
|
|
1191
|
+
]
|
|
1192
|
+
});
|
|
1193
|
+
if (isCancel(shouldAdd)) {
|
|
1194
|
+
cancel(`â Operation cancelled.`);
|
|
1195
|
+
process.exit(0);
|
|
1196
|
+
}
|
|
1197
|
+
let envs = [];
|
|
1198
|
+
if (isMissingSecret) envs.push("BETTER_AUTH_SECRET");
|
|
1199
|
+
if (isMissingUrl) envs.push("BETTER_AUTH_URL");
|
|
1200
|
+
if (shouldAdd === "yes") {
|
|
1201
|
+
try {
|
|
1202
|
+
await updateEnvs({
|
|
1203
|
+
files: [path.join(cwd, targetEnvFile)],
|
|
1204
|
+
envs,
|
|
1205
|
+
isCommented: false
|
|
1206
|
+
});
|
|
1207
|
+
} catch (error) {
|
|
1208
|
+
log.error(`Failed to add ENV variables to ${targetEnvFile}`);
|
|
1209
|
+
log.error(JSON.stringify(error, null, 2));
|
|
1210
|
+
process.exit(1);
|
|
1211
|
+
}
|
|
1212
|
+
log.success(`đ ENV variables successfully added!`);
|
|
1213
|
+
if (isMissingUrl) log.info(`Be sure to update your BETTER_AUTH_URL according to your app's needs.`);
|
|
1214
|
+
} else if (shouldAdd === "no") log.info(`Skipping ENV step.`);
|
|
1215
|
+
else if (shouldAdd === "other") {
|
|
1216
|
+
if (!envFiles.length) {
|
|
1217
|
+
cancel("No env files found. Please create an env file first.");
|
|
1218
|
+
process.exit(0);
|
|
1219
|
+
}
|
|
1220
|
+
const envFilesToUpdate = await multiselect({
|
|
1221
|
+
message: "Select the .env files you want to update",
|
|
1222
|
+
options: envFiles.map((x) => ({
|
|
1223
|
+
value: path.join(cwd, x),
|
|
1224
|
+
label: x
|
|
1225
|
+
})),
|
|
1226
|
+
required: false
|
|
1227
|
+
});
|
|
1228
|
+
if (isCancel(envFilesToUpdate)) {
|
|
1229
|
+
cancel("â Operation cancelled.");
|
|
1230
|
+
process.exit(0);
|
|
1231
|
+
}
|
|
1232
|
+
if (envFilesToUpdate.length === 0) log.info("No .env files to update. Skipping...");
|
|
1233
|
+
else {
|
|
1234
|
+
try {
|
|
1235
|
+
await updateEnvs({
|
|
1236
|
+
files: envFilesToUpdate,
|
|
1237
|
+
envs,
|
|
1238
|
+
isCommented: false
|
|
1239
|
+
});
|
|
1240
|
+
} catch (error) {
|
|
1241
|
+
log.error(`Failed to update .env files:`);
|
|
1242
|
+
log.error(JSON.stringify(error, null, 2));
|
|
1243
|
+
process.exit(1);
|
|
1244
|
+
}
|
|
1245
|
+
log.success(`đ ENV files successfully updated!`);
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
} catch (error) {}
|
|
1250
|
+
outro(outroText);
|
|
1251
|
+
console.log();
|
|
1252
|
+
process.exit(0);
|
|
1253
|
+
}
|
|
1254
|
+
const init = new Command("init").option("-c, --cwd <cwd>", "The working directory.", process.cwd()).option("--config <config>", "The path to the auth configuration file. defaults to the first `auth.ts` file found.").option("--tsconfig <tsconfig>", "The path to the tsconfig file.").option("--skip-db", "Skip the database setup.").option("--skip-plugins", "Skip the plugins setup.").option("--package-manager <package-manager>", "The package manager you want to use.").action(initAction);
|
|
1255
|
+
async function getLatestNpmVersion(packageName) {
|
|
1256
|
+
try {
|
|
1257
|
+
const response = await fetch(`https://registry.npmjs.org/${packageName}`);
|
|
1258
|
+
if (!response.ok) throw new Error(`Package not found: ${response.statusText}`);
|
|
1259
|
+
return (await response.json())["dist-tags"].latest;
|
|
1260
|
+
} catch (error) {
|
|
1261
|
+
throw error?.message;
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
async function getPackageManager$1() {
|
|
1265
|
+
const { hasBun, hasPnpm } = await checkPackageManagers();
|
|
1266
|
+
if (!hasBun && !hasPnpm) return "npm";
|
|
1267
|
+
const packageManagerOptions = [];
|
|
1268
|
+
if (hasPnpm) packageManagerOptions.push({
|
|
1269
|
+
value: "pnpm",
|
|
1270
|
+
label: "pnpm",
|
|
1271
|
+
hint: "recommended"
|
|
1272
|
+
});
|
|
1273
|
+
if (hasBun) packageManagerOptions.push({
|
|
1274
|
+
value: "bun",
|
|
1275
|
+
label: "bun"
|
|
1276
|
+
});
|
|
1277
|
+
packageManagerOptions.push({
|
|
1278
|
+
value: "npm",
|
|
1279
|
+
hint: "not recommended"
|
|
1280
|
+
});
|
|
1281
|
+
let packageManager = await select({
|
|
1282
|
+
message: "Choose a package manager",
|
|
1283
|
+
options: packageManagerOptions
|
|
1284
|
+
});
|
|
1285
|
+
if (isCancel(packageManager)) {
|
|
1286
|
+
cancel(`Operation cancelled.`);
|
|
1287
|
+
process.exit(0);
|
|
1288
|
+
}
|
|
1289
|
+
return packageManager;
|
|
1290
|
+
}
|
|
1291
|
+
async function getEnvFiles(cwd) {
|
|
1292
|
+
return (await fs$1.readdir(cwd)).filter((x) => x.startsWith(".env"));
|
|
1293
|
+
}
|
|
1294
|
+
async function updateEnvs({ envs, files, isCommented }) {
|
|
1295
|
+
let previouslyGeneratedSecret = null;
|
|
1296
|
+
for (const file of files) {
|
|
1297
|
+
const lines = (await fs$1.readFile(file, "utf8")).split("\n");
|
|
1298
|
+
const newLines = envs.map((x) => `${isCommented ? "# " : ""}${x}=${getEnvDescription(x) ?? `"some_value"`}`);
|
|
1299
|
+
newLines.push("");
|
|
1300
|
+
newLines.push(...lines);
|
|
1301
|
+
await fs$1.writeFile(file, newLines.join("\n"), "utf8");
|
|
1302
|
+
}
|
|
1303
|
+
function getEnvDescription(env) {
|
|
1304
|
+
if (env === "DATABASE_HOST") return `"The host of your database"`;
|
|
1305
|
+
if (env === "DATABASE_PORT") return `"The port of your database"`;
|
|
1306
|
+
if (env === "DATABASE_USER") return `"The username of your database"`;
|
|
1307
|
+
if (env === "DATABASE_PASSWORD") return `"The password of your database"`;
|
|
1308
|
+
if (env === "DATABASE_NAME") return `"The name of your database"`;
|
|
1309
|
+
if (env === "DATABASE_URL") return `"The URL of your database"`;
|
|
1310
|
+
if (env === "BETTER_AUTH_SECRET") {
|
|
1311
|
+
previouslyGeneratedSecret = previouslyGeneratedSecret ?? generateSecretHash();
|
|
1312
|
+
return `"${previouslyGeneratedSecret}"`;
|
|
1313
|
+
}
|
|
1314
|
+
if (env === "BETTER_AUTH_URL") return `"http://localhost:3000" # Your APP URL`;
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
//#endregion
|
|
1319
|
+
//#region src/utils/add-svelte-kit-env-modules.ts
|
|
1320
|
+
/**
|
|
1321
|
+
* Adds SvelteKit environment modules and path aliases
|
|
1322
|
+
* @param aliases - The aliases object to populate
|
|
1323
|
+
* @param cwd - Current working directory (optional, defaults to process.cwd())
|
|
1324
|
+
*/
|
|
1325
|
+
function addSvelteKitEnvModules(aliases, cwd) {
|
|
1326
|
+
const workingDir = cwd || process.cwd();
|
|
1327
|
+
aliases["$env/dynamic/private"] = createDataUriModule(createDynamicEnvModule());
|
|
1328
|
+
aliases["$env/dynamic/public"] = createDataUriModule(createDynamicEnvModule());
|
|
1329
|
+
aliases["$env/static/private"] = createDataUriModule(createStaticEnvModule(filterPrivateEnv("PUBLIC_", "")));
|
|
1330
|
+
aliases["$env/static/public"] = createDataUriModule(createStaticEnvModule(filterPublicEnv("PUBLIC_", "")));
|
|
1331
|
+
const svelteKitAliases = getSvelteKitPathAliases(workingDir);
|
|
1332
|
+
Object.assign(aliases, svelteKitAliases);
|
|
1333
|
+
}
|
|
1334
|
+
function getSvelteKitPathAliases(cwd) {
|
|
1335
|
+
const aliases = {};
|
|
1336
|
+
const packageJsonPath = path.join(cwd, "package.json");
|
|
1337
|
+
const svelteConfigPath = path.join(cwd, "svelte.config.js");
|
|
1338
|
+
const svelteConfigTsPath = path.join(cwd, "svelte.config.ts");
|
|
1339
|
+
let isSvelteKitProject = false;
|
|
1340
|
+
if (fs.existsSync(packageJsonPath)) try {
|
|
1341
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
1342
|
+
isSvelteKitProject = !!{
|
|
1343
|
+
...packageJson.dependencies,
|
|
1344
|
+
...packageJson.devDependencies
|
|
1345
|
+
}["@sveltejs/kit"];
|
|
1346
|
+
} catch {}
|
|
1347
|
+
if (!isSvelteKitProject) isSvelteKitProject = fs.existsSync(svelteConfigPath) || fs.existsSync(svelteConfigTsPath);
|
|
1348
|
+
if (!isSvelteKitProject) return aliases;
|
|
1349
|
+
const libPaths = [path.join(cwd, "src", "lib"), path.join(cwd, "lib")];
|
|
1350
|
+
for (const libPath of libPaths) if (fs.existsSync(libPath)) {
|
|
1351
|
+
aliases["$lib"] = libPath;
|
|
1352
|
+
for (const subPath of [
|
|
1353
|
+
"server",
|
|
1354
|
+
"utils",
|
|
1355
|
+
"components",
|
|
1356
|
+
"stores"
|
|
1357
|
+
]) {
|
|
1358
|
+
const subDir = path.join(libPath, subPath);
|
|
1359
|
+
if (fs.existsSync(subDir)) aliases[`$lib/${subPath}`] = subDir;
|
|
1360
|
+
}
|
|
1361
|
+
break;
|
|
1362
|
+
}
|
|
1363
|
+
aliases["$app/server"] = createDataUriModule(createAppServerModule());
|
|
1364
|
+
const customAliases = getSvelteConfigAliases(cwd);
|
|
1365
|
+
Object.assign(aliases, customAliases);
|
|
1366
|
+
return aliases;
|
|
1367
|
+
}
|
|
1368
|
+
function getSvelteConfigAliases(cwd) {
|
|
1369
|
+
const aliases = {};
|
|
1370
|
+
const configPaths = [path.join(cwd, "svelte.config.js"), path.join(cwd, "svelte.config.ts")];
|
|
1371
|
+
for (const configPath of configPaths) if (fs.existsSync(configPath)) {
|
|
1372
|
+
try {
|
|
1373
|
+
const aliasMatch = fs.readFileSync(configPath, "utf-8").match(/alias\s*:\s*\{([^}]+)\}/);
|
|
1374
|
+
if (aliasMatch && aliasMatch[1]) {
|
|
1375
|
+
const aliasMatches = aliasMatch[1].matchAll(/['"`](\$[^'"`]+)['"`]\s*:\s*['"`]([^'"`]+)['"`]/g);
|
|
1376
|
+
for (const match of aliasMatches) {
|
|
1377
|
+
const [, alias, target] = match;
|
|
1378
|
+
if (alias && target) {
|
|
1379
|
+
aliases[alias + "/*"] = path.resolve(cwd, target) + "/*";
|
|
1380
|
+
aliases[alias] = path.resolve(cwd, target);
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
} catch {}
|
|
1385
|
+
break;
|
|
1386
|
+
}
|
|
1387
|
+
return aliases;
|
|
1388
|
+
}
|
|
1389
|
+
function createAppServerModule() {
|
|
1390
|
+
return `
|
|
1391
|
+
// $app/server stub for CLI compatibility
|
|
1392
|
+
export default {};
|
|
1393
|
+
// jiti dirty hack: .unknown
|
|
1394
|
+
`;
|
|
1395
|
+
}
|
|
1396
|
+
function createDataUriModule(module) {
|
|
1397
|
+
return `data:text/javascript;charset=utf-8,${encodeURIComponent(module)}`;
|
|
1398
|
+
}
|
|
1399
|
+
function createStaticEnvModule(env) {
|
|
1400
|
+
return `
|
|
1401
|
+
${Object.keys(env).filter((k) => validIdentifier.test(k) && !reserved.has(k)).map((k) => `export const ${k} = ${JSON.stringify(env[k])};`).join("\n")}
|
|
1402
|
+
// jiti dirty hack: .unknown
|
|
1403
|
+
`;
|
|
1404
|
+
}
|
|
1405
|
+
function createDynamicEnvModule() {
|
|
1406
|
+
return `
|
|
1407
|
+
export const env = process.env;
|
|
1408
|
+
// jiti dirty hack: .unknown
|
|
1409
|
+
`;
|
|
1410
|
+
}
|
|
1411
|
+
function filterPrivateEnv(publicPrefix, privatePrefix) {
|
|
1412
|
+
return Object.fromEntries(Object.entries(process.env).filter(([k]) => k.startsWith(privatePrefix) && (publicPrefix === "" || !k.startsWith(publicPrefix))));
|
|
1413
|
+
}
|
|
1414
|
+
function filterPublicEnv(publicPrefix, privatePrefix) {
|
|
1415
|
+
return Object.fromEntries(Object.entries(process.env).filter(([k]) => k.startsWith(publicPrefix) && (privatePrefix === "" || !k.startsWith(privatePrefix))));
|
|
1416
|
+
}
|
|
1417
|
+
const validIdentifier = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
|
|
1418
|
+
const reserved = new Set([
|
|
1419
|
+
"do",
|
|
1420
|
+
"if",
|
|
1421
|
+
"in",
|
|
1422
|
+
"for",
|
|
1423
|
+
"let",
|
|
1424
|
+
"new",
|
|
1425
|
+
"try",
|
|
1426
|
+
"var",
|
|
1427
|
+
"case",
|
|
1428
|
+
"else",
|
|
1429
|
+
"enum",
|
|
1430
|
+
"eval",
|
|
1431
|
+
"null",
|
|
1432
|
+
"this",
|
|
1433
|
+
"true",
|
|
1434
|
+
"void",
|
|
1435
|
+
"with",
|
|
1436
|
+
"await",
|
|
1437
|
+
"break",
|
|
1438
|
+
"catch",
|
|
1439
|
+
"class",
|
|
1440
|
+
"const",
|
|
1441
|
+
"false",
|
|
1442
|
+
"super",
|
|
1443
|
+
"throw",
|
|
1444
|
+
"while",
|
|
1445
|
+
"yield",
|
|
1446
|
+
"delete",
|
|
1447
|
+
"export",
|
|
1448
|
+
"import",
|
|
1449
|
+
"public",
|
|
1450
|
+
"return",
|
|
1451
|
+
"static",
|
|
1452
|
+
"switch",
|
|
1453
|
+
"typeof",
|
|
1454
|
+
"default",
|
|
1455
|
+
"extends",
|
|
1456
|
+
"finally",
|
|
1457
|
+
"package",
|
|
1458
|
+
"private",
|
|
1459
|
+
"continue",
|
|
1460
|
+
"debugger",
|
|
1461
|
+
"function",
|
|
1462
|
+
"arguments",
|
|
1463
|
+
"interface",
|
|
1464
|
+
"protected",
|
|
1465
|
+
"implements",
|
|
1466
|
+
"instanceof"
|
|
1467
|
+
]);
|
|
1468
|
+
|
|
1469
|
+
//#endregion
|
|
1470
|
+
//#region src/utils/get-config.ts
|
|
1471
|
+
let possiblePaths = [
|
|
1472
|
+
"auth.ts",
|
|
1473
|
+
"auth.tsx",
|
|
1474
|
+
"auth.js",
|
|
1475
|
+
"auth.jsx",
|
|
1476
|
+
"auth.server.js",
|
|
1477
|
+
"auth.server.ts"
|
|
1478
|
+
];
|
|
1479
|
+
possiblePaths = [
|
|
1480
|
+
...possiblePaths,
|
|
1481
|
+
...possiblePaths.map((it) => `lib/server/${it}`),
|
|
1482
|
+
...possiblePaths.map((it) => `server/${it}`),
|
|
1483
|
+
...possiblePaths.map((it) => `lib/${it}`),
|
|
1484
|
+
...possiblePaths.map((it) => `utils/${it}`)
|
|
1485
|
+
];
|
|
1486
|
+
possiblePaths = [
|
|
1487
|
+
...possiblePaths,
|
|
1488
|
+
...possiblePaths.map((it) => `src/${it}`),
|
|
1489
|
+
...possiblePaths.map((it) => `app/${it}`)
|
|
1490
|
+
];
|
|
1491
|
+
function resolveReferencePath(configDir, refPath) {
|
|
1492
|
+
const resolvedPath = path.resolve(configDir, refPath);
|
|
1493
|
+
if (refPath.endsWith(".json")) return resolvedPath;
|
|
1494
|
+
if (fs.existsSync(resolvedPath)) try {
|
|
1495
|
+
if (fs.statSync(resolvedPath).isFile()) return resolvedPath;
|
|
1496
|
+
} catch {}
|
|
1497
|
+
return path.resolve(configDir, refPath, "tsconfig.json");
|
|
1498
|
+
}
|
|
1499
|
+
function getPathAliasesRecursive(tsconfigPath, visited = /* @__PURE__ */ new Set()) {
|
|
1500
|
+
if (visited.has(tsconfigPath)) return {};
|
|
1501
|
+
visited.add(tsconfigPath);
|
|
1502
|
+
if (!fs.existsSync(tsconfigPath)) {
|
|
1503
|
+
logger.warn(`Referenced tsconfig not found: ${tsconfigPath}`);
|
|
1504
|
+
return {};
|
|
1505
|
+
}
|
|
1506
|
+
try {
|
|
1507
|
+
const tsConfig = getTsconfigInfo(void 0, tsconfigPath);
|
|
1508
|
+
const { paths = {}, baseUrl = "." } = tsConfig.compilerOptions || {};
|
|
1509
|
+
const result = {};
|
|
1510
|
+
const configDir = path.dirname(tsconfigPath);
|
|
1511
|
+
const obj = Object.entries(paths);
|
|
1512
|
+
for (const [alias, aliasPaths] of obj) for (const aliasedPath of aliasPaths) {
|
|
1513
|
+
const resolvedBaseUrl = path.resolve(configDir, baseUrl);
|
|
1514
|
+
const finalAlias = alias.slice(-1) === "*" ? alias.slice(0, -1) : alias;
|
|
1515
|
+
const finalAliasedPath = aliasedPath.slice(-1) === "*" ? aliasedPath.slice(0, -1) : aliasedPath;
|
|
1516
|
+
result[finalAlias || ""] = path.join(resolvedBaseUrl, finalAliasedPath);
|
|
1517
|
+
}
|
|
1518
|
+
if (tsConfig.references) for (const ref of tsConfig.references) {
|
|
1519
|
+
const refAliases = getPathAliasesRecursive(resolveReferencePath(configDir, ref.path), visited);
|
|
1520
|
+
for (const [alias, aliasPath] of Object.entries(refAliases)) if (!(alias in result)) result[alias] = aliasPath;
|
|
1521
|
+
}
|
|
1522
|
+
return result;
|
|
1523
|
+
} catch (error) {
|
|
1524
|
+
logger.warn(`Error parsing tsconfig at ${tsconfigPath}: ${error}`);
|
|
1525
|
+
return {};
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
function getPathAliases(cwd) {
|
|
1529
|
+
const tsConfigPath = path.join(cwd, "tsconfig.json");
|
|
1530
|
+
if (!fs.existsSync(tsConfigPath)) return null;
|
|
1531
|
+
try {
|
|
1532
|
+
const result = getPathAliasesRecursive(tsConfigPath);
|
|
1533
|
+
addSvelteKitEnvModules(result);
|
|
1534
|
+
return result;
|
|
1535
|
+
} catch (error) {
|
|
1536
|
+
console.error(error);
|
|
1537
|
+
throw new BetterAuthError("Error parsing tsconfig.json");
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
/**
|
|
1541
|
+
* .tsx files are not supported by Jiti.
|
|
1542
|
+
*/
|
|
1543
|
+
const jitiOptions = (cwd) => {
|
|
1544
|
+
const alias = getPathAliases(cwd) || {};
|
|
1545
|
+
return {
|
|
1546
|
+
transformOptions: { babel: { presets: [[babelPresetTypeScript, {
|
|
1547
|
+
isTSX: true,
|
|
1548
|
+
allExtensions: true
|
|
1549
|
+
}], [babelPresetReact, { runtime: "automatic" }]] } },
|
|
1550
|
+
extensions: [
|
|
1551
|
+
".ts",
|
|
1552
|
+
".tsx",
|
|
1553
|
+
".js",
|
|
1554
|
+
".jsx"
|
|
1555
|
+
],
|
|
1556
|
+
alias
|
|
1557
|
+
};
|
|
1558
|
+
};
|
|
1559
|
+
const isDefaultExport = (object) => {
|
|
1560
|
+
return typeof object === "object" && object !== null && !Array.isArray(object) && Object.keys(object).length > 0 && "options" in object;
|
|
1561
|
+
};
|
|
1562
|
+
async function getConfig({ cwd, configPath, shouldThrowOnError = false }) {
|
|
1563
|
+
try {
|
|
1564
|
+
let configFile = null;
|
|
1565
|
+
if (configPath) {
|
|
1566
|
+
let resolvedPath = path.join(cwd, configPath);
|
|
1567
|
+
if (existsSync(configPath)) resolvedPath = configPath;
|
|
1568
|
+
const { config } = await loadConfig({
|
|
1569
|
+
configFile: resolvedPath,
|
|
1570
|
+
dotenv: true,
|
|
1571
|
+
jitiOptions: jitiOptions(cwd)
|
|
1572
|
+
});
|
|
1573
|
+
if (!("auth" in config) && !isDefaultExport(config)) {
|
|
1574
|
+
if (shouldThrowOnError) throw new Error(`Couldn't read your auth config in ${resolvedPath}. Make sure to default export your auth instance or to export as a variable named auth.`);
|
|
1575
|
+
logger.error(`[#better-auth]: Couldn't read your auth config in ${resolvedPath}. Make sure to default export your auth instance or to export as a variable named auth.`);
|
|
1576
|
+
process.exit(1);
|
|
1577
|
+
}
|
|
1578
|
+
configFile = "auth" in config ? config.auth?.options : config.options;
|
|
1579
|
+
}
|
|
1580
|
+
if (!configFile) for (const possiblePath of possiblePaths) try {
|
|
1581
|
+
const { config } = await loadConfig({
|
|
1582
|
+
configFile: possiblePath,
|
|
1583
|
+
jitiOptions: jitiOptions(cwd)
|
|
1584
|
+
});
|
|
1585
|
+
if (Object.keys(config).length > 0) {
|
|
1586
|
+
configFile = config.auth?.options || config.default?.options || null;
|
|
1587
|
+
if (!configFile) {
|
|
1588
|
+
if (shouldThrowOnError) throw new Error("Couldn't read your auth config. Make sure to default export your auth instance or to export as a variable named auth.");
|
|
1589
|
+
logger.error("[#better-auth]: Couldn't read your auth config.");
|
|
1590
|
+
console.log("");
|
|
1591
|
+
logger.info("[#better-auth]: Make sure to default export your auth instance or to export as a variable named auth.");
|
|
1592
|
+
process.exit(1);
|
|
1593
|
+
}
|
|
1594
|
+
break;
|
|
1595
|
+
}
|
|
1596
|
+
} catch (e) {
|
|
1597
|
+
if (typeof e === "object" && e && "message" in e && typeof e.message === "string" && e.message.includes("This module cannot be imported from a Client Component module")) {
|
|
1598
|
+
if (shouldThrowOnError) throw new Error(`Please remove import 'server-only' from your auth config file temporarily. The CLI cannot resolve the configuration with it included. You can re-add it after running the CLI.`);
|
|
1599
|
+
logger.error(`Please remove import 'server-only' from your auth config file temporarily. The CLI cannot resolve the configuration with it included. You can re-add it after running the CLI.`);
|
|
1600
|
+
process.exit(1);
|
|
1601
|
+
}
|
|
1602
|
+
if (shouldThrowOnError) throw e;
|
|
1603
|
+
logger.error("[#better-auth]: Couldn't read your auth config.", e);
|
|
1604
|
+
process.exit(1);
|
|
1605
|
+
}
|
|
1606
|
+
return configFile;
|
|
1607
|
+
} catch (e) {
|
|
1608
|
+
if (typeof e === "object" && e && "message" in e && typeof e.message === "string" && e.message.includes("This module cannot be imported from a Client Component module")) {
|
|
1609
|
+
if (shouldThrowOnError) throw new Error(`Please remove import 'server-only' from your auth config file temporarily. The CLI cannot resolve the configuration with it included. You can re-add it after running the CLI.`);
|
|
1610
|
+
logger.error(`Please remove import 'server-only' from your auth config file temporarily. The CLI cannot resolve the configuration with it included. You can re-add it after running the CLI.`);
|
|
1611
|
+
process.exit(1);
|
|
1612
|
+
}
|
|
1613
|
+
if (shouldThrowOnError) throw e;
|
|
1614
|
+
logger.error("Couldn't read your auth config.", e);
|
|
1615
|
+
process.exit(1);
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
//#endregion
|
|
1620
|
+
//#region src/commands/migrate.ts
|
|
1621
|
+
async function migrateAction(opts) {
|
|
1622
|
+
const options = z.object({
|
|
1623
|
+
cwd: z.string(),
|
|
1624
|
+
config: z.string().optional(),
|
|
1625
|
+
y: z.boolean().optional(),
|
|
1626
|
+
yes: z.boolean().optional()
|
|
1627
|
+
}).parse(opts);
|
|
1628
|
+
const cwd = path.resolve(options.cwd);
|
|
1629
|
+
if (!existsSync(cwd)) {
|
|
1630
|
+
logger.error(`The directory "${cwd}" does not exist.`);
|
|
1631
|
+
process.exit(1);
|
|
1632
|
+
}
|
|
1633
|
+
const config = await getConfig({
|
|
1634
|
+
cwd,
|
|
1635
|
+
configPath: options.config
|
|
1636
|
+
});
|
|
1637
|
+
if (!config) {
|
|
1638
|
+
logger.error("No configuration file found. Add a `auth.ts` file to your project or pass the path to the configuration file using the `--config` flag.");
|
|
1639
|
+
return;
|
|
1640
|
+
}
|
|
1641
|
+
const db = await getAdapter(config);
|
|
1642
|
+
if (!db) {
|
|
1643
|
+
logger.error("Invalid database configuration. Make sure you're not using adapters. Migrate command only works with built-in Kysely adapter.");
|
|
1644
|
+
process.exit(1);
|
|
1645
|
+
}
|
|
1646
|
+
if (db.id !== "kysely") {
|
|
1647
|
+
if (db.id === "prisma") {
|
|
1648
|
+
logger.error("The migrate command only works with the built-in Kysely adapter. For Prisma, run `npx @better-auth/cli generate` to create the schema, then use Prismaâs migrate or push to apply it.");
|
|
1649
|
+
try {
|
|
1650
|
+
await (await createTelemetry(config)).publish({
|
|
1651
|
+
type: "cli_migrate",
|
|
1652
|
+
payload: {
|
|
1653
|
+
outcome: "unsupported_adapter",
|
|
1654
|
+
adapter: "prisma",
|
|
1655
|
+
config: getTelemetryAuthConfig(config)
|
|
1656
|
+
}
|
|
1657
|
+
});
|
|
1658
|
+
} catch {}
|
|
1659
|
+
process.exit(0);
|
|
1660
|
+
}
|
|
1661
|
+
if (db.id === "drizzle") {
|
|
1662
|
+
logger.error("The migrate command only works with the built-in Kysely adapter. For Drizzle, run `npx @better-auth/cli generate` to create the schema, then use Drizzleâs migrate or push to apply it.");
|
|
1663
|
+
try {
|
|
1664
|
+
await (await createTelemetry(config)).publish({
|
|
1665
|
+
type: "cli_migrate",
|
|
1666
|
+
payload: {
|
|
1667
|
+
outcome: "unsupported_adapter",
|
|
1668
|
+
adapter: "drizzle",
|
|
1669
|
+
config: getTelemetryAuthConfig(config)
|
|
1670
|
+
}
|
|
1671
|
+
});
|
|
1672
|
+
} catch {}
|
|
1673
|
+
process.exit(0);
|
|
1674
|
+
}
|
|
1675
|
+
logger.error("Migrate command isn't supported for this adapter.");
|
|
1676
|
+
try {
|
|
1677
|
+
await (await createTelemetry(config)).publish({
|
|
1678
|
+
type: "cli_migrate",
|
|
1679
|
+
payload: {
|
|
1680
|
+
outcome: "unsupported_adapter",
|
|
1681
|
+
adapter: db.id,
|
|
1682
|
+
config: getTelemetryAuthConfig(config)
|
|
1683
|
+
}
|
|
1684
|
+
});
|
|
1685
|
+
} catch {}
|
|
1686
|
+
process.exit(1);
|
|
1687
|
+
}
|
|
1688
|
+
const spinner$1 = yoctoSpinner({ text: "preparing migration..." }).start();
|
|
1689
|
+
const { toBeAdded, toBeCreated, runMigrations } = await getMigrations(config);
|
|
1690
|
+
if (!toBeAdded.length && !toBeCreated.length) {
|
|
1691
|
+
spinner$1.stop();
|
|
1692
|
+
logger.info("đ No migrations needed.");
|
|
1693
|
+
try {
|
|
1694
|
+
await (await createTelemetry(config)).publish({
|
|
1695
|
+
type: "cli_migrate",
|
|
1696
|
+
payload: {
|
|
1697
|
+
outcome: "no_changes",
|
|
1698
|
+
config: getTelemetryAuthConfig(config)
|
|
1699
|
+
}
|
|
1700
|
+
});
|
|
1701
|
+
} catch {}
|
|
1702
|
+
process.exit(0);
|
|
1703
|
+
}
|
|
1704
|
+
spinner$1.stop();
|
|
1705
|
+
logger.info(`đ The migration will affect the following:`);
|
|
1706
|
+
for (const table of [...toBeCreated, ...toBeAdded]) console.log("->", chalk.magenta(Object.keys(table.fields).join(", ")), chalk.white("fields on"), chalk.yellow(`${table.table}`), chalk.white("table."));
|
|
1707
|
+
if (options.y) {
|
|
1708
|
+
console.warn("WARNING: --y is deprecated. Consider -y or --yes");
|
|
1709
|
+
options.yes = true;
|
|
1710
|
+
}
|
|
1711
|
+
let migrate$1 = options.yes;
|
|
1712
|
+
if (!migrate$1) migrate$1 = (await prompts({
|
|
1713
|
+
type: "confirm",
|
|
1714
|
+
name: "migrate",
|
|
1715
|
+
message: "Are you sure you want to run these migrations?",
|
|
1716
|
+
initial: false
|
|
1717
|
+
})).migrate;
|
|
1718
|
+
if (!migrate$1) {
|
|
1719
|
+
logger.info("Migration cancelled.");
|
|
1720
|
+
try {
|
|
1721
|
+
await (await createTelemetry(config)).publish({
|
|
1722
|
+
type: "cli_migrate",
|
|
1723
|
+
payload: {
|
|
1724
|
+
outcome: "aborted",
|
|
1725
|
+
config: getTelemetryAuthConfig(config)
|
|
1726
|
+
}
|
|
1727
|
+
});
|
|
1728
|
+
} catch {}
|
|
1729
|
+
process.exit(0);
|
|
1730
|
+
}
|
|
1731
|
+
spinner$1?.start("migrating...");
|
|
1732
|
+
await runMigrations();
|
|
1733
|
+
spinner$1.stop();
|
|
1734
|
+
logger.info("đ migration was completed successfully!");
|
|
1735
|
+
try {
|
|
1736
|
+
await (await createTelemetry(config)).publish({
|
|
1737
|
+
type: "cli_migrate",
|
|
1738
|
+
payload: {
|
|
1739
|
+
outcome: "migrated",
|
|
1740
|
+
config: getTelemetryAuthConfig(config)
|
|
1741
|
+
}
|
|
1742
|
+
});
|
|
1743
|
+
} catch {}
|
|
1744
|
+
process.exit(0);
|
|
1745
|
+
}
|
|
1746
|
+
const migrate = new Command("migrate").option("-c, --cwd <cwd>", "the working directory. defaults to the current directory.", process.cwd()).option("--config <config>", "the path to the configuration file. defaults to the first configuration file found.").option("-y, --yes", "automatically accept and run migrations without prompting", false).option("--y", "(deprecated) same as --yes", false).action(migrateAction);
|
|
1747
|
+
|
|
1748
|
+
//#endregion
|
|
1749
|
+
//#region src/generators/drizzle.ts
|
|
1750
|
+
function convertToSnakeCase(str, camelCase) {
|
|
1751
|
+
if (camelCase) return str;
|
|
1752
|
+
return str.replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2").replace(/([a-z\d])([A-Z])/g, "$1_$2").toLowerCase();
|
|
1753
|
+
}
|
|
1754
|
+
const generateDrizzleSchema = async ({ options, file, adapter }) => {
|
|
1755
|
+
const tables = getAuthTables(options);
|
|
1756
|
+
const filePath = file || "./auth-schema.ts";
|
|
1757
|
+
const databaseType = adapter.options?.provider;
|
|
1758
|
+
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`);
|
|
1759
|
+
const fileExist = existsSync(filePath);
|
|
1760
|
+
let code = generateImport({
|
|
1761
|
+
databaseType,
|
|
1762
|
+
tables,
|
|
1763
|
+
options
|
|
1764
|
+
});
|
|
1765
|
+
for (const tableKey in tables) {
|
|
1766
|
+
const table = tables[tableKey];
|
|
1767
|
+
const modelName = getModelName(table.modelName, adapter.options);
|
|
1768
|
+
const fields = table.fields;
|
|
1769
|
+
function getType(name, field) {
|
|
1770
|
+
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`);
|
|
1771
|
+
name = convertToSnakeCase(name, adapter.options?.camelCase);
|
|
1772
|
+
if (field.references?.field === "id") {
|
|
1773
|
+
if (options.advanced?.database?.useNumberId) if (databaseType === "pg") return `integer('${name}')`;
|
|
1774
|
+
else if (databaseType === "mysql") return `int('${name}')`;
|
|
1775
|
+
else return `integer('${name}')`;
|
|
1776
|
+
if (field.references.field) {
|
|
1777
|
+
if (databaseType === "mysql") return `varchar('${name}', { length: 36 })`;
|
|
1778
|
+
}
|
|
1779
|
+
return `text('${name}')`;
|
|
1780
|
+
}
|
|
1781
|
+
const type = field.type;
|
|
1782
|
+
return {
|
|
1783
|
+
string: {
|
|
1784
|
+
sqlite: `text('${name}')`,
|
|
1785
|
+
pg: `text('${name}')`,
|
|
1786
|
+
mysql: field.unique ? `varchar('${name}', { length: 255 })` : field.references ? `varchar('${name}', { length: 36 })` : `text('${name}')`
|
|
1787
|
+
},
|
|
1788
|
+
boolean: {
|
|
1789
|
+
sqlite: `integer('${name}', { mode: 'boolean' })`,
|
|
1790
|
+
pg: `boolean('${name}')`,
|
|
1791
|
+
mysql: `boolean('${name}')`
|
|
1792
|
+
},
|
|
1793
|
+
number: {
|
|
1794
|
+
sqlite: `integer('${name}')`,
|
|
1795
|
+
pg: field.bigint ? `bigint('${name}', { mode: 'number' })` : `integer('${name}')`,
|
|
1796
|
+
mysql: field.bigint ? `bigint('${name}', { mode: 'number' })` : `int('${name}')`
|
|
1797
|
+
},
|
|
1798
|
+
date: {
|
|
1799
|
+
sqlite: `integer('${name}', { mode: 'timestamp_ms' })`,
|
|
1800
|
+
pg: `timestamp('${name}')`,
|
|
1801
|
+
mysql: `timestamp('${name}', { fsp: 3 })`
|
|
1802
|
+
},
|
|
1803
|
+
"number[]": {
|
|
1804
|
+
sqlite: `integer('${name}').array()`,
|
|
1805
|
+
pg: field.bigint ? `bigint('${name}', { mode: 'number' }).array()` : `integer('${name}').array()`,
|
|
1806
|
+
mysql: field.bigint ? `bigint('${name}', { mode: 'number' }).array()` : `int('${name}').array()`
|
|
1807
|
+
},
|
|
1808
|
+
"string[]": {
|
|
1809
|
+
sqlite: `text('${name}').array()`,
|
|
1810
|
+
pg: `text('${name}').array()`,
|
|
1811
|
+
mysql: `text('${name}').array()`
|
|
1812
|
+
},
|
|
1813
|
+
json: {
|
|
1814
|
+
sqlite: `text('${name}')`,
|
|
1815
|
+
pg: `jsonb('${name}')`,
|
|
1816
|
+
mysql: `json('${name}')`
|
|
1817
|
+
}
|
|
1818
|
+
}[type][databaseType];
|
|
1819
|
+
}
|
|
1820
|
+
let id = "";
|
|
1821
|
+
if (options.advanced?.database?.useNumberId) if (databaseType === "pg") id = `serial("id").primaryKey()`;
|
|
1822
|
+
else if (databaseType === "sqlite") id = `integer("id", { mode: "number" }).primaryKey({ autoIncrement: true })`;
|
|
1823
|
+
else id = `int("id").autoincrement().primaryKey()`;
|
|
1824
|
+
else if (databaseType === "mysql") id = `varchar('id', { length: 36 }).primaryKey()`;
|
|
1825
|
+
else if (databaseType === "pg") id = `text('id').primaryKey()`;
|
|
1826
|
+
else id = `text('id').primaryKey()`;
|
|
1827
|
+
const schema = `export const ${modelName} = ${databaseType}Table("${convertToSnakeCase(modelName, adapter.options?.camelCase)}", {
|
|
1828
|
+
id: ${id},
|
|
1829
|
+
${Object.keys(fields).map((field) => {
|
|
1830
|
+
const attr = fields[field];
|
|
1831
|
+
const fieldName = attr.fieldName || field;
|
|
1832
|
+
let type = getType(fieldName, attr);
|
|
1833
|
+
if (attr.defaultValue !== null && typeof attr.defaultValue !== "undefined") if (typeof attr.defaultValue === "function") {
|
|
1834
|
+
if (attr.type === "date" && attr.defaultValue.toString().includes("new Date()")) if (databaseType === "sqlite") type += `.default(sql\`(cast(unixepoch('subsecond') * 1000 as integer))\`)`;
|
|
1835
|
+
else type += `.defaultNow()`;
|
|
1836
|
+
} else if (typeof attr.defaultValue === "string") type += `.default("${attr.defaultValue}")`;
|
|
1837
|
+
else type += `.default(${attr.defaultValue})`;
|
|
1838
|
+
if (attr.onUpdate && attr.type === "date") {
|
|
1839
|
+
if (typeof attr.onUpdate === "function") type += `.$onUpdate(${attr.onUpdate})`;
|
|
1840
|
+
}
|
|
1841
|
+
return `${fieldName}: ${type}${attr.required ? ".notNull()" : ""}${attr.unique ? ".unique()" : ""}${attr.references ? `.references(()=> ${getModelName(tables[attr.references.model]?.modelName || attr.references.model, adapter.options)}.${fields[attr.references.field]?.fieldName || attr.references.field}, { onDelete: '${attr.references.onDelete || "cascade"}' })` : ""}`;
|
|
1842
|
+
}).join(",\n ")}
|
|
1843
|
+
});`;
|
|
1844
|
+
code += `\n${schema}\n`;
|
|
1845
|
+
}
|
|
1846
|
+
return {
|
|
1847
|
+
code: await prettier.format(code, { parser: "typescript" }),
|
|
1848
|
+
fileName: filePath,
|
|
1849
|
+
overwrite: fileExist
|
|
1850
|
+
};
|
|
1851
|
+
};
|
|
1852
|
+
function generateImport({ databaseType, tables, options }) {
|
|
1853
|
+
const rootImports = [];
|
|
1854
|
+
const coreImports = [];
|
|
1855
|
+
let hasBigint = false;
|
|
1856
|
+
let hasJson = false;
|
|
1857
|
+
for (const table of Object.values(tables)) {
|
|
1858
|
+
for (const field of Object.values(table.fields)) {
|
|
1859
|
+
if (field.bigint) hasBigint = true;
|
|
1860
|
+
if (field.type === "json") hasJson = true;
|
|
1861
|
+
}
|
|
1862
|
+
if (hasJson && hasBigint) break;
|
|
1863
|
+
}
|
|
1864
|
+
const useNumberId = options.advanced?.database?.useNumberId;
|
|
1865
|
+
coreImports.push(`${databaseType}Table`);
|
|
1866
|
+
coreImports.push(databaseType === "mysql" ? "varchar, text" : databaseType === "pg" ? "text" : "text");
|
|
1867
|
+
coreImports.push(hasBigint ? databaseType !== "sqlite" ? "bigint" : "" : "");
|
|
1868
|
+
coreImports.push(databaseType !== "sqlite" ? "timestamp, boolean" : "");
|
|
1869
|
+
if (databaseType === "mysql") {
|
|
1870
|
+
const hasNonBigintNumber = Object.values(tables).some((table) => Object.values(table.fields).some((field) => (field.type === "number" || field.type === "number[]") && !field.bigint));
|
|
1871
|
+
if (!!useNumberId || hasNonBigintNumber) coreImports.push("int");
|
|
1872
|
+
} else if (databaseType === "pg") {
|
|
1873
|
+
const hasNonBigintNumber = Object.values(tables).some((table) => Object.values(table.fields).some((field) => (field.type === "number" || field.type === "number[]") && !field.bigint));
|
|
1874
|
+
const hasFkToId = Object.values(tables).some((table) => Object.values(table.fields).some((field) => field.references?.field === "id"));
|
|
1875
|
+
if (hasNonBigintNumber || options.advanced?.database?.useNumberId && hasFkToId) coreImports.push("integer");
|
|
1876
|
+
} else coreImports.push("integer");
|
|
1877
|
+
coreImports.push(useNumberId ? databaseType === "pg" ? "serial" : "" : "");
|
|
1878
|
+
if (hasJson) {
|
|
1879
|
+
if (databaseType === "pg") coreImports.push("jsonb");
|
|
1880
|
+
if (databaseType === "mysql") coreImports.push("json");
|
|
1881
|
+
}
|
|
1882
|
+
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");
|
|
1883
|
+
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`;
|
|
1884
|
+
}
|
|
1885
|
+
function getModelName(modelName, options) {
|
|
1886
|
+
return options?.usePlural ? `${modelName}s` : modelName;
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
//#endregion
|
|
1890
|
+
//#region src/generators/prisma.ts
|
|
1891
|
+
const generatePrismaSchema = async ({ adapter, options, file }) => {
|
|
1892
|
+
const provider = adapter.options?.provider || "postgresql";
|
|
1893
|
+
const tables = getAuthTables(options);
|
|
1894
|
+
const filePath = file || "./prisma/schema.prisma";
|
|
1895
|
+
const schemaPrismaExist = existsSync(path.join(process.cwd(), filePath));
|
|
1896
|
+
let schemaPrisma = "";
|
|
1897
|
+
if (schemaPrismaExist) schemaPrisma = await fs$1.readFile(path.join(process.cwd(), filePath), "utf-8");
|
|
1898
|
+
else schemaPrisma = getNewPrisma(provider);
|
|
1899
|
+
const manyToManyRelations = /* @__PURE__ */ new Map();
|
|
1900
|
+
for (const table in tables) {
|
|
1901
|
+
const fields = tables[table]?.fields;
|
|
1902
|
+
for (const field in fields) {
|
|
1903
|
+
const attr = fields[field];
|
|
1904
|
+
if (attr.references) {
|
|
1905
|
+
const referencedOriginalModel = attr.references.model;
|
|
1906
|
+
const referencedModelNameCap = capitalizeFirstLetter(tables[referencedOriginalModel]?.modelName || referencedOriginalModel);
|
|
1907
|
+
if (!manyToManyRelations.has(referencedModelNameCap)) manyToManyRelations.set(referencedModelNameCap, /* @__PURE__ */ new Set());
|
|
1908
|
+
const currentModelNameCap = capitalizeFirstLetter(tables[table]?.modelName || table);
|
|
1909
|
+
manyToManyRelations.get(referencedModelNameCap).add(currentModelNameCap);
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
const schema = produceSchema(schemaPrisma, (builder) => {
|
|
1914
|
+
for (const table in tables) {
|
|
1915
|
+
const originalTableName = table;
|
|
1916
|
+
const customModelName = tables[table]?.modelName || table;
|
|
1917
|
+
const modelName = capitalizeFirstLetter(customModelName);
|
|
1918
|
+
const fields = tables[table]?.fields;
|
|
1919
|
+
function getType({ isBigint, isOptional, type }) {
|
|
1920
|
+
if (type === "string") return isOptional ? "String?" : "String";
|
|
1921
|
+
if (type === "number" && isBigint) return isOptional ? "BigInt?" : "BigInt";
|
|
1922
|
+
if (type === "number") return isOptional ? "Int?" : "Int";
|
|
1923
|
+
if (type === "boolean") return isOptional ? "Boolean?" : "Boolean";
|
|
1924
|
+
if (type === "date") return isOptional ? "DateTime?" : "DateTime";
|
|
1925
|
+
if (type === "json") return isOptional ? "Json?" : "Json";
|
|
1926
|
+
if (type === "string[]") return isOptional ? "String[]" : "String[]";
|
|
1927
|
+
if (type === "number[]") return isOptional ? "Int[]" : "Int[]";
|
|
1928
|
+
}
|
|
1929
|
+
const prismaModel = builder.findByType("model", { name: modelName });
|
|
1930
|
+
if (!prismaModel) if (provider === "mongodb") builder.model(modelName).field("id", "String").attribute("id").attribute(`map("_id")`);
|
|
1931
|
+
else if (options.advanced?.database?.useNumberId) builder.model(modelName).field("id", "Int").attribute("id").attribute("default(autoincrement())");
|
|
1932
|
+
else builder.model(modelName).field("id", "String").attribute("id");
|
|
1933
|
+
for (const field in fields) {
|
|
1934
|
+
const attr = fields[field];
|
|
1935
|
+
const fieldName = attr.fieldName || field;
|
|
1936
|
+
if (prismaModel) {
|
|
1937
|
+
if (builder.findByType("field", {
|
|
1938
|
+
name: fieldName,
|
|
1939
|
+
within: prismaModel.properties
|
|
1940
|
+
})) continue;
|
|
1941
|
+
}
|
|
1942
|
+
const fieldBuilder = builder.model(modelName).field(fieldName, field === "id" && options.advanced?.database?.useNumberId ? getType({
|
|
1943
|
+
isBigint: false,
|
|
1944
|
+
isOptional: false,
|
|
1945
|
+
type: "number"
|
|
1946
|
+
}) : getType({
|
|
1947
|
+
isBigint: attr?.bigint || false,
|
|
1948
|
+
isOptional: !attr?.required,
|
|
1949
|
+
type: attr.references?.field === "id" ? options.advanced?.database?.useNumberId ? "number" : "string" : attr.type
|
|
1950
|
+
}));
|
|
1951
|
+
if (field === "id") {
|
|
1952
|
+
fieldBuilder.attribute("id");
|
|
1953
|
+
if (provider === "mongodb") fieldBuilder.attribute(`map("_id")`);
|
|
1954
|
+
}
|
|
1955
|
+
if (attr.unique) builder.model(modelName).blockAttribute(`unique([${fieldName}])`);
|
|
1956
|
+
if (attr.defaultValue !== void 0) {
|
|
1957
|
+
if (field === "createdAt") fieldBuilder.attribute("default(now())");
|
|
1958
|
+
else if (typeof attr.defaultValue === "boolean") fieldBuilder.attribute(`default(${attr.defaultValue})`);
|
|
1959
|
+
else if (typeof attr.defaultValue === "function") {}
|
|
1960
|
+
}
|
|
1961
|
+
if (field === "updatedAt" && attr.onUpdate) fieldBuilder.attribute("updatedAt");
|
|
1962
|
+
else if (attr.onUpdate) {}
|
|
1963
|
+
if (attr.references) {
|
|
1964
|
+
const referencedOriginalModelName = attr.references.model;
|
|
1965
|
+
const referencedCustomModelName = tables[referencedOriginalModelName]?.modelName || referencedOriginalModelName;
|
|
1966
|
+
let action = "Cascade";
|
|
1967
|
+
if (attr.references.onDelete === "no action") action = "NoAction";
|
|
1968
|
+
else if (attr.references.onDelete === "set null") action = "SetNull";
|
|
1969
|
+
else if (attr.references.onDelete === "set default") action = "SetDefault";
|
|
1970
|
+
else if (attr.references.onDelete === "restrict") action = "Restrict";
|
|
1971
|
+
builder.model(modelName).field(`${referencedCustomModelName.toLowerCase()}`, `${capitalizeFirstLetter(referencedCustomModelName)}${!attr.required ? "?" : ""}`).attribute(`relation(fields: [${fieldName}], references: [${attr.references.field}], onDelete: ${action})`);
|
|
1972
|
+
}
|
|
1973
|
+
if (!attr.unique && !attr.references && provider === "mysql" && attr.type === "string") builder.model(modelName).field(fieldName).attribute("db.Text");
|
|
1974
|
+
}
|
|
1975
|
+
if (manyToManyRelations.has(modelName)) for (const relatedModel of manyToManyRelations.get(modelName)) {
|
|
1976
|
+
const fieldName = `${relatedModel.toLowerCase()}s`;
|
|
1977
|
+
if (!builder.findByType("field", {
|
|
1978
|
+
name: fieldName,
|
|
1979
|
+
within: prismaModel?.properties
|
|
1980
|
+
})) builder.model(modelName).field(fieldName, `${relatedModel}[]`);
|
|
1981
|
+
}
|
|
1982
|
+
const hasAttribute = builder.findByType("attribute", {
|
|
1983
|
+
name: "map",
|
|
1984
|
+
within: prismaModel?.properties
|
|
1985
|
+
});
|
|
1986
|
+
const hasChanged = customModelName !== originalTableName;
|
|
1987
|
+
if (!hasAttribute) builder.model(modelName).blockAttribute("map", `${hasChanged ? customModelName : originalTableName}`);
|
|
1988
|
+
}
|
|
1989
|
+
});
|
|
1990
|
+
const schemaChanged = schema.trim() !== schemaPrisma.trim();
|
|
1991
|
+
return {
|
|
1992
|
+
code: schemaChanged ? schema : "",
|
|
1993
|
+
fileName: filePath,
|
|
1994
|
+
overwrite: schemaPrismaExist && schemaChanged
|
|
1995
|
+
};
|
|
1996
|
+
};
|
|
1997
|
+
const getNewPrisma = (provider) => `generator client {
|
|
1998
|
+
provider = "prisma-client-js"
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
datasource db {
|
|
2002
|
+
provider = "${provider}"
|
|
2003
|
+
url = ${provider === "sqlite" ? `"file:./dev.db"` : `env("DATABASE_URL")`}
|
|
2004
|
+
}`;
|
|
2005
|
+
|
|
2006
|
+
//#endregion
|
|
2007
|
+
//#region src/generators/kysely.ts
|
|
2008
|
+
const generateMigrations = async ({ options, file }) => {
|
|
2009
|
+
const { compileMigrations } = await getMigrations(options);
|
|
2010
|
+
const migrations = await compileMigrations();
|
|
2011
|
+
return {
|
|
2012
|
+
code: migrations.trim() === ";" ? "" : migrations,
|
|
2013
|
+
fileName: file || `./better-auth_migrations/${(/* @__PURE__ */ new Date()).toISOString().replace(/:/g, "-")}.sql`
|
|
2014
|
+
};
|
|
2015
|
+
};
|
|
2016
|
+
|
|
2017
|
+
//#endregion
|
|
2018
|
+
//#region src/generators/index.ts
|
|
2019
|
+
const adapters = {
|
|
2020
|
+
prisma: generatePrismaSchema,
|
|
2021
|
+
drizzle: generateDrizzleSchema,
|
|
2022
|
+
kysely: generateMigrations
|
|
2023
|
+
};
|
|
2024
|
+
const generateSchema = (opts) => {
|
|
2025
|
+
const adapter = opts.adapter;
|
|
2026
|
+
const generator = adapter.id in adapters ? adapters[adapter.id] : null;
|
|
2027
|
+
if (generator) return generator(opts);
|
|
2028
|
+
if (adapter.createSchema) return adapter.createSchema(opts.options, opts.file).then(({ code, path: fileName, overwrite }) => ({
|
|
2029
|
+
code,
|
|
2030
|
+
fileName,
|
|
2031
|
+
overwrite
|
|
2032
|
+
}));
|
|
2033
|
+
logger.error(`${adapter.id} is not supported. If it is a custom adapter, please request the maintainer to implement createSchema`);
|
|
2034
|
+
process.exit(1);
|
|
2035
|
+
};
|
|
2036
|
+
|
|
2037
|
+
//#endregion
|
|
2038
|
+
//#region src/commands/generate.ts
|
|
2039
|
+
async function generateAction(opts) {
|
|
2040
|
+
const options = z.object({
|
|
2041
|
+
cwd: z.string(),
|
|
2042
|
+
config: z.string().optional(),
|
|
2043
|
+
output: z.string().optional(),
|
|
2044
|
+
y: z.boolean().optional(),
|
|
2045
|
+
yes: z.boolean().optional()
|
|
2046
|
+
}).parse(opts);
|
|
2047
|
+
const cwd = path.resolve(options.cwd);
|
|
2048
|
+
if (!existsSync(cwd)) {
|
|
2049
|
+
logger.error(`The directory "${cwd}" does not exist.`);
|
|
2050
|
+
process.exit(1);
|
|
2051
|
+
}
|
|
2052
|
+
const config = await getConfig({
|
|
2053
|
+
cwd,
|
|
2054
|
+
configPath: options.config
|
|
2055
|
+
});
|
|
2056
|
+
if (!config) {
|
|
2057
|
+
logger.error("No configuration file found. Add a `auth.ts` file to your project or pass the path to the configuration file using the `--config` flag.");
|
|
2058
|
+
return;
|
|
2059
|
+
}
|
|
2060
|
+
const adapter = await getAdapter(config).catch((e) => {
|
|
2061
|
+
logger.error(e.message);
|
|
2062
|
+
process.exit(1);
|
|
2063
|
+
});
|
|
2064
|
+
const spinner$1 = yoctoSpinner({ text: "preparing schema..." }).start();
|
|
2065
|
+
const schema = await generateSchema({
|
|
2066
|
+
adapter,
|
|
2067
|
+
file: options.output,
|
|
2068
|
+
options: config
|
|
2069
|
+
});
|
|
2070
|
+
spinner$1.stop();
|
|
2071
|
+
if (!schema.code) {
|
|
2072
|
+
logger.info("Your schema is already up to date.");
|
|
2073
|
+
try {
|
|
2074
|
+
await (await createTelemetry(config)).publish({
|
|
2075
|
+
type: "cli_generate",
|
|
2076
|
+
payload: {
|
|
2077
|
+
outcome: "no_changes",
|
|
2078
|
+
config: getTelemetryAuthConfig(config, {
|
|
2079
|
+
adapter: adapter.id,
|
|
2080
|
+
database: typeof config.database === "function" ? "adapter" : "kysely"
|
|
2081
|
+
})
|
|
2082
|
+
}
|
|
2083
|
+
});
|
|
2084
|
+
} catch {}
|
|
2085
|
+
process.exit(0);
|
|
2086
|
+
}
|
|
2087
|
+
if (schema.overwrite) {
|
|
2088
|
+
let confirm$2 = options.y || options.yes;
|
|
2089
|
+
if (!confirm$2) confirm$2 = (await prompts({
|
|
2090
|
+
type: "confirm",
|
|
2091
|
+
name: "confirm",
|
|
2092
|
+
message: `The file ${schema.fileName} already exists. Do you want to ${chalk.yellow(`${schema.overwrite ? "overwrite" : "append"}`)} the schema to the file?`
|
|
2093
|
+
})).confirm;
|
|
2094
|
+
if (confirm$2) {
|
|
2095
|
+
if (!existsSync(path.join(cwd, schema.fileName))) await fs$1.mkdir(path.dirname(path.join(cwd, schema.fileName)), { recursive: true });
|
|
2096
|
+
if (schema.overwrite) await fs$1.writeFile(path.join(cwd, schema.fileName), schema.code);
|
|
2097
|
+
else await fs$1.appendFile(path.join(cwd, schema.fileName), schema.code);
|
|
2098
|
+
logger.success(`đ Schema was ${schema.overwrite ? "overwritten" : "appended"} successfully!`);
|
|
2099
|
+
try {
|
|
2100
|
+
await (await createTelemetry(config)).publish({
|
|
2101
|
+
type: "cli_generate",
|
|
2102
|
+
payload: {
|
|
2103
|
+
outcome: schema.overwrite ? "overwritten" : "appended",
|
|
2104
|
+
config: getTelemetryAuthConfig(config)
|
|
2105
|
+
}
|
|
2106
|
+
});
|
|
2107
|
+
} catch {}
|
|
2108
|
+
process.exit(0);
|
|
2109
|
+
} else {
|
|
2110
|
+
logger.error("Schema generation aborted.");
|
|
2111
|
+
try {
|
|
2112
|
+
await (await createTelemetry(config)).publish({
|
|
2113
|
+
type: "cli_generate",
|
|
2114
|
+
payload: {
|
|
2115
|
+
outcome: "aborted",
|
|
2116
|
+
config: getTelemetryAuthConfig(config)
|
|
2117
|
+
}
|
|
2118
|
+
});
|
|
2119
|
+
} catch {}
|
|
2120
|
+
process.exit(1);
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
if (options.y) {
|
|
2124
|
+
console.warn("WARNING: --y is deprecated. Consider -y or --yes");
|
|
2125
|
+
options.yes = true;
|
|
2126
|
+
}
|
|
2127
|
+
let confirm$1 = options.yes;
|
|
2128
|
+
if (!confirm$1) confirm$1 = (await prompts({
|
|
2129
|
+
type: "confirm",
|
|
2130
|
+
name: "confirm",
|
|
2131
|
+
message: `Do you want to generate the schema to ${chalk.yellow(schema.fileName)}?`
|
|
2132
|
+
})).confirm;
|
|
2133
|
+
if (!confirm$1) {
|
|
2134
|
+
logger.error("Schema generation aborted.");
|
|
2135
|
+
try {
|
|
2136
|
+
await (await createTelemetry(config)).publish({
|
|
2137
|
+
type: "cli_generate",
|
|
2138
|
+
payload: {
|
|
2139
|
+
outcome: "aborted",
|
|
2140
|
+
config: getTelemetryAuthConfig(config)
|
|
2141
|
+
}
|
|
2142
|
+
});
|
|
2143
|
+
} catch {}
|
|
2144
|
+
process.exit(1);
|
|
2145
|
+
}
|
|
2146
|
+
if (!options.output) {
|
|
2147
|
+
if (!existsSync(path.dirname(path.join(cwd, schema.fileName)))) await fs$1.mkdir(path.dirname(path.join(cwd, schema.fileName)), { recursive: true });
|
|
2148
|
+
}
|
|
2149
|
+
await fs$1.writeFile(options.output || path.join(cwd, schema.fileName), schema.code);
|
|
2150
|
+
logger.success(`đ Schema was generated successfully!`);
|
|
2151
|
+
try {
|
|
2152
|
+
await (await createTelemetry(config)).publish({
|
|
2153
|
+
type: "cli_generate",
|
|
2154
|
+
payload: {
|
|
2155
|
+
outcome: "generated",
|
|
2156
|
+
config: getTelemetryAuthConfig(config)
|
|
2157
|
+
}
|
|
2158
|
+
});
|
|
2159
|
+
} catch {}
|
|
2160
|
+
process.exit(0);
|
|
2161
|
+
}
|
|
2162
|
+
const generate = new Command("generate").option("-c, --cwd <cwd>", "the working directory. defaults to the current directory.", process.cwd()).option("--config <config>", "the path to the configuration file. defaults to the first configuration file found.").option("--output <output>", "the file to output to the generated schema").option("-y, --yes", "automatically answer yes to all prompts", false).option("--y", "(deprecated) same as --yes", false).action(generateAction);
|
|
2163
|
+
|
|
2164
|
+
//#endregion
|
|
2165
|
+
//#region src/commands/login.ts
|
|
2166
|
+
const DEMO_URL = "https://demo.better-auth.com";
|
|
2167
|
+
const CLIENT_ID = "better-auth-cli";
|
|
2168
|
+
const CONFIG_DIR = path.join(os.homedir(), ".better-auth");
|
|
2169
|
+
const TOKEN_FILE = path.join(CONFIG_DIR, "token.json");
|
|
2170
|
+
async function loginAction(opts) {
|
|
2171
|
+
const options = z.object({
|
|
2172
|
+
serverUrl: z.string().optional(),
|
|
2173
|
+
clientId: z.string().optional()
|
|
2174
|
+
}).parse(opts);
|
|
2175
|
+
const serverUrl = options.serverUrl || DEMO_URL;
|
|
2176
|
+
const clientId = options.clientId || CLIENT_ID;
|
|
2177
|
+
intro(chalk.bold("đ Better Auth CLI Login (Demo)"));
|
|
2178
|
+
console.log(chalk.yellow("â ď¸ This is a demo feature for testing device authorization flow."));
|
|
2179
|
+
console.log(chalk.gray(" It connects to the Better Auth demo server for testing purposes.\n"));
|
|
2180
|
+
if (await getStoredToken()) {
|
|
2181
|
+
const shouldReauth = await confirm({
|
|
2182
|
+
message: "You're already logged in. Do you want to log in again?",
|
|
2183
|
+
initialValue: false
|
|
2184
|
+
});
|
|
2185
|
+
if (isCancel(shouldReauth) || !shouldReauth) {
|
|
2186
|
+
cancel("Login cancelled");
|
|
2187
|
+
process.exit(0);
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
const authClient = createAuthClient({
|
|
2191
|
+
baseURL: serverUrl,
|
|
2192
|
+
plugins: [deviceAuthorizationClient()]
|
|
2193
|
+
});
|
|
2194
|
+
const spinner$1 = yoctoSpinner({ text: "Requesting device authorization..." });
|
|
2195
|
+
spinner$1.start();
|
|
2196
|
+
try {
|
|
2197
|
+
const { data, error } = await authClient.device.code({
|
|
2198
|
+
client_id: clientId,
|
|
2199
|
+
scope: "openid profile email"
|
|
2200
|
+
});
|
|
2201
|
+
spinner$1.stop();
|
|
2202
|
+
if (error || !data) {
|
|
2203
|
+
logger.error(`Failed to request device authorization: ${error?.error_description || "Unknown error"}`);
|
|
2204
|
+
process.exit(1);
|
|
2205
|
+
}
|
|
2206
|
+
const { device_code, user_code, verification_uri, verification_uri_complete, interval = 5, expires_in } = data;
|
|
2207
|
+
console.log("");
|
|
2208
|
+
console.log(chalk.cyan("đą Device Authorization Required"));
|
|
2209
|
+
console.log("");
|
|
2210
|
+
console.log(`Please visit: ${chalk.underline.blue(verification_uri)}`);
|
|
2211
|
+
console.log(`Enter code: ${chalk.bold.green(user_code)}`);
|
|
2212
|
+
console.log("");
|
|
2213
|
+
const shouldOpen = await confirm({
|
|
2214
|
+
message: "Open browser automatically?",
|
|
2215
|
+
initialValue: true
|
|
2216
|
+
});
|
|
2217
|
+
if (!isCancel(shouldOpen) && shouldOpen) await open(verification_uri_complete || verification_uri);
|
|
2218
|
+
console.log(chalk.gray(`Waiting for authorization (expires in ${Math.floor(expires_in / 60)} minutes)...`));
|
|
2219
|
+
const token = await pollForToken(authClient, device_code, clientId, interval);
|
|
2220
|
+
if (token) {
|
|
2221
|
+
await storeToken(token);
|
|
2222
|
+
const { data: session } = await authClient.getSession({ fetchOptions: { headers: { Authorization: `Bearer ${token.access_token}` } } });
|
|
2223
|
+
outro(chalk.green(`â
Demo login successful! Logged in as ${session?.user?.name || session?.user?.email || "User"}`));
|
|
2224
|
+
console.log(chalk.gray("\nđ Note: This was a demo authentication for testing purposes."));
|
|
2225
|
+
console.log(chalk.blue("\nFor more information, visit: https://better-auth.com/docs/plugins/device-authorization"));
|
|
2226
|
+
}
|
|
2227
|
+
} catch (err) {
|
|
2228
|
+
spinner$1.stop();
|
|
2229
|
+
logger.error(`Login failed: ${err instanceof Error ? err.message : "Unknown error"}`);
|
|
2230
|
+
process.exit(1);
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
async function pollForToken(authClient, deviceCode, clientId, initialInterval) {
|
|
2234
|
+
let pollingInterval = initialInterval;
|
|
2235
|
+
const spinner$1 = yoctoSpinner({
|
|
2236
|
+
text: "",
|
|
2237
|
+
color: "cyan"
|
|
2238
|
+
});
|
|
2239
|
+
let dots = 0;
|
|
2240
|
+
return new Promise((resolve, reject) => {
|
|
2241
|
+
const poll = async () => {
|
|
2242
|
+
dots = (dots + 1) % 4;
|
|
2243
|
+
spinner$1.text = chalk.gray(`Polling for authorization${".".repeat(dots)}${" ".repeat(3 - dots)}`);
|
|
2244
|
+
if (!spinner$1.isSpinning) spinner$1.start();
|
|
2245
|
+
try {
|
|
2246
|
+
const { data, error } = await authClient.device.token({
|
|
2247
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
2248
|
+
device_code: deviceCode,
|
|
2249
|
+
client_id: clientId,
|
|
2250
|
+
fetchOptions: { headers: { "user-agent": `Better Auth CLI` } }
|
|
2251
|
+
});
|
|
2252
|
+
if (data?.access_token) {
|
|
2253
|
+
spinner$1.stop();
|
|
2254
|
+
resolve(data);
|
|
2255
|
+
return;
|
|
2256
|
+
} else if (error) switch (error.error) {
|
|
2257
|
+
case "authorization_pending": break;
|
|
2258
|
+
case "slow_down":
|
|
2259
|
+
pollingInterval += 5;
|
|
2260
|
+
spinner$1.text = chalk.yellow(`Slowing down polling to ${pollingInterval}s`);
|
|
2261
|
+
break;
|
|
2262
|
+
case "access_denied":
|
|
2263
|
+
spinner$1.stop();
|
|
2264
|
+
logger.error("Access was denied by the user");
|
|
2265
|
+
process.exit(1);
|
|
2266
|
+
break;
|
|
2267
|
+
case "expired_token":
|
|
2268
|
+
spinner$1.stop();
|
|
2269
|
+
logger.error("The device code has expired. Please try again.");
|
|
2270
|
+
process.exit(1);
|
|
2271
|
+
break;
|
|
2272
|
+
default:
|
|
2273
|
+
spinner$1.stop();
|
|
2274
|
+
logger.error(`Error: ${error.error_description}`);
|
|
2275
|
+
process.exit(1);
|
|
2276
|
+
}
|
|
2277
|
+
} catch (err) {
|
|
2278
|
+
spinner$1.stop();
|
|
2279
|
+
logger.error(`Network error: ${err instanceof Error ? err.message : "Unknown error"}`);
|
|
2280
|
+
process.exit(1);
|
|
2281
|
+
}
|
|
2282
|
+
setTimeout(poll, pollingInterval * 1e3);
|
|
2283
|
+
};
|
|
2284
|
+
setTimeout(poll, pollingInterval * 1e3);
|
|
2285
|
+
});
|
|
2286
|
+
}
|
|
2287
|
+
async function storeToken(token) {
|
|
2288
|
+
try {
|
|
2289
|
+
await fs$1.mkdir(CONFIG_DIR, { recursive: true });
|
|
2290
|
+
const tokenData = {
|
|
2291
|
+
access_token: token.access_token,
|
|
2292
|
+
token_type: token.token_type || "Bearer",
|
|
2293
|
+
scope: token.scope,
|
|
2294
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
2295
|
+
};
|
|
2296
|
+
await fs$1.writeFile(TOKEN_FILE, JSON.stringify(tokenData, null, 2), "utf-8");
|
|
2297
|
+
} catch (error) {
|
|
2298
|
+
logger.warn("Failed to store authentication token locally");
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
async function getStoredToken() {
|
|
2302
|
+
try {
|
|
2303
|
+
const data = await fs$1.readFile(TOKEN_FILE, "utf-8");
|
|
2304
|
+
return JSON.parse(data);
|
|
2305
|
+
} catch {
|
|
2306
|
+
return null;
|
|
2307
|
+
}
|
|
2308
|
+
}
|
|
2309
|
+
const login = new Command("login").description("Demo: Test device authorization flow with Better Auth demo server").option("--server-url <url>", "The Better Auth server URL", DEMO_URL).option("--client-id <id>", "The OAuth client ID", CLIENT_ID).action(loginAction);
|
|
2310
|
+
|
|
2311
|
+
//#endregion
|
|
2312
|
+
//#region src/commands/info.ts
|
|
2313
|
+
function getSystemInfo() {
|
|
2314
|
+
const platform = os.platform();
|
|
2315
|
+
const arch = os.arch();
|
|
2316
|
+
const version = os.version();
|
|
2317
|
+
const release = os.release();
|
|
2318
|
+
const cpus = os.cpus();
|
|
2319
|
+
const memory = os.totalmem();
|
|
2320
|
+
const freeMemory = os.freemem();
|
|
2321
|
+
return {
|
|
2322
|
+
platform,
|
|
2323
|
+
arch,
|
|
2324
|
+
version,
|
|
2325
|
+
release,
|
|
2326
|
+
cpuCount: cpus.length,
|
|
2327
|
+
cpuModel: cpus[0]?.model || "Unknown",
|
|
2328
|
+
totalMemory: `${(memory / 1024 / 1024 / 1024).toFixed(2)} GB`,
|
|
2329
|
+
freeMemory: `${(freeMemory / 1024 / 1024 / 1024).toFixed(2)} GB`
|
|
2330
|
+
};
|
|
2331
|
+
}
|
|
2332
|
+
function getNodeInfo() {
|
|
2333
|
+
return {
|
|
2334
|
+
version: process.version,
|
|
2335
|
+
env: process.env.NODE_ENV || "development"
|
|
2336
|
+
};
|
|
2337
|
+
}
|
|
2338
|
+
function getPackageManager() {
|
|
2339
|
+
const userAgent = process.env.npm_config_user_agent || "";
|
|
2340
|
+
if (userAgent.includes("yarn")) return {
|
|
2341
|
+
name: "yarn",
|
|
2342
|
+
version: getVersion("yarn")
|
|
2343
|
+
};
|
|
2344
|
+
if (userAgent.includes("pnpm")) return {
|
|
2345
|
+
name: "pnpm",
|
|
2346
|
+
version: getVersion("pnpm")
|
|
2347
|
+
};
|
|
2348
|
+
if (userAgent.includes("bun")) return {
|
|
2349
|
+
name: "bun",
|
|
2350
|
+
version: getVersion("bun")
|
|
2351
|
+
};
|
|
2352
|
+
return {
|
|
2353
|
+
name: "npm",
|
|
2354
|
+
version: getVersion("npm")
|
|
2355
|
+
};
|
|
2356
|
+
}
|
|
2357
|
+
function getVersion(command) {
|
|
2358
|
+
try {
|
|
2359
|
+
return execSync(`${command} --version`, { encoding: "utf8" }).trim();
|
|
2360
|
+
} catch {
|
|
2361
|
+
return "Not installed";
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
function getFrameworkInfo(projectRoot) {
|
|
2365
|
+
const packageJsonPath = path.join(projectRoot, "package.json");
|
|
2366
|
+
if (!existsSync(packageJsonPath)) return null;
|
|
2367
|
+
try {
|
|
2368
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
2369
|
+
const deps = {
|
|
2370
|
+
...packageJson.dependencies,
|
|
2371
|
+
...packageJson.devDependencies
|
|
2372
|
+
};
|
|
2373
|
+
const frameworks = {
|
|
2374
|
+
next: deps["next"],
|
|
2375
|
+
react: deps["react"],
|
|
2376
|
+
vue: deps["vue"],
|
|
2377
|
+
nuxt: deps["nuxt"],
|
|
2378
|
+
svelte: deps["svelte"],
|
|
2379
|
+
"@sveltejs/kit": deps["@sveltejs/kit"],
|
|
2380
|
+
express: deps["express"],
|
|
2381
|
+
fastify: deps["fastify"],
|
|
2382
|
+
hono: deps["hono"],
|
|
2383
|
+
remix: deps["@remix-run/react"],
|
|
2384
|
+
astro: deps["astro"],
|
|
2385
|
+
solid: deps["solid-js"],
|
|
2386
|
+
qwik: deps["@builder.io/qwik"]
|
|
2387
|
+
};
|
|
2388
|
+
const installedFrameworks = Object.entries(frameworks).filter(([_, version]) => version).map(([name, version]) => ({
|
|
2389
|
+
name,
|
|
2390
|
+
version
|
|
2391
|
+
}));
|
|
2392
|
+
return installedFrameworks.length > 0 ? installedFrameworks : null;
|
|
2393
|
+
} catch {
|
|
2394
|
+
return null;
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
function getDatabaseInfo(projectRoot) {
|
|
2398
|
+
const packageJsonPath = path.join(projectRoot, "package.json");
|
|
2399
|
+
if (!existsSync(packageJsonPath)) return null;
|
|
2400
|
+
try {
|
|
2401
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
2402
|
+
const deps = {
|
|
2403
|
+
...packageJson.dependencies,
|
|
2404
|
+
...packageJson.devDependencies
|
|
2405
|
+
};
|
|
2406
|
+
const databases = {
|
|
2407
|
+
"better-sqlite3": deps["better-sqlite3"],
|
|
2408
|
+
"@libsql/client": deps["@libsql/client"],
|
|
2409
|
+
"@libsql/kysely-libsql": deps["@libsql/kysely-libsql"],
|
|
2410
|
+
mysql2: deps["mysql2"],
|
|
2411
|
+
pg: deps["pg"],
|
|
2412
|
+
postgres: deps["postgres"],
|
|
2413
|
+
"@prisma/client": deps["@prisma/client"],
|
|
2414
|
+
drizzle: deps["drizzle-orm"],
|
|
2415
|
+
kysely: deps["kysely"],
|
|
2416
|
+
mongodb: deps["mongodb"],
|
|
2417
|
+
"@neondatabase/serverless": deps["@neondatabase/serverless"],
|
|
2418
|
+
"@vercel/postgres": deps["@vercel/postgres"],
|
|
2419
|
+
"@planetscale/database": deps["@planetscale/database"]
|
|
2420
|
+
};
|
|
2421
|
+
const installedDatabases = Object.entries(databases).filter(([_, version]) => version).map(([name, version]) => ({
|
|
2422
|
+
name,
|
|
2423
|
+
version
|
|
2424
|
+
}));
|
|
2425
|
+
return installedDatabases.length > 0 ? installedDatabases : null;
|
|
2426
|
+
} catch {
|
|
2427
|
+
return null;
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
function sanitizeBetterAuthConfig(config) {
|
|
2431
|
+
if (!config) return null;
|
|
2432
|
+
const sanitized = JSON.parse(JSON.stringify(config));
|
|
2433
|
+
const sensitiveKeys = [
|
|
2434
|
+
"secret",
|
|
2435
|
+
"clientSecret",
|
|
2436
|
+
"clientId",
|
|
2437
|
+
"authToken",
|
|
2438
|
+
"apiKey",
|
|
2439
|
+
"apiSecret",
|
|
2440
|
+
"privateKey",
|
|
2441
|
+
"publicKey",
|
|
2442
|
+
"password",
|
|
2443
|
+
"token",
|
|
2444
|
+
"webhook",
|
|
2445
|
+
"connectionString",
|
|
2446
|
+
"databaseUrl",
|
|
2447
|
+
"databaseURL",
|
|
2448
|
+
"TURSO_AUTH_TOKEN",
|
|
2449
|
+
"TURSO_DATABASE_URL",
|
|
2450
|
+
"MYSQL_DATABASE_URL",
|
|
2451
|
+
"DATABASE_URL",
|
|
2452
|
+
"POSTGRES_URL",
|
|
2453
|
+
"MONGODB_URI",
|
|
2454
|
+
"stripeKey",
|
|
2455
|
+
"stripeWebhookSecret"
|
|
2456
|
+
];
|
|
2457
|
+
const allowedKeys = [
|
|
2458
|
+
"baseURL",
|
|
2459
|
+
"callbackURL",
|
|
2460
|
+
"redirectURL",
|
|
2461
|
+
"trustedOrigins",
|
|
2462
|
+
"appName"
|
|
2463
|
+
];
|
|
2464
|
+
function redactSensitive(obj, parentKey) {
|
|
2465
|
+
if (typeof obj !== "object" || obj === null) {
|
|
2466
|
+
if (parentKey && typeof obj === "string" && obj.length > 0) {
|
|
2467
|
+
if (allowedKeys.some((allowed) => parentKey.toLowerCase() === allowed.toLowerCase())) return obj;
|
|
2468
|
+
const lowerKey = parentKey.toLowerCase();
|
|
2469
|
+
if (sensitiveKeys.some((key) => {
|
|
2470
|
+
const lowerSensitiveKey = key.toLowerCase();
|
|
2471
|
+
return lowerKey === lowerSensitiveKey || lowerKey.endsWith(lowerSensitiveKey);
|
|
2472
|
+
})) return "[REDACTED]";
|
|
2473
|
+
}
|
|
2474
|
+
return obj;
|
|
2475
|
+
}
|
|
2476
|
+
if (Array.isArray(obj)) return obj.map((item) => redactSensitive(item, parentKey));
|
|
2477
|
+
const result = {};
|
|
2478
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
2479
|
+
if (allowedKeys.some((allowed) => key.toLowerCase() === allowed.toLowerCase())) {
|
|
2480
|
+
result[key] = value;
|
|
2481
|
+
continue;
|
|
2482
|
+
}
|
|
2483
|
+
const lowerKey = key.toLowerCase();
|
|
2484
|
+
if (sensitiveKeys.some((sensitiveKey) => {
|
|
2485
|
+
const lowerSensitiveKey = sensitiveKey.toLowerCase();
|
|
2486
|
+
return lowerKey === lowerSensitiveKey || lowerKey.endsWith(lowerSensitiveKey);
|
|
2487
|
+
})) if (typeof value === "string" && value.length > 0) result[key] = "[REDACTED]";
|
|
2488
|
+
else if (typeof value === "object" && value !== null) result[key] = redactSensitive(value, key);
|
|
2489
|
+
else result[key] = value;
|
|
2490
|
+
else result[key] = redactSensitive(value, key);
|
|
2491
|
+
}
|
|
2492
|
+
return result;
|
|
2493
|
+
}
|
|
2494
|
+
if (sanitized.database) {
|
|
2495
|
+
if (typeof sanitized.database === "string") sanitized.database = "[REDACTED]";
|
|
2496
|
+
else if (sanitized.database.url) sanitized.database.url = "[REDACTED]";
|
|
2497
|
+
if (sanitized.database.authToken) sanitized.database.authToken = "[REDACTED]";
|
|
2498
|
+
}
|
|
2499
|
+
if (sanitized.socialProviders) {
|
|
2500
|
+
for (const provider in sanitized.socialProviders) if (sanitized.socialProviders[provider]) sanitized.socialProviders[provider] = redactSensitive(sanitized.socialProviders[provider], provider);
|
|
2501
|
+
}
|
|
2502
|
+
if (sanitized.emailAndPassword?.sendResetPassword) sanitized.emailAndPassword.sendResetPassword = "[Function]";
|
|
2503
|
+
if (sanitized.emailVerification?.sendVerificationEmail) sanitized.emailVerification.sendVerificationEmail = "[Function]";
|
|
2504
|
+
if (sanitized.plugins && Array.isArray(sanitized.plugins)) sanitized.plugins = sanitized.plugins.map((plugin) => {
|
|
2505
|
+
if (typeof plugin === "function") return "[Plugin Function]";
|
|
2506
|
+
if (plugin && typeof plugin === "object") return {
|
|
2507
|
+
name: plugin.id || plugin.name || "unknown",
|
|
2508
|
+
config: redactSensitive(plugin.config || plugin)
|
|
2509
|
+
};
|
|
2510
|
+
return plugin;
|
|
2511
|
+
});
|
|
2512
|
+
return redactSensitive(sanitized);
|
|
2513
|
+
}
|
|
2514
|
+
async function getBetterAuthInfo(projectRoot, configPath, suppressLogs = false) {
|
|
2515
|
+
try {
|
|
2516
|
+
const originalLog = console.log;
|
|
2517
|
+
const originalWarn = console.warn;
|
|
2518
|
+
const originalError = console.error;
|
|
2519
|
+
if (suppressLogs) {
|
|
2520
|
+
console.log = () => {};
|
|
2521
|
+
console.warn = () => {};
|
|
2522
|
+
console.error = () => {};
|
|
2523
|
+
}
|
|
2524
|
+
try {
|
|
2525
|
+
const config = await getConfig({
|
|
2526
|
+
cwd: projectRoot,
|
|
2527
|
+
configPath,
|
|
2528
|
+
shouldThrowOnError: false
|
|
2529
|
+
});
|
|
2530
|
+
const packageInfo = await getPackageInfo();
|
|
2531
|
+
return {
|
|
2532
|
+
version: packageInfo.dependencies?.["better-auth"] || packageInfo.devDependencies?.["better-auth"] || packageInfo.peerDependencies?.["better-auth"] || packageInfo.optionalDependencies?.["better-auth"] || "Unknown",
|
|
2533
|
+
config: sanitizeBetterAuthConfig(config)
|
|
2534
|
+
};
|
|
2535
|
+
} finally {
|
|
2536
|
+
if (suppressLogs) {
|
|
2537
|
+
console.log = originalLog;
|
|
2538
|
+
console.warn = originalWarn;
|
|
2539
|
+
console.error = originalError;
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
} catch (error) {
|
|
2543
|
+
return {
|
|
2544
|
+
version: "Unknown",
|
|
2545
|
+
config: null,
|
|
2546
|
+
error: error instanceof Error ? error.message : "Failed to load Better Auth config"
|
|
2547
|
+
};
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
function formatOutput(data, indent = 0) {
|
|
2551
|
+
const spaces = " ".repeat(indent);
|
|
2552
|
+
if (data === null || data === void 0) return `${spaces}${chalk.gray("N/A")}`;
|
|
2553
|
+
if (typeof data === "string" || typeof data === "number" || typeof data === "boolean") return `${spaces}${data}`;
|
|
2554
|
+
if (Array.isArray(data)) {
|
|
2555
|
+
if (data.length === 0) return `${spaces}${chalk.gray("[]")}`;
|
|
2556
|
+
return data.map((item) => formatOutput(item, indent)).join("\n");
|
|
2557
|
+
}
|
|
2558
|
+
if (typeof data === "object") {
|
|
2559
|
+
const entries = Object.entries(data);
|
|
2560
|
+
if (entries.length === 0) return `${spaces}${chalk.gray("{}")}`;
|
|
2561
|
+
return entries.map(([key, value]) => {
|
|
2562
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) return `${spaces}${chalk.cyan(key)}:\n${formatOutput(value, indent + 2)}`;
|
|
2563
|
+
return `${spaces}${chalk.cyan(key)}: ${formatOutput(value, 0)}`;
|
|
2564
|
+
}).join("\n");
|
|
2565
|
+
}
|
|
2566
|
+
return `${spaces}${JSON.stringify(data)}`;
|
|
2567
|
+
}
|
|
2568
|
+
const info = new Command("info").description("Display system and Better Auth configuration information").option("--cwd <cwd>", "The working directory", process.cwd()).option("--config <config>", "Path to the Better Auth configuration file").option("-j, --json", "Output as JSON").option("-c, --copy", "Copy output to clipboard (requires pbcopy/xclip)").action(async (options) => {
|
|
2569
|
+
const projectRoot = path.resolve(options.cwd || process.cwd());
|
|
2570
|
+
const systemInfo = getSystemInfo();
|
|
2571
|
+
const nodeInfo = getNodeInfo();
|
|
2572
|
+
const packageManager = getPackageManager();
|
|
2573
|
+
const frameworks = getFrameworkInfo(projectRoot);
|
|
2574
|
+
const databases = getDatabaseInfo(projectRoot);
|
|
2575
|
+
const betterAuthInfo = await getBetterAuthInfo(projectRoot, options.config, options.json);
|
|
2576
|
+
const fullInfo = {
|
|
2577
|
+
system: systemInfo,
|
|
2578
|
+
node: nodeInfo,
|
|
2579
|
+
packageManager,
|
|
2580
|
+
frameworks,
|
|
2581
|
+
databases,
|
|
2582
|
+
betterAuth: betterAuthInfo
|
|
2583
|
+
};
|
|
2584
|
+
if (options.json) {
|
|
2585
|
+
const jsonOutput = JSON.stringify(fullInfo, null, 2);
|
|
2586
|
+
console.log(jsonOutput);
|
|
2587
|
+
if (options.copy) try {
|
|
2588
|
+
const platform = os.platform();
|
|
2589
|
+
if (platform === "darwin") {
|
|
2590
|
+
execSync("pbcopy", { input: jsonOutput });
|
|
2591
|
+
console.log(chalk.green("\nâ Copied to clipboard"));
|
|
2592
|
+
} else if (platform === "linux") {
|
|
2593
|
+
execSync("xclip -selection clipboard", { input: jsonOutput });
|
|
2594
|
+
console.log(chalk.green("\nâ Copied to clipboard"));
|
|
2595
|
+
} else if (platform === "win32") {
|
|
2596
|
+
execSync("clip", { input: jsonOutput });
|
|
2597
|
+
console.log(chalk.green("\nâ Copied to clipboard"));
|
|
2598
|
+
}
|
|
2599
|
+
} catch {
|
|
2600
|
+
console.log(chalk.yellow("\nâ Could not copy to clipboard"));
|
|
2601
|
+
}
|
|
2602
|
+
return;
|
|
2603
|
+
}
|
|
2604
|
+
console.log(chalk.bold("\nđ Better Auth System Information\n"));
|
|
2605
|
+
console.log(chalk.gray("=".repeat(50)));
|
|
2606
|
+
console.log(chalk.bold.white("\nđĽď¸ System Information:"));
|
|
2607
|
+
console.log(formatOutput(systemInfo, 2));
|
|
2608
|
+
console.log(chalk.bold.white("\nđŚ Node.js:"));
|
|
2609
|
+
console.log(formatOutput(nodeInfo, 2));
|
|
2610
|
+
console.log(chalk.bold.white("\nđŚ Package Manager:"));
|
|
2611
|
+
console.log(formatOutput(packageManager, 2));
|
|
2612
|
+
if (frameworks) {
|
|
2613
|
+
console.log(chalk.bold.white("\nđ Frameworks:"));
|
|
2614
|
+
console.log(formatOutput(frameworks, 2));
|
|
2615
|
+
}
|
|
2616
|
+
if (databases) {
|
|
2617
|
+
console.log(chalk.bold.white("\nđž Database Clients:"));
|
|
2618
|
+
console.log(formatOutput(databases, 2));
|
|
2619
|
+
}
|
|
2620
|
+
console.log(chalk.bold.white("\nđ Better Auth:"));
|
|
2621
|
+
if (betterAuthInfo.error) console.log(` ${chalk.red("Error:")} ${betterAuthInfo.error}`);
|
|
2622
|
+
else {
|
|
2623
|
+
console.log(` ${chalk.cyan("Version")}: ${betterAuthInfo.version}`);
|
|
2624
|
+
if (betterAuthInfo.config) {
|
|
2625
|
+
console.log(` ${chalk.cyan("Configuration")}:`);
|
|
2626
|
+
console.log(formatOutput(betterAuthInfo.config, 4));
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
console.log(chalk.gray("\n" + "=".repeat(50)));
|
|
2630
|
+
console.log(chalk.gray("\nđĄ Tip: Use --json flag for JSON output"));
|
|
2631
|
+
console.log(chalk.gray("đĄ Use --copy flag to copy output to clipboard"));
|
|
2632
|
+
console.log(chalk.gray("đĄ When reporting issues, include this information\n"));
|
|
2633
|
+
if (options.copy) {
|
|
2634
|
+
const textOutput = `
|
|
2635
|
+
Better Auth System Information
|
|
2636
|
+
==============================
|
|
2637
|
+
|
|
2638
|
+
System Information:
|
|
2639
|
+
${JSON.stringify(systemInfo, null, 2)}
|
|
2640
|
+
|
|
2641
|
+
Node.js:
|
|
2642
|
+
${JSON.stringify(nodeInfo, null, 2)}
|
|
2643
|
+
|
|
2644
|
+
Package Manager:
|
|
2645
|
+
${JSON.stringify(packageManager, null, 2)}
|
|
2646
|
+
|
|
2647
|
+
Frameworks:
|
|
2648
|
+
${JSON.stringify(frameworks, null, 2)}
|
|
2649
|
+
|
|
2650
|
+
Database Clients:
|
|
2651
|
+
${JSON.stringify(databases, null, 2)}
|
|
2652
|
+
|
|
2653
|
+
Better Auth:
|
|
2654
|
+
${JSON.stringify(betterAuthInfo, null, 2)}
|
|
2655
|
+
`;
|
|
2656
|
+
try {
|
|
2657
|
+
const platform = os.platform();
|
|
2658
|
+
if (platform === "darwin") {
|
|
2659
|
+
execSync("pbcopy", { input: textOutput });
|
|
2660
|
+
console.log(chalk.green("â Copied to clipboard"));
|
|
2661
|
+
} else if (platform === "linux") {
|
|
2662
|
+
execSync("xclip -selection clipboard", { input: textOutput });
|
|
2663
|
+
console.log(chalk.green("â Copied to clipboard"));
|
|
2664
|
+
} else if (platform === "win32") {
|
|
2665
|
+
execSync("clip", { input: textOutput });
|
|
2666
|
+
console.log(chalk.green("â Copied to clipboard"));
|
|
2667
|
+
}
|
|
2668
|
+
} catch {
|
|
2669
|
+
console.log(chalk.yellow("â Could not copy to clipboard"));
|
|
2670
|
+
}
|
|
2671
|
+
}
|
|
2672
|
+
});
|
|
2673
|
+
|
|
2674
|
+
//#endregion
|
|
2675
|
+
//#region src/commands/mcp.ts
|
|
2676
|
+
async function mcpAction(options) {
|
|
2677
|
+
const mcpUrl = "https://mcp.chonkie.ai/better-auth/better-auth-builder/mcp";
|
|
2678
|
+
const mcpName = "Better Auth";
|
|
2679
|
+
if (options.cursor) await handleCursorAction(mcpUrl, mcpName);
|
|
2680
|
+
else if (options.claudeCode) handleClaudeCodeAction(mcpUrl);
|
|
2681
|
+
else if (options.openCode) handleOpenCodeAction(mcpUrl);
|
|
2682
|
+
else if (options.manual) handleManualAction(mcpUrl, mcpName);
|
|
2683
|
+
else showAllOptions(mcpUrl, mcpName);
|
|
2684
|
+
}
|
|
2685
|
+
async function handleCursorAction(mcpUrl, mcpName) {
|
|
2686
|
+
const mcpConfig = { url: mcpUrl };
|
|
2687
|
+
const encodedConfig = base64.encode(new TextEncoder().encode(JSON.stringify(mcpConfig)));
|
|
2688
|
+
const deeplinkUrl = `cursor://anysphere.cursor-deeplink/mcp/install?name=${encodeURIComponent(mcpName)}&config=${encodedConfig}`;
|
|
2689
|
+
console.log(chalk.bold.blue("đ Adding Better Auth MCP to Cursor..."));
|
|
2690
|
+
try {
|
|
2691
|
+
const platform = os$1.platform();
|
|
2692
|
+
let command;
|
|
2693
|
+
switch (platform) {
|
|
2694
|
+
case "darwin":
|
|
2695
|
+
command = `open "${deeplinkUrl}"`;
|
|
2696
|
+
break;
|
|
2697
|
+
case "win32":
|
|
2698
|
+
command = `start "" "${deeplinkUrl}"`;
|
|
2699
|
+
break;
|
|
2700
|
+
case "linux":
|
|
2701
|
+
command = `xdg-open "${deeplinkUrl}"`;
|
|
2702
|
+
break;
|
|
2703
|
+
default: throw new Error(`Unsupported platform: ${platform}`);
|
|
2704
|
+
}
|
|
2705
|
+
execSync(command, { stdio: "inherit" });
|
|
2706
|
+
console.log(chalk.green("\nâ Cursor MCP installed successfully!"));
|
|
2707
|
+
} catch (error) {
|
|
2708
|
+
console.log(chalk.yellow("\nâ Could not automatically open Cursor. Please copy the deeplink URL above and open it manually."));
|
|
2709
|
+
console.log(chalk.gray("\nYou can also manually add this configuration to your Cursor MCP settings:"));
|
|
2710
|
+
console.log(chalk.gray(JSON.stringify(mcpConfig, null, 2)));
|
|
2711
|
+
}
|
|
2712
|
+
console.log(chalk.bold.white("\n⨠Next Steps:"));
|
|
2713
|
+
console.log(chalk.gray("⢠The MCP server will be added to your Cursor configuration"));
|
|
2714
|
+
console.log(chalk.gray("⢠You can now use Better Auth features directly in Cursor"));
|
|
2715
|
+
}
|
|
2716
|
+
function handleClaudeCodeAction(mcpUrl) {
|
|
2717
|
+
console.log(chalk.bold.blue("đ¤ Adding Better Auth MCP to Claude Code..."));
|
|
2718
|
+
const command = `claude mcp add --transport http better-auth ${mcpUrl}`;
|
|
2719
|
+
try {
|
|
2720
|
+
execSync(command, { stdio: "inherit" });
|
|
2721
|
+
console.log(chalk.green("\nâ Claude Code MCP installed successfully!"));
|
|
2722
|
+
} catch (error) {
|
|
2723
|
+
console.log(chalk.yellow("\nâ Could not automatically add to Claude Code. Please run this command manually:"));
|
|
2724
|
+
console.log(chalk.cyan(command));
|
|
2725
|
+
}
|
|
2726
|
+
console.log(chalk.bold.white("\n⨠Next Steps:"));
|
|
2727
|
+
console.log(chalk.gray("⢠The MCP server will be added to your Claude Code configuration"));
|
|
2728
|
+
console.log(chalk.gray("⢠You can now use Better Auth features directly in Claude Code"));
|
|
2729
|
+
}
|
|
2730
|
+
function handleOpenCodeAction(mcpUrl) {
|
|
2731
|
+
console.log(chalk.bold.blue("đ§ Adding Better Auth MCP to Open Code..."));
|
|
2732
|
+
const openCodeConfig = {
|
|
2733
|
+
$schema: "https://opencode.ai/config.json",
|
|
2734
|
+
mcp: { "Better Auth": {
|
|
2735
|
+
type: "remote",
|
|
2736
|
+
url: mcpUrl,
|
|
2737
|
+
enabled: true
|
|
2738
|
+
} }
|
|
2739
|
+
};
|
|
2740
|
+
const configPath = path$1.join(process.cwd(), "opencode.json");
|
|
2741
|
+
try {
|
|
2742
|
+
let existingConfig = {};
|
|
2743
|
+
if (fs$2.existsSync(configPath)) {
|
|
2744
|
+
const existingContent = fs$2.readFileSync(configPath, "utf8");
|
|
2745
|
+
existingConfig = JSON.parse(existingContent);
|
|
2746
|
+
}
|
|
2747
|
+
const mergedConfig = {
|
|
2748
|
+
...existingConfig,
|
|
2749
|
+
...openCodeConfig,
|
|
2750
|
+
mcp: {
|
|
2751
|
+
...existingConfig.mcp,
|
|
2752
|
+
...openCodeConfig.mcp
|
|
2753
|
+
}
|
|
2754
|
+
};
|
|
2755
|
+
fs$2.writeFileSync(configPath, JSON.stringify(mergedConfig, null, 2));
|
|
2756
|
+
console.log(chalk.green(`\nâ Open Code configuration written to ${configPath}`));
|
|
2757
|
+
console.log(chalk.green("â Better Auth MCP added successfully!"));
|
|
2758
|
+
} catch (error) {
|
|
2759
|
+
console.log(chalk.yellow("\nâ Could not automatically write opencode.json. Please add this configuration manually:"));
|
|
2760
|
+
console.log(chalk.cyan(JSON.stringify(openCodeConfig, null, 2)));
|
|
2761
|
+
}
|
|
2762
|
+
console.log(chalk.bold.white("\n⨠Next Steps:"));
|
|
2763
|
+
console.log(chalk.gray("⢠Restart Open Code to load the new MCP server"));
|
|
2764
|
+
console.log(chalk.gray("⢠You can now use Better Auth features directly in Open Code"));
|
|
2765
|
+
}
|
|
2766
|
+
function handleManualAction(mcpUrl, mcpName) {
|
|
2767
|
+
console.log(chalk.bold.blue("đ Adding Better Auth MCP Configuration..."));
|
|
2768
|
+
const manualConfig = { [mcpName]: { url: mcpUrl } };
|
|
2769
|
+
const configPath = path$1.join(process.cwd(), "mcp.json");
|
|
2770
|
+
try {
|
|
2771
|
+
let existingConfig = {};
|
|
2772
|
+
if (fs$2.existsSync(configPath)) {
|
|
2773
|
+
const existingContent = fs$2.readFileSync(configPath, "utf8");
|
|
2774
|
+
existingConfig = JSON.parse(existingContent);
|
|
2775
|
+
}
|
|
2776
|
+
const mergedConfig = {
|
|
2777
|
+
...existingConfig,
|
|
2778
|
+
...manualConfig
|
|
2779
|
+
};
|
|
2780
|
+
fs$2.writeFileSync(configPath, JSON.stringify(mergedConfig, null, 2));
|
|
2781
|
+
console.log(chalk.green(`\nâ MCP configuration written to ${configPath}`));
|
|
2782
|
+
console.log(chalk.green("â Better Auth MCP added successfully!"));
|
|
2783
|
+
} catch (error) {
|
|
2784
|
+
console.log(chalk.yellow("\nâ Could not automatically write mcp.json. Please add this configuration manually:"));
|
|
2785
|
+
console.log(chalk.cyan(JSON.stringify(manualConfig, null, 2)));
|
|
2786
|
+
}
|
|
2787
|
+
console.log(chalk.bold.white("\n⨠Next Steps:"));
|
|
2788
|
+
console.log(chalk.gray("⢠Restart your MCP client to load the new server"));
|
|
2789
|
+
console.log(chalk.gray("⢠You can now use Better Auth features directly in your MCP client"));
|
|
2790
|
+
}
|
|
2791
|
+
function showAllOptions(mcpUrl, mcpName) {
|
|
2792
|
+
console.log(chalk.bold.blue("đ Better Auth MCP Server"));
|
|
2793
|
+
console.log(chalk.gray("Choose your MCP client to get started:"));
|
|
2794
|
+
console.log();
|
|
2795
|
+
console.log(chalk.bold.white("Available Commands:"));
|
|
2796
|
+
console.log(chalk.cyan(" --cursor ") + chalk.gray("Add to Cursor"));
|
|
2797
|
+
console.log(chalk.cyan(" --claude-code ") + chalk.gray("Add to Claude Code"));
|
|
2798
|
+
console.log(chalk.cyan(" --open-code ") + chalk.gray("Add to Open Code"));
|
|
2799
|
+
console.log(chalk.cyan(" --manual ") + chalk.gray("Manual configuration"));
|
|
2800
|
+
console.log();
|
|
2801
|
+
}
|
|
2802
|
+
const mcp = new Command("mcp").description("Add Better Auth MCP server to MCP Clients").option("--cursor", "Automatically open Cursor with the MCP configuration").option("--claude-code", "Show Claude Code MCP configuration command").option("--open-code", "Show Open Code MCP configuration").option("--manual", "Show manual MCP configuration for mcp.json").action(mcpAction);
|
|
2803
|
+
|
|
2804
|
+
//#endregion
|
|
2805
|
+
//#region src/index.ts
|
|
2806
|
+
process.on("SIGINT", () => process.exit(0));
|
|
2807
|
+
process.on("SIGTERM", () => process.exit(0));
|
|
2808
|
+
async function main() {
|
|
2809
|
+
const program = new Command("better-auth");
|
|
2810
|
+
let packageInfo = {};
|
|
2811
|
+
try {
|
|
2812
|
+
packageInfo = await getPackageInfo();
|
|
2813
|
+
} catch (error) {}
|
|
2814
|
+
program.addCommand(init).addCommand(migrate).addCommand(generate).addCommand(generateSecret).addCommand(info).addCommand(login).addCommand(mcp).version(packageInfo.version || "1.1.2").description("Better Auth CLI").action(() => program.help());
|
|
2815
|
+
program.parse();
|
|
2816
|
+
}
|
|
2817
|
+
main().catch((error) => {
|
|
2818
|
+
console.error("Error running Better Auth CLI:", error);
|
|
2819
|
+
process.exit(1);
|
|
2820
|
+
});
|
|
2821
|
+
|
|
2822
|
+
//#endregion
|
|
2823
|
+
export { };
|