mirror of
https://github.com/better-auth/better-auth.git
synced 2026-10-18 11:32:15 -05:00
Merge branch 'canary' into fix/openapi-required-id-field
This commit is contained in:
@@ -19,8 +19,6 @@
|
||||
"@libsql/client": "^0.8.1",
|
||||
"@libsql/kysely-libsql": "^0.4.1",
|
||||
"@number-flow/react": "^0.5.10",
|
||||
"@prisma/adapter-libsql": "^5.22.0",
|
||||
"@prisma/client": "^5.22.0",
|
||||
"@radix-ui/react-accordion": "^1.2.12",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||
"@radix-ui/react-aspect-ratio": "^1.1.7",
|
||||
@@ -67,7 +65,7 @@
|
||||
"lucide-react": "^0.542.0",
|
||||
"mini-svg-data-uri": "^1.4.4",
|
||||
"mysql2": "^3.14.4",
|
||||
"next": "16.0.1",
|
||||
"next": "16.0.7",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "catalog:react19",
|
||||
"react-day-picker": "9.9.0",
|
||||
@@ -92,7 +90,6 @@
|
||||
"@types/react-dom": "catalog:react19",
|
||||
"@types/ua-parser-js": "^0.7.39",
|
||||
"postcss": "^8.5.6",
|
||||
"prisma": "^5.22.0",
|
||||
"tailwindcss": "^4.1.13",
|
||||
"tw-animate-css": "^1.3.8"
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"better-auth": "workspace:*",
|
||||
"next": "16.0.1",
|
||||
"next": "16.0.7",
|
||||
"react": "catalog:react19",
|
||||
"react-dom": "catalog:react19"
|
||||
},
|
||||
|
||||
@@ -999,7 +999,7 @@ 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: "Tiktok",
|
||||
title: "TikTok",
|
||||
href: "/docs/authentication/tiktok",
|
||||
icon: () => (
|
||||
<svg
|
||||
|
||||
@@ -59,3 +59,44 @@ const signIn = async () => {
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### Middleware
|
||||
|
||||
You can use TanStack Start's middleware to protect routes that require authentication. Create a middleware that checks for a valid session and redirects unauthenticated users to the login page.
|
||||
|
||||
```ts title="src/middleware/auth.ts"
|
||||
import { redirect } from "@tanstack/react-router";
|
||||
import { createMiddleware } from "@tanstack/react-start";
|
||||
import { auth } from "./auth";
|
||||
|
||||
export const authMiddleware = createMiddleware().server(
|
||||
async ({ next, request }) => {
|
||||
const session = await auth.api.getSession({ headers: request.headers })
|
||||
|
||||
if (!session) {
|
||||
throw redirect({ to: "/login" })
|
||||
}
|
||||
|
||||
return await next()
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
You can then use this middleware in your route definitions to protect specific routes:
|
||||
|
||||
```tsx title="src/routes/dashboard.tsx"
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { authMiddleware } from '@/lib/middleware'
|
||||
|
||||
export const Route = createFileRoute('/dashboard')({
|
||||
component: RouteComponent,
|
||||
server: {
|
||||
middleware: [authMiddleware],
|
||||
},
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
return <div>Hello "/dashboard"!</div>
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -612,7 +612,7 @@ You can set the active organization by calling the `organization.setActive` func
|
||||
set to `null`.
|
||||
</Callout>
|
||||
|
||||
<APIMethod path="/organization/set-active" method="POST">
|
||||
<APIMethod path="/organization/set-active" method="POST" requireSession>
|
||||
```ts
|
||||
type setActiveOrganization = {
|
||||
/**
|
||||
@@ -858,7 +858,7 @@ export const auth = betterAuth({
|
||||
|
||||
To invite users to an organization, you can use the `invite` function provided by the client. The `invite` function takes an object with the following properties:
|
||||
|
||||
<APIMethod path="/organization/invite-member" method="POST">
|
||||
<APIMethod path="/organization/invite-member" method="POST" requireSession>
|
||||
```ts
|
||||
type createInvitation = {
|
||||
/**
|
||||
@@ -900,7 +900,7 @@ When a user receives an invitation email, they can click on the invitation link
|
||||
|
||||
Make sure to call the `acceptInvitation` function after the user is logged in.
|
||||
|
||||
<APIMethod path="/organization/accept-invitation" method="POST">
|
||||
<APIMethod path="/organization/accept-invitation" method="POST" requireSession>
|
||||
```ts
|
||||
type acceptInvitation = {
|
||||
/**
|
||||
@@ -937,7 +937,7 @@ If a user has sent out an invitation, you can use this method to cancel it.
|
||||
|
||||
If you're looking for how a user can reject an invitation, you can find that [here](#reject-invitation).
|
||||
|
||||
<APIMethod path="/organization/cancel-invitation" method="POST" noResult>
|
||||
<APIMethod path="/organization/cancel-invitation" method="POST" noResult requireSession>
|
||||
```ts
|
||||
type cancelInvitation = {
|
||||
/**
|
||||
@@ -952,7 +952,7 @@ type cancelInvitation = {
|
||||
|
||||
If this user has received an invitation, but wants to decline it, this method will allow you to do so by rejecting it.
|
||||
|
||||
<APIMethod path="/organization/reject-invitation" method="POST" noResult>
|
||||
<APIMethod path="/organization/reject-invitation" method="POST" noResult requireSession>
|
||||
```ts
|
||||
type rejectInvitation = {
|
||||
/**
|
||||
@@ -993,7 +993,7 @@ type getInvitation = {
|
||||
|
||||
To list all invitations for a given organization you can use the `listInvitations` function provided by the client.
|
||||
|
||||
<APIMethod path="/organization/list-invitations" method="GET">
|
||||
<APIMethod path="/organization/list-invitations" method="GET" requireSession>
|
||||
```ts
|
||||
type listInvitations = {
|
||||
/**
|
||||
@@ -1033,7 +1033,7 @@ const invitations = await auth.api.listUserInvitations({
|
||||
|
||||
To list all members of an organization you can use the `listMembers` function.
|
||||
|
||||
<APIMethod path="/organization/list-members" method="GET">
|
||||
<APIMethod path="/organization/list-members" method="GET" requireSession>
|
||||
|
||||
```ts
|
||||
type listMembers = {
|
||||
@@ -1077,7 +1077,7 @@ type listMembers = {
|
||||
|
||||
To remove you can use `organization.removeMember`
|
||||
|
||||
<APIMethod path="/organization/remove-member" method="POST">
|
||||
<APIMethod path="/organization/remove-member" method="POST" requireSession>
|
||||
```ts
|
||||
type removeMember = {
|
||||
/**
|
||||
@@ -1096,7 +1096,7 @@ type removeMember = {
|
||||
|
||||
To update the role of a member in an organization, you can use the `organization.updateMemberRole`. If the user has the permission to update the role of the member, the role will be updated.
|
||||
|
||||
<APIMethod path="/organization/update-member-role" method="POST" noResult>
|
||||
<APIMethod path="/organization/update-member-role" method="POST" noResult requireSession>
|
||||
```ts
|
||||
type updateMemberRole = {
|
||||
/**
|
||||
@@ -1911,7 +1911,7 @@ type removeTeam = {
|
||||
|
||||
Sets the given team as the current active team. If `teamId` is `null` the current active team is unset.
|
||||
|
||||
<APIMethod path="/organization/set-active-team" method="POST">
|
||||
<APIMethod path="/organization/set-active-team" method="POST" requireSession>
|
||||
```ts
|
||||
type setActiveTeam = {
|
||||
/**
|
||||
@@ -1926,7 +1926,7 @@ type setActiveTeam = {
|
||||
|
||||
List all teams that the current user is a part of.
|
||||
|
||||
<APIMethod path="/organization/list-user-teams" method="GET">
|
||||
<APIMethod path="/organization/list-user-teams" method="GET" requireSession>
|
||||
```ts
|
||||
type listUserTeams = {
|
||||
}
|
||||
@@ -1937,7 +1937,7 @@ List all teams that the current user is a part of.
|
||||
|
||||
List the members of the given team.
|
||||
|
||||
<APIMethod path="/organization/list-team-members" method="POST" forceAsQuery>
|
||||
<APIMethod path="/organization/list-team-members" method="POST" forceAsQuery requireSession>
|
||||
```ts
|
||||
type listTeamMembers = {
|
||||
/**
|
||||
@@ -1952,7 +1952,7 @@ type listTeamMembers = {
|
||||
|
||||
Add a member to a team.
|
||||
|
||||
<APIMethod path="/organization/add-team-member" method="POST">
|
||||
<APIMethod path="/organization/add-team-member" method="POST" requireSession>
|
||||
```ts
|
||||
type addTeamMember = {
|
||||
/**
|
||||
@@ -1971,7 +1971,7 @@ type addTeamMember = {
|
||||
|
||||
Remove a member from a team.
|
||||
|
||||
<APIMethod path="/organization/remove-team-member" method="POST">
|
||||
<APIMethod path="/organization/remove-team-member" method="POST" requireSession>
|
||||
```ts
|
||||
type removeTeamMember = {
|
||||
/**
|
||||
|
||||
+1
-1
@@ -74,7 +74,7 @@
|
||||
"lottie-react": "^2.4.1",
|
||||
"lucide-react": "^0.542.0",
|
||||
"motion": "^12.23.12",
|
||||
"next": "16.0.1",
|
||||
"next": "16.0.7",
|
||||
"next-themes": "^0.4.6",
|
||||
"prism-react-renderer": "^2.4.1",
|
||||
"react": "catalog:react19",
|
||||
|
||||
+2
-1
@@ -6,6 +6,7 @@
|
||||
"typecheck": "tsc --project tsconfig.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"better-auth": "workspace:*"
|
||||
"better-auth": "workspace:*",
|
||||
"@better-auth/sso": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { betterAuth } from "better-auth";
|
||||
import { oidcProvider } from "better-auth/plugins";
|
||||
|
||||
export const auth = betterAuth({
|
||||
plugins: [
|
||||
oidcProvider({
|
||||
loginPage: "/login",
|
||||
}),
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
import { sso } from "@better-auth/sso";
|
||||
import { betterAuth } from "better-auth";
|
||||
|
||||
export const auth = betterAuth({
|
||||
plugins: [sso()],
|
||||
});
|
||||
@@ -449,10 +449,10 @@
|
||||
"@noble/ciphers": "^2.0.0",
|
||||
"@noble/hashes": "^2.0.0",
|
||||
"better-call": "catalog:",
|
||||
"ms": "4.0.0-nightly.202508271359",
|
||||
"defu": "^6.1.4",
|
||||
"jose": "^6.1.0",
|
||||
"kysely": "^0.28.5",
|
||||
"ms": "4.0.0-nightly.202508271359",
|
||||
"nanostores": "^1.0.1",
|
||||
"zod": "^4.1.12"
|
||||
},
|
||||
@@ -471,7 +471,7 @@
|
||||
"happy-dom": "^20.0.10",
|
||||
"listhen": "^1.9.0",
|
||||
"mongodb": "^6.18.0",
|
||||
"msw": "^2.12.1",
|
||||
"msw": "^2.12.4",
|
||||
"mysql2": "^3.14.4",
|
||||
"next": "^16.0.1",
|
||||
"oauth2-mock-server": "^7.2.1",
|
||||
|
||||
@@ -13,6 +13,83 @@ import { betterAuth } from "../auth";
|
||||
import type { Account, Session, User, Verification } from "../types";
|
||||
import type { Logger } from "./test-adapter";
|
||||
|
||||
/**
|
||||
* Test entry type that supports both callback and object formats.
|
||||
* Object format allows specifying migration options that will be applied before the test runs.
|
||||
*/
|
||||
export type TestEntry =
|
||||
| ((context: {
|
||||
readonly skip: {
|
||||
(note?: string | undefined): never;
|
||||
(condition: boolean, note?: string | undefined): void;
|
||||
};
|
||||
}) => Promise<void>)
|
||||
| {
|
||||
migrateBetterAuth?: BetterAuthOptions;
|
||||
test: (context: {
|
||||
readonly skip: {
|
||||
(note?: string | undefined): never;
|
||||
(condition: boolean, note?: string | undefined): void;
|
||||
};
|
||||
}) => Promise<void>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Deep equality comparison for BetterAuthOptions.
|
||||
* Handles nested objects, arrays, and primitive values.
|
||||
*/
|
||||
function deepEqual(a: any, b: any): boolean {
|
||||
if (a === b) return true;
|
||||
if (a == null || b == null) return a === b;
|
||||
if (typeof a !== typeof b) return false;
|
||||
|
||||
if (typeof a === "object") {
|
||||
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
||||
|
||||
if (Array.isArray(a)) {
|
||||
if (a.length !== b.length) return false;
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (!deepEqual(a[i], b[i])) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const keysA = Object.keys(a);
|
||||
const keysB = Object.keys(b);
|
||||
|
||||
if (keysA.length !== keysB.length) return false;
|
||||
|
||||
for (const key of keysA) {
|
||||
if (!keysB.includes(key)) return false;
|
||||
if (!deepEqual(a[key], b[key])) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Statistics tracking for test suites.
|
||||
*/
|
||||
export type TestSuiteStats = {
|
||||
migrationCount: number;
|
||||
totalMigrationTime: number;
|
||||
testCount: number;
|
||||
suiteStartTime: number;
|
||||
suiteDuration: number;
|
||||
suiteName: string;
|
||||
groupingStats?: {
|
||||
totalGroups: number;
|
||||
averageTestsPerGroup: number;
|
||||
largestGroupSize: number;
|
||||
smallestGroupSize: number;
|
||||
groupsWithMultipleTests: number;
|
||||
totalTestsInGroups: number;
|
||||
};
|
||||
};
|
||||
|
||||
type GenerateFn = <M extends "user" | "session" | "verification" | "account">(
|
||||
Model: M,
|
||||
) => Promise<
|
||||
@@ -81,20 +158,7 @@ export type InsertRandomFn = <
|
||||
>;
|
||||
|
||||
export const createTestSuite = <
|
||||
Tests extends Record<
|
||||
string,
|
||||
(context: {
|
||||
/**
|
||||
* Mark tests as skipped. All execution after this call will be skipped.
|
||||
* This function throws an error, so make sure you are not catching it accidentally.
|
||||
* @see {@link https://vitest.dev/guide/test-context#skip}
|
||||
*/
|
||||
readonly skip: {
|
||||
(note?: string | undefined): never;
|
||||
(condition: boolean, note?: string | undefined): void;
|
||||
};
|
||||
}) => Promise<void>
|
||||
>,
|
||||
Tests extends Record<string, TestEntry>,
|
||||
AdditionalOptions extends Record<string, any> = {},
|
||||
>(
|
||||
suiteName: string,
|
||||
@@ -177,7 +241,7 @@ export const createTestSuite = <
|
||||
cleanup: () => Promise<void>;
|
||||
runMigrations: () => Promise<void>;
|
||||
prefixTests?: string | undefined;
|
||||
onTestFinish: () => Promise<void>;
|
||||
onTestFinish: (stats: TestSuiteStats) => Promise<void>;
|
||||
customIdGenerator?: () => any | Promise<any> | undefined;
|
||||
transformIdOutput?: (id: any) => string | undefined;
|
||||
}) => {
|
||||
@@ -310,18 +374,57 @@ export const createTestSuite = <
|
||||
}
|
||||
};
|
||||
|
||||
let didMigrateOnOptionsModify = false;
|
||||
// Track current applied BetterAuth options state
|
||||
let currentAppliedOptions: BetterAuthOptions | null = null;
|
||||
|
||||
const resetBetterAuthOptions = async () => {
|
||||
adapter = await helpers.adapter();
|
||||
await helpers.modifyBetterAuthOptions(
|
||||
config.defaultBetterAuthOptions || {},
|
||||
// Statistics tracking
|
||||
const stats: TestSuiteStats = {
|
||||
migrationCount: 0,
|
||||
totalMigrationTime: 0,
|
||||
testCount: 0,
|
||||
suiteStartTime: performance.now(),
|
||||
suiteDuration: 0,
|
||||
suiteName,
|
||||
};
|
||||
|
||||
/**
|
||||
* Apply BetterAuth options and run migrations if needed.
|
||||
* Tracks migration statistics.
|
||||
*/
|
||||
const applyOptionsAndMigrate = async (
|
||||
options: BetterAuthOptions | Partial<BetterAuthOptions>,
|
||||
forceMigrate: boolean = false,
|
||||
): Promise<BetterAuthOptions> => {
|
||||
const finalOptions = deepmerge(
|
||||
config?.defaultBetterAuthOptions || {},
|
||||
options || {},
|
||||
);
|
||||
if (didMigrateOnOptionsModify) {
|
||||
didMigrateOnOptionsModify = false;
|
||||
await helpers.runMigrations();
|
||||
|
||||
// Check if options have changed
|
||||
const optionsChanged = !deepEqual(currentAppliedOptions, finalOptions);
|
||||
|
||||
if (optionsChanged || forceMigrate) {
|
||||
adapter = await helpers.adapter();
|
||||
await helpers.modifyBetterAuthOptions(finalOptions);
|
||||
|
||||
if (config.alwaysMigrate || forceMigrate) {
|
||||
const migrationStart = performance.now();
|
||||
await helpers.runMigrations();
|
||||
const migrationTime = performance.now() - migrationStart;
|
||||
|
||||
stats.migrationCount++;
|
||||
stats.totalMigrationTime += migrationTime;
|
||||
|
||||
adapter = await helpers.adapter();
|
||||
}
|
||||
|
||||
currentAppliedOptions = finalOptions;
|
||||
} else {
|
||||
// Options haven't changed, just update the reference
|
||||
currentAppliedOptions = finalOptions;
|
||||
}
|
||||
|
||||
return finalOptions;
|
||||
};
|
||||
|
||||
const transformGeneratedModel = (data: Record<string, any>) => {
|
||||
@@ -493,14 +596,7 @@ export const createTestSuite = <
|
||||
opts: BetterAuthOptions,
|
||||
shouldRunMigrations: boolean,
|
||||
) => {
|
||||
const res = helpers.modifyBetterAuthOptions(
|
||||
deepmerge(config?.defaultBetterAuthOptions || {}, opts),
|
||||
);
|
||||
if (config.alwaysMigrate || shouldRunMigrations) {
|
||||
didMigrateOnOptionsModify = true;
|
||||
await helpers.runMigrations();
|
||||
}
|
||||
return res;
|
||||
return await applyOptionsAndMigrate(opts, shouldRunMigrations);
|
||||
};
|
||||
|
||||
const additionalOptions = { ...options };
|
||||
@@ -558,55 +654,231 @@ export const createTestSuite = <
|
||||
await helpers.cleanup();
|
||||
} catch {}
|
||||
if (config.defaultBetterAuthOptions && !allDisabled) {
|
||||
await helpers.modifyBetterAuthOptions(
|
||||
await applyOptionsAndMigrate(
|
||||
config.defaultBetterAuthOptions,
|
||||
config.alwaysMigrate,
|
||||
);
|
||||
if (config.alwaysMigrate) {
|
||||
await helpers.runMigrations();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Extract test function and migration options from a test entry.
|
||||
*/
|
||||
const extractTestEntry = (
|
||||
entry: TestEntry,
|
||||
): {
|
||||
testFn: (context: {
|
||||
readonly skip: {
|
||||
(note?: string | undefined): never;
|
||||
(condition: boolean, note?: string | undefined): void;
|
||||
};
|
||||
}) => Promise<void>;
|
||||
migrateBetterAuth?: BetterAuthOptions;
|
||||
} => {
|
||||
if (typeof entry === "function") {
|
||||
return { testFn: entry };
|
||||
}
|
||||
return {
|
||||
testFn: entry.test,
|
||||
migrateBetterAuth: entry.migrateBetterAuth,
|
||||
};
|
||||
};
|
||||
|
||||
// Convert test entries to array with migration info (moved before onFinish for access)
|
||||
const testEntries = Object.entries(fullTests).map(([name, entry]) => {
|
||||
const { testFn, migrateBetterAuth } = extractTestEntry(
|
||||
entry as TestEntry,
|
||||
);
|
||||
return { name, testFn, migrateBetterAuth };
|
||||
});
|
||||
|
||||
/**
|
||||
* Group tests by their migrateBetterAuth options.
|
||||
* Tests with equal migration options are grouped together.
|
||||
*/
|
||||
type TestGroup = {
|
||||
migrationOptions: BetterAuthOptions | null | undefined;
|
||||
testIndices: number[];
|
||||
};
|
||||
|
||||
const groupTestsByMigrationOptions = (): TestGroup[] => {
|
||||
const groups: TestGroup[] = [];
|
||||
let currentGroup: TestGroup | null = null;
|
||||
|
||||
for (let i = 0; i < testEntries.length; i++) {
|
||||
const { migrateBetterAuth } = testEntries[i]!;
|
||||
const isSkipped =
|
||||
(allDisabled &&
|
||||
options?.disableTests?.[testEntries[i]!.name] !== false) ||
|
||||
(options?.disableTests?.[testEntries[i]!.name] ?? false);
|
||||
|
||||
// Skip grouping for skipped tests - they'll be handled individually
|
||||
if (isSkipped) {
|
||||
if (currentGroup) {
|
||||
groups.push(currentGroup);
|
||||
currentGroup = null;
|
||||
}
|
||||
groups.push({
|
||||
migrationOptions: migrateBetterAuth,
|
||||
testIndices: [i],
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if this test belongs to the current group
|
||||
if (
|
||||
currentGroup &&
|
||||
deepEqual(currentGroup.migrationOptions, migrateBetterAuth)
|
||||
) {
|
||||
currentGroup.testIndices.push(i);
|
||||
} else {
|
||||
// Start a new group
|
||||
if (currentGroup) {
|
||||
groups.push(currentGroup);
|
||||
}
|
||||
currentGroup = {
|
||||
migrationOptions: migrateBetterAuth,
|
||||
testIndices: [i],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Add the last group if it exists
|
||||
if (currentGroup) {
|
||||
groups.push(currentGroup);
|
||||
}
|
||||
|
||||
return groups;
|
||||
};
|
||||
|
||||
const testGroups = groupTestsByMigrationOptions();
|
||||
|
||||
// Calculate grouping statistics
|
||||
const calculateGroupingStats = () => {
|
||||
const nonSkippedGroups = testGroups.filter(
|
||||
(group) => group.testIndices.length > 0,
|
||||
);
|
||||
const groupSizes = nonSkippedGroups.map(
|
||||
(group) => group.testIndices.length,
|
||||
);
|
||||
|
||||
if (groupSizes.length === 0) {
|
||||
return {
|
||||
totalGroups: 0,
|
||||
averageTestsPerGroup: 0,
|
||||
largestGroupSize: 0,
|
||||
smallestGroupSize: 0,
|
||||
groupsWithMultipleTests: 0,
|
||||
totalTestsInGroups: 0,
|
||||
};
|
||||
}
|
||||
|
||||
const totalTestsInGroups = groupSizes.reduce(
|
||||
(sum, size) => sum + size,
|
||||
0,
|
||||
);
|
||||
const groupsWithMultipleTests = groupSizes.filter(
|
||||
(size) => size > 1,
|
||||
).length;
|
||||
|
||||
return {
|
||||
totalGroups: nonSkippedGroups.length,
|
||||
averageTestsPerGroup: totalTestsInGroups / nonSkippedGroups.length,
|
||||
largestGroupSize: Math.max(...groupSizes),
|
||||
smallestGroupSize: Math.min(...groupSizes),
|
||||
groupsWithMultipleTests,
|
||||
totalTestsInGroups,
|
||||
};
|
||||
};
|
||||
|
||||
const onFinish = async (testName: string) => {
|
||||
await cleanupCreatedRows();
|
||||
await resetBetterAuthOptions();
|
||||
// Check if this is the last test by comparing current test index with total tests
|
||||
const testEntries = Object.entries(fullTests);
|
||||
|
||||
const currentTestIndex = testEntries.findIndex(
|
||||
([name]) =>
|
||||
name === testName.replace(/\[.*?\] /, "").replace(/ ─ /g, " - "),
|
||||
({ name }) => name === testName,
|
||||
);
|
||||
const isLastTest = currentTestIndex === testEntries.length - 1;
|
||||
|
||||
if (isLastTest) {
|
||||
await helpers.onTestFinish();
|
||||
stats.suiteDuration = performance.now() - stats.suiteStartTime;
|
||||
stats.groupingStats = calculateGroupingStats();
|
||||
await helpers.onTestFinish(stats);
|
||||
}
|
||||
};
|
||||
|
||||
if (allDisabled) {
|
||||
await resetBetterAuthOptions();
|
||||
}
|
||||
// Track the current group's migration options
|
||||
let currentGroupMigrationOptions: BetterAuthOptions | null | undefined =
|
||||
null;
|
||||
|
||||
for (let i = 0; i < testEntries.length; i++) {
|
||||
const { name: testName, testFn, migrateBetterAuth } = testEntries[i]!;
|
||||
|
||||
// Find which group this test belongs to
|
||||
const testGroup = testGroups.find((group) =>
|
||||
group.testIndices.includes(i),
|
||||
);
|
||||
const isFirstInGroup = testGroup && testGroup.testIndices[0] === i;
|
||||
|
||||
for (let [testName, testFn] of Object.entries(fullTests)) {
|
||||
let shouldSkip =
|
||||
(allDisabled && options?.disableTests?.[testName] !== false) ||
|
||||
(options?.disableTests?.[testName] ?? false);
|
||||
testName = testName.replace(
|
||||
|
||||
let displayName = testName.replace(
|
||||
" - ",
|
||||
` ${TTY_COLORS.dim}${dash}${TTY_COLORS.undim} `,
|
||||
);
|
||||
if (config.prefixTests) {
|
||||
testName = `${config.prefixTests} ${TTY_COLORS.dim}>${TTY_COLORS.undim} ${testName}`;
|
||||
displayName = `${config.prefixTests} ${TTY_COLORS.dim}>${TTY_COLORS.undim} ${displayName}`;
|
||||
}
|
||||
if (helpers.prefixTests) {
|
||||
testName = `[${TTY_COLORS.dim}${helpers.prefixTests}${TTY_COLORS.undim}] ${testName}`;
|
||||
displayName = `[${TTY_COLORS.dim}${helpers.prefixTests}${TTY_COLORS.undim}] ${displayName}`;
|
||||
}
|
||||
|
||||
test.skipIf(shouldSkip)(
|
||||
testName,
|
||||
displayName,
|
||||
{ timeout: 30000 },
|
||||
async ({ onTestFailed, skip }) => {
|
||||
resetDebugLogs();
|
||||
|
||||
// Apply migration options before test runs
|
||||
await (async () => {
|
||||
if (shouldSkip) return;
|
||||
|
||||
const thisMigration = deepmerge(
|
||||
config.defaultBetterAuthOptions || {},
|
||||
migrateBetterAuth || {},
|
||||
);
|
||||
|
||||
// If this is the first test in a group, migrate to the group's options
|
||||
if (isFirstInGroup && testGroup) {
|
||||
const groupMigrationOptions = testGroup.migrationOptions;
|
||||
const groupFinalOptions = deepmerge(
|
||||
config.defaultBetterAuthOptions || {},
|
||||
groupMigrationOptions || {},
|
||||
);
|
||||
|
||||
// Only migrate if the group's options are different from current state
|
||||
if (
|
||||
!deepEqual(
|
||||
currentGroupMigrationOptions,
|
||||
groupMigrationOptions,
|
||||
)
|
||||
) {
|
||||
await applyOptionsAndMigrate(groupFinalOptions, true);
|
||||
currentGroupMigrationOptions = groupMigrationOptions;
|
||||
}
|
||||
}
|
||||
// If this test is not in a group or not first in group, check if migration is needed
|
||||
else if (
|
||||
!deepEqual(currentGroupMigrationOptions, migrateBetterAuth)
|
||||
) {
|
||||
await applyOptionsAndMigrate(thisMigration, true);
|
||||
currentGroupMigrationOptions = migrateBetterAuth;
|
||||
}
|
||||
})();
|
||||
|
||||
stats.testCount++;
|
||||
|
||||
onTestFailed(async () => {
|
||||
printDebugLogs();
|
||||
await onFinish(testName);
|
||||
|
||||
@@ -4,7 +4,7 @@ import { deepmerge, initGetModelName } from "@better-auth/core/db/adapter";
|
||||
import { TTY_COLORS } from "@better-auth/core/env";
|
||||
import { afterAll, beforeAll, describe } from "vitest";
|
||||
import { getAuthTables } from "../db";
|
||||
import type { createTestSuite } from "./create-test-suite";
|
||||
import type { createTestSuite, TestSuiteStats } from "./create-test-suite";
|
||||
|
||||
export type Logger = {
|
||||
info: (...args: any[]) => void;
|
||||
@@ -197,12 +197,158 @@ export const testAdapter = async ({
|
||||
return {
|
||||
execute: () => {
|
||||
describe(adapterDisplayName, async () => {
|
||||
// Collect statistics from all test suites
|
||||
const allSuiteStats: TestSuiteStats[] = [];
|
||||
|
||||
beforeAll(async () => {
|
||||
await migrate();
|
||||
}, 60000);
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanup();
|
||||
|
||||
// Display statistics summary
|
||||
if (allSuiteStats.length > 0) {
|
||||
const totalMigrations = allSuiteStats.reduce(
|
||||
(sum, stats) => sum + stats.migrationCount,
|
||||
0,
|
||||
);
|
||||
const totalMigrationTime = allSuiteStats.reduce(
|
||||
(sum, stats) => sum + stats.totalMigrationTime,
|
||||
0,
|
||||
);
|
||||
const totalTests = allSuiteStats.reduce(
|
||||
(sum, stats) => sum + stats.testCount,
|
||||
0,
|
||||
);
|
||||
const totalDuration = allSuiteStats.reduce(
|
||||
(sum, stats) => sum + stats.suiteDuration,
|
||||
0,
|
||||
);
|
||||
|
||||
const dash = "─";
|
||||
const separator = `${dash.repeat(80)}`;
|
||||
|
||||
console.log(`\n${TTY_COLORS.fg.cyan}${separator}`);
|
||||
console.log(
|
||||
`${TTY_COLORS.fg.cyan}${TTY_COLORS.bright}TEST SUITE STATISTICS SUMMARY${TTY_COLORS.reset}`,
|
||||
);
|
||||
console.log(
|
||||
`${TTY_COLORS.fg.cyan}${separator}${TTY_COLORS.reset}\n`,
|
||||
);
|
||||
|
||||
// Per-suite breakdown
|
||||
for (const stats of allSuiteStats) {
|
||||
const avgMigrationTime =
|
||||
stats.migrationCount > 0
|
||||
? (stats.totalMigrationTime / stats.migrationCount).toFixed(2)
|
||||
: "0.00";
|
||||
console.log(
|
||||
`${TTY_COLORS.fg.magenta}${stats.suiteName}${TTY_COLORS.reset}:`,
|
||||
);
|
||||
console.log(
|
||||
` Tests: ${TTY_COLORS.fg.green}${stats.testCount}${TTY_COLORS.reset}`,
|
||||
);
|
||||
console.log(
|
||||
` Migrations: ${TTY_COLORS.fg.yellow}${stats.migrationCount}${TTY_COLORS.reset} (avg: ${avgMigrationTime}ms)`,
|
||||
);
|
||||
console.log(
|
||||
` Total Migration Time: ${TTY_COLORS.fg.yellow}${stats.totalMigrationTime.toFixed(2)}ms${TTY_COLORS.reset}`,
|
||||
);
|
||||
console.log(
|
||||
` Suite Duration: ${TTY_COLORS.fg.blue}${stats.suiteDuration.toFixed(2)}ms${TTY_COLORS.reset}`,
|
||||
);
|
||||
|
||||
// Display grouping statistics if available
|
||||
if (stats.groupingStats) {
|
||||
const {
|
||||
totalGroups,
|
||||
averageTestsPerGroup,
|
||||
largestGroupSize,
|
||||
smallestGroupSize,
|
||||
groupsWithMultipleTests,
|
||||
} = stats.groupingStats;
|
||||
console.log(
|
||||
` Test Groups: ${TTY_COLORS.fg.cyan}${totalGroups}${TTY_COLORS.reset}`,
|
||||
);
|
||||
if (totalGroups > 0) {
|
||||
console.log(
|
||||
` Avg Tests/Group: ${TTY_COLORS.fg.cyan}${averageTestsPerGroup.toFixed(2)}${TTY_COLORS.reset}`,
|
||||
);
|
||||
console.log(
|
||||
` Largest Group: ${TTY_COLORS.fg.cyan}${largestGroupSize}${TTY_COLORS.reset}`,
|
||||
);
|
||||
console.log(
|
||||
` Smallest Group: ${TTY_COLORS.fg.cyan}${smallestGroupSize}${TTY_COLORS.reset}`,
|
||||
);
|
||||
console.log(
|
||||
` Groups w/ Multiple Tests: ${TTY_COLORS.fg.cyan}${groupsWithMultipleTests}${TTY_COLORS.reset}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("");
|
||||
}
|
||||
|
||||
// Totals
|
||||
const avgMigrationTime =
|
||||
totalMigrations > 0
|
||||
? (totalMigrationTime / totalMigrations).toFixed(2)
|
||||
: "0.00";
|
||||
|
||||
// Calculate total grouping statistics
|
||||
const totalGroups = allSuiteStats.reduce(
|
||||
(sum, stats) => sum + (stats.groupingStats?.totalGroups || 0),
|
||||
0,
|
||||
);
|
||||
const totalGroupsWithMultipleTests = allSuiteStats.reduce(
|
||||
(sum, stats) =>
|
||||
sum + (stats.groupingStats?.groupsWithMultipleTests || 0),
|
||||
0,
|
||||
);
|
||||
const totalTestsInGroups = allSuiteStats.reduce(
|
||||
(sum, stats) =>
|
||||
sum + (stats.groupingStats?.totalTestsInGroups || 0),
|
||||
0,
|
||||
);
|
||||
const avgTestsPerGroup =
|
||||
totalGroups > 0 ? totalTestsInGroups / totalGroups : 0;
|
||||
|
||||
console.log(`${TTY_COLORS.fg.cyan}${separator}`);
|
||||
console.log(
|
||||
`${TTY_COLORS.fg.cyan}${TTY_COLORS.bright}TOTALS${TTY_COLORS.reset}`,
|
||||
);
|
||||
console.log(
|
||||
` Total Tests: ${TTY_COLORS.fg.green}${totalTests}${TTY_COLORS.reset}`,
|
||||
);
|
||||
console.log(
|
||||
` Total Migrations: ${TTY_COLORS.fg.yellow}${totalMigrations}${TTY_COLORS.reset} (avg: ${avgMigrationTime}ms)`,
|
||||
);
|
||||
console.log(
|
||||
` Total Migration Time: ${TTY_COLORS.fg.yellow}${totalMigrationTime.toFixed(2)}ms${TTY_COLORS.reset}`,
|
||||
);
|
||||
console.log(
|
||||
` Total Duration: ${TTY_COLORS.fg.blue}${totalDuration.toFixed(2)}ms${TTY_COLORS.reset}`,
|
||||
);
|
||||
|
||||
// Display total grouping statistics
|
||||
if (totalGroups > 0) {
|
||||
console.log(
|
||||
` Total Test Groups: ${TTY_COLORS.fg.cyan}${totalGroups}${TTY_COLORS.reset}`,
|
||||
);
|
||||
console.log(
|
||||
` Avg Tests/Group: ${TTY_COLORS.fg.cyan}${avgTestsPerGroup.toFixed(2)}${TTY_COLORS.reset}`,
|
||||
);
|
||||
console.log(
|
||||
` Groups w/ Multiple Tests: ${TTY_COLORS.fg.cyan}${totalGroupsWithMultipleTests}${TTY_COLORS.reset}`,
|
||||
);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`${TTY_COLORS.fg.cyan}${separator}${TTY_COLORS.reset}\n`,
|
||||
);
|
||||
}
|
||||
|
||||
await onFinish?.();
|
||||
}, 60000);
|
||||
|
||||
@@ -227,7 +373,9 @@ export const testAdapter = async ({
|
||||
cleanup,
|
||||
prefixTests,
|
||||
runMigrations: migrate,
|
||||
onTestFinish: async () => {},
|
||||
onTestFinish: async (stats: TestSuiteStats) => {
|
||||
allSuiteStats.push(stats);
|
||||
},
|
||||
customIdGenerator,
|
||||
transformIdOutput,
|
||||
});
|
||||
|
||||
@@ -97,36 +97,35 @@ export const getNormalTestSuiteTests = (
|
||||
});
|
||||
expect(findResult).toEqual(res);
|
||||
},
|
||||
"create - should return null for nullable foreign keys": async () => {
|
||||
await modifyBetterAuthOptions(
|
||||
{
|
||||
plugins: [
|
||||
{
|
||||
id: "nullable-test",
|
||||
schema: {
|
||||
testModel: {
|
||||
fields: {
|
||||
nullableReference: {
|
||||
type: "string",
|
||||
references: { field: "id", model: "user" },
|
||||
required: false,
|
||||
},
|
||||
"create - should return null for nullable foreign keys": {
|
||||
migrateBetterAuth: {
|
||||
plugins: [
|
||||
{
|
||||
id: "nullable-test",
|
||||
schema: {
|
||||
testModel: {
|
||||
fields: {
|
||||
nullableReference: {
|
||||
type: "string",
|
||||
references: { field: "id", model: "user" },
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies BetterAuthPlugin,
|
||||
],
|
||||
},
|
||||
true,
|
||||
);
|
||||
const { nullableReference } = await adapter.create<{
|
||||
nullableReference: string | null;
|
||||
}>({
|
||||
model: "testModel",
|
||||
data: { nullableReference: null },
|
||||
forceAllowId: true,
|
||||
});
|
||||
expect(nullableReference).toBeNull();
|
||||
},
|
||||
} satisfies BetterAuthPlugin,
|
||||
],
|
||||
},
|
||||
test: async () => {
|
||||
const { nullableReference } = await adapter.create<{
|
||||
nullableReference: string | null;
|
||||
}>({
|
||||
model: "testModel",
|
||||
data: { nullableReference: null },
|
||||
forceAllowId: true,
|
||||
});
|
||||
expect(nullableReference).toBeNull();
|
||||
},
|
||||
},
|
||||
|
||||
"create - should apply default values to fields": async () => {
|
||||
|
||||
@@ -38,6 +38,7 @@ export type * from "@better-fetch/fetch";
|
||||
// @ts-expect-error
|
||||
export type * from "nanostores";
|
||||
export type * from "../plugins/access";
|
||||
export type * from "../plugins/organization";
|
||||
export type * from "../types/helper";
|
||||
export type { UnionToIntersection } from "../types/helper";
|
||||
//#endregion
|
||||
|
||||
@@ -3,6 +3,7 @@ import type {
|
||||
BetterAuthClientPlugin,
|
||||
} from "@better-auth/core";
|
||||
import type { BASE_ERROR_CODES } from "@better-auth/core/error";
|
||||
import { capitalizeFirstLetter } from "@better-auth/core/utils";
|
||||
import type {
|
||||
BetterFetchError,
|
||||
BetterFetchResponse,
|
||||
@@ -23,10 +24,6 @@ function getAtomKey(str: string) {
|
||||
return `use${capitalizeFirstLetter(str)}`;
|
||||
}
|
||||
|
||||
export function capitalizeFirstLetter(str: string) {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
}
|
||||
|
||||
type InferResolvedHooks<O extends BetterAuthClientOptions> = O extends {
|
||||
plugins: Array<infer Plugin>;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import type {
|
||||
BetterAuthClientPlugin,
|
||||
} from "@better-auth/core";
|
||||
import type { BASE_ERROR_CODES } from "@better-auth/core/error";
|
||||
import { capitalizeFirstLetter } from "@better-auth/core/utils";
|
||||
import type {
|
||||
BetterFetchError,
|
||||
BetterFetchResponse,
|
||||
@@ -23,10 +24,6 @@ function getAtomKey(str: string) {
|
||||
return `use${capitalizeFirstLetter(str)}`;
|
||||
}
|
||||
|
||||
export function capitalizeFirstLetter(str: string) {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
}
|
||||
|
||||
type InferResolvedHooks<O extends BetterAuthClientOptions> = O extends {
|
||||
plugins: Array<infer Plugin>;
|
||||
}
|
||||
|
||||
@@ -3,13 +3,13 @@ import type {
|
||||
BetterAuthClientPlugin,
|
||||
} from "@better-auth/core";
|
||||
import type { BASE_ERROR_CODES } from "@better-auth/core/error";
|
||||
import { capitalizeFirstLetter } from "@better-auth/core/utils";
|
||||
import type {
|
||||
BetterFetchError,
|
||||
BetterFetchResponse,
|
||||
} from "@better-fetch/fetch";
|
||||
import type { Accessor } from "solid-js";
|
||||
import type { PrettifyDeep, UnionToIntersection } from "../../types/helper";
|
||||
import { capitalizeFirstLetter } from "../../utils/misc";
|
||||
import { getClientConfig } from "../config";
|
||||
import { createDynamicPathProxy } from "../proxy";
|
||||
import type {
|
||||
|
||||
@@ -3,13 +3,13 @@ import type {
|
||||
BetterAuthClientPlugin,
|
||||
} from "@better-auth/core";
|
||||
import type { BASE_ERROR_CODES } from "@better-auth/core/error";
|
||||
import { capitalizeFirstLetter } from "@better-auth/core/utils";
|
||||
import type {
|
||||
BetterFetchError,
|
||||
BetterFetchResponse,
|
||||
} from "@better-fetch/fetch";
|
||||
import type { Atom } from "nanostores";
|
||||
import type { PrettifyDeep, UnionToIntersection } from "../../types/helper";
|
||||
import { capitalizeFirstLetter } from "../../utils/misc";
|
||||
import { getClientConfig } from "../config";
|
||||
import { createDynamicPathProxy } from "../proxy";
|
||||
import type {
|
||||
|
||||
@@ -3,13 +3,13 @@ import type {
|
||||
BetterAuthClientPlugin,
|
||||
} from "@better-auth/core";
|
||||
import type { BASE_ERROR_CODES } from "@better-auth/core/error";
|
||||
import { capitalizeFirstLetter } from "@better-auth/core/utils";
|
||||
import type {
|
||||
BetterFetchError,
|
||||
BetterFetchResponse,
|
||||
} from "@better-fetch/fetch";
|
||||
import type { Atom } from "nanostores";
|
||||
import type { PrettifyDeep, UnionToIntersection } from "../types/helper";
|
||||
import { capitalizeFirstLetter } from "../utils/misc";
|
||||
import { getClientConfig } from "./config";
|
||||
import { createDynamicPathProxy } from "./proxy";
|
||||
import type {
|
||||
|
||||
@@ -3,13 +3,13 @@ import type {
|
||||
BetterAuthClientPlugin,
|
||||
} from "@better-auth/core";
|
||||
import type { BASE_ERROR_CODES } from "@better-auth/core/error";
|
||||
import { capitalizeFirstLetter } from "@better-auth/core/utils";
|
||||
import type {
|
||||
BetterFetchError,
|
||||
BetterFetchResponse,
|
||||
} from "@better-fetch/fetch";
|
||||
import type { DeepReadonly, Ref } from "vue";
|
||||
import type { PrettifyDeep, UnionToIntersection } from "../../types/helper";
|
||||
import { capitalizeFirstLetter } from "../../utils/misc";
|
||||
import { getClientConfig } from "../config";
|
||||
import { createDynamicPathProxy } from "../proxy";
|
||||
import type {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { BetterAuthOptions } from "@better-auth/core";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { getCookieCache, getCookies, getSessionCookie } from "../cookies";
|
||||
import { parseUserOutput } from "../db/schema";
|
||||
import { getTestInstance } from "../test-utils/test-instance";
|
||||
import { parseSetCookieHeader } from "./cookie-utils";
|
||||
|
||||
@@ -481,36 +482,6 @@ describe("Cookie Cache Field Filtering", () => {
|
||||
expect(cache?.user?.internalNote).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should always preserve the user.id in cookie cache", async () => {
|
||||
const { client, testUser, cookieSetter } = await getTestInstance({
|
||||
secret: "better-auth.secret",
|
||||
session: {
|
||||
cookieCache: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
const headers = new Headers();
|
||||
await client.signIn.email(
|
||||
{
|
||||
email: testUser.email,
|
||||
password: testUser.password,
|
||||
},
|
||||
{
|
||||
onSuccess: cookieSetter(headers),
|
||||
},
|
||||
);
|
||||
const request = new Request("https://example.com/api/auth/session", {
|
||||
headers,
|
||||
});
|
||||
const cache = await getCookieCache(request, {
|
||||
secret: "better-auth.secret",
|
||||
});
|
||||
expect(cache).not.toBeNull();
|
||||
expect(cache?.user?.id).toBeDefined();
|
||||
expect(typeof cache?.user?.id).toBe("string");
|
||||
});
|
||||
|
||||
it("should correctly filter multiple user fields based on returned config", async () => {
|
||||
const { client, testUser, cookieSetter } = await getTestInstance({
|
||||
secret: "better-auth.secret",
|
||||
@@ -573,6 +544,26 @@ describe("Cookie Cache Field Filtering", () => {
|
||||
expect(cache?.user?.adminFlags).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should always include id in parseUserOutput", () => {
|
||||
const options = {
|
||||
user: {
|
||||
additionalFields: {
|
||||
id: { type: "string", returned: false },
|
||||
},
|
||||
},
|
||||
} as any;
|
||||
const user = {
|
||||
id: "custom-oauth-id-123",
|
||||
email: "test@example.com",
|
||||
emailVerified: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
name: "Test User",
|
||||
};
|
||||
const result = parseUserOutput(options, user);
|
||||
expect(result.id).toBe("custom-oauth-id-123");
|
||||
});
|
||||
|
||||
it("should reduce cookie size when large fields are excluded", async () => {
|
||||
const largeString = "x".repeat(2000);
|
||||
const { client, testUser, cookieSetter } = await getTestInstance({
|
||||
|
||||
@@ -112,9 +112,20 @@ export function parseInputData<T extends Record<string, any>>(
|
||||
continue;
|
||||
}
|
||||
if (fields[key]!.validator?.input && data[key] !== undefined) {
|
||||
parsedData[key] = fields[key]!.validator.input["~standard"].validate(
|
||||
const result = fields[key]!.validator.input["~standard"].validate(
|
||||
data[key],
|
||||
);
|
||||
if (result instanceof Promise) {
|
||||
throw new APIError("INTERNAL_SERVER_ERROR", {
|
||||
message: "Async validation is not supported for additional fields",
|
||||
});
|
||||
}
|
||||
if ("issues" in result && result.issues) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: result.issues[0]?.message || "Validation Error",
|
||||
});
|
||||
}
|
||||
parsedData[key] = result.value;
|
||||
continue;
|
||||
}
|
||||
if (fields[key]!.transform?.input && data[key] !== undefined) {
|
||||
@@ -180,12 +191,16 @@ export function mergeSchema<S extends BetterAuthPluginDBSchema>(
|
||||
schema: S,
|
||||
newSchema?:
|
||||
| {
|
||||
[K in keyof S]?: {
|
||||
modelName?: string;
|
||||
fields?: {
|
||||
[P: string]: string;
|
||||
};
|
||||
};
|
||||
[K in keyof S]?:
|
||||
| {
|
||||
modelName?: string | undefined;
|
||||
fields?:
|
||||
| {
|
||||
[P: string]: string;
|
||||
}
|
||||
| undefined;
|
||||
}
|
||||
| undefined;
|
||||
}
|
||||
| undefined,
|
||||
) {
|
||||
|
||||
@@ -1681,37 +1681,98 @@ describe("api-key", async () => {
|
||||
// Sessions from API keys
|
||||
// =========================================================================
|
||||
|
||||
it("should get session from an API key", async () => {
|
||||
const { client, auth, signInWithTestUser } = await getTestInstance(
|
||||
{
|
||||
plugins: [
|
||||
apiKey({
|
||||
enableSessionForAPIKeys: true,
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
clientOptions: {
|
||||
plugins: [apiKeyClient()],
|
||||
describe("enableSessionForAPIKeys", () => {
|
||||
it("should get session from an API key", async () => {
|
||||
const { client, auth, signInWithTestUser } = await getTestInstance(
|
||||
{
|
||||
plugins: [
|
||||
apiKey({
|
||||
enableSessionForAPIKeys: true,
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
{
|
||||
clientOptions: {
|
||||
plugins: [apiKeyClient()],
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const { headers: userHeaders } = await signInWithTestUser();
|
||||
const { headers: userHeaders } = await signInWithTestUser();
|
||||
|
||||
const { data: apiKey2 } = await client.apiKey.create(
|
||||
{},
|
||||
{ headers: userHeaders },
|
||||
);
|
||||
if (!apiKey2) return;
|
||||
const headers = new Headers();
|
||||
headers.set("x-api-key", apiKey2.key);
|
||||
const { data: apiKey2 } = await client.apiKey.create(
|
||||
{},
|
||||
{ headers: userHeaders },
|
||||
);
|
||||
if (!apiKey2) return;
|
||||
const headers = new Headers();
|
||||
headers.set("x-api-key", apiKey2.key);
|
||||
|
||||
const session = await auth.api.getSession({
|
||||
headers: headers,
|
||||
const session = await auth.api.getSession({
|
||||
headers: headers,
|
||||
});
|
||||
|
||||
expect(session?.session).toBeDefined();
|
||||
});
|
||||
|
||||
expect(session?.session).toBeDefined();
|
||||
it("should not get session from an API key if enableSessionForAPIKeys is false", async () => {
|
||||
const { client, auth, signInWithTestUser } = await getTestInstance(
|
||||
{
|
||||
plugins: [
|
||||
apiKey({
|
||||
enableSessionForAPIKeys: false,
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
clientOptions: {
|
||||
plugins: [apiKeyClient()],
|
||||
},
|
||||
},
|
||||
);
|
||||
const { headers: userHeaders } = await signInWithTestUser();
|
||||
const { data: apiKey2 } = await client.apiKey.create(
|
||||
{},
|
||||
{ headers: userHeaders },
|
||||
);
|
||||
if (!apiKey2) return;
|
||||
const headers = new Headers();
|
||||
headers.set("x-api-key", apiKey2.key);
|
||||
const session = await auth.api.getSession({
|
||||
headers: headers,
|
||||
});
|
||||
expect(session).toBeNull();
|
||||
});
|
||||
|
||||
it("should get the Response object when asResponse is true", async () => {
|
||||
const { client, auth, signInWithTestUser } = await getTestInstance(
|
||||
{
|
||||
plugins: [
|
||||
apiKey({
|
||||
enableSessionForAPIKeys: true,
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
clientOptions: {
|
||||
plugins: [apiKeyClient()],
|
||||
},
|
||||
},
|
||||
);
|
||||
const { headers: userHeaders } = await signInWithTestUser();
|
||||
const { data: apiKey2 } = await client.apiKey.create(
|
||||
{},
|
||||
{ headers: userHeaders },
|
||||
);
|
||||
if (!apiKey2) return;
|
||||
const headers = new Headers();
|
||||
headers.set("x-api-key", apiKey2.key);
|
||||
const res = await auth.api.getSession({
|
||||
headers: headers,
|
||||
asResponse: true,
|
||||
});
|
||||
expect(res).toBeInstanceOf(Response);
|
||||
});
|
||||
});
|
||||
|
||||
// =========================================================================
|
||||
|
||||
@@ -7,18 +7,73 @@ import { getJwksAdapter } from "./adapter";
|
||||
import type { JwtOptions } from "./types";
|
||||
import { createJwk, toExpJWT } from "./utils";
|
||||
|
||||
type JWTPayloadWithOptional = {
|
||||
/**
|
||||
* JWT Issuer
|
||||
*
|
||||
* @see {@link https://www.rfc-editor.org/rfc/rfc7519#section-4.1.1 RFC7519#section-4.1.1}
|
||||
*/
|
||||
iss?: string | undefined;
|
||||
|
||||
/**
|
||||
* JWT Subject
|
||||
*
|
||||
* @see {@link https://www.rfc-editor.org/rfc/rfc7519#section-4.1.2 RFC7519#section-4.1.2}
|
||||
*/
|
||||
sub?: string | undefined;
|
||||
|
||||
/**
|
||||
* JWT Audience
|
||||
*
|
||||
* @see {@link https://www.rfc-editor.org/rfc/rfc7519#section-4.1.3 RFC7519#section-4.1.3}
|
||||
*/
|
||||
aud?: string | string[] | undefined;
|
||||
|
||||
/**
|
||||
* JWT ID
|
||||
*
|
||||
* @see {@link https://www.rfc-editor.org/rfc/rfc7519#section-4.1.7 RFC7519#section-4.1.7}
|
||||
*/
|
||||
jti?: string | undefined;
|
||||
|
||||
/**
|
||||
* JWT Not Before
|
||||
*
|
||||
* @see {@link https://www.rfc-editor.org/rfc/rfc7519#section-4.1.5 RFC7519#section-4.1.5}
|
||||
*/
|
||||
nbf?: number | undefined;
|
||||
|
||||
/**
|
||||
* JWT Expiration Time
|
||||
*
|
||||
* @see {@link https://www.rfc-editor.org/rfc/rfc7519#section-4.1.4 RFC7519#section-4.1.4}
|
||||
*/
|
||||
exp?: number | undefined;
|
||||
|
||||
/**
|
||||
* JWT Issued At
|
||||
*
|
||||
* @see {@link https://www.rfc-editor.org/rfc/rfc7519#section-4.1.6 RFC7519#section-4.1.6}
|
||||
*/
|
||||
iat?: number | undefined;
|
||||
|
||||
/** Any other JWT Claim Set member. */
|
||||
[propName: string]: unknown | undefined;
|
||||
};
|
||||
|
||||
export async function signJWT(
|
||||
ctx: GenericEndpointContext,
|
||||
config: {
|
||||
options?: JwtOptions | undefined;
|
||||
payload: JWTPayload;
|
||||
payload: JWTPayloadWithOptional;
|
||||
},
|
||||
) {
|
||||
const { options, payload } = config;
|
||||
const { options } = config;
|
||||
const payload = config.payload as JWTPayload;
|
||||
|
||||
// Iat
|
||||
const nowSeconds = Math.floor(Date.now() / 1000);
|
||||
const iat = payload.iat;
|
||||
const iat = payload.iat!;
|
||||
|
||||
// Exp
|
||||
let exp = payload.exp;
|
||||
@@ -29,7 +84,7 @@ export async function signJWT(
|
||||
exp = exp ?? defaultExp;
|
||||
|
||||
// Nbf
|
||||
const nbf = payload.nbf;
|
||||
const nbf = payload.nbf!;
|
||||
|
||||
// Iss
|
||||
const iss = payload.iss;
|
||||
@@ -94,7 +149,7 @@ export async function getJwtToken(
|
||||
) {
|
||||
const payload = !options?.jwt?.definePayload
|
||||
? ctx.context.session!.user
|
||||
: await options?.jwt.definePayload(ctx.context.session!);
|
||||
: await options.jwt.definePayload(ctx.context.session!);
|
||||
|
||||
return await signJWT(ctx, {
|
||||
options,
|
||||
|
||||
@@ -13,7 +13,7 @@ export interface JwtOptions {
|
||||
* Useful if jwks are not managed at /jwks or
|
||||
* if your jwks are signed with a certificate and placed on your CDN.
|
||||
*/
|
||||
remoteUrl?: string;
|
||||
remoteUrl?: string | undefined;
|
||||
/**
|
||||
* Key pair configuration
|
||||
* @description A subset of the options available for the generateKeyPair function
|
||||
@@ -22,26 +22,26 @@ export interface JwtOptions {
|
||||
*
|
||||
* @default { alg: 'EdDSA', crv: 'Ed25519' }
|
||||
*/
|
||||
keyPairConfig?: JWKOptions;
|
||||
keyPairConfig?: JWKOptions | undefined;
|
||||
/**
|
||||
* Disable private key encryption
|
||||
* @description Disable the encryption of the private key in the database
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
disablePrivateKeyEncryption?: boolean;
|
||||
disablePrivateKeyEncryption?: boolean | undefined;
|
||||
/**
|
||||
* The key rotation interval in seconds.
|
||||
*
|
||||
* @default undefined (disabled)
|
||||
*/
|
||||
rotationInterval?: number;
|
||||
rotationInterval?: number | undefined;
|
||||
/**
|
||||
* The grace period in seconds.
|
||||
*
|
||||
* @default 2592000 (30 days)
|
||||
*/
|
||||
gracePeriod?: number;
|
||||
gracePeriod?: number | undefined;
|
||||
/**
|
||||
* The path of the endpoint exposing the JWKS.
|
||||
* When set, this replaces the default /jwks endpoint.
|
||||
@@ -50,7 +50,7 @@ export interface JwtOptions {
|
||||
* @default /jwks
|
||||
* @example "/.well-known/jwks.json"
|
||||
*/
|
||||
jwksPath?: string;
|
||||
jwksPath?: string | undefined;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
@@ -59,11 +59,11 @@ export interface JwtOptions {
|
||||
/**
|
||||
* The issuer of the JWT
|
||||
*/
|
||||
issuer?: string;
|
||||
issuer?: string | undefined;
|
||||
/**
|
||||
* The audience of the JWT
|
||||
*/
|
||||
audience?: string;
|
||||
audience?: string | undefined;
|
||||
/**
|
||||
* Set the "exp" (Expiration Time) Claim.
|
||||
*
|
||||
@@ -87,14 +87,14 @@ export interface JwtOptions {
|
||||
*
|
||||
* @default 15m
|
||||
*/
|
||||
expirationTime?: number | string | Date;
|
||||
expirationTime?: number | string | Date | undefined;
|
||||
/**
|
||||
* A function that is called to define the payload of the JWT
|
||||
*/
|
||||
definePayload?: (session: {
|
||||
user: User & Record<string, any>;
|
||||
session: Session & Record<string, any>;
|
||||
}) => Promise<Record<string, any>> | Record<string, any>;
|
||||
}) => Promise<Record<string, any>> | Record<string, any> | undefined;
|
||||
/**
|
||||
* A function that is called to get the subject of the JWT
|
||||
*
|
||||
@@ -103,7 +103,7 @@ export interface JwtOptions {
|
||||
getSubject?: (session: {
|
||||
user: User & Record<string, any>;
|
||||
session: Session & Record<string, any>;
|
||||
}) => Promise<string> | string;
|
||||
}) => Promise<string> | string | undefined;
|
||||
/**
|
||||
* A custom function to remote sign the jwt payload.
|
||||
*
|
||||
@@ -114,7 +114,7 @@ export interface JwtOptions {
|
||||
* @requires jwks.remoteUrl
|
||||
* @invalidates other jwt.* options
|
||||
*/
|
||||
sign?: (payload: JWTPayload) => Awaitable<string>;
|
||||
sign?: ((payload: JWTPayload) => Awaitable<string>) | undefined;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
|
||||
@@ -148,7 +148,9 @@ export async function authorize(
|
||||
}
|
||||
|
||||
const requestScope =
|
||||
query.scope?.split(" ").filter((s) => s) || opts.defaultScope.split(" ");
|
||||
query.scope?.split(" ").filter((s) => s) ||
|
||||
opts.defaultScope?.split(" ") ||
|
||||
[];
|
||||
const invalidScopes = requestScope.filter((scope) => {
|
||||
return !opts.scopes.includes(scope);
|
||||
});
|
||||
@@ -191,7 +193,7 @@ export async function authorize(
|
||||
}
|
||||
|
||||
const code = generateRandomString(32, "a-z", "A-Z", "0-9");
|
||||
const codeExpiresInMs = opts.codeExpiresIn * 1000;
|
||||
const codeExpiresInMs = opts.codeExpiresIn! * 1000;
|
||||
const expiresAt = new Date(Date.now() + codeExpiresInMs);
|
||||
|
||||
// Determine if consent is required
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
import { getCurrentAuthContext } from "@better-auth/core/context";
|
||||
import { base64 } from "@better-auth/utils/base64";
|
||||
import { createHash } from "@better-auth/utils/hash";
|
||||
import type { OpenAPIParameter } from "better-call";
|
||||
import { jwtVerify, SignJWT } from "jose";
|
||||
import * as z from "zod";
|
||||
import { APIError, getSessionFromCtx, sessionMiddleware } from "../../api";
|
||||
@@ -132,6 +133,10 @@ export const getMetadata = (
|
||||
};
|
||||
};
|
||||
|
||||
const DEFAULT_CODE_EXPIRES_IN = 600;
|
||||
const DEFAULT_ACCESS_TOKEN_EXPIRES_IN = 3600;
|
||||
const DEFAULT_REFRESH_TOKEN_EXPIRES_IN = 604800;
|
||||
|
||||
/**
|
||||
* OpenID Connect (OIDC) plugin for Better Auth. This plugin implements the
|
||||
* authorization code flow and the token exchange flow. It also implements the
|
||||
@@ -148,10 +153,10 @@ export const oidcProvider = (options: OIDCOptions) => {
|
||||
};
|
||||
|
||||
const opts = {
|
||||
codeExpiresIn: 600,
|
||||
codeExpiresIn: DEFAULT_CODE_EXPIRES_IN,
|
||||
defaultScope: "openid",
|
||||
accessTokenExpiresIn: 3600,
|
||||
refreshTokenExpiresIn: 604800,
|
||||
accessTokenExpiresIn: DEFAULT_ACCESS_TOKEN_EXPIRES_IN,
|
||||
refreshTokenExpiresIn: DEFAULT_REFRESH_TOKEN_EXPIRES_IN,
|
||||
allowPlainCodeChallengeMethod: true,
|
||||
storeClientSecret: "plain" as const,
|
||||
...options,
|
||||
@@ -464,7 +469,8 @@ export const oidcProvider = (options: OIDCOptions) => {
|
||||
});
|
||||
}
|
||||
const code = generateRandomString(32, "a-z", "A-Z", "0-9");
|
||||
const codeExpiresInMs = opts.codeExpiresIn * 1000;
|
||||
const codeExpiresInMs =
|
||||
(opts?.codeExpiresIn ?? DEFAULT_CODE_EXPIRES_IN) * 1000;
|
||||
const expiresAt = new Date(Date.now() + codeExpiresInMs);
|
||||
await ctx.context.internalAdapter.updateVerificationValue(
|
||||
verification.id,
|
||||
@@ -848,7 +854,8 @@ export const oidcProvider = (options: OIDCOptions) => {
|
||||
...additionalUserClaims,
|
||||
};
|
||||
const expirationTime =
|
||||
Math.floor(Date.now() / 1000) + opts.accessTokenExpiresIn;
|
||||
Math.floor(Date.now() / 1000) +
|
||||
(opts?.accessTokenExpiresIn ?? DEFAULT_ACCESS_TOKEN_EXPIRES_IN);
|
||||
|
||||
let idToken: string;
|
||||
|
||||
@@ -1519,7 +1526,6 @@ export const oidcProvider = (options: OIDCOptions) => {
|
||||
"/oauth2/endsession",
|
||||
{
|
||||
method: ["GET", "POST"],
|
||||
operationId: "oauth2EndSession",
|
||||
query: z
|
||||
.object({
|
||||
id_token_hint: z.string().optional(),
|
||||
@@ -1584,7 +1590,7 @@ export const oidcProvider = (options: OIDCOptions) => {
|
||||
required: false,
|
||||
schema: { type: "string" },
|
||||
},
|
||||
],
|
||||
] as OpenAPIParameter[],
|
||||
responses: {
|
||||
"302": {
|
||||
description:
|
||||
|
||||
@@ -77,7 +77,7 @@ export interface OIDCOptions {
|
||||
| ((props: {
|
||||
clientId: string;
|
||||
clientName: string;
|
||||
clientIcon?: string;
|
||||
clientIcon?: string | undefined;
|
||||
clientMetadata: Record<string, any> | null;
|
||||
code: string;
|
||||
scopes: string[];
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export { generateId } from "@better-auth/core/utils";
|
||||
export * from "../oauth2/state";
|
||||
export * from "./hide-metadata";
|
||||
export * from "./misc";
|
||||
|
||||
@@ -51,13 +51,14 @@
|
||||
"@babel/core": "^7.28.4",
|
||||
"@babel/preset-react": "^7.27.1",
|
||||
"@babel/preset-typescript": "^7.27.1",
|
||||
"@better-auth/core": "workspace:*",
|
||||
"@better-auth/telemetry": "workspace:*",
|
||||
"@better-auth/utils": "0.3.0",
|
||||
"@clack/prompts": "^0.11.0",
|
||||
"@mrleebo/prisma-ast": "^0.13.0",
|
||||
"@prisma/client": "^5.22.0",
|
||||
"@types/pg": "^8.15.5",
|
||||
"better-auth": "workspace:*",
|
||||
"@better-auth/core": "workspace:*",
|
||||
"better-auth": "workspace:^",
|
||||
"better-sqlite3": "^12.2.0",
|
||||
"c12": "^3.2.0",
|
||||
"chalk": "^5.6.2",
|
||||
@@ -67,7 +68,6 @@
|
||||
"open": "^10.2.0",
|
||||
"pg": "^8.16.3",
|
||||
"prettier": "^3.6.2",
|
||||
"prisma": "^5.22.0",
|
||||
"prompts": "^2.4.2",
|
||||
"semver": "^7.7.2",
|
||||
"yocto-spinner": "^0.2.3",
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { existsSync } from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { createTelemetry, getTelemetryAuthConfig, logger } from "better-auth";
|
||||
import {
|
||||
createTelemetry,
|
||||
getTelemetryAuthConfig,
|
||||
} from "@better-auth/telemetry";
|
||||
import { getAdapter } from "better-auth/db";
|
||||
import chalk from "chalk";
|
||||
import { Command } from "commander";
|
||||
@@ -24,7 +27,7 @@ export async function generateAction(opts: any) {
|
||||
|
||||
const cwd = path.resolve(options.cwd);
|
||||
if (!existsSync(cwd)) {
|
||||
logger.error(`The directory "${cwd}" does not exist.`);
|
||||
console.error(`The directory "${cwd}" does not exist.`);
|
||||
process.exit(1);
|
||||
}
|
||||
const config = await getConfig({
|
||||
@@ -32,14 +35,14 @@ export async function generateAction(opts: any) {
|
||||
configPath: options.config,
|
||||
});
|
||||
if (!config) {
|
||||
logger.error(
|
||||
console.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.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const adapter = await getAdapter(config).catch((e) => {
|
||||
logger.error(e.message);
|
||||
console.error(e.message);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
@@ -53,7 +56,7 @@ export async function generateAction(opts: any) {
|
||||
|
||||
spinner.stop();
|
||||
if (!schema.code) {
|
||||
logger.info("Your schema is already up to date.");
|
||||
console.log("Your schema is already up to date.");
|
||||
// telemetry: track generate attempted, no changes
|
||||
try {
|
||||
const telemetry = await createTelemetry(config);
|
||||
@@ -98,7 +101,7 @@ export async function generateAction(opts: any) {
|
||||
} else {
|
||||
await fs.appendFile(path.join(cwd, schema.fileName), schema.code);
|
||||
}
|
||||
logger.success(
|
||||
console.log(
|
||||
`🚀 Schema was ${
|
||||
schema.overwrite ? "overwritten" : "appended"
|
||||
} successfully!`,
|
||||
@@ -116,7 +119,7 @@ export async function generateAction(opts: any) {
|
||||
} catch {}
|
||||
process.exit(0);
|
||||
} else {
|
||||
logger.error("Schema generation aborted.");
|
||||
console.error("Schema generation aborted.");
|
||||
// telemetry: track generate aborted
|
||||
try {
|
||||
const telemetry = await createTelemetry(config);
|
||||
@@ -151,7 +154,7 @@ export async function generateAction(opts: any) {
|
||||
}
|
||||
|
||||
if (!confirm) {
|
||||
logger.error("Schema generation aborted.");
|
||||
console.error("Schema generation aborted.");
|
||||
// telemetry: track generate aborted before write
|
||||
try {
|
||||
const telemetry = await createTelemetry(config);
|
||||
@@ -175,7 +178,7 @@ export async function generateAction(opts: any) {
|
||||
options.output || path.join(cwd, schema.fileName),
|
||||
schema.code,
|
||||
);
|
||||
logger.success(`🚀 Schema was generated successfully!`);
|
||||
console.log(`🚀 Schema was generated successfully!`);
|
||||
// telemetry: track generate success
|
||||
try {
|
||||
const telemetry = await createTelemetry(config);
|
||||
|
||||
@@ -2,7 +2,6 @@ import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { cancel, confirm, intro, isCancel, outro } from "@clack/prompts";
|
||||
import { logger } from "better-auth";
|
||||
import { createAuthClient } from "better-auth/client";
|
||||
import { deviceAuthorizationClient } from "better-auth/client/plugins";
|
||||
import chalk from "chalk";
|
||||
@@ -73,7 +72,7 @@ export async function loginAction(opts: any) {
|
||||
spinner.stop();
|
||||
|
||||
if (error || !data) {
|
||||
logger.error(
|
||||
console.error(
|
||||
`Failed to request device authorization: ${error?.error_description || "Unknown error"}`,
|
||||
);
|
||||
process.exit(1);
|
||||
@@ -154,7 +153,7 @@ export async function loginAction(opts: any) {
|
||||
}
|
||||
} catch (err) {
|
||||
spinner.stop();
|
||||
logger.error(
|
||||
console.error(
|
||||
`Login failed: ${err instanceof Error ? err.message : "Unknown error"}`,
|
||||
);
|
||||
process.exit(1);
|
||||
@@ -209,23 +208,23 @@ async function pollForToken(
|
||||
break;
|
||||
case "access_denied":
|
||||
spinner.stop();
|
||||
logger.error("Access was denied by the user");
|
||||
console.error("Access was denied by the user");
|
||||
process.exit(1);
|
||||
break;
|
||||
case "expired_token":
|
||||
spinner.stop();
|
||||
logger.error("The device code has expired. Please try again.");
|
||||
console.error("The device code has expired. Please try again.");
|
||||
process.exit(1);
|
||||
break;
|
||||
default:
|
||||
spinner.stop();
|
||||
logger.error(`Error: ${error.error_description}`);
|
||||
console.error(`Error: ${error.error_description}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
spinner.stop();
|
||||
logger.error(
|
||||
console.error(
|
||||
`Network error: ${err instanceof Error ? err.message : "Unknown error"}`,
|
||||
);
|
||||
process.exit(1);
|
||||
@@ -254,7 +253,7 @@ async function storeToken(token: any): Promise<void> {
|
||||
|
||||
await fs.writeFile(TOKEN_FILE, JSON.stringify(tokenData, null, 2), "utf-8");
|
||||
} catch (error) {
|
||||
logger.warn("Failed to store authentication token locally");
|
||||
console.warn("Failed to store authentication token locally");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { existsSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { createTelemetry, getTelemetryAuthConfig, logger } from "better-auth";
|
||||
import {
|
||||
createTelemetry,
|
||||
getTelemetryAuthConfig,
|
||||
} from "@better-auth/telemetry";
|
||||
import { getAdapter, getMigrations } from "better-auth/db";
|
||||
import chalk from "chalk";
|
||||
import { Command } from "commander";
|
||||
@@ -21,7 +24,7 @@ export async function migrateAction(opts: any) {
|
||||
|
||||
const cwd = path.resolve(options.cwd);
|
||||
if (!existsSync(cwd)) {
|
||||
logger.error(`The directory "${cwd}" does not exist.`);
|
||||
console.error(`The directory "${cwd}" does not exist.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -30,7 +33,7 @@ export async function migrateAction(opts: any) {
|
||||
configPath: options.config,
|
||||
});
|
||||
if (!config) {
|
||||
logger.error(
|
||||
console.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.",
|
||||
);
|
||||
return;
|
||||
@@ -39,7 +42,7 @@ export async function migrateAction(opts: any) {
|
||||
const db = await getAdapter(config);
|
||||
|
||||
if (!db) {
|
||||
logger.error(
|
||||
console.error(
|
||||
"Invalid database configuration. Make sure you're not using adapters. Migrate command only works with built-in Kysely adapter.",
|
||||
);
|
||||
process.exit(1);
|
||||
@@ -47,8 +50,8 @@ export async function migrateAction(opts: any) {
|
||||
|
||||
if (db.id !== "kysely") {
|
||||
if (db.id === "prisma") {
|
||||
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.",
|
||||
console.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.",
|
||||
);
|
||||
try {
|
||||
const telemetry = await createTelemetry(config);
|
||||
@@ -64,8 +67,8 @@ export async function migrateAction(opts: any) {
|
||||
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 Drizzle’s migrate or push to apply it.",
|
||||
console.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.",
|
||||
);
|
||||
try {
|
||||
const telemetry = await createTelemetry(config);
|
||||
@@ -80,7 +83,7 @@ export async function migrateAction(opts: any) {
|
||||
} catch {}
|
||||
process.exit(0);
|
||||
}
|
||||
logger.error("Migrate command isn't supported for this adapter.");
|
||||
console.error("Migrate command isn't supported for this adapter.");
|
||||
try {
|
||||
const telemetry = await createTelemetry(config);
|
||||
await telemetry.publish({
|
||||
@@ -101,7 +104,7 @@ export async function migrateAction(opts: any) {
|
||||
|
||||
if (!toBeAdded.length && !toBeCreated.length) {
|
||||
spinner.stop();
|
||||
logger.info("🚀 No migrations needed.");
|
||||
console.log("🚀 No migrations needed.");
|
||||
try {
|
||||
const telemetry = await createTelemetry(config);
|
||||
await telemetry.publish({
|
||||
@@ -116,7 +119,7 @@ export async function migrateAction(opts: any) {
|
||||
}
|
||||
|
||||
spinner.stop();
|
||||
logger.info(`🔑 The migration will affect the following:`);
|
||||
console.log(`🔑 The migration will affect the following:`);
|
||||
|
||||
for (const table of [...toBeCreated, ...toBeAdded]) {
|
||||
console.log(
|
||||
@@ -145,7 +148,7 @@ export async function migrateAction(opts: any) {
|
||||
}
|
||||
|
||||
if (!migrate) {
|
||||
logger.info("Migration cancelled.");
|
||||
console.log("Migration cancelled.");
|
||||
try {
|
||||
const telemetry = await createTelemetry(config);
|
||||
await telemetry.publish({
|
||||
@@ -159,7 +162,7 @@ export async function migrateAction(opts: any) {
|
||||
spinner?.start("migrating...");
|
||||
await runMigrations();
|
||||
spinner.stop();
|
||||
logger.info("🚀 migration was completed successfully!");
|
||||
console.log("🚀 migration was completed successfully!");
|
||||
try {
|
||||
const telemetry = await createTelemetry(config);
|
||||
await telemetry.publish({
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import Crypto from "node:crypto";
|
||||
import { logger } from "better-auth";
|
||||
import chalk from "chalk";
|
||||
import { Command } from "commander";
|
||||
|
||||
export const generateSecret = new Command("secret").action(() => {
|
||||
const secret = generateSecretHash();
|
||||
logger.info(`\nAdd the following to your .env file:
|
||||
console.log(`\nAdd the following to your .env file:
|
||||
${
|
||||
chalk.gray("# Auth Secret") + chalk.green(`\nBETTER_AUTH_SECRET=${secret}`)
|
||||
}`);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { spinner as clackSpinner } from "@clack/prompts";
|
||||
import { logger } from "better-auth";
|
||||
import type { SupportedDatabases, SupportedPlugin } from "../commands/init";
|
||||
|
||||
export type Import = {
|
||||
@@ -545,7 +544,7 @@ export async function generateAuthConfig({
|
||||
`Something went wrong while generating/updating your new auth config file.`,
|
||||
1,
|
||||
);
|
||||
logger.error(error.message);
|
||||
console.error(error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -566,7 +565,7 @@ export async function generateAuthConfig({
|
||||
`Something went wrong while generating/updating your new auth config file.`,
|
||||
1,
|
||||
);
|
||||
logger.error(error.message);
|
||||
console.error(error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { BetterAuthOptions } from "@better-auth/core";
|
||||
import type { DBAdapter } from "@better-auth/core/db/adapter";
|
||||
import type { BetterAuthOptions } from "better-auth";
|
||||
import { logger } from "better-auth";
|
||||
import { generateDrizzleSchema } from "./drizzle";
|
||||
import { generateMigrations } from "./kysely";
|
||||
import { generatePrismaSchema } from "./prisma";
|
||||
@@ -36,7 +35,7 @@ export const generateSchema = (opts: {
|
||||
}));
|
||||
}
|
||||
|
||||
logger.error(
|
||||
console.error(
|
||||
`${adapter.id} is not supported. If it is a custom adapter, please request the maintainer to implement createSchema`,
|
||||
);
|
||||
process.exit(1);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { existsSync } from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { capitalizeFirstLetter } from "@better-auth/core/utils";
|
||||
import { produceSchema } from "@mrleebo/prisma-ast";
|
||||
import { capitalizeFirstLetter } from "better-auth";
|
||||
import { initGetFieldName, initGetModelName } from "better-auth/adapters";
|
||||
import type { FieldType } from "better-auth/db";
|
||||
import { getAuthTables } from "better-auth/db";
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { BetterAuthOptions, DBAdapter } from "better-auth";
|
||||
import type { BetterAuthOptions } from "@better-auth/core";
|
||||
import type { DBAdapter } from "@better-auth/core/db/adapter";
|
||||
|
||||
export interface SchemaGenerator {
|
||||
<Options extends BetterAuthOptions>(opts: {
|
||||
|
||||
@@ -4,8 +4,8 @@ import path from "node:path";
|
||||
import babelPresetReact from "@babel/preset-react";
|
||||
// @ts-expect-error
|
||||
import babelPresetTypeScript from "@babel/preset-typescript";
|
||||
import type { BetterAuthOptions } from "better-auth";
|
||||
import { BetterAuthError, logger } from "better-auth";
|
||||
import type { BetterAuthOptions } from "@better-auth/core";
|
||||
import { BetterAuthError } from "@better-auth/core/error";
|
||||
import { loadConfig } from "c12";
|
||||
import type { JitiOptions } from "jiti";
|
||||
import { addCloudflareModules } from "./add-cloudflare-modules";
|
||||
@@ -76,7 +76,7 @@ function getPathAliasesRecursive(
|
||||
visited.add(tsconfigPath);
|
||||
|
||||
if (!fs.existsSync(tsconfigPath)) {
|
||||
logger.warn(`Referenced tsconfig not found: ${tsconfigPath}`);
|
||||
console.warn(`Referenced tsconfig not found: ${tsconfigPath}`);
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -114,7 +114,7 @@ function getPathAliasesRecursive(
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
logger.warn(`Error parsing tsconfig at ${tsconfigPath}: ${error}`);
|
||||
console.warn(`Error parsing tsconfig at ${tsconfigPath}: ${error}`);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
@@ -204,7 +204,7 @@ export async function getConfig({
|
||||
`Couldn't read your auth config in ${resolvedPath}. Make sure to default export your auth instance or to export as a variable named auth.`,
|
||||
);
|
||||
}
|
||||
logger.error(
|
||||
console.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.`,
|
||||
);
|
||||
process.exit(1);
|
||||
@@ -236,9 +236,9 @@ export async function getConfig({
|
||||
"Couldn't read your auth config. Make sure to default export your auth instance or to export as a variable named auth.",
|
||||
);
|
||||
}
|
||||
logger.error("[#better-auth]: Couldn't read your auth config.");
|
||||
console.error("[#better-auth]: Couldn't read your auth config.");
|
||||
console.log("");
|
||||
logger.info(
|
||||
console.log(
|
||||
"[#better-auth]: Make sure to default export your auth instance or to export as a variable named auth.",
|
||||
);
|
||||
process.exit(1);
|
||||
@@ -260,7 +260,7 @@ export async function getConfig({
|
||||
`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.`,
|
||||
);
|
||||
}
|
||||
logger.error(
|
||||
console.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.`,
|
||||
);
|
||||
process.exit(1);
|
||||
@@ -268,7 +268,7 @@ export async function getConfig({
|
||||
if (shouldThrowOnError) {
|
||||
throw e;
|
||||
}
|
||||
logger.error("[#better-auth]: Couldn't read your auth config.", e);
|
||||
console.error("[#better-auth]: Couldn't read your auth config.", e);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -289,7 +289,7 @@ export async function getConfig({
|
||||
`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.`,
|
||||
);
|
||||
}
|
||||
logger.error(
|
||||
console.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.`,
|
||||
);
|
||||
process.exit(1);
|
||||
@@ -298,7 +298,7 @@ export async function getConfig({
|
||||
throw e;
|
||||
}
|
||||
|
||||
logger.error("Couldn't read your auth config.", e);
|
||||
console.error("Couldn't read your auth config.", e);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { BetterAuthOptions } from "@better-auth/core";
|
||||
import { passkey } from "@better-auth/passkey";
|
||||
import type { BetterAuthOptions } from "better-auth";
|
||||
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
||||
import { twoFactor, username } from "better-auth/plugins";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import type { BetterAuthOptions } from "better-auth";
|
||||
import type { BetterAuthOptions } from "@better-auth/core";
|
||||
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
||||
import { prismaAdapter } from "better-auth/adapters/prisma";
|
||||
import { organization, twoFactor, username } from "better-auth/plugins";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { BetterAuthPlugin } from "better-auth";
|
||||
import type { BetterAuthPlugin } from "@better-auth/core";
|
||||
import { betterAuth } from "better-auth";
|
||||
import Database from "better-sqlite3";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
@@ -6,30 +6,39 @@ import type { AsyncLocalStorage } from "node:async_hooks";
|
||||
// We only export the type here to avoid issues in environments where AsyncLocalStorage is not available.
|
||||
export type { AsyncLocalStorage };
|
||||
|
||||
const AsyncLocalStoragePromise: Promise<typeof AsyncLocalStorage> = import(
|
||||
/* @vite-ignore */
|
||||
/* webpackIgnore: true */
|
||||
"node:async_hooks"
|
||||
)
|
||||
.then((mod) => mod.AsyncLocalStorage)
|
||||
.catch((err) => {
|
||||
if ("AsyncLocalStorage" in globalThis) {
|
||||
return (globalThis as any).AsyncLocalStorage;
|
||||
}
|
||||
console.warn(
|
||||
"[better-auth] Warning: AsyncLocalStorage is not available in this environment. Some features may not work as expected.",
|
||||
);
|
||||
console.warn(
|
||||
"[better-auth] Please read more about this warning at https://better-auth.com/docs/installation#mount-handler",
|
||||
);
|
||||
console.warn(
|
||||
"[better-auth] If you are using Cloudflare Workers, please see: https://developers.cloudflare.com/workers/configuration/compatibility-flags/#nodejs-compatibility-flag",
|
||||
);
|
||||
throw err;
|
||||
});
|
||||
const AsyncLocalStoragePromise: Promise<typeof AsyncLocalStorage | null> =
|
||||
import(
|
||||
/* @vite-ignore */
|
||||
/* webpackIgnore: true */
|
||||
"node:async_hooks"
|
||||
)
|
||||
.then((mod) => mod.AsyncLocalStorage)
|
||||
.catch((err) => {
|
||||
if ("AsyncLocalStorage" in globalThis) {
|
||||
return (globalThis as any).AsyncLocalStorage;
|
||||
}
|
||||
if (typeof window !== "undefined") {
|
||||
return null;
|
||||
}
|
||||
console.warn(
|
||||
"[better-auth] Warning: AsyncLocalStorage is not available in this environment. Some features may not work as expected.",
|
||||
);
|
||||
console.warn(
|
||||
"[better-auth] Please read more about this warning at https://better-auth.com/docs/installation#mount-handler",
|
||||
);
|
||||
console.warn(
|
||||
"[better-auth] If you are using Cloudflare Workers, please see: https://developers.cloudflare.com/workers/configuration/compatibility-flags/#nodejs-compatibility-flag",
|
||||
);
|
||||
throw err;
|
||||
});
|
||||
|
||||
export async function getAsyncLocalStorage(): Promise<
|
||||
typeof AsyncLocalStorage
|
||||
> {
|
||||
return AsyncLocalStoragePromise;
|
||||
const mod = await AsyncLocalStoragePromise;
|
||||
if (mod === null) {
|
||||
throw new Error("getAsyncLocalStorage is only available in server code");
|
||||
} else {
|
||||
return mod;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export { defineErrorCodes } from "./error-codes";
|
||||
export { generateId } from "./id";
|
||||
export { safeJSONParse } from "./json";
|
||||
export { capitalizeFirstLetter } from "./string";
|
||||
|
||||
File diff suppressed because one or more lines are too long
Generated
+1602
-2153
File diff suppressed because it is too large
Load Diff
+2
-5
@@ -16,10 +16,7 @@ catalogs:
|
||||
react19:
|
||||
'@types/react': ^19.2.0
|
||||
'@types/react-dom': ^19.2.0
|
||||
react: ^19.2.0
|
||||
react-dom: ^19.2.0
|
||||
react: ^19.2.1
|
||||
react-dom: ^19.2.1
|
||||
|
||||
neverBuiltDependencies: []
|
||||
|
||||
patchedDependencies:
|
||||
msw: patches/msw.patch
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
"@better-auth/core": "workspace:*",
|
||||
"@better-fetch/fetch": "catalog:",
|
||||
"better-auth": "workspace:*",
|
||||
"msw": "^2.12.1",
|
||||
"msw": "^2.12.4",
|
||||
"openid-client": "^6.8.1",
|
||||
"vitest": "catalog:"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user