@adonis-agora/authkit-server 0.69.0 → 0.71.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.
Files changed (48) hide show
  1. package/build/commands/commands.json +18 -0
  2. package/build/commands/normalize_emails.d.ts +21 -0
  3. package/build/commands/normalize_emails.js +106 -0
  4. package/build/index.d.ts +4 -3
  5. package/build/index.js +6 -2
  6. package/build/src/accounts/account_store.d.ts +65 -1
  7. package/build/src/accounts/account_store.js +24 -0
  8. package/build/src/accounts/lucid_store/core.d.ts +2 -2
  9. package/build/src/accounts/lucid_store/core.js +16 -0
  10. package/build/src/accounts/lucid_store/mfa.js +22 -0
  11. package/build/src/audit/audit_sink.d.ts +1 -1
  12. package/build/src/audit/audit_sink.js +4 -0
  13. package/build/src/commands/import_users.js +7 -2
  14. package/build/src/commands/normalize_emails.d.ts +78 -0
  15. package/build/src/commands/normalize_emails.js +129 -0
  16. package/build/src/host/account_api/account_api_controller.d.ts +2 -0
  17. package/build/src/host/account_api/account_api_controller.js +22 -3
  18. package/build/src/host/account_api/account_mfa_api_controller.d.ts +101 -0
  19. package/build/src/host/account_api/account_mfa_api_controller.js +286 -0
  20. package/build/src/host/account_api/account_orgs_api_controller.d.ts +126 -0
  21. package/build/src/host/account_api/account_orgs_api_controller.js +468 -0
  22. package/build/src/host/account_lockout.js +7 -2
  23. package/build/src/host/admin_api/admin_users_service.js +11 -3
  24. package/build/src/host/admin_api/dto.d.ts +1 -1
  25. package/build/src/host/admin_validators.d.ts +2 -2
  26. package/build/src/host/admin_validators.js +3 -2
  27. package/build/src/host/controllers/account_mfa_controller.js +1 -2
  28. package/build/src/host/controllers/account_orgs_controller.js +4 -18
  29. package/build/src/host/controllers/account_security_controller.js +3 -1
  30. package/build/src/host/controllers/account_session_controller.js +7 -5
  31. package/build/src/host/controllers/interaction_controller.d.ts +10 -0
  32. package/build/src/host/controllers/interaction_controller.js +46 -14
  33. package/build/src/host/controllers/registration_controller.js +15 -5
  34. package/build/src/host/controllers/social_controller.js +8 -1
  35. package/build/src/host/email_identifier.d.ts +29 -0
  36. package/build/src/host/email_identifier.js +31 -0
  37. package/build/src/host/org_policy.d.ts +26 -0
  38. package/build/src/host/org_policy.js +35 -0
  39. package/build/src/host/passkey_registration_challenge.d.ts +12 -0
  40. package/build/src/host/passkey_registration_challenge.js +12 -0
  41. package/build/src/host/register_auth_host.js +51 -0
  42. package/build/src/host/sudo_mode.d.ts +17 -0
  43. package/build/src/host/sudo_mode.js +31 -12
  44. package/build/src/host/ui-dist/assets/{index-D9CYQnZR.js → index-Dct63ai-.js} +2 -2
  45. package/build/src/host/ui-dist/index.html +1 -1
  46. package/build/src/host/validators.d.ts +5 -5
  47. package/build/src/host/validators.js +17 -5
  48. package/package.json +2 -2
@@ -153,6 +153,24 @@
153
153
  "options": { "startApp": true },
154
154
  "filePath": "import_users.js"
155
155
  },
156
+ {
157
+ "commandName": "authkit:users:normalize-emails",
158
+ "description": "Migra os e-mails gravados para a forma normalizada da identidade (trim + lowercase). Sem --apply apenas relata.",
159
+ "namespace": "authkit",
160
+ "aliases": [],
161
+ "flags": [
162
+ {
163
+ "name": "apply",
164
+ "flagName": "apply",
165
+ "required": false,
166
+ "type": "boolean",
167
+ "description": "Grava a normalização (sem ele, apenas relata)."
168
+ }
169
+ ],
170
+ "args": [],
171
+ "options": { "startApp": true },
172
+ "filePath": "normalize_emails.js"
173
+ },
156
174
  {
157
175
  "commandName": "authkit:settings:list",
158
176
  "description": "Lista todas as runtime settings presentes em `auth_settings`.",
@@ -0,0 +1,21 @@
1
+ import { BaseCommand } from '@adonisjs/core/ace';
2
+ import type { CommandOptions } from '@adonisjs/core/types/ace';
3
+ /**
4
+ * Migra os endereços GRAVADOS para a forma normalizada da identidade (`trim` +
5
+ * `toLowerCase`) — a MESMA que o cadastro grava e que o login busca.
6
+ *
7
+ * É PASSO OBRIGATÓRIO do upgrade para quem tem contas criadas antes da v0.69:
8
+ * o cadastro antigo gravava o endereço mutilado (`.normalizeEmail()` do VineJS
9
+ * removia pontos e `+tag` no gmail) e import/convite/provider social gravavam a
10
+ * grafia crua, com maiúsculas. O login busca UMA forma só; qualquer outra grafia
11
+ * gravada deixa a conta INALCANÇÁVEL, sem mensagem de erro (a tela é à prova de
12
+ * enumeração).
13
+ */
14
+ export default class AuthkitNormalizeEmails extends BaseCommand {
15
+ static commandName: string;
16
+ static description: string;
17
+ static help: string[];
18
+ static options: CommandOptions;
19
+ apply?: boolean;
20
+ run(): Promise<void>;
21
+ }
@@ -0,0 +1,106 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { BaseCommand, flags } from '@adonisjs/core/ace';
8
+ import { normalizeAccountEmails } from '../src/commands/normalize_emails.js';
9
+ import { resolveAuthkitConfig } from '../src/commands/resolve_config.js';
10
+ /**
11
+ * Migra os endereços GRAVADOS para a forma normalizada da identidade (`trim` +
12
+ * `toLowerCase`) — a MESMA que o cadastro grava e que o login busca.
13
+ *
14
+ * É PASSO OBRIGATÓRIO do upgrade para quem tem contas criadas antes da v0.69:
15
+ * o cadastro antigo gravava o endereço mutilado (`.normalizeEmail()` do VineJS
16
+ * removia pontos e `+tag` no gmail) e import/convite/provider social gravavam a
17
+ * grafia crua, com maiúsculas. O login busca UMA forma só; qualquer outra grafia
18
+ * gravada deixa a conta INALCANÇÁVEL, sem mensagem de erro (a tela é à prova de
19
+ * enumeração).
20
+ */
21
+ export default class AuthkitNormalizeEmails extends BaseCommand {
22
+ static commandName = 'authkit:users:normalize-emails';
23
+ static description = 'Migra os e-mails gravados para a forma normalizada da identidade (trim + lowercase). Sem --apply apenas relata.';
24
+ static help = [
25
+ 'Sem --apply o comando NÃO escreve nada: varre as contas e relata quantas mudariam,',
26
+ 'quais, e as COLISÕES (duas contas que colapsam no mesmo endereço).',
27
+ 'Com --apply grava, recusando-se a tocar em qualquer conta envolvida numa colisão —',
28
+ 'a migração nunca funde contas nem escolhe vencedor: as colididas saem listadas para',
29
+ 'decisão humana.',
30
+ '',
31
+ 'Sai com código != 0 quando há colisões (ou erro de escrita): é a sua deixa de que',
32
+ 'a base ainda precisa de decisão humana.',
33
+ '',
34
+ 'Exemplos:',
35
+ ' node ace authkit:users:normalize-emails',
36
+ ' node ace authkit:users:normalize-emails --apply',
37
+ ];
38
+ static options = { startApp: true };
39
+ async run() {
40
+ const config = await this.app.container.make('config');
41
+ // Resolve o config provider exportado por defineConfig (provider cru não tem accountStore).
42
+ const authkitConfig = await resolveAuthkitConfig(this.app, config.get('authkit', null));
43
+ const store = authkitConfig?.accountStore;
44
+ if (!store) {
45
+ this.logger.logError("❌ config('authkit').accountStore ausente.");
46
+ this.exitCode = 1;
47
+ return;
48
+ }
49
+ let report;
50
+ try {
51
+ report = await normalizeAccountEmails(store, { apply: this.apply });
52
+ }
53
+ catch (error) {
54
+ this.logger.logError(`❌ ${error.message}`);
55
+ this.exitCode = 1;
56
+ return;
57
+ }
58
+ if (!this.apply) {
59
+ this.logger.info('🧪 Relatório — nenhum dado foi alterado (use --apply para gravar).');
60
+ }
61
+ this.logger.info(`🔎 ${report.scanned} conta(s) varrida(s); ${report.alreadyNormalized} já normalizada(s).`);
62
+ if (report.changes.length === 0) {
63
+ this.logger.success('✅ Nenhum endereço a normalizar.');
64
+ }
65
+ else if (this.apply) {
66
+ this.logger.success(`✅ ${report.applied} endereço(s) normalizado(s).`);
67
+ }
68
+ else {
69
+ this.logger.warning(`⚠️ ${report.changes.length} endereço(s) seriam normalizado(s):`);
70
+ }
71
+ for (const change of report.changes) {
72
+ if (change.error)
73
+ continue;
74
+ this.logger.info(` ${change.from} → ${change.to}`);
75
+ }
76
+ const failures = report.changes.filter((change) => !!change.error);
77
+ if (failures.length > 0) {
78
+ this.logger.logError(`❌ ${failures.length} não gravada(s):`);
79
+ for (const change of failures) {
80
+ this.logger.logError(` ${change.from} → ${change.to}: ${change.error}`);
81
+ }
82
+ this.exitCode = 1;
83
+ }
84
+ if (report.unusable.length > 0) {
85
+ // Não entram em "já normalizada": o relatório não dá por boa uma linha que
86
+ // deixou para trás.
87
+ this.logger.warning(`⚠️ ${report.unusable.length} conta(s) com e-mail vazio/inutilizável — NÃO migrada(s):`);
88
+ for (const account of report.unusable) {
89
+ this.logger.warning(` id ${account.accountId}: ${JSON.stringify(account.email)}`);
90
+ }
91
+ }
92
+ if (report.collisions.length > 0) {
93
+ this.logger.logError(`❌ ${report.collisions.length} colisão(ões) — ${report.skippedByCollision} conta(s) NÃO tocada(s). Decida à mão (a migração não funde contas):`);
94
+ for (const collision of report.collisions) {
95
+ this.logger.logError(` ${collision.email}:`);
96
+ for (const account of collision.accounts) {
97
+ this.logger.logError(` - ${account.email} (id ${account.accountId})`);
98
+ }
99
+ }
100
+ this.exitCode = 1;
101
+ }
102
+ }
103
+ }
104
+ __decorate([
105
+ flags.boolean({ description: 'Grava a normalização (sem ele, apenas relata).' })
106
+ ], AuthkitNormalizeEmails.prototype, "apply", void 0);
package/build/index.d.ts CHANGED
@@ -3,8 +3,8 @@
3
3
  * O comando do AdonisJS importa o entrypoint principal e procura por estes exports.
4
4
  */
5
5
  export { configure } from './commands/configure.js';
6
- export type { AccountDeletionCapability, AccountImportCapability, AccountSecurityCapability, AccountStatusCapability, AccountStore, ActiveOrgInfo, AdminCapability, AuthAccount, CoreAccountStore, CreateAccountInput, EmailVerificationStatusCapability, ImportAccountInput, LinkProviderIdentityInput, ListAccountsParams, LoginMethodsPreferenceCapability, MagicLinkCapability, MfaCapability, OrganizationsCapability, OrgInvitation, OrgMember, OrgSummary, OtpLoginCapability, OtpLoginVerifyResult, Paginated, PasskeySummary, ProfileCapability, ProviderIdentityCapability, ProviderIdentitySummary, WebauthnCapability, } from './src/accounts/account_store.js';
7
- export { supportsAccountDeletion, supportsAccountImport, supportsAccountSecurity, supportsAccountStatus, supportsEmailVerificationStatus, supportsLoginMethodsPreference, supportsMagicLink, supportsMfa, supportsOrganizations, supportsOtpLogin, supportsPasskeys, supportsProfile, supportsProviderIdentity, } from './src/accounts/account_store.js';
6
+ export type { AccountDeletionCapability, AccountEmailRewriteCapability, AccountImportCapability, AccountSecurityCapability, AccountStatusCapability, AccountStore, ActiveOrgInfo, AdminCapability, AuthAccount, CoreAccountStore, CreateAccountInput, EmailVerificationStatusCapability, ImportAccountInput, LinkProviderIdentityInput, ListAccountsParams, LoginMethodsPreferenceCapability, MagicLinkCapability, MfaCapability, OrganizationsCapability, OrgInvitation, OrgMember, OrgSummary, OtpLoginCapability, OtpLoginVerifyResult, Paginated, PasskeySummary, ProfileCapability, ProviderIdentityCapability, ProviderIdentitySummary, WebauthnCapability, } from './src/accounts/account_store.js';
7
+ export { supportsAccountDeletion, supportsAccountEmailRewrite, supportsAccountImport, supportsAccountSecurity, supportsAccountStatus, supportsEmailVerificationStatus, supportsLoginMethodsPreference, supportsMagicLink, supportsMfa, supportsOrganizations, supportsOtpLogin, supportsPasskeys, supportsProfile, supportsProviderIdentity, } from './src/accounts/account_store.js';
8
8
  export type { AccountSecretEncrypter, LucidAccountStoreOptions, } from './src/accounts/lucid_account_store.js';
9
9
  export { appKeyEncrypter, lucidAccountStore } from './src/accounts/lucid_account_store.js';
10
10
  export { AuthOrganization, AuthOrganizationInvitation, AuthOrganizationMember, defaultOrganizationModels, } from './src/accounts/lucid_store/organization_models.js';
@@ -64,6 +64,7 @@ export { deriveLockedSettingKeys, isSettingLocked, lockedSettingKeys, POLICY_ROU
64
64
  export { consoleLoginUrl, getAccountId, hasAccountSession, realAccountId, } from './src/host/console_session.js';
65
65
  export type { AuthkitCsrfOptions } from './src/host/csrf.js';
66
66
  export { authkitCsrfExceptions } from './src/host/csrf.js';
67
+ export { normalizeEmailIdentifier } from './src/host/email_identifier.js';
67
68
  export type { ResolveGeo } from './src/host/geo.js';
68
69
  export { GEO_RESOLVE_TIMEOUT_MS, resolveGeoSafe } from './src/host/geo.js';
69
70
  export type { AuthMessages, I18nConfig } from './src/host/i18n.js';
@@ -94,7 +95,7 @@ export { sudoMethods } from './src/host/sudo/index.js';
94
95
  export { completeSudo, fail as failSudo, LAST_METHOD_SESSION_KEY, sudoContextFrom, } from './src/host/sudo/runtime.js';
95
96
  export type { SudoContext, SudoMethod, SudoMethodDescriptor, SudoRouteHelpers, } from './src/host/sudo/types.js';
96
97
  export type { ResolvedSudoModeSetting, SudoModeSetting, } from './src/host/sudo_mode.js';
97
- export { isSudoActive, markSudo, requireSudo, resolveEffectiveSudoMode, SUDO_ACCOUNT_SESSION_KEY, SUDO_MODE_DEFAULTS, SUDO_SESSION_KEY, } from './src/host/sudo_mode.js';
98
+ export { isSudoActive, isSudoSatisfied, markSudo, requireSudo, resolveEffectiveSudoMode, SUDO_ACCOUNT_SESSION_KEY, SUDO_MODE_DEFAULTS, SUDO_SESSION_KEY, } from './src/host/sudo_mode.js';
98
99
  export { barChartSvg } from './src/host/svg_chart.js';
99
100
  export type { ResolvedTrustedDevicesConfig, TrustedDevicePayload, TrustedDevicesConfigInput, } from './src/host/trusted_device.js';
100
101
  export { buildTrustedDevicePayload, isTrustedDeviceValid, resolveTrustedDevices, TRUSTED_DEVICE_COOKIE, } from './src/host/trusted_device.js';
package/build/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  * O comando do AdonisJS importa o entrypoint principal e procura por estes exports.
4
4
  */
5
5
  export { configure } from './commands/configure.js';
6
- export { supportsAccountDeletion, supportsAccountImport, supportsAccountSecurity, supportsAccountStatus, supportsEmailVerificationStatus, supportsLoginMethodsPreference, supportsMagicLink, supportsMfa, supportsOrganizations, supportsOtpLogin, supportsPasskeys, supportsProfile, supportsProviderIdentity, } from './src/accounts/account_store.js';
6
+ export { supportsAccountDeletion, supportsAccountEmailRewrite, supportsAccountImport, supportsAccountSecurity, supportsAccountStatus, supportsEmailVerificationStatus, supportsLoginMethodsPreference, supportsMagicLink, supportsMfa, supportsOrganizations, supportsOtpLogin, supportsPasskeys, supportsProfile, supportsProviderIdentity, } from './src/accounts/account_store.js';
7
7
  export { appKeyEncrypter, lucidAccountStore } from './src/accounts/lucid_account_store.js';
8
8
  export { AuthOrganization, AuthOrganizationInvitation, AuthOrganizationMember, defaultOrganizationModels, } from './src/accounts/lucid_store/organization_models.js';
9
9
  export { lucidStores } from './src/accounts/lucid_stores.js';
@@ -49,6 +49,7 @@ export { brandFor, isFirstParty, isFirstPartyClient } from './src/host/branding.
49
49
  export { deriveLockedSettingKeys, isSettingLocked, lockedSettingKeys, POLICY_ROUTE_OPTIONS, resetLockedSettingKeys, SettingLockedError, setLockedSettingKeys, } from './src/host/config_locks.js';
50
50
  export { consoleLoginUrl, getAccountId, hasAccountSession, realAccountId, } from './src/host/console_session.js';
51
51
  export { authkitCsrfExceptions } from './src/host/csrf.js';
52
+ export { normalizeEmailIdentifier } from './src/host/email_identifier.js';
52
53
  export { GEO_RESOLVE_TIMEOUT_MS, resolveGeoSafe } from './src/host/geo.js';
53
54
  export { BUILTIN_MESSAGES, DEFAULT_LOCALE, DEFAULT_MESSAGES, PT_BR_MESSAGES, resolveMessages, translate, } from './src/host/i18n.js';
54
55
  export { ensureConsoleSession } from './src/host/idp_session_bridge.js';
@@ -90,7 +91,10 @@ export { sudoMethods } from './src/host/sudo/index.js';
90
91
  // `returnTo` montado na mão é um open redirect esperando acontecer.
91
92
  export { completeSudo, fail as failSudo, LAST_METHOD_SESSION_KEY, sudoContextFrom, } from './src/host/sudo/runtime.js';
92
93
  // Sudo mode — helpers for host controllers that require step-up authentication.
93
- export { isSudoActive, markSudo, requireSudo, resolveEffectiveSudoMode, SUDO_ACCOUNT_SESSION_KEY, SUDO_MODE_DEFAULTS, SUDO_SESSION_KEY, } from './src/host/sudo_mode.js';
94
+ export { isSudoActive,
95
+ // A DECISÃO de sudo sem efeito na resposta — o que o host chama quando quer
96
+ // recusar em JSON em vez de redirecionar para `/account/confirm`.
97
+ isSudoSatisfied, markSudo, requireSudo, resolveEffectiveSudoMode, SUDO_ACCOUNT_SESSION_KEY, SUDO_MODE_DEFAULTS, SUDO_SESSION_KEY, } from './src/host/sudo_mode.js';
94
98
  export { barChartSvg } from './src/host/svg_chart.js';
95
99
  export { buildTrustedDevicePayload, isTrustedDeviceValid, resolveTrustedDevices, TRUSTED_DEVICE_COOKIE, } from './src/host/trusted_device.js';
96
100
  export { parseUserAgent } from './src/host/user_agent.js';
@@ -278,6 +278,26 @@ export interface MfaCapability {
278
278
  consumeRecoveryCode(accountId: string, code: string): Promise<boolean>;
279
279
  /** Desliga o MFA: limpa segredo + mfaEnabledAt + recovery codes. */
280
280
  disableMfa(accountId: string): Promise<void>;
281
+ /**
282
+ * Quantos recovery codes AINDA não foram consumidos. OPCIONAL: existe para a
283
+ * tela de segundo fator dizer "restam N" sem revelar os códigos (só o hash
284
+ * fica guardado). `null` quando o MFA não está ativo.
285
+ *
286
+ * Opcional de propósito: um store de terceiro escrito contra a versão
287
+ * anterior de {@link MfaCapability} continua satisfazendo o tipo, e os
288
+ * callers fazem capability-probe (`typeof store.countRecoveryCodes ===
289
+ * 'function'`) antes de chamar.
290
+ */
291
+ countRecoveryCodes?(accountId: string): Promise<number | null>;
292
+ /**
293
+ * Regenera os recovery codes de uma conta com MFA ATIVO: descarta os antigos
294
+ * (inclusive os não usados) e devolve os novos em claro — uma única vez, como
295
+ * no {@link MfaCapability.confirmTotpEnrollment}. `null` quando o MFA não
296
+ * está ativo (não há o que regenerar).
297
+ *
298
+ * Opcional pelo mesmo motivo de {@link MfaCapability.countRecoveryCodes}.
299
+ */
300
+ regenerateRecoveryCodes?(accountId: string): Promise<string[] | null>;
281
301
  }
282
302
  /**
283
303
  * MFA / WebAuthn (passkeys) — 2º fator alternativo ao TOTP. Como o TOTP, é uma
@@ -421,6 +441,28 @@ export interface AccountImportCapability {
421
441
  */
422
442
  importAccount(input: ImportAccountInput): Promise<AuthAccount | null>;
423
443
  }
444
+ /**
445
+ * Regravação ADMINISTRATIVA do endereço da conta (comando
446
+ * `authkit:users:normalize-emails`). CAPACIDADE opcional, presente no store
447
+ * Lucid default.
448
+ *
449
+ * Distinta de {@link AccountSecurityCapability.requestEmailChange}: NÃO há
450
+ * cerimônia de confirmação (nenhum token viaja, nenhum e-mail é enviado) e o
451
+ * estado de "e-mail verificado" fica COMO ESTÁ — a migração só canonicaliza a
452
+ * grafia de uma caixa postal que já era a mesma (`trim` + `toLowerCase`), então
453
+ * marcar como verificado seria conceder uma prova que ninguém deu.
454
+ *
455
+ * Não é um caminho para trocar de titular: quem quer mudar de caixa postal passa
456
+ * pelo fluxo com confirmação. Um store pode simplesmente não a implementar — aí
457
+ * o `--apply` do comando recusa e a base é migrada por SQL do host.
458
+ */
459
+ export interface AccountEmailRewriteCapability {
460
+ /**
461
+ * Regrava o e-mail da conta. Retorna `false` quando a conta não existe ou
462
+ * quando o endereço já pertence a OUTRA conta (a migração nunca funde contas).
463
+ */
464
+ rewriteAccountEmail(accountId: string, email: string): Promise<boolean>;
465
+ }
424
466
  /**
425
467
  * Login sem senha por "magic link" — um token de uso único e curta duração
426
468
  * enviado por e-mail. CAPACIDADE opcional: stores sem suporte omitem os métodos e
@@ -641,7 +683,7 @@ export type AccountStore = CoreAccountStore & {
641
683
  * Undefined → `users` (back-compat com stores próprios).
642
684
  */
643
685
  readonly accountTable?: string;
644
- } & Partial<MfaCapability & WebauthnCapability & ProviderIdentityCapability & AccountSecurityCapability & AccountStatusCapability & ProfileCapability & MagicLinkCapability & OtpLoginCapability & EmailVerificationStatusCapability & AccountDeletionCapability & AccountImportCapability & OrganizationsCapability & PasswordHistoryCapability & PasswordExpirationCapability & LoginMethodsPreferenceCapability>;
686
+ } & Partial<MfaCapability & WebauthnCapability & ProviderIdentityCapability & AccountSecurityCapability & AccountStatusCapability & ProfileCapability & MagicLinkCapability & OtpLoginCapability & EmailVerificationStatusCapability & AccountDeletionCapability & AccountImportCapability & AccountEmailRewriteCapability & OrganizationsCapability & PasswordHistoryCapability & PasswordExpirationCapability & LoginMethodsPreferenceCapability>;
645
687
  /** Type guard: o store implementa a capacidade de MFA / TOTP. */
646
688
  export declare function supportsMfa(store: AccountStore): store is AccountStore & MfaCapability;
647
689
  /**
@@ -654,6 +696,26 @@ export declare function supportsCountByGlobalRole(store: AccountStore): store is
654
696
  };
655
697
  /** Type guard: o store implementa a capacidade de passkeys / WebAuthn. */
656
698
  export declare function supportsPasskeys(store: AccountStore): store is AccountStore & WebauthnCapability;
699
+ /**
700
+ * Type guard: o store sabe REGENERAR recovery codes
701
+ * ({@link MfaCapability.regenerateRecoveryCodes}).
702
+ *
703
+ * Guard PRÓPRIO, separado de {@link supportsMfa}, porque o método é opcional
704
+ * dentro da capacidade: um store escrito antes desta versão implementa MFA
705
+ * inteiro e não implementa este método. Quem oferece o botão "gerar novos
706
+ * códigos" pergunta por aqui.
707
+ */
708
+ export declare function supportsRecoveryCodeRegeneration(store: AccountStore): store is AccountStore & MfaCapability & {
709
+ regenerateRecoveryCodes(accountId: string): Promise<string[] | null>;
710
+ };
711
+ /**
712
+ * Type guard: o store sabe CONTAR recovery codes restantes
713
+ * ({@link MfaCapability.countRecoveryCodes}). Mesmo motivo do
714
+ * {@link supportsRecoveryCodeRegeneration} para ser um guard separado.
715
+ */
716
+ export declare function supportsRecoveryCodeCount(store: AccountStore): store is AccountStore & MfaCapability & {
717
+ countRecoveryCodes(accountId: string): Promise<number | null>;
718
+ };
657
719
  /** Type guard: o store implementa account linking por identidade de provider. */
658
720
  export declare function supportsProviderIdentity(store: AccountStore): store is AccountStore & ProviderIdentityCapability;
659
721
  /** Type guard: o store implementa o self-service de segurança (senha/e-mail). */
@@ -672,6 +734,8 @@ export declare function supportsEmailVerificationStatus(store: AccountStore): st
672
734
  export declare function supportsAccountDeletion(store: AccountStore): store is AccountStore & AccountDeletionCapability;
673
735
  /** Type guard: o store implementa o import em massa de contas. */
674
736
  export declare function supportsAccountImport(store: AccountStore): store is AccountStore & AccountImportCapability;
737
+ /** Type guard: o store implementa a regravação administrativa do e-mail. */
738
+ export declare function supportsAccountEmailRewrite(store: AccountStore): store is AccountStore & AccountEmailRewriteCapability;
675
739
  /** Type guard: o store implementa histórico de senhas (disallow_password_reuse). */
676
740
  export declare function supportsPasswordHistory(store: AccountStore): store is AccountStore & PasswordHistoryCapability;
677
741
  /** Type guard: o store implementa expiração de senha (password_changed_at coluna). */
@@ -18,6 +18,26 @@ export function supportsCountByGlobalRole(store) {
18
18
  export function supportsPasskeys(store) {
19
19
  return typeof store.listPasskeys === 'function';
20
20
  }
21
+ /**
22
+ * Type guard: o store sabe REGENERAR recovery codes
23
+ * ({@link MfaCapability.regenerateRecoveryCodes}).
24
+ *
25
+ * Guard PRÓPRIO, separado de {@link supportsMfa}, porque o método é opcional
26
+ * dentro da capacidade: um store escrito antes desta versão implementa MFA
27
+ * inteiro e não implementa este método. Quem oferece o botão "gerar novos
28
+ * códigos" pergunta por aqui.
29
+ */
30
+ export function supportsRecoveryCodeRegeneration(store) {
31
+ return typeof store.regenerateRecoveryCodes === 'function';
32
+ }
33
+ /**
34
+ * Type guard: o store sabe CONTAR recovery codes restantes
35
+ * ({@link MfaCapability.countRecoveryCodes}). Mesmo motivo do
36
+ * {@link supportsRecoveryCodeRegeneration} para ser um guard separado.
37
+ */
38
+ export function supportsRecoveryCodeCount(store) {
39
+ return typeof store.countRecoveryCodes === 'function';
40
+ }
21
41
  /** Type guard: o store implementa account linking por identidade de provider. */
22
42
  export function supportsProviderIdentity(store) {
23
43
  return typeof store.findByProviderIdentity === 'function';
@@ -55,6 +75,10 @@ export function supportsAccountDeletion(store) {
55
75
  export function supportsAccountImport(store) {
56
76
  return typeof store.importAccount === 'function';
57
77
  }
78
+ /** Type guard: o store implementa a regravação administrativa do e-mail. */
79
+ export function supportsAccountEmailRewrite(store) {
80
+ return typeof store.rewriteAccountEmail === 'function';
81
+ }
58
82
  /** Type guard: o store implementa histórico de senhas (disallow_password_reuse). */
59
83
  export function supportsPasswordHistory(store) {
60
84
  return typeof store.isPasswordReused === 'function';
@@ -1,4 +1,4 @@
1
- import type { AccountImportCapability, AccountSecurityCapability, CoreAccountStore, MagicLinkCapability, OtpLoginCapability } from '../account_store.js';
1
+ import type { AccountEmailRewriteCapability, AccountImportCapability, AccountSecurityCapability, CoreAccountStore, MagicLinkCapability, OtpLoginCapability } from '../account_store.js';
2
2
  import type { LucidStoreContext } from './shared.js';
3
3
  /**
4
4
  * Núcleo SEMPRE presente do {@link CoreAccountStore} sobre um model Lucid:
@@ -6,4 +6,4 @@ import type { LucidStoreContext } from './shared.js';
6
6
  * (listagem paginada + roles globais) e o self-service de segurança
7
7
  * ({@link AccountSecurityCapability}: trocar senha/e-mail).
8
8
  */
9
- export declare function buildCore(ctx: LucidStoreContext): CoreAccountStore & AccountSecurityCapability & MagicLinkCapability & OtpLoginCapability & AccountImportCapability;
9
+ export declare function buildCore(ctx: LucidStoreContext): CoreAccountStore & AccountSecurityCapability & MagicLinkCapability & OtpLoginCapability & AccountImportCapability & AccountEmailRewriteCapability;
@@ -432,6 +432,22 @@ export function buildCore(ctx) {
432
432
  await row.save();
433
433
  return { token, account: toAccount(row), newEmail };
434
434
  },
435
+ async rewriteAccountEmail(accountId, email) {
436
+ const row = await Model.find(accountId);
437
+ if (!row)
438
+ return false;
439
+ // Nunca funde contas: se o endereço já é de OUTRA conta, recusa. O comando
440
+ // já detecta a colisão antes de chegar aqui; isto é a rede de baixo, para
441
+ // a corrida (outra conta gravada entre o relatório e a escrita).
442
+ const taken = await Model.query().where('email', email).first();
443
+ if (taken && taken.id !== row.id)
444
+ return false;
445
+ row.email = email;
446
+ // `emailVerifiedAt` fica COMO ESTÁ: a caixa postal é a mesma, só a grafia
447
+ // gravada muda — não há prova nova para registrar.
448
+ await row.save();
449
+ return true;
450
+ },
435
451
  async confirmEmailChange(token) {
436
452
  if (!token || !token.startsWith(EMAIL_CHANGE_PREFIX))
437
453
  return { ok: false };
@@ -100,6 +100,28 @@ export function buildMfa(ctx) {
100
100
  await repo.upsert(accountId, { recoveryCodes: remaining });
101
101
  return true;
102
102
  },
103
+ async countRecoveryCodes(accountId) {
104
+ const state = await repo.read(accountId);
105
+ // Sem MFA ativo não há conjunto de códigos a contar — `null` diz "não se
106
+ // aplica", diferente de `0` ("acabaram, gere novos").
107
+ if (!state || !state.mfaEnabledAt)
108
+ return null;
109
+ return Array.isArray(state.recoveryCodes) ? state.recoveryCodes.length : 0;
110
+ },
111
+ async regenerateRecoveryCodes(accountId) {
112
+ const state = await repo.read(accountId);
113
+ // Só regenera sobre MFA JÁ ATIVO. Num enrollment pendente os códigos saem
114
+ // do `confirmTotpEnrollment`; deixar regenerar antes disso criaria um
115
+ // conjunto válido de credenciais de recuperação para um fator que o
116
+ // usuário ainda não provou possuir.
117
+ if (!state || !state.mfaEnabledAt)
118
+ return null;
119
+ const codes = Array.from({ length: recoveryCodeCount }, () => generateRecoveryCode());
120
+ // Substitui o conjunto inteiro: os antigos (inclusive os não usados)
121
+ // param de valer no mesmo instante — é esse o ponto de "regenerar".
122
+ await repo.upsert(accountId, { recoveryCodes: codes.map(sha256) });
123
+ return codes;
124
+ },
103
125
  async disableMfa(accountId) {
104
126
  // Limpa todo o estado de MFA. Inclui o anti-replay: um futuro re-enroll começa do zero.
105
127
  await repo.clear(accountId);
@@ -8,7 +8,7 @@
8
8
  * varre os `audit.record({ type: ... })` do `src/` e falha quando um evento
9
9
  * emitido não está aqui.
10
10
  */
11
- export declare const AUDIT_EVENT_TYPES: readonly ['login.success', 'login.failure', 'signup', 'password_reset.issued', 'password_reset.consumed', 'pat.issued', 'pat.revoked', 'pat.used', 'impersonation', 'impersonation.panel_viewed', 'impersonation.stopped', 'mfa.enabled', 'mfa.disabled', 'account.locked', 'passkey.registered', 'passkey.removed', 'email_verification.issued', 'email_verification.consumed', 'client.created', 'client.updated', 'client.deleted', 'session.revoked_all', 'password.changed', 'password.rehashed', 'email.change_requested', 'email.changed', 'login.new_ip_notified', 'login.new_device', 'login.otp_sent', 'login.otp_verified', 'login.otp_failed', 'login.otp_invalidated', 'bot_protection.rejected', 'grant.revoked_by_user', 'user.created', 'user.password_reset_sent', 'user.disabled', 'user.enabled', 'user.deleted', 'profile.updated', 'account.deleted', 'account.exported', 'keys.rotated', 'organization.created', 'organization.updated', 'organization.deleted', 'organization.member_added', 'organization.member_removed', 'organization.member_role_changed', 'organization.member_role_updated', 'organization.switched', 'organization.deactivated', 'organization.invitation_sent', 'organization.invitation_accepted', 'organization.invitation_revoked', 'email_change.requested', 'email_change.confirmed', 'email_change.cancelled', 'security_notice.sent', 'settings.updated', 'maintenance.enabled', 'maintenance.disabled', 'trusted_device.revoked', 'password.expired_change_forced', 'otp.locked', 'otp.unlocked', 'otp.unlock_failed', 'sudo.confirmed', 'session.single_enforced', 'account.expired_login_blocked', 'account.expiration_warned', 'login.magic_link_sent', 'session.revoked', 'account.signed_out_all', 'client.secret_regenerated', 'roles_catalog.updated'];
11
+ export declare const AUDIT_EVENT_TYPES: readonly ['login.success', 'login.failure', 'signup', 'password_reset.issued', 'password_reset.consumed', 'pat.issued', 'pat.revoked', 'pat.used', 'impersonation', 'impersonation.panel_viewed', 'impersonation.stopped', 'mfa.enabled', 'mfa.disabled', 'mfa.recovery_codes_regenerated', 'account.locked', 'passkey.registered', 'passkey.removed', 'email_verification.issued', 'email_verification.consumed', 'client.created', 'client.updated', 'client.deleted', 'session.revoked_all', 'password.changed', 'password.rehashed', 'email.change_requested', 'email.changed', 'login.new_ip_notified', 'login.new_device', 'login.otp_sent', 'login.otp_verified', 'login.otp_failed', 'login.otp_invalidated', 'bot_protection.rejected', 'grant.revoked_by_user', 'user.created', 'user.password_reset_sent', 'user.disabled', 'user.enabled', 'user.deleted', 'profile.updated', 'account.deleted', 'account.exported', 'keys.rotated', 'organization.created', 'organization.updated', 'organization.deleted', 'organization.member_added', 'organization.member_removed', 'organization.member_role_changed', 'organization.member_role_updated', 'organization.switched', 'organization.deactivated', 'organization.invitation_sent', 'organization.invitation_accepted', 'organization.invitation_revoked', 'email_change.requested', 'email_change.confirmed', 'email_change.cancelled', 'security_notice.sent', 'settings.updated', 'maintenance.enabled', 'maintenance.disabled', 'trusted_device.revoked', 'password.expired_change_forced', 'otp.locked', 'otp.unlocked', 'otp.unlock_failed', 'sudo.confirmed', 'session.single_enforced', 'account.expired_login_blocked', 'account.expiration_warned', 'login.magic_link_sent', 'session.revoked', 'account.signed_out_all', 'client.secret_regenerated', 'roles_catalog.updated'];
12
12
  /** Tipos de eventos de auditoria relevantes para segurança emitidos pelo IdP. */
13
13
  export type AuditEventType = (typeof AUDIT_EVENT_TYPES)[number];
14
14
  /**
@@ -29,6 +29,10 @@ export const AUDIT_EVENT_TYPES = [
29
29
  'impersonation.stopped',
30
30
  'mfa.enabled',
31
31
  'mfa.disabled',
32
+ // Os recovery codes foram TROCADOS: os antigos (inclusive os não usados)
33
+ // deixaram de valer no mesmo instante. Evento próprio, e não `mfa.enabled`,
34
+ // porque o fator não mudou — mudou o conjunto de credenciais de contorno.
35
+ 'mfa.recovery_codes_regenerated',
32
36
  'account.locked',
33
37
  'passkey.registered',
34
38
  'passkey.removed',
@@ -1,4 +1,5 @@
1
1
  import { supportsAccountImport } from '../accounts/account_store.js';
2
+ import { normalizeEmailIdentifier } from '../host/email_identifier.js';
2
3
  /**
3
4
  * Faz o parse do conteúdo do arquivo: aceita NDJSON (uma linha JSON por usuário)
4
5
  * OU um array JSON. Retorna os registros + os erros de parsing por linha. PURO
@@ -52,12 +53,16 @@ export async function importUsers(store, records, options = {}) {
52
53
  const report = { created: 0, skippedDuplicate: 0, errors: [] };
53
54
  const canImport = supportsAccountImport(store);
54
55
  for (const { line, record } of records) {
55
- const email = record.email?.trim();
56
+ // MESMA normalização do cadastro/login (`trim` + `toLowerCase`) — sem ela o
57
+ // import gravava `Davi@Acme.com` e o login (normalizado) não achava a conta.
58
+ const email = normalizeEmailIdentifier(record.email);
56
59
  if (!email) {
57
60
  report.errors.push({ line, reason: 'missing email' });
58
61
  continue;
59
62
  }
60
- // Duplicado: e-mail já existe → pula.
63
+ // Duplicado pela MESMA forma que o login busca — a normalizada. Uma base com
64
+ // endereços gravados em outra grafia pede `authkit:users:normalize-emails`
65
+ // ANTES do import, senão o duplicado passa despercebido.
61
66
  const existing = await store.findByEmail(email);
62
67
  if (existing) {
63
68
  report.skippedDuplicate++;
@@ -0,0 +1,78 @@
1
+ import type { AccountStore } from '../accounts/account_store.js';
2
+ /** Uma conta cujo endereço gravado difere da forma normalizada. */
3
+ export interface EmailNormalizationChange {
4
+ accountId: string;
5
+ /** Endereço como está gravado hoje. */
6
+ from: string;
7
+ /** Forma normalizada (`trim` + `toLowerCase`) — o que o login busca. */
8
+ to: string;
9
+ /** `true` quando o `--apply` gravou; `false` no relatório e quando a escrita falhou. */
10
+ applied: boolean;
11
+ /** Motivo da escrita não ter acontecido (só com `--apply`). */
12
+ error?: string;
13
+ }
14
+ /**
15
+ * Duas ou mais contas que colapsam no MESMO endereço normalizado — por exemplo
16
+ * `Davi@Acme.com` e `davi@acme.com`. A migração NÃO escolhe vencedor e NÃO funde
17
+ * contas: lista as envolvidas e não toca em nenhuma delas.
18
+ */
19
+ export interface EmailNormalizationCollision {
20
+ /** A forma normalizada em que as contas colidem. */
21
+ email: string;
22
+ /** As contas envolvidas, com o endereço que cada uma tem gravado hoje. */
23
+ accounts: {
24
+ accountId: string;
25
+ email: string;
26
+ }[];
27
+ }
28
+ export interface NormalizeEmailsReport {
29
+ /** Contas varridas. */
30
+ scanned: number;
31
+ /** Contas que já estão na forma normalizada (não são tocadas). */
32
+ alreadyNormalized: number;
33
+ /** As que mudariam (ou mudaram, com `--apply`), fora de colisão. */
34
+ changes: EmailNormalizationChange[];
35
+ /** Quantas foram efetivamente gravadas (0 sem `--apply`). */
36
+ applied: number;
37
+ /** Colisões encontradas — decisão humana. */
38
+ collisions: EmailNormalizationCollision[];
39
+ /** Contas que a colisão impediu de mexer (soma das contas de `collisions`). */
40
+ skippedByCollision: number;
41
+ /**
42
+ * Contas cujo endereço gravado NÃO normaliza para nada (coluna nula, vazia ou
43
+ * só espaços). Não entram em `alreadyNormalized`: gravar string vazia como
44
+ * identidade seria pior que deixar como está, e um operador que lê "já
45
+ * normalizada" não pode achar que estas linhas estão bem.
46
+ */
47
+ unusable: {
48
+ accountId: string;
49
+ email: string;
50
+ }[];
51
+ }
52
+ /**
53
+ * Migração dos endereços GRAVADOS para a forma normalizada da identidade
54
+ * (`normalizeEmailIdentifier`: `trim` + `toLowerCase`).
55
+ *
56
+ * POR QUE ELA EXISTE: até a v0.68 o cadastro gravava o endereço mutilado pelo
57
+ * `.normalizeEmail()` do VineJS (no gmail, sem pontos e sem `+tag`), e import,
58
+ * convite e provider social gravavam a grafia crua, com maiúsculas. O login
59
+ * busca UMA forma só — a normalizada. Uma conta gravada em qualquer outra grafia
60
+ * fica INALCANÇÁVEL, e, por o login ser à prova de enumeração, sem nenhuma
61
+ * mensagem de erro. Esta migração é o que reencontra essas contas.
62
+ *
63
+ * Em modo RELATÓRIO (default) não escreve nada: varre, calcula e devolve o que
64
+ * mudaria. Com `apply`, grava — RECUSANDO-SE a tocar em qualquer conta envolvida
65
+ * numa colisão (duas contas que colapsam no mesmo endereço). Nunca funde contas,
66
+ * nunca escolhe vencedor: as colididas saem listadas para decisão humana.
67
+ *
68
+ * A varredura acontece INTEIRA antes de qualquer escrita, de propósito: a
69
+ * listagem é ordenada por e-mail, e reescrever endereços no meio da paginação
70
+ * moveria linhas entre páginas — contas seriam puladas sem nenhum sinal.
71
+ *
72
+ * Lógica PURA quanto a CLI (recebe o store, devolve o relatório) — testável sem
73
+ * ace e sem banco.
74
+ */
75
+ export declare function normalizeAccountEmails(store: AccountStore, options?: {
76
+ apply?: boolean;
77
+ pageSize?: number;
78
+ }): Promise<NormalizeEmailsReport>;