@adonis-agora/authkit-server 0.75.0 → 0.77.0

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 (101) hide show
  1. package/README.md +148 -0
  2. package/build/host/views/account/apps.edge +30 -0
  3. package/build/host/views/agents/consent.edge +60 -0
  4. package/build/host/views/agents/done.edge +24 -0
  5. package/build/host/views/consent.edge +9 -1
  6. package/build/host/views/mfa-challenge.edge +42 -3
  7. package/build/host/views/partials/styles.edge +1 -1
  8. package/build/index.d.ts +18 -1
  9. package/build/index.js +9 -1
  10. package/build/providers/authkit_server_provider.js +75 -50
  11. package/build/src/accounts/account_store.d.ts +2 -0
  12. package/build/src/accounts/lucid_account_store.js +2 -0
  13. package/build/src/adapters/adapter_contract.d.ts +2 -0
  14. package/build/src/adapters/database_adapter.d.ts +1 -0
  15. package/build/src/adapters/database_adapter.js +26 -0
  16. package/build/src/adapters/redis_adapter.d.ts +1 -0
  17. package/build/src/adapters/redis_adapter.js +30 -0
  18. package/build/src/agents/agent_identity.d.ts +33 -0
  19. package/build/src/agents/agent_identity.js +156 -0
  20. package/build/src/agents/config.d.ts +99 -0
  21. package/build/src/agents/config.js +101 -0
  22. package/build/src/agents/delegation_service.d.ts +154 -0
  23. package/build/src/agents/delegation_service.js +394 -0
  24. package/build/src/agents/delegation_store.d.ts +93 -0
  25. package/build/src/agents/delegation_store.js +222 -0
  26. package/build/src/agents/middleware.d.ts +73 -0
  27. package/build/src/agents/middleware.js +113 -0
  28. package/build/src/agents/protocol.d.ts +62 -0
  29. package/build/src/agents/protocol.js +51 -0
  30. package/build/src/agents/runtime.d.ts +36 -0
  31. package/build/src/agents/runtime.js +65 -0
  32. package/build/src/agents/signer.d.ts +34 -0
  33. package/build/src/agents/signer.js +70 -0
  34. package/build/src/audit/audit_sink.d.ts +1 -1
  35. package/build/src/audit/audit_sink.js +5 -0
  36. package/build/src/controllers/authorization_server_metadata_controller.d.ts +10 -0
  37. package/build/src/controllers/authorization_server_metadata_controller.js +20 -0
  38. package/build/src/define_config.d.ts +50 -3
  39. package/build/src/define_config.js +42 -1
  40. package/build/src/host/account_api/account_api_controller.d.ts +12 -0
  41. package/build/src/host/account_api/account_api_controller.js +40 -0
  42. package/build/src/host/admin_api/dto.d.ts +1 -1
  43. package/build/src/host/admin_sessions_service.d.ts +2 -0
  44. package/build/src/host/admin_sessions_service.js +18 -0
  45. package/build/src/host/auth_host_config.d.ts +4 -0
  46. package/build/src/host/client_names.d.ts +11 -0
  47. package/build/src/host/client_names.js +12 -0
  48. package/build/src/host/controllers/account_apps_controller.d.ts +2 -0
  49. package/build/src/host/controllers/account_apps_controller.js +39 -3
  50. package/build/src/host/controllers/account_orgs_controller.js +2 -1
  51. package/build/src/host/controllers/account_security_controller.js +8 -4
  52. package/build/src/host/controllers/account_session_controller.js +2 -1
  53. package/build/src/host/controllers/agent_consent_controller.d.ts +17 -0
  54. package/build/src/host/controllers/agent_consent_controller.js +138 -0
  55. package/build/src/host/controllers/agent_oauth_controller.d.ts +23 -0
  56. package/build/src/host/controllers/agent_oauth_controller.js +110 -0
  57. package/build/src/host/controllers/interaction_controller.d.ts +13 -1
  58. package/build/src/host/controllers/interaction_controller.js +380 -24
  59. package/build/src/host/csrf.d.ts +8 -18
  60. package/build/src/host/csrf.js +28 -2
  61. package/build/src/host/custom_login.d.ts +36 -0
  62. package/build/src/host/custom_login.js +76 -0
  63. package/build/src/host/custom_login_completion.d.ts +4 -0
  64. package/build/src/host/custom_login_completion.js +33 -0
  65. package/build/src/host/custom_mfa.d.ts +77 -0
  66. package/build/src/host/custom_mfa.js +268 -0
  67. package/build/src/host/i18n.d.ts +58 -0
  68. package/build/src/host/i18n.js +64 -6
  69. package/build/src/host/idp_session_bridge.d.ts +5 -3
  70. package/build/src/host/idp_session_bridge.js +5 -3
  71. package/build/src/host/oidc_rp_guard.d.ts +5 -1
  72. package/build/src/host/oidc_rp_guard.js +15 -2
  73. package/build/src/host/persistent_rp_session.d.ts +8 -0
  74. package/build/src/host/persistent_rp_session.js +74 -0
  75. package/build/src/host/redirect_exact.d.ts +12 -0
  76. package/build/src/host/redirect_exact.js +17 -0
  77. package/build/src/host/register_auth_host.js +60 -7
  78. package/build/src/host/request_url.d.ts +10 -0
  79. package/build/src/host/request_url.js +17 -0
  80. package/build/src/host/sudo/methods/magic_link.js +2 -1
  81. package/build/src/host/sudo/runtime.js +3 -2
  82. package/build/src/host/sudo_mode.js +4 -4
  83. package/build/src/host/whatsapp_code_sender.d.ts +20 -0
  84. package/build/src/host/whatsapp_code_sender.js +9 -0
  85. package/build/src/host/whatsapp_senders/http.d.ts +5 -0
  86. package/build/src/host/whatsapp_senders/http.js +33 -0
  87. package/build/src/host/whatsapp_senders/meta_whatsapp_code_sender.d.ts +20 -0
  88. package/build/src/host/whatsapp_senders/meta_whatsapp_code_sender.js +51 -0
  89. package/build/src/host/whatsapp_senders/whatsmiau_code_sender.d.ts +14 -0
  90. package/build/src/host/whatsapp_senders/whatsmiau_code_sender.js +36 -0
  91. package/build/src/mcp/mcp_oauth.d.ts +85 -0
  92. package/build/src/mcp/mcp_oauth.js +154 -0
  93. package/build/src/provider/build_provider.js +15 -1
  94. package/build/src/provider/oidc_service.d.ts +8 -0
  95. package/build/src/provider/oidc_service.js +16 -2
  96. package/build/src/schema/ensure.js +92 -0
  97. package/build/stubs/ui/react/pages/consent.tsx +17 -1
  98. package/build/stubs/ui/react/pages/mfa-challenge.tsx +207 -37
  99. package/package.json +2 -1
  100. package/stubs/ui/react/pages/consent.tsx +17 -1
  101. package/stubs/ui/react/pages/mfa-challenge.tsx +207 -37
@@ -0,0 +1,70 @@
1
+ import { CompactSign, createLocalJWKSet, importJWK, SignJWT, } from 'jose';
2
+ /** O PACT só admite estes algoritmos nos tokens de delegação e recibos (§5.4). */
3
+ const DELEGATION_ALGS = ['ES256', 'RS256'];
4
+ function algOf(jwk) {
5
+ if (DELEGATION_ALGS.includes(jwk.alg))
6
+ return jwk.alg;
7
+ if (jwk.alg)
8
+ return null;
9
+ // JWKS inline sem `alg`: infere pelo tipo da chave.
10
+ if (jwk.kty === 'EC' && jwk.crv === 'P-256')
11
+ return 'ES256';
12
+ if (jwk.kty === 'RSA')
13
+ return 'RS256';
14
+ return null;
15
+ }
16
+ /** Lança quando o JWKS não tem nenhuma chave que possa assinar tokens de delegação. */
17
+ export function assertDelegationSigningKey(jwks) {
18
+ if (!jwks.keys.some((k) => algOf(k) !== null)) {
19
+ throw new Error('authkit: personalAgents.delegation exige uma chave ES256 ou RS256 no keystore (PACT §5.4). Configure `jwks.algorithm` como RS256 ou ES256.');
20
+ }
21
+ }
22
+ /**
23
+ * Signer sobre o keystore do IdP: a primeira chave ES256/RS256 do JWKS assina
24
+ * (a primeira é a corrente após uma rotação); o JWKS público inteiro verifica,
25
+ * então tokens assinados antes de uma rotação continuam válidos no período de
26
+ * graça. Lê a fonte a cada chamada — rotação ao vivo não exige restart.
27
+ */
28
+ export function keystoreSigner(source) {
29
+ const imported = new Map();
30
+ let localSet = null;
31
+ const current = async () => {
32
+ const jwk = source.signingJwks.keys.find((k) => algOf(k) !== null);
33
+ if (!jwk) {
34
+ throw new Error('authkit: personal agents exigem uma chave ES256 ou RS256 no keystore (PACT §5.4). Configure `jwks.algorithm` como RS256 ou ES256.');
35
+ }
36
+ const alg = algOf(jwk);
37
+ const cacheKey = `${jwk.kid ?? ''}:${alg}`;
38
+ let key = imported.get(cacheKey);
39
+ if (!key) {
40
+ key = importJWK(jwk, alg);
41
+ imported.set(cacheKey, key);
42
+ }
43
+ return { key: await key, alg, kid: jwk.kid };
44
+ };
45
+ return {
46
+ algorithms: [...DELEGATION_ALGS],
47
+ async signJwt(payload, { issuedAt, expiresIn }) {
48
+ const { key, alg, kid } = await current();
49
+ const iat = Math.floor(issuedAt.getTime() / 1000);
50
+ return new SignJWT(payload)
51
+ .setProtectedHeader({ alg, typ: 'JWT', ...(kid ? { kid } : {}) })
52
+ .setIssuedAt(iat)
53
+ .setExpirationTime(iat + expiresIn)
54
+ .sign(key);
55
+ },
56
+ async signJson(value) {
57
+ const { key, alg, kid } = await current();
58
+ return new CompactSign(new TextEncoder().encode(JSON.stringify(value)))
59
+ .setProtectedHeader({ alg, ...(kid ? { kid } : {}) })
60
+ .sign(key);
61
+ },
62
+ keySet() {
63
+ const jwks = source.publicJwks;
64
+ if (!localSet || localSet.jwks !== jwks) {
65
+ localSet = { jwks, set: createLocalJWKSet(jwks) };
66
+ }
67
+ return localSet.set;
68
+ },
69
+ };
70
+ }
@@ -8,7 +8,7 @@
8
8
  * varre os `audit.record({ type: ... })` do `src/` e falha quando um evento
9
9
  * emitido não está aqui.
10
10
  */
11
- export declare const AUDIT_EVENT_TYPES: readonly ['login.success', 'login.failure', 'signup', 'password_reset.issued', 'password_reset.consumed', 'pat.issued', 'pat.revoked', 'pat.used', 'impersonation', 'impersonation.panel_viewed', 'impersonation.stopped', 'mfa.enabled', 'mfa.disabled', 'mfa.recovery_codes_regenerated', 'account.locked', 'passkey.registered', 'passkey.removed', 'email_verification.issued', 'email_verification.consumed', 'client.created', 'client.updated', 'client.deleted', 'session.revoked_all', 'password.changed', 'password.rehashed', 'email.change_requested', 'email.changed', 'login.new_ip_notified', 'login.new_device', 'login.otp_sent', 'login.otp_verified', 'login.otp_failed', 'login.otp_invalidated', 'bot_protection.rejected', 'grant.revoked_by_user', 'user.created', 'user.password_reset_sent', 'user.disabled', 'user.enabled', 'user.deleted', 'profile.updated', 'account.deleted', 'account.exported', 'keys.rotated', 'organization.created', 'organization.updated', 'organization.deleted', 'organization.member_added', 'organization.member_removed', 'organization.member_role_changed', 'organization.member_role_updated', 'organization.switched', 'organization.deactivated', 'organization.invitation_sent', 'organization.invitation_accepted', 'organization.invitation_revoked', 'email_change.requested', 'email_change.confirmed', 'email_change.cancelled', 'security_notice.sent', 'settings.updated', 'maintenance.enabled', 'maintenance.disabled', 'trusted_device.revoked', 'password.expired_change_forced', 'otp.locked', 'otp.unlocked', 'otp.unlock_failed', 'sudo.confirmed', 'session.single_enforced', 'account.expired_login_blocked', 'account.expiration_warned', 'login.magic_link_sent', 'session.revoked', 'account.signed_out_all', 'client.secret_regenerated', 'roles_catalog.updated'];
11
+ export declare const AUDIT_EVENT_TYPES: readonly ['login.success', 'login.failure', 'signup', 'password_reset.issued', 'password_reset.consumed', 'pat.issued', 'pat.revoked', 'pat.used', 'impersonation', 'impersonation.panel_viewed', 'impersonation.stopped', 'mfa.enabled', 'mfa.disabled', 'mfa.recovery_codes_regenerated', 'account.locked', 'passkey.registered', 'passkey.removed', 'email_verification.issued', 'email_verification.consumed', 'client.created', 'client.updated', 'client.deleted', 'session.revoked_all', 'password.changed', 'password.rehashed', 'email.change_requested', 'email.changed', 'login.new_ip_notified', 'login.new_device', 'login.otp_sent', 'login.otp_verified', 'login.otp_failed', 'login.otp_invalidated', 'bot_protection.rejected', 'grant.revoked_by_user', 'user.created', 'user.password_reset_sent', 'user.disabled', 'user.enabled', 'user.deleted', 'profile.updated', 'account.deleted', 'account.exported', 'keys.rotated', 'organization.created', 'organization.updated', 'organization.deleted', 'organization.member_added', 'organization.member_removed', 'organization.member_role_changed', 'organization.member_role_updated', 'organization.switched', 'organization.deactivated', 'organization.invitation_sent', 'organization.invitation_accepted', 'organization.invitation_revoked', 'email_change.requested', 'email_change.confirmed', 'email_change.cancelled', 'security_notice.sent', 'settings.updated', 'maintenance.enabled', 'maintenance.disabled', 'trusted_device.revoked', 'password.expired_change_forced', 'otp.locked', 'otp.unlocked', 'otp.unlock_failed', 'sudo.confirmed', 'session.single_enforced', 'account.expired_login_blocked', 'account.expiration_warned', 'login.magic_link_sent', 'session.revoked', 'account.signed_out_all', 'client.secret_regenerated', 'roles_catalog.updated', 'agent.delegation_approved', 'agent.delegation_denied', 'agent.grant_revoked'];
12
12
  /** Tipos de eventos de auditoria relevantes para segurança emitidos pelo IdP. */
13
13
  export type AuditEventType = (typeof AUDIT_EVENT_TYPES)[number];
14
14
  /**
@@ -108,4 +108,9 @@ export const AUDIT_EVENT_TYPES = [
108
108
  'account.signed_out_all',
109
109
  'client.secret_regenerated',
110
110
  'roles_catalog.updated',
111
+ // Personal agents (PACT §5): o usuário aprovou/negou a delegação pedida por um
112
+ // agente, ou revogou um grant no console. `clientId` = issuer do agente.
113
+ 'agent.delegation_approved',
114
+ 'agent.delegation_denied',
115
+ 'agent.grant_revoked',
111
116
  ];
@@ -0,0 +1,10 @@
1
+ import type { HttpContext } from '@adonisjs/core/http';
2
+ /**
3
+ * Metadata do servidor de autorização no caminho da RFC 8414 §3.1: com o issuer em
4
+ * `https://app/oidc`, o documento fica em `https://app/.well-known/oauth-authorization-server/oidc`
5
+ * — onde os clientes MCP o procuram primeiro. O provider só o serve sob o próprio mount
6
+ * (`/oidc/.well-known/...`); aqui a requisição é reescrita para lá.
7
+ */
8
+ export default class AuthorizationServerMetadataController {
9
+ handle(ctx: HttpContext): Promise<void>;
10
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Metadata do servidor de autorização no caminho da RFC 8414 §3.1: com o issuer em
3
+ * `https://app/oidc`, o documento fica em `https://app/.well-known/oauth-authorization-server/oidc`
4
+ * — onde os clientes MCP o procuram primeiro. O provider só o serve sob o próprio mount
5
+ * (`/oidc/.well-known/...`); aqui a requisição é reescrita para lá.
6
+ */
7
+ export default class AuthorizationServerMetadataController {
8
+ async handle(ctx) {
9
+ const service = await ctx.containerResolver.make('authkit.server');
10
+ const req = ctx.request.request;
11
+ const res = ctx.response.response;
12
+ const issuerPath = new URL(service.config.issuer).pathname.replace(/\/+$/, '');
13
+ req.url = `${issuerPath}/.well-known/oauth-authorization-server`;
14
+ return new Promise((resolve) => {
15
+ res.on('finish', resolve);
16
+ res.on('close', resolve);
17
+ service.callback(req, res);
18
+ });
19
+ }
20
+ }
@@ -2,17 +2,22 @@ import type { AccessTokenFormat, AccessTokensConfig, ClientConfig, JwksConfig, K
2
2
  import type { HttpContext } from '@adonisjs/core/http';
3
3
  import type { AccountStore, AuthAccount } from './accounts/account_store.js';
4
4
  import { type AdapterFactory, adapters, type OidcAdapterClass } from './adapters/factory.js';
5
+ import { type PersonalAgentsConfigInput, type ResolvedPersonalAgentsConfig } from './agents/config.js';
5
6
  import type { AuditSink } from './audit/audit_sink.js';
6
7
  import { type EventsConfigInput } from './events/dispatcher.js';
7
8
  import { type BotProtectionConfigInput, type ResolvedBotProtectionConfig } from './host/bot_protection.js';
8
9
  import { type BrandingConfig } from './host/branding.js';
9
10
  import { type PolicyRouteOption } from './host/config_locks.js';
11
+ import type { CustomLoginMethods } from './host/custom_login.js';
12
+ import type { CustomMfaMethods } from './host/custom_mfa.js';
10
13
  import type { ResolveGeo } from './host/geo.js';
11
14
  import { type AuthMessages, type I18nConfig } from './host/i18n.js';
12
15
  import { type OtpLoginConfigInput, type ResolvedOtpLoginConfig } from './host/otp_login.js';
13
16
  import type { AuthHostOptions } from './host/register_auth_host.js';
14
17
  import type { SudoMethod } from './host/sudo/types.js';
15
18
  import { type ResolvedTrustedDevicesConfig, type TrustedDevicesConfigInput } from './host/trusted_device.js';
19
+ import type { WhatsappCodeSenderBinding } from './host/whatsapp_code_sender.js';
20
+ import { type McpOAuthConfigInput, type ResolvedMcpOAuthConfig } from './mcp/mcp_oauth.js';
16
21
  import type { PatStore } from './pat/pat_store.js';
17
22
  import { type RedirectUriPolicy, type ResolvedRedirectUriPolicy, type ValidateRegistrationHook } from './provider/registration_policy.js';
18
23
  export type { AuthAccount };
@@ -757,6 +762,17 @@ export interface ResolvedInteractionRecoveryConfig {
757
762
  redirectTo?: string;
758
763
  }
759
764
  export interface AuthServerConfigInput {
765
+ /** Host-defined primary authentication classes or instances, keyed by stable method ID. */
766
+ customLoginMethods?: CustomLoginMethods;
767
+ /** Optional enrolled factor registry and total distinct factor count (primary included). */
768
+ mfa?: {
769
+ methods?: CustomMfaMethods;
770
+ requiredFactors?: number;
771
+ };
772
+ /** OTP delivery only. The host remains responsible for issuing and verifying codes. */
773
+ whatsapp?: {
774
+ sender: WhatsappCodeSenderBinding;
775
+ };
760
776
  issuer: string;
761
777
  adapter: AdapterFactory;
762
778
  /**
@@ -975,6 +991,14 @@ export interface AuthServerConfigInput {
975
991
  dynamicRegistration?: DynamicRegistrationConfigInput;
976
992
  /** Device Authorization Grant (RFC 8628). Default: desligado. */
977
993
  deviceFlow?: DeviceFlowConfigInput;
994
+ /**
995
+ * Personal agents (PACT — https://openpactprotocol.org): plataformas de agente
996
+ * que falam com este app em nome de um usuário. Identidade por JWT assinado
997
+ * pelo agente (JWKS) e, com `delegation`, consentimento do usuário via device
998
+ * flow para o agente agir na conta dele dentro de scopes do app. Default:
999
+ * desligado.
1000
+ */
1001
+ personalAgents?: PersonalAgentsConfigInput;
978
1002
  /** Uploads (avatar) via o `@adonisjs/drive` do app. Default: drive default, 5MB. */
979
1003
  uploads?: UploadsConfigInput;
980
1004
  /** DPoP — sender-constrained tokens (RFC 9449). Default: desligado. */
@@ -1024,6 +1048,14 @@ export interface AuthServerConfigInput {
1024
1048
  * `resources` mapeia resource indicators (RFC 8707) para audiences/scopes/formato/TTL por API.
1025
1049
  */
1026
1050
  accessTokens?: AccessTokensConfig;
1051
+ /**
1052
+ * Login OAuth de clientes MCP (Claude Code, Claude, ChatGPT, VS Code, Cursor): `true` liga o
1053
+ * registro dinâmico restrito aos redirects deles, o refresh token desses clientes e os
1054
+ * `resource` dos servidores MCP — os de `resources` e os que se registram em runtime (o MCP do
1055
+ * `@adonis-agora/agent`). Um `dynamicRegistration` declarado continua mandando no registro.
1056
+ * Ver `src/mcp/mcp_oauth.ts`. Default: desligado.
1057
+ */
1058
+ mcp?: boolean | McpOAuthConfigInput;
1027
1059
  /**
1028
1060
  * Console admin do IdP (B6). Default: desligado.
1029
1061
  *
@@ -1056,11 +1088,13 @@ export interface AuthServerConfigInput {
1056
1088
  */
1057
1089
  resolveGeo?: ResolveGeo;
1058
1090
  /**
1059
- * Gestão automática do schema das OITO tabelas do authkit:
1091
+ * Gestão automática do schema das ONZE tabelas do authkit:
1060
1092
  * `authkit_oidc_payloads`, `auth_settings`, `auth_password_history`,
1061
- * `auth_mfa`, `auth_session_revocations` e as três de organizations
1093
+ * `auth_mfa`, `auth_session_revocations`, as três de organizations
1062
1094
  * (`auth_organizations`, `auth_organization_members`,
1063
- * `auth_organization_invitations`). Ver `TABLES` em `schema/ensure.ts` — a
1095
+ * `auth_organization_invitations`) e as três de personal agents
1096
+ * (`auth_agent_device_codes`, `auth_agent_grants`,
1097
+ * `auth_agent_refresh_tokens`). Ver `TABLES` em `schema/ensure.ts` — a
1064
1098
  * lista aqui existe para o leitor, mas quem manda é aquele array.
1065
1099
  *
1066
1100
  * FORA desta gestão: `authkit_keystore`, criada sob demanda pelo
@@ -1132,6 +1166,16 @@ export interface AuthServerConfigInput {
1132
1166
  };
1133
1167
  }
1134
1168
  export interface ResolvedServerConfig {
1169
+ customLoginMethods?: CustomLoginMethods;
1170
+ /** Optional enrolled factor registry and total distinct factor count (primary included). */
1171
+ mfa?: {
1172
+ methods?: CustomMfaMethods;
1173
+ requiredFactors?: number;
1174
+ };
1175
+ /** OTP delivery only. The host remains responsible for issuing and verifying codes. */
1176
+ whatsapp?: {
1177
+ sender: WhatsappCodeSenderBinding;
1178
+ };
1135
1179
  issuer: string;
1136
1180
  AdapterClass: OidcAdapterClass;
1137
1181
  /**
@@ -1214,6 +1258,8 @@ export interface ResolvedServerConfig {
1214
1258
  dynamicRegistration: ResolvedDynamicRegistrationConfig;
1215
1259
  /** Device Authorization Grant resolvido (default desligado). */
1216
1260
  deviceFlow: ResolvedDeviceFlowConfig;
1261
+ /** Personal agents resolvido (undefined = desligado). */
1262
+ personalAgents?: ResolvedPersonalAgentsConfig;
1217
1263
  /** Uploads resolvido (avatar via drive do app; sempre presente). */
1218
1264
  uploads: ResolvedUploadsConfig;
1219
1265
  /** DPoP resolvido (default desligado). */
@@ -1236,6 +1282,7 @@ export interface ResolvedServerConfig {
1236
1282
  registration: ResolvedRegistrationConfig;
1237
1283
  /** Access Tokens resolvido (RFC 9068; default opaque). */
1238
1284
  accessTokens: ResolvedAccessTokensConfig;
1285
+ mcp: ResolvedMcpOAuthConfig;
1239
1286
  /** Console admin resolvido (sempre presente; default desligado). */
1240
1287
  admin: ResolvedAdminConfig;
1241
1288
  /** Admin REST API resolvida (sempre presente; default desligada). */
@@ -1,5 +1,7 @@
1
1
  import { configProvider } from '@adonisjs/core';
2
2
  import { adapters } from './adapters/factory.js';
3
+ import { resolvePersonalAgentsConfig, } from './agents/config.js';
4
+ import { assertDelegationSigningKey } from './agents/signer.js';
3
5
  import { composeAuditSink, resolveEvents } from './events/dispatcher.js';
4
6
  import { resolveBotProtection, } from './host/bot_protection.js';
5
7
  import { resolveBranding } from './host/branding.js';
@@ -13,6 +15,7 @@ import { generateJwks } from './keys/jwks_manager.js';
13
15
  import { KeystoreCodec } from './keys/keystore_codec.js';
14
16
  import { loadEncryptionService } from './keys/keystore_crypto.js';
15
17
  import { KeystoreManager, resolveKeystoreVault } from './keys/keystore_manager.js';
18
+ import { mcpClientRegistration, resolveMcpOAuth, } from './mcp/mcp_oauth.js';
16
19
  import { OPEN_REGISTRATION_REDIRECT_POLICY, resolveRedirectUriPolicy, } from './provider/registration_policy.js';
17
20
  export { adapters };
18
21
  const RATE_LIMIT_DEFAULTS = {
@@ -81,6 +84,20 @@ export function resolveDynamicRegistration(input) {
81
84
  validateRegistration: input?.validateRegistration,
82
85
  };
83
86
  }
87
+ /**
88
+ * O registro dinâmico que `mcp` liga quando o app não declarou o seu: aberto, só para os
89
+ * redirects dos clientes MCP, pedindo `offline_access` para quem quer refresh token.
90
+ */
91
+ function mcpDynamicRegistration(mcp) {
92
+ const resolved = resolveMcpOAuth(mcp);
93
+ if (!resolved.enabled)
94
+ return undefined;
95
+ return {
96
+ enabled: true,
97
+ redirectUriPolicy: resolved.redirectUriPolicy,
98
+ validateRegistration: (metadata) => mcpClientRegistration(metadata),
99
+ };
100
+ }
84
101
  export function resolveDeviceFlow(input) {
85
102
  return { enabled: input?.enabled ?? false };
86
103
  }
@@ -256,6 +273,11 @@ export function jwksAutoFallbackWarning(storePath) {
256
273
  return `AuthKit: jwks 'auto' caiu no fallback de disco (${storePath}) — a chave privada de assinatura será persistida em arquivo. Para produção, defina AUTHKIT_JWKS (secret manager) ou configure jwks.store explicitamente.`;
257
274
  }
258
275
  export function defineConfig(config) {
276
+ const requiredFactors = config.mfa?.requiredFactors;
277
+ if (requiredFactors !== undefined &&
278
+ (!Number.isInteger(requiredFactors) || requiredFactors < 2 || requiredFactors > 8)) {
279
+ throw new Error('mfa.requiredFactors must be an integer between 2 and 8');
280
+ }
259
281
  return configProvider.create(async (app) => {
260
282
  const AdapterClass = await config.adapter.resolver(app);
261
283
  // `session.adapter` ausente ⇒ mesma classe do default (back-compat: o
@@ -317,6 +339,20 @@ export function defineConfig(config) {
317
339
  else {
318
340
  jwks = { keys: jwksConfig.keys ?? [] };
319
341
  }
342
+ // Personal agents com delegação assinam tokens e recibos com este keystore,
343
+ // e o PACT só admite ES256/RS256 — falha no boot, não na primeira troca de token.
344
+ const personalAgents = resolvePersonalAgentsConfig(config.personalAgents);
345
+ if (personalAgents?.delegation)
346
+ assertDelegationSigningKey(jwks);
347
+ // Debaixo do mountPath, a rota curinga do oidc-provider (`${mount}/*`)
348
+ // engoliria as rotas dos agentes — e o `authkitCsrfExceptions` (que isenta
349
+ // tudo que contém o mountPath) tiraria o CSRF da tela de consentimento.
350
+ const oidcMount = (config.mountPath ?? '/oidc').replace(/\/+$/, '');
351
+ if (personalAgents &&
352
+ oidcMount &&
353
+ (personalAgents.prefix === oidcMount || personalAgents.prefix.startsWith(`${oidcMount}/`))) {
354
+ throw new Error(`authkit: personalAgents.prefix "${personalAgents.prefix}" não pode ficar debaixo do mountPath do OIDC ("${oidcMount}").`);
355
+ }
320
356
  // BACKSTOP DE SUDO. Um host cujo `sudo.methods` não tem um único método
321
357
  // satisfazível por conta sem senha fica bricado para TODA operação sob
322
358
  // `requireSudo` — e hoje isso só se descobre quando um usuário não consegue
@@ -363,6 +399,9 @@ export function defineConfig(config) {
363
399
  return acc ? { id: acc.id } : null;
364
400
  },
365
401
  accountStore: config.accountStore,
402
+ customLoginMethods: config.customLoginMethods,
403
+ mfa: config.mfa,
404
+ whatsapp: config.whatsapp,
366
405
  patStore: config.patStore,
367
406
  mountPath: config.mountPath ?? '/oidc',
368
407
  accountHome: config.accountHome,
@@ -405,8 +444,9 @@ export function defineConfig(config) {
405
444
  // top-level não os fornece — consumidor declara UMA vez. Top-level ainda vence.
406
445
  mfaIssuer: effectiveMfaIssuer,
407
446
  webauthn: resolveWebauthn(config.issuer, effectiveMfaIssuer, config.webauthn ?? config.accountStore?.__webauthn),
408
- dynamicRegistration: resolveDynamicRegistration(config.dynamicRegistration),
447
+ dynamicRegistration: resolveDynamicRegistration(config.dynamicRegistration ?? mcpDynamicRegistration(config.mcp)),
409
448
  deviceFlow: resolveDeviceFlow(config.deviceFlow),
449
+ personalAgents,
410
450
  uploads: resolveUploads(config.uploads),
411
451
  dpop: resolveDpop(config.dpop),
412
452
  par: resolvePar(config.par),
@@ -424,6 +464,7 @@ export function defineConfig(config) {
424
464
  : undefined,
425
465
  registration: resolveRegistration(config.registration),
426
466
  accessTokens: resolveAccessTokens(config.issuer, config.accessTokens),
467
+ mcp: resolveMcpOAuth(config.mcp),
427
468
  admin: resolveAdmin(config.admin),
428
469
  adminApi: resolveAdminApi(config.adminApi),
429
470
  organizations: resolveOrganizations(config.organizations),
@@ -119,6 +119,7 @@ export default class AccountApiController {
119
119
  grants: number;
120
120
  accessTokens: number;
121
121
  refreshTokens: number;
122
+ agentGrants?: number;
122
123
  ok: boolean;
123
124
  }>;
124
125
  /**
@@ -133,6 +134,7 @@ export default class AccountApiController {
133
134
  grants: number;
134
135
  accessTokens: number;
135
136
  refreshTokens: number;
137
+ agentGrants?: number;
136
138
  ok: boolean;
137
139
  signedOut: boolean;
138
140
  }>;
@@ -141,6 +143,7 @@ export default class AccountApiController {
141
143
  supported: boolean;
142
144
  apps: {
143
145
  clientId: string;
146
+ name: string;
144
147
  accessTokens: number;
145
148
  refreshTokens: number;
146
149
  }[];
@@ -151,6 +154,15 @@ export default class AccountApiController {
151
154
  grants: number;
152
155
  accessTokens: number;
153
156
  refreshTokens: number;
157
+ agentGrants?: number;
158
+ ok: boolean;
159
+ }>;
160
+ /** Personal agents com delegação ativa na conta (scopes aprovados). */
161
+ listAgents(ctx: HttpContext): Promise<void | {
162
+ agents: import("../../agents/delegation_service.ts").DelegationGrantSummary[];
163
+ }>;
164
+ /** Revoga a delegação de um personal agent (efeito imediato nos tokens). */
165
+ revokeAgent(ctx: HttpContext): Promise<void | {
154
166
  ok: boolean;
155
167
  }>;
156
168
  /** Status completo do MFA: TOTP + passkeys + recovery. */
@@ -27,8 +27,10 @@
27
27
  * GET /account/api/orgs/:id → detalhe de org (requer membership)
28
28
  * GET /account/api/orgs/invitations → convites pendentes
29
29
  */
30
+ import { clientDisplayNames } from '../client_names.js';
30
31
  import '../augmentations.js';
31
32
  import { supportsAccountSecurity, supportsLoginMethodsPreference, supportsMagicLink, supportsOrganizations, supportsPasskeys, supportsProfile, supportsRecoveryCodeCount, supportsRecoveryCodeRegeneration, } from '../../accounts/account_store.js';
33
+ import { personalAgentsFor } from '../../agents/runtime.js';
32
34
  import { PasswordPolicyError } from '../../password/password_manager.js';
33
35
  import { accountPath } from '../account_paths.js';
34
36
  import { ACCOUNT_SESSION_KEY } from '../account_session_key.js';
@@ -575,12 +577,14 @@ export default class AccountApiController {
575
577
  return { supported: false, apps: [] };
576
578
  }
577
579
  const grants = await adminSessions.listGrants(userId);
580
+ const names = await clientDisplayNames(service, grants.map((g) => g.clientId).filter((id) => !!id));
578
581
  return {
579
582
  supported: true,
580
583
  apps: grants
581
584
  .filter((g) => !!g.clientId)
582
585
  .map((g) => ({
583
586
  clientId: g.clientId,
587
+ name: names.get(g.clientId) ?? g.clientId,
584
588
  accessTokens: g.accessTokens,
585
589
  refreshTokens: g.refreshTokens,
586
590
  })),
@@ -609,6 +613,42 @@ export default class AccountApiController {
609
613
  });
610
614
  return { ok: true, ...result };
611
615
  }
616
+ // ─── GET /account/api/agents ─────────────────────────────────────────────
617
+ /** Personal agents com delegação ativa na conta (scopes aprovados). */
618
+ async listAgents(ctx) {
619
+ const runtime = await personalAgentsFor(ctx);
620
+ if (!runtime?.delegation) {
621
+ return ctx.response
622
+ .status(404)
623
+ .send(apiErr('capability_unsupported', 'Personal agent delegation is not enabled.'));
624
+ }
625
+ const userId = ctx.session.get(ACCOUNT_SESSION_KEY);
626
+ const grants = await runtime.delegation.listGrants(userId);
627
+ return { agents: grants };
628
+ }
629
+ // ─── DELETE /account/api/agents/:id ──────────────────────────────────────
630
+ /** Revoga a delegação de um personal agent (efeito imediato nos tokens). */
631
+ async revokeAgent(ctx) {
632
+ const runtime = await personalAgentsFor(ctx);
633
+ if (!runtime?.delegation) {
634
+ return ctx.response
635
+ .status(404)
636
+ .send(apiErr('capability_unsupported', 'Personal agent delegation is not enabled.'));
637
+ }
638
+ const service = await ctx.containerResolver.make('authkit.server');
639
+ const userId = ctx.session.get(ACCOUNT_SESSION_KEY);
640
+ const grantId = ctx.request.param('id');
641
+ if (!(await runtime.delegation.revokeGrant(userId, grantId))) {
642
+ return ctx.response.status(404).send(apiErr('not_found', 'Grant not found.'));
643
+ }
644
+ await service.config.audit?.record({
645
+ type: 'agent.grant_revoked',
646
+ accountId: userId,
647
+ ip: ctx.request.ip?.() ?? null,
648
+ metadata: { grantId },
649
+ });
650
+ return { ok: true };
651
+ }
612
652
  // ─── GET /account/api/mfa ────────────────────────────────────────────────
613
653
  /** Status completo do MFA: TOTP + passkeys + recovery. */
614
654
  async mfaStatus(ctx) {
@@ -49,7 +49,7 @@ export declare function grantDto(grant: AdminGrant): {
49
49
  };
50
50
  export declare function auditDto(event: StoredAuditEvent): {
51
51
  id: string;
52
- type: "account.deleted" | "account.expiration_warned" | "account.expired_login_blocked" | "account.exported" | "account.locked" | "account.signed_out_all" | "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";
52
+ type: "account.deleted" | "account.expiration_warned" | "account.expired_login_blocked" | "account.exported" | "account.locked" | "account.signed_out_all" | "agent.delegation_approved" | "agent.delegation_denied" | "agent.grant_revoked" | "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";
53
53
  accountId: string | null;
54
54
  email: string | null;
55
55
  clientId: string | null;
@@ -45,6 +45,8 @@ export interface RevokeResult {
45
45
  grants: number;
46
46
  accessTokens: number;
47
47
  refreshTokens: number;
48
+ /** Delegações de personal agents revogadas junto (ausente quando nenhuma). */
49
+ agentGrants?: number;
48
50
  }
49
51
  export declare class AdminSessionsService {
50
52
  #private;
@@ -1,5 +1,6 @@
1
1
  import { getBootedApp } from '../../services/booted_app.js';
2
2
  import { pickModelAdapterClass } from '../adapters/factory.js';
3
+ import { buildPersonalAgentsRuntime } from '../agents/runtime.js';
3
4
  import { normalizeActiveOrg } from './active_org_cookie.js';
4
5
  /**
5
6
  * Serviço de inspeção/revogação das SESSÕES e GRANTS ativos de uma conta.
@@ -23,7 +24,9 @@ export class AdminSessionsService {
23
24
  #accountStore;
24
25
  /** Conexão Lucid das tabelas authkit (schema `auth`) — onde vive auth_session_revocations. */
25
26
  #schemaConnection;
27
+ #oidc;
26
28
  constructor(oidc) {
29
+ this.#oidc = oidc;
27
30
  this.#AdapterClass = oidc.config.AdapterClass;
28
31
  // `Session`/`Grant`/tokens vivem no adapter da sessão quando `session:`
29
32
  // está configurado (default: o mesmo do `AdapterClass`). `??` cobre
@@ -81,6 +84,17 @@ export class AdminSessionsService {
81
84
  await this.#logRevocationFailure(accountId, error);
82
85
  }
83
86
  }
87
+ /**
88
+ * Revogação total também corta os personal agents: "sair de todas as sessões",
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. Sem `personalAgents.delegation` é no-op — e aí nem
91
+ * resolve o banco. Diferente do `recordSubRevocation`, uma falha AQUI propaga:
92
+ * não há outra camada que corte esses tokens.
93
+ */
94
+ async #revokeAgentGrants(accountId) {
95
+ const runtime = await buildPersonalAgentsRuntime(this.#oidc, () => getBootedApp().container.make('lucid.db'));
96
+ return runtime?.delegation ? runtime.delegation.revokeAllGrants(accountId) : 0;
97
+ }
84
98
  /**
85
99
  * Loga (best-effort, sem nunca lançar) a falha de {@link recordSubRevocation}. Prefere o
86
100
  * `logger` do container; se ele não puder ser resolvido (ex.: o próprio `getBootedApp()` foi o
@@ -240,11 +254,13 @@ export class AdminSessionsService {
240
254
  // no default (`now`) — não há sessão nova a proteger nesse caminho.
241
255
  const cutoff = typeof preserved?.loginTs === 'number' ? new Date((preserved.loginTs - 1) * 1000) : undefined;
242
256
  await this.recordSubRevocation(accountId, cutoff);
257
+ const agentGrants = await this.#revokeAgentGrants(accountId);
243
258
  return {
244
259
  sessions: sessionsToRevoke.length,
245
260
  grants: grants.length,
246
261
  accessTokens,
247
262
  refreshTokens,
263
+ ...(agentGrants > 0 ? { agentGrants } : {}),
248
264
  };
249
265
  }
250
266
  /**
@@ -277,11 +293,13 @@ export class AdminSessionsService {
277
293
  }
278
294
  // Revogação total → propaga p/ clients cookie-based via tabela compartilhada (instantâneo).
279
295
  await this.recordSubRevocation(accountId);
296
+ const agentGrants = await this.#revokeAgentGrants(accountId);
280
297
  return {
281
298
  sessions: sessions.length,
282
299
  grants: grants.length,
283
300
  accessTokens,
284
301
  refreshTokens,
302
+ ...(agentGrants > 0 ? { agentGrants } : {}),
285
303
  };
286
304
  }
287
305
  /**
@@ -35,6 +35,10 @@ export interface AuthHostRuntimeConfig {
35
35
  * Ver `deriveLockedRouteOptions`.
36
36
  */
37
37
  lockedRouteOptions?: PolicyRouteOption[];
38
+ /** Personal agents ligados → prefixo das rotas deles (`/agents`). */
39
+ personalAgents?: {
40
+ prefix: string;
41
+ };
38
42
  /**
39
43
  * API headless (Clerk-style). Presente quando o host a declarou — o provider o
40
44
  * stash no boot para `registerAuthHost` montar as rotas sem reler o config inteiro.
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Nome de exibição de cada client: o `client_name` que ele registrou (um cliente MCP registra o
3
+ * seu, ex.: "Claude Code"), senão o próprio `client_id`. Client que sumiu do adapter → o id.
4
+ */
5
+ export declare function clientDisplayNames(service: {
6
+ provider: {
7
+ Client: {
8
+ find(id: string): Promise<any>;
9
+ };
10
+ };
11
+ }, clientIds: string[]): Promise<Map<string, string>>;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Nome de exibição de cada client: o `client_name` que ele registrou (um cliente MCP registra o
3
+ * seu, ex.: "Claude Code"), senão o próprio `client_id`. Client que sumiu do adapter → o id.
4
+ */
5
+ export async function clientDisplayNames(service, clientIds) {
6
+ const names = new Map();
7
+ await Promise.all([...new Set(clientIds)].map(async (clientId) => {
8
+ const client = await service.provider.Client.find(clientId).catch(() => null);
9
+ names.set(clientId, client?.clientName || clientId);
10
+ }));
11
+ return names;
12
+ }
@@ -12,4 +12,6 @@ export default class AccountAppsController {
12
12
  index(ctx: HttpContext): Promise<any>;
13
13
  /** POST /account/apps/:clientId/revoke — revoga o acesso de um client. */
14
14
  revoke(ctx: HttpContext): Promise<void>;
15
+ /** POST /account/apps/agents/:grantId/revoke — revoga a delegação de um personal agent. */
16
+ revokeAgent(ctx: HttpContext): Promise<void>;
15
17
  }