rowbound 1.0.2

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.
Files changed (71) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +258 -0
  3. package/dist/adapters/adapter.d.ts +1 -0
  4. package/dist/adapters/adapter.js +1 -0
  5. package/dist/adapters/sheets/sheets-adapter.d.ts +66 -0
  6. package/dist/adapters/sheets/sheets-adapter.js +531 -0
  7. package/dist/cli/config.d.ts +2 -0
  8. package/dist/cli/config.js +397 -0
  9. package/dist/cli/env.d.ts +3 -0
  10. package/dist/cli/env.js +103 -0
  11. package/dist/cli/format.d.ts +5 -0
  12. package/dist/cli/format.js +6 -0
  13. package/dist/cli/index.d.ts +2 -0
  14. package/dist/cli/index.js +39 -0
  15. package/dist/cli/init.d.ts +10 -0
  16. package/dist/cli/init.js +72 -0
  17. package/dist/cli/run.d.ts +2 -0
  18. package/dist/cli/run.js +212 -0
  19. package/dist/cli/runs.d.ts +2 -0
  20. package/dist/cli/runs.js +108 -0
  21. package/dist/cli/status.d.ts +2 -0
  22. package/dist/cli/status.js +108 -0
  23. package/dist/cli/sync.d.ts +2 -0
  24. package/dist/cli/sync.js +84 -0
  25. package/dist/cli/watch.d.ts +2 -0
  26. package/dist/cli/watch.js +348 -0
  27. package/dist/core/condition.d.ts +25 -0
  28. package/dist/core/condition.js +66 -0
  29. package/dist/core/defaults.d.ts +3 -0
  30. package/dist/core/defaults.js +7 -0
  31. package/dist/core/engine.d.ts +50 -0
  32. package/dist/core/engine.js +234 -0
  33. package/dist/core/env.d.ts +13 -0
  34. package/dist/core/env.js +72 -0
  35. package/dist/core/exec.d.ts +24 -0
  36. package/dist/core/exec.js +134 -0
  37. package/dist/core/extractor.d.ts +10 -0
  38. package/dist/core/extractor.js +33 -0
  39. package/dist/core/http-client.d.ts +32 -0
  40. package/dist/core/http-client.js +161 -0
  41. package/dist/core/rate-limiter.d.ts +25 -0
  42. package/dist/core/rate-limiter.js +64 -0
  43. package/dist/core/reconcile.d.ts +24 -0
  44. package/dist/core/reconcile.js +192 -0
  45. package/dist/core/run-format.d.ts +39 -0
  46. package/dist/core/run-format.js +201 -0
  47. package/dist/core/run-state.d.ts +64 -0
  48. package/dist/core/run-state.js +141 -0
  49. package/dist/core/run-tracker.d.ts +15 -0
  50. package/dist/core/run-tracker.js +57 -0
  51. package/dist/core/safe-compare.d.ts +8 -0
  52. package/dist/core/safe-compare.js +19 -0
  53. package/dist/core/shell-escape.d.ts +7 -0
  54. package/dist/core/shell-escape.js +9 -0
  55. package/dist/core/tab-resolver.d.ts +17 -0
  56. package/dist/core/tab-resolver.js +44 -0
  57. package/dist/core/template.d.ts +32 -0
  58. package/dist/core/template.js +82 -0
  59. package/dist/core/types.d.ts +105 -0
  60. package/dist/core/types.js +2 -0
  61. package/dist/core/url-guard.d.ts +21 -0
  62. package/dist/core/url-guard.js +184 -0
  63. package/dist/core/validator.d.ts +11 -0
  64. package/dist/core/validator.js +261 -0
  65. package/dist/core/waterfall.d.ts +26 -0
  66. package/dist/core/waterfall.js +55 -0
  67. package/dist/index.d.ts +15 -0
  68. package/dist/index.js +16 -0
  69. package/dist/mcp/server.d.ts +1 -0
  70. package/dist/mcp/server.js +943 -0
  71. package/package.json +67 -0
@@ -0,0 +1,348 @@
1
+ import { createServer, } from "node:http";
2
+ import { SheetsAdapter } from "../adapters/sheets/sheets-adapter.js";
3
+ import { runPipeline } from "../core/engine.js";
4
+ import { buildSafeEnv } from "../core/env.js";
5
+ import { reconcile } from "../core/reconcile.js";
6
+ import { createRunState } from "../core/run-state.js";
7
+ import { createRunTracker } from "../core/run-tracker.js";
8
+ import { safeCompare } from "../core/safe-compare.js";
9
+ import { bold, dim, error as fmtError, warn } from "./format.js";
10
+ // ---------------------------------------------------------------------------
11
+ // Rate limiter — simple in-memory per-IP sliding window (60 req/min)
12
+ // ---------------------------------------------------------------------------
13
+ const RATE_LIMIT_WINDOW_MS = 60_000;
14
+ const RATE_LIMIT_MAX = 60;
15
+ function createRateLimiter() {
16
+ const hits = new Map();
17
+ return (ip) => {
18
+ const now = Date.now();
19
+ const timestamps = hits.get(ip) ?? [];
20
+ const recent = timestamps.filter((t) => now - t < RATE_LIMIT_WINDOW_MS);
21
+ if (recent.length >= RATE_LIMIT_MAX) {
22
+ hits.set(ip, recent);
23
+ return false; // rate limited
24
+ }
25
+ recent.push(now);
26
+ hits.set(ip, recent);
27
+ return true; // allowed
28
+ };
29
+ }
30
+ function timestamp() {
31
+ return new Date().toLocaleTimeString("en-GB", { hour12: false });
32
+ }
33
+ async function executePipelineRun(adapter, ref, config, env, signal, sheetId, sheetName) {
34
+ // Reconcile column registry (detect renames, track new columns, migrate v1→v2)
35
+ const reconciled = await reconcile(adapter, ref, config);
36
+ if (reconciled.configChanged) {
37
+ await adapter.writeConfig(ref, reconciled.config);
38
+ }
39
+ const tabConfig = reconciled.tabConfig;
40
+ const resolvedConfig = { ...reconciled.config, actions: tabConfig.actions };
41
+ const runState = createRunState({
42
+ sheetId,
43
+ sheetName,
44
+ config: resolvedConfig,
45
+ totalRows: 0,
46
+ dryRun: false,
47
+ });
48
+ const tracker = createRunTracker(runState);
49
+ const result = await runPipeline({
50
+ adapter,
51
+ ref,
52
+ config: resolvedConfig,
53
+ env,
54
+ signal,
55
+ columnMap: tabConfig.columns,
56
+ onRowStart: (rowIndex, row) => {
57
+ tracker.onRowStart(rowIndex, row);
58
+ },
59
+ onActionComplete: (rowIndex, actionId, value) => {
60
+ tracker.onActionComplete(rowIndex, actionId, value);
61
+ },
62
+ onError: (rowIndex, actionId, error) => {
63
+ tracker.onError(rowIndex, actionId, error);
64
+ },
65
+ onRowComplete: (rowIndex, updates) => {
66
+ tracker.onRowComplete(rowIndex, updates);
67
+ },
68
+ });
69
+ runState.totalRows = result.totalRows;
70
+ await tracker.finalize(signal.aborted);
71
+ return result;
72
+ }
73
+ export function registerWatch(program) {
74
+ program
75
+ .command("watch")
76
+ .description("Watch a sheet for changes and run the pipeline continuously")
77
+ .argument("<sheetId>", "Google Sheets spreadsheet ID")
78
+ .option("--tab <name>", "Sheet tab name", "Sheet1")
79
+ .option("--interval <seconds>", "Polling interval in seconds", "30")
80
+ .option("--port <port>", "Webhook server port", "3000")
81
+ .option("--webhook-host <host>", "Webhook server bind address", "127.0.0.1")
82
+ .option("--webhook-token <token>", "Bearer token for webhook authentication")
83
+ .action(async (sheetId, opts) => {
84
+ const adapter = new SheetsAdapter();
85
+ const ref = {
86
+ spreadsheetId: sheetId,
87
+ sheetName: opts.tab,
88
+ };
89
+ const intervalSeconds = parseInt(opts.interval, 10);
90
+ const port = parseInt(opts.port, 10);
91
+ if (Number.isNaN(intervalSeconds) || intervalSeconds < 1) {
92
+ console.error(fmtError("Invalid --interval value. Must be a positive integer."));
93
+ process.exitCode = 1;
94
+ return;
95
+ }
96
+ if (Number.isNaN(port) || port < 1 || port > 65535) {
97
+ console.error(fmtError("Invalid --port value. Must be between 1 and 65535."));
98
+ process.exitCode = 1;
99
+ return;
100
+ }
101
+ // Validate config exists before starting watch
102
+ let config;
103
+ try {
104
+ config = await adapter.readConfig(ref);
105
+ }
106
+ catch (err) {
107
+ const msg = err instanceof Error ? err.message : String(err);
108
+ console.error(fmtError("Failed to read config:"), msg);
109
+ process.exitCode = 1;
110
+ return;
111
+ }
112
+ if (!config) {
113
+ console.error(fmtError("No Rowbound config found. Run 'rowbound init <sheetId>' first."));
114
+ process.exitCode = 1;
115
+ return;
116
+ }
117
+ // Check if any actions exist (in v2 tabs or v1 top-level)
118
+ const hasActions = config.tabs
119
+ ? Object.values(config.tabs).some((t) => t.actions.length > 0)
120
+ : config.actions.length > 0;
121
+ if (!hasActions) {
122
+ console.error(fmtError("No actions configured. Add actions with 'rowbound config add-action'."));
123
+ process.exitCode = 1;
124
+ return;
125
+ }
126
+ const webhookToken = opts.webhookToken ?? process.env.ROWBOUND_WEBHOOK_TOKEN;
127
+ const controller = new AbortController();
128
+ let isRunning = false;
129
+ // Run one pipeline cycle, guarded by the isRunning flag
130
+ async function runOnce() {
131
+ if (isRunning) {
132
+ return null;
133
+ }
134
+ isRunning = true;
135
+ try {
136
+ // Re-read config each tick so hot-reload of actions works
137
+ const freshConfig = await adapter.readConfig(ref);
138
+ const activeConfig = freshConfig ?? config;
139
+ // Rebuild env from fresh config so new env references are picked up
140
+ const env = buildSafeEnv(activeConfig);
141
+ const result = await executePipelineRun(adapter, ref, activeConfig, env, controller.signal, sheetId, opts.tab);
142
+ return result;
143
+ }
144
+ finally {
145
+ isRunning = false;
146
+ }
147
+ }
148
+ // --- Friendly startup message (UX-011) ---
149
+ // Resolve tab name from config if available
150
+ let displayName;
151
+ if (config.tabs) {
152
+ const tabEntry = Object.values(config.tabs).find((t) => t.name === opts.tab);
153
+ if (tabEntry) {
154
+ displayName = tabEntry.name;
155
+ }
156
+ }
157
+ if (displayName) {
158
+ console.log(`Watching ${bold(displayName)} every ${intervalSeconds}s... ${dim(sheetId)}`);
159
+ }
160
+ else {
161
+ console.log(`Watching ${bold(opts.tab)} every ${intervalSeconds}s... ${dim(sheetId)}`);
162
+ }
163
+ // --- Initial pipeline run (UX-010) ---
164
+ console.log(`[${timestamp()}] Running initial pipeline...`);
165
+ try {
166
+ const result = await runOnce();
167
+ if (result && result.updates > 0) {
168
+ console.log(`[${timestamp()}] Initial run: ${result.processedRows} rows, ${result.updates} updates`);
169
+ }
170
+ else if (result) {
171
+ console.log(`[${timestamp()}] Initial run complete, no updates needed`);
172
+ }
173
+ }
174
+ catch (error) {
175
+ const msg = error instanceof Error ? error.message : String(error);
176
+ console.error(`[${timestamp()}] Initial pipeline error: ${msg}`);
177
+ // Don't return — let the interval start anyway
178
+ }
179
+ // --- Polling loop ---
180
+ const intervalId = setInterval(async () => {
181
+ if (controller.signal.aborted)
182
+ return;
183
+ console.log(`[${timestamp()}] Checking for new rows...`);
184
+ try {
185
+ const result = await runOnce();
186
+ if (result && result.updates > 0) {
187
+ console.log(`[${timestamp()}] Processed ${result.processedRows} rows, ${result.updates} updates`);
188
+ }
189
+ }
190
+ catch (error) {
191
+ const msg = error instanceof Error ? error.message : String(error);
192
+ console.error(`[${timestamp()}] Pipeline error: ${msg}`);
193
+ }
194
+ }, intervalSeconds * 1000);
195
+ // --- Webhook HTTP server ---
196
+ const isAllowed = createRateLimiter();
197
+ const server = createServer(async (req, res) => {
198
+ // Rate limit by remote IP
199
+ const ip = req.socket.remoteAddress ??
200
+ req.headers["x-forwarded-for"]?.toString() ??
201
+ "unknown";
202
+ if (!isAllowed(ip)) {
203
+ res.writeHead(429, { "Content-Type": "application/json" });
204
+ res.end(JSON.stringify({ error: "Too Many Requests" }));
205
+ return;
206
+ }
207
+ if (req.method !== "POST" || req.url !== "/webhook") {
208
+ res.writeHead(404, { "Content-Type": "application/json" });
209
+ res.end(JSON.stringify({ error: "Not found" }));
210
+ return;
211
+ }
212
+ // Authenticate if token is configured (constant-time comparison)
213
+ if (webhookToken) {
214
+ const authHeader = req.headers.authorization ?? "";
215
+ if (!safeCompare(authHeader, `Bearer ${webhookToken}`)) {
216
+ res.writeHead(401, { "Content-Type": "application/json" });
217
+ res.end(JSON.stringify({ error: "Unauthorized" }));
218
+ return;
219
+ }
220
+ }
221
+ // Parse JSON body with size limit (1MB)
222
+ const chunks = [];
223
+ let totalBytes = 0;
224
+ for await (const chunk of req) {
225
+ totalBytes += chunk.length;
226
+ if (totalBytes > 1_048_576) {
227
+ res.writeHead(413, { "Content-Type": "application/json" });
228
+ res.end(JSON.stringify({ error: "Payload too large" }));
229
+ return;
230
+ }
231
+ chunks.push(chunk);
232
+ }
233
+ let body;
234
+ try {
235
+ body = JSON.parse(Buffer.concat(chunks).toString("utf-8"));
236
+ }
237
+ catch {
238
+ res.writeHead(400, { "Content-Type": "application/json" });
239
+ res.end(JSON.stringify({ error: "Invalid JSON body" }));
240
+ return;
241
+ }
242
+ // If body contains row data, write it to the sheet
243
+ if (body && typeof body === "object" && !Array.isArray(body)) {
244
+ const rowData = body;
245
+ if (Object.keys(rowData).length > 0) {
246
+ try {
247
+ const MAX_CELL_LENGTH = 50_000;
248
+ const headers = await adapter.getHeaders(ref);
249
+ const headerSet = new Set(headers);
250
+ const rows = await adapter.readRows(ref);
251
+ const nextRow = rows.length + 2; // +2 because row 1 is headers, data starts at 2
252
+ const updates = [];
253
+ for (const h of headers) {
254
+ if (!(h in rowData))
255
+ continue;
256
+ const val = rowData[h];
257
+ // Skip fields not matching known column headers
258
+ if (!headerSet.has(h))
259
+ continue;
260
+ // Type check: only allow strings and numbers
261
+ if (typeof val !== "string" && typeof val !== "number") {
262
+ continue;
263
+ }
264
+ const strVal = String(val);
265
+ // Size check: Google Sheets cell limit
266
+ if (strVal.length > MAX_CELL_LENGTH)
267
+ continue;
268
+ updates.push({
269
+ row: nextRow,
270
+ column: h,
271
+ value: strVal,
272
+ });
273
+ }
274
+ if (updates.length > 0) {
275
+ await adapter.writeBatch(ref, updates);
276
+ }
277
+ }
278
+ catch (error) {
279
+ const msg = error instanceof Error ? error.message : String(error);
280
+ console.error(`[${timestamp()}] Webhook write error: ${msg}`);
281
+ }
282
+ }
283
+ }
284
+ // Trigger pipeline run immediately
285
+ console.log(`[${timestamp()}] Webhook received, running pipeline...`);
286
+ try {
287
+ const result = await runOnce();
288
+ if (result) {
289
+ res.writeHead(200, { "Content-Type": "application/json" });
290
+ res.end(JSON.stringify({
291
+ ok: true,
292
+ processedRows: result.processedRows,
293
+ updates: result.updates,
294
+ errors: result.errors.length,
295
+ }));
296
+ }
297
+ else {
298
+ // Pipeline already running
299
+ res.writeHead(200, { "Content-Type": "application/json" });
300
+ res.end(JSON.stringify({
301
+ ok: true,
302
+ message: "Pipeline already in progress, skipped.",
303
+ }));
304
+ }
305
+ }
306
+ catch (error) {
307
+ const msg = error instanceof Error ? error.message : String(error);
308
+ console.error(`[${timestamp()}] Webhook pipeline error: ${msg}`);
309
+ res.writeHead(500, { "Content-Type": "application/json" });
310
+ res.end(JSON.stringify({ error: msg }));
311
+ }
312
+ });
313
+ // Server timeouts
314
+ server.headersTimeout = 10_000;
315
+ server.requestTimeout = 30_000;
316
+ server.keepAliveTimeout = 5_000;
317
+ const webhookHost = opts.webhookHost;
318
+ server.listen(port, webhookHost, () => {
319
+ console.log(`Webhook server listening on http://${webhookHost}:${port}`);
320
+ if (!webhookToken) {
321
+ console.warn(warn("WARNING: Webhook server running WITHOUT authentication."));
322
+ console.warn(warn("Anyone who can reach this port can trigger pipeline runs and write data to your sheet."));
323
+ console.warn(warn("Set ROWBOUND_WEBHOOK_TOKEN or use --webhook-token to secure the endpoint."));
324
+ }
325
+ });
326
+ // --- Graceful shutdown (NEW-005/MISSED-006) ---
327
+ // Store handler references so they can be removed on cleanup
328
+ const onSigInt = () => {
329
+ shutdown();
330
+ };
331
+ const onSigTerm = () => {
332
+ shutdown();
333
+ };
334
+ const shutdown = () => {
335
+ console.log("\nShutting down...");
336
+ controller.abort();
337
+ clearInterval(intervalId);
338
+ // Remove signal handlers to prevent accumulation
339
+ process.removeListener("SIGINT", onSigInt);
340
+ process.removeListener("SIGTERM", onSigTerm);
341
+ server.close(() => {
342
+ console.log("Watch stopped.");
343
+ });
344
+ };
345
+ process.on("SIGINT", onSigInt);
346
+ process.on("SIGTERM", onSigTerm);
347
+ });
348
+ }
@@ -0,0 +1,25 @@
1
+ import type { ExecutionContext } from "./types.js";
2
+ /**
3
+ * Pre-check an expression for forbidden keywords as defense-in-depth.
4
+ * Throws if the expression contains any keyword that could be used
5
+ * to escape the vm sandbox.
6
+ *
7
+ * Exported so engine.ts can use the same check for transform expressions.
8
+ */
9
+ export declare function preCheckExpression(expr: string): void;
10
+ /**
11
+ * Evaluate a JavaScript expression in a sandboxed context.
12
+ *
13
+ * WARNING: Node.js vm module is NOT a security boundary. The pre-check
14
+ * and Object.create(null) sandbox are defense-in-depth measures only.
15
+ * Do not rely on this for untrusted code execution.
16
+ *
17
+ * - Empty/undefined expression returns true (no condition = always run)
18
+ * - Sandbox exposes: row, env, results
19
+ * - Uses Object.create(null) to sever prototype chain (prevents escape via
20
+ * this.constructor.constructor('return process')())
21
+ * - Pre-checks for forbidden keywords (process, require, import, etc.)
22
+ * - Times out after 100ms to prevent infinite loops
23
+ * - Result is coerced to boolean
24
+ */
25
+ export declare function evaluateCondition(expression: string | undefined, context: ExecutionContext): boolean;
@@ -0,0 +1,66 @@
1
+ import vm from "node:vm";
2
+ const FORBIDDEN_KEYWORDS = [
3
+ "process",
4
+ "require",
5
+ "import",
6
+ "globalThis",
7
+ "global",
8
+ "Function",
9
+ "__proto__",
10
+ "prototype",
11
+ "constructor",
12
+ "eval",
13
+ "Reflect",
14
+ "Proxy",
15
+ "Symbol",
16
+ "WeakRef",
17
+ "this",
18
+ ];
19
+ /**
20
+ * Pre-check an expression for forbidden keywords as defense-in-depth.
21
+ * Throws if the expression contains any keyword that could be used
22
+ * to escape the vm sandbox.
23
+ *
24
+ * Exported so engine.ts can use the same check for transform expressions.
25
+ */
26
+ export function preCheckExpression(expr) {
27
+ for (const keyword of FORBIDDEN_KEYWORDS) {
28
+ if (new RegExp(`\\b${keyword}\\b`).test(expr)) {
29
+ throw new Error(`Expression contains forbidden keyword: "${keyword}"`);
30
+ }
31
+ }
32
+ }
33
+ /**
34
+ * Evaluate a JavaScript expression in a sandboxed context.
35
+ *
36
+ * WARNING: Node.js vm module is NOT a security boundary. The pre-check
37
+ * and Object.create(null) sandbox are defense-in-depth measures only.
38
+ * Do not rely on this for untrusted code execution.
39
+ *
40
+ * - Empty/undefined expression returns true (no condition = always run)
41
+ * - Sandbox exposes: row, env, results
42
+ * - Uses Object.create(null) to sever prototype chain (prevents escape via
43
+ * this.constructor.constructor('return process')())
44
+ * - Pre-checks for forbidden keywords (process, require, import, etc.)
45
+ * - Times out after 100ms to prevent infinite loops
46
+ * - Result is coerced to boolean
47
+ */
48
+ export function evaluateCondition(expression, context) {
49
+ if (!expression || expression.trim() === "") {
50
+ return true;
51
+ }
52
+ preCheckExpression(expression);
53
+ const rawSandbox = Object.create(null);
54
+ rawSandbox.row = { ...context.row };
55
+ rawSandbox.env = context.env;
56
+ rawSandbox.results = context.results ?? {};
57
+ const sandbox = vm.createContext(rawSandbox);
58
+ try {
59
+ const result = vm.runInContext(expression, sandbox, { timeout: 100 });
60
+ return Boolean(result);
61
+ }
62
+ catch {
63
+ // Timeout or syntax error — treat as false
64
+ return false;
65
+ }
66
+ }
@@ -0,0 +1,3 @@
1
+ import type { PipelineSettings } from "./types.js";
2
+ /** Default pipeline settings used during initialization. */
3
+ export declare const defaultSettings: PipelineSettings;
@@ -0,0 +1,7 @@
1
+ /** Default pipeline settings used during initialization. */
2
+ export const defaultSettings = {
3
+ concurrency: 1,
4
+ rateLimit: 10,
5
+ retryAttempts: 3,
6
+ retryBackoff: "exponential",
7
+ };
@@ -0,0 +1,50 @@
1
+ import type { Adapter, CellUpdate, ExecutionContext, PipelineConfig, Row, SheetRef } from "./types.js";
2
+ export interface RunPipelineOptions {
3
+ adapter: Adapter;
4
+ ref: SheetRef;
5
+ config: PipelineConfig;
6
+ env: Record<string, string>;
7
+ range?: string;
8
+ actionFilter?: string;
9
+ dryRun?: boolean;
10
+ signal?: AbortSignal;
11
+ /** Column map: columnId -> current header name. Used to build ID-keyed rows. */
12
+ columnMap?: Record<string, string>;
13
+ onTotalRows?: (total: number) => void;
14
+ onRowStart?: (rowIndex: number, row: Row) => void;
15
+ onRowComplete?: (rowIndex: number, updates: CellUpdate[]) => void;
16
+ onActionComplete?: (rowIndex: number, actionId: string, value: string | null) => void;
17
+ onError?: (rowIndex: number, actionId: string, error: Error) => void;
18
+ }
19
+ export interface RunResult {
20
+ totalRows: number;
21
+ processedRows: number;
22
+ skippedRows: number;
23
+ errors: Array<{
24
+ rowIndex: number;
25
+ actionId: string;
26
+ error: string;
27
+ }>;
28
+ updates: number;
29
+ }
30
+ /**
31
+ * Evaluate a JavaScript expression in a sandboxed context, returning the result as a string.
32
+ *
33
+ * WARNING: Node.js vm module is NOT a security boundary. The pre-check
34
+ * and Object.create(null) sandbox are defense-in-depth measures only.
35
+ * Do not rely on this for untrusted code execution.
36
+ *
37
+ * Unlike evaluateCondition (which coerces to boolean), this returns the raw value
38
+ * stringified — used for transform action expressions.
39
+ */
40
+ export declare function evaluateExpression(expression: string, context: ExecutionContext): string;
41
+ /**
42
+ * Run the full pipeline: read rows, process each through actions, write results back.
43
+ *
44
+ * Execution flow per row:
45
+ * 1. Read row data (header -> value map)
46
+ * 2. For each action: evaluate condition, execute, update in-memory row state
47
+ * 3. Batch-write all cell updates for the row
48
+ * 4. Fire progress callbacks
49
+ */
50
+ export declare function runPipeline(options: RunPipelineOptions): Promise<RunResult>;