@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
package/README.md
CHANGED
|
@@ -14,6 +14,10 @@ wrapper around [`oidc-provider`](https://github.com/panva/node-oidc-provider).
|
|
|
14
14
|
- **Personal agents** — AI assistants acting for your users: agent identity by signed JWT
|
|
15
15
|
(JWKS, no shared secrets) and user-approved, revocable delegation with scopes you define,
|
|
16
16
|
over a pluggable protocol adapter ([PACT](https://openpactprotocol.org) built in).
|
|
17
|
+
Plus the [Personal Agent Protocol ("Poppy")](https://personalagentprotocol.org/docs/spec):
|
|
18
|
+
discovery, DPoP-bound agent sessions, Direct/Device/Mediated sign-in, MCP and browser sessions.
|
|
19
|
+
⚠️ **Experimental** — tracks the spec's Draft 0.1 and will change (possibly breaking, outside
|
|
20
|
+
semver majors) while the spec is a draft.
|
|
17
21
|
- **Native apps** — RFC 8252 native clients (`authkit:clients:create --native`: private-use
|
|
18
22
|
scheme / claimed https / loopback redirects, always public + PKCE) and the `@adonisjs/auth`
|
|
19
23
|
guards `oidcRpGuard` (session) and `oidcBearerGuard` (`Authorization: Bearer` access tokens,
|
|
@@ -8,7 +8,16 @@
|
|
|
8
8
|
@if(request)
|
|
9
9
|
<form method="POST" action="{{ action }}">
|
|
10
10
|
<input type="hidden" name="_csrf" value="{{ csrfToken }}">
|
|
11
|
-
|
|
11
|
+
@if(request.hidden)
|
|
12
|
+
@each(field in request.hidden)
|
|
13
|
+
<input type="hidden" name="{{ field.name }}" value="{{ field.value }}">
|
|
14
|
+
@end
|
|
15
|
+
@else
|
|
16
|
+
<input type="hidden" name="user_code" value="{{ request.userCode }}">
|
|
17
|
+
@end
|
|
18
|
+
@if(request.logoUri)
|
|
19
|
+
<img src="{{ request.logoUri }}" alt="" class="mb-4 h-12 w-12 rounded-lg" referrerpolicy="no-referrer">
|
|
20
|
+
@end
|
|
12
21
|
<h1 class="text-xl font-semibold text-gray-900">{{ t('agents.consent.title', { agent: request.agentName }) }}</h1>
|
|
13
22
|
<p class="mt-2 text-sm text-gray-600">{{ t('agents.consent.body', { origin: request.agentOrigin }) }}</p>
|
|
14
23
|
@if(account && account.email)
|
|
@@ -25,7 +34,9 @@
|
|
|
25
34
|
@end
|
|
26
35
|
</fieldset>
|
|
27
36
|
|
|
28
|
-
|
|
37
|
+
@if(request.userCode)
|
|
38
|
+
<p class="mt-4 text-xs text-gray-500">{{ t('agents.consent.code', { code: request.userCode }) }}</p>
|
|
39
|
+
@end
|
|
29
40
|
|
|
30
41
|
<div class="mt-6 flex gap-3">
|
|
31
42
|
<button type="submit" name="decision" value="deny"
|
|
@@ -16,6 +16,9 @@
|
|
|
16
16
|
@elseif(status === 'denied')
|
|
17
17
|
<h1 class="text-xl font-semibold text-gray-900">{{ t('agents.done.denied_title') }}</h1>
|
|
18
18
|
<p class="mt-2 text-sm text-gray-600">{{ t('agents.done.return') }}</p>
|
|
19
|
+
@elseif(status === 'invalid')
|
|
20
|
+
<h1 class="text-xl font-semibold text-gray-900">{{ t('agents.done.invalid_title') }}</h1>
|
|
21
|
+
<p class="mt-2 text-sm text-gray-600">{{ t('agents.done.invalid_body') }}</p>
|
|
19
22
|
@else
|
|
20
23
|
<h1 class="text-xl font-semibold text-gray-900">{{ t('agents.done.expired_title') }}</h1>
|
|
21
24
|
<p class="mt-2 text-sm text-gray-600">{{ t('agents.done.expired_body') }}</p>
|
package/build/index.d.ts
CHANGED
|
@@ -16,6 +16,15 @@ export type { PersonalAgentDelegationConfigInput, PersonalAgentRegistration, Per
|
|
|
16
16
|
export type { DelegationContext, DelegationGrantSummary, DelegationReceipt, } from './src/agents/delegation_service.js';
|
|
17
17
|
export type { PersonalAgentAuthOptions, PersonalAgentRequestContext, } from './src/agents/middleware.js';
|
|
18
18
|
export { personalAgentAuth, personalAgentOf, personalAgentReceipt, personalAgentSecurity, personalAgentStepUp, } from './src/agents/middleware.js';
|
|
19
|
+
export type { PoppyAgentProtocolEntry, PoppyApiEntry, PoppyBrowserLogin, PoppyClientsConfigInput, PoppyConfigInput, PoppyLimiter, PoppyMediatedConfigInput, PoppyMediatedField, PoppyMediatedResult, PoppyMediatedVerifyInput, PoppyPrincipalBase, PoppyReplayStore, ResolvedPoppyConfig, } from './src/agents/poppy/config.js';
|
|
20
|
+
export { POPPY_READ, POPPY_WRITE } from './src/agents/poppy/config.js';
|
|
21
|
+
export { accessTokenHash, type DpopNonceSource, type DpopResult, hmacNonceSource, type VerifyDpopInput, verifyDpopProof, } from './src/agents/poppy/dpop.js';
|
|
22
|
+
export { PoppyError, type PoppyErrorCode } from './src/agents/poppy/errors.js';
|
|
23
|
+
export { databaseReplayStore, memoryReplayStore, redisReplayStore, } from './src/agents/poppy/replay.js';
|
|
24
|
+
export { installPoppySlots, POPPY_AUTHENTICATE_SLOT, type PoppyAuthenticate, type PoppyAuthResult, type PoppyPrincipal, type PoppySlotRequest, poppyAuth, poppyOf, verifyPoppyRequest, writePoppyAuthError, } from './src/agents/poppy/resource.js';
|
|
25
|
+
export { isBlockedAddress, SafeFetchError, safeFetchJson, } from './src/agents/poppy/safe_fetch.js';
|
|
26
|
+
export type { PoppyAuthError, PoppyGrantSummary, PoppySessionState, PoppyTokenResponse, PoppyVerifyOptions, } from './src/agents/poppy/service.js';
|
|
27
|
+
export { type PoppyWebSessionMarker, poppyWebSession, poppyWebSessionOf, } from './src/agents/poppy/web_session.js';
|
|
19
28
|
export type { BuiltinProtocolId, PactOptions, PersonalAgentProtocol, } from './src/agents/protocol.js';
|
|
20
29
|
export { pact, pactProtocol } from './src/agents/protocol.js';
|
|
21
30
|
export type { AuditEvent, AuditEventType, AuditPage, AuditSink, ListAuditParams, StoredAuditEvent, } from './src/audit/audit_sink.js';
|
|
@@ -99,6 +108,7 @@ export { OidcRpGuard, oidcRpGuard } from './src/host/oidc_rp_guard.js';
|
|
|
99
108
|
export { evaluateLoginOtp, generateOtpCode, OTP_LOGIN_DEFAULTS, type OtpLoginConfigInput, type OtpVerifyOutcome, type ResolvedOtpLoginConfig, resolveOtpLoginConfig, } from './src/host/otp_login.js';
|
|
100
109
|
export type { PersistentRpOptions } from './src/host/persistent_rp_session.js';
|
|
101
110
|
export { endRpSession } from './src/host/persistent_rp_session.js';
|
|
111
|
+
export { POPPY_WEB_SESSION_KEY } from './src/host/poppy_session_key.js';
|
|
102
112
|
export type { AuthThrottles, ThrottleMiddleware } from './src/host/rate_limit.js';
|
|
103
113
|
export { createAuthThrottles } from './src/host/rate_limit.js';
|
|
104
114
|
export type { AccountScreensOptions, AuthHostOptions, AuthHostRouteMap, } from './src/host/register_auth_host.js';
|
package/build/index.js
CHANGED
|
@@ -9,6 +9,13 @@ export { AuthOrganization, AuthOrganizationInvitation, AuthOrganizationMember, d
|
|
|
9
9
|
export { lucidStores } from './src/accounts/lucid_stores.js';
|
|
10
10
|
export { PersonalAgentVerifier } from './src/agents/agent_identity.js';
|
|
11
11
|
export { personalAgentAuth, personalAgentOf, personalAgentReceipt, personalAgentSecurity, personalAgentStepUp, } from './src/agents/middleware.js';
|
|
12
|
+
export { POPPY_READ, POPPY_WRITE } from './src/agents/poppy/config.js';
|
|
13
|
+
export { accessTokenHash, hmacNonceSource, verifyDpopProof, } from './src/agents/poppy/dpop.js';
|
|
14
|
+
export { PoppyError } from './src/agents/poppy/errors.js';
|
|
15
|
+
export { databaseReplayStore, memoryReplayStore, redisReplayStore, } from './src/agents/poppy/replay.js';
|
|
16
|
+
export { installPoppySlots, POPPY_AUTHENTICATE_SLOT, poppyAuth, poppyOf, verifyPoppyRequest, writePoppyAuthError, } from './src/agents/poppy/resource.js';
|
|
17
|
+
export { isBlockedAddress, SafeFetchError, safeFetchJson, } from './src/agents/poppy/safe_fetch.js';
|
|
18
|
+
export { poppyWebSession, poppyWebSessionOf, } from './src/agents/poppy/web_session.js';
|
|
12
19
|
export { pact, pactProtocol } from './src/agents/protocol.js';
|
|
13
20
|
export { lucidAuditSink } from './src/audit/lucid_audit_sink.js';
|
|
14
21
|
export { adapters, defineConfig, resolveAdmin, resolveAdminApi, resolveAuthMethodsConfig, resolveDynamicRegistration, resolveLogin, resolveNotifications, resolveOrganizations, resolvePasswordless, resolveRateLimit, resolveRegistration, resolveWebauthn, toSeconds, } from './src/define_config.js';
|
|
@@ -80,6 +87,7 @@ export { OidcRpGuard, oidcRpGuard } from './src/host/oidc_rp_guard.js';
|
|
|
80
87
|
// Login por OTP (código digitável): config + helpers puros.
|
|
81
88
|
export { evaluateLoginOtp, generateOtpCode, OTP_LOGIN_DEFAULTS, resolveOtpLoginConfig, } from './src/host/otp_login.js';
|
|
82
89
|
export { endRpSession } from './src/host/persistent_rp_session.js';
|
|
90
|
+
export { POPPY_WEB_SESSION_KEY } from './src/host/poppy_session_key.js';
|
|
83
91
|
export { createAuthThrottles } from './src/host/rate_limit.js';
|
|
84
92
|
export { registerAuthHost } from './src/host/register_auth_host.js';
|
|
85
93
|
export { edgeRenderer } from './src/host/renderers/edge_renderer.js';
|
|
@@ -130,7 +130,14 @@ export default class AuthkitServerProvider {
|
|
|
130
130
|
// Defaults estruturais de `config.routes` (o argumento ainda vence).
|
|
131
131
|
routes: typeof config.routes === 'object' ? config.routes : undefined,
|
|
132
132
|
lockedRouteOptions: config.lockedRouteOptions,
|
|
133
|
-
personalAgents: config.personalAgents
|
|
133
|
+
personalAgents: config.personalAgents
|
|
134
|
+
? {
|
|
135
|
+
prefix: config.personalAgents.prefix,
|
|
136
|
+
...(config.personalAgents.poppy
|
|
137
|
+
? { poppyPrefix: config.personalAgents.poppy.prefix }
|
|
138
|
+
: {}),
|
|
139
|
+
}
|
|
140
|
+
: undefined,
|
|
134
141
|
// API headless — repassa pro registerAuthHost montar as rotas.
|
|
135
142
|
headless: config.headless
|
|
136
143
|
? {
|
|
@@ -139,6 +146,13 @@ export default class AuthkitServerProvider {
|
|
|
139
146
|
}
|
|
140
147
|
: undefined,
|
|
141
148
|
});
|
|
149
|
+
// Poppy: o slot global que o `@adonis-agora/agent` usa para autenticar o endpoint de
|
|
150
|
+
// conversas (e o MCP) sem importar este pacote — o contrato é o símbolo.
|
|
151
|
+
if (config.personalAgents?.poppy) {
|
|
152
|
+
const { installPoppySlots } = await import('../src/agents/poppy/resource.js');
|
|
153
|
+
const { poppyUrls } = await import('../src/agents/poppy/service.js');
|
|
154
|
+
installPoppySlots(poppyUrls(config.issuer, config.personalAgents.poppy.prefix).issuer);
|
|
155
|
+
}
|
|
142
156
|
// Auto-montagem das rotas (`config.routes`). Chama a MESMA função exportada que o
|
|
143
157
|
// `start/routes.ts` chamaria; não há segunda implementação. "As rotas não subiram" não pode
|
|
144
158
|
// degradar em silêncio: uma falha aqui propaga.
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* recibos) não depende de protocolo; o formato do fio vem do adapter em
|
|
13
13
|
* `protocol` (default: PACT — https://openpactprotocol.org). Ver `protocol.ts`.
|
|
14
14
|
*/
|
|
15
|
+
import { type PoppyConfigInput, type ResolvedPoppyConfig } from './poppy/config.js';
|
|
15
16
|
import { type BuiltinProtocolId, type PersonalAgentProtocol } from './protocol.js';
|
|
16
17
|
/** Registro de um personal agent conhecido (PACT §3.1). */
|
|
17
18
|
export interface PersonalAgentRegistration {
|
|
@@ -55,10 +56,12 @@ export interface PersonalAgentsConfigInput {
|
|
|
55
56
|
*/
|
|
56
57
|
protocol?: BuiltinProtocolId | PersonalAgentProtocol;
|
|
57
58
|
/**
|
|
58
|
-
* Valor que os personal agents põem no `aud` do JWT deles. Opaco e
|
|
59
|
-
* por ESTE app no registro do agente — um valor por app, não
|
|
59
|
+
* Valor que os personal agents põem no `aud` do JWT deles (PACT). Opaco e
|
|
60
|
+
* atribuído por ESTE app no registro do agente — um valor por app, não
|
|
61
|
+
* derivado de URL. Liga o PACT; obrigatório, a não ser num app só-Poppy
|
|
62
|
+
* (`poppy` sem `audience`).
|
|
60
63
|
*/
|
|
61
|
-
audience
|
|
64
|
+
audience?: string;
|
|
62
65
|
/**
|
|
63
66
|
* Agentes aceitos: uma lista estática, ou uma função que resolve pelo `iss`
|
|
64
67
|
* (para quem guarda o registro no banco). Default: nenhum.
|
|
@@ -75,6 +78,15 @@ export interface PersonalAgentsConfigInput {
|
|
|
75
78
|
delegation?: PersonalAgentDelegationConfigInput;
|
|
76
79
|
/** Prefixo das rotas (`{prefix}/oauth/*`, `{prefix}/consent`). Default: `/agents`. */
|
|
77
80
|
prefix?: string;
|
|
81
|
+
/**
|
|
82
|
+
* Personal Agent Protocol ("Poppy", https://personalagentprotocol.org) — o
|
|
83
|
+
* segundo protocolo, ao lado do PACT. Um app pode servir os dois. Ver
|
|
84
|
+
* `poppy/config.ts`.
|
|
85
|
+
*
|
|
86
|
+
* @experimental Poppy (Personal Agent Protocol) Draft 0.1 — acompanha a spec em desenvolvimento e PODE MUDAR
|
|
87
|
+
* de forma incompatível fora de majors enquanto ela for draft.
|
|
88
|
+
*/
|
|
89
|
+
poppy?: PoppyConfigInput;
|
|
78
90
|
}
|
|
79
91
|
export interface ResolvedPersonalAgentDelegationConfig {
|
|
80
92
|
interfaceUrl: string;
|
|
@@ -85,12 +97,19 @@ export interface ResolvedPersonalAgentDelegationConfig {
|
|
|
85
97
|
pollInterval: number;
|
|
86
98
|
}
|
|
87
99
|
export interface ResolvedPersonalAgentsConfig {
|
|
100
|
+
/** PACT ligado (`audience` configurado). */
|
|
101
|
+
pact: boolean;
|
|
88
102
|
protocol: PersonalAgentProtocol;
|
|
103
|
+
/** `''` quando o PACT está desligado. */
|
|
89
104
|
audience: string;
|
|
90
105
|
resolveAgent: PersonalAgentResolver;
|
|
106
|
+
/** A lista `agents` do config quando é um array (o console mostra); `null` = resolver do host. */
|
|
107
|
+
staticAgents: PersonalAgentRegistration[] | null;
|
|
91
108
|
open: boolean;
|
|
92
109
|
delegation?: ResolvedPersonalAgentDelegationConfig;
|
|
93
110
|
prefix: string;
|
|
111
|
+
/** Poppy resolvido (undefined = desligado). */
|
|
112
|
+
poppy?: ResolvedPoppyConfig;
|
|
94
113
|
}
|
|
95
114
|
export declare function normalizePersonalAgentsPrefix(prefix: string | undefined): string;
|
|
96
115
|
export declare function resolvePersonalAgentsConfig(input: PersonalAgentsConfigInput | undefined): ResolvedPersonalAgentsConfig | undefined;
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* recibos) não depende de protocolo; o formato do fio vem do adapter em
|
|
13
13
|
* `protocol` (default: PACT — https://openpactprotocol.org). Ver `protocol.ts`.
|
|
14
14
|
*/
|
|
15
|
+
import { resolvePoppyConfig, } from './poppy/config.js';
|
|
15
16
|
import { BUILTIN_PROTOCOLS, } from './protocol.js';
|
|
16
17
|
const SCOPE_ID = /^[\x21\x23-\x5B\x5D-\x7E]+$/; // RFC 6749 §3.3 scope-token
|
|
17
18
|
function assertUrl(value, what) {
|
|
@@ -39,14 +40,23 @@ export function normalizePersonalAgentsPrefix(prefix) {
|
|
|
39
40
|
export function resolvePersonalAgentsConfig(input) {
|
|
40
41
|
if (!input)
|
|
41
42
|
return undefined;
|
|
42
|
-
|
|
43
|
-
|
|
43
|
+
const pact = !!input.audience?.trim();
|
|
44
|
+
if (!pact && !input.poppy) {
|
|
45
|
+
throw new Error('authkit: personalAgents.audience é obrigatório (o `aud` dos JWTs dos agentes PACT) — ou configure só `personalAgents.poppy`.');
|
|
46
|
+
}
|
|
47
|
+
if (!pact && input.delegation) {
|
|
48
|
+
throw new Error('authkit: personalAgents.delegation é do PACT e exige `personalAgents.audience`.');
|
|
49
|
+
}
|
|
50
|
+
if (input.protocol === 'poppy') {
|
|
51
|
+
throw new Error('authkit: o Poppy não é um `protocol` do PACT — configure-o em `personalAgents.poppy` (os dois convivem).');
|
|
44
52
|
}
|
|
45
53
|
let resolveAgent;
|
|
54
|
+
let staticAgents = null;
|
|
46
55
|
if (typeof input.agents === 'function') {
|
|
47
56
|
resolveAgent = input.agents;
|
|
48
57
|
}
|
|
49
58
|
else {
|
|
59
|
+
staticAgents = [...(input.agents ?? [])];
|
|
50
60
|
const byIssuer = new Map();
|
|
51
61
|
for (const agent of input.agents ?? []) {
|
|
52
62
|
assertUrl(agent.issuer, `agents[].issuer`);
|
|
@@ -83,13 +93,21 @@ export function resolvePersonalAgentsConfig(input) {
|
|
|
83
93
|
if (!protocol) {
|
|
84
94
|
throw new Error(`authkit: personalAgents.protocol "${String(input.protocol)}" desconhecido (embutidos: ${Object.keys(BUILTIN_PROTOCOLS).join(', ')}).`);
|
|
85
95
|
}
|
|
96
|
+
const prefix = normalizePersonalAgentsPrefix(input.prefix);
|
|
97
|
+
const poppy = resolvePoppyConfig(input.poppy, input.delegation?.scopes);
|
|
98
|
+
if (poppy && pact && (poppy.prefix === prefix || poppy.prefix.startsWith(`${prefix}/`))) {
|
|
99
|
+
throw new Error(`authkit: personalAgents.poppy.prefix "${poppy.prefix}" não pode ficar debaixo de personalAgents.prefix ("${prefix}").`);
|
|
100
|
+
}
|
|
86
101
|
return {
|
|
102
|
+
pact,
|
|
87
103
|
protocol,
|
|
88
|
-
audience: input.audience,
|
|
104
|
+
audience: pact ? input.audience : '',
|
|
89
105
|
resolveAgent,
|
|
106
|
+
staticAgents,
|
|
90
107
|
open: input.open === true,
|
|
91
108
|
delegation,
|
|
92
|
-
prefix
|
|
109
|
+
prefix,
|
|
110
|
+
poppy,
|
|
93
111
|
};
|
|
94
112
|
}
|
|
95
113
|
/** `"a b c"` → `['a','b','c']` sem duplicatas, na ordem. */
|
|
@@ -30,6 +30,13 @@ function newUserCode() {
|
|
|
30
30
|
code += USER_CODE_ALPHABET[randomInt(USER_CODE_ALPHABET.length)];
|
|
31
31
|
return `${code.slice(0, 4)}-${code.slice(4)}`;
|
|
32
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* A tabela de grants é compartilhada com o Poppy (Account Tokens, `pgrant_`):
|
|
35
|
+
* o PACT só enxerga os dele.
|
|
36
|
+
*/
|
|
37
|
+
function isPactGrant(id) {
|
|
38
|
+
return !id.startsWith('pgrant_');
|
|
39
|
+
}
|
|
33
40
|
/** Aceita o código como o usuário digitar: minúsculas, sem hífen, com espaços. */
|
|
34
41
|
export function normalizeUserCode(input) {
|
|
35
42
|
if (typeof input !== 'string')
|
|
@@ -183,7 +190,7 @@ export class PersonalAgentDelegation {
|
|
|
183
190
|
if (!userCode)
|
|
184
191
|
return null;
|
|
185
192
|
const row = await this.#store.findDeviceByUserCode(userCode);
|
|
186
|
-
if (
|
|
193
|
+
if (row?.status !== 'pending' || row.expiresAt <= this.#now())
|
|
187
194
|
return null;
|
|
188
195
|
return {
|
|
189
196
|
userCode: row.userCode,
|
|
@@ -201,7 +208,7 @@ export class PersonalAgentDelegation {
|
|
|
201
208
|
const userCode = normalizeUserCode(input.userCode);
|
|
202
209
|
const row = userCode ? await this.#store.findDeviceByUserCode(userCode) : null;
|
|
203
210
|
const now = this.#now();
|
|
204
|
-
if (
|
|
211
|
+
if (row?.status !== 'pending' || row.expiresAt <= now)
|
|
205
212
|
return null;
|
|
206
213
|
const requested = parseScope(row.requestedScope);
|
|
207
214
|
const granted = requested.filter((id) => input.scopes.includes(id));
|
|
@@ -217,7 +224,7 @@ export class PersonalAgentDelegation {
|
|
|
217
224
|
return null;
|
|
218
225
|
const expiresAt = new Date(now.getTime() + this.#cfg.grantTtl * 1000);
|
|
219
226
|
const existing = (await this.#store.findGrantsFor(input.accountId, row.clientId, row.agentSub))
|
|
220
|
-
.filter((g) => g.expiresAt > now)
|
|
227
|
+
.filter((g) => isPactGrant(g.id) && g.expiresAt > now)
|
|
221
228
|
.sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime())[0];
|
|
222
229
|
let result = null;
|
|
223
230
|
if (existing) {
|
|
@@ -248,7 +255,7 @@ export class PersonalAgentDelegation {
|
|
|
248
255
|
async deny(input) {
|
|
249
256
|
const userCode = normalizeUserCode(input.userCode);
|
|
250
257
|
const row = userCode ? await this.#store.findDeviceByUserCode(userCode) : null;
|
|
251
|
-
if (
|
|
258
|
+
if (row?.status !== 'pending')
|
|
252
259
|
return false;
|
|
253
260
|
return this.#store.transitionDevice(row.id, 'pending', {
|
|
254
261
|
status: 'denied',
|
|
@@ -259,7 +266,7 @@ export class PersonalAgentDelegation {
|
|
|
259
266
|
async listGrants(accountId) {
|
|
260
267
|
const now = this.#now();
|
|
261
268
|
return (await this.#store.listGrants(accountId))
|
|
262
|
-
.filter((g) => g.expiresAt > now)
|
|
269
|
+
.filter((g) => isPactGrant(g.id) && g.expiresAt > now)
|
|
263
270
|
.map((g) => ({
|
|
264
271
|
id: g.id,
|
|
265
272
|
clientId: g.clientId,
|
|
@@ -270,7 +277,9 @@ export class PersonalAgentDelegation {
|
|
|
270
277
|
}));
|
|
271
278
|
}
|
|
272
279
|
/** Revoga na hora: tokens já emitidos param de valer na próxima request. */
|
|
273
|
-
revokeGrant(accountId, grantId) {
|
|
280
|
+
async revokeGrant(accountId, grantId) {
|
|
281
|
+
if (!isPactGrant(grantId))
|
|
282
|
+
return false;
|
|
274
283
|
return this.#store.revokeGrant(accountId, grantId, this.#now());
|
|
275
284
|
}
|
|
276
285
|
/** Revoga TODAS as delegações da conta. Devolve quantas estavam vivas. */
|
|
@@ -340,7 +349,7 @@ export class PersonalAgentDelegation {
|
|
|
340
349
|
*/
|
|
341
350
|
async #converge(accountId, clientId, agentSub, now) {
|
|
342
351
|
const live = (await this.#store.findGrantsFor(accountId, clientId, agentSub))
|
|
343
|
-
.filter((g) => g.expiresAt > now)
|
|
352
|
+
.filter((g) => isPactGrant(g.id) && g.expiresAt > now)
|
|
344
353
|
.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime() || a.id.localeCompare(b.id));
|
|
345
354
|
const [survivor, ...extras] = live;
|
|
346
355
|
if (!survivor)
|
|
@@ -355,7 +364,8 @@ export class PersonalAgentDelegation {
|
|
|
355
364
|
return { grantId: survivor.id, scopes: parseScope(scope) };
|
|
356
365
|
}
|
|
357
366
|
async #grantUsable(grant, agent, now) {
|
|
358
|
-
return (grant.
|
|
367
|
+
return (isPactGrant(grant.id) &&
|
|
368
|
+
grant.revokedAt === null &&
|
|
359
369
|
grant.expiresAt > now &&
|
|
360
370
|
grant.clientId === agent.issuer &&
|
|
361
371
|
grant.agentSub === agent.sub &&
|
|
@@ -37,7 +37,13 @@ export interface GrantRow {
|
|
|
37
37
|
createdAt: Date;
|
|
38
38
|
updatedAt: Date;
|
|
39
39
|
}
|
|
40
|
+
export interface LiveGrantFilter {
|
|
41
|
+
protocol?: 'pact' | 'poppy';
|
|
42
|
+
clientId?: string;
|
|
43
|
+
accountId?: string;
|
|
44
|
+
}
|
|
40
45
|
export declare class DelegationStore {
|
|
46
|
+
#private;
|
|
41
47
|
private conn;
|
|
42
48
|
/** `conn` = uma conexão Lucid (`db.connection(name?)`) — ou uma transação. */
|
|
43
49
|
constructor(conn: () => any);
|
|
@@ -75,6 +81,16 @@ export declare class DelegationStore {
|
|
|
75
81
|
/** Revoga um grant DA CONTA. `false` = não existe, é de outra conta ou já revogado. */
|
|
76
82
|
revokeGrant(accountId: string, id: string, at: Date): Promise<boolean>;
|
|
77
83
|
revokeAllGrants(accountId: string, at: Date): Promise<number>;
|
|
84
|
+
listLiveGrants(input: LiveGrantFilter & {
|
|
85
|
+
now: Date;
|
|
86
|
+
offset: number;
|
|
87
|
+
limit: number;
|
|
88
|
+
}): Promise<GrantRow[]>;
|
|
89
|
+
countLiveGrants(now: Date, filter?: LiveGrantFilter): Promise<number>;
|
|
90
|
+
/** Conexões vivas de um protocolo por agente: `client_id` → número de grants. */
|
|
91
|
+
countLiveGrantsByClient(now: Date, protocol: 'pact' | 'poppy'): Promise<Map<string, number>>;
|
|
92
|
+
/** Ids dos grants não revogados de um agente (vivos ou não) — para revogar todos. */
|
|
93
|
+
unrevokedGrantIdsOfClient(clientId: string): Promise<string[]>;
|
|
78
94
|
/** Revoga um grant sem checar a conta — reação a reuso de refresh token. */
|
|
79
95
|
revokeGrantById(id: string, at: Date): Promise<void>;
|
|
80
96
|
insertRefreshToken(row: {
|
|
@@ -183,6 +183,50 @@ export class DelegationStore {
|
|
|
183
183
|
.update({ revoked_at: at, updated_at: at });
|
|
184
184
|
return affected(result);
|
|
185
185
|
}
|
|
186
|
+
// ─── console admin (todas as contas) ──────────────────────────────────────
|
|
187
|
+
/**
|
|
188
|
+
* Grants vivos (não revogados nem expirados em `now`), de todas as contas. O
|
|
189
|
+
* protocolo sai do prefixo do id: `agrant_` (PACT) ou `pgrant_` (Poppy).
|
|
190
|
+
*/
|
|
191
|
+
#live(now, filter = {}) {
|
|
192
|
+
let q = this.conn().from(GRANT_TABLE).whereNull('revoked_at').where('expires_at', '>', now);
|
|
193
|
+
if (filter.protocol)
|
|
194
|
+
q = q.where('id', 'like', filter.protocol === 'poppy' ? 'pgrant_%' : 'agrant_%');
|
|
195
|
+
if (filter.clientId)
|
|
196
|
+
q = q.where('client_id', filter.clientId);
|
|
197
|
+
if (filter.accountId)
|
|
198
|
+
q = q.where('account_id', filter.accountId);
|
|
199
|
+
return q;
|
|
200
|
+
}
|
|
201
|
+
async listLiveGrants(input) {
|
|
202
|
+
const rows = await this.#live(input.now, input)
|
|
203
|
+
.orderBy('created_at', 'desc')
|
|
204
|
+
.orderBy('id', 'asc')
|
|
205
|
+
.offset(input.offset)
|
|
206
|
+
.limit(input.limit);
|
|
207
|
+
return rows.map(toGrant);
|
|
208
|
+
}
|
|
209
|
+
async countLiveGrants(now, filter = {}) {
|
|
210
|
+
const row = await this.#live(now, filter).count('* as total').first();
|
|
211
|
+
return Number(row?.total ?? 0);
|
|
212
|
+
}
|
|
213
|
+
/** Conexões vivas de um protocolo por agente: `client_id` → número de grants. */
|
|
214
|
+
async countLiveGrantsByClient(now, protocol) {
|
|
215
|
+
const rows = await this.#live(now, { protocol })
|
|
216
|
+
.select('client_id')
|
|
217
|
+
.count('* as total')
|
|
218
|
+
.groupBy('client_id');
|
|
219
|
+
return new Map(rows.map((r) => [r.client_id, Number(r.total)]));
|
|
220
|
+
}
|
|
221
|
+
/** Ids dos grants não revogados de um agente (vivos ou não) — para revogar todos. */
|
|
222
|
+
async unrevokedGrantIdsOfClient(clientId) {
|
|
223
|
+
const rows = await this.conn()
|
|
224
|
+
.from(GRANT_TABLE)
|
|
225
|
+
.where('client_id', clientId)
|
|
226
|
+
.whereNull('revoked_at')
|
|
227
|
+
.select('id');
|
|
228
|
+
return rows.map((r) => r.id);
|
|
229
|
+
}
|
|
186
230
|
/** Revoga um grant sem checar a conta — reação a reuso de refresh token. */
|
|
187
231
|
async revokeGrantById(id, at) {
|
|
188
232
|
await this.conn()
|
|
@@ -40,7 +40,8 @@ export function personalAgentAuth(options = {}) {
|
|
|
40
40
|
const mode = options.delegation ?? 'optional';
|
|
41
41
|
return async (ctx, next) => {
|
|
42
42
|
const runtime = await personalAgentsFor(ctx);
|
|
43
|
-
|
|
43
|
+
// Sem PACT (app só-Poppy) não há JWT de agente para verificar.
|
|
44
|
+
if (!runtime?.config.pact)
|
|
44
45
|
return ctx.response.notFound();
|
|
45
46
|
const { protocol } = runtime.config;
|
|
46
47
|
// Autentica ANTES de ler o corpo (PACT §3.4).
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identidade do personal agent no Poppy (§4.1): o `client_id` é uma URL HTTPS
|
|
3
|
+
* que devolve o documento de metadata do agente (OAuth Client ID Metadata
|
|
4
|
+
* Document). Buscamos (à prova de SSRF), validamos, guardamos em cache e
|
|
5
|
+
* importamos as chaves públicas do `jwks_uri`.
|
|
6
|
+
*/
|
|
7
|
+
import { type JWTVerifyGetKey } from 'jose';
|
|
8
|
+
import type { ResolvedPoppyConfig } from './config.js';
|
|
9
|
+
import { type JsonFetcher } from './safe_fetch.js';
|
|
10
|
+
export interface PoppyClient {
|
|
11
|
+
clientId: string;
|
|
12
|
+
/** `client_name`, ou o host do `client_id`. */
|
|
13
|
+
name: string;
|
|
14
|
+
logoUri: string | null;
|
|
15
|
+
jwksUri: string;
|
|
16
|
+
redirectUris: string[];
|
|
17
|
+
extensions: Record<string, unknown>;
|
|
18
|
+
/** Chaves públicas do `jwks_uri`. */
|
|
19
|
+
keys: JWTVerifyGetKey;
|
|
20
|
+
}
|
|
21
|
+
/** Valida um documento de metadata e devolve os campos usados. Lança `invalid_client`. */
|
|
22
|
+
export declare function validateClientMetadata(fetchedFrom: string, doc: unknown, options?: {
|
|
23
|
+
allowHttp?: boolean;
|
|
24
|
+
}): Omit<PoppyClient, 'keys'>;
|
|
25
|
+
export interface PoppyClientRegistryOptions {
|
|
26
|
+
/** Busca de JSON. Default: {@link safeFetchJson}. */
|
|
27
|
+
fetchJson?: JsonFetcher;
|
|
28
|
+
now?: () => number;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Resolve e valida agentes pelo `client_id`, com cache (respeita `max-age`,
|
|
32
|
+
* entre 1 min e 24 h; falhas ficam 1 min em cache para que o mesmo `client_id`
|
|
33
|
+
* não vire uma busca de saída por request).
|
|
34
|
+
*/
|
|
35
|
+
export declare class PoppyClientRegistry {
|
|
36
|
+
#private;
|
|
37
|
+
constructor(cfg: ResolvedPoppyConfig, options?: PoppyClientRegistryOptions);
|
|
38
|
+
/**
|
|
39
|
+
* O agente, ou `invalid_client`: `client_id` não é HTTPS, bloqueado, não
|
|
40
|
+
* registrado (quando o app exige registro), metadata inválida/inalcançável.
|
|
41
|
+
*/
|
|
42
|
+
resolve(clientId: unknown): Promise<PoppyClient>;
|
|
43
|
+
/** Nome de exibição sem falhar (console de conta): metadata em cache/buscada, ou o host. */
|
|
44
|
+
displayName(clientId: string): Promise<string>;
|
|
45
|
+
}
|