@tangle-network/agent-integrations 0.25.6 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -2
- package/dist/bin/tangle-catalog-runtime.js +3 -2
- package/dist/bin/tangle-catalog-runtime.js.map +1 -1
- package/dist/catalog.d.ts +1 -1
- package/dist/catalog.js +3 -2
- package/dist/chunk-2TW2QKGZ.js +94 -0
- package/dist/chunk-2TW2QKGZ.js.map +1 -0
- package/dist/{chunk-S54DPRDU.js → chunk-ALCIWTIR.js} +103 -5
- package/dist/chunk-ALCIWTIR.js.map +1 -0
- package/dist/{chunk-WC63AI4Q.js → chunk-GA4VTE3U.js} +1249 -169
- package/dist/chunk-GA4VTE3U.js.map +1 -0
- package/dist/connectors/adapters/index.d.ts +1 -1
- package/dist/connectors/adapters/index.js +8 -1
- package/dist/connectors/index.d.ts +1 -1
- package/dist/connectors/index.js +14 -6
- package/dist/{index-BQY5ry2s.d.ts → index-D4D4CEKX.d.ts} +177 -9
- package/dist/index.d.ts +2 -2
- package/dist/index.js +17 -7
- package/dist/registry.d.ts +139 -2
- package/dist/registry.js +3 -2
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.js +3 -2
- package/dist/specs.d.ts +1 -1
- package/dist/tangle-catalog-runtime.d.ts +1 -1
- package/dist/tangle-catalog-runtime.js +3 -2
- package/dist/webhooks/index.d.ts +193 -0
- package/dist/webhooks/index.js +285 -0
- package/dist/webhooks/index.js.map +1 -0
- package/examples/discover-capabilities.ts +46 -0
- package/examples/webhook-router.ts +56 -0
- package/package.json +15 -12
- package/dist/chunk-S54DPRDU.js.map +0 -1
- package/dist/chunk-WC63AI4Q.js.map +0 -1
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import {
|
|
2
|
+
firstHeader,
|
|
3
|
+
verifyHmacSignature,
|
|
4
|
+
verifySlackSignature,
|
|
5
|
+
verifyStripeSignature
|
|
6
|
+
} from "../chunk-2TW2QKGZ.js";
|
|
7
|
+
|
|
8
|
+
// src/webhooks/router.ts
|
|
9
|
+
var WebhookRouter = class {
|
|
10
|
+
providers;
|
|
11
|
+
deliver;
|
|
12
|
+
resolveSecret;
|
|
13
|
+
idempotency;
|
|
14
|
+
idempotencyTtlMs;
|
|
15
|
+
onError;
|
|
16
|
+
nowFn;
|
|
17
|
+
constructor(opts) {
|
|
18
|
+
this.providers = new Map(opts.providers.map((p) => [p.id, p]));
|
|
19
|
+
this.deliver = opts.deliver;
|
|
20
|
+
this.resolveSecret = opts.resolveSecret;
|
|
21
|
+
this.idempotency = opts.idempotency;
|
|
22
|
+
this.idempotencyTtlMs = opts.idempotencyTtlMs ?? 7 * 24 * 60 * 60 * 1e3;
|
|
23
|
+
this.onError = opts.onError ?? defaultOnError;
|
|
24
|
+
this.nowFn = opts.now ?? Date.now;
|
|
25
|
+
}
|
|
26
|
+
/** Process one inbound webhook request. Pure with respect to side-
|
|
27
|
+
* effects on the router instance — safe to call concurrently. */
|
|
28
|
+
async handle(request) {
|
|
29
|
+
const provider = this.providers.get(request.providerId);
|
|
30
|
+
if (!provider) {
|
|
31
|
+
return { status: 404, body: { error: "unknown_provider", provider: request.providerId } };
|
|
32
|
+
}
|
|
33
|
+
const secret = await this.resolveSecret(provider.id, request.headers);
|
|
34
|
+
if (!secret) {
|
|
35
|
+
return { status: 401, body: { error: "missing_secret", provider: provider.id } };
|
|
36
|
+
}
|
|
37
|
+
const verification = provider.verifySignature({
|
|
38
|
+
rawBody: request.rawBody,
|
|
39
|
+
headers: request.headers,
|
|
40
|
+
secret
|
|
41
|
+
});
|
|
42
|
+
if (!verification.valid) {
|
|
43
|
+
return { status: 401, body: { error: "invalid_signature", reason: verification.reason } };
|
|
44
|
+
}
|
|
45
|
+
let events;
|
|
46
|
+
try {
|
|
47
|
+
events = await provider.parse({ rawBody: request.rawBody, headers: request.headers, now: this.nowFn() });
|
|
48
|
+
} catch (err) {
|
|
49
|
+
this.onError(err, { provider: provider.id });
|
|
50
|
+
return { status: 400, body: { error: "parse_error", message: errMessage(err) } };
|
|
51
|
+
}
|
|
52
|
+
const accepted = [];
|
|
53
|
+
for (const event of events) {
|
|
54
|
+
if (event.providerEventId && this.idempotency) {
|
|
55
|
+
const already = await this.idempotency.seen(event.providerEventId);
|
|
56
|
+
if (already) continue;
|
|
57
|
+
}
|
|
58
|
+
accepted.push(event);
|
|
59
|
+
}
|
|
60
|
+
queueMicrotask(() => {
|
|
61
|
+
void this.deliverEach(accepted);
|
|
62
|
+
});
|
|
63
|
+
return { status: 200, body: { received: accepted.length, total: events.length } };
|
|
64
|
+
}
|
|
65
|
+
async deliverEach(events) {
|
|
66
|
+
for (const event of events) {
|
|
67
|
+
try {
|
|
68
|
+
await this.deliver(event);
|
|
69
|
+
if (event.providerEventId && this.idempotency) {
|
|
70
|
+
await this.idempotency.remember(event.providerEventId, this.idempotencyTtlMs);
|
|
71
|
+
}
|
|
72
|
+
} catch (err) {
|
|
73
|
+
this.onError(err, {
|
|
74
|
+
provider: event.provider,
|
|
75
|
+
eventType: event.eventType,
|
|
76
|
+
providerEventId: event.providerEventId
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
function defaultOnError(err, context) {
|
|
83
|
+
console.error("[WebhookRouter]", context, err);
|
|
84
|
+
}
|
|
85
|
+
function errMessage(err) {
|
|
86
|
+
return err instanceof Error ? err.message : String(err);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// src/webhooks/providers.ts
|
|
90
|
+
import { createHmac, timingSafeEqual } from "crypto";
|
|
91
|
+
var stripeWebhookProvider = {
|
|
92
|
+
id: "stripe",
|
|
93
|
+
verifySignature({ rawBody, headers, secret }) {
|
|
94
|
+
const sig = firstHeader(headers, "stripe-signature");
|
|
95
|
+
if (!sig) return { valid: false, reason: "missing_stripe_signature" };
|
|
96
|
+
return verifyStripeSignature(rawBody, sig, secret) ? { valid: true } : { valid: false, reason: "invalid_signature" };
|
|
97
|
+
},
|
|
98
|
+
parse({ rawBody, headers, now }) {
|
|
99
|
+
const evt = safeJson(rawBody);
|
|
100
|
+
if (!evt || typeof evt !== "object") return [];
|
|
101
|
+
const e = evt;
|
|
102
|
+
return [
|
|
103
|
+
{
|
|
104
|
+
provider: "stripe",
|
|
105
|
+
eventType: typeof e.type === "string" ? e.type : "stripe.unknown",
|
|
106
|
+
providerEventId: typeof e.id === "string" ? e.id : void 0,
|
|
107
|
+
receivedAt: now ?? Date.now(),
|
|
108
|
+
payload: evt,
|
|
109
|
+
headers: normalizeHeaders(headers)
|
|
110
|
+
}
|
|
111
|
+
];
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
var slackWebhookProvider = {
|
|
115
|
+
id: "slack",
|
|
116
|
+
verifySignature({ rawBody, headers, secret }) {
|
|
117
|
+
const sig = firstHeader(headers, "x-slack-signature");
|
|
118
|
+
const ts = firstHeader(headers, "x-slack-request-timestamp");
|
|
119
|
+
if (!sig || !ts) return { valid: false, reason: "missing_slack_signature_or_timestamp" };
|
|
120
|
+
return verifySlackSignature(rawBody, sig, ts, secret) ? { valid: true } : { valid: false, reason: "invalid_signature" };
|
|
121
|
+
},
|
|
122
|
+
parse({ rawBody, headers, now }) {
|
|
123
|
+
const evt = safeJson(rawBody);
|
|
124
|
+
if (!evt || typeof evt !== "object") return [];
|
|
125
|
+
if (evt.type === "url_verification") {
|
|
126
|
+
return [{
|
|
127
|
+
provider: "slack",
|
|
128
|
+
eventType: "slack.url_verification",
|
|
129
|
+
receivedAt: now ?? Date.now(),
|
|
130
|
+
payload: evt,
|
|
131
|
+
headers: normalizeHeaders(headers)
|
|
132
|
+
}];
|
|
133
|
+
}
|
|
134
|
+
const eventType = `slack.${evt.event?.type ?? evt.type ?? "unknown"}`;
|
|
135
|
+
return [{
|
|
136
|
+
provider: "slack",
|
|
137
|
+
eventType,
|
|
138
|
+
providerEventId: typeof evt.event_id === "string" ? evt.event_id : void 0,
|
|
139
|
+
receivedAt: now ?? Date.now(),
|
|
140
|
+
payload: evt,
|
|
141
|
+
headers: normalizeHeaders(headers)
|
|
142
|
+
}];
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
var docusealWebhookProvider = {
|
|
146
|
+
id: "docuseal",
|
|
147
|
+
verifySignature({ rawBody, headers, secret }) {
|
|
148
|
+
const sig = firstHeader(headers, "x-docuseal-signature");
|
|
149
|
+
if (!sig) return { valid: false, reason: "missing_docuseal_signature" };
|
|
150
|
+
const expected = createHmac("sha256", secret).update(rawBody).digest("hex");
|
|
151
|
+
const a = Buffer.from(sig.toLowerCase(), "utf-8");
|
|
152
|
+
const b = Buffer.from(expected, "utf-8");
|
|
153
|
+
if (a.length !== b.length) return { valid: false, reason: "invalid_signature" };
|
|
154
|
+
return timingSafeEqual(a, b) ? { valid: true } : { valid: false, reason: "invalid_signature" };
|
|
155
|
+
},
|
|
156
|
+
parse({ rawBody, headers, now }) {
|
|
157
|
+
const evt = safeJson(rawBody);
|
|
158
|
+
if (!evt || typeof evt !== "object") return [];
|
|
159
|
+
return [{
|
|
160
|
+
provider: "docuseal",
|
|
161
|
+
eventType: `docuseal.${evt.event_type ?? "unknown"}`,
|
|
162
|
+
providerEventId: typeof evt.event_id === "string" ? evt.event_id : void 0,
|
|
163
|
+
receivedAt: now ?? Date.now(),
|
|
164
|
+
payload: evt,
|
|
165
|
+
headers: normalizeHeaders(headers)
|
|
166
|
+
}];
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
var gmailWebhookProvider = {
|
|
170
|
+
id: "gmail",
|
|
171
|
+
verifySignature({ headers, secret }) {
|
|
172
|
+
const auth = firstHeader(headers, "authorization");
|
|
173
|
+
if (!auth) return { valid: false, reason: "missing_authorization" };
|
|
174
|
+
const m = /^(?:Bearer|Token)\s+(.+)$/i.exec(auth);
|
|
175
|
+
if (!m) return { valid: false, reason: "invalid_authorization_format" };
|
|
176
|
+
const a = Buffer.from(m[1], "utf-8");
|
|
177
|
+
const b = Buffer.from(secret, "utf-8");
|
|
178
|
+
if (a.length !== b.length) return { valid: false, reason: "invalid_signature" };
|
|
179
|
+
return timingSafeEqual(a, b) ? { valid: true } : { valid: false, reason: "invalid_signature" };
|
|
180
|
+
},
|
|
181
|
+
parse({ rawBody, headers, now }) {
|
|
182
|
+
const envelope = safeJson(rawBody);
|
|
183
|
+
if (!envelope?.message?.data) return [];
|
|
184
|
+
let payload;
|
|
185
|
+
try {
|
|
186
|
+
payload = JSON.parse(Buffer.from(envelope.message.data, "base64").toString("utf-8"));
|
|
187
|
+
} catch {
|
|
188
|
+
return [];
|
|
189
|
+
}
|
|
190
|
+
const inner = payload;
|
|
191
|
+
return [{
|
|
192
|
+
provider: "gmail",
|
|
193
|
+
eventType: "gmail.history_changed",
|
|
194
|
+
providerEventId: envelope.message.messageId,
|
|
195
|
+
receivedAt: now ?? Date.now(),
|
|
196
|
+
payload: { ...inner, publishTime: envelope.message.publishTime },
|
|
197
|
+
headers: normalizeHeaders(headers)
|
|
198
|
+
}];
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
var gdriveWebhookProvider = {
|
|
202
|
+
id: "gdrive",
|
|
203
|
+
verifySignature({ headers, secret }) {
|
|
204
|
+
const token = firstHeader(headers, "x-goog-channel-token");
|
|
205
|
+
if (!token) return { valid: false, reason: "missing_channel_token" };
|
|
206
|
+
const a = Buffer.from(token, "utf-8");
|
|
207
|
+
const b = Buffer.from(secret, "utf-8");
|
|
208
|
+
if (a.length !== b.length) return { valid: false, reason: "invalid_signature" };
|
|
209
|
+
return timingSafeEqual(a, b) ? { valid: true } : { valid: false, reason: "invalid_signature" };
|
|
210
|
+
},
|
|
211
|
+
parse({ headers, now }) {
|
|
212
|
+
const resourceId = firstHeader(headers, "x-goog-resource-id");
|
|
213
|
+
const resourceState = firstHeader(headers, "x-goog-resource-state") ?? "unknown";
|
|
214
|
+
const channelId = firstHeader(headers, "x-goog-channel-id");
|
|
215
|
+
const messageNumber = firstHeader(headers, "x-goog-message-number");
|
|
216
|
+
if (resourceState === "sync") {
|
|
217
|
+
return [{
|
|
218
|
+
provider: "gdrive",
|
|
219
|
+
eventType: "gdrive.channel.sync",
|
|
220
|
+
providerEventId: messageNumber ? `${channelId}-${messageNumber}` : void 0,
|
|
221
|
+
receivedAt: now ?? Date.now(),
|
|
222
|
+
payload: { channelId, resourceId, resourceState },
|
|
223
|
+
headers: normalizeHeaders(headers)
|
|
224
|
+
}];
|
|
225
|
+
}
|
|
226
|
+
return [{
|
|
227
|
+
provider: "gdrive",
|
|
228
|
+
eventType: `gdrive.resource.${resourceState}`,
|
|
229
|
+
providerEventId: messageNumber ? `${channelId}-${messageNumber}` : void 0,
|
|
230
|
+
receivedAt: now ?? Date.now(),
|
|
231
|
+
payload: { channelId, resourceId, resourceState },
|
|
232
|
+
headers: normalizeHeaders(headers)
|
|
233
|
+
}];
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
function genericHmacWebhookProvider(options) {
|
|
237
|
+
const header = options.signatureHeader ?? "x-signature";
|
|
238
|
+
return {
|
|
239
|
+
id: options.id,
|
|
240
|
+
verifySignature({ rawBody, headers, secret }) {
|
|
241
|
+
const sig = firstHeader(headers, header);
|
|
242
|
+
if (!sig) return { valid: false, reason: `missing_${header}` };
|
|
243
|
+
return verifyHmacSignature(rawBody, sig, secret, {
|
|
244
|
+
algorithm: options.algorithm ?? "sha256",
|
|
245
|
+
signaturePrefix: options.signaturePrefix
|
|
246
|
+
}) ? { valid: true } : { valid: false, reason: "invalid_signature" };
|
|
247
|
+
},
|
|
248
|
+
parse: options.parse ?? (({ rawBody, headers, now }) => {
|
|
249
|
+
const evt = safeJson(rawBody) ?? rawBody;
|
|
250
|
+
return [{
|
|
251
|
+
provider: options.id,
|
|
252
|
+
eventType: `${options.id}.event`,
|
|
253
|
+
receivedAt: now ?? Date.now(),
|
|
254
|
+
payload: evt,
|
|
255
|
+
headers: normalizeHeaders(headers)
|
|
256
|
+
}];
|
|
257
|
+
})
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
function safeJson(s) {
|
|
261
|
+
try {
|
|
262
|
+
return JSON.parse(s);
|
|
263
|
+
} catch {
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
function normalizeHeaders(headers) {
|
|
268
|
+
const out = {};
|
|
269
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
270
|
+
if (v === void 0) continue;
|
|
271
|
+
const value = Array.isArray(v) ? v[0] : v;
|
|
272
|
+
if (typeof value === "string") out[k.toLowerCase()] = value;
|
|
273
|
+
}
|
|
274
|
+
return out;
|
|
275
|
+
}
|
|
276
|
+
export {
|
|
277
|
+
WebhookRouter,
|
|
278
|
+
docusealWebhookProvider,
|
|
279
|
+
gdriveWebhookProvider,
|
|
280
|
+
genericHmacWebhookProvider,
|
|
281
|
+
gmailWebhookProvider,
|
|
282
|
+
slackWebhookProvider,
|
|
283
|
+
stripeWebhookProvider
|
|
284
|
+
};
|
|
285
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/webhooks/router.ts","../../src/webhooks/providers.ts"],"sourcesContent":["/**\n * @stable Provider-agnostic inbound webhook router.\n *\n * Consumer hooks a single HTTP handler at `/webhook/:provider/:event`\n * (or whatever pathing they prefer) and forwards the request through\n * `WebhookRouter.handle()`. The router:\n *\n * 1. Resolves the registered provider entry.\n * 2. Calls the provider's `verifySignature(rawBody, headers, secrets)`.\n * Failure → 401 fast, no downstream work.\n * 3. Calls the provider's `parse(rawBody, headers)` to extract zero or\n * more normalized events.\n * 4. Enqueues each event for async processing via the consumer-supplied\n * `deliver(event)` callback (best-effort fire-and-forget — the\n * router does NOT block the HTTP response on the consumer's work).\n * 5. Returns 200 fast with `{received: events.length}`.\n *\n * Replay protection: providers that sign timestamps (Stripe, Slack)\n * already reject stale signatures inside `verifySignature`. For providers\n * that don't (DocuSeal, GDrive push), the router exposes a pluggable\n * `idempotency` hook: if `idempotency.seen(providerEventId)` returns\n * true, the router 200s without invoking `deliver()`. Consumers wire\n * this to a durable kv (D1 / Redis / Postgres unique-index).\n *\n * Why a router and not a per-provider express app: the runtime contract\n * a product cares about is \"an inbound event came in, here's the\n * normalized envelope\". Verification, parsing, and idempotency-dedup\n * are mechanical and provider-specific — the router owns them. The\n * consumer's `deliver()` is the only place product logic runs.\n *\n * Stability: `@stable` — additions to `WebhookEnvelope` must be\n * additive; the router's HTTP contract (paths, status codes) is frozen\n * at 200 (ok), 400 (bad request), 401 (bad signature), 404 (unknown\n * provider), 405 (provider has no inbound surface).\n */\n\nexport interface WebhookHeaders {\n [name: string]: string | string[] | undefined\n}\n\n/** Normalized inbound event the router emits after parsing. */\nexport interface WebhookEnvelope<TPayload = unknown> {\n /** Provider id (matches the `:provider` path segment). */\n provider: string\n /** Optional event class — e.g., 'customer.subscription.deleted'. The\n * provider's parser decides. Used for routing inside `deliver()`. */\n eventType: string\n /** Provider-emitted event id, when present. Used for the idempotency\n * short-circuit. */\n providerEventId?: string\n /** Wall-clock receive time. */\n receivedAt: number\n /** Provider payload, normalized to the provider's documented event\n * shape. The router does NOT reshape this — `parse()` is the contract. */\n payload: TPayload\n /** Headers passed through for downstream handlers that want them\n * (e.g., to extract custom routing metadata). Always lowercased keys. */\n headers: Record<string, string>\n}\n\nexport type SignatureVerification =\n | { valid: true }\n | { valid: false; reason: string }\n\n/** Per-provider plug-in. Stateless — the router calls `verifySignature`\n * then `parse` on every request. The provider's HTTP-shape concerns\n * (e.g., raw body required) are documented per provider. */\nexport interface WebhookProvider {\n /** Stable provider id (`stripe`, `docuseal`, `gdrive`, ...). */\n id: string\n /** Verify the inbound signature. Receives the EXACT raw body string —\n * consumers MUST preserve raw bytes through their HTTP server (do not\n * parse JSON before forwarding here). */\n verifySignature(input: {\n rawBody: string\n headers: WebhookHeaders\n secret: string\n }): SignatureVerification\n /** Parse the validated raw body into zero or more normalized events.\n * A single push payload may carry multiple events (e.g., Slack bulk\n * delivery). Return [] to ack the push as a no-op. */\n parse(input: {\n rawBody: string\n headers: WebhookHeaders\n now?: number\n }): WebhookEnvelope[] | Promise<WebhookEnvelope[]>\n}\n\nexport interface WebhookIdempotencyStore {\n /** Returns true if this providerEventId has been processed already.\n * Implementations should be O(1) (Redis SETNX, D1 UNIQUE constraint). */\n seen(providerEventId: string): Promise<boolean> | boolean\n /** Marks a providerEventId as processed. Called AFTER `deliver()` has\n * been invoked. */\n remember(providerEventId: string, ttlMs: number): Promise<void> | void\n}\n\nexport interface WebhookRouterOptions {\n /** Provider registry. Pass any number of providers; routing is by id. */\n providers: WebhookProvider[]\n /** Async callback invoked with every accepted event. Fire-and-forget\n * from the router's perspective — the HTTP response is sent before\n * this resolves. Throws are caught and reported via `onError`. */\n deliver(event: WebhookEnvelope): Promise<void> | void\n /** Resolve the signing secret for a provider id at request time. The\n * router never holds secrets — the consumer's vault resolves them. */\n resolveSecret(providerId: string, headers: WebhookHeaders): Promise<string | null> | string | null\n /** Optional idempotency-dedup hook. Required for providers that don't\n * sign timestamps in their signature scheme (DocuSeal, Drive push). */\n idempotency?: WebhookIdempotencyStore\n /** TTL on idempotency entries. Default 7 days — long enough that a\n * provider's normal retry-window can't re-deliver. */\n idempotencyTtlMs?: number\n /** Surface delivery errors. Default: console.error. */\n onError?(err: unknown, context: { provider: string; eventType?: string; providerEventId?: string }): void\n /** Override `now()` for tests. */\n now?(): number\n}\n\nexport interface WebhookRouterRequest {\n providerId: string\n rawBody: string\n headers: WebhookHeaders\n}\n\nexport interface WebhookRouterResponse {\n status: number\n body: unknown\n headers?: Record<string, string>\n}\n\n/**\n * Router instance. Stateless aside from the provider registry — safe to\n * share across requests; build once per process.\n */\nexport class WebhookRouter {\n private readonly providers: Map<string, WebhookProvider>\n private readonly deliver: WebhookRouterOptions['deliver']\n private readonly resolveSecret: WebhookRouterOptions['resolveSecret']\n private readonly idempotency?: WebhookIdempotencyStore\n private readonly idempotencyTtlMs: number\n private readonly onError: NonNullable<WebhookRouterOptions['onError']>\n private readonly nowFn: () => number\n\n constructor(opts: WebhookRouterOptions) {\n this.providers = new Map(opts.providers.map((p) => [p.id, p]))\n this.deliver = opts.deliver\n this.resolveSecret = opts.resolveSecret\n this.idempotency = opts.idempotency\n this.idempotencyTtlMs = opts.idempotencyTtlMs ?? 7 * 24 * 60 * 60 * 1000\n this.onError = opts.onError ?? defaultOnError\n this.nowFn = opts.now ?? Date.now\n }\n\n /** Process one inbound webhook request. Pure with respect to side-\n * effects on the router instance — safe to call concurrently. */\n async handle(request: WebhookRouterRequest): Promise<WebhookRouterResponse> {\n const provider = this.providers.get(request.providerId)\n if (!provider) {\n return { status: 404, body: { error: 'unknown_provider', provider: request.providerId } }\n }\n const secret = await this.resolveSecret(provider.id, request.headers)\n if (!secret) {\n return { status: 401, body: { error: 'missing_secret', provider: provider.id } }\n }\n const verification = provider.verifySignature({\n rawBody: request.rawBody,\n headers: request.headers,\n secret,\n })\n if (!verification.valid) {\n return { status: 401, body: { error: 'invalid_signature', reason: verification.reason } }\n }\n\n let events: WebhookEnvelope[]\n try {\n events = await provider.parse({ rawBody: request.rawBody, headers: request.headers, now: this.nowFn() })\n } catch (err) {\n this.onError(err, { provider: provider.id })\n return { status: 400, body: { error: 'parse_error', message: errMessage(err) } }\n }\n\n const accepted: WebhookEnvelope[] = []\n for (const event of events) {\n if (event.providerEventId && this.idempotency) {\n const already = await this.idempotency.seen(event.providerEventId)\n if (already) continue\n }\n accepted.push(event)\n }\n\n // Deliver async — do NOT block the HTTP response. Errors land in\n // `onError`; the provider already got its 200 by then so it will\n // not retry.\n queueMicrotask(() => {\n void this.deliverEach(accepted)\n })\n\n return { status: 200, body: { received: accepted.length, total: events.length } }\n }\n\n private async deliverEach(events: WebhookEnvelope[]): Promise<void> {\n for (const event of events) {\n try {\n await this.deliver(event)\n if (event.providerEventId && this.idempotency) {\n await this.idempotency.remember(event.providerEventId, this.idempotencyTtlMs)\n }\n } catch (err) {\n this.onError(err, {\n provider: event.provider,\n eventType: event.eventType,\n providerEventId: event.providerEventId,\n })\n }\n }\n }\n}\n\nfunction defaultOnError(err: unknown, context: { provider: string; eventType?: string; providerEventId?: string }): void {\n // eslint-disable-next-line no-console\n console.error('[WebhookRouter]', context, err)\n}\n\nfunction errMessage(err: unknown): string {\n return err instanceof Error ? err.message : String(err)\n}\n","/**\n * Pre-built `WebhookProvider` implementations for the inbound surfaces\n * the substrate ships first-party verifiers for.\n *\n * Each provider implementation is intentionally thin: it delegates\n * signature verification to the corresponding pure function in\n * `connectors/webhooks.ts` and parses the body into one or more\n * normalized `WebhookEnvelope` rows. Anything provider-specific that\n * doesn't fit cleanly (Slack URL-verification handshake, etc.) is\n * surfaced via the envelope `eventType` so the consumer's `deliver()`\n * can branch.\n */\n\nimport {\n firstHeader,\n verifyHmacSignature,\n verifySlackSignature,\n verifyStripeSignature,\n} from '../connectors/webhooks.js'\nimport type { WebhookEnvelope, WebhookHeaders, WebhookProvider, SignatureVerification } from './router.js'\nimport { createHmac, timingSafeEqual } from 'node:crypto'\n\n/** Stripe webhook provider. Signature header `Stripe-Signature`. */\nexport const stripeWebhookProvider: WebhookProvider = {\n id: 'stripe',\n verifySignature({ rawBody, headers, secret }): SignatureVerification {\n const sig = firstHeader(headers, 'stripe-signature')\n if (!sig) return { valid: false, reason: 'missing_stripe_signature' }\n return verifyStripeSignature(rawBody, sig, secret)\n ? { valid: true }\n : { valid: false, reason: 'invalid_signature' }\n },\n parse({ rawBody, headers, now }): WebhookEnvelope[] {\n const evt = safeJson(rawBody)\n if (!evt || typeof evt !== 'object') return []\n const e = evt as { id?: unknown; type?: unknown }\n return [\n {\n provider: 'stripe',\n eventType: typeof e.type === 'string' ? e.type : 'stripe.unknown',\n providerEventId: typeof e.id === 'string' ? e.id : undefined,\n receivedAt: now ?? Date.now(),\n payload: evt,\n headers: normalizeHeaders(headers),\n },\n ]\n },\n}\n\n/** Slack Events API provider. Handles the `url_verification` handshake\n * by emitting a synthetic event the consumer's `deliver()` can echo. */\nexport const slackWebhookProvider: WebhookProvider = {\n id: 'slack',\n verifySignature({ rawBody, headers, secret }): SignatureVerification {\n const sig = firstHeader(headers, 'x-slack-signature')\n const ts = firstHeader(headers, 'x-slack-request-timestamp')\n if (!sig || !ts) return { valid: false, reason: 'missing_slack_signature_or_timestamp' }\n return verifySlackSignature(rawBody, sig, ts, secret)\n ? { valid: true }\n : { valid: false, reason: 'invalid_signature' }\n },\n parse({ rawBody, headers, now }): WebhookEnvelope[] {\n const evt = safeJson(rawBody) as { type?: string; event_id?: string; event?: { type?: string } } | null\n if (!evt || typeof evt !== 'object') return []\n if (evt.type === 'url_verification') {\n return [{\n provider: 'slack',\n eventType: 'slack.url_verification',\n receivedAt: now ?? Date.now(),\n payload: evt,\n headers: normalizeHeaders(headers),\n }]\n }\n const eventType = `slack.${evt.event?.type ?? evt.type ?? 'unknown'}`\n return [{\n provider: 'slack',\n eventType,\n providerEventId: typeof evt.event_id === 'string' ? evt.event_id : undefined,\n receivedAt: now ?? Date.now(),\n payload: evt,\n headers: normalizeHeaders(headers),\n }]\n },\n}\n\n/** DocuSeal webhook provider. Signature header `X-Docuseal-Signature`. */\nexport const docusealWebhookProvider: WebhookProvider = {\n id: 'docuseal',\n verifySignature({ rawBody, headers, secret }): SignatureVerification {\n const sig = firstHeader(headers, 'x-docuseal-signature')\n if (!sig) return { valid: false, reason: 'missing_docuseal_signature' }\n const expected = createHmac('sha256', secret).update(rawBody).digest('hex')\n const a = Buffer.from(sig.toLowerCase(), 'utf-8')\n const b = Buffer.from(expected, 'utf-8')\n if (a.length !== b.length) return { valid: false, reason: 'invalid_signature' }\n return timingSafeEqual(a, b) ? { valid: true } : { valid: false, reason: 'invalid_signature' }\n },\n parse({ rawBody, headers, now }): WebhookEnvelope[] {\n const evt = safeJson(rawBody) as { event_type?: string; event_id?: string } | null\n if (!evt || typeof evt !== 'object') return []\n return [{\n provider: 'docuseal',\n eventType: `docuseal.${evt.event_type ?? 'unknown'}`,\n providerEventId: typeof evt.event_id === 'string' ? evt.event_id : undefined,\n receivedAt: now ?? Date.now(),\n payload: evt,\n headers: normalizeHeaders(headers),\n }]\n },\n}\n\n/** Gmail push provider. Cloud Pub/Sub posts a JWT-signed envelope; the\n * *payload* is base64 JSON describing the changed history range. The\n * signature scheme here is the Pub/Sub JWT auth header — when supplied,\n * consumers SHOULD verify the JWT against Google's well-known\n * certificates. We accept the simpler \"Bearer <pubsub-shared-secret>\"\n * variant by default (matching `verifyHmacSignature`). */\nexport const gmailWebhookProvider: WebhookProvider = {\n id: 'gmail',\n verifySignature({ headers, secret }): SignatureVerification {\n const auth = firstHeader(headers, 'authorization')\n if (!auth) return { valid: false, reason: 'missing_authorization' }\n // Accept either \"Bearer <secret>\" or \"Token <secret>\" formats. This\n // is the simple per-tenant shared-secret path; JWT verification is\n // left to the consumer's `deliver()` when they need full Google JWT\n // chain validation.\n const m = /^(?:Bearer|Token)\\s+(.+)$/i.exec(auth)\n if (!m) return { valid: false, reason: 'invalid_authorization_format' }\n const a = Buffer.from(m[1]!, 'utf-8')\n const b = Buffer.from(secret, 'utf-8')\n if (a.length !== b.length) return { valid: false, reason: 'invalid_signature' }\n return timingSafeEqual(a, b) ? { valid: true } : { valid: false, reason: 'invalid_signature' }\n },\n parse({ rawBody, headers, now }): WebhookEnvelope[] {\n const envelope = safeJson(rawBody) as { message?: { data?: string; messageId?: string; publishTime?: string } } | null\n if (!envelope?.message?.data) return []\n let payload: unknown\n try {\n payload = JSON.parse(Buffer.from(envelope.message.data, 'base64').toString('utf-8'))\n } catch {\n return []\n }\n const inner = payload as { historyId?: number | string; emailAddress?: string }\n return [{\n provider: 'gmail',\n eventType: 'gmail.history_changed',\n providerEventId: envelope.message.messageId,\n receivedAt: now ?? Date.now(),\n payload: { ...inner, publishTime: envelope.message.publishTime },\n headers: normalizeHeaders(headers),\n }]\n },\n}\n\n/** Google Drive push provider. Drive does NOT sign the body — it uses\n * the per-channel token (`X-Goog-Channel-Token`) as the shared secret.\n * The router compares it constant-time against the resolved secret. */\nexport const gdriveWebhookProvider: WebhookProvider = {\n id: 'gdrive',\n verifySignature({ headers, secret }): SignatureVerification {\n const token = firstHeader(headers, 'x-goog-channel-token')\n if (!token) return { valid: false, reason: 'missing_channel_token' }\n const a = Buffer.from(token, 'utf-8')\n const b = Buffer.from(secret, 'utf-8')\n if (a.length !== b.length) return { valid: false, reason: 'invalid_signature' }\n return timingSafeEqual(a, b) ? { valid: true } : { valid: false, reason: 'invalid_signature' }\n },\n parse({ headers, now }): WebhookEnvelope[] {\n const resourceId = firstHeader(headers, 'x-goog-resource-id')\n const resourceState = firstHeader(headers, 'x-goog-resource-state') ?? 'unknown'\n const channelId = firstHeader(headers, 'x-goog-channel-id')\n const messageNumber = firstHeader(headers, 'x-goog-message-number')\n if (resourceState === 'sync') {\n return [{\n provider: 'gdrive',\n eventType: 'gdrive.channel.sync',\n providerEventId: messageNumber ? `${channelId}-${messageNumber}` : undefined,\n receivedAt: now ?? Date.now(),\n payload: { channelId, resourceId, resourceState },\n headers: normalizeHeaders(headers),\n }]\n }\n return [{\n provider: 'gdrive',\n eventType: `gdrive.resource.${resourceState}`,\n providerEventId: messageNumber ? `${channelId}-${messageNumber}` : undefined,\n receivedAt: now ?? Date.now(),\n payload: { channelId, resourceId, resourceState },\n headers: normalizeHeaders(headers),\n }]\n },\n}\n\n/** Generic HMAC provider — for the long-tail webhook source where the\n * caller has standardised on a single sha256-of-body scheme. Header\n * `X-Signature` by default; override at provider-build time if needed. */\nexport function genericHmacWebhookProvider(options: {\n id: string\n signatureHeader?: string\n algorithm?: 'sha256' | 'sha1' | 'sha512'\n signaturePrefix?: string\n /** Parser to convert the raw body into envelopes. Defaults to\n * \"one event with eventType=<provider>.event and payload=JSON\". */\n parse?: WebhookProvider['parse']\n}): WebhookProvider {\n const header = options.signatureHeader ?? 'x-signature'\n return {\n id: options.id,\n verifySignature({ rawBody, headers, secret }) {\n const sig = firstHeader(headers, header)\n if (!sig) return { valid: false, reason: `missing_${header}` }\n return verifyHmacSignature(rawBody, sig, secret, {\n algorithm: options.algorithm ?? 'sha256',\n signaturePrefix: options.signaturePrefix,\n })\n ? { valid: true }\n : { valid: false, reason: 'invalid_signature' }\n },\n parse:\n options.parse ??\n (({ rawBody, headers, now }) => {\n const evt = safeJson(rawBody) ?? rawBody\n return [{\n provider: options.id,\n eventType: `${options.id}.event`,\n receivedAt: now ?? Date.now(),\n payload: evt,\n headers: normalizeHeaders(headers),\n }]\n }),\n }\n}\n\nfunction safeJson(s: string): unknown {\n try {\n return JSON.parse(s)\n } catch {\n return null\n }\n}\n\nfunction normalizeHeaders(headers: WebhookHeaders): Record<string, string> {\n const out: Record<string, string> = {}\n for (const [k, v] of Object.entries(headers)) {\n if (v === undefined) continue\n const value = Array.isArray(v) ? v[0] : v\n if (typeof value === 'string') out[k.toLowerCase()] = value\n }\n return out\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discover the capabilities a workspace can invoke right now.
|
|
3
|
+
*
|
|
4
|
+
* The agent runtime asks the question "what can this workspace do?" and
|
|
5
|
+
* gets back a typed list of MCP-shape tool descriptors gated by the
|
|
6
|
+
* scopes the workspace has granted on each connection.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
discoverWorkspaceCapabilities,
|
|
11
|
+
InMemoryConnectionStore,
|
|
12
|
+
createMockIntegrationProvider,
|
|
13
|
+
} from '@tangle-network/agent-integrations'
|
|
14
|
+
|
|
15
|
+
const owner = { type: 'team' as const, id: 'workspace_acme' }
|
|
16
|
+
|
|
17
|
+
const store = new InMemoryConnectionStore()
|
|
18
|
+
await store.put({
|
|
19
|
+
id: 'conn_gmail',
|
|
20
|
+
owner,
|
|
21
|
+
providerId: 'mock',
|
|
22
|
+
connectorId: 'gmail',
|
|
23
|
+
status: 'active',
|
|
24
|
+
grantedScopes: ['email.read'],
|
|
25
|
+
createdAt: new Date().toISOString(),
|
|
26
|
+
updatedAt: new Date().toISOString(),
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const provider = createMockIntegrationProvider()
|
|
30
|
+
|
|
31
|
+
const discovery = await discoverWorkspaceCapabilities({
|
|
32
|
+
owner,
|
|
33
|
+
store,
|
|
34
|
+
providers: [provider],
|
|
35
|
+
// includeUnconnected: true, // uncomment to render "connect to unlock" affordances
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
for (const capability of discovery.capabilities) {
|
|
39
|
+
console.log(
|
|
40
|
+
`${capability.id} risk=${capability.risk} scopes=${capability.scopes.join(',')} connected=${capability.connected}`,
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (discovery.unreachableConnectors.length > 0) {
|
|
45
|
+
console.warn('Unreachable connectors:', discovery.unreachableConnectors)
|
|
46
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire the inbound webhook router behind a single HTTP handler.
|
|
3
|
+
*
|
|
4
|
+
* The router takes care of signature verification, parsing, and
|
|
5
|
+
* idempotency dedup. The product's `deliver()` callback runs async and
|
|
6
|
+
* sees a normalized envelope.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
WebhookRouter,
|
|
11
|
+
stripeWebhookProvider,
|
|
12
|
+
docusealWebhookProvider,
|
|
13
|
+
slackWebhookProvider,
|
|
14
|
+
} from '@tangle-network/agent-integrations/webhooks'
|
|
15
|
+
|
|
16
|
+
const idempotency = (() => {
|
|
17
|
+
const seen = new Set<string>()
|
|
18
|
+
return {
|
|
19
|
+
seen: (id: string) => seen.has(id),
|
|
20
|
+
remember: (id: string) => {
|
|
21
|
+
seen.add(id)
|
|
22
|
+
},
|
|
23
|
+
}
|
|
24
|
+
})()
|
|
25
|
+
|
|
26
|
+
const router = new WebhookRouter({
|
|
27
|
+
providers: [stripeWebhookProvider, docusealWebhookProvider, slackWebhookProvider],
|
|
28
|
+
idempotency,
|
|
29
|
+
resolveSecret: async (providerId) => {
|
|
30
|
+
// In production: pull from a secret manager keyed by the requesting
|
|
31
|
+
// tenant. Headers (e.g., a Stripe Account-Id) are available to scope
|
|
32
|
+
// the lookup when multiple tenants share a provider.
|
|
33
|
+
if (providerId === 'stripe') return process.env.STRIPE_WEBHOOK_SECRET ?? null
|
|
34
|
+
if (providerId === 'docuseal') return process.env.DOCUSEAL_WEBHOOK_SECRET ?? null
|
|
35
|
+
if (providerId === 'slack') return process.env.SLACK_SIGNING_SECRET ?? null
|
|
36
|
+
return null
|
|
37
|
+
},
|
|
38
|
+
deliver: async (event) => {
|
|
39
|
+
console.log(`[webhook] ${event.eventType} (${event.providerEventId ?? 'no-id'})`)
|
|
40
|
+
// Branch on eventType and enqueue domain-specific work here.
|
|
41
|
+
},
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
// In an HTTP handler:
|
|
45
|
+
// const rawBody = await req.text()
|
|
46
|
+
// const result = await router.handle({
|
|
47
|
+
// providerId: req.params.provider,
|
|
48
|
+
// rawBody,
|
|
49
|
+
// headers: Object.fromEntries(req.headers.entries()),
|
|
50
|
+
// })
|
|
51
|
+
// return new Response(JSON.stringify(result.body), {
|
|
52
|
+
// status: result.status,
|
|
53
|
+
// headers: { 'content-type': 'application/json' },
|
|
54
|
+
// })
|
|
55
|
+
|
|
56
|
+
void router
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tangle-network/agent-integrations",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.0",
|
|
4
4
|
"description": "Vendor-neutral integration contracts and runtime helpers for sandbox and agent apps.",
|
|
5
5
|
"homepage": "https://github.com/tangle-network/agent-integrations#readme",
|
|
6
6
|
"repository": {
|
|
@@ -34,6 +34,11 @@
|
|
|
34
34
|
"import": "./dist/connectors/adapters/index.js",
|
|
35
35
|
"default": "./dist/connectors/adapters/index.js"
|
|
36
36
|
},
|
|
37
|
+
"./webhooks": {
|
|
38
|
+
"types": "./dist/webhooks/index.d.ts",
|
|
39
|
+
"import": "./dist/webhooks/index.js",
|
|
40
|
+
"default": "./dist/webhooks/index.js"
|
|
41
|
+
},
|
|
37
42
|
"./registry": {
|
|
38
43
|
"types": "./dist/registry.d.ts",
|
|
39
44
|
"import": "./dist/registry.js",
|
|
@@ -68,15 +73,6 @@
|
|
|
68
73
|
"publishConfig": {
|
|
69
74
|
"access": "public"
|
|
70
75
|
},
|
|
71
|
-
"scripts": {
|
|
72
|
-
"build": "tsup",
|
|
73
|
-
"dev": "tsup --watch",
|
|
74
|
-
"audit:execution": "pnpm build >/dev/null && node scripts/audit-integration-execution.mjs",
|
|
75
|
-
"prepare": "tsup",
|
|
76
|
-
"test": "vitest run",
|
|
77
|
-
"test:watch": "vitest",
|
|
78
|
-
"typecheck": "tsc --noEmit"
|
|
79
|
-
},
|
|
80
76
|
"devDependencies": {
|
|
81
77
|
"@types/node": "^25.6.0",
|
|
82
78
|
"tsup": "^8.0.0",
|
|
@@ -87,5 +83,12 @@
|
|
|
87
83
|
"node": ">=20"
|
|
88
84
|
},
|
|
89
85
|
"license": "MIT",
|
|
90
|
-
"
|
|
91
|
-
|
|
86
|
+
"scripts": {
|
|
87
|
+
"build": "tsup",
|
|
88
|
+
"dev": "tsup --watch",
|
|
89
|
+
"audit:execution": "pnpm build >/dev/null && node scripts/audit-integration-execution.mjs",
|
|
90
|
+
"test": "vitest run",
|
|
91
|
+
"test:watch": "vitest",
|
|
92
|
+
"typecheck": "tsc --noEmit"
|
|
93
|
+
}
|
|
94
|
+
}
|