@adonis-agora/authkit-server 0.76.0 → 0.78.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 (88) hide show
  1. package/README.md +149 -0
  2. package/build/host/views/agents/consent.edge +13 -2
  3. package/build/host/views/agents/done.edge +3 -0
  4. package/build/host/views/mfa-challenge.edge +42 -3
  5. package/build/index.d.ts +19 -1
  6. package/build/index.js +13 -1
  7. package/build/providers/authkit_server_provider.js +15 -1
  8. package/build/src/accounts/account_store.d.ts +2 -0
  9. package/build/src/accounts/lucid_account_store.js +2 -0
  10. package/build/src/adapters/adapter_contract.d.ts +2 -0
  11. package/build/src/adapters/database_adapter.d.ts +1 -0
  12. package/build/src/adapters/database_adapter.js +26 -0
  13. package/build/src/adapters/redis_adapter.d.ts +1 -0
  14. package/build/src/adapters/redis_adapter.js +30 -0
  15. package/build/src/agents/config.d.ts +20 -3
  16. package/build/src/agents/config.js +19 -4
  17. package/build/src/agents/delegation_service.js +18 -8
  18. package/build/src/agents/middleware.js +2 -1
  19. package/build/src/agents/poppy/client_registry.d.ts +45 -0
  20. package/build/src/agents/poppy/client_registry.js +226 -0
  21. package/build/src/agents/poppy/config.d.ts +362 -0
  22. package/build/src/agents/poppy/config.js +201 -0
  23. package/build/src/agents/poppy/dpop.d.ts +61 -0
  24. package/build/src/agents/poppy/dpop.js +158 -0
  25. package/build/src/agents/poppy/errors.d.ts +20 -0
  26. package/build/src/agents/poppy/errors.js +22 -0
  27. package/build/src/agents/poppy/replay.d.ts +25 -0
  28. package/build/src/agents/poppy/replay.js +106 -0
  29. package/build/src/agents/poppy/resource.d.ts +95 -0
  30. package/build/src/agents/poppy/resource.js +125 -0
  31. package/build/src/agents/poppy/safe_fetch.d.ts +48 -0
  32. package/build/src/agents/poppy/safe_fetch.js +256 -0
  33. package/build/src/agents/poppy/service.d.ts +280 -0
  34. package/build/src/agents/poppy/service.js +1452 -0
  35. package/build/src/agents/poppy/store.d.ts +109 -0
  36. package/build/src/agents/poppy/store.js +260 -0
  37. package/build/src/agents/poppy/web_session.d.ts +32 -0
  38. package/build/src/agents/poppy/web_session.js +104 -0
  39. package/build/src/agents/protocol.d.ts +8 -4
  40. package/build/src/agents/runtime.d.ts +17 -1
  41. package/build/src/agents/runtime.js +138 -13
  42. package/build/src/define_config.d.ts +24 -0
  43. package/build/src/define_config.js +14 -0
  44. package/build/src/host/account_api/account_api_controller.d.ts +12 -1
  45. package/build/src/host/account_api/account_api_controller.js +10 -5
  46. package/build/src/host/admin_sessions_service.js +8 -5
  47. package/build/src/host/auth_host_config.d.ts +2 -1
  48. package/build/src/host/controllers/account_apps_controller.js +13 -3
  49. package/build/src/host/controllers/account_security_controller.js +8 -4
  50. package/build/src/host/controllers/interaction_controller.d.ts +13 -1
  51. package/build/src/host/controllers/interaction_controller.js +352 -24
  52. package/build/src/host/controllers/poppy_consent_controller.d.ts +19 -0
  53. package/build/src/host/controllers/poppy_consent_controller.js +173 -0
  54. package/build/src/host/controllers/poppy_controller.d.ts +25 -0
  55. package/build/src/host/controllers/poppy_controller.js +194 -0
  56. package/build/src/host/csrf.d.ts +7 -0
  57. package/build/src/host/csrf.js +16 -1
  58. package/build/src/host/custom_login.d.ts +36 -0
  59. package/build/src/host/custom_login.js +76 -0
  60. package/build/src/host/custom_login_completion.d.ts +4 -0
  61. package/build/src/host/custom_login_completion.js +33 -0
  62. package/build/src/host/custom_mfa.d.ts +77 -0
  63. package/build/src/host/custom_mfa.js +268 -0
  64. package/build/src/host/i18n.d.ts +8 -0
  65. package/build/src/host/i18n.js +14 -6
  66. package/build/src/host/idp_session_bridge.d.ts +5 -3
  67. package/build/src/host/idp_session_bridge.js +5 -3
  68. package/build/src/host/oidc_rp_guard.d.ts +5 -1
  69. package/build/src/host/oidc_rp_guard.js +15 -2
  70. package/build/src/host/persistent_rp_session.d.ts +8 -0
  71. package/build/src/host/persistent_rp_session.js +74 -0
  72. package/build/src/host/poppy_session_key.d.ts +9 -0
  73. package/build/src/host/poppy_session_key.js +9 -0
  74. package/build/src/host/register_auth_host.js +72 -0
  75. package/build/src/host/whatsapp_code_sender.d.ts +20 -0
  76. package/build/src/host/whatsapp_code_sender.js +9 -0
  77. package/build/src/host/whatsapp_senders/http.d.ts +5 -0
  78. package/build/src/host/whatsapp_senders/http.js +33 -0
  79. package/build/src/host/whatsapp_senders/meta_whatsapp_code_sender.d.ts +20 -0
  80. package/build/src/host/whatsapp_senders/meta_whatsapp_code_sender.js +51 -0
  81. package/build/src/host/whatsapp_senders/whatsmiau_code_sender.d.ts +14 -0
  82. package/build/src/host/whatsapp_senders/whatsmiau_code_sender.js +36 -0
  83. package/build/src/provider/oidc_service.d.ts +5 -0
  84. package/build/src/provider/oidc_service.js +15 -4
  85. package/build/src/schema/ensure.js +124 -0
  86. package/build/stubs/ui/react/pages/mfa-challenge.tsx +207 -37
  87. package/package.json +2 -1
  88. package/stubs/ui/react/pages/mfa-challenge.tsx +207 -37
@@ -1,7 +1,13 @@
1
1
  import { supportsAccountStatus } from '../accounts/account_store.js';
2
+ import { findMcpResource } from '../mcp/mcp_oauth.js';
2
3
  import { PersonalAgentVerifier } from './agent_identity.js';
3
4
  import { PersonalAgentDelegation } from './delegation_service.js';
4
5
  import { DelegationStore } from './delegation_store.js';
6
+ import { PoppyClientRegistry } from './poppy/client_registry.js';
7
+ import { hmacNonceSource } from './poppy/dpop.js';
8
+ import { databaseReplayStore, memoryLimiter, memoryReplayStore, redisReplayStore, } from './poppy/replay.js';
9
+ import { PoppyService, poppyUrls } from './poppy/service.js';
10
+ import { PoppyStore } from './poppy/store.js';
5
11
  import { keystoreSigner } from './signer.js';
6
12
  /**
7
13
  * Deriva as URLs da ORIGEM do issuer configurado — nunca do Host da request
@@ -20,11 +26,77 @@ export function personalAgentUrls(oidcIssuer, prefix) {
20
26
  };
21
27
  }
22
28
  const runtimes = new WeakMap();
29
+ /** Ponte Poppy → oidc-provider: Bearer de MCP de Sessions logadas viram access tokens dele. */
30
+ function mcpBridge(service, poppyApis) {
31
+ return {
32
+ isMcpResource(url) {
33
+ try {
34
+ return findMcpResource(url, service.config.issuer, service.config.mcp) !== null;
35
+ }
36
+ catch {
37
+ return false;
38
+ }
39
+ },
40
+ async mint(input) {
41
+ const provider = service.provider;
42
+ if (!provider?.AccessToken || !provider?.Grant)
43
+ return null;
44
+ // Só faz sentido quando a integração MCP do authkit está ligada, ou o MCP é de `apis`.
45
+ if (!service.config.mcp?.enabled &&
46
+ !poppyApis.some((a) => a.type === 'mcp' && a.url.replace(/\/+$/, '') === input.resource.replace(/\/+$/, ''))) {
47
+ return null;
48
+ }
49
+ const remaining = Math.max(60, Math.floor((input.grantExpiresAt.getTime() - Date.now()) / 1000));
50
+ let grant = await provider.Grant.find(input.grantId);
51
+ if (!grant) {
52
+ grant = new provider.Grant({
53
+ jti: input.grantId,
54
+ accountId: input.accountId,
55
+ clientId: input.clientId,
56
+ expiresIn: remaining,
57
+ });
58
+ await grant.save();
59
+ }
60
+ const at = new provider.AccessToken({
61
+ accountId: input.accountId,
62
+ clientId: input.clientId,
63
+ grantId: input.grantId,
64
+ scope: input.scope,
65
+ gty: 'poppy',
66
+ expiresIn: input.expiresIn,
67
+ });
68
+ at.aud = input.resource;
69
+ return at.save();
70
+ },
71
+ async revoke(grantId) {
72
+ const provider = service.provider;
73
+ if (!provider?.Grant)
74
+ return;
75
+ await provider.AccessToken?.revokeByGrantId?.(grantId);
76
+ const grant = await provider.Grant.find(grantId);
77
+ if (grant)
78
+ await grant.destroy();
79
+ },
80
+ };
81
+ }
82
+ async function resolveReplay(replay, conn, make) {
83
+ if (replay === 'memory')
84
+ return memoryReplayStore();
85
+ if (replay === 'database')
86
+ return databaseReplayStore(conn);
87
+ if (typeof replay.claim === 'function')
88
+ return replay;
89
+ const name = replay.redis;
90
+ if (!make)
91
+ throw new Error('authkit: personalAgents.poppy.replay { redis } exige o container.');
92
+ const redis = await make('redis');
93
+ return redisReplayStore(redis.connection(name));
94
+ }
23
95
  /**
24
96
  * Monta (uma vez por `OidcService`) o runtime de personal agents. `null` quando
25
97
  * `personalAgents` não está no config — as rotas respondem 404.
26
98
  */
27
- export function buildPersonalAgentsRuntime(service, makeDb) {
99
+ export function buildPersonalAgentsRuntime(service, makeDb, options = {}) {
28
100
  let runtime = runtimes.get(service);
29
101
  if (!runtime) {
30
102
  runtime = (async () => {
@@ -32,25 +104,76 @@ export function buildPersonalAgentsRuntime(service, makeDb) {
32
104
  if (!config)
33
105
  return null;
34
106
  const urls = personalAgentUrls(service.config.issuer, config.prefix);
35
- let delegation = null;
36
- if (config.delegation) {
107
+ // Conta apagada ou suspensa corta a delegação na hora.
108
+ const isAccountActive = async (accountId) => {
109
+ const store = service.config.accountStore;
110
+ if (!(await store.findById(accountId)))
111
+ return false;
112
+ return !(supportsAccountStatus(store) && (await store.isDisabled(accountId)));
113
+ };
114
+ let conn = null;
115
+ if (config.delegation || config.poppy) {
37
116
  const db = await makeDb();
38
117
  const connection = service.config.schema.connection;
118
+ conn = () => (connection ? db.connection(connection) : db.connection());
119
+ }
120
+ const store = conn ? new DelegationStore(conn) : null;
121
+ let delegation = null;
122
+ if (config.delegation && store) {
39
123
  delegation = new PersonalAgentDelegation({
40
124
  cfg: config.delegation,
41
- store: new DelegationStore(() => connection ? db.connection(connection) : db.connection()),
125
+ store,
42
126
  signer: keystoreSigner(service),
43
127
  urls: { issuer: urls.issuer, consent: urls.consent },
44
- // Conta apagada ou suspensa corta a delegação na hora.
45
- isAccountActive: async (accountId) => {
46
- const store = service.config.accountStore;
47
- if (!(await store.findById(accountId)))
48
- return false;
49
- return !(supportsAccountStatus(store) && (await store.isDisabled(accountId)));
50
- },
128
+ isAccountActive,
129
+ });
130
+ }
131
+ let poppy = null;
132
+ if (config.poppy && store && conn) {
133
+ const cfg = config.poppy;
134
+ const limiter = cfg.rateLimit === false
135
+ ? null
136
+ : typeof cfg.rateLimit === 'function'
137
+ ? cfg.rateLimit
138
+ : memoryLimiter(cfg.rateLimit);
139
+ const secret = service.deriveSecret('poppy-dpop-nonce');
140
+ poppy = new PoppyService({
141
+ cfg,
142
+ urls: poppyUrls(service.config.issuer, cfg.prefix),
143
+ store: new PoppyStore(conn),
144
+ grants: store,
145
+ registry: new PoppyClientRegistry(cfg, { fetchJson: options.fetchJson }),
146
+ replay: await resolveReplay(cfg.replay, conn, options.make),
147
+ limiter,
148
+ nonces: hmacNonceSource(secret),
149
+ isAccountActive,
150
+ mcp: mcpBridge(service, cfg.apis),
151
+ now: options.now,
51
152
  });
52
153
  }
53
- return { config, urls, verifier: new PersonalAgentVerifier(config), delegation };
154
+ const revokeAgentGrant = async (accountId, grantId) => {
155
+ if (grantId.startsWith('pgrant_')) {
156
+ return poppy ? poppy.revokeAccountGrant(accountId, grantId) : false;
157
+ }
158
+ return delegation ? delegation.revokeGrant(accountId, grantId) : false;
159
+ };
160
+ const revokeAllAgentGrants = async (accountId) => {
161
+ let count = 0;
162
+ if (poppy)
163
+ count += await poppy.revokeAllAccountGrants(accountId);
164
+ if (delegation)
165
+ count += await delegation.revokeAllGrants(accountId);
166
+ return count;
167
+ };
168
+ return {
169
+ config,
170
+ urls,
171
+ verifier: new PersonalAgentVerifier(config),
172
+ delegation,
173
+ poppy,
174
+ revokeAgentGrant,
175
+ revokeAllAgentGrants,
176
+ };
54
177
  })();
55
178
  // Falha (DB indisponível) não fica em cache — a próxima request tenta de novo.
56
179
  runtime.catch(() => runtimes.delete(service));
@@ -61,5 +184,7 @@ export function buildPersonalAgentsRuntime(service, makeDb) {
61
184
  /** Runtime a partir de uma request. */
62
185
  export async function personalAgentsFor(ctx) {
63
186
  const service = await ctx.containerResolver.make('authkit.server');
64
- return buildPersonalAgentsRuntime(service, () => ctx.containerResolver.make('lucid.db'));
187
+ return buildPersonalAgentsRuntime(service, () => ctx.containerResolver.make('lucid.db'), {
188
+ make: (binding) => ctx.containerResolver.make(binding),
189
+ });
65
190
  }
@@ -8,12 +8,15 @@ import { type EventsConfigInput } from './events/dispatcher.js';
8
8
  import { type BotProtectionConfigInput, type ResolvedBotProtectionConfig } from './host/bot_protection.js';
9
9
  import { type BrandingConfig } from './host/branding.js';
10
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';
11
13
  import type { ResolveGeo } from './host/geo.js';
12
14
  import { type AuthMessages, type I18nConfig } from './host/i18n.js';
13
15
  import { type OtpLoginConfigInput, type ResolvedOtpLoginConfig } from './host/otp_login.js';
14
16
  import type { AuthHostOptions } from './host/register_auth_host.js';
15
17
  import type { SudoMethod } from './host/sudo/types.js';
16
18
  import { type ResolvedTrustedDevicesConfig, type TrustedDevicesConfigInput } from './host/trusted_device.js';
19
+ import type { WhatsappCodeSenderBinding } from './host/whatsapp_code_sender.js';
17
20
  import { type McpOAuthConfigInput, type ResolvedMcpOAuthConfig } from './mcp/mcp_oauth.js';
18
21
  import type { PatStore } from './pat/pat_store.js';
19
22
  import { type RedirectUriPolicy, type ResolvedRedirectUriPolicy, type ValidateRegistrationHook } from './provider/registration_policy.js';
@@ -759,6 +762,17 @@ export interface ResolvedInteractionRecoveryConfig {
759
762
  redirectTo?: string;
760
763
  }
761
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
+ };
762
776
  issuer: string;
763
777
  adapter: AdapterFactory;
764
778
  /**
@@ -1152,6 +1166,16 @@ export interface AuthServerConfigInput {
1152
1166
  };
1153
1167
  }
1154
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
+ };
1155
1179
  issuer: string;
1156
1180
  AdapterClass: OidcAdapterClass;
1157
1181
  /**
@@ -273,6 +273,11 @@ export function jwksAutoFallbackWarning(storePath) {
273
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.`;
274
274
  }
275
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
+ }
276
281
  return configProvider.create(async (app) => {
277
282
  const AdapterClass = await config.adapter.resolver(app);
278
283
  // `session.adapter` ausente ⇒ mesma classe do default (back-compat: o
@@ -348,6 +353,12 @@ export function defineConfig(config) {
348
353
  (personalAgents.prefix === oidcMount || personalAgents.prefix.startsWith(`${oidcMount}/`))) {
349
354
  throw new Error(`authkit: personalAgents.prefix "${personalAgents.prefix}" não pode ficar debaixo do mountPath do OIDC ("${oidcMount}").`);
350
355
  }
356
+ const poppyPrefix = personalAgents?.poppy?.prefix;
357
+ if (poppyPrefix &&
358
+ oidcMount &&
359
+ (poppyPrefix === oidcMount || poppyPrefix.startsWith(`${oidcMount}/`))) {
360
+ throw new Error(`authkit: personalAgents.poppy.prefix "${poppyPrefix}" não pode ficar debaixo do mountPath do OIDC ("${oidcMount}").`);
361
+ }
351
362
  // BACKSTOP DE SUDO. Um host cujo `sudo.methods` não tem um único método
352
363
  // satisfazível por conta sem senha fica bricado para TODA operação sob
353
364
  // `requireSudo` — e hoje isso só se descobre quando um usuário não consegue
@@ -394,6 +405,9 @@ export function defineConfig(config) {
394
405
  return acc ? { id: acc.id } : null;
395
406
  },
396
407
  accountStore: config.accountStore,
408
+ customLoginMethods: config.customLoginMethods,
409
+ mfa: config.mfa,
410
+ whatsapp: config.whatsapp,
397
411
  patStore: config.patStore,
398
412
  mountPath: config.mountPath ?? '/oidc',
399
413
  accountHome: config.accountHome,
@@ -159,7 +159,18 @@ export default class AccountApiController {
159
159
  }>;
160
160
  /** Personal agents com delegação ativa na conta (scopes aprovados). */
161
161
  listAgents(ctx: HttpContext): Promise<void | {
162
- agents: import("../../agents/delegation_service.ts").DelegationGrantSummary[];
162
+ agents: {
163
+ id: string;
164
+ clientId: string;
165
+ scopes: {
166
+ id: string;
167
+ description: string;
168
+ }[];
169
+ createdAt: Date;
170
+ updatedAt: Date;
171
+ expiresAt: Date;
172
+ protocol: string;
173
+ }[];
163
174
  }>;
164
175
  /** Revoga a delegação de um personal agent (efeito imediato nos tokens). */
165
176
  revokeAgent(ctx: HttpContext): Promise<void | {
@@ -617,20 +617,25 @@ export default class AccountApiController {
617
617
  /** Personal agents com delegação ativa na conta (scopes aprovados). */
618
618
  async listAgents(ctx) {
619
619
  const runtime = await personalAgentsFor(ctx);
620
- if (!runtime?.delegation) {
620
+ if (!runtime?.delegation && !runtime?.poppy) {
621
621
  return ctx.response
622
622
  .status(404)
623
623
  .send(apiErr('capability_unsupported', 'Personal agent delegation is not enabled.'));
624
624
  }
625
625
  const userId = ctx.session.get(ACCOUNT_SESSION_KEY);
626
- const grants = await runtime.delegation.listGrants(userId);
627
- return { agents: grants };
626
+ const pact = runtime.delegation
627
+ ? (await runtime.delegation.listGrants(userId)).map((g) => ({ ...g, protocol: 'pact' }))
628
+ : [];
629
+ const poppy = runtime.poppy
630
+ ? (await runtime.poppy.listGrants(userId)).map((g) => ({ ...g, protocol: 'poppy' }))
631
+ : [];
632
+ return { agents: [...pact, ...poppy] };
628
633
  }
629
634
  // ─── DELETE /account/api/agents/:id ──────────────────────────────────────
630
635
  /** Revoga a delegação de um personal agent (efeito imediato nos tokens). */
631
636
  async revokeAgent(ctx) {
632
637
  const runtime = await personalAgentsFor(ctx);
633
- if (!runtime?.delegation) {
638
+ if (!runtime?.delegation && !runtime?.poppy) {
634
639
  return ctx.response
635
640
  .status(404)
636
641
  .send(apiErr('capability_unsupported', 'Personal agent delegation is not enabled.'));
@@ -638,7 +643,7 @@ export default class AccountApiController {
638
643
  const service = await ctx.containerResolver.make('authkit.server');
639
644
  const userId = ctx.session.get(ACCOUNT_SESSION_KEY);
640
645
  const grantId = ctx.request.param('id');
641
- if (!(await runtime.delegation.revokeGrant(userId, grantId))) {
646
+ if (!(await runtime.revokeAgentGrant(userId, grantId))) {
642
647
  return ctx.response.status(404).send(apiErr('not_found', 'Grant not found.'));
643
648
  }
644
649
  await service.config.audit?.record({
@@ -87,13 +87,14 @@ 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. 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.
90
+ * agente agindo na conta (delegações do PACT e Account Tokens do Poppy). Sem
91
+ * `personalAgents` é no-op — e aí nem resolve o banco. Diferente do
92
+ * `recordSubRevocation`, uma falha AQUI propaga: não há outra camada que corte
93
+ * esses tokens.
93
94
  */
94
95
  async #revokeAgentGrants(accountId) {
95
96
  const runtime = await buildPersonalAgentsRuntime(this.#oidc, () => getBootedApp().container.make('lucid.db'));
96
- return runtime?.delegation ? runtime.delegation.revokeAllGrants(accountId) : 0;
97
+ return runtime ? runtime.revokeAllAgentGrants(accountId) : 0;
97
98
  }
98
99
  /**
99
100
  * Loga (best-effort, sem nunca lançar) a falha de {@link recordSubRevocation}. Prefere o
@@ -183,7 +184,9 @@ export class AdminSessionsService {
183
184
  */
184
185
  async listGrants(accountId) {
185
186
  const rows = await this.#listModel('Grant');
186
- const grants = rows.filter((r) => r.payload.accountId === accountId);
187
+ // Os grants `pgrant_` são o espelho de um Account Token do Poppy (Bearer de MCP) — aparecem e
188
+ // são revogados como personal agent, não como app OIDC.
189
+ const grants = rows.filter((r) => r.payload.accountId === accountId && !r.id.startsWith('pgrant_'));
187
190
  if (grants.length === 0)
188
191
  return [];
189
192
  const atByGrant = await this.#countByGrant('AccessToken');
@@ -35,9 +35,10 @@ export interface AuthHostRuntimeConfig {
35
35
  * Ver `deriveLockedRouteOptions`.
36
36
  */
37
37
  lockedRouteOptions?: PolicyRouteOption[];
38
- /** Personal agents ligados → prefixo das rotas deles (`/agents`). */
38
+ /** Personal agents ligados → prefixo das rotas deles (`/agents`) e do Poppy (`/poppy`). */
39
39
  personalAgents?: {
40
40
  prefix: string;
41
+ poppyPrefix?: string;
41
42
  };
42
43
  /**
43
44
  * API headless (Clerk-style). Presente quando o host a declarou — o provider o
@@ -28,7 +28,7 @@ export default class AccountAppsController {
28
28
  const revoked = ctx.session.flashMessages.get('appRevoked');
29
29
  // Personal agents com delegação ativa (só quando a feature está ligada).
30
30
  const agentsRuntime = await personalAgentsFor(ctx);
31
- const agents = agentsRuntime?.delegation
31
+ const pactAgents = agentsRuntime?.delegation
32
32
  ? await Promise.all((await agentsRuntime.delegation.listGrants(accountId)).map(async (g) => ({
33
33
  id: g.id,
34
34
  name: displayNameFor((await agentsRuntime.config.resolveAgent(g.clientId)) ?? {
@@ -38,6 +38,16 @@ export default class AccountAppsController {
38
38
  scopes: g.scopes,
39
39
  })))
40
40
  : null;
41
+ // Account Tokens do Poppy: o agente é desconectado pela mesma lista (§4.9).
42
+ const poppy = agentsRuntime?.poppy;
43
+ const poppyAgents = poppy
44
+ ? await Promise.all((await poppy.listGrants(accountId)).map(async (g) => ({
45
+ id: g.id,
46
+ name: await poppy.registry.displayName(g.clientId),
47
+ scopes: g.scopes,
48
+ })))
49
+ : null;
50
+ const agents = pactAgents || poppyAgents ? [...(pactAgents ?? []), ...(poppyAgents ?? [])] : null;
41
51
  return render(ctx, 'account/apps', {
42
52
  csrfToken: ctx.request.csrfToken,
43
53
  supported,
@@ -78,13 +88,13 @@ export default class AccountAppsController {
78
88
  /** POST /account/apps/agents/:grantId/revoke — revoga a delegação de um personal agent. */
79
89
  async revokeAgent(ctx) {
80
90
  const runtime = await personalAgentsFor(ctx);
81
- if (!runtime?.delegation)
91
+ if (!runtime?.delegation && !runtime?.poppy)
82
92
  return ctx.response.notFound();
83
93
  const service = await ctx.containerResolver.make('authkit.server');
84
94
  const cfg = service.config;
85
95
  const accountId = ctx.session.get(ACCOUNT_SESSION_KEY);
86
96
  const grantId = ctx.request.param('grantId');
87
- if (await runtime.delegation.revokeGrant(accountId, grantId)) {
97
+ if (await runtime.revokeAgentGrant(accountId, grantId)) {
88
98
  await cfg.audit?.record({
89
99
  type: 'agent.grant_revoked',
90
100
  accountId,
@@ -158,15 +158,19 @@ export default class AccountSecurityController {
158
158
  return ctx.response.redirect(getAccountLoginUrl());
159
159
  }
160
160
  const { currentPassword, confirmEmail } = await ctx.request.validateUsing(deleteAccountValidator);
161
- // Confirmação: senha atual correta OU e-mail digitado batendo com o da conta
162
- // (case-insensitive). Sem nenhuma das duas → recusa (não deleta).
161
+ // Confirmação: senha atual correta OU e-mail digitado batendo com o da conta.
162
+ // Contas sem e-mail podem confirmar o telefone já verificado pelo host.
163
163
  let confirmed = false;
164
164
  if (currentPassword) {
165
165
  confirmed = !!(await store.verifyCredentials(account.email, currentPassword));
166
166
  }
167
167
  if (!confirmed && confirmEmail) {
168
- confirmed =
169
- normalizeEmailIdentifier(confirmEmail) === normalizeEmailIdentifier(account.email);
168
+ confirmed = account.email
169
+ ? normalizeEmailIdentifier(confirmEmail) === normalizeEmailIdentifier(account.email)
170
+ : !!account.phone &&
171
+ /^[+\d\s().-]+$/.test(confirmEmail) &&
172
+ /\d/.test(confirmEmail) &&
173
+ confirmEmail.replace(/\D/g, '') === account.phone.replace(/\D/g, '');
170
174
  }
171
175
  if (!confirmed) {
172
176
  ctx.session.flash('deleteError', translate(cfg.messages, 'account.delete.invalid_confirmation'));
@@ -1,7 +1,14 @@
1
1
  import '../augmentations.js';
2
2
  import type { HttpContext } from '@adonisjs/core/http';
3
+ import { type CustomLoginIdentity } from '../custom_login.js';
3
4
  export default class AuthInteractionController {
4
5
  #private;
6
+ /** Shared completion for trusted host authentication; primary proof is the host's responsibility. */
7
+ completeCustomLogin(ctx: HttpContext, input: CustomLoginIdentity & {
8
+ method: string;
9
+ factorId?: string;
10
+ passwordless?: boolean;
11
+ }): Promise<unknown>;
5
12
  show(ctx: HttpContext): Promise<any>;
6
13
  /**
7
14
  * POST /auth/interaction/:uid/identifier
@@ -29,7 +36,12 @@ export default class AuthInteractionController {
29
36
  * 2º fator: lê o accountId pendente da sessão e aceita um código TOTP (`code`)
30
37
  * OU um recovery code (`recoveryCode`). Em caso de sucesso finaliza a interaction.
31
38
  */
32
- mfaVerify(ctx: HttpContext): Promise<any>;
39
+ private renderMfaChallenge;
40
+ private configuredMfaGate;
41
+ customMfaBegin(ctx: HttpContext): Promise<unknown>;
42
+ customMfaVerify(ctx: HttpContext): Promise<unknown>;
43
+ private finishVerifiedMfa;
44
+ mfaVerify(ctx: HttpContext): Promise<unknown>;
33
45
  /**
34
46
  * true se o authorize request exige MFA via acr_values (contém o mfaAcr da
35
47
  * config de step-up). `acr_values` é a string separada por espaços padrão OIDC.