@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
JavaScript
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