@adonis-agora/authkit-server 0.54.0 → 0.55.1

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 +30 -13
  60. package/build/src/provider/token_exchange.d.ts +25 -1
  61. package/build/src/provider/token_exchange.js +29 -2
  62. package/package.json +6 -3
  63. /package/build/{password → src/password}/common_passwords.txt +0 -0
@@ -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.
@@ -18,8 +18,17 @@ export declare function sudoContextFrom(ctx: HttpContext): Promise<SudoContext>;
18
18
  * Registra a lista montada. Chamado UMA vez por `registerAuthHost`, e
19
19
  * SUBSTITUI (não acumula): registrar o host de novo é redefinir o que existe,
20
20
  * não somar ao que existia.
21
+ *
22
+ * `fromDefaults` distingue "esta é a lista de defaults da LIB" de "esta é a
23
+ * lista que o HOST escreveu" (argumento de `registerAuthHost` ou
24
+ * `config.sudo.methods`). A distinção existe porque a derivação do deadlock
25
+ * (`derivedSudoMethods`) só pode mexer na primeira: mexer na segunda seria
26
+ * mudar, sem pedir, o que um host declarou explicitamente — e a promessa de
27
+ * `sudoMethods` é que ele SUBSTITUI os defaults.
21
28
  */
22
- export declare function setMountedSudoMethods(methods: SudoMethod[]): void;
29
+ export declare function setMountedSudoMethods(methods: SudoMethod[], opts?: {
30
+ fromDefaults?: boolean;
31
+ }): void;
23
32
  /** Um método com este id teve rotas montadas? Usado só para avisar de drift. */
24
33
  export declare function isSudoMethodMounted(methodId: string): boolean;
25
34
  /**
@@ -33,6 +42,29 @@ export declare function isSudoMethodMounted(methodId: string): boolean;
33
42
  * circular se os métodos importassem de volta o controller.
34
43
  */
35
44
  export declare function explicitSudoMethods(cfg: ResolvedServerConfig): SudoMethod[] | null;
45
+ /**
46
+ * O DEFAULT da lib, resolvido contra o config — o conserto do deadlock.
47
+ *
48
+ * `registerAuthHost` monta `[password, passkey, magicLink]` sem saber nada do
49
+ * config (a montagem acontece antes de o config lazy resolver). Aqui o config
50
+ * está resolvido, e é aqui que se decide qual metade vale:
51
+ *
52
+ * - host COM senha → `[password, passkey]`, o histórico byte a byte. O endpoint
53
+ * do magic link fica montado e inerte: quem quiser oferecê-lo declara
54
+ * `config.sudo.methods` — e agora isso FUNCIONA, porque a rota existe (era
55
+ * metade da promessa de 0.46 que ficava faltando).
56
+ * - host que declarou `authMethods: { password: false }` → `[passkey, magicLink]`.
57
+ * Sai o campo de senha, que nesse deployment é uma opção que não pode dar
58
+ * certo, e entra o único método que uma conta sem credencial prévia consegue
59
+ * satisfazer (ver `CREDENTIAL_FREE_SUDO_METHOD_IDS` em `sudo/satisfiability.ts`).
60
+ *
61
+ * SÓ MEXE NA LISTA DE DEFAULTS (`mountedFromDefaults`). Lista escrita pelo host
62
+ * passa intacta — inclusive quando isso o deixa em deadlock, caso em que o aviso
63
+ * de boot (`define_config.ts`) é quem fala. Adivinhar a intenção de uma lista
64
+ * explícita seria pior: é a lista dele, e a promessa documentada é que ela
65
+ * SUBSTITUI os defaults.
66
+ */
67
+ export declare function derivedSudoMethods(cfg: ResolvedServerConfig): SudoMethod[];
36
68
  /**
37
69
  * O método `methodId` está habilitado para ESTE host?
38
70
  *
@@ -41,9 +73,11 @@ export declare function explicitSudoMethods(cfg: ResolvedServerConfig): SudoMeth
41
73
  * continuaria vivo e concedendo sudo — uma config que aparenta restringir e não
42
74
  * restringe é pior que nenhuma config.
43
75
  *
44
- * Sem configuração explícita nada foi restringido: vale o que tem rota montada.
45
- * Isso é deliberado a lista de defaults não é a fonte de verdade do que está
46
- * montado, e tratá-la como tal derrubaria um método customizado do host.
76
+ * Sem configuração explícita nada foi restringido: vale o que tem rota montada,
77
+ * MENOS o que a derivação do default tirou (ver `isDefaultSudoMethodDerivedOut`).
78
+ * Continua não sendo a lista de defaults a fonte de verdade do que está montado —
79
+ * tratá-la como tal derrubaria um método customizado do host —, e a derivação só
80
+ * remove ids que a própria lib pôs na lista.
47
81
  */
48
82
  export declare function isSudoMethodEnabled(cfg: ResolvedServerConfig, methodId: string): boolean;
49
83
  /**
@@ -64,6 +98,12 @@ export declare function isSudoMethodEnabled(cfg: ResolvedServerConfig, methodId:
64
98
  * fica estruturalmente impossível no caso sem config: é literalmente a mesma
65
99
  * lista. O aviso de flag-drift do controller passa a valer só para o caso que
66
100
  * sobra — config explícita divergindo do que foi montado.
101
+ *
102
+ * "A lista montada" é a lista montada DERIVADA (`derivedSudoMethods`), e os
103
+ * handlers aplicam a MESMA derivação pela mesma função — o drift continua
104
+ * impossível. A derivação existe porque a montagem não conhece o config: ela é
105
+ * quem tira o campo de senha de um host que declarou não ter senha, e quem
106
+ * mantém o default histórico intacto para quem tem.
67
107
  */
68
108
  export declare function configuredSudoMethods(cfg: ResolvedServerConfig): SudoMethod[];
69
109
  /**
@@ -40,13 +40,28 @@ export async function sudoContextFrom(ctx) {
40
40
  * ver `configuredSudoMethods`.
41
41
  */
42
42
  const mountedSudoMethods = [];
43
+ /**
44
+ * A lista montada é a de DEFAULTS da lib (e não uma escrita pelo host)?
45
+ *
46
+ * Só a lista de defaults é derivada do config (`derivedSudoMethods`). Uma lista
47
+ * que o host escreveu vale ao pé da letra — ver o docblock de `setMountedSudoMethods`.
48
+ */
49
+ let mountedFromDefaults = false;
43
50
  /**
44
51
  * Registra a lista montada. Chamado UMA vez por `registerAuthHost`, e
45
52
  * SUBSTITUI (não acumula): registrar o host de novo é redefinir o que existe,
46
53
  * não somar ao que existia.
54
+ *
55
+ * `fromDefaults` distingue "esta é a lista de defaults da LIB" de "esta é a
56
+ * lista que o HOST escreveu" (argumento de `registerAuthHost` ou
57
+ * `config.sudo.methods`). A distinção existe porque a derivação do deadlock
58
+ * (`derivedSudoMethods`) só pode mexer na primeira: mexer na segunda seria
59
+ * mudar, sem pedir, o que um host declarou explicitamente — e a promessa de
60
+ * `sudoMethods` é que ele SUBSTITUI os defaults.
47
61
  */
48
- export function setMountedSudoMethods(methods) {
62
+ export function setMountedSudoMethods(methods, opts = {}) {
49
63
  mountedSudoMethods.splice(0, mountedSudoMethods.length, ...methods);
64
+ mountedFromDefaults = opts.fromDefaults === true;
50
65
  }
51
66
  /** Um método com este id teve rotas montadas? Usado só para avisar de drift. */
52
67
  export function isSudoMethodMounted(methodId) {
@@ -66,6 +81,67 @@ export function explicitSudoMethods(cfg) {
66
81
  const configured = cfg?.sudo?.methods;
67
82
  return Array.isArray(configured) && configured.length ? configured : null;
68
83
  }
84
+ /**
85
+ * O host declarou, PELO CONFIG, que este deployment não tem senha usável?
86
+ *
87
+ * `authMethods: { password: false }` é pin de config e é autoritativo — tem
88
+ * prioridade sobre o runtime setting e o console admin mostra o toggle travado.
89
+ * Quem declara isso está dizendo que ninguém entra por senha; contas criadas
90
+ * pelo signup passwordless nem têm senha (a coluna leva um hash aleatório
91
+ * inutilizável, indistinguível de um hash real de dentro do pacote).
92
+ *
93
+ * NÃO usa `passwordless.*`: aquelas flags LIGAM vias alternativas de login sem
94
+ * desligar a senha, então um host com `passwordless.magicLink: true` pode
95
+ * perfeitamente ter usuários que conhecem a própria senha. Só o pin em `false`
96
+ * é uma afirmação sobre o deployment inteiro.
97
+ */
98
+ function isPasswordlessHost(cfg) {
99
+ return cfg?.authMethods?.password === false;
100
+ }
101
+ /**
102
+ * O DEFAULT da lib, resolvido contra o config — o conserto do deadlock.
103
+ *
104
+ * `registerAuthHost` monta `[password, passkey, magicLink]` sem saber nada do
105
+ * config (a montagem acontece antes de o config lazy resolver). Aqui o config
106
+ * está resolvido, e é aqui que se decide qual metade vale:
107
+ *
108
+ * - host COM senha → `[password, passkey]`, o histórico byte a byte. O endpoint
109
+ * do magic link fica montado e inerte: quem quiser oferecê-lo declara
110
+ * `config.sudo.methods` — e agora isso FUNCIONA, porque a rota existe (era
111
+ * metade da promessa de 0.46 que ficava faltando).
112
+ * - host que declarou `authMethods: { password: false }` → `[passkey, magicLink]`.
113
+ * Sai o campo de senha, que nesse deployment é uma opção que não pode dar
114
+ * certo, e entra o único método que uma conta sem credencial prévia consegue
115
+ * satisfazer (ver `CREDENTIAL_FREE_SUDO_METHOD_IDS` em `sudo/satisfiability.ts`).
116
+ *
117
+ * SÓ MEXE NA LISTA DE DEFAULTS (`mountedFromDefaults`). Lista escrita pelo host
118
+ * passa intacta — inclusive quando isso o deixa em deadlock, caso em que o aviso
119
+ * de boot (`define_config.ts`) é quem fala. Adivinhar a intenção de uma lista
120
+ * explícita seria pior: é a lista dele, e a promessa documentada é que ela
121
+ * SUBSTITUI os defaults.
122
+ */
123
+ export function derivedSudoMethods(cfg) {
124
+ return mountedSudoMethods.filter((m) => !isDefaultSudoMethodDerivedOut(cfg, m?.id));
125
+ }
126
+ /**
127
+ * Este id foi DERIVADO PARA FORA da lista de defaults neste host?
128
+ *
129
+ * Ponto único da regra, consultado pelos DOIS lados (a tela, via
130
+ * `configuredSudoMethods`, e os handlers, via `isSudoMethodEnabled`) — é o que
131
+ * torna o drift entre eles estruturalmente impossível também no caso derivado.
132
+ *
133
+ * Nunca responde `true` para uma lista de host, nem para um id que não seja um
134
+ * dos dois built-in do default. Ou seja: só REMOVE, e só remove o que a própria
135
+ * lib pôs lá.
136
+ */
137
+ function isDefaultSudoMethodDerivedOut(cfg, methodId) {
138
+ if (!mountedFromDefaults)
139
+ return false;
140
+ // Host passwordless: cai a senha (não há senha usável neste deployment).
141
+ // Host com senha: cai o magic link (posture histórica preservada — o default
142
+ // não passa a oferecer um step-up por e-mail a quem nunca pediu).
143
+ return isPasswordlessHost(cfg) ? methodId === 'password' : methodId === 'magic-link';
144
+ }
69
145
  /**
70
146
  * O método `methodId` está habilitado para ESTE host?
71
147
  *
@@ -74,14 +150,16 @@ export function explicitSudoMethods(cfg) {
74
150
  * continuaria vivo e concedendo sudo — uma config que aparenta restringir e não
75
151
  * restringe é pior que nenhuma config.
76
152
  *
77
- * Sem configuração explícita nada foi restringido: vale o que tem rota montada.
78
- * Isso é deliberado a lista de defaults não é a fonte de verdade do que está
79
- * montado, e tratá-la como tal derrubaria um método customizado do host.
153
+ * Sem configuração explícita nada foi restringido: vale o que tem rota montada,
154
+ * MENOS o que a derivação do default tirou (ver `isDefaultSudoMethodDerivedOut`).
155
+ * Continua não sendo a lista de defaults a fonte de verdade do que está montado —
156
+ * tratá-la como tal derrubaria um método customizado do host —, e a derivação só
157
+ * remove ids que a própria lib pôs na lista.
80
158
  */
81
159
  export function isSudoMethodEnabled(cfg, methodId) {
82
160
  const explicit = explicitSudoMethods(cfg);
83
161
  if (explicit === null)
84
- return true;
162
+ return !isDefaultSudoMethodDerivedOut(cfg, methodId);
85
163
  return explicit.some((m) => m?.id === methodId);
86
164
  }
87
165
  /**
@@ -102,9 +180,15 @@ export function isSudoMethodEnabled(cfg, methodId) {
102
180
  * fica estruturalmente impossível no caso sem config: é literalmente a mesma
103
181
  * lista. O aviso de flag-drift do controller passa a valer só para o caso que
104
182
  * sobra — config explícita divergindo do que foi montado.
183
+ *
184
+ * "A lista montada" é a lista montada DERIVADA (`derivedSudoMethods`), e os
185
+ * handlers aplicam a MESMA derivação pela mesma função — o drift continua
186
+ * impossível. A derivação existe porque a montagem não conhece o config: ela é
187
+ * quem tira o campo de senha de um host que declarou não ter senha, e quem
188
+ * mantém o default histórico intacto para quem tem.
105
189
  */
106
190
  export function configuredSudoMethods(cfg) {
107
- return explicitSudoMethods(cfg) ?? mountedSudoMethods;
191
+ return explicitSudoMethods(cfg) ?? derivedSudoMethods(cfg);
108
192
  }
109
193
  /**
110
194
  * Verbos HTTP com a forma `(pattern, handler, ...)` — o handler é o SEGUNDO
@@ -0,0 +1,62 @@
1
+ /**
2
+ * "Este host consegue satisfazer o próprio sudo?" — a pergunta que ninguém
3
+ * fazia, e cuja resposta "não" trancava o usuário fora de exportar/excluir os
4
+ * próprios dados.
5
+ *
6
+ * MÓDULO FOLHA DE PROPÓSITO: importa só TIPOS. `define_config.ts` precisa
7
+ * chamar o aviso de boot daqui, e `sudo/runtime.ts` precisa da mesma lista de
8
+ * ids — importar `runtime.ts` de dentro do `define_config` fecharia um ciclo
9
+ * (`runtime` → controllers → ... → `define_config`) cuja ordem de avaliação é
10
+ * exatamente o tipo de coisa que quebra em produção e não em teste.
11
+ */
12
+ import type { SudoMethod } from './types.js';
13
+ /**
14
+ * Métodos built-in que uma conta SEM senha e SEM passkey consegue satisfazer —
15
+ * os únicos que quebram o deadlock do host passwordless.
16
+ *
17
+ * `oidc-step-up` não exige nada previamente cadastrado (é o `prompt=login` do
18
+ * próprio protocolo); `magic-link` exige apenas que a conta tenha e-mail, e a
19
+ * ENTREGA tem fallback para o mailer default do host-kit, então nem hook é
20
+ * obrigatório. Os outros dois built-in — `password` e `passkey` — exigem, por
21
+ * definição, uma credencial que o usuário teria de ter cadastrado ANTES, e é
22
+ * essa a pré-condição que um host passwordless não satisfaz.
23
+ */
24
+ export declare const CREDENTIAL_FREE_SUDO_METHOD_IDS: readonly string[];
25
+ /**
26
+ * Avisa, NO BOOT, quando a configuração de sudo deste host não tem um único
27
+ * método que uma conta sem senha possa satisfazer.
28
+ *
29
+ * QUANDO DISPARA (as quatro condições, todas necessárias):
30
+ *
31
+ * 1. O host declarou `sudo.methods` EXPLICITAMENTE. Sem declaração vale o
32
+ * default derivado, que já resolve o caso passwordless — não há o que avisar.
33
+ * 2. O deployment tem contas sem senha usável: `authMethods.password === false`
34
+ * (ninguém entra por senha) ou `passwordless.signup === true` (o cadastro
35
+ * público cria contas com um hash aleatório inutilizável).
36
+ * 3. Nenhum dos métodos declarados é credential-free.
37
+ * 4. TODOS os métodos declarados são built-in deste pacote — ou seja, o pacote
38
+ * consegue de fato PROVAR que a lista exige credencial prévia.
39
+ *
40
+ * WARN, NÃO THROW. Recusar o boot seria transformar um upgrade de patch/minor
41
+ * numa aplicação que não sobe, em produção, por uma condição que é
42
+ * DEGRADAÇÃO — a tela de confirmação fica sem opções, o que já falha fechado — e
43
+ * não uma brecha. Pior: a remediação nem sempre é uma linha de config
44
+ * (`oidcStepUp` exige uma rota e um callback no host), então o app ficaria fora
45
+ * do ar até alguém escrever código. E a detecção é, por construção, incompleta
46
+ * (item 4): um throw baseado numa heurística derruba host correto. O irmão
47
+ * `adonis-agent` recusa montar quando a superfície PROVADAMENTE não funciona;
48
+ * aqui não há prova — há forte suspeita, e o lugar disso é um aviso alto.
49
+ *
50
+ * @returns a mensagem emitida, ou `null` quando não havia nada a avisar (para
51
+ * teste; o efeito de verdade é o `console.warn`).
52
+ */
53
+ export declare function warnUnsatisfiableSudoConfig(input: {
54
+ /** `config.sudo.methods` — a lista que o host declarou, se declarou. */
55
+ methods?: SudoMethod[];
56
+ /** `authMethods.password` pinado em `false`? */
57
+ passwordPinnedOff: boolean;
58
+ /** `passwordless.signup` ligado? */
59
+ passwordlessSignup: boolean;
60
+ /** Injeção para teste. Default: `console.warn`. */
61
+ warn?: (message: string) => void;
62
+ }): string | null;
@@ -0,0 +1,89 @@
1
+ /**
2
+ * "Este host consegue satisfazer o próprio sudo?" — a pergunta que ninguém
3
+ * fazia, e cuja resposta "não" trancava o usuário fora de exportar/excluir os
4
+ * próprios dados.
5
+ *
6
+ * MÓDULO FOLHA DE PROPÓSITO: importa só TIPOS. `define_config.ts` precisa
7
+ * chamar o aviso de boot daqui, e `sudo/runtime.ts` precisa da mesma lista de
8
+ * ids — importar `runtime.ts` de dentro do `define_config` fecharia um ciclo
9
+ * (`runtime` → controllers → ... → `define_config`) cuja ordem de avaliação é
10
+ * exatamente o tipo de coisa que quebra em produção e não em teste.
11
+ */
12
+ /**
13
+ * Métodos built-in que uma conta SEM senha e SEM passkey consegue satisfazer —
14
+ * os únicos que quebram o deadlock do host passwordless.
15
+ *
16
+ * `oidc-step-up` não exige nada previamente cadastrado (é o `prompt=login` do
17
+ * próprio protocolo); `magic-link` exige apenas que a conta tenha e-mail, e a
18
+ * ENTREGA tem fallback para o mailer default do host-kit, então nem hook é
19
+ * obrigatório. Os outros dois built-in — `password` e `passkey` — exigem, por
20
+ * definição, uma credencial que o usuário teria de ter cadastrado ANTES, e é
21
+ * essa a pré-condição que um host passwordless não satisfaz.
22
+ */
23
+ export const CREDENTIAL_FREE_SUDO_METHOD_IDS = ['oidc-step-up', 'magic-link'];
24
+ /**
25
+ * Ids dos métodos que ESTE pacote implementa.
26
+ *
27
+ * Serve para o aviso abaixo se CALAR diante de um método customizado: de um
28
+ * método do SPI o pacote não tem como saber se ele exige credencial prévia, e um
29
+ * aviso de boot que grita para uma configuração correta é um aviso que o host
30
+ * aprende a ignorar — justamente o que não pode acontecer com este.
31
+ */
32
+ const BUILTIN_SUDO_METHOD_IDS = [
33
+ 'password',
34
+ 'passkey',
35
+ 'oidc-step-up',
36
+ 'magic-link',
37
+ ];
38
+ /**
39
+ * Avisa, NO BOOT, quando a configuração de sudo deste host não tem um único
40
+ * método que uma conta sem senha possa satisfazer.
41
+ *
42
+ * QUANDO DISPARA (as quatro condições, todas necessárias):
43
+ *
44
+ * 1. O host declarou `sudo.methods` EXPLICITAMENTE. Sem declaração vale o
45
+ * default derivado, que já resolve o caso passwordless — não há o que avisar.
46
+ * 2. O deployment tem contas sem senha usável: `authMethods.password === false`
47
+ * (ninguém entra por senha) ou `passwordless.signup === true` (o cadastro
48
+ * público cria contas com um hash aleatório inutilizável).
49
+ * 3. Nenhum dos métodos declarados é credential-free.
50
+ * 4. TODOS os métodos declarados são built-in deste pacote — ou seja, o pacote
51
+ * consegue de fato PROVAR que a lista exige credencial prévia.
52
+ *
53
+ * WARN, NÃO THROW. Recusar o boot seria transformar um upgrade de patch/minor
54
+ * numa aplicação que não sobe, em produção, por uma condição que é
55
+ * DEGRADAÇÃO — a tela de confirmação fica sem opções, o que já falha fechado — e
56
+ * não uma brecha. Pior: a remediação nem sempre é uma linha de config
57
+ * (`oidcStepUp` exige uma rota e um callback no host), então o app ficaria fora
58
+ * do ar até alguém escrever código. E a detecção é, por construção, incompleta
59
+ * (item 4): um throw baseado numa heurística derruba host correto. O irmão
60
+ * `adonis-agent` recusa montar quando a superfície PROVADAMENTE não funciona;
61
+ * aqui não há prova — há forte suspeita, e o lugar disso é um aviso alto.
62
+ *
63
+ * @returns a mensagem emitida, ou `null` quando não havia nada a avisar (para
64
+ * teste; o efeito de verdade é o `console.warn`).
65
+ */
66
+ export function warnUnsatisfiableSudoConfig(input) {
67
+ const declared = Array.isArray(input.methods) ? input.methods : [];
68
+ if (!declared.length)
69
+ return null;
70
+ if (!input.passwordPinnedOff && !input.passwordlessSignup)
71
+ return null;
72
+ const ids = declared.map((m) => m?.id).filter((id) => typeof id === 'string');
73
+ if (ids.length !== declared.length)
74
+ return null; // lista malformada: não é este o aviso
75
+ if (ids.some((id) => CREDENTIAL_FREE_SUDO_METHOD_IDS.includes(id)))
76
+ return null;
77
+ if (!ids.every((id) => BUILTIN_SUDO_METHOD_IDS.includes(id)))
78
+ return null;
79
+ const trigger = input.passwordPinnedOff
80
+ ? 'authMethods: { password: false }'
81
+ : 'passwordless: { signup: true }';
82
+ const message = [
83
+ `authkit: sudo mode SEM SAÍDA para contas sem senha. Este host declarou \`${trigger}\`, então existem contas sem senha usável — mas \`sudo.methods\` só lista [${ids.join(', ')}], e todos exigem uma credencial cadastrada ANTES.`,
84
+ 'Consequência: essas contas ficam trancadas fora de TODA operação sob `requireSudo` — exportar/excluir dados (LGPD), MFA, Personal Access Tokens, troca de e-mail — inclusive fora do cadastro de passkey, que é o que destravaria o resto.',
85
+ 'Saída: acrescente `sudoMethods.oidcStepUp({ url: "/auth/step-up" })` (reautenticação no seu IdP) ou `sudoMethods.magicLink()` (link de confirmação por e-mail, enviado pelo mailer do app quando não há hook `mail.onSudoLink`) à lista de `sudo.methods`.',
86
+ ].join(' ');
87
+ (input.warn ?? console.warn)(message);
88
+ return message;
89
+ }