mirror of
https://github.com/LukeHagar/better-auth.git
synced 2025-12-10 04:19:32 +00:00
Merge branch 'canary' into v1.3.9-staging
# Conflicts: # packages/better-auth/package.json # packages/cli/package.json # packages/expo/package.json # packages/sso/package.json # packages/stripe/package.json
This commit is contained in:
4
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
4
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -46,9 +46,9 @@ body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: System info
|
||||
description: Output of `npx envinfo --system --browsers`
|
||||
description: Output of `npx @better-auth/cli info --json`
|
||||
render: bash
|
||||
placeholder: System and browsers
|
||||
placeholder: System and Better Auth info
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
||||
36
.github/workflows/branch-rules.yml
vendored
Normal file
36
.github/workflows/branch-rules.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
name: Branch Rules
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- canary
|
||||
- 'v*-staging'
|
||||
|
||||
jobs:
|
||||
check-branch:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Filter changed files
|
||||
uses: dorny/paths-filter@v3
|
||||
id: filter
|
||||
with:
|
||||
base: ${{ github.base_ref }}
|
||||
filters: |
|
||||
# 'src' filter matches any file NOT in the docs/ directory
|
||||
src:
|
||||
- '!docs/**'
|
||||
# 'docs' filter matches any file in the docs/ directory
|
||||
docs:
|
||||
- 'docs/**'
|
||||
|
||||
- name: Enforce code change PRs target canary or staging
|
||||
if: steps.filter.outputs.src == 'true' && github.base_ref != 'canary' && !startsWith(github.base_ref, 'v')
|
||||
run: |
|
||||
echo "Error: Pull requests with code changes must target 'canary' or a versioned staging branch (e.g., 'v1.2.3-staging')."
|
||||
exit 1
|
||||
@@ -53,7 +53,9 @@ export default function Layout({ children }: { children: ReactNode }) {
|
||||
}}
|
||||
search={{
|
||||
enabled: true,
|
||||
SearchDialog: CustomSearchDialog,
|
||||
SearchDialog: process.env.ORAMA_PRIVATE_API_KEY
|
||||
? CustomSearchDialog
|
||||
: undefined,
|
||||
}}
|
||||
>
|
||||
<NavbarProvider>
|
||||
|
||||
@@ -44,6 +44,27 @@ export const Icons = {
|
||||
></path>
|
||||
</svg>
|
||||
),
|
||||
lynx: (props?: SVGProps<any>) => (
|
||||
<svg
|
||||
className={props?.className}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1.2em"
|
||||
height="1.2em"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M5.19683 4.42475L2.58326 6.22507C2.29923 6.42072 2.10258 6.71472 2.0335 7.04698L1.77798 8.27604C1.76443 8.34121 1.73438 8.40204 1.69058 8.45301L0.504615 10.0077C0.344941 10.1935 0.348791 10.6871 0.786137 11.0031C0.953848 11.1493 1.17807 11.477 1.43312 11.8497L1.43312 11.8497C1.97313 12.639 2.65131 13.6302 3.22376 13.5272C4.03382 13.2426 5.02541 13.1516 5.78673 13.5272C7.2667 14.8049 6.90331 15.983 6.38234 17.672C6.17271 18.3516 5.93755 19.114 5.78673 19.9988C6.49755 17.4117 8.09592 14.4069 10.9781 13.3874C10.4588 12.9632 9.39906 12.5691 8.46129 12.4742C8.46129 12.4742 11.3423 10.0077 14.8957 8.87434C12.4151 2.97197 8.32052 0.151295 8.32052 0.151295C8.11187 -0.10677 7.69054 -0.0221092 7.6036 0.295351C7.53376 1.22845 7.41798 1.86295 7.22685 2.46105L5.78673 0.799291C5.6363 0.61794 5.33557 0.722655 5.33707 0.955861C5.5809 2.31136 5.54668 3.07222 5.19683 4.42475ZM6.21052 4.30085L6.21912 4.3003C6.23794 4.29909 6.25646 4.29664 6.27456 4.29302L6.21052 4.30085ZM8.15541 1.25793C9.1423 2.96321 9.58937 3.932 9.74102 5.73998C8.6912 5.14364 8.23382 4.99187 7.46183 4.99565C7.91215 3.62621 8.04976 2.8016 8.15541 1.25793Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M14.4988 13.9228C10.479 14.8427 8.19556 16.2278 6.44922 19.9994C9.58406 14.7399 19.0737 15.6805 19.0737 15.6805C18.8964 14.8214 17.0097 13.183 15.7688 12.1782C15.7688 12.1782 16.7699 10.9474 18.983 10.3244C18.983 10.3244 14.728 10.5709 12.2508 11.9084C13.0533 12.3319 14.0812 13.0467 14.4988 13.9228Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
solidStart: (props?: SVGProps<any>) => (
|
||||
<svg
|
||||
className={props?.className}
|
||||
|
||||
@@ -1402,6 +1402,11 @@ C0.7,239.6,62.1,0.5,62.2,0.4c0,0,54,13.8,119.9,30.8S302.1,62,302.2,62c0.2,0,0.2,
|
||||
icon: Icons.expo,
|
||||
href: "/docs/integrations/expo",
|
||||
},
|
||||
{
|
||||
title: "Lynx",
|
||||
icon: Icons.lynx,
|
||||
href: "/docs/integrations/lynx",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -109,6 +109,21 @@ export const auth = betterAuth({
|
||||
})
|
||||
```
|
||||
|
||||
If you like to disable rate limiting for a specific path, you can set it to `false` or return `false` from the custom rule function.
|
||||
|
||||
```ts title="auth.ts"
|
||||
import { betterAuth } from "better-auth";
|
||||
|
||||
export const auth = betterAuth({
|
||||
//...other options
|
||||
rateLimit: {
|
||||
customRules: {
|
||||
"/get-session": false,
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### Storage
|
||||
|
||||
By default, rate limit data is stored in memory, which may not be suitable for many use cases, particularly in serverless environments. To address this, you can use a database, secondary storage, or custom storage for storing rate limit data.
|
||||
|
||||
@@ -294,7 +294,7 @@ Create a new file or route in your framework's designated catch-all route handle
|
||||
|
||||
const app = new Hono();
|
||||
|
||||
app.on(["POST", "GET"], "/api/auth/**", (c) => auth.handler(c.req.raw));
|
||||
app.on(["POST", "GET"], "/api/auth/*", (c) => auth.handler(c.req.raw));
|
||||
|
||||
serve(app);
|
||||
```
|
||||
|
||||
212
docs/content/docs/integrations/lynx.mdx
Normal file
212
docs/content/docs/integrations/lynx.mdx
Normal file
@@ -0,0 +1,212 @@
|
||||
---
|
||||
title: Lynx Integration
|
||||
description: Integrate Better Auth with Lynx cross-platform framework.
|
||||
---
|
||||
|
||||
This integration guide is for using Better Auth with [Lynx](https://lynxjs.org), a cross-platform rendering framework that enables developers to build applications for Android, iOS, and Web platforms with native rendering performance.
|
||||
|
||||
Before you start, make sure you have a Better Auth instance configured. If you haven't done that yet, check out the [installation](/docs/installation).
|
||||
|
||||
## Installation
|
||||
|
||||
Install Better Auth and the Lynx React dependency:
|
||||
|
||||
```package-install
|
||||
better-auth @lynx-js/react
|
||||
```
|
||||
|
||||
## Create Client Instance
|
||||
|
||||
Import `createAuthClient` from `better-auth/lynx` to create your client instance:
|
||||
|
||||
```ts title="lib/auth-client.ts"
|
||||
import { createAuthClient } from "better-auth/lynx"
|
||||
|
||||
export const authClient = createAuthClient({
|
||||
baseURL: "http://localhost:3000" // The base URL of your auth server
|
||||
})
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
The Lynx client provides the same API as other Better Auth clients, with optimized integration for Lynx's reactive system.
|
||||
|
||||
### Authentication Methods
|
||||
|
||||
```ts
|
||||
import { authClient } from "./lib/auth-client"
|
||||
|
||||
// Sign in with email and password
|
||||
await authClient.signIn.email({
|
||||
email: "test@user.com",
|
||||
password: "password1234"
|
||||
})
|
||||
|
||||
// Sign up
|
||||
await authClient.signUp.email({
|
||||
email: "test@user.com",
|
||||
password: "password1234",
|
||||
name: "John Doe"
|
||||
})
|
||||
|
||||
// Sign out
|
||||
await authClient.signOut()
|
||||
```
|
||||
|
||||
### Hooks
|
||||
|
||||
The Lynx client includes reactive hooks that integrate seamlessly with Lynx's component system:
|
||||
|
||||
#### useSession
|
||||
|
||||
```tsx title="components/user.tsx"
|
||||
import { authClient } from "../lib/auth-client"
|
||||
|
||||
export function User() {
|
||||
const {
|
||||
data: session,
|
||||
isPending, // loading state
|
||||
error // error object
|
||||
} = authClient.useSession()
|
||||
|
||||
if (isPending) return <div>Loading...</div>
|
||||
if (error) return <div>Error: {error.message}</div>
|
||||
|
||||
return (
|
||||
<div>
|
||||
{session ? (
|
||||
<div>
|
||||
<p>Welcome, {session.user.name}!</p>
|
||||
<button onClick={() => authClient.signOut()}>
|
||||
Sign Out
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<button onClick={() => authClient.signIn.social({
|
||||
provider: 'github'
|
||||
})}>
|
||||
Sign In with GitHub
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Store Integration
|
||||
|
||||
The Lynx client uses [nanostores](https://github.com/nanostores/nanostores) for state management and provides a `useStore` hook for accessing reactive state:
|
||||
|
||||
```tsx title="components/session-info.tsx"
|
||||
import { useStore } from "better-auth/lynx"
|
||||
import { authClient } from "../lib/auth-client"
|
||||
|
||||
export function SessionInfo() {
|
||||
// Access the session store directly
|
||||
const session = useStore(authClient.$store.session)
|
||||
|
||||
return (
|
||||
<div>
|
||||
{session && (
|
||||
<pre>{JSON.stringify(session, null, 2)}</pre>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Advanced Store Usage
|
||||
|
||||
You can use the store with selective key watching for optimized re-renders:
|
||||
|
||||
```tsx title="components/optimized-user.tsx"
|
||||
import { useStore } from "better-auth/lynx"
|
||||
import { authClient } from "../lib/auth-client"
|
||||
|
||||
export function OptimizedUser() {
|
||||
// Only re-render when specific keys change
|
||||
const session = useStore(authClient.$store.session, {
|
||||
keys: ['user.name', 'user.email'] // Only watch these specific keys
|
||||
})
|
||||
|
||||
return (
|
||||
<div>
|
||||
{session?.user && (
|
||||
<div>
|
||||
<h2>{session.user.name}</h2>
|
||||
<p>{session.user.email}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Plugin Support
|
||||
|
||||
The Lynx client supports all Better Auth plugins:
|
||||
|
||||
```ts title="lib/auth-client.ts"
|
||||
import { createAuthClient } from "better-auth/lynx"
|
||||
import { magicLinkClient } from "better-auth/client/plugins"
|
||||
|
||||
const authClient = createAuthClient({
|
||||
plugins: [
|
||||
magicLinkClient()
|
||||
]
|
||||
})
|
||||
|
||||
// Use plugin methods
|
||||
await authClient.signIn.magicLink({
|
||||
email: "test@email.com"
|
||||
})
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
Error handling works the same as other Better Auth clients:
|
||||
|
||||
```tsx title="components/login-form.tsx"
|
||||
import { authClient } from "../lib/auth-client"
|
||||
|
||||
export function LoginForm() {
|
||||
const signIn = async (email: string, password: string) => {
|
||||
const { data, error } = await authClient.signIn.email({
|
||||
email,
|
||||
password
|
||||
})
|
||||
|
||||
if (error) {
|
||||
console.error('Login failed:', error.message)
|
||||
return
|
||||
}
|
||||
|
||||
console.log('Login successful:', data)
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
const formData = new FormData(e.target)
|
||||
signIn(formData.get('email'), formData.get('password'))
|
||||
}}>
|
||||
<input name="email" type="email" placeholder="Email" />
|
||||
<input name="password" type="password" placeholder="Password" />
|
||||
<button type="submit">Sign In</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
The Lynx client provides:
|
||||
|
||||
- **Cross-Platform Support**: Works across Android, iOS, and Web platforms
|
||||
- **Optimized Performance**: Built specifically for Lynx's reactive system
|
||||
- **Nanostores Integration**: Uses nanostores for efficient state management
|
||||
- **Selective Re-rendering**: Watch specific store keys to minimize unnecessary updates
|
||||
- **Full API Compatibility**: All Better Auth methods and plugins work seamlessly
|
||||
- **TypeScript Support**: Full type safety with TypeScript inference
|
||||
|
||||
The Lynx integration maintains all the features and benefits of Better Auth while providing optimal performance and developer experience within Lynx's cross-platform ecosystem.
|
||||
@@ -57,7 +57,7 @@ The **MCP** plugin lets your app act as an OAuth provider for MCP clients. It ha
|
||||
|
||||
### OAuth Discovery Metadata
|
||||
|
||||
Add a route to expose OAuth metadata for MCP clients:
|
||||
Better Auth already handles the `/api/auth/.well-known/oauth-authorization-server` route automatically but some client may fail to parse the `WWW-Authenticate` header and default to `/.well-known/oauth-authorization-server` (this can happen, for example, if your CORS configuration doesn't expose the `WWW-Authenticate`). For this reason it's better to add a route to expose OAuth metadata for MCP clients:
|
||||
|
||||
```ts title=".well-known/oauth-authorization-server/route.ts"
|
||||
import { oAuthDiscoveryMetadata } from "better-auth/plugins";
|
||||
@@ -68,7 +68,7 @@ export const GET = oAuthDiscoveryMetadata(auth);
|
||||
|
||||
### OAuth Protected Resource Metadata
|
||||
|
||||
Add a route to expose protected resource metadata for MCP clients:
|
||||
Better Auth already handles the `/api/auth/.well-known/oauth-protected-resource` route automatically but some client may fail to parse the `WWW-Authenticate` header and default to `/.well-known/oauth-protected-resource` (this can happen, for example, if your CORS configuration doesn't expose the `WWW-Authenticate`). For this reason it's better to add a route to expose OAuth metadata for MCP clients:
|
||||
|
||||
```ts title="/.well-known/oauth-protected-resource/route.ts"
|
||||
import { oAuthProtectedResourceMetadata } from "better-auth/plugins";
|
||||
@@ -187,6 +187,11 @@ The MCP plugin accepts the following configuration options:
|
||||
type: "string",
|
||||
required: true
|
||||
},
|
||||
resource: {
|
||||
description: "The resource that should be returned by the protected resource metadata endpoint",
|
||||
type: "string",
|
||||
required: false
|
||||
},
|
||||
oidcConfig: {
|
||||
description: "Optional OIDC configuration options",
|
||||
type: "object",
|
||||
|
||||
@@ -2,6 +2,7 @@ import { describe, it } from "node:test";
|
||||
import { spawn } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { join } from "node:path";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const fixturesDir = fileURLToPath(new URL("./fixtures", import.meta.url));
|
||||
|
||||
@@ -16,12 +17,26 @@ describe("(cloudflare) simple server", () => {
|
||||
cp.kill("SIGINT");
|
||||
});
|
||||
|
||||
const unexpectedStrings = new Set(["node:sqlite"]);
|
||||
|
||||
cp.stdout.on("data", (data) => {
|
||||
console.log(data.toString());
|
||||
for (const str of unexpectedStrings) {
|
||||
assert(
|
||||
!data.toString().includes(str),
|
||||
`Output should not contain "${str}"`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
cp.stderr.on("data", (data) => {
|
||||
console.error(data.toString());
|
||||
for (const str of unexpectedStrings) {
|
||||
assert(
|
||||
!data.toString().includes(str),
|
||||
`Error output should not contain "${str}"`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "better-auth",
|
||||
"version": "1.3.8",
|
||||
"version": "1.3.9-beta.4",
|
||||
"description": "The most comprehensive authentication library for TypeScript.",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
@@ -700,22 +700,26 @@
|
||||
"dependencies": {
|
||||
"@better-auth/utils": "0.2.6",
|
||||
"@better-fetch/fetch": "catalog:",
|
||||
"@noble/ciphers": "^0.6.0",
|
||||
"@noble/hashes": "^1.8.0",
|
||||
"@noble/ciphers": "^2.0.0",
|
||||
"@noble/hashes": "^2.0.0",
|
||||
"@simplewebauthn/browser": "^13.1.2",
|
||||
"@simplewebauthn/server": "^13.1.2",
|
||||
"better-call": "catalog:",
|
||||
"defu": "^6.1.4",
|
||||
"jose": "^5.10.0",
|
||||
"jose": "^6.1.0",
|
||||
"kysely": "^0.28.5",
|
||||
"nanostores": "^0.11.4",
|
||||
"zod": "^4.1.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@lynx-js/react": "*",
|
||||
"react": "^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@lynx-js/react": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
@@ -724,6 +728,7 @@
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lynx-js/react": "^0.112.5",
|
||||
"@prisma/client": "^5.22.0",
|
||||
"@tanstack/react-start": "^1.131.3",
|
||||
"@types/better-sqlite3": "^7.6.13",
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
gt,
|
||||
gte,
|
||||
inArray,
|
||||
notInArray,
|
||||
like,
|
||||
lt,
|
||||
lte,
|
||||
@@ -163,6 +164,15 @@ export const drizzleAdapter = (db: DB, config: DrizzleAdapterConfig) =>
|
||||
return [inArray(schemaModel[field], w.value)];
|
||||
}
|
||||
|
||||
if (w.operator === "not_in") {
|
||||
if (!Array.isArray(w.value)) {
|
||||
throw new BetterAuthError(
|
||||
`The value for the field "${w.field}" must be an array when using the "not_in" operator.`,
|
||||
);
|
||||
}
|
||||
return [notInArray(schemaModel[field], w.value)];
|
||||
}
|
||||
|
||||
if (w.operator === "contains") {
|
||||
return [like(schemaModel[field], `%${w.value}%`)];
|
||||
}
|
||||
@@ -213,6 +223,14 @@ export const drizzleAdapter = (db: DB, config: DrizzleAdapterConfig) =>
|
||||
}
|
||||
return inArray(schemaModel[field], w.value);
|
||||
}
|
||||
if (w.operator === "not_in") {
|
||||
if (!Array.isArray(w.value)) {
|
||||
throw new BetterAuthError(
|
||||
`The value for the field "${w.field}" must be an array when using the "not_in" operator.`,
|
||||
);
|
||||
}
|
||||
return notInArray(schemaModel[field], w.value);
|
||||
}
|
||||
return eq(schemaModel[field], w.value);
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -110,7 +110,14 @@ export const createKyselyAdapter = async (config: BetterAuthOptions) => {
|
||||
let DatabaseSync: typeof import("node:sqlite").DatabaseSync | undefined =
|
||||
undefined;
|
||||
try {
|
||||
({ DatabaseSync } = await import("node:sqlite"));
|
||||
let nodeSqlite: string = "node:sqlite";
|
||||
// Ignore both Vite and Webpack for dynamic import as they both try to pre-bundle 'node:sqlite' which might fail
|
||||
// It's okay because we are in a try-catch block
|
||||
({ DatabaseSync } = await import(
|
||||
/* @vite-ignore */
|
||||
/* webpackIgnore: true */
|
||||
nodeSqlite
|
||||
));
|
||||
} catch (error: unknown) {
|
||||
if (
|
||||
error !== null &&
|
||||
|
||||
@@ -136,6 +136,14 @@ export const kyselyAdapter = (db: Kysely<any>, config?: KyselyAdapterConfig) =>
|
||||
return eb(field, "in", Array.isArray(value) ? value : [value]);
|
||||
}
|
||||
|
||||
if (operator.toLowerCase() === "not_in") {
|
||||
return eb(
|
||||
field,
|
||||
"not in",
|
||||
Array.isArray(value) ? value : [value],
|
||||
);
|
||||
}
|
||||
|
||||
if (operator === "contains") {
|
||||
return eb(field, "like", `%${value}%`);
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
IDLE
|
||||
RUNNING
|
||||
@@ -51,6 +51,12 @@ export const memoryAdapter = (db: MemoryDB, config?: MemoryAdapterConfig) =>
|
||||
}
|
||||
// @ts-expect-error
|
||||
return value.includes(record[field]);
|
||||
} else if (operator === "not_in") {
|
||||
if (!Array.isArray(value)) {
|
||||
throw new Error("Value must be an array");
|
||||
}
|
||||
// @ts-expect-error
|
||||
return !value.includes(record[field]);
|
||||
} else if (operator === "contains") {
|
||||
return record[field].includes(value);
|
||||
} else if (operator === "starts_with") {
|
||||
|
||||
@@ -177,6 +177,15 @@ export const mongodbAdapter = (db: Db, config?: MongoDBAdapterConfig) => {
|
||||
},
|
||||
};
|
||||
break;
|
||||
case "not_in":
|
||||
condition = {
|
||||
[field]: {
|
||||
$nin: Array.isArray(value)
|
||||
? value.map((v) => serializeID({ field, value: v, model }))
|
||||
: [serializeID({ field, value, model })],
|
||||
},
|
||||
};
|
||||
break;
|
||||
case "gt":
|
||||
condition = { [field]: { $gt: value } };
|
||||
break;
|
||||
|
||||
@@ -70,6 +70,8 @@ export const prismaAdapter = (prisma: PrismaClient, config: PrismaConfig) =>
|
||||
return "endsWith";
|
||||
case "ne":
|
||||
return "not";
|
||||
case "not_in":
|
||||
return "notIn";
|
||||
default:
|
||||
return operator;
|
||||
}
|
||||
|
||||
@@ -31,6 +31,8 @@ const adapterTests = {
|
||||
SHOULD_FIND_MANY_WITH_WHERE: "should find many with where",
|
||||
SHOULD_FIND_MANY_WITH_OPERATORS: "should find many with operators",
|
||||
SHOULD_WORK_WITH_REFERENCE_FIELDS: "should work with reference fields",
|
||||
SHOULD_FIND_MANY_WITH_NOT_IN_OPERATOR:
|
||||
"should find many with not in operator",
|
||||
SHOULD_FIND_MANY_WITH_SORT_BY: "should find many with sortBy",
|
||||
SHOULD_FIND_MANY_WITH_LIMIT: "should find many with limit",
|
||||
SHOULD_FIND_MANY_WITH_OFFSET: "should find many with offset",
|
||||
@@ -369,6 +371,54 @@ async function adapterTest(
|
||||
},
|
||||
);
|
||||
|
||||
test.skipIf(disabledTests?.SHOULD_FIND_MANY_WITH_NOT_IN_OPERATOR)(
|
||||
`${testPrefix ? `${testPrefix} - ` : ""}${
|
||||
adapterTests.SHOULD_FIND_MANY_WITH_NOT_IN_OPERATOR
|
||||
}`,
|
||||
async ({ onTestFailed }) => {
|
||||
await resetDebugLogs();
|
||||
onTestFailed(async () => {
|
||||
await printDebugLogs();
|
||||
});
|
||||
|
||||
const newUser3 = await (await adapter()).create<User>({
|
||||
model: "user",
|
||||
data: {
|
||||
name: "user",
|
||||
email: "test-email3email.com",
|
||||
emailVerified: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
});
|
||||
const allUsers = await (await adapter()).findMany<User>({
|
||||
model: "user",
|
||||
});
|
||||
expect(allUsers.length).toBe(6);
|
||||
const usersWithoutNotIn = await (await adapter()).findMany<User>({
|
||||
model: "user",
|
||||
where: [
|
||||
{
|
||||
field: "id",
|
||||
operator: "not_in",
|
||||
value: [user.id, newUser3.id],
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(usersWithoutNotIn.length).toBe(4);
|
||||
//cleanup
|
||||
await (await adapter()).delete({
|
||||
model: "user",
|
||||
where: [
|
||||
{
|
||||
field: "id",
|
||||
value: newUser3.id,
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test.skipIf(disabledTests?.SHOULD_WORK_WITH_REFERENCE_FIELDS)(
|
||||
`${testPrefix ? `${testPrefix} - ` : ""}${
|
||||
adapterTests.SHOULD_WORK_WITH_REFERENCE_FIELDS
|
||||
|
||||
@@ -3,7 +3,6 @@ import type { AuthContext } from "../init";
|
||||
import type { BetterAuthOptions } from "../types";
|
||||
import type { UnionToIntersection } from "../types/helper";
|
||||
import { originCheckMiddleware } from "./middlewares/origin-check";
|
||||
import { BetterAuthError } from "../error";
|
||||
import {
|
||||
callbackOAuth,
|
||||
forgetPassword,
|
||||
|
||||
@@ -162,6 +162,10 @@ export async function onRequestRateLimit(req: Request, ctx: AuthContext) {
|
||||
window = resolved.window;
|
||||
max = resolved.max;
|
||||
}
|
||||
|
||||
if (resolved === false) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -155,6 +155,7 @@ describe("should work with custom rules", async () => {
|
||||
window: 10,
|
||||
max: 3,
|
||||
},
|
||||
"/get-session": false,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -196,4 +197,14 @@ describe("should work with custom rules", async () => {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("should not rate limit if custom rule is false", async () => {
|
||||
let i = 0;
|
||||
let response = null;
|
||||
for (; i < 110; i++) {
|
||||
response = await client.getSession().then((res) => res.error);
|
||||
}
|
||||
expect(response).toBeNull();
|
||||
expect(i).toBe(110);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -132,6 +132,13 @@ export const callbackOAuth = createAuthEndpoint(
|
||||
return redirectOnError("unable_to_link_account");
|
||||
}
|
||||
|
||||
if (
|
||||
userInfo.email !== link.email &&
|
||||
c.context.options.account?.accountLinking?.allowDifferentEmails !== true
|
||||
) {
|
||||
return redirectOnError("email_doesn't_match");
|
||||
}
|
||||
|
||||
const existingAccount = await c.context.internalAdapter.findAccount(
|
||||
String(userInfo.id),
|
||||
);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { describe, expect } from "vitest";
|
||||
import { describe, expect, vi } from "vitest";
|
||||
import { getTestInstance } from "../../test-utils/test-instance";
|
||||
import { parseSetCookieHeader } from "../../cookies";
|
||||
import { APIError } from "better-call";
|
||||
import { BASE_ERROR_CODES } from "../../error/codes";
|
||||
|
||||
/**
|
||||
* More test can be found in `session.test.ts`
|
||||
@@ -44,4 +46,66 @@ describe("sign-in", async (it) => {
|
||||
expect(session?.session.ipAddress).toBe(headerObj["X-Forwarded-For"]);
|
||||
expect(session?.session.userAgent).toBe(headerObj["User-Agent"]);
|
||||
});
|
||||
|
||||
it("verification email will be sent if sendOnSignIn is enabled", async () => {
|
||||
const sendVerificationEmail = vi.fn();
|
||||
const { auth, testUser } = await getTestInstance({
|
||||
emailVerification: {
|
||||
sendOnSignIn: true,
|
||||
sendVerificationEmail,
|
||||
},
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
requireEmailVerification: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(sendVerificationEmail).toHaveBeenCalledTimes(1);
|
||||
|
||||
await expect(
|
||||
auth.api.signInEmail({
|
||||
body: {
|
||||
email: testUser.email,
|
||||
password: testUser.password,
|
||||
},
|
||||
}),
|
||||
).rejects.toThrowError(
|
||||
new APIError("FORBIDDEN", {
|
||||
message: BASE_ERROR_CODES.EMAIL_NOT_VERIFIED,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(sendVerificationEmail).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("verification email will not be sent if sendOnSignIn is disabled", async () => {
|
||||
const sendVerificationEmail = vi.fn();
|
||||
const { auth, testUser } = await getTestInstance({
|
||||
emailVerification: {
|
||||
sendOnSignIn: false,
|
||||
sendVerificationEmail,
|
||||
},
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
requireEmailVerification: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(sendVerificationEmail).toHaveBeenCalledTimes(1);
|
||||
|
||||
await expect(
|
||||
auth.api.signInEmail({
|
||||
body: {
|
||||
email: testUser.email,
|
||||
password: testUser.password,
|
||||
},
|
||||
}),
|
||||
).rejects.toThrowError(
|
||||
new APIError("FORBIDDEN", {
|
||||
message: BASE_ERROR_CODES.EMAIL_NOT_VERIFIED,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(sendVerificationEmail).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -134,6 +134,22 @@ export const signUpEmail = <O extends BetterAuthOptions>() =>
|
||||
},
|
||||
},
|
||||
},
|
||||
"422": {
|
||||
description:
|
||||
"Unprocessable Entity. User already exists or failed to create user.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
message: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -689,6 +689,21 @@ export const changeEmail = createAuthEndpoint(
|
||||
},
|
||||
},
|
||||
},
|
||||
"422": {
|
||||
description: "Unprocessable Entity. Email already exists",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
message: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
106
packages/better-auth/src/client/lynx/index.ts
Normal file
106
packages/better-auth/src/client/lynx/index.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import { getClientConfig } from "../config";
|
||||
import type {
|
||||
BetterAuthClientPlugin,
|
||||
ClientOptions,
|
||||
InferActions,
|
||||
InferClientAPI,
|
||||
InferErrorCodes,
|
||||
IsSignal,
|
||||
} from "../types";
|
||||
import { createDynamicPathProxy } from "../proxy";
|
||||
import type { PrettifyDeep, UnionToIntersection } from "../../types/helper";
|
||||
import type {
|
||||
BetterFetchError,
|
||||
BetterFetchResponse,
|
||||
} from "@better-fetch/fetch";
|
||||
import { useStore } from "./lynx-store";
|
||||
import type { BASE_ERROR_CODES } from "../../error/codes";
|
||||
import type { SessionQueryParams } from "../types";
|
||||
|
||||
function getAtomKey(str: string) {
|
||||
return `use${capitalizeFirstLetter(str)}`;
|
||||
}
|
||||
|
||||
export function capitalizeFirstLetter(str: string) {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
}
|
||||
|
||||
type InferResolvedHooks<O extends ClientOptions> = O["plugins"] extends Array<
|
||||
infer Plugin
|
||||
>
|
||||
? Plugin extends BetterAuthClientPlugin
|
||||
? Plugin["getAtoms"] extends (fetch: any) => infer Atoms
|
||||
? Atoms extends Record<string, any>
|
||||
? {
|
||||
[key in keyof Atoms as IsSignal<key> extends true
|
||||
? never
|
||||
: key extends string
|
||||
? `use${Capitalize<key>}`
|
||||
: never]: () => ReturnType<Atoms[key]["get"]>;
|
||||
}
|
||||
: {}
|
||||
: {}
|
||||
: {}
|
||||
: {};
|
||||
|
||||
export function createAuthClient<Option extends ClientOptions>(
|
||||
options?: Option,
|
||||
) {
|
||||
const {
|
||||
pluginPathMethods,
|
||||
pluginsActions,
|
||||
pluginsAtoms,
|
||||
$fetch,
|
||||
$store,
|
||||
atomListeners,
|
||||
} = getClientConfig(options);
|
||||
let resolvedHooks: Record<string, any> = {};
|
||||
for (const [key, value] of Object.entries(pluginsAtoms)) {
|
||||
resolvedHooks[getAtomKey(key)] = () => useStore(value);
|
||||
}
|
||||
|
||||
const routes = {
|
||||
...pluginsActions,
|
||||
...resolvedHooks,
|
||||
$fetch,
|
||||
$store,
|
||||
};
|
||||
const proxy = createDynamicPathProxy(
|
||||
routes,
|
||||
$fetch,
|
||||
pluginPathMethods,
|
||||
pluginsAtoms,
|
||||
atomListeners,
|
||||
);
|
||||
|
||||
type ClientAPI = InferClientAPI<Option>;
|
||||
type Session = ClientAPI extends {
|
||||
getSession: () => Promise<infer Res>;
|
||||
}
|
||||
? Res extends BetterFetchResponse<infer S>
|
||||
? S
|
||||
: Res
|
||||
: never;
|
||||
return proxy as UnionToIntersection<InferResolvedHooks<Option>> &
|
||||
ClientAPI &
|
||||
InferActions<Option> & {
|
||||
useSession: () => {
|
||||
data: Session;
|
||||
isPending: boolean;
|
||||
error: BetterFetchError | null;
|
||||
refetch: (queryParams?: { query?: SessionQueryParams }) => void;
|
||||
};
|
||||
$Infer: {
|
||||
Session: NonNullable<Session>;
|
||||
};
|
||||
$fetch: typeof $fetch;
|
||||
$store: typeof $store;
|
||||
$ERROR_CODES: PrettifyDeep<
|
||||
InferErrorCodes<Option> & typeof BASE_ERROR_CODES
|
||||
>;
|
||||
};
|
||||
}
|
||||
|
||||
export { useStore };
|
||||
export type * from "@better-fetch/fetch";
|
||||
export type * from "nanostores";
|
||||
73
packages/better-auth/src/client/lynx/lynx-store.ts
Normal file
73
packages/better-auth/src/client/lynx/lynx-store.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { listenKeys } from "nanostores";
|
||||
import { useCallback, useRef, useSyncExternalStore } from "@lynx-js/react";
|
||||
import type { Store, StoreValue } from "nanostores";
|
||||
import type { DependencyList } from "@lynx-js/react";
|
||||
|
||||
type StoreKeys<T> = T extends { setKey: (k: infer K, v: any) => unknown }
|
||||
? K
|
||||
: never;
|
||||
|
||||
export interface UseStoreOptions<SomeStore> {
|
||||
/**
|
||||
* @default
|
||||
* ```ts
|
||||
* [store, options.keys]
|
||||
* ```
|
||||
*/
|
||||
deps?: DependencyList;
|
||||
|
||||
/**
|
||||
* Will re-render components only on specific key changes.
|
||||
*/
|
||||
keys?: StoreKeys<SomeStore>[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to store changes and get store's value.
|
||||
*
|
||||
* Can be used with store builder too.
|
||||
*
|
||||
* ```js
|
||||
* import { useStore } from 'nanostores/react'
|
||||
*
|
||||
* import { router } from '../store/router'
|
||||
*
|
||||
* export const Layout = () => {
|
||||
* let page = useStore(router)
|
||||
* if (page.route === 'home') {
|
||||
* return <HomePage />
|
||||
* } else {
|
||||
* return <Error404 />
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param store Store instance.
|
||||
* @returns Store value.
|
||||
*/
|
||||
export function useStore<SomeStore extends Store>(
|
||||
store: SomeStore,
|
||||
options: UseStoreOptions<SomeStore> = {},
|
||||
): StoreValue<SomeStore> {
|
||||
let snapshotRef = useRef<StoreValue<SomeStore>>(store.get());
|
||||
|
||||
const { keys, deps = [store, keys] } = options;
|
||||
|
||||
let subscribe = useCallback((onChange: () => void) => {
|
||||
const emitChange = (value: StoreValue<SomeStore>) => {
|
||||
if (snapshotRef.current === value) return;
|
||||
snapshotRef.current = value;
|
||||
onChange();
|
||||
};
|
||||
|
||||
emitChange(store.value);
|
||||
if (keys?.length) {
|
||||
return listenKeys(store as any, keys, emitChange);
|
||||
}
|
||||
return store.listen(emitChange);
|
||||
}, deps);
|
||||
|
||||
let get = () => snapshotRef.current as StoreValue<SomeStore>;
|
||||
|
||||
return useSyncExternalStore(subscribe, get, get);
|
||||
}
|
||||
@@ -25,7 +25,7 @@ export interface UseStoreOptions<SomeStore> {
|
||||
/**
|
||||
* Subscribe to store changes and get store's value.
|
||||
*
|
||||
* Can be user with store builder too.
|
||||
* Can be used with store builder too.
|
||||
*
|
||||
* ```js
|
||||
* import { useStore } from 'nanostores/react'
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { createHash } from "@better-auth/utils/hash";
|
||||
import { xchacha20poly1305 } from "@noble/ciphers/chacha";
|
||||
import { bytesToHex, hexToBytes, utf8ToBytes } from "@noble/ciphers/utils";
|
||||
import { managedNonce } from "@noble/ciphers/webcrypto";
|
||||
import { xchacha20poly1305 } from "@noble/ciphers/chacha.js";
|
||||
import {
|
||||
bytesToHex,
|
||||
hexToBytes,
|
||||
utf8ToBytes,
|
||||
managedNonce,
|
||||
} from "@noble/ciphers/utils.js";
|
||||
|
||||
export type SymmetricEncryptOptions = {
|
||||
key: string;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { constantTimeEqual } from "./buffer";
|
||||
import { scryptAsync } from "@noble/hashes/scrypt";
|
||||
import { scryptAsync } from "@noble/hashes/scrypt.js";
|
||||
import { getRandomValues } from "@better-auth/utils";
|
||||
import { hex } from "@better-auth/utils/hex";
|
||||
import { hexToBytes } from "@noble/hashes/utils";
|
||||
import { hexToBytes } from "@noble/hashes/utils.js";
|
||||
import { BetterAuthError } from "../error";
|
||||
|
||||
const config = {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { APIError, createAuthEndpoint, sessionMiddleware } from "../../../api";
|
||||
import { createAuthEndpoint, sessionMiddleware } from "../../../api";
|
||||
import type { apiKeySchema } from "../schema";
|
||||
import type { ApiKey } from "../types";
|
||||
import type { AuthContext } from "../../../types";
|
||||
|
||||
@@ -9,7 +9,6 @@ import type { PredefinedApiKeyOptions } from ".";
|
||||
import { safeJSONParse } from "../../../utils/json";
|
||||
import { role } from "../../access";
|
||||
import { defaultKeyHasher } from "../";
|
||||
import { createApiKey } from "./create-api-key";
|
||||
|
||||
export async function validateApiKey({
|
||||
hashedKey,
|
||||
|
||||
@@ -93,7 +93,9 @@ describe("Custom Session Plugin Tests", async () => {
|
||||
expect(session.newData).toEqual({ message: "Hello, World!" });
|
||||
});
|
||||
|
||||
it("should not create memory leaks with multiple plugin instances", async () => {
|
||||
it.skipIf(globalThis.gc == null)(
|
||||
"should not create memory leaks with multiple plugin instances",
|
||||
async () => {
|
||||
const initialMemory = process.memoryUsage();
|
||||
|
||||
const pluginInstances = [];
|
||||
@@ -112,11 +114,9 @@ describe("Custom Session Plugin Tests", async () => {
|
||||
});
|
||||
pluginInstances.push(plugin);
|
||||
}
|
||||
|
||||
// Force garbage collection if available (in test environment)
|
||||
if (global.gc) {
|
||||
global.gc();
|
||||
}
|
||||
// Force garbage collection (only works if Node.js is started with --expose-gc)
|
||||
// @ts-expect-error
|
||||
globalThis.gc();
|
||||
|
||||
const afterPluginCreation = process.memoryUsage();
|
||||
|
||||
@@ -130,5 +130,6 @@ describe("Custom Session Plugin Tests", async () => {
|
||||
expect(pluginInstances).toHaveLength(sessionCount);
|
||||
expect(pluginInstances[0].id).toBe("custom-session");
|
||||
expect(pluginInstances[sessionCount - 1].id).toBe("custom-session");
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -99,6 +99,7 @@ export async function getJwtToken(
|
||||
return await signJWT(ctx, {
|
||||
options,
|
||||
payload: {
|
||||
iat: Math.floor(Date.now() / 1000),
|
||||
...payload,
|
||||
sub:
|
||||
(await options?.jwt?.getSubject?.(ctx.context.session!)) ??
|
||||
|
||||
@@ -28,6 +28,7 @@ import { logger } from "../../utils";
|
||||
|
||||
interface MCPOptions {
|
||||
loginPage: string;
|
||||
resource?: string;
|
||||
oidcConfig?: OIDCOptions;
|
||||
}
|
||||
|
||||
@@ -85,15 +86,15 @@ export const getMCPProviderMetadata = (
|
||||
|
||||
export const getMCPProtectedResourceMetadata = (
|
||||
ctx: GenericEndpointContext,
|
||||
options?: OIDCOptions,
|
||||
options?: MCPOptions,
|
||||
) => {
|
||||
const baseURL = ctx.context.baseURL;
|
||||
|
||||
return {
|
||||
resource: baseURL,
|
||||
resource: options?.resource ?? new URL(baseURL).origin,
|
||||
authorization_servers: [baseURL],
|
||||
jwks_uri: options?.metadata?.jwks_uri ?? `${baseURL}/mcp/jwks`,
|
||||
scopes_supported: options?.metadata?.scopes_supported ?? [
|
||||
jwks_uri: options?.oidcConfig?.metadata?.jwks_uri ?? `${baseURL}/mcp/jwks`,
|
||||
scopes_supported: options?.oidcConfig?.metadata?.scopes_supported ?? [
|
||||
"openid",
|
||||
"profile",
|
||||
"email",
|
||||
@@ -946,7 +947,7 @@ export const withMcpAuth = <
|
||||
const session = await auth.api.getMcpSession({
|
||||
headers: req.headers,
|
||||
});
|
||||
const wwwAuthenticateValue = `Bearer resource_metadata=${baseURL}/api/auth/.well-known/oauth-protected-resource`;
|
||||
const wwwAuthenticateValue = `Bearer resource_metadata="${baseURL}/.well-known/oauth-protected-resource"`;
|
||||
if (!session) {
|
||||
return Response.json(
|
||||
{
|
||||
@@ -962,6 +963,8 @@ export const withMcpAuth = <
|
||||
status: 401,
|
||||
headers: {
|
||||
"WWW-Authenticate": wwwAuthenticateValue,
|
||||
// we also add this headers otherwise browser based clients will not be able to read the `www-authenticate` header
|
||||
"Access-Control-Expose-Headers": "WWW-Authenticate",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { afterAll, describe, it } from "vitest";
|
||||
import { getTestInstance } from "../../test-utils/test-instance";
|
||||
import { mcp } from ".";
|
||||
import { mcp, withMcpAuth } from ".";
|
||||
import { genericOAuth } from "../generic-oauth";
|
||||
import type { Client } from "../oidc-provider/types";
|
||||
import { createAuthClient } from "../../client";
|
||||
@@ -354,6 +354,21 @@ describe("mcp", async () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should expose OAuth protected resource metadata", async ({ expect }) => {
|
||||
const metadata = await serverClient.$fetch(
|
||||
"/.well-known/oauth-protected-resource",
|
||||
);
|
||||
|
||||
expect(metadata.data).toMatchObject({
|
||||
resource: baseURL,
|
||||
authorization_servers: [`${baseURL}/api/auth`],
|
||||
jwks_uri: `${baseURL}/api/auth/mcp/jwks`,
|
||||
scopes_supported: ["openid", "profile", "email", "offline_access"],
|
||||
bearer_methods_supported: ["header"],
|
||||
resource_signing_alg_values_supported: ["RS256", "none"],
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle token refresh flow", async ({ expect }) => {
|
||||
// Create a confidential client for easier testing (avoids PKCE complexity)
|
||||
const createdClient = await serverClient.$fetch("/mcp/register", {
|
||||
@@ -511,4 +526,24 @@ describe("mcp", async () => {
|
||||
"code verifier is missing",
|
||||
);
|
||||
});
|
||||
|
||||
describe("withMCPAuth", () => {
|
||||
it("should return 401 if the request is not authenticated returning the right WWW-Authenticate header", async ({
|
||||
expect,
|
||||
}) => {
|
||||
// Test the handler using a newly instantiated Request instead of the server, since this route isn't handled by the server
|
||||
const response = await withMcpAuth(auth, async () => {
|
||||
// it will never be reached since the request is not authenticated
|
||||
return new Response("unnecessary");
|
||||
})(new Request(`${baseURL}/mcp`));
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
expect(response.headers.get("WWW-Authenticate")).toBe(
|
||||
`Bearer resource_metadata="${baseURL}/api/auth/.well-known/oauth-protected-resource"`,
|
||||
);
|
||||
expect(response.headers.get("Access-Control-Expose-Headers")).toBe(
|
||||
"WWW-Authenticate",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,16 +4,7 @@ import type {
|
||||
OpenAPIParameter,
|
||||
OpenAPISchemaType,
|
||||
} from "better-call";
|
||||
import {
|
||||
z,
|
||||
ZodArray,
|
||||
ZodBoolean,
|
||||
ZodNumber,
|
||||
ZodObject,
|
||||
ZodOptional,
|
||||
ZodString,
|
||||
ZodType,
|
||||
} from "zod/v4";
|
||||
import { z, ZodObject, ZodOptional, ZodType } from "zod/v4";
|
||||
import { getEndpoints } from "../../api";
|
||||
import { getAuthTables } from "../../db";
|
||||
import type { AuthContext, BetterAuthOptions } from "../../types";
|
||||
@@ -117,13 +108,12 @@ function getParameters(options: EndpointOptions) {
|
||||
name: key,
|
||||
in: "query",
|
||||
schema: {
|
||||
type: getTypeFromZodType(value as ZodType<any>),
|
||||
...processZodType(value as ZodType<any>),
|
||||
...("minLength" in value && (value as any).minLength
|
||||
? {
|
||||
minLength: (value as any).minLength as number,
|
||||
}
|
||||
: {}),
|
||||
description: (value as any).description,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -148,10 +138,7 @@ function getRequestBody(options: EndpointOptions): any {
|
||||
const required: string[] = [];
|
||||
Object.entries(shape).forEach(([key, value]) => {
|
||||
if (value instanceof ZodType) {
|
||||
properties[key] = {
|
||||
type: getTypeFromZodType(value as ZodType<any>),
|
||||
description: (value as any).description,
|
||||
};
|
||||
properties[key] = processZodType(value as ZodType<any>);
|
||||
if (!(value instanceof z.ZodOptional)) {
|
||||
required.push(key);
|
||||
}
|
||||
@@ -178,6 +165,48 @@ function getRequestBody(options: EndpointOptions): any {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function processZodType(zodType: ZodType<any>): any {
|
||||
// optional unwrapping
|
||||
if (zodType instanceof ZodOptional) {
|
||||
const innerType = (zodType as any)._def.innerType;
|
||||
const innerSchema = processZodType(innerType);
|
||||
return {
|
||||
...innerSchema,
|
||||
nullable: true,
|
||||
};
|
||||
}
|
||||
// object unwrapping
|
||||
if (zodType instanceof ZodObject) {
|
||||
const shape = (zodType as any).shape;
|
||||
if (shape) {
|
||||
const properties: Record<string, any> = {};
|
||||
const required: string[] = [];
|
||||
Object.entries(shape).forEach(([key, value]) => {
|
||||
if (value instanceof ZodType) {
|
||||
properties[key] = processZodType(value as ZodType<any>);
|
||||
if (!(value instanceof z.ZodOptional)) {
|
||||
required.push(key);
|
||||
}
|
||||
}
|
||||
});
|
||||
return {
|
||||
type: "object",
|
||||
properties,
|
||||
...(required.length > 0 ? { required } : {}),
|
||||
description: (zodType as any).description,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// For primitive types, get the correct type from the unwrapped ZodType
|
||||
const baseSchema = {
|
||||
type: getTypeFromZodType(zodType),
|
||||
description: (zodType as any).description,
|
||||
};
|
||||
|
||||
return baseSchema;
|
||||
}
|
||||
|
||||
function getResponse(responses?: Record<string, any>) {
|
||||
return {
|
||||
"400": {
|
||||
|
||||
@@ -54,4 +54,35 @@ describe("open-api", async (it) => {
|
||||
expect(schemas["User"].required).toContain("role");
|
||||
expect(schemas["User"].required).not.toContain("preferences");
|
||||
});
|
||||
|
||||
it("should properly handle nested objects in request body schema", async () => {
|
||||
const schema = await auth.api.generateOpenAPISchema();
|
||||
const paths = schema.paths as Record<string, any>;
|
||||
|
||||
const signInSocialPath = paths["/sign-in/social"];
|
||||
expect(signInSocialPath).toBeDefined();
|
||||
|
||||
const requestBody = signInSocialPath.post.requestBody;
|
||||
expect(requestBody).toBeDefined();
|
||||
|
||||
const schema_properties =
|
||||
requestBody.content["application/json"].schema.properties;
|
||||
expect(schema_properties.idToken).toBeDefined();
|
||||
expect(schema_properties.idToken.type).toBe("object");
|
||||
expect(schema_properties.idToken.properties).toBeDefined();
|
||||
expect(schema_properties.idToken.properties.token).toBeDefined();
|
||||
expect(schema_properties.idToken.properties.token.type).toBe("string");
|
||||
expect(schema_properties.idToken.properties.accessToken).toBeDefined();
|
||||
expect(schema_properties.idToken.properties.accessToken.type).toBe(
|
||||
"string",
|
||||
);
|
||||
expect(schema_properties.idToken.properties.refreshToken).toBeDefined();
|
||||
expect(schema_properties.idToken.properties.refreshToken.type).toBe(
|
||||
"string",
|
||||
);
|
||||
|
||||
expect(schema_properties.idToken.required).toContain("token");
|
||||
expect(schema_properties.idToken.required).not.toContain("accessToken");
|
||||
expect(schema_properties.idToken.required).not.toContain("refreshToken");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -215,6 +215,7 @@ export const organizationClient = <CO extends OrganizationClientOptions>(
|
||||
},
|
||||
pathMethods: {
|
||||
"/organization/get-full-organization": "GET",
|
||||
"/organization/list-user-teams": "GET",
|
||||
},
|
||||
atomListeners: [
|
||||
{
|
||||
|
||||
@@ -491,7 +491,7 @@ export const organization = <O extends OrganizationOptions>(options?: O) => {
|
||||
/**
|
||||
* ### Endpoint
|
||||
*
|
||||
* POST `/organization/list-user-teams`
|
||||
* GET `/organization/list-user-teams`
|
||||
*
|
||||
* ### API Methods
|
||||
*
|
||||
|
||||
@@ -201,6 +201,21 @@ export const username = (options?: UsernameOptions) => {
|
||||
},
|
||||
},
|
||||
},
|
||||
422: {
|
||||
description: "Unprocessable Entity. Validation error",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
message: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { OAuthProvider, ProviderOptions } from "../oauth2";
|
||||
import { createAuthorizationURL } from "../oauth2";
|
||||
import { logger } from "../utils/logger";
|
||||
import { decodeJwt } from "jose";
|
||||
import { base64 } from "@better-auth/utils/base64";
|
||||
|
||||
export interface PayPalProfile {
|
||||
user_id: string;
|
||||
@@ -108,9 +109,9 @@ export const paypal = (options: PayPalOptions) => {
|
||||
* PayPal requires Basic Auth for token exchange
|
||||
**/
|
||||
|
||||
const credentials = Buffer.from(
|
||||
const credentials = base64.encode(
|
||||
`${options.clientId}:${options.clientSecret}`,
|
||||
).toString("base64");
|
||||
);
|
||||
|
||||
try {
|
||||
const response = await betterFetch(tokenEndpoint, {
|
||||
@@ -153,9 +154,9 @@ export const paypal = (options: PayPalOptions) => {
|
||||
refreshAccessToken: options.refreshAccessToken
|
||||
? options.refreshAccessToken
|
||||
: async (refreshToken) => {
|
||||
const credentials = Buffer.from(
|
||||
const credentials = base64.encode(
|
||||
`${options.clientId}:${options.clientSecret}`,
|
||||
).toString("base64");
|
||||
);
|
||||
|
||||
try {
|
||||
const response = await betterFetch(tokenEndpoint, {
|
||||
|
||||
@@ -161,10 +161,13 @@ export const tiktok = (options: TiktokOptions) => {
|
||||
return refreshAccessToken({
|
||||
refreshToken,
|
||||
options: {
|
||||
clientKey: options.clientKey,
|
||||
clientSecret: options.clientSecret,
|
||||
},
|
||||
tokenEndpoint: "https://open.tiktokapis.com/v2/oauth/token/",
|
||||
authentication: "post",
|
||||
extraParams: {
|
||||
client_key: options.clientKey,
|
||||
},
|
||||
});
|
||||
},
|
||||
async getUserInfo(token) {
|
||||
|
||||
@@ -13,6 +13,7 @@ export type Where = {
|
||||
| "gt"
|
||||
| "gte"
|
||||
| "in"
|
||||
| "not_in"
|
||||
| "contains"
|
||||
| "starts_with"
|
||||
| "ends_with"; //eq by default
|
||||
|
||||
@@ -609,12 +609,17 @@ export type BetterAuthOptions = {
|
||||
*/
|
||||
max: number;
|
||||
}
|
||||
| false
|
||||
| ((request: Request) =>
|
||||
| { window: number; max: number }
|
||||
| Promise<{
|
||||
| false
|
||||
| Promise<
|
||||
| {
|
||||
window: number;
|
||||
max: number;
|
||||
}>);
|
||||
}
|
||||
| false
|
||||
>);
|
||||
};
|
||||
/**
|
||||
* Storage configuration
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { keccak_256 } from "@noble/hashes/sha3";
|
||||
import { keccak_256 } from "@noble/hashes/sha3.js";
|
||||
import { utf8ToBytes } from "@noble/hashes/utils.js";
|
||||
|
||||
/**
|
||||
* TS implementation of ERC-55 ("Mixed-case checksum address encoding") using @noble/hashes
|
||||
@@ -8,7 +9,7 @@ import { keccak_256 } from "@noble/hashes/sha3";
|
||||
export function toChecksumAddress(address: string) {
|
||||
address = address.toLowerCase().replace("0x", "");
|
||||
// Hash the address (treat it as UTF-8) and return as a hex string
|
||||
const hash = [...keccak_256(address)]
|
||||
const hash = [...keccak_256(utf8ToBytes(address))]
|
||||
.map((v) => v.toString(16).padStart(2, "0"))
|
||||
.join("");
|
||||
let ret = "0x";
|
||||
|
||||
11
packages/better-auth/vitest.config.ts
Normal file
11
packages/better-auth/vitest.config.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
poolOptions: {
|
||||
forks: {
|
||||
execArgv: ["--expose-gc"],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@better-auth/cli",
|
||||
"version": "1.3.8",
|
||||
"version": "1.3.9-beta.4",
|
||||
"description": "The CLI for Better Auth",
|
||||
"module": "dist/index.mjs",
|
||||
"repository": {
|
||||
@@ -49,7 +49,7 @@
|
||||
"better-auth": "workspace:*",
|
||||
"better-sqlite3": "^12.2.0",
|
||||
"c12": "^3.2.0",
|
||||
"chalk": "^5.6.0",
|
||||
"chalk": "^5.6.2",
|
||||
"commander": "^12.1.0",
|
||||
"dotenv": "^16.6.1",
|
||||
"drizzle-orm": "^0.33.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@better-auth/expo",
|
||||
"version": "1.3.8",
|
||||
"version": "1.3.9-beta.4",
|
||||
"description": "",
|
||||
"main": "dist/index.cjs",
|
||||
"module": "dist/index.mjs",
|
||||
|
||||
@@ -202,7 +202,8 @@ export const expoClient = (opts: ExpoClientOptions) => {
|
||||
|
||||
if (
|
||||
context.data?.redirect &&
|
||||
context.request.url.toString().includes("/sign-in") &&
|
||||
(context.request.url.toString().includes("/sign-in") ||
|
||||
context.request.url.toString().includes("/link-social")) &&
|
||||
!context.request?.body.includes("idToken") // id token is used for silent sign-in
|
||||
) {
|
||||
const callbackURL = JSON.parse(context.request.body)?.callbackURL;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@better-auth/sso",
|
||||
"author": "Bereket Engida",
|
||||
"version": "1.3.8",
|
||||
"version": "1.3.9-beta.4",
|
||||
"main": "dist/index.cjs",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@better-auth/stripe",
|
||||
"author": "Bereket Engida",
|
||||
"version": "1.3.8",
|
||||
"version": "1.3.9-beta.4",
|
||||
"main": "dist/index.cjs",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
|
||||
@@ -265,6 +265,11 @@ export const stripe = <O extends StripeOptions>(options: O) => {
|
||||
},
|
||||
],
|
||||
})
|
||||
: referenceId
|
||||
? await ctx.context.adapter.findOne<Subscription>({
|
||||
model: "subscription",
|
||||
where: [{ field: "referenceId", value: referenceId }],
|
||||
})
|
||||
: null;
|
||||
|
||||
if (ctx.body.subscriptionId && !subscriptionToUpdate) {
|
||||
@@ -329,12 +334,14 @@ export const stripe = <O extends StripeOptions>(options: O) => {
|
||||
(sub) => sub.status === "active" || sub.status === "trialing",
|
||||
),
|
||||
);
|
||||
|
||||
const activeSubscription = activeSubscriptions.find((sub) =>
|
||||
subscriptionToUpdate?.stripeSubscriptionId || ctx.body.subscriptionId
|
||||
? sub.id === subscriptionToUpdate?.stripeSubscriptionId ||
|
||||
sub.id === ctx.body.subscriptionId
|
||||
: true,
|
||||
: false,
|
||||
);
|
||||
|
||||
const subscriptions = subscriptionToUpdate
|
||||
? [subscriptionToUpdate]
|
||||
: await ctx.context.adapter.findMany<Subscription>({
|
||||
@@ -431,12 +438,16 @@ export const stripe = <O extends StripeOptions>(options: O) => {
|
||||
ctx,
|
||||
);
|
||||
|
||||
const alreadyHasTrial = subscription.status === "trialing";
|
||||
const hasEverTrialed = subscriptions.some((s) => {
|
||||
const samePlan = s.plan?.toLowerCase() === plan.name.toLowerCase();
|
||||
const hadTrial =
|
||||
!!(s.trialStart || s.trialEnd) || s.status === "trialing";
|
||||
return samePlan && hadTrial;
|
||||
});
|
||||
|
||||
const freeTrial =
|
||||
!alreadyHasTrial && plan.freeTrial
|
||||
? {
|
||||
trial_period_days: plan.freeTrial.days,
|
||||
}
|
||||
!hasEverTrialed && plan.freeTrial
|
||||
? { trial_period_days: plan.freeTrial.days }
|
||||
: undefined;
|
||||
|
||||
let priceIdToUse: string | undefined = undefined;
|
||||
|
||||
@@ -994,4 +994,112 @@ describe("stripe", async () => {
|
||||
return_url: "http://localhost:3000/dashboard",
|
||||
});
|
||||
});
|
||||
|
||||
it("should not update personal subscription when upgrading with an org referenceId", async () => {
|
||||
const orgId = "org_b67GF32Cljh7u588AuEblmLVobclDRcP";
|
||||
|
||||
const testOptions = {
|
||||
...stripeOptions,
|
||||
stripeClient: _stripe,
|
||||
subscription: {
|
||||
...stripeOptions.subscription,
|
||||
authorizeReference: async () => true,
|
||||
},
|
||||
} as unknown as StripeOptions;
|
||||
|
||||
const testAuth = betterAuth({
|
||||
baseURL: "http://localhost:3000",
|
||||
database: memory,
|
||||
emailAndPassword: { enabled: true },
|
||||
plugins: [stripe(testOptions)],
|
||||
});
|
||||
const testCtx = await testAuth.$context;
|
||||
|
||||
const testAuthClient = createAuthClient({
|
||||
baseURL: "http://localhost:3000",
|
||||
plugins: [bearer(), stripeClient({ subscription: true })],
|
||||
fetchOptions: {
|
||||
customFetchImpl: async (url, init) =>
|
||||
testAuth.handler(new Request(url, init)),
|
||||
},
|
||||
});
|
||||
|
||||
// Sign up and sign in the user
|
||||
const userRes = await testAuthClient.signUp.email(
|
||||
{ ...testUser, email: "org-ref@email.com" },
|
||||
{ throw: true },
|
||||
);
|
||||
const headers = new Headers();
|
||||
await testAuthClient.signIn.email(
|
||||
{ ...testUser, email: "org-ref@email.com" },
|
||||
{ throw: true, onSuccess: setCookieToHeader(headers) },
|
||||
);
|
||||
|
||||
// Create a personal subscription (referenceId = user id)
|
||||
await testAuthClient.subscription.upgrade({
|
||||
plan: "starter",
|
||||
fetchOptions: { headers },
|
||||
});
|
||||
|
||||
const personalSub = await testCtx.adapter.findOne<Subscription>({
|
||||
model: "subscription",
|
||||
where: [{ field: "referenceId", value: userRes.user.id }],
|
||||
});
|
||||
expect(personalSub).toBeTruthy();
|
||||
|
||||
await testCtx.adapter.update({
|
||||
model: "subscription",
|
||||
update: {
|
||||
status: "active",
|
||||
stripeSubscriptionId: "sub_personal_active_123",
|
||||
},
|
||||
where: [{ field: "id", value: personalSub!.id }],
|
||||
});
|
||||
|
||||
mockStripe.subscriptions.list.mockResolvedValue({
|
||||
data: [
|
||||
{
|
||||
id: "sub_personal_active_123",
|
||||
status: "active",
|
||||
items: {
|
||||
data: [
|
||||
{
|
||||
id: "si_1",
|
||||
price: { id: process.env.STRIPE_PRICE_ID_1 },
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Attempt to upgrade using an org referenceId
|
||||
const upgradeRes = await testAuthClient.subscription.upgrade({
|
||||
plan: "starter",
|
||||
referenceId: orgId,
|
||||
fetchOptions: { headers },
|
||||
});
|
||||
console.log(upgradeRes);
|
||||
|
||||
// // It should NOT go through billing portal (which would update the personal sub)
|
||||
expect(mockStripe.billingPortal.sessions.create).not.toHaveBeenCalled();
|
||||
expect(upgradeRes.data?.url).toBeDefined();
|
||||
|
||||
const orgSub = await testCtx.adapter.findOne<Subscription>({
|
||||
model: "subscription",
|
||||
where: [{ field: "referenceId", value: orgId }],
|
||||
});
|
||||
expect(orgSub).toMatchObject({
|
||||
referenceId: orgId,
|
||||
status: "incomplete",
|
||||
plan: "starter",
|
||||
});
|
||||
|
||||
const personalAfter = await testCtx.adapter.findOne<Subscription>({
|
||||
model: "subscription",
|
||||
where: [{ field: "id", value: personalSub!.id }],
|
||||
});
|
||||
expect(personalAfter?.status).toBe("active");
|
||||
});
|
||||
});
|
||||
|
||||
82
pnpm-lock.yaml
generated
82
pnpm-lock.yaml
generated
@@ -10,8 +10,8 @@ catalogs:
|
||||
specifier: ^1.1.18
|
||||
version: 1.1.18
|
||||
better-call:
|
||||
specifier: 1.0.16
|
||||
version: 1.0.16
|
||||
specifier: 1.0.18
|
||||
version: 1.0.18
|
||||
typescript:
|
||||
specifier: ^5.9.2
|
||||
version: 5.9.2
|
||||
@@ -179,7 +179,7 @@ importers:
|
||||
version: link:../../packages/better-auth
|
||||
better-call:
|
||||
specifier: 'catalog:'
|
||||
version: 1.0.16
|
||||
version: 1.0.18
|
||||
better-sqlite3:
|
||||
specifier: ^12.2.0
|
||||
version: 12.2.0
|
||||
@@ -613,11 +613,11 @@ importers:
|
||||
specifier: 'catalog:'
|
||||
version: 1.1.18
|
||||
'@noble/ciphers':
|
||||
specifier: ^0.6.0
|
||||
version: 0.6.0
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.0
|
||||
'@noble/hashes':
|
||||
specifier: ^1.8.0
|
||||
version: 1.8.0
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.0
|
||||
'@simplewebauthn/browser':
|
||||
specifier: ^13.1.2
|
||||
version: 13.1.2
|
||||
@@ -626,13 +626,13 @@ importers:
|
||||
version: 13.1.2
|
||||
better-call:
|
||||
specifier: 'catalog:'
|
||||
version: 1.0.16
|
||||
version: 1.0.18
|
||||
defu:
|
||||
specifier: ^6.1.4
|
||||
version: 6.1.4
|
||||
jose:
|
||||
specifier: ^5.10.0
|
||||
version: 5.10.0
|
||||
specifier: ^6.1.0
|
||||
version: 6.1.0
|
||||
kysely:
|
||||
specifier: ^0.28.5
|
||||
version: 0.28.5
|
||||
@@ -643,6 +643,9 @@ importers:
|
||||
specifier: ^4.1.5
|
||||
version: 4.1.5
|
||||
devDependencies:
|
||||
'@lynx-js/react':
|
||||
specifier: ^0.112.5
|
||||
version: 0.112.5(@types/react@18.3.23)
|
||||
'@prisma/client':
|
||||
specifier: ^5.22.0
|
||||
version: 5.22.0(prisma@5.22.0)
|
||||
@@ -773,8 +776,8 @@ importers:
|
||||
specifier: ^3.2.0
|
||||
version: 3.2.0(magicast@0.3.5)
|
||||
chalk:
|
||||
specifier: ^5.6.0
|
||||
version: 5.6.0
|
||||
specifier: ^5.6.2
|
||||
version: 5.6.2
|
||||
commander:
|
||||
specifier: ^12.1.0
|
||||
version: 12.1.0
|
||||
@@ -885,7 +888,7 @@ importers:
|
||||
version: link:../better-auth
|
||||
better-call:
|
||||
specifier: 'catalog:'
|
||||
version: 1.0.16
|
||||
version: 1.0.18
|
||||
express:
|
||||
specifier: ^5.1.0
|
||||
version: 5.1.0
|
||||
@@ -901,7 +904,7 @@ importers:
|
||||
version: link:../better-auth
|
||||
better-call:
|
||||
specifier: 'catalog:'
|
||||
version: 1.0.16
|
||||
version: 1.0.18
|
||||
stripe:
|
||||
specifier: ^18.5.0
|
||||
version: 18.5.0(@types/node@24.3.0)
|
||||
@@ -2459,6 +2462,9 @@ packages:
|
||||
'@hexagon/base64@1.1.28':
|
||||
resolution: {integrity: sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==}
|
||||
|
||||
'@hongzhiyuan/preact@10.24.0-00213bad':
|
||||
resolution: {integrity: sha512-bHWp4ZDK5ZimcY+bTWw3S3xGiB8eROpZj0RK3FClNIaTOajb0b11CsT3K+pdeakgPgq1jWN3T2e2rfrPm40JsQ==}
|
||||
|
||||
'@hookform/resolvers@5.2.1':
|
||||
resolution: {integrity: sha512-u0+6X58gkjMcxur1wRWokA7XsiiBJ6aK17aPZxhkoYiK5J+HcTx0Vhu9ovXe6H+dVpO6cjrn2FkJTryXEMlryQ==}
|
||||
peerDependencies:
|
||||
@@ -2908,6 +2914,15 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@lynx-js/react@0.112.5':
|
||||
resolution: {integrity: sha512-noFV05FjKsLtnTQLjEbjK4IPLPK+AEJ3r2klYjvLCPe9pQYXyHqoiN9bWwLFZDvcPGsXSdJ5TiahTTZuagGRjA==}
|
||||
peerDependencies:
|
||||
'@lynx-js/types': '*'
|
||||
'@types/react': ^18
|
||||
peerDependenciesMeta:
|
||||
'@lynx-js/types':
|
||||
optional: true
|
||||
|
||||
'@mapbox/node-pre-gyp@2.0.0':
|
||||
resolution: {integrity: sha512-llMXd39jtP0HpQLVI37Bf1m2ADlEb35GYSh1SDSLsBhR+5iCxiNGlT31yqbNtVHygHAtMy6dWFERpU2JgufhPg==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -3031,13 +3046,18 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@noble/ciphers@0.6.0':
|
||||
resolution: {integrity: sha512-mIbq/R9QXk5/cTfESb1OKtyFnk7oc1Om/8onA1158K9/OZUQFDEVy55jVTato+xmp3XX6F6Qh0zz0Nc1AxAlRQ==}
|
||||
'@noble/ciphers@2.0.0':
|
||||
resolution: {integrity: sha512-j/l6jpnpaIBM87cAYPJzi/6TgqmBv9spkqPyCXvRYsu5uxqh6tPJZDnD85yo8VWqzTuTQPgfv7NgT63u7kbwAQ==}
|
||||
engines: {node: '>= 20.19.0'}
|
||||
|
||||
'@noble/hashes@1.8.0':
|
||||
resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==}
|
||||
engines: {node: ^14.21.3 || >=16}
|
||||
|
||||
'@noble/hashes@2.0.0':
|
||||
resolution: {integrity: sha512-h8VUBlE8R42+XIDO229cgisD287im3kdY6nbNZJFjc6ZvKIXPYXe6Vc/t+kyjFdMFyt5JpapzTsEg8n63w5/lw==}
|
||||
engines: {node: '>= 20.19.0'}
|
||||
|
||||
'@nodelib/fs.scandir@2.1.5':
|
||||
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
||||
engines: {node: '>= 8'}
|
||||
@@ -5783,8 +5803,8 @@ packages:
|
||||
resolution: {integrity: sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
better-call@1.0.16:
|
||||
resolution: {integrity: sha512-42dgJ1rOtc0anOoxjXPOWuel/Z/4aeO7EJ2SiXNwvlkySSgjXhNjAjTMWa8DL1nt6EXS3jl3VKC3mPsU/lUgVA==}
|
||||
better-call@1.0.18:
|
||||
resolution: {integrity: sha512-Ojyck3P3fs/egBmCW50tvfbCJorNV5KphfPOKrkCxPfOr8Brth1ruDtAJuhHVHEUiWrXv+vpEgWQk7m7FzhbbQ==}
|
||||
|
||||
better-opn@3.0.2:
|
||||
resolution: {integrity: sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ==}
|
||||
@@ -5986,8 +6006,8 @@ packages:
|
||||
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
chalk@5.6.0:
|
||||
resolution: {integrity: sha512-46QrSQFyVSEyYAgQ22hQ+zDa60YHA4fBstHmtSApj1Y5vKtG27fWowW03jCk5KcbXEWPZUIR894aARCA/G1kfQ==}
|
||||
chalk@5.6.2:
|
||||
resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==}
|
||||
engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
|
||||
|
||||
character-entities-html4@2.1.0:
|
||||
@@ -8489,6 +8509,9 @@ packages:
|
||||
jose@5.10.0:
|
||||
resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==}
|
||||
|
||||
jose@6.1.0:
|
||||
resolution: {integrity: sha512-TTQJyoEoKcC1lscpVDCSsVgYzUDg/0Bt3WE//WiTPK6uOCQC2KZS4MpugbMWt/zyjkopgZoXhZuCi00gLudfUA==}
|
||||
|
||||
jotai@2.13.1:
|
||||
resolution: {integrity: sha512-cRsw6kFeGC9Z/D3egVKrTXRweycZ4z/k7i2MrfCzPYsL9SIWcPXTyqv258/+Ay8VUEcihNiE/coBLE6Kic6b8A==}
|
||||
engines: {node: '>=12.20.0'}
|
||||
@@ -14136,6 +14159,8 @@ snapshots:
|
||||
|
||||
'@hexagon/base64@1.1.28': {}
|
||||
|
||||
'@hongzhiyuan/preact@10.24.0-00213bad': {}
|
||||
|
||||
'@hookform/resolvers@5.2.1(react-hook-form@7.62.0(react@19.1.1))':
|
||||
dependencies:
|
||||
'@standard-schema/utils': 0.3.0
|
||||
@@ -14552,6 +14577,11 @@ snapshots:
|
||||
'@libsql/win32-x64-msvc@0.5.20':
|
||||
optional: true
|
||||
|
||||
'@lynx-js/react@0.112.5(@types/react@18.3.23)':
|
||||
dependencies:
|
||||
'@types/react': 18.3.23
|
||||
preact: '@hongzhiyuan/preact@10.24.0-00213bad'
|
||||
|
||||
'@mapbox/node-pre-gyp@2.0.0':
|
||||
dependencies:
|
||||
consola: 3.4.2
|
||||
@@ -14759,10 +14789,12 @@ snapshots:
|
||||
'@next/swc-win32-x64-msvc@15.5.2':
|
||||
optional: true
|
||||
|
||||
'@noble/ciphers@0.6.0': {}
|
||||
'@noble/ciphers@2.0.0': {}
|
||||
|
||||
'@noble/hashes@1.8.0': {}
|
||||
|
||||
'@noble/hashes@2.0.0': {}
|
||||
|
||||
'@nodelib/fs.scandir@2.1.5':
|
||||
dependencies:
|
||||
'@nodelib/fs.stat': 2.0.5
|
||||
@@ -16213,7 +16245,9 @@ snapshots:
|
||||
metro-runtime: 0.83.1
|
||||
transitivePeerDependencies:
|
||||
- '@babel/core'
|
||||
- bufferutil
|
||||
- supports-color
|
||||
- utf-8-validate
|
||||
|
||||
'@react-native/normalize-colors@0.79.5': {}
|
||||
|
||||
@@ -18098,7 +18132,7 @@ snapshots:
|
||||
dependencies:
|
||||
safe-buffer: 5.1.2
|
||||
|
||||
better-call@1.0.16:
|
||||
better-call@1.0.18:
|
||||
dependencies:
|
||||
'@better-fetch/fetch': 1.1.18
|
||||
rou3: 0.5.1
|
||||
@@ -18357,7 +18391,7 @@ snapshots:
|
||||
ansi-styles: 4.3.0
|
||||
supports-color: 7.2.0
|
||||
|
||||
chalk@5.6.0: {}
|
||||
chalk@5.6.2: {}
|
||||
|
||||
character-entities-html4@2.1.0: {}
|
||||
|
||||
@@ -21129,6 +21163,8 @@ snapshots:
|
||||
|
||||
jose@5.10.0: {}
|
||||
|
||||
jose@6.1.0: {}
|
||||
|
||||
jotai@2.13.1(@babel/core@7.28.3)(@babel/template@7.27.2)(@types/react@19.1.12)(react@19.1.1):
|
||||
optionalDependencies:
|
||||
'@babel/core': 7.28.3
|
||||
|
||||
@@ -3,16 +3,26 @@ packages:
|
||||
- docs
|
||||
- demo/*
|
||||
- e2e/**
|
||||
neverBuiltDependencies: []
|
||||
|
||||
catalog:
|
||||
'@better-fetch/fetch': ^1.1.18
|
||||
better-call: 1.0.18
|
||||
typescript: ^5.9.2
|
||||
unbuild: ^3.6.1
|
||||
vitest: ^3.2.4
|
||||
|
||||
catalogs:
|
||||
react18:
|
||||
'@types/react': ^19.1.12
|
||||
'@types/react-dom': ^19.1.9
|
||||
react: 19.1.1
|
||||
react-dom: 19.1.1
|
||||
catalog:
|
||||
"better-call": "1.0.16"
|
||||
"@better-fetch/fetch": "^1.1.18"
|
||||
"unbuild": "^3.6.1"
|
||||
"typescript": "^5.9.2"
|
||||
"vitest": "^3.2.4"
|
||||
|
||||
neverBuiltDependencies: []
|
||||
|
||||
overrides:
|
||||
brace-expansion@>=1.0.0 <=1.1.11: '>=1.1.12'
|
||||
cookie@<0.7.0: '>=0.7.0'
|
||||
esbuild@<=0.24.2: '>=0.25.0'
|
||||
miniflare>zod: ^3.25.1
|
||||
zod: ^4.1.5
|
||||
|
||||
Reference in New Issue
Block a user