UNPKG

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
// 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