@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,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registro de personal agents feito pelo console admin — tabela LIB-OWNED
|
|
3
|
+
* `auth_agent_registry` (ver `schema/ensure.ts`). SOMA ao `defineConfig`:
|
|
4
|
+
*
|
|
5
|
+
* - `allowed`: o agente entra mesmo fora da lista do config (PACT: issuer + JWKS;
|
|
6
|
+
* Poppy: `client_id` registrado quando `clients.requireRegistration`).
|
|
7
|
+
* - `blocked`: o agente é recusado mesmo que o config o liste — bloquear é a
|
|
8
|
+
* ação de emergência, e precisa valer sem deploy.
|
|
9
|
+
*
|
|
10
|
+
* O que o config bloqueia continua bloqueado: o console não desfaz o config.
|
|
11
|
+
*
|
|
12
|
+
* A tabela é pequena e lida em TODA verificação de agente PACT, então as linhas
|
|
13
|
+
* ficam num snapshot em memória por {@link SNAPSHOT_TTL_MS}. Escritas por este
|
|
14
|
+
* processo invalidam na hora; em outras instâncias o efeito chega no próximo
|
|
15
|
+
* refresh — o bloqueio corta os tokens já emitidos pelo banco (grants revogados,
|
|
16
|
+
* Sessions encerradas), então o atraso só vale para um agente começando do zero.
|
|
17
|
+
*/
|
|
18
|
+
import { randomBytes } from 'node:crypto';
|
|
19
|
+
export const REGISTRY_TABLE = 'auth_agent_registry';
|
|
20
|
+
/** Quanto tempo o snapshot vale antes de reler a tabela. */
|
|
21
|
+
export const SNAPSHOT_TTL_MS = 15 * 1000;
|
|
22
|
+
function toDate(value) {
|
|
23
|
+
if (value instanceof Date)
|
|
24
|
+
return value;
|
|
25
|
+
if (typeof value === 'number')
|
|
26
|
+
return new Date(value);
|
|
27
|
+
if (typeof value === 'string' && /^\d+$/.test(value))
|
|
28
|
+
return new Date(Number(value));
|
|
29
|
+
return new Date(String(value));
|
|
30
|
+
}
|
|
31
|
+
function toRow(row) {
|
|
32
|
+
return {
|
|
33
|
+
id: row.id,
|
|
34
|
+
protocol: row.protocol,
|
|
35
|
+
identifier: row.identifier,
|
|
36
|
+
status: row.status,
|
|
37
|
+
name: row.name ?? null,
|
|
38
|
+
jwksUri: row.jwks_uri ?? null,
|
|
39
|
+
updatedBy: row.updated_by ?? null,
|
|
40
|
+
createdAt: toDate(row.created_at),
|
|
41
|
+
updatedAt: toDate(row.updated_at),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const key = (protocol, identifier) => `${protocol} ${identifier}`;
|
|
45
|
+
export class AgentRegistryStore {
|
|
46
|
+
#conn;
|
|
47
|
+
#now;
|
|
48
|
+
#snapshot = null;
|
|
49
|
+
#loading = null;
|
|
50
|
+
constructor(conn, options = {}) {
|
|
51
|
+
this.#conn = conn;
|
|
52
|
+
this.#now = options.now ?? Date.now;
|
|
53
|
+
}
|
|
54
|
+
/** Todas as linhas, mais recentes primeiro. Lê do banco (console). */
|
|
55
|
+
async list() {
|
|
56
|
+
const rows = await this.#conn().from(REGISTRY_TABLE).orderBy('updated_at', 'desc');
|
|
57
|
+
return rows.map(toRow);
|
|
58
|
+
}
|
|
59
|
+
/** A linha do agente, pelo snapshot. `null` = o console não diz nada sobre ele. */
|
|
60
|
+
async find(protocol, identifier) {
|
|
61
|
+
return (await this.#rows()).get(key(protocol, identifier)) ?? null;
|
|
62
|
+
}
|
|
63
|
+
async findById(id) {
|
|
64
|
+
const row = await this.#conn().from(REGISTRY_TABLE).where('id', id).first();
|
|
65
|
+
return row ? toRow(row) : null;
|
|
66
|
+
}
|
|
67
|
+
/** Cria ou troca a linha de (protocolo, identificador). */
|
|
68
|
+
async upsert(input) {
|
|
69
|
+
const now = new Date(this.#now());
|
|
70
|
+
const existing = await this.#conn()
|
|
71
|
+
.from(REGISTRY_TABLE)
|
|
72
|
+
.where('protocol', input.protocol)
|
|
73
|
+
.where('identifier', input.identifier)
|
|
74
|
+
.first();
|
|
75
|
+
const values = {
|
|
76
|
+
status: input.status,
|
|
77
|
+
name: input.name ?? null,
|
|
78
|
+
jwks_uri: input.jwksUri ?? null,
|
|
79
|
+
updated_by: input.updatedBy ?? null,
|
|
80
|
+
updated_at: now,
|
|
81
|
+
};
|
|
82
|
+
if (existing) {
|
|
83
|
+
await this.#conn().from(REGISTRY_TABLE).where('id', existing.id).update(values);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
await this.#conn()
|
|
87
|
+
.table(REGISTRY_TABLE)
|
|
88
|
+
.insert({
|
|
89
|
+
id: `areg_${randomBytes(12).toString('base64url')}`,
|
|
90
|
+
protocol: input.protocol,
|
|
91
|
+
identifier: input.identifier,
|
|
92
|
+
created_at: now,
|
|
93
|
+
...values,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
this.invalidate();
|
|
97
|
+
const row = await this.#conn()
|
|
98
|
+
.from(REGISTRY_TABLE)
|
|
99
|
+
.where('protocol', input.protocol)
|
|
100
|
+
.where('identifier', input.identifier)
|
|
101
|
+
.first();
|
|
102
|
+
return toRow(row);
|
|
103
|
+
}
|
|
104
|
+
async delete(id) {
|
|
105
|
+
const result = await this.#conn().from(REGISTRY_TABLE).where('id', id).delete();
|
|
106
|
+
this.invalidate();
|
|
107
|
+
return (typeof result === 'number' ? result : Array.isArray(result) ? result.length : 0) > 0;
|
|
108
|
+
}
|
|
109
|
+
invalidate() {
|
|
110
|
+
this.#snapshot = null;
|
|
111
|
+
}
|
|
112
|
+
async #rows() {
|
|
113
|
+
const now = this.#now();
|
|
114
|
+
if (this.#snapshot && this.#snapshot.expiresAt > now)
|
|
115
|
+
return this.#snapshot.rows;
|
|
116
|
+
if (!this.#loading) {
|
|
117
|
+
this.#loading = (async () => {
|
|
118
|
+
try {
|
|
119
|
+
const rows = (await this.#conn().from(REGISTRY_TABLE)).map(toRow);
|
|
120
|
+
const map = new Map(rows.map((r) => [key(r.protocol, r.identifier), r]));
|
|
121
|
+
this.#snapshot = { rows: map, expiresAt: this.#now() + SNAPSHOT_TTL_MS };
|
|
122
|
+
return map;
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
// Banco fora (ou tabela ainda não criada): fica com o último snapshot, ou
|
|
126
|
+
// nenhum — o config continua valendo sozinho. Tenta de novo logo.
|
|
127
|
+
const rows = this.#snapshot?.rows ?? new Map();
|
|
128
|
+
this.#snapshot = { rows, expiresAt: this.#now() + 1000 };
|
|
129
|
+
return rows;
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
this.#loading = null;
|
|
133
|
+
}
|
|
134
|
+
})();
|
|
135
|
+
}
|
|
136
|
+
return this.#loading;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -3,6 +3,10 @@ import type { OidcService } from '../provider/oidc_service.js';
|
|
|
3
3
|
import { PersonalAgentVerifier } from './agent_identity.js';
|
|
4
4
|
import type { ResolvedPersonalAgentsConfig } from './config.js';
|
|
5
5
|
import { PersonalAgentDelegation } from './delegation_service.js';
|
|
6
|
+
import { DelegationStore, type GrantRow } from './delegation_store.js';
|
|
7
|
+
import type { JsonFetcher } from './poppy/safe_fetch.js';
|
|
8
|
+
import { PoppyService } from './poppy/service.js';
|
|
9
|
+
import { type AgentProtocolId, AgentRegistryStore } from './registry_store.js';
|
|
6
10
|
/** URLs públicas do authorization server de delegação. */
|
|
7
11
|
export interface PersonalAgentUrls {
|
|
8
12
|
/** `issuer` (RFC 8414) — também o `iss` dos tokens de delegação. */
|
|
@@ -23,14 +27,39 @@ export declare function personalAgentUrls(oidcIssuer: string, prefix: string): P
|
|
|
23
27
|
export interface PersonalAgentsRuntime {
|
|
24
28
|
config: ResolvedPersonalAgentsConfig;
|
|
25
29
|
urls: PersonalAgentUrls;
|
|
30
|
+
/** Verificador do JWT do agente PACT (inerte quando o PACT está desligado — veja `config.pact`). */
|
|
26
31
|
verifier: PersonalAgentVerifier;
|
|
27
32
|
/** `null` quando o app não configurou `personalAgents.delegation`. */
|
|
28
33
|
delegation: PersonalAgentDelegation | null;
|
|
34
|
+
/** `null` quando o app não configurou `personalAgents.poppy`. */
|
|
35
|
+
poppy: PoppyService | null;
|
|
36
|
+
/** Revoga um grant de personal agent DA CONTA (PACT ou Poppy). */
|
|
37
|
+
revokeAgentGrant(accountId: string, grantId: string): Promise<boolean>;
|
|
38
|
+
/** Revoga todos os grants de personal agents da conta. Devolve quantos estavam vivos. */
|
|
39
|
+
revokeAllAgentGrants(accountId: string): Promise<number>;
|
|
40
|
+
/**
|
|
41
|
+
* Registro do console admin (agentes liberados/bloqueados além do config).
|
|
42
|
+
* `null` sem banco (PACT só de identidade sem `lucid.db`).
|
|
43
|
+
*/
|
|
44
|
+
registry: AgentRegistryStore | null;
|
|
45
|
+
/** Grants de todas as contas (console admin). `null` sem banco. */
|
|
46
|
+
grants: DelegationStore | null;
|
|
47
|
+
/** Revoga um grant de QUALQUER conta (console admin). Devolve o grant, ou `null` se já não valia. */
|
|
48
|
+
revokeGrantById(grantId: string): Promise<GrantRow | null>;
|
|
49
|
+
/** Desconecta um agente de todas as contas (bloqueio). Devolve quantos grants estavam vivos. */
|
|
50
|
+
disconnectAgent(protocol: AgentProtocolId, identifier: string): Promise<number>;
|
|
51
|
+
}
|
|
52
|
+
export interface BuildPersonalAgentsOptions {
|
|
53
|
+
/** Resolve serviços do container (redis do replay store). */
|
|
54
|
+
make?: (binding: string) => Promise<any>;
|
|
55
|
+
/** Busca de JSON (metadata/JWKS dos agentes Poppy) — injetável nos testes. */
|
|
56
|
+
fetchJson?: JsonFetcher;
|
|
57
|
+
now?: () => Date;
|
|
29
58
|
}
|
|
30
59
|
/**
|
|
31
60
|
* Monta (uma vez por `OidcService`) o runtime de personal agents. `null` quando
|
|
32
61
|
* `personalAgents` não está no config — as rotas respondem 404.
|
|
33
62
|
*/
|
|
34
|
-
export declare function buildPersonalAgentsRuntime(service: OidcService, makeDb: () => Promise<any
|
|
63
|
+
export declare function buildPersonalAgentsRuntime(service: OidcService, makeDb: () => Promise<any>, options?: BuildPersonalAgentsOptions): Promise<PersonalAgentsRuntime | null>;
|
|
35
64
|
/** Runtime a partir de uma request. */
|
|
36
65
|
export declare function personalAgentsFor(ctx: HttpContext): Promise<PersonalAgentsRuntime | null>;
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import { supportsAccountStatus } from '../accounts/account_store.js';
|
|
2
|
+
import { findMcpResource } from '../mcp/mcp_oauth.js';
|
|
2
3
|
import { PersonalAgentVerifier } from './agent_identity.js';
|
|
3
4
|
import { PersonalAgentDelegation } from './delegation_service.js';
|
|
4
5
|
import { DelegationStore } from './delegation_store.js';
|
|
6
|
+
import { PoppyClientRegistry } from './poppy/client_registry.js';
|
|
7
|
+
import { hmacNonceSource } from './poppy/dpop.js';
|
|
8
|
+
import { databaseReplayStore, memoryLimiter, memoryReplayStore, redisReplayStore, } from './poppy/replay.js';
|
|
9
|
+
import { PoppyService, poppyUrls } from './poppy/service.js';
|
|
10
|
+
import { PoppyStore } from './poppy/store.js';
|
|
11
|
+
import { AgentRegistryStore } from './registry_store.js';
|
|
5
12
|
import { keystoreSigner } from './signer.js';
|
|
6
13
|
/**
|
|
7
14
|
* Deriva as URLs da ORIGEM do issuer configurado — nunca do Host da request
|
|
@@ -20,11 +27,112 @@ export function personalAgentUrls(oidcIssuer, prefix) {
|
|
|
20
27
|
};
|
|
21
28
|
}
|
|
22
29
|
const runtimes = new WeakMap();
|
|
30
|
+
/** Ponte Poppy → oidc-provider: Bearer de MCP de Sessions logadas viram access tokens dele. */
|
|
31
|
+
function mcpBridge(service, poppyApis) {
|
|
32
|
+
return {
|
|
33
|
+
isMcpResource(url) {
|
|
34
|
+
try {
|
|
35
|
+
return findMcpResource(url, service.config.issuer, service.config.mcp) !== null;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
async mint(input) {
|
|
42
|
+
const provider = service.provider;
|
|
43
|
+
if (!provider?.AccessToken || !provider?.Grant)
|
|
44
|
+
return null;
|
|
45
|
+
// Só faz sentido quando a integração MCP do authkit está ligada, ou o MCP é de `apis`.
|
|
46
|
+
if (!service.config.mcp?.enabled &&
|
|
47
|
+
!poppyApis.some((a) => a.type === 'mcp' && a.url.replace(/\/+$/, '') === input.resource.replace(/\/+$/, ''))) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
const remaining = Math.max(60, Math.floor((input.grantExpiresAt.getTime() - Date.now()) / 1000));
|
|
51
|
+
let grant = await provider.Grant.find(input.grantId);
|
|
52
|
+
if (!grant) {
|
|
53
|
+
grant = new provider.Grant({
|
|
54
|
+
jti: input.grantId,
|
|
55
|
+
accountId: input.accountId,
|
|
56
|
+
clientId: input.clientId,
|
|
57
|
+
expiresIn: remaining,
|
|
58
|
+
});
|
|
59
|
+
await grant.save();
|
|
60
|
+
}
|
|
61
|
+
const at = new provider.AccessToken({
|
|
62
|
+
accountId: input.accountId,
|
|
63
|
+
clientId: input.clientId,
|
|
64
|
+
grantId: input.grantId,
|
|
65
|
+
scope: input.scope,
|
|
66
|
+
gty: 'poppy',
|
|
67
|
+
expiresIn: input.expiresIn,
|
|
68
|
+
});
|
|
69
|
+
at.aud = input.resource;
|
|
70
|
+
return at.save();
|
|
71
|
+
},
|
|
72
|
+
async revoke(grantId) {
|
|
73
|
+
const provider = service.provider;
|
|
74
|
+
if (!provider?.Grant)
|
|
75
|
+
return;
|
|
76
|
+
await provider.AccessToken?.revokeByGrantId?.(grantId);
|
|
77
|
+
const grant = await provider.Grant.find(grantId);
|
|
78
|
+
if (grant)
|
|
79
|
+
await grant.destroy();
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
async function resolveReplay(replay, conn, make) {
|
|
84
|
+
if (replay === 'memory')
|
|
85
|
+
return memoryReplayStore();
|
|
86
|
+
if (replay === 'database')
|
|
87
|
+
return databaseReplayStore(conn);
|
|
88
|
+
if (typeof replay.claim === 'function')
|
|
89
|
+
return replay;
|
|
90
|
+
const name = replay.redis;
|
|
91
|
+
if (!make)
|
|
92
|
+
throw new Error('authkit: personalAgents.poppy.replay { redis } exige o container.');
|
|
93
|
+
const redis = await make('redis');
|
|
94
|
+
return redisReplayStore(redis.connection(name));
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Config com o registro do console por cima: um agente BLOQUEADO no console é
|
|
98
|
+
* recusado mesmo que o config o liste; um LIBERADO entra mesmo fora da lista.
|
|
99
|
+
* O que o config bloqueia continua bloqueado.
|
|
100
|
+
*/
|
|
101
|
+
function withRegistry(config, registry) {
|
|
102
|
+
const resolveAgent = async (issuer) => {
|
|
103
|
+
const row = await registry.find('pact', issuer);
|
|
104
|
+
if (row?.status === 'blocked') {
|
|
105
|
+
return { issuer, jwksUri: row.jwksUri ?? '', name: row.name ?? undefined, enabled: false };
|
|
106
|
+
}
|
|
107
|
+
const configured = await config.resolveAgent(issuer);
|
|
108
|
+
if (configured)
|
|
109
|
+
return configured;
|
|
110
|
+
if (row?.status === 'allowed' && row.jwksUri) {
|
|
111
|
+
return { issuer, jwksUri: row.jwksUri, name: row.name ?? undefined };
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
};
|
|
115
|
+
const poppy = config.poppy;
|
|
116
|
+
return {
|
|
117
|
+
...config,
|
|
118
|
+
resolveAgent,
|
|
119
|
+
poppy: poppy && {
|
|
120
|
+
...poppy,
|
|
121
|
+
clients: {
|
|
122
|
+
...poppy.clients,
|
|
123
|
+
isBlocked: async (id) => (await poppy.clients.isBlocked(id)) ||
|
|
124
|
+
(await registry.find('poppy', id))?.status === 'blocked',
|
|
125
|
+
isRegistered: async (id) => (await poppy.clients.isRegistered(id)) ||
|
|
126
|
+
(await registry.find('poppy', id))?.status === 'allowed',
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|
|
23
131
|
/**
|
|
24
132
|
* Monta (uma vez por `OidcService`) o runtime de personal agents. `null` quando
|
|
25
133
|
* `personalAgents` não está no config — as rotas respondem 404.
|
|
26
134
|
*/
|
|
27
|
-
export function buildPersonalAgentsRuntime(service, makeDb) {
|
|
135
|
+
export function buildPersonalAgentsRuntime(service, makeDb, options = {}) {
|
|
28
136
|
let runtime = runtimes.get(service);
|
|
29
137
|
if (!runtime) {
|
|
30
138
|
runtime = (async () => {
|
|
@@ -32,25 +140,122 @@ export function buildPersonalAgentsRuntime(service, makeDb) {
|
|
|
32
140
|
if (!config)
|
|
33
141
|
return null;
|
|
34
142
|
const urls = personalAgentUrls(service.config.issuer, config.prefix);
|
|
35
|
-
|
|
36
|
-
|
|
143
|
+
// Conta apagada ou suspensa corta a delegação na hora.
|
|
144
|
+
const isAccountActive = async (accountId) => {
|
|
145
|
+
const store = service.config.accountStore;
|
|
146
|
+
if (!(await store.findById(accountId)))
|
|
147
|
+
return false;
|
|
148
|
+
return !(supportsAccountStatus(store) && (await store.isDisabled(accountId)));
|
|
149
|
+
};
|
|
150
|
+
let conn = null;
|
|
151
|
+
const connection = service.config.schema.connection;
|
|
152
|
+
if (config.delegation || config.poppy) {
|
|
37
153
|
const db = await makeDb();
|
|
38
|
-
|
|
154
|
+
conn = () => (connection ? db.connection(connection) : db.connection());
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
// PACT só de identidade não precisa de banco; com ele, o console pode bloquear.
|
|
158
|
+
try {
|
|
159
|
+
const db = await makeDb();
|
|
160
|
+
if (db)
|
|
161
|
+
conn = () => (connection ? db.connection(connection) : db.connection());
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
conn = null;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const store = conn ? new DelegationStore(conn) : null;
|
|
168
|
+
const registry = conn ? new AgentRegistryStore(conn) : null;
|
|
169
|
+
const effective = registry ? withRegistry(config, registry) : config;
|
|
170
|
+
let delegation = null;
|
|
171
|
+
if (config.delegation && store) {
|
|
39
172
|
delegation = new PersonalAgentDelegation({
|
|
40
173
|
cfg: config.delegation,
|
|
41
|
-
store
|
|
174
|
+
store,
|
|
42
175
|
signer: keystoreSigner(service),
|
|
43
176
|
urls: { issuer: urls.issuer, consent: urls.consent },
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
177
|
+
isAccountActive,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
let poppy = null;
|
|
181
|
+
if (effective.poppy && store && conn) {
|
|
182
|
+
const cfg = effective.poppy;
|
|
183
|
+
const limiter = cfg.rateLimit === false
|
|
184
|
+
? null
|
|
185
|
+
: typeof cfg.rateLimit === 'function'
|
|
186
|
+
? cfg.rateLimit
|
|
187
|
+
: memoryLimiter(cfg.rateLimit);
|
|
188
|
+
const secret = service.deriveSecret('poppy-dpop-nonce');
|
|
189
|
+
poppy = new PoppyService({
|
|
190
|
+
cfg,
|
|
191
|
+
urls: poppyUrls(service.config.issuer, cfg.prefix),
|
|
192
|
+
store: new PoppyStore(conn),
|
|
193
|
+
grants: store,
|
|
194
|
+
registry: new PoppyClientRegistry(cfg, { fetchJson: options.fetchJson }),
|
|
195
|
+
replay: await resolveReplay(cfg.replay, conn, options.make),
|
|
196
|
+
limiter,
|
|
197
|
+
nonces: hmacNonceSource(secret),
|
|
198
|
+
isAccountActive,
|
|
199
|
+
mcp: mcpBridge(service, cfg.apis),
|
|
200
|
+
now: options.now,
|
|
51
201
|
});
|
|
52
202
|
}
|
|
53
|
-
|
|
203
|
+
const revokeAgentGrant = async (accountId, grantId) => {
|
|
204
|
+
if (grantId.startsWith('pgrant_')) {
|
|
205
|
+
return poppy ? poppy.revokeAccountGrant(accountId, grantId) : false;
|
|
206
|
+
}
|
|
207
|
+
return delegation ? delegation.revokeGrant(accountId, grantId) : false;
|
|
208
|
+
};
|
|
209
|
+
const revokeAllAgentGrants = async (accountId) => {
|
|
210
|
+
let count = 0;
|
|
211
|
+
if (poppy)
|
|
212
|
+
count += await poppy.revokeAllAccountGrants(accountId);
|
|
213
|
+
if (delegation)
|
|
214
|
+
count += await delegation.revokeAllGrants(accountId);
|
|
215
|
+
return count;
|
|
216
|
+
};
|
|
217
|
+
const revokeGrantById = async (grantId) => {
|
|
218
|
+
if (!store)
|
|
219
|
+
return null;
|
|
220
|
+
const grant = await store.findGrant(grantId);
|
|
221
|
+
const now = options.now?.() ?? new Date();
|
|
222
|
+
// Expirado já não vale: revogar não muda nada (e não vira evento de auditoria).
|
|
223
|
+
if (!grant || grant.revokedAt || grant.expiresAt <= now)
|
|
224
|
+
return null;
|
|
225
|
+
if (grantId.startsWith('pgrant_')) {
|
|
226
|
+
if (!poppy)
|
|
227
|
+
return null;
|
|
228
|
+
await poppy.revokeGrant(grantId);
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
await store.revokeGrantById(grantId, now);
|
|
232
|
+
}
|
|
233
|
+
return grant;
|
|
234
|
+
};
|
|
235
|
+
const disconnectAgent = async (protocol, identifier) => {
|
|
236
|
+
if (!store)
|
|
237
|
+
return 0;
|
|
238
|
+
if (protocol === 'poppy')
|
|
239
|
+
return poppy ? poppy.disconnectClient(identifier) : 0;
|
|
240
|
+
const ids = (await store.unrevokedGrantIdsOfClient(identifier)).filter((id) => !id.startsWith('pgrant_'));
|
|
241
|
+
const at = options.now?.() ?? new Date();
|
|
242
|
+
for (const id of ids)
|
|
243
|
+
await store.revokeGrantById(id, at);
|
|
244
|
+
return ids.length;
|
|
245
|
+
};
|
|
246
|
+
return {
|
|
247
|
+
config: effective,
|
|
248
|
+
urls,
|
|
249
|
+
verifier: new PersonalAgentVerifier(effective),
|
|
250
|
+
delegation,
|
|
251
|
+
poppy,
|
|
252
|
+
revokeAgentGrant,
|
|
253
|
+
revokeAllAgentGrants,
|
|
254
|
+
registry,
|
|
255
|
+
grants: store,
|
|
256
|
+
revokeGrantById,
|
|
257
|
+
disconnectAgent,
|
|
258
|
+
};
|
|
54
259
|
})();
|
|
55
260
|
// Falha (DB indisponível) não fica em cache — a próxima request tenta de novo.
|
|
56
261
|
runtime.catch(() => runtimes.delete(service));
|
|
@@ -61,5 +266,7 @@ export function buildPersonalAgentsRuntime(service, makeDb) {
|
|
|
61
266
|
/** Runtime a partir de uma request. */
|
|
62
267
|
export async function personalAgentsFor(ctx) {
|
|
63
268
|
const service = await ctx.containerResolver.make('authkit.server');
|
|
64
|
-
return buildPersonalAgentsRuntime(service, () => ctx.containerResolver.make('lucid.db')
|
|
269
|
+
return buildPersonalAgentsRuntime(service, () => ctx.containerResolver.make('lucid.db'), {
|
|
270
|
+
make: (binding) => ctx.containerResolver.make(binding),
|
|
271
|
+
});
|
|
65
272
|
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* varre os `audit.record({ type: ... })` do `src/` e falha quando um evento
|
|
9
9
|
* emitido não está aqui.
|
|
10
10
|
*/
|
|
11
|
-
export declare const AUDIT_EVENT_TYPES: readonly ['login.success', 'login.failure', 'signup', 'password_reset.issued', 'password_reset.consumed', 'pat.issued', 'pat.revoked', 'pat.used', 'impersonation', 'impersonation.panel_viewed', 'impersonation.stopped', 'mfa.enabled', 'mfa.disabled', 'mfa.recovery_codes_regenerated', 'account.locked', 'passkey.registered', 'passkey.removed', 'email_verification.issued', 'email_verification.consumed', 'client.created', 'client.updated', 'client.deleted', 'session.revoked_all', 'password.changed', 'password.rehashed', 'email.change_requested', 'email.changed', 'login.new_ip_notified', 'login.new_device', 'login.otp_sent', 'login.otp_verified', 'login.otp_failed', 'login.otp_invalidated', 'bot_protection.rejected', 'grant.revoked_by_user', 'user.created', 'user.password_reset_sent', 'user.disabled', 'user.enabled', 'user.deleted', 'profile.updated', 'account.deleted', 'account.exported', 'keys.rotated', 'organization.created', 'organization.updated', 'organization.deleted', 'organization.member_added', 'organization.member_removed', 'organization.member_role_changed', 'organization.member_role_updated', 'organization.switched', 'organization.deactivated', 'organization.invitation_sent', 'organization.invitation_accepted', 'organization.invitation_revoked', 'email_change.requested', 'email_change.confirmed', 'email_change.cancelled', 'security_notice.sent', 'settings.updated', 'maintenance.enabled', 'maintenance.disabled', 'trusted_device.revoked', 'password.expired_change_forced', 'otp.locked', 'otp.unlocked', 'otp.unlock_failed', 'sudo.confirmed', 'session.single_enforced', 'account.expired_login_blocked', 'account.expiration_warned', 'login.magic_link_sent', 'session.revoked', 'account.signed_out_all', 'client.secret_regenerated', 'roles_catalog.updated', 'agent.delegation_approved', 'agent.delegation_denied', 'agent.grant_revoked'];
|
|
11
|
+
export declare const AUDIT_EVENT_TYPES: readonly ['login.success', 'login.failure', 'signup', 'password_reset.issued', 'password_reset.consumed', 'pat.issued', 'pat.revoked', 'pat.used', 'impersonation', 'impersonation.panel_viewed', 'impersonation.stopped', 'mfa.enabled', 'mfa.disabled', 'mfa.recovery_codes_regenerated', 'account.locked', 'passkey.registered', 'passkey.removed', 'email_verification.issued', 'email_verification.consumed', 'client.created', 'client.updated', 'client.deleted', 'session.revoked_all', 'password.changed', 'password.rehashed', 'email.change_requested', 'email.changed', 'login.new_ip_notified', 'login.new_device', 'login.otp_sent', 'login.otp_verified', 'login.otp_failed', 'login.otp_invalidated', 'bot_protection.rejected', 'grant.revoked_by_user', 'user.created', 'user.password_reset_sent', 'user.disabled', 'user.enabled', 'user.deleted', 'profile.updated', 'account.deleted', 'account.exported', 'keys.rotated', 'organization.created', 'organization.updated', 'organization.deleted', 'organization.member_added', 'organization.member_removed', 'organization.member_role_changed', 'organization.member_role_updated', 'organization.switched', 'organization.deactivated', 'organization.invitation_sent', 'organization.invitation_accepted', 'organization.invitation_revoked', 'email_change.requested', 'email_change.confirmed', 'email_change.cancelled', 'security_notice.sent', 'settings.updated', 'maintenance.enabled', 'maintenance.disabled', 'trusted_device.revoked', 'password.expired_change_forced', 'otp.locked', 'otp.unlocked', 'otp.unlock_failed', 'sudo.confirmed', 'session.single_enforced', 'account.expired_login_blocked', 'account.expiration_warned', 'login.magic_link_sent', 'session.revoked', 'account.signed_out_all', 'client.secret_regenerated', 'roles_catalog.updated', 'agent.delegation_approved', 'agent.delegation_denied', 'agent.grant_revoked', 'agent.grant_revoked_by_admin', 'agent.allowed', 'agent.blocked', 'agent.registry_removed'];
|
|
12
12
|
/** Tipos de eventos de auditoria relevantes para segurança emitidos pelo IdP. */
|
|
13
13
|
export type AuditEventType = (typeof AUDIT_EVENT_TYPES)[number];
|
|
14
14
|
/**
|
|
@@ -113,4 +113,10 @@ export const AUDIT_EVENT_TYPES = [
|
|
|
113
113
|
'agent.delegation_approved',
|
|
114
114
|
'agent.delegation_denied',
|
|
115
115
|
'agent.grant_revoked',
|
|
116
|
+
// Console admin: um admin revogou a conexão de um usuário, ou liberou/bloqueou um
|
|
117
|
+
// agente (PACT pelo `iss`, Poppy pelo `client_id`).
|
|
118
|
+
'agent.grant_revoked_by_admin',
|
|
119
|
+
'agent.allowed',
|
|
120
|
+
'agent.blocked',
|
|
121
|
+
'agent.registry_removed',
|
|
116
122
|
];
|
|
@@ -353,6 +353,12 @@ export function defineConfig(config) {
|
|
|
353
353
|
(personalAgents.prefix === oidcMount || personalAgents.prefix.startsWith(`${oidcMount}/`))) {
|
|
354
354
|
throw new Error(`authkit: personalAgents.prefix "${personalAgents.prefix}" não pode ficar debaixo do mountPath do OIDC ("${oidcMount}").`);
|
|
355
355
|
}
|
|
356
|
+
const poppyPrefix = personalAgents?.poppy?.prefix;
|
|
357
|
+
if (poppyPrefix &&
|
|
358
|
+
oidcMount &&
|
|
359
|
+
(poppyPrefix === oidcMount || poppyPrefix.startsWith(`${oidcMount}/`))) {
|
|
360
|
+
throw new Error(`authkit: personalAgents.poppy.prefix "${poppyPrefix}" não pode ficar debaixo do mountPath do OIDC ("${oidcMount}").`);
|
|
361
|
+
}
|
|
356
362
|
// BACKSTOP DE SUDO. Um host cujo `sudo.methods` não tem um único método
|
|
357
363
|
// satisfazível por conta sem senha fica bricado para TODA operação sob
|
|
358
364
|
// `requireSudo` — e hoje isso só se descobre quando um usuário não consegue
|
|
@@ -159,7 +159,18 @@ export default class AccountApiController {
|
|
|
159
159
|
}>;
|
|
160
160
|
/** Personal agents com delegação ativa na conta (scopes aprovados). */
|
|
161
161
|
listAgents(ctx: HttpContext): Promise<void | {
|
|
162
|
-
agents:
|
|
162
|
+
agents: {
|
|
163
|
+
id: string;
|
|
164
|
+
clientId: string;
|
|
165
|
+
scopes: {
|
|
166
|
+
id: string;
|
|
167
|
+
description: string;
|
|
168
|
+
}[];
|
|
169
|
+
createdAt: Date;
|
|
170
|
+
updatedAt: Date;
|
|
171
|
+
expiresAt: Date;
|
|
172
|
+
protocol: string;
|
|
173
|
+
}[];
|
|
163
174
|
}>;
|
|
164
175
|
/** Revoga a delegação de um personal agent (efeito imediato nos tokens). */
|
|
165
176
|
revokeAgent(ctx: HttpContext): Promise<void | {
|
|
@@ -617,20 +617,25 @@ export default class AccountApiController {
|
|
|
617
617
|
/** Personal agents com delegação ativa na conta (scopes aprovados). */
|
|
618
618
|
async listAgents(ctx) {
|
|
619
619
|
const runtime = await personalAgentsFor(ctx);
|
|
620
|
-
if (!runtime?.delegation) {
|
|
620
|
+
if (!runtime?.delegation && !runtime?.poppy) {
|
|
621
621
|
return ctx.response
|
|
622
622
|
.status(404)
|
|
623
623
|
.send(apiErr('capability_unsupported', 'Personal agent delegation is not enabled.'));
|
|
624
624
|
}
|
|
625
625
|
const userId = ctx.session.get(ACCOUNT_SESSION_KEY);
|
|
626
|
-
const
|
|
627
|
-
|
|
626
|
+
const pact = runtime.delegation
|
|
627
|
+
? (await runtime.delegation.listGrants(userId)).map((g) => ({ ...g, protocol: 'pact' }))
|
|
628
|
+
: [];
|
|
629
|
+
const poppy = runtime.poppy
|
|
630
|
+
? (await runtime.poppy.listGrants(userId)).map((g) => ({ ...g, protocol: 'poppy' }))
|
|
631
|
+
: [];
|
|
632
|
+
return { agents: [...pact, ...poppy] };
|
|
628
633
|
}
|
|
629
634
|
// ─── DELETE /account/api/agents/:id ──────────────────────────────────────
|
|
630
635
|
/** Revoga a delegação de um personal agent (efeito imediato nos tokens). */
|
|
631
636
|
async revokeAgent(ctx) {
|
|
632
637
|
const runtime = await personalAgentsFor(ctx);
|
|
633
|
-
if (!runtime?.delegation) {
|
|
638
|
+
if (!runtime?.delegation && !runtime?.poppy) {
|
|
634
639
|
return ctx.response
|
|
635
640
|
.status(404)
|
|
636
641
|
.send(apiErr('capability_unsupported', 'Personal agent delegation is not enabled.'));
|
|
@@ -638,7 +643,7 @@ export default class AccountApiController {
|
|
|
638
643
|
const service = await ctx.containerResolver.make('authkit.server');
|
|
639
644
|
const userId = ctx.session.get(ACCOUNT_SESSION_KEY);
|
|
640
645
|
const grantId = ctx.request.param('id');
|
|
641
|
-
if (!(await runtime.
|
|
646
|
+
if (!(await runtime.revokeAgentGrant(userId, grantId))) {
|
|
642
647
|
return ctx.response.status(404).send(apiErr('not_found', 'Grant not found.'));
|
|
643
648
|
}
|
|
644
649
|
await service.config.audit?.record({
|
|
@@ -49,7 +49,7 @@ export declare function grantDto(grant: AdminGrant): {
|
|
|
49
49
|
};
|
|
50
50
|
export declare function auditDto(event: StoredAuditEvent): {
|
|
51
51
|
id: string;
|
|
52
|
-
type: "account.deleted" | "account.expiration_warned" | "account.expired_login_blocked" | "account.exported" | "account.locked" | "account.signed_out_all" | "agent.delegation_approved" | "agent.delegation_denied" | "agent.grant_revoked" | "bot_protection.rejected" | "client.created" | "client.deleted" | "client.secret_regenerated" | "client.updated" | "email.change_requested" | "email.changed" | "email_change.cancelled" | "email_change.confirmed" | "email_change.requested" | "email_verification.consumed" | "email_verification.issued" | "grant.revoked_by_user" | "impersonation" | "impersonation.panel_viewed" | "impersonation.stopped" | "keys.rotated" | "login.failure" | "login.magic_link_sent" | "login.new_device" | "login.new_ip_notified" | "login.otp_failed" | "login.otp_invalidated" | "login.otp_sent" | "login.otp_verified" | "login.success" | "maintenance.disabled" | "maintenance.enabled" | "mfa.disabled" | "mfa.enabled" | "mfa.recovery_codes_regenerated" | "organization.created" | "organization.deactivated" | "organization.deleted" | "organization.invitation_accepted" | "organization.invitation_revoked" | "organization.invitation_sent" | "organization.member_added" | "organization.member_removed" | "organization.member_role_changed" | "organization.member_role_updated" | "organization.switched" | "organization.updated" | "otp.locked" | "otp.unlock_failed" | "otp.unlocked" | "passkey.registered" | "passkey.removed" | "password.changed" | "password.expired_change_forced" | "password.rehashed" | "password_reset.consumed" | "password_reset.issued" | "pat.issued" | "pat.revoked" | "pat.used" | "profile.updated" | "roles_catalog.updated" | "security_notice.sent" | "session.revoked" | "session.revoked_all" | "session.single_enforced" | "settings.updated" | "signup" | "sudo.confirmed" | "trusted_device.revoked" | "user.created" | "user.deleted" | "user.disabled" | "user.enabled" | "user.password_reset_sent";
|
|
52
|
+
type: "account.deleted" | "account.expiration_warned" | "account.expired_login_blocked" | "account.exported" | "account.locked" | "account.signed_out_all" | "agent.allowed" | "agent.blocked" | "agent.delegation_approved" | "agent.delegation_denied" | "agent.grant_revoked" | "agent.grant_revoked_by_admin" | "agent.registry_removed" | "bot_protection.rejected" | "client.created" | "client.deleted" | "client.secret_regenerated" | "client.updated" | "email.change_requested" | "email.changed" | "email_change.cancelled" | "email_change.confirmed" | "email_change.requested" | "email_verification.consumed" | "email_verification.issued" | "grant.revoked_by_user" | "impersonation" | "impersonation.panel_viewed" | "impersonation.stopped" | "keys.rotated" | "login.failure" | "login.magic_link_sent" | "login.new_device" | "login.new_ip_notified" | "login.otp_failed" | "login.otp_invalidated" | "login.otp_sent" | "login.otp_verified" | "login.success" | "maintenance.disabled" | "maintenance.enabled" | "mfa.disabled" | "mfa.enabled" | "mfa.recovery_codes_regenerated" | "organization.created" | "organization.deactivated" | "organization.deleted" | "organization.invitation_accepted" | "organization.invitation_revoked" | "organization.invitation_sent" | "organization.member_added" | "organization.member_removed" | "organization.member_role_changed" | "organization.member_role_updated" | "organization.switched" | "organization.updated" | "otp.locked" | "otp.unlock_failed" | "otp.unlocked" | "passkey.registered" | "passkey.removed" | "password.changed" | "password.expired_change_forced" | "password.rehashed" | "password_reset.consumed" | "password_reset.issued" | "pat.issued" | "pat.revoked" | "pat.used" | "profile.updated" | "roles_catalog.updated" | "security_notice.sent" | "session.revoked" | "session.revoked_all" | "session.single_enforced" | "settings.updated" | "signup" | "sudo.confirmed" | "trusted_device.revoked" | "user.created" | "user.deleted" | "user.disabled" | "user.enabled" | "user.password_reset_sent";
|
|
53
53
|
accountId: string | null;
|
|
54
54
|
email: string | null;
|
|
55
55
|
clientId: string | null;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import { extname } from 'node:path';
|
|
3
|
+
import { accountPath } from '../account_paths.js';
|
|
3
4
|
import { resolveAccountRoles } from '../account_roles.js';
|
|
4
5
|
import { getAdminPrefix } from '../admin_prefix.js';
|
|
5
6
|
// ─── Shell HTML cache ─────────────────────────────────────────────────────────
|
|
@@ -134,6 +135,12 @@ export default class AdminShellController {
|
|
|
134
135
|
currentUser,
|
|
135
136
|
endpoints: {
|
|
136
137
|
api: `${adminBase}/api`,
|
|
138
|
+
// Confirmação de identidade (sudo) das ações destrutivas.
|
|
139
|
+
confirm: accountPath('confirm'),
|
|
140
|
+
},
|
|
141
|
+
features: {
|
|
142
|
+
// A página de personal agents só aparece com `personalAgents` no config.
|
|
143
|
+
personalAgents: !!cfg.personalAgents,
|
|
137
144
|
},
|
|
138
145
|
};
|
|
139
146
|
const { html: rawHtml } = await loadShell();
|