@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.
- package/build/commands/ui_preset.js +15 -1
- package/build/host/views/account/confirm.edge +118 -52
- package/build/host/views/account/mfa.edge +1 -1
- package/build/host/views/login.edge +2 -4
- package/build/host/views/mfa-challenge.edge +1 -1
- package/build/host/views/partials/styles.edge +1 -1
- package/build/index.d.ts +5 -1
- package/build/index.js +15 -1
- package/build/src/define_config.d.ts +51 -0
- package/build/src/define_config.js +8 -0
- package/build/src/host/assets/webauthn.js +2 -0
- package/build/src/host/controllers/account_confirm_controller.d.ts +11 -14
- package/build/src/host/controllers/account_confirm_controller.js +42 -130
- package/build/src/host/controllers/account_session_controller.js +20 -4
- package/build/src/host/controllers/webauthn_asset_controller.d.ts +22 -0
- package/build/src/host/controllers/webauthn_asset_controller.js +66 -0
- package/build/src/host/i18n.d.ts +14 -0
- package/build/src/host/i18n.js +16 -0
- package/build/src/host/impersonation_session.js +15 -0
- package/build/src/host/rate_limit.d.ts +10 -0
- package/build/src/host/rate_limit.js +6 -0
- package/build/src/host/register_auth_host.d.ts +19 -0
- package/build/src/host/register_auth_host.js +72 -4
- package/build/src/host/sudo/index.d.ts +47 -0
- package/build/src/host/sudo/index.js +41 -0
- package/build/src/host/sudo/methods/magic_link.d.ts +43 -0
- package/build/src/host/sudo/methods/magic_link.js +174 -0
- package/build/src/host/sudo/methods/oidc_step_up.d.ts +68 -0
- package/build/src/host/sudo/methods/oidc_step_up.js +78 -0
- package/build/src/host/sudo/methods/passkey.d.ts +28 -0
- package/build/src/host/sudo/methods/passkey.js +139 -0
- package/build/src/host/sudo/methods/password.d.ts +19 -0
- package/build/src/host/sudo/methods/password.js +93 -0
- package/build/src/host/sudo/runtime.d.ts +141 -0
- package/build/src/host/sudo/runtime.js +327 -0
- package/build/src/host/sudo/types.d.ts +93 -0
- package/build/src/host/sudo/types.js +1 -0
- package/build/src/host/sudo_mode.d.ts +116 -6
- package/build/src/host/sudo_mode.js +133 -7
- package/package.json +6 -2
- package/build/stubs/ui/edge/views/consent.edge +0 -13
- package/build/stubs/ui/edge/views/login.edge +0 -19
- package/stubs/ui/edge/views/consent.edge +0 -13
- package/stubs/ui/edge/views/login.edge +0 -19
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { isSudoMethodEnabled } from '../runtime.js';
|
|
2
|
+
/**
|
|
3
|
+
* Confirmação por senha — o método histórico.
|
|
4
|
+
*
|
|
5
|
+
* URL LEGADA: registra `POST /account/confirm` (não `/account/confirm/password`)
|
|
6
|
+
* porque hosts externos e telas customizadas (fora deste pacote) dependem
|
|
7
|
+
* desse path histórico — trocá-lo quebraria essas integrações sem que o
|
|
8
|
+
* pacote tenha como saber ou migrar por elas. É também a razão de `register`
|
|
9
|
+
* receber o router cru em vez de o runtime montar por convenção a partir do
|
|
10
|
+
* `id`.
|
|
11
|
+
*
|
|
12
|
+
* LIMITAÇÃO CONHECIDA de `isAvailable`: ele responde "a conta tem hash?", não
|
|
13
|
+
* "o usuário conhece a senha?". Host que cria contas passwordless gravando um
|
|
14
|
+
* hash aleatório para satisfazer uma coluna NOT NULL verá este método como
|
|
15
|
+
* disponível e mostrará um campo que ninguém consegue preencher. De dentro do
|
|
16
|
+
* pacote, hash aleatório e hash real são indistinguíveis; a correção é do host
|
|
17
|
+
* (coluna nullable) ou basta omitir `password()` da lista de `methods`.
|
|
18
|
+
*/
|
|
19
|
+
export function password() {
|
|
20
|
+
return {
|
|
21
|
+
id: 'password',
|
|
22
|
+
/**
|
|
23
|
+
* Fallback conservador herdado de `isPasswordless` (o original em
|
|
24
|
+
* `account_confirm_controller.ts`): sem informação, assumimos que a conta
|
|
25
|
+
* TEM senha.
|
|
26
|
+
*
|
|
27
|
+
* `__getRawRow` não faz parte do contrato público de `AccountStore` — é um
|
|
28
|
+
* escape hatch interno do store Lucid (mesmo cast usado em outros pontos do
|
|
29
|
+
* pacote, ex.: account_security_controller.ts). Um `AccountStore` do SPI
|
|
30
|
+
* (o público-alvo deste método) não é obrigado a implementá-lo. Se
|
|
31
|
+
* tratássemos "não sei" como indisponível, um store customizado sem esse
|
|
32
|
+
* escape hatch perderia o método `password` mesmo com `verifyCredentials`
|
|
33
|
+
* funcionando — e, sem passkey também, o usuário fica sem nenhum método e
|
|
34
|
+
* travado fora do sudo mode. Por isso os três casos abaixo:
|
|
35
|
+
*
|
|
36
|
+
* - `__getRawRow` ausente (`undefined`) → disponível (não sabemos, não escondemos).
|
|
37
|
+
* - Presente e devolve hash não-vazio → disponível.
|
|
38
|
+
* - Presente e devolve hash vazio/nulo (ou linha nula) → indisponível (sabemos que não há senha).
|
|
39
|
+
* - Lança exceção → disponível (mesmo espírito conservador: não sabemos).
|
|
40
|
+
*
|
|
41
|
+
* Esconder o método é sempre pior que mostrá-lo: uma opção visível que
|
|
42
|
+
* falha é recuperável (o usuário tenta e escolhe outra); uma opção
|
|
43
|
+
* escondida não é nem descobrível.
|
|
44
|
+
*/
|
|
45
|
+
async isAvailable(c) {
|
|
46
|
+
const getRawRow = c.cfg.accountStore.__getRawRow;
|
|
47
|
+
if (typeof getRawRow !== 'function')
|
|
48
|
+
return true;
|
|
49
|
+
try {
|
|
50
|
+
const row = await getRawRow.call(c.cfg.accountStore, c.accountId);
|
|
51
|
+
// Store expõe a função e respondeu (linha nula ou hash vazio/nulo): sabemos
|
|
52
|
+
// que não há senha → indisponível.
|
|
53
|
+
return Boolean(row?.password);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
async describe() {
|
|
60
|
+
return {
|
|
61
|
+
labelKey: 'account.confirm.method.password',
|
|
62
|
+
kind: 'form',
|
|
63
|
+
endpoint: '/account/confirm',
|
|
64
|
+
fields: [
|
|
65
|
+
{ name: 'password', type: 'password', labelKey: 'account.confirm.password_label' },
|
|
66
|
+
],
|
|
67
|
+
};
|
|
68
|
+
},
|
|
69
|
+
register(router, h) {
|
|
70
|
+
router.post('/account/confirm', async (ctx) => {
|
|
71
|
+
const c = await h.contextFrom(ctx);
|
|
72
|
+
// ANTES de qualquer verificação: o host desligou este método?
|
|
73
|
+
// A rota é montada incondicionalmente (decisão de tempo de registro),
|
|
74
|
+
// então `config.sudo.methods` só desabilita de fato se o handler
|
|
75
|
+
// recusar. Responde `fail` — o mesmo redirect+flash de uma senha
|
|
76
|
+
// errada — em vez de 404: assim a resposta não distingue "método
|
|
77
|
+
// desligado" de "senha incorreta" e não vaza a config do host.
|
|
78
|
+
if (!isSudoMethodEnabled(c.cfg, 'password'))
|
|
79
|
+
return h.fail(c, 'account.confirm.error');
|
|
80
|
+
const { password: submitted } = ctx.request.only(['password']);
|
|
81
|
+
// `c.account` é nullable (sessão viva de conta apagada → findById null)
|
|
82
|
+
// e `email` pode vir vazio de um store customizado; sem e-mail não há
|
|
83
|
+
// como verificar credenciais.
|
|
84
|
+
if (!submitted || !c.account || !c.account.email)
|
|
85
|
+
return h.fail(c, 'account.confirm.error');
|
|
86
|
+
const ok = await c.cfg.accountStore.verifyCredentials(c.account.email, submitted);
|
|
87
|
+
if (!ok)
|
|
88
|
+
return h.fail(c, 'account.confirm.error');
|
|
89
|
+
return h.completeSudo(c, 'password');
|
|
90
|
+
});
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import type { HttpContext, Router } from '@adonisjs/core/http';
|
|
2
|
+
import type { ResolvedServerConfig } from '../../define_config.js';
|
|
3
|
+
import type { SudoContext, SudoMethod, SudoRouteHelpers } from './types.js';
|
|
4
|
+
/** Último método usado com sucesso — só ordena a tela, não restringe nada. */
|
|
5
|
+
export declare const LAST_METHOD_SESSION_KEY = "authkit_sudo_last_method";
|
|
6
|
+
/**
|
|
7
|
+
* Monta o `SudoContext` a partir do `HttpContext`.
|
|
8
|
+
*
|
|
9
|
+
* Mora AQUI, e não no controller, porque é o construtor canônico do contexto
|
|
10
|
+
* que `completeSudo`/`fail` recebem e que todo `SudoMethod` usa — é runtime do
|
|
11
|
+
* SPI, não detalhe da tela. Enquanto morava em `controllers/`, o barrel do SPI
|
|
12
|
+
* reexportava de lá só para esconder isso, e o ciclo
|
|
13
|
+
* `runtime → controller → runtime` era o que obrigava `configuredSudoMethods` a
|
|
14
|
+
* viver longe de `isSudoMethodEnabled` — origem do drift entre os dois lados.
|
|
15
|
+
*/
|
|
16
|
+
export declare function sudoContextFrom(ctx: HttpContext): Promise<SudoContext>;
|
|
17
|
+
/**
|
|
18
|
+
* Registra a lista montada. Chamado UMA vez por `registerAuthHost`, e
|
|
19
|
+
* SUBSTITUI (não acumula): registrar o host de novo é redefinir o que existe,
|
|
20
|
+
* não somar ao que existia.
|
|
21
|
+
*/
|
|
22
|
+
export declare function setMountedSudoMethods(methods: SudoMethod[]): void;
|
|
23
|
+
/** Um método com este id teve rotas montadas? Usado só para avisar de drift. */
|
|
24
|
+
export declare function isSudoMethodMounted(methodId: string): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Lista de métodos que o host configurou EXPLICITAMENTE, ou `null` quando ele
|
|
27
|
+
* não configurou nada (ausente ou vazio → "não restringi").
|
|
28
|
+
*
|
|
29
|
+
* Ponto ÚNICO de leitura de `config.sudo.methods`. Existe aqui (e não no
|
|
30
|
+
* controller) porque quem precisa dela são os dois lados — a tela, que decide o
|
|
31
|
+
* que OFERECER, e os handlers dos métodos, que decidem o que ACEITAR — e o
|
|
32
|
+
* controller já importa os métodos built-in, o que tornaria a dependência
|
|
33
|
+
* circular se os métodos importassem de volta o controller.
|
|
34
|
+
*/
|
|
35
|
+
export declare function explicitSudoMethods(cfg: ResolvedServerConfig): SudoMethod[] | null;
|
|
36
|
+
/**
|
|
37
|
+
* O método `methodId` está habilitado para ESTE host?
|
|
38
|
+
*
|
|
39
|
+
* Toda rota registrada por um `SudoMethod` DEVE começar por aqui. Sem essa
|
|
40
|
+
* checagem, `config.sudo.methods` só esconderia o método da tela: o endpoint
|
|
41
|
+
* continuaria vivo e concedendo sudo — uma config que aparenta restringir e não
|
|
42
|
+
* restringe é pior que nenhuma config.
|
|
43
|
+
*
|
|
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.
|
|
47
|
+
*/
|
|
48
|
+
export declare function isSudoMethodEnabled(cfg: ResolvedServerConfig, methodId: string): boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Lista efetiva de métodos da TELA — o irmão de `isSudoMethodEnabled`, e por
|
|
51
|
+
* isso mora coladinho nele.
|
|
52
|
+
*
|
|
53
|
+
* Sem config explícita cai na lista MONTADA, exatamente a mesma resposta que
|
|
54
|
+
* `isSudoMethodEnabled` dá do lado dos handlers ("vale o que tem rota"). Antes
|
|
55
|
+
* caía numa lista de defaults hardcoded, e os dois lados divergiam de verdade:
|
|
56
|
+
* um host que fizesse só
|
|
57
|
+
*
|
|
58
|
+
* ```ts
|
|
59
|
+
* registerAuthHost(router, { sudoMethods: [sudoMethods.magicLink()] })
|
|
60
|
+
* ```
|
|
61
|
+
*
|
|
62
|
+
* montava só magic-link, mas a tela oferecia password + passkey (ambos 404) e
|
|
63
|
+
* NÃO oferecia magic-link, que funcionava. Caindo na lista montada, o drift
|
|
64
|
+
* fica estruturalmente impossível no caso sem config: é literalmente a mesma
|
|
65
|
+
* lista. O aviso de flag-drift do controller passa a valer só para o caso que
|
|
66
|
+
* sobra — config explícita divergindo do que foi montado.
|
|
67
|
+
*/
|
|
68
|
+
export declare function configuredSudoMethods(cfg: ResolvedServerConfig): SudoMethod[];
|
|
69
|
+
/**
|
|
70
|
+
* Envelopa o router entregue a `method.register` para que TODO handler que ele
|
|
71
|
+
* registrar seja barrado quando `config.sudo.methods` não incluir o método.
|
|
72
|
+
*
|
|
73
|
+
* POR QUE NO PONTO DE REGISTRO, e não dentro de cada handler: a barreira é a
|
|
74
|
+
* diferença entre uma config que restringe e uma que só esconde a opção da tela
|
|
75
|
+
* enquanto o endpoint segue concedendo sudo (falha Critical). Deixá-la a cargo
|
|
76
|
+
* de quem escreve o método significa que o PRIMEIRO método que esquecer a
|
|
77
|
+
* chamada reabre a falha — e nada detecta. Aqui a garantia é estrutural: o
|
|
78
|
+
* método não tem como registrar uma rota desguardada, porque não é ele quem
|
|
79
|
+
* segura o router.
|
|
80
|
+
*
|
|
81
|
+
* Os built-in CONTINUAM checando por dentro, e isso não é redundância inútil:
|
|
82
|
+
* cada um recusa na forma que o seu endpoint exige (o `passkey/options` é XHR e
|
|
83
|
+
* devolve JSON 404; um 302 para HTML quebraria o cliente). Este envelope é o
|
|
84
|
+
* piso genérico — recusa com `fail`, o mesmo redirect+flash de um erro comum,
|
|
85
|
+
* que não distingue "método desligado" de "credencial errada" e portanto não
|
|
86
|
+
* vaza a config do host.
|
|
87
|
+
*
|
|
88
|
+
* O QUE NÃO CABE NA BARREIRA É RECUSADO, não tolerado: um handler que não seja
|
|
89
|
+
* função (tupla `[Controller, 'metodo']`) e os atalhos `resource()`/
|
|
90
|
+
* `shallowResource()` lançam no ponto de registro. Ver
|
|
91
|
+
* `ROUTER_CONTROLLER_SHORTCUTS` e o `assertWrappableHandler` abaixo.
|
|
92
|
+
* (`on()` continua passando: ele registra redirect/render estático, sem handler
|
|
93
|
+
* que possa alcançar `completeSudo`.)
|
|
94
|
+
*
|
|
95
|
+
* CUSTO: resolve o config do container antes do handler. Não usa
|
|
96
|
+
* `contextFrom` no caminho feliz de propósito — aquilo faz `findById`, e pagar
|
|
97
|
+
* uma leitura de conta a mais em toda rota de sudo para uma checagem que só lê
|
|
98
|
+
* a config seria desperdício. O contexto completo só é montado para recusar.
|
|
99
|
+
*/
|
|
100
|
+
export declare function guardSudoRoutes(router: Router, methodId: string, h: SudoRouteHelpers,
|
|
101
|
+
/**
|
|
102
|
+
* Aplica o throttle do host a CADA rota que o método registrar (no-op quando
|
|
103
|
+
* o rate-limit está desligado). É `registerAuthHost` quem passa isto, porque
|
|
104
|
+
* é lá que os throttles existem — e é aqui que a rota nasce.
|
|
105
|
+
*
|
|
106
|
+
* O throttle que chega aqui é o do bucket de SUDO, não o de login. Mesmos
|
|
107
|
+
* limites, contagem separada: login mede um anônimo adivinhando credenciais,
|
|
108
|
+
* sudo mede um usuário JÁ autenticado reprovando a própria identidade.
|
|
109
|
+
* Compartilhando o bucket, quem erra a senha no `/account/confirm` gastava o
|
|
110
|
+
* orçamento de login do próprio IP — e um ataque de credencial no login
|
|
111
|
+
* trancava a confirmação de quem está legitimamente logado atrás do mesmo
|
|
112
|
+
* NAT. Ver `ResolvedRateLimitConfig.sudo`.
|
|
113
|
+
*
|
|
114
|
+
* Fica no wrapper, e não no contrato de `SudoRouteHelpers`, porque throttle
|
|
115
|
+
* não é decisão do método: um método que pudesse pedir throttle poderia
|
|
116
|
+
* também NÃO pedir, e o `POST` que emite o magic link de sudo — que dispara
|
|
117
|
+
* um e-mail por chamada — voltaria a ficar sem nenhum. Aqui a cobertura é da
|
|
118
|
+
* mesma natureza da barreira de `config.sudo.methods`: estrutural, sem
|
|
119
|
+
* depender de quem escreve o método lembrar de nada.
|
|
120
|
+
*/
|
|
121
|
+
applyThrottle?: (route: unknown) => void): Router;
|
|
122
|
+
/**
|
|
123
|
+
* ÚNICO ponto do pacote que concede sudo. Nenhum `SudoMethod` chama
|
|
124
|
+
* `markSudo` diretamente: o método decide se verificou, o runtime concede,
|
|
125
|
+
* audita e redireciona.
|
|
126
|
+
*/
|
|
127
|
+
export declare function completeSudo(c: SudoContext, methodId: string): Promise<unknown>;
|
|
128
|
+
/**
|
|
129
|
+
* Falha de confirmação: flash + volta pro /account/confirm preservando o
|
|
130
|
+
* destino. Substitui a coreografia que estava duplicada cinco vezes no
|
|
131
|
+
* controller.
|
|
132
|
+
*/
|
|
133
|
+
export declare function fail(c: SudoContext, messageKey: string): Promise<unknown>;
|
|
134
|
+
/**
|
|
135
|
+
* Filtra os métodos disponíveis para esta conta e promove o último usado.
|
|
136
|
+
*
|
|
137
|
+
* `isAvailable` que lança NÃO derruba a tela: um método quebrado não pode
|
|
138
|
+
* trancar o usuário fora dos outros — mesmo espírito do FAIL-SAFE de
|
|
139
|
+
* `requireSudo`.
|
|
140
|
+
*/
|
|
141
|
+
export declare function resolveAvailableMethods(c: SudoContext, methods: SudoMethod[]): Promise<SudoMethod[]>;
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import { markSudo } from '../sudo_mode.js';
|
|
2
|
+
import { accountHome } from '../account_home.js';
|
|
3
|
+
import { translate } from '../i18n.js';
|
|
4
|
+
import { ACCOUNT_SESSION_KEY } from '../middleware/account_auth.js';
|
|
5
|
+
import { validateReturnTo } from '../controllers/account_session_controller.js';
|
|
6
|
+
/** Último método usado com sucesso — só ordena a tela, não restringe nada. */
|
|
7
|
+
export const LAST_METHOD_SESSION_KEY = 'authkit_sudo_last_method';
|
|
8
|
+
/**
|
|
9
|
+
* Monta o `SudoContext` a partir do `HttpContext`.
|
|
10
|
+
*
|
|
11
|
+
* Mora AQUI, e não no controller, porque é o construtor canônico do contexto
|
|
12
|
+
* que `completeSudo`/`fail` recebem e que todo `SudoMethod` usa — é runtime do
|
|
13
|
+
* SPI, não detalhe da tela. Enquanto morava em `controllers/`, o barrel do SPI
|
|
14
|
+
* reexportava de lá só para esconder isso, e o ciclo
|
|
15
|
+
* `runtime → controller → runtime` era o que obrigava `configuredSudoMethods` a
|
|
16
|
+
* viver longe de `isSudoMethodEnabled` — origem do drift entre os dois lados.
|
|
17
|
+
*/
|
|
18
|
+
export async function sudoContextFrom(ctx) {
|
|
19
|
+
const service = await ctx.containerResolver.make('authkit.server');
|
|
20
|
+
const cfg = service.config;
|
|
21
|
+
const accountId = ctx.session.get(ACCOUNT_SESSION_KEY);
|
|
22
|
+
const account = await cfg.accountStore.findById(accountId);
|
|
23
|
+
// PRECEDÊNCIA do return_to. Num GET a query string é a única fonte real. Num
|
|
24
|
+
// POST o alvo do redirect vem do campo hidden do form: deixar a query string
|
|
25
|
+
// vencer permitiria a um link `?return_to=...` sequestrar o destino de um form
|
|
26
|
+
// que o usuário já preencheu — e seria uma mudança silenciosa de um alvo de
|
|
27
|
+
// redirect em relação ao comportamento histórico (`request.input`, que no
|
|
28
|
+
// Adonis já dá precedência ao corpo). `validateReturnTo` roda nos dois casos.
|
|
29
|
+
const fromBody = ctx.request.input?.('return_to');
|
|
30
|
+
const fromQuery = ctx.request.qs?.()?.return_to;
|
|
31
|
+
const isPost = String(ctx.request.method?.() ?? '').toUpperCase() === 'POST';
|
|
32
|
+
const raw = isPost ? (fromBody ?? fromQuery) : (fromQuery ?? fromBody);
|
|
33
|
+
return { ctx, cfg, accountId, account, returnTo: validateReturnTo(raw) };
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Métodos cujas rotas FORAM montadas por `registerAuthHost`, na ordem em que
|
|
37
|
+
* ele as montou. Guarda os OBJETOS, não só os ids, porque esta lista é também
|
|
38
|
+
* a lista efetiva da TELA quando o host não configura `config.sudo.methods` —
|
|
39
|
+
* ver `configuredSudoMethods`.
|
|
40
|
+
*/
|
|
41
|
+
const mountedSudoMethods = [];
|
|
42
|
+
/**
|
|
43
|
+
* Registra a lista montada. Chamado UMA vez por `registerAuthHost`, e
|
|
44
|
+
* SUBSTITUI (não acumula): registrar o host de novo é redefinir o que existe,
|
|
45
|
+
* não somar ao que existia.
|
|
46
|
+
*/
|
|
47
|
+
export function setMountedSudoMethods(methods) {
|
|
48
|
+
mountedSudoMethods.splice(0, mountedSudoMethods.length, ...methods);
|
|
49
|
+
}
|
|
50
|
+
/** Um método com este id teve rotas montadas? Usado só para avisar de drift. */
|
|
51
|
+
export function isSudoMethodMounted(methodId) {
|
|
52
|
+
return mountedSudoMethods.some((m) => m?.id === methodId);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Lista de métodos que o host configurou EXPLICITAMENTE, ou `null` quando ele
|
|
56
|
+
* não configurou nada (ausente ou vazio → "não restringi").
|
|
57
|
+
*
|
|
58
|
+
* Ponto ÚNICO de leitura de `config.sudo.methods`. Existe aqui (e não no
|
|
59
|
+
* controller) porque quem precisa dela são os dois lados — a tela, que decide o
|
|
60
|
+
* que OFERECER, e os handlers dos métodos, que decidem o que ACEITAR — e o
|
|
61
|
+
* controller já importa os métodos built-in, o que tornaria a dependência
|
|
62
|
+
* circular se os métodos importassem de volta o controller.
|
|
63
|
+
*/
|
|
64
|
+
export function explicitSudoMethods(cfg) {
|
|
65
|
+
const configured = cfg?.sudo?.methods;
|
|
66
|
+
return Array.isArray(configured) && configured.length ? configured : null;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* O método `methodId` está habilitado para ESTE host?
|
|
70
|
+
*
|
|
71
|
+
* Toda rota registrada por um `SudoMethod` DEVE começar por aqui. Sem essa
|
|
72
|
+
* checagem, `config.sudo.methods` só esconderia o método da tela: o endpoint
|
|
73
|
+
* continuaria vivo e concedendo sudo — uma config que aparenta restringir e não
|
|
74
|
+
* restringe é pior que nenhuma config.
|
|
75
|
+
*
|
|
76
|
+
* Sem configuração explícita nada foi restringido: vale o que tem rota montada.
|
|
77
|
+
* Isso é deliberado — a lista de defaults não é a fonte de verdade do que está
|
|
78
|
+
* montado, e tratá-la como tal derrubaria um método customizado do host.
|
|
79
|
+
*/
|
|
80
|
+
export function isSudoMethodEnabled(cfg, methodId) {
|
|
81
|
+
const explicit = explicitSudoMethods(cfg);
|
|
82
|
+
if (explicit === null)
|
|
83
|
+
return true;
|
|
84
|
+
return explicit.some((m) => m?.id === methodId);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Lista efetiva de métodos da TELA — o irmão de `isSudoMethodEnabled`, e por
|
|
88
|
+
* isso mora coladinho nele.
|
|
89
|
+
*
|
|
90
|
+
* Sem config explícita cai na lista MONTADA, exatamente a mesma resposta que
|
|
91
|
+
* `isSudoMethodEnabled` dá do lado dos handlers ("vale o que tem rota"). Antes
|
|
92
|
+
* caía numa lista de defaults hardcoded, e os dois lados divergiam de verdade:
|
|
93
|
+
* um host que fizesse só
|
|
94
|
+
*
|
|
95
|
+
* ```ts
|
|
96
|
+
* registerAuthHost(router, { sudoMethods: [sudoMethods.magicLink()] })
|
|
97
|
+
* ```
|
|
98
|
+
*
|
|
99
|
+
* montava só magic-link, mas a tela oferecia password + passkey (ambos 404) e
|
|
100
|
+
* NÃO oferecia magic-link, que funcionava. Caindo na lista montada, o drift
|
|
101
|
+
* fica estruturalmente impossível no caso sem config: é literalmente a mesma
|
|
102
|
+
* lista. O aviso de flag-drift do controller passa a valer só para o caso que
|
|
103
|
+
* sobra — config explícita divergindo do que foi montado.
|
|
104
|
+
*/
|
|
105
|
+
export function configuredSudoMethods(cfg) {
|
|
106
|
+
return explicitSudoMethods(cfg) ?? mountedSudoMethods;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Verbos HTTP com a forma `(pattern, handler, ...)` — o handler é o SEGUNDO
|
|
110
|
+
* argumento.
|
|
111
|
+
*
|
|
112
|
+
* `route` NÃO entra aqui: sua assinatura é `(pattern, methods, handler)`, com o
|
|
113
|
+
* handler no TERCEIRO argumento, e por isso tem tratamento próprio no Proxy.
|
|
114
|
+
* Deixá-lo de fora da barreira seria um bypass silencioso — `router.route()`
|
|
115
|
+
* registra rota igual a qualquer verbo.
|
|
116
|
+
*/
|
|
117
|
+
const ROUTER_VERBS = ['get', 'post', 'put', 'patch', 'delete', 'any'];
|
|
118
|
+
/**
|
|
119
|
+
* Atalhos do `Router` que registram rotas SEM receber um handler-função: eles
|
|
120
|
+
* expandem um CONTROLLER em N rotas por convenção. Não há função para embrulhar
|
|
121
|
+
* — a barreira não tem por onde entrar —, então são recusados no boot.
|
|
122
|
+
*
|
|
123
|
+
* Ver `assertWrappableHandler` para o porquê de recusar em vez de deixar passar.
|
|
124
|
+
*/
|
|
125
|
+
const ROUTER_CONTROLLER_SHORTCUTS = ['resource', 'shallowResource'];
|
|
126
|
+
/**
|
|
127
|
+
* Envelopa o router entregue a `method.register` para que TODO handler que ele
|
|
128
|
+
* registrar seja barrado quando `config.sudo.methods` não incluir o método.
|
|
129
|
+
*
|
|
130
|
+
* POR QUE NO PONTO DE REGISTRO, e não dentro de cada handler: a barreira é a
|
|
131
|
+
* diferença entre uma config que restringe e uma que só esconde a opção da tela
|
|
132
|
+
* enquanto o endpoint segue concedendo sudo (falha Critical). Deixá-la a cargo
|
|
133
|
+
* de quem escreve o método significa que o PRIMEIRO método que esquecer a
|
|
134
|
+
* chamada reabre a falha — e nada detecta. Aqui a garantia é estrutural: o
|
|
135
|
+
* método não tem como registrar uma rota desguardada, porque não é ele quem
|
|
136
|
+
* segura o router.
|
|
137
|
+
*
|
|
138
|
+
* Os built-in CONTINUAM checando por dentro, e isso não é redundância inútil:
|
|
139
|
+
* cada um recusa na forma que o seu endpoint exige (o `passkey/options` é XHR e
|
|
140
|
+
* devolve JSON 404; um 302 para HTML quebraria o cliente). Este envelope é o
|
|
141
|
+
* piso genérico — recusa com `fail`, o mesmo redirect+flash de um erro comum,
|
|
142
|
+
* que não distingue "método desligado" de "credencial errada" e portanto não
|
|
143
|
+
* vaza a config do host.
|
|
144
|
+
*
|
|
145
|
+
* O QUE NÃO CABE NA BARREIRA É RECUSADO, não tolerado: um handler que não seja
|
|
146
|
+
* função (tupla `[Controller, 'metodo']`) e os atalhos `resource()`/
|
|
147
|
+
* `shallowResource()` lançam no ponto de registro. Ver
|
|
148
|
+
* `ROUTER_CONTROLLER_SHORTCUTS` e o `assertWrappableHandler` abaixo.
|
|
149
|
+
* (`on()` continua passando: ele registra redirect/render estático, sem handler
|
|
150
|
+
* que possa alcançar `completeSudo`.)
|
|
151
|
+
*
|
|
152
|
+
* CUSTO: resolve o config do container antes do handler. Não usa
|
|
153
|
+
* `contextFrom` no caminho feliz de propósito — aquilo faz `findById`, e pagar
|
|
154
|
+
* uma leitura de conta a mais em toda rota de sudo para uma checagem que só lê
|
|
155
|
+
* a config seria desperdício. O contexto completo só é montado para recusar.
|
|
156
|
+
*/
|
|
157
|
+
export function guardSudoRoutes(router, methodId, h,
|
|
158
|
+
/**
|
|
159
|
+
* Aplica o throttle do host a CADA rota que o método registrar (no-op quando
|
|
160
|
+
* o rate-limit está desligado). É `registerAuthHost` quem passa isto, porque
|
|
161
|
+
* é lá que os throttles existem — e é aqui que a rota nasce.
|
|
162
|
+
*
|
|
163
|
+
* O throttle que chega aqui é o do bucket de SUDO, não o de login. Mesmos
|
|
164
|
+
* limites, contagem separada: login mede um anônimo adivinhando credenciais,
|
|
165
|
+
* sudo mede um usuário JÁ autenticado reprovando a própria identidade.
|
|
166
|
+
* Compartilhando o bucket, quem erra a senha no `/account/confirm` gastava o
|
|
167
|
+
* orçamento de login do próprio IP — e um ataque de credencial no login
|
|
168
|
+
* trancava a confirmação de quem está legitimamente logado atrás do mesmo
|
|
169
|
+
* NAT. Ver `ResolvedRateLimitConfig.sudo`.
|
|
170
|
+
*
|
|
171
|
+
* Fica no wrapper, e não no contrato de `SudoRouteHelpers`, porque throttle
|
|
172
|
+
* não é decisão do método: um método que pudesse pedir throttle poderia
|
|
173
|
+
* também NÃO pedir, e o `POST` que emite o magic link de sudo — que dispara
|
|
174
|
+
* um e-mail por chamada — voltaria a ficar sem nenhum. Aqui a cobertura é da
|
|
175
|
+
* mesma natureza da barreira de `config.sudo.methods`: estrutural, sem
|
|
176
|
+
* depender de quem escreve o método lembrar de nada.
|
|
177
|
+
*/
|
|
178
|
+
applyThrottle) {
|
|
179
|
+
const wrap = (handler) => async (ctx) => {
|
|
180
|
+
const service = await ctx.containerResolver.make('authkit.server');
|
|
181
|
+
if (isSudoMethodEnabled(service.config, methodId))
|
|
182
|
+
return handler(ctx);
|
|
183
|
+
const c = await h.contextFrom(ctx);
|
|
184
|
+
return h.fail(c, 'account.confirm.error');
|
|
185
|
+
};
|
|
186
|
+
/**
|
|
187
|
+
* A barreira só sabe embrulhar handler-FUNÇÃO. Qualquer outra forma
|
|
188
|
+
* (`[Controller, 'method']`, string `'Controller.method'`) registra uma rota
|
|
189
|
+
* que `config.sudo.methods` não desabilita e que alcança o `completeSudo`
|
|
190
|
+
* público — um bypass silencioso, no exato ponto onde a barreira devia ser
|
|
191
|
+
* estrutural.
|
|
192
|
+
*
|
|
193
|
+
* Por isso LANÇA, em vez de deixar passar. Antes o argumento era "nenhum
|
|
194
|
+
* built-in registra assim" — mas isso é propriedade dos built-in, não da
|
|
195
|
+
* barreira, e o público-alvo do SPI é justamente quem não é built-in.
|
|
196
|
+
*
|
|
197
|
+
* É boot-time e é alto: o host descobre no primeiro `node ace serve`, não em
|
|
198
|
+
* produção com uma rota de sudo desguardada. A saída é registrar um wrapper
|
|
199
|
+
* de uma linha (`(ctx) => new Ctrl().metodo(ctx)`), que passa pela barreira.
|
|
200
|
+
*/
|
|
201
|
+
const assertWrappableHandler = (verb, pattern, handler) => {
|
|
202
|
+
if (typeof handler === 'function')
|
|
203
|
+
return handler;
|
|
204
|
+
throw new Error(`authkit: o método de sudo "${methodId}" registrou "${verb} ${pattern}" com um handler ` +
|
|
205
|
+
`que não é função (${handler === undefined ? 'undefined' : typeof handler}). ` +
|
|
206
|
+
'Métodos de sudo precisam registrar handler-função para receber a barreira de ' +
|
|
207
|
+
'`config.sudo.methods` — uma tupla `[Controller, \'metodo\']` registraria uma rota que ' +
|
|
208
|
+
'a config não desabilita e que alcança `completeSudo`. Envolva o controller numa ' +
|
|
209
|
+
"função: `router.post(pattern, (ctx) => new Controller().metodo(ctx))`.");
|
|
210
|
+
};
|
|
211
|
+
const wrapIfFn = (verb, pattern, handler) => wrap(assertWrappableHandler(verb, pattern, handler));
|
|
212
|
+
return new Proxy(router, {
|
|
213
|
+
// `receiver` é DELIBERADAMENTE `target`, não o Proxy: o `Router` do Adonis é
|
|
214
|
+
// uma classe com campos privados (`#app`, `#globalMatchers`, `#pushToRoutes`),
|
|
215
|
+
// e ler/chamar um membro com `this` apontando para o Proxy lança
|
|
216
|
+
// `TypeError: Cannot read private member #app`.
|
|
217
|
+
get(target, prop) {
|
|
218
|
+
const original = Reflect.get(target, prop, target);
|
|
219
|
+
if (typeof original !== 'function')
|
|
220
|
+
return original;
|
|
221
|
+
// Throttle na rota recém-criada. O `Route` real é preservado e devolvido,
|
|
222
|
+
// então `.as()`/`.use()` seguem encadeando do lado de fora.
|
|
223
|
+
const throttled = (route) => {
|
|
224
|
+
applyThrottle?.(route);
|
|
225
|
+
return route;
|
|
226
|
+
};
|
|
227
|
+
// `route(pattern, methods, handler)` — handler no TERCEIRO argumento.
|
|
228
|
+
if (prop === 'route') {
|
|
229
|
+
return (pattern, methods, handler, ...rest) => throttled(original.call(target, pattern, methods, wrapIfFn('route', pattern, handler), ...rest));
|
|
230
|
+
}
|
|
231
|
+
if (ROUTER_VERBS.includes(prop)) {
|
|
232
|
+
return (pattern, handler, ...rest) => throttled(original.call(target, pattern, wrapIfFn(String(prop).toUpperCase(), pattern, handler), ...rest));
|
|
233
|
+
}
|
|
234
|
+
// `resource()`/`shallowResource()` expandem um controller em N rotas por
|
|
235
|
+
// convenção: não passa handler-função por lugar nenhum, e a barreira não
|
|
236
|
+
// teria o que embrulhar. Recusa no boot pela mesma razão da tupla — deixar
|
|
237
|
+
// passar seria registrar rotas de sudo que `config.sudo.methods` não
|
|
238
|
+
// desabilita.
|
|
239
|
+
if (ROUTER_CONTROLLER_SHORTCUTS.includes(prop)) {
|
|
240
|
+
return (pattern) => {
|
|
241
|
+
throw new Error(`authkit: o método de sudo "${methodId}" chamou router.${String(prop)}("${pattern}"), ` +
|
|
242
|
+
'que registra rotas a partir de um controller e portanto não pode receber a ' +
|
|
243
|
+
'barreira de `config.sudo.methods`. Registre as rotas do método uma a uma, com ' +
|
|
244
|
+
'handler-função.');
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
// Todo o RESTO da API do `Router` (`group`, `on`, `where`, `use`, ...)
|
|
248
|
+
// segue funcionando, ligado ao router real. Sem o
|
|
249
|
+
// `bind`, um `router.group(() => ...)` — uso perfeitamente legítimo do
|
|
250
|
+
// tipo `Router` que `SudoMethod.register` declara receber — rodaria com
|
|
251
|
+
// `this === Proxy` e explodiria no boot da aplicação.
|
|
252
|
+
//
|
|
253
|
+
// `original.call(target, ...)` nos verbos e o `bind` aqui preservam o
|
|
254
|
+
// RETORNO real (`Route`/`RouteGroup`), então `.as()`/`.use()`/
|
|
255
|
+
// `.middleware()`/`.prefix()` continuam encadeando normalmente.
|
|
256
|
+
return original.bind(target);
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* ÚNICO ponto do pacote que concede sudo. Nenhum `SudoMethod` chama
|
|
262
|
+
* `markSudo` diretamente: o método decide se verificou, o runtime concede,
|
|
263
|
+
* audita e redireciona.
|
|
264
|
+
*/
|
|
265
|
+
export async function completeSudo(c, methodId) {
|
|
266
|
+
// SEGUNDA barreira estrutural, irmã de `guardSudoRoutes`: aquela cobre
|
|
267
|
+
// "o método está habilitado?", esta cobre "a conta existe?".
|
|
268
|
+
//
|
|
269
|
+
// `sudoContextFrom` deixa `account: null` quando `findById` não acha nada —
|
|
270
|
+
// sessão viva de conta apagada ou anonimizada. Cada built-in já checa por
|
|
271
|
+
// dentro, mas depender disso é a mesma falha que `guardSudoRoutes` existe
|
|
272
|
+
// para eliminar do outro lado: o PRIMEIRO método desatento que fizer
|
|
273
|
+
// `contextFrom` → `completeSudo` concederia sudo sobre uma conta que não
|
|
274
|
+
// existe mais, e nada detectaria. A garantia tem de estar aqui, no único
|
|
275
|
+
// ponto de concessão.
|
|
276
|
+
// Falsy, não `=== null`: um contexto montado à mão que simplesmente omita
|
|
277
|
+
// `account` não pode escapar da barreira por um detalhe de forma.
|
|
278
|
+
if (!c.account)
|
|
279
|
+
return fail(c, 'account.confirm.error');
|
|
280
|
+
markSudo(c.ctx);
|
|
281
|
+
c.ctx.session.put(LAST_METHOD_SESSION_KEY, methodId);
|
|
282
|
+
await c.cfg.audit?.record({
|
|
283
|
+
type: 'sudo.confirmed',
|
|
284
|
+
accountId: c.accountId,
|
|
285
|
+
ip: c.ctx.request.ip?.() ?? null,
|
|
286
|
+
metadata: { method: methodId },
|
|
287
|
+
});
|
|
288
|
+
return c.ctx.response.redirect(c.returnTo ?? accountHome(c.cfg));
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Falha de confirmação: flash + volta pro /account/confirm preservando o
|
|
292
|
+
* destino. Substitui a coreografia que estava duplicada cinco vezes no
|
|
293
|
+
* controller.
|
|
294
|
+
*/
|
|
295
|
+
export async function fail(c, messageKey) {
|
|
296
|
+
c.ctx.session.flash('confirmError', translate(c.cfg.messages, messageKey));
|
|
297
|
+
const qs = c.returnTo ? `?return_to=${encodeURIComponent(c.returnTo)}` : '';
|
|
298
|
+
return c.ctx.response.redirect(`/account/confirm${qs}`);
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Filtra os métodos disponíveis para esta conta e promove o último usado.
|
|
302
|
+
*
|
|
303
|
+
* `isAvailable` que lança NÃO derruba a tela: um método quebrado não pode
|
|
304
|
+
* trancar o usuário fora dos outros — mesmo espírito do FAIL-SAFE de
|
|
305
|
+
* `requireSudo`.
|
|
306
|
+
*/
|
|
307
|
+
export async function resolveAvailableMethods(c, methods) {
|
|
308
|
+
const checked = await Promise.all(methods.map(async (m) => {
|
|
309
|
+
try {
|
|
310
|
+
return (await m.isAvailable(c)) ? m : null;
|
|
311
|
+
}
|
|
312
|
+
catch (error) {
|
|
313
|
+
// Fail-safe: um `isAvailable` quebrado não pode trancar o usuário fora
|
|
314
|
+
// dos outros métodos — mas precisa deixar rastro, senão um typo vira
|
|
315
|
+
// um método que some da tela em produção sem ninguém saber por quê.
|
|
316
|
+
// `?.` defensivo: em teste, `fakeSudoContext` pode não ter logger.
|
|
317
|
+
c.ctx.logger?.warn({ method: m.id, err: error }, `authkit: isAvailable() do método de sudo "${m.id}" lançou — método omitido da lista`);
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
}));
|
|
321
|
+
const available = checked.filter((m) => m !== null);
|
|
322
|
+
const last = c.ctx.session.get(LAST_METHOD_SESSION_KEY);
|
|
323
|
+
if (!last)
|
|
324
|
+
return available;
|
|
325
|
+
const preferred = available.filter((m) => m.id === last);
|
|
326
|
+
return preferred.length ? [...preferred, ...available.filter((m) => m.id !== last)] : available;
|
|
327
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { HttpContext, Router } from '@adonisjs/core/http';
|
|
2
|
+
import type { ResolvedServerConfig } from '../../define_config.js';
|
|
3
|
+
/** Contexto entregue a todo método de sudo. */
|
|
4
|
+
export interface SudoContext {
|
|
5
|
+
ctx: HttpContext;
|
|
6
|
+
/**
|
|
7
|
+
* Conta logada no console — carregada com `accountStore.findById`.
|
|
8
|
+
*
|
|
9
|
+
* PODE ser null. O comentário original dizia "nunca null: o accountGuard já
|
|
10
|
+
* rodou", mas o `accountGuard` (register_auth_host.ts:91) só verifica que a
|
|
11
|
+
* CHAVE de sessão existe; ele nunca carrega a conta. Sessão viva de conta
|
|
12
|
+
* apagada/anonimizada → `findById` devolve null. Todo método deve tratar.
|
|
13
|
+
*
|
|
14
|
+
* `email` é `string` (não `string | null`) porque é o que o `AccountStore`
|
|
15
|
+
* de fato garante: `findById` devolve `AuthAccount`, cujo `email: string`
|
|
16
|
+
* (accounts/account_store.ts:4). A nulidade real está no objeto, não no campo.
|
|
17
|
+
*/
|
|
18
|
+
account: {
|
|
19
|
+
id: string;
|
|
20
|
+
email: string;
|
|
21
|
+
} | null;
|
|
22
|
+
accountId: string;
|
|
23
|
+
/** Config resolvida do authkit (accountStore, messages, audit, mail...). */
|
|
24
|
+
cfg: ResolvedServerConfig;
|
|
25
|
+
/** Destino pós-confirmação, já validado — só caminhos internos. */
|
|
26
|
+
returnTo: string | null;
|
|
27
|
+
}
|
|
28
|
+
/** Como a tela deve renderizar o passo deste método. */
|
|
29
|
+
export interface SudoMethodDescriptor {
|
|
30
|
+
/** Chave i18n do rótulo. Ex.: 'account.confirm.method.magic_link'. */
|
|
31
|
+
labelKey: string;
|
|
32
|
+
/**
|
|
33
|
+
* 'form' — a tela renderiza `fields` e dá POST em `endpoint`.
|
|
34
|
+
* 'action' — a tela dá POST em `endpoint` sem input.
|
|
35
|
+
* 'redirect' — a tela manda o usuário para `endpoint` (fluxo externo).
|
|
36
|
+
* 'webauthn' — a tela precisa RODAR o handshake WebAuthn antes de postar:
|
|
37
|
+
* pede as options em `${endpoint}/options`, chama
|
|
38
|
+
* `navigator.credentials.get` (via `@simplewebauthn/browser`) e
|
|
39
|
+
* posta a assertion serializada no campo `response` de
|
|
40
|
+
* `endpoint`.
|
|
41
|
+
*
|
|
42
|
+
* `'webauthn'` existe como KIND próprio, e não como um caso especial do id
|
|
43
|
+
* `passkey`, justamente para a tela não voltar a conhecer método nenhum pelo
|
|
44
|
+
* nome: o endpoint de options é DERIVADO do descritor. Qualquer método do SPI
|
|
45
|
+
* que implemente o mesmo par `POST <endpoint>/options` + `POST <endpoint>`
|
|
46
|
+
* ganha a tela embutida de graça.
|
|
47
|
+
*
|
|
48
|
+
* Um método `'webauthn'` NÃO é utilizável sem JavaScript. Renderizá-lo como
|
|
49
|
+
* um form de submit direto manda `response` vazio e o handler recusa sempre —
|
|
50
|
+
* foi exatamente essa a regressão que motivou este kind.
|
|
51
|
+
*/
|
|
52
|
+
kind: 'form' | 'action' | 'redirect' | 'webauthn';
|
|
53
|
+
endpoint: string;
|
|
54
|
+
fields?: Array<{
|
|
55
|
+
name: string;
|
|
56
|
+
type: 'password' | 'text';
|
|
57
|
+
labelKey: string;
|
|
58
|
+
}>;
|
|
59
|
+
}
|
|
60
|
+
/** Helpers que o runtime entrega às rotas de um método. */
|
|
61
|
+
export interface SudoRouteHelpers {
|
|
62
|
+
/** Monta o SudoContext a partir do HttpContext (resolve config, conta, returnTo). */
|
|
63
|
+
contextFrom(ctx: HttpContext): Promise<SudoContext>;
|
|
64
|
+
/** ÚNICO ponto de concessão de sudo no pacote. */
|
|
65
|
+
completeSudo(c: SudoContext, methodId: string): Promise<unknown>;
|
|
66
|
+
/** Flash de erro + volta pro /account/confirm preservando return_to. */
|
|
67
|
+
fail(c: SudoContext, messageKey: string): Promise<unknown>;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Método de confirmação de identidade (sudo mode).
|
|
71
|
+
*
|
|
72
|
+
* REGRA CENTRAL: um método NUNCA chama `markSudo`. Ele decide apenas SE
|
|
73
|
+
* verificou; conceder é do runtime, via `completeSudo`. `markSudo` é a
|
|
74
|
+
* concessão de privilégio — espalhá-la por N métodos multiplicaria por N as
|
|
75
|
+
* chances de alguém conceder sem ter verificado.
|
|
76
|
+
*/
|
|
77
|
+
export interface SudoMethod {
|
|
78
|
+
/** Estável. Vai no audit (`metadata.method`) e na preferência lembrada. */
|
|
79
|
+
readonly id: string;
|
|
80
|
+
/** Disponível para ESTA conta? Ex.: passkey só se houver passkey cadastrada. */
|
|
81
|
+
isAvailable(c: SudoContext): Promise<boolean>;
|
|
82
|
+
/** O que a tela mostra para este método. */
|
|
83
|
+
describe(c: SudoContext): Promise<SudoMethodDescriptor>;
|
|
84
|
+
/**
|
|
85
|
+
* Endpoints próprios. Opcional: métodos puramente 'redirect' (oidcStepUp)
|
|
86
|
+
* não registram nada, porque o fluxo sai do pacote.
|
|
87
|
+
*
|
|
88
|
+
* Recebe o router cru (não monta por convenção a partir do `id`) porque
|
|
89
|
+
* `password` e `passkey` precisam manter URLs legadas que uma convenção
|
|
90
|
+
* não comportaria.
|
|
91
|
+
*/
|
|
92
|
+
register?(router: Router, h: SudoRouteHelpers): void;
|
|
93
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|