@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 CHANGED
@@ -172,6 +172,8 @@ OAuth credentials.
172
172
  | `buildCanonicalLaunchConnectors` | Product-ready launch action schemas for Calendar, Gmail, Drive, GitHub, and Slack. |
173
173
  | `validateProviderPassthroughRequest` | Policy-gated provider-native HTTP escape hatch validation. |
174
174
  | `buildIntegrationToolCatalog` | Converts connector actions into agent/tool definitions. |
175
+ | `discoverWorkspaceCapabilities` | Per-workspace capability discovery: scope-gated, MCP-shape tool descriptors for agent runtime binding. |
176
+ | `WebhookRouter` (+ `stripeWebhookProvider`, `docusealWebhookProvider`, `slackWebhookProvider`, `gmailWebhookProvider`, `gdriveWebhookProvider`, `genericHmacWebhookProvider`) | Inbound webhook router: verify signature, parse, idempotency dedup, async deliver. |
175
177
  | `searchIntegrationTools` | Intent search over normalized integration tools. |
176
178
  | `buildDefaultIntegrationRegistry` | Composes setup specs and vendored catalog metadata into one deduplicated connector registry. |
177
179
  | `composeIntegrationRegistry` | Merges arbitrary catalog sources with explicit aliases, precedence, support tiers, and conflict diagnostics. |
@@ -317,13 +319,16 @@ package-runtime execution.
317
319
  Current first-party adapters:
318
320
 
319
321
  - Google Calendar
320
- - Microsoft Calendar
322
+ - Google Drive
321
323
  - Google Sheets
324
+ - Gmail
325
+ - Microsoft Calendar
322
326
  - Slack
323
327
  - Slack Events
324
328
  - HubSpot
325
329
  - Notion database
326
- - Stripe payments pack
330
+ - DocuSeal
331
+ - Stripe payments pack (customers, invoices, checkout, subscriptions, billing portal)
327
332
  - Stripe webhook receiver
328
333
  - Twilio SMS
329
334
  - Generic webhook
@@ -349,6 +354,10 @@ Runnable examples live in [`examples/`](./examples):
349
354
  REST connector spec.
350
355
  - [`examples/calendar-exercise-app.ts`](./examples/calendar-exercise-app.ts) -
351
356
  generated-app golden path: manifest, consent copy, bridge env, and invoke.
357
+ - [`examples/discover-capabilities.ts`](./examples/discover-capabilities.ts) -
358
+ per-workspace capability discovery for agent tool-registry binding.
359
+ - [`examples/webhook-router.ts`](./examples/webhook-router.ts) - inbound
360
+ webhook router with Stripe, DocuSeal, and Slack providers wired in.
352
361
 
353
362
  The README stays short; examples are separate so they can be copied and expanded
354
363
  without obscuring the package contract.
@@ -4,10 +4,11 @@ import {
4
4
  buildTangleCatalogRuntimePackageManifest,
5
5
  renderTangleCatalogRuntimePnpmAddCommand,
6
6
  startTangleCatalogRuntimeNodeServer
7
- } from "../chunk-S54DPRDU.js";
7
+ } from "../chunk-ALCIWTIR.js";
8
8
  import "../chunk-4JQ754PA.js";
9
9
  import "../chunk-376UBTNB.js";
10
- import "../chunk-WC63AI4Q.js";
10
+ import "../chunk-GA4VTE3U.js";
11
+ import "../chunk-2TW2QKGZ.js";
11
12
 
12
13
  // src/bin/tangle-catalog-runtime.ts
13
14
  var args = new Set(process.argv.slice(2));
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/bin/tangle-catalog-runtime.ts"],"sourcesContent":["#!/usr/bin/env node\nimport {\n buildTangleCatalogRuntimePackageManifest,\n renderTangleCatalogRuntimePnpmAddCommand,\n} from '../tangle-catalog.js'\nimport { auditTangleCatalogRuntimePackages } from '../tangle-catalog-runtime.js'\nimport { startTangleCatalogRuntimeNodeServer } from '../tangle-catalog-runtime-server.js'\n\nconst args = new Set(process.argv.slice(2))\nif (args.has('--print-package-json')) {\n console.log(JSON.stringify(buildTangleCatalogRuntimePackageManifest({\n agentIntegrationsVersion: process.env.TANGLE_AGENT_INTEGRATIONS_VERSION,\n }), null, 2))\n process.exit(0)\n}\n\nif (args.has('--print-pnpm-add')) {\n console.log(renderTangleCatalogRuntimePnpmAddCommand({\n agentIntegrationsVersion: process.env.TANGLE_AGENT_INTEGRATIONS_VERSION,\n }))\n process.exit(0)\n}\n\nif (args.has('--audit-packages')) {\n const connectorIds = process.env.TANGLE_CATALOG_AUDIT_CONNECTORS\n ?.split(',')\n .map((id) => id.trim())\n .filter(Boolean)\n console.log(JSON.stringify(await auditTangleCatalogRuntimePackages({ connectorIds }), null, 2))\n process.exit(0)\n}\n\nconst secret = process.env.TANGLE_CATALOG_RUNTIME_SECRET\nif (!secret || secret.length < 32) {\n console.error('TANGLE_CATALOG_RUNTIME_SECRET must be set to at least 32 characters.')\n process.exit(1)\n}\n\nconst authResolverUrl = process.env.TANGLE_CATALOG_AUTH_RESOLVER_URL\nconst authResolverSecret = process.env.TANGLE_CATALOG_AUTH_RESOLVER_SECRET\nif (Boolean(authResolverUrl) !== Boolean(authResolverSecret)) {\n console.error('TANGLE_CATALOG_AUTH_RESOLVER_URL and TANGLE_CATALOG_AUTH_RESOLVER_SECRET must be set together.')\n process.exit(1)\n}\n\nconst port = Number(process.env.PORT ?? process.env.TANGLE_CATALOG_RUNTIME_PORT ?? 4109)\nif (!Number.isInteger(port) || port < 1 || port > 65_535) {\n console.error('PORT must be an integer between 1 and 65535.')\n process.exit(1)\n}\n\nconst server = await startTangleCatalogRuntimeNodeServer({\n secret,\n host: process.env.HOST ?? process.env.TANGLE_CATALOG_RUNTIME_HOST ?? '0.0.0.0',\n port,\n authResolver: authResolverUrl && authResolverSecret\n ? {\n endpoint: authResolverUrl,\n secret: authResolverSecret,\n }\n : false,\n onLog: (event) => {\n const line = JSON.stringify({\n level: event.level,\n message: event.message,\n ...event.metadata,\n })\n if (event.level === 'error') console.error(line)\n else console.log(line)\n },\n})\n\nconsole.log(JSON.stringify({\n level: 'info',\n message: 'Tangle catalog runtime listening.',\n url: server.url,\n}))\n\nfor (const signal of ['SIGINT', 'SIGTERM'] as const) {\n process.once(signal, async () => {\n await server.close()\n process.exit(0)\n })\n}\n"],"mappings":";;;;;;;;;;;;AAQA,IAAM,OAAO,IAAI,IAAI,QAAQ,KAAK,MAAM,CAAC,CAAC;AAC1C,IAAI,KAAK,IAAI,sBAAsB,GAAG;AACpC,UAAQ,IAAI,KAAK,UAAU,yCAAyC;AAAA,IAClE,0BAA0B,QAAQ,IAAI;AAAA,EACxC,CAAC,GAAG,MAAM,CAAC,CAAC;AACZ,UAAQ,KAAK,CAAC;AAChB;AAEA,IAAI,KAAK,IAAI,kBAAkB,GAAG;AAChC,UAAQ,IAAI,yCAAyC;AAAA,IACnD,0BAA0B,QAAQ,IAAI;AAAA,EACxC,CAAC,CAAC;AACF,UAAQ,KAAK,CAAC;AAChB;AAEA,IAAI,KAAK,IAAI,kBAAkB,GAAG;AAChC,QAAM,eAAe,QAAQ,IAAI,iCAC7B,MAAM,GAAG,EACV,IAAI,CAAC,OAAO,GAAG,KAAK,CAAC,EACrB,OAAO,OAAO;AACjB,UAAQ,IAAI,KAAK,UAAU,MAAM,kCAAkC,EAAE,aAAa,CAAC,GAAG,MAAM,CAAC,CAAC;AAC9F,UAAQ,KAAK,CAAC;AAChB;AAEA,IAAM,SAAS,QAAQ,IAAI;AAC3B,IAAI,CAAC,UAAU,OAAO,SAAS,IAAI;AACjC,UAAQ,MAAM,sEAAsE;AACpF,UAAQ,KAAK,CAAC;AAChB;AAEA,IAAM,kBAAkB,QAAQ,IAAI;AACpC,IAAM,qBAAqB,QAAQ,IAAI;AACvC,IAAI,QAAQ,eAAe,MAAM,QAAQ,kBAAkB,GAAG;AAC5D,UAAQ,MAAM,gGAAgG;AAC9G,UAAQ,KAAK,CAAC;AAChB;AAEA,IAAM,OAAO,OAAO,QAAQ,IAAI,QAAQ,QAAQ,IAAI,+BAA+B,IAAI;AACvF,IAAI,CAAC,OAAO,UAAU,IAAI,KAAK,OAAO,KAAK,OAAO,OAAQ;AACxD,UAAQ,MAAM,8CAA8C;AAC5D,UAAQ,KAAK,CAAC;AAChB;AAEA,IAAM,SAAS,MAAM,oCAAoC;AAAA,EACvD;AAAA,EACA,MAAM,QAAQ,IAAI,QAAQ,QAAQ,IAAI,+BAA+B;AAAA,EACrE;AAAA,EACA,cAAc,mBAAmB,qBAC7B;AAAA,IACE,UAAU;AAAA,IACV,QAAQ;AAAA,EACV,IACA;AAAA,EACJ,OAAO,CAAC,UAAU;AAChB,UAAM,OAAO,KAAK,UAAU;AAAA,MAC1B,OAAO,MAAM;AAAA,MACb,SAAS,MAAM;AAAA,MACf,GAAG,MAAM;AAAA,IACX,CAAC;AACD,QAAI,MAAM,UAAU,QAAS,SAAQ,MAAM,IAAI;AAAA,QAC1C,SAAQ,IAAI,IAAI;AAAA,EACvB;AACF,CAAC;AAED,QAAQ,IAAI,KAAK,UAAU;AAAA,EACzB,OAAO;AAAA,EACP,SAAS;AAAA,EACT,KAAK,OAAO;AACd,CAAC,CAAC;AAEF,WAAW,UAAU,CAAC,UAAU,SAAS,GAAY;AACnD,UAAQ,KAAK,QAAQ,YAAY;AAC/B,UAAM,OAAO,MAAM;AACnB,YAAQ,KAAK,CAAC;AAAA,EAChB,CAAC;AACH;","names":[]}
1
+ {"version":3,"sources":["../../src/bin/tangle-catalog-runtime.ts"],"sourcesContent":["#!/usr/bin/env node\nimport {\n buildTangleCatalogRuntimePackageManifest,\n renderTangleCatalogRuntimePnpmAddCommand,\n} from '../tangle-catalog.js'\nimport { auditTangleCatalogRuntimePackages } from '../tangle-catalog-runtime.js'\nimport { startTangleCatalogRuntimeNodeServer } from '../tangle-catalog-runtime-server.js'\n\nconst args = new Set(process.argv.slice(2))\nif (args.has('--print-package-json')) {\n console.log(JSON.stringify(buildTangleCatalogRuntimePackageManifest({\n agentIntegrationsVersion: process.env.TANGLE_AGENT_INTEGRATIONS_VERSION,\n }), null, 2))\n process.exit(0)\n}\n\nif (args.has('--print-pnpm-add')) {\n console.log(renderTangleCatalogRuntimePnpmAddCommand({\n agentIntegrationsVersion: process.env.TANGLE_AGENT_INTEGRATIONS_VERSION,\n }))\n process.exit(0)\n}\n\nif (args.has('--audit-packages')) {\n const connectorIds = process.env.TANGLE_CATALOG_AUDIT_CONNECTORS\n ?.split(',')\n .map((id) => id.trim())\n .filter(Boolean)\n console.log(JSON.stringify(await auditTangleCatalogRuntimePackages({ connectorIds }), null, 2))\n process.exit(0)\n}\n\nconst secret = process.env.TANGLE_CATALOG_RUNTIME_SECRET\nif (!secret || secret.length < 32) {\n console.error('TANGLE_CATALOG_RUNTIME_SECRET must be set to at least 32 characters.')\n process.exit(1)\n}\n\nconst authResolverUrl = process.env.TANGLE_CATALOG_AUTH_RESOLVER_URL\nconst authResolverSecret = process.env.TANGLE_CATALOG_AUTH_RESOLVER_SECRET\nif (Boolean(authResolverUrl) !== Boolean(authResolverSecret)) {\n console.error('TANGLE_CATALOG_AUTH_RESOLVER_URL and TANGLE_CATALOG_AUTH_RESOLVER_SECRET must be set together.')\n process.exit(1)\n}\n\nconst port = Number(process.env.PORT ?? process.env.TANGLE_CATALOG_RUNTIME_PORT ?? 4109)\nif (!Number.isInteger(port) || port < 1 || port > 65_535) {\n console.error('PORT must be an integer between 1 and 65535.')\n process.exit(1)\n}\n\nconst server = await startTangleCatalogRuntimeNodeServer({\n secret,\n host: process.env.HOST ?? process.env.TANGLE_CATALOG_RUNTIME_HOST ?? '0.0.0.0',\n port,\n authResolver: authResolverUrl && authResolverSecret\n ? {\n endpoint: authResolverUrl,\n secret: authResolverSecret,\n }\n : false,\n onLog: (event) => {\n const line = JSON.stringify({\n level: event.level,\n message: event.message,\n ...event.metadata,\n })\n if (event.level === 'error') console.error(line)\n else console.log(line)\n },\n})\n\nconsole.log(JSON.stringify({\n level: 'info',\n message: 'Tangle catalog runtime listening.',\n url: server.url,\n}))\n\nfor (const signal of ['SIGINT', 'SIGTERM'] as const) {\n process.once(signal, async () => {\n await server.close()\n process.exit(0)\n })\n}\n"],"mappings":";;;;;;;;;;;;;AAQA,IAAM,OAAO,IAAI,IAAI,QAAQ,KAAK,MAAM,CAAC,CAAC;AAC1C,IAAI,KAAK,IAAI,sBAAsB,GAAG;AACpC,UAAQ,IAAI,KAAK,UAAU,yCAAyC;AAAA,IAClE,0BAA0B,QAAQ,IAAI;AAAA,EACxC,CAAC,GAAG,MAAM,CAAC,CAAC;AACZ,UAAQ,KAAK,CAAC;AAChB;AAEA,IAAI,KAAK,IAAI,kBAAkB,GAAG;AAChC,UAAQ,IAAI,yCAAyC;AAAA,IACnD,0BAA0B,QAAQ,IAAI;AAAA,EACxC,CAAC,CAAC;AACF,UAAQ,KAAK,CAAC;AAChB;AAEA,IAAI,KAAK,IAAI,kBAAkB,GAAG;AAChC,QAAM,eAAe,QAAQ,IAAI,iCAC7B,MAAM,GAAG,EACV,IAAI,CAAC,OAAO,GAAG,KAAK,CAAC,EACrB,OAAO,OAAO;AACjB,UAAQ,IAAI,KAAK,UAAU,MAAM,kCAAkC,EAAE,aAAa,CAAC,GAAG,MAAM,CAAC,CAAC;AAC9F,UAAQ,KAAK,CAAC;AAChB;AAEA,IAAM,SAAS,QAAQ,IAAI;AAC3B,IAAI,CAAC,UAAU,OAAO,SAAS,IAAI;AACjC,UAAQ,MAAM,sEAAsE;AACpF,UAAQ,KAAK,CAAC;AAChB;AAEA,IAAM,kBAAkB,QAAQ,IAAI;AACpC,IAAM,qBAAqB,QAAQ,IAAI;AACvC,IAAI,QAAQ,eAAe,MAAM,QAAQ,kBAAkB,GAAG;AAC5D,UAAQ,MAAM,gGAAgG;AAC9G,UAAQ,KAAK,CAAC;AAChB;AAEA,IAAM,OAAO,OAAO,QAAQ,IAAI,QAAQ,QAAQ,IAAI,+BAA+B,IAAI;AACvF,IAAI,CAAC,OAAO,UAAU,IAAI,KAAK,OAAO,KAAK,OAAO,OAAQ;AACxD,UAAQ,MAAM,8CAA8C;AAC5D,UAAQ,KAAK,CAAC;AAChB;AAEA,IAAM,SAAS,MAAM,oCAAoC;AAAA,EACvD;AAAA,EACA,MAAM,QAAQ,IAAI,QAAQ,QAAQ,IAAI,+BAA+B;AAAA,EACrE;AAAA,EACA,cAAc,mBAAmB,qBAC7B;AAAA,IACE,UAAU;AAAA,IACV,QAAQ;AAAA,EACV,IACA;AAAA,EACJ,OAAO,CAAC,UAAU;AAChB,UAAM,OAAO,KAAK,UAAU;AAAA,MAC1B,OAAO,MAAM;AAAA,MACb,SAAS,MAAM;AAAA,MACf,GAAG,MAAM;AAAA,IACX,CAAC;AACD,QAAI,MAAM,UAAU,QAAS,SAAQ,MAAM,IAAI;AAAA,QAC1C,SAAQ,IAAI,IAAI;AAAA,EACvB;AACF,CAAC;AAED,QAAQ,IAAI,KAAK,UAAU;AAAA,EACzB,OAAO;AAAA,EACP,SAAS;AAAA,EACT,KAAK,OAAO;AACd,CAAC,CAAC;AAEF,WAAW,UAAU,CAAC,UAAU,SAAS,GAAY;AACnD,UAAQ,KAAK,QAAQ,YAAY;AAC/B,UAAM,OAAO,MAAM;AACnB,YAAQ,KAAK,CAAC;AAAA,EAChB,CAAC;AACH;","names":[]}
package/dist/catalog.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  export { I as IntegrationCatalogView, a as IntegrationToolDefinition, b as IntegrationToolSearchFilters, c as IntegrationToolSearchResult, M as McpToolDefinition, d as buildIntegrationCatalogView, e as buildIntegrationToolCatalog, i as integrationToolName, p as parseIntegrationToolName, s as searchIntegrationTools, t as toMcpTools } from './registry.js';
2
- import './index-BQY5ry2s.js';
2
+ import './index-D4D4CEKX.js';
3
3
  import './connectors/index.js';
4
4
  import 'node:http';
package/dist/catalog.js CHANGED
@@ -5,10 +5,11 @@ import {
5
5
  parseIntegrationToolName,
6
6
  searchIntegrationTools,
7
7
  toMcpTools
8
- } from "./chunk-S54DPRDU.js";
8
+ } from "./chunk-ALCIWTIR.js";
9
9
  import "./chunk-4JQ754PA.js";
10
10
  import "./chunk-376UBTNB.js";
11
- import "./chunk-WC63AI4Q.js";
11
+ import "./chunk-GA4VTE3U.js";
12
+ import "./chunk-2TW2QKGZ.js";
12
13
  export {
13
14
  buildIntegrationCatalogView,
14
15
  buildIntegrationToolCatalog,
@@ -0,0 +1,94 @@
1
+ // src/connectors/webhooks.ts
2
+ import { createHmac, timingSafeEqual } from "crypto";
3
+ var DEFAULT_SIGNATURE_TOLERANCE_SECONDS = 5 * 60;
4
+ function parseStripeSignatureHeader(header) {
5
+ const acc = { sigs: [] };
6
+ for (const part of header.split(",")) {
7
+ const idx = part.indexOf("=");
8
+ if (idx < 0) continue;
9
+ const key = part.slice(0, idx).trim();
10
+ const val = part.slice(idx + 1).trim();
11
+ if (key === "t") {
12
+ const n = Number(val);
13
+ if (Number.isFinite(n)) acc.ts = n;
14
+ } else if (key === "v1") {
15
+ acc.sigs.push(val);
16
+ }
17
+ }
18
+ if (acc.ts === void 0 || acc.sigs.length === 0) return null;
19
+ return { t: acc.ts, sigs: acc.sigs };
20
+ }
21
+ function verifyStripeSignature(rawBody, signatureHeader, secret, options = {}) {
22
+ const parsed = parseStripeSignatureHeader(signatureHeader);
23
+ if (!parsed) return false;
24
+ const tolerance = options.toleranceSeconds ?? DEFAULT_SIGNATURE_TOLERANCE_SECONDS;
25
+ const now = options.now ?? Math.floor(Date.now() / 1e3);
26
+ if (Math.abs(now - parsed.t) > tolerance) return false;
27
+ const expected = createHmac("sha256", secret).update(`${parsed.t}.${rawBody}`).digest("hex");
28
+ const expectedBuf = Buffer.from(expected, "utf8");
29
+ for (const sig of parsed.sigs) {
30
+ const sigBuf = Buffer.from(sig, "utf8");
31
+ if (sigBuf.length !== expectedBuf.length) continue;
32
+ if (timingSafeEqual(sigBuf, expectedBuf)) return true;
33
+ }
34
+ return false;
35
+ }
36
+ function verifySlackSignature(rawBody, signatureHeader, timestampHeader, secret, options = {}) {
37
+ if (!signatureHeader.startsWith("v0=")) return false;
38
+ const ts = Number(timestampHeader);
39
+ if (!Number.isFinite(ts)) return false;
40
+ const tolerance = options.toleranceSeconds ?? DEFAULT_SIGNATURE_TOLERANCE_SECONDS;
41
+ const now = options.now ?? Math.floor(Date.now() / 1e3);
42
+ if (Math.abs(now - ts) > tolerance) return false;
43
+ const expected = "v0=" + createHmac("sha256", secret).update(`v0:${ts}:${rawBody}`).digest("hex");
44
+ const expectedBuf = Buffer.from(expected, "utf8");
45
+ const sigBuf = Buffer.from(signatureHeader, "utf8");
46
+ if (sigBuf.length !== expectedBuf.length) return false;
47
+ return timingSafeEqual(sigBuf, expectedBuf);
48
+ }
49
+ function verifyHmacSignature(rawBody, signatureHeader, secret, options = {}) {
50
+ const algorithm = options.algorithm ?? "sha256";
51
+ const prefix = options.signaturePrefix ?? "";
52
+ const lower = options.lowercaseHex ?? true;
53
+ let candidate = signatureHeader;
54
+ if (prefix) {
55
+ if (!candidate.startsWith(prefix)) return false;
56
+ candidate = candidate.slice(prefix.length);
57
+ }
58
+ if (lower) candidate = candidate.toLowerCase();
59
+ const expected = createHmac(algorithm, secret).update(rawBody).digest("hex");
60
+ const expectedBuf = Buffer.from(expected, "utf8");
61
+ const sigBuf = Buffer.from(candidate, "utf8");
62
+ if (sigBuf.length !== expectedBuf.length) return false;
63
+ return timingSafeEqual(sigBuf, expectedBuf);
64
+ }
65
+ function verifyTwilioSignature(input, options = {}) {
66
+ if (!input.authToken) {
67
+ return options.skipWhenAuthTokenMissing === true;
68
+ }
69
+ const signature = input.signatureHeader;
70
+ if (!signature || Array.isArray(signature)) return false;
71
+ if (!input.fullUrl) return false;
72
+ const data = options.bodyAsRaw === true ? input.fullUrl + (options.rawBody ?? "") : Object.keys(input.params ?? {}).sort().reduce((acc, key) => acc + key + (input.params[key] ?? ""), input.fullUrl);
73
+ const expected = createHmac("sha1", input.authToken).update(data).digest("base64");
74
+ const expectedBuf = Buffer.from(expected);
75
+ const sigBuf = Buffer.from(signature);
76
+ if (expectedBuf.length !== sigBuf.length) return false;
77
+ return timingSafeEqual(expectedBuf, sigBuf);
78
+ }
79
+ function firstHeader(headers, name) {
80
+ const v = headers[name] ?? headers[name.toLowerCase()] ?? Object.entries(headers).find(([key]) => key.toLowerCase() === name.toLowerCase())?.[1];
81
+ if (Array.isArray(v)) return v[0];
82
+ return typeof v === "string" ? v : void 0;
83
+ }
84
+
85
+ export {
86
+ DEFAULT_SIGNATURE_TOLERANCE_SECONDS,
87
+ parseStripeSignatureHeader,
88
+ verifyStripeSignature,
89
+ verifySlackSignature,
90
+ verifyHmacSignature,
91
+ verifyTwilioSignature,
92
+ firstHeader
93
+ };
94
+ //# sourceMappingURL=chunk-2TW2QKGZ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/connectors/webhooks.ts"],"sourcesContent":["/**\n * Inbound webhook signature verifiers — provider-specific HMAC schemes.\n *\n * Each signature scheme is a pure function:\n * (rawBody: string, headers, secret, now?) → boolean\n *\n * Constant-time comparison via `crypto.timingSafeEqual`. Timestamps are\n * checked against a configurable tolerance to bound replay risk; the default\n * mirrors the upstream provider's documented window (Stripe: 5 min, Slack: 5 min).\n *\n * These verifiers are the building blocks for any inbound-webhook receiver\n * (a route + a `verify` call + a per-event handler). They live in this\n * package so every consumer of the integration substrate gets correct\n * verification — not just one product reimplementing it.\n */\n\nimport { createHmac, timingSafeEqual } from 'node:crypto'\n\n/** Default replay-protection window. Providers commonly use 5 minutes. */\nexport const DEFAULT_SIGNATURE_TOLERANCE_SECONDS = 5 * 60\n\n// ─── Stripe ─────────────────────────────────────────────────────────────\n//\n// Stripe signs webhooks with a single header `Stripe-Signature` of the form\n//\n// t=<timestamp>,v1=<sig1>,v1=<sig2>,...\n//\n// where `t` is the Unix timestamp the event was generated, and each `v1`\n// is `HMAC-SHA256(secret, \"<t>.<rawBody>\")`. Multiple `v1` entries appear\n// during secret rotation — any one matching is sufficient.\n//\n// https://stripe.com/docs/webhooks/signatures\n\nexport interface ParsedStripeSignatureHeader {\n t: number\n sigs: string[]\n}\n\nexport function parseStripeSignatureHeader(header: string): ParsedStripeSignatureHeader | null {\n const acc: { ts?: number; sigs: string[] } = { sigs: [] }\n for (const part of header.split(',')) {\n const idx = part.indexOf('=')\n if (idx < 0) continue\n const key = part.slice(0, idx).trim()\n const val = part.slice(idx + 1).trim()\n if (key === 't') {\n const n = Number(val)\n if (Number.isFinite(n)) acc.ts = n\n } else if (key === 'v1') {\n acc.sigs.push(val)\n }\n }\n if (acc.ts === undefined || acc.sigs.length === 0) return null\n return { t: acc.ts, sigs: acc.sigs }\n}\n\nexport interface StripeVerifyOptions {\n /** Replay-protection window in seconds. Default 300. */\n toleranceSeconds?: number\n /** Override `now()` for tests. UTC seconds. */\n now?: number\n}\n\n/** Verify a Stripe webhook signature against the raw request body. */\nexport function verifyStripeSignature(\n rawBody: string,\n signatureHeader: string,\n secret: string,\n options: StripeVerifyOptions = {},\n): boolean {\n const parsed = parseStripeSignatureHeader(signatureHeader)\n if (!parsed) return false\n const tolerance = options.toleranceSeconds ?? DEFAULT_SIGNATURE_TOLERANCE_SECONDS\n const now = options.now ?? Math.floor(Date.now() / 1000)\n if (Math.abs(now - parsed.t) > tolerance) return false\n const expected = createHmac('sha256', secret).update(`${parsed.t}.${rawBody}`).digest('hex')\n const expectedBuf = Buffer.from(expected, 'utf8')\n for (const sig of parsed.sigs) {\n const sigBuf = Buffer.from(sig, 'utf8')\n if (sigBuf.length !== expectedBuf.length) continue\n if (timingSafeEqual(sigBuf, expectedBuf)) return true\n }\n return false\n}\n\n// ─── Slack ──────────────────────────────────────────────────────────────\n//\n// Slack signs request bodies with two headers:\n//\n// X-Slack-Signature: v0=<HMAC-SHA256(secret, \"v0:<ts>:<body>\")>\n// X-Slack-Request-Timestamp: <ts>\n//\n// https://api.slack.com/authentication/verifying-requests-from-slack\n\nexport interface SlackVerifyOptions {\n toleranceSeconds?: number\n now?: number\n}\n\nexport function verifySlackSignature(\n rawBody: string,\n signatureHeader: string,\n timestampHeader: string,\n secret: string,\n options: SlackVerifyOptions = {},\n): boolean {\n if (!signatureHeader.startsWith('v0=')) return false\n const ts = Number(timestampHeader)\n if (!Number.isFinite(ts)) return false\n const tolerance = options.toleranceSeconds ?? DEFAULT_SIGNATURE_TOLERANCE_SECONDS\n const now = options.now ?? Math.floor(Date.now() / 1000)\n if (Math.abs(now - ts) > tolerance) return false\n const expected = 'v0=' + createHmac('sha256', secret).update(`v0:${ts}:${rawBody}`).digest('hex')\n const expectedBuf = Buffer.from(expected, 'utf8')\n const sigBuf = Buffer.from(signatureHeader, 'utf8')\n if (sigBuf.length !== expectedBuf.length) return false\n return timingSafeEqual(sigBuf, expectedBuf)\n}\n\n// ─── Generic HMAC ───────────────────────────────────────────────────────\n//\n// For \"we shipped a webhook URL with a shared HMAC secret\" patterns —\n// covers any custom integration where the operator picks the message\n// format. The signed message is the literal `rawBody` (no timestamp\n// prefix); replay protection is the caller's responsibility (use a\n// nonce header + a small dedup cache).\n\nexport interface GenericHmacVerifyOptions {\n /** sha256 (default) | sha1 | sha512 — matches the algorithm the receiver\n * computed at sign time. */\n algorithm?: 'sha256' | 'sha1' | 'sha512'\n /** Optional prefix the receiver prepends to the signature in the header\n * (e.g., `'sha256='`). Stripped before constant-time comparison. */\n signaturePrefix?: string\n /** Lowercase comparison (most providers emit hex-lowercase). Default true. */\n lowercaseHex?: boolean\n}\n\nexport function verifyHmacSignature(\n rawBody: string,\n signatureHeader: string,\n secret: string,\n options: GenericHmacVerifyOptions = {},\n): boolean {\n const algorithm = options.algorithm ?? 'sha256'\n const prefix = options.signaturePrefix ?? ''\n const lower = options.lowercaseHex ?? true\n let candidate = signatureHeader\n if (prefix) {\n if (!candidate.startsWith(prefix)) return false\n candidate = candidate.slice(prefix.length)\n }\n if (lower) candidate = candidate.toLowerCase()\n const expected = createHmac(algorithm, secret).update(rawBody).digest('hex')\n const expectedBuf = Buffer.from(expected, 'utf8')\n const sigBuf = Buffer.from(candidate, 'utf8')\n if (sigBuf.length !== expectedBuf.length) return false\n return timingSafeEqual(sigBuf, expectedBuf)\n}\n\n// ─── Twilio ─────────────────────────────────────────────────────────────\n//\n// Twilio's webhook signature scheme is unlike Stripe/Slack — it doesn't\n// sign the raw body. It signs the concatenation of:\n//\n// fullUrl + sortedConcatenatedParams\n//\n// where `fullUrl` is the public URL Twilio called (scheme + host + path\n// + query, exactly as Twilio constructed it from your console config),\n// and `sortedConcatenatedParams` is `key1+value1+key2+value2+...` over\n// the alphabetically-sorted keys of the POST body params (form-encoded).\n//\n// For JSON-bodied Twilio webhooks (Conversations API), the body is signed\n// as raw bytes — pass `{ bodyAsRaw: true, rawBody }` for that path.\n//\n// HMAC-SHA1, base64-encoded. Header: `X-Twilio-Signature`.\n//\n// https://www.twilio.com/docs/usage/webhooks/webhooks-security\n\nexport interface TwilioVerifyOptions {\n /** Skip verification when the auth token isn't configured. Useful in\n * dev where the receiver wants to accept any payload. Default `false`\n * — production should always require a configured token. */\n skipWhenAuthTokenMissing?: boolean\n /** When true, sign the raw body instead of the URL-encoded sorted-params\n * reduction. Twilio uses raw-body signing for `application/json`\n * webhook bodies. Default `false`. */\n bodyAsRaw?: boolean\n /** When `bodyAsRaw` is true, the raw body to sign. Ignored otherwise. */\n rawBody?: string\n}\n\n/** Verify a Twilio webhook signature. */\nexport function verifyTwilioSignature(\n input: {\n authToken: string | null | undefined\n signatureHeader: string | string[] | undefined\n fullUrl: string | null | undefined\n params: Record<string, string> | undefined\n },\n options: TwilioVerifyOptions = {},\n): boolean {\n if (!input.authToken) {\n return options.skipWhenAuthTokenMissing === true\n }\n const signature = input.signatureHeader\n if (!signature || Array.isArray(signature)) return false\n if (!input.fullUrl) return false\n\n const data = options.bodyAsRaw === true\n ? input.fullUrl + (options.rawBody ?? '')\n : Object.keys(input.params ?? {})\n .sort()\n .reduce((acc, key) => acc + key + (input.params![key] ?? ''), input.fullUrl)\n\n const expected = createHmac('sha1', input.authToken).update(data).digest('base64')\n const expectedBuf = Buffer.from(expected)\n const sigBuf = Buffer.from(signature)\n if (expectedBuf.length !== sigBuf.length) return false\n return timingSafeEqual(expectedBuf, sigBuf)\n}\n\n// ─── Header helper ──────────────────────────────────────────────────────\n//\n// Most fastify/express adapters expose request headers as\n// `Record<string, string | string[] | undefined>`. This helper picks the\n// first canonical value for a given name (case-insensitive).\n\nexport function firstHeader(\n headers: Record<string, string | string[] | undefined>,\n name: string,\n): string | undefined {\n const v = headers[name]\n ?? headers[name.toLowerCase()]\n ?? Object.entries(headers).find(([key]) => key.toLowerCase() === name.toLowerCase())?.[1]\n if (Array.isArray(v)) return v[0]\n return typeof v === 'string' ? v : undefined\n}\n"],"mappings":"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","names":[]}
@@ -1199,6 +1199,100 @@ function secretKey(ref) {
1199
1199
  return `${ref.provider}:${ref.id}`;
1200
1200
  }
1201
1201
 
1202
+ // src/discovery.ts
1203
+ async function discoverWorkspaceCapabilities(input) {
1204
+ const connections = await resolveConnections(input);
1205
+ const connectors = await resolveConnectors(input);
1206
+ const activeConnectionsByConnector = /* @__PURE__ */ new Map();
1207
+ for (const conn of connections) {
1208
+ if (conn.status !== "active") continue;
1209
+ if (!activeConnectionsByConnector.has(conn.connectorId)) {
1210
+ activeConnectionsByConnector.set(conn.connectorId, conn);
1211
+ }
1212
+ }
1213
+ const capabilities = [];
1214
+ const triggers = [];
1215
+ const countsByConnector = {};
1216
+ const unreachableConnectors = [];
1217
+ for (const connector of connectors) {
1218
+ const connection = activeConnectionsByConnector.get(connector.id);
1219
+ const connected = Boolean(connection);
1220
+ if (!connected && !input.includeUnconnected) continue;
1221
+ const grantedScopes = new Set(connection?.grantedScopes ?? []);
1222
+ let actionsAdded = 0;
1223
+ for (const action of connector.actions) {
1224
+ const missing2 = action.requiredScopes.filter((scope) => !grantedScopes.has(scope));
1225
+ if (connected && missing2.length > 0 && !input.includeMissingScopes) continue;
1226
+ capabilities.push(toCapability(connector, action, connection));
1227
+ actionsAdded += 1;
1228
+ }
1229
+ for (const trigger2 of connector.triggers ?? []) {
1230
+ const missing2 = trigger2.requiredScopes.filter((scope) => !grantedScopes.has(scope));
1231
+ if (connected && missing2.length > 0 && !input.includeMissingScopes) continue;
1232
+ triggers.push(toTrigger2(connector, trigger2, connection));
1233
+ }
1234
+ countsByConnector[connector.id] = actionsAdded;
1235
+ if (connected && actionsAdded === 0 && connector.actions.length > 0) {
1236
+ unreachableConnectors.push({
1237
+ connectorId: connector.id,
1238
+ reason: "all_actions_missing_scope"
1239
+ });
1240
+ }
1241
+ }
1242
+ return { capabilities, triggers, countsByConnector, unreachableConnectors };
1243
+ }
1244
+ async function resolveConnections(input) {
1245
+ if (input.connections) return input.connections;
1246
+ if (input.store) return await input.store.listByOwner(input.owner);
1247
+ throw new Error("discoverWorkspaceCapabilities: provide either connections or store");
1248
+ }
1249
+ async function resolveConnectors(input) {
1250
+ if (input.connectors) return input.connectors;
1251
+ if (input.providers) {
1252
+ const lists = await Promise.all(input.providers.map((p) => Promise.resolve(p.listConnectors())));
1253
+ return lists.flat();
1254
+ }
1255
+ throw new Error("discoverWorkspaceCapabilities: provide either connectors or providers");
1256
+ }
1257
+ function toCapability(connector, action, connection) {
1258
+ return {
1259
+ id: `${connector.id}.${action.id}`,
1260
+ title: action.title,
1261
+ description: action.description,
1262
+ category: connector.category,
1263
+ connectorId: connector.id,
1264
+ providerId: connector.providerId,
1265
+ actionId: action.id,
1266
+ scopes: action.requiredScopes,
1267
+ risk: action.risk,
1268
+ dataClass: action.dataClass,
1269
+ toolSchema: {
1270
+ name: `${connector.id}.${action.id}`,
1271
+ description: action.description,
1272
+ inputSchema: action.inputSchema,
1273
+ outputSchema: action.outputSchema
1274
+ },
1275
+ connected: Boolean(connection),
1276
+ connectionId: connection?.id,
1277
+ approvalRequired: action.approvalRequired
1278
+ };
1279
+ }
1280
+ function toTrigger2(connector, trigger2, connection) {
1281
+ return {
1282
+ id: `${connector.id}.${trigger2.id}`,
1283
+ title: trigger2.title,
1284
+ description: trigger2.description,
1285
+ category: connector.category,
1286
+ connectorId: connector.id,
1287
+ providerId: connector.providerId,
1288
+ triggerId: trigger2.id,
1289
+ scopes: trigger2.requiredScopes,
1290
+ dataClass: trigger2.dataClass,
1291
+ connected: Boolean(connection),
1292
+ connectionId: connection?.id
1293
+ };
1294
+ }
1295
+
1202
1296
  // src/events.ts
1203
1297
  var InMemoryIntegrationEventStore = class {
1204
1298
  events = /* @__PURE__ */ new Map();
@@ -1362,7 +1456,9 @@ var DefaultIntegrationActionGuard = class {
1362
1456
  }
1363
1457
  try {
1364
1458
  const result = await proceed();
1365
- await this.writeIdempotency(idempotencyKey, requestHash, result);
1459
+ if (result.ok) {
1460
+ await this.writeIdempotency(idempotencyKey, requestHash, result);
1461
+ }
1366
1462
  await this.audit?.record(createIntegrationAuditEvent({
1367
1463
  type: result.ok ? "action.invoked" : "action.failed",
1368
1464
  actor: ctx.connection.owner,
@@ -3400,7 +3496,8 @@ var IntegrationHub = class {
3400
3496
  assertScopes(connection, action.requiredScopes);
3401
3497
  assertScopes({ ...connection, grantedScopes: capability.scopes }, action.requiredScopes);
3402
3498
  const fullRequest = { ...request, connectionId: connection.id };
3403
- if (this.policy) {
3499
+ const proceed = async () => {
3500
+ if (!this.policy) return provider.invokeAction(connection, fullRequest);
3404
3501
  const decision = await this.policy.decide({
3405
3502
  connection,
3406
3503
  request: fullRequest,
@@ -3418,8 +3515,8 @@ var IntegrationHub = class {
3418
3515
  metadata: { policyDecision: decision.decision, reason: decision.reason, ...decision.metadata }
3419
3516
  };
3420
3517
  }
3421
- }
3422
- const proceed = () => Promise.resolve(provider.invokeAction(connection, fullRequest));
3518
+ return provider.invokeAction(connection, fullRequest);
3519
+ };
3423
3520
  if (this.guard) {
3424
3521
  return this.guard.invokeAction({ connection, request: fullRequest, action }, proceed);
3425
3522
  }
@@ -4370,6 +4467,7 @@ export {
4370
4467
  resolveConnectionCredentials,
4371
4468
  createCredentialBackedAdapterProvider,
4372
4469
  revokeConnection,
4470
+ discoverWorkspaceCapabilities,
4373
4471
  InMemoryIntegrationEventStore,
4374
4472
  receiveIntegrationWebhook,
4375
4473
  storedEventToTriggerEvent,
@@ -4428,4 +4526,4 @@ export {
4428
4526
  signCapability,
4429
4527
  verifyCapabilityToken
4430
4528
  };
4431
- //# sourceMappingURL=chunk-S54DPRDU.js.map
4529
+ //# sourceMappingURL=chunk-ALCIWTIR.js.map