@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.
package/dist/index.js ADDED
@@ -0,0 +1,647 @@
1
+ // src/define.ts
2
+ function defineConnector(spec) {
3
+ if (!spec.provider) throw new Error("defineConnector: provider is required");
4
+ const names = /* @__PURE__ */ new Set();
5
+ for (const a of spec.actions) {
6
+ if (names.has(a.name)) throw new Error(`defineConnector: duplicate action name '${a.name}'`);
7
+ names.add(a.name);
8
+ if (!a.op === !a.execute) throw new Error(`defineConnector: action '${a.name}' needs exactly one of { op, execute }`);
9
+ }
10
+ return spec;
11
+ }
12
+
13
+ // src/fence.ts
14
+ function fenceConnectorOutput(raw) {
15
+ let text;
16
+ if (typeof raw === "string") {
17
+ text = raw;
18
+ } else {
19
+ try {
20
+ text = JSON.stringify(raw) ?? String(raw);
21
+ } catch {
22
+ text = String(raw);
23
+ }
24
+ }
25
+ return { fenced: `<connector-output untrusted="true">
26
+ ${text}
27
+ </connector-output>` };
28
+ }
29
+
30
+ // src/materialize.ts
31
+ function materializeConnectors(connectors, backend) {
32
+ const seen = /* @__PURE__ */ new Set();
33
+ for (const def of connectors) {
34
+ for (const action of def.actions) {
35
+ if (seen.has(action.name)) {
36
+ throw new Error(
37
+ `materializeConnectors: duplicate connector action name '${action.name}' across connectors \u2014 action names must be unique (a collision would silently shadow another connector's action and route calls to the wrong connection)`
38
+ );
39
+ }
40
+ seen.add(action.name);
41
+ }
42
+ }
43
+ return async (ctx) => {
44
+ const all = [];
45
+ for (const def of connectors) {
46
+ try {
47
+ all.push(...await backend.materialize(def, ctx));
48
+ } catch (err) {
49
+ throw new Error(
50
+ `materializeConnectors: backend.materialize failed for provider '${def.provider}': ${err instanceof Error ? err.message : String(err)}`,
51
+ { cause: err }
52
+ );
53
+ }
54
+ }
55
+ return all;
56
+ };
57
+ }
58
+
59
+ // src/connectors/slack.ts
60
+ import { z } from "zod";
61
+ var slackConnector = defineConnector({
62
+ provider: "slack",
63
+ actions: [
64
+ {
65
+ name: "slack.post_message",
66
+ description: "Post a message to a Slack channel or thread.",
67
+ inputSchema: z.object({ channel: z.string(), text: z.string(), thread_ts: z.string().optional() }),
68
+ needsApproval: true,
69
+ // side-effecting: posts to a channel — must be SP5-gated
70
+ op: { method: "POST", path: "/chat.postMessage" }
71
+ },
72
+ {
73
+ name: "slack.search",
74
+ description: "Search Slack messages.",
75
+ inputSchema: z.object({ query: z.string(), count: z.number().optional() }),
76
+ needsApproval: false,
77
+ // read-only
78
+ op: { method: "GET", path: "/search.messages" }
79
+ }
80
+ ],
81
+ // Trigger events (K3): a Slack Events API `message` fires a run. `normalize` receives the full outer payload
82
+ // (event_callback) and distills it to a `summary` (the run prompt) + the structured fields.
83
+ events: [
84
+ {
85
+ type: "message",
86
+ normalize: (raw) => {
87
+ const outer = raw ?? {};
88
+ const ev = outer.event ?? {};
89
+ const s = (v) => typeof v === "string" ? v : "";
90
+ const channel = s(ev.channel);
91
+ const user = s(ev.user);
92
+ const text = s(ev.text);
93
+ return {
94
+ summary: `New Slack message from ${user || "someone"} in ${channel || "a channel"}: ${text}`,
95
+ channel,
96
+ user,
97
+ text,
98
+ ts: s(ev.ts)
99
+ };
100
+ }
101
+ }
102
+ ]
103
+ });
104
+
105
+ // src/connectors/linear.ts
106
+ import { z as z2 } from "zod";
107
+ var CREATE_ISSUE = (
108
+ /* GraphQL */
109
+ `
110
+ mutation CreateIssue($input: IssueCreateInput!) {
111
+ issueCreate(input: $input) { success issue { id identifier url title } }
112
+ }`
113
+ );
114
+ var ADD_COMMENT = (
115
+ /* GraphQL */
116
+ `
117
+ mutation AddComment($input: CommentCreateInput!) {
118
+ commentCreate(input: $input) { success comment { id url } }
119
+ }`
120
+ );
121
+ var SEARCH_ISSUES = (
122
+ /* GraphQL */
123
+ `
124
+ query SearchIssues($term: String!, $first: Int) {
125
+ issueSearch(query: $term, first: $first) { nodes { id identifier title url state { name } } }
126
+ }`
127
+ );
128
+ var str = (v) => typeof v === "string" ? v : "";
129
+ var linearConnector = defineConnector({
130
+ provider: "linear",
131
+ actions: [
132
+ {
133
+ name: "linear.create_issue",
134
+ description: "Create a Linear issue.",
135
+ inputSchema: z2.object({ teamId: z2.string(), title: z2.string(), description: z2.string().optional() }),
136
+ needsApproval: true,
137
+ // side-effecting
138
+ execute: async (input, ctx) => {
139
+ const i = input;
140
+ return ctx.connection.proxy({
141
+ method: "POST",
142
+ path: "/graphql",
143
+ body: { query: CREATE_ISSUE, variables: { input: { teamId: i.teamId, title: i.title, description: i.description } } }
144
+ });
145
+ }
146
+ },
147
+ {
148
+ name: "linear.add_comment",
149
+ description: "Add a comment to a Linear issue.",
150
+ inputSchema: z2.object({ issueId: z2.string(), body: z2.string() }),
151
+ needsApproval: true,
152
+ // side-effecting
153
+ execute: async (input, ctx) => {
154
+ const i = input;
155
+ return ctx.connection.proxy({
156
+ method: "POST",
157
+ path: "/graphql",
158
+ body: { query: ADD_COMMENT, variables: { input: { issueId: i.issueId, body: i.body } } }
159
+ });
160
+ }
161
+ },
162
+ {
163
+ name: "linear.search_issues",
164
+ description: "Search Linear issues.",
165
+ inputSchema: z2.object({ term: z2.string(), first: z2.number().optional() }),
166
+ needsApproval: false,
167
+ // read-only
168
+ execute: async (input, ctx) => {
169
+ const i = input;
170
+ return ctx.connection.proxy({
171
+ method: "POST",
172
+ path: "/graphql",
173
+ body: { query: SEARCH_ISSUES, variables: { term: i.term, first: i.first ?? 10 } }
174
+ });
175
+ }
176
+ }
177
+ ],
178
+ // Trigger events (K5): a Linear webhook for an Issue or Comment. `normalize` receives the full webhook payload
179
+ // (`{ action, type, data, ... }`) and distills it to a `summary` (the run prompt) + structured fields.
180
+ events: [
181
+ {
182
+ type: "Issue",
183
+ normalize: (raw) => {
184
+ const p = raw ?? {};
185
+ const d = p.data ?? {};
186
+ return {
187
+ summary: `Linear issue ${str(p.action) || "event"}: ${str(d.identifier)} ${str(d.title)}`.trim(),
188
+ action: str(p.action),
189
+ identifier: str(d.identifier),
190
+ title: str(d.title),
191
+ url: str(d.url)
192
+ };
193
+ }
194
+ },
195
+ {
196
+ type: "Comment",
197
+ normalize: (raw) => {
198
+ const p = raw ?? {};
199
+ const d = p.data ?? {};
200
+ return { summary: `Linear comment ${str(p.action) || "event"}: ${str(d.body)}`.trim(), action: str(p.action), body: str(d.body), url: str(d.url) };
201
+ }
202
+ }
203
+ ]
204
+ });
205
+
206
+ // src/connectors/email.ts
207
+ import { z as z3 } from "zod";
208
+ var enc = new TextEncoder();
209
+ function b64url(bytes) {
210
+ let s = "";
211
+ for (const b of bytes) s += String.fromCharCode(b);
212
+ return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
213
+ }
214
+ function b64urlDecode(s) {
215
+ const b = atob(s.replace(/-/g, "+").replace(/_/g, "/"));
216
+ return Uint8Array.from(b, (c) => c.charCodeAt(0));
217
+ }
218
+ async function hmac(secret, msg) {
219
+ const key = await crypto.subtle.importKey("raw", enc.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
220
+ return new Uint8Array(await crypto.subtle.sign("HMAC", key, enc.encode(msg)));
221
+ }
222
+ function timingEqual(a, b) {
223
+ if (a.length !== b.length) return false;
224
+ let diff = 0;
225
+ for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
226
+ return diff === 0;
227
+ }
228
+ function emailConnector(opts) {
229
+ return defineConnector({
230
+ provider: "email",
231
+ actions: [
232
+ {
233
+ name: "email.send",
234
+ description: "Send a plain-text email.",
235
+ inputSchema: z3.object({ to: z3.string(), subject: z3.string(), text: z3.string() }),
236
+ needsApproval: true,
237
+ // side-effecting + arbitrary recipient ⇒ always gated (human approves to + body)
238
+ execute: async (input, ctx) => {
239
+ const i = input;
240
+ return ctx.connection.proxy({ method: "POST", path: "/emails", body: { from: opts.from, to: i.to, subject: i.subject, text: i.text } });
241
+ }
242
+ }
243
+ ],
244
+ events: [{ type: "inbound", normalize: (raw) => raw }]
245
+ });
246
+ }
247
+ async function mintReplyToken(followupId, secret, opts) {
248
+ const payload = { f: followupId };
249
+ if (opts?.expiresInSec != null) payload.e = (opts.nowSec ?? Math.floor(Date.now() / 1e3)) + opts.expiresInSec;
250
+ const json = JSON.stringify(payload);
251
+ return `${b64url(enc.encode(json))}.${b64url(await hmac(secret, json))}`;
252
+ }
253
+ async function verifyReplyToken(token, secret, opts) {
254
+ const parts = token.split(".");
255
+ if (parts.length !== 2) return null;
256
+ const [payloadPart, sigPart] = parts;
257
+ if (!payloadPart || !sigPart) return null;
258
+ let json;
259
+ try {
260
+ json = new TextDecoder().decode(b64urlDecode(payloadPart));
261
+ } catch {
262
+ return null;
263
+ }
264
+ if (!timingEqual(b64url(await hmac(secret, json)), sigPart)) return null;
265
+ let payload;
266
+ try {
267
+ payload = JSON.parse(json);
268
+ } catch {
269
+ return null;
270
+ }
271
+ if (typeof payload.f !== "string") return null;
272
+ if (typeof payload.e === "number" && (opts?.nowSec ?? Math.floor(Date.now() / 1e3)) > payload.e) return null;
273
+ return payload.f;
274
+ }
275
+ function parseReplyToken(address) {
276
+ const m = /^[^+@\s]+\+([^@\s]+)@/.exec(address.trim());
277
+ return m?.[1] ?? null;
278
+ }
279
+ async function verifyInboundEmailSignature(input) {
280
+ if (!input.signature) return false;
281
+ const mac = await hmac(input.secret, input.payload);
282
+ const hex = [...mac].map((b) => b.toString(16).padStart(2, "0")).join("");
283
+ return timingEqual(hex, input.signature);
284
+ }
285
+ function emailQuestionDelivery(opts) {
286
+ const provider = opts.provider ?? "email";
287
+ const ttl = opts.tokenTtlSec ?? 604800;
288
+ return {
289
+ async deliver(q, ctx, route) {
290
+ if (route.channel !== "email") throw new Error(`emailQuestionDelivery: route is '${route.channel}', not email`);
291
+ const token = await mintReplyToken(q.followupId, opts.tokenSecret, { expiresInSec: ttl });
292
+ const connection = await opts.backend.resolveConnection({ provider }, ctx);
293
+ const res = await connection.proxy({
294
+ method: "POST",
295
+ path: "/emails",
296
+ // text ONLY (no html); the prompt is plain data inside a fixed template; reply_to carries the correlation.
297
+ body: { from: opts.from, to: route.target.to, subject: "A question for you", text: q.prompt, reply_to: opts.replyAddress(token) }
298
+ });
299
+ const messageId = res?.id;
300
+ if (!messageId) throw new Error("emailQuestionDelivery: send returned no message id");
301
+ return { channel: "email", ref: { message_id: messageId } };
302
+ }
303
+ };
304
+ }
305
+
306
+ // src/static-backend.ts
307
+ import { defineTool } from "@nightowlsdev/core";
308
+ function staticBackend(opts) {
309
+ const fetchImpl = opts.fetchImpl ?? fetch;
310
+ const resolveConnection = async (ref, ctx) => {
311
+ const conn = opts.connections[ref.provider];
312
+ if (!conn) throw new Error(`staticBackend: no connection configured for provider '${ref.provider}'`);
313
+ let token = conn.token;
314
+ if (token === void 0 && conn.secretRef) {
315
+ token = opts.secrets ? await opts.secrets.resolve(conn.secretRef, ctx) : void 0;
316
+ if (token === void 0) {
317
+ throw new Error(
318
+ `staticBackend: credential '${conn.secretRef}' for provider '${ref.provider}' could not be resolved (no SecretResolver configured, or the ref is unknown)`
319
+ );
320
+ }
321
+ }
322
+ const authHeader = conn.authHeader ?? ((t) => t ? { Authorization: `Bearer ${t}` } : {});
323
+ return {
324
+ accountId: conn.accountId ?? "default",
325
+ async proxy(req) {
326
+ const bodyless = req.method === "GET";
327
+ const path = req.path.startsWith("/") ? req.path : `/${req.path}`;
328
+ const url = new URL(conn.baseUrl.replace(/\/+$/, "") + path);
329
+ for (const [k, v] of Object.entries(req.query ?? {})) {
330
+ if (v === void 0 || v === null) continue;
331
+ url.searchParams.set(k, typeof v === "object" ? JSON.stringify(v) : String(v));
332
+ }
333
+ const headers = {
334
+ ...bodyless ? {} : { "content-type": "application/json" },
335
+ ...authHeader(token),
336
+ ...req.headers ?? {}
337
+ };
338
+ const res = await fetchImpl(url.toString(), {
339
+ method: req.method,
340
+ headers,
341
+ body: bodyless || req.body === void 0 ? void 0 : JSON.stringify(req.body)
342
+ });
343
+ const text = await res.text();
344
+ let parsed = text;
345
+ try {
346
+ parsed = text ? JSON.parse(text) : null;
347
+ } catch {
348
+ }
349
+ if (!res.ok) {
350
+ const snippet = text.length > 300 ? `${text.slice(0, 300)}\u2026` : text;
351
+ throw new Error(`staticBackend proxy ${req.method} ${req.path} -> ${res.status}${snippet ? `: ${snippet}` : ""}`);
352
+ }
353
+ return parsed;
354
+ }
355
+ };
356
+ };
357
+ return {
358
+ resolveConnection,
359
+ async materialize(def, _ctx) {
360
+ return def.actions.map((action) => {
361
+ const needsApproval = action.needsApproval ?? true;
362
+ const run = async (input, toolCtx) => {
363
+ const swarmCtx = {
364
+ tenantId: toolCtx.tenantId,
365
+ userId: toolCtx.userId,
366
+ runId: toolCtx.runId,
367
+ agentSlug: "",
368
+ threadId: ""
369
+ };
370
+ const connection = await resolveConnection({ provider: def.provider }, swarmCtx);
371
+ const op = action.op;
372
+ const result = action.execute ? await action.execute(input, { ...toolCtx, connection }) : (
373
+ // A GET op carries its input as query params (a body would make fetch throw); others as a body.
374
+ await connection.proxy(
375
+ op.method === "GET" ? { method: op.method, path: op.path, query: input ?? {} } : { method: op.method, path: op.path, body: input }
376
+ )
377
+ );
378
+ return fenceConnectorOutput(result);
379
+ };
380
+ const handle = defineTool({
381
+ name: action.name,
382
+ description: action.description ?? action.name,
383
+ inputSchema: action.inputSchema,
384
+ origin: "first-party",
385
+ needsApproval,
386
+ execute: run
387
+ });
388
+ return Object.assign(handle, { execute: run });
389
+ });
390
+ }
391
+ };
392
+ }
393
+
394
+ // src/nango-backend.ts
395
+ import { defineTool as defineTool2 } from "@nightowlsdev/core";
396
+ function nangoBackend(opts) {
397
+ const resolveConnection = async (ref, ctx) => {
398
+ const conn = await opts.resolveConnection(ref, ctx);
399
+ if (!conn) {
400
+ throw new Error(`nangoBackend: no connection for provider '${ref.provider}' (tenant must connect via OAuth)`);
401
+ }
402
+ return {
403
+ accountId: conn.accountId ?? "default",
404
+ async proxy(req) {
405
+ return opts.nango.proxy({
406
+ connectionId: conn.connectionId,
407
+ ...conn.providerConfigKey ? { providerConfigKey: conn.providerConfigKey } : {},
408
+ method: req.method,
409
+ path: req.path,
410
+ ...req.query ? { query: req.query } : {},
411
+ ...req.body !== void 0 ? { body: req.body } : {},
412
+ ...req.headers ? { headers: req.headers } : {}
413
+ });
414
+ }
415
+ };
416
+ };
417
+ return {
418
+ resolveConnection,
419
+ async materialize(def, _ctx) {
420
+ return def.actions.map((action) => {
421
+ if (!action.execute && !action.op) {
422
+ throw new Error(`nangoBackend: action '${action.name}' has neither op nor execute`);
423
+ }
424
+ const needsApproval = action.needsApproval ?? true;
425
+ const run = async (input, toolCtx) => {
426
+ const swarmCtx = {
427
+ tenantId: toolCtx.tenantId,
428
+ userId: toolCtx.userId,
429
+ runId: toolCtx.runId,
430
+ agentSlug: "",
431
+ threadId: ""
432
+ };
433
+ const connection = await resolveConnection({ provider: def.provider }, swarmCtx);
434
+ const op = action.op;
435
+ const result = action.execute ? await action.execute(input, { ...toolCtx, connection }) : (
436
+ // A GET op carries its input as query params; others as a body (same routing as staticBackend).
437
+ await connection.proxy(
438
+ op.method === "GET" ? { method: op.method, path: op.path, query: input ?? {} } : { method: op.method, path: op.path, body: input }
439
+ )
440
+ );
441
+ return fenceConnectorOutput(result);
442
+ };
443
+ const handle = defineTool2({
444
+ name: action.name,
445
+ description: action.description ?? action.name,
446
+ inputSchema: action.inputSchema,
447
+ origin: "first-party",
448
+ needsApproval,
449
+ execute: run
450
+ });
451
+ return Object.assign(handle, { execute: run });
452
+ });
453
+ }
454
+ };
455
+ }
456
+
457
+ // src/trigger.ts
458
+ var hexFromBuffer = (buf) => [...new Uint8Array(buf)].map((b) => b.toString(16).padStart(2, "0")).join("");
459
+ function safeEqual(a, b) {
460
+ if (a.length !== b.length) return false;
461
+ let diff = 0;
462
+ for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
463
+ return diff === 0;
464
+ }
465
+ async function verifySlackSignature(input) {
466
+ if (!input.signature || !input.timestamp) return { ok: false, reason: "missing signature/timestamp" };
467
+ const ts = Number(input.timestamp);
468
+ if (!Number.isFinite(ts)) return { ok: false, reason: "bad timestamp" };
469
+ const now = input.nowSec ?? Math.floor(Date.now() / 1e3);
470
+ if (Math.abs(now - ts) > (input.maxSkewSec ?? 300)) return { ok: false, reason: "stale timestamp" };
471
+ const enc2 = new TextEncoder();
472
+ const key = await crypto.subtle.importKey("raw", enc2.encode(input.signingSecret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
473
+ const mac = await crypto.subtle.sign("HMAC", key, enc2.encode(`v0:${input.timestamp}:${input.body}`));
474
+ const expected = `v0=${hexFromBuffer(mac)}`;
475
+ return safeEqual(expected, input.signature) ? { ok: true } : { ok: false, reason: "signature mismatch" };
476
+ }
477
+ async function verifyLinearSignature(input) {
478
+ if (!input.signature) return { ok: false, reason: "missing signature" };
479
+ const enc2 = new TextEncoder();
480
+ const key = await crypto.subtle.importKey("raw", enc2.encode(input.secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
481
+ const mac = await crypto.subtle.sign("HMAC", key, enc2.encode(input.body));
482
+ return safeEqual(hexFromBuffer(mac), input.signature) ? { ok: true } : { ok: false, reason: "signature mismatch" };
483
+ }
484
+ var asRecord = (v) => typeof v === "object" && v !== null ? v : {};
485
+ var asString = (v) => typeof v === "string" ? v : void 0;
486
+ function triggerMessage(provider, eventType, normalized) {
487
+ const summary = asString(asRecord(normalized).summary);
488
+ return summary ?? `${provider} ${eventType} event`;
489
+ }
490
+ async function handleTriggerEvent(provider, rawPayload, opts) {
491
+ if (!opts.skipSignatureCheck) {
492
+ if (!opts.signingSecret) return { status: "rejected", reason: "no signing secret" };
493
+ const verified = await verifySlackSignature({
494
+ signingSecret: opts.signingSecret,
495
+ signature: opts.signature,
496
+ timestamp: opts.timestamp,
497
+ body: rawPayload,
498
+ nowSec: opts.nowSec?.(),
499
+ maxSkewSec: opts.maxSkewSec
500
+ });
501
+ if (!verified.ok) return { status: "rejected", reason: verified.reason };
502
+ }
503
+ let body;
504
+ try {
505
+ body = asRecord(JSON.parse(rawPayload));
506
+ } catch {
507
+ return { status: "rejected", reason: "invalid json" };
508
+ }
509
+ if (body.type === "url_verification") {
510
+ const challenge = asString(body.challenge);
511
+ return challenge ? { status: "challenge", challenge } : { status: "rejected", reason: "missing challenge" };
512
+ }
513
+ if (body.type !== "event_callback") return { status: "ignored", reason: `unhandled outer type ${String(body.type)}` };
514
+ const externalId = asString(body.event_id);
515
+ if (!externalId) return { status: "ignored", reason: "missing event_id" };
516
+ const inner = asRecord(body.event);
517
+ const eventType = asString(inner.type);
518
+ if (!eventType) return { status: "ignored", reason: "missing event.type" };
519
+ const workspaceId = asString(body.team_id);
520
+ if (!await opts.dedupe.markSeen({ provider, externalId })) return { status: "ignored", reason: "duplicate" };
521
+ const spec = opts.connector.events?.find((e) => e.type === eventType);
522
+ if (!spec) return { status: "ignored", reason: `no event spec for ${eventType}` };
523
+ const normalized = spec.normalize ? spec.normalize(body) : body;
524
+ const subs = await opts.lookupSubscriptions({ provider, workspaceId, eventType });
525
+ if (subs.length === 0) return { status: "ignored", reason: "no subscriptions" };
526
+ const fenced = fenceConnectorOutput(normalized).fenced;
527
+ const message = triggerMessage(provider, eventType, normalized);
528
+ const runs = [];
529
+ const failed = [];
530
+ for (const sub of subs) {
531
+ const runId = opts.mintRunId();
532
+ try {
533
+ await opts.enqueue(
534
+ {
535
+ message,
536
+ context: { trigger: { provider, type: eventType, externalId, payload: fenced } },
537
+ ...sub.workflow ? { workflow: sub.workflow } : {}
538
+ },
539
+ { tenantId: sub.orgId, userId: sub.ownerUserId, agentSlug: sub.agentSlug, threadId: sub.threadId, runId }
540
+ );
541
+ runs.push({ runId, agentSlug: sub.agentSlug });
542
+ } catch (err) {
543
+ failed.push({ agentSlug: sub.agentSlug, error: err instanceof Error ? err.message : String(err) });
544
+ }
545
+ }
546
+ return { status: "enqueued", runs, failed: failed.length };
547
+ }
548
+
549
+ // src/delivery.ts
550
+ function escapeSlackText(s) {
551
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
552
+ }
553
+ function renderSlackQuestion(prompt, _field) {
554
+ return {
555
+ blocks: [
556
+ { type: "section", text: { type: "plain_text", text: prompt, emoji: false } },
557
+ { type: "context", elements: [{ type: "plain_text", text: "Reply in thread to answer.", emoji: false }] }
558
+ ],
559
+ text: "You have a question to answer."
560
+ // fixed fallback; the prompt is ONLY in the inert plain_text block
561
+ };
562
+ }
563
+ function slackQuestionDelivery(opts) {
564
+ const provider = opts.provider ?? "slack";
565
+ return {
566
+ async deliver(q, ctx, route) {
567
+ if (route.channel !== "slack") throw new Error(`slackQuestionDelivery: route is '${route.channel}', not slack`);
568
+ const channelId = route.target.channelId;
569
+ const connection = await opts.backend.resolveConnection({ provider }, ctx);
570
+ const rendered = renderSlackQuestion(q.prompt, q.field);
571
+ const res = await connection.proxy({
572
+ method: "POST",
573
+ path: "/chat.postMessage",
574
+ body: { channel: channelId, blocks: rendered.blocks, text: rendered.text }
575
+ });
576
+ const deliveryTs = res?.ts;
577
+ if (!deliveryTs) throw new Error("slackQuestionDelivery: chat.postMessage returned no ts");
578
+ return { channel: "slack", ref: { team_id: route.target.teamId, channel_id: channelId, delivery_ts: deliveryTs } };
579
+ }
580
+ };
581
+ }
582
+
583
+ // src/inbound.ts
584
+ function sanitizeReply(raw, opts) {
585
+ const maxChars = opts?.maxChars ?? 4e3;
586
+ const lines = raw.replace(/\r\n/g, "\n").split("\n");
587
+ const out = [];
588
+ for (const line of lines) {
589
+ if (/^\s*>/.test(line)) continue;
590
+ if (/^\s*--\s*$/.test(line)) break;
591
+ if (/^\s*On .+ wrote:\s*$/.test(line)) break;
592
+ out.push(line);
593
+ }
594
+ return out.join("\n").trim().slice(0, maxChars);
595
+ }
596
+ function correlateSlackInbound(event) {
597
+ const outer = event ?? {};
598
+ const ev = outer.event ?? {};
599
+ const s = (v) => typeof v === "string" ? v : void 0;
600
+ if (ev.subtype !== void 0 || ev.bot_id !== void 0) return null;
601
+ const teamId = s(outer.team_id);
602
+ const channelId = s(ev.channel);
603
+ const threadTs = s(ev.thread_ts);
604
+ const userId = s(ev.user);
605
+ const text = s(ev.text) ?? "";
606
+ if (!teamId || !channelId || !threadTs || !userId) return null;
607
+ return { teamId, channelId, threadTs, text, userId };
608
+ }
609
+ async function handleInboundReply(input, opts) {
610
+ if (!await opts.recordInboundOnce({ provider: input.provider, externalId: input.externalId })) {
611
+ return { status: "ignored", reason: "duplicate-transport" };
612
+ }
613
+ const corr = await opts.correlate(input);
614
+ if (!corr) return { status: "ignored", reason: "no-correlation" };
615
+ const auth = await opts.authorizeReplier({ tenantId: corr.tenantId, provider: input.provider, replier: input.replier });
616
+ if (!auth || auth.userId !== corr.approverUserId) return { status: "ignored", reason: "unauthorized-approver" };
617
+ const answer = (opts.sanitize ?? sanitizeReply)(input.text);
618
+ if (!answer) return { status: "ignored", reason: "empty-reply" };
619
+ if (!await opts.answerOnce(corr.followupId, corr.tenantId)) return { status: "ignored", reason: "already-answered" };
620
+ const ctx = await opts.resolveContext(corr);
621
+ const { runId } = await opts.resumeEnqueue({ runId: corr.runId, toolCallId: corr.toolCallId, followupId: corr.followupId, answer }, ctx);
622
+ return { status: "resumed", runId };
623
+ }
624
+ export {
625
+ correlateSlackInbound,
626
+ defineConnector,
627
+ emailConnector,
628
+ emailQuestionDelivery,
629
+ escapeSlackText,
630
+ fenceConnectorOutput,
631
+ handleInboundReply,
632
+ handleTriggerEvent,
633
+ linearConnector,
634
+ materializeConnectors,
635
+ mintReplyToken,
636
+ nangoBackend,
637
+ parseReplyToken,
638
+ renderSlackQuestion,
639
+ sanitizeReply,
640
+ slackConnector,
641
+ slackQuestionDelivery,
642
+ staticBackend,
643
+ verifyInboundEmailSignature,
644
+ verifyLinearSignature,
645
+ verifyReplyToken,
646
+ verifySlackSignature
647
+ };
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@nightowlsdev/connectors",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "license": "MIT",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/cueplusplus/corale.git",
12
+ "directory": "packages/connectors"
13
+ },
14
+ "homepage": "https://github.com/cueplusplus/corale#readme",
15
+ "sideEffects": false,
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "import": "./dist/index.js",
20
+ "require": "./dist/index.cjs"
21
+ }
22
+ },
23
+ "main": "./dist/index.cjs",
24
+ "module": "./dist/index.js",
25
+ "types": "./dist/index.d.ts",
26
+ "files": [
27
+ "dist"
28
+ ],
29
+ "dependencies": {
30
+ "zod": "^4.0.0"
31
+ },
32
+ "peerDependencies": {
33
+ "@nightowlsdev/core": "0.4.0"
34
+ },
35
+ "devDependencies": {
36
+ "@types/node": "^24.12.4",
37
+ "tsup": "8.5.1",
38
+ "typescript": "6.0.3",
39
+ "vitest": "^3.2.0",
40
+ "@nightowlsdev/core": "0.4.0",
41
+ "@nightowlsdev/eslint-config": "0.0.0",
42
+ "@nightowlsdev/tsconfig": "0.0.0"
43
+ },
44
+ "scripts": {
45
+ "build": "tsup",
46
+ "typecheck": "tsc --noEmit",
47
+ "test": "vitest run",
48
+ "lint": "eslint src"
49
+ }
50
+ }