@visualautomate/plugin-sdk 0.6.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // src/index.ts
@@ -23,216 +33,36 @@ __export(index_exports, {
23
33
  ALLOWED_PACKAGES: () => ALLOWED_PACKAGES,
24
34
  ALLOWED_PACKAGE_NAMES: () => ALLOWED_PACKAGE_NAMES,
25
35
  BLOCKED_GLOBALS: () => BLOCKED_GLOBALS,
36
+ BLOCKED_GLOBAL_NAMES: () => BLOCKED_GLOBAL_NAMES,
26
37
  CONTEXT_API: () => CONTEXT_API,
38
+ MAX_CODE_LENGTH: () => MAX_CODE_LENGTH,
39
+ PLATFORM_NODE_MAJOR: () => PLATFORM_NODE_MAJOR,
40
+ PROVIDED_GLOBALS: () => PROVIDED_GLOBALS,
41
+ PUSH_CHECK: () => PUSH_CHECK,
27
42
  RESTRICTED_PATTERNS: () => RESTRICTED_PATTERNS,
43
+ RETIRED_TIERS: () => RETIRED_TIERS,
44
+ SANDBOX_OVERRIDES: () => SANDBOX_OVERRIDES,
45
+ SANDBOX_TIERS: () => SANDBOX_TIERS,
46
+ SIGNED_URL_SECONDS: () => SIGNED_URL_SECONDS,
47
+ STATE_LIMITS: () => STATE_LIMITS,
48
+ checkManifest: () => checkManifest,
28
49
  createLocalState: () => createLocalState,
29
50
  createLocalStorage: () => createLocalStorage,
51
+ executeOf: () => executeOf,
30
52
  loadPluginCode: () => loadPluginCode,
53
+ pluginConsole: () => pluginConsole,
54
+ sandboxGlobals: () => sandboxGlobals,
31
55
  sandboxRequire: () => sandboxRequire,
32
56
  simulate: () => simulate,
33
57
  validatePluginCode: () => validatePluginCode
34
58
  });
35
59
  module.exports = __toCommonJS(index_exports);
36
60
 
37
- // src/simulate.ts
38
- function toBytes(data) {
39
- return typeof data === "string" ? new TextEncoder().encode(data) : data;
40
- }
41
- function createLocalStorage(seed = {}) {
42
- const files = /* @__PURE__ */ new Map();
43
- for (const [path, data] of Object.entries(seed)) {
44
- files.set(path, {
45
- bytes: toBytes(data),
46
- contentType: "application/octet-stream",
47
- uploadedAt: (/* @__PURE__ */ new Date()).toISOString()
48
- });
49
- }
50
- const result = (path, file) => ({
51
- url: `memory://${path}`,
52
- size: file.bytes.length,
53
- contentType: file.contentType,
54
- uploadedAt: file.uploadedAt,
55
- // The real one expires; this one does not, and says so rather than
56
- // inventing a date a test might come to rely on.
57
- expiresAt: "never (local simulation)"
58
- });
59
- const storage = {
60
- async put(path, data, contentType = "application/octet-stream") {
61
- const file = {
62
- bytes: toBytes(data),
63
- contentType,
64
- uploadedAt: (/* @__PURE__ */ new Date()).toISOString()
65
- };
66
- files.set(path, file);
67
- return result(path, file);
68
- },
69
- async get(path) {
70
- return files.get(path)?.bytes ?? null;
71
- },
72
- async delete(path) {
73
- files.delete(path);
74
- },
75
- async list(prefix) {
76
- const listed = [];
77
- let totalSize = 0;
78
- for (const [path, file] of files) {
79
- if (prefix && !path.startsWith(prefix)) continue;
80
- listed.push({
81
- url: `memory://${path}`,
82
- pathname: path,
83
- size: file.bytes.length,
84
- uploadedAt: file.uploadedAt
85
- });
86
- totalSize += file.bytes.length;
87
- }
88
- return { files: listed, totalSize };
89
- },
90
- async head(path) {
91
- const file = files.get(path);
92
- return file ? { size: file.bytes.length, contentType: file.contentType } : null;
93
- },
94
- async signedUrl(path) {
95
- return files.has(path) ? `memory://${path}` : null;
96
- },
97
- /**
98
- * Really fetches, because that is the only way to find out whether the
99
- * URL a plugin builds is the URL it meant. The bytes land in the same
100
- * in-memory store the rest of these read from.
101
- */
102
- async download(url, path, contentType) {
103
- const res = await fetch(url);
104
- if (!res.ok) throw new Error(`Downloading ${url} failed with status ${res.status}`);
105
- const file = {
106
- bytes: new Uint8Array(await res.arrayBuffer()),
107
- contentType: contentType ?? res.headers.get("content-type") ?? "application/octet-stream",
108
- uploadedAt: (/* @__PURE__ */ new Date()).toISOString()
109
- };
110
- files.set(path, file);
111
- return result(path, file);
112
- }
113
- };
114
- return { storage, files };
115
- }
116
- function createLocalState(seed = {}) {
117
- const values = new Map(Object.entries(seed));
118
- const state = {
119
- async get(key) {
120
- return values.has(key) ? values.get(key) : null;
121
- },
122
- async set(key, value) {
123
- values.set(key, value);
124
- },
125
- async delete(key) {
126
- values.delete(key);
127
- },
128
- async list(prefix) {
129
- const keys = [...values.keys()].filter((k) => !prefix || k.startsWith(prefix));
130
- return { keys };
131
- }
132
- };
133
- return { state, values };
134
- }
135
- function absent(name, what) {
136
- throw new Error(
137
- `context.${name} is not available locally. ${what} Pass a stub to simulate({ ${name}: \u2026 }) to test the code around it, or run the step on a canvas to exercise the real thing.`
138
- );
139
- }
140
- async function simulate(plugin, options = {}) {
141
- const startTime = Date.now();
142
- const execute = typeof plugin === "function" ? plugin : plugin?.execute;
143
- const { storage, files } = createLocalStorage(options.storage);
144
- const { state, values } = createLocalState(options.state);
145
- if (typeof execute !== "function") {
146
- return {
147
- status: "error",
148
- output: null,
149
- error: "This module exports no execute function. A plugin is `export default { execute }`, `module.exports.execute = \u2026`, or a default export that is the function itself.",
150
- durationMs: Date.now() - startTime,
151
- files,
152
- state: values
153
- };
154
- }
155
- const context = {
156
- workflowId: options.workflowId ?? "local-workflow",
157
- nodeId: options.nodeId ?? "local-node",
158
- userId: options.userId ?? "local-user",
159
- executionId: options.executionId ?? `local-${Date.now()}`,
160
- startedAt: new Date(startTime).toISOString(),
161
- secrets: options.secrets ?? {},
162
- storage,
163
- state,
164
- connections: options.connections ?? {
165
- get: async () => absent("connections", "It reaches an account somebody connected on the platform.")
166
- },
167
- media: options.media ?? {
168
- run: async () => absent("media", "It runs ffmpeg in a container of its own.")
169
- }
170
- };
171
- const timeoutMs = options.timeoutMs ?? 3e4;
172
- let timer;
173
- const timeout = new Promise((_, reject) => {
174
- timer = setTimeout(
175
- () => reject(new Error(`Plugin execution timed out after ${timeoutMs}ms`)),
176
- timeoutMs
177
- );
178
- });
179
- try {
180
- const raw = await Promise.race([
181
- Promise.resolve(execute(options.input ?? {}, options.config ?? {}, context)),
182
- timeout
183
- ]);
184
- const durationMs = Date.now() - startTime;
185
- if (!raw || typeof raw !== "object") {
186
- return {
187
- status: "error",
188
- output: null,
189
- error: `A plugin must return an object like { output: "success", data: {} }. This one returned ${raw === void 0 ? "undefined" : JSON.stringify(raw)}.`,
190
- durationMs,
191
- files,
192
- state: values
193
- };
194
- }
195
- const output = raw;
196
- if (typeof output.output !== "string") {
197
- return {
198
- status: "error",
199
- output: null,
200
- error: 'The returned object has no `output`. That field names which port the run leaves by \u2014 usually "success" or "error" \u2014 and it has to be one of the `outputs` in your manifest.',
201
- durationMs,
202
- files,
203
- state: values
204
- };
205
- }
206
- return {
207
- status: "success",
208
- output: { ...output, data: output.data ?? {} },
209
- durationMs,
210
- outputPort: output.output,
211
- logs: output.metadata?.logs,
212
- files,
213
- state: values
214
- };
215
- } catch (error) {
216
- const durationMs = Date.now() - startTime;
217
- const message = error instanceof Error ? error.message : String(error);
218
- return {
219
- status: message.includes("timed out") ? "timeout" : "error",
220
- output: null,
221
- error: message,
222
- durationMs,
223
- files,
224
- state: values
225
- };
226
- } finally {
227
- if (timer) clearTimeout(timer);
228
- }
229
- }
230
-
231
61
  // src/platform-rules.ts
232
62
  var ALLOWED_PACKAGES = [
233
63
  {
234
64
  "name": "nodemailer",
235
- "version": "^6.10.0"
65
+ "version": "^10.0.16"
236
66
  },
237
67
  {
238
68
  "name": "lodash",
@@ -448,7 +278,7 @@ var ALLOWED_PACKAGES = [
448
278
  },
449
279
  {
450
280
  "name": "uuid",
451
- "version": "^11.0.5"
281
+ "version": "^11.1.1"
452
282
  },
453
283
  {
454
284
  "name": "nanoid",
@@ -736,7 +566,7 @@ var ALLOWED_PACKAGES = [
736
566
  },
737
567
  {
738
568
  "name": "fast-xml-parser",
739
- "version": "^4.5.1"
569
+ "version": "^5.11.2"
740
570
  },
741
571
  {
742
572
  "name": "xml2js",
@@ -908,7 +738,7 @@ var ALLOWED_PACKAGES = [
908
738
  },
909
739
  {
910
740
  "name": "diff",
911
- "version": "^7.0.0"
741
+ "version": "^8.0.4"
912
742
  },
913
743
  {
914
744
  "name": "diff-match-patch",
@@ -1331,6 +1161,11 @@ var ALLOWED_PACKAGES = [
1331
1161
  "version": "^2.1.8"
1332
1162
  }
1333
1163
  ];
1164
+ var SANDBOX_OVERRIDES = {
1165
+ "exceljs": {
1166
+ "uuid": "^11.1.1"
1167
+ }
1168
+ };
1334
1169
  var RESTRICTED_PATTERNS = [
1335
1170
  /\bprocess\s*\.\s*exit/,
1336
1171
  /\bprocess\s*\.\s*env/,
@@ -1366,10 +1201,392 @@ var RESTRICTED_PATTERNS = [
1366
1201
  /\bError\s*\.\s*captureStackTrace\b/,
1367
1202
  /\bWebAssembly\b/
1368
1203
  ];
1204
+ var MAX_CODE_LENGTH = 1e5;
1205
+ var PLATFORM_NODE_MAJOR = 24;
1206
+ var PROVIDED_GLOBALS = [
1207
+ "fetch",
1208
+ "Request",
1209
+ "Response",
1210
+ "Headers",
1211
+ "URL",
1212
+ "URLSearchParams",
1213
+ "btoa",
1214
+ "atob",
1215
+ "console",
1216
+ "setTimeout",
1217
+ "clearTimeout",
1218
+ "setInterval",
1219
+ "clearInterval",
1220
+ "crypto",
1221
+ "TextEncoder",
1222
+ "TextDecoder",
1223
+ "ArrayBuffer",
1224
+ "Uint8Array",
1225
+ "Uint16Array",
1226
+ "Uint32Array",
1227
+ "Int8Array",
1228
+ "Int16Array",
1229
+ "Int32Array",
1230
+ "Float32Array",
1231
+ "Float64Array",
1232
+ "DataView",
1233
+ "Blob",
1234
+ "FormData",
1235
+ "AbortController",
1236
+ "AbortSignal",
1237
+ "require"
1238
+ ];
1239
+ var BLOCKED_GLOBAL_NAMES = [
1240
+ "process",
1241
+ "global",
1242
+ "module",
1243
+ "exports",
1244
+ "__dirname",
1245
+ "__filename",
1246
+ "eval",
1247
+ "Function",
1248
+ "child_process",
1249
+ "fs",
1250
+ "os",
1251
+ "cluster",
1252
+ "dgram",
1253
+ "dns",
1254
+ "net",
1255
+ "http",
1256
+ "https",
1257
+ "path",
1258
+ "readline",
1259
+ "stream",
1260
+ "tls",
1261
+ "zlib",
1262
+ "buffer",
1263
+ "Buffer",
1264
+ "globalThis",
1265
+ "self",
1266
+ "window",
1267
+ "SharedArrayBuffer",
1268
+ "Atomics",
1269
+ "WeakRef",
1270
+ "FinalizationRegistry",
1271
+ "AsyncFunction",
1272
+ "GeneratorFunction",
1273
+ "AsyncGeneratorFunction",
1274
+ "WebAssembly"
1275
+ ];
1276
+ var SANDBOX_TIERS = [
1277
+ {
1278
+ "id": "standard",
1279
+ "label": "Standard",
1280
+ "vcpus": 0.5,
1281
+ "memoryGb": 4
1282
+ },
1283
+ {
1284
+ "id": "boosted",
1285
+ "label": "Boosted",
1286
+ "vcpus": 1,
1287
+ "memoryGb": 6
1288
+ },
1289
+ {
1290
+ "id": "high",
1291
+ "label": "High",
1292
+ "vcpus": 2,
1293
+ "memoryGb": 8
1294
+ }
1295
+ ];
1296
+ var RETIRED_TIERS = {
1297
+ "max": "high"
1298
+ };
1299
+ var STATE_LIMITS = {
1300
+ /** One value, once encoded as JSON. */
1301
+ maxValueBytes: 128 * 1024,
1302
+ /** The shortest `ttlSeconds` the store accepts. */
1303
+ minTtlSeconds: 60
1304
+ };
1305
+ var SIGNED_URL_SECONDS = { default: 900, min: 60, max: 3600 };
1306
+ var PUSH_CHECK = {
1307
+ /** When manifest.json names no `timeoutMs`. The test file's is not read. */
1308
+ defaultTimeoutMs: 3e4,
1309
+ /** The longest `timeoutMs` a manifest is given. */
1310
+ maxTimeoutMs: 6e4,
1311
+ /** The port a run must leave by when the test file names none. */
1312
+ defaultExpectOutput: "success",
1313
+ /** A declared step (an `action` block) is tested in the portal's editor, not on a push. */
1314
+ declaredStepsTestedOnPush: false
1315
+ };
1316
+
1317
+ // src/runtime.ts
1318
+ var import_node_vm = __toESM(require("node:vm"), 1);
1319
+ function pluginConsole(target = console) {
1320
+ const forward = (level) => (...args) => {
1321
+ const write = target[level] ?? target.log ?? console.log;
1322
+ write.apply(target, args);
1323
+ };
1324
+ return { log: forward("log"), error: forward("error"), warn: forward("warn"), info: forward("info") };
1325
+ }
1326
+ function sandboxGlobals(options = {}) {
1327
+ const host = globalThis;
1328
+ const globals = {};
1329
+ for (const name of PROVIDED_GLOBALS) {
1330
+ if (name === "require") continue;
1331
+ if (name === "console") globals.console = pluginConsole(options.console);
1332
+ else if (name === "fetch") globals.fetch = options.fetch ?? host.fetch;
1333
+ else globals[name] = host[name];
1334
+ }
1335
+ for (const name of BLOCKED_GLOBAL_NAMES) globals[name] = void 0;
1336
+ return globals;
1337
+ }
1338
+ function loadPluginCode(code, require2, options = {}) {
1339
+ const sandbox = sandboxGlobals(options);
1340
+ Object.defineProperty(sandbox, "constructor", { value: void 0, writable: false, configurable: false });
1341
+ const wrapped = '(function(module, exports, context, require) { "use strict"; var console = context.console; ' + code + " })";
1342
+ const script = new import_node_vm.default.Script(wrapped, { filename: "plugin.js" });
1343
+ const context = import_node_vm.default.createContext(sandbox, { name: "plugin-sandbox" });
1344
+ const fn = script.runInContext(context, { timeout: options.timeoutMs ?? 1e4 });
1345
+ const moduleObject = { exports: {} };
1346
+ fn(moduleObject, moduleObject.exports, { ...sandbox }, require2);
1347
+ return moduleObject.exports;
1348
+ }
1349
+ function executeOf(exported) {
1350
+ if (typeof exported === "function") return exported;
1351
+ const object = exported;
1352
+ if (object && typeof object.execute === "function") return object.execute;
1353
+ if (object && typeof object.default === "function") return object.default;
1354
+ return null;
1355
+ }
1356
+
1357
+ // src/simulate.ts
1358
+ function toBytes(data) {
1359
+ return typeof data === "string" ? new TextEncoder().encode(data) : data;
1360
+ }
1361
+ function createLocalStorage(seed = {}) {
1362
+ const files = /* @__PURE__ */ new Map();
1363
+ for (const [path, data] of Object.entries(seed)) {
1364
+ files.set(path, {
1365
+ bytes: toBytes(data),
1366
+ contentType: "application/octet-stream",
1367
+ uploadedAt: (/* @__PURE__ */ new Date()).toISOString()
1368
+ });
1369
+ }
1370
+ const result = (path, file) => ({
1371
+ url: `memory://${path}`,
1372
+ size: file.bytes.length,
1373
+ contentType: file.contentType,
1374
+ uploadedAt: file.uploadedAt,
1375
+ // The real one expires; this one does not, and says so rather than
1376
+ // inventing a date a test might come to rely on.
1377
+ expiresAt: "never (local simulation)"
1378
+ });
1379
+ const storage = {
1380
+ async put(path, data, contentType = "application/octet-stream") {
1381
+ const file = {
1382
+ bytes: toBytes(data),
1383
+ contentType,
1384
+ uploadedAt: (/* @__PURE__ */ new Date()).toISOString()
1385
+ };
1386
+ files.set(path, file);
1387
+ return result(path, file);
1388
+ },
1389
+ async get(path) {
1390
+ return files.get(path)?.bytes ?? null;
1391
+ },
1392
+ async delete(path) {
1393
+ files.delete(path);
1394
+ },
1395
+ async list(prefix) {
1396
+ const listed = [];
1397
+ let totalSize = 0;
1398
+ for (const [path, file] of files) {
1399
+ if (prefix && !path.startsWith(prefix)) continue;
1400
+ listed.push({
1401
+ url: `memory://${path}`,
1402
+ pathname: path,
1403
+ size: file.bytes.length,
1404
+ uploadedAt: file.uploadedAt
1405
+ });
1406
+ totalSize += file.bytes.length;
1407
+ }
1408
+ return { files: listed, totalSize };
1409
+ },
1410
+ async head(path) {
1411
+ const file = files.get(path);
1412
+ return file ? { size: file.bytes.length, contentType: file.contentType } : null;
1413
+ },
1414
+ async signedUrl(path, seconds) {
1415
+ const valid = Math.min(Math.max(Number(seconds) || SIGNED_URL_SECONDS.default, SIGNED_URL_SECONDS.min), SIGNED_URL_SECONDS.max);
1416
+ return files.has(path) ? `memory://${path}?expires=${valid}` : null;
1417
+ },
1418
+ async shareUrl(path, seconds) {
1419
+ const valid = Number(seconds) || SIGNED_URL_SECONDS.default;
1420
+ return files.has(path) ? `memory://share/${path}?expires=${valid}` : null;
1421
+ },
1422
+ /**
1423
+ * Really fetches, because that is the only way to find out whether the
1424
+ * URL a plugin builds is the URL it meant. The bytes land in the same
1425
+ * in-memory store the rest of these read from.
1426
+ */
1427
+ async download(url, path, contentType) {
1428
+ const res = await fetch(url);
1429
+ if (!res.ok) throw new Error(`Downloading ${url} failed with status ${res.status}`);
1430
+ const file = {
1431
+ bytes: new Uint8Array(await res.arrayBuffer()),
1432
+ contentType: contentType ?? res.headers.get("content-type") ?? "application/octet-stream",
1433
+ uploadedAt: (/* @__PURE__ */ new Date()).toISOString()
1434
+ };
1435
+ files.set(path, file);
1436
+ return result(path, file);
1437
+ }
1438
+ };
1439
+ return { storage, files };
1440
+ }
1441
+ function createLocalState(seed = {}) {
1442
+ const values = new Map(Object.entries(seed));
1443
+ const expiries = /* @__PURE__ */ new Map();
1444
+ const expired = (key) => {
1445
+ const at = expiries.get(key);
1446
+ if (at === void 0 || at > Date.now()) return false;
1447
+ values.delete(key);
1448
+ expiries.delete(key);
1449
+ return true;
1450
+ };
1451
+ const state = {
1452
+ async get(key) {
1453
+ if (expired(key)) return null;
1454
+ return values.has(key) ? values.get(key) : null;
1455
+ },
1456
+ async set(key, value, options) {
1457
+ const encoded = JSON.stringify(value);
1458
+ if (encoded === void 0) throw new Error(`state.set("${key}"): the value must be JSON-serialisable.`);
1459
+ const bytes = new TextEncoder().encode(encoded).length;
1460
+ if (bytes > STATE_LIMITS.maxValueBytes) {
1461
+ throw new Error(
1462
+ `state.set("${key}"): the value is ${Math.ceil(bytes / 1024)} KB once encoded, and a value may be at most ${STATE_LIMITS.maxValueBytes / 1024} KB. Put large data in storage and keep its path here.`
1463
+ );
1464
+ }
1465
+ const ttl = options?.ttlSeconds;
1466
+ if (ttl !== void 0 && (typeof ttl !== "number" || ttl < STATE_LIMITS.minTtlSeconds)) {
1467
+ throw new Error(`state.set("${key}"): ttlSeconds must be at least ${STATE_LIMITS.minTtlSeconds}.`);
1468
+ }
1469
+ values.set(key, JSON.parse(encoded));
1470
+ if (ttl !== void 0) expiries.set(key, Date.now() + ttl * 1e3);
1471
+ else expiries.delete(key);
1472
+ },
1473
+ async delete(key) {
1474
+ values.delete(key);
1475
+ expiries.delete(key);
1476
+ },
1477
+ async list(prefix) {
1478
+ const keys = [...values.keys()].filter((key) => !expired(key) && (!prefix || key.startsWith(prefix))).map((key) => {
1479
+ const at = expiries.get(key);
1480
+ return { key, expiresAt: at === void 0 ? null : new Date(at).toISOString() };
1481
+ });
1482
+ return { keys, complete: true };
1483
+ }
1484
+ };
1485
+ return { state, values };
1486
+ }
1487
+ function absent(name, what) {
1488
+ throw new Error(
1489
+ `context.${name} is not available locally. ${what} Pass a stub to simulate({ ${name}: \u2026 }) to test the code around it, or run the step on a canvas to exercise the real thing.`
1490
+ );
1491
+ }
1492
+ async function simulate(plugin, options = {}) {
1493
+ const startTime = Date.now();
1494
+ const execute = executeOf(plugin);
1495
+ const { storage, files } = createLocalStorage(options.storage);
1496
+ const { state, values } = createLocalState(options.state);
1497
+ if (typeof execute !== "function") {
1498
+ return {
1499
+ status: "error",
1500
+ output: null,
1501
+ error: "Plugin must export an execute function: `module.exports = { execute }`, `module.exports.execute = \u2026`, or a function as `module.exports` or `module.exports.default`.",
1502
+ durationMs: Date.now() - startTime,
1503
+ files,
1504
+ state: values
1505
+ };
1506
+ }
1507
+ const context = {
1508
+ workflowId: options.workflowId ?? "local-workflow",
1509
+ nodeId: options.nodeId ?? "local-node",
1510
+ userId: options.userId ?? "local-user",
1511
+ executionId: options.executionId ?? `local-${Date.now()}`,
1512
+ startedAt: new Date(startTime).toISOString(),
1513
+ secrets: options.secrets ?? {},
1514
+ storage,
1515
+ state,
1516
+ // Absent unless a stub is given, as on the platform when no account was
1517
+ // chosen for the step: the push check runs a step with only the accounts
1518
+ // its test file names, and none can be named here.
1519
+ connections: options.connections,
1520
+ media: options.media ?? {
1521
+ run: async () => absent("media", "It runs ffmpeg in a container of its own.")
1522
+ }
1523
+ };
1524
+ const timeoutMs = options.timeoutMs ?? PUSH_CHECK.defaultTimeoutMs;
1525
+ let timer;
1526
+ const timeout = new Promise((_, reject) => {
1527
+ timer = setTimeout(
1528
+ () => reject(new Error(`Plugin execution timed out after ${timeoutMs}ms`)),
1529
+ timeoutMs
1530
+ );
1531
+ });
1532
+ try {
1533
+ const raw = await Promise.race([
1534
+ Promise.resolve(execute(options.input ?? {}, options.config ?? {}, context)),
1535
+ timeout
1536
+ ]);
1537
+ const durationMs = Date.now() - startTime;
1538
+ if (!raw || typeof raw !== "object") {
1539
+ return {
1540
+ status: "error",
1541
+ output: null,
1542
+ error: `A plugin must return an object like { output: "success", data: {} }. This one returned ${raw === void 0 ? "undefined" : JSON.stringify(raw)}.`,
1543
+ durationMs,
1544
+ files,
1545
+ state: values
1546
+ };
1547
+ }
1548
+ const output = raw;
1549
+ if (typeof output.output !== "string") {
1550
+ return {
1551
+ status: "error",
1552
+ output: null,
1553
+ error: 'The returned object has no `output`. That field names which port the run leaves by \u2014 usually "success" or "error" \u2014 and it has to be one of the `outputs` in your manifest.',
1554
+ durationMs,
1555
+ files,
1556
+ state: values
1557
+ };
1558
+ }
1559
+ return {
1560
+ status: "success",
1561
+ output: { ...output, data: output.data ?? {} },
1562
+ durationMs,
1563
+ outputPort: output.output,
1564
+ logs: output.metadata?.logs,
1565
+ files,
1566
+ state: values
1567
+ };
1568
+ } catch (error) {
1569
+ const durationMs = Date.now() - startTime;
1570
+ const message = error && typeof error === "object" && typeof error.message === "string" ? error.message : String(error);
1571
+ return {
1572
+ status: message.includes("timed out") ? "timeout" : "error",
1573
+ output: null,
1574
+ error: message,
1575
+ durationMs,
1576
+ files,
1577
+ state: values
1578
+ };
1579
+ } finally {
1580
+ if (timer) clearTimeout(timer);
1581
+ }
1582
+ }
1369
1583
 
1370
1584
  // src/validate.ts
1371
1585
  var ALLOWED_PACKAGE_NAMES = new Set(ALLOWED_PACKAGES.map((p) => p.name));
1372
1586
  function validatePluginCode(code) {
1587
+ if (code.length > MAX_CODE_LENGTH) {
1588
+ return { valid: false, error: `Plugin code exceeds maximum length of ${Math.round(MAX_CODE_LENGTH / 1e3)}KB.` };
1589
+ }
1373
1590
  const hasExecute = code.includes("module.exports") || code.includes("exports.execute") || code.includes("export");
1374
1591
  if (!hasExecute) {
1375
1592
  return {
@@ -1419,11 +1636,228 @@ function sandboxRequire(load) {
1419
1636
  }
1420
1637
  };
1421
1638
  }
1422
- function loadPluginCode(code, require2) {
1423
- const module2 = { exports: {} };
1424
- const body = new Function("module", "exports", "require", `"use strict"; ${code}`);
1425
- body(module2, module2.exports, require2);
1426
- return module2.exports;
1639
+
1640
+ // src/manifest-check.ts
1641
+ var KNOWN_BLOCKS = /* @__PURE__ */ new Set([
1642
+ "properties",
1643
+ "inputs",
1644
+ "outputs",
1645
+ "outputFields",
1646
+ "auth",
1647
+ "trigger",
1648
+ "action",
1649
+ "timeoutMs",
1650
+ "memoryLimitMb",
1651
+ "icon",
1652
+ "color",
1653
+ "description",
1654
+ "version"
1655
+ ]);
1656
+ var PROPERTY_TYPES = /* @__PURE__ */ new Set([
1657
+ "string",
1658
+ "number",
1659
+ "boolean",
1660
+ "select",
1661
+ "multiselect",
1662
+ "json",
1663
+ "code",
1664
+ "color",
1665
+ "date",
1666
+ "connection",
1667
+ "file",
1668
+ "path",
1669
+ "paths",
1670
+ "secret"
1671
+ ]);
1672
+ var AUTH_TYPES = /* @__PURE__ */ new Set(["oauth2", "api_key", "basic", "none"]);
1673
+ var HOST_KEYS = ["url", "baseUrl", "base_url", "host"];
1674
+ var METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE"]);
1675
+ var BODY_TYPES = /* @__PURE__ */ new Set(["json", "form", "multipart"]);
1676
+ var PAGINATE_FROM = /* @__PURE__ */ new Set(["response", "item", "page", "offset"]);
1677
+ var MAX_ITEM_LIMIT = 1e3;
1678
+ var MAX_PAGES = 50;
1679
+ var MAX_MEMORY_MB = 128;
1680
+ var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1681
+ var isRelativePath = (path) => typeof path === "string" && path.trim() !== "" && !/^[a-z][a-z0-9+.-]*:/i.test(path) && !path.startsWith("//");
1682
+ function checkManifest(raw, hasCode) {
1683
+ const errors = [];
1684
+ const warnings = [];
1685
+ if (!isObject(raw)) {
1686
+ return { ok: false, kind: hasCode ? "code" : "declared", errors: ["manifest.json must be a JSON object."], warnings };
1687
+ }
1688
+ const m = raw;
1689
+ const kind = isObject(m.trigger) ? "trigger" : isObject(m.action) ? isObject(m.action.request) ? "declared" : "outputs-only" : "code";
1690
+ for (const key of Object.keys(m)) {
1691
+ if (!KNOWN_BLOCKS.has(key)) warnings.push(`"${key}" is not something a manifest can declare, and is dropped when the step is saved.`);
1692
+ }
1693
+ const properties = isObject(m.properties) ? m.properties : {};
1694
+ if (m.properties !== void 0 && !isObject(m.properties)) errors.push("properties must be an object of fields.");
1695
+ const typeOf = (name) => typeof name === "string" && isObject(properties[name]) ? properties[name].type : void 0;
1696
+ for (const [key, value] of Object.entries(properties)) {
1697
+ if (!isObject(value)) {
1698
+ errors.push(`properties.${key} must be an object with at least a label and a type.`);
1699
+ continue;
1700
+ }
1701
+ if (typeof value.type !== "string" || !PROPERTY_TYPES.has(value.type)) {
1702
+ warnings.push(`properties.${key} has type ${JSON.stringify(value.type)}, which the platform does not know; it becomes a plain text field.`);
1703
+ }
1704
+ if (typeof value.label !== "string" || !value.label.trim()) {
1705
+ warnings.push(`properties.${key} has no label; the canvas shows "${key}" instead.`);
1706
+ }
1707
+ if ((value.type === "select" || value.type === "multiselect") && value.options === void 0 && value.optionsFrom === void 0) {
1708
+ warnings.push(`properties.${key} is a ${value.type} with no options and no optionsFrom; there is nothing to choose.`);
1709
+ }
1710
+ if (value.options !== void 0) {
1711
+ const options = Array.isArray(value.options) ? value.options : null;
1712
+ if (!options || options.some((o) => !isObject(o) || typeof o.label !== "string" || typeof o.value !== "string")) {
1713
+ errors.push(`properties.${key}.options must be a list of { "label": "\u2026", "value": "\u2026" }, both text.`);
1714
+ }
1715
+ }
1716
+ if (value.optionsFrom !== void 0) checkOptionsFrom(key, value, typeOf, errors);
1717
+ }
1718
+ if (m.auth !== void 0) {
1719
+ if (!isObject(m.auth)) errors.push("auth must be an object.");
1720
+ else {
1721
+ if (typeof m.auth.type !== "string" || !AUTH_TYPES.has(m.auth.type)) {
1722
+ errors.push(`auth.type must be one of ${[...AUTH_TYPES].join(", ")}.`);
1723
+ }
1724
+ for (const key of HOST_KEYS) {
1725
+ if (key in m.auth) errors.push(`auth.${key} is refused: a manifest may not name a host. The provider's base URL belongs to the platform.`);
1726
+ }
1727
+ }
1728
+ }
1729
+ for (const ports of ["inputs", "outputs"]) {
1730
+ if (m[ports] !== void 0 && (!Array.isArray(m[ports]) || m[ports].some((p) => typeof p !== "string"))) {
1731
+ errors.push(`${ports} must be a list of port names.`);
1732
+ }
1733
+ }
1734
+ if (Array.isArray(m.outputs) && m.outputs.length === 0 && kind !== "trigger") {
1735
+ warnings.push("outputs is empty: no line can be drawn from this step.");
1736
+ }
1737
+ if (m.timeoutMs !== void 0) {
1738
+ if (typeof m.timeoutMs !== "number" || m.timeoutMs <= 0) errors.push("timeoutMs must be a number of milliseconds.");
1739
+ else if (m.timeoutMs > PUSH_CHECK.maxTimeoutMs) warnings.push(`timeoutMs ${m.timeoutMs} is cut to ${PUSH_CHECK.maxTimeoutMs}, the most a step is given.`);
1740
+ }
1741
+ if (typeof m.memoryLimitMb === "number" && m.memoryLimitMb > MAX_MEMORY_MB) {
1742
+ warnings.push(`memoryLimitMb ${m.memoryLimitMb} is cut to ${MAX_MEMORY_MB}.`);
1743
+ }
1744
+ if (m.action !== void 0) {
1745
+ if (!isObject(m.action)) errors.push("action must be an object.");
1746
+ else checkAction(m.action, properties, typeOf, errors, warnings);
1747
+ if (hasCode) errors.push("This step has both an action block and code. A step is one or the other: remove index.js, or the action block.");
1748
+ }
1749
+ if (m.trigger !== void 0) {
1750
+ if (!isObject(m.trigger)) errors.push("trigger must be an object.");
1751
+ else checkTrigger(m.trigger, typeOf, errors, warnings);
1752
+ }
1753
+ if (kind === "code" && !hasCode) {
1754
+ errors.push("There is no index.js and no action or trigger block, so this step has nothing to run.");
1755
+ }
1756
+ return { ok: errors.length === 0, kind, errors, warnings };
1757
+ }
1758
+ function checkConnectionName(where, name, typeOf, errors) {
1759
+ if (typeof name !== "string" || !name) {
1760
+ errors.push(`${where} must name one of this step's properties of type "connection".`);
1761
+ } else if (typeOf(name) === void 0) {
1762
+ errors.push(`${where} names "${name}", which is not one of this step's properties.`);
1763
+ } else if (typeOf(name) !== "connection") {
1764
+ errors.push(`${where} names "${name}", which is a ${String(typeOf(name))} property rather than a connection.`);
1765
+ }
1766
+ }
1767
+ function checkAction(a, properties, typeOf, errors, warnings) {
1768
+ const request = a.request;
1769
+ if (request !== void 0) {
1770
+ if (!isObject(request)) {
1771
+ errors.push("action.request must be an object.");
1772
+ return;
1773
+ }
1774
+ const viaConnection = a.connection !== void 0;
1775
+ const viaBaseUrl = a.baseUrl !== void 0;
1776
+ if (viaConnection && viaBaseUrl) errors.push("action has both connection and baseUrl: a request goes as an account or to a named host, never both.");
1777
+ if (!viaConnection && !viaBaseUrl) errors.push("action.request needs either connection (an account to act as) or baseUrl (the https:// host to call).");
1778
+ if (viaConnection) checkConnectionName("action.connection", a.connection, typeOf, errors);
1779
+ if (viaBaseUrl) {
1780
+ if (typeof a.baseUrl !== "string" || !/^https:\/\//i.test(a.baseUrl)) errors.push("action.baseUrl must start with https://.");
1781
+ else if (/\{\{\s*input\./.test(a.baseUrl)) errors.push("action.baseUrl may read the step's fields ({{config.\u2026}}) but never {{input.\u2026}}: what an earlier step hands on must not decide where a key is sent.");
1782
+ }
1783
+ const method = String(request.method ?? "GET").toUpperCase();
1784
+ if (!METHODS.has(method)) errors.push(`action.request.method must be one of ${[...METHODS].join(", ")}.`);
1785
+ if (!isRelativePath(request.path)) errors.push("action.request.path must be a path relative to the base URL, never a full URL.");
1786
+ if (request.bodyType !== void 0 && !BODY_TYPES.has(String(request.bodyType))) {
1787
+ errors.push(`action.request.bodyType must be one of ${[...BODY_TYPES].join(", ")}.`);
1788
+ }
1789
+ if (request.body !== void 0 && method === "GET") warnings.push("action.request has a body on a GET; most APIs ignore it.");
1790
+ if (viaConnection && isObject(request.headers)) {
1791
+ for (const header of Object.keys(request.headers)) {
1792
+ if (header.toLowerCase() === "authorization") warnings.push("action.request sets Authorization while acting as an account; the platform adds the account's credential itself.");
1793
+ }
1794
+ }
1795
+ }
1796
+ if (a.outputs !== void 0 && !isObject(a.outputs)) errors.push("action.outputs must be an object of name to value.");
1797
+ if (a.outputs === void 0) warnings.push("action has no outputs: the step hands nothing on.");
1798
+ for (const key of ["errorPath", "okPath", "okValue"]) {
1799
+ if (a[key] !== void 0 && typeof a[key] !== "string") errors.push(`action.${key} must be text.`);
1800
+ }
1801
+ if (a.items !== void 0) {
1802
+ if (!isObject(a.items) || typeof a.items.path !== "string") errors.push('action.items must be { "path": "\u2026" }, with "" when the answer is the list itself.');
1803
+ else if (a.items.limit !== void 0 && (typeof a.items.limit !== "number" || a.items.limit < 1 || a.items.limit > MAX_ITEM_LIMIT)) {
1804
+ errors.push(`action.items.limit must be a number from 1 to ${MAX_ITEM_LIMIT}.`);
1805
+ }
1806
+ }
1807
+ if (a.paginate !== void 0) {
1808
+ if (!isObject(a.paginate) || !PAGINATE_FROM.has(String(a.paginate.from))) {
1809
+ errors.push(`action.paginate.from must be one of ${[...PAGINATE_FROM].join(", ")}.`);
1810
+ } else if (a.paginate.maxPages !== void 0 && (typeof a.paginate.maxPages !== "number" || a.paginate.maxPages > MAX_PAGES)) {
1811
+ errors.push(`action.paginate.maxPages must be at most ${MAX_PAGES}.`);
1812
+ }
1813
+ if (a.items === void 0) errors.push("action.paginate needs action.items: pages are pages of a list.");
1814
+ }
1815
+ const text = JSON.stringify({ baseUrl: a.baseUrl, request: a.request, outputs: a.outputs });
1816
+ for (const [, key] of text.matchAll(/\{\{\s*config\.([A-Za-z0-9_$]+)/g)) {
1817
+ if (!(key in properties)) warnings.push(`{{config.${key}}} names no property; it is always empty.`);
1818
+ }
1819
+ }
1820
+ function checkTrigger(t, typeOf, errors, warnings) {
1821
+ checkConnectionName("trigger.connection", t.connection, typeOf, errors);
1822
+ if (!isObject(t.request) || !isRelativePath(t.request.path)) errors.push("trigger.request.path must be a path relative to the provider.");
1823
+ else if (t.request.method !== void 0 && !["GET", "POST"].includes(String(t.request.method).toUpperCase())) {
1824
+ errors.push("trigger.request.method must be GET or POST.");
1825
+ }
1826
+ if (typeof t.items !== "string") errors.push('trigger.items must be the dotted path to the list in the answer ("" when the answer is the list).');
1827
+ if (typeof t.id !== "string" || !t.id) errors.push("trigger.id must be the dotted path, within an item, to what makes it unique.");
1828
+ if (t.poll !== void 0 && typeof t.poll !== "number" && (typeof t.poll !== "string" || typeOf(t.poll) === void 0)) {
1829
+ errors.push("trigger.poll must be a number of minutes, or the name of a property that holds one.");
1830
+ }
1831
+ if (t.cursor !== void 0) {
1832
+ if (!isObject(t.cursor) || !["response", "item"].includes(String(t.cursor.from)) || typeof t.cursor.path !== "string") {
1833
+ errors.push('trigger.cursor must be { "from": "response" | "item", "path": "\u2026" }.');
1834
+ } else if (t.cursor.initial !== "now") {
1835
+ warnings.push(`trigger.cursor has no "initial": "now": switching the trigger on starts a run, at the user's cost, for every item already there.`);
1836
+ }
1837
+ }
1838
+ if (t.hydrate !== void 0 && (!isObject(t.hydrate) || !isRelativePath(t.hydrate.path))) {
1839
+ errors.push("trigger.hydrate.path must be a path relative to the provider.");
1840
+ }
1841
+ if (t.maxPerPoll !== void 0 && (typeof t.maxPerPoll !== "number" || t.maxPerPoll < 1)) errors.push("trigger.maxPerPoll must be a positive number.");
1842
+ }
1843
+ function checkOptionsFrom(key, property, typeOf, errors) {
1844
+ const where = `properties.${key}.optionsFrom`;
1845
+ if (property.type !== "select" && property.type !== "multiselect") {
1846
+ errors.push(`${where} only means something on a select or multiselect, and ${key} is a ${String(property.type)}.`);
1847
+ }
1848
+ const o = property.optionsFrom;
1849
+ if (!isObject(o)) {
1850
+ errors.push(`${where} must be an object.`);
1851
+ return;
1852
+ }
1853
+ checkConnectionName(`${where}.connection`, o.connection, typeOf, errors);
1854
+ if (typeof o.label !== "string" || !o.label.trim()) errors.push(`${where}.label must say what each option shows, e.g. {{item.name}}.`);
1855
+ if (typeof o.value !== "string" || !o.value.trim()) errors.push(`${where}.value must say what choosing it stores, e.g. {{item.id}}.`);
1856
+ if (!isObject(o.request) || !isRelativePath(o.request.path)) errors.push(`${where}.request.path must be a path relative to the provider.`);
1857
+ else {
1858
+ if (o.request.method !== void 0 && String(o.request.method).toUpperCase() !== "GET") errors.push(`${where}.request must be a GET: options are read whenever somebody opens the step.`);
1859
+ if (o.request.body !== void 0) errors.push(`${where}.request cannot have a body: it is a GET.`);
1860
+ }
1427
1861
  }
1428
1862
 
1429
1863
  // src/context-api.ts