@adonis-agora/authkit-server 0.75.0 → 0.77.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 (101) hide show
  1. package/README.md +148 -0
  2. package/build/host/views/account/apps.edge +30 -0
  3. package/build/host/views/agents/consent.edge +60 -0
  4. package/build/host/views/agents/done.edge +24 -0
  5. package/build/host/views/consent.edge +9 -1
  6. package/build/host/views/mfa-challenge.edge +42 -3
  7. package/build/host/views/partials/styles.edge +1 -1
  8. package/build/index.d.ts +18 -1
  9. package/build/index.js +9 -1
  10. package/build/providers/authkit_server_provider.js +75 -50
  11. package/build/src/accounts/account_store.d.ts +2 -0
  12. package/build/src/accounts/lucid_account_store.js +2 -0
  13. package/build/src/adapters/adapter_contract.d.ts +2 -0
  14. package/build/src/adapters/database_adapter.d.ts +1 -0
  15. package/build/src/adapters/database_adapter.js +26 -0
  16. package/build/src/adapters/redis_adapter.d.ts +1 -0
  17. package/build/src/adapters/redis_adapter.js +30 -0
  18. package/build/src/agents/agent_identity.d.ts +33 -0
  19. package/build/src/agents/agent_identity.js +156 -0
  20. package/build/src/agents/config.d.ts +99 -0
  21. package/build/src/agents/config.js +101 -0
  22. package/build/src/agents/delegation_service.d.ts +154 -0
  23. package/build/src/agents/delegation_service.js +394 -0
  24. package/build/src/agents/delegation_store.d.ts +93 -0
  25. package/build/src/agents/delegation_store.js +222 -0
  26. package/build/src/agents/middleware.d.ts +73 -0
  27. package/build/src/agents/middleware.js +113 -0
  28. package/build/src/agents/protocol.d.ts +62 -0
  29. package/build/src/agents/protocol.js +51 -0
  30. package/build/src/agents/runtime.d.ts +36 -0
  31. package/build/src/agents/runtime.js +65 -0
  32. package/build/src/agents/signer.d.ts +34 -0
  33. package/build/src/agents/signer.js +70 -0
  34. package/build/src/audit/audit_sink.d.ts +1 -1
  35. package/build/src/audit/audit_sink.js +5 -0
  36. package/build/src/controllers/authorization_server_metadata_controller.d.ts +10 -0
  37. package/build/src/controllers/authorization_server_metadata_controller.js +20 -0
  38. package/build/src/define_config.d.ts +50 -3
  39. package/build/src/define_config.js +42 -1
  40. package/build/src/host/account_api/account_api_controller.d.ts +12 -0
  41. package/build/src/host/account_api/account_api_controller.js +40 -0
  42. package/build/src/host/admin_api/dto.d.ts +1 -1
  43. package/build/src/host/admin_sessions_service.d.ts +2 -0
  44. package/build/src/host/admin_sessions_service.js +18 -0
  45. package/build/src/host/auth_host_config.d.ts +4 -0
  46. package/build/src/host/client_names.d.ts +11 -0
  47. package/build/src/host/client_names.js +12 -0
  48. package/build/src/host/controllers/account_apps_controller.d.ts +2 -0
  49. package/build/src/host/controllers/account_apps_controller.js +39 -3
  50. package/build/src/host/controllers/account_orgs_controller.js +2 -1
  51. package/build/src/host/controllers/account_security_controller.js +8 -4
  52. package/build/src/host/controllers/account_session_controller.js +2 -1
  53. package/build/src/host/controllers/agent_consent_controller.d.ts +17 -0
  54. package/build/src/host/controllers/agent_consent_controller.js +138 -0
  55. package/build/src/host/controllers/agent_oauth_controller.d.ts +23 -0
  56. package/build/src/host/controllers/agent_oauth_controller.js +110 -0
  57. package/build/src/host/controllers/interaction_controller.d.ts +13 -1
  58. package/build/src/host/controllers/interaction_controller.js +380 -24
  59. package/build/src/host/csrf.d.ts +8 -18
  60. package/build/src/host/csrf.js +28 -2
  61. package/build/src/host/custom_login.d.ts +36 -0
  62. package/build/src/host/custom_login.js +76 -0
  63. package/build/src/host/custom_login_completion.d.ts +4 -0
  64. package/build/src/host/custom_login_completion.js +33 -0
  65. package/build/src/host/custom_mfa.d.ts +77 -0
  66. package/build/src/host/custom_mfa.js +268 -0
  67. package/build/src/host/i18n.d.ts +58 -0
  68. package/build/src/host/i18n.js +64 -6
  69. package/build/src/host/idp_session_bridge.d.ts +5 -3
  70. package/build/src/host/idp_session_bridge.js +5 -3
  71. package/build/src/host/oidc_rp_guard.d.ts +5 -1
  72. package/build/src/host/oidc_rp_guard.js +15 -2
  73. package/build/src/host/persistent_rp_session.d.ts +8 -0
  74. package/build/src/host/persistent_rp_session.js +74 -0
  75. package/build/src/host/redirect_exact.d.ts +12 -0
  76. package/build/src/host/redirect_exact.js +17 -0
  77. package/build/src/host/register_auth_host.js +60 -7
  78. package/build/src/host/request_url.d.ts +10 -0
  79. package/build/src/host/request_url.js +17 -0
  80. package/build/src/host/sudo/methods/magic_link.js +2 -1
  81. package/build/src/host/sudo/runtime.js +3 -2
  82. package/build/src/host/sudo_mode.js +4 -4
  83. package/build/src/host/whatsapp_code_sender.d.ts +20 -0
  84. package/build/src/host/whatsapp_code_sender.js +9 -0
  85. package/build/src/host/whatsapp_senders/http.d.ts +5 -0
  86. package/build/src/host/whatsapp_senders/http.js +33 -0
  87. package/build/src/host/whatsapp_senders/meta_whatsapp_code_sender.d.ts +20 -0
  88. package/build/src/host/whatsapp_senders/meta_whatsapp_code_sender.js +51 -0
  89. package/build/src/host/whatsapp_senders/whatsmiau_code_sender.d.ts +14 -0
  90. package/build/src/host/whatsapp_senders/whatsmiau_code_sender.js +36 -0
  91. package/build/src/mcp/mcp_oauth.d.ts +85 -0
  92. package/build/src/mcp/mcp_oauth.js +154 -0
  93. package/build/src/provider/build_provider.js +15 -1
  94. package/build/src/provider/oidc_service.d.ts +8 -0
  95. package/build/src/provider/oidc_service.js +16 -2
  96. package/build/src/schema/ensure.js +92 -0
  97. package/build/stubs/ui/react/pages/consent.tsx +17 -1
  98. package/build/stubs/ui/react/pages/mfa-challenge.tsx +207 -37
  99. package/package.json +2 -1
  100. package/stubs/ui/react/pages/consent.tsx +17 -1
  101. package/stubs/ui/react/pages/mfa-challenge.tsx +207 -37
@@ -47,58 +47,36 @@ export default class AuthkitServerProvider {
47
47
  if (this.app.config.get('authkit')) {
48
48
  resolveAppKey(this.app);
49
49
  }
50
- // Config locks: trava as settings definidas explicitamente no defineConfig
51
- // (config vence em runtime; a UI/Admin API não pode alterá-las). Fail-safe:
52
- // qualquer erro → sem locks (comportamento legado).
53
- let resolvedConfig = null;
54
- try {
55
- const value = this.app.config.get('authkit');
56
- if (value) {
57
- const config = (await configProvider.resolve(this.app, value));
58
- if (config) {
59
- resolvedConfig = config;
60
- if (config.lockedSettingKeys?.length) {
61
- const { setLockedSettingKeys } = await import('../src/host/config_locks.js');
62
- setLockedSettingKeys(config.lockedSettingKeys);
50
+ // Config locks + stash dos bits de routing + auto-montagem: tudo sai do config RESOLVIDO.
51
+ //
52
+ // A resolução pode falhar AQUI e dar certo um instante depois: o `jwks` de um keystore
53
+ // criptografado precisa do serviço de encryption, que nem sempre está pronto durante o boot
54
+ // dos providers. Antes, o catch engolia a falha e nada derivado do config valia — locks,
55
+ // `sudo.methods`, headless, personal agents — sem um aviso. Agora a falha aqui é tentada de
56
+ // novo no `booted` (todos os providers prontos, ANTES do preload `start/routes.ts`), e só a
57
+ // segunda falha desiste, com warning.
58
+ if (this.app.config.get('authkit')) {
59
+ let config;
60
+ try {
61
+ config = await this.#resolveConfig();
62
+ }
63
+ catch {
64
+ this.app.booted(async () => {
65
+ let late;
66
+ try {
67
+ late = await this.#resolveConfig();
63
68
  }
64
- // Stash dos bits de routing p/ o registerAuthHost ler do config (dedup).
65
- // boot() roda antes do preload start/routes.ts, então estará disponível lá.
66
- const { setAuthHostConfig } = await import('../src/host/auth_host_config.js');
67
- setAuthHostConfig({
68
- mountPath: config.mountPath,
69
- social: config.social,
70
- rateLimit: config.rateLimit,
71
- adminEnabled: config.admin.enabled,
72
- adminApiEnabled: config.adminApi.enabled,
73
- // `config.sudo.methods` passa a decidir também o que é MONTADO — sem
74
- // isto o host teria de repetir a lista no `registerAuthHost`, e as
75
- // duas divergiriam (tela oferecendo endpoint que dá 404).
76
- sudoMethods: config.sudo?.methods,
77
- // Defaults estruturais de `config.routes` (o argumento ainda vence).
78
- routes: typeof config.routes === 'object' ? config.routes : undefined,
79
- lockedRouteOptions: config.lockedRouteOptions,
80
- // API headless — repassa pro registerAuthHost montar as rotas.
81
- headless: config.headless
82
- ? {
83
- baseUrl: config.headless.baseUrl,
84
- resolveAccountId: config.headless.resolveAccountId,
85
- }
86
- : undefined,
87
- });
88
- }
69
+ catch (error) {
70
+ const logger = await this.app.container.make('logger');
71
+ logger.warn({ err: error }, 'authkit: config não resolveu no boot — locks, stash de rotas (registerAuthHost) e config.routes não foram aplicados');
72
+ return;
73
+ }
74
+ if (late)
75
+ await this.#applyResolvedConfig(late);
76
+ });
89
77
  }
90
- }
91
- catch {
92
- /* sem locks / sem stash → registerAuthHost cai em opts/defaults */
93
- }
94
- // Auto-montagem das rotas (`config.routes`). FORA do try/catch fail-safe
95
- // acima de propósito: "as rotas não subiram" não pode degradar em silêncio —
96
- // seria um app inteiro em 404 sem nenhuma pista. Chama a MESMA função
97
- // exportada que o `start/routes.ts` chamaria; não há segunda implementação.
98
- if (resolvedConfig?.routes) {
99
- const router = await this.app.container.make('router');
100
- const { autoMountAuthHost } = await import('../src/host/register_auth_host.js');
101
- autoMountAuthHost(router);
78
+ if (config)
79
+ await this.#applyResolvedConfig(config);
102
80
  }
103
81
  // Registra o disco "authkit" no edge.js para que os templates sejam referenciados
104
82
  // como `authkit::login`, `authkit::account/tokens`, etc.
@@ -123,6 +101,53 @@ export default class AuthkitServerProvider {
123
101
  // edge.js ausente (host headless/Inertia-only que não usa edgeRenderer) — ignora.
124
102
  }
125
103
  }
104
+ async #resolveConfig() {
105
+ const value = this.app.config.get('authkit');
106
+ return (await configProvider.resolve(this.app, value));
107
+ }
108
+ /**
109
+ * Aplica o que o boot deriva do config resolvido: os locks de settings, o stash que o
110
+ * `registerAuthHost` lê e a auto-montagem de `config.routes` (cuja falha propaga).
111
+ */
112
+ async #applyResolvedConfig(config) {
113
+ if (config.lockedSettingKeys?.length) {
114
+ const { setLockedSettingKeys } = await import('../src/host/config_locks.js');
115
+ setLockedSettingKeys(config.lockedSettingKeys);
116
+ }
117
+ // Stash dos bits de routing p/ o registerAuthHost ler do config (dedup). Roda antes do
118
+ // preload start/routes.ts (no boot, ou no `booted` quando o boot não conseguiu resolver).
119
+ const { setAuthHostConfig } = await import('../src/host/auth_host_config.js');
120
+ setAuthHostConfig({
121
+ mountPath: config.mountPath,
122
+ social: config.social,
123
+ rateLimit: config.rateLimit,
124
+ adminEnabled: config.admin.enabled,
125
+ adminApiEnabled: config.adminApi.enabled,
126
+ // `config.sudo.methods` passa a decidir também o que é MONTADO — sem isto o host teria de
127
+ // repetir a lista no `registerAuthHost`, e as duas divergiriam (tela oferecendo endpoint
128
+ // que dá 404).
129
+ sudoMethods: config.sudo?.methods,
130
+ // Defaults estruturais de `config.routes` (o argumento ainda vence).
131
+ routes: typeof config.routes === 'object' ? config.routes : undefined,
132
+ lockedRouteOptions: config.lockedRouteOptions,
133
+ personalAgents: config.personalAgents ? { prefix: config.personalAgents.prefix } : undefined,
134
+ // API headless — repassa pro registerAuthHost montar as rotas.
135
+ headless: config.headless
136
+ ? {
137
+ baseUrl: config.headless.baseUrl,
138
+ resolveAccountId: config.headless.resolveAccountId,
139
+ }
140
+ : undefined,
141
+ });
142
+ // Auto-montagem das rotas (`config.routes`). Chama a MESMA função exportada que o
143
+ // `start/routes.ts` chamaria; não há segunda implementação. "As rotas não subiram" não pode
144
+ // degradar em silêncio: uma falha aqui propaga.
145
+ if (config.routes) {
146
+ const router = await this.app.container.make('router');
147
+ const { autoMountAuthHost } = await import('../src/host/register_auth_host.js');
148
+ autoMountAuthHost(router);
149
+ }
150
+ }
126
151
  register() {
127
152
  // Entrega o app BOOTADO ao singleton de `services/main` (e ao
128
153
  // `recordSubRevocation` do AdminSessionsService) para que nunca precisem
@@ -3,6 +3,8 @@ import type { UserLoginMethods } from '../host/user_login_methods.js';
3
3
  export interface AuthAccount {
4
4
  id: string;
5
5
  email: string;
6
+ /** Host-verified phone identity. Omit until possession has been verified. */
7
+ phone?: string;
6
8
  globalRoles?: string[];
7
9
  name?: string;
8
10
  avatarUrl?: string;
@@ -182,6 +182,7 @@ export function lucidAccountStore(Model, options = {}) {
182
182
  // token (e para todo call-site tipado como string).
183
183
  id: String(row.id),
184
184
  email: row.email,
185
+ ...(row.phone && row.phoneVerifiedAt ? { phone: row.phone } : {}),
185
186
  globalRoles: row.globalRoles ?? [],
186
187
  name: row.fullName ?? undefined,
187
188
  avatarUrl: row.avatarUrl ?? undefined,
@@ -328,6 +329,7 @@ export async function lucidAccountStoreAsync(Model, options = {}) {
328
329
  // Ver a nota em `toAccount` acima: `sub` precisa ser string.
329
330
  id: String(row.id),
330
331
  email: row.email,
332
+ ...(row.phone && row.phoneVerifiedAt ? { phone: row.phone } : {}),
331
333
  globalRoles: row.globalRoles ?? [],
332
334
  name: row.fullName ?? undefined,
333
335
  avatarUrl: row.avatarUrl ?? undefined,
@@ -24,6 +24,8 @@ export interface OidcAdapter {
24
24
  findByUid(uid: string): Promise<OidcPayload | undefined>;
25
25
  consume(id: string): Promise<void>;
26
26
  destroy(id: string): Promise<void>;
27
+ /** Extend an existing remembered Session atomically; never recreate revoked records. */
28
+ renewSession?(id: string, expiresIn: number): Promise<boolean>;
27
29
  revokeByGrantId(grantId: string): Promise<void>;
28
30
  /**
29
31
  * Enumeração GENÉRICA dos artefatos do model deste adapter (id + payload). Usada
@@ -6,6 +6,7 @@ export declare class DatabaseAdapter implements OidcAdapter {
6
6
  private db;
7
7
  constructor(name: string, db: Database);
8
8
  upsert(id: string, payload: OidcPayload, expiresIn: number): Promise<void>;
9
+ renewSession(id: string, expiresIn: number): Promise<boolean>;
9
10
  find(id: string): Promise<OidcPayload | undefined>;
10
11
  findByUid(uid: string): Promise<OidcPayload | undefined>;
11
12
  findByUserCode(userCode: string): Promise<OidcPayload | undefined>;
@@ -38,6 +38,32 @@ export class DatabaseAdapter {
38
38
  }
39
39
  return JSON.parse(record.payload);
40
40
  }
41
+ async renewSession(id, expiresIn) {
42
+ if (this.name !== 'Session' || !Number.isSafeInteger(expiresIn) || expiresIn < 1)
43
+ return false;
44
+ const row = await this.#query().where('id', id).first();
45
+ const payload = await this.#parse(row);
46
+ const now = Date.now();
47
+ if (!payload ||
48
+ payload.transient ||
49
+ typeof payload.exp !== 'number' ||
50
+ payload.exp <= now / 1000)
51
+ return false;
52
+ // Compare-and-set: concurrent logout or mutation cannot be undone by renewal.
53
+ const changed = await this.#query()
54
+ .where('id', id)
55
+ .where('payload', row.payload)
56
+ .where('expires_at', '>', new Date(now).toISOString())
57
+ .update({
58
+ payload: JSON.stringify({ ...payload, exp: Math.floor(now / 1000) + expiresIn }),
59
+ expires_at: new Date(now + expiresIn * 1000).toISOString(),
60
+ });
61
+ if (Number(changed) > 0)
62
+ return true;
63
+ // A parallel renewal is harmless; a deletion must remain deleted.
64
+ const current = await this.find(id);
65
+ return Boolean(current && !current.transient && typeof current.exp === 'number' && current.exp > now / 1000);
66
+ }
41
67
  async find(id) {
42
68
  return this.#parse(await this.#query().where('id', id).first());
43
69
  }
@@ -7,6 +7,7 @@ export declare class RedisAdapter implements OidcAdapter {
7
7
  private prefix;
8
8
  constructor(name: string, redis: Redis, prefix: string);
9
9
  upsert(id: string, payload: OidcPayload, expiresIn: number): Promise<void>;
10
+ renewSession(id: string, expiresIn: number): Promise<boolean>;
10
11
  find(id: string): Promise<OidcPayload | undefined>;
11
12
  findByUid(uid: string): Promise<OidcPayload | undefined>;
12
13
  findByUserCode(userCode: string): Promise<OidcPayload | undefined>;
@@ -52,6 +52,36 @@ export class RedisAdapter {
52
52
  multi.expire(key, expiresIn);
53
53
  await multi.exec();
54
54
  }
55
+ async renewSession(id, expiresIn) {
56
+ if (this.name !== 'Session' || !Number.isSafeInteger(expiresIn) || expiresIn < 1)
57
+ return false;
58
+ for (let attempt = 0; attempt < 3; attempt++) {
59
+ const raw = await this.redis.get(this.#key(id));
60
+ if (!raw)
61
+ return false;
62
+ const payload = JSON.parse(raw);
63
+ const now = Math.floor(Date.now() / 1000);
64
+ if (payload.transient || typeof payload.exp !== 'number' || payload.exp <= now)
65
+ return false;
66
+ const updated = JSON.stringify({ ...payload, exp: now + expiresIn });
67
+ const result = await this.redis.eval(`
68
+ local raw = redis.call('GET', KEYS[1])
69
+ if not raw then return 0 end
70
+ if raw ~= ARGV[1] then return 2 end
71
+ redis.call('SET', KEYS[1], ARGV[2], 'EX', ARGV[3])
72
+ if ARGV[4] ~= '' then redis.call('SET', KEYS[2], ARGV[4], 'EX', ARGV[3]) end
73
+ return 1
74
+ `, 2, this.#key(id), payload.uid ? this.#uidKey(payload.uid) : this.#key(id), raw, updated, expiresIn, payload.uid ? id : '');
75
+ if (result !== 2)
76
+ return result === 1;
77
+ }
78
+ // Another request may have renewed while we were comparing the payload.
79
+ const current = await this.find(id);
80
+ return Boolean(current &&
81
+ !current.transient &&
82
+ typeof current.exp === 'number' &&
83
+ current.exp > Date.now() / 1000);
84
+ }
55
85
  async find(id) {
56
86
  const data = await this.redis.get(this.#key(id));
57
87
  if (!data)
@@ -0,0 +1,33 @@
1
+ import { type JWTPayload, type JWTVerifyGetKey } from 'jose';
2
+ import type { PersonalAgentRegistration, ResolvedPersonalAgentsConfig } from './config.js';
3
+ /** Quem está chamando: o agente e o usuário DELE (`sub`, opaco). */
4
+ export interface PersonalAgentIdentity {
5
+ /** `iss` do JWT — identifica o agente; também é o `client_id` OAuth dele. */
6
+ issuer: string;
7
+ /** Usuário do agente. Opaco e estável; NÃO é a conta neste app. */
8
+ sub: string;
9
+ /** Nome de exibição do agente (registro ou host do issuer). */
10
+ name: string;
11
+ claims: JWTPayload;
12
+ }
13
+ export interface PersonalAgentVerifierOptions {
14
+ /** Fonte das chaves por `jwksUri`. Default: `createRemoteJWKSet` com cache. */
15
+ getKey?: (jwksUri: string) => JWTVerifyGetKey;
16
+ /** `fetch` da descoberta OIDC no modo `open`. Default: o global. */
17
+ fetch?: typeof fetch;
18
+ now?: () => Date;
19
+ }
20
+ /** Extrai o token de um header `Authorization: Bearer <token>`. */
21
+ export declare function bearerToken(header: string | null | undefined): string | null;
22
+ export declare function displayNameFor(registration: PersonalAgentRegistration): string;
23
+ /**
24
+ * Verifica o JWT que um personal agent manda em `Authorization: Bearer`, com
25
+ * as regras do protocolo configurado (PACT §3.2: ES256/RS256, vida ≤ 300 s,
26
+ * 30 s de relógio). Devolve `null` para QUALQUER falha — o chamador responde
27
+ * 401 sem detalhe.
28
+ */
29
+ export declare class PersonalAgentVerifier {
30
+ #private;
31
+ constructor(cfg: ResolvedPersonalAgentsConfig, options?: PersonalAgentVerifierOptions);
32
+ verify(authorization: string | null | undefined): Promise<PersonalAgentIdentity | null>;
33
+ }
@@ -0,0 +1,156 @@
1
+ import { createRemoteJWKSet, decodeJwt, decodeProtectedHeader, jwtVerify, } from 'jose';
2
+ /** Descoberta do modo `open`: quanto tempo um resultado vale, e quantos guardar. */
3
+ const DISCOVERY_TTL_MS = 60 * 60 * 1000;
4
+ const DISCOVERY_FAILURE_TTL_MS = 60 * 1000;
5
+ const MAX_CACHED_ISSUERS = 1000;
6
+ /** Map com teto: passando do limite, sai a entrada mais antiga (ordem de inserção). */
7
+ function setBounded(map, key, value) {
8
+ map.delete(key);
9
+ map.set(key, value);
10
+ if (map.size > MAX_CACHED_ISSUERS)
11
+ map.delete(map.keys().next().value);
12
+ }
13
+ /** Extrai o token de um header `Authorization: Bearer <token>`. */
14
+ export function bearerToken(header) {
15
+ if (!header)
16
+ return null;
17
+ const match = /^Bearer\s+(\S+)\s*$/i.exec(header);
18
+ return match ? match[1] : null;
19
+ }
20
+ export function displayNameFor(registration) {
21
+ if (registration.name)
22
+ return registration.name;
23
+ try {
24
+ return new URL(registration.issuer).host;
25
+ }
26
+ catch {
27
+ return registration.issuer;
28
+ }
29
+ }
30
+ /**
31
+ * Verifica o JWT que um personal agent manda em `Authorization: Bearer`, com
32
+ * as regras do protocolo configurado (PACT §3.2: ES256/RS256, vida ≤ 300 s,
33
+ * 30 s de relógio). Devolve `null` para QUALQUER falha — o chamador responde
34
+ * 401 sem detalhe.
35
+ */
36
+ export class PersonalAgentVerifier {
37
+ #cfg;
38
+ #getKey;
39
+ #fetch;
40
+ #now;
41
+ #remoteSets = new Map();
42
+ #discovered = new Map();
43
+ constructor(cfg, options = {}) {
44
+ this.#cfg = cfg;
45
+ this.#getKey = options.getKey ?? ((uri) => this.#remoteSet(uri));
46
+ this.#fetch = options.fetch ?? globalThis.fetch;
47
+ this.#now = options.now ?? (() => new Date());
48
+ }
49
+ async verify(authorization) {
50
+ const token = bearerToken(authorization);
51
+ if (!token)
52
+ return null;
53
+ const rules = this.#cfg.protocol.identity;
54
+ let issuer;
55
+ try {
56
+ if (!rules.algorithms.includes(decodeProtectedHeader(token).alg ?? ''))
57
+ return null;
58
+ issuer = decodeJwt(token).iss;
59
+ }
60
+ catch {
61
+ return null;
62
+ }
63
+ if (typeof issuer !== 'string' || !issuer)
64
+ return null;
65
+ const registration = await this.#registrationFor(issuer);
66
+ if (!registration || registration.enabled === false)
67
+ return null;
68
+ const now = this.#now();
69
+ let payload;
70
+ try {
71
+ ({ payload } = await jwtVerify(token, this.#getKey(registration.jwksUri), {
72
+ algorithms: rules.algorithms,
73
+ issuer: registration.issuer,
74
+ audience: this.#cfg.audience,
75
+ clockTolerance: rules.clockSkewSeconds,
76
+ currentDate: now,
77
+ requiredClaims: ['sub', 'iat', 'exp'],
78
+ }));
79
+ }
80
+ catch {
81
+ return null;
82
+ }
83
+ // O que o `jwtVerify` não cobre: `aud` é UMA string, `iat` não está no
84
+ // futuro além da tolerância, e a vida do token é curta.
85
+ const nowSeconds = Math.floor(now.getTime() / 1000);
86
+ if (typeof payload.aud !== 'string')
87
+ return null;
88
+ if (typeof payload.sub !== 'string' || !payload.sub)
89
+ return null;
90
+ if (payload.iat > nowSeconds + rules.clockSkewSeconds)
91
+ return null;
92
+ if (payload.exp - payload.iat > rules.maxLifetimeSeconds)
93
+ return null;
94
+ return {
95
+ issuer: registration.issuer,
96
+ sub: payload.sub,
97
+ name: displayNameFor(registration),
98
+ claims: payload,
99
+ };
100
+ }
101
+ async #registrationFor(issuer) {
102
+ const registered = await this.#cfg.resolveAgent(issuer);
103
+ if (registered)
104
+ return registered;
105
+ if (!this.#cfg.open)
106
+ return null;
107
+ const jwksUri = await this.#discover(issuer);
108
+ return jwksUri ? { issuer, jwksUri } : null;
109
+ }
110
+ /**
111
+ * Modo `open`: `jwks_uri` via `{iss}/.well-known/openid-configuration`, só
112
+ * HTTPS. O `iss` vem de um JWT ainda NÃO verificado, então tudo é limitado:
113
+ * falhas também ficam em cache (um minuto — o mesmo `iss` não vira uma
114
+ * requisição de saída por request), acertos expiram (o `jwks_uri` pode
115
+ * mudar) e o cache tem teto de entradas.
116
+ */
117
+ #discover(issuer) {
118
+ const now = this.#now().getTime();
119
+ const cached = this.#discovered.get(issuer);
120
+ if (cached && cached.expiresAt > now)
121
+ return cached.result;
122
+ const result = this.#fetchJwksUri(issuer);
123
+ const entry = { result, expiresAt: now + DISCOVERY_TTL_MS };
124
+ setBounded(this.#discovered, issuer, entry);
125
+ result.then((uri) => {
126
+ if (!uri)
127
+ entry.expiresAt = now + DISCOVERY_FAILURE_TTL_MS;
128
+ });
129
+ return result;
130
+ }
131
+ async #fetchJwksUri(issuer) {
132
+ try {
133
+ const url = new URL(issuer);
134
+ if (url.protocol !== 'https:')
135
+ return null;
136
+ const res = await this.#fetch(`${issuer.replace(/\/+$/, '')}/.well-known/openid-configuration`, { signal: AbortSignal.timeout(5000), redirect: 'error' });
137
+ if (!res.ok)
138
+ return null;
139
+ const meta = (await res.json());
140
+ if (meta.issuer !== issuer || typeof meta.jwks_uri !== 'string')
141
+ return null;
142
+ return new URL(meta.jwks_uri).protocol === 'https:' ? meta.jwks_uri : null;
143
+ }
144
+ catch {
145
+ return null;
146
+ }
147
+ }
148
+ #remoteSet(jwksUri) {
149
+ let set = this.#remoteSets.get(jwksUri);
150
+ if (!set) {
151
+ set = createRemoteJWKSet(new URL(jwksUri));
152
+ setBounded(this.#remoteSets, jwksUri, set);
153
+ }
154
+ return set;
155
+ }
156
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Personal agents — config.
3
+ *
4
+ * Um PERSONAL AGENT é uma plataforma de agente (ChatGPT, Meta AI, um assistente
5
+ * próprio…) que fala com este app EM NOME de um usuário. Ele se identifica com um
6
+ * JWT assinado pela própria chave (publicada num JWKS) — sem segredo
7
+ * compartilhado — e, opcionalmente, recebe do usuário uma DELEGAÇÃO: o usuário
8
+ * loga aqui, aprova scopes definidos pelo app, e o agente passa a agir na conta
9
+ * dele só dentro desses scopes.
10
+ *
11
+ * O núcleo (identidade do agente, device flow, grants, tokens de delegação,
12
+ * recibos) não depende de protocolo; o formato do fio vem do adapter em
13
+ * `protocol` (default: PACT — https://openpactprotocol.org). Ver `protocol.ts`.
14
+ */
15
+ import { type BuiltinProtocolId, type PersonalAgentProtocol } from './protocol.js';
16
+ /** Registro de um personal agent conhecido (PACT §3.1). */
17
+ export interface PersonalAgentRegistration {
18
+ /** Valor exato que o agente põe no `iss` do JWT. Também é o `client_id` OAuth dele. */
19
+ issuer: string;
20
+ /** URL HTTPS do JWKS do agente. Estável — a rotação acontece dentro do JWKS. */
21
+ jwksUri: string;
22
+ /** Nome exibido na tela de consentimento. Default: o host do `issuer`. */
23
+ name?: string;
24
+ /** `false` desabilita o agente: as requests dele passam a receber 401. Default: true. */
25
+ enabled?: boolean;
26
+ }
27
+ /** Resolve um agente pelo `iss`. `null` = desconhecido (401). */
28
+ export type PersonalAgentResolver = (issuer: string) => PersonalAgentRegistration | null | Promise<PersonalAgentRegistration | null>;
29
+ export interface PersonalAgentDelegationConfigInput {
30
+ /**
31
+ * URL do endpoint do agente DESTE app que os personal agents chamam (a
32
+ * "interface URL" do Agent Card). Vira o `aud` dos tokens de delegação — um
33
+ * token só vale para ela.
34
+ */
35
+ interfaceUrl: string;
36
+ /**
37
+ * Scopes que o app oferece, `id → descrição`. A descrição aparece VERBATIM na
38
+ * tela de consentimento, então escreva para o usuário final
39
+ * (`{ 'orders:read': 'Ver seus pedidos e o status deles' }`).
40
+ */
41
+ scopes: Record<string, string>;
42
+ /** TTL do token de delegação, em segundos. Default: 3600 (o PACT recomenda ≤ 1h). */
43
+ accessTokenTtl?: number;
44
+ /** Validade do grant (e dos refresh tokens dele), em segundos. Default: 30 dias. */
45
+ grantTtl?: number;
46
+ /** Validade do `device_code`/`user_code`, em segundos. Default: 600. */
47
+ deviceCodeTtl?: number;
48
+ /** Intervalo mínimo de polling do token endpoint, em segundos. Default: 5. */
49
+ pollInterval?: number;
50
+ }
51
+ export interface PersonalAgentsConfigInput {
52
+ /**
53
+ * Protocolo do fio: um embutido pelo id (`'pact'`) ou um adapter próprio
54
+ * (ver {@link PersonalAgentProtocol}). Default: `'pact'`.
55
+ */
56
+ protocol?: BuiltinProtocolId | PersonalAgentProtocol;
57
+ /**
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.
60
+ */
61
+ audience: string;
62
+ /**
63
+ * Agentes aceitos: uma lista estática, ou uma função que resolve pelo `iss`
64
+ * (para quem guarda o registro no banco). Default: nenhum.
65
+ */
66
+ agents?: PersonalAgentRegistration[] | PersonalAgentResolver;
67
+ /**
68
+ * `true` aceita QUALQUER agente cujo `iss` publique um JWKS via OIDC discovery
69
+ * (`{iss}/.well-known/openid-configuration`), além dos registrados. A
70
+ * verificação do JWT continua completa; só a lista de permitidos some.
71
+ * Default: false.
72
+ */
73
+ open?: boolean;
74
+ /** Delegação (PACT §5). Ausente = só identidade (o agente fala, mas não age na conta). */
75
+ delegation?: PersonalAgentDelegationConfigInput;
76
+ /** Prefixo das rotas (`{prefix}/oauth/*`, `{prefix}/consent`). Default: `/agents`. */
77
+ prefix?: string;
78
+ }
79
+ export interface ResolvedPersonalAgentDelegationConfig {
80
+ interfaceUrl: string;
81
+ scopes: Record<string, string>;
82
+ accessTokenTtl: number;
83
+ grantTtl: number;
84
+ deviceCodeTtl: number;
85
+ pollInterval: number;
86
+ }
87
+ export interface ResolvedPersonalAgentsConfig {
88
+ protocol: PersonalAgentProtocol;
89
+ audience: string;
90
+ resolveAgent: PersonalAgentResolver;
91
+ open: boolean;
92
+ delegation?: ResolvedPersonalAgentDelegationConfig;
93
+ prefix: string;
94
+ }
95
+ export declare function normalizePersonalAgentsPrefix(prefix: string | undefined): string;
96
+ export declare function resolvePersonalAgentsConfig(input: PersonalAgentsConfigInput | undefined): ResolvedPersonalAgentsConfig | undefined;
97
+ /** `"a b c"` → `['a','b','c']` sem duplicatas, na ordem. */
98
+ export declare function parseScope(value: string | null | undefined): string[];
99
+ export declare function formatScope(scopes: Iterable<string>): string;
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Personal agents — config.
3
+ *
4
+ * Um PERSONAL AGENT é uma plataforma de agente (ChatGPT, Meta AI, um assistente
5
+ * próprio…) que fala com este app EM NOME de um usuário. Ele se identifica com um
6
+ * JWT assinado pela própria chave (publicada num JWKS) — sem segredo
7
+ * compartilhado — e, opcionalmente, recebe do usuário uma DELEGAÇÃO: o usuário
8
+ * loga aqui, aprova scopes definidos pelo app, e o agente passa a agir na conta
9
+ * dele só dentro desses scopes.
10
+ *
11
+ * O núcleo (identidade do agente, device flow, grants, tokens de delegação,
12
+ * recibos) não depende de protocolo; o formato do fio vem do adapter em
13
+ * `protocol` (default: PACT — https://openpactprotocol.org). Ver `protocol.ts`.
14
+ */
15
+ import { BUILTIN_PROTOCOLS, } from './protocol.js';
16
+ const SCOPE_ID = /^[\x21\x23-\x5B\x5D-\x7E]+$/; // RFC 6749 §3.3 scope-token
17
+ function assertUrl(value, what) {
18
+ try {
19
+ new URL(value);
20
+ }
21
+ catch {
22
+ throw new Error(`authkit: personalAgents.${what} precisa ser uma URL absoluta (recebeu "${value}").`);
23
+ }
24
+ }
25
+ function positive(value, fallback, what) {
26
+ if (value === undefined)
27
+ return fallback;
28
+ if (!Number.isFinite(value) || value <= 0) {
29
+ throw new Error(`authkit: personalAgents.delegation.${what} precisa ser > 0.`);
30
+ }
31
+ return Math.floor(value);
32
+ }
33
+ export function normalizePersonalAgentsPrefix(prefix) {
34
+ const trimmed = (prefix ?? '/agents').trim().replace(/\/+$/, '');
35
+ if (!trimmed)
36
+ return '/agents';
37
+ return trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
38
+ }
39
+ export function resolvePersonalAgentsConfig(input) {
40
+ if (!input)
41
+ return undefined;
42
+ if (!input.audience?.trim()) {
43
+ throw new Error('authkit: personalAgents.audience é obrigatório (o `aud` dos JWTs dos agentes).');
44
+ }
45
+ let resolveAgent;
46
+ if (typeof input.agents === 'function') {
47
+ resolveAgent = input.agents;
48
+ }
49
+ else {
50
+ const byIssuer = new Map();
51
+ for (const agent of input.agents ?? []) {
52
+ assertUrl(agent.issuer, `agents[].issuer`);
53
+ assertUrl(agent.jwksUri, `agents[].jwksUri`);
54
+ byIssuer.set(agent.issuer, agent);
55
+ }
56
+ resolveAgent = (issuer) => byIssuer.get(issuer) ?? null;
57
+ }
58
+ let delegation;
59
+ if (input.delegation) {
60
+ const d = input.delegation;
61
+ assertUrl(d.interfaceUrl, 'delegation.interfaceUrl');
62
+ const ids = Object.keys(d.scopes ?? {});
63
+ if (ids.length === 0) {
64
+ throw new Error('authkit: personalAgents.delegation.scopes precisa de pelo menos um scope.');
65
+ }
66
+ for (const id of ids) {
67
+ if (!SCOPE_ID.test(id)) {
68
+ throw new Error(`authkit: personalAgents.delegation.scopes — id inválido "${id}" (sem espaços/aspas).`);
69
+ }
70
+ }
71
+ delegation = {
72
+ interfaceUrl: d.interfaceUrl,
73
+ scopes: { ...d.scopes },
74
+ accessTokenTtl: positive(d.accessTokenTtl, 3600, 'accessTokenTtl'),
75
+ grantTtl: positive(d.grantTtl, 30 * 24 * 3600, 'grantTtl'),
76
+ deviceCodeTtl: positive(d.deviceCodeTtl, 600, 'deviceCodeTtl'),
77
+ pollInterval: positive(d.pollInterval, 5, 'pollInterval'),
78
+ };
79
+ }
80
+ const protocol = typeof input.protocol === 'object'
81
+ ? input.protocol
82
+ : BUILTIN_PROTOCOLS[input.protocol ?? 'pact'];
83
+ if (!protocol) {
84
+ throw new Error(`authkit: personalAgents.protocol "${String(input.protocol)}" desconhecido (embutidos: ${Object.keys(BUILTIN_PROTOCOLS).join(', ')}).`);
85
+ }
86
+ return {
87
+ protocol,
88
+ audience: input.audience,
89
+ resolveAgent,
90
+ open: input.open === true,
91
+ delegation,
92
+ prefix: normalizePersonalAgentsPrefix(input.prefix),
93
+ };
94
+ }
95
+ /** `"a b c"` → `['a','b','c']` sem duplicatas, na ordem. */
96
+ export function parseScope(value) {
97
+ return [...new Set((value ?? '').split(' ').filter(Boolean))];
98
+ }
99
+ export function formatScope(scopes) {
100
+ return [...scopes].join(' ');
101
+ }