@tangle-network/agent-integrations 0.25.7 → 0.27.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 +6 -2
- package/dist/bin/tangle-catalog-runtime.js.map +1 -1
- package/dist/catalog.d.ts +4 -1
- package/dist/catalog.js +6 -2
- package/dist/chunk-2TW2QKGZ.js +94 -0
- package/dist/chunk-2TW2QKGZ.js.map +1 -0
- package/dist/chunk-ATYHZXLL.js +457 -0
- package/dist/chunk-ATYHZXLL.js.map +1 -0
- package/dist/{chunk-A5I3EYU5.js → chunk-ICSBYCE2.js} +122 -1
- package/dist/chunk-ICSBYCE2.js.map +1 -0
- package/dist/{chunk-WC63AI4Q.js → chunk-JU25UDN2.js} +1252 -225
- package/dist/chunk-JU25UDN2.js.map +1 -0
- package/dist/chunk-P24T3MLM.js +106 -0
- package/dist/chunk-P24T3MLM.js.map +1 -0
- package/dist/chunk-SVQ4PHDZ.js +129 -0
- package/dist/chunk-SVQ4PHDZ.js.map +1 -0
- package/dist/connect/index.d.ts +112 -0
- package/dist/connect/index.js +14 -0
- package/dist/connect/index.js.map +1 -0
- package/dist/connectors/adapters/index.d.ts +593 -1
- package/dist/connectors/adapters/index.js +22 -1
- package/dist/connectors/index.d.ts +2 -1
- package/dist/connectors/index.js +32 -10
- package/dist/index.d.ts +5 -2
- package/dist/index.js +57 -11
- package/dist/middleware/index.d.ts +137 -0
- package/dist/middleware/index.js +14 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/registry.d.ts +165 -2
- package/dist/registry.js +6 -2
- package/dist/runtime.d.ts +4 -1
- package/dist/runtime.js +6 -2
- package/dist/specs.d.ts +4 -1
- package/dist/tangle-catalog-runtime.d.ts +4 -1
- package/dist/tangle-catalog-runtime.js +6 -2
- package/dist/tangle-id-CTU4kGId.d.ts +553 -0
- 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 +25 -12
- package/dist/chunk-A5I3EYU5.js.map +0 -1
- package/dist/chunk-WC63AI4Q.js.map +0 -1
- package/dist/index-BQY5ry2s.d.ts +0 -808
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
|
-
-
|
|
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
|
-
-
|
|
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,14 @@ import {
|
|
|
4
4
|
buildTangleCatalogRuntimePackageManifest,
|
|
5
5
|
renderTangleCatalogRuntimePnpmAddCommand,
|
|
6
6
|
startTangleCatalogRuntimeNodeServer
|
|
7
|
-
} from "../chunk-
|
|
7
|
+
} from "../chunk-ICSBYCE2.js";
|
|
8
|
+
import "../chunk-SVQ4PHDZ.js";
|
|
8
9
|
import "../chunk-4JQ754PA.js";
|
|
9
10
|
import "../chunk-376UBTNB.js";
|
|
10
|
-
import "../chunk-
|
|
11
|
+
import "../chunk-JU25UDN2.js";
|
|
12
|
+
import "../chunk-2TW2QKGZ.js";
|
|
13
|
+
import "../chunk-P24T3MLM.js";
|
|
14
|
+
import "../chunk-ATYHZXLL.js";
|
|
11
15
|
|
|
12
16
|
// src/bin/tangle-catalog-runtime.ts
|
|
13
17
|
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":"
|
|
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,7 @@
|
|
|
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 './
|
|
2
|
+
import './tangle-id-CTU4kGId.js';
|
|
3
|
+
import './connect/index.js';
|
|
4
|
+
import './middleware/index.js';
|
|
3
5
|
import './connectors/index.js';
|
|
6
|
+
import './connectors/adapters/index.js';
|
|
4
7
|
import 'node:http';
|
package/dist/catalog.js
CHANGED
|
@@ -5,10 +5,14 @@ import {
|
|
|
5
5
|
parseIntegrationToolName,
|
|
6
6
|
searchIntegrationTools,
|
|
7
7
|
toMcpTools
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-ICSBYCE2.js";
|
|
9
|
+
import "./chunk-SVQ4PHDZ.js";
|
|
9
10
|
import "./chunk-4JQ754PA.js";
|
|
10
11
|
import "./chunk-376UBTNB.js";
|
|
11
|
-
import "./chunk-
|
|
12
|
+
import "./chunk-JU25UDN2.js";
|
|
13
|
+
import "./chunk-2TW2QKGZ.js";
|
|
14
|
+
import "./chunk-P24T3MLM.js";
|
|
15
|
+
import "./chunk-ATYHZXLL.js";
|
|
12
16
|
export {
|
|
13
17
|
buildIntegrationCatalogView,
|
|
14
18
|
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":[]}
|