@better-auth/passkey 1.4.0-beta.16

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.
@@ -0,0 +1,2 @@
1
+ import { n as Passkey, r as PasskeyOptions, t as passkey } from "./index-B_gZWjC3.js";
2
+ export { Passkey, PasskeyOptions, passkey };
package/dist/index.js ADDED
@@ -0,0 +1,558 @@
1
+ import { createAuthEndpoint } from "@better-auth/core/api";
2
+ import { base64 } from "@better-auth/utils/base64";
3
+ import { generateAuthenticationOptions, generateRegistrationOptions, verifyAuthenticationResponse, verifyRegistrationResponse } from "@simplewebauthn/server";
4
+ import { generateId } from "better-auth";
5
+ import { freshSessionMiddleware, getSessionFromCtx, sessionMiddleware } from "better-auth/api";
6
+ import { setSessionCookie } from "better-auth/cookies";
7
+ import { generateRandomString } from "better-auth/crypto";
8
+ import { mergeSchema } from "better-auth/db";
9
+ import { APIError } from "better-call";
10
+ import * as z from "zod";
11
+ import { defineErrorCodes } from "@better-auth/core/utils";
12
+
13
+ //#region src/error-codes.ts
14
+ const PASSKEY_ERROR_CODES = defineErrorCodes({
15
+ CHALLENGE_NOT_FOUND: "Challenge not found",
16
+ YOU_ARE_NOT_ALLOWED_TO_REGISTER_THIS_PASSKEY: "You are not allowed to register this passkey",
17
+ FAILED_TO_VERIFY_REGISTRATION: "Failed to verify registration",
18
+ PASSKEY_NOT_FOUND: "Passkey not found",
19
+ AUTHENTICATION_FAILED: "Authentication failed",
20
+ UNABLE_TO_CREATE_SESSION: "Unable to create session",
21
+ FAILED_TO_UPDATE_PASSKEY: "Failed to update passkey"
22
+ });
23
+
24
+ //#endregion
25
+ //#region src/schema.ts
26
+ const schema = { passkey: { fields: {
27
+ name: {
28
+ type: "string",
29
+ required: false
30
+ },
31
+ publicKey: {
32
+ type: "string",
33
+ required: true
34
+ },
35
+ userId: {
36
+ type: "string",
37
+ references: {
38
+ model: "user",
39
+ field: "id"
40
+ },
41
+ required: true
42
+ },
43
+ credentialID: {
44
+ type: "string",
45
+ required: true
46
+ },
47
+ counter: {
48
+ type: "number",
49
+ required: true
50
+ },
51
+ deviceType: {
52
+ type: "string",
53
+ required: true
54
+ },
55
+ backedUp: {
56
+ type: "boolean",
57
+ required: true
58
+ },
59
+ transports: {
60
+ type: "string",
61
+ required: false
62
+ },
63
+ createdAt: {
64
+ type: "date",
65
+ required: false
66
+ },
67
+ aaguid: {
68
+ type: "string",
69
+ required: false
70
+ }
71
+ } } };
72
+
73
+ //#endregion
74
+ //#region src/utils.ts
75
+ function getRpID(options, baseURL) {
76
+ return options.rpID || (baseURL ? new URL(baseURL).hostname : "localhost");
77
+ }
78
+
79
+ //#endregion
80
+ //#region src/index.ts
81
+ const passkey = (options) => {
82
+ const opts = {
83
+ origin: null,
84
+ ...options,
85
+ advanced: {
86
+ webAuthnChallengeCookie: "better-auth-passkey",
87
+ ...options?.advanced
88
+ }
89
+ };
90
+ const expirationTime = new Date(Date.now() + 1e3 * 60 * 5);
91
+ const currentTime = /* @__PURE__ */ new Date();
92
+ const maxAgeInSeconds = Math.floor((expirationTime.getTime() - currentTime.getTime()) / 1e3);
93
+ return {
94
+ id: "passkey",
95
+ endpoints: {
96
+ generatePasskeyRegistrationOptions: createAuthEndpoint("/passkey/generate-register-options", {
97
+ method: "GET",
98
+ use: [freshSessionMiddleware],
99
+ query: z.object({
100
+ authenticatorAttachment: z.enum(["platform", "cross-platform"]).optional(),
101
+ name: z.string().optional()
102
+ }).optional(),
103
+ metadata: {
104
+ client: false,
105
+ openapi: {
106
+ description: "Generate registration options for a new passkey",
107
+ responses: { 200: {
108
+ description: "Success",
109
+ parameters: { query: {
110
+ authenticatorAttachment: {
111
+ description: `Type of authenticator to use for registration.
112
+ "platform" for device-specific authenticators,
113
+ "cross-platform" for authenticators that can be used across devices.`,
114
+ required: false
115
+ },
116
+ name: {
117
+ description: `Optional custom name for the passkey.
118
+ This can help identify the passkey when managing multiple credentials.`,
119
+ required: false
120
+ }
121
+ } },
122
+ content: { "application/json": { schema: {
123
+ type: "object",
124
+ properties: {
125
+ challenge: { type: "string" },
126
+ rp: {
127
+ type: "object",
128
+ properties: {
129
+ name: { type: "string" },
130
+ id: { type: "string" }
131
+ }
132
+ },
133
+ user: {
134
+ type: "object",
135
+ properties: {
136
+ id: { type: "string" },
137
+ name: { type: "string" },
138
+ displayName: { type: "string" }
139
+ }
140
+ },
141
+ pubKeyCredParams: {
142
+ type: "array",
143
+ items: {
144
+ type: "object",
145
+ properties: {
146
+ type: { type: "string" },
147
+ alg: { type: "number" }
148
+ }
149
+ }
150
+ },
151
+ timeout: { type: "number" },
152
+ excludeCredentials: {
153
+ type: "array",
154
+ items: {
155
+ type: "object",
156
+ properties: {
157
+ id: { type: "string" },
158
+ type: { type: "string" },
159
+ transports: {
160
+ type: "array",
161
+ items: { type: "string" }
162
+ }
163
+ }
164
+ }
165
+ },
166
+ authenticatorSelection: {
167
+ type: "object",
168
+ properties: {
169
+ authenticatorAttachment: { type: "string" },
170
+ requireResidentKey: { type: "boolean" },
171
+ userVerification: { type: "string" }
172
+ }
173
+ },
174
+ attestation: { type: "string" },
175
+ extensions: { type: "object" }
176
+ }
177
+ } } }
178
+ } }
179
+ }
180
+ }
181
+ }, async (ctx) => {
182
+ const { session } = ctx.context;
183
+ const userPasskeys = await ctx.context.adapter.findMany({
184
+ model: "passkey",
185
+ where: [{
186
+ field: "userId",
187
+ value: session.user.id
188
+ }]
189
+ });
190
+ const userID = new TextEncoder().encode(generateRandomString(32, "a-z", "0-9"));
191
+ let options$1;
192
+ options$1 = await generateRegistrationOptions({
193
+ rpName: opts.rpName || ctx.context.appName,
194
+ rpID: getRpID(opts, ctx.context.options.baseURL),
195
+ userID,
196
+ userName: ctx.query?.name || session.user.email || session.user.id,
197
+ userDisplayName: session.user.email || session.user.id,
198
+ attestationType: "none",
199
+ excludeCredentials: userPasskeys.map((passkey$1) => ({
200
+ id: passkey$1.credentialID,
201
+ transports: passkey$1.transports?.split(",")
202
+ })),
203
+ authenticatorSelection: {
204
+ residentKey: "preferred",
205
+ userVerification: "preferred",
206
+ ...opts.authenticatorSelection || {},
207
+ ...ctx.query?.authenticatorAttachment ? { authenticatorAttachment: ctx.query.authenticatorAttachment } : {}
208
+ }
209
+ });
210
+ const id = generateId(32);
211
+ const webAuthnCookie = ctx.context.createAuthCookie(opts.advanced.webAuthnChallengeCookie);
212
+ await ctx.setSignedCookie(webAuthnCookie.name, id, ctx.context.secret, {
213
+ ...webAuthnCookie.attributes,
214
+ maxAge: maxAgeInSeconds
215
+ });
216
+ await ctx.context.internalAdapter.createVerificationValue({
217
+ identifier: id,
218
+ value: JSON.stringify({
219
+ expectedChallenge: options$1.challenge,
220
+ userData: { id: session.user.id }
221
+ }),
222
+ expiresAt: expirationTime
223
+ });
224
+ return ctx.json(options$1, { status: 200 });
225
+ }),
226
+ generatePasskeyAuthenticationOptions: createAuthEndpoint("/passkey/generate-authenticate-options", {
227
+ method: "POST",
228
+ metadata: { openapi: {
229
+ description: "Generate authentication options for a passkey",
230
+ responses: { 200: {
231
+ description: "Success",
232
+ content: { "application/json": { schema: {
233
+ type: "object",
234
+ properties: {
235
+ challenge: { type: "string" },
236
+ rp: {
237
+ type: "object",
238
+ properties: {
239
+ name: { type: "string" },
240
+ id: { type: "string" }
241
+ }
242
+ },
243
+ user: {
244
+ type: "object",
245
+ properties: {
246
+ id: { type: "string" },
247
+ name: { type: "string" },
248
+ displayName: { type: "string" }
249
+ }
250
+ },
251
+ timeout: { type: "number" },
252
+ allowCredentials: {
253
+ type: "array",
254
+ items: {
255
+ type: "object",
256
+ properties: {
257
+ id: { type: "string" },
258
+ type: { type: "string" },
259
+ transports: {
260
+ type: "array",
261
+ items: { type: "string" }
262
+ }
263
+ }
264
+ }
265
+ },
266
+ userVerification: { type: "string" },
267
+ authenticatorSelection: {
268
+ type: "object",
269
+ properties: {
270
+ authenticatorAttachment: { type: "string" },
271
+ requireResidentKey: { type: "boolean" },
272
+ userVerification: { type: "string" }
273
+ }
274
+ },
275
+ extensions: { type: "object" }
276
+ }
277
+ } } }
278
+ } }
279
+ } }
280
+ }, async (ctx) => {
281
+ const session = await getSessionFromCtx(ctx);
282
+ let userPasskeys = [];
283
+ if (session) userPasskeys = await ctx.context.adapter.findMany({
284
+ model: "passkey",
285
+ where: [{
286
+ field: "userId",
287
+ value: session.user.id
288
+ }]
289
+ });
290
+ const options$1 = await generateAuthenticationOptions({
291
+ rpID: getRpID(opts, ctx.context.options.baseURL),
292
+ userVerification: "preferred",
293
+ ...userPasskeys.length ? { allowCredentials: userPasskeys.map((passkey$1) => ({
294
+ id: passkey$1.credentialID,
295
+ transports: passkey$1.transports?.split(",")
296
+ })) } : {}
297
+ });
298
+ const data = {
299
+ expectedChallenge: options$1.challenge,
300
+ userData: { id: session?.user.id || "" }
301
+ };
302
+ const id = generateId(32);
303
+ const webAuthnCookie = ctx.context.createAuthCookie(opts.advanced.webAuthnChallengeCookie);
304
+ await ctx.setSignedCookie(webAuthnCookie.name, id, ctx.context.secret, {
305
+ ...webAuthnCookie.attributes,
306
+ maxAge: maxAgeInSeconds
307
+ });
308
+ await ctx.context.internalAdapter.createVerificationValue({
309
+ identifier: id,
310
+ value: JSON.stringify(data),
311
+ expiresAt: expirationTime
312
+ });
313
+ return ctx.json(options$1, { status: 200 });
314
+ }),
315
+ verifyPasskeyRegistration: createAuthEndpoint("/passkey/verify-registration", {
316
+ method: "POST",
317
+ body: z.object({
318
+ response: z.any(),
319
+ name: z.string().meta({ description: "Name of the passkey" }).optional()
320
+ }),
321
+ use: [freshSessionMiddleware],
322
+ metadata: { openapi: {
323
+ description: "Verify registration of a new passkey",
324
+ responses: {
325
+ 200: {
326
+ description: "Success",
327
+ content: { "application/json": { schema: { $ref: "#/components/schemas/Passkey" } } }
328
+ },
329
+ 400: { description: "Bad request" }
330
+ }
331
+ } }
332
+ }, async (ctx) => {
333
+ const origin = options?.origin || ctx.headers?.get("origin") || "";
334
+ if (!origin) return ctx.json(null, { status: 400 });
335
+ const resp = ctx.body.response;
336
+ const webAuthnCookie = ctx.context.createAuthCookie(opts.advanced.webAuthnChallengeCookie);
337
+ const challengeId = await ctx.getSignedCookie(webAuthnCookie.name, ctx.context.secret);
338
+ if (!challengeId) throw new APIError("BAD_REQUEST", { message: PASSKEY_ERROR_CODES.CHALLENGE_NOT_FOUND });
339
+ const data = await ctx.context.internalAdapter.findVerificationValue(challengeId);
340
+ if (!data) return ctx.json(null, { status: 400 });
341
+ const { expectedChallenge, userData } = JSON.parse(data.value);
342
+ if (userData.id !== ctx.context.session.user.id) throw new APIError("UNAUTHORIZED", { message: PASSKEY_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_REGISTER_THIS_PASSKEY });
343
+ try {
344
+ const { verified, registrationInfo } = await verifyRegistrationResponse({
345
+ response: resp,
346
+ expectedChallenge,
347
+ expectedOrigin: origin,
348
+ expectedRPID: getRpID(opts, ctx.context.options.baseURL),
349
+ requireUserVerification: false
350
+ });
351
+ if (!verified || !registrationInfo) return ctx.json(null, { status: 400 });
352
+ const { aaguid, credentialDeviceType, credentialBackedUp, credential, credentialType } = registrationInfo;
353
+ const pubKey = base64.encode(credential.publicKey);
354
+ const newPasskey = {
355
+ name: ctx.body.name,
356
+ userId: userData.id,
357
+ credentialID: credential.id,
358
+ publicKey: pubKey,
359
+ counter: credential.counter,
360
+ deviceType: credentialDeviceType,
361
+ transports: resp.response.transports.join(","),
362
+ backedUp: credentialBackedUp,
363
+ createdAt: /* @__PURE__ */ new Date(),
364
+ aaguid
365
+ };
366
+ const newPasskeyRes = await ctx.context.adapter.create({
367
+ model: "passkey",
368
+ data: newPasskey
369
+ });
370
+ return ctx.json(newPasskeyRes, { status: 200 });
371
+ } catch (e) {
372
+ console.log(e);
373
+ throw new APIError("INTERNAL_SERVER_ERROR", { message: PASSKEY_ERROR_CODES.FAILED_TO_VERIFY_REGISTRATION });
374
+ }
375
+ }),
376
+ verifyPasskeyAuthentication: createAuthEndpoint("/passkey/verify-authentication", {
377
+ method: "POST",
378
+ body: z.object({ response: z.record(z.any(), z.any()) }),
379
+ metadata: {
380
+ openapi: {
381
+ description: "Verify authentication of a passkey",
382
+ responses: { 200: {
383
+ description: "Success",
384
+ content: { "application/json": { schema: {
385
+ type: "object",
386
+ properties: {
387
+ session: { $ref: "#/components/schemas/Session" },
388
+ user: { $ref: "#/components/schemas/User" }
389
+ }
390
+ } } }
391
+ } }
392
+ },
393
+ $Infer: { body: {} }
394
+ }
395
+ }, async (ctx) => {
396
+ const origin = options?.origin || ctx.headers?.get("origin") || "";
397
+ if (!origin) throw new APIError("BAD_REQUEST", { message: "origin missing" });
398
+ const resp = ctx.body.response;
399
+ const webAuthnCookie = ctx.context.createAuthCookie(opts.advanced.webAuthnChallengeCookie);
400
+ const challengeId = await ctx.getSignedCookie(webAuthnCookie.name, ctx.context.secret);
401
+ if (!challengeId) throw new APIError("BAD_REQUEST", { message: PASSKEY_ERROR_CODES.CHALLENGE_NOT_FOUND });
402
+ const data = await ctx.context.internalAdapter.findVerificationValue(challengeId);
403
+ if (!data) throw new APIError("BAD_REQUEST", { message: PASSKEY_ERROR_CODES.CHALLENGE_NOT_FOUND });
404
+ const { expectedChallenge } = JSON.parse(data.value);
405
+ const passkey$1 = await ctx.context.adapter.findOne({
406
+ model: "passkey",
407
+ where: [{
408
+ field: "credentialID",
409
+ value: resp.id
410
+ }]
411
+ });
412
+ if (!passkey$1) throw new APIError("UNAUTHORIZED", { message: PASSKEY_ERROR_CODES.PASSKEY_NOT_FOUND });
413
+ try {
414
+ const verification = await verifyAuthenticationResponse({
415
+ response: resp,
416
+ expectedChallenge,
417
+ expectedOrigin: origin,
418
+ expectedRPID: getRpID(opts, ctx.context.options.baseURL),
419
+ credential: {
420
+ id: passkey$1.credentialID,
421
+ publicKey: base64.decode(passkey$1.publicKey),
422
+ counter: passkey$1.counter,
423
+ transports: passkey$1.transports?.split(",")
424
+ },
425
+ requireUserVerification: false
426
+ });
427
+ const { verified } = verification;
428
+ if (!verified) throw new APIError("UNAUTHORIZED", { message: PASSKEY_ERROR_CODES.AUTHENTICATION_FAILED });
429
+ await ctx.context.adapter.update({
430
+ model: "passkey",
431
+ where: [{
432
+ field: "id",
433
+ value: passkey$1.id
434
+ }],
435
+ update: { counter: verification.authenticationInfo.newCounter }
436
+ });
437
+ const s = await ctx.context.internalAdapter.createSession(passkey$1.userId);
438
+ if (!s) throw new APIError("INTERNAL_SERVER_ERROR", { message: PASSKEY_ERROR_CODES.UNABLE_TO_CREATE_SESSION });
439
+ const user = await ctx.context.internalAdapter.findUserById(passkey$1.userId);
440
+ if (!user) throw new APIError("INTERNAL_SERVER_ERROR", { message: "User not found" });
441
+ await setSessionCookie(ctx, {
442
+ session: s,
443
+ user
444
+ });
445
+ return ctx.json({ session: s }, { status: 200 });
446
+ } catch (e) {
447
+ ctx.context.logger.error("Failed to verify authentication", e);
448
+ throw new APIError("BAD_REQUEST", { message: PASSKEY_ERROR_CODES.AUTHENTICATION_FAILED });
449
+ }
450
+ }),
451
+ listPasskeys: createAuthEndpoint("/passkey/list-user-passkeys", {
452
+ method: "GET",
453
+ use: [sessionMiddleware],
454
+ metadata: { openapi: {
455
+ description: "List all passkeys for the authenticated user",
456
+ responses: { "200": {
457
+ description: "Passkeys retrieved successfully",
458
+ content: { "application/json": { schema: {
459
+ type: "array",
460
+ items: {
461
+ $ref: "#/components/schemas/Passkey",
462
+ required: [
463
+ "id",
464
+ "userId",
465
+ "publicKey",
466
+ "createdAt",
467
+ "updatedAt"
468
+ ]
469
+ },
470
+ description: "Array of passkey objects associated with the user"
471
+ } } }
472
+ } }
473
+ } }
474
+ }, async (ctx) => {
475
+ const passkeys = await ctx.context.adapter.findMany({
476
+ model: "passkey",
477
+ where: [{
478
+ field: "userId",
479
+ value: ctx.context.session.user.id
480
+ }]
481
+ });
482
+ return ctx.json(passkeys, { status: 200 });
483
+ }),
484
+ deletePasskey: createAuthEndpoint("/passkey/delete-passkey", {
485
+ method: "POST",
486
+ body: z.object({ id: z.string().meta({ description: "The ID of the passkey to delete. Eg: \"some-passkey-id\"" }) }),
487
+ use: [sessionMiddleware],
488
+ metadata: { openapi: {
489
+ description: "Delete a specific passkey",
490
+ responses: { "200": {
491
+ description: "Passkey deleted successfully",
492
+ content: { "application/json": { schema: {
493
+ type: "object",
494
+ properties: { status: {
495
+ type: "boolean",
496
+ description: "Indicates whether the deletion was successful"
497
+ } },
498
+ required: ["status"]
499
+ } } }
500
+ } }
501
+ } }
502
+ }, async (ctx) => {
503
+ await ctx.context.adapter.delete({
504
+ model: "passkey",
505
+ where: [{
506
+ field: "id",
507
+ value: ctx.body.id
508
+ }]
509
+ });
510
+ return ctx.json(null, { status: 200 });
511
+ }),
512
+ updatePasskey: createAuthEndpoint("/passkey/update-passkey", {
513
+ method: "POST",
514
+ body: z.object({
515
+ id: z.string().meta({ description: `The ID of the passkey which will be updated. Eg: \"passkey-id\"` }),
516
+ name: z.string().meta({ description: `The new name which the passkey will be updated to. Eg: \"my-new-passkey-name\"` })
517
+ }),
518
+ use: [sessionMiddleware],
519
+ metadata: { openapi: {
520
+ description: "Update a specific passkey's name",
521
+ responses: { "200": {
522
+ description: "Passkey updated successfully",
523
+ content: { "application/json": { schema: {
524
+ type: "object",
525
+ properties: { passkey: { $ref: "#/components/schemas/Passkey" } },
526
+ required: ["passkey"]
527
+ } } }
528
+ } }
529
+ } }
530
+ }, async (ctx) => {
531
+ const passkey$1 = await ctx.context.adapter.findOne({
532
+ model: "passkey",
533
+ where: [{
534
+ field: "id",
535
+ value: ctx.body.id
536
+ }]
537
+ });
538
+ if (!passkey$1) throw new APIError("NOT_FOUND", { message: PASSKEY_ERROR_CODES.PASSKEY_NOT_FOUND });
539
+ if (passkey$1.userId !== ctx.context.session.user.id) throw new APIError("UNAUTHORIZED", { message: PASSKEY_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_REGISTER_THIS_PASSKEY });
540
+ const updatedPasskey = await ctx.context.adapter.update({
541
+ model: "passkey",
542
+ where: [{
543
+ field: "id",
544
+ value: ctx.body.id
545
+ }],
546
+ update: { name: ctx.body.name }
547
+ });
548
+ if (!updatedPasskey) throw new APIError("INTERNAL_SERVER_ERROR", { message: PASSKEY_ERROR_CODES.FAILED_TO_UPDATE_PASSKEY });
549
+ return ctx.json({ passkey: updatedPasskey }, { status: 200 });
550
+ })
551
+ },
552
+ schema: mergeSchema(schema, options?.schema),
553
+ $ERROR_CODES: PASSKEY_ERROR_CODES
554
+ };
555
+ };
556
+
557
+ //#endregion
558
+ export { passkey };
package/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "@better-auth/passkey",
3
+ "version": "1.4.0-beta.16",
4
+ "type": "module",
5
+ "description": "Passkey plugin for Better Auth",
6
+ "main": "dist/index.js",
7
+ "module": "dist/index.js",
8
+ "publishConfig": {
9
+ "access": "public"
10
+ },
11
+ "exports": {
12
+ ".": {
13
+ "better-auth-dev-source": "./src/index.ts",
14
+ "types": "./dist/index.d.ts",
15
+ "default": "./dist/index.js"
16
+ },
17
+ "./client": {
18
+ "better-auth-dev-source": "./src/client.ts",
19
+ "types": "./dist/client.d.ts",
20
+ "default": "./dist/client.js"
21
+ }
22
+ },
23
+ "typesVersions": {
24
+ "*": {
25
+ "*": [
26
+ "./dist/index.d.ts"
27
+ ],
28
+ "client": [
29
+ "./dist/client.d.ts"
30
+ ]
31
+ }
32
+ },
33
+ "devDependencies": {
34
+ "tsdown": "^0.15.11",
35
+ "@better-auth/core": "1.4.0-beta.16",
36
+ "better-auth": "1.4.0-beta.16"
37
+ },
38
+ "dependencies": {
39
+ "@simplewebauthn/browser": "^13.1.2",
40
+ "@simplewebauthn/server": "^13.1.2",
41
+ "zod": "^4.1.5"
42
+ },
43
+ "peerDependencies": {
44
+ "nanostores": "^1.0.1",
45
+ "@better-auth/utils": "0.3.0",
46
+ "better-call": "1.0.24",
47
+ "@better-fetch/fetch": "1.1.18",
48
+ "@better-auth/core": "1.4.0-beta.16",
49
+ "better-auth": "1.4.0-beta.16"
50
+ },
51
+ "files": [
52
+ "dist"
53
+ ],
54
+ "repository": {
55
+ "type": "git",
56
+ "url": "https://github.com/better-auth/better-auth",
57
+ "directory": "packages/passkey"
58
+ },
59
+ "homepage": "https://www.better-auth.com/docs/plugins/passkey",
60
+ "keywords": [
61
+ "auth",
62
+ "passkey",
63
+ "typescript",
64
+ "better-auth"
65
+ ],
66
+ "license": "MIT",
67
+ "scripts": {
68
+ "test": "vitest",
69
+ "build": "tsdown",
70
+ "dev": "tsdown --watch",
71
+ "typecheck": "tsc --project tsconfig.json"
72
+ }
73
+ }