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

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 (74) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +4 -5
  3. package/build/index.d.ts +5 -11
  4. package/build/index.js +4 -9
  5. package/build/providers/authkit_server_provider.js +6 -8
  6. package/build/src/agents/config.d.ts +9 -13
  7. package/build/src/agents/config.js +17 -11
  8. package/build/src/agents/delegation_service.js +3 -3
  9. package/build/src/agents/delegation_store.d.ts +3 -2
  10. package/build/src/agents/delegation_store.js +6 -5
  11. package/build/src/agents/extension.d.ts +261 -0
  12. package/build/src/agents/extension.js +40 -0
  13. package/build/src/agents/middleware.js +1 -1
  14. package/build/src/agents/protocol.d.ts +4 -4
  15. package/build/src/agents/registry_store.d.ts +3 -2
  16. package/build/src/agents/registry_store.js +1 -1
  17. package/build/src/agents/runtime.d.ts +26 -7
  18. package/build/src/agents/runtime.js +110 -75
  19. package/build/src/audit/audit_sink.js +1 -1
  20. package/build/src/define_config.d.ts +0 -12
  21. package/build/src/define_config.js +9 -7
  22. package/build/src/host/account_api/account_api_controller.js +8 -6
  23. package/build/src/host/account_session_sign_in.d.ts +12 -0
  24. package/build/src/host/account_session_sign_in.js +19 -0
  25. package/build/src/host/admin_console/console_personal_agents_controller.d.ts +1 -6
  26. package/build/src/host/admin_console/console_personal_agents_controller.js +50 -30
  27. package/build/src/host/admin_sessions_service.js +8 -5
  28. package/build/src/host/auth_host_config.d.ts +6 -2
  29. package/build/src/host/controllers/account_apps_controller.js +15 -11
  30. package/build/src/host/csrf.d.ts +3 -3
  31. package/build/src/host/csrf.js +3 -13
  32. package/build/src/host/default_mailer.d.ts +11 -6
  33. package/build/src/host/default_mailer.js +16 -12
  34. package/build/src/host/i18n.d.ts +3 -14
  35. package/build/src/host/i18n.js +15 -13
  36. package/build/src/host/register_auth_host.js +26 -61
  37. package/build/src/host/ui-dist/assets/index-BqyJYRVX.js +67 -0
  38. package/build/src/host/ui-dist/index.html +1 -1
  39. package/build/src/provider/oidc_service.d.ts +1 -1
  40. package/build/src/provider/oidc_service.js +1 -1
  41. package/build/src/schema/ensure.d.ts +14 -0
  42. package/build/src/schema/ensure.js +43 -150
  43. package/package.json +2 -2
  44. package/build/src/agents/poppy/client_registry.d.ts +0 -50
  45. package/build/src/agents/poppy/client_registry.js +0 -260
  46. package/build/src/agents/poppy/config.d.ts +0 -396
  47. package/build/src/agents/poppy/config.js +0 -228
  48. package/build/src/agents/poppy/dpop.d.ts +0 -61
  49. package/build/src/agents/poppy/dpop.js +0 -158
  50. package/build/src/agents/poppy/email_code_sign_in.d.ts +0 -27
  51. package/build/src/agents/poppy/email_code_sign_in.js +0 -140
  52. package/build/src/agents/poppy/errors.d.ts +0 -25
  53. package/build/src/agents/poppy/errors.js +0 -25
  54. package/build/src/agents/poppy/replay.d.ts +0 -25
  55. package/build/src/agents/poppy/replay.js +0 -106
  56. package/build/src/agents/poppy/resource.d.ts +0 -100
  57. package/build/src/agents/poppy/resource.js +0 -129
  58. package/build/src/agents/poppy/safe_fetch.d.ts +0 -48
  59. package/build/src/agents/poppy/safe_fetch.js +0 -262
  60. package/build/src/agents/poppy/service.d.ts +0 -293
  61. package/build/src/agents/poppy/service.js +0 -1458
  62. package/build/src/agents/poppy/session_jwt.d.ts +0 -30
  63. package/build/src/agents/poppy/session_jwt.js +0 -133
  64. package/build/src/agents/poppy/store.d.ts +0 -106
  65. package/build/src/agents/poppy/store.js +0 -261
  66. package/build/src/agents/poppy/web_session.d.ts +0 -39
  67. package/build/src/agents/poppy/web_session.js +0 -122
  68. package/build/src/host/controllers/poppy_consent_controller.d.ts +0 -19
  69. package/build/src/host/controllers/poppy_consent_controller.js +0 -199
  70. package/build/src/host/controllers/poppy_controller.d.ts +0 -33
  71. package/build/src/host/controllers/poppy_controller.js +0 -219
  72. package/build/src/host/poppy_session_key.d.ts +0 -9
  73. package/build/src/host/poppy_session_key.js +0 -9
  74. package/build/src/host/ui-dist/assets/index-D0EIeyKK.js +0 -67
@@ -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
@@ -1,4 +1,5 @@
1
1
  import type { HttpContext } from '@adonisjs/core/http';
2
+ import type { ResolvedPersonalAgentExtension } from '../agents/extension.js';
2
3
  import type { AuthSocialConfig, ResolvedRateLimitConfig } from '../define_config.js';
3
4
  import type { PolicyRouteOption } from './config_locks.js';
4
5
  import type { AuthHostOptions } from './register_auth_host.js';
@@ -35,10 +36,13 @@ export interface AuthHostRuntimeConfig {
35
36
  * Ver `deriveLockedRouteOptions`.
36
37
  */
37
38
  lockedRouteOptions?: PolicyRouteOption[];
38
- /** Personal agents ligados → prefixo das rotas deles (`/agents`) e do Poppy (`/poppy`). */
39
+ /**
40
+ * Personal agents ligados → prefixo das rotas do PACT (`/agents`) e as
41
+ * `personalAgents.extensions` resolvidas (rotas, isenções de CSRF, navegador do agente).
42
+ */
39
43
  personalAgents?: {
40
44
  prefix: string;
41
- poppyPrefix?: string;
45
+ extensions?: readonly ResolvedPersonalAgentExtension[];
42
46
  };
43
47
  /**
44
48
  * `accountSession.acceptIdpSession: false` com `authMethods: { password: false }`:
@@ -38,16 +38,19 @@ export default class AccountAppsController {
38
38
  scopes: g.scopes,
39
39
  })))
40
40
  : null;
41
- // Refresh tokens do Poppy: o agente é desconectado pela mesma lista (§5.5).
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
+ // Conexões das `personalAgents.extensions`: desconectadas pela mesma lista.
42
+ let extensionAgents = null;
43
+ for (const { runtime } of agentsRuntime?.extensions.values() ?? []) {
44
+ extensionAgents ??= [];
45
+ for (const g of await runtime.listAccountGrants(accountId)) {
46
+ extensionAgents.push({
47
+ id: g.id,
48
+ name: await runtime.displayName(g.clientId),
49
+ scopes: g.scopes,
50
+ });
51
+ }
52
+ }
53
+ const agents = pactAgents || extensionAgents ? [...(pactAgents ?? []), ...(extensionAgents ?? [])] : null;
51
54
  return render(ctx, 'account/apps', {
52
55
  csrfToken: ctx.request.csrfToken,
53
56
  supported,
@@ -89,8 +92,9 @@ export default class AccountAppsController {
89
92
  /** POST /account/apps/agents/:grantId/revoke — revoga a delegação de um personal agent. */
90
93
  async revokeAgent(ctx) {
91
94
  const runtime = await personalAgentsFor(ctx);
92
- if (!runtime?.delegation && !runtime?.poppy)
95
+ if (!runtime || (!runtime.delegation && runtime.extensions.size === 0)) {
93
96
  return ctx.response.notFound();
97
+ }
94
98
  const service = await ctx.containerResolver.make('authkit.server');
95
99
  const cfg = service.config;
96
100
  const accountId = ctx.session.get(ACCOUNT_SESSION_KEY);
@@ -11,10 +11,10 @@ export interface AuthkitCsrfOptions {
11
11
  */
12
12
  personalAgentsPrefix?: string | false;
13
13
  /**
14
- * Prefixo do Poppy: isenta token/revoke/device authorization e a entrada do navegador
15
- * (o código com o cookie `SameSite=Strict` é o que protege, §6).
14
+ * Isenta os `csrfExemptPaths` das `personalAgents.extensions` do config. Default: true.
15
+ * `false` desliga.
16
16
  */
17
- poppyPrefix?: string | false;
17
+ personalAgentExtensions?: boolean;
18
18
  }
19
19
  /**
20
20
  * Retorna `true` quando `url` é uma rota AuthKit que deve ser ISENTA de CSRF
@@ -16,7 +16,6 @@
16
16
  * ```
17
17
  */
18
18
  import { normalizePersonalAgentsPrefix } from '../agents/config.js';
19
- import { normalizePoppyPrefix } from '../agents/poppy/config.js';
20
19
  import { getAuthHostConfig } from './auth_host_config.js';
21
20
  /**
22
21
  * Retorna `true` quando `url` é uma rota AuthKit que deve ser ISENTA de CSRF
@@ -34,21 +33,12 @@ export function authkitCsrfExceptions(url, options = {}) {
34
33
  ? normalizePersonalAgentsPrefix(options.personalAgentsPrefix)
35
34
  : getAuthHostConfig()?.personalAgents?.prefix;
36
35
  const agentsOAuth = agentsPrefix ? `${agentsPrefix}/oauth/` : null;
37
- const poppyPrefix = options.poppyPrefix === false
38
- ? undefined
39
- : options.poppyPrefix !== undefined
40
- ? normalizePoppyPrefix(options.poppyPrefix)
41
- : getAuthHostConfig()?.personalAgents?.poppyPrefix;
42
36
  const path = url.split('?')[0];
43
- const poppyExempt = !!poppyPrefix &&
44
- (path === `${poppyPrefix}/oauth/token` ||
45
- path === `${poppyPrefix}/oauth/revoke` ||
46
- path === `${poppyPrefix}/oauth/device` ||
47
- path === `${poppyPrefix}/browser-session` ||
48
- path === `${poppyPrefix}/browser-session/authorize`);
37
+ const extensionExempt = options.personalAgentExtensions !== false &&
38
+ (getAuthHostConfig()?.personalAgents?.extensions ?? []).some((e) => e.csrfExemptPaths?.includes(path));
49
39
  return (url.includes(mountPath) ||
50
40
  url.includes('/authkit/pat') ||
51
41
  (backchannel !== null && url === backchannel) ||
52
42
  (agentsOAuth !== null && url.startsWith(agentsOAuth)) ||
53
- poppyExempt);
43
+ extensionExempt);
54
44
  }
@@ -67,15 +67,20 @@ export declare function sendMagicLinkEmail(ctx: HttpContext, data: {
67
67
  channel?: 'code' | 'link';
68
68
  }): Promise<void>;
69
69
  /**
70
- * Envia o código do Mediated Sign-In pronto do Poppy (`poppyEmailCodeSignIn`) pelo
71
- * mailer default do host. Só código, sem link: quem digita é a pessoa, no agente.
72
- * Best-effort: no fallback (sem mail) loga o código, como o magic link loga o link; nunca lança.
70
+ * Envia um e-mail só com um código (sem link: quem digita é a pessoa, em outro lugar) pelo mailer
71
+ * default do host, com o branding e o i18n do authkit. Os textos saem do catálogo pelas chaves
72
+ * `{messages}.subject`, `.heading`, `.intro`, `.code_label` e `.fallback`, com `params`
73
+ * interpolados — uma extension registra as dela em `messages`. Best-effort: sem `@adonisjs/mail`
74
+ * loga o código (dev); nunca lança.
75
+ *
76
+ * Estável no 1.x (é o mailer das `personalAgents.extensions`).
73
77
  */
74
- export declare function sendPoppySignInCodeEmail(ctx: HttpContext, data: {
78
+ export declare function sendAuthkitCodeEmail(ctx: HttpContext, data: {
75
79
  email: string;
76
80
  code: string;
77
- agentName: string;
78
- expiresInMinutes: number;
81
+ /** Prefixo das chaves no catálogo (`'mail.poppy_sign_in'`). */
82
+ messages: string;
83
+ params?: Record<string, string | number>;
79
84
  }): Promise<void>;
80
85
  /**
81
86
  * Envia o link de CONFIRMAÇÃO DE IDENTIDADE (sudo) pelo mailer default do host.
@@ -295,32 +295,36 @@ export async function sendMagicLinkEmail(ctx, data) {
295
295
  }
296
296
  }
297
297
  /**
298
- * Envia o código do Mediated Sign-In pronto do Poppy (`poppyEmailCodeSignIn`) pelo
299
- * mailer default do host. Só código, sem link: quem digita é a pessoa, no agente.
300
- * Best-effort: no fallback (sem mail) loga o código, como o magic link loga o link; nunca lança.
298
+ * Envia um e-mail só com um código (sem link: quem digita é a pessoa, em outro lugar) pelo mailer
299
+ * default do host, com o branding e o i18n do authkit. Os textos saem do catálogo pelas chaves
300
+ * `{messages}.subject`, `.heading`, `.intro`, `.code_label` e `.fallback`, com `params`
301
+ * interpolados — uma extension registra as dela em `messages`. Best-effort: sem `@adonisjs/mail`
302
+ * loga o código (dev); nunca lança.
303
+ *
304
+ * Estável no 1.x (é o mailer das `personalAgents.extensions`).
301
305
  */
302
- export async function sendPoppySignInCodeEmail(ctx, data) {
306
+ export async function sendAuthkitCodeEmail(ctx, data) {
303
307
  try {
304
308
  const brand = resolveBrand(ctx);
305
309
  const { messages: t, locale } = resolveMailMessages(ctx);
306
- const params = { agent: data.agentName, minutes: data.expiresInMinutes };
310
+ const key = (name) => `${data.messages}.${name}`;
307
311
  const content = renderTransactionalEmail({
308
312
  brand,
309
313
  locale,
310
- subject: translate(t, 'mail.poppy_sign_in.subject', params),
311
- heading: translate(t, 'mail.poppy_sign_in.heading', params),
312
- intro: translate(t, 'mail.poppy_sign_in.intro', params),
313
- footnote: translate(t, 'mail.poppy_sign_in.fallback'),
314
+ subject: translate(t, key('subject'), data.params),
315
+ heading: translate(t, key('heading'), data.params),
316
+ intro: translate(t, key('intro'), data.params),
317
+ footnote: translate(t, key('fallback'), data.params),
314
318
  code: data.code,
315
- codeLabel: translate(t, 'mail.poppy_sign_in.code_label'),
319
+ codeLabel: translate(t, key('code_label'), data.params),
316
320
  });
317
321
  const sent = await sendEmail(ctx, data.email, content);
318
322
  if (!sent) {
319
- ctx.logger.info({ email: data.email, agent: data.agentName, code: data.code }, 'authkit: código do Poppy Mediated Sign-In (dev — @adonisjs/mail ausente)');
323
+ ctx.logger.info({ email: data.email, code: data.code, messages: data.messages, ...(data.params ?? {}) }, 'authkit: código por e-mail (dev — @adonisjs/mail ausente)');
320
324
  }
321
325
  }
322
326
  catch (error) {
323
- ctx.logger.error({ err: error, email: data.email }, 'authkit: falha ao enviar o código do Poppy Mediated Sign-In');
327
+ ctx.logger.error({ err: error, email: data.email, messages: data.messages }, 'authkit: falha ao enviar o e-mail com código');
324
328
  }
325
329
  }
326
330
  /**
@@ -560,11 +560,6 @@ export declare const DEFAULT_MESSAGES: {
560
560
  'mail.magic_link.code_subject': string;
561
561
  'mail.magic_link.code_intro': string;
562
562
  'mail.magic_link.code_only_label': string;
563
- 'mail.poppy_sign_in.subject': string;
564
- 'mail.poppy_sign_in.heading': string;
565
- 'mail.poppy_sign_in.intro': string;
566
- 'mail.poppy_sign_in.code_label': string;
567
- 'mail.poppy_sign_in.fallback': string;
568
563
  'mail.sudo_link.subject': string;
569
564
  'mail.sudo_link.heading': string;
570
565
  'mail.sudo_link.intro': string;
@@ -1318,11 +1313,6 @@ export declare const PT_BR_MESSAGES: {
1318
1313
  'mail.magic_link.code_subject': string;
1319
1314
  'mail.magic_link.code_intro': string;
1320
1315
  'mail.magic_link.code_only_label': string;
1321
- 'mail.poppy_sign_in.subject': string;
1322
- 'mail.poppy_sign_in.heading': string;
1323
- 'mail.poppy_sign_in.intro': string;
1324
- 'mail.poppy_sign_in.code_label': string;
1325
- 'mail.poppy_sign_in.fallback': string;
1326
1316
  'mail.sudo_link.subject': string;
1327
1317
  'mail.sudo_link.heading': string;
1328
1318
  'mail.sudo_link.intro': string;
@@ -1547,11 +1537,10 @@ export declare const PT_BR_MESSAGES: {
1547
1537
  */
1548
1538
  export declare const BUILTIN_MESSAGES: Record<string, AuthMessages>;
1549
1539
  /**
1550
- * Resolve o catálogo ativo. Começa do catálogo embutido do locale selecionado
1551
- * (ou do default `en` quando o locale não é embutido), depois mescla os
1552
- * overrides do host por cima. Sem config, retorna o default `en` intacto.
1553
- * Chaves omitidas caem no default `en` (fallback de cobertura).
1540
+ * Soma os catálogos de uma extension (`messages`) aos embutidos. Chamado quando o config resolve;
1541
+ * os overrides do host (`i18n.messages`) continuam vencendo.
1554
1542
  */
1543
+ export declare function registerExtensionMessages(catalogs: Readonly<Record<string, Readonly<Record<string, string>>>>): void;
1555
1544
  export declare function resolveMessages(i18n?: I18nConfig): AuthMessages;
1556
1545
  /**
1557
1546
  * Retorna a string para `key` (cai na própria `key` quando ausente) com
@@ -603,12 +603,6 @@ export const DEFAULT_MESSAGES = {
603
603
  'mail.magic_link.code_subject': 'Your login code',
604
604
  'mail.magic_link.code_intro': 'Use the code below to sign in. It expires shortly and can be used once.',
605
605
  'mail.magic_link.code_only_label': 'Enter this code to sign in:',
606
- // Código do Mediated Sign-In do Poppy: um personal agent pede para entrar na conta.
607
- 'mail.poppy_sign_in.subject': 'Your code to connect {agent}',
608
- 'mail.poppy_sign_in.heading': 'Connect {agent} to your account',
609
- 'mail.poppy_sign_in.intro': '{agent} asked to sign in to your account. If that was you, give it the code below. It expires in {minutes} minutes and works once.',
610
- 'mail.poppy_sign_in.code_label': 'Your code:',
611
- 'mail.poppy_sign_in.fallback': 'If you did not ask for this, ignore this email — nobody gets in without the code. Never share it with anyone else.',
612
606
  // Link de CONFIRMAÇÃO DE IDENTIDADE (sudo). Copy DISTINTA da do magic link de
613
607
  // login de propósito: este link não autentica ninguém — quem o abre já está
614
608
  // logado e só reprova que é quem diz ser. Prometer "entre na sua conta" num
@@ -1447,12 +1441,6 @@ export const PT_BR_MESSAGES = {
1447
1441
  'mail.magic_link.code_subject': 'Seu código de login',
1448
1442
  'mail.magic_link.code_intro': 'Use o código abaixo para entrar. Ele expira em instantes e serve para um único acesso.',
1449
1443
  'mail.magic_link.code_only_label': 'Digite este código para entrar:',
1450
- // Código do Mediated Sign-In do Poppy — ver o comentário no catálogo `en`.
1451
- 'mail.poppy_sign_in.subject': 'Seu código para conectar {agent}',
1452
- 'mail.poppy_sign_in.heading': 'Conecte {agent} à sua conta',
1453
- 'mail.poppy_sign_in.intro': '{agent} pediu para entrar na sua conta. Se foi você, informe o código abaixo. Ele expira em {minutes} minutos e serve uma única vez.',
1454
- 'mail.poppy_sign_in.code_label': 'Seu código:',
1455
- 'mail.poppy_sign_in.fallback': 'Se não foi você, ignore este e-mail — ninguém entra sem o código. Nunca o compartilhe com outra pessoa.',
1456
1444
  // Link de CONFIRMAÇÃO DE IDENTIDADE (sudo) — ver o comentário no catálogo `en`.
1457
1445
  'mail.sudo_link.subject': 'Confirme que é você',
1458
1446
  'mail.sudo_link.heading': 'Confirme sua identidade',
@@ -1713,13 +1701,27 @@ export const BUILTIN_MESSAGES = {
1713
1701
  * overrides do host por cima. Sem config, retorna o default `en` intacto.
1714
1702
  * Chaves omitidas caem no default `en` (fallback de cobertura).
1715
1703
  */
1704
+ /** Catálogos das `personalAgents.extensions`, por locale (ver {@link registerExtensionMessages}). */
1705
+ const extensionMessages = {};
1706
+ /**
1707
+ * Soma os catálogos de uma extension (`messages`) aos embutidos. Chamado quando o config resolve;
1708
+ * os overrides do host (`i18n.messages`) continuam vencendo.
1709
+ */
1710
+ export function registerExtensionMessages(catalogs) {
1711
+ for (const [locale, messages] of Object.entries(catalogs)) {
1712
+ extensionMessages[locale] = { ...(extensionMessages[locale] ?? {}), ...messages };
1713
+ }
1714
+ }
1716
1715
  export function resolveMessages(i18n) {
1717
1716
  const locale = i18n?.locale ?? DEFAULT_LOCALE;
1718
1717
  // Base: sempre o default `en` para garantir cobertura total das chaves; o
1719
- // catálogo embutido do locale (ex.: pt-BR) é mesclado por cima.
1718
+ // catálogo embutido do locale (ex.: pt-BR) é mesclado por cima. Os das extensions entram
1719
+ // em cada camada, abaixo dos overrides do host.
1720
1720
  const base = {
1721
1721
  ...DEFAULT_MESSAGES,
1722
+ ...(extensionMessages[DEFAULT_LOCALE] ?? {}),
1722
1723
  ...(BUILTIN_MESSAGES[locale] ?? {}),
1724
+ ...(locale === DEFAULT_LOCALE ? {} : (extensionMessages[locale] ?? {})),
1723
1725
  };
1724
1726
  const overrides = i18n?.messages?.[locale];
1725
1727
  if (!overrides)
@@ -12,7 +12,6 @@ import { getAuthHostConfig, markAuthHostAutoMounted, wasAuthHostAutoMounted, } f
12
12
  import { consoleDeniedPage } from './console_denied_page.js';
13
13
  import { CONSOLE_SSO_CALLBACK_PATH, CONSOLE_SSO_START_PATH } from './console_sso.js';
14
14
  import { endBridgedIdpSession, ensureConsoleSession } from './idp_session_bridge.js';
15
- import { POPPY_WEB_SESSION_KEY } from './poppy_session_key.js';
16
15
  import { createAuthThrottles } from './rate_limit.js';
17
16
  import { redirectExact } from './redirect_exact.js';
18
17
  import { requestPathWithQuery } from './request_url.js';
@@ -121,10 +120,21 @@ function buildLoginRedirect(ctx, extra) {
121
120
  * classe NÃO era aplicada em runtime num grupo, deixando /account/tokens e
122
121
  * /account/mfa acessíveis sem sessão.
123
122
  */
124
- /** A sessão é do navegador de um personal agent Poppy (§5)? */
125
- function isPoppyBrowser(ctx) {
126
- const marker = ctx.session?.get?.(POPPY_WEB_SESSION_KEY);
127
- return typeof marker === 'object' && marker !== null && typeof marker.sessionId === 'string';
123
+ /**
124
+ * A sessão é do navegador de um personal agent (`isAgentBrowser` de uma extension)? Um hook que
125
+ * lança conta como "é": na dúvida, o console fica fechado.
126
+ */
127
+ function isAgentBrowser(ctx) {
128
+ for (const extension of getAuthHostConfig()?.personalAgents?.extensions ?? []) {
129
+ try {
130
+ if (extension.isAgentBrowser?.(ctx))
131
+ return true;
132
+ }
133
+ catch {
134
+ return true;
135
+ }
136
+ }
137
+ return false;
128
138
  }
129
139
  /**
130
140
  * `true` when the caller wants JSON (an `Accept` that prefers `application/json`,
@@ -149,9 +159,9 @@ function unauthenticated(ctx, redirectTo) {
149
159
  return redirectExact(ctx.response, redirectTo);
150
160
  }
151
161
  const accountGuard = async (ctx, next) => {
152
- // O navegador de um personal agent Poppy (§5) não entra no console de conta nem
153
- // nas telas de consentimento: a sessão é do agente, não do usuário.
154
- if (isPoppyBrowser(ctx))
162
+ // O navegador de um personal agent não entra no console de conta nem nas telas de
163
+ // consentimento: a sessão é do agente, não do usuário.
164
+ if (isAgentBrowser(ctx))
155
165
  return ctx.response.status(403).send('');
156
166
  // Sessão do console — ou, com `accountSession.acceptIdpSession`, a do IdP (SSO).
157
167
  if (!(await ensureConsoleSession(ctx))) {
@@ -186,7 +196,7 @@ export const adminGuard = async (ctx, next) => {
186
196
  if (!cfg.admin.enabled) {
187
197
  return ctx.response.notFound();
188
198
  }
189
- if (isPoppyBrowser(ctx))
199
+ if (isAgentBrowser(ctx))
190
200
  return ctx.response.status(403).send('');
191
201
  // Com `accountSession.acceptIdpSession`, a sessão do IdP também abre o console.
192
202
  await ensureConsoleSession(ctx);
@@ -289,8 +299,6 @@ const C = {
289
299
  accountConfirm: () => import('./controllers/account_confirm_controller.js'),
290
300
  agentOAuth: () => import('./controllers/agent_oauth_controller.js'),
291
301
  agentConsent: () => import('./controllers/agent_consent_controller.js'),
292
- poppy: () => import('./controllers/poppy_controller.js'),
293
- poppyConsent: () => import('./controllers/poppy_consent_controller.js'),
294
302
  webauthnAsset: () => import('./controllers/webauthn_asset_controller.js'),
295
303
  logoutAsset: () => import('./controllers/logout_asset_controller.js'),
296
304
  passkeyAutofillAsset: () => import('./controllers/passkey_autofill_asset_controller.js'),
@@ -671,56 +679,13 @@ export function registerAuthHost(router, opts = {}) {
671
679
  })
672
680
  .use([accountGuard]);
673
681
  }
674
- // Personal Agent Protocol (Poppy): descoberta, authorization server (token,
675
- // revogação, device authorization), Mediated Sign-In e sessão de navegador —
676
- // server-to-server ou POST cross-site do navegador do agente (sem CSRF, ver
677
- // `authkitCsrfExceptions`). As telas do usuário (authorize/device) exigem a
678
- // sessão de conta e mantêm o CSRF.
679
- const poppyPrefix = hostCfg?.personalAgents?.poppyPrefix;
680
- if (poppyPrefix) {
681
- // Nomes explícitos: o Router nomeia pela dupla controller.método, e as duas URLs da metadata
682
- // (RFC 8414 na raiz e debaixo do issuer) usam o mesmo — "poppy.metadata already exists" no boot.
683
- router.get('/.well-known/poppy.json', [C.poppy, 'discovery']).as('authkit.poppy.discovery');
684
- router
685
- .get('/.well-known/oauth-protected-resource', [C.poppy, 'protectedResource'])
686
- .as('authkit.poppy.protected_resource');
687
- router
688
- .get(`/.well-known/oauth-authorization-server${poppyPrefix}`, [C.poppy, 'metadata'])
689
- .as('authkit.poppy.metadata');
690
- router
691
- .get(`${poppyPrefix}/.well-known/oauth-authorization-server`, [C.poppy, 'metadata'])
692
- .as('authkit.poppy.metadata_under_issuer');
693
- router.post(`${poppyPrefix}/oauth/token`, [C.poppy, 'token']).as('authkit.poppy.token');
694
- router.post(`${poppyPrefix}/oauth/revoke`, [C.poppy, 'revoke']).as('authkit.poppy.revoke');
695
- router
696
- .post(`${poppyPrefix}/oauth/device`, [C.poppy, 'deviceAuthorization'])
697
- .as('authkit.poppy.device_authorization');
698
- // A página grava um pedido sem autenticação: throttle por IP.
699
- withLogin(router
700
- .get(`${poppyPrefix}/browser-session`, [C.poppy, 'browserSessionPage'])
701
- .as('authkit.poppy.browser_session_page'));
702
- router
703
- .post(`${poppyPrefix}/browser-session`, [C.poppy, 'browserSession'])
704
- .as('authkit.poppy.browser_session');
705
- router
706
- .post(`${poppyPrefix}/browser-session/authorize`, [C.poppy, 'browserSessionAuthorize'])
707
- .as('authkit.poppy.browser_session_authorize');
708
- router
709
- .group(() => {
710
- router
711
- .get(`${poppyPrefix}/oauth/authorize`, [C.poppyConsent, 'authorize'])
712
- .as('authkit.poppy.authorize');
713
- router
714
- .post(`${poppyPrefix}/oauth/authorize`, [C.poppyConsent, 'decideAuthorization'])
715
- .as('authkit.poppy.authorize_decide');
716
- withOtpLogin(router
717
- .get(`${poppyPrefix}/device`, [C.poppyConsent, 'device'])
718
- .as('authkit.poppy.device'));
719
- withOtpLogin(router
720
- .post(`${poppyPrefix}/device`, [C.poppyConsent, 'decideDevice'])
721
- .as('authkit.poppy.device_decide'));
722
- })
723
- .use([accountGuard]);
682
+ // `personalAgents.extensions`: cada uma monta as rotas dela, com o guard de conta e os
683
+ // buckets de rate limit das telas embutidas.
684
+ for (const extension of hostCfg?.personalAgents?.extensions ?? []) {
685
+ extension.routes?.(router, {
686
+ accountGuard,
687
+ throttle: { login: (route) => withLogin(route), otp: (route) => withOtpLogin(route) },
688
+ });
724
689
  }
725
690
  // Paths do console de conta (configuráveis/localizáveis via `accountRoutes`).
726
691
  // As TELAS vêm de `accountPath(key)` (prefixo + segmento configurável); os