refactor: split the passkey plugin into a separate package (#5769)

This commit is contained in:
Taesu
2025-11-05 17:05:39 +00:00
committed by GitHub
parent 414d069303
commit f327e336c8
27 changed files with 454 additions and 248 deletions
+1 -1
View File
@@ -1,3 +1,4 @@
import { passkeyClient } from "@better-auth/passkey/client";
import { stripeClient } from "@better-auth/stripe/client";
import {
adminClient,
@@ -8,7 +9,6 @@ import {
oidcClient,
oneTapClient,
organizationClient,
passkeyClient,
twoFactorClient,
} from "better-auth/client/plugins";
import { createAuthClient } from "better-auth/react";
+1 -1
View File
@@ -1,3 +1,4 @@
import { passkey } from "@better-auth/passkey";
import { sso } from "@better-auth/sso";
import { stripe } from "@better-auth/stripe";
import { LibsqlDialect } from "@libsql/kysely-libsql";
@@ -16,7 +17,6 @@ import {
organization,
twoFactor,
} from "better-auth/plugins";
import { passkey } from "better-auth/plugins/passkey";
import { MysqlDialect } from "kysely";
import { createPool } from "mysql2/promise";
import { Stripe } from "stripe";
+1
View File
@@ -13,6 +13,7 @@
"dependencies": {
"@better-auth/sso": "workspace:*",
"@better-auth/stripe": "workspace:*",
"@better-auth/passkey": "workspace:*",
"@hookform/resolvers": "^5.2.1",
"@libsql/client": "^0.8.1",
"@libsql/kysely-libsql": "^0.4.1",
+6 -3
View File
@@ -161,15 +161,18 @@ const tabs: { name: "auth.ts" | "client.ts"; code: string }[] = [
enabled: true,
},
plugins: [
organization(),
twoFactor(),
organization(),
twoFactor(),
]
})`,
},
{
name: "client.ts",
code: `const client = createAuthClient({
plugins: [passkeyClient()]
plugins: [
organizationClient(),
twoFactorClient(),
]
});`,
},
];
+28 -30
View File
@@ -10,39 +10,20 @@ The passkey plugin implementation is powered by [SimpleWebAuthn](https://simplew
## Installation
<Steps>
<Step>
### Install the plugin
```package-install
npm install @better-auth/passkey
```
</Step>
<Step>
### Add the plugin to your auth config
To add the passkey plugin to your auth config, you need to import the plugin and pass it to the `plugins` option of the auth instance.
**Options**
`rpID`: A unique identifier for your website based on your auth server origin. 'localhost' is okay for local dev. RP ID can be formed by discarding zero or more labels from the left of its effective domain
until it hits an effective TLD. So `www.example.com` can use the RP IDs `www.example.com` or `example.com`. But not `com`, because that's an eTLD.
`rpName`: Human-readable title for your website
`origin`: The origin URL at which your better-auth server is hosted. `http://localhost` and `http://localhost:PORT` are also valid. Do **NOT** include any trailing /
`authenticatorSelection`: Allows customization of WebAuthn authenticator selection criteria. Leave unspecified for default settings.
- `authenticatorAttachment`: Specifies the type of authenticator
- `platform`: Authenticator is attached to the platform (e.g., fingerprint reader)
- `cross-platform`: Authenticator is not attached to the platform (e.g., security key)
- Default: `not set` (both platform and cross-platform allowed, with platform preferred)
- `residentKey`: Determines credential storage behavior.
- `required`: User MUST store credentials on the authenticator (highest security)
- `preferred`: Encourages credential storage but not mandatory
- `discouraged`: No credential storage required (fastest experience)
- Default: `preferred`
- `userVerification`: Controls biometric/PIN verification during authentication:
- `required`: User MUST verify identity (highest security)
- `preferred`: Verification encouraged but not mandatory
- `discouraged`: No verification required (fastest experience)
- Default: `preferred`
```ts title="auth.ts"
import { betterAuth } from "better-auth"
import { passkey } from "better-auth/plugins/passkey" // [!code highlight]
import { passkey } from "@better-auth/passkey" // [!code highlight]
export const auth = betterAuth({
plugins: [ // [!code highlight]
@@ -75,7 +56,7 @@ The passkey plugin implementation is powered by [SimpleWebAuthn](https://simplew
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client"
import { passkeyClient } from "better-auth/client/plugins"
import { passkeyClient } from "@better-auth/passkey/client"
export const authClient = createAuthClient({
plugins: [ // [!code highlight]
@@ -340,6 +321,23 @@ until it hits an effective TLD. So `www.example.com` can use the RP IDs `www.exa
**origin**: The origin URL at which your better-auth server is hosted. `http://localhost` and `http://localhost:PORT` are also valid. Do NOT include any trailing /.
**authenticatorSelection**: Allows customization of WebAuthn authenticator selection criteria. When unspecified, both platform and cross-platform authenticators are allowed with `preferred` settings for `residentKey` and `userVerification`.
**authenticatorSelection**: Allows customization of WebAuthn authenticator selection criteria. Leave unspecified for default settings.
**aaguid**: (optional) Authenticator Attestation GUID. This is a unique identifier for the passkey provider (device or authenticator type) and can be used to identify the type of passkey device used during registration or authentication.
- `authenticatorAttachment`: Specifies the type of authenticator
- `platform`: Authenticator is attached to the platform (e.g., fingerprint reader)
- `cross-platform`: Authenticator is not attached to the platform (e.g., security key)
- Default: `not set` (both platform and cross-platform allowed, with platform preferred)
- `residentKey`: Determines credential storage behavior.
- `required`: User MUST store credentials on the authenticator (highest security)
- `preferred`: Encourages credential storage but not mandatory
- `discouraged`: No credential storage required (fastest experience)
- Default: `preferred`
- `userVerification`: Controls biometric/PIN verification during authentication:
- `required`: User MUST verify identity (highest security)
- `preferred`: Verification encouraged but not mandatory
- `discouraged`: No verification required (fastest experience)
- Default: `preferred`
**advanced**: Advanced options
- `webAuthnChallengeCookie`: Cookie name for storing WebAuthn challenge ID during authentication flow (Default: `better-auth-passkey`)
@@ -8,6 +8,7 @@
"dependencies": {
"@better-auth/sso": "workspace:*",
"@better-auth/stripe": "workspace:*",
"@better-auth/passkey": "workspace:*",
"better-auth": "workspace:*",
"stripe": "^18.5.0"
}
+1 -1
View File
@@ -1,3 +1,4 @@
import { passkey } from "@better-auth/passkey";
import { sso } from "@better-auth/sso";
import { stripe } from "@better-auth/stripe";
import { betterAuth } from "better-auth";
@@ -15,7 +16,6 @@ import {
organization,
twoFactor,
} from "better-auth/plugins";
import { passkey } from "better-auth/plugins/passkey";
import { Stripe } from "stripe";
export const auth = betterAuth({
-10
View File
@@ -257,11 +257,6 @@
"types": "./dist/plugins/one-time-token/index.d.ts",
"default": "./dist/plugins/one-time-token/index.js"
},
"./plugins/passkey": {
"better-auth-dev-source": "./src/plugins/passkey/index.ts",
"types": "./dist/plugins/passkey/index.d.ts",
"default": "./dist/plugins/passkey/index.js"
},
"./plugins/phone-number": {
"better-auth-dev-source": "./src/plugins/phone-number/index.ts",
"types": "./dist/plugins/phone-number/index.d.ts",
@@ -416,9 +411,6 @@
"plugins/organization/access": [
"./dist/plugins/organization/access/index.d.ts"
],
"plugins/passkey": [
"./dist/plugins/passkey/index.d.ts"
],
"plugins/phone-number": [
"./dist/plugins/phone-number/index.d.ts"
],
@@ -443,8 +435,6 @@
"@better-fetch/fetch": "catalog:",
"@noble/ciphers": "^2.0.0",
"@noble/hashes": "^2.0.0",
"@simplewebauthn/browser": "^13.1.2",
"@simplewebauthn/server": "^13.1.2",
"@standard-schema/spec": "^1.0.0",
"better-call": "catalog:",
"defu": "^6.1.4",
@@ -8,7 +8,7 @@ import { describe, expect, expectTypeOf, it, vi } from "vitest";
import type { Ref } from "vue";
import { twoFactorClient } from "../plugins";
import type { Session, SessionQueryParams } from "../types";
import { organizationClient, passkeyClient } from "./plugins";
import { emailOTPClient, organizationClient } from "./plugins";
import { createAuthClient as createReactClient } from "./react";
import { createAuthClient as createSolidClient } from "./solid";
import { createAuthClient as createSvelteClient } from "./svelte";
@@ -278,7 +278,7 @@ describe("type", () => {
it("should infer session react", () => {
const client = createReactClient({
plugins: [organizationClient(), twoFactorClient(), passkeyClient()],
plugins: [organizationClient(), twoFactorClient(), emailOTPClient()],
});
const $infer = client.$Infer.Session;
expectTypeOf<typeof $infer.user>().toEqualTypeOf<{
@@ -1,4 +1,3 @@
export type * from "@simplewebauthn/server";
export * from "../../plugins/additional-fields/client";
export * from "../../plugins/admin/client";
export * from "../../plugins/anonymous/client";
@@ -15,7 +14,6 @@ export * from "../../plugins/oidc-provider/client";
export * from "../../plugins/one-tap/client";
export * from "../../plugins/one-time-token/client";
export * from "../../plugins/organization/client";
export * from "../../plugins/passkey/client";
export * from "../../plugins/phone-number/client";
export * from "../../plugins/siwe/client";
export * from "../../plugins/two-factor/client";
-1
View File
@@ -51,7 +51,6 @@ export default defineConfig({
"./src/plugins/oauth-proxy/index.ts",
"./src/plugins/organization/index.ts",
"./src/plugins/organization/access/index.ts",
"./src/plugins/passkey/index.ts",
"./src/plugins/phone-number/index.ts",
"./src/plugins/two-factor/index.ts",
"./src/plugins/username/index.ts",
+1
View File
@@ -34,6 +34,7 @@
"exports": "./dist/index.js",
"bin": "./dist/index.js",
"devDependencies": {
"@better-auth/passkey": "workspace:*",
"@types/semver": "^7.7.1",
"@types/better-sqlite3": "^7.6.13",
"@types/prompts": "^2.4.9",
+2 -2
View File
@@ -97,8 +97,8 @@ export const supportedPlugins = [
id: "passkey",
name: "passkey",
clientName: "passkeyClient",
path: `better-auth/plugins/passkey`,
clientPath: "better-auth/client/plugins",
path: `@better-auth/passkey`,
clientPath: "@better-auth/passkey/client",
},
{
id: "generic-oauth",
+1 -1
View File
@@ -1,7 +1,7 @@
import { passkey } from "@better-auth/passkey";
import type { BetterAuthOptions } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { twoFactor, username } from "better-auth/plugins";
import { passkey } from "better-auth/plugins/passkey";
import { describe, expect, it } from "vitest";
import { generateDrizzleSchema } from "../src/generators/drizzle";
+54
View File
@@ -0,0 +1,54 @@
# Better Auth Passkey Plugin
## Installation
```bash
# Using npm
npm install better-auth @better-auth/passkey
# Using yarn
yarn add better-auth @better-auth/passkey
# Using pnpm
pnpm add better-auth @better-auth/passkey
# Using bun
bun add better-auth @better-auth/passkey
```
## Usage
### Server
```typescript
import { betterAuth } from "better-auth";
import { passkey } from "@better-auth/passkey";
export const auth = betterAuth({
plugins: [
passkey({
rpID: "example.com",
rpName: "My App",
}),
],
});
```
### Client
```typescript
import { createAuthClient } from "better-auth/client";
import { passkeyClient } from "@better-auth/passkey/client";
export const authClient = createAuthClient({
plugins: [passkeyClient()],
});
```
## Documentation
For more information, visit the [Better Auth Passkey documentation](https://better-auth.com/docs/plugins/passkey).
## License
MIT
+73
View File
@@ -0,0 +1,73 @@
{
"name": "@better-auth/passkey",
"version": "1.4.0-beta.15",
"type": "module",
"description": "Passkey plugin for Better Auth",
"main": "dist/index.js",
"module": "dist/index.js",
"publishConfig": {
"access": "public"
},
"scripts": {
"test": "vitest",
"build": "tsdown",
"dev": "tsdown --watch",
"typecheck": "tsc --project tsconfig.json"
},
"exports": {
".": {
"better-auth-dev-source": "./src/index.ts",
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./client": {
"better-auth-dev-source": "./src/client.ts",
"types": "./dist/client.d.ts",
"default": "./dist/client.js"
}
},
"typesVersions": {
"*": {
"*": [
"./dist/index.d.ts"
],
"client": [
"./dist/client.d.ts"
]
}
},
"devDependencies": {
"@better-auth/core": "workspace:*",
"better-auth": "workspace:*",
"tsdown": "catalog:"
},
"dependencies": {
"@simplewebauthn/browser": "^13.1.2",
"@simplewebauthn/server": "^13.1.2",
"zod": "^4.1.5"
},
"peerDependencies": {
"nanostores": "^1.0.1",
"@better-auth/utils": "0.3.0",
"better-call": "catalog:",
"@better-fetch/fetch": "catalog:",
"@better-auth/core": "workspace:*",
"better-auth": "workspace:*"
},
"files": [
"dist"
],
"repository": {
"type": "git",
"url": "https://github.com/better-auth/better-auth",
"directory": "packages/passkey"
},
"homepage": "https://www.better-auth.com/docs/plugins/passkey",
"keywords": [
"auth",
"passkey",
"typescript",
"better-auth"
],
"license": "MIT"
}
@@ -9,10 +9,11 @@ import {
startRegistration,
WebAuthnError,
} from "@simplewebauthn/browser";
import { useAuthQuery } from "better-auth/client";
import type { Session, User } from "better-auth/types";
import { atom } from "nanostores";
import { useAuthQuery } from "../../client";
import type { Session, User } from "../../types";
import type { Passkey, passkey as passkeyPl } from ".";
import type { passkey } from ".";
import type { Passkey } from "./types";
export const getPasskeyActions = (
$fetch: BetterFetch,
@@ -216,7 +217,7 @@ export const passkeyClient = () => {
const $listPasskeys = atom<any>();
return {
id: "passkey",
$InferServerPlugin: {} as ReturnType<typeof passkeyPl>,
$InferServerPlugin: {} as ReturnType<typeof passkey>,
getActions: ($fetch, $store) =>
getPasskeyActions($fetch, {
$listPasskeys,
+12
View File
@@ -0,0 +1,12 @@
import { defineErrorCodes } from "@better-auth/core/utils";
export const PASSKEY_ERROR_CODES = defineErrorCodes({
CHALLENGE_NOT_FOUND: "Challenge not found",
YOU_ARE_NOT_ALLOWED_TO_REGISTER_THIS_PASSKEY:
"You are not allowed to register this passkey",
FAILED_TO_VERIFY_REGISTRATION: "Failed to verify registration",
PASSKEY_NOT_FOUND: "Passkey not found",
AUTHENTICATION_FAILED: "Authentication failed",
UNABLE_TO_CREATE_SESSION: "Unable to create session",
FAILED_TO_UPDATE_PASSKEY: "Failed to update passkey",
});
@@ -1,12 +1,9 @@
import type { BetterAuthPlugin } from "@better-auth/core";
import { createAuthEndpoint } from "@better-auth/core/api";
import type { BetterAuthPluginDBSchema } from "@better-auth/core/db";
import { defineErrorCodes } from "@better-auth/core/utils";
import { base64 } from "@better-auth/utils/base64";
import type {
AuthenticationResponseJSON,
AuthenticatorTransportFuture,
CredentialDeviceType,
PublicKeyCredentialCreationOptionsJSON,
} from "@simplewebauthn/server";
import {
@@ -15,97 +12,21 @@ import {
verifyAuthenticationResponse,
verifyRegistrationResponse,
} from "@simplewebauthn/server";
import { generateId } from "better-auth";
import {
freshSessionMiddleware,
getSessionFromCtx,
sessionMiddleware,
} from "better-auth/api";
import { setSessionCookie } from "better-auth/cookies";
import { generateRandomString } from "better-auth/crypto";
import { mergeSchema } from "better-auth/db";
import { APIError } from "better-call";
import * as z from "zod";
import { sessionMiddleware } from "../../api";
import { freshSessionMiddleware, getSessionFromCtx } from "../../api/routes";
import { setSessionCookie } from "../../cookies";
import { generateRandomString } from "../../crypto/random";
import { mergeSchema } from "../../db/schema";
import type { InferOptionSchema } from "../../types/plugins";
import { generateId } from "../../utils";
interface WebAuthnChallengeValue {
expectedChallenge: string;
userData: {
id: string;
};
}
const ERROR_CODES = defineErrorCodes({
CHALLENGE_NOT_FOUND: "Challenge not found",
YOU_ARE_NOT_ALLOWED_TO_REGISTER_THIS_PASSKEY:
"You are not allowed to register this passkey",
FAILED_TO_VERIFY_REGISTRATION: "Failed to verify registration",
PASSKEY_NOT_FOUND: "Passkey not found",
AUTHENTICATION_FAILED: "Authentication failed",
UNABLE_TO_CREATE_SESSION: "Unable to create session",
FAILED_TO_UPDATE_PASSKEY: "Failed to update passkey",
});
function getRpID(options: PasskeyOptions, baseURL?: string | undefined) {
return (
options.rpID || (baseURL ? new URL(baseURL).hostname : "localhost") // default rpID
);
}
export interface PasskeyOptions {
/**
* A unique identifier for your website. 'localhost' is okay for
* local dev
*
* @default "localhost"
*/
rpID?: string | undefined;
/**
* Human-readable title for your website
*
* @default "Better Auth"
*/
rpName?: string | undefined;
/**
* The URL at which registrations and authentications should occur.
* `http://localhost` and `http://localhost:PORT` are also valid.
* Do NOT include any trailing /
*
* if this isn't provided. The client itself will
* pass this value.
*/
origin?: (string | string[] | null) | undefined;
/**
* Allow customization of the authenticatorSelection options
* during passkey registration.
*/
authenticatorSelection?: AuthenticatorSelectionCriteria | undefined;
/**
* Advanced options
*/
advanced?:
| {
webAuthnChallengeCookie?: string;
}
| undefined;
/**
* Schema for the passkey model
*/
schema?: InferOptionSchema<typeof schema> | undefined;
}
export type Passkey = {
id: string;
name?: string | undefined;
publicKey: string;
userId: string;
credentialID: string;
counter: number;
deviceType: CredentialDeviceType;
backedUp: boolean;
transports?: string | undefined;
createdAt: Date;
aaguid?: string | undefined;
};
import { PASSKEY_ERROR_CODES } from "./error-codes";
import { schema } from "./schema";
import type { Passkey, PasskeyOptions, WebAuthnChallengeValue } from "./types";
import { getRpID } from "./utils";
export const passkey = (options?: PasskeyOptions | undefined) => {
const opts = {
@@ -531,7 +452,7 @@ export const passkey = (options?: PasskeyOptions | undefined) => {
);
if (!challengeId) {
throw new APIError("BAD_REQUEST", {
message: ERROR_CODES.CHALLENGE_NOT_FOUND,
message: PASSKEY_ERROR_CODES.CHALLENGE_NOT_FOUND,
});
}
@@ -550,7 +471,8 @@ export const passkey = (options?: PasskeyOptions | undefined) => {
if (userData.id !== ctx.context.session.user.id) {
throw new APIError("UNAUTHORIZED", {
message: ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_REGISTER_THIS_PASSKEY,
message:
PASSKEY_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_REGISTER_THIS_PASSKEY,
});
}
@@ -604,7 +526,7 @@ export const passkey = (options?: PasskeyOptions | undefined) => {
} catch (e) {
console.log(e);
throw new APIError("INTERNAL_SERVER_ERROR", {
message: ERROR_CODES.FAILED_TO_VERIFY_REGISTRATION,
message: PASSKEY_ERROR_CODES.FAILED_TO_VERIFY_REGISTRATION,
});
}
},
@@ -664,7 +586,7 @@ export const passkey = (options?: PasskeyOptions | undefined) => {
);
if (!challengeId) {
throw new APIError("BAD_REQUEST", {
message: ERROR_CODES.CHALLENGE_NOT_FOUND,
message: PASSKEY_ERROR_CODES.CHALLENGE_NOT_FOUND,
});
}
@@ -674,7 +596,7 @@ export const passkey = (options?: PasskeyOptions | undefined) => {
);
if (!data) {
throw new APIError("BAD_REQUEST", {
message: ERROR_CODES.CHALLENGE_NOT_FOUND,
message: PASSKEY_ERROR_CODES.CHALLENGE_NOT_FOUND,
});
}
const { expectedChallenge } = JSON.parse(
@@ -691,7 +613,7 @@ export const passkey = (options?: PasskeyOptions | undefined) => {
});
if (!passkey) {
throw new APIError("UNAUTHORIZED", {
message: ERROR_CODES.PASSKEY_NOT_FOUND,
message: PASSKEY_ERROR_CODES.PASSKEY_NOT_FOUND,
});
}
try {
@@ -713,7 +635,7 @@ export const passkey = (options?: PasskeyOptions | undefined) => {
const { verified } = verification;
if (!verified)
throw new APIError("UNAUTHORIZED", {
message: ERROR_CODES.AUTHENTICATION_FAILED,
message: PASSKEY_ERROR_CODES.AUTHENTICATION_FAILED,
});
await ctx.context.adapter.update<Passkey>({
@@ -733,7 +655,7 @@ export const passkey = (options?: PasskeyOptions | undefined) => {
);
if (!s) {
throw new APIError("INTERNAL_SERVER_ERROR", {
message: ERROR_CODES.UNABLE_TO_CREATE_SESSION,
message: PASSKEY_ERROR_CODES.UNABLE_TO_CREATE_SESSION,
});
}
const user = await ctx.context.internalAdapter.findUserById(
@@ -759,7 +681,7 @@ export const passkey = (options?: PasskeyOptions | undefined) => {
} catch (e) {
ctx.context.logger.error("Failed to verify authentication", e);
throw new APIError("BAD_REQUEST", {
message: ERROR_CODES.AUTHENTICATION_FAILED,
message: PASSKEY_ERROR_CODES.AUTHENTICATION_FAILED,
});
}
},
@@ -956,13 +878,14 @@ export const passkey = (options?: PasskeyOptions | undefined) => {
if (!passkey) {
throw new APIError("NOT_FOUND", {
message: ERROR_CODES.PASSKEY_NOT_FOUND,
message: PASSKEY_ERROR_CODES.PASSKEY_NOT_FOUND,
});
}
if (passkey.userId !== ctx.context.session.user.id) {
throw new APIError("UNAUTHORIZED", {
message: ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_REGISTER_THIS_PASSKEY,
message:
PASSKEY_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_REGISTER_THIS_PASSKEY,
});
}
@@ -981,7 +904,7 @@ export const passkey = (options?: PasskeyOptions | undefined) => {
if (!updatedPasskey) {
throw new APIError("INTERNAL_SERVER_ERROR", {
message: ERROR_CODES.FAILED_TO_UPDATE_PASSKEY,
message: PASSKEY_ERROR_CODES.FAILED_TO_UPDATE_PASSKEY,
});
}
return ctx.json(
@@ -996,57 +919,8 @@ export const passkey = (options?: PasskeyOptions | undefined) => {
),
},
schema: mergeSchema(schema, options?.schema),
$ERROR_CODES: ERROR_CODES,
$ERROR_CODES: PASSKEY_ERROR_CODES,
} satisfies BetterAuthPlugin;
};
const schema = {
passkey: {
fields: {
name: {
type: "string",
required: false,
},
publicKey: {
type: "string",
required: true,
},
userId: {
type: "string",
references: {
model: "user",
field: "id",
},
required: true,
},
credentialID: {
type: "string",
required: true,
},
counter: {
type: "number",
required: true,
},
deviceType: {
type: "string",
required: true,
},
backedUp: {
type: "boolean",
required: true,
},
transports: {
type: "string",
required: false,
},
createdAt: {
type: "date",
required: false,
},
aaguid: {
type: "string",
required: false,
},
},
},
} satisfies BetterAuthPluginDBSchema;
export type { Passkey, PasskeyOptions };
@@ -1,6 +1,6 @@
import { createAuthClient } from "better-auth/client";
import { getTestInstanceMemory as getTestInstance } from "better-auth/test";
import { describe, expect, it } from "vitest";
import { createAuthClient } from "../../client";
import { getTestInstance } from "../../test-utils/test-instance";
import { type Passkey, passkey } from ".";
import { passkeyClient } from "./client";
+52
View File
@@ -0,0 +1,52 @@
import type { BetterAuthPluginDBSchema } from "@better-auth/core/db";
export const schema = {
passkey: {
fields: {
name: {
type: "string",
required: false,
},
publicKey: {
type: "string",
required: true,
},
userId: {
type: "string",
references: {
model: "user",
field: "id",
},
required: true,
},
credentialID: {
type: "string",
required: true,
},
counter: {
type: "number",
required: true,
},
deviceType: {
type: "string",
required: true,
},
backedUp: {
type: "boolean",
required: true,
},
transports: {
type: "string",
required: false,
},
createdAt: {
type: "date",
required: false,
},
aaguid: {
type: "string",
required: false,
},
},
},
} satisfies BetterAuthPluginDBSchema;
+74
View File
@@ -0,0 +1,74 @@
import type { CredentialDeviceType } from "@simplewebauthn/server";
import type { InferOptionSchema } from "better-auth/types";
import type { schema } from "./schema";
/**
* @internal
*/
export interface WebAuthnChallengeValue {
expectedChallenge: string;
userData: {
id: string;
};
}
export interface PasskeyOptions {
/**
* A unique identifier for your website. 'localhost' is okay for
* local dev
*
* @default "localhost"
*/
rpID?: string | undefined;
/**
* Human-readable title for your website
*
* @default "Better Auth"
*/
rpName?: string | undefined;
/**
* The URL at which registrations and authentications should occur.
* `http://localhost` and `http://localhost:PORT` are also valid.
* Do NOT include any trailing /
*
* if this isn't provided. The client itself will
* pass this value.
*/
origin?: (string | string[] | null) | undefined;
/**
* Allow customization of the authenticatorSelection options
* during passkey registration.
*/
authenticatorSelection?: AuthenticatorSelectionCriteria | undefined;
/**
* Advanced options
*/
advanced?:
| {
/**
* Cookie name for storing WebAuthn challenge ID during authentication flow
*
* @default "better-auth-passkey"
*/
webAuthnChallengeCookie?: string;
}
| undefined;
/**
* Schema for the passkey model
*/
schema?: InferOptionSchema<typeof schema> | undefined;
}
export type Passkey = {
id: string;
name?: string | undefined;
publicKey: string;
userId: string;
credentialID: string;
counter: number;
deviceType: CredentialDeviceType;
backedUp: boolean;
transports?: string | undefined;
createdAt: Date;
aaguid?: string | undefined;
};
+7
View File
@@ -0,0 +1,7 @@
import type { PasskeyOptions } from "./types";
export function getRpID(options: PasskeyOptions, baseURL?: string | undefined) {
return (
options.rpID || (baseURL ? new URL(baseURL).hostname : "localhost") // default rpID
);
}
+15
View File
@@ -0,0 +1,15 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"lib": ["esnext", "dom", "dom.iterable"]
},
"include": ["./src"],
"references": [
{
"path": "../better-auth/tsconfig.json"
},
{
"path": "../core/tsconfig.json"
}
]
}
+16
View File
@@ -0,0 +1,16 @@
import { defineConfig } from "tsdown";
export default defineConfig({
dts: { build: true, incremental: true },
format: ["esm"],
entry: ["./src/index.ts", "./src/client.ts"],
external: [
"nanostores",
"@better-auth/utils",
"better-call",
"@better-fetch/fetch",
"@better-auth/core",
"better-auth",
],
treeshake: true,
});
+67 -33
View File
@@ -206,6 +206,9 @@ importers:
demo/nextjs:
dependencies:
'@better-auth/passkey':
specifier: workspace:*
version: link:../../packages/passkey
'@better-auth/sso':
specifier: workspace:*
version: link:../../packages/sso
@@ -352,7 +355,7 @@ importers:
version: 12.23.12(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
geist:
specifier: ^1.4.2
version: 1.4.2(next@16.0.1(@opentelemetry/api@1.9.0)(@playwright/test@1.55.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(sass@1.90.0))
version: 1.4.2(next@16.0.1(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.55.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(sass@1.90.0))
input-otp:
specifier: ^1.4.2
version: 1.4.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
@@ -639,7 +642,7 @@ importers:
version: 15.8.3(@oramacloud/client@2.1.4)(@tanstack/react-router@1.131.27(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(algoliasearch@5.36.0)(lucide-react@0.542.0(react@19.2.0))(next@16.0.1(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.55.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(sass@1.90.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(tailwindcss@4.1.13)
geist:
specifier: ^1.4.2
version: 1.4.2(next@16.0.1(@opentelemetry/api@1.9.0)(@playwright/test@1.55.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(sass@1.90.0))
version: 1.4.2(next@16.0.1(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.55.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(sass@1.90.0))
gray-matter:
specifier: ^4.0.3
version: 4.0.3
@@ -873,6 +876,9 @@ importers:
e2e/smoke/test/fixtures/tsconfig-declaration:
dependencies:
'@better-auth/passkey':
specifier: workspace:*
version: link:../../../../../packages/passkey
'@better-auth/sso':
specifier: workspace:*
version: link:../../../../../packages/sso
@@ -946,12 +952,6 @@ importers:
'@noble/hashes':
specifier: ^2.0.0
version: 2.0.0
'@simplewebauthn/browser':
specifier: ^13.1.2
version: 13.1.2
'@simplewebauthn/server':
specifier: ^13.1.2
version: 13.1.2
'@standard-schema/spec':
specifier: ^1.0.0
version: 1.0.0
@@ -1161,6 +1161,9 @@ importers:
specifier: ^4.1.5
version: 4.1.5
devDependencies:
'@better-auth/passkey':
specifier: workspace:*
version: link:../passkey
'@types/better-sqlite3':
specifier: ^7.6.13
version: 7.6.13
@@ -1263,6 +1266,40 @@ importers:
specifier: 'catalog:'
version: 0.15.11(typescript@5.9.3)
packages/passkey:
dependencies:
'@better-auth/utils':
specifier: 0.3.0
version: 0.3.0
'@better-fetch/fetch':
specifier: 'catalog:'
version: 1.1.18
'@simplewebauthn/browser':
specifier: ^13.1.2
version: 13.1.2
'@simplewebauthn/server':
specifier: ^13.1.2
version: 13.1.2
better-call:
specifier: 'catalog:'
version: 1.0.24
nanostores:
specifier: ^1.0.1
version: 1.0.1
zod:
specifier: ^4.1.5
version: 4.1.5
devDependencies:
'@better-auth/core':
specifier: workspace:*
version: link:../core
better-auth:
specifier: workspace:*
version: link:../better-auth
tsdown:
specifier: 'catalog:'
version: 0.15.11(typescript@5.9.3)
packages/sso:
dependencies:
'@better-fetch/fetch':
@@ -13695,7 +13732,7 @@ snapshots:
'@babel/helper-member-expression-to-functions@7.27.1':
dependencies:
'@babel/traverse': 7.28.4
'@babel/types': 7.28.4
'@babel/types': 7.28.5
transitivePeerDependencies:
- supports-color
@@ -13728,7 +13765,7 @@ snapshots:
'@babel/helper-optimise-call-expression@7.27.1':
dependencies:
'@babel/types': 7.28.4
'@babel/types': 7.28.5
'@babel/helper-plugin-utils@7.27.1': {}
@@ -15367,7 +15404,7 @@ snapshots:
postcss: 8.4.49
resolve-from: 5.0.0
optionalDependencies:
expo: 54.0.21(@babel/core@7.28.4)(@expo/metro-runtime@6.1.2)(expo-router@6.0.14)(graphql@16.11.0)(react-native@0.80.2(@babel/core@7.28.4)(@react-native-community/cli@20.0.1(typescript@5.9.3))(@types/react@19.2.2)(react@19.2.0))(react@19.2.0)
expo: 54.0.21(@babel/core@7.28.4)(@expo/metro-runtime@6.1.2)(expo-router@6.0.14)(graphql@16.11.0)(react-native@0.81.5(@babel/core@7.28.4)(@react-native-community/cli@20.0.1(typescript@5.9.3))(@react-native/metro-config@0.81.0(@babel/core@7.28.4))(@types/react@19.2.2)(react@19.2.0))(react@19.2.0)
transitivePeerDependencies:
- bufferutil
- supports-color
@@ -15452,7 +15489,7 @@ snapshots:
'@expo/json-file': 10.0.7
'@react-native/normalize-colors': 0.81.5
debug: 4.4.3
expo: 54.0.21(@babel/core@7.28.4)(@expo/metro-runtime@6.1.2)(expo-router@6.0.14)(graphql@16.11.0)(react-native@0.80.2(@babel/core@7.28.4)(@react-native-community/cli@20.0.1(typescript@5.9.3))(@types/react@19.2.2)(react@19.2.0))(react@19.2.0)
expo: 54.0.21(@babel/core@7.28.4)(@expo/metro-runtime@6.1.2)(expo-router@6.0.14)(graphql@16.11.0)(react-native@0.81.5(@babel/core@7.28.4)(@react-native-community/cli@20.0.1(typescript@5.9.3))(@react-native/metro-config@0.81.0(@babel/core@7.28.4))(@types/react@19.2.2)(react@19.2.0))(react@19.2.0)
resolve-from: 5.0.0
semver: 7.7.3
xml2js: 0.6.0
@@ -17676,9 +17713,7 @@ snapshots:
metro-runtime: 0.83.3
transitivePeerDependencies:
- '@babel/core'
- bufferutil
- supports-color
- utf-8-validate
optional: true
'@react-native/normalize-colors@0.74.89': {}
@@ -17960,7 +17995,7 @@ snapshots:
estree-walker: 2.0.2
fdir: 6.5.0(picomatch@4.0.3)
is-reference: 1.2.1
magic-string: 0.30.19
magic-string: 0.30.21
picomatch: 4.0.3
optionalDependencies:
rollup: 4.52.5
@@ -17969,7 +18004,7 @@ snapshots:
dependencies:
'@rollup/pluginutils': 5.2.0(rollup@4.52.5)
estree-walker: 2.0.2
magic-string: 0.30.19
magic-string: 0.30.21
optionalDependencies:
rollup: 4.52.5
@@ -17992,7 +18027,7 @@ snapshots:
'@rollup/plugin-replace@6.0.2(rollup@4.52.5)':
dependencies:
'@rollup/pluginutils': 5.2.0(rollup@4.52.5)
magic-string: 0.30.19
magic-string: 0.30.21
optionalDependencies:
rollup: 4.52.5
@@ -18630,7 +18665,7 @@ snapshots:
'@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.4)
'@babel/template': 7.27.2
'@babel/traverse': 7.28.4
'@babel/types': 7.28.4
'@babel/types': 7.28.5
'@tanstack/directive-functions-plugin': 1.131.2(vite@7.1.11(@types/node@24.9.2)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.30.1)(sass@1.90.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))
babel-dead-code-elimination: 1.0.10
tiny-invariant: 1.3.3
@@ -18658,7 +18693,7 @@ snapshots:
dependencies:
'@babel/code-frame': 7.26.2
'@babel/core': 7.28.4
'@babel/types': 7.28.4
'@babel/types': 7.28.5
'@tanstack/router-core': 1.131.27
'@tanstack/router-generator': 1.131.27
'@tanstack/router-plugin': 1.131.27(@tanstack/react-router@1.131.27(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite-plugin-solid@2.11.8(solid-js@1.9.9)(vite@7.1.11(@types/node@24.9.2)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.30.1)(sass@1.90.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(vite@7.1.11(@types/node@24.9.2)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.30.1)(sass@1.90.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))
@@ -19222,7 +19257,7 @@ snapshots:
'@vue/compiler-core@3.5.19':
dependencies:
'@babel/parser': 7.28.4
'@babel/parser': 7.28.5
'@vue/shared': 3.5.19
entities: 4.5.0
estree-walker: 2.0.2
@@ -19669,7 +19704,7 @@ snapshots:
resolve-from: 5.0.0
optionalDependencies:
'@babel/runtime': 7.28.4
expo: 54.0.21(@babel/core@7.28.4)(@expo/metro-runtime@6.1.2)(expo-router@6.0.14)(graphql@16.11.0)(react-native@0.80.2(@babel/core@7.28.4)(@react-native-community/cli@20.0.1(typescript@5.9.3))(@types/react@19.2.2)(react@19.2.0))(react@19.2.0)
expo: 54.0.21(@babel/core@7.28.4)(@expo/metro-runtime@6.1.2)(expo-router@6.0.14)(graphql@16.11.0)(react-native@0.81.5(@babel/core@7.28.4)(@react-native-community/cli@20.0.1(typescript@5.9.3))(@react-native/metro-config@0.81.0(@babel/core@7.28.4))(@types/react@19.2.2)(react@19.2.0))(react@19.2.0)
transitivePeerDependencies:
- '@babel/core'
- supports-color
@@ -19716,7 +19751,7 @@ snapshots:
'@better-auth/utils': 0.3.0
'@better-fetch/fetch': 1.1.18
rou3: 0.5.1
set-cookie-parser: 2.7.1
set-cookie-parser: 2.7.2
uncrypto: 0.1.3
better-opn@3.0.2:
@@ -21159,7 +21194,7 @@ snapshots:
expo-keep-awake@15.0.7(expo@54.0.21)(react@19.2.0):
dependencies:
expo: 54.0.21(@babel/core@7.28.4)(@expo/metro-runtime@6.1.2)(expo-router@6.0.14)(graphql@16.11.0)(react-native@0.80.2(@babel/core@7.28.4)(@react-native-community/cli@20.0.1(typescript@5.9.3))(@types/react@19.2.2)(react@19.2.0))(react@19.2.0)
expo: 54.0.21(@babel/core@7.28.4)(@expo/metro-runtime@6.1.2)(expo-router@6.0.14)(graphql@16.11.0)(react-native@0.81.5(@babel/core@7.28.4)(@react-native-community/cli@20.0.1(typescript@5.9.3))(@react-native/metro-config@0.81.0(@babel/core@7.28.4))(@types/react@19.2.2)(react@19.2.0))(react@19.2.0)
react: 19.2.0
expo-linking@7.1.7(expo@54.0.21)(react-native@0.80.2(@babel/core@7.28.4)(@react-native-community/cli@20.0.1(typescript@5.9.3))(@types/react@19.2.2)(react@19.2.0))(react@19.2.0):
@@ -21811,7 +21846,7 @@ snapshots:
function-bind@1.1.2: {}
geist@1.4.2(next@16.0.1(@opentelemetry/api@1.9.0)(@playwright/test@1.55.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(sass@1.90.0)):
geist@1.4.2(next@16.0.1(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.55.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(sass@1.90.0)):
dependencies:
next: 16.0.1(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.55.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(sass@1.90.0)
@@ -22947,8 +22982,8 @@ snapshots:
magicast@0.2.11:
dependencies:
'@babel/parser': 7.28.4
'@babel/types': 7.28.4
'@babel/parser': 7.28.5
'@babel/types': 7.28.5
recast: 0.23.11
magicast@0.3.5:
@@ -23567,11 +23602,11 @@ snapshots:
dependencies:
'@babel/code-frame': 7.27.1
'@babel/core': 7.28.4
'@babel/generator': 7.28.3
'@babel/parser': 7.28.4
'@babel/generator': 7.28.5
'@babel/parser': 7.28.5
'@babel/template': 7.27.2
'@babel/traverse': 7.28.4
'@babel/types': 7.28.4
'@babel/types': 7.28.5
accepts: 1.3.8
chalk: 4.1.2
ci-info: 2.0.0
@@ -26145,8 +26180,7 @@ snapshots:
set-cookie-parser@2.7.1: {}
set-cookie-parser@2.7.2:
optional: true
set-cookie-parser@2.7.2: {}
setimmediate@1.0.5: {}
@@ -26982,7 +27016,7 @@ snapshots:
escape-string-regexp: 5.0.0
estree-walker: 3.0.3
local-pkg: 1.1.2
magic-string: 0.30.19
magic-string: 0.30.21
mlly: 1.7.4
pathe: 2.0.3
picomatch: 4.0.3
@@ -27105,7 +27139,7 @@ snapshots:
unwasm@0.3.11:
dependencies:
knitwork: 1.2.0
magic-string: 0.30.19
magic-string: 0.30.21
mlly: 1.7.4
pathe: 2.0.3
pkg-types: 2.3.0
+3
View File
@@ -18,6 +18,9 @@
{
"path": "./packages/expo/tsconfig.json"
},
{
"path": "./packages/passkey/tsconfig.json"
},
{
"path": "./packages/sso/tsconfig.json"
},