@adonis-agora/authkit-server 1.0.0-rc.0 → 1.0.0-rc.2

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 (91) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +4 -5
  3. package/build/index.d.ts +7 -11
  4. package/build/index.js +5 -9
  5. package/build/providers/authkit_server_provider.js +14 -8
  6. package/build/src/agents/config.d.ts +9 -13
  7. package/build/src/agents/config.js +17 -11
  8. package/build/src/agents/delegation_service.js +3 -3
  9. package/build/src/agents/delegation_store.d.ts +3 -2
  10. package/build/src/agents/delegation_store.js +6 -5
  11. package/build/src/agents/extension.d.ts +261 -0
  12. package/build/src/agents/extension.js +40 -0
  13. package/build/src/agents/middleware.js +1 -1
  14. package/build/src/agents/protocol.d.ts +4 -4
  15. package/build/src/agents/registry_store.d.ts +3 -2
  16. package/build/src/agents/registry_store.js +1 -1
  17. package/build/src/agents/runtime.d.ts +26 -7
  18. package/build/src/agents/runtime.js +110 -75
  19. package/build/src/audit/audit_sink.js +1 -1
  20. package/build/src/console_authorize.d.ts +56 -0
  21. package/build/src/console_authorize.js +85 -0
  22. package/build/src/define_config.d.ts +20 -14
  23. package/build/src/define_config.js +11 -8
  24. package/build/src/doctor/checks.d.ts +7 -1
  25. package/build/src/doctor/checks.js +47 -1
  26. package/build/src/host/account_api/account_api_controller.js +8 -6
  27. package/build/src/host/account_session_sign_in.d.ts +12 -0
  28. package/build/src/host/account_session_sign_in.js +19 -0
  29. package/build/src/host/admin_clients_service.js +10 -1
  30. package/build/src/host/admin_console/console_personal_agents_controller.d.ts +1 -6
  31. package/build/src/host/admin_console/console_personal_agents_controller.js +50 -30
  32. package/build/src/host/admin_console_warning.d.ts +8 -0
  33. package/build/src/host/admin_console_warning.js +18 -0
  34. package/build/src/host/admin_sessions_service.js +8 -5
  35. package/build/src/host/auth_host_config.d.ts +11 -2
  36. package/build/src/host/console_denied_page.d.ts +6 -0
  37. package/build/src/host/console_denied_page.js +18 -0
  38. package/build/src/host/console_sso.d.ts +24 -0
  39. package/build/src/host/console_sso.js +70 -0
  40. package/build/src/host/controllers/account_apps_controller.js +15 -11
  41. package/build/src/host/controllers/account_session_controller.js +10 -1
  42. package/build/src/host/controllers/console_sso_controller.d.ts +12 -0
  43. package/build/src/host/controllers/console_sso_controller.js +61 -0
  44. package/build/src/host/controllers/interaction_controller.js +3 -1
  45. package/build/src/host/csrf.d.ts +3 -3
  46. package/build/src/host/csrf.js +3 -13
  47. package/build/src/host/default_mailer.d.ts +11 -6
  48. package/build/src/host/default_mailer.js +16 -12
  49. package/build/src/host/i18n.d.ts +3 -14
  50. package/build/src/host/i18n.js +15 -13
  51. package/build/src/host/register_auth_host.d.ts +2 -0
  52. package/build/src/host/register_auth_host.js +110 -64
  53. package/build/src/host/ui-dist/assets/index-BqyJYRVX.js +67 -0
  54. package/build/src/host/ui-dist/index.html +1 -1
  55. package/build/src/provider/build_provider.js +6 -0
  56. package/build/src/provider/oidc_service.d.ts +1 -1
  57. package/build/src/provider/oidc_service.js +1 -1
  58. package/build/src/schema/ensure.d.ts +14 -0
  59. package/build/src/schema/ensure.js +43 -150
  60. package/package.json +5 -5
  61. package/build/src/agents/poppy/client_registry.d.ts +0 -50
  62. package/build/src/agents/poppy/client_registry.js +0 -260
  63. package/build/src/agents/poppy/config.d.ts +0 -396
  64. package/build/src/agents/poppy/config.js +0 -228
  65. package/build/src/agents/poppy/dpop.d.ts +0 -61
  66. package/build/src/agents/poppy/dpop.js +0 -158
  67. package/build/src/agents/poppy/email_code_sign_in.d.ts +0 -27
  68. package/build/src/agents/poppy/email_code_sign_in.js +0 -140
  69. package/build/src/agents/poppy/errors.d.ts +0 -25
  70. package/build/src/agents/poppy/errors.js +0 -25
  71. package/build/src/agents/poppy/replay.d.ts +0 -25
  72. package/build/src/agents/poppy/replay.js +0 -106
  73. package/build/src/agents/poppy/resource.d.ts +0 -100
  74. package/build/src/agents/poppy/resource.js +0 -129
  75. package/build/src/agents/poppy/safe_fetch.d.ts +0 -48
  76. package/build/src/agents/poppy/safe_fetch.js +0 -262
  77. package/build/src/agents/poppy/service.d.ts +0 -293
  78. package/build/src/agents/poppy/service.js +0 -1458
  79. package/build/src/agents/poppy/session_jwt.d.ts +0 -30
  80. package/build/src/agents/poppy/session_jwt.js +0 -133
  81. package/build/src/agents/poppy/store.d.ts +0 -106
  82. package/build/src/agents/poppy/store.js +0 -261
  83. package/build/src/agents/poppy/web_session.d.ts +0 -39
  84. package/build/src/agents/poppy/web_session.js +0 -122
  85. package/build/src/host/controllers/poppy_consent_controller.d.ts +0 -19
  86. package/build/src/host/controllers/poppy_consent_controller.js +0 -199
  87. package/build/src/host/controllers/poppy_controller.d.ts +0 -33
  88. package/build/src/host/controllers/poppy_controller.js +0 -219
  89. package/build/src/host/poppy_session_key.d.ts +0 -9
  90. package/build/src/host/poppy_session_key.js +0 -9
  91. package/build/src/host/ui-dist/assets/index-D0EIeyKK.js +0 -67
@@ -5,6 +5,7 @@ import { AdminSessionsService } from '../admin_sessions_service.js';
5
5
  import { guardBotProtection, resolveEffectiveBotProtection } from '../bot_protection.js';
6
6
  import { brandFor, isFirstParty } from '../branding.js';
7
7
  import { consentRequestParams } from '../consent_params.js';
8
+ import { CONSOLE_SSO_CLIENT_ID } from '../console_sso.js';
8
9
  import { isCustomLoginMethodId } from '../custom_login.js';
9
10
  import { completeCustomLoginSession } from '../custom_login_completion.js';
10
11
  import { beginCustomMfa, clearMfaFlow, customMfaViewProps, getMfaFlow, initializeMfaFlow, recordVerifiedMfaFactor, resolveCustomMfaMethods, verifyCustomMfa, } from '../custom_mfa.js';
@@ -254,7 +255,8 @@ export default class AuthInteractionController {
254
255
  }
255
256
  }
256
257
  if (details.prompt.name === 'consent' &&
257
- isFirstParty(cfg.branding, details.params.client_id)) {
258
+ (details.params.client_id === CONSOLE_SSO_CLIENT_ID ||
259
+ isFirstParty(cfg.branding, details.params.client_id))) {
258
260
  // Clients first-party: auto-concede o consent (pula a tela de consent).
259
261
  // interactions.consent monta o Grant + interactionFinished e escreve o
260
262
  // redirect de volta para o client — opera via provider.interactionDetails,
@@ -11,10 +11,10 @@ export interface AuthkitCsrfOptions {
11
11
  */
12
12
  personalAgentsPrefix?: string | false;
13
13
  /**
14
- * Prefixo do Poppy: isenta token/revoke/device authorization e a entrada do navegador
15
- * (o código com o cookie `SameSite=Strict` é o que protege, §6).
14
+ * Isenta os `csrfExemptPaths` das `personalAgents.extensions` do config. Default: true.
15
+ * `false` desliga.
16
16
  */
17
- poppyPrefix?: string | false;
17
+ personalAgentExtensions?: boolean;
18
18
  }
19
19
  /**
20
20
  * Retorna `true` quando `url` é uma rota AuthKit que deve ser ISENTA de CSRF
@@ -16,7 +16,6 @@
16
16
  * ```
17
17
  */
18
18
  import { normalizePersonalAgentsPrefix } from '../agents/config.js';
19
- import { normalizePoppyPrefix } from '../agents/poppy/config.js';
20
19
  import { getAuthHostConfig } from './auth_host_config.js';
21
20
  /**
22
21
  * Retorna `true` quando `url` é uma rota AuthKit que deve ser ISENTA de CSRF
@@ -34,21 +33,12 @@ export function authkitCsrfExceptions(url, options = {}) {
34
33
  ? normalizePersonalAgentsPrefix(options.personalAgentsPrefix)
35
34
  : getAuthHostConfig()?.personalAgents?.prefix;
36
35
  const agentsOAuth = agentsPrefix ? `${agentsPrefix}/oauth/` : null;
37
- const poppyPrefix = options.poppyPrefix === false
38
- ? undefined
39
- : options.poppyPrefix !== undefined
40
- ? normalizePoppyPrefix(options.poppyPrefix)
41
- : getAuthHostConfig()?.personalAgents?.poppyPrefix;
42
36
  const path = url.split('?')[0];
43
- const poppyExempt = !!poppyPrefix &&
44
- (path === `${poppyPrefix}/oauth/token` ||
45
- path === `${poppyPrefix}/oauth/revoke` ||
46
- path === `${poppyPrefix}/oauth/device` ||
47
- path === `${poppyPrefix}/browser-session` ||
48
- path === `${poppyPrefix}/browser-session/authorize`);
37
+ const extensionExempt = options.personalAgentExtensions !== false &&
38
+ (getAuthHostConfig()?.personalAgents?.extensions ?? []).some((e) => e.csrfExemptPaths?.includes(path));
49
39
  return (url.includes(mountPath) ||
50
40
  url.includes('/authkit/pat') ||
51
41
  (backchannel !== null && url === backchannel) ||
52
42
  (agentsOAuth !== null && url.startsWith(agentsOAuth)) ||
53
- poppyExempt);
43
+ extensionExempt);
54
44
  }
@@ -67,15 +67,20 @@ export declare function sendMagicLinkEmail(ctx: HttpContext, data: {
67
67
  channel?: 'code' | 'link';
68
68
  }): Promise<void>;
69
69
  /**
70
- * Envia o código do Mediated Sign-In pronto do Poppy (`poppyEmailCodeSignIn`) pelo
71
- * mailer default do host. Só código, sem link: quem digita é a pessoa, no agente.
72
- * Best-effort: no fallback (sem mail) loga o código, como o magic link loga o link; nunca lança.
70
+ * Envia um e-mail só com um código (sem link: quem digita é a pessoa, em outro lugar) pelo mailer
71
+ * default do host, com o branding e o i18n do authkit. Os textos saem do catálogo pelas chaves
72
+ * `{messages}.subject`, `.heading`, `.intro`, `.code_label` e `.fallback`, com `params`
73
+ * interpolados — uma extension registra as dela em `messages`. Best-effort: sem `@adonisjs/mail`
74
+ * loga o código (dev); nunca lança.
75
+ *
76
+ * Estável no 1.x (é o mailer das `personalAgents.extensions`).
73
77
  */
74
- export declare function sendPoppySignInCodeEmail(ctx: HttpContext, data: {
78
+ export declare function sendAuthkitCodeEmail(ctx: HttpContext, data: {
75
79
  email: string;
76
80
  code: string;
77
- agentName: string;
78
- expiresInMinutes: number;
81
+ /** Prefixo das chaves no catálogo (`'mail.poppy_sign_in'`). */
82
+ messages: string;
83
+ params?: Record<string, string | number>;
79
84
  }): Promise<void>;
80
85
  /**
81
86
  * Envia o link de CONFIRMAÇÃO DE IDENTIDADE (sudo) pelo mailer default do host.
@@ -295,32 +295,36 @@ export async function sendMagicLinkEmail(ctx, data) {
295
295
  }
296
296
  }
297
297
  /**
298
- * Envia o código do Mediated Sign-In pronto do Poppy (`poppyEmailCodeSignIn`) pelo
299
- * mailer default do host. Só código, sem link: quem digita é a pessoa, no agente.
300
- * Best-effort: no fallback (sem mail) loga o código, como o magic link loga o link; nunca lança.
298
+ * Envia um e-mail só com um código (sem link: quem digita é a pessoa, em outro lugar) pelo mailer
299
+ * default do host, com o branding e o i18n do authkit. Os textos saem do catálogo pelas chaves
300
+ * `{messages}.subject`, `.heading`, `.intro`, `.code_label` e `.fallback`, com `params`
301
+ * interpolados — uma extension registra as dela em `messages`. Best-effort: sem `@adonisjs/mail`
302
+ * loga o código (dev); nunca lança.
303
+ *
304
+ * Estável no 1.x (é o mailer das `personalAgents.extensions`).
301
305
  */
302
- export async function sendPoppySignInCodeEmail(ctx, data) {
306
+ export async function sendAuthkitCodeEmail(ctx, data) {
303
307
  try {
304
308
  const brand = resolveBrand(ctx);
305
309
  const { messages: t, locale } = resolveMailMessages(ctx);
306
- const params = { agent: data.agentName, minutes: data.expiresInMinutes };
310
+ const key = (name) => `${data.messages}.${name}`;
307
311
  const content = renderTransactionalEmail({
308
312
  brand,
309
313
  locale,
310
- subject: translate(t, 'mail.poppy_sign_in.subject', params),
311
- heading: translate(t, 'mail.poppy_sign_in.heading', params),
312
- intro: translate(t, 'mail.poppy_sign_in.intro', params),
313
- footnote: translate(t, 'mail.poppy_sign_in.fallback'),
314
+ subject: translate(t, key('subject'), data.params),
315
+ heading: translate(t, key('heading'), data.params),
316
+ intro: translate(t, key('intro'), data.params),
317
+ footnote: translate(t, key('fallback'), data.params),
314
318
  code: data.code,
315
- codeLabel: translate(t, 'mail.poppy_sign_in.code_label'),
319
+ codeLabel: translate(t, key('code_label'), data.params),
316
320
  });
317
321
  const sent = await sendEmail(ctx, data.email, content);
318
322
  if (!sent) {
319
- ctx.logger.info({ email: data.email, agent: data.agentName, code: data.code }, 'authkit: código do Poppy Mediated Sign-In (dev — @adonisjs/mail ausente)');
323
+ ctx.logger.info({ email: data.email, code: data.code, messages: data.messages, ...(data.params ?? {}) }, 'authkit: código por e-mail (dev — @adonisjs/mail ausente)');
320
324
  }
321
325
  }
322
326
  catch (error) {
323
- ctx.logger.error({ err: error, email: data.email }, 'authkit: falha ao enviar o código do Poppy Mediated Sign-In');
327
+ ctx.logger.error({ err: error, email: data.email, messages: data.messages }, 'authkit: falha ao enviar o e-mail com código');
324
328
  }
325
329
  }
326
330
  /**
@@ -560,11 +560,6 @@ export declare const DEFAULT_MESSAGES: {
560
560
  'mail.magic_link.code_subject': string;
561
561
  'mail.magic_link.code_intro': string;
562
562
  'mail.magic_link.code_only_label': string;
563
- 'mail.poppy_sign_in.subject': string;
564
- 'mail.poppy_sign_in.heading': string;
565
- 'mail.poppy_sign_in.intro': string;
566
- 'mail.poppy_sign_in.code_label': string;
567
- 'mail.poppy_sign_in.fallback': string;
568
563
  'mail.sudo_link.subject': string;
569
564
  'mail.sudo_link.heading': string;
570
565
  'mail.sudo_link.intro': string;
@@ -1318,11 +1313,6 @@ export declare const PT_BR_MESSAGES: {
1318
1313
  'mail.magic_link.code_subject': string;
1319
1314
  'mail.magic_link.code_intro': string;
1320
1315
  'mail.magic_link.code_only_label': string;
1321
- 'mail.poppy_sign_in.subject': string;
1322
- 'mail.poppy_sign_in.heading': string;
1323
- 'mail.poppy_sign_in.intro': string;
1324
- 'mail.poppy_sign_in.code_label': string;
1325
- 'mail.poppy_sign_in.fallback': string;
1326
1316
  'mail.sudo_link.subject': string;
1327
1317
  'mail.sudo_link.heading': string;
1328
1318
  'mail.sudo_link.intro': string;
@@ -1547,11 +1537,10 @@ export declare const PT_BR_MESSAGES: {
1547
1537
  */
1548
1538
  export declare const BUILTIN_MESSAGES: Record<string, AuthMessages>;
1549
1539
  /**
1550
- * Resolve o catálogo ativo. Começa do catálogo embutido do locale selecionado
1551
- * (ou do default `en` quando o locale não é embutido), depois mescla os
1552
- * overrides do host por cima. Sem config, retorna o default `en` intacto.
1553
- * Chaves omitidas caem no default `en` (fallback de cobertura).
1540
+ * Soma os catálogos de uma extension (`messages`) aos embutidos. Chamado quando o config resolve;
1541
+ * os overrides do host (`i18n.messages`) continuam vencendo.
1554
1542
  */
1543
+ export declare function registerExtensionMessages(catalogs: Readonly<Record<string, Readonly<Record<string, string>>>>): void;
1555
1544
  export declare function resolveMessages(i18n?: I18nConfig): AuthMessages;
1556
1545
  /**
1557
1546
  * Retorna a string para `key` (cai na própria `key` quando ausente) com
@@ -603,12 +603,6 @@ export const DEFAULT_MESSAGES = {
603
603
  'mail.magic_link.code_subject': 'Your login code',
604
604
  'mail.magic_link.code_intro': 'Use the code below to sign in. It expires shortly and can be used once.',
605
605
  'mail.magic_link.code_only_label': 'Enter this code to sign in:',
606
- // Código do Mediated Sign-In do Poppy: um personal agent pede para entrar na conta.
607
- 'mail.poppy_sign_in.subject': 'Your code to connect {agent}',
608
- 'mail.poppy_sign_in.heading': 'Connect {agent} to your account',
609
- 'mail.poppy_sign_in.intro': '{agent} asked to sign in to your account. If that was you, give it the code below. It expires in {minutes} minutes and works once.',
610
- 'mail.poppy_sign_in.code_label': 'Your code:',
611
- 'mail.poppy_sign_in.fallback': 'If you did not ask for this, ignore this email — nobody gets in without the code. Never share it with anyone else.',
612
606
  // Link de CONFIRMAÇÃO DE IDENTIDADE (sudo). Copy DISTINTA da do magic link de
613
607
  // login de propósito: este link não autentica ninguém — quem o abre já está
614
608
  // logado e só reprova que é quem diz ser. Prometer "entre na sua conta" num
@@ -1447,12 +1441,6 @@ export const PT_BR_MESSAGES = {
1447
1441
  'mail.magic_link.code_subject': 'Seu código de login',
1448
1442
  'mail.magic_link.code_intro': 'Use o código abaixo para entrar. Ele expira em instantes e serve para um único acesso.',
1449
1443
  'mail.magic_link.code_only_label': 'Digite este código para entrar:',
1450
- // Código do Mediated Sign-In do Poppy — ver o comentário no catálogo `en`.
1451
- 'mail.poppy_sign_in.subject': 'Seu código para conectar {agent}',
1452
- 'mail.poppy_sign_in.heading': 'Conecte {agent} à sua conta',
1453
- 'mail.poppy_sign_in.intro': '{agent} pediu para entrar na sua conta. Se foi você, informe o código abaixo. Ele expira em {minutes} minutos e serve uma única vez.',
1454
- 'mail.poppy_sign_in.code_label': 'Seu código:',
1455
- 'mail.poppy_sign_in.fallback': 'Se não foi você, ignore este e-mail — ninguém entra sem o código. Nunca o compartilhe com outra pessoa.',
1456
1444
  // Link de CONFIRMAÇÃO DE IDENTIDADE (sudo) — ver o comentário no catálogo `en`.
1457
1445
  'mail.sudo_link.subject': 'Confirme que é você',
1458
1446
  'mail.sudo_link.heading': 'Confirme sua identidade',
@@ -1713,13 +1701,27 @@ export const BUILTIN_MESSAGES = {
1713
1701
  * overrides do host por cima. Sem config, retorna o default `en` intacto.
1714
1702
  * Chaves omitidas caem no default `en` (fallback de cobertura).
1715
1703
  */
1704
+ /** Catálogos das `personalAgents.extensions`, por locale (ver {@link registerExtensionMessages}). */
1705
+ const extensionMessages = {};
1706
+ /**
1707
+ * Soma os catálogos de uma extension (`messages`) aos embutidos. Chamado quando o config resolve;
1708
+ * os overrides do host (`i18n.messages`) continuam vencendo.
1709
+ */
1710
+ export function registerExtensionMessages(catalogs) {
1711
+ for (const [locale, messages] of Object.entries(catalogs)) {
1712
+ extensionMessages[locale] = { ...(extensionMessages[locale] ?? {}), ...messages };
1713
+ }
1714
+ }
1716
1715
  export function resolveMessages(i18n) {
1717
1716
  const locale = i18n?.locale ?? DEFAULT_LOCALE;
1718
1717
  // Base: sempre o default `en` para garantir cobertura total das chaves; o
1719
- // catálogo embutido do locale (ex.: pt-BR) é mesclado por cima.
1718
+ // catálogo embutido do locale (ex.: pt-BR) é mesclado por cima. Os das extensions entram
1719
+ // em cada camada, abaixo dos overrides do host.
1720
1720
  const base = {
1721
1721
  ...DEFAULT_MESSAGES,
1722
+ ...(extensionMessages[DEFAULT_LOCALE] ?? {}),
1722
1723
  ...(BUILTIN_MESSAGES[locale] ?? {}),
1724
+ ...(locale === DEFAULT_LOCALE ? {} : (extensionMessages[locale] ?? {})),
1723
1725
  };
1724
1726
  const overrides = i18n?.messages?.[locale];
1725
1727
  if (!overrides)
@@ -20,6 +20,8 @@ export declare function wantsJson(ctx: any): boolean;
20
20
  * (senão → `accountHome(cfg)`, default /account/security — NÃO revela a
21
21
  * existência do /admin, e cai numa tela que o host controla via `accountHome`;
22
22
  * se a tela default estiver desmontada, aponte `config.accountHome` para uma montada).
23
+ * 3. `config.admin.authorize` liberando (403/401/redirect conforme o resultado). Omitido:
24
+ * aberto fora de produção, 403 em produção.
23
25
  * As roles permitidas são resolvidas em runtime do `authkit.server` (config lazy).
24
26
  */
25
27
  export declare const adminGuard: (ctx: any, next: () => Promise<void>) => Promise<any>;
@@ -1,3 +1,5 @@
1
+ import { getBootedApp } from '../../services/booted_app.js';
2
+ import { resolveConsoleAuthorize, } from '../console_authorize.js';
1
3
  import { resolveRateLimit } from '../define_config.js';
2
4
  import { accountHome } from './account_home.js';
3
5
  import { getAccountLoginUrl, setAccountLoginUrl } from './account_login_url.js';
@@ -7,8 +9,9 @@ import { ACCOUNT_SESSION_KEY } from './account_session_key.js';
7
9
  import { adminApiGuard } from './admin_api/admin_api_guard.js';
8
10
  import { normalizeAdminApiPrefix, normalizeAdminPrefix, setAdminApiPrefix, setAdminPrefix, } from './admin_prefix.js';
9
11
  import { getAuthHostConfig, markAuthHostAutoMounted, wasAuthHostAutoMounted, } from './auth_host_config.js';
10
- import { ensureConsoleSession } from './idp_session_bridge.js';
11
- import { POPPY_WEB_SESSION_KEY } from './poppy_session_key.js';
12
+ import { consoleDeniedPage } from './console_denied_page.js';
13
+ import { CONSOLE_SSO_CALLBACK_PATH, CONSOLE_SSO_START_PATH } from './console_sso.js';
14
+ import { endBridgedIdpSession, ensureConsoleSession } from './idp_session_bridge.js';
12
15
  import { createAuthThrottles } from './rate_limit.js';
13
16
  import { redirectExact } from './redirect_exact.js';
14
17
  import { requestPathWithQuery } from './request_url.js';
@@ -67,7 +70,10 @@ async function checkAndRefreshIdle(ctx) {
67
70
  const lastSeen = ctx.session?.get(ACCOUNT_LAST_SEEN_KEY);
68
71
  const now = Date.now();
69
72
  if (lastSeen !== undefined && now - lastSeen > idleMs) {
70
- // Idle excedido: encerra a sessão.
73
+ // Idle excedido: encerra a sessão. Console aberto pela sessão do IdP (SSO):
74
+ // encerra também essa sessão, senão a ponte reabriria o console no próximo
75
+ // request e o idle timeout não teria efeito.
76
+ await endBridgedIdpSession(ctx);
71
77
  ctx.session?.forget(ACCOUNT_SESSION_KEY);
72
78
  ctx.session?.forget(ACCOUNT_LAST_SEEN_KEY);
73
79
  return true;
@@ -114,10 +120,21 @@ function buildLoginRedirect(ctx, extra) {
114
120
  * classe NÃO era aplicada em runtime num grupo, deixando /account/tokens e
115
121
  * /account/mfa acessíveis sem sessão.
116
122
  */
117
- /** A sessão é do navegador de um personal agent Poppy (§5)? */
118
- function isPoppyBrowser(ctx) {
119
- const marker = ctx.session?.get?.(POPPY_WEB_SESSION_KEY);
120
- return typeof marker === 'object' && marker !== null && typeof marker.sessionId === 'string';
123
+ /**
124
+ * A sessão é do navegador de um personal agent (`isAgentBrowser` de uma extension)? Um hook que
125
+ * lança conta como "é": na dúvida, o console fica fechado.
126
+ */
127
+ function isAgentBrowser(ctx) {
128
+ for (const extension of getAuthHostConfig()?.personalAgents?.extensions ?? []) {
129
+ try {
130
+ if (extension.isAgentBrowser?.(ctx))
131
+ return true;
132
+ }
133
+ catch {
134
+ return true;
135
+ }
136
+ }
137
+ return false;
121
138
  }
122
139
  /**
123
140
  * `true` when the caller wants JSON (an `Accept` that prefers `application/json`,
@@ -142,9 +159,9 @@ function unauthenticated(ctx, redirectTo) {
142
159
  return redirectExact(ctx.response, redirectTo);
143
160
  }
144
161
  const accountGuard = async (ctx, next) => {
145
- // O navegador de um personal agent Poppy (§5) não entra no console de conta nem
146
- // nas telas de consentimento: a sessão é do agente, não do usuário.
147
- if (isPoppyBrowser(ctx))
162
+ // O navegador de um personal agent não entra no console de conta nem nas telas de
163
+ // consentimento: a sessão é do agente, não do usuário.
164
+ if (isAgentBrowser(ctx))
148
165
  return ctx.response.status(403).send('');
149
166
  // Sessão do console — ou, com `accountSession.acceptIdpSession`, a do IdP (SSO).
150
167
  if (!(await ensureConsoleSession(ctx))) {
@@ -166,6 +183,8 @@ const accountGuard = async (ctx, next) => {
166
183
  * (senão → `accountHome(cfg)`, default /account/security — NÃO revela a
167
184
  * existência do /admin, e cai numa tela que o host controla via `accountHome`;
168
185
  * se a tela default estiver desmontada, aponte `config.accountHome` para uma montada).
186
+ * 3. `config.admin.authorize` liberando (403/401/redirect conforme o resultado). Omitido:
187
+ * aberto fora de produção, 403 em produção.
169
188
  * As roles permitidas são resolvidas em runtime do `authkit.server` (config lazy).
170
189
  */
171
190
  export const adminGuard = async (ctx, next) => {
@@ -177,7 +196,7 @@ export const adminGuard = async (ctx, next) => {
177
196
  if (!cfg.admin.enabled) {
178
197
  return ctx.response.notFound();
179
198
  }
180
- if (isPoppyBrowser(ctx))
199
+ if (isAgentBrowser(ctx))
181
200
  return ctx.response.status(403).send('');
182
201
  // Com `accountSession.acceptIdpSession`, a sessão do IdP também abre o console.
183
202
  await ensureConsoleSession(ctx);
@@ -206,10 +225,63 @@ export const adminGuard = async (ctx, next) => {
206
225
  .status(403)
207
226
  .send({ error: { code: 'forbidden', message: 'Admin role required.' } });
208
227
  }
209
- return ctx.response.redirect(accountHome(cfg));
228
+ return redirectExact(ctx.response, accountHome(cfg));
210
229
  }
230
+ // `admin.authorize`, after the roles: both must pass. Omitted, the console is open
231
+ // outside production and closed (403) in production.
232
+ const decision = cfg.admin.authorize
233
+ ? await adminAuthorizeChecker(cfg.admin.authorize)(ctx)
234
+ : appInProduction()
235
+ ? { allowed: false, status: 403 }
236
+ : { allowed: true };
237
+ if (!decision.allowed)
238
+ return consoleDenied(ctx, decision);
211
239
  return next();
212
240
  };
241
+ /**
242
+ * Whether the app runs in production. Unknown (the provider did not register the
243
+ * app) counts as production, so an omitted `admin.authorize` fails closed.
244
+ */
245
+ function appInProduction() {
246
+ try {
247
+ return getBootedApp().inProduction !== false;
248
+ }
249
+ catch {
250
+ return true;
251
+ }
252
+ }
253
+ /** The response for an `admin.authorize` denial: same rules in every `@adonis-agora` console. */
254
+ function consoleDenied(ctx, decision) {
255
+ const json = wantsJson(ctx);
256
+ if (decision.redirectTo) {
257
+ if (json) {
258
+ return ctx.response
259
+ .status(401)
260
+ .send({ error: 'unauthenticated', redirectTo: decision.redirectTo });
261
+ }
262
+ return redirectExact(ctx.response, decision.redirectTo);
263
+ }
264
+ if (decision.status === 401) {
265
+ if (json)
266
+ return ctx.response.status(401).send({ error: 'unauthenticated' });
267
+ ctx.response.header('Content-Type', 'text/html; charset=utf-8');
268
+ return ctx.response.status(401).send(consoleDeniedPage(401));
269
+ }
270
+ if (json)
271
+ return ctx.response.status(403).send({ error: 'forbidden' });
272
+ ctx.response.header('Content-Type', 'text/html; charset=utf-8');
273
+ return ctx.response.status(403).send(consoleDeniedPage(403));
274
+ }
275
+ /** One checker per configured `admin.authorize`, so a lazy import runs once. */
276
+ const adminAuthorizeCheckers = new WeakMap();
277
+ function adminAuthorizeChecker(authorize) {
278
+ let checker = adminAuthorizeCheckers.get(authorize);
279
+ if (!checker) {
280
+ checker = resolveConsoleAuthorize(authorize);
281
+ adminAuthorizeCheckers.set(authorize, checker);
282
+ }
283
+ return checker;
284
+ }
213
285
  const C = {
214
286
  oidc: () => import('../controllers/oidc_callback_controller.js'),
215
287
  authorizationServerMetadata: () => import('../controllers/authorization_server_metadata_controller.js'),
@@ -218,6 +290,7 @@ const C = {
218
290
  social: () => import('./controllers/social_controller.js'),
219
291
  patIntrospection: () => import('./controllers/pat_introspection_controller.js'),
220
292
  accountSession: () => import('./controllers/account_session_controller.js'),
293
+ consoleSso: () => import('./controllers/console_sso_controller.js'),
221
294
  accountTokens: () => import('./controllers/account_tokens_controller.js'),
222
295
  accountSecurity: () => import('./controllers/account_security_controller.js'),
223
296
  accountApps: () => import('./controllers/account_apps_controller.js'),
@@ -226,8 +299,6 @@ const C = {
226
299
  accountConfirm: () => import('./controllers/account_confirm_controller.js'),
227
300
  agentOAuth: () => import('./controllers/agent_oauth_controller.js'),
228
301
  agentConsent: () => import('./controllers/agent_consent_controller.js'),
229
- poppy: () => import('./controllers/poppy_controller.js'),
230
- poppyConsent: () => import('./controllers/poppy_consent_controller.js'),
231
302
  webauthnAsset: () => import('./controllers/webauthn_asset_controller.js'),
232
303
  logoutAsset: () => import('./controllers/logout_asset_controller.js'),
233
304
  passkeyAutofillAsset: () => import('./controllers/passkey_autofill_asset_controller.js'),
@@ -380,6 +451,17 @@ export function registerAuthHost(router, opts = {}) {
380
451
  if (accountLoginUrlOpt !== undefined) {
381
452
  setAccountLoginUrl(accountLoginUrlOpt);
382
453
  }
454
+ // Console sem porta de entrada: SSO desligado (`acceptIdpSession: false`) deixa só o
455
+ // login próprio do console, que é por senha; com a senha desligada ninguém entra em
456
+ // `/account/*` nem no console admin. Só passa com um `accountLoginUrl` do host (aqui
457
+ // ou em `routes.accountLoginUrl`) — por isso a checagem é no registro, não no config.
458
+ if (hostCfg?.consoleNeedsLoginUrl && !accountLoginUrlOpt) {
459
+ throw new Error('authkit: `accountSession.acceptIdpSession: false` com `authMethods: { password: false }` ' +
460
+ 'deixa o console de conta e o console admin sem nenhum jeito de entrar (o login próprio ' +
461
+ 'do console é por senha). Remova `acceptIdpSession: false` (o default entra pelo login ' +
462
+ 'do IdP) ou passe `accountLoginUrl` (no `registerAuthHost` ou em `routes`) apontando ' +
463
+ 'para um login do seu app.');
464
+ }
383
465
  // Montagem por tela do console de conta. `undefined` → tudo montado;
384
466
  // `false` → nada; objeto → cada flag ausente default `true`.
385
467
  const accountOpt = opts.account ?? routesCfg?.account;
@@ -506,6 +588,13 @@ export function registerAuthHost(router, opts = {}) {
506
588
  // Provider OIDC (wildcard + root) — o que registerOidcRoutes fazia.
507
589
  router.any(`${mount}/*`, [C.oidc]).as('authkit.oidc.wildcard');
508
590
  router.any(mount, [C.oidc]).as('authkit.oidc.root');
591
+ // Login do console pelo IdP (SSO; client interno `authkit-console`, ver
592
+ // `console_sso.ts`). Sempre montado: com `acceptIdpSession: false` o início só
593
+ // redireciona para o login próprio do console.
594
+ router.get(CONSOLE_SSO_START_PATH, [C.consoleSso, 'start']).as('authkit.console_sso.start');
595
+ router
596
+ .get(CONSOLE_SSO_CALLBACK_PATH, [C.consoleSso, 'callback'])
597
+ .as('authkit.console_sso.callback');
509
598
  // Interaction (login multi-step + consent + signup).
510
599
  // Cada rota passa por `keep(key, …)`: entra em `AuthHostRouteMap.routes`/`urls` e
511
600
  // recebe o `extraMiddleware` do host (depois do throttle da lib).
@@ -590,56 +679,13 @@ export function registerAuthHost(router, opts = {}) {
590
679
  })
591
680
  .use([accountGuard]);
592
681
  }
593
- // Personal Agent Protocol (Poppy): descoberta, authorization server (token,
594
- // revogação, device authorization), Mediated Sign-In e sessão de navegador —
595
- // server-to-server ou POST cross-site do navegador do agente (sem CSRF, ver
596
- // `authkitCsrfExceptions`). As telas do usuário (authorize/device) exigem a
597
- // sessão de conta e mantêm o CSRF.
598
- const poppyPrefix = hostCfg?.personalAgents?.poppyPrefix;
599
- if (poppyPrefix) {
600
- // Nomes explícitos: o Router nomeia pela dupla controller.método, e as duas URLs da metadata
601
- // (RFC 8414 na raiz e debaixo do issuer) usam o mesmo — "poppy.metadata already exists" no boot.
602
- router.get('/.well-known/poppy.json', [C.poppy, 'discovery']).as('authkit.poppy.discovery');
603
- router
604
- .get('/.well-known/oauth-protected-resource', [C.poppy, 'protectedResource'])
605
- .as('authkit.poppy.protected_resource');
606
- router
607
- .get(`/.well-known/oauth-authorization-server${poppyPrefix}`, [C.poppy, 'metadata'])
608
- .as('authkit.poppy.metadata');
609
- router
610
- .get(`${poppyPrefix}/.well-known/oauth-authorization-server`, [C.poppy, 'metadata'])
611
- .as('authkit.poppy.metadata_under_issuer');
612
- router.post(`${poppyPrefix}/oauth/token`, [C.poppy, 'token']).as('authkit.poppy.token');
613
- router.post(`${poppyPrefix}/oauth/revoke`, [C.poppy, 'revoke']).as('authkit.poppy.revoke');
614
- router
615
- .post(`${poppyPrefix}/oauth/device`, [C.poppy, 'deviceAuthorization'])
616
- .as('authkit.poppy.device_authorization');
617
- // A página grava um pedido sem autenticação: throttle por IP.
618
- withLogin(router
619
- .get(`${poppyPrefix}/browser-session`, [C.poppy, 'browserSessionPage'])
620
- .as('authkit.poppy.browser_session_page'));
621
- router
622
- .post(`${poppyPrefix}/browser-session`, [C.poppy, 'browserSession'])
623
- .as('authkit.poppy.browser_session');
624
- router
625
- .post(`${poppyPrefix}/browser-session/authorize`, [C.poppy, 'browserSessionAuthorize'])
626
- .as('authkit.poppy.browser_session_authorize');
627
- router
628
- .group(() => {
629
- router
630
- .get(`${poppyPrefix}/oauth/authorize`, [C.poppyConsent, 'authorize'])
631
- .as('authkit.poppy.authorize');
632
- router
633
- .post(`${poppyPrefix}/oauth/authorize`, [C.poppyConsent, 'decideAuthorization'])
634
- .as('authkit.poppy.authorize_decide');
635
- withOtpLogin(router
636
- .get(`${poppyPrefix}/device`, [C.poppyConsent, 'device'])
637
- .as('authkit.poppy.device'));
638
- withOtpLogin(router
639
- .post(`${poppyPrefix}/device`, [C.poppyConsent, 'decideDevice'])
640
- .as('authkit.poppy.device_decide'));
641
- })
642
- .use([accountGuard]);
682
+ // `personalAgents.extensions`: cada uma monta as rotas dela, com o guard de conta e os
683
+ // buckets de rate limit das telas embutidas.
684
+ for (const extension of hostCfg?.personalAgents?.extensions ?? []) {
685
+ extension.routes?.(router, {
686
+ accountGuard,
687
+ throttle: { login: (route) => withLogin(route), otp: (route) => withOtpLogin(route) },
688
+ });
643
689
  }
644
690
  // Paths do console de conta (configuráveis/localizáveis via `accountRoutes`).
645
691
  // As TELAS vêm de `accountPath(key)` (prefixo + segmento configurável); os