@nightowlsdev/connectors 1.0.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.
@@ -0,0 +1,478 @@
1
+ import { z } from 'zod';
2
+ import { SwarmToolContext, SwarmContext, SwarmTool, AskField, SecretResolver } from '@nightowlsdev/core';
3
+
4
+ /** Which connection (provider account) an action acts with. */
5
+ interface ConnectionRef {
6
+ provider: string;
7
+ accountLabel?: string;
8
+ }
9
+ /** A backend-agnostic HTTP request a `BoundConnection` knows how to authorize + send. */
10
+ interface ProxyRequest {
11
+ method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
12
+ path: string;
13
+ query?: Record<string, unknown>;
14
+ body?: unknown;
15
+ headers?: Record<string, string>;
16
+ }
17
+ /**
18
+ * What an action's `execute` sees on `ctx.connection`: a live, tenant-scoped connection.
19
+ * `proxy()` ONLY — there is deliberately NO raw `token` field (connectors-architecture Codex #6):
20
+ * core has no redaction layer on tool outputs, so exposing a token would invite a custom `execute`
21
+ * returning it straight to the model.
22
+ */
23
+ interface BoundConnection {
24
+ accountId: string;
25
+ proxy(req: ProxyRequest): Promise<unknown>;
26
+ }
27
+ /** The context a custom action `execute` receives — the tool context plus the resolved connection. */
28
+ interface ConnectorActionContext extends SwarmToolContext {
29
+ connection: BoundConnection;
30
+ }
31
+ /** One agent-callable action of a connector. Provide EXACTLY ONE of `op` (declarative HTTP) or `execute` (custom). */
32
+ interface ConnectorActionSpec<I = unknown> {
33
+ name: string;
34
+ description?: string;
35
+ inputSchema: z.ZodType<I>;
36
+ /**
37
+ * The connector DSL OWNS this default (connectors-architecture Codex #10): `defineTool` only defaults
38
+ * `needsApproval` for `origin:"mcp"`. Set it explicitly per action — `true` (side-effecting) by default.
39
+ */
40
+ needsApproval?: boolean;
41
+ /** Declarative HTTP op — the materialized tool calls `connection.proxy({ method, path, body: input })`. */
42
+ op?: {
43
+ method: ProxyRequest["method"];
44
+ path: string;
45
+ };
46
+ /** Custom execute — receives `ctx.connection`; its result is fenced by the backend. */
47
+ execute?: (input: I, ctx: ConnectorActionContext) => Promise<unknown>;
48
+ }
49
+ /** A subscribable event (used by the trigger intake — declared here, consumed in a later slice). */
50
+ interface ConnectorEventSpec {
51
+ type: string;
52
+ normalize?: (raw: unknown) => unknown;
53
+ }
54
+ /** One declared unit bundling a single external service's actions + events + connection auth metadata. */
55
+ interface ConnectorDef {
56
+ provider: string;
57
+ connection?: {
58
+ kind: "oauth2" | "apikey" | "static";
59
+ scopes?: string[];
60
+ };
61
+ actions: ConnectorActionSpec[];
62
+ events?: ConnectorEventSpec[];
63
+ }
64
+ /**
65
+ * A materialized connector action: a `SwarmTool` (so it can be granted to agents + inherits the SP5 gate),
66
+ * directly executable for delivery/tests. Its output is ALWAYS fenced (the per-backend contract, Codex #5).
67
+ */
68
+ interface ConnectorTool extends SwarmTool {
69
+ execute(input: unknown, ctx: SwarmToolContext): Promise<{
70
+ fenced: string;
71
+ }>;
72
+ }
73
+ /** A pluggable backend (static/env, remote-MCP, or Nango) the connector seam swaps without agent-code change. */
74
+ interface ConnectorBackend {
75
+ /** Materialize a connector's actions into SwarmTools (async/lazy, like `McpConnector.listTools`). */
76
+ materialize(def: ConnectorDef, ctx: SwarmContext): Promise<SwarmTool[]>;
77
+ /** Resolve a live, refreshed, tenant-scoped connection at call time. */
78
+ resolveConnection(ref: ConnectionRef, ctx: SwarmContext): Promise<BoundConnection>;
79
+ dispose?(): Promise<void>;
80
+ }
81
+
82
+ /**
83
+ * Declare a connector (plain validated data — engine-wall-clean, no Mastra types). Validation is compile-cheap and pure:
84
+ * a provider is required, action names are unique, and each action declares EXACTLY ONE of `op`/`execute`.
85
+ */
86
+ declare function defineConnector(spec: ConnectorDef): ConnectorDef;
87
+
88
+ /** Fence untrusted connector (external-service) output before it is returned to the model. */
89
+ declare function fenceConnectorOutput(raw: unknown): {
90
+ fenced: string;
91
+ };
92
+
93
+ /**
94
+ * Adapt a set of connectors + a backend into the per-request resolver core's `defineSwarm({ connectorTools })`
95
+ * expects. Core stays connector-agnostic — it only ever sees `(ctx) => Promise<SwarmTool[]>` — so the dependency
96
+ * arrow is one-way (`@nightowlsdev/connectors → @nightowlsdev/core`), never circular.
97
+ *
98
+ * Fail-fast by design: a duplicate action name across connectors throws at construction (a name collision would
99
+ * otherwise silently last-wins in the agent's by-name tool map and route the model's call to the WRONG
100
+ * connection); a `backend.materialize` failure rejects the whole request rather than silently under-granting
101
+ * tools (a materialize failure is a real config/connection problem worth surfacing, not hiding).
102
+ */
103
+ declare function materializeConnectors(connectors: ConnectorDef[], backend: ConnectorBackend): (ctx: SwarmContext) => Promise<SwarmTool[]>;
104
+
105
+ /**
106
+ * First-party Slack connector (Web API via `staticBackend`). Credentials (`SLACK_BOT_TOKEN`) + the base URL come
107
+ * from the backend connection at request time; this def only declares the actions + their approval defaults.
108
+ *
109
+ * Approval defaults are OWNED here (parent Codex #10): `post_message` side-effects ⇒ gated; `search` is read-only.
110
+ */
111
+ declare const slackConnector: ConnectorDef;
112
+
113
+ /** First-party Linear connector (action + trigger). The bug-report example (gap-map) becomes real with this. */
114
+ declare const linearConnector: ConnectorDef;
115
+
116
+ /**
117
+ * Out-of-band HITL delivery (spec §5): ship a suspended run's `swarm.question` to a channel and return a
118
+ * correlation ref. Engine-wall clean, no DB — the backend (proxy) is injected. Delivery is gated on CONTENT
119
+ * (§5.5): the model's `prompt` is DATA inside a fixed template, rendered INERT (Slack Block Kit `plain_text`,
120
+ * which parses no mrkdwn/links/mentions), and may go ONLY to the run's route-allowlisted target.
121
+ */
122
+ /** A route is resolved server-side (from thread/subscription config), NEVER from a model-named address (§5.5). */
123
+ type DeliveryRoute = {
124
+ channel: "slack";
125
+ target: {
126
+ teamId: string;
127
+ channelId: string;
128
+ };
129
+ } | {
130
+ channel: "email";
131
+ target: {
132
+ to: string;
133
+ };
134
+ };
135
+ /** The post-egress correlation handle (the message id/ts exists only after the send). */
136
+ interface DeliveryRef {
137
+ channel: string;
138
+ ref: Record<string, unknown>;
139
+ }
140
+ interface QuestionToDeliver {
141
+ followupId: string;
142
+ toolCallId: string;
143
+ prompt: string;
144
+ field?: AskField;
145
+ to: string;
146
+ }
147
+ interface QuestionDelivery {
148
+ deliver(q: QuestionToDeliver, ctx: SwarmContext, route: DeliveryRoute): Promise<DeliveryRef>;
149
+ }
150
+ /** Neutralize Slack control sequences for the case a `mrkdwn` block is unavoidable (§5.5): escape `&<>` so
151
+ * `<!channel>` / `<@U…>` / links can't be parsed as mentions/markup. `plain_text` blocks need no escaping. */
152
+ declare function escapeSlackText(s: string): string;
153
+ /** Render the ask as INERT Block Kit: the model's prompt sits in a `plain_text` block (no mrkdwn/mention parsing).
154
+ * The `text` fallback is a FIXED agent-authored string — never the model prompt — so the notification preview
155
+ * (which DOES parse mrkdwn) can't be used to inject. */
156
+ declare function renderSlackQuestion(prompt: string, _field?: AskField): {
157
+ blocks: unknown[];
158
+ text: string;
159
+ };
160
+ /** Slack `QuestionDelivery` over a connector backend's proxy (`chat.postMessage`). Posts to the route's
161
+ * allowlisted channel ONLY; returns `{team_id, channel_id, delivery_ts}` where `delivery_ts` is the new message
162
+ * ts (each ask is its own top-level message, so the reply's `thread_ts` equals it — exact correlation, §5.3). */
163
+ declare function slackQuestionDelivery(opts: {
164
+ backend: ConnectorBackend;
165
+ provider?: string;
166
+ }): QuestionDelivery;
167
+
168
+ /** First-party Email connector. `from` is deployment config (a verified sender), NOT model-controlled. The
169
+ * `email.send` action is **text/plain only** (no `html` — no markup/injection surface) and SP5-gated, so a human
170
+ * approves the recipient + body before any send. */
171
+ declare function emailConnector(opts: {
172
+ from: string;
173
+ }): ConnectorDef;
174
+ /**
175
+ * Mint a tamper-proof reply token for a followup: `b64url({f,e?}).b64url(HMAC(payload))`, embedded in the
176
+ * delivery's `reply-to` (e.g. `reply+<token>@domain`) so an email reply correlates back to the followup without
177
+ * trusting any sender-controlled header.
178
+ *
179
+ * **TTL (Blocks #1):** pass `expiresInSec` to embed a signed expiry — a forwarded/leaked email's token then stops
180
+ * correlating after the window. (Independently, the followup record is itself a time-bound: `findSuspended` only
181
+ * returns LIVE suspended runs, so a token to an answered/dead followup correlates to nothing.)
182
+ *
183
+ * **Responder binding (Blocks #2):** the token authenticates the `followupId` ONLY — it does NOT authorize a
184
+ * sender. The K4 inbound pipeline's `authorizeReplier` independently checks the provider-VERIFIED replier against
185
+ * the ask's approver, so a forwarded email from a non-approver fails authz even with a valid token.
186
+ */
187
+ declare function mintReplyToken(followupId: string, secret: string, opts?: {
188
+ expiresInSec?: number;
189
+ nowSec?: number;
190
+ }): Promise<string>;
191
+ /** Verify a reply token → the `followupId`, or null if malformed/tampered/expired (constant-time HMAC compare
192
+ * BEFORE parsing the payload). */
193
+ declare function verifyReplyToken(token: string, secret: string, opts?: {
194
+ nowSec?: number;
195
+ }): Promise<string | null>;
196
+ /** Extract the reply token from a `local+<token>@domain` inbound address. Null when there's no `+token` part. */
197
+ declare function parseReplyToken(address: string): string | null;
198
+ /** Verify an inbound-email provider webhook signature (HMAC-SHA256 over the raw body, hex). The reply's identity
199
+ * comes from the provider's AUTHENTICATED result (the signed payload's verified `from` + SPF/DKIM verdict) —
200
+ * never a raw `From` header. */
201
+ declare function verifyInboundEmailSignature(input: {
202
+ payload: string;
203
+ signature: string | null | undefined;
204
+ secret: string;
205
+ }): Promise<boolean>;
206
+ /** Deliver a suspended run's ask over email (text/plain) with a signed reply-token in `reply-to`, so the human's
207
+ * reply resumes the run via the K4 inbound pipeline. Goes ONLY to the route-allowlisted recipient (§5.5). */
208
+ declare function emailQuestionDelivery(opts: {
209
+ backend: ConnectorBackend;
210
+ from: string;
211
+ /** Build the reply-to address carrying the token, e.g. `(t) => `reply+${t}@owls.example``. */
212
+ replyAddress: (token: string) => string;
213
+ tokenSecret: string;
214
+ /** Reply-token TTL (seconds). Default 7 days — a leaked/forwarded email stops correlating after the window. */
215
+ tokenTtlSec?: number;
216
+ provider?: string;
217
+ }): QuestionDelivery;
218
+
219
+ /** A statically-configured connection (creds from env/config — the zero-infra dev/test backend). */
220
+ interface StaticConnection {
221
+ baseUrl: string;
222
+ /** A literal token (dev), OR a `secretRef` resolved via the `SecretResolver` at execute time. */
223
+ token?: string;
224
+ secretRef?: string;
225
+ accountId?: string;
226
+ /** Override the auth header (default: `{ Authorization: "Bearer <token>" }`). */
227
+ authHeader?: (token: string | undefined) => Record<string, string>;
228
+ }
229
+ interface StaticBackendOpts {
230
+ /** Connections keyed by provider. */
231
+ connections: Record<string, StaticConnection>;
232
+ secrets?: SecretResolver;
233
+ /** Injectable `fetch` (default: global `fetch`) — for tests/integration. */
234
+ fetchImpl?: typeof fetch;
235
+ }
236
+ /**
237
+ * The zero-infra reference backend: resolves credentials from env/config (no OAuth, no `owl_connections`),
238
+ * materializes each action into a fenced `SwarmTool`, and authorizes HTTP via a bearer-token proxy.
239
+ */
240
+ declare function staticBackend(opts: StaticBackendOpts): ConnectorBackend;
241
+
242
+ /** A single Nango proxy request — the host adapts the Nango SDK's `proxy()` to this shape. Returns the parsed
243
+ * response body; throws on a non-2xx (the host adapter owns HTTP-status handling, like a `fetch` wrapper). */
244
+ interface NangoProxyRequest {
245
+ connectionId: string;
246
+ providerConfigKey?: string;
247
+ /** Reuse the strict `ProxyRequest` union so a typo (e.g. `"POSTT"`) can't compile — the backend only ever
248
+ * forwards a connector op's method, which is already that union. */
249
+ method: ProxyRequest["method"];
250
+ path: string;
251
+ query?: Record<string, unknown>;
252
+ body?: unknown;
253
+ headers?: Record<string, string>;
254
+ }
255
+ /** The injected Nango client surface `nangoBackend` needs — Nango injects + refreshes the OAuth token internally,
256
+ * so no raw token ever reaches this code (architecture §5: "in the Nango path we store no raw tokens"). */
257
+ interface NangoProxy {
258
+ proxy(req: NangoProxyRequest): Promise<unknown>;
259
+ }
260
+ /** The resolved tenant connection: the Nango `connectionId` (token lives in Nango) + display metadata. */
261
+ interface ResolvedNangoConnection {
262
+ connectionId: string;
263
+ accountId?: string;
264
+ providerConfigKey?: string;
265
+ }
266
+ /** Tenant-scoped connection lookup — the host backs this with `owl_connections WHERE org_id = ctx.tenantId AND
267
+ * provider = …`. Returns `null` when the tenant hasn't connected the provider (⇒ the tool fails loud → re-auth). */
268
+ type NangoConnectionLookup = (ref: ConnectionRef, ctx: SwarmContext) => Promise<ResolvedNangoConnection | null>;
269
+ interface NangoBackendOpts {
270
+ nango: NangoProxy;
271
+ resolveConnection: NangoConnectionLookup;
272
+ }
273
+ /**
274
+ * The platform OAuth backend: resolves a tenant's connection at CALL time (so a resumed run gets current creds —
275
+ * architecture §5) and proxies authorized HTTP through Nango. Vendor-agnostic + hermetically testable: the Nango
276
+ * client and the `owl_connections`-backed lookup are injected (mirrors `staticBackend`'s injected `fetchImpl`).
277
+ */
278
+ declare function nangoBackend(opts: NangoBackendOpts): ConnectorBackend;
279
+
280
+ /**
281
+ * Trigger intake (architecture §4.2 / parent §6): a provider webhook → verify → dedupe → normalize → enqueue a
282
+ * run per subscription. Built in `@nightowlsdev/connectors` (engine-wall clean, no DB) — every side dependency is
283
+ * INJECTED, so the host wires the real dedupe store / subscription lookup / durable runner while this stays
284
+ * hermetically testable. Identity is SERVER-SIDE (from the subscription), never from the event body (Codex #2/#3);
285
+ * the raw event payload is FENCED untrusted in the run input (Codex #5).
286
+ */
287
+ /** Verify a Slack request signature. Slack signs `v0:${timestamp}:${rawBody}` with HMAC-SHA256 keyed by the
288
+ * signing secret, hex, as `v0=…` in `X-Slack-Signature`; the timestamp must be fresh (replay guard). */
289
+ declare function verifySlackSignature(input: {
290
+ signingSecret: string;
291
+ signature: string | null | undefined;
292
+ timestamp: string | null | undefined;
293
+ body: string;
294
+ nowSec?: number;
295
+ maxSkewSec?: number;
296
+ }): Promise<{
297
+ ok: true;
298
+ } | {
299
+ ok: false;
300
+ reason: string;
301
+ }>;
302
+ /** Verify a Linear webhook signature: HMAC-SHA256 of the RAW body keyed by the webhook signing secret, hex, in
303
+ * the `linear-signature` header (no prefix). Web Crypto + constant-time compare, like the Slack verifier. */
304
+ declare function verifyLinearSignature(input: {
305
+ secret: string;
306
+ signature: string | null | undefined;
307
+ body: string;
308
+ }): Promise<{
309
+ ok: true;
310
+ } | {
311
+ ok: false;
312
+ reason: string;
313
+ }>;
314
+ /** A code-defined (v1) subscription: which agent, owned by which user, in which org/thread, fires on an event. */
315
+ interface TriggerSubscription {
316
+ orgId: string;
317
+ ownerUserId: string;
318
+ agentSlug: string;
319
+ threadId: string;
320
+ workflow?: string;
321
+ }
322
+ /** The durable runner seam (a subset of the core runner's `enqueue`) — injected so this stays @mastra-free. */
323
+ type TriggerEnqueue = (input: {
324
+ message: string;
325
+ context?: Record<string, unknown>;
326
+ workflow?: string;
327
+ }, ctx: {
328
+ tenantId: string;
329
+ userId: string;
330
+ agentSlug: string;
331
+ threadId: string;
332
+ runId: string;
333
+ }) => Promise<{
334
+ runId: string;
335
+ }>;
336
+ /** Idempotency store (Codex #8). `markSeen` returns true if the key is NEW (first time), false if already seen. */
337
+ interface TriggerDedupe {
338
+ markSeen(key: {
339
+ provider: string;
340
+ externalId: string;
341
+ }): Promise<boolean>;
342
+ }
343
+ interface HandleTriggerOpts {
344
+ connector: ConnectorDef;
345
+ /** The Slack signing secret — required UNLESS `skipSignatureCheck` (the caller already verified, e.g. Nango). */
346
+ signingSecret?: string;
347
+ signature?: string | null | undefined;
348
+ timestamp?: string | null | undefined;
349
+ /**
350
+ * Skip the provider HMAC because the request was already authenticated by a trusted forwarder (Nango verifies
351
+ * the provider signature before forwarding with its own Nango signature). The caller MUST have verified the
352
+ * forwarder's signature first. (The url_verification handler still runs but is never reached via a forwarded
353
+ * event — Nango answers Slack's challenge itself during webhook setup.)
354
+ */
355
+ skipSignatureCheck?: boolean;
356
+ dedupe: TriggerDedupe;
357
+ lookupSubscriptions: (input: {
358
+ provider: string;
359
+ workspaceId?: string;
360
+ eventType: string;
361
+ }) => Promise<TriggerSubscription[]>;
362
+ enqueue: TriggerEnqueue;
363
+ mintRunId: () => string;
364
+ nowSec?: () => number;
365
+ maxSkewSec?: number;
366
+ }
367
+ type HandleTriggerResult = {
368
+ status: "challenge";
369
+ challenge: string;
370
+ } | {
371
+ status: "rejected";
372
+ reason: string;
373
+ } | {
374
+ status: "ignored";
375
+ reason: string;
376
+ } | {
377
+ status: "enqueued";
378
+ runs: Array<{
379
+ runId: string;
380
+ agentSlug: string;
381
+ }>;
382
+ failed: number;
383
+ };
384
+ declare function handleTriggerEvent(provider: string, rawPayload: string, opts: HandleTriggerOpts): Promise<HandleTriggerResult>;
385
+
386
+ /**
387
+ * Inbound reply → resume (spec §5.4): correlate an untrusted channel reply back to its `followupId` and resume
388
+ * the suspended run — EXACTLY ONCE, treating the reply as untrusted. Engine-wall clean; every side effect
389
+ * (transport dedupe, correlation, authz, the answer-once CAS, resume) is injected, so this stays hermetic.
390
+ */
391
+ /** Strip quoted text / signatures and cap size — the reply is UNTRUSTED before it becomes `answer` (§5.4 step 4). */
392
+ declare function sanitizeReply(raw: string, opts?: {
393
+ maxChars?: number;
394
+ }): string;
395
+ /** Pull the exact Slack correlation keys from a verified inbound message event: the reply's `thread_ts` equals
396
+ * the ask's `delivery_ts` (each ask is its own top-level message — one reply ↔ one ask, §5.3). */
397
+ declare function correlateSlackInbound(event: unknown): {
398
+ teamId: string;
399
+ channelId: string;
400
+ threadTs: string;
401
+ text: string;
402
+ userId: string;
403
+ } | null;
404
+ interface CorrelatedFollowup {
405
+ tenantId: string;
406
+ followupId: string;
407
+ runId: string;
408
+ toolCallId: string;
409
+ approverUserId: string;
410
+ }
411
+ interface InboundReplyInput {
412
+ provider: string;
413
+ externalId: string;
414
+ text: string;
415
+ /** Provider-VERIFIED replier identity (slack member id within team / verified email) — never raw `From`. */
416
+ replier: {
417
+ externalId: string;
418
+ teamId?: string;
419
+ };
420
+ /** Exact correlation keys (provider-specific), passed to `correlate`. */
421
+ correlation: Record<string, unknown>;
422
+ }
423
+ interface ResumeCtx {
424
+ tenantId: string;
425
+ userId: string;
426
+ agentSlug: string;
427
+ threadId: string;
428
+ runId: string;
429
+ }
430
+ interface HandleInboundOpts {
431
+ /** Atomic insert-or-false on (provider, external_id): true ⇒ NEW (proceed); false ⇒ retried event (ignore). */
432
+ recordInboundOnce: (key: {
433
+ provider: string;
434
+ externalId: string;
435
+ }) => Promise<boolean>;
436
+ /** Resolve the inbound reply to its pending followup via the exact delivery keys. Null ⇒ no match (ignore). */
437
+ correlate: (input: InboundReplyInput) => Promise<CorrelatedFollowup | null>;
438
+ /** Map the VERIFIED replier identity → an internal user (user_channel_identity, verified_at set). Null ⇒ deny. */
439
+ authorizeReplier: (input: {
440
+ tenantId: string;
441
+ provider: string;
442
+ replier: {
443
+ externalId: string;
444
+ teamId?: string;
445
+ };
446
+ }) => Promise<{
447
+ userId: string;
448
+ } | null>;
449
+ /** The answer-once CAS (storage.markFollowupAnswered): true ⇒ THIS call won (resume); false ⇒ already answered. */
450
+ answerOnce: (followupId: string, tenantId: string) => Promise<boolean>;
451
+ /** Reconstruct the run's SwarmContext from its row (as the resume route does). */
452
+ resolveContext: (corr: CorrelatedFollowup) => Promise<ResumeCtx>;
453
+ /** Durable resume — returns {runId}, never streams (a webhook can't drain a stream). */
454
+ resumeEnqueue: (args: {
455
+ runId: string;
456
+ toolCallId: string;
457
+ followupId: string;
458
+ answer: unknown;
459
+ }, ctx: ResumeCtx) => Promise<{
460
+ runId: string;
461
+ }>;
462
+ sanitize?: (raw: string) => string;
463
+ }
464
+ type HandleInboundResult = {
465
+ status: "resumed";
466
+ runId: string;
467
+ } | {
468
+ status: "ignored";
469
+ reason: string;
470
+ };
471
+ /**
472
+ * The §5.4 pipeline: transport-dedupe → correlate → authz → sanitize → answer-once CAS → durable resume. NEVER
473
+ * resumes twice: two DISTINCT inbound events for one followup both pass dedupe + correlate, but only the CAS
474
+ * winner resumes (the loser is ignored). Returns a result for every path — the host ACKs 200 on `ignored`.
475
+ */
476
+ declare function handleInboundReply(input: InboundReplyInput, opts: HandleInboundOpts): Promise<HandleInboundResult>;
477
+
478
+ export { type BoundConnection, type ConnectionRef, type ConnectorActionContext, type ConnectorActionSpec, type ConnectorBackend, type ConnectorDef, type ConnectorEventSpec, type ConnectorTool, type CorrelatedFollowup, type DeliveryRef, type DeliveryRoute, type HandleInboundOpts, type HandleInboundResult, type HandleTriggerOpts, type HandleTriggerResult, type InboundReplyInput, type NangoBackendOpts, type NangoConnectionLookup, type NangoProxy, type NangoProxyRequest, type ProxyRequest, type QuestionDelivery, type QuestionToDeliver, type ResolvedNangoConnection, type ResumeCtx, type StaticBackendOpts, type StaticConnection, type TriggerDedupe, type TriggerEnqueue, type TriggerSubscription, correlateSlackInbound, defineConnector, emailConnector, emailQuestionDelivery, escapeSlackText, fenceConnectorOutput, handleInboundReply, handleTriggerEvent, linearConnector, materializeConnectors, mintReplyToken, nangoBackend, parseReplyToken, renderSlackQuestion, sanitizeReply, slackConnector, slackQuestionDelivery, staticBackend, verifyInboundEmailSignature, verifyLinearSignature, verifyReplyToken, verifySlackSignature };