@adonis-agora/authkit-server 0.68.4 → 0.70.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 (54) hide show
  1. package/build/commands/import_users.js +6 -1
  2. package/build/index.d.ts +5 -1
  3. package/build/index.js +7 -1
  4. package/build/src/accounts/account_store.d.ts +40 -0
  5. package/build/src/accounts/account_store.js +20 -0
  6. package/build/src/accounts/lucid_store/mfa.js +22 -0
  7. package/build/src/audit/audit_sink.d.ts +1 -1
  8. package/build/src/audit/audit_sink.js +4 -0
  9. package/build/src/commands/import_users.d.ts +7 -0
  10. package/build/src/commands/import_users.js +15 -3
  11. package/build/src/define_config.d.ts +81 -0
  12. package/build/src/define_config.js +18 -3
  13. package/build/src/host/account_api/account_api_controller.d.ts +2 -0
  14. package/build/src/host/account_api/account_api_controller.js +22 -3
  15. package/build/src/host/account_api/account_mfa_api_controller.d.ts +101 -0
  16. package/build/src/host/account_api/account_mfa_api_controller.js +286 -0
  17. package/build/src/host/account_api/account_orgs_api_controller.d.ts +126 -0
  18. package/build/src/host/account_api/account_orgs_api_controller.js +468 -0
  19. package/build/src/host/account_lockout.js +7 -2
  20. package/build/src/host/admin_api/admin_orgs_service.js +3 -2
  21. package/build/src/host/admin_api/admin_users_service.js +13 -3
  22. package/build/src/host/admin_api/dto.d.ts +1 -1
  23. package/build/src/host/admin_validators.d.ts +2 -2
  24. package/build/src/host/admin_validators.js +3 -2
  25. package/build/src/host/controllers/account_mfa_controller.js +1 -2
  26. package/build/src/host/controllers/account_orgs_controller.js +19 -11
  27. package/build/src/host/controllers/account_security_controller.js +3 -1
  28. package/build/src/host/controllers/account_session_controller.js +17 -6
  29. package/build/src/host/controllers/interaction_controller.d.ts +10 -0
  30. package/build/src/host/controllers/interaction_controller.js +93 -34
  31. package/build/src/host/controllers/registration_controller.js +35 -9
  32. package/build/src/host/controllers/social_controller.js +11 -2
  33. package/build/src/host/email_identifier.d.ts +92 -0
  34. package/build/src/host/email_identifier.js +234 -0
  35. package/build/src/host/idp_session_bridge.d.ts +55 -0
  36. package/build/src/host/idp_session_bridge.js +108 -0
  37. package/build/src/host/middleware/account_auth.js +3 -2
  38. package/build/src/host/org_policy.d.ts +26 -0
  39. package/build/src/host/org_policy.js +35 -0
  40. package/build/src/host/passkey_registration_challenge.d.ts +12 -0
  41. package/build/src/host/passkey_registration_challenge.js +12 -0
  42. package/build/src/host/register_auth_host.js +56 -1
  43. package/build/src/host/runtime_toggles.d.ts +2 -2
  44. package/build/src/host/runtime_toggles.js +3 -1
  45. package/build/src/host/sudo_mode.d.ts +17 -0
  46. package/build/src/host/sudo_mode.js +31 -12
  47. package/build/src/host/ui-dist/assets/{index-D9CYQnZR.js → index-Dct63ai-.js} +2 -2
  48. package/build/src/host/ui-dist/index.html +1 -1
  49. package/build/src/host/validators.d.ts +5 -5
  50. package/build/src/host/validators.js +17 -5
  51. package/build/src/provider/build_provider.js +46 -14
  52. package/build/src/provider/registration_policy.d.ts +99 -0
  53. package/build/src/provider/registration_policy.js +229 -0
  54. package/package.json +2 -2
@@ -53,7 +53,12 @@ export default class AuthkitImportUsers extends BaseCommand {
53
53
  return;
54
54
  }
55
55
  const { records, parseErrors } = parseImportFile(content);
56
- const report = await importUsers(store, records, { dryRun: this.dryRun });
56
+ const report = await importUsers(store, records, {
57
+ dryRun: this.dryRun,
58
+ // Ponte legada do config (default ligada) — a checagem de duplicado
59
+ // enxerga as contas gravadas com o endereço mutilado pelo cadastro antigo.
60
+ legacyFallback: authkitConfig?.login?.legacyEmailFallback ?? true,
61
+ });
57
62
  // Erros de parsing entram no relatório agregado.
58
63
  report.errors.unshift(...parseErrors);
59
64
  if (this.dryRun) {
package/build/index.d.ts CHANGED
@@ -64,10 +64,13 @@ 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 type { EmailIdentifierLookup, ResolvedEmailIdentifier, } from './src/host/email_identifier.js';
68
+ export { legacyNormalizeEmailIdentifier, normalizeEmailIdentifier, resolveEmailIdentifier, } from './src/host/email_identifier.js';
67
69
  export type { ResolveGeo } from './src/host/geo.js';
68
70
  export { GEO_RESOLVE_TIMEOUT_MS, resolveGeoSafe } from './src/host/geo.js';
69
71
  export type { AuthMessages, I18nConfig } from './src/host/i18n.js';
70
72
  export { BUILTIN_MESSAGES, DEFAULT_LOCALE, DEFAULT_MESSAGES, PT_BR_MESSAGES, resolveMessages, translate, } from './src/host/i18n.js';
73
+ export { ensureConsoleSession } from './src/host/idp_session_bridge.js';
71
74
  export type { ImpersonationClientLike, ImpersonationPanel } from './src/host/impersonation.js';
72
75
  export { buildImpersonationPanel } from './src/host/impersonation.js';
73
76
  export type { ImpersonationStartErrorCode, ImpersonationState, StartImpersonationParams, StopImpersonationOptions, TokenExchangeResult, } from './src/host/impersonation_session.js';
@@ -93,7 +96,7 @@ export { sudoMethods } from './src/host/sudo/index.js';
93
96
  export { completeSudo, fail as failSudo, LAST_METHOD_SESSION_KEY, sudoContextFrom, } from './src/host/sudo/runtime.js';
94
97
  export type { SudoContext, SudoMethod, SudoMethodDescriptor, SudoRouteHelpers, } from './src/host/sudo/types.js';
95
98
  export type { ResolvedSudoModeSetting, SudoModeSetting, } from './src/host/sudo_mode.js';
96
- export { isSudoActive, markSudo, requireSudo, resolveEffectiveSudoMode, SUDO_ACCOUNT_SESSION_KEY, SUDO_MODE_DEFAULTS, SUDO_SESSION_KEY, } from './src/host/sudo_mode.js';
99
+ export { isSudoActive, isSudoSatisfied, markSudo, requireSudo, resolveEffectiveSudoMode, SUDO_ACCOUNT_SESSION_KEY, SUDO_MODE_DEFAULTS, SUDO_SESSION_KEY, } from './src/host/sudo_mode.js';
97
100
  export { barChartSvg } from './src/host/svg_chart.js';
98
101
  export type { ResolvedTrustedDevicesConfig, TrustedDevicePayload, TrustedDevicesConfigInput, } from './src/host/trusted_device.js';
99
102
  export { buildTrustedDevicePayload, isTrustedDeviceValid, resolveTrustedDevices, TRUSTED_DEVICE_COOKIE, } from './src/host/trusted_device.js';
@@ -122,6 +125,7 @@ export { lucidPatStore } from './src/pat/lucid_pat_store.js';
122
125
  export type { IssuePatInput, PatRecord, PatStore } from './src/pat/pat_store.js';
123
126
  export { generatePatToken, hashPatToken } from './src/pat/pat_tokens.js';
124
127
  export { OidcService } from './src/provider/oidc_service.js';
128
+ export { checkClientRegistration, classifyRedirect, type RedirectUriPolicy, type RegistrationOperation, RegistrationPolicyError, type ResolvedRedirectUriPolicy, type ValidateRegistrationHook, } from './src/provider/registration_policy.js';
125
129
  export { registerOidcRoutes } from './src/register_routes.js';
126
130
  export type { EnsureSchemaOptions, EnsureSchemaReport } from './src/schema/ensure.js';
127
131
  export { ensureAuthkitSchema } from './src/schema/ensure.js';
package/build/index.js CHANGED
@@ -49,8 +49,10 @@ 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 { legacyNormalizeEmailIdentifier, normalizeEmailIdentifier, resolveEmailIdentifier, } 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';
55
+ export { ensureConsoleSession } from './src/host/idp_session_bridge.js';
54
56
  export { buildImpersonationPanel } from './src/host/impersonation.js';
55
57
  // Session impersonation — RP-side glue that routes through the IdP's RFC 8693
56
58
  // token-exchange (the IdP validates the admin role + audits). See
@@ -89,7 +91,10 @@ export { sudoMethods } from './src/host/sudo/index.js';
89
91
  // `returnTo` montado na mão é um open redirect esperando acontecer.
90
92
  export { completeSudo, fail as failSudo, LAST_METHOD_SESSION_KEY, sudoContextFrom, } from './src/host/sudo/runtime.js';
91
93
  // Sudo mode — helpers for host controllers that require step-up authentication.
92
- 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';
93
98
  export { barChartSvg } from './src/host/svg_chart.js';
94
99
  export { buildTrustedDevicePayload, isTrustedDeviceValid, resolveTrustedDevices, TRUSTED_DEVICE_COOKIE, } from './src/host/trusted_device.js';
95
100
  export { parseUserAgent } from './src/host/user_agent.js';
@@ -110,6 +115,7 @@ export { __setFetchForTests as __setPwnedFetchForTests, isPasswordPwned, } from
110
115
  export { lucidPatStore } from './src/pat/lucid_pat_store.js';
111
116
  export { generatePatToken, hashPatToken } from './src/pat/pat_tokens.js';
112
117
  export { OidcService } from './src/provider/oidc_service.js';
118
+ export { checkClientRegistration, classifyRedirect, RegistrationPolicyError, } from './src/provider/registration_policy.js';
113
119
  export { registerOidcRoutes } from './src/register_routes.js';
114
120
  export { ensureAuthkitSchema } from './src/schema/ensure.js';
115
121
  export { stubsRoot } from './stubs/main.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
@@ -654,6 +674,26 @@ export declare function supportsCountByGlobalRole(store: AccountStore): store is
654
674
  };
655
675
  /** Type guard: o store implementa a capacidade de passkeys / WebAuthn. */
656
676
  export declare function supportsPasskeys(store: AccountStore): store is AccountStore & WebauthnCapability;
677
+ /**
678
+ * Type guard: o store sabe REGENERAR recovery codes
679
+ * ({@link MfaCapability.regenerateRecoveryCodes}).
680
+ *
681
+ * Guard PRÓPRIO, separado de {@link supportsMfa}, porque o método é opcional
682
+ * dentro da capacidade: um store escrito antes desta versão implementa MFA
683
+ * inteiro e não implementa este método. Quem oferece o botão "gerar novos
684
+ * códigos" pergunta por aqui.
685
+ */
686
+ export declare function supportsRecoveryCodeRegeneration(store: AccountStore): store is AccountStore & MfaCapability & {
687
+ regenerateRecoveryCodes(accountId: string): Promise<string[] | null>;
688
+ };
689
+ /**
690
+ * Type guard: o store sabe CONTAR recovery codes restantes
691
+ * ({@link MfaCapability.countRecoveryCodes}). Mesmo motivo do
692
+ * {@link supportsRecoveryCodeRegeneration} para ser um guard separado.
693
+ */
694
+ export declare function supportsRecoveryCodeCount(store: AccountStore): store is AccountStore & MfaCapability & {
695
+ countRecoveryCodes(accountId: string): Promise<number | null>;
696
+ };
657
697
  /** Type guard: o store implementa account linking por identidade de provider. */
658
698
  export declare function supportsProviderIdentity(store: AccountStore): store is AccountStore & ProviderIdentityCapability;
659
699
  /** Type guard: o store implementa o self-service de segurança (senha/e-mail). */
@@ -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';
@@ -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',
@@ -42,10 +42,17 @@ export declare function parseImportFile(content: string): {
42
42
  *
43
43
  * Lógica PURA quanto a I/O de arquivo (recebe os registros já parseados) — fácil
44
44
  * de testar. Lança se o store não suporta import nem create.
45
+ *
46
+ * `legacyFallback` (default ligado, como no resto da lib) faz a checagem de
47
+ * duplicado enxergar também a conta que o cadastro antigo gravou com o endereço
48
+ * mutilado — importar a grafia REAL do dono dela pula como duplicado em vez de
49
+ * criar uma SEGUNDA conta. O comando não tem acesso ao config, então o flag
50
+ * chega por aqui.
45
51
  */
46
52
  export declare function importUsers(store: AccountStore, records: {
47
53
  line: number;
48
54
  record: ImportUserRecord;
49
55
  }[], options?: {
50
56
  dryRun?: boolean;
57
+ legacyFallback?: boolean;
51
58
  }): Promise<ImportReport>;
@@ -1,4 +1,5 @@
1
1
  import { supportsAccountImport } from '../accounts/account_store.js';
2
+ import { normalizeEmailIdentifier, resolveEmailIdentifier } 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
@@ -47,18 +48,29 @@ export function parseImportFile(content) {
47
48
  *
48
49
  * Lógica PURA quanto a I/O de arquivo (recebe os registros já parseados) — fácil
49
50
  * de testar. Lança se o store não suporta import nem create.
51
+ *
52
+ * `legacyFallback` (default ligado, como no resto da lib) faz a checagem de
53
+ * duplicado enxergar também a conta que o cadastro antigo gravou com o endereço
54
+ * mutilado — importar a grafia REAL do dono dela pula como duplicado em vez de
55
+ * criar uma SEGUNDA conta. O comando não tem acesso ao config, então o flag
56
+ * chega por aqui.
50
57
  */
51
58
  export async function importUsers(store, records, options = {}) {
52
59
  const report = { created: 0, skippedDuplicate: 0, errors: [] };
53
60
  const canImport = supportsAccountImport(store);
54
61
  for (const { line, record } of records) {
55
- const email = record.email?.trim();
62
+ // MESMA normalização do cadastro/login (`trim` + `toLowerCase`) — sem ela o
63
+ // import gravava `Davi@Acme.com` e o login (normalizado) não achava a conta.
64
+ const email = normalizeEmailIdentifier(record.email);
56
65
  if (!email) {
57
66
  report.errors.push({ line, reason: 'missing email' });
58
67
  continue;
59
68
  }
60
- // Duplicado: e-mail já existe → pula.
61
- const existing = await store.findByEmail(email);
69
+ // Duplicado: e-mail já existe → pula. Enxerga também a conta gravada com o
70
+ // endereço mutilado (ponte legada) — empate segue criando, como no cadastro.
71
+ const existing = (await resolveEmailIdentifier(store, record.email, {
72
+ legacyFallback: options.legacyFallback,
73
+ })).account;
62
74
  if (existing) {
63
75
  report.skippedDuplicate++;
64
76
  continue;
@@ -14,6 +14,7 @@ import type { AuthHostOptions } from './host/register_auth_host.js';
14
14
  import type { SudoMethod } from './host/sudo/types.js';
15
15
  import { type ResolvedTrustedDevicesConfig, type TrustedDevicesConfigInput } from './host/trusted_device.js';
16
16
  import type { PatStore } from './pat/pat_store.js';
17
+ import { type RedirectUriPolicy, type ResolvedRedirectUriPolicy, type ValidateRegistrationHook } from './provider/registration_policy.js';
17
18
  export type { AuthAccount };
18
19
  export { adapters };
19
20
  export type AuthHostRenderer = (ctx: HttpContext, view: string, props: Record<string, unknown>) => unknown;
@@ -305,11 +306,37 @@ export interface DynamicRegistrationConfigInput {
305
306
  * registrado via o `registration_access_token` devolvido no registro. Default: false.
306
307
  */
307
308
  management?: boolean;
309
+ /**
310
+ * Política de redirect URIs aplicada a TODO registro (`POST /reg`) e update
311
+ * (`PUT /reg/:id`) ANTES do oidc-provider. Com a política ativa, o client
312
+ * também fica restrito ao fluxo de código (`authorization_code` +
313
+ * `refresh_token`, `response_type=code`; PKCE já é obrigatório no IdP), e
314
+ * um client só-loopback/app instalado é registrado como `application_type: native`.
315
+ *
316
+ * Default:
317
+ * - registro ABERTO (sem `initialAccessToken`): `{ loopback: true }` — só
318
+ * `http://localhost|127.0.0.1|[::1]` em qualquer porta. Callbacks web de
319
+ * fornecedores (ex.: `https://claude.ai/api/mcp/auth_callback`) e esquemas
320
+ * de app (`cursor`, `vscode`) precisam ser listados em `exact`/`appSchemes`.
321
+ * - registro com `initialAccessToken`: sem política (quem tem o IAT é confiável).
322
+ *
323
+ * `false` desliga a política explicitamente (comportamento puro do oidc-provider).
324
+ */
325
+ redirectUriPolicy?: RedirectUriPolicy | false;
326
+ /**
327
+ * Gancho do host rodado depois da política de redirect: valida/ajusta o
328
+ * metadata do registro. Lance {@link RegistrationPolicyError} para recusar com
329
+ * `400`; retorne um objeto para substituir o metadata.
330
+ */
331
+ validateRegistration?: ValidateRegistrationHook;
308
332
  }
309
333
  export interface ResolvedDynamicRegistrationConfig {
310
334
  enabled: boolean;
311
335
  initialAccessToken?: string;
312
336
  management: boolean;
337
+ /** `null` = sem política de redirect (oidc-provider puro). */
338
+ redirectUriPolicy: ResolvedRedirectUriPolicy | null;
339
+ validateRegistration?: ValidateRegistrationHook;
313
340
  }
314
341
  /**
315
342
  * Resolve a config de registro dinâmico e VALIDA invariantes em tempo de resolução.
@@ -510,10 +537,26 @@ export interface LoginConfigInput {
510
537
  * idêntico ao de antes, e-mail idêntico). Ver `host/otp_login.ts`.
511
538
  */
512
539
  otp?: OtpLoginConfigInput;
540
+ /**
541
+ * PONTE TEMPORÁRIA: no passo de identificador, quando o e-mail normalizado
542
+ * (`trim` + `toLowerCase`) não acha conta nenhuma, tenta também o endereço
543
+ * exatamente como foi digitado e a normalização LEGADA (os defaults do
544
+ * validator.js que o cadastro aplicava até a v0.68 e que, no gmail, removiam
545
+ * pontos e sub-endereço `+tag`). Só aceita quando essas formas apontam para
546
+ * EXATAMENTE UMA conta — empate é tratado como "não achei".
547
+ *
548
+ * Existe para que as contas que nasceram com o endereço mutilado
549
+ * (`davi.carvalho96@gmail.com` gravado como `davicarvalho96@gmail.com`)
550
+ * continuem tendo caminho de volta. Default: `true` — desligar tranca essas
551
+ * contas do lado de fora. Depois de migrar os endereços gravados, desligue e,
552
+ * mais adiante, a ponte inteira sai da lib (ver `host/email_identifier.ts`).
553
+ */
554
+ legacyEmailFallback?: boolean;
513
555
  }
514
556
  export interface ResolvedLoginConfig {
515
557
  requireVerifiedEmail: boolean;
516
558
  otp: ResolvedOtpLoginConfig;
559
+ legacyEmailFallback: boolean;
517
560
  }
518
561
  export declare function resolveLogin(input?: LoginConfigInput): ResolvedLoginConfig;
519
562
  /**
@@ -635,6 +678,9 @@ export declare function resolveAdminApi(input?: AdminApiConfigInput): ResolvedAd
635
678
  * A role `'owner'` é reservada: uma org SEMPRE precisa de pelo menos um owner.
636
679
  * `allowSelfCreate`: se um usuário autenticado pode criar sua própria org (default false).
637
680
  * `invitationTtlHours`: TTL dos convites em horas (default 168 = 7 dias).
681
+ * Os três são o default estático da política; a setting `organizations_policy`
682
+ * os sobrescreve em runtime — exceto quando `organizations` está declarado no
683
+ * config, o que trava a setting e faz destes campos a política efetiva.
638
684
  * `claimStrategy: 'active'`: emite claims da org ATIVA da sessão (única estratégia implementada).
639
685
  */
640
686
  export interface OrganizationsConfigInput {
@@ -646,6 +692,21 @@ export interface OrganizationsConfigInput {
646
692
  * Default: 'active'.
647
693
  */
648
694
  claimStrategy?: 'active';
695
+ /**
696
+ * Catálogo de roles de org aceitas em convites/membros. `owner` é sempre
697
+ * garantido. Default: `['owner', 'admin', 'member']`.
698
+ *
699
+ * Declarar `organizations` TRAVA a setting `organizations_policy` (ver
700
+ * `config-locks`): com a chave travada, os campos de política daqui SÃO a
701
+ * política efetiva. Sem eles, a política trava no default da lib — era o
702
+ * que acontecia antes destes campos existirem (e.g. `allowSelfCreate` preso
703
+ * em `false`, sem jeito de ligar).
704
+ */
705
+ roles?: string[];
706
+ /** Usuário autenticado pode criar a própria org em `/account/orgs`. Default: false. */
707
+ allowSelfCreate?: boolean;
708
+ /** TTL dos convites em horas. Default: 168 (7 dias). */
709
+ invitationTtlHours?: number;
649
710
  }
650
711
  export interface ResolvedOrganizationsConfig {
651
712
  /** `undefined` = auto (decide em runtime pelo capability-probing do store). */
@@ -1061,6 +1122,22 @@ export interface AuthServerConfigInput {
1061
1122
  adonisAuth?: {
1062
1123
  guard: string;
1063
1124
  };
1125
+ /**
1126
+ * Sessão do console de conta (`/account/*`, e o `/admin/*`).
1127
+ *
1128
+ * `acceptIdpSession: true` — SSO: o console aceita a sessão ATIVA do IdP (o
1129
+ * login feito na interaction OIDC — senha, magic link, OTP, passkey, social)
1130
+ * em vez de pedir um segundo login. A sessão de console aberta assim fica
1131
+ * amarrada à sessão do IdP: termina quando ela termina (logout OIDC,
1132
+ * expiração), e o "Sair" do console encerra também a sessão do IdP. Contas
1133
+ * desabilitadas não entram. Operações sensíveis continuam pedindo sudo.
1134
+ *
1135
+ * Default `false`: o console só aceita a própria sessão (`POST /account/login`),
1136
+ * como sempre.
1137
+ */
1138
+ accountSession?: {
1139
+ acceptIdpSession?: boolean;
1140
+ };
1064
1141
  }
1065
1142
  export interface ResolvedServerConfig {
1066
1143
  issuer: string;
@@ -1204,6 +1281,10 @@ export interface ResolvedServerConfig {
1204
1281
  adonisAuth?: {
1205
1282
  guard: string;
1206
1283
  };
1284
+ /** Sessão do console. Ver {@link AuthServerConfigInput.accountSession}. */
1285
+ accountSession: {
1286
+ acceptIdpSession: boolean;
1287
+ };
1207
1288
  }
1208
1289
  export declare function toSeconds(value: string | number | undefined, fallback: number): number;
1209
1290
  /**
@@ -13,6 +13,7 @@ import { generateJwks } from './keys/jwks_manager.js';
13
13
  import { KeystoreCodec } from './keys/keystore_codec.js';
14
14
  import { loadEncryptionService } from './keys/keystore_crypto.js';
15
15
  import { KeystoreManager, resolveKeystoreVault } from './keys/keystore_manager.js';
16
+ import { OPEN_REGISTRATION_REDIRECT_POLICY, resolveRedirectUriPolicy, } from './provider/registration_policy.js';
16
17
  export { adapters };
17
18
  const RATE_LIMIT_DEFAULTS = {
18
19
  login: { points: 10, duration: '1 min' },
@@ -64,10 +65,20 @@ export function resolveDynamicRegistration(input) {
64
65
  'dynamicRegistration.enabled: true (RFC 7591). Habilite o registro dinâmico ' +
65
66
  'ou desligue o management.');
66
67
  }
68
+ const declared = input?.redirectUriPolicy;
69
+ const redirectUriPolicy = declared === false
70
+ ? null
71
+ : declared
72
+ ? resolveRedirectUriPolicy(declared)
73
+ : input?.initialAccessToken
74
+ ? null
75
+ : { ...OPEN_REGISTRATION_REDIRECT_POLICY };
67
76
  return {
68
77
  enabled,
69
78
  initialAccessToken: input?.initialAccessToken,
70
79
  management,
80
+ redirectUriPolicy,
81
+ validateRegistration: input?.validateRegistration,
71
82
  };
72
83
  }
73
84
  export function resolveDeviceFlow(input) {
@@ -126,6 +137,7 @@ export function resolveLogin(input) {
126
137
  return {
127
138
  requireVerifiedEmail: input?.requireVerifiedEmail ?? false,
128
139
  otp: resolveOtpLoginConfig(input?.otp),
140
+ legacyEmailFallback: input?.legacyEmailFallback ?? true,
129
141
  };
130
142
  }
131
143
  export function resolveRegistration(input) {
@@ -183,9 +195,11 @@ export function resolveAdminApi(input) {
183
195
  export function resolveOrganizations(input) {
184
196
  return {
185
197
  enabled: input?.enabled,
186
- roles: ['owner', 'admin', 'member'],
187
- allowSelfCreate: false,
188
- invitationTtlHours: 168,
198
+ roles: input?.roles && input.roles.length > 0 ? input.roles : ['owner', 'admin', 'member'],
199
+ allowSelfCreate: input?.allowSelfCreate ?? false,
200
+ invitationTtlHours: typeof input?.invitationTtlHours === 'number' && input.invitationTtlHours >= 1
201
+ ? Math.floor(input.invitationTtlHours)
202
+ : 168,
189
203
  claimStrategy: input?.claimStrategy ?? 'active',
190
204
  };
191
205
  }
@@ -433,6 +447,7 @@ export function defineConfig(config) {
433
447
  lockedRouteOptions: deriveLockedRouteOptions(config),
434
448
  // Opt-in: ausente = authkit nunca toca `ctx.auth` (comportamento de sempre).
435
449
  adonisAuth: config.adonisAuth,
450
+ accountSession: { acceptIdpSession: config.accountSession?.acceptIdpSession === true },
436
451
  };
437
452
  });
438
453
  }
@@ -170,6 +170,8 @@ export default class AccountApiController {
170
170
  };
171
171
  recovery: {
172
172
  available: any;
173
+ remaining: number | null;
174
+ regenerable: boolean;
173
175
  };
174
176
  }>;
175
177
  /**
@@ -28,7 +28,7 @@
28
28
  * GET /account/api/orgs/invitations → convites pendentes
29
29
  */
30
30
  import '../augmentations.js';
31
- import { supportsAccountSecurity, supportsLoginMethodsPreference, supportsMagicLink, supportsOrganizations, supportsPasskeys, supportsProfile, } from '../../accounts/account_store.js';
31
+ import { supportsAccountSecurity, supportsLoginMethodsPreference, supportsMagicLink, supportsOrganizations, supportsPasskeys, supportsProfile, supportsRecoveryCodeCount, supportsRecoveryCodeRegeneration, } from '../../accounts/account_store.js';
32
32
  import { PasswordPolicyError } from '../../password/password_manager.js';
33
33
  import { accountPath } from '../account_paths.js';
34
34
  import { ACCOUNT_SESSION_KEY } from '../account_session_key.js';
@@ -627,6 +627,16 @@ export default class AccountApiController {
627
627
  /* fail-safe */
628
628
  }
629
629
  }
630
+ // Quantos recovery codes restam (capability-probed, fail-safe).
631
+ let recoveryRemaining = null;
632
+ if (supportsRecoveryCodeCount(cfg.accountStore)) {
633
+ try {
634
+ recoveryRemaining = await cfg.accountStore.countRecoveryCodes(userId);
635
+ }
636
+ catch {
637
+ /* fail-safe */
638
+ }
639
+ }
630
640
  return {
631
641
  enabled,
632
642
  totp: { enrolled: enabled },
@@ -639,8 +649,17 @@ export default class AccountApiController {
639
649
  createdAt: p.createdAt ?? null,
640
650
  })),
641
651
  },
642
- // Recovery codes are shown once via the existing POST /account/mfa/confirm flow.
643
- recovery: { available: enabled },
652
+ recovery: {
653
+ // Os códigos em si NUNCA voltam aqui — só no instante em que são
654
+ // criados (`/mfa/totp/confirm` ou `/mfa/recovery-codes`), porque é só
655
+ // o hash deles que fica guardado.
656
+ available: enabled,
657
+ // `remaining` é `null` quando o store não sabe contar (capacidade
658
+ // opcional) — diferente de `0`, que significa "acabaram, gere novos".
659
+ remaining: recoveryRemaining,
660
+ // O host só mostra o botão "gerar novos códigos" se houver como.
661
+ regenerable: supportsRecoveryCodeRegeneration(cfg.accountStore),
662
+ },
644
663
  };
645
664
  }
646
665
  // ─── GET /account/api/login-methods ─────────────────────────────────────
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Account Self-Service JSON API — SEGUNDO FATOR (TOTP + passkeys)
3
+ *
4
+ * Espelho JSON do `AccountMfaController`, para o host que desenha a própria
5
+ * tela de "segundo fator" e não pode navegar o browser no meio do fluxo.
6
+ *
7
+ * Mapa de rotas (sob o `accountGuard`; mutantes sob o CSRF do shield do host):
8
+ * POST /account/api/mfa/totp/enroll → inicia o enrolamento (sudo)
9
+ * POST /account/api/mfa/totp/confirm → confirma com o código (throttled)
10
+ * POST /account/api/mfa/totp/disable → desliga o MFA (sudo)
11
+ * POST /account/api/mfa/recovery-codes → regenera os códigos (sudo)
12
+ * POST /account/api/mfa/passkeys/options → options da cerimônia de registro
13
+ * POST /account/api/mfa/passkeys/verify → verifica o attestation (sudo)
14
+ *
15
+ * Paridade de gates com o console HTML, endpoint a endpoint:
16
+ *
17
+ * | ação | console HTML | aqui |
18
+ * | ---------------- | ------------ | ---------------- |
19
+ * | enroll | sudo | sudo → 403 JSON |
20
+ * | confirm | sem sudo | sem sudo |
21
+ * | disable | sudo | sudo → 403 JSON |
22
+ * | passkey options | sem sudo | sem sudo |
23
+ * | passkey verify | sudo | sudo → 403 JSON |
24
+ * | recovery codes | (não existe) | sudo → 403 JSON |
25
+ *
26
+ * A única diferença é a FORMA da recusa: `requireSudo` devolve um redirect para
27
+ * `/account/confirm`, que numa SPA chega como uma página HTML onde ela esperava
28
+ * JSON. Aqui a recusa vira `403 { error: { code: 'sudo_required' } }` e a tela
29
+ * do host manda o usuário confirmar identidade por conta própria. Mesma
30
+ * política, resposta legível por máquina.
31
+ *
32
+ * `confirm` NÃO exige sudo — porque o `enroll` que criou o segredo pendente já
33
+ * exigiu, e pedir de novo no passo seguinte quebraria o enrolamento de quem
34
+ * demorou a digitar o código. É exatamente o que o console faz. O que o console
35
+ * não tem, e aqui existe, é o THROTTLE: o código de 6 dígitos é adivinhável, e
36
+ * a rota carrega o bucket de sudo (por IP) — mais apertado que o form, nunca
37
+ * mais frouxo.
38
+ *
39
+ * Os dois caminhos (clássico e JSON) COMPARTILHAM o slot do desafio WebAuthn
40
+ * (`PASSKEY_REG_CHALLENGE_KEY`), então um `options` pedido num deles pode ser
41
+ * finalizado pelo outro e nunca existem dois desafios vivos na mesma sessão.
42
+ */
43
+ import '../augmentations.js';
44
+ import type { HttpContext } from '@adonisjs/core/http';
45
+ export default class AccountMfaApiController {
46
+ #private;
47
+ /**
48
+ * Inicia o enrolamento TOTP: segredo pendente + `otpauth://` URI + o QR já
49
+ * renderizado como data-URL (o console renderiza server-side; aqui a tela do
50
+ * host recebe pronto e decide se mostra o QR, o segredo, ou os dois).
51
+ */
52
+ enrollTotp(ctx: HttpContext): Promise<void | {
53
+ secret: string;
54
+ otpauthUri: string;
55
+ qrDataUrl: string;
56
+ }>;
57
+ /**
58
+ * Confirma o enrolamento com o código do app autenticador. Sucesso ativa o
59
+ * MFA e devolve os recovery codes — UMA vez, como no console (lá eles vão num
60
+ * flash; aqui, no corpo da resposta, que é o equivalente headless).
61
+ */
62
+ confirmTotp(ctx: HttpContext): Promise<void | {
63
+ ok: boolean;
64
+ enabled: boolean;
65
+ recoveryCodes: string[];
66
+ }>;
67
+ /** Desliga o MFA (TOTP + recovery codes). Exige sudo, como o console. */
68
+ disableTotp(ctx: HttpContext): Promise<{
69
+ ok: boolean;
70
+ enabled: boolean;
71
+ } | undefined>;
72
+ /**
73
+ * Regenera os recovery codes de uma conta com MFA ATIVO e devolve os novos —
74
+ * uma única vez. Exige sudo: o resultado é um conjunto de credenciais que
75
+ * contorna o segundo fator, então vale o mesmo gate do `enroll`/`disable`.
76
+ *
77
+ * Capability-probed: stores que não implementam
78
+ * `regenerateRecoveryCodes` respondem 422 em vez de 500.
79
+ */
80
+ regenerateRecoveryCodes(ctx: HttpContext): Promise<void | {
81
+ ok: boolean;
82
+ recoveryCodes: string[];
83
+ }>;
84
+ /**
85
+ * Options da cerimônia de REGISTRO de passkey, guardando o desafio na sessão.
86
+ * Sem sudo, igual ao endpoint clássico: quem paga o gate é o `verify`, que é
87
+ * onde a credencial passa a existir.
88
+ */
89
+ passkeyRegisterOptions(ctx: HttpContext): Promise<any>;
90
+ /**
91
+ * Verifica o attestation contra o desafio da sessão e persiste a credencial.
92
+ *
93
+ * O endpoint clássico responde 302 numa navegação e `{ok:true}` num fetch;
94
+ * este responde SEMPRE JSON — inclusive na recusa de sudo, que lá é um
95
+ * redirect. É a diferença que faz a cerimônia caber numa SPA: o browser não
96
+ * pode navegar entre `startRegistration()` e a verificação.
97
+ */
98
+ passkeyRegisterVerify(ctx: HttpContext): Promise<void | {
99
+ ok: boolean;
100
+ }>;
101
+ }