@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.
- package/build/commands/commands.json +18 -0
- package/build/commands/normalize_emails.d.ts +21 -0
- package/build/commands/normalize_emails.js +106 -0
- package/build/index.d.ts +4 -3
- package/build/index.js +6 -2
- package/build/src/accounts/account_store.d.ts +65 -1
- package/build/src/accounts/account_store.js +24 -0
- package/build/src/accounts/lucid_store/core.d.ts +2 -2
- package/build/src/accounts/lucid_store/core.js +16 -0
- package/build/src/accounts/lucid_store/mfa.js +22 -0
- package/build/src/audit/audit_sink.d.ts +1 -1
- package/build/src/audit/audit_sink.js +4 -0
- package/build/src/commands/import_users.js +7 -2
- package/build/src/commands/normalize_emails.d.ts +78 -0
- package/build/src/commands/normalize_emails.js +129 -0
- package/build/src/host/account_api/account_api_controller.d.ts +2 -0
- package/build/src/host/account_api/account_api_controller.js +22 -3
- package/build/src/host/account_api/account_mfa_api_controller.d.ts +101 -0
- package/build/src/host/account_api/account_mfa_api_controller.js +286 -0
- package/build/src/host/account_api/account_orgs_api_controller.d.ts +126 -0
- package/build/src/host/account_api/account_orgs_api_controller.js +468 -0
- package/build/src/host/account_lockout.js +7 -2
- package/build/src/host/admin_api/admin_users_service.js +11 -3
- package/build/src/host/admin_api/dto.d.ts +1 -1
- package/build/src/host/admin_validators.d.ts +2 -2
- package/build/src/host/admin_validators.js +3 -2
- package/build/src/host/controllers/account_mfa_controller.js +1 -2
- package/build/src/host/controllers/account_orgs_controller.js +4 -18
- package/build/src/host/controllers/account_security_controller.js +3 -1
- package/build/src/host/controllers/account_session_controller.js +7 -5
- package/build/src/host/controllers/interaction_controller.d.ts +10 -0
- package/build/src/host/controllers/interaction_controller.js +46 -14
- package/build/src/host/controllers/registration_controller.js +15 -5
- package/build/src/host/controllers/social_controller.js +8 -1
- package/build/src/host/email_identifier.d.ts +29 -0
- package/build/src/host/email_identifier.js +31 -0
- package/build/src/host/org_policy.d.ts +26 -0
- package/build/src/host/org_policy.js +35 -0
- package/build/src/host/passkey_registration_challenge.d.ts +12 -0
- package/build/src/host/passkey_registration_challenge.js +12 -0
- package/build/src/host/register_auth_host.js +51 -0
- package/build/src/host/sudo_mode.d.ts +17 -0
- package/build/src/host/sudo_mode.js +31 -12
- package/build/src/host/ui-dist/assets/{index-D9CYQnZR.js → index-Dct63ai-.js} +2 -2
- package/build/src/host/ui-dist/index.html +1 -1
- package/build/src/host/validators.d.ts +5 -5
- package/build/src/host/validators.js +17 -5
- 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,
|
|
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
|
-
|
|
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
|
|
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>;
|