@adminforth/two-factors-auth 2.1.1 → 2.1.3

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/build.log CHANGED
@@ -13,5 +13,5 @@ custom/package.json
13
13
  custom/tsconfig.json
14
14
  custom/utils.js
15
15
 
16
- sent 71,446 bytes received 172 bytes 143,236.00 bytes/sec
17
- total size is 70,806 speedup is 0.99
16
+ sent 76,042 bytes received 172 bytes 152,428.00 bytes/sec
17
+ total size is 75,402 speedup is 0.99
@@ -98,6 +98,9 @@
98
98
  import { IconShieldOutline } from '@iconify-prerendered/vue-flowbite';
99
99
  import ErrorMessage from '@/components/ErrorMessage.vue';
100
100
  import { handlePasskeyAlert } from './utils.js';
101
+
102
+ // Global provided by AdminForth runtime
103
+ declare const adminforth: any;
101
104
 
102
105
  const { t } = useI18n();
103
106
  const code = ref(null);
@@ -108,6 +111,29 @@
108
111
  const codeError = ref(null);
109
112
  const isFetchingPasskey = ref(false);
110
113
 
114
+ // Shared WebAuthn state to prevent concurrent navigator.credentials.get calls
115
+ const webAuthn = {
116
+ controller: null as AbortController | null,
117
+ inFlight: false,
118
+ };
119
+
120
+ function cancelPendingWebAuthn(reason?: string) {
121
+ try {
122
+ if (webAuthn.controller) {
123
+ // Abort any pending WebAuthn request
124
+ webAuthn.controller.abort();
125
+ }
126
+ } catch (e) {
127
+ // no-op
128
+ } finally {
129
+ webAuthn.controller = null;
130
+ webAuthn.inFlight = false;
131
+ if (reason) {
132
+ console.debug('[WebAuthn] Aborted pending request:', reason);
133
+ }
134
+ }
135
+ }
136
+
111
137
  onBeforeMount(() => {
112
138
  if (localStorage.getItem('isAuthorized') === 'true') {
113
139
  coreStore.fetchMenuAndResource();
@@ -147,11 +173,16 @@
147
173
 
148
174
  onMounted(async () => {
149
175
  if (localStorage.getItem('isAuthorized') !== 'true') {
176
+ // Safety: ensure no pending WebAuthn request survives SPA navigation
177
+ cancelPendingWebAuthn('mount-init');
150
178
  await nextTick();
151
179
  await isCMAAvailable();
152
180
  tagOtpInputs();
181
+ console.log("Checking if device supports passkeys:", isPasskeysSupported.value);
153
182
  if (isPasskeysSupported.value === true) {
183
+ console.log("Device supports passkeys, checking if user has passkeys...");
154
184
  await checkIfUserHasPasskeys();
185
+ console.log("Does user have passkeys:", doesUserHavePasskeys.value);
155
186
  }
156
187
  document.addEventListener('focusin', handleGlobalFocusIn, true);
157
188
  focusFirstAvailableOtpInput();
@@ -164,20 +195,34 @@
164
195
  watch(route, (newRoute) => {
165
196
  codeError.value = null;
166
197
  if ( newRoute.hash === '#passkey' ) {
198
+ cancelPendingWebAuthn('switch-to-passkey');
167
199
  confirmationMode.value = 'passkey';
168
200
  } else if ( newRoute.hash === '#code' ) {
201
+ cancelPendingWebAuthn('switch-to-code');
169
202
  confirmationMode.value = 'code';
170
203
  }
171
204
  });
172
205
 
173
206
  async function isCMAAvailable() {
207
+ let isCMA_supported = false;
208
+ let isParsingSupported = false;
174
209
  if (window.PublicKeyCredential &&
175
210
  PublicKeyCredential.isConditionalMediationAvailable) {
176
211
  const isCMA = await PublicKeyCredential.isConditionalMediationAvailable();
177
- if (isCMA) {
178
- isPasskeysSupported.value = true;
179
- }
212
+ if (isCMA) {
213
+ isCMA_supported = true;
214
+ }
215
+ }
216
+ try {
217
+ const temp = PublicKeyCredential.parseRequestOptionsFromJSON({challenge: 'test123', timeout: 60000, userVerification: 'required'});
218
+ if (temp && typeof temp === 'object') {
219
+ isParsingSupported = true;
220
+ }
221
+ } catch (e) {
222
+ adminforth.alert({message: 'Please ensure your browser supports passkeys.', variant: 'warning'});
223
+ isParsingSupported = false;
180
224
  }
225
+ isPasskeysSupported.value = (isCMA_supported && isParsingSupported);
181
226
  }
182
227
 
183
228
  onBeforeUnmount(() => {
@@ -185,11 +230,15 @@
185
230
  document.removeEventListener('focusin', handleGlobalFocusIn, true);
186
231
  const rootEl = otpRoot.value;
187
232
  rootEl && rootEl.removeEventListener('focusout', handleFocusOut, true);
233
+ // Abort any in-flight WebAuthn request when leaving the component
234
+ cancelPendingWebAuthn('component-unmount');
188
235
  });
189
236
 
190
237
  async function sendCode (value: any, factorMode: 'TOTP' | 'passkey', passkeyOptions: any) {
191
238
  inProgress.value = true;
192
239
  const usePasskey = factorMode === 'passkey';
240
+ console.log("Sending code with factorMode:", factorMode);
241
+ console.log("Passkey options:", passkeyOptions);
193
242
  const resp = await callAdminForthApi({
194
243
  method: 'POST',
195
244
  path: '/plugin/twofa/confirmLogin',
@@ -200,12 +249,15 @@
200
249
  secret: null,
201
250
  }
202
251
  })
252
+ console.log("Response from confirmLogin:", resp);
203
253
  if ( resp.allowedLogin ) {
204
254
  if ( route.meta.isPasskeysEnabled && !doesUserHavePasskeys.value ) {
205
255
  handlePasskeyAlert(route.meta.suggestionPeriod, router);
206
256
  }
257
+ console.log("Login confirmed, finishing login...");
207
258
  await user.finishLogin();
208
259
  } else {
260
+ console.log("Login not allowed, showing error:", resp.error);
209
261
  if (usePasskey) {
210
262
  showErrorTost(t(resp.error));
211
263
  codeError.value = resp.error || t('Passkey authentication failed');
@@ -249,8 +301,16 @@
249
301
  }
250
302
 
251
303
  async function usePasskeyButton() {
304
+ // Cancel any stray pending requests from earlier flows
305
+ cancelPendingWebAuthn('button-pressed');
306
+ codeError.value = null;
252
307
  isFetchingPasskey.value = true;
253
- const { _options, challengeId } = await createSignInRequest();
308
+ const signIn = await createSignInRequest();
309
+ if (!signIn) {
310
+ isFetchingPasskey.value = false;
311
+ return;
312
+ }
313
+ const { _options, challengeId } = signIn;
254
314
  const options = PublicKeyCredential.parseRequestOptionsFromJSON(_options);
255
315
  const credential = await authenticate(options);
256
316
  if (!credential) {
@@ -269,15 +329,18 @@
269
329
 
270
330
  async function createSignInRequest() {
271
331
  let response;
332
+ console.log("Creating sign-in request for passkey...");
272
333
  try {
273
334
  response = await callAdminForthApi({
274
335
  path: `/plugin/passkeys/signInRequest`,
275
336
  method: 'POST',
276
337
  });
277
338
  } catch (error) {
339
+ console.log("Error creating sign-in request:", error);
278
340
  console.error('Error creating sign-in request:', error);
279
341
  return;
280
342
  }
343
+ console.log("Sign-in request response:", response);
281
344
  if (response.ok === true) {
282
345
  return { _options: response.data, challengeId: response.challengeId };
283
346
  } else {
@@ -287,25 +350,54 @@
287
350
  }
288
351
 
289
352
  async function authenticate(options) {
353
+ console.log("Authenticating with options:", options);
290
354
  try {
355
+ // Guard: prevent concurrent navigator.credentials.get calls
356
+ if (webAuthn.inFlight) {
357
+ console.warn('[WebAuthn] A request is already in flight. Aborting previous and retrying.');
358
+ cancelPendingWebAuthn('pre-auth-guard');
359
+ }
360
+
291
361
  const abortController = new AbortController();
362
+ webAuthn.controller = abortController;
363
+ webAuthn.inFlight = true;
364
+
292
365
  const credential = await navigator.credentials.get({
293
366
  publicKey: options,
294
367
  signal: abortController.signal,
368
+ // mediation can be set if using conditional UI, omitted here intentionally
295
369
  });
370
+ console.log("Credential obtained:", credential);
296
371
  return credential;
297
372
  } catch (error) {
298
373
  console.error('Error during authentication:', error);
374
+ // Handle specific concurrent/pending request error cases gracefully
375
+ const name = (error && (error.name || error.constructor?.name)) || '';
376
+ const message = (error && error.message) || '';
377
+ if (name === 'AbortError') {
378
+ // Aborted intentionally; no user-facing error needed
379
+ return null;
380
+ }
381
+ if (name === 'InvalidStateError' || name === 'OperationError' || /pending/i.test(message)) {
382
+ adminforth.alert({ message: t('Another security prompt is already open. Please try again.'), variant: 'warning' });
383
+ codeError.value = t('A previous passkey attempt was still pending. Please try again.');
384
+ return null;
385
+ }
299
386
  adminforth.alert({message: `Error during authentication: ${error}`, variant: 'warning'});
300
387
  codeError.value = 'Error during authentication.';
301
388
  }
389
+ finally {
390
+ // Clear in-flight state regardless of outcome
391
+ webAuthn.inFlight = false;
392
+ webAuthn.controller = null;
393
+ }
302
394
  }
303
395
 
304
396
 
305
- function getOtpInputs() {
397
+ function getOtpInputs(): HTMLInputElement[] {
306
398
  const root = otpRoot.value;
307
399
  if (!root) return [];
308
- return Array.from(root.querySelectorAll('input.otp-input'));
400
+ return Array.from(root.querySelectorAll('input.otp-input')) as HTMLInputElement[];
309
401
  }
310
402
 
311
403
  function focusFirstAvailableOtpInput() {
@@ -318,7 +410,7 @@
318
410
  function handleGlobalFocusIn(event) {
319
411
  const inputs = getOtpInputs();
320
412
  if (!inputs.length) return;
321
- const target = event.target;
413
+ const target = event.target as HTMLInputElement | null;
322
414
  if (!target) return;
323
415
  if (!inputs.includes(target)) {
324
416
  requestAnimationFrame(() => {
@@ -331,8 +423,9 @@
331
423
  requestAnimationFrame(() => {
332
424
  const inputs = getOtpInputs();
333
425
  if (!inputs.length) return;
334
- const active = document.activeElement;
335
- if (!active || !inputs.includes(active)) {
426
+ const active = document.activeElement as Element | null;
427
+ const activeInput = active instanceof HTMLInputElement ? active : null;
428
+ if (!activeInput || !inputs.includes(activeInput)) {
336
429
  focusFirstAvailableOtpInput();
337
430
  }
338
431
  });
@@ -327,6 +327,8 @@
327
327
  }
328
328
 
329
329
  function checkForCompatibility() {
330
+ let isCMA_supported = false;
331
+ let isParsingSupported = false;
330
332
  if (window.PublicKeyCredential &&
331
333
  PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable &&
332
334
  PublicKeyCredential.isConditionalMediationAvailable) {
@@ -335,13 +337,23 @@
335
337
  PublicKeyCredential.isConditionalMediationAvailable(),
336
338
  ]).then(results => {
337
339
  if (results.every(r => r === true)) {
338
- isPasskeySupported.value = true;
340
+ isCMA_supported = true;
339
341
  } else {
340
- isPasskeySupported.value = false;
342
+ isCMA_supported = false;
341
343
  addPasskeyMode.value = 'cross-platform';
342
344
  }
343
345
  });
344
- }
346
+ }
347
+ try {
348
+ const temp = PublicKeyCredential.parseRequestOptionsFromJSON({challenge: 'test123', timeout: 60000, userVerification: 'required'});
349
+ if (temp && typeof temp === 'object') {
350
+ isParsingSupported = true;
351
+ }
352
+ } catch (e) {
353
+ adminforth.alert({message: 'Please ensure your browser supports passkeys.', variant: 'warning'});
354
+ isParsingSupported = false;
355
+ }
356
+ isPasskeySupported.value = (isCMA_supported && isParsingSupported);
345
357
  }
346
358
 
347
359
  async function fetchInformationFromTheBackend(confirmationResult) {
@@ -98,6 +98,9 @@
98
98
  import { IconShieldOutline } from '@iconify-prerendered/vue-flowbite';
99
99
  import ErrorMessage from '@/components/ErrorMessage.vue';
100
100
  import { handlePasskeyAlert } from './utils.js';
101
+
102
+ // Global provided by AdminForth runtime
103
+ declare const adminforth: any;
101
104
 
102
105
  const { t } = useI18n();
103
106
  const code = ref(null);
@@ -108,6 +111,29 @@
108
111
  const codeError = ref(null);
109
112
  const isFetchingPasskey = ref(false);
110
113
 
114
+ // Shared WebAuthn state to prevent concurrent navigator.credentials.get calls
115
+ const webAuthn = {
116
+ controller: null as AbortController | null,
117
+ inFlight: false,
118
+ };
119
+
120
+ function cancelPendingWebAuthn(reason?: string) {
121
+ try {
122
+ if (webAuthn.controller) {
123
+ // Abort any pending WebAuthn request
124
+ webAuthn.controller.abort();
125
+ }
126
+ } catch (e) {
127
+ // no-op
128
+ } finally {
129
+ webAuthn.controller = null;
130
+ webAuthn.inFlight = false;
131
+ if (reason) {
132
+ console.debug('[WebAuthn] Aborted pending request:', reason);
133
+ }
134
+ }
135
+ }
136
+
111
137
  onBeforeMount(() => {
112
138
  if (localStorage.getItem('isAuthorized') === 'true') {
113
139
  coreStore.fetchMenuAndResource();
@@ -147,11 +173,16 @@
147
173
 
148
174
  onMounted(async () => {
149
175
  if (localStorage.getItem('isAuthorized') !== 'true') {
176
+ // Safety: ensure no pending WebAuthn request survives SPA navigation
177
+ cancelPendingWebAuthn('mount-init');
150
178
  await nextTick();
151
179
  await isCMAAvailable();
152
180
  tagOtpInputs();
181
+ console.log("Checking if device supports passkeys:", isPasskeysSupported.value);
153
182
  if (isPasskeysSupported.value === true) {
183
+ console.log("Device supports passkeys, checking if user has passkeys...");
154
184
  await checkIfUserHasPasskeys();
185
+ console.log("Does user have passkeys:", doesUserHavePasskeys.value);
155
186
  }
156
187
  document.addEventListener('focusin', handleGlobalFocusIn, true);
157
188
  focusFirstAvailableOtpInput();
@@ -164,20 +195,34 @@
164
195
  watch(route, (newRoute) => {
165
196
  codeError.value = null;
166
197
  if ( newRoute.hash === '#passkey' ) {
198
+ cancelPendingWebAuthn('switch-to-passkey');
167
199
  confirmationMode.value = 'passkey';
168
200
  } else if ( newRoute.hash === '#code' ) {
201
+ cancelPendingWebAuthn('switch-to-code');
169
202
  confirmationMode.value = 'code';
170
203
  }
171
204
  });
172
205
 
173
206
  async function isCMAAvailable() {
207
+ let isCMA_supported = false;
208
+ let isParsingSupported = false;
174
209
  if (window.PublicKeyCredential &&
175
210
  PublicKeyCredential.isConditionalMediationAvailable) {
176
211
  const isCMA = await PublicKeyCredential.isConditionalMediationAvailable();
177
- if (isCMA) {
178
- isPasskeysSupported.value = true;
179
- }
212
+ if (isCMA) {
213
+ isCMA_supported = true;
214
+ }
215
+ }
216
+ try {
217
+ const temp = PublicKeyCredential.parseRequestOptionsFromJSON({challenge: 'test123', timeout: 60000, userVerification: 'required'});
218
+ if (temp && typeof temp === 'object') {
219
+ isParsingSupported = true;
220
+ }
221
+ } catch (e) {
222
+ adminforth.alert({message: 'Please ensure your browser supports passkeys.', variant: 'warning'});
223
+ isParsingSupported = false;
180
224
  }
225
+ isPasskeysSupported.value = (isCMA_supported && isParsingSupported);
181
226
  }
182
227
 
183
228
  onBeforeUnmount(() => {
@@ -185,11 +230,15 @@
185
230
  document.removeEventListener('focusin', handleGlobalFocusIn, true);
186
231
  const rootEl = otpRoot.value;
187
232
  rootEl && rootEl.removeEventListener('focusout', handleFocusOut, true);
233
+ // Abort any in-flight WebAuthn request when leaving the component
234
+ cancelPendingWebAuthn('component-unmount');
188
235
  });
189
236
 
190
237
  async function sendCode (value: any, factorMode: 'TOTP' | 'passkey', passkeyOptions: any) {
191
238
  inProgress.value = true;
192
239
  const usePasskey = factorMode === 'passkey';
240
+ console.log("Sending code with factorMode:", factorMode);
241
+ console.log("Passkey options:", passkeyOptions);
193
242
  const resp = await callAdminForthApi({
194
243
  method: 'POST',
195
244
  path: '/plugin/twofa/confirmLogin',
@@ -200,12 +249,15 @@
200
249
  secret: null,
201
250
  }
202
251
  })
252
+ console.log("Response from confirmLogin:", resp);
203
253
  if ( resp.allowedLogin ) {
204
254
  if ( route.meta.isPasskeysEnabled && !doesUserHavePasskeys.value ) {
205
255
  handlePasskeyAlert(route.meta.suggestionPeriod, router);
206
256
  }
257
+ console.log("Login confirmed, finishing login...");
207
258
  await user.finishLogin();
208
259
  } else {
260
+ console.log("Login not allowed, showing error:", resp.error);
209
261
  if (usePasskey) {
210
262
  showErrorTost(t(resp.error));
211
263
  codeError.value = resp.error || t('Passkey authentication failed');
@@ -249,8 +301,16 @@
249
301
  }
250
302
 
251
303
  async function usePasskeyButton() {
304
+ // Cancel any stray pending requests from earlier flows
305
+ cancelPendingWebAuthn('button-pressed');
306
+ codeError.value = null;
252
307
  isFetchingPasskey.value = true;
253
- const { _options, challengeId } = await createSignInRequest();
308
+ const signIn = await createSignInRequest();
309
+ if (!signIn) {
310
+ isFetchingPasskey.value = false;
311
+ return;
312
+ }
313
+ const { _options, challengeId } = signIn;
254
314
  const options = PublicKeyCredential.parseRequestOptionsFromJSON(_options);
255
315
  const credential = await authenticate(options);
256
316
  if (!credential) {
@@ -269,15 +329,18 @@
269
329
 
270
330
  async function createSignInRequest() {
271
331
  let response;
332
+ console.log("Creating sign-in request for passkey...");
272
333
  try {
273
334
  response = await callAdminForthApi({
274
335
  path: `/plugin/passkeys/signInRequest`,
275
336
  method: 'POST',
276
337
  });
277
338
  } catch (error) {
339
+ console.log("Error creating sign-in request:", error);
278
340
  console.error('Error creating sign-in request:', error);
279
341
  return;
280
342
  }
343
+ console.log("Sign-in request response:", response);
281
344
  if (response.ok === true) {
282
345
  return { _options: response.data, challengeId: response.challengeId };
283
346
  } else {
@@ -287,25 +350,54 @@
287
350
  }
288
351
 
289
352
  async function authenticate(options) {
353
+ console.log("Authenticating with options:", options);
290
354
  try {
355
+ // Guard: prevent concurrent navigator.credentials.get calls
356
+ if (webAuthn.inFlight) {
357
+ console.warn('[WebAuthn] A request is already in flight. Aborting previous and retrying.');
358
+ cancelPendingWebAuthn('pre-auth-guard');
359
+ }
360
+
291
361
  const abortController = new AbortController();
362
+ webAuthn.controller = abortController;
363
+ webAuthn.inFlight = true;
364
+
292
365
  const credential = await navigator.credentials.get({
293
366
  publicKey: options,
294
367
  signal: abortController.signal,
368
+ // mediation can be set if using conditional UI, omitted here intentionally
295
369
  });
370
+ console.log("Credential obtained:", credential);
296
371
  return credential;
297
372
  } catch (error) {
298
373
  console.error('Error during authentication:', error);
374
+ // Handle specific concurrent/pending request error cases gracefully
375
+ const name = (error && (error.name || error.constructor?.name)) || '';
376
+ const message = (error && error.message) || '';
377
+ if (name === 'AbortError') {
378
+ // Aborted intentionally; no user-facing error needed
379
+ return null;
380
+ }
381
+ if (name === 'InvalidStateError' || name === 'OperationError' || /pending/i.test(message)) {
382
+ adminforth.alert({ message: t('Another security prompt is already open. Please try again.'), variant: 'warning' });
383
+ codeError.value = t('A previous passkey attempt was still pending. Please try again.');
384
+ return null;
385
+ }
299
386
  adminforth.alert({message: `Error during authentication: ${error}`, variant: 'warning'});
300
387
  codeError.value = 'Error during authentication.';
301
388
  }
389
+ finally {
390
+ // Clear in-flight state regardless of outcome
391
+ webAuthn.inFlight = false;
392
+ webAuthn.controller = null;
393
+ }
302
394
  }
303
395
 
304
396
 
305
- function getOtpInputs() {
397
+ function getOtpInputs(): HTMLInputElement[] {
306
398
  const root = otpRoot.value;
307
399
  if (!root) return [];
308
- return Array.from(root.querySelectorAll('input.otp-input'));
400
+ return Array.from(root.querySelectorAll('input.otp-input')) as HTMLInputElement[];
309
401
  }
310
402
 
311
403
  function focusFirstAvailableOtpInput() {
@@ -318,7 +410,7 @@
318
410
  function handleGlobalFocusIn(event) {
319
411
  const inputs = getOtpInputs();
320
412
  if (!inputs.length) return;
321
- const target = event.target;
413
+ const target = event.target as HTMLInputElement | null;
322
414
  if (!target) return;
323
415
  if (!inputs.includes(target)) {
324
416
  requestAnimationFrame(() => {
@@ -331,8 +423,9 @@
331
423
  requestAnimationFrame(() => {
332
424
  const inputs = getOtpInputs();
333
425
  if (!inputs.length) return;
334
- const active = document.activeElement;
335
- if (!active || !inputs.includes(active)) {
426
+ const active = document.activeElement as Element | null;
427
+ const activeInput = active instanceof HTMLInputElement ? active : null;
428
+ if (!activeInput || !inputs.includes(activeInput)) {
336
429
  focusFirstAvailableOtpInput();
337
430
  }
338
431
  });
@@ -327,6 +327,8 @@
327
327
  }
328
328
 
329
329
  function checkForCompatibility() {
330
+ let isCMA_supported = false;
331
+ let isParsingSupported = false;
330
332
  if (window.PublicKeyCredential &&
331
333
  PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable &&
332
334
  PublicKeyCredential.isConditionalMediationAvailable) {
@@ -335,13 +337,23 @@
335
337
  PublicKeyCredential.isConditionalMediationAvailable(),
336
338
  ]).then(results => {
337
339
  if (results.every(r => r === true)) {
338
- isPasskeySupported.value = true;
340
+ isCMA_supported = true;
339
341
  } else {
340
- isPasskeySupported.value = false;
342
+ isCMA_supported = false;
341
343
  addPasskeyMode.value = 'cross-platform';
342
344
  }
343
345
  });
344
- }
346
+ }
347
+ try {
348
+ const temp = PublicKeyCredential.parseRequestOptionsFromJSON({challenge: 'test123', timeout: 60000, userVerification: 'required'});
349
+ if (temp && typeof temp === 'object') {
350
+ isParsingSupported = true;
351
+ }
352
+ } catch (e) {
353
+ adminforth.alert({message: 'Please ensure your browser supports passkeys.', variant: 'warning'});
354
+ isParsingSupported = false;
355
+ }
356
+ isPasskeySupported.value = (isCMA_supported && isParsingSupported);
345
357
  }
346
358
 
347
359
  async function fetchInformationFromTheBackend(confirmationResult) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adminforth/two-factors-auth",
3
- "version": "2.1.1",
3
+ "version": "2.1.3",
4
4
  "description": "AdminForth plugin for two factors authentication",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",