UNPKG

@adonis-agora/agent

Version:

Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.

95 lines • 3.69 kB
import { PoppyError } from './errors.js'; import { parseRetryAfter, sleep, wwwAuthError } from './util.js'; /** * The one place requests leave this client. It signs DPoP proofs, remembers `DPoP-Nonce` * values per origin and retries once on `use_dpop_nonce` (RFC 9449 section 8/9), and waits * out a 429 `Retry-After` once (4.3, "Rate limits"). */ export class PoppyHttp { fetch; nonces = new Map(); maxRetryAfter; log; constructor(opts = {}) { this.fetch = opts.fetch ?? ((input, init) => globalThis.fetch(input, init)); this.maxRetryAfter = opts.maxRetryAfterSeconds ?? 30; this.log = opts.log; } async request(url, init = {}) { const method = (init.method ?? 'GET').toUpperCase(); const origin = new URL(url).origin; let nonceRetried = false; let rateRetried = false; for (;;) { const headers = new Headers(init.headers); const scheme = init.scheme ?? (init.dpop ? 'DPoP' : 'Bearer'); if (init.accessToken) headers.set('authorization', `${scheme} ${init.accessToken}`); if (init.dpop) { headers.set('dpop', await init.dpop.proof({ htm: method, htu: url, accessToken: scheme === 'DPoP' ? init.accessToken : undefined, nonce: this.nonces.get(origin), })); } const body = typeof init.body === 'function' ? await init.body() : init.body; const res = await this.fetch(url, { method, headers, ...(body !== undefined ? { body } : {}), ...(init.signal !== undefined ? { signal: init.signal } : {}), // A redirect would change `htu`, so DPoP requests never follow one silently. redirect: init.redirect ?? (init.dpop ? 'manual' : 'follow'), }); const nonce = res.headers.get('dpop-nonce'); if (nonce) this.nonces.set(origin, nonce); if (init.dpop && nonce && !nonceRetried && (await isUseDpopNonce(res))) { nonceRetried = true; this.log?.(`DPoP nonce required by ${origin}; retrying with the nonce`); continue; } if (res.status === 429 && !rateRetried) { const wait = parseRetryAfter(res.headers.get('retry-after')); if (wait !== undefined && wait <= this.maxRetryAfter) { rateRetried = true; this.log?.(`rate limited by ${origin}; waiting ${wait}s (Retry-After)`); await sleep(wait * 1000, init.signal); continue; } throw new PoppyError('rate_limited', `rate limited by ${origin}`, { status: 429, details: { retry_after: wait }, }); } return res; } } } async function isUseDpopNonce(res) { if (res.status !== 400 && res.status !== 401) return false; if (wwwAuthError(res.headers.get('www-authenticate')).error === 'use_dpop_nonce') return true; try { const body = (await res.clone().json()); return body?.error === 'use_dpop_nonce'; } catch { return false; } } /** Reads a JSON body, tolerating an empty one. */ export async function readJson(res) { const text = await res.text(); if (!text) return undefined; try { return JSON.parse(text); } catch { return undefined; } } //# sourceMappingURL=http.js.map