@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.
- package/README.md +4 -0
- package/build/host/views/agents/consent.edge +13 -2
- package/build/host/views/agents/done.edge +3 -0
- package/build/index.d.ts +10 -0
- package/build/index.js +8 -0
- package/build/providers/authkit_server_provider.js +15 -1
- package/build/src/agents/config.d.ts +22 -3
- package/build/src/agents/config.js +22 -4
- package/build/src/agents/delegation_service.js +18 -8
- package/build/src/agents/delegation_store.d.ts +16 -0
- package/build/src/agents/delegation_store.js +44 -0
- package/build/src/agents/middleware.js +2 -1
- package/build/src/agents/poppy/client_registry.d.ts +45 -0
- package/build/src/agents/poppy/client_registry.js +226 -0
- package/build/src/agents/poppy/config.d.ts +365 -0
- package/build/src/agents/poppy/config.js +208 -0
- package/build/src/agents/poppy/dpop.d.ts +61 -0
- package/build/src/agents/poppy/dpop.js +158 -0
- package/build/src/agents/poppy/errors.d.ts +20 -0
- package/build/src/agents/poppy/errors.js +22 -0
- package/build/src/agents/poppy/replay.d.ts +25 -0
- package/build/src/agents/poppy/replay.js +106 -0
- package/build/src/agents/poppy/resource.d.ts +95 -0
- package/build/src/agents/poppy/resource.js +125 -0
- package/build/src/agents/poppy/safe_fetch.d.ts +48 -0
- package/build/src/agents/poppy/safe_fetch.js +256 -0
- package/build/src/agents/poppy/service.d.ts +286 -0
- package/build/src/agents/poppy/service.js +1464 -0
- package/build/src/agents/poppy/store.d.ts +111 -0
- package/build/src/agents/poppy/store.js +269 -0
- package/build/src/agents/poppy/web_session.d.ts +32 -0
- package/build/src/agents/poppy/web_session.js +104 -0
- package/build/src/agents/protocol.d.ts +8 -4
- package/build/src/agents/registry_store.d.ts +41 -0
- package/build/src/agents/registry_store.js +138 -0
- package/build/src/agents/runtime.d.ts +30 -1
- package/build/src/agents/runtime.js +221 -14
- package/build/src/audit/audit_sink.d.ts +1 -1
- package/build/src/audit/audit_sink.js +6 -0
- package/build/src/define_config.js +6 -0
- package/build/src/host/account_api/account_api_controller.d.ts +12 -1
- package/build/src/host/account_api/account_api_controller.js +10 -5
- package/build/src/host/admin_api/dto.d.ts +1 -1
- package/build/src/host/admin_console/admin_shell_controller.js +7 -0
- package/build/src/host/admin_console/console_personal_agents_controller.d.ts +83 -0
- package/build/src/host/admin_console/console_personal_agents_controller.js +337 -0
- package/build/src/host/admin_sessions_service.js +8 -5
- package/build/src/host/auth_host_config.d.ts +2 -1
- package/build/src/host/controllers/account_apps_controller.js +13 -3
- package/build/src/host/controllers/poppy_consent_controller.d.ts +19 -0
- package/build/src/host/controllers/poppy_consent_controller.js +173 -0
- package/build/src/host/controllers/poppy_controller.d.ts +25 -0
- package/build/src/host/controllers/poppy_controller.js +194 -0
- package/build/src/host/csrf.d.ts +7 -0
- package/build/src/host/csrf.js +16 -1
- package/build/src/host/i18n.d.ts +4 -0
- package/build/src/host/i18n.js +4 -0
- package/build/src/host/poppy_session_key.d.ts +9 -0
- package/build/src/host/poppy_session_key.js +9 -0
- package/build/src/host/register_auth_host.js +76 -0
- package/build/src/host/ui-dist/assets/index-LrScaR9C.js +155 -0
- package/build/src/host/ui-dist/index.html +1 -1
- package/build/src/provider/oidc_service.d.ts +5 -0
- package/build/src/provider/oidc_service.js +9 -2
- package/build/src/schema/ensure.js +152 -0
- package/package.json +2 -2
- 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
|
|
91
|
-
* resolve o banco. Diferente do
|
|
92
|
-
* não há outra camada que corte
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
+
}
|