feat: add telemetry (#3822)

* feat: telemetry

Co-authored-by: Kinfe123 <kinfishtech@gmail.com>

* chore: remove changeset

* fix: do not generate project id unless telemetry is enabled

* fix: return `isInsiderContainerCached`

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>

* chore: remove unused utils file

* fix: properly cache generated project id

* feat: interpret empty env vars as false

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>

* fix: use nullish coalescing to set fallback

* fix: should be `isInsideContainerCached`

* fix: unique icons + tooltip for telemetry component

* fix: import child process from node

* fix: remove quotes in description

Co-authored-by: Alex Yang <himself65@outlook.com>

* fix: address reviews

Co-authored-by: Alex Yang <himself65@outlook.com>

* chore: refactor

* refactor

* add tests

* cache pkg json

* add cli tracking

* add migrate

* chore fix xi

* skip tet

* update snapshot

* chore: fix typecheck

* Expand telemetry docs: list collected fields, clarify anonymous redaction via getTelemetryAuthConfig, and document CLI events and audit/opt‑out paths.

* docs

* doc cleanup

* fixes

* remove git first commit message

* update docs

---------

Co-authored-by: Kinfe123 <kinfishtech@gmail.com>
Co-authored-by: Bereket Engida <86073083+Bekacru@users.noreply.github.com>
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
Co-authored-by: Alex Yang <himself65@outlook.com>
Co-authored-by: Bereket Engida <Bekacru@gmail.com>
This commit is contained in:
Fraol Lemecha
2025-08-10 11:11:56 +03:00
committed by GitHub
parent 0a596beb3a
commit 2915e4c92d
31 changed files with 1373 additions and 55 deletions

View File

@@ -24,6 +24,7 @@ import { DividerText } from "@/components/divider-text";
import { APIMethod } from "@/components/api-method";
import { LLMCopyButton, ViewOptions } from "./page.client";
import { GenerateAppleJwt } from "@/components/generate-apple-jwt";
import Telemetry from "@/components/mdx/telemetry";
const { AutoTypeTable } = createTypeTable();
@@ -125,6 +126,7 @@ export default async function Page({
iframe: (props) => (
<iframe {...props} className="w-full h-[500px]" />
),
Telemetry,
}}
/>

View File

@@ -0,0 +1,138 @@
import { Badge } from "@/components/ui/badge";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import {
Settings,
ShieldCheck,
Package2,
Terminal,
GitBranch,
Cpu,
CircleCheck,
Box,
Plug,
GitCommitHorizontal,
Globe,
MemoryStick,
CircuitBoard,
Brain,
ServerCog,
Container,
AppWindow,
TerminalSquare,
CirclePlay,
} from "lucide-react";
const telemetryPoints = [
{
icon: GitCommitHorizontal,
label: "Anonymous Project ID",
title: "Unique project identifier, anonymized for privacy.",
},
{
icon: ShieldCheck,
label: "Sanitized Config",
title:
"Auth configuration options passed into Better Auth, cleaned of sensitive info.",
},
{ icon: Plug, label: "Enabled Plugins", title: "List of active plugins." },
{
icon: Settings,
label: "Better Auth Version",
title: "Current version of Better Auth.",
},
{
icon: ServerCog,
label: "Database",
title: "Type and version of the database in use.",
},
{
icon: Box,
label: "Framework",
title: "The framework powering the app and its version.",
},
{
icon: Package2,
label: "Package Manager",
title: "The package manager in use and its version.",
},
{
icon: Terminal,
label: "Runtime",
title: "The JavaScript runtime in use and its version.",
},
{ icon: Globe, label: "OS", title: "Operating system of the host machine." },
{
icon: CircuitBoard,
label: "CPU Arch",
title: "Processor architecture type.",
},
{ icon: Cpu, label: "CPU Count", title: "Number of CPU cores available." },
{ icon: Brain, label: "CPU Model", title: "Model identifier of the CPU." },
{
icon: MemoryStick,
label: "Total Memory",
title: "Total system memory (RAM) installed.",
},
{
icon: GitBranch,
label: "isGit",
title: "Indicates if the project is version controlled by Git.",
},
{
icon: CircleCheck,
label: "isProduction",
title: "Flag showing if running in production mode.",
},
{
icon: CirclePlay,
label: "isCI",
title:
"Whether the code is running in a Continuous Integration environment.",
},
{
icon: AppWindow,
label: "isWSL",
title: "True if running inside Windows Subsystem for Linux.",
},
{
icon: Container,
label: "isDocker",
title: "True if running inside a Docker container.",
},
{
icon: TerminalSquare,
label: "isTTY",
title: "True if running inside a TTY shell.",
},
];
export default function Telemetry() {
return (
<TooltipProvider>
<div className="flex flex-wrap gap-2">
{telemetryPoints.map(({ icon: Icon, label, title }, index) => (
<Tooltip key={index}>
<TooltipTrigger asChild>
<Badge
variant="outline"
className="flex items-center gap-1.5 cursor-help"
>
<Icon className="w-3.5 h-3.5" />
{label}
</Badge>
</TooltipTrigger>
<TooltipContent>
<p className="text-sm">{title}</p>
</TooltipContent>
</Tooltip>
))}
</div>
</TooltipProvider>
);
}

View File

@@ -1,4 +1,5 @@
import {
Binoculars,
Book,
CircleHelp,
Database,
@@ -1963,11 +1964,17 @@ C0.7,239.6,62.1,0.5,62.2,0.4c0,0,54,13.8,119.9,30.8S302.1,62,302.2,62c0.2,0,0.2,
href: "/docs/reference/resources",
icon: () => <Book className="w-4 h-4 text-current" />,
},
{
title: "Security",
href: "/docs/reference/security",
icon: () => <ShieldCheck className="w-4 h-4 text-current" />,
},
{
title: "Telemetry",
href: "/docs/reference/telemetry",
icon: () => <Binoculars className="w-4 h-4 text-current" />,
},
{
title: "FAQ",
href: "/docs/reference/faq",
@@ -1975,45 +1982,6 @@ C0.7,239.6,62.1,0.5,62.2,0.4c0,0,54,13.8,119.9,30.8S302.1,62,302.2,62c0.2,0,0.2,
},
],
},
// {
// title: "Contribute",
// Icon: () => (
//
// ),
// list: [
// {
// title: "Getting Started",
// href: "/docs/contribute/getting-started",
// icon: () => <BookOpenCheck className="text-current size-4" />,
// },
// {
// title: "Areas to Contribute",
// href: "/docs/contribute/areas-to-contribute",
// icon: () => <HandHelping className="w-4 h-4 text-current" />,
// },
// // {
// // title: "Database Adapters",
// // href: "/docs/contribute/database-adapters",
// // icon: () => <Plug className="w-4 h-4 text-current" />,
// // },
// {
// title: "Testing",
// href: "/docs/contribute/testing",
// icon: () => <FlaskConical className="w-4 h-4 text-current" />,
// },
// {
// title: "Documenting",
// href: "/docs/contribute/documenting",
// icon: () => <NotebookPen className="w-4 h-4 text-current" />,
// },
// {
// title: "Security Issues",
// href: "/docs/contribute/security-issues",
// icon: () => <ShieldCheck className="w-4 h-4 text-current" />,
// },
// ],
// },
];
export const examples: Content[] = [

View File

@@ -52,7 +52,7 @@ function TooltipContent({
{...props}
>
{children}
<TooltipPrimitive.Arrow className="-z-10 relative bg-primary dark:bg-stone-900 dark:fill-stone-900 fill-primary size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
<TooltipPrimitive.Arrow className="-z-10 relative bg-primary fill-primary size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
</TooltipPrimitive.Content>
</TooltipPrimitive.Portal>
);

View File

@@ -605,3 +605,16 @@ export const auth = betterAuth({
disabledPaths: ["/sign-up/email", "/sign-in/email"],
})
```
## `telemetry`
Enable or disable Better Auth's telemetry collection. (default: `true`)
```ts
import { betterAuth } from "better-auth";
export const auth = betterAuth({
telemetry: {
enabled: false,
}
})
```

View File

@@ -0,0 +1,68 @@
---
title: Telemetry
description: Better Auth now collects anonymous telemetry data about general usage.
---
## Why is telemetry collected?
Telemetry data helps us understand how Better Auth is being used across different environments so we can improve performance, prioritize features, and fix issues more effectively. Telemetry data helps us improve Better Auth by giving us insight into how its used in real-world environments. It guides our decisions on performance optimizations, feature development, and bug fixes. All data is collected anonymously and with privacy in mind, and users can opt out at any time.
## What is being collected?
The following data points may be reported. Everything is anonymous and intended for aggregate insights only.
- **Anonymous identifier**: A non-reversible hash derived from your project (`package.json` name and optionally `baseURL`). This lets us deduplicate events per project without knowing who you are.
- **Runtime**: `{ name: "node" | "bun" | "deno", version }`.
- **Environment**: one of `development`, `production`, `test`, or `ci`.
- **Framework (if detected)**: `{ name, version }` for frameworks like Next.js, Nuxt, Remix, Astro, SvelteKit, etc.
- **Database (if detected)**: `{ name, version }` for integrations like PostgreSQL, MySQL, SQLite, Prisma, Drizzle, MongoDB, etc.
- **System info**: platform, OS release, architecture, CPU count/model/speed, total memory, and flags like `isDocker`, `isWSL`, `isTTY`.
- **Package manager**: `{ name, version }` derived from the npm user agent.
- **Redacted auth config snapshot**: A minimized, privacypreserving view of your `betterAuth` options produced by `getTelemetryAuthConfig`.
We also collect anonymous telemetry from the CLI:
- **CLI generate (`cli_generate`)**: outcome `generated | overwritten | appended | no_changes | aborted` plus redacted config.
- **CLI migrate (`cli_migrate`)**: outcome `migrated | no_changes | aborted | unsupported_adapter` plus adapter id (when relevant) and redacted config.
You can audit telemetry locally by setting the `BETTER_AUTH_TELEMETRY_DEBUG=1` environment variable when running your project. In this debug mode, telemetry events are logged only to the console.
## How is my data protected?
All collected data is fully anonymous and only useful in aggregate. It cannot be traced back to any individual source and is accessible only to a small group of core Better Auth maintainers to guide roadmap decisions.
- **No PII or secrets**: We do not collect emails, usernames, tokens, secrets, client IDs, client secrets, or database URLs.
- **No full config**: We never send your full `betterAuth` configuration. Instead we send a reduced, redacted snapshot of nonsensitive toggles and counts.
- **Redaction by design**: See [detect-auth-config.ts](https://github.com/better-auth/better-auth/blob/main/packages/better-auth/src/telemetry/detectors/detect-auth-config.ts) in the Better Auth source for the exact shape of what is included. It purposely converts sensitive values to booleans, counts, or generic identifiers.
## How can I disable it?
You can disable telemetry collection in your auth config or by setting an environment variable.
- Via your auth config.
```ts title="auth.ts"
export const auth = betterAuth({
// [!code highlight]
telemetry: { // [!code highlight]
enabled: false // [!code highlight]
} // [!code highlight]
});
```
- Via an environment variable.
```env title=".env"
# Enable telemetry
BETTER_AUTH_TELEMETRY=1
# Disable telemetry
BETTER_AUTH_TELEMETRY=0
```
### When is telemetry sent?
- On `betterAuth` initialization (`type: "init"`).
- On CLI actions: `generate` and `migrate` as described above.
Telemetry is disabled automatically in tests (`NODE_ENV=test`) unless explicitly overridden by internal tooling.

View File

@@ -733,6 +733,7 @@
"solid-js": "^1.9.3",
"tarn": "^3.0.2",
"tedious": "^18.6.1",
"type-fest": "^4.41.0",
"typescript": "catalog:",
"unbuild": "catalog:",
"vue": "^3.5.13",

View File

@@ -127,6 +127,7 @@ exports[`init > should match config 1`] = `
"hash": [Function],
"verify": [Function],
},
"publishTelemetry": [Function],
"rateLimit": {
"enabled": false,
"max": 100,

View File

@@ -8,14 +8,14 @@ import {
import type { BetterAuthOptions } from "../../types";
import type { KyselyDatabaseType } from "./types";
function getDatabaseType(
export function getKyselyDatabaseType(
db: BetterAuthOptions["database"],
): KyselyDatabaseType | null {
if (!db) {
return null;
}
if ("dialect" in db) {
return getDatabaseType(db.dialect as Dialect);
return getKyselyDatabaseType(db.dialect as Dialect);
}
if ("createDriver" in db) {
if (db instanceof SqliteDialect) {
@@ -73,7 +73,7 @@ export const createKyselyAdapter = async (config: BetterAuthOptions) => {
let dialect: Dialect | undefined = undefined;
const databaseType = getDatabaseType(db);
const databaseType = getKyselyDatabaseType(db);
if ("createDriver" in db) {
dialect = db;

View File

@@ -10,3 +10,8 @@ export type * from "zod/v4/core";
export type * from "./types/helper";
// export this as we are referencing OAuth2Tokens in the `refresh-token` api as return type
export type * from "./oauth2/types";
// telemetry exports for CLI and consumers
export { createTelemetry } from "./telemetry";
export { getTelemetryAuthConfig } from "./telemetry/detectors/detect-auth-config";
export type { TelemetryEvent } from "./telemetry/types";

View File

@@ -27,13 +27,15 @@ import { checkPassword } from "./utils/password";
import { getBaseURL } from "./utils/url";
import type { LiteralUnion } from "./types/helper";
import { BetterAuthError } from "./error";
import { createTelemetry } from "./telemetry";
import type { TelemetryEvent } from "./telemetry/types";
import { getKyselyDatabaseType } from "./adapters/kysely-adapter";
export const init = async (options: BetterAuthOptions) => {
const adapter = await getAdapter(options);
const plugins = options.plugins || [];
const internalPlugins = getInternalPlugins(options);
const logger = createLogger(options.logger);
const baseURL = getBaseURL(options.baseURL, options.basePath);
const secret =
@@ -57,6 +59,7 @@ export const init = async (options: BetterAuthOptions) => {
basePath: options.basePath || "/api/auth",
plugins: plugins.concat(internalPlugins),
};
const cookies = getCookies(options);
const tables = getAuthTables(options);
const providers = Object.keys(options.socialProviders || {})
@@ -91,7 +94,15 @@ export const init = async (options: BetterAuthOptions) => {
return generateId(size);
};
const ctx: AuthContext = {
const { publish } = await createTelemetry(options, {
adapter: adapter.id,
database:
typeof options.database === "function"
? "adapter"
: getKyselyDatabaseType(options.database) || "unknown",
});
let ctx: AuthContext = {
appName: options.appName || "Better Auth",
socialProviders: providers,
options,
@@ -120,7 +131,7 @@ export const init = async (options: BetterAuthOptions) => {
(options.secondaryStorage ? "secondary-storage" : "memory"),
},
authCookies: cookies,
logger: logger,
logger,
generateId: generateIdFunc,
session: null,
secondaryStorage: options.secondaryStorage,
@@ -155,6 +166,7 @@ export const init = async (options: BetterAuthOptions) => {
const { runMigrations } = await getMigrations(options);
await runMigrations();
},
publishTelemetry: publish,
};
let { context } = runPluginInit(ctx);
return context;
@@ -219,6 +231,7 @@ export type AuthContext = {
};
tables: ReturnType<typeof getAuthTables>;
runMigrations: () => Promise<void>;
publishTelemetry: (event: TelemetryEvent) => Promise<void>;
};
function runPluginInit(ctx: AuthContext) {

View File

@@ -0,0 +1,200 @@
import type { SocialProviders } from "../../social-providers";
import type { BetterAuthOptions } from "../../types";
import type { TelemetryContext } from "../types";
export function getTelemetryAuthConfig(
options: BetterAuthOptions,
context?: TelemetryContext,
) {
return {
database: context?.database,
adapter: context?.adapter,
emailVerification: {
sendVerificationEmail: !!options.emailVerification?.sendVerificationEmail,
sendOnSignUp: !!options.emailVerification?.sendOnSignUp,
sendOnSignIn: !!options.emailVerification?.sendOnSignIn,
autoSignInAfterVerification:
!!options.emailVerification?.autoSignInAfterVerification,
expiresIn: options.emailVerification?.expiresIn,
onEmailVerification: !!options.emailVerification?.onEmailVerification,
afterEmailVerification:
!!options.emailVerification?.afterEmailVerification,
},
emailAndPassword: {
enabled: !!options.emailAndPassword?.enabled,
disableSignUp: !!options.emailAndPassword?.disableSignUp,
requireEmailVerification:
!!options.emailAndPassword?.requireEmailVerification,
maxPasswordLength: options.emailAndPassword?.maxPasswordLength,
minPasswordLength: options.emailAndPassword?.minPasswordLength,
sendResetPassword: !!options.emailAndPassword?.sendResetPassword,
resetPasswordTokenExpiresIn:
options.emailAndPassword?.resetPasswordTokenExpiresIn,
onPasswordReset: !!options.emailAndPassword?.onPasswordReset,
password: {
hash: !!options.emailAndPassword?.password?.hash,
verify: !!options.emailAndPassword?.password?.verify,
},
autoSignIn: !!options.emailAndPassword?.autoSignIn,
revokeSessionsOnPasswordReset:
!!options.emailAndPassword?.revokeSessionsOnPasswordReset,
},
socialProviders: Object.keys(options.socialProviders || {}).map((p) => {
const provider = options.socialProviders?.[p as keyof SocialProviders];
if (!provider) return {};
return {
id: p,
mapProfileToUser: !!provider.mapProfileToUser,
disableDefaultScope: !!provider.disableDefaultScope,
disableIdTokenSignIn: !!provider.disableIdTokenSignIn,
disableImplicitSignUp: provider.disableImplicitSignUp,
disableSignUp: provider.disableSignUp,
getUserInfo: !!provider.getUserInfo,
overrideUserInfoOnSignIn: !!provider.overrideUserInfoOnSignIn,
prompt: provider.prompt,
verifyIdToken: !!provider.verifyIdToken,
scope: provider.scope,
refreshAccessToken: !!provider.refreshAccessToken,
};
}),
plugins: options.plugins?.map((p) => p.id.toString()),
user: {
modelName: options.user?.modelName,
fields: options.user?.fields,
additionalFields: options.user?.additionalFields,
changeEmail: {
enabled: options.user?.changeEmail?.enabled,
sendChangeEmailVerification:
!!options.user?.changeEmail?.sendChangeEmailVerification,
},
},
verification: {
modelName: options.verification?.modelName,
disableCleanup: options.verification?.disableCleanup,
fields: options.verification?.fields,
},
session: {
modelName: options.session?.modelName,
additionalFields: options.session?.additionalFields,
cookieCache: {
enabled: options.session?.cookieCache?.enabled,
maxAge: options.session?.cookieCache?.maxAge,
},
disableSessionRefresh: options.session?.disableSessionRefresh,
expiresIn: options.session?.expiresIn,
fields: options.session?.fields,
freshAge: options.session?.freshAge,
preserveSessionInDatabase: options.session?.preserveSessionInDatabase,
storeSessionInDatabase: options.session?.storeSessionInDatabase,
updateAge: options.session?.updateAge,
},
account: {
modelName: options.account?.modelName,
fields: options.account?.fields,
encryptOAuthTokens: options.account?.encryptOAuthTokens,
updateAccountOnSignIn: options.account?.updateAccountOnSignIn,
accountLinking: {
enabled: options.account?.accountLinking?.enabled,
trustedProviders: options.account?.accountLinking?.trustedProviders,
updateUserInfoOnLink:
options.account?.accountLinking?.updateUserInfoOnLink,
allowUnlinkingAll: options.account?.accountLinking?.allowUnlinkingAll,
},
},
hooks: {
after: !!options.hooks?.after,
before: !!options.hooks?.before,
},
secondaryStorage: !!options.secondaryStorage,
advanced: {
cookiePrefix: !!options.advanced?.cookiePrefix, //this shouldn't be tracked
cookies: !!options.advanced?.cookies,
crossSubDomainCookies: {
domain: !!options.advanced?.crossSubDomainCookies?.domain,
enabled: options.advanced?.crossSubDomainCookies?.enabled,
additionalCookies:
options.advanced?.crossSubDomainCookies?.additionalCookies,
},
database: {
useNumberId: !!options.advanced?.database?.useNumberId,
generateId: options.advanced?.database?.generateId,
defaultFindManyLimit: options.advanced?.database?.defaultFindManyLimit,
},
useSecureCookies: options.advanced?.useSecureCookies,
ipAddress: {
disableIpTracking: options.advanced?.ipAddress?.disableIpTracking,
ipAddressHeaders: options.advanced?.ipAddress?.ipAddressHeaders,
},
disableCSRFCheck: options.advanced?.disableCSRFCheck,
cookieAttributes: {
expires: options.advanced?.defaultCookieAttributes?.expires,
secure: options.advanced?.defaultCookieAttributes?.secure,
sameSite: options.advanced?.defaultCookieAttributes?.sameSite,
domain: !!options.advanced?.defaultCookieAttributes?.domain,
path: options.advanced?.defaultCookieAttributes?.path,
httpOnly: options.advanced?.defaultCookieAttributes?.httpOnly,
},
},
trustedOrigins: options.trustedOrigins?.length,
rateLimit: {
storage: options.rateLimit?.storage,
modelName: options.rateLimit?.modelName,
window: options.rateLimit?.window,
customStorage: !!options.rateLimit?.customStorage,
enabled: options.rateLimit?.enabled,
max: options.rateLimit?.max,
},
onAPIError: {
errorURL: options.onAPIError?.errorURL,
onError: !!options.onAPIError?.onError,
throw: options.onAPIError?.throw,
},
logger: {
disabled: options.logger?.disabled,
level: options.logger?.level,
log: !!options.logger?.log,
},
databaseHooks: {
user: {
create: {
after: !!options.databaseHooks?.user?.create?.after,
before: !!options.databaseHooks?.user?.create?.before,
},
update: {
after: !!options.databaseHooks?.user?.update?.after,
before: !!options.databaseHooks?.user?.update?.before,
},
},
session: {
create: {
after: !!options.databaseHooks?.session?.create?.after,
before: !!options.databaseHooks?.session?.create?.before,
},
update: {
after: !!options.databaseHooks?.session?.update?.after,
before: !!options.databaseHooks?.session?.update?.before,
},
},
account: {
create: {
after: !!options.databaseHooks?.account?.create?.after,
before: !!options.databaseHooks?.account?.create?.before,
},
update: {
after: !!options.databaseHooks?.account?.update?.after,
before: !!options.databaseHooks?.account?.update?.before,
},
},
verification: {
create: {
after: !!options.databaseHooks?.verification?.create?.after,
before: !!options.databaseHooks?.verification?.create?.before,
},
update: {
after: !!options.databaseHooks?.verification?.update?.after,
before: !!options.databaseHooks?.verification?.update?.before,
},
},
},
};
}

View File

@@ -0,0 +1,22 @@
import { getPackageVersion } from "../../utils/package-json";
import type { DetectionInfo } from "../types";
const DATABASES: Record<string, string> = {
pg: "postgresql",
mysql: "mysql",
mariadb: "mariadb",
sqlite3: "sqlite",
"better-sqlite3": "sqlite",
"@prisma/client": "prisma",
mongoose: "mongodb",
mongodb: "mongodb",
"drizzle-orm": "drizzle",
};
export async function detectDatabase(): Promise<DetectionInfo | undefined> {
for (const [pkg, name] of Object.entries(DATABASES)) {
const version = await getPackageVersion(pkg);
if (version) return { name, version };
}
return undefined;
}

View File

@@ -0,0 +1,23 @@
import { getPackageVersion } from "../../utils/package-json";
const FRAMEWORKS: Record<string, string> = {
next: "next",
nuxt: "nuxt",
"@remix-run/server-runtime": "remix",
astro: "astro",
"@sveltejs/kit": "sveltekit",
"solid-start": "solid-start",
"tanstack-start": "tanstack-start",
hono: "hono",
express: "express",
elysia: "elysia",
expo: "expo",
};
export async function detectFramework() {
for (const [pkg, name] of Object.entries(FRAMEWORKS)) {
const version = await getPackageVersion(pkg);
if (version) return { name, version };
}
return undefined;
}

View File

@@ -0,0 +1,17 @@
// https://github.com/zkochan/packages/blob/main/which-pm-runs/index.js
export function detectPackageManager() {
const userAgent = process.env.npm_config_user_agent;
if (!userAgent) {
return undefined;
}
const pmSpec = userAgent.split(" ")[0];
const separatorPos = pmSpec.lastIndexOf("/");
const name = pmSpec.substring(0, separatorPos);
return {
name: name === "npminstall" ? "cnpm" : name,
version: pmSpec.substring(separatorPos + 1),
};
}

View File

@@ -0,0 +1,28 @@
import { getEnvVar, isTest } from "../../utils/env";
import { isCI } from "./detect-system-info";
export function detectRuntime() {
// @ts-expect-error: TS doesn't know about Deno global
if (typeof Deno !== "undefined") {
// @ts-expect-error: TS doesn't know about Deno global
const denoVersion = Deno?.version?.deno ?? null;
return { name: "deno", version: denoVersion };
}
if (typeof Bun !== "undefined") {
const bunVersion = Bun?.version ?? null;
return { name: "bun", version: bunVersion };
}
return { name: "node", version: process.versions.node ?? null };
}
export function detectEnvironment() {
return getEnvVar("NODE_ENV") === "production"
? "production"
: isCI()
? "ci"
: isTest()
? "test"
: "development";
}

View File

@@ -0,0 +1,106 @@
import fs from "fs";
import os from "os";
import process from "process";
import { env } from "../../utils/env";
export function detectSystemInfo() {
const cpus = os.cpus();
return {
systemPlatform: os.platform(),
systemRelease: os.release(),
systemArchitecture: os.arch(),
cpuCount: cpus.length,
cpuModel: cpus.length ? cpus[0].model : null,
cpuSpeed: cpus.length ? cpus[0].speed : null,
memory: os.totalmem(),
isWSL: isWsl(),
isDocker: isDocker(),
isTTY: process.stdout.isTTY,
};
}
let isDockerCached: boolean | undefined;
function hasDockerEnv() {
try {
fs.statSync("/.dockerenv");
return true;
} catch {
return false;
}
}
function hasDockerCGroup() {
try {
return fs.readFileSync("/proc/self/cgroup", "utf8").includes("docker");
} catch {
return false;
}
}
function isDocker() {
if (isDockerCached === undefined) {
isDockerCached = hasDockerEnv() || hasDockerCGroup();
}
return isDockerCached;
}
function isWsl() {
if (process.platform !== "linux") {
return false;
}
if (os.release().toLowerCase().includes("microsoft")) {
if (isInsideContainer()) {
return false;
}
return true;
}
try {
return fs
.readFileSync("/proc/version", "utf8")
.toLowerCase()
.includes("microsoft")
? !isInsideContainer()
: false;
} catch {
return false;
}
}
let isInsideContainerCached: boolean | undefined;
const hasContainerEnv = () => {
try {
fs.statSync("/run/.containerenv");
return true;
} catch {
return false;
}
};
function isInsideContainer() {
if (isInsideContainerCached === undefined) {
isInsideContainerCached = hasContainerEnv() || isDocker();
}
return isInsideContainerCached;
}
export function isCI() {
return (
env.CI !== "false" &&
("BUILD_ID" in env || // Jenkins, Cloudbees
"BUILD_NUMBER" in env || // Jenkins, TeamCity (fixed typo: extra space removed)
"CI" in env || // Travis CI, CircleCI, Cirrus CI, Gitlab CI, Appveyor, CodeShip, dsari, Cloudflare
"CI_APP_ID" in env || // Appflow
"CI_BUILD_ID" in env || // Appflow
"CI_BUILD_NUMBER" in env || // Appflow
"CI_NAME" in env || // Codeship and others
"CONTINUOUS_INTEGRATION" in env || // Travis CI, Cirrus CI
"RUN_ID" in env) // TaskCluster, dsari
);
}

View File

@@ -0,0 +1,76 @@
import { ENV, getBooleanEnvVar, isTest } from "../utils/env";
import { getProjectId } from "./project-id";
import type { BetterAuthOptions } from "../types";
import { detectEnvironment, detectRuntime } from "./detectors/detect-runtime";
import { detectDatabase } from "./detectors/detect-database";
import { detectFramework } from "./detectors/detect-framework";
import { detectSystemInfo } from "./detectors/detect-system-info";
import { detectPackageManager } from "./detectors/detect-project-info";
import { betterFetch } from "@better-fetch/fetch";
import type { TelemetryContext, TelemetryEvent } from "./types";
import { logger } from "../utils";
import { getTelemetryAuthConfig } from "./detectors/detect-auth-config";
export async function createTelemetry(
options: BetterAuthOptions,
context?: TelemetryContext,
) {
const debugEnabled = getBooleanEnvVar("BETTER_AUTH_TELEMETRY_DEBUG", false);
const TELEMETRY_ENDPOINT = ENV.BETTER_AUTH_TELEMETRY_ENDPOINT;
const track = async (event: TelemetryEvent) => {
try {
if (context?.customTrack) {
await context.customTrack(event);
} else {
if (debugEnabled) {
await Promise.resolve(
logger.info("telemetry event", JSON.stringify(event, null, 2)),
);
} else {
await betterFetch(TELEMETRY_ENDPOINT, {
method: "POST",
body: event,
});
}
}
} catch {}
};
const isEnabled = async () => {
const telemetryEnabled =
options.telemetry?.enabled !== undefined
? options.telemetry.enabled
: true;
const envEnabled = getBooleanEnvVar("BETTER_AUTH_TELEMETRY", true);
return (
envEnabled && telemetryEnabled && (context?.skipTestCheck || !isTest())
);
};
const anonymousId = await getProjectId(options.baseURL);
const payload = {
config: getTelemetryAuthConfig(options),
runtime: detectRuntime(),
database: await detectDatabase(),
framework: await detectFramework(),
environment: detectEnvironment(),
systemInfo: detectSystemInfo(),
packageManager: detectPackageManager(),
};
const enabled = await isEnabled();
if (enabled) {
void track({ type: "init", payload, anonymousId });
}
return {
publish: async (event: TelemetryEvent) => {
if (!enabled) return;
await track({
type: event.type,
payload: event.payload,
anonymousId,
});
},
};
}

View File

@@ -0,0 +1,27 @@
import { generateId } from "../utils";
import { hashToBase64 } from "../crypto";
import { getNameFromLocalPackageJson } from "../utils/package-json";
let projectIdCached: string | null = null;
export async function getProjectId(
baseUrl: string | undefined,
): Promise<string> {
if (projectIdCached) return projectIdCached;
const projectName = await getNameFromLocalPackageJson();
if (projectName) {
projectIdCached = await hashToBase64(
baseUrl ? baseUrl + projectName : projectName,
);
return projectIdCached;
}
if (baseUrl) {
projectIdCached = await hashToBase64(baseUrl);
return projectIdCached;
}
projectIdCached = generateId(32);
return projectIdCached;
}

View File

@@ -0,0 +1,298 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { createTelemetry } from "./index";
import type { TelemetryEvent } from "./types";
vi.mock("@better-fetch/fetch", () => ({
betterFetch: vi.fn(async () => ({ status: 200 })),
}));
vi.mock("./project-id", () => ({
getProjectId: vi.fn(async () => "anon-123"),
}));
vi.mock("./detectors/detect-runtime", () => ({
detectRuntime: vi.fn(() => ({ name: "node", version: "test" })),
detectEnvironment: vi.fn(() => "test"),
}));
vi.mock("./detectors/detect-database", () => ({
detectDatabase: vi.fn(async () => ({ name: "postgresql", version: "1.0.0" })),
}));
vi.mock("./detectors/detect-framework", () => ({
detectFramework: vi.fn(async () => ({ name: "next", version: "15.0.0" })),
}));
vi.mock("./detectors/detect-system-info", () => ({
detectSystemInfo: vi.fn(() => ({
systemPlatform: "darwin",
systemRelease: "24.6.0",
systemArchitecture: "arm64",
cpuCount: 8,
cpuModel: "Apple M3",
cpuSpeed: 3200,
memory: 16 * 1024 * 1024 * 1024,
isDocker: false,
isTTY: true,
isWSL: false,
isCI: false,
})),
isCI: vi.fn(() => false),
}));
vi.mock("./detectors/detect-project-info", () => ({
detectPackageManager: vi.fn(() => ({ name: "pnpm", version: "9.0.0" })),
}));
beforeEach(() => {
vi.resetModules();
vi.clearAllMocks();
process.env.BETTER_AUTH_TELEMETRY = "";
process.env.BETTER_AUTH_TELEMETRY_DEBUG = "";
});
describe("telemetry", () => {
it("publishes events when enabled", async () => {
let event: TelemetryEvent | undefined;
const track = vi.fn().mockImplementation(async (e) => {
event = e;
});
await createTelemetry(
{
baseURL: "http://localhost.com", //this shouldn't be tracked
appName: "test", //this shouldn't be tracked
advanced: {
cookiePrefix: "test", //this shouldn't be tracked - should set to true
crossSubDomainCookies: {
domain: ".test.com", //this shouldn't be tracked - should set to true
enabled: true,
},
},
},
{ customTrack: track, skipTestCheck: true },
);
expect(event).toMatchObject({
type: "init",
payload: {
config: {
emailVerification: {
sendVerificationEmail: false,
sendOnSignUp: false,
sendOnSignIn: false,
autoSignInAfterVerification: false,
expiresIn: undefined,
onEmailVerification: false,
afterEmailVerification: false,
},
emailAndPassword: {
enabled: false,
disableSignUp: false,
requireEmailVerification: false,
maxPasswordLength: undefined,
minPasswordLength: undefined,
sendResetPassword: false,
resetPasswordTokenExpiresIn: undefined,
onPasswordReset: false,
password: { hash: false, verify: false },
autoSignIn: false,
revokeSessionsOnPasswordReset: false,
},
socialProviders: [],
plugins: undefined,
user: {
modelName: undefined,
fields: undefined,
additionalFields: undefined,
changeEmail: {
enabled: undefined,
sendChangeEmailVerification: false,
},
},
verification: {
modelName: undefined,
disableCleanup: undefined,
fields: undefined,
},
session: {
modelName: undefined,
additionalFields: undefined,
cookieCache: { enabled: undefined, maxAge: undefined },
disableSessionRefresh: undefined,
expiresIn: undefined,
fields: undefined,
freshAge: undefined,
preserveSessionInDatabase: undefined,
storeSessionInDatabase: undefined,
updateAge: undefined,
},
account: {
modelName: undefined,
fields: undefined,
encryptOAuthTokens: undefined,
updateAccountOnSignIn: undefined,
accountLinking: {
enabled: undefined,
trustedProviders: undefined,
updateUserInfoOnLink: undefined,
allowUnlinkingAll: undefined,
},
},
hooks: { after: false, before: false },
secondaryStorage: false,
advanced: {
cookiePrefix: true,
cookies: false,
crossSubDomainCookies: {
domain: true,
enabled: true,
additionalCookies: undefined,
},
database: {
useNumberId: false,
generateId: undefined,
defaultFindManyLimit: undefined,
},
useSecureCookies: undefined,
ipAddress: {
disableIpTracking: undefined,
ipAddressHeaders: undefined,
},
disableCSRFCheck: undefined,
cookieAttributes: {
expires: undefined,
secure: undefined,
sameSite: undefined,
domain: false,
path: undefined,
httpOnly: undefined,
},
},
trustedOrigins: undefined,
rateLimit: {
storage: undefined,
modelName: undefined,
window: undefined,
customStorage: false,
enabled: undefined,
max: undefined,
},
onAPIError: {
errorURL: undefined,
onError: false,
throw: undefined,
},
logger: { disabled: undefined, level: undefined, log: false },
databaseHooks: {
user: {
create: {
after: false,
before: false,
},
update: {
after: false,
before: false,
},
},
session: {
create: {
after: false,
before: false,
},
update: {
after: false,
before: false,
},
},
account: {
create: {
after: false,
before: false,
},
update: {
after: false,
before: false,
},
},
verification: {
create: {
after: false,
before: false,
},
update: {
after: false,
before: false,
},
},
},
},
runtime: { name: "node", version: "test" },
database: { name: "postgresql", version: "1.0.0" },
framework: { name: "next", version: "15.0.0" },
environment: "test",
systemInfo: {
systemPlatform: "darwin",
systemRelease: "24.6.0",
systemArchitecture: "arm64",
cpuCount: 8,
cpuModel: "Apple M3",
cpuSpeed: 3200,
memory: 17179869184,
isDocker: false,
isTTY: true,
isWSL: false,
isCI: false,
},
packageManager: { name: "pnpm", version: "9.0.0" },
},
anonymousId: "anon-123",
});
});
it("does not publish when disabled via env", async () => {
process.env.BETTER_AUTH_TELEMETRY = "false";
let event: TelemetryEvent | undefined;
const track = vi.fn().mockImplementation(async (e) => {
event = e;
});
await createTelemetry(
{
baseURL: "http://localhost",
},
{ customTrack: track, skipTestCheck: true },
);
expect(event).toBeUndefined();
expect(track).not.toBeCalled();
});
it("does not publish when disabled via option", async () => {
let event: TelemetryEvent | undefined;
const track = vi.fn().mockImplementation(async (e) => {
event = e;
});
await createTelemetry(
{
baseURL: "http://localhost",
telemetry: { enabled: false },
},
{ customTrack: track, skipTestCheck: true },
);
expect(event).toBeUndefined();
expect(track).not.toBeCalled();
});
it("shouldn't fail cause track isn't being reached", async () => {
await expect(
createTelemetry(
{
baseURL: "http://localhost",
},
{
customTrack() {
throw new Error("test");
},
skipTestCheck: true,
},
),
).resolves.not.throw(Error);
});
});

View File

@@ -0,0 +1,46 @@
export interface DetectionInfo {
name: string;
version: string | null;
}
export interface SystemInfo {
// Software information
systemPlatform: string;
systemRelease: string;
systemArchitecture: string;
// Machine information
cpuCount: number;
cpuModel: string | null;
cpuSpeed: number | null;
memory: number;
// Environment information
isDocker: boolean;
isTTY: boolean;
isWSL: boolean;
isCI: boolean;
}
export interface AuthConfigInfo {
options: any;
plugins: string[];
}
export interface ProjectInfo {
isGit: boolean;
packageManager: DetectionInfo | null;
}
export interface TelemetryEvent {
type: string;
anonymousId?: string;
payload: Record<string, any>;
}
export interface TelemetryContext {
customTrack?: (event: TelemetryEvent) => Promise<void>;
database?: string;
adapter?: string;
skipTestCheck?: boolean;
}

View File

@@ -173,7 +173,6 @@ export type BetterAuthOptions = {
* Auto signin the user after they verify their email
*/
autoSignInAfterVerification?: boolean;
/**
* Number of seconds the verification token is
* valid for.
@@ -1046,4 +1045,15 @@ export type BetterAuthOptions = {
* Paths you want to disable.
*/
disabledPaths?: string[];
/**
* Telemetry configuration
*/
telemetry?: {
/**
* Enable telemetry collection
*
* @default true
*/
enabled?: boolean;
};
};

View File

@@ -0,0 +1,13 @@
export async function awaitObject<T extends Record<string, Promise<any>>>(
promises: T,
): Promise<{ [K in keyof T]: Awaited<T[K]> }> {
const entries = Object.entries(promises) as [keyof T, T[keyof T]][];
const results = await Promise.all(entries.map(([, promise]) => promise));
const resolved: Partial<{ [K in keyof T]: Awaited<T[K]> }> = {};
entries.forEach(([key], index) => {
resolved[key] = results[index];
});
return resolved as { [K in keyof T]: Awaited<T[K]> };
}

View File

@@ -54,4 +54,68 @@ export const isProduction = nodeENV === "production";
export const isDevelopment = nodeENV === "dev" || nodeENV === "development";
/** Detect if `NODE_ENV` environment variable is `test` */
export const isTest = nodeENV === "test" || toBoolean(env.TEST);
export const isTest = () => nodeENV === "test" || toBoolean(env.TEST);
/**
* Get environment variable with fallback
*/
export function getEnvVar<Fallback extends string>(
key: string,
fallback?: Fallback,
): Fallback extends string ? string : string | undefined {
if (typeof process !== "undefined" && process.env) {
return process.env[key] ?? (fallback as any);
}
// @ts-expect-error deno
if (typeof Deno !== "undefined") {
// @ts-expect-error deno
return Deno.env.get(key) ?? (fallback as string);
}
// Handle Bun
if (typeof Bun !== "undefined") {
return Bun.env[key] ?? (fallback as string);
}
return fallback as any;
}
/**
* Get boolean environment variable
*/
export function getBooleanEnvVar(key: string, fallback = true): boolean {
const value = getEnvVar(key);
if (!value) return fallback;
return value !== "0" && value.toLowerCase() !== "false" && value !== "";
}
/**
* Common environment variables used in Better Auth
*/
export const ENV = {
get BETTER_AUTH_SECRET() {
return getEnvVar("BETTER_AUTH_SECRET");
},
get AUTH_SECRET() {
return getEnvVar("AUTH_SECRET");
},
get BETTER_AUTH_TELEMETRY() {
return getEnvVar("BETTER_AUTH_TELEMETRY");
},
get BETTER_AUTH_TELEMETRY_ID() {
return getEnvVar("BETTER_AUTH_TELEMETRY_ID");
},
get NODE_ENV() {
return getEnvVar("NODE_ENV", "development");
},
get PACKAGE_VERSION() {
return getEnvVar("PACKAGE_VERSION", "0.0.0");
},
get BETTER_AUTH_TELEMETRY_ENDPOINT() {
return getEnvVar(
"BETTER_AUTH_TELEMETRY_ENDPOINT",
"https://telemetry.better-auth.com/v1/track",
);
},
} as const;

View File

@@ -9,7 +9,7 @@ export function getIp(
return null;
}
if (isTest) {
if (isTest()) {
return "127.0.0.1"; // Use a fixed IP for test environments
}

View File

@@ -0,0 +1,60 @@
import fs from "fs/promises";
import path from "path";
import type { PackageJson } from "type-fest";
let packageJSONCache: PackageJson | undefined;
async function readRootPackageJson() {
if (packageJSONCache) return packageJSONCache;
try {
const raw = await fs.readFile(
path.join(process.cwd(), "package.json"),
"utf-8",
);
packageJSONCache = JSON.parse(raw);
return packageJSONCache as PackageJson;
} catch {}
return undefined;
}
export async function getPackageVersion(pkg: string) {
if (packageJSONCache) {
return (packageJSONCache.dependencies?.[pkg] ||
packageJSONCache.devDependencies?.[pkg] ||
packageJSONCache.peerDependencies?.[pkg]) as string | undefined;
}
try {
const pkgJsonPath = path.join(
process.cwd(),
"node_modules",
pkg,
"package.json",
);
const raw = await fs.readFile(pkgJsonPath, "utf-8");
const json = JSON.parse(raw);
const resolved =
(json.version as string) ||
(await getVersionFromLocalPackageJson(pkg)) ||
undefined;
return resolved;
} catch {}
const fromRoot = await getVersionFromLocalPackageJson(pkg);
return fromRoot;
}
async function getVersionFromLocalPackageJson(pkg: string) {
const json = await readRootPackageJson();
if (!json) return undefined;
const allDeps = {
...json.dependencies,
...json.devDependencies,
...json.peerDependencies,
} as Record<string, string | undefined>;
return allDeps[pkg];
}
export async function getNameFromLocalPackageJson() {
const json = await readRootPackageJson();
return json?.name as string | undefined;
}

View File

@@ -3,7 +3,7 @@ import { getConfig } from "../utils/get-config";
import * as z from "zod/v4";
import { existsSync } from "fs";
import path from "path";
import { logger } from "better-auth";
import { logger, createTelemetry, getTelemetryAuthConfig } from "better-auth";
import yoctoSpinner from "yocto-spinner";
import prompts from "prompts";
import fs from "fs/promises";
@@ -54,6 +54,21 @@ export async function generateAction(opts: any) {
spinner.stop();
if (!schema.code) {
logger.info("Your schema is already up to date.");
// telemetry: track generate attempted, no changes
try {
const telemetry = await createTelemetry(config);
await telemetry.publish({
type: "cli_generate",
payload: {
outcome: "no_changes",
config: getTelemetryAuthConfig(config, {
adapter: adapter.id,
database:
typeof config.database === "function" ? "adapter" : "kysely",
}),
},
});
} catch {}
process.exit(0);
}
if (schema.overwrite) {
@@ -88,9 +103,31 @@ export async function generateAction(opts: any) {
schema.overwrite ? "overwritten" : "appended"
} successfully!`,
);
// telemetry: track generate success overwrite/append
try {
const telemetry = await createTelemetry(config);
await telemetry.publish({
type: "cli_generate",
payload: {
outcome: schema.overwrite ? "overwritten" : "appended",
config: getTelemetryAuthConfig(config),
},
});
} catch {}
process.exit(0);
} else {
logger.error("Schema generation aborted.");
// telemetry: track generate aborted
try {
const telemetry = await createTelemetry(config);
await telemetry.publish({
type: "cli_generate",
payload: {
outcome: "aborted",
config: getTelemetryAuthConfig(config),
},
});
} catch {}
process.exit(1);
}
}
@@ -115,6 +152,14 @@ export async function generateAction(opts: any) {
if (!confirm) {
logger.error("Schema generation aborted.");
// telemetry: track generate aborted before write
try {
const telemetry = await createTelemetry(config);
await telemetry.publish({
type: "cli_generate",
payload: { outcome: "aborted", config: getTelemetryAuthConfig(config) },
});
} catch {}
process.exit(1);
}
@@ -131,6 +176,14 @@ export async function generateAction(opts: any) {
schema.code,
);
logger.success(`🚀 Schema was generated successfully!`);
// telemetry: track generate success
try {
const telemetry = await createTelemetry(config);
await telemetry.publish({
type: "cli_generate",
payload: { outcome: "generated", config: getTelemetryAuthConfig(config) },
});
} catch {}
process.exit(0);
}

View File

@@ -5,7 +5,7 @@ import path from "path";
import yoctoSpinner from "yocto-spinner";
import chalk from "chalk";
import prompts from "prompts";
import { logger } from "better-auth";
import { logger, createTelemetry, getTelemetryAuthConfig } from "better-auth";
import { getAdapter, getMigrations } from "better-auth/db";
import { getConfig } from "../utils/get-config";
@@ -50,15 +50,48 @@ export async function migrateAction(opts: any) {
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 Prismas migrate or push to apply it.",
);
try {
const telemetry = await createTelemetry(config);
await telemetry.publish({
type: "cli_migrate",
payload: {
outcome: "unsupported_adapter",
adapter: "prisma",
config: getTelemetryAuthConfig(config),
},
});
} catch {}
process.exit(0);
}
if (db.id === "drizzle") {
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 Drizzles migrate or push to apply it.",
);
try {
const telemetry = await createTelemetry(config);
await telemetry.publish({
type: "cli_migrate",
payload: {
outcome: "unsupported_adapter",
adapter: "drizzle",
config: getTelemetryAuthConfig(config),
},
});
} catch {}
process.exit(0);
}
logger.error("Migrate command isn't supported for this adapter.");
try {
const telemetry = await createTelemetry(config);
await telemetry.publish({
type: "cli_migrate",
payload: {
outcome: "unsupported_adapter",
adapter: db.id,
config: getTelemetryAuthConfig(config),
},
});
} catch {}
process.exit(1);
}
@@ -69,6 +102,16 @@ export async function migrateAction(opts: any) {
if (!toBeAdded.length && !toBeCreated.length) {
spinner.stop();
logger.info("🚀 No migrations needed.");
try {
const telemetry = await createTelemetry(config);
await telemetry.publish({
type: "cli_migrate",
payload: {
outcome: "no_changes",
config: getTelemetryAuthConfig(config),
},
});
} catch {}
process.exit(0);
}
@@ -103,6 +146,13 @@ export async function migrateAction(opts: any) {
if (!migrate) {
logger.info("Migration cancelled.");
try {
const telemetry = await createTelemetry(config);
await telemetry.publish({
type: "cli_migrate",
payload: { outcome: "aborted", config: getTelemetryAuthConfig(config) },
});
} catch {}
process.exit(0);
}
@@ -110,6 +160,13 @@ export async function migrateAction(opts: any) {
await runMigrations();
spinner.stop();
logger.info("🚀 migration was completed successfully!");
try {
const telemetry = await createTelemetry(config);
await telemetry.publish({
type: "cli_migrate",
payload: { outcome: "migrated", config: getTelemetryAuthConfig(config) },
});
} catch {}
process.exit(0);
}

View File

@@ -1,18 +1,22 @@
#!/usr/bin/env node
import { Command } from "commander";
import { init } from "./commands/init";
import { migrate } from "./commands/migrate";
import { generate } from "./commands/generate";
import "dotenv/config";
import { generateSecret } from "./commands/secret";
import { getPackageInfo } from "./utils/get-package-info";
import { init } from "./commands/init";
import "dotenv/config";
// handle exit
process.on("SIGINT", () => process.exit(0));
process.on("SIGTERM", () => process.exit(0));
async function main() {
const program = new Command("better-auth");
let packageInfo: Record<string, any> = {};
try {
packageInfo = await getPackageInfo();
@@ -20,12 +24,14 @@ async function main() {
// it doesn't matter if we can't read the package.json file, we'll just use an empty object
}
program
.addCommand(init)
.addCommand(migrate)
.addCommand(generate)
.addCommand(generateSecret)
.addCommand(init)
.version(packageInfo.version || "1.1.2")
.description("Better Auth CLI");
.description("Better Auth CLI")
.action(() => program.help());
program.parse();
}

3
pnpm-lock.yaml generated
View File

@@ -1567,6 +1567,9 @@ importers:
tedious:
specifier: ^18.6.1
version: 18.6.1
type-fest:
specifier: ^4.41.0
version: 4.41.0
typescript:
specifier: 'catalog:'
version: 5.9.2