diff --git a/apps/web/package.json b/apps/web/package.json index 42a3a6cf66..d46b264dfe 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -28,7 +28,7 @@ "dependencies": { "@aws-sdk/client-s3": "3.1000.0", "@aws-sdk/s3-request-presigner": "3.1000.0", - "@better-auth/drizzle-adapter": "^1.6.22", + "@better-auth/drizzle-adapter": "^1.7.12", "@codemirror/lang-yaml": "^6.0.0", "@codemirror/merge": "^6.0.0", "@codemirror/state": "^6.0.0", @@ -92,7 +92,7 @@ "@zxcvbn-ts/core": "^4.1.2", "ai": "^6.0.116", "ansi-to-react": "^6.2.6", - "better-auth": "^1.6.22", + "better-auth": "^1.7.12", "better-sse": "^0.16.1", "change-case": "^5.4.4", "class-variance-authority": "^0.7.1", diff --git a/apps/web/src/app/(onboarding)/setup/StepAuthEnvVars.client.test.tsx b/apps/web/src/app/(onboarding)/setup/StepAuthEnvVars.client.test.tsx index 5ebcce9a5a..f557c5c153 100644 --- a/apps/web/src/app/(onboarding)/setup/StepAuthEnvVars.client.test.tsx +++ b/apps/web/src/app/(onboarding)/setup/StepAuthEnvVars.client.test.tsx @@ -47,8 +47,7 @@ vi.mock('sonner', () => ({ vi.mock('@/lib/auth-client', () => ({ authClient: { signIn: { - oauth2: signInOauth2Mock, - social: signInSocialMock, + social: signInOauth2Mock, }, }, })); @@ -1064,7 +1063,7 @@ describe('StepAuthEnvVars', () => { await waitFor(() => { expect(signInOauth2Mock).toHaveBeenCalledWith({ - providerId: 'microsoft-entra-id', + provider: 'microsoft-entra-id', callbackURL: '/setup', disableRedirect: true, }); @@ -1108,7 +1107,7 @@ describe('StepAuthEnvVars', () => { await waitFor(() => { expect(signInOauth2Mock).toHaveBeenCalledWith({ - providerId: 'slack', + provider: 'slack', callbackURL: '/api/slack/install-after-auth?redirect=%2Fsetup', disableRedirect: true, }); @@ -1149,7 +1148,7 @@ describe('StepAuthEnvVars', () => { await waitFor(() => { expect(signInOauth2Mock).toHaveBeenCalledWith({ - providerId: 'microsoft-entra-id', + provider: 'microsoft-entra-id', callbackURL: '/setup', disableRedirect: true, }); diff --git a/apps/web/src/app/(onboarding)/setup/StepAuthEnvVars.tsx b/apps/web/src/app/(onboarding)/setup/StepAuthEnvVars.tsx index fde3f72cb8..e8dec6a84c 100644 --- a/apps/web/src/app/(onboarding)/setup/StepAuthEnvVars.tsx +++ b/apps/web/src/app/(onboarding)/setup/StepAuthEnvVars.tsx @@ -24,17 +24,11 @@ import { const BOOTSTRAP_SIGN_IN_CALLBACK_PATH = '/setup'; -function getOAuth2ProviderId( +// Microsoft sign-in is registered under the Entra provider id. +function toSocialProviderId( providerId: SetupAuthStatus['preselectedProvider'], -): string | null { - switch (providerId) { - case 'slack': - return providerId; - case 'microsoft': - return 'microsoft-entra-id'; - default: - return null; - } +): string { + return providerId === 'microsoft' ? 'microsoft-entra-id' : providerId; } export function StepAuthEnvVars({ @@ -117,18 +111,11 @@ export function StepAuthEnvVars({ selectedProvider.id, BOOTSTRAP_SIGN_IN_CALLBACK_PATH, ); - const oauth2ProviderId = getOAuth2ProviderId(selectedProvider.id); - const result = oauth2ProviderId - ? await authClient.signIn.oauth2({ - providerId: oauth2ProviderId, - callbackURL, - disableRedirect: true, - }) - : await authClient.signIn.social({ - provider: selectedProvider.id, - callbackURL, - disableRedirect: true, - }); + const result = await authClient.signIn.social({ + provider: toSocialProviderId(selectedProvider.id), + callbackURL, + disableRedirect: true, + }); if (result.error) { toast.error(result.error.message || 'Unable to sign in.'); diff --git a/apps/web/src/app/(unauthenticated)/auth-form.client.test.tsx b/apps/web/src/app/(unauthenticated)/auth-form.client.test.tsx index 9bf0c0e3c5..914bdc170a 100644 --- a/apps/web/src/app/(unauthenticated)/auth-form.client.test.tsx +++ b/apps/web/src/app/(unauthenticated)/auth-form.client.test.tsx @@ -19,7 +19,7 @@ vi.mock('next/navigation', () => ({ vi.mock('@/lib/auth-client', () => ({ authClient: { signIn: { - oauth2: signInOauth2Mock, + social: signInOauth2Mock, }, }, })); @@ -51,7 +51,7 @@ describe('AuthForm', () => { await waitFor(() => { expect(signInOauth2Mock).toHaveBeenCalledWith({ - providerId: 'slack', + provider: 'slack', callbackURL: '/api/slack/install-after-auth?redirect=%2Ftasks%3Fview%3Dmine', }); @@ -69,7 +69,7 @@ describe('AuthForm', () => { await waitFor(() => { expect(signInOauth2Mock).toHaveBeenCalledWith({ - providerId: 'microsoft-entra-id', + provider: 'microsoft-entra-id', callbackURL: '/setup', }); }); @@ -86,7 +86,7 @@ describe('AuthForm', () => { await waitFor(() => { expect(signInOauth2Mock).toHaveBeenCalledWith({ - providerId: 'microsoft-entra-id', + provider: 'microsoft-entra-id', callbackURL: '/setup', }); }); @@ -103,7 +103,7 @@ describe('AuthForm', () => { await waitFor(() => { expect(signInOauth2Mock).toHaveBeenCalledWith({ - providerId: 'microsoft-entra-id', + provider: 'microsoft-entra-id', callbackURL: '/setup', }); }); @@ -120,7 +120,7 @@ describe('AuthForm', () => { await waitFor(() => { expect(signInOauth2Mock).toHaveBeenCalledWith({ - providerId: 'microsoft-entra-id', + provider: 'microsoft-entra-id', callbackURL: '/settings', }); }); diff --git a/apps/web/src/app/(unauthenticated)/auth-form.tsx b/apps/web/src/app/(unauthenticated)/auth-form.tsx index 157f327041..f31efbd129 100644 --- a/apps/web/src/app/(unauthenticated)/auth-form.tsx +++ b/apps/web/src/app/(unauthenticated)/auth-form.tsx @@ -109,8 +109,8 @@ export function AuthForm({ try { const callbackURL = getAuthProviderCallbackUrl(provider, redirectUrl); - const result = await authClient.signIn.oauth2({ - providerId: provider === 'slack' ? provider : 'microsoft-entra-id', + const result = await authClient.signIn.social({ + provider: provider === 'slack' ? provider : 'microsoft-entra-id', callbackURL, }); diff --git a/apps/web/src/app/api/teams/auth/__tests__/route.test.ts b/apps/web/src/app/api/teams/auth/__tests__/route.test.ts index 6f27970de5..8bf00b0082 100644 --- a/apps/web/src/app/api/teams/auth/__tests__/route.test.ts +++ b/apps/web/src/app/api/teams/auth/__tests__/route.test.ts @@ -80,12 +80,12 @@ describe('GET /api/teams/auth', () => { const authRequest = handleAuthRequestMock.mock.calls[0]?.[0] as Request; await expect(authRequest.clone().json()).resolves.toMatchObject({ - providerId: 'microsoft-entra-id', + provider: 'microsoft-entra-id', callbackURL: '/api/teams/auth?state=token-1', errorCallbackURL: '/api/teams/auth?state=token-1', }); expect(authRequest.url).toBe( - 'http://localhost:13000/api/auth/sign-in/oauth2', + 'http://localhost:13000/api/auth/sign-in/social', ); // Better Auth rejects POSTs without an Origin (MISSING_OR_NULL_ORIGIN). expect(authRequest.headers.get('origin')).toBe('http://localhost:13000'); @@ -135,11 +135,11 @@ describe('GET /api/teams/auth', () => { ); const authRequest = handleAuthRequestMock.mock.calls[0]?.[0] as Request; - expect(authRequest.url).toBe('http://localhost:13000/api/auth/oauth2/link'); + expect(authRequest.url).toBe('http://localhost:13000/api/auth/link-social'); expect(authRequest.headers.get('cookie')).toBe('session=abc'); expect(authRequest.headers.get('origin')).toBe('http://localhost:13000'); await expect(authRequest.clone().json()).resolves.toMatchObject({ - providerId: 'microsoft-entra-id', + provider: 'microsoft-entra-id', callbackURL: '/api/teams/auth?state=token-3&linked=1', errorCallbackURL: '/api/teams/auth?state=token-3&linked=1', }); diff --git a/apps/web/src/app/api/teams/auth/route.ts b/apps/web/src/app/api/teams/auth/route.ts index ad573aa229..6a7a210fa4 100644 --- a/apps/web/src/app/api/teams/auth/route.ts +++ b/apps/web/src/app/api/teams/auth/route.ts @@ -98,14 +98,14 @@ async function startMicrosoftOAuth({ const authResponse = await handleAuthRequest( new Request( new URL( - type === 'link' ? '/api/auth/oauth2/link' : '/api/auth/sign-in/oauth2', + type === 'link' ? '/api/auth/link-social' : '/api/auth/sign-in/social', callbackHost, ), { method: 'POST', headers, body: JSON.stringify({ - providerId: MICROSOFT_ENTRA_PROVIDER_ID, + provider: MICROSOFT_ENTRA_PROVIDER_ID, callbackURL: returnPath, errorCallbackURL: returnPath, }), diff --git a/apps/web/src/hooks/linked-accounts/shared.ts b/apps/web/src/hooks/linked-accounts/shared.ts index f60664b4ea..3d106868e9 100644 --- a/apps/web/src/hooks/linked-accounts/shared.ts +++ b/apps/web/src/hooks/linked-accounts/shared.ts @@ -27,8 +27,8 @@ export function createUseAuthenticateOAuthLinkedAccount({ return function useAuthenticateOAuthLinkedAccount() { return useMutation({ mutationFn: async (redirectPath: string) => { - const result = await authClient.oauth2.link({ - providerId, + const result = await authClient.linkSocial({ + provider: providerId, callbackURL: redirectPath, errorCallbackURL: redirectPath, }); diff --git a/apps/web/src/lib/auth-client.client.test.ts b/apps/web/src/lib/auth-client.client.test.ts new file mode 100644 index 0000000000..867004e776 --- /dev/null +++ b/apps/web/src/lib/auth-client.client.test.ts @@ -0,0 +1,27 @@ +afterEach(() => vi.unstubAllGlobals()); + +it('uses the upgraded library social sign-in and explicit linking endpoints', async () => { + const fetch = vi.fn(async (_input: RequestInfo | URL, _init?: RequestInit) => + Response.json({ + url: 'https://provider.example.test/authorize', + redirect: false, + }), + ); + vi.stubGlobal('fetch', fetch); + vi.resetModules(); + const { authClient } = await import('./auth-client'); + await authClient.signIn.social({ + provider: 'microsoft-entra-id', + callbackURL: '/sessions', + disableRedirect: true, + }); + await authClient.linkSocial({ + provider: 'gitlab', + callbackURL: '/settings/personal', + }); + expect(fetch).toHaveBeenCalledTimes(2); + expect(String(fetch.mock.calls[0]?.[0])).toContain( + '/api/auth/sign-in/social', + ); + expect(String(fetch.mock.calls[1]?.[0])).toContain('/api/auth/link-social'); +}); diff --git a/apps/web/src/lib/auth-client.ts b/apps/web/src/lib/auth-client.ts index f29edb5029..281432a160 100644 --- a/apps/web/src/lib/auth-client.ts +++ b/apps/web/src/lib/auth-client.ts @@ -1,13 +1,12 @@ 'use client'; import { createAuthClient } from 'better-auth/react'; -import { genericOAuthClient } from 'better-auth/client/plugins'; type BaseAuthClient = ReturnType; type OAuth2SignInInput = { callbackURL?: string; disableRedirect?: boolean; - providerId: string; + provider: string; }; type OAuth2SignInResult = { data?: { redirect?: boolean; url?: string } | null; @@ -16,7 +15,7 @@ type OAuth2SignInResult = { type OAuth2LinkInput = { callbackURL: string; errorCallbackURL?: string; - providerId: string; + provider: string; scopes?: string[]; }; type OAuth2LinkResult = { @@ -58,17 +57,14 @@ type ChangeEmailResult = { }; type RoomoteAuthClient = BaseAuthClient & { signIn: BaseAuthClient['signIn'] & { - oauth2(input: OAuth2SignInInput): Promise; - }; - oauth2: { - link(input: OAuth2LinkInput): Promise; + social(input: OAuth2SignInInput): Promise; }; + linkSocial(input: OAuth2LinkInput): Promise; unlinkAccount(input: UnlinkAccountInput): Promise; resetPassword(input: ResetPasswordInput): Promise; changePassword(input: ChangePasswordInput): Promise; changeEmail(input: ChangeEmailInput): Promise; }; -export const authClient: RoomoteAuthClient = createAuthClient({ - plugins: [genericOAuthClient()], -}) as RoomoteAuthClient; +export const authClient: RoomoteAuthClient = + createAuthClient() as RoomoteAuthClient; diff --git a/apps/web/src/lib/server/auth-oauth-compatibility.ts b/apps/web/src/lib/server/auth-oauth-compatibility.ts new file mode 100644 index 0000000000..45c1c3ca35 --- /dev/null +++ b/apps/web/src/lib/server/auth-oauth-compatibility.ts @@ -0,0 +1,204 @@ +import type { BetterAuthPlugin } from 'better-auth'; +import { + addOAuthServerContext, + createAuthMiddleware, + getOAuthState, + getSessionFromCtx, +} from 'better-auth/api'; +import type { OAuthProvider } from 'better-auth/oauth2'; +import { verifyProviderIdToken } from 'better-auth/oauth2'; +import { microsoft } from 'better-auth/social-providers'; + +export const MICROSOFT_ENTRA_PROVIDER_ID = 'microsoft-entra-id'; + +// Providers registered with the legacy generic-OAuth callback URL whose +// profiles may carry placeholder or unverified emails. +const PROVIDER_IDS = new Set([ + 'slack', + MICROSOFT_ENTRA_PROVIDER_ID, + 'gitlab', + 'gitea', + 'bitbucket', + 'ado', +]); + +type ProviderTokens = Parameters[0]; +type ProviderUserInfo = NonNullable< + Awaited> +>; + +// The built-in Microsoft helper decodes the ID token without verifying it. +// Verify it first (signature, issuer, audience, expiry, tenant, nonce), then +// bind the old helper's OIDC userinfo profile (name, preferred_username and +// optional email_verified) to the verified subject. +async function verifyMicrosoftProfile( + provider: OAuthProvider, + tokens: ProviderTokens, +): Promise { + if ( + !tokens.idToken || + !(await verifyProviderIdToken( + provider, + tokens.idToken, + tokens.expectedIdTokenNonce, + )) + ) + return null; + const info = await provider.getUserInfo(tokens); + if (!info) return null; + const response = await fetch('https://graph.microsoft.com/oidc/userinfo', { + headers: { Authorization: `Bearer ${tokens.accessToken}` }, + }); + if (!response.ok) return null; + const profile = (await response.json()) as { + sub?: string; + name?: string; + given_name?: string; + family_name?: string; + email?: string; + preferred_username?: string; + picture?: string; + email_verified?: boolean; + }; + if (!('sub' in info.data) || profile.sub !== info.data.sub) return null; + return { + data: info.data, + user: { + name: + profile.name ?? + (`${profile.given_name ?? ''} ${profile.family_name ?? ''}`.trim() || + undefined), + email: profile.email ?? profile.preferred_username, + image: profile.picture, + emailVerified: profile.email_verified ?? false, + }, + }; +} + +// Explicit linking is the only flow allowed to accept these providers' +// unverified or placeholder emails. Core's link flow only checks +// userInfo.emailVerified and never persists it, so marking the profile +// verified for a server-stamped explicit link lets that link proceed while +// sign-in keeps core's default policy: an unverified email can neither +// create a user implicitly linked by email nor attach itself to an existing +// user, and returning users still sign in by their linked identity. +async function applyExplicitLinkTrust( + providerId: string, + info: ProviderUserInfo, +): Promise { + const state = await getOAuthState(); + const marker = state?.serverContext?.roomoteExplicitLink; + const explicitLink = + !!state?.link && + !!marker && + typeof marker === 'object' && + 'providerId' in marker && + marker.providerId === providerId && + 'userId' in marker && + marker.userId === state.link.userId; + return explicitLink + ? { ...info, user: { ...info.user, emailVerified: true } } + : info; +} + +export function oauthCompatibilityPlugin(options?: { + clientId: string; + clientSecret: string; + tenantId: string; +}): BetterAuthPlugin { + return { + id: 'roomote-oauth-compatibility', + init(ctx) { + const providers: OAuthProvider[] = [...ctx.socialProviders]; + if (options) { + const provider = microsoft({ + ...options, + disableDefaultScope: true, + // offline_access stores an Entra refresh token so delegated + // Microsoft Graph tokens (Teams history reads) can be minted later. + scope: ['openid', 'profile', 'email', 'offline_access'], + disableProfilePhoto: true, + }); + providers.push({ + ...provider, + id: MICROSOFT_ENTRA_PROVIDER_ID, + requiresIdTokenNonce: true, + // Existing accounts use the app-scoped OIDC subject, not Graph oid. + // Keep all upstream signature, issuer, audience and tenant checks. + accountSubject: ({ profile }) => + 'sub' in profile && typeof profile.sub === 'string' + ? profile.sub + : '', + }); + } + return { + context: { + socialProviders: providers.map((provider) => ({ + ...provider, + // Keep provider-registered URLs stable, including on dynamically + // resolved preview hosts. Core composes the redirect URI for both + // authorization and token exchange as baseURL + callbackPath. + callbackPath: `/oauth2/callback/${provider.id}`, + createAuthorizationURL: async (data) => { + const url = await provider.createAuthorizationURL({ + ...data, + // The Slack helper uses the authenticated userinfo endpoint, + // and needs a fresh authorization nonce for every request. + ...(provider.id === 'slack' + ? { idTokenNonce: data.idTokenNonce ?? crypto.randomUUID() } + : {}), + }); + // The built-in Microsoft helper does not yet forward the core + // nonce argument; bind its verified ID token to this flow too. + if ( + provider.id === MICROSOFT_ENTRA_PROVIDER_ID && + data.idTokenNonce + ) { + url.searchParams.set('nonce', data.idTokenNonce); + } + return url; + }, + getUserInfo: async (tokens) => { + const info = + provider.id === MICROSOFT_ENTRA_PROVIDER_ID + ? await verifyMicrosoftProfile(provider, tokens) + : await provider.getUserInfo(tokens); + if (!info || !PROVIDER_IDS.has(provider.id)) return info; + return applyExplicitLinkTrust(provider.id, info); + }, + })), + }, + }; + }, + hooks: { + before: [ + { + matcher: (ctx) => + ctx.path === '/link-social' && PROVIDER_IDS.has(ctx.body?.provider), + handler: createAuthMiddleware(async (ctx) => { + const session = await getSessionFromCtx(ctx); + if (session) + await addOAuthServerContext({ + roomoteExplicitLink: { + providerId: ctx.body.provider, + userId: session.user.id, + }, + }); + }), + }, + ], + }, + }; +} + +// Rewrites the registered legacy callback URL onto core's /callback/:id route +// before dispatch, so state and nonce validation still run in core. Core +// answers a form POST callback with a 302 to its own /api/auth/callback/:id, +// so that path is browser-visible and must stay reachable as well. +export function normalizeLegacyOAuthCallback(request: Request): Request { + const url = new URL(request.url); + const match = /^\/api\/auth\/oauth2\/callback\/([^/]+)$/.exec(url.pathname); + if (!match || !PROVIDER_IDS.has(match[1]!)) return request; + url.pathname = `/api/auth/callback/${match[1]}`; + return new Request(url, request); +} diff --git a/apps/web/src/lib/server/auth-oauth.integration.test.ts b/apps/web/src/lib/server/auth-oauth.integration.test.ts new file mode 100644 index 0000000000..6fea28b479 --- /dev/null +++ b/apps/web/src/lib/server/auth-oauth.integration.test.ts @@ -0,0 +1,483 @@ +import { generateKeyPairSync, sign } from 'node:crypto'; +import { authAccounts, authUsers, db, eq } from '@roomote/db/server'; + +vi.mock('./bootstrap-runtime-env', () => ({ bootstrapWebRuntimeEnv: vi.fn() })); +vi.mock('./auth-provider-config', () => ({ + resolveAuthProviderConfig: vi.fn(), +})); +vi.mock('./better-auth-base-url', () => ({ + getBetterAuthBaseUrlConfig: () => ({ + allowedHosts: ['auth.example.test', 'preview.example.test'], + protocol: 'https', + }), +})); +vi.mock('./env', () => ({ + Env: { R_APP_URL: 'https://auth.example.test' }, + getBetterAuthSecret: () => 'oauth-integration-test-secret-long-enough', + isEmailChannelEnabled: () => false, +})); +vi.mock('./access-policy', () => ({ + isNewAuthUserEmailAllowed: async () => true, + isSignInAllowedByAccessPolicy: async () => true, +})); +vi.mock('./license', () => ({ hasSeatAvailable: async () => true })); + +import { resolveAuthProviderConfig } from './auth-provider-config'; +import { handleAuthRequest } from './auth'; + +const TENANT = '12345678-1234-1234-1234-123456789abc'; +const CONSUMER = '9188040d-6c67-4c5b-b112-36a304b66dad'; +const { privateKey, publicKey } = generateKeyPairSync('rsa', { + modulusLength: 2048, +}); +const jwk = { + ...publicKey.export({ format: 'jwk' }), + kid: 'fixture', + alg: 'RS256', + use: 'sig', +}; +let subject: string; +let tenantClaim: string; +let exchanges: URLSearchParams[]; +let userIds: string[]; +let extraClaims: Record; +let tokenNonce: string | null; +let slackEmailVerified: boolean; + +function jwt() { + const encode = (value: object) => + Buffer.from(JSON.stringify(value)).toString('base64url'); + const input = `${encode({ alg: 'RS256', kid: 'fixture' })}.${encode({ + sub: subject, + oid: `different-oid-${subject}`, + tid: tenantClaim, + iss: `https://login.microsoftonline.com/${tenantClaim}/v2.0`, + aud: 'fixture-client', + iat: Math.floor(Date.now() / 1000), + exp: Math.floor(Date.now() / 1000) + 3600, + email: `${subject}@example.test`, + nonce: tokenNonce, + name: 'OAuth Fixture', + email_verified: true, + ...extraClaims, + })}`; + return `${input}.${sign('RSA-SHA256', Buffer.from(input), privateKey).toString('base64url')}`; +} + +function cookies(response: Response) { + return response.headers + .getSetCookie() + .map((value) => value.split(';')[0]) + .join('; '); +} + +async function request( + path: string, + body?: object, + cookie = '', + host = 'auth.example.test', +) { + return handleAuthRequest( + new Request(`https://${host}/api/auth/${path}`, { + method: body ? 'POST' : 'GET', + headers: { + origin: `https://${host}`, + cookie, + ...(body ? { 'content-type': 'application/json' } : {}), + }, + ...(body ? { body: JSON.stringify(body) } : {}), + }), + ); +} + +async function start( + provider: string, + cookie = '', + host = 'auth.example.test', +) { + const response = await request( + cookie ? 'link-social' : 'sign-in/social', + { + provider, + callbackURL: '/settings/personal', + disableRedirect: true, + }, + cookie, + host, + ); + expect(response.status).toBe(200); + const url = new URL((await response.json()).url); + expect(url.searchParams.get('redirect_uri')).toBe( + `https://${host}/api/auth/oauth2/callback/${provider}`, + ); + return { response, url }; +} + +async function finish( + provider: string, + started: Awaited>, + post = false, +) { + tokenNonce = started.url.searchParams.get('nonce'); + const callback = new URL(started.url.searchParams.get('redirect_uri')!); + const params = new URLSearchParams({ + code: 'normal-fixture-code', + state: started.url.searchParams.get('state')!, + }); + if (!post) callback.search = params.toString(); + return handleAuthRequest( + new Request(callback, { + method: post ? 'POST' : 'GET', + headers: { + cookie: cookies(started.response), + ...(post + ? { 'content-type': 'application/x-www-form-urlencoded' } + : {}), + }, + ...(post ? { body: params.toString() } : {}), + }), + ); +} + +beforeEach(() => { + subject = crypto.randomUUID(); + tenantClaim = TENANT; + exchanges = []; + userIds = []; + extraClaims = {}; + slackEmailVerified = true; + vi.mocked(resolveAuthProviderConfig).mockResolvedValue({ + signature: crypto.randomUUID(), + slackClientId: 'fixture-client', + slackClientSecret: 'fixture-secret', + microsoftClientId: 'fixture-client', + microsoftClientSecret: 'fixture-secret', + microsoftTenantId: TENANT, + gitlabClientId: 'fixture-client', + gitlabClientSecret: 'fixture-secret', + gitlabBaseUrl: 'https://gitlab.example.test', + giteaClientId: 'fixture-client', + giteaClientSecret: 'fixture-secret', + giteaBaseUrl: 'https://gitea.example.test', + bitbucketClientId: 'fixture-client', + bitbucketClientSecret: 'fixture-secret', + bitbucketBaseUrl: 'https://bitbucket.org', + adoClientId: 'fixture-client', + adoClientSecret: 'fixture-secret', + adoOrganization: 'fixture', + adoTenantId: 'common', + } as Awaited>); + vi.stubGlobal( + 'fetch', + vi.fn(async (input: string | URL | Request, init?: RequestInit) => { + const url = new URL( + input instanceof Request ? input.url : input.toString(), + ); + if (url.pathname.endsWith('/keys')) return Response.json({ keys: [jwk] }); + if ( + url.pathname.endsWith('/token') || + url.pathname.endsWith('/access_token') || + url.pathname.endsWith('openid.connect.token') + ) { + exchanges.push(new URLSearchParams(String(init?.body))); + return Response.json({ + access_token: 'fixture-access', + token_type: 'Bearer', + expires_in: 3600, + ...(url.hostname === 'login.microsoftonline.com' + ? { id_token: jwt() } + : {}), + }); + } + if (url.pathname.endsWith('openid.connect.userInfo')) { + return Response.json({ + sub: subject, + email: `${subject}@example.test`, + name: 'Slack Fixture', + email_verified: slackEmailVerified, + }); + } + if ( + url.hostname === 'graph.microsoft.com' && + url.pathname === '/oidc/userinfo' + ) { + return Response.json({ + sub: subject, + preferred_username: `${subject}@example.test`, + given_name: 'Microsoft', + family_name: 'Fixture', + }); + } + if (url.pathname === '/api/v4/user') { + return Response.json({ + id: subject, + username: 'fixture', + email: `${subject}@example.test`, + name: 'GitLab Fixture', + }); + } + if (url.pathname === '/api/v1/user') + return Response.json({ + id: subject, + login: 'fixture', + email: `${subject}@example.test`, + }); + if (url.pathname === '/2.0/user') + return Response.json({ account_id: subject, display_name: 'Fixture' }); + if (url.pathname === '/_apis/connectionData') + return Response.json({ + authenticatedUser: { + id: subject, + uniqueName: `${subject}@example.test`, + displayName: 'Fixture', + }, + }); + if (url.pathname === '/_apis/profile/profiles/me') + return Response.json({ + id: subject, + emailAddress: `${subject}@example.test`, + }); + throw new Error( + `Unexpected provider request: ${url.origin}${url.pathname}`, + ); + }), + ); +}); + +afterEach(async () => { + for (const id of userIds) + await db.delete(authUsers).where(eq(authUsers.id, id)); + vi.unstubAllGlobals(); +}); + +async function existingUser(providerId: string) { + const id = crypto.randomUUID(); + userIds.push(id); + await db.insert(authUsers).values({ + id, + name: 'Returning Fixture', + email: `${subject}@example.test`, + emailVerified: true, + }); + const accountId = crypto.randomUUID(); + await db + .insert(authAccounts) + .values({ id: accountId, userId: id, providerId, accountId: subject }); + return { id, accountId }; +} + +it.each(['slack', 'microsoft-entra-id', 'gitlab', 'gitea', 'bitbucket', 'ado'])( + 'initiates %s through real core sign-in and preserves registered preview callbacks', + async (provider) => { + await start(provider, '', 'preview.example.test'); + }, +); + +it('creates a fresh Slack nonce for each authorization request and retains scopes', async () => { + const a = await start('slack'); + const b = await start('slack'); + expect(a.url.searchParams.get('nonce')).toBeTruthy(); + expect(b.url.searchParams.get('nonce')).not.toBe( + a.url.searchParams.get('nonce'), + ); + expect(a.url.searchParams.get('scope')?.split(' ')).toEqual( + expect.arrayContaining(['openid', 'profile', 'email']), + ); +}); + +it.each([false, true])( + 'completes Slack returning-user callback (POST=%s) without duplicating identity', + async (post) => { + const user = await existingUser('slack'); + const started = await start('slack'); + const response = await finish('slack', started, post); + // Core turns form_post into a same-origin GET before completing the flow. + const completed = + post && response.headers.get('location')?.includes('/callback/') + ? await handleAuthRequest( + new Request( + new URL( + response.headers.get('location')!, + 'https://auth.example.test', + ), + { headers: { cookie: cookies(started.response) } }, + ), + ) + : response; + expect(completed.status).toBe(302); + expect(completed.headers.get('location')).toBe('/settings/personal'); + const session = await request('get-session', undefined, cookies(completed)); + expect((await session.json()).user.id).toBe(user.id); + expect( + ( + await db.query.authAccounts.findMany({ + where: eq(authAccounts.userId, user.id), + }) + ).map((a) => a.id), + ).toEqual([user.accountId]); + expect(exchanges[0]?.get('redirect_uri')).toBe( + started.url.searchParams.get('redirect_uri'), + ); + }, +); + +it.each([TENANT, 'common', 'organizations', 'consumers'])( + 'retains Microsoft sub identity and validated sign-in for tenant %s', + async (tenant) => { + const config = await resolveAuthProviderConfig(); + vi.mocked(resolveAuthProviderConfig).mockResolvedValue({ + ...config, + signature: crypto.randomUUID(), + microsoftTenantId: tenant, + }); + if (tenant === 'consumers') tenantClaim = CONSUMER; + const user = await existingUser('microsoft-entra-id'); + const started = await start('microsoft-entra-id'); + const response = await finish('microsoft-entra-id', started); + expect(response.headers.get('location')).toBe('/settings/personal'); + const session = await request('get-session', undefined, cookies(response)); + expect((await session.json()).user.id).toBe(user.id); + const accounts = await db.query.authAccounts.findMany({ + where: eq(authAccounts.userId, user.id), + }); + expect(accounts).toHaveLength(1); + expect(accounts[0]).toMatchObject({ + id: user.accountId, + accountId: subject, + providerId: 'microsoft-entra-id', + }); + expect(exchanges[0]?.get('redirect_uri')).toBe( + started.url.searchParams.get('redirect_uri'), + ); + }, +); + +it.each(['slack', 'microsoft-entra-id', 'gitlab', 'gitea', 'bitbucket', 'ado'])( + 'links %s to an existing real session', + async (provider) => { + const signup = await request('sign-up/email', { + email: `${subject}@example.test`, + name: 'Link Fixture', + password: 'fixture-password-123!', + }); + expect(signup.status).toBe(200); + const id = (await signup.json()).user.id as string; + userIds.push(id); + const started = await start(provider, cookies(signup)); + const response = await finish(provider, started); + expect(response.headers.get('location')).toBe('/settings/personal'); + expect( + await db.query.authAccounts.findFirst({ + where: eq( + authAccounts.accountId, + provider === 'ado' ? `${subject}@example.test` : subject, + ), + }), + ).toMatchObject({ userId: id, providerId: provider }); + }, +); + +it('retains returning generic OAuth sign-in by existing provider identity', async () => { + const user = await existingUser('gitlab'); + const response = await finish('gitlab', await start('gitlab')); + expect(response.headers.get('location')).toBe('/settings/personal'); + expect( + (await (await request('get-session', undefined, cookies(response))).json()) + .user.id, + ).toBe(user.id); +}); + +it.each(['slack', 'gitlab', 'microsoft-entra-id'])( + 'keeps unverified %s profiles out of implicit email linking', + async (provider) => { + slackEmailVerified = false; + const signup = await request('sign-up/email', { + email: `${subject}@example.test`, + name: 'Existing Fixture', + password: 'fixture-password-123!', + }); + const userId = (await signup.json()).user.id as string; + userIds.push(userId); + const response = await finish(provider, await start(provider)); + expect(response.headers.get('location')).toContain('error='); + expect( + await db.query.authAccounts.findFirst({ + where: eq(authAccounts.accountId, subject), + }), + ).toBeUndefined(); + }, +); + +it('preserves new Microsoft sign-up with userinfo preferred_username and no email verification claim', async () => { + const response = await finish( + 'microsoft-entra-id', + await start('microsoft-entra-id'), + ); + expect(response.headers.get('location')).toBe('/settings/personal'); + const session = await ( + await request('get-session', undefined, cookies(response)) + ).json(); + userIds.push(session.user.id); + expect(session.user).toMatchObject({ + email: `${subject}@example.test`, + name: 'Microsoft Fixture', + emailVerified: false, + }); +}); + +it.each([ + { aud: 'different-client' }, + { iss: 'https://other-issuer.example.test' }, + { exp: 1 }, + { nonce: 'different-flow' }, +])( + 'retains upstream Microsoft ID token validation for invalid fixture claims %j', + async (claims) => { + extraClaims = claims; + await existingUser('microsoft-entra-id'); + const response = await finish( + 'microsoft-entra-id', + await start('microsoft-entra-id'), + ); + expect(response.headers.get('location')).toContain('error='); + expect( + await (await request('get-session', undefined, cookies(response))).json(), + ).toBeNull(); + }, +); + +it('completes a legacy Microsoft form POST callback', async () => { + const user = await existingUser('microsoft-entra-id'); + const started = await start('microsoft-entra-id'); + const response = await finish('microsoft-entra-id', started, true); + const completed = await handleAuthRequest( + new Request( + new URL(response.headers.get('location')!, 'https://auth.example.test'), + { headers: { cookie: cookies(started.response) } }, + ), + ); + expect(completed.headers.get('location')).toBe('/settings/personal'); + expect( + (await (await request('get-session', undefined, cookies(completed))).json()) + .user.id, + ).toBe(user.id); +}); + +it.each(['organizations', 'consumers'])( + 'retains Microsoft account-class restrictions for %s', + async (tenant) => { + const config = await resolveAuthProviderConfig(); + vi.mocked(resolveAuthProviderConfig).mockResolvedValue({ + ...config, + signature: crypto.randomUUID(), + microsoftTenantId: tenant, + }); + tenantClaim = tenant === 'organizations' ? CONSUMER : TENANT; + await existingUser('microsoft-entra-id'); + const response = await finish( + 'microsoft-entra-id', + await start('microsoft-entra-id'), + ); + expect(response.headers.get('location')).toContain('error='); + }, +); diff --git a/apps/web/src/lib/server/auth.ts b/apps/web/src/lib/server/auth.ts index 9a494fa62a..6aecf9552d 100644 --- a/apps/web/src/lib/server/auth.ts +++ b/apps/web/src/lib/server/auth.ts @@ -3,7 +3,7 @@ import { AsyncLocalStorage } from 'node:async_hooks'; import { betterAuth } from 'better-auth'; import { APIError } from 'better-auth/api'; import { nextCookies } from 'better-auth/next-js'; -import { genericOAuth, microsoftEntraId, slack } from 'better-auth/plugins'; +import { genericOAuth, slack } from 'better-auth/plugins'; import { drizzleAdapter } from '@better-auth/drizzle-adapter'; import { normalizeAdoLinkedAccountKey } from '@roomote/ado'; // Subpath import on purpose: the SDK barrel drags the whole server graph @@ -30,6 +30,11 @@ import { Env, getBetterAuthSecret, isEmailChannelEnabled } from './env'; import { getBetterAuthBaseUrlConfig } from './better-auth-base-url'; import { withCanonicalForwardedProto } from './canonical-forwarded-proto'; import { bootstrapWebRuntimeEnv } from './bootstrap-runtime-env'; +import { + MICROSOFT_ENTRA_PROVIDER_ID, + normalizeLegacyOAuthCallback, + oauthCompatibilityPlugin, +} from './auth-oauth-compatibility'; import { isNewAuthUserEmailAllowed, isSignInAllowedByAccessPolicy, @@ -202,7 +207,6 @@ type AdoProfile = { id?: unknown; }; -const MICROSOFT_ENTRA_PROVIDER_ID = 'microsoft-entra-id'; const SLACK_OPENID_SCOPES = ['openid', 'profile', 'email']; const DEFAULT_GITLAB_BASE_URL = 'https://gitlab.com'; const DEFAULT_ADO_BASE_URL = 'https://dev.azure.com'; @@ -779,25 +783,9 @@ async function createAuth(authProviderConfig: ResolvedAuthProviderConfig) { clientSecret: slackClientSecret, scopes: SLACK_OPENID_SCOPES, }), - authorizationUrlParams: () => ({ - nonce: crypto.randomUUID(), - }), }, ] : []), - ...(hasMicrosoftAuth - ? [ - microsoftEntraId({ - clientId: microsoftClientId, - clientSecret: microsoftClientSecret, - tenantId: microsoftTenantId, - // offline_access stores an Entra refresh token so delegated - // Microsoft Graph tokens (Teams history reads) can be minted for - // the linked user later. - scopes: ['openid', 'profile', 'email', 'offline_access'], - }), - ] - : []), ...(gitlabClientId && gitlabClientSecret ? [ { @@ -1260,6 +1248,15 @@ async function createAuth(authProviderConfig: ResolvedAuthProviderConfig) { ...(genericOAuthConfigs.length > 0 ? [genericOAuth({ config: genericOAuthConfigs })] : []), + oauthCompatibilityPlugin( + hasMicrosoftAuth + ? { + clientId: microsoftClientId, + clientSecret: microsoftClientSecret, + tenantId: microsoftTenantId, + } + : undefined, + ), nextCookies(), ], }); @@ -1287,6 +1284,6 @@ export async function handleAuthRequest(request: Request) { // otherwise have no access to the request. return runWithInviteContext( extractInviteTokenFromRequest(normalizedRequest), - () => auth.handler(normalizedRequest), + () => auth.handler(normalizeLegacyOAuthCallback(normalizedRequest)), ); } diff --git a/package.json b/package.json index ec470dc342..752d92561a 100644 --- a/package.json +++ b/package.json @@ -109,8 +109,8 @@ "glob": ">=13.0.3", "follow-redirects": "1.16.1", "ioredis": "5.10.1", - "better-auth>zod": "^4.3.6", - "@better-auth/core>zod": "^4.3.6", + "better-auth>zod": "^4.5.0", + "@better-auth/core>zod": "^4.5.0", "js-cookie": "3.0.7", "js-yaml": "4.3.2", "gray-matter>js-yaml": "3.15.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1231b98298..b1bea814c7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -25,8 +25,8 @@ overrides: glob: '>=13.0.3' follow-redirects: 1.16.1 ioredis: 5.10.1 - better-auth>zod: ^4.3.6 - '@better-auth/core>zod': ^4.3.6 + better-auth>zod: ^4.5.0 + '@better-auth/core>zod': ^4.5.0 js-cookie: 3.0.7 js-yaml: 4.3.2 gray-matter>js-yaml: 3.15.2 @@ -274,7 +274,7 @@ importers: version: 8.5.1(jiti@2.7.0)(postcss@8.5.23)(tsx@4.20.4)(typescript@5.9.3)(yaml@2.9.0) vitest: specifier: 4.1.12 - version: 4.1.12(@edge-runtime/vm@3.2.0)(@opentelemetry/api@1.9.1)(@types/node@24.12.4)(jsdom@26.1.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))(vite@7.3.7(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.20.4)(yaml@2.9.0)) + version: 4.1.12(@edge-runtime/vm@3.2.0)(@opentelemetry/api@1.9.0)(@types/node@24.12.4)(jsdom@26.1.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))(vite@7.3.7(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.20.4)(yaml@2.9.0)) apps/bullmq: dependencies: @@ -359,7 +359,7 @@ importers: version: 8.5.1(jiti@2.7.0)(postcss@8.5.23)(tsx@4.20.4)(typescript@5.9.3)(yaml@2.9.0) vitest: specifier: 4.1.12 - version: 4.1.12(@edge-runtime/vm@3.2.0)(@opentelemetry/api@1.9.0)(@types/node@24.12.4)(jsdom@26.1.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))(vite@7.3.7(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.20.4)(yaml@2.9.0)) + version: 4.1.12(@edge-runtime/vm@3.2.0)(@opentelemetry/api@1.9.1)(@types/node@24.12.4)(jsdom@26.1.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))(vite@7.3.7(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.20.4)(yaml@2.9.0)) apps/controller: dependencies: @@ -554,8 +554,8 @@ importers: specifier: 3.1000.0 version: 3.1000.0 '@better-auth/drizzle-adapter': - specifier: ^1.6.22 - version: 1.6.22(@better-auth/core@1.6.22(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@3.25.76))(jose@6.2.4)(kysely@0.29.4)(nanostores@1.4.1))(@better-auth/utils@0.4.2)(drizzle-orm@0.45.2(@electric-sql/pglite@0.3.0)(@libsql/client-wasm@0.15.5)(@opentelemetry/api@1.9.1)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(kysely@0.29.4)(pg@8.15.6)(postgres@3.4.8)) + specifier: ^1.7.12 + version: 1.7.12(@better-auth/core@1.7.12(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.4(zod@3.25.76))(jose@6.2.4)(kysely@0.29.4)(nanostores@1.4.1))(@better-auth/utils@0.5.0)(drizzle-orm@0.45.2(@electric-sql/pglite@0.3.0)(@libsql/client-wasm@0.15.5)(@opentelemetry/api@1.9.1)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(kysely@0.29.4)(pg@8.15.6)(postgres@3.4.8)) '@codemirror/lang-yaml': specifier: ^6.0.0 version: 6.1.3 @@ -746,8 +746,8 @@ importers: specifier: ^6.2.6 version: 6.2.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) better-auth: - specifier: ^1.6.22 - version: 1.6.22(@opentelemetry/api@1.9.1)(better-sqlite3@11.10.0)(drizzle-kit@0.31.9)(drizzle-orm@0.45.2(@electric-sql/pglite@0.3.0)(@libsql/client-wasm@0.15.5)(@opentelemetry/api@1.9.1)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(kysely@0.29.4)(pg@8.15.6)(postgres@3.4.8))(next@16.4.1(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@types/node@24.12.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.15.6)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.1.12(@edge-runtime/vm@3.2.0)(@opentelemetry/api@1.9.1)(@types/node@24.12.4)(jsdom@26.1.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))(vite@7.3.7(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.20.4)(yaml@2.9.0))) + specifier: ^1.7.12 + version: 1.7.12(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.9)(drizzle-orm@0.45.2(@electric-sql/pglite@0.3.0)(@libsql/client-wasm@0.15.5)(@opentelemetry/api@1.9.1)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(kysely@0.29.4)(pg@8.15.6)(postgres@3.4.8))(next@16.4.1(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@types/node@24.12.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.15.6)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.1.12(@edge-runtime/vm@3.2.0)(@opentelemetry/api@1.9.1)(@types/node@24.12.4)(jsdom@26.1.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))(vite@7.3.7(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.20.4)(yaml@2.9.0))) better-sse: specifier: ^0.16.1 version: 0.16.1 @@ -2221,64 +2221,61 @@ packages: '@balena/dockerignore@1.0.2': resolution: {integrity: sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==} - '@better-auth/core@1.6.22': - resolution: {integrity: sha512-aFH/5nzmR501jAJPKjJfiVg4BrkcjVCqq9WS9JnhTruE/2PIWopv1QGMiRIRqxXaPbHczri7cqRdhep3Lg5PMw==} + '@better-auth/core@1.7.12': + resolution: {integrity: sha512-SUStBGHJ13KnSO6+7U/1DSlfuOtln4jtOrFZqzQtxZVHZIrAoMjwHNGitlF+s7hvKbuC3Z14xjnqfLLqIqGfxw==} peerDependencies: - '@better-auth/utils': 0.4.2 - '@better-fetch/fetch': 1.3.1 - '@cloudflare/workers-types': '>=4' + '@better-auth/utils': ^0.4.2 || ^0.5.0 + '@better-fetch/fetch': 1.3.2 '@opentelemetry/api': ^1.9.0 - better-call: 1.3.7 + better-call: 1.4.4 jose: ^6.1.0 kysely: ^0.28.5 || ^0.29.0 nanostores: ^1.0.1 peerDependenciesMeta: - '@cloudflare/workers-types': - optional: true '@opentelemetry/api': optional: true - '@better-auth/drizzle-adapter@1.6.22': - resolution: {integrity: sha512-uNa9qH53CfxBmuKP8kbLWxY90oIRwUPn5BcHhO+szK05e2yh6EYwSNNivDSqV3YG5HPfjtPHulklInjq1wtm3w==} + '@better-auth/drizzle-adapter@1.7.12': + resolution: {integrity: sha512-Qzoyuf05aK01Pc0k3EXv8Xj3XCT2m2dCWc5HoB7x+xX6N3Pm2eQsca+UIArsJcbuNEWWP3L51oIebnBoHJ5DTw==} peerDependencies: - '@better-auth/core': ^1.6.22 - '@better-auth/utils': 0.4.2 - drizzle-orm: ^0.45.2 + '@better-auth/core': ^1.7.12 + '@better-auth/utils': ^0.4.2 || ^0.5.0 + drizzle-orm: ^0.45.2 || >=1.0.0-rc.1 <2.0.0 peerDependenciesMeta: drizzle-orm: optional: true - '@better-auth/kysely-adapter@1.6.22': - resolution: {integrity: sha512-4k/07lPRizlQi+B+uOE5CwTfH3w+Lq8ZDX1nDN1+e+glRVKAIfHoLvC9cfAVcCbio3DDrl0RTbwjLwjEhG0LxA==} + '@better-auth/kysely-adapter@1.7.12': + resolution: {integrity: sha512-1PaFXktLv4n3DmrGbPaCj5SsvcT40NnWw7NLua5/J1+8/A051N5ZzqOwJifXEIQX5k6NdwvmymwDQxKytduEAQ==} peerDependencies: - '@better-auth/core': ^1.6.22 - '@better-auth/utils': 0.4.2 + '@better-auth/core': ^1.7.12 + '@better-auth/utils': ^0.4.2 || ^0.5.0 kysely: ^0.28.17 || ^0.29.0 peerDependenciesMeta: kysely: optional: true - '@better-auth/memory-adapter@1.6.22': - resolution: {integrity: sha512-rbepe/gHhWs0aF4fAu6+l+wNPJxT9XN4U+Hqa1Y/5HhjtT9y5evo3INrSWlkIOymsMaQ0cBPrSL5pm9Z195hcA==} + '@better-auth/memory-adapter@1.7.12': + resolution: {integrity: sha512-glaDwXzDptnddHqRuu7fj47FAssUcOzLNgdlTGJoEPrspkX69x2vnXwgAzNuApjZI21WLtPH/rQ6CN+DQGybPA==} peerDependencies: - '@better-auth/core': ^1.6.22 - '@better-auth/utils': 0.4.2 + '@better-auth/core': ^1.7.12 + '@better-auth/utils': ^0.4.2 || ^0.5.0 - '@better-auth/mongo-adapter@1.6.22': - resolution: {integrity: sha512-OYnfySHlVkIx7y6XNBsCHjKhl6IYGprRkFwifc/TAuPBVjoRKhLKRAXuzMdWTQYFt4FYb6holbhjNUrXxPIWcw==} + '@better-auth/mongo-adapter@1.7.12': + resolution: {integrity: sha512-HKV4sgYSw/Gdt9AuHlESMkiKL72SWdGweDy1Wsyxoh06BpG7aNu4aSJ7marNpMdKZteyRbxWWqQkA9ediA8qFw==} peerDependencies: - '@better-auth/core': ^1.6.22 - '@better-auth/utils': 0.4.2 + '@better-auth/core': ^1.7.12 + '@better-auth/utils': ^0.4.2 || ^0.5.0 mongodb: ^6.0.0 || ^7.0.0 peerDependenciesMeta: mongodb: optional: true - '@better-auth/prisma-adapter@1.6.22': - resolution: {integrity: sha512-I6lWQwLva732V600u5dLM2kRcQ94pRZOVVfZ87+Ow9RBxMUnV+I+YQ5h2yggdN2tmsmITacZTy1DSZbDxGu0LQ==} + '@better-auth/prisma-adapter@1.7.12': + resolution: {integrity: sha512-FZRX6ORbwhDmeYE1Tl0IPtUn8v3BlVFgbJxDnm/DCNHxuFRf+0t4HfddUgYeYehHFR8aSDsdYX44fEFWM9877w==} peerDependencies: - '@better-auth/core': ^1.6.22 - '@better-auth/utils': 0.4.2 + '@better-auth/core': ^1.7.12 + '@better-auth/utils': ^0.4.2 || ^0.5.0 '@prisma/client': ^5.0.0 || ^6.0.0 || ^7.0.0 prisma: ^5.0.0 || ^6.0.0 || ^7.0.0 peerDependenciesMeta: @@ -2287,18 +2284,18 @@ packages: prisma: optional: true - '@better-auth/telemetry@1.6.22': - resolution: {integrity: sha512-glq/oEk9qP+zGh9k/WUH5+pwvBCMolNNhaAVBCtYQrkADFee2gP3VoPs1YeO9coNuOmBhc+AYSIHs+fL9DoJnw==} + '@better-auth/telemetry@1.7.12': + resolution: {integrity: sha512-DaDkKNnI99vKGGbWq9KJg2uYeaOjsK9nIBw9T2m2KG/RkxpvQRjYkZ00++up2bZQkR5pB1nukGbplza3UJG/Xg==} peerDependencies: - '@better-auth/core': ^1.6.22 - '@better-auth/utils': 0.4.2 - '@better-fetch/fetch': 1.3.1 + '@better-auth/core': ^1.7.12 + '@better-auth/utils': ^0.4.2 || ^0.5.0 + '@better-fetch/fetch': 1.3.2 - '@better-auth/utils@0.4.2': - resolution: {integrity: sha512-AUxrvu+HaaODsUyzDxFgwd/8RZ1yZaYo42LXKSrU2oGgR38pS1ij8nqQKNgtTWoYGpNevNXtCfgTy6loHveW9A==} + '@better-auth/utils@0.5.0': + resolution: {integrity: sha512-BL8W4EfIZFwlu0r54m3v1ztjDhu6dDe/amLTm0xybmbZaNgYUqhD3SjpAsnq0q8YD6/ki4iwIgxJNLP/N3TxiA==} - '@better-fetch/fetch@1.3.1': - resolution: {integrity: sha512-ABkD1WhyfPZprKRQI3bhATjeiFuNWC9PXhfGWqL+sg/gKrM977oFrYkdb4msM3hgUGonr7KlOsOFT5TU2rht9g==} + '@better-fetch/fetch@1.3.2': + resolution: {integrity: sha512-Gs7n99b5tqUC6cQAPbV0uED3IraHB6xQbHLQ/C3l7ZFafHScOx9pQ+DYmP5blbLFShVWLqxNUlI9wi4xU/X+ow==} '@blaxel/core@0.3.0': resolution: {integrity: sha512-QfTpeh+qEe57GiwPfkT9HvBkMILrrq8k8fo2pyjU/nm2y+1tbBVK9reXwgy2Ne8oTPjlDJN/uBJb54nGSm0lmA==} @@ -7144,17 +7141,16 @@ packages: before-after-hook@4.0.0: resolution: {integrity: sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==} - better-auth@1.6.22: - resolution: {integrity: sha512-B5s6+lPsDWp8rGLRnvNyr5h9tftG9zLRjNrlkEJdYRhcuhPhJiw9b8o6ibgxEFpSAdUqoDaP5/FLqfu8QsXIVg==} + better-auth@1.7.12: + resolution: {integrity: sha512-f1fEG0QdTslzbmFA+vOGFFW/XXCWkG4kBxLeH9n1vp1otTH9+ZzV6EJ6797GStWeiYYgS5Vnt9xqE12KwSyAAw==} peerDependencies: '@lynx-js/react': '*' '@prisma/client': ^5.0.0 || ^6.0.0 || ^7.0.0 - '@sveltejs/kit': ^2.0.0 + '@sveltejs/kit': ^2.0.0 || ^3.0.0 '@tanstack/react-start': ^1.0.0 '@tanstack/solid-start': ^1.0.0 - better-sqlite3: ^12.0.0 - drizzle-kit: '>=0.31.4' - drizzle-orm: ^0.45.2 + drizzle-kit: '>=0.31.4 || >=1.0.0-beta.1' + drizzle-orm: ^0.45.2 || >=1.0.0-rc.1 <2.0.0 mongodb: ^6.0.0 || ^7.0.0 mysql2: ^3.0.0 next: ^14.0.0 || ^15.0.0 || ^16.0.0 @@ -7177,8 +7173,6 @@ packages: optional: true '@tanstack/solid-start': optional: true - better-sqlite3: - optional: true drizzle-kit: optional: true drizzle-orm: @@ -7206,8 +7200,8 @@ packages: vue: optional: true - better-call@1.3.7: - resolution: {integrity: sha512-Al51/hjp2SSp6CRTa3F2ptcx4yQVS1xWKoY6jcVXqNYOap6mHFP2jUBn5EwIL4iIed1/Sq4hlQ+Umm6EflZG+w==} + better-call@1.4.4: + resolution: {integrity: sha512-Gq4XdGFy4dmUxFEAX2nj0ycFcFbD9ZWh8YROouZoyXeF1M11TpYbplk0pDByTFF8Wz+MYa6W9052c9m7dD+mfw==} peerDependencies: zod: ^3.25.76 peerDependenciesMeta: @@ -11045,8 +11039,8 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true - rou3@0.7.12: - resolution: {integrity: sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg==} + rou3@0.9.2: + resolution: {integrity: sha512-3SOzvaAg8rkHrXtRjpCvCvbyO5to9oOO27Z/XqHEYXfMRVSw/qMIVdmaOk9W2lcRLtR6dlqTjo9hDeJk70QBYQ==} roughjs@4.6.6: resolution: {integrity: sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==} @@ -12352,8 +12346,8 @@ packages: zod@4.3.6: resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} - zod@4.4.3: - resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + zod@4.6.7: + resolution: {integrity: sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==} zustand@5.0.10: resolution: {integrity: sha512-U1AiltS1O9hSy3rul+Ub82ut2fqIAefiSuwECWt6jlMVUGejvf+5omLcRBSzqbRagSM3hQZbtzdeRc6QVScXTg==} @@ -13101,60 +13095,59 @@ snapshots: '@balena/dockerignore@1.0.2': {} - '@better-auth/core@1.6.22(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@3.25.76))(jose@6.2.4)(kysely@0.29.4)(nanostores@1.4.1)': + '@better-auth/core@1.7.12(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.4(zod@3.25.76))(jose@6.2.4)(kysely@0.29.4)(nanostores@1.4.1)': dependencies: - '@better-auth/utils': 0.4.2 - '@better-fetch/fetch': 1.3.1 - '@opentelemetry/semantic-conventions': 1.43.0 + '@better-auth/utils': 0.5.0 + '@better-fetch/fetch': 1.3.2 '@standard-schema/spec': 1.1.0 - better-call: 1.3.7(zod@4.4.3) + better-call: 1.4.4(zod@4.6.7) jose: 6.2.4 kysely: 0.29.4 nanostores: 1.4.1 - zod: 4.4.3 + zod: 4.6.7 optionalDependencies: '@opentelemetry/api': 1.9.1 - '@better-auth/drizzle-adapter@1.6.22(@better-auth/core@1.6.22(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@3.25.76))(jose@6.2.4)(kysely@0.29.4)(nanostores@1.4.1))(@better-auth/utils@0.4.2)(drizzle-orm@0.45.2(@electric-sql/pglite@0.3.0)(@libsql/client-wasm@0.15.5)(@opentelemetry/api@1.9.1)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(kysely@0.29.4)(pg@8.15.6)(postgres@3.4.8))': + '@better-auth/drizzle-adapter@1.7.12(@better-auth/core@1.7.12(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.4(zod@3.25.76))(jose@6.2.4)(kysely@0.29.4)(nanostores@1.4.1))(@better-auth/utils@0.5.0)(drizzle-orm@0.45.2(@electric-sql/pglite@0.3.0)(@libsql/client-wasm@0.15.5)(@opentelemetry/api@1.9.1)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(kysely@0.29.4)(pg@8.15.6)(postgres@3.4.8))': dependencies: - '@better-auth/core': 1.6.22(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@3.25.76))(jose@6.2.4)(kysely@0.29.4)(nanostores@1.4.1) - '@better-auth/utils': 0.4.2 + '@better-auth/core': 1.7.12(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.4(zod@3.25.76))(jose@6.2.4)(kysely@0.29.4)(nanostores@1.4.1) + '@better-auth/utils': 0.5.0 optionalDependencies: drizzle-orm: 0.45.2(@electric-sql/pglite@0.3.0)(@libsql/client-wasm@0.15.5)(@opentelemetry/api@1.9.1)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(kysely@0.29.4)(pg@8.15.6)(postgres@3.4.8) - '@better-auth/kysely-adapter@1.6.22(@better-auth/core@1.6.22(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@3.25.76))(jose@6.2.4)(kysely@0.29.4)(nanostores@1.4.1))(@better-auth/utils@0.4.2)(kysely@0.29.4)': + '@better-auth/kysely-adapter@1.7.12(@better-auth/core@1.7.12(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.4(zod@3.25.76))(jose@6.2.4)(kysely@0.29.4)(nanostores@1.4.1))(@better-auth/utils@0.5.0)(kysely@0.29.4)': dependencies: - '@better-auth/core': 1.6.22(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@3.25.76))(jose@6.2.4)(kysely@0.29.4)(nanostores@1.4.1) - '@better-auth/utils': 0.4.2 + '@better-auth/core': 1.7.12(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.4(zod@3.25.76))(jose@6.2.4)(kysely@0.29.4)(nanostores@1.4.1) + '@better-auth/utils': 0.5.0 optionalDependencies: kysely: 0.29.4 - '@better-auth/memory-adapter@1.6.22(@better-auth/core@1.6.22(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@3.25.76))(jose@6.2.4)(kysely@0.29.4)(nanostores@1.4.1))(@better-auth/utils@0.4.2)': + '@better-auth/memory-adapter@1.7.12(@better-auth/core@1.7.12(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.4(zod@3.25.76))(jose@6.2.4)(kysely@0.29.4)(nanostores@1.4.1))(@better-auth/utils@0.5.0)': dependencies: - '@better-auth/core': 1.6.22(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@3.25.76))(jose@6.2.4)(kysely@0.29.4)(nanostores@1.4.1) - '@better-auth/utils': 0.4.2 + '@better-auth/core': 1.7.12(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.4(zod@3.25.76))(jose@6.2.4)(kysely@0.29.4)(nanostores@1.4.1) + '@better-auth/utils': 0.5.0 - '@better-auth/mongo-adapter@1.6.22(@better-auth/core@1.6.22(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@3.25.76))(jose@6.2.4)(kysely@0.29.4)(nanostores@1.4.1))(@better-auth/utils@0.4.2)': + '@better-auth/mongo-adapter@1.7.12(@better-auth/core@1.7.12(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.4(zod@3.25.76))(jose@6.2.4)(kysely@0.29.4)(nanostores@1.4.1))(@better-auth/utils@0.5.0)': dependencies: - '@better-auth/core': 1.6.22(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@3.25.76))(jose@6.2.4)(kysely@0.29.4)(nanostores@1.4.1) - '@better-auth/utils': 0.4.2 + '@better-auth/core': 1.7.12(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.4(zod@3.25.76))(jose@6.2.4)(kysely@0.29.4)(nanostores@1.4.1) + '@better-auth/utils': 0.5.0 - '@better-auth/prisma-adapter@1.6.22(@better-auth/core@1.6.22(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@3.25.76))(jose@6.2.4)(kysely@0.29.4)(nanostores@1.4.1))(@better-auth/utils@0.4.2)': + '@better-auth/prisma-adapter@1.7.12(@better-auth/core@1.7.12(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.4(zod@3.25.76))(jose@6.2.4)(kysely@0.29.4)(nanostores@1.4.1))(@better-auth/utils@0.5.0)': dependencies: - '@better-auth/core': 1.6.22(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@3.25.76))(jose@6.2.4)(kysely@0.29.4)(nanostores@1.4.1) - '@better-auth/utils': 0.4.2 + '@better-auth/core': 1.7.12(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.4(zod@3.25.76))(jose@6.2.4)(kysely@0.29.4)(nanostores@1.4.1) + '@better-auth/utils': 0.5.0 - '@better-auth/telemetry@1.6.22(@better-auth/core@1.6.22(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@3.25.76))(jose@6.2.4)(kysely@0.29.4)(nanostores@1.4.1))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)': + '@better-auth/telemetry@1.7.12(@better-auth/core@1.7.12(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.4(zod@3.25.76))(jose@6.2.4)(kysely@0.29.4)(nanostores@1.4.1))(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)': dependencies: - '@better-auth/core': 1.6.22(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@3.25.76))(jose@6.2.4)(kysely@0.29.4)(nanostores@1.4.1) - '@better-auth/utils': 0.4.2 - '@better-fetch/fetch': 1.3.1 + '@better-auth/core': 1.7.12(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.4(zod@3.25.76))(jose@6.2.4)(kysely@0.29.4)(nanostores@1.4.1) + '@better-auth/utils': 0.5.0 + '@better-fetch/fetch': 1.3.2 - '@better-auth/utils@0.4.2': + '@better-auth/utils@0.5.0': dependencies: '@noble/hashes': 2.2.0 - '@better-fetch/fetch@1.3.1': {} + '@better-fetch/fetch@1.3.2': {} '@blaxel/core@0.3.0(@cfworker/json-schema@4.1.1)(@hey-api/openapi-ts@0.99.0(typescript@5.9.3))(bufferutil@4.1.0)(utf-8-validate@6.0.6)': dependencies: @@ -18137,27 +18130,26 @@ snapshots: before-after-hook@4.0.0: {} - better-auth@1.6.22(@opentelemetry/api@1.9.1)(better-sqlite3@11.10.0)(drizzle-kit@0.31.9)(drizzle-orm@0.45.2(@electric-sql/pglite@0.3.0)(@libsql/client-wasm@0.15.5)(@opentelemetry/api@1.9.1)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(kysely@0.29.4)(pg@8.15.6)(postgres@3.4.8))(next@16.4.1(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@types/node@24.12.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.15.6)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.1.12(@edge-runtime/vm@3.2.0)(@opentelemetry/api@1.9.1)(@types/node@24.12.4)(jsdom@26.1.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))(vite@7.3.7(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.20.4)(yaml@2.9.0))): - dependencies: - '@better-auth/core': 1.6.22(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@3.25.76))(jose@6.2.4)(kysely@0.29.4)(nanostores@1.4.1) - '@better-auth/drizzle-adapter': 1.6.22(@better-auth/core@1.6.22(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@3.25.76))(jose@6.2.4)(kysely@0.29.4)(nanostores@1.4.1))(@better-auth/utils@0.4.2)(drizzle-orm@0.45.2(@electric-sql/pglite@0.3.0)(@libsql/client-wasm@0.15.5)(@opentelemetry/api@1.9.1)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(kysely@0.29.4)(pg@8.15.6)(postgres@3.4.8)) - '@better-auth/kysely-adapter': 1.6.22(@better-auth/core@1.6.22(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@3.25.76))(jose@6.2.4)(kysely@0.29.4)(nanostores@1.4.1))(@better-auth/utils@0.4.2)(kysely@0.29.4) - '@better-auth/memory-adapter': 1.6.22(@better-auth/core@1.6.22(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@3.25.76))(jose@6.2.4)(kysely@0.29.4)(nanostores@1.4.1))(@better-auth/utils@0.4.2) - '@better-auth/mongo-adapter': 1.6.22(@better-auth/core@1.6.22(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@3.25.76))(jose@6.2.4)(kysely@0.29.4)(nanostores@1.4.1))(@better-auth/utils@0.4.2) - '@better-auth/prisma-adapter': 1.6.22(@better-auth/core@1.6.22(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@3.25.76))(jose@6.2.4)(kysely@0.29.4)(nanostores@1.4.1))(@better-auth/utils@0.4.2) - '@better-auth/telemetry': 1.6.22(@better-auth/core@1.6.22(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@3.25.76))(jose@6.2.4)(kysely@0.29.4)(nanostores@1.4.1))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1) - '@better-auth/utils': 0.4.2 - '@better-fetch/fetch': 1.3.1 + better-auth@1.7.12(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.9)(drizzle-orm@0.45.2(@electric-sql/pglite@0.3.0)(@libsql/client-wasm@0.15.5)(@opentelemetry/api@1.9.1)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(kysely@0.29.4)(pg@8.15.6)(postgres@3.4.8))(next@16.4.1(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@types/node@24.12.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.15.6)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.1.12(@edge-runtime/vm@3.2.0)(@opentelemetry/api@1.9.1)(@types/node@24.12.4)(jsdom@26.1.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))(vite@7.3.7(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.20.4)(yaml@2.9.0))): + dependencies: + '@better-auth/core': 1.7.12(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.4(zod@3.25.76))(jose@6.2.4)(kysely@0.29.4)(nanostores@1.4.1) + '@better-auth/drizzle-adapter': 1.7.12(@better-auth/core@1.7.12(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.4(zod@3.25.76))(jose@6.2.4)(kysely@0.29.4)(nanostores@1.4.1))(@better-auth/utils@0.5.0)(drizzle-orm@0.45.2(@electric-sql/pglite@0.3.0)(@libsql/client-wasm@0.15.5)(@opentelemetry/api@1.9.1)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(kysely@0.29.4)(pg@8.15.6)(postgres@3.4.8)) + '@better-auth/kysely-adapter': 1.7.12(@better-auth/core@1.7.12(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.4(zod@3.25.76))(jose@6.2.4)(kysely@0.29.4)(nanostores@1.4.1))(@better-auth/utils@0.5.0)(kysely@0.29.4) + '@better-auth/memory-adapter': 1.7.12(@better-auth/core@1.7.12(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.4(zod@3.25.76))(jose@6.2.4)(kysely@0.29.4)(nanostores@1.4.1))(@better-auth/utils@0.5.0) + '@better-auth/mongo-adapter': 1.7.12(@better-auth/core@1.7.12(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.4(zod@3.25.76))(jose@6.2.4)(kysely@0.29.4)(nanostores@1.4.1))(@better-auth/utils@0.5.0) + '@better-auth/prisma-adapter': 1.7.12(@better-auth/core@1.7.12(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.4(zod@3.25.76))(jose@6.2.4)(kysely@0.29.4)(nanostores@1.4.1))(@better-auth/utils@0.5.0) + '@better-auth/telemetry': 1.7.12(@better-auth/core@1.7.12(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.4(zod@3.25.76))(jose@6.2.4)(kysely@0.29.4)(nanostores@1.4.1))(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2) + '@better-auth/utils': 0.5.0 + '@better-fetch/fetch': 1.3.2 '@noble/ciphers': 2.2.0 '@noble/hashes': 2.2.0 - better-call: 1.3.7(zod@4.4.3) + better-call: 1.4.4(zod@4.6.7) defu: 6.1.7 jose: 6.2.4 kysely: 0.29.4 nanostores: 1.4.1 - zod: 4.4.3 + zod: 4.6.7 optionalDependencies: - better-sqlite3: 11.10.0 drizzle-kit: 0.31.9 drizzle-orm: 0.45.2(@electric-sql/pglite@0.3.0)(@libsql/client-wasm@0.15.5)(@opentelemetry/api@1.9.1)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(kysely@0.29.4)(pg@8.15.6)(postgres@3.4.8) next: 16.4.1(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@types/node@24.12.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -18166,17 +18158,16 @@ snapshots: react-dom: 19.2.4(react@19.2.4) vitest: 4.1.12(@edge-runtime/vm@3.2.0)(@opentelemetry/api@1.9.1)(@types/node@24.12.4)(jsdom@26.1.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))(vite@7.3.7(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.20.4)(yaml@2.9.0)) transitivePeerDependencies: - - '@cloudflare/workers-types' - '@opentelemetry/api' - better-call@1.3.7(zod@4.4.3): + better-call@1.4.4(zod@4.6.7): dependencies: - '@better-auth/utils': 0.4.2 - '@better-fetch/fetch': 1.3.1 - rou3: 0.7.12 + '@better-auth/utils': 0.5.0 + '@better-fetch/fetch': 1.3.2 + rou3: 0.9.2 set-cookie-parser: 3.1.2 optionalDependencies: - zod: 4.4.3 + zod: 4.6.7 better-path-resolve@1.0.0: dependencies: @@ -22759,7 +22750,7 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.61.1 fsevents: 2.3.3 - rou3@0.7.12: {} + rou3@0.9.2: {} roughjs@4.6.6: dependencies: @@ -24296,7 +24287,7 @@ snapshots: zod@4.3.6: {} - zod@4.4.3: {} + zod@4.6.7: {} zustand@5.0.10(@types/react@19.2.10)(immer@11.1.4)(react@19.2.4)(use-sync-external-store@1.5.0(react@19.2.4)): optionalDependencies: