@adonis-agora/authkit-server 0.76.0 → 0.78.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 (88) hide show
  1. package/README.md +149 -0
  2. package/build/host/views/agents/consent.edge +13 -2
  3. package/build/host/views/agents/done.edge +3 -0
  4. package/build/host/views/mfa-challenge.edge +42 -3
  5. package/build/index.d.ts +19 -1
  6. package/build/index.js +13 -1
  7. package/build/providers/authkit_server_provider.js +15 -1
  8. package/build/src/accounts/account_store.d.ts +2 -0
  9. package/build/src/accounts/lucid_account_store.js +2 -0
  10. package/build/src/adapters/adapter_contract.d.ts +2 -0
  11. package/build/src/adapters/database_adapter.d.ts +1 -0
  12. package/build/src/adapters/database_adapter.js +26 -0
  13. package/build/src/adapters/redis_adapter.d.ts +1 -0
  14. package/build/src/adapters/redis_adapter.js +30 -0
  15. package/build/src/agents/config.d.ts +20 -3
  16. package/build/src/agents/config.js +19 -4
  17. package/build/src/agents/delegation_service.js +18 -8
  18. package/build/src/agents/middleware.js +2 -1
  19. package/build/src/agents/poppy/client_registry.d.ts +45 -0
  20. package/build/src/agents/poppy/client_registry.js +226 -0
  21. package/build/src/agents/poppy/config.d.ts +362 -0
  22. package/build/src/agents/poppy/config.js +201 -0
  23. package/build/src/agents/poppy/dpop.d.ts +61 -0
  24. package/build/src/agents/poppy/dpop.js +158 -0
  25. package/build/src/agents/poppy/errors.d.ts +20 -0
  26. package/build/src/agents/poppy/errors.js +22 -0
  27. package/build/src/agents/poppy/replay.d.ts +25 -0
  28. package/build/src/agents/poppy/replay.js +106 -0
  29. package/build/src/agents/poppy/resource.d.ts +95 -0
  30. package/build/src/agents/poppy/resource.js +125 -0
  31. package/build/src/agents/poppy/safe_fetch.d.ts +48 -0
  32. package/build/src/agents/poppy/safe_fetch.js +256 -0
  33. package/build/src/agents/poppy/service.d.ts +280 -0
  34. package/build/src/agents/poppy/service.js +1452 -0
  35. package/build/src/agents/poppy/store.d.ts +109 -0
  36. package/build/src/agents/poppy/store.js +260 -0
  37. package/build/src/agents/poppy/web_session.d.ts +32 -0
  38. package/build/src/agents/poppy/web_session.js +104 -0
  39. package/build/src/agents/protocol.d.ts +8 -4
  40. package/build/src/agents/runtime.d.ts +17 -1
  41. package/build/src/agents/runtime.js +138 -13
  42. package/build/src/define_config.d.ts +24 -0
  43. package/build/src/define_config.js +14 -0
  44. package/build/src/host/account_api/account_api_controller.d.ts +12 -1
  45. package/build/src/host/account_api/account_api_controller.js +10 -5
  46. package/build/src/host/admin_sessions_service.js +8 -5
  47. package/build/src/host/auth_host_config.d.ts +2 -1
  48. package/build/src/host/controllers/account_apps_controller.js +13 -3
  49. package/build/src/host/controllers/account_security_controller.js +8 -4
  50. package/build/src/host/controllers/interaction_controller.d.ts +13 -1
  51. package/build/src/host/controllers/interaction_controller.js +352 -24
  52. package/build/src/host/controllers/poppy_consent_controller.d.ts +19 -0
  53. package/build/src/host/controllers/poppy_consent_controller.js +173 -0
  54. package/build/src/host/controllers/poppy_controller.d.ts +25 -0
  55. package/build/src/host/controllers/poppy_controller.js +194 -0
  56. package/build/src/host/csrf.d.ts +7 -0
  57. package/build/src/host/csrf.js +16 -1
  58. package/build/src/host/custom_login.d.ts +36 -0
  59. package/build/src/host/custom_login.js +76 -0
  60. package/build/src/host/custom_login_completion.d.ts +4 -0
  61. package/build/src/host/custom_login_completion.js +33 -0
  62. package/build/src/host/custom_mfa.d.ts +77 -0
  63. package/build/src/host/custom_mfa.js +268 -0
  64. package/build/src/host/i18n.d.ts +8 -0
  65. package/build/src/host/i18n.js +14 -6
  66. package/build/src/host/idp_session_bridge.d.ts +5 -3
  67. package/build/src/host/idp_session_bridge.js +5 -3
  68. package/build/src/host/oidc_rp_guard.d.ts +5 -1
  69. package/build/src/host/oidc_rp_guard.js +15 -2
  70. package/build/src/host/persistent_rp_session.d.ts +8 -0
  71. package/build/src/host/persistent_rp_session.js +74 -0
  72. package/build/src/host/poppy_session_key.d.ts +9 -0
  73. package/build/src/host/poppy_session_key.js +9 -0
  74. package/build/src/host/register_auth_host.js +72 -0
  75. package/build/src/host/whatsapp_code_sender.d.ts +20 -0
  76. package/build/src/host/whatsapp_code_sender.js +9 -0
  77. package/build/src/host/whatsapp_senders/http.d.ts +5 -0
  78. package/build/src/host/whatsapp_senders/http.js +33 -0
  79. package/build/src/host/whatsapp_senders/meta_whatsapp_code_sender.d.ts +20 -0
  80. package/build/src/host/whatsapp_senders/meta_whatsapp_code_sender.js +51 -0
  81. package/build/src/host/whatsapp_senders/whatsmiau_code_sender.d.ts +14 -0
  82. package/build/src/host/whatsapp_senders/whatsmiau_code_sender.js +36 -0
  83. package/build/src/provider/oidc_service.d.ts +5 -0
  84. package/build/src/provider/oidc_service.js +15 -4
  85. package/build/src/schema/ensure.js +124 -0
  86. package/build/stubs/ui/react/pages/mfa-challenge.tsx +207 -37
  87. package/package.json +2 -1
  88. package/stubs/ui/react/pages/mfa-challenge.tsx +207 -37
@@ -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,8 +40,15 @@ 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;
46
54
  if (typeof input.agents === 'function') {
@@ -83,13 +91,20 @@ export function resolvePersonalAgentsConfig(input) {
83
91
  if (!protocol) {
84
92
  throw new Error(`authkit: personalAgents.protocol "${String(input.protocol)}" desconhecido (embutidos: ${Object.keys(BUILTIN_PROTOCOLS).join(', ')}).`);
85
93
  }
94
+ const prefix = normalizePersonalAgentsPrefix(input.prefix);
95
+ const poppy = resolvePoppyConfig(input.poppy, input.delegation?.scopes);
96
+ if (poppy && pact && (poppy.prefix === prefix || poppy.prefix.startsWith(`${prefix}/`))) {
97
+ throw new Error(`authkit: personalAgents.poppy.prefix "${poppy.prefix}" não pode ficar debaixo de personalAgents.prefix ("${prefix}").`);
98
+ }
86
99
  return {
100
+ pact,
87
101
  protocol,
88
- audience: input.audience,
102
+ audience: pact ? input.audience : '',
89
103
  resolveAgent,
90
104
  open: input.open === true,
91
105
  delegation,
92
- prefix: normalizePersonalAgentsPrefix(input.prefix),
106
+ prefix,
107
+ poppy,
93
108
  };
94
109
  }
95
110
  /** `"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 &&
@@ -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
+ }
@@ -0,0 +1,226 @@
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 { createLocalJWKSet } from 'jose';
8
+ import { PoppyError } from './errors.js';
9
+ import { insecureDevFetchJson, SafeFetchError, safeFetchJson, } from './safe_fetch.js';
10
+ const MIN_TTL_MS = 60 * 1000;
11
+ const MAX_TTL_MS = 24 * 3600 * 1000;
12
+ const DEFAULT_TTL_MS = 3600 * 1000;
13
+ const FAILURE_TTL_MS = 60 * 1000;
14
+ const MAX_CACHED = 1000;
15
+ /** Releitura do JWKS quando o `kid` não está no conjunto em cache — no máximo a cada 30 s. */
16
+ const JWKS_REFRESH_COOLDOWN_MS = 30 * 1000;
17
+ function hostOf(url) {
18
+ try {
19
+ return new URL(url).host.toLowerCase();
20
+ }
21
+ catch {
22
+ return null;
23
+ }
24
+ }
25
+ function isHttpsUrl(value, allowHttp = false) {
26
+ if (typeof value !== 'string')
27
+ return false;
28
+ try {
29
+ const url = new URL(value);
30
+ const schemeOk = url.protocol === 'https:' || (allowHttp && url.protocol === 'http:');
31
+ return schemeOk && !url.username && !url.password && !url.hash;
32
+ }
33
+ catch {
34
+ return false;
35
+ }
36
+ }
37
+ function ttlFrom(maxAge) {
38
+ if (maxAge === null)
39
+ return DEFAULT_TTL_MS;
40
+ return Math.min(MAX_TTL_MS, Math.max(MIN_TTL_MS, maxAge * 1000));
41
+ }
42
+ function setBounded(map, key, value) {
43
+ map.delete(key);
44
+ map.set(key, value);
45
+ if (map.size > MAX_CACHED)
46
+ map.delete(map.keys().next().value);
47
+ }
48
+ /** Valida um documento de metadata e devolve os campos usados. Lança `invalid_client`. */
49
+ export function validateClientMetadata(fetchedFrom, doc, options = {}) {
50
+ const allowHttp = options.allowHttp === true;
51
+ const bad = (why) => new PoppyError('invalid_client', `client metadata: ${why}`);
52
+ if (!doc || typeof doc !== 'object' || Array.isArray(doc))
53
+ throw bad('not a JSON object');
54
+ const d = doc;
55
+ if (d.client_id !== fetchedFrom)
56
+ throw bad('client_id must equal the URL it was fetched from');
57
+ const host = hostOf(fetchedFrom);
58
+ if (!isHttpsUrl(d.jwks_uri, allowHttp) || hostOf(d.jwks_uri) !== host) {
59
+ throw bad('jwks_uri must be an HTTPS URL on the same domain as client_id');
60
+ }
61
+ const redirects = d.redirect_uris ?? [];
62
+ if (!Array.isArray(redirects))
63
+ throw bad('redirect_uris must be an array');
64
+ for (const uri of redirects) {
65
+ if (!isHttpsUrl(uri, allowHttp) || hostOf(uri) !== host) {
66
+ throw bad('every redirect_uri must be an HTTPS URL on the same domain as client_id');
67
+ }
68
+ }
69
+ if (d.token_endpoint_auth_method !== 'private_key_jwt') {
70
+ throw bad('token_endpoint_auth_method must be private_key_jwt');
71
+ }
72
+ if (d.jwks !== undefined)
73
+ throw bad('keys are published at jwks_uri only');
74
+ const name = typeof d.client_name === 'string' && d.client_name.trim() ? d.client_name : host;
75
+ const extensions = d.extensions && typeof d.extensions === 'object' && !Array.isArray(d.extensions)
76
+ ? d.extensions
77
+ : {};
78
+ return {
79
+ clientId: fetchedFrom,
80
+ name: name.slice(0, 200),
81
+ logoUri: isHttpsUrl(d.logo_uri, allowHttp) ? d.logo_uri : null,
82
+ jwksUri: d.jwks_uri,
83
+ redirectUris: redirects,
84
+ extensions,
85
+ };
86
+ }
87
+ /** Só chaves públicas (§4.1) e pelo menos uma. */
88
+ function validateJwks(doc) {
89
+ const keys = doc?.keys;
90
+ if (!Array.isArray(keys) || keys.length === 0) {
91
+ throw new PoppyError('invalid_client', 'jwks_uri must serve a non-empty JWK set');
92
+ }
93
+ for (const key of keys) {
94
+ if (!key || typeof key !== 'object')
95
+ throw new PoppyError('invalid_client', 'invalid JWK');
96
+ const k = key;
97
+ if ('d' in k || 'p' in k || 'q' in k || k.kty === 'oct') {
98
+ throw new PoppyError('invalid_client', 'jwks_uri must contain only public keys');
99
+ }
100
+ }
101
+ return { keys: keys };
102
+ }
103
+ /**
104
+ * Resolve e valida agentes pelo `client_id`, com cache (respeita `max-age`,
105
+ * entre 1 min e 24 h; falhas ficam 1 min em cache para que o mesmo `client_id`
106
+ * não vire uma busca de saída por request).
107
+ */
108
+ export class PoppyClientRegistry {
109
+ #cfg;
110
+ #fetch;
111
+ #now;
112
+ #clients = new Map();
113
+ #jwks = new Map();
114
+ constructor(cfg, options = {}) {
115
+ this.#cfg = cfg;
116
+ this.#fetch =
117
+ options.fetchJson ??
118
+ (cfg.allowInsecureClientIds
119
+ ? (url) => insecureDevFetchJson(url)
120
+ : (url) => safeFetchJson(url));
121
+ this.#now = options.now ?? Date.now;
122
+ }
123
+ /**
124
+ * O agente, ou `invalid_client`: `client_id` não é HTTPS, bloqueado, não
125
+ * registrado (quando o app exige registro), metadata inválida/inalcançável.
126
+ */
127
+ async resolve(clientId) {
128
+ if (!isHttpsUrl(clientId, this.#cfg.allowInsecureClientIds)) {
129
+ throw new PoppyError('invalid_client', 'client_id must be an HTTPS URL');
130
+ }
131
+ if (await this.#cfg.clients.isBlocked(clientId)) {
132
+ throw new PoppyError('invalid_client', 'client_id is blocked');
133
+ }
134
+ if (this.#cfg.clients.requireRegistration &&
135
+ !(await this.#cfg.clients.isRegistered(clientId))) {
136
+ throw new PoppyError('invalid_client', 'client_id is not registered');
137
+ }
138
+ const meta = await this.#metadata(clientId);
139
+ return { ...meta, keys: this.#keySet(meta.jwksUri) };
140
+ }
141
+ /** Nome de exibição sem falhar (console de conta): metadata em cache/buscada, ou o host. */
142
+ async displayName(clientId) {
143
+ try {
144
+ return (await this.#metadata(clientId)).name;
145
+ }
146
+ catch {
147
+ return hostOf(clientId) ?? clientId;
148
+ }
149
+ }
150
+ #metadata(clientId) {
151
+ const now = this.#now();
152
+ const cached = this.#clients.get(clientId);
153
+ if (cached && cached.expiresAt > now)
154
+ return cached.value;
155
+ const entry = {
156
+ value: Promise.resolve(null),
157
+ expiresAt: now + FAILURE_TTL_MS,
158
+ };
159
+ entry.value = (async () => {
160
+ let fetched;
161
+ try {
162
+ fetched = await this.#fetch(clientId);
163
+ }
164
+ catch (error) {
165
+ const why = error instanceof SafeFetchError ? error.message : 'unreachable';
166
+ throw new PoppyError('invalid_client', `client metadata: ${why}`);
167
+ }
168
+ const meta = validateClientMetadata(clientId, fetched.body, {
169
+ allowHttp: this.#cfg.allowInsecureClientIds,
170
+ });
171
+ entry.expiresAt = this.#now() + ttlFrom(fetched.maxAge);
172
+ return meta;
173
+ })();
174
+ entry.value.catch(() => {
175
+ entry.expiresAt = this.#now() + FAILURE_TTL_MS;
176
+ });
177
+ setBounded(this.#clients, clientId, entry);
178
+ return entry.value;
179
+ }
180
+ #fetchJwks(jwksUri, force) {
181
+ const now = this.#now();
182
+ const cached = this.#jwks.get(jwksUri);
183
+ if (cached && cached.expiresAt > now && !force)
184
+ return cached.value;
185
+ if (cached && force && now - cached.fetchedAt < JWKS_REFRESH_COOLDOWN_MS)
186
+ return cached.value;
187
+ const entry = {
188
+ value: Promise.resolve({ keys: [] }),
189
+ expiresAt: now + FAILURE_TTL_MS,
190
+ fetchedAt: now,
191
+ };
192
+ entry.value = (async () => {
193
+ let fetched;
194
+ try {
195
+ fetched = await this.#fetch(jwksUri);
196
+ }
197
+ catch (error) {
198
+ const why = error instanceof SafeFetchError ? error.message : 'unreachable';
199
+ throw new PoppyError('invalid_client', `jwks_uri: ${why}`);
200
+ }
201
+ const jwks = validateJwks(fetched.body);
202
+ entry.expiresAt = this.#now() + ttlFrom(fetched.maxAge);
203
+ return jwks;
204
+ })();
205
+ entry.value.catch(() => {
206
+ entry.expiresAt = this.#now() + FAILURE_TTL_MS;
207
+ });
208
+ setBounded(this.#jwks, jwksUri, entry);
209
+ return entry.value;
210
+ }
211
+ /** `JWTVerifyGetKey` que relê o JWKS uma vez quando o `kid` é desconhecido (rotação). */
212
+ #keySet(jwksUri) {
213
+ return async (header, token) => {
214
+ const jwks = await this.#fetchJwks(jwksUri, false);
215
+ try {
216
+ return await createLocalJWKSet(jwks)(header, token);
217
+ }
218
+ catch (error) {
219
+ const refreshed = await this.#fetchJwks(jwksUri, true);
220
+ if (refreshed === jwks)
221
+ throw error;
222
+ return createLocalJWKSet(refreshed)(header, token);
223
+ }
224
+ };
225
+ }
226
+ }