@adonis-agora/authkit-server 0.54.0 → 0.55.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 (63) hide show
  1. package/build/index.d.ts +4 -2
  2. package/build/index.js +2 -1
  3. package/build/providers/authkit_server_provider.js +18 -0
  4. package/build/src/accounts/lucid_account_store.d.ts +35 -0
  5. package/build/src/accounts/lucid_account_store.js +9 -0
  6. package/build/src/accounts/lucid_store/core.js +88 -19
  7. package/build/src/accounts/lucid_store/shared.d.ts +14 -0
  8. package/build/src/accounts/lucid_store/token_hash.d.ts +79 -0
  9. package/build/src/accounts/lucid_store/token_hash.js +145 -0
  10. package/build/src/define_config.d.ts +93 -8
  11. package/build/src/define_config.js +28 -2
  12. package/build/src/host/account_api/account_api_controller.js +5 -4
  13. package/build/src/host/admin_api/admin_users_service.js +2 -1
  14. package/build/src/host/admin_api/api_orgs_controller.js +2 -1
  15. package/build/src/host/admin_console/console_impersonation_controller.d.ts +15 -1
  16. package/build/src/host/admin_console/console_impersonation_controller.js +26 -2
  17. package/build/src/host/admin_console/console_orgs_controller.js +3 -1
  18. package/build/src/host/admin_validators.d.ts +3 -3
  19. package/build/src/host/auth_host_config.d.ts +30 -0
  20. package/build/src/host/auth_host_config.js +15 -0
  21. package/build/src/host/config_locks.d.ts +29 -0
  22. package/build/src/host/config_locks.js +46 -0
  23. package/build/src/host/console_session.d.ts +38 -2
  24. package/build/src/host/console_session.js +46 -2
  25. package/build/src/host/controllers/account_mfa_controller.js +5 -5
  26. package/build/src/host/controllers/account_orgs_controller.js +2 -1
  27. package/build/src/host/controllers/account_security_controller.js +6 -5
  28. package/build/src/host/controllers/interaction_controller.js +122 -26
  29. package/build/src/host/controllers/registration_controller.js +5 -4
  30. package/build/src/host/controllers/social_controller.js +37 -0
  31. package/build/src/host/default_mailer.d.ts +36 -5
  32. package/build/src/host/default_mailer.js +63 -10
  33. package/build/src/host/i18n.d.ts +10 -0
  34. package/build/src/host/i18n.js +16 -0
  35. package/build/src/host/login_attempt.d.ts +88 -2
  36. package/build/src/host/login_attempt.js +101 -48
  37. package/build/src/host/login_notify.js +2 -2
  38. package/build/src/host/oidc_rp_guard.d.ts +25 -2
  39. package/build/src/host/oidc_rp_guard.js +55 -9
  40. package/build/src/host/origin.d.ts +21 -0
  41. package/build/src/host/origin.js +22 -0
  42. package/build/src/host/register_auth_host.d.ts +104 -3
  43. package/build/src/host/register_auth_host.js +222 -26
  44. package/build/src/host/runtime_settings.d.ts +16 -0
  45. package/build/src/host/runtime_settings.js +24 -0
  46. package/build/src/host/runtime_toggles.d.ts +10 -0
  47. package/build/src/host/runtime_toggles.js +4 -0
  48. package/build/src/host/security_notice_service.d.ts +4 -2
  49. package/build/src/host/security_notice_service.js +4 -2
  50. package/build/src/host/sudo/index.d.ts +8 -0
  51. package/build/src/host/sudo/index.js +8 -0
  52. package/build/src/host/sudo/methods/magic_link.d.ts +17 -3
  53. package/build/src/host/sudo/methods/magic_link.js +37 -13
  54. package/build/src/host/sudo/runtime.d.ts +44 -4
  55. package/build/src/host/sudo/runtime.js +90 -6
  56. package/build/src/host/sudo/satisfiability.d.ts +62 -0
  57. package/build/src/host/sudo/satisfiability.js +89 -0
  58. package/build/src/password/common_passwords.js +27 -7
  59. package/build/src/provider/oidc_service.js +25 -13
  60. package/build/src/provider/token_exchange.d.ts +12 -1
  61. package/build/src/provider/token_exchange.js +12 -0
  62. package/package.json +6 -3
  63. /package/build/{password → src/password}/common_passwords.txt +0 -0
@@ -6,9 +6,11 @@ import type { AuditSink } from './audit/audit_sink.js';
6
6
  import { type EventsConfigInput } from './events/dispatcher.js';
7
7
  import { type BotProtectionConfigInput, type ResolvedBotProtectionConfig } from './host/bot_protection.js';
8
8
  import type { BrandingConfig } from './host/branding.js';
9
+ import { type PolicyRouteOption } from './host/config_locks.js';
9
10
  import type { ResolveGeo } from './host/geo.js';
10
11
  import { type AuthMessages, type I18nConfig } from './host/i18n.js';
11
12
  import { type OtpLoginConfigInput, type ResolvedOtpLoginConfig } from './host/otp_login.js';
13
+ import type { AuthHostOptions } from './host/register_auth_host.js';
12
14
  import type { SudoMethod } from './host/sudo/types.js';
13
15
  import { type ResolvedTrustedDevicesConfig, type TrustedDevicesConfigInput } from './host/trusted_device.js';
14
16
  import type { PatStore } from './pat/pat_store.js';
@@ -58,7 +60,13 @@ export interface MailHooks {
58
60
  /**
59
61
  * Envia o link de CONFIRMAÇÃO DE IDENTIDADE (sudo). Distinto de
60
62
  * `onMagicLink`: aquele autentica, este só concede sudo a quem já está
61
- * logado. Sem este hook, `sudoMethods.magicLink()` fica indisponível.
63
+ * logado.
64
+ *
65
+ * OPCIONAL, como todo hook de e-mail daqui: sem ele o próprio host-kit envia
66
+ * pelo mailer default (`@adonisjs/mail`), com branding e i18n. Antes a
67
+ * ausência deste hook tornava `sudoMethods.magicLink()` INDISPONÍVEL, e num
68
+ * host passwordless isso fechava o deadlock de sudo — o único método sem
69
+ * credencial prévia dependia de alguém ter escrito um hook.
62
70
  */
63
71
  onSudoLink?: (data: {
64
72
  email: string;
@@ -154,6 +162,20 @@ export interface MailHooks {
154
162
  address: string;
155
163
  name?: string;
156
164
  };
165
+ /**
166
+ * Escape hatch: origem (`scheme://host[:port]`) usada para montar os links
167
+ * EMAILADOS (reset de senha, magic link, desbloqueio OTP, convites de
168
+ * organização, verificação/troca de e-mail, avisos de segurança), no lugar
169
+ * do `issuer` resolvido.
170
+ *
171
+ * Por default (campo ausente) os links usam `new URL(issuer).origin` — NUNCA
172
+ * `request.host()` / `X-Forwarded-Host`, que são client-supplied e abrem
173
+ * "password-reset poisoning" (ver `host/origin.ts`). Use este campo apenas
174
+ * quando o mesmo issuer é servido legitimamente sob MÚLTIPLOS hostnames
175
+ * públicos e os links devem seguir um hostname fixo diferente do issuer —
176
+ * o comportamento normal (issuer único) não precisa disto.
177
+ */
178
+ origin?: string;
157
179
  }
158
180
  /** Bucket de rate-limit: pontos (requests) permitidos por janela de duração. */
159
181
  export interface RateLimitBucket {
@@ -529,6 +551,30 @@ export interface AdminConfigInput {
529
551
  enabled: boolean;
530
552
  /** Roles globais que dão acesso ao /admin. Default: ['ADMIN']. */
531
553
  roles?: string[];
554
+ /**
555
+ * Interruptor de impersonation. Governa as DUAS superfícies:
556
+ *
557
+ * 1. o grant RFC 8693 `urn:ietf:params:oauth:grant-type:token-exchange`
558
+ * registrado no provider OIDC (`src/provider/oidc_service.ts`) — é por
559
+ * ele que um admin troca o próprio access token pelo de outra conta;
560
+ * 2. o painel de impersonation do console admin
561
+ * (`GET {prefix}/api/impersonation/:userId`).
562
+ *
563
+ * `false` desliga as duas: o grant deixa de existir no provider (o token
564
+ * endpoint passa a responder `unsupported_grant_type`) e o painel responde
565
+ * 404. É um kill switch de verdade, não só um esconde-UI.
566
+ *
567
+ * Default: **`true`** — back-compat. O grant sempre foi registrado
568
+ * incondicionalmente e há hosts que o consomem a partir do PRÓPRIO /admin,
569
+ * sem montar o console desta lib; um default `false` os quebraria em runtime,
570
+ * em silêncio. Virar o default para `false` (secure-by-default) é decisão de
571
+ * major.
572
+ *
573
+ * Declarar esta chave também TRAVA a setting de runtime `admin_impersonation`
574
+ * (ver `src/host/config_locks.ts`): o arquivo passa a mandar e a UI/Admin API
575
+ * não altera mais o valor.
576
+ */
577
+ impersonation?: boolean;
532
578
  }
533
579
  export interface ResolvedAdminConfig {
534
580
  enabled: boolean;
@@ -681,6 +727,33 @@ export interface AuthServerConfigInput {
681
727
  patStore?: PatStore;
682
728
  /** Caminho base onde o host-kit monta as rotas OIDC. Default: '/oidc'. */
683
729
  mountPath?: string;
730
+ /**
731
+ * Montagem AUTOMÁTICA das rotas do host-kit, sem `registerAuthHost` no
732
+ * `start/routes.ts`.
733
+ *
734
+ * - ausente (default) → NÃO auto-monta. O app chama `registerAuthHost(router)`
735
+ * no `start/routes.ts`, como sempre (back-compat).
736
+ * - `true` → o provider monta as rotas no boot chamando `registerAuthHost` —
737
+ * literalmente a mesma função, não uma segunda implementação.
738
+ * - objeto → auto-monta usando estes valores como DEFAULTS estruturais.
739
+ * - `false` → não auto-monta, explicitamente (o kill switch documentado).
740
+ *
741
+ * ⚠️ Auto-montar E chamar `registerAuthHost` no `start/routes.ts` é duplo
742
+ * registro: a chamada manual LANÇA com a instrução de qual dos dois remover
743
+ * (duas rotas com o mesmo nome derrubam o boot do AdonisJS).
744
+ *
745
+ * ⚠️ A auto-montagem roda no `boot()` do provider, ANTES do `start/routes.ts`
746
+ * — as rotas do authkit ficam registradas primeiro, e os wildcards
747
+ * (`${mountPath}/*`, `${adminPrefix}/*`) casam antes de rotas do app com
748
+ * padrão sobreposto. Quem precisa da ordem inversa continua chamando
749
+ * `registerAuthHost` na posição que quiser.
750
+ *
751
+ * As chaves de POLÍTICA aqui dentro (`social`, `rateLimit`, `sudoMethods`,
752
+ * `admin`, `adminApi`) são redundantes: elas já vêm dos campos de topo do
753
+ * config e são travadas por eles. Use este objeto para o ESTRUTURAL —
754
+ * `accountRoutes`, `account`, `accountLoginUrl`, `mountPath`.
755
+ */
756
+ routes?: boolean | AuthHostOptions;
684
757
  /**
685
758
  * Destino default da área da conta: pós-login do console (sem `return_to`),
686
759
  * confirmações de e-mail e fallback de redirects. Default: '/account/security'.
@@ -721,16 +794,21 @@ export interface AuthServerConfigInput {
721
794
  * Métodos de confirmação de identidade (sudo mode). A ordem do array é a
722
795
  * ordem de exibição; o último método usado com sucesso é promovido ao topo.
723
796
  *
724
- * Ausente → a lista que `registerAuthHost` MONTOU (que por sua vez cai em
725
- * `[password(), passkey()]` sem `AuthHostOptions.sudoMethods`, o
726
- * comportamento histórico). É a mesma resposta que os handlers dão nesse
727
- * caso "vale o que tem rota" —, e é o que impede a tela de oferecer um
728
- * método sem endpoint ou de esconder um que funciona.
797
+ * Ausente → a lista que `registerAuthHost` MONTOU, DERIVADA contra este
798
+ * config: `[password, passkey]` num host com senha (o comportamento
799
+ * histórico) e `[passkey, magicLink]` num host que declarou
800
+ * `authMethods: { password: false }` que de outra forma não teria um único
801
+ * método satisfazível. É a mesma resposta que os handlers dão nesse caso —
802
+ * "vale o que tem rota, menos o que este host não consegue satisfazer" —, e é
803
+ * o que impede a tela de oferecer um método sem endpoint ou de esconder um que
804
+ * funciona. Ver `derivedSudoMethods` em `host/sudo/runtime.ts`.
729
805
  *
730
- * Host passwordless (autentica por OIDC/magic link) DEVE incluir ao menos um
806
+ * DECLARAR esta lista DESLIGA a derivação: ela SUBSTITUI os defaults e vale ao
807
+ * pé da letra. Um host passwordless que a declare PRECISA incluir ao menos um
731
808
  * método que não exija credencial previamente cadastrada — `oidcStepUp()` ou
732
809
  * `magicLink()` — senão o usuário fica sem caminho para exportar/excluir os
733
- * próprios dados.
810
+ * próprios dados, e é isso que o aviso de boot de
811
+ * `host/sudo/satisfiability.ts` denuncia.
734
812
  */
735
813
  sudo?: {
736
814
  methods?: SudoMethod[];
@@ -1000,6 +1078,13 @@ export interface ResolvedServerConfig {
1000
1078
  };
1001
1079
  /** Keys de `auth_settings` travadas por terem sido definidas no defineConfig. */
1002
1080
  lockedSettingKeys: string[];
1081
+ /** Auto-montagem das rotas do host-kit. Ver {@link AuthServerConfigInput.routes}. */
1082
+ routes?: boolean | AuthHostOptions;
1083
+ /**
1084
+ * Opções de POLÍTICA de `registerAuthHost` travadas por terem sido definidas
1085
+ * no defineConfig (config vence sobre o argumento). Ver `deriveLockedRouteOptions`.
1086
+ */
1087
+ lockedRouteOptions: PolicyRouteOption[];
1003
1088
  /** Integração opt-in com `@adonisjs/auth` (ausente = não integrado; comportamento de sempre). */
1004
1089
  adonisAuth?: {
1005
1090
  guard: string;
@@ -2,10 +2,11 @@ import { configProvider } from '@adonisjs/core';
2
2
  import { adapters } from './adapters/factory.js';
3
3
  import { composeAuditSink, resolveEvents } from './events/dispatcher.js';
4
4
  import { resolveBotProtection, } from './host/bot_protection.js';
5
- import { deriveLockedSettingKeys } from './host/config_locks.js';
5
+ import { deriveLockedRouteOptions, deriveLockedSettingKeys, } from './host/config_locks.js';
6
6
  import { resolveMessages } from './host/i18n.js';
7
7
  import { resolveOtpLoginConfig, } from './host/otp_login.js';
8
8
  import { edgeRenderer } from './host/renderers/edge_renderer.js';
9
+ import { warnUnsatisfiableSudoConfig } from './host/sudo/satisfiability.js';
9
10
  import { resolveTrustedDevices, } from './host/trusted_device.js';
10
11
  import { generateJwks } from './keys/jwks_manager.js';
11
12
  import { KeystoreCodec } from './keys/keystore_codec.js';
@@ -158,7 +159,9 @@ export function resolveAdmin(input) {
158
159
  return {
159
160
  enabled: input?.enabled ?? false,
160
161
  roles: input?.roles && input.roles.length > 0 ? input.roles : ['ADMIN'],
161
- impersonation: false,
162
+ // Default `true`: preserva o comportamento histórico (grant sempre
163
+ // registrado). Ver o docblock de `AdminConfigInput.impersonation`.
164
+ impersonation: input?.impersonation !== false,
162
165
  };
163
166
  }
164
167
  /** Lê API keys de `AUTHKIT_ADMIN_API_KEY` (uma ou várias, separadas por vírgula). */
@@ -294,6 +297,25 @@ export function defineConfig(config) {
294
297
  else {
295
298
  jwks = { keys: jwksConfig.keys ?? [] };
296
299
  }
300
+ // BACKSTOP DE SUDO. Um host cujo `sudo.methods` não tem um único método
301
+ // satisfazível por conta sem senha fica bricado para TODA operação sob
302
+ // `requireSudo` — e hoje isso só se descobre quando um usuário não consegue
303
+ // excluir a própria conta. Aqui é o único ponto do pacote onde `sudo.methods`,
304
+ // `authMethods` e `passwordless` estão os três resolvidos e juntos, e este
305
+ // callback roda no boot (`configProvider.resolve` no `boot()` do provider),
306
+ // antes de qualquer request. Ver `host/sudo/satisfiability.ts` para as quatro
307
+ // condições e para o porquê de ser WARN e não THROW.
308
+ //
309
+ // LIMITE CONHECIDO: só vê a lista que passou pelo CONFIG. Um host que declare
310
+ // a lista APENAS no argumento `registerAuthHost(router, { sudoMethods })` não
311
+ // é coberto — o registro de rotas acontece fora daqui e o `authMethods` não
312
+ // chega lá. É o caso do ator informado (quem escreveu a lista à mão), não o
313
+ // do host que herdou os defaults, que é quem o aviso existe para pegar.
314
+ warnUnsatisfiableSudoConfig({
315
+ methods: config.sudo?.methods,
316
+ passwordPinnedOff: config.authMethods?.password === false,
317
+ passwordlessSignup: config.passwordless?.signup === true,
318
+ });
297
319
  // #9: mfaIssuer efetivo — top-level do defineConfig vence; senão o do lucidAccountStore; senão default.
298
320
  const effectiveMfaIssuer = config.mfaIssuer ??
299
321
  config.accountStore?.__mfaIssuer ??
@@ -389,6 +411,10 @@ export function defineConfig(config) {
389
411
  // Keys de auth_settings travadas porque foram definidas no defineConfig:
390
412
  // config vence e a UI/Admin API não pode alterá-las (ver host/config_locks.ts).
391
413
  lockedSettingKeys: deriveLockedSettingKeys(config),
414
+ routes: config.routes,
415
+ // Mesma regra do `lockedSettingKeys`, outro eixo: config × argumento de
416
+ // `registerAuthHost` (ver host/config_locks.ts).
417
+ lockedRouteOptions: deriveLockedRouteOptions(config),
392
418
  // Opt-in: ausente = authkit nunca toca `ctx.auth` (comportamento de sempre).
393
419
  adonisAuth: config.adonisAuth,
394
420
  };
@@ -38,6 +38,7 @@ import { AvatarUploadError, isAvatarUploadSupported, storeAvatar } from '../avat
38
38
  import { sendEmailChangeConfirmationEmail, sendEmailChangeNoticeEmail } from '../default_mailer.js';
39
39
  import { translate } from '../i18n.js';
40
40
  import { ACCOUNT_SESSION_KEY } from '../middleware/account_auth.js';
41
+ import { authkitOrigin } from '../origin.js';
41
42
  import { resolveRuntimeSettings } from '../runtime_settings.js';
42
43
  import { resolveEffectiveEmailChange, resolveEffectivePasswordHistory, } from '../runtime_toggles.js';
43
44
  import { dispatchSecurityNotice } from '../security_notice_service.js';
@@ -331,7 +332,7 @@ export default class AccountApiController {
331
332
  kind: 'password_changed',
332
333
  ip: ctx.request.ip?.() ?? null,
333
334
  timestamp: new Date().toISOString(),
334
- }, cfg.mail, cfg.audit);
335
+ }, cfg.mail, cfg.audit, cfg);
335
336
  }
336
337
  return { ok: true };
337
338
  }
@@ -389,7 +390,7 @@ export default class AccountApiController {
389
390
  email: newEmail,
390
391
  ip: ctx.request.ip?.() ?? null,
391
392
  });
392
- const origin = `${ctx.request.protocol()}://${ctx.request.host()}`;
393
+ const origin = authkitOrigin(cfg);
393
394
  const confirmUrl = `${origin}${accountPath('emailConfirm')}?token=${encodeURIComponent(issued.token)}`;
394
395
  if (cfg.mail?.onEmailChangeConfirm) {
395
396
  await cfg.mail.onEmailChangeConfirm({
@@ -414,7 +415,7 @@ export default class AccountApiController {
414
415
  await cfg.mail.onEmailChangeNotice({ email: account.email, newEmail });
415
416
  }
416
417
  else {
417
- await sendEmailChangeNoticeEmail(ctx, { email: account.email, newEmail });
418
+ await sendEmailChangeNoticeEmail(ctx, cfg, { email: account.email, newEmail });
418
419
  }
419
420
  }
420
421
  return { ok: true, email: newEmail };
@@ -692,7 +693,7 @@ export default class AccountApiController {
692
693
  kind: 'passkey_removed',
693
694
  ip: ctx.request.ip?.() ?? null,
694
695
  timestamp: new Date().toISOString(),
695
- }, cfg.mail, cfg.audit);
696
+ }, cfg.mail, cfg.audit, cfg);
696
697
  }
697
698
  return { ok: true, removed: credentialId };
698
699
  }
@@ -3,6 +3,7 @@ import { supportsAccountDeletion, supportsAccountStatus, supportsCountByGlobalRo
3
3
  import { PasswordPolicyError } from '../../password/password_manager.js';
4
4
  import { AccountDeletionService } from '../account_deletion_service.js';
5
5
  import { sendPasswordResetEmail } from '../default_mailer.js';
6
+ import { authkitOrigin } from '../origin.js';
6
7
  import { resolveEffectiveRolesCatalog } from '../runtime_toggles.js';
7
8
  /**
8
9
  * Lógica de gestão de usuários compartilhada entre o console admin (B6, HTML) e a
@@ -292,7 +293,7 @@ export class AdminUsersService {
292
293
  const issued = await this.cfg.accountStore.issuePasswordResetToken(email);
293
294
  if (!issued)
294
295
  return;
295
- const origin = `${ctx.request.protocol()}://${ctx.request.host()}`;
296
+ const origin = authkitOrigin(this.cfg);
296
297
  const resetUrl = `${origin}/auth/reset-password?token=${encodeURIComponent(issued.token)}`;
297
298
  if (this.cfg.mail?.onPasswordReset) {
298
299
  await this.cfg.mail.onPasswordReset({
@@ -1,5 +1,6 @@
1
1
  import '../augmentations.js';
2
2
  import { orgAddMemberValidator, orgCreateValidator, orgInvitationValidator, orgMemberRoleValidator, orgUpdateValidator, } from '../admin_validators.js';
3
+ import { authkitOrigin } from '../origin.js';
3
4
  import { resolveRuntimeSettings } from '../runtime_settings.js';
4
5
  import { AdminOrgsService } from './admin_orgs_service.js';
5
6
  import { apiError, orgDetailDto, orgDto, orgInvitationDto } from './dto.js';
@@ -158,7 +159,7 @@ export default class ApiOrgsController {
158
159
  const svc = new AdminOrgsService(cfg);
159
160
  const orgId = ctx.request.param('id');
160
161
  const { email, role } = await ctx.request.validateUsing(orgInvitationValidator);
161
- const origin = `${ctx.request.protocol()}://${ctx.request.host()}`;
162
+ const origin = authkitOrigin(cfg);
162
163
  const result = await svc.createInvitation(orgId, { email, role: role ?? 'member' }, actor, origin, await resolveRuntimeSettings(ctx));
163
164
  if (!result.ok) {
164
165
  if (result.reason === 'not_supported')
@@ -7,7 +7,21 @@ import type { HttpContext } from '@adonisjs/core/http';
7
7
  *
8
8
  * Retorna os parâmetros RFC 8693 (token exchange) para o admin assumir a
9
9
  * identidade de um usuário-alvo. 404 quando impersonation está desabilitado na
10
- * config ou quando nenhum client tem o grant token-exchange habilitado.
10
+ * config, quando a setting de runtime `admin_impersonation` desliga o painel, ou
11
+ * quando nenhum client tem o grant token-exchange habilitado.
12
+ *
13
+ * DUAS CAMADAS, PROPOSITALMENTE ASSIMÉTRICAS:
14
+ *
15
+ * 1. `config.admin.impersonation` decide se a CAPACIDADE existe. É decisão de
16
+ * boot: é ela que registra (ou não) o grant RFC 8693 no provider OIDC. Uma
17
+ * setting de runtime não desregistra rota que nunca foi registrada.
18
+ * 2. a setting `admin_impersonation` decide se o CONSOLE OFERECE o painel.
19
+ * É decisão de runtime, mudável sem redeploy.
20
+ *
21
+ * A setting só APERTA, nunca AFROUXA — o gate de config é checado ANTES dela, e
22
+ * declarar `admin.impersonation` no `defineConfig` TRAVA a key (ver
23
+ * `host/config_locks.ts`), de forma que `getSetting` devolve null e o resolver
24
+ * cai no valor do config. Config > runtime, a mesma precedência do resto da lib.
11
25
  */
12
26
  export default class ConsoleImpersonationController {
13
27
  handle(ctx: HttpContext): Promise<void | {
@@ -2,6 +2,8 @@ import '../augmentations.js';
2
2
  import { apiError } from '../admin_api/dto.js';
3
3
  import { buildImpersonationPanel } from '../impersonation.js';
4
4
  import { ACCOUNT_SESSION_KEY } from '../middleware/account_auth.js';
5
+ import { resolveRuntimeSettingsOrNoop } from '../runtime_settings.js';
6
+ import { resolveEffectiveAdminImpersonation } from '../runtime_toggles.js';
5
7
  /**
6
8
  * Endpoint JSON do painel de impersonation do console admin React.
7
9
  *
@@ -9,16 +11,38 @@ import { ACCOUNT_SESSION_KEY } from '../middleware/account_auth.js';
9
11
  *
10
12
  * Retorna os parâmetros RFC 8693 (token exchange) para o admin assumir a
11
13
  * identidade de um usuário-alvo. 404 quando impersonation está desabilitado na
12
- * config ou quando nenhum client tem o grant token-exchange habilitado.
14
+ * config, quando a setting de runtime `admin_impersonation` desliga o painel, ou
15
+ * quando nenhum client tem o grant token-exchange habilitado.
16
+ *
17
+ * DUAS CAMADAS, PROPOSITALMENTE ASSIMÉTRICAS:
18
+ *
19
+ * 1. `config.admin.impersonation` decide se a CAPACIDADE existe. É decisão de
20
+ * boot: é ela que registra (ou não) o grant RFC 8693 no provider OIDC. Uma
21
+ * setting de runtime não desregistra rota que nunca foi registrada.
22
+ * 2. a setting `admin_impersonation` decide se o CONSOLE OFERECE o painel.
23
+ * É decisão de runtime, mudável sem redeploy.
24
+ *
25
+ * A setting só APERTA, nunca AFROUXA — o gate de config é checado ANTES dela, e
26
+ * declarar `admin.impersonation` no `defineConfig` TRAVA a key (ver
27
+ * `host/config_locks.ts`), de forma que `getSetting` devolve null e o resolver
28
+ * cai no valor do config. Config > runtime, a mesma precedência do resto da lib.
13
29
  */
14
30
  export default class ConsoleImpersonationController {
15
31
  async handle(ctx) {
16
32
  const service = await ctx.containerResolver.make('authkit.server');
17
33
  const cfg = service.config;
18
- // Impersonation precisa estar explicitamente habilitado no config.
34
+ // Camada 1 o kill switch de config. Sem ele o grant token-exchange nem
35
+ // existe no provider; devolver os parâmetros do painel seria mentir.
19
36
  if (!cfg.admin.impersonation) {
20
37
  return ctx.response.notFound(apiError('capability_unsupported', 'Impersonation não está habilitado nesta instalação.'));
21
38
  }
39
+ // Camada 2 — a política de runtime. Fail-safe: sem tabela `auth_settings`
40
+ // (ou com a key travada por config) o resolver cai no valor do config.
41
+ const runtimeSettings = await resolveRuntimeSettingsOrNoop(ctx);
42
+ const effective = await resolveEffectiveAdminImpersonation(runtimeSettings, cfg.admin.impersonation);
43
+ if (!effective.enabled) {
44
+ return ctx.response.notFound(apiError('capability_unsupported', 'O painel de impersonation está desligado pela setting de runtime `admin_impersonation`.'));
45
+ }
22
46
  const targetId = ctx.request.param('userId');
23
47
  const account = await cfg.accountStore.findById(targetId);
24
48
  if (!account) {
@@ -3,6 +3,7 @@ import { AdminOrgsService } from '../admin_api/admin_orgs_service.js';
3
3
  import { apiError, orgDetailDto, orgDto } from '../admin_api/dto.js';
4
4
  import { orgAddMemberValidator, orgCreateValidator, orgInvitationValidator, orgMemberRoleValidator, orgUpdateValidator, } from '../admin_validators.js';
5
5
  import { ACCOUNT_SESSION_KEY } from '../middleware/account_auth.js';
6
+ import { authkitOrigin } from '../origin.js';
6
7
  import { resolveRuntimeSettings } from '../runtime_settings.js';
7
8
  /**
8
9
  * Endpoints JSON de organizações do console admin React.
@@ -201,7 +202,8 @@ export default class ConsoleOrgsController {
201
202
  }
202
203
  const orgId = ctx.request.param('id');
203
204
  const { email, role } = await ctx.request.validateUsing(orgInvitationValidator);
204
- const origin = `${ctx.request.protocol()}://${ctx.request.host()}`;
205
+ const service = await ctx.containerResolver.make('authkit.server');
206
+ const origin = authkitOrigin(service.config);
205
207
  const result = await svc.createInvitation(orgId, { email, role: role ?? 'member' }, this.actor(ctx), origin, await this.settings(ctx));
206
208
  if ('ok' in result && result.ok === false) {
207
209
  if (result.reason === 'not_found') {
@@ -96,16 +96,16 @@ export declare const adminUserUpdateValidator: import("@vinejs/vine").VineValida
96
96
  avatarUrl: import("@vinejs/vine/schema/base/literal").OptionalModifier<import("@vinejs/vine/schema/base/literal").NullableModifier<import("@vinejs/vine").VineString>>;
97
97
  }, {
98
98
  name?: string | null | undefined;
99
- globalRoles?: string[] | null | undefined;
100
99
  avatarUrl?: string | null | undefined;
100
+ globalRoles?: string[] | null | undefined;
101
101
  }, {
102
102
  name?: string | null | undefined;
103
- globalRoles?: string[] | undefined;
104
103
  avatarUrl?: string | null | undefined;
104
+ globalRoles?: string[] | undefined;
105
105
  }, {
106
106
  name?: string | null | undefined;
107
- globalRoles?: string[] | undefined;
108
107
  avatarUrl?: string | null | undefined;
108
+ globalRoles?: string[] | undefined;
109
109
  }>, Record<string, any> | undefined>;
110
110
  /** Substituição de roles globais no console (PATCH /users/:id/roles). */
111
111
  export declare const adminUserRolesValidator: import("@vinejs/vine").VineValidator<import("@vinejs/vine").VineObject<{
@@ -1,4 +1,7 @@
1
1
  import type { AuthSocialConfig, ResolvedRateLimitConfig } from '../define_config.js';
2
+ import type { PolicyRouteOption } from './config_locks.js';
3
+ import type { AuthHostOptions } from './register_auth_host.js';
4
+ import type { SudoMethod } from './sudo/types.js';
2
5
  /**
3
6
  * Bits de routing do config resolvido que o `registerAuthHost` precisa em tempo de
4
7
  * REGISTRO de rota (síncrono). Stash feito no `boot()` do provider — que roda ANTES
@@ -13,10 +16,37 @@ export interface AuthHostRuntimeConfig {
13
16
  rateLimit: ResolvedRateLimitConfig;
14
17
  adminEnabled: boolean;
15
18
  adminApiEnabled: boolean;
19
+ /**
20
+ * `config.sudo.methods` — a lista que a TELA oferece e que os handlers
21
+ * ACEITAM. Presente aqui para que `registerAuthHost` MONTE exatamente ela,
22
+ * em vez de exigir que o host repita a lista no `start/routes.ts`.
23
+ */
24
+ sudoMethods?: SudoMethod[];
25
+ /**
26
+ * Defaults ESTRUTURAIS declarados em `config.routes` (prefixo do console de
27
+ * conta, telas montadas, destino de login). Argumentos de `registerAuthHost`
28
+ * continuam vencendo sobre estes — ver a regra de precedência no docblock de
29
+ * `registerAuthHost`.
30
+ */
31
+ routes?: AuthHostOptions;
32
+ /**
33
+ * Opções de POLÍTICA travadas por terem sido declaradas no `defineConfig`.
34
+ * Ver `deriveLockedRouteOptions`.
35
+ */
36
+ lockedRouteOptions?: PolicyRouteOption[];
16
37
  }
17
38
  /** Stash dos bits de routing (chamado no boot do provider). */
18
39
  export declare function setAuthHostConfig(config: AuthHostRuntimeConfig): void;
19
40
  /** Lê os bits de routing stashados; undefined se o boot ainda não rodou (fallback p/ opts/defaults). */
20
41
  export declare function getAuthHostConfig(): AuthHostRuntimeConfig | undefined;
42
+ /**
43
+ * Registra que o provider já montou as rotas automaticamente (`config.routes`).
44
+ * A partir daí uma chamada manual a `registerAuthHost` é um DUPLO REGISTRO —
45
+ * duas rotas com o mesmo nome derrubam o boot do AdonisJS —, então ela lança
46
+ * com a instrução de qual dos dois caminhos remover.
47
+ */
48
+ export declare function markAuthHostAutoMounted(): void;
49
+ /** O provider já auto-montou as rotas neste processo? */
50
+ export declare function wasAuthHostAutoMounted(): boolean;
21
51
  /** Limpa o stash — uso em testes. */
22
52
  export declare function resetAuthHostConfig(): void;
@@ -1,4 +1,5 @@
1
1
  let stashed;
2
+ let autoMounted = false;
2
3
  /** Stash dos bits de routing (chamado no boot do provider). */
3
4
  export function setAuthHostConfig(config) {
4
5
  stashed = config;
@@ -7,7 +8,21 @@ export function setAuthHostConfig(config) {
7
8
  export function getAuthHostConfig() {
8
9
  return stashed;
9
10
  }
11
+ /**
12
+ * Registra que o provider já montou as rotas automaticamente (`config.routes`).
13
+ * A partir daí uma chamada manual a `registerAuthHost` é um DUPLO REGISTRO —
14
+ * duas rotas com o mesmo nome derrubam o boot do AdonisJS —, então ela lança
15
+ * com a instrução de qual dos dois caminhos remover.
16
+ */
17
+ export function markAuthHostAutoMounted() {
18
+ autoMounted = true;
19
+ }
20
+ /** O provider já auto-montou as rotas neste processo? */
21
+ export function wasAuthHostAutoMounted() {
22
+ return autoMounted;
23
+ }
10
24
  /** Limpa o stash — uso em testes. */
11
25
  export function resetAuthHostConfig() {
12
26
  stashed = undefined;
27
+ autoMounted = false;
13
28
  }
@@ -22,6 +22,35 @@
22
22
  * travam (UI sempre controla).
23
23
  */
24
24
  export declare function deriveLockedSettingKeys(config: Record<string, any>): string[];
25
+ /**
26
+ * Opções de rota que são POLÍTICA (decidem o que é PERMITIDO), e não estrutura
27
+ * (onde as rotas moram). Ver `AuthHostOptions` e o docblock de
28
+ * `registerAuthHost`.
29
+ */
30
+ export declare const POLICY_ROUTE_OPTIONS: readonly ["social", "rateLimit", "sudoMethods", "admin", "adminApi"];
31
+ /** Uma opção de política de `AuthHostOptions`. */
32
+ export type PolicyRouteOption = (typeof POLICY_ROUTE_OPTIONS)[number];
33
+ /**
34
+ * Deriva as opções de rota TRAVADAS a partir dos campos EXPLICITAMENTE presentes
35
+ * no input do `defineConfig` — a MESMA regra de {@link deriveLockedSettingKeys}
36
+ * ("declarou no arquivo → o arquivo manda"), aplicada ao outro eixo: config vs.
37
+ * argumento de `registerAuthHost` (em vez de config vs. runtime setting).
38
+ *
39
+ * Travada significa: `registerAuthHost(router, { <key>: ... })` NÃO altera o
40
+ * comportamento; o valor do config vence e a divergência é reportada
41
+ * (`AuthHostRouteMap.overriddenByConfig` + `console.warn` no boot).
42
+ *
43
+ * O motivo de existir: sem isso, `start/routes.ts` pode AFROUXAR o que o config
44
+ * declarou — desligar o rate-limit, montar login social que o config não
45
+ * declara, trocar a lista de métodos de sudo — e o `config/authkit.ts` deixa de
46
+ * ser auditável (seria preciso ler o routes.ts de cada app para saber o que
47
+ * está valendo).
48
+ *
49
+ * Só trava o que o config DECLARA. Um campo ausente do config nunca trava: o
50
+ * argumento continua livre (back-compat com quem só configura pelo routes.ts) —
51
+ * exatamente como `authMethods` só trava os métodos que ele lista.
52
+ */
53
+ export declare function deriveLockedRouteOptions(config: Record<string, any>): PolicyRouteOption[];
25
54
  /** Erro lançado ao tentar gravar/remover uma setting travada via `defineConfig`. */
26
55
  export declare class SettingLockedError extends Error {
27
56
  readonly code = "E_SETTING_LOCKED";
@@ -43,6 +43,52 @@ export function deriveLockedSettingKeys(config) {
43
43
  add(config.ttl !== undefined, SETTING_KEYS.TOKEN_TTL);
44
44
  return locked;
45
45
  }
46
+ /**
47
+ * Opções de rota que são POLÍTICA (decidem o que é PERMITIDO), e não estrutura
48
+ * (onde as rotas moram). Ver `AuthHostOptions` e o docblock de
49
+ * `registerAuthHost`.
50
+ */
51
+ export const POLICY_ROUTE_OPTIONS = [
52
+ 'social',
53
+ 'rateLimit',
54
+ 'sudoMethods',
55
+ 'admin',
56
+ 'adminApi',
57
+ ];
58
+ /**
59
+ * Deriva as opções de rota TRAVADAS a partir dos campos EXPLICITAMENTE presentes
60
+ * no input do `defineConfig` — a MESMA regra de {@link deriveLockedSettingKeys}
61
+ * ("declarou no arquivo → o arquivo manda"), aplicada ao outro eixo: config vs.
62
+ * argumento de `registerAuthHost` (em vez de config vs. runtime setting).
63
+ *
64
+ * Travada significa: `registerAuthHost(router, { <key>: ... })` NÃO altera o
65
+ * comportamento; o valor do config vence e a divergência é reportada
66
+ * (`AuthHostRouteMap.overriddenByConfig` + `console.warn` no boot).
67
+ *
68
+ * O motivo de existir: sem isso, `start/routes.ts` pode AFROUXAR o que o config
69
+ * declarou — desligar o rate-limit, montar login social que o config não
70
+ * declara, trocar a lista de métodos de sudo — e o `config/authkit.ts` deixa de
71
+ * ser auditável (seria preciso ler o routes.ts de cada app para saber o que
72
+ * está valendo).
73
+ *
74
+ * Só trava o que o config DECLARA. Um campo ausente do config nunca trava: o
75
+ * argumento continua livre (back-compat com quem só configura pelo routes.ts) —
76
+ * exatamente como `authMethods` só trava os métodos que ele lista.
77
+ */
78
+ export function deriveLockedRouteOptions(config) {
79
+ const locked = [];
80
+ if (config.social !== undefined)
81
+ locked.push('social');
82
+ if (config.rateLimit !== undefined)
83
+ locked.push('rateLimit');
84
+ if (config.sudo?.methods !== undefined)
85
+ locked.push('sudoMethods');
86
+ if (config.admin !== undefined)
87
+ locked.push('admin');
88
+ if (config.adminApi !== undefined)
89
+ locked.push('adminApi');
90
+ return locked;
91
+ }
46
92
  /** Erro lançado ao tentar gravar/remover uma setting travada via `defineConfig`. */
47
93
  export class SettingLockedError extends Error {
48
94
  code = 'E_SETTING_LOCKED';
@@ -9,10 +9,46 @@ import type { HttpContext } from '@adonisjs/core/http';
9
9
  * sessão mudar.
10
10
  */
11
11
  /**
12
- * Retorna o id da conta logada no console (ou `null` quando não há
13
- * sessão).
12
+ * Retorna o id da conta que o request está representando no console (ou `null`
13
+ * quando não há sessão).
14
+ *
15
+ * ATENÇÃO: com impersonation ativa isto é a conta PERSONIFICADA, não o admin
16
+ * que a personificou. É o comportamento certo para "como qual conta este
17
+ * request está agindo?" e é a entrada ERRADA para uma checagem de
18
+ * permissão/role — para isso use `realAccountId`.
14
19
  */
15
20
  export declare function getAccountId(ctx: HttpContext): string | null;
21
+ /**
22
+ * Retorna o id do HUMANO real por trás do request — o id que uma decisão de
23
+ * autorização deve usar.
24
+ *
25
+ * ```
26
+ * impersonation ativa → o id do impersonator (o admin real)
27
+ * sem impersonation → o id da conta logada
28
+ * sem sessão → null
29
+ * ```
30
+ *
31
+ * USE ESTE em qualquer checagem de permissão/role ("esta pessoa é admin?",
32
+ * "esta pessoa pode aprovar isto?"). Use `getAccountId` só para responder
33
+ * "como qual conta este request está agindo?" (queries com escopo no alvo,
34
+ * UI, banner de impersonation).
35
+ *
36
+ * POR QUE ISSO EXISTE. Durante uma impersonation, `getAccountId` devolve a
37
+ * conta PERSONIFICADA — é o comportamento correto dele e o resto do app
38
+ * depende disso. Mas ele é, por isso mesmo, a entrada ERRADA para um role
39
+ * check: perguntar "o `getAccountId` é admin?" com impersonation ativa
40
+ * pergunta sobre o usuário personificado e, quando ele por acaso for admin,
41
+ * entrega os privilégios do admin a quem está sendo personificado — e, no
42
+ * sentido inverso, faz o admin real perder o próprio acesso. `realAccountId`
43
+ * ignora a troca de conta e responde sempre sobre quem está de fato dirigindo
44
+ * a sessão.
45
+ *
46
+ * @example
47
+ * // gate de /admin: a pessoa por trás do request é admin?
48
+ * const id = realAccountId(ctx)
49
+ * if (!id || !(await authz.hasRole(id, 'admin'))) throw new Error('forbidden')
50
+ */
51
+ export declare function realAccountId(ctx: HttpContext): string | null;
16
52
  /**
17
53
  * `true` quando o request carrega uma sessão de conta do console.
18
54
  */