@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,5 +1,7 @@
1
1
  import { randomBytes } from 'node:crypto';
2
- import { assertClientMetadata } from './client_metadata.js';
2
+ import { CLIENT_DISABLED_KEY, isClientDisabled, normalizeServiceTenant, SERVICE_TENANT_KEY, serviceTenantOf, } from '../service_tenants/client_metadata.js';
3
+ import { assertClientMetadata, ClientMetadataError, } from './client_metadata.js';
4
+ import { CONSOLE_SSO_CLIENT_ID } from './console_sso.js';
3
5
  /** Métodos públicos (sem segredo) — espelham os "non-secret" do oidc-provider. */
4
6
  const PUBLIC_AUTH_METHODS = new Set(['none']);
5
7
  /** Gera um identificador opaco no estilo do oidc-provider (~43 chars base64url). */
@@ -51,6 +53,7 @@ export class AdminClientsService {
51
53
  */
52
54
  async create(input) {
53
55
  const clientId = (input.clientId ?? '').trim() || randomId();
56
+ assertNotReservedClientId(clientId);
54
57
  const confidential = !PUBLIC_AUTH_METHODS.has(input.tokenEndpointAuthMethod);
55
58
  const clientSecret = confidential ? randomId() : undefined;
56
59
  const payload = this.#buildPayload(clientId, input, clientSecret);
@@ -68,6 +71,7 @@ export class AdminClientsService {
68
71
  */
69
72
  async createWithSecret(input, providedSecret) {
70
73
  const clientId = (input.clientId ?? '').trim() || randomId();
74
+ assertNotReservedClientId(clientId);
71
75
  const confidential = !PUBLIC_AUTH_METHODS.has(input.tokenEndpointAuthMethod);
72
76
  const clientSecret = confidential ? (providedSecret ?? randomId()) : undefined;
73
77
  const payload = this.#buildPayload(clientId, input, clientSecret);
@@ -108,14 +112,37 @@ export class AdminClientsService {
108
112
  logoUri: input.logoUri === null
109
113
  ? undefined
110
114
  : (input.logoUri ?? existing.logo_uri),
115
+ serviceTenant: input.serviceTenant === undefined
116
+ ? serviceTenantOf(existing)
117
+ : input.serviceTenant,
111
118
  };
112
119
  const confidential = !PUBLIC_AUTH_METHODS.has(merged.tokenEndpointAuthMethod);
113
120
  // Mantém o secret atual se continua confidencial; se virou public, remove-o.
114
121
  const clientSecret = confidential ? (previousSecret ?? randomId()) : undefined;
115
122
  const payload = this.#buildPayload(clientId, merged, clientSecret);
123
+ if (isClientDisabled(existing))
124
+ payload[CLIENT_DISABLED_KEY] = true;
116
125
  await this.#adapter.upsert(clientId, payload, 0);
117
126
  await this.oidc.evictDynamicClientCache();
118
127
  }
128
+ /**
129
+ * Disables or re-enables a client. A disabled client gets `invalid_client` on every
130
+ * endpoint from the next request on (tokens already issued stay valid until they
131
+ * expire). Returns `false` when the client does not exist.
132
+ */
133
+ async setDisabled(clientId, disabled) {
134
+ const existing = await this.#adapter.find(clientId);
135
+ if (!existing)
136
+ return false;
137
+ const payload = { ...existing };
138
+ if (disabled)
139
+ payload[CLIENT_DISABLED_KEY] = true;
140
+ else
141
+ delete payload[CLIENT_DISABLED_KEY];
142
+ await this.#adapter.upsert(clientId, payload, 0);
143
+ await this.oidc.evictDynamicClientCache();
144
+ return true;
145
+ }
119
146
  /**
120
147
  * Regenera o client_secret de um client confidencial, preservando o resto da
121
148
  * metadata. Retorna o novo secret em claro (mostrado UMA vez). Lança se o
@@ -145,6 +172,8 @@ export class AdminClientsService {
145
172
  * `client_metadata.ts`). Lança `ClientMetadataError`.
146
173
  */
147
174
  #assertValid(input) {
175
+ if (input.serviceTenant)
176
+ this.#assertServiceTenant(input);
148
177
  assertClientMetadata({
149
178
  applicationType: input.applicationType ?? 'web',
150
179
  tokenEndpointAuthMethod: input.tokenEndpointAuthMethod,
@@ -153,6 +182,27 @@ export class AdminClientsService {
153
182
  postLogoutRedirectUris: input.postLogoutRedirectUris,
154
183
  });
155
184
  }
185
+ /** Rules of a service-tenant client (see {@link ClientInput.serviceTenant}). */
186
+ #assertServiceTenant(input) {
187
+ if (!this.oidc.config.serviceTenants?.enabled) {
188
+ throw new ClientMetadataError('service tenants are not enabled: declare serviceTenants.resources in config/authkit.ts');
189
+ }
190
+ if (PUBLIC_AUTH_METHODS.has(input.tokenEndpointAuthMethod)) {
191
+ throw new ClientMetadataError('a service tenant must be confidential (it has a secret)');
192
+ }
193
+ if ((input.applicationType ?? 'web') !== 'web') {
194
+ throw new ClientMetadataError('a service tenant must be a web (server) client');
195
+ }
196
+ if (input.grantTypes.length !== 1 || input.grantTypes[0] !== 'client_credentials') {
197
+ throw new ClientMetadataError('a service tenant can only use the client_credentials grant');
198
+ }
199
+ if (input.redirectUris.length || input.postLogoutRedirectUris.length) {
200
+ throw new ClientMetadataError('a service tenant has no redirect URIs');
201
+ }
202
+ if (input.backchannelLogoutUri) {
203
+ throw new ClientMetadataError('a service tenant has no back-channel logout URI');
204
+ }
205
+ }
156
206
  /**
157
207
  * Monta o payload na forma snake_case que o oidc-provider espera/persiste —
158
208
  * verificada contra o que o registro dinâmico (RFC 7591) grava. As chaves de
@@ -190,6 +240,12 @@ export class AdminClientsService {
190
240
  payload.backchannel_logout_session_required = input.backchannelLogoutSessionRequired;
191
241
  }
192
242
  }
243
+ if (input.serviceTenant) {
244
+ const tenant = normalizeServiceTenant(input.serviceTenant);
245
+ if (typeof tenant === 'string')
246
+ throw new ClientMetadataError(tenant);
247
+ payload[SERVICE_TENANT_KEY] = tenant;
248
+ }
193
249
  return payload;
194
250
  }
195
251
  /** Projeta um payload persistido para a forma exibida no console admin. */
@@ -206,6 +262,8 @@ export class AdminClientsService {
206
262
  applicationType: p.application_type === 'native' ? 'native' : 'web',
207
263
  clientName: typeof p.client_name === 'string' && p.client_name ? p.client_name : null,
208
264
  logoUri: typeof p.logo_uri === 'string' && p.logo_uri ? p.logo_uri : null,
265
+ disabled: isClientDisabled(p),
266
+ serviceTenant: serviceTenantOf(p),
209
267
  };
210
268
  if (p.backchannel_logout_uri) {
211
269
  result.backchannelLogoutUri = p.backchannel_logout_uri;
@@ -215,3 +273,9 @@ export class AdminClientsService {
215
273
  return result;
216
274
  }
217
275
  }
276
+ /** O `client_id` do client interno do console (ver `console_sso.ts`) não pode ser cadastrado. */
277
+ function assertNotReservedClientId(clientId) {
278
+ if (clientId === CONSOLE_SSO_CLIENT_ID) {
279
+ throw new ClientMetadataError(`client_id "${clientId}" é reservado pelo authkit`);
280
+ }
281
+ }
@@ -0,0 +1,14 @@
1
+ import type { HttpContext } from '@adonisjs/core/http';
2
+ import { type AgoraConsoleEntry, type AgoraConsoleRegistry, type AgoraConsoleSearchHit } from '../../console_switcher.js';
3
+ /** The admin console's id in the `'agora.consoles'` registry. */
4
+ export declare const ADMIN_CONSOLE_ID = "authkit";
5
+ /** The `'agora.consoles'` registry, or `null` (none bound, or no booted app). */
6
+ export declare function consoleRegistry(): Promise<AgoraConsoleRegistry | null>;
7
+ /**
8
+ * Global search of the admin console: users by email (as the account store's `listAccounts`
9
+ * search matches them), and organizations by name or slug.
10
+ * Each hit opens the matching drawer in the console.
11
+ */
12
+ export declare function searchAdminConsole(ctx: HttpContext, adminPrefix: string, query: string, limit: number): Promise<AgoraConsoleSearchHit[]>;
13
+ /** The admin console's registry entry, for the shell mounted at `adminPrefix`. */
14
+ export declare function adminConsoleEntry(adminPrefix: string, authorize: AgoraConsoleEntry['authorize']): AgoraConsoleEntry;
@@ -0,0 +1,80 @@
1
+ import { getBootedApp } from '../../../services/booted_app.js';
2
+ import { supportsOrganizations } from '../../accounts/account_store.js';
3
+ import { readAgoraConsoles, } from '../../console_switcher.js';
4
+ /** The admin console's id in the `'agora.consoles'` registry. */
5
+ export const ADMIN_CONSOLE_ID = 'authkit';
6
+ /** The `'agora.consoles'` registry, or `null` (none bound, or no booted app). */
7
+ export async function consoleRegistry() {
8
+ try {
9
+ return await readAgoraConsoles(getBootedApp());
10
+ }
11
+ catch {
12
+ return null;
13
+ }
14
+ }
15
+ const ORG_CACHE_MS = 30_000;
16
+ const orgCache = new WeakMap();
17
+ /** The store's organizations, cached for 30 s per store so a search session does one query. */
18
+ function cachedOrgs(store, load) {
19
+ const hit = orgCache.get(store);
20
+ if (hit && Date.now() - hit.at < ORG_CACHE_MS)
21
+ return hit.rows;
22
+ const rows = load();
23
+ orgCache.set(store, { at: Date.now(), rows });
24
+ rows.catch(() => orgCache.delete(store));
25
+ return rows;
26
+ }
27
+ /**
28
+ * Global search of the admin console: users by email (as the account store's `listAccounts`
29
+ * search matches them), and organizations by name or slug.
30
+ * Each hit opens the matching drawer in the console.
31
+ */
32
+ export async function searchAdminConsole(ctx, adminPrefix, query, limit) {
33
+ const service = await ctx.containerResolver.make('authkit.server');
34
+ const store = service.config.accountStore;
35
+ const hits = [];
36
+ const accounts = await store.listAccounts({ search: query, page: 1, size: limit });
37
+ for (const account of accounts.data.slice(0, limit)) {
38
+ hits.push({
39
+ title: account.email,
40
+ subtitle: account.name ? `User · ${account.name}` : 'User',
41
+ href: `${adminPrefix}?view=users&user=${encodeURIComponent(account.id)}`,
42
+ });
43
+ }
44
+ // Organizations only from a store that lists them in one query, for queries of 2+ characters,
45
+ // from a list cached briefly per process: search runs on every (debounced) keystroke.
46
+ if (hits.length < limit && query.length >= 2 && supportsOrganizations(store)) {
47
+ const listAll = store
48
+ .listAllOrgsWithMemberCount;
49
+ if (typeof listAll === 'function') {
50
+ const needle = query.toLowerCase();
51
+ const orgs = await cachedOrgs(store, () => listAll.call(store));
52
+ for (const org of orgs) {
53
+ if (hits.length >= limit)
54
+ break;
55
+ if (!org.name.toLowerCase().includes(needle) && !org.slug.toLowerCase().includes(needle)) {
56
+ continue;
57
+ }
58
+ hits.push({
59
+ title: org.name,
60
+ subtitle: `Organization · ${org.slug}`,
61
+ href: `${adminPrefix}?view=orgs&org=${encodeURIComponent(org.id)}`,
62
+ });
63
+ }
64
+ }
65
+ }
66
+ return hits;
67
+ }
68
+ /** The admin console's registry entry, for the shell mounted at `adminPrefix`. */
69
+ export function adminConsoleEntry(adminPrefix, authorize) {
70
+ return {
71
+ id: ADMIN_CONSOLE_ID,
72
+ title: 'Identity',
73
+ package: '@adonis-agora/authkit-server',
74
+ icon: 'identity',
75
+ path: adminPrefix,
76
+ order: 90,
77
+ authorize,
78
+ search: (ctx, query, limit) => searchAdminConsole(ctx, adminPrefix, query, limit),
79
+ };
80
+ }
@@ -1,8 +1,10 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import { extname } from 'node:path';
3
+ import { countOtherConsoles } from '../../console_switcher.js';
3
4
  import { accountPath } from '../account_paths.js';
4
5
  import { resolveAccountRoles, rolesManagedExternally } from '../account_roles.js';
5
6
  import { getAdminPrefix } from '../admin_prefix.js';
7
+ import { ADMIN_CONSOLE_ID, consoleRegistry } from './admin_console_entry.js';
6
8
  // ─── Shell HTML cache ─────────────────────────────────────────────────────────
7
9
  /**
8
10
  * Points to the Vite-built index.html inside build/host/ui-dist/.
@@ -141,6 +143,9 @@ export default class AdminShellController {
141
143
  }
142
144
  }
143
145
  const adminBase = getAdminPrefix();
146
+ // Console switcher: only announced when other `@adonis-agora` consoles are registered; the SPA
147
+ // then asks the endpoint which of them this viewer may open.
148
+ const others = countOtherConsoles(await consoleRegistry(), ADMIN_CONSOLE_ID);
144
149
  const authkitConfig = {
145
150
  adminBase,
146
151
  csrfToken: ctx.request.csrfToken ?? null,
@@ -152,6 +157,9 @@ export default class AdminShellController {
152
157
  // Confirmação de identidade (sudo) das ações destrutivas.
153
158
  confirm: accountPath('confirm'),
154
159
  },
160
+ ...(others > 0 ? { switcher: { endpoint: `${adminBase}/api/consoles`, others } } : {}),
161
+ // Global search (Ctrl/Cmd-K) across this console and every other one the viewer may open.
162
+ search: { endpoint: `${adminBase}/api/consoles/search` },
155
163
  features: {
156
164
  // A página de personal agents só aparece com `personalAgents` no config.
157
165
  personalAgents: !!cfg.personalAgents,
@@ -9,8 +9,11 @@ import type { HttpContext } from '@adonisjs/core/http';
9
9
  * PATCH {prefix}/api/clients/:id → atualizar metadata
10
10
  * DELETE {prefix}/api/clients/:id → remover client
11
11
  * POST {prefix}/api/clients/:id/regenerate-secret → gerar novo secret
12
+ * POST {prefix}/api/clients/:id/disable → stop issuing tokens to the client
13
+ * POST {prefix}/api/clients/:id/enable → issue tokens again
12
14
  */
13
15
  export default class ConsoleClientsController {
16
+ #private;
14
17
  /** GET {prefix}/api/clients */
15
18
  index(ctx: HttpContext): Promise<{
16
19
  data: {
@@ -25,6 +28,8 @@ export default class ConsoleClientsController {
25
28
  backchannelLogoutSessionRequired: boolean;
26
29
  clientName: string | null;
27
30
  logoUri: string | null;
31
+ disabled: boolean;
32
+ serviceTenant: import("../../service_tenants/client_metadata.ts").ServiceTenantInfo | null;
28
33
  }[];
29
34
  canList: boolean;
30
35
  }>;
@@ -41,6 +46,8 @@ export default class ConsoleClientsController {
41
46
  backchannelLogoutSessionRequired: boolean;
42
47
  clientName: string | null;
43
48
  logoUri: string | null;
49
+ disabled: boolean;
50
+ serviceTenant: import("../../service_tenants/client_metadata.ts").ServiceTenantInfo | null;
44
51
  }>;
45
52
  /** POST {prefix}/api/clients */
46
53
  store(ctx: HttpContext): Promise<void | {
@@ -60,12 +67,46 @@ export default class ConsoleClientsController {
60
67
  backchannelLogoutSessionRequired: boolean;
61
68
  clientName: string | null;
62
69
  logoUri: string | null;
70
+ disabled: boolean;
71
+ serviceTenant: import("../../service_tenants/client_metadata.ts").ServiceTenantInfo | null;
63
72
  }>;
64
73
  /** DELETE {prefix}/api/clients/:id */
65
74
  destroy(ctx: HttpContext): Promise<void | {
66
75
  ok: boolean;
67
76
  deleted: string;
68
77
  }>;
78
+ /** POST {prefix}/api/clients/:id/disable */
79
+ disable(ctx: HttpContext): Promise<void | {
80
+ clientId: string;
81
+ confidential: boolean;
82
+ grantTypes: string[];
83
+ redirectUris: string[];
84
+ postLogoutRedirectUris: string[];
85
+ tokenEndpointAuthMethod: string;
86
+ applicationType: import("../client_metadata.js").ApplicationType;
87
+ backchannelLogoutUri: string | null;
88
+ backchannelLogoutSessionRequired: boolean;
89
+ clientName: string | null;
90
+ logoUri: string | null;
91
+ disabled: boolean;
92
+ serviceTenant: import("../../service_tenants/client_metadata.ts").ServiceTenantInfo | null;
93
+ }>;
94
+ /** POST {prefix}/api/clients/:id/enable */
95
+ enable(ctx: HttpContext): Promise<void | {
96
+ clientId: string;
97
+ confidential: boolean;
98
+ grantTypes: string[];
99
+ redirectUris: string[];
100
+ postLogoutRedirectUris: string[];
101
+ tokenEndpointAuthMethod: string;
102
+ applicationType: import("../client_metadata.js").ApplicationType;
103
+ backchannelLogoutUri: string | null;
104
+ backchannelLogoutSessionRequired: boolean;
105
+ clientName: string | null;
106
+ logoUri: string | null;
107
+ disabled: boolean;
108
+ serviceTenant: import("../../service_tenants/client_metadata.ts").ServiceTenantInfo | null;
109
+ }>;
69
110
  /** POST {prefix}/api/clients/:id/regenerate-secret */
70
111
  regenerateSecret(ctx: HttpContext): Promise<void | {
71
112
  clientId: string;
@@ -19,6 +19,8 @@ function invalidMetadata(ctx, err) {
19
19
  * PATCH {prefix}/api/clients/:id → atualizar metadata
20
20
  * DELETE {prefix}/api/clients/:id → remover client
21
21
  * POST {prefix}/api/clients/:id/regenerate-secret → gerar novo secret
22
+ * POST {prefix}/api/clients/:id/disable → stop issuing tokens to the client
23
+ * POST {prefix}/api/clients/:id/enable → issue tokens again
22
24
  */
23
25
  export default class ConsoleClientsController {
24
26
  /** GET {prefix}/api/clients */
@@ -107,6 +109,34 @@ export default class ConsoleClientsController {
107
109
  });
108
110
  return { ok: true, deleted: id };
109
111
  }
112
+ /** POST {prefix}/api/clients/:id/disable */
113
+ async disable(ctx) {
114
+ return this.#setDisabled(ctx, true);
115
+ }
116
+ /** POST {prefix}/api/clients/:id/enable */
117
+ async enable(ctx) {
118
+ return this.#setDisabled(ctx, false);
119
+ }
120
+ /** Disabled clients get `invalid_client` from the next request on. */
121
+ async #setDisabled(ctx, disabled) {
122
+ const service = await ctx.containerResolver.make('authkit.server');
123
+ const svc = new AdminClientsService(service);
124
+ const id = ctx.request.param('id');
125
+ if (!(await svc.setDisabled(id, disabled))) {
126
+ return ctx.response.notFound(apiError('not_found', 'Client não encontrado.'));
127
+ }
128
+ const client = await svc.find(id);
129
+ await service.config.audit?.record({
130
+ type: disabled ? 'client.disabled' : 'client.enabled',
131
+ clientId: id,
132
+ ip: ctx.request.ip?.() ?? null,
133
+ metadata: {
134
+ actor: 'admin-console',
135
+ ...(client?.serviceTenant ? { partition: client.serviceTenant.partition } : {}),
136
+ },
137
+ });
138
+ return clientDto(client);
139
+ }
110
140
  /** POST {prefix}/api/clients/:id/regenerate-secret */
111
141
  async regenerateSecret(ctx) {
112
142
  const service = await ctx.containerResolver.make('authkit.server');
@@ -129,7 +159,10 @@ export default class ConsoleClientsController {
129
159
  type: 'client.secret_regenerated',
130
160
  clientId: id,
131
161
  ip: ctx.request.ip?.() ?? null,
132
- metadata: { actor: 'admin-console' },
162
+ metadata: {
163
+ actor: 'admin-console',
164
+ ...(existing.serviceTenant ? { partition: existing.serviceTenant.partition } : {}),
165
+ },
133
166
  });
134
167
  return { clientId: id, clientSecret: newSecret };
135
168
  }
@@ -22,7 +22,7 @@ interface AgentEntry {
22
22
  connections: number;
23
23
  }
24
24
  /**
25
- * Endpoints JSON de personal agents (PACT e Poppy) do console admin React.
25
+ * Endpoints JSON de personal agents (PACT e `personalAgents.extensions`) do console admin React.
26
26
  *
27
27
  * GET {prefix}/api/personal-agents → agentes (config + console + conectados)
28
28
  * GET {prefix}/api/personal-agents/connections?clientId=&accountId=&protocol=&page=&size=
@@ -41,11 +41,6 @@ export default class ConsolePersonalAgentsController {
41
41
  /** A lista do config é uma função do host (o console não enxerga). */
42
42
  hostResolver: boolean;
43
43
  } | null;
44
- poppy: {
45
- requireRegistration: boolean;
46
- hostResolver: boolean;
47
- allowInsecureClientIds: boolean;
48
- } | null;
49
44
  };
50
45
  agents: AgentEntry[];
51
46
  }>;
@@ -31,10 +31,21 @@ function enabledProtocols(runtime) {
31
31
  const out = [];
32
32
  if (runtime.config.pact)
33
33
  out.push('pact');
34
- if (runtime.config.poppy)
35
- out.push('poppy');
34
+ out.push(...runtime.extensions.keys());
36
35
  return out;
37
36
  }
37
+ /** O protocolo dono de um grant, pelo prefixo do id. */
38
+ function protocolOfGrant(runtime, grantId) {
39
+ for (const [id, { extension }] of runtime.extensions) {
40
+ if (grantId.startsWith(extension.grantIdPrefix))
41
+ return id;
42
+ }
43
+ return 'pact';
44
+ }
45
+ /** Prefixo de id dos grants de um protocolo (`agrant_` = PACT). */
46
+ function grantPrefixOf(runtime, protocol) {
47
+ return runtime.extensions.get(protocol)?.extension.grantIdPrefix ?? 'agrant_';
48
+ }
38
49
  function isUrl(value, allowHttp) {
39
50
  if (typeof value !== 'string' || value.length > 2048)
40
51
  return false;
@@ -52,14 +63,14 @@ function isUrl(value, allowHttp) {
52
63
  async function agentName(runtime, protocol, identifier, fallback) {
53
64
  if (fallback)
54
65
  return fallback;
55
- if (protocol === 'poppy' && runtime.poppy) {
56
- return runtime.poppy.registry.displayName(identifier);
57
- }
66
+ const extension = runtime.extensions.get(protocol);
67
+ if (extension)
68
+ return extension.runtime.displayName(identifier);
58
69
  const registration = await runtime.config.resolveAgent(identifier);
59
70
  return displayNameFor(registration ?? { issuer: identifier, jwksUri: '' });
60
71
  }
61
72
  /**
62
- * Endpoints JSON de personal agents (PACT e Poppy) do console admin React.
73
+ * Endpoints JSON de personal agents (PACT e `personalAgents.extensions`) do console admin React.
63
74
  *
64
75
  * GET {prefix}/api/personal-agents → agentes (config + console + conectados)
65
76
  * GET {prefix}/api/personal-agents/connections?clientId=&accountId=&protocol=&page=&size=
@@ -105,17 +116,20 @@ export default class ConsolePersonalAgentsController {
105
116
  e.jwksUri = agent.jwksUri;
106
117
  names.set(keyOf('pact', agent.issuer), agent.name ?? null);
107
118
  }
108
- for (const [clientId, count] of await runtime.grants.countLiveGrantsByClient(now, 'pact')) {
119
+ for (const [clientId, count] of await runtime.grants.countLiveGrantsByClient(now, 'agrant_')) {
109
120
  entry('pact', clientId).connections = count;
110
121
  }
111
122
  }
112
- if (cfg.poppy) {
113
- for (const id of cfg.poppy.clients.registeredList ?? [])
114
- entry('poppy', id).config = 'allowed';
115
- for (const id of cfg.poppy.clients.blockedList ?? [])
116
- entry('poppy', id).config = 'blocked';
117
- for (const [clientId, count] of await runtime.grants.countLiveGrantsByClient(now, 'poppy')) {
118
- entry('poppy', clientId).connections = count;
123
+ const summaries = {};
124
+ for (const [protocol, { extension, runtime: ext }] of runtime.extensions) {
125
+ const summary = ext.consoleSummary();
126
+ summaries[protocol] = { label: extension.label, ...summary };
127
+ for (const id of summary.configured.allowed)
128
+ entry(protocol, id).config = 'allowed';
129
+ for (const id of summary.configured.blocked)
130
+ entry(protocol, id).config = 'blocked';
131
+ for (const [clientId, count] of await runtime.grants.countLiveGrantsByClient(now, extension.grantIdPrefix)) {
132
+ entry(protocol, clientId).connections = count;
119
133
  }
120
134
  }
121
135
  for (const row of await runtime.registry.list()) {
@@ -153,13 +167,17 @@ export default class ConsolePersonalAgentsController {
153
167
  hostResolver: cfg.staticAgents === null,
154
168
  }
155
169
  : null,
156
- poppy: cfg.poppy
157
- ? {
158
- requireRegistration: cfg.poppy.clients.requireRegistration,
159
- hostResolver: cfg.poppy.clients.registeredList === null || cfg.poppy.clients.blockedList === null,
160
- allowInsecureClientIds: cfg.poppy.allowInsecureClientIds,
161
- }
162
- : null,
170
+ ...Object.fromEntries(Object.entries(summaries).map(([protocol, summary]) => [
171
+ protocol,
172
+ {
173
+ label: summary.label,
174
+ requireRegistration: summary.requireRegistration,
175
+ hostResolver: summary.hostResolver,
176
+ allowHttpIdentifiers: summary.allowHttpIdentifiers,
177
+ identifierLabel: summary.identifierLabel,
178
+ identifierPlaceholder: summary.identifierPlaceholder ?? null,
179
+ },
180
+ ])),
163
181
  },
164
182
  agents,
165
183
  };
@@ -172,11 +190,9 @@ export default class ConsolePersonalAgentsController {
172
190
  const qs = ctx.request.qs();
173
191
  const page = parseListPage(qs.page);
174
192
  const size = parseListSize(qs.size);
175
- const protocol = qs.protocol === 'pact' || qs.protocol === 'poppy'
176
- ? qs.protocol
177
- : undefined;
193
+ const protocol = qs.protocol && enabledProtocols(runtime).includes(qs.protocol) ? qs.protocol : undefined;
178
194
  const filter = {
179
- protocol,
195
+ idPrefix: protocol ? grantPrefixOf(runtime, protocol) : undefined,
180
196
  clientId: qs.clientId?.trim() || undefined,
181
197
  accountId: qs.accountId?.trim() || undefined,
182
198
  };
@@ -189,7 +205,7 @@ export default class ConsolePersonalAgentsController {
189
205
  const emails = new Map();
190
206
  const nameCache = new Map();
191
207
  const data = await Promise.all(rows.map(async (g) => {
192
- const rowProtocol = g.id.startsWith('pgrant_') ? 'poppy' : 'pact';
208
+ const rowProtocol = protocolOfGrant(runtime, g.id);
193
209
  if (!emails.has(g.accountId)) {
194
210
  const account = await store.findById(g.accountId).catch(() => null);
195
211
  emails.set(g.accountId, account?.email ?? null);
@@ -248,17 +264,21 @@ export default class ConsolePersonalAgentsController {
248
264
  const invalid = (message) => ctx.response.unprocessableEntity(apiError('invalid_request', message));
249
265
  const protocol = body.protocol;
250
266
  if (!enabledProtocols(runtime).includes(protocol)) {
251
- return invalid('`protocol` precisa ser um protocolo ligado (`pact` ou `poppy`).');
267
+ return invalid(`\`protocol\` precisa ser um protocolo ligado (${enabledProtocols(runtime)
268
+ .map((p) => `\`${p}\``)
269
+ .join(', ')}).`);
252
270
  }
253
271
  const status = body.status;
254
272
  if (status !== 'allowed' && status !== 'blocked') {
255
273
  return invalid('`status` precisa ser `allowed` ou `blocked`.');
256
274
  }
257
- const allowHttp = protocol === 'poppy' && !!runtime.config.poppy?.allowInsecureClientIds;
275
+ const extension = runtime.extensions.get(protocol);
276
+ const summary = extension?.runtime.consoleSummary();
277
+ const allowHttp = !!summary?.allowHttpIdentifiers;
258
278
  const identifier = typeof body.identifier === 'string' ? body.identifier.trim() : '';
259
279
  if (!isUrl(identifier, allowHttp)) {
260
- return invalid(protocol === 'poppy'
261
- ? '`identifier` precisa ser o `client_id` do agente (URL HTTPS do documento de metadata).'
280
+ return invalid(summary
281
+ ? `\`identifier\` precisa ser uma URL ${allowHttp ? 'http(s)' : 'HTTPS'}: ${summary.identifierLabel}.`
262
282
  : '`identifier` precisa ser o `iss` do agente (URL HTTPS).');
263
283
  }
264
284
  const name = typeof body.name === 'string' && body.name.trim() ? body.name.trim().slice(0, 200) : null;
@@ -0,0 +1,11 @@
1
+ import type { HttpContext } from '@adonisjs/core/http';
2
+ /**
3
+ * The console switcher endpoints of the admin console (behind the admin console's own guard).
4
+ *
5
+ * GET {prefix}/api/consoles → the other consoles this viewer may open
6
+ * GET {prefix}/api/consoles/search?q= → hits from every console this viewer may open
7
+ */
8
+ export default class ConsoleSwitcherController {
9
+ index(ctx: HttpContext): Promise<import("../../console_switcher.js").VisibleConsoles>;
10
+ search(ctx: HttpContext): Promise<import("../../console_switcher.js").ConsoleSearchResults>;
11
+ }
@@ -0,0 +1,23 @@
1
+ import { listVisibleConsoles, searchConsoles } from '../../console_switcher.js';
2
+ import { ADMIN_CONSOLE_ID, consoleRegistry } from './admin_console_entry.js';
3
+ /**
4
+ * The console switcher endpoints of the admin console (behind the admin console's own guard).
5
+ *
6
+ * GET {prefix}/api/consoles → the other consoles this viewer may open
7
+ * GET {prefix}/api/consoles/search?q= → hits from every console this viewer may open
8
+ */
9
+ export default class ConsoleSwitcherController {
10
+ async index(ctx) {
11
+ const body = await listVisibleConsoles(await consoleRegistry(), ADMIN_CONSOLE_ID, ctx, {
12
+ logger: ctx.logger,
13
+ });
14
+ ctx.response.header('Cache-Control', 'private, no-store');
15
+ return body;
16
+ }
17
+ async search(ctx) {
18
+ const q = ctx.request.input('q', '');
19
+ const body = await searchConsoles(await consoleRegistry(), ADMIN_CONSOLE_ID, ctx, typeof q === 'string' ? q : '', { logger: ctx.logger });
20
+ ctx.response.header('Cache-Control', 'private, no-store');
21
+ return body;
22
+ }
23
+ }
@@ -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
+ }
@@ -87,7 +87,7 @@ export class AdminSessionsService {
87
87
  /**
88
88
  * Revogação total também corta os personal agents: "sair de todas as sessões",
89
89
  * reset de senha, a ação do admin e a exclusão de conta não podem deixar um
90
- * agente agindo na conta (delegações do PACT e refresh tokens do Poppy). Sem
90
+ * agente agindo na conta (delegações do PACT e conexões das extensions). Sem
91
91
  * `personalAgents` é no-op — e aí nem resolve o banco. Diferente do
92
92
  * `recordSubRevocation`, uma falha AQUI propaga: não há outra camada que corte
93
93
  * esses tokens.
@@ -142,11 +142,14 @@ export class AdminSessionsService {
142
142
  return [];
143
143
  return (await adapter.list()).filter((r) => grantIds.has(r.payload.grantId));
144
144
  }
145
- /** Grants da conta, exceto os espelhos `pgrant_` de refresh tokens do Poppy. */
145
+ /**
146
+ * Grants da conta, exceto os espelhos dos grants das `personalAgents.extensions` (os access
147
+ * tokens de MCP que elas emitem pelo oidc-provider usam o id do grant delas) — aparecem e são
148
+ * revogados como personal agent, não como app OIDC.
149
+ */
146
150
  async #accountGrants(accountId) {
147
- // Os grants `pgrant_` são o espelho de um refresh token do Poppy (Bearer de MCP) — aparecem e
148
- // são revogados como personal agent, não como app OIDC.
149
- return (await this.#listModelOfAccount('Grant', accountId)).filter((r) => !r.id.startsWith('pgrant_'));
151
+ const prefixes = (this.#oidc.config.personalAgents?.extensions ?? []).map((e) => e.grantIdPrefix);
152
+ return (await this.#listModelOfAccount('Grant', accountId)).filter((r) => !prefixes.some((prefix) => r.id.startsWith(prefix)));
150
153
  }
151
154
  /**
152
155
  * Conta TODAS as sessões ativas do IdP (todas as contas). 0 quando o adapter não