@vornrun/connector-sdk 0.6.1 → 0.7.0-beta.10

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,2220 @@
1
+ // src/packaging.ts
2
+ import { builtinModules } from "module";
3
+ import { readFileSync } from "fs";
4
+ import { dirname, isAbsolute, join, resolve } from "path";
5
+ var MAX_PACK_BYTES = 8 * 1024 * 1024;
6
+ var LIFECYCLE_SCRIPTS = [
7
+ "preinstall",
8
+ "install",
9
+ "postinstall",
10
+ "prepare",
11
+ "prepublish",
12
+ "prepublishOnly",
13
+ "postpublish"
14
+ ];
15
+ var BUILTINS = new Set(builtinModules);
16
+ function finding(code, target, message) {
17
+ return { level: "error", code, target, message };
18
+ }
19
+ function lifecycleScriptFindings(pkg) {
20
+ const scripts = pkg?.scripts;
21
+ if (!scripts || typeof scripts !== "object") return [];
22
+ const named = LIFECYCLE_SCRIPTS.filter((name) => typeof scripts[name] === "string");
23
+ if (named.length === 0) return [];
24
+ return [
25
+ finding(
26
+ "lifecycle-scripts",
27
+ "package.json",
28
+ `Remove the ${named.join(", ")} script(s); a pack is installed by copying files, never by running them`
29
+ )
30
+ ];
31
+ }
32
+ function bundleDependencyFindings(external) {
33
+ const specifiers = /* @__PURE__ */ new Set();
34
+ for (const specifier of external) {
35
+ if (specifier.startsWith(".") || specifier.startsWith("/")) continue;
36
+ if (specifier.startsWith("node:") || BUILTINS.has(specifier)) continue;
37
+ specifiers.add(specifier);
38
+ }
39
+ if (specifiers.size === 0) return [];
40
+ return [
41
+ finding(
42
+ "runtime-dependencies",
43
+ "bundle",
44
+ `${[...specifiers].sort().join(", ")} stayed outside the bundle; a pack must launch with no install step`
45
+ )
46
+ ];
47
+ }
48
+ function packageDirFor(resolveDir, entry) {
49
+ const from = resolve(resolveDir);
50
+ if (entry === void 0) return from;
51
+ return entry.startsWith(".") || isAbsolute(entry) ? dirname(resolve(from, entry)) : from;
52
+ }
53
+ function readNearestPackageJson(fromDir) {
54
+ let current = resolve(fromDir);
55
+ for (; ; ) {
56
+ try {
57
+ return JSON.parse(readFileSync(join(current, "package.json"), "utf8"));
58
+ } catch {
59
+ const parent = dirname(current);
60
+ if (parent === current) return void 0;
61
+ current = parent;
62
+ }
63
+ }
64
+ }
65
+ function packEntryContents(entry, sdkModule = "@vornrun/connector-sdk") {
66
+ return [
67
+ `import { serveConnector } from ${JSON.stringify(sdkModule)}`,
68
+ `import * as entry from ${JSON.stringify(entry)}`,
69
+ "const exported = Object.values(entry).find((value) => value && Array.isArray(value.triggers))",
70
+ `if (!exported) throw new Error(${JSON.stringify(`${entry} exports no connector`)})`,
71
+ "await serveConnector(exported)",
72
+ ""
73
+ ].join("\n");
74
+ }
75
+ async function esbuildBundle(request) {
76
+ const { build } = await import("esbuild");
77
+ const result = await build({
78
+ stdin: {
79
+ contents: request.contents,
80
+ resolveDir: request.resolveDir,
81
+ sourcefile: "vorn-connector-pack.js",
82
+ loader: "js"
83
+ },
84
+ bundle: true,
85
+ platform: "node",
86
+ target: "node20",
87
+ format: "esm",
88
+ write: false,
89
+ metafile: true,
90
+ legalComments: "none"
91
+ });
92
+ const output = Object.values(result.metafile.outputs)[0];
93
+ return {
94
+ code: result.outputFiles[0].text,
95
+ external: (output?.imports ?? []).filter((item) => item.external).map((item) => item.path)
96
+ };
97
+ }
98
+
99
+ // src/normalize.ts
100
+ var RESERVED_KEYS = [
101
+ "externalId",
102
+ "title",
103
+ "url",
104
+ "description",
105
+ "status",
106
+ "labels",
107
+ "assignee",
108
+ "updatedAt"
109
+ ];
110
+ var UNSAFE_KEYS = ["__proto__", "constructor", "prototype"];
111
+ function itemExternalId(item) {
112
+ return String(item.externalId ?? "").trim();
113
+ }
114
+ function itemTimestamp(item, fallback) {
115
+ return isoTimestamp(item.updatedAt, fallback);
116
+ }
117
+ function isoTimestamp(value, fallback) {
118
+ if (value === void 0) return fallback;
119
+ const date = value instanceof Date ? value : new Date(value);
120
+ if (Number.isNaN(date.getTime())) {
121
+ throw new Error(`Invalid updatedAt: ${String(value)}`);
122
+ }
123
+ return date.toISOString();
124
+ }
125
+ function normalizeItem(item, polledAt) {
126
+ const externalId = itemExternalId(item);
127
+ if (!externalId) {
128
+ throw new Error("Connector item is missing externalId");
129
+ }
130
+ if (!item.title || !item.title.trim()) {
131
+ throw new Error(`Connector item ${externalId} is missing title`);
132
+ }
133
+ const extra = {};
134
+ for (const [key, value] of Object.entries(item.data ?? {})) {
135
+ if (RESERVED_KEYS.includes(key)) continue;
136
+ if (UNSAFE_KEYS.includes(key)) continue;
137
+ extra[key] = value;
138
+ }
139
+ return {
140
+ ...extra,
141
+ externalId,
142
+ title: item.title,
143
+ url: item.url ?? "",
144
+ description: item.description ?? "",
145
+ status: item.status ?? "open",
146
+ labels: item.labels ?? [],
147
+ ...item.assignee !== void 0 && { assignee: item.assignee },
148
+ updatedAt: isoTimestamp(item.updatedAt, polledAt)
149
+ };
150
+ }
151
+ function normalizeItems(items, polledAt) {
152
+ const seen = /* @__PURE__ */ new Set();
153
+ return items.map((item) => {
154
+ const normalized = normalizeItem(item, polledAt);
155
+ if (seen.has(normalized.externalId)) {
156
+ throw new Error(`Duplicate externalId "${normalized.externalId}" in one poll page`);
157
+ }
158
+ seen.add(normalized.externalId);
159
+ return normalized;
160
+ });
161
+ }
162
+
163
+ // src/dedupe.ts
164
+ var MAX_BOUNDARY_IDS = 500;
165
+ function decodeCursor(cursor, strategy) {
166
+ if (!cursor) return void 0;
167
+ let parsed;
168
+ try {
169
+ parsed = JSON.parse(cursor);
170
+ } catch (error) {
171
+ throw new Error(`Cursor is not valid SDK cursor JSON: ${cursor}`, { cause: error });
172
+ }
173
+ const state = parsed;
174
+ if (!state || typeof state !== "object" || state.v !== 1 || state.s !== strategy) {
175
+ throw new Error(`Cursor does not belong to the "${strategy}" strategy: ${cursor}`);
176
+ }
177
+ const wellFormed = state.s === "timestamp" ? typeof state.t === "string" && Array.isArray(state.ids) && state.ids.every((id) => typeof id === "string") : typeof state.id === "string";
178
+ if (!wellFormed) {
179
+ throw new Error(`Cursor is missing the fields the "${strategy}" strategy needs: ${cursor}`);
180
+ }
181
+ return state;
182
+ }
183
+ function compare(left, right) {
184
+ if (left === right) return 0;
185
+ return left < right ? -1 : 1;
186
+ }
187
+ function page(chronological, context, hadCursor, nextCursor) {
188
+ const delivered = context.limit === void 0 ? chronological : chronological.slice(0, context.limit);
189
+ if (delivered.length === 0) {
190
+ return { items: [], ...context.cursor !== void 0 && { nextCursor: context.cursor } };
191
+ }
192
+ return {
193
+ items: delivered.map((entry) => entry.item),
194
+ nextCursor: JSON.stringify(nextCursor(delivered)),
195
+ // Only drain a backlog we know we truncated, and only once a cursor
196
+ // exists — a first poll should not pull the source's entire history.
197
+ hasMore: chronological.length > delivered.length && hadCursor
198
+ };
199
+ }
200
+ function timestampPoll(fetched, state, context, polledAt) {
201
+ const boundary = state?.t ?? context.since;
202
+ const seen = new Set(state?.ids ?? []);
203
+ const fresh = [];
204
+ const pinnedAlreadySeen = [];
205
+ for (const item of fetched) {
206
+ const id = itemExternalId(item);
207
+ const pinned = item.updatedAt === void 0 && boundary !== void 0;
208
+ const at = pinned ? boundary : itemTimestamp(item, polledAt);
209
+ const isNew = boundary === void 0 || at > boundary || at === boundary && !seen.has(id);
210
+ if (isNew) fresh.push({ item, at, id, ...pinned && { pinned: true } });
211
+ else if (pinned) pinnedAlreadySeen.push(id);
212
+ }
213
+ fresh.sort((left, right) => compare(left.at, right.at) || compare(left.id, right.id));
214
+ return page(fresh, context, state !== void 0, (delivered) => {
215
+ const newest = delivered[delivered.length - 1].at;
216
+ const atNewest = [];
217
+ for (let i = delivered.length - 1; i >= 0 && delivered[i].at === newest; i -= 1) {
218
+ atNewest.push(delivered[i].id);
219
+ }
220
+ const carried = newest === boundary ? [...seen, ...atNewest] : [
221
+ ...pinnedAlreadySeen,
222
+ ...delivered.filter((entry) => entry.pinned).map((entry) => entry.id),
223
+ ...atNewest
224
+ ];
225
+ return { v: 1, s: "timestamp", t: newest, ids: carried.slice(-MAX_BOUNDARY_IDS) };
226
+ });
227
+ }
228
+ function lastItemPoll(fetched, state, context, polledAt) {
229
+ const keyed = fetched.map((item) => ({
230
+ item,
231
+ at: itemTimestamp(item, polledAt),
232
+ id: itemExternalId(item)
233
+ }));
234
+ const stopAt = state ? keyed.findIndex((entry) => entry.id === state.id) : -1;
235
+ const chronological = (stopAt === -1 ? keyed : keyed.slice(0, stopAt)).reverse();
236
+ return page(chronological, context, state !== void 0, (delivered) => ({
237
+ v: 1,
238
+ s: "lastItem",
239
+ id: delivered[delivered.length - 1].id
240
+ }));
241
+ }
242
+ async function pollWithDedupe(trigger, context) {
243
+ const strategy = trigger.dedupe;
244
+ const fetchItems = trigger.fetch;
245
+ if (!strategy || !fetchItems) {
246
+ throw new Error(`Trigger ${trigger.type} is not a declarative trigger`);
247
+ }
248
+ const polledAt = context.now();
249
+ if (strategy === "lastItem") {
250
+ const state2 = decodeCursor(context.cursor, "lastItem");
251
+ const fetched2 = await runFetch(trigger.type, fetchItems, {
252
+ config: context.config,
253
+ ...state2 && { lastItemId: state2.id },
254
+ ...context.limit !== void 0 && { limit: context.limit },
255
+ now: context.now,
256
+ fetch: context.fetch
257
+ });
258
+ return lastItemPoll(fetched2, state2, context, polledAt);
259
+ }
260
+ const state = decodeCursor(context.cursor, "timestamp");
261
+ const since = state?.t ?? context.since;
262
+ const fetched = await runFetch(trigger.type, fetchItems, {
263
+ config: context.config,
264
+ ...since !== void 0 && { since },
265
+ ...context.limit !== void 0 && { limit: context.limit },
266
+ now: context.now,
267
+ fetch: context.fetch
268
+ });
269
+ return timestampPoll(fetched, state, context, polledAt);
270
+ }
271
+ async function runFetch(type, fetchItems, context) {
272
+ const fetched = await fetchItems(context);
273
+ if (!Array.isArray(fetched)) {
274
+ throw new Error(`Trigger ${type} fetch() did not return an array`);
275
+ }
276
+ return fetched;
277
+ }
278
+
279
+ // src/post-receive.ts
280
+ var UNSAFE_KEYS2 = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
281
+ function isRecord(value) {
282
+ return typeof value === "object" && value !== null && !Array.isArray(value);
283
+ }
284
+ function segments(path) {
285
+ return path.split(".").map((part) => part.trim()).filter((part) => part !== "");
286
+ }
287
+ function valueAt(value, path) {
288
+ let current = value;
289
+ for (const key of segments(path)) {
290
+ if (UNSAFE_KEYS2.has(key)) return void 0;
291
+ if (Array.isArray(current)) {
292
+ const index = Number(key);
293
+ if (!Number.isInteger(index)) return void 0;
294
+ current = current[index];
295
+ continue;
296
+ }
297
+ if (!isRecord(current)) return void 0;
298
+ current = Object.prototype.hasOwnProperty.call(current, key) ? current[key] : void 0;
299
+ }
300
+ return current;
301
+ }
302
+ function withValueAt(value, path, next) {
303
+ const keys = segments(path);
304
+ if (keys.length === 0) return next;
305
+ const [head, ...rest] = keys;
306
+ if (UNSAFE_KEYS2.has(head)) return value;
307
+ if (Array.isArray(value)) {
308
+ const index = Number(head);
309
+ if (!Number.isInteger(index)) return value;
310
+ const copy = [...value];
311
+ copy[index] = rest.length === 0 ? next : withValueAt(copy[index], rest.join("."), next);
312
+ return copy;
313
+ }
314
+ const base = isRecord(value) ? value : {};
315
+ return {
316
+ ...base,
317
+ [head]: rest.length === 0 ? next : withValueAt(base[head], rest.join("."), next)
318
+ };
319
+ }
320
+ function pick(value, keys) {
321
+ if (Array.isArray(value)) return value.map((entry) => pick(entry, keys));
322
+ if (!isRecord(value)) return value;
323
+ const out = {};
324
+ for (const key of keys) {
325
+ if (UNSAFE_KEYS2.has(key)) continue;
326
+ if (Object.prototype.hasOwnProperty.call(value, key)) out[key] = value[key];
327
+ }
328
+ return out;
329
+ }
330
+ function rename(value, from, to) {
331
+ if (Array.isArray(value)) return value.map((entry) => rename(entry, from, to));
332
+ if (!isRecord(value)) return value;
333
+ if (UNSAFE_KEYS2.has(from) || UNSAFE_KEYS2.has(to)) return value;
334
+ if (!Object.prototype.hasOwnProperty.call(value, from)) return value;
335
+ const out = {};
336
+ for (const [key, entry] of Object.entries(value)) {
337
+ if (key === from) out[to] = entry;
338
+ else if (key !== to) out[key] = entry;
339
+ }
340
+ return out;
341
+ }
342
+ function applyOp(value, op) {
343
+ if (op.op === "flatten") return valueAt(value, op.path);
344
+ const target = op.path === void 0 ? value : valueAt(value, op.path);
345
+ if (op.path !== void 0 && target === void 0) return value;
346
+ let next;
347
+ if (op.op === "pick") next = pick(target, op.keys);
348
+ else if (op.op === "rename") next = rename(target, op.from, op.to);
349
+ else if (op.op === "filter") {
350
+ next = Array.isArray(target) ? target.filter((entry) => isRecord(entry) && valueAt(entry, op.key) === op.equals) : target;
351
+ } else {
352
+ next = Array.isArray(target) ? target.map((entry) => applyPostReceive(entry, op.ops)) : target;
353
+ }
354
+ return op.path === void 0 ? next : withValueAt(value, op.path, next);
355
+ }
356
+ function applyPostReceive(value, ops) {
357
+ return (ops ?? []).reduce(applyOp, value);
358
+ }
359
+
360
+ // src/request.ts
361
+ var MAX_ERROR_BODY = 500;
362
+ var PLACEHOLDER = /\{\{\s*(args|config)\.([A-Za-z0-9_.-]+)\s*\}\}/g;
363
+ var WHOLE_PLACEHOLDER = /^\{\{\s*(args|config)\.([A-Za-z0-9_.-]+)\s*\}\}$/;
364
+ function lookup(source, path, scope) {
365
+ return valueAt(source === "args" ? scope.args : scope.config, path);
366
+ }
367
+ var intoUrl = (value, source) => source === "config" ? value : encodeURIComponent(value);
368
+ function intoHeader(name) {
369
+ return (value) => {
370
+ if (/[\r\n]/.test(value)) {
371
+ throw new Error(`Header "${name}" would carry a line ending, which is not allowed`);
372
+ }
373
+ return value;
374
+ };
375
+ }
376
+ function resolveTemplates(value, scope, substitute) {
377
+ if (typeof value === "string") {
378
+ const whole = WHOLE_PLACEHOLDER.exec(value);
379
+ if (whole) {
380
+ const resolved = lookup(whole[1], whole[2], scope);
381
+ if (substitute === void 0 || resolved === void 0 || resolved === null) return resolved;
382
+ return substitute(String(resolved), whole[1]);
383
+ }
384
+ return value.replace(PLACEHOLDER, (_match, source, path) => {
385
+ const resolved = lookup(source, path, scope);
386
+ if (resolved === void 0 || resolved === null) return "";
387
+ const text = String(resolved);
388
+ return substitute === void 0 ? text : substitute(text, source);
389
+ });
390
+ }
391
+ if (Array.isArray(value)) return value.map((entry) => resolveTemplates(entry, scope, substitute));
392
+ if (typeof value === "object" && value !== null) {
393
+ const out = {};
394
+ for (const [key, entry] of Object.entries(value)) {
395
+ out[key] = resolveTemplates(entry, scope, substitute);
396
+ }
397
+ return out;
398
+ }
399
+ return value;
400
+ }
401
+ function resolveHeaders(raw, scope) {
402
+ const out = {};
403
+ for (const [name, value] of Object.entries(raw ?? {})) {
404
+ const resolved = resolveTemplates(value, scope, intoHeader(name));
405
+ if (resolved === void 0 || resolved === null || resolved === "") continue;
406
+ out[name] = String(resolved);
407
+ }
408
+ return out;
409
+ }
410
+ function stringMap(raw) {
411
+ const out = {};
412
+ if (typeof raw !== "object" || raw === null) return out;
413
+ for (const [key, value] of Object.entries(raw)) {
414
+ if (value === void 0 || value === null || value === "") continue;
415
+ out[key] = String(value);
416
+ }
417
+ return out;
418
+ }
419
+ function resolveRequest(request, scope) {
420
+ const method = (request.method ?? "GET").toUpperCase();
421
+ const rawUrl = resolveTemplates(request.url, scope, intoUrl);
422
+ if (typeof rawUrl !== "string" || rawUrl.trim() === "") {
423
+ throw new Error("Request has no URL once its templates are resolved");
424
+ }
425
+ let url;
426
+ try {
427
+ url = new URL(rawUrl);
428
+ } catch {
429
+ throw new Error(`Request URL is not a URL once its templates are resolved: "${rawUrl}"`);
430
+ }
431
+ for (const [key, value] of Object.entries(stringMap(resolveTemplates(request.query, scope)))) {
432
+ url.searchParams.set(key, value);
433
+ }
434
+ const headers = resolveHeaders(request.headers, scope);
435
+ const resolved = { url: url.toString(), method, headers };
436
+ if (request.body !== void 0 && method !== "GET" && method !== "HEAD") {
437
+ const body = resolveTemplates(request.body, scope);
438
+ if (body !== void 0) {
439
+ resolved.body = typeof body === "string" ? body : JSON.stringify(body);
440
+ if (!Object.keys(headers).some((key) => key.toLowerCase() === "content-type")) {
441
+ resolved.headers["content-type"] = typeof body === "string" ? "text/plain" : "application/json";
442
+ }
443
+ }
444
+ }
445
+ return resolved;
446
+ }
447
+ async function readBody(response) {
448
+ const text = await response.text();
449
+ if (text === "") return void 0;
450
+ const type = response.headers.get("content-type") ?? "";
451
+ if (!type.includes("json")) return text;
452
+ try {
453
+ return JSON.parse(text);
454
+ } catch {
455
+ return text;
456
+ }
457
+ }
458
+ function describeFailure(response, body) {
459
+ const detail = typeof body === "string" ? body : body === void 0 ? "" : JSON.stringify(body);
460
+ const quoted = detail.length > MAX_ERROR_BODY ? `${detail.slice(0, MAX_ERROR_BODY)}\u2026` : detail;
461
+ return `Request failed with ${response.status} ${response.statusText}${quoted ? `: ${quoted}` : ""}`;
462
+ }
463
+ async function sendRequest(resolved, options) {
464
+ const response = await options.fetchImpl(resolved.url, {
465
+ method: resolved.method,
466
+ headers: resolved.headers,
467
+ ...resolved.body !== void 0 && { body: resolved.body }
468
+ });
469
+ const body = await readBody(response);
470
+ if (!response.ok) throw new Error(describeFailure(response, body));
471
+ return { response, body };
472
+ }
473
+ function asOutput(value) {
474
+ if (Array.isArray(value)) return { items: value };
475
+ if (typeof value === "object" && value !== null) return value;
476
+ return value === void 0 ? {} : { result: value };
477
+ }
478
+ var MAX_REQUEST_PAGES = 100;
479
+ var LINK_NEXT = /<([^>]+)>\s*;[^,]*\brel\s*=\s*"?next"?/i;
480
+ function nextLink(header) {
481
+ const match = header === null ? null : LINK_NEXT.exec(header);
482
+ return match ? match[1] : void 0;
483
+ }
484
+ function pageItems(body, itemsPath) {
485
+ const value = itemsPath === void 0 ? body : valueAt(body, itemsPath);
486
+ return Array.isArray(value) ? value : void 0;
487
+ }
488
+ async function collectPages(request, strategy, scope, options) {
489
+ const collected = [];
490
+ const seen = /* @__PURE__ */ new Set();
491
+ let page2 = strategy.kind === "page" ? strategy.startPage ?? 1 : 0;
492
+ let cursor;
493
+ let nextUrl;
494
+ for (let index = 0; index < MAX_REQUEST_PAGES; index++) {
495
+ const resolved = resolveRequest(request, scope);
496
+ if (nextUrl !== void 0) resolved.url = nextUrl;
497
+ if (strategy.kind === "cursor" && cursor !== void 0) {
498
+ const url = new URL(resolved.url);
499
+ url.searchParams.set(strategy.param, cursor);
500
+ resolved.url = url.toString();
501
+ }
502
+ if (strategy.kind === "page") {
503
+ const url = new URL(resolved.url);
504
+ url.searchParams.set(strategy.param, String(page2));
505
+ resolved.url = url.toString();
506
+ }
507
+ if (seen.has(resolved.url)) {
508
+ throw new Error(`Request for ${request.url} asked for the same page twice`);
509
+ }
510
+ seen.add(resolved.url);
511
+ const { response, body } = await sendRequest(resolved, options);
512
+ const items = pageItems(body, strategy.itemsPath);
513
+ if (items === void 0) {
514
+ if (index === 0) {
515
+ const where = strategy.itemsPath === void 0 ? "the response is not a list" : `the response has no list at "${strategy.itemsPath}"`;
516
+ throw new Error(`Cannot page through this request: ${where}`);
517
+ }
518
+ return collected;
519
+ }
520
+ collected.push(...items);
521
+ if (items.length === 0) return collected;
522
+ if (strategy.kind === "cursor") {
523
+ const next = valueAt(body, strategy.cursorPath);
524
+ if (next === void 0 || next === null || next === "") return collected;
525
+ cursor = String(next);
526
+ continue;
527
+ }
528
+ if (strategy.kind === "link") {
529
+ nextUrl = nextLink(response.headers.get("link"));
530
+ if (nextUrl === void 0) return collected;
531
+ continue;
532
+ }
533
+ page2 += 1;
534
+ }
535
+ throw new Error(`Request for ${request.url} exceeded ${MAX_REQUEST_PAGES} pages`);
536
+ }
537
+ async function executeRequest(request, postReceive, scope, options) {
538
+ if (request.paginate) {
539
+ const items = await collectPages(request, request.paginate, scope, options);
540
+ return asOutput(applyPostReceive(items, postReceive));
541
+ }
542
+ const { body } = await sendRequest(resolveRequest(request, scope), options);
543
+ return asOutput(applyPostReceive(body, postReceive));
544
+ }
545
+
546
+ // src/resilience.ts
547
+ var RETRYABLE_STATUS = /* @__PURE__ */ new Set([408, 425, 429, 500, 502, 503, 504]);
548
+ var DEFAULT_ATTEMPTS = 3;
549
+ var DEFAULT_BASE_DELAY_MS = 250;
550
+ var DEFAULT_MAX_DELAY_MS = 3e4;
551
+ var MAX_ATTEMPTS = 10;
552
+ var MAX_TOTAL_WAIT_MS = 12e4;
553
+ var wait = (ms) => new Promise((resolve3) => {
554
+ setTimeout(resolve3, ms);
555
+ });
556
+ function retryAfterMs(header, now) {
557
+ if (!header) return void 0;
558
+ const trimmed = header.trim();
559
+ if (trimmed === "") return void 0;
560
+ const seconds = Number(trimmed);
561
+ if (Number.isFinite(seconds)) return Math.max(0, seconds * 1e3);
562
+ const at = Date.parse(trimmed);
563
+ if (Number.isNaN(at)) return void 0;
564
+ return Math.max(0, at - now);
565
+ }
566
+ function backoffMs(attempt, policy = {}) {
567
+ const base = policy.baseDelayMs ?? DEFAULT_BASE_DELAY_MS;
568
+ const max = policy.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;
569
+ return Math.min(max, base * 2 ** attempt);
570
+ }
571
+ function resilientFetch(options) {
572
+ const attempts = Math.min(MAX_ATTEMPTS, Math.max(1, options.retry?.attempts ?? DEFAULT_ATTEMPTS));
573
+ const sleep = options.sleep ?? wait;
574
+ const ceiling = options.retry?.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;
575
+ const send = async (input, init) => {
576
+ let waited = 0;
577
+ const pause = async (ms) => {
578
+ if (waited + ms > MAX_TOTAL_WAIT_MS) return false;
579
+ waited += ms;
580
+ await sleep(ms);
581
+ return true;
582
+ };
583
+ for (let attempt = 0; attempt < attempts; attempt++) {
584
+ const last = attempt === attempts - 1;
585
+ try {
586
+ const response = await options.fetchImpl(input, init);
587
+ if (!RETRYABLE_STATUS.has(response.status)) return response;
588
+ if (!options.retryable || last) return response;
589
+ const asked = retryAfterMs(response.headers.get("retry-after"), Date.now());
590
+ const delay = asked === void 0 ? backoffMs(attempt, options.retry) : Math.min(asked, ceiling);
591
+ if (!await pause(delay)) return response;
592
+ } catch (error) {
593
+ if (!options.retryable || last) throw error;
594
+ if (!await pause(backoffMs(attempt, options.retry))) throw error;
595
+ }
596
+ }
597
+ throw new Error("Request was never attempted");
598
+ };
599
+ return send;
600
+ }
601
+
602
+ // src/runtime.ts
603
+ var MAX_POLL_PAGES = 1e3;
604
+ async function runPoll(connector, triggerType, options = {}) {
605
+ const trigger = connector.triggers.find((entry) => entry.type === triggerType);
606
+ if (!trigger) {
607
+ throw new Error(`Connector ${connector.id} has no trigger "${triggerType}"`);
608
+ }
609
+ const now = options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
610
+ const polledAt = now();
611
+ const context = {
612
+ config: options.config ?? {},
613
+ ...options.since !== void 0 && { since: options.since },
614
+ ...options.cursor !== void 0 && { cursor: options.cursor },
615
+ ...options.limit !== void 0 && { limit: options.limit },
616
+ now,
617
+ // A poll only reads, so every failure it meets is worth trying again.
618
+ fetch: resilientFetch({
619
+ fetchImpl: options.fetchImpl ?? globalThis.fetch,
620
+ retryable: true,
621
+ ...options.retry !== void 0 && { retry: options.retry },
622
+ ...options.sleep !== void 0 && { sleep: options.sleep }
623
+ })
624
+ };
625
+ const outcome = typeof trigger.poll === "function" ? await trigger.poll(context) : await pollWithDedupe(trigger, context);
626
+ if (!outcome || !Array.isArray(outcome.items)) {
627
+ throw new Error(`Trigger ${triggerType} did not return an items array`);
628
+ }
629
+ if (outcome.hasMore && !outcome.nextCursor) {
630
+ throw new Error(`Trigger ${triggerType} reported more pages without a nextCursor`);
631
+ }
632
+ return {
633
+ items: normalizeItems(outcome.items, polledAt),
634
+ ...outcome.nextCursor !== void 0 && { nextCursor: outcome.nextCursor },
635
+ hasMore: outcome.hasMore === true
636
+ };
637
+ }
638
+ async function drainPoll(connector, triggerType, options = {}) {
639
+ const collected = [];
640
+ let cursor = options.cursor;
641
+ for (let page2 = 0; page2 < MAX_POLL_PAGES; page2++) {
642
+ const result = await runPoll(connector, triggerType, {
643
+ ...options,
644
+ ...cursor !== void 0 && { cursor }
645
+ });
646
+ collected.push(...result.items);
647
+ if (!result.hasMore) return collected;
648
+ if (result.nextCursor === cursor) {
649
+ throw new Error(`Trigger ${triggerType} did not advance its cursor`);
650
+ }
651
+ cursor = result.nextCursor;
652
+ }
653
+ throw new Error(`Trigger ${triggerType} exceeded ${MAX_POLL_PAGES} pages`);
654
+ }
655
+ var SAFE_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD"]);
656
+ async function runOptions(connector, name, options = {}) {
657
+ const loader = connector.options?.[name];
658
+ if (!loader) {
659
+ throw new Error(`Connector ${connector.id} serves no options set "${name}"`);
660
+ }
661
+ const loaded = await loader({
662
+ config: options.config ?? {},
663
+ now: options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString()),
664
+ fetch: resilientFetch({
665
+ fetchImpl: options.fetchImpl ?? globalThis.fetch,
666
+ retryable: true,
667
+ ...options.retry !== void 0 && { retry: options.retry },
668
+ ...options.sleep !== void 0 && { sleep: options.sleep }
669
+ })
670
+ });
671
+ if (!Array.isArray(loaded)) {
672
+ throw new Error(`Options set "${name}" did not return an array`);
673
+ }
674
+ return loaded.map(
675
+ (entry) => typeof entry === "string" ? { value: entry } : { ...entry, value: String(entry.value) }
676
+ );
677
+ }
678
+ var MAX_QUOTED_VALUE = 80;
679
+ function quote(value) {
680
+ return value.length > MAX_QUOTED_VALUE ? `${value.slice(0, MAX_QUOTED_VALUE)}\u2026` : value;
681
+ }
682
+ function coerceArg(value, type) {
683
+ if (typeof value !== "string") return value;
684
+ if (type === "number") {
685
+ const parsed = Number(value);
686
+ if (Number.isNaN(parsed)) throw new Error(`Expected a number, got "${quote(value)}"`);
687
+ return parsed;
688
+ }
689
+ if (type === "boolean") {
690
+ if (value === "true") return true;
691
+ if (value === "false") return false;
692
+ throw new Error(`Expected a boolean, got "${quote(value)}"`);
693
+ }
694
+ if (type === "json") {
695
+ try {
696
+ return JSON.parse(value);
697
+ } catch {
698
+ throw new Error(`Expected JSON, got "${quote(value)}"`);
699
+ }
700
+ }
701
+ return value;
702
+ }
703
+ async function runAction(connector, actionType, args, options = {}) {
704
+ const action = connector.actions.find((entry) => entry.type === actionType);
705
+ if (!action) {
706
+ throw new Error(`Connector ${connector.id} has no action "${actionType}"`);
707
+ }
708
+ const coerced = { ...args };
709
+ for (const input of action.inputs ?? []) {
710
+ const value = coerced[input.key];
711
+ if (value === void 0 || value === "") {
712
+ if (input.required) throw new Error(`Action ${actionType} requires "${input.key}"`);
713
+ delete coerced[input.key];
714
+ continue;
715
+ }
716
+ try {
717
+ coerced[input.key] = coerceArg(value, input.type);
718
+ } catch (error) {
719
+ throw new Error(
720
+ `Action ${actionType} argument "${input.key}": ${error instanceof Error ? error.message : String(error)}`,
721
+ { cause: error }
722
+ );
723
+ }
724
+ }
725
+ const config = options.config ?? {};
726
+ const method = (action.request?.method ?? "GET").toUpperCase();
727
+ const retryable = action.idempotent === true || action.request !== void 0 && SAFE_METHODS.has(method);
728
+ const fetchImpl = resilientFetch({
729
+ fetchImpl: options.fetchImpl ?? globalThis.fetch,
730
+ retryable,
731
+ ...options.retry !== void 0 && { retry: options.retry },
732
+ ...options.sleep !== void 0 && { sleep: options.sleep }
733
+ });
734
+ if (action.request !== void 0) {
735
+ try {
736
+ return await executeRequest(
737
+ action.request,
738
+ action.postReceive,
739
+ { args: coerced, config },
740
+ { fetchImpl }
741
+ );
742
+ } catch (error) {
743
+ throw new Error(
744
+ `Action ${actionType}: ${error instanceof Error ? error.message : String(error)}`,
745
+ { cause: error }
746
+ );
747
+ }
748
+ }
749
+ if (typeof action.run !== "function") {
750
+ throw new Error(`Action ${actionType} has neither a run() implementation nor a request`);
751
+ }
752
+ const output = await action.run(coerced, {
753
+ config,
754
+ now: options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString()),
755
+ fetch: fetchImpl
756
+ });
757
+ return output ?? {};
758
+ }
759
+
760
+ // src/define.ts
761
+ var KEY_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]*$/;
762
+ var PATH_DATA_PATTERN = /^[MmZzLlHhVvCcSsQqTtAa0-9\s,.\-+eE]+$/;
763
+ var VIEW_BOX_PATTERN = /^-?[\d.]+\s+-?[\d.]+\s+-?[\d.]+\s+-?[\d.]+$/;
764
+ var DEDUPE_STRATEGIES = ["timestamp", "lastItem"];
765
+ var AUTH_RUNGS = ["none", "cli", "key", "oauth"];
766
+ var ABSOLUTE_URL_PATTERN = /^https?:\/\//i;
767
+ var CONFIG_ROOTED_URL_PATTERN = /^\{\{\s*config\./;
768
+ function assertUnique(kind, keys) {
769
+ const seen = /* @__PURE__ */ new Set();
770
+ for (const key of keys) {
771
+ if (seen.has(key)) throw new Error(`Duplicate ${kind} "${key}"`);
772
+ seen.add(key);
773
+ }
774
+ }
775
+ function assertAuth(definition) {
776
+ const auth = definition.auth;
777
+ if (!auth) return;
778
+ const id = definition.id;
779
+ if (!AUTH_RUNGS.includes(auth.rung)) {
780
+ throw new Error(
781
+ `Connector ${id} declares unknown auth rung ${JSON.stringify(auth.rung)}; expected ${AUTH_RUNGS.join(", ")}`
782
+ );
783
+ }
784
+ if (auth.rung === "cli" && !auth.probe?.command?.trim()) {
785
+ throw new Error(`Connector ${id} borrows a CLI login but declares no probe command to ask it`);
786
+ }
787
+ if (auth.rung === "key") {
788
+ const keys = auth.keys ?? [];
789
+ if (keys.length === 0) {
790
+ throw new Error(`Connector ${id} signs in with a key but names no config field holding it`);
791
+ }
792
+ const declared = new Set((definition.config ?? []).map((field) => field.key));
793
+ for (const key of keys) {
794
+ if (!declared.has(key)) {
795
+ throw new Error(`Connector ${id} names auth key "${key}", which is not a config field`);
796
+ }
797
+ }
798
+ }
799
+ if (auth.rung === "none") {
800
+ const secret = (definition.config ?? []).find((field) => field.secret === true);
801
+ if (secret) {
802
+ throw new Error(
803
+ `Connector ${id} claims it needs no sign-in but declares secret field "${secret.key}"`
804
+ );
805
+ }
806
+ }
807
+ }
808
+ function envNameFor(key, explicit) {
809
+ if (explicit) return explicit;
810
+ return key.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[-\s]+/g, "_").toUpperCase();
811
+ }
812
+ function defineConnector(definition) {
813
+ if (!KEY_PATTERN.test(definition.id ?? "")) {
814
+ throw new Error(`Connector id "${definition.id}" must start with a letter and be url-safe`);
815
+ }
816
+ if (!definition.name?.trim()) {
817
+ throw new Error(`Connector ${definition.id} is missing a name`);
818
+ }
819
+ if (definition.icon) {
820
+ const { viewBox, paths } = definition.icon;
821
+ if (!Array.isArray(paths) || paths.length === 0) {
822
+ throw new Error(`Connector ${definition.id} has an icon with no paths`);
823
+ }
824
+ for (const path of paths) {
825
+ if (typeof path !== "string" || !PATH_DATA_PATTERN.test(path)) {
826
+ throw new Error(
827
+ `Connector ${definition.id} has an icon path that is not SVG path data. Only path data is accepted, not markup.`
828
+ );
829
+ }
830
+ }
831
+ if (viewBox !== void 0 && !VIEW_BOX_PATTERN.test(viewBox)) {
832
+ throw new Error(`Connector ${definition.id} has an icon viewBox that is not four numbers`);
833
+ }
834
+ }
835
+ const triggers = definition.triggers ?? [];
836
+ const actions = definition.actions ?? [];
837
+ if (triggers.length === 0 && actions.length === 0) {
838
+ throw new Error(`Connector ${definition.id} declares no triggers and no actions`);
839
+ }
840
+ for (const trigger of triggers) {
841
+ if (!KEY_PATTERN.test(trigger.type ?? "")) {
842
+ throw new Error(`Trigger type "${trigger.type}" must start with a letter and be url-safe`);
843
+ }
844
+ const loose = trigger;
845
+ const declarative = typeof loose.fetch === "function";
846
+ const imperative = typeof loose.poll === "function";
847
+ if (declarative && imperative) {
848
+ throw new Error(`Trigger ${trigger.type} declares both fetch() and poll(); pick one`);
849
+ }
850
+ if (declarative !== (loose.dedupe !== void 0)) {
851
+ throw new Error(
852
+ `Trigger ${trigger.type} needs fetch() and a dedupe strategy together, not one alone`
853
+ );
854
+ }
855
+ if (loose.dedupe !== void 0 && !DEDUPE_STRATEGIES.includes(loose.dedupe)) {
856
+ throw new Error(
857
+ `Trigger ${trigger.type} has unknown dedupe strategy ${JSON.stringify(loose.dedupe)}; expected ${DEDUPE_STRATEGIES.join(" or ")}`
858
+ );
859
+ }
860
+ if (loose.poll !== void 0 && !imperative) {
861
+ throw new Error(`Trigger ${trigger.type} declares poll but it is not a function`);
862
+ }
863
+ if (!declarative && !imperative) {
864
+ throw new Error(`Trigger ${trigger.type} is missing a fetch() or poll() implementation`);
865
+ }
866
+ }
867
+ for (const action of actions) {
868
+ if (!KEY_PATTERN.test(action.type ?? "")) {
869
+ throw new Error(`Action type "${action.type}" must start with a letter and be url-safe`);
870
+ }
871
+ const loose = action;
872
+ const written = typeof loose.run === "function";
873
+ const declared = loose.request !== void 0;
874
+ if (written && declared) {
875
+ throw new Error(`Action ${action.type} declares both run() and a request; pick one`);
876
+ }
877
+ if (!written && !declared) {
878
+ throw new Error(`Action ${action.type} is missing a run() implementation or a request`);
879
+ }
880
+ if (declared) {
881
+ const request = loose.request;
882
+ if (typeof request?.url !== "string" || request.url.trim() === "") {
883
+ throw new Error(`Action ${action.type} declares a request with no URL`);
884
+ }
885
+ const url = request.url.trim();
886
+ if (!ABSOLUTE_URL_PATTERN.test(url) && !CONFIG_ROOTED_URL_PATTERN.test(url)) {
887
+ throw new Error(
888
+ `Action ${action.type} declares the request URL "${url}", which is neither absolute nor rooted in a {{config.\u2026}} value`
889
+ );
890
+ }
891
+ }
892
+ if (!declared && loose.postReceive !== void 0) {
893
+ throw new Error(`Action ${action.type} has postReceive but no request for it to reshape`);
894
+ }
895
+ for (const input of action.inputs ?? []) {
896
+ if (input.loadOptions !== void 0 && definition.options?.[input.loadOptions] === void 0) {
897
+ throw new Error(
898
+ `Action ${action.type} argument "${input.key}" loads options from "${input.loadOptions}", which the connector does not serve`
899
+ );
900
+ }
901
+ }
902
+ }
903
+ assertUnique(
904
+ "trigger",
905
+ triggers.map((trigger) => trigger.type)
906
+ );
907
+ assertUnique(
908
+ "action",
909
+ actions.map((action) => action.type)
910
+ );
911
+ assertUnique(
912
+ "config field",
913
+ (definition.config ?? []).map((field) => field.key)
914
+ );
915
+ assertAuth(definition);
916
+ return {
917
+ ...definition,
918
+ version: definition.version ?? "0.0.0",
919
+ config: definition.config ?? [],
920
+ triggers,
921
+ actions
922
+ };
923
+ }
924
+ function resolveConfig(connector, env = process.env) {
925
+ const config = {};
926
+ const missing = [];
927
+ for (const field of connector.config) {
928
+ const name = envNameFor(field.key, field.env);
929
+ const value = env[name] ?? field.default;
930
+ if (value === void 0 || value === "") {
931
+ if (field.required) missing.push(`${field.key} (${name})`);
932
+ continue;
933
+ }
934
+ config[field.key] = value;
935
+ }
936
+ if (missing.length > 0) {
937
+ throw new Error(
938
+ `Connector ${connector.id} is missing required configuration: ${missing.join(", ")}`
939
+ );
940
+ }
941
+ return config;
942
+ }
943
+
944
+ // src/setup.ts
945
+ function pollToolName(triggerType) {
946
+ return `poll_${triggerType}`;
947
+ }
948
+ var MANIFEST_TOOL = "vorn_connector_manifest";
949
+ var PREFLIGHT_TOOL = "vorn_connector_preflight";
950
+ var OPTIONS_TOOL = "vorn_connector_options";
951
+ function connectionSetup(connector, triggerType) {
952
+ const trigger = connector.triggers.find((entry) => entry.type === triggerType);
953
+ if (!trigger) {
954
+ throw new Error(`Connector ${connector.id} has no trigger "${triggerType}"`);
955
+ }
956
+ return {
957
+ connectorId: connector.id,
958
+ triggerType,
959
+ filters: {
960
+ pollTool: pollToolName(triggerType),
961
+ itemsPath: "items",
962
+ idField: "externalId",
963
+ timestampField: "updatedAt",
964
+ titleField: "title",
965
+ urlField: "url",
966
+ cursorArg: "cursor",
967
+ cursorPath: "nextCursor"
968
+ },
969
+ env: connector.config.map((field) => ({
970
+ name: envNameFor(field.key, field.env),
971
+ required: field.required === true,
972
+ secret: field.secret === true,
973
+ ...field.description !== void 0 && { description: field.description },
974
+ ...field.builderHint !== void 0 && { builderHint: field.builderHint }
975
+ }))
976
+ };
977
+ }
978
+ function connectorManifest(connector) {
979
+ return {
980
+ id: connector.id,
981
+ name: connector.name,
982
+ version: connector.version,
983
+ ...connector.description !== void 0 && { description: connector.description },
984
+ ...connector.icon !== void 0 && { icon: connector.icon },
985
+ ...connector.auth !== void 0 && { auth: connector.auth },
986
+ triggers: connector.triggers.map((trigger) => ({
987
+ type: trigger.type,
988
+ label: trigger.label,
989
+ ...trigger.description !== void 0 && { description: trigger.description },
990
+ // Carried through so the app can seed a connection's status mapping and
991
+ // its polling workflow. Absent when the connector said nothing, which is
992
+ // different from saying there is nothing.
993
+ ...trigger.statusMapping !== void 0 && { statusMapping: trigger.statusMapping },
994
+ ...trigger.defaultWorkflow !== void 0 && { defaultWorkflow: trigger.defaultWorkflow },
995
+ setup: connectionSetup(connector, trigger.type)
996
+ })),
997
+ actions: connector.actions.map((action) => ({
998
+ type: action.type,
999
+ label: action.label,
1000
+ ...action.description !== void 0 && { description: action.description },
1001
+ inputs: (action.inputs ?? []).map((input) => ({
1002
+ key: input.key,
1003
+ label: input.label,
1004
+ type: input.type ?? "string",
1005
+ required: input.required === true,
1006
+ ...input.options !== void 0 && { options: input.options },
1007
+ ...input.loadOptions !== void 0 && { loadOptions: input.loadOptions },
1008
+ ...input.builderHint !== void 0 && { builderHint: input.builderHint }
1009
+ })),
1010
+ ...action.outputs !== void 0 && { outputs: action.outputs },
1011
+ ...action.sample !== void 0 && { sample: action.sample }
1012
+ }))
1013
+ };
1014
+ }
1015
+
1016
+ // src/harness.ts
1017
+ function matches(route, method, url) {
1018
+ if (route.method && route.method.toUpperCase() !== method) return false;
1019
+ if (route.url instanceof RegExp) return route.url.test(url);
1020
+ let pathname;
1021
+ try {
1022
+ pathname = new URL(url).pathname;
1023
+ } catch {
1024
+ return false;
1025
+ }
1026
+ return route.url.endsWith("/") ? pathname.startsWith(route.url) : pathname === route.url;
1027
+ }
1028
+ function reply(route) {
1029
+ const body = typeof route.body === "string" ? route.body : JSON.stringify(route.body ?? {});
1030
+ return new Response(body, {
1031
+ status: route.status ?? 200,
1032
+ headers: { "content-type": "application/json", ...route.headers }
1033
+ });
1034
+ }
1035
+ var MockRouteMissError = class extends Error {
1036
+ constructor(method, url) {
1037
+ super(`No mock route for ${method} ${url}`);
1038
+ this.name = "MockRouteMissError";
1039
+ }
1040
+ };
1041
+ function escapedMockHttp(error) {
1042
+ for (let current = error; current instanceof Error; current = current.cause) {
1043
+ if (current instanceof MockRouteMissError) return true;
1044
+ if (current.message.includes("No mock route for ")) return true;
1045
+ }
1046
+ return false;
1047
+ }
1048
+ var serving = false;
1049
+ async function withMockHttp(routes, body) {
1050
+ if (serving) {
1051
+ throw new Error(
1052
+ "withMockHttp is already serving; give one call every route it needs rather than installing a second stub"
1053
+ );
1054
+ }
1055
+ serving = true;
1056
+ const calls = [];
1057
+ const original = globalThis.fetch;
1058
+ globalThis.fetch = (async (input, init) => {
1059
+ const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
1060
+ const own = typeof input === "object" && "method" in input ? input.method : void 0;
1061
+ const method = (init?.method ?? own ?? "GET").toUpperCase();
1062
+ calls.push({
1063
+ method,
1064
+ url,
1065
+ ...typeof init?.body === "string" && { body: init.body }
1066
+ });
1067
+ const route = routes.find((candidate) => matches(candidate, method, url));
1068
+ if (!route) throw new MockRouteMissError(method, url);
1069
+ return reply(route);
1070
+ });
1071
+ try {
1072
+ return { result: await body(), calls };
1073
+ } finally {
1074
+ globalThis.fetch = original;
1075
+ serving = false;
1076
+ }
1077
+ }
1078
+ function createConnectorHarness(connector, harnessOptions = {}) {
1079
+ const defaults = (options = {}) => ({
1080
+ ...harnessOptions.config && { config: harnessOptions.config },
1081
+ ...harnessOptions.now && { now: harnessOptions.now },
1082
+ ...harnessOptions.fetchImpl && { fetchImpl: harnessOptions.fetchImpl },
1083
+ ...harnessOptions.sleep && { sleep: harnessOptions.sleep },
1084
+ ...options
1085
+ });
1086
+ return {
1087
+ poll: (triggerType, options) => runPoll(connector, triggerType, defaults(options)),
1088
+ drain: (triggerType, options) => drainPoll(connector, triggerType, defaults(options)),
1089
+ execute: (actionType, args = {}) => runAction(connector, actionType, args, {
1090
+ ...harnessOptions.config && { config: harnessOptions.config },
1091
+ ...harnessOptions.now && { now: harnessOptions.now },
1092
+ ...harnessOptions.fetchImpl && { fetchImpl: harnessOptions.fetchImpl },
1093
+ ...harnessOptions.sleep && { sleep: harnessOptions.sleep }
1094
+ }),
1095
+ manifest: () => connectorManifest(connector),
1096
+ async pollTwice(triggerType, options) {
1097
+ const first = await drainPoll(connector, triggerType, defaults(options));
1098
+ const watermark = first.reduce(
1099
+ (newest, item) => newest === void 0 || item.updatedAt > newest ? item.updatedAt : newest,
1100
+ options?.since
1101
+ );
1102
+ const second = await drainPoll(
1103
+ connector,
1104
+ triggerType,
1105
+ defaults({ ...options, ...watermark !== void 0 && { since: watermark } })
1106
+ );
1107
+ return watermark === void 0 ? second : second.filter((item) => item.updatedAt > watermark);
1108
+ },
1109
+ withMockHttp
1110
+ };
1111
+ }
1112
+
1113
+ // src/check.ts
1114
+ var EXECUTABLE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
1115
+ var CREDENTIAL_NAME = /(secret|token|password|passphrase|api[-_]?key|credential)/i;
1116
+ var INPUT_TYPES = /* @__PURE__ */ new Set(["string", "number", "boolean", "select", "json"]);
1117
+ function finding2(level, code, target, message) {
1118
+ return { level, code, target, message };
1119
+ }
1120
+ function authFindings(connector) {
1121
+ const auth = connector.auth;
1122
+ if (!auth) {
1123
+ return [
1124
+ finding2(
1125
+ "warn",
1126
+ "auth-undeclared",
1127
+ connector.id,
1128
+ "does not say how it signs in, so the app cannot tell anyone before they install it"
1129
+ )
1130
+ ];
1131
+ }
1132
+ const found = [];
1133
+ const command = auth.probe?.command?.trim() ?? "";
1134
+ const args = auth.probe?.args ?? [];
1135
+ if (auth.rung === "cli") {
1136
+ if (!EXECUTABLE_NAME.test(command)) {
1137
+ found.push(
1138
+ finding2(
1139
+ "error",
1140
+ "auth-probe-missing",
1141
+ `${connector.id} auth`,
1142
+ `probe command "${command}" is not a bare executable name, so the host drops it and the rung promises a sign-in it cannot ask for`
1143
+ )
1144
+ );
1145
+ }
1146
+ if (args.some((arg) => typeof arg !== "string")) {
1147
+ found.push(
1148
+ finding2(
1149
+ "error",
1150
+ "auth-probe-missing",
1151
+ `${connector.id} auth`,
1152
+ "probe arguments must all be strings, or the host drops the probe"
1153
+ )
1154
+ );
1155
+ }
1156
+ }
1157
+ return found;
1158
+ }
1159
+ function secretFindings(connector) {
1160
+ const named = new Set(connector.auth?.keys ?? []);
1161
+ return connector.config.filter((field) => !field.secret).filter((field) => named.has(field.key) || CREDENTIAL_NAME.test(field.key)).map(
1162
+ (field) => finding2(
1163
+ named.has(field.key) ? "error" : "warn",
1164
+ "secret-not-marked",
1165
+ `config ${field.key}`,
1166
+ "holds a credential but is not marked `secret`, so Vorn would store it unencrypted"
1167
+ )
1168
+ );
1169
+ }
1170
+ function actionShapeFindings(action) {
1171
+ const target = `action ${action.type}`;
1172
+ const found = [];
1173
+ if (!action.outputs?.length) {
1174
+ found.push(
1175
+ finding2(
1176
+ "warn",
1177
+ "action-no-outputs",
1178
+ target,
1179
+ "declares no outputs, so a later step has nothing to autocomplete from"
1180
+ )
1181
+ );
1182
+ }
1183
+ for (const input of action.inputs ?? []) {
1184
+ if (input.type !== void 0 && !INPUT_TYPES.has(input.type)) {
1185
+ found.push(
1186
+ finding2(
1187
+ "error",
1188
+ "input-type-unsupported",
1189
+ `${target} input ${input.key}`,
1190
+ `declares type "${input.type}", which Vorn cannot draw a field for`
1191
+ )
1192
+ );
1193
+ }
1194
+ if (input.type === "select" && !input.options?.length && !input.loadOptions) {
1195
+ found.push(
1196
+ finding2(
1197
+ "error",
1198
+ "input-type-unsupported",
1199
+ `${target} input ${input.key}`,
1200
+ "is a select with neither fixed options nor a loadOptions set to draw from"
1201
+ )
1202
+ );
1203
+ }
1204
+ }
1205
+ return found;
1206
+ }
1207
+ async function packageFindings(options) {
1208
+ if (options.packageDir === void 0) return [];
1209
+ const pkg = readNearestPackageJson(packageDirFor(options.packageDir, options.entry));
1210
+ const found = [...lifecycleScriptFindings(pkg)];
1211
+ const vorn = pkg?.vorn;
1212
+ const keywords = Array.isArray(vorn?.keywords) ? vorn.keywords : [];
1213
+ if (keywords.length === 0) {
1214
+ found.push(
1215
+ finding2(
1216
+ "warn",
1217
+ "keywords-missing",
1218
+ "package.json",
1219
+ "names no keywords, so the connector is findable only by its own name"
1220
+ )
1221
+ );
1222
+ }
1223
+ if (options.bundle && options.entry !== void 0) {
1224
+ const built = await options.bundle({
1225
+ contents: packEntryContents(options.entry),
1226
+ resolveDir: options.packageDir
1227
+ });
1228
+ found.push(...bundleDependencyFindings(built.external));
1229
+ }
1230
+ return found;
1231
+ }
1232
+ function sampleArg(input) {
1233
+ if (input.type === "number") return "1";
1234
+ if (input.type === "boolean") return "false";
1235
+ if (input.type === "json") return "{}";
1236
+ if (input.type === "select") return input.options?.[0]?.value ?? "check";
1237
+ return "check";
1238
+ }
1239
+ function mockConfig(connector) {
1240
+ const config = {};
1241
+ for (const field of connector.config) {
1242
+ config[field.key] = field.default ?? `mock-${field.key}`;
1243
+ }
1244
+ return config;
1245
+ }
1246
+ async function mockFindings(connector, options) {
1247
+ if (!options.mock) return [];
1248
+ const config = options.config ?? mockConfig(connector);
1249
+ const routes = options.mockRoutes ?? [{ url: /.*/ }];
1250
+ const level = options.mockRoutes?.length ? "error" : "warn";
1251
+ const found = [];
1252
+ for (const action of connector.actions) {
1253
+ const args = Object.fromEntries(
1254
+ (action.inputs ?? []).map((input) => [input.key, sampleArg(input)])
1255
+ );
1256
+ const { result: thrown, calls } = await withMockHttp(routes, async () => {
1257
+ try {
1258
+ await runAction(connector, action.type, args, {
1259
+ config,
1260
+ ...options.now && { now: options.now }
1261
+ });
1262
+ return void 0;
1263
+ } catch (error) {
1264
+ return error;
1265
+ }
1266
+ });
1267
+ if (thrown !== void 0) {
1268
+ const reason = thrown instanceof Error ? thrown.message : String(thrown);
1269
+ const escaped = escapedMockHttp(thrown);
1270
+ found.push(
1271
+ finding2(
1272
+ escaped ? "error" : level,
1273
+ escaped ? "mock-network-escape" : "mock-action-failed",
1274
+ `action ${action.type}`,
1275
+ `did not run against served HTTP: ${reason}`
1276
+ )
1277
+ );
1278
+ continue;
1279
+ }
1280
+ if (calls.length === 0) {
1281
+ found.push(
1282
+ finding2(
1283
+ "warn",
1284
+ "mock-not-observed",
1285
+ `action ${action.type}`,
1286
+ "made no request the stub could see, so this run vouches for nothing it did"
1287
+ )
1288
+ );
1289
+ }
1290
+ }
1291
+ return found;
1292
+ }
1293
+ var AUTH_FAILURE = /\b(401|403|unauthor|unauthenticat|forbidden|invalid[- ]?(token|credential))/i;
1294
+ function liveRunnable(action) {
1295
+ if (action.idempotent !== true) return false;
1296
+ if (action.sample !== void 0) return true;
1297
+ return !(action.inputs ?? []).some((input) => input.required === true);
1298
+ }
1299
+ function liveExamines(connector) {
1300
+ return connector.preflight !== void 0 || connector.actions.some(liveRunnable);
1301
+ }
1302
+ async function liveFindings(connector, options) {
1303
+ if (!options.live) return [];
1304
+ const found = [];
1305
+ if (connector.preflight) {
1306
+ try {
1307
+ const result = await connector.preflight();
1308
+ if (!result.ok) {
1309
+ found.push(
1310
+ finding2(
1311
+ "error",
1312
+ "preflight-failed",
1313
+ connector.id,
1314
+ result.message ?? "reported that it is not ready, without saying why"
1315
+ )
1316
+ );
1317
+ return found;
1318
+ }
1319
+ } catch (error) {
1320
+ const reason = error instanceof Error ? error.message : String(error);
1321
+ found.push(finding2("error", "preflight-failed", connector.id, `threw: ${reason}`));
1322
+ return found;
1323
+ }
1324
+ }
1325
+ for (const action of connector.actions.filter(liveRunnable)) {
1326
+ try {
1327
+ await runAction(connector, action.type, action.sample ?? {}, {
1328
+ config: options.config ?? {},
1329
+ ...options.now && { now: options.now }
1330
+ });
1331
+ } catch (error) {
1332
+ const reason = error instanceof Error ? error.message : String(error);
1333
+ found.push(
1334
+ finding2(
1335
+ AUTH_FAILURE.test(reason) ? "error" : "warn",
1336
+ "live-action-failed",
1337
+ `action ${action.type}`,
1338
+ `threw: ${reason}`
1339
+ )
1340
+ );
1341
+ }
1342
+ }
1343
+ return found;
1344
+ }
1345
+ function sampleTrigger(trigger) {
1346
+ return { ...trigger, poll: void 0, fetch: () => trigger.sample ?? [] };
1347
+ }
1348
+ async function checkPollBehaviour(connector, trigger, options) {
1349
+ const found = [];
1350
+ const probe = { ...connector, triggers: [trigger] };
1351
+ const target = `trigger ${trigger.type}`;
1352
+ const now = options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
1353
+ const attempt = async (cursor, code, what) => {
1354
+ try {
1355
+ return await runPoll(probe, trigger.type, {
1356
+ config: options.config ?? {},
1357
+ ...cursor !== void 0 && { cursor },
1358
+ now
1359
+ });
1360
+ } catch (error) {
1361
+ const reason = error instanceof Error ? error.message : String(error);
1362
+ return finding2("error", code, target, `${what} threw: ${reason}`);
1363
+ }
1364
+ };
1365
+ const first = await attempt(void 0, "poll-failed", "first poll");
1366
+ if ("level" in first) return [first];
1367
+ if (first.items.length === 0) {
1368
+ found.push(
1369
+ finding2("warn", "no-items", target, "returned nothing, so delivery could not be verified")
1370
+ );
1371
+ return found;
1372
+ }
1373
+ if (first.nextCursor === void 0) {
1374
+ found.push(
1375
+ finding2(
1376
+ "error",
1377
+ "no-cursor",
1378
+ target,
1379
+ "returned items but no nextCursor, so every poll will redeliver them"
1380
+ )
1381
+ );
1382
+ return found;
1383
+ }
1384
+ const second = await attempt(
1385
+ first.nextCursor,
1386
+ "cursor-rejected",
1387
+ "re-polling with its own nextCursor"
1388
+ );
1389
+ if ("level" in second) return [...found, second];
1390
+ const delivered = new Set(first.items.map((item) => item.externalId));
1391
+ const repeated = second.items.filter((item) => delivered.has(item.externalId));
1392
+ if (repeated.length > 0) {
1393
+ found.push(
1394
+ finding2(
1395
+ "error",
1396
+ "redelivers-items",
1397
+ target,
1398
+ `re-polling with its own nextCursor returned ${repeated.length} already-delivered item(s), starting with "${repeated[0].externalId}"`
1399
+ )
1400
+ );
1401
+ }
1402
+ if (second.hasMore && second.nextCursor === first.nextCursor) {
1403
+ found.push(
1404
+ finding2("error", "stuck-cursor", target, "reports more pages but its cursor never advances")
1405
+ );
1406
+ }
1407
+ return found;
1408
+ }
1409
+ async function checkConnector(connector, options = {}) {
1410
+ const found = [];
1411
+ if (!connector.description?.trim()) {
1412
+ found.push(
1413
+ finding2(
1414
+ "warn",
1415
+ "missing-description",
1416
+ connector.id,
1417
+ "has no description; agents use it to decide when the connector applies"
1418
+ )
1419
+ );
1420
+ }
1421
+ found.push(...authFindings(connector));
1422
+ found.push(...secretFindings(connector));
1423
+ found.push(...await packageFindings(options));
1424
+ found.push(...await mockFindings(connector, options));
1425
+ found.push(...await liveFindings(connector, options));
1426
+ const perTrigger = await Promise.all(
1427
+ connector.triggers.map(async (trigger) => {
1428
+ const target = `trigger ${trigger.type}`;
1429
+ const triggerFindings = [];
1430
+ if (!trigger.description?.trim()) {
1431
+ triggerFindings.push(finding2("warn", "missing-description", target, "has no description"));
1432
+ }
1433
+ if (options.live) {
1434
+ triggerFindings.push(...await checkPollBehaviour(connector, trigger, options));
1435
+ } else if (!trigger.sample?.length) {
1436
+ triggerFindings.push(
1437
+ finding2(
1438
+ "warn",
1439
+ "unverifiable",
1440
+ target,
1441
+ "has no sample items and no credentials were supplied, so nothing could be verified"
1442
+ )
1443
+ );
1444
+ } else if (!trigger.dedupe) {
1445
+ triggerFindings.push(
1446
+ finding2(
1447
+ "warn",
1448
+ "sample-unusable",
1449
+ target,
1450
+ "declares sample items but implements poll() directly, so they cannot be replayed; re-run with --live"
1451
+ )
1452
+ );
1453
+ } else {
1454
+ triggerFindings.push(
1455
+ ...await checkPollBehaviour(connector, sampleTrigger(trigger), options)
1456
+ );
1457
+ }
1458
+ return triggerFindings;
1459
+ })
1460
+ );
1461
+ found.push(...perTrigger.flat());
1462
+ for (const action of connector.actions) {
1463
+ const target = `action ${action.type}`;
1464
+ if (!action.description?.trim()) {
1465
+ found.push(
1466
+ finding2("warn", "missing-description", target, "has no description for the agent to read")
1467
+ );
1468
+ }
1469
+ if (action.idempotent === void 0) {
1470
+ found.push(
1471
+ finding2(
1472
+ "warn",
1473
+ "missing-idempotent",
1474
+ target,
1475
+ "does not declare `idempotent`, so an agent cannot tell whether retrying is safe"
1476
+ )
1477
+ );
1478
+ }
1479
+ found.push(...actionShapeFindings(action));
1480
+ for (const input of action.inputs ?? []) {
1481
+ if (!input.description?.trim()) {
1482
+ found.push(
1483
+ finding2(
1484
+ "warn",
1485
+ "missing-description",
1486
+ `${target} input ${input.key}`,
1487
+ "has no description"
1488
+ )
1489
+ );
1490
+ }
1491
+ }
1492
+ }
1493
+ return found;
1494
+ }
1495
+ var CHECK_OWNERS = {
1496
+ "pack-too-large": null,
1497
+ "missing-description": "manifest",
1498
+ "auth-undeclared": "auth",
1499
+ "auth-probe-missing": "auth",
1500
+ "secret-not-marked": "secrets",
1501
+ "action-no-outputs": "actions",
1502
+ "input-type-unsupported": "actions",
1503
+ "missing-idempotent": "actions",
1504
+ "poll-failed": "dedupe",
1505
+ "no-items": "dedupe",
1506
+ "no-cursor": "dedupe",
1507
+ "cursor-rejected": "dedupe",
1508
+ "redelivers-items": "dedupe",
1509
+ "stuck-cursor": "dedupe",
1510
+ unverifiable: "dedupe",
1511
+ "sample-unusable": "dedupe",
1512
+ "lifecycle-scripts": "no-lifecycle-scripts",
1513
+ "keywords-missing": "keywords",
1514
+ "runtime-dependencies": "no-runtime-deps",
1515
+ "mock-action-failed": "mock",
1516
+ "mock-network-escape": "mock",
1517
+ "mock-not-observed": "mock",
1518
+ "preflight-failed": "live",
1519
+ "live-action-failed": "live"
1520
+ };
1521
+ function checksRun(connector, options) {
1522
+ const names = ["manifest", "auth"];
1523
+ if (connector.config.length > 0) names.push("secrets");
1524
+ if (connector.actions.length > 0) names.push("actions");
1525
+ if (connector.triggers.length > 0) names.push("dedupe");
1526
+ if (options.packageDir !== void 0) names.push("no-lifecycle-scripts", "keywords");
1527
+ if (options.bundle && options.entry !== void 0) names.push("no-runtime-deps");
1528
+ if (options.mock && connector.actions.length > 0) names.push("mock");
1529
+ if (options.live && liveExamines(connector)) names.push("live");
1530
+ return names;
1531
+ }
1532
+ async function runConformance(connector, options = {}) {
1533
+ const findings = await checkConnector(connector, options);
1534
+ const spoiled = new Set(findings.map((item) => CHECK_OWNERS[item.code]).filter(Boolean));
1535
+ const passed = checksRun(connector, options).filter((name) => !spoiled.has(name));
1536
+ const failed = findings.some((item) => item.level === "error");
1537
+ const now = options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
1538
+ return {
1539
+ findings,
1540
+ passed,
1541
+ // A receipt listing nothing would read as verified while vouching for
1542
+ // nothing at all, which is worse than saying nothing.
1543
+ ...!failed && passed.length > 0 && {
1544
+ receipt: {
1545
+ schema: 1,
1546
+ version: connector.version,
1547
+ checkedAt: now(),
1548
+ checks: passed
1549
+ }
1550
+ }
1551
+ };
1552
+ }
1553
+ function formatFindings(findings) {
1554
+ return findings.map((item) => `${item.level.padEnd(5)} ${item.target}: ${item.message} [${item.code}]`).join("\n");
1555
+ }
1556
+
1557
+ // src/pack.ts
1558
+ import { mkdtemp, mkdir, rm, stat, writeFile } from "fs/promises";
1559
+ import { tmpdir } from "os";
1560
+ import { join as join2, resolve as resolve2 } from "path";
1561
+ function finding3(code, target, message) {
1562
+ return { level: "error", code, target, message };
1563
+ }
1564
+ function packFileName(connector) {
1565
+ return `${connector.id}-${connector.version}.vorn.tgz`;
1566
+ }
1567
+ async function packConnector(connector, options) {
1568
+ const resolveDir = resolve2(options.resolveDir ?? process.cwd());
1569
+ const entryDir = packageDirFor(resolveDir, options.entry);
1570
+ const findings = await checkConnector(connector);
1571
+ findings.push(...lifecycleScriptFindings(readNearestPackageJson(entryDir)));
1572
+ if (findings.some((item) => item.level === "error")) return { findings };
1573
+ const contents = packEntryContents(options.entry, options.sdkModule);
1574
+ const bundle = options.bundle ?? esbuildBundle;
1575
+ const built = await bundle({ contents, resolveDir });
1576
+ findings.push(...bundleDependencyFindings(built.external));
1577
+ if (findings.some((item) => item.level === "error")) return { findings };
1578
+ const outDir = resolve2(options.outDir ?? process.cwd());
1579
+ await mkdir(outDir, { recursive: true });
1580
+ const file = join2(outDir, packFileName(connector));
1581
+ const staging = await mkdtemp(join2(tmpdir(), "vorn-pack-"));
1582
+ try {
1583
+ await writeFile(join2(staging, "index.js"), built.code, "utf8");
1584
+ await writeFile(
1585
+ join2(staging, "manifest.json"),
1586
+ `${JSON.stringify(connectorManifest(connector), null, 2)}
1587
+ `,
1588
+ "utf8"
1589
+ );
1590
+ const { create } = await import("tar");
1591
+ await create({ gzip: true, file, cwd: staging }, ["manifest.json", "index.js"]);
1592
+ } finally {
1593
+ await rm(staging, { recursive: true, force: true });
1594
+ }
1595
+ const bytes = (await stat(file)).size;
1596
+ const maxBytes = options.maxBytes ?? MAX_PACK_BYTES;
1597
+ if (bytes > maxBytes) {
1598
+ await rm(file, { force: true });
1599
+ return {
1600
+ findings: [
1601
+ ...findings,
1602
+ finding3(
1603
+ "pack-too-large",
1604
+ "bundle",
1605
+ `The pack is ${Math.round(bytes / 1024)} KB; Vorn installs at most ${Math.round(maxBytes / 1024)} KB`
1606
+ )
1607
+ ]
1608
+ };
1609
+ }
1610
+ return { findings, file, bytes };
1611
+ }
1612
+
1613
+ // src/scaffold.ts
1614
+ var ID_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]*$/;
1615
+ var SDK_DEPENDENCY_RANGE = "^0.7.0-beta.9";
1616
+ var SCAFFOLD_VERSION = "0.1.0";
1617
+ var VITEST_RANGE = "^4.1.10";
1618
+ function jsonFile(value) {
1619
+ return `${JSON.stringify(value, null, 2)}
1620
+ `;
1621
+ }
1622
+ function titleCase(id) {
1623
+ return id.split(/[-_]+/).filter((part) => part !== "").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
1624
+ }
1625
+ function packageJson(id, description, inRepo) {
1626
+ return jsonFile({
1627
+ name: inRepo ? `@vornrun/connector-${id}` : `vorn-connector-${id}`,
1628
+ version: SCAFFOLD_VERSION,
1629
+ description,
1630
+ type: "module",
1631
+ license: "MIT",
1632
+ bin: { [`vorn-connector-${id}`]: "dist/index.js" },
1633
+ main: "./dist/index.js",
1634
+ types: "./dist/index.d.ts",
1635
+ files: ["dist", "README.md", ...inRepo ? ["CHANGELOG.md"] : []],
1636
+ ...inRepo && {
1637
+ repository: {
1638
+ type: "git",
1639
+ url: "git+https://github.com/vorn-run/connectors.git",
1640
+ directory: `packages/${id}`
1641
+ }
1642
+ },
1643
+ scripts: {
1644
+ // In the repository the config file is the one definition of the build.
1645
+ build: inRepo ? "tsup" : "tsup src/index.ts --format esm --target node22 --clean --dts",
1646
+ check: "vorn-connector check src/index.ts",
1647
+ pack: "vorn-connector pack src/index.ts",
1648
+ test: "vitest run",
1649
+ typecheck: "tsc --noEmit"
1650
+ },
1651
+ dependencies: { "@vornrun/connector-sdk": SDK_DEPENDENCY_RANGE },
1652
+ devDependencies: {
1653
+ ...inRepo && { "@types/node": "^22.10.2", "@vitest/coverage-v8": VITEST_RANGE },
1654
+ tsup: "^8.5.1",
1655
+ typescript: "^6.0.3",
1656
+ vitest: VITEST_RANGE
1657
+ },
1658
+ // Read by the catalog build: how this connector is filed, found, and what it asks of you.
1659
+ vorn: {
1660
+ category: "Other",
1661
+ keywords: [id],
1662
+ ...inRepo && { auth: "Say in one line what signing in takes." }
1663
+ }
1664
+ });
1665
+ }
1666
+ function tsconfig() {
1667
+ return jsonFile({
1668
+ compilerOptions: {
1669
+ target: "ES2022",
1670
+ lib: ["ES2022"],
1671
+ module: "ESNext",
1672
+ moduleResolution: "bundler",
1673
+ allowSyntheticDefaultImports: true,
1674
+ esModuleInterop: true,
1675
+ strict: true,
1676
+ skipLibCheck: true,
1677
+ types: ["node"],
1678
+ noEmit: true,
1679
+ ignoreDeprecations: "6.0",
1680
+ allowImportingTsExtensions: true
1681
+ },
1682
+ include: ["src/**/*", "vitest.config.ts"]
1683
+ });
1684
+ }
1685
+ function tsupConfig() {
1686
+ return `import { defineConfig } from 'tsup'
1687
+
1688
+ export default defineConfig({
1689
+ entry: ['src/index.ts'],
1690
+ format: ['esm'],
1691
+ target: 'node22',
1692
+ clean: true,
1693
+ dts: true,
1694
+ // Vorn spawns the built file directly.
1695
+ banner: { js: '#!/usr/bin/env node' }
1696
+ })
1697
+ `;
1698
+ }
1699
+ function vitestConfig() {
1700
+ return `import shared from '../../vitest.shared.ts'
1701
+
1702
+ export default shared
1703
+ `;
1704
+ }
1705
+ function changelog() {
1706
+ return `# Changelog
1707
+
1708
+ ## ${SCAFFOLD_VERSION}
1709
+
1710
+ - First release.
1711
+ `;
1712
+ }
1713
+ function connectorSource(id, name, description) {
1714
+ return `import { defineConnector } from '@vornrun/connector-sdk'
1715
+
1716
+ export const connector = defineConnector({
1717
+ id: ${JSON.stringify(id)},
1718
+ name: ${JSON.stringify(name)},
1719
+ description: ${JSON.stringify(description)},
1720
+ version: '${SCAFFOLD_VERSION}',
1721
+ // Prefer a login the machine already has: { rung: 'cli', probe: { command: 'tool', args: ['auth', 'status'] } }
1722
+ auth: { rung: 'key', keys: ['apiToken'] },
1723
+ config: [
1724
+ {
1725
+ key: 'apiToken',
1726
+ label: 'API token',
1727
+ required: true,
1728
+ secret: true,
1729
+ builderHint: 'Say where a token is created and which scopes it needs'
1730
+ },
1731
+ { key: 'baseUrl', label: 'Base URL', default: 'https://api.example.com' }
1732
+ ],
1733
+ triggers: [
1734
+ {
1735
+ type: 'itemCreated',
1736
+ label: 'Item created',
1737
+ description: 'Items created since the last poll',
1738
+ // Return what is there; the SDK handles cursors and de-duplication.
1739
+ dedupe: 'timestamp',
1740
+ async fetch(context) {
1741
+ const url = new URL('/v1/items', context.config.baseUrl)
1742
+ if (context.since) url.searchParams.set('updated_since', context.since)
1743
+ // \`context.fetch\` retries and backs off; the global one does not.
1744
+ const response = await context.fetch(url, {
1745
+ headers: { authorization: 'Bearer ' + context.config.apiToken }
1746
+ })
1747
+ if (!response.ok) throw new Error('Listing items failed with ' + response.status)
1748
+ const body = (await response.json()) as { items: Array<Record<string, string>> }
1749
+ return body.items.map((item) => ({
1750
+ externalId: item.id,
1751
+ title: item.title,
1752
+ url: item.html_url,
1753
+ updatedAt: item.updated_at
1754
+ }))
1755
+ }
1756
+ }
1757
+ ],
1758
+ actions: [
1759
+ {
1760
+ type: 'createItem',
1761
+ label: 'Create item',
1762
+ description: 'Create one item',
1763
+ inputs: [
1764
+ { key: 'title', label: 'Title', required: true },
1765
+ { key: 'body', label: 'Body' }
1766
+ ],
1767
+ outputs: [{ key: 'id', type: 'string', description: 'The created item' }],
1768
+ // Declared, not written: the SDK fills the templates, sends it, and keeps what postReceive names.
1769
+ request: {
1770
+ method: 'POST',
1771
+ url: '{{config.baseUrl}}/v1/items',
1772
+ headers: { authorization: 'Bearer {{config.apiToken}}' },
1773
+ body: { title: '{{args.title}}', body: '{{args.body}}' }
1774
+ },
1775
+ postReceive: [{ op: 'pick', keys: ['id'] }]
1776
+ }
1777
+ ]
1778
+ })
1779
+ `;
1780
+ }
1781
+ function entrySource() {
1782
+ return `import { realpathSync } from 'node:fs'
1783
+ import { fileURLToPath } from 'node:url'
1784
+ import { serveConnector } from '@vornrun/connector-sdk'
1785
+ import { connector } from './connector'
1786
+
1787
+ /** True when this file was run directly rather than imported. */
1788
+ export function isEntryPoint(moduleUrl: string, argv = process.argv): boolean {
1789
+ const invoked = argv[1]
1790
+ if (invoked === undefined) return false
1791
+ try {
1792
+ return realpathSync(fileURLToPath(moduleUrl)) === realpathSync(invoked)
1793
+ } catch {
1794
+ return false
1795
+ }
1796
+ }
1797
+
1798
+ /** Serve on stdio when run directly, which is what Vorn spawns; says whether it did. */
1799
+ export async function serveIfEntryPoint(
1800
+ moduleUrl: string,
1801
+ serve: (c: typeof connector) => Promise<void> = serveConnector
1802
+ ): Promise<boolean> {
1803
+ if (!isEntryPoint(moduleUrl)) return false
1804
+ await serve(connector)
1805
+ return true
1806
+ }
1807
+ `;
1808
+ }
1809
+ function entryTestSource() {
1810
+ return `import { describe, expect, it, vi } from 'vitest'
1811
+ import { realpathSync } from 'node:fs'
1812
+ import { fileURLToPath } from 'node:url'
1813
+ import { isEntryPoint, serveIfEntryPoint } from './entry'
1814
+ import connector, { connector as named } from './index'
1815
+
1816
+ const HERE = import.meta.url
1817
+
1818
+ describe('isEntryPoint', () => {
1819
+ it('is false when the process was started without a script', () => {
1820
+ expect(isEntryPoint(HERE, ['node'])).toBe(false)
1821
+ })
1822
+
1823
+ it('is true when argv points at this module, through a symlink or not', () => {
1824
+ expect(isEntryPoint(HERE, ['node', fileURLToPath(HERE)])).toBe(true)
1825
+ expect(isEntryPoint(HERE, ['node', realpathSync(fileURLToPath(HERE))])).toBe(true)
1826
+ })
1827
+
1828
+ it('is false when the module is running under the test runner', () => {
1829
+ expect(isEntryPoint(HERE)).toBe(false)
1830
+ })
1831
+
1832
+ it('is false rather than throwing when a path cannot be resolved', () => {
1833
+ expect(isEntryPoint(HERE, ['node', '/nowhere/that/exists'])).toBe(false)
1834
+ })
1835
+ })
1836
+
1837
+ describe('serveIfEntryPoint', () => {
1838
+ it('starts nothing when the module was merely imported', async () => {
1839
+ const serve = vi.fn(async () => {})
1840
+ expect(await serveIfEntryPoint(HERE, serve)).toBe(false)
1841
+ expect(serve).not.toHaveBeenCalled()
1842
+ })
1843
+ })
1844
+
1845
+ describe('the packaged connector', () => {
1846
+ it('is the same connector under both exports', () => {
1847
+ expect(connector).toBe(named)
1848
+ expect(connector.version).toMatch(/^\\d+\\.\\d+\\.\\d+/)
1849
+ })
1850
+ })
1851
+ `;
1852
+ }
1853
+ function indexSource() {
1854
+ return `import { connector } from './connector'
1855
+ import { serveIfEntryPoint } from './entry'
1856
+
1857
+ export { connector }
1858
+ export default connector
1859
+
1860
+ await serveIfEntryPoint(import.meta.url)
1861
+ `;
1862
+ }
1863
+ function testSource(name) {
1864
+ return `import { describe, expect, it, vi } from 'vitest'
1865
+ import { createConnectorHarness } from '@vornrun/connector-sdk'
1866
+ import { connector } from './connector'
1867
+
1868
+ /** Answers the connector's calls from here, so the test needs no network. */
1869
+ function fakeFetch(body: unknown) {
1870
+ return vi.fn(async () =>
1871
+ new Response(JSON.stringify(body), { headers: { 'content-type': 'application/json' } })
1872
+ ) as unknown as typeof fetch
1873
+ }
1874
+
1875
+ const config = { apiToken: 'test-token', baseUrl: 'https://api.example.com' }
1876
+
1877
+ describe(${JSON.stringify(name)}, () => {
1878
+ it('reports the items the source lists', async () => {
1879
+ const harness = createConnectorHarness(connector, {
1880
+ config,
1881
+ fetchImpl: fakeFetch({
1882
+ items: [
1883
+ {
1884
+ id: '1',
1885
+ title: 'First item',
1886
+ html_url: 'https://example.com/1',
1887
+ updated_at: '2026-01-01T00:00:00.000Z'
1888
+ }
1889
+ ]
1890
+ })
1891
+ })
1892
+
1893
+ const page = await harness.poll('itemCreated')
1894
+
1895
+ expect(page.items).toHaveLength(1)
1896
+ expect(page.items[0].externalId).toBe('1')
1897
+ })
1898
+
1899
+ it('does not deliver the same item twice', async () => {
1900
+ const harness = createConnectorHarness(connector, {
1901
+ config,
1902
+ fetchImpl: fakeFetch({
1903
+ items: [
1904
+ {
1905
+ id: '1',
1906
+ title: 'First item',
1907
+ html_url: 'https://example.com/1',
1908
+ updated_at: '2026-01-01T00:00:00.000Z'
1909
+ }
1910
+ ]
1911
+ })
1912
+ })
1913
+
1914
+ expect(await harness.pollTwice('itemCreated')).toEqual([])
1915
+ })
1916
+
1917
+ it('creates an item and keeps only its id', async () => {
1918
+ const harness = createConnectorHarness(connector, {
1919
+ config,
1920
+ fetchImpl: fakeFetch({ id: '42', extra: 'ignored' })
1921
+ })
1922
+
1923
+ expect(await harness.execute('createItem', { title: 'A title' })).toEqual({ id: '42' })
1924
+ })
1925
+ })
1926
+ `;
1927
+ }
1928
+ function readme(id, name, description) {
1929
+ return `# ${name}
1930
+
1931
+ ${description}
1932
+
1933
+ ## Build and check
1934
+
1935
+ \`\`\`sh
1936
+ yarn install
1937
+ yarn build
1938
+ yarn check # verifies the connector against Vorn's contract
1939
+ yarn test
1940
+ yarn pack # writes ${id}-${SCAFFOLD_VERSION}.vorn.tgz, installable in Vorn
1941
+ \`\`\`
1942
+
1943
+ ## Settings
1944
+
1945
+ | Setting | Environment | Required |
1946
+ | --- | --- | --- |
1947
+ | API token | \`API_TOKEN\` | yes |
1948
+ | Base URL | \`BASE_URL\` | no |
1949
+
1950
+ ## What it offers
1951
+
1952
+ - **Item created** \u2014 polls for items created since the last run.
1953
+ - **Create item** \u2014 creates one item and returns its id.
1954
+
1955
+ Rename the trigger, the action and the settings to whatever this connector
1956
+ really talks to; the shapes here are a starting point, not a rule.
1957
+ `;
1958
+ }
1959
+ function scaffoldFiles(options) {
1960
+ if (!ID_PATTERN.test(options.id ?? "")) {
1961
+ throw new Error(`Connector id "${options.id}" must start with a letter and be url-safe`);
1962
+ }
1963
+ const name = options.name?.trim() || titleCase(options.id);
1964
+ const description = options.description?.trim() || `${name} connector for Vorn`;
1965
+ const inRepo = options.repoConventions ?? false;
1966
+ return [
1967
+ { path: "package.json", contents: packageJson(options.id, description, inRepo) },
1968
+ { path: "src/connector.ts", contents: connectorSource(options.id, name, description) },
1969
+ { path: "src/entry.ts", contents: entrySource() },
1970
+ { path: "src/index.ts", contents: indexSource() },
1971
+ { path: "src/connector.test.ts", contents: testSource(name) },
1972
+ { path: "src/entry.test.ts", contents: entryTestSource() },
1973
+ { path: "README.md", contents: readme(options.id, name, description) },
1974
+ ...inRepo ? [
1975
+ { path: "CHANGELOG.md", contents: changelog() },
1976
+ { path: "tsconfig.json", contents: tsconfig() },
1977
+ { path: "tsup.config.ts", contents: tsupConfig() },
1978
+ { path: "vitest.config.ts", contents: vitestConfig() }
1979
+ ] : []
1980
+ ];
1981
+ }
1982
+
1983
+ // src/server.ts
1984
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
1985
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
1986
+ import { z } from "zod";
1987
+ function json(value) {
1988
+ return {
1989
+ // Vorn reads `structuredContent` to build step output and to find the
1990
+ // `items` array a poll returned; the text block keeps the result readable
1991
+ // in any generic MCP client.
1992
+ content: [{ type: "text", text: JSON.stringify(value, null, 2) }],
1993
+ structuredContent: value
1994
+ };
1995
+ }
1996
+ function failure(error) {
1997
+ return {
1998
+ content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }],
1999
+ isError: true
2000
+ };
2001
+ }
2002
+ function describeInput(input) {
2003
+ const base = input.description ?? input.label;
2004
+ if (input.loadOptions !== void 0) {
2005
+ return `${base}. Choices come from this connector's "${input.loadOptions}" list.`;
2006
+ }
2007
+ if (input.type === "json") return `${base}. Takes JSON.`;
2008
+ const choices = (input.options ?? []).map((option) => option.value).filter((value) => typeof value === "string" && value !== "");
2009
+ if (choices.length > 0) return `${base}. Suggested values: ${choices.join(", ")}.`;
2010
+ return base;
2011
+ }
2012
+ function inputShape(inputs) {
2013
+ const shape = {};
2014
+ for (const input of inputs) {
2015
+ const base = z.string().describe(describeInput(input));
2016
+ shape[input.key] = input.required ? base : base.optional();
2017
+ }
2018
+ return shape;
2019
+ }
2020
+ function scalar(type) {
2021
+ if (type === "number") return z.number();
2022
+ if (type === "boolean") return z.boolean();
2023
+ return z.string();
2024
+ }
2025
+ function outputSchema(outputs) {
2026
+ const shape = {};
2027
+ for (const output of outputs) {
2028
+ shape[output.key] = scalar(output.type).optional().describe(output.description ?? output.key);
2029
+ }
2030
+ return z.looseObject(shape);
2031
+ }
2032
+ function createConnectorServer(connector, options = {}) {
2033
+ const server = new McpServer(
2034
+ { name: connector.id, version: connector.version },
2035
+ { capabilities: { tools: {} } }
2036
+ );
2037
+ let cached = options.config;
2038
+ const config = () => cached ??= resolveConfig(connector);
2039
+ server.registerTool(
2040
+ MANIFEST_TOOL,
2041
+ {
2042
+ description: `Describe the ${connector.name} connector and how to configure it`,
2043
+ inputSchema: {},
2044
+ outputSchema: z.looseObject({})
2045
+ },
2046
+ () => json(connectorManifest(connector))
2047
+ );
2048
+ if (connector.preflight) {
2049
+ const preflight = connector.preflight.bind(connector);
2050
+ server.registerTool(
2051
+ PREFLIGHT_TOOL,
2052
+ {
2053
+ description: `Check whether ${connector.name} can run right now`,
2054
+ inputSchema: {},
2055
+ // Declared rather than left open like the manifest's: this shape is
2056
+ // fixed, so a caller can validate against it. Still loose, because a
2057
+ // connector adding a field of its own should not fail the call.
2058
+ outputSchema: z.looseObject({
2059
+ ok: z.boolean().describe("Whether the connector could run right now"),
2060
+ message: z.string().optional().describe("What to do about it, when it could not")
2061
+ })
2062
+ },
2063
+ async () => {
2064
+ try {
2065
+ return json({ ...await preflight() });
2066
+ } catch (error) {
2067
+ return json({
2068
+ ok: false,
2069
+ message: error instanceof Error ? error.message : String(error)
2070
+ });
2071
+ }
2072
+ }
2073
+ );
2074
+ }
2075
+ const optionSets = Object.keys(connector.options ?? {});
2076
+ if (optionSets.length > 0) {
2077
+ server.registerTool(
2078
+ OPTIONS_TOOL,
2079
+ {
2080
+ description: `List what one of ${connector.name}'s fields can be set to`,
2081
+ inputSchema: {
2082
+ name: z.enum(optionSets).describe("Which options set to list")
2083
+ },
2084
+ outputSchema: z.looseObject({
2085
+ options: z.array(z.looseObject({ value: z.string(), label: z.string().optional() })).describe("The choices, each a value to send and words to show")
2086
+ })
2087
+ },
2088
+ async (args) => {
2089
+ try {
2090
+ return json({
2091
+ options: await runOptions(connector, args.name, {
2092
+ config: config(),
2093
+ ...options.now && { now: options.now }
2094
+ })
2095
+ });
2096
+ } catch (error) {
2097
+ return failure(error);
2098
+ }
2099
+ }
2100
+ );
2101
+ }
2102
+ for (const trigger of connector.triggers) {
2103
+ server.registerTool(
2104
+ pollToolName(trigger.type),
2105
+ {
2106
+ description: trigger.description ?? `Poll ${connector.name} for ${trigger.label}`,
2107
+ inputSchema: {
2108
+ since: z.string().optional().describe("Only return items changed after this ISO timestamp"),
2109
+ cursor: z.string().optional().describe("Opaque cursor from a previous page"),
2110
+ limit: z.string().optional().describe("Maximum number of items to return")
2111
+ },
2112
+ outputSchema: z.looseObject({
2113
+ items: z.array(z.looseObject({})).describe("Normalized items"),
2114
+ nextCursor: z.string().optional().describe("Cursor for the next page"),
2115
+ hasMore: z.boolean().describe("Whether another page is immediately available")
2116
+ })
2117
+ },
2118
+ async (args) => {
2119
+ try {
2120
+ const limit = args.limit === void 0 ? void 0 : Number(args.limit);
2121
+ if (limit !== void 0 && !Number.isFinite(limit)) {
2122
+ throw new Error(`Invalid limit "${args.limit}"`);
2123
+ }
2124
+ return json(
2125
+ await runPoll(connector, trigger.type, {
2126
+ config: config(),
2127
+ ...args.since !== void 0 && { since: args.since },
2128
+ ...args.cursor !== void 0 && { cursor: args.cursor },
2129
+ ...limit !== void 0 && { limit },
2130
+ ...options.now && { now: options.now }
2131
+ })
2132
+ );
2133
+ } catch (error) {
2134
+ return failure(error);
2135
+ }
2136
+ }
2137
+ );
2138
+ }
2139
+ for (const action of connector.actions) {
2140
+ const base = action.description ?? `${action.label} in ${connector.name}`;
2141
+ const retryHint = action.idempotent === void 0 ? "" : action.idempotent ? " Safe to retry: repeating this call with the same arguments has no additional effect." : " Not idempotent: repeating this call performs the operation again.";
2142
+ server.registerTool(
2143
+ action.type,
2144
+ {
2145
+ // Carries the authored label, so a picker can name the action rather than its tool.
2146
+ title: action.label,
2147
+ description: `${base}${retryHint}`,
2148
+ inputSchema: inputShape(action.inputs ?? []),
2149
+ outputSchema: outputSchema(action.outputs ?? [])
2150
+ },
2151
+ async (args) => {
2152
+ try {
2153
+ return json(
2154
+ await runAction(connector, action.type, args, {
2155
+ config: config(),
2156
+ ...options.now && { now: options.now }
2157
+ })
2158
+ );
2159
+ } catch (error) {
2160
+ return failure(error);
2161
+ }
2162
+ }
2163
+ );
2164
+ }
2165
+ return server;
2166
+ }
2167
+ async function serveConnector(connector, options = {}) {
2168
+ const server = createConnectorServer(connector, options);
2169
+ await server.connect(new StdioServerTransport());
2170
+ }
2171
+
2172
+ export {
2173
+ MAX_PACK_BYTES,
2174
+ lifecycleScriptFindings,
2175
+ bundleDependencyFindings,
2176
+ readNearestPackageJson,
2177
+ esbuildBundle,
2178
+ normalizeItem,
2179
+ normalizeItems,
2180
+ pollWithDedupe,
2181
+ valueAt,
2182
+ applyPostReceive,
2183
+ resolveTemplates,
2184
+ resolveRequest,
2185
+ asOutput,
2186
+ MAX_REQUEST_PAGES,
2187
+ nextLink,
2188
+ executeRequest,
2189
+ retryAfterMs,
2190
+ backoffMs,
2191
+ resilientFetch,
2192
+ MAX_POLL_PAGES,
2193
+ runPoll,
2194
+ drainPoll,
2195
+ runOptions,
2196
+ runAction,
2197
+ envNameFor,
2198
+ defineConnector,
2199
+ resolveConfig,
2200
+ pollToolName,
2201
+ MANIFEST_TOOL,
2202
+ PREFLIGHT_TOOL,
2203
+ OPTIONS_TOOL,
2204
+ connectionSetup,
2205
+ connectorManifest,
2206
+ MockRouteMissError,
2207
+ escapedMockHttp,
2208
+ withMockHttp,
2209
+ createConnectorHarness,
2210
+ checkConnector,
2211
+ CHECK_OWNERS,
2212
+ runConformance,
2213
+ formatFindings,
2214
+ packFileName,
2215
+ packConnector,
2216
+ titleCase,
2217
+ scaffoldFiles,
2218
+ createConnectorServer,
2219
+ serveConnector
2220
+ };