@adonis-agora/authkit-server 0.53.0 → 0.55.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/index.d.ts +6 -2
- package/build/index.js +7 -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 +112 -0
- package/build/src/host/oidc_rp_guard.js +200 -0
- 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 +25 -13
- package/build/src/provider/token_exchange.d.ts +12 -1
- package/build/src/provider/token_exchange.js +12 -0
- package/package.json +6 -3
- /package/build/{password → src/password}/common_passwords.txt +0 -0
|
@@ -1,28 +1,43 @@
|
|
|
1
1
|
import { resolveRateLimit } from '../define_config.js';
|
|
2
2
|
import { accountHome } from './account_home.js';
|
|
3
3
|
import { getAccountLoginUrl, setAccountLoginUrl } from './account_login_url.js';
|
|
4
|
-
import { accountPath, joinAccountPath, setAccountPaths, } from './account_paths.js';
|
|
4
|
+
import { accountPath, accountPathsMap, accountPrefix, joinAccountPath, setAccountPaths, } from './account_paths.js';
|
|
5
5
|
import { resolveAccountRoles } from './account_roles.js';
|
|
6
6
|
import { adminApiGuard } from './admin_api/admin_api_guard.js';
|
|
7
7
|
import { normalizeAdminApiPrefix, normalizeAdminPrefix, setAdminApiPrefix, setAdminPrefix, } from './admin_prefix.js';
|
|
8
|
-
import { getAuthHostConfig } from './auth_host_config.js';
|
|
8
|
+
import { getAuthHostConfig, markAuthHostAutoMounted, wasAuthHostAutoMounted, } from './auth_host_config.js';
|
|
9
9
|
import { ACCOUNT_SESSION_KEY } from './middleware/account_auth.js';
|
|
10
10
|
import { createAuthThrottles } from './rate_limit.js';
|
|
11
11
|
import { resolveRuntimeSettings } from './runtime_settings.js';
|
|
12
12
|
import { resolveEffectiveSessionPolicy } from './runtime_toggles.js';
|
|
13
|
+
import { magicLink as sudoMagicLink } from './sudo/methods/magic_link.js';
|
|
13
14
|
import { passkey as sudoPasskey } from './sudo/methods/passkey.js';
|
|
14
15
|
import { password as sudoPassword } from './sudo/methods/password.js';
|
|
15
16
|
import { completeSudo, fail, guardSudoRoutes, setMountedSudoMethods, sudoContextFrom, } from './sudo/runtime.js';
|
|
16
17
|
/**
|
|
17
|
-
* Métodos de sudo
|
|
18
|
-
*
|
|
18
|
+
* Métodos de sudo cujas ROTAS são montadas quando o host não passa
|
|
19
|
+
* `sudoMethods`.
|
|
19
20
|
*
|
|
20
|
-
*
|
|
21
|
-
* `
|
|
22
|
-
*
|
|
23
|
-
*
|
|
21
|
+
* `magicLink()` entra aqui, e a lista deixou de ser o histórico
|
|
22
|
+
* `[password, passkey]`, porque os dois históricos exigem uma credencial
|
|
23
|
+
* PREVIAMENTE CADASTRADA. Num host passwordless não existe nenhuma: o usuário
|
|
24
|
+
* não tem senha, não tem passkey — e cadastrar passkey exige sudo. Deadlock
|
|
25
|
+
* fechado, e era o DEFAULT. O `oidcStepUp` não pode entrar (exige uma URL do
|
|
26
|
+
* host), então o magic link de sudo é o único método sem credencial prévia que
|
|
27
|
+
* a lib consegue montar sozinha.
|
|
28
|
+
*
|
|
29
|
+
* MONTAR NÃO É OFERECER. Esta lista decide apenas quais ENDPOINTS existem; o
|
|
30
|
+
* que a tela oferece e o que os handlers aceitam é derivado do config resolvido,
|
|
31
|
+
* em `sudo/runtime.ts` (`derivedSudoMethods`) — um host COM senha continua
|
|
32
|
+
* recebendo exatamente `[password, passkey]`, e o endpoint do magic link fica
|
|
33
|
+
* montado mas inerte. Não é possível decidir a oferta aqui: a montagem acontece
|
|
34
|
+
* antes de o config (lazy) resolver, e `authMethods` vive no config.
|
|
35
|
+
*
|
|
36
|
+
* PONTO ÚNICO. A tela não tem uma cópia desta lista: sem `config.sudo.methods`,
|
|
37
|
+
* `configuredSudoMethods` cai no que FOI MONTADO (derivado). Duas listas de
|
|
38
|
+
* default é como os dois lados divergiam.
|
|
24
39
|
*/
|
|
25
|
-
const SUDO_METHOD_DEFAULTS = [sudoPassword(), sudoPasskey()];
|
|
40
|
+
const SUDO_METHOD_DEFAULTS = [sudoPassword(), sudoPasskey(), sudoMagicLink()];
|
|
26
41
|
/** Chave da sessão Adonis que registra o timestamp da última atividade (idle timeout). */
|
|
27
42
|
export const ACCOUNT_LAST_SEEN_KEY = 'authkit_last_seen';
|
|
28
43
|
/**
|
|
@@ -190,37 +205,123 @@ const C = {
|
|
|
190
205
|
/**
|
|
191
206
|
* Monta todas as rotas do host-kit do Authorization Server numa chamada.
|
|
192
207
|
* Substitui registerOidcRoutes + o hand-wiring do start/routes.ts do host.
|
|
208
|
+
*
|
|
209
|
+
* ── A REGRA DE PRECEDÊNCIA (config × argumento) ─────────────────────────────
|
|
210
|
+
*
|
|
211
|
+
* 1. **Argumento omitido HERDA do config.** `registerAuthHost(router)` é
|
|
212
|
+
* totalmente config-driven; `registerAuthHost(router, { mountPath: '/sso' })`
|
|
213
|
+
* troca o mountPath e herda TODO o resto. Omitir uma chave significa "usa o
|
|
214
|
+
* config", nunca "usa nada" — é o que dispensa repetir `sudo.methods` no
|
|
215
|
+
* `start/routes.ts`.
|
|
216
|
+
*
|
|
217
|
+
* 2. **Chaves ESTRUTURAIS: o argumento vence.** `mountPath`, os prefixos
|
|
218
|
+
* (`admin.prefix`, `adminApi.prefix`, `accountRoutes.prefix`), os segmentos
|
|
219
|
+
* de tela, `account` (quais telas montar) e `accountLoginUrl`. São decisões
|
|
220
|
+
* do ponto de chamada por natureza, e a forma de função é estritamente mais
|
|
221
|
+
* expressiva (dá para montar duas vezes sob dois prefixos — nenhum config
|
|
222
|
+
* expressa isso).
|
|
223
|
+
*
|
|
224
|
+
* 3. **Chaves de POLÍTICA: o config vence, e trava.** `social`, `rateLimit`,
|
|
225
|
+
* `sudoMethods` e o liga/desliga de `admin`/`adminApi` decidem o que é
|
|
226
|
+
* PERMITIDO. Quando o `defineConfig` as declara, o argumento NÃO as altera —
|
|
227
|
+
* senão o `config/authkit.ts` deixa de ser auditável e seria preciso ler o
|
|
228
|
+
* `start/routes.ts` de cada app para saber o que está valendo. Mesma regra
|
|
229
|
+
* (e mesma derivação) de `defineConfig({ authMethods })`, que já fixa os
|
|
230
|
+
* métodos de login contra o runtime. Ver `deriveLockedRouteOptions`.
|
|
231
|
+
* A divergência NÃO é silenciosa: sai um `console.warn` nomeando a chave e
|
|
232
|
+
* a chave aparece em `AuthHostRouteMap.overriddenByConfig`.
|
|
233
|
+
*
|
|
234
|
+
* Devolve o {@link AuthHostRouteMap} resolvido — entregue-o ao frontend em vez
|
|
235
|
+
* de hardcodar `href`s.
|
|
193
236
|
*/
|
|
194
237
|
export function registerAuthHost(router, opts = {}) {
|
|
195
238
|
// Config resolvido (stashado no boot do provider) — fonte única; `opts` só faz
|
|
196
|
-
// OVERRIDE. Elimina o drift: o consumidor pode chamar
|
|
197
|
-
// e
|
|
239
|
+
// OVERRIDE das chaves ESTRUTURAIS. Elimina o drift: o consumidor pode chamar
|
|
240
|
+
// `registerAuthHost(router)` e tudo vem do config/authkit.ts.
|
|
198
241
|
// Fallback p/ defaults quando o stash não está disponível (ex.: testes sem boot).
|
|
199
242
|
const hostCfg = getAuthHostConfig();
|
|
200
|
-
|
|
201
|
-
//
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
243
|
+
// `config.routes` já montou tudo no boot do provider. Registrar de novo
|
|
244
|
+
// duplicaria CADA rota — e duas rotas com o mesmo nome derrubam o boot do
|
|
245
|
+
// AdonisJS com um `RuntimeException` que não diz de onde veio. Falha aqui,
|
|
246
|
+
// alto e com a saída explícita.
|
|
247
|
+
if (wasAuthHostAutoMounted()) {
|
|
248
|
+
throw new Error('authkit: registerAuthHost() foi chamado depois de `routes` no config/authkit.ts já ter montado as rotas automaticamente — o duplo registro derruba o boot ("route name already exists"). Escolha UM dos dois: remova a chamada do start/routes.ts, ou ponha `routes: false` no defineConfig e configure tudo pela chamada.');
|
|
249
|
+
}
|
|
250
|
+
// Defaults ESTRUTURAIS declarados em `config.routes` — o argumento ainda vence.
|
|
251
|
+
const routesCfg = hostCfg?.routes;
|
|
252
|
+
// Chaves de POLÍTICA travadas pelo defineConfig (ver o item 3 acima).
|
|
253
|
+
const locked = new Set(hostCfg?.lockedRouteOptions ?? []);
|
|
254
|
+
const overriddenByConfig = [];
|
|
255
|
+
/**
|
|
256
|
+
* Uma chave de política travada IGNORA o argumento e reporta. Não silencia:
|
|
257
|
+
* um argumento sem efeito que ninguém percebe é como o drift começa.
|
|
258
|
+
*/
|
|
259
|
+
const isLocked = (key, passed) => {
|
|
260
|
+
if (!locked.has(key))
|
|
261
|
+
return false;
|
|
262
|
+
if (passed) {
|
|
263
|
+
overriddenByConfig.push(key);
|
|
264
|
+
console.warn(`authkit: registerAuthHost(router, { ${key} }) foi IGNORADO — "${key}" é uma opção de política e está definida no defineConfig(), que vence (o config precisa ser auditável sem ler o start/routes.ts). Remova-a da chamada, ou remova-a do config para controlá-la aqui.`);
|
|
265
|
+
}
|
|
266
|
+
return true;
|
|
267
|
+
};
|
|
268
|
+
const mount = opts.mountPath ?? routesCfg?.mountPath ?? hostCfg?.mountPath ?? '/oidc';
|
|
269
|
+
// social — POLÍTICA: decide se existe um caminho de autenticação a mais.
|
|
270
|
+
const social = isLocked('social', opts.social !== undefined)
|
|
271
|
+
? hostCfg?.social
|
|
272
|
+
: (opts.social ?? routesCfg?.social ?? hostCfg?.social);
|
|
273
|
+
// admin/adminApi — o LIGA/DESLIGA é política (config vence); o PREFIXO é
|
|
274
|
+
// estrutural (o argumento vence). Por isso os dois eixos são resolvidos
|
|
275
|
+
// separadamente em vez de a opção inteira ser um bloco só.
|
|
276
|
+
const enableFrom = (v) => v === undefined ? undefined : v !== false;
|
|
277
|
+
const prefixFrom = (...vs) => {
|
|
278
|
+
for (const v of vs)
|
|
279
|
+
if (typeof v === 'object' && v?.prefix)
|
|
280
|
+
return v.prefix;
|
|
281
|
+
return undefined;
|
|
282
|
+
};
|
|
283
|
+
// `{ prefix: '/painel' }` mexe SÓ no eixo estrutural: só há divergência a
|
|
284
|
+
// reportar quando a intenção de liga/desliga do argumento CONTRADIZ o config.
|
|
285
|
+
const contradicts = (arg, configEnabled) => {
|
|
286
|
+
const intent = enableFrom(arg);
|
|
287
|
+
return intent !== undefined && intent !== (configEnabled ?? false);
|
|
288
|
+
};
|
|
289
|
+
const adminEnabled = isLocked('admin', contradicts(opts.admin, hostCfg?.adminEnabled))
|
|
290
|
+
? (hostCfg?.adminEnabled ?? false)
|
|
291
|
+
: (enableFrom(opts.admin) ?? enableFrom(routesCfg?.admin) ?? hostCfg?.adminEnabled ?? false);
|
|
292
|
+
const adminPrefixOpt = prefixFrom(opts.admin, routesCfg?.admin);
|
|
293
|
+
const adminOpt = adminEnabled ? { prefix: adminPrefixOpt } : undefined;
|
|
294
|
+
const adminApiEnabled = isLocked('adminApi', contradicts(opts.adminApi, hostCfg?.adminApiEnabled))
|
|
295
|
+
? (hostCfg?.adminApiEnabled ?? false)
|
|
296
|
+
: (enableFrom(opts.adminApi) ??
|
|
297
|
+
enableFrom(routesCfg?.adminApi) ??
|
|
298
|
+
hostCfg?.adminApiEnabled ??
|
|
299
|
+
false);
|
|
300
|
+
const adminApiPrefixOpt = prefixFrom(opts.adminApi, routesCfg?.adminApi);
|
|
301
|
+
const adminApiOpt = adminApiEnabled
|
|
302
|
+
? { prefix: adminApiPrefixOpt }
|
|
303
|
+
: undefined;
|
|
205
304
|
// Prefixo/segmentos configuráveis do console de conta — persiste no singleton
|
|
206
305
|
// de processo ANTES de qualquer construção de rota/closure, para que o registro
|
|
207
306
|
// de rotas, os guards, os controllers, os e-mails e as views leiam o mesmo
|
|
208
307
|
// valor. Top-level de propósito (vale mesmo com `account: false`): as rotas de
|
|
209
308
|
// sudo e a JSON API respeitam o prefixo. Ausente → default `/account/*`
|
|
210
309
|
// (back-compat). Ver `account_paths.ts`.
|
|
211
|
-
|
|
212
|
-
|
|
310
|
+
const accountRoutesOpt = opts.accountRoutes ?? routesCfg?.accountRoutes;
|
|
311
|
+
if (accountRoutesOpt !== undefined) {
|
|
312
|
+
setAccountPaths(accountRoutesOpt);
|
|
213
313
|
}
|
|
214
314
|
// Destino do redirect de "faça login" — persiste no singleton de processo para
|
|
215
315
|
// que os guards (closures de tempo de registro), o middleware, os controllers e
|
|
216
316
|
// as views leiam o mesmo valor. Só quando a opção foi passada (senão deriva de
|
|
217
317
|
// `accountPath('login')` — back-compat).
|
|
218
|
-
|
|
219
|
-
|
|
318
|
+
const accountLoginUrlOpt = opts.accountLoginUrl ?? routesCfg?.accountLoginUrl;
|
|
319
|
+
if (accountLoginUrlOpt !== undefined) {
|
|
320
|
+
setAccountLoginUrl(accountLoginUrlOpt);
|
|
220
321
|
}
|
|
221
322
|
// Montagem por tela do console de conta. `undefined` → tudo montado;
|
|
222
323
|
// `false` → nada; objeto → cada flag ausente default `true`.
|
|
223
|
-
const accountOpt = opts.account;
|
|
324
|
+
const accountOpt = opts.account ?? routesCfg?.account;
|
|
224
325
|
const mountScreen = (key) => {
|
|
225
326
|
if (accountOpt === false)
|
|
226
327
|
return false;
|
|
@@ -234,10 +335,21 @@ export function registerAuthHost(router, opts = {}) {
|
|
|
234
335
|
const mountSecurity = mountScreen('security');
|
|
235
336
|
const mountMfa = mountScreen('mfa');
|
|
236
337
|
const mountApps = mountScreen('apps');
|
|
338
|
+
// Ids dos métodos de sudo efetivamente montados — preenchido dentro do grupo
|
|
339
|
+
// do console de conta (a callback do `.group()` roda de forma síncrona) e
|
|
340
|
+
// devolvido no `AuthHostRouteMap`.
|
|
341
|
+
let mountedSudoIds = [];
|
|
237
342
|
// Throttles opt-in (anti-brute-force). `undefined` quando rate-limit desligado.
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
343
|
+
// POLÍTICA: `rateLimit: { enabled: false }` num routes.ts desligaria a proteção
|
|
344
|
+
// anti-brute-force que o config ligou — e o config.rateLimit JÁ trava a setting
|
|
345
|
+
// de runtime homônima (`deriveLockedSettingKeys`). Travar aqui só fecha o
|
|
346
|
+
// terceiro caminho para o mesmo valor.
|
|
347
|
+
const rateLimitOpt = opts.rateLimit ?? routesCfg?.rateLimit;
|
|
348
|
+
const resolvedRateLimit = isLocked('rateLimit', opts.rateLimit !== undefined)
|
|
349
|
+
? (hostCfg?.rateLimit ?? resolveRateLimit(undefined))
|
|
350
|
+
: rateLimitOpt !== undefined
|
|
351
|
+
? resolveRateLimit(rateLimitOpt)
|
|
352
|
+
: (hostCfg?.rateLimit ?? resolveRateLimit(undefined));
|
|
241
353
|
const throttles = createAuthThrottles(resolvedRateLimit);
|
|
242
354
|
// Helpers: aplicam o middleware de throttle quando presente; senão no-op.
|
|
243
355
|
const withLogin = (route) => {
|
|
@@ -413,7 +525,16 @@ export function registerAuthHost(router, opts = {}) {
|
|
|
413
525
|
completeSudo,
|
|
414
526
|
fail,
|
|
415
527
|
};
|
|
416
|
-
|
|
528
|
+
// POLÍTICA. `config.sudo.methods` já decide o que a tela OFERECE e o que
|
|
529
|
+
// os handlers ACEITAM (`isSudoMethodEnabled`); agora decide também o que é
|
|
530
|
+
// MONTADO. Era a única das três decisões que ficava fora do config, e era
|
|
531
|
+
// exatamente por isso que o host tinha de manter a lista em DOIS lugares —
|
|
532
|
+
// divergiram, a tela oferecia um método sem endpoint (404) e escondia um
|
|
533
|
+
// que funcionava.
|
|
534
|
+
const sudoMethodsDeclared = isLocked('sudoMethods', opts.sudoMethods !== undefined)
|
|
535
|
+
? hostCfg?.sudoMethods
|
|
536
|
+
: (opts.sudoMethods ?? routesCfg?.sudoMethods ?? hostCfg?.sudoMethods);
|
|
537
|
+
const sudoMethodsToMount = sudoMethodsDeclared ?? SUDO_METHOD_DEFAULTS;
|
|
417
538
|
for (const method of sudoMethodsToMount) {
|
|
418
539
|
// `guardSudoRoutes` embrulha os handlers que o método registrar, para
|
|
419
540
|
// que `config.sudo.methods` os desabilite de fato mesmo que o método
|
|
@@ -434,7 +555,16 @@ export function registerAuthHost(router, opts = {}) {
|
|
|
434
555
|
// A lista montada é a fonte de verdade dos DOIS lados quando o host não
|
|
435
556
|
// configura `config.sudo.methods`: a tela oferece exatamente isto, e os
|
|
436
557
|
// handlers aceitam exatamente isto.
|
|
437
|
-
|
|
558
|
+
//
|
|
559
|
+
// O segundo argumento diz se a lista é a da LIB ou a do HOST, e a
|
|
560
|
+
// distinção é o que mantém o conserto do deadlock inofensivo: só a lista
|
|
561
|
+
// de defaults é derivada do config (`derivedSudoMethods`). Uma lista que o
|
|
562
|
+
// host escreveu — no argumento ou no `config.sudo.methods` — vale ao pé da
|
|
563
|
+
// letra, em qualquer direção, porque foi ele quem a escreveu.
|
|
564
|
+
setMountedSudoMethods(sudoMethodsToMount, {
|
|
565
|
+
fromDefaults: sudoMethodsDeclared === undefined,
|
|
566
|
+
});
|
|
567
|
+
mountedSudoIds = sudoMethodsToMount.map((m) => m.id);
|
|
438
568
|
// Organizations (multi-tenancy) — tela `orgs`. Montadas por default;
|
|
439
569
|
// controller retorna 404/403 sem tabelas (capability-probed).
|
|
440
570
|
if (mountOrgs) {
|
|
@@ -489,11 +619,16 @@ export function registerAuthHost(router, opts = {}) {
|
|
|
489
619
|
router.get(`${apiBase}/orgs/:id`, [C.accountApi, 'showOrg']);
|
|
490
620
|
})
|
|
491
621
|
.use([accountGuard]);
|
|
622
|
+
// Prefixos resolvidos dos consoles — `null` quando o grupo não foi montado.
|
|
623
|
+
// Compõem o `AuthHostRouteMap` devolvido no fim.
|
|
624
|
+
let resolvedAdminPrefix = null;
|
|
625
|
+
let resolvedAdminApiPrefix = null;
|
|
492
626
|
// Console admin (do config.admin.enabled ou opts). Protegido pelo adminGuard (sessão + role global).
|
|
493
627
|
if (adminOpt) {
|
|
494
628
|
// Resolve o prefixo: `true` → '/admin' (default); objeto → usa prefix fornecido.
|
|
495
629
|
const rawPrefix = typeof adminOpt === 'object' && adminOpt.prefix ? adminOpt.prefix : '/admin';
|
|
496
630
|
const ap = normalizeAdminPrefix(rawPrefix);
|
|
631
|
+
resolvedAdminPrefix = ap;
|
|
497
632
|
// Persiste no singleton de processo para que controllers e views usem o mesmo prefixo.
|
|
498
633
|
setAdminPrefix(ap);
|
|
499
634
|
router
|
|
@@ -586,6 +721,7 @@ export function registerAuthHost(router, opts = {}) {
|
|
|
586
721
|
? adminApiOpt.prefix
|
|
587
722
|
: '/api/authkit/v1';
|
|
588
723
|
const aap = normalizeAdminApiPrefix(rawApiPrefix);
|
|
724
|
+
resolvedAdminApiPrefix = aap;
|
|
589
725
|
// Persiste no singleton de processo para que o SDK remoto e outros consumidores
|
|
590
726
|
// usem o mesmo prefixo sem precisar receber a opção.
|
|
591
727
|
setAdminApiPrefix(aap);
|
|
@@ -648,4 +784,64 @@ export function registerAuthHost(router, opts = {}) {
|
|
|
648
784
|
// O `withApiThrottle` (introspection, por token) continua como camada adicional.
|
|
649
785
|
.use(throttles ? [throttles.adminIp, adminApiGuard] : [adminApiGuard]);
|
|
650
786
|
}
|
|
787
|
+
// ─── Mapa resolvido (R4) ───────────────────────────────────────────────────
|
|
788
|
+
// Lido DEPOIS de todo o registro: `accountPath()`/`accountPrefix()` já
|
|
789
|
+
// refletem o `accountRoutes` aplicado no topo, e os prefixos de console já
|
|
790
|
+
// foram normalizados. É este objeto que o host entrega ao frontend em vez de
|
|
791
|
+
// hardcodar `href`s — o que faltava para um override de path ser um recurso
|
|
792
|
+
// inteiro, e não só metade (certo no servidor, errado na UI).
|
|
793
|
+
return {
|
|
794
|
+
mountPath: mount,
|
|
795
|
+
account: {
|
|
796
|
+
prefix: accountPrefix(),
|
|
797
|
+
api: apiBase,
|
|
798
|
+
paths: accountPathsMap(),
|
|
799
|
+
loginUrl: getAccountLoginUrl(),
|
|
800
|
+
screens: {
|
|
801
|
+
login: mountLogin,
|
|
802
|
+
tokens: mountTokens,
|
|
803
|
+
orgs: mountOrgs,
|
|
804
|
+
security: mountSecurity,
|
|
805
|
+
mfa: mountMfa,
|
|
806
|
+
apps: mountApps,
|
|
807
|
+
},
|
|
808
|
+
},
|
|
809
|
+
admin: resolvedAdminPrefix ? { prefix: resolvedAdminPrefix } : null,
|
|
810
|
+
adminApi: resolvedAdminApiPrefix ? { prefix: resolvedAdminApiPrefix } : null,
|
|
811
|
+
names: {
|
|
812
|
+
webauthnAsset: 'authkit.assets.webauthn',
|
|
813
|
+
oidcWildcard: 'authkit.oidc.wildcard',
|
|
814
|
+
oidcRoot: 'authkit.oidc.root',
|
|
815
|
+
...(resolvedAdminPrefix
|
|
816
|
+
? {
|
|
817
|
+
consoleAssets: 'authkit_console_assets',
|
|
818
|
+
consoleRoot: 'authkit_console_root',
|
|
819
|
+
consoleShell: 'authkit_console_shell',
|
|
820
|
+
}
|
|
821
|
+
: {}),
|
|
822
|
+
},
|
|
823
|
+
sudoMethods: mountedSudoIds,
|
|
824
|
+
overriddenByConfig,
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
/**
|
|
828
|
+
* Auto-montagem a partir de `config.routes` (R1). Chamada UMA vez pelo
|
|
829
|
+
* `boot()` do provider.
|
|
830
|
+
*
|
|
831
|
+
* O CAMINHO AUTOMÁTICO É O CAMINHO MANUAL: chama literalmente
|
|
832
|
+
* `registerAuthHost`, sem nenhuma segunda implementação. Toda a resolução
|
|
833
|
+
* (herança do config, precedência política × estrutural, mapa de retorno) é a
|
|
834
|
+
* mesma — duas implementações de um comportamento sempre divergem.
|
|
835
|
+
*
|
|
836
|
+
* Não recebe opções: os defaults ESTRUTURAIS vêm de `config.routes` pelo stash
|
|
837
|
+
* (`AuthHostRuntimeConfig.routes`), pelo mesmo caminho que uma chamada manual
|
|
838
|
+
* os leria.
|
|
839
|
+
*
|
|
840
|
+
* @internal
|
|
841
|
+
*/
|
|
842
|
+
export function autoMountAuthHost(router) {
|
|
843
|
+
const map = registerAuthHost(router);
|
|
844
|
+
// Depois, nunca antes: `registerAuthHost` LANÇA quando já houve auto-mount.
|
|
845
|
+
markAuthHostAutoMounted();
|
|
846
|
+
return map;
|
|
651
847
|
}
|
|
@@ -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.
|