@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.mjs
CHANGED
|
@@ -1,202 +1,8 @@
|
|
|
1
|
-
// src/simulate.ts
|
|
2
|
-
function toBytes(data) {
|
|
3
|
-
return typeof data === "string" ? new TextEncoder().encode(data) : data;
|
|
4
|
-
}
|
|
5
|
-
function createLocalStorage(seed = {}) {
|
|
6
|
-
const files = /* @__PURE__ */ new Map();
|
|
7
|
-
for (const [path, data] of Object.entries(seed)) {
|
|
8
|
-
files.set(path, {
|
|
9
|
-
bytes: toBytes(data),
|
|
10
|
-
contentType: "application/octet-stream",
|
|
11
|
-
uploadedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
12
|
-
});
|
|
13
|
-
}
|
|
14
|
-
const result = (path, file) => ({
|
|
15
|
-
url: `memory://${path}`,
|
|
16
|
-
size: file.bytes.length,
|
|
17
|
-
contentType: file.contentType,
|
|
18
|
-
uploadedAt: file.uploadedAt,
|
|
19
|
-
// The real one expires; this one does not, and says so rather than
|
|
20
|
-
// inventing a date a test might come to rely on.
|
|
21
|
-
expiresAt: "never (local simulation)"
|
|
22
|
-
});
|
|
23
|
-
const storage = {
|
|
24
|
-
async put(path, data, contentType = "application/octet-stream") {
|
|
25
|
-
const file = {
|
|
26
|
-
bytes: toBytes(data),
|
|
27
|
-
contentType,
|
|
28
|
-
uploadedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
29
|
-
};
|
|
30
|
-
files.set(path, file);
|
|
31
|
-
return result(path, file);
|
|
32
|
-
},
|
|
33
|
-
async get(path) {
|
|
34
|
-
return files.get(path)?.bytes ?? null;
|
|
35
|
-
},
|
|
36
|
-
async delete(path) {
|
|
37
|
-
files.delete(path);
|
|
38
|
-
},
|
|
39
|
-
async list(prefix) {
|
|
40
|
-
const listed = [];
|
|
41
|
-
let totalSize = 0;
|
|
42
|
-
for (const [path, file] of files) {
|
|
43
|
-
if (prefix && !path.startsWith(prefix)) continue;
|
|
44
|
-
listed.push({
|
|
45
|
-
url: `memory://${path}`,
|
|
46
|
-
pathname: path,
|
|
47
|
-
size: file.bytes.length,
|
|
48
|
-
uploadedAt: file.uploadedAt
|
|
49
|
-
});
|
|
50
|
-
totalSize += file.bytes.length;
|
|
51
|
-
}
|
|
52
|
-
return { files: listed, totalSize };
|
|
53
|
-
},
|
|
54
|
-
async head(path) {
|
|
55
|
-
const file = files.get(path);
|
|
56
|
-
return file ? { size: file.bytes.length, contentType: file.contentType } : null;
|
|
57
|
-
},
|
|
58
|
-
async signedUrl(path) {
|
|
59
|
-
return files.has(path) ? `memory://${path}` : null;
|
|
60
|
-
},
|
|
61
|
-
/**
|
|
62
|
-
* Really fetches, because that is the only way to find out whether the
|
|
63
|
-
* URL a plugin builds is the URL it meant. The bytes land in the same
|
|
64
|
-
* in-memory store the rest of these read from.
|
|
65
|
-
*/
|
|
66
|
-
async download(url, path, contentType) {
|
|
67
|
-
const res = await fetch(url);
|
|
68
|
-
if (!res.ok) throw new Error(`Downloading ${url} failed with status ${res.status}`);
|
|
69
|
-
const file = {
|
|
70
|
-
bytes: new Uint8Array(await res.arrayBuffer()),
|
|
71
|
-
contentType: contentType ?? res.headers.get("content-type") ?? "application/octet-stream",
|
|
72
|
-
uploadedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
73
|
-
};
|
|
74
|
-
files.set(path, file);
|
|
75
|
-
return result(path, file);
|
|
76
|
-
}
|
|
77
|
-
};
|
|
78
|
-
return { storage, files };
|
|
79
|
-
}
|
|
80
|
-
function createLocalState(seed = {}) {
|
|
81
|
-
const values = new Map(Object.entries(seed));
|
|
82
|
-
const state = {
|
|
83
|
-
async get(key) {
|
|
84
|
-
return values.has(key) ? values.get(key) : null;
|
|
85
|
-
},
|
|
86
|
-
async set(key, value) {
|
|
87
|
-
values.set(key, value);
|
|
88
|
-
},
|
|
89
|
-
async delete(key) {
|
|
90
|
-
values.delete(key);
|
|
91
|
-
},
|
|
92
|
-
async list(prefix) {
|
|
93
|
-
const keys = [...values.keys()].filter((k) => !prefix || k.startsWith(prefix));
|
|
94
|
-
return { keys };
|
|
95
|
-
}
|
|
96
|
-
};
|
|
97
|
-
return { state, values };
|
|
98
|
-
}
|
|
99
|
-
function absent(name, what) {
|
|
100
|
-
throw new Error(
|
|
101
|
-
`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.`
|
|
102
|
-
);
|
|
103
|
-
}
|
|
104
|
-
async function simulate(plugin, options = {}) {
|
|
105
|
-
const startTime = Date.now();
|
|
106
|
-
const execute = typeof plugin === "function" ? plugin : plugin?.execute;
|
|
107
|
-
const { storage, files } = createLocalStorage(options.storage);
|
|
108
|
-
const { state, values } = createLocalState(options.state);
|
|
109
|
-
if (typeof execute !== "function") {
|
|
110
|
-
return {
|
|
111
|
-
status: "error",
|
|
112
|
-
output: null,
|
|
113
|
-
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.",
|
|
114
|
-
durationMs: Date.now() - startTime,
|
|
115
|
-
files,
|
|
116
|
-
state: values
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
const context = {
|
|
120
|
-
workflowId: options.workflowId ?? "local-workflow",
|
|
121
|
-
nodeId: options.nodeId ?? "local-node",
|
|
122
|
-
userId: options.userId ?? "local-user",
|
|
123
|
-
executionId: options.executionId ?? `local-${Date.now()}`,
|
|
124
|
-
startedAt: new Date(startTime).toISOString(),
|
|
125
|
-
secrets: options.secrets ?? {},
|
|
126
|
-
storage,
|
|
127
|
-
state,
|
|
128
|
-
connections: options.connections ?? {
|
|
129
|
-
get: async () => absent("connections", "It reaches an account somebody connected on the platform.")
|
|
130
|
-
},
|
|
131
|
-
media: options.media ?? {
|
|
132
|
-
run: async () => absent("media", "It runs ffmpeg in a container of its own.")
|
|
133
|
-
}
|
|
134
|
-
};
|
|
135
|
-
const timeoutMs = options.timeoutMs ?? 3e4;
|
|
136
|
-
let timer;
|
|
137
|
-
const timeout = new Promise((_, reject) => {
|
|
138
|
-
timer = setTimeout(
|
|
139
|
-
() => reject(new Error(`Plugin execution timed out after ${timeoutMs}ms`)),
|
|
140
|
-
timeoutMs
|
|
141
|
-
);
|
|
142
|
-
});
|
|
143
|
-
try {
|
|
144
|
-
const raw = await Promise.race([
|
|
145
|
-
Promise.resolve(execute(options.input ?? {}, options.config ?? {}, context)),
|
|
146
|
-
timeout
|
|
147
|
-
]);
|
|
148
|
-
const durationMs = Date.now() - startTime;
|
|
149
|
-
if (!raw || typeof raw !== "object") {
|
|
150
|
-
return {
|
|
151
|
-
status: "error",
|
|
152
|
-
output: null,
|
|
153
|
-
error: `A plugin must return an object like { output: "success", data: {} }. This one returned ${raw === void 0 ? "undefined" : JSON.stringify(raw)}.`,
|
|
154
|
-
durationMs,
|
|
155
|
-
files,
|
|
156
|
-
state: values
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
const output = raw;
|
|
160
|
-
if (typeof output.output !== "string") {
|
|
161
|
-
return {
|
|
162
|
-
status: "error",
|
|
163
|
-
output: null,
|
|
164
|
-
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.',
|
|
165
|
-
durationMs,
|
|
166
|
-
files,
|
|
167
|
-
state: values
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
return {
|
|
171
|
-
status: "success",
|
|
172
|
-
output: { ...output, data: output.data ?? {} },
|
|
173
|
-
durationMs,
|
|
174
|
-
outputPort: output.output,
|
|
175
|
-
logs: output.metadata?.logs,
|
|
176
|
-
files,
|
|
177
|
-
state: values
|
|
178
|
-
};
|
|
179
|
-
} catch (error) {
|
|
180
|
-
const durationMs = Date.now() - startTime;
|
|
181
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
182
|
-
return {
|
|
183
|
-
status: message.includes("timed out") ? "timeout" : "error",
|
|
184
|
-
output: null,
|
|
185
|
-
error: message,
|
|
186
|
-
durationMs,
|
|
187
|
-
files,
|
|
188
|
-
state: values
|
|
189
|
-
};
|
|
190
|
-
} finally {
|
|
191
|
-
if (timer) clearTimeout(timer);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
1
|
// src/platform-rules.ts
|
|
196
2
|
var ALLOWED_PACKAGES = [
|
|
197
3
|
{
|
|
198
4
|
"name": "nodemailer",
|
|
199
|
-
"version": "^
|
|
5
|
+
"version": "^10.0.16"
|
|
200
6
|
},
|
|
201
7
|
{
|
|
202
8
|
"name": "lodash",
|
|
@@ -412,7 +218,7 @@ var ALLOWED_PACKAGES = [
|
|
|
412
218
|
},
|
|
413
219
|
{
|
|
414
220
|
"name": "uuid",
|
|
415
|
-
"version": "^11.
|
|
221
|
+
"version": "^11.1.1"
|
|
416
222
|
},
|
|
417
223
|
{
|
|
418
224
|
"name": "nanoid",
|
|
@@ -700,7 +506,7 @@ var ALLOWED_PACKAGES = [
|
|
|
700
506
|
},
|
|
701
507
|
{
|
|
702
508
|
"name": "fast-xml-parser",
|
|
703
|
-
"version": "^
|
|
509
|
+
"version": "^5.11.2"
|
|
704
510
|
},
|
|
705
511
|
{
|
|
706
512
|
"name": "xml2js",
|
|
@@ -872,7 +678,7 @@ var ALLOWED_PACKAGES = [
|
|
|
872
678
|
},
|
|
873
679
|
{
|
|
874
680
|
"name": "diff",
|
|
875
|
-
"version": "^
|
|
681
|
+
"version": "^8.0.4"
|
|
876
682
|
},
|
|
877
683
|
{
|
|
878
684
|
"name": "diff-match-patch",
|
|
@@ -1295,6 +1101,11 @@ var ALLOWED_PACKAGES = [
|
|
|
1295
1101
|
"version": "^2.1.8"
|
|
1296
1102
|
}
|
|
1297
1103
|
];
|
|
1104
|
+
var SANDBOX_OVERRIDES = {
|
|
1105
|
+
"exceljs": {
|
|
1106
|
+
"uuid": "^11.1.1"
|
|
1107
|
+
}
|
|
1108
|
+
};
|
|
1298
1109
|
var RESTRICTED_PATTERNS = [
|
|
1299
1110
|
/\bprocess\s*\.\s*exit/,
|
|
1300
1111
|
/\bprocess\s*\.\s*env/,
|
|
@@ -1330,10 +1141,392 @@ var RESTRICTED_PATTERNS = [
|
|
|
1330
1141
|
/\bError\s*\.\s*captureStackTrace\b/,
|
|
1331
1142
|
/\bWebAssembly\b/
|
|
1332
1143
|
];
|
|
1144
|
+
var MAX_CODE_LENGTH = 1e5;
|
|
1145
|
+
var PLATFORM_NODE_MAJOR = 24;
|
|
1146
|
+
var PROVIDED_GLOBALS = [
|
|
1147
|
+
"fetch",
|
|
1148
|
+
"Request",
|
|
1149
|
+
"Response",
|
|
1150
|
+
"Headers",
|
|
1151
|
+
"URL",
|
|
1152
|
+
"URLSearchParams",
|
|
1153
|
+
"btoa",
|
|
1154
|
+
"atob",
|
|
1155
|
+
"console",
|
|
1156
|
+
"setTimeout",
|
|
1157
|
+
"clearTimeout",
|
|
1158
|
+
"setInterval",
|
|
1159
|
+
"clearInterval",
|
|
1160
|
+
"crypto",
|
|
1161
|
+
"TextEncoder",
|
|
1162
|
+
"TextDecoder",
|
|
1163
|
+
"ArrayBuffer",
|
|
1164
|
+
"Uint8Array",
|
|
1165
|
+
"Uint16Array",
|
|
1166
|
+
"Uint32Array",
|
|
1167
|
+
"Int8Array",
|
|
1168
|
+
"Int16Array",
|
|
1169
|
+
"Int32Array",
|
|
1170
|
+
"Float32Array",
|
|
1171
|
+
"Float64Array",
|
|
1172
|
+
"DataView",
|
|
1173
|
+
"Blob",
|
|
1174
|
+
"FormData",
|
|
1175
|
+
"AbortController",
|
|
1176
|
+
"AbortSignal",
|
|
1177
|
+
"require"
|
|
1178
|
+
];
|
|
1179
|
+
var BLOCKED_GLOBAL_NAMES = [
|
|
1180
|
+
"process",
|
|
1181
|
+
"global",
|
|
1182
|
+
"module",
|
|
1183
|
+
"exports",
|
|
1184
|
+
"__dirname",
|
|
1185
|
+
"__filename",
|
|
1186
|
+
"eval",
|
|
1187
|
+
"Function",
|
|
1188
|
+
"child_process",
|
|
1189
|
+
"fs",
|
|
1190
|
+
"os",
|
|
1191
|
+
"cluster",
|
|
1192
|
+
"dgram",
|
|
1193
|
+
"dns",
|
|
1194
|
+
"net",
|
|
1195
|
+
"http",
|
|
1196
|
+
"https",
|
|
1197
|
+
"path",
|
|
1198
|
+
"readline",
|
|
1199
|
+
"stream",
|
|
1200
|
+
"tls",
|
|
1201
|
+
"zlib",
|
|
1202
|
+
"buffer",
|
|
1203
|
+
"Buffer",
|
|
1204
|
+
"globalThis",
|
|
1205
|
+
"self",
|
|
1206
|
+
"window",
|
|
1207
|
+
"SharedArrayBuffer",
|
|
1208
|
+
"Atomics",
|
|
1209
|
+
"WeakRef",
|
|
1210
|
+
"FinalizationRegistry",
|
|
1211
|
+
"AsyncFunction",
|
|
1212
|
+
"GeneratorFunction",
|
|
1213
|
+
"AsyncGeneratorFunction",
|
|
1214
|
+
"WebAssembly"
|
|
1215
|
+
];
|
|
1216
|
+
var SANDBOX_TIERS = [
|
|
1217
|
+
{
|
|
1218
|
+
"id": "standard",
|
|
1219
|
+
"label": "Standard",
|
|
1220
|
+
"vcpus": 0.5,
|
|
1221
|
+
"memoryGb": 4
|
|
1222
|
+
},
|
|
1223
|
+
{
|
|
1224
|
+
"id": "boosted",
|
|
1225
|
+
"label": "Boosted",
|
|
1226
|
+
"vcpus": 1,
|
|
1227
|
+
"memoryGb": 6
|
|
1228
|
+
},
|
|
1229
|
+
{
|
|
1230
|
+
"id": "high",
|
|
1231
|
+
"label": "High",
|
|
1232
|
+
"vcpus": 2,
|
|
1233
|
+
"memoryGb": 8
|
|
1234
|
+
}
|
|
1235
|
+
];
|
|
1236
|
+
var RETIRED_TIERS = {
|
|
1237
|
+
"max": "high"
|
|
1238
|
+
};
|
|
1239
|
+
var STATE_LIMITS = {
|
|
1240
|
+
/** One value, once encoded as JSON. */
|
|
1241
|
+
maxValueBytes: 128 * 1024,
|
|
1242
|
+
/** The shortest `ttlSeconds` the store accepts. */
|
|
1243
|
+
minTtlSeconds: 60
|
|
1244
|
+
};
|
|
1245
|
+
var SIGNED_URL_SECONDS = { default: 900, min: 60, max: 3600 };
|
|
1246
|
+
var PUSH_CHECK = {
|
|
1247
|
+
/** When manifest.json names no `timeoutMs`. The test file's is not read. */
|
|
1248
|
+
defaultTimeoutMs: 3e4,
|
|
1249
|
+
/** The longest `timeoutMs` a manifest is given. */
|
|
1250
|
+
maxTimeoutMs: 6e4,
|
|
1251
|
+
/** The port a run must leave by when the test file names none. */
|
|
1252
|
+
defaultExpectOutput: "success",
|
|
1253
|
+
/** A declared step (an `action` block) is tested in the portal's editor, not on a push. */
|
|
1254
|
+
declaredStepsTestedOnPush: false
|
|
1255
|
+
};
|
|
1256
|
+
|
|
1257
|
+
// src/runtime.ts
|
|
1258
|
+
import vm from "node:vm";
|
|
1259
|
+
function pluginConsole(target = console) {
|
|
1260
|
+
const forward = (level) => (...args) => {
|
|
1261
|
+
const write = target[level] ?? target.log ?? console.log;
|
|
1262
|
+
write.apply(target, args);
|
|
1263
|
+
};
|
|
1264
|
+
return { log: forward("log"), error: forward("error"), warn: forward("warn"), info: forward("info") };
|
|
1265
|
+
}
|
|
1266
|
+
function sandboxGlobals(options = {}) {
|
|
1267
|
+
const host = globalThis;
|
|
1268
|
+
const globals = {};
|
|
1269
|
+
for (const name of PROVIDED_GLOBALS) {
|
|
1270
|
+
if (name === "require") continue;
|
|
1271
|
+
if (name === "console") globals.console = pluginConsole(options.console);
|
|
1272
|
+
else if (name === "fetch") globals.fetch = options.fetch ?? host.fetch;
|
|
1273
|
+
else globals[name] = host[name];
|
|
1274
|
+
}
|
|
1275
|
+
for (const name of BLOCKED_GLOBAL_NAMES) globals[name] = void 0;
|
|
1276
|
+
return globals;
|
|
1277
|
+
}
|
|
1278
|
+
function loadPluginCode(code, require2, options = {}) {
|
|
1279
|
+
const sandbox = sandboxGlobals(options);
|
|
1280
|
+
Object.defineProperty(sandbox, "constructor", { value: void 0, writable: false, configurable: false });
|
|
1281
|
+
const wrapped = '(function(module, exports, context, require) { "use strict"; var console = context.console; ' + code + " })";
|
|
1282
|
+
const script = new vm.Script(wrapped, { filename: "plugin.js" });
|
|
1283
|
+
const context = vm.createContext(sandbox, { name: "plugin-sandbox" });
|
|
1284
|
+
const fn = script.runInContext(context, { timeout: options.timeoutMs ?? 1e4 });
|
|
1285
|
+
const moduleObject = { exports: {} };
|
|
1286
|
+
fn(moduleObject, moduleObject.exports, { ...sandbox }, require2);
|
|
1287
|
+
return moduleObject.exports;
|
|
1288
|
+
}
|
|
1289
|
+
function executeOf(exported) {
|
|
1290
|
+
if (typeof exported === "function") return exported;
|
|
1291
|
+
const object = exported;
|
|
1292
|
+
if (object && typeof object.execute === "function") return object.execute;
|
|
1293
|
+
if (object && typeof object.default === "function") return object.default;
|
|
1294
|
+
return null;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
// src/simulate.ts
|
|
1298
|
+
function toBytes(data) {
|
|
1299
|
+
return typeof data === "string" ? new TextEncoder().encode(data) : data;
|
|
1300
|
+
}
|
|
1301
|
+
function createLocalStorage(seed = {}) {
|
|
1302
|
+
const files = /* @__PURE__ */ new Map();
|
|
1303
|
+
for (const [path, data] of Object.entries(seed)) {
|
|
1304
|
+
files.set(path, {
|
|
1305
|
+
bytes: toBytes(data),
|
|
1306
|
+
contentType: "application/octet-stream",
|
|
1307
|
+
uploadedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1308
|
+
});
|
|
1309
|
+
}
|
|
1310
|
+
const result = (path, file) => ({
|
|
1311
|
+
url: `memory://${path}`,
|
|
1312
|
+
size: file.bytes.length,
|
|
1313
|
+
contentType: file.contentType,
|
|
1314
|
+
uploadedAt: file.uploadedAt,
|
|
1315
|
+
// The real one expires; this one does not, and says so rather than
|
|
1316
|
+
// inventing a date a test might come to rely on.
|
|
1317
|
+
expiresAt: "never (local simulation)"
|
|
1318
|
+
});
|
|
1319
|
+
const storage = {
|
|
1320
|
+
async put(path, data, contentType = "application/octet-stream") {
|
|
1321
|
+
const file = {
|
|
1322
|
+
bytes: toBytes(data),
|
|
1323
|
+
contentType,
|
|
1324
|
+
uploadedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1325
|
+
};
|
|
1326
|
+
files.set(path, file);
|
|
1327
|
+
return result(path, file);
|
|
1328
|
+
},
|
|
1329
|
+
async get(path) {
|
|
1330
|
+
return files.get(path)?.bytes ?? null;
|
|
1331
|
+
},
|
|
1332
|
+
async delete(path) {
|
|
1333
|
+
files.delete(path);
|
|
1334
|
+
},
|
|
1335
|
+
async list(prefix) {
|
|
1336
|
+
const listed = [];
|
|
1337
|
+
let totalSize = 0;
|
|
1338
|
+
for (const [path, file] of files) {
|
|
1339
|
+
if (prefix && !path.startsWith(prefix)) continue;
|
|
1340
|
+
listed.push({
|
|
1341
|
+
url: `memory://${path}`,
|
|
1342
|
+
pathname: path,
|
|
1343
|
+
size: file.bytes.length,
|
|
1344
|
+
uploadedAt: file.uploadedAt
|
|
1345
|
+
});
|
|
1346
|
+
totalSize += file.bytes.length;
|
|
1347
|
+
}
|
|
1348
|
+
return { files: listed, totalSize };
|
|
1349
|
+
},
|
|
1350
|
+
async head(path) {
|
|
1351
|
+
const file = files.get(path);
|
|
1352
|
+
return file ? { size: file.bytes.length, contentType: file.contentType } : null;
|
|
1353
|
+
},
|
|
1354
|
+
async signedUrl(path, seconds) {
|
|
1355
|
+
const valid = Math.min(Math.max(Number(seconds) || SIGNED_URL_SECONDS.default, SIGNED_URL_SECONDS.min), SIGNED_URL_SECONDS.max);
|
|
1356
|
+
return files.has(path) ? `memory://${path}?expires=${valid}` : null;
|
|
1357
|
+
},
|
|
1358
|
+
async shareUrl(path, seconds) {
|
|
1359
|
+
const valid = Number(seconds) || SIGNED_URL_SECONDS.default;
|
|
1360
|
+
return files.has(path) ? `memory://share/${path}?expires=${valid}` : null;
|
|
1361
|
+
},
|
|
1362
|
+
/**
|
|
1363
|
+
* Really fetches, because that is the only way to find out whether the
|
|
1364
|
+
* URL a plugin builds is the URL it meant. The bytes land in the same
|
|
1365
|
+
* in-memory store the rest of these read from.
|
|
1366
|
+
*/
|
|
1367
|
+
async download(url, path, contentType) {
|
|
1368
|
+
const res = await fetch(url);
|
|
1369
|
+
if (!res.ok) throw new Error(`Downloading ${url} failed with status ${res.status}`);
|
|
1370
|
+
const file = {
|
|
1371
|
+
bytes: new Uint8Array(await res.arrayBuffer()),
|
|
1372
|
+
contentType: contentType ?? res.headers.get("content-type") ?? "application/octet-stream",
|
|
1373
|
+
uploadedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1374
|
+
};
|
|
1375
|
+
files.set(path, file);
|
|
1376
|
+
return result(path, file);
|
|
1377
|
+
}
|
|
1378
|
+
};
|
|
1379
|
+
return { storage, files };
|
|
1380
|
+
}
|
|
1381
|
+
function createLocalState(seed = {}) {
|
|
1382
|
+
const values = new Map(Object.entries(seed));
|
|
1383
|
+
const expiries = /* @__PURE__ */ new Map();
|
|
1384
|
+
const expired = (key) => {
|
|
1385
|
+
const at = expiries.get(key);
|
|
1386
|
+
if (at === void 0 || at > Date.now()) return false;
|
|
1387
|
+
values.delete(key);
|
|
1388
|
+
expiries.delete(key);
|
|
1389
|
+
return true;
|
|
1390
|
+
};
|
|
1391
|
+
const state = {
|
|
1392
|
+
async get(key) {
|
|
1393
|
+
if (expired(key)) return null;
|
|
1394
|
+
return values.has(key) ? values.get(key) : null;
|
|
1395
|
+
},
|
|
1396
|
+
async set(key, value, options) {
|
|
1397
|
+
const encoded = JSON.stringify(value);
|
|
1398
|
+
if (encoded === void 0) throw new Error(`state.set("${key}"): the value must be JSON-serialisable.`);
|
|
1399
|
+
const bytes = new TextEncoder().encode(encoded).length;
|
|
1400
|
+
if (bytes > STATE_LIMITS.maxValueBytes) {
|
|
1401
|
+
throw new Error(
|
|
1402
|
+
`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.`
|
|
1403
|
+
);
|
|
1404
|
+
}
|
|
1405
|
+
const ttl = options?.ttlSeconds;
|
|
1406
|
+
if (ttl !== void 0 && (typeof ttl !== "number" || ttl < STATE_LIMITS.minTtlSeconds)) {
|
|
1407
|
+
throw new Error(`state.set("${key}"): ttlSeconds must be at least ${STATE_LIMITS.minTtlSeconds}.`);
|
|
1408
|
+
}
|
|
1409
|
+
values.set(key, JSON.parse(encoded));
|
|
1410
|
+
if (ttl !== void 0) expiries.set(key, Date.now() + ttl * 1e3);
|
|
1411
|
+
else expiries.delete(key);
|
|
1412
|
+
},
|
|
1413
|
+
async delete(key) {
|
|
1414
|
+
values.delete(key);
|
|
1415
|
+
expiries.delete(key);
|
|
1416
|
+
},
|
|
1417
|
+
async list(prefix) {
|
|
1418
|
+
const keys = [...values.keys()].filter((key) => !expired(key) && (!prefix || key.startsWith(prefix))).map((key) => {
|
|
1419
|
+
const at = expiries.get(key);
|
|
1420
|
+
return { key, expiresAt: at === void 0 ? null : new Date(at).toISOString() };
|
|
1421
|
+
});
|
|
1422
|
+
return { keys, complete: true };
|
|
1423
|
+
}
|
|
1424
|
+
};
|
|
1425
|
+
return { state, values };
|
|
1426
|
+
}
|
|
1427
|
+
function absent(name, what) {
|
|
1428
|
+
throw new Error(
|
|
1429
|
+
`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.`
|
|
1430
|
+
);
|
|
1431
|
+
}
|
|
1432
|
+
async function simulate(plugin, options = {}) {
|
|
1433
|
+
const startTime = Date.now();
|
|
1434
|
+
const execute = executeOf(plugin);
|
|
1435
|
+
const { storage, files } = createLocalStorage(options.storage);
|
|
1436
|
+
const { state, values } = createLocalState(options.state);
|
|
1437
|
+
if (typeof execute !== "function") {
|
|
1438
|
+
return {
|
|
1439
|
+
status: "error",
|
|
1440
|
+
output: null,
|
|
1441
|
+
error: "Plugin must export an execute function: `module.exports = { execute }`, `module.exports.execute = \u2026`, or a function as `module.exports` or `module.exports.default`.",
|
|
1442
|
+
durationMs: Date.now() - startTime,
|
|
1443
|
+
files,
|
|
1444
|
+
state: values
|
|
1445
|
+
};
|
|
1446
|
+
}
|
|
1447
|
+
const context = {
|
|
1448
|
+
workflowId: options.workflowId ?? "local-workflow",
|
|
1449
|
+
nodeId: options.nodeId ?? "local-node",
|
|
1450
|
+
userId: options.userId ?? "local-user",
|
|
1451
|
+
executionId: options.executionId ?? `local-${Date.now()}`,
|
|
1452
|
+
startedAt: new Date(startTime).toISOString(),
|
|
1453
|
+
secrets: options.secrets ?? {},
|
|
1454
|
+
storage,
|
|
1455
|
+
state,
|
|
1456
|
+
// Absent unless a stub is given, as on the platform when no account was
|
|
1457
|
+
// chosen for the step: the push check runs a step with only the accounts
|
|
1458
|
+
// its test file names, and none can be named here.
|
|
1459
|
+
connections: options.connections,
|
|
1460
|
+
media: options.media ?? {
|
|
1461
|
+
run: async () => absent("media", "It runs ffmpeg in a container of its own.")
|
|
1462
|
+
}
|
|
1463
|
+
};
|
|
1464
|
+
const timeoutMs = options.timeoutMs ?? PUSH_CHECK.defaultTimeoutMs;
|
|
1465
|
+
let timer;
|
|
1466
|
+
const timeout = new Promise((_, reject) => {
|
|
1467
|
+
timer = setTimeout(
|
|
1468
|
+
() => reject(new Error(`Plugin execution timed out after ${timeoutMs}ms`)),
|
|
1469
|
+
timeoutMs
|
|
1470
|
+
);
|
|
1471
|
+
});
|
|
1472
|
+
try {
|
|
1473
|
+
const raw = await Promise.race([
|
|
1474
|
+
Promise.resolve(execute(options.input ?? {}, options.config ?? {}, context)),
|
|
1475
|
+
timeout
|
|
1476
|
+
]);
|
|
1477
|
+
const durationMs = Date.now() - startTime;
|
|
1478
|
+
if (!raw || typeof raw !== "object") {
|
|
1479
|
+
return {
|
|
1480
|
+
status: "error",
|
|
1481
|
+
output: null,
|
|
1482
|
+
error: `A plugin must return an object like { output: "success", data: {} }. This one returned ${raw === void 0 ? "undefined" : JSON.stringify(raw)}.`,
|
|
1483
|
+
durationMs,
|
|
1484
|
+
files,
|
|
1485
|
+
state: values
|
|
1486
|
+
};
|
|
1487
|
+
}
|
|
1488
|
+
const output = raw;
|
|
1489
|
+
if (typeof output.output !== "string") {
|
|
1490
|
+
return {
|
|
1491
|
+
status: "error",
|
|
1492
|
+
output: null,
|
|
1493
|
+
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.',
|
|
1494
|
+
durationMs,
|
|
1495
|
+
files,
|
|
1496
|
+
state: values
|
|
1497
|
+
};
|
|
1498
|
+
}
|
|
1499
|
+
return {
|
|
1500
|
+
status: "success",
|
|
1501
|
+
output: { ...output, data: output.data ?? {} },
|
|
1502
|
+
durationMs,
|
|
1503
|
+
outputPort: output.output,
|
|
1504
|
+
logs: output.metadata?.logs,
|
|
1505
|
+
files,
|
|
1506
|
+
state: values
|
|
1507
|
+
};
|
|
1508
|
+
} catch (error) {
|
|
1509
|
+
const durationMs = Date.now() - startTime;
|
|
1510
|
+
const message = error && typeof error === "object" && typeof error.message === "string" ? error.message : String(error);
|
|
1511
|
+
return {
|
|
1512
|
+
status: message.includes("timed out") ? "timeout" : "error",
|
|
1513
|
+
output: null,
|
|
1514
|
+
error: message,
|
|
1515
|
+
durationMs,
|
|
1516
|
+
files,
|
|
1517
|
+
state: values
|
|
1518
|
+
};
|
|
1519
|
+
} finally {
|
|
1520
|
+
if (timer) clearTimeout(timer);
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1333
1523
|
|
|
1334
1524
|
// src/validate.ts
|
|
1335
1525
|
var ALLOWED_PACKAGE_NAMES = new Set(ALLOWED_PACKAGES.map((p) => p.name));
|
|
1336
1526
|
function validatePluginCode(code) {
|
|
1527
|
+
if (code.length > MAX_CODE_LENGTH) {
|
|
1528
|
+
return { valid: false, error: `Plugin code exceeds maximum length of ${Math.round(MAX_CODE_LENGTH / 1e3)}KB.` };
|
|
1529
|
+
}
|
|
1337
1530
|
const hasExecute = code.includes("module.exports") || code.includes("exports.execute") || code.includes("export");
|
|
1338
1531
|
if (!hasExecute) {
|
|
1339
1532
|
return {
|
|
@@ -1383,11 +1576,248 @@ function sandboxRequire(load) {
|
|
|
1383
1576
|
}
|
|
1384
1577
|
};
|
|
1385
1578
|
}
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1579
|
+
|
|
1580
|
+
// src/manifest-check.ts
|
|
1581
|
+
var KNOWN_BLOCKS = /* @__PURE__ */ new Set([
|
|
1582
|
+
"properties",
|
|
1583
|
+
"inputs",
|
|
1584
|
+
"outputs",
|
|
1585
|
+
"outputFields",
|
|
1586
|
+
"auth",
|
|
1587
|
+
"trigger",
|
|
1588
|
+
"action",
|
|
1589
|
+
"timeoutMs",
|
|
1590
|
+
"memoryLimitMb",
|
|
1591
|
+
"icon",
|
|
1592
|
+
"color",
|
|
1593
|
+
"description",
|
|
1594
|
+
"version"
|
|
1595
|
+
]);
|
|
1596
|
+
var PROPERTY_TYPES = /* @__PURE__ */ new Set([
|
|
1597
|
+
"string",
|
|
1598
|
+
"number",
|
|
1599
|
+
"boolean",
|
|
1600
|
+
"select",
|
|
1601
|
+
"multiselect",
|
|
1602
|
+
"json",
|
|
1603
|
+
"code",
|
|
1604
|
+
"color",
|
|
1605
|
+
"date",
|
|
1606
|
+
"connection",
|
|
1607
|
+
"file",
|
|
1608
|
+
"path",
|
|
1609
|
+
"paths",
|
|
1610
|
+
"secret"
|
|
1611
|
+
]);
|
|
1612
|
+
var AUTH_TYPES = /* @__PURE__ */ new Set(["oauth2", "api_key", "basic", "none"]);
|
|
1613
|
+
var HOST_KEYS = ["url", "baseUrl", "base_url", "host"];
|
|
1614
|
+
var METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE"]);
|
|
1615
|
+
var BODY_TYPES = /* @__PURE__ */ new Set(["json", "form", "multipart"]);
|
|
1616
|
+
var PAGINATE_FROM = /* @__PURE__ */ new Set(["response", "item", "page", "offset"]);
|
|
1617
|
+
var MAX_ITEM_LIMIT = 1e3;
|
|
1618
|
+
var MAX_PAGES = 50;
|
|
1619
|
+
var MAX_MEMORY_MB = 128;
|
|
1620
|
+
var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1621
|
+
var isRelativePath = (path) => typeof path === "string" && path.trim() !== "" && !/^[a-z][a-z0-9+.-]*:/i.test(path) && !path.startsWith("//");
|
|
1622
|
+
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"`;
|
|
1623
|
+
function accountUseIn(manifest) {
|
|
1624
|
+
if (!isObject(manifest)) return [];
|
|
1625
|
+
const m = manifest;
|
|
1626
|
+
const uses = [];
|
|
1627
|
+
if (isObject(m.properties)) {
|
|
1628
|
+
for (const [key, value] of Object.entries(m.properties)) {
|
|
1629
|
+
if (!isObject(value)) continue;
|
|
1630
|
+
if (value.type === "connection") uses.push(`properties.${key} is a connection: an account to act as`);
|
|
1631
|
+
if (value.optionsFrom !== void 0) uses.push(`properties.${key}.optionsFrom reads its options as an account`);
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
if (isObject(m.action) && m.action.connection !== void 0) uses.push("action.connection makes the call as an account");
|
|
1635
|
+
if (m.trigger !== void 0) uses.push("trigger watches an account");
|
|
1636
|
+
if (isObject(m.auth) && typeof m.auth.provider === "string" && m.auth.provider) uses.push(`auth.provider names "${m.auth.provider}"`);
|
|
1637
|
+
return uses;
|
|
1638
|
+
}
|
|
1639
|
+
function checkManifest(raw, hasCode) {
|
|
1640
|
+
const errors = [];
|
|
1641
|
+
const warnings = [];
|
|
1642
|
+
if (!isObject(raw)) {
|
|
1643
|
+
return { ok: false, kind: hasCode ? "code" : "declared", errors: ["manifest.json must be a JSON object."], warnings };
|
|
1644
|
+
}
|
|
1645
|
+
const m = raw;
|
|
1646
|
+
const kind = isObject(m.trigger) ? "trigger" : isObject(m.action) ? isObject(m.action.request) ? "declared" : "outputs-only" : "code";
|
|
1647
|
+
for (const key of Object.keys(m)) {
|
|
1648
|
+
if (!KNOWN_BLOCKS.has(key)) warnings.push(`"${key}" is not something a manifest can declare, and is dropped when the step is saved.`);
|
|
1649
|
+
}
|
|
1650
|
+
const properties = isObject(m.properties) ? m.properties : {};
|
|
1651
|
+
if (m.properties !== void 0 && !isObject(m.properties)) errors.push("properties must be an object of fields.");
|
|
1652
|
+
const typeOf = (name) => typeof name === "string" && isObject(properties[name]) ? properties[name].type : void 0;
|
|
1653
|
+
for (const [key, value] of Object.entries(properties)) {
|
|
1654
|
+
if (!isObject(value)) {
|
|
1655
|
+
errors.push(`properties.${key} must be an object with at least a label and a type.`);
|
|
1656
|
+
continue;
|
|
1657
|
+
}
|
|
1658
|
+
if (typeof value.type !== "string" || !PROPERTY_TYPES.has(value.type)) {
|
|
1659
|
+
warnings.push(`properties.${key} has type ${JSON.stringify(value.type)}, which the platform does not know; it becomes a plain text field.`);
|
|
1660
|
+
}
|
|
1661
|
+
if (typeof value.label !== "string" || !value.label.trim()) {
|
|
1662
|
+
warnings.push(`properties.${key} has no label; the canvas shows "${key}" instead.`);
|
|
1663
|
+
}
|
|
1664
|
+
if ((value.type === "select" || value.type === "multiselect") && value.options === void 0 && value.optionsFrom === void 0) {
|
|
1665
|
+
warnings.push(`properties.${key} is a ${value.type} with no options and no optionsFrom; there is nothing to choose.`);
|
|
1666
|
+
}
|
|
1667
|
+
if (value.options !== void 0) {
|
|
1668
|
+
const options = Array.isArray(value.options) ? value.options : null;
|
|
1669
|
+
if (!options || options.some((o) => !isObject(o) || typeof o.label !== "string" || typeof o.value !== "string")) {
|
|
1670
|
+
errors.push(`properties.${key}.options must be a list of { "label": "\u2026", "value": "\u2026" }, both text.`);
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
if (value.optionsFrom !== void 0) checkOptionsFrom(key, value, typeOf, errors);
|
|
1674
|
+
}
|
|
1675
|
+
if (m.auth !== void 0) {
|
|
1676
|
+
if (!isObject(m.auth)) errors.push("auth must be an object.");
|
|
1677
|
+
else {
|
|
1678
|
+
if (typeof m.auth.type !== "string" || !AUTH_TYPES.has(m.auth.type)) {
|
|
1679
|
+
errors.push(`auth.type must be one of ${[...AUTH_TYPES].join(", ")}.`);
|
|
1680
|
+
} else if (m.auth.type !== "none" && !m.auth.provider) {
|
|
1681
|
+
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".`);
|
|
1682
|
+
}
|
|
1683
|
+
for (const key of HOST_KEYS) {
|
|
1684
|
+
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.`);
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
for (const ports of ["inputs", "outputs"]) {
|
|
1689
|
+
if (m[ports] !== void 0 && (!Array.isArray(m[ports]) || m[ports].some((p) => typeof p !== "string"))) {
|
|
1690
|
+
errors.push(`${ports} must be a list of port names.`);
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
if (Array.isArray(m.outputs) && m.outputs.length === 0 && kind !== "trigger") {
|
|
1694
|
+
warnings.push("outputs is empty: no line can be drawn from this step.");
|
|
1695
|
+
}
|
|
1696
|
+
if (m.timeoutMs !== void 0) {
|
|
1697
|
+
if (typeof m.timeoutMs !== "number" || m.timeoutMs <= 0) errors.push("timeoutMs must be a number of milliseconds.");
|
|
1698
|
+
else if (m.timeoutMs > PUSH_CHECK.maxTimeoutMs) warnings.push(`timeoutMs ${m.timeoutMs} is cut to ${PUSH_CHECK.maxTimeoutMs}, the most a step is given.`);
|
|
1699
|
+
}
|
|
1700
|
+
if (typeof m.memoryLimitMb === "number" && m.memoryLimitMb > MAX_MEMORY_MB) {
|
|
1701
|
+
warnings.push(`memoryLimitMb ${m.memoryLimitMb} is cut to ${MAX_MEMORY_MB}.`);
|
|
1702
|
+
}
|
|
1703
|
+
if (m.action !== void 0) {
|
|
1704
|
+
if (!isObject(m.action)) errors.push("action must be an object.");
|
|
1705
|
+
else checkAction(m.action, properties, typeOf, errors, warnings);
|
|
1706
|
+
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.");
|
|
1707
|
+
}
|
|
1708
|
+
if (m.trigger !== void 0) {
|
|
1709
|
+
if (!isObject(m.trigger)) errors.push("trigger must be an object.");
|
|
1710
|
+
else checkTrigger(m.trigger, typeOf, errors, warnings);
|
|
1711
|
+
}
|
|
1712
|
+
if (kind === "code" && !hasCode) {
|
|
1713
|
+
errors.push("There is no index.js and no action or trigger block, so this step has nothing to run.");
|
|
1714
|
+
}
|
|
1715
|
+
for (const use of accountUseIn(m)) errors.push(`${use}. ${ACCOUNTS_ARE_OURS}`);
|
|
1716
|
+
return { ok: errors.length === 0, kind, errors, warnings };
|
|
1717
|
+
}
|
|
1718
|
+
function checkConnectionName(where, name, typeOf, errors) {
|
|
1719
|
+
if (typeof name !== "string" || !name) {
|
|
1720
|
+
errors.push(`${where} must name one of this step's properties of type "connection".`);
|
|
1721
|
+
} else if (typeOf(name) === void 0) {
|
|
1722
|
+
errors.push(`${where} names "${name}", which is not one of this step's properties.`);
|
|
1723
|
+
} else if (typeOf(name) !== "connection") {
|
|
1724
|
+
errors.push(`${where} names "${name}", which is a ${String(typeOf(name))} property rather than a connection.`);
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
function checkAction(a, properties, typeOf, errors, warnings) {
|
|
1728
|
+
const request = a.request;
|
|
1729
|
+
if (request !== void 0) {
|
|
1730
|
+
if (!isObject(request)) {
|
|
1731
|
+
errors.push("action.request must be an object.");
|
|
1732
|
+
return;
|
|
1733
|
+
}
|
|
1734
|
+
const viaConnection = a.connection !== void 0;
|
|
1735
|
+
const viaBaseUrl = a.baseUrl !== void 0;
|
|
1736
|
+
if (viaConnection && viaBaseUrl) errors.push("action has both connection and baseUrl: a request goes as an account or to a named host, never both.");
|
|
1737
|
+
if (!viaConnection && !viaBaseUrl) errors.push("action.request needs either connection (an account to act as) or baseUrl (the https:// host to call).");
|
|
1738
|
+
if (viaConnection) checkConnectionName("action.connection", a.connection, typeOf, errors);
|
|
1739
|
+
if (viaBaseUrl) {
|
|
1740
|
+
if (typeof a.baseUrl !== "string" || !/^https:\/\//i.test(a.baseUrl)) errors.push("action.baseUrl must start with https://.");
|
|
1741
|
+
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.");
|
|
1742
|
+
}
|
|
1743
|
+
const method = String(request.method ?? "GET").toUpperCase();
|
|
1744
|
+
if (!METHODS.has(method)) errors.push(`action.request.method must be one of ${[...METHODS].join(", ")}.`);
|
|
1745
|
+
if (!isRelativePath(request.path)) errors.push("action.request.path must be a path relative to the base URL, never a full URL.");
|
|
1746
|
+
if (request.bodyType !== void 0 && !BODY_TYPES.has(String(request.bodyType))) {
|
|
1747
|
+
errors.push(`action.request.bodyType must be one of ${[...BODY_TYPES].join(", ")}.`);
|
|
1748
|
+
}
|
|
1749
|
+
if (request.body !== void 0 && method === "GET") warnings.push("action.request has a body on a GET; most APIs ignore it.");
|
|
1750
|
+
if (viaConnection && isObject(request.headers)) {
|
|
1751
|
+
for (const header of Object.keys(request.headers)) {
|
|
1752
|
+
if (header.toLowerCase() === "authorization") warnings.push("action.request sets Authorization while acting as an account; the platform adds the account's credential itself.");
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
if (a.outputs !== void 0 && !isObject(a.outputs)) errors.push("action.outputs must be an object of name to value.");
|
|
1757
|
+
if (a.outputs === void 0) warnings.push("action has no outputs: the step hands nothing on.");
|
|
1758
|
+
for (const key of ["errorPath", "okPath", "okValue"]) {
|
|
1759
|
+
if (a[key] !== void 0 && typeof a[key] !== "string") errors.push(`action.${key} must be text.`);
|
|
1760
|
+
}
|
|
1761
|
+
if (a.items !== void 0) {
|
|
1762
|
+
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.');
|
|
1763
|
+
else if (a.items.limit !== void 0 && (typeof a.items.limit !== "number" || a.items.limit < 1 || a.items.limit > MAX_ITEM_LIMIT)) {
|
|
1764
|
+
errors.push(`action.items.limit must be a number from 1 to ${MAX_ITEM_LIMIT}.`);
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
if (a.paginate !== void 0) {
|
|
1768
|
+
if (!isObject(a.paginate) || !PAGINATE_FROM.has(String(a.paginate.from))) {
|
|
1769
|
+
errors.push(`action.paginate.from must be one of ${[...PAGINATE_FROM].join(", ")}.`);
|
|
1770
|
+
} else if (a.paginate.maxPages !== void 0 && (typeof a.paginate.maxPages !== "number" || a.paginate.maxPages > MAX_PAGES)) {
|
|
1771
|
+
errors.push(`action.paginate.maxPages must be at most ${MAX_PAGES}.`);
|
|
1772
|
+
}
|
|
1773
|
+
if (a.items === void 0) errors.push("action.paginate needs action.items: pages are pages of a list.");
|
|
1774
|
+
}
|
|
1775
|
+
const text = JSON.stringify({ baseUrl: a.baseUrl, request: a.request, outputs: a.outputs });
|
|
1776
|
+
for (const [, key] of text.matchAll(/\{\{\s*config\.([A-Za-z0-9_$]+)/g)) {
|
|
1777
|
+
if (!(key in properties)) warnings.push(`{{config.${key}}} names no property; it is always empty.`);
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
function checkTrigger(t, typeOf, errors, warnings) {
|
|
1781
|
+
checkConnectionName("trigger.connection", t.connection, typeOf, errors);
|
|
1782
|
+
if (!isObject(t.request) || !isRelativePath(t.request.path)) errors.push("trigger.request.path must be a path relative to the provider.");
|
|
1783
|
+
else if (t.request.method !== void 0 && !["GET", "POST"].includes(String(t.request.method).toUpperCase())) {
|
|
1784
|
+
errors.push("trigger.request.method must be GET or POST.");
|
|
1785
|
+
}
|
|
1786
|
+
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).');
|
|
1787
|
+
if (typeof t.id !== "string" || !t.id) errors.push("trigger.id must be the dotted path, within an item, to what makes it unique.");
|
|
1788
|
+
if (t.poll !== void 0 && typeof t.poll !== "number" && (typeof t.poll !== "string" || typeOf(t.poll) === void 0)) {
|
|
1789
|
+
errors.push("trigger.poll must be a number of minutes, or the name of a property that holds one.");
|
|
1790
|
+
}
|
|
1791
|
+
if (t.cursor !== void 0) {
|
|
1792
|
+
if (!isObject(t.cursor) || !["response", "item"].includes(String(t.cursor.from)) || typeof t.cursor.path !== "string") {
|
|
1793
|
+
errors.push('trigger.cursor must be { "from": "response" | "item", "path": "\u2026" }.');
|
|
1794
|
+
} else if (t.cursor.initial !== "now") {
|
|
1795
|
+
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.`);
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
if (t.hydrate !== void 0 && (!isObject(t.hydrate) || !isRelativePath(t.hydrate.path))) {
|
|
1799
|
+
errors.push("trigger.hydrate.path must be a path relative to the provider.");
|
|
1800
|
+
}
|
|
1801
|
+
if (t.maxPerPoll !== void 0 && (typeof t.maxPerPoll !== "number" || t.maxPerPoll < 1)) errors.push("trigger.maxPerPoll must be a positive number.");
|
|
1802
|
+
}
|
|
1803
|
+
function checkOptionsFrom(key, property, typeOf, errors) {
|
|
1804
|
+
const where = `properties.${key}.optionsFrom`;
|
|
1805
|
+
if (property.type !== "select" && property.type !== "multiselect") {
|
|
1806
|
+
errors.push(`${where} only means something on a select or multiselect, and ${key} is a ${String(property.type)}.`);
|
|
1807
|
+
}
|
|
1808
|
+
const o = property.optionsFrom;
|
|
1809
|
+
if (!isObject(o)) {
|
|
1810
|
+
errors.push(`${where} must be an object.`);
|
|
1811
|
+
return;
|
|
1812
|
+
}
|
|
1813
|
+
checkConnectionName(`${where}.connection`, o.connection, typeOf, errors);
|
|
1814
|
+
if (typeof o.label !== "string" || !o.label.trim()) errors.push(`${where}.label must say what each option shows, e.g. {{item.name}}.`);
|
|
1815
|
+
if (typeof o.value !== "string" || !o.value.trim()) errors.push(`${where}.value must say what choosing it stores, e.g. {{item.id}}.`);
|
|
1816
|
+
if (!isObject(o.request) || !isRelativePath(o.request.path)) errors.push(`${where}.request.path must be a path relative to the provider.`);
|
|
1817
|
+
else {
|
|
1818
|
+
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.`);
|
|
1819
|
+
if (o.request.body !== void 0) errors.push(`${where}.request cannot have a body: it is a GET.`);
|
|
1820
|
+
}
|
|
1391
1821
|
}
|
|
1392
1822
|
|
|
1393
1823
|
// src/context-api.ts
|
|
@@ -1496,14 +1926,30 @@ var BLOCKED_GLOBALS = [
|
|
|
1496
1926
|
{ name: "__dirname", instead: "There is no meaningful filesystem." }
|
|
1497
1927
|
];
|
|
1498
1928
|
export {
|
|
1929
|
+
ACCOUNTS_ARE_OURS,
|
|
1499
1930
|
ALLOWED_PACKAGES,
|
|
1500
1931
|
ALLOWED_PACKAGE_NAMES,
|
|
1501
1932
|
BLOCKED_GLOBALS,
|
|
1933
|
+
BLOCKED_GLOBAL_NAMES,
|
|
1502
1934
|
CONTEXT_API,
|
|
1935
|
+
MAX_CODE_LENGTH,
|
|
1936
|
+
PLATFORM_NODE_MAJOR,
|
|
1937
|
+
PROVIDED_GLOBALS,
|
|
1938
|
+
PUSH_CHECK,
|
|
1503
1939
|
RESTRICTED_PATTERNS,
|
|
1940
|
+
RETIRED_TIERS,
|
|
1941
|
+
SANDBOX_OVERRIDES,
|
|
1942
|
+
SANDBOX_TIERS,
|
|
1943
|
+
SIGNED_URL_SECONDS,
|
|
1944
|
+
STATE_LIMITS,
|
|
1945
|
+
accountUseIn,
|
|
1946
|
+
checkManifest,
|
|
1504
1947
|
createLocalState,
|
|
1505
1948
|
createLocalStorage,
|
|
1949
|
+
executeOf,
|
|
1506
1950
|
loadPluginCode,
|
|
1951
|
+
pluginConsole,
|
|
1952
|
+
sandboxGlobals,
|
|
1507
1953
|
sandboxRequire,
|
|
1508
1954
|
simulate,
|
|
1509
1955
|
validatePluginCode
|