@adonis-agora/authkit-server 0.53.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 +6 -2
  2. package/build/index.js +7 -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 +112 -0
  39. package/build/src/host/oidc_rp_guard.js +200 -0
  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
@@ -1,28 +1,43 @@
1
1
  import { resolveRateLimit } from '../define_config.js';
2
2
  import { accountHome } from './account_home.js';
3
3
  import { getAccountLoginUrl, setAccountLoginUrl } from './account_login_url.js';
4
- import { accountPath, joinAccountPath, setAccountPaths, } from './account_paths.js';
4
+ import { accountPath, accountPathsMap, accountPrefix, joinAccountPath, setAccountPaths, } from './account_paths.js';
5
5
  import { resolveAccountRoles } from './account_roles.js';
6
6
  import { adminApiGuard } from './admin_api/admin_api_guard.js';
7
7
  import { normalizeAdminApiPrefix, normalizeAdminPrefix, setAdminApiPrefix, setAdminPrefix, } from './admin_prefix.js';
8
- import { getAuthHostConfig } from './auth_host_config.js';
8
+ import { getAuthHostConfig, markAuthHostAutoMounted, wasAuthHostAutoMounted, } from './auth_host_config.js';
9
9
  import { ACCOUNT_SESSION_KEY } from './middleware/account_auth.js';
10
10
  import { createAuthThrottles } from './rate_limit.js';
11
11
  import { resolveRuntimeSettings } from './runtime_settings.js';
12
12
  import { resolveEffectiveSessionPolicy } from './runtime_toggles.js';
13
+ import { magicLink as sudoMagicLink } from './sudo/methods/magic_link.js';
13
14
  import { passkey as sudoPasskey } from './sudo/methods/passkey.js';
14
15
  import { password as sudoPassword } from './sudo/methods/password.js';
15
16
  import { completeSudo, fail, guardSudoRoutes, setMountedSudoMethods, sudoContextFrom, } from './sudo/runtime.js';
16
17
  /**
17
- * Métodos de sudo montados quando o host não passa `sudoMethods` —
18
- * comportamento histórico (senha + passkey).
18
+ * Métodos de sudo cujas ROTAS são montadas quando o host não passa
19
+ * `sudoMethods`.
19
20
  *
20
- * PONTO ÚNICO. A tela não tem mais uma cópia desta lista: sem
21
- * `config.sudo.methods`, `configuredSudoMethods` cai no que FOI MONTADO, ou
22
- * seja, no resultado do `??` abaixo. Duas listas de default é como os dois
23
- * lados divergiam.
21
+ * `magicLink()` entra aqui, e a lista deixou de ser o histórico
22
+ * `[password, passkey]`, porque os dois históricos exigem uma credencial
23
+ * PREVIAMENTE CADASTRADA. Num host passwordless não existe nenhuma: o usuário
24
+ * não tem senha, não tem passkey — e cadastrar passkey exige sudo. Deadlock
25
+ * fechado, e era o DEFAULT. O `oidcStepUp` não pode entrar (exige uma URL do
26
+ * host), então o magic link de sudo é o único método sem credencial prévia que
27
+ * a lib consegue montar sozinha.
28
+ *
29
+ * MONTAR NÃO É OFERECER. Esta lista decide apenas quais ENDPOINTS existem; o
30
+ * que a tela oferece e o que os handlers aceitam é derivado do config resolvido,
31
+ * em `sudo/runtime.ts` (`derivedSudoMethods`) — um host COM senha continua
32
+ * recebendo exatamente `[password, passkey]`, e o endpoint do magic link fica
33
+ * montado mas inerte. Não é possível decidir a oferta aqui: a montagem acontece
34
+ * antes de o config (lazy) resolver, e `authMethods` vive no config.
35
+ *
36
+ * PONTO ÚNICO. A tela não tem uma cópia desta lista: sem `config.sudo.methods`,
37
+ * `configuredSudoMethods` cai no que FOI MONTADO (derivado). Duas listas de
38
+ * default é como os dois lados divergiam.
24
39
  */
25
- const SUDO_METHOD_DEFAULTS = [sudoPassword(), sudoPasskey()];
40
+ const SUDO_METHOD_DEFAULTS = [sudoPassword(), sudoPasskey(), sudoMagicLink()];
26
41
  /** Chave da sessão Adonis que registra o timestamp da última atividade (idle timeout). */
27
42
  export const ACCOUNT_LAST_SEEN_KEY = 'authkit_last_seen';
28
43
  /**
@@ -190,37 +205,123 @@ const C = {
190
205
  /**
191
206
  * Monta todas as rotas do host-kit do Authorization Server numa chamada.
192
207
  * Substitui registerOidcRoutes + o hand-wiring do start/routes.ts do host.
208
+ *
209
+ * ── A REGRA DE PRECEDÊNCIA (config × argumento) ─────────────────────────────
210
+ *
211
+ * 1. **Argumento omitido HERDA do config.** `registerAuthHost(router)` é
212
+ * totalmente config-driven; `registerAuthHost(router, { mountPath: '/sso' })`
213
+ * troca o mountPath e herda TODO o resto. Omitir uma chave significa "usa o
214
+ * config", nunca "usa nada" — é o que dispensa repetir `sudo.methods` no
215
+ * `start/routes.ts`.
216
+ *
217
+ * 2. **Chaves ESTRUTURAIS: o argumento vence.** `mountPath`, os prefixos
218
+ * (`admin.prefix`, `adminApi.prefix`, `accountRoutes.prefix`), os segmentos
219
+ * de tela, `account` (quais telas montar) e `accountLoginUrl`. São decisões
220
+ * do ponto de chamada por natureza, e a forma de função é estritamente mais
221
+ * expressiva (dá para montar duas vezes sob dois prefixos — nenhum config
222
+ * expressa isso).
223
+ *
224
+ * 3. **Chaves de POLÍTICA: o config vence, e trava.** `social`, `rateLimit`,
225
+ * `sudoMethods` e o liga/desliga de `admin`/`adminApi` decidem o que é
226
+ * PERMITIDO. Quando o `defineConfig` as declara, o argumento NÃO as altera —
227
+ * senão o `config/authkit.ts` deixa de ser auditável e seria preciso ler o
228
+ * `start/routes.ts` de cada app para saber o que está valendo. Mesma regra
229
+ * (e mesma derivação) de `defineConfig({ authMethods })`, que já fixa os
230
+ * métodos de login contra o runtime. Ver `deriveLockedRouteOptions`.
231
+ * A divergência NÃO é silenciosa: sai um `console.warn` nomeando a chave e
232
+ * a chave aparece em `AuthHostRouteMap.overriddenByConfig`.
233
+ *
234
+ * Devolve o {@link AuthHostRouteMap} resolvido — entregue-o ao frontend em vez
235
+ * de hardcodar `href`s.
193
236
  */
194
237
  export function registerAuthHost(router, opts = {}) {
195
238
  // Config resolvido (stashado no boot do provider) — fonte única; `opts` só faz
196
- // OVERRIDE. Elimina o drift: o consumidor pode chamar `registerAuthHost(router)`
197
- // e mountPath/social/rateLimit/admin/adminApi vêm do config/authkit.ts.
239
+ // OVERRIDE das chaves ESTRUTURAIS. Elimina o drift: o consumidor pode chamar
240
+ // `registerAuthHost(router)` e tudo vem do config/authkit.ts.
198
241
  // Fallback p/ defaults quando o stash não está disponível (ex.: testes sem boot).
199
242
  const hostCfg = getAuthHostConfig();
200
- const mount = opts.mountPath ?? hostCfg?.mountPath ?? '/oidc';
201
- // social: opt explícito > config. admin/adminApi: opt explícito > (config.enabled monta).
202
- const social = opts.social ?? hostCfg?.social;
203
- const adminOpt = opts.admin ?? (hostCfg?.adminEnabled ? true : undefined);
204
- const adminApiOpt = opts.adminApi ?? (hostCfg?.adminApiEnabled ? true : undefined);
243
+ // `config.routes` montou tudo no boot do provider. Registrar de novo
244
+ // duplicaria CADA rota e duas rotas com o mesmo nome derrubam o boot do
245
+ // AdonisJS com um `RuntimeException` que não diz de onde veio. Falha aqui,
246
+ // alto e com a saída explícita.
247
+ if (wasAuthHostAutoMounted()) {
248
+ throw new Error('authkit: registerAuthHost() foi chamado depois de `routes` no config/authkit.ts já ter montado as rotas automaticamente — o duplo registro derruba o boot ("route name already exists"). Escolha UM dos dois: remova a chamada do start/routes.ts, ou ponha `routes: false` no defineConfig e configure tudo pela chamada.');
249
+ }
250
+ // Defaults ESTRUTURAIS declarados em `config.routes` — o argumento ainda vence.
251
+ const routesCfg = hostCfg?.routes;
252
+ // Chaves de POLÍTICA travadas pelo defineConfig (ver o item 3 acima).
253
+ const locked = new Set(hostCfg?.lockedRouteOptions ?? []);
254
+ const overriddenByConfig = [];
255
+ /**
256
+ * Uma chave de política travada IGNORA o argumento e reporta. Não silencia:
257
+ * um argumento sem efeito que ninguém percebe é como o drift começa.
258
+ */
259
+ const isLocked = (key, passed) => {
260
+ if (!locked.has(key))
261
+ return false;
262
+ if (passed) {
263
+ overriddenByConfig.push(key);
264
+ console.warn(`authkit: registerAuthHost(router, { ${key} }) foi IGNORADO — "${key}" é uma opção de política e está definida no defineConfig(), que vence (o config precisa ser auditável sem ler o start/routes.ts). Remova-a da chamada, ou remova-a do config para controlá-la aqui.`);
265
+ }
266
+ return true;
267
+ };
268
+ const mount = opts.mountPath ?? routesCfg?.mountPath ?? hostCfg?.mountPath ?? '/oidc';
269
+ // social — POLÍTICA: decide se existe um caminho de autenticação a mais.
270
+ const social = isLocked('social', opts.social !== undefined)
271
+ ? hostCfg?.social
272
+ : (opts.social ?? routesCfg?.social ?? hostCfg?.social);
273
+ // admin/adminApi — o LIGA/DESLIGA é política (config vence); o PREFIXO é
274
+ // estrutural (o argumento vence). Por isso os dois eixos são resolvidos
275
+ // separadamente em vez de a opção inteira ser um bloco só.
276
+ const enableFrom = (v) => v === undefined ? undefined : v !== false;
277
+ const prefixFrom = (...vs) => {
278
+ for (const v of vs)
279
+ if (typeof v === 'object' && v?.prefix)
280
+ return v.prefix;
281
+ return undefined;
282
+ };
283
+ // `{ prefix: '/painel' }` mexe SÓ no eixo estrutural: só há divergência a
284
+ // reportar quando a intenção de liga/desliga do argumento CONTRADIZ o config.
285
+ const contradicts = (arg, configEnabled) => {
286
+ const intent = enableFrom(arg);
287
+ return intent !== undefined && intent !== (configEnabled ?? false);
288
+ };
289
+ const adminEnabled = isLocked('admin', contradicts(opts.admin, hostCfg?.adminEnabled))
290
+ ? (hostCfg?.adminEnabled ?? false)
291
+ : (enableFrom(opts.admin) ?? enableFrom(routesCfg?.admin) ?? hostCfg?.adminEnabled ?? false);
292
+ const adminPrefixOpt = prefixFrom(opts.admin, routesCfg?.admin);
293
+ const adminOpt = adminEnabled ? { prefix: adminPrefixOpt } : undefined;
294
+ const adminApiEnabled = isLocked('adminApi', contradicts(opts.adminApi, hostCfg?.adminApiEnabled))
295
+ ? (hostCfg?.adminApiEnabled ?? false)
296
+ : (enableFrom(opts.adminApi) ??
297
+ enableFrom(routesCfg?.adminApi) ??
298
+ hostCfg?.adminApiEnabled ??
299
+ false);
300
+ const adminApiPrefixOpt = prefixFrom(opts.adminApi, routesCfg?.adminApi);
301
+ const adminApiOpt = adminApiEnabled
302
+ ? { prefix: adminApiPrefixOpt }
303
+ : undefined;
205
304
  // Prefixo/segmentos configuráveis do console de conta — persiste no singleton
206
305
  // de processo ANTES de qualquer construção de rota/closure, para que o registro
207
306
  // de rotas, os guards, os controllers, os e-mails e as views leiam o mesmo
208
307
  // valor. Top-level de propósito (vale mesmo com `account: false`): as rotas de
209
308
  // sudo e a JSON API respeitam o prefixo. Ausente → default `/account/*`
210
309
  // (back-compat). Ver `account_paths.ts`.
211
- if (opts.accountRoutes !== undefined) {
212
- setAccountPaths(opts.accountRoutes);
310
+ const accountRoutesOpt = opts.accountRoutes ?? routesCfg?.accountRoutes;
311
+ if (accountRoutesOpt !== undefined) {
312
+ setAccountPaths(accountRoutesOpt);
213
313
  }
214
314
  // Destino do redirect de "faça login" — persiste no singleton de processo para
215
315
  // que os guards (closures de tempo de registro), o middleware, os controllers e
216
316
  // as views leiam o mesmo valor. Só quando a opção foi passada (senão deriva de
217
317
  // `accountPath('login')` — back-compat).
218
- if (opts.accountLoginUrl !== undefined) {
219
- setAccountLoginUrl(opts.accountLoginUrl);
318
+ const accountLoginUrlOpt = opts.accountLoginUrl ?? routesCfg?.accountLoginUrl;
319
+ if (accountLoginUrlOpt !== undefined) {
320
+ setAccountLoginUrl(accountLoginUrlOpt);
220
321
  }
221
322
  // Montagem por tela do console de conta. `undefined` → tudo montado;
222
323
  // `false` → nada; objeto → cada flag ausente default `true`.
223
- const accountOpt = opts.account;
324
+ const accountOpt = opts.account ?? routesCfg?.account;
224
325
  const mountScreen = (key) => {
225
326
  if (accountOpt === false)
226
327
  return false;
@@ -234,10 +335,21 @@ export function registerAuthHost(router, opts = {}) {
234
335
  const mountSecurity = mountScreen('security');
235
336
  const mountMfa = mountScreen('mfa');
236
337
  const mountApps = mountScreen('apps');
338
+ // Ids dos métodos de sudo efetivamente montados — preenchido dentro do grupo
339
+ // do console de conta (a callback do `.group()` roda de forma síncrona) e
340
+ // devolvido no `AuthHostRouteMap`.
341
+ let mountedSudoIds = [];
237
342
  // Throttles opt-in (anti-brute-force). `undefined` quando rate-limit desligado.
238
- const resolvedRateLimit = opts.rateLimit !== undefined
239
- ? resolveRateLimit(opts.rateLimit)
240
- : (hostCfg?.rateLimit ?? resolveRateLimit(undefined));
343
+ // POLÍTICA: `rateLimit: { enabled: false }` num routes.ts desligaria a proteção
344
+ // anti-brute-force que o config ligou — e o config.rateLimit JÁ trava a setting
345
+ // de runtime homônima (`deriveLockedSettingKeys`). Travar aqui só fecha o
346
+ // terceiro caminho para o mesmo valor.
347
+ const rateLimitOpt = opts.rateLimit ?? routesCfg?.rateLimit;
348
+ const resolvedRateLimit = isLocked('rateLimit', opts.rateLimit !== undefined)
349
+ ? (hostCfg?.rateLimit ?? resolveRateLimit(undefined))
350
+ : rateLimitOpt !== undefined
351
+ ? resolveRateLimit(rateLimitOpt)
352
+ : (hostCfg?.rateLimit ?? resolveRateLimit(undefined));
241
353
  const throttles = createAuthThrottles(resolvedRateLimit);
242
354
  // Helpers: aplicam o middleware de throttle quando presente; senão no-op.
243
355
  const withLogin = (route) => {
@@ -413,7 +525,16 @@ export function registerAuthHost(router, opts = {}) {
413
525
  completeSudo,
414
526
  fail,
415
527
  };
416
- const sudoMethodsToMount = opts?.sudoMethods ?? SUDO_METHOD_DEFAULTS;
528
+ // POLÍTICA. `config.sudo.methods` decide o que a tela OFERECE e o que
529
+ // os handlers ACEITAM (`isSudoMethodEnabled`); agora decide também o que é
530
+ // MONTADO. Era a única das três decisões que ficava fora do config, e era
531
+ // exatamente por isso que o host tinha de manter a lista em DOIS lugares —
532
+ // divergiram, a tela oferecia um método sem endpoint (404) e escondia um
533
+ // que funcionava.
534
+ const sudoMethodsDeclared = isLocked('sudoMethods', opts.sudoMethods !== undefined)
535
+ ? hostCfg?.sudoMethods
536
+ : (opts.sudoMethods ?? routesCfg?.sudoMethods ?? hostCfg?.sudoMethods);
537
+ const sudoMethodsToMount = sudoMethodsDeclared ?? SUDO_METHOD_DEFAULTS;
417
538
  for (const method of sudoMethodsToMount) {
418
539
  // `guardSudoRoutes` embrulha os handlers que o método registrar, para
419
540
  // que `config.sudo.methods` os desabilite de fato mesmo que o método
@@ -434,7 +555,16 @@ export function registerAuthHost(router, opts = {}) {
434
555
  // A lista montada é a fonte de verdade dos DOIS lados quando o host não
435
556
  // configura `config.sudo.methods`: a tela oferece exatamente isto, e os
436
557
  // handlers aceitam exatamente isto.
437
- setMountedSudoMethods(sudoMethodsToMount);
558
+ //
559
+ // O segundo argumento diz se a lista é a da LIB ou a do HOST, e a
560
+ // distinção é o que mantém o conserto do deadlock inofensivo: só a lista
561
+ // de defaults é derivada do config (`derivedSudoMethods`). Uma lista que o
562
+ // host escreveu — no argumento ou no `config.sudo.methods` — vale ao pé da
563
+ // letra, em qualquer direção, porque foi ele quem a escreveu.
564
+ setMountedSudoMethods(sudoMethodsToMount, {
565
+ fromDefaults: sudoMethodsDeclared === undefined,
566
+ });
567
+ mountedSudoIds = sudoMethodsToMount.map((m) => m.id);
438
568
  // Organizations (multi-tenancy) — tela `orgs`. Montadas por default;
439
569
  // controller retorna 404/403 sem tabelas (capability-probed).
440
570
  if (mountOrgs) {
@@ -489,11 +619,16 @@ export function registerAuthHost(router, opts = {}) {
489
619
  router.get(`${apiBase}/orgs/:id`, [C.accountApi, 'showOrg']);
490
620
  })
491
621
  .use([accountGuard]);
622
+ // Prefixos resolvidos dos consoles — `null` quando o grupo não foi montado.
623
+ // Compõem o `AuthHostRouteMap` devolvido no fim.
624
+ let resolvedAdminPrefix = null;
625
+ let resolvedAdminApiPrefix = null;
492
626
  // Console admin (do config.admin.enabled ou opts). Protegido pelo adminGuard (sessão + role global).
493
627
  if (adminOpt) {
494
628
  // Resolve o prefixo: `true` → '/admin' (default); objeto → usa prefix fornecido.
495
629
  const rawPrefix = typeof adminOpt === 'object' && adminOpt.prefix ? adminOpt.prefix : '/admin';
496
630
  const ap = normalizeAdminPrefix(rawPrefix);
631
+ resolvedAdminPrefix = ap;
497
632
  // Persiste no singleton de processo para que controllers e views usem o mesmo prefixo.
498
633
  setAdminPrefix(ap);
499
634
  router
@@ -586,6 +721,7 @@ export function registerAuthHost(router, opts = {}) {
586
721
  ? adminApiOpt.prefix
587
722
  : '/api/authkit/v1';
588
723
  const aap = normalizeAdminApiPrefix(rawApiPrefix);
724
+ resolvedAdminApiPrefix = aap;
589
725
  // Persiste no singleton de processo para que o SDK remoto e outros consumidores
590
726
  // usem o mesmo prefixo sem precisar receber a opção.
591
727
  setAdminApiPrefix(aap);
@@ -648,4 +784,64 @@ export function registerAuthHost(router, opts = {}) {
648
784
  // O `withApiThrottle` (introspection, por token) continua como camada adicional.
649
785
  .use(throttles ? [throttles.adminIp, adminApiGuard] : [adminApiGuard]);
650
786
  }
787
+ // ─── Mapa resolvido (R4) ───────────────────────────────────────────────────
788
+ // Lido DEPOIS de todo o registro: `accountPath()`/`accountPrefix()` já
789
+ // refletem o `accountRoutes` aplicado no topo, e os prefixos de console já
790
+ // foram normalizados. É este objeto que o host entrega ao frontend em vez de
791
+ // hardcodar `href`s — o que faltava para um override de path ser um recurso
792
+ // inteiro, e não só metade (certo no servidor, errado na UI).
793
+ return {
794
+ mountPath: mount,
795
+ account: {
796
+ prefix: accountPrefix(),
797
+ api: apiBase,
798
+ paths: accountPathsMap(),
799
+ loginUrl: getAccountLoginUrl(),
800
+ screens: {
801
+ login: mountLogin,
802
+ tokens: mountTokens,
803
+ orgs: mountOrgs,
804
+ security: mountSecurity,
805
+ mfa: mountMfa,
806
+ apps: mountApps,
807
+ },
808
+ },
809
+ admin: resolvedAdminPrefix ? { prefix: resolvedAdminPrefix } : null,
810
+ adminApi: resolvedAdminApiPrefix ? { prefix: resolvedAdminApiPrefix } : null,
811
+ names: {
812
+ webauthnAsset: 'authkit.assets.webauthn',
813
+ oidcWildcard: 'authkit.oidc.wildcard',
814
+ oidcRoot: 'authkit.oidc.root',
815
+ ...(resolvedAdminPrefix
816
+ ? {
817
+ consoleAssets: 'authkit_console_assets',
818
+ consoleRoot: 'authkit_console_root',
819
+ consoleShell: 'authkit_console_shell',
820
+ }
821
+ : {}),
822
+ },
823
+ sudoMethods: mountedSudoIds,
824
+ overriddenByConfig,
825
+ };
826
+ }
827
+ /**
828
+ * Auto-montagem a partir de `config.routes` (R1). Chamada UMA vez pelo
829
+ * `boot()` do provider.
830
+ *
831
+ * O CAMINHO AUTOMÁTICO É O CAMINHO MANUAL: chama literalmente
832
+ * `registerAuthHost`, sem nenhuma segunda implementação. Toda a resolução
833
+ * (herança do config, precedência política × estrutural, mapa de retorno) é a
834
+ * mesma — duas implementações de um comportamento sempre divergem.
835
+ *
836
+ * Não recebe opções: os defaults ESTRUTURAIS vêm de `config.routes` pelo stash
837
+ * (`AuthHostRuntimeConfig.routes`), pelo mesmo caminho que uma chamada manual
838
+ * os leria.
839
+ *
840
+ * @internal
841
+ */
842
+ export function autoMountAuthHost(router) {
843
+ const map = registerAuthHost(router);
844
+ // Depois, nunca antes: `registerAuthHost` LANÇA quando já houve auto-mount.
845
+ markAuthHostAutoMounted();
846
+ return map;
651
847
  }
@@ -179,3 +179,19 @@ export declare class RuntimeSettings implements SettingsCapability {
179
179
  * ```
180
180
  */
181
181
  export declare function resolveRuntimeSettings(ctx: HttpContext): Promise<RuntimeSettings | null>;
182
+ /**
183
+ * Variante NON-NULL de {@link resolveRuntimeSettings}: quando a resolução falha
184
+ * (DB ausente, serviço não registrado), devolve um {@link RuntimeSettings} no-op
185
+ * cujo probe de tabela lança — logo `hasTable()` vira false e TODA leitura
186
+ * retorna null, fazendo os resolvers caírem no config estático.
187
+ *
188
+ * Use esta quando o chamador precisa passar um `SettingsCapability` non-null
189
+ * adiante (é o caso dos gates de login, que só rodam as checagens de expiração
190
+ * quando recebem `settings`). Use {@link resolveRuntimeSettings} quando o `null`
191
+ * carrega semântica própria (ex.: responder `capability_unsupported`).
192
+ *
193
+ * Vive AQUI, e não em um controller, porque é sobre runtime settings — o
194
+ * `interaction_controller` e o `social_controller` a compartilham; uma segunda
195
+ * cópia da degradação no-op é como as duas divergem.
196
+ */
197
+ export declare function resolveRuntimeSettingsOrNoop(ctx: HttpContext): Promise<RuntimeSettings>;
@@ -305,3 +305,27 @@ export async function resolveRuntimeSettings(ctx) {
305
305
  return null;
306
306
  }
307
307
  }
308
+ /**
309
+ * Variante NON-NULL de {@link resolveRuntimeSettings}: quando a resolução falha
310
+ * (DB ausente, serviço não registrado), devolve um {@link RuntimeSettings} no-op
311
+ * cujo probe de tabela lança — logo `hasTable()` vira false e TODA leitura
312
+ * retorna null, fazendo os resolvers caírem no config estático.
313
+ *
314
+ * Use esta quando o chamador precisa passar um `SettingsCapability` non-null
315
+ * adiante (é o caso dos gates de login, que só rodam as checagens de expiração
316
+ * quando recebem `settings`). Use {@link resolveRuntimeSettings} quando o `null`
317
+ * carrega semântica própria (ex.: responder `capability_unsupported`).
318
+ *
319
+ * Vive AQUI, e não em um controller, porque é sobre runtime settings — o
320
+ * `interaction_controller` e o `social_controller` a compartilham; uma segunda
321
+ * cópia da degradação no-op é como as duas divergem.
322
+ */
323
+ export async function resolveRuntimeSettingsOrNoop(ctx) {
324
+ const rs = await resolveRuntimeSettings(ctx);
325
+ return (rs ??
326
+ new RuntimeSettings({
327
+ table: () => {
328
+ throw new Error('no-op');
329
+ },
330
+ }));
331
+ }
@@ -659,6 +659,12 @@ export declare function resolveEffectiveTokenTtl(settings: SettingsCapability, c
659
659
  *
660
660
  * Controla se o painel de impersonation (RFC 8693 token exchange) é exibido no
661
661
  * console admin. FALLBACK: campo ausente cai em `config.admin.impersonation`.
662
+ *
663
+ * ESCOPO — governa o PAINEL, não o GRANT. Registrar o grant RFC 8693 no provider
664
+ * OIDC é decisão de boot de `config.admin.impersonation`; uma setting de runtime
665
+ * não desregistra rota que nunca foi registrada. Consumida por
666
+ * `host/admin_console/console_impersonation_controller.ts`, que checa o gate de
667
+ * config ANTES desta setting — logo o runtime só APERTA, nunca AFROUXA.
662
668
  */
663
669
  export interface AdminImpersonationSetting {
664
670
  enabled?: boolean;
@@ -672,6 +678,10 @@ export interface ResolvedAdminImpersonationSetting {
672
678
  *
673
679
  * Precedência: setting BD → configDefault → lib default (false).
674
680
  * FAIL-SAFE: qualquer erro → configDefault.
681
+ *
682
+ * Quando `admin.impersonation` foi declarado no `defineConfig`, a key fica
683
+ * travada e `settings.getSetting` devolve null (ver `host/config_locks.ts`) —
684
+ * então este resolver cai no `configDefault` sozinho. Config > runtime.
675
685
  */
676
686
  export declare function resolveEffectiveAdminImpersonation(settings: SettingsCapability, configDefault?: boolean): Promise<ResolvedAdminImpersonationSetting>;
677
687
  /**
@@ -633,6 +633,10 @@ export async function resolveEffectiveTokenTtl(settings, configDefault = {}) {
633
633
  *
634
634
  * Precedência: setting BD → configDefault → lib default (false).
635
635
  * FAIL-SAFE: qualquer erro → configDefault.
636
+ *
637
+ * Quando `admin.impersonation` foi declarado no `defineConfig`, a key fica
638
+ * travada e `settings.getSetting` devolve null (ver `host/config_locks.ts`) —
639
+ * então este resolver cai no `configDefault` sozinho. Config > runtime.
636
640
  */
637
641
  export async function resolveEffectiveAdminImpersonation(settings, configDefault = false) {
638
642
  const defaults = { enabled: configDefault };
@@ -8,7 +8,7 @@
8
8
  */
9
9
  import type { HttpContext } from '@adonisjs/core/http';
10
10
  import type { AuditSink } from '../audit/audit_sink.js';
11
- import type { MailHooks } from '../define_config.js';
11
+ import type { MailHooks, ResolvedServerConfig } from '../define_config.js';
12
12
  import type { SecurityNotificationKind } from './runtime_toggles.js';
13
13
  /**
14
14
  * Contexto para disparo de uma notificação de segurança.
@@ -32,5 +32,7 @@ export interface SecurityNoticeContext {
32
32
  * @param notice Contexto da notificação
33
33
  * @param mailHooks Hooks de mail do config (opcional)
34
34
  * @param audit Sink de auditoria (opcional)
35
+ * @param cfg Config resolvido — só usado para derivar o origin do link
36
+ * do e-mail default (`authkitOrigin`); nunca do `request.host()`.
35
37
  */
36
- export declare function dispatchSecurityNotice(ctx: HttpContext, notice: SecurityNoticeContext, mailHooks: Pick<MailHooks, 'onSecurityNotice'> | undefined, audit: AuditSink | undefined): Promise<void>;
38
+ export declare function dispatchSecurityNotice(ctx: HttpContext, notice: SecurityNoticeContext, mailHooks: Pick<MailHooks, 'onSecurityNotice'> | undefined, audit: AuditSink | undefined, cfg: ResolvedServerConfig): Promise<void>;
@@ -17,8 +17,10 @@ import { resolveEffectiveSecurityNotifications } from './runtime_toggles.js';
17
17
  * @param notice Contexto da notificação
18
18
  * @param mailHooks Hooks de mail do config (opcional)
19
19
  * @param audit Sink de auditoria (opcional)
20
+ * @param cfg Config resolvido — só usado para derivar o origin do link
21
+ * do e-mail default (`authkitOrigin`); nunca do `request.host()`.
20
22
  */
21
- export async function dispatchSecurityNotice(ctx, notice, mailHooks, audit) {
23
+ export async function dispatchSecurityNotice(ctx, notice, mailHooks, audit, cfg) {
22
24
  try {
23
25
  // Resolve settings em runtime (fail-safe: sem tabela → defaults habilitados).
24
26
  let enabled = true;
@@ -59,7 +61,7 @@ export async function dispatchSecurityNotice(ctx, notice, mailHooks, audit) {
59
61
  await mailHooks.onSecurityNotice(noticeData);
60
62
  }
61
63
  else {
62
- await sendSecurityNoticeEmail(ctx, {
64
+ await sendSecurityNoticeEmail(ctx, cfg, {
63
65
  email: notice.account.email,
64
66
  kind: notice.kind,
65
67
  timestamp,
@@ -19,6 +19,14 @@ import { password } from './methods/password.js';
19
19
  * SEM `config.sudo.methods` não há como divergir: a tela cai na própria lista
20
20
  * montada por `registerAuthHost`, a mesma que os handlers aceitam.
21
21
  *
22
+ * E é essa mesma separação que define o que é DEFAULT. `registerAuthHost` monta
23
+ * `[password, passkey, magicLink]` sem conhecer o config; o config resolvido
24
+ * decide quais deles valem (`derivedSudoMethods`, em `runtime.ts`): host com
25
+ * senha fica com `[password, passkey]` (o histórico), host que declarou
26
+ * `authMethods: { password: false }` fica com `[passkey, magicLink]`. A regra é
27
+ * UMA função, consultada pelos dois lados — montar não é oferecer, e continuar
28
+ * não havendo como divergir.
29
+ *
22
30
  * ```ts
23
31
  * defineConfig({
24
32
  * sudo: {
@@ -19,6 +19,14 @@ import { password } from './methods/password.js';
19
19
  * SEM `config.sudo.methods` não há como divergir: a tela cai na própria lista
20
20
  * montada por `registerAuthHost`, a mesma que os handlers aceitam.
21
21
  *
22
+ * E é essa mesma separação que define o que é DEFAULT. `registerAuthHost` monta
23
+ * `[password, passkey, magicLink]` sem conhecer o config; o config resolvido
24
+ * decide quais deles valem (`derivedSudoMethods`, em `runtime.ts`): host com
25
+ * senha fica com `[password, passkey]` (o histórico), host que declarou
26
+ * `authMethods: { password: false }` fica com `[passkey, magicLink]`. A regra é
27
+ * UMA função, consultada pelos dois lados — montar não é oferecer, e continuar
28
+ * não havendo como divergir.
29
+ *
22
30
  * ```ts
23
31
  * defineConfig({
24
32
  * sudo: {
@@ -36,8 +36,22 @@ export declare function verifySudoLinkToken(c: SudoContext, token: string): bool
36
36
  /**
37
37
  * Confirmação por link enviado ao e-mail da conta.
38
38
  *
39
- * Depende do hook `mail.onSudoLink`, DISTINTO de `mail.onMagicLink` justamente
40
- * para que o host não confunda um link que autentica com um que só concede
41
- * sudo a quem já está logado. Sem o hook, o método fica indisponível.
39
+ * O hook `mail.onSudoLink` é DISTINTO de `mail.onMagicLink` justamente para que
40
+ * o host não confunda um link que autentica com um que só concede sudo a quem já
41
+ * está logado e continua tendo PRIORIDADE quando declarado.
42
+ *
43
+ * SEM O HOOK O MÉTODO SEGUE DISPONÍVEL: o próprio host-kit envia o e-mail pelo
44
+ * mailer default (`sendSudoLinkEmail`), exatamente como já faz com reset de
45
+ * senha, verificação de e-mail e o magic link de login. Antes o método se
46
+ * declarava indisponível sem hook, e num host passwordless isso FECHAVA o
47
+ * deadlock — sem senha, sem passkey e sem hook não sobrava nenhum método
48
+ * satisfazível na tela `/account/confirm`, e o usuário ficava trancado fora de
49
+ * exportar/excluir os próprios dados, do MFA, dos PATs e da troca de e-mail,
50
+ * inclusive fora do cadastro de passkey que destravaria o resto. Exigir um hook
51
+ * escrito à mão para que o único método sem credencial prévia funcione é
52
+ * transformar uma saída de emergência em opt-in.
53
+ *
54
+ * A única exigência que resta é a que não tem substituto: a conta precisa ter
55
+ * e-mail.
42
56
  */
43
57
  export declare function magicLink(): SudoMethod;
@@ -1,5 +1,6 @@
1
1
  import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';
2
2
  import { accountPath } from '../../account_paths.js';
3
+ import { sendSudoLinkEmail } from '../../default_mailer.js';
3
4
  import { translate } from '../../i18n.js';
4
5
  import { isSudoMethodEnabled } from '../runtime.js';
5
6
  /** Token de sudo pendente, guardado na sessão que o pediu. */
@@ -90,17 +91,29 @@ function requestOrigin(ctx) {
90
91
  /**
91
92
  * Confirmação por link enviado ao e-mail da conta.
92
93
  *
93
- * Depende do hook `mail.onSudoLink`, DISTINTO de `mail.onMagicLink` justamente
94
- * para que o host não confunda um link que autentica com um que só concede
95
- * sudo a quem já está logado. Sem o hook, o método fica indisponível.
94
+ * O hook `mail.onSudoLink` é DISTINTO de `mail.onMagicLink` justamente para que
95
+ * o host não confunda um link que autentica com um que só concede sudo a quem já
96
+ * está logado e continua tendo PRIORIDADE quando declarado.
97
+ *
98
+ * SEM O HOOK O MÉTODO SEGUE DISPONÍVEL: o próprio host-kit envia o e-mail pelo
99
+ * mailer default (`sendSudoLinkEmail`), exatamente como já faz com reset de
100
+ * senha, verificação de e-mail e o magic link de login. Antes o método se
101
+ * declarava indisponível sem hook, e num host passwordless isso FECHAVA o
102
+ * deadlock — sem senha, sem passkey e sem hook não sobrava nenhum método
103
+ * satisfazível na tela `/account/confirm`, e o usuário ficava trancado fora de
104
+ * exportar/excluir os próprios dados, do MFA, dos PATs e da troca de e-mail,
105
+ * inclusive fora do cadastro de passkey que destravaria o resto. Exigir um hook
106
+ * escrito à mão para que o único método sem credencial prévia funcione é
107
+ * transformar uma saída de emergência em opt-in.
108
+ *
109
+ * A única exigência que resta é a que não tem substituto: a conta precisa ter
110
+ * e-mail.
96
111
  */
97
112
  export function magicLink() {
98
113
  return {
99
114
  id: 'magic-link',
100
115
  async isAvailable(c) {
101
- if (!c.account?.email)
102
- return false;
103
- return typeof c.cfg?.mail?.onSudoLink === 'function';
116
+ return Boolean(c.account?.email);
104
117
  },
105
118
  async describe() {
106
119
  return {
@@ -122,20 +135,31 @@ export function magicLink() {
122
135
  // e sem e-mail não há para onde mandar o link.
123
136
  if (!c.account?.email)
124
137
  return h.fail(c, 'account.confirm.error');
125
- // Checado ANTES de emitir: um token emitido sem ninguém para entregá-lo
126
- // é lixo na sessão, e a `isAvailable` já prometeu que sem hook o método
127
- // não existe.
128
- const onSudoLink = c.cfg?.mail?.onSudoLink;
129
- if (typeof onSudoLink !== 'function')
130
- return h.fail(c, 'account.confirm.error');
131
138
  const qs = c.returnTo ? `?return_to=${encodeURIComponent(c.returnTo)}` : '';
132
139
  const token = issueSudoLinkToken(c);
133
140
  const path = `${accountPath('confirm')}/magic-link/${token}${qs}`;
134
141
  const origin = requestOrigin(ctx);
142
+ const sudoUrl = origin ? `${origin}${path}` : path;
143
+ // ENTREGA. Hook do host quando declarado; senão o mailer default do
144
+ // host-kit (mesma precedência de todo e-mail da lib). O que NÃO é
145
+ // opcional é haver uma entrega: um método de sudo que só funciona se o
146
+ // host escrever um hook não serve de saída de emergência para o host
147
+ // passwordless, que é justamente quem depende dele.
148
+ const onSudoLink = c.cfg?.mail?.onSudoLink;
149
+ let delivered = false;
135
150
  try {
136
- await onSudoLink({ email: c.account.email, sudoUrl: origin ? `${origin}${path}` : path });
151
+ if (typeof onSudoLink === 'function') {
152
+ await onSudoLink({ email: c.account.email, sudoUrl });
153
+ delivered = true;
154
+ }
155
+ else {
156
+ delivered = await sendSudoLinkEmail(ctx, { email: c.account.email, sudoUrl });
157
+ }
137
158
  }
138
159
  catch {
160
+ delivered = false;
161
+ }
162
+ if (!delivered) {
139
163
  // O envio falhou: apaga o pendente. Não é risco de segurança (o
140
164
  // segredo não chegou a lugar nenhum), mas deixá-lo lá invalidaria
141
165
  // silenciosamente um token anterior ainda válido do usuário.