aiwg
Version:
Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo
193 lines • 7.6 kB
JavaScript
// HTTP wrapper for A2A client + v1 fallback.
//
// Responsibilities:
// - Bearer auth header injection
// - A2A-Extensions header injection on every mutating call (#1254)
// - Echo-verification warning when expected extensions are missing
// - Idempotent-Replayed surfacing as a typed flag on the response
// - RFC 7807 problem+json error parsing → typed `A2AError`
// - Sunset / Deprecated / Link rel=successor-version header capture
// for #1259 (deprecation telemetry — uses callbacks so the Prometheus
// counter and structured logger plug in without coupling to the
// telemetry module)
// - Optional fail-on-deprecated mode (AIWG_FAIL_ON_DEPRECATED=true)
export class A2AError extends Error {
status;
problem;
path;
constructor(status, path, problem) {
super(`${status} ${problem.code ?? problem.title}: ${problem.detail ?? problem.title}`);
this.name = 'A2AError';
this.status = status;
this.problem = problem;
this.path = path;
}
}
const MUTATING_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
export class A2AHttpClient {
baseUrl;
bearer;
defaultExtensions;
failOnDeprecated;
fetchImpl;
onDeprecation;
onExtensionEchoMissing;
/** Per-process dedupe set: one log per (path, sunset_date). */
seenDeprecations = new Set();
constructor(opts) {
this.baseUrl = opts.baseUrl.replace(/\/+$/, '');
this.bearer = opts.bearer;
this.defaultExtensions = opts.defaultExtensions ?? [];
this.failOnDeprecated = opts.failOnDeprecated ?? false;
this.fetchImpl = opts.fetch ?? fetch;
this.onDeprecation = opts.onDeprecation;
this.onExtensionEchoMissing = opts.onExtensionEchoMissing;
}
async request(path, options = {}) {
const method = (options.method ?? 'GET').toUpperCase();
const url = path.startsWith('http') ? path : this.baseUrl + path;
const headers = {
authorization: `Bearer ${options.bearer ?? this.bearer}`,
accept: 'application/json',
};
if (options.body !== undefined && options.bodyRaw === undefined) {
headers['content-type'] = 'application/json';
}
// Inject A2A-Extensions on mutating calls. Caller can override with
// options.extensions (empty array clears injection).
if (MUTATING_METHODS.has(method)) {
const exts = options.extensions ?? this.defaultExtensions;
if (exts.length > 0) {
headers['a2a-extensions'] = exts.join(', ');
}
}
// Caller overrides last so explicit headers win.
if (options.headers) {
for (const [k, v] of Object.entries(options.headers)) {
headers[k.toLowerCase()] = v;
}
}
const init = { method, headers, signal: options.signal };
if (options.bodyRaw !== undefined) {
init.body = options.bodyRaw;
}
else if (options.body !== undefined) {
init.body = JSON.stringify(options.body);
}
const resp = await this.fetchImpl(url, init);
// Capture deprecation headers regardless of status.
const deprecation = captureDeprecation(path, resp.headers);
if (deprecation) {
const key = `${deprecation.path}|${deprecation.sunset ?? ''}`;
if (!this.seenDeprecations.has(key)) {
this.seenDeprecations.add(key);
if (this.onDeprecation)
this.onDeprecation(deprecation);
}
if (this.failOnDeprecated) {
throw new A2AError(resp.status, path, {
type: 'about:blank',
title: 'Deprecated endpoint',
detail: `AIWG_FAIL_ON_DEPRECATED is set; ${path} is deprecated (sunset=${deprecation.sunset ?? '?'})`,
code: 'aiwg.deprecation_strict',
});
}
}
const idempotentReplayed = (resp.headers.get('idempotent-replayed') ?? '')
.toLowerCase()
.includes('true');
const activatedExtensions = parseExtensionList(resp.headers.get('a2a-extensions'));
// Warn if mutating call requested extensions that weren't echoed.
if (MUTATING_METHODS.has(method)) {
const requested = options.extensions ?? this.defaultExtensions;
if (requested.length > 0) {
const missing = requested.filter((e) => !activatedExtensions.includes(e));
if (missing.length > 0 && this.onExtensionEchoMissing) {
this.onExtensionEchoMissing(requested, activatedExtensions, path);
}
}
}
if (options.raw) {
return {
status: resp.status,
body: undefined,
headers: resp.headers,
idempotentReplayed,
activatedExtensions,
rawBody: resp.body,
...(deprecation ? { deprecation } : {}),
};
}
// Parse body (JSON, problem+json, or empty).
let body;
const ct = resp.headers.get('content-type') ?? '';
if (resp.status !== 204 && resp.status !== 205) {
const text = await resp.text();
if (text.length > 0) {
if (ct.includes('json')) {
try {
body = JSON.parse(text);
}
catch (err) {
throw new A2AError(resp.status, path, {
type: 'about:blank',
title: 'Invalid JSON',
detail: `Response was not parseable JSON: ${err.message}`,
code: 'aiwg.invalid_response',
});
}
}
else {
body = text;
}
}
}
if (resp.status >= 400) {
const problem = body ?? {
type: 'about:blank',
title: `HTTP ${resp.status}`,
};
throw new A2AError(resp.status, path, problem);
}
return {
status: resp.status,
body,
headers: resp.headers,
idempotentReplayed,
activatedExtensions,
...(deprecation ? { deprecation } : {}),
};
}
}
// ---------- header helpers ----------
function parseExtensionList(header) {
if (!header)
return [];
return header
.split(',')
.map((s) => s.trim())
.filter((s) => s.length > 0);
}
function captureDeprecation(path, headers) {
const sunset = headers.get('sunset') ?? undefined;
const deprecated = headers.get('deprecated') ?? undefined;
const link = headers.get('link') ?? undefined;
if (!sunset && !deprecated && !link)
return undefined;
let successor;
if (link) {
// Match the `successor-version` rel target — RFC 5988 Link entries are
// comma-separated; we look for `rel="successor-version"` or `rel=successor-version`.
const re = /<([^>]+)>\s*;\s*[^,]*rel\s*=\s*"?successor-version"?/i;
const m = re.exec(link);
if (m)
successor = m[1];
}
return {
path,
...(sunset ? { sunset } : {}),
...(deprecated ? { deprecated } : {}),
...(successor ? { successor } : {}),
};
}
//# sourceMappingURL=http.js.map