@tangle-network/agent-integrations 0.33.0 → 0.33.2

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.
Files changed (65) hide show
  1. package/README.md +2 -7
  2. package/dist/catalog.d.ts +2 -2
  3. package/dist/catalog.js +9 -7
  4. package/dist/{chunk-7T5YTVER.js → chunk-53NQJZAT.js} +8 -318
  5. package/dist/chunk-53NQJZAT.js.map +1 -0
  6. package/dist/chunk-7P2LN4VT.js +318 -0
  7. package/dist/chunk-7P2LN4VT.js.map +1 -0
  8. package/dist/{chunk-XO2RSS6Y.js → chunk-C4CGT5JE.js} +52 -669
  9. package/dist/chunk-C4CGT5JE.js.map +1 -0
  10. package/dist/{chunk-YPZORI3G.js → chunk-CR35IEKW.js} +2 -2
  11. package/dist/{chunk-43VQSANC.js → chunk-D57YS6XC.js} +2 -2
  12. package/dist/{chunk-6N23S4JY.js → chunk-DT3H3NIW.js} +532 -5
  13. package/dist/chunk-DT3H3NIW.js.map +1 -0
  14. package/dist/{chunk-RF3RH374.js → chunk-O553GSCX.js} +2 -2
  15. package/dist/chunk-PZ5AY32C.js +10 -0
  16. package/dist/{chunk-NQ7OPDUM.js → chunk-ZDK7Y4QG.js} +12 -1
  17. package/dist/chunk-ZDK7Y4QG.js.map +1 -0
  18. package/dist/connect/index.d.ts +2 -2
  19. package/dist/connect/index.js +3 -2
  20. package/dist/connectors/adapters/index.d.ts +2 -2
  21. package/dist/connectors/adapters/index.js +3 -2
  22. package/dist/connectors/index.d.ts +2 -2
  23. package/dist/connectors/index.js +3 -2
  24. package/dist/{consumer-CzJgntej.d.ts → consumer-yV4NtH2h.d.ts} +1 -1
  25. package/dist/consumer.d.ts +3 -3
  26. package/dist/consumer.js +3 -2
  27. package/dist/{core-types-D5Dc65Ud.d.ts → core-types-CjWifQOf.d.ts} +2 -2
  28. package/dist/coverage-catalog.d.ts +26 -0
  29. package/dist/coverage-catalog.js +12 -0
  30. package/dist/coverage-catalog.js.map +1 -0
  31. package/dist/index.d.ts +74 -58
  32. package/dist/index.js +16 -47
  33. package/dist/middleware/index.d.ts +2 -2
  34. package/dist/middleware/index.js +3 -2
  35. package/dist/registry.d.ts +2 -2
  36. package/dist/registry.js +9 -7
  37. package/dist/runtime.d.ts +2 -2
  38. package/dist/runtime.js +9 -7
  39. package/dist/specs.d.ts +2 -2
  40. package/dist/specs.js +3 -1
  41. package/dist/stripe/index.js +1 -0
  42. package/dist/stripe/index.js.map +1 -1
  43. package/dist/{tangle-id-DA_qj-O_.d.ts → tangle-id-hDDWP-2f.d.ts} +1 -1
  44. package/dist/{types-XdpvaIzW.d.ts → types-Bxg-wJkW.d.ts} +12 -3
  45. package/dist/webhooks/index.js +1 -0
  46. package/dist/webhooks/index.js.map +1 -1
  47. package/docs/adapter-triage.md +9 -42
  48. package/docs/external-product-integration.md +3 -4
  49. package/docs/integration-execution-audit.md +134 -70
  50. package/docs/integration-execution-matrix.json +3062 -1823
  51. package/package.json +14 -25
  52. package/dist/bin/tangle-catalog-runtime.d.ts +0 -1
  53. package/dist/bin/tangle-catalog-runtime.js +0 -82
  54. package/dist/bin/tangle-catalog-runtime.js.map +0 -1
  55. package/dist/chunk-6N23S4JY.js.map +0 -1
  56. package/dist/chunk-7T5YTVER.js.map +0 -1
  57. package/dist/chunk-NQ7OPDUM.js.map +0 -1
  58. package/dist/chunk-XO2RSS6Y.js.map +0 -1
  59. package/dist/tangle-catalog-runtime-2HddXxoM.d.ts +0 -242
  60. package/dist/tangle-catalog-runtime.d.ts +0 -3
  61. package/dist/tangle-catalog-runtime.js +0 -32
  62. /package/dist/{chunk-YPZORI3G.js.map → chunk-CR35IEKW.js.map} +0 -0
  63. /package/dist/{chunk-43VQSANC.js.map → chunk-D57YS6XC.js.map} +0 -0
  64. /package/dist/{chunk-RF3RH374.js.map → chunk-O553GSCX.js.map} +0 -0
  65. /package/dist/{tangle-catalog-runtime.js.map → chunk-PZ5AY32C.js.map} +0 -0
@@ -1 +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":[]}
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":[]}
@@ -49,48 +49,15 @@ Aliases matter when comparing coverage:
49
49
  - `stripe` maps to `stripe-pack` for outbound payment actions.
50
50
  - `twilio` maps to `twilio-sms`.
51
51
 
52
- ## Tangle Catalog Execution Path
53
-
54
- The repo does not need hundreds of hand-written adapter files. Long-tail
55
- execution uses one strict catalog executor:
56
-
57
- ```ts
58
- import {
59
- createTangleCatalogExecutorProvider,
60
- createTangleCatalogHttpExecutor,
61
- IntegrationHub,
62
- } from '@tangle-network/agent-integrations'
63
-
64
- const provider = createTangleCatalogExecutorProvider({
65
- executeAction: createTangleCatalogHttpExecutor({
66
- endpoint: 'https://integrations.example.com/integration-runtime',
67
- secret: process.env.TANGLE_CATALOG_RUNTIME_SECRET,
68
- }),
69
- })
70
-
71
- const hub = new IntegrationHub({ providers: [provider], store, capabilitySecret })
72
- ```
73
-
74
- This routes normalized Tangle Integrations Catalog contracts through a signed
75
- runtime executor. Without that executor, the same contracts remain useful for
76
- connection discovery, planning, and setup UI, but they are not exposed as
77
- callable tools.
78
-
79
- The execution boundary remains the Tangle boundary:
80
-
81
- - `IntegrationHub` checks connection status, scopes, capability tokens, policy,
82
- approvals, and guard hooks.
83
- - The provider validates the connector and action before calling the executor.
84
- - The executor owns package loading, provider credentials, sandboxing, and
85
- upstream runtime quirks.
86
- - Trigger contracts and provider hooks are present; runtime services still need
87
- package-specific webhook or polling hosting for live trigger execution.
88
-
89
- Runtime images can prove their installed package coverage with:
90
-
91
- ```sh
92
- tangle-catalog-runtime --audit-packages
93
- ```
52
+ ## Direct Adapter Execution Path
53
+
54
+ Product execution goes through native `ConnectorAdapter` implementations. The
55
+ imported catalog is a coverage and backlog source, not an execution backend.
56
+
57
+ When a connector matters, add or improve a file in `src/connectors/adapters/`,
58
+ export it from `src/connectors/adapters/index.ts`, and cover the important
59
+ actions with focused adapter tests. Keep action ids stable when porting from
60
+ catalog metadata so agents and capability policies do not churn.
94
61
 
95
62
  ## Current Setup/Catalog Coverage
96
63
 
@@ -165,12 +165,11 @@ backend:
165
165
 
166
166
  - native adapter
167
167
  - hosted integration gateway
168
- - Tangle catalog runtime
169
168
  - product-specific provider
170
169
 
171
- Use `buildDefaultIntegrationRegistry({ tangleCatalogRuntimeExecutable: true })`
172
- only after the catalog runtime is deployed and audited with
173
- `auditTangleCatalogRuntimePackages()`.
170
+ Imported catalog entries are backlog only. Do not expose them as callable tools
171
+ until they are backed by a reviewed adapter or an intentionally owned hosted
172
+ provider.
174
173
 
175
174
  ## Security Requirements
176
175
 
@@ -7,7 +7,7 @@ This audit separates product contracts from implementation backends:
7
7
  - **Tangle contract**: the connector has a Tangle-owned action/trigger/auth contract.
8
8
  - **Setup-ready**: we have setup/auth/runbook metadata for product UI and admin configuration.
9
9
  - **Native adapter backend**: this repo ships a reviewed direct adapter implementation.
10
- - **Package runtime backend**: a Tangle runtime service executes the connector package behind the same Tangle contract.
10
+ - **Native adapter backlog**: a connector contract exists, but product-grade direct execution still needs a reviewed adapter.
11
11
 
12
12
  ## Summary
13
13
 
@@ -30,12 +30,12 @@ This audit separates product contracts from implementation backends:
30
30
  | Contracts with mapped actions | 669 |
31
31
  | Contracts with mapped triggers | 669 |
32
32
  | Contracts with mapped auth | 669 |
33
- | Native adapter backends | 10 |
34
- | Native adapter surfaces shipped | 17 |
35
- | Package-runtime backends | 659 |
36
- | Runtime manifest dependencies | 670 |
37
- | Tangle catalog connectors exposable behind runtime | 669 |
38
- | Tangle catalog actions exposable behind runtime | 3790 |
33
+ | Native adapter backends | 432 |
34
+ | Native adapter surfaces shipped | 466 |
35
+ | Package-runtime backends | 237 |
36
+ | Runtime manifest dependencies for catalog-only connectors | 238 |
37
+ | Catalog-only connectors exposable behind runtime | 237 |
38
+ | Catalog-only actions exposable behind runtime | 1081 |
39
39
 
40
40
  Full machine-readable matrix: [integration-execution-matrix.json](./integration-execution-matrix.json).
41
41
 
@@ -65,25 +65,91 @@ Full machine-readable matrix: [integration-execution-matrix.json](./integration-
65
65
 
66
66
  ## Native Adapter Backends
67
67
 
68
- These are direct in-repo implementations. They are not the only first-class contracts:
69
-
70
- - `google-calendar`
71
- - `google-sheets`
72
- - `microsoft-calendar`
73
- - `hubspot`
74
- - `slack`
75
- - `notion-database`
76
- - `twilio-sms`
77
- - `phony`
78
- - `stripe-pack`
79
- - `webhook`
80
- - `stripe`
81
- - `slack-inbound`
82
- - `github`
83
- - `gitlab`
68
+ These are direct in-repo implementations. They are not the only first-class contracts.
69
+ The full set is in the machine-readable matrix; representative native adapters:
70
+
71
+ - `activecampaign`
72
+ - `acumbamail`
73
+ - `adobe-creative-cloud`
74
+ - `afforai`
75
+ - `agentx`
76
+ - `aidbase`
77
+ - `aiprise`
78
+ - `air-ops`
79
+ - `aircall`
84
80
  - `airtable`
81
+ - `airtop`
82
+ - `alai`
83
+ - `alt-text-ai`
84
+ - `alttextify`
85
+ - `amazon-bedrock`
86
+ - `amazon-secrets-manager`
87
+ - `amazon-ses`
88
+ - `amazon-sns`
89
+ - `amazon-sqs`
90
+ - `amazon-textract`
91
+ - `aminos`
92
+ - `ampeco`
93
+ - `anthropic`
94
+ - `apitable`
95
+ - `apitemplate-io`
96
+ - `apollo`
97
+ - `appfollow`
85
98
  - `asana`
86
- - `salesforce`
99
+ - `ashby`
100
+ - `asknews`
101
+ - `assemblyai`
102
+ - `attio`
103
+ - `auth0`
104
+ - `autocalls`
105
+ - `avian`
106
+ - `avoma`
107
+ - `azure-ad`
108
+ - `azure-communication-services`
109
+ - `azure-openai`
110
+ - `backblaze`
111
+ - `bamboohr`
112
+ - `barcode-lookup`
113
+ - `baremetrics`
114
+ - `basecamp`
115
+ - `beamer`
116
+ - `bettermode`
117
+ - `bexio`
118
+ - `bigcommerce`
119
+ - `bigin-by-zoho`
120
+ - `billplz`
121
+ - `bitly`
122
+ - `bland-ai`
123
+ - `bluesky`
124
+ - `bolna`
125
+ - `bonjoro`
126
+ - `bookedin`
127
+ - `box`
128
+ - `brave-search`
129
+ - `braze`
130
+ - `brilliant-directories`
131
+ - `browse-ai`
132
+ - `cal-com`
133
+ - `calendly`
134
+ - `campaign-monitor`
135
+ - `canny`
136
+ - `canva`
137
+ - `capsule-crm`
138
+ - `captain-data`
139
+ - `cashfree-payments`
140
+ - `certopus`
141
+ - `chainalysis-api`
142
+ - `chargebee`
143
+ - `chargekeep`
144
+ - `chartly`
145
+ - `chat-data`
146
+ - `chatbase`
147
+ - `chatling`
148
+ - `chatnode`
149
+ - `chatwoot`
150
+ - `checkout`
151
+
152
+ ...and 386 more native adapter surfaces.
87
153
 
88
154
  Executable setup specs:
89
155
 
@@ -109,77 +175,77 @@ Executable setup specs:
109
175
  | --- | --- | --- |
110
176
  | Tangle first-class contracts | Done | 669 connectors have Tangle-owned action/trigger/auth/runtime contracts. |
111
177
  | Connector discovery/catalog search | Done | 669 catalog connectors, 3790 actions, 998 triggers normalized into Tangle catalog shapes. |
112
- | Native adapter execution | Done for listed native backends | 17 reviewed native adapter surfaces ship from this package; 10 overlap the 669 catalog contracts. |
178
+ | Native adapter execution | Done for listed native backends | 466 reviewed native adapter surfaces ship from this package; 432 overlap the 669 catalog contracts. |
113
179
  | OAuth/API-key setup metadata | Partial | 142 setup specs exist; 14 are executable setup specs and 128 are catalog/setup-only. |
114
- | Package-runtime action execution | Wiring done; runtime deployment/smoke pending | 659 contracts use package-runtime backends with package names and 3790 catalog upstream action names. |
115
- | Runtime dependency manifest | Done | `buildTangleCatalogRuntimePackageManifest()` emits 670 dependencies for a complete package-runtime worker install. |
116
- | Runtime package coverage audit | Done | `auditTangleCatalogRuntimePackages()` and `tangle-catalog-runtime --audit-packages` verify installed packages, piece exports, exact action mappings, and trigger surfaces in a deployed worker. |
180
+ | Direct adapter backlog | Tracked | 237 contracts still need native/direct adapters before they should be product-executable. |
181
+ | Legacy runtime dependency manifest | Deprecated | `buildTangleCatalogRuntimePackageManifest()` is retained only as an audit/provenance helper; products should not deploy a package runner for normal execution. |
182
+ | Runtime package coverage audit | Removed from launch path | Package-runner smoke is no longer a product launch gate; port demanded integrations to direct adapters instead. |
117
183
  | Long-tail credential mapping | Mostly mapped | 648 connectors have auth field metadata. 0 custom-auth connectors still need exact manual auth fields. |
118
184
  | Trigger provider flow | Done structurally | 998 triggers are cataloged, 998 have upstream names, and catalog providers can route subscribe/unsubscribe/normalize hooks. Runtime services still need package-specific trigger hosting. |
119
- | Sandbox/app invocation envelope | Done | The library has capability bundles, invocation envelopes, policy checks, guard hooks, signed catalog runtime HTTP calls, and generated-app client helpers. |
185
+ | Sandbox/app invocation envelope | Done | The library has capability bundles, invocation envelopes, policy checks, guard hooks, and generated-app client helpers. |
120
186
  | Live provider smoke tests | Not globally done | First-party adapters can be tested by consumers with credentials; long-tail smoke matrix is not generated yet. |
121
187
 
122
188
  ## Concrete Not-Done Buckets
123
189
 
124
190
  | Bucket | Count | What it means |
125
191
  | --- | ---: | --- |
126
- | Package-runtime contracts needing deployed runtime smoke verification | 659 | Connector has a Tangle contract and package backend; deployed runtime still needs package-load/live-smoke proof. |
192
+ | Contracts needing native/direct adapters | 237 | Connector has a Tangle contract but no reviewed direct adapter yet. |
127
193
  | Catalog connectors with zero upstream action names | 0 | These entries need catalog action-name mapping before exact package-runtime invocation can work. |
128
194
  | Custom-auth catalog connectors needing manual credential-field mapping | 0 | These are still custom auth and no field names were extracted from source. |
129
195
  | Catalog connectors with triggers needing runtime-service hosting | 288 | Trigger metadata and provider hooks exist; runtime services still need package-specific webhook/polling hosting. |
130
196
 
131
- Examples needing deployed runtime smoke verification:
197
+ Examples needing native/direct adapter ports:
132
198
 
133
- - `activecampaign` -> `@activepieces/piece-activecampaign`
134
199
  - `activepieces` -> `@activepieces/piece-activepieces`
135
200
  - `actualbudget` -> `@activepieces/piece-actualbudget`
136
201
  - `acuity-scheduling` -> `@activepieces/piece-acuity-scheduling`
137
- - `acumbamail` -> `@activepieces/piece-acumbamail`
138
- - `afforai` -> `@activepieces/piece-afforai`
139
- - `agentx` -> `@activepieces/piece-agentx`
140
202
  - `ai` -> `@activepieces/piece-ai`
141
203
  - `aianswer` -> `@activepieces/piece-aianswer`
142
- - `aidbase` -> `@activepieces/piece-aidbase`
143
- - `aiprise` -> `@activepieces/piece-aiprise`
144
- - `air-ops` -> `@activepieces/piece-air-ops`
145
- - `aircall` -> `@activepieces/piece-aircall`
146
204
  - `airparser` -> `@activepieces/piece-airparser`
147
- - `airtop` -> `@activepieces/piece-airtop`
148
- - `alai` -> `@activepieces/piece-alai`
149
205
  - `algolia` -> `@activepieces/piece-algolia`
150
- - `alt-text-ai` -> `@activepieces/piece-alt-text-ai`
151
- - `alttextify` -> `@activepieces/piece-alttextify`
152
- - `amazon-bedrock` -> `@activepieces/piece-amazon-bedrock`
153
206
  - `amazon-s3` -> `@activepieces/piece-amazon-s3`
154
- - `amazon-secrets-manager` -> `@activepieces/piece-amazon-secrets-manager`
155
- - `amazon-ses` -> `@activepieces/piece-amazon-ses`
156
- - `amazon-sns` -> `@activepieces/piece-amazon-sns`
157
- - `amazon-sqs` -> `@activepieces/piece-amazon-sqs`
158
- - `amazon-textract` -> `@activepieces/piece-amazon-textract`
159
- - `aminos` -> `@activepieces/piece-aminos`
160
- - `ampeco` -> `@activepieces/piece-ampeco`
161
207
  - `anyhook-graphql` -> `@activepieces/piece-anyhook-graphql`
162
208
  - `anyhook-websocket` -> `@activepieces/piece-anyhook-websocket`
163
209
  - `apify` -> `@activepieces/piece-apify`
164
- - `apitable` -> `@activepieces/piece-apitable`
165
- - `apitemplate-io` -> `@activepieces/piece-apitemplate-io`
166
- - `apollo` -> `@activepieces/piece-apollo`
167
- - `appfollow` -> `@activepieces/piece-appfollow`
168
- - `ashby` -> `@activepieces/piece-ashby`
169
210
  - `ask-handle` -> `@activepieces/piece-ask-handle`
170
- - `asknews` -> `@activepieces/piece-asknews`
171
211
  - `assembled` -> `@activepieces/piece-assembled`
172
- - `assemblyai` -> `@activepieces/piece-assemblyai`
212
+ - `azure-blob-storage` -> `@activepieces/piece-azure-blob-storage`
213
+ - `bannerbear` -> `@activepieces/piece-bannerbear`
214
+ - `base44` -> `@activepieces/piece-base44`
215
+ - `baserow` -> `@activepieces/piece-baserow`
216
+ - `beehiiv` -> `@activepieces/piece-beehiiv`
217
+ - `bika` -> `@activepieces/piece-bika`
218
+ - `binance` -> `@activepieces/piece-binance`
219
+ - `blockscout` -> `@activepieces/piece-blockscout`
220
+ - `bokio` -> `@activepieces/piece-bokio`
221
+ - `browserless` -> `@activepieces/piece-browserless`
222
+ - `bubble` -> `@activepieces/piece-bubble`
223
+ - `bumpups` -> `@activepieces/piece-bumpups`
224
+ - `bursty-ai` -> `@activepieces/piece-bursty-ai`
225
+ - `buttondown` -> `@activepieces/piece-buttondown`
226
+ - `call-rounded` -> `@activepieces/piece-rounded-studio`
227
+ - `camb-ai` -> `@activepieces/piece-camb-ai`
228
+ - `carbone` -> `@activepieces/piece-carbone`
229
+ - `cartloom` -> `@activepieces/piece-cartloom`
230
+ - `chain-aware` -> `@activepieces/piece-chain-aware`
231
+ - `chaindesk` -> `@activepieces/piece-chaindesk`
232
+ - `chat-aid` -> `@activepieces/piece-chat-aid`
233
+ - `chatfly` -> `@activepieces/piece-chatfly`
234
+ - `chatsistant` -> `@activepieces/piece-chatsistant`
235
+ - `chess-com` -> `@activepieces/piece-chess-com`
236
+ - `clarifai` -> `@activepieces/piece-clarifai`
237
+ - `claude` -> `@activepieces/piece-claude`
238
+ - `clearoutphone` -> `@activepieces/piece-clearoutphone`
173
239
 
174
240
  Manual custom auth mapping gap: none.
175
241
 
176
242
  ## Completion Claims And Remaining Proof Gates
177
243
 
178
244
  1. **Tangle first-class connector contracts are complete.**
179
- All 669 catalog entries have Tangle-owned contracts. 10 use native adapter backends; 659 use package-runtime backends.
245
+ All 669 catalog entries have Tangle-owned contracts. 432 use native adapter backends; 237 are backlog for native ports.
180
246
 
181
247
  2. **Action-name mapping exists for cataloged actions.**
182
- Done for cataloged actions: the catalog currently has 3790 actions and 3790 upstream action-name mappings in the checked-in catalog. The runtime executor uses those names automatically and still accepts explicit `actionAliases` for overrides. Deployed smoke verification proves those names against the installed packages.
248
+ Done for cataloged actions: the catalog currently has 3790 actions and 3790 upstream action-name mappings in the checked-in catalog. Direct adapters should preserve stable Tangle action ids when porting demanded backlog connectors.
183
249
 
184
250
  3. **Credential field mapping is complete for catalog auth setup.**
185
251
  Auth shapes are api_key: 519, oauth2: 118, none: 21, custom: 11. The catalog now includes auth field metadata for all 648 connectors that require credentials. 0 custom-auth connectors need manual auth-field mapping.
@@ -188,19 +254,17 @@ Manual custom auth mapping gap: none.
188
254
  There are 998 catalog triggers and 998 upstream trigger names. The provider flow supports trigger subscribe/unsubscribe/normalize hooks. Runtime services still need live webhook/polling smoke verification.
189
255
 
190
256
  5. **Native adapter coverage is intentionally smaller than contract breadth.**
191
- This repo ships 17 native adapter surfaces. 10 overlap the 669 catalog contracts; the other first-class contracts use package-runtime backends.
257
+ This repo ships 466 native adapter surfaces. 432 overlap the 669 catalog contracts; the remaining catalog contracts are not product-executable until ported.
192
258
 
193
259
  ## Concrete Launch Interpretation
194
260
 
195
261
  - It is accurate to say: **we have 669 first-class Tangle integration contracts.**
196
- - It is accurate to say: **all product code can use one IntegrationHub/tool contract across native and package-runtime backends.**
197
- - It is accurate to say: **deployed runtime smoke verification is the remaining proof step for package-runtime connectors.**
262
+ - It is accurate to say: **product execution should use direct/native adapters.**
263
+ - It is accurate to say: **the remaining 237 catalog-only contracts are backlog, not runtime-ready product surface.**
198
264
 
199
- ## Runtime Proof Gate
265
+ ## Native Port Gate
200
266
 
201
- Run `tangle-catalog-runtime --audit-packages` inside the deployed runtime image
202
- after installing the manifest from `--print-package-json` or
203
- `--print-pnpm-add`. That produces the concrete package-load/action-map/trigger
204
- surface matrix for the exact runtime image products will call. Live provider
205
- smoke tests still require real OAuth/API-key credentials from the product
206
- environment.
267
+ Port high-demand backlog connectors into `src/connectors/adapters/`, export
268
+ them from `src/connectors/adapters/index.ts`, add focused adapter tests, and
269
+ rerun this audit. Live provider smoke tests still require real OAuth/API-key
270
+ credentials from the product environment.