Merge branch 'canary' into chore/avoid-get-with-body

This commit is contained in:
Jonathan Samines
2025-11-06 08:34:02 -06:00
35 changed files with 936 additions and 752 deletions
+1 -1
View File
@@ -3,7 +3,7 @@ title: Cookies
description: Learn how cookies are used in Better Auth.
---
Cookies are used to store data such as session tokens, OAuth state, and more. All cookies are signed using the `secret` key provided in the auth options.
Cookies are used to store data such as session tokens, session data, OAuth state, and more. All cookies are signed using the `secret` key provided in the auth options or the `BETTER_AUTH_SECRET` environment variable.
### Cookie Prefix
+15 -15
View File
@@ -137,11 +137,11 @@ You can customize the customer creation process:
stripe({
// ... other options
createCustomerOnSignUp: true,
onCustomerCreate: async ({ customer, stripeCustomer, user }, request) => {
onCustomerCreate: async ({ stripeCustomer, user }, ctx) => {
// Do something with the newly created customer
console.log(`Customer ${customer.id} created for user ${user.id}`);
console.log(`Customer ${stripeCustomer.id} created for user ${user.id}`);
},
getCustomerCreateParams: async ({ user, session }, request) => {
getCustomerCreateParams: async (user, ctx) => {
// Customize the Stripe customer creation parameters
return {
metadata: {
@@ -570,11 +570,11 @@ You can configure trial periods for your plans:
// Called when a trial starts
await sendTrialStartEmail(subscription.referenceId);
},
onTrialEnd: async ({ subscription, user }, request) => {
onTrialEnd: async ({ subscription }, ctx) => {
// Called when a trial ends
await sendTrialEndEmail(user.email);
await sendTrialEndEmail(subscription.referenceId);
},
onTrialExpired: async (subscription) => {
onTrialExpired: async (subscription, ctx) => {
// Called when a trial expires without conversion
await sendTrialExpiredEmail(subscription.referenceId);
}
@@ -711,9 +711,9 @@ stripe({
**createCustomerOnSignUp**: `boolean` - Whether to automatically create a Stripe customer when a user signs up. Default: `false`.
**onCustomerCreate**: `(data: { customer: Customer, stripeCustomer: Stripe.Customer, user: User }, request?: Request) => Promise<void>` - A function called after a customer is created.
**onCustomerCreate**: `(data: { stripeCustomer: Stripe.Customer, user: User }, ctx: GenericEndpointContext) => Promise<void>` - A function called after a customer is created.
**getCustomerCreateParams**: `(data: { user: User, session: Session }, request?: Request) => Promise<{}>` - A function to customize the Stripe customer creation parameters.
**getCustomerCreateParams**: `(user: User, ctx: GenericEndpointContext) => Promise<{}>` - A function to customize the Stripe customer creation parameters.
**onEvent**: `(event: Stripe.Event) => Promise<void>` - A function called for any Stripe webhook event.
@@ -725,7 +725,7 @@ stripe({
**requireEmailVerification**: `boolean` - Whether to require email verification before allowing subscription upgrades. Default: `false`.
**authorizeReference**: `(data: { user: User, session: Session, referenceId: string, action: "upgrade-subscription" | "list-subscription" | "cancel-subscription" | "restore-subscription"}, request?: Request) => Promise<boolean>` - A function to authorize reference IDs.
**authorizeReference**: `(data: { user: User, session: Session, referenceId: string, action: "upgrade-subscription" | "list-subscription" | "cancel-subscription" | "restore-subscription"}, ctx: GenericEndpointContext) => Promise<boolean>` - A function to authorize reference IDs.
### Plan Configuration
@@ -748,8 +748,8 @@ Each plan can have the following properties:
**freeTrial**: Object containing trial configuration:
- **days**: `number` - Number of trial days.
- **onTrialStart**: `(subscription: Subscription) => Promise<void>` - Called when a trial starts.
- **onTrialEnd**: `(data: { subscription: Subscription, user: User }, request?: Request) => Promise<void>` - Called when a trial ends.
- **onTrialExpired**: `(subscription: Subscription) => Promise<void>` - Called when a trial expires without conversion.
- **onTrialEnd**: `(data: { subscription: Subscription }, ctx: GenericEndpointContext) => Promise<void>` - Called when a trial ends.
- **onTrialExpired**: `(subscription: Subscription, ctx: GenericEndpointContext) => Promise<void>` - Called when a trial expires without conversion.
## Advanced Usage
@@ -795,7 +795,7 @@ subscription: {
You can customize the Stripe Checkout session with additional parameters:
```ts title="auth.ts"
getCheckoutSessionParams: async ({ user, session, plan, subscription }, request) => {
getCheckoutSessionParams: async ({ user, session, plan, subscription }, ctx) => {
return {
params: {
allow_promotion_codes: true,
@@ -827,7 +827,7 @@ To collect tax IDs from the customer, set `tax_id_collection` to true:
```ts title="auth.ts"
subscription: {
// ... other options
getCheckoutSessionParams: async ({ user, session, plan, subscription }, request) => {
getCheckoutSessionParams: async ({ user, session, plan, subscription }, ctx) => {
return {
params: {
tax_id_collection: {
@@ -846,7 +846,7 @@ To enable automatic tax calculation using the customer's location, set `automati
```ts title="auth.ts"
subscription: {
// ... other options
getCheckoutSessionParams: async ({ user, session, plan, subscription }, request) => {
getCheckoutSessionParams: async ({ user, session, plan, subscription }, ctx) => {
return {
params: {
automatic_tax: {
@@ -903,4 +903,4 @@ For local development, you can use the Stripe CLI to forward webhooks to your lo
stripe listen --forward-to localhost:3000/api/auth/stripe/webhook
```
This will provide you with a webhook signing secret that you can use in your local environment.
This will provide you with a webhook signing secret that you can use in your local environment.
+151 -151
View File
@@ -1,6 +1,6 @@
{
"name": "better-auth",
"version": "1.4.0-beta.15",
"version": "1.4.0-beta.16",
"description": "The most comprehensive authentication framework for TypeScript.",
"type": "module",
"license": "MIT",
@@ -29,402 +29,402 @@
"prepare": "prisma generate --schema ./src/adapters/prisma-adapter/test/base.prisma",
"typecheck": "tsc --project tsconfig.json"
},
"main": "./dist/index.js",
"module": "./dist/index.js",
"main": "./dist/index.mjs",
"module": "./dist/index.mjs",
"exports": {
".": {
"better-auth-dev-source": "./src/index.ts",
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
"types": "./dist/index.d.mts",
"default": "./dist/index.mjs"
},
"./social-providers": {
"better-auth-dev-source": "./src/social-providers/index.ts",
"types": "./dist/social-providers/index.d.ts",
"default": "./dist/social-providers/index.js"
"types": "./dist/social-providers/index.d.mts",
"default": "./dist/social-providers/index.mjs"
},
"./client": {
"better-auth-dev-source": "./src/client/index.ts",
"types": "./dist/client/index.d.ts",
"default": "./dist/client/index.js"
"types": "./dist/client/index.d.mts",
"default": "./dist/client/index.mjs"
},
"./client/plugins": {
"better-auth-dev-source": "./src/client/plugins/index.ts",
"types": "./dist/client/plugins/index.d.ts",
"default": "./dist/client/plugins/index.js"
"types": "./dist/client/plugins/index.d.mts",
"default": "./dist/client/plugins/index.mjs"
},
"./types": {
"better-auth-dev-source": "./src/types/index.ts",
"types": "./dist/types/index.d.ts",
"default": "./dist/types/index.js"
"types": "./dist/types/index.d.mts",
"default": "./dist/types/index.mjs"
},
"./crypto": {
"better-auth-dev-source": "./src/crypto/index.ts",
"types": "./dist/crypto/index.d.ts",
"default": "./dist/crypto/index.js"
"types": "./dist/crypto/index.d.mts",
"default": "./dist/crypto/index.mjs"
},
"./cookies": {
"better-auth-dev-source": "./src/cookies/index.ts",
"types": "./dist/cookies/index.d.ts",
"default": "./dist/cookies/index.js"
"types": "./dist/cookies/index.d.mts",
"default": "./dist/cookies/index.mjs"
},
"./oauth2": {
"better-auth-dev-source": "./src/oauth2/index.ts",
"types": "./dist/oauth2/index.d.ts",
"default": "./dist/oauth2/index.js"
"types": "./dist/oauth2/index.d.mts",
"default": "./dist/oauth2/index.mjs"
},
"./react": {
"better-auth-dev-source": "./src/client/react/index.ts",
"types": "./dist/client/react/index.d.ts",
"default": "./dist/client/react/index.js"
"types": "./dist/client/react/index.d.mts",
"default": "./dist/client/react/index.mjs"
},
"./solid": {
"better-auth-dev-source": "./src/client/solid/index.ts",
"types": "./dist/client/solid/index.d.ts",
"default": "./dist/client/solid/index.js"
"types": "./dist/client/solid/index.d.mts",
"default": "./dist/client/solid/index.mjs"
},
"./lynx": {
"better-auth-dev-source": "./src/client/lynx/index.ts",
"types": "./dist/client/lynx/index.d.ts",
"default": "./dist/client/lynx/index.js"
"types": "./dist/client/lynx/index.d.mts",
"default": "./dist/client/lynx/index.mjs"
},
"./test": {
"better-auth-dev-source": "./src/test-utils/index.ts",
"types": "./dist/test-utils/index.d.ts",
"default": "./dist/test-utils/index.js"
"types": "./dist/test-utils/index.d.mts",
"default": "./dist/test-utils/index.mjs"
},
"./api": {
"better-auth-dev-source": "./src/api/index.ts",
"types": "./dist/api/index.d.ts",
"default": "./dist/api/index.js"
"types": "./dist/api/index.d.mts",
"default": "./dist/api/index.mjs"
},
"./db": {
"better-auth-dev-source": "./src/db/index.ts",
"types": "./dist/db/index.d.ts",
"default": "./dist/db/index.js"
"types": "./dist/db/index.d.mts",
"default": "./dist/db/index.mjs"
},
"./vue": {
"better-auth-dev-source": "./src/client/vue/index.ts",
"types": "./dist/client/vue/index.d.ts",
"default": "./dist/client/vue/index.js"
"types": "./dist/client/vue/index.d.mts",
"default": "./dist/client/vue/index.mjs"
},
"./plugins": {
"better-auth-dev-source": "./src/plugins/index.ts",
"types": "./dist/plugins/index.d.ts",
"default": "./dist/plugins/index.js"
"types": "./dist/plugins/index.d.mts",
"default": "./dist/plugins/index.mjs"
},
"./svelte-kit": {
"better-auth-dev-source": "./src/integrations/svelte-kit.ts",
"types": "./dist/integrations/svelte-kit.d.ts",
"default": "./dist/integrations/svelte-kit.js"
"types": "./dist/integrations/svelte-kit.d.mts",
"default": "./dist/integrations/svelte-kit.mjs"
},
"./solid-start": {
"better-auth-dev-source": "./src/integrations/solid-start.ts",
"types": "./dist/integrations/solid-start.d.ts",
"default": "./dist/integrations/solid-start.js"
"types": "./dist/integrations/solid-start.d.mts",
"default": "./dist/integrations/solid-start.mjs"
},
"./svelte": {
"better-auth-dev-source": "./src/client/svelte/index.ts",
"types": "./dist/client/svelte/index.d.ts",
"default": "./dist/client/svelte/index.js"
"types": "./dist/client/svelte/index.d.mts",
"default": "./dist/client/svelte/index.mjs"
},
"./next-js": {
"better-auth-dev-source": "./src/integrations/next-js.ts",
"types": "./dist/integrations/next-js.d.ts",
"default": "./dist/integrations/next-js.js"
"types": "./dist/integrations/next-js.d.mts",
"default": "./dist/integrations/next-js.mjs"
},
"./react-start": {
"better-auth-dev-source": "./src/integrations/react-start.ts",
"types": "./dist/integrations/react-start.d.ts",
"default": "./dist/integrations/react-start.js"
"types": "./dist/integrations/react-start.d.mts",
"default": "./dist/integrations/react-start.mjs"
},
"./node": {
"better-auth-dev-source": "./src/integrations/node.ts",
"types": "./dist/integrations/node.d.ts",
"default": "./dist/integrations/node.js"
"types": "./dist/integrations/node.d.mts",
"default": "./dist/integrations/node.mjs"
},
"./adapters/prisma": {
"better-auth-dev-source": "./src/adapters/prisma-adapter/index.ts",
"types": "./dist/adapters/prisma-adapter/index.d.ts",
"default": "./dist/adapters/prisma-adapter/index.js"
"types": "./dist/adapters/prisma-adapter/index.d.mts",
"default": "./dist/adapters/prisma-adapter/index.mjs"
},
"./adapters/drizzle": {
"better-auth-dev-source": "./src/adapters/drizzle-adapter/index.ts",
"types": "./dist/adapters/drizzle-adapter/index.d.ts",
"default": "./dist/adapters/drizzle-adapter/index.js"
"types": "./dist/adapters/drizzle-adapter/index.d.mts",
"default": "./dist/adapters/drizzle-adapter/index.mjs"
},
"./adapters/mongodb": {
"better-auth-dev-source": "./src/adapters/mongodb-adapter/index.ts",
"types": "./dist/adapters/mongodb-adapter/index.d.ts",
"default": "./dist/adapters/mongodb-adapter/index.js"
"types": "./dist/adapters/mongodb-adapter/index.d.mts",
"default": "./dist/adapters/mongodb-adapter/index.mjs"
},
"./adapters/memory": {
"better-auth-dev-source": "./src/adapters/memory-adapter/index.ts",
"types": "./dist/adapters/memory-adapter/index.d.ts",
"default": "./dist/adapters/memory-adapter/index.js"
"types": "./dist/adapters/memory-adapter/index.d.mts",
"default": "./dist/adapters/memory-adapter/index.mjs"
},
"./adapters/test": {
"better-auth-dev-source": "./src/adapters/test.ts",
"types": "./dist/adapters/test.d.ts",
"default": "./dist/adapters/test.js"
"types": "./dist/adapters/test.d.mts",
"default": "./dist/adapters/test.mjs"
},
"./adapters": {
"better-auth-dev-source": "./src/adapters/index.ts",
"types": "./dist/adapters/index.d.ts",
"default": "./dist/adapters/index.js"
"types": "./dist/adapters/index.d.mts",
"default": "./dist/adapters/index.mjs"
},
"./plugins/access": {
"better-auth-dev-source": "./src/plugins/access/index.ts",
"types": "./dist/plugins/access/index.d.ts",
"default": "./dist/plugins/access/index.js"
"types": "./dist/plugins/access/index.d.mts",
"default": "./dist/plugins/access/index.mjs"
},
"./plugins/admin": {
"better-auth-dev-source": "./src/plugins/admin/index.ts",
"types": "./dist/plugins/admin/index.d.ts",
"default": "./dist/plugins/admin/index.js"
"types": "./dist/plugins/admin/index.d.mts",
"default": "./dist/plugins/admin/index.mjs"
},
"./plugins/admin/access": {
"better-auth-dev-source": "./src/plugins/admin/access/index.ts",
"types": "./dist/plugins/admin/access/index.d.ts",
"default": "./dist/plugins/admin/access/index.js"
"types": "./dist/plugins/admin/access/index.d.mts",
"default": "./dist/plugins/admin/access/index.mjs"
},
"./plugins/anonymous": {
"better-auth-dev-source": "./src/plugins/anonymous/index.ts",
"types": "./dist/plugins/anonymous/index.d.ts",
"default": "./dist/plugins/anonymous/index.js"
"types": "./dist/plugins/anonymous/index.d.mts",
"default": "./dist/plugins/anonymous/index.mjs"
},
"./plugins/bearer": {
"better-auth-dev-source": "./src/plugins/bearer/index.ts",
"types": "./dist/plugins/bearer/index.d.ts",
"default": "./dist/plugins/bearer/index.js"
"types": "./dist/plugins/bearer/index.d.mts",
"default": "./dist/plugins/bearer/index.mjs"
},
"./plugins/custom-session": {
"better-auth-dev-source": "./src/plugins/custom-session/index.ts",
"types": "./dist/plugins/custom-session/index.d.ts",
"default": "./dist/plugins/custom-session/index.js"
"types": "./dist/plugins/custom-session/index.d.mts",
"default": "./dist/plugins/custom-session/index.mjs"
},
"./plugins/email-otp": {
"better-auth-dev-source": "./src/plugins/email-otp/index.ts",
"types": "./dist/plugins/email-otp/index.d.ts",
"default": "./dist/plugins/email-otp/index.js"
"types": "./dist/plugins/email-otp/index.d.mts",
"default": "./dist/plugins/email-otp/index.mjs"
},
"./plugins/generic-oauth": {
"better-auth-dev-source": "./src/plugins/generic-oauth/index.ts",
"types": "./dist/plugins/generic-oauth/index.d.ts",
"default": "./dist/plugins/generic-oauth/index.js"
"types": "./dist/plugins/generic-oauth/index.d.mts",
"default": "./dist/plugins/generic-oauth/index.mjs"
},
"./plugins/jwt": {
"better-auth-dev-source": "./src/plugins/jwt/index.ts",
"types": "./dist/plugins/jwt/index.d.ts",
"default": "./dist/plugins/jwt/index.js"
"types": "./dist/plugins/jwt/index.d.mts",
"default": "./dist/plugins/jwt/index.mjs"
},
"./plugins/haveibeenpwned": {
"better-auth-dev-source": "./src/plugins/haveibeenpwned/index.ts",
"types": "./dist/plugins/haveibeenpwned/index.d.ts",
"default": "./dist/plugins/haveibeenpwned/index.js"
"types": "./dist/plugins/haveibeenpwned/index.d.mts",
"default": "./dist/plugins/haveibeenpwned/index.mjs"
},
"./plugins/oidc-provider": {
"better-auth-dev-source": "./src/plugins/oidc-provider/index.ts",
"types": "./dist/plugins/oidc-provider/index.d.ts",
"default": "./dist/plugins/oidc-provider/index.js"
"types": "./dist/plugins/oidc-provider/index.d.mts",
"default": "./dist/plugins/oidc-provider/index.mjs"
},
"./plugins/magic-link": {
"better-auth-dev-source": "./src/plugins/magic-link/index.ts",
"types": "./dist/plugins/magic-link/index.d.ts",
"default": "./dist/plugins/magic-link/index.js"
"types": "./dist/plugins/magic-link/index.d.mts",
"default": "./dist/plugins/magic-link/index.mjs"
},
"./plugins/multi-session": {
"better-auth-dev-source": "./src/plugins/multi-session/index.ts",
"types": "./dist/plugins/multi-session/index.d.ts",
"default": "./dist/plugins/multi-session/index.js"
"types": "./dist/plugins/multi-session/index.d.mts",
"default": "./dist/plugins/multi-session/index.mjs"
},
"./plugins/oauth-proxy": {
"better-auth-dev-source": "./src/plugins/oauth-proxy/index.ts",
"types": "./dist/plugins/oauth-proxy/index.d.ts",
"default": "./dist/plugins/oauth-proxy/index.js"
"types": "./dist/plugins/oauth-proxy/index.d.mts",
"default": "./dist/plugins/oauth-proxy/index.mjs"
},
"./plugins/organization": {
"better-auth-dev-source": "./src/plugins/organization/index.ts",
"types": "./dist/plugins/organization/index.d.ts",
"default": "./dist/plugins/organization/index.js"
"types": "./dist/plugins/organization/index.d.mts",
"default": "./dist/plugins/organization/index.mjs"
},
"./plugins/organization/access": {
"better-auth-dev-source": "./src/plugins/organization/access/index.ts",
"types": "./dist/plugins/organization/access/index.d.ts",
"default": "./dist/plugins/organization/access/index.js"
"types": "./dist/plugins/organization/access/index.d.mts",
"default": "./dist/plugins/organization/access/index.mjs"
},
"./plugins/one-time-token": {
"better-auth-dev-source": "./src/plugins/one-time-token/index.ts",
"types": "./dist/plugins/one-time-token/index.d.ts",
"default": "./dist/plugins/one-time-token/index.js"
"types": "./dist/plugins/one-time-token/index.d.mts",
"default": "./dist/plugins/one-time-token/index.mjs"
},
"./plugins/phone-number": {
"better-auth-dev-source": "./src/plugins/phone-number/index.ts",
"types": "./dist/plugins/phone-number/index.d.ts",
"default": "./dist/plugins/phone-number/index.js"
"types": "./dist/plugins/phone-number/index.d.mts",
"default": "./dist/plugins/phone-number/index.mjs"
},
"./plugins/two-factor": {
"better-auth-dev-source": "./src/plugins/two-factor/index.ts",
"types": "./dist/plugins/two-factor/index.d.ts",
"default": "./dist/plugins/two-factor/index.js"
"types": "./dist/plugins/two-factor/index.d.mts",
"default": "./dist/plugins/two-factor/index.mjs"
},
"./plugins/username": {
"better-auth-dev-source": "./src/plugins/username/index.ts",
"types": "./dist/plugins/username/index.d.ts",
"default": "./dist/plugins/username/index.js"
"types": "./dist/plugins/username/index.d.mts",
"default": "./dist/plugins/username/index.mjs"
},
"./plugins/siwe": {
"better-auth-dev-source": "./src/plugins/siwe/index.ts",
"types": "./dist/plugins/siwe/index.d.ts",
"default": "./dist/plugins/siwe/index.js"
"types": "./dist/plugins/siwe/index.d.mts",
"default": "./dist/plugins/siwe/index.mjs"
},
"./plugins/device-authorization": {
"better-auth-dev-source": "./src/plugins/device-authorization/index.ts",
"types": "./dist/plugins/device-authorization/index.d.ts",
"default": "./dist/plugins/device-authorization/index.js"
"types": "./dist/plugins/device-authorization/index.d.mts",
"default": "./dist/plugins/device-authorization/index.mjs"
}
},
"typesVersions": {
"*": {
"*": [
"./dist/index.d.ts"
"./dist/index.d.mts"
],
"node": [
"./dist/integrations/node.d.ts"
"./dist/integrations/node.d.mts"
],
"react": [
"./dist/client/react/index.d.ts"
"./dist/client/react/index.d.mts"
],
"vue": [
"./dist/client/vue/index.d.ts"
"./dist/client/vue/index.d.mts"
],
"svelte": [
"./dist/client/svelte/index.d.ts"
"./dist/client/svelte/index.d.mts"
],
"social-providers": [
"./dist/social-providers/index.d.ts"
"./dist/social-providers/index.d.mts"
],
"client": [
"./dist/client/index.d.ts"
"./dist/client/index.d.mts"
],
"client/plugins": [
"./dist/client/plugins/index.d.ts"
"./dist/client/plugins/index.d.mts"
],
"types": [
"./dist/types/index.d.ts"
"./dist/types/index.d.mts"
],
"crypto": [
"./dist/crypto/index.d.ts"
"./dist/crypto/index.d.mts"
],
"cookies": [
"./dist/cookies/index.d.ts"
"./dist/cookies/index.d.mts"
],
"oauth2": [
"./dist/oauth2/index.d.ts"
"./dist/oauth2/index.d.mts"
],
"solid": [
"./dist/client/solid/index.d.ts"
"./dist/client/solid/index.d.mts"
],
"lynx": [
"./dist/client/lynx/index.d.ts"
"./dist/client/lynx/index.d.mts"
],
"api": [
"./dist/api/index.d.ts"
"./dist/api/index.d.mts"
],
"db": [
"./dist/db/index.d.ts"
"./dist/db/index.d.mts"
],
"svelte-kit": [
"./dist/integrations/svelte-kit.d.ts"
"./dist/integrations/svelte-kit.d.mts"
],
"solid-start": [
"./dist/integrations/solid-start.d.ts"
"./dist/integrations/solid-start.d.mts"
],
"next-js": [
"./dist/integrations/next-js.d.ts"
"./dist/integrations/next-js.d.mts"
],
"react-start": [
"./dist/integrations/react-start.d.ts"
"./dist/integrations/react-start.d.mts"
],
"adapters": [
"./dist/adapters/index.d.ts"
"./dist/adapters/index.d.mts"
],
"adapters/prisma": [
"./dist/adapters/prisma-adapter/index.d.ts"
"./dist/adapters/prisma-adapter/index.d.mts"
],
"adapters/drizzle": [
"./dist/adapters/drizzle-adapter/index.d.ts"
"./dist/adapters/drizzle-adapter/index.d.mts"
],
"adapters/mongodb": [
"./dist/adapters/mongodb-adapter/index.d.ts"
"./dist/adapters/mongodb-adapter/index.d.mts"
],
"adapters/memory": [
"./dist/adapters/memory-adapter/index.d.ts"
"./dist/adapters/memory-adapter/index.d.mts"
],
"plugins": [
"./dist/plugins/index.d.ts"
"./dist/plugins/index.d.mts"
],
"plugins/access": [
"./dist/plugins/access/index.d.ts"
"./dist/plugins/access/index.d.mts"
],
"plugins/admin": [
"./dist/plugins/admin/index.d.ts"
"./dist/plugins/admin/index.d.mts"
],
"plugins/admin/access": [
"./dist/plugins/admin/access/index.d.ts"
"./dist/plugins/admin/access/index.d.mts"
],
"plugins/anonymous": [
"./dist/plugins/anonymous/index.d.ts"
"./dist/plugins/anonymous/index.d.mts"
],
"plugins/bearer": [
"./dist/plugins/bearer/index.d.ts"
"./dist/plugins/bearer/index.d.mts"
],
"plugins/custom-session": [
"./dist/plugins/custom-session/index.d.ts"
"./dist/plugins/custom-session/index.d.mts"
],
"plugins/email-otp": [
"./dist/plugins/email-otp/index.d.ts"
"./dist/plugins/email-otp/index.d.mts"
],
"plugins/generic-oauth": [
"./dist/plugins/generic-oauth/index.d.ts"
"./dist/plugins/generic-oauth/index.d.mts"
],
"plugins/haveibeenpwned": [
"./dist/plugins/haveibeenpwned/index.d.ts"
"./dist/plugins/haveibeenpwned/index.d.mts"
],
"plugins/oauth-proxy": [
"./dist/plugins/oauth-proxy/index.d.ts"
"./dist/plugins/oauth-proxy/index.d.mts"
],
"plugins/one-time-token": [
"./dist/plugins/one-time-token/index.d.ts"
"./dist/plugins/one-time-token/index.d.mts"
],
"plugins/oidc-provider": [
"./dist/plugins/oidc-provider/index.d.ts"
"./dist/plugins/oidc-provider/index.d.mts"
],
"plugins/jwt": [
"./dist/plugins/jwt/index.d.ts"
"./dist/plugins/jwt/index.d.mts"
],
"plugins/magic-link": [
"./dist/plugins/magic-link/index.d.ts"
"./dist/plugins/magic-link/index.d.mts"
],
"plugins/organization": [
"./dist/plugins/organization/index.d.ts"
"./dist/plugins/organization/index.d.mts"
],
"plugins/organization/access": [
"./dist/plugins/organization/access/index.d.ts"
"./dist/plugins/organization/access/index.d.mts"
],
"plugins/phone-number": [
"./dist/plugins/phone-number/index.d.ts"
"./dist/plugins/phone-number/index.d.mts"
],
"plugins/two-factor": [
"./dist/plugins/two-factor/index.d.ts"
"./dist/plugins/two-factor/index.d.mts"
],
"plugins/username": [
"./dist/plugins/username/index.d.ts"
"./dist/plugins/username/index.d.mts"
],
"plugins/siwe": [
"./dist/plugins/siwe/index.d.ts"
"./dist/plugins/siwe/index.d.mts"
],
"plugins/device-authorization": [
"./dist/plugins/device-authorization/index.d.ts"
"./dist/plugins/device-authorization/index.d.mts"
]
}
},
@@ -514,7 +514,7 @@
"tarn": "^3.0.2",
"tedious": "^18.6.1",
"tsdown": "catalog:",
"type-fest": "^4.41.0",
"type-fest": "^5.2.0",
"typescript": "catalog:",
"vue": "^3.5.18"
},
@@ -333,7 +333,7 @@ export const createAdapterFactory =
});
}
for (const field in fields) {
const value = data[field];
let value = data[field];
const fieldAttributes = fields[field];
let newFieldName: string =
@@ -347,6 +347,26 @@ export const createAdapterFactory =
) {
continue;
}
// In some endpoints (like signUpEmail) where there isn't proper Zod validation,
// we might recieve a date as a string (this is because of the client converting the Date to a string
// when sending to the server). Because of this, we'll convert the string to a Date.
if (
fieldAttributes &&
fieldAttributes.type === "date" &&
!(value instanceof Date) &&
typeof value === "string"
) {
try {
value = new Date(value);
} catch {
logger.error("[Adapter Factory] Failed to convert string to date", {
value,
field,
});
}
}
// If the value is undefined, but the fieldAttr provides a `defaultValue`, then we'll use that.
let newValue = withApplyDefault(value, fieldAttributes!, action);
@@ -1,4 +1,6 @@
import { expect } from "vitest";
import { setCookieToHeader } from "../../cookies";
import type { Session, User } from "../../types";
import { createTestSuite } from "../create-test-suite";
/**
@@ -6,21 +8,30 @@ import { createTestSuite } from "../create-test-suite";
*/
export const authFlowTestSuite = createTestSuite(
"auth-flow",
{},
{
defaultBetterAuthOptions: {
emailAndPassword: {
enabled: true,
password: {
hash: async (password) => password,
async verify(data) {
return data.hash === data.password;
},
},
},
},
},
(
{ generate, getAuth, modifyBetterAuthOptions, tryCatch },
{
generate,
getAuth,
modifyBetterAuthOptions,
tryCatch,
getBetterAuthOptions,
},
debug?: { showDB?: () => Promise<void> } | undefined,
) => ({
"should successfully sign up": async () => {
await modifyBetterAuthOptions(
{
emailAndPassword: {
enabled: true,
password: { hash: async (password) => password },
},
},
false,
);
const auth = await getAuth();
const user = await generate("user");
const start = Date.now();
@@ -43,20 +54,6 @@ export const authFlowTestSuite = createTestSuite(
expect(result.user.updatedAt).toBeDefined();
},
"should successfully sign in": async () => {
await modifyBetterAuthOptions(
{
emailAndPassword: {
enabled: true,
password: {
hash: async (password) => password,
async verify(data) {
return data.hash === data.password;
},
},
},
},
false,
);
const auth = await getAuth();
const user = await generate("user");
const password = crypto.randomUUID();
@@ -78,51 +75,37 @@ export const authFlowTestSuite = createTestSuite(
expect(result.user.id).toBe(signUpResult.user.id);
},
"should successfully get session": async () => {
await modifyBetterAuthOptions(
{
emailAndPassword: {
enabled: true,
password: { hash: async (password) => password },
},
},
false,
);
const auth = await getAuth();
const user = await generate("user");
const password = crypto.randomUUID();
const { headers, response: signUpResult } = await auth.api.signUpEmail({
const response = await auth.api.signUpEmail({
body: {
email: user.email,
password: password,
name: user.name,
image: user.image || "",
},
returnHeaders: true,
asResponse: true,
});
// Convert set-cookie header to cookie header for getSession call
const modifiedHeaders = new Headers(headers);
if (headers.has("set-cookie")) {
modifiedHeaders.set("cookie", headers.getSetCookie().join("; "));
modifiedHeaders.delete("set-cookie");
}
const headers = new Headers();
setCookieToHeader(headers)({ response });
const start = Date.now();
const result = await auth.api.getSession({
headers: modifiedHeaders,
headers,
});
const end = Date.now();
console.log(`getSession took ${end - start}ms`);
const signUpResult = (await response.json()) as {
user: User;
session: Session;
};
signUpResult.user.createdAt = new Date(signUpResult.user.createdAt);
signUpResult.user.updatedAt = new Date(signUpResult.user.updatedAt);
expect(result?.user).toBeDefined();
expect(result?.user).toStrictEqual(signUpResult.user);
expect(result?.session).toBeDefined();
},
"should not sign in with invalid email": async () => {
await modifyBetterAuthOptions(
{ emailAndPassword: { enabled: true } },
false,
);
const auth = await getAuth();
const user = await generate("user");
const { data, error } = await tryCatch(
@@ -136,10 +119,6 @@ export const authFlowTestSuite = createTestSuite(
"should store and retrieve timestamps correctly across timezones":
async () => {
using _ = recoverProcessTZ();
await modifyBetterAuthOptions(
{ emailAndPassword: { enabled: true } },
false,
);
const auth = await getAuth();
const user = await generate("user");
const password = crypto.randomUUID();
@@ -160,6 +139,32 @@ export const authFlowTestSuite = createTestSuite(
userSignIn.user.createdAt.toISOString(),
);
},
"should sign up with additional fields": async () => {
await modifyBetterAuthOptions(
{ user: { additionalFields: { dateField: { type: "date" } } } },
true,
);
const auth = await getAuth();
const user = await generate("user");
const dateField = new Date();
const response = await auth.api.signUpEmail({
body: {
email: user.email,
name: user.name,
password: crypto.randomUUID(),
//@ts-expect-error - we are testing with additional fields
dateField: dateField.toISOString(), // using iso string to simulate client to server communication (this should be converted back to Date)
},
asResponse: true,
});
const headers = new Headers();
setCookieToHeader(headers)({ response });
const result = await auth.api.getSession({
headers,
});
//@ts-expect-error - we are testing with additional fields
expect(result?.user.dateField).toStrictEqual(dateField);
},
}),
);
@@ -113,6 +113,74 @@ export const getNormalTestSuiteTests = ({
});
expect(nullableReference).toBeNull();
},
"create - should apply default values to fields": async () => {
await modifyBetterAuthOptions(
{
user: {
additionalFields: {
testField: {
type: "string",
defaultValue: "test-value",
},
cbDefaultValueField: {
type: "string",
defaultValue: () => {
return "advanced-test-value";
},
},
},
},
plugins: [
{
id: "default-fields-test",
schema: {
testModel: {
fields: {
testField: {
type: "string",
defaultValue: "test-value",
},
cbDefaultValueField: {
type: "string",
defaultValue: () => {
return "advanced-test-value";
},
},
},
},
},
},
],
},
true,
);
const result = await adapter.create<{
testField?: string;
id: string;
cbDefaultValueField?: string;
}>({
model: "testModel",
data: {},
});
expect(result.id).toBeDefined();
expect(result.id).toBeTypeOf("string");
expect(result.testField).toBe("test-value");
expect(result.cbDefaultValueField).toBe("advanced-test-value");
const userResult = await adapter.create<
User & { testField?: string; cbDefaultValueField?: string }
>({
model: "user",
data: {
...(await generate("user")),
},
forceAllowId: true,
});
expect(userResult).toBeDefined();
expect(userResult?.testField).toBe("test-value");
expect(userResult?.cbDefaultValueField).toBe("advanced-test-value");
},
"findOne - should find a model": async () => {
const [user] = await insertRandom("user");
const result = await adapter.findOne<User>({
@@ -121,6 +189,103 @@ export const getNormalTestSuiteTests = ({
});
expect(result).toEqual(user);
},
"findOne - should not apply defaultValue if value not found": async () => {
await modifyBetterAuthOptions(
{
user: {
additionalFields: {
testField: {
type: "string",
required: false,
defaultValue: "test-value",
},
cbDefaultValueField: {
type: "string",
required: false,
defaultValue: () => {
return "advanced-test-value";
},
},
},
},
plugins: [
{
id: "default-fields-test",
schema: {
testModel: {
fields: {
testField: {
type: "string",
required: false,
defaultValue: "test-value",
},
cbDefaultValueField: {
type: "string",
required: false,
defaultValue: () => {
return "advanced-test-value";
},
},
},
},
},
},
],
},
true,
);
const first = await adapter.create<{
testField?: string | null;
id: string;
cbDefaultValueField?: string | null;
}>({
model: "testModel",
data: {
testField: null,
cbDefaultValueField: null,
},
});
const second = await adapter.create<
User & {
testField?: string | null;
cbDefaultValueField?: string | null;
}
>({
model: "user",
data: {
...(await generate("user")),
testField: null,
cbDefaultValueField: null,
},
forceAllowId: true,
});
const result = await adapter.findOne<{
testField?: string;
id: string;
cbDefaultValueField?: string;
}>({
model: "testModel",
where: [{ field: "id", value: first.id }],
});
expect(result).not.toBeNull();
expect(result?.testField).toBeNull();
expect(result?.cbDefaultValueField).toBeNull();
const resultTwo = await adapter.findMany<
User & {
testField?: string | null;
cbDefaultValueField?: string | null;
}
>({
model: "user",
where: [{ field: "id", value: second.id }],
});
expect(resultTwo).not.toBeNull();
expect(resultTwo.length).toBe(1);
expect(resultTwo[0]?.testField).toBeNull();
expect(resultTwo[0]?.cbDefaultValueField).toBeNull();
},
"findOne - should find a model using a reference field": async () => {
const [user, session] = await insertRandom("session");
const result = await adapter.findOne<User>({
+8 -5
View File
@@ -15,12 +15,15 @@ export function withApplyDefault(
}
return value;
}
if (value === undefined || value === null) {
if (field.defaultValue !== undefined) {
if (typeof field.defaultValue === "function") {
return field.defaultValue();
if (action === "create") {
// we do not want to apply default values if the value is null & not required
if (value === undefined || (field.required === true && value === null)) {
if (field.defaultValue !== undefined) {
if (typeof field.defaultValue === "function") {
return field.defaultValue();
}
return field.defaultValue;
}
return field.defaultValue;
}
}
return value;
+3 -1
View File
@@ -1,12 +1,14 @@
//#region Re-exports necessaries from core module
export type { StandardSchemaV1 } from "@better-auth/core";
export * from "@better-auth/core";
//#endregion
export { getCurrentAdapter } from "@better-auth/core/context";
export * from "@better-auth/core/env";
export * from "@better-auth/core/error";
export * from "@better-auth/core/oauth2";
export * from "@better-auth/core/utils";
//#endregion
export type * from "better-call";
export * from "./auth";
export * from "./types";
export * from "./utils";
@@ -91,14 +91,14 @@ export async function authorizeMCPOAuth(
}
return {
...res,
redirectURLs: res.redirectURLs.split(","),
redirectUrls: res.redirectUrls.split(","),
metadata: res.metadata ? JSON.parse(res.metadata) : {},
} as Client;
});
if (!client) {
throw ctx.redirect(`${ctx.context.baseURL}/error?error=invalid_client`);
}
const redirectURI = client.redirectURLs.find(
const redirectURI = client.redirectUrls.find(
(url) => url === ctx.query.redirect_uri,
);
@@ -453,7 +453,7 @@ export const mcp = (options: MCPOptions) => {
}
return {
...res,
redirectURLs: res.redirectURLs.split(","),
redirectUrls: res.redirectUrls.split(","),
metadata: res.metadata ? JSON.parse(res.metadata) : {},
} as Client;
});
@@ -722,7 +722,7 @@ export const mcp = (options: MCPOptions) => {
description:
"Secret key for the client. Not included for public clients.",
},
redirectURLs: {
redirectUrls: {
type: "array",
items: { type: "string", format: "uri" },
description: "List of allowed redirect URLs",
@@ -763,7 +763,7 @@ export const mcp = (options: MCPOptions) => {
required: [
"name",
"clientId",
"redirectURLs",
"redirectUrls",
"type",
"authenticationScheme",
"disabled",
@@ -844,7 +844,7 @@ export const mcp = (options: MCPOptions) => {
metadata: body.metadata ? JSON.stringify(body.metadata) : null,
clientId: clientId,
clientSecret: finalClientSecret,
redirectURLs: body.redirect_uris.join(","),
redirectUrls: body.redirect_uris.join(","),
type: clientType,
authenticationScheme:
body.token_endpoint_auth_method || "client_secret_basic",
@@ -106,7 +106,7 @@ describe("mcp", async () => {
publicClient = {
clientId: (createdClient.data as any).client_id,
clientSecret: "", // Public clients don't have secrets, but our type expects a string
redirectURLs: (createdClient.data as any).redirect_uris,
redirectUrls: (createdClient.data as any).redirect_uris,
metadata: {},
icon: (createdClient.data as any).logo_uri || "",
type: "public",
@@ -152,7 +152,7 @@ describe("mcp", async () => {
confidentialClient = {
clientId: (createdClient.data as any).client_id,
clientSecret: (createdClient.data as any).client_secret,
redirectURLs: (createdClient.data as any).redirect_uris,
redirectUrls: (createdClient.data as any).redirect_uris,
metadata: {},
icon: (createdClient.data as any).logo_uri || "",
type: "web",
@@ -243,7 +243,7 @@ describe("mcp", async () => {
grant_type: "authorization_code",
client_id: publicClient.clientId,
code: authCode,
redirect_uri: publicClient.redirectURLs[0],
redirect_uri: publicClient.redirectUrls[0],
// Missing code_verifier for public client
},
});
@@ -435,7 +435,7 @@ describe("mcp", async () => {
const userinfoClient = {
clientId: (createdClient.data as any).client_id,
clientSecret: (createdClient.data as any).client_secret,
redirectURLs: (createdClient.data as any).redirect_uris,
redirectUrls: (createdClient.data as any).redirect_uris,
};
// Set up OAuth flow
@@ -110,7 +110,7 @@ export async function authorize(
);
throw ctx.redirect(errorURL);
}
const redirectURI = client.redirectURLs.find(
const redirectURI = client.redirectUrls.find(
(url) => url === ctx.query.redirect_uri,
);
@@ -62,7 +62,7 @@ export async function getClient(
}
return {
...res,
redirectURLs: (res.redirectURLs ?? "").split(","),
redirectUrls: (res.redirectUrls ?? "").split(","),
metadata: res.metadata ? JSON.parse(res.metadata) : {},
} as Client;
});
@@ -1369,7 +1369,7 @@ export const oidcProvider = (options: OIDCOptions) => {
metadata: body.metadata ? JSON.stringify(body.metadata) : null,
clientId: clientId,
clientSecret: storedClientSecret,
redirectURLs: body.redirect_uris.join(","),
redirectUrls: body.redirect_uris.join(","),
type: "web",
authenticationScheme:
body.token_endpoint_auth_method || "client_secret_basic",
@@ -115,7 +115,7 @@ describe("oidc", async () => {
let application: Client = {
clientId: "test-client-id",
clientSecret: "test-client-secret-oidc",
redirectURLs: ["http://localhost:3000/api/auth/oauth2/callback/test"],
redirectUrls: ["http://localhost:3000/api/auth/oauth2/callback/test"],
metadata: {},
icon: "",
type: "web",
@@ -126,7 +126,7 @@ describe("oidc", async () => {
it("should create oidc client", async ({ expect }) => {
const createdClient = await serverClient.oauth2.register({
client_name: application.name,
redirect_uris: application.redirectURLs,
redirect_uris: application.redirectUrls,
logo_uri: application.icon,
});
expect(createdClient.data).toMatchObject({
@@ -145,7 +145,7 @@ describe("oidc", async () => {
application = {
clientId: createdClient.data.client_id,
clientSecret: createdClient.data.client_secret,
redirectURLs: createdClient.data.redirect_uris,
redirectUrls: createdClient.data.redirect_uris,
metadata: {},
icon: createdClient.data.logo_uri || "",
type: "web",
@@ -480,7 +480,7 @@ describe("oidc storage", async () => {
let application: Client = {
clientId: "test-client-id",
clientSecret: "test-client-secret-oidc",
redirectURLs: ["http://localhost:3000/api/auth/oauth2/callback/test"],
redirectUrls: ["http://localhost:3000/api/auth/oauth2/callback/test"],
metadata: {},
icon: "",
type: "web",
@@ -489,7 +489,7 @@ describe("oidc storage", async () => {
};
const createdClient = await serverClient.oauth2.register({
client_name: application.name,
redirect_uris: application.redirectURLs,
redirect_uris: application.redirectUrls,
logo_uri: application.icon,
});
expect(createdClient.data).toMatchObject({
@@ -508,7 +508,7 @@ describe("oidc storage", async () => {
application = {
clientId: createdClient.data.client_id,
clientSecret: createdClient.data.client_secret,
redirectURLs: createdClient.data.redirect_uris,
redirectUrls: createdClient.data.redirect_uris,
metadata: {},
icon: createdClient.data.logo_uri || "",
type: "web",
@@ -858,7 +858,7 @@ describe("oidc-jwt", async () => {
let application: Client = {
clientId: "test-client-id",
clientSecret: "test-client-secret-oidc",
redirectURLs: ["http://localhost:3000/api/auth/oauth2/callback/test"],
redirectUrls: ["http://localhost:3000/api/auth/oauth2/callback/test"],
metadata: {},
icon: "",
type: "web",
@@ -867,7 +867,7 @@ describe("oidc-jwt", async () => {
};
const createdClient = await serverClient.oauth2.register({
client_name: application.name,
redirect_uris: application.redirectURLs,
redirect_uris: application.redirectUrls,
logo_uri: application.icon,
});
expect(createdClient.data).toMatchObject({
@@ -886,7 +886,7 @@ describe("oidc-jwt", async () => {
application = {
clientId: createdClient.data.client_id,
clientSecret: createdClient.data.client_secret,
redirectURLs: createdClient.data.redirect_uris,
redirectUrls: createdClient.data.redirect_uris,
metadata: {},
icon: createdClient.data.logo_uri || "",
type: "web",
@@ -23,7 +23,7 @@ export const schema = {
type: "string",
required: false,
},
redirectURLs: {
redirectUrls: {
type: "string",
},
type: {
@@ -313,7 +313,7 @@ export interface Client {
*
* For example, `https://example.com/auth/callback`
*/
redirectURLs: string[];
redirectUrls: string[];
/**
* The name of the client.
*/
+2 -257
View File
@@ -1,257 +1,2 @@
import type { BetterAuthClientOptions } from "@better-auth/core";
import type { SuccessContext } from "@better-fetch/fetch";
import { afterAll } from "vitest";
import { mongodbAdapter } from "../adapters/mongodb-adapter";
import { betterAuth } from "../auth";
import { createAuthClient } from "../client";
import { parseSetCookieHeader, setCookieToHeader } from "../cookies";
import { getAdapter, getMigrations } from "../db";
import { bearer } from "../plugins";
import type { BetterAuthOptions, Session, User } from "../types";
import { getBaseURL } from "../utils/url";
export async function getTestInstanceMemory<
O extends Partial<BetterAuthOptions>,
C extends BetterAuthClientOptions,
>(
options?: O | undefined,
config?:
| {
clientOptions?: C;
port?: number;
disableTestUser?: boolean;
testUser?: Partial<User>;
testWith?: "sqlite" | "postgres" | "mongodb" | "mysql" | "memory";
}
| undefined,
) {
const testWith = config?.testWith || "memory";
async function getPostgres() {
const { Kysely, PostgresDialect } = await import("kysely");
const { Pool } = await import("pg");
return new Kysely({
dialect: new PostgresDialect({
pool: new Pool({
connectionString:
"postgres://user:password@localhost:5432/better_auth",
}),
}),
});
}
async function getMysql() {
const { Kysely, MysqlDialect } = await import("kysely");
const { createPool } = await import("mysql2/promise");
return new Kysely({
dialect: new MysqlDialect(
createPool("mysql://user:password@localhost:3306/better_auth"),
),
});
}
async function mongodbClient() {
const { MongoClient } = await import("mongodb");
const dbClient = async (connectionString: string, dbName: string) => {
const client = new MongoClient(connectionString);
await client.connect();
const db = client.db(dbName);
return db;
};
const db = await dbClient("mongodb://127.0.0.1:27017", "better-auth");
return db;
}
const opts = {
socialProviders: {
github: {
clientId: "test",
clientSecret: "test",
},
google: {
clientId: "test",
clientSecret: "test",
},
},
secret: "better-auth.secret",
database:
testWith === "postgres"
? { db: await getPostgres(), type: "postgres" }
: testWith === "mongodb"
? mongodbAdapter(await mongodbClient())
: testWith === "mysql"
? { db: await getMysql(), type: "mysql" }
: undefined,
emailAndPassword: {
enabled: true,
},
rateLimit: {
enabled: false,
},
advanced: {
cookies: {},
},
} satisfies BetterAuthOptions;
const auth = betterAuth({
baseURL: "http://localhost:" + (config?.port || 3000),
...opts,
...options,
advanced: {
disableCSRFCheck: true,
...options?.advanced,
},
plugins: [bearer(), ...(options?.plugins || [])],
} as unknown as O extends undefined ? typeof opts : O & typeof opts);
const testUser = {
email: "test@test.com",
password: "test123456",
name: "test user",
...config?.testUser,
};
async function createTestUser() {
if (config?.disableTestUser) {
return;
}
//@ts-expect-error
const res = await auth.api.signUpEmail({
body: testUser,
});
}
if (testWith !== "mongodb" && testWith !== "memory") {
const { runMigrations } = await getMigrations({
...auth.options,
database: opts.database,
});
await runMigrations();
}
await createTestUser();
afterAll(async () => {
if (testWith === "mongodb") {
const db = await mongodbClient();
await db.dropDatabase();
return;
}
if (testWith === "postgres") {
const { sql } = await import("kysely");
const postgres = await getPostgres();
await sql`DROP SCHEMA public CASCADE; CREATE SCHEMA public;`.execute(
postgres,
);
await postgres.destroy();
return;
}
if (testWith === "mysql") {
const { sql } = await import("kysely");
const mysql = await getMysql();
await sql`SET FOREIGN_KEY_CHECKS = 0;`.execute(mysql);
const tables = await mysql.introspection.getTables();
for (const table of tables) {
// @ts-expect-error
await mysql.deleteFrom(table.name).execute();
}
await sql`SET FOREIGN_KEY_CHECKS = 1;`.execute(mysql);
return;
}
});
async function signInWithTestUser() {
if (config?.disableTestUser) {
throw new Error("Test user is disabled");
}
let headers = new Headers();
const setCookie = (name: string, value: string) => {
const current = headers.get("cookie");
headers.set("cookie", `${current || ""}; ${name}=${value}`);
};
//@ts-expect-error
const { data, error } = await client.signIn.email({
email: testUser.email,
password: testUser.password,
fetchOptions: {
//@ts-expect-error
onSuccess(context) {
const header = context.response.headers.get("set-cookie");
const cookies = parseSetCookieHeader(header || "");
const signedCookie = cookies.get("better-auth.session_token")?.value;
headers.set("cookie", `better-auth.session_token=${signedCookie}`);
},
},
});
return {
session: data.session as Session,
user: data.user as User,
headers,
setCookie,
};
}
async function signInWithUser(email: string, password: string) {
let headers = new Headers();
//@ts-expect-error
const { data } = await client.signIn.email({
email,
password,
fetchOptions: {
//@ts-expect-error
onSuccess(context) {
const header = context.response.headers.get("set-cookie");
const cookies = parseSetCookieHeader(header || "");
const signedCookie = cookies.get("better-auth.session_token")?.value;
headers.set("cookie", `better-auth.session_token=${signedCookie}`);
},
},
});
return {
res: data as {
user: User;
session: Session;
},
headers,
};
}
const customFetchImpl = async (
url: string | URL | Request,
init?: RequestInit | undefined,
) => {
return auth.handler(new Request(url, init));
};
function sessionSetter(headers: Headers) {
return (context: SuccessContext) => {
const header = context.response.headers.get("set-cookie");
if (header) {
const cookies = parseSetCookieHeader(header || "");
const signedCookie = cookies.get("better-auth.session_token")?.value;
headers.set("cookie", `better-auth.session_token=${signedCookie}`);
}
};
}
const client = createAuthClient({
...(config?.clientOptions as C extends undefined ? {} : C),
baseURL: getBaseURL(
options?.baseURL || "http://localhost:" + (config?.port || 3000),
options?.basePath || "/api/auth",
),
fetchOptions: {
customFetchImpl,
},
});
return {
auth,
client,
testUser,
signInWithTestUser,
signInWithUser,
cookieSetter: setCookieToHeader,
customFetchImpl,
sessionSetter,
db: await getAdapter(auth.options),
};
}
export { convertSetCookieToCookie } from "./headers";
export { getTestInstance } from "./test-instance";
@@ -1,25 +0,0 @@
import * as fs from "fs";
import path from "path";
export type State = "IDLE" | "RUNNING";
export function makeTestState(dirname: string) {
const stateFilePath = path.join(dirname, "./state.txt");
function getState(): State {
try {
return fs
.readFileSync(stateFilePath, "utf-8")
.split("\n")[0]!
.trim() as State;
} catch {
return "IDLE";
}
}
function setState(state: State) {
fs.writeFileSync(stateFilePath, state, "utf-8");
}
return { stateFilePath, getState, setState };
}
@@ -4,18 +4,12 @@ import type {
BetterAuthOptions,
} from "@better-auth/core";
import type { SuccessContext } from "@better-fetch/fetch";
import Database from "better-sqlite3";
import { Kysely, MysqlDialect, PostgresDialect, sql } from "kysely";
import { MongoClient } from "mongodb";
import { createPool } from "mysql2/promise";
import { Pool } from "pg";
import { sql } from "kysely";
import { afterAll } from "vitest";
import { mongodbAdapter } from "../adapters/mongodb-adapter";
import { betterAuth } from "../auth";
import { createAuthClient } from "../client/vanilla";
import { createAuthClient } from "../client";
import { parseSetCookieHeader, setCookieToHeader } from "../cookies";
import { getMigrations } from "../db/get-migration";
import { getAdapter } from "../db/utils";
import { getAdapter, getMigrations } from "../db";
import { bearer } from "../plugins";
import type { Session, User } from "../types";
import { getBaseURL } from "../utils/url";
@@ -35,7 +29,7 @@ afterAll(async () => {
});
export async function getTestInstance<
O extends Partial<BetterAuthOptions>,
O extends Omit<Partial<BetterAuthOptions>, "database">,
C extends BetterAuthClientOptions,
>(
options?: O | undefined,
@@ -51,23 +45,36 @@ export async function getTestInstance<
) {
const testWith = config?.testWith || "sqlite";
const postgres = new Kysely({
dialect: new PostgresDialect({
pool: new Pool({
connectionString: "postgres://user:password@localhost:5432/better_auth",
async function getPostgres() {
const { Kysely, PostgresDialect } = await import("kysely");
const { Pool } = await import("pg");
return new Kysely({
dialect: new PostgresDialect({
pool: new Pool({
connectionString:
"postgres://user:password@localhost:5432/better_auth",
}),
}),
}),
});
});
}
const sqlite = new Database(":memory:");
async function getSqlite() {
const { default: Database } = await import("better-sqlite3");
return new Database(":memory:");
}
const mysql = new Kysely({
dialect: new MysqlDialect(
createPool("mysql://user:password@localhost:3306/better_auth"),
),
});
async function getMysql() {
const { Kysely, MysqlDialect } = await import("kysely");
const { createPool } = await import("mysql2/promise");
return new Kysely({
dialect: new MysqlDialect(
createPool("mysql://user:password@localhost:3306/better_auth"),
),
});
}
async function mongodbClient() {
const { MongoClient } = await import("mongodb");
const dbClient = async (connectionString: string, dbName: string) => {
const client = new MongoClient(connectionString);
await client.connect();
@@ -92,12 +99,15 @@ export async function getTestInstance<
secret: "better-auth.secret",
database:
testWith === "postgres"
? { db: postgres, type: "postgres" }
? { db: await getPostgres(), type: "postgres" }
: testWith === "mongodb"
? mongodbAdapter(await mongodbClient())
? await Promise.all([
mongodbClient(),
await import("../adapters/mongodb-adapter"),
]).then(([db, { mongodbAdapter }]) => mongodbAdapter(db))
: testWith === "mysql"
? { db: mysql, type: "mysql" }
: sqlite,
? { db: await getMysql(), type: "mysql" }
: await getSqlite(),
emailAndPassword: {
enabled: true,
},
@@ -152,6 +162,7 @@ export async function getTestInstance<
return;
}
if (testWith === "postgres") {
const postgres = await getPostgres();
await sql`DROP SCHEMA public CASCADE; CREATE SCHEMA public;`.execute(
postgres,
);
@@ -160,6 +171,7 @@ export async function getTestInstance<
}
if (testWith === "mysql") {
const mysql = await getMysql();
await sql`SET FOREIGN_KEY_CHECKS = 0;`.execute(mysql);
const tables = await mysql.introspection.getTables();
for (const table of tables) {
@@ -170,6 +182,7 @@ export async function getTestInstance<
return;
}
if (testWith === "sqlite") {
const sqlite = await getSqlite();
sqlite.close();
return;
}
+9 -8
View File
@@ -1,19 +1,19 @@
{
"name": "@better-auth/cli",
"version": "1.4.0-beta.15",
"version": "1.4.0-beta.16",
"type": "module",
"description": "The CLI for Better Auth",
"module": "dist/index.js",
"module": "dist/index.mjs",
"repository": {
"type": "git",
"url": "https://github.com/better-auth/better-auth",
"directory": "packages/cli"
},
"homepage": "https://www.better-auth.com/docs/concepts/cli",
"main": "./dist/index.js",
"main": "./dist/index.mjs",
"scripts": {
"build": "tsdown",
"start": "node ./dist/index.js",
"start": "node ./dist/index.mjs",
"dev": "tsx ./src/index.ts",
"test": "vitest",
"typecheck": "tsc --project tsconfig.json"
@@ -21,7 +21,7 @@
"publishConfig": {
"access": "public",
"executableFiles": [
"./dist/index.js"
"./dist/index.mjs"
]
},
"license": "MIT",
@@ -31,15 +31,16 @@
"typescript",
"better-auth"
],
"exports": "./dist/index.js",
"bin": "./dist/index.js",
"exports": "./dist/index.mjs",
"bin": "./dist/index.mjs",
"devDependencies": {
"@better-auth/passkey": "workspace:*",
"@types/semver": "^7.7.1",
"@types/better-sqlite3": "^7.6.13",
"@types/prompts": "^2.4.9",
"@types/semver": "^7.7.1",
"tsdown": "catalog:",
"tsx": "^4.20.6",
"type-fest": "^5.2.0",
"typescript": "catalog:"
},
"dependencies": {
+1 -8
View File
@@ -3,6 +3,7 @@ import fs from "node:fs/promises";
import path from "node:path";
import { promisify } from "node:util";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { cliPath } from "./utils";
const execAsync = promisify(exec);
@@ -43,8 +44,6 @@ describe("info command", () => {
},
}),
);
const cliPath = path.join(process.cwd(), "dist", "index.js");
const { stdout } = await execAsync(`node ${cliPath} info --json`, {
cwd: tmpDir,
});
@@ -109,7 +108,6 @@ describe("info command", () => {
})`,
);
const cliPath = path.join(process.cwd(), "dist", "index.js");
const { stdout } = await execAsync(`node ${cliPath} info --json`, {
cwd: tmpDir,
});
@@ -161,7 +159,6 @@ describe("info command", () => {
}),
);
const cliPath = path.join(process.cwd(), "dist", "index.js");
const { stdout } = await execAsync(`node ${cliPath} info --json`, {
cwd: tmpDir,
});
@@ -206,7 +203,6 @@ describe("info command", () => {
}),
);
const cliPath = path.join(process.cwd(), "dist", "index.js");
const { stdout } = await execAsync(`node ${cliPath} info --json`, {
cwd: tmpDir,
});
@@ -263,7 +259,6 @@ describe("info command", () => {
})`,
);
const cliPath = path.join(process.cwd(), "dist", "index.js");
const { stdout } = await execAsync(
`node ${cliPath} info --config config/auth.config.ts --json`,
{ cwd: tmpDir },
@@ -314,7 +309,6 @@ describe("info command", () => {
})`,
);
const cliPath = path.join(process.cwd(), "dist", "index.js");
const { stdout } = await execAsync(`node ${cliPath} info --json`, {
cwd: tmpDir,
});
@@ -338,7 +332,6 @@ describe("info command", () => {
it("should handle missing package.json gracefully", async () => {
// Don't create package.json
const cliPath = path.join(process.cwd(), "dist", "index.js");
const { stdout } = await execAsync(`node ${cliPath} info --json`, {
cwd: tmpDir,
});
+14
View File
@@ -0,0 +1,14 @@
import fs from "node:fs/promises";
import path from "node:path";
import type { PackageJson } from "type-fest";
const rootDir = path.resolve(import.meta.dirname, "..");
const getCliPath = async () => {
const pkgJson: PackageJson = JSON.parse(
await fs.readFile(path.join(rootDir, "package.json"), "utf-8"),
);
return path.join(rootDir, pkgJson.bin as string);
};
export const cliPath = await getCliPath();
+36 -36
View File
@@ -1,101 +1,101 @@
{
"name": "@better-auth/core",
"version": "1.4.0-beta.15",
"version": "1.4.0-beta.16",
"description": "The most comprehensive authentication framework for TypeScript.",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
"main": "./dist/index.mjs",
"module": "./dist/index.mjs",
"exports": {
".": {
"better-auth-dev-source": "./src/index.ts",
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
"types": "./dist/index.d.mts",
"default": "./dist/index.mjs"
},
"./api": {
"better-auth-dev-source": "./src/api/index.ts",
"types": "./dist/api/index.d.ts",
"default": "./dist/api/index.js"
"types": "./dist/api/index.d.mts",
"default": "./dist/api/index.mjs"
},
"./async_hooks": {
"better-auth-dev-source": "./src/async_hooks/index.ts",
"types": "./dist/async_hooks/index.d.ts",
"default": "./dist/async_hooks/index.js"
"types": "./dist/async_hooks/index.d.mts",
"default": "./dist/async_hooks/index.mjs"
},
"./context": {
"better-auth-dev-source": "./src/context/index.ts",
"types": "./dist/context/index.d.ts",
"default": "./dist/context/index.js"
"types": "./dist/context/index.d.mts",
"default": "./dist/context/index.mjs"
},
"./env": {
"better-auth-dev-source": "./src/env/index.ts",
"types": "./dist/env/index.d.ts",
"default": "./dist/env/index.js"
"types": "./dist/env/index.d.mts",
"default": "./dist/env/index.mjs"
},
"./error": {
"better-auth-dev-source": "./src/error/index.ts",
"types": "./dist/error/index.d.ts",
"default": "./dist/error/index.js"
"types": "./dist/error/index.d.mts",
"default": "./dist/error/index.mjs"
},
"./utils": {
"better-auth-dev-source": "./src/utils/index.ts",
"types": "./dist/utils/index.d.ts",
"default": "./dist/utils/index.js"
"types": "./dist/utils/index.d.mts",
"default": "./dist/utils/index.mjs"
},
"./social-providers": {
"better-auth-dev-source": "./src/social-providers/index.ts",
"types": "./dist/social-providers/index.d.ts",
"default": "./dist/social-providers/index.js"
"types": "./dist/social-providers/index.d.mts",
"default": "./dist/social-providers/index.mjs"
},
"./db": {
"better-auth-dev-source": "./src/db/index.ts",
"types": "./dist/db/index.d.ts",
"default": "./dist/db/index.js"
"types": "./dist/db/index.d.mts",
"default": "./dist/db/index.mjs"
},
"./db/adapter": {
"better-auth-dev-source": "./src/db/adapter/index.ts",
"types": "./dist/db/adapter/index.d.ts",
"default": "./dist/db/adapter/index.js"
"types": "./dist/db/adapter/index.d.mts",
"default": "./dist/db/adapter/index.mjs"
},
"./oauth2": {
"better-auth-dev-source": "./src/oauth2/index.ts",
"types": "./dist/oauth2/index.d.ts",
"default": "./dist/oauth2/index.js"
"types": "./dist/oauth2/index.d.mts",
"default": "./dist/oauth2/index.mjs"
}
},
"typesVersions": {
"*": {
".": [
"dist/index.d.ts"
"dist/index.d.mts"
],
"api": [
"dist/api/index.d.ts"
"dist/api/index.d.mts"
],
"async_hooks": [
"dist/async_hooks/index.d.ts"
"dist/async_hooks/index.d.mts"
],
"context": [
"dist/context/index.d.ts"
"dist/context/index.d.mts"
],
"env": [
"dist/env/index.d.ts"
"dist/env/index.d.mts"
],
"error": [
"dist/error/index.d.ts"
"dist/error/index.d.mts"
],
"utils": [
"dist/utils/index.d.ts"
"dist/utils/index.d.mts"
],
"social-providers": [
"dist/social-providers/index.d.ts"
"dist/social-providers/index.d.mts"
],
"db": [
"dist/db/index.d.ts"
"dist/db/index.d.mts"
],
"db/adapter": [
"dist/db/adapter/index.d.ts"
"dist/db/adapter/index.d.mts"
],
"oauth2": [
"dist/oauth2/index.d.ts"
"dist/oauth2/index.d.mts"
]
}
},
+9 -9
View File
@@ -1,10 +1,10 @@
{
"name": "@better-auth/expo",
"version": "1.4.0-beta.15",
"version": "1.4.0-beta.16",
"type": "module",
"description": "Better Auth integration for Expo and React Native applications.",
"main": "dist/index.js",
"module": "dist/index.js",
"main": "dist/index.mjs",
"module": "dist/index.mjs",
"repository": {
"type": "git",
"url": "https://github.com/better-auth/better-auth",
@@ -20,22 +20,22 @@
"exports": {
".": {
"better-auth-dev-source": "./src/index.ts",
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
"types": "./dist/index.d.mts",
"default": "./dist/index.mjs"
},
"./client": {
"better-auth-dev-source": "./src/client.ts",
"types": "./dist/client.d.ts",
"default": "./dist/client.js"
"types": "./dist/client.d.mts",
"default": "./dist/client.mjs"
}
},
"typesVersions": {
"*": {
"*": [
"./dist/index.d.ts"
"./dist/index.d.mts"
],
"client": [
"./dist/client.d.ts"
"./dist/client.d.mts"
]
}
},
+6
View File
@@ -3,6 +3,12 @@ import { defineConfig } from "tsdown";
export default defineConfig({
dts: { build: true, incremental: true },
format: ["esm"],
outExtension({ format }) {
if (format === "esm") {
return { js: ".mjs", dts: ".d.mts" };
}
return { js: ".js", dts: ".d.ts" };
},
entry: ["./src/index.ts", "./src/client.ts"],
external: [
"better-auth",
+9 -9
View File
@@ -1,10 +1,10 @@
{
"name": "@better-auth/passkey",
"version": "1.4.0-beta.15",
"version": "1.4.0-beta.16",
"type": "module",
"description": "Passkey plugin for Better Auth",
"main": "dist/index.js",
"module": "dist/index.js",
"main": "dist/index.mjs",
"module": "dist/index.mjs",
"publishConfig": {
"access": "public"
},
@@ -17,22 +17,22 @@
"exports": {
".": {
"better-auth-dev-source": "./src/index.ts",
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
"types": "./dist/index.d.mts",
"default": "./dist/index.mjs"
},
"./client": {
"better-auth-dev-source": "./src/client.ts",
"types": "./dist/client.d.ts",
"default": "./dist/client.js"
"types": "./dist/client.d.mts",
"default": "./dist/client.mjs"
}
},
"typesVersions": {
"*": {
"*": [
"./dist/index.d.ts"
"./dist/index.d.mts"
],
"client": [
"./dist/client.d.ts"
"./dist/client.d.mts"
]
}
},
+1 -1
View File
@@ -1,5 +1,5 @@
import { createAuthClient } from "better-auth/client";
import { getTestInstanceMemory as getTestInstance } from "better-auth/test";
import { getTestInstance } from "better-auth/test";
import { describe, expect, it } from "vitest";
import { type Passkey, passkey } from ".";
import { passkeyClient } from "./client";
+6
View File
@@ -3,6 +3,12 @@ import { defineConfig } from "tsdown";
export default defineConfig({
dts: { build: true, incremental: true },
format: ["esm"],
outExtension({ format }) {
if (format === "esm") {
return { js: ".mjs", dts: ".d.mts" };
}
return { js: ".js", dts: ".d.ts" };
},
entry: ["./src/index.ts", "./src/client.ts"],
external: [
"nanostores",
+9 -9
View File
@@ -1,9 +1,9 @@
{
"name": "@better-auth/sso",
"author": "Bereket Engida",
"version": "1.4.0-beta.15",
"version": "1.4.0-beta.16",
"type": "module",
"main": "dist/index.js",
"main": "dist/index.mjs",
"homepage": "https://www.better-auth.com/docs/plugins/sso",
"repository": {
"type": "git",
@@ -26,7 +26,7 @@
"publishConfig": {
"access": "public"
},
"module": "dist/index.js",
"module": "dist/index.mjs",
"description": "SSO plugin for Better Auth",
"scripts": {
"test": "vitest",
@@ -37,22 +37,22 @@
"exports": {
".": {
"better-auth-dev-source": "./src/index.ts",
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
"types": "./dist/index.d.mts",
"default": "./dist/index.mjs"
},
"./client": {
"better-auth-dev-source": "./src/client.ts",
"types": "./dist/client.d.ts",
"default": "./dist/client.js"
"types": "./dist/client.d.mts",
"default": "./dist/client.mjs"
}
},
"typesVersions": {
"*": {
"*": [
"./dist/index.d.ts"
"./dist/index.d.mts"
],
"client": [
"./dist/client.d.ts"
"./dist/client.d.mts"
]
}
},
+1 -1
View File
@@ -1,7 +1,7 @@
import { betterFetch } from "@better-fetch/fetch";
import { createAuthClient } from "better-auth/client";
import { organization } from "better-auth/plugins";
import { getTestInstanceMemory as getTestInstance } from "better-auth/test";
import { getTestInstance } from "better-auth/test";
import { OAuth2Server } from "oauth2-mock-server";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { sso } from ".";
+4 -4
View File
@@ -4,7 +4,7 @@ import { memoryAdapter } from "better-auth/adapters/memory";
import { createAuthClient } from "better-auth/client";
import { setCookieToHeader } from "better-auth/cookies";
import { bearer } from "better-auth/plugins";
import { getTestInstanceMemory } from "better-auth/test";
import { getTestInstance } from "better-auth/test";
import bodyParser from "body-parser";
import { randomUUID } from "crypto";
import type {
@@ -926,7 +926,7 @@ describe("SAML SSO", async () => {
});
it("should not allow creating a provider if limit is set to 0", async () => {
const { auth, signInWithTestUser } = await getTestInstanceMemory({
const { auth, signInWithTestUser } = await getTestInstance({
plugins: [sso({ providersLimit: 0 })],
});
const { headers } = await signInWithTestUser();
@@ -957,7 +957,7 @@ describe("SAML SSO", async () => {
});
it("should not allow creating a provider if limit is reached", async () => {
const { auth, signInWithTestUser } = await getTestInstanceMemory({
const { auth, signInWithTestUser } = await getTestInstance({
plugins: [sso({ providersLimit: 1 })],
});
const { headers } = await signInWithTestUser();
@@ -1011,7 +1011,7 @@ describe("SAML SSO", async () => {
});
it("should not allow creating a provider if limit from function is reached", async () => {
const { auth, signInWithTestUser } = await getTestInstanceMemory({
const { auth, signInWithTestUser } = await getTestInstance({
plugins: [
sso({
providersLimit: async (user) => {
+9 -9
View File
@@ -1,9 +1,9 @@
{
"name": "@better-auth/stripe",
"author": "Bereket Engida",
"version": "1.4.0-beta.15",
"version": "1.4.0-beta.16",
"type": "module",
"main": "dist/index.js",
"main": "dist/index.mjs",
"license": "MIT",
"homepage": "https://www.better-auth.com/docs/plugins/stripe",
"repository": {
@@ -16,7 +16,7 @@
"auth",
"stripe"
],
"module": "dist/index.js",
"module": "dist/index.mjs",
"description": "Stripe plugin for Better Auth",
"scripts": {
"test": "vitest",
@@ -30,22 +30,22 @@
"exports": {
".": {
"better-auth-dev-source": "./src/index.ts",
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
"types": "./dist/index.d.mts",
"default": "./dist/index.mjs"
},
"./client": {
"better-auth-dev-source": "./src/client.ts",
"types": "./dist/client.d.ts",
"default": "./dist/client.js"
"types": "./dist/client.d.mts",
"default": "./dist/client.mjs"
}
},
"typesVersions": {
"*": {
"*": [
"./dist/index.d.ts"
"./dist/index.d.mts"
],
"client": [
"./dist/client.d.ts"
"./dist/client.d.mts"
]
}
},
+7 -7
View File
@@ -1,21 +1,21 @@
{
"name": "@better-auth/telemetry",
"version": "1.4.0-beta.15",
"version": "1.4.0-beta.16",
"description": "Telemetry package for Better Auth",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
"main": "./dist/index.mjs",
"module": "./dist/index.mjs",
"exports": {
".": {
"better-auth-dev-source": "./src/index.ts",
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
"types": "./dist/index.d.mts",
"default": "./dist/index.mjs"
}
},
"typesVersions": {
"*": {
"index": [
"dist/index.d.ts"
"dist/index.d.mts"
]
}
},
@@ -27,7 +27,7 @@
"devDependencies": {
"@better-auth/core": "workspace:*",
"tsdown": "catalog:",
"type-fest": "^4.31.0"
"type-fest": "^5.2.0"
},
"dependencies": {
"@better-auth/utils": "0.3.0",
+326 -90
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -7,7 +7,7 @@ packages:
catalog:
'@better-fetch/fetch': 1.1.18
better-call: 1.0.26
tsdown: ^0.15.11
tsdown: ^0.16.0
typescript: ^5.9.3
vitest: ^3.2.4