@better-auth/passkey 1.7.0-beta.4 → 1.7.0-beta.6
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-z0BvlclO.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-DvfQNSl9.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">;
|
|
@@ -263,25 +294,10 @@ declare const passkey: (options?: PasskeyOptions | undefined) => {
|
|
|
263
294
|
openapi: {
|
|
264
295
|
operationId: string;
|
|
265
296
|
description: string;
|
|
297
|
+
parameters: better_call0.OpenAPIParameter[];
|
|
266
298
|
responses: {
|
|
267
299
|
200: {
|
|
268
300
|
description: string;
|
|
269
|
-
parameters: {
|
|
270
|
-
query: {
|
|
271
|
-
authenticatorAttachment: {
|
|
272
|
-
description: string;
|
|
273
|
-
required: boolean;
|
|
274
|
-
};
|
|
275
|
-
name: {
|
|
276
|
-
description: string;
|
|
277
|
-
required: boolean;
|
|
278
|
-
};
|
|
279
|
-
context: {
|
|
280
|
-
description: string;
|
|
281
|
-
required: boolean;
|
|
282
|
-
};
|
|
283
|
-
};
|
|
284
|
-
};
|
|
285
301
|
content: {
|
|
286
302
|
"application/json": {
|
|
287
303
|
schema: {
|
|
@@ -817,4 +833,4 @@ declare const passkey: (options?: PasskeyOptions | undefined) => {
|
|
|
817
833
|
options: PasskeyOptions | undefined;
|
|
818
834
|
};
|
|
819
835
|
//#endregion
|
|
820
|
-
export {
|
|
836
|
+
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-z0BvlclO.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-DvfQNSl9.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";
|
|
@@ -52,6 +52,35 @@ const generatePasskeyQuerySchema = z.object({
|
|
|
52
52
|
name: z.string().optional(),
|
|
53
53
|
context: z.string().optional()
|
|
54
54
|
}).optional();
|
|
55
|
+
const generatePasskeyRegistrationOptionsOpenAPIParameters = [
|
|
56
|
+
{
|
|
57
|
+
name: "authenticatorAttachment",
|
|
58
|
+
in: "query",
|
|
59
|
+
required: false,
|
|
60
|
+
description: `Type of authenticator to use for registration.
|
|
61
|
+
"platform" for device-specific authenticators,
|
|
62
|
+
"cross-platform" for authenticators that can be used across devices.`,
|
|
63
|
+
schema: {
|
|
64
|
+
type: "string",
|
|
65
|
+
enum: ["platform", "cross-platform"]
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: "name",
|
|
70
|
+
in: "query",
|
|
71
|
+
required: false,
|
|
72
|
+
description: `Optional custom name for the passkey.
|
|
73
|
+
This can help identify the passkey when managing multiple credentials.`,
|
|
74
|
+
schema: { type: "string" }
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: "context",
|
|
78
|
+
in: "query",
|
|
79
|
+
required: false,
|
|
80
|
+
description: "Optional context for passkey-first registration flows.",
|
|
81
|
+
schema: { type: "string" }
|
|
82
|
+
}
|
|
83
|
+
];
|
|
55
84
|
const generatePasskeyRegistrationOptions = (opts, { maxAgeInSeconds }) => {
|
|
56
85
|
return createAuthEndpoint("/passkey/generate-register-options", {
|
|
57
86
|
method: "GET",
|
|
@@ -60,25 +89,9 @@ const generatePasskeyRegistrationOptions = (opts, { maxAgeInSeconds }) => {
|
|
|
60
89
|
metadata: { openapi: {
|
|
61
90
|
operationId: "generatePasskeyRegistrationOptions",
|
|
62
91
|
description: "Generate registration options for a new passkey",
|
|
92
|
+
parameters: generatePasskeyRegistrationOptionsOpenAPIParameters,
|
|
63
93
|
responses: { 200: {
|
|
64
94
|
description: "Success",
|
|
65
|
-
parameters: { query: {
|
|
66
|
-
authenticatorAttachment: {
|
|
67
|
-
description: `Type of authenticator to use for registration.
|
|
68
|
-
"platform" for device-specific authenticators,
|
|
69
|
-
"cross-platform" for authenticators that can be used across devices.`,
|
|
70
|
-
required: false
|
|
71
|
-
},
|
|
72
|
-
name: {
|
|
73
|
-
description: `Optional custom name for the passkey.
|
|
74
|
-
This can help identify the passkey when managing multiple credentials.`,
|
|
75
|
-
required: false
|
|
76
|
-
},
|
|
77
|
-
context: {
|
|
78
|
-
description: "Optional context for passkey-first registration flows.",
|
|
79
|
-
required: false
|
|
80
|
-
}
|
|
81
|
-
} },
|
|
82
95
|
content: { "application/json": { schema: {
|
|
83
96
|
type: "object",
|
|
84
97
|
properties: {
|
|
@@ -178,6 +191,7 @@ const generatePasskeyRegistrationOptions = (opts, { maxAgeInSeconds }) => {
|
|
|
178
191
|
await ctx.context.internalAdapter.createVerificationValue({
|
|
179
192
|
identifier: verificationToken,
|
|
180
193
|
value: JSON.stringify({
|
|
194
|
+
type: "registration",
|
|
181
195
|
expectedChallenge: options.challenge,
|
|
182
196
|
userData: {
|
|
183
197
|
id: user.id,
|
|
@@ -268,6 +282,7 @@ const generatePasskeyAuthenticationOptions = (opts, { maxAgeInSeconds }) => crea
|
|
|
268
282
|
})) } : {}
|
|
269
283
|
});
|
|
270
284
|
const data = {
|
|
285
|
+
type: "authentication",
|
|
271
286
|
expectedChallenge: options.challenge,
|
|
272
287
|
userData: { id: session?.user.id || "" }
|
|
273
288
|
};
|
|
@@ -287,7 +302,7 @@ const generatePasskeyAuthenticationOptions = (opts, { maxAgeInSeconds }) => crea
|
|
|
287
302
|
});
|
|
288
303
|
const verifyPasskeyRegistrationBodySchema = z.object({
|
|
289
304
|
response: z.any(),
|
|
290
|
-
name: z.string().meta({ description: "Name of the passkey" }).optional()
|
|
305
|
+
name: z.string().trim().meta({ description: "Name of the passkey" }).optional()
|
|
291
306
|
});
|
|
292
307
|
const verifyPasskeyRegistration = (options) => {
|
|
293
308
|
const requireSession = options.registration?.requireSession ?? true;
|
|
@@ -315,7 +330,8 @@ const verifyPasskeyRegistration = (options) => {
|
|
|
315
330
|
if (!verificationToken) throw APIError.from("BAD_REQUEST", PASSKEY_ERROR_CODES.CHALLENGE_NOT_FOUND);
|
|
316
331
|
const data = await ctx.context.internalAdapter.consumeVerificationValue(verificationToken);
|
|
317
332
|
if (!data) throw APIError.from("BAD_REQUEST", PASSKEY_ERROR_CODES.CHALLENGE_NOT_FOUND);
|
|
318
|
-
const { expectedChallenge, userData, context } = JSON.parse(data.value);
|
|
333
|
+
const { type: ceremony, expectedChallenge, userData, context } = JSON.parse(data.value);
|
|
334
|
+
if (ceremony !== "registration") throw APIError.from("BAD_REQUEST", PASSKEY_ERROR_CODES.CHALLENGE_NOT_FOUND);
|
|
319
335
|
const session = requireSession ? ctx.context.session : await getSessionFromCtx(ctx);
|
|
320
336
|
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
337
|
try {
|
|
@@ -335,6 +351,7 @@ const verifyPasskeyRegistration = (options) => {
|
|
|
335
351
|
displayName: userData.displayName
|
|
336
352
|
};
|
|
337
353
|
let targetUserId = resolvedUser.id;
|
|
354
|
+
let resolvedName = ctx.body.name || void 0;
|
|
338
355
|
if (options.registration?.afterVerification) {
|
|
339
356
|
const result = await options.registration.afterVerification({
|
|
340
357
|
ctx,
|
|
@@ -348,10 +365,12 @@ const verifyPasskeyRegistration = (options) => {
|
|
|
348
365
|
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
366
|
targetUserId = result.userId;
|
|
350
367
|
}
|
|
368
|
+
if (!resolvedName) resolvedName = result?.name?.trim() || void 0;
|
|
351
369
|
}
|
|
370
|
+
if (!targetUserId) throw APIError.from("BAD_REQUEST", PASSKEY_ERROR_CODES.RESOLVED_USER_INVALID);
|
|
352
371
|
const pubKey = base64.encode(credential.publicKey);
|
|
353
372
|
const newPasskey = {
|
|
354
|
-
name:
|
|
373
|
+
name: resolvedName,
|
|
355
374
|
userId: targetUserId,
|
|
356
375
|
credentialID: credential.id,
|
|
357
376
|
publicKey: pubKey,
|
|
@@ -404,7 +423,8 @@ const verifyPasskeyAuthentication = (options) => createAuthEndpoint("/passkey/ve
|
|
|
404
423
|
if (!verificationToken) throw APIError.from("BAD_REQUEST", PASSKEY_ERROR_CODES.CHALLENGE_NOT_FOUND);
|
|
405
424
|
const data = await ctx.context.internalAdapter.consumeVerificationValue(verificationToken);
|
|
406
425
|
if (!data) throw APIError.from("BAD_REQUEST", PASSKEY_ERROR_CODES.CHALLENGE_NOT_FOUND);
|
|
407
|
-
const { expectedChallenge } = JSON.parse(data.value);
|
|
426
|
+
const { type: ceremony, expectedChallenge } = JSON.parse(data.value);
|
|
427
|
+
if (ceremony !== "authentication") throw APIError.from("BAD_REQUEST", PASSKEY_ERROR_CODES.CHALLENGE_NOT_FOUND);
|
|
408
428
|
const passkey = await ctx.context.adapter.findOne({
|
|
409
429
|
model: "passkey",
|
|
410
430
|
where: [{
|
|
@@ -576,7 +596,7 @@ const updatePasskey = createAuthEndpoint("/passkey/update-passkey", {
|
|
|
576
596
|
method: "POST",
|
|
577
597
|
body: z.object({
|
|
578
598
|
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\"` })
|
|
599
|
+
name: z.string().trim().min(1).meta({ description: `The new name which the passkey will be updated to. Eg: \"my-new-passkey-name\"` })
|
|
580
600
|
}),
|
|
581
601
|
use: [sessionMiddleware, requireResourceOwnership({
|
|
582
602
|
model: "passkey",
|
|
@@ -660,6 +680,63 @@ const schema = { passkey: { fields: {
|
|
|
660
680
|
}
|
|
661
681
|
} } };
|
|
662
682
|
//#endregion
|
|
683
|
+
//#region src/authenticator-metadata.ts
|
|
684
|
+
/**
|
|
685
|
+
* Best-effort map of common authenticator AAGUIDs to a human-readable provider
|
|
686
|
+
* name, for labeling passkeys in management UIs.
|
|
687
|
+
*
|
|
688
|
+
* An AAGUID identifies an authenticator *model* (not a device or a user) and is
|
|
689
|
+
* present only in the registration response. Better Auth stores it on every
|
|
690
|
+
* passkey row and returns it from `listPasskeys`, so a display label can be
|
|
691
|
+
* resolved wherever passkeys are rendered.
|
|
692
|
+
*
|
|
693
|
+
* This list is intentionally small and not authoritative. Many authenticators
|
|
694
|
+
* are missing, and privacy-preserving platforms report an all-zero AAGUID
|
|
695
|
+
* (`00000000-0000-0000-0000-000000000000`) that matches nothing here. Notably,
|
|
696
|
+
* Apple devices zero the AAGUID under the default `attestation: "none"` flow, so
|
|
697
|
+
* the Apple entries below only appear in attested or managed contexts. For full
|
|
698
|
+
* coverage, resolve against the community-maintained source instead:
|
|
699
|
+
*
|
|
700
|
+
* - https://github.com/passkeydeveloper/passkey-authenticator-aaguids
|
|
701
|
+
*
|
|
702
|
+
* Names mirror that source verbatim.
|
|
703
|
+
*/
|
|
704
|
+
const commonAuthenticatorNames = {
|
|
705
|
+
"ea9b8d66-4d01-1d21-3ce4-b6b48cb575d4": "Google Password Manager",
|
|
706
|
+
"fbfc3007-154e-4ecc-8c0b-6e020557d7bd": "Apple Passwords",
|
|
707
|
+
"dd4ec289-e01d-41c9-bb89-70fa845d4bf2": "iCloud Keychain (Managed)",
|
|
708
|
+
"08987058-cadc-4b81-b6e1-30de50dcbe96": "Windows Hello",
|
|
709
|
+
"9ddd1817-af5a-4672-a2b9-3e3dd95000a9": "Windows Hello",
|
|
710
|
+
"6028b017-b1d4-4c02-b4b3-afcdafc96bb2": "Windows Hello",
|
|
711
|
+
"bada5566-a7aa-401f-bd96-45619a55120d": "1Password",
|
|
712
|
+
"d548826e-79b4-db40-a3d8-11116f7e8349": "Bitwarden",
|
|
713
|
+
"531126d6-e717-415c-9320-3d9aa6981239": "Dashlane",
|
|
714
|
+
"b78a0a55-6ef8-d246-a042-ba0f6d55050c": "LastPass",
|
|
715
|
+
"b84e4048-15dc-4dd0-8640-f4f60813c8af": "NordPass",
|
|
716
|
+
"50726f74-6f6e-5061-7373-50726f746f6e": "Proton Pass",
|
|
717
|
+
"0ea242b4-43c4-4a1b-8b17-dd6d0b6baec6": "Keeper",
|
|
718
|
+
"53414d53-554e-4700-0000-000000000000": "Samsung Pass"
|
|
719
|
+
};
|
|
720
|
+
/**
|
|
721
|
+
* Resolve a best-effort provider name for an authenticator AAGUID.
|
|
722
|
+
*
|
|
723
|
+
* Returns `undefined` when the AAGUID is unknown, empty, or the all-zero value
|
|
724
|
+
* reported by privacy-preserving platforms. Casing and surrounding whitespace
|
|
725
|
+
* are normalized before lookup.
|
|
726
|
+
*
|
|
727
|
+
* @example
|
|
728
|
+
* ```ts
|
|
729
|
+
* const label = passkey.name || getAuthenticatorName(passkey.aaguid) || "Passkey";
|
|
730
|
+
* ```
|
|
731
|
+
*/
|
|
732
|
+
const ANONYMOUS_AAGUID = "00000000-0000-0000-0000-000000000000";
|
|
733
|
+
const getAuthenticatorName = (aaguid) => {
|
|
734
|
+
const normalized = aaguid?.trim().toLowerCase();
|
|
735
|
+
if (!normalized || normalized === ANONYMOUS_AAGUID) return void 0;
|
|
736
|
+
const name = commonAuthenticatorNames[normalized];
|
|
737
|
+
return typeof name === "string" ? name : void 0;
|
|
738
|
+
};
|
|
739
|
+
//#endregion
|
|
663
740
|
//#region src/index.ts
|
|
664
741
|
const MAX_AGE_IN_SECONDS = 300;
|
|
665
742
|
const passkey = (options) => {
|
|
@@ -689,4 +766,4 @@ const passkey = (options) => {
|
|
|
689
766
|
};
|
|
690
767
|
};
|
|
691
768
|
//#endregion
|
|
692
|
-
export { PASSKEY_ERROR_CODES, passkey };
|
|
769
|
+
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.6",
|
|
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.6",
|
|
59
|
+
"better-auth": "1.7.0-beta.6"
|
|
60
60
|
},
|
|
61
61
|
"peerDependencies": {
|
|
62
|
-
"@better-auth/utils": "0.4.
|
|
63
|
-
"@better-fetch/fetch": "1.1
|
|
64
|
-
"better-call": "1.3.
|
|
62
|
+
"@better-auth/utils": "0.4.2",
|
|
63
|
+
"@better-fetch/fetch": "1.3.1",
|
|
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.6",
|
|
67
|
+
"better-auth": "^1.7.0-beta.6"
|
|
68
68
|
},
|
|
69
69
|
"scripts": {
|
|
70
70
|
"build": "tsdown",
|