@better-auth/passkey 1.5.7-beta.1 → 1.6.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-BfRDyiNp.mjs";
1
+ import { n as PASSKEY_ERROR_CODES, o as PasskeyOptions, r as Passkey, t as passkey } from "./index-DD5Lute1.mjs";
2
2
  export { PASSKEY_ERROR_CODES, Passkey, PasskeyOptions, passkey };
package/dist/index.mjs CHANGED
@@ -1,10 +1,10 @@
1
- import { t as PASSKEY_ERROR_CODES } from "./error-codes-BwAsYefH.mjs";
1
+ import { n as PASSKEY_ERROR_CODES, t as PACKAGE_VERSION } from "./version-DSSbBvqx.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";
5
5
  import { base64 } from "@better-auth/utils/base64";
6
6
  import { generateAuthenticationOptions, generateRegistrationOptions, verifyAuthenticationResponse, verifyRegistrationResponse } from "@simplewebauthn/server";
7
- import { freshSessionMiddleware, getSessionFromCtx, sessionMiddleware } from "better-auth/api";
7
+ import { freshSessionMiddleware, getSessionFromCtx, requireResourceOwnership, sessionMiddleware } from "better-auth/api";
8
8
  import { setSessionCookie } from "better-auth/cookies";
9
9
  import { generateRandomString } from "better-auth/crypto";
10
10
  import * as z from "zod";
@@ -14,136 +14,183 @@ function getRpID(options, baseURL) {
14
14
  }
15
15
  //#endregion
16
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
+ };
17
50
  const generatePasskeyQuerySchema = z.object({
18
51
  authenticatorAttachment: z.enum(["platform", "cross-platform"]).optional(),
19
- name: z.string().optional()
52
+ name: z.string().optional(),
53
+ context: z.string().optional()
20
54
  }).optional();
21
- const generatePasskeyRegistrationOptions = (opts, { maxAgeInSeconds }) => createAuthEndpoint("/passkey/generate-register-options", {
22
- method: "GET",
23
- use: [freshSessionMiddleware],
24
- query: generatePasskeyQuerySchema,
25
- metadata: { openapi: {
26
- operationId: "generatePasskeyRegistrationOptions",
27
- description: "Generate registration options for a new passkey",
28
- responses: { 200: {
29
- description: "Success",
30
- parameters: { query: {
31
- authenticatorAttachment: {
32
- 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.
33
68
  "platform" for device-specific authenticators,
34
69
  "cross-platform" for authenticators that can be used across devices.`,
35
- required: false
36
- },
37
- name: {
38
- description: `Optional custom name for the passkey.
39
- This can help identify the passkey when managing multiple credentials.`,
40
- required: false
41
- }
42
- } },
43
- content: { "application/json": { schema: {
44
- type: "object",
45
- properties: {
46
- challenge: { type: "string" },
47
- rp: {
48
- type: "object",
49
- properties: {
50
- name: { type: "string" },
51
- id: { type: "string" }
52
- }
70
+ required: false
53
71
  },
54
- user: {
55
- type: "object",
56
- properties: {
57
- id: { type: "string" },
58
- name: { type: "string" },
59
- displayName: { type: "string" }
60
- }
72
+ name: {
73
+ description: `Optional custom name for the passkey.
74
+ This can help identify the passkey when managing multiple credentials.`,
75
+ required: false
61
76
  },
62
- pubKeyCredParams: {
63
- type: "array",
64
- 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: {
65
87
  type: "object",
66
88
  properties: {
67
- type: { type: "string" },
68
- alg: { type: "number" }
89
+ name: { type: "string" },
90
+ id: { type: "string" }
69
91
  }
70
- }
71
- },
72
- timeout: { type: "number" },
73
- excludeCredentials: {
74
- type: "array",
75
- items: {
92
+ },
93
+ user: {
76
94
  type: "object",
77
95
  properties: {
78
96
  id: { type: "string" },
79
- type: { type: "string" },
80
- transports: {
81
- type: "array",
82
- 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" }
83
108
  }
84
109
  }
85
- }
86
- },
87
- authenticatorSelection: {
88
- type: "object",
89
- properties: {
90
- authenticatorAttachment: { type: "string" },
91
- requireResidentKey: { type: "boolean" },
92
- userVerification: { type: "string" }
93
- }
94
- },
95
- attestation: { type: "string" },
96
- extensions: { type: "object" }
97
- }
98
- } } }
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
+ } }
99
139
  } }
100
- } }
101
- }, async (ctx) => {
102
- const { session } = ctx.context;
103
- const userPasskeys = await ctx.context.adapter.findMany({
104
- model: "passkey",
105
- where: [{
106
- field: "userId",
107
- value: session.user.id
108
- }]
109
- });
110
- const userID = new TextEncoder().encode(generateRandomString(32, "a-z", "0-9"));
111
- const baseURLString = typeof ctx.context.options.baseURL === "string" ? ctx.context.options.baseURL : void 0;
112
- const options = await generateRegistrationOptions({
113
- rpName: opts.rpName || ctx.context.appName,
114
- rpID: getRpID(opts, baseURLString),
115
- userID,
116
- userName: ctx.query?.name || session.user.email || session.user.id,
117
- userDisplayName: session.user.email || session.user.id,
118
- attestationType: "none",
119
- excludeCredentials: userPasskeys.map((passkey) => ({
120
- id: passkey.credentialID,
121
- transports: passkey.transports?.split(",")
122
- })),
123
- authenticatorSelection: {
124
- residentKey: "preferred",
125
- userVerification: "preferred",
126
- ...opts.authenticatorSelection || {},
127
- ...ctx.query?.authenticatorAttachment ? { authenticatorAttachment: ctx.query.authenticatorAttachment } : {}
128
- }
129
- });
130
- const verificationToken = generateRandomString(32);
131
- const webAuthnCookie = ctx.context.createAuthCookie(opts.advanced.webAuthnChallengeCookie);
132
- await ctx.setSignedCookie(webAuthnCookie.name, verificationToken, ctx.context.secret, {
133
- ...webAuthnCookie.attributes,
134
- maxAge: maxAgeInSeconds
135
- });
136
- const expirationTime = new Date(Date.now() + maxAgeInSeconds * 1e3);
137
- await ctx.context.internalAdapter.createVerificationValue({
138
- identifier: verificationToken,
139
- value: JSON.stringify({
140
- expectedChallenge: options.challenge,
141
- userData: { id: session.user.id }
142
- }),
143
- 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 });
144
192
  });
145
- return ctx.json(options, { status: 200 });
146
- });
193
+ };
147
194
  const generatePasskeyAuthenticationOptions = (opts, { maxAgeInSeconds }) => createAuthEndpoint("/passkey/generate-authenticate-options", {
148
195
  method: "GET",
149
196
  metadata: { openapi: {
@@ -209,9 +256,12 @@ const generatePasskeyAuthenticationOptions = (opts, { maxAgeInSeconds }) => crea
209
256
  value: session.user.id
210
257
  }]
211
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);
212
261
  const options = await generateAuthenticationOptions({
213
- rpID: getRpID(opts, typeof ctx.context.options.baseURL === "string" ? ctx.context.options.baseURL : void 0),
262
+ rpID: getRpID(opts, baseURLString),
214
263
  userVerification: "preferred",
264
+ extensions: authenticationExtensions,
215
265
  ...userPasskeys.length ? { allowCredentials: userPasskeys.map((passkey) => ({
216
266
  id: passkey.credentialID,
217
267
  transports: passkey.transports?.split(",")
@@ -239,66 +289,91 @@ const verifyPasskeyRegistrationBodySchema = z.object({
239
289
  response: z.any(),
240
290
  name: z.string().meta({ description: "Name of the passkey" }).optional()
241
291
  });
242
- const verifyPasskeyRegistration = (options) => createAuthEndpoint("/passkey/verify-registration", {
243
- method: "POST",
244
- body: verifyPasskeyRegistrationBodySchema,
245
- use: [freshSessionMiddleware],
246
- metadata: { openapi: {
247
- operationId: "passkeyVerifyRegistration",
248
- description: "Verify registration of a new passkey",
249
- responses: {
250
- 200: {
251
- description: "Success",
252
- content: { "application/json": { schema: { $ref: "#/components/schemas/Passkey" } } }
253
- },
254
- 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);
255
374
  }
256
- } }
257
- }, async (ctx) => {
258
- const origin = options?.origin || ctx.headers?.get("origin") || "";
259
- if (!origin) throw APIError.from("BAD_REQUEST", PASSKEY_ERROR_CODES.FAILED_TO_VERIFY_REGISTRATION);
260
- const resp = ctx.body.response;
261
- const webAuthnCookie = ctx.context.createAuthCookie(options.advanced.webAuthnChallengeCookie);
262
- const verificationToken = await ctx.getSignedCookie(webAuthnCookie.name, ctx.context.secret);
263
- if (!verificationToken) throw APIError.from("BAD_REQUEST", PASSKEY_ERROR_CODES.CHALLENGE_NOT_FOUND);
264
- const data = await ctx.context.internalAdapter.findVerificationValue(verificationToken);
265
- if (!data) throw APIError.from("BAD_REQUEST", PASSKEY_ERROR_CODES.CHALLENGE_NOT_FOUND);
266
- const { expectedChallenge, userData } = JSON.parse(data.value);
267
- if (userData.id !== ctx.context.session.user.id) throw APIError.from("UNAUTHORIZED", PASSKEY_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_REGISTER_THIS_PASSKEY);
268
- try {
269
- const { verified, registrationInfo } = await verifyRegistrationResponse({
270
- response: resp,
271
- expectedChallenge,
272
- expectedOrigin: origin,
273
- expectedRPID: getRpID(options, typeof ctx.context.options.baseURL === "string" ? ctx.context.options.baseURL : void 0),
274
- requireUserVerification: false
275
- });
276
- if (!verified || !registrationInfo) throw APIError.from("BAD_REQUEST", PASSKEY_ERROR_CODES.FAILED_TO_VERIFY_REGISTRATION);
277
- const { aaguid, credentialDeviceType, credentialBackedUp, credential } = registrationInfo;
278
- const pubKey = base64.encode(credential.publicKey);
279
- const newPasskey = {
280
- name: ctx.body.name,
281
- userId: userData.id,
282
- credentialID: credential.id,
283
- publicKey: pubKey,
284
- counter: credential.counter,
285
- deviceType: credentialDeviceType,
286
- transports: resp.response.transports.join(","),
287
- backedUp: credentialBackedUp,
288
- createdAt: /* @__PURE__ */ new Date(),
289
- aaguid
290
- };
291
- const newPasskeyRes = await ctx.context.adapter.create({
292
- model: "passkey",
293
- data: newPasskey
294
- });
295
- await ctx.context.internalAdapter.deleteVerificationByIdentifier(verificationToken);
296
- return ctx.json(newPasskeyRes, { status: 200 });
297
- } catch (e) {
298
- ctx.context.logger.error("Failed to verify registration", e);
299
- throw APIError.from("INTERNAL_SERVER_ERROR", PASSKEY_ERROR_CODES.FAILED_TO_VERIFY_REGISTRATION);
300
- }
301
- });
375
+ });
376
+ };
302
377
  const verifyPasskeyAuthenticationBodySchema = z.object({ response: z.record(z.any(), z.any()) });
303
378
  const verifyPasskeyAuthentication = (options) => createAuthEndpoint("/passkey/verify-authentication", {
304
379
  method: "POST",
@@ -354,6 +429,11 @@ const verifyPasskeyAuthentication = (options) => createAuthEndpoint("/passkey/ve
354
429
  });
355
430
  const { verified } = verification;
356
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
+ });
357
437
  await ctx.context.adapter.update({
358
438
  model: "passkey",
359
439
  where: [{
@@ -443,7 +523,13 @@ const listPasskeys = createAuthEndpoint("/passkey/list-user-passkeys", {
443
523
  const deletePasskey = createAuthEndpoint("/passkey/delete-passkey", {
444
524
  method: "POST",
445
525
  body: z.object({ id: z.string().meta({ description: "The ID of the passkey to delete. Eg: \"some-passkey-id\"" }) }),
446
- use: [sessionMiddleware],
526
+ use: [sessionMiddleware, requireResourceOwnership({
527
+ model: "passkey",
528
+ idParam: "id",
529
+ idSource: "body",
530
+ notFoundError: PASSKEY_ERROR_CODES.PASSKEY_NOT_FOUND,
531
+ forbiddenStatus: "UNAUTHORIZED"
532
+ })],
447
533
  metadata: { openapi: {
448
534
  description: "Delete a specific passkey",
449
535
  responses: { "200": {
@@ -459,20 +545,11 @@ const deletePasskey = createAuthEndpoint("/passkey/delete-passkey", {
459
545
  } }
460
546
  } }
461
547
  }, async (ctx) => {
462
- const passkey = await ctx.context.adapter.findOne({
463
- model: "passkey",
464
- where: [{
465
- field: "id",
466
- value: ctx.body.id
467
- }]
468
- });
469
- if (!passkey) throw APIError.from("NOT_FOUND", PASSKEY_ERROR_CODES.PASSKEY_NOT_FOUND);
470
- if (passkey.userId !== ctx.context.session.user.id) throw new APIError("UNAUTHORIZED");
471
548
  await ctx.context.adapter.delete({
472
549
  model: "passkey",
473
550
  where: [{
474
551
  field: "id",
475
- value: passkey.id
552
+ value: ctx.body.id
476
553
  }]
477
554
  });
478
555
  return ctx.json({ status: true });
@@ -498,7 +575,14 @@ const updatePasskey = createAuthEndpoint("/passkey/update-passkey", {
498
575
  id: z.string().meta({ description: `The ID of the passkey which will be updated. Eg: \"passkey-id\"` }),
499
576
  name: z.string().meta({ description: `The new name which the passkey will be updated to. Eg: \"my-new-passkey-name\"` })
500
577
  }),
501
- use: [sessionMiddleware],
578
+ use: [sessionMiddleware, requireResourceOwnership({
579
+ model: "passkey",
580
+ idParam: "id",
581
+ idSource: "body",
582
+ notFoundError: PASSKEY_ERROR_CODES.PASSKEY_NOT_FOUND,
583
+ forbiddenError: PASSKEY_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_REGISTER_THIS_PASSKEY,
584
+ forbiddenStatus: "UNAUTHORIZED"
585
+ })],
502
586
  metadata: { openapi: {
503
587
  description: "Update a specific passkey's name",
504
588
  responses: { "200": {
@@ -511,15 +595,6 @@ const updatePasskey = createAuthEndpoint("/passkey/update-passkey", {
511
595
  } }
512
596
  } }
513
597
  }, async (ctx) => {
514
- const passkey = await ctx.context.adapter.findOne({
515
- model: "passkey",
516
- where: [{
517
- field: "id",
518
- value: ctx.body.id
519
- }]
520
- });
521
- if (!passkey) throw APIError.from("NOT_FOUND", PASSKEY_ERROR_CODES.PASSKEY_NOT_FOUND);
522
- if (passkey.userId !== ctx.context.session.user.id) throw APIError.from("UNAUTHORIZED", PASSKEY_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_REGISTER_THIS_PASSKEY);
523
598
  const updatedPasskey = await ctx.context.adapter.update({
524
599
  model: "passkey",
525
600
  where: [{
@@ -595,6 +670,7 @@ const passkey = (options) => {
595
670
  };
596
671
  return {
597
672
  id: "passkey",
673
+ version: PACKAGE_VERSION,
598
674
  endpoints: {
599
675
  generatePasskeyRegistrationOptions: generatePasskeyRegistrationOptions(opts, { maxAgeInSeconds: MAX_AGE_IN_SECONDS }),
600
676
  generatePasskeyAuthenticationOptions: generatePasskeyAuthenticationOptions(opts, { maxAgeInSeconds: MAX_AGE_IN_SECONDS }),
@@ -611,5 +687,3 @@ const passkey = (options) => {
611
687
  };
612
688
  //#endregion
613
689
  export { PASSKEY_ERROR_CODES, passkey };
614
-
615
- //# sourceMappingURL=index.mjs.map
@@ -11,9 +11,13 @@ const PASSKEY_ERROR_CODES = defineErrorCodes({
11
11
  PREVIOUSLY_REGISTERED: "Previously registered",
12
12
  REGISTRATION_CANCELLED: "Registration cancelled",
13
13
  AUTH_CANCELLED: "Auth cancelled",
14
- 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"
15
18
  });
16
19
  //#endregion
17
- export { PASSKEY_ERROR_CODES as t };
18
-
19
- //# sourceMappingURL=error-codes-BwAsYefH.mjs.map
20
+ //#region src/version.ts
21
+ const PACKAGE_VERSION = "1.6.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.7-beta.1",
3
+ "version": "1.6.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
  ],
@@ -54,21 +55,21 @@
54
55
  },
55
56
  "devDependencies": {
56
57
  "tsdown": "0.21.1",
57
- "@better-auth/core": "1.5.7-beta.1",
58
- "better-auth": "1.5.7-beta.1"
58
+ "@better-auth/core": "1.6.0",
59
+ "better-auth": "1.6.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": "2.0.2",
64
+ "better-call": "1.3.5",
64
65
  "nanostores": "^1.0.1",
65
- "@better-auth/core": "1.5.7-beta.1",
66
- "better-auth": "1.5.7-beta.1"
66
+ "@better-auth/core": "^1.6.0",
67
+ "better-auth": "^1.6.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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"}