@adonis-agora/authkit-server 0.45.0 → 0.47.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/build/commands/ui_preset.js +15 -1
  2. package/build/host/views/account/confirm.edge +118 -52
  3. package/build/host/views/account/mfa.edge +1 -1
  4. package/build/host/views/login.edge +2 -4
  5. package/build/host/views/mfa-challenge.edge +1 -1
  6. package/build/host/views/otp-unlock.edge +2 -2
  7. package/build/host/views/partials/styles.edge +1 -1
  8. package/build/index.d.ts +5 -1
  9. package/build/index.js +15 -1
  10. package/build/providers/authkit_server_provider.js +7 -0
  11. package/build/services/booted_app.d.ts +8 -0
  12. package/build/services/booted_app.js +27 -0
  13. package/build/services/main.d.ts +7 -0
  14. package/build/services/main.js +9 -1
  15. package/build/src/define_config.d.ts +51 -0
  16. package/build/src/define_config.js +8 -0
  17. package/build/src/host/account_login_url.d.ts +41 -0
  18. package/build/src/host/account_login_url.js +50 -0
  19. package/build/src/host/admin_sessions_service.d.ts +3 -1
  20. package/build/src/host/admin_sessions_service.js +35 -3
  21. package/build/src/host/assets/webauthn.js +2 -0
  22. package/build/src/host/console_session.d.ts +4 -0
  23. package/build/src/host/console_session.js +9 -2
  24. package/build/src/host/controllers/account_confirm_controller.d.ts +11 -14
  25. package/build/src/host/controllers/account_confirm_controller.js +42 -130
  26. package/build/src/host/controllers/account_orgs_controller.js +7 -4
  27. package/build/src/host/controllers/account_security_controller.js +3 -2
  28. package/build/src/host/controllers/account_session_controller.js +23 -5
  29. package/build/src/host/controllers/account_tokens_controller.js +11 -0
  30. package/build/src/host/controllers/pat_introspection_controller.js +10 -0
  31. package/build/src/host/controllers/webauthn_asset_controller.d.ts +22 -0
  32. package/build/src/host/controllers/webauthn_asset_controller.js +66 -0
  33. package/build/src/host/i18n.d.ts +14 -0
  34. package/build/src/host/i18n.js +16 -0
  35. package/build/src/host/impersonation_session.js +15 -0
  36. package/build/src/host/middleware/account_auth.js +3 -1
  37. package/build/src/host/rate_limit.d.ts +10 -0
  38. package/build/src/host/rate_limit.js +6 -0
  39. package/build/src/host/register_auth_host.d.ts +85 -2
  40. package/build/src/host/register_auth_host.js +188 -61
  41. package/build/src/host/renderers/edge_renderer.js +6 -1
  42. package/build/src/host/renderers/inertia_renderer.d.ts +55 -0
  43. package/build/src/host/renderers/inertia_renderer.js +55 -0
  44. package/build/src/host/sudo/index.d.ts +47 -0
  45. package/build/src/host/sudo/index.js +41 -0
  46. package/build/src/host/sudo/methods/magic_link.d.ts +43 -0
  47. package/build/src/host/sudo/methods/magic_link.js +174 -0
  48. package/build/src/host/sudo/methods/oidc_step_up.d.ts +68 -0
  49. package/build/src/host/sudo/methods/oidc_step_up.js +78 -0
  50. package/build/src/host/sudo/methods/passkey.d.ts +28 -0
  51. package/build/src/host/sudo/methods/passkey.js +139 -0
  52. package/build/src/host/sudo/methods/password.d.ts +19 -0
  53. package/build/src/host/sudo/methods/password.js +93 -0
  54. package/build/src/host/sudo/runtime.d.ts +141 -0
  55. package/build/src/host/sudo/runtime.js +327 -0
  56. package/build/src/host/sudo/types.d.ts +93 -0
  57. package/build/src/host/sudo/types.js +1 -0
  58. package/build/src/host/sudo_mode.d.ts +116 -6
  59. package/build/src/host/sudo_mode.js +133 -7
  60. package/package.json +6 -2
  61. package/build/stubs/ui/edge/views/consent.edge +0 -13
  62. package/build/stubs/ui/edge/views/login.edge +0 -19
  63. package/stubs/ui/edge/views/consent.edge +0 -13
  64. package/stubs/ui/edge/views/login.edge +0 -19
@@ -8,6 +8,20 @@ import { setAdminPrefix, normalizeAdminPrefix, setAdminApiPrefix, normalizeAdmin
8
8
  import { resolveRuntimeSettings } from './runtime_settings.js';
9
9
  import { resolveEffectiveSessionPolicy } from './runtime_toggles.js';
10
10
  import { getAuthHostConfig } from './auth_host_config.js';
11
+ import { setAccountLoginUrl, getAccountLoginUrl } from './account_login_url.js';
12
+ import { completeSudo, fail, guardSudoRoutes, sudoContextFrom, setMountedSudoMethods, } from './sudo/runtime.js';
13
+ import { password as sudoPassword } from './sudo/methods/password.js';
14
+ import { passkey as sudoPasskey } from './sudo/methods/passkey.js';
15
+ /**
16
+ * Métodos de sudo montados quando o host não passa `sudoMethods` —
17
+ * comportamento histórico (senha + passkey).
18
+ *
19
+ * PONTO ÚNICO. A tela não tem mais uma cópia desta lista: sem
20
+ * `config.sudo.methods`, `configuredSudoMethods` cai no que FOI MONTADO, ou
21
+ * seja, no resultado do `??` abaixo. Duas listas de default é como os dois
22
+ * lados divergiam.
23
+ */
24
+ const SUDO_METHOD_DEFAULTS = [sudoPassword(), sudoPasskey()];
11
25
  /** Chave da sessão Adonis que registra o timestamp da última atividade (idle timeout). */
12
26
  export const ACCOUNT_LAST_SEEN_KEY = 'authkit_last_seen';
13
27
  /**
@@ -55,16 +69,26 @@ async function checkAndRefreshIdle(ctx) {
55
69
  * @internal
56
70
  */
57
71
  function buildLoginRedirect(ctx, extra) {
72
+ // Destino configurável (`accountLoginUrl`): default `/account/login`, mas um
73
+ // host que desmontou a tela de login (`account: { login: false }`) aponta para
74
+ // a própria rota de login dele (ex.: `/login`). Ver `account_login_url.ts`.
75
+ const loginUrl = getAccountLoginUrl();
58
76
  const url = ctx.request?.url?.() ?? '';
59
77
  const qs = ctx.request?.parsedUrl?.search ?? '';
60
78
  const dest = qs ? `${url}${qs}` : url;
61
79
  // Só inclui return_to quando há um caminho real (não vazio, não é o próprio login).
62
- if (dest && dest !== '/' && !dest.startsWith('/account/login')) {
80
+ if (dest && dest !== '/' && !dest.startsWith(loginUrl)) {
63
81
  const encoded = encodeURIComponent(dest);
64
- const base = extra ? `/account/login?${extra}&return_to=${encoded}` : `/account/login?return_to=${encoded}`;
82
+ const sep = loginUrl.includes('?') ? '&' : '?';
83
+ const base = extra
84
+ ? `${loginUrl}${sep}${extra}&return_to=${encoded}`
85
+ : `${loginUrl}${sep}return_to=${encoded}`;
65
86
  return base;
66
87
  }
67
- return extra ? `/account/login?${extra}` : '/account/login';
88
+ if (!extra)
89
+ return loginUrl;
90
+ const sep = loginUrl.includes('?') ? '&' : '?';
91
+ return `${loginUrl}${sep}${extra}`;
68
92
  }
69
93
  /**
70
94
  * Guard inline do console de conta. Usamos uma closure (forma confiável do
@@ -87,9 +111,11 @@ const accountGuard = async (ctx, next) => {
87
111
  * Guard do console admin (B6). Como o `accountGuard`, é uma closure inline (forma
88
112
  * confiável do `.use()` num grupo). Exige:
89
113
  * 0. `config.admin.enabled` ligado (senão → 404; ver nota de flag-drift abaixo);
90
- * 1. sessão de conta ativa (senão → /account/login);
114
+ * 1. sessão de conta ativa (senão → `accountLoginUrl`, default /account/login);
91
115
  * 2. a conta logada com pelo menos UMA das `config.admin.roles` nas roles globais
92
- * (senão → /account/tokens, evitando vazar a existência do /admin).
116
+ * (senão → `accountHome(cfg)`, default /account/security NÃO revela a
117
+ * existência do /admin, e cai numa tela que o host controla via `accountHome`;
118
+ * se a tela default estiver desmontada, aponte `config.accountHome` para uma montada).
93
119
  * As roles permitidas são resolvidas em runtime do `authkit.server` (config lazy).
94
120
  */
95
121
  export const adminGuard = async (ctx, next) => {
@@ -138,6 +164,7 @@ const C = {
138
164
  accountMfa: () => import('./controllers/account_mfa_controller.js'),
139
165
  accountOrgs: () => import('./controllers/account_orgs_controller.js'),
140
166
  accountConfirm: () => import('./controllers/account_confirm_controller.js'),
167
+ webauthnAsset: () => import('./controllers/webauthn_asset_controller.js'),
141
168
  // Console React JSON API (session-authed, under {prefix}/api/*).
142
169
  consoleShell: () => import('./admin_console/admin_shell_controller.js'),
143
170
  consoleOverview: () => import('./admin_console/console_overview_controller.js'),
@@ -174,6 +201,29 @@ export function registerAuthHost(router, opts = {}) {
174
201
  const social = opts.social ?? hostCfg?.social;
175
202
  const adminOpt = opts.admin ?? (hostCfg?.adminEnabled ? true : undefined);
176
203
  const adminApiOpt = opts.adminApi ?? (hostCfg?.adminApiEnabled ? true : undefined);
204
+ // Destino do redirect de "faça login" — persiste no singleton de processo para
205
+ // que os guards (closures de tempo de registro), o middleware, os controllers e
206
+ // as views leiam o mesmo valor. Só quando a opção foi passada (senão fica o
207
+ // default `/account/login` do singleton — back-compat).
208
+ if (opts.accountLoginUrl !== undefined) {
209
+ setAccountLoginUrl(opts.accountLoginUrl);
210
+ }
211
+ // Montagem por tela do console de conta. `undefined` → tudo montado;
212
+ // `false` → nada; objeto → cada flag ausente default `true`.
213
+ const accountOpt = opts.account;
214
+ const mountScreen = (key) => {
215
+ if (accountOpt === false)
216
+ return false;
217
+ if (accountOpt && typeof accountOpt === 'object')
218
+ return accountOpt[key] !== false;
219
+ return true;
220
+ };
221
+ const mountLogin = mountScreen('login');
222
+ const mountTokens = mountScreen('tokens');
223
+ const mountOrgs = mountScreen('orgs');
224
+ const mountSecurity = mountScreen('security');
225
+ const mountMfa = mountScreen('mfa');
226
+ const mountApps = mountScreen('apps');
177
227
  // Throttles opt-in (anti-brute-force). `undefined` quando rate-limit desligado.
178
228
  const resolvedRateLimit = opts.rateLimit !== undefined
179
229
  ? resolveRateLimit(opts.rateLimit)
@@ -188,6 +238,25 @@ export function registerAuthHost(router, opts = {}) {
188
238
  if (throttles)
189
239
  route.use([throttles.introspection]);
190
240
  };
241
+ // Bucket PRÓPRIO das rotas de sudo. Antes elas levavam o `withLogin`, o que
242
+ // funcionava mas somava dois orçamentos que medem coisas diferentes: login é
243
+ // um anônimo adivinhando credenciais, sudo é um usuário JÁ autenticado
244
+ // reprovando a própria identidade. Ver `ResolvedRateLimitConfig.sudo`.
245
+ const withSudo = (route) => {
246
+ if (throttles)
247
+ route.use([throttles.sudo]);
248
+ };
249
+ // ─── Assets estáticos do host-kit (públicos, sem autenticação) ─────────────
250
+ // Bundle do @simplewebauthn/browser servido pelo próprio app, no lugar do
251
+ // import de CDN público que as views de login/MFA/confirm faziam.
252
+ //
253
+ // Path FIXO e no topo, de propósito:
254
+ // • não pode viver sob o prefixo do console admin (`admin` é opt-in) —
255
+ // login.edge e mfa-challenge.edge precisam do script em qualquer host;
256
+ // • sem guard, porque é carregado NA tela de login, antes de haver sessão;
257
+ // • registrado ANTES do wildcard `${mount}/*` para que nenhum mountPath
258
+ // agressivo (ex.: '/') consiga engolir o asset e quebrar o login.
259
+ router.get('/authkit/assets/webauthn.js', [C.webauthnAsset]).as('authkit.assets.webauthn');
191
260
  // Provider OIDC (wildcard + root) — o que registerOidcRoutes fazia.
192
261
  router.any(`${mount}/*`, [C.oidc]).as('authkit.oidc.wildcard');
193
262
  router.any(mount, [C.oidc]).as('authkit.oidc.root');
@@ -225,68 +294,126 @@ export function registerAuthHost(router, opts = {}) {
225
294
  // PAT introspection (server-to-server).
226
295
  withIntrospection(router.post('/authkit/pat/introspect', [C.patIntrospection, 'handle']));
227
296
  // Organizations — invitation accept (sem guard: controller lida com não-autenticado).
228
- router.get('/account/orgs/invitations/:token/accept', [C.accountOrgs, 'showAcceptInvitation']);
229
- router.post('/account/orgs/invitations/:token/accept', [C.accountOrgs, 'acceptInvitation']);
297
+ // Parte da tela `orgs`: desmontada junto (sem multi-tenancy, não há convite a aceitar).
298
+ if (mountOrgs) {
299
+ router.get('/account/orgs/invitations/:token/accept', [C.accountOrgs, 'showAcceptInvitation']);
300
+ router.post('/account/orgs/invitations/:token/accept', [C.accountOrgs, 'acceptInvitation']);
301
+ }
230
302
  // Console de conta (login de sessão do IdP + gerência de PAT).
231
- router.get('/account/login', [C.accountSession, 'show']);
232
- // L6: throttle por IP no login/logout do console de conta (anti-brute-force),
233
- // alinhado com as demais rotas de credencial (interaction, forgot, reset).
234
- withLogin(router.post('/account/login', [C.accountSession, 'login']));
235
- withLogin(router.post('/account/logout', [C.accountSession, 'logout']));
303
+ // Tela `login` desmontável: hosts passwordless delegam ao OIDC próprio e
304
+ // apontam `accountLoginUrl` para a rota de login deles.
305
+ if (mountLogin) {
306
+ router.get('/account/login', [C.accountSession, 'show']);
307
+ // L6: throttle por IP no login/logout do console de conta (anti-brute-force),
308
+ // alinhado com as demais rotas de credencial (interaction, forgot, reset).
309
+ withLogin(router.post('/account/login', [C.accountSession, 'login']));
310
+ withLogin(router.post('/account/logout', [C.accountSession, 'logout']));
311
+ }
236
312
  // Confirmação de troca de e-mail (standalone, GET-only — consome o token do link;
237
- // pode ser aberta em outro dispositivo, então NÃO exige sessão).
238
- router.get('/account/email/confirm', [C.accountSecurity, 'confirmEmail']);
313
+ // pode ser aberta em outro dispositivo, então NÃO exige sessão). Parte da tela
314
+ // `security` (o terminal do fluxo de troca de e-mail).
315
+ if (mountSecurity) {
316
+ router.get('/account/email/confirm', [C.accountSecurity, 'confirmEmail']);
317
+ }
239
318
  // Rotas de tokens protegidas por AccountAuthMiddleware (redireciona para /account/login se não autenticado).
240
319
  router
241
320
  .group(() => {
242
- router.get('/account/tokens', [C.accountTokens, 'index']);
243
- router.post('/account/tokens', [C.accountTokens, 'store']);
244
- router.post('/account/tokens/:id/revoke', [C.accountTokens, 'destroy']);
245
- // Segurança da conta: trocar senha + solicitar troca de e-mail + perfil.
246
- router.get('/account/security', [C.accountSecurity, 'index']);
247
- router.post('/account/security/password', [C.accountSecurity, 'changePassword']);
248
- router.post('/account/security/email', [C.accountSecurity, 'changeEmail']);
249
- router.post('/account/security/email/cancel', [C.accountSecurity, 'cancelEmailChange']);
250
- router.post('/account/security/profile', [C.accountSecurity, 'updateProfile']);
251
- // LGPD/GDPR: export de dados (portabilidade) + deleção self-service (danger zone).
252
- // O export carrega o throttle de login (anti-abuso) quando o rate-limit existe.
253
- withLogin(router.get('/account/security/export', [C.accountSecurity, 'exportData']));
254
- router.post('/account/security/delete', [C.accountSecurity, 'deleteAccount']);
255
- // Trusted devices: limpa o cookie de confiança DESTE navegador.
256
- router.post('/account/security/trusted-devices/revoke', [
257
- C.accountSecurity,
258
- 'revokeTrustedDevices',
259
- ]);
260
- // Apps com acesso (consentimento): lista os grants da conta + revogação por client.
261
- router.get('/account/apps', [C.accountApps, 'index']);
262
- router.post('/account/apps/:clientId/revoke', [C.accountApps, 'revoke']);
263
- // MFA / TOTP (enrollment, confirmação, disable).
264
- router.get('/account/mfa', [C.accountMfa, 'index']);
265
- router.post('/account/mfa/enroll', [C.accountMfa, 'enroll']);
266
- router.post('/account/mfa/confirm', [C.accountMfa, 'confirm']);
267
- router.post('/account/mfa/disable', [C.accountMfa, 'disable']);
268
- // MFA / WebAuthn (passkeys): registro (begin/finish) + remoção.
269
- router.post('/account/mfa/passkeys/options', [C.accountMfa, 'passkeyRegisterOptions']);
270
- router.post('/account/mfa/passkeys/verify', [C.accountMfa, 'passkeyRegisterVerify']);
271
- router.post('/account/mfa/passkeys/:id/remove', [C.accountMfa, 'passkeyRemove']);
272
- // Sudo mode (confirm identity): GET exibe o formulário; POST verifica a senha.
321
+ // Personal Access Tokens (tela `tokens`).
322
+ if (mountTokens) {
323
+ router.get('/account/tokens', [C.accountTokens, 'index']);
324
+ router.post('/account/tokens', [C.accountTokens, 'store']);
325
+ router.post('/account/tokens/:id/revoke', [C.accountTokens, 'destroy']);
326
+ }
327
+ // Segurança da conta: trocar senha + solicitar troca de e-mail + perfil (tela `security`).
328
+ if (mountSecurity) {
329
+ router.get('/account/security', [C.accountSecurity, 'index']);
330
+ router.post('/account/security/password', [C.accountSecurity, 'changePassword']);
331
+ router.post('/account/security/email', [C.accountSecurity, 'changeEmail']);
332
+ router.post('/account/security/email/cancel', [C.accountSecurity, 'cancelEmailChange']);
333
+ router.post('/account/security/profile', [C.accountSecurity, 'updateProfile']);
334
+ // LGPD/GDPR: export de dados (portabilidade) + deleção self-service (danger zone).
335
+ // O export carrega o throttle de login (anti-abuso) quando o rate-limit existe.
336
+ withLogin(router.get('/account/security/export', [C.accountSecurity, 'exportData']));
337
+ router.post('/account/security/delete', [C.accountSecurity, 'deleteAccount']);
338
+ // Trusted devices: limpa o cookie de confiança DESTE navegador.
339
+ router.post('/account/security/trusted-devices/revoke', [
340
+ C.accountSecurity,
341
+ 'revokeTrustedDevices',
342
+ ]);
343
+ }
344
+ // Apps com acesso (consentimento): lista os grants da conta + revogação por client (tela `apps`).
345
+ if (mountApps) {
346
+ router.get('/account/apps', [C.accountApps, 'index']);
347
+ router.post('/account/apps/:clientId/revoke', [C.accountApps, 'revoke']);
348
+ }
349
+ // MFA — TOTP + passkeys (tela `mfa`).
350
+ if (mountMfa) {
351
+ // MFA / TOTP (enrollment, confirmação, disable).
352
+ router.get('/account/mfa', [C.accountMfa, 'index']);
353
+ router.post('/account/mfa/enroll', [C.accountMfa, 'enroll']);
354
+ router.post('/account/mfa/confirm', [C.accountMfa, 'confirm']);
355
+ router.post('/account/mfa/disable', [C.accountMfa, 'disable']);
356
+ // MFA / WebAuthn (passkeys): registro (begin/finish) + remoção.
357
+ router.post('/account/mfa/passkeys/options', [C.accountMfa, 'passkeyRegisterOptions']);
358
+ router.post('/account/mfa/passkeys/verify', [C.accountMfa, 'passkeyRegisterVerify']);
359
+ router.post('/account/mfa/passkeys/:id/remove', [C.accountMfa, 'passkeyRemove']);
360
+ }
361
+ // Sudo mode (confirm identity): o GET lista os métodos; cada método
362
+ // registra suas próprias rotas de verificação (SPI `SudoMethod`).
273
363
  router.get('/account/confirm', [C.accountConfirm, 'show']);
274
- router.post('/account/confirm', [C.accountConfirm, 'confirm']);
275
- router.post('/account/confirm/passkey/options', [C.accountConfirm, 'passkeyOptions']);
276
- router.post('/account/confirm/passkey', [C.accountConfirm, 'passkeyConfirm']);
277
- // Organizations (multi-tenancy) sempre montadas; controller retorna 404/403 sem tabelas.
278
- router.get('/account/orgs', [C.accountOrgs, 'index']);
279
- router.post('/account/orgs', [C.accountOrgs, 'store']);
280
- router.post('/account/orgs/deactivate', [C.accountOrgs, 'deactivate']);
281
- router.post('/account/orgs/:id/activate', [C.accountOrgs, 'activate']);
282
- router.post('/account/orgs/:id/leave', [C.accountOrgs, 'leave']);
283
- router.post('/account/orgs/:id/invite', [C.accountOrgs, 'invite']);
284
- router.post('/account/orgs/:id/members/:accountId/remove', [C.accountOrgs, 'removeMember']);
285
- router.post('/account/orgs/:id/invitations/:invId/revoke', [C.accountOrgs, 'revokeInvitation']);
286
- // JSON endpoints for React hooks (authkit-react).
287
- router.get('/account/orgs/json', [C.accountOrgs, 'listJson']);
288
- router.get('/account/orgs/invitations/json', [C.accountOrgs, 'listInvitationsJson']);
289
- router.get('/account/orgs/:id/json', [C.accountOrgs, 'showJson']);
364
+ // Rotas próprias dos métodos de sudo — DENTRO do grupo com `accountGuard`.
365
+ // O guard não é só "tem sessão": ele roda `checkAndRefreshIdle`, que apaga
366
+ // a sessão vencida por idle e refresca `authkit_last_seen`. Fora do grupo,
367
+ // uma sessão já vencida (ainda não colhida) podia postar a senha correta e
368
+ // receber `markSudo` — e as rotas de sudo não refrescavam o last-seen.
369
+ // Nenhum método built-in é alcançável por GET vindo de e-mail: o token de
370
+ // sudo por magic link vive na PRÓPRIA sessão, então o usuário precisa
371
+ // estar logado no mesmo navegador de qualquer forma. Um método que
372
+ // genuinamente não puder ficar sob o guard precisa de uma decisão
373
+ // explícita, não de mover todos para fora.
374
+ const helpers = {
375
+ contextFrom: sudoContextFrom,
376
+ completeSudo,
377
+ fail,
378
+ };
379
+ const sudoMethodsToMount = opts?.sudoMethods ?? SUDO_METHOD_DEFAULTS;
380
+ for (const method of sudoMethodsToMount) {
381
+ // `guardSudoRoutes` embrulha os handlers que o método registrar, para
382
+ // que `config.sudo.methods` os desabilite de fato mesmo que o método
383
+ // não tenha checado nada por dentro. Ver o docblock lá.
384
+ //
385
+ // `withSudo` vai junto: TODA rota de um método de sudo leva o throttle
386
+ // do bucket de SUDO (no-op sem rate-limit). Não é adorno — o POST que
387
+ // emite o magic link de sudo dispara um e-mail por chamada, e o
388
+ // `accountGuard` sozinho só exige uma sessão viva, que o abusador tem.
389
+ // Aplicar aqui, no wrapper, cobre também os métodos customizados, que
390
+ // não teriam como pedir throttle pelo `SudoRouteHelpers`.
391
+ //
392
+ // Bucket próprio, não o de login: mesmos limites, contagem separada —
393
+ // errar a senha na tela de confirmação não pode gastar o orçamento de
394
+ // login do IP, nem vice-versa.
395
+ method.register?.(guardSudoRoutes(router, method.id, helpers, withSudo), helpers);
396
+ }
397
+ // A lista montada é a fonte de verdade dos DOIS lados quando o host não
398
+ // configura `config.sudo.methods`: a tela oferece exatamente isto, e os
399
+ // handlers aceitam exatamente isto.
400
+ setMountedSudoMethods(sudoMethodsToMount);
401
+ // Organizations (multi-tenancy) — tela `orgs`. Montadas por default;
402
+ // controller retorna 404/403 sem tabelas (capability-probed).
403
+ if (mountOrgs) {
404
+ router.get('/account/orgs', [C.accountOrgs, 'index']);
405
+ router.post('/account/orgs', [C.accountOrgs, 'store']);
406
+ router.post('/account/orgs/deactivate', [C.accountOrgs, 'deactivate']);
407
+ router.post('/account/orgs/:id/activate', [C.accountOrgs, 'activate']);
408
+ router.post('/account/orgs/:id/leave', [C.accountOrgs, 'leave']);
409
+ router.post('/account/orgs/:id/invite', [C.accountOrgs, 'invite']);
410
+ router.post('/account/orgs/:id/members/:accountId/remove', [C.accountOrgs, 'removeMember']);
411
+ router.post('/account/orgs/:id/invitations/:invId/revoke', [C.accountOrgs, 'revokeInvitation']);
412
+ // JSON endpoints for React hooks (authkit-react).
413
+ router.get('/account/orgs/json', [C.accountOrgs, 'listJson']);
414
+ router.get('/account/orgs/invitations/json', [C.accountOrgs, 'listInvitationsJson']);
415
+ router.get('/account/orgs/:id/json', [C.accountOrgs, 'showJson']);
416
+ }
290
417
  // ─── Account self-service JSON API (authkit-react TanStack hooks) ─────
291
418
  // ⚠️ ORDER MATTERS: fixed-segment routes before parameterised ones.
292
419
  // Registered INSIDE the accountGuard group → same session-auth protection.
@@ -1,4 +1,5 @@
1
1
  import { DEFAULT_MESSAGES, translate } from '../i18n.js';
2
+ import { getAccountLoginUrl } from '../account_login_url.js';
2
3
  /**
3
4
  * Resolve o catálogo de mensagens ativo a partir do `authkit.server` (config
4
5
  * resolvida com o locale do host). Defensivo: se o container/serviço não estiver
@@ -28,7 +29,11 @@ async function resolveMessagesFromCtx(ctx) {
28
29
  export async function renderEdgeView(ctx, view, props) {
29
30
  const messages = await resolveMessagesFromCtx(ctx);
30
31
  const t = (key, params) => translate(messages, key, params);
31
- return ctx.view.render(`authkit::${view}`, { ...props, t, messages });
32
+ // `loginUrl` como prop global: as views que linkam "faça login" (ex.: `otp-unlock`)
33
+ // usam o destino configurável (`accountLoginUrl`) em vez do `/account/login` fixo,
34
+ // que pode estar desmontado. Props explícitas ainda têm precedência (spread depois).
35
+ const loginUrl = getAccountLoginUrl();
36
+ return ctx.view.render(`authkit::${view}`, { loginUrl, ...props, t, messages });
32
37
  }
33
38
  /** Renderer do seam para hosts Edge. As views são donas-da-lib (disco `authkit::`). */
34
39
  export function edgeRenderer() {
@@ -66,5 +66,60 @@ export type AuthkitScreen = 'login' | 'signup' | 'consent' | 'forgot' | 'reset'
66
66
  * | `returnTo` | `string \| null` | Caminho interno de destino pós-login (já validado pelo servidor — só caminhos internos). Quando presente, o formulário deve incluir `<input type="hidden" name="return_to" value={returnTo} />`. O servidor revalida o valor no POST; hosts com tela custom precisam propagar esse hidden input. |
67
67
  * | `error` | `string \| undefined` | Mensagem de erro de autenticação localizada (credenciais inválidas, conta bloqueada, etc.). |
68
68
  * | `messages` | `AuthMessages` | Catálogo de mensagens i18n. |
69
+ *
70
+ * ### Props da tela `account/security`
71
+ *
72
+ * | Prop | Tipo | Descrição |
73
+ * |-------------------------|---------------------|-----------|
74
+ * | `csrfToken` | `string` | Token CSRF para os formulários da tela. |
75
+ * | `supported` | `boolean` | `false` quando o store não suporta o self-service de segurança (troca de senha/e-mail) — a tela deve degradar. |
76
+ * | `profileSupported` | `boolean` | `true` quando o store suporta editar nome/avatar (`updateProfile`). |
77
+ * | `avatarUploadSupported` | `boolean` | `true` quando algum backend (drive OU media) pode armazenar o upload de avatar. |
78
+ * | `email` | `string` | E-mail atual da conta (`''` se ausente). |
79
+ * | `name` | `string` | Nome atual da conta (`''` se ausente). |
80
+ * | `avatarUrl` | `string` | URL do avatar atual (`''` se ausente). |
81
+ * | `passwordChanged` | `string \| null` | Flash de sucesso da troca de senha (mensagem localizada) ou `null`. |
82
+ * | `emailChangeRequested` | `string \| null` | Flash: link de confirmação de troca de e-mail enviado (ou cancelamento) ou `null`. |
83
+ * | `emailChanged` | `string \| null` | Flash: troca de e-mail concluída ou `null`. |
84
+ * | `profileUpdated` | `string \| null` | Flash: perfil atualizado ou `null`. |
85
+ * | `error` | `string \| null` | Flash de erro de segurança (senha inválida, e-mail em uso, política violada) ou `null`. |
86
+ * | `trustedDevicesEnabled` | `boolean` | `true` quando o recurso de dispositivos confiáveis está ligado. |
87
+ * | `trustedDevicesRevoked` | `string \| null` | Flash: confiança deste navegador revogada ou `null`. |
88
+ * | `sessionsSupported` | `boolean` | `true` quando o adapter OIDC enumera as sessões ativas da conta. |
89
+ * | `sessions` | `Array<{ loginTs: string; browser: string; os: string; ip: string; location: string }>` | Sessões ativas da própria conta (vazio quando não suportado). `loginTs` é ISO ou `''`. |
90
+ * | `exportSupported` | `boolean` | Sempre `true` — export de dados (portabilidade/LGPD) disponível para a conta logada. |
91
+ * | `deletionSupported` | `boolean` | `true` quando o store suporta hard delete (danger zone). |
92
+ * | `deleteError` | `string \| null` | Flash de erro da confirmação de deleção ou `null`. |
93
+ * | `messages` | `AuthMessages` | Catálogo de mensagens i18n. |
94
+ *
95
+ * ### Props da tela `account/mfa`
96
+ *
97
+ * | Prop | Tipo | Descrição |
98
+ * |---------------------|---------------------|-----------|
99
+ * | `csrfToken` | `string` | Token CSRF para os formulários de enroll/confirm/disable e passkeys. |
100
+ * | `enabled` | `boolean` | `true` quando o TOTP já está confirmado (habilitado) para a conta. |
101
+ * | `recoveryCodes` | `string[] \| null` | Códigos de recuperação recém-gerados (exibidos UMA vez após enroll/confirm) ou `null`. |
102
+ * | `passkeysSupported` | `boolean` | `true` quando o store persiste credenciais WebAuthn (passkeys). |
103
+ * | `passkeys` | `Array<{ id: string; label?: string; createdAt: string }>` | Passkeys cadastradas (vazio quando não suportado). `id` é base64url; `createdAt` é ISO. |
104
+ * | `messages` | `AuthMessages` | Catálogo de mensagens i18n. |
105
+ *
106
+ * ### Props da tela `account/confirm` (sudo — confirmar identidade)
107
+ *
108
+ * | Prop | Tipo | Descrição |
109
+ * |---------------|---------------------|-----------|
110
+ * | `csrfToken` | `string` | Token CSRF para o POST de cada método. |
111
+ * | `returnTo` | `string \| null` | Caminho interno de destino após confirmar (validado pelo servidor) ou `null`. |
112
+ * | `error` | `string \| null` | Flash de erro da última tentativa de confirmação ou `null`. |
113
+ * | `notice` | `string \| null` | Flash informativo (ex.: "link de confirmação enviado") ou `null`. |
114
+ * | `methods` | `Array<{ id: string; labelKey: string; kind: 'form' \| 'action' \| 'redirect' \| 'webauthn'; endpoint: string; fields?: Array<{ name: string; type: 'password' \| 'text'; labelKey: string }> }>` | Métodos de sudo disponíveis para a conta. A tela renderiza por `kind`; `endpoint` é o POST de verificação (`webauthn` pede options em `${endpoint}/options`). |
115
+ * | `preferredId` | `string \| null` | `id` do último método usado (destaque na UI) ou `null`. |
116
+ * | `messages` | `AuthMessages` | Catálogo de mensagens i18n. |
117
+ *
118
+ * ### Props da tela `account/email-confirmed` (terminal do link de troca de e-mail)
119
+ *
120
+ * | Prop | Tipo | Descrição |
121
+ * |------------|----------------|-----------|
122
+ * | `ok` | `boolean` | `true` quando o token era válido e o novo e-mail foi aplicado; `false` para token inválido/expirado ou store sem suporte. A tela mostra sucesso ou falha conforme o valor. |
123
+ * | `messages` | `AuthMessages` | Catálogo de mensagens i18n. |
69
124
  */
70
125
  export declare function inertiaRenderer(opts: InertiaRendererOptions): (ctx: HttpContext, view: string, props: Record<string, unknown>) => Promise<any>;
@@ -37,6 +37,61 @@ async function resolveMessagesFromCtx(ctx) {
37
37
  * | `returnTo` | `string \| null` | Caminho interno de destino pós-login (já validado pelo servidor — só caminhos internos). Quando presente, o formulário deve incluir `<input type="hidden" name="return_to" value={returnTo} />`. O servidor revalida o valor no POST; hosts com tela custom precisam propagar esse hidden input. |
38
38
  * | `error` | `string \| undefined` | Mensagem de erro de autenticação localizada (credenciais inválidas, conta bloqueada, etc.). |
39
39
  * | `messages` | `AuthMessages` | Catálogo de mensagens i18n. |
40
+ *
41
+ * ### Props da tela `account/security`
42
+ *
43
+ * | Prop | Tipo | Descrição |
44
+ * |-------------------------|---------------------|-----------|
45
+ * | `csrfToken` | `string` | Token CSRF para os formulários da tela. |
46
+ * | `supported` | `boolean` | `false` quando o store não suporta o self-service de segurança (troca de senha/e-mail) — a tela deve degradar. |
47
+ * | `profileSupported` | `boolean` | `true` quando o store suporta editar nome/avatar (`updateProfile`). |
48
+ * | `avatarUploadSupported` | `boolean` | `true` quando algum backend (drive OU media) pode armazenar o upload de avatar. |
49
+ * | `email` | `string` | E-mail atual da conta (`''` se ausente). |
50
+ * | `name` | `string` | Nome atual da conta (`''` se ausente). |
51
+ * | `avatarUrl` | `string` | URL do avatar atual (`''` se ausente). |
52
+ * | `passwordChanged` | `string \| null` | Flash de sucesso da troca de senha (mensagem localizada) ou `null`. |
53
+ * | `emailChangeRequested` | `string \| null` | Flash: link de confirmação de troca de e-mail enviado (ou cancelamento) ou `null`. |
54
+ * | `emailChanged` | `string \| null` | Flash: troca de e-mail concluída ou `null`. |
55
+ * | `profileUpdated` | `string \| null` | Flash: perfil atualizado ou `null`. |
56
+ * | `error` | `string \| null` | Flash de erro de segurança (senha inválida, e-mail em uso, política violada) ou `null`. |
57
+ * | `trustedDevicesEnabled` | `boolean` | `true` quando o recurso de dispositivos confiáveis está ligado. |
58
+ * | `trustedDevicesRevoked` | `string \| null` | Flash: confiança deste navegador revogada ou `null`. |
59
+ * | `sessionsSupported` | `boolean` | `true` quando o adapter OIDC enumera as sessões ativas da conta. |
60
+ * | `sessions` | `Array<{ loginTs: string; browser: string; os: string; ip: string; location: string }>` | Sessões ativas da própria conta (vazio quando não suportado). `loginTs` é ISO ou `''`. |
61
+ * | `exportSupported` | `boolean` | Sempre `true` — export de dados (portabilidade/LGPD) disponível para a conta logada. |
62
+ * | `deletionSupported` | `boolean` | `true` quando o store suporta hard delete (danger zone). |
63
+ * | `deleteError` | `string \| null` | Flash de erro da confirmação de deleção ou `null`. |
64
+ * | `messages` | `AuthMessages` | Catálogo de mensagens i18n. |
65
+ *
66
+ * ### Props da tela `account/mfa`
67
+ *
68
+ * | Prop | Tipo | Descrição |
69
+ * |---------------------|---------------------|-----------|
70
+ * | `csrfToken` | `string` | Token CSRF para os formulários de enroll/confirm/disable e passkeys. |
71
+ * | `enabled` | `boolean` | `true` quando o TOTP já está confirmado (habilitado) para a conta. |
72
+ * | `recoveryCodes` | `string[] \| null` | Códigos de recuperação recém-gerados (exibidos UMA vez após enroll/confirm) ou `null`. |
73
+ * | `passkeysSupported` | `boolean` | `true` quando o store persiste credenciais WebAuthn (passkeys). |
74
+ * | `passkeys` | `Array<{ id: string; label?: string; createdAt: string }>` | Passkeys cadastradas (vazio quando não suportado). `id` é base64url; `createdAt` é ISO. |
75
+ * | `messages` | `AuthMessages` | Catálogo de mensagens i18n. |
76
+ *
77
+ * ### Props da tela `account/confirm` (sudo — confirmar identidade)
78
+ *
79
+ * | Prop | Tipo | Descrição |
80
+ * |---------------|---------------------|-----------|
81
+ * | `csrfToken` | `string` | Token CSRF para o POST de cada método. |
82
+ * | `returnTo` | `string \| null` | Caminho interno de destino após confirmar (validado pelo servidor) ou `null`. |
83
+ * | `error` | `string \| null` | Flash de erro da última tentativa de confirmação ou `null`. |
84
+ * | `notice` | `string \| null` | Flash informativo (ex.: "link de confirmação enviado") ou `null`. |
85
+ * | `methods` | `Array<{ id: string; labelKey: string; kind: 'form' \| 'action' \| 'redirect' \| 'webauthn'; endpoint: string; fields?: Array<{ name: string; type: 'password' \| 'text'; labelKey: string }> }>` | Métodos de sudo disponíveis para a conta. A tela renderiza por `kind`; `endpoint` é o POST de verificação (`webauthn` pede options em `${endpoint}/options`). |
86
+ * | `preferredId` | `string \| null` | `id` do último método usado (destaque na UI) ou `null`. |
87
+ * | `messages` | `AuthMessages` | Catálogo de mensagens i18n. |
88
+ *
89
+ * ### Props da tela `account/email-confirmed` (terminal do link de troca de e-mail)
90
+ *
91
+ * | Prop | Tipo | Descrição |
92
+ * |------------|----------------|-----------|
93
+ * | `ok` | `boolean` | `true` quando o token era válido e o novo e-mail foi aplicado; `false` para token inválido/expirado ou store sem suporte. A tela mostra sucesso ou falha conforme o valor. |
94
+ * | `messages` | `AuthMessages` | Catálogo de mensagens i18n. |
40
95
  */
41
96
  export function inertiaRenderer(opts) {
42
97
  const allowed = opts.views ? new Set(opts.views) : null;
@@ -0,0 +1,47 @@
1
+ import { password } from './methods/password.js';
2
+ import { passkey } from './methods/passkey.js';
3
+ import { oidcStepUp } from './methods/oidc_step_up.js';
4
+ import { magicLink } from './methods/magic_link.js';
5
+ /**
6
+ * Métodos de confirmação de identidade (sudo mode), no mesmo padrão de factory
7
+ * usado em `stores.*` e `retrievers.*` das libs irmãs.
8
+ *
9
+ * A lista vai em DOIS lugares, e eles precisam casar:
10
+ *
11
+ * - `config/authkit.ts` → `sudo.methods` decide o que a TELA oferece e o que os
12
+ * handlers ACEITAM;
13
+ * - `registerAuthHost(router, { sudoMethods })` decide o que tem ROTA montada.
14
+ *
15
+ * São dois porque a montagem de rotas acontece antes de o config (lazy)
16
+ * resolver — mesma razão de `social`/`admin`/`rateLimit`. Divergiram, a tela
17
+ * loga um aviso de flag-drift e o endpoint faltante dá 404.
18
+ *
19
+ * SEM `config.sudo.methods` não há como divergir: a tela cai na própria lista
20
+ * montada por `registerAuthHost`, a mesma que os handlers aceitam.
21
+ *
22
+ * ```ts
23
+ * defineConfig({
24
+ * sudo: {
25
+ * methods: [
26
+ * sudoMethods.oidcStepUp({ url: '/auth/step-up' }),
27
+ * sudoMethods.magicLink(),
28
+ * sudoMethods.passkey(),
29
+ * sudoMethods.password(),
30
+ * ],
31
+ * },
32
+ * })
33
+ * ```
34
+ */
35
+ export declare const sudoMethods: {
36
+ password: typeof password;
37
+ passkey: typeof passkey;
38
+ oidcStepUp: typeof oidcStepUp;
39
+ magicLink: typeof magicLink;
40
+ };
41
+ export type { SudoMethod, SudoContext, SudoMethodDescriptor, SudoRouteHelpers } from './types.js';
42
+ /**
43
+ * Montagem do `SudoContext` a partir do `HttpContext`. Reexportado aqui por
44
+ * simetria com `sudoMethods` e os tipos: quem escreve um método (ou a rota de
45
+ * callback do `oidcStepUp`) precisa dos três.
46
+ */
47
+ export { sudoContextFrom } from './runtime.js';
@@ -0,0 +1,41 @@
1
+ import { password } from './methods/password.js';
2
+ import { passkey } from './methods/passkey.js';
3
+ import { oidcStepUp } from './methods/oidc_step_up.js';
4
+ import { magicLink } from './methods/magic_link.js';
5
+ /**
6
+ * Métodos de confirmação de identidade (sudo mode), no mesmo padrão de factory
7
+ * usado em `stores.*` e `retrievers.*` das libs irmãs.
8
+ *
9
+ * A lista vai em DOIS lugares, e eles precisam casar:
10
+ *
11
+ * - `config/authkit.ts` → `sudo.methods` decide o que a TELA oferece e o que os
12
+ * handlers ACEITAM;
13
+ * - `registerAuthHost(router, { sudoMethods })` decide o que tem ROTA montada.
14
+ *
15
+ * São dois porque a montagem de rotas acontece antes de o config (lazy)
16
+ * resolver — mesma razão de `social`/`admin`/`rateLimit`. Divergiram, a tela
17
+ * loga um aviso de flag-drift e o endpoint faltante dá 404.
18
+ *
19
+ * SEM `config.sudo.methods` não há como divergir: a tela cai na própria lista
20
+ * montada por `registerAuthHost`, a mesma que os handlers aceitam.
21
+ *
22
+ * ```ts
23
+ * defineConfig({
24
+ * sudo: {
25
+ * methods: [
26
+ * sudoMethods.oidcStepUp({ url: '/auth/step-up' }),
27
+ * sudoMethods.magicLink(),
28
+ * sudoMethods.passkey(),
29
+ * sudoMethods.password(),
30
+ * ],
31
+ * },
32
+ * })
33
+ * ```
34
+ */
35
+ export const sudoMethods = { password, passkey, oidcStepUp, magicLink };
36
+ /**
37
+ * Montagem do `SudoContext` a partir do `HttpContext`. Reexportado aqui por
38
+ * simetria com `sudoMethods` e os tipos: quem escreve um método (ou a rota de
39
+ * callback do `oidcStepUp`) precisa dos três.
40
+ */
41
+ export { sudoContextFrom } from './runtime.js';
@@ -0,0 +1,43 @@
1
+ import type { SudoContext, SudoMethod } from '../types.js';
2
+ /** Token de sudo pendente, guardado na sessão que o pediu. */
3
+ export declare const SUDO_LINK_SESSION_KEY = "authkit_sudo_link";
4
+ /** Mesma janela dos magic links de login. */
5
+ export declare const SUDO_LINK_TTL_MS: number;
6
+ /**
7
+ * Emite um token de sudo e guarda o HASH na sessão que pediu.
8
+ *
9
+ * O TOKEN É PRÓPRIO, DE ESCOPO SUDO — nunca o token de login
10
+ * (`issueMagicLinkToken`/`consumeMagicLinkToken` do `AccountStore`). Aquele é
11
+ * credencial de AUTENTICAÇÃO: reusá-lo faria de um link de sudo vazado uma
12
+ * sessão completa.
13
+ *
14
+ * | propriedade | valor | razão |
15
+ * |---|---|---|
16
+ * | geração | `randomBytes(32)` hex | entropia de credencial |
17
+ * | armazenamento | HASH na sessão que pediu | não guarda o segredo em claro |
18
+ * | escopo | só marca sudo | nunca autentica |
19
+ * | validade | 5 min | mesma janela dos magic links de login |
20
+ * | uso | único (apagado no consumo) | replay |
21
+ * | navegador | só o mesmo (vive na sessão) | step-up é reprova de QUEM ESTÁ ALI |
22
+ * | conta | vinculado ao `accountId` emissor | sessão sobrevive à troca de conta |
23
+ *
24
+ * O "só mesmo navegador" é propriedade desejada aqui, diferente do magic link
25
+ * de login, onde é limitação conhecida.
26
+ *
27
+ * Exportada (em vez de membro `__` do método) para ser testável sem furar a
28
+ * API pública do `SudoMethod`.
29
+ */
30
+ export declare function issueSudoLinkToken(c: SudoContext): string;
31
+ /**
32
+ * Consome o token: single-use, vinculado à conta emissora, expira em 5 min,
33
+ * comparação em tempo constante.
34
+ */
35
+ export declare function verifySudoLinkToken(c: SudoContext, token: string): boolean;
36
+ /**
37
+ * Confirmação por link enviado ao e-mail da conta.
38
+ *
39
+ * Depende do hook `mail.onSudoLink`, DISTINTO de `mail.onMagicLink` justamente
40
+ * para que o host não confunda um link que autentica com um que só concede
41
+ * sudo a quem já está logado. Sem o hook, o método fica indisponível.
42
+ */
43
+ export declare function magicLink(): SudoMethod;