@dudousxd/adonis-authkit-server 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/index.d.ts CHANGED
@@ -26,7 +26,7 @@ export { inertiaRenderer } from './src/host/renderers/inertia_renderer.js';
26
26
  export { edgeRenderer } from './src/host/renderers/edge_renderer.js';
27
27
  export { brandFor, isFirstParty } from './src/host/branding.js';
28
28
  export type { BrandingConfig, ClientBrand } from './src/host/branding.js';
29
- export { resolveMessages, translate, DEFAULT_MESSAGES, DEFAULT_LOCALE } from './src/host/i18n.js';
29
+ export { resolveMessages, translate, DEFAULT_MESSAGES, PT_BR_MESSAGES, BUILTIN_MESSAGES, DEFAULT_LOCALE, } from './src/host/i18n.js';
30
30
  export type { I18nConfig, AuthMessages } from './src/host/i18n.js';
31
31
  export type { AuthHostRenderer, AuthSocialConfig } from './src/define_config.js';
32
32
  export { registerAuthHost } from './src/host/register_auth_host.js';
package/build/index.js CHANGED
@@ -17,7 +17,7 @@ export { withAuditLog } from './src/mixins/with_audit_log.js';
17
17
  export { inertiaRenderer } from './src/host/renderers/inertia_renderer.js';
18
18
  export { edgeRenderer } from './src/host/renderers/edge_renderer.js';
19
19
  export { brandFor, isFirstParty } from './src/host/branding.js';
20
- export { resolveMessages, translate, DEFAULT_MESSAGES, DEFAULT_LOCALE } from './src/host/i18n.js';
20
+ export { resolveMessages, translate, DEFAULT_MESSAGES, PT_BR_MESSAGES, BUILTIN_MESSAGES, DEFAULT_LOCALE, } from './src/host/i18n.js';
21
21
  export { registerAuthHost } from './src/host/register_auth_host.js';
22
22
  export { resolveRateLimit, resolveNotifications } from './src/define_config.js';
23
23
  export { createAuthThrottles } from './src/host/rate_limit.js';
@@ -39,7 +39,9 @@ export default class AccountMfaController {
39
39
  const userId = ctx.session.get(ACCOUNT_SESSION_KEY);
40
40
  const generated = await cfg.accountStore.generatePasskeyRegistrationOptions?.(userId);
41
41
  if (!generated) {
42
- return ctx.response.notFound({ message: 'Passkeys indisponíveis' });
42
+ return ctx.response.notFound({
43
+ message: translate(cfg.messages, 'errors.passkeys_unavailable'),
44
+ });
43
45
  }
44
46
  ctx.session.put(PASSKEY_REG_CHALLENGE_KEY, generated.challenge);
45
47
  return generated.options;
@@ -54,7 +56,9 @@ export default class AccountMfaController {
54
56
  const userId = ctx.session.get(ACCOUNT_SESSION_KEY);
55
57
  const challenge = ctx.session.get(PASSKEY_REG_CHALLENGE_KEY);
56
58
  if (!challenge) {
57
- return ctx.response.badRequest({ message: 'Desafio expirado' });
59
+ return ctx.response.badRequest({
60
+ message: translate(cfg.messages, 'errors.challenge_expired'),
61
+ });
58
62
  }
59
63
  const body = ctx.request.input('response', ctx.request.body());
60
64
  const ok = (await cfg.accountStore.verifyPasskeyRegistration?.(userId, body, challenge)) ?? false;
@@ -240,11 +240,15 @@ export default class AuthInteractionController {
240
240
  const cfg = service.config;
241
241
  const accountId = ctx.session.get(MFA_PENDING_KEY);
242
242
  if (!accountId) {
243
- return ctx.response.badRequest({ message: 'Sessão expirada' });
243
+ return ctx.response.badRequest({
244
+ message: translate(cfg.messages, 'errors.session_expired'),
245
+ });
244
246
  }
245
247
  const generated = await cfg.accountStore.generatePasskeyAuthenticationOptions?.(accountId);
246
248
  if (!generated) {
247
- return ctx.response.notFound({ message: 'Nenhuma passkey registrada' });
249
+ return ctx.response.notFound({
250
+ message: translate(cfg.messages, 'errors.no_passkey_registered'),
251
+ });
248
252
  }
249
253
  ctx.session.put(PASSKEY_AUTH_CHALLENGE_KEY, generated.challenge);
250
254
  return generated.options;
@@ -7,6 +7,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
7
7
  return path;
8
8
  };
9
9
  import { renderTransactionalEmail } from './email_templates.js';
10
+ import { resolveMessages, translate, } from './i18n.js';
10
11
  let mailServicePromise;
11
12
  /**
12
13
  * Importa o service de mail do HOST de forma preguiçosa e fail-safe.
@@ -78,6 +79,22 @@ function resolveBrand(ctx) {
78
79
  }
79
80
  return { appName: 'AuthKit' };
80
81
  }
82
+ /**
83
+ * Resolve o catálogo de mensagens i18n a partir do `config/authkit.ts` (i18n).
84
+ * Cai no default (`en`) se a config não for resolvível. Usado para localizar
85
+ * os e-mails transacionais (assunto/cabeçalho/corpo).
86
+ */
87
+ function resolveMailMessages(ctx) {
88
+ try {
89
+ const resolver = ctx.containerResolver;
90
+ const i18n = resolver.app?.config?.get?.('authkit')?.i18n;
91
+ return { messages: resolveMessages(i18n), locale: i18n?.locale ?? 'en' };
92
+ }
93
+ catch {
94
+ // sem config authkit resolvível — usa o default `en`.
95
+ return { messages: resolveMessages(), locale: 'en' };
96
+ }
97
+ }
81
98
  async function sendEmail(ctx, to, content) {
82
99
  const mail = await loadMail();
83
100
  if (!mail)
@@ -97,14 +114,17 @@ async function sendEmail(ctx, to, content) {
97
114
  export async function sendPasswordResetEmail(ctx, data) {
98
115
  try {
99
116
  const brand = resolveBrand(ctx);
117
+ const { messages: t, locale } = resolveMailMessages(ctx);
100
118
  const content = renderTransactionalEmail({
101
119
  brand,
102
- subject: 'Redefinição de senha',
103
- heading: 'Redefinição de senha',
104
- intro: `Recebemos um pedido para redefinir a senha da sua conta em ${brand.appName}. Clique no botão abaixo para escolher uma nova senha. Se não foi você, ignore este e-mail.`,
105
- ctaLabel: 'Redefinir senha',
120
+ locale,
121
+ linkFallback: translate(t, 'mail.common.link_fallback'),
122
+ subject: translate(t, 'mail.reset.subject'),
123
+ heading: translate(t, 'mail.reset.heading'),
124
+ intro: translate(t, 'mail.reset.intro'),
125
+ ctaLabel: translate(t, 'mail.reset.cta'),
106
126
  ctaUrl: data.resetUrl,
107
- footnote: 'Por segurança, este link expira em breve e só pode ser usado uma vez.',
127
+ footnote: translate(t, 'mail.reset.fallback'),
108
128
  });
109
129
  const sent = await sendEmail(ctx, data.email, content);
110
130
  if (!sent) {
@@ -122,14 +142,17 @@ export async function sendPasswordResetEmail(ctx, data) {
122
142
  export async function sendEmailChangeConfirmationEmail(ctx, data) {
123
143
  try {
124
144
  const brand = resolveBrand(ctx);
145
+ const { messages: t, locale } = resolveMailMessages(ctx);
125
146
  const content = renderTransactionalEmail({
126
147
  brand,
127
- subject: 'Confirme seu novo e-mail',
128
- heading: 'Confirme seu novo e-mail',
129
- intro: `Recebemos um pedido para alterar o e-mail da sua conta em ${brand.appName} para este endereço. Clique no botão abaixo para confirmar a alteração. Se não foi você, ignore este e-mail — nada será alterado.`,
130
- ctaLabel: 'Confirmar novo e-mail',
148
+ locale,
149
+ linkFallback: translate(t, 'mail.common.link_fallback'),
150
+ subject: translate(t, 'mail.email_change.subject'),
151
+ heading: translate(t, 'mail.email_change.heading'),
152
+ intro: translate(t, 'mail.email_change.intro'),
153
+ ctaLabel: translate(t, 'mail.email_change.cta'),
131
154
  ctaUrl: data.confirmUrl,
132
- footnote: 'Por segurança, este link expira em breve e só pode ser usado uma vez.',
155
+ footnote: translate(t, 'mail.email_change.fallback'),
133
156
  });
134
157
  const sent = await sendEmail(ctx, data.email, content);
135
158
  if (!sent) {
@@ -147,15 +170,23 @@ export async function sendEmailChangeConfirmationEmail(ctx, data) {
147
170
  export async function sendNewLoginEmail(ctx, data) {
148
171
  try {
149
172
  const brand = resolveBrand(ctx);
173
+ const { messages: t, locale } = resolveMailMessages(ctx);
150
174
  const origin = `${ctx.request.protocol()}://${ctx.request.host()}`;
175
+ const intro = [
176
+ translate(t, 'mail.new_login.intro'),
177
+ translate(t, 'mail.new_login.when', { date: data.when }),
178
+ translate(t, 'mail.new_login.ip', { ip: data.ip }),
179
+ ].join(' ');
151
180
  const content = renderTransactionalEmail({
152
181
  brand,
153
- subject: 'Novo acesso à sua conta',
154
- heading: 'Novo acesso à sua conta',
155
- intro: `Detectamos um novo acesso à sua conta em ${brand.appName} a partir do IP ${data.ip} em ${data.when}. Se foi você, nenhuma ação é necessária. Se não reconhece este acesso, altere sua senha imediatamente.`,
156
- ctaLabel: 'Acessar minha conta',
182
+ locale,
183
+ linkFallback: translate(t, 'mail.common.link_fallback'),
184
+ subject: translate(t, 'mail.new_login.subject'),
185
+ heading: translate(t, 'mail.new_login.heading'),
186
+ intro,
187
+ ctaLabel: translate(t, 'account.security.title'),
157
188
  ctaUrl: `${origin}/account/security`,
158
- footnote: 'Você está recebendo este alerta porque um login foi feito de um endereço de IP não visto antes.',
189
+ footnote: translate(t, 'mail.new_login.fallback'),
159
190
  });
160
191
  const sent = await sendEmail(ctx, data.email, content);
161
192
  if (!sent) {
@@ -173,12 +204,15 @@ export async function sendNewLoginEmail(ctx, data) {
173
204
  export async function sendEmailVerificationEmail(ctx, data) {
174
205
  try {
175
206
  const brand = resolveBrand(ctx);
207
+ const { messages: t, locale } = resolveMailMessages(ctx);
176
208
  const content = renderTransactionalEmail({
177
209
  brand,
178
- subject: 'Verifique seu e-mail',
179
- heading: 'Confirme seu e-mail',
180
- intro: `Bem-vindo(a) ao ${brand.appName}! Confirme seu endereço de e-mail clicando no botão abaixo para ativar sua conta.`,
181
- ctaLabel: 'Verificar e-mail',
210
+ locale,
211
+ linkFallback: translate(t, 'mail.common.link_fallback'),
212
+ subject: translate(t, 'mail.verify.subject'),
213
+ heading: translate(t, 'mail.verify.heading'),
214
+ intro: translate(t, 'mail.verify.intro'),
215
+ ctaLabel: translate(t, 'mail.verify.cta'),
182
216
  ctaUrl: data.verifyUrl,
183
217
  });
184
218
  const sent = await sendEmail(ctx, data.email, content);
@@ -30,6 +30,10 @@ interface EmailTemplateInput {
30
30
  ctaUrl: string;
31
31
  /** Linha auxiliar abaixo do botão (ex.: validade do link). */
32
32
  footnote?: string;
33
+ /** Texto que precede o link de fallback (i18n). Default em inglês. */
34
+ linkFallback?: string;
35
+ /** Locale do documento HTML (atributo `lang`). Default: 'en'. */
36
+ locale?: string;
33
37
  }
34
38
  export declare function renderTransactionalEmail(input: EmailTemplateInput): EmailContent;
35
39
  export {};
@@ -21,8 +21,11 @@ export function renderTransactionalEmail(input) {
21
21
  const accent = input.brand.accent || FALLBACK_ACCENT;
22
22
  const company = input.brand.company || appName;
23
23
  const year = '©'; // ano resolvido fora (sem Date.* aqui); rodapé usa só o nome.
24
+ const lang = input.locale || 'en';
25
+ const linkFallback = input.linkFallback ||
26
+ 'If the button does not work, copy and paste this link into your browser:';
24
27
  const html = `<!doctype html>
25
- <html lang="pt-BR">
28
+ <html lang="${esc(lang)}">
26
29
  <head>
27
30
  <meta charset="utf-8">
28
31
  <meta name="viewport" content="width=device-width, initial-scale=1">
@@ -42,7 +45,7 @@ export function renderTransactionalEmail(input) {
42
45
  <a href="${esc(input.ctaUrl)}" style="display:inline-block;padding:12px 24px;font-size:15px;font-weight:600;color:#ffffff;text-decoration:none;border-radius:8px;">${esc(input.ctaLabel)}</a>
43
46
  </td></tr></table>
44
47
  ${input.footnote ? `<p style="margin:24px 0 0;font-size:13px;line-height:1.5;color:#6b7280;">${esc(input.footnote)}</p>` : ''}
45
- <p style="margin:24px 0 0;font-size:13px;line-height:1.5;color:#6b7280;">Se o botão não funcionar, copie e cole este link no navegador:<br><a href="${esc(input.ctaUrl)}" style="color:${esc(accent)};word-break:break-all;">${esc(input.ctaUrl)}</a></p>
48
+ <p style="margin:24px 0 0;font-size:13px;line-height:1.5;color:#6b7280;">${esc(linkFallback)}<br><a href="${esc(input.ctaUrl)}" style="color:${esc(accent)};word-break:break-all;">${esc(input.ctaUrl)}</a></p>
46
49
  </td></tr>
47
50
  <tr><td style="padding:24px 28px 28px;border-top:1px solid #f3f4f6;">
48
51
  <p style="margin:0;font-size:12px;line-height:1.5;color:#9ca3af;">${esc(company)} ${year}</p>
@@ -3,26 +3,29 @@
3
3
  *
4
4
  * Todas as strings visíveis ao usuário das views Edge (e as mensagens de
5
5
  * flash/erro produzidas pelos controllers) vivem num catálogo achatado de
6
- * chaves pontilhadas. O default embutido é pt-BR — os apps continuam
7
- * funcionando SEM nenhuma configuração. O host pode sobrescrever chaves
8
- * pontuais ou fornecer locales inteiros (ex.: `en`) via `I18nConfig`.
6
+ * chaves pontilhadas. O default embutido é inglês (`en`) — os apps continuam
7
+ * funcionando SEM nenhuma configuração. O pt-BR é um locale embutido: basta
8
+ * `i18n: { locale: 'pt-BR' }`. O host também pode sobrescrever chaves pontuais
9
+ * ou fornecer locales inteiros (ex.: `fr`) via `I18nConfig`.
9
10
  */
10
11
  /** Catálogo achatado de chaves de mensagem → strings. */
11
12
  export type AuthMessages = Record<string, string>;
12
13
  export interface I18nConfig {
13
- /** Locale ativo. Default: 'pt-BR'. */
14
+ /** Locale ativo. Default: 'en'. Locale embutido extra: 'pt-BR'. */
14
15
  locale?: string;
15
16
  /**
16
17
  * Locales adicionais e/ou overrides pontuais. As chaves do locale ativo são
17
- * mescladas SOBRE o default pt-BR então o host pode trocar só algumas
18
- * chaves ou trazer um locale novo por completo.
18
+ * mescladas SOBRE o catálogo embutido do locale (ou sobre o default `en`
19
+ * quando o locale não é embutido) então o host pode trocar só algumas
20
+ * chaves, complementar um locale embutido, ou trazer um locale novo por
21
+ * completo.
19
22
  */
20
23
  messages?: Record<string, Partial<AuthMessages>>;
21
24
  }
22
25
  /** Locale default do host-kit. */
23
- export declare const DEFAULT_LOCALE = "pt-BR";
26
+ export declare const DEFAULT_LOCALE = "en";
24
27
  /**
25
- * Catálogo default (pt-BR) — cobre TODAS as strings visíveis ao usuário das
28
+ * Catálogo default (inglês) — cobre TODAS as strings visíveis ao usuário das
26
29
  * views e as mensagens de flash/erro dos controllers. Chaves agrupadas por tela.
27
30
  */
28
31
  export declare const DEFAULT_MESSAGES: {
@@ -244,11 +247,302 @@ export declare const DEFAULT_MESSAGES: {
244
247
  'errors.signup_failed': string;
245
248
  'errors.invalid_or_expired_token': string;
246
249
  'errors.account_locked': string;
250
+ 'errors.session_expired': string;
251
+ 'errors.challenge_expired': string;
252
+ 'errors.passkeys_unavailable': string;
253
+ 'errors.no_passkey_registered': string;
254
+ 'mail.common.link_fallback': string;
255
+ 'mail.reset.subject': string;
256
+ 'mail.reset.heading': string;
257
+ 'mail.reset.intro': string;
258
+ 'mail.reset.cta': string;
259
+ 'mail.reset.fallback': string;
260
+ 'mail.reset.expires': string;
261
+ 'mail.verify.subject': string;
262
+ 'mail.verify.heading': string;
263
+ 'mail.verify.intro': string;
264
+ 'mail.verify.cta': string;
265
+ 'mail.verify.fallback': string;
266
+ 'mail.verify.expires': string;
267
+ 'mail.new_login.subject': string;
268
+ 'mail.new_login.heading': string;
269
+ 'mail.new_login.intro': string;
270
+ 'mail.new_login.when': string;
271
+ 'mail.new_login.ip': string;
272
+ 'mail.new_login.device': string;
273
+ 'mail.new_login.fallback': string;
274
+ 'mail.email_change.subject': string;
275
+ 'mail.email_change.heading': string;
276
+ 'mail.email_change.intro': string;
277
+ 'mail.email_change.cta': string;
278
+ 'mail.email_change.fallback': string;
279
+ 'mail.email_change.expires': string;
247
280
  };
248
281
  /**
249
- * Resolve o catálogo ativo: mescla os overrides do locale selecionado SOBRE o
250
- * default pt-BR. Sem config, retorna os defaults intactos. Chaves omitidas pelo
251
- * locale escolhido caem no default pt-BR (fallback de cobertura).
282
+ * Catálogo embutido pt-BR. Espelha TODAS as chaves do default `en`. Ativado
283
+ * com `i18n: { locale: 'pt-BR' }` sem nenhuma config extra de mensagens.
284
+ */
285
+ export declare const PT_BR_MESSAGES: {
286
+ 'common.app_fallback': string;
287
+ 'common.brand_eyebrow': string;
288
+ 'login.page_title': string;
289
+ 'login.title': string;
290
+ 'login.identifier_intro': string;
291
+ 'login.email_label': string;
292
+ 'login.identifier_submit': string;
293
+ 'login.create_account': string;
294
+ 'login.forgot_password': string;
295
+ 'login.divider_or': string;
296
+ 'login.google': string;
297
+ 'login.greeting': string;
298
+ 'login.switch_account': string;
299
+ 'login.password_label': string;
300
+ 'login.submit': string;
301
+ 'signup.page_title': string;
302
+ 'signup.title': string;
303
+ 'signup.intro': string;
304
+ 'signup.name_label': string;
305
+ 'signup.email_label': string;
306
+ 'signup.password_label': string;
307
+ 'signup.submit': string;
308
+ 'signup.have_account': string;
309
+ 'forgot.page_title': string;
310
+ 'forgot.sent_title': string;
311
+ 'forgot.sent_body': string;
312
+ 'forgot.title': string;
313
+ 'forgot.intro': string;
314
+ 'forgot.email_label': string;
315
+ 'forgot.submit': string;
316
+ 'reset.page_title': string;
317
+ 'reset.done_title': string;
318
+ 'reset.done_body': string;
319
+ 'reset.title': string;
320
+ 'reset.intro': string;
321
+ 'reset.password_label': string;
322
+ 'reset.submit': string;
323
+ 'verify_email.page_title': string;
324
+ 'verify_email.verified_title': string;
325
+ 'verify_email.verified_body': string;
326
+ 'verify_email.invalid_title': string;
327
+ 'verify_email.invalid_body': string;
328
+ 'mfa_challenge.page_title': string;
329
+ 'mfa_challenge.title': string;
330
+ 'mfa_challenge.intro': string;
331
+ 'mfa_challenge.code_label': string;
332
+ 'mfa_challenge.submit': string;
333
+ 'mfa_challenge.recovery_summary': string;
334
+ 'mfa_challenge.recovery_submit': string;
335
+ 'mfa_challenge.passkey_button': string;
336
+ 'mfa_challenge.passkey_error': string;
337
+ 'consent.page_title': string;
338
+ 'consent.title': string;
339
+ 'consent.body': string;
340
+ 'consent.submit': string;
341
+ 'account.login.page_title': string;
342
+ 'account.login.title': string;
343
+ 'account.login.intro': string;
344
+ 'account.login.email_label': string;
345
+ 'account.login.password_label': string;
346
+ 'account.login.submit': string;
347
+ 'account.tokens.page_title': string;
348
+ 'account.tokens.title': string;
349
+ 'account.tokens.logout': string;
350
+ 'account.tokens.security': string;
351
+ 'account.tokens.created_notice': string;
352
+ 'account.tokens.name_placeholder': string;
353
+ 'account.tokens.create': string;
354
+ 'account.tokens.empty': string;
355
+ 'account.tokens.created_at': string;
356
+ 'account.tokens.last_used': string;
357
+ 'account.tokens.never_used': string;
358
+ 'account.tokens.scopes': string;
359
+ 'account.tokens.audience': string;
360
+ 'account.tokens.revoke': string;
361
+ 'account.security.page_title': string;
362
+ 'account.security.title': string;
363
+ 'account.security.logout': string;
364
+ 'account.security.current_email': string;
365
+ 'account.security.not_supported': string;
366
+ 'account.security.password_section': string;
367
+ 'account.security.current_password_label': string;
368
+ 'account.security.new_password_label': string;
369
+ 'account.security.change_password_submit': string;
370
+ 'account.security.password_changed': string;
371
+ 'account.security.email_section': string;
372
+ 'account.security.email_intro': string;
373
+ 'account.security.new_email_label': string;
374
+ 'account.security.email_password_label': string;
375
+ 'account.security.change_email_submit': string;
376
+ 'account.security.email_change_requested': string;
377
+ 'account.security.email_changed': string;
378
+ 'account.email_confirmed.page_title': string;
379
+ 'account.email_confirmed.ok_title': string;
380
+ 'account.email_confirmed.ok_body': string;
381
+ 'account.email_confirmed.invalid_title': string;
382
+ 'account.email_confirmed.invalid_body': string;
383
+ 'account.mfa.page_title': string;
384
+ 'account.mfa.title': string;
385
+ 'account.mfa.logout': string;
386
+ 'account.mfa.recovery_codes_notice': string;
387
+ 'account.mfa.enroll_intro': string;
388
+ 'account.mfa.qr_alt': string;
389
+ 'account.mfa.manual_intro': string;
390
+ 'account.mfa.confirm_code_label': string;
391
+ 'account.mfa.activate': string;
392
+ 'account.mfa.enabled_html': string;
393
+ 'account.mfa.disable': string;
394
+ 'account.mfa.disabled_intro': string;
395
+ 'account.mfa.enable': string;
396
+ 'mfa.passkey.section_title': string;
397
+ 'mfa.passkey.section_intro': string;
398
+ 'mfa.passkey.add': string;
399
+ 'mfa.passkey.remove': string;
400
+ 'mfa.passkey.empty': string;
401
+ 'mfa.passkey.unnamed': string;
402
+ 'mfa.passkey.created_at': string;
403
+ 'mfa.passkey.register_error': string;
404
+ 'mfa.passkey.unsupported': string;
405
+ 'admin.nav.dashboard': string;
406
+ 'admin.nav.users': string;
407
+ 'admin.nav.clients': string;
408
+ 'admin.nav.audit': string;
409
+ 'admin.nav.logout': string;
410
+ 'admin.dashboard.page_title': string;
411
+ 'admin.dashboard.title': string;
412
+ 'admin.dashboard.users_count': string;
413
+ 'admin.dashboard.clients_count': string;
414
+ 'admin.dashboard.audit_count': string;
415
+ 'admin.dashboard.recent_title': string;
416
+ 'admin.users.page_title': string;
417
+ 'admin.users.title': string;
418
+ 'admin.users.search_placeholder': string;
419
+ 'admin.users.search': string;
420
+ 'admin.users.empty': string;
421
+ 'admin.users.roles_placeholder': string;
422
+ 'admin.users.save_roles': string;
423
+ 'admin.users.sessions': string;
424
+ 'admin.sessions.page_title': string;
425
+ 'admin.sessions.title': string;
426
+ 'admin.sessions.account': string;
427
+ 'admin.sessions.back': string;
428
+ 'admin.sessions.not_supported': string;
429
+ 'admin.sessions.revoked_notice': string;
430
+ 'admin.sessions.sessions_section': string;
431
+ 'admin.sessions.sessions_empty': string;
432
+ 'admin.sessions.session_login_ts': string;
433
+ 'admin.sessions.session_amr': string;
434
+ 'admin.sessions.grants_section': string;
435
+ 'admin.sessions.grants_empty': string;
436
+ 'admin.sessions.grant_client': string;
437
+ 'admin.sessions.grant_tokens': string;
438
+ 'admin.sessions.revoke_all': string;
439
+ 'admin.sessions.revoke_confirm': string;
440
+ 'admin.clients.page_title': string;
441
+ 'admin.clients.title': string;
442
+ 'admin.clients.empty': string;
443
+ 'admin.clients.confidential': string;
444
+ 'admin.clients.public': string;
445
+ 'admin.clients.grants': string;
446
+ 'admin.clients.redirect_uris': string;
447
+ 'admin.clients.dynamic_notice': string;
448
+ 'admin.clients.static_section': string;
449
+ 'admin.clients.dynamic_section': string;
450
+ 'admin.clients.dynamic_empty': string;
451
+ 'admin.clients.dynamic_not_supported': string;
452
+ 'admin.clients.new': string;
453
+ 'admin.clients.new_title': string;
454
+ 'admin.clients.edit_title': string;
455
+ 'admin.clients.edit': string;
456
+ 'admin.clients.delete': string;
457
+ 'admin.clients.delete_confirm': string;
458
+ 'admin.clients.regenerate_secret': string;
459
+ 'admin.clients.regenerate_confirm': string;
460
+ 'admin.clients.back': string;
461
+ 'admin.clients.cancel': string;
462
+ 'admin.clients.save': string;
463
+ 'admin.clients.create': string;
464
+ 'admin.clients.secret_once_title': string;
465
+ 'admin.clients.secret_once_notice': string;
466
+ 'admin.clients.field_client_id': string;
467
+ 'admin.clients.field_client_id_placeholder': string;
468
+ 'admin.clients.field_client_id_help': string;
469
+ 'admin.clients.field_redirect_uris': string;
470
+ 'admin.clients.field_redirect_uris_help': string;
471
+ 'admin.clients.field_post_logout_uris': string;
472
+ 'admin.clients.field_post_logout_uris_help': string;
473
+ 'admin.clients.field_grant_types': string;
474
+ 'admin.clients.field_auth_method': string;
475
+ 'admin.audit.page_title': string;
476
+ 'admin.audit.title': string;
477
+ 'admin.audit.type_placeholder': string;
478
+ 'admin.audit.subject_placeholder': string;
479
+ 'admin.audit.filter': string;
480
+ 'admin.audit.empty': string;
481
+ 'admin.audit.not_supported': string;
482
+ 'admin.pagination.page': string;
483
+ 'admin.pagination.prev': string;
484
+ 'admin.pagination.next': string;
485
+ 'device.input.title': string;
486
+ 'device.input.intro': string;
487
+ 'device.input.submit': string;
488
+ 'device.input.error_invalid': string;
489
+ 'device.input.error_aborted': string;
490
+ 'device.input.error_generic': string;
491
+ 'device.confirm.title': string;
492
+ 'device.confirm.body': string;
493
+ 'device.confirm.submit': string;
494
+ 'device.confirm.abort': string;
495
+ 'device.success.title': string;
496
+ 'device.success.body': string;
497
+ 'mfa_challenge.required_no_enrollment': string;
498
+ 'errors.invalid_credentials': string;
499
+ 'errors.invalid_code': string;
500
+ 'errors.email_taken': string;
501
+ 'errors.signup_failed': string;
502
+ 'errors.invalid_or_expired_token': string;
503
+ 'errors.account_locked': string;
504
+ 'errors.session_expired': string;
505
+ 'errors.challenge_expired': string;
506
+ 'errors.passkeys_unavailable': string;
507
+ 'errors.no_passkey_registered': string;
508
+ 'mail.common.link_fallback': string;
509
+ 'mail.reset.subject': string;
510
+ 'mail.reset.heading': string;
511
+ 'mail.reset.intro': string;
512
+ 'mail.reset.cta': string;
513
+ 'mail.reset.fallback': string;
514
+ 'mail.reset.expires': string;
515
+ 'mail.verify.subject': string;
516
+ 'mail.verify.heading': string;
517
+ 'mail.verify.intro': string;
518
+ 'mail.verify.cta': string;
519
+ 'mail.verify.fallback': string;
520
+ 'mail.verify.expires': string;
521
+ 'mail.new_login.subject': string;
522
+ 'mail.new_login.heading': string;
523
+ 'mail.new_login.intro': string;
524
+ 'mail.new_login.when': string;
525
+ 'mail.new_login.ip': string;
526
+ 'mail.new_login.device': string;
527
+ 'mail.new_login.fallback': string;
528
+ 'mail.email_change.subject': string;
529
+ 'mail.email_change.heading': string;
530
+ 'mail.email_change.intro': string;
531
+ 'mail.email_change.cta': string;
532
+ 'mail.email_change.fallback': string;
533
+ 'mail.email_change.expires': string;
534
+ };
535
+ /**
536
+ * Locales embutidos no host-kit. O `en` é o default; o `pt-BR` está disponível
537
+ * com `i18n: { locale: 'pt-BR' }` sem nenhuma config de mensagens extra. Os
538
+ * overrides/locales do host (via `I18nConfig.messages`) são mesclados por cima.
539
+ */
540
+ export declare const BUILTIN_MESSAGES: Record<string, AuthMessages>;
541
+ /**
542
+ * Resolve o catálogo ativo. Começa do catálogo embutido do locale selecionado
543
+ * (ou do default `en` quando o locale não é embutido), depois mescla os
544
+ * overrides do host por cima. Sem config, retorna o default `en` intacto.
545
+ * Chaves omitidas caem no default `en` (fallback de cobertura).
252
546
  */
253
547
  export declare function resolveMessages(i18n?: I18nConfig): AuthMessages;
254
548
  /**
@@ -3,17 +3,299 @@
3
3
  *
4
4
  * Todas as strings visíveis ao usuário das views Edge (e as mensagens de
5
5
  * flash/erro produzidas pelos controllers) vivem num catálogo achatado de
6
- * chaves pontilhadas. O default embutido é pt-BR — os apps continuam
7
- * funcionando SEM nenhuma configuração. O host pode sobrescrever chaves
8
- * pontuais ou fornecer locales inteiros (ex.: `en`) via `I18nConfig`.
6
+ * chaves pontilhadas. O default embutido é inglês (`en`) — os apps continuam
7
+ * funcionando SEM nenhuma configuração. O pt-BR é um locale embutido: basta
8
+ * `i18n: { locale: 'pt-BR' }`. O host também pode sobrescrever chaves pontuais
9
+ * ou fornecer locales inteiros (ex.: `fr`) via `I18nConfig`.
9
10
  */
10
11
  /** Locale default do host-kit. */
11
- export const DEFAULT_LOCALE = 'pt-BR';
12
+ export const DEFAULT_LOCALE = 'en';
12
13
  /**
13
- * Catálogo default (pt-BR) — cobre TODAS as strings visíveis ao usuário das
14
+ * Catálogo default (inglês) — cobre TODAS as strings visíveis ao usuário das
14
15
  * views e as mensagens de flash/erro dos controllers. Chaves agrupadas por tela.
15
16
  */
16
17
  export const DEFAULT_MESSAGES = {
18
+ // Comum / fallback de marca.
19
+ 'common.app_fallback': 'Auth',
20
+ 'common.brand_eyebrow': 'Auth',
21
+ // Tela de login (interaction OIDC: identifier + password).
22
+ 'login.page_title': 'Login',
23
+ 'login.title': 'Login',
24
+ 'login.identifier_intro': 'Enter your email to continue.',
25
+ 'login.email_label': 'Email',
26
+ 'login.identifier_submit': 'Continue',
27
+ 'login.create_account': 'Create account',
28
+ 'login.forgot_password': 'Forgot password',
29
+ 'login.divider_or': 'or',
30
+ 'login.google': 'Sign in with Google',
31
+ 'login.greeting': 'Hi, {name}',
32
+ 'login.switch_account': 'Switch account',
33
+ 'login.password_label': 'Password',
34
+ 'login.submit': 'Log in',
35
+ // Tela de cadastro (signup).
36
+ 'signup.page_title': 'Create account',
37
+ 'signup.title': 'Create account',
38
+ 'signup.intro': 'Fill in your details to get started.',
39
+ 'signup.name_label': 'Name',
40
+ 'signup.email_label': 'Email',
41
+ 'signup.password_label': 'Password',
42
+ 'signup.submit': 'Create account',
43
+ 'signup.have_account': 'I already have an account',
44
+ // Recuperação de senha (forgot).
45
+ 'forgot.page_title': 'Reset password',
46
+ 'forgot.sent_title': 'Email sent',
47
+ 'forgot.sent_body': 'If the email exists, we will send reset instructions.',
48
+ 'forgot.title': 'Reset password',
49
+ 'forgot.intro': 'We will send you a link to reset your password.',
50
+ 'forgot.email_label': 'Email',
51
+ 'forgot.submit': 'Send link',
52
+ // Redefinição de senha (reset).
53
+ 'reset.page_title': 'Reset password',
54
+ 'reset.done_title': 'Password reset',
55
+ 'reset.done_body': 'You can now log in with your new password.',
56
+ 'reset.title': 'New password',
57
+ 'reset.intro': 'Choose a new password for your account.',
58
+ 'reset.password_label': 'Password',
59
+ 'reset.submit': 'Reset',
60
+ // Verificação de e-mail (verify-email).
61
+ 'verify_email.page_title': 'Verify email',
62
+ 'verify_email.verified_title': 'Email verified',
63
+ 'verify_email.verified_body': 'Your email was confirmed successfully.',
64
+ 'verify_email.invalid_title': 'Invalid link',
65
+ 'verify_email.invalid_body': 'The verification link is invalid or has already been used.',
66
+ // Desafio de MFA no fluxo de login (mfa-challenge).
67
+ 'mfa_challenge.page_title': 'Two-factor verification',
68
+ 'mfa_challenge.title': 'Two-factor verification',
69
+ 'mfa_challenge.intro': 'Open your authenticator app and enter the 6-digit code.',
70
+ 'mfa_challenge.code_label': 'Code',
71
+ 'mfa_challenge.submit': 'Verify',
72
+ 'mfa_challenge.recovery_summary': 'Use a recovery code',
73
+ 'mfa_challenge.recovery_submit': 'Log in with a recovery code',
74
+ 'mfa_challenge.passkey_button': 'Use passkey',
75
+ 'mfa_challenge.passkey_error': 'Could not authenticate with the passkey. Please try again.',
76
+ // Consent (autorização de cliente OIDC).
77
+ 'consent.page_title': 'Authorize',
78
+ 'consent.title': 'Authorize access',
79
+ // `{app}` é interpolado com o nome do app já envolto em <strong> (renderizado
80
+ // raw na view). O nome vem do branding (config-trusted).
81
+ 'consent.body': 'The app <strong>{app}</strong> wants to access your account.',
82
+ 'consent.submit': 'Authorize',
83
+ // Console de conta — login (account/login).
84
+ 'account.login.page_title': 'My account',
85
+ 'account.login.title': 'My account',
86
+ 'account.login.intro': 'Manage your access tokens.',
87
+ 'account.login.email_label': 'Email',
88
+ 'account.login.password_label': 'Password',
89
+ 'account.login.submit': 'Log in',
90
+ // Console de conta — tokens (account/tokens).
91
+ 'account.tokens.page_title': 'Access tokens',
92
+ 'account.tokens.title': 'Access tokens',
93
+ 'account.tokens.logout': 'Log out',
94
+ 'account.tokens.security': 'Security',
95
+ 'account.tokens.created_notice': 'Token created — copy it now, it will not be shown again:',
96
+ 'account.tokens.name_placeholder': 'Token name (e.g. CI deploy)',
97
+ 'account.tokens.create': 'Create',
98
+ 'account.tokens.empty': 'No tokens yet.',
99
+ 'account.tokens.created_at': 'Created on {date}',
100
+ 'account.tokens.last_used': '· last used {date}',
101
+ 'account.tokens.never_used': '· never used',
102
+ 'account.tokens.scopes': 'Scopes: {scopes}',
103
+ 'account.tokens.audience': 'Audience: {audience}',
104
+ 'account.tokens.revoke': 'Revoke',
105
+ // Console de conta — segurança (account/security): senha + e-mail.
106
+ 'account.security.page_title': 'Account security',
107
+ 'account.security.title': 'Account security',
108
+ 'account.security.logout': 'Log out',
109
+ 'account.security.current_email': 'Current email: {email}',
110
+ 'account.security.not_supported': 'Changing password and email is not available in this installation.',
111
+ 'account.security.password_section': 'Change password',
112
+ 'account.security.current_password_label': 'Current password',
113
+ 'account.security.new_password_label': 'New password',
114
+ 'account.security.change_password_submit': 'Change password',
115
+ 'account.security.password_changed': 'Password changed successfully.',
116
+ 'account.security.email_section': 'Change email',
117
+ 'account.security.email_intro': 'We will send a confirmation link to the new address. The change only takes effect after confirmation.',
118
+ 'account.security.new_email_label': 'New email',
119
+ 'account.security.email_password_label': 'Current password',
120
+ 'account.security.change_email_submit': 'Request email change',
121
+ 'account.security.email_change_requested': 'We sent a confirmation link to {email}. Click it to complete the change.',
122
+ 'account.security.email_changed': 'Email changed successfully.',
123
+ // Confirmação de troca de e-mail (account/email-confirmed).
124
+ 'account.email_confirmed.page_title': 'Email confirmation',
125
+ 'account.email_confirmed.ok_title': 'Email changed',
126
+ 'account.email_confirmed.ok_body': 'Your new email has been confirmed and is now active.',
127
+ 'account.email_confirmed.invalid_title': 'Invalid link',
128
+ 'account.email_confirmed.invalid_body': 'The confirmation link is invalid or has already been used.',
129
+ // Console de conta — MFA (account/mfa).
130
+ 'account.mfa.page_title': 'Two-factor verification',
131
+ 'account.mfa.title': 'Two-factor verification',
132
+ 'account.mfa.logout': 'Log out',
133
+ 'account.mfa.recovery_codes_notice': 'Save your recovery codes — they will not be shown again:',
134
+ 'account.mfa.enroll_intro': 'Scan the QR code with your authenticator app (Google Authenticator, 1Password, etc.).',
135
+ 'account.mfa.qr_alt': 'TOTP QR code',
136
+ 'account.mfa.manual_intro': 'Or enter it manually:',
137
+ 'account.mfa.confirm_code_label': 'Confirmation code',
138
+ 'account.mfa.activate': 'Enable two-factor verification',
139
+ 'account.mfa.enabled_html': 'Two-factor verification is <span class="font-semibold text-emerald-700">enabled</span> on this account.',
140
+ 'account.mfa.disable': 'Disable',
141
+ 'account.mfa.disabled_intro': 'Two-factor verification is disabled. Enable it to protect your account with an authenticator app.',
142
+ 'account.mfa.enable': 'Enable two-factor verification',
143
+ // Console de conta — passkeys (WebAuthn) na tela de MFA.
144
+ 'mfa.passkey.section_title': 'Passkeys',
145
+ 'mfa.passkey.section_intro': 'Use a passkey (biometrics, device PIN, or security key) as a second factor, without typing codes.',
146
+ 'mfa.passkey.add': 'Add passkey',
147
+ 'mfa.passkey.remove': 'Remove',
148
+ 'mfa.passkey.empty': 'No passkeys registered.',
149
+ 'mfa.passkey.unnamed': 'Passkey',
150
+ 'mfa.passkey.created_at': 'Created on {date}',
151
+ 'mfa.passkey.register_error': 'Could not register the passkey. Please try again.',
152
+ 'mfa.passkey.unsupported': 'Your browser does not support passkeys.',
153
+ // Console admin (B6) — navegação compartilhada.
154
+ 'admin.nav.dashboard': 'Dashboard',
155
+ 'admin.nav.users': 'Users',
156
+ 'admin.nav.clients': 'Clients',
157
+ 'admin.nav.audit': 'Audit',
158
+ 'admin.nav.logout': 'Log out',
159
+ // Console admin — dashboard.
160
+ 'admin.dashboard.page_title': 'Admin dashboard',
161
+ 'admin.dashboard.title': 'Admin dashboard',
162
+ 'admin.dashboard.users_count': 'Users',
163
+ 'admin.dashboard.clients_count': 'Clients',
164
+ 'admin.dashboard.audit_count': 'Audit events',
165
+ 'admin.dashboard.recent_title': 'Recent events',
166
+ // Console admin — usuários.
167
+ 'admin.users.page_title': 'Users',
168
+ 'admin.users.title': 'Users',
169
+ 'admin.users.search_placeholder': 'Search by email',
170
+ 'admin.users.search': 'Search',
171
+ 'admin.users.empty': 'No users found.',
172
+ 'admin.users.roles_placeholder': 'Roles (comma-separated)',
173
+ 'admin.users.save_roles': 'Save roles',
174
+ 'admin.users.sessions': 'Sessions',
175
+ // Console admin — sessões/grants ativos de uma conta.
176
+ 'admin.sessions.page_title': 'Active sessions',
177
+ 'admin.sessions.title': 'Active sessions',
178
+ 'admin.sessions.account': 'Account: {email}',
179
+ 'admin.sessions.back': 'Back to users',
180
+ 'admin.sessions.not_supported': 'The configured OIDC adapter does not support enumeration — session inspection is unavailable.',
181
+ 'admin.sessions.revoked_notice': 'Revoked: {sessions} session(s), {grants} grant(s), {accessTokens} access token(s), {refreshTokens} refresh token(s).',
182
+ 'admin.sessions.sessions_section': 'Sessions (IdP login)',
183
+ 'admin.sessions.sessions_empty': 'No active sessions.',
184
+ 'admin.sessions.session_login_ts': 'Login: {date}',
185
+ 'admin.sessions.session_amr': 'Methods: {amr}',
186
+ 'admin.sessions.grants_section': 'Grants (per-client authorizations)',
187
+ 'admin.sessions.grants_empty': 'No active grants.',
188
+ 'admin.sessions.grant_client': 'Client: {clientId}',
189
+ 'admin.sessions.grant_tokens': '{accessTokens} access · {refreshTokens} refresh',
190
+ 'admin.sessions.revoke_all': 'Revoke all sessions and grants',
191
+ 'admin.sessions.revoke_confirm': 'Revoke all sessions and grants for this account? The user will need to log in again and issued tokens will stop working.',
192
+ // Console admin — clients.
193
+ 'admin.clients.page_title': 'OAuth clients',
194
+ 'admin.clients.title': 'OAuth clients',
195
+ 'admin.clients.empty': 'No clients configured.',
196
+ 'admin.clients.confidential': 'Confidential',
197
+ 'admin.clients.public': 'Public',
198
+ 'admin.clients.grants': 'Grants: {grants}',
199
+ 'admin.clients.redirect_uris': 'Redirects: {uris}',
200
+ 'admin.clients.dynamic_notice': 'Dynamic client registration (RFC 7591) is on — clients registered via /reg are persisted in the adapter and appear in the dynamic section below.',
201
+ 'admin.clients.static_section': 'Static clients (config)',
202
+ 'admin.clients.dynamic_section': 'Dynamic clients (adapter)',
203
+ 'admin.clients.dynamic_empty': 'No dynamic clients persisted.',
204
+ 'admin.clients.dynamic_not_supported': 'The configured OIDC adapter does not support client enumeration — dynamic management is unavailable.',
205
+ 'admin.clients.new': 'New client',
206
+ 'admin.clients.new_title': 'New OIDC client',
207
+ 'admin.clients.edit_title': 'Edit OIDC client',
208
+ 'admin.clients.edit': 'Edit',
209
+ 'admin.clients.delete': 'Delete',
210
+ 'admin.clients.delete_confirm': 'Delete this client? This action cannot be undone.',
211
+ 'admin.clients.regenerate_secret': 'Regenerate secret',
212
+ 'admin.clients.regenerate_confirm': 'Regenerate the secret? The current secret will stop working immediately.',
213
+ 'admin.clients.back': 'Back',
214
+ 'admin.clients.cancel': 'Cancel',
215
+ 'admin.clients.save': 'Save',
216
+ 'admin.clients.create': 'Create client',
217
+ 'admin.clients.secret_once_title': 'Save the client_secret now',
218
+ 'admin.clients.secret_once_notice': 'This is the only time the secret is shown. Copy it now — it cannot be retrieved later.',
219
+ 'admin.clients.field_client_id': 'Client ID',
220
+ 'admin.clients.field_client_id_placeholder': 'leave blank to generate automatically',
221
+ 'admin.clients.field_client_id_help': 'Optional. If empty, a random identifier will be generated.',
222
+ 'admin.clients.field_redirect_uris': 'Redirect URIs',
223
+ 'admin.clients.field_redirect_uris_help': 'One URI per line.',
224
+ 'admin.clients.field_post_logout_uris': 'Post-logout redirect URIs',
225
+ 'admin.clients.field_post_logout_uris_help': 'One URI per line (optional).',
226
+ 'admin.clients.field_grant_types': 'Grant types',
227
+ 'admin.clients.field_auth_method': 'Token endpoint auth method',
228
+ // Console admin — auditoria.
229
+ 'admin.audit.page_title': 'Audit',
230
+ 'admin.audit.title': 'Audit log',
231
+ 'admin.audit.type_placeholder': 'Filter by type',
232
+ 'admin.audit.subject_placeholder': 'Filter by subject (accountId)',
233
+ 'admin.audit.filter': 'Filter',
234
+ 'admin.audit.empty': 'No events found.',
235
+ 'admin.audit.not_supported': 'The configured audit sink does not support querying.',
236
+ // Console admin — paginação compartilhada.
237
+ 'admin.pagination.page': 'Page {page} of {total}',
238
+ 'admin.pagination.prev': 'Previous',
239
+ 'admin.pagination.next': 'Next',
240
+ // Device Authorization Grant (RFC 8628) — telas servidas pelo oidc-provider.
241
+ 'device.input.title': 'Sign in to the device',
242
+ 'device.input.intro': 'Enter the code shown on your device.',
243
+ 'device.input.submit': 'Continue',
244
+ 'device.input.error_invalid': 'The code you entered is incorrect. Please try again.',
245
+ 'device.input.error_aborted': 'The login request was aborted.',
246
+ 'device.input.error_generic': 'An error occurred while processing your request.',
247
+ 'device.confirm.title': 'Confirm device',
248
+ 'device.confirm.body': 'The code below should be displayed on your device. Confirm only if you recognize it.',
249
+ 'device.confirm.submit': 'Continue',
250
+ 'device.confirm.abort': 'Cancel',
251
+ 'device.success.title': 'Login complete',
252
+ 'device.success.body': 'You are signed in. You can return to your device.',
253
+ // Step-up auth (acr_values): cliente exige MFA mas a conta não tem MFA enrolado.
254
+ 'mfa_challenge.required_no_enrollment': 'This client requires two-factor verification. Set up MFA in your account console to continue.',
255
+ // Mensagens de erro/flash produzidas pelos controllers.
256
+ 'errors.invalid_credentials': 'Invalid credentials',
257
+ 'errors.invalid_code': 'Invalid code',
258
+ 'errors.email_taken': 'Email already registered',
259
+ 'errors.signup_failed': 'Could not create the account',
260
+ 'errors.invalid_or_expired_token': 'Invalid or expired token',
261
+ 'errors.account_locked': 'Account temporarily locked due to too many attempts. Try again in {seconds}s.',
262
+ 'errors.session_expired': 'Session expired',
263
+ 'errors.challenge_expired': 'Challenge expired',
264
+ 'errors.passkeys_unavailable': 'Passkeys unavailable',
265
+ 'errors.no_passkey_registered': 'No passkey registered',
266
+ // Assuntos/corpos de e-mail transacional (default_mailer).
267
+ 'mail.common.link_fallback': "If the button does not work, copy and paste this link into your browser:",
268
+ 'mail.reset.subject': 'Reset your password',
269
+ 'mail.reset.heading': 'Reset your password',
270
+ 'mail.reset.intro': 'We received a request to reset the password for your account.',
271
+ 'mail.reset.cta': 'Reset password',
272
+ 'mail.reset.fallback': 'If you did not request this, you can ignore this email.',
273
+ 'mail.reset.expires': 'This link expires in {minutes} minutes.',
274
+ 'mail.verify.subject': 'Verify your email',
275
+ 'mail.verify.heading': 'Verify your email',
276
+ 'mail.verify.intro': 'Confirm your email address to finish setting up your account.',
277
+ 'mail.verify.cta': 'Verify email',
278
+ 'mail.verify.fallback': 'If you did not create this account, you can ignore this email.',
279
+ 'mail.verify.expires': 'This link expires in {minutes} minutes.',
280
+ 'mail.new_login.subject': 'New login to your account',
281
+ 'mail.new_login.heading': 'New login detected',
282
+ 'mail.new_login.intro': 'We detected a new login to your account.',
283
+ 'mail.new_login.when': 'When: {date}',
284
+ 'mail.new_login.ip': 'IP address: {ip}',
285
+ 'mail.new_login.device': 'Device: {device}',
286
+ 'mail.new_login.fallback': 'If this was you, no action is needed. If not, reset your password right away.',
287
+ 'mail.email_change.subject': 'Confirm your new email',
288
+ 'mail.email_change.heading': 'Confirm your new email',
289
+ 'mail.email_change.intro': 'We received a request to change the email address on your account. Confirm the new address below.',
290
+ 'mail.email_change.cta': 'Confirm new email',
291
+ 'mail.email_change.fallback': 'If you did not request this, you can ignore this email.',
292
+ 'mail.email_change.expires': 'This link expires in {minutes} minutes.',
293
+ };
294
+ /**
295
+ * Catálogo embutido pt-BR. Espelha TODAS as chaves do default `en`. Ativado
296
+ * com `i18n: { locale: 'pt-BR' }` sem nenhuma config extra de mensagens.
297
+ */
298
+ export const PT_BR_MESSAGES = {
17
299
  // Comum / fallback de marca.
18
300
  'common.app_fallback': 'Auth',
19
301
  'common.brand_eyebrow': 'Auth',
@@ -75,8 +357,6 @@ export const DEFAULT_MESSAGES = {
75
357
  // Consent (autorização de cliente OIDC).
76
358
  'consent.page_title': 'Autorizar',
77
359
  'consent.title': 'Autorizar acesso',
78
- // `{app}` é interpolado com o nome do app já envolto em <strong> (renderizado
79
- // raw na view). O nome vem do branding (config-trusted).
80
360
  'consent.body': 'O app <strong>{app}</strong> quer acessar sua conta.',
81
361
  'consent.submit': 'Autorizar',
82
362
  // Console de conta — login (account/login).
@@ -258,20 +538,63 @@ export const DEFAULT_MESSAGES = {
258
538
  'errors.signup_failed': 'Não foi possível criar a conta',
259
539
  'errors.invalid_or_expired_token': 'Token inválido ou expirado',
260
540
  'errors.account_locked': 'Conta temporariamente bloqueada por excesso de tentativas. Tente novamente em {seconds}s.',
541
+ 'errors.session_expired': 'Sessão expirada',
542
+ 'errors.challenge_expired': 'Desafio expirado',
543
+ 'errors.passkeys_unavailable': 'Passkeys indisponíveis',
544
+ 'errors.no_passkey_registered': 'Nenhuma passkey registrada',
545
+ // Assuntos/corpos de e-mail transacional (default_mailer).
546
+ 'mail.common.link_fallback': 'Se o botão não funcionar, copie e cole este link no navegador:',
547
+ 'mail.reset.subject': 'Redefinição de senha',
548
+ 'mail.reset.heading': 'Redefinição de senha',
549
+ 'mail.reset.intro': 'Recebemos um pedido para redefinir a senha da sua conta.',
550
+ 'mail.reset.cta': 'Redefinir senha',
551
+ 'mail.reset.fallback': 'Se você não solicitou isso, pode ignorar este e-mail.',
552
+ 'mail.reset.expires': 'Este link expira em {minutes} minutos.',
553
+ 'mail.verify.subject': 'Verifique seu e-mail',
554
+ 'mail.verify.heading': 'Verifique seu e-mail',
555
+ 'mail.verify.intro': 'Confirme seu endereço de e-mail para concluir a configuração da conta.',
556
+ 'mail.verify.cta': 'Verificar e-mail',
557
+ 'mail.verify.fallback': 'Se você não criou esta conta, pode ignorar este e-mail.',
558
+ 'mail.verify.expires': 'Este link expira em {minutes} minutos.',
559
+ 'mail.new_login.subject': 'Novo login na sua conta',
560
+ 'mail.new_login.heading': 'Novo login detectado',
561
+ 'mail.new_login.intro': 'Detectamos um novo login na sua conta.',
562
+ 'mail.new_login.when': 'Quando: {date}',
563
+ 'mail.new_login.ip': 'Endereço IP: {ip}',
564
+ 'mail.new_login.device': 'Dispositivo: {device}',
565
+ 'mail.new_login.fallback': 'Se foi você, nenhuma ação é necessária. Caso contrário, redefina sua senha imediatamente.',
566
+ 'mail.email_change.subject': 'Confirme seu novo e-mail',
567
+ 'mail.email_change.heading': 'Confirme seu novo e-mail',
568
+ 'mail.email_change.intro': 'Recebemos um pedido para trocar o e-mail da sua conta. Confirme o novo endereço abaixo.',
569
+ 'mail.email_change.cta': 'Confirmar novo e-mail',
570
+ 'mail.email_change.fallback': 'Se você não solicitou isso, pode ignorar este e-mail.',
571
+ 'mail.email_change.expires': 'Este link expira em {minutes} minutos.',
572
+ };
573
+ /**
574
+ * Locales embutidos no host-kit. O `en` é o default; o `pt-BR` está disponível
575
+ * com `i18n: { locale: 'pt-BR' }` sem nenhuma config de mensagens extra. Os
576
+ * overrides/locales do host (via `I18nConfig.messages`) são mesclados por cima.
577
+ */
578
+ export const BUILTIN_MESSAGES = {
579
+ en: DEFAULT_MESSAGES,
580
+ 'pt-BR': PT_BR_MESSAGES,
261
581
  };
262
582
  /**
263
- * Resolve o catálogo ativo: mescla os overrides do locale selecionado SOBRE o
264
- * default pt-BR. Sem config, retorna os defaults intactos. Chaves omitidas pelo
265
- * locale escolhido caem no default pt-BR (fallback de cobertura).
583
+ * Resolve o catálogo ativo. Começa do catálogo embutido do locale selecionado
584
+ * (ou do default `en` quando o locale não é embutido), depois mescla os
585
+ * overrides do host por cima. Sem config, retorna o default `en` intacto.
586
+ * Chaves omitidas caem no default `en` (fallback de cobertura).
266
587
  */
267
588
  export function resolveMessages(i18n) {
268
- const base = { ...DEFAULT_MESSAGES };
269
589
  const locale = i18n?.locale ?? DEFAULT_LOCALE;
590
+ // Base: sempre o default `en` para garantir cobertura total das chaves; o
591
+ // catálogo embutido do locale (ex.: pt-BR) é mesclado por cima.
592
+ const base = { ...DEFAULT_MESSAGES, ...(BUILTIN_MESSAGES[locale] ?? {}) };
270
593
  const overrides = i18n?.messages?.[locale];
271
594
  if (!overrides)
272
595
  return base;
273
596
  // Mescla só valores definidos (o `Partial` permite undefined); chaves omitidas
274
- // seguem caindo no default pt-BR.
597
+ // seguem caindo no catálogo base.
275
598
  for (const [key, value] of Object.entries(overrides)) {
276
599
  if (value !== undefined)
277
600
  base[key] = value;
@@ -5,11 +5,11 @@ function renderDashboardHtml(snapshot) {
5
5
  const histograms = Object.entries(snapshot.histograms)
6
6
  .map(([k, h]) => `<tr><td>${k}</td><td>${h.count}</td><td>${h.sum}</td><td>${h.min}</td><td>${h.max}</td></tr>`)
7
7
  .join('');
8
- return `<!doctype html><html lang="pt-br"><head><meta charset="utf-8"><meta http-equiv="refresh" content="5"><title>AuthKit — Métricas</title>
8
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="refresh" content="5"><title>AuthKit — Metrics</title>
9
9
  <style>body{font-family:system-ui,sans-serif;margin:2rem;color:#111}h1{font-size:1.2rem}table{border-collapse:collapse;margin:1rem 0;width:100%}th,td{border:1px solid #ddd;padding:.4rem .6rem;text-align:left;font-size:.9rem}th{background:#f5f5f5}</style>
10
- </head><body><h1>AuthKit — Métricas</h1><p>Atualizado: ${snapshot.updatedAt ? new Date(snapshot.updatedAt).toISOString() : '—'}</p>
11
- <h2>Counters</h2><table><thead><tr><th>Métrica</th><th>Total</th></tr></thead><tbody>${counters || '<tr><td colspan="2">—</td></tr>'}</tbody></table>
12
- <h2>Histograms</h2><table><thead><tr><th>Métrica</th><th>Count</th><th>Sum</th><th>Min</th><th>Max</th></tr></thead><tbody>${histograms || '<tr><td colspan="5">—</td></tr>'}</tbody></table>
10
+ </head><body><h1>AuthKit — Metrics</h1><p>Updated: ${snapshot.updatedAt ? new Date(snapshot.updatedAt).toISOString() : '—'}</p>
11
+ <h2>Counters</h2><table><thead><tr><th>Metric</th><th>Total</th></tr></thead><tbody>${counters || '<tr><td colspan="2">—</td></tr>'}</tbody></table>
12
+ <h2>Histograms</h2><table><thead><tr><th>Metric</th><th>Count</th><th>Sum</th><th>Min</th><th>Max</th></tr></thead><tbody>${histograms || '<tr><td colspan="5">—</td></tr>'}</tbody></table>
13
13
  </body></html>`;
14
14
  }
15
15
  export default class MetricsController {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dudousxd/adonis-authkit-server",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "AdonisJS OIDC/OAuth2 provider (Identity Provider) toolkit: ejectable auth server with sessions, rate-limiting, MFA/TOTP, audit log, federated logout and OpenTelemetry metrics.",
5
5
  "license": "MIT",
6
6
  "author": "dudousxd",