diff --git a/docs/content/docs/features/local-install.mdx b/docs/content/docs/features/local-install.mdx
index db3ecc5f..081e1ae8 100644
--- a/docs/content/docs/features/local-install.mdx
+++ b/docs/content/docs/features/local-install.mdx
@@ -118,6 +118,7 @@ installed component.
findOne,
findMany,
updateOne,
+ incrementOne,
updateMany,
deleteOne,
deleteMany,
diff --git a/docs/content/docs/framework-guides/expo.mdx b/docs/content/docs/framework-guides/expo.mdx
index cd712d69..446f2146 100644
--- a/docs/content/docs/framework-guides/expo.mdx
+++ b/docs/content/docs/framework-guides/expo.mdx
@@ -17,7 +17,7 @@ description: Install and configure Convex + Better Auth for Expo.
```npm
npm install convex@latest @convex-dev/better-auth
- npm install better-auth@~1.6.15 @better-auth/expo@~1.6.15
+ npm install better-auth@~1.7.7 @better-auth/expo@~1.7.7
```
diff --git a/docs/content/docs/framework-guides/next.mdx b/docs/content/docs/framework-guides/next.mdx
index b2b2b626..c434d0fa 100644
--- a/docs/content/docs/framework-guides/next.mdx
+++ b/docs/content/docs/framework-guides/next.mdx
@@ -22,7 +22,7 @@ description: Install and configure Convex + Better Auth for Next.js.
```npm
npm install convex@latest @convex-dev/better-auth
- npm install better-auth@~1.6.15
+ npm install better-auth@~1.7.7
```
diff --git a/docs/content/docs/framework-guides/react.mdx b/docs/content/docs/framework-guides/react.mdx
index 80528313..76908b30 100644
--- a/docs/content/docs/framework-guides/react.mdx
+++ b/docs/content/docs/framework-guides/react.mdx
@@ -22,7 +22,7 @@ description: Install and configure Convex + Better Auth for React.
```npm
npm install convex@latest @convex-dev/better-auth
- npm install better-auth@~1.6.15
+ npm install better-auth@~1.7.7
```
diff --git a/docs/content/docs/framework-guides/sveltekit.mdx b/docs/content/docs/framework-guides/sveltekit.mdx
index 2702fec0..f1742d0e 100644
--- a/docs/content/docs/framework-guides/sveltekit.mdx
+++ b/docs/content/docs/framework-guides/sveltekit.mdx
@@ -99,7 +99,7 @@ npm install convex @mmailaender/convex-svelte
```npm
npm install @convex-dev/better-auth @mmailaender/convex-better-auth-svelte
- npm install better-auth@~1.6.15
+ npm install better-auth@~1.7.7
```
diff --git a/docs/content/docs/framework-guides/tanstack-start.mdx b/docs/content/docs/framework-guides/tanstack-start.mdx
index 3d9196ba..782ec0e5 100644
--- a/docs/content/docs/framework-guides/tanstack-start.mdx
+++ b/docs/content/docs/framework-guides/tanstack-start.mdx
@@ -23,7 +23,7 @@ description: Install and configure Convex + Better Auth for TanStack Start.
```npm
npm install convex@latest @convex-dev/better-auth
- npm install better-auth@~1.6.15
+ npm install better-auth@~1.7.7
npm install @types/node --save-dev
```
diff --git a/docs/content/docs/migrations/meta.json b/docs/content/docs/migrations/meta.json
index c5bc92ec..0da75a15 100644
--- a/docs/content/docs/migrations/meta.json
+++ b/docs/content/docs/migrations/meta.json
@@ -1,5 +1,5 @@
{
"title": "Migrations",
- "pages": ["migrate-to-0-12", "migrate-to-0-11", "migrate-to-0-10", "migrate-to-0-9", "migrate-to-0-8"],
+ "pages": ["migrate-to-0-13", "migrate-to-0-12", "migrate-to-0-11", "migrate-to-0-10", "migrate-to-0-9", "migrate-to-0-8"],
"defaultOpen": true
}
diff --git a/docs/content/docs/migrations/migrate-to-0-13.mdx b/docs/content/docs/migrations/migrate-to-0-13.mdx
new file mode 100644
index 00000000..2dbe16b4
--- /dev/null
+++ b/docs/content/docs/migrations/migrate-to-0-13.mdx
@@ -0,0 +1,308 @@
+---
+title: Migrate to 0.13
+description: Migrate to @convex-dev/better-auth@0.13
+---
+
+## Upgrade
+
+
+
+
+
+### Install dependencies
+
+Update Better Auth and the component. Better Auth must be on `1.7.7` or later in
+the `1.7.x` line. Update any installed `@better-auth/*` packages together.
+
+```npm
+npm install @convex-dev/better-auth@^0.13.0
+npm install better-auth@~1.7.7
+```
+
+
+ Review the [Better Auth 1.7 upgrade
+ guide](https://www.better-auth.com/docs/guides/1-7-upgrade-guide) and [1.7.7
+ release notes](https://github.com/better-auth/better-auth/releases/tag/v1.7.7)
+ for the features your app uses.
+
+
+
+
+
+
+### Check existing accounts
+
+Accounts still use `(providerId, accountId)`, as in 1.6. **No issuer backfill is
+needed.** Check for duplicate pairs before upgrading; Better Auth now rejects
+ambiguous account lookups. Resolve duplicates using trusted provider data and
+keep different users separate. Credential accounts should have `accountId` equal
+to `userId`.
+
+If you tested 1.7.0–1.7.2, see [Early 1.7 deployments](#early-17-deployments)
+below. Microsoft accounts have a separate migration described below.
+
+
+
+
+
+### Deploy and test
+
+Back up your data and test the upgrade on a development deployment first.
+Regenerate Convex bindings, then deploy the component and application changes
+together. Verify sign-in, existing sessions, account linking, token refresh, and
+`/api/auth/convex/jwks` for the features you use.
+
+Upgrade servers sharing verification storage and OAuth Proxy participants
+together. Request fresh Magic Links and restart pending OAuth/SAML sign-ins
+after deploying, as required by the 1.7.7 security fixes.
+
+
+
+
+
+## Breaking changes
+
+### Account selectors
+
+Read the local account `id` from `listAccounts` and pass it as `accountId` to
+`unlinkAccount`. For `getAccessToken`, `refreshToken`, and `accountInfo`, pass
+exactly one of `{ accountId: account.id }` or `{ useAccountCookie: true }`.
+Remove `providerId` from these selectors. Cookie selection requires
+`account.storeAccountCookie` in your Better Auth configuration.
+
+### Generic OAuth
+
+Use the standard social client APIs:
+
+```ts
+await authClient.signIn.social({ provider: "my-provider" });
+await authClient.linkSocial({ provider: "my-provider" });
+```
+
+These replace `signIn.oauth2()` and `oauth2.link()`. Remove
+`genericOAuthClient()` and change provider callback URLs from
+`/api/auth/oauth2/callback/:id` to `/api/auth/callback/:id`.
+
+PKCE is enabled by default. Set `pkce: false` only for providers that reject it.
+Remove `issuer` and `requireIssuerValidation`; issuer validation is automatic.
+
+Review custom provider identity mapping. Discovery providers use verified `sub`,
+and plain OAuth providers use `id`. Set `accountSubject` if that default is not
+the provider's immutable identifier. Keep the provider identifier in
+`getUserInfo().data`; `getUserInfo().user` contains mutable user fields, and
+`mapProfileToUser` can no longer return `id`. See the upstream
+[account identity guidance](https://www.better-auth.com/docs/guides/1-7-upgrade-guide#account-identity-keeps-the-provider-key).
+
+### Microsoft accounts
+
+Microsoft accounts now use `oid` instead of `sub`, including the Generic OAuth
+Microsoft Entra helper. Pause Microsoft sign-in and linking while updating
+stored account IDs from verified ID tokens or a trusted Entra export. The old
+account row alone cannot supply this mapping.
+
+The Generic OAuth `microsoftEntraId` helper requires a concrete tenant GUID and
+an `id_token` from the token exchange. Use the built-in Microsoft provider for
+`common`, `organizations`, or `consumers`. Follow the upstream guide's
+[Microsoft migration steps](https://www.better-auth.com/docs/guides/1-7-upgrade-guide#migrate-microsoft-account-identifiers).
+
+### Database joins
+
+Move `experimental.joins` to `advanced.database.joins`. The Convex adapter
+continues to disable native joins and use its existing relation queries.
+
+### Expo
+
+The `@better-auth/expo` client's `getCookie()` now returns a promise. Await it
+and make callbacks that read cookies asynchronous. Passing `expo-secure-store`
+directly still works; custom storage must implement `getItem`, `getItemAsync`,
+`setItem`, and `setItemAsync`. See the upstream
+[Expo migration guidance](https://www.better-auth.com/docs/guides/1-7-upgrade-guide#expo-and-react-native).
+
+### Two-factor authentication
+
+Check the returned `method` before reading `totpURI` or backup codes from
+`enableTwoFactor`. Those fields are present only when `method === "totp"`. The
+Next.js and TanStack examples include this check.
+
+## Early 1.7 deployments
+
+Better Auth 1.7.3 removed the required `issuer` field introduced in 1.7.0–1.7.2.
+The bundled component keeps it optional to accept existing data, but no longer
+includes issuer indexes or backfill functions.
+
+For Local Install, remove issuer-based constraints and indexes. Keep `issuer`
+optional until stored values are removed. If different issuers shared one
+provider ID and overlapping account IDs, assign distinct provider IDs and update
+the matching account rows before resuming sign-in.
+
+## OAuth or OIDC provider
+
+The conversion below is for apps that used `oidcProvider` to act as an identity
+provider. Apps that only sign in through an external provider can skip it.
+
+If you already use `@better-auth/oauth-provider` with Local Install, follow the
+upstream
+[OAuth client migration](https://www.better-auth.com/docs/guides/1-7-upgrade-guide#migrate-oauth-client-records)
+before applying the generated schema. Existing clients need field backfills and
+configuration changes; schema generation alone does not migrate their data. Skip
+the legacy export and cleanup below.
+
+Use a temporary Convex schema that accepts both the existing and migrated client
+records. Backfill the new fields and remove the legacy `type` and `public`
+fields from existing `oauthClient` documents before deploying the final
+generated schema. Remove those fields in a component mutation with
+`ctx.db.patch(id, { type: undefined, public: undefined })`; setting them to
+`null` still leaves fields that the final schema rejects.
+
+### Export and clear legacy records
+
+Back up your data and pause authentication writes and public HTTP routes during
+the conversion. Keep the bundled component mounted until export and cleanup are
+complete. Add these internal wrappers:
+
+```ts title="convex/migrations.ts"
+import { v } from "convex/values";
+import { internalMutation, internalQuery } from "./_generated/server";
+import { components } from "./_generated/api";
+
+export const listLegacyOAuthApplications = internalQuery({
+ args: { cursor: v.union(v.string(), v.null()) },
+ handler: async (ctx, { cursor }) => {
+ return await ctx.runQuery(
+ components.betterAuth.migrations.listLegacyOAuthApplications,
+ { paginationOpts: { cursor, numItems: 100 } }
+ );
+ },
+});
+
+export const clearLegacyOAuthProviderRecords = internalMutation({
+ args: {
+ table: v.union(
+ v.literal("oauthApplication"),
+ v.literal("oauthAccessToken"),
+ v.literal("oauthConsent")
+ ),
+ cursor: v.union(v.string(), v.null()),
+ },
+ handler: async (ctx, { table, cursor }) => {
+ return await ctx.runMutation(
+ components.betterAuth.migrations.clearLegacyOAuthProviderRecords,
+ { table, paginationOpts: { cursor, numItems: 100 } }
+ );
+ },
+});
+```
+
+Export every page of client registrations and save the results securely. The
+export omits `clientSecret`; review custom `metadata` before reusing it.
+
+```bash
+npx convex run migrations:listLegacyOAuthApplications '{"cursor":null}'
+```
+
+After verifying the export, clear the old tokens, consents, and applications in
+that order. This invalidates existing provider tokens and removes client
+secrets.
+
+```bash
+npx convex run migrations:clearLegacyOAuthProviderRecords \
+ '{"table":"oauthAccessToken","cursor":null}'
+npx convex run migrations:clearLegacyOAuthProviderRecords \
+ '{"table":"oauthConsent","cursor":null}'
+npx convex run migrations:clearLegacyOAuthProviderRecords \
+ '{"table":"oauthApplication","cursor":null}'
+```
+
+For each command, pass the returned `continueCursor` to the next call until
+`isDone` is true. A completed cursor is terminal; use `null` only to start a new
+pass. Add `--prod` when running against production.
+
+### Switch to Local Install
+
+1. Follow the [Local Install guide](/local-install). Keep the component mounted
+ as `betterAuth` so existing data stays under `components.betterAuth`.
+2. Install `@better-auth/oauth-provider@~1.7.7` and replace `oidcProvider()`
+ with `jwt()` and `oauthProvider()` in your static Better Auth options. Follow
+ the upstream
+ [provider migration guide](https://www.better-auth.com/docs/guides/1-7-upgrade-guide#as-an-identity-provider).
+3. Generate the new schema after adding the plugin and clearing all three legacy
+ tables. Retain optional `account.issuer` if existing records still contain
+ it.
+4. Deploy while writes remain paused. Confirm that existing users, accounts, and
+ sessions remain under the same component mount before proceeding.
+5. Re-register exported clients. Map `redirectUrls` to `redirectUris`, parse
+ legacy `metadata` as JSON, and review grant types and token authentication.
+ Distribute new client secrets and issue new provider tokens.
+6. Test authorization, consent, token issuance, refresh, user info, and logout
+ before restoring traffic. Restore the backup with the old application version
+ if the conversion fails.
+
+### Configure provider signing keys
+
+Use database-backed signing keys for this provider setup. Static keys passed to
+`convex({ jwks: ... })` are not supported: the JWT plugin signs provider tokens
+with database keys, while Convex publishes the static keys. Clients would then
+be unable to verify those provider tokens.
+
+In the runtime plugin array, place `jwt()` before `convex()` and
+`oauthProvider()` after `convex()`. These plugins share endpoint keys, so order
+matters. `registerRoutes` and `registerRoutesLazy` direct root OpenID discovery
+to the OAuth provider when it is configured, so root discovery advertises the
+provider issuer. Convex JWT verifiers should continue to use the Convex issuer
+and `/api/auth/convex/jwks` directly.
+
+Configure the JWT plugin to use Convex's public signing-key URL:
+
+```ts
+jwt({
+ disableSettingJwtHeader: true,
+ jwks: {
+ remoteUrl: `${process.env.CONVEX_SITE_URL}/api/auth/convex/jwks`,
+ keyPairConfig: { alg: "RS256" },
+ },
+});
+```
+
+Replace `/api/auth` if you use a custom `basePath`. Use the same signing
+algorithm as your Convex auth configuration (`RS256` by default). Disabling the
+JWT header avoids generating an extra token when reading a session; Convex
+clients use `/convex/token`.
+
+### Custom consent pages
+
+Before displaying consent details, verify the signed query on the server with
+`verifyOAuthQueryParams` from `@better-auth/oauth-provider`. Use the provider's
+resolved secret, `(await auth.$context).secret`, from your server-side auth
+instance and keep it on the server. See the upstream
+[consent screen example](https://www.better-auth.com/docs/plugins/oauth-provider#consent-screen).
+
+## Local Install additional steps
+
+Generate the 1.7.7 schema to a side file and compare it with your deployed
+schema:
+
+```npm
+cd convex/betterAuth
+npx auth generate --output generatedSchema.next.ts
+```
+
+Preserve existing plugin tables, custom fields, and indexes. Include the
+generated credential lookup index `userId_providerId_accountId` on
+`["userId", "providerId", "accountId"]` and export `incrementOne` with your
+other adapter functions. The adapter implements `consumeOne` using the
+component's atomic `deleteOne`.
+
+If you already use Local Install with `oidcProvider`, copy
+`src/component/migrations.ts` from the installed `@convex-dev/better-auth`
+package to `convex/betterAuth/migrations.ts`. Install `convex-helpers` as a
+direct dependency, choosing a version compatible with your installed Convex
+version. The helpers use its `paginator`, since `ctx.db.query(...).paginate()`
+is not supported inside components. Use the internal wrappers above to call
+them.
+
+Before exporting, deploy a temporary schema that retains `oauthApplication` and
+accepts both legacy and new `oauthAccessToken` and `oauthConsent` records. Keep
+both sets of fields and indexes, make required new fields such as `clientId` and
+`scopes` optional, and allow `scopes` to be either a string or an array of
+strings. Clear legacy records before applying the final provider schema, and
+remove the temporary migration helpers and wrappers in that same deployment.
diff --git a/docs/content/docs/supported-plugins.mdx b/docs/content/docs/supported-plugins.mdx
index f3df30ef..0c2bf46a 100644
--- a/docs/content/docs/supported-plugins.mdx
+++ b/docs/content/docs/supported-plugins.mdx
@@ -5,12 +5,12 @@ description: Supported plugins for Convex + Better Auth
## Supported plugins
-Any Better Auth plugin may be used with Convex + Better Auth, but only a subset
-are considered supported for the integration. Convex + Better Auth works out of
-the box for these without any required schema changes.
+Better Auth plugins may be used with Convex + Better Auth when their runtime and
+adapter requirements are met, but only a subset are considered supported for the
+integration. These work without any required schema changes.
-For plugins that require schema changes and aren't in the list, check out [Local
-Install](/local-install).
+For plugins that require schema changes and aren't in the list, check out
+[Local Install](/local-install).
- [Anonymous](https://www.better-auth.com/docs/plugins/anonymous)
- [Email OTP](https://www.better-auth.com/docs/plugins/email-otp)
@@ -22,9 +22,23 @@ Install](/local-install).
- [Two Factor](https://www.better-auth.com/docs/plugins/2fa)
- [Username](https://www.better-auth.com/docs/plugins/username)
+### Local Install plugins
+
+Schema-changing plugins that are not bundled with the component require
+[Local Install](/local-install). This includes the
+[`@better-auth/oauth-provider`](https://www.better-auth.com/docs/plugins/oauth-provider)
+package used when your application acts as an OAuth or OIDC provider.
+
+Local Install allows schema changes but does not add adapter capabilities
+required by a plugin. Check its requirements before using it.
+
### Incompatible plugins
Official Better Auth plugins that are incompatible with Convex + Better Auth
(even when Local Install is used):
-- [SSO](https://www.better-auth.com/docs/plugins/sso) - has direct dependencies on Node.js
+- [SSO](https://www.better-auth.com/docs/plugins/sso) - has direct dependencies
+ on Node.js
+- [SCIM](https://www.better-auth.com/docs/plugins/scim) - requires native
+ interactive adapter transactions in Better Auth 1.7, which the Convex adapter
+ does not provide.
diff --git a/e2e/.gitignore b/e2e/.gitignore
index 6aaf5b72..0288404e 100644
--- a/e2e/.gitignore
+++ b/e2e/.gitignore
@@ -7,3 +7,6 @@ convex-local-backend
convex-local-backend.log
convex_local_storage
convex_local_backend.sqlite3
+
+# Generated when the E2E fixture is deployed.
+convex/**/_generated/
diff --git a/e2e/convex/auth.config.ts b/e2e/convex/auth.config.ts
new file mode 100644
index 00000000..793ec105
--- /dev/null
+++ b/e2e/convex/auth.config.ts
@@ -0,0 +1,2 @@
+import { getAuthConfigProvider } from "@convex-dev/better-auth/auth-config";
+export default { providers: [getAuthConfigProvider()] };
diff --git a/e2e/convex/auth.ts b/e2e/convex/auth.ts
new file mode 100644
index 00000000..b1a0c45a
--- /dev/null
+++ b/e2e/convex/auth.ts
@@ -0,0 +1,40 @@
+import { createClient, type GenericCtx } from "@convex-dev/better-auth";
+import { convex } from "@convex-dev/better-auth/plugins";
+import { oauthProvider } from "@better-auth/oauth-provider";
+import { betterAuth, type BetterAuthOptions } from "better-auth/minimal";
+import { jwt } from "better-auth/plugins/jwt";
+import { components } from "./_generated/api";
+import type { DataModel } from "./_generated/dataModel";
+import schema from "./betterAuth/schema";
+import authConfig from "./auth.config";
+
+export const authComponent = createClient(
+ components.oauthProvider,
+ { local: { schema } }
+);
+export const createAuthOptions = (ctx: GenericCtx) =>
+ ({
+ emailAndPassword: { enabled: true },
+ baseURL: process.env.CONVEX_SITE_URL,
+ database: authComponent.adapter(ctx),
+ plugins: [
+ jwt({
+ disableSettingJwtHeader: true,
+ jwks: {
+ remoteUrl: `${process.env.CONVEX_SITE_URL}/api/auth/convex/jwks`,
+ keyPairConfig: { alg: "RS256" },
+ },
+ }),
+ convex({ authConfig }),
+ oauthProvider({
+ loginPage: "/sign-in",
+ consentPage: "/consent",
+ scopes: ["openid", "profile", "email", "offline_access"],
+ grantTypes: ["authorization_code", "refresh_token"],
+ refreshTokenReuseInterval: 0,
+ }),
+ ],
+ }) satisfies BetterAuthOptions;
+
+export const createAuth = (ctx: GenericCtx) =>
+ betterAuth(createAuthOptions(ctx));
diff --git a/e2e/convex/betterAuth/adapter.ts b/e2e/convex/betterAuth/adapter.ts
new file mode 100644
index 00000000..69c8c7c1
--- /dev/null
+++ b/e2e/convex/betterAuth/adapter.ts
@@ -0,0 +1,14 @@
+import { createApi } from "@convex-dev/better-auth";
+import schema from "./schema";
+import { createAuthOptions } from "../auth";
+
+export const {
+ create,
+ findOne,
+ findMany,
+ updateOne,
+ incrementOne,
+ updateMany,
+ deleteOne,
+ deleteMany,
+} = createApi(schema, createAuthOptions);
diff --git a/e2e/convex/betterAuth/auth.ts b/e2e/convex/betterAuth/auth.ts
new file mode 100644
index 00000000..f572a538
--- /dev/null
+++ b/e2e/convex/betterAuth/auth.ts
@@ -0,0 +1,3 @@
+import { createAuth } from "../auth";
+// Only used by the Better Auth schema generator.
+export const auth = createAuth({} as never);
diff --git a/e2e/convex/betterAuth/convex.config.ts b/e2e/convex/betterAuth/convex.config.ts
new file mode 100644
index 00000000..948c02a7
--- /dev/null
+++ b/e2e/convex/betterAuth/convex.config.ts
@@ -0,0 +1,2 @@
+import { defineComponent } from "convex/server";
+export default defineComponent("betterAuth");
diff --git a/e2e/convex/betterAuth/schema.ts b/e2e/convex/betterAuth/schema.ts
new file mode 100644
index 00000000..61127971
--- /dev/null
+++ b/e2e/convex/betterAuth/schema.ts
@@ -0,0 +1,207 @@
+/**
+ * This file is auto-generated. Do not edit this file manually.
+ * To regenerate the schema, from your project root:
+ *
+ * cd convex/betterAuth
+ * npx auth generate --output schema.ts
+ *
+ * To customize the schema, generate to an alternate file and import
+ * the table definitions to your schema file. See
+ * https://labs.convex.dev/better-auth/features/local-install#adding-custom-indexes.
+ */
+
+import { defineSchema, defineTable } from "convex/server";
+import { v } from "convex/values";
+
+export const tables = {
+ user: defineTable({
+ name: v.string(),
+ email: v.string(),
+ emailVerified: v.boolean(),
+ image: v.optional(v.union(v.null(), v.string())),
+ createdAt: v.number(),
+ updatedAt: v.number(),
+ userId: v.optional(v.union(v.null(), v.string())),
+ })
+ .index("email_name", ["email", "name"])
+ .index("name", ["name"])
+ .index("userId", ["userId"]),
+ session: defineTable({
+ expiresAt: v.number(),
+ token: v.string(),
+ createdAt: v.number(),
+ updatedAt: v.number(),
+ ipAddress: v.optional(v.union(v.null(), v.string())),
+ userAgent: v.optional(v.union(v.null(), v.string())),
+ userId: v.string(),
+ })
+ .index("expiresAt", ["expiresAt"])
+ .index("expiresAt_userId", ["expiresAt", "userId"])
+ .index("token", ["token"])
+ .index("userId", ["userId"]),
+ account: defineTable({
+ accountId: v.string(),
+ providerId: v.string(),
+ userId: v.string(),
+ accessToken: v.optional(v.union(v.null(), v.string())),
+ refreshToken: v.optional(v.union(v.null(), v.string())),
+ idToken: v.optional(v.union(v.null(), v.string())),
+ accessTokenExpiresAt: v.optional(v.union(v.null(), v.number())),
+ refreshTokenExpiresAt: v.optional(v.union(v.null(), v.number())),
+ scope: v.optional(v.union(v.null(), v.string())),
+ password: v.optional(v.union(v.null(), v.string())),
+ createdAt: v.number(),
+ updatedAt: v.number(),
+ })
+ .index("accountId", ["accountId"])
+ .index("accountId_providerId", ["accountId", "providerId"])
+ .index("providerId_userId", ["providerId", "userId"])
+ .index("userId_providerId_accountId", ["userId", "providerId", "accountId"])
+ .index("userId", ["userId"]),
+ verification: defineTable({
+ identifier: v.string(),
+ value: v.string(),
+ expiresAt: v.number(),
+ createdAt: v.number(),
+ updatedAt: v.number(),
+ })
+ .index("expiresAt", ["expiresAt"])
+ .index("identifier", ["identifier"]),
+ jwks: defineTable({
+ publicKey: v.string(),
+ privateKey: v.string(),
+ createdAt: v.number(),
+ expiresAt: v.optional(v.union(v.null(), v.number())),
+ alg: v.optional(v.union(v.null(), v.string())),
+ crv: v.optional(v.union(v.null(), v.string())),
+ }),
+ oauthClient: defineTable({
+ clientId: v.string(),
+ clientSecret: v.optional(v.union(v.null(), v.string())),
+ clientDiscoveryId: v.optional(v.union(v.null(), v.string())),
+ disabled: v.optional(v.union(v.null(), v.boolean())),
+ skipConsent: v.optional(v.union(v.null(), v.boolean())),
+ enableEndSession: v.optional(v.union(v.null(), v.boolean())),
+ subjectType: v.optional(v.union(v.null(), v.string())),
+ scopes: v.optional(v.union(v.null(), v.array(v.string()))),
+ clientCredentialsScopes: v.optional(v.union(v.null(), v.array(v.string()))),
+ userId: v.optional(v.union(v.null(), v.string())),
+ createdAt: v.optional(v.union(v.null(), v.number())),
+ updatedAt: v.optional(v.union(v.null(), v.number())),
+ name: v.optional(v.union(v.null(), v.string())),
+ uri: v.optional(v.union(v.null(), v.string())),
+ icon: v.optional(v.union(v.null(), v.string())),
+ contacts: v.optional(v.union(v.null(), v.array(v.string()))),
+ tos: v.optional(v.union(v.null(), v.string())),
+ policy: v.optional(v.union(v.null(), v.string())),
+ softwareId: v.optional(v.union(v.null(), v.string())),
+ softwareVersion: v.optional(v.union(v.null(), v.string())),
+ softwareStatement: v.optional(v.union(v.null(), v.string())),
+ redirectUris: v.array(v.string()),
+ postLogoutRedirectUris: v.optional(v.union(v.null(), v.array(v.string()))),
+ backchannelLogoutUri: v.optional(v.union(v.null(), v.string())),
+ backchannelLogoutSessionRequired: v.optional(
+ v.union(v.null(), v.boolean())
+ ),
+ tokenEndpointAuthMethod: v.optional(v.union(v.null(), v.string())),
+ applicationType: v.optional(v.union(v.null(), v.string())),
+ jwks: v.optional(v.union(v.null(), v.string())),
+ jwksUri: v.optional(v.union(v.null(), v.string())),
+ grantTypes: v.optional(v.union(v.null(), v.array(v.string()))),
+ responseTypes: v.optional(v.union(v.null(), v.array(v.string()))),
+ requirePKCE: v.optional(v.union(v.null(), v.boolean())),
+ dpopBoundAccessTokens: v.optional(v.union(v.null(), v.boolean())),
+ referenceId: v.optional(v.union(v.null(), v.string())),
+ metadata: v.optional(v.union(v.null(), v.string())),
+ })
+ .index("clientId", ["clientId"])
+ .index("userId", ["userId"]),
+ oauthResource: defineTable({
+ identifier: v.string(),
+ name: v.string(),
+ accessTokenTtl: v.optional(v.union(v.null(), v.number())),
+ refreshTokenTtl: v.optional(v.union(v.null(), v.number())),
+ signingAlgorithm: v.optional(v.union(v.null(), v.string())),
+ signingKeyId: v.optional(v.union(v.null(), v.string())),
+ allowedScopes: v.optional(v.union(v.null(), v.array(v.string()))),
+ customClaims: v.optional(v.union(v.null(), v.string())),
+ dpopBoundAccessTokensRequired: v.optional(v.union(v.null(), v.boolean())),
+ disabled: v.optional(v.union(v.null(), v.boolean())),
+ createdAt: v.optional(v.union(v.null(), v.number())),
+ updatedAt: v.optional(v.union(v.null(), v.number())),
+ policyVersion: v.optional(v.union(v.null(), v.number())),
+ metadata: v.optional(v.union(v.null(), v.string())),
+ }).index("identifier", ["identifier"]),
+ oauthClientResource: defineTable({
+ clientId: v.string(),
+ resourceId: v.string(),
+ metadata: v.optional(v.union(v.null(), v.string())),
+ createdAt: v.optional(v.union(v.null(), v.number())),
+ })
+ .index("clientId_resourceId", ["clientId", "resourceId"])
+ .index("clientId", ["clientId"])
+ .index("resourceId", ["resourceId"]),
+ oauthRefreshToken: defineTable({
+ token: v.string(),
+ clientId: v.string(),
+ sessionId: v.optional(v.union(v.null(), v.string())),
+ userId: v.string(),
+ referenceId: v.optional(v.union(v.null(), v.string())),
+ authorizationCodeId: v.optional(v.union(v.null(), v.string())),
+ resources: v.optional(v.union(v.null(), v.array(v.string()))),
+ requestedUserInfoClaims: v.optional(v.union(v.null(), v.array(v.string()))),
+ expiresAt: v.optional(v.union(v.null(), v.number())),
+ createdAt: v.optional(v.union(v.null(), v.number())),
+ revoked: v.optional(v.union(v.null(), v.number())),
+ rotatedAt: v.optional(v.union(v.null(), v.number())),
+ rotationReplayResponse: v.optional(v.union(v.null(), v.string())),
+ rotationReplayExpiresAt: v.optional(v.union(v.null(), v.number())),
+ authTime: v.optional(v.union(v.null(), v.number())),
+ confirmation: v.optional(v.union(v.null(), v.string())),
+ scopes: v.array(v.string()),
+ })
+ .index("token", ["token"])
+ .index("clientId", ["clientId"])
+ .index("sessionId", ["sessionId"])
+ .index("userId", ["userId"]),
+ oauthAccessToken: defineTable({
+ token: v.optional(v.union(v.null(), v.string())),
+ clientId: v.string(),
+ sessionId: v.optional(v.union(v.null(), v.string())),
+ userId: v.optional(v.union(v.null(), v.string())),
+ referenceId: v.optional(v.union(v.null(), v.string())),
+ authorizationCodeId: v.optional(v.union(v.null(), v.string())),
+ resources: v.optional(v.union(v.null(), v.array(v.string()))),
+ requestedUserInfoClaims: v.optional(v.union(v.null(), v.array(v.string()))),
+ refreshId: v.optional(v.union(v.null(), v.string())),
+ expiresAt: v.optional(v.union(v.null(), v.number())),
+ createdAt: v.optional(v.union(v.null(), v.number())),
+ revoked: v.optional(v.union(v.null(), v.number())),
+ confirmation: v.optional(v.union(v.null(), v.string())),
+ scopes: v.array(v.string()),
+ })
+ .index("token", ["token"])
+ .index("clientId", ["clientId"])
+ .index("sessionId", ["sessionId"])
+ .index("userId", ["userId"])
+ .index("refreshId", ["refreshId"]),
+ oauthConsent: defineTable({
+ clientId: v.string(),
+ userId: v.optional(v.union(v.null(), v.string())),
+ referenceId: v.optional(v.union(v.null(), v.string())),
+ resources: v.optional(v.union(v.null(), v.array(v.string()))),
+ requestedUserInfoClaims: v.optional(v.union(v.null(), v.array(v.string()))),
+ scopes: v.array(v.string()),
+ createdAt: v.optional(v.union(v.null(), v.number())),
+ updatedAt: v.optional(v.union(v.null(), v.number())),
+ })
+ .index("clientId_userId", ["clientId", "userId"])
+ .index("userId", ["userId"]),
+ oauthClientAssertion: defineTable({
+ expiresAt: v.number(),
+ }),
+};
+
+const schema = defineSchema(tables);
+
+export default schema;
diff --git a/e2e/convex/convex.config.ts b/e2e/convex/convex.config.ts
new file mode 100644
index 00000000..2f87f607
--- /dev/null
+++ b/e2e/convex/convex.config.ts
@@ -0,0 +1,6 @@
+import { defineApp } from "convex/server";
+import oauthProvider from "./betterAuth/convex.config";
+
+const app = defineApp();
+app.use(oauthProvider, { name: "oauthProvider" });
+export default app;
diff --git a/e2e/convex/http.ts b/e2e/convex/http.ts
new file mode 100644
index 00000000..d6af2133
--- /dev/null
+++ b/e2e/convex/http.ts
@@ -0,0 +1,19 @@
+import { httpRouter, httpActionGeneric } from "convex/server";
+import { authComponent, createAuth } from "./auth";
+
+const http = httpRouter();
+const registration = process.env.OAUTH_ROUTE_REGISTRATION;
+if (registration !== "eager" && registration !== "lazy") {
+ throw new Error("OAUTH_ROUTE_REGISTRATION must be eager or lazy");
+}
+http.route({
+ path: "/registration-mode",
+ method: "GET",
+ handler: httpActionGeneric(async () => Response.json({ registration })),
+});
+if (registration === "lazy") {
+ authComponent.registerRoutesLazy(http, createAuth);
+} else {
+ authComponent.registerRoutes(http, createAuth);
+}
+export default http;
diff --git a/e2e/convex/schema.ts b/e2e/convex/schema.ts
new file mode 100644
index 00000000..8cc1e2ba
--- /dev/null
+++ b/e2e/convex/schema.ts
@@ -0,0 +1,2 @@
+import { defineSchema } from "convex/server";
+export default defineSchema({});
diff --git a/e2e/convex/tsconfig.json b/e2e/convex/tsconfig.json
new file mode 100644
index 00000000..42b170fb
--- /dev/null
+++ b/e2e/convex/tsconfig.json
@@ -0,0 +1,18 @@
+{
+ "compilerOptions": {
+ "allowJs": true,
+ "strict": true,
+ "moduleResolution": "Bundler",
+ "jsx": "react-jsx",
+ "skipLibCheck": true,
+ "allowSyntheticDefaultImports": true,
+ "target": "ESNext",
+ "lib": ["ES2023", "dom"],
+ "forceConsistentCasingInFileNames": true,
+ "module": "ESNext",
+ "isolatedModules": true,
+ "noEmit": true
+ },
+ "include": ["./**/*"],
+ "exclude": ["./_generated"]
+}
diff --git a/e2e/oauth-provider/provider.spec.ts b/e2e/oauth-provider/provider.spec.ts
new file mode 100644
index 00000000..49ae9fe4
--- /dev/null
+++ b/e2e/oauth-provider/provider.spec.ts
@@ -0,0 +1,158 @@
+import { createHash, randomBytes, randomUUID } from "node:crypto";
+import { test, expect, type APIRequestContext } from "@playwright/test";
+import { createLocalJWKSet, jwtVerify } from "jose";
+
+const site = "http://127.0.0.1:3211";
+const base = `${site}/api/auth`;
+const redirectUri = "https://client.example.com/callback";
+
+async function authorize(request: APIRequestContext) {
+ const signup = await request.post(`${base}/sign-up/email`, {
+ data: {
+ name: "OAuth test user",
+ email: `${randomUUID()}@example.com`,
+ password: "TestPassword123!",
+ },
+ });
+ expect(signup.status()).toBe(200);
+ const { user } = await signup.json();
+ const registration = await request.post(`${base}/oauth2/create-client`, {
+ data: {
+ client_name: "E2E client",
+ redirect_uris: [redirectUri],
+ token_endpoint_auth_method: "client_secret_post",
+ grant_types: ["authorization_code", "refresh_token"],
+ response_types: ["code"],
+ scope: "openid profile email offline_access",
+ },
+ });
+ expect(registration.status()).toBe(201);
+ const { client_id, client_secret } = await registration.json();
+ expect(client_id).toBeTruthy();
+ expect(client_secret).toBeTruthy();
+ const verifier = randomBytes(32).toString("base64url");
+ const state = randomUUID();
+ const nonce = randomUUID();
+ const authorization = await request.get(`${base}/oauth2/authorize`, {
+ params: {
+ client_id,
+ redirect_uri: redirectUri,
+ response_type: "code",
+ scope: "openid profile email offline_access",
+ state,
+ nonce,
+ code_challenge: createHash("sha256").update(verifier).digest("base64url"),
+ code_challenge_method: "S256",
+ prompt: "consent",
+ },
+ maxRedirects: 0,
+ });
+ expect(authorization.status()).toBe(302);
+ const consentUrl = new URL(authorization.headers().location, site);
+ expect(consentUrl.pathname).toBe("/consent");
+ const consent = await request.post(`${base}/oauth2/consent`, {
+ data: { accept: true, oauth_query: consentUrl.search.slice(1) },
+ });
+ expect(consent.status()).toBe(200);
+ const callback = new URL((await consent.json()).url);
+ expect(`${callback.origin}${callback.pathname}`).toBe(redirectUri);
+ expect(callback.searchParams.get("state")).toBe(state);
+ const code = callback.searchParams.get("code");
+ expect(code).toBeTruthy();
+ const credentials = { client_id, client_secret };
+ const exchange = await request.post(`${base}/oauth2/token`, {
+ headers: { cookie: "" },
+ form: {
+ ...credentials,
+ grant_type: "authorization_code",
+ code: code!,
+ code_verifier: verifier,
+ redirect_uri: redirectUri,
+ },
+ });
+ expect(exchange.status()).toBe(200);
+ const tokens = await exchange.json();
+ expect(tokens.access_token).toBeTruthy();
+ expect(tokens.refresh_token).toBeTruthy();
+ expect(tokens.id_token).toBeTruthy();
+ return { credentials, tokens, user, nonce };
+}
+
+test.describe(`${process.env.OAUTH_ROUTE_REGISTRATION} OAuth provider`, () => {
+ test.beforeEach(async ({ request }) => {
+ const mode = await request.get("/registration-mode");
+ expect(mode.status()).toBe(200);
+ expect(await mode.json()).toEqual({
+ registration: process.env.OAUTH_ROUTE_REGISTRATION,
+ });
+ });
+ test("discovery leads to provider metadata and keys that verify its ID token", async ({
+ request,
+ }) => {
+ const root = await request.get(`${site}/.well-known/openid-configuration`, {
+ maxRedirects: 0,
+ });
+ expect(root.status()).toBe(302);
+ expect(root.headers().location).toBe(
+ `${base}/.well-known/openid-configuration`
+ );
+ const discovery = await request.get(root.headers().location);
+ expect(discovery.status()).toBe(200);
+ const metadata = await discovery.json();
+ expect(metadata).toMatchObject({
+ issuer: base,
+ authorization_endpoint: `${base}/oauth2/authorize`,
+ token_endpoint: `${base}/oauth2/token`,
+ jwks_uri: `${base}/convex/jwks`,
+ });
+ const { credentials, tokens, user, nonce } = await authorize(request);
+ const keys = await request.get(metadata.jwks_uri);
+ expect(keys.status()).toBe(200);
+ const jwks = await keys.json();
+ expect(jwks.keys.length).toBeGreaterThan(0);
+ for (const key of jwks.keys) expect(key).not.toHaveProperty("d");
+ const { payload } = await jwtVerify(
+ tokens.id_token,
+ createLocalJWKSet(jwks),
+ {
+ issuer: metadata.issuer,
+ audience: credentials.client_id,
+ algorithms: ["RS256"],
+ }
+ );
+ expect(payload).toMatchObject({ sub: user.id, nonce });
+ });
+
+ test("refresh rotates the token and rejects reuse of the old token and its family", async ({
+ request,
+ }) => {
+ const { credentials, tokens, user } = await authorize(request);
+ const refresh = (token: string) =>
+ request.post(`${base}/oauth2/token`, {
+ headers: { cookie: "" },
+ form: {
+ ...credentials,
+ grant_type: "refresh_token",
+ refresh_token: token,
+ },
+ });
+ const rotation = await refresh(tokens.refresh_token);
+ expect(rotation.status()).toBe(200);
+ const rotated = await rotation.json();
+ expect(rotated.refresh_token).toBeTruthy();
+ expect(rotated.refresh_token).not.toBe(tokens.refresh_token);
+ expect(rotated.access_token).toBeTruthy();
+ const profile = await request.get(`${base}/oauth2/userinfo`, {
+ headers: { cookie: "", authorization: `Bearer ${rotated.access_token}` },
+ });
+ expect(profile.status()).toBe(200);
+ expect(await profile.json()).toMatchObject({ sub: user.id });
+
+ const replay = await refresh(tokens.refresh_token);
+ expect(replay.status()).toBe(400);
+ expect(await replay.json()).toMatchObject({ error: "invalid_grant" });
+ const family = await refresh(rotated.refresh_token);
+ expect(family.status()).toBe(400);
+ expect(await family.json()).toMatchObject({ error: "invalid_grant" });
+ });
+});
diff --git a/e2e/package-lock.json b/e2e/package-lock.json
index 576f4b09..2edf1c9d 100644
--- a/e2e/package-lock.json
+++ b/e2e/package-lock.json
@@ -6,9 +6,666 @@
"": {
"name": "e2e-tests",
"dependencies": {
+ "@better-auth/oauth-provider": "1.7.7",
+ "@convex-dev/better-auth": "file:..",
"@playwright/test": "^1.60.0",
"adm-zip": "^0.5.16",
- "dotenv": "^17.0.0"
+ "better-auth": "1.7.7",
+ "convex": "1.41.0",
+ "dotenv": "^17.0.0",
+ "jose": "^6.1.0"
+ }
+ },
+ "..": {
+ "name": "@convex-dev/better-auth",
+ "version": "0.12.5",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@better-fetch/fetch": "^1.3.2",
+ "common-tags": "^1.8.2",
+ "convex-helpers": "^0.1.95",
+ "jose": "^6.1.0",
+ "remeda": "^2.32.0",
+ "semver": "^7.7.3",
+ "type-fest": "^5.0.0",
+ "zod": "^4.0.0"
+ },
+ "devDependencies": {
+ "@better-auth/core": "1.7.7",
+ "@better-auth/test-utils": "1.7.7",
+ "@convex-dev/eslint-plugin": "2.0.0",
+ "@edge-runtime/vm": "5.0.0",
+ "@eslint/eslintrc": "3.3.5",
+ "@eslint/js": "9.39.4",
+ "@tanstack/react-start": "1.167.65",
+ "@types/common-tags": "1.8.4",
+ "@types/node": "24.12.3",
+ "@types/react": "19.2.14",
+ "@types/react-dom": "19.2.3",
+ "@types/semver": "7.7.1",
+ "better-auth": "1.7.7",
+ "chokidar-cli": "3.0.0",
+ "concurrently": "10.0.3",
+ "convex": "1.41.0",
+ "convex-test": "0.0.51",
+ "eslint": "9.39.4",
+ "eslint-plugin-import": "2.32.0",
+ "eslint-plugin-react": "7.37.5",
+ "eslint-plugin-react-hooks": "7.1.1",
+ "eslint-plugin-react-refresh": "0.5.2",
+ "globals": "17.6.0",
+ "next": "16.2.6",
+ "pkg-pr-new": "0.0.71",
+ "prettier": "3.8.3",
+ "react": "19.2.6",
+ "react-dom": "19.2.6",
+ "typescript": "5.9.3",
+ "typescript-eslint": "8.59.2",
+ "vitest": "4.1.10"
+ },
+ "peerDependencies": {
+ "better-auth": ">=1.7.7 <1.8.0",
+ "convex": "^1.25.0",
+ "react": "^18.3.1 || ^19.0.0"
+ }
+ },
+ "node_modules/@better-auth/core": {
+ "version": "1.7.7",
+ "resolved": "https://registry.npmjs.org/@better-auth/core/-/core-1.7.7.tgz",
+ "integrity": "sha512-srgFzHEjB2WWlokFz1i4BfG41sNhsA105Pukgqu6RrEh5A+LSkzf1mCHydVipkjt9l5qswtBMi7IL6ouHbZL4Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@opentelemetry/semantic-conventions": "^1.41.1",
+ "@standard-schema/spec": "^1.1.0",
+ "zod": "^4.5.4"
+ },
+ "peerDependencies": {
+ "@better-auth/utils": "0.4.2",
+ "@better-fetch/fetch": "1.3.2",
+ "@opentelemetry/api": "^1.9.0",
+ "better-call": "1.4.0",
+ "jose": "^6.1.0",
+ "kysely": "^0.28.5 || ^0.29.0",
+ "nanostores": "^1.0.1"
+ },
+ "peerDependenciesMeta": {
+ "@opentelemetry/api": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@better-auth/drizzle-adapter": {
+ "version": "1.7.7",
+ "resolved": "https://registry.npmjs.org/@better-auth/drizzle-adapter/-/drizzle-adapter-1.7.7.tgz",
+ "integrity": "sha512-qon0U94PR5FQysuyGoAlGVQpIIiNg5dtDpwjE63sN/HjoRWZadZzHPcS9mn7JsJyQZK6iTNhd7LD0AaXt2f78w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@better-auth/core": "^1.7.7",
+ "@better-auth/utils": "0.4.2",
+ "drizzle-orm": "^0.45.2 || >=1.0.0-rc.1 <2.0.0"
+ },
+ "peerDependenciesMeta": {
+ "drizzle-orm": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@better-auth/kysely-adapter": {
+ "version": "1.7.7",
+ "resolved": "https://registry.npmjs.org/@better-auth/kysely-adapter/-/kysely-adapter-1.7.7.tgz",
+ "integrity": "sha512-9FONOCgOcrQI9wJ5v1oDGIg9sT7pa9RjZZQ4fsCTudo8R87SrIGNUvUxDFTJhZvUblvFfGaIBR9Vb8LA+8bqLQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@better-auth/core": "^1.7.7",
+ "@better-auth/utils": "0.4.2",
+ "kysely": "^0.28.17 || ^0.29.0"
+ },
+ "peerDependenciesMeta": {
+ "kysely": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@better-auth/memory-adapter": {
+ "version": "1.7.7",
+ "resolved": "https://registry.npmjs.org/@better-auth/memory-adapter/-/memory-adapter-1.7.7.tgz",
+ "integrity": "sha512-FqKFEe+b5tXXV0gRbyirca+qXgMpLOeJ8Wk19yykb/scQKy0WJ4k22WzEV3TTPctQV9DZhwCXPlDtQhnlOv5Gg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@better-auth/core": "^1.7.7",
+ "@better-auth/utils": "0.4.2"
+ }
+ },
+ "node_modules/@better-auth/mongo-adapter": {
+ "version": "1.7.7",
+ "resolved": "https://registry.npmjs.org/@better-auth/mongo-adapter/-/mongo-adapter-1.7.7.tgz",
+ "integrity": "sha512-ZnVDuRrXqbf0oHphrEN27oTImNH5NTO26dtWatq5cXyYAsH7goof9QIoTOmxTbBxUQahKBw9T6+9h6n+RtOE+g==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@better-auth/core": "^1.7.7",
+ "@better-auth/utils": "0.4.2",
+ "mongodb": "^6.0.0 || ^7.0.0"
+ },
+ "peerDependenciesMeta": {
+ "mongodb": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@better-auth/oauth-provider": {
+ "version": "1.7.7",
+ "resolved": "https://registry.npmjs.org/@better-auth/oauth-provider/-/oauth-provider-1.7.7.tgz",
+ "integrity": "sha512-dc4C/ZF+8qc88ebcU8kaFp8yladCtfTkuUt2zBSmYw25YCNC5BoUP9rpAkIx4nemlZ11gLtSnkEPc0sA7MwVmw==",
+ "license": "MIT",
+ "dependencies": {
+ "jose": "^6.2.3",
+ "zod": "^4.5.4"
+ },
+ "peerDependencies": {
+ "@better-auth/core": "^1.7.7",
+ "@better-auth/utils": "0.4.2",
+ "@better-fetch/fetch": "1.3.2",
+ "better-auth": "^1.7.7",
+ "better-call": "1.4.0"
+ }
+ },
+ "node_modules/@better-auth/prisma-adapter": {
+ "version": "1.7.7",
+ "resolved": "https://registry.npmjs.org/@better-auth/prisma-adapter/-/prisma-adapter-1.7.7.tgz",
+ "integrity": "sha512-4YcnrcVdvWiAZw0sZl63tWUClPXg5r3QsLn7C2sG7kKEBY06zOng0ibecyxRBixKq1zE1ceHuEUqez5Rhi+nQA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@better-auth/core": "^1.7.7",
+ "@better-auth/utils": "0.4.2",
+ "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0",
+ "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@prisma/client": {
+ "optional": true
+ },
+ "prisma": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@better-auth/telemetry": {
+ "version": "1.7.7",
+ "resolved": "https://registry.npmjs.org/@better-auth/telemetry/-/telemetry-1.7.7.tgz",
+ "integrity": "sha512-PaRA6i+kAioVYmza2gHPcdrxP0hAotph4KV9hft2GeNJq+AZXDiBu3FuLM9HxvcmiZeUT74Vufqd9ypmB8veTA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@better-auth/core": "^1.7.7",
+ "@better-auth/utils": "0.4.2",
+ "@better-fetch/fetch": "1.3.2"
+ }
+ },
+ "node_modules/@better-auth/utils": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/@better-auth/utils/-/utils-0.4.2.tgz",
+ "integrity": "sha512-AUxrvu+HaaODsUyzDxFgwd/8RZ1yZaYo42LXKSrU2oGgR38pS1ij8nqQKNgtTWoYGpNevNXtCfgTy6loHveW9A==",
+ "license": "MIT",
+ "dependencies": {
+ "@noble/hashes": "^2.0.1"
+ }
+ },
+ "node_modules/@better-fetch/fetch": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/@better-fetch/fetch/-/fetch-1.3.2.tgz",
+ "integrity": "sha512-Gs7n99b5tqUC6cQAPbV0uED3IraHB6xQbHLQ/C3l7ZFafHScOx9pQ+DYmP5blbLFShVWLqxNUlI9wi4xU/X+ow==",
+ "license": "MIT"
+ },
+ "node_modules/@convex-dev/better-auth": {
+ "resolved": "..",
+ "link": true
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.0.tgz",
+ "integrity": "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==",
+ "cpu": [
+ "ppc64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.0.tgz",
+ "integrity": "sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.0.tgz",
+ "integrity": "sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.0.tgz",
+ "integrity": "sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.0.tgz",
+ "integrity": "sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.0.tgz",
+ "integrity": "sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.0.tgz",
+ "integrity": "sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.0.tgz",
+ "integrity": "sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.0.tgz",
+ "integrity": "sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.0.tgz",
+ "integrity": "sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.0.tgz",
+ "integrity": "sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.0.tgz",
+ "integrity": "sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==",
+ "cpu": [
+ "loong64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.0.tgz",
+ "integrity": "sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==",
+ "cpu": [
+ "mips64el"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.0.tgz",
+ "integrity": "sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.0.tgz",
+ "integrity": "sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.0.tgz",
+ "integrity": "sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==",
+ "cpu": [
+ "s390x"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.0.tgz",
+ "integrity": "sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.0.tgz",
+ "integrity": "sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.0.tgz",
+ "integrity": "sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.0.tgz",
+ "integrity": "sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.0.tgz",
+ "integrity": "sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.0.tgz",
+ "integrity": "sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.0.tgz",
+ "integrity": "sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.0.tgz",
+ "integrity": "sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.0.tgz",
+ "integrity": "sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.0.tgz",
+ "integrity": "sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@noble/ciphers": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-2.4.0.tgz",
+ "integrity": "sha512-AnjFn0Jv92laAkvMrghlFZq4qQCIN/4DxFV/eooqtC2YTjB7kBeLMS2T9KJX4Dn+ZVXLOwK0lSgqDtx9gvxtiw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 20.19.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/@noble/hashes": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.4.0.tgz",
+ "integrity": "sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 20.19.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/@opentelemetry/semantic-conventions": {
+ "version": "1.43.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.43.0.tgz",
+ "integrity": "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=14"
}
},
"node_modules/@playwright/test": {
@@ -26,6 +683,12 @@
"node": ">=18"
}
},
+ "node_modules/@standard-schema/spec": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
+ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
+ "license": "MIT"
+ },
"node_modules/adm-zip": {
"version": "0.5.17",
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.17.tgz",
@@ -35,6 +698,180 @@
"node": ">=12.0"
}
},
+ "node_modules/better-auth": {
+ "version": "1.7.7",
+ "resolved": "https://registry.npmjs.org/better-auth/-/better-auth-1.7.7.tgz",
+ "integrity": "sha512-Zhgxi/c5ylmfB/sX+nwnBbsFf/j6k0L8DvL6CJXZSrXgxc5pPMC/e7j812zNd4L4B4ELmdURCPw3X8nAOUdcjw==",
+ "license": "MIT",
+ "dependencies": {
+ "@better-auth/core": "1.7.7",
+ "@better-auth/drizzle-adapter": "1.7.7",
+ "@better-auth/kysely-adapter": "1.7.7",
+ "@better-auth/memory-adapter": "1.7.7",
+ "@better-auth/mongo-adapter": "1.7.7",
+ "@better-auth/prisma-adapter": "1.7.7",
+ "@better-auth/telemetry": "1.7.7",
+ "@better-auth/utils": "0.4.2",
+ "@better-fetch/fetch": "1.3.2",
+ "@noble/ciphers": "^2.2.0",
+ "@noble/hashes": "^2.2.0",
+ "better-call": "1.4.0",
+ "defu": "^6.1.4",
+ "jose": "^6.2.3",
+ "kysely": "^0.28.17 || ^0.29.0",
+ "nanostores": "^1.3.0",
+ "zod": "^4.5.4"
+ },
+ "peerDependencies": {
+ "@lynx-js/react": "*",
+ "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0",
+ "@sveltejs/kit": "^2.0.0",
+ "@tanstack/react-start": "^1.0.0",
+ "@tanstack/solid-start": "^1.0.0",
+ "drizzle-kit": ">=0.31.4 || >=1.0.0-beta.1",
+ "drizzle-orm": "^0.45.2 || >=1.0.0-rc.1 <2.0.0",
+ "mongodb": "^6.0.0 || ^7.0.0",
+ "mysql2": "^3.0.0",
+ "next": "^14.0.0 || ^15.0.0 || ^16.0.0",
+ "pg": "^8.0.0",
+ "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0",
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0",
+ "solid-js": "^1.0.0",
+ "svelte": "^4.0.0 || ^5.0.0",
+ "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0",
+ "vue": "^3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@lynx-js/react": {
+ "optional": true
+ },
+ "@prisma/client": {
+ "optional": true
+ },
+ "@sveltejs/kit": {
+ "optional": true
+ },
+ "@tanstack/react-start": {
+ "optional": true
+ },
+ "@tanstack/solid-start": {
+ "optional": true
+ },
+ "drizzle-kit": {
+ "optional": true
+ },
+ "drizzle-orm": {
+ "optional": true
+ },
+ "mongodb": {
+ "optional": true
+ },
+ "mysql2": {
+ "optional": true
+ },
+ "next": {
+ "optional": true
+ },
+ "pg": {
+ "optional": true
+ },
+ "prisma": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ },
+ "solid-js": {
+ "optional": true
+ },
+ "svelte": {
+ "optional": true
+ },
+ "vitest": {
+ "optional": true
+ },
+ "vue": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/better-call": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/better-call/-/better-call-1.4.0.tgz",
+ "integrity": "sha512-bBKOT4vv1kZLDgxVePdilk/Jwkn+dtRRsmi3DzHcDP+WnswyVl6dR59l2HEeP/0cB+bDoopASAesWDPIdd/zZA==",
+ "license": "MIT",
+ "dependencies": {
+ "@better-auth/utils": "^0.5.0",
+ "@better-fetch/fetch": "^1.3.1",
+ "rou3": "^0.9.1",
+ "set-cookie-parser": "^3.1.2"
+ },
+ "peerDependencies": {
+ "zod": "^4.0.0"
+ },
+ "peerDependenciesMeta": {
+ "zod": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/better-call/node_modules/@better-auth/utils": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/@better-auth/utils/-/utils-0.5.0.tgz",
+ "integrity": "sha512-BL8W4EfIZFwlu0r54m3v1ztjDhu6dDe/amLTm0xybmbZaNgYUqhD3SjpAsnq0q8YD6/ki4iwIgxJNLP/N3TxiA==",
+ "license": "MIT",
+ "dependencies": {
+ "@noble/hashes": "^2.0.1"
+ }
+ },
+ "node_modules/convex": {
+ "version": "1.41.0",
+ "resolved": "https://registry.npmjs.org/convex/-/convex-1.41.0.tgz",
+ "integrity": "sha512-euxVf6yfpB7/VGKOobkLgjpbJidsUgW+b0ezonEyCUPqlpHFwR4/yIiI1hjjErzraiw91GxrtxpXQClMLNqU+w==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "esbuild": "0.27.0",
+ "prettier": "^3.0.0",
+ "ws": "8.20.1"
+ },
+ "bin": {
+ "convex": "bin/main.js"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=7.0.0"
+ },
+ "peerDependencies": {
+ "@auth0/auth0-react": "^2.0.1",
+ "@clerk/clerk-react": "^4.12.8 || ^5.0.0",
+ "@clerk/react": "^6.4.3",
+ "react": "^18.0.0 || ^19.0.0-0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@auth0/auth0-react": {
+ "optional": true
+ },
+ "@clerk/clerk-react": {
+ "optional": true
+ },
+ "@clerk/react": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/defu": {
+ "version": "6.1.7",
+ "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz",
+ "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==",
+ "license": "MIT"
+ },
"node_modules/dotenv": {
"version": "17.4.2",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz",
@@ -47,6 +884,47 @@
"url": "https://dotenvx.com"
}
},
+ "node_modules/esbuild": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.0.tgz",
+ "integrity": "sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.27.0",
+ "@esbuild/android-arm": "0.27.0",
+ "@esbuild/android-arm64": "0.27.0",
+ "@esbuild/android-x64": "0.27.0",
+ "@esbuild/darwin-arm64": "0.27.0",
+ "@esbuild/darwin-x64": "0.27.0",
+ "@esbuild/freebsd-arm64": "0.27.0",
+ "@esbuild/freebsd-x64": "0.27.0",
+ "@esbuild/linux-arm": "0.27.0",
+ "@esbuild/linux-arm64": "0.27.0",
+ "@esbuild/linux-ia32": "0.27.0",
+ "@esbuild/linux-loong64": "0.27.0",
+ "@esbuild/linux-mips64el": "0.27.0",
+ "@esbuild/linux-ppc64": "0.27.0",
+ "@esbuild/linux-riscv64": "0.27.0",
+ "@esbuild/linux-s390x": "0.27.0",
+ "@esbuild/linux-x64": "0.27.0",
+ "@esbuild/netbsd-arm64": "0.27.0",
+ "@esbuild/netbsd-x64": "0.27.0",
+ "@esbuild/openbsd-arm64": "0.27.0",
+ "@esbuild/openbsd-x64": "0.27.0",
+ "@esbuild/openharmony-arm64": "0.27.0",
+ "@esbuild/sunos-x64": "0.27.0",
+ "@esbuild/win32-arm64": "0.27.0",
+ "@esbuild/win32-ia32": "0.27.0",
+ "@esbuild/win32-x64": "0.27.0"
+ }
+ },
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
@@ -61,6 +939,39 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
+ "node_modules/jose": {
+ "version": "6.2.12",
+ "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.12.tgz",
+ "integrity": "sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/panva"
+ }
+ },
+ "node_modules/kysely": {
+ "version": "0.29.6",
+ "resolved": "https://registry.npmjs.org/kysely/-/kysely-0.29.6.tgz",
+ "integrity": "sha512-hHaB8C/rfzDDtr/t8YZwxAuPJTT0zHyaPoVzcXwDYhYNAgH/4sIfVhi/XLLIY+bL/FqaIJnjATDbi8ObSELmxg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=22.0.0"
+ }
+ },
+ "node_modules/nanostores": {
+ "version": "1.5.4",
+ "resolved": "https://registry.npmjs.org/nanostores/-/nanostores-1.5.4.tgz",
+ "integrity": "sha512-4UXY7OAJEK0Y6V3SudBU7cXjAzTDc189R78dDOh0Xbm19gpKYto6WizSg58LJf12lTsRxKSLGyE117OiLpM4DQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": "^20.0.0 || >=22.0.0"
+ }
+ },
"node_modules/playwright": {
"version": "1.60.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz",
@@ -90,6 +1001,63 @@
"engines": {
"node": ">=18"
}
+ },
+ "node_modules/prettier": {
+ "version": "3.9.9",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.9.tgz",
+ "integrity": "sha512-Z/CJHIkdujO/OtN7nXUii0Rf3VT5SRuhjBA82Xvu2XhBUgX3nhP67T0LHceBdQLex7OOFGTox+Q5Yg8Jk2Qivg==",
+ "license": "MIT",
+ "bin": {
+ "prettier": "bin/prettier.cjs"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
+ }
+ },
+ "node_modules/rou3": {
+ "version": "0.9.2",
+ "resolved": "https://registry.npmjs.org/rou3/-/rou3-0.9.2.tgz",
+ "integrity": "sha512-3SOzvaAg8rkHrXtRjpCvCvbyO5to9oOO27Z/XqHEYXfMRVSw/qMIVdmaOk9W2lcRLtR6dlqTjo9hDeJk70QBYQ==",
+ "license": "MIT"
+ },
+ "node_modules/set-cookie-parser": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.2.tgz",
+ "integrity": "sha512-5/r/lTwbJ3zQ+qwdUFZYeRNqda7P5HD8zQKqlSjdGt1/S0cjLAphHusj4Y58ahDtWn/g32xrIS58/ikOvwl0Lw==",
+ "license": "MIT"
+ },
+ "node_modules/ws": {
+ "version": "8.20.1",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz",
+ "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/zod": {
+ "version": "4.6.5",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.5.tgz",
+ "integrity": "sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
}
}
}
diff --git a/e2e/package.json b/e2e/package.json
index d6d9a9b9..3a6c5ee0 100644
--- a/e2e/package.json
+++ b/e2e/package.json
@@ -7,8 +7,13 @@
"testExistingBackend": "./run-e2e-tests.sh"
},
"dependencies": {
+ "@better-auth/oauth-provider": "1.7.7",
+ "@convex-dev/better-auth": "file:..",
"@playwright/test": "^1.60.0",
"adm-zip": "^0.5.16",
- "dotenv": "^17.0.0"
+ "better-auth": "1.7.7",
+ "convex": "1.41.0",
+ "dotenv": "^17.0.0",
+ "jose": "^6.1.0"
}
}
diff --git a/e2e/playwright.oauth.config.ts b/e2e/playwright.oauth.config.ts
new file mode 100644
index 00000000..7eced2d4
--- /dev/null
+++ b/e2e/playwright.oauth.config.ts
@@ -0,0 +1,13 @@
+import { defineConfig } from "@playwright/test";
+
+export default defineConfig({
+ testDir: "./oauth-provider",
+ forbidOnly: !!process.env.CI,
+ workers: 1,
+ timeout: 60_000,
+ reporter: "list",
+ use: {
+ baseURL: "http://127.0.0.1:3211",
+ extraHTTPHeaders: { origin: "http://127.0.0.1:3211" },
+ },
+});
diff --git a/e2e/run-e2e-tests.sh b/e2e/run-e2e-tests.sh
index 0fc33dea..b425aa2d 100755
--- a/e2e/run-e2e-tests.sh
+++ b/e2e/run-e2e-tests.sh
@@ -27,3 +27,12 @@ popd > /dev/null
echo "Running Playwright tests..."
npx playwright test --config "$SCRIPT_DIR/playwright.config.ts"
+
+pushd "$SCRIPT_DIR" > /dev/null
+for registration in eager lazy; do
+ echo "Running OAuth provider tests ($registration)..."
+ npx convex env set OAUTH_ROUTE_REGISTRATION "$registration" --url "$CONVEX_URL" --admin-key "$ADMIN_KEY"
+ npx convex deploy --url "$CONVEX_URL" --admin-key "$ADMIN_KEY" -y
+ OAUTH_ROUTE_REGISTRATION="$registration" npx playwright test --config "$SCRIPT_DIR/playwright.oauth.config.ts"
+done
+popd > /dev/null
diff --git a/examples/next/app/(auth)/settings/EnableTwoFactor.tsx b/examples/next/app/(auth)/settings/EnableTwoFactor.tsx
index e33859de..f7311e8a 100644
--- a/examples/next/app/(auth)/settings/EnableTwoFactor.tsx
+++ b/examples/next/app/(auth)/settings/EnableTwoFactor.tsx
@@ -53,7 +53,7 @@ export default function EnableTwoFactor() {
const { data } = await authClient.twoFactor.enable({
password,
});
- if (data?.totpURI) {
+ if (data?.method === "totp") {
setTotpUri(data.totpURI);
if (data.backupCodes) {
setBackupCodes(data.backupCodes);
diff --git a/examples/next/app/(unauth)/sign-in/SignIn.tsx b/examples/next/app/(unauth)/sign-in/SignIn.tsx
index 9d5d4a8d..96e6c9bc 100644
--- a/examples/next/app/(unauth)/sign-in/SignIn.tsx
+++ b/examples/next/app/(unauth)/sign-in/SignIn.tsx
@@ -152,9 +152,9 @@ export default function SignIn() {
};
const handleSlackSignIn = async () => {
- await authClient.signIn.oauth2(
+ await authClient.signIn.social(
{
- providerId: "slack",
+ provider: "slack",
},
{
onRequest: () => {
diff --git a/examples/next/convex/betterAuth/_generated/component.ts b/examples/next/convex/betterAuth/_generated/component.ts
index 97ab5f56..6c60a754 100644
--- a/examples/next/convex/betterAuth/_generated/component.ts
+++ b/examples/next/convex/betterAuth/_generated/component.ts
@@ -88,6 +88,8 @@ export type ComponentApi =
| {
data: {
backupCodes: string;
+ failedVerificationCount?: null | number;
+ lockedUntil?: null | number;
secret: string;
userId: string;
verified?: null | boolean;
@@ -96,7 +98,9 @@ export type ComponentApi =
}
| {
data: {
+ alg?: null | string;
createdAt: number;
+ crv?: null | string;
expiresAt?: null | number;
privateKey: string;
publicKey: string;
@@ -271,6 +275,8 @@ export type ComponentApi =
| "backupCodes"
| "userId"
| "verified"
+ | "failedVerificationCount"
+ | "lockedUntil"
| "_id";
mode?: "sensitive" | "insensitive";
operator?:
@@ -303,6 +309,8 @@ export type ComponentApi =
| "privateKey"
| "createdAt"
| "expiresAt"
+ | "alg"
+ | "crv"
| "_id";
mode?: "sensitive" | "insensitive";
operator?:
@@ -501,6 +509,8 @@ export type ComponentApi =
| "backupCodes"
| "userId"
| "verified"
+ | "failedVerificationCount"
+ | "lockedUntil"
| "_id";
mode?: "sensitive" | "insensitive";
operator?:
@@ -533,6 +543,8 @@ export type ComponentApi =
| "privateKey"
| "createdAt"
| "expiresAt"
+ | "alg"
+ | "crv"
| "_id";
mode?: "sensitive" | "insensitive";
operator?:
@@ -654,6 +666,298 @@ export type ComponentApi =
any,
Name
>;
+ incrementOne: FunctionReference<
+ "mutation",
+ "internal",
+ {
+ input:
+ | {
+ increment: Record;
+ model: "user";
+ set?: {
+ createdAt?: number;
+ displayUsername?: null | string;
+ email?: string;
+ emailVerified?: boolean;
+ foo?: null | string;
+ image?: null | string;
+ isAnonymous?: null | boolean;
+ name?: string;
+ twoFactorEnabled?: null | boolean;
+ updatedAt?: number;
+ userId?: null | string;
+ username?: null | string;
+ };
+ where: Array<{
+ connector?: "AND" | "OR";
+ field:
+ | "name"
+ | "email"
+ | "emailVerified"
+ | "image"
+ | "createdAt"
+ | "updatedAt"
+ | "isAnonymous"
+ | "username"
+ | "displayUsername"
+ | "twoFactorEnabled"
+ | "userId"
+ | "foo"
+ | "_id";
+ mode?: "sensitive" | "insensitive";
+ operator?:
+ | "lt"
+ | "lte"
+ | "gt"
+ | "gte"
+ | "eq"
+ | "in"
+ | "not_in"
+ | "ne"
+ | "contains"
+ | "starts_with"
+ | "ends_with";
+ value:
+ | string
+ | number
+ | boolean
+ | Array
+ | Array
+ | null;
+ }>;
+ }
+ | {
+ increment: Record;
+ model: "session";
+ set?: {
+ createdAt?: number;
+ expiresAt?: number;
+ ipAddress?: null | string;
+ token?: string;
+ updatedAt?: number;
+ userAgent?: null | string;
+ userId?: string;
+ };
+ where: Array<{
+ connector?: "AND" | "OR";
+ field:
+ | "expiresAt"
+ | "token"
+ | "createdAt"
+ | "updatedAt"
+ | "ipAddress"
+ | "userAgent"
+ | "userId"
+ | "_id";
+ mode?: "sensitive" | "insensitive";
+ operator?:
+ | "lt"
+ | "lte"
+ | "gt"
+ | "gte"
+ | "eq"
+ | "in"
+ | "not_in"
+ | "ne"
+ | "contains"
+ | "starts_with"
+ | "ends_with";
+ value:
+ | string
+ | number
+ | boolean
+ | Array
+ | Array
+ | null;
+ }>;
+ }
+ | {
+ increment: Record;
+ model: "account";
+ set?: {
+ accessToken?: null | string;
+ accessTokenExpiresAt?: null | number;
+ accountId?: string;
+ createdAt?: number;
+ idToken?: null | string;
+ password?: null | string;
+ providerId?: string;
+ refreshToken?: null | string;
+ refreshTokenExpiresAt?: null | number;
+ scope?: null | string;
+ updatedAt?: number;
+ userId?: string;
+ };
+ where: Array<{
+ connector?: "AND" | "OR";
+ field:
+ | "accountId"
+ | "providerId"
+ | "userId"
+ | "accessToken"
+ | "refreshToken"
+ | "idToken"
+ | "accessTokenExpiresAt"
+ | "refreshTokenExpiresAt"
+ | "scope"
+ | "password"
+ | "createdAt"
+ | "updatedAt"
+ | "_id";
+ mode?: "sensitive" | "insensitive";
+ operator?:
+ | "lt"
+ | "lte"
+ | "gt"
+ | "gte"
+ | "eq"
+ | "in"
+ | "not_in"
+ | "ne"
+ | "contains"
+ | "starts_with"
+ | "ends_with";
+ value:
+ | string
+ | number
+ | boolean
+ | Array
+ | Array
+ | null;
+ }>;
+ }
+ | {
+ increment: Record;
+ model: "verification";
+ set?: {
+ createdAt?: number;
+ expiresAt?: number;
+ identifier?: string;
+ updatedAt?: number;
+ value?: string;
+ };
+ where: Array<{
+ connector?: "AND" | "OR";
+ field:
+ | "identifier"
+ | "value"
+ | "expiresAt"
+ | "createdAt"
+ | "updatedAt"
+ | "_id";
+ mode?: "sensitive" | "insensitive";
+ operator?:
+ | "lt"
+ | "lte"
+ | "gt"
+ | "gte"
+ | "eq"
+ | "in"
+ | "not_in"
+ | "ne"
+ | "contains"
+ | "starts_with"
+ | "ends_with";
+ value:
+ | string
+ | number
+ | boolean
+ | Array
+ | Array
+ | null;
+ }>;
+ }
+ | {
+ increment: Record;
+ model: "twoFactor";
+ set?: {
+ backupCodes?: string;
+ failedVerificationCount?: null | number;
+ lockedUntil?: null | number;
+ secret?: string;
+ userId?: string;
+ verified?: null | boolean;
+ };
+ where: Array<{
+ connector?: "AND" | "OR";
+ field:
+ | "secret"
+ | "backupCodes"
+ | "userId"
+ | "verified"
+ | "failedVerificationCount"
+ | "lockedUntil"
+ | "_id";
+ mode?: "sensitive" | "insensitive";
+ operator?:
+ | "lt"
+ | "lte"
+ | "gt"
+ | "gte"
+ | "eq"
+ | "in"
+ | "not_in"
+ | "ne"
+ | "contains"
+ | "starts_with"
+ | "ends_with";
+ value:
+ | string
+ | number
+ | boolean
+ | Array
+ | Array
+ | null;
+ }>;
+ }
+ | {
+ increment: Record;
+ model: "jwks";
+ set?: {
+ alg?: null | string;
+ createdAt?: number;
+ crv?: null | string;
+ expiresAt?: null | number;
+ privateKey?: string;
+ publicKey?: string;
+ };
+ where: Array<{
+ connector?: "AND" | "OR";
+ field:
+ | "publicKey"
+ | "privateKey"
+ | "createdAt"
+ | "expiresAt"
+ | "alg"
+ | "crv"
+ | "_id";
+ mode?: "sensitive" | "insensitive";
+ operator?:
+ | "lt"
+ | "lte"
+ | "gt"
+ | "gte"
+ | "eq"
+ | "in"
+ | "not_in"
+ | "ne"
+ | "contains"
+ | "starts_with"
+ | "ends_with";
+ value:
+ | string
+ | number
+ | boolean
+ | Array
+ | Array
+ | null;
+ }>;
+ };
+ onUpdateHandle?: string;
+ },
+ any,
+ Name
+ >;
updateMany: FunctionReference<
"mutation",
"internal",
@@ -855,6 +1159,8 @@ export type ComponentApi =
model: "twoFactor";
update: {
backupCodes?: string;
+ failedVerificationCount?: null | number;
+ lockedUntil?: null | number;
secret?: string;
userId?: string;
verified?: null | boolean;
@@ -866,6 +1172,8 @@ export type ComponentApi =
| "backupCodes"
| "userId"
| "verified"
+ | "failedVerificationCount"
+ | "lockedUntil"
| "_id";
mode?: "sensitive" | "insensitive";
operator?:
@@ -892,7 +1200,9 @@ export type ComponentApi =
| {
model: "jwks";
update: {
+ alg?: null | string;
createdAt?: number;
+ crv?: null | string;
expiresAt?: null | number;
privateKey?: string;
publicKey?: string;
@@ -904,6 +1214,8 @@ export type ComponentApi =
| "privateKey"
| "createdAt"
| "expiresAt"
+ | "alg"
+ | "crv"
| "_id";
mode?: "sensitive" | "insensitive";
operator?:
@@ -1141,6 +1453,8 @@ export type ComponentApi =
model: "twoFactor";
update: {
backupCodes?: string;
+ failedVerificationCount?: null | number;
+ lockedUntil?: null | number;
secret?: string;
userId?: string;
verified?: null | boolean;
@@ -1152,6 +1466,8 @@ export type ComponentApi =
| "backupCodes"
| "userId"
| "verified"
+ | "failedVerificationCount"
+ | "lockedUntil"
| "_id";
mode?: "sensitive" | "insensitive";
operator?:
@@ -1178,7 +1494,9 @@ export type ComponentApi =
| {
model: "jwks";
update: {
+ alg?: null | string;
createdAt?: number;
+ crv?: null | string;
expiresAt?: null | number;
privateKey?: string;
publicKey?: string;
@@ -1190,6 +1508,8 @@ export type ComponentApi =
| "privateKey"
| "createdAt"
| "expiresAt"
+ | "alg"
+ | "crv"
| "_id";
mode?: "sensitive" | "insensitive";
operator?:
diff --git a/examples/next/convex/betterAuth/adapter.ts b/examples/next/convex/betterAuth/adapter.ts
index 58547fd9..69c8c7c1 100644
--- a/examples/next/convex/betterAuth/adapter.ts
+++ b/examples/next/convex/betterAuth/adapter.ts
@@ -7,6 +7,7 @@ export const {
findOne,
findMany,
updateOne,
+ incrementOne,
updateMany,
deleteOne,
deleteMany,
diff --git a/examples/next/convex/betterAuth/generatedSchema.ts b/examples/next/convex/betterAuth/generatedSchema.ts
index c352749c..2e6af099 100644
--- a/examples/next/convex/betterAuth/generatedSchema.ts
+++ b/examples/next/convex/betterAuth/generatedSchema.ts
@@ -28,7 +28,7 @@ export const tables = {
userId: v.optional(v.union(v.null(), v.string())),
foo: v.optional(v.union(v.null(), v.string())),
})
- .index("email_name", ["email","name"])
+ .index("email_name", ["email", "name"])
.index("name", ["name"])
.index("userId", ["userId"])
.index("username", ["username"]),
@@ -42,7 +42,7 @@ export const tables = {
userId: v.string(),
})
.index("expiresAt", ["expiresAt"])
- .index("expiresAt_userId", ["expiresAt","userId"])
+ .index("expiresAt_userId", ["expiresAt", "userId"])
.index("token", ["token"])
.index("userId", ["userId"]),
account: defineTable({
@@ -60,8 +60,13 @@ export const tables = {
updatedAt: v.number(),
})
.index("accountId", ["accountId"])
- .index("accountId_providerId", ["accountId","providerId"])
- .index("providerId_userId", ["providerId","userId"])
+ .index("accountId_providerId", ["accountId", "providerId"])
+ .index("providerId_userId", ["providerId", "userId"])
+ .index("userId_providerId_accountId", [
+ "userId",
+ "providerId",
+ "accountId",
+ ])
.index("userId", ["userId"]),
verification: defineTable({
identifier: v.string(),
@@ -77,13 +82,16 @@ export const tables = {
backupCodes: v.string(),
userId: v.string(),
verified: v.optional(v.union(v.null(), v.boolean())),
- })
- .index("userId", ["userId"]),
+ failedVerificationCount: v.optional(v.union(v.null(), v.number())),
+ lockedUntil: v.optional(v.union(v.null(), v.number())),
+ }).index("userId", ["userId"]),
jwks: defineTable({
publicKey: v.string(),
privateKey: v.string(),
createdAt: v.number(),
expiresAt: v.optional(v.union(v.null(), v.number())),
+ alg: v.optional(v.union(v.null(), v.string())),
+ crv: v.optional(v.union(v.null(), v.string())),
}),
};
diff --git a/examples/next/lib/auth-client.tsx b/examples/next/lib/auth-client.tsx
index 6f5ca766..00128b37 100644
--- a/examples/next/lib/auth-client.tsx
+++ b/examples/next/lib/auth-client.tsx
@@ -4,7 +4,6 @@ import {
twoFactorClient,
magicLinkClient,
emailOTPClient,
- genericOAuthClient,
anonymousClient,
inferAdditionalFields,
} from "better-auth/client/plugins";
@@ -24,7 +23,6 @@ export const authClient = createAuthClient({
magicLinkClient(),
emailOTPClient(),
twoFactorClient(),
- genericOAuthClient(),
convexClient(),
],
});
diff --git a/examples/next/package-lock.json b/examples/next/package-lock.json
index 40c7eae8..1732847a 100644
--- a/examples/next/package-lock.json
+++ b/examples/next/package-lock.json
@@ -52,7 +52,7 @@
"version": "0.12.5",
"license": "Apache-2.0",
"dependencies": {
- "@better-fetch/fetch": "^1.1.18",
+ "@better-fetch/fetch": "^1.3.2",
"common-tags": "^1.8.2",
"convex-helpers": "^0.1.128",
"jose": "^6.1.0",
@@ -62,8 +62,8 @@
"zod": "^4.0.0"
},
"devDependencies": {
- "@better-auth/core": "1.6.15",
- "@better-auth/test-utils": "1.6.15",
+ "@better-auth/core": "1.7.7",
+ "@better-auth/test-utils": "1.7.7",
"@convex-dev/eslint-plugin": "2.0.0",
"@edge-runtime/vm": "5.0.0",
"@eslint/eslintrc": "3.3.5",
@@ -74,7 +74,7 @@
"@types/react": "19.2.14",
"@types/react-dom": "19.2.3",
"@types/semver": "7.7.1",
- "better-auth": "1.6.15",
+ "better-auth": "1.7.7",
"chokidar-cli": "3.0.0",
"concurrently": "10.0.3",
"convex": "1.46.0",
@@ -92,34 +92,34 @@
"react-dom": "19.2.6",
"typescript": "5.9.3",
"typescript-eslint": "8.59.2",
- "vitest": "4.1.5"
+ "vitest": "4.1.10"
},
"peerDependencies": {
- "better-auth": ">=1.6.11 <1.7.0",
+ "better-auth": ">=1.7.7 <1.8.0",
"convex": "^1.25.0",
"react": "^18.3.1 || ^19.0.0"
}
},
"../../node_modules/better-auth": {
- "version": "1.6.15",
- "license": "MIT",
- "dependencies": {
- "@better-auth/core": "1.6.15",
- "@better-auth/drizzle-adapter": "1.6.15",
- "@better-auth/kysely-adapter": "1.6.15",
- "@better-auth/memory-adapter": "1.6.15",
- "@better-auth/mongo-adapter": "1.6.15",
- "@better-auth/prisma-adapter": "1.6.15",
- "@better-auth/telemetry": "1.6.15",
- "@better-auth/utils": "0.4.1",
- "@better-fetch/fetch": "1.1.21",
- "@noble/ciphers": "^2.1.1",
- "@noble/hashes": "^2.0.1",
- "better-call": "1.3.5",
+ "version": "1.7.1",
+ "license": "MIT",
+ "dependencies": {
+ "@better-auth/core": "1.7.1",
+ "@better-auth/drizzle-adapter": "1.7.1",
+ "@better-auth/kysely-adapter": "1.7.1",
+ "@better-auth/memory-adapter": "1.7.1",
+ "@better-auth/mongo-adapter": "1.7.1",
+ "@better-auth/prisma-adapter": "1.7.1",
+ "@better-auth/telemetry": "1.7.1",
+ "@better-auth/utils": "0.4.2",
+ "@better-fetch/fetch": "1.3.1",
+ "@noble/ciphers": "^2.2.0",
+ "@noble/hashes": "^2.2.0",
+ "better-call": "1.4.0",
"defu": "^6.1.4",
- "jose": "^6.1.3",
+ "jose": "^6.2.3",
"kysely": "^0.28.17 || ^0.29.0",
- "nanostores": "^1.1.1",
+ "nanostores": "^1.3.0",
"zod": "^4.3.6"
},
"peerDependencies": {
@@ -129,8 +129,8 @@
"@tanstack/react-start": "^1.0.0",
"@tanstack/solid-start": "^1.0.0",
"better-sqlite3": "^12.0.0",
- "drizzle-kit": ">=0.31.4",
- "drizzle-orm": "^0.45.2",
+ "drizzle-kit": ">=0.31.4 || >=1.0.0-beta.1",
+ "drizzle-orm": "^0.45.2 || >=1.0.0-rc.1 <2.0.0",
"mongodb": "^6.0.0 || ^7.0.0",
"mysql2": "^3.0.0",
"next": "^14.0.0 || ^15.0.0 || ^16.0.0",
diff --git a/examples/react/package-lock.json b/examples/react/package-lock.json
index 249da82b..0f60d0dc 100644
--- a/examples/react/package-lock.json
+++ b/examples/react/package-lock.json
@@ -51,7 +51,7 @@
"version": "0.12.5",
"license": "Apache-2.0",
"dependencies": {
- "@better-fetch/fetch": "^1.1.18",
+ "@better-fetch/fetch": "^1.3.2",
"common-tags": "^1.8.2",
"convex-helpers": "^0.1.128",
"jose": "^6.1.0",
@@ -61,8 +61,8 @@
"zod": "^4.0.0"
},
"devDependencies": {
- "@better-auth/core": "1.6.15",
- "@better-auth/test-utils": "1.6.15",
+ "@better-auth/core": "1.7.7",
+ "@better-auth/test-utils": "1.7.7",
"@convex-dev/eslint-plugin": "2.0.0",
"@edge-runtime/vm": "5.0.0",
"@eslint/eslintrc": "3.3.5",
@@ -73,7 +73,7 @@
"@types/react": "19.2.14",
"@types/react-dom": "19.2.3",
"@types/semver": "7.7.1",
- "better-auth": "1.6.15",
+ "better-auth": "1.7.7",
"chokidar-cli": "3.0.0",
"concurrently": "10.0.3",
"convex": "1.46.0",
@@ -91,34 +91,34 @@
"react-dom": "19.2.6",
"typescript": "5.9.3",
"typescript-eslint": "8.59.2",
- "vitest": "4.1.5"
+ "vitest": "4.1.10"
},
"peerDependencies": {
- "better-auth": ">=1.6.11 <1.7.0",
+ "better-auth": ">=1.7.7 <1.8.0",
"convex": "^1.25.0",
"react": "^18.3.1 || ^19.0.0"
}
},
"../../node_modules/better-auth": {
- "version": "1.6.15",
- "license": "MIT",
- "dependencies": {
- "@better-auth/core": "1.6.15",
- "@better-auth/drizzle-adapter": "1.6.15",
- "@better-auth/kysely-adapter": "1.6.15",
- "@better-auth/memory-adapter": "1.6.15",
- "@better-auth/mongo-adapter": "1.6.15",
- "@better-auth/prisma-adapter": "1.6.15",
- "@better-auth/telemetry": "1.6.15",
- "@better-auth/utils": "0.4.1",
- "@better-fetch/fetch": "1.1.21",
- "@noble/ciphers": "^2.1.1",
- "@noble/hashes": "^2.0.1",
- "better-call": "1.3.5",
+ "version": "1.7.1",
+ "license": "MIT",
+ "dependencies": {
+ "@better-auth/core": "1.7.1",
+ "@better-auth/drizzle-adapter": "1.7.1",
+ "@better-auth/kysely-adapter": "1.7.1",
+ "@better-auth/memory-adapter": "1.7.1",
+ "@better-auth/mongo-adapter": "1.7.1",
+ "@better-auth/prisma-adapter": "1.7.1",
+ "@better-auth/telemetry": "1.7.1",
+ "@better-auth/utils": "0.4.2",
+ "@better-fetch/fetch": "1.3.1",
+ "@noble/ciphers": "^2.2.0",
+ "@noble/hashes": "^2.2.0",
+ "better-call": "1.4.0",
"defu": "^6.1.4",
- "jose": "^6.1.3",
+ "jose": "^6.2.3",
"kysely": "^0.28.17 || ^0.29.0",
- "nanostores": "^1.1.1",
+ "nanostores": "^1.3.0",
"zod": "^4.3.6"
},
"peerDependencies": {
@@ -128,8 +128,8 @@
"@tanstack/react-start": "^1.0.0",
"@tanstack/solid-start": "^1.0.0",
"better-sqlite3": "^12.0.0",
- "drizzle-kit": ">=0.31.4",
- "drizzle-orm": "^0.45.2",
+ "drizzle-kit": ">=0.31.4 || >=1.0.0-beta.1",
+ "drizzle-orm": "^0.45.2 || >=1.0.0-rc.1 <2.0.0",
"mongodb": "^6.0.0 || ^7.0.0",
"mysql2": "^3.0.0",
"next": "^14.0.0 || ^15.0.0 || ^16.0.0",
diff --git a/examples/tanstack/convex/betterAuth/_generated/component.ts b/examples/tanstack/convex/betterAuth/_generated/component.ts
index 14a14be6..832040aa 100644
--- a/examples/tanstack/convex/betterAuth/_generated/component.ts
+++ b/examples/tanstack/convex/betterAuth/_generated/component.ts
@@ -8,7 +8,7 @@
* @module
*/
-import type { FunctionReference } from "convex/server";
+import type { FunctionReference } from 'convex/server'
/**
* A utility for referencing a Convex component's exposed API.
@@ -25,1184 +25,1500 @@ export type ComponentApi =
{
adapter: {
create: FunctionReference<
- "mutation",
- "internal",
+ 'mutation',
+ 'internal',
{
input:
| {
data: {
- createdAt: number;
- email: string;
- emailVerified: boolean;
- foo?: null | string;
- image?: null | string;
- isAnonymous?: null | boolean;
- name: string;
- twoFactorEnabled?: null | boolean;
- updatedAt: number;
- userId?: null | string;
- };
- model: "user";
+ createdAt: number
+ email: string
+ emailVerified: boolean
+ foo?: null | string
+ image?: null | string
+ isAnonymous?: null | boolean
+ name: string
+ twoFactorEnabled?: null | boolean
+ updatedAt: number
+ userId?: null | string
+ }
+ model: 'user'
}
| {
data: {
- createdAt: number;
- expiresAt: number;
- ipAddress?: null | string;
- token: string;
- updatedAt: number;
- userAgent?: null | string;
- userId: string;
- };
- model: "session";
+ createdAt: number
+ expiresAt: number
+ ipAddress?: null | string
+ token: string
+ updatedAt: number
+ userAgent?: null | string
+ userId: string
+ }
+ model: 'session'
}
| {
data: {
- accessToken?: null | string;
- accessTokenExpiresAt?: null | number;
- accountId: string;
- createdAt: number;
- idToken?: null | string;
- password?: null | string;
- providerId: string;
- refreshToken?: null | string;
- refreshTokenExpiresAt?: null | number;
- scope?: null | string;
- updatedAt: number;
- userId: string;
- };
- model: "account";
+ accessToken?: null | string
+ accessTokenExpiresAt?: null | number
+ accountId: string
+ createdAt: number
+ idToken?: null | string
+ password?: null | string
+ providerId: string
+ refreshToken?: null | string
+ refreshTokenExpiresAt?: null | number
+ scope?: null | string
+ updatedAt: number
+ userId: string
+ }
+ model: 'account'
}
| {
data: {
- createdAt: number;
- expiresAt: number;
- identifier: string;
- updatedAt: number;
- value: string;
- };
- model: "verification";
+ createdAt: number
+ expiresAt: number
+ identifier: string
+ updatedAt: number
+ value: string
+ }
+ model: 'verification'
}
| {
data: {
- backupCodes: string;
- secret: string;
- userId: string;
- verified?: null | boolean;
- };
- model: "twoFactor";
+ backupCodes: string
+ failedVerificationCount?: null | number
+ lockedUntil?: null | number
+ secret: string
+ userId: string
+ verified?: null | boolean
+ }
+ model: 'twoFactor'
}
| {
data: {
- createdAt: number;
- expiresAt?: null | number;
- privateKey: string;
- publicKey: string;
- };
- model: "jwks";
- };
- onCreateHandle?: string;
- select?: Array;
+ alg?: null | string
+ createdAt: number
+ crv?: null | string
+ expiresAt?: null | number
+ privateKey: string
+ publicKey: string
+ }
+ model: 'jwks'
+ }
+ onCreateHandle?: string
+ select?: Array
},
any,
Name
- >;
+ >
deleteMany: FunctionReference<
- "mutation",
- "internal",
+ 'mutation',
+ 'internal',
{
input:
| {
- model: "user";
+ model: 'user'
where?: Array<{
- connector?: "AND" | "OR";
+ connector?: 'AND' | 'OR'
field:
- | "name"
- | "email"
- | "emailVerified"
- | "image"
- | "createdAt"
- | "updatedAt"
- | "twoFactorEnabled"
- | "isAnonymous"
- | "userId"
- | "foo"
- | "_id";
- mode?: "sensitive" | "insensitive";
+ | 'name'
+ | 'email'
+ | 'emailVerified'
+ | 'image'
+ | 'createdAt'
+ | 'updatedAt'
+ | 'twoFactorEnabled'
+ | 'isAnonymous'
+ | 'userId'
+ | 'foo'
+ | '_id'
+ mode?: 'sensitive' | 'insensitive'
operator?:
- | "lt"
- | "lte"
- | "gt"
- | "gte"
- | "eq"
- | "in"
- | "not_in"
- | "ne"
- | "contains"
- | "starts_with"
- | "ends_with";
+ | 'lt'
+ | 'lte'
+ | 'gt'
+ | 'gte'
+ | 'eq'
+ | 'in'
+ | 'not_in'
+ | 'ne'
+ | 'contains'
+ | 'starts_with'
+ | 'ends_with'
value:
| string
| number
| boolean
| Array
| Array
- | null;
- }>;
+ | null
+ }>
}
| {
- model: "session";
+ model: 'session'
where?: Array<{
- connector?: "AND" | "OR";
+ connector?: 'AND' | 'OR'
field:
- | "expiresAt"
- | "token"
- | "createdAt"
- | "updatedAt"
- | "ipAddress"
- | "userAgent"
- | "userId"
- | "_id";
- mode?: "sensitive" | "insensitive";
+ | 'expiresAt'
+ | 'token'
+ | 'createdAt'
+ | 'updatedAt'
+ | 'ipAddress'
+ | 'userAgent'
+ | 'userId'
+ | '_id'
+ mode?: 'sensitive' | 'insensitive'
operator?:
- | "lt"
- | "lte"
- | "gt"
- | "gte"
- | "eq"
- | "in"
- | "not_in"
- | "ne"
- | "contains"
- | "starts_with"
- | "ends_with";
+ | 'lt'
+ | 'lte'
+ | 'gt'
+ | 'gte'
+ | 'eq'
+ | 'in'
+ | 'not_in'
+ | 'ne'
+ | 'contains'
+ | 'starts_with'
+ | 'ends_with'
value:
| string
| number
| boolean
| Array
| Array
- | null;
- }>;
+ | null
+ }>
}
| {
- model: "account";
+ model: 'account'
where?: Array<{
- connector?: "AND" | "OR";
+ connector?: 'AND' | 'OR'
field:
- | "accountId"
- | "providerId"
- | "userId"
- | "accessToken"
- | "refreshToken"
- | "idToken"
- | "accessTokenExpiresAt"
- | "refreshTokenExpiresAt"
- | "scope"
- | "password"
- | "createdAt"
- | "updatedAt"
- | "_id";
- mode?: "sensitive" | "insensitive";
+ | 'accountId'
+ | 'providerId'
+ | 'userId'
+ | 'accessToken'
+ | 'refreshToken'
+ | 'idToken'
+ | 'accessTokenExpiresAt'
+ | 'refreshTokenExpiresAt'
+ | 'scope'
+ | 'password'
+ | 'createdAt'
+ | 'updatedAt'
+ | '_id'
+ mode?: 'sensitive' | 'insensitive'
operator?:
- | "lt"
- | "lte"
- | "gt"
- | "gte"
- | "eq"
- | "in"
- | "not_in"
- | "ne"
- | "contains"
- | "starts_with"
- | "ends_with";
+ | 'lt'
+ | 'lte'
+ | 'gt'
+ | 'gte'
+ | 'eq'
+ | 'in'
+ | 'not_in'
+ | 'ne'
+ | 'contains'
+ | 'starts_with'
+ | 'ends_with'
value:
| string
| number
| boolean
| Array
| Array
- | null;
- }>;
+ | null
+ }>
}
| {
- model: "verification";
+ model: 'verification'
where?: Array<{
- connector?: "AND" | "OR";
+ connector?: 'AND' | 'OR'
field:
- | "identifier"
- | "value"
- | "expiresAt"
- | "createdAt"
- | "updatedAt"
- | "_id";
- mode?: "sensitive" | "insensitive";
+ | 'identifier'
+ | 'value'
+ | 'expiresAt'
+ | 'createdAt'
+ | 'updatedAt'
+ | '_id'
+ mode?: 'sensitive' | 'insensitive'
operator?:
- | "lt"
- | "lte"
- | "gt"
- | "gte"
- | "eq"
- | "in"
- | "not_in"
- | "ne"
- | "contains"
- | "starts_with"
- | "ends_with";
+ | 'lt'
+ | 'lte'
+ | 'gt'
+ | 'gte'
+ | 'eq'
+ | 'in'
+ | 'not_in'
+ | 'ne'
+ | 'contains'
+ | 'starts_with'
+ | 'ends_with'
value:
| string
| number
| boolean
| Array
| Array
- | null;
- }>;
+ | null
+ }>
}
| {
- model: "twoFactor";
+ model: 'twoFactor'
where?: Array<{
- connector?: "AND" | "OR";
+ connector?: 'AND' | 'OR'
field:
- | "secret"
- | "backupCodes"
- | "userId"
- | "verified"
- | "_id";
- mode?: "sensitive" | "insensitive";
+ | 'secret'
+ | 'backupCodes'
+ | 'userId'
+ | 'verified'
+ | 'failedVerificationCount'
+ | 'lockedUntil'
+ | '_id'
+ mode?: 'sensitive' | 'insensitive'
operator?:
- | "lt"
- | "lte"
- | "gt"
- | "gte"
- | "eq"
- | "in"
- | "not_in"
- | "ne"
- | "contains"
- | "starts_with"
- | "ends_with";
+ | 'lt'
+ | 'lte'
+ | 'gt'
+ | 'gte'
+ | 'eq'
+ | 'in'
+ | 'not_in'
+ | 'ne'
+ | 'contains'
+ | 'starts_with'
+ | 'ends_with'
value:
| string
| number
| boolean
| Array
| Array
- | null;
- }>;
+ | null
+ }>
}
| {
- model: "jwks";
+ model: 'jwks'
where?: Array<{
- connector?: "AND" | "OR";
+ connector?: 'AND' | 'OR'
field:
- | "publicKey"
- | "privateKey"
- | "createdAt"
- | "expiresAt"
- | "_id";
- mode?: "sensitive" | "insensitive";
+ | 'publicKey'
+ | 'privateKey'
+ | 'createdAt'
+ | 'expiresAt'
+ | 'alg'
+ | 'crv'
+ | '_id'
+ mode?: 'sensitive' | 'insensitive'
operator?:
- | "lt"
- | "lte"
- | "gt"
- | "gte"
- | "eq"
- | "in"
- | "not_in"
- | "ne"
- | "contains"
- | "starts_with"
- | "ends_with";
+ | 'lt'
+ | 'lte'
+ | 'gt'
+ | 'gte'
+ | 'eq'
+ | 'in'
+ | 'not_in'
+ | 'ne'
+ | 'contains'
+ | 'starts_with'
+ | 'ends_with'
value:
| string
| number
| boolean
| Array
| Array
- | null;
- }>;
- };
- onDeleteHandle?: string;
+ | null
+ }>
+ }
+ onDeleteHandle?: string
paginationOpts: {
- cursor: string | null;
- endCursor?: string | null;
- id?: number;
- maximumBytesRead?: number;
- maximumRowsRead?: number;
- numItems: number;
- };
+ cursor: string | null
+ endCursor?: string | null
+ id?: number
+ maximumBytesRead?: number
+ maximumRowsRead?: number
+ numItems: number
+ }
},
any,
Name
- >;
+ >
deleteOne: FunctionReference<
- "mutation",
- "internal",
+ 'mutation',
+ 'internal',
{
input:
| {
- model: "user";
+ model: 'user'
where?: Array<{
- connector?: "AND" | "OR";
+ connector?: 'AND' | 'OR'
field:
- | "name"
- | "email"
- | "emailVerified"
- | "image"
- | "createdAt"
- | "updatedAt"
- | "twoFactorEnabled"
- | "isAnonymous"
- | "userId"
- | "foo"
- | "_id";
- mode?: "sensitive" | "insensitive";
+ | 'name'
+ | 'email'
+ | 'emailVerified'
+ | 'image'
+ | 'createdAt'
+ | 'updatedAt'
+ | 'twoFactorEnabled'
+ | 'isAnonymous'
+ | 'userId'
+ | 'foo'
+ | '_id'
+ mode?: 'sensitive' | 'insensitive'
operator?:
- | "lt"
- | "lte"
- | "gt"
- | "gte"
- | "eq"
- | "in"
- | "not_in"
- | "ne"
- | "contains"
- | "starts_with"
- | "ends_with";
+ | 'lt'
+ | 'lte'
+ | 'gt'
+ | 'gte'
+ | 'eq'
+ | 'in'
+ | 'not_in'
+ | 'ne'
+ | 'contains'
+ | 'starts_with'
+ | 'ends_with'
value:
| string
| number
| boolean
| Array
| Array
- | null;
- }>;
+ | null
+ }>
}
| {
- model: "session";
+ model: 'session'
where?: Array<{
- connector?: "AND" | "OR";
+ connector?: 'AND' | 'OR'
field:
- | "expiresAt"
- | "token"
- | "createdAt"
- | "updatedAt"
- | "ipAddress"
- | "userAgent"
- | "userId"
- | "_id";
- mode?: "sensitive" | "insensitive";
+ | 'expiresAt'
+ | 'token'
+ | 'createdAt'
+ | 'updatedAt'
+ | 'ipAddress'
+ | 'userAgent'
+ | 'userId'
+ | '_id'
+ mode?: 'sensitive' | 'insensitive'
operator?:
- | "lt"
- | "lte"
- | "gt"
- | "gte"
- | "eq"
- | "in"
- | "not_in"
- | "ne"
- | "contains"
- | "starts_with"
- | "ends_with";
+ | 'lt'
+ | 'lte'
+ | 'gt'
+ | 'gte'
+ | 'eq'
+ | 'in'
+ | 'not_in'
+ | 'ne'
+ | 'contains'
+ | 'starts_with'
+ | 'ends_with'
value:
| string
| number
| boolean
| Array
| Array
- | null;
- }>;
+ | null
+ }>
}
| {
- model: "account";
+ model: 'account'
where?: Array<{
- connector?: "AND" | "OR";
+ connector?: 'AND' | 'OR'
field:
- | "accountId"
- | "providerId"
- | "userId"
- | "accessToken"
- | "refreshToken"
- | "idToken"
- | "accessTokenExpiresAt"
- | "refreshTokenExpiresAt"
- | "scope"
- | "password"
- | "createdAt"
- | "updatedAt"
- | "_id";
- mode?: "sensitive" | "insensitive";
+ | 'accountId'
+ | 'providerId'
+ | 'userId'
+ | 'accessToken'
+ | 'refreshToken'
+ | 'idToken'
+ | 'accessTokenExpiresAt'
+ | 'refreshTokenExpiresAt'
+ | 'scope'
+ | 'password'
+ | 'createdAt'
+ | 'updatedAt'
+ | '_id'
+ mode?: 'sensitive' | 'insensitive'
operator?:
- | "lt"
- | "lte"
- | "gt"
- | "gte"
- | "eq"
- | "in"
- | "not_in"
- | "ne"
- | "contains"
- | "starts_with"
- | "ends_with";
+ | 'lt'
+ | 'lte'
+ | 'gt'
+ | 'gte'
+ | 'eq'
+ | 'in'
+ | 'not_in'
+ | 'ne'
+ | 'contains'
+ | 'starts_with'
+ | 'ends_with'
value:
| string
| number
| boolean
| Array
| Array
- | null;
- }>;
+ | null
+ }>
}
| {
- model: "verification";
+ model: 'verification'
where?: Array<{
- connector?: "AND" | "OR";
+ connector?: 'AND' | 'OR'
field:
- | "identifier"
- | "value"
- | "expiresAt"
- | "createdAt"
- | "updatedAt"
- | "_id";
- mode?: "sensitive" | "insensitive";
+ | 'identifier'
+ | 'value'
+ | 'expiresAt'
+ | 'createdAt'
+ | 'updatedAt'
+ | '_id'
+ mode?: 'sensitive' | 'insensitive'
operator?:
- | "lt"
- | "lte"
- | "gt"
- | "gte"
- | "eq"
- | "in"
- | "not_in"
- | "ne"
- | "contains"
- | "starts_with"
- | "ends_with";
+ | 'lt'
+ | 'lte'
+ | 'gt'
+ | 'gte'
+ | 'eq'
+ | 'in'
+ | 'not_in'
+ | 'ne'
+ | 'contains'
+ | 'starts_with'
+ | 'ends_with'
value:
| string
| number
| boolean
| Array
| Array
- | null;
- }>;
+ | null
+ }>
}
| {
- model: "twoFactor";
+ model: 'twoFactor'
where?: Array<{
- connector?: "AND" | "OR";
+ connector?: 'AND' | 'OR'
field:
- | "secret"
- | "backupCodes"
- | "userId"
- | "verified"
- | "_id";
- mode?: "sensitive" | "insensitive";
+ | 'secret'
+ | 'backupCodes'
+ | 'userId'
+ | 'verified'
+ | 'failedVerificationCount'
+ | 'lockedUntil'
+ | '_id'
+ mode?: 'sensitive' | 'insensitive'
operator?:
- | "lt"
- | "lte"
- | "gt"
- | "gte"
- | "eq"
- | "in"
- | "not_in"
- | "ne"
- | "contains"
- | "starts_with"
- | "ends_with";
+ | 'lt'
+ | 'lte'
+ | 'gt'
+ | 'gte'
+ | 'eq'
+ | 'in'
+ | 'not_in'
+ | 'ne'
+ | 'contains'
+ | 'starts_with'
+ | 'ends_with'
value:
| string
| number
| boolean
| Array
| Array
- | null;
- }>;
+ | null
+ }>
}
| {
- model: "jwks";
+ model: 'jwks'
where?: Array<{
- connector?: "AND" | "OR";
+ connector?: 'AND' | 'OR'
field:
- | "publicKey"
- | "privateKey"
- | "createdAt"
- | "expiresAt"
- | "_id";
- mode?: "sensitive" | "insensitive";
+ | 'publicKey'
+ | 'privateKey'
+ | 'createdAt'
+ | 'expiresAt'
+ | 'alg'
+ | 'crv'
+ | '_id'
+ mode?: 'sensitive' | 'insensitive'
operator?:
- | "lt"
- | "lte"
- | "gt"
- | "gte"
- | "eq"
- | "in"
- | "not_in"
- | "ne"
- | "contains"
- | "starts_with"
- | "ends_with";
+ | 'lt'
+ | 'lte'
+ | 'gt'
+ | 'gte'
+ | 'eq'
+ | 'in'
+ | 'not_in'
+ | 'ne'
+ | 'contains'
+ | 'starts_with'
+ | 'ends_with'
value:
| string
| number
| boolean
| Array
| Array
- | null;
- }>;
- };
- onDeleteHandle?: string;
+ | null
+ }>
+ }
+ onDeleteHandle?: string
},
any,
Name
- >;
+ >
findMany: FunctionReference<
- "query",
- "internal",
+ 'query',
+ 'internal',
{
- join?: any;
- limit?: number;
+ join?: any
+ limit?: number
model:
- | "user"
- | "session"
- | "account"
- | "verification"
- | "twoFactor"
- | "jwks";
- offset?: number;
+ | 'user'
+ | 'session'
+ | 'account'
+ | 'verification'
+ | 'twoFactor'
+ | 'jwks'
+ offset?: number
paginationOpts: {
- cursor: string | null;
- endCursor?: string | null;
- id?: number;
- maximumBytesRead?: number;
- maximumRowsRead?: number;
- numItems: number;
- };
- select?: Array;
- sortBy?: { direction: "asc" | "desc"; field: string };
+ cursor: string | null
+ endCursor?: string | null
+ id?: number
+ maximumBytesRead?: number
+ maximumRowsRead?: number
+ numItems: number
+ }
+ select?: Array
+ sortBy?: { direction: 'asc' | 'desc'; field: string }
where?: Array<{
- connector?: "AND" | "OR";
- field: string;
- mode?: "sensitive" | "insensitive";
+ connector?: 'AND' | 'OR'
+ field: string
+ mode?: 'sensitive' | 'insensitive'
operator?:
- | "lt"
- | "lte"
- | "gt"
- | "gte"
- | "eq"
- | "in"
- | "not_in"
- | "ne"
- | "contains"
- | "starts_with"
- | "ends_with";
+ | 'lt'
+ | 'lte'
+ | 'gt'
+ | 'gte'
+ | 'eq'
+ | 'in'
+ | 'not_in'
+ | 'ne'
+ | 'contains'
+ | 'starts_with'
+ | 'ends_with'
value:
| string
| number
| boolean
| Array
| Array
- | null;
- }>;
+ | null
+ }>
},
any,
Name
- >;
+ >
findOne: FunctionReference<
- "query",
- "internal",
+ 'query',
+ 'internal',
{
- join?: any;
+ join?: any
model:
- | "user"
- | "session"
- | "account"
- | "verification"
- | "twoFactor"
- | "jwks";
- select?: Array;
+ | 'user'
+ | 'session'
+ | 'account'
+ | 'verification'
+ | 'twoFactor'
+ | 'jwks'
+ select?: Array
where?: Array<{
- connector?: "AND" | "OR";
- field: string;
- mode?: "sensitive" | "insensitive";
+ connector?: 'AND' | 'OR'
+ field: string
+ mode?: 'sensitive' | 'insensitive'
operator?:
- | "lt"
- | "lte"
- | "gt"
- | "gte"
- | "eq"
- | "in"
- | "not_in"
- | "ne"
- | "contains"
- | "starts_with"
- | "ends_with";
+ | 'lt'
+ | 'lte'
+ | 'gt'
+ | 'gte'
+ | 'eq'
+ | 'in'
+ | 'not_in'
+ | 'ne'
+ | 'contains'
+ | 'starts_with'
+ | 'ends_with'
value:
| string
| number
| boolean
| Array
| Array
- | null;
- }>;
+ | null
+ }>
+ },
+ any,
+ Name
+ >
+ incrementOne: FunctionReference<
+ 'mutation',
+ 'internal',
+ {
+ input:
+ | {
+ increment: Record
+ model: 'user'
+ set?: {
+ createdAt?: number
+ email?: string
+ emailVerified?: boolean
+ foo?: null | string
+ image?: null | string
+ isAnonymous?: null | boolean
+ name?: string
+ twoFactorEnabled?: null | boolean
+ updatedAt?: number
+ userId?: null | string
+ }
+ where: Array<{
+ connector?: 'AND' | 'OR'
+ field:
+ | 'name'
+ | 'email'
+ | 'emailVerified'
+ | 'image'
+ | 'createdAt'
+ | 'updatedAt'
+ | 'twoFactorEnabled'
+ | 'isAnonymous'
+ | 'userId'
+ | 'foo'
+ | '_id'
+ mode?: 'sensitive' | 'insensitive'
+ operator?:
+ | 'lt'
+ | 'lte'
+ | 'gt'
+ | 'gte'
+ | 'eq'
+ | 'in'
+ | 'not_in'
+ | 'ne'
+ | 'contains'
+ | 'starts_with'
+ | 'ends_with'
+ value:
+ | string
+ | number
+ | boolean
+ | Array
+ | Array
+ | null
+ }>
+ }
+ | {
+ increment: Record
+ model: 'session'
+ set?: {
+ createdAt?: number
+ expiresAt?: number
+ ipAddress?: null | string
+ token?: string
+ updatedAt?: number
+ userAgent?: null | string
+ userId?: string
+ }
+ where: Array<{
+ connector?: 'AND' | 'OR'
+ field:
+ | 'expiresAt'
+ | 'token'
+ | 'createdAt'
+ | 'updatedAt'
+ | 'ipAddress'
+ | 'userAgent'
+ | 'userId'
+ | '_id'
+ mode?: 'sensitive' | 'insensitive'
+ operator?:
+ | 'lt'
+ | 'lte'
+ | 'gt'
+ | 'gte'
+ | 'eq'
+ | 'in'
+ | 'not_in'
+ | 'ne'
+ | 'contains'
+ | 'starts_with'
+ | 'ends_with'
+ value:
+ | string
+ | number
+ | boolean
+ | Array
+ | Array
+ | null
+ }>
+ }
+ | {
+ increment: Record
+ model: 'account'
+ set?: {
+ accessToken?: null | string
+ accessTokenExpiresAt?: null | number
+ accountId?: string
+ createdAt?: number
+ idToken?: null | string
+ password?: null | string
+ providerId?: string
+ refreshToken?: null | string
+ refreshTokenExpiresAt?: null | number
+ scope?: null | string
+ updatedAt?: number
+ userId?: string
+ }
+ where: Array<{
+ connector?: 'AND' | 'OR'
+ field:
+ | 'accountId'
+ | 'providerId'
+ | 'userId'
+ | 'accessToken'
+ | 'refreshToken'
+ | 'idToken'
+ | 'accessTokenExpiresAt'
+ | 'refreshTokenExpiresAt'
+ | 'scope'
+ | 'password'
+ | 'createdAt'
+ | 'updatedAt'
+ | '_id'
+ mode?: 'sensitive' | 'insensitive'
+ operator?:
+ | 'lt'
+ | 'lte'
+ | 'gt'
+ | 'gte'
+ | 'eq'
+ | 'in'
+ | 'not_in'
+ | 'ne'
+ | 'contains'
+ | 'starts_with'
+ | 'ends_with'
+ value:
+ | string
+ | number
+ | boolean
+ | Array
+ | Array
+ | null
+ }>
+ }
+ | {
+ increment: Record
+ model: 'verification'
+ set?: {
+ createdAt?: number
+ expiresAt?: number
+ identifier?: string
+ updatedAt?: number
+ value?: string
+ }
+ where: Array<{
+ connector?: 'AND' | 'OR'
+ field:
+ | 'identifier'
+ | 'value'
+ | 'expiresAt'
+ | 'createdAt'
+ | 'updatedAt'
+ | '_id'
+ mode?: 'sensitive' | 'insensitive'
+ operator?:
+ | 'lt'
+ | 'lte'
+ | 'gt'
+ | 'gte'
+ | 'eq'
+ | 'in'
+ | 'not_in'
+ | 'ne'
+ | 'contains'
+ | 'starts_with'
+ | 'ends_with'
+ value:
+ | string
+ | number
+ | boolean
+ | Array
+ | Array
+ | null
+ }>
+ }
+ | {
+ increment: Record
+ model: 'twoFactor'
+ set?: {
+ backupCodes?: string
+ failedVerificationCount?: null | number
+ lockedUntil?: null | number
+ secret?: string
+ userId?: string
+ verified?: null | boolean
+ }
+ where: Array<{
+ connector?: 'AND' | 'OR'
+ field:
+ | 'secret'
+ | 'backupCodes'
+ | 'userId'
+ | 'verified'
+ | 'failedVerificationCount'
+ | 'lockedUntil'
+ | '_id'
+ mode?: 'sensitive' | 'insensitive'
+ operator?:
+ | 'lt'
+ | 'lte'
+ | 'gt'
+ | 'gte'
+ | 'eq'
+ | 'in'
+ | 'not_in'
+ | 'ne'
+ | 'contains'
+ | 'starts_with'
+ | 'ends_with'
+ value:
+ | string
+ | number
+ | boolean
+ | Array
+ | Array
+ | null
+ }>
+ }
+ | {
+ increment: Record
+ model: 'jwks'
+ set?: {
+ alg?: null | string
+ createdAt?: number
+ crv?: null | string
+ expiresAt?: null | number
+ privateKey?: string
+ publicKey?: string
+ }
+ where: Array<{
+ connector?: 'AND' | 'OR'
+ field:
+ | 'publicKey'
+ | 'privateKey'
+ | 'createdAt'
+ | 'expiresAt'
+ | 'alg'
+ | 'crv'
+ | '_id'
+ mode?: 'sensitive' | 'insensitive'
+ operator?:
+ | 'lt'
+ | 'lte'
+ | 'gt'
+ | 'gte'
+ | 'eq'
+ | 'in'
+ | 'not_in'
+ | 'ne'
+ | 'contains'
+ | 'starts_with'
+ | 'ends_with'
+ value:
+ | string
+ | number
+ | boolean
+ | Array
+ | Array
+ | null
+ }>
+ }
+ onUpdateHandle?: string
},
any,
Name
- >;
+ >
updateMany: FunctionReference<
- "mutation",
- "internal",
+ 'mutation',
+ 'internal',
{
input:
| {
- model: "user";
+ model: 'user'
update: {
- createdAt?: number;
- email?: string;
- emailVerified?: boolean;
- foo?: null | string;
- image?: null | string;
- isAnonymous?: null | boolean;
- name?: string;
- twoFactorEnabled?: null | boolean;
- updatedAt?: number;
- userId?: null | string;
- };
+ createdAt?: number
+ email?: string
+ emailVerified?: boolean
+ foo?: null | string
+ image?: null | string
+ isAnonymous?: null | boolean
+ name?: string
+ twoFactorEnabled?: null | boolean
+ updatedAt?: number
+ userId?: null | string
+ }
where?: Array<{
- connector?: "AND" | "OR";
+ connector?: 'AND' | 'OR'
field:
- | "name"
- | "email"
- | "emailVerified"
- | "image"
- | "createdAt"
- | "updatedAt"
- | "twoFactorEnabled"
- | "isAnonymous"
- | "userId"
- | "foo"
- | "_id";
- mode?: "sensitive" | "insensitive";
+ | 'name'
+ | 'email'
+ | 'emailVerified'
+ | 'image'
+ | 'createdAt'
+ | 'updatedAt'
+ | 'twoFactorEnabled'
+ | 'isAnonymous'
+ | 'userId'
+ | 'foo'
+ | '_id'
+ mode?: 'sensitive' | 'insensitive'
operator?:
- | "lt"
- | "lte"
- | "gt"
- | "gte"
- | "eq"
- | "in"
- | "not_in"
- | "ne"
- | "contains"
- | "starts_with"
- | "ends_with";
+ | 'lt'
+ | 'lte'
+ | 'gt'
+ | 'gte'
+ | 'eq'
+ | 'in'
+ | 'not_in'
+ | 'ne'
+ | 'contains'
+ | 'starts_with'
+ | 'ends_with'
value:
| string
| number
| boolean
| Array
| Array
- | null;
- }>;
+ | null
+ }>
}
| {
- model: "session";
+ model: 'session'
update: {
- createdAt?: number;
- expiresAt?: number;
- ipAddress?: null | string;
- token?: string;
- updatedAt?: number;
- userAgent?: null | string;
- userId?: string;
- };
+ createdAt?: number
+ expiresAt?: number
+ ipAddress?: null | string
+ token?: string
+ updatedAt?: number
+ userAgent?: null | string
+ userId?: string
+ }
where?: Array<{
- connector?: "AND" | "OR";
+ connector?: 'AND' | 'OR'
field:
- | "expiresAt"
- | "token"
- | "createdAt"
- | "updatedAt"
- | "ipAddress"
- | "userAgent"
- | "userId"
- | "_id";
- mode?: "sensitive" | "insensitive";
+ | 'expiresAt'
+ | 'token'
+ | 'createdAt'
+ | 'updatedAt'
+ | 'ipAddress'
+ | 'userAgent'
+ | 'userId'
+ | '_id'
+ mode?: 'sensitive' | 'insensitive'
operator?:
- | "lt"
- | "lte"
- | "gt"
- | "gte"
- | "eq"
- | "in"
- | "not_in"
- | "ne"
- | "contains"
- | "starts_with"
- | "ends_with";
+ | 'lt'
+ | 'lte'
+ | 'gt'
+ | 'gte'
+ | 'eq'
+ | 'in'
+ | 'not_in'
+ | 'ne'
+ | 'contains'
+ | 'starts_with'
+ | 'ends_with'
value:
| string
| number
| boolean
| Array
| Array
- | null;
- }>;
+ | null
+ }>
}
| {
- model: "account";
+ model: 'account'
update: {
- accessToken?: null | string;
- accessTokenExpiresAt?: null | number;
- accountId?: string;
- createdAt?: number;
- idToken?: null | string;
- password?: null | string;
- providerId?: string;
- refreshToken?: null | string;
- refreshTokenExpiresAt?: null | number;
- scope?: null | string;
- updatedAt?: number;
- userId?: string;
- };
+ accessToken?: null | string
+ accessTokenExpiresAt?: null | number
+ accountId?: string
+ createdAt?: number
+ idToken?: null | string
+ password?: null | string
+ providerId?: string
+ refreshToken?: null | string
+ refreshTokenExpiresAt?: null | number
+ scope?: null | string
+ updatedAt?: number
+ userId?: string
+ }
where?: Array<{
- connector?: "AND" | "OR";
+ connector?: 'AND' | 'OR'
field:
- | "accountId"
- | "providerId"
- | "userId"
- | "accessToken"
- | "refreshToken"
- | "idToken"
- | "accessTokenExpiresAt"
- | "refreshTokenExpiresAt"
- | "scope"
- | "password"
- | "createdAt"
- | "updatedAt"
- | "_id";
- mode?: "sensitive" | "insensitive";
+ | 'accountId'
+ | 'providerId'
+ | 'userId'
+ | 'accessToken'
+ | 'refreshToken'
+ | 'idToken'
+ | 'accessTokenExpiresAt'
+ | 'refreshTokenExpiresAt'
+ | 'scope'
+ | 'password'
+ | 'createdAt'
+ | 'updatedAt'
+ | '_id'
+ mode?: 'sensitive' | 'insensitive'
operator?:
- | "lt"
- | "lte"
- | "gt"
- | "gte"
- | "eq"
- | "in"
- | "not_in"
- | "ne"
- | "contains"
- | "starts_with"
- | "ends_with";
+ | 'lt'
+ | 'lte'
+ | 'gt'
+ | 'gte'
+ | 'eq'
+ | 'in'
+ | 'not_in'
+ | 'ne'
+ | 'contains'
+ | 'starts_with'
+ | 'ends_with'
value:
| string
| number
| boolean
| Array
| Array
- | null;
- }>;
+ | null
+ }>
}
| {
- model: "verification";
+ model: 'verification'
update: {
- createdAt?: number;
- expiresAt?: number;
- identifier?: string;
- updatedAt?: number;
- value?: string;
- };
+ createdAt?: number
+ expiresAt?: number
+ identifier?: string
+ updatedAt?: number
+ value?: string
+ }
where?: Array<{
- connector?: "AND" | "OR";
+ connector?: 'AND' | 'OR'
field:
- | "identifier"
- | "value"
- | "expiresAt"
- | "createdAt"
- | "updatedAt"
- | "_id";
- mode?: "sensitive" | "insensitive";
+ | 'identifier'
+ | 'value'
+ | 'expiresAt'
+ | 'createdAt'
+ | 'updatedAt'
+ | '_id'
+ mode?: 'sensitive' | 'insensitive'
operator?:
- | "lt"
- | "lte"
- | "gt"
- | "gte"
- | "eq"
- | "in"
- | "not_in"
- | "ne"
- | "contains"
- | "starts_with"
- | "ends_with";
+ | 'lt'
+ | 'lte'
+ | 'gt'
+ | 'gte'
+ | 'eq'
+ | 'in'
+ | 'not_in'
+ | 'ne'
+ | 'contains'
+ | 'starts_with'
+ | 'ends_with'
value:
| string
| number
| boolean
| Array
| Array
- | null;
- }>;
+ | null
+ }>
}
| {
- model: "twoFactor";
+ model: 'twoFactor'
update: {
- backupCodes?: string;
- secret?: string;
- userId?: string;
- verified?: null | boolean;
- };
+ backupCodes?: string
+ failedVerificationCount?: null | number
+ lockedUntil?: null | number
+ secret?: string
+ userId?: string
+ verified?: null | boolean
+ }
where?: Array<{
- connector?: "AND" | "OR";
+ connector?: 'AND' | 'OR'
field:
- | "secret"
- | "backupCodes"
- | "userId"
- | "verified"
- | "_id";
- mode?: "sensitive" | "insensitive";
+ | 'secret'
+ | 'backupCodes'
+ | 'userId'
+ | 'verified'
+ | 'failedVerificationCount'
+ | 'lockedUntil'
+ | '_id'
+ mode?: 'sensitive' | 'insensitive'
operator?:
- | "lt"
- | "lte"
- | "gt"
- | "gte"
- | "eq"
- | "in"
- | "not_in"
- | "ne"
- | "contains"
- | "starts_with"
- | "ends_with";
+ | 'lt'
+ | 'lte'
+ | 'gt'
+ | 'gte'
+ | 'eq'
+ | 'in'
+ | 'not_in'
+ | 'ne'
+ | 'contains'
+ | 'starts_with'
+ | 'ends_with'
value:
| string
| number
| boolean
| Array
| Array
- | null;
- }>;
+ | null
+ }>
}
| {
- model: "jwks";
+ model: 'jwks'
update: {
- createdAt?: number;
- expiresAt?: null | number;
- privateKey?: string;
- publicKey?: string;
- };
+ alg?: null | string
+ createdAt?: number
+ crv?: null | string
+ expiresAt?: null | number
+ privateKey?: string
+ publicKey?: string
+ }
where?: Array<{
- connector?: "AND" | "OR";
+ connector?: 'AND' | 'OR'
field:
- | "publicKey"
- | "privateKey"
- | "createdAt"
- | "expiresAt"
- | "_id";
- mode?: "sensitive" | "insensitive";
+ | 'publicKey'
+ | 'privateKey'
+ | 'createdAt'
+ | 'expiresAt'
+ | 'alg'
+ | 'crv'
+ | '_id'
+ mode?: 'sensitive' | 'insensitive'
operator?:
- | "lt"
- | "lte"
- | "gt"
- | "gte"
- | "eq"
- | "in"
- | "not_in"
- | "ne"
- | "contains"
- | "starts_with"
- | "ends_with";
+ | 'lt'
+ | 'lte'
+ | 'gt'
+ | 'gte'
+ | 'eq'
+ | 'in'
+ | 'not_in'
+ | 'ne'
+ | 'contains'
+ | 'starts_with'
+ | 'ends_with'
value:
| string
| number
| boolean
| Array
| Array
- | null;
- }>;
- };
- onUpdateHandle?: string;
+ | null
+ }>
+ }
+ onUpdateHandle?: string
paginationOpts: {
- cursor: string | null;
- endCursor?: string | null;
- id?: number;
- maximumBytesRead?: number;
- maximumRowsRead?: number;
- numItems: number;
- };
+ cursor: string | null
+ endCursor?: string | null
+ id?: number
+ maximumBytesRead?: number
+ maximumRowsRead?: number
+ numItems: number
+ }
},
any,
Name
- >;
+ >
updateOne: FunctionReference<
- "mutation",
- "internal",
+ 'mutation',
+ 'internal',
{
input:
| {
- model: "user";
+ model: 'user'
update: {
- createdAt?: number;
- email?: string;
- emailVerified?: boolean;
- foo?: null | string;
- image?: null | string;
- isAnonymous?: null | boolean;
- name?: string;
- twoFactorEnabled?: null | boolean;
- updatedAt?: number;
- userId?: null | string;
- };
+ createdAt?: number
+ email?: string
+ emailVerified?: boolean
+ foo?: null | string
+ image?: null | string
+ isAnonymous?: null | boolean
+ name?: string
+ twoFactorEnabled?: null | boolean
+ updatedAt?: number
+ userId?: null | string
+ }
where?: Array<{
- connector?: "AND" | "OR";
+ connector?: 'AND' | 'OR'
field:
- | "name"
- | "email"
- | "emailVerified"
- | "image"
- | "createdAt"
- | "updatedAt"
- | "twoFactorEnabled"
- | "isAnonymous"
- | "userId"
- | "foo"
- | "_id";
- mode?: "sensitive" | "insensitive";
+ | 'name'
+ | 'email'
+ | 'emailVerified'
+ | 'image'
+ | 'createdAt'
+ | 'updatedAt'
+ | 'twoFactorEnabled'
+ | 'isAnonymous'
+ | 'userId'
+ | 'foo'
+ | '_id'
+ mode?: 'sensitive' | 'insensitive'
operator?:
- | "lt"
- | "lte"
- | "gt"
- | "gte"
- | "eq"
- | "in"
- | "not_in"
- | "ne"
- | "contains"
- | "starts_with"
- | "ends_with";
+ | 'lt'
+ | 'lte'
+ | 'gt'
+ | 'gte'
+ | 'eq'
+ | 'in'
+ | 'not_in'
+ | 'ne'
+ | 'contains'
+ | 'starts_with'
+ | 'ends_with'
value:
| string
| number
| boolean
| Array
| Array
- | null;
- }>;
+ | null
+ }>
}
| {
- model: "session";
+ model: 'session'
update: {
- createdAt?: number;
- expiresAt?: number;
- ipAddress?: null | string;
- token?: string;
- updatedAt?: number;
- userAgent?: null | string;
- userId?: string;
- };
+ createdAt?: number
+ expiresAt?: number
+ ipAddress?: null | string
+ token?: string
+ updatedAt?: number
+ userAgent?: null | string
+ userId?: string
+ }
where?: Array<{
- connector?: "AND" | "OR";
+ connector?: 'AND' | 'OR'
field:
- | "expiresAt"
- | "token"
- | "createdAt"
- | "updatedAt"
- | "ipAddress"
- | "userAgent"
- | "userId"
- | "_id";
- mode?: "sensitive" | "insensitive";
+ | 'expiresAt'
+ | 'token'
+ | 'createdAt'
+ | 'updatedAt'
+ | 'ipAddress'
+ | 'userAgent'
+ | 'userId'
+ | '_id'
+ mode?: 'sensitive' | 'insensitive'
operator?:
- | "lt"
- | "lte"
- | "gt"
- | "gte"
- | "eq"
- | "in"
- | "not_in"
- | "ne"
- | "contains"
- | "starts_with"
- | "ends_with";
+ | 'lt'
+ | 'lte'
+ | 'gt'
+ | 'gte'
+ | 'eq'
+ | 'in'
+ | 'not_in'
+ | 'ne'
+ | 'contains'
+ | 'starts_with'
+ | 'ends_with'
value:
| string
| number
| boolean
| Array
| Array
- | null;
- }>;
+ | null
+ }>
}
| {
- model: "account";
+ model: 'account'
update: {
- accessToken?: null | string;
- accessTokenExpiresAt?: null | number;
- accountId?: string;
- createdAt?: number;
- idToken?: null | string;
- password?: null | string;
- providerId?: string;
- refreshToken?: null | string;
- refreshTokenExpiresAt?: null | number;
- scope?: null | string;
- updatedAt?: number;
- userId?: string;
- };
+ accessToken?: null | string
+ accessTokenExpiresAt?: null | number
+ accountId?: string
+ createdAt?: number
+ idToken?: null | string
+ password?: null | string
+ providerId?: string
+ refreshToken?: null | string
+ refreshTokenExpiresAt?: null | number
+ scope?: null | string
+ updatedAt?: number
+ userId?: string
+ }
where?: Array<{
- connector?: "AND" | "OR";
+ connector?: 'AND' | 'OR'
field:
- | "accountId"
- | "providerId"
- | "userId"
- | "accessToken"
- | "refreshToken"
- | "idToken"
- | "accessTokenExpiresAt"
- | "refreshTokenExpiresAt"
- | "scope"
- | "password"
- | "createdAt"
- | "updatedAt"
- | "_id";
- mode?: "sensitive" | "insensitive";
+ | 'accountId'
+ | 'providerId'
+ | 'userId'
+ | 'accessToken'
+ | 'refreshToken'
+ | 'idToken'
+ | 'accessTokenExpiresAt'
+ | 'refreshTokenExpiresAt'
+ | 'scope'
+ | 'password'
+ | 'createdAt'
+ | 'updatedAt'
+ | '_id'
+ mode?: 'sensitive' | 'insensitive'
operator?:
- | "lt"
- | "lte"
- | "gt"
- | "gte"
- | "eq"
- | "in"
- | "not_in"
- | "ne"
- | "contains"
- | "starts_with"
- | "ends_with";
+ | 'lt'
+ | 'lte'
+ | 'gt'
+ | 'gte'
+ | 'eq'
+ | 'in'
+ | 'not_in'
+ | 'ne'
+ | 'contains'
+ | 'starts_with'
+ | 'ends_with'
value:
| string
| number
| boolean
| Array
| Array
- | null;
- }>;
+ | null
+ }>
}
| {
- model: "verification";
+ model: 'verification'
update: {
- createdAt?: number;
- expiresAt?: number;
- identifier?: string;
- updatedAt?: number;
- value?: string;
- };
+ createdAt?: number
+ expiresAt?: number
+ identifier?: string
+ updatedAt?: number
+ value?: string
+ }
where?: Array<{
- connector?: "AND" | "OR";
+ connector?: 'AND' | 'OR'
field:
- | "identifier"
- | "value"
- | "expiresAt"
- | "createdAt"
- | "updatedAt"
- | "_id";
- mode?: "sensitive" | "insensitive";
+ | 'identifier'
+ | 'value'
+ | 'expiresAt'
+ | 'createdAt'
+ | 'updatedAt'
+ | '_id'
+ mode?: 'sensitive' | 'insensitive'
operator?:
- | "lt"
- | "lte"
- | "gt"
- | "gte"
- | "eq"
- | "in"
- | "not_in"
- | "ne"
- | "contains"
- | "starts_with"
- | "ends_with";
+ | 'lt'
+ | 'lte'
+ | 'gt'
+ | 'gte'
+ | 'eq'
+ | 'in'
+ | 'not_in'
+ | 'ne'
+ | 'contains'
+ | 'starts_with'
+ | 'ends_with'
value:
| string
| number
| boolean
| Array
| Array
- | null;
- }>;
+ | null
+ }>
}
| {
- model: "twoFactor";
+ model: 'twoFactor'
update: {
- backupCodes?: string;
- secret?: string;
- userId?: string;
- verified?: null | boolean;
- };
+ backupCodes?: string
+ failedVerificationCount?: null | number
+ lockedUntil?: null | number
+ secret?: string
+ userId?: string
+ verified?: null | boolean
+ }
where?: Array<{
- connector?: "AND" | "OR";
+ connector?: 'AND' | 'OR'
field:
- | "secret"
- | "backupCodes"
- | "userId"
- | "verified"
- | "_id";
- mode?: "sensitive" | "insensitive";
+ | 'secret'
+ | 'backupCodes'
+ | 'userId'
+ | 'verified'
+ | 'failedVerificationCount'
+ | 'lockedUntil'
+ | '_id'
+ mode?: 'sensitive' | 'insensitive'
operator?:
- | "lt"
- | "lte"
- | "gt"
- | "gte"
- | "eq"
- | "in"
- | "not_in"
- | "ne"
- | "contains"
- | "starts_with"
- | "ends_with";
+ | 'lt'
+ | 'lte'
+ | 'gt'
+ | 'gte'
+ | 'eq'
+ | 'in'
+ | 'not_in'
+ | 'ne'
+ | 'contains'
+ | 'starts_with'
+ | 'ends_with'
value:
| string
| number
| boolean
| Array
| Array
- | null;
- }>;
+ | null
+ }>
}
| {
- model: "jwks";
+ model: 'jwks'
update: {
- createdAt?: number;
- expiresAt?: null | number;
- privateKey?: string;
- publicKey?: string;
- };
+ alg?: null | string
+ createdAt?: number
+ crv?: null | string
+ expiresAt?: null | number
+ privateKey?: string
+ publicKey?: string
+ }
where?: Array<{
- connector?: "AND" | "OR";
+ connector?: 'AND' | 'OR'
field:
- | "publicKey"
- | "privateKey"
- | "createdAt"
- | "expiresAt"
- | "_id";
- mode?: "sensitive" | "insensitive";
+ | 'publicKey'
+ | 'privateKey'
+ | 'createdAt'
+ | 'expiresAt'
+ | 'alg'
+ | 'crv'
+ | '_id'
+ mode?: 'sensitive' | 'insensitive'
operator?:
- | "lt"
- | "lte"
- | "gt"
- | "gte"
- | "eq"
- | "in"
- | "not_in"
- | "ne"
- | "contains"
- | "starts_with"
- | "ends_with";
+ | 'lt'
+ | 'lte'
+ | 'gt'
+ | 'gte'
+ | 'eq'
+ | 'in'
+ | 'not_in'
+ | 'ne'
+ | 'contains'
+ | 'starts_with'
+ | 'ends_with'
value:
| string
| number
| boolean
| Array
| Array
- | null;
- }>;
- };
- onUpdateHandle?: string;
+ | null
+ }>
+ }
+ onUpdateHandle?: string
},
any,
Name
- >;
- };
- };
+ >
+ }
+ }
diff --git a/examples/tanstack/convex/betterAuth/adapter.ts b/examples/tanstack/convex/betterAuth/adapter.ts
index e8f90a76..301e9e9e 100644
--- a/examples/tanstack/convex/betterAuth/adapter.ts
+++ b/examples/tanstack/convex/betterAuth/adapter.ts
@@ -7,6 +7,7 @@ export const {
findOne,
findMany,
updateOne,
+ incrementOne,
updateMany,
deleteOne,
deleteMany,
diff --git a/examples/tanstack/convex/betterAuth/schema.ts b/examples/tanstack/convex/betterAuth/schema.ts
index 2fade4aa..67730355 100644
--- a/examples/tanstack/convex/betterAuth/schema.ts
+++ b/examples/tanstack/convex/betterAuth/schema.ts
@@ -3,15 +3,15 @@
* To regenerate the schema, from your project root:
*
* cd convex/betterAuth
- * npx auth generate
+ * npx auth generate --output schema.ts
*
* To customize the schema, generate to an alternate file and import
* the table definitions to your schema file. See
* https://labs.convex.dev/better-auth/features/local-install#adding-custom-indexes.
*/
-import { defineSchema, defineTable } from "convex/server";
-import { v } from "convex/values";
+import { defineSchema, defineTable } from 'convex/server'
+import { v } from 'convex/values'
export const tables = {
user: defineTable({
@@ -26,9 +26,9 @@ export const tables = {
userId: v.optional(v.union(v.null(), v.string())),
foo: v.optional(v.union(v.null(), v.string())),
})
- .index("email_name", ["email","name"])
- .index("name", ["name"])
- .index("userId", ["userId"]),
+ .index('email_name', ['email', 'name'])
+ .index('name', ['name'])
+ .index('userId', ['userId']),
session: defineTable({
expiresAt: v.number(),
token: v.string(),
@@ -38,10 +38,10 @@ export const tables = {
userAgent: v.optional(v.union(v.null(), v.string())),
userId: v.string(),
})
- .index("expiresAt", ["expiresAt"])
- .index("expiresAt_userId", ["expiresAt","userId"])
- .index("token", ["token"])
- .index("userId", ["userId"]),
+ .index('expiresAt', ['expiresAt'])
+ .index('expiresAt_userId', ['expiresAt', 'userId'])
+ .index('token', ['token'])
+ .index('userId', ['userId']),
account: defineTable({
accountId: v.string(),
providerId: v.string(),
@@ -56,10 +56,15 @@ export const tables = {
createdAt: v.number(),
updatedAt: v.number(),
})
- .index("accountId", ["accountId"])
- .index("accountId_providerId", ["accountId","providerId"])
- .index("providerId_userId", ["providerId","userId"])
- .index("userId", ["userId"]),
+ .index('accountId', ['accountId'])
+ .index('accountId_providerId', ['accountId', 'providerId'])
+ .index('providerId_userId', ['providerId', 'userId'])
+ .index('userId_providerId_accountId', [
+ 'userId',
+ 'providerId',
+ 'accountId',
+ ])
+ .index('userId', ['userId']),
verification: defineTable({
identifier: v.string(),
value: v.string(),
@@ -67,23 +72,26 @@ export const tables = {
createdAt: v.number(),
updatedAt: v.number(),
})
- .index("expiresAt", ["expiresAt"])
- .index("identifier", ["identifier"]),
+ .index('expiresAt', ['expiresAt'])
+ .index('identifier', ['identifier']),
twoFactor: defineTable({
secret: v.string(),
backupCodes: v.string(),
userId: v.string(),
verified: v.optional(v.union(v.null(), v.boolean())),
- })
- .index("userId", ["userId"]),
+ failedVerificationCount: v.optional(v.union(v.null(), v.number())),
+ lockedUntil: v.optional(v.union(v.null(), v.number())),
+ }).index('userId', ['userId']),
jwks: defineTable({
publicKey: v.string(),
privateKey: v.string(),
createdAt: v.number(),
expiresAt: v.optional(v.union(v.null(), v.number())),
+ alg: v.optional(v.union(v.null(), v.string())),
+ crv: v.optional(v.union(v.null(), v.string())),
}),
-};
+}
-const schema = defineSchema(tables);
+const schema = defineSchema(tables)
-export default schema;
+export default schema
diff --git a/examples/tanstack/package-lock.json b/examples/tanstack/package-lock.json
index 388d7a6b..6d547b3c 100644
--- a/examples/tanstack/package-lock.json
+++ b/examples/tanstack/package-lock.json
@@ -60,7 +60,7 @@
"version": "0.12.5",
"license": "Apache-2.0",
"dependencies": {
- "@better-fetch/fetch": "^1.1.18",
+ "@better-fetch/fetch": "^1.3.2",
"common-tags": "^1.8.2",
"convex-helpers": "^0.1.128",
"jose": "^6.1.0",
@@ -70,8 +70,8 @@
"zod": "^4.0.0"
},
"devDependencies": {
- "@better-auth/core": "1.6.15",
- "@better-auth/test-utils": "1.6.15",
+ "@better-auth/core": "1.7.7",
+ "@better-auth/test-utils": "1.7.7",
"@convex-dev/eslint-plugin": "2.0.0",
"@edge-runtime/vm": "5.0.0",
"@eslint/eslintrc": "3.3.5",
@@ -82,7 +82,7 @@
"@types/react": "19.2.14",
"@types/react-dom": "19.2.3",
"@types/semver": "7.7.1",
- "better-auth": "1.6.15",
+ "better-auth": "1.7.7",
"chokidar-cli": "3.0.0",
"concurrently": "10.0.3",
"convex": "1.46.0",
@@ -100,34 +100,34 @@
"react-dom": "19.2.6",
"typescript": "5.9.3",
"typescript-eslint": "8.59.2",
- "vitest": "4.1.5"
+ "vitest": "4.1.10"
},
"peerDependencies": {
- "better-auth": ">=1.6.11 <1.7.0",
+ "better-auth": ">=1.7.7 <1.8.0",
"convex": "^1.25.0",
"react": "^18.3.1 || ^19.0.0"
}
},
"../../node_modules/better-auth": {
- "version": "1.6.15",
- "license": "MIT",
- "dependencies": {
- "@better-auth/core": "1.6.15",
- "@better-auth/drizzle-adapter": "1.6.15",
- "@better-auth/kysely-adapter": "1.6.15",
- "@better-auth/memory-adapter": "1.6.15",
- "@better-auth/mongo-adapter": "1.6.15",
- "@better-auth/prisma-adapter": "1.6.15",
- "@better-auth/telemetry": "1.6.15",
- "@better-auth/utils": "0.4.1",
- "@better-fetch/fetch": "1.1.21",
- "@noble/ciphers": "^2.1.1",
- "@noble/hashes": "^2.0.1",
- "better-call": "1.3.5",
+ "version": "1.7.1",
+ "license": "MIT",
+ "dependencies": {
+ "@better-auth/core": "1.7.1",
+ "@better-auth/drizzle-adapter": "1.7.1",
+ "@better-auth/kysely-adapter": "1.7.1",
+ "@better-auth/memory-adapter": "1.7.1",
+ "@better-auth/mongo-adapter": "1.7.1",
+ "@better-auth/prisma-adapter": "1.7.1",
+ "@better-auth/telemetry": "1.7.1",
+ "@better-auth/utils": "0.4.2",
+ "@better-fetch/fetch": "1.3.1",
+ "@noble/ciphers": "^2.2.0",
+ "@noble/hashes": "^2.2.0",
+ "better-call": "1.4.0",
"defu": "^6.1.4",
- "jose": "^6.1.3",
+ "jose": "^6.2.3",
"kysely": "^0.28.17 || ^0.29.0",
- "nanostores": "^1.1.1",
+ "nanostores": "^1.3.0",
"zod": "^4.3.6"
},
"peerDependencies": {
@@ -137,8 +137,8 @@
"@tanstack/react-start": "^1.0.0",
"@tanstack/solid-start": "^1.0.0",
"better-sqlite3": "^12.0.0",
- "drizzle-kit": ">=0.31.4",
- "drizzle-orm": "^0.45.2",
+ "drizzle-kit": ">=0.31.4 || >=1.0.0-beta.1",
+ "drizzle-orm": "^0.45.2 || >=1.0.0-rc.1 <2.0.0",
"mongodb": "^6.0.0 || ^7.0.0",
"mysql2": "^3.0.0",
"next": "^14.0.0 || ^15.0.0 || ^16.0.0",
diff --git a/examples/tanstack/src/components/EnableTwoFactor.tsx b/examples/tanstack/src/components/EnableTwoFactor.tsx
index 5f837a5d..22a62cf0 100644
--- a/examples/tanstack/src/components/EnableTwoFactor.tsx
+++ b/examples/tanstack/src/components/EnableTwoFactor.tsx
@@ -46,7 +46,7 @@ export default function EnableTwoFactor({ onBack }: { onBack: () => void }) {
const { data } = await authClient.twoFactor.enable({
password,
})
- if (data?.totpURI) {
+ if (data?.method === 'totp') {
setTotpUri(data.totpURI)
if (data.backupCodes) {
setBackupCodes(data.backupCodes)
diff --git a/package-lock.json b/package-lock.json
index 9d7f564d..b5a65cf9 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,7 +9,7 @@
"version": "0.12.5",
"license": "Apache-2.0",
"dependencies": {
- "@better-fetch/fetch": "^1.1.18",
+ "@better-fetch/fetch": "^1.3.2",
"common-tags": "^1.8.2",
"convex-helpers": "^0.1.128",
"jose": "^6.1.0",
@@ -19,8 +19,8 @@
"zod": "^4.0.0"
},
"devDependencies": {
- "@better-auth/core": "1.6.15",
- "@better-auth/test-utils": "1.6.15",
+ "@better-auth/core": "1.7.7",
+ "@better-auth/test-utils": "1.7.7",
"@convex-dev/eslint-plugin": "2.0.0",
"@edge-runtime/vm": "5.0.0",
"@eslint/eslintrc": "3.3.5",
@@ -31,7 +31,7 @@
"@types/react": "19.2.14",
"@types/react-dom": "19.2.3",
"@types/semver": "7.7.1",
- "better-auth": "1.6.15",
+ "better-auth": "1.7.7",
"chokidar-cli": "3.0.0",
"concurrently": "10.0.3",
"convex": "1.46.0",
@@ -52,7 +52,7 @@
"vitest": "4.1.5"
},
"peerDependencies": {
- "better-auth": ">=1.6.11 <1.7.0",
+ "better-auth": ">=1.7.7 <1.8.0",
"convex": "^1.25.0",
"react": "^18.3.1 || ^19.0.0"
}
@@ -318,45 +318,41 @@
}
},
"node_modules/@better-auth/core": {
- "version": "1.6.15",
- "resolved": "https://registry.npmjs.org/@better-auth/core/-/core-1.6.15.tgz",
- "integrity": "sha512-gPgeEn+2t2cohSKj1CJFCJLo4jNqvtvhKP/y6Uwuurg4ReEf2Y9PUCYffvbp7DDc+GevWTuD0Cv9x0+e83+HpA==",
+ "version": "1.7.7",
+ "resolved": "https://registry.npmjs.org/@better-auth/core/-/core-1.7.7.tgz",
+ "integrity": "sha512-srgFzHEjB2WWlokFz1i4BfG41sNhsA105Pukgqu6RrEh5A+LSkzf1mCHydVipkjt9l5qswtBMi7IL6ouHbZL4Q==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@opentelemetry/semantic-conventions": "^1.39.0",
+ "@opentelemetry/semantic-conventions": "^1.41.1",
"@standard-schema/spec": "^1.1.0",
- "zod": "^4.3.6"
+ "zod": "^4.5.4"
},
"peerDependencies": {
- "@better-auth/utils": "0.4.1",
- "@better-fetch/fetch": "1.1.21",
- "@cloudflare/workers-types": ">=4",
+ "@better-auth/utils": "0.4.2",
+ "@better-fetch/fetch": "1.3.2",
"@opentelemetry/api": "^1.9.0",
- "better-call": "1.3.5",
+ "better-call": "1.4.0",
"jose": "^6.1.0",
"kysely": "^0.28.5 || ^0.29.0",
"nanostores": "^1.0.1"
},
"peerDependenciesMeta": {
- "@cloudflare/workers-types": {
- "optional": true
- },
"@opentelemetry/api": {
"optional": true
}
}
},
"node_modules/@better-auth/drizzle-adapter": {
- "version": "1.6.15",
- "resolved": "https://registry.npmjs.org/@better-auth/drizzle-adapter/-/drizzle-adapter-1.6.15.tgz",
- "integrity": "sha512-+ho2RozN6cZmCN+6XkZd/ec5iolVlefgInQ7znJ18qRPbgllHxdyu9tGrgKZyEsXSCyqAa6wi5Yb4hd3WZmTNQ==",
+ "version": "1.7.7",
+ "resolved": "https://registry.npmjs.org/@better-auth/drizzle-adapter/-/drizzle-adapter-1.7.7.tgz",
+ "integrity": "sha512-qon0U94PR5FQysuyGoAlGVQpIIiNg5dtDpwjE63sN/HjoRWZadZzHPcS9mn7JsJyQZK6iTNhd7LD0AaXt2f78w==",
"dev": true,
"license": "MIT",
"peerDependencies": {
- "@better-auth/core": "^1.6.15",
- "@better-auth/utils": "0.4.1",
- "drizzle-orm": "^0.45.2"
+ "@better-auth/core": "1.7.7",
+ "@better-auth/utils": "0.4.2",
+ "drizzle-orm": "^0.45.2 || >=1.0.0-rc.1 <2.0.0"
},
"peerDependenciesMeta": {
"drizzle-orm": {
@@ -365,14 +361,14 @@
}
},
"node_modules/@better-auth/kysely-adapter": {
- "version": "1.6.15",
- "resolved": "https://registry.npmjs.org/@better-auth/kysely-adapter/-/kysely-adapter-1.6.15.tgz",
- "integrity": "sha512-E29Sugm+DWRK4oQNBPrjPA4kBYiKy2bBtX1arIlkuyAB9A1BEb4Xn+8t7wlNIF5eFcxpkFZ3F80ceSpWDSYU0Q==",
+ "version": "1.7.7",
+ "resolved": "https://registry.npmjs.org/@better-auth/kysely-adapter/-/kysely-adapter-1.7.7.tgz",
+ "integrity": "sha512-9FONOCgOcrQI9wJ5v1oDGIg9sT7pa9RjZZQ4fsCTudo8R87SrIGNUvUxDFTJhZvUblvFfGaIBR9Vb8LA+8bqLQ==",
"dev": true,
"license": "MIT",
"peerDependencies": {
- "@better-auth/core": "^1.6.15",
- "@better-auth/utils": "0.4.1",
+ "@better-auth/core": "1.7.7",
+ "@better-auth/utils": "0.4.2",
"kysely": "^0.28.17 || ^0.29.0"
},
"peerDependenciesMeta": {
@@ -382,25 +378,25 @@
}
},
"node_modules/@better-auth/memory-adapter": {
- "version": "1.6.15",
- "resolved": "https://registry.npmjs.org/@better-auth/memory-adapter/-/memory-adapter-1.6.15.tgz",
- "integrity": "sha512-BRE5Ft0Tn5thOPjCmyXHge7kOI/paLybaKDU+Hzg24DZMU6JvWooYWzvf52r1viDbxq5tz0G97iyaH4bouDyug==",
+ "version": "1.7.7",
+ "resolved": "https://registry.npmjs.org/@better-auth/memory-adapter/-/memory-adapter-1.7.7.tgz",
+ "integrity": "sha512-FqKFEe+b5tXXV0gRbyirca+qXgMpLOeJ8Wk19yykb/scQKy0WJ4k22WzEV3TTPctQV9DZhwCXPlDtQhnlOv5Gg==",
"dev": true,
"license": "MIT",
"peerDependencies": {
- "@better-auth/core": "^1.6.15",
- "@better-auth/utils": "0.4.1"
+ "@better-auth/core": "1.7.7",
+ "@better-auth/utils": "0.4.2"
}
},
"node_modules/@better-auth/mongo-adapter": {
- "version": "1.6.15",
- "resolved": "https://registry.npmjs.org/@better-auth/mongo-adapter/-/mongo-adapter-1.6.15.tgz",
- "integrity": "sha512-r0X9AtFhwDeOU1KMP9kZ5NeV0O3TSFzn8+uidaH+aicy2e1dNUsd9nPYrozEloSxdA2ZOnv5gD4jzH7HPeKkUQ==",
+ "version": "1.7.7",
+ "resolved": "https://registry.npmjs.org/@better-auth/mongo-adapter/-/mongo-adapter-1.7.7.tgz",
+ "integrity": "sha512-ZnVDuRrXqbf0oHphrEN27oTImNH5NTO26dtWatq5cXyYAsH7goof9QIoTOmxTbBxUQahKBw9T6+9h6n+RtOE+g==",
"dev": true,
"license": "MIT",
"peerDependencies": {
- "@better-auth/core": "^1.6.15",
- "@better-auth/utils": "0.4.1",
+ "@better-auth/core": "1.7.7",
+ "@better-auth/utils": "0.4.2",
"mongodb": "^6.0.0 || ^7.0.0"
},
"peerDependenciesMeta": {
@@ -410,14 +406,14 @@
}
},
"node_modules/@better-auth/prisma-adapter": {
- "version": "1.6.15",
- "resolved": "https://registry.npmjs.org/@better-auth/prisma-adapter/-/prisma-adapter-1.6.15.tgz",
- "integrity": "sha512-pUOlIUnpMu2l8C2ytgu46+OoOr3LZ+aLpJMFRGIPdnWl1BF2co2YaoZPT3+ZBu1Lzm0FrBN1ZRCAzZbkFoeIAQ==",
+ "version": "1.7.7",
+ "resolved": "https://registry.npmjs.org/@better-auth/prisma-adapter/-/prisma-adapter-1.7.7.tgz",
+ "integrity": "sha512-4YcnrcVdvWiAZw0sZl63tWUClPXg5r3QsLn7C2sG7kKEBY06zOng0ibecyxRBixKq1zE1ceHuEUqez5Rhi+nQA==",
"dev": true,
"license": "MIT",
"peerDependencies": {
- "@better-auth/core": "^1.6.15",
- "@better-auth/utils": "0.4.1",
+ "@better-auth/core": "1.7.7",
+ "@better-auth/utils": "0.4.2",
"@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0",
"prisma": "^5.0.0 || ^6.0.0 || ^7.0.0"
},
@@ -431,33 +427,33 @@
}
},
"node_modules/@better-auth/telemetry": {
- "version": "1.6.15",
- "resolved": "https://registry.npmjs.org/@better-auth/telemetry/-/telemetry-1.6.15.tgz",
- "integrity": "sha512-eoFlUPVrVXLML9saHD11OSKKSRCAYETgRgBW4vQF3Y6pCgmThKD9oehYk3kkb/FsRgfqmsocmvAFqsiyjPIygg==",
+ "version": "1.7.7",
+ "resolved": "https://registry.npmjs.org/@better-auth/telemetry/-/telemetry-1.7.7.tgz",
+ "integrity": "sha512-PaRA6i+kAioVYmza2gHPcdrxP0hAotph4KV9hft2GeNJq+AZXDiBu3FuLM9HxvcmiZeUT74Vufqd9ypmB8veTA==",
"dev": true,
"license": "MIT",
"peerDependencies": {
- "@better-auth/core": "^1.6.15",
- "@better-auth/utils": "0.4.1",
- "@better-fetch/fetch": "1.1.21"
+ "@better-auth/core": "1.7.7",
+ "@better-auth/utils": "0.4.2",
+ "@better-fetch/fetch": "1.3.2"
}
},
"node_modules/@better-auth/test-utils": {
- "version": "1.6.15",
- "resolved": "https://registry.npmjs.org/@better-auth/test-utils/-/test-utils-1.6.15.tgz",
- "integrity": "sha512-q5rvEsAhBHAXniwMlZpE1GiRSBPu/3rZM4iA5yOJ5uDw7fZjvWuufsJUc8raQuripXquLQZBYlGtVInHBadMyw==",
+ "version": "1.7.7",
+ "resolved": "https://registry.npmjs.org/@better-auth/test-utils/-/test-utils-1.7.7.tgz",
+ "integrity": "sha512-110vhQ9r/va7h/4ABIcejoy+p/lwBgoyEI8d4N1RRZ8Zh4T8IiDnGsOCj/VUVP3WBAy2vIZcd3rajZtjkg9yAw==",
"dev": true,
"license": "MIT",
"peerDependencies": {
- "@better-auth/core": "^1.6.15",
- "better-auth": "^1.6.15",
- "vitest": "^4.1.5"
+ "@better-auth/core": "1.7.7",
+ "better-auth": "1.7.7",
+ "vitest": "^4.0.0 || ^5.0.0"
}
},
"node_modules/@better-auth/utils": {
- "version": "0.4.1",
- "resolved": "https://registry.npmjs.org/@better-auth/utils/-/utils-0.4.1.tgz",
- "integrity": "sha512-SZBPRPF3z0nBvE5ygOkxae35wnnXPRShmqFo78S+qslLeFoPu/pMgnXAuNKFMMybac3tiLaVg1e3MQW5MC+1iA==",
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/@better-auth/utils/-/utils-0.4.2.tgz",
+ "integrity": "sha512-AUxrvu+HaaODsUyzDxFgwd/8RZ1yZaYo42LXKSrU2oGgR38pS1ij8nqQKNgtTWoYGpNevNXtCfgTy6loHveW9A==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -465,9 +461,10 @@
}
},
"node_modules/@better-fetch/fetch": {
- "version": "1.1.21",
- "resolved": "https://registry.npmjs.org/@better-fetch/fetch/-/fetch-1.1.21.tgz",
- "integrity": "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A=="
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/@better-fetch/fetch/-/fetch-1.3.2.tgz",
+ "integrity": "sha512-Gs7n99b5tqUC6cQAPbV0uED3IraHB6xQbHLQ/C3l7ZFafHScOx9pQ+DYmP5blbLFShVWLqxNUlI9wi4xU/X+ow==",
+ "license": "MIT"
},
"node_modules/@convex-dev/eslint-plugin": {
"version": "2.0.0",
@@ -2062,9 +2059,9 @@
}
},
"node_modules/@noble/ciphers": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-2.2.0.tgz",
- "integrity": "sha512-Z6pjIZ/8IJcCGzb2S/0Px5J81yij85xASuk1teLNeg75bfT07MV3a/O2Mtn1I2se43k3lkVEcFaR10N4cgQcZA==",
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-2.4.0.tgz",
+ "integrity": "sha512-AnjFn0Jv92laAkvMrghlFZq4qQCIN/4DxFV/eooqtC2YTjB7kBeLMS2T9KJX4Dn+ZVXLOwK0lSgqDtx9gvxtiw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2075,9 +2072,9 @@
}
},
"node_modules/@noble/hashes": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz",
- "integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==",
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.3.0.tgz",
+ "integrity": "sha512-oN+QwyX7VSHotibwubG3kpzbwKrfnyR6OOO+3Nk/53ADL7FmgHHz4TgrbaYKvvOw09u6QTx0oiH1cNCIOuN0CQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2140,9 +2137,9 @@
}
},
"node_modules/@opentelemetry/semantic-conventions": {
- "version": "1.40.0",
- "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.40.0.tgz",
- "integrity": "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw==",
+ "version": "1.43.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.43.0.tgz",
+ "integrity": "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==",
"dev": true,
"license": "Apache-2.0",
"engines": {
@@ -3826,29 +3823,29 @@
}
},
"node_modules/better-auth": {
- "version": "1.6.15",
- "resolved": "https://registry.npmjs.org/better-auth/-/better-auth-1.6.15.tgz",
- "integrity": "sha512-0nuQuEru3ZrLF+9xFUuN3llAmR+6gHLtLunoXaZxB9lXGjSmfBcc6SZUgYq4DfzugPnLvdnzYazsyprZFSFC4Q==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@better-auth/core": "1.6.15",
- "@better-auth/drizzle-adapter": "1.6.15",
- "@better-auth/kysely-adapter": "1.6.15",
- "@better-auth/memory-adapter": "1.6.15",
- "@better-auth/mongo-adapter": "1.6.15",
- "@better-auth/prisma-adapter": "1.6.15",
- "@better-auth/telemetry": "1.6.15",
- "@better-auth/utils": "0.4.1",
- "@better-fetch/fetch": "1.1.21",
- "@noble/ciphers": "^2.1.1",
- "@noble/hashes": "^2.0.1",
- "better-call": "1.3.5",
+ "version": "1.7.7",
+ "resolved": "https://registry.npmjs.org/better-auth/-/better-auth-1.7.7.tgz",
+ "integrity": "sha512-Zhgxi/c5ylmfB/sX+nwnBbsFf/j6k0L8DvL6CJXZSrXgxc5pPMC/e7j812zNd4L4B4ELmdURCPw3X8nAOUdcjw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@better-auth/core": "1.7.7",
+ "@better-auth/drizzle-adapter": "1.7.7",
+ "@better-auth/kysely-adapter": "1.7.7",
+ "@better-auth/memory-adapter": "1.7.7",
+ "@better-auth/mongo-adapter": "1.7.7",
+ "@better-auth/prisma-adapter": "1.7.7",
+ "@better-auth/telemetry": "1.7.7",
+ "@better-auth/utils": "0.4.2",
+ "@better-fetch/fetch": "1.3.2",
+ "@noble/ciphers": "^2.2.0",
+ "@noble/hashes": "^2.2.0",
+ "better-call": "1.4.0",
"defu": "^6.1.4",
- "jose": "^6.1.3",
+ "jose": "^6.2.3",
"kysely": "^0.28.17 || ^0.29.0",
- "nanostores": "^1.1.1",
- "zod": "^4.3.6"
+ "nanostores": "^1.3.0",
+ "zod": "^4.5.4"
},
"peerDependencies": {
"@lynx-js/react": "*",
@@ -3856,9 +3853,8 @@
"@sveltejs/kit": "^2.0.0",
"@tanstack/react-start": "^1.0.0",
"@tanstack/solid-start": "^1.0.0",
- "better-sqlite3": "^12.0.0",
- "drizzle-kit": ">=0.31.4",
- "drizzle-orm": "^0.45.2",
+ "drizzle-kit": ">=0.31.4 || >=1.0.0-beta.1",
+ "drizzle-orm": "^0.45.2 || >=1.0.0-rc.1 <2.0.0",
"mongodb": "^6.0.0 || ^7.0.0",
"mysql2": "^3.0.0",
"next": "^14.0.0 || ^15.0.0 || ^16.0.0",
@@ -3868,7 +3864,7 @@
"react-dom": "^18.0.0 || ^19.0.0",
"solid-js": "^1.0.0",
"svelte": "^4.0.0 || ^5.0.0",
- "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0",
+ "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0",
"vue": "^3.0.0"
},
"peerDependenciesMeta": {
@@ -3887,9 +3883,6 @@
"@tanstack/solid-start": {
"optional": true
},
- "better-sqlite3": {
- "optional": true
- },
"drizzle-kit": {
"optional": true
},
@@ -3932,16 +3925,16 @@
}
},
"node_modules/better-call": {
- "version": "1.3.5",
- "resolved": "https://registry.npmjs.org/better-call/-/better-call-1.3.5.tgz",
- "integrity": "sha512-kOFJkBP7utAQLEYrobZm3vkTH8mXq5GNgvjc5/XEST1ilVHaxXUXfeDeFlqoETMtyqS4+3/h4ONX2i++ebZrvA==",
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/better-call/-/better-call-1.4.0.tgz",
+ "integrity": "sha512-bBKOT4vv1kZLDgxVePdilk/Jwkn+dtRRsmi3DzHcDP+WnswyVl6dR59l2HEeP/0cB+bDoopASAesWDPIdd/zZA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@better-auth/utils": "^0.4.0",
- "@better-fetch/fetch": "^1.1.21",
- "rou3": "^0.7.12",
- "set-cookie-parser": "^3.0.1"
+ "@better-auth/utils": "^0.5.0",
+ "@better-fetch/fetch": "^1.3.1",
+ "rou3": "^0.9.1",
+ "set-cookie-parser": "^3.1.2"
},
"peerDependencies": {
"zod": "^4.0.0"
@@ -3952,6 +3945,16 @@
}
}
},
+ "node_modules/better-call/node_modules/@better-auth/utils": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/@better-auth/utils/-/utils-0.5.0.tgz",
+ "integrity": "sha512-BL8W4EfIZFwlu0r54m3v1ztjDhu6dDe/amLTm0xybmbZaNgYUqhD3SjpAsnq0q8YD6/ki4iwIgxJNLP/N3TxiA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@noble/hashes": "^2.0.1"
+ }
+ },
"node_modules/binary-extensions": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
@@ -6890,9 +6893,9 @@
}
},
"node_modules/nanostores": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/nanostores/-/nanostores-1.2.0.tgz",
- "integrity": "sha512-F0wCzbsH80G7XXo0Jd9/AVQC7ouWY6idUCTnMwW5t/Rv9W8qmO6endavDwg7TNp5GbugwSukFMVZqzPSrSMndg==",
+ "version": "1.5.2",
+ "resolved": "https://registry.npmjs.org/nanostores/-/nanostores-1.5.2.tgz",
+ "integrity": "sha512-B0UbxzK1s0CN8Xht6r+7iT5+xV8PTaRERR1nATeplRv1Rw5YLWfVAid0hkqY3EceqpG4RjTk8GAwIxQY39Rnwg==",
"dev": true,
"funding": [
{
@@ -7592,9 +7595,9 @@
"license": "MIT"
},
"node_modules/rou3": {
- "version": "0.7.12",
- "resolved": "https://registry.npmjs.org/rou3/-/rou3-0.7.12.tgz",
- "integrity": "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg==",
+ "version": "0.9.2",
+ "resolved": "https://registry.npmjs.org/rou3/-/rou3-0.9.2.tgz",
+ "integrity": "sha512-3SOzvaAg8rkHrXtRjpCvCvbyO5to9oOO27Z/XqHEYXfMRVSw/qMIVdmaOk9W2lcRLtR6dlqTjo9hDeJk70QBYQ==",
"dev": true,
"license": "MIT"
},
@@ -7713,9 +7716,9 @@
"license": "ISC"
},
"node_modules/set-cookie-parser": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.0.tgz",
- "integrity": "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==",
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.2.tgz",
+ "integrity": "sha512-5/r/lTwbJ3zQ+qwdUFZYeRNqda7P5HD8zQKqlSjdGt1/S0cjLAphHusj4Y58ahDtWn/g32xrIS58/ikOvwl0Lw==",
"dev": true,
"license": "MIT"
},
@@ -8240,9 +8243,9 @@
}
},
"node_modules/tinyrainbow": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz",
- "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==",
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz",
+ "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==",
"dev": true,
"license": "MIT",
"engines": {
diff --git a/package.json b/package.json
index 17366e76..f9b5a9d9 100644
--- a/package.json
+++ b/package.json
@@ -101,13 +101,13 @@
}
},
"peerDependencies": {
- "better-auth": ">=1.6.11 <1.7.0",
+ "better-auth": ">=1.7.7 <1.8.0",
"convex": "^1.25.0",
"react": "^18.3.1 || ^19.0.0"
},
"devDependencies": {
- "@better-auth/core": "1.6.15",
- "@better-auth/test-utils": "1.6.15",
+ "@better-auth/core": "1.7.7",
+ "@better-auth/test-utils": "1.7.7",
"@convex-dev/eslint-plugin": "2.0.0",
"@edge-runtime/vm": "5.0.0",
"@eslint/eslintrc": "3.3.5",
@@ -118,7 +118,7 @@
"@types/react": "19.2.14",
"@types/react-dom": "19.2.3",
"@types/semver": "7.7.1",
- "better-auth": "1.6.15",
+ "better-auth": "1.7.7",
"chokidar-cli": "3.0.0",
"concurrently": "10.0.3",
"convex": "1.46.0",
@@ -141,7 +141,7 @@
"types": "./dist/client/index.d.ts",
"module": "./dist/client/index.js",
"dependencies": {
- "@better-fetch/fetch": "^1.1.18",
+ "@better-fetch/fetch": "^1.3.2",
"common-tags": "^1.8.2",
"convex-helpers": "^0.1.128",
"jose": "^6.1.0",
diff --git a/src/auth-config.test.ts b/src/auth-config.test.ts
new file mode 100644
index 00000000..a7725bc5
--- /dev/null
+++ b/src/auth-config.test.ts
@@ -0,0 +1,66 @@
+import { afterEach, beforeEach, describe, expect, it } from "vitest";
+import { getAuthConfigProvider } from "./auth-config.js";
+
+describe("getAuthConfigProvider", () => {
+ const originalConvexSiteUrl = process.env.CONVEX_SITE_URL;
+
+ beforeEach(() => {
+ process.env.CONVEX_SITE_URL = "https://deployment.convex.site/";
+ });
+
+ afterEach(() => {
+ if (originalConvexSiteUrl === undefined) {
+ delete process.env.CONVEX_SITE_URL;
+ } else {
+ process.env.CONVEX_SITE_URL = originalConvexSiteUrl;
+ }
+ });
+
+ it.each([
+ [undefined, "https://deployment.convex.site/api/auth/convex/jwks"],
+ ["/", "https://deployment.convex.site/convex/jwks"],
+ ["/custom/auth/", "https://deployment.convex.site/custom/auth/convex/jwks"],
+ ["custom/auth", "https://deployment.convex.site/custom/auth/convex/jwks"],
+ ])("normalizes the %s base path", (basePath, expected) => {
+ expect(getAuthConfigProvider({ basePath }).jwks).toBe(expected);
+ });
+
+ it("reports a missing Convex site URL", () => {
+ delete process.env.CONVEX_SITE_URL;
+
+ expect(() => getAuthConfigProvider()).toThrow("CONVEX_SITE_URL is not set");
+ });
+
+ it.each(["deployment.convex.site", "ftp://deployment.convex.site"])(
+ "rejects the invalid Convex site URL %s",
+ (siteUrl) => {
+ process.env.CONVEX_SITE_URL = siteUrl;
+
+ expect(() => getAuthConfigProvider()).toThrow(
+ "CONVEX_SITE_URL must be a valid HTTP or HTTPS URL"
+ );
+ expect(() => getAuthConfigProvider({ jwks: "[]" })).toThrow(
+ "CONVEX_SITE_URL must be a valid HTTP or HTTPS URL"
+ );
+ }
+ );
+
+ it.each([
+ "https://deployment.convex.site?region=us",
+ "https://deployment.convex.site#fragment",
+ ])("rejects URL components in the Convex site URL %s", (siteUrl) => {
+ process.env.CONVEX_SITE_URL = siteUrl;
+
+ expect(() => getAuthConfigProvider()).toThrow(
+ "CONVEX_SITE_URL must not include a query string or fragment"
+ );
+ });
+
+ it("accepts an HTTP URL for a local Convex deployment", () => {
+ process.env.CONVEX_SITE_URL = "http://127.0.0.1:3211";
+
+ expect(getAuthConfigProvider().jwks).toBe(
+ "http://127.0.0.1:3211/api/auth/convex/jwks"
+ );
+ });
+});
diff --git a/src/auth-config.ts b/src/auth-config.ts
index 246974fc..b7ecae36 100644
--- a/src/auth-config.ts
+++ b/src/auth-config.ts
@@ -1,6 +1,7 @@
import type { JwtOptions } from "better-auth/plugins/jwt";
import type { AuthProvider } from "convex/server";
import type { JSONWebKeySet } from "jose";
+import { getJwksUrl, validateConvexSiteUrl } from "./jwks-url.js";
type JwksDoc = {
id: string;
@@ -69,14 +70,19 @@ export const getAuthConfigProvider = (opts?: {
*/
jwks?: string;
}) => {
+ const siteUrl = process.env.CONVEX_SITE_URL;
+ if (!siteUrl) {
+ throw new Error("CONVEX_SITE_URL is not set");
+ }
+ validateConvexSiteUrl(siteUrl);
const parsedJwks = opts?.jwks ? JSON.parse(opts.jwks) : undefined;
return {
type: "customJwt",
- issuer: `${process.env.CONVEX_SITE_URL}`,
+ issuer: siteUrl,
applicationID: "convex",
algorithm: "RS256",
jwks: parsedJwks
? `data:text/plain;charset=utf-8;base64,${btoa(JSON.stringify(createPublicJwks(parsedJwks)))}`
- : `${process.env.CONVEX_SITE_URL}${opts?.basePath ?? "/api/auth"}/convex/jwks`,
+ : getJwksUrl(siteUrl, opts?.basePath),
} satisfies AuthProvider;
};
diff --git a/src/auth-options.ts b/src/auth-options.ts
index 6281482f..eda273f4 100644
--- a/src/auth-options.ts
+++ b/src/auth-options.ts
@@ -5,7 +5,6 @@ import { emailOTP } from "better-auth/plugins/email-otp";
import { genericOAuth } from "better-auth/plugins/generic-oauth";
import { jwt } from "better-auth/plugins/jwt";
import { magicLink } from "better-auth/plugins/magic-link";
-import { oidcProvider } from "better-auth/plugins/oidc-provider";
import { oneTimeToken } from "better-auth/plugins/one-time-token";
import { phoneNumber } from "better-auth/plugins/phone-number";
import { twoFactor } from "better-auth/plugins/two-factor";
@@ -35,10 +34,6 @@ export const options = {
},
],
}),
- oidcProvider({
- loginPage: "/login",
- __skipDeprecationWarning: true,
- }),
bearer(),
oneTimeToken(),
jwt(),
diff --git a/src/client/adapter-compatibility.test.ts b/src/client/adapter-compatibility.test.ts
new file mode 100644
index 00000000..d552c9d7
--- /dev/null
+++ b/src/client/adapter-compatibility.test.ts
@@ -0,0 +1,200 @@
+///
+
+import { describe, expect, it } from "vitest";
+import { convexTest } from "convex-test";
+import { api } from "../component/_generated/api.js";
+import schema from "../component/schema.js";
+import { convexAdapter } from "./adapter.js";
+
+const setup = () => {
+ const t = convexTest(schema, import.meta.glob("../component/**/*.*s"));
+ const adapter = convexAdapter(
+ {
+ runQuery: t.query.bind(t),
+ runMutation: t.mutation.bind(t),
+ } as any,
+ { adapter: api.adapter } as any
+ )({});
+ return { t, adapter };
+};
+
+describe("Better Auth 1.7.7 compatibility", () => {
+ it.each([false, true])(
+ "keeps modelKey out of component calls (OR: %s)",
+ async (or) => {
+ const { adapter } = setup();
+ await adapter.create({
+ model: "user",
+ data: { name: "Alice", email: "alice@example.com" },
+ });
+ const where = or
+ ? [
+ { field: "name", value: "Alice", connector: "OR" as const },
+ {
+ field: "email",
+ value: "alice@example.com",
+ connector: "OR" as const,
+ },
+ ]
+ : [{ field: "name", value: "Alice" }];
+ expect(await adapter.findOne({ model: "user", where })).toMatchObject({
+ name: "Alice",
+ });
+ expect(await adapter.findMany({ model: "user", where })).toHaveLength(1);
+ expect(await adapter.count({ model: "user", where })).toBe(1);
+ expect(
+ await adapter.updateMany({
+ model: "user",
+ where,
+ update: { emailVerified: true },
+ })
+ ).toBe(1);
+ expect(await adapter.findOne({ model: "user", where })).toMatchObject({
+ emailVerified: true,
+ });
+ expect(await adapter.deleteMany({ model: "user", where })).toBe(1);
+ expect(await adapter.count({ model: "user", where })).toBe(0);
+ }
+ );
+
+ it("does not revisit rows when filtered pagination splits a page", async () => {
+ const { t, adapter } = setup();
+ await t.run(async (ctx) => {
+ // Rejected rows force the scan budget to split before 200 matches.
+ for (let i = 0; i < 4; i++) {
+ await ctx.db.insert("user", {
+ name: `Other ${i}`,
+ email: `other-${i}@example.com`,
+ emailVerified: false,
+ createdAt: 1,
+ updatedAt: 1,
+ });
+ }
+ for (let i = 0; i < 205; i++) {
+ await ctx.db.insert("user", {
+ name: `Fixture ${i}`,
+ email: `fixture-${i}@example.com`,
+ emailVerified: false,
+ createdAt: 1,
+ updatedAt: 1,
+ });
+ }
+ });
+ const where = [
+ { field: "email", operator: "starts_with" as const, value: "fixture-" },
+ ];
+ expect(await adapter.count({ model: "user", where })).toBe(205);
+ const users = await adapter.findMany<{ id: string }>({
+ model: "user",
+ where,
+ limit: 1000,
+ });
+ expect(users).toHaveLength(205);
+ expect(new Set(users.map((user) => user.id)).size).toBe(205);
+ const limited = await adapter.findMany<{ id: string }>({
+ model: "user",
+ where,
+ limit: 200,
+ });
+ expect(limited).toHaveLength(200);
+ expect(new Set(limited.map((user) => user.id)).size).toBe(200);
+ expect(
+ await adapter.updateMany({
+ model: "user",
+ where,
+ update: { name: "Updated" },
+ })
+ ).toBe(205);
+ expect(await adapter.deleteMany({ model: "user", where })).toBe(205);
+ expect(await adapter.count({ model: "user", where })).toBe(0);
+ });
+
+ it.each([undefined, null, "already-set"])(
+ "guards atomic updates against a null optional field (stored: %s)",
+ async (image) => {
+ const { adapter } = setup();
+ const user = await adapter.create<{ id: string }>({
+ model: "user",
+ data: {
+ name: "Alice",
+ email: "alice@example.com",
+ ...(image !== undefined ? { image } : {}),
+ },
+ });
+ const where = [
+ { field: "id", value: user.id },
+ { field: "image", operator: "eq" as const, value: null },
+ ];
+ const expectedMatch = image == null;
+ expect(Boolean(await adapter.findOne({ model: "user", where }))).toBe(
+ expectedMatch
+ );
+ expect(await adapter.findMany({ model: "user", where })).toHaveLength(
+ expectedMatch ? 1 : 0
+ );
+ expect(
+ Boolean(
+ await adapter.findOne({
+ model: "user",
+ where: [
+ { field: "id", value: user.id },
+ { field: "image", operator: "ne", value: null },
+ ],
+ })
+ )
+ ).toBe(!expectedMatch);
+ // OAuth refresh rotation uses this shape: an empty increment with a
+ // guarded set, where the revocation field may never have been written.
+ const claimed = await adapter.incrementOne<{ image: string }>({
+ model: "user",
+ where,
+ increment: {},
+ set: { image: "claimed" },
+ });
+ expect(claimed?.image ?? null).toBe(expectedMatch ? "claimed" : null);
+ expect(
+ await adapter.incrementOne({
+ model: "user",
+ where,
+ increment: {},
+ set: { image: "claimed-again" },
+ })
+ ).toBeNull();
+ }
+ );
+
+ it("creates and updates core accounts without an issuer backfill", async () => {
+ const { t, adapter } = setup();
+ const account = await adapter.create({
+ model: "account",
+ data: {
+ accountId: "subject",
+ providerId: "provider-a",
+ userId: "user-a",
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ },
+ });
+ await adapter.create({
+ model: "account",
+ data: {
+ accountId: "subject",
+ providerId: "provider-b",
+ userId: "user-b",
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ },
+ });
+ await adapter.update({
+ model: "account",
+ where: [{ field: "id", value: account.id }],
+ update: { scope: "openid" },
+ });
+ const accounts = await t.run((ctx) => ctx.db.query("account").collect());
+ expect(accounts).toHaveLength(2);
+ expect(accounts.every((row) => row.issuer === undefined)).toBe(true);
+ expect(accounts.find((row) => row.providerId === "provider-a")?.scope).toBe(
+ "openid"
+ );
+ });
+});
diff --git a/src/client/adapter-utils.test.ts b/src/client/adapter-utils.test.ts
new file mode 100644
index 00000000..1e0680f1
--- /dev/null
+++ b/src/client/adapter-utils.test.ts
@@ -0,0 +1,131 @@
+///
+
+import { describe, expect, it, vi } from "vitest";
+import { convexTest } from "convex-test";
+import { defineSchema, defineTable } from "convex/server";
+import { v } from "convex/values";
+import type { BetterAuthDBSchema } from "better-auth/db";
+import { checkUniqueFields, paginate } from "./adapter-utils.js";
+
+const schema = defineSchema({
+ account: defineTable({
+ issuer: v.string(),
+ accountId: v.string(),
+ }).index("issuer_accountId", ["issuer", "accountId"]),
+});
+
+const betterAuthSchema = {
+ account: {
+ modelName: "account",
+ fields: {
+ issuer: { type: "string", required: true },
+ accountId: { type: "string", required: true },
+ },
+ indexes: [{ fields: ["issuer", "accountId"], unique: true }],
+ },
+} satisfies BetterAuthDBSchema;
+
+const credentialSchema = defineSchema({
+ account: defineTable({
+ userId: v.string(),
+ providerId: v.string(),
+ issuer: v.string(),
+ accountId: v.string(),
+ }).index("userId_providerId_issuer_accountId", [
+ "userId",
+ "providerId",
+ "issuer",
+ "accountId",
+ ]),
+});
+
+const credentialBetterAuthSchema = {
+ account: {
+ modelName: "account",
+ fields: {
+ userId: { type: "string", required: true },
+ providerId: { type: "string", required: true },
+ issuer: { type: "string", required: true },
+ accountId: { type: "string", required: true },
+ },
+ },
+} satisfies BetterAuthDBSchema;
+
+const modules = import.meta.glob("../component/**/*.*s");
+
+describe("ordered compound indexes", () => {
+ it("uses an index for Better Auth's credential account lookup", async () => {
+ const t = convexTest(credentialSchema, modules);
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => undefined);
+
+ try {
+ await t.run((ctx) =>
+ paginate(ctx, credentialSchema, credentialBetterAuthSchema, {
+ model: "account",
+ where: [
+ { field: "userId", value: "user-1" },
+ { field: "providerId", value: "credential" },
+ { field: "issuer", value: "local:credential" },
+ { field: "accountId", value: "user-1" },
+ ],
+ paginationOpts: { cursor: null, numItems: 1 },
+ })
+ );
+ expect(warn).not.toHaveBeenCalled();
+ } finally {
+ warn.mockRestore();
+ }
+ });
+
+ it("pairs equality values with fields in the selected index order", async () => {
+ const t = convexTest(schema, modules);
+ await t.run(async (ctx) => {
+ await ctx.db.insert("account", {
+ issuer: "issuer-value",
+ accountId: "account-value",
+ });
+ await ctx.db.insert("account", {
+ issuer: "account-value",
+ accountId: "issuer-value",
+ });
+ });
+
+ const result = await t.run((ctx) =>
+ paginate(ctx, schema, betterAuthSchema, {
+ model: "account",
+ where: [
+ { field: "accountId", value: "account-value" },
+ { field: "issuer", value: "issuer-value" },
+ ],
+ paginationOpts: { cursor: null, numItems: 10 },
+ })
+ );
+
+ expect(result.page).toHaveLength(1);
+ expect(result.page[0]).toMatchObject({
+ issuer: "issuer-value",
+ accountId: "account-value",
+ });
+ });
+
+ it("checks compound uniqueness against the declared index order", async () => {
+ const t = convexTest(schema, modules);
+ await t.run((ctx) =>
+ ctx.db.insert("account", {
+ issuer: "issuer-value",
+ accountId: "account-value",
+ })
+ );
+
+ await expect(
+ t.run((ctx) =>
+ checkUniqueFields(ctx, schema, betterAuthSchema, "account", {
+ issuer: "issuer-value",
+ accountId: "account-value",
+ })
+ )
+ ).rejects.toThrow(
+ "account unique constraint issuer+accountId already exists"
+ );
+ });
+});
diff --git a/src/client/adapter-utils.ts b/src/client/adapter-utils.ts
index 7e04ffdf..66eafe76 100644
--- a/src/client/adapter-utils.ts
+++ b/src/client/adapter-utils.ts
@@ -58,33 +58,54 @@ export const adapterArgsValidator = v.object({
offset: v.optional(v.number()),
});
-const isUniqueField = (
+const getUniqueConstraints = (
betterAuthSchema: BetterAuthDBSchema,
- model: string,
- field: string
+ model: string
) => {
- const fields = Object.values(betterAuthSchema).find(
+ const table = Object.values(betterAuthSchema).find(
(value) => value.modelName === model
- )?.fields;
- if (!fields) {
- return false;
+ );
+ if (!table) {
+ return [];
}
- return Object.entries(fields)
+ const singleFieldConstraints = Object.entries(table.fields)
.filter(([, value]) => value.unique)
- .map(([key]) => key)
- .includes(field);
+ .map(([key, value]) => [value.fieldName ?? key]);
+ const tableConstraints =
+ table.indexes
+ ?.filter((index) => index.unique)
+ .map((index) =>
+ index.fields.map((field) => table.fields[field]?.fieldName ?? field)
+ ) ?? [];
+ return [...singleFieldConstraints, ...tableConstraints];
};
+const isUniqueField = (
+ betterAuthSchema: BetterAuthDBSchema,
+ model: string,
+ field: string
+) =>
+ getUniqueConstraints(betterAuthSchema, model).some(
+ (constraint) => constraint.length === 1 && constraint[0] === field
+ );
export const hasUniqueFields = (
betterAuthSchema: BetterAuthDBSchema,
model: string,
input: Record
) => {
- for (const field of Object.keys(input)) {
- if (isUniqueField(betterAuthSchema, model, field)) {
- return true;
- }
- }
- return false;
+ const inputFields = new Set(Object.keys(input));
+ return getUniqueConstraints(betterAuthSchema, model).some((constraint) =>
+ constraint.every((field) => inputFields.has(field))
+ );
+};
+export const touchesUniqueFields = (
+ betterAuthSchema: BetterAuthDBSchema,
+ model: string,
+ input: Record
+) => {
+ const inputFields = new Set(Object.keys(input));
+ return getUniqueConstraints(betterAuthSchema, model).some((constraint) =>
+ constraint.some((field) => inputFields.has(field))
+ );
};
const findIndex = (
@@ -170,10 +191,7 @@ const findIndex = (
const indexEqFields =
where
?.filter((w) => !w.operator || w.operator === "eq")
- .sort((a, b) => {
- return a.field.localeCompare(b.field);
- })
- .map((w) => [w.field, w.value]) ?? [];
+ .map((w) => [w.field, w.value] as const) ?? [];
if (!indexEqFields?.length && !boundField && !args.sortBy) {
return;
}
@@ -185,8 +203,16 @@ const findIndex = (
const sortField = args.sortBy?.field;
// We internally use _creationTime in place of Better Auth's createdAt
- const indexFields = indexEqFields
- .map(([field]) => field)
+ const eqFields = indexEqFields.map(([field]) => field);
+ const eqValues = new Map(indexEqFields);
+ const suffixFields = [
+ boundField && boundField !== "createdAt" ? boundField : "",
+ sortField && sortField !== "createdAt" && boundField !== sortField
+ ? sortField
+ : "",
+ ].filter(Boolean);
+ const indexFields = [...eqFields]
+ .sort((a, b) => a.localeCompare(b))
.concat(boundField && boundField !== "createdAt" ? boundField : "")
.concat(
sortField && sortField !== "createdAt" && boundField !== sortField
@@ -204,18 +230,35 @@ const findIndex = (
indexDescriptor: "by_creation_time",
fields: [],
}
- : indexes.find(({ fields }: { fields: string[] }) => {
- const fieldsMatch = indexFields.every(
- (field, idx) => field === fields[idx]
- );
- // If sorting by createdAt, no intermediate fields can be on the index
- // as they may override the createdAt sort order.
- const boundFieldMatch =
- boundField === "createdAt" || sortField === "createdAt"
- ? indexFields.length === fields.length
- : true;
- return fieldsMatch && boundFieldMatch;
- });
+ : (indexes.find(({ fields }: { fields: string[] }) =>
+ matchesIndex(fields, true)
+ ) ??
+ indexes.find(({ fields }: { fields: string[] }) =>
+ matchesIndex(fields, false)
+ ));
+
+ function matchesIndex(fields: string[], requireExactEqOrder: boolean) {
+ if (
+ eqValues.size !== eqFields.length ||
+ fields.length < eqFields.length + suffixFields.length
+ ) {
+ return false;
+ }
+ const eqPrefix = fields.slice(0, eqFields.length);
+ const eqFieldsMatch = requireExactEqOrder
+ ? eqPrefix.every((field, index) => field === eqFields[index])
+ : eqPrefix.every((field) => eqValues.has(field));
+ const suffixFieldsMatch = suffixFields.every(
+ (field, index) => field === fields[eqFields.length + index]
+ );
+ // If sorting by createdAt, no intermediate fields can be on the index
+ // as they may override the createdAt sort order.
+ const createdAtFieldMatch =
+ boundField === "createdAt" || sortField === "createdAt"
+ ? eqFields.length + suffixFields.length === fields.length
+ : true;
+ return eqFieldsMatch && suffixFieldsMatch && createdAtFieldMatch;
+ }
if (!index) {
return { indexFields };
}
@@ -227,7 +270,9 @@ const findIndex = (
boundField,
sortField,
values: {
- eq: indexEqFields.map(([, value]) => value),
+ eq: index.fields
+ .slice(0, eqFields.length)
+ .map((field: string) => [field, eqValues.get(field)!] as const),
lt: lowerBound?.operator === "lt" ? lowerBound.value : undefined,
lte: lowerBound?.operator === "lte" ? lowerBound.value : undefined,
gt: upperBound?.operator === "gt" ? upperBound.value : undefined,
@@ -246,31 +291,51 @@ export const checkUniqueFields = async <
input: Record,
doc?: Record
) => {
- if (!hasUniqueFields(betterAuthSchema, table, input)) {
- return;
- }
- for (const field of Object.keys(input)) {
- if (!isUniqueField(betterAuthSchema, table, field)) {
+ const mergedDoc = { ...doc, ...input };
+ for (const constraint of getUniqueConstraints(betterAuthSchema, table)) {
+ if (
+ !constraint.some((field) => field in input) ||
+ !constraint.every((field) => mergedDoc[field] !== undefined)
+ ) {
continue;
}
- const { index } =
- findIndex(schema, {
- model: table,
- where: [
- { field, operator: "eq", value: input[field as keyof typeof input] },
- ],
- }) || {};
+ const { index } = findIndex(schema, {
+ model: table,
+ where: constraint.map((field) => ({
+ field,
+ operator: "eq" as const,
+ value: mergedDoc[field],
+ })),
+ }) || { index: undefined };
if (!index) {
- throw new Error(`No index found for ${table}${field}`);
+ throw new Error(
+ `No index found for unique constraint ${table}.${constraint.join("+")}`
+ );
}
- const existingDoc = await ctx.db
+ const indexConstraintFields = index.fields.slice(0, constraint.length);
+ if (
+ indexConstraintFields.length !== constraint.length ||
+ !indexConstraintFields.every((field) => constraint.includes(field))
+ ) {
+ throw new Error(
+ `Index ${index.indexDescriptor} does not match unique constraint ${table}.${constraint.join("+")}`
+ );
+ }
+ const existingDocs = await ctx.db
.query(table as any)
.withIndex(index.indexDescriptor, (q) =>
- q.eq(field, input[field as keyof typeof input])
+ indexConstraintFields.reduce(
+ (query: any, field: string) => query.eq(field, mergedDoc[field]),
+ q
+ )
)
- .unique();
- if (existingDoc && existingDoc._id !== doc?._id) {
- throw new Error(`${table} ${field} already exists`);
+ .take(2);
+ if (existingDocs.some((existingDoc) => existingDoc._id !== doc?._id)) {
+ const constraintName =
+ constraint.length === 1
+ ? constraint[0]
+ : `unique constraint ${constraint.join("+")}`;
+ throw new Error(`${table} ${constraintName} already exists`);
}
}
};
@@ -343,7 +408,9 @@ const filterByWhere = <
switch (w.operator) {
case undefined:
case "eq": {
- return value === w.value;
+ // Convex omits unset optional fields; Better Auth's null guards must
+ // also match those fields (for example, unrevoked refresh tokens).
+ return w.value === null ? value == null : value === w.value;
}
case "in": {
return Array.isArray(w.value) && (w.value as any[]).includes(value);
@@ -366,7 +433,7 @@ const filterByWhere = <
return value === w.value || isGreaterThan(value, w.value);
}
case "ne": {
- return value !== w.value;
+ return w.value === null ? value != null : value !== w.value;
}
case "contains": {
return typeof value === "string" && value.includes(w.value as string);
@@ -409,8 +476,8 @@ const generateQuery = (
usableIndex.indexDescriptor,
hasValues
? (q: any) => {
- for (const [idx, value] of (values?.eq ?? []).entries()) {
- q = q.eq(usableIndex.fields[idx], value);
+ for (const [field, value] of values?.eq ?? []) {
+ q = q.eq(field, value);
}
if (values?.lt !== undefined) {
q = q.lt(boundField, values.lt);
@@ -518,9 +585,7 @@ export const paginate = async <
where: [uniqueWhere],
}) || {};
if (uniqueWhere.field !== "_id" && !index) {
- throw new Error(
- `No index found for ${args.model}.${uniqueWhere.field}`
- );
+ throw new Error(`No index found for ${args.model}.${uniqueWhere.field}`);
}
const doc =
uniqueWhere.field === "_id"
@@ -530,7 +595,7 @@ export const paginate = async <
: await ctx.db
.query(args.model as any)
.withIndex(index?.indexDescriptor as any, (q) =>
- q.eq(index?.fields[0], uniqueWhere.value)
+ q.eq(uniqueWhere.field, uniqueWhere.value)
)
.unique();
diff --git a/src/client/adapter.test.ts b/src/client/adapter.test.ts
index c5fe58ee..f6139834 100644
--- a/src/client/adapter.test.ts
+++ b/src/client/adapter.test.ts
@@ -25,6 +25,8 @@ import {
renameModelUserTableTestSuite,
} from "../test/adapter-factory/index.js";
+import { constraintsProfileOptions } from "../component/testProfiles/auth-options.constraints.js";
+
const MIN_NODE_MAJOR = 24;
const currentNodeMajor = Number.parseInt(
process.versions.node.split(".")[0] ?? "0",
@@ -91,6 +93,7 @@ const getOverrideBetterAuthOptions = (
type AdapterModule = ComponentApi["adapter"];
type TestProfileName =
+ | "adapterConstraints"
| "adapterAdditionalFields"
| "adapterPluginTable"
| "adapterRenameField"
@@ -169,10 +172,28 @@ if (currentNodeMajor < MIN_NODE_MAJOR) {
}),
transactionsTestSuite({ disableTests: { ALL: true } }),
coreAuthFlowTestSuite(),
- convexCustomTestSuite(),
],
});
+ const constraintsProfileClient = createClient(
+ profileApi("adapterConstraints"),
+ { verbose: false }
+ );
+ const { execute: executeConstraintsProfile } = await testAdapter({
+ adapter: () => constraintsProfileClient.adapter(wrappedCtx),
+ runMigrations: noMigrations,
+ overrideBetterAuthOptions: (opts) =>
+ getOverrideBetterAuthOptions({
+ ...opts,
+ plugins: [
+ ...(opts.plugins ?? []),
+ ...constraintsProfileOptions.plugins,
+ ],
+ }),
+ prefixTests: "profile:custom-constraints",
+ tests: [convexCustomTestSuite()],
+ });
+
const { execute: executeAdditionalFieldsProfile } = await testAdapter({
adapter: () => additionalFieldsProfileClient.adapter(wrappedCtx),
runMigrations: noMigrations,
@@ -225,6 +246,7 @@ if (currentNodeMajor < MIN_NODE_MAJOR) {
});
executeBaseProfile();
+ executeConstraintsProfile();
executeAdditionalFieldsProfile();
executePluginTableProfile();
executeRenameFieldProfile();
diff --git a/src/client/adapter.ts b/src/client/adapter.ts
index c40b60ac..ad73e3fd 100644
--- a/src/client/adapter.ts
+++ b/src/client/adapter.ts
@@ -3,6 +3,7 @@ import type { DBAdapterDebugLogOption } from "better-auth/adapters";
import { createFunctionHandle } from "convex/server";
import type {
FunctionHandle,
+ FunctionReference,
GenericActionCtx,
GenericDataModel,
PaginationOptions,
@@ -17,10 +18,9 @@ import { asyncMap } from "convex-helpers";
import { prop, sortBy } from "remeda";
import { isRunMutationCtx } from "../utils/index.js";
import type { Doc, TableNames } from "../component/_generated/dataModel.js";
-import type { ComponentApi } from "../component/_generated/component.js";
import type { AuthFunctions, GenericCtx, Triggers } from "./index.js";
-let didWarnExperimentalJoinsUnsupported = false;
+let didWarnJoinsUnsupported = false;
const handlePagination = async (
next: ({
@@ -46,11 +46,9 @@ const handlePagination = async (
const onResult = (
result: SetOptional, "page"> & { count?: number }
) => {
- state.cursor =
- result.pageStatus === "SplitRecommended" ||
- result.pageStatus === "SplitRequired"
- ? (result.splitCursor ?? result.continueCursor)
- : result.continueCursor;
+ // splitCursor divides the page we already received. Continuing there would
+ // revisit its second half and double-count reads and mutations.
+ state.cursor = result.continueCursor;
if (result.page) {
state.docs.push(...result.page);
state.isDone = (limit && state.docs.length >= limit) || result.isDone;
@@ -84,8 +82,7 @@ const handlePagination = async (
// A page that returns nothing and doesn't advance the cursor while !isDone
// can never terminate; fail loudly instead of looping forever.
const advanced = state.cursor !== cursorBeforePage;
- const produced =
- (result.page?.length ?? 0) > 0 || (result.count ?? 0) > 0;
+ const produced = (result.page?.length ?? 0) > 0 || (result.count ?? 0) > 0;
if (!state.isDone && !advanced && !produced) {
throw new Error(
"handlePagination made no forward progress; aborting to avoid an infinite pagination loop"
@@ -184,7 +181,16 @@ export const convexAdapter = <
>(
ctx: Ctx,
api: {
- adapter: ComponentApi["adapter"];
+ adapter: {
+ create: FunctionReference<"mutation", "internal">;
+ findOne: FunctionReference<"query", "internal">;
+ findMany: FunctionReference<"query", "internal">;
+ updateOne: FunctionReference<"mutation", "internal">;
+ incrementOne: FunctionReference<"mutation", "internal">;
+ updateMany: FunctionReference<"mutation", "internal">;
+ deleteOne: FunctionReference<"mutation", "internal">;
+ deleteMany: FunctionReference<"mutation", "internal">;
+ };
},
config: {
debugLogs?: DBAdapterDebugLogOption;
@@ -228,16 +234,19 @@ export const convexAdapter = <
adapter: ({ options }) => {
// Disable telemetry in all cases because it requires Node
options.telemetry = { enabled: false };
- if (options.experimental?.joins) {
- options.experimental = {
- ...options.experimental,
- joins: false,
+ if (options.advanced?.database?.joins) {
+ options.advanced = {
+ ...options.advanced,
+ database: {
+ ...options.advanced.database,
+ joins: false,
+ },
};
- if (!didWarnExperimentalJoinsUnsupported) {
- didWarnExperimentalJoinsUnsupported = true;
+ if (!didWarnJoinsUnsupported) {
+ didWarnJoinsUnsupported = true;
// eslint-disable-next-line no-console
console.warn(
- "[convex-better-auth] Better Auth experimental.joins is not supported by the Convex adapter yet. Forcing experimental.joins = false."
+ "[convex-better-auth] Better Auth advanced.database.joins is not supported by the Convex adapter yet. Forcing advanced.database.joins = false."
);
}
}
@@ -247,15 +256,13 @@ export const convexAdapter = <
where: (Where & { join?: undefined })[];
}) => {
const results = await asyncMap(data.where, async (w) =>
- handlePagination(
- async ({ paginationOpts }) => {
- return await ctx.runQuery(api.adapter.findMany, {
- model: data.model as TableNames,
- where: parseWhere(w),
- paginationOpts,
- });
- }
- )
+ handlePagination(async ({ paginationOpts }) => {
+ return await ctx.runQuery(api.adapter.findMany, {
+ model: data.model as TableNames,
+ where: parseWhere(w),
+ paginationOpts,
+ });
+ })
);
const ids = dedupeDocsById(results.flatMap((r) => r.docs))
.map((doc) => getDocId(doc))
@@ -288,7 +295,8 @@ export const convexAdapter = <
onCreateHandle: onCreateHandle,
});
},
- findOne: async (data): Promise => {
+ // modelKey is Better Auth metadata, not a Convex component argument.
+ findOne: async ({ modelKey: _modelKey, ...data }): Promise => {
if (data.where?.every((w) => w.connector === "OR")) {
for (const w of data.where) {
const result = await ctx.runQuery(api.adapter.findOne, {
@@ -307,7 +315,7 @@ export const convexAdapter = <
where: parseWhere(data.where),
});
},
- findMany: async (data): Promise => {
+ findMany: async ({ modelKey: _modelKey, ...data }): Promise => {
if (data.offset) {
throw new Error("offset not supported");
}
@@ -355,7 +363,7 @@ export const convexAdapter = <
);
return result.docs;
},
- count: async (data) => {
+ count: async ({ modelKey: _modelKey, ...data }) => {
// Yes, count is just findMany returning a number.
if (data.where?.some((w) => w.connector === "OR")) {
const results = await asyncMap(data.where, async (w) =>
@@ -410,6 +418,52 @@ export const convexAdapter = <
onUpdateHandle: onUpdateHandle,
});
},
+ consumeOne: async (data): Promise => {
+ if (!("runMutation" in ctx)) {
+ throw new Error("ctx is not a mutation ctx");
+ }
+ if (!data.where.length) {
+ return null;
+ }
+ const onDeleteHandle =
+ config.authFunctions?.onDelete &&
+ config.triggers?.[data.model]?.onDelete
+ ? ((await createFunctionHandle(
+ config.authFunctions.onDelete
+ )) as FunctionHandle<"mutation">)
+ : undefined;
+ return ctx.runMutation(api.adapter.deleteOne, {
+ input: {
+ model: data.model as TableNames,
+ where: parseWhere(data.where),
+ },
+ onDeleteHandle,
+ });
+ },
+ incrementOne: async (data): Promise => {
+ if (!("runMutation" in ctx)) {
+ throw new Error("ctx is not a mutation ctx");
+ }
+ if (!data.where.length) {
+ return null;
+ }
+ const onUpdateHandle =
+ config.authFunctions?.onUpdate &&
+ config.triggers?.[data.model]?.onUpdate
+ ? ((await createFunctionHandle(
+ config.authFunctions.onUpdate
+ )) as FunctionHandle<"mutation">)
+ : undefined;
+ return ctx.runMutation(api.adapter.incrementOne, {
+ input: {
+ model: data.model as TableNames,
+ where: parseWhere(data.where),
+ increment: data.increment,
+ set: data.set as any,
+ },
+ onUpdateHandle,
+ });
+ },
delete: async (data) => {
if (!("runMutation" in ctx)) {
throw new Error("ctx is not a mutation ctx");
@@ -429,7 +483,7 @@ export const convexAdapter = <
onDeleteHandle: onDeleteHandle,
});
},
- deleteMany: async (data) => {
+ deleteMany: async ({ modelKey: _modelKey, ...data }) => {
if (!("runMutation" in ctx)) {
throw new Error("ctx is not a mutation ctx");
}
@@ -469,7 +523,7 @@ export const convexAdapter = <
});
return result.count;
},
- updateMany: async (data) => {
+ updateMany: async ({ modelKey: _modelKey, ...data }) => {
if (!("runMutation" in ctx)) {
throw new Error("ctx is not a mutation ctx");
}
diff --git a/src/client/create-api.test.ts b/src/client/create-api.test.ts
new file mode 100644
index 00000000..e03370c8
--- /dev/null
+++ b/src/client/create-api.test.ts
@@ -0,0 +1,29 @@
+import { describe, expect, it } from "vitest";
+import type { BetterAuthDBSchema } from "better-auth/db";
+import { assertRequiredFields } from "./create-api.js";
+
+describe("assertRequiredFields", () => {
+ it("resolves renamed models and stored field names", () => {
+ const schema = {
+ account: {
+ modelName: "customAccount",
+ fields: {
+ issuer: {
+ type: "string",
+ required: true,
+ fieldName: "customIssuer",
+ },
+ },
+ },
+ } satisfies BetterAuthDBSchema;
+
+ expect(() => assertRequiredFields(schema, "customAccount", {})).toThrow(
+ "Missing required field customAccount.customIssuer"
+ );
+ expect(() =>
+ assertRequiredFields(schema, "customAccount", {
+ customIssuer: "https://issuer.example.com",
+ })
+ ).not.toThrow();
+ });
+});
diff --git a/src/client/create-api.ts b/src/client/create-api.ts
index 97679db6..a1083dd6 100644
--- a/src/client/create-api.ts
+++ b/src/client/create-api.ts
@@ -3,7 +3,12 @@ import {
paginationOptsValidator,
queryGeneric,
} from "convex/server";
-import type { FunctionHandle, SchemaDefinition } from "convex/server";
+import type {
+ FunctionHandle,
+ GenericDataModel,
+ GenericMutationCtx,
+ SchemaDefinition,
+} from "convex/server";
import { v } from "convex/values";
import type { GenericId } from "convex/values";
import { asyncMap } from "convex-helpers";
@@ -15,8 +20,10 @@ import {
listOne,
paginate,
selectFields,
+ touchesUniqueFields,
} from "./adapter-utils.js";
import { getAuthTables } from "better-auth/db";
+import type { BetterAuthDBSchema } from "better-auth/db";
import type { TableNames } from "../component/_generated/dataModel.js";
import type { BetterAuthOptions } from "better-auth/minimal";
@@ -58,11 +65,122 @@ const whereValidator = (
mode: v.optional(v.union(v.literal("sensitive"), v.literal("insensitive"))),
});
+const requiredFieldNames = (
+ betterAuthSchema: BetterAuthDBSchema,
+ model: string
+) => {
+ const table = Object.values(betterAuthSchema).find(
+ (value) => value.modelName === model
+ );
+ if (!table) {
+ return [];
+ }
+ return Object.entries(table.fields)
+ .filter(([, field]) => field.required)
+ .map(([fieldName, field]) => field.fieldName ?? fieldName);
+};
+
+export const assertRequiredFields = (
+ betterAuthSchema: BetterAuthDBSchema,
+ model: string,
+ data: Record
+) => {
+ for (const fieldName of requiredFieldNames(betterAuthSchema, model)) {
+ if (data[fieldName] == null) {
+ throw new Error(`Missing required field ${model}.${fieldName}`);
+ }
+ }
+};
+
+const assertRequiredUpdateFields = (
+ betterAuthSchema: BetterAuthDBSchema,
+ model: string,
+ update: Record
+) => {
+ for (const fieldName of requiredFieldNames(betterAuthSchema, model)) {
+ if (
+ Object.prototype.hasOwnProperty.call(update, fieldName) &&
+ update[fieldName] == null
+ ) {
+ throw new Error(`Cannot clear required field ${model}.${fieldName}`);
+ }
+ }
+};
+
+const assertRequiredFieldTransitions = (
+ betterAuthSchema: BetterAuthDBSchema,
+ model: string,
+ before: Record,
+ after: Record
+) => {
+ for (const fieldName of requiredFieldNames(betterAuthSchema, model)) {
+ if (
+ after[fieldName] == null &&
+ (before[fieldName] != null ||
+ (!Object.prototype.hasOwnProperty.call(before, fieldName) &&
+ Object.prototype.hasOwnProperty.call(after, fieldName)))
+ ) {
+ throw new Error(`Trigger cleared required field ${model}.${fieldName}`);
+ }
+ }
+};
+
export const createApi = >(
schema: Schema,
createAuthOptions: (ctx: any) => BetterAuthOptions
) => {
const betterAuthSchema = getAuthTables(createAuthOptions({} as any));
+ const checkPersistedUniqueFields = async (
+ ctx: GenericMutationCtx,
+ model: string,
+ doc: Record
+ ) => {
+ await checkUniqueFields(ctx, schema, betterAuthSchema, model, doc, doc);
+ };
+ const applySingleUpdate = async >(
+ ctx: GenericMutationCtx,
+ model: TableNames,
+ doc: Doc,
+ update: Record,
+ onUpdateHandle?: string
+ ): Promise => {
+ assertRequiredUpdateFields(betterAuthSchema, model, update);
+ await checkUniqueFields(ctx, schema, betterAuthSchema, model, update, doc);
+ await ctx.db.patch(model, doc._id as GenericId, update as any);
+ const updatedDoc = await ctx.db.get(
+ model,
+ doc._id as GenericId
+ );
+ if (!updatedDoc) {
+ throw new Error(`Failed to update ${model}`);
+ }
+ if (!onUpdateHandle) {
+ return updatedDoc as Doc;
+ }
+ await ctx.runMutation(onUpdateHandle as FunctionHandle<"mutation">, {
+ model,
+ newDoc: updatedDoc,
+ oldDoc: doc,
+ });
+ const triggeredDoc = await ctx.db.get(
+ model,
+ doc._id as GenericId
+ );
+ if (!triggeredDoc) {
+ throw new Error(
+ `Failed to update ${model} (deleted by onUpdate trigger?)`
+ );
+ }
+ assertRequiredFieldTransitions(
+ betterAuthSchema,
+ model,
+ updatedDoc,
+ triggeredDoc
+ );
+ await checkPersistedUniqueFields(ctx, model, triggeredDoc);
+ return triggeredDoc as Doc;
+ };
+
return {
create: mutationGeneric({
args: {
@@ -78,6 +196,11 @@ export const createApi = >(
onCreateHandle: v.optional(v.string()),
},
handler: async (ctx, args) => {
+ assertRequiredFields(
+ betterAuthSchema,
+ args.input.model,
+ args.input.data
+ );
await checkUniqueFields(
ctx,
schema,
@@ -108,6 +231,8 @@ export const createApi = >(
`Failed to create ${args.input.model} (deleted by onCreate trigger?)`
);
}
+ assertRequiredFields(betterAuthSchema, args.input.model, updatedDoc);
+ await checkPersistedUniqueFields(ctx, args.input.model, updatedDoc);
return selectFields(updatedDoc, args.select);
}
return result;
@@ -168,49 +293,64 @@ export const createApi = >(
handler: async (ctx, args) => {
const doc = await listOne(ctx, schema, betterAuthSchema, args.input);
if (!doc) {
- throw new Error(`Failed to update ${args.input.model}`);
+ return null;
}
- await checkUniqueFields(
+ return await applySingleUpdate(
ctx,
- schema,
- betterAuthSchema,
args.input.model,
+ doc,
args.input.update,
- doc
- );
- await ctx.db.patch(
- args.input.model,
- doc._id as GenericId,
- args.input.update as any
- );
- const updatedDoc = await ctx.db.get(
- args.input.model,
- doc._id as GenericId
+ args.onUpdateHandle
);
- if (!updatedDoc) {
- throw new Error(`Failed to update ${args.input.model}`);
- }
- if (args.onUpdateHandle) {
- await ctx.runMutation(
- args.onUpdateHandle as FunctionHandle<"mutation">,
- {
- model: args.input.model,
- newDoc: updatedDoc,
- oldDoc: doc,
+ },
+ }),
+ incrementOne: mutationGeneric({
+ args: {
+ input: v.union(
+ ...Object.entries(schema.tables).map(
+ ([name, table]: [string, Schema["tables"][string]]) => {
+ const tableName = name as TableNames;
+ return v.object({
+ model: v.literal(tableName),
+ where: v.array(whereValidator(schema, tableName)),
+ increment: v.record(v.string(), v.number()),
+ set: v.optional(v.object(partial(table.validator.fields))),
+ });
}
- );
- const innerUpdatedDoc = await ctx.db.get(
- args.input.model,
- doc._id as GenericId
- );
- if (!innerUpdatedDoc) {
+ )
+ ),
+ onUpdateHandle: v.optional(v.string()),
+ },
+ handler: async (ctx, args) => {
+ if (!args.input.where.length) {
+ return null;
+ }
+ const doc = await listOne(ctx, schema, betterAuthSchema, args.input);
+ if (!doc) {
+ return null;
+ }
+
+ const update: Record = { ...args.input.set };
+ for (const [field, delta] of Object.entries(args.input.increment)) {
+ const current = doc[field];
+ if (
+ current !== undefined &&
+ current !== null &&
+ typeof current !== "number"
+ ) {
throw new Error(
- `Failed to update ${args.input.model} (deleted by onUpdate trigger?)`
+ `Cannot increment non-numeric field ${args.input.model}.${field}`
);
}
- return innerUpdatedDoc;
+ update[field] = (current ?? 0) + delta;
}
- return updatedDoc;
+ return await applySingleUpdate(
+ ctx,
+ args.input.model,
+ doc,
+ update,
+ args.onUpdateHandle
+ );
},
}),
updateMany: mutationGeneric({
@@ -232,16 +372,40 @@ export const createApi = >(
onUpdateHandle: v.optional(v.string()),
},
handler: async (ctx, args) => {
+ const uniqueFieldsTouched = touchesUniqueFields(
+ betterAuthSchema,
+ args.input.model,
+ args.input.update ?? {}
+ );
+ const firstInWhere = args.input.where?.find(
+ (where) => where.operator === "in"
+ );
+ const usesAtomicIdSet = firstInWhere?.field === "_id";
+ const requestedPageSize = args.paginationOpts.numItems;
const { page, ...result } = await paginate(
ctx,
schema,
betterAuthSchema,
{
...args.input,
- paginationOpts: args.paginationOpts,
+ paginationOpts: uniqueFieldsTouched
+ ? {
+ ...args.paginationOpts,
+ numItems: requestedPageSize + 1,
+ }
+ : args.paginationOpts,
}
);
if (args.input.update) {
+ if (
+ uniqueFieldsTouched &&
+ !usesAtomicIdSet &&
+ (page.length > requestedPageSize || !result.isDone)
+ ) {
+ throw new Error(
+ `Cannot update unique fields across multiple pages in ${args.input.model}`
+ );
+ }
if (
hasUniqueFields(
betterAuthSchema,
@@ -254,7 +418,12 @@ export const createApi = >(
`Attempted to set unique fields in multiple documents in ${args.input.model} with the same value. Fields: ${Object.keys(args.input.update ?? {}).join(", ")}`
);
}
- await asyncMap(page, async (doc) => {
+ const updateDoc = async (doc: (typeof page)[number]) => {
+ assertRequiredUpdateFields(
+ betterAuthSchema,
+ args.input.model,
+ args.input.update ?? {}
+ );
await checkUniqueFields(
ctx,
schema,
@@ -270,19 +439,50 @@ export const createApi = >(
);
if (args.onUpdateHandle) {
+ const updatedDoc = await ctx.db.get(
+ args.input.model,
+ doc._id as GenericId
+ );
+ if (!updatedDoc) {
+ throw new Error(`Failed to update ${args.input.model}`);
+ }
await ctx.runMutation(
args.onUpdateHandle as FunctionHandle<"mutation">,
{
model: args.input.model,
- newDoc: await ctx.db.get(
- args.input.model,
- doc._id as GenericId
- ),
+ newDoc: updatedDoc,
oldDoc: doc,
}
);
+ const triggeredDoc = await ctx.db.get(
+ args.input.model,
+ doc._id as GenericId
+ );
+ if (!triggeredDoc) {
+ throw new Error(
+ `Failed to update ${args.input.model} (deleted by onUpdate trigger?)`
+ );
+ }
+ assertRequiredFieldTransitions(
+ betterAuthSchema,
+ args.input.model,
+ updatedDoc,
+ triggeredDoc
+ );
+ await checkPersistedUniqueFields(
+ ctx,
+ args.input.model,
+ triggeredDoc
+ );
}
- });
+ };
+ if (uniqueFieldsTouched) {
+ for (const doc of page) {
+ await updateDoc(doc);
+ }
+ } else {
+ await asyncMap(page, updateDoc);
+ }
}
return {
...result,
diff --git a/src/client/create-client.test.ts b/src/client/create-client.test.ts
index 548bdf43..6d2bbc9c 100644
--- a/src/client/create-client.test.ts
+++ b/src/client/create-client.test.ts
@@ -1,5 +1,5 @@
-import { describe, expect, it, vi } from "vitest";
-import { httpRouter } from "convex/server";
+import { afterEach, describe, expect, it, vi } from "vitest";
+import { httpActionGeneric, httpRouter } from "convex/server";
import { createClient } from "./create-client.js";
const component = {
@@ -29,6 +29,92 @@ const getRouteHandler = (
};
describe("createClient route registration", () => {
+ afterEach(() => vi.unstubAllEnvs());
+
+ describe.each(["registerRoutes", "registerRoutesLazy"] as const)(
+ "%s discovery",
+ (register) => {
+ it.each([
+ [undefined, false, "/api/auth/convex/.well-known/openid-configuration"],
+ [undefined, true, "/api/auth/.well-known/openid-configuration"],
+ ["/custom/auth", true, "/custom/auth/.well-known/openid-configuration"],
+ [
+ "/custom/auth",
+ false,
+ "/custom/auth/convex/.well-known/openid-configuration",
+ ],
+ ])(
+ "redirects basePath=%s provider=%s",
+ async (basePath, provider, target) => {
+ vi.stubEnv("CONVEX_SITE_URL", "https://deployment.convex.site");
+ const client = createClient(component);
+ const http = httpRouter();
+ const createAuth = vi.fn(() => ({
+ handler: async (request: Request) =>
+ new URL(request.url).pathname === target
+ ? Response.json({ issuer: "https://deployment.convex.site" })
+ : new Response(null, { status: 404 }),
+ options: {
+ basePath,
+ plugins: provider ? [{ id: "oauth-provider" }] : [],
+ },
+ $context: Promise.resolve({ options: {} }),
+ }));
+ if (register === "registerRoutesLazy") {
+ client.registerRoutesLazy(http, createAuth, { basePath });
+ expect(createAuth).not.toHaveBeenCalled();
+ } else {
+ client.registerRoutes(http, createAuth);
+ }
+ const root =
+ "https://deployment.convex.site/.well-known/openid-configuration";
+ const redirect = await getRouteHandler(
+ http,
+ new URL(root).pathname,
+ "GET"
+ )!._handler({}, new Request(root));
+ expect(redirect.status).toBe(302);
+ const location = redirect.headers.get("location")!;
+ expect(location).toBe(`https://deployment.convex.site${target}`);
+ const response = await getRouteHandler(
+ http,
+ new URL(location).pathname,
+ "GET"
+ )!._handler({}, new Request(location));
+ expect(response.status).toBe(200);
+ }
+ );
+
+ it("preserves an application-defined discovery route", async () => {
+ const client = createClient(component);
+ const http = httpRouter();
+ http.route({
+ path: "/.well-known/openid-configuration",
+ method: "GET",
+ handler: httpActionGeneric(async () =>
+ Response.json({ custom: true })
+ ),
+ });
+ const createAuth = vi.fn(() => ({
+ handler: async () => new Response(null, { status: 404 }),
+ options: { plugins: [{ id: "oauth-provider" }] },
+ $context: Promise.resolve({ options: {} }),
+ }));
+ client[register](http, createAuth);
+ const response = await getRouteHandler(
+ http,
+ "/.well-known/openid-configuration",
+ "GET"
+ )!._handler(
+ {},
+ new Request(
+ "https://deployment.convex.site/.well-known/openid-configuration"
+ )
+ );
+ expect(await response.json()).toEqual({ custom: true });
+ });
+ }
+ );
it("registerRoutes eagerly initializes auth and infers basePath", () => {
const client = createClient(component);
const http = httpRouter();
@@ -145,7 +231,9 @@ describe("createClient route registration", () => {
const forwardedRequest = handler.mock.calls[0]?.[0];
expect(forwardedRequest).toBeInstanceOf(Request);
- expect(forwardedRequest.headers.get("x-forwarded-host")).toBe("app.example.com");
+ expect(forwardedRequest.headers.get("x-forwarded-host")).toBe(
+ "app.example.com"
+ );
expect(forwardedRequest.headers.get("x-forwarded-proto")).toBe("https");
});
diff --git a/src/client/create-client.ts b/src/client/create-client.ts
index 9b4c3f77..561b3a86 100644
--- a/src/client/create-client.ts
+++ b/src/client/create-client.ts
@@ -20,6 +20,12 @@ import type defaultSchema from "../component/schema.js";
import type { CreateAuth, GenericCtx } from "./index.js";
import type { TrustedOriginsOption } from "../utils/index.js";
+const getOpenIdConfigPath = (plugins: unknown) =>
+ Array.isArray(plugins) &&
+ plugins.some((plugin) => plugin?.id === "oauth-provider")
+ ? "/.well-known/openid-configuration"
+ : "/convex/.well-known/openid-configuration";
+
export type AuthFunctions = {
onCreate?: FunctionReference<"mutation", "internal", { [key: string]: any }>;
onUpdate?: FunctionReference<"mutation", "internal", { [key: string]: any }>;
@@ -65,6 +71,7 @@ type SlimComponentApi = {
findOne: FunctionReference<"query", "internal">;
findMany: FunctionReference<"query", "internal">;
updateOne: FunctionReference<"mutation", "internal">;
+ incrementOne: FunctionReference<"mutation", "internal">;
updateMany: FunctionReference<"mutation", "internal">;
deleteOne: FunctionReference<"mutation", "internal">;
deleteMany: FunctionReference<"mutation", "internal">;
@@ -411,7 +418,7 @@ export const createClient = <
path: "/.well-known/openid-configuration",
method: "GET",
handler: httpActionGeneric(async () => {
- const url = `${process.env.CONVEX_SITE_URL}${path}/convex/.well-known/openid-configuration`;
+ const url = `${process.env.CONVEX_SITE_URL}${path}${getOpenIdConfigPath(staticAuth.options.plugins)}`;
return Response.redirect(url);
}),
});
@@ -530,8 +537,9 @@ export const createClient = <
http.route({
path: "/.well-known/openid-configuration",
method: "GET",
- handler: httpActionGeneric(async () => {
- const url = `${process.env.CONVEX_SITE_URL}${path}/convex/.well-known/openid-configuration`;
+ handler: httpActionGeneric(async (ctx) => {
+ const auth = createAuth(ctx as any);
+ const url = `${process.env.CONVEX_SITE_URL}${path}${getOpenIdConfigPath(auth.options.plugins)}`;
return Response.redirect(url);
}),
});
diff --git a/src/client/create-schema.test.ts b/src/client/create-schema.test.ts
new file mode 100644
index 00000000..8c7e9027
--- /dev/null
+++ b/src/client/create-schema.test.ts
@@ -0,0 +1,92 @@
+import { describe, expect, it } from "vitest";
+import type { BetterAuthDBSchema } from "better-auth/db";
+import { createSchema } from "./create-schema.js";
+
+describe("createSchema", () => {
+ it("generates the Better Auth credential account lookup index", async () => {
+ const tables = {
+ account: {
+ modelName: "account",
+ fields: {
+ userId: {
+ type: "string",
+ required: true,
+ references: { field: "id", model: "user" },
+ },
+ providerId: { type: "string", required: true },
+ accountId: { type: "string", required: true },
+ },
+ },
+ } satisfies BetterAuthDBSchema;
+
+ const { code } = await createSchema({ tables });
+
+ expect(code).toContain(
+ '.index("userId_providerId_accountId", ["userId","providerId","accountId"])'
+ );
+ expect(code).toContain('.index("userId", ["userId"])');
+ });
+
+ it("generates table-level compound indexes without mutating their order", async () => {
+ const indexFields = ["issuer", "accountId"] as [string, ...string[]];
+ const tables = {
+ account: {
+ modelName: "account",
+ fields: {
+ issuer: { type: "string", required: true },
+ accountId: { type: "string", required: true },
+ },
+ indexes: [{ fields: indexFields, unique: true }],
+ },
+ } satisfies BetterAuthDBSchema;
+
+ const { code } = await createSchema({ tables });
+
+ expect(code).toContain(
+ '.index("issuer_accountId", ["issuer","accountId"])'
+ );
+ expect(indexFields).toEqual(["issuer", "accountId"]);
+ });
+
+ it("does not add a standalone index covered by a compound prefix", async () => {
+ const tables = {
+ user: {
+ modelName: "user",
+ fields: {
+ email: { type: "string", required: true, unique: true },
+ name: { type: "string", required: true },
+ },
+ },
+ } satisfies BetterAuthDBSchema;
+
+ const { code } = await createSchema({ tables });
+
+ expect(code).toContain('.index("email_name", ["email","name"])');
+ expect(code).not.toContain('.index("email", ["email"])');
+ });
+
+ it("keeps reversed compound indexes distinct", async () => {
+ const tables = {
+ account: {
+ modelName: "account",
+ fields: {
+ issuer: { type: "string", required: true },
+ accountId: { type: "string", required: true },
+ },
+ indexes: [
+ { fields: ["issuer", "accountId"], unique: true },
+ { fields: ["accountId", "issuer"], unique: false },
+ ],
+ },
+ } satisfies BetterAuthDBSchema;
+
+ const { code } = await createSchema({ tables });
+
+ expect(code).toContain(
+ '.index("issuer_accountId", ["issuer","accountId"])'
+ );
+ expect(code).toContain(
+ '.index("accountId_issuer", ["accountId","issuer"])'
+ );
+ });
+});
diff --git a/src/client/create-schema.ts b/src/client/create-schema.ts
index 464274c0..2fcf2fee 100644
--- a/src/client/create-schema.ts
+++ b/src/client/create-schema.ts
@@ -16,7 +16,13 @@ const resolveCdTarget = (
// Manually add fields to index on for schema generation,
// all fields in the schema specialFields are automatically indexed
export const indexFields = {
- account: ["accountId", ["accountId", "providerId"], ["providerId", "userId"]],
+ account: [
+ "accountId",
+ ["accountId", "providerId"],
+ ["providerId", "userId"],
+ ["userId", "providerId", "accountId"],
+ "userId",
+ ],
rateLimit: ["key"],
session: ["expiresAt", ["expiresAt", "userId"]],
verification: ["expiresAt", "identifier"],
@@ -53,10 +59,14 @@ const specialFields = (tables: BetterAuthDBSchema) =>
const mergedIndexFields = (tables: BetterAuthDBSchema) =>
Object.fromEntries(
Object.entries(tables).map(([key, table]) => {
- const manualIndexes =
+ const schemaIndexes =
+ table.indexes?.map((index) =>
+ index.fields.map((field) => table.fields[field]?.fieldName ?? field)
+ ) ?? [];
+ const manualIndexes: string[][] =
indexFields[key as keyof typeof indexFields]?.map((index) => {
return typeof index === "string"
- ? (table.fields[index]?.fieldName ?? index)
+ ? [table.fields[index]?.fieldName ?? index]
: index.map((i) => table.fields[i]?.fieldName ?? i);
}) || [];
const specialFieldIndexes = Object.keys(
@@ -64,11 +74,25 @@ const mergedIndexFields = (tables: BetterAuthDBSchema) =>
{}
).filter(
(index) =>
- !manualIndexes.some((m) =>
- Array.isArray(m) ? m[0] === index : m === index
- )
+ !manualIndexes.some((manualIndex) => manualIndex[0] === index)
);
- return [key, manualIndexes.concat(specialFieldIndexes)];
+ const indexes = schemaIndexes.concat(
+ manualIndexes,
+ specialFieldIndexes.map((field) => [field])
+ );
+ const deduplicated = indexes.filter((index, indexPosition) => {
+ return (
+ indexes.findIndex((candidate) => {
+ return (
+ candidate.length === index.length &&
+ candidate.every(
+ (field, fieldPosition) => field === index[fieldPosition]
+ )
+ );
+ }) === indexPosition
+ );
+ });
+ return [key, deduplicated];
})
);
@@ -110,7 +134,10 @@ export const createSchema = async ({
projectRoot !== null
? path.relative(projectRoot, cwd).split(path.sep).join("/")
: null;
- const generateCommand = `npx auth generate${file ? ` --output ${file}` : ""}`;
+ const outputPath = file ? path.relative(cwd, file) : file;
+ const generateCommand = `npx auth generate${
+ outputPath ? ` --output ${outputPath}` : ""
+ }`;
const cdTarget = resolveCdTarget(relativeDir, path.basename(cwd));
const commandBlock = cdTarget
? ` * cd ${cdTarget}\n * ${generateCommand}`
@@ -167,7 +194,7 @@ export const tables = {
mergedIndexFields(tables)[
tableKey as keyof typeof mergedIndexFields
]?.map((index) => {
- const indexArray = Array.isArray(index) ? index.sort() : [index];
+ const indexArray = Array.isArray(index) ? index : [index];
const indexName = indexArray.join("_");
return `.index("${indexName}", ${JSON.stringify(indexArray)})`;
}) || [];
diff --git a/src/client/required-fields.test.ts b/src/client/required-fields.test.ts
new file mode 100644
index 00000000..be2f8ebf
--- /dev/null
+++ b/src/client/required-fields.test.ts
@@ -0,0 +1,293 @@
+///
+
+import { describe, expect, it } from "vitest";
+import { convexTest } from "convex-test";
+import { createFunctionHandle } from "convex/server";
+import { constraintsProfileOptions } from "../component/testProfiles/auth-options.constraints.js";
+import schema from "../component/testProfiles/schema.constraints.js";
+import { api, internal } from "../component/_generated/api.js";
+import type { DataModel } from "../component/_generated/dataModel.js";
+import { createClient } from "./index.js";
+
+const accountData = () => ({
+ issuer: "local:credential",
+ accountId: "user-1",
+ providerId: "credential",
+ userId: "user-1",
+ password: "password-hash",
+ createdAt: Date.now(),
+ updatedAt: Date.now(),
+});
+
+const createAccount = async (
+ t: ReturnType,
+ onCreateHandle?: string
+) =>
+ await t.run(async (ctx) =>
+ ctx.runMutation(api.testProfiles.adapterConstraints.create, {
+ input: { model: "account", data: accountData() },
+ onCreateHandle,
+ })
+ );
+
+const insertLegacyAccount = async (
+ t: ReturnType,
+ suffix: string
+) =>
+ await t.run((ctx) =>
+ ctx.db.insert("account", {
+ accountId: `legacy-account-${suffix}`,
+ providerId: "credential",
+ userId: `legacy-user-${suffix}`,
+ password: "password-hash",
+ createdAt: Date.now(),
+ updatedAt: 100,
+ })
+ );
+
+describe("required fields", () => {
+ it.each(["updateOne", "incrementOne", "updateMany"] as const)(
+ "rejects null issuer through %s",
+ async (operation) => {
+ const t = convexTest(schema, import.meta.glob("../component/**/*.*s"));
+ await createAccount(t);
+
+ const mutation = t.run(async (ctx) => {
+ if (operation === "updateOne") {
+ return await ctx.runMutation(
+ api.testProfiles.adapterConstraints.updateOne,
+ {
+ input: {
+ model: "account",
+ update: { issuer: null },
+ where: [
+ { field: "accountId", operator: "eq", value: "user-1" },
+ ],
+ },
+ }
+ );
+ }
+ if (operation === "incrementOne") {
+ return await ctx.runMutation(
+ api.testProfiles.adapterConstraints.incrementOne,
+ {
+ input: {
+ model: "account",
+ increment: { updatedAt: 1 },
+ set: { issuer: null },
+ where: [
+ { field: "accountId", operator: "eq", value: "user-1" },
+ ],
+ },
+ }
+ );
+ }
+ return await ctx.runMutation(
+ api.testProfiles.adapterConstraints.updateMany,
+ {
+ input: {
+ model: "account",
+ update: { issuer: null },
+ where: [{ field: "accountId", operator: "eq", value: "user-1" }],
+ },
+ paginationOpts: { cursor: null, numItems: 10 },
+ }
+ );
+ });
+
+ await expect(mutation).rejects.toThrow(
+ "Cannot clear required field account.issuer"
+ );
+ const accounts = await t.run((ctx) => ctx.db.query("account").collect());
+ expect(accounts).toHaveLength(1);
+ expect(accounts[0]?.issuer).toBe("local:credential");
+ }
+ );
+
+ it.each(["updateOne", "incrementOne", "updateMany"] as const)(
+ "allows an unrelated %s on a legacy account",
+ async (operation) => {
+ const t = convexTest(schema, import.meta.glob("../component/**/*.*s"));
+ await insertLegacyAccount(t, operation);
+
+ await t.run(async (ctx) => {
+ const where = [
+ {
+ field: "accountId",
+ operator: "eq" as const,
+ value: `legacy-account-${operation}`,
+ },
+ ];
+ if (operation === "updateOne") {
+ await ctx.runMutation(api.testProfiles.adapterConstraints.updateOne, {
+ input: {
+ model: "account",
+ update: { accessToken: "updated-token" },
+ where,
+ },
+ });
+ return;
+ }
+ if (operation === "incrementOne") {
+ await ctx.runMutation(
+ api.testProfiles.adapterConstraints.incrementOne,
+ {
+ input: {
+ model: "account",
+ increment: { updatedAt: 1 },
+ where,
+ },
+ }
+ );
+ return;
+ }
+ await ctx.runMutation(api.testProfiles.adapterConstraints.updateMany, {
+ input: {
+ model: "account",
+ update: { scope: "updated-scope" },
+ where,
+ },
+ paginationOpts: { cursor: null, numItems: 10 },
+ });
+ });
+
+ const accounts = await t.run((ctx) => ctx.db.query("account").collect());
+ expect(accounts).toHaveLength(1);
+ expect(accounts[0]?.issuer).toBeUndefined();
+ if (operation === "updateOne") {
+ expect(accounts[0]?.accessToken).toBe("updated-token");
+ } else if (operation === "incrementOne") {
+ expect(accounts[0]?.updatedAt).toBe(101);
+ } else {
+ expect(accounts[0]?.scope).toBe("updated-scope");
+ }
+ }
+ );
+
+ it("updates multiple pages containing a legacy account", async () => {
+ const t = convexTest(schema, import.meta.glob("../component/**/*.*s"));
+ await t.run(async (ctx) => {
+ for (let i = 0; i < 401; i += 1) {
+ await ctx.db.insert("account", {
+ ...(i === 250 ? {} : { issuer: `local:legacy-${i}` }),
+ accountId: `legacy-bulk-account-${i}`,
+ providerId: "legacy-bulk-provider",
+ userId: `legacy-bulk-user-${i.toString().padStart(3, "0")}`,
+ createdAt: Date.now(),
+ updatedAt: Date.now(),
+ });
+ }
+ });
+ const adapter = createClient(
+ { adapter: api.testProfiles.adapterConstraints } as any,
+ {
+ verbose: false,
+ }
+ ).adapter({
+ runMutation: t.mutation.bind(t),
+ runQuery: t.query.bind(t),
+ } as any)(constraintsProfileOptions);
+
+ await expect(
+ adapter.updateMany({
+ model: "account",
+ where: [{ field: "providerId", value: "legacy-bulk-provider" }],
+ update: { scope: "updated-scope" },
+ })
+ ).resolves.toBe(401);
+
+ const accounts = await t.run((ctx) => ctx.db.query("account").collect());
+ expect(accounts).toHaveLength(401);
+ expect(accounts.every(({ scope }) => scope === "updated-scope")).toBe(true);
+ expect(accounts.filter(({ issuer }) => issuer === undefined)).toHaveLength(
+ 1
+ );
+ });
+
+ it("rolls back an onCreate trigger that clears a required field", async () => {
+ const t = convexTest(schema, import.meta.glob("../component/**/*.*s"));
+
+ await expect(
+ t.run(async (ctx) => {
+ const handle = await createFunctionHandle(
+ internal.testTriggerHandlers.accountOnCreateIssuerClearer
+ );
+ return await ctx.runMutation(
+ api.testProfiles.adapterConstraints.create,
+ {
+ input: { model: "account", data: accountData() },
+ onCreateHandle: handle,
+ }
+ );
+ })
+ ).rejects.toThrow("Missing required field account.issuer");
+ expect(await t.run((ctx) => ctx.db.query("account").collect())).toEqual([]);
+ });
+
+ it("rolls back an onUpdate trigger that clears a required field", async () => {
+ const t = convexTest(schema, import.meta.glob("../component/**/*.*s"));
+ await createAccount(t);
+
+ await expect(
+ t.run(async (ctx) => {
+ const handle = await createFunctionHandle(
+ internal.testTriggerHandlers.accountOnUpdateIssuerClearer
+ );
+ return await ctx.runMutation(
+ api.testProfiles.adapterConstraints.updateOne,
+ {
+ input: {
+ model: "account",
+ update: { updatedAt: Date.now() + 1 },
+ where: [{ field: "accountId", operator: "eq", value: "user-1" }],
+ },
+ onUpdateHandle: handle,
+ }
+ );
+ })
+ ).rejects.toThrow("Trigger cleared required field account.issuer");
+
+ const accounts = await t.run((ctx) => ctx.db.query("account").collect());
+ expect(accounts).toHaveLength(1);
+ expect(accounts[0]?.issuer).toBe("local:credential");
+ });
+
+ it("allows a no-op trigger on a legacy account with a null issuer", async () => {
+ const t = convexTest(schema, import.meta.glob("../component/**/*.*s"));
+ await t.run((ctx) =>
+ ctx.db.insert("account", {
+ issuer: null,
+ accountId: "legacy-null-account",
+ providerId: "credential",
+ userId: "legacy-null-user",
+ createdAt: Date.now(),
+ updatedAt: Date.now(),
+ })
+ );
+
+ await t.run(async (ctx) => {
+ const handle = await createFunctionHandle(
+ internal.testTriggerHandlers.accountOnUpdateNoop
+ );
+ await ctx.runMutation(api.testProfiles.adapterConstraints.updateOne, {
+ input: {
+ model: "account",
+ update: { scope: "updated-scope" },
+ where: [
+ {
+ field: "accountId",
+ operator: "eq",
+ value: "legacy-null-account",
+ },
+ ],
+ },
+ onUpdateHandle: handle,
+ });
+ });
+
+ const accounts = await t.run((ctx) => ctx.db.query("account").collect());
+ expect(accounts).toHaveLength(1);
+ expect(accounts[0]?.issuer).toBeNull();
+ expect(accounts[0]?.scope).toBe("updated-scope");
+ });
+});
diff --git a/src/client/triggers.test.ts b/src/client/triggers.test.ts
index 73ab6e68..6b668e0f 100644
--- a/src/client/triggers.test.ts
+++ b/src/client/triggers.test.ts
@@ -3,7 +3,7 @@
import { describe, expect, it } from "vitest";
import { convexTest } from "convex-test";
import { createFunctionHandle } from "convex/server";
-import schema from "../component/schema.js";
+import schema from "../component/testProfiles/schema.constraints.js";
import { api, internal } from "../component/_generated/api.js";
const baseSessionData = () => ({
@@ -14,14 +14,33 @@ const baseSessionData = () => ({
userAgent: "original",
});
+const accountData = ({
+ issuer,
+ accountId,
+ providerId,
+ userId,
+}: {
+ issuer: string;
+ accountId: string;
+ providerId: string;
+ userId: string;
+}) => ({
+ issuer,
+ accountId,
+ providerId,
+ userId,
+ createdAt: Date.now(),
+ updatedAt: Date.now(),
+});
+
describe("trigger result propagation", () => {
- it("api.adapter.create returns the doc reflecting onCreateHandle writes", async () => {
+ it("api.testProfiles.adapterConstraints.create returns the doc reflecting onCreateHandle writes", async () => {
const t = convexTest(schema, import.meta.glob("../component/**/*.*s"));
const result = await t.run(async (ctx) => {
const handle = await createFunctionHandle(
internal.testTriggerHandlers.sessionOnCreateUpdater
);
- return await ctx.runMutation(api.adapter.create, {
+ return await ctx.runMutation(api.testProfiles.adapterConstraints.create, {
input: {
model: "session",
data: { ...baseSessionData(), token: "create-token-1" },
@@ -32,10 +51,10 @@ describe("trigger result propagation", () => {
expect(result.userAgent).toBe("trigger-ran-on-create");
});
- it("api.adapter.updateOne returns the doc reflecting onUpdateHandle writes", async () => {
+ it("api.testProfiles.adapterConstraints.updateOne returns the doc reflecting onUpdateHandle writes", async () => {
const t = convexTest(schema, import.meta.glob("../component/**/*.*s"));
const result = await t.run(async (ctx) => {
- await ctx.runMutation(api.adapter.create, {
+ await ctx.runMutation(api.testProfiles.adapterConstraints.create, {
input: {
model: "session",
data: { ...baseSessionData(), token: "update-token-1" },
@@ -44,15 +63,278 @@ describe("trigger result propagation", () => {
const handle = await createFunctionHandle(
internal.testTriggerHandlers.sessionOnUpdateUpdater
);
- return await ctx.runMutation(api.adapter.updateOne, {
+ return await ctx.runMutation(
+ api.testProfiles.adapterConstraints.updateOne,
+ {
+ input: {
+ model: "session",
+ update: { userAgent: "set-by-update" },
+ where: [
+ { field: "token", operator: "eq", value: "update-token-1" },
+ ],
+ },
+ onUpdateHandle: handle,
+ }
+ );
+ });
+ expect(result).toMatchObject({ userAgent: "trigger-ran-on-update" });
+ });
+
+ it("api.testProfiles.adapterConstraints.incrementOne returns the doc reflecting onUpdateHandle writes", async () => {
+ const t = convexTest(schema, import.meta.glob("../component/**/*.*s"));
+ const result = await t.run(async (ctx) => {
+ await ctx.runMutation(api.testProfiles.adapterConstraints.create, {
input: {
model: "session",
- update: { userAgent: "set-by-update" },
- where: [{ field: "token", operator: "eq", value: "update-token-1" }],
+ data: {
+ ...baseSessionData(),
+ expiresAt: 100,
+ token: "increment-token-1",
+ },
},
- onUpdateHandle: handle,
});
+ const handle = await createFunctionHandle(
+ internal.testTriggerHandlers.sessionOnUpdateUpdater
+ );
+ return await ctx.runMutation(
+ api.testProfiles.adapterConstraints.incrementOne,
+ {
+ input: {
+ model: "session",
+ increment: { expiresAt: 1 },
+ where: [
+ {
+ field: "token",
+ operator: "eq",
+ value: "increment-token-1",
+ },
+ ],
+ },
+ onUpdateHandle: handle,
+ }
+ );
+ });
+ expect(result).toMatchObject({
+ expiresAt: 101,
+ userAgent: "trigger-ran-on-update",
+ });
+ });
+
+ it("api.testProfiles.adapterConstraints.updateMany runs onUpdateHandle for every updated doc", async () => {
+ const t = convexTest(schema, import.meta.glob("../component/**/*.*s"));
+ const result = await t.run(async (ctx) => {
+ for (const token of ["bulk-update-token-1", "bulk-update-token-2"]) {
+ await ctx.runMutation(api.testProfiles.adapterConstraints.create, {
+ input: {
+ model: "session",
+ data: { ...baseSessionData(), token },
+ },
+ });
+ }
+ const handle = await createFunctionHandle(
+ internal.testTriggerHandlers.sessionOnUpdateUpdater
+ );
+ return await ctx.runMutation(
+ api.testProfiles.adapterConstraints.updateMany,
+ {
+ input: {
+ model: "session",
+ update: { userAgent: "set-by-update" },
+ where: [{ field: "userId", operator: "eq", value: "user-1" }],
+ },
+ paginationOpts: { cursor: null, numItems: 10 },
+ onUpdateHandle: handle,
+ }
+ );
+ });
+ expect(result.count).toBe(2);
+
+ const sessions = await t.run((ctx) => ctx.db.query("session").collect());
+ expect(sessions).toHaveLength(2);
+ expect(
+ sessions.every(({ userAgent }) => userAgent === "trigger-ran-on-update")
+ ).toBe(true);
+ });
+});
+
+describe("trigger unique constraint enforcement", () => {
+ it("rolls back an onCreate trigger that creates an account identity collision", async () => {
+ const t = convexTest(schema, import.meta.glob("../component/**/*.*s"));
+ await t.run((ctx) =>
+ ctx.runMutation(api.testProfiles.adapterConstraints.create, {
+ input: {
+ model: "account",
+ data: accountData({
+ issuer: "collision-issuer",
+ accountId: "collision-account",
+ providerId: "existing-provider",
+ userId: "existing-user",
+ }),
+ },
+ })
+ );
+
+ await expect(
+ t.run(async (ctx) => {
+ const handle = await createFunctionHandle(
+ internal.testTriggerHandlers.accountOnCreateIssuerCollider
+ );
+ return await ctx.runMutation(
+ api.testProfiles.adapterConstraints.create,
+ {
+ input: {
+ model: "account",
+ data: accountData({
+ issuer: "safe-issuer",
+ accountId: "collision-account",
+ providerId: "candidate-provider",
+ userId: "candidate-user",
+ }),
+ },
+ onCreateHandle: handle,
+ }
+ );
+ })
+ ).rejects.toThrow(
+ "account unique constraint issuer+accountId already exists"
+ );
+
+ const accounts = await t.run((ctx) => ctx.db.query("account").collect());
+ expect(accounts).toHaveLength(1);
+ expect(accounts[0]).toMatchObject({
+ issuer: "collision-issuer",
+ accountId: "collision-account",
+ providerId: "existing-provider",
+ });
+ });
+
+ it("rolls back an onUpdate trigger that creates an account identity collision", async () => {
+ const t = convexTest(schema, import.meta.glob("../component/**/*.*s"));
+ await t.run(async (ctx) => {
+ for (const data of [
+ accountData({
+ issuer: "collision-issuer",
+ accountId: "collision-account",
+ providerId: "existing-provider",
+ userId: "existing-user",
+ }),
+ accountData({
+ issuer: "collision-issuer",
+ accountId: "safe-account",
+ providerId: "candidate-provider",
+ userId: "candidate-user",
+ }),
+ ]) {
+ await ctx.runMutation(api.testProfiles.adapterConstraints.create, {
+ input: { model: "account", data },
+ });
+ }
+ });
+
+ await expect(
+ t.run(async (ctx) => {
+ const handle = await createFunctionHandle(
+ internal.testTriggerHandlers.accountOnUpdateAccountIdCollider
+ );
+ return await ctx.runMutation(
+ api.testProfiles.adapterConstraints.updateOne,
+ {
+ input: {
+ model: "account",
+ update: { scope: "updated-scope" },
+ where: [
+ {
+ field: "providerId",
+ operator: "eq",
+ value: "candidate-provider",
+ },
+ ],
+ },
+ onUpdateHandle: handle,
+ }
+ );
+ })
+ ).rejects.toThrow(
+ "account unique constraint issuer+accountId already exists"
+ );
+
+ const candidate = await t.run((ctx) =>
+ ctx.db
+ .query("account")
+ .withIndex("providerId_userId", (q) =>
+ q
+ .eq("providerId", "candidate-provider")
+ .eq("userId", "candidate-user")
+ )
+ .unique()
+ );
+ expect(candidate).toMatchObject({
+ accountId: "safe-account",
+ issuer: "collision-issuer",
+ });
+ expect(candidate?.scope).toBeUndefined();
+ });
+
+ it("rolls back updateMany when a trigger creates an account identity collision", async () => {
+ const t = convexTest(schema, import.meta.glob("../component/**/*.*s"));
+ await t.run(async (ctx) => {
+ for (const data of [
+ accountData({
+ issuer: "collision-issuer",
+ accountId: "collision-account",
+ providerId: "existing-provider",
+ userId: "existing-user",
+ }),
+ accountData({
+ issuer: "collision-issuer",
+ accountId: "safe-account",
+ providerId: "candidate-provider",
+ userId: "candidate-user",
+ }),
+ ]) {
+ await ctx.runMutation(api.testProfiles.adapterConstraints.create, {
+ input: { model: "account", data },
+ });
+ }
+ });
+
+ await expect(
+ t.run(async (ctx) => {
+ const handle = await createFunctionHandle(
+ internal.testTriggerHandlers.accountOnUpdateAccountIdCollider
+ );
+ return await ctx.runMutation(
+ api.testProfiles.adapterConstraints.updateMany,
+ {
+ input: {
+ model: "account",
+ update: { scope: "updated-scope" },
+ where: [
+ {
+ field: "providerId",
+ operator: "eq",
+ value: "candidate-provider",
+ },
+ ],
+ },
+ paginationOpts: { cursor: null, numItems: 10 },
+ onUpdateHandle: handle,
+ }
+ );
+ })
+ ).rejects.toThrow(
+ "account unique constraint issuer+accountId already exists"
+ );
+
+ const accounts = await t.run((ctx) => ctx.db.query("account").collect());
+ expect(accounts).toHaveLength(2);
+ const candidate = accounts.find(
+ ({ providerId }) => providerId === "candidate-provider"
+ );
+ expect(candidate).toMatchObject({
+ accountId: "safe-account",
+ issuer: "collision-issuer",
});
- expect(result.userAgent).toBe("trigger-ran-on-update");
+ expect(candidate?.scope).toBeUndefined();
});
});
diff --git a/src/client/update-many-pagination.test.ts b/src/client/update-many-pagination.test.ts
new file mode 100644
index 00000000..0dc58231
--- /dev/null
+++ b/src/client/update-many-pagination.test.ts
@@ -0,0 +1,55 @@
+///
+
+import { describe, expect, it } from "vitest";
+import { convexTest } from "convex-test";
+import schema from "../component/testProfiles/schema.constraints.js";
+import { api } from "../component/_generated/api.js";
+
+describe("updateMany pagination", () => {
+ it("does not treat a later ID filter as the atomic ID-set path", async () => {
+ const t = convexTest(schema, import.meta.glob("../component/**/*.*s"));
+ const ids = await t.run(async (ctx) => {
+ const insertedIds = [];
+ for (let i = 0; i < 300; i += 1) {
+ insertedIds.push(
+ await ctx.db.insert("account", {
+ issuer: `original-${i}`,
+ accountId: `mixed-filter-account-${i}`,
+ providerId: "mixed-filter-provider",
+ userId: `mixed-filter-user-${i}`,
+ scope: "mixed-filter-scope",
+ createdAt: Date.now(),
+ updatedAt: Date.now(),
+ })
+ );
+ }
+ return insertedIds;
+ });
+
+ await expect(
+ t.mutation(api.testProfiles.adapterConstraints.updateMany, {
+ input: {
+ model: "account",
+ update: { issuer: "updated-issuer" },
+ where: [
+ {
+ field: "scope",
+ operator: "in",
+ value: ["mixed-filter-scope"],
+ },
+ { field: "_id", operator: "in", value: ids },
+ ],
+ },
+ paginationOpts: { cursor: null, numItems: 200 },
+ })
+ ).rejects.toThrow(
+ "Cannot update unique fields across multiple pages in account"
+ );
+
+ const accounts = await t.run((ctx) => ctx.db.query("account").collect());
+ expect(accounts).toHaveLength(300);
+ expect(
+ accounts.every(({ issuer }) => issuer?.startsWith("original-") === true)
+ ).toBe(true);
+ });
+});
diff --git a/src/component/_generated/api.ts b/src/component/_generated/api.ts
index 72723865..c0e1b2fc 100644
--- a/src/component/_generated/api.ts
+++ b/src/component/_generated/api.ts
@@ -9,7 +9,9 @@
*/
import type * as adapter from "../adapter.js";
+import type * as migrations from "../migrations.js";
import type * as testProfiles_adapterAdditionalFields from "../testProfiles/adapterAdditionalFields.js";
+import type * as testProfiles_adapterConstraints from "../testProfiles/adapterConstraints.js";
import type * as testProfiles_adapterOrganizationJoins from "../testProfiles/adapterOrganizationJoins.js";
import type * as testProfiles_adapterPluginTable from "../testProfiles/adapterPluginTable.js";
import type * as testProfiles_adapterRenameField from "../testProfiles/adapterRenameField.js";
@@ -26,7 +28,9 @@ import { anyApi, componentsGeneric } from "convex/server";
const fullApi: ApiFromModules<{
adapter: typeof adapter;
+ migrations: typeof migrations;
"testProfiles/adapterAdditionalFields": typeof testProfiles_adapterAdditionalFields;
+ "testProfiles/adapterConstraints": typeof testProfiles_adapterConstraints;
"testProfiles/adapterOrganizationJoins": typeof testProfiles_adapterOrganizationJoins;
"testProfiles/adapterPluginTable": typeof testProfiles_adapterPluginTable;
"testProfiles/adapterRenameField": typeof testProfiles_adapterRenameField;
diff --git a/src/component/_generated/component.ts b/src/component/_generated/component.ts
index b7e27676..e4dd6382 100644
--- a/src/component/_generated/component.ts
+++ b/src/component/_generated/component.ts
@@ -66,6 +66,7 @@ export type ComponentApi =
accountId: string;
createdAt: number;
idToken?: null | string;
+ issuer?: null | string;
password?: null | string;
providerId: string;
refreshToken?: null | string;
@@ -89,6 +90,8 @@ export type ComponentApi =
| {
data: {
backupCodes: string;
+ failedVerificationCount?: null | number;
+ lockedUntil?: null | number;
secret: string;
userId: string;
verified?: null | boolean;
@@ -138,7 +141,9 @@ export type ComponentApi =
}
| {
data: {
+ alg?: null | string;
createdAt: number;
+ crv?: null | string;
expiresAt?: null | number;
privateKey: string;
publicKey: string;
@@ -241,6 +246,7 @@ export type ComponentApi =
where?: Array<{
connector?: "AND" | "OR";
field:
+ | "issuer"
| "accountId"
| "providerId"
| "userId"
@@ -318,6 +324,8 @@ export type ComponentApi =
| "backupCodes"
| "userId"
| "verified"
+ | "failedVerificationCount"
+ | "lockedUntil"
| "_id";
mode?: "sensitive" | "insensitive";
operator?:
@@ -460,6 +468,8 @@ export type ComponentApi =
| "privateKey"
| "createdAt"
| "expiresAt"
+ | "alg"
+ | "crv"
| "_id";
mode?: "sensitive" | "insensitive";
operator?:
@@ -609,6 +619,7 @@ export type ComponentApi =
where?: Array<{
connector?: "AND" | "OR";
field:
+ | "issuer"
| "accountId"
| "providerId"
| "userId"
@@ -686,6 +697,8 @@ export type ComponentApi =
| "backupCodes"
| "userId"
| "verified"
+ | "failedVerificationCount"
+ | "lockedUntil"
| "_id";
mode?: "sensitive" | "insensitive";
operator?:
@@ -828,6 +841,8 @@ export type ComponentApi =
| "privateKey"
| "createdAt"
| "expiresAt"
+ | "alg"
+ | "crv"
| "_id";
mode?: "sensitive" | "insensitive";
operator?:
@@ -984,14 +999,15 @@ export type ComponentApi =
any,
Name
>;
- updateMany: FunctionReference<
+ incrementOne: FunctionReference<
"mutation",
"internal",
{
input:
| {
+ increment: Record;
model: "user";
- update: {
+ set?: {
createdAt?: number;
displayUsername?: null | string;
email?: string;
@@ -1006,7 +1022,7 @@ export type ComponentApi =
userId?: null | string;
username?: null | string;
};
- where?: Array<{
+ where: Array<{
connector?: "AND" | "OR";
field:
| "name"
@@ -1046,8 +1062,9 @@ export type ComponentApi =
}>;
}
| {
+ increment: Record;
model: "session";
- update: {
+ set?: {
createdAt?: number;
expiresAt?: number;
ipAddress?: null | string;
@@ -1056,7 +1073,7 @@ export type ComponentApi =
userAgent?: null | string;
userId?: string;
};
- where?: Array<{
+ where: Array<{
connector?: "AND" | "OR";
field:
| "expiresAt"
@@ -1090,13 +1107,15 @@ export type ComponentApi =
}>;
}
| {
+ increment: Record;
model: "account";
- update: {
+ set?: {
accessToken?: null | string;
accessTokenExpiresAt?: null | number;
accountId?: string;
createdAt?: number;
idToken?: null | string;
+ issuer?: null | string;
password?: null | string;
providerId?: string;
refreshToken?: null | string;
@@ -1105,9 +1124,10 @@ export type ComponentApi =
updatedAt?: number;
userId?: string;
};
- where?: Array<{
+ where: Array<{
connector?: "AND" | "OR";
field:
+ | "issuer"
| "accountId"
| "providerId"
| "userId"
@@ -1144,15 +1164,16 @@ export type ComponentApi =
}>;
}
| {
+ increment: Record;
model: "verification";
- update: {
+ set?: {
createdAt?: number;
expiresAt?: number;
identifier?: string;
updatedAt?: number;
value?: string;
};
- where?: Array<{
+ where: Array<{
connector?: "AND" | "OR";
field:
| "identifier"
@@ -1184,20 +1205,25 @@ export type ComponentApi =
}>;
}
| {
+ increment: Record;
model: "twoFactor";
- update: {
+ set?: {
backupCodes?: string;
+ failedVerificationCount?: null | number;
+ lockedUntil?: null | number;
secret?: string;
userId?: string;
verified?: null | boolean;
};
- where?: Array<{
+ where: Array<{
connector?: "AND" | "OR";
field:
| "secret"
| "backupCodes"
| "userId"
| "verified"
+ | "failedVerificationCount"
+ | "lockedUntil"
| "_id";
mode?: "sensitive" | "insensitive";
operator?:
@@ -1222,8 +1248,9 @@ export type ComponentApi =
}>;
}
| {
+ increment: Record;
model: "oauthApplication";
- update: {
+ set?: {
clientId?: null | string;
clientSecret?: null | string;
createdAt?: null | number;
@@ -1236,7 +1263,7 @@ export type ComponentApi =
updatedAt?: null | number;
userId?: null | string;
};
- where?: Array<{
+ where: Array<{
connector?: "AND" | "OR";
field:
| "name"
@@ -1274,8 +1301,9 @@ export type ComponentApi =
}>;
}
| {
+ increment: Record;
model: "oauthAccessToken";
- update: {
+ set?: {
accessToken?: null | string;
accessTokenExpiresAt?: null | number;
clientId?: null | string;
@@ -1286,7 +1314,7 @@ export type ComponentApi =
updatedAt?: null | number;
userId?: null | string;
};
- where?: Array<{
+ where: Array<{
connector?: "AND" | "OR";
field:
| "accessToken"
@@ -1322,8 +1350,9 @@ export type ComponentApi =
}>;
}
| {
+ increment: Record;
model: "oauthConsent";
- update: {
+ set?: {
clientId?: null | string;
consentGiven?: null | boolean;
createdAt?: null | number;
@@ -1331,7 +1360,7 @@ export type ComponentApi =
updatedAt?: null | number;
userId?: null | string;
};
- where?: Array<{
+ where: Array<{
connector?: "AND" | "OR";
field:
| "clientId"
@@ -1364,20 +1393,25 @@ export type ComponentApi =
}>;
}
| {
+ increment: Record;
model: "jwks";
- update: {
+ set?: {
+ alg?: null | string;
createdAt?: number;
+ crv?: null | string;
expiresAt?: null | number;
privateKey?: string;
publicKey?: string;
};
- where?: Array<{
+ where: Array<{
connector?: "AND" | "OR";
field:
| "publicKey"
| "privateKey"
| "createdAt"
| "expiresAt"
+ | "alg"
+ | "crv"
| "_id";
mode?: "sensitive" | "insensitive";
operator?:
@@ -1402,9 +1436,10 @@ export type ComponentApi =
}>;
}
| {
+ increment: Record;
model: "rateLimit";
- update: { count?: number; key?: string; lastRequest?: number };
- where?: Array<{
+ set?: { count?: number; key?: string; lastRequest?: number };
+ where: Array<{
connector?: "AND" | "OR";
field: "key" | "count" | "lastRequest" | "_id";
mode?: "sensitive" | "insensitive";
@@ -1430,19 +1465,11 @@ export type ComponentApi =
}>;
};
onUpdateHandle?: string;
- paginationOpts: {
- cursor: string | null;
- endCursor?: string | null;
- id?: number;
- maximumBytesRead?: number;
- maximumRowsRead?: number;
- numItems: number;
- };
},
any,
Name
>;
- updateOne: FunctionReference<
+ updateMany: FunctionReference<
"mutation",
"internal",
{
@@ -1555,6 +1582,7 @@ export type ComponentApi =
accountId?: string;
createdAt?: number;
idToken?: null | string;
+ issuer?: null | string;
password?: null | string;
providerId?: string;
refreshToken?: null | string;
@@ -1566,6 +1594,7 @@ export type ComponentApi =
where?: Array<{
connector?: "AND" | "OR";
field:
+ | "issuer"
| "accountId"
| "providerId"
| "userId"
@@ -1645,6 +1674,8 @@ export type ComponentApi =
model: "twoFactor";
update: {
backupCodes?: string;
+ failedVerificationCount?: null | number;
+ lockedUntil?: null | number;
secret?: string;
userId?: string;
verified?: null | boolean;
@@ -1656,6 +1687,8 @@ export type ComponentApi =
| "backupCodes"
| "userId"
| "verified"
+ | "failedVerificationCount"
+ | "lockedUntil"
| "_id";
mode?: "sensitive" | "insensitive";
operator?:
@@ -1824,7 +1857,9 @@ export type ComponentApi =
| {
model: "jwks";
update: {
+ alg?: null | string;
createdAt?: number;
+ crv?: null | string;
expiresAt?: null | number;
privateKey?: string;
publicKey?: string;
@@ -1836,6 +1871,8 @@ export type ComponentApi =
| "privateKey"
| "createdAt"
| "expiresAt"
+ | "alg"
+ | "crv"
| "_id";
mode?: "sensitive" | "insensitive";
operator?:
@@ -1888,212 +1925,6730 @@ export type ComponentApi =
}>;
};
onUpdateHandle?: string;
+ paginationOpts: {
+ cursor: string | null;
+ endCursor?: string | null;
+ id?: number;
+ maximumBytesRead?: number;
+ maximumRowsRead?: number;
+ numItems: number;
+ };
},
any,
Name
>;
- };
- testProfiles: {
- adapterAdditionalFields: {
- create: FunctionReference<
- "mutation",
- "internal",
- {
- input:
- | {
- data: {
- cbDefaultValueField?: null | string;
- createdAt: number;
- customField?: null | string;
- dateField?: null | number;
- displayUsername?: null | string;
- email: string;
- emailVerified: boolean;
- image?: null | string;
- isAnonymous?: null | boolean;
- name: string;
- numericField?: null | number;
- phoneNumber?: null | string;
- phoneNumberVerified?: null | boolean;
- testField?: null | string;
- twoFactorEnabled?: null | boolean;
- updatedAt: number;
- userId?: null | string;
- username?: null | string;
- };
- model: "user";
- }
- | {
- data: {
- createdAt: number;
- expiresAt: number;
- ipAddress?: null | string;
- token: string;
- updatedAt: number;
- userAgent?: null | string;
- userId: string;
- };
- model: "session";
- }
- | {
- data: {
- accessToken?: null | string;
- accessTokenExpiresAt?: null | number;
- accountId: string;
- createdAt: number;
- idToken?: null | string;
- password?: null | string;
- providerId: string;
- refreshToken?: null | string;
- refreshTokenExpiresAt?: null | number;
- scope?: null | string;
- updatedAt: number;
- userId: string;
- };
- model: "account";
- }
- | {
- data: {
- createdAt: number;
- expiresAt: number;
- identifier: string;
- updatedAt: number;
- value: string;
- };
- model: "verification";
- }
- | {
- data: {
- backupCodes: string;
- secret: string;
- userId: string;
- verified?: null | boolean;
- };
- model: "twoFactor";
- }
- | {
- data: {
- clientId?: null | string;
- clientSecret?: null | string;
- createdAt?: null | number;
- disabled?: null | boolean;
- icon?: null | string;
- metadata?: null | string;
- name?: null | string;
- redirectUrls?: null | string;
- type?: null | string;
- updatedAt?: null | number;
- userId?: null | string;
- };
- model: "oauthApplication";
- }
- | {
- data: {
- accessToken?: null | string;
- accessTokenExpiresAt?: null | number;
- clientId?: null | string;
- createdAt?: null | number;
- refreshToken?: null | string;
- refreshTokenExpiresAt?: null | number;
- scopes?: null | string;
- updatedAt?: null | number;
- userId?: null | string;
- };
- model: "oauthAccessToken";
- }
- | {
- data: {
- clientId?: null | string;
- consentGiven?: null | boolean;
- createdAt?: null | number;
- scopes?: null | string;
- updatedAt?: null | number;
- userId?: null | string;
- };
- model: "oauthConsent";
- }
- | {
- data: {
- createdAt: number;
- expiresAt?: null | number;
- privateKey: string;
- publicKey: string;
- };
- model: "jwks";
- }
- | {
- data: { count: number; key: string; lastRequest: number };
- model: "rateLimit";
+ updateOne: FunctionReference<
+ "mutation",
+ "internal",
+ {
+ input:
+ | {
+ model: "user";
+ update: {
+ createdAt?: number;
+ displayUsername?: null | string;
+ email?: string;
+ emailVerified?: boolean;
+ image?: null | string;
+ isAnonymous?: null | boolean;
+ name?: string;
+ phoneNumber?: null | string;
+ phoneNumberVerified?: null | boolean;
+ twoFactorEnabled?: null | boolean;
+ updatedAt?: number;
+ userId?: null | string;
+ username?: null | string;
};
- onCreateHandle?: string;
- select?: Array;
- },
- any,
- Name
- >;
- deleteMany: FunctionReference<
- "mutation",
- "internal",
- {
- input:
- | {
- model: "user";
- where?: Array<{
- connector?: "AND" | "OR";
- field:
- | "name"
- | "email"
- | "emailVerified"
- | "image"
- | "createdAt"
- | "updatedAt"
- | "twoFactorEnabled"
- | "isAnonymous"
- | "username"
- | "displayUsername"
- | "phoneNumber"
- | "phoneNumberVerified"
- | "userId"
- | "customField"
- | "numericField"
- | "testField"
- | "cbDefaultValueField"
- | "dateField"
- | "_id";
- mode?: "sensitive" | "insensitive";
- operator?:
- | "lt"
- | "lte"
- | "gt"
- | "gte"
- | "eq"
- | "in"
- | "not_in"
- | "ne"
- | "contains"
- | "starts_with"
- | "ends_with";
- value:
- | string
- | number
- | boolean
- | Array
- | Array
- | null;
- }>;
- }
- | {
- model: "session";
- where?: Array<{
- connector?: "AND" | "OR";
- field:
- | "expiresAt"
- | "token"
+ where?: Array<{
+ connector?: "AND" | "OR";
+ field:
+ | "name"
+ | "email"
+ | "emailVerified"
+ | "image"
+ | "createdAt"
+ | "updatedAt"
+ | "twoFactorEnabled"
+ | "isAnonymous"
+ | "username"
+ | "displayUsername"
+ | "phoneNumber"
+ | "phoneNumberVerified"
+ | "userId"
+ | "_id";
+ mode?: "sensitive" | "insensitive";
+ operator?:
+ | "lt"
+ | "lte"
+ | "gt"
+ | "gte"
+ | "eq"
+ | "in"
+ | "not_in"
+ | "ne"
+ | "contains"
+ | "starts_with"
+ | "ends_with";
+ value:
+ | string
+ | number
+ | boolean
+ | Array
+ | Array
+ | null;
+ }>;
+ }
+ | {
+ model: "session";
+ update: {
+ createdAt?: number;
+ expiresAt?: number;
+ ipAddress?: null | string;
+ token?: string;
+ updatedAt?: number;
+ userAgent?: null | string;
+ userId?: string;
+ };
+ where?: Array<{
+ connector?: "AND" | "OR";
+ field:
+ | "expiresAt"
+ | "token"
+ | "createdAt"
+ | "updatedAt"
+ | "ipAddress"
+ | "userAgent"
+ | "userId"
+ | "_id";
+ mode?: "sensitive" | "insensitive";
+ operator?:
+ | "lt"
+ | "lte"
+ | "gt"
+ | "gte"
+ | "eq"
+ | "in"
+ | "not_in"
+ | "ne"
+ | "contains"
+ | "starts_with"
+ | "ends_with";
+ value:
+ | string
+ | number
+ | boolean
+ | Array
+ | Array
+ | null;
+ }>;
+ }
+ | {
+ model: "account";
+ update: {
+ accessToken?: null | string;
+ accessTokenExpiresAt?: null | number;
+ accountId?: string;
+ createdAt?: number;
+ idToken?: null | string;
+ issuer?: null | string;
+ password?: null | string;
+ providerId?: string;
+ refreshToken?: null | string;
+ refreshTokenExpiresAt?: null | number;
+ scope?: null | string;
+ updatedAt?: number;
+ userId?: string;
+ };
+ where?: Array<{
+ connector?: "AND" | "OR";
+ field:
+ | "issuer"
+ | "accountId"
+ | "providerId"
+ | "userId"
+ | "accessToken"
+ | "refreshToken"
+ | "idToken"
+ | "accessTokenExpiresAt"
+ | "refreshTokenExpiresAt"
+ | "scope"
+ | "password"
+ | "createdAt"
+ | "updatedAt"
+ | "_id";
+ mode?: "sensitive" | "insensitive";
+ operator?:
+ | "lt"
+ | "lte"
+ | "gt"
+ | "gte"
+ | "eq"
+ | "in"
+ | "not_in"
+ | "ne"
+ | "contains"
+ | "starts_with"
+ | "ends_with";
+ value:
+ | string
+ | number
+ | boolean
+ | Array
+ | Array
+ | null;
+ }>;
+ }
+ | {
+ model: "verification";
+ update: {
+ createdAt?: number;
+ expiresAt?: number;
+ identifier?: string;
+ updatedAt?: number;
+ value?: string;
+ };
+ where?: Array<{
+ connector?: "AND" | "OR";
+ field:
+ | "identifier"
+ | "value"
+ | "expiresAt"
+ | "createdAt"
+ | "updatedAt"
+ | "_id";
+ mode?: "sensitive" | "insensitive";
+ operator?:
+ | "lt"
+ | "lte"
+ | "gt"
+ | "gte"
+ | "eq"
+ | "in"
+ | "not_in"
+ | "ne"
+ | "contains"
+ | "starts_with"
+ | "ends_with";
+ value:
+ | string
+ | number
+ | boolean
+ | Array
+ | Array
+ | null;
+ }>;
+ }
+ | {
+ model: "twoFactor";
+ update: {
+ backupCodes?: string;
+ failedVerificationCount?: null | number;
+ lockedUntil?: null | number;
+ secret?: string;
+ userId?: string;
+ verified?: null | boolean;
+ };
+ where?: Array<{
+ connector?: "AND" | "OR";
+ field:
+ | "secret"
+ | "backupCodes"
+ | "userId"
+ | "verified"
+ | "failedVerificationCount"
+ | "lockedUntil"
+ | "_id";
+ mode?: "sensitive" | "insensitive";
+ operator?:
+ | "lt"
+ | "lte"
+ | "gt"
+ | "gte"
+ | "eq"
+ | "in"
+ | "not_in"
+ | "ne"
+ | "contains"
+ | "starts_with"
+ | "ends_with";
+ value:
+ | string
+ | number
+ | boolean
+ | Array
+ | Array
+ | null;
+ }>;
+ }
+ | {
+ model: "oauthApplication";
+ update: {
+ clientId?: null | string;
+ clientSecret?: null | string;
+ createdAt?: null | number;
+ disabled?: null | boolean;
+ icon?: null | string;
+ metadata?: null | string;
+ name?: null | string;
+ redirectUrls?: null | string;
+ type?: null | string;
+ updatedAt?: null | number;
+ userId?: null | string;
+ };
+ where?: Array<{
+ connector?: "AND" | "OR";
+ field:
+ | "name"
+ | "icon"
+ | "metadata"
+ | "clientId"
+ | "clientSecret"
+ | "redirectUrls"
+ | "type"
+ | "disabled"
+ | "userId"
+ | "createdAt"
+ | "updatedAt"
+ | "_id";
+ mode?: "sensitive" | "insensitive";
+ operator?:
+ | "lt"
+ | "lte"
+ | "gt"
+ | "gte"
+ | "eq"
+ | "in"
+ | "not_in"
+ | "ne"
+ | "contains"
+ | "starts_with"
+ | "ends_with";
+ value:
+ | string
+ | number
+ | boolean
+ | Array
+ | Array
+ | null;
+ }>;
+ }
+ | {
+ model: "oauthAccessToken";
+ update: {
+ accessToken?: null | string;
+ accessTokenExpiresAt?: null | number;
+ clientId?: null | string;
+ createdAt?: null | number;
+ refreshToken?: null | string;
+ refreshTokenExpiresAt?: null | number;
+ scopes?: null | string;
+ updatedAt?: null | number;
+ userId?: null | string;
+ };
+ where?: Array<{
+ connector?: "AND" | "OR";
+ field:
+ | "accessToken"
+ | "refreshToken"
+ | "accessTokenExpiresAt"
+ | "refreshTokenExpiresAt"
+ | "clientId"
+ | "userId"
+ | "scopes"
+ | "createdAt"
+ | "updatedAt"
+ | "_id";
+ mode?: "sensitive" | "insensitive";
+ operator?:
+ | "lt"
+ | "lte"
+ | "gt"
+ | "gte"
+ | "eq"
+ | "in"
+ | "not_in"
+ | "ne"
+ | "contains"
+ | "starts_with"
+ | "ends_with";
+ value:
+ | string
+ | number
+ | boolean
+ | Array
+ | Array
+ | null;
+ }>;
+ }
+ | {
+ model: "oauthConsent";
+ update: {
+ clientId?: null | string;
+ consentGiven?: null | boolean;
+ createdAt?: null | number;
+ scopes?: null | string;
+ updatedAt?: null | number;
+ userId?: null | string;
+ };
+ where?: Array<{
+ connector?: "AND" | "OR";
+ field:
+ | "clientId"
+ | "userId"
+ | "scopes"
+ | "createdAt"
+ | "updatedAt"
+ | "consentGiven"
+ | "_id";
+ mode?: "sensitive" | "insensitive";
+ operator?:
+ | "lt"
+ | "lte"
+ | "gt"
+ | "gte"
+ | "eq"
+ | "in"
+ | "not_in"
+ | "ne"
+ | "contains"
+ | "starts_with"
+ | "ends_with";
+ value:
+ | string
+ | number
+ | boolean
+ | Array
+ | Array
+ | null;
+ }>;
+ }
+ | {
+ model: "jwks";
+ update: {
+ alg?: null | string;
+ createdAt?: number;
+ crv?: null | string;
+ expiresAt?: null | number;
+ privateKey?: string;
+ publicKey?: string;
+ };
+ where?: Array<{
+ connector?: "AND" | "OR";
+ field:
+ | "publicKey"
+ | "privateKey"
+ | "createdAt"
+ | "expiresAt"
+ | "alg"
+ | "crv"
+ | "_id";
+ mode?: "sensitive" | "insensitive";
+ operator?:
+ | "lt"
+ | "lte"
+ | "gt"
+ | "gte"
+ | "eq"
+ | "in"
+ | "not_in"
+ | "ne"
+ | "contains"
+ | "starts_with"
+ | "ends_with";
+ value:
+ | string
+ | number
+ | boolean
+ | Array
+ | Array
+ | null;
+ }>;
+ }
+ | {
+ model: "rateLimit";
+ update: { count?: number; key?: string; lastRequest?: number };
+ where?: Array<{
+ connector?: "AND" | "OR";
+ field: "key" | "count" | "lastRequest" | "_id";
+ mode?: "sensitive" | "insensitive";
+ operator?:
+ | "lt"
+ | "lte"
+ | "gt"
+ | "gte"
+ | "eq"
+ | "in"
+ | "not_in"
+ | "ne"
+ | "contains"
+ | "starts_with"
+ | "ends_with";
+ value:
+ | string
+ | number
+ | boolean
+ | Array
+ | Array
+ | null;
+ }>;
+ };
+ onUpdateHandle?: string;
+ },
+ any,
+ Name
+ >;
+ };
+ migrations: {
+ clearLegacyOAuthProviderRecords: FunctionReference<
+ "mutation",
+ "internal",
+ {
+ paginationOpts: {
+ cursor: string | null;
+ endCursor?: string | null;
+ id?: number;
+ maximumBytesRead?: number;
+ maximumRowsRead?: number;
+ numItems: number;
+ };
+ table: "oauthApplication" | "oauthAccessToken" | "oauthConsent";
+ },
+ { continueCursor: string; deleted: number; isDone: boolean },
+ Name
+ >;
+ listLegacyOAuthApplications: FunctionReference<
+ "query",
+ "internal",
+ {
+ paginationOpts: {
+ cursor: string | null;
+ endCursor?: string | null;
+ id?: number;
+ maximumBytesRead?: number;
+ maximumRowsRead?: number;
+ numItems: number;
+ };
+ },
+ {
+ continueCursor: string;
+ isDone: boolean;
+ page: Array<{
+ _creationTime: number;
+ _id: string;
+ clientId?: null | string;
+ createdAt?: null | number;
+ disabled?: null | boolean;
+ hadClientSecret: boolean;
+ icon?: null | string;
+ metadata?: null | string;
+ name?: null | string;
+ redirectUrls?: null | string;
+ type?: null | string;
+ updatedAt?: null | number;
+ userId?: null | string;
+ }>;
+ pageStatus?: "SplitRecommended" | "SplitRequired" | null;
+ splitCursor?: string | null;
+ },
+ Name
+ >;
+ };
+ testProfiles: {
+ adapterAdditionalFields: {
+ create: FunctionReference<
+ "mutation",
+ "internal",
+ {
+ input:
+ | {
+ data: {
+ cbDefaultValueField?: null | string;
+ createdAt: number;
+ customField?: null | string;
+ dateField?: null | number;
+ displayUsername?: null | string;
+ email: string;
+ emailVerified: boolean;
+ image?: null | string;
+ isAnonymous?: null | boolean;
+ name: string;
+ numericField?: null | number;
+ phoneNumber?: null | string;
+ phoneNumberVerified?: null | boolean;
+ testField?: null | string;
+ twoFactorEnabled?: null | boolean;
+ updatedAt: number;
+ userId?: null | string;
+ username?: null | string;
+ };
+ model: "user";
+ }
+ | {
+ data: {
+ createdAt: number;
+ expiresAt: number;
+ ipAddress?: null | string;
+ token: string;
+ updatedAt: number;
+ userAgent?: null | string;
+ userId: string;
+ };
+ model: "session";
+ }
+ | {
+ data: {
+ accessToken?: null | string;
+ accessTokenExpiresAt?: null | number;
+ accountId: string;
+ createdAt: number;
+ idToken?: null | string;
+ issuer?: null | string;
+ password?: null | string;
+ providerId: string;
+ refreshToken?: null | string;
+ refreshTokenExpiresAt?: null | number;
+ scope?: null | string;
+ updatedAt: number;
+ userId: string;
+ };
+ model: "account";
+ }
+ | {
+ data: {
+ createdAt: number;
+ expiresAt: number;
+ identifier: string;
+ updatedAt: number;
+ value: string;
+ };
+ model: "verification";
+ }
+ | {
+ data: {
+ backupCodes: string;
+ failedVerificationCount?: null | number;
+ lockedUntil?: null | number;
+ secret: string;
+ userId: string;
+ verified?: null | boolean;
+ };
+ model: "twoFactor";
+ }
+ | {
+ data: {
+ clientId?: null | string;
+ clientSecret?: null | string;
+ createdAt?: null | number;
+ disabled?: null | boolean;
+ icon?: null | string;
+ metadata?: null | string;
+ name?: null | string;
+ redirectUrls?: null | string;
+ type?: null | string;
+ updatedAt?: null | number;
+ userId?: null | string;
+ };
+ model: "oauthApplication";
+ }
+ | {
+ data: {
+ accessToken?: null | string;
+ accessTokenExpiresAt?: null | number;
+ clientId?: null | string;
+ createdAt?: null | number;
+ refreshToken?: null | string;
+ refreshTokenExpiresAt?: null | number;
+ scopes?: null | string;
+ updatedAt?: null | number;
+ userId?: null | string;
+ };
+ model: "oauthAccessToken";
+ }
+ | {
+ data: {
+ clientId?: null | string;
+ consentGiven?: null | boolean;
+ createdAt?: null | number;
+ scopes?: null | string;
+ updatedAt?: null | number;
+ userId?: null | string;
+ };
+ model: "oauthConsent";
+ }
+ | {
+ data: {
+ alg?: null | string;
+ createdAt: number;
+ crv?: null | string;
+ expiresAt?: null | number;
+ privateKey: string;
+ publicKey: string;
+ };
+ model: "jwks";
+ }
+ | {
+ data: { count: number; key: string; lastRequest: number };
+ model: "rateLimit";
+ };
+ onCreateHandle?: string;
+ select?: Array;
+ },
+ any,
+ Name
+ >;
+ deleteMany: FunctionReference<
+ "mutation",
+ "internal",
+ {
+ input:
+ | {
+ model: "user";
+ where?: Array<{
+ connector?: "AND" | "OR";
+ field:
+ | "name"
+ | "email"
+ | "emailVerified"
+ | "image"
+ | "createdAt"
+ | "updatedAt"
+ | "twoFactorEnabled"
+ | "isAnonymous"
+ | "username"
+ | "displayUsername"
+ | "phoneNumber"
+ | "phoneNumberVerified"
+ | "userId"
+ | "customField"
+ | "numericField"
+ | "testField"
+ | "cbDefaultValueField"
+ | "dateField"
+ | "_id";
+ mode?: "sensitive" | "insensitive";
+ operator?:
+ | "lt"
+ | "lte"
+ | "gt"
+ | "gte"
+ | "eq"
+ | "in"
+ | "not_in"
+ | "ne"
+ | "contains"
+ | "starts_with"
+ | "ends_with";
+ value:
+ | string
+ | number
+ | boolean
+ | Array
+ | Array
+ | null;
+ }>;
+ }
+ | {
+ model: "session";
+ where?: Array<{
+ connector?: "AND" | "OR";
+ field:
+ | "expiresAt"
+ | "token"
+ | "createdAt"
+ | "updatedAt"
+ | "ipAddress"
+ | "userAgent"
+ | "userId"
+ | "_id";
+ mode?: "sensitive" | "insensitive";
+ operator?:
+ | "lt"
+ | "lte"
+ | "gt"
+ | "gte"
+ | "eq"
+ | "in"
+ | "not_in"
+ | "ne"
+ | "contains"
+ | "starts_with"
+ | "ends_with";
+ value:
+ | string
+ | number
+ | boolean
+ | Array
+ | Array
+ | null;
+ }>;
+ }
+ | {
+ model: "account";
+ where?: Array<{
+ connector?: "AND" | "OR";
+ field:
+ | "issuer"
+ | "accountId"
+ | "providerId"
+ | "userId"
+ | "accessToken"
+ | "refreshToken"
+ | "idToken"
+ | "accessTokenExpiresAt"
+ | "refreshTokenExpiresAt"
+ | "scope"
+ | "password"
+ | "createdAt"
+ | "updatedAt"
+ | "_id";
+ mode?: "sensitive" | "insensitive";
+ operator?:
+ | "lt"
+ | "lte"
+ | "gt"
+ | "gte"
+ | "eq"
+ | "in"
+ | "not_in"
+ | "ne"
+ | "contains"
+ | "starts_with"
+ | "ends_with";
+ value:
+ | string
+ | number
+ | boolean
+ | Array
+ | Array
+ | null;
+ }>;
+ }
+ | {
+ model: "verification";
+ where?: Array<{
+ connector?: "AND" | "OR";
+ field:
+ | "identifier"
+ | "value"
+ | "expiresAt"
+ | "createdAt"
+ | "updatedAt"
+ | "_id";
+ mode?: "sensitive" | "insensitive";
+ operator?:
+ | "lt"
+ | "lte"
+ | "gt"
+ | "gte"
+ | "eq"
+ | "in"
+ | "not_in"
+ | "ne"
+ | "contains"
+ | "starts_with"
+ | "ends_with";
+ value:
+ | string
+ | number
+ | boolean
+ | Array
+ | Array
+ | null;
+ }>;
+ }
+ | {
+ model: "twoFactor";
+ where?: Array<{
+ connector?: "AND" | "OR";
+ field:
+ | "secret"
+ | "backupCodes"
+ | "userId"
+ | "verified"
+ | "failedVerificationCount"
+ | "lockedUntil"
+ | "_id";
+ mode?: "sensitive" | "insensitive";
+ operator?:
+ | "lt"
+ | "lte"
+ | "gt"
+ | "gte"
+ | "eq"
+ | "in"
+ | "not_in"
+ | "ne"
+ | "contains"
+ | "starts_with"
+ | "ends_with";
+ value:
+ | string
+ | number
+ | boolean
+ | Array
+ | Array
+ | null;
+ }>;
+ }
+ | {
+ model: "oauthApplication";
+ where?: Array<{
+ connector?: "AND" | "OR";
+ field:
+ | "name"
+ | "icon"
+ | "metadata"
+ | "clientId"
+ | "clientSecret"
+ | "redirectUrls"
+ | "type"
+ | "disabled"
+ | "userId"
+ | "createdAt"
+ | "updatedAt"
+ | "_id";
+ mode?: "sensitive" | "insensitive";
+ operator?:
+ | "lt"
+ | "lte"
+ | "gt"
+ | "gte"
+ | "eq"
+ | "in"
+ | "not_in"
+ | "ne"
+ | "contains"
+ | "starts_with"
+ | "ends_with";
+ value:
+ | string
+ | number
+ | boolean
+ | Array
+ | Array
+ | null;
+ }>;
+ }
+ | {
+ model: "oauthAccessToken";
+ where?: Array<{
+ connector?: "AND" | "OR";
+ field:
+ | "accessToken"
+ | "refreshToken"
+ | "accessTokenExpiresAt"
+ | "refreshTokenExpiresAt"
+ | "clientId"
+ | "userId"
+ | "scopes"
+ | "createdAt"
+ | "updatedAt"
+ | "_id";
+ mode?: "sensitive" | "insensitive";
+ operator?:
+ | "lt"
+ | "lte"
+ | "gt"
+ | "gte"
+ | "eq"
+ | "in"
+ | "not_in"
+ | "ne"
+ | "contains"
+ | "starts_with"
+ | "ends_with";
+ value:
+ | string
+ | number
+ | boolean
+ | Array
+ | Array
+ | null;
+ }>;
+ }
+ | {
+ model: "oauthConsent";
+ where?: Array<{
+ connector?: "AND" | "OR";
+ field:
+ | "clientId"
+ | "userId"
+ | "scopes"
+ | "createdAt"
+ | "updatedAt"
+ | "consentGiven"
+ | "_id";
+ mode?: "sensitive" | "insensitive";
+ operator?:
+ | "lt"
+ | "lte"
+ | "gt"
+ | "gte"
+ | "eq"
+ | "in"
+ | "not_in"
+ | "ne"
+ | "contains"
+ | "starts_with"
+ | "ends_with";
+ value:
+ | string
+ | number
+ | boolean
+ | Array
+ | Array
+ | null;
+ }>;
+ }
+ | {
+ model: "jwks";
+ where?: Array<{
+ connector?: "AND" | "OR";
+ field:
+ | "publicKey"
+ | "privateKey"
+ | "createdAt"
+ | "expiresAt"
+ | "alg"
+ | "crv"
+ | "_id";
+ mode?: "sensitive" | "insensitive";
+ operator?:
+ | "lt"
+ | "lte"
+ | "gt"
+ | "gte"
+ | "eq"
+ | "in"
+ | "not_in"
+ | "ne"
+ | "contains"
+ | "starts_with"
+ | "ends_with";
+ value:
+ | string
+ | number
+ | boolean
+ | Array
+ | Array
+ | null;
+ }>;
+ }
+ | {
+ model: "rateLimit";
+ where?: Array<{
+ connector?: "AND" | "OR";
+ field: "key" | "count" | "lastRequest" | "_id";
+ mode?: "sensitive" | "insensitive";
+ operator?:
+ | "lt"
+ | "lte"
+ | "gt"
+ | "gte"
+ | "eq"
+ | "in"
+ | "not_in"
+ | "ne"
+ | "contains"
+ | "starts_with"
+ | "ends_with";
+ value:
+ | string
+ | number
+ | boolean
+ | Array
+ | Array
+ | null;
+ }>;
+ };
+ onDeleteHandle?: string;
+ paginationOpts: {
+ cursor: string | null;
+ endCursor?: string | null;
+ id?: number;
+ maximumBytesRead?: number;
+ maximumRowsRead?: number;
+ numItems: number;
+ };
+ },
+ any,
+ Name
+ >;
+ deleteOne: FunctionReference<
+ "mutation",
+ "internal",
+ {
+ input:
+ | {
+ model: "user";
+ where?: Array<{
+ connector?: "AND" | "OR";
+ field:
+ | "name"
+ | "email"
+ | "emailVerified"
+ | "image"
+ | "createdAt"
+ | "updatedAt"
+ | "twoFactorEnabled"
+ | "isAnonymous"
+ | "username"
+ | "displayUsername"
+ | "phoneNumber"
+ | "phoneNumberVerified"
+ | "userId"
+ | "customField"
+ | "numericField"
+ | "testField"
+ | "cbDefaultValueField"
+ | "dateField"
+ | "_id";
+ mode?: "sensitive" | "insensitive";
+ operator?:
+ | "lt"
+ | "lte"
+ | "gt"
+ | "gte"
+ | "eq"
+ | "in"
+ | "not_in"
+ | "ne"
+ | "contains"
+ | "starts_with"
+ | "ends_with";
+ value:
+ | string
+ | number
+ | boolean
+ | Array
+ | Array
+ | null;
+ }>;
+ }
+ | {
+ model: "session";
+ where?: Array<{
+ connector?: "AND" | "OR";
+ field:
+ | "expiresAt"
+ | "token"
+ | "createdAt"
+ | "updatedAt"
+ | "ipAddress"
+ | "userAgent"
+ | "userId"
+ | "_id";
+ mode?: "sensitive" | "insensitive";
+ operator?:
+ | "lt"
+ | "lte"
+ | "gt"
+ | "gte"
+ | "eq"
+ | "in"
+ | "not_in"
+ | "ne"
+ | "contains"
+ | "starts_with"
+ | "ends_with";
+ value:
+ | string
+ | number
+ | boolean
+ | Array
+ | Array
+ | null;
+ }>;
+ }
+ | {
+ model: "account";
+ where?: Array<{
+ connector?: "AND" | "OR";
+ field:
+ | "issuer"
+ | "accountId"
+ | "providerId"
+ | "userId"
+ | "accessToken"
+ | "refreshToken"
+ | "idToken"
+ | "accessTokenExpiresAt"
+ | "refreshTokenExpiresAt"
+ | "scope"
+ | "password"
+ | "createdAt"
+ | "updatedAt"
+ | "_id";
+ mode?: "sensitive" | "insensitive";
+ operator?:
+ | "lt"
+ | "lte"
+ | "gt"
+ | "gte"
+ | "eq"
+ | "in"
+ | "not_in"
+ | "ne"
+ | "contains"
+ | "starts_with"
+ | "ends_with";
+ value:
+ | string
+ | number
+ | boolean
+ | Array
+ | Array
+ | null;
+ }>;
+ }
+ | {
+ model: "verification";
+ where?: Array<{
+ connector?: "AND" | "OR";
+ field:
+ | "identifier"
+ | "value"
+ | "expiresAt"
+ | "createdAt"
+ | "updatedAt"
+ | "_id";
+ mode?: "sensitive" | "insensitive";
+ operator?:
+ | "lt"
+ | "lte"
+ | "gt"
+ | "gte"
+ | "eq"
+ | "in"
+ | "not_in"
+ | "ne"
+ | "contains"
+ | "starts_with"
+ | "ends_with";
+ value:
+ | string
+ | number
+ | boolean
+ | Array
+ | Array
+ | null;
+ }>;
+ }
+ | {
+ model: "twoFactor";
+ where?: Array<{
+ connector?: "AND" | "OR";
+ field:
+ | "secret"
+ | "backupCodes"
+ | "userId"
+ | "verified"
+ | "failedVerificationCount"
+ | "lockedUntil"
+ | "_id";
+ mode?: "sensitive" | "insensitive";
+ operator?:
+ | "lt"
+ | "lte"
+ | "gt"
+ | "gte"
+ | "eq"
+ | "in"
+ | "not_in"
+ | "ne"
+ | "contains"
+ | "starts_with"
+ | "ends_with";
+ value:
+ | string
+ | number
+ | boolean
+ | Array
+ | Array
+ | null;
+ }>;
+ }
+ | {
+ model: "oauthApplication";
+ where?: Array<{
+ connector?: "AND" | "OR";
+ field:
+ | "name"
+ | "icon"
+ | "metadata"
+ | "clientId"
+ | "clientSecret"
+ | "redirectUrls"
+ | "type"
+ | "disabled"
+ | "userId"
+ | "createdAt"
+ | "updatedAt"
+ | "_id";
+ mode?: "sensitive" | "insensitive";
+ operator?:
+ | "lt"
+ | "lte"
+ | "gt"
+ | "gte"
+ | "eq"
+ | "in"
+ | "not_in"
+ | "ne"
+ | "contains"
+ | "starts_with"
+ | "ends_with";
+ value:
+ | string
+ | number
+ | boolean
+ | Array
+ | Array