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

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 (98) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +4 -5
  3. package/build/commands/clients_create.d.ts +4 -0
  4. package/build/commands/clients_create.js +97 -0
  5. package/build/commands/commands.json +14 -0
  6. package/build/index.d.ts +8 -11
  7. package/build/index.js +5 -9
  8. package/build/providers/authkit_server_provider.js +6 -8
  9. package/build/src/agents/config.d.ts +9 -13
  10. package/build/src/agents/config.js +17 -11
  11. package/build/src/agents/delegation_service.js +3 -3
  12. package/build/src/agents/delegation_store.d.ts +3 -2
  13. package/build/src/agents/delegation_store.js +6 -5
  14. package/build/src/agents/extension.d.ts +261 -0
  15. package/build/src/agents/extension.js +40 -0
  16. package/build/src/agents/middleware.js +1 -1
  17. package/build/src/agents/protocol.d.ts +4 -4
  18. package/build/src/agents/registry_store.d.ts +3 -2
  19. package/build/src/agents/registry_store.js +1 -1
  20. package/build/src/agents/runtime.d.ts +26 -7
  21. package/build/src/agents/runtime.js +110 -75
  22. package/build/src/audit/audit_sink.d.ts +1 -1
  23. package/build/src/audit/audit_sink.js +6 -1
  24. package/build/src/define_config.d.ts +16 -12
  25. package/build/src/define_config.js +11 -7
  26. package/build/src/host/access_token_verifier.js +7 -0
  27. package/build/src/host/account_api/account_api_controller.js +8 -6
  28. package/build/src/host/account_session_sign_in.d.ts +12 -0
  29. package/build/src/host/account_session_sign_in.js +19 -0
  30. package/build/src/host/admin_api/api_clients_controller.d.ts +37 -0
  31. package/build/src/host/admin_api/api_clients_controller.js +31 -1
  32. package/build/src/host/admin_api/dto.d.ts +3 -1
  33. package/build/src/host/admin_api/dto.js +2 -0
  34. package/build/src/host/admin_clients_service.d.ts +20 -0
  35. package/build/src/host/admin_clients_service.js +55 -0
  36. package/build/src/host/admin_console/console_clients_controller.d.ts +41 -0
  37. package/build/src/host/admin_console/console_clients_controller.js +34 -1
  38. package/build/src/host/admin_console/console_personal_agents_controller.d.ts +1 -6
  39. package/build/src/host/admin_console/console_personal_agents_controller.js +50 -30
  40. package/build/src/host/admin_sessions_service.js +8 -5
  41. package/build/src/host/admin_validators.d.ts +29 -0
  42. package/build/src/host/admin_validators.js +30 -0
  43. package/build/src/host/auth_host_config.d.ts +6 -2
  44. package/build/src/host/controllers/account_apps_controller.js +15 -11
  45. package/build/src/host/csrf.d.ts +3 -3
  46. package/build/src/host/csrf.js +3 -13
  47. package/build/src/host/default_mailer.d.ts +11 -6
  48. package/build/src/host/default_mailer.js +16 -12
  49. package/build/src/host/i18n.d.ts +3 -14
  50. package/build/src/host/i18n.js +15 -13
  51. package/build/src/host/register_auth_host.js +30 -61
  52. package/build/src/host/ui-dist/assets/index-u0bMxkLg.js +67 -0
  53. package/build/src/host/ui-dist/index.html +1 -1
  54. package/build/src/provider/build_provider.js +37 -6
  55. package/build/src/provider/oidc_service.d.ts +1 -1
  56. package/build/src/provider/oidc_service.js +1 -1
  57. package/build/src/schema/ensure.d.ts +14 -0
  58. package/build/src/schema/ensure.js +43 -150
  59. package/build/src/service_tenants/client_metadata.d.ts +39 -0
  60. package/build/src/service_tenants/client_metadata.js +80 -0
  61. package/build/src/service_tenants/config.d.ts +44 -0
  62. package/build/src/service_tenants/config.js +44 -0
  63. package/build/src/service_tenants/provider_hooks.d.ts +52 -0
  64. package/build/src/service_tenants/provider_hooks.js +124 -0
  65. package/build/src/service_tenants/tenant_verifier.d.ts +73 -0
  66. package/build/src/service_tenants/tenant_verifier.js +104 -0
  67. package/package.json +2 -2
  68. package/build/src/agents/poppy/client_registry.d.ts +0 -50
  69. package/build/src/agents/poppy/client_registry.js +0 -260
  70. package/build/src/agents/poppy/config.d.ts +0 -396
  71. package/build/src/agents/poppy/config.js +0 -228
  72. package/build/src/agents/poppy/dpop.d.ts +0 -61
  73. package/build/src/agents/poppy/dpop.js +0 -158
  74. package/build/src/agents/poppy/email_code_sign_in.d.ts +0 -27
  75. package/build/src/agents/poppy/email_code_sign_in.js +0 -140
  76. package/build/src/agents/poppy/errors.d.ts +0 -25
  77. package/build/src/agents/poppy/errors.js +0 -25
  78. package/build/src/agents/poppy/replay.d.ts +0 -25
  79. package/build/src/agents/poppy/replay.js +0 -106
  80. package/build/src/agents/poppy/resource.d.ts +0 -100
  81. package/build/src/agents/poppy/resource.js +0 -129
  82. package/build/src/agents/poppy/safe_fetch.d.ts +0 -48
  83. package/build/src/agents/poppy/safe_fetch.js +0 -262
  84. package/build/src/agents/poppy/service.d.ts +0 -293
  85. package/build/src/agents/poppy/service.js +0 -1458
  86. package/build/src/agents/poppy/session_jwt.d.ts +0 -30
  87. package/build/src/agents/poppy/session_jwt.js +0 -133
  88. package/build/src/agents/poppy/store.d.ts +0 -106
  89. package/build/src/agents/poppy/store.js +0 -261
  90. package/build/src/agents/poppy/web_session.d.ts +0 -39
  91. package/build/src/agents/poppy/web_session.js +0 -122
  92. package/build/src/host/controllers/poppy_consent_controller.d.ts +0 -19
  93. package/build/src/host/controllers/poppy_consent_controller.js +0 -199
  94. package/build/src/host/controllers/poppy_controller.d.ts +0 -33
  95. package/build/src/host/controllers/poppy_controller.js +0 -219
  96. package/build/src/host/poppy_session_key.d.ts +0 -9
  97. package/build/src/host/poppy_session_key.js +0 -9
  98. package/build/src/host/ui-dist/assets/index-D0EIeyKK.js +0 -67
@@ -40,6 +40,8 @@ export declare function clientDto(client: AdminClient): {
40
40
  backchannelLogoutSessionRequired: boolean;
41
41
  clientName: string | null;
42
42
  logoUri: string | null;
43
+ disabled: boolean;
44
+ serviceTenant: import("../../service_tenants/client_metadata.ts").ServiceTenantInfo | null;
43
45
  };
44
46
  /** Inclui o secret (mostrado UMA vez) em create/regenerate. */
45
47
  export declare function createdClientDto(created: CreatedClient): {
@@ -67,7 +69,7 @@ export declare function grantDto(grant: AdminGrant): {
67
69
  };
68
70
  export declare function auditDto(event: StoredAuditEvent): {
69
71
  id: string;
70
- type: "account.deleted" | "account.expiration_warned" | "account.expired_login_blocked" | "account.exported" | "account.locked" | "account.signed_out_all" | "agent.allowed" | "agent.blocked" | "agent.delegation_approved" | "agent.delegation_denied" | "agent.grant_revoked" | "agent.grant_revoked_by_admin" | "agent.registry_removed" | "bot_protection.rejected" | "client.created" | "client.deleted" | "client.secret_regenerated" | "client.updated" | "email.change_requested" | "email.changed" | "email_change.cancelled" | "email_change.confirmed" | "email_change.requested" | "email_verification.consumed" | "email_verification.issued" | "grant.revoked_by_user" | "impersonation" | "impersonation.panel_viewed" | "impersonation.stopped" | "keys.rotated" | "login.failure" | "login.magic_link_sent" | "login.new_device" | "login.new_ip_notified" | "login.otp_failed" | "login.otp_invalidated" | "login.otp_sent" | "login.otp_verified" | "login.success" | "maintenance.disabled" | "maintenance.enabled" | "mfa.disabled" | "mfa.enabled" | "mfa.recovery_codes_regenerated" | "organization.created" | "organization.deactivated" | "organization.deleted" | "organization.invitation_accepted" | "organization.invitation_revoked" | "organization.invitation_sent" | "organization.member_added" | "organization.member_removed" | "organization.member_role_changed" | "organization.member_role_updated" | "organization.switched" | "organization.updated" | "otp.locked" | "otp.unlock_failed" | "otp.unlocked" | "passkey.registered" | "passkey.removed" | "password.changed" | "password.expired_change_forced" | "password.rehashed" | "password_reset.consumed" | "password_reset.issued" | "pat.issued" | "pat.revoked" | "pat.used" | "profile.updated" | "roles_catalog.updated" | "security_notice.sent" | "session.revoked" | "session.revoked_all" | "session.single_enforced" | "settings.updated" | "signup" | "sudo.confirmed" | "trusted_device.revoked" | "user.created" | "user.deleted" | "user.disabled" | "user.enabled" | "user.password_reset_sent";
72
+ type: "account.deleted" | "account.expiration_warned" | "account.expired_login_blocked" | "account.exported" | "account.locked" | "account.signed_out_all" | "agent.allowed" | "agent.blocked" | "agent.delegation_approved" | "agent.delegation_denied" | "agent.grant_revoked" | "agent.grant_revoked_by_admin" | "agent.registry_removed" | "bot_protection.rejected" | "client.created" | "client.deleted" | "client.disabled" | "client.enabled" | "client.secret_regenerated" | "client.updated" | "email.change_requested" | "email.changed" | "email_change.cancelled" | "email_change.confirmed" | "email_change.requested" | "email_verification.consumed" | "email_verification.issued" | "grant.revoked_by_user" | "impersonation" | "impersonation.panel_viewed" | "impersonation.stopped" | "keys.rotated" | "login.failure" | "login.magic_link_sent" | "login.new_device" | "login.new_ip_notified" | "login.otp_failed" | "login.otp_invalidated" | "login.otp_sent" | "login.otp_verified" | "login.success" | "maintenance.disabled" | "maintenance.enabled" | "mfa.disabled" | "mfa.enabled" | "mfa.recovery_codes_regenerated" | "organization.created" | "organization.deactivated" | "organization.deleted" | "organization.invitation_accepted" | "organization.invitation_revoked" | "organization.invitation_sent" | "organization.member_added" | "organization.member_removed" | "organization.member_role_changed" | "organization.member_role_updated" | "organization.switched" | "organization.updated" | "otp.locked" | "otp.unlock_failed" | "otp.unlocked" | "passkey.registered" | "passkey.removed" | "password.changed" | "password.expired_change_forced" | "password.rehashed" | "password_reset.consumed" | "password_reset.issued" | "pat.issued" | "pat.revoked" | "pat.used" | "profile.updated" | "roles_catalog.updated" | "security_notice.sent" | "service_tenant.token_issued" | "session.revoked" | "session.revoked_all" | "session.single_enforced" | "settings.updated" | "signup" | "sudo.confirmed" | "trusted_device.revoked" | "user.created" | "user.deleted" | "user.disabled" | "user.enabled" | "user.password_reset_sent";
71
73
  accountId: string | null;
72
74
  email: string | null;
73
75
  clientId: string | null;
@@ -32,6 +32,8 @@ export function clientDto(client) {
32
32
  backchannelLogoutSessionRequired: client.backchannelLogoutSessionRequired ?? false,
33
33
  clientName: client.clientName,
34
34
  logoUri: client.logoUri,
35
+ disabled: client.disabled,
36
+ serviceTenant: client.serviceTenant,
35
37
  };
36
38
  }
37
39
  /** Inclui o secret (mostrado UMA vez) em create/regenerate. */
@@ -1,4 +1,5 @@
1
1
  import type { OidcService } from '../provider/oidc_service.js';
2
+ import { type ServiceTenantInfo } from '../service_tenants/client_metadata.js';
2
3
  import { type ApplicationType } from './client_metadata.js';
3
4
  /** Métodos de autenticação no token endpoint suportados pelo formulário admin. */
4
5
  export type TokenEndpointAuthMethod = 'client_secret_basic' | 'client_secret_post' | 'none';
@@ -30,6 +31,15 @@ export interface ClientInput {
30
31
  * `backchannelLogoutUri` não está definido.
31
32
  */
32
33
  backchannelLogoutSessionRequired?: boolean;
34
+ /**
35
+ * Makes the client a service tenant: confidential, `client_credentials` only, no
36
+ * redirect URIs; its tokens carry `partition` and `capabilities`. On update, `null`
37
+ * turns it back into a regular client; absent keeps the current value.
38
+ */
39
+ serviceTenant?: {
40
+ partition: string;
41
+ capabilities?: string[];
42
+ } | null;
33
43
  }
34
44
  /** Client persistido, apresentado ao console admin. */
35
45
  export interface AdminClient {
@@ -49,6 +59,10 @@ export interface AdminClient {
49
59
  clientName: string | null;
50
60
  /** `logo_uri` registrado, ou `null`. */
51
61
  logoUri: string | null;
62
+ /** Disabled clients get no tokens (`invalid_client`) until enabled again. */
63
+ disabled: boolean;
64
+ /** Service-tenant settings, or `null` for a regular client. */
65
+ serviceTenant: ServiceTenantInfo | null;
52
66
  }
53
67
  /** Resultado de uma criação: o client + o secret em claro (mostrado UMA vez). */
54
68
  export interface CreatedClient {
@@ -94,6 +108,12 @@ export declare class AdminClientsService {
94
108
  * PRESERVANDO o client_secret existente. Lança se o client não existe.
95
109
  */
96
110
  update(clientId: string, input: Partial<ClientInput>): Promise<void>;
111
+ /**
112
+ * Disables or re-enables a client. A disabled client gets `invalid_client` on every
113
+ * endpoint from the next request on (tokens already issued stay valid until they
114
+ * expire). Returns `false` when the client does not exist.
115
+ */
116
+ setDisabled(clientId: string, disabled: boolean): Promise<boolean>;
97
117
  /**
98
118
  * Regenera o client_secret de um client confidencial, preservando o resto da
99
119
  * metadata. Retorna o novo secret em claro (mostrado UMA vez). Lança se o
@@ -1,4 +1,5 @@
1
1
  import { randomBytes } from 'node:crypto';
2
+ import { CLIENT_DISABLED_KEY, isClientDisabled, normalizeServiceTenant, SERVICE_TENANT_KEY, serviceTenantOf, } from '../service_tenants/client_metadata.js';
2
3
  import { assertClientMetadata, ClientMetadataError, } from './client_metadata.js';
3
4
  import { CONSOLE_SSO_CLIENT_ID } from './console_sso.js';
4
5
  /** Métodos públicos (sem segredo) — espelham os "non-secret" do oidc-provider. */
@@ -111,14 +112,37 @@ export class AdminClientsService {
111
112
  logoUri: input.logoUri === null
112
113
  ? undefined
113
114
  : (input.logoUri ?? existing.logo_uri),
115
+ serviceTenant: input.serviceTenant === undefined
116
+ ? serviceTenantOf(existing)
117
+ : input.serviceTenant,
114
118
  };
115
119
  const confidential = !PUBLIC_AUTH_METHODS.has(merged.tokenEndpointAuthMethod);
116
120
  // Mantém o secret atual se continua confidencial; se virou public, remove-o.
117
121
  const clientSecret = confidential ? (previousSecret ?? randomId()) : undefined;
118
122
  const payload = this.#buildPayload(clientId, merged, clientSecret);
123
+ if (isClientDisabled(existing))
124
+ payload[CLIENT_DISABLED_KEY] = true;
119
125
  await this.#adapter.upsert(clientId, payload, 0);
120
126
  await this.oidc.evictDynamicClientCache();
121
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
+ }
122
146
  /**
123
147
  * Regenera o client_secret de um client confidencial, preservando o resto da
124
148
  * metadata. Retorna o novo secret em claro (mostrado UMA vez). Lança se o
@@ -148,6 +172,8 @@ export class AdminClientsService {
148
172
  * `client_metadata.ts`). Lança `ClientMetadataError`.
149
173
  */
150
174
  #assertValid(input) {
175
+ if (input.serviceTenant)
176
+ this.#assertServiceTenant(input);
151
177
  assertClientMetadata({
152
178
  applicationType: input.applicationType ?? 'web',
153
179
  tokenEndpointAuthMethod: input.tokenEndpointAuthMethod,
@@ -156,6 +182,27 @@ export class AdminClientsService {
156
182
  postLogoutRedirectUris: input.postLogoutRedirectUris,
157
183
  });
158
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
+ }
159
206
  /**
160
207
  * Monta o payload na forma snake_case que o oidc-provider espera/persiste —
161
208
  * verificada contra o que o registro dinâmico (RFC 7591) grava. As chaves de
@@ -193,6 +240,12 @@ export class AdminClientsService {
193
240
  payload.backchannel_logout_session_required = input.backchannelLogoutSessionRequired;
194
241
  }
195
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
+ }
196
249
  return payload;
197
250
  }
198
251
  /** Projeta um payload persistido para a forma exibida no console admin. */
@@ -209,6 +262,8 @@ export class AdminClientsService {
209
262
  applicationType: p.application_type === 'native' ? 'native' : 'web',
210
263
  clientName: typeof p.client_name === 'string' && p.client_name ? p.client_name : null,
211
264
  logoUri: typeof p.logo_uri === 'string' && p.logo_uri ? p.logo_uri : null,
265
+ disabled: isClientDisabled(p),
266
+ serviceTenant: serviceTenantOf(p),
212
267
  };
213
268
  if (p.backchannel_logout_uri) {
214
269
  result.backchannelLogoutUri = p.backchannel_logout_uri;
@@ -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;
@@ -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
@@ -17,6 +17,19 @@ export declare const clientInputValidator: import("@vinejs/vine").VineValidator<
17
17
  backchannelLogoutSessionRequired: import("@vinejs/vine/schema/base/literal").OptionalModifier<import("@vinejs/vine").VineBoolean>;
18
18
  clientName: import("@vinejs/vine/schema/base/literal").OptionalModifier<import("@vinejs/vine/schema/base/literal").NullableModifier<import("@vinejs/vine").VineString>>;
19
19
  logoUri: import("@vinejs/vine/schema/base/literal").OptionalModifier<import("@vinejs/vine/schema/base/literal").NullableModifier<import("@vinejs/vine").VineString>>;
20
+ serviceTenant: import("@vinejs/vine").OptionalModifier<import("@vinejs/vine").NullableModifier<import("@vinejs/vine").VineObject<{
21
+ partition: import("@vinejs/vine").VineString;
22
+ capabilities: import("@vinejs/vine").OptionalModifier<import("@vinejs/vine").VineArray<import("@vinejs/vine").VineString>>;
23
+ }, {
24
+ capabilities?: string[] | null | undefined;
25
+ partition: string;
26
+ }, {
27
+ capabilities?: string[] | undefined;
28
+ partition: string;
29
+ }, {
30
+ capabilities?: string[] | undefined;
31
+ partition: string;
32
+ }>>>;
20
33
  }, {
21
34
  applicationType?: "native" | "web" | null | undefined;
22
35
  backchannelLogoutSessionRequired?: string | number | boolean | null | undefined;
@@ -27,6 +40,10 @@ export declare const clientInputValidator: import("@vinejs/vine").VineValidator<
27
40
  logoUri?: string | null | undefined;
28
41
  postLogoutRedirectUris?: string[] | null | undefined;
29
42
  redirectUris?: string[] | null | undefined;
43
+ serviceTenant?: {
44
+ capabilities?: string[] | null | undefined;
45
+ partition: string;
46
+ } | null | undefined;
30
47
  tokenEndpointAuthMethod?: "client_secret_basic" | "client_secret_post" | "none" | null | undefined;
31
48
  }, {
32
49
  applicationType?: "native" | "web" | undefined;
@@ -38,6 +55,10 @@ export declare const clientInputValidator: import("@vinejs/vine").VineValidator<
38
55
  logoUri?: string | null | undefined;
39
56
  postLogoutRedirectUris?: string[] | undefined;
40
57
  redirectUris?: string[] | undefined;
58
+ serviceTenant?: {
59
+ capabilities?: string[] | undefined;
60
+ partition: string;
61
+ } | null | undefined;
41
62
  tokenEndpointAuthMethod?: "client_secret_basic" | "client_secret_post" | "none" | undefined;
42
63
  }, {
43
64
  applicationType?: "native" | "web" | undefined;
@@ -49,6 +70,10 @@ export declare const clientInputValidator: import("@vinejs/vine").VineValidator<
49
70
  logoUri?: string | null | undefined;
50
71
  postLogoutRedirectUris?: string[] | undefined;
51
72
  redirectUris?: string[] | undefined;
73
+ serviceTenant?: {
74
+ capabilities?: string[] | undefined;
75
+ partition: string;
76
+ } | null | undefined;
52
77
  tokenEndpointAuthMethod?: "client_secret_basic" | "client_secret_post" | "none" | undefined;
53
78
  }>, Record<string, any> | undefined>;
54
79
  /** Forma validada de um client (saída do {@link clientInputValidator}). */
@@ -63,6 +88,10 @@ export type ClientInputFields = {
63
88
  backchannelLogoutSessionRequired?: boolean;
64
89
  clientName?: string | null;
65
90
  logoUri?: string | null;
91
+ serviceTenant?: {
92
+ partition: string;
93
+ capabilities?: string[];
94
+ } | null;
66
95
  };
67
96
  /**
68
97
  * Mapeia o input validado para um {@link ClientInput} COMPLETO (create), com defaults.
@@ -1,4 +1,5 @@
1
1
  import vine from '@vinejs/vine';
2
+ import { CAPABILITY_PATTERN, MAX_CAPABILITIES, PARTITION_PATTERN, } from '../service_tenants/client_metadata.js';
2
3
  import { APPLICATION_TYPES, redirectUriProblem } from './client_metadata.js';
3
4
  import { normalizeEmailIdentifier } from './email_identifier.js';
4
5
  /**
@@ -103,6 +104,18 @@ export const clientInputValidator = vine.compile(vine.object({
103
104
  // `null` (update) removes them.
104
105
  clientName: vine.string().trim().maxLength(200).nullable().optional(),
105
106
  logoUri: vine.string().trim().url(URL_OPTS).nullable().optional(),
107
+ // Service tenant (`client_credentials` + partition claim). `null` (update) turns it
108
+ // back into a regular client.
109
+ serviceTenant: vine
110
+ .object({
111
+ partition: vine.string().trim().regex(PARTITION_PATTERN),
112
+ capabilities: vine
113
+ .array(vine.string().trim().regex(CAPABILITY_PATTERN))
114
+ .maxLength(MAX_CAPABILITIES)
115
+ .optional(),
116
+ })
117
+ .nullable()
118
+ .optional(),
106
119
  }));
107
120
  /**
108
121
  * Mapeia o input validado para um {@link ClientInput} COMPLETO (create), com defaults.
@@ -111,6 +124,21 @@ export const clientInputValidator = vine.compile(vine.object({
111
124
  */
112
125
  export function clientCreateInput(v) {
113
126
  const applicationType = v.applicationType ?? 'web';
127
+ // A service tenant only uses client_credentials: that is its default grant.
128
+ if (v.serviceTenant) {
129
+ return {
130
+ clientId: v.clientId?.trim() || undefined,
131
+ applicationType,
132
+ redirectUris: v.redirectUris ?? [],
133
+ postLogoutRedirectUris: v.postLogoutRedirectUris ?? [],
134
+ grantTypes: v.grantTypes ?? ['client_credentials'],
135
+ tokenEndpointAuthMethod: v.tokenEndpointAuthMethod ?? 'client_secret_basic',
136
+ backchannelLogoutUri: v.backchannelLogoutUri || undefined,
137
+ clientName: v.clientName || undefined,
138
+ logoUri: v.logoUri || undefined,
139
+ serviceTenant: v.serviceTenant,
140
+ };
141
+ }
114
142
  return {
115
143
  clientId: v.clientId?.trim() || undefined,
116
144
  applicationType,
@@ -148,6 +176,8 @@ export function clientPartialInput(v) {
148
176
  out.clientName = v.clientName?.trim() || null;
149
177
  if (v.logoUri !== undefined)
150
178
  out.logoUri = v.logoUri?.trim() || null;
179
+ if (v.serviceTenant !== undefined)
180
+ out.serviceTenant = v.serviceTenant;
151
181
  return out;
152
182
  }
153
183
  // ─── Usuários ───────────────────────────────────────────────────────────────