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

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 (177) hide show
  1. package/CHANGELOG.md +111 -0
  2. package/README.md +4 -5
  3. package/build/commands/agora_doctor.d.ts +22 -0
  4. package/build/commands/agora_doctor.js +109 -0
  5. package/build/commands/clients_create.d.ts +4 -0
  6. package/build/commands/clients_create.js +97 -0
  7. package/build/commands/commands.json +78 -0
  8. package/build/commands/doctor.js +8 -74
  9. package/build/host/views/account/apps.edge +1 -5
  10. package/build/host/views/account/extension.edge +77 -0
  11. package/build/host/views/account/mfa.edge +2 -0
  12. package/build/host/views/account/partials/extension_actions.edge +14 -0
  13. package/build/host/views/account/partials/extension_block.edge +197 -0
  14. package/build/host/views/account/partials/extension_value.edge +8 -0
  15. package/build/host/views/account/partials/nav.edge +14 -0
  16. package/build/host/views/account/security.edge +1 -5
  17. package/build/host/views/account/tokens.edge +2 -2
  18. package/build/host/views/agents/consent.edge +11 -2
  19. package/build/host/views/agents/done.edge +1 -1
  20. package/build/host/views/partials/styles.edge +1 -1
  21. package/build/index.d.ts +14 -12
  22. package/build/index.js +8 -9
  23. package/build/providers/authkit_server_provider.js +36 -8
  24. package/build/src/account/extension.d.ts +10 -0
  25. package/build/src/account/extension.js +9 -0
  26. package/build/src/account/http.d.ts +77 -0
  27. package/build/src/account/http.js +381 -0
  28. package/build/src/account/mount.d.ts +40 -0
  29. package/build/src/account/mount.js +171 -0
  30. package/build/src/account/registry.d.ts +41 -0
  31. package/build/src/account/registry.js +81 -0
  32. package/build/src/account/remote.d.ts +72 -0
  33. package/build/src/account/remote.js +406 -0
  34. package/build/src/account/runtime.d.ts +289 -0
  35. package/build/src/account/runtime.js +641 -0
  36. package/build/src/accounts/account_store.d.ts +6 -0
  37. package/build/src/accounts/lucid_store/organizations.js +6 -0
  38. package/build/src/agents/config.d.ts +9 -13
  39. package/build/src/agents/config.js +17 -11
  40. package/build/src/agents/delegation_service.js +3 -3
  41. package/build/src/agents/delegation_store.d.ts +3 -2
  42. package/build/src/agents/delegation_store.js +6 -5
  43. package/build/src/agents/extension.d.ts +261 -0
  44. package/build/src/agents/extension.js +40 -0
  45. package/build/src/agents/middleware.js +1 -1
  46. package/build/src/agents/protocol.d.ts +4 -4
  47. package/build/src/agents/registry_store.d.ts +3 -2
  48. package/build/src/agents/registry_store.js +1 -1
  49. package/build/src/agents/runtime.d.ts +26 -7
  50. package/build/src/agents/runtime.js +110 -75
  51. package/build/src/audit/audit_sink.d.ts +1 -1
  52. package/build/src/audit/audit_sink.js +10 -1
  53. package/build/src/console_authorize.d.ts +56 -0
  54. package/build/src/console_authorize.js +85 -0
  55. package/build/src/console_switcher.d.ts +132 -0
  56. package/build/src/console_switcher.js +206 -0
  57. package/build/src/define_config.d.ts +54 -14
  58. package/build/src/define_config.js +17 -8
  59. package/build/src/dev_tools/contract.d.ts +72 -0
  60. package/build/src/dev_tools/contract.js +10 -0
  61. package/build/src/dev_tools/gate.d.ts +17 -0
  62. package/build/src/dev_tools/gate.js +69 -0
  63. package/build/src/dev_tools/register.d.ts +11 -0
  64. package/build/src/dev_tools/register.js +25 -0
  65. package/build/src/dev_tools/tools.d.ts +24 -0
  66. package/build/src/dev_tools/tools.js +384 -0
  67. package/build/src/doctor/agora_checks.d.ts +15 -0
  68. package/build/src/doctor/agora_checks.js +150 -0
  69. package/build/src/doctor/agora_doctor.d.ts +115 -0
  70. package/build/src/doctor/agora_doctor.js +692 -0
  71. package/build/src/doctor/checks.d.ts +7 -1
  72. package/build/src/doctor/checks.js +47 -1
  73. package/build/src/doctor/doctor_input.d.ts +27 -0
  74. package/build/src/doctor/doctor_input.js +86 -0
  75. package/build/src/doctor/index.d.ts +5 -0
  76. package/build/src/doctor/index.js +4 -0
  77. package/build/src/host/access_token_verifier.js +7 -0
  78. package/build/src/host/account_api/account_api_controller.js +8 -6
  79. package/build/src/host/account_session_sign_in.d.ts +12 -0
  80. package/build/src/host/account_session_sign_in.js +19 -0
  81. package/build/src/host/admin_api/admin_users_service.d.ts +5 -2
  82. package/build/src/host/admin_api/api_clients_controller.d.ts +37 -0
  83. package/build/src/host/admin_api/api_clients_controller.js +31 -1
  84. package/build/src/host/admin_api/dto.d.ts +3 -1
  85. package/build/src/host/admin_api/dto.js +2 -0
  86. package/build/src/host/admin_authorize.d.ts +8 -0
  87. package/build/src/host/admin_authorize.js +25 -0
  88. package/build/src/host/admin_clients_service.d.ts +20 -0
  89. package/build/src/host/admin_clients_service.js +65 -1
  90. package/build/src/host/admin_console/admin_console_entry.d.ts +14 -0
  91. package/build/src/host/admin_console/admin_console_entry.js +80 -0
  92. package/build/src/host/admin_console/admin_shell_controller.js +8 -0
  93. package/build/src/host/admin_console/console_clients_controller.d.ts +41 -0
  94. package/build/src/host/admin_console/console_clients_controller.js +34 -1
  95. package/build/src/host/admin_console/console_personal_agents_controller.d.ts +1 -6
  96. package/build/src/host/admin_console/console_personal_agents_controller.js +50 -30
  97. package/build/src/host/admin_console/console_switcher_controller.d.ts +11 -0
  98. package/build/src/host/admin_console/console_switcher_controller.js +23 -0
  99. package/build/src/host/admin_console_warning.d.ts +8 -0
  100. package/build/src/host/admin_console_warning.js +18 -0
  101. package/build/src/host/admin_sessions_service.js +8 -5
  102. package/build/src/host/admin_validators.d.ts +29 -0
  103. package/build/src/host/admin_validators.js +30 -0
  104. package/build/src/host/auth_host_config.d.ts +14 -2
  105. package/build/src/host/console_denied_page.d.ts +6 -0
  106. package/build/src/host/console_denied_page.js +18 -0
  107. package/build/src/host/console_sso.d.ts +24 -0
  108. package/build/src/host/console_sso.js +70 -0
  109. package/build/src/host/controllers/account_apps_controller.js +15 -11
  110. package/build/src/host/controllers/account_session_controller.js +10 -1
  111. package/build/src/host/controllers/console_sso_controller.d.ts +12 -0
  112. package/build/src/host/controllers/console_sso_controller.js +61 -0
  113. package/build/src/host/controllers/interaction_controller.js +3 -1
  114. package/build/src/host/csrf.d.ts +3 -3
  115. package/build/src/host/csrf.js +3 -13
  116. package/build/src/host/default_mailer.d.ts +11 -6
  117. package/build/src/host/default_mailer.js +16 -12
  118. package/build/src/host/i18n.d.ts +41 -14
  119. package/build/src/host/i18n.js +55 -13
  120. package/build/src/host/rate_limit.d.ts +8 -0
  121. package/build/src/host/rate_limit.js +9 -0
  122. package/build/src/host/register_auth_host.d.ts +16 -0
  123. package/build/src/host/register_auth_host.js +230 -88
  124. package/build/src/host/renderers/edge_renderer.js +5 -0
  125. package/build/src/host/renderers/inertia_renderer.js +7 -1
  126. package/build/src/host/screen_props.d.ts +39 -8
  127. package/build/src/host/ui-dist/assets/index-Dq3dJVBv.js +67 -0
  128. package/build/src/host/ui-dist/assets/{index-DTSmD4RU.css → index-WNIyCEPA.css} +1 -1
  129. package/build/src/host/ui-dist/index.html +2 -2
  130. package/build/src/provider/build_provider.js +43 -6
  131. package/build/src/provider/oidc_service.d.ts +1 -1
  132. package/build/src/provider/oidc_service.js +1 -1
  133. package/build/src/schema/ensure.d.ts +14 -0
  134. package/build/src/schema/ensure.js +43 -150
  135. package/build/src/service_tenants/client_metadata.d.ts +39 -0
  136. package/build/src/service_tenants/client_metadata.js +80 -0
  137. package/build/src/service_tenants/config.d.ts +44 -0
  138. package/build/src/service_tenants/config.js +44 -0
  139. package/build/src/service_tenants/provider_hooks.d.ts +52 -0
  140. package/build/src/service_tenants/provider_hooks.js +124 -0
  141. package/build/src/service_tenants/tenant_verifier.d.ts +73 -0
  142. package/build/src/service_tenants/tenant_verifier.js +104 -0
  143. package/build/src/tenancy/tenant_labels.d.ts +30 -0
  144. package/build/src/tenancy/tenant_labels.js +64 -0
  145. package/build/types.d.ts +3 -1
  146. package/package.json +12 -9
  147. package/build/src/agents/poppy/client_registry.d.ts +0 -50
  148. package/build/src/agents/poppy/client_registry.js +0 -260
  149. package/build/src/agents/poppy/config.d.ts +0 -396
  150. package/build/src/agents/poppy/config.js +0 -228
  151. package/build/src/agents/poppy/dpop.d.ts +0 -61
  152. package/build/src/agents/poppy/dpop.js +0 -158
  153. package/build/src/agents/poppy/email_code_sign_in.d.ts +0 -27
  154. package/build/src/agents/poppy/email_code_sign_in.js +0 -140
  155. package/build/src/agents/poppy/errors.d.ts +0 -25
  156. package/build/src/agents/poppy/errors.js +0 -25
  157. package/build/src/agents/poppy/replay.d.ts +0 -25
  158. package/build/src/agents/poppy/replay.js +0 -106
  159. package/build/src/agents/poppy/resource.d.ts +0 -100
  160. package/build/src/agents/poppy/resource.js +0 -129
  161. package/build/src/agents/poppy/safe_fetch.d.ts +0 -48
  162. package/build/src/agents/poppy/safe_fetch.js +0 -262
  163. package/build/src/agents/poppy/service.d.ts +0 -293
  164. package/build/src/agents/poppy/service.js +0 -1458
  165. package/build/src/agents/poppy/session_jwt.d.ts +0 -30
  166. package/build/src/agents/poppy/session_jwt.js +0 -133
  167. package/build/src/agents/poppy/store.d.ts +0 -106
  168. package/build/src/agents/poppy/store.js +0 -261
  169. package/build/src/agents/poppy/web_session.d.ts +0 -39
  170. package/build/src/agents/poppy/web_session.js +0 -122
  171. package/build/src/host/controllers/poppy_consent_controller.d.ts +0 -19
  172. package/build/src/host/controllers/poppy_consent_controller.js +0 -199
  173. package/build/src/host/controllers/poppy_controller.d.ts +0 -33
  174. package/build/src/host/controllers/poppy_controller.js +0 -219
  175. package/build/src/host/poppy_session_key.d.ts +0 -9
  176. package/build/src/host/poppy_session_key.js +0 -9
  177. package/build/src/host/ui-dist/assets/index-D0EIeyKK.js +0 -67
@@ -1,3 +1,6 @@
1
+ import { getBootedApp } from '../../services/booted_app.js';
2
+ import { mountAccountExtensions } from '../account/mount.js';
3
+ import { registerAgoraConsole } from '../console_switcher.js';
1
4
  import { resolveRateLimit } from '../define_config.js';
2
5
  import { accountHome } from './account_home.js';
3
6
  import { getAccountLoginUrl, setAccountLoginUrl } from './account_login_url.js';
@@ -5,10 +8,13 @@ import { accountPath, accountPathsMap, accountPrefix, joinAccountPath, setAccoun
5
8
  import { resolveAccountRoles } from './account_roles.js';
6
9
  import { ACCOUNT_SESSION_KEY } from './account_session_key.js';
7
10
  import { adminApiGuard } from './admin_api/admin_api_guard.js';
11
+ import { adminAuthorizeChecker, appInProduction } from './admin_authorize.js';
12
+ import { adminConsoleEntry } from './admin_console/admin_console_entry.js';
8
13
  import { normalizeAdminApiPrefix, normalizeAdminPrefix, setAdminApiPrefix, setAdminPrefix, } from './admin_prefix.js';
9
14
  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';
15
+ import { consoleDeniedPage } from './console_denied_page.js';
16
+ import { CONSOLE_SSO_CALLBACK_PATH, CONSOLE_SSO_START_PATH } from './console_sso.js';
17
+ import { endBridgedIdpSession, ensureConsoleSession } from './idp_session_bridge.js';
12
18
  import { createAuthThrottles } from './rate_limit.js';
13
19
  import { redirectExact } from './redirect_exact.js';
14
20
  import { requestPathWithQuery } from './request_url.js';
@@ -67,7 +73,10 @@ async function checkAndRefreshIdle(ctx) {
67
73
  const lastSeen = ctx.session?.get(ACCOUNT_LAST_SEEN_KEY);
68
74
  const now = Date.now();
69
75
  if (lastSeen !== undefined && now - lastSeen > idleMs) {
70
- // Idle excedido: encerra a sessão.
76
+ // Idle excedido: encerra a sessão. Console aberto pela sessão do IdP (SSO):
77
+ // encerra também essa sessão, senão a ponte reabriria o console no próximo
78
+ // request e o idle timeout não teria efeito.
79
+ await endBridgedIdpSession(ctx);
71
80
  ctx.session?.forget(ACCOUNT_SESSION_KEY);
72
81
  ctx.session?.forget(ACCOUNT_LAST_SEEN_KEY);
73
82
  return true;
@@ -81,6 +90,23 @@ async function checkAndRefreshIdle(ctx) {
81
90
  return false;
82
91
  }
83
92
  }
93
+ /** Read-only twin of {@link checkAndRefreshIdle}: whether the idle timeout passed. Never writes. */
94
+ async function isIdleExpired(ctx) {
95
+ try {
96
+ const runtimeSettings = await resolveRuntimeSettings(ctx);
97
+ if (!runtimeSettings)
98
+ return false;
99
+ const policy = await resolveEffectiveSessionPolicy(runtimeSettings);
100
+ const idleMs = policy.idleTimeoutMinutes * 60 * 1000;
101
+ if (idleMs <= 0)
102
+ return false;
103
+ const lastSeen = ctx.session?.get(ACCOUNT_LAST_SEEN_KEY);
104
+ return lastSeen !== undefined && Date.now() - lastSeen > idleMs;
105
+ }
106
+ catch {
107
+ return false;
108
+ }
109
+ }
84
110
  /**
85
111
  * Constrói a URL de redirect para o login da conta, preservando o destino
86
112
  * original como `return_to` (URL-encoded). A URL de destino é o path + query
@@ -114,10 +140,21 @@ function buildLoginRedirect(ctx, extra) {
114
140
  * classe NÃO era aplicada em runtime num grupo, deixando /account/tokens e
115
141
  * /account/mfa acessíveis sem sessão.
116
142
  */
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';
143
+ /**
144
+ * A sessão é do navegador de um personal agent (`isAgentBrowser` de uma extension)? Um hook que
145
+ * lança conta como "é": na dúvida, o console fica fechado.
146
+ */
147
+ function isAgentBrowser(ctx) {
148
+ for (const extension of getAuthHostConfig()?.personalAgents?.extensions ?? []) {
149
+ try {
150
+ if (extension.isAgentBrowser?.(ctx))
151
+ return true;
152
+ }
153
+ catch {
154
+ return true;
155
+ }
156
+ }
157
+ return false;
121
158
  }
122
159
  /**
123
160
  * `true` when the caller wants JSON (an `Accept` that prefers `application/json`,
@@ -142,9 +179,9 @@ function unauthenticated(ctx, redirectTo) {
142
179
  return redirectExact(ctx.response, redirectTo);
143
180
  }
144
181
  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))
182
+ // O navegador de um personal agent não entra no console de conta nem nas telas de
183
+ // consentimento: a sessão é do agente, não do usuário.
184
+ if (isAgentBrowser(ctx))
148
185
  return ctx.response.status(403).send('');
149
186
  // Sessão do console — ou, com `accountSession.acceptIdpSession`, a do IdP (SSO).
150
187
  if (!(await ensureConsoleSession(ctx))) {
@@ -166,50 +203,139 @@ const accountGuard = async (ctx, next) => {
166
203
  * (senão → `accountHome(cfg)`, default /account/security — NÃO revela a
167
204
  * existência do /admin, e cai numa tela que o host controla via `accountHome`;
168
205
  * se a tela default estiver desmontada, aponte `config.accountHome` para uma montada).
206
+ * 3. `config.admin.authorize` liberando (403/401/redirect conforme o resultado). Omitido:
207
+ * aberto fora de produção, 403 em produção.
169
208
  * As roles permitidas são resolvidas em runtime do `authkit.server` (config lazy).
170
209
  */
171
210
  export const adminGuard = async (ctx, next) => {
211
+ const access = await evaluateAdminAccess(ctx);
212
+ switch (access.kind) {
213
+ case 'allowed':
214
+ return next();
215
+ case 'disabled':
216
+ // Defesa contra flag-drift: as rotas são montadas com `admin: true` em tempo de
217
+ // registro, ANTES de o config resolver. Se o config tiver `admin.enabled: false`,
218
+ // as rotas existem mas o console deve estar desligado — 404 (não vaza a existência).
219
+ return ctx.response.notFound();
220
+ case 'agent':
221
+ return ctx.response.status(403).send('');
222
+ case 'signed-out':
223
+ // `/account/login` é sempre o login da conta — NÃO muda com o prefixo admin.
224
+ return unauthenticated(ctx, buildLoginRedirect(ctx));
225
+ case 'idle':
226
+ return unauthenticated(ctx, buildLoginRedirect(ctx, 'reason=idle'));
227
+ case 'not-admin':
228
+ // Evita vazar a existência do console admin: redireciona para o accountHome
229
+ // (sem mostrar a URL do admin).
230
+ if (wantsJson(ctx)) {
231
+ return ctx.response
232
+ .status(403)
233
+ .send({ error: { code: 'forbidden', message: 'Admin role required.' } });
234
+ }
235
+ return redirectExact(ctx.response, accountHome(access.cfg));
236
+ case 'denied':
237
+ return consoleDenied(ctx, access.decision);
238
+ }
239
+ };
240
+ /**
241
+ * The admin console gate: the session, then `admin.roles`, then `admin.authorize`.
242
+ *
243
+ * `probe: true` is the console switcher asking from another console: it reads the idle timeout
244
+ * without refreshing or ending the session.
245
+ */
246
+ async function evaluateAdminAccess(ctx, options = {}) {
172
247
  const service = await ctx.containerResolver.make('authkit.server');
173
248
  const cfg = service.config;
174
- // Defesa contra flag-drift: as rotas são montadas com `admin: true` em tempo de
175
- // registro, ANTES de o config resolver. Se o config tiver `admin.enabled: false`,
176
- // as rotas existem mas o console deve estar desligado — 404 (não vaza a existência).
177
- if (!cfg.admin.enabled) {
178
- return ctx.response.notFound();
179
- }
180
- if (isPoppyBrowser(ctx))
181
- return ctx.response.status(403).send('');
249
+ if (!cfg.admin.enabled)
250
+ return { kind: 'disabled' };
251
+ if (isAgentBrowser(ctx))
252
+ return { kind: 'agent' };
182
253
  // Com `accountSession.acceptIdpSession`, a sessão do IdP também abre o console.
183
254
  await ensureConsoleSession(ctx);
184
255
  const accountId = ctx.session?.get(ACCOUNT_SESSION_KEY);
185
- if (!accountId) {
186
- // `/account/login` é sempre o login da conta — NÃO muda com o prefixo admin.
187
- return unauthenticated(ctx, buildLoginRedirect(ctx));
188
- }
256
+ if (!accountId)
257
+ return { kind: 'signed-out' };
189
258
  // Idle timeout: também protege o console admin.
190
- const idleExpired = await checkAndRefreshIdle(ctx);
191
- if (idleExpired) {
192
- return unauthenticated(ctx, buildLoginRedirect(ctx, 'reason=idle'));
193
- }
259
+ const idleExpired = options.probe ? await isIdleExpired(ctx) : await checkAndRefreshIdle(ctx);
260
+ if (idleExpired)
261
+ return { kind: 'idle' };
194
262
  const allowed = cfg.admin.roles;
195
263
  const account = await cfg.accountStore.findById(accountId);
196
264
  // Resolve roles through the host's role authority (`resolveTokenRoles`) when set — the same source
197
265
  // the token claim is minted from — so an app-role admin reaches the console. Falls back to the
198
266
  // account's stored `globalRoles` when no hook is configured.
199
267
  const roles = account ? await resolveAccountRoles(cfg, account) : [];
200
- const isAdmin = roles.some((r) => allowed.includes(r));
201
- if (!isAdmin) {
202
- // Evita vazar a existência do console admin: redireciona para o accountHome
203
- // (sem mostrar a URL do admin).
204
- if (wantsJson(ctx)) {
268
+ if (!roles.some((r) => allowed.includes(r)))
269
+ return { kind: 'not-admin', cfg };
270
+ // `admin.authorize`, after the roles: both must pass. Omitted, the console is open
271
+ // outside production and closed (403) in production.
272
+ const decision = cfg.admin.authorize
273
+ ? await adminAuthorizeChecker(cfg.admin.authorize)(ctx)
274
+ : appInProduction()
275
+ ? { allowed: false, status: 403 }
276
+ : { allowed: true };
277
+ if (!decision.allowed)
278
+ return { kind: 'denied', decision };
279
+ return { kind: 'allowed' };
280
+ }
281
+ /**
282
+ * The admin console gate as the console switcher reads it: `{ allowed }`, never a response, never a
283
+ * throw. Used as the `authorize` of the admin console's `'agora.consoles'` entry.
284
+ */
285
+ export async function adminConsoleDecision(ctx) {
286
+ try {
287
+ const access = await evaluateAdminAccess(ctx, { probe: true });
288
+ if (access.kind === 'allowed')
289
+ return { allowed: true };
290
+ const unauthenticated = access.kind === 'signed-out' ||
291
+ access.kind === 'idle' ||
292
+ (access.kind === 'denied' && access.decision.status === 401);
293
+ return { allowed: false, reason: unauthenticated ? 'unauthenticated' : 'forbidden' };
294
+ }
295
+ catch {
296
+ return { allowed: false, reason: 'forbidden' };
297
+ }
298
+ }
299
+ /**
300
+ * Registers the admin console in the shared `'agora.consoles'` registry, so every other
301
+ * `@adonis-agora` console's switcher can link to it. A no-op when the provider did not register the
302
+ * app (tests that mount routes on a bare router).
303
+ */
304
+ function announceAdminConsole(adminPrefix) {
305
+ let app;
306
+ try {
307
+ app = getBootedApp();
308
+ }
309
+ catch {
310
+ return;
311
+ }
312
+ void (async () => {
313
+ const logger = (await app.container.make('logger').catch(() => undefined));
314
+ await registerAgoraConsole(app, adminConsoleEntry(adminPrefix, adminConsoleDecision), logger);
315
+ })().catch(() => { });
316
+ }
317
+ /** The response for an `admin.authorize` denial: same rules in every `@adonis-agora` console. */
318
+ function consoleDenied(ctx, decision) {
319
+ const json = wantsJson(ctx);
320
+ if (decision.redirectTo) {
321
+ if (json) {
205
322
  return ctx.response
206
- .status(403)
207
- .send({ error: { code: 'forbidden', message: 'Admin role required.' } });
323
+ .status(401)
324
+ .send({ error: 'unauthenticated', redirectTo: decision.redirectTo });
208
325
  }
209
- return ctx.response.redirect(accountHome(cfg));
326
+ return redirectExact(ctx.response, decision.redirectTo);
210
327
  }
211
- return next();
212
- };
328
+ if (decision.status === 401) {
329
+ if (json)
330
+ return ctx.response.status(401).send({ error: 'unauthenticated' });
331
+ ctx.response.header('Content-Type', 'text/html; charset=utf-8');
332
+ return ctx.response.status(401).send(consoleDeniedPage(401));
333
+ }
334
+ if (json)
335
+ return ctx.response.status(403).send({ error: 'forbidden' });
336
+ ctx.response.header('Content-Type', 'text/html; charset=utf-8');
337
+ return ctx.response.status(403).send(consoleDeniedPage(403));
338
+ }
213
339
  const C = {
214
340
  oidc: () => import('../controllers/oidc_callback_controller.js'),
215
341
  authorizationServerMetadata: () => import('../controllers/authorization_server_metadata_controller.js'),
@@ -218,6 +344,7 @@ const C = {
218
344
  social: () => import('./controllers/social_controller.js'),
219
345
  patIntrospection: () => import('./controllers/pat_introspection_controller.js'),
220
346
  accountSession: () => import('./controllers/account_session_controller.js'),
347
+ consoleSso: () => import('./controllers/console_sso_controller.js'),
221
348
  accountTokens: () => import('./controllers/account_tokens_controller.js'),
222
349
  accountSecurity: () => import('./controllers/account_security_controller.js'),
223
350
  accountApps: () => import('./controllers/account_apps_controller.js'),
@@ -226,8 +353,6 @@ const C = {
226
353
  accountConfirm: () => import('./controllers/account_confirm_controller.js'),
227
354
  agentOAuth: () => import('./controllers/agent_oauth_controller.js'),
228
355
  agentConsent: () => import('./controllers/agent_consent_controller.js'),
229
- poppy: () => import('./controllers/poppy_controller.js'),
230
- poppyConsent: () => import('./controllers/poppy_consent_controller.js'),
231
356
  webauthnAsset: () => import('./controllers/webauthn_asset_controller.js'),
232
357
  logoutAsset: () => import('./controllers/logout_asset_controller.js'),
233
358
  passkeyAutofillAsset: () => import('./controllers/passkey_autofill_asset_controller.js'),
@@ -238,6 +363,7 @@ const C = {
238
363
  // Console React JSON API (session-authed, under {prefix}/api/*).
239
364
  consoleShell: () => import('./admin_console/admin_shell_controller.js'),
240
365
  consoleOverview: () => import('./admin_console/console_overview_controller.js'),
366
+ consoleSwitcher: () => import('./admin_console/console_switcher_controller.js'),
241
367
  consoleUsers: () => import('./admin_console/console_users_controller.js'),
242
368
  consoleSessions: () => import('./admin_console/console_sessions_controller.js'),
243
369
  consoleClients: () => import('./admin_console/console_clients_controller.js'),
@@ -380,6 +506,17 @@ export function registerAuthHost(router, opts = {}) {
380
506
  if (accountLoginUrlOpt !== undefined) {
381
507
  setAccountLoginUrl(accountLoginUrlOpt);
382
508
  }
509
+ // Console sem porta de entrada: SSO desligado (`acceptIdpSession: false`) deixa só o
510
+ // login próprio do console, que é por senha; com a senha desligada ninguém entra em
511
+ // `/account/*` nem no console admin. Só passa com um `accountLoginUrl` do host (aqui
512
+ // ou em `routes.accountLoginUrl`) — por isso a checagem é no registro, não no config.
513
+ if (hostCfg?.consoleNeedsLoginUrl && !accountLoginUrlOpt) {
514
+ throw new Error('authkit: `accountSession.acceptIdpSession: false` com `authMethods: { password: false }` ' +
515
+ 'deixa o console de conta e o console admin sem nenhum jeito de entrar (o login próprio ' +
516
+ 'do console é por senha). Remova `acceptIdpSession: false` (o default entra pelo login ' +
517
+ 'do IdP) ou passe `accountLoginUrl` (no `registerAuthHost` ou em `routes`) apontando ' +
518
+ 'para um login do seu app.');
519
+ }
383
520
  // Montagem por tela do console de conta. `undefined` → tudo montado;
384
521
  // `false` → nada; objeto → cada flag ausente default `true`.
385
522
  const accountOpt = opts.account ?? routesCfg?.account;
@@ -506,6 +643,13 @@ export function registerAuthHost(router, opts = {}) {
506
643
  // Provider OIDC (wildcard + root) — o que registerOidcRoutes fazia.
507
644
  router.any(`${mount}/*`, [C.oidc]).as('authkit.oidc.wildcard');
508
645
  router.any(mount, [C.oidc]).as('authkit.oidc.root');
646
+ // Login do console pelo IdP (SSO; client interno `authkit-console`, ver
647
+ // `console_sso.ts`). Sempre montado: com `acceptIdpSession: false` o início só
648
+ // redireciona para o login próprio do console.
649
+ router.get(CONSOLE_SSO_START_PATH, [C.consoleSso, 'start']).as('authkit.console_sso.start');
650
+ router
651
+ .get(CONSOLE_SSO_CALLBACK_PATH, [C.consoleSso, 'callback'])
652
+ .as('authkit.console_sso.callback');
509
653
  // Interaction (login multi-step + consent + signup).
510
654
  // Cada rota passa por `keep(key, …)`: entra em `AuthHostRouteMap.routes`/`urls` e
511
655
  // recebe o `extraMiddleware` do host (depois do throttle da lib).
@@ -590,56 +734,13 @@ export function registerAuthHost(router, opts = {}) {
590
734
  })
591
735
  .use([accountGuard]);
592
736
  }
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]);
737
+ // `personalAgents.extensions`: cada uma monta as rotas dela, com o guard de conta e os
738
+ // buckets de rate limit das telas embutidas.
739
+ for (const extension of hostCfg?.personalAgents?.extensions ?? []) {
740
+ extension.routes?.(router, {
741
+ accountGuard,
742
+ throttle: { login: (route) => withLogin(route), otp: (route) => withOtpLogin(route) },
743
+ });
643
744
  }
644
745
  // Paths do console de conta (configuráveis/localizáveis via `accountRoutes`).
645
746
  // As TELAS vêm de `accountPath(key)` (prefixo + segmento configurável); os
@@ -888,6 +989,36 @@ export function registerAuthHost(router, opts = {}) {
888
989
  router.post(`${apiBase}/mfa/passkeys/verify`, [C.accountMfaApi, 'passkeyRegisterVerify']);
889
990
  })
890
991
  .use([accountGuard]);
992
+ // Account console extensions (`@adonis-agora/authkit-server/account-extension`): pages other
993
+ // packages register from their provider's `boot()`. Same guard as the built-in screens; the
994
+ // page routes mount once every provider booted. `account: false` turns them off too.
995
+ const extensionEntries = [];
996
+ if (accountOpt !== false) {
997
+ const builtins = {};
998
+ if (mountSecurity)
999
+ builtins.security = securityPath;
1000
+ if (mountMfa)
1001
+ builtins.mfa = mfaPath;
1002
+ if (mountTokens)
1003
+ builtins.tokens = tokensPath;
1004
+ if (mountApps)
1005
+ builtins.apps = appsPath;
1006
+ if (mountOrgs)
1007
+ builtins.orgs = orgsPath;
1008
+ mountAccountExtensions(router, {
1009
+ guard: accountGuard,
1010
+ ...(throttles
1011
+ ? { throttle: { html: throttles.accountActions, json: throttles.accountActionsJson } }
1012
+ : {}),
1013
+ overrides: hostCfg?.accountConsole?.extensions,
1014
+ ...(hostCfg?.accountConsole?.remote ? { remote: hostCfg.accountConsole.remote } : {}),
1015
+ ...(hostCfg?.accountConsole?.actionTimeoutMs !== undefined
1016
+ ? { actionTimeoutMs: hostCfg.accountConsole.actionTimeoutMs }
1017
+ : {}),
1018
+ builtins,
1019
+ entries: extensionEntries,
1020
+ });
1021
+ }
891
1022
  // API headless (Clerk-style) — montada fora do `accountGuard` (não depende da
892
1023
  // sessão de CONTA), autenticada pelo resolver do host (`headless.resolveAccountId`).
893
1024
  // `PUT` usa CSRF igual à API do console (middleware de shield do host).
@@ -913,6 +1044,8 @@ export function registerAuthHost(router, opts = {}) {
913
1044
  resolvedAdminPrefix = ap;
914
1045
  // Persiste no singleton de processo para que controllers e views usem o mesmo prefixo.
915
1046
  setAdminPrefix(ap);
1047
+ // Announce the console to the other `@adonis-agora` consoles (the switcher).
1048
+ announceAdminConsole(ap);
916
1049
  router
917
1050
  .group(() => {
918
1051
  // Console admin (SPA React self-contained): todas as rotas GET servem o
@@ -926,6 +1059,10 @@ export function registerAuthHost(router, opts = {}) {
926
1059
  // ─── JSON API do console (session-authed via adminGuard upstream) ─────
927
1060
  // GET {ap}/api/overview
928
1061
  router.get(`${ap}/api/overview`, [C.consoleOverview, 'handle']);
1062
+ // Console switcher: the other `@adonis-agora` consoles this viewer may open, and the
1063
+ // global search across them (search BEFORE the list: fixed path first).
1064
+ router.get(`${ap}/api/consoles/search`, [C.consoleSwitcher, 'search']);
1065
+ router.get(`${ap}/api/consoles`, [C.consoleSwitcher, 'index']);
929
1066
  // Usuários.
930
1067
  router.get(`${ap}/api/users`, [C.consoleUsers, 'index']);
931
1068
  router.get(`${ap}/api/users/:id`, [C.consoleUsers, 'show']);
@@ -951,6 +1088,8 @@ export function registerAuthHost(router, opts = {}) {
951
1088
  router.get(`${ap}/api/clients/:id`, [C.consoleClients, 'show']);
952
1089
  router.patch(`${ap}/api/clients/:id`, [C.consoleClients, 'update']);
953
1090
  router.delete(`${ap}/api/clients/:id`, [C.consoleClients, 'destroy']);
1091
+ router.post(`${ap}/api/clients/:id/disable`, [C.consoleClients, 'disable']);
1092
+ router.post(`${ap}/api/clients/:id/enable`, [C.consoleClients, 'enable']);
954
1093
  router.post(`${ap}/api/clients/:id/regenerate-secret`, [
955
1094
  C.consoleClients,
956
1095
  'regenerateSecret',
@@ -1050,6 +1189,8 @@ export function registerAuthHost(router, opts = {}) {
1050
1189
  withApiThrottle(router.patch('/clients/:id', [C.apiClients, 'update']));
1051
1190
  withApiThrottle(router.post('/clients/:id/regenerate-secret', [C.apiClients, 'regenerateSecret']));
1052
1191
  withApiThrottle(router.delete('/clients/:id', [C.apiClients, 'destroy']));
1192
+ withApiThrottle(router.post('/clients/:id/disable', [C.apiClients, 'disable']));
1193
+ withApiThrottle(router.post('/clients/:id/enable', [C.apiClients, 'enable']));
1053
1194
  // Organizações.
1054
1195
  withApiThrottle(router.get('/organizations', [C.apiOrgs, 'index']));
1055
1196
  withApiThrottle(router.post('/organizations', [C.apiOrgs, 'store']));
@@ -1107,6 +1248,7 @@ export function registerAuthHost(router, opts = {}) {
1107
1248
  mfa: mountMfa,
1108
1249
  apps: mountApps,
1109
1250
  },
1251
+ extensions: extensionEntries,
1110
1252
  },
1111
1253
  admin: resolvedAdminPrefix ? { prefix: resolvedAdminPrefix } : null,
1112
1254
  adminApi: resolvedAdminApiPrefix ? { prefix: resolvedAdminApiPrefix } : null,
@@ -1,3 +1,4 @@
1
+ import { accountNavFor } from '../../account/runtime.js';
1
2
  import { getAccountLoginUrl } from '../account_login_url.js';
2
3
  import { accountPathsMap } from '../account_paths.js';
3
4
  import { DEFAULT_MESSAGES, translate } from '../i18n.js';
@@ -40,9 +41,13 @@ export async function renderEdgeView(ctx, view, props) {
40
41
  // `accountRoutes` — sem cada view conhecer o prefixo. Os action-subpaths
41
42
  // fixos (`/password`, `/passkeys/verify`, ...) são concatenados na própria view.
42
43
  const accountPaths = accountPathsMap();
44
+ // `accountNav` como prop global das telas do console de conta: telas embutidas montadas e
45
+ // páginas de extension visíveis para a conta (partial `authkit::account/partials/nav`).
46
+ const accountNav = props.accountNav ?? (await accountNavFor(ctx, view, props));
43
47
  return ctx.view.render(`authkit::${view}`, {
44
48
  loginUrl,
45
49
  accountPaths,
50
+ accountNav,
46
51
  ...props,
47
52
  t,
48
53
  messages,
@@ -1,3 +1,4 @@
1
+ import { accountNavFor } from '../../account/runtime.js';
1
2
  import { DEFAULT_MESSAGES } from '../i18n.js';
2
3
  import { renderEdgeView } from './edge_renderer.js';
3
4
  /**
@@ -68,6 +69,11 @@ export function inertiaRenderer(opts) {
68
69
  // Happy path: view vai ao Inertia.
69
70
  // `messages` vai como shared prop para as páginas React traduzirem.
70
71
  const messages = await resolveMessagesFromCtx(ctx);
71
- return ctx.inertia.render(`${opts.prefix}/${view}`, { ...props, messages });
72
+ const accountNav = props.accountNav ?? (await accountNavFor(ctx, view, props));
73
+ return ctx.inertia.render(`${opts.prefix}/${view}`, {
74
+ ...(accountNav ? { accountNav } : {}),
75
+ ...props,
76
+ messages,
77
+ });
72
78
  };
73
79
  }
@@ -8,6 +8,8 @@
8
8
  * typed per view, so a controller that drifts from these types fails the
9
9
  * package's `tsc`.
10
10
  */
11
+ import type { ResolvedActionStep } from '../account/http.js';
12
+ import type { AccountNavEntry, ResolvedAccountPage } from '../account/runtime.js';
11
13
  import type { OrgInvitation, OrgMember, OrgSummary } from '../accounts/account_store.js';
12
14
  import type { PatRecord } from '../pat/pat_store.js';
13
15
  import type { AccountConfirmProps, AccountEmailConfirmedProps, AccountLoginProps, AccountMfaProps, AccountSecurityProps } from './account_screen_props.js';
@@ -183,16 +185,22 @@ export interface ThrottledProps extends WithMessages {
183
185
  retryUrl: string;
184
186
  }
185
187
  /** The agent asking for access, on `agents/consent`. */
188
+ /** One scope on the personal-agent consent screen. */
189
+ export interface AgentConsentScope {
190
+ id: string;
191
+ description: string;
192
+ /** i18n key shown instead of `description` (an extension's message). */
193
+ descriptionKey?: string;
194
+ /** Not ticked for the person: granted only if they tick it themselves. */
195
+ optIn?: boolean;
196
+ }
186
197
  export interface AgentConsentRequest {
187
198
  /** Device-flow user code, when the agent uses it. */
188
199
  userCode?: string;
189
200
  agentName: string | null;
190
201
  agentOrigin: string | null;
191
202
  logoUri?: string | null;
192
- scopes: Array<{
193
- id: string;
194
- description: string;
195
- }>;
203
+ scopes: Array<AgentConsentScope>;
196
204
  /** Extra hidden form fields to send back. */
197
205
  hidden?: Array<{
198
206
  name: string;
@@ -216,10 +224,7 @@ export interface AgentsConsentProps extends WithMessages {
216
224
  export interface AgentsDoneProps extends WithMessages {
217
225
  status: 'approved' | 'denied' | 'expired' | 'invalid';
218
226
  agentName: string | null;
219
- scopes: Array<{
220
- id: string;
221
- description: string;
222
- }>;
227
+ scopes: Array<AgentConsentScope>;
223
228
  }
224
229
  /** `account/apps` — apps and personal agents with access to the account. */
225
230
  export interface AccountAppsProps extends WithMessages {
@@ -276,6 +281,31 @@ export interface AccountOrgsProps extends WithMessages {
276
281
  invitation?: OrgInvitation | null;
277
282
  token?: string;
278
283
  }
284
+ /**
285
+ * `account/extension` — a page an account console extension added. `page` is the resolved page
286
+ * (texts translated, values formatted); `step` is set on the confirmation step of an action.
287
+ * `accountNav` (the console navigation) is added by the renderer to every account screen.
288
+ */
289
+ export interface AccountExtensionProps extends WithMessages {
290
+ csrfToken: string;
291
+ page: ResolvedAccountPage;
292
+ step: ResolvedActionStep | null;
293
+ flash: {
294
+ tone: 'success' | 'danger';
295
+ text: string;
296
+ } | null;
297
+ /** The response's CSP nonce, for the scripts of `component` blocks. */
298
+ cspNonce: string | null;
299
+ accountNav?: AccountNavEntry[] | null;
300
+ /** `<html lang>` of the console locale. */
301
+ htmlLang?: string;
302
+ /** Edge only: the `<script>` tag of a `component` block's script (nonce included). */
303
+ scriptTag?: (script: {
304
+ src: string;
305
+ integrity: string | null;
306
+ module: boolean;
307
+ }) => string;
308
+ }
279
309
  /** Props of every screen, by view name. */
280
310
  export interface AuthkitScreenProps {
281
311
  login: LoginProps;
@@ -299,6 +329,7 @@ export interface AuthkitScreenProps {
299
329
  'account/orgs': AccountOrgsProps;
300
330
  'account/confirm': AccountConfirmProps;
301
331
  'account/email-confirmed': AccountEmailConfirmedProps;
332
+ 'account/extension': AccountExtensionProps;
302
333
  }
303
334
  /** Every screen authkit-server renders. */
304
335
  export type AuthkitScreen = keyof AuthkitScreenProps;