@adonis-agora/authkit-server 0.77.0 → 0.79.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 (67) hide show
  1. package/README.md +4 -0
  2. package/build/host/views/agents/consent.edge +13 -2
  3. package/build/host/views/agents/done.edge +3 -0
  4. package/build/index.d.ts +10 -0
  5. package/build/index.js +8 -0
  6. package/build/providers/authkit_server_provider.js +15 -1
  7. package/build/src/agents/config.d.ts +22 -3
  8. package/build/src/agents/config.js +22 -4
  9. package/build/src/agents/delegation_service.js +18 -8
  10. package/build/src/agents/delegation_store.d.ts +16 -0
  11. package/build/src/agents/delegation_store.js +44 -0
  12. package/build/src/agents/middleware.js +2 -1
  13. package/build/src/agents/poppy/client_registry.d.ts +45 -0
  14. package/build/src/agents/poppy/client_registry.js +226 -0
  15. package/build/src/agents/poppy/config.d.ts +365 -0
  16. package/build/src/agents/poppy/config.js +208 -0
  17. package/build/src/agents/poppy/dpop.d.ts +61 -0
  18. package/build/src/agents/poppy/dpop.js +158 -0
  19. package/build/src/agents/poppy/errors.d.ts +20 -0
  20. package/build/src/agents/poppy/errors.js +22 -0
  21. package/build/src/agents/poppy/replay.d.ts +25 -0
  22. package/build/src/agents/poppy/replay.js +106 -0
  23. package/build/src/agents/poppy/resource.d.ts +95 -0
  24. package/build/src/agents/poppy/resource.js +125 -0
  25. package/build/src/agents/poppy/safe_fetch.d.ts +48 -0
  26. package/build/src/agents/poppy/safe_fetch.js +256 -0
  27. package/build/src/agents/poppy/service.d.ts +286 -0
  28. package/build/src/agents/poppy/service.js +1464 -0
  29. package/build/src/agents/poppy/store.d.ts +111 -0
  30. package/build/src/agents/poppy/store.js +269 -0
  31. package/build/src/agents/poppy/web_session.d.ts +32 -0
  32. package/build/src/agents/poppy/web_session.js +104 -0
  33. package/build/src/agents/protocol.d.ts +8 -4
  34. package/build/src/agents/registry_store.d.ts +41 -0
  35. package/build/src/agents/registry_store.js +138 -0
  36. package/build/src/agents/runtime.d.ts +30 -1
  37. package/build/src/agents/runtime.js +221 -14
  38. package/build/src/audit/audit_sink.d.ts +1 -1
  39. package/build/src/audit/audit_sink.js +6 -0
  40. package/build/src/define_config.js +6 -0
  41. package/build/src/host/account_api/account_api_controller.d.ts +12 -1
  42. package/build/src/host/account_api/account_api_controller.js +10 -5
  43. package/build/src/host/admin_api/dto.d.ts +1 -1
  44. package/build/src/host/admin_console/admin_shell_controller.js +7 -0
  45. package/build/src/host/admin_console/console_personal_agents_controller.d.ts +83 -0
  46. package/build/src/host/admin_console/console_personal_agents_controller.js +337 -0
  47. package/build/src/host/admin_sessions_service.js +8 -5
  48. package/build/src/host/auth_host_config.d.ts +2 -1
  49. package/build/src/host/controllers/account_apps_controller.js +13 -3
  50. package/build/src/host/controllers/poppy_consent_controller.d.ts +19 -0
  51. package/build/src/host/controllers/poppy_consent_controller.js +173 -0
  52. package/build/src/host/controllers/poppy_controller.d.ts +25 -0
  53. package/build/src/host/controllers/poppy_controller.js +194 -0
  54. package/build/src/host/csrf.d.ts +7 -0
  55. package/build/src/host/csrf.js +16 -1
  56. package/build/src/host/i18n.d.ts +4 -0
  57. package/build/src/host/i18n.js +4 -0
  58. package/build/src/host/poppy_session_key.d.ts +9 -0
  59. package/build/src/host/poppy_session_key.js +9 -0
  60. package/build/src/host/register_auth_host.js +76 -0
  61. package/build/src/host/ui-dist/assets/index-LrScaR9C.js +155 -0
  62. package/build/src/host/ui-dist/index.html +1 -1
  63. package/build/src/provider/oidc_service.d.ts +5 -0
  64. package/build/src/provider/oidc_service.js +9 -2
  65. package/build/src/schema/ensure.js +152 -0
  66. package/package.json +2 -2
  67. package/build/src/host/ui-dist/assets/index-CIqtoNcO.js +0 -155
@@ -0,0 +1,83 @@
1
+ import '../augmentations.js';
2
+ import type { HttpContext } from '@adonisjs/core/http';
3
+ import type { AgentProtocolId } from '../../agents/registry_store.js';
4
+ type ListStatus = 'allowed' | 'blocked';
5
+ interface AgentEntry {
6
+ protocol: AgentProtocolId;
7
+ identifier: string;
8
+ name: string;
9
+ /** O que o `defineConfig` diz (só listas estáticas); `null` = nada. */
10
+ config: ListStatus | null;
11
+ /** O que o console diz; `null` = nada. */
12
+ console: {
13
+ id: string;
14
+ status: ListStatus;
15
+ updatedAt: string;
16
+ updatedBy: string | null;
17
+ } | null;
18
+ /** Resultado: bloqueado em qualquer lado vence; senão liberado em qualquer lado; senão `unlisted`. */
19
+ effective: ListStatus | 'unlisted';
20
+ jwksUri: string | null;
21
+ /** Conexões vivas (grants não revogados nem expirados). */
22
+ connections: number;
23
+ }
24
+ /**
25
+ * Endpoints JSON de personal agents (PACT e Poppy) do console admin React.
26
+ *
27
+ * GET {prefix}/api/personal-agents → agentes (config + console + conectados)
28
+ * GET {prefix}/api/personal-agents/connections?clientId=&accountId=&protocol=&page=&size=
29
+ * POST {prefix}/api/personal-agents/connections/:grantId/revoke (sudo)
30
+ * POST {prefix}/api/personal-agents/registry → libera/bloqueia (sudo)
31
+ * DELETE {prefix}/api/personal-agents/registry/:id → tira do registro do console (sudo)
32
+ *
33
+ * 404 `capability_unsupported` quando `personalAgents` não está configurado ou não há banco.
34
+ */
35
+ export default class ConsolePersonalAgentsController {
36
+ /** GET {prefix}/api/personal-agents */
37
+ index(ctx: HttpContext): Promise<void | {
38
+ protocols: {
39
+ pact: {
40
+ open: boolean;
41
+ /** A lista do config é uma função do host (o console não enxerga). */
42
+ hostResolver: boolean;
43
+ } | null;
44
+ poppy: {
45
+ requireRegistration: boolean;
46
+ hostResolver: boolean;
47
+ allowInsecureClientIds: boolean;
48
+ } | null;
49
+ };
50
+ agents: AgentEntry[];
51
+ }>;
52
+ /** GET {prefix}/api/personal-agents/connections */
53
+ connections(ctx: HttpContext): Promise<void | {
54
+ meta: {
55
+ page: number;
56
+ size: number;
57
+ total: number;
58
+ };
59
+ data: {
60
+ id: string;
61
+ protocol: AgentProtocolId;
62
+ clientId: string;
63
+ agentName: string;
64
+ accountId: string;
65
+ accountEmail: string | null;
66
+ scopes: string[];
67
+ createdAt: string;
68
+ updatedAt: string;
69
+ expiresAt: string;
70
+ }[];
71
+ }>;
72
+ /** POST {prefix}/api/personal-agents/connections/:grantId/revoke */
73
+ revokeConnection(ctx: HttpContext): Promise<void | {}>;
74
+ /**
75
+ * POST {prefix}/api/personal-agents/registry
76
+ * body: `{ protocol, identifier, status: 'allowed'|'blocked', name?, jwksUri? }`.
77
+ * Bloquear também desconecta o agente de todas as contas, na hora.
78
+ */
79
+ upsert(ctx: HttpContext): Promise<void | {}>;
80
+ /** DELETE {prefix}/api/personal-agents/registry/:id */
81
+ destroy(ctx: HttpContext): Promise<void | {}>;
82
+ }
83
+ export {};
@@ -0,0 +1,337 @@
1
+ import '../augmentations.js';
2
+ import { displayNameFor } from '../../agents/agent_identity.js';
3
+ import { parseScope } from '../../agents/config.js';
4
+ import { personalAgentsFor } from '../../agents/runtime.js';
5
+ import { parseListPage, parseListSize } from '../../pagination.js';
6
+ import { ACCOUNT_SESSION_KEY } from '../account_session_key.js';
7
+ import { apiError } from '../admin_api/dto.js';
8
+ import { resolveRuntimeSettings } from '../runtime_settings.js';
9
+ import { requireSudo } from '../sudo_mode.js';
10
+ /** Mesmo gate de sudo das outras ações destrutivas do console (sessões, chaves). */
11
+ async function gateSudo(ctx) {
12
+ const settings = await resolveRuntimeSettings(ctx);
13
+ const result = await requireSudo(ctx, settings);
14
+ if (result === true)
15
+ return null;
16
+ return ctx.response
17
+ .status(403)
18
+ .send(apiError('sudo_required', 'Identity confirmation required.'));
19
+ }
20
+ function notSupported(ctx) {
21
+ return ctx.response.notFound(apiError('capability_unsupported', 'Personal agents não estão configurados (`personalAgents` no defineConfig) ou não há banco.'));
22
+ }
23
+ /** O runtime com banco, ou `null` (404 honesto). */
24
+ async function runtimeFor(ctx) {
25
+ const runtime = await personalAgentsFor(ctx);
26
+ if (!runtime?.registry || !runtime.grants)
27
+ return null;
28
+ return runtime;
29
+ }
30
+ function enabledProtocols(runtime) {
31
+ const out = [];
32
+ if (runtime.config.pact)
33
+ out.push('pact');
34
+ if (runtime.config.poppy)
35
+ out.push('poppy');
36
+ return out;
37
+ }
38
+ function isUrl(value, allowHttp) {
39
+ if (typeof value !== 'string' || value.length > 2048)
40
+ return false;
41
+ try {
42
+ const url = new URL(value);
43
+ return ((url.protocol === 'https:' || (allowHttp && url.protocol === 'http:')) &&
44
+ !url.hash &&
45
+ !url.username &&
46
+ !url.password);
47
+ }
48
+ catch {
49
+ return false;
50
+ }
51
+ }
52
+ async function agentName(runtime, protocol, identifier, fallback) {
53
+ if (fallback)
54
+ return fallback;
55
+ if (protocol === 'poppy' && runtime.poppy) {
56
+ return runtime.poppy.registry.displayName(identifier);
57
+ }
58
+ const registration = await runtime.config.resolveAgent(identifier);
59
+ return displayNameFor(registration ?? { issuer: identifier, jwksUri: '' });
60
+ }
61
+ /**
62
+ * Endpoints JSON de personal agents (PACT e Poppy) do console admin React.
63
+ *
64
+ * GET {prefix}/api/personal-agents → agentes (config + console + conectados)
65
+ * GET {prefix}/api/personal-agents/connections?clientId=&accountId=&protocol=&page=&size=
66
+ * POST {prefix}/api/personal-agents/connections/:grantId/revoke (sudo)
67
+ * POST {prefix}/api/personal-agents/registry → libera/bloqueia (sudo)
68
+ * DELETE {prefix}/api/personal-agents/registry/:id → tira do registro do console (sudo)
69
+ *
70
+ * 404 `capability_unsupported` quando `personalAgents` não está configurado ou não há banco.
71
+ */
72
+ export default class ConsolePersonalAgentsController {
73
+ /** GET {prefix}/api/personal-agents */
74
+ async index(ctx) {
75
+ const runtime = await runtimeFor(ctx);
76
+ if (!runtime)
77
+ return notSupported(ctx);
78
+ const cfg = runtime.config;
79
+ const now = new Date();
80
+ const entries = new Map();
81
+ const keyOf = (p, id) => `${p} ${id}`;
82
+ const entry = (protocol, identifier) => {
83
+ const k = keyOf(protocol, identifier);
84
+ let e = entries.get(k);
85
+ if (!e) {
86
+ e = {
87
+ protocol,
88
+ identifier,
89
+ name: '',
90
+ config: null,
91
+ console: null,
92
+ effective: 'unlisted',
93
+ jwksUri: null,
94
+ connections: 0,
95
+ };
96
+ entries.set(k, e);
97
+ }
98
+ return e;
99
+ };
100
+ const names = new Map();
101
+ if (cfg.pact) {
102
+ for (const agent of cfg.staticAgents ?? []) {
103
+ const e = entry('pact', agent.issuer);
104
+ e.config = agent.enabled === false ? 'blocked' : 'allowed';
105
+ e.jwksUri = agent.jwksUri;
106
+ names.set(keyOf('pact', agent.issuer), agent.name ?? null);
107
+ }
108
+ for (const [clientId, count] of await runtime.grants.countLiveGrantsByClient(now, 'pact')) {
109
+ entry('pact', clientId).connections = count;
110
+ }
111
+ }
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;
119
+ }
120
+ }
121
+ for (const row of await runtime.registry.list()) {
122
+ if (!enabledProtocols(runtime).includes(row.protocol))
123
+ continue;
124
+ const e = entry(row.protocol, row.identifier);
125
+ e.console = {
126
+ id: row.id,
127
+ status: row.status,
128
+ updatedAt: row.updatedAt.toISOString(),
129
+ updatedBy: row.updatedBy,
130
+ };
131
+ if (row.jwksUri)
132
+ e.jwksUri = row.jwksUri;
133
+ if (row.name)
134
+ names.set(keyOf(row.protocol, row.identifier), row.name);
135
+ }
136
+ const agents = await Promise.all([...entries.values()].map(async (e) => {
137
+ const statuses = [e.config, e.console?.status];
138
+ e.effective = statuses.includes('blocked')
139
+ ? 'blocked'
140
+ : statuses.includes('allowed')
141
+ ? 'allowed'
142
+ : 'unlisted';
143
+ e.name = await agentName(runtime, e.protocol, e.identifier, names.get(keyOf(e.protocol, e.identifier)) ?? null);
144
+ return e;
145
+ }));
146
+ agents.sort((a, b) => b.connections - a.connections || a.name.localeCompare(b.name));
147
+ return {
148
+ protocols: {
149
+ pact: cfg.pact
150
+ ? {
151
+ open: cfg.open,
152
+ /** A lista do config é uma função do host (o console não enxerga). */
153
+ hostResolver: cfg.staticAgents === null,
154
+ }
155
+ : 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,
163
+ },
164
+ agents,
165
+ };
166
+ }
167
+ /** GET {prefix}/api/personal-agents/connections */
168
+ async connections(ctx) {
169
+ const runtime = await runtimeFor(ctx);
170
+ if (!runtime)
171
+ return notSupported(ctx);
172
+ const qs = ctx.request.qs();
173
+ const page = parseListPage(qs.page);
174
+ const size = parseListSize(qs.size);
175
+ const protocol = qs.protocol === 'pact' || qs.protocol === 'poppy'
176
+ ? qs.protocol
177
+ : undefined;
178
+ const filter = {
179
+ protocol,
180
+ clientId: qs.clientId?.trim() || undefined,
181
+ accountId: qs.accountId?.trim() || undefined,
182
+ };
183
+ const now = new Date();
184
+ const [total, rows] = await Promise.all([
185
+ runtime.grants.countLiveGrants(now, filter),
186
+ runtime.grants.listLiveGrants({ ...filter, now, offset: (page - 1) * size, limit: size }),
187
+ ]);
188
+ const store = (await ctx.containerResolver.make('authkit.server')).config.accountStore;
189
+ const emails = new Map();
190
+ const nameCache = new Map();
191
+ const data = await Promise.all(rows.map(async (g) => {
192
+ const rowProtocol = g.id.startsWith('pgrant_') ? 'poppy' : 'pact';
193
+ if (!emails.has(g.accountId)) {
194
+ const account = await store.findById(g.accountId).catch(() => null);
195
+ emails.set(g.accountId, account?.email ?? null);
196
+ }
197
+ const k = `${rowProtocol} ${g.clientId}`;
198
+ if (!nameCache.has(k))
199
+ nameCache.set(k, agentName(runtime, rowProtocol, g.clientId, null));
200
+ return {
201
+ id: g.id,
202
+ protocol: rowProtocol,
203
+ clientId: g.clientId,
204
+ agentName: await nameCache.get(k),
205
+ accountId: g.accountId,
206
+ accountEmail: emails.get(g.accountId) ?? null,
207
+ scopes: parseScope(g.scope),
208
+ createdAt: g.createdAt.toISOString(),
209
+ updatedAt: g.updatedAt.toISOString(),
210
+ expiresAt: g.expiresAt.toISOString(),
211
+ };
212
+ }));
213
+ return { meta: { page, size, total }, data };
214
+ }
215
+ /** POST {prefix}/api/personal-agents/connections/:grantId/revoke */
216
+ async revokeConnection(ctx) {
217
+ const runtime = await runtimeFor(ctx);
218
+ if (!runtime)
219
+ return notSupported(ctx);
220
+ const denied = await gateSudo(ctx);
221
+ if (denied)
222
+ return denied;
223
+ const grantId = ctx.request.param('grantId');
224
+ const grant = await runtime.revokeGrantById(grantId);
225
+ if (!grant) {
226
+ return ctx.response.notFound(apiError('not_found', 'Conexão não encontrada ou já revogada.'));
227
+ }
228
+ const cfg = (await ctx.containerResolver.make('authkit.server')).config;
229
+ await cfg.audit?.record({
230
+ type: 'agent.grant_revoked_by_admin',
231
+ accountId: grant.accountId,
232
+ clientId: grant.clientId,
233
+ ip: ctx.request.ip?.() ?? null,
234
+ metadata: { grantId, actor: ctx.session.get(ACCOUNT_SESSION_KEY) ?? null },
235
+ });
236
+ return { revoked: true };
237
+ }
238
+ /**
239
+ * POST {prefix}/api/personal-agents/registry
240
+ * body: `{ protocol, identifier, status: 'allowed'|'blocked', name?, jwksUri? }`.
241
+ * Bloquear também desconecta o agente de todas as contas, na hora.
242
+ */
243
+ async upsert(ctx) {
244
+ const runtime = await runtimeFor(ctx);
245
+ if (!runtime)
246
+ return notSupported(ctx);
247
+ const body = (ctx.request.body() ?? {});
248
+ const invalid = (message) => ctx.response.unprocessableEntity(apiError('invalid_request', message));
249
+ const protocol = body.protocol;
250
+ if (!enabledProtocols(runtime).includes(protocol)) {
251
+ return invalid('`protocol` precisa ser um protocolo ligado (`pact` ou `poppy`).');
252
+ }
253
+ const status = body.status;
254
+ if (status !== 'allowed' && status !== 'blocked') {
255
+ return invalid('`status` precisa ser `allowed` ou `blocked`.');
256
+ }
257
+ const allowHttp = protocol === 'poppy' && !!runtime.config.poppy?.allowInsecureClientIds;
258
+ const identifier = typeof body.identifier === 'string' ? body.identifier.trim() : '';
259
+ if (!isUrl(identifier, allowHttp)) {
260
+ return invalid(protocol === 'poppy'
261
+ ? '`identifier` precisa ser o `client_id` do agente (URL HTTPS do documento de metadata).'
262
+ : '`identifier` precisa ser o `iss` do agente (URL HTTPS).');
263
+ }
264
+ const name = typeof body.name === 'string' && body.name.trim() ? body.name.trim().slice(0, 200) : null;
265
+ let jwksUri = null;
266
+ if (protocol === 'pact') {
267
+ const raw = typeof body.jwksUri === 'string' ? body.jwksUri.trim() : '';
268
+ if (raw && !isUrl(raw, false))
269
+ return invalid('`jwksUri` precisa ser uma URL HTTPS.');
270
+ jwksUri = raw || null;
271
+ if (status === 'allowed' && !jwksUri) {
272
+ return invalid('Liberar um agente PACT exige o `jwksUri` dele.');
273
+ }
274
+ }
275
+ const denied = await gateSudo(ctx);
276
+ if (denied)
277
+ return denied;
278
+ const actor = ctx.session.get(ACCOUNT_SESSION_KEY) ?? null;
279
+ const row = await runtime.registry.upsert({
280
+ protocol,
281
+ identifier,
282
+ status,
283
+ name,
284
+ jwksUri,
285
+ updatedBy: actor,
286
+ });
287
+ const disconnected = status === 'blocked' ? await runtime.disconnectAgent(protocol, identifier) : 0;
288
+ const cfg = (await ctx.containerResolver.make('authkit.server')).config;
289
+ const event = {
290
+ clientId: identifier,
291
+ ip: ctx.request.ip?.() ?? null,
292
+ metadata: { protocol, actor, disconnected },
293
+ };
294
+ if (status === 'blocked')
295
+ await cfg.audit?.record({ type: 'agent.blocked', ...event });
296
+ else
297
+ await cfg.audit?.record({ type: 'agent.allowed', ...event });
298
+ return {
299
+ entry: {
300
+ id: row.id,
301
+ protocol: row.protocol,
302
+ identifier: row.identifier,
303
+ status: row.status,
304
+ name: row.name,
305
+ jwksUri: row.jwksUri,
306
+ updatedAt: row.updatedAt.toISOString(),
307
+ },
308
+ disconnected,
309
+ };
310
+ }
311
+ /** DELETE {prefix}/api/personal-agents/registry/:id */
312
+ async destroy(ctx) {
313
+ const runtime = await runtimeFor(ctx);
314
+ if (!runtime)
315
+ return notSupported(ctx);
316
+ const denied = await gateSudo(ctx);
317
+ if (denied)
318
+ return denied;
319
+ const id = ctx.request.param('id');
320
+ const row = await runtime.registry.findById(id);
321
+ if (!row || !(await runtime.registry.delete(id))) {
322
+ return ctx.response.notFound(apiError('not_found', 'Entrada não encontrada.'));
323
+ }
324
+ const cfg = (await ctx.containerResolver.make('authkit.server')).config;
325
+ await cfg.audit?.record({
326
+ type: 'agent.registry_removed',
327
+ clientId: row.identifier,
328
+ ip: ctx.request.ip?.() ?? null,
329
+ metadata: {
330
+ protocol: row.protocol,
331
+ status: row.status,
332
+ actor: ctx.session.get(ACCOUNT_SESSION_KEY) ?? null,
333
+ },
334
+ });
335
+ return { removed: true };
336
+ }
337
+ }
@@ -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,
@@ -0,0 +1,19 @@
1
+ import '../augmentations.js';
2
+ import type { HttpContext } from '@adonisjs/core/http';
3
+ /**
4
+ * Telas do usuário no Poppy: o consentimento do Direct Sign-In (o
5
+ * `authorization_endpoint`, §4.5) e a do Device Sign-In (o `verification_uri`,
6
+ * §4.6). Montadas atrás do `accountGuard`: sem sessão, o usuário passa pelo login
7
+ * DESTE app e volta. Abrir o link nunca aprova sozinho — a aprovação é um POST
8
+ * com CSRF. Recusadas durante impersonation.
9
+ */
10
+ export default class PoppyConsentController {
11
+ /** GET {prefix}/oauth/authorize */
12
+ authorize(ctx: HttpContext): Promise<any>;
13
+ /** POST {prefix}/oauth/authorize — `request_id`, `decision=allow|deny`, `scope[]`. */
14
+ decideAuthorization(ctx: HttpContext): Promise<any>;
15
+ /** GET {prefix}/device?user_code=XXXX-XXXX */
16
+ device(ctx: HttpContext): Promise<any>;
17
+ /** POST {prefix}/device — `user_code`, `decision`, `scope[]`. */
18
+ decideDevice(ctx: HttpContext): Promise<any>;
19
+ }