@vornrun/connector-sdk 0.7.0-beta.13 → 0.7.0-beta.14

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.
@@ -1,6 +1,264 @@
1
+ // src/define.ts
2
+ var KEY_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]*$/;
3
+ var PATH_DATA_PATTERN = /^[MmZzLlHhVvCcSsQqTtAa0-9\s,.\-+eE]+$/;
4
+ var VIEW_BOX_PATTERN = /^-?[\d.]+\s+-?[\d.]+\s+-?[\d.]+\s+-?[\d.]+$/;
5
+ var DEDUPE_STRATEGIES = ["timestamp", "lastItem"];
6
+ var AUTH_RUNGS = ["none", "cli", "key", "oauth"];
7
+ var ABSOLUTE_URL_PATTERN = /^https?:\/\//i;
8
+ var CONFIG_ROOTED_URL_PATTERN = /^\{\{\s*config\./;
9
+ function assertUnique(kind, keys) {
10
+ const seen = /* @__PURE__ */ new Set();
11
+ for (const key of keys) {
12
+ if (seen.has(key)) throw new Error(`Duplicate ${kind} "${key}"`);
13
+ seen.add(key);
14
+ }
15
+ }
16
+ function assertAuth(definition) {
17
+ const auth = definition.auth;
18
+ if (!auth) return;
19
+ const id = definition.id;
20
+ if (!AUTH_RUNGS.includes(auth.rung)) {
21
+ throw new Error(
22
+ `Connector ${id} declares unknown auth rung ${JSON.stringify(auth.rung)}; expected ${AUTH_RUNGS.join(", ")}`
23
+ );
24
+ }
25
+ if (auth.rung === "cli" && !auth.probe?.command?.trim()) {
26
+ throw new Error(`Connector ${id} borrows a CLI login but declares no probe command to ask it`);
27
+ }
28
+ if (auth.rung === "key") {
29
+ const keys = auth.keys ?? [];
30
+ if (keys.length === 0) {
31
+ throw new Error(`Connector ${id} signs in with a key but names no config field holding it`);
32
+ }
33
+ const declared = new Set((definition.config ?? []).map((field) => field.key));
34
+ for (const key of keys) {
35
+ if (!declared.has(key)) {
36
+ throw new Error(`Connector ${id} names auth key "${key}", which is not a config field`);
37
+ }
38
+ }
39
+ }
40
+ if (auth.rung === "none") {
41
+ const secret = (definition.config ?? []).find((field) => field.secret === true);
42
+ if (secret) {
43
+ throw new Error(
44
+ `Connector ${id} claims it needs no sign-in but declares secret field "${secret.key}"`
45
+ );
46
+ }
47
+ }
48
+ }
49
+ function envNameFor(key, explicit) {
50
+ if (explicit) return explicit;
51
+ return key.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[-\s]+/g, "_").toUpperCase();
52
+ }
53
+ function defineConnector(definition) {
54
+ if (!KEY_PATTERN.test(definition.id ?? "")) {
55
+ throw new Error(`Connector id "${definition.id}" must start with a letter and be url-safe`);
56
+ }
57
+ if (!definition.name?.trim()) {
58
+ throw new Error(`Connector ${definition.id} is missing a name`);
59
+ }
60
+ if (definition.icon) {
61
+ const { viewBox, paths } = definition.icon;
62
+ if (!Array.isArray(paths) || paths.length === 0) {
63
+ throw new Error(`Connector ${definition.id} has an icon with no paths`);
64
+ }
65
+ for (const path of paths) {
66
+ if (typeof path !== "string" || !PATH_DATA_PATTERN.test(path)) {
67
+ throw new Error(
68
+ `Connector ${definition.id} has an icon path that is not SVG path data. Only path data is accepted, not markup.`
69
+ );
70
+ }
71
+ }
72
+ if (viewBox !== void 0 && !VIEW_BOX_PATTERN.test(viewBox)) {
73
+ throw new Error(`Connector ${definition.id} has an icon viewBox that is not four numbers`);
74
+ }
75
+ }
76
+ const triggers = definition.triggers ?? [];
77
+ const actions = definition.actions ?? [];
78
+ if (triggers.length === 0 && actions.length === 0) {
79
+ throw new Error(`Connector ${definition.id} declares no triggers and no actions`);
80
+ }
81
+ for (const trigger of triggers) {
82
+ if (!KEY_PATTERN.test(trigger.type ?? "")) {
83
+ throw new Error(`Trigger type "${trigger.type}" must start with a letter and be url-safe`);
84
+ }
85
+ const loose = trigger;
86
+ const declarative = typeof loose.fetch === "function";
87
+ const imperative = typeof loose.poll === "function";
88
+ if (declarative && imperative) {
89
+ throw new Error(`Trigger ${trigger.type} declares both fetch() and poll(); pick one`);
90
+ }
91
+ if (declarative !== (loose.dedupe !== void 0)) {
92
+ throw new Error(
93
+ `Trigger ${trigger.type} needs fetch() and a dedupe strategy together, not one alone`
94
+ );
95
+ }
96
+ if (loose.dedupe !== void 0 && !DEDUPE_STRATEGIES.includes(loose.dedupe)) {
97
+ throw new Error(
98
+ `Trigger ${trigger.type} has unknown dedupe strategy ${JSON.stringify(loose.dedupe)}; expected ${DEDUPE_STRATEGIES.join(" or ")}`
99
+ );
100
+ }
101
+ if (loose.poll !== void 0 && !imperative) {
102
+ throw new Error(`Trigger ${trigger.type} declares poll but it is not a function`);
103
+ }
104
+ if (!declarative && !imperative) {
105
+ throw new Error(`Trigger ${trigger.type} is missing a fetch() or poll() implementation`);
106
+ }
107
+ }
108
+ for (const action of actions) {
109
+ if (!KEY_PATTERN.test(action.type ?? "")) {
110
+ throw new Error(`Action type "${action.type}" must start with a letter and be url-safe`);
111
+ }
112
+ const loose = action;
113
+ const written = typeof loose.run === "function";
114
+ const declared = loose.request !== void 0;
115
+ if (written && declared) {
116
+ throw new Error(`Action ${action.type} declares both run() and a request; pick one`);
117
+ }
118
+ if (!written && !declared) {
119
+ throw new Error(`Action ${action.type} is missing a run() implementation or a request`);
120
+ }
121
+ if (declared) {
122
+ const request = loose.request;
123
+ if (typeof request?.url !== "string" || request.url.trim() === "") {
124
+ throw new Error(`Action ${action.type} declares a request with no URL`);
125
+ }
126
+ const url = request.url.trim();
127
+ if (!ABSOLUTE_URL_PATTERN.test(url) && !CONFIG_ROOTED_URL_PATTERN.test(url)) {
128
+ throw new Error(
129
+ `Action ${action.type} declares the request URL "${url}", which is neither absolute nor rooted in a {{config.\u2026}} value`
130
+ );
131
+ }
132
+ }
133
+ if (!declared && loose.postReceive !== void 0) {
134
+ throw new Error(`Action ${action.type} has postReceive but no request for it to reshape`);
135
+ }
136
+ for (const input of action.inputs ?? []) {
137
+ if (input.loadOptions !== void 0 && definition.options?.[input.loadOptions] === void 0) {
138
+ throw new Error(
139
+ `Action ${action.type} argument "${input.key}" loads options from "${input.loadOptions}", which the connector does not serve`
140
+ );
141
+ }
142
+ }
143
+ }
144
+ assertUnique(
145
+ "trigger",
146
+ triggers.map((trigger) => trigger.type)
147
+ );
148
+ assertUnique(
149
+ "action",
150
+ actions.map((action) => action.type)
151
+ );
152
+ assertUnique(
153
+ "config field",
154
+ (definition.config ?? []).map((field) => field.key)
155
+ );
156
+ assertAuth(definition);
157
+ return {
158
+ ...definition,
159
+ version: definition.version ?? "0.0.0",
160
+ config: definition.config ?? [],
161
+ triggers,
162
+ actions
163
+ };
164
+ }
165
+ function resolveConfig(connector, env = process.env) {
166
+ const config = {};
167
+ const missing = [];
168
+ for (const field of connector.config) {
169
+ const name = envNameFor(field.key, field.env);
170
+ const value = env[name] ?? field.default;
171
+ if (value === void 0 || value === "") {
172
+ if (field.required) missing.push(`${field.key} (${name})`);
173
+ continue;
174
+ }
175
+ config[field.key] = value;
176
+ }
177
+ if (missing.length > 0) {
178
+ throw new Error(
179
+ `Connector ${connector.id} is missing required configuration: ${missing.join(", ")}`
180
+ );
181
+ }
182
+ return config;
183
+ }
184
+
185
+ // src/setup.ts
186
+ function pollToolName(triggerType) {
187
+ return `poll_${triggerType}`;
188
+ }
189
+ var MANIFEST_TOOL = "vorn_connector_manifest";
190
+ var PREFLIGHT_TOOL = "vorn_connector_preflight";
191
+ var OPTIONS_TOOL = "vorn_connector_options";
192
+ function connectionSetup(connector, triggerType) {
193
+ const trigger = connector.triggers.find((entry) => entry.type === triggerType);
194
+ if (!trigger) {
195
+ throw new Error(`Connector ${connector.id} has no trigger "${triggerType}"`);
196
+ }
197
+ return {
198
+ connectorId: connector.id,
199
+ triggerType,
200
+ filters: {
201
+ pollTool: pollToolName(triggerType),
202
+ itemsPath: "items",
203
+ idField: "externalId",
204
+ timestampField: "updatedAt",
205
+ titleField: "title",
206
+ urlField: "url",
207
+ cursorArg: "cursor",
208
+ cursorPath: "nextCursor"
209
+ },
210
+ env: connector.config.map((field) => ({
211
+ name: envNameFor(field.key, field.env),
212
+ required: field.required === true,
213
+ secret: field.secret === true,
214
+ ...field.description !== void 0 && { description: field.description },
215
+ ...field.builderHint !== void 0 && { builderHint: field.builderHint }
216
+ }))
217
+ };
218
+ }
219
+ function connectorManifest(connector) {
220
+ return {
221
+ id: connector.id,
222
+ name: connector.name,
223
+ version: connector.version,
224
+ ...connector.description !== void 0 && { description: connector.description },
225
+ ...connector.icon !== void 0 && { icon: connector.icon },
226
+ ...connector.auth !== void 0 && { auth: connector.auth },
227
+ triggers: connector.triggers.map((trigger) => ({
228
+ type: trigger.type,
229
+ label: trigger.label,
230
+ ...trigger.description !== void 0 && { description: trigger.description },
231
+ // Carried through so the app can seed a connection's status mapping and
232
+ // its polling workflow. Absent when the connector said nothing, which is
233
+ // different from saying there is nothing.
234
+ ...trigger.statusMapping !== void 0 && { statusMapping: trigger.statusMapping },
235
+ ...trigger.defaultWorkflow !== void 0 && { defaultWorkflow: trigger.defaultWorkflow },
236
+ setup: connectionSetup(connector, trigger.type)
237
+ })),
238
+ actions: connector.actions.map((action) => ({
239
+ type: action.type,
240
+ label: action.label,
241
+ ...action.description !== void 0 && { description: action.description },
242
+ inputs: (action.inputs ?? []).map((input) => ({
243
+ key: input.key,
244
+ label: input.label,
245
+ type: input.type ?? "string",
246
+ required: input.required === true,
247
+ ...input.options !== void 0 && { options: input.options },
248
+ ...input.loadOptions !== void 0 && { loadOptions: input.loadOptions },
249
+ ...input.builderHint !== void 0 && { builderHint: input.builderHint }
250
+ })),
251
+ ...action.outputs !== void 0 && { outputs: action.outputs },
252
+ ...action.sample !== void 0 && { sample: action.sample }
253
+ }))
254
+ };
255
+ }
256
+
1
257
  // src/packaging.ts
2
258
  import { builtinModules } from "module";
3
259
  import { readFileSync } from "fs";
260
+ import { mkdtemp, writeFile } from "fs/promises";
261
+ import { tmpdir } from "os";
4
262
  import { dirname, isAbsolute, join, resolve } from "path";
5
263
  var MAX_PACK_BYTES = 8 * 1024 * 1024;
6
264
  var LIFECYCLE_SCRIPTS = [
@@ -13,8 +271,8 @@ var LIFECYCLE_SCRIPTS = [
13
271
  "postpublish"
14
272
  ];
15
273
  var BUILTINS = new Set(builtinModules);
16
- function finding(code, target, message) {
17
- return { level: "error", code, target, message };
274
+ function finding(code, target, message, level = "error") {
275
+ return { level, code, target, message };
18
276
  }
19
277
  function lifecycleScriptFindings(pkg) {
20
278
  const scripts = pkg?.scripts;
@@ -45,6 +303,120 @@ function bundleDependencyFindings(external) {
45
303
  )
46
304
  ];
47
305
  }
306
+ var RELATIVE_REQUIRE = /(?:__)?(?:require(?:\.resolve)?|import)\(\s*(['"])(\.\.?\/[^'"]*)\1\s*\)/y;
307
+ var RELATIVE_CREATE_REQUIRE = /createRequire\([^()]*\)\(\s*(['"])(\.\.?\/[^'"]*)\1\s*\)/y;
308
+ var CALL_WORDS = /* @__PURE__ */ new Set(["require", "__require", "createRequire", "import"]);
309
+ var BEFORE_REGEX = /* @__PURE__ */ new Set(["", ..."(,=:[!&|?{};+-*%~^<>"]);
310
+ var BEFORE_REGEX_WORDS = /* @__PURE__ */ new Set([
311
+ "return",
312
+ "typeof",
313
+ "instanceof",
314
+ "in",
315
+ "of",
316
+ "new",
317
+ "delete",
318
+ "void",
319
+ "case",
320
+ "do",
321
+ "else",
322
+ "yield",
323
+ "await"
324
+ ]);
325
+ var WORD = /[\w$]/;
326
+ function endOfQuoted(code, start) {
327
+ const quote2 = code[start];
328
+ let i = start + 1;
329
+ while (i < code.length) {
330
+ if (code[i] === "\\") {
331
+ i += 2;
332
+ continue;
333
+ }
334
+ if (code[i] === quote2) return i + 1;
335
+ i += 1;
336
+ }
337
+ return code.length;
338
+ }
339
+ function endOfRegex(code, start) {
340
+ let i = start + 1;
341
+ let inClass = false;
342
+ while (i < code.length) {
343
+ const ch = code[i];
344
+ if (ch === "\\") {
345
+ i += 2;
346
+ continue;
347
+ }
348
+ if (ch === "\n") return i;
349
+ if (ch === "[") inClass = true;
350
+ else if (ch === "]") inClass = false;
351
+ else if (ch === "/" && !inClass) return i + 1;
352
+ i += 1;
353
+ }
354
+ return code.length;
355
+ }
356
+ function relativeRuntimeSpecifiers(code) {
357
+ const found = /* @__PURE__ */ new Set();
358
+ let previous = "";
359
+ let previousWord = "";
360
+ let i = 0;
361
+ while (i < code.length) {
362
+ const ch = code[i];
363
+ if (ch === "/" && code[i + 1] === "/") {
364
+ const end = code.indexOf("\n", i);
365
+ i = end === -1 ? code.length : end;
366
+ continue;
367
+ }
368
+ if (ch === "/" && code[i + 1] === "*") {
369
+ const end = code.indexOf("*/", i + 2);
370
+ i = end === -1 ? code.length : end + 2;
371
+ continue;
372
+ }
373
+ if (ch === '"' || ch === "'" || ch === "`") {
374
+ i = endOfQuoted(code, i);
375
+ previous = ch;
376
+ previousWord = "";
377
+ continue;
378
+ }
379
+ if (ch === "/" && (BEFORE_REGEX.has(previous) || BEFORE_REGEX_WORDS.has(previousWord))) {
380
+ i = endOfRegex(code, i);
381
+ previous = "/";
382
+ previousWord = "";
383
+ continue;
384
+ }
385
+ if (WORD.test(ch)) {
386
+ const start = i;
387
+ while (i < code.length && WORD.test(code[i])) i += 1;
388
+ const word = code.slice(start, i);
389
+ if (CALL_WORDS.has(word) && code[start - 1] !== ".") {
390
+ for (const pattern of [RELATIVE_REQUIRE, RELATIVE_CREATE_REQUIRE]) {
391
+ pattern.lastIndex = start;
392
+ const match = pattern.exec(code);
393
+ if (match) found.add(match[2]);
394
+ }
395
+ }
396
+ previous = code[i - 1];
397
+ previousWord = word;
398
+ continue;
399
+ }
400
+ if (!/\s/.test(ch)) {
401
+ previous = ch;
402
+ previousWord = "";
403
+ }
404
+ i += 1;
405
+ }
406
+ return [...found];
407
+ }
408
+ function bundledRequireFindings(code) {
409
+ const specifiers = relativeRuntimeSpecifiers(code);
410
+ if (specifiers.length === 0) return [];
411
+ return [
412
+ finding(
413
+ "runtime-dependencies",
414
+ "bundle",
415
+ `${specifiers.sort().join(", ")} ${specifiers.length === 1 ? "is" : "are"} required at runtime; a pack is one file, so nothing beside it survives packing`,
416
+ "warn"
417
+ )
418
+ ];
419
+ }
48
420
  function packageDirFor(resolveDir, entry) {
49
421
  const from = resolve(resolveDir);
50
422
  if (entry === void 0) return from;
@@ -72,6 +444,100 @@ function packEntryContents(entry, sdkModule = "@vornrun/connector-sdk") {
72
444
  ""
73
445
  ].join("\n");
74
446
  }
447
+ async function stagePack(connector, code) {
448
+ const dir = await mkdtemp(join(tmpdir(), "vorn-pack-"));
449
+ await writeFile(join(dir, "index.js"), code, "utf8");
450
+ await writeFile(
451
+ join(dir, "manifest.json"),
452
+ `${JSON.stringify(connectorManifest(connector), null, 2)}
453
+ `,
454
+ "utf8"
455
+ );
456
+ return dir;
457
+ }
458
+ var LAUNCH_TIMEOUT_MS = 15e3;
459
+ var LAUNCH_ENV_KEYS = [
460
+ "PATH",
461
+ "HOME",
462
+ "USERPROFILE",
463
+ "HOMEDRIVE",
464
+ "HOMEPATH",
465
+ "APPDATA",
466
+ "LOCALAPPDATA",
467
+ "PROGRAMDATA",
468
+ "PROGRAMFILES",
469
+ "SystemRoot",
470
+ "SYSTEMDRIVE",
471
+ "COMSPEC",
472
+ "PATHEXT",
473
+ "TMPDIR",
474
+ "TEMP",
475
+ "TMP",
476
+ "LANG",
477
+ "LC_ALL",
478
+ "LC_CTYPE",
479
+ "TZ",
480
+ "SHELL",
481
+ "TERM",
482
+ "USER",
483
+ "LOGNAME",
484
+ "NODE_EXTRA_CA_CERTS"
485
+ ];
486
+ function launchEnv() {
487
+ const env = {};
488
+ for (const key of LAUNCH_ENV_KEYS) {
489
+ const value = process.env[key];
490
+ if (value !== void 0) env[key] = value;
491
+ }
492
+ return env;
493
+ }
494
+ function errorLine(text) {
495
+ const lines = text.split("\n").map((line) => line.trim()).filter((line) => line !== "");
496
+ return [...lines].reverse().find((line) => /Error\b/.test(line)) ?? lines[lines.length - 1];
497
+ }
498
+ function withTimeout(promise, ms, message) {
499
+ let timer;
500
+ return Promise.race([
501
+ promise.finally(() => clearTimeout(timer)),
502
+ new Promise((_, reject) => {
503
+ timer = setTimeout(() => reject(new Error(message)), ms);
504
+ })
505
+ ]);
506
+ }
507
+ async function packLaunchFindings(dir) {
508
+ const { Client } = await import("@modelcontextprotocol/sdk/client/index.js");
509
+ const { StdioClientTransport } = await import("@modelcontextprotocol/sdk/client/stdio.js");
510
+ const transport = new StdioClientTransport({
511
+ command: process.execPath,
512
+ args: ["index.js"],
513
+ cwd: dir,
514
+ env: launchEnv(),
515
+ stderr: "pipe"
516
+ });
517
+ const client = new Client({ name: "vorn-connector-check", version: "1" }, { capabilities: {} });
518
+ let stderr = "";
519
+ transport.stderr?.on("data", (chunk) => {
520
+ stderr += chunk.toString();
521
+ });
522
+ try {
523
+ await withTimeout(
524
+ client.connect(transport),
525
+ LAUNCH_TIMEOUT_MS,
526
+ `did not answer within ${LAUNCH_TIMEOUT_MS / 1e3}s of starting`
527
+ );
528
+ return [];
529
+ } catch (error) {
530
+ const said = error instanceof Error ? error.message : String(error);
531
+ return [
532
+ finding("pack-launch", "bundle", `did not start as a pack: ${errorLine(stderr) ?? said}`)
533
+ ];
534
+ } finally {
535
+ await client.close().catch(() => {
536
+ });
537
+ await transport.close().catch(() => {
538
+ });
539
+ }
540
+ }
75
541
  async function esbuildBundle(request) {
76
542
  const { build } = await import("esbuild");
77
543
  const result = await build({
@@ -87,7 +553,15 @@ async function esbuildBundle(request) {
87
553
  format: "esm",
88
554
  write: false,
89
555
  metafile: true,
90
- legalComments: "none"
556
+ legalComments: "none",
557
+ // A bundled CommonJS dependency asks for its builtins through esbuild's shim, which throws unless a real require is in scope.
558
+ banner: {
559
+ js: [
560
+ "import { createRequire as __vornCreateRequire } from 'node:module'",
561
+ "const require = __vornCreateRequire(import.meta.url)",
562
+ ""
563
+ ].join("\n")
564
+ }
91
565
  });
92
566
  const output = Object.values(result.metafile.outputs)[0];
93
567
  return {
@@ -681,336 +1155,80 @@ function quote(value) {
681
1155
  }
682
1156
  function coerceArg(value, type) {
683
1157
  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
- }
1158
+ if (type === "number") {
1159
+ const parsed = Number(value);
1160
+ if (Number.isNaN(parsed)) throw new Error(`Expected a number, got "${quote(value)}"`);
1161
+ return parsed;
834
1162
  }
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`);
1163
+ if (type === "boolean") {
1164
+ if (value === "true") return true;
1165
+ if (value === "false") return false;
1166
+ throw new Error(`Expected a boolean, got "${quote(value)}"`);
839
1167
  }
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`);
1168
+ if (type === "json") {
1169
+ try {
1170
+ return JSON.parse(value);
1171
+ } catch {
1172
+ throw new Error(`Expected JSON, got "${quote(value)}"`);
843
1173
  }
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`);
1174
+ }
1175
+ return value;
1176
+ }
1177
+ async function runAction(connector, actionType, args, options = {}) {
1178
+ const action = connector.actions.find((entry) => entry.type === actionType);
1179
+ if (!action) {
1180
+ throw new Error(`Connector ${connector.id} has no action "${actionType}"`);
1181
+ }
1182
+ const coerced = { ...args };
1183
+ for (const input of action.inputs ?? []) {
1184
+ const value = coerced[input.key];
1185
+ if (value === void 0 || value === "") {
1186
+ if (input.required) throw new Error(`Action ${actionType} requires "${input.key}"`);
1187
+ delete coerced[input.key];
1188
+ continue;
849
1189
  }
850
- if (declarative !== (loose.dedupe !== void 0)) {
1190
+ try {
1191
+ coerced[input.key] = coerceArg(value, input.type);
1192
+ } catch (error) {
851
1193
  throw new Error(
852
- `Trigger ${trigger.type} needs fetch() and a dedupe strategy together, not one alone`
1194
+ `Action ${actionType} argument "${input.key}": ${error instanceof Error ? error.message : String(error)}`,
1195
+ { cause: error }
853
1196
  );
854
1197
  }
855
- if (loose.dedupe !== void 0 && !DEDUPE_STRATEGIES.includes(loose.dedupe)) {
1198
+ }
1199
+ const config = options.config ?? {};
1200
+ const method = (action.request?.method ?? "GET").toUpperCase();
1201
+ const retryable = action.idempotent === true || action.request !== void 0 && SAFE_METHODS.has(method);
1202
+ const fetchImpl = resilientFetch({
1203
+ fetchImpl: options.fetchImpl ?? globalThis.fetch,
1204
+ retryable,
1205
+ ...options.retry !== void 0 && { retry: options.retry },
1206
+ ...options.sleep !== void 0 && { sleep: options.sleep }
1207
+ });
1208
+ if (action.request !== void 0) {
1209
+ try {
1210
+ return await executeRequest(
1211
+ action.request,
1212
+ action.postReceive,
1213
+ { args: coerced, config },
1214
+ { fetchImpl }
1215
+ );
1216
+ } catch (error) {
856
1217
  throw new Error(
857
- `Trigger ${trigger.type} has unknown dedupe strategy ${JSON.stringify(loose.dedupe)}; expected ${DEDUPE_STRATEGIES.join(" or ")}`
1218
+ `Action ${actionType}: ${error instanceof Error ? error.message : String(error)}`,
1219
+ { cause: error }
858
1220
  );
859
1221
  }
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
1222
  }
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}"`);
1223
+ if (typeof action.run !== "function") {
1224
+ throw new Error(`Action ${actionType} has neither a run() implementation nor a request`);
955
1225
  }
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
- };
1226
+ const output = await action.run(coerced, {
1227
+ config,
1228
+ now: options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString()),
1229
+ fetch: fetchImpl
1230
+ });
1231
+ return output ?? {};
1014
1232
  }
1015
1233
 
1016
1234
  // src/harness.ts
@@ -1111,6 +1329,7 @@ function createConnectorHarness(connector, harnessOptions = {}) {
1111
1329
  }
1112
1330
 
1113
1331
  // src/check.ts
1332
+ import { rm } from "fs/promises";
1114
1333
  var EXECUTABLE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
1115
1334
  var CREDENTIAL_NAME = /(secret|token|password|passphrase|api[-_]?key|credential)/i;
1116
1335
  var INPUT_TYPES = /* @__PURE__ */ new Set(["string", "number", "boolean", "select", "json"]);
@@ -1204,7 +1423,13 @@ function actionShapeFindings(action) {
1204
1423
  }
1205
1424
  return found;
1206
1425
  }
1207
- async function packageFindings(options) {
1426
+ function bundles(options) {
1427
+ return Boolean(options.bundle && options.entry !== void 0 && options.packageDir !== void 0);
1428
+ }
1429
+ function launches(options) {
1430
+ return bundles(options) && options.mock === true;
1431
+ }
1432
+ async function packageFindings(connector, options) {
1208
1433
  if (options.packageDir === void 0) return [];
1209
1434
  const pkg = readNearestPackageJson(packageDirFor(options.packageDir, options.entry));
1210
1435
  const found = [...lifecycleScriptFindings(pkg)];
@@ -1225,7 +1450,15 @@ async function packageFindings(options) {
1225
1450
  contents: packEntryContents(options.entry),
1226
1451
  resolveDir: options.packageDir
1227
1452
  });
1228
- found.push(...bundleDependencyFindings(built.external));
1453
+ found.push(...bundleDependencyFindings(built.external), ...bundledRequireFindings(built.code));
1454
+ if (options.mock) {
1455
+ const dir = await stagePack(connector, built.code);
1456
+ try {
1457
+ found.push(...await packLaunchFindings(dir));
1458
+ } finally {
1459
+ await rm(dir, { recursive: true, force: true });
1460
+ }
1461
+ }
1229
1462
  }
1230
1463
  return found;
1231
1464
  }
@@ -1420,7 +1653,7 @@ async function checkConnector(connector, options = {}) {
1420
1653
  }
1421
1654
  found.push(...authFindings(connector));
1422
1655
  found.push(...secretFindings(connector));
1423
- found.push(...await packageFindings(options));
1656
+ found.push(...await packageFindings(connector, options));
1424
1657
  found.push(...await mockFindings(connector, options));
1425
1658
  found.push(...await liveFindings(connector, options));
1426
1659
  const perTrigger = await Promise.all(
@@ -1516,7 +1749,8 @@ var CHECK_OWNERS = {
1516
1749
  "mock-network-escape": "mock",
1517
1750
  "mock-not-observed": "mock",
1518
1751
  "preflight-failed": "live",
1519
- "live-action-failed": "live"
1752
+ "live-action-failed": "live",
1753
+ "pack-launch": "launch"
1520
1754
  };
1521
1755
  function checksRun(connector, options) {
1522
1756
  const names = ["manifest", "auth"];
@@ -1524,7 +1758,8 @@ function checksRun(connector, options) {
1524
1758
  if (connector.actions.length > 0) names.push("actions");
1525
1759
  if (connector.triggers.length > 0) names.push("dedupe");
1526
1760
  if (options.packageDir !== void 0) names.push("no-lifecycle-scripts", "keywords");
1527
- if (options.bundle && options.entry !== void 0) names.push("no-runtime-deps");
1761
+ if (bundles(options)) names.push("no-runtime-deps");
1762
+ if (launches(options)) names.push("launch");
1528
1763
  if (options.mock && connector.actions.length > 0) names.push("mock");
1529
1764
  if (options.live && liveExamines(connector)) names.push("live");
1530
1765
  return names;
@@ -1555,8 +1790,7 @@ function formatFindings(findings) {
1555
1790
  }
1556
1791
 
1557
1792
  // src/pack.ts
1558
- import { mkdtemp, mkdir, rm, stat, writeFile } from "fs/promises";
1559
- import { tmpdir } from "os";
1793
+ import { mkdir, rm as rm2, stat } from "fs/promises";
1560
1794
  import { join as join2, resolve as resolve2 } from "path";
1561
1795
  function finding3(code, target, message) {
1562
1796
  return { level: "error", code, target, message };
@@ -1573,29 +1807,24 @@ async function packConnector(connector, options) {
1573
1807
  const contents = packEntryContents(options.entry, options.sdkModule);
1574
1808
  const bundle = options.bundle ?? esbuildBundle;
1575
1809
  const built = await bundle({ contents, resolveDir });
1576
- findings.push(...bundleDependencyFindings(built.external));
1810
+ findings.push(...bundleDependencyFindings(built.external), ...bundledRequireFindings(built.code));
1577
1811
  if (findings.some((item) => item.level === "error")) return { findings };
1578
1812
  const outDir = resolve2(options.outDir ?? process.cwd());
1579
1813
  await mkdir(outDir, { recursive: true });
1580
1814
  const file = join2(outDir, packFileName(connector));
1581
- const staging = await mkdtemp(join2(tmpdir(), "vorn-pack-"));
1815
+ const staging = await stagePack(connector, built.code);
1582
1816
  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
- );
1817
+ findings.push(...await (options.launch ?? packLaunchFindings)(staging));
1818
+ if (findings.some((item) => item.level === "error")) return { findings };
1590
1819
  const { create } = await import("tar");
1591
1820
  await create({ gzip: true, file, cwd: staging }, ["manifest.json", "index.js"]);
1592
1821
  } finally {
1593
- await rm(staging, { recursive: true, force: true });
1822
+ await rm2(staging, { recursive: true, force: true });
1594
1823
  }
1595
1824
  const bytes = (await stat(file)).size;
1596
1825
  const maxBytes = options.maxBytes ?? MAX_PACK_BYTES;
1597
1826
  if (bytes > maxBytes) {
1598
- await rm(file, { force: true });
1827
+ await rm2(file, { force: true });
1599
1828
  return {
1600
1829
  findings: [
1601
1830
  ...findings,
@@ -1663,7 +1892,7 @@ function packageJson(id, description, inRepo) {
1663
1892
  }
1664
1893
  });
1665
1894
  }
1666
- function tsconfig() {
1895
+ function tsconfig(inRepo) {
1667
1896
  return jsonFile({
1668
1897
  compilerOptions: {
1669
1898
  target: "ES2022",
@@ -1676,10 +1905,11 @@ function tsconfig() {
1676
1905
  skipLibCheck: true,
1677
1906
  types: ["node"],
1678
1907
  noEmit: true,
1908
+ resolveJsonModule: true,
1679
1909
  ignoreDeprecations: "6.0",
1680
1910
  allowImportingTsExtensions: true
1681
1911
  },
1682
- include: ["src/**/*", "vitest.config.ts"]
1912
+ include: ["src/**/*", ...inRepo ? ["vitest.config.ts"] : []]
1683
1913
  });
1684
1914
  }
1685
1915
  function tsupConfig() {
@@ -1712,12 +1942,14 @@ function changelog() {
1712
1942
  }
1713
1943
  function connectorSource(id, name, description) {
1714
1944
  return `import { defineConnector } from '@vornrun/connector-sdk'
1945
+ // Bundled at build time: a pack is one file, so a version read from disk is not there to read.
1946
+ import pkg from '../package.json'
1715
1947
 
1716
1948
  export const connector = defineConnector({
1717
1949
  id: ${JSON.stringify(id)},
1718
1950
  name: ${JSON.stringify(name)},
1719
1951
  description: ${JSON.stringify(description)},
1720
- version: '${SCAFFOLD_VERSION}',
1952
+ version: pkg.version,
1721
1953
  // Prefer a login the machine already has: { rung: 'cli', probe: { command: 'tool', args: ['auth', 'status'] } }
1722
1954
  auth: { rung: 'key', keys: ['apiToken'] },
1723
1955
  config: [
@@ -1971,9 +2203,10 @@ function scaffoldFiles(options) {
1971
2203
  { path: "src/connector.test.ts", contents: testSource(name) },
1972
2204
  { path: "src/entry.test.ts", contents: entryTestSource() },
1973
2205
  { path: "README.md", contents: readme(options.id, name, description) },
2206
+ // Everywhere: the generated source imports its package.json, which needs resolveJsonModule to compile.
2207
+ { path: "tsconfig.json", contents: tsconfig(inRepo) },
1974
2208
  ...inRepo ? [
1975
2209
  { path: "CHANGELOG.md", contents: changelog() },
1976
- { path: "tsconfig.json", contents: tsconfig() },
1977
2210
  { path: "tsup.config.ts", contents: tsupConfig() },
1978
2211
  { path: "vitest.config.ts", contents: vitestConfig() }
1979
2212
  ] : []
@@ -2170,9 +2403,19 @@ async function serveConnector(connector, options = {}) {
2170
2403
  }
2171
2404
 
2172
2405
  export {
2406
+ envNameFor,
2407
+ defineConnector,
2408
+ resolveConfig,
2409
+ pollToolName,
2410
+ MANIFEST_TOOL,
2411
+ PREFLIGHT_TOOL,
2412
+ OPTIONS_TOOL,
2413
+ connectionSetup,
2414
+ connectorManifest,
2173
2415
  MAX_PACK_BYTES,
2174
2416
  lifecycleScriptFindings,
2175
2417
  bundleDependencyFindings,
2418
+ bundledRequireFindings,
2176
2419
  readNearestPackageJson,
2177
2420
  esbuildBundle,
2178
2421
  normalizeItem,
@@ -2194,15 +2437,6 @@ export {
2194
2437
  drainPoll,
2195
2438
  runOptions,
2196
2439
  runAction,
2197
- envNameFor,
2198
- defineConnector,
2199
- resolveConfig,
2200
- pollToolName,
2201
- MANIFEST_TOOL,
2202
- PREFLIGHT_TOOL,
2203
- OPTIONS_TOOL,
2204
- connectionSetup,
2205
- connectorManifest,
2206
2440
  MockRouteMissError,
2207
2441
  escapedMockHttp,
2208
2442
  withMockHttp,