@adonis-agora/authkit-server 0.90.2 → 1.0.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/CHANGELOG.md +3654 -0
  2. package/build/commands/clients_create.d.ts +1 -0
  3. package/build/commands/clients_create.js +15 -2
  4. package/build/commands/commands.json +7 -0
  5. package/build/host/ui/admin.html +1 -1
  6. package/build/host/views/account/apps.edge +8 -3
  7. package/build/host/views/partials/styles.edge +1 -1
  8. package/build/index.d.ts +5 -3
  9. package/build/index.js +4 -5
  10. package/build/providers/authkit_server_provider.js +8 -0
  11. package/build/src/accounts/lucid_account_store.d.ts +0 -18
  12. package/build/src/accounts/lucid_account_store.js +7 -48
  13. package/build/src/accounts/lucid_store/mfa.js +1 -1
  14. package/build/src/accounts/lucid_store/organizations.d.ts +1 -1
  15. package/build/src/accounts/lucid_store/password_hygiene.d.ts +2 -3
  16. package/build/src/accounts/lucid_store/password_hygiene.js +2 -3
  17. package/build/src/accounts/lucid_store/shared.d.ts +0 -7
  18. package/build/src/accounts/lucid_store/shared.js +0 -14
  19. package/build/src/accounts/lucid_stores.d.ts +1 -1
  20. package/build/src/adapters/redis_adapter.js +12 -2
  21. package/build/src/console_authorize.d.ts +56 -0
  22. package/build/src/console_authorize.js +85 -0
  23. package/build/src/define_config.d.ts +26 -14
  24. package/build/src/define_config.js +5 -3
  25. package/build/src/doctor/checks.d.ts +10 -5
  26. package/build/src/doctor/checks.js +52 -8
  27. package/build/src/host/account_api/account_api_controller.d.ts +2 -1
  28. package/build/src/host/account_api/account_api_controller.js +5 -11
  29. package/build/src/host/admin_api/api_clients_controller.d.ts +9 -3
  30. package/build/src/host/admin_api/dto.d.ts +3 -1
  31. package/build/src/host/admin_api/dto.js +3 -1
  32. package/build/src/host/admin_clients_service.d.ts +13 -5
  33. package/build/src/host/admin_clients_service.js +28 -6
  34. package/build/src/host/admin_console/admin_shell_controller.d.ts +16 -1
  35. package/build/src/host/admin_console/admin_shell_controller.js +21 -7
  36. package/build/src/host/admin_console/console_clients_controller.d.ts +21 -2
  37. package/build/src/host/admin_console/console_clients_controller.js +9 -0
  38. package/build/src/host/admin_console/console_impersonation_controller.js +3 -8
  39. package/build/src/host/admin_console/console_overview_controller.d.ts +2 -2
  40. package/build/src/host/admin_console/console_overview_controller.js +4 -2
  41. package/build/src/host/admin_console_warning.d.ts +8 -0
  42. package/build/src/host/admin_console_warning.js +18 -0
  43. package/build/src/host/admin_validators.d.ts +15 -11
  44. package/build/src/host/admin_validators.js +16 -9
  45. package/build/src/host/auth_host_config.d.ts +5 -0
  46. package/build/src/host/client_names.d.ts +9 -2
  47. package/build/src/host/client_names.js +11 -3
  48. package/build/src/host/consent_params.d.ts +7 -0
  49. package/build/src/host/consent_params.js +27 -0
  50. package/build/src/host/console_denied_page.d.ts +6 -0
  51. package/build/src/host/console_denied_page.js +18 -0
  52. package/build/src/host/console_sso.d.ts +24 -0
  53. package/build/src/host/console_sso.js +70 -0
  54. package/build/src/host/controllers/account_apps_controller.js +4 -3
  55. package/build/src/host/controllers/account_confirm_controller.d.ts +0 -7
  56. package/build/src/host/controllers/account_confirm_controller.js +0 -6
  57. package/build/src/host/controllers/account_orgs_controller.d.ts +0 -50
  58. package/build/src/host/controllers/account_orgs_controller.js +7 -113
  59. package/build/src/host/controllers/account_security_controller.js +7 -11
  60. package/build/src/host/controllers/account_session_controller.js +10 -1
  61. package/build/src/host/controllers/agent_consent_controller.js +23 -5
  62. package/build/src/host/controllers/console_sso_controller.d.ts +12 -0
  63. package/build/src/host/controllers/console_sso_controller.js +61 -0
  64. package/build/src/host/controllers/interaction_controller.js +9 -3
  65. package/build/src/host/controllers/poppy_consent_controller.js +33 -7
  66. package/build/src/host/controllers/registration_controller.js +7 -2
  67. package/build/src/host/i18n.d.ts +0 -4
  68. package/build/src/host/i18n.js +0 -6
  69. package/build/src/host/impersonation.d.ts +7 -15
  70. package/build/src/host/impersonation.js +7 -16
  71. package/build/src/host/interaction_recovery.js +4 -1
  72. package/build/src/host/login_attempt.js +5 -1
  73. package/build/src/host/middleware/account_auth.js +6 -0
  74. package/build/src/host/rate_limit.js +5 -1
  75. package/build/src/host/register_auth_host.d.ts +8 -0
  76. package/build/src/host/register_auth_host.js +116 -11
  77. package/build/src/host/renderers/inertia_renderer.d.ts +9 -21
  78. package/build/src/host/renderers/inertia_renderer.js +5 -6
  79. package/build/src/host/screen_props.d.ts +309 -0
  80. package/build/src/host/screen_props.js +11 -0
  81. package/build/src/host/sudo_mode.d.ts +5 -11
  82. package/build/src/host/sudo_mode.js +5 -11
  83. package/build/src/host/ui-dist/assets/index-D0EIeyKK.js +67 -0
  84. package/build/src/host/ui-dist/index.html +1 -1
  85. package/build/src/password/password_manager.js +5 -0
  86. package/build/src/provider/build_provider.js +6 -34
  87. package/build/src/provider/oidc_service.d.ts +0 -2
  88. package/build/src/provider/oidc_service.js +1 -12
  89. package/build/src/provider/token_exchange.d.ts +1 -3
  90. package/build/src/provider/token_exchange.js +1 -1
  91. package/build/stubs/config/authkit_react.stub +3 -3
  92. package/package.json +12 -11
  93. package/skills/authkit-interactions/SKILL.md +32 -5
  94. package/stubs/config/authkit_react.stub +3 -3
  95. package/build/src/commands/import_clients.d.ts +0 -40
  96. package/build/src/commands/import_clients.js +0 -81
  97. package/build/src/host/ui-dist/assets/index-DUVBDAyh.js +0 -67
  98. package/build/src/mixins/with_mfa.d.ts +0 -35
  99. package/build/src/mixins/with_mfa.js +0 -21
@@ -1,5 +1,6 @@
1
1
  import { randomBytes } from 'node:crypto';
2
- import { assertClientMetadata } from './client_metadata.js';
2
+ import { assertClientMetadata, ClientMetadataError, } from './client_metadata.js';
3
+ import { CONSOLE_SSO_CLIENT_ID } from './console_sso.js';
3
4
  /** Métodos públicos (sem segredo) — espelham os "non-secret" do oidc-provider. */
4
5
  const PUBLIC_AUTH_METHODS = new Set(['none']);
5
6
  /** Gera um identificador opaco no estilo do oidc-provider (~43 chars base64url). */
@@ -51,6 +52,7 @@ export class AdminClientsService {
51
52
  */
52
53
  async create(input) {
53
54
  const clientId = (input.clientId ?? '').trim() || randomId();
55
+ assertNotReservedClientId(clientId);
54
56
  const confidential = !PUBLIC_AUTH_METHODS.has(input.tokenEndpointAuthMethod);
55
57
  const clientSecret = confidential ? randomId() : undefined;
56
58
  const payload = this.#buildPayload(clientId, input, clientSecret);
@@ -60,15 +62,15 @@ export class AdminClientsService {
60
62
  return { clientId, clientSecret };
61
63
  }
62
64
  /**
63
- * Cria um client PRESERVANDO um `clientSecret` fornecido externamente (em vez de
64
- * gerar um aleatório). Útil para migração de clients estáticos do config, onde o
65
- * secret original deve ser preservado para não quebrar apps já configurados.
65
+ * Cria um client com um `clientSecret` fornecido externamente (em vez de gerar
66
+ * um aleatório) — ex.: para manter o secret que os RPs já usam.
66
67
  *
67
68
  * Para clients públicos (auth method `none`), `providedSecret` é ignorado.
68
69
  * Lança se o `clientId` já existir no adapter — chame `find` antes.
69
70
  */
70
71
  async createWithSecret(input, providedSecret) {
71
72
  const clientId = (input.clientId ?? '').trim() || randomId();
73
+ assertNotReservedClientId(clientId);
72
74
  const confidential = !PUBLIC_AUTH_METHODS.has(input.tokenEndpointAuthMethod);
73
75
  const clientSecret = confidential ? (providedSecret ?? randomId()) : undefined;
74
76
  const payload = this.#buildPayload(clientId, input, clientSecret);
@@ -98,9 +100,17 @@ export class AdminClientsService {
98
100
  existing.token_endpoint_auth_method ??
99
101
  'client_secret_basic',
100
102
  applicationType: input.applicationType ?? existing.application_type ?? 'web',
101
- backchannelLogoutUri: input.backchannelLogoutUri ?? existing.backchannel_logout_uri,
103
+ backchannelLogoutUri: input.backchannelLogoutUri === null
104
+ ? undefined
105
+ : (input.backchannelLogoutUri ?? existing.backchannel_logout_uri),
102
106
  backchannelLogoutSessionRequired: input.backchannelLogoutSessionRequired ??
103
107
  existing.backchannel_logout_session_required,
108
+ clientName: input.clientName === null
109
+ ? undefined
110
+ : (input.clientName ?? existing.client_name),
111
+ logoUri: input.logoUri === null
112
+ ? undefined
113
+ : (input.logoUri ?? existing.logo_uri),
104
114
  };
105
115
  const confidential = !PUBLIC_AUTH_METHODS.has(merged.tokenEndpointAuthMethod);
106
116
  // Mantém o secret atual se continua confidencial; se virou public, remove-o.
@@ -173,6 +183,10 @@ export class AdminClientsService {
173
183
  // sempre (sem `application_type` o oidc-provider assume `web`).
174
184
  if (input.applicationType === 'native')
175
185
  payload.application_type = 'native';
186
+ if (input.clientName)
187
+ payload.client_name = input.clientName;
188
+ if (input.logoUri)
189
+ payload.logo_uri = input.logoUri;
176
190
  if (input.backchannelLogoutUri) {
177
191
  payload.backchannel_logout_uri = input.backchannelLogoutUri;
178
192
  if (input.backchannelLogoutSessionRequired !== undefined) {
@@ -188,11 +202,13 @@ export class AdminClientsService {
188
202
  const result = {
189
203
  clientId: row.clientId,
190
204
  confidential: !!p.client_secret,
191
- grants: p.grant_types ?? ['authorization_code', 'refresh_token'],
205
+ grantTypes: p.grant_types ?? ['authorization_code', 'refresh_token'],
192
206
  redirectUris: p.redirect_uris ?? [],
193
207
  postLogoutRedirectUris: p.post_logout_redirect_uris ?? [],
194
208
  tokenEndpointAuthMethod: authMethod,
195
209
  applicationType: p.application_type === 'native' ? 'native' : 'web',
210
+ clientName: typeof p.client_name === 'string' && p.client_name ? p.client_name : null,
211
+ logoUri: typeof p.logo_uri === 'string' && p.logo_uri ? p.logo_uri : null,
196
212
  };
197
213
  if (p.backchannel_logout_uri) {
198
214
  result.backchannelLogoutUri = p.backchannel_logout_uri;
@@ -202,3 +218,9 @@ export class AdminClientsService {
202
218
  return result;
203
219
  }
204
220
  }
221
+ /** O `client_id` do client interno do console (ver `console_sso.ts`) não pode ser cadastrado. */
222
+ function assertNotReservedClientId(clientId) {
223
+ if (clientId === CONSOLE_SSO_CLIENT_ID) {
224
+ throw new ClientMetadataError(`client_id "${clientId}" é reservado pelo authkit`);
225
+ }
226
+ }
@@ -1,4 +1,19 @@
1
1
  import type { HttpContext } from '@adonisjs/core/http';
2
+ /** Id of the JSON data block the SPA reads its config from. */
3
+ export declare const ADMIN_CONFIG_ELEMENT_ID = "authkit-admin-config";
4
+ /**
5
+ * Serializes the config for an HTML data block: `<`, `>`, `&` and the JS line
6
+ * separators are escaped so no value can close the `<script>` element.
7
+ */
8
+ export declare function serializeConfigBlock(config: Record<string, unknown>): string;
9
+ /**
10
+ * Embeds the config as a `<script type="application/json">` data block right
11
+ * after `<body>`. A data block is never executed, so a strict CSP
12
+ * (`script-src 'self' 'nonce-…'`) leaves it alone; the SPA parses it.
13
+ * Also rewrites `/__AUTHKIT_BASE__/` → actual adminBase so all asset URLs
14
+ * produced by Vite (base: '/__AUTHKIT_BASE__/') resolve correctly at runtime.
15
+ */
16
+ export declare function prepareShell(html: string, config: Record<string, unknown>): string;
2
17
  /**
3
18
  * Controla o serving do console admin React (Missão B).
4
19
  *
@@ -6,7 +21,7 @@ import type { HttpContext } from '@adonisjs/core/http';
6
21
  * `serveAsset` — serve arquivos estáticos do build (JS/CSS/fonts/images) com cache.
7
22
  *
8
23
  * O shell HTML é o index.html produzido pelo Vite build em `build/host/ui-dist/`.
9
- * O servidor injeta `window.__AUTHKIT__` e reescreve o placeholder de base dinâmica
24
+ * O servidor embute a config num data block JSON (`#authkit-admin-config`) e reescreve o placeholder de base dinâmica
10
25
  * `/__AUTHKIT_BASE__/` → `${adminBase}/assets/` para que todos os imports de assets
11
26
  * Vite resolvam corretamente independente do prefixo admin configurado.
12
27
  *
@@ -50,19 +50,33 @@ then restart the server.</p>
50
50
  }
51
51
  }
52
52
  // ─── Config injection ─────────────────────────────────────────────────────────
53
+ /** Id of the JSON data block the SPA reads its config from. */
54
+ export const ADMIN_CONFIG_ELEMENT_ID = 'authkit-admin-config';
53
55
  /**
54
- * Injects `window.__AUTHKIT__` into the HTML shell right after `<body>`.
56
+ * Serializes the config for an HTML data block: `<`, `>`, `&` and the JS line
57
+ * separators are escaped so no value can close the `<script>` element.
58
+ */
59
+ export function serializeConfigBlock(config) {
60
+ return JSON.stringify(config)
61
+ .replace(/</g, '\\u003c')
62
+ .replace(/>/g, '\\u003e')
63
+ .replace(/&/g, '\\u0026')
64
+ .replace(/\u2028/g, '\\u2028')
65
+ .replace(/\u2029/g, '\\u2029');
66
+ }
67
+ /**
68
+ * Embeds the config as a `<script type="application/json">` data block right
69
+ * after `<body>`. A data block is never executed, so a strict CSP
70
+ * (`script-src 'self' 'nonce-…'`) leaves it alone; the SPA parses it.
55
71
  * Also rewrites `/__AUTHKIT_BASE__/` → actual adminBase so all asset URLs
56
72
  * produced by Vite (base: '/__AUTHKIT_BASE__/') resolve correctly at runtime.
57
73
  */
58
- function prepareShell(html, config) {
74
+ export function prepareShell(html, config) {
59
75
  const adminBase = config.adminBase;
60
76
  // 1. Rewrite the Vite placeholder base in ALL occurrences (src/href attributes).
61
- // Vite puts `/__AUTHKIT_BASE__/assets/foo.js` so we rewrite `/__AUTHKIT_BASE__/`
62
- // to `${adminBase}/` so asset URLs become `${adminBase}/assets/foo.js`.
63
77
  const rewritten = html.replaceAll('/__AUTHKIT_BASE__/', `${adminBase}/`);
64
- // 2. Inject config script right after <body>.
65
- const script = `<script>window.__AUTHKIT__=${JSON.stringify(config)};</script>`;
78
+ // 2. Inject the config data block right after <body>.
79
+ const script = `<script type="application/json" id="${ADMIN_CONFIG_ELEMENT_ID}">${serializeConfigBlock(config)}</script>`;
66
80
  const bodyIdx = rewritten.indexOf('<body');
67
81
  if (bodyIdx === -1)
68
82
  return script + rewritten;
@@ -97,7 +111,7 @@ const MIME = {
97
111
  * `serveAsset` — serve arquivos estáticos do build (JS/CSS/fonts/images) com cache.
98
112
  *
99
113
  * O shell HTML é o index.html produzido pelo Vite build em `build/host/ui-dist/`.
100
- * O servidor injeta `window.__AUTHKIT__` e reescreve o placeholder de base dinâmica
114
+ * O servidor embute a config num data block JSON (`#authkit-admin-config`) e reescreve o placeholder de base dinâmica
101
115
  * `/__AUTHKIT_BASE__/` → `${adminBase}/assets/` para que todos os imports de assets
102
116
  * Vite resolvam corretamente independente do prefixo admin configurado.
103
117
  *
@@ -5,6 +5,7 @@ import type { HttpContext } from '@adonisjs/core/http';
5
5
  *
6
6
  * GET {prefix}/api/clients → lista clients dinâmicos
7
7
  * POST {prefix}/api/clients → criar client (secret retornado UMA vez)
8
+ * GET {prefix}/api/clients/:id → um client (404 se não existir)
8
9
  * PATCH {prefix}/api/clients/:id → atualizar metadata
9
10
  * DELETE {prefix}/api/clients/:id → remover client
10
11
  * POST {prefix}/api/clients/:id/regenerate-secret → gerar novo secret
@@ -15,16 +16,32 @@ export default class ConsoleClientsController {
15
16
  data: {
16
17
  clientId: string;
17
18
  confidential: boolean;
18
- grants: string[];
19
+ grantTypes: string[];
19
20
  redirectUris: string[];
20
21
  postLogoutRedirectUris: string[];
21
22
  tokenEndpointAuthMethod: string;
22
23
  applicationType: import("../client_metadata.js").ApplicationType;
23
24
  backchannelLogoutUri: string | null;
24
25
  backchannelLogoutSessionRequired: boolean;
26
+ clientName: string | null;
27
+ logoUri: string | null;
25
28
  }[];
26
29
  canList: boolean;
27
30
  }>;
31
+ /** GET {prefix}/api/clients/:id */
32
+ show(ctx: HttpContext): Promise<void | {
33
+ clientId: string;
34
+ confidential: boolean;
35
+ grantTypes: string[];
36
+ redirectUris: string[];
37
+ postLogoutRedirectUris: string[];
38
+ tokenEndpointAuthMethod: string;
39
+ applicationType: import("../client_metadata.js").ApplicationType;
40
+ backchannelLogoutUri: string | null;
41
+ backchannelLogoutSessionRequired: boolean;
42
+ clientName: string | null;
43
+ logoUri: string | null;
44
+ }>;
28
45
  /** POST {prefix}/api/clients */
29
46
  store(ctx: HttpContext): Promise<void | {
30
47
  clientId: string;
@@ -34,13 +51,15 @@ export default class ConsoleClientsController {
34
51
  update(ctx: HttpContext): Promise<void | {
35
52
  clientId: string;
36
53
  confidential: boolean;
37
- grants: string[];
54
+ grantTypes: string[];
38
55
  redirectUris: string[];
39
56
  postLogoutRedirectUris: string[];
40
57
  tokenEndpointAuthMethod: string;
41
58
  applicationType: import("../client_metadata.js").ApplicationType;
42
59
  backchannelLogoutUri: string | null;
43
60
  backchannelLogoutSessionRequired: boolean;
61
+ clientName: string | null;
62
+ logoUri: string | null;
44
63
  }>;
45
64
  /** DELETE {prefix}/api/clients/:id */
46
65
  destroy(ctx: HttpContext): Promise<void | {
@@ -15,6 +15,7 @@ function invalidMetadata(ctx, err) {
15
15
  *
16
16
  * GET {prefix}/api/clients → lista clients dinâmicos
17
17
  * POST {prefix}/api/clients → criar client (secret retornado UMA vez)
18
+ * GET {prefix}/api/clients/:id → um client (404 se não existir)
18
19
  * PATCH {prefix}/api/clients/:id → atualizar metadata
19
20
  * DELETE {prefix}/api/clients/:id → remover client
20
21
  * POST {prefix}/api/clients/:id/regenerate-secret → gerar novo secret
@@ -30,6 +31,14 @@ export default class ConsoleClientsController {
30
31
  const clients = await svc.list();
31
32
  return { data: clients.map(clientDto), canList: true };
32
33
  }
34
+ /** GET {prefix}/api/clients/:id */
35
+ async show(ctx) {
36
+ const service = await ctx.containerResolver.make('authkit.server');
37
+ const client = await new AdminClientsService(service).find(ctx.request.param('id'));
38
+ if (!client)
39
+ return ctx.response.notFound(apiError('not_found', 'Client não encontrado.'));
40
+ return clientDto(client);
41
+ }
33
42
  /** POST {prefix}/api/clients */
34
43
  async store(ctx) {
35
44
  const service = await ctx.containerResolver.make('authkit.server');
@@ -49,11 +49,7 @@ export default class ConsoleImpersonationController {
49
49
  if (!account) {
50
50
  return ctx.response.notFound(apiError('not_found', 'Usuário não encontrado.'));
51
51
  }
52
- // Clients de RUNTIME. `cfg.clients` é sempre `[]` num host real — clients
53
- // vivem no adapter, criados pelo console/Admin API/`authkit:clients:create`.
54
- // Ler só o config fazia o painel devolver 404 SEMPRE, que é por que o botão
55
- // do console nunca funcionou. O fallback para o config cobre hosts legados
56
- // com clients estáticos e adapters que não enumeram.
52
+ // Clients vivem no adapter, criados pelo console/Admin API/`authkit:clients:create`.
57
53
  const panel = buildImpersonationPanel(cfg, targetId, await listRuntimeClients(service));
58
54
  if (!panel) {
59
55
  return ctx.response.notFound(apiError('no_token_exchange_client', 'Nenhum client habilitado ao grant token-exchange encontrado.'));
@@ -83,8 +79,7 @@ export default class ConsoleImpersonationController {
83
79
  }
84
80
  /**
85
81
  * Clients persistidos que podem hospedar o token-exchange. Best-effort: um
86
- * adapter sem `list` (ou que falhe) devolve lista vazia e o
87
- * `buildImpersonationPanel` cai no `cfg.clients`.
82
+ * adapter sem `list` (ou que falhe) devolve lista vazia e o painel responde 404.
88
83
  */
89
84
  async function listRuntimeClients(service) {
90
85
  try {
@@ -93,7 +88,7 @@ async function listRuntimeClients(service) {
93
88
  return [];
94
89
  return (await clients.list()).map((c) => ({
95
90
  clientId: c.clientId,
96
- grants: c.grants,
91
+ grantTypes: c.grantTypes,
97
92
  confidential: c.confidential,
98
93
  }));
99
94
  }
@@ -5,7 +5,7 @@ import type { HttpContext } from '@adonisjs/core/http';
5
5
  *
6
6
  * Métricas-resumo do IdP para o dashboard do console React. Reutiliza
7
7
  * `computeAdminStats` (mesma fonte do console Edge) e adiciona:
8
- * - `clientsCount` — número de clients na config
8
+ * - `clientsCount` — número de clients no adapter (0 quando o adapter não enumera)
9
9
  * - `auditTotal` — total de eventos de auditoria (0 quando sink não suporta)
10
10
  * - `recentEvents` — últimos 5 eventos de auditoria ([] quando não suporta)
11
11
  */
@@ -20,7 +20,7 @@ export default class ConsoleOverviewController {
20
20
  signUpsPerDay: import("../admin_stats_service.js").DailyPoint[];
21
21
  windowDays: number;
22
22
  auditSupported: boolean;
23
- clientsCount: any;
23
+ clientsCount: number;
24
24
  auditTotal: any;
25
25
  recentEvents: any;
26
26
  }>;
@@ -1,5 +1,6 @@
1
1
  import '../augmentations.js';
2
2
  import { LIST_FIRST_PAGE } from '../../pagination.js';
3
+ import { AdminClientsService } from '../admin_clients_service.js';
3
4
  import { AdminSessionsService } from '../admin_sessions_service.js';
4
5
  import { computeAdminStats } from '../admin_stats_service.js';
5
6
  /**
@@ -7,7 +8,7 @@ import { computeAdminStats } from '../admin_stats_service.js';
7
8
  *
8
9
  * Métricas-resumo do IdP para o dashboard do console React. Reutiliza
9
10
  * `computeAdminStats` (mesma fonte do console Edge) e adiciona:
10
- * - `clientsCount` — número de clients na config
11
+ * - `clientsCount` — número de clients no adapter (0 quando o adapter não enumera)
11
12
  * - `auditTotal` — total de eventos de auditoria (0 quando sink não suporta)
12
13
  * - `recentEvents` — últimos 5 eventos de auditoria ([] quando não suporta)
13
14
  */
@@ -17,7 +18,8 @@ export default class ConsoleOverviewController {
17
18
  const cfg = service.config;
18
19
  const sessions = new AdminSessionsService(service);
19
20
  const stats = await computeAdminStats(cfg, sessions);
20
- const clientsCount = cfg.clients.length;
21
+ const clients = new AdminClientsService(service);
22
+ const clientsCount = clients.canList ? (await clients.list()).length : 0;
21
23
  const recentResult = typeof cfg.audit?.list === 'function'
22
24
  ? await cfg.audit.list({ page: LIST_FIRST_PAGE, size: 5 })
23
25
  : null;
@@ -0,0 +1,8 @@
1
+ /** The boot warning for an admin console with no `admin.authorize`. Same wording in every console. */
2
+ export declare const ADMIN_CONSOLE_NO_AUTHORIZE_WARNING = "[@adonis-agora/authkit-server] console has no `authorize` configured: it is open outside production and denies every request in production. Set `authorize` in config/authkit.ts.";
3
+ /** Logs {@link ADMIN_CONSOLE_NO_AUTHORIZE_WARNING} once per process. */
4
+ export declare function warnAdminConsoleUnauthorized(logger: {
5
+ warn: (message: string) => unknown;
6
+ } | null | undefined): boolean;
7
+ /** Test helper: lets the warning fire again. */
8
+ export declare function resetAdminConsoleWarning(): void;
@@ -0,0 +1,18 @@
1
+ /** The boot warning for an admin console with no `admin.authorize`. Same wording in every console. */
2
+ export const ADMIN_CONSOLE_NO_AUTHORIZE_WARNING = '[@adonis-agora/authkit-server] console has no `authorize` configured: it is open outside production and denies every request in production. Set `authorize` in config/authkit.ts.';
3
+ let warned = false;
4
+ /** Logs {@link ADMIN_CONSOLE_NO_AUTHORIZE_WARNING} once per process. */
5
+ export function warnAdminConsoleUnauthorized(logger) {
6
+ if (warned)
7
+ return false;
8
+ warned = true;
9
+ if (logger)
10
+ logger.warn(ADMIN_CONSOLE_NO_AUTHORIZE_WARNING);
11
+ else
12
+ console.warn(ADMIN_CONSOLE_NO_AUTHORIZE_WARNING);
13
+ return true;
14
+ }
15
+ /** Test helper: lets the warning fire again. */
16
+ export function resetAdminConsoleWarning() {
17
+ warned = false;
18
+ }
@@ -4,8 +4,7 @@ import { type ApplicationType } from './client_metadata.js';
4
4
  * Input de client OIDC — serve create E update (PATCH). Tudo opcional: no create
5
5
  * o controller aplica defaults (ver {@link clientCreateInput}); no update os campos
6
6
  * ausentes (undefined) são preservados pelo `AdminClientsService.update` (merge,
7
- * ver {@link clientPartialInput}). `grants` é aceito como alias de `grantTypes`
8
- * (o DTO de saída usa `grants`).
7
+ * ver {@link clientPartialInput}).
9
8
  */
10
9
  export declare const clientInputValidator: import("@vinejs/vine").VineValidator<import("@vinejs/vine").VineObject<{
11
10
  clientId: import("@vinejs/vine/schema/base/literal").OptionalModifier<import("@vinejs/vine").VineString>;
@@ -13,37 +12,41 @@ export declare const clientInputValidator: import("@vinejs/vine").VineValidator<
13
12
  redirectUris: import("@vinejs/vine").OptionalModifier<import("@vinejs/vine").VineArray<import("@vinejs/vine").VineString>>;
14
13
  postLogoutRedirectUris: import("@vinejs/vine").OptionalModifier<import("@vinejs/vine").VineArray<import("@vinejs/vine").VineString>>;
15
14
  grantTypes: import("@vinejs/vine").OptionalModifier<import("@vinejs/vine").VineArray<import("@vinejs/vine").VineEnum<readonly ["authorization_code", "refresh_token", "client_credentials", "urn:ietf:params:oauth:grant-type:token-exchange"]>>>;
16
- grants: import("@vinejs/vine").OptionalModifier<import("@vinejs/vine").VineArray<import("@vinejs/vine").VineEnum<readonly ["authorization_code", "refresh_token", "client_credentials", "urn:ietf:params:oauth:grant-type:token-exchange"]>>>;
17
15
  tokenEndpointAuthMethod: import("@vinejs/vine/schema/base/literal").OptionalModifier<import("@vinejs/vine").VineEnum<readonly ["client_secret_basic", "client_secret_post", "none"]>>;
18
- backchannelLogoutUri: import("@vinejs/vine/schema/base/literal").OptionalModifier<import("@vinejs/vine").VineString>;
16
+ backchannelLogoutUri: import("@vinejs/vine/schema/base/literal").OptionalModifier<import("@vinejs/vine/schema/base/literal").NullableModifier<import("@vinejs/vine").VineString>>;
19
17
  backchannelLogoutSessionRequired: import("@vinejs/vine/schema/base/literal").OptionalModifier<import("@vinejs/vine").VineBoolean>;
18
+ clientName: import("@vinejs/vine/schema/base/literal").OptionalModifier<import("@vinejs/vine/schema/base/literal").NullableModifier<import("@vinejs/vine").VineString>>;
19
+ logoUri: import("@vinejs/vine/schema/base/literal").OptionalModifier<import("@vinejs/vine/schema/base/literal").NullableModifier<import("@vinejs/vine").VineString>>;
20
20
  }, {
21
21
  applicationType?: "native" | "web" | null | undefined;
22
22
  backchannelLogoutSessionRequired?: string | number | boolean | null | undefined;
23
23
  backchannelLogoutUri?: string | null | undefined;
24
24
  clientId?: string | null | undefined;
25
+ clientName?: string | null | undefined;
25
26
  grantTypes?: ("authorization_code" | "client_credentials" | "refresh_token" | "urn:ietf:params:oauth:grant-type:token-exchange")[] | null | undefined;
26
- grants?: ("authorization_code" | "client_credentials" | "refresh_token" | "urn:ietf:params:oauth:grant-type:token-exchange")[] | null | undefined;
27
+ logoUri?: string | null | undefined;
27
28
  postLogoutRedirectUris?: string[] | null | undefined;
28
29
  redirectUris?: string[] | null | undefined;
29
30
  tokenEndpointAuthMethod?: "client_secret_basic" | "client_secret_post" | "none" | null | undefined;
30
31
  }, {
31
32
  applicationType?: "native" | "web" | undefined;
32
33
  backchannelLogoutSessionRequired?: boolean | undefined;
33
- backchannelLogoutUri?: string | undefined;
34
+ backchannelLogoutUri?: string | null | undefined;
34
35
  clientId?: string | undefined;
36
+ clientName?: string | null | undefined;
35
37
  grantTypes?: ("authorization_code" | "client_credentials" | "refresh_token" | "urn:ietf:params:oauth:grant-type:token-exchange")[] | undefined;
36
- grants?: ("authorization_code" | "client_credentials" | "refresh_token" | "urn:ietf:params:oauth:grant-type:token-exchange")[] | undefined;
38
+ logoUri?: string | null | undefined;
37
39
  postLogoutRedirectUris?: string[] | undefined;
38
40
  redirectUris?: string[] | undefined;
39
41
  tokenEndpointAuthMethod?: "client_secret_basic" | "client_secret_post" | "none" | undefined;
40
42
  }, {
41
43
  applicationType?: "native" | "web" | undefined;
42
44
  backchannelLogoutSessionRequired?: boolean | undefined;
43
- backchannelLogoutUri?: string | undefined;
45
+ backchannelLogoutUri?: string | null | undefined;
44
46
  clientId?: string | undefined;
47
+ clientName?: string | null | undefined;
45
48
  grantTypes?: ("authorization_code" | "client_credentials" | "refresh_token" | "urn:ietf:params:oauth:grant-type:token-exchange")[] | undefined;
46
- grants?: ("authorization_code" | "client_credentials" | "refresh_token" | "urn:ietf:params:oauth:grant-type:token-exchange")[] | undefined;
49
+ logoUri?: string | null | undefined;
47
50
  postLogoutRedirectUris?: string[] | undefined;
48
51
  redirectUris?: string[] | undefined;
49
52
  tokenEndpointAuthMethod?: "client_secret_basic" | "client_secret_post" | "none" | undefined;
@@ -55,10 +58,11 @@ export type ClientInputFields = {
55
58
  redirectUris?: string[];
56
59
  postLogoutRedirectUris?: string[];
57
60
  grantTypes?: string[];
58
- grants?: string[];
59
61
  tokenEndpointAuthMethod?: TokenEndpointAuthMethod;
60
- backchannelLogoutUri?: string;
62
+ backchannelLogoutUri?: string | null;
61
63
  backchannelLogoutSessionRequired?: boolean;
64
+ clientName?: string | null;
65
+ logoUri?: string | null;
62
66
  };
63
67
  /**
64
68
  * Mapeia o input validado para um {@link ClientInput} COMPLETO (create), com defaults.
@@ -83,8 +83,7 @@ const grantTypeArray = vine.array(vine.enum(ALLOWED_GRANT_TYPES));
83
83
  * Input de client OIDC — serve create E update (PATCH). Tudo opcional: no create
84
84
  * o controller aplica defaults (ver {@link clientCreateInput}); no update os campos
85
85
  * ausentes (undefined) são preservados pelo `AdminClientsService.update` (merge,
86
- * ver {@link clientPartialInput}). `grants` é aceito como alias de `grantTypes`
87
- * (o DTO de saída usa `grants`).
86
+ * ver {@link clientPartialInput}).
88
87
  */
89
88
  export const clientInputValidator = vine.compile(vine.object({
90
89
  clientId: vine.string().trim().optional(),
@@ -96,11 +95,14 @@ export const clientInputValidator = vine.compile(vine.object({
96
95
  postLogoutRedirectUris: redirectUriArray.optional(),
97
96
  // grant_types restrito à allowlist; `implicit` e desconhecidos viram 422 (M10).
98
97
  grantTypes: grantTypeArray.optional(),
99
- grants: grantTypeArray.optional(),
100
98
  tokenEndpointAuthMethod: authMethod.optional(),
101
99
  // backchannel_logout_uri também precisa ser URI absoluta (L10).
102
- backchannelLogoutUri: vine.string().trim().url(URL_OPTS).optional(),
100
+ // `null` (update) remove a URI.
101
+ backchannelLogoutUri: vine.string().trim().url(URL_OPTS).nullable().optional(),
103
102
  backchannelLogoutSessionRequired: vine.boolean().optional(),
103
+ // `null` (update) removes them.
104
+ clientName: vine.string().trim().maxLength(200).nullable().optional(),
105
+ logoUri: vine.string().trim().url(URL_OPTS).nullable().optional(),
104
106
  }));
105
107
  /**
106
108
  * Mapeia o input validado para um {@link ClientInput} COMPLETO (create), com defaults.
@@ -114,10 +116,12 @@ export function clientCreateInput(v) {
114
116
  applicationType,
115
117
  redirectUris: v.redirectUris ?? [],
116
118
  postLogoutRedirectUris: v.postLogoutRedirectUris ?? [],
117
- grantTypes: v.grantTypes ?? v.grants ?? [],
119
+ grantTypes: v.grantTypes ?? [],
118
120
  tokenEndpointAuthMethod: v.tokenEndpointAuthMethod ?? (applicationType === 'native' ? 'none' : 'client_secret_basic'),
119
121
  backchannelLogoutUri: v.backchannelLogoutUri || undefined,
120
122
  backchannelLogoutSessionRequired: v.backchannelLogoutSessionRequired,
123
+ clientName: v.clientName || undefined,
124
+ logoUri: v.logoUri || undefined,
121
125
  };
122
126
  }
123
127
  /**
@@ -132,15 +136,18 @@ export function clientPartialInput(v) {
132
136
  out.redirectUris = v.redirectUris;
133
137
  if (v.postLogoutRedirectUris !== undefined)
134
138
  out.postLogoutRedirectUris = v.postLogoutRedirectUris;
135
- const grants = v.grantTypes ?? v.grants;
136
- if (grants !== undefined)
137
- out.grantTypes = grants;
139
+ if (v.grantTypes !== undefined)
140
+ out.grantTypes = v.grantTypes;
138
141
  if (v.tokenEndpointAuthMethod !== undefined)
139
142
  out.tokenEndpointAuthMethod = v.tokenEndpointAuthMethod;
140
143
  if (v.backchannelLogoutUri !== undefined)
141
- out.backchannelLogoutUri = v.backchannelLogoutUri.trim() || undefined;
144
+ out.backchannelLogoutUri = v.backchannelLogoutUri?.trim() || null;
142
145
  if (v.backchannelLogoutSessionRequired !== undefined)
143
146
  out.backchannelLogoutSessionRequired = v.backchannelLogoutSessionRequired;
147
+ if (v.clientName !== undefined)
148
+ out.clientName = v.clientName?.trim() || null;
149
+ if (v.logoUri !== undefined)
150
+ out.logoUri = v.logoUri?.trim() || null;
144
151
  return out;
145
152
  }
146
153
  // ─── Usuários ───────────────────────────────────────────────────────────────
@@ -40,6 +40,11 @@ export interface AuthHostRuntimeConfig {
40
40
  prefix: string;
41
41
  poppyPrefix?: string;
42
42
  };
43
+ /**
44
+ * `accountSession.acceptIdpSession: false` com `authMethods: { password: false }`:
45
+ * o console só entra por um `accountLoginUrl` do host (`registerAuthHost` recusa sem ele).
46
+ */
47
+ consoleNeedsLoginUrl?: boolean;
43
48
  /**
44
49
  * API headless (Clerk-style). Presente quando o host a declarou — o provider o
45
50
  * stash no boot para `registerAuthHost` montar as rotas sem reler o config inteiro.
@@ -2,10 +2,17 @@
2
2
  * Nome de exibição de cada client: o `client_name` que ele registrou (um cliente MCP registra o
3
3
  * seu, ex.: "Claude Code"), senão o próprio `client_id`. Client que sumiu do adapter → o id.
4
4
  */
5
- export declare function clientDisplayNames(service: {
5
+ export declare function clientDisplayNames(service: ClientLookup, clientIds: string[]): Promise<Map<string, string>>;
6
+ type ClientLookup = {
6
7
  provider: {
7
8
  Client: {
8
9
  find(id: string): Promise<any>;
9
10
  };
10
11
  };
11
- }, clientIds: string[]): Promise<Map<string, string>>;
12
+ };
13
+ /** Nome de exibição e logo (`logo_uri` registrado, ou `null`) de cada client. */
14
+ export declare function clientDisplayInfo(service: ClientLookup, clientIds: string[]): Promise<Map<string, {
15
+ name: string;
16
+ logoUrl: string | null;
17
+ }>>;
18
+ export {};
@@ -3,10 +3,18 @@
3
3
  * seu, ex.: "Claude Code"), senão o próprio `client_id`. Client que sumiu do adapter → o id.
4
4
  */
5
5
  export async function clientDisplayNames(service, clientIds) {
6
- const names = new Map();
6
+ const info = await clientDisplayInfo(service, clientIds);
7
+ return new Map([...info].map(([id, v]) => [id, v.name]));
8
+ }
9
+ /** Nome de exibição e logo (`logo_uri` registrado, ou `null`) de cada client. */
10
+ export async function clientDisplayInfo(service, clientIds) {
11
+ const info = new Map();
7
12
  await Promise.all([...new Set(clientIds)].map(async (clientId) => {
8
13
  const client = await service.provider.Client.find(clientId).catch(() => null);
9
- names.set(clientId, client?.clientName || clientId);
14
+ info.set(clientId, {
15
+ name: client?.clientName || clientId,
16
+ logoUrl: typeof client?.logoUri === 'string' && client.logoUri ? client.logoUri : null,
17
+ });
10
18
  }));
11
- return names;
19
+ return info;
12
20
  }
@@ -0,0 +1,7 @@
1
+ import type { ConsentRequestParams } from './screen_props.js';
2
+ /**
3
+ * The interaction's request parameters for the consent screen: `client_id` and the
4
+ * standard string parameters are guaranteed strings (or absent); everything else is
5
+ * passed through untouched.
6
+ */
7
+ export declare function consentRequestParams(raw: Record<string, unknown> | undefined): ConsentRequestParams;
@@ -0,0 +1,27 @@
1
+ /** Optional standard parameters of an authorization request, typed as strings. */
2
+ const CONSENT_STRING_PARAMS = [
3
+ 'redirect_uri',
4
+ 'response_type',
5
+ 'scope',
6
+ 'state',
7
+ 'prompt',
8
+ 'nonce',
9
+ 'code_challenge',
10
+ 'code_challenge_method',
11
+ ];
12
+ /**
13
+ * The interaction's request parameters for the consent screen: `client_id` and the
14
+ * standard string parameters are guaranteed strings (or absent); everything else is
15
+ * passed through untouched.
16
+ */
17
+ export function consentRequestParams(raw) {
18
+ const params = { ...raw, client_id: String(raw?.client_id ?? '') };
19
+ for (const key of CONSENT_STRING_PARAMS) {
20
+ const value = raw?.[key];
21
+ if (typeof value === 'string')
22
+ params[key] = value;
23
+ else
24
+ delete params[key];
25
+ }
26
+ return params;
27
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * The page a browser gets when `admin.authorize` denies the admin console
3
+ * (403), or asks for a sign-in it cannot do itself (401). Self-contained: no
4
+ * script and no external asset, so any CSP renders it.
5
+ */
6
+ export declare function consoleDeniedPage(status: 401 | 403): string;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The page a browser gets when `admin.authorize` denies the admin console
3
+ * (403), or asks for a sign-in it cannot do itself (401). Self-contained: no
4
+ * script and no external asset, so any CSP renders it.
5
+ */
6
+ export function consoleDeniedPage(status) {
7
+ const title = status === 401 ? 'Sign in required' : 'Access denied';
8
+ const body = status === 401
9
+ ? 'This console needs credentials this request did not carry.'
10
+ : 'You do not have access to this console.';
11
+ return `<!doctype html>
12
+ <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
13
+ <title>${title}</title>
14
+ <style>body{font-family:system-ui,sans-serif;background:#f9fafb;color:#111827;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0;padding:16px}
15
+ .box{max-width:420px;text-align:center}h1{font-size:20px;margin:0 0 8px}p{color:#4b5563;font-size:14px;line-height:1.6;margin:0}
16
+ .code{font-size:12px;color:#9ca3af;margin-bottom:8px}</style>
17
+ </head><body><main class="box"><p class="code">${status}</p><h1>${title}</h1><p>${body}</p></main></body></html>`;
18
+ }