@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
@@ -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
+ }
@@ -11,7 +11,7 @@
11
11
  * Fonte dos dados: compilação de senhas comuns de domínio público.
12
12
  * Licença do arquivo de dados: CC0 / Public Domain.
13
13
  */
14
- import { readFileSync } from 'node:fs';
14
+ import { existsSync, readFileSync } from 'node:fs';
15
15
  import { dirname, join } from 'node:path';
16
16
  import { fileURLToPath } from 'node:url';
17
17
  /**
@@ -23,16 +23,36 @@ let _commonPasswordsSet = null;
23
23
  /**
24
24
  * Carrega o arquivo de senhas comuns uma única vez (lazy).
25
25
  *
26
- * FAIL-SAFE: se o arquivo não existir ou não puder ser lido (ex.: bundle sem
27
- * assets), retorna um Set vazio a checagem vira no-op sem quebrar o fluxo.
26
+ * Onde o arquivo é esperado: o build script copia `common_passwords.txt`
27
+ * para o mesmo diretório do módulo compilado (`build/src/password/`, que
28
+ * `tsc` com `rootDir: "./"` preserva o subcaminho `src/`). Em dev via
29
+ * ts-exec (sem build), o módulo roda direto de `src/password/`, onde o
30
+ * `.txt` já mora ao lado do `.ts` no repo.
31
+ *
32
+ * Checamos mais de um candidato — não só o caminho-irmão — para sobreviver
33
+ * a uma mudança futura em `rootDir`/`outDir` ou a uma regressão no script de
34
+ * cópia do build: o caminho-irmão cobre o layout atual (build e dev); os
35
+ * demais são fallbacks para o layout antigo (`build/password/`) e para ler
36
+ * direto de `src/` a partir de um build.
37
+ *
38
+ * FAIL-SAFE: se o arquivo não existir em nenhum candidato, ou não puder ser
39
+ * lido (ex.: bundle sem assets), retorna um Set vazio — a checagem vira
40
+ * no-op sem quebrar o fluxo. Uma senha comum indevidamente aceita é sempre
41
+ * preferível a um login/signup quebrado por um asset ausente.
28
42
  */
29
43
  function loadCommonPasswords() {
30
44
  if (_commonPasswordsSet !== null)
31
45
  return _commonPasswordsSet;
32
46
  try {
33
- const __filename = fileURLToPath(import.meta.url);
34
- const __dir = dirname(__filename);
35
- const filePath = join(__dir, 'common_passwords.txt');
47
+ const __dir = dirname(fileURLToPath(import.meta.url));
48
+ const candidates = [
49
+ join(__dir, 'common_passwords.txt'), // caminho-irmão: build/src/password/ (prod) ou src/password/ (dev via ts-exec)
50
+ join(__dir, '..', '..', 'password', 'common_passwords.txt'), // layout antigo: build/password/
51
+ join(__dir, '..', '..', '..', 'src', 'password', 'common_passwords.txt'), // fallback: src/ a partir de um build
52
+ ];
53
+ const filePath = candidates.find((p) => existsSync(p));
54
+ if (!filePath)
55
+ throw new Error('common_passwords.txt not found in any candidate path');
36
56
  const content = readFileSync(filePath, 'utf-8');
37
57
  const entries = content
38
58
  .split('\n')
@@ -41,7 +61,7 @@ function loadCommonPasswords() {
41
61
  _commonPasswordsSet = new Set(entries);
42
62
  }
43
63
  catch {
44
- // Fail-safe: arquivo ausente → Set vazio (no-op check).
64
+ // Fail-safe: arquivo ausente em todos os candidatos → Set vazio (no-op check).
45
65
  _commonPasswordsSet = new Set();
46
66
  }
47
67
  return _commonPasswordsSet;
@@ -118,19 +118,31 @@ export class OidcService {
118
118
  },
119
119
  }, this.sessionTtlHolder, this.tokenTtlHolder);
120
120
  wireProviderEvents(provider, this.recorder);
121
- registerTokenExchange(provider, {
122
- findAccount: config.findAccount,
123
- globalRolesClaim: config.globalRolesClaim,
124
- resolveTokenRoles: config.resolveTokenRoles,
125
- // Resource indicators (RFC 8707) suportados: o `audience` default + cada
126
- // resource declarado. Usado para validar `audience`/`resource` no pedido de
127
- // token-exchange — alvos fora desta lista são rejeitados (invalid_target).
128
- supportedResources: [
129
- config.accessTokens.audience,
130
- ...Object.keys(config.accessTokens.resources),
131
- ],
132
- audit: config.audit,
133
- });
121
+ // KILL SWITCH de impersonation (`admin.impersonation`, default `true`).
122
+ // Com a flag em `false` o grant simplesmente NÃO EXISTE no provider — o
123
+ // token endpoint responde `unsupported_grant_type` — em vez de existir e
124
+ // recusar caso a caso. É a mesma flag que fecha o painel do console
125
+ // (`console_impersonation_controller.ts`): uma declaração, duas superfícies.
126
+ if (config.admin.impersonation) {
127
+ registerTokenExchange(provider, {
128
+ findAccount: config.findAccount,
129
+ globalRolesClaim: config.globalRolesClaim,
130
+ resolveTokenRoles: config.resolveTokenRoles,
131
+ // Resource indicators (RFC 8707) suportados: o `audience` default + cada
132
+ // resource declarado. Usado para validar `audience`/`resource` no pedido de
133
+ // token-exchange — alvos fora desta lista são rejeitados (invalid_target).
134
+ supportedResources: [
135
+ config.accessTokens.audience,
136
+ ...Object.keys(config.accessTokens.resources),
137
+ ],
138
+ // LOAD-BEARING: sem isto o gate de status do alvo dentro de
139
+ // `token_exchange.ts` degrada para "allowed" (o campo é opcional, para não
140
+ // quebrar hosts com stores mínimos) e um admin consegue impersonar uma
141
+ // conta que acabou de desabilitar, recebendo tokens plenamente funcionais.
142
+ accountStore: config.accountStore,
143
+ audit: config.audit,
144
+ });
145
+ }
134
146
  // Quando o issuer tem um path (ex.: http://host/oidc), o provider precisa ser
135
147
  // MONTADO sob esse path via koa-mount. Isso faz o oidc-provider gerar URLs de
136
148
  // discovery e redirects de resume/interaction CORRETAMENTE prefixados (ex.:
@@ -1,4 +1,4 @@
1
- import type { AuthAccount } from '../accounts/account_store.js';
1
+ import type { AccountStore, AuthAccount } from '../accounts/account_store.js';
2
2
  import type { AuditSink } from '../audit/audit_sink.js';
3
3
  export interface TokenExchangeAccount {
4
4
  id: string;
@@ -32,5 +32,16 @@ export interface TokenExchangeDeps {
32
32
  supportedResources?: string[];
33
33
  /** Sink de auditoria (best-effort). Quando presente, registra `impersonation`. */
34
34
  audit?: AuditSink;
35
+ /**
36
+ * AccountStore do host, usado para checar se o alvo da impersonação está
37
+ * desabilitado/expirado (mesmo gate de `attemptPasswordLogin`, via
38
+ * `assertAccountEnabled`). Impersonar uma conta desabilitada mintaria tokens
39
+ * funcionais para uma identidade que deveria estar sem acesso algum.
40
+ *
41
+ * OPCIONAL e capability-probed (via `supportsAccountStatus`, dentro do
42
+ * helper): ausente ou sem a capacidade → degrada para "permitido" (mesma
43
+ * regra de todo o resto da lib — nunca quebra hosts com um store mínimo).
44
+ */
45
+ accountStore?: AccountStore;
35
46
  }
36
47
  export declare function registerTokenExchange(provider: any, deps: TokenExchangeDeps): void;
@@ -1,4 +1,5 @@
1
1
  import { errors } from 'oidc-provider';
2
+ import { assertAccountEnabled } from '../host/login_attempt.js';
2
3
  const TOKEN_EXCHANGE = 'urn:ietf:params:oauth:grant-type:token-exchange';
3
4
  const ACCESS_TOKEN_TYPE = 'urn:ietf:params:oauth:token-type:access_token';
4
5
  /**
@@ -46,6 +47,17 @@ export function registerTokenExchange(provider, deps) {
46
47
  if (!target) {
47
48
  throw new errors.InvalidGrant('requested_subject not found');
48
49
  }
50
+ // Status do alvo (disabled/expirado): mesmo gate de `attemptPasswordLogin`.
51
+ // Sem isso, impersonar um alvo desabilitado mintava tokens funcionais para
52
+ // uma identidade que o admin acreditava ter revogado. `accountStore` é
53
+ // opcional/capability-probed — sem ele (ou sem a capacidade), permanece
54
+ // "permitido" como antes.
55
+ if (deps.accountStore) {
56
+ const statusGate = await assertAccountEnabled({ accountStore: deps.accountStore, audit: deps.audit }, target.id, { email: target.email ?? '', ip: ctx.req?.socket?.remoteAddress ?? null });
57
+ if (!statusGate.allowed) {
58
+ throw new errors.InvalidGrant('requested_subject is disabled');
59
+ }
60
+ }
49
61
  // audience/resource: se o pedido vier com um alvo, ele PRECISA estar entre os
50
62
  // resource indicators suportados. Caso contrário rejeitamos (conservador) —
51
63
  // nunca embutimos audiência arbitrária no token emitido.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adonis-agora/authkit-server",
3
- "version": "0.53.0",
3
+ "version": "0.55.0",
4
4
  "description": "AdonisJS OIDC/OAuth2 provider (Identity Provider) toolkit: ejectable auth server with sessions, rate-limiting, MFA/TOTP, audit log, federated logout and OpenTelemetry metrics.",
5
5
  "license": "MIT",
6
6
  "author": "dudousxd",
@@ -124,6 +124,8 @@
124
124
  "@japa/assert": "4.2.0",
125
125
  "@japa/runner": "5.3.0",
126
126
  "@poppinss/ts-exec": "1.4.4",
127
+ "@types/better-sqlite3": "^7.6.13",
128
+ "@types/ioredis-mock": "^8.2.7",
127
129
  "@types/koa": "^2.15.2",
128
130
  "@types/koa-mount": "^4.0.5",
129
131
  "@types/luxon": "3.7.1",
@@ -151,11 +153,12 @@
151
153
  "@adonis-agora/authkit-react": "0.18.0"
152
154
  },
153
155
  "scripts": {
154
- "build": "node scripts/build_host_css.mjs && node scripts/build_webauthn.mjs && node scripts/build_ui.mjs && node -e \"const fs=require('node:fs');for(const d of ['build/stubs','build/host/views'])fs.rmSync(d,{recursive:true,force:true})\" && tsc && node -e \"require('node:fs').cpSync('src/host/assets','build/src/host/assets',{recursive:true})\" && node -e \"require('node:fs').cpSync('stubs','build/stubs',{recursive:true,filter:(s)=>!s.endsWith('.ts')})\" && node -e \"const fs=require('node:fs');if(fs.existsSync('assets'))fs.cpSync('assets','build/assets',{recursive:true})\" && node -e \"require('node:fs').cpSync('src/host/views','build/host/views',{recursive:true})\" && node -e \"const fs=require('node:fs');fs.mkdirSync('build/host/ui',{recursive:true});fs.readdirSync('src/host/ui').filter(f=>f.endsWith('.html')).forEach(f=>fs.copyFileSync('src/host/ui/'+f,'build/host/ui/'+f))\" && node -e \"require('node:fs').copyFileSync('commands/commands.json','build/commands/commands.json')\" && node -e \"const fs=require('node:fs');fs.mkdirSync('build/password',{recursive:true});fs.copyFileSync('src/password/common_passwords.txt','build/password/common_passwords.txt')\"",
156
+ "build": "node scripts/build_host_css.mjs && node scripts/build_webauthn.mjs && node scripts/build_ui.mjs && node -e \"const fs=require('node:fs');for(const d of ['build/stubs','build/host/views'])fs.rmSync(d,{recursive:true,force:true})\" && tsc && node -e \"require('node:fs').cpSync('src/host/assets','build/src/host/assets',{recursive:true})\" && node -e \"require('node:fs').cpSync('stubs','build/stubs',{recursive:true,filter:(s)=>!s.endsWith('.ts')})\" && node -e \"const fs=require('node:fs');if(fs.existsSync('assets'))fs.cpSync('assets','build/assets',{recursive:true})\" && node -e \"require('node:fs').cpSync('src/host/views','build/host/views',{recursive:true})\" && node -e \"const fs=require('node:fs');fs.mkdirSync('build/host/ui',{recursive:true});fs.readdirSync('src/host/ui').filter(f=>f.endsWith('.html')).forEach(f=>fs.copyFileSync('src/host/ui/'+f,'build/host/ui/'+f))\" && node -e \"require('node:fs').copyFileSync('commands/commands.json','build/commands/commands.json')\" && node -e \"const fs=require('node:fs');fs.mkdirSync('build/src/password',{recursive:true});fs.copyFileSync('src/password/common_passwords.txt','build/src/password/common_passwords.txt')\"",
155
157
  "build:ui": "node scripts/build_ui.mjs",
156
158
  "build:webauthn": "node scripts/build_webauthn.mjs",
157
159
  "check:webauthn-bundle": "node scripts/check_webauthn_bundle.mjs",
158
- "typecheck": "tsc --noEmit && node scripts/typecheck_ui.mjs",
160
+ "typecheck": "tsc --noEmit && node scripts/typecheck_ui.mjs && node scripts/typecheck_tests.mjs",
161
+ "typecheck:tests": "node scripts/typecheck_tests.mjs",
159
162
  "test": "node --import=@poppinss/ts-exec bin/test.ts",
160
163
  "build:host-css": "node scripts/build_host_css.mjs"
161
164
  }