@adonis-agora/authkit-server 0.77.0 → 0.79.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +4 -0
  2. package/build/host/views/agents/consent.edge +13 -2
  3. package/build/host/views/agents/done.edge +3 -0
  4. package/build/index.d.ts +10 -0
  5. package/build/index.js +8 -0
  6. package/build/providers/authkit_server_provider.js +15 -1
  7. package/build/src/agents/config.d.ts +22 -3
  8. package/build/src/agents/config.js +22 -4
  9. package/build/src/agents/delegation_service.js +18 -8
  10. package/build/src/agents/delegation_store.d.ts +16 -0
  11. package/build/src/agents/delegation_store.js +44 -0
  12. package/build/src/agents/middleware.js +2 -1
  13. package/build/src/agents/poppy/client_registry.d.ts +45 -0
  14. package/build/src/agents/poppy/client_registry.js +226 -0
  15. package/build/src/agents/poppy/config.d.ts +365 -0
  16. package/build/src/agents/poppy/config.js +208 -0
  17. package/build/src/agents/poppy/dpop.d.ts +61 -0
  18. package/build/src/agents/poppy/dpop.js +158 -0
  19. package/build/src/agents/poppy/errors.d.ts +20 -0
  20. package/build/src/agents/poppy/errors.js +22 -0
  21. package/build/src/agents/poppy/replay.d.ts +25 -0
  22. package/build/src/agents/poppy/replay.js +106 -0
  23. package/build/src/agents/poppy/resource.d.ts +95 -0
  24. package/build/src/agents/poppy/resource.js +125 -0
  25. package/build/src/agents/poppy/safe_fetch.d.ts +48 -0
  26. package/build/src/agents/poppy/safe_fetch.js +256 -0
  27. package/build/src/agents/poppy/service.d.ts +286 -0
  28. package/build/src/agents/poppy/service.js +1464 -0
  29. package/build/src/agents/poppy/store.d.ts +111 -0
  30. package/build/src/agents/poppy/store.js +269 -0
  31. package/build/src/agents/poppy/web_session.d.ts +32 -0
  32. package/build/src/agents/poppy/web_session.js +104 -0
  33. package/build/src/agents/protocol.d.ts +8 -4
  34. package/build/src/agents/registry_store.d.ts +41 -0
  35. package/build/src/agents/registry_store.js +138 -0
  36. package/build/src/agents/runtime.d.ts +30 -1
  37. package/build/src/agents/runtime.js +221 -14
  38. package/build/src/audit/audit_sink.d.ts +1 -1
  39. package/build/src/audit/audit_sink.js +6 -0
  40. package/build/src/define_config.js +6 -0
  41. package/build/src/host/account_api/account_api_controller.d.ts +12 -1
  42. package/build/src/host/account_api/account_api_controller.js +10 -5
  43. package/build/src/host/admin_api/dto.d.ts +1 -1
  44. package/build/src/host/admin_console/admin_shell_controller.js +7 -0
  45. package/build/src/host/admin_console/console_personal_agents_controller.d.ts +83 -0
  46. package/build/src/host/admin_console/console_personal_agents_controller.js +337 -0
  47. package/build/src/host/admin_sessions_service.js +8 -5
  48. package/build/src/host/auth_host_config.d.ts +2 -1
  49. package/build/src/host/controllers/account_apps_controller.js +13 -3
  50. package/build/src/host/controllers/poppy_consent_controller.d.ts +19 -0
  51. package/build/src/host/controllers/poppy_consent_controller.js +173 -0
  52. package/build/src/host/controllers/poppy_controller.d.ts +25 -0
  53. package/build/src/host/controllers/poppy_controller.js +194 -0
  54. package/build/src/host/csrf.d.ts +7 -0
  55. package/build/src/host/csrf.js +16 -1
  56. package/build/src/host/i18n.d.ts +4 -0
  57. package/build/src/host/i18n.js +4 -0
  58. package/build/src/host/poppy_session_key.d.ts +9 -0
  59. package/build/src/host/poppy_session_key.js +9 -0
  60. package/build/src/host/register_auth_host.js +76 -0
  61. package/build/src/host/ui-dist/assets/index-LrScaR9C.js +155 -0
  62. package/build/src/host/ui-dist/index.html +1 -1
  63. package/build/src/provider/oidc_service.d.ts +5 -0
  64. package/build/src/provider/oidc_service.js +9 -2
  65. package/build/src/schema/ensure.js +152 -0
  66. package/package.json +2 -2
  67. package/build/src/host/ui-dist/assets/index-CIqtoNcO.js +0 -155
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
- <input type="hidden" name="user_code" value="{{ request.userCode }}">
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
- <p class="mt-4 text-xs text-gray-500">{{ t('agents.consent.code', { code: request.userCode }) }}</p>
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 ? { prefix: config.personalAgents.prefix } : undefined,
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 atribuído
59
- * por ESTE app no registro do agente — um valor por app, não derivado de URL.
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: string;
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
- if (!input.audience?.trim()) {
43
- throw new Error('authkit: personalAgents.audience é obrigatório (o `aud` dos JWTs dos agentes).');
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: normalizePersonalAgentsPrefix(input.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 (!row || row.status !== 'pending' || row.expiresAt <= this.#now())
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 (!row || row.status !== 'pending' || row.expiresAt <= now)
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 (!row || row.status !== 'pending')
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.revokedAt === null &&
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
- if (!runtime)
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
+ }