@adonis-agora/authkit-server 0.74.0 → 0.76.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 +3 -0
- package/build/host/views/account/apps.edge +30 -0
- package/build/host/views/agents/consent.edge +60 -0
- package/build/host/views/agents/done.edge +24 -0
- package/build/host/views/consent.edge +9 -1
- package/build/host/views/partials/styles.edge +1 -1
- package/build/index.d.ts +9 -0
- package/build/index.js +4 -0
- package/build/providers/authkit_server_provider.js +75 -50
- package/build/src/agents/agent_identity.d.ts +33 -0
- package/build/src/agents/agent_identity.js +156 -0
- package/build/src/agents/config.d.ts +99 -0
- package/build/src/agents/config.js +101 -0
- package/build/src/agents/delegation_service.d.ts +154 -0
- package/build/src/agents/delegation_service.js +394 -0
- package/build/src/agents/delegation_store.d.ts +93 -0
- package/build/src/agents/delegation_store.js +222 -0
- package/build/src/agents/middleware.d.ts +73 -0
- package/build/src/agents/middleware.js +113 -0
- package/build/src/agents/protocol.d.ts +62 -0
- package/build/src/agents/protocol.js +51 -0
- package/build/src/agents/runtime.d.ts +36 -0
- package/build/src/agents/runtime.js +65 -0
- package/build/src/agents/signer.d.ts +34 -0
- package/build/src/agents/signer.js +70 -0
- package/build/src/audit/audit_sink.d.ts +1 -1
- package/build/src/audit/audit_sink.js +5 -0
- package/build/src/controllers/authorization_server_metadata_controller.d.ts +10 -0
- package/build/src/controllers/authorization_server_metadata_controller.js +20 -0
- package/build/src/define_config.d.ts +34 -3
- package/build/src/define_config.js +35 -1
- package/build/src/host/access_token_verifier.d.ts +7 -0
- package/build/src/host/access_token_verifier.js +8 -0
- package/build/src/host/account_api/account_api_controller.d.ts +12 -0
- package/build/src/host/account_api/account_api_controller.js +40 -0
- package/build/src/host/admin_api/dto.d.ts +1 -1
- package/build/src/host/admin_sessions_service.d.ts +2 -0
- package/build/src/host/admin_sessions_service.js +18 -0
- package/build/src/host/auth_host_config.d.ts +4 -0
- package/build/src/host/bearer_account.d.ts +17 -0
- package/build/src/host/bearer_account.js +10 -0
- package/build/src/host/client_names.d.ts +11 -0
- package/build/src/host/client_names.js +12 -0
- package/build/src/host/console_session.js +3 -5
- package/build/src/host/controllers/account_apps_controller.d.ts +2 -0
- package/build/src/host/controllers/account_apps_controller.js +39 -3
- package/build/src/host/controllers/account_orgs_controller.js +2 -1
- package/build/src/host/controllers/account_session_controller.js +2 -1
- package/build/src/host/controllers/agent_consent_controller.d.ts +17 -0
- package/build/src/host/controllers/agent_consent_controller.js +138 -0
- package/build/src/host/controllers/agent_oauth_controller.d.ts +23 -0
- package/build/src/host/controllers/agent_oauth_controller.js +110 -0
- package/build/src/host/controllers/interaction_controller.js +28 -0
- package/build/src/host/csrf.d.ts +8 -18
- package/build/src/host/csrf.js +28 -2
- package/build/src/host/i18n.d.ts +54 -0
- package/build/src/host/i18n.js +54 -0
- package/build/src/host/impersonation_session.d.ts +5 -0
- package/build/src/host/impersonation_session.js +37 -4
- package/build/src/host/oidc_bearer_guard.js +10 -1
- package/build/src/host/redirect_exact.d.ts +12 -0
- package/build/src/host/redirect_exact.js +17 -0
- package/build/src/host/register_auth_host.js +48 -7
- package/build/src/host/request_url.d.ts +10 -0
- package/build/src/host/request_url.js +17 -0
- package/build/src/host/sudo/methods/magic_link.js +2 -1
- package/build/src/host/sudo/runtime.js +3 -2
- package/build/src/host/sudo_mode.js +4 -4
- package/build/src/mcp/mcp_oauth.d.ts +85 -0
- package/build/src/mcp/mcp_oauth.js +154 -0
- package/build/src/provider/build_provider.js +20 -1
- package/build/src/provider/oidc_service.d.ts +8 -0
- package/build/src/provider/oidc_service.js +11 -0
- package/build/src/provider/token_exchange.d.ts +26 -0
- package/build/src/provider/token_exchange.js +44 -3
- package/build/src/schema/ensure.js +92 -0
- package/build/stubs/ui/react/pages/consent.tsx +17 -1
- package/package.json +1 -1
- package/stubs/ui/react/pages/consent.tsx +17 -1
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistência da delegação de personal agents — três tabelas LIB-OWNED (ver
|
|
3
|
+
* `schema/ensure.ts`): pedidos de device flow, grants e refresh tokens.
|
|
4
|
+
*
|
|
5
|
+
* Query builder puro (sem model Lucid): as tabelas são da lib, o host não as
|
|
6
|
+
* estende. Comparações de data acontecem em JS depois de buscar a linha pela
|
|
7
|
+
* chave — timestamp em SQL compara diferente em sqlite/pg/mysql; status e
|
|
8
|
+
* chaves únicas, não.
|
|
9
|
+
*/
|
|
10
|
+
export const DEVICE_TABLE = 'auth_agent_device_codes';
|
|
11
|
+
export const GRANT_TABLE = 'auth_agent_grants';
|
|
12
|
+
export const REFRESH_TABLE = 'auth_agent_refresh_tokens';
|
|
13
|
+
/** sqlite devolve epoch-ms, mysql/pg devolvem Date, alguns drivers string. */
|
|
14
|
+
function toDate(value) {
|
|
15
|
+
if (value instanceof Date)
|
|
16
|
+
return value;
|
|
17
|
+
if (typeof value === 'number')
|
|
18
|
+
return new Date(value);
|
|
19
|
+
if (typeof value === 'string' && /^\d+$/.test(value))
|
|
20
|
+
return new Date(Number(value));
|
|
21
|
+
return new Date(String(value));
|
|
22
|
+
}
|
|
23
|
+
function toDateOrNull(value) {
|
|
24
|
+
return value === null || value === undefined ? null : toDate(value);
|
|
25
|
+
}
|
|
26
|
+
function toDevice(row) {
|
|
27
|
+
return {
|
|
28
|
+
id: row.id,
|
|
29
|
+
deviceCodeHash: row.device_code_hash,
|
|
30
|
+
userCode: row.user_code,
|
|
31
|
+
clientId: row.client_id,
|
|
32
|
+
agentSub: row.agent_sub,
|
|
33
|
+
requestedScope: row.requested_scope,
|
|
34
|
+
status: row.status,
|
|
35
|
+
accountId: row.account_id ?? null,
|
|
36
|
+
grantId: row.grant_id ?? null,
|
|
37
|
+
intervalSeconds: Number(row.interval_seconds),
|
|
38
|
+
lastPolledAt: toDateOrNull(row.last_polled_at),
|
|
39
|
+
expiresAt: toDate(row.expires_at),
|
|
40
|
+
createdAt: toDate(row.created_at),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function toGrant(row) {
|
|
44
|
+
return {
|
|
45
|
+
id: row.id,
|
|
46
|
+
accountId: row.account_id,
|
|
47
|
+
clientId: row.client_id,
|
|
48
|
+
agentSub: row.agent_sub,
|
|
49
|
+
scope: row.scope,
|
|
50
|
+
expiresAt: toDate(row.expires_at),
|
|
51
|
+
revokedAt: toDateOrNull(row.revoked_at),
|
|
52
|
+
createdAt: toDate(row.created_at),
|
|
53
|
+
updatedAt: toDate(row.updated_at),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function affected(result) {
|
|
57
|
+
// knex devolve o número de linhas; alguns drivers, um array.
|
|
58
|
+
if (typeof result === 'number')
|
|
59
|
+
return result;
|
|
60
|
+
if (Array.isArray(result))
|
|
61
|
+
return result.length;
|
|
62
|
+
return 0;
|
|
63
|
+
}
|
|
64
|
+
export class DelegationStore {
|
|
65
|
+
conn;
|
|
66
|
+
/** `conn` = uma conexão Lucid (`db.connection(name?)`) — ou uma transação. */
|
|
67
|
+
constructor(conn) {
|
|
68
|
+
this.conn = conn;
|
|
69
|
+
}
|
|
70
|
+
// ─── device codes ─────────────────────────────────────────────────────────
|
|
71
|
+
async insertDevice(row) {
|
|
72
|
+
await this.conn().table(DEVICE_TABLE).insert({
|
|
73
|
+
id: row.id,
|
|
74
|
+
device_code_hash: row.deviceCodeHash,
|
|
75
|
+
user_code: row.userCode,
|
|
76
|
+
client_id: row.clientId,
|
|
77
|
+
agent_sub: row.agentSub,
|
|
78
|
+
requested_scope: row.requestedScope,
|
|
79
|
+
status: row.status,
|
|
80
|
+
interval_seconds: row.intervalSeconds,
|
|
81
|
+
expires_at: row.expiresAt,
|
|
82
|
+
created_at: row.createdAt,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
async findDeviceByCodeHash(hash) {
|
|
86
|
+
const row = await this.conn().from(DEVICE_TABLE).where('device_code_hash', hash).first();
|
|
87
|
+
return row ? toDevice(row) : null;
|
|
88
|
+
}
|
|
89
|
+
async findDeviceByUserCode(userCode) {
|
|
90
|
+
const row = await this.conn().from(DEVICE_TABLE).where('user_code', userCode).first();
|
|
91
|
+
return row ? toDevice(row) : null;
|
|
92
|
+
}
|
|
93
|
+
/** Liga o pedido aprovado ao grant — a partir daqui o polling do agente recebe o token. */
|
|
94
|
+
async setDeviceGrant(id, grantId) {
|
|
95
|
+
await this.conn().from(DEVICE_TABLE).where('id', id).update({ grant_id: grantId });
|
|
96
|
+
}
|
|
97
|
+
async markPolled(id, at) {
|
|
98
|
+
await this.conn().from(DEVICE_TABLE).where('id', id).update({ last_polled_at: at });
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Transição de estado ATÔMICA: só aplica se o pedido ainda está em `from`.
|
|
102
|
+
* `false` = outra request chegou antes (aprovação dupla, poll concorrente).
|
|
103
|
+
*/
|
|
104
|
+
async transitionDevice(id, from, patch) {
|
|
105
|
+
const update = { status: patch.status };
|
|
106
|
+
if (patch.accountId !== undefined)
|
|
107
|
+
update.account_id = patch.accountId;
|
|
108
|
+
if (patch.grantId !== undefined)
|
|
109
|
+
update.grant_id = patch.grantId;
|
|
110
|
+
const result = await this.conn()
|
|
111
|
+
.from(DEVICE_TABLE)
|
|
112
|
+
.where('id', id)
|
|
113
|
+
.where('status', from)
|
|
114
|
+
.update(update);
|
|
115
|
+
return affected(result) > 0;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Housekeeping: apaga pedidos que expiraram antes de `before`. O parâmetro
|
|
119
|
+
* vai pelo mesmo binding do INSERT, então a comparação é coerente no dialeto.
|
|
120
|
+
*/
|
|
121
|
+
async deleteDevicesExpiredBefore(before) {
|
|
122
|
+
await this.conn().from(DEVICE_TABLE).where('expires_at', '<', before).delete();
|
|
123
|
+
}
|
|
124
|
+
// ─── grants ───────────────────────────────────────────────────────────────
|
|
125
|
+
async insertGrant(row) {
|
|
126
|
+
await this.conn().table(GRANT_TABLE).insert({
|
|
127
|
+
id: row.id,
|
|
128
|
+
account_id: row.accountId,
|
|
129
|
+
client_id: row.clientId,
|
|
130
|
+
agent_sub: row.agentSub,
|
|
131
|
+
scope: row.scope,
|
|
132
|
+
expires_at: row.expiresAt,
|
|
133
|
+
created_at: row.createdAt,
|
|
134
|
+
updated_at: row.updatedAt,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
async findGrant(id) {
|
|
138
|
+
const row = await this.conn().from(GRANT_TABLE).where('id', id).first();
|
|
139
|
+
return row ? toGrant(row) : null;
|
|
140
|
+
}
|
|
141
|
+
/** Grants NÃO revogados da tripla (conta, agente, usuário do agente). */
|
|
142
|
+
async findGrantsFor(accountId, clientId, agentSub) {
|
|
143
|
+
const rows = await this.conn()
|
|
144
|
+
.from(GRANT_TABLE)
|
|
145
|
+
.where('account_id', accountId)
|
|
146
|
+
.where('client_id', clientId)
|
|
147
|
+
.where('agent_sub', agentSub)
|
|
148
|
+
.whereNull('revoked_at');
|
|
149
|
+
return rows.map(toGrant);
|
|
150
|
+
}
|
|
151
|
+
/** Atualiza um grant NÃO revogado. `false` = revogado (ou sumiu) nesse meio-tempo. */
|
|
152
|
+
async updateGrant(id, patch) {
|
|
153
|
+
const result = await this.conn()
|
|
154
|
+
.from(GRANT_TABLE)
|
|
155
|
+
.where('id', id)
|
|
156
|
+
.whereNull('revoked_at')
|
|
157
|
+
.update({ scope: patch.scope, expires_at: patch.expiresAt, updated_at: patch.updatedAt });
|
|
158
|
+
return affected(result) > 0;
|
|
159
|
+
}
|
|
160
|
+
async listGrants(accountId) {
|
|
161
|
+
const rows = await this.conn()
|
|
162
|
+
.from(GRANT_TABLE)
|
|
163
|
+
.where('account_id', accountId)
|
|
164
|
+
.whereNull('revoked_at')
|
|
165
|
+
.orderBy('created_at', 'desc');
|
|
166
|
+
return rows.map(toGrant);
|
|
167
|
+
}
|
|
168
|
+
/** Revoga um grant DA CONTA. `false` = não existe, é de outra conta ou já revogado. */
|
|
169
|
+
async revokeGrant(accountId, id, at) {
|
|
170
|
+
const result = await this.conn()
|
|
171
|
+
.from(GRANT_TABLE)
|
|
172
|
+
.where('id', id)
|
|
173
|
+
.where('account_id', accountId)
|
|
174
|
+
.whereNull('revoked_at')
|
|
175
|
+
.update({ revoked_at: at, updated_at: at });
|
|
176
|
+
return affected(result) > 0;
|
|
177
|
+
}
|
|
178
|
+
async revokeAllGrants(accountId, at) {
|
|
179
|
+
const result = await this.conn()
|
|
180
|
+
.from(GRANT_TABLE)
|
|
181
|
+
.where('account_id', accountId)
|
|
182
|
+
.whereNull('revoked_at')
|
|
183
|
+
.update({ revoked_at: at, updated_at: at });
|
|
184
|
+
return affected(result);
|
|
185
|
+
}
|
|
186
|
+
/** Revoga um grant sem checar a conta — reação a reuso de refresh token. */
|
|
187
|
+
async revokeGrantById(id, at) {
|
|
188
|
+
await this.conn()
|
|
189
|
+
.from(GRANT_TABLE)
|
|
190
|
+
.where('id', id)
|
|
191
|
+
.whereNull('revoked_at')
|
|
192
|
+
.update({ revoked_at: at, updated_at: at });
|
|
193
|
+
}
|
|
194
|
+
// ─── refresh tokens ───────────────────────────────────────────────────────
|
|
195
|
+
async insertRefreshToken(row) {
|
|
196
|
+
await this.conn().table(REFRESH_TABLE).insert({
|
|
197
|
+
token_hash: row.tokenHash,
|
|
198
|
+
grant_id: row.grantId,
|
|
199
|
+
expires_at: row.expiresAt,
|
|
200
|
+
created_at: row.createdAt,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
async findRefreshToken(tokenHash) {
|
|
204
|
+
const row = await this.conn().from(REFRESH_TABLE).where('token_hash', tokenHash).first();
|
|
205
|
+
return row
|
|
206
|
+
? {
|
|
207
|
+
grantId: row.grant_id,
|
|
208
|
+
expiresAt: toDate(row.expires_at),
|
|
209
|
+
usedAt: toDateOrNull(row.used_at),
|
|
210
|
+
}
|
|
211
|
+
: null;
|
|
212
|
+
}
|
|
213
|
+
/** Marca o refresh como usado (uso único, atômico). `false` = já tinha sido usado. */
|
|
214
|
+
async useRefreshToken(tokenHash, at) {
|
|
215
|
+
const result = await this.conn()
|
|
216
|
+
.from(REFRESH_TABLE)
|
|
217
|
+
.where('token_hash', tokenHash)
|
|
218
|
+
.whereNull('used_at')
|
|
219
|
+
.update({ used_at: at });
|
|
220
|
+
return affected(result) > 0;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { HttpContext } from '@adonisjs/core/http';
|
|
2
|
+
import type { PersonalAgentIdentity } from './agent_identity.js';
|
|
3
|
+
import type { DelegationContext } from './delegation_service.js';
|
|
4
|
+
import type { PersonalAgentProtocol } from './protocol.js';
|
|
5
|
+
/** 401 sem corpo, só o challenge do protocolo (PACT §3.4/§5.5). */
|
|
6
|
+
export declare function personalAgentUnauthorized(ctx: HttpContext, protocol: PersonalAgentProtocol, error?: 'invalid_token'): void;
|
|
7
|
+
/** O que o middleware anexa à request. */
|
|
8
|
+
export interface PersonalAgentRequestContext extends PersonalAgentIdentity {
|
|
9
|
+
/** Delegação válida enviada junto, ou `null` (o agente fala só com identidade). */
|
|
10
|
+
delegation: DelegationContext | null;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* O agente que chamou e a delegação dele — preenchido por
|
|
14
|
+
* {@link personalAgentAuth}. `null` numa rota sem o middleware.
|
|
15
|
+
*/
|
|
16
|
+
export declare function personalAgentOf(ctx: HttpContext): PersonalAgentRequestContext | null;
|
|
17
|
+
export interface PersonalAgentAuthOptions {
|
|
18
|
+
/**
|
|
19
|
+
* - `'optional'` (default): valida a delegação SE vier; sem ela, segue só
|
|
20
|
+
* com identidade. É o que o PACT pede no endpoint do agente — falta de
|
|
21
|
+
* scope vira step-up, não erro.
|
|
22
|
+
* - `'required'`: sem delegação válida, 401 `invalid_token` (APIs que só
|
|
23
|
+
* fazem sentido agindo na conta).
|
|
24
|
+
*/
|
|
25
|
+
delegation?: 'optional' | 'required';
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Middleware das rotas que os personal agents chamam (o endpoint de agente do
|
|
29
|
+
* app, ou uma API exposta a eles). Verifica o JWT do agente e, se vier, o token
|
|
30
|
+
* de delegação no header do protocolo — falhas respondem 401 sem corpo. Em
|
|
31
|
+
* sucesso, {@link personalAgentOf} devolve o agente e a delegação.
|
|
32
|
+
*
|
|
33
|
+
* ```ts
|
|
34
|
+
* import { personalAgentAuth } from '@adonis-agora/authkit-server'
|
|
35
|
+
* router.post('/a2a/message:send', [A2aController, 'send']).use(personalAgentAuth())
|
|
36
|
+
* ```
|
|
37
|
+
*
|
|
38
|
+
* Lembre de isentar essas rotas do CSRF (são server-to-server).
|
|
39
|
+
*/
|
|
40
|
+
export declare function personalAgentAuth(options?: PersonalAgentAuthOptions): (ctx: HttpContext, next: () => Promise<void>) => Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* Bloco de segurança da descoberta do app — no PACT, os `securitySchemes` e
|
|
43
|
+
* `securityRequirements` do Agent Card (§2.1, §5.1). Faça spread no documento
|
|
44
|
+
* que o app serve.
|
|
45
|
+
*/
|
|
46
|
+
export declare function personalAgentSecurity(ctx: HttpContext, options?: {
|
|
47
|
+
/**
|
|
48
|
+
* A interface URL do card. Um app com vários agentes serve um card por agente, mas a delegação
|
|
49
|
+
* vale para UMA interface (`delegation.interfaceUrl`): os demais cards anunciam só identidade.
|
|
50
|
+
*/
|
|
51
|
+
interfaceUrl?: string;
|
|
52
|
+
}): Promise<Record<string, unknown>>;
|
|
53
|
+
/**
|
|
54
|
+
* Step-up: o turno precisa de scopes que o token não tem. Devolve o `metadata`
|
|
55
|
+
* da resposta que pede autorização ao usuário (no PACT, a task
|
|
56
|
+
* `TASK_STATE_AUTH_REQUIRED` de §5.5) — os scopes faltantes e um link de
|
|
57
|
+
* consentimento pedindo só eles. Aprovar por esse link SOMA os scopes ao grant
|
|
58
|
+
* existente; o agente obtém o token novo repetindo o device flow, ou com um
|
|
59
|
+
* refresh, que já sai com o grant atualizado.
|
|
60
|
+
*/
|
|
61
|
+
export declare function personalAgentStepUp(ctx: HttpContext, missingScopes: string[]): Promise<Record<string, unknown>>;
|
|
62
|
+
/**
|
|
63
|
+
* Recibo assinado de uma resposta servida sob delegação, já no `metadata` do
|
|
64
|
+
* protocolo (no PACT, `pact.receipt` — §5.6). O PACT exige um em TODA resposta
|
|
65
|
+
* com delegação.
|
|
66
|
+
*/
|
|
67
|
+
export declare function personalAgentReceipt(ctx: HttpContext, input: {
|
|
68
|
+
scopesUsed: string[];
|
|
69
|
+
actions?: {
|
|
70
|
+
tool: string;
|
|
71
|
+
argsHash?: string;
|
|
72
|
+
}[];
|
|
73
|
+
}): Promise<Record<string, unknown>>;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { personalAgentsFor } from './runtime.js';
|
|
2
|
+
/** 401 sem corpo, só o challenge do protocolo (PACT §3.4/§5.5). */
|
|
3
|
+
export function personalAgentUnauthorized(ctx, protocol, error) {
|
|
4
|
+
ctx.response.header('WWW-Authenticate', protocol.challenge(error));
|
|
5
|
+
return ctx.response.status(401).send('');
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Guardado fora do `HttpContext` de propósito: uma augmentation alcançável
|
|
9
|
+
* pelo barrel vazaria para os tipos de todo host (ver
|
|
10
|
+
* `augmentation_isolation.spec.ts`).
|
|
11
|
+
*/
|
|
12
|
+
const requestContexts = new WeakMap();
|
|
13
|
+
/**
|
|
14
|
+
* O agente que chamou e a delegação dele — preenchido por
|
|
15
|
+
* {@link personalAgentAuth}. `null` numa rota sem o middleware.
|
|
16
|
+
*/
|
|
17
|
+
export function personalAgentOf(ctx) {
|
|
18
|
+
return requestContexts.get(ctx) ?? null;
|
|
19
|
+
}
|
|
20
|
+
async function requireRuntime(ctx, helper) {
|
|
21
|
+
const runtime = await personalAgentsFor(ctx);
|
|
22
|
+
if (!runtime)
|
|
23
|
+
throw new Error(`authkit: ${helper} exige \`personalAgents\` no config.`);
|
|
24
|
+
return runtime;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Middleware das rotas que os personal agents chamam (o endpoint de agente do
|
|
28
|
+
* app, ou uma API exposta a eles). Verifica o JWT do agente e, se vier, o token
|
|
29
|
+
* de delegação no header do protocolo — falhas respondem 401 sem corpo. Em
|
|
30
|
+
* sucesso, {@link personalAgentOf} devolve o agente e a delegação.
|
|
31
|
+
*
|
|
32
|
+
* ```ts
|
|
33
|
+
* import { personalAgentAuth } from '@adonis-agora/authkit-server'
|
|
34
|
+
* router.post('/a2a/message:send', [A2aController, 'send']).use(personalAgentAuth())
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* Lembre de isentar essas rotas do CSRF (são server-to-server).
|
|
38
|
+
*/
|
|
39
|
+
export function personalAgentAuth(options = {}) {
|
|
40
|
+
const mode = options.delegation ?? 'optional';
|
|
41
|
+
return async (ctx, next) => {
|
|
42
|
+
const runtime = await personalAgentsFor(ctx);
|
|
43
|
+
if (!runtime)
|
|
44
|
+
return ctx.response.notFound();
|
|
45
|
+
const { protocol } = runtime.config;
|
|
46
|
+
// Autentica ANTES de ler o corpo (PACT §3.4).
|
|
47
|
+
const agent = await runtime.verifier.verify(ctx.request.header('authorization'));
|
|
48
|
+
if (!agent)
|
|
49
|
+
return personalAgentUnauthorized(ctx, protocol);
|
|
50
|
+
let delegation = null;
|
|
51
|
+
const header = ctx.request.header(protocol.delegationHeader);
|
|
52
|
+
if (header) {
|
|
53
|
+
delegation = runtime.delegation ? await runtime.delegation.verify(agent, header) : null;
|
|
54
|
+
if (!delegation)
|
|
55
|
+
return personalAgentUnauthorized(ctx, protocol, 'invalid_token');
|
|
56
|
+
}
|
|
57
|
+
else if (mode === 'required') {
|
|
58
|
+
return personalAgentUnauthorized(ctx, protocol, 'invalid_token');
|
|
59
|
+
}
|
|
60
|
+
requestContexts.set(ctx, { ...agent, delegation });
|
|
61
|
+
return next();
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Bloco de segurança da descoberta do app — no PACT, os `securitySchemes` e
|
|
66
|
+
* `securityRequirements` do Agent Card (§2.1, §5.1). Faça spread no documento
|
|
67
|
+
* que o app serve.
|
|
68
|
+
*/
|
|
69
|
+
export async function personalAgentSecurity(ctx, options = {}) {
|
|
70
|
+
const runtime = await requireRuntime(ctx, 'personalAgentSecurity');
|
|
71
|
+
const delegates = runtime.delegation !== null &&
|
|
72
|
+
(options.interfaceUrl === undefined ||
|
|
73
|
+
options.interfaceUrl === runtime.delegation.interfaceUrl);
|
|
74
|
+
return runtime.config.protocol.discovery({
|
|
75
|
+
deviceAuthorizationUrl: runtime.urls.deviceAuthorization,
|
|
76
|
+
tokenUrl: runtime.urls.token,
|
|
77
|
+
metadataUrl: runtime.urls.metadata,
|
|
78
|
+
scopes: delegates ? runtime.delegation.scopes : null,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Step-up: o turno precisa de scopes que o token não tem. Devolve o `metadata`
|
|
83
|
+
* da resposta que pede autorização ao usuário (no PACT, a task
|
|
84
|
+
* `TASK_STATE_AUTH_REQUIRED` de §5.5) — os scopes faltantes e um link de
|
|
85
|
+
* consentimento pedindo só eles. Aprovar por esse link SOMA os scopes ao grant
|
|
86
|
+
* existente; o agente obtém o token novo repetindo o device flow, ou com um
|
|
87
|
+
* refresh, que já sai com o grant atualizado.
|
|
88
|
+
*/
|
|
89
|
+
export async function personalAgentStepUp(ctx, missingScopes) {
|
|
90
|
+
const runtime = await requireRuntime(ctx, 'personalAgentStepUp');
|
|
91
|
+
const agent = personalAgentOf(ctx);
|
|
92
|
+
if (!runtime.delegation || !agent) {
|
|
93
|
+
throw new Error('authkit: personalAgentStepUp exige `personalAgents.delegation` e o personalAgentAuth() na rota.');
|
|
94
|
+
}
|
|
95
|
+
const link = await runtime.delegation.requestDevice(agent, missingScopes.join(' '));
|
|
96
|
+
return runtime.config.protocol.stepUp({
|
|
97
|
+
missingScopes,
|
|
98
|
+
verificationUriComplete: link.verification_uri_complete,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Recibo assinado de uma resposta servida sob delegação, já no `metadata` do
|
|
103
|
+
* protocolo (no PACT, `pact.receipt` — §5.6). O PACT exige um em TODA resposta
|
|
104
|
+
* com delegação.
|
|
105
|
+
*/
|
|
106
|
+
export async function personalAgentReceipt(ctx, input) {
|
|
107
|
+
const runtime = await requireRuntime(ctx, 'personalAgentReceipt');
|
|
108
|
+
const delegation = personalAgentOf(ctx)?.delegation;
|
|
109
|
+
if (!runtime.delegation || !delegation) {
|
|
110
|
+
throw new Error('authkit: personalAgentReceipt só vale numa request com delegação válida.');
|
|
111
|
+
}
|
|
112
|
+
return runtime.config.protocol.receipt(await runtime.delegation.receipt(delegation, input));
|
|
113
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { DelegationReceipt } from './delegation_service.js';
|
|
2
|
+
/**
|
|
3
|
+
* Adapter de PROTOCOLO dos personal agents. O núcleo (identidade do agente por
|
|
4
|
+
* JWT/JWKS, device flow, grants, tokens de delegação, recibos) é o mesmo em
|
|
5
|
+
* qualquer protocolo; o que muda é o "fio": regras do JWT do agente, nome do
|
|
6
|
+
* header de delegação, challenge do 401, bloco de descoberta, formato do
|
|
7
|
+
* step-up e do recibo.
|
|
8
|
+
*
|
|
9
|
+
* Embutido hoje: `'pact'` (https://openpactprotocol.org, v1.0). Outro protocolo
|
|
10
|
+
* (ex.: o Personal Agent Protocol da Sierra, quando sair a spec) entra como
|
|
11
|
+
* mais um objeto desta interface — passado direto em `personalAgents.protocol`
|
|
12
|
+
* enquanto não for embutido.
|
|
13
|
+
*/
|
|
14
|
+
export interface PersonalAgentProtocol {
|
|
15
|
+
/** Identificador estável (`'pact'`). */
|
|
16
|
+
readonly id: string;
|
|
17
|
+
/** Regras do JWT com que o agente se identifica. */
|
|
18
|
+
readonly identity: {
|
|
19
|
+
/** Algoritmos aceitos; os demais são rejeitados antes de buscar chave. */
|
|
20
|
+
algorithms: string[];
|
|
21
|
+
/** Vida máxima (`exp - iat`), em segundos. */
|
|
22
|
+
maxLifetimeSeconds: number;
|
|
23
|
+
/** Tolerância de relógio, em segundos. */
|
|
24
|
+
clockSkewSeconds: number;
|
|
25
|
+
};
|
|
26
|
+
/** Header (minúsculo) que carrega o token de delegação junto do JWT do agente. */
|
|
27
|
+
readonly delegationHeader: string;
|
|
28
|
+
/** Valor do `WWW-Authenticate` nos 401. `invalid_token` = delegação inválida. */
|
|
29
|
+
challenge(error?: 'invalid_token'): string;
|
|
30
|
+
/** Bloco de segurança que o app publica na descoberta (Agent Card, no PACT). */
|
|
31
|
+
discovery(input: {
|
|
32
|
+
deviceAuthorizationUrl: string;
|
|
33
|
+
tokenUrl: string;
|
|
34
|
+
metadataUrl: string;
|
|
35
|
+
/** `null` = delegação desligada (só identidade). */
|
|
36
|
+
scopes: Record<string, string> | null;
|
|
37
|
+
}): Record<string, unknown>;
|
|
38
|
+
/** Metadata da resposta que pede mais scopes ao usuário (step-up). */
|
|
39
|
+
stepUp(input: {
|
|
40
|
+
missingScopes: string[];
|
|
41
|
+
verificationUriComplete: string;
|
|
42
|
+
}): Record<string, unknown>;
|
|
43
|
+
/** Metadata que carrega o recibo de uma resposta servida sob delegação. */
|
|
44
|
+
receipt(receipt: DelegationReceipt): Record<string, unknown>;
|
|
45
|
+
}
|
|
46
|
+
export interface PactOptions {
|
|
47
|
+
/**
|
|
48
|
+
* Nome do esquema de identidade no Agent Card. Default `'platformJwt'`, o da implementação de
|
|
49
|
+
* referência e da suíte de conformidade do PACT — o texto da spec diz `'paJwt'`. Os clientes
|
|
50
|
+
* escolhem o esquema pelo tipo, não pelo nome, então os dois interoperam.
|
|
51
|
+
*/
|
|
52
|
+
identitySchemeName?: string;
|
|
53
|
+
}
|
|
54
|
+
/** PACT 1.0 — §3.2 (JWT do agente), §2.1/§5.1 (Agent Card), §5.5 (step-up), §5.6 (recibo). */
|
|
55
|
+
export declare function pact(options?: PactOptions): PersonalAgentProtocol;
|
|
56
|
+
/** O adapter PACT com os defaults da spec. */
|
|
57
|
+
export declare const pactProtocol: PersonalAgentProtocol;
|
|
58
|
+
/** Protocolos embutidos, pelo id aceito em `personalAgents.protocol`. */
|
|
59
|
+
export declare const BUILTIN_PROTOCOLS: {
|
|
60
|
+
readonly pact: PersonalAgentProtocol;
|
|
61
|
+
};
|
|
62
|
+
export type BuiltinProtocolId = keyof typeof BUILTIN_PROTOCOLS;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/** PACT 1.0 — §3.2 (JWT do agente), §2.1/§5.1 (Agent Card), §5.5 (step-up), §5.6 (recibo). */
|
|
2
|
+
export function pact(options = {}) {
|
|
3
|
+
const identity = options.identitySchemeName ?? 'platformJwt';
|
|
4
|
+
return {
|
|
5
|
+
id: 'pact',
|
|
6
|
+
identity: { algorithms: ['ES256', 'RS256'], maxLifetimeSeconds: 300, clockSkewSeconds: 30 },
|
|
7
|
+
delegationHeader: 'x-a2a-user-delegation',
|
|
8
|
+
challenge(error) {
|
|
9
|
+
return error ? `Bearer realm="a2a", error="${error}"` : 'Bearer realm="a2a"';
|
|
10
|
+
},
|
|
11
|
+
discovery({ deviceAuthorizationUrl, tokenUrl, metadataUrl, scopes }) {
|
|
12
|
+
const identityScheme = { httpAuthSecurityScheme: { scheme: 'Bearer', bearerFormat: 'JWT' } };
|
|
13
|
+
const identityOnly = { schemes: { [identity]: { list: [] } } };
|
|
14
|
+
if (!scopes) {
|
|
15
|
+
return {
|
|
16
|
+
securitySchemes: { [identity]: identityScheme },
|
|
17
|
+
securityRequirements: [identityOnly],
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
securitySchemes: {
|
|
22
|
+
[identity]: identityScheme,
|
|
23
|
+
userDelegation: {
|
|
24
|
+
oauth2SecurityScheme: {
|
|
25
|
+
flows: { deviceCode: { deviceAuthorizationUrl, tokenUrl, scopes: { ...scopes } } },
|
|
26
|
+
oauth2MetadataUrl: metadataUrl,
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
// A entrada só-identidade FICA (§5.1): o agente sempre pode falar sem delegação.
|
|
31
|
+
securityRequirements: [
|
|
32
|
+
identityOnly,
|
|
33
|
+
{ schemes: { [identity]: { list: [] }, userDelegation: { list: [] } } },
|
|
34
|
+
],
|
|
35
|
+
};
|
|
36
|
+
},
|
|
37
|
+
stepUp({ missingScopes, verificationUriComplete }) {
|
|
38
|
+
return {
|
|
39
|
+
'pact.missingScopes': missingScopes,
|
|
40
|
+
'pact.verificationUriComplete': verificationUriComplete,
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
receipt(receipt) {
|
|
44
|
+
return { 'pact.receipt': { jws: receipt.jws, claims: receipt.claims } };
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/** O adapter PACT com os defaults da spec. */
|
|
49
|
+
export const pactProtocol = pact();
|
|
50
|
+
/** Protocolos embutidos, pelo id aceito em `personalAgents.protocol`. */
|
|
51
|
+
export const BUILTIN_PROTOCOLS = { pact: pactProtocol };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { HttpContext } from '@adonisjs/core/http';
|
|
2
|
+
import type { OidcService } from '../provider/oidc_service.js';
|
|
3
|
+
import { PersonalAgentVerifier } from './agent_identity.js';
|
|
4
|
+
import type { ResolvedPersonalAgentsConfig } from './config.js';
|
|
5
|
+
import { PersonalAgentDelegation } from './delegation_service.js';
|
|
6
|
+
/** URLs públicas do authorization server de delegação. */
|
|
7
|
+
export interface PersonalAgentUrls {
|
|
8
|
+
/** `issuer` (RFC 8414) — também o `iss` dos tokens de delegação. */
|
|
9
|
+
issuer: string;
|
|
10
|
+
metadata: string;
|
|
11
|
+
jwks: string;
|
|
12
|
+
deviceAuthorization: string;
|
|
13
|
+
token: string;
|
|
14
|
+
/** Tela de consentimento — o `verification_uri` do device flow. */
|
|
15
|
+
consent: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Deriva as URLs da ORIGEM do issuer configurado — nunca do Host da request
|
|
19
|
+
* (mesma regra dos links de e-mail: um Host forjado não pode virar o `iss`).
|
|
20
|
+
*/
|
|
21
|
+
export declare function personalAgentUrls(oidcIssuer: string, prefix: string): PersonalAgentUrls;
|
|
22
|
+
/** Tudo que as rotas e o middleware de personal agents usam, já montado. */
|
|
23
|
+
export interface PersonalAgentsRuntime {
|
|
24
|
+
config: ResolvedPersonalAgentsConfig;
|
|
25
|
+
urls: PersonalAgentUrls;
|
|
26
|
+
verifier: PersonalAgentVerifier;
|
|
27
|
+
/** `null` quando o app não configurou `personalAgents.delegation`. */
|
|
28
|
+
delegation: PersonalAgentDelegation | null;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Monta (uma vez por `OidcService`) o runtime de personal agents. `null` quando
|
|
32
|
+
* `personalAgents` não está no config — as rotas respondem 404.
|
|
33
|
+
*/
|
|
34
|
+
export declare function buildPersonalAgentsRuntime(service: OidcService, makeDb: () => Promise<any>): Promise<PersonalAgentsRuntime | null>;
|
|
35
|
+
/** Runtime a partir de uma request. */
|
|
36
|
+
export declare function personalAgentsFor(ctx: HttpContext): Promise<PersonalAgentsRuntime | null>;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { supportsAccountStatus } from '../accounts/account_store.js';
|
|
2
|
+
import { PersonalAgentVerifier } from './agent_identity.js';
|
|
3
|
+
import { PersonalAgentDelegation } from './delegation_service.js';
|
|
4
|
+
import { DelegationStore } from './delegation_store.js';
|
|
5
|
+
import { keystoreSigner } from './signer.js';
|
|
6
|
+
/**
|
|
7
|
+
* Deriva as URLs da ORIGEM do issuer configurado — nunca do Host da request
|
|
8
|
+
* (mesma regra dos links de e-mail: um Host forjado não pode virar o `iss`).
|
|
9
|
+
*/
|
|
10
|
+
export function personalAgentUrls(oidcIssuer, prefix) {
|
|
11
|
+
const origin = new URL(oidcIssuer).origin;
|
|
12
|
+
const issuer = `${origin}${prefix}/oauth`;
|
|
13
|
+
return {
|
|
14
|
+
issuer,
|
|
15
|
+
metadata: `${issuer}/.well-known/oauth-authorization-server`,
|
|
16
|
+
jwks: `${issuer}/jwks.json`,
|
|
17
|
+
deviceAuthorization: `${issuer}/device_authorization`,
|
|
18
|
+
token: `${issuer}/token`,
|
|
19
|
+
consent: `${origin}${prefix}/consent`,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
const runtimes = new WeakMap();
|
|
23
|
+
/**
|
|
24
|
+
* Monta (uma vez por `OidcService`) o runtime de personal agents. `null` quando
|
|
25
|
+
* `personalAgents` não está no config — as rotas respondem 404.
|
|
26
|
+
*/
|
|
27
|
+
export function buildPersonalAgentsRuntime(service, makeDb) {
|
|
28
|
+
let runtime = runtimes.get(service);
|
|
29
|
+
if (!runtime) {
|
|
30
|
+
runtime = (async () => {
|
|
31
|
+
const config = service.config.personalAgents;
|
|
32
|
+
if (!config)
|
|
33
|
+
return null;
|
|
34
|
+
const urls = personalAgentUrls(service.config.issuer, config.prefix);
|
|
35
|
+
let delegation = null;
|
|
36
|
+
if (config.delegation) {
|
|
37
|
+
const db = await makeDb();
|
|
38
|
+
const connection = service.config.schema.connection;
|
|
39
|
+
delegation = new PersonalAgentDelegation({
|
|
40
|
+
cfg: config.delegation,
|
|
41
|
+
store: new DelegationStore(() => connection ? db.connection(connection) : db.connection()),
|
|
42
|
+
signer: keystoreSigner(service),
|
|
43
|
+
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
|
+
},
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return { config, urls, verifier: new PersonalAgentVerifier(config), delegation };
|
|
54
|
+
})();
|
|
55
|
+
// Falha (DB indisponível) não fica em cache — a próxima request tenta de novo.
|
|
56
|
+
runtime.catch(() => runtimes.delete(service));
|
|
57
|
+
runtimes.set(service, runtime);
|
|
58
|
+
}
|
|
59
|
+
return runtime;
|
|
60
|
+
}
|
|
61
|
+
/** Runtime a partir de uma request. */
|
|
62
|
+
export async function personalAgentsFor(ctx) {
|
|
63
|
+
const service = await ctx.containerResolver.make('authkit.server');
|
|
64
|
+
return buildPersonalAgentsRuntime(service, () => ctx.containerResolver.make('lucid.db'));
|
|
65
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type JWTPayload, type JWTVerifyGetKey } from 'jose';
|
|
2
|
+
/** Fonte das chaves — o `OidcService` (troca de objeto numa rotação). */
|
|
3
|
+
export interface SigningKeySource {
|
|
4
|
+
readonly signingJwks: {
|
|
5
|
+
keys: Record<string, any>[];
|
|
6
|
+
};
|
|
7
|
+
readonly publicJwks: {
|
|
8
|
+
keys: Record<string, any>[];
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export interface AgentSigner {
|
|
12
|
+
/** Assina um JWT (token de delegação). */
|
|
13
|
+
signJwt(payload: JWTPayload, options: {
|
|
14
|
+
issuedAt: Date;
|
|
15
|
+
expiresIn: number;
|
|
16
|
+
}): Promise<string>;
|
|
17
|
+
/** JWS compacto de um objeto JSON arbitrário (recibo, PACT §5.6). */
|
|
18
|
+
signJson(value: unknown): Promise<string>;
|
|
19
|
+
/** Chaves públicas para verificar o que este signer assinou. */
|
|
20
|
+
keySet(): JWTVerifyGetKey;
|
|
21
|
+
/** Algoritmos que este signer pode ter usado (para o `jwtVerify`). */
|
|
22
|
+
readonly algorithms: string[];
|
|
23
|
+
}
|
|
24
|
+
/** Lança quando o JWKS não tem nenhuma chave que possa assinar tokens de delegação. */
|
|
25
|
+
export declare function assertDelegationSigningKey(jwks: {
|
|
26
|
+
keys: Record<string, any>[];
|
|
27
|
+
}): void;
|
|
28
|
+
/**
|
|
29
|
+
* Signer sobre o keystore do IdP: a primeira chave ES256/RS256 do JWKS assina
|
|
30
|
+
* (a primeira é a corrente após uma rotação); o JWKS público inteiro verifica,
|
|
31
|
+
* então tokens assinados antes de uma rotação continuam válidos no período de
|
|
32
|
+
* graça. Lê a fonte a cada chamada — rotação ao vivo não exige restart.
|
|
33
|
+
*/
|
|
34
|
+
export declare function keystoreSigner(source: SigningKeySource): AgentSigner;
|