@better-auth/passkey 1.7.0-beta.3 → 1.7.0-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/client.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { a as Passkey, c as PasskeyOptions, d as WebAuthnChallengeValue, l as PasskeyRegistrationOptions, n as PASSKEY_ERROR_CODES, o as PasskeyAuthenticationOptions, s as PasskeyExtensionsResolver, t as passkey, u as PasskeyRegistrationUser } from "./index-Ci52vhOT.mjs";
|
|
2
2
|
import { AuthenticationExtensionsClientInputs, AuthenticationExtensionsClientOutputs, AuthenticationResponseJSON, RegistrationResponseJSON } from "@simplewebauthn/server";
|
|
3
3
|
import * as better_auth_client0 from "better-auth/client";
|
|
4
4
|
import * as nanostores from "nanostores";
|
package/dist/client.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as PASSKEY_ERROR_CODES, t as PACKAGE_VERSION } from "./version-
|
|
1
|
+
import { n as PASSKEY_ERROR_CODES, t as PACKAGE_VERSION } from "./version-tMqL0F6x.mjs";
|
|
2
2
|
import { WebAuthnError, startAuthentication, startRegistration } from "@simplewebauthn/browser";
|
|
3
3
|
import { useAuthQuery } from "better-auth/client";
|
|
4
4
|
import { atom } from "nanostores";
|
|
@@ -100,7 +100,13 @@ interface PasskeyRegistrationOptions {
|
|
|
100
100
|
}) => Awaitable<PasskeyRegistrationUser>) | undefined;
|
|
101
101
|
/**
|
|
102
102
|
* Callback after a successful registration verification.
|
|
103
|
-
* Useful for user linking or
|
|
103
|
+
* Useful for user linking, auditing, or labeling the passkey.
|
|
104
|
+
*
|
|
105
|
+
* Return `userId` to attribute the passkey to a different user. Return `name`
|
|
106
|
+
* to set the stored label when the client did not provide one; the AAGUID is
|
|
107
|
+
* available via `verification.registrationInfo?.aaguid`. A non-empty
|
|
108
|
+
* client-supplied name always takes precedence over the returned `name`;
|
|
109
|
+
* whitespace-only input is treated as absent.
|
|
104
110
|
*/
|
|
105
111
|
afterVerification?: ((args: {
|
|
106
112
|
ctx: GenericEndpointContext;
|
|
@@ -110,6 +116,7 @@ interface PasskeyRegistrationOptions {
|
|
|
110
116
|
context?: string | null | undefined;
|
|
111
117
|
}) => Awaitable<{
|
|
112
118
|
userId?: string;
|
|
119
|
+
name?: string;
|
|
113
120
|
} | void>) | undefined;
|
|
114
121
|
/**
|
|
115
122
|
* Optional WebAuthn extensions to include in registration options.
|
|
@@ -196,6 +203,30 @@ type Passkey = {
|
|
|
196
203
|
aaguid?: string | undefined;
|
|
197
204
|
};
|
|
198
205
|
//#endregion
|
|
206
|
+
//#region src/authenticator-metadata.d.ts
|
|
207
|
+
/**
|
|
208
|
+
* Best-effort map of common authenticator AAGUIDs to a human-readable provider
|
|
209
|
+
* name, for labeling passkeys in management UIs.
|
|
210
|
+
*
|
|
211
|
+
* An AAGUID identifies an authenticator *model* (not a device or a user) and is
|
|
212
|
+
* present only in the registration response. Better Auth stores it on every
|
|
213
|
+
* passkey row and returns it from `listPasskeys`, so a display label can be
|
|
214
|
+
* resolved wherever passkeys are rendered.
|
|
215
|
+
*
|
|
216
|
+
* This list is intentionally small and not authoritative. Many authenticators
|
|
217
|
+
* are missing, and privacy-preserving platforms report an all-zero AAGUID
|
|
218
|
+
* (`00000000-0000-0000-0000-000000000000`) that matches nothing here. Notably,
|
|
219
|
+
* Apple devices zero the AAGUID under the default `attestation: "none"` flow, so
|
|
220
|
+
* the Apple entries below only appear in attested or managed contexts. For full
|
|
221
|
+
* coverage, resolve against the community-maintained source instead:
|
|
222
|
+
*
|
|
223
|
+
* - https://github.com/passkeydeveloper/passkey-authenticator-aaguids
|
|
224
|
+
*
|
|
225
|
+
* Names mirror that source verbatim.
|
|
226
|
+
*/
|
|
227
|
+
declare const commonAuthenticatorNames: Record<string, string>;
|
|
228
|
+
declare const getAuthenticatorName: (aaguid: string | null | undefined) => string | undefined;
|
|
229
|
+
//#endregion
|
|
199
230
|
//#region src/error-codes.d.ts
|
|
200
231
|
declare const PASSKEY_ERROR_CODES: {
|
|
201
232
|
CHALLENGE_NOT_FOUND: better_auth0.RawError<"CHALLENGE_NOT_FOUND">;
|
|
@@ -817,4 +848,4 @@ declare const passkey: (options?: PasskeyOptions | undefined) => {
|
|
|
817
848
|
options: PasskeyOptions | undefined;
|
|
818
849
|
};
|
|
819
850
|
//#endregion
|
|
820
|
-
export {
|
|
851
|
+
export { Passkey as a, PasskeyOptions as c, WebAuthnChallengeValue as d, getAuthenticatorName as i, PasskeyRegistrationOptions as l, PASSKEY_ERROR_CODES as n, PasskeyAuthenticationOptions as o, commonAuthenticatorNames as r, PasskeyExtensionsResolver as s, passkey as t, PasskeyRegistrationUser as u };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export { PASSKEY_ERROR_CODES, Passkey, PasskeyOptions, passkey };
|
|
1
|
+
import { a as Passkey, c as PasskeyOptions, i as getAuthenticatorName, n as PASSKEY_ERROR_CODES, r as commonAuthenticatorNames, t as passkey } from "./index-Ci52vhOT.mjs";
|
|
2
|
+
export { PASSKEY_ERROR_CODES, Passkey, PasskeyOptions, commonAuthenticatorNames, getAuthenticatorName, passkey };
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as PASSKEY_ERROR_CODES, t as PACKAGE_VERSION } from "./version-
|
|
1
|
+
import { n as PASSKEY_ERROR_CODES, t as PACKAGE_VERSION } from "./version-tMqL0F6x.mjs";
|
|
2
2
|
import { mergeSchema } from "better-auth/db";
|
|
3
3
|
import { createAuthEndpoint } from "@better-auth/core/api";
|
|
4
4
|
import { APIError } from "@better-auth/core/error";
|
|
@@ -287,7 +287,7 @@ const generatePasskeyAuthenticationOptions = (opts, { maxAgeInSeconds }) => crea
|
|
|
287
287
|
});
|
|
288
288
|
const verifyPasskeyRegistrationBodySchema = z.object({
|
|
289
289
|
response: z.any(),
|
|
290
|
-
name: z.string().meta({ description: "Name of the passkey" }).optional()
|
|
290
|
+
name: z.string().trim().meta({ description: "Name of the passkey" }).optional()
|
|
291
291
|
});
|
|
292
292
|
const verifyPasskeyRegistration = (options) => {
|
|
293
293
|
const requireSession = options.registration?.requireSession ?? true;
|
|
@@ -313,7 +313,7 @@ const verifyPasskeyRegistration = (options) => {
|
|
|
313
313
|
const webAuthnCookie = ctx.context.createAuthCookie(options.advanced.webAuthnChallengeCookie);
|
|
314
314
|
const verificationToken = await ctx.getSignedCookie(webAuthnCookie.name, ctx.context.secret);
|
|
315
315
|
if (!verificationToken) throw APIError.from("BAD_REQUEST", PASSKEY_ERROR_CODES.CHALLENGE_NOT_FOUND);
|
|
316
|
-
const data = await ctx.context.internalAdapter.
|
|
316
|
+
const data = await ctx.context.internalAdapter.consumeVerificationValue(verificationToken);
|
|
317
317
|
if (!data) throw APIError.from("BAD_REQUEST", PASSKEY_ERROR_CODES.CHALLENGE_NOT_FOUND);
|
|
318
318
|
const { expectedChallenge, userData, context } = JSON.parse(data.value);
|
|
319
319
|
const session = requireSession ? ctx.context.session : await getSessionFromCtx(ctx);
|
|
@@ -335,6 +335,7 @@ const verifyPasskeyRegistration = (options) => {
|
|
|
335
335
|
displayName: userData.displayName
|
|
336
336
|
};
|
|
337
337
|
let targetUserId = resolvedUser.id;
|
|
338
|
+
let resolvedName = ctx.body.name || void 0;
|
|
338
339
|
if (options.registration?.afterVerification) {
|
|
339
340
|
const result = await options.registration.afterVerification({
|
|
340
341
|
ctx,
|
|
@@ -348,16 +349,17 @@ const verifyPasskeyRegistration = (options) => {
|
|
|
348
349
|
if (session?.user?.id && result.userId !== session.user.id) throw APIError.from("UNAUTHORIZED", PASSKEY_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_REGISTER_THIS_PASSKEY);
|
|
349
350
|
targetUserId = result.userId;
|
|
350
351
|
}
|
|
352
|
+
if (!resolvedName) resolvedName = result?.name?.trim() || void 0;
|
|
351
353
|
}
|
|
352
354
|
const pubKey = base64.encode(credential.publicKey);
|
|
353
355
|
const newPasskey = {
|
|
354
|
-
name:
|
|
356
|
+
name: resolvedName,
|
|
355
357
|
userId: targetUserId,
|
|
356
358
|
credentialID: credential.id,
|
|
357
359
|
publicKey: pubKey,
|
|
358
360
|
counter: credential.counter,
|
|
359
361
|
deviceType: credentialDeviceType,
|
|
360
|
-
transports: resp.response.transports
|
|
362
|
+
transports: resp.response.transports?.join(",") ?? "",
|
|
361
363
|
backedUp: credentialBackedUp,
|
|
362
364
|
createdAt: /* @__PURE__ */ new Date(),
|
|
363
365
|
aaguid
|
|
@@ -366,9 +368,9 @@ const verifyPasskeyRegistration = (options) => {
|
|
|
366
368
|
model: "passkey",
|
|
367
369
|
data: newPasskey
|
|
368
370
|
});
|
|
369
|
-
await ctx.context.internalAdapter.deleteVerificationByIdentifier(verificationToken);
|
|
370
371
|
return ctx.json(newPasskeyRes, { status: 200 });
|
|
371
372
|
} catch (e) {
|
|
373
|
+
if (e instanceof APIError) throw e;
|
|
372
374
|
ctx.context.logger.error("Failed to verify registration", e);
|
|
373
375
|
throw APIError.from("INTERNAL_SERVER_ERROR", PASSKEY_ERROR_CODES.FAILED_TO_VERIFY_REGISTRATION);
|
|
374
376
|
}
|
|
@@ -402,7 +404,7 @@ const verifyPasskeyAuthentication = (options) => createAuthEndpoint("/passkey/ve
|
|
|
402
404
|
const webAuthnCookie = ctx.context.createAuthCookie(options.advanced.webAuthnChallengeCookie);
|
|
403
405
|
const verificationToken = await ctx.getSignedCookie(webAuthnCookie.name, ctx.context.secret);
|
|
404
406
|
if (!verificationToken) throw APIError.from("BAD_REQUEST", PASSKEY_ERROR_CODES.CHALLENGE_NOT_FOUND);
|
|
405
|
-
const data = await ctx.context.internalAdapter.
|
|
407
|
+
const data = await ctx.context.internalAdapter.consumeVerificationValue(verificationToken);
|
|
406
408
|
if (!data) throw APIError.from("BAD_REQUEST", PASSKEY_ERROR_CODES.CHALLENGE_NOT_FOUND);
|
|
407
409
|
const { expectedChallenge } = JSON.parse(data.value);
|
|
408
410
|
const passkey = await ctx.context.adapter.findOne({
|
|
@@ -450,12 +452,12 @@ const verifyPasskeyAuthentication = (options) => createAuthEndpoint("/passkey/ve
|
|
|
450
452
|
session: s,
|
|
451
453
|
user
|
|
452
454
|
});
|
|
453
|
-
await ctx.context.internalAdapter.deleteVerificationByIdentifier(verificationToken);
|
|
454
455
|
return ctx.json({
|
|
455
456
|
session: s,
|
|
456
457
|
user
|
|
457
458
|
}, { status: 200 });
|
|
458
459
|
} catch (e) {
|
|
460
|
+
if (e instanceof APIError) throw e;
|
|
459
461
|
ctx.context.logger.error("Failed to verify authentication", e);
|
|
460
462
|
throw APIError.from("BAD_REQUEST", PASSKEY_ERROR_CODES.AUTHENTICATION_FAILED);
|
|
461
463
|
}
|
|
@@ -576,7 +578,7 @@ const updatePasskey = createAuthEndpoint("/passkey/update-passkey", {
|
|
|
576
578
|
method: "POST",
|
|
577
579
|
body: z.object({
|
|
578
580
|
id: z.string().meta({ description: `The ID of the passkey which will be updated. Eg: \"passkey-id\"` }),
|
|
579
|
-
name: z.string().meta({ description: `The new name which the passkey will be updated to. Eg: \"my-new-passkey-name\"` })
|
|
581
|
+
name: z.string().trim().min(1).meta({ description: `The new name which the passkey will be updated to. Eg: \"my-new-passkey-name\"` })
|
|
580
582
|
}),
|
|
581
583
|
use: [sessionMiddleware, requireResourceOwnership({
|
|
582
584
|
model: "passkey",
|
|
@@ -660,6 +662,63 @@ const schema = { passkey: { fields: {
|
|
|
660
662
|
}
|
|
661
663
|
} } };
|
|
662
664
|
//#endregion
|
|
665
|
+
//#region src/authenticator-metadata.ts
|
|
666
|
+
/**
|
|
667
|
+
* Best-effort map of common authenticator AAGUIDs to a human-readable provider
|
|
668
|
+
* name, for labeling passkeys in management UIs.
|
|
669
|
+
*
|
|
670
|
+
* An AAGUID identifies an authenticator *model* (not a device or a user) and is
|
|
671
|
+
* present only in the registration response. Better Auth stores it on every
|
|
672
|
+
* passkey row and returns it from `listPasskeys`, so a display label can be
|
|
673
|
+
* resolved wherever passkeys are rendered.
|
|
674
|
+
*
|
|
675
|
+
* This list is intentionally small and not authoritative. Many authenticators
|
|
676
|
+
* are missing, and privacy-preserving platforms report an all-zero AAGUID
|
|
677
|
+
* (`00000000-0000-0000-0000-000000000000`) that matches nothing here. Notably,
|
|
678
|
+
* Apple devices zero the AAGUID under the default `attestation: "none"` flow, so
|
|
679
|
+
* the Apple entries below only appear in attested or managed contexts. For full
|
|
680
|
+
* coverage, resolve against the community-maintained source instead:
|
|
681
|
+
*
|
|
682
|
+
* - https://github.com/passkeydeveloper/passkey-authenticator-aaguids
|
|
683
|
+
*
|
|
684
|
+
* Names mirror that source verbatim.
|
|
685
|
+
*/
|
|
686
|
+
const commonAuthenticatorNames = {
|
|
687
|
+
"ea9b8d66-4d01-1d21-3ce4-b6b48cb575d4": "Google Password Manager",
|
|
688
|
+
"fbfc3007-154e-4ecc-8c0b-6e020557d7bd": "Apple Passwords",
|
|
689
|
+
"dd4ec289-e01d-41c9-bb89-70fa845d4bf2": "iCloud Keychain (Managed)",
|
|
690
|
+
"08987058-cadc-4b81-b6e1-30de50dcbe96": "Windows Hello",
|
|
691
|
+
"9ddd1817-af5a-4672-a2b9-3e3dd95000a9": "Windows Hello",
|
|
692
|
+
"6028b017-b1d4-4c02-b4b3-afcdafc96bb2": "Windows Hello",
|
|
693
|
+
"bada5566-a7aa-401f-bd96-45619a55120d": "1Password",
|
|
694
|
+
"d548826e-79b4-db40-a3d8-11116f7e8349": "Bitwarden",
|
|
695
|
+
"531126d6-e717-415c-9320-3d9aa6981239": "Dashlane",
|
|
696
|
+
"b78a0a55-6ef8-d246-a042-ba0f6d55050c": "LastPass",
|
|
697
|
+
"b84e4048-15dc-4dd0-8640-f4f60813c8af": "NordPass",
|
|
698
|
+
"50726f74-6f6e-5061-7373-50726f746f6e": "Proton Pass",
|
|
699
|
+
"0ea242b4-43c4-4a1b-8b17-dd6d0b6baec6": "Keeper",
|
|
700
|
+
"53414d53-554e-4700-0000-000000000000": "Samsung Pass"
|
|
701
|
+
};
|
|
702
|
+
/**
|
|
703
|
+
* Resolve a best-effort provider name for an authenticator AAGUID.
|
|
704
|
+
*
|
|
705
|
+
* Returns `undefined` when the AAGUID is unknown, empty, or the all-zero value
|
|
706
|
+
* reported by privacy-preserving platforms. Casing and surrounding whitespace
|
|
707
|
+
* are normalized before lookup.
|
|
708
|
+
*
|
|
709
|
+
* @example
|
|
710
|
+
* ```ts
|
|
711
|
+
* const label = passkey.name || getAuthenticatorName(passkey.aaguid) || "Passkey";
|
|
712
|
+
* ```
|
|
713
|
+
*/
|
|
714
|
+
const ANONYMOUS_AAGUID = "00000000-0000-0000-0000-000000000000";
|
|
715
|
+
const getAuthenticatorName = (aaguid) => {
|
|
716
|
+
const normalized = aaguid?.trim().toLowerCase();
|
|
717
|
+
if (!normalized || normalized === ANONYMOUS_AAGUID) return void 0;
|
|
718
|
+
const name = commonAuthenticatorNames[normalized];
|
|
719
|
+
return typeof name === "string" ? name : void 0;
|
|
720
|
+
};
|
|
721
|
+
//#endregion
|
|
663
722
|
//#region src/index.ts
|
|
664
723
|
const MAX_AGE_IN_SECONDS = 300;
|
|
665
724
|
const passkey = (options) => {
|
|
@@ -689,4 +748,4 @@ const passkey = (options) => {
|
|
|
689
748
|
};
|
|
690
749
|
};
|
|
691
750
|
//#endregion
|
|
692
|
-
export { PASSKEY_ERROR_CODES, passkey };
|
|
751
|
+
export { PASSKEY_ERROR_CODES, commonAuthenticatorNames, getAuthenticatorName, passkey };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@better-auth/passkey",
|
|
3
|
-
"version": "1.7.0-beta.
|
|
3
|
+
"version": "1.7.0-beta.5",
|
|
4
4
|
"description": "Passkey plugin for Better Auth",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -55,16 +55,16 @@
|
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
57
|
"tsdown": "0.21.1",
|
|
58
|
-
"@better-auth/core": "1.7.0-beta.
|
|
59
|
-
"better-auth": "1.7.0-beta.
|
|
58
|
+
"@better-auth/core": "1.7.0-beta.5",
|
|
59
|
+
"better-auth": "1.7.0-beta.5"
|
|
60
60
|
},
|
|
61
61
|
"peerDependencies": {
|
|
62
|
-
"@better-auth/utils": "0.4.
|
|
63
|
-
"@better-fetch/fetch": "1.
|
|
64
|
-
"better-call": "1.3.
|
|
62
|
+
"@better-auth/utils": "0.4.1",
|
|
63
|
+
"@better-fetch/fetch": "1.2.2",
|
|
64
|
+
"better-call": "1.3.6",
|
|
65
65
|
"nanostores": "^1.0.1",
|
|
66
|
-
"@better-auth/core": "^1.7.0-beta.
|
|
67
|
-
"better-auth": "^1.7.0-beta.
|
|
66
|
+
"@better-auth/core": "^1.7.0-beta.5",
|
|
67
|
+
"better-auth": "^1.7.0-beta.5"
|
|
68
68
|
},
|
|
69
69
|
"scripts": {
|
|
70
70
|
"build": "tsdown",
|