@better-auth/passkey 1.5.6 → 1.6.0-beta.0

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/index.d.mts CHANGED
@@ -1,2 +1,2 @@
1
- import { i as PasskeyOptions, n as PASSKEY_ERROR_CODES, r as Passkey, t as passkey } from "./index-B6ZC0I-g.mjs";
1
+ import { n as PASSKEY_ERROR_CODES, o as PasskeyOptions, r as Passkey, t as passkey } from "./index-BzKpmgHh.mjs";
2
2
  export { PASSKEY_ERROR_CODES, Passkey, PasskeyOptions, passkey };
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { t as PASSKEY_ERROR_CODES } from "./error-codes-DJf-1Ecu.mjs";
1
+ import { n as PASSKEY_ERROR_CODES, t as PACKAGE_VERSION } from "./version-B7zkjZSd.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";
@@ -8,144 +8,189 @@ import { freshSessionMiddleware, getSessionFromCtx, sessionMiddleware } from "be
8
8
  import { setSessionCookie } from "better-auth/cookies";
9
9
  import { generateRandomString } from "better-auth/crypto";
10
10
  import * as z from "zod";
11
-
12
11
  //#region src/utils.ts
13
12
  function getRpID(options, baseURL) {
14
13
  return options.rpID || (baseURL ? new URL(baseURL).hostname : "localhost");
15
14
  }
16
-
17
15
  //#endregion
18
16
  //#region src/routes.ts
17
+ const resolveExtensions = async (extensions, ctx) => {
18
+ if (!extensions) return;
19
+ if (typeof extensions === "function") return await extensions({ ctx });
20
+ return extensions;
21
+ };
22
+ const resolveRegistrationUser = async (opts, ctx) => {
23
+ if (opts.registration?.requireSession ?? true) {
24
+ const session = ctx.context?.session;
25
+ if (!session?.user?.id) throw APIError.from("UNAUTHORIZED", PASSKEY_ERROR_CODES.SESSION_REQUIRED);
26
+ const sessionName = session.user.email || session.user.id;
27
+ return {
28
+ id: session.user.id,
29
+ name: sessionName,
30
+ displayName: sessionName
31
+ };
32
+ }
33
+ const session = await getSessionFromCtx(ctx);
34
+ if (session?.user?.id) {
35
+ const sessionName = session.user.email || session.user.id;
36
+ return {
37
+ id: session.user.id,
38
+ name: sessionName,
39
+ displayName: sessionName
40
+ };
41
+ }
42
+ if (!opts.registration?.resolveUser) throw APIError.from("BAD_REQUEST", PASSKEY_ERROR_CODES.RESOLVE_USER_REQUIRED);
43
+ const resolvedUser = await opts.registration.resolveUser({
44
+ ctx,
45
+ context: ctx.query?.context ?? null
46
+ });
47
+ if (!resolvedUser?.id || !resolvedUser?.name) throw APIError.from("BAD_REQUEST", PASSKEY_ERROR_CODES.RESOLVED_USER_INVALID);
48
+ return resolvedUser;
49
+ };
19
50
  const generatePasskeyQuerySchema = z.object({
20
51
  authenticatorAttachment: z.enum(["platform", "cross-platform"]).optional(),
21
- name: z.string().optional()
52
+ name: z.string().optional(),
53
+ context: z.string().optional()
22
54
  }).optional();
23
- const generatePasskeyRegistrationOptions = (opts, { maxAgeInSeconds }) => createAuthEndpoint("/passkey/generate-register-options", {
24
- method: "GET",
25
- use: [freshSessionMiddleware],
26
- query: generatePasskeyQuerySchema,
27
- metadata: { openapi: {
28
- operationId: "generatePasskeyRegistrationOptions",
29
- description: "Generate registration options for a new passkey",
30
- responses: { 200: {
31
- description: "Success",
32
- parameters: { query: {
33
- authenticatorAttachment: {
34
- description: `Type of authenticator to use for registration.
55
+ const generatePasskeyRegistrationOptions = (opts, { maxAgeInSeconds }) => {
56
+ return createAuthEndpoint("/passkey/generate-register-options", {
57
+ method: "GET",
58
+ use: opts.registration?.requireSession ?? true ? [freshSessionMiddleware] : void 0,
59
+ query: generatePasskeyQuerySchema,
60
+ metadata: { openapi: {
61
+ operationId: "generatePasskeyRegistrationOptions",
62
+ description: "Generate registration options for a new passkey",
63
+ responses: { 200: {
64
+ description: "Success",
65
+ parameters: { query: {
66
+ authenticatorAttachment: {
67
+ description: `Type of authenticator to use for registration.
35
68
  "platform" for device-specific authenticators,
36
69
  "cross-platform" for authenticators that can be used across devices.`,
37
- required: false
38
- },
39
- name: {
40
- description: `Optional custom name for the passkey.
41
- This can help identify the passkey when managing multiple credentials.`,
42
- required: false
43
- }
44
- } },
45
- content: { "application/json": { schema: {
46
- type: "object",
47
- properties: {
48
- challenge: { type: "string" },
49
- rp: {
50
- type: "object",
51
- properties: {
52
- name: { type: "string" },
53
- id: { type: "string" }
54
- }
70
+ required: false
55
71
  },
56
- user: {
57
- type: "object",
58
- properties: {
59
- id: { type: "string" },
60
- name: { type: "string" },
61
- displayName: { type: "string" }
62
- }
72
+ name: {
73
+ description: `Optional custom name for the passkey.
74
+ This can help identify the passkey when managing multiple credentials.`,
75
+ required: false
63
76
  },
64
- pubKeyCredParams: {
65
- type: "array",
66
- items: {
77
+ context: {
78
+ description: "Optional context for passkey-first registration flows.",
79
+ required: false
80
+ }
81
+ } },
82
+ content: { "application/json": { schema: {
83
+ type: "object",
84
+ properties: {
85
+ challenge: { type: "string" },
86
+ rp: {
67
87
  type: "object",
68
88
  properties: {
69
- type: { type: "string" },
70
- alg: { type: "number" }
89
+ name: { type: "string" },
90
+ id: { type: "string" }
71
91
  }
72
- }
73
- },
74
- timeout: { type: "number" },
75
- excludeCredentials: {
76
- type: "array",
77
- items: {
92
+ },
93
+ user: {
78
94
  type: "object",
79
95
  properties: {
80
96
  id: { type: "string" },
81
- type: { type: "string" },
82
- transports: {
83
- type: "array",
84
- items: { type: "string" }
97
+ name: { type: "string" },
98
+ displayName: { type: "string" }
99
+ }
100
+ },
101
+ pubKeyCredParams: {
102
+ type: "array",
103
+ items: {
104
+ type: "object",
105
+ properties: {
106
+ type: { type: "string" },
107
+ alg: { type: "number" }
85
108
  }
86
109
  }
87
- }
88
- },
89
- authenticatorSelection: {
90
- type: "object",
91
- properties: {
92
- authenticatorAttachment: { type: "string" },
93
- requireResidentKey: { type: "boolean" },
94
- userVerification: { type: "string" }
95
- }
96
- },
97
- attestation: { type: "string" },
98
- extensions: { type: "object" }
99
- }
100
- } } }
110
+ },
111
+ timeout: { type: "number" },
112
+ excludeCredentials: {
113
+ type: "array",
114
+ items: {
115
+ type: "object",
116
+ properties: {
117
+ id: { type: "string" },
118
+ type: { type: "string" },
119
+ transports: {
120
+ type: "array",
121
+ items: { type: "string" }
122
+ }
123
+ }
124
+ }
125
+ },
126
+ authenticatorSelection: {
127
+ type: "object",
128
+ properties: {
129
+ authenticatorAttachment: { type: "string" },
130
+ requireResidentKey: { type: "boolean" },
131
+ userVerification: { type: "string" }
132
+ }
133
+ },
134
+ attestation: { type: "string" },
135
+ extensions: { type: "object" }
136
+ }
137
+ } } }
138
+ } }
101
139
  } }
102
- } }
103
- }, async (ctx) => {
104
- const { session } = ctx.context;
105
- const userPasskeys = await ctx.context.adapter.findMany({
106
- model: "passkey",
107
- where: [{
108
- field: "userId",
109
- value: session.user.id
110
- }]
111
- });
112
- const userID = new TextEncoder().encode(generateRandomString(32, "a-z", "0-9"));
113
- const baseURLString = typeof ctx.context.options.baseURL === "string" ? ctx.context.options.baseURL : void 0;
114
- const options = await generateRegistrationOptions({
115
- rpName: opts.rpName || ctx.context.appName,
116
- rpID: getRpID(opts, baseURLString),
117
- userID,
118
- userName: ctx.query?.name || session.user.email || session.user.id,
119
- userDisplayName: session.user.email || session.user.id,
120
- attestationType: "none",
121
- excludeCredentials: userPasskeys.map((passkey) => ({
122
- id: passkey.credentialID,
123
- transports: passkey.transports?.split(",")
124
- })),
125
- authenticatorSelection: {
126
- residentKey: "preferred",
127
- userVerification: "preferred",
128
- ...opts.authenticatorSelection || {},
129
- ...ctx.query?.authenticatorAttachment ? { authenticatorAttachment: ctx.query.authenticatorAttachment } : {}
130
- }
131
- });
132
- const verificationToken = generateRandomString(32);
133
- const webAuthnCookie = ctx.context.createAuthCookie(opts.advanced.webAuthnChallengeCookie);
134
- await ctx.setSignedCookie(webAuthnCookie.name, verificationToken, ctx.context.secret, {
135
- ...webAuthnCookie.attributes,
136
- maxAge: maxAgeInSeconds
137
- });
138
- const expirationTime = new Date(Date.now() + maxAgeInSeconds * 1e3);
139
- await ctx.context.internalAdapter.createVerificationValue({
140
- identifier: verificationToken,
141
- value: JSON.stringify({
142
- expectedChallenge: options.challenge,
143
- userData: { id: session.user.id }
144
- }),
145
- expiresAt: expirationTime
140
+ }, async (ctx) => {
141
+ const user = await resolveRegistrationUser(opts, ctx);
142
+ const userPasskeys = await ctx.context.adapter.findMany({
143
+ model: "passkey",
144
+ where: [{
145
+ field: "userId",
146
+ value: user.id
147
+ }]
148
+ });
149
+ const registrationExtensions = await resolveExtensions(opts.registration?.extensions, ctx);
150
+ const userID = new TextEncoder().encode(generateRandomString(32, "a-z", "0-9"));
151
+ const baseURLString = typeof ctx.context.options.baseURL === "string" ? ctx.context.options.baseURL : void 0;
152
+ const options = await generateRegistrationOptions({
153
+ rpName: opts.rpName || ctx.context.appName,
154
+ rpID: getRpID(opts, baseURLString),
155
+ userID,
156
+ userName: ctx.query?.name || user.name || user.id,
157
+ userDisplayName: user.displayName || user.name || user.id,
158
+ attestationType: "none",
159
+ excludeCredentials: userPasskeys.map((passkey) => ({
160
+ id: passkey.credentialID,
161
+ transports: passkey.transports?.split(",")
162
+ })),
163
+ authenticatorSelection: {
164
+ residentKey: "preferred",
165
+ userVerification: "preferred",
166
+ ...opts.authenticatorSelection || {},
167
+ ...ctx.query?.authenticatorAttachment ? { authenticatorAttachment: ctx.query.authenticatorAttachment } : {}
168
+ },
169
+ extensions: registrationExtensions
170
+ });
171
+ const verificationToken = generateRandomString(32);
172
+ const webAuthnCookie = ctx.context.createAuthCookie(opts.advanced.webAuthnChallengeCookie);
173
+ await ctx.setSignedCookie(webAuthnCookie.name, verificationToken, ctx.context.secret, {
174
+ ...webAuthnCookie.attributes,
175
+ maxAge: maxAgeInSeconds
176
+ });
177
+ const expirationTime = new Date(Date.now() + maxAgeInSeconds * 1e3);
178
+ await ctx.context.internalAdapter.createVerificationValue({
179
+ identifier: verificationToken,
180
+ value: JSON.stringify({
181
+ expectedChallenge: options.challenge,
182
+ userData: {
183
+ id: user.id,
184
+ name: user.name,
185
+ displayName: user.displayName
186
+ },
187
+ context: ctx.query?.context ?? null
188
+ }),
189
+ expiresAt: expirationTime
190
+ });
191
+ return ctx.json(options, { status: 200 });
146
192
  });
147
- return ctx.json(options, { status: 200 });
148
- });
193
+ };
149
194
  const generatePasskeyAuthenticationOptions = (opts, { maxAgeInSeconds }) => createAuthEndpoint("/passkey/generate-authenticate-options", {
150
195
  method: "GET",
151
196
  metadata: { openapi: {
@@ -211,9 +256,12 @@ const generatePasskeyAuthenticationOptions = (opts, { maxAgeInSeconds }) => crea
211
256
  value: session.user.id
212
257
  }]
213
258
  });
259
+ const baseURLString = typeof ctx.context.options.baseURL === "string" ? ctx.context.options.baseURL : void 0;
260
+ const authenticationExtensions = await resolveExtensions(opts.authentication?.extensions, ctx);
214
261
  const options = await generateAuthenticationOptions({
215
- rpID: getRpID(opts, typeof ctx.context.options.baseURL === "string" ? ctx.context.options.baseURL : void 0),
262
+ rpID: getRpID(opts, baseURLString),
216
263
  userVerification: "preferred",
264
+ extensions: authenticationExtensions,
217
265
  ...userPasskeys.length ? { allowCredentials: userPasskeys.map((passkey) => ({
218
266
  id: passkey.credentialID,
219
267
  transports: passkey.transports?.split(",")
@@ -241,66 +289,91 @@ const verifyPasskeyRegistrationBodySchema = z.object({
241
289
  response: z.any(),
242
290
  name: z.string().meta({ description: "Name of the passkey" }).optional()
243
291
  });
244
- const verifyPasskeyRegistration = (options) => createAuthEndpoint("/passkey/verify-registration", {
245
- method: "POST",
246
- body: verifyPasskeyRegistrationBodySchema,
247
- use: [freshSessionMiddleware],
248
- metadata: { openapi: {
249
- operationId: "passkeyVerifyRegistration",
250
- description: "Verify registration of a new passkey",
251
- responses: {
252
- 200: {
253
- description: "Success",
254
- content: { "application/json": { schema: { $ref: "#/components/schemas/Passkey" } } }
255
- },
256
- 400: { description: "Bad request" }
292
+ const verifyPasskeyRegistration = (options) => {
293
+ const requireSession = options.registration?.requireSession ?? true;
294
+ return createAuthEndpoint("/passkey/verify-registration", {
295
+ method: "POST",
296
+ body: verifyPasskeyRegistrationBodySchema,
297
+ use: requireSession ? [freshSessionMiddleware] : void 0,
298
+ metadata: { openapi: {
299
+ operationId: "passkeyVerifyRegistration",
300
+ description: "Verify registration of a new passkey",
301
+ responses: {
302
+ 200: {
303
+ description: "Success",
304
+ content: { "application/json": { schema: { $ref: "#/components/schemas/Passkey" } } }
305
+ },
306
+ 400: { description: "Bad request" }
307
+ }
308
+ } }
309
+ }, async (ctx) => {
310
+ const origin = options?.origin || ctx.headers?.get("origin") || "";
311
+ if (!origin) throw APIError.from("BAD_REQUEST", PASSKEY_ERROR_CODES.FAILED_TO_VERIFY_REGISTRATION);
312
+ const resp = ctx.body.response;
313
+ const webAuthnCookie = ctx.context.createAuthCookie(options.advanced.webAuthnChallengeCookie);
314
+ const verificationToken = await ctx.getSignedCookie(webAuthnCookie.name, ctx.context.secret);
315
+ if (!verificationToken) throw APIError.from("BAD_REQUEST", PASSKEY_ERROR_CODES.CHALLENGE_NOT_FOUND);
316
+ const data = await ctx.context.internalAdapter.findVerificationValue(verificationToken);
317
+ if (!data) throw APIError.from("BAD_REQUEST", PASSKEY_ERROR_CODES.CHALLENGE_NOT_FOUND);
318
+ const { expectedChallenge, userData, context } = JSON.parse(data.value);
319
+ const session = requireSession ? ctx.context.session : await getSessionFromCtx(ctx);
320
+ if (session?.user?.id && userData.id !== session.user.id) throw APIError.from("UNAUTHORIZED", PASSKEY_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_REGISTER_THIS_PASSKEY);
321
+ try {
322
+ const verification = await verifyRegistrationResponse({
323
+ response: resp,
324
+ expectedChallenge,
325
+ expectedOrigin: origin,
326
+ expectedRPID: getRpID(options, typeof ctx.context.options.baseURL === "string" ? ctx.context.options.baseURL : void 0),
327
+ requireUserVerification: false
328
+ });
329
+ const { verified, registrationInfo } = verification;
330
+ if (!verified || !registrationInfo) throw APIError.from("BAD_REQUEST", PASSKEY_ERROR_CODES.FAILED_TO_VERIFY_REGISTRATION);
331
+ const { aaguid, credentialDeviceType, credentialBackedUp, credential } = registrationInfo;
332
+ const resolvedUser = {
333
+ id: userData.id,
334
+ name: userData.name || userData.id,
335
+ displayName: userData.displayName
336
+ };
337
+ let targetUserId = resolvedUser.id;
338
+ if (options.registration?.afterVerification) {
339
+ const result = await options.registration.afterVerification({
340
+ ctx,
341
+ verification,
342
+ user: resolvedUser,
343
+ clientData: resp,
344
+ context
345
+ });
346
+ if (result?.userId) {
347
+ if (typeof result.userId !== "string" || !result.userId) throw APIError.from("BAD_REQUEST", PASSKEY_ERROR_CODES.RESOLVED_USER_INVALID);
348
+ 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
+ targetUserId = result.userId;
350
+ }
351
+ }
352
+ const pubKey = base64.encode(credential.publicKey);
353
+ const newPasskey = {
354
+ name: ctx.body.name,
355
+ userId: targetUserId,
356
+ credentialID: credential.id,
357
+ publicKey: pubKey,
358
+ counter: credential.counter,
359
+ deviceType: credentialDeviceType,
360
+ transports: resp.response.transports.join(","),
361
+ backedUp: credentialBackedUp,
362
+ createdAt: /* @__PURE__ */ new Date(),
363
+ aaguid
364
+ };
365
+ const newPasskeyRes = await ctx.context.adapter.create({
366
+ model: "passkey",
367
+ data: newPasskey
368
+ });
369
+ await ctx.context.internalAdapter.deleteVerificationByIdentifier(verificationToken);
370
+ return ctx.json(newPasskeyRes, { status: 200 });
371
+ } catch (e) {
372
+ ctx.context.logger.error("Failed to verify registration", e);
373
+ throw APIError.from("INTERNAL_SERVER_ERROR", PASSKEY_ERROR_CODES.FAILED_TO_VERIFY_REGISTRATION);
257
374
  }
258
- } }
259
- }, async (ctx) => {
260
- const origin = options?.origin || ctx.headers?.get("origin") || "";
261
- if (!origin) throw APIError.from("BAD_REQUEST", PASSKEY_ERROR_CODES.FAILED_TO_VERIFY_REGISTRATION);
262
- const resp = ctx.body.response;
263
- const webAuthnCookie = ctx.context.createAuthCookie(options.advanced.webAuthnChallengeCookie);
264
- const verificationToken = await ctx.getSignedCookie(webAuthnCookie.name, ctx.context.secret);
265
- if (!verificationToken) throw APIError.from("BAD_REQUEST", PASSKEY_ERROR_CODES.CHALLENGE_NOT_FOUND);
266
- const data = await ctx.context.internalAdapter.findVerificationValue(verificationToken);
267
- if (!data) throw APIError.from("BAD_REQUEST", PASSKEY_ERROR_CODES.CHALLENGE_NOT_FOUND);
268
- const { expectedChallenge, userData } = JSON.parse(data.value);
269
- if (userData.id !== ctx.context.session.user.id) throw APIError.from("UNAUTHORIZED", PASSKEY_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_REGISTER_THIS_PASSKEY);
270
- try {
271
- const { verified, registrationInfo } = await verifyRegistrationResponse({
272
- response: resp,
273
- expectedChallenge,
274
- expectedOrigin: origin,
275
- expectedRPID: getRpID(options, typeof ctx.context.options.baseURL === "string" ? ctx.context.options.baseURL : void 0),
276
- requireUserVerification: false
277
- });
278
- if (!verified || !registrationInfo) throw APIError.from("BAD_REQUEST", PASSKEY_ERROR_CODES.FAILED_TO_VERIFY_REGISTRATION);
279
- const { aaguid, credentialDeviceType, credentialBackedUp, credential } = registrationInfo;
280
- const pubKey = base64.encode(credential.publicKey);
281
- const newPasskey = {
282
- name: ctx.body.name,
283
- userId: userData.id,
284
- credentialID: credential.id,
285
- publicKey: pubKey,
286
- counter: credential.counter,
287
- deviceType: credentialDeviceType,
288
- transports: resp.response.transports.join(","),
289
- backedUp: credentialBackedUp,
290
- createdAt: /* @__PURE__ */ new Date(),
291
- aaguid
292
- };
293
- const newPasskeyRes = await ctx.context.adapter.create({
294
- model: "passkey",
295
- data: newPasskey
296
- });
297
- await ctx.context.internalAdapter.deleteVerificationByIdentifier(verificationToken);
298
- return ctx.json(newPasskeyRes, { status: 200 });
299
- } catch (e) {
300
- ctx.context.logger.error("Failed to verify registration", e);
301
- throw APIError.from("INTERNAL_SERVER_ERROR", PASSKEY_ERROR_CODES.FAILED_TO_VERIFY_REGISTRATION);
302
- }
303
- });
375
+ });
376
+ };
304
377
  const verifyPasskeyAuthenticationBodySchema = z.object({ response: z.record(z.any(), z.any()) });
305
378
  const verifyPasskeyAuthentication = (options) => createAuthEndpoint("/passkey/verify-authentication", {
306
379
  method: "POST",
@@ -356,6 +429,11 @@ const verifyPasskeyAuthentication = (options) => createAuthEndpoint("/passkey/ve
356
429
  });
357
430
  const { verified } = verification;
358
431
  if (!verified) throw APIError.from("UNAUTHORIZED", PASSKEY_ERROR_CODES.AUTHENTICATION_FAILED);
432
+ if (options.authentication?.afterVerification) await options.authentication.afterVerification({
433
+ ctx,
434
+ verification,
435
+ clientData: resp
436
+ });
359
437
  await ctx.context.adapter.update({
360
438
  model: "passkey",
361
439
  where: [{
@@ -427,7 +505,6 @@ const listPasskeys = createAuthEndpoint("/passkey/list-user-passkeys", {
427
505
  });
428
506
  return ctx.json(passkeys, { status: 200 });
429
507
  });
430
- const deletePasskeyBodySchema = z.object({ id: z.string().meta({ description: "The ID of the passkey to delete. Eg: \"some-passkey-id\"" }) });
431
508
  /**
432
509
  * ### Endpoint
433
510
  *
@@ -445,7 +522,7 @@ const deletePasskeyBodySchema = z.object({ id: z.string().meta({ description: "T
445
522
  */
446
523
  const deletePasskey = createAuthEndpoint("/passkey/delete-passkey", {
447
524
  method: "POST",
448
- body: deletePasskeyBodySchema,
525
+ body: z.object({ id: z.string().meta({ description: "The ID of the passkey to delete. Eg: \"some-passkey-id\"" }) }),
449
526
  use: [sessionMiddleware],
450
527
  metadata: { openapi: {
451
528
  description: "Delete a specific passkey",
@@ -480,10 +557,6 @@ const deletePasskey = createAuthEndpoint("/passkey/delete-passkey", {
480
557
  });
481
558
  return ctx.json({ status: true });
482
559
  });
483
- const updatePassKeyBodySchema = z.object({
484
- id: z.string().meta({ description: `The ID of the passkey which will be updated. Eg: \"passkey-id\"` }),
485
- name: z.string().meta({ description: `The new name which the passkey will be updated to. Eg: \"my-new-passkey-name\"` })
486
- });
487
560
  /**
488
561
  * ### Endpoint
489
562
  *
@@ -501,7 +574,10 @@ const updatePassKeyBodySchema = z.object({
501
574
  */
502
575
  const updatePasskey = createAuthEndpoint("/passkey/update-passkey", {
503
576
  method: "POST",
504
- body: updatePassKeyBodySchema,
577
+ body: z.object({
578
+ 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\"` })
580
+ }),
505
581
  use: [sessionMiddleware],
506
582
  metadata: { openapi: {
507
583
  description: "Update a specific passkey's name",
@@ -535,7 +611,6 @@ const updatePasskey = createAuthEndpoint("/passkey/update-passkey", {
535
611
  if (!updatedPasskey) throw APIError.from("INTERNAL_SERVER_ERROR", PASSKEY_ERROR_CODES.FAILED_TO_UPDATE_PASSKEY);
536
612
  return ctx.json({ passkey: updatedPasskey }, { status: 200 });
537
613
  });
538
-
539
614
  //#endregion
540
615
  //#region src/schema.ts
541
616
  const schema = { passkey: { fields: {
@@ -586,7 +661,6 @@ const schema = { passkey: { fields: {
586
661
  required: false
587
662
  }
588
663
  } } };
589
-
590
664
  //#endregion
591
665
  //#region src/index.ts
592
666
  const MAX_AGE_IN_SECONDS = 300;
@@ -601,6 +675,7 @@ const passkey = (options) => {
601
675
  };
602
676
  return {
603
677
  id: "passkey",
678
+ version: PACKAGE_VERSION,
604
679
  endpoints: {
605
680
  generatePasskeyRegistrationOptions: generatePasskeyRegistrationOptions(opts, { maxAgeInSeconds: MAX_AGE_IN_SECONDS }),
606
681
  generatePasskeyAuthenticationOptions: generatePasskeyAuthenticationOptions(opts, { maxAgeInSeconds: MAX_AGE_IN_SECONDS }),
@@ -615,7 +690,5 @@ const passkey = (options) => {
615
690
  options
616
691
  };
617
692
  };
618
-
619
693
  //#endregion
620
694
  export { PASSKEY_ERROR_CODES, passkey };
621
- //# sourceMappingURL=index.mjs.map
@@ -1,5 +1,4 @@
1
1
  import { defineErrorCodes } from "@better-auth/core/utils/error-codes";
2
-
3
2
  //#region src/error-codes.ts
4
3
  const PASSKEY_ERROR_CODES = defineErrorCodes({
5
4
  CHALLENGE_NOT_FOUND: "Challenge not found",
@@ -12,9 +11,13 @@ const PASSKEY_ERROR_CODES = defineErrorCodes({
12
11
  PREVIOUSLY_REGISTERED: "Previously registered",
13
12
  REGISTRATION_CANCELLED: "Registration cancelled",
14
13
  AUTH_CANCELLED: "Auth cancelled",
15
- UNKNOWN_ERROR: "Unknown error"
14
+ UNKNOWN_ERROR: "Unknown error",
15
+ SESSION_REQUIRED: "Passkey registration requires an authenticated session",
16
+ RESOLVE_USER_REQUIRED: "Passkey registration requires either an authenticated session or a resolveUser callback when requireSession is false",
17
+ RESOLVED_USER_INVALID: "Resolved user is invalid"
16
18
  });
17
-
18
19
  //#endregion
19
- export { PASSKEY_ERROR_CODES as t };
20
- //# sourceMappingURL=error-codes-DJf-1Ecu.mjs.map
20
+ //#region src/version.ts
21
+ const PACKAGE_VERSION = "1.6.0-beta.0";
22
+ //#endregion
23
+ export { PASSKEY_ERROR_CODES as n, PACKAGE_VERSION as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@better-auth/passkey",
3
- "version": "1.5.6",
3
+ "version": "1.6.0-beta.0",
4
4
  "description": "Passkey plugin for Better Auth",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -19,6 +19,7 @@
19
19
  "publishConfig": {
20
20
  "access": "public"
21
21
  },
22
+ "sideEffects": false,
22
23
  "files": [
23
24
  "dist"
24
25
  ],
@@ -53,22 +54,22 @@
53
54
  "zod": "^4.3.6"
54
55
  },
55
56
  "devDependencies": {
56
- "tsdown": "0.21.0-beta.2",
57
- "@better-auth/core": "1.5.6",
58
- "better-auth": "1.5.6"
57
+ "tsdown": "0.21.1",
58
+ "@better-auth/core": "1.6.0-beta.0",
59
+ "better-auth": "1.6.0-beta.0"
59
60
  },
60
61
  "peerDependencies": {
61
- "@better-auth/utils": "0.3.1",
62
+ "@better-auth/utils": "0.4.0",
62
63
  "@better-fetch/fetch": "1.1.21",
63
- "better-call": "1.3.2",
64
+ "better-call": "2.0.3",
64
65
  "nanostores": "^1.0.1",
65
- "@better-auth/core": "1.5.6",
66
- "better-auth": "1.5.6"
66
+ "@better-auth/core": "^1.6.0-beta.0",
67
+ "better-auth": "^1.6.0-beta.0"
67
68
  },
68
69
  "scripts": {
69
70
  "build": "tsdown",
70
71
  "dev": "tsdown --watch",
71
- "lint:package": "publint run --strict",
72
+ "lint:package": "publint run --strict --pack false",
72
73
  "lint:types": "attw --profile esm-only --pack .",
73
74
  "typecheck": "tsc --project tsconfig.json",
74
75
  "test": "vitest",
@@ -1 +0,0 @@
1
- {"version":3,"file":"client.mjs","names":[],"sources":["../src/client.ts"],"sourcesContent":["import type {\n\tBetterAuthClientPlugin,\n\tClientFetchOption,\n\tClientStore,\n} from \"@better-auth/core\";\nimport type { BetterFetch } from \"@better-fetch/fetch\";\nimport type {\n\tPublicKeyCredentialCreationOptionsJSON,\n\tPublicKeyCredentialRequestOptionsJSON,\n} from \"@simplewebauthn/browser\";\nimport {\n\tstartAuthentication,\n\tstartRegistration,\n\tWebAuthnError,\n} from \"@simplewebauthn/browser\";\nimport { useAuthQuery } from \"better-auth/client\";\nimport type { Session, User } from \"better-auth/types\";\nimport { atom } from \"nanostores\";\nimport type { passkey } from \".\";\nimport { PASSKEY_ERROR_CODES } from \"./error-codes\";\nimport type { Passkey } from \"./types\";\n\nexport const getPasskeyActions = (\n\t$fetch: BetterFetch,\n\t{\n\t\t$listPasskeys,\n\t\t$store,\n\t}: {\n\t\t$listPasskeys: ReturnType<typeof atom<any>>;\n\t\t$store: ClientStore;\n\t},\n) => {\n\tconst signInPasskey = async (\n\t\topts?:\n\t\t\t| {\n\t\t\t\t\tautoFill?: boolean;\n\t\t\t\t\tfetchOptions?: ClientFetchOption;\n\t\t\t }\n\t\t\t| undefined,\n\t\toptions?: ClientFetchOption | undefined,\n\t) => {\n\t\tconst response = await $fetch<PublicKeyCredentialRequestOptionsJSON>(\n\t\t\t\"/passkey/generate-authenticate-options\",\n\t\t\t{\n\t\t\t\tmethod: \"GET\",\n\t\t\t\tthrow: false,\n\t\t\t},\n\t\t);\n\t\tif (!response.data) {\n\t\t\treturn response;\n\t\t}\n\t\ttry {\n\t\t\tconst res = await startAuthentication({\n\t\t\t\toptionsJSON: response.data,\n\t\t\t\tuseBrowserAutofill: opts?.autoFill,\n\t\t\t});\n\t\t\tconst verified = await $fetch<{\n\t\t\t\tsession: Session;\n\t\t\t\tuser: User;\n\t\t\t}>(\"/passkey/verify-authentication\", {\n\t\t\t\tbody: {\n\t\t\t\t\tresponse: res,\n\t\t\t\t},\n\t\t\t\t...opts?.fetchOptions,\n\t\t\t\t...options,\n\t\t\t\tmethod: \"POST\",\n\t\t\t\tthrow: false,\n\t\t\t});\n\t\t\t$listPasskeys.set(Math.random());\n\t\t\t$store.notify(\"$sessionSignal\");\n\n\t\t\treturn verified;\n\t\t} catch (err) {\n\t\t\t// Error logs ran on the front-end\n\t\t\tconsole.error(`[Better Auth] Error verifying passkey`, err);\n\t\t\treturn {\n\t\t\t\tdata: null,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"AUTH_CANCELLED\",\n\t\t\t\t\tmessage: PASSKEY_ERROR_CODES.AUTH_CANCELLED,\n\t\t\t\t\tstatus: 400,\n\t\t\t\t\tstatusText: \"BAD_REQUEST\",\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t};\n\n\tconst registerPasskey = async (\n\t\topts?:\n\t\t\t| {\n\t\t\t\t\tfetchOptions?: ClientFetchOption;\n\t\t\t\t\t/**\n\t\t\t\t\t * The name of the passkey. This is used to\n\t\t\t\t\t * identify the passkey in the UI.\n\t\t\t\t\t */\n\t\t\t\t\tname?: string;\n\n\t\t\t\t\t/**\n\t\t\t\t\t * The type of attachment for the passkey. Defaults to both\n\t\t\t\t\t * platform and cross-platform allowed, with platform preferred.\n\t\t\t\t\t */\n\t\t\t\t\tauthenticatorAttachment?: \"platform\" | \"cross-platform\";\n\n\t\t\t\t\t/**\n\t\t\t\t\t * Try to silently create a passkey with the password manager that the user just signed\n\t\t\t\t\t * in with.\n\t\t\t\t\t * @default false\n\t\t\t\t\t */\n\t\t\t\t\tuseAutoRegister?: boolean;\n\t\t\t }\n\t\t\t| undefined,\n\t\tfetchOpts?: ClientFetchOption | undefined,\n\t) => {\n\t\tconst options = await $fetch<PublicKeyCredentialCreationOptionsJSON>(\n\t\t\t\"/passkey/generate-register-options\",\n\t\t\t{\n\t\t\t\tmethod: \"GET\",\n\t\t\t\tquery: {\n\t\t\t\t\t...(opts?.authenticatorAttachment && {\n\t\t\t\t\t\tauthenticatorAttachment: opts.authenticatorAttachment,\n\t\t\t\t\t}),\n\t\t\t\t\t...(opts?.name && {\n\t\t\t\t\t\tname: opts.name,\n\t\t\t\t\t}),\n\t\t\t\t},\n\t\t\t\tthrow: false,\n\t\t\t},\n\t\t);\n\n\t\tif (!options.data) {\n\t\t\treturn options;\n\t\t}\n\t\ttry {\n\t\t\tconst res = await startRegistration({\n\t\t\t\toptionsJSON: options.data,\n\t\t\t\tuseAutoRegister: opts?.useAutoRegister,\n\t\t\t});\n\t\t\tconst verified = await $fetch<Passkey>(\"/passkey/verify-registration\", {\n\t\t\t\t...opts?.fetchOptions,\n\t\t\t\t...fetchOpts,\n\t\t\t\tbody: {\n\t\t\t\t\tresponse: res,\n\t\t\t\t\tname: opts?.name,\n\t\t\t\t},\n\t\t\t\tmethod: \"POST\",\n\t\t\t\tthrow: false,\n\t\t\t});\n\n\t\t\tif (!verified.data) {\n\t\t\t\treturn verified;\n\t\t\t}\n\t\t\t$listPasskeys.set(Math.random());\n\t\t\treturn verified;\n\t\t} catch (e) {\n\t\t\tif (e instanceof WebAuthnError) {\n\t\t\t\tif (e.code === \"ERROR_AUTHENTICATOR_PREVIOUSLY_REGISTERED\") {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tdata: null,\n\t\t\t\t\t\terror: {\n\t\t\t\t\t\t\tcode: e.code,\n\t\t\t\t\t\t\tmessage: PASSKEY_ERROR_CODES.PREVIOUSLY_REGISTERED,\n\t\t\t\t\t\t\tstatus: 400,\n\t\t\t\t\t\t\tstatusText: \"BAD_REQUEST\",\n\t\t\t\t\t\t},\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t\tif (e.code === \"ERROR_CEREMONY_ABORTED\") {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tdata: null,\n\t\t\t\t\t\terror: {\n\t\t\t\t\t\t\tcode: e.code,\n\t\t\t\t\t\t\tmessage: PASSKEY_ERROR_CODES.REGISTRATION_CANCELLED,\n\t\t\t\t\t\t\tstatus: 400,\n\t\t\t\t\t\t\tstatusText: \"BAD_REQUEST\",\n\t\t\t\t\t\t},\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t\treturn {\n\t\t\t\t\tdata: null,\n\t\t\t\t\terror: {\n\t\t\t\t\t\tcode: e.code,\n\t\t\t\t\t\tmessage: e.message,\n\t\t\t\t\t\tstatus: 400,\n\t\t\t\t\t\tstatusText: \"BAD_REQUEST\",\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tdata: null,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"UNKNOWN_ERROR\",\n\t\t\t\t\tmessage:\n\t\t\t\t\t\te instanceof Error ? e.message : PASSKEY_ERROR_CODES.UNKNOWN_ERROR,\n\t\t\t\t\tstatus: 500,\n\t\t\t\t\tstatusText: \"INTERNAL_SERVER_ERROR\",\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t};\n\n\treturn {\n\t\tsignIn: {\n\t\t\t/**\n\t\t\t * Sign in with a registered passkey\n\t\t\t */\n\t\t\tpasskey: signInPasskey,\n\t\t},\n\t\tpasskey: {\n\t\t\t/**\n\t\t\t * Add a passkey to the user account\n\t\t\t */\n\t\t\taddPasskey: registerPasskey,\n\t\t},\n\t\t/**\n\t\t * Inferred Internal Types\n\t\t */\n\t\t$Infer: {} as {\n\t\t\tPasskey: Passkey;\n\t\t},\n\t};\n};\n\nexport const passkeyClient = () => {\n\tconst $listPasskeys = atom<any>();\n\treturn {\n\t\tid: \"passkey\",\n\t\t$InferServerPlugin: {} as ReturnType<typeof passkey>,\n\t\tgetActions: ($fetch, $store) =>\n\t\t\tgetPasskeyActions($fetch, {\n\t\t\t\t$listPasskeys,\n\t\t\t\t$store,\n\t\t\t}),\n\t\tgetAtoms($fetch) {\n\t\t\tconst listPasskeys = useAuthQuery<Passkey[]>(\n\t\t\t\t$listPasskeys,\n\t\t\t\t\"/passkey/list-user-passkeys\",\n\t\t\t\t$fetch,\n\t\t\t\t{\n\t\t\t\t\tmethod: \"GET\",\n\t\t\t\t},\n\t\t\t);\n\t\t\treturn {\n\t\t\t\tlistPasskeys,\n\t\t\t\t$listPasskeys,\n\t\t\t};\n\t\t},\n\t\tpathMethods: {\n\t\t\t\"/passkey/register\": \"POST\",\n\t\t\t\"/passkey/authenticate\": \"POST\",\n\t\t},\n\t\tatomListeners: [\n\t\t\t{\n\t\t\t\tmatcher(path) {\n\t\t\t\t\treturn (\n\t\t\t\t\t\tpath === \"/passkey/verify-registration\" ||\n\t\t\t\t\t\tpath === \"/passkey/delete-passkey\" ||\n\t\t\t\t\t\tpath === \"/passkey/update-passkey\" ||\n\t\t\t\t\t\tpath === \"/sign-out\"\n\t\t\t\t\t);\n\t\t\t\t},\n\t\t\t\tsignal: \"$listPasskeys\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tmatcher: (path) => path === \"/passkey/verify-authentication\",\n\t\t\t\tsignal: \"$sessionSignal\",\n\t\t\t},\n\t\t],\n\t\t$ERROR_CODES: PASSKEY_ERROR_CODES,\n\t} satisfies BetterAuthClientPlugin;\n};\n\nexport type * from \"@simplewebauthn/server\";\nexport * from \"./error-codes\";\nexport type * from \"./types\";\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"error-codes-DJf-1Ecu.mjs","names":[],"sources":["../src/error-codes.ts"],"sourcesContent":["import { defineErrorCodes } from \"@better-auth/core/utils/error-codes\";\n\nexport const PASSKEY_ERROR_CODES = defineErrorCodes({\n\tCHALLENGE_NOT_FOUND: \"Challenge not found\",\n\tYOU_ARE_NOT_ALLOWED_TO_REGISTER_THIS_PASSKEY:\n\t\t\"You are not allowed to register this passkey\",\n\tFAILED_TO_VERIFY_REGISTRATION: \"Failed to verify registration\",\n\tPASSKEY_NOT_FOUND: \"Passkey not found\",\n\tAUTHENTICATION_FAILED: \"Authentication failed\",\n\tUNABLE_TO_CREATE_SESSION: \"Unable to create session\",\n\tFAILED_TO_UPDATE_PASSKEY: \"Failed to update passkey\",\n\tPREVIOUSLY_REGISTERED: \"Previously registered\",\n\tREGISTRATION_CANCELLED: \"Registration cancelled\",\n\tAUTH_CANCELLED: \"Auth cancelled\",\n\tUNKNOWN_ERROR: \"Unknown error\",\n});\n"],"mappings":";;;AAEA,MAAa,sBAAsB,iBAAiB;CACnD,qBAAqB;CACrB,8CACC;CACD,+BAA+B;CAC/B,mBAAmB;CACnB,uBAAuB;CACvB,0BAA0B;CAC1B,0BAA0B;CAC1B,uBAAuB;CACvB,wBAAwB;CACxB,gBAAgB;CAChB,eAAe;CACf,CAAC"}