@adonis-agora/authkit-server 0.45.0 → 0.46.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 (44) hide show
  1. package/build/commands/ui_preset.js +15 -1
  2. package/build/host/views/account/confirm.edge +118 -52
  3. package/build/host/views/account/mfa.edge +1 -1
  4. package/build/host/views/login.edge +2 -4
  5. package/build/host/views/mfa-challenge.edge +1 -1
  6. package/build/host/views/partials/styles.edge +1 -1
  7. package/build/index.d.ts +5 -1
  8. package/build/index.js +15 -1
  9. package/build/src/define_config.d.ts +51 -0
  10. package/build/src/define_config.js +8 -0
  11. package/build/src/host/assets/webauthn.js +2 -0
  12. package/build/src/host/controllers/account_confirm_controller.d.ts +11 -14
  13. package/build/src/host/controllers/account_confirm_controller.js +42 -130
  14. package/build/src/host/controllers/account_session_controller.js +20 -4
  15. package/build/src/host/controllers/webauthn_asset_controller.d.ts +22 -0
  16. package/build/src/host/controllers/webauthn_asset_controller.js +66 -0
  17. package/build/src/host/i18n.d.ts +14 -0
  18. package/build/src/host/i18n.js +16 -0
  19. package/build/src/host/impersonation_session.js +15 -0
  20. package/build/src/host/rate_limit.d.ts +10 -0
  21. package/build/src/host/rate_limit.js +6 -0
  22. package/build/src/host/register_auth_host.d.ts +19 -0
  23. package/build/src/host/register_auth_host.js +72 -4
  24. package/build/src/host/sudo/index.d.ts +47 -0
  25. package/build/src/host/sudo/index.js +41 -0
  26. package/build/src/host/sudo/methods/magic_link.d.ts +43 -0
  27. package/build/src/host/sudo/methods/magic_link.js +174 -0
  28. package/build/src/host/sudo/methods/oidc_step_up.d.ts +68 -0
  29. package/build/src/host/sudo/methods/oidc_step_up.js +78 -0
  30. package/build/src/host/sudo/methods/passkey.d.ts +28 -0
  31. package/build/src/host/sudo/methods/passkey.js +139 -0
  32. package/build/src/host/sudo/methods/password.d.ts +19 -0
  33. package/build/src/host/sudo/methods/password.js +93 -0
  34. package/build/src/host/sudo/runtime.d.ts +141 -0
  35. package/build/src/host/sudo/runtime.js +327 -0
  36. package/build/src/host/sudo/types.d.ts +93 -0
  37. package/build/src/host/sudo/types.js +1 -0
  38. package/build/src/host/sudo_mode.d.ts +116 -6
  39. package/build/src/host/sudo_mode.js +133 -7
  40. package/package.json +6 -2
  41. package/build/stubs/ui/edge/views/consent.edge +0 -13
  42. package/build/stubs/ui/edge/views/login.edge +0 -19
  43. package/stubs/ui/edge/views/consent.edge +0 -13
  44. package/stubs/ui/edge/views/login.edge +0 -19
@@ -9,7 +9,9 @@
9
9
  * - `enabled`: habilita/desabilita o sudo mode. Default: true.
10
10
  * - `graceMinutes`: janela de graça em minutos. Default: 15.
11
11
  *
12
- * Chave de sessão: `authkit_sudo_at` (timestamp ms).
12
+ * Chaves de sessão: `authkit_sudo_at` (timestamp ms) + `authkit_sudo_account`
13
+ * (a conta que confirmou). As duas juntas são a marca — ver
14
+ * `SUDO_ACCOUNT_SESSION_KEY`.
13
15
  */
14
16
  import type { HttpContext } from '@adonisjs/core/http';
15
17
  import type { SettingsCapability } from './runtime_settings.js';
@@ -24,17 +26,84 @@ export interface ResolvedSudoModeSetting {
24
26
  export declare const SUDO_MODE_DEFAULTS: ResolvedSudoModeSetting;
25
27
  /**
26
28
  * Resolve a setting `sudo_mode` em runtime (fail-safe).
29
+ *
30
+ * FAIL-SAFE, e de propósito: setting ausente, malformada ou store indisponível
31
+ * caem em `SUDO_MODE_DEFAULTS` em vez de lançar. A pergunta que esta função
32
+ * responde é de DISPONIBILIDADE — "o toggle de sudo mode está ligado, e com que
33
+ * janela de graça?" —, não de identidade. Um erro aqui não é sinal de credencial
34
+ * suspeita; é o store de settings fora do ar.
35
+ *
36
+ * Note que o default é `enabled: true`: o fail-safe não desliga o sudo mode, ele
37
+ * mantém a política default LIGADA. Quem decide o que fazer com uma sessão sem
38
+ * sudo continua sendo `isSudoActive` (fail-CLOSED). Ver o docblock de
39
+ * `requireSudo` para a assimetria completa entre as duas posturas — ela é
40
+ * escolha, não descuido.
27
41
  */
28
42
  export declare function resolveEffectiveSudoMode(settings: SettingsCapability): Promise<ResolvedSudoModeSetting>;
29
43
  /** Chave da sessão Adonis que registra quando o sudo foi confirmado. */
30
44
  export declare const SUDO_SESSION_KEY = "authkit_sudo_at";
31
45
  /**
32
- * Registra o timestamp de confirmação de sudo na sessão (NOW).
33
- * Chamar após o usuário confirmar sua identidade (senha ou passkey).
46
+ * Conta que CONFIRMOU o sudo registrado em `SUDO_SESSION_KEY`.
47
+ *
48
+ * Por que existe: o timestamp sozinho é uma marca de "alguém confirmou a
49
+ * identidade nesta sessão há pouco" — sem dizer QUEM. E a sessão sobrevive à
50
+ * troca de conta: o `regenerate()` só troca o id do cookie e MIGRA os dados
51
+ * (é o invariante descrito no M6 de `account_session_controller.ts`). Sem esta
52
+ * vinculação, todo caminho que troca a conta da sessão TRANSFERE o sudo junto:
53
+ *
54
+ * - impersonation: um admin que confirmou sudo sobre a PRÓPRIA conta passava
55
+ * a ter sudo sobre a conta personificada (`startImpersonation`); e o sudo
56
+ * obtido ENQUANTO personificava voltava a valer sobre a conta do admin
57
+ * (`stopImpersonation`);
58
+ * - navegador compartilhado: A confirma sudo, faz logout, B loga, e B herda
59
+ * a graça de A (hoje mascarado porque `login()` chama `markSudo` logo em
60
+ * seguida — acidente, não desenho).
61
+ *
62
+ * A garantia vira ESTRUTURAL em vez de depender de alguém lembrar de limpar a
63
+ * marca em toda transição futura: qualquer troca de conta, inclusive as que
64
+ * ainda não existem, invalida o sudo sem tocar em código novo.
65
+ *
66
+ * Chave SEPARADA de propósito (mesma decisão de
67
+ * `CONFIRM_PASSKEY_CHALLENGE_ACCOUNT_KEY`): o valor de `SUDO_SESSION_KEY` é um
68
+ * número e isso é contratual — está pinado em
69
+ * `tests/host/account_confirm_controller.spec.ts` com `assert.isNumber`. Então
70
+ * a vinculação é ADITIVA, em vez de mudar a forma do valor pinado para
71
+ * `{ at, accountId }`. Bônus: as assinaturas públicas de `markSudo` /
72
+ * `isSudoActive` (exportadas em `index.ts`) ficam intactas.
73
+ */
74
+ export declare const SUDO_ACCOUNT_SESSION_KEY = "authkit_sudo_account";
75
+ /**
76
+ * Registra o timestamp de confirmação de sudo na sessão (NOW), VINCULADO à
77
+ * conta atual da sessão. Chamar após o usuário confirmar sua identidade.
78
+ *
79
+ * Sem conta na sessão não há a quem vincular: grava o timestamp (contrato
80
+ * pinado) e APAGA qualquer vinculação anterior, para que a marca não fique
81
+ * herdando o dono antigo. O resultado é uma marca órfã, que `isSudoActive`
82
+ * recusa (fail-closed).
83
+ *
84
+ * @deprecated Para conceder sudo, use `completeSudo(sudoContextFrom(ctx), id)`.
85
+ * `markSudo` grava a marca e nada mais: não registra o audit `sudo.confirmed`,
86
+ * não lembra o método usado e não redireciona para o `return_to`. Um host que a
87
+ * chame direto — o caminho natural no callback de `oidcStepUp`, antes de
88
+ * `completeSudo` ser público — concede privilégio sem deixar rastro nenhum.
89
+ * Continua exportada porque o login primário do próprio pacote a usa (ver o
90
+ * CHANGELOG desta versão) e porque removê-la seria breaking; o uso legítimo
91
+ * restante é ler/limpar a marca em fluxos que não são confirmação de
92
+ * identidade.
34
93
  */
35
94
  export declare function markSudo(ctx: HttpContext): void;
36
95
  /**
37
- * Verifica se o sudo está ativo (dentro da janela de graça).
96
+ * Verifica se o sudo está ativo: dentro da janela de graça E confirmado PELA
97
+ * CONTA que está logada agora.
98
+ *
99
+ * FAIL-CLOSED na vinculação (mesma postura do challenge de passkey): marca sem
100
+ * `accountId`, ou com um `accountId` que não bate com a conta atual da sessão,
101
+ * é recusada. Isso inclui sessões que já estavam vivas antes deste deploy — elas
102
+ * perdem o sudo e reconfirmam, que é o lado seguro do trade-off.
103
+ *
104
+ * Esta postura é o OPOSTO do fail-safe de `requireSudo`, e as duas estão certas:
105
+ * aqui a pergunta é "esta marca é minha?" (identidade), lá é "o toggle está
106
+ * ligado?" (disponibilidade). O docblock de `requireSudo` desenvolve a distinção.
38
107
  *
39
108
  * @returns `true` se o sudo está ativo (dentro da graça); `false` caso contrário.
40
109
  */
@@ -50,7 +119,48 @@ export declare function isSudoActive(ctx: HttpContext, graceMinutes: number): bo
50
119
  * if (result !== true) return result
51
120
  * ```
52
121
  *
53
- * FAIL-SAFE: qualquer erro settings → retorna `true` (deixa passar).
54
- * Quando `sudo_mode.enabled = false`, sempre retorna `true`.
122
+ * FAIL-SAFE: qualquer erro ao resolver a setting → retorna `true` (deixa
123
+ * passar). Quando `sudo_mode.enabled = false`, sempre retorna `true`.
124
+ *
125
+ * ---
126
+ *
127
+ * POR QUE ESTE FAIL-SAFE CONVIVE COM O FAIL-CLOSED DAS VINCULAÇÕES.
128
+ *
129
+ * Lendo este arquivo (e os vizinhos) aparecem duas posturas OPOSTAS diante de
130
+ * uma situação anômala, e isso é deliberado — não é um dos dois lados por
131
+ * consertar. Quem "harmonizar" as duas quebra alguma coisa:
132
+ *
133
+ * - FAIL-CLOSED nas vinculações à conta: a marca de sudo
134
+ * (`SUDO_ACCOUNT_SESSION_KEY` em `isSudoActive`), o challenge de passkey do
135
+ * confirm e o token pendente do magic link de sudo. Todas recusam quando a
136
+ * vinculação falta ou não bate.
137
+ * - FAIL-SAFE aqui, ao resolver a setting `sudo_mode`.
138
+ *
139
+ * A diferença não é de rigor, é de PERGUNTA:
140
+ *
141
+ * - As vinculações perguntam **"esta credencial é minha?"**. É IDENTIDADE. Uma
142
+ * resposta duvidosa é indistinguível de uma resposta negativa — uma marca
143
+ * sem dono pode ser a marca de outra conta que sobreviveu à troca de sessão.
144
+ * Deixar passar concede privilégio a quem talvez não o tenha, e o custo do
145
+ * erro é escalação. Identidade duvidosa é recusada; o usuário reconfirma,
146
+ * que é barato.
147
+ * - Este fail-safe pergunta **"o toggle de sudo mode está ligado?"**. É
148
+ * DISPONIBILIDADE, uma questão de configuração, e a resposta não diz nada
149
+ * sobre quem é o usuário. Quem chega aqui já passou pelo `accountGuard`:
150
+ * tem sessão de conta viva e autenticada.
151
+ *
152
+ * Inverter ESTE lado para fail-closed transformaria uma indisponibilidade do
153
+ * store de settings (BD fora do ar, timeout, migração em curso) num lockout
154
+ * TOTAL: todo mundo, simultaneamente, trancado fora da própria área de conta —
155
+ * inclusive fora dos caminhos de recuperação. E o ataque que isso evitaria
156
+ * exige que o atacante já tenha uma sessão autenticada da vítima E consiga
157
+ * derrubar o store de settings no mesmo instante. Trocar uma falha rara e
158
+ * condicionada por uma indisponibilidade certa e generalizada é o pior dos dois
159
+ * negócios.
160
+ *
161
+ * O fail-safe também é ESTREITO: ele só decide se a BARREIRA roda. Ele não
162
+ * concede sudo, não fabrica marca nenhuma e não mexe em vinculação. Se a setting
163
+ * resolve normalmente, a decisão volta inteira para `isSudoActive` — e lá a
164
+ * postura é fail-closed de novo.
55
165
  */
56
166
  export declare function requireSudo(ctx: HttpContext, settings: SettingsCapability | null): Promise<true | unknown>;
@@ -9,8 +9,11 @@
9
9
  * - `enabled`: habilita/desabilita o sudo mode. Default: true.
10
10
  * - `graceMinutes`: janela de graça em minutos. Default: 15.
11
11
  *
12
- * Chave de sessão: `authkit_sudo_at` (timestamp ms).
12
+ * Chaves de sessão: `authkit_sudo_at` (timestamp ms) + `authkit_sudo_account`
13
+ * (a conta que confirmou). As duas juntas são a marca — ver
14
+ * `SUDO_ACCOUNT_SESSION_KEY`.
13
15
  */
16
+ import { ACCOUNT_SESSION_KEY } from './middleware/account_auth.js';
14
17
  import { SETTING_KEYS } from './runtime_toggles.js';
15
18
  export const SUDO_MODE_DEFAULTS = {
16
19
  enabled: true,
@@ -18,6 +21,18 @@ export const SUDO_MODE_DEFAULTS = {
18
21
  };
19
22
  /**
20
23
  * Resolve a setting `sudo_mode` em runtime (fail-safe).
24
+ *
25
+ * FAIL-SAFE, e de propósito: setting ausente, malformada ou store indisponível
26
+ * caem em `SUDO_MODE_DEFAULTS` em vez de lançar. A pergunta que esta função
27
+ * responde é de DISPONIBILIDADE — "o toggle de sudo mode está ligado, e com que
28
+ * janela de graça?" —, não de identidade. Um erro aqui não é sinal de credencial
29
+ * suspeita; é o store de settings fora do ar.
30
+ *
31
+ * Note que o default é `enabled: true`: o fail-safe não desliga o sudo mode, ele
32
+ * mantém a política default LIGADA. Quem decide o que fazer com uma sessão sem
33
+ * sudo continua sendo `isSudoActive` (fail-CLOSED). Ver o docblock de
34
+ * `requireSudo` para a assimetria completa entre as duas posturas — ela é
35
+ * escolha, não descuido.
21
36
  */
22
37
  export async function resolveEffectiveSudoMode(settings) {
23
38
  try {
@@ -44,14 +59,74 @@ export async function resolveEffectiveSudoMode(settings) {
44
59
  /** Chave da sessão Adonis que registra quando o sudo foi confirmado. */
45
60
  export const SUDO_SESSION_KEY = 'authkit_sudo_at';
46
61
  /**
47
- * Registra o timestamp de confirmação de sudo na sessão (NOW).
48
- * Chamar após o usuário confirmar sua identidade (senha ou passkey).
62
+ * Conta que CONFIRMOU o sudo registrado em `SUDO_SESSION_KEY`.
63
+ *
64
+ * Por que existe: o timestamp sozinho é uma marca de "alguém confirmou a
65
+ * identidade nesta sessão há pouco" — sem dizer QUEM. E a sessão sobrevive à
66
+ * troca de conta: o `regenerate()` só troca o id do cookie e MIGRA os dados
67
+ * (é o invariante descrito no M6 de `account_session_controller.ts`). Sem esta
68
+ * vinculação, todo caminho que troca a conta da sessão TRANSFERE o sudo junto:
69
+ *
70
+ * - impersonation: um admin que confirmou sudo sobre a PRÓPRIA conta passava
71
+ * a ter sudo sobre a conta personificada (`startImpersonation`); e o sudo
72
+ * obtido ENQUANTO personificava voltava a valer sobre a conta do admin
73
+ * (`stopImpersonation`);
74
+ * - navegador compartilhado: A confirma sudo, faz logout, B loga, e B herda
75
+ * a graça de A (hoje mascarado porque `login()` chama `markSudo` logo em
76
+ * seguida — acidente, não desenho).
77
+ *
78
+ * A garantia vira ESTRUTURAL em vez de depender de alguém lembrar de limpar a
79
+ * marca em toda transição futura: qualquer troca de conta, inclusive as que
80
+ * ainda não existem, invalida o sudo sem tocar em código novo.
81
+ *
82
+ * Chave SEPARADA de propósito (mesma decisão de
83
+ * `CONFIRM_PASSKEY_CHALLENGE_ACCOUNT_KEY`): o valor de `SUDO_SESSION_KEY` é um
84
+ * número e isso é contratual — está pinado em
85
+ * `tests/host/account_confirm_controller.spec.ts` com `assert.isNumber`. Então
86
+ * a vinculação é ADITIVA, em vez de mudar a forma do valor pinado para
87
+ * `{ at, accountId }`. Bônus: as assinaturas públicas de `markSudo` /
88
+ * `isSudoActive` (exportadas em `index.ts`) ficam intactas.
89
+ */
90
+ export const SUDO_ACCOUNT_SESSION_KEY = 'authkit_sudo_account';
91
+ /**
92
+ * Registra o timestamp de confirmação de sudo na sessão (NOW), VINCULADO à
93
+ * conta atual da sessão. Chamar após o usuário confirmar sua identidade.
94
+ *
95
+ * Sem conta na sessão não há a quem vincular: grava o timestamp (contrato
96
+ * pinado) e APAGA qualquer vinculação anterior, para que a marca não fique
97
+ * herdando o dono antigo. O resultado é uma marca órfã, que `isSudoActive`
98
+ * recusa (fail-closed).
99
+ *
100
+ * @deprecated Para conceder sudo, use `completeSudo(sudoContextFrom(ctx), id)`.
101
+ * `markSudo` grava a marca e nada mais: não registra o audit `sudo.confirmed`,
102
+ * não lembra o método usado e não redireciona para o `return_to`. Um host que a
103
+ * chame direto — o caminho natural no callback de `oidcStepUp`, antes de
104
+ * `completeSudo` ser público — concede privilégio sem deixar rastro nenhum.
105
+ * Continua exportada porque o login primário do próprio pacote a usa (ver o
106
+ * CHANGELOG desta versão) e porque removê-la seria breaking; o uso legítimo
107
+ * restante é ler/limpar a marca em fluxos que não são confirmação de
108
+ * identidade.
49
109
  */
50
110
  export function markSudo(ctx) {
111
+ const accountId = ctx.session.get(ACCOUNT_SESSION_KEY);
51
112
  ctx.session.put(SUDO_SESSION_KEY, Date.now());
113
+ if (accountId)
114
+ ctx.session.put(SUDO_ACCOUNT_SESSION_KEY, accountId);
115
+ else
116
+ ctx.session.forget(SUDO_ACCOUNT_SESSION_KEY);
52
117
  }
53
118
  /**
54
- * Verifica se o sudo está ativo (dentro da janela de graça).
119
+ * Verifica se o sudo está ativo: dentro da janela de graça E confirmado PELA
120
+ * CONTA que está logada agora.
121
+ *
122
+ * FAIL-CLOSED na vinculação (mesma postura do challenge de passkey): marca sem
123
+ * `accountId`, ou com um `accountId` que não bate com a conta atual da sessão,
124
+ * é recusada. Isso inclui sessões que já estavam vivas antes deste deploy — elas
125
+ * perdem o sudo e reconfirmam, que é o lado seguro do trade-off.
126
+ *
127
+ * Esta postura é o OPOSTO do fail-safe de `requireSudo`, e as duas estão certas:
128
+ * aqui a pergunta é "esta marca é minha?" (identidade), lá é "o toggle está
129
+ * ligado?" (disponibilidade). O docblock de `requireSudo` desenvolve a distinção.
55
130
  *
56
131
  * @returns `true` se o sudo está ativo (dentro da graça); `false` caso contrário.
57
132
  */
@@ -59,6 +134,14 @@ export function isSudoActive(ctx, graceMinutes) {
59
134
  const sudoAt = ctx.session.get(SUDO_SESSION_KEY);
60
135
  if (!sudoAt)
61
136
  return false;
137
+ // A vinculação é checada ANTES da graça: uma marca de outra conta não é
138
+ // "sudo expirado", é sudo que nunca valeu aqui.
139
+ const sudoAccountId = ctx.session.get(SUDO_ACCOUNT_SESSION_KEY);
140
+ if (!sudoAccountId)
141
+ return false;
142
+ const currentAccountId = ctx.session.get(ACCOUNT_SESSION_KEY);
143
+ if (!currentAccountId || sudoAccountId !== currentAccountId)
144
+ return false;
62
145
  const graceMs = graceMinutes * 60 * 1000;
63
146
  return Date.now() - sudoAt <= graceMs;
64
147
  }
@@ -73,8 +156,49 @@ export function isSudoActive(ctx, graceMinutes) {
73
156
  * if (result !== true) return result
74
157
  * ```
75
158
  *
76
- * FAIL-SAFE: qualquer erro settings → retorna `true` (deixa passar).
77
- * Quando `sudo_mode.enabled = false`, sempre retorna `true`.
159
+ * FAIL-SAFE: qualquer erro ao resolver a setting → retorna `true` (deixa
160
+ * passar). Quando `sudo_mode.enabled = false`, sempre retorna `true`.
161
+ *
162
+ * ---
163
+ *
164
+ * POR QUE ESTE FAIL-SAFE CONVIVE COM O FAIL-CLOSED DAS VINCULAÇÕES.
165
+ *
166
+ * Lendo este arquivo (e os vizinhos) aparecem duas posturas OPOSTAS diante de
167
+ * uma situação anômala, e isso é deliberado — não é um dos dois lados por
168
+ * consertar. Quem "harmonizar" as duas quebra alguma coisa:
169
+ *
170
+ * - FAIL-CLOSED nas vinculações à conta: a marca de sudo
171
+ * (`SUDO_ACCOUNT_SESSION_KEY` em `isSudoActive`), o challenge de passkey do
172
+ * confirm e o token pendente do magic link de sudo. Todas recusam quando a
173
+ * vinculação falta ou não bate.
174
+ * - FAIL-SAFE aqui, ao resolver a setting `sudo_mode`.
175
+ *
176
+ * A diferença não é de rigor, é de PERGUNTA:
177
+ *
178
+ * - As vinculações perguntam **"esta credencial é minha?"**. É IDENTIDADE. Uma
179
+ * resposta duvidosa é indistinguível de uma resposta negativa — uma marca
180
+ * sem dono pode ser a marca de outra conta que sobreviveu à troca de sessão.
181
+ * Deixar passar concede privilégio a quem talvez não o tenha, e o custo do
182
+ * erro é escalação. Identidade duvidosa é recusada; o usuário reconfirma,
183
+ * que é barato.
184
+ * - Este fail-safe pergunta **"o toggle de sudo mode está ligado?"**. É
185
+ * DISPONIBILIDADE, uma questão de configuração, e a resposta não diz nada
186
+ * sobre quem é o usuário. Quem chega aqui já passou pelo `accountGuard`:
187
+ * tem sessão de conta viva e autenticada.
188
+ *
189
+ * Inverter ESTE lado para fail-closed transformaria uma indisponibilidade do
190
+ * store de settings (BD fora do ar, timeout, migração em curso) num lockout
191
+ * TOTAL: todo mundo, simultaneamente, trancado fora da própria área de conta —
192
+ * inclusive fora dos caminhos de recuperação. E o ataque que isso evitaria
193
+ * exige que o atacante já tenha uma sessão autenticada da vítima E consiga
194
+ * derrubar o store de settings no mesmo instante. Trocar uma falha rara e
195
+ * condicionada por uma indisponibilidade certa e generalizada é o pior dos dois
196
+ * negócios.
197
+ *
198
+ * O fail-safe também é ESTREITO: ele só decide se a BARREIRA roda. Ele não
199
+ * concede sudo, não fabrica marca nenhuma e não mexe em vinculação. Se a setting
200
+ * resolve normalmente, a decisão volta inteira para `isSudoActive` — e lá a
201
+ * postura é fail-closed de novo.
78
202
  */
79
203
  export async function requireSudo(ctx, settings) {
80
204
  try {
@@ -85,7 +209,9 @@ export async function requireSudo(ctx, settings) {
85
209
  return true;
86
210
  }
87
211
  catch {
88
- // FAIL-SAFE: erro ao resolver a setting → deixa passar.
212
+ // FAIL-SAFE: erro ao resolver a setting → deixa passar. Disponibilidade, não
213
+ // identidade — ver o docblock acima para o porquê de isto NÃO contradizer o
214
+ // fail-closed de `isSudoActive`.
89
215
  return true;
90
216
  }
91
217
  // Fora da graça: redireciona para confirmação.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adonis-agora/authkit-server",
3
- "version": "0.45.0",
3
+ "version": "0.46.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",
@@ -100,6 +100,7 @@
100
100
  }
101
101
  },
102
102
  "dependencies": {
103
+ "@simplewebauthn/browser": "^13.3.0",
103
104
  "@simplewebauthn/server": "^13.3.1",
104
105
  "jose": "^5.9.0",
105
106
  "koa": "^3.2.1",
@@ -130,6 +131,7 @@
130
131
  "@types/qrcode": "^1.5.5",
131
132
  "better-sqlite3": "^11.0.0",
132
133
  "edge.js": "6.5.1",
134
+ "esbuild": "0.25.12",
133
135
  "ioredis": "5.4.2",
134
136
  "ioredis-mock": "^8.9.0",
135
137
  "luxon": "3.7.2",
@@ -149,8 +151,10 @@
149
151
  "@adonis-agora/authkit-react": "0.16.0"
150
152
  },
151
153
  "scripts": {
152
- "build": "node scripts/build_host_css.mjs && node scripts/build_ui.mjs && tsc && 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')\"",
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')\"",
153
155
  "build:ui": "node scripts/build_ui.mjs",
156
+ "build:webauthn": "node scripts/build_webauthn.mjs",
157
+ "check:webauthn-bundle": "node scripts/check_webauthn_bundle.mjs",
154
158
  "typecheck": "tsc --noEmit && node scripts/typecheck_ui.mjs",
155
159
  "test": "node --import=@poppinss/ts-exec bin/test.ts",
156
160
  "build:host-css": "node scripts/build_host_css.mjs"
@@ -1,13 +0,0 @@
1
- {{{
2
- exports({ to: app.makePath('resources/views/authkit/consent.edge') })
3
- }}}
4
- <!doctype html>
5
- <html lang="pt-br"><head><meta charset="utf-8"><title>Autorizar</title>
6
- <script src="https://cdn.tailwindcss.com"></script></head>
7
- <body class="min-h-screen flex items-center justify-center bg-gray-50">
8
- <form method="POST" action="/auth/interaction/{{ uid }}/consent" class="w-full max-w-sm bg-white p-6 rounded-lg shadow">
9
- <h1 class="text-lg font-semibold mb-2">Autorizar acesso</h1>
10
- <p class="text-sm text-gray-600 mb-4">O app <strong>{{ params.client_id }}</strong> quer acessar sua conta.</p>
11
- <button class="w-full bg-black text-white rounded py-2">Autorizar</button>
12
- </form>
13
- </body></html>
@@ -1,19 +0,0 @@
1
- {{{
2
- exports({ to: app.makePath('resources/views/authkit/login.edge') })
3
- }}}
4
- <!doctype html>
5
- <html lang="pt-br"><head><meta charset="utf-8"><title>Entrar</title>
6
- <script src="https://cdn.tailwindcss.com"></script></head>
7
- <body class="min-h-screen flex items-center justify-center bg-gray-50">
8
- <form method="POST" action="/auth/interaction/{{ uid }}/login" class="w-full max-w-sm bg-white p-6 rounded-lg shadow">
9
- <h1 class="text-lg font-semibold mb-4">Entrar</h1>
10
- @if(error)
11
- <p class="text-red-600 text-sm mb-3">{{ error }}</p>
12
- @end
13
- <label class="block text-sm mb-1">E-mail</label>
14
- <input name="email" type="email" required class="w-full border rounded px-3 py-2 mb-3" />
15
- <label class="block text-sm mb-1">Senha</label>
16
- <input name="password" type="password" required class="w-full border rounded px-3 py-2 mb-4" />
17
- <button class="w-full bg-black text-white rounded py-2">Entrar</button>
18
- </form>
19
- </body></html>
@@ -1,13 +0,0 @@
1
- {{{
2
- exports({ to: app.makePath('resources/views/authkit/consent.edge') })
3
- }}}
4
- <!doctype html>
5
- <html lang="pt-br"><head><meta charset="utf-8"><title>Autorizar</title>
6
- <script src="https://cdn.tailwindcss.com"></script></head>
7
- <body class="min-h-screen flex items-center justify-center bg-gray-50">
8
- <form method="POST" action="/auth/interaction/{{ uid }}/consent" class="w-full max-w-sm bg-white p-6 rounded-lg shadow">
9
- <h1 class="text-lg font-semibold mb-2">Autorizar acesso</h1>
10
- <p class="text-sm text-gray-600 mb-4">O app <strong>{{ params.client_id }}</strong> quer acessar sua conta.</p>
11
- <button class="w-full bg-black text-white rounded py-2">Autorizar</button>
12
- </form>
13
- </body></html>
@@ -1,19 +0,0 @@
1
- {{{
2
- exports({ to: app.makePath('resources/views/authkit/login.edge') })
3
- }}}
4
- <!doctype html>
5
- <html lang="pt-br"><head><meta charset="utf-8"><title>Entrar</title>
6
- <script src="https://cdn.tailwindcss.com"></script></head>
7
- <body class="min-h-screen flex items-center justify-center bg-gray-50">
8
- <form method="POST" action="/auth/interaction/{{ uid }}/login" class="w-full max-w-sm bg-white p-6 rounded-lg shadow">
9
- <h1 class="text-lg font-semibold mb-4">Entrar</h1>
10
- @if(error)
11
- <p class="text-red-600 text-sm mb-3">{{ error }}</p>
12
- @end
13
- <label class="block text-sm mb-1">E-mail</label>
14
- <input name="email" type="email" required class="w-full border rounded px-3 py-2 mb-3" />
15
- <label class="block text-sm mb-1">Senha</label>
16
- <input name="password" type="password" required class="w-full border rounded px-3 py-2 mb-4" />
17
- <button class="w-full bg-black text-white rounded py-2">Entrar</button>
18
- </form>
19
- </body></html>