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

108 lines (92 loc) 3.4 kB
/** * WebhookAdapter — sends daemon events as HTTP POST with optional HMAC signature * * Each call to send() POSTs the event as JSON to the configured URL. * When a secret is provided the request includes an X-AIWG-Signature header * computed as HMAC-SHA256 over the serialised body (same convention as * GitHub webhooks). An event-type filter can be supplied so the adapter * only forwards events the consumer cares about. * * @implements #521 */ import { ChannelAdapter } from './base-adapter.mjs'; import http from 'node:http'; import https from 'node:https'; import crypto from 'node:crypto'; export class WebhookAdapter extends ChannelAdapter { /** * @param {Object} options * @param {string} [options.id='webhook'] - Adapter identifier * @param {string} options.url - Destination URL (http or https) * @param {string} [options.secret] - HMAC secret; omit to skip signing * @param {string[]|null} [options.events] - Allowlist of event types; null = all */ constructor({ id, url, secret, events } = {}) { super({ id: id || 'webhook', type: 'webhook' }); if (!url) throw new Error('WebhookAdapter requires a url'); this.url = new URL(url); this.secret = secret || null; this.events = events || null; // null means accept every event type } /** * POST an event to the webhook URL. * * Skipped silently when an events allowlist is configured and the * message type is not in that list. * * @param {Object} message - Event object * @param {string} message.type - Event type used for filtering and headers * @param {*} [message.data] - Arbitrary event payload * @param {string} [message.timestamp] * @returns {Promise<{ statusCode: number, body: string }>} */ async send(message) { // Apply event-type filter if (this.events !== null && !this.events.includes(message.type)) { return; } const body = JSON.stringify(message); const headers = { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body), 'X-AIWG-Event': message.type || 'unknown', }; if (this.secret) { const sig = crypto .createHmac('sha256', this.secret) .update(body) .digest('hex'); headers['X-AIWG-Signature'] = `sha256=${sig}`; } return this._post(body, headers); } // --- Private helpers --- /** * Perform the HTTP(S) POST and return a promise that resolves to * { statusCode, body } or rejects on a network error. * * @param {string} body * @param {Object} headers * @returns {Promise<{ statusCode: number, body: string }>} */ _post(body, headers) { const transport = this.url.protocol === 'https:' ? https : http; const options = { hostname: this.url.hostname, port: this.url.port || (this.url.protocol === 'https:' ? 443 : 80), path: this.url.pathname + this.url.search, method: 'POST', headers, }; return new Promise((resolve, reject) => { const req = transport.request(options, (res) => { let responseBody = ''; res.on('data', (chunk) => { responseBody += chunk; }); res.on('end', () => resolve({ statusCode: res.statusCode, body: responseBody })); }); req.on('error', reject); req.write(body); req.end(); }); } }