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