@adonis-agora/authkit-server 0.54.0 → 0.55.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/index.d.ts +4 -2
- package/build/index.js +2 -1
- package/build/providers/authkit_server_provider.js +18 -0
- package/build/src/accounts/lucid_account_store.d.ts +35 -0
- package/build/src/accounts/lucid_account_store.js +9 -0
- package/build/src/accounts/lucid_store/core.js +88 -19
- package/build/src/accounts/lucid_store/shared.d.ts +14 -0
- package/build/src/accounts/lucid_store/token_hash.d.ts +79 -0
- package/build/src/accounts/lucid_store/token_hash.js +145 -0
- package/build/src/define_config.d.ts +93 -8
- package/build/src/define_config.js +28 -2
- package/build/src/host/account_api/account_api_controller.js +5 -4
- package/build/src/host/admin_api/admin_users_service.js +2 -1
- package/build/src/host/admin_api/api_orgs_controller.js +2 -1
- package/build/src/host/admin_console/console_impersonation_controller.d.ts +15 -1
- package/build/src/host/admin_console/console_impersonation_controller.js +26 -2
- package/build/src/host/admin_console/console_orgs_controller.js +3 -1
- package/build/src/host/admin_validators.d.ts +3 -3
- package/build/src/host/auth_host_config.d.ts +30 -0
- package/build/src/host/auth_host_config.js +15 -0
- package/build/src/host/config_locks.d.ts +29 -0
- package/build/src/host/config_locks.js +46 -0
- package/build/src/host/console_session.d.ts +38 -2
- package/build/src/host/console_session.js +46 -2
- package/build/src/host/controllers/account_mfa_controller.js +5 -5
- package/build/src/host/controllers/account_orgs_controller.js +2 -1
- package/build/src/host/controllers/account_security_controller.js +6 -5
- package/build/src/host/controllers/interaction_controller.js +122 -26
- package/build/src/host/controllers/registration_controller.js +5 -4
- package/build/src/host/controllers/social_controller.js +37 -0
- package/build/src/host/default_mailer.d.ts +36 -5
- package/build/src/host/default_mailer.js +63 -10
- package/build/src/host/i18n.d.ts +10 -0
- package/build/src/host/i18n.js +16 -0
- package/build/src/host/login_attempt.d.ts +88 -2
- package/build/src/host/login_attempt.js +101 -48
- package/build/src/host/login_notify.js +2 -2
- package/build/src/host/oidc_rp_guard.d.ts +25 -2
- package/build/src/host/oidc_rp_guard.js +55 -9
- package/build/src/host/origin.d.ts +21 -0
- package/build/src/host/origin.js +22 -0
- package/build/src/host/register_auth_host.d.ts +104 -3
- package/build/src/host/register_auth_host.js +222 -26
- package/build/src/host/runtime_settings.d.ts +16 -0
- package/build/src/host/runtime_settings.js +24 -0
- package/build/src/host/runtime_toggles.d.ts +10 -0
- package/build/src/host/runtime_toggles.js +4 -0
- package/build/src/host/security_notice_service.d.ts +4 -2
- package/build/src/host/security_notice_service.js +4 -2
- package/build/src/host/sudo/index.d.ts +8 -0
- package/build/src/host/sudo/index.js +8 -0
- package/build/src/host/sudo/methods/magic_link.d.ts +17 -3
- package/build/src/host/sudo/methods/magic_link.js +37 -13
- package/build/src/host/sudo/runtime.d.ts +44 -4
- package/build/src/host/sudo/runtime.js +90 -6
- package/build/src/host/sudo/satisfiability.d.ts +62 -0
- package/build/src/host/sudo/satisfiability.js +89 -0
- package/build/src/password/common_passwords.js +27 -7
- package/build/src/provider/oidc_service.js +30 -13
- package/build/src/provider/token_exchange.d.ts +25 -1
- package/build/src/provider/token_exchange.js +29 -2
- package/package.json +6 -3
- /package/build/{password → src/password}/common_passwords.txt +0 -0
|
@@ -179,3 +179,19 @@ export declare class RuntimeSettings implements SettingsCapability {
|
|
|
179
179
|
* ```
|
|
180
180
|
*/
|
|
181
181
|
export declare function resolveRuntimeSettings(ctx: HttpContext): Promise<RuntimeSettings | null>;
|
|
182
|
+
/**
|
|
183
|
+
* Variante NON-NULL de {@link resolveRuntimeSettings}: quando a resolução falha
|
|
184
|
+
* (DB ausente, serviço não registrado), devolve um {@link RuntimeSettings} no-op
|
|
185
|
+
* cujo probe de tabela lança — logo `hasTable()` vira false e TODA leitura
|
|
186
|
+
* retorna null, fazendo os resolvers caírem no config estático.
|
|
187
|
+
*
|
|
188
|
+
* Use esta quando o chamador precisa passar um `SettingsCapability` non-null
|
|
189
|
+
* adiante (é o caso dos gates de login, que só rodam as checagens de expiração
|
|
190
|
+
* quando recebem `settings`). Use {@link resolveRuntimeSettings} quando o `null`
|
|
191
|
+
* carrega semântica própria (ex.: responder `capability_unsupported`).
|
|
192
|
+
*
|
|
193
|
+
* Vive AQUI, e não em um controller, porque é sobre runtime settings — o
|
|
194
|
+
* `interaction_controller` e o `social_controller` a compartilham; uma segunda
|
|
195
|
+
* cópia da degradação no-op é como as duas divergem.
|
|
196
|
+
*/
|
|
197
|
+
export declare function resolveRuntimeSettingsOrNoop(ctx: HttpContext): Promise<RuntimeSettings>;
|
|
@@ -305,3 +305,27 @@ export async function resolveRuntimeSettings(ctx) {
|
|
|
305
305
|
return null;
|
|
306
306
|
}
|
|
307
307
|
}
|
|
308
|
+
/**
|
|
309
|
+
* Variante NON-NULL de {@link resolveRuntimeSettings}: quando a resolução falha
|
|
310
|
+
* (DB ausente, serviço não registrado), devolve um {@link RuntimeSettings} no-op
|
|
311
|
+
* cujo probe de tabela lança — logo `hasTable()` vira false e TODA leitura
|
|
312
|
+
* retorna null, fazendo os resolvers caírem no config estático.
|
|
313
|
+
*
|
|
314
|
+
* Use esta quando o chamador precisa passar um `SettingsCapability` non-null
|
|
315
|
+
* adiante (é o caso dos gates de login, que só rodam as checagens de expiração
|
|
316
|
+
* quando recebem `settings`). Use {@link resolveRuntimeSettings} quando o `null`
|
|
317
|
+
* carrega semântica própria (ex.: responder `capability_unsupported`).
|
|
318
|
+
*
|
|
319
|
+
* Vive AQUI, e não em um controller, porque é sobre runtime settings — o
|
|
320
|
+
* `interaction_controller` e o `social_controller` a compartilham; uma segunda
|
|
321
|
+
* cópia da degradação no-op é como as duas divergem.
|
|
322
|
+
*/
|
|
323
|
+
export async function resolveRuntimeSettingsOrNoop(ctx) {
|
|
324
|
+
const rs = await resolveRuntimeSettings(ctx);
|
|
325
|
+
return (rs ??
|
|
326
|
+
new RuntimeSettings({
|
|
327
|
+
table: () => {
|
|
328
|
+
throw new Error('no-op');
|
|
329
|
+
},
|
|
330
|
+
}));
|
|
331
|
+
}
|
|
@@ -659,6 +659,12 @@ export declare function resolveEffectiveTokenTtl(settings: SettingsCapability, c
|
|
|
659
659
|
*
|
|
660
660
|
* Controla se o painel de impersonation (RFC 8693 token exchange) é exibido no
|
|
661
661
|
* console admin. FALLBACK: campo ausente cai em `config.admin.impersonation`.
|
|
662
|
+
*
|
|
663
|
+
* ESCOPO — governa o PAINEL, não o GRANT. Registrar o grant RFC 8693 no provider
|
|
664
|
+
* OIDC é decisão de boot de `config.admin.impersonation`; uma setting de runtime
|
|
665
|
+
* não desregistra rota que nunca foi registrada. Consumida por
|
|
666
|
+
* `host/admin_console/console_impersonation_controller.ts`, que checa o gate de
|
|
667
|
+
* config ANTES desta setting — logo o runtime só APERTA, nunca AFROUXA.
|
|
662
668
|
*/
|
|
663
669
|
export interface AdminImpersonationSetting {
|
|
664
670
|
enabled?: boolean;
|
|
@@ -672,6 +678,10 @@ export interface ResolvedAdminImpersonationSetting {
|
|
|
672
678
|
*
|
|
673
679
|
* Precedência: setting BD → configDefault → lib default (false).
|
|
674
680
|
* FAIL-SAFE: qualquer erro → configDefault.
|
|
681
|
+
*
|
|
682
|
+
* Quando `admin.impersonation` foi declarado no `defineConfig`, a key fica
|
|
683
|
+
* travada e `settings.getSetting` devolve null (ver `host/config_locks.ts`) —
|
|
684
|
+
* então este resolver cai no `configDefault` sozinho. Config > runtime.
|
|
675
685
|
*/
|
|
676
686
|
export declare function resolveEffectiveAdminImpersonation(settings: SettingsCapability, configDefault?: boolean): Promise<ResolvedAdminImpersonationSetting>;
|
|
677
687
|
/**
|
|
@@ -633,6 +633,10 @@ export async function resolveEffectiveTokenTtl(settings, configDefault = {}) {
|
|
|
633
633
|
*
|
|
634
634
|
* Precedência: setting BD → configDefault → lib default (false).
|
|
635
635
|
* FAIL-SAFE: qualquer erro → configDefault.
|
|
636
|
+
*
|
|
637
|
+
* Quando `admin.impersonation` foi declarado no `defineConfig`, a key fica
|
|
638
|
+
* travada e `settings.getSetting` devolve null (ver `host/config_locks.ts`) —
|
|
639
|
+
* então este resolver cai no `configDefault` sozinho. Config > runtime.
|
|
636
640
|
*/
|
|
637
641
|
export async function resolveEffectiveAdminImpersonation(settings, configDefault = false) {
|
|
638
642
|
const defaults = { enabled: configDefault };
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import type { HttpContext } from '@adonisjs/core/http';
|
|
10
10
|
import type { AuditSink } from '../audit/audit_sink.js';
|
|
11
|
-
import type { MailHooks } from '../define_config.js';
|
|
11
|
+
import type { MailHooks, ResolvedServerConfig } from '../define_config.js';
|
|
12
12
|
import type { SecurityNotificationKind } from './runtime_toggles.js';
|
|
13
13
|
/**
|
|
14
14
|
* Contexto para disparo de uma notificação de segurança.
|
|
@@ -32,5 +32,7 @@ export interface SecurityNoticeContext {
|
|
|
32
32
|
* @param notice Contexto da notificação
|
|
33
33
|
* @param mailHooks Hooks de mail do config (opcional)
|
|
34
34
|
* @param audit Sink de auditoria (opcional)
|
|
35
|
+
* @param cfg Config resolvido — só usado para derivar o origin do link
|
|
36
|
+
* do e-mail default (`authkitOrigin`); nunca do `request.host()`.
|
|
35
37
|
*/
|
|
36
|
-
export declare function dispatchSecurityNotice(ctx: HttpContext, notice: SecurityNoticeContext, mailHooks: Pick<MailHooks, 'onSecurityNotice'> | undefined, audit: AuditSink | undefined): Promise<void>;
|
|
38
|
+
export declare function dispatchSecurityNotice(ctx: HttpContext, notice: SecurityNoticeContext, mailHooks: Pick<MailHooks, 'onSecurityNotice'> | undefined, audit: AuditSink | undefined, cfg: ResolvedServerConfig): Promise<void>;
|
|
@@ -17,8 +17,10 @@ import { resolveEffectiveSecurityNotifications } from './runtime_toggles.js';
|
|
|
17
17
|
* @param notice Contexto da notificação
|
|
18
18
|
* @param mailHooks Hooks de mail do config (opcional)
|
|
19
19
|
* @param audit Sink de auditoria (opcional)
|
|
20
|
+
* @param cfg Config resolvido — só usado para derivar o origin do link
|
|
21
|
+
* do e-mail default (`authkitOrigin`); nunca do `request.host()`.
|
|
20
22
|
*/
|
|
21
|
-
export async function dispatchSecurityNotice(ctx, notice, mailHooks, audit) {
|
|
23
|
+
export async function dispatchSecurityNotice(ctx, notice, mailHooks, audit, cfg) {
|
|
22
24
|
try {
|
|
23
25
|
// Resolve settings em runtime (fail-safe: sem tabela → defaults habilitados).
|
|
24
26
|
let enabled = true;
|
|
@@ -59,7 +61,7 @@ export async function dispatchSecurityNotice(ctx, notice, mailHooks, audit) {
|
|
|
59
61
|
await mailHooks.onSecurityNotice(noticeData);
|
|
60
62
|
}
|
|
61
63
|
else {
|
|
62
|
-
await sendSecurityNoticeEmail(ctx, {
|
|
64
|
+
await sendSecurityNoticeEmail(ctx, cfg, {
|
|
63
65
|
email: notice.account.email,
|
|
64
66
|
kind: notice.kind,
|
|
65
67
|
timestamp,
|
|
@@ -19,6 +19,14 @@ import { password } from './methods/password.js';
|
|
|
19
19
|
* SEM `config.sudo.methods` não há como divergir: a tela cai na própria lista
|
|
20
20
|
* montada por `registerAuthHost`, a mesma que os handlers aceitam.
|
|
21
21
|
*
|
|
22
|
+
* E é essa mesma separação que define o que é DEFAULT. `registerAuthHost` monta
|
|
23
|
+
* `[password, passkey, magicLink]` sem conhecer o config; o config resolvido
|
|
24
|
+
* decide quais deles valem (`derivedSudoMethods`, em `runtime.ts`): host com
|
|
25
|
+
* senha fica com `[password, passkey]` (o histórico), host que declarou
|
|
26
|
+
* `authMethods: { password: false }` fica com `[passkey, magicLink]`. A regra é
|
|
27
|
+
* UMA função, consultada pelos dois lados — montar não é oferecer, e continuar
|
|
28
|
+
* não havendo como divergir.
|
|
29
|
+
*
|
|
22
30
|
* ```ts
|
|
23
31
|
* defineConfig({
|
|
24
32
|
* sudo: {
|
|
@@ -19,6 +19,14 @@ import { password } from './methods/password.js';
|
|
|
19
19
|
* SEM `config.sudo.methods` não há como divergir: a tela cai na própria lista
|
|
20
20
|
* montada por `registerAuthHost`, a mesma que os handlers aceitam.
|
|
21
21
|
*
|
|
22
|
+
* E é essa mesma separação que define o que é DEFAULT. `registerAuthHost` monta
|
|
23
|
+
* `[password, passkey, magicLink]` sem conhecer o config; o config resolvido
|
|
24
|
+
* decide quais deles valem (`derivedSudoMethods`, em `runtime.ts`): host com
|
|
25
|
+
* senha fica com `[password, passkey]` (o histórico), host que declarou
|
|
26
|
+
* `authMethods: { password: false }` fica com `[passkey, magicLink]`. A regra é
|
|
27
|
+
* UMA função, consultada pelos dois lados — montar não é oferecer, e continuar
|
|
28
|
+
* não havendo como divergir.
|
|
29
|
+
*
|
|
22
30
|
* ```ts
|
|
23
31
|
* defineConfig({
|
|
24
32
|
* sudo: {
|
|
@@ -36,8 +36,22 @@ export declare function verifySudoLinkToken(c: SudoContext, token: string): bool
|
|
|
36
36
|
/**
|
|
37
37
|
* Confirmação por link enviado ao e-mail da conta.
|
|
38
38
|
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
39
|
+
* O hook `mail.onSudoLink` é DISTINTO de `mail.onMagicLink` justamente para que
|
|
40
|
+
* o host não confunda um link que autentica com um que só concede sudo a quem já
|
|
41
|
+
* está logado — e continua tendo PRIORIDADE quando declarado.
|
|
42
|
+
*
|
|
43
|
+
* SEM O HOOK O MÉTODO SEGUE DISPONÍVEL: o próprio host-kit envia o e-mail pelo
|
|
44
|
+
* mailer default (`sendSudoLinkEmail`), exatamente como já faz com reset de
|
|
45
|
+
* senha, verificação de e-mail e o magic link de login. Antes o método se
|
|
46
|
+
* declarava indisponível sem hook, e num host passwordless isso FECHAVA o
|
|
47
|
+
* deadlock — sem senha, sem passkey e sem hook não sobrava nenhum método
|
|
48
|
+
* satisfazível na tela `/account/confirm`, e o usuário ficava trancado fora de
|
|
49
|
+
* exportar/excluir os próprios dados, do MFA, dos PATs e da troca de e-mail,
|
|
50
|
+
* inclusive fora do cadastro de passkey que destravaria o resto. Exigir um hook
|
|
51
|
+
* escrito à mão para que o único método sem credencial prévia funcione é
|
|
52
|
+
* transformar uma saída de emergência em opt-in.
|
|
53
|
+
*
|
|
54
|
+
* A única exigência que resta é a que não tem substituto: a conta precisa ter
|
|
55
|
+
* e-mail.
|
|
42
56
|
*/
|
|
43
57
|
export declare function magicLink(): SudoMethod;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';
|
|
2
2
|
import { accountPath } from '../../account_paths.js';
|
|
3
|
+
import { sendSudoLinkEmail } from '../../default_mailer.js';
|
|
3
4
|
import { translate } from '../../i18n.js';
|
|
4
5
|
import { isSudoMethodEnabled } from '../runtime.js';
|
|
5
6
|
/** Token de sudo pendente, guardado na sessão que o pediu. */
|
|
@@ -90,17 +91,29 @@ function requestOrigin(ctx) {
|
|
|
90
91
|
/**
|
|
91
92
|
* Confirmação por link enviado ao e-mail da conta.
|
|
92
93
|
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
94
|
+
* O hook `mail.onSudoLink` é DISTINTO de `mail.onMagicLink` justamente para que
|
|
95
|
+
* o host não confunda um link que autentica com um que só concede sudo a quem já
|
|
96
|
+
* está logado — e continua tendo PRIORIDADE quando declarado.
|
|
97
|
+
*
|
|
98
|
+
* SEM O HOOK O MÉTODO SEGUE DISPONÍVEL: o próprio host-kit envia o e-mail pelo
|
|
99
|
+
* mailer default (`sendSudoLinkEmail`), exatamente como já faz com reset de
|
|
100
|
+
* senha, verificação de e-mail e o magic link de login. Antes o método se
|
|
101
|
+
* declarava indisponível sem hook, e num host passwordless isso FECHAVA o
|
|
102
|
+
* deadlock — sem senha, sem passkey e sem hook não sobrava nenhum método
|
|
103
|
+
* satisfazível na tela `/account/confirm`, e o usuário ficava trancado fora de
|
|
104
|
+
* exportar/excluir os próprios dados, do MFA, dos PATs e da troca de e-mail,
|
|
105
|
+
* inclusive fora do cadastro de passkey que destravaria o resto. Exigir um hook
|
|
106
|
+
* escrito à mão para que o único método sem credencial prévia funcione é
|
|
107
|
+
* transformar uma saída de emergência em opt-in.
|
|
108
|
+
*
|
|
109
|
+
* A única exigência que resta é a que não tem substituto: a conta precisa ter
|
|
110
|
+
* e-mail.
|
|
96
111
|
*/
|
|
97
112
|
export function magicLink() {
|
|
98
113
|
return {
|
|
99
114
|
id: 'magic-link',
|
|
100
115
|
async isAvailable(c) {
|
|
101
|
-
|
|
102
|
-
return false;
|
|
103
|
-
return typeof c.cfg?.mail?.onSudoLink === 'function';
|
|
116
|
+
return Boolean(c.account?.email);
|
|
104
117
|
},
|
|
105
118
|
async describe() {
|
|
106
119
|
return {
|
|
@@ -122,20 +135,31 @@ export function magicLink() {
|
|
|
122
135
|
// e sem e-mail não há para onde mandar o link.
|
|
123
136
|
if (!c.account?.email)
|
|
124
137
|
return h.fail(c, 'account.confirm.error');
|
|
125
|
-
// Checado ANTES de emitir: um token emitido sem ninguém para entregá-lo
|
|
126
|
-
// é lixo na sessão, e a `isAvailable` já prometeu que sem hook o método
|
|
127
|
-
// não existe.
|
|
128
|
-
const onSudoLink = c.cfg?.mail?.onSudoLink;
|
|
129
|
-
if (typeof onSudoLink !== 'function')
|
|
130
|
-
return h.fail(c, 'account.confirm.error');
|
|
131
138
|
const qs = c.returnTo ? `?return_to=${encodeURIComponent(c.returnTo)}` : '';
|
|
132
139
|
const token = issueSudoLinkToken(c);
|
|
133
140
|
const path = `${accountPath('confirm')}/magic-link/${token}${qs}`;
|
|
134
141
|
const origin = requestOrigin(ctx);
|
|
142
|
+
const sudoUrl = origin ? `${origin}${path}` : path;
|
|
143
|
+
// ENTREGA. Hook do host quando declarado; senão o mailer default do
|
|
144
|
+
// host-kit (mesma precedência de todo e-mail da lib). O que NÃO é
|
|
145
|
+
// opcional é haver uma entrega: um método de sudo que só funciona se o
|
|
146
|
+
// host escrever um hook não serve de saída de emergência para o host
|
|
147
|
+
// passwordless, que é justamente quem depende dele.
|
|
148
|
+
const onSudoLink = c.cfg?.mail?.onSudoLink;
|
|
149
|
+
let delivered = false;
|
|
135
150
|
try {
|
|
136
|
-
|
|
151
|
+
if (typeof onSudoLink === 'function') {
|
|
152
|
+
await onSudoLink({ email: c.account.email, sudoUrl });
|
|
153
|
+
delivered = true;
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
delivered = await sendSudoLinkEmail(ctx, { email: c.account.email, sudoUrl });
|
|
157
|
+
}
|
|
137
158
|
}
|
|
138
159
|
catch {
|
|
160
|
+
delivered = false;
|
|
161
|
+
}
|
|
162
|
+
if (!delivered) {
|
|
139
163
|
// O envio falhou: apaga o pendente. Não é risco de segurança (o
|
|
140
164
|
// segredo não chegou a lugar nenhum), mas deixá-lo lá invalidaria
|
|
141
165
|
// silenciosamente um token anterior ainda válido do usuário.
|
|
@@ -18,8 +18,17 @@ export declare function sudoContextFrom(ctx: HttpContext): Promise<SudoContext>;
|
|
|
18
18
|
* Registra a lista montada. Chamado UMA vez por `registerAuthHost`, e
|
|
19
19
|
* SUBSTITUI (não acumula): registrar o host de novo é redefinir o que existe,
|
|
20
20
|
* não somar ao que existia.
|
|
21
|
+
*
|
|
22
|
+
* `fromDefaults` distingue "esta é a lista de defaults da LIB" de "esta é a
|
|
23
|
+
* lista que o HOST escreveu" (argumento de `registerAuthHost` ou
|
|
24
|
+
* `config.sudo.methods`). A distinção existe porque a derivação do deadlock
|
|
25
|
+
* (`derivedSudoMethods`) só pode mexer na primeira: mexer na segunda seria
|
|
26
|
+
* mudar, sem pedir, o que um host declarou explicitamente — e a promessa de
|
|
27
|
+
* `sudoMethods` é que ele SUBSTITUI os defaults.
|
|
21
28
|
*/
|
|
22
|
-
export declare function setMountedSudoMethods(methods: SudoMethod[]
|
|
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
|
-
*
|
|
46
|
-
*
|
|
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
|
-
*
|
|
79
|
-
*
|
|
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
|
|
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) ??
|
|
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
|
+
}
|