@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
|
@@ -1,6 +1,27 @@
|
|
|
1
|
-
import { symbols } from '@adonisjs/auth';
|
|
2
1
|
import { RuntimeException } from '@adonisjs/core/exceptions';
|
|
3
2
|
import { ACCOUNT_SESSION_KEY } from './middleware/account_auth.js';
|
|
3
|
+
/** Memo do construtor entre instâncias — o `import()` só paga o custo uma vez. */
|
|
4
|
+
let cachedUnauthorizedAccess;
|
|
5
|
+
/**
|
|
6
|
+
* Captura o `E_UNAUTHORIZED_ACCESS` real do `@adonisjs/auth` sem import
|
|
7
|
+
* estático. Chamado no boot pelo {@link oidcRpGuard} (falha cedo e com uma
|
|
8
|
+
* mensagem útil se o peer não estiver instalado) e, como rede de segurança, na
|
|
9
|
+
* primeira `authenticate()` de um guard construído à mão.
|
|
10
|
+
*/
|
|
11
|
+
export async function loadUnauthorizedAccess() {
|
|
12
|
+
if (cachedUnauthorizedAccess)
|
|
13
|
+
return cachedUnauthorizedAccess;
|
|
14
|
+
try {
|
|
15
|
+
const auth = (await import('@adonisjs/auth'));
|
|
16
|
+
cachedUnauthorizedAccess = auth.errors.E_UNAUTHORIZED_ACCESS;
|
|
17
|
+
return cachedUnauthorizedAccess;
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
throw new RuntimeException('oidcRpGuard() precisa de "@adonisjs/auth" instalado (é um peer opcional do ' +
|
|
21
|
+
'@adonis-agora/authkit-server, só necessário se você plugar este guard em config/auth.ts). ' +
|
|
22
|
+
'Rode `npm i @adonisjs/auth` (ou pnpm/yarn).', { cause: error });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
4
25
|
/**
|
|
5
26
|
* Guard de `@adonisjs/auth` pra Relying Parties OIDC — o app não autentica
|
|
6
27
|
* ninguém (sem senha, sem remember-me); a identidade vem da sessão gravada
|
|
@@ -32,22 +53,36 @@ export class OidcRpGuard {
|
|
|
32
53
|
#sessionKey;
|
|
33
54
|
#emitter;
|
|
34
55
|
#userProvider;
|
|
35
|
-
|
|
56
|
+
#unauthorized;
|
|
36
57
|
driverName = 'oidc_rp';
|
|
37
58
|
authenticationAttempted = false;
|
|
38
59
|
isAuthenticated = false;
|
|
39
60
|
isLoggedOut = false;
|
|
40
61
|
user;
|
|
41
|
-
constructor(name, ctx, sessionKey, emitter, userProvider) {
|
|
62
|
+
constructor(name, ctx, sessionKey, emitter, userProvider, unauthorized) {
|
|
42
63
|
this.#name = name;
|
|
43
64
|
this.#ctx = ctx;
|
|
44
65
|
this.#sessionKey = sessionKey;
|
|
45
66
|
this.#emitter = emitter;
|
|
46
67
|
this.#userProvider = userProvider;
|
|
68
|
+
this.#unauthorized = unauthorized ?? cachedUnauthorizedAccess;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* O `E_UNAUTHORIZED_ACCESS` do framework — `status` 401 e os renderers
|
|
72
|
+
* html/json, então o handler de exceção do host trata igual ao dos guards
|
|
73
|
+
* nativos. Se o construtor ainda não foi resolvido (guard instanciado à mão,
|
|
74
|
+
* antes de qualquer `authenticate()`), cai num `RuntimeException` em vez de
|
|
75
|
+
* mentir sobre o tipo.
|
|
76
|
+
*/
|
|
77
|
+
#unauthorizedError(message) {
|
|
78
|
+
const Unauthorized = this.#unauthorized ?? cachedUnauthorizedAccess;
|
|
79
|
+
if (!Unauthorized)
|
|
80
|
+
return new RuntimeException(message);
|
|
81
|
+
return new Unauthorized(message, { guardDriverName: this.driverName });
|
|
47
82
|
}
|
|
48
83
|
getUserOrFail() {
|
|
49
84
|
if (!this.user) {
|
|
50
|
-
throw
|
|
85
|
+
throw this.#unauthorizedError('Cannot access user. Authentication has not been attempted or failed.');
|
|
51
86
|
}
|
|
52
87
|
return this.user;
|
|
53
88
|
}
|
|
@@ -89,13 +124,14 @@ export class OidcRpGuard {
|
|
|
89
124
|
return this.getUserOrFail();
|
|
90
125
|
}
|
|
91
126
|
this.authenticationAttempted = true;
|
|
127
|
+
this.#unauthorized ??= await loadUnauthorizedAccess();
|
|
92
128
|
const userId = this.#ctx.session.get(this.#sessionKey);
|
|
93
129
|
if (!userId) {
|
|
94
130
|
this.#emitter.emit('oidc_rp:authentication_failed', {
|
|
95
131
|
ctx: this.#ctx,
|
|
96
132
|
guardName: this.#name,
|
|
97
133
|
});
|
|
98
|
-
throw
|
|
134
|
+
throw this.#unauthorizedError('Unauthorized');
|
|
99
135
|
}
|
|
100
136
|
const guardUser = await this.#userProvider.findById(userId);
|
|
101
137
|
if (!guardUser) {
|
|
@@ -104,7 +140,7 @@ export class OidcRpGuard {
|
|
|
104
140
|
ctx: this.#ctx,
|
|
105
141
|
guardName: this.#name,
|
|
106
142
|
});
|
|
107
|
-
throw
|
|
143
|
+
throw this.#unauthorizedError('Unauthorized');
|
|
108
144
|
}
|
|
109
145
|
this.user = guardUser.getOriginal();
|
|
110
146
|
this.isAuthenticated = true;
|
|
@@ -115,13 +151,22 @@ export class OidcRpGuard {
|
|
|
115
151
|
});
|
|
116
152
|
return this.user;
|
|
117
153
|
}
|
|
154
|
+
/**
|
|
155
|
+
* `authenticate()` sem lançar — mas SÓ para falha de autenticação. Igual ao
|
|
156
|
+
* `SessionGuard` nativo: engole apenas `E_UNAUTHORIZED_ACCESS` e relança o
|
|
157
|
+
* resto. Um `catch` cego aqui transformava uma queda do banco dentro de
|
|
158
|
+
* `provider.findById` em "não logado" para todo mundo, sem nada nos logs.
|
|
159
|
+
*/
|
|
118
160
|
async check() {
|
|
119
161
|
try {
|
|
120
162
|
await this.authenticate();
|
|
121
163
|
return true;
|
|
122
164
|
}
|
|
123
|
-
catch {
|
|
124
|
-
|
|
165
|
+
catch (error) {
|
|
166
|
+
const Unauthorized = this.#unauthorized ?? cachedUnauthorizedAccess;
|
|
167
|
+
if (Unauthorized && error instanceof Unauthorized)
|
|
168
|
+
return false;
|
|
169
|
+
throw error;
|
|
125
170
|
}
|
|
126
171
|
}
|
|
127
172
|
async authenticateAsClient(user) {
|
|
@@ -139,6 +184,7 @@ export function oidcRpGuard(config) {
|
|
|
139
184
|
async resolver(name, app) {
|
|
140
185
|
const emitter = await app.container.make('emitter');
|
|
141
186
|
const sessionKey = config.sessionKey ?? ACCOUNT_SESSION_KEY;
|
|
187
|
+
const unauthorized = await loadUnauthorizedAccess();
|
|
142
188
|
let userProvider;
|
|
143
189
|
if (typeof config.provider.resolver === 'function') {
|
|
144
190
|
userProvider = await config.provider.resolver(app);
|
|
@@ -147,7 +193,7 @@ export function oidcRpGuard(config) {
|
|
|
147
193
|
userProvider = config.provider;
|
|
148
194
|
}
|
|
149
195
|
return (ctx) => {
|
|
150
|
-
return new OidcRpGuard(name, ctx, sessionKey, emitter, userProvider);
|
|
196
|
+
return new OidcRpGuard(name, ctx, sessionKey, emitter, userProvider, unauthorized);
|
|
151
197
|
};
|
|
152
198
|
},
|
|
153
199
|
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ResolvedServerConfig } from '../define_config.js';
|
|
2
|
+
/**
|
|
3
|
+
* The canonical public origin for links we EMAIL (password reset, magic link,
|
|
4
|
+
* OTP unlock, org invitations, email verification/change, security notices).
|
|
5
|
+
*
|
|
6
|
+
* Never derive these from `request.host()` / `request.protocol()` (which reads
|
|
7
|
+
* `X-Forwarded-Proto` under a trusting proxy config): both are client-supplied.
|
|
8
|
+
* An attacker who can reach the server directly (common when the app sits
|
|
9
|
+
* behind a load balancer that does not pin `Host`) can submit a password-reset
|
|
10
|
+
* or magic-link request for a victim's address with a `Host` of their choosing,
|
|
11
|
+
* and the victim's genuine email ends up pointing at attacker-controlled
|
|
12
|
+
* infrastructure. This is classic password-reset poisoning.
|
|
13
|
+
*
|
|
14
|
+
* Defaults to the origin (`scheme://host[:port]`, no trailing slash, mount
|
|
15
|
+
* path stripped) of the resolved `issuer` — the one canonical origin this
|
|
16
|
+
* library already has. Hosts that legitimately serve the same issuer under
|
|
17
|
+
* multiple public hostnames (and therefore want emailed links to follow the
|
|
18
|
+
* request instead of normalizing to a single hostname) can opt out via the
|
|
19
|
+
* `mail.origin` config escape hatch.
|
|
20
|
+
*/
|
|
21
|
+
export declare function authkitOrigin(cfg: ResolvedServerConfig): string;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The canonical public origin for links we EMAIL (password reset, magic link,
|
|
3
|
+
* OTP unlock, org invitations, email verification/change, security notices).
|
|
4
|
+
*
|
|
5
|
+
* Never derive these from `request.host()` / `request.protocol()` (which reads
|
|
6
|
+
* `X-Forwarded-Proto` under a trusting proxy config): both are client-supplied.
|
|
7
|
+
* An attacker who can reach the server directly (common when the app sits
|
|
8
|
+
* behind a load balancer that does not pin `Host`) can submit a password-reset
|
|
9
|
+
* or magic-link request for a victim's address with a `Host` of their choosing,
|
|
10
|
+
* and the victim's genuine email ends up pointing at attacker-controlled
|
|
11
|
+
* infrastructure. This is classic password-reset poisoning.
|
|
12
|
+
*
|
|
13
|
+
* Defaults to the origin (`scheme://host[:port]`, no trailing slash, mount
|
|
14
|
+
* path stripped) of the resolved `issuer` — the one canonical origin this
|
|
15
|
+
* library already has. Hosts that legitimately serve the same issuer under
|
|
16
|
+
* multiple public hostnames (and therefore want emailed links to follow the
|
|
17
|
+
* request instead of normalizing to a single hostname) can opt out via the
|
|
18
|
+
* `mail.origin` config escape hatch.
|
|
19
|
+
*/
|
|
20
|
+
export function authkitOrigin(cfg) {
|
|
21
|
+
return cfg.mail?.origin ?? new URL(cfg.issuer).origin;
|
|
22
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Router } from '@adonisjs/core/http';
|
|
2
2
|
import type { AuthSocialConfig, RateLimitConfigInput } from '../define_config.js';
|
|
3
|
-
import { type AccountPathsOptions } from './account_paths.js';
|
|
3
|
+
import { type AccountPathKey, type AccountPathsOptions } from './account_paths.js';
|
|
4
|
+
import type { PolicyRouteOption } from './config_locks.js';
|
|
4
5
|
import type { SudoMethod } from './sudo/types.js';
|
|
5
6
|
/** Chave da sessão Adonis que registra o timestamp da última atividade (idle timeout). */
|
|
6
7
|
export declare const ACCOUNT_LAST_SEEN_KEY = "authkit_last_seen";
|
|
@@ -108,7 +109,14 @@ export interface AuthHostOptions {
|
|
|
108
109
|
* fechada, mas é a promessa do SPI pela metade; `magicLink()` em particular
|
|
109
110
|
* não teria como ser alcançado em runtime.
|
|
110
111
|
*
|
|
111
|
-
* Ausente → `[password(), passkey()]
|
|
112
|
+
* Ausente → `[password(), passkey(), magicLink()]`, e o config resolvido
|
|
113
|
+
* decide quais deles a tela OFERECE: host com senha recebe
|
|
114
|
+
* `[password, passkey]` (o histórico, byte a byte) e host que declarou
|
|
115
|
+
* `authMethods: { password: false }` recebe `[passkey, magicLink]` — sem isso
|
|
116
|
+
* ele não teria um único método satisfazível. Ver `derivedSudoMethods` em
|
|
117
|
+
* `sudo/runtime.ts`.
|
|
118
|
+
*
|
|
119
|
+
* Passar a opção desliga essa derivação: a lista é do host, ao pé da letra.
|
|
112
120
|
*/
|
|
113
121
|
sudoMethods?: SudoMethod[];
|
|
114
122
|
/**
|
|
@@ -203,8 +211,101 @@ export interface AccountScreensOptions {
|
|
|
203
211
|
/** Apps com acesso / grants de consentimento OIDC (`/account/apps*`). */
|
|
204
212
|
apps?: boolean;
|
|
205
213
|
}
|
|
214
|
+
/**
|
|
215
|
+
* Mapa RESOLVIDO das rotas montadas, devolvido por `registerAuthHost`.
|
|
216
|
+
*
|
|
217
|
+
* Existe porque os overrides de path (`accountRoutes`) chegavam ao servidor
|
|
218
|
+
* (via `accountPath()`) mas NÃO ao frontend: o layout React e os formulários do
|
|
219
|
+
* host tinham de repetir os mesmos paths à mão, e um override era meio-recurso —
|
|
220
|
+
* certo no servidor, errado na UI. Entregue este mapa ao frontend (uma shared
|
|
221
|
+
* prop do Inertia, um `<script type="application/json">`, um endpoint) em vez de
|
|
222
|
+
* hardcodar `href`s.
|
|
223
|
+
*
|
|
224
|
+
* @example
|
|
225
|
+
* const authkitRoutes = registerAuthHost(router)
|
|
226
|
+
* router.get('/', ({ inertia }) => inertia.render('home', { authkitRoutes }))
|
|
227
|
+
*/
|
|
228
|
+
export interface AuthHostRouteMap {
|
|
229
|
+
/** Onde o provider OIDC foi montado (o wildcard é `${mountPath}/*`). */
|
|
230
|
+
mountPath: string;
|
|
231
|
+
/** Console de conta: prefixo, base da JSON API e o path de CADA tela. */
|
|
232
|
+
account: {
|
|
233
|
+
/** Prefixo base resolvido (default `/account`). */
|
|
234
|
+
prefix: string;
|
|
235
|
+
/** Base da JSON API do console de conta (default `/account/api`). */
|
|
236
|
+
api: string;
|
|
237
|
+
/** Path completo de cada tela navegável (`security` → `/account/security`). */
|
|
238
|
+
paths: Record<AccountPathKey, string>;
|
|
239
|
+
/** Destino do redirect de "faça login" em vigor. */
|
|
240
|
+
loginUrl: string;
|
|
241
|
+
/** Quais telas foram efetivamente montadas. */
|
|
242
|
+
screens: Record<keyof AccountScreensOptions, boolean>;
|
|
243
|
+
};
|
|
244
|
+
/** Console admin: prefixo resolvido, ou `null` quando não foi montado. */
|
|
245
|
+
admin: {
|
|
246
|
+
prefix: string;
|
|
247
|
+
} | null;
|
|
248
|
+
/** Admin REST API: prefixo resolvido, ou `null` quando não foi montada. */
|
|
249
|
+
adminApi: {
|
|
250
|
+
prefix: string;
|
|
251
|
+
} | null;
|
|
252
|
+
/** Nomes das rotas nomeadas (as demais herdam o auto-naming do AdonisJS). */
|
|
253
|
+
names: Record<string, string>;
|
|
254
|
+
/** Ids dos métodos de sudo cujas rotas foram montadas, na ordem de montagem. */
|
|
255
|
+
sudoMethods: string[];
|
|
256
|
+
/**
|
|
257
|
+
* Opções de POLÍTICA passadas como argumento que foram IGNORADAS porque o
|
|
258
|
+
* `defineConfig` as declarou (config vence). Vazio no caso normal. Cada uma
|
|
259
|
+
* também sai como `console.warn` no boot — ver a regra de precedência abaixo.
|
|
260
|
+
*/
|
|
261
|
+
overriddenByConfig: PolicyRouteOption[];
|
|
262
|
+
}
|
|
206
263
|
/**
|
|
207
264
|
* Monta todas as rotas do host-kit do Authorization Server numa chamada.
|
|
208
265
|
* Substitui registerOidcRoutes + o hand-wiring do start/routes.ts do host.
|
|
266
|
+
*
|
|
267
|
+
* ── A REGRA DE PRECEDÊNCIA (config × argumento) ─────────────────────────────
|
|
268
|
+
*
|
|
269
|
+
* 1. **Argumento omitido HERDA do config.** `registerAuthHost(router)` é
|
|
270
|
+
* totalmente config-driven; `registerAuthHost(router, { mountPath: '/sso' })`
|
|
271
|
+
* troca o mountPath e herda TODO o resto. Omitir uma chave significa "usa o
|
|
272
|
+
* config", nunca "usa nada" — é o que dispensa repetir `sudo.methods` no
|
|
273
|
+
* `start/routes.ts`.
|
|
274
|
+
*
|
|
275
|
+
* 2. **Chaves ESTRUTURAIS: o argumento vence.** `mountPath`, os prefixos
|
|
276
|
+
* (`admin.prefix`, `adminApi.prefix`, `accountRoutes.prefix`), os segmentos
|
|
277
|
+
* de tela, `account` (quais telas montar) e `accountLoginUrl`. São decisões
|
|
278
|
+
* do ponto de chamada por natureza, e a forma de função é estritamente mais
|
|
279
|
+
* expressiva (dá para montar duas vezes sob dois prefixos — nenhum config
|
|
280
|
+
* expressa isso).
|
|
281
|
+
*
|
|
282
|
+
* 3. **Chaves de POLÍTICA: o config vence, e trava.** `social`, `rateLimit`,
|
|
283
|
+
* `sudoMethods` e o liga/desliga de `admin`/`adminApi` decidem o que é
|
|
284
|
+
* PERMITIDO. Quando o `defineConfig` as declara, o argumento NÃO as altera —
|
|
285
|
+
* senão o `config/authkit.ts` deixa de ser auditável e seria preciso ler o
|
|
286
|
+
* `start/routes.ts` de cada app para saber o que está valendo. Mesma regra
|
|
287
|
+
* (e mesma derivação) de `defineConfig({ authMethods })`, que já fixa os
|
|
288
|
+
* métodos de login contra o runtime. Ver `deriveLockedRouteOptions`.
|
|
289
|
+
* A divergência NÃO é silenciosa: sai um `console.warn` nomeando a chave e
|
|
290
|
+
* a chave aparece em `AuthHostRouteMap.overriddenByConfig`.
|
|
291
|
+
*
|
|
292
|
+
* Devolve o {@link AuthHostRouteMap} resolvido — entregue-o ao frontend em vez
|
|
293
|
+
* de hardcodar `href`s.
|
|
294
|
+
*/
|
|
295
|
+
export declare function registerAuthHost(router: Router, opts?: AuthHostOptions): AuthHostRouteMap;
|
|
296
|
+
/**
|
|
297
|
+
* Auto-montagem a partir de `config.routes` (R1). Chamada UMA vez pelo
|
|
298
|
+
* `boot()` do provider.
|
|
299
|
+
*
|
|
300
|
+
* O CAMINHO AUTOMÁTICO É O CAMINHO MANUAL: chama literalmente
|
|
301
|
+
* `registerAuthHost`, sem nenhuma segunda implementação. Toda a resolução
|
|
302
|
+
* (herança do config, precedência política × estrutural, mapa de retorno) é a
|
|
303
|
+
* mesma — duas implementações de um comportamento sempre divergem.
|
|
304
|
+
*
|
|
305
|
+
* Não recebe opções: os defaults ESTRUTURAIS vêm de `config.routes` pelo stash
|
|
306
|
+
* (`AuthHostRuntimeConfig.routes`), pelo mesmo caminho que uma chamada manual
|
|
307
|
+
* os leria.
|
|
308
|
+
*
|
|
309
|
+
* @internal
|
|
209
310
|
*/
|
|
210
|
-
export declare function
|
|
311
|
+
export declare function autoMountAuthHost(router: Router): AuthHostRouteMap;
|
|
@@ -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
|
}
|