@mrgnw/anahtar 0.0.11 → 0.0.12

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.
@@ -1,14 +1,19 @@
1
1
  <script lang="ts">
2
2
  import { guessDeviceName } from '../device.js';
3
+ import { resolveMessages, detectLocaleClient, type AuthMessages } from '../i18n/index.js';
3
4
  import OtpInput from './OtpInput.svelte';
4
5
  import PasskeyPrompt from './PasskeyPrompt.svelte';
5
6
 
6
7
  interface Props {
7
8
  apiBase?: string;
9
+ locale?: string;
10
+ messages?: Partial<AuthMessages>;
8
11
  onSuccess?: () => void;
9
12
  }
10
13
 
11
- let { apiBase = '/api/auth', onSuccess }: Props = $props();
14
+ let { apiBase = '/api/auth', locale, messages: messageOverrides, onSuccess }: Props = $props();
15
+
16
+ let m = $derived(resolveMessages(locale ?? detectLocaleClient(), messageOverrides));
12
17
 
13
18
  let step = $state<1 | 2 | 3 | 4>(1);
14
19
  let congratsTimeout: ReturnType<typeof setTimeout> | null = null;
@@ -38,6 +43,8 @@ async function tryConditionalWebAuthn() {
38
43
  optionsJSON: options,
39
44
  useBrowserAutofill: true
40
45
  });
46
+ // User selected a passkey — show loading while we verify
47
+ loading = true;
41
48
  const verifyRes = await fetch(`${apiBase}/passkey/login-finish`, {
42
49
  method: 'POST',
43
50
  headers: { 'Content-Type': 'application/json' },
@@ -48,13 +55,15 @@ async function tryConditionalWebAuthn() {
48
55
  }
49
56
  } catch {
50
57
  // Passkey autofill not available or cancelled
58
+ } finally {
59
+ loading = false;
51
60
  }
52
61
  }
53
62
 
54
63
  async function handleEmailSubmit() {
55
64
  error = '';
56
65
  if (!email.includes('@')) {
57
- error = 'Please enter a valid email address.';
66
+ error = m.errorInvalidEmail;
58
67
  return;
59
68
  }
60
69
  loading = true;
@@ -73,7 +82,7 @@ async function handleEmailSubmit() {
73
82
  }
74
83
  step = 2;
75
84
  } catch {
76
- error = 'Something went wrong. Please try again.';
85
+ error = m.errorGeneric;
77
86
  } finally {
78
87
  loading = false;
79
88
  }
@@ -90,7 +99,7 @@ async function handleOtpComplete(code: string) {
90
99
  });
91
100
  if (!res.ok) {
92
101
  const data = await res.json().catch(() => null);
93
- error = data?.error ?? 'Invalid code. Please try again.';
102
+ error = data?.error ?? m.errorInvalidCode;
94
103
  otpInput?.clear();
95
104
  return;
96
105
  }
@@ -101,7 +110,7 @@ async function handleOtpComplete(code: string) {
101
110
  step = 3;
102
111
  }
103
112
  } catch {
104
- error = 'Something went wrong. Please try again.';
113
+ error = m.errorGeneric;
105
114
  } finally {
106
115
  loading = false;
107
116
  }
@@ -118,12 +127,12 @@ async function resendCode() {
118
127
  });
119
128
  if (!res.ok) {
120
129
  const data = await res.json().catch(() => null);
121
- error = data?.error ?? 'Failed to resend code.';
130
+ error = data?.error ?? m.errorResendFailed;
122
131
  return;
123
132
  }
124
133
  otpInput?.clear();
125
134
  } catch {
126
- error = 'Something went wrong. Please try again.';
135
+ error = m.errorGeneric;
127
136
  } finally {
128
137
  loading = false;
129
138
  }
@@ -166,7 +175,7 @@ function handlePasskeySkip() {
166
175
  bind:value={email}
167
176
  required
168
177
  autocomplete="username webauthn"
169
- placeholder="you@example.com"
178
+ placeholder={m.emailPlaceholder}
170
179
  class="anahtar-input"
171
180
  />
172
181
 
@@ -175,12 +184,12 @@ function handlePasskeySkip() {
175
184
  {/if}
176
185
 
177
186
  <button type="submit" disabled={loading} class="anahtar-button">
178
- {loading ? '...' : 'Continue'}
187
+ {loading ? '...' : m.continue}
179
188
  </button>
180
189
  </form>
181
190
  {:else if step === 2}
182
191
  <div class="anahtar-otp-step">
183
- <p class="anahtar-subtitle">We sent a code to</p>
192
+ <p class="anahtar-subtitle">{m.codeSentTo}</p>
184
193
  <p class="anahtar-email">{email}</p>
185
194
 
186
195
  <OtpInput bind:this={otpInput} onComplete={handleOtpComplete} disabled={loading} />
@@ -190,12 +199,12 @@ function handlePasskeySkip() {
190
199
  {/if}
191
200
 
192
201
  {#if loading}
193
- <p class="anahtar-subtitle">Verifying...</p>
202
+ <p class="anahtar-subtitle">{m.verifying}</p>
194
203
  {/if}
195
204
 
196
205
  <div class="anahtar-links">
197
206
  <button onclick={resendCode} disabled={loading} class="anahtar-link">
198
- Didn't get it? Resend
207
+ {m.resend}
199
208
  </button>
200
209
  <button
201
210
  onclick={() => {
@@ -204,12 +213,12 @@ function handlePasskeySkip() {
204
213
  }}
205
214
  class="anahtar-link"
206
215
  >
207
- Use a different email
216
+ {m.differentEmail}
208
217
  </button>
209
218
  </div>
210
219
  </div>
211
220
  {:else if step === 3}
212
- <PasskeyPrompt onRegister={handlePasskeyRegister} onSkip={handlePasskeySkip} />
221
+ <PasskeyPrompt {m} onRegister={handlePasskeyRegister} onSkip={handlePasskeySkip} />
213
222
  {:else if step === 4}
214
223
  <div class="anahtar-congrats">
215
224
  <div class="anahtar-congrats-icon">
@@ -220,9 +229,9 @@ function handlePasskeySkip() {
220
229
  <path d="m17.5 4.5 2 2"/>
221
230
  </svg>
222
231
  </div>
223
- <p class="anahtar-congrats-title">You're a passkey!</p>
232
+ <p class="anahtar-congrats-title">{m.passkeySuccess}</p>
224
233
  <button onclick={() => onSuccess?.()} class="anahtar-button">
225
- Continue
234
+ {m.continue}
226
235
  </button>
227
236
  </div>
228
237
  {/if}
@@ -1,5 +1,8 @@
1
+ import { type AuthMessages } from '../i18n/index.js';
1
2
  interface Props {
2
3
  apiBase?: string;
4
+ locale?: string;
5
+ messages?: Partial<AuthMessages>;
3
6
  onSuccess?: () => void;
4
7
  }
5
8
  declare const AuthFlow: import("svelte").Component<Props, {}, "">;
@@ -1,11 +1,14 @@
1
1
  <script lang="ts">
2
+ import type { AuthMessages } from '../i18n/types.js';
3
+
2
4
  interface Props {
5
+ m: AuthMessages;
3
6
  countdownSeconds?: number;
4
7
  onRegister: () => Promise<void>;
5
8
  onSkip: () => void;
6
9
  }
7
10
 
8
- let { countdownSeconds = 3, onRegister, onSkip }: Props = $props();
11
+ let { m, countdownSeconds = 3, onRegister, onSkip }: Props = $props();
9
12
 
10
13
  let countdown = $state(countdownSeconds);
11
14
  let failed = $state(false);
@@ -70,15 +73,15 @@ let dashOffset = $derived(circumference * (1 - countdown / countdownSeconds));
70
73
  </div>
71
74
 
72
75
  {#if !failed}
73
- <p class="anahtar-passkey-title">Making you a passkey</p>
74
- <p class="anahtar-passkey-subtitle">for easier login</p>
75
- <button onclick={onSkip} class="anahtar-passkey-skip">Skip</button>
76
+ <p class="anahtar-passkey-title">{m.passkeyCreating}</p>
77
+ <p class="anahtar-passkey-subtitle">{m.passkeySubtitle}</p>
78
+ <button onclick={onSkip} class="anahtar-passkey-skip">{m.passkeySkip}</button>
76
79
  {:else}
77
- <p class="anahtar-passkey-title">Set up a passkey?</p>
80
+ <p class="anahtar-passkey-title">{m.passkeySetup}</p>
78
81
  <button onclick={triggerRegistration} class="anahtar-passkey-add" disabled={registering}>
79
- Add passkey
82
+ {m.passkeyAdd}
80
83
  </button>
81
- <button onclick={onSkip} class="anahtar-passkey-skip">Maybe later</button>
84
+ <button onclick={onSkip} class="anahtar-passkey-skip">{m.passkeyMaybeLater}</button>
82
85
  {/if}
83
86
  </div>
84
87
 
@@ -1,4 +1,6 @@
1
+ import type { AuthMessages } from '../i18n/types.js';
1
2
  interface Props {
3
+ m: AuthMessages;
2
4
  countdownSeconds?: number;
3
5
  onRegister: () => Promise<void>;
4
6
  onSkip: () => void;
@@ -2,3 +2,5 @@ export { guessDeviceName } from '../device.js';
2
2
  export { default as AuthFlow } from './AuthFlow.svelte';
3
3
  export { default as OtpInput } from './OtpInput.svelte';
4
4
  export { default as PasskeyPrompt } from './PasskeyPrompt.svelte';
5
+ export type { AuthMessages } from '../i18n/types.js';
6
+ export { resolveMessages, detectLocaleClient, locales } from '../i18n/index.js';
@@ -2,3 +2,4 @@ export { guessDeviceName } from '../device.js';
2
2
  export { default as AuthFlow } from './AuthFlow.svelte';
3
3
  export { default as OtpInput } from './OtpInput.svelte';
4
4
  export { default as PasskeyPrompt } from './PasskeyPrompt.svelte';
5
+ export { resolveMessages, detectLocaleClient, locales } from '../i18n/index.js';
@@ -0,0 +1,3 @@
1
+ import type { AuthMessages } from './types.js';
2
+ declare const ar: AuthMessages;
3
+ export default ar;
@@ -0,0 +1,28 @@
1
+ const ar = {
2
+ emailPlaceholder: 'you@example.com',
3
+ continue: 'متابعة',
4
+ codeSentTo: 'أرسلنا رمزًا إلى',
5
+ verifying: 'جارٍ التحقق...',
6
+ resend: 'لم يصلك؟ إعادة الإرسال',
7
+ differentEmail: 'استخدام بريد إلكتروني آخر',
8
+ passkeyCreating: 'جارٍ إنشاء مفتاح المرور',
9
+ passkeySubtitle: 'لتسجيل دخول أسهل',
10
+ passkeySkip: 'تخطي',
11
+ passkeySetup: 'إعداد مفتاح مرور؟',
12
+ passkeyAdd: 'إضافة مفتاح مرور',
13
+ passkeyMaybeLater: 'ربما لاحقًا',
14
+ passkeySuccess: 'مفتاح المرور جاهز!',
15
+ errorInvalidEmail: 'يرجى إدخال بريد إلكتروني صالح.',
16
+ errorGeneric: 'حدث خطأ ما. يرجى المحاولة مرة أخرى.',
17
+ errorResendFailed: 'فشل إعادة إرسال الرمز.',
18
+ errorInvalidCode: 'رمز غير صالح. يرجى المحاولة مرة أخرى.',
19
+ errorCodeExpired: 'انتهت صلاحية الرمز. يرجى طلب رمز جديد.',
20
+ errorTooManyAttempts: 'محاولات كثيرة جدًا. يرجى طلب رمز جديد.',
21
+ errorInvalidInput: 'إدخال غير صالح',
22
+ errorNotAuthenticated: 'غير مسجل الدخول',
23
+ errorNotFound: 'غير موجود',
24
+ errorAuthFailed: 'فشل التحقق من الهوية',
25
+ errorPasskeyRegFailed: 'فشل تسجيل مفتاح المرور',
26
+ errorPasskeyNotFound: 'مفتاح المرور غير موجود',
27
+ };
28
+ export default ar;
@@ -0,0 +1,3 @@
1
+ import type { AuthMessages } from './types.js';
2
+ declare const de: AuthMessages;
3
+ export default de;
@@ -0,0 +1,28 @@
1
+ const de = {
2
+ emailPlaceholder: 'du@beispiel.de',
3
+ continue: 'Weiter',
4
+ codeSentTo: 'Wir haben einen Code gesendet an',
5
+ verifying: 'Wird uberpruft...',
6
+ resend: 'Nicht erhalten? Erneut senden',
7
+ differentEmail: 'Andere E-Mail verwenden',
8
+ passkeyCreating: 'Passkey wird erstellt',
9
+ passkeySubtitle: 'fur einfacheres Anmelden',
10
+ passkeySkip: 'Uberspringen',
11
+ passkeySetup: 'Passkey einrichten?',
12
+ passkeyAdd: 'Passkey hinzufugen',
13
+ passkeyMaybeLater: 'Vielleicht spater',
14
+ passkeySuccess: 'Dein Passkey ist bereit!',
15
+ errorInvalidEmail: 'Bitte gib eine gultige E-Mail-Adresse ein.',
16
+ errorGeneric: 'Etwas ist schiefgelaufen. Bitte versuche es erneut.',
17
+ errorResendFailed: 'Code konnte nicht erneut gesendet werden.',
18
+ errorInvalidCode: 'Ungueltiger Code. Bitte versuche es erneut.',
19
+ errorCodeExpired: 'Code abgelaufen. Bitte fordere einen neuen an.',
20
+ errorTooManyAttempts: 'Zu viele Versuche. Bitte fordere einen neuen Code an.',
21
+ errorInvalidInput: 'Ungueltige Eingabe',
22
+ errorNotAuthenticated: 'Nicht authentifiziert',
23
+ errorNotFound: 'Nicht gefunden',
24
+ errorAuthFailed: 'Authentifizierung fehlgeschlagen',
25
+ errorPasskeyRegFailed: 'Passkey-Registrierung fehlgeschlagen',
26
+ errorPasskeyNotFound: 'Passkey nicht gefunden',
27
+ };
28
+ export default de;
@@ -0,0 +1,3 @@
1
+ import type { AuthMessages } from './types.js';
2
+ declare const en: AuthMessages;
3
+ export default en;
@@ -0,0 +1,28 @@
1
+ const en = {
2
+ emailPlaceholder: 'you@example.com',
3
+ continue: 'Continue',
4
+ codeSentTo: 'We sent a code to',
5
+ verifying: 'Verifying...',
6
+ resend: "Didn't get it? Resend",
7
+ differentEmail: 'Use a different email',
8
+ passkeyCreating: 'Making you a passkey',
9
+ passkeySubtitle: 'for easier login',
10
+ passkeySkip: 'Skip',
11
+ passkeySetup: 'Set up a passkey?',
12
+ passkeyAdd: 'Add passkey',
13
+ passkeyMaybeLater: 'Maybe later',
14
+ passkeySuccess: "You've got a passkey!",
15
+ errorInvalidEmail: 'Please enter a valid email address.',
16
+ errorGeneric: 'Something went wrong. Please try again.',
17
+ errorResendFailed: 'Failed to resend code.',
18
+ errorInvalidCode: 'Invalid code. Please try again.',
19
+ errorCodeExpired: 'Code expired. Please request a new one.',
20
+ errorTooManyAttempts: 'Too many attempts. Please request a new code.',
21
+ errorInvalidInput: 'Invalid input',
22
+ errorNotAuthenticated: 'Not authenticated',
23
+ errorNotFound: 'Not found',
24
+ errorAuthFailed: 'Authentication failed',
25
+ errorPasskeyRegFailed: 'Passkey registration failed',
26
+ errorPasskeyNotFound: 'Passkey not found',
27
+ };
28
+ export default en;
@@ -0,0 +1,3 @@
1
+ import type { AuthMessages } from './types.js';
2
+ declare const es: AuthMessages;
3
+ export default es;
@@ -0,0 +1,28 @@
1
+ const es = {
2
+ emailPlaceholder: 'tu@ejemplo.com',
3
+ continue: 'Continuar',
4
+ codeSentTo: 'Enviamos un codigo a',
5
+ verifying: 'Verificando...',
6
+ resend: 'No lo recibiste? Reenviar',
7
+ differentEmail: 'Usar otro correo',
8
+ passkeyCreating: 'Creando tu passkey',
9
+ passkeySubtitle: 'para iniciar sesion mas facil',
10
+ passkeySkip: 'Omitir',
11
+ passkeySetup: 'Configurar passkey?',
12
+ passkeyAdd: 'Agregar passkey',
13
+ passkeyMaybeLater: 'Quiza despues',
14
+ passkeySuccess: 'Ya tienes tu passkey!',
15
+ errorInvalidEmail: 'Ingresa un correo electronico valido.',
16
+ errorGeneric: 'Algo salio mal. Intenta de nuevo.',
17
+ errorResendFailed: 'No se pudo reenviar el codigo.',
18
+ errorInvalidCode: 'Codigo invalido. Intenta de nuevo.',
19
+ errorCodeExpired: 'El codigo expiro. Solicita uno nuevo.',
20
+ errorTooManyAttempts: 'Demasiados intentos. Solicita un nuevo codigo.',
21
+ errorInvalidInput: 'Entrada invalida',
22
+ errorNotAuthenticated: 'No autenticado',
23
+ errorNotFound: 'No encontrado',
24
+ errorAuthFailed: 'Fallo la autenticacion',
25
+ errorPasskeyRegFailed: 'Fallo el registro del passkey',
26
+ errorPasskeyNotFound: 'Passkey no encontrado',
27
+ };
28
+ export default es;
@@ -0,0 +1,3 @@
1
+ import type { AuthMessages } from './types.js';
2
+ declare const fr: AuthMessages;
3
+ export default fr;
@@ -0,0 +1,28 @@
1
+ const fr = {
2
+ emailPlaceholder: 'vous@exemple.com',
3
+ continue: 'Continuer',
4
+ codeSentTo: 'Nous avons envoye un code a',
5
+ verifying: 'Verification...',
6
+ resend: 'Pas recu ? Renvoyer',
7
+ differentEmail: 'Utiliser un autre e-mail',
8
+ passkeyCreating: 'Creation de votre passkey',
9
+ passkeySubtitle: 'pour une connexion plus facile',
10
+ passkeySkip: 'Passer',
11
+ passkeySetup: 'Configurer un passkey ?',
12
+ passkeyAdd: 'Ajouter un passkey',
13
+ passkeyMaybeLater: 'Plus tard',
14
+ passkeySuccess: 'Votre passkey est pret !',
15
+ errorInvalidEmail: 'Veuillez entrer une adresse e-mail valide.',
16
+ errorGeneric: 'Une erreur est survenue. Veuillez reessayer.',
17
+ errorResendFailed: "Echec de l'envoi du code.",
18
+ errorInvalidCode: 'Code invalide. Veuillez reessayer.',
19
+ errorCodeExpired: 'Code expire. Veuillez en demander un nouveau.',
20
+ errorTooManyAttempts: 'Trop de tentatives. Veuillez demander un nouveau code.',
21
+ errorInvalidInput: 'Entree invalide',
22
+ errorNotAuthenticated: 'Non authentifie',
23
+ errorNotFound: 'Non trouve',
24
+ errorAuthFailed: "Echec de l'authentification",
25
+ errorPasskeyRegFailed: "Echec de l'enregistrement du passkey",
26
+ errorPasskeyNotFound: 'Passkey introuvable',
27
+ };
28
+ export default fr;
@@ -0,0 +1,7 @@
1
+ import type { AuthMessages } from './types.js';
2
+ export type { AuthMessages } from './types.js';
3
+ export { default as en } from './en.js';
4
+ export declare const locales: Record<string, AuthMessages>;
5
+ export declare function resolveMessages(locale?: string, overrides?: Partial<AuthMessages>): AuthMessages;
6
+ export declare function detectLocaleClient(): string;
7
+ export declare function detectLocaleServer(request: Request): string;
@@ -0,0 +1,41 @@
1
+ import en from './en.js';
2
+ import ar from './ar.js';
3
+ import de from './de.js';
4
+ import es from './es.js';
5
+ import fr from './fr.js';
6
+ import ja from './ja.js';
7
+ import ko from './ko.js';
8
+ import pt from './pt.js';
9
+ import tr from './tr.js';
10
+ import zh from './zh.js';
11
+ export { default as en } from './en.js';
12
+ export const locales = {
13
+ en,
14
+ ar,
15
+ de,
16
+ es,
17
+ fr,
18
+ ja,
19
+ ko,
20
+ pt,
21
+ tr,
22
+ zh,
23
+ };
24
+ export function resolveMessages(locale, overrides) {
25
+ const lang = locale?.split('-')[0]?.toLowerCase();
26
+ const base = (lang && locales[lang]) || en;
27
+ return overrides ? { ...base, ...overrides } : base;
28
+ }
29
+ export function detectLocaleClient() {
30
+ if (typeof navigator !== 'undefined') {
31
+ return navigator.language?.split('-')[0] ?? 'en';
32
+ }
33
+ return 'en';
34
+ }
35
+ export function detectLocaleServer(request) {
36
+ const header = request.headers.get('accept-language');
37
+ if (!header)
38
+ return 'en';
39
+ const first = header.split(',')[0];
40
+ return first?.split('-')[0]?.trim() ?? 'en';
41
+ }
@@ -0,0 +1,3 @@
1
+ import type { AuthMessages } from './types.js';
2
+ declare const ja: AuthMessages;
3
+ export default ja;
@@ -0,0 +1,28 @@
1
+ const ja = {
2
+ emailPlaceholder: 'you@example.com',
3
+ continue: '続ける',
4
+ codeSentTo: '確認コードを送信しました:',
5
+ verifying: '確認中...',
6
+ resend: '届きませんか?再送信',
7
+ differentEmail: '別のメールアドレスを使用',
8
+ passkeyCreating: 'パスキーを作成中',
9
+ passkeySubtitle: 'より簡単にログイン',
10
+ passkeySkip: 'スキップ',
11
+ passkeySetup: 'パスキーを設定しますか?',
12
+ passkeyAdd: 'パスキーを追加',
13
+ passkeyMaybeLater: 'あとで',
14
+ passkeySuccess: 'パスキーの準備ができました!',
15
+ errorInvalidEmail: '有効なメールアドレスを入力してください。',
16
+ errorGeneric: 'エラーが発生しました。もう一度お試しください。',
17
+ errorResendFailed: 'コードの再送信に失敗しました。',
18
+ errorInvalidCode: '無効なコードです。もう一度お試しください。',
19
+ errorCodeExpired: 'コードの有効期限が切れました。新しいコードをリクエストしてください。',
20
+ errorTooManyAttempts: '試行回数が多すぎます。新しいコードをリクエストしてください。',
21
+ errorInvalidInput: '無効な入力',
22
+ errorNotAuthenticated: '未認証',
23
+ errorNotFound: '見つかりません',
24
+ errorAuthFailed: '認証に失敗しました',
25
+ errorPasskeyRegFailed: 'パスキーの登録に失敗しました',
26
+ errorPasskeyNotFound: 'パスキーが見つかりません',
27
+ };
28
+ export default ja;
@@ -0,0 +1,3 @@
1
+ import type { AuthMessages } from './types.js';
2
+ declare const ko: AuthMessages;
3
+ export default ko;
@@ -0,0 +1,28 @@
1
+ const ko = {
2
+ emailPlaceholder: 'you@example.com',
3
+ continue: '계속',
4
+ codeSentTo: '인증 코드를 보냈습니다:',
5
+ verifying: '확인 중...',
6
+ resend: '받지 못하셨나요? 다시 보내기',
7
+ differentEmail: '다른 이메일 사용',
8
+ passkeyCreating: '패스키 생성 중',
9
+ passkeySubtitle: '더 쉬운 로그인을 위해',
10
+ passkeySkip: '건너뛰기',
11
+ passkeySetup: '패스키를 설정할까요?',
12
+ passkeyAdd: '패스키 추가',
13
+ passkeyMaybeLater: '나중에',
14
+ passkeySuccess: '패스키가 준비되었습니다!',
15
+ errorInvalidEmail: '유효한 이메일 주소를 입력해주세요.',
16
+ errorGeneric: '문제가 발생했습니다. 다시 시도해주세요.',
17
+ errorResendFailed: '코드 재전송에 실패했습니다.',
18
+ errorInvalidCode: '잘못된 코드입니다. 다시 시도해주세요.',
19
+ errorCodeExpired: '코드가 만료되었습니다. 새 코드를 요청해주세요.',
20
+ errorTooManyAttempts: '시도 횟수가 너무 많습니다. 새 코드를 요청해주세요.',
21
+ errorInvalidInput: '잘못된 입력',
22
+ errorNotAuthenticated: '인증되지 않음',
23
+ errorNotFound: '찾을 수 없음',
24
+ errorAuthFailed: '인증 실패',
25
+ errorPasskeyRegFailed: '패스키 등록 실패',
26
+ errorPasskeyNotFound: '패스키를 찾을 수 없음',
27
+ };
28
+ export default ko;
@@ -0,0 +1,3 @@
1
+ import type { AuthMessages } from './types.js';
2
+ declare const pt: AuthMessages;
3
+ export default pt;
@@ -0,0 +1,28 @@
1
+ const pt = {
2
+ emailPlaceholder: 'voce@exemplo.com',
3
+ continue: 'Continuar',
4
+ codeSentTo: 'Enviamos um codigo para',
5
+ verifying: 'Verificando...',
6
+ resend: 'Nao recebeu? Reenviar',
7
+ differentEmail: 'Usar outro e-mail',
8
+ passkeyCreating: 'Criando sua passkey',
9
+ passkeySubtitle: 'para login mais facil',
10
+ passkeySkip: 'Pular',
11
+ passkeySetup: 'Configurar passkey?',
12
+ passkeyAdd: 'Adicionar passkey',
13
+ passkeyMaybeLater: 'Talvez depois',
14
+ passkeySuccess: 'Sua passkey esta pronta!',
15
+ errorInvalidEmail: 'Digite um endereco de e-mail valido.',
16
+ errorGeneric: 'Algo deu errado. Tente novamente.',
17
+ errorResendFailed: 'Falha ao reenviar o codigo.',
18
+ errorInvalidCode: 'Codigo invalido. Tente novamente.',
19
+ errorCodeExpired: 'Codigo expirado. Solicite um novo.',
20
+ errorTooManyAttempts: 'Muitas tentativas. Solicite um novo codigo.',
21
+ errorInvalidInput: 'Entrada invalida',
22
+ errorNotAuthenticated: 'Nao autenticado',
23
+ errorNotFound: 'Nao encontrado',
24
+ errorAuthFailed: 'Falha na autenticacao',
25
+ errorPasskeyRegFailed: 'Falha no registro da passkey',
26
+ errorPasskeyNotFound: 'Passkey nao encontrada',
27
+ };
28
+ export default pt;
@@ -0,0 +1,3 @@
1
+ import type { AuthMessages } from './types.js';
2
+ declare const tr: AuthMessages;
3
+ export default tr;
@@ -0,0 +1,28 @@
1
+ const tr = {
2
+ emailPlaceholder: 'sen@ornek.com',
3
+ continue: 'Devam',
4
+ codeSentTo: 'Kod gonderildi:',
5
+ verifying: 'Dogrulanıyor...',
6
+ resend: 'Almadın mı? Tekrar gonder',
7
+ differentEmail: 'Baska e-posta kullan',
8
+ passkeyCreating: 'Anahtar olusturuluyor',
9
+ passkeySubtitle: 'daha kolay giris icin',
10
+ passkeySkip: 'Atla',
11
+ passkeySetup: 'Anahtar olustur?',
12
+ passkeyAdd: 'Anahtar ekle',
13
+ passkeyMaybeLater: 'Belki sonra',
14
+ passkeySuccess: 'Anahtarın hazır!',
15
+ errorInvalidEmail: 'Gecerli bir e-posta adresi girin.',
16
+ errorGeneric: 'Bir hata olustu. Lutfen tekrar deneyin.',
17
+ errorResendFailed: 'Kod tekrar gonderilemedi.',
18
+ errorInvalidCode: 'Gecersiz kod. Lutfen tekrar deneyin.',
19
+ errorCodeExpired: 'Kodun suresi doldu. Yeni bir kod isteyin.',
20
+ errorTooManyAttempts: 'Cok fazla deneme. Yeni bir kod isteyin.',
21
+ errorInvalidInput: 'Gecersiz giris',
22
+ errorNotAuthenticated: 'Kimlik dogrulanmadı',
23
+ errorNotFound: 'Bulunamadı',
24
+ errorAuthFailed: 'Kimlik dogrulama basarısız',
25
+ errorPasskeyRegFailed: 'Anahtar kaydı basarısız',
26
+ errorPasskeyNotFound: 'Anahtar bulunamadı',
27
+ };
28
+ export default tr;
@@ -0,0 +1,27 @@
1
+ export interface AuthMessages {
2
+ emailPlaceholder: string;
3
+ continue: string;
4
+ codeSentTo: string;
5
+ verifying: string;
6
+ resend: string;
7
+ differentEmail: string;
8
+ passkeyCreating: string;
9
+ passkeySubtitle: string;
10
+ passkeySkip: string;
11
+ passkeySetup: string;
12
+ passkeyAdd: string;
13
+ passkeyMaybeLater: string;
14
+ passkeySuccess: string;
15
+ errorInvalidEmail: string;
16
+ errorGeneric: string;
17
+ errorResendFailed: string;
18
+ errorInvalidCode: string;
19
+ errorCodeExpired: string;
20
+ errorTooManyAttempts: string;
21
+ errorInvalidInput: string;
22
+ errorNotAuthenticated: string;
23
+ errorNotFound: string;
24
+ errorAuthFailed: string;
25
+ errorPasskeyRegFailed: string;
26
+ errorPasskeyNotFound: string;
27
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ import type { AuthMessages } from './types.js';
2
+ declare const zh: AuthMessages;
3
+ export default zh;
@@ -0,0 +1,28 @@
1
+ const zh = {
2
+ emailPlaceholder: 'you@example.com',
3
+ continue: '继续',
4
+ codeSentTo: '验证码已发送至',
5
+ verifying: '验证中...',
6
+ resend: '没收到?重新发送',
7
+ differentEmail: '使用其他邮箱',
8
+ passkeyCreating: '正在创建通行密钥',
9
+ passkeySubtitle: '更便捷的登录方式',
10
+ passkeySkip: '跳过',
11
+ passkeySetup: '设置通行密钥?',
12
+ passkeyAdd: '添加通行密钥',
13
+ passkeyMaybeLater: '以后再说',
14
+ passkeySuccess: '通行密钥已就绪!',
15
+ errorInvalidEmail: '请输入有效的电子邮箱地址。',
16
+ errorGeneric: '出了点问题,请重试。',
17
+ errorResendFailed: '重新发送失败。',
18
+ errorInvalidCode: '验证码无效,请重试。',
19
+ errorCodeExpired: '验证码已过期,请重新获取。',
20
+ errorTooManyAttempts: '尝试次数过多,请重新获取验证码。',
21
+ errorInvalidInput: '输入无效',
22
+ errorNotAuthenticated: '未登录',
23
+ errorNotFound: '未找到',
24
+ errorAuthFailed: '身份验证失败',
25
+ errorPasskeyRegFailed: '通行密钥注册失败',
26
+ errorPasskeyNotFound: '未找到通行密钥',
27
+ };
28
+ export default zh;
package/dist/index.d.ts CHANGED
@@ -2,6 +2,8 @@ import type { AuthConfig } from './types.js';
2
2
  export { resolveConfig } from './config.js';
3
3
  export { guessDeviceName } from './device.js';
4
4
  export type { AuthConfig, AuthDB, AuthUser, FullPasskeyRecord, MaybePromise, NewPasskey, OTPRecord, OtpResult, PasskeyRecord, ResolvedConfig, SessionRecord } from './types.js';
5
+ export type { AuthMessages } from './i18n/types.js';
6
+ export { resolveMessages, detectLocaleClient, detectLocaleServer, locales } from './i18n/index.js';
5
7
  export declare function createAuth(config: AuthConfig): Promise<{
6
8
  handle: import("@sveltejs/kit").Handle;
7
9
  handlers: {
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@ import { createHandle } from './kit/handle.js';
3
3
  import { createHandlers } from './kit/handlers.js';
4
4
  export { resolveConfig } from './config.js';
5
5
  export { guessDeviceName } from './device.js';
6
+ export { resolveMessages, detectLocaleClient, detectLocaleServer, locales } from './i18n/index.js';
6
7
  export async function createAuth(config) {
7
8
  const resolved = resolveConfig(config);
8
9
  await config.db.init();
@@ -2,11 +2,16 @@ import { json } from '@sveltejs/kit';
2
2
  import { generateOTP, verifyOTP } from '../otp.js';
3
3
  import { generateAuthenticationChallenge, generateRegistrationChallenge, removePasskey, verifyAuthenticationResponse, verifyRegistrationResponse } from '../passkey.js';
4
4
  import { createSession, invalidateSession, validateSession } from '../session.js';
5
+ import { resolveMessages, detectLocaleServer } from '../i18n/index.js';
5
6
  const SESSION_MAX_AGE_SECONDS = 30 * 24 * 60 * 60;
6
- function requireAuth(event) {
7
+ function getMessages(event, config) {
8
+ const locale = config.locale ?? detectLocaleServer(event.request);
9
+ return resolveMessages(locale, config.messages);
10
+ }
11
+ function requireAuth(event, m) {
7
12
  const user = event.locals.user;
8
13
  if (!user)
9
- return json({ error: 'Not authenticated' }, { status: 401 });
14
+ return json({ error: m.errorNotAuthenticated }, { status: 401 });
10
15
  return user;
11
16
  }
12
17
  export function createHandlers(config) {
@@ -24,9 +29,10 @@ export function createHandlers(config) {
24
29
  start: {
25
30
  method: 'POST',
26
31
  handler: async (event) => {
32
+ const m = getMessages(event, config);
27
33
  const body = await event.request.json().catch(() => null);
28
34
  if (!body || typeof body.email !== 'string' || !body.email.includes('@')) {
29
- return json({ error: 'Invalid email' }, { status: 400 });
35
+ return json({ error: m.errorInvalidEmail }, { status: 400 });
30
36
  }
31
37
  const { code } = await generateOTP(config.db, body.email, config);
32
38
  await config.onSendOTP(body.email, code);
@@ -36,16 +42,17 @@ export function createHandlers(config) {
36
42
  verify: {
37
43
  method: 'POST',
38
44
  handler: async (event) => {
45
+ const m = getMessages(event, config);
39
46
  const body = await event.request.json().catch(() => null);
40
47
  if (!body || typeof body.email !== 'string' || typeof body.code !== 'string') {
41
- return json({ error: 'Invalid input' }, { status: 400 });
48
+ return json({ error: m.errorInvalidInput }, { status: 400 });
42
49
  }
43
50
  const otp = await verifyOTP(config.db, body.email, body.code, config);
44
51
  if (!otp.ok) {
45
52
  const messages = {
46
- invalid: 'Invalid code. Please try again.',
47
- expired: 'Code expired. Please request a new one.',
48
- rate_limited: 'Too many attempts. Please request a new code.'
53
+ invalid: m.errorInvalidCode,
54
+ expired: m.errorCodeExpired,
55
+ rate_limited: m.errorTooManyAttempts,
49
56
  };
50
57
  return json({ error: messages[otp.error] }, { status: otp.error === 'rate_limited' ? 429 : 400 });
51
58
  }
@@ -87,12 +94,13 @@ export function createHandlers(config) {
87
94
  'passkey/login-finish': {
88
95
  method: 'POST',
89
96
  handler: async (event) => {
97
+ const m = getMessages(event, config);
90
98
  const body = await event.request.json().catch(() => null);
91
99
  if (!body)
92
- return json({ error: 'Invalid input' }, { status: 400 });
100
+ return json({ error: m.errorInvalidInput }, { status: 400 });
93
101
  const result = await verifyAuthenticationResponse(config.db, body, event.url);
94
102
  if (!result)
95
- return json({ error: 'Authentication failed' }, { status: 401 });
103
+ return json({ error: m.errorAuthFailed }, { status: 401 });
96
104
  const session = await createSession(config.db, result.user.id, config);
97
105
  event.cookies.set(config.cookie, session.sessionToken, cookieOpts(event));
98
106
  return json({ user: result.user });
@@ -101,7 +109,8 @@ export function createHandlers(config) {
101
109
  'passkey/register-start': {
102
110
  method: 'POST',
103
111
  handler: async (event) => {
104
- const user = requireAuth(event);
112
+ const m = getMessages(event, config);
113
+ const user = requireAuth(event, m);
105
114
  if (user instanceof Response)
106
115
  return user;
107
116
  const options = await generateRegistrationChallenge(config.db, user, event.url, config);
@@ -111,18 +120,19 @@ export function createHandlers(config) {
111
120
  'passkey/register-finish': {
112
121
  method: 'POST',
113
122
  handler: async (event) => {
114
- const user = requireAuth(event);
123
+ const m = getMessages(event, config);
124
+ const user = requireAuth(event, m);
115
125
  if (user instanceof Response)
116
126
  return user;
117
127
  const body = await event.request.json().catch(() => null);
118
128
  if (!body)
119
- return json({ error: 'Invalid input' }, { status: 400 });
129
+ return json({ error: m.errorInvalidInput }, { status: 400 });
120
130
  const { name, ...response } = body;
121
131
  const passkeyName = typeof name === 'string' && name.trim() ? name.trim() : null;
122
132
  const result = await verifyRegistrationResponse(config.db, user.id, response, event.url, passkeyName);
123
133
  if (!result.ok) {
124
134
  console.error('register-finish failed:', result.reason);
125
- return json({ error: 'Passkey registration failed', reason: result.reason }, { status: 400 });
135
+ return json({ error: m.errorPasskeyRegFailed, reason: result.reason }, { status: 400 });
126
136
  }
127
137
  return json({ success: true });
128
138
  }
@@ -130,23 +140,25 @@ export function createHandlers(config) {
130
140
  'passkey/remove': {
131
141
  method: 'POST',
132
142
  handler: async (event) => {
133
- const user = requireAuth(event);
143
+ const m = getMessages(event, config);
144
+ const user = requireAuth(event, m);
134
145
  if (user instanceof Response)
135
146
  return user;
136
147
  const body = await event.request.json().catch(() => null);
137
148
  if (!body || typeof body.passkeyId !== 'string') {
138
- return json({ error: 'Invalid input' }, { status: 400 });
149
+ return json({ error: m.errorInvalidInput }, { status: 400 });
139
150
  }
140
151
  const success = await removePasskey(config.db, body.passkeyId, user.id);
141
152
  if (!success)
142
- return json({ error: 'Passkey not found' }, { status: 404 });
153
+ return json({ error: m.errorPasskeyNotFound }, { status: 404 });
143
154
  return json({ success: true });
144
155
  }
145
156
  },
146
157
  'skip-passkey': {
147
158
  method: 'POST',
148
159
  handler: async (event) => {
149
- const user = requireAuth(event);
160
+ const m = getMessages(event, config);
161
+ const user = requireAuth(event, m);
150
162
  if (user instanceof Response)
151
163
  return user;
152
164
  await config.db.setSkipPasskeyPrompt(user.id, true);
@@ -162,22 +174,24 @@ export function createHandlers(config) {
162
174
  }
163
175
  return {
164
176
  GET: async (event) => {
177
+ const m = getMessages(event, config);
165
178
  const path = getRoute(event);
166
179
  if (!path)
167
- return json({ error: 'Not found' }, { status: 404 });
180
+ return json({ error: m.errorNotFound }, { status: 404 });
168
181
  const route = routes[path];
169
182
  if (!route || route.method !== 'GET') {
170
- return json({ error: 'Not found' }, { status: 404 });
183
+ return json({ error: m.errorNotFound }, { status: 404 });
171
184
  }
172
185
  return route.handler(event);
173
186
  },
174
187
  POST: async (event) => {
188
+ const m = getMessages(event, config);
175
189
  const path = getRoute(event);
176
190
  if (!path)
177
- return json({ error: 'Not found' }, { status: 404 });
191
+ return json({ error: m.errorNotFound }, { status: 404 });
178
192
  const route = routes[path];
179
193
  if (!route || route.method !== 'POST') {
180
- return json({ error: 'Not found' }, { status: 404 });
194
+ return json({ error: m.errorNotFound }, { status: 404 });
181
195
  }
182
196
  return route.handler(event);
183
197
  }
package/dist/types.d.ts CHANGED
@@ -86,8 +86,12 @@ export interface AuthConfig {
86
86
  otpLength?: number;
87
87
  otpMaxAttempts?: number;
88
88
  rpName?: string;
89
+ locale?: string;
90
+ messages?: Partial<import('./i18n/types.js').AuthMessages>;
89
91
  onSendOTP: (email: string, code: string) => Promise<void>;
90
92
  }
91
- export interface ResolvedConfig extends Required<Omit<AuthConfig, 'onSendOTP'>> {
93
+ export interface ResolvedConfig extends Required<Omit<AuthConfig, 'onSendOTP' | 'locale' | 'messages'>> {
94
+ locale?: string;
95
+ messages?: Partial<import('./i18n/types.js').AuthMessages>;
92
96
  onSendOTP: (email: string, code: string) => Promise<void>;
93
97
  }
package/package.json CHANGED
@@ -1,92 +1,92 @@
1
1
  {
2
- "name": "@mrgnw/anahtar",
3
- "version": "0.0.11",
4
- "description": "Opinionated, reusable auth for SvelteKit. Email+OTP + passkeys.",
5
- "license": "MIT",
6
- "type": "module",
7
- "repository": {
8
- "type": "git",
9
- "url": "https://github.com/mrgnw/anahtar.git"
10
- },
11
- "publishConfig": {
12
- "registry": "https://registry.npmjs.org/",
13
- "access": "public"
14
- },
15
- "scripts": {
16
- "build": "svelte-package",
17
- "check": "svelte-check --tsconfig ./tsconfig.json",
18
- "test": "vitest run --config vitest.unit.ts && vitest run --config vitest.browser.ts",
19
- "test:unit": "vitest run --config vitest.unit.ts",
20
- "test:browser": "vitest run --config vitest.browser.ts",
21
- "prepublishOnly": "pnpm build"
22
- },
23
- "svelte": "./dist/index.js",
24
- "types": "./dist/index.d.ts",
25
- "exports": {
26
- ".": {
27
- "types": "./dist/index.d.ts",
28
- "svelte": "./dist/index.js",
29
- "default": "./dist/index.js"
30
- },
31
- "./sqlite": {
32
- "types": "./dist/db/sqlite.d.ts",
33
- "default": "./dist/db/sqlite.js"
34
- },
35
- "./postgres": {
36
- "types": "./dist/db/postgres.d.ts",
37
- "default": "./dist/db/postgres.js"
38
- },
39
- "./d1": {
40
- "types": "./dist/db/d1.d.ts",
41
- "default": "./dist/db/d1.js"
42
- },
43
- "./components": {
44
- "types": "./dist/components/index.d.ts",
45
- "svelte": "./dist/components/index.js",
46
- "default": "./dist/components/index.js"
47
- }
48
- },
49
- "files": [
50
- "dist",
51
- "!dist/**/*.test.*"
52
- ],
53
- "peerDependencies": {
54
- "@simplewebauthn/browser": "^13.0.0",
55
- "@sveltejs/kit": "^2.0.0",
56
- "svelte": "^5.0.0"
57
- },
58
- "peerDependenciesMeta": {
59
- "svelte": {
60
- "optional": true
61
- },
62
- "@simplewebauthn/browser": {
63
- "optional": true
64
- }
65
- },
66
- "dependencies": {
67
- "@oslojs/crypto": "^1.0.1",
68
- "@oslojs/encoding": "^1.1.0",
69
- "@simplewebauthn/server": "^13.2.2"
70
- },
71
- "pnpm": {
72
- "onlyBuiltDependencies": [
73
- "better-sqlite3"
74
- ]
75
- },
76
- "devDependencies": {
77
- "@simplewebauthn/browser": "^13.2.2",
78
- "@sveltejs/kit": "^2.31.1",
79
- "@sveltejs/package": "^2.3.7",
80
- "@sveltejs/vite-plugin-svelte": "^6.2.4",
81
- "@testing-library/jest-dom": "^6.9.1",
82
- "@testing-library/svelte": "^5.3.1",
83
- "@testing-library/user-event": "^14.6.1",
84
- "@types/better-sqlite3": "^7.6.13",
85
- "better-sqlite3": "^12.6.2",
86
- "happy-dom": "^20.6.1",
87
- "svelte": "^5.38.1",
88
- "svelte-check": "^4.3.1",
89
- "typescript": "^5.9.2",
90
- "vitest": "^3.2.1"
91
- }
2
+ "name": "@mrgnw/anahtar",
3
+ "version": "0.0.12",
4
+ "description": "Opinionated, reusable auth for SvelteKit. Email+OTP + passkeys.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/mrgnw/anahtar.git"
10
+ },
11
+ "publishConfig": {
12
+ "registry": "https://registry.npmjs.org/",
13
+ "access": "public"
14
+ },
15
+ "scripts": {
16
+ "build": "svelte-package",
17
+ "check": "svelte-check --tsconfig ./tsconfig.json",
18
+ "test": "vitest run --config vitest.unit.ts && vitest run --config vitest.browser.ts",
19
+ "test:unit": "vitest run --config vitest.unit.ts",
20
+ "test:browser": "vitest run --config vitest.browser.ts",
21
+ "prepublishOnly": "pnpm build"
22
+ },
23
+ "svelte": "./dist/index.js",
24
+ "types": "./dist/index.d.ts",
25
+ "exports": {
26
+ ".": {
27
+ "types": "./dist/index.d.ts",
28
+ "svelte": "./dist/index.js",
29
+ "default": "./dist/index.js"
30
+ },
31
+ "./sqlite": {
32
+ "types": "./dist/db/sqlite.d.ts",
33
+ "default": "./dist/db/sqlite.js"
34
+ },
35
+ "./postgres": {
36
+ "types": "./dist/db/postgres.d.ts",
37
+ "default": "./dist/db/postgres.js"
38
+ },
39
+ "./d1": {
40
+ "types": "./dist/db/d1.d.ts",
41
+ "default": "./dist/db/d1.js"
42
+ },
43
+ "./components": {
44
+ "types": "./dist/components/index.d.ts",
45
+ "svelte": "./dist/components/index.js",
46
+ "default": "./dist/components/index.js"
47
+ }
48
+ },
49
+ "files": [
50
+ "dist",
51
+ "!dist/**/*.test.*"
52
+ ],
53
+ "peerDependencies": {
54
+ "@simplewebauthn/browser": "^13.0.0",
55
+ "@sveltejs/kit": "^2.0.0",
56
+ "svelte": "^5.0.0"
57
+ },
58
+ "peerDependenciesMeta": {
59
+ "svelte": {
60
+ "optional": true
61
+ },
62
+ "@simplewebauthn/browser": {
63
+ "optional": true
64
+ }
65
+ },
66
+ "dependencies": {
67
+ "@oslojs/crypto": "^1.0.1",
68
+ "@oslojs/encoding": "^1.1.0",
69
+ "@simplewebauthn/server": "^13.2.2"
70
+ },
71
+ "pnpm": {
72
+ "onlyBuiltDependencies": [
73
+ "better-sqlite3"
74
+ ]
75
+ },
76
+ "devDependencies": {
77
+ "@simplewebauthn/browser": "^13.2.2",
78
+ "@sveltejs/kit": "^2.31.1",
79
+ "@sveltejs/package": "^2.3.7",
80
+ "@sveltejs/vite-plugin-svelte": "^6.2.4",
81
+ "@testing-library/jest-dom": "^6.9.1",
82
+ "@testing-library/svelte": "^5.3.1",
83
+ "@testing-library/user-event": "^14.6.1",
84
+ "@types/better-sqlite3": "^7.6.13",
85
+ "better-sqlite3": "^12.6.2",
86
+ "happy-dom": "^20.6.1",
87
+ "svelte": "^5.38.1",
88
+ "svelte-check": "^4.3.1",
89
+ "typescript": "^5.9.2",
90
+ "vitest": "^3.2.1"
91
+ }
92
92
  }