@timo9378/flow2code 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.js ADDED
@@ -0,0 +1,3042 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/server/index.ts
4
+ import { createServer } from "http";
5
+ import { readFile, stat } from "fs/promises";
6
+ import { join as join2, extname, dirname as dirname2 } from "path";
7
+ import { fileURLToPath } from "url";
8
+ import { existsSync as existsSync2 } from "fs";
9
+
10
+ // src/lib/compiler/compiler.ts
11
+ import { Project } from "ts-morph";
12
+
13
+ // src/lib/ir/types.ts
14
+ var CURRENT_IR_VERSION = "1.0.0";
15
+
16
+ // src/lib/ir/migrations/engine.ts
17
+ var migrations = [];
18
+ function migrateIR(raw, targetVersion = CURRENT_IR_VERSION) {
19
+ const applied = [];
20
+ let current = { ...raw };
21
+ if (current.version === targetVersion) {
22
+ return { ir: current, applied, migrated: false };
23
+ }
24
+ const maxIterations = migrations.length + 1;
25
+ let iterations = 0;
26
+ while (current.version !== targetVersion) {
27
+ if (iterations++ > maxIterations) {
28
+ throw new MigrationError(
29
+ `Migration exceeded max iterations (${maxIterations}), possible circular migration`,
30
+ current.version,
31
+ targetVersion
32
+ );
33
+ }
34
+ const migration = migrations.find(
35
+ (m) => m.fromVersion === current.version
36
+ );
37
+ if (!migration) {
38
+ throw new MigrationError(
39
+ `No migration path found from ${current.version} to ${targetVersion}`,
40
+ current.version,
41
+ targetVersion
42
+ );
43
+ }
44
+ current = migration.migrate(current);
45
+ applied.push(
46
+ `${migration.fromVersion} \u2192 ${migration.toVersion}: ${migration.description}`
47
+ );
48
+ }
49
+ return { ir: current, applied, migrated: true };
50
+ }
51
+ function needsMigration(version, targetVersion = CURRENT_IR_VERSION) {
52
+ return version !== targetVersion;
53
+ }
54
+ var MigrationError = class extends Error {
55
+ constructor(message, fromVersion, targetVersion) {
56
+ super(message);
57
+ this.fromVersion = fromVersion;
58
+ this.targetVersion = targetVersion;
59
+ this.name = "MigrationError";
60
+ }
61
+ };
62
+
63
+ // src/lib/ir/validator.ts
64
+ function validateFlowIR(ir) {
65
+ const errors = [];
66
+ if (!ir || typeof ir !== "object") {
67
+ return {
68
+ valid: false,
69
+ errors: [{ code: "INVALID_INPUT", message: "IR input must be a non-null object" }]
70
+ };
71
+ }
72
+ if (!Array.isArray(ir.nodes)) {
73
+ return {
74
+ valid: false,
75
+ errors: [{ code: "MISSING_NODES", message: "IR is missing required 'nodes' array" }]
76
+ };
77
+ }
78
+ if (!Array.isArray(ir.edges)) {
79
+ return {
80
+ valid: false,
81
+ errors: [{ code: "MISSING_EDGES", message: "IR is missing required 'edges' array" }]
82
+ };
83
+ }
84
+ let workingIR = ir;
85
+ let migrated = false;
86
+ let migrationLog;
87
+ if (needsMigration(ir.version)) {
88
+ try {
89
+ const result = migrateIR(
90
+ { version: ir.version, meta: ir.meta, nodes: ir.nodes, edges: ir.edges },
91
+ CURRENT_IR_VERSION
92
+ );
93
+ if (result.migrated) {
94
+ workingIR = result.ir;
95
+ migrated = true;
96
+ migrationLog = result.applied;
97
+ }
98
+ } catch (err) {
99
+ if (err instanceof MigrationError) {
100
+ errors.push({
101
+ code: "MIGRATION_FAILED",
102
+ message: `IR version migration failed: ${err.message}`
103
+ });
104
+ } else {
105
+ errors.push({
106
+ code: "INVALID_VERSION",
107
+ message: `Unsupported IR version: ${ir.version}`
108
+ });
109
+ }
110
+ }
111
+ }
112
+ if (!migrated && workingIR.version !== CURRENT_IR_VERSION) {
113
+ errors.push({
114
+ code: "INVALID_VERSION",
115
+ message: `Unsupported IR version: ${workingIR.version} (current: ${CURRENT_IR_VERSION})`
116
+ });
117
+ }
118
+ const workingNodeMap = new Map(workingIR.nodes.map((n) => [n.id, n]));
119
+ const triggers = workingIR.nodes.filter(
120
+ (n) => n.category === "trigger" /* TRIGGER */
121
+ );
122
+ if (triggers.length === 0) {
123
+ errors.push({
124
+ code: "NO_TRIGGER",
125
+ message: "Workflow must contain at least one trigger node"
126
+ });
127
+ }
128
+ if (triggers.length > 1) {
129
+ errors.push({
130
+ code: "MULTIPLE_TRIGGERS",
131
+ message: `Workflow must have exactly one trigger, found ${triggers.length}`
132
+ });
133
+ }
134
+ const idSet = /* @__PURE__ */ new Set();
135
+ for (const node of workingIR.nodes) {
136
+ if (idSet.has(node.id)) {
137
+ errors.push({
138
+ code: "DUPLICATE_NODE_ID",
139
+ message: `Duplicate node ID: ${node.id}`,
140
+ nodeId: node.id
141
+ });
142
+ }
143
+ idSet.add(node.id);
144
+ }
145
+ for (const edge of workingIR.edges) {
146
+ if (!workingNodeMap.has(edge.sourceNodeId)) {
147
+ errors.push({
148
+ code: "INVALID_EDGE_SOURCE",
149
+ message: `Edge "${edge.id}" references non-existent source node "${edge.sourceNodeId}"`,
150
+ edgeId: edge.id
151
+ });
152
+ }
153
+ if (!workingNodeMap.has(edge.targetNodeId)) {
154
+ errors.push({
155
+ code: "INVALID_EDGE_TARGET",
156
+ message: `Edge "${edge.id}" references non-existent target node "${edge.targetNodeId}"`,
157
+ edgeId: edge.id
158
+ });
159
+ }
160
+ }
161
+ const cycleErrors = detectCycles(workingIR.nodes, workingIR.edges);
162
+ errors.push(...cycleErrors);
163
+ const connectedNodes = /* @__PURE__ */ new Set();
164
+ for (const edge of workingIR.edges) {
165
+ connectedNodes.add(edge.sourceNodeId);
166
+ connectedNodes.add(edge.targetNodeId);
167
+ }
168
+ for (const node of workingIR.nodes) {
169
+ if (node.category !== "trigger" /* TRIGGER */ && !connectedNodes.has(node.id)) {
170
+ errors.push({
171
+ code: "ORPHAN_NODE",
172
+ message: `Node "${node.id}" (${node.label}) is not connected to any other node`,
173
+ nodeId: node.id
174
+ });
175
+ }
176
+ }
177
+ return {
178
+ valid: errors.length === 0,
179
+ errors,
180
+ migrated,
181
+ migratedIR: migrated ? workingIR : void 0,
182
+ migrationLog
183
+ };
184
+ }
185
+ function detectCycles(nodes, edges) {
186
+ const errors = [];
187
+ const adjacency = /* @__PURE__ */ new Map();
188
+ for (const node of nodes) {
189
+ adjacency.set(node.id, []);
190
+ }
191
+ for (const edge of edges) {
192
+ adjacency.get(edge.sourceNodeId)?.push(edge.targetNodeId);
193
+ }
194
+ const WHITE = 0;
195
+ const GRAY = 1;
196
+ const BLACK = 2;
197
+ const color = /* @__PURE__ */ new Map();
198
+ for (const node of nodes) {
199
+ color.set(node.id, WHITE);
200
+ }
201
+ function dfs(nodeId) {
202
+ color.set(nodeId, GRAY);
203
+ for (const neighbor of adjacency.get(nodeId) ?? []) {
204
+ if (color.get(neighbor) === GRAY) {
205
+ errors.push({
206
+ code: "CYCLE_DETECTED",
207
+ message: `Cycle detected: node "${nodeId}" \u2192 "${neighbor}"`,
208
+ nodeId
209
+ });
210
+ } else if (color.get(neighbor) === WHITE) {
211
+ dfs(neighbor);
212
+ }
213
+ }
214
+ color.set(nodeId, BLACK);
215
+ }
216
+ for (const node of nodes) {
217
+ if (color.get(node.id) === WHITE) {
218
+ dfs(node.id);
219
+ }
220
+ }
221
+ return errors;
222
+ }
223
+
224
+ // src/lib/ir/topological-sort.ts
225
+ function buildGraph(nodes, edges) {
226
+ const indegree = /* @__PURE__ */ new Map();
227
+ const adjacency = /* @__PURE__ */ new Map();
228
+ const reverseAdjacency = /* @__PURE__ */ new Map();
229
+ for (const node of nodes) {
230
+ indegree.set(node.id, 0);
231
+ adjacency.set(node.id, /* @__PURE__ */ new Set());
232
+ reverseAdjacency.set(node.id, /* @__PURE__ */ new Set());
233
+ }
234
+ for (const edge of edges) {
235
+ adjacency.get(edge.sourceNodeId).add(edge.targetNodeId);
236
+ reverseAdjacency.get(edge.targetNodeId).add(edge.sourceNodeId);
237
+ indegree.set(
238
+ edge.targetNodeId,
239
+ (indegree.get(edge.targetNodeId) ?? 0) + 1
240
+ );
241
+ }
242
+ return { indegree, adjacency, reverseAdjacency };
243
+ }
244
+ function topologicalSort(ir) {
245
+ const { nodes, edges } = ir;
246
+ const { indegree, adjacency, reverseAdjacency } = buildGraph(nodes, edges);
247
+ const indegreeCopy = new Map(indegree);
248
+ const sortedNodeIds = [];
249
+ const steps = [];
250
+ let currentLevel = [];
251
+ for (const [nodeId, degree] of indegreeCopy) {
252
+ if (degree === 0) {
253
+ currentLevel.push(nodeId);
254
+ }
255
+ }
256
+ let stepIndex = 0;
257
+ while (currentLevel.length > 0) {
258
+ steps.push({
259
+ index: stepIndex,
260
+ nodeIds: [...currentLevel],
261
+ concurrent: currentLevel.length > 1
262
+ });
263
+ sortedNodeIds.push(...currentLevel);
264
+ const nextLevel = [];
265
+ for (const nodeId of currentLevel) {
266
+ for (const neighbor of adjacency.get(nodeId) ?? []) {
267
+ const newDegree = (indegreeCopy.get(neighbor) ?? 1) - 1;
268
+ indegreeCopy.set(neighbor, newDegree);
269
+ if (newDegree === 0) {
270
+ nextLevel.push(neighbor);
271
+ }
272
+ }
273
+ }
274
+ currentLevel = nextLevel;
275
+ stepIndex++;
276
+ }
277
+ const dependencies = /* @__PURE__ */ new Map();
278
+ const dependents = /* @__PURE__ */ new Map();
279
+ for (const node of nodes) {
280
+ dependencies.set(node.id, reverseAdjacency.get(node.id) ?? /* @__PURE__ */ new Set());
281
+ dependents.set(node.id, adjacency.get(node.id) ?? /* @__PURE__ */ new Set());
282
+ }
283
+ if (sortedNodeIds.length !== nodes.length) {
284
+ throw new Error(
285
+ `Topological sort failed: cycle detected in graph. Sorted ${sortedNodeIds.length}/${nodes.length} nodes.`
286
+ );
287
+ }
288
+ return {
289
+ steps,
290
+ sortedNodeIds,
291
+ dependencies,
292
+ dependents
293
+ };
294
+ }
295
+
296
+ // src/lib/compiler/expression-parser.ts
297
+ function parseExpression(expr, context) {
298
+ const tokens = tokenize(expr);
299
+ return tokens.map((token) => {
300
+ if (token.type === "literal") return token.value;
301
+ return resolveReference(parseReference(token.value), context);
302
+ }).join("");
303
+ }
304
+ function tokenize(input) {
305
+ const tokens = [];
306
+ let i = 0;
307
+ let literalBuf = "";
308
+ while (i < input.length) {
309
+ if (input[i] === "\\" && input[i + 1] === "{" && input[i + 2] === "{") {
310
+ literalBuf += "{{";
311
+ i += 3;
312
+ continue;
313
+ }
314
+ if (input[i] === "{" && input[i + 1] === "{") {
315
+ if (literalBuf) {
316
+ tokens.push({ type: "literal", value: literalBuf });
317
+ literalBuf = "";
318
+ }
319
+ i += 2;
320
+ const refStart = i;
321
+ let bracketDepth = 0;
322
+ while (i < input.length) {
323
+ if (input[i] === "[") {
324
+ bracketDepth++;
325
+ i++;
326
+ } else if (input[i] === "]") {
327
+ bracketDepth--;
328
+ i++;
329
+ } else if (input[i] === "}" && input[i + 1] === "}" && bracketDepth === 0) {
330
+ break;
331
+ } else {
332
+ i++;
333
+ }
334
+ }
335
+ if (i >= input.length) {
336
+ throw new ExpressionParseError(
337
+ `Unclosed template expression: missing matching '}}' (at position ${refStart - 2})`,
338
+ input,
339
+ refStart - 2
340
+ );
341
+ }
342
+ const refContent = input.slice(refStart, i).trim();
343
+ if (!refContent) {
344
+ throw new ExpressionParseError(
345
+ "Empty template expression {{}}",
346
+ input,
347
+ refStart - 2
348
+ );
349
+ }
350
+ tokens.push({ type: "reference", value: refContent });
351
+ i += 2;
352
+ continue;
353
+ }
354
+ literalBuf += input[i];
355
+ i++;
356
+ }
357
+ if (literalBuf) {
358
+ tokens.push({ type: "literal", value: literalBuf });
359
+ }
360
+ return tokens;
361
+ }
362
+ function parseReference(ref) {
363
+ const match = ref.match(/^(\$?\w+)((?:\.[\w]+|\[.+?\])*)$/);
364
+ if (!match) {
365
+ const dotIndex = ref.indexOf(".");
366
+ const bracketIndex = ref.indexOf("[");
367
+ let splitAt = -1;
368
+ if (dotIndex !== -1 && bracketIndex !== -1) {
369
+ splitAt = Math.min(dotIndex, bracketIndex);
370
+ } else if (dotIndex !== -1) {
371
+ splitAt = dotIndex;
372
+ } else if (bracketIndex !== -1) {
373
+ splitAt = bracketIndex;
374
+ }
375
+ if (splitAt > 0) {
376
+ return {
377
+ base: ref.slice(0, splitAt),
378
+ path: ref.slice(splitAt)
379
+ };
380
+ }
381
+ return { base: ref, path: "" };
382
+ }
383
+ return {
384
+ base: match[1],
385
+ path: match[2] || ""
386
+ };
387
+ }
388
+ function resolveReference(ref, context) {
389
+ const { base, path } = ref;
390
+ if (base === "$input") {
391
+ return resolveInputRef(path, context);
392
+ }
393
+ if (base === "$trigger") {
394
+ return resolveTriggerRef(path, context);
395
+ }
396
+ if (context.scopeStack) {
397
+ for (let i = context.scopeStack.length - 1; i >= 0; i--) {
398
+ const scope = context.scopeStack[i];
399
+ if (scope.nodeId === base) {
400
+ return `${scope.scopeVar}['${base}']${path}`;
401
+ }
402
+ }
403
+ }
404
+ if (context.blockScopedNodeIds?.has(base)) {
405
+ return `flowState['${base}']${path}`;
406
+ }
407
+ if (context.symbolTable?.hasVar(base)) {
408
+ return `${context.symbolTable.getVarName(base)}${path}`;
409
+ }
410
+ return `flowState['${base}']${path}`;
411
+ }
412
+ function resolveInputRef(path, context) {
413
+ if (!context.currentNodeId) {
414
+ throw new Error(
415
+ `Expression parser error: No current node context for $input reference`
416
+ );
417
+ }
418
+ const incoming = context.ir.edges.filter(
419
+ (e) => e.targetNodeId === context.currentNodeId
420
+ );
421
+ const dataSource = incoming.find((e) => {
422
+ const src = context.nodeMap.get(e.sourceNodeId);
423
+ return src && src.category !== "trigger" /* TRIGGER */;
424
+ }) || incoming[0];
425
+ if (dataSource) {
426
+ const srcId = dataSource.sourceNodeId;
427
+ if (context.blockScopedNodeIds?.has(srcId)) {
428
+ return `flowState['${srcId}']${path}`;
429
+ }
430
+ if (context.symbolTable?.hasVar(srcId)) {
431
+ return `${context.symbolTable.getVarName(srcId)}${path}`;
432
+ }
433
+ return `flowState['${srcId}']${path}`;
434
+ }
435
+ throw new Error(
436
+ `Expression parser error: Node "${context.currentNodeId}" has no input connected`
437
+ );
438
+ }
439
+ function resolveTriggerRef(path, context) {
440
+ const trigger = context.ir.nodes.find(
441
+ (n) => n.category === "trigger" /* TRIGGER */
442
+ );
443
+ if (trigger) {
444
+ if (context.symbolTable?.hasVar(trigger.id)) {
445
+ return `${context.symbolTable.getVarName(trigger.id)}${path}`;
446
+ }
447
+ return `flowState['${trigger.id}']${path}`;
448
+ }
449
+ return "undefined";
450
+ }
451
+ var ExpressionParseError = class extends Error {
452
+ constructor(message, expression, position) {
453
+ super(message);
454
+ this.expression = expression;
455
+ this.position = position;
456
+ this.name = "ExpressionParseError";
457
+ }
458
+ };
459
+
460
+ // src/lib/compiler/platforms/types.ts
461
+ var platformRegistry = /* @__PURE__ */ new Map();
462
+ function registerPlatform(name, factory) {
463
+ platformRegistry.set(name, factory);
464
+ }
465
+ function getPlatform(name) {
466
+ const factory = platformRegistry.get(name);
467
+ if (!factory) {
468
+ throw new Error(
469
+ `Unknown platform "${name}". Available platforms: ${[...platformRegistry.keys()].join(", ")}`
470
+ );
471
+ }
472
+ return factory();
473
+ }
474
+
475
+ // src/lib/compiler/platforms/nextjs.ts
476
+ var NextjsPlatform = class {
477
+ name = "nextjs";
478
+ generateImports(sourceFile, trigger, _context) {
479
+ if (trigger.nodeType !== "http_webhook" /* HTTP_WEBHOOK */) return;
480
+ const params = trigger.params;
481
+ const isGetOrDelete = ["GET", "DELETE"].includes(params.method);
482
+ sourceFile.addImportDeclaration({
483
+ namedImports: isGetOrDelete ? ["NextRequest", "NextResponse"] : ["NextResponse"],
484
+ moduleSpecifier: "next/server"
485
+ });
486
+ }
487
+ generateFunction(sourceFile, trigger, _context, bodyGenerator) {
488
+ switch (trigger.nodeType) {
489
+ case "http_webhook" /* HTTP_WEBHOOK */:
490
+ this.generateHttpFunction(sourceFile, trigger, bodyGenerator);
491
+ break;
492
+ case "cron_job" /* CRON_JOB */:
493
+ this.generateCronFunction(sourceFile, trigger, bodyGenerator);
494
+ break;
495
+ case "manual" /* MANUAL */:
496
+ this.generateManualFunction(sourceFile, trigger, bodyGenerator);
497
+ break;
498
+ default:
499
+ throw new Error(`Unsupported trigger type: ${trigger.nodeType}`);
500
+ }
501
+ }
502
+ generateResponse(writer, bodyExpr, statusCode, headers) {
503
+ if (headers && Object.keys(headers).length > 0) {
504
+ writer.writeLine(
505
+ `throw new EarlyResponse(NextResponse.json(${bodyExpr}, { status: ${statusCode}, headers: ${JSON.stringify(headers)} }));`
506
+ );
507
+ } else {
508
+ writer.writeLine(
509
+ `throw new EarlyResponse(NextResponse.json(${bodyExpr}, { status: ${statusCode} }));`
510
+ );
511
+ }
512
+ }
513
+ generateErrorResponse(writer) {
514
+ writer.write("if (error instanceof EarlyResponse) ").block(() => {
515
+ writer.writeLine("return error.response;");
516
+ });
517
+ writer.writeLine('console.error("Workflow failed:", error);');
518
+ writer.writeLine(
519
+ 'return NextResponse.json({ error: error instanceof Error ? error.message : "Internal Server Error" }, { status: 500 });'
520
+ );
521
+ }
522
+ getOutputFilePath(trigger) {
523
+ if (trigger.nodeType === "http_webhook" /* HTTP_WEBHOOK */) {
524
+ const params = trigger.params;
525
+ const routePath = params.routePath.replace(/^\//, "");
526
+ return `src/app/${routePath}/route.ts`;
527
+ }
528
+ if (trigger.nodeType === "cron_job" /* CRON_JOB */) {
529
+ const params = trigger.params;
530
+ return `src/lib/cron/${params.functionName}.ts`;
531
+ }
532
+ if (trigger.nodeType === "manual" /* MANUAL */) {
533
+ const params = trigger.params;
534
+ return `src/lib/functions/${params.functionName}.ts`;
535
+ }
536
+ return "src/generated/flow.ts";
537
+ }
538
+ getImplicitDependencies() {
539
+ return [];
540
+ }
541
+ generateTriggerInit(writer, trigger, context) {
542
+ const varName = context.symbolTable.getVarName(trigger.id);
543
+ switch (trigger.nodeType) {
544
+ case "http_webhook" /* HTTP_WEBHOOK */: {
545
+ const params = trigger.params;
546
+ const isGetOrDelete = ["GET", "DELETE"].includes(params.method);
547
+ if (isGetOrDelete) {
548
+ writer.writeLine("const searchParams = req.nextUrl.searchParams;");
549
+ writer.writeLine(
550
+ "const query = Object.fromEntries(searchParams.entries());"
551
+ );
552
+ writer.writeLine(`const ${varName} = { query, url: req.url };`);
553
+ writer.writeLine(`flowState['${trigger.id}'] = ${varName};`);
554
+ } else if (params.parseBody && ["POST", "PUT", "PATCH"].includes(params.method)) {
555
+ writer.writeLine("let body: unknown;");
556
+ writer.write("try ").block(() => {
557
+ writer.writeLine("body = await req.json();");
558
+ });
559
+ writer.write(" catch ").block(() => {
560
+ writer.writeLine(
561
+ 'return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });'
562
+ );
563
+ });
564
+ writer.writeLine(`const ${varName} = { body, url: req.url };`);
565
+ writer.writeLine(`flowState['${trigger.id}'] = ${varName};`);
566
+ } else {
567
+ writer.writeLine(`const ${varName} = { url: req.url };`);
568
+ writer.writeLine(`flowState['${trigger.id}'] = ${varName};`);
569
+ }
570
+ break;
571
+ }
572
+ case "cron_job" /* CRON_JOB */: {
573
+ writer.writeLine(
574
+ `const ${varName} = { triggeredAt: new Date().toISOString() };`
575
+ );
576
+ writer.writeLine(`flowState['${trigger.id}'] = ${varName};`);
577
+ break;
578
+ }
579
+ case "manual" /* MANUAL */: {
580
+ const params = trigger.params;
581
+ if (params.args.length > 0) {
582
+ const argsObj = params.args.map((a) => a.name).join(", ");
583
+ writer.writeLine(`const ${varName} = { ${argsObj} };`);
584
+ writer.writeLine(`flowState['${trigger.id}'] = ${varName};`);
585
+ }
586
+ break;
587
+ }
588
+ }
589
+ }
590
+ // ── Private ──
591
+ generateHttpFunction(sourceFile, trigger, bodyGenerator) {
592
+ const params = trigger.params;
593
+ const isGetOrDelete = ["GET", "DELETE"].includes(params.method);
594
+ const funcDecl = sourceFile.addFunction({
595
+ name: params.method,
596
+ isAsync: true,
597
+ isExported: true,
598
+ parameters: [
599
+ { name: "req", type: isGetOrDelete ? "NextRequest" : "Request" }
600
+ ]
601
+ });
602
+ sourceFile.insertStatements(funcDecl.getChildIndex(), (writer) => {
603
+ writer.writeLine("class EarlyResponse extends Error {");
604
+ writer.writeLine(" constructor(public response: Response) { super(); }");
605
+ writer.writeLine("}");
606
+ writer.blankLine();
607
+ });
608
+ funcDecl.addStatements((writer) => {
609
+ writer.write("try ").block(() => {
610
+ bodyGenerator(writer);
611
+ });
612
+ writer.write(" catch (error) ").block(() => {
613
+ this.generateErrorResponse(writer);
614
+ });
615
+ });
616
+ }
617
+ generateCronFunction(sourceFile, trigger, bodyGenerator) {
618
+ const params = trigger.params;
619
+ sourceFile.addStatements(`// @schedule ${params.schedule}`);
620
+ const funcDecl = sourceFile.addFunction({
621
+ name: params.functionName,
622
+ isAsync: true,
623
+ isExported: true
624
+ });
625
+ funcDecl.addStatements((writer) => {
626
+ bodyGenerator(writer);
627
+ });
628
+ }
629
+ generateManualFunction(sourceFile, trigger, bodyGenerator) {
630
+ const params = trigger.params;
631
+ const funcDecl = sourceFile.addFunction({
632
+ name: params.functionName,
633
+ isAsync: true,
634
+ isExported: true,
635
+ parameters: params.args.map((arg) => ({
636
+ name: arg.name,
637
+ type: arg.type
638
+ }))
639
+ });
640
+ funcDecl.addStatements((writer) => {
641
+ bodyGenerator(writer);
642
+ });
643
+ }
644
+ };
645
+
646
+ // src/lib/compiler/platforms/express.ts
647
+ var ExpressPlatform = class {
648
+ name = "express";
649
+ generateImports(sourceFile, trigger, _context) {
650
+ if (trigger.nodeType === "http_webhook" /* HTTP_WEBHOOK */) {
651
+ sourceFile.addImportDeclaration({
652
+ namedImports: ["Request", "Response"],
653
+ moduleSpecifier: "express"
654
+ });
655
+ }
656
+ }
657
+ generateFunction(sourceFile, trigger, _context, bodyGenerator) {
658
+ switch (trigger.nodeType) {
659
+ case "http_webhook" /* HTTP_WEBHOOK */:
660
+ this.generateHttpHandler(sourceFile, trigger, bodyGenerator);
661
+ break;
662
+ case "cron_job" /* CRON_JOB */:
663
+ this.generateCronFunction(sourceFile, trigger, bodyGenerator);
664
+ break;
665
+ case "manual" /* MANUAL */:
666
+ this.generateManualFunction(sourceFile, trigger, bodyGenerator);
667
+ break;
668
+ default:
669
+ throw new Error(`Unsupported trigger type: ${trigger.nodeType}`);
670
+ }
671
+ }
672
+ generateResponse(writer, bodyExpr, statusCode, headers) {
673
+ if (headers && Object.keys(headers).length > 0) {
674
+ for (const [key, value] of Object.entries(headers)) {
675
+ writer.writeLine(`res.setHeader(${JSON.stringify(key)}, ${JSON.stringify(value)});`);
676
+ }
677
+ }
678
+ writer.writeLine(`throw new EarlyResponse(() => res.status(${statusCode}).json(${bodyExpr}));`);
679
+ }
680
+ generateErrorResponse(writer) {
681
+ writer.write("if (error instanceof EarlyResponse) ").block(() => {
682
+ writer.writeLine("return error.send();");
683
+ });
684
+ writer.writeLine('console.error("Workflow failed:", error);');
685
+ writer.writeLine(
686
+ 'return res.status(500).json({ error: error instanceof Error ? error.message : "Internal Server Error" });'
687
+ );
688
+ }
689
+ getOutputFilePath(trigger) {
690
+ if (trigger.nodeType === "http_webhook" /* HTTP_WEBHOOK */) {
691
+ const params = trigger.params;
692
+ const routePath = params.routePath.replace(/^\//, "").replace(/\//g, "-");
693
+ return `src/routes/${routePath}.ts`;
694
+ }
695
+ if (trigger.nodeType === "cron_job" /* CRON_JOB */) {
696
+ const params = trigger.params;
697
+ return `src/cron/${params.functionName}.ts`;
698
+ }
699
+ if (trigger.nodeType === "manual" /* MANUAL */) {
700
+ const params = trigger.params;
701
+ return `src/functions/${params.functionName}.ts`;
702
+ }
703
+ return "src/generated/flow.ts";
704
+ }
705
+ getImplicitDependencies() {
706
+ return ["express", "@types/express"];
707
+ }
708
+ generateTriggerInit(writer, trigger, context) {
709
+ const varName = context.symbolTable.getVarName(trigger.id);
710
+ switch (trigger.nodeType) {
711
+ case "http_webhook" /* HTTP_WEBHOOK */: {
712
+ const params = trigger.params;
713
+ const isGetOrDelete = ["GET", "DELETE"].includes(params.method);
714
+ if (isGetOrDelete) {
715
+ writer.writeLine(
716
+ "const query = req.query as Record<string, string>;"
717
+ );
718
+ writer.writeLine(
719
+ `const ${varName} = { query, url: req.originalUrl };`
720
+ );
721
+ writer.writeLine(`flowState['${trigger.id}'] = ${varName};`);
722
+ } else if (params.parseBody && ["POST", "PUT", "PATCH"].includes(params.method)) {
723
+ writer.writeLine("const body = req.body;");
724
+ writer.writeLine(
725
+ `const ${varName} = { body, url: req.originalUrl };`
726
+ );
727
+ writer.writeLine(`flowState['${trigger.id}'] = ${varName};`);
728
+ } else {
729
+ writer.writeLine(
730
+ `const ${varName} = { url: req.originalUrl };`
731
+ );
732
+ writer.writeLine(`flowState['${trigger.id}'] = ${varName};`);
733
+ }
734
+ break;
735
+ }
736
+ case "cron_job" /* CRON_JOB */: {
737
+ writer.writeLine(
738
+ `const ${varName} = { triggeredAt: new Date().toISOString() };`
739
+ );
740
+ writer.writeLine(`flowState['${trigger.id}'] = ${varName};`);
741
+ break;
742
+ }
743
+ case "manual" /* MANUAL */: {
744
+ const params = trigger.params;
745
+ if (params.args.length > 0) {
746
+ const argsObj = params.args.map((a) => a.name).join(", ");
747
+ writer.writeLine(`const ${varName} = { ${argsObj} };`);
748
+ writer.writeLine(`flowState['${trigger.id}'] = ${varName};`);
749
+ }
750
+ break;
751
+ }
752
+ }
753
+ }
754
+ // ── Private ──
755
+ generateHttpHandler(sourceFile, trigger, bodyGenerator) {
756
+ const params = trigger.params;
757
+ const funcDecl = sourceFile.addFunction({
758
+ name: `handle${params.method.charAt(0)}${params.method.slice(1).toLowerCase()}`,
759
+ isAsync: true,
760
+ isExported: true,
761
+ parameters: [
762
+ { name: "req", type: "Request" },
763
+ { name: "res", type: "Response" }
764
+ ]
765
+ });
766
+ sourceFile.insertStatements(funcDecl.getChildIndex(), (writer) => {
767
+ writer.writeLine("class EarlyResponse extends Error {");
768
+ writer.writeLine(" constructor(public send: () => void) { super(); }");
769
+ writer.writeLine("}");
770
+ writer.blankLine();
771
+ });
772
+ funcDecl.addStatements((writer) => {
773
+ writer.write("try ").block(() => {
774
+ bodyGenerator(writer);
775
+ });
776
+ writer.write(" catch (error) ").block(() => {
777
+ this.generateErrorResponse(writer);
778
+ });
779
+ });
780
+ }
781
+ generateCronFunction(sourceFile, trigger, bodyGenerator) {
782
+ const params = trigger.params;
783
+ sourceFile.addStatements(`// @schedule ${params.schedule}`);
784
+ const funcDecl = sourceFile.addFunction({
785
+ name: params.functionName,
786
+ isAsync: true,
787
+ isExported: true
788
+ });
789
+ funcDecl.addStatements((writer) => {
790
+ bodyGenerator(writer);
791
+ });
792
+ }
793
+ generateManualFunction(sourceFile, trigger, bodyGenerator) {
794
+ const params = trigger.params;
795
+ const funcDecl = sourceFile.addFunction({
796
+ name: params.functionName,
797
+ isAsync: true,
798
+ isExported: true,
799
+ parameters: params.args.map((arg) => ({
800
+ name: arg.name,
801
+ type: arg.type
802
+ }))
803
+ });
804
+ funcDecl.addStatements((writer) => {
805
+ bodyGenerator(writer);
806
+ });
807
+ }
808
+ };
809
+
810
+ // src/lib/compiler/platforms/cloudflare.ts
811
+ var CloudflarePlatform = class {
812
+ name = "cloudflare";
813
+ generateImports(_sourceFile, _trigger, _context) {
814
+ }
815
+ generateFunction(sourceFile, trigger, _context, bodyGenerator) {
816
+ switch (trigger.nodeType) {
817
+ case "http_webhook" /* HTTP_WEBHOOK */:
818
+ this.generateFetchHandler(sourceFile, trigger, bodyGenerator);
819
+ break;
820
+ case "cron_job" /* CRON_JOB */:
821
+ this.generateScheduledHandler(sourceFile, trigger, bodyGenerator);
822
+ break;
823
+ case "manual" /* MANUAL */:
824
+ this.generateManualFunction(sourceFile, trigger, bodyGenerator);
825
+ break;
826
+ default:
827
+ throw new Error(`Unsupported trigger type: ${trigger.nodeType}`);
828
+ }
829
+ }
830
+ generateResponse(writer, bodyExpr, statusCode, headers) {
831
+ const headersObj = headers && Object.keys(headers).length > 0 ? `, { status: ${statusCode}, headers: ${JSON.stringify({ "Content-Type": "application/json", ...headers })} }` : `, { status: ${statusCode}, headers: { "Content-Type": "application/json" } }`;
832
+ writer.writeLine(
833
+ `throw new EarlyResponse(new Response(JSON.stringify(${bodyExpr})${headersObj}));`
834
+ );
835
+ }
836
+ generateErrorResponse(writer) {
837
+ writer.write("if (error instanceof EarlyResponse) ").block(() => {
838
+ writer.writeLine("return error.response;");
839
+ });
840
+ writer.writeLine('console.error("Workflow failed:", error);');
841
+ writer.writeLine(
842
+ `return new Response(JSON.stringify({ error: error instanceof Error ? error.message : "Internal Server Error" }), { status: 500, headers: { "Content-Type": "application/json" } });`
843
+ );
844
+ }
845
+ getOutputFilePath(trigger) {
846
+ if (trigger.nodeType === "http_webhook" /* HTTP_WEBHOOK */) {
847
+ return "src/worker.ts";
848
+ }
849
+ if (trigger.nodeType === "cron_job" /* CRON_JOB */) {
850
+ const params = trigger.params;
851
+ return `src/scheduled/${params.functionName}.ts`;
852
+ }
853
+ if (trigger.nodeType === "manual" /* MANUAL */) {
854
+ const params = trigger.params;
855
+ return `src/functions/${params.functionName}.ts`;
856
+ }
857
+ return "src/generated/flow.ts";
858
+ }
859
+ getImplicitDependencies() {
860
+ return ["@cloudflare/workers-types"];
861
+ }
862
+ generateTriggerInit(writer, trigger, context) {
863
+ const varName = context.symbolTable.getVarName(trigger.id);
864
+ switch (trigger.nodeType) {
865
+ case "http_webhook" /* HTTP_WEBHOOK */: {
866
+ const params = trigger.params;
867
+ const isGetOrDelete = ["GET", "DELETE"].includes(params.method);
868
+ if (isGetOrDelete) {
869
+ writer.writeLine(
870
+ "const url = new URL(request.url);"
871
+ );
872
+ writer.writeLine(
873
+ "const query = Object.fromEntries(url.searchParams.entries());"
874
+ );
875
+ writer.writeLine(
876
+ `const ${varName} = { query, url: request.url };`
877
+ );
878
+ writer.writeLine(`flowState['${trigger.id}'] = ${varName};`);
879
+ } else if (params.parseBody && ["POST", "PUT", "PATCH"].includes(params.method)) {
880
+ writer.writeLine("let body: unknown;");
881
+ writer.write("try ").block(() => {
882
+ writer.writeLine("body = await request.json();");
883
+ });
884
+ writer.write(" catch ").block(() => {
885
+ writer.writeLine(
886
+ 'return new Response(JSON.stringify({ error: "Invalid JSON body" }), { status: 400, headers: { "Content-Type": "application/json" } });'
887
+ );
888
+ });
889
+ writer.writeLine(
890
+ `const ${varName} = { body, url: request.url };`
891
+ );
892
+ writer.writeLine(`flowState['${trigger.id}'] = ${varName};`);
893
+ } else {
894
+ writer.writeLine(
895
+ `const ${varName} = { url: request.url };`
896
+ );
897
+ writer.writeLine(`flowState['${trigger.id}'] = ${varName};`);
898
+ }
899
+ break;
900
+ }
901
+ case "cron_job" /* CRON_JOB */: {
902
+ writer.writeLine(
903
+ `const ${varName} = { triggeredAt: new Date().toISOString() };`
904
+ );
905
+ writer.writeLine(`flowState['${trigger.id}'] = ${varName};`);
906
+ break;
907
+ }
908
+ case "manual" /* MANUAL */: {
909
+ const params = trigger.params;
910
+ if (params.args.length > 0) {
911
+ const argsObj = params.args.map((a) => a.name).join(", ");
912
+ writer.writeLine(`const ${varName} = { ${argsObj} };`);
913
+ writer.writeLine(`flowState['${trigger.id}'] = ${varName};`);
914
+ }
915
+ break;
916
+ }
917
+ }
918
+ }
919
+ // ── Private ──
920
+ generateFetchHandler(sourceFile, trigger, bodyGenerator) {
921
+ const params = trigger.params;
922
+ sourceFile.addStatements(`// Cloudflare Workers handler`);
923
+ const funcDecl = sourceFile.addFunction({
924
+ name: "handleRequest",
925
+ isAsync: true,
926
+ isExported: true,
927
+ parameters: [
928
+ { name: "request", type: "Request" },
929
+ { name: "env", type: "Env" },
930
+ { name: "ctx", type: "ExecutionContext" }
931
+ ]
932
+ });
933
+ sourceFile.insertStatements(funcDecl.getChildIndex(), (writer) => {
934
+ writer.writeLine("class EarlyResponse extends Error {");
935
+ writer.writeLine(" constructor(public response: Response) { super(); }");
936
+ writer.writeLine("}");
937
+ writer.blankLine();
938
+ });
939
+ funcDecl.addStatements((writer) => {
940
+ writer.write("try ").block(() => {
941
+ bodyGenerator(writer);
942
+ });
943
+ writer.write(" catch (error) ").block(() => {
944
+ this.generateErrorResponse(writer);
945
+ });
946
+ });
947
+ sourceFile.addStatements(`
948
+ export default {
949
+ async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
950
+ return handleRequest(request, env, ctx);
951
+ },
952
+ };`);
953
+ }
954
+ generateScheduledHandler(sourceFile, trigger, bodyGenerator) {
955
+ const params = trigger.params;
956
+ sourceFile.addStatements(`// @schedule ${params.schedule}`);
957
+ const funcDecl = sourceFile.addFunction({
958
+ name: params.functionName,
959
+ isAsync: true,
960
+ isExported: true
961
+ });
962
+ funcDecl.addStatements((writer) => {
963
+ bodyGenerator(writer);
964
+ });
965
+ }
966
+ generateManualFunction(sourceFile, trigger, bodyGenerator) {
967
+ const params = trigger.params;
968
+ const funcDecl = sourceFile.addFunction({
969
+ name: params.functionName,
970
+ isAsync: true,
971
+ isExported: true,
972
+ parameters: params.args.map((arg) => ({
973
+ name: arg.name,
974
+ type: arg.type
975
+ }))
976
+ });
977
+ funcDecl.addStatements((writer) => {
978
+ bodyGenerator(writer);
979
+ });
980
+ }
981
+ };
982
+
983
+ // src/lib/compiler/platforms/index.ts
984
+ registerPlatform("nextjs", () => new NextjsPlatform());
985
+ registerPlatform("express", () => new ExpressPlatform());
986
+ registerPlatform("cloudflare", () => new CloudflarePlatform());
987
+
988
+ // src/lib/compiler/plugins/types.ts
989
+ function createPluginRegistry() {
990
+ const map = /* @__PURE__ */ new Map();
991
+ return {
992
+ register(plugin) {
993
+ map.set(plugin.nodeType, plugin);
994
+ },
995
+ registerAll(plugins) {
996
+ for (const p of plugins) map.set(p.nodeType, p);
997
+ },
998
+ get(nodeType) {
999
+ return map.get(nodeType);
1000
+ },
1001
+ has(nodeType) {
1002
+ return map.has(nodeType);
1003
+ },
1004
+ getAll() {
1005
+ return new Map(map);
1006
+ },
1007
+ clear() {
1008
+ map.clear();
1009
+ }
1010
+ };
1011
+ }
1012
+ var globalRegistry = createPluginRegistry();
1013
+ function getPlugin(nodeType) {
1014
+ return globalRegistry.get(nodeType);
1015
+ }
1016
+
1017
+ // src/lib/compiler/plugins/builtin.ts
1018
+ function inferTypeFromExpression(expr) {
1019
+ const trimmed = expr.trim();
1020
+ if (/\.map\s*\(/.test(trimmed)) return "unknown[]";
1021
+ if (/\.filter\s*\(/.test(trimmed)) return "unknown[]";
1022
+ if (/\.flatMap\s*\(/.test(trimmed)) return "unknown[]";
1023
+ if (/\.slice\s*\(/.test(trimmed)) return "unknown[]";
1024
+ if (/\.concat\s*\(/.test(trimmed)) return "unknown[]";
1025
+ if (/\.sort\s*\(/.test(trimmed)) return "unknown[]";
1026
+ if (/\.reverse\s*\(/.test(trimmed)) return "unknown[]";
1027
+ if (/Array\.from\s*\(/.test(trimmed)) return "unknown[]";
1028
+ if (/\.flat\s*\(/.test(trimmed)) return "unknown[]";
1029
+ if (/\.entries\s*\(/.test(trimmed)) return "[string, unknown][]";
1030
+ if (/\.reduce\s*\(/.test(trimmed)) return "unknown";
1031
+ if (/\.length\b/.test(trimmed)) return "number";
1032
+ if (/\.indexOf\s*\(/.test(trimmed)) return "number";
1033
+ if (/\.findIndex\s*\(/.test(trimmed)) return "number";
1034
+ if (/parseInt\s*\(/.test(trimmed)) return "number";
1035
+ if (/parseFloat\s*\(/.test(trimmed)) return "number";
1036
+ if (/Number\s*\(/.test(trimmed)) return "number";
1037
+ if (/Math\./.test(trimmed)) return "number";
1038
+ if (/\.includes\s*\(/.test(trimmed)) return "boolean";
1039
+ if (/\.some\s*\(/.test(trimmed)) return "boolean";
1040
+ if (/\.every\s*\(/.test(trimmed)) return "boolean";
1041
+ if (/\.has\s*\(/.test(trimmed)) return "boolean";
1042
+ if (/^!/.test(trimmed)) return "boolean";
1043
+ if (/===|!==|==|!=|>=|<=|>|<|&&|\|\|/.test(trimmed)) return "boolean";
1044
+ if (/\.join\s*\(/.test(trimmed)) return "string";
1045
+ if (/\.toString\s*\(/.test(trimmed)) return "string";
1046
+ if (/\.trim\s*\(/.test(trimmed)) return "string";
1047
+ if (/\.replace\s*\(/.test(trimmed)) return "string";
1048
+ if (/\.toLowerCase\s*\(/.test(trimmed)) return "string";
1049
+ if (/\.toUpperCase\s*\(/.test(trimmed)) return "string";
1050
+ if (/String\s*\(/.test(trimmed)) return "string";
1051
+ if (/JSON\.stringify\s*\(/.test(trimmed)) return "string";
1052
+ if (/`[^`]*`/.test(trimmed)) return "string";
1053
+ if (/JSON\.parse\s*\(/.test(trimmed)) return "unknown";
1054
+ if (/Object\.keys\s*\(/.test(trimmed)) return "string[]";
1055
+ if (/Object\.values\s*\(/.test(trimmed)) return "unknown[]";
1056
+ if (/Object\.entries\s*\(/.test(trimmed)) return "[string, unknown][]";
1057
+ if (/Object\.assign\s*\(/.test(trimmed)) return "Record<string, unknown>";
1058
+ if (/Object\.fromEntries\s*\(/.test(trimmed)) return "Record<string, unknown>";
1059
+ if (/^\{/.test(trimmed)) return "Record<string, unknown>";
1060
+ if (/^\[/.test(trimmed)) return "unknown[]";
1061
+ if (/\.\.\.\s*\{\{/.test(trimmed)) return "Record<string, unknown>";
1062
+ if (/\.find\s*\(/.test(trimmed)) return "unknown | undefined";
1063
+ return "unknown";
1064
+ }
1065
+ function inferTypeFromCode(code, returnVar) {
1066
+ const declMatch = code.match(
1067
+ new RegExp(`(?:const|let|var)\\s+${escapeRegex(returnVar)}\\s*(?::\\s*([^=]+?))?\\s*=\\s*(.+?)(?:;|$)`, "m")
1068
+ );
1069
+ if (declMatch) {
1070
+ const typeAnnotation = declMatch[1]?.trim();
1071
+ if (typeAnnotation) return typeAnnotation;
1072
+ const initializer = declMatch[2]?.trim();
1073
+ if (initializer) {
1074
+ if (/^\[/.test(initializer)) return "unknown[]";
1075
+ if (/^\{/.test(initializer)) return "Record<string, unknown>";
1076
+ if (/^["'`]/.test(initializer)) return "string";
1077
+ if (/^\d/.test(initializer)) return "number";
1078
+ if (/^(true|false)$/.test(initializer)) return "boolean";
1079
+ if (/^new Map/.test(initializer)) return "Map<unknown, unknown>";
1080
+ if (/^new Set/.test(initializer)) return "Set<unknown>";
1081
+ if (/\.map\s*\(/.test(initializer)) return "unknown[]";
1082
+ if (/\.filter\s*\(/.test(initializer)) return "unknown[]";
1083
+ }
1084
+ }
1085
+ return "unknown";
1086
+ }
1087
+ function escapeRegex(s) {
1088
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1089
+ }
1090
+ var httpWebhookPlugin = {
1091
+ nodeType: "http_webhook" /* HTTP_WEBHOOK */,
1092
+ generate: () => {
1093
+ },
1094
+ getOutputType(node) {
1095
+ const params = node.params;
1096
+ if (["GET", "DELETE"].includes(params.method)) {
1097
+ return "{ query: Record<string, string>; url: string }";
1098
+ }
1099
+ if (params.parseBody) {
1100
+ return "{ body: unknown; url: string }";
1101
+ }
1102
+ return "{ url: string }";
1103
+ }
1104
+ };
1105
+ var cronJobPlugin = {
1106
+ nodeType: "cron_job" /* CRON_JOB */,
1107
+ generate: () => {
1108
+ },
1109
+ getOutputType: () => "{ triggeredAt: string }"
1110
+ };
1111
+ var manualPlugin = {
1112
+ nodeType: "manual" /* MANUAL */,
1113
+ generate: () => {
1114
+ },
1115
+ getOutputType: () => "Record<string, unknown>"
1116
+ };
1117
+ var fetchApiPlugin = {
1118
+ nodeType: "fetch_api" /* FETCH_API */,
1119
+ generate(node, writer, context) {
1120
+ const params = node.params;
1121
+ const url = context.resolveEnvVars(params.url);
1122
+ writer.write("try ").block(() => {
1123
+ const hasBody = params.body && ["POST", "PUT", "PATCH"].includes(params.method);
1124
+ writer.writeLine(`const response = await fetch(${url}, {`);
1125
+ writer.writeLine(` method: "${params.method}",`);
1126
+ if (params.headers && Object.keys(params.headers).length > 0) {
1127
+ writer.writeLine(` headers: ${JSON.stringify(params.headers)},`);
1128
+ } else if (hasBody) {
1129
+ writer.writeLine(
1130
+ ` headers: { "Content-Type": "application/json" },`
1131
+ );
1132
+ }
1133
+ if (hasBody) {
1134
+ const bodyExpr = context.resolveExpression(params.body, node.id);
1135
+ if (bodyExpr.trimStart().startsWith("JSON.stringify")) {
1136
+ writer.writeLine(` body: ${bodyExpr},`);
1137
+ } else {
1138
+ writer.writeLine(` body: JSON.stringify(${bodyExpr}),`);
1139
+ }
1140
+ }
1141
+ writer.writeLine("});");
1142
+ writer.blankLine();
1143
+ writer.write("if (!response.ok) ").block(() => {
1144
+ writer.writeLine(
1145
+ `throw new Error(\`${node.label} failed: HTTP \${response.status} \${response.statusText}\`);`
1146
+ );
1147
+ });
1148
+ writer.blankLine();
1149
+ if (params.parseJson) {
1150
+ writer.writeLine(`const data = await response.json();`);
1151
+ writer.writeLine(`flowState['${node.id}'] = {`);
1152
+ writer.writeLine(` data,`);
1153
+ writer.writeLine(` status: response.status,`);
1154
+ writer.writeLine(` headers: Object.fromEntries(response.headers.entries()),`);
1155
+ writer.writeLine(`};`);
1156
+ } else {
1157
+ writer.writeLine(`flowState['${node.id}'] = response;`);
1158
+ }
1159
+ });
1160
+ writer.write(" catch (fetchError) ").block(() => {
1161
+ writer.writeLine(
1162
+ `console.error("Fetch failed for ${node.label}:", fetchError);`
1163
+ );
1164
+ writer.writeLine(`throw fetchError;`);
1165
+ });
1166
+ },
1167
+ getRequiredPackages: () => [],
1168
+ getOutputType(node) {
1169
+ const params = node.params;
1170
+ return params.parseJson ? "{ data: unknown; status: number; headers: Record<string, string> }" : "Response";
1171
+ }
1172
+ };
1173
+ var sqlQueryPlugin = {
1174
+ nodeType: "sql_query" /* SQL_QUERY */,
1175
+ generate(node, writer) {
1176
+ const params = node.params;
1177
+ switch (params.orm) {
1178
+ case "drizzle":
1179
+ writer.writeLine(`// Drizzle ORM Query`);
1180
+ writer.writeLine(
1181
+ `const result = await db.execute(sql\`${params.query}\`);`
1182
+ );
1183
+ writer.writeLine(`flowState['${node.id}'] = result;`);
1184
+ break;
1185
+ case "prisma":
1186
+ writer.writeLine(`// Prisma Query`);
1187
+ writer.writeLine(
1188
+ `const result = await prisma.$queryRaw\`${params.query}\`;`
1189
+ );
1190
+ writer.writeLine(`flowState['${node.id}'] = result;`);
1191
+ break;
1192
+ case "raw":
1193
+ default:
1194
+ writer.writeLine(`// Raw SQL Query`);
1195
+ writer.writeLine(
1196
+ `const result = await db.query(\`${params.query}\`);`
1197
+ );
1198
+ writer.writeLine(`flowState['${node.id}'] = result;`);
1199
+ break;
1200
+ }
1201
+ },
1202
+ getRequiredPackages(node) {
1203
+ const params = node.params;
1204
+ const map = {
1205
+ drizzle: ["drizzle-orm"],
1206
+ prisma: ["@prisma/client"],
1207
+ raw: []
1208
+ };
1209
+ return map[params.orm] ?? [];
1210
+ },
1211
+ getOutputType: () => "unknown[]"
1212
+ };
1213
+ var redisCachePlugin = {
1214
+ nodeType: "redis_cache" /* REDIS_CACHE */,
1215
+ generate(node, writer) {
1216
+ const params = node.params;
1217
+ const needsInterpolation = params.key.includes("${") || params.key.includes("{{");
1218
+ const keyExpr = needsInterpolation ? `\`${params.key}\`` : `"${params.key}"`;
1219
+ switch (params.operation) {
1220
+ case "get":
1221
+ writer.writeLine(
1222
+ `flowState['${node.id}'] = await redis.get(${keyExpr});`
1223
+ );
1224
+ break;
1225
+ case "set":
1226
+ if (params.ttl) {
1227
+ writer.writeLine(
1228
+ `await redis.set(${keyExpr}, ${params.value ?? "null"}, "EX", ${params.ttl});`
1229
+ );
1230
+ } else {
1231
+ writer.writeLine(
1232
+ `await redis.set(${keyExpr}, ${params.value ?? "null"});`
1233
+ );
1234
+ }
1235
+ writer.writeLine(`flowState['${node.id}'] = true;`);
1236
+ break;
1237
+ case "del":
1238
+ writer.writeLine(`await redis.del(${keyExpr});`);
1239
+ writer.writeLine(`flowState['${node.id}'] = true;`);
1240
+ break;
1241
+ }
1242
+ },
1243
+ getRequiredPackages: () => ["ioredis"],
1244
+ getOutputType(node) {
1245
+ const params = node.params;
1246
+ return params.operation === "get" ? "string | null" : "boolean";
1247
+ }
1248
+ };
1249
+ var DANGEROUS_CODE_PATTERNS = [
1250
+ { pattern: /\bprocess\.exit\b/, desc: "process.exit() \u2014 terminates the Node.js process" },
1251
+ { pattern: /\bchild_process\b/, desc: "child_process \u2014 can execute arbitrary system commands" },
1252
+ { pattern: /\beval\s*\(/, desc: "eval() \u2014 dynamically executes arbitrary code" },
1253
+ { pattern: /\bnew\s+Function\s*\(/, desc: "new Function() \u2014 dynamically constructs functions" },
1254
+ { pattern: /\brequire\s*\(\s*['"]fs['"]/, desc: "require('fs') \u2014 file system access" },
1255
+ { pattern: /\bimport\s*\(\s*['"]fs['"]/, desc: "import('fs') \u2014 file system access" },
1256
+ { pattern: /\bfs\.\w*(unlink|rmdir|rm|writeFile)\b/, desc: "fs delete/write operations" }
1257
+ ];
1258
+ var customCodePlugin = {
1259
+ nodeType: "custom_code" /* CUSTOM_CODE */,
1260
+ generate(node, writer, context) {
1261
+ const params = node.params;
1262
+ const warnings = [];
1263
+ for (const { pattern, desc } of DANGEROUS_CODE_PATTERNS) {
1264
+ if (pattern.test(params.code)) {
1265
+ warnings.push(desc);
1266
+ }
1267
+ }
1268
+ if (warnings.length > 0) {
1269
+ writer.writeLine(`// \u26A0\uFE0F SECURITY WARNING: This custom code uses the following dangerous APIs:`);
1270
+ for (const w of warnings) {
1271
+ writer.writeLine(`// - ${w}`);
1272
+ }
1273
+ writer.writeLine(`// Please carefully review this code before deployment.`);
1274
+ if (context && "addWarning" in context) {
1275
+ const addWarning = context.addWarning;
1276
+ addWarning?.(`[${node.id}] Custom code uses dangerous API: ${warnings.join(", ")}`);
1277
+ }
1278
+ }
1279
+ const resultVar = `custom_result_${node.id.replace(/[^a-zA-Z0-9_]/g, "_")}`;
1280
+ if (params.returnVariable) {
1281
+ writer.writeLine(`const ${resultVar} = await (async () => {`);
1282
+ } else {
1283
+ writer.writeLine(`await (async () => {`);
1284
+ }
1285
+ writer.writeLine(`// Custom Code: ${node.label}`);
1286
+ const lines = params.code.split("\n");
1287
+ for (const line of lines) {
1288
+ writer.writeLine(` ${line}`);
1289
+ }
1290
+ if (params.returnVariable) {
1291
+ writer.writeLine(` if (typeof ${params.returnVariable} !== 'undefined') return ${params.returnVariable};`);
1292
+ }
1293
+ writer.writeLine(`})();`);
1294
+ if (params.returnVariable) {
1295
+ writer.writeLine(`flowState['${node.id}'] = ${resultVar};`);
1296
+ }
1297
+ },
1298
+ getOutputType(node) {
1299
+ const params = node.params;
1300
+ if (params.returnType) return params.returnType;
1301
+ if (!params.returnVariable) return "void";
1302
+ return inferTypeFromCode(params.code, params.returnVariable);
1303
+ }
1304
+ };
1305
+ var callSubflowPlugin = {
1306
+ nodeType: "call_subflow" /* CALL_SUBFLOW */,
1307
+ generate(node, writer, context) {
1308
+ const params = node.params;
1309
+ const existing = context.imports.get(params.flowPath);
1310
+ if (existing) {
1311
+ existing.add(params.functionName);
1312
+ } else {
1313
+ context.imports.set(params.flowPath, /* @__PURE__ */ new Set([params.functionName]));
1314
+ }
1315
+ const args = Object.entries(params.inputMapping).map(([key, expr]) => {
1316
+ const resolved = context.resolveExpression(expr, node.id);
1317
+ return `${key}: ${resolved}`;
1318
+ }).join(", ");
1319
+ writer.writeLine(
1320
+ `flowState['${node.id}'] = await ${params.functionName}({ ${args} });`
1321
+ );
1322
+ },
1323
+ getOutputType(node) {
1324
+ const params = node.params;
1325
+ if (params.returnType) return params.returnType;
1326
+ return `Awaited<ReturnType<typeof ${params.functionName}>>`;
1327
+ }
1328
+ };
1329
+ var ifElsePlugin = {
1330
+ nodeType: "if_else" /* IF_ELSE */,
1331
+ generate(node, writer, context) {
1332
+ const params = node.params;
1333
+ const trueEdges = context.ir.edges.filter(
1334
+ (e) => e.sourceNodeId === node.id && e.sourcePortId === "true"
1335
+ );
1336
+ const falseEdges = context.ir.edges.filter(
1337
+ (e) => e.sourceNodeId === node.id && e.sourcePortId === "false"
1338
+ );
1339
+ const conditionExpr = context.resolveExpression(
1340
+ params.condition,
1341
+ node.id
1342
+ );
1343
+ writer.write(`if (${conditionExpr}) `).block(() => {
1344
+ writer.writeLine(`flowState['${node.id}'] = true;`);
1345
+ for (const edge of trueEdges) {
1346
+ const childNode = context.nodeMap.get(edge.targetNodeId);
1347
+ if (childNode) {
1348
+ context.generateChildNode(writer, childNode);
1349
+ }
1350
+ }
1351
+ });
1352
+ if (falseEdges.length > 0) {
1353
+ writer.write(" else ").block(() => {
1354
+ writer.writeLine(`flowState['${node.id}'] = false;`);
1355
+ for (const edge of falseEdges) {
1356
+ const childNode = context.nodeMap.get(edge.targetNodeId);
1357
+ if (childNode) {
1358
+ context.generateChildNode(writer, childNode);
1359
+ }
1360
+ }
1361
+ });
1362
+ }
1363
+ },
1364
+ getOutputType: () => "boolean"
1365
+ };
1366
+ var forLoopPlugin = {
1367
+ nodeType: "for_loop" /* FOR_LOOP */,
1368
+ generate(node, writer, context) {
1369
+ const params = node.params;
1370
+ const iterableExpr = context.resolveExpression(
1371
+ params.iterableExpression,
1372
+ node.id
1373
+ );
1374
+ const sanitizedId = node.id.replace(/[^a-zA-Z0-9_]/g, "_");
1375
+ const scopeVar = `_scope_${sanitizedId}`;
1376
+ writer.writeLine(`const ${sanitizedId}_results: unknown[] = [];`);
1377
+ if (params.indexVariable) {
1378
+ writer.write(
1379
+ `for (const [${params.indexVariable}, ${params.itemVariable}] of (${iterableExpr}).entries()) `
1380
+ );
1381
+ } else {
1382
+ writer.write(
1383
+ `for (const ${params.itemVariable} of ${iterableExpr}) `
1384
+ );
1385
+ }
1386
+ writer.block(() => {
1387
+ writer.writeLine(
1388
+ `const ${scopeVar}: Record<string, unknown> = {};`
1389
+ );
1390
+ writer.writeLine(
1391
+ `${scopeVar}['${node.id}'] = ${params.itemVariable};`
1392
+ );
1393
+ context.pushScope(node.id, scopeVar);
1394
+ const childEdges = context.ir.edges.filter(
1395
+ (e) => e.sourceNodeId === node.id && e.sourcePortId === "body"
1396
+ );
1397
+ for (const edge of childEdges) {
1398
+ const childNode = context.nodeMap.get(edge.targetNodeId);
1399
+ if (childNode) {
1400
+ context.generateChildNode(writer, childNode);
1401
+ }
1402
+ }
1403
+ context.popScope();
1404
+ writer.writeLine(`${sanitizedId}_results.push(${params.itemVariable});`);
1405
+ });
1406
+ writer.writeLine(`flowState['${node.id}'] = ${sanitizedId}_results;`);
1407
+ },
1408
+ getOutputType: () => "unknown[]"
1409
+ };
1410
+ var tryCatchPlugin = {
1411
+ nodeType: "try_catch" /* TRY_CATCH */,
1412
+ generate(node, writer, context) {
1413
+ const params = node.params;
1414
+ const successEdges = context.ir.edges.filter(
1415
+ (e) => e.sourceNodeId === node.id && e.sourcePortId === "success"
1416
+ );
1417
+ const errorEdges = context.ir.edges.filter(
1418
+ (e) => e.sourceNodeId === node.id && e.sourcePortId === "error"
1419
+ );
1420
+ const tryScopeVar = `_scope_${node.id.replace(/[^a-zA-Z0-9_]/g, "_")}_try`;
1421
+ const catchScopeVar = `_scope_${node.id.replace(/[^a-zA-Z0-9_]/g, "_")}_catch`;
1422
+ writer.write("try ").block(() => {
1423
+ writer.writeLine(
1424
+ `const ${tryScopeVar}: Record<string, unknown> = {};`
1425
+ );
1426
+ context.pushScope(node.id, tryScopeVar);
1427
+ for (const edge of successEdges) {
1428
+ const childNode = context.nodeMap.get(edge.targetNodeId);
1429
+ if (childNode) {
1430
+ context.generateChildNode(writer, childNode);
1431
+ }
1432
+ }
1433
+ context.popScope();
1434
+ writer.writeLine(
1435
+ `flowState['${node.id}'] = { success: true };`
1436
+ );
1437
+ });
1438
+ writer.write(` catch (${params.errorVariable}) `).block(() => {
1439
+ writer.writeLine(
1440
+ `console.error("Error in ${node.label}:", ${params.errorVariable});`
1441
+ );
1442
+ writer.writeLine(
1443
+ `const ${catchScopeVar}: Record<string, unknown> = {};`
1444
+ );
1445
+ writer.writeLine(
1446
+ `flowState['${node.id}'] = { success: false, error: ${params.errorVariable} };`
1447
+ );
1448
+ context.pushScope(node.id, catchScopeVar);
1449
+ for (const edge of errorEdges) {
1450
+ const childNode = context.nodeMap.get(edge.targetNodeId);
1451
+ if (childNode) {
1452
+ context.generateChildNode(writer, childNode);
1453
+ }
1454
+ }
1455
+ context.popScope();
1456
+ });
1457
+ },
1458
+ getOutputType: () => "{ success: boolean; error?: unknown }"
1459
+ };
1460
+ var promiseAllPlugin = {
1461
+ nodeType: "promise_all" /* PROMISE_ALL */,
1462
+ generate(node, writer) {
1463
+ writer.writeLine(`// Promise.all handled by concurrent execution`);
1464
+ writer.writeLine(
1465
+ `flowState['${node.id}'] = undefined; // populated by concurrent handler`
1466
+ );
1467
+ },
1468
+ getOutputType: () => "unknown[]"
1469
+ };
1470
+ var declarePlugin = {
1471
+ nodeType: "declare" /* DECLARE */,
1472
+ generate(node, writer) {
1473
+ const params = node.params;
1474
+ const keyword = params.isConst ? "const" : "let";
1475
+ const initialValue = params.initialValue ?? "undefined";
1476
+ writer.writeLine(`${keyword} ${params.name} = ${initialValue};`);
1477
+ writer.writeLine(`flowState['${node.id}'] = ${params.name};`);
1478
+ },
1479
+ getOutputType(node) {
1480
+ const params = node.params;
1481
+ return params.dataType;
1482
+ }
1483
+ };
1484
+ var transformPlugin = {
1485
+ nodeType: "transform" /* TRANSFORM */,
1486
+ generate(node, writer, context) {
1487
+ const params = node.params;
1488
+ const expr = context.resolveExpression(params.expression, node.id);
1489
+ writer.writeLine(`flowState['${node.id}'] = ${expr};`);
1490
+ },
1491
+ getOutputType(node) {
1492
+ const params = node.params;
1493
+ return inferTypeFromExpression(params.expression);
1494
+ }
1495
+ };
1496
+ var returnResponsePlugin = {
1497
+ nodeType: "return_response" /* RETURN_RESPONSE */,
1498
+ generate(node, writer, context) {
1499
+ const params = node.params;
1500
+ const bodyExpr = context.resolveExpression(
1501
+ params.bodyExpression,
1502
+ node.id
1503
+ );
1504
+ const ctx = context;
1505
+ if (ctx.__platformResponse) {
1506
+ ctx.__platformResponse(writer, bodyExpr, params.statusCode, params.headers);
1507
+ } else {
1508
+ if (params.headers && Object.keys(params.headers).length > 0) {
1509
+ writer.writeLine(
1510
+ `return NextResponse.json(${bodyExpr}, { status: ${params.statusCode}, headers: ${JSON.stringify(params.headers)} });`
1511
+ );
1512
+ } else {
1513
+ writer.writeLine(
1514
+ `return NextResponse.json(${bodyExpr}, { status: ${params.statusCode} });`
1515
+ );
1516
+ }
1517
+ }
1518
+ },
1519
+ getOutputType: () => "never"
1520
+ };
1521
+ var builtinPlugins = [
1522
+ // Triggers
1523
+ httpWebhookPlugin,
1524
+ cronJobPlugin,
1525
+ manualPlugin,
1526
+ // Actions
1527
+ fetchApiPlugin,
1528
+ sqlQueryPlugin,
1529
+ redisCachePlugin,
1530
+ customCodePlugin,
1531
+ callSubflowPlugin,
1532
+ // Logic
1533
+ ifElsePlugin,
1534
+ forLoopPlugin,
1535
+ tryCatchPlugin,
1536
+ promiseAllPlugin,
1537
+ // Variables
1538
+ declarePlugin,
1539
+ transformPlugin,
1540
+ // Output
1541
+ returnResponsePlugin
1542
+ ];
1543
+
1544
+ // src/lib/compiler/type-inference.ts
1545
+ function inferFlowStateTypes(ir, registry) {
1546
+ const nodeTypes = /* @__PURE__ */ new Map();
1547
+ for (const node of ir.nodes) {
1548
+ const type = inferNodeOutputType(node, registry);
1549
+ nodeTypes.set(node.id, type);
1550
+ }
1551
+ const fields = ir.nodes.map((node) => {
1552
+ const type = nodeTypes.get(node.id) || "unknown";
1553
+ const safeId = node.id;
1554
+ return ` '${safeId}'?: ${type};`;
1555
+ }).join("\n");
1556
+ const interfaceCode = `interface FlowState {
1557
+ ${fields}
1558
+ }`;
1559
+ return { interfaceCode, nodeTypes };
1560
+ }
1561
+ function inferNodeOutputType(node, registry) {
1562
+ const plugin = registry?.get(node.nodeType) ?? getPlugin(node.nodeType);
1563
+ if (plugin?.getOutputType) {
1564
+ return plugin.getOutputType(node);
1565
+ }
1566
+ if (node.outputs && node.outputs.length > 0) {
1567
+ const primaryOutput = node.outputs[0];
1568
+ return mapFlowDataTypeToTS(primaryOutput.dataType);
1569
+ }
1570
+ return "unknown";
1571
+ }
1572
+ function mapFlowDataTypeToTS(dataType) {
1573
+ switch (dataType) {
1574
+ case "string":
1575
+ return "string";
1576
+ case "number":
1577
+ return "number";
1578
+ case "boolean":
1579
+ return "boolean";
1580
+ case "object":
1581
+ return "Record<string, unknown>";
1582
+ case "array":
1583
+ return "unknown[]";
1584
+ case "void":
1585
+ return "void";
1586
+ case "Response":
1587
+ return "Response";
1588
+ case "any":
1589
+ default:
1590
+ return "unknown";
1591
+ }
1592
+ }
1593
+
1594
+ // src/lib/compiler/symbol-table.ts
1595
+ function buildSymbolTable(ir) {
1596
+ const mappings = /* @__PURE__ */ new Map();
1597
+ const usedNames = /* @__PURE__ */ new Set();
1598
+ for (const node of ir.nodes) {
1599
+ let name = labelToVarName(node.label);
1600
+ if (!name || RESERVED_WORDS.has(name)) {
1601
+ name = `${name || "node"}Result`;
1602
+ }
1603
+ if (/^[0-9]/.test(name)) {
1604
+ name = `_${name}`;
1605
+ }
1606
+ let uniqueName = name;
1607
+ let counter = 2;
1608
+ while (usedNames.has(uniqueName)) {
1609
+ uniqueName = `${name}${counter}`;
1610
+ counter++;
1611
+ }
1612
+ usedNames.add(uniqueName);
1613
+ mappings.set(node.id, uniqueName);
1614
+ }
1615
+ return {
1616
+ getVarName(nodeId) {
1617
+ return mappings.get(nodeId) ?? `node_${nodeId.replace(/[^a-zA-Z0-9_]/g, "_")}`;
1618
+ },
1619
+ hasVar(nodeId) {
1620
+ return mappings.has(nodeId);
1621
+ },
1622
+ getAllMappings() {
1623
+ return mappings;
1624
+ }
1625
+ };
1626
+ }
1627
+ function labelToVarName(label) {
1628
+ const cleaned = label.replace(/[/\-_.]/g, " ").replace(/[^a-zA-Z0-9\s]/g, "").trim();
1629
+ if (!cleaned) return "";
1630
+ const words = cleaned.replace(/([a-z])([A-Z])/g, "$1 $2").split(/\s+/).filter((w) => w.length > 0);
1631
+ if (words.length === 0) return "";
1632
+ return words.map((word, i) => {
1633
+ const lower = word.toLowerCase();
1634
+ if (i === 0) return lower;
1635
+ return lower.charAt(0).toUpperCase() + lower.slice(1);
1636
+ }).join("");
1637
+ }
1638
+ var RESERVED_WORDS = /* @__PURE__ */ new Set([
1639
+ // JavaScript reserved words
1640
+ "break",
1641
+ "case",
1642
+ "catch",
1643
+ "continue",
1644
+ "debugger",
1645
+ "default",
1646
+ "delete",
1647
+ "do",
1648
+ "else",
1649
+ "finally",
1650
+ "for",
1651
+ "function",
1652
+ "if",
1653
+ "in",
1654
+ "instanceof",
1655
+ "new",
1656
+ "return",
1657
+ "switch",
1658
+ "this",
1659
+ "throw",
1660
+ "try",
1661
+ "typeof",
1662
+ "var",
1663
+ "void",
1664
+ "while",
1665
+ "with",
1666
+ "class",
1667
+ "const",
1668
+ "enum",
1669
+ "export",
1670
+ "extends",
1671
+ "import",
1672
+ "super",
1673
+ "implements",
1674
+ "interface",
1675
+ "let",
1676
+ "package",
1677
+ "private",
1678
+ "protected",
1679
+ "public",
1680
+ "static",
1681
+ "yield",
1682
+ "await",
1683
+ "async",
1684
+ // Built-in global values
1685
+ "undefined",
1686
+ "null",
1687
+ "true",
1688
+ "false",
1689
+ "NaN",
1690
+ "Infinity",
1691
+ // Common identifiers in generated code (avoid shadowing)
1692
+ "req",
1693
+ "res",
1694
+ "body",
1695
+ "query",
1696
+ "data",
1697
+ "result",
1698
+ "response",
1699
+ "error",
1700
+ "flowState",
1701
+ "searchParams",
1702
+ "NextResponse",
1703
+ "NextRequest"
1704
+ ]);
1705
+
1706
+ // src/lib/compiler/compiler.ts
1707
+ function compile(ir, options) {
1708
+ const pluginRegistry = createPluginRegistry();
1709
+ pluginRegistry.registerAll(builtinPlugins);
1710
+ if (options?.plugins) {
1711
+ pluginRegistry.registerAll(options.plugins);
1712
+ }
1713
+ const validation = validateFlowIR(ir);
1714
+ if (!validation.valid) {
1715
+ return {
1716
+ success: false,
1717
+ errors: validation.errors.map((e) => `[${e.code}] ${e.message}`)
1718
+ };
1719
+ }
1720
+ let plan;
1721
+ try {
1722
+ plan = topologicalSort(ir);
1723
+ } catch (err) {
1724
+ return {
1725
+ success: false,
1726
+ errors: [err instanceof Error ? err.message : String(err)]
1727
+ };
1728
+ }
1729
+ const nodeMap = new Map(ir.nodes.map((n) => [n.id, n]));
1730
+ const platformName = options?.platform ?? "nextjs";
1731
+ const platform = getPlatform(platformName);
1732
+ const symbolTable = buildSymbolTable(ir);
1733
+ const context = {
1734
+ ir,
1735
+ plan,
1736
+ nodeMap,
1737
+ envVars: /* @__PURE__ */ new Set(),
1738
+ imports: /* @__PURE__ */ new Map(),
1739
+ requiredPackages: /* @__PURE__ */ new Set(),
1740
+ sourceMapEntries: /* @__PURE__ */ new Map(),
1741
+ currentLine: 1,
1742
+ platform,
1743
+ symbolTable,
1744
+ scopeStack: [],
1745
+ childBlockNodeIds: /* @__PURE__ */ new Set(),
1746
+ dagMode: false,
1747
+ symbolTableExclusions: /* @__PURE__ */ new Set(),
1748
+ generatedBlockNodeIds: /* @__PURE__ */ new Set(),
1749
+ pluginRegistry
1750
+ };
1751
+ const trigger = ir.nodes.find((n) => n.category === "trigger" /* TRIGGER */);
1752
+ const preComputedBlockNodes = computeControlFlowDescendants(ir, trigger.id);
1753
+ for (const nodeId of preComputedBlockNodes) {
1754
+ context.childBlockNodeIds.add(nodeId);
1755
+ context.symbolTableExclusions.add(nodeId);
1756
+ }
1757
+ const hasConcurrency = plan.steps.some(
1758
+ (s) => s.concurrent && s.nodeIds.filter((id) => id !== trigger.id).length > 1
1759
+ );
1760
+ if (hasConcurrency) {
1761
+ context.dagMode = true;
1762
+ for (const node of ir.nodes) {
1763
+ if (node.category !== "trigger" /* TRIGGER */) {
1764
+ context.symbolTableExclusions.add(node.id);
1765
+ }
1766
+ }
1767
+ }
1768
+ const project = new Project({ useInMemoryFileSystem: true });
1769
+ const sourceFile = project.createSourceFile("generated.ts", "");
1770
+ try {
1771
+ generateCode(sourceFile, trigger, context);
1772
+ } catch (err) {
1773
+ throw err;
1774
+ }
1775
+ sourceFile.formatText({
1776
+ indentSize: 2,
1777
+ convertTabsToSpaces: true
1778
+ });
1779
+ const code = sourceFile.getFullText();
1780
+ const filePath = platform.getOutputFilePath(trigger);
1781
+ collectRequiredPackages(ir, context);
1782
+ const sourceMap = buildSourceMap(code, ir, filePath);
1783
+ const dependencies = {
1784
+ all: [...context.requiredPackages].sort(),
1785
+ missing: [...context.requiredPackages].sort(),
1786
+ installCommand: context.requiredPackages.size > 0 ? `npm install ${[...context.requiredPackages].sort().join(" ")}` : void 0
1787
+ };
1788
+ return {
1789
+ success: true,
1790
+ code,
1791
+ filePath,
1792
+ dependencies,
1793
+ sourceMap
1794
+ };
1795
+ }
1796
+ function generateCode(sourceFile, trigger, context) {
1797
+ const { platform } = context;
1798
+ platform.generateImports(sourceFile, trigger, {
1799
+ ir: context.ir,
1800
+ nodeMap: context.nodeMap,
1801
+ envVars: context.envVars,
1802
+ imports: context.imports
1803
+ });
1804
+ platform.generateFunction(
1805
+ sourceFile,
1806
+ trigger,
1807
+ {
1808
+ ir: context.ir,
1809
+ nodeMap: context.nodeMap,
1810
+ envVars: context.envVars,
1811
+ imports: context.imports
1812
+ },
1813
+ (writer) => {
1814
+ generateFunctionBody(writer, trigger, context);
1815
+ }
1816
+ );
1817
+ }
1818
+ function generateFunctionBody(writer, trigger, context) {
1819
+ const { ir } = context;
1820
+ const typeInfo = inferFlowStateTypes(ir, context.pluginRegistry);
1821
+ writer.writeLine(typeInfo.interfaceCode);
1822
+ writer.writeLine("const flowState: Partial<FlowState> = {};");
1823
+ writer.blankLine();
1824
+ context.platform.generateTriggerInit(writer, trigger, {
1825
+ symbolTable: context.symbolTable
1826
+ });
1827
+ writer.blankLine();
1828
+ generateNodeChain(writer, trigger.id, context);
1829
+ }
1830
+ var CONTROL_FLOW_PORT_MAP = {
1831
+ ["if_else" /* IF_ELSE */]: /* @__PURE__ */ new Set(["true", "false"]),
1832
+ ["for_loop" /* FOR_LOOP */]: /* @__PURE__ */ new Set(["body"]),
1833
+ ["try_catch" /* TRY_CATCH */]: /* @__PURE__ */ new Set(["success", "error"])
1834
+ };
1835
+ function isControlFlowEdge(edge, nodeMap) {
1836
+ const sourceNode = nodeMap.get(edge.sourceNodeId);
1837
+ if (!sourceNode) return false;
1838
+ const controlPorts = CONTROL_FLOW_PORT_MAP[sourceNode.nodeType];
1839
+ return controlPorts !== void 0 && controlPorts.has(edge.sourcePortId);
1840
+ }
1841
+ function computeControlFlowDescendants(ir, triggerId) {
1842
+ const nodeMap = new Map(ir.nodes.map((n) => [n.id, n]));
1843
+ const strippedSuccessors = /* @__PURE__ */ new Map();
1844
+ for (const node of ir.nodes) {
1845
+ strippedSuccessors.set(node.id, /* @__PURE__ */ new Set());
1846
+ }
1847
+ for (const edge of ir.edges) {
1848
+ if (isControlFlowEdge(edge, nodeMap)) continue;
1849
+ strippedSuccessors.get(edge.sourceNodeId)?.add(edge.targetNodeId);
1850
+ }
1851
+ const reachableWithoutControlFlow = /* @__PURE__ */ new Set();
1852
+ const queue = [triggerId];
1853
+ while (queue.length > 0) {
1854
+ const id = queue.shift();
1855
+ if (reachableWithoutControlFlow.has(id)) continue;
1856
+ reachableWithoutControlFlow.add(id);
1857
+ for (const succ of strippedSuccessors.get(id) ?? []) {
1858
+ if (!reachableWithoutControlFlow.has(succ)) {
1859
+ queue.push(succ);
1860
+ }
1861
+ }
1862
+ }
1863
+ const childBlockNodeIds = /* @__PURE__ */ new Set();
1864
+ for (const node of ir.nodes) {
1865
+ if (node.id === triggerId) continue;
1866
+ if (!reachableWithoutControlFlow.has(node.id)) {
1867
+ childBlockNodeIds.add(node.id);
1868
+ }
1869
+ }
1870
+ return childBlockNodeIds;
1871
+ }
1872
+ function generateBlockContinuation(writer, fromNodeId, context) {
1873
+ const reachable = /* @__PURE__ */ new Set();
1874
+ const bfsQueue = [fromNodeId];
1875
+ const edgeSuccessors = /* @__PURE__ */ new Map();
1876
+ for (const edge of context.ir.edges) {
1877
+ if (!edgeSuccessors.has(edge.sourceNodeId)) {
1878
+ edgeSuccessors.set(edge.sourceNodeId, []);
1879
+ }
1880
+ edgeSuccessors.get(edge.sourceNodeId).push(edge.targetNodeId);
1881
+ }
1882
+ while (bfsQueue.length > 0) {
1883
+ const id = bfsQueue.shift();
1884
+ if (reachable.has(id)) continue;
1885
+ reachable.add(id);
1886
+ for (const succ of edgeSuccessors.get(id) ?? []) {
1887
+ if (!reachable.has(succ)) {
1888
+ bfsQueue.push(succ);
1889
+ }
1890
+ }
1891
+ }
1892
+ for (const nodeId of context.plan.sortedNodeIds) {
1893
+ if (nodeId === fromNodeId) continue;
1894
+ if (!reachable.has(nodeId)) continue;
1895
+ if (!context.childBlockNodeIds.has(nodeId)) continue;
1896
+ if (context.generatedBlockNodeIds.has(nodeId)) continue;
1897
+ const deps = context.plan.dependencies.get(nodeId) ?? /* @__PURE__ */ new Set();
1898
+ const allBlockDepsReady = [...deps].every((depId) => {
1899
+ if (!context.childBlockNodeIds.has(depId)) return true;
1900
+ return context.generatedBlockNodeIds.has(depId);
1901
+ });
1902
+ if (!allBlockDepsReady) continue;
1903
+ const node = context.nodeMap.get(nodeId);
1904
+ if (!node) continue;
1905
+ context.generatedBlockNodeIds.add(nodeId);
1906
+ context.symbolTableExclusions.add(nodeId);
1907
+ writer.writeLine(`// --- ${node.label} (${node.nodeType}) [${node.id}] ---`);
1908
+ generateNodeBody(writer, node, context);
1909
+ }
1910
+ }
1911
+ function generateNodeChain(writer, triggerId, context) {
1912
+ if (context.dagMode) {
1913
+ generateNodeChainDAG(writer, triggerId, context);
1914
+ } else {
1915
+ generateNodeChainSequential(writer, triggerId, context);
1916
+ }
1917
+ }
1918
+ function generateNodeChainSequential(writer, triggerId, context) {
1919
+ const { plan, nodeMap } = context;
1920
+ for (const step of plan.steps) {
1921
+ const activeNodes = step.nodeIds.filter(
1922
+ (id) => id !== triggerId && !context.childBlockNodeIds.has(id)
1923
+ );
1924
+ if (activeNodes.length === 0) continue;
1925
+ if (step.concurrent && activeNodes.length > 1) {
1926
+ generateConcurrentNodes(writer, activeNodes, context);
1927
+ } else {
1928
+ for (const nodeId of activeNodes) {
1929
+ const node = nodeMap.get(nodeId);
1930
+ if (!node) continue;
1931
+ generateSingleNode(writer, node, context);
1932
+ writer.blankLine();
1933
+ }
1934
+ }
1935
+ }
1936
+ }
1937
+ function resolveToDAGNodes(depId, triggerId, context, visited = /* @__PURE__ */ new Set()) {
1938
+ if (visited.has(depId) || depId === triggerId) return [];
1939
+ visited.add(depId);
1940
+ if (!context.childBlockNodeIds.has(depId)) return [depId];
1941
+ const parentDeps = context.plan.dependencies.get(depId) ?? /* @__PURE__ */ new Set();
1942
+ const result = [];
1943
+ for (const pd of parentDeps) {
1944
+ result.push(...resolveToDAGNodes(pd, triggerId, context, visited));
1945
+ }
1946
+ return result;
1947
+ }
1948
+ function generateNodeChainDAG(writer, triggerId, context) {
1949
+ const { plan, nodeMap } = context;
1950
+ const allNodeIds = plan.sortedNodeIds.filter((id) => id !== triggerId);
1951
+ const outputNodeIds = [];
1952
+ const dagNodeIds = [];
1953
+ for (const id of allNodeIds) {
1954
+ if (context.childBlockNodeIds.has(id)) continue;
1955
+ const node = nodeMap.get(id);
1956
+ if (!node) continue;
1957
+ if (node.category === "output" /* OUTPUT */) {
1958
+ outputNodeIds.push(id);
1959
+ } else {
1960
+ dagNodeIds.push(id);
1961
+ }
1962
+ }
1963
+ if (dagNodeIds.length > 0) {
1964
+ writer.writeLine("// --- DAG Concurrent Execution ---");
1965
+ writer.blankLine();
1966
+ }
1967
+ for (const nodeId of dagNodeIds) {
1968
+ const node = nodeMap.get(nodeId);
1969
+ const rawDeps = [...plan.dependencies.get(nodeId) ?? []];
1970
+ const resolvedDeps = /* @__PURE__ */ new Set();
1971
+ for (const depId of rawDeps) {
1972
+ for (const dagDep of resolveToDAGNodes(depId, triggerId, context)) {
1973
+ resolvedDeps.add(dagDep);
1974
+ }
1975
+ }
1976
+ resolvedDeps.delete(nodeId);
1977
+ const promiseVar = `p_${sanitizeId(nodeId)}`;
1978
+ writer.write(`const ${promiseVar} = (async () => `).block(() => {
1979
+ for (const depId of resolvedDeps) {
1980
+ writer.writeLine(`await p_${sanitizeId(depId)};`);
1981
+ }
1982
+ writer.writeLine(`// --- ${node.label} (${node.nodeType}) [${node.id}] ---`);
1983
+ generateNodeBody(writer, node, context);
1984
+ });
1985
+ writer.writeLine(`)();`);
1986
+ writer.writeLine(`${promiseVar}.catch(() => {});`);
1987
+ writer.blankLine();
1988
+ }
1989
+ if (dagNodeIds.length > 0) {
1990
+ writer.writeLine("// --- Sync Barrier: await all DAG promises before output ---");
1991
+ const allPromiseVars = dagNodeIds.map((id) => `p_${sanitizeId(id)}`);
1992
+ writer.writeLine(`await Promise.allSettled([${allPromiseVars.join(", ")}]);`);
1993
+ writer.blankLine();
1994
+ }
1995
+ for (const nodeId of outputNodeIds) {
1996
+ const node = nodeMap.get(nodeId);
1997
+ const rawDeps = [...plan.dependencies.get(nodeId) ?? []];
1998
+ const resolvedDeps = /* @__PURE__ */ new Set();
1999
+ for (const depId of rawDeps) {
2000
+ for (const dagDep of resolveToDAGNodes(depId, triggerId, context)) {
2001
+ resolvedDeps.add(dagDep);
2002
+ }
2003
+ }
2004
+ for (const depId of resolvedDeps) {
2005
+ writer.writeLine(`await p_${sanitizeId(depId)};`);
2006
+ }
2007
+ writer.writeLine(`// --- ${node.label} (${node.nodeType}) [${node.id}] ---`);
2008
+ generateNodeBody(writer, node, context);
2009
+ }
2010
+ }
2011
+ function generateConcurrentNodes(writer, nodeIds, context) {
2012
+ const { nodeMap } = context;
2013
+ const activeNodeIds = nodeIds.filter((id) => !context.childBlockNodeIds.has(id));
2014
+ if (activeNodeIds.length === 0) return;
2015
+ if (activeNodeIds.length === 1) {
2016
+ const node = nodeMap.get(activeNodeIds[0]);
2017
+ if (node) {
2018
+ generateSingleNode(writer, node, context);
2019
+ writer.blankLine();
2020
+ }
2021
+ return;
2022
+ }
2023
+ writer.writeLine("// --- Concurrent Execution ---");
2024
+ const taskNames = [];
2025
+ for (const nodeId of activeNodeIds) {
2026
+ const node = nodeMap.get(nodeId);
2027
+ if (!node) continue;
2028
+ const taskName = `task_${sanitizeId(nodeId)}`;
2029
+ taskNames.push(taskName);
2030
+ writer.write(`const ${taskName} = async () => `).block(() => {
2031
+ generateNodeBody(writer, node, context);
2032
+ });
2033
+ writer.writeLine(";");
2034
+ }
2035
+ writer.writeLine(
2036
+ `const [${taskNames.map((_, i) => `r${i}`).join(", ")}] = await Promise.all([${taskNames.map((t) => `${t}()`).join(", ")}]);`
2037
+ );
2038
+ activeNodeIds.forEach((nodeId, i) => {
2039
+ writer.writeLine(`flowState['${nodeId}'] = r${i};`);
2040
+ });
2041
+ activeNodeIds.forEach((nodeId) => {
2042
+ const varName = context.symbolTable.getVarName(nodeId);
2043
+ writer.writeLine(`const ${varName} = flowState['${nodeId}'];`);
2044
+ });
2045
+ writer.blankLine();
2046
+ }
2047
+ function generateSingleNode(writer, node, context) {
2048
+ writer.writeLine(`// --- ${node.label} (${node.nodeType}) [${node.id}] ---`);
2049
+ generateNodeBody(writer, node, context);
2050
+ if (node.category !== "output" /* OUTPUT */) {
2051
+ const varName = context.symbolTable.getVarName(node.id);
2052
+ writer.writeLine(`const ${varName} = flowState['${node.id}'];`);
2053
+ }
2054
+ }
2055
+ function generateNodeBody(writer, node, context) {
2056
+ const plugin = context.pluginRegistry.get(node.nodeType);
2057
+ if (plugin) {
2058
+ const pluginCtx = createPluginContext(context);
2059
+ plugin.generate(node, writer, pluginCtx);
2060
+ } else {
2061
+ throw new Error(
2062
+ `[flow2code] Unsupported node type: "${node.nodeType}". Register a plugin via pluginRegistry.register() or use a built-in node type.`
2063
+ );
2064
+ }
2065
+ }
2066
+ function createPluginContext(context) {
2067
+ return {
2068
+ ir: context.ir,
2069
+ nodeMap: context.nodeMap,
2070
+ envVars: context.envVars,
2071
+ imports: context.imports,
2072
+ requiredPackages: context.requiredPackages,
2073
+ getVarName(nodeId) {
2074
+ return context.symbolTable.getVarName(nodeId);
2075
+ },
2076
+ resolveExpression(expr, currentNodeId) {
2077
+ const exprContext = {
2078
+ ir: context.ir,
2079
+ nodeMap: context.nodeMap,
2080
+ symbolTable: context.symbolTable,
2081
+ scopeStack: context.scopeStack.length > 0 ? [...context.scopeStack] : void 0,
2082
+ // Merge child block + DAG exclusion list
2083
+ blockScopedNodeIds: context.symbolTableExclusions.size > 0 ? context.symbolTableExclusions : void 0,
2084
+ currentNodeId
2085
+ };
2086
+ return parseExpression(expr, exprContext);
2087
+ },
2088
+ resolveEnvVars(url) {
2089
+ return resolveEnvVars(url, context);
2090
+ },
2091
+ generateChildNode(writer, node) {
2092
+ context.childBlockNodeIds.add(node.id);
2093
+ context.symbolTableExclusions.add(node.id);
2094
+ context.generatedBlockNodeIds.add(node.id);
2095
+ writer.writeLine(`// --- ${node.label} (${node.nodeType}) [${node.id}] ---`);
2096
+ generateNodeBody(writer, node, context);
2097
+ generateBlockContinuation(writer, node.id, context);
2098
+ },
2099
+ pushScope(nodeId, scopeVar) {
2100
+ context.scopeStack.push({ nodeId, scopeVar });
2101
+ },
2102
+ popScope() {
2103
+ context.scopeStack.pop();
2104
+ },
2105
+ __platformResponse: context.platform.generateResponse.bind(context.platform)
2106
+ };
2107
+ }
2108
+ function sanitizeId(id) {
2109
+ return id.replace(/[^a-zA-Z0-9_]/g, "_");
2110
+ }
2111
+ function resolveEnvVars(url, context) {
2112
+ const hasEnvVar = /\$\{(\w+)\}/.test(url);
2113
+ if (hasEnvVar) {
2114
+ return "`" + url.replace(/\$\{(\w+)\}/g, (_match, varName) => {
2115
+ context.envVars.add(varName);
2116
+ return "${process.env." + varName + "}";
2117
+ }) + "`";
2118
+ }
2119
+ if (url.includes("${")) {
2120
+ return "`" + url + "`";
2121
+ }
2122
+ return `"${url}"`;
2123
+ }
2124
+ function collectRequiredPackages(ir, context) {
2125
+ for (const node of ir.nodes) {
2126
+ const plugin = context.pluginRegistry.get(node.nodeType);
2127
+ if (plugin?.getRequiredPackages) {
2128
+ const packages = plugin.getRequiredPackages(node);
2129
+ packages.forEach((pkg) => context.requiredPackages.add(pkg));
2130
+ }
2131
+ }
2132
+ const platformDeps = context.platform.getImplicitDependencies();
2133
+ platformDeps.forEach((pkg) => context.requiredPackages.add(pkg));
2134
+ }
2135
+ function buildSourceMap(code, ir, filePath) {
2136
+ const lines = code.split("\n");
2137
+ const mappings = {};
2138
+ const nodeMarkerRegex = /^[\s]*\/\/ --- .+? \(.+?\) \[(.+?)\] ---$/;
2139
+ let currentNodeId = null;
2140
+ let currentStartLine = 0;
2141
+ for (let i = 0; i < lines.length; i++) {
2142
+ const lineNum = i + 1;
2143
+ const match = lines[i].match(nodeMarkerRegex);
2144
+ if (match) {
2145
+ if (currentNodeId) {
2146
+ mappings[currentNodeId] = {
2147
+ startLine: currentStartLine,
2148
+ endLine: lineNum - 1
2149
+ };
2150
+ }
2151
+ const [, nodeId] = match;
2152
+ if (ir.nodes.some((n) => n.id === nodeId)) {
2153
+ currentNodeId = nodeId;
2154
+ currentStartLine = lineNum;
2155
+ } else {
2156
+ currentNodeId = null;
2157
+ }
2158
+ }
2159
+ }
2160
+ if (currentNodeId) {
2161
+ mappings[currentNodeId] = {
2162
+ startLine: currentStartLine,
2163
+ endLine: lines.length
2164
+ };
2165
+ }
2166
+ const trigger = ir.nodes.find((n) => n.category === "trigger" /* TRIGGER */);
2167
+ if (trigger && !mappings[trigger.id]) {
2168
+ for (let i = 0; i < lines.length; i++) {
2169
+ if (lines[i].includes("export async function") || lines[i].includes("@schedule")) {
2170
+ mappings[trigger.id] = { startLine: i + 1, endLine: lines.length };
2171
+ break;
2172
+ }
2173
+ }
2174
+ }
2175
+ return {
2176
+ version: 1,
2177
+ generatedFile: filePath,
2178
+ mappings
2179
+ };
2180
+ }
2181
+
2182
+ // src/lib/ir/security.ts
2183
+ var SECURITY_PATTERNS = [
2184
+ // ── Critical: Remote Code Execution / System Access ──
2185
+ { pattern: /\beval\s*\(/, desc: "eval() \u2014 dynamically executes arbitrary code", severity: "critical" },
2186
+ { pattern: /\bnew\s+Function\s*\(/, desc: "new Function() \u2014 dynamically constructs a function", severity: "critical" },
2187
+ { pattern: /\bchild_process\b/, desc: "child_process \u2014 can execute arbitrary system commands", severity: "critical" },
2188
+ { pattern: /\bexec\s*\(/, desc: "exec() \u2014 executes shell commands", severity: "critical" },
2189
+ { pattern: /\bexecSync\s*\(/, desc: "execSync() \u2014 synchronously executes shell commands", severity: "critical" },
2190
+ { pattern: /\bspawn\s*\(/, desc: "spawn() \u2014 spawns a child process", severity: "critical" },
2191
+ { pattern: /\bprocess\.exit\b/, desc: "process.exit() \u2014 terminates the Node.js process", severity: "critical" },
2192
+ { pattern: /\bprocess\.env\b/, desc: "process.env \u2014 accesses environment variables (may leak secrets)", severity: "critical" },
2193
+ { pattern: /\bprocess\.kill\b/, desc: "process.kill() \u2014 kills a process", severity: "critical" },
2194
+ { pattern: /\brequire\s*\(\s*['"`]child_process/, desc: "require('child_process')", severity: "critical" },
2195
+ { pattern: /\brequire\s*\(\s*['"`]vm['"`]/, desc: "require('vm') \u2014 V8 virtual machine", severity: "critical" },
2196
+ // ── Critical: File System Destructive ──
2197
+ { pattern: /\bfs\.\w*(unlink|rmdir|rm|rmSync|unlinkSync)\b/, desc: "fs delete operation", severity: "critical" },
2198
+ { pattern: /\bfs\.\w*(writeFile|writeFileSync|appendFile)\b/, desc: "fs write operation", severity: "critical" },
2199
+ { pattern: /\brequire\s*\(\s*['"`]fs['"`]\)/, desc: "require('fs') \u2014 file system access", severity: "critical" },
2200
+ // ── Warning: Network / Dynamic Import ──
2201
+ { pattern: /\bimport\s*\(/, desc: "dynamic import() \u2014 can load arbitrary modules", severity: "warning" },
2202
+ { pattern: /\brequire\s*\(\s*['"`]https?['"`]/, desc: "require('http/https') \u2014 network module", severity: "warning" },
2203
+ { pattern: /\brequire\s*\(\s*['"`]net['"`]/, desc: "require('net') \u2014 low-level network access", severity: "warning" },
2204
+ { pattern: /\bglobalThis\b/, desc: "globalThis \u2014 accesses the global scope", severity: "warning" },
2205
+ { pattern: /\b__proto__\b/, desc: "__proto__ \u2014 prototype pollution risk", severity: "warning" },
2206
+ { pattern: /\bconstructor\s*\[\s*['"`]/, desc: "constructor[] \u2014 prototype pollution risk", severity: "warning" },
2207
+ // ── Warning: FS Read (non-destructive but sensitive) ──
2208
+ { pattern: /\brequire\s*\(\s*['"`]fs['"`]\s*\)\.read/, desc: "fs read operation", severity: "warning" },
2209
+ { pattern: /\bfs\.\w*(readFile|readFileSync|readdir)\b/, desc: "fs read operation", severity: "warning" },
2210
+ // ── Info: Uncommon patterns ──
2211
+ { pattern: /\bsetTimeout\s*\(\s*[^,]+,\s*\d{5,}/, desc: "long setTimeout (>10s) \u2014 possibly a malicious delay", severity: "info" },
2212
+ { pattern: /\bwhile\s*\(\s*true\s*\)/, desc: "while(true) \u2014 possible infinite loop", severity: "info" },
2213
+ { pattern: /\bfor\s*\(\s*;\s*;\s*\)/, desc: "for(;;) \u2014 possible infinite loop", severity: "info" }
2214
+ ];
2215
+ function truncate(str, maxLen) {
2216
+ return str.length > maxLen ? str.slice(0, maxLen) + "\u2026" : str;
2217
+ }
2218
+ function scanCode(nodeId, nodeLabel, code) {
2219
+ const findings = [];
2220
+ for (const { pattern, desc, severity } of SECURITY_PATTERNS) {
2221
+ const globalPattern = new RegExp(pattern.source, "g");
2222
+ let match;
2223
+ while ((match = globalPattern.exec(code)) !== null) {
2224
+ findings.push({
2225
+ severity,
2226
+ nodeId,
2227
+ nodeLabel,
2228
+ pattern: desc,
2229
+ match: truncate(match[0], 80)
2230
+ });
2231
+ }
2232
+ }
2233
+ return findings;
2234
+ }
2235
+ function extractCodeFields(node) {
2236
+ const codes = [];
2237
+ const params = node.params;
2238
+ if (!params) return codes;
2239
+ if (node.nodeType === "custom_code" /* CUSTOM_CODE */ && typeof params.code === "string") {
2240
+ codes.push(params.code);
2241
+ }
2242
+ if (typeof params.expression === "string") {
2243
+ codes.push(params.expression);
2244
+ }
2245
+ if (typeof params.inputMapping === "string") {
2246
+ codes.push(params.inputMapping);
2247
+ } else if (typeof params.inputMapping === "object" && params.inputMapping !== null) {
2248
+ codes.push(JSON.stringify(params.inputMapping));
2249
+ }
2250
+ if (typeof params.body === "string") {
2251
+ codes.push(params.body);
2252
+ }
2253
+ if (typeof params.query === "string") {
2254
+ codes.push(params.query);
2255
+ }
2256
+ if (typeof params.bodyExpression === "string") {
2257
+ codes.push(params.bodyExpression);
2258
+ }
2259
+ if (typeof params.condition === "string") {
2260
+ codes.push(params.condition);
2261
+ }
2262
+ return codes;
2263
+ }
2264
+ function validateIRSecurity(ir) {
2265
+ const findings = [];
2266
+ let nodesScanned = 0;
2267
+ for (const node of ir.nodes) {
2268
+ const codeFields = extractCodeFields(node);
2269
+ if (codeFields.length === 0) continue;
2270
+ nodesScanned++;
2271
+ for (const code of codeFields) {
2272
+ const nodeFindings = scanCode(node.id, node.label ?? node.id, code);
2273
+ findings.push(...nodeFindings);
2274
+ }
2275
+ }
2276
+ const hasCritical = findings.some((f) => f.severity === "critical");
2277
+ return {
2278
+ safe: !hasCritical,
2279
+ findings,
2280
+ nodesScanned
2281
+ };
2282
+ }
2283
+ function formatSecurityReport(result) {
2284
+ if (result.findings.length === 0) {
2285
+ return `\u2705 Security check passed (scanned ${result.nodesScanned} nodes, no dangerous patterns detected)`;
2286
+ }
2287
+ const lines = [];
2288
+ const critical = result.findings.filter((f) => f.severity === "critical");
2289
+ const warnings = result.findings.filter((f) => f.severity === "warning");
2290
+ const infos = result.findings.filter((f) => f.severity === "info");
2291
+ lines.push(`\u26A0\uFE0F Security check results (scanned ${result.nodesScanned} nodes)`);
2292
+ lines.push("");
2293
+ if (critical.length > 0) {
2294
+ lines.push(`\u{1F534} Critical (${critical.length}):`);
2295
+ for (const f of critical) {
2296
+ lines.push(` [${f.nodeId}] ${f.pattern} \u2014 match: "${f.match}"`);
2297
+ }
2298
+ }
2299
+ if (warnings.length > 0) {
2300
+ lines.push(`\u{1F7E1} Warning (${warnings.length}):`);
2301
+ for (const f of warnings) {
2302
+ lines.push(` [${f.nodeId}] ${f.pattern} \u2014 match: "${f.match}"`);
2303
+ }
2304
+ }
2305
+ if (infos.length > 0) {
2306
+ lines.push(`\u{1F535} Info (${infos.length}):`);
2307
+ for (const f of infos) {
2308
+ lines.push(` [${f.nodeId}] ${f.pattern} \u2014 match: "${f.match}"`);
2309
+ }
2310
+ }
2311
+ return lines.join("\n");
2312
+ }
2313
+
2314
+ // src/lib/openapi/converter.ts
2315
+ function convertOpenAPIToFlowIR(jsonInput) {
2316
+ idCounter = 0;
2317
+ const errors = [];
2318
+ let spec;
2319
+ try {
2320
+ spec = typeof jsonInput === "string" ? JSON.parse(jsonInput) : jsonInput;
2321
+ } catch (e) {
2322
+ return {
2323
+ success: false,
2324
+ flows: [],
2325
+ errors: [`JSON parse failed: ${e instanceof Error ? e.message : String(e)}`],
2326
+ summary: { totalPaths: 0, totalOperations: 0, tags: [] }
2327
+ };
2328
+ }
2329
+ if (!spec.openapi || !spec.paths) {
2330
+ return {
2331
+ success: false,
2332
+ flows: [],
2333
+ errors: ["Not a valid OpenAPI spec: missing openapi or paths field"],
2334
+ summary: { totalPaths: 0, totalOperations: 0, tags: [] }
2335
+ };
2336
+ }
2337
+ const flows = [];
2338
+ const allTags = /* @__PURE__ */ new Set();
2339
+ let totalOps = 0;
2340
+ const methods = ["get", "post", "put", "patch", "delete"];
2341
+ for (const [path, pathItem] of Object.entries(spec.paths)) {
2342
+ for (const method of methods) {
2343
+ const operation = pathItem[method];
2344
+ if (!operation) continue;
2345
+ totalOps++;
2346
+ operation.tags?.forEach((t) => allTags.add(t));
2347
+ try {
2348
+ const flow = convertSingleOperation(
2349
+ path,
2350
+ method.toUpperCase(),
2351
+ operation,
2352
+ spec
2353
+ );
2354
+ flows.push(flow);
2355
+ } catch (e) {
2356
+ errors.push(
2357
+ `${method.toUpperCase()} ${path}: ${e instanceof Error ? e.message : String(e)}`
2358
+ );
2359
+ }
2360
+ }
2361
+ }
2362
+ return {
2363
+ success: errors.length === 0,
2364
+ flows,
2365
+ errors,
2366
+ summary: {
2367
+ totalPaths: Object.keys(spec.paths).length,
2368
+ totalOperations: totalOps,
2369
+ tags: [...allTags].sort()
2370
+ }
2371
+ };
2372
+ }
2373
+ var idCounter = 0;
2374
+ function nextId(prefix) {
2375
+ return `${prefix}_${++idCounter}`;
2376
+ }
2377
+ function convertSingleOperation(path, method, operation, _spec) {
2378
+ const nodes = [];
2379
+ const edges = [];
2380
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2381
+ const isBodyMethod = ["POST", "PUT", "PATCH"].includes(method);
2382
+ const hasRequestBody = isBodyMethod && !!operation.requestBody;
2383
+ const hasQueryParams = operation.parameters?.some((p) => p.in === "query");
2384
+ const hasPathParams = operation.parameters?.some((p) => p.in === "path");
2385
+ const triggerId = nextId("trigger");
2386
+ const queryParamsDef = operation.parameters?.filter((p) => p.in === "query").map((p) => ({
2387
+ name: p.name,
2388
+ type: mapOpenAPIType(p.schema?.type),
2389
+ required: p.required ?? false
2390
+ }));
2391
+ const triggerNode = {
2392
+ id: triggerId,
2393
+ nodeType: "http_webhook" /* HTTP_WEBHOOK */,
2394
+ category: "trigger" /* TRIGGER */,
2395
+ label: `${method} ${path}`,
2396
+ params: {
2397
+ method,
2398
+ routePath: convertPathToNextJS(path),
2399
+ parseBody: hasRequestBody,
2400
+ ...queryParamsDef && queryParamsDef.length > 0 ? { queryParams: queryParamsDef } : {}
2401
+ },
2402
+ inputs: [],
2403
+ outputs: buildTriggerOutputs(method, hasRequestBody, hasQueryParams)
2404
+ };
2405
+ nodes.push(triggerNode);
2406
+ let prevNodeId = triggerId;
2407
+ if (hasPathParams) {
2408
+ const transformId = nextId("transform");
2409
+ const pathParams = operation.parameters.filter((p) => p.in === "path");
2410
+ const transformNode = {
2411
+ id: transformId,
2412
+ nodeType: "transform",
2413
+ category: "variable" /* VARIABLE */,
2414
+ label: "Extract Path Params",
2415
+ params: {
2416
+ expression: `{ ${pathParams.map((p) => `${p.name}: {{$trigger}}.query.${p.name}`).join(", ")} }`
2417
+ },
2418
+ inputs: [
2419
+ { id: "input", label: "Input", dataType: "any", required: true }
2420
+ ],
2421
+ outputs: [{ id: "result", label: "Result", dataType: "object" }]
2422
+ };
2423
+ nodes.push(transformNode);
2424
+ edges.push({
2425
+ id: nextId("edge"),
2426
+ sourceNodeId: prevNodeId,
2427
+ sourcePortId: "request",
2428
+ targetNodeId: transformId,
2429
+ targetPortId: "input"
2430
+ });
2431
+ prevNodeId = transformId;
2432
+ }
2433
+ const responseId = nextId("response");
2434
+ const successStatus = getSuccessStatus(operation);
2435
+ const responseNode = {
2436
+ id: responseId,
2437
+ nodeType: "return_response" /* RETURN_RESPONSE */,
2438
+ category: "output" /* OUTPUT */,
2439
+ label: operation.summary || `Return ${successStatus}`,
2440
+ params: {
2441
+ statusCode: successStatus,
2442
+ bodyExpression: "{{$input}}",
2443
+ headers: { "Content-Type": "application/json" }
2444
+ },
2445
+ inputs: [
2446
+ { id: "data", label: "Data", dataType: "any", required: true }
2447
+ ],
2448
+ outputs: []
2449
+ };
2450
+ nodes.push(responseNode);
2451
+ edges.push({
2452
+ id: nextId("edge"),
2453
+ sourceNodeId: prevNodeId,
2454
+ sourcePortId: prevNodeId === triggerId ? "request" : "result",
2455
+ targetNodeId: responseId,
2456
+ targetPortId: "data"
2457
+ });
2458
+ return {
2459
+ version: "1.0.0",
2460
+ meta: {
2461
+ name: operation.operationId || `${method} ${path}`,
2462
+ description: operation.description || operation.summary || "",
2463
+ createdAt: now,
2464
+ updatedAt: now
2465
+ },
2466
+ nodes,
2467
+ edges
2468
+ };
2469
+ }
2470
+ function buildTriggerOutputs(method, hasBody, hasQuery) {
2471
+ const outputs = [
2472
+ { id: "request", label: "Request", dataType: "object" }
2473
+ ];
2474
+ if (hasBody) {
2475
+ outputs.push({ id: "body", label: "Body", dataType: "object" });
2476
+ }
2477
+ if (hasQuery) {
2478
+ outputs.push({ id: "query", label: "Query", dataType: "object" });
2479
+ }
2480
+ return outputs;
2481
+ }
2482
+ function getSuccessStatus(operation) {
2483
+ if (!operation.responses) return 200;
2484
+ const statusCodes = Object.keys(operation.responses);
2485
+ const success = statusCodes.find((s) => s.startsWith("2"));
2486
+ return success ? parseInt(success, 10) : 200;
2487
+ }
2488
+ function convertPathToNextJS(path) {
2489
+ const apiPath = path.startsWith("/api") ? path : `/api${path}`;
2490
+ return apiPath.replace(/\{(\w+)\}/g, "[$1]");
2491
+ }
2492
+ function mapOpenAPIType(type) {
2493
+ switch (type) {
2494
+ case "integer":
2495
+ case "number":
2496
+ return "number";
2497
+ case "boolean":
2498
+ return "boolean";
2499
+ case "array":
2500
+ return "array";
2501
+ case "object":
2502
+ return "object";
2503
+ case "string":
2504
+ default:
2505
+ return "string";
2506
+ }
2507
+ }
2508
+
2509
+ // src/lib/ai/prompt.ts
2510
+ var FLOW_IR_SYSTEM_PROMPT = `You are Flow2Code AI, a specialist in generating FlowIR JSON for a visual API builder.
2511
+
2512
+ ## Your Task
2513
+ Given a user's natural language description of an API endpoint or workflow, generate a valid FlowIR JSON object.
2514
+
2515
+ ## FlowIR Schema
2516
+
2517
+ ### Top-level structure:
2518
+ \`\`\`json
2519
+ {
2520
+ "version": "1.0.0",
2521
+ "meta": {
2522
+ "name": "string",
2523
+ "description": "string (optional)",
2524
+ "createdAt": "ISO 8601 date string",
2525
+ "updatedAt": "ISO 8601 date string"
2526
+ },
2527
+ "nodes": [ ... ],
2528
+ "edges": [ ... ]
2529
+ }
2530
+ \`\`\`
2531
+
2532
+ ### Node structure:
2533
+ \`\`\`json
2534
+ {
2535
+ "id": "unique_string (e.g. trigger_1, fetch_1, response_1)",
2536
+ "nodeType": "one of the NodeType enums below",
2537
+ "category": "trigger | action | logic | variable | output",
2538
+ "label": "human readable label",
2539
+ "params": { ... type-specific params ... },
2540
+ "inputs": [{ "id": "string", "label": "string", "dataType": "string|number|boolean|object|array|any|void|Response", "required": boolean }],
2541
+ "outputs": [{ "id": "string", "label": "string", "dataType": "string|number|boolean|object|array|any|void|Response" }]
2542
+ }
2543
+ \`\`\`
2544
+
2545
+ ### Edge structure:
2546
+ \`\`\`json
2547
+ {
2548
+ "id": "unique_string (e.g. e1, e2)",
2549
+ "sourceNodeId": "node id",
2550
+ "sourcePortId": "output port id",
2551
+ "targetNodeId": "node id",
2552
+ "targetPortId": "input port id"
2553
+ }
2554
+ \`\`\`
2555
+
2556
+ ### Available Node Types:
2557
+
2558
+ #### Triggers (category: "trigger") \u2014 exactly ONE required per flow:
2559
+ 1. **http_webhook** \u2014 params: { method: "GET"|"POST"|"PUT"|"PATCH"|"DELETE", routePath: "/api/...", parseBody: boolean }
2560
+ - outputs: [{ id: "request", label: "Request", dataType: "object" }, { id: "body", label: "Body", dataType: "object" }, { id: "query", label: "Query", dataType: "object" }]
2561
+ - inputs: none
2562
+
2563
+ 2. **cron_job** \u2014 params: { schedule: "cron expression", functionName: "string" }
2564
+ - outputs: [{ id: "output", label: "Output", dataType: "any" }]
2565
+
2566
+ 3. **manual** \u2014 params: { functionName: "string", args: [{ name: "string", type: "FlowDataType" }] }
2567
+ - outputs: [{ id: "output", label: "Output", dataType: "any" }]
2568
+
2569
+ #### Actions (category: "action"):
2570
+ 4. **fetch_api** \u2014 params: { url: "string (supports \${ENV_VAR})", method: "GET"|"POST"|"PUT"|"PATCH"|"DELETE", headers?: {}, body?: "string", parseJson: boolean }
2571
+ - inputs: [{ id: "input", label: "Input", dataType: "any", required: false }]
2572
+ - outputs: [{ id: "response", label: "Response", dataType: "object" }, { id: "data", label: "Data", dataType: "any" }]
2573
+
2574
+ 5. **sql_query** \u2014 params: { orm: "drizzle"|"prisma"|"raw", query: "SQL string", params?: [] }
2575
+ - inputs: [{ id: "input", label: "Input", dataType: "any", required: false }]
2576
+ - outputs: [{ id: "result", label: "Result", dataType: "array" }]
2577
+
2578
+ 6. **redis_cache** \u2014 params: { operation: "get"|"set"|"del", key: "string", value?: "string", ttl?: number }
2579
+ - inputs: [{ id: "input", label: "Input", dataType: "any", required: false }]
2580
+ - outputs: [{ id: "value", label: "Value", dataType: "any" }]
2581
+
2582
+ 7. **custom_code** \u2014 params: { code: "TypeScript code", returnVariable?: "string" }
2583
+ - inputs: [{ id: "input", label: "Input", dataType: "any", required: false }]
2584
+ - outputs: [{ id: "result", label: "Result", dataType: "any" }]
2585
+
2586
+ #### Logic (category: "logic"):
2587
+ 8. **if_else** \u2014 params: { condition: "TypeScript expression" }
2588
+ - inputs: [{ id: "input", label: "Input", dataType: "any", required: true }]
2589
+ - outputs: [{ id: "true", label: "True", dataType: "any" }, { id: "false", label: "False", dataType: "any" }]
2590
+
2591
+ 9. **for_loop** \u2014 params: { iterableExpression: "string", itemVariable: "string", indexVariable?: "string" }
2592
+ - inputs: [{ id: "iterable", label: "Iterable", dataType: "array", required: true }]
2593
+ - outputs: [{ id: "item", label: "Item", dataType: "any" }, { id: "result", label: "Result", dataType: "array" }]
2594
+
2595
+ 10. **try_catch** \u2014 params: { errorVariable: "string" }
2596
+ - inputs: [{ id: "input", label: "Input", dataType: "any", required: true }]
2597
+ - outputs: [{ id: "success", label: "Success", dataType: "any" }, { id: "error", label: "Error", dataType: "object" }]
2598
+
2599
+ 11. **promise_all** \u2014 params: {}
2600
+ - inputs: [{ id: "task1", label: "Task 1", dataType: "any", required: true }, { id: "task2", label: "Task 2", dataType: "any", required: true }]
2601
+ - outputs: [{ id: "results", label: "Results", dataType: "array" }]
2602
+
2603
+ #### Variables (category: "variable"):
2604
+ 12. **declare** \u2014 params: { name: "string", dataType: "FlowDataType", initialValue?: "expression", isConst: boolean }
2605
+ - outputs: [{ id: "value", label: "Value", dataType: "any" }]
2606
+
2607
+ 13. **transform** \u2014 params: { expression: "TypeScript expression" }
2608
+ - inputs: [{ id: "input", label: "Input", dataType: "any", required: true }]
2609
+ - outputs: [{ id: "output", label: "Output", dataType: "any" }]
2610
+
2611
+ #### Output (category: "output"):
2612
+ 14. **return_response** \u2014 params: { statusCode: number, bodyExpression: "JS expression string", headers?: {} }
2613
+ - inputs: [{ id: "data", label: "Data", dataType: "any", required: true }]
2614
+
2615
+ ## Variable Reference System
2616
+ - Nodes access previous node's output via: flowState['nodeId']
2617
+ - In params like condition or bodyExpression, use: flowState['nodeId'] directly
2618
+ - For environment variables in URLs, use: \${ENV_VAR_NAME}
2619
+
2620
+ ## Rules
2621
+ 1. There must be EXACTLY ONE trigger node
2622
+ 2. All node IDs must be unique
2623
+ 3. Edges must reference valid node IDs and port IDs
2624
+ 4. No cycles allowed in the graph
2625
+ 5. STRICT RULE: Every non-trigger node MUST be connected via an edge. For parallel execution branches, you MUST create separate edges connecting the trigger's output to EACH parallel node's input. Do NOT leave any node orphaned!
2626
+ 6. Use descriptive labels for nodes
2627
+ 7. Generate sensible default values
2628
+ 8. For HTTP APIs that receive data, use parseBody: true with POST/PUT/PATCH methods
2629
+ 9. Always end HTTP flows with a return_response node
2630
+ 10. Use meaningful nodeId naming like "trigger_1", "fetch_users", "check_auth", "response_ok"
2631
+
2632
+ ## Output
2633
+ Return ONLY valid JSON (no markdown, no explanation). The JSON must conform to the FlowIR schema above.
2634
+ `;
2635
+
2636
+ // src/server/handlers.ts
2637
+ import { writeFileSync, mkdirSync, existsSync, readFileSync } from "fs";
2638
+ import { join, dirname, resolve } from "path";
2639
+ function handleCompile(body, projectRoot) {
2640
+ try {
2641
+ const ir = body.ir;
2642
+ const shouldWrite = body.write !== false;
2643
+ if (!ir) {
2644
+ return { status: 400, body: { success: false, error: "Missing 'ir' in request body" } };
2645
+ }
2646
+ const result = compile(ir);
2647
+ if (!result.success) {
2648
+ return {
2649
+ status: 400,
2650
+ body: { success: false, error: result.errors?.join("\n") }
2651
+ };
2652
+ }
2653
+ let writtenPath = null;
2654
+ if (shouldWrite && result.filePath && result.code) {
2655
+ const fullPath = resolve(join(projectRoot, result.filePath));
2656
+ if (!fullPath.startsWith(resolve(projectRoot))) {
2657
+ return {
2658
+ status: 400,
2659
+ body: { success: false, error: "Output path escapes project root" }
2660
+ };
2661
+ }
2662
+ const dir = dirname(fullPath);
2663
+ if (!existsSync(dir)) {
2664
+ mkdirSync(dir, { recursive: true });
2665
+ }
2666
+ writeFileSync(fullPath, result.code, "utf-8");
2667
+ writtenPath = fullPath;
2668
+ if (result.sourceMap) {
2669
+ const mapPath = fullPath.replace(/\.ts$/, ".flow.map.json");
2670
+ writeFileSync(mapPath, JSON.stringify(result.sourceMap, null, 2), "utf-8");
2671
+ }
2672
+ if (result.dependencies && result.dependencies.all.length > 0) {
2673
+ const pkgPath = join(projectRoot, "package.json");
2674
+ if (existsSync(pkgPath)) {
2675
+ try {
2676
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
2677
+ const installed = /* @__PURE__ */ new Set([
2678
+ ...Object.keys(pkg.dependencies ?? {}),
2679
+ ...Object.keys(pkg.devDependencies ?? {})
2680
+ ]);
2681
+ result.dependencies.missing = result.dependencies.all.filter(
2682
+ (d) => !installed.has(d)
2683
+ );
2684
+ } catch {
2685
+ }
2686
+ }
2687
+ }
2688
+ }
2689
+ return {
2690
+ status: 200,
2691
+ body: {
2692
+ success: true,
2693
+ code: result.code,
2694
+ filePath: result.filePath,
2695
+ writtenTo: writtenPath,
2696
+ dependencies: result.dependencies,
2697
+ sourceMap: result.sourceMap
2698
+ }
2699
+ };
2700
+ } catch (err) {
2701
+ return {
2702
+ status: 500,
2703
+ body: {
2704
+ success: false,
2705
+ error: `Server error: ${err instanceof Error ? err.message : String(err)}`
2706
+ }
2707
+ };
2708
+ }
2709
+ }
2710
+ async function handleGenerate(body) {
2711
+ try {
2712
+ const { prompt } = body;
2713
+ if (!prompt || typeof prompt !== "string") {
2714
+ return { status: 400, body: { success: false, error: "Missing required 'prompt' string" } };
2715
+ }
2716
+ const apiKey = process.env.OPENAI_API_KEY;
2717
+ if (!apiKey) {
2718
+ return { status: 500, body: { success: false, error: "OPENAI_API_KEY environment variable is not set" } };
2719
+ }
2720
+ const baseUrl = process.env.OPENAI_BASE_URL ?? "https://api.openai.com/v1";
2721
+ const model = process.env.OPENAI_MODEL ?? "gpt-4o-mini";
2722
+ const response = await fetch(`${baseUrl}/chat/completions`, {
2723
+ method: "POST",
2724
+ headers: {
2725
+ "Content-Type": "application/json",
2726
+ Authorization: `Bearer ${apiKey}`
2727
+ },
2728
+ body: JSON.stringify({
2729
+ model,
2730
+ messages: [
2731
+ { role: "system", content: FLOW_IR_SYSTEM_PROMPT },
2732
+ { role: "user", content: prompt }
2733
+ ],
2734
+ temperature: 0.2,
2735
+ response_format: { type: "json_object" }
2736
+ })
2737
+ });
2738
+ if (!response.ok) {
2739
+ const errText = await response.text();
2740
+ return { status: 502, body: { success: false, error: `LLM API error (${response.status}): ${errText}` } };
2741
+ }
2742
+ const data = await response.json();
2743
+ const content = data.choices?.[0]?.message?.content;
2744
+ if (!content) {
2745
+ return { status: 502, body: { success: false, error: "LLM returned empty content" } };
2746
+ }
2747
+ let jsonStr = content;
2748
+ const codeBlockMatch = content.match(/```(?:json)?\s*\n?([\s\S]*?)\n?\s*```/);
2749
+ if (codeBlockMatch) jsonStr = codeBlockMatch[1];
2750
+ let ir;
2751
+ try {
2752
+ ir = JSON.parse(jsonStr);
2753
+ } catch {
2754
+ return { status: 422, body: { success: false, error: "Failed to parse LLM JSON response", raw: content } };
2755
+ }
2756
+ const triggerNode = ir.nodes.find((n) => n.category === "trigger");
2757
+ if (triggerNode) {
2758
+ const triggerOutputPortId = triggerNode.outputs?.[0]?.id || "output";
2759
+ const connectedTargetNodeIds = new Set(ir.edges.map((e) => e.targetNodeId));
2760
+ const existingEdgeIds = new Set(ir.edges.map((e) => e.id));
2761
+ let healedCount = 0;
2762
+ ir.nodes.forEach((node) => {
2763
+ if (node.id !== triggerNode.id && !connectedTargetNodeIds.has(node.id) && node.inputs && node.inputs.length > 0) {
2764
+ let edgeId;
2765
+ do {
2766
+ edgeId = `healed_e_${crypto.randomUUID().slice(0, 8)}`;
2767
+ } while (existingEdgeIds.has(edgeId));
2768
+ existingEdgeIds.add(edgeId);
2769
+ ir.edges.push({
2770
+ id: edgeId,
2771
+ sourceNodeId: triggerNode.id,
2772
+ sourcePortId: triggerNode.nodeType === "http_webhook" ? "request" : triggerOutputPortId,
2773
+ targetNodeId: node.id,
2774
+ targetPortId: node.inputs[0].id
2775
+ });
2776
+ healedCount++;
2777
+ }
2778
+ });
2779
+ if (healedCount > 0) {
2780
+ console.warn(`[AutoHeal] Connected ${healedCount} orphaned nodes to trigger.`);
2781
+ }
2782
+ }
2783
+ const validation = validateFlowIR(ir);
2784
+ if (!validation.valid) {
2785
+ return {
2786
+ status: 422,
2787
+ body: { success: false, error: "LLM-generated IR failed validation", validationErrors: validation.errors, raw: ir }
2788
+ };
2789
+ }
2790
+ const security = validateIRSecurity(ir);
2791
+ const securityReport = security.findings.length > 0 ? formatSecurityReport(security) : void 0;
2792
+ return { status: 200, body: { success: true, ir, security: { safe: security.safe, findings: security.findings, report: securityReport } } };
2793
+ } catch (err) {
2794
+ return {
2795
+ status: 500,
2796
+ body: { success: false, error: `Server error: ${err instanceof Error ? err.message : String(err)}` }
2797
+ };
2798
+ }
2799
+ }
2800
+ function handleImportOpenAPI(body) {
2801
+ try {
2802
+ if (!body.spec) {
2803
+ return { status: 400, body: { error: "Missing 'spec' field in request body" } };
2804
+ }
2805
+ const result = convertOpenAPIToFlowIR(body.spec);
2806
+ let filteredFlows = result.flows;
2807
+ if (body.filter?.paths && Array.isArray(body.filter.paths)) {
2808
+ const paths = body.filter.paths;
2809
+ filteredFlows = filteredFlows.filter(
2810
+ (flow) => paths.some((p) => flow.meta.name.includes(p))
2811
+ );
2812
+ }
2813
+ return {
2814
+ status: 200,
2815
+ body: {
2816
+ success: result.success,
2817
+ flows: filteredFlows,
2818
+ summary: result.summary,
2819
+ errors: result.errors
2820
+ }
2821
+ };
2822
+ } catch (error) {
2823
+ return {
2824
+ status: 500,
2825
+ body: { error: error instanceof Error ? error.message : "Internal Server Error" }
2826
+ };
2827
+ }
2828
+ }
2829
+
2830
+ // src/server/index.ts
2831
+ var __filename = fileURLToPath(import.meta.url);
2832
+ var __dirname = dirname2(__filename);
2833
+ function resolveStaticDir() {
2834
+ const candidates = [
2835
+ join2(__dirname, "..", "out"),
2836
+ // dist/server.js → ../out (npm package structure)
2837
+ join2(__dirname, "out"),
2838
+ // dist/out/
2839
+ join2(__dirname, "..", "..", "out"),
2840
+ // src/server/index.ts → ../../out (dev)
2841
+ join2(process.cwd(), "out")
2842
+ // fallback: cwd/out
2843
+ ];
2844
+ for (const dir of candidates) {
2845
+ if (existsSync2(join2(dir, "index.html"))) {
2846
+ return dir;
2847
+ }
2848
+ }
2849
+ return candidates[0];
2850
+ }
2851
+ var MIME_TYPES = {
2852
+ ".html": "text/html; charset=utf-8",
2853
+ ".js": "application/javascript; charset=utf-8",
2854
+ ".css": "text/css; charset=utf-8",
2855
+ ".json": "application/json; charset=utf-8",
2856
+ ".png": "image/png",
2857
+ ".jpg": "image/jpeg",
2858
+ ".jpeg": "image/jpeg",
2859
+ ".gif": "image/gif",
2860
+ ".svg": "image/svg+xml",
2861
+ ".ico": "image/x-icon",
2862
+ ".woff": "font/woff",
2863
+ ".woff2": "font/woff2",
2864
+ ".ttf": "font/ttf",
2865
+ ".map": "application/json",
2866
+ ".txt": "text/plain; charset=utf-8",
2867
+ ".webp": "image/webp"
2868
+ };
2869
+ var isDev = process.env.NODE_ENV !== "production";
2870
+ function setCors(res) {
2871
+ const origin = isDev ? "*" : process.env.CORS_ORIGIN || "";
2872
+ if (!origin) return;
2873
+ res.setHeader("Access-Control-Allow-Origin", origin);
2874
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
2875
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
2876
+ }
2877
+ function setSecurityHeaders(res) {
2878
+ const csp = isDev ? [
2879
+ "default-src 'self'",
2880
+ "script-src 'self' 'unsafe-inline' 'unsafe-eval'",
2881
+ "style-src 'self' 'unsafe-inline'",
2882
+ "img-src 'self' data: blob:",
2883
+ "font-src 'self' data:",
2884
+ "connect-src 'self' *",
2885
+ "frame-ancestors 'self'",
2886
+ "form-action 'self'",
2887
+ "base-uri 'self'"
2888
+ ].join("; ") : [
2889
+ "default-src 'self'",
2890
+ "script-src 'self'",
2891
+ "style-src 'self' 'unsafe-inline'",
2892
+ "img-src 'self' data: blob:",
2893
+ "font-src 'self' data:",
2894
+ "connect-src 'self'",
2895
+ "frame-ancestors 'self'",
2896
+ "form-action 'self'",
2897
+ "base-uri 'self'"
2898
+ ].join("; ");
2899
+ res.setHeader("Content-Security-Policy", csp);
2900
+ res.setHeader("X-Content-Type-Options", "nosniff");
2901
+ res.setHeader("X-Frame-Options", "SAMEORIGIN");
2902
+ res.setHeader("X-XSS-Protection", "1; mode=block");
2903
+ res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
2904
+ }
2905
+ function sendJson(res, status, body) {
2906
+ res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
2907
+ res.end(JSON.stringify(body));
2908
+ }
2909
+ var MAX_BODY_SIZE = 2 * 1024 * 1024;
2910
+ async function readBody(req) {
2911
+ return new Promise((resolve2, reject) => {
2912
+ const chunks = [];
2913
+ let totalSize = 0;
2914
+ req.on("data", (chunk) => {
2915
+ totalSize += chunk.length;
2916
+ if (totalSize > MAX_BODY_SIZE) {
2917
+ req.destroy();
2918
+ reject(new Error(`Body too large (max ${MAX_BODY_SIZE / 1024 / 1024} MB)`));
2919
+ return;
2920
+ }
2921
+ chunks.push(chunk);
2922
+ });
2923
+ req.on("end", () => resolve2(Buffer.concat(chunks).toString("utf-8")));
2924
+ req.on("error", reject);
2925
+ });
2926
+ }
2927
+ async function parseJsonBody(req) {
2928
+ const raw = await readBody(req);
2929
+ return JSON.parse(raw);
2930
+ }
2931
+ async function serveStatic(staticDir, pathname, res) {
2932
+ let filePath = join2(staticDir, pathname === "/" ? "index.html" : pathname);
2933
+ if (!extname(filePath)) {
2934
+ filePath += ".html";
2935
+ }
2936
+ try {
2937
+ const s = await stat(filePath);
2938
+ if (!s.isFile()) return false;
2939
+ const ext = extname(filePath).toLowerCase();
2940
+ const contentType = MIME_TYPES[ext] || "application/octet-stream";
2941
+ const content = await readFile(filePath);
2942
+ res.writeHead(200, { "Content-Type": contentType });
2943
+ res.end(content);
2944
+ return true;
2945
+ } catch {
2946
+ return false;
2947
+ }
2948
+ }
2949
+ async function handleRequest(req, res, staticDir, projectRoot) {
2950
+ setCors(res);
2951
+ setSecurityHeaders(res);
2952
+ const { method } = req;
2953
+ const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
2954
+ const pathname = url.pathname;
2955
+ if (method === "OPTIONS") {
2956
+ res.writeHead(204);
2957
+ res.end();
2958
+ return;
2959
+ }
2960
+ if (pathname.startsWith("/api/")) {
2961
+ if (method !== "POST") {
2962
+ sendJson(res, 405, { error: "Method Not Allowed" });
2963
+ return;
2964
+ }
2965
+ let body;
2966
+ try {
2967
+ const contentType = req.headers["content-type"] || "";
2968
+ if (!contentType.includes("application/json")) {
2969
+ sendJson(res, 415, { error: "Content-Type must be application/json" });
2970
+ return;
2971
+ }
2972
+ body = await parseJsonBody(req);
2973
+ } catch (err) {
2974
+ const msg = err instanceof Error ? err.message : "Invalid JSON body";
2975
+ sendJson(res, 400, { error: msg });
2976
+ return;
2977
+ }
2978
+ if (pathname === "/api/compile") {
2979
+ const result = handleCompile(body, projectRoot);
2980
+ sendJson(res, result.status, result.body);
2981
+ return;
2982
+ }
2983
+ if (pathname === "/api/generate") {
2984
+ const result = await handleGenerate(body);
2985
+ sendJson(res, result.status, result.body);
2986
+ return;
2987
+ }
2988
+ if (pathname === "/api/import-openapi") {
2989
+ const result = handleImportOpenAPI(body);
2990
+ sendJson(res, result.status, result.body);
2991
+ return;
2992
+ }
2993
+ sendJson(res, 404, { error: `Unknown API route: ${pathname}` });
2994
+ return;
2995
+ }
2996
+ const served = await serveStatic(staticDir, pathname, res);
2997
+ if (served) return;
2998
+ const indexPath = join2(staticDir, "index.html");
2999
+ try {
3000
+ const content = await readFile(indexPath, "utf-8");
3001
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
3002
+ res.end(content);
3003
+ } catch {
3004
+ res.writeHead(404, { "Content-Type": "text/plain" });
3005
+ res.end("404 Not Found \u2014 UI has not been built yet, please run pnpm build:ui first");
3006
+ }
3007
+ }
3008
+ function startServer(options = {}) {
3009
+ const port = options.port ?? (Number(process.env.PORT) || 3100);
3010
+ const host = options.host ?? "0.0.0.0";
3011
+ const staticDir = options.staticDir ?? resolveStaticDir();
3012
+ const projectRoot = options.projectRoot ?? process.cwd();
3013
+ const server = createServer((req, res) => {
3014
+ handleRequest(req, res, staticDir, projectRoot).catch((err) => {
3015
+ console.error("[flow2code] Internal error:", err);
3016
+ res.writeHead(500, { "Content-Type": "text/plain" });
3017
+ res.end("Internal Server Error");
3018
+ });
3019
+ });
3020
+ server.listen(port, host, () => {
3021
+ const url = `http://localhost:${port}`;
3022
+ if (options.onReady) {
3023
+ options.onReady(url);
3024
+ } else {
3025
+ console.log(`
3026
+ \u{1F680} Flow2Code Dev Server`);
3027
+ console.log(` \u251C\u2500 Local: ${url}`);
3028
+ console.log(` \u251C\u2500 API: ${url}/api/compile`);
3029
+ console.log(` \u251C\u2500 Static: ${staticDir}`);
3030
+ console.log(` \u2514\u2500 Project: ${projectRoot}
3031
+ `);
3032
+ }
3033
+ });
3034
+ return server;
3035
+ }
3036
+ export {
3037
+ handleCompile,
3038
+ handleGenerate,
3039
+ handleImportOpenAPI,
3040
+ startServer
3041
+ };
3042
+ //# sourceMappingURL=server.js.map