Upload folder using huggingface_hub
Browse filesThis view is limited to 50 files because it contains too many changes. Β See raw diff
- Dockerfile +32 -0
- README.md +393 -7
- apps/simulator/.dockerignore +9 -0
- apps/simulator/Dockerfile +25 -0
- apps/simulator/README.md +36 -0
- apps/simulator/package.json +31 -0
- apps/simulator/pnpm-lock.yaml +1492 -0
- apps/simulator/pnpm-workspace.yaml +2 -0
- apps/simulator/src/agents/compiler.ts +99 -0
- apps/simulator/src/agents/credentialIntelligence.ts +177 -0
- apps/simulator/src/agents/graphEngine.ts +110 -0
- apps/simulator/src/agents/intentInterpreter.ts +48 -0
- apps/simulator/src/agents/simulator.ts +89 -0
- apps/simulator/src/agents/validator.ts +486 -0
- apps/simulator/src/agents/workflowPlanner.ts +36 -0
- apps/simulator/src/index.ts +96 -0
- apps/simulator/src/knowledge/nodeRegistry.ts +1078 -0
- apps/simulator/src/middleware/internalAuth.ts +27 -0
- apps/simulator/src/prompts/compiler.ts +128 -0
- apps/simulator/src/prompts/graphEngine.ts +81 -0
- apps/simulator/src/prompts/intentInterpreter.ts +51 -0
- apps/simulator/src/prompts/simulator.ts +103 -0
- apps/simulator/src/prompts/workflowPlanner.ts +80 -0
- apps/simulator/src/routes/generate.ts +244 -0
- apps/simulator/src/routes/simulate.ts +60 -0
- apps/simulator/src/routes/validate.ts +61 -0
- apps/simulator/src/services/llmClient.ts +69 -0
- apps/simulator/src/services/memorySystem.ts +333 -0
- apps/simulator/src/services/mockDataGenerator.ts +61 -0
- apps/simulator/src/services/qualityScorer.ts +155 -0
- apps/simulator/src/services/selfHealing.ts +377 -0
- apps/simulator/src/services/swarmOrchestrator.ts +228 -0
- apps/simulator/src/services/webhookAutoBind.ts +409 -0
- apps/simulator/src/types/workflow.ts +50 -0
- apps/simulator/tsconfig.json +26 -0
- apps/worker/package.json +21 -0
- apps/worker/src/index.ts +77 -0
- apps/worker/src/middleware/auth.ts +32 -0
- apps/worker/src/middleware/errorHandler.ts +16 -0
- apps/worker/src/middleware/rateLimit.ts +50 -0
- apps/worker/src/routes/activate.ts +167 -0
- apps/worker/src/routes/approval.ts +169 -0
- apps/worker/src/routes/deploy.ts +173 -0
- apps/worker/src/routes/generate.ts +149 -0
- apps/worker/src/routes/health.ts +51 -0
- apps/worker/src/routes/reports.ts +73 -0
- apps/worker/src/routes/simulate.ts +89 -0
- apps/worker/src/routes/validate.ts +77 -0
- apps/worker/src/types/env.ts +33 -0
- apps/worker/src/types/workflow.ts +499 -0
Dockerfile
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM node:20-slim
|
| 2 |
+
|
| 3 |
+
# Install pnpm
|
| 4 |
+
RUN npm install -g pnpm
|
| 5 |
+
|
| 6 |
+
WORKDIR /app
|
| 7 |
+
|
| 8 |
+
# Copy all package.json files for dependency installation
|
| 9 |
+
COPY package.json pnpm-lock.yaml* ./
|
| 10 |
+
COPY apps/simulator/package.json ./apps/simulator/
|
| 11 |
+
COPY apps/worker/package.json ./apps/worker/
|
| 12 |
+
|
| 13 |
+
# Install all dependencies in the monorepo
|
| 14 |
+
RUN pnpm install
|
| 15 |
+
|
| 16 |
+
# Copy the rest of the monorepo files
|
| 17 |
+
COPY . .
|
| 18 |
+
|
| 19 |
+
# Build the simulator service
|
| 20 |
+
RUN cd apps/simulator && pnpm install && ./node_modules/.bin/tsc
|
| 21 |
+
|
| 22 |
+
# Hugging Face Spaces run on port 7860 by default
|
| 23 |
+
EXPOSE 7860
|
| 24 |
+
|
| 25 |
+
# Set environment variables
|
| 26 |
+
ENV PORT=7860
|
| 27 |
+
ENV NODE_ENV=production
|
| 28 |
+
|
| 29 |
+
# Ensure the correct entry point is used
|
| 30 |
+
# Based on the original Dockerfile: CMD ["node", "apps/simulator/dist/apps/simulator/src/index.js"]
|
| 31 |
+
# But we need to make sure the path is correct after build
|
| 32 |
+
CMD ["node", "apps/simulator/dist/apps/simulator/src/index.js"]
|
README.md
CHANGED
|
@@ -1,10 +1,396 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Workflow Factor OS
|
| 2 |
+
|
| 3 |
+
> **Production-Grade AI Workflow Orchestrator for n8n**
|
| 4 |
+
> Built on GitHub + Cloudflare Workers Free Plan Architecture
|
| 5 |
+
|
| 6 |
+
---
|
| 7 |
+
|
| 8 |
+
## What Is This?
|
| 9 |
+
|
| 10 |
+
Workflow Factor OS is a **production-grade AI Workflow Engineering Platform** that designs, validates, simulates, deploys, and safely activates n8n workflows with professional engineering quality.
|
| 11 |
+
|
| 12 |
+
This is **not** a simple JSON generator or prompt-to-workflow toy.
|
| 13 |
+
|
| 14 |
+
It behaves like:
|
| 15 |
+
- ποΈ A **workflow engineering team**
|
| 16 |
+
- π₯οΈ A **workflow operating system**
|
| 17 |
+
- ποΈ An **AI orchestration platform**
|
| 18 |
+
|
| 19 |
+
---
|
| 20 |
+
|
| 21 |
+
## Architecture Overview
|
| 22 |
+
|
| 23 |
+
```
|
| 24 |
+
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 25 |
+
β USER / FRONTEND β
|
| 26 |
+
βββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 27 |
+
β HTTPS
|
| 28 |
+
βββββββββββββββββββββββΌββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 29 |
+
β CLOUDFLARE WORKER (apps/worker) β
|
| 30 |
+
β β’ Thin API Gateway + Orchestrator β
|
| 31 |
+
β β’ Auth, Rate Limiting, KV State Storage β
|
| 32 |
+
β β’ Routes: /generate /validate /simulate /deploy /approve /activate β
|
| 33 |
+
β β’ CPU budget: 10ms (Free Plan) β
|
| 34 |
+
βββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 35 |
+
β Internal API (X-Internal-Secret)
|
| 36 |
+
βββββββββββββββββββββββΌββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 37 |
+
β SIMULATOR SERVICE (apps/simulator) β External Node.js/Express β
|
| 38 |
+
β Heavy CPU work offloaded here: β
|
| 39 |
+
β β’ Intent Interpreter Agent (GPT-4o) β
|
| 40 |
+
β β’ Workflow Planner Agent (GPT-4o) β
|
| 41 |
+
β β’ Graph Engine (IR Layer) (GPT-4o) β
|
| 42 |
+
β β’ Workflow Compiler (GPT-4o) β
|
| 43 |
+
β β’ Validation Engine (multi-stage) β
|
| 44 |
+
β β’ Dry Run Simulator (GPT-4o + chaos testing) β
|
| 45 |
+
β β’ Credential Intelligence (n8n API scan) β
|
| 46 |
+
β β’ Quality Scorer (multi-dimension scoring) β
|
| 47 |
+
βββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 48 |
+
β n8n REST API
|
| 49 |
+
βββββββββββββββββββββββΌββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 50 |
+
β n8n INSTANCE β
|
| 51 |
+
β β’ Workflow storage (deployed but NOT activated) β
|
| 52 |
+
β β’ Credential management β
|
| 53 |
+
β β’ Workflow activation (only after human approval) β
|
| 54 |
+
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 55 |
+
```
|
| 56 |
+
|
| 57 |
+
---
|
| 58 |
+
|
| 59 |
+
## Workflow Lifecycle
|
| 60 |
+
|
| 61 |
+
```
|
| 62 |
+
User Request
|
| 63 |
+
β
|
| 64 |
+
βΌ
|
| 65 |
+
[1] Intent Interpretation β classify type, integrations, risk
|
| 66 |
+
β
|
| 67 |
+
βΌ
|
| 68 |
+
[2] Architecture Planning β layered design, risk analysis
|
| 69 |
+
β
|
| 70 |
+
βΌ
|
| 71 |
+
[3] Graph IR Generation β internal WorkflowGraph (source of truth)
|
| 72 |
+
β
|
| 73 |
+
βΌ
|
| 74 |
+
[4] Compile to n8n JSON β valid, schema-compliant n8n workflow
|
| 75 |
+
β
|
| 76 |
+
βΌ
|
| 77 |
+
[5] Multi-Stage Validation β schema, credentials, graph, expressions, reliability
|
| 78 |
+
β
|
| 79 |
+
βΌ
|
| 80 |
+
[6] Dry-Run Simulation β mock data, chaos tests, failure prediction
|
| 81 |
+
β
|
| 82 |
+
βΌ
|
| 83 |
+
[7] Deploy to n8n β active: false (NEVER auto-activate)
|
| 84 |
+
β
|
| 85 |
+
βΌ
|
| 86 |
+
[8] Credential Check β verify all required credentials exist
|
| 87 |
+
β
|
| 88 |
+
βΌ
|
| 89 |
+
[9] Human Approval Request β reviewer sees: architecture, risks, scores
|
| 90 |
+
β
|
| 91 |
+
βΌ
|
| 92 |
+
[10] Human Decision β approve / reject / modify
|
| 93 |
+
β
|
| 94 |
+
βΌ
|
| 95 |
+
[11] Activation Engine β final validation + activate via n8n API
|
| 96 |
+
β
|
| 97 |
+
βΌ
|
| 98 |
+
[12] Audit Log β full trail of every lifecycle event
|
| 99 |
+
```
|
| 100 |
+
|
| 101 |
+
---
|
| 102 |
+
|
| 103 |
+
## Repository Structure
|
| 104 |
+
|
| 105 |
+
```
|
| 106 |
+
workflow-factor-os/
|
| 107 |
+
β
|
| 108 |
+
βββ apps/
|
| 109 |
+
β βββ worker/ # Cloudflare Worker (API Gateway)
|
| 110 |
+
β β βββ src/
|
| 111 |
+
β β βββ index.ts # Hono app entry point
|
| 112 |
+
β β βββ routes/ # generate, validate, simulate, deploy, approval, activate, reports, health
|
| 113 |
+
β β βββ middleware/ # auth, rateLimit, errorHandler
|
| 114 |
+
β β βββ types/ # env.ts, workflow.ts (all shared types)
|
| 115 |
+
β β βββ utils/ # audit.ts, ids.ts
|
| 116 |
+
β β
|
| 117 |
+
β βββ simulator/ # External Compute Service (Node.js/Express)
|
| 118 |
+
β βββ src/
|
| 119 |
+
β βββ index.ts # Express app entry point
|
| 120 |
+
β βββ routes/ # generate, validate, simulate
|
| 121 |
+
β βββ middleware/ # internalAuth
|
| 122 |
+
β βββ agents/ # intentInterpreter, workflowPlanner, graphEngine, compiler, validator, simulator, credentialIntelligence
|
| 123 |
+
β βββ services/ # mockDataGenerator, qualityScorer
|
| 124 |
+
β βββ prompts/ # intentInterpreter, workflowPlanner, graphEngine, compiler, simulator
|
| 125 |
+
β βββ knowledge/ # nodeRegistry (real n8n node schemas)
|
| 126 |
+
β βββ types/ # workflow types (re-export)
|
| 127 |
+
β
|
| 128 |
+
βββ core/ # Domain core modules (extensible)
|
| 129 |
+
β βββ interpreter/ # Intent Interpreter logic
|
| 130 |
+
β βββ planner/ # Architecture Planner logic
|
| 131 |
+
β βββ graph/ # Graph IR Engine
|
| 132 |
+
β βββ compiler/ # Workflow Compiler
|
| 133 |
+
β βββ validator/ # Validation Engine
|
| 134 |
+
β βββ simulator/ # Dry Run Simulator
|
| 135 |
+
β βββ deployer/ # Deployment Engine
|
| 136 |
+
β βββ credentials/ # Credential Intelligence
|
| 137 |
+
β βββ approval/ # Human Approval System
|
| 138 |
+
β βββ activation/ # Activation Engine
|
| 139 |
+
β
|
| 140 |
+
βββ knowledge/
|
| 141 |
+
β βββ n8n-schema/ # n8n JSON schema definitions
|
| 142 |
+
β βββ node-definitions/ # Extracted node registry
|
| 143 |
+
β βββ workflow-patterns/ # Retry, webhook safety, AI, error handling patterns
|
| 144 |
+
β βββ anti-patterns/ # Known bad patterns to avoid
|
| 145 |
+
β βββ execution-rules/ # n8n runtime rules
|
| 146 |
+
β
|
| 147 |
+
βββ integrations/
|
| 148 |
+
β βββ n8n-client/ # Typed n8n REST API client
|
| 149 |
+
β βββ llm-providers/ # Provider-agnostic LLM abstraction
|
| 150 |
+
β βββ database/ # External DB client (future)
|
| 151 |
+
β βββ queue/ # External queue client (future)
|
| 152 |
+
β
|
| 153 |
+
βββ validation/
|
| 154 |
+
β βββ schema/ # Zod schemas for all types
|
| 155 |
+
β βββ graph/ # Graph validation utilities
|
| 156 |
+
β βββ credentials/ # Credential validation
|
| 157 |
+
β βββ expressions/ # Expression safety analysis
|
| 158 |
+
β βββ reliability/ # Retry, fallback, monitoring checks
|
| 159 |
+
β
|
| 160 |
+
βββ observability/
|
| 161 |
+
β βββ logging/ # Pino structured logger
|
| 162 |
+
β βββ metrics/ # Metrics collection (future)
|
| 163 |
+
β βββ tracing/ # Distributed tracing (future)
|
| 164 |
+
β βββ audit/ # Audit trail
|
| 165 |
+
β
|
| 166 |
+
βββ config/
|
| 167 |
+
β βββ prompts/ # Central prompt registry
|
| 168 |
+
β βββ env/ # .env.example
|
| 169 |
+
β βββ n8n/ # n8n API endpoint config
|
| 170 |
+
β
|
| 171 |
+
βββ scripts/
|
| 172 |
+
β βββ build-knowledge-base.js # Extract n8n node schemas
|
| 173 |
+
β βββ validate-env.js # Pre-deploy env validation
|
| 174 |
+
β
|
| 175 |
+
βββ docs/ # Architecture diagrams, API docs
|
| 176 |
+
```
|
| 177 |
+
|
| 178 |
---
|
| 179 |
+
|
| 180 |
+
## API Endpoints
|
| 181 |
+
|
| 182 |
+
### Cloudflare Worker (Public API)
|
| 183 |
+
|
| 184 |
+
| Method | Endpoint | Description |
|
| 185 |
+
|--------|----------|-------------|
|
| 186 |
+
| `GET` | `/health` | Health check (public) |
|
| 187 |
+
| `POST` | `/api/workflows/generate` | Full pipeline: intent β plan β graph β compile β validate β simulate |
|
| 188 |
+
| `POST` | `/api/workflows/validate` | Re-run validation on existing job |
|
| 189 |
+
| `POST` | `/api/workflows/simulate` | Re-run dry-run simulation |
|
| 190 |
+
| `POST` | `/api/workflows/deploy` | Deploy to n8n (active: false) |
|
| 191 |
+
| `POST` | `/api/workflows/request-approval` | Create human approval request |
|
| 192 |
+
| `GET` | `/api/workflows/approve/:jobId` | Get approval request details |
|
| 193 |
+
| `POST` | `/api/workflows/approve` | Submit approval decision |
|
| 194 |
+
| `POST` | `/api/workflows/activate` | Activate approved workflow |
|
| 195 |
+
| `GET` | `/api/reports/workflow/:id` | Full job report |
|
| 196 |
+
| `GET` | `/api/reports/validation/:id` | Validation report |
|
| 197 |
+
| `GET` | `/api/reports/simulation/:id` | Simulation report |
|
| 198 |
+
| `GET` | `/api/reports/credential/:id` | Credential analysis |
|
| 199 |
+
| `GET` | `/api/reports/audit/:id` | Full audit trail |
|
| 200 |
+
|
| 201 |
+
### Simulator Service (Internal β not public)
|
| 202 |
+
|
| 203 |
+
| Method | Endpoint | Description |
|
| 204 |
+
|--------|----------|-------------|
|
| 205 |
+
| `GET` | `/health` | Service health |
|
| 206 |
+
| `POST` | `/process/generate` | Full AI pipeline |
|
| 207 |
+
| `POST` | `/process/validate` | Validation engine |
|
| 208 |
+
| `POST` | `/process/simulate` | Dry-run simulation |
|
| 209 |
+
|
| 210 |
---
|
| 211 |
|
| 212 |
+
## Quick Start
|
| 213 |
+
|
| 214 |
+
### 1. Prerequisites
|
| 215 |
+
|
| 216 |
+
- Node.js >= 18
|
| 217 |
+
- A running n8n instance with API key
|
| 218 |
+
- OpenAI API key
|
| 219 |
+
- Cloudflare account (free plan OK)
|
| 220 |
+
|
| 221 |
+
### 2. Clone and Install
|
| 222 |
+
|
| 223 |
+
```bash
|
| 224 |
+
git clone https://github.com/pyaesonegtckglay-dotcom/workflow-factor-os.git
|
| 225 |
+
cd workflow-factor-os
|
| 226 |
+
npm install
|
| 227 |
+
```
|
| 228 |
+
|
| 229 |
+
### 3. Configure Simulator Service
|
| 230 |
+
|
| 231 |
+
```bash
|
| 232 |
+
cp config/env/.env.example apps/simulator/.env
|
| 233 |
+
# Edit .env with your values
|
| 234 |
+
```
|
| 235 |
+
|
| 236 |
+
### 4. Start Simulator Service
|
| 237 |
+
|
| 238 |
+
```bash
|
| 239 |
+
npm run dev:simulator
|
| 240 |
+
# Runs on http://localhost:3001
|
| 241 |
+
```
|
| 242 |
+
|
| 243 |
+
### 5. Configure & Deploy Cloudflare Worker
|
| 244 |
+
|
| 245 |
+
```bash
|
| 246 |
+
cd apps/worker
|
| 247 |
+
|
| 248 |
+
# Set secrets
|
| 249 |
+
wrangler secret put OPENAI_API_KEY
|
| 250 |
+
wrangler secret put N8N_API_KEY
|
| 251 |
+
wrangler secret put INTERNAL_API_SECRET
|
| 252 |
+
|
| 253 |
+
# Create KV namespace
|
| 254 |
+
wrangler kv:namespace create "WFO_CACHE"
|
| 255 |
+
# Copy the ID into wrangler.toml
|
| 256 |
+
|
| 257 |
+
# Update wrangler.toml with your SIMULATOR_URL and N8N_BASE_URL
|
| 258 |
+
|
| 259 |
+
# Deploy
|
| 260 |
+
npm run deploy
|
| 261 |
+
```
|
| 262 |
+
|
| 263 |
+
### 6. Test
|
| 264 |
+
|
| 265 |
+
```bash
|
| 266 |
+
# Generate a workflow
|
| 267 |
+
curl -X POST https://your-worker.workers.dev/api/workflows/generate \
|
| 268 |
+
-H "X-API-Key: your-secret" \
|
| 269 |
+
-H "Content-Type: application/json" \
|
| 270 |
+
-d '{
|
| 271 |
+
"request": "Build a Telegram bot that receives messages, processes them with GPT-4o, and logs results to Google Sheets"
|
| 272 |
+
}'
|
| 273 |
+
|
| 274 |
+
# Get the jobId from response, then check full report:
|
| 275 |
+
curl https://your-worker.workers.dev/api/reports/workflow/<jobId> \
|
| 276 |
+
-H "X-API-Key: your-secret"
|
| 277 |
+
```
|
| 278 |
+
|
| 279 |
+
---
|
| 280 |
+
|
| 281 |
+
## 12 Core Engineering Systems
|
| 282 |
+
|
| 283 |
+
| System | Location | Description |
|
| 284 |
+
|--------|----------|-------------|
|
| 285 |
+
| Intent Interpreter | `apps/simulator/src/agents/intentInterpreter.ts` | Classifies request, identifies integrations, risk, domain |
|
| 286 |
+
| Workflow Planner | `apps/simulator/src/agents/workflowPlanner.ts` | Layered architecture design, risk analysis |
|
| 287 |
+
| Graph Engine (IR) | `apps/simulator/src/agents/graphEngine.ts` | WorkflowGraph IR - source of truth |
|
| 288 |
+
| Pattern Knowledge Engine | `knowledge/workflow-patterns/` | Retry, webhook safety, AI, error patterns |
|
| 289 |
+
| n8n Repository Intelligence | `apps/simulator/src/knowledge/nodeRegistry.ts` | Real n8n node schemas |
|
| 290 |
+
| Workflow Compiler | `apps/simulator/src/agents/compiler.ts` | Graph IR β valid n8n JSON |
|
| 291 |
+
| Validation Engine | `apps/simulator/src/agents/validator.ts` | 5-stage validation |
|
| 292 |
+
| Dry Run Simulation | `apps/simulator/src/agents/simulator.ts` | Mock execution + chaos testing |
|
| 293 |
+
| Deployment Engine | `apps/worker/src/routes/deploy.ts` | Safe n8n deployment (never auto-activate) |
|
| 294 |
+
| Credential Intelligence | `apps/simulator/src/agents/credentialIntelligence.ts` | Credential scan + missing detection |
|
| 295 |
+
| Human Approval System | `apps/worker/src/routes/approval.ts` | Mandatory human gate before activation |
|
| 296 |
+
| Activation Engine | `apps/worker/src/routes/activate.ts` | Final safety checks + n8n activation |
|
| 297 |
+
|
| 298 |
+
---
|
| 299 |
+
|
| 300 |
+
## Engineering Philosophy
|
| 301 |
+
|
| 302 |
+
### NEVER:
|
| 303 |
+
- β Instantly activate workflows
|
| 304 |
+
- β Skip validation
|
| 305 |
+
- β Skip dry-run simulation
|
| 306 |
+
- β Assume credentials exist
|
| 307 |
+
- β Hallucinate node parameters
|
| 308 |
+
- β Generate monolithic spaghetti workflows
|
| 309 |
+
|
| 310 |
+
### ALWAYS:
|
| 311 |
+
- β
Validate before deploying
|
| 312 |
+
- β
Simulate before deploying
|
| 313 |
+
- β
Check credentials
|
| 314 |
+
- β
Require human approval
|
| 315 |
+
- β
Use modular workflow design
|
| 316 |
+
- β
Think about retries and fallbacks
|
| 317 |
+
- β
Think about observability
|
| 318 |
+
- β
Use optional chaining in expressions
|
| 319 |
+
|
| 320 |
+
---
|
| 321 |
+
|
| 322 |
+
## Cloudflare Workers Free Plan Adaptations
|
| 323 |
+
|
| 324 |
+
Per the architecture addendum, the CF Worker is a **thin API gateway** only:
|
| 325 |
+
|
| 326 |
+
| Component | Free Plan Approach |
|
| 327 |
+
|-----------|-------------------|
|
| 328 |
+
| Intent Interpreter | Offloaded to Simulator service |
|
| 329 |
+
| Workflow Planner | Offloaded to Simulator service |
|
| 330 |
+
| Graph Engine | Offloaded to Simulator service |
|
| 331 |
+
| Compiler | Offloaded to Simulator service |
|
| 332 |
+
| Validation Engine | Offloaded to Simulator service |
|
| 333 |
+
| Dry Run Simulator | Offloaded to Simulator service |
|
| 334 |
+
| State Storage | Workers KV (job records, 7-day TTL) |
|
| 335 |
+
| Credential Check | Worker calls n8n API directly |
|
| 336 |
+
| Deployment | Worker calls n8n API directly |
|
| 337 |
+
| Activation | Worker calls n8n API directly |
|
| 338 |
+
| Auth & Rate Limiting | Worker handles (minimal CPU) |
|
| 339 |
+
|
| 340 |
+
---
|
| 341 |
+
|
| 342 |
+
## Workflow Quality Scoring
|
| 343 |
+
|
| 344 |
+
Every generated workflow is scored across 9 dimensions:
|
| 345 |
+
|
| 346 |
+
| Dimension | Weight | What It Measures |
|
| 347 |
+
|-----------|--------|-----------------|
|
| 348 |
+
| Schema Correctness | 25% | Valid n8n JSON structure |
|
| 349 |
+
| Credential Readiness | 20% | All required credentials present |
|
| 350 |
+
| Graph Validity | 20% | No orphans, proper connections |
|
| 351 |
+
| Expression Safety | 15% | Null-safe expressions |
|
| 352 |
+
| Reliability | 20% | Retry policies, fallbacks, monitoring |
|
| 353 |
+
|
| 354 |
+
**Deployment Readiness:**
|
| 355 |
+
- π’ 85-100: Low risk β safe to deploy
|
| 356 |
+
- π‘ 65-84: Medium risk β review warnings
|
| 357 |
+
- π 40-64: High risk β fix issues first
|
| 358 |
+
- π΄ 0-39: Critical β do not deploy
|
| 359 |
+
|
| 360 |
+
---
|
| 361 |
+
|
| 362 |
+
## Workflow Lifecycle States
|
| 363 |
+
|
| 364 |
+
```
|
| 365 |
+
draft β planned β generated β validated β simulated β
|
| 366 |
+
deployed β awaiting_approval β approved/rejected β activated β monitored β deprecated
|
| 367 |
+
```
|
| 368 |
+
|
| 369 |
+
---
|
| 370 |
+
|
| 371 |
+
## Security
|
| 372 |
+
|
| 373 |
+
- All API endpoints protected by `X-API-Key` header
|
| 374 |
+
- Worker β Simulator communication uses shared `INTERNAL_API_SECRET`
|
| 375 |
+
- All secrets stored as Cloudflare Worker Secrets (never in wrangler.toml)
|
| 376 |
+
- Credentials never logged or exposed in responses
|
| 377 |
+
- Rate limiting: 20 requests/minute per IP via KV
|
| 378 |
+
|
| 379 |
+
---
|
| 380 |
+
|
| 381 |
+
## Future Roadmap
|
| 382 |
+
|
| 383 |
+
- [ ] Multi-user / RBAC support
|
| 384 |
+
- [ ] Workflow template marketplace
|
| 385 |
+
- [ ] AI workflow memory (remember successful patterns)
|
| 386 |
+
- [ ] Autonomous workflow optimization
|
| 387 |
+
- [ ] Self-healing workflows
|
| 388 |
+
- [ ] Runtime anomaly detection
|
| 389 |
+
- [ ] Web UI for human approval dashboard
|
| 390 |
+
- [ ] D1 database for persistent job storage (beyond KV)
|
| 391 |
+
|
| 392 |
+
---
|
| 393 |
+
|
| 394 |
+
## License
|
| 395 |
+
|
| 396 |
+
MIT
|
apps/simulator/.dockerignore
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
node_modules
|
| 2 |
+
dist
|
| 3 |
+
.env
|
| 4 |
+
.git
|
| 5 |
+
.github
|
| 6 |
+
.vscode
|
| 7 |
+
.DS_Store
|
| 8 |
+
apps/*/node_modules
|
| 9 |
+
apps/*/dist
|
apps/simulator/Dockerfile
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM node:20-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
# Install pnpm
|
| 6 |
+
RUN npm install -g pnpm
|
| 7 |
+
|
| 8 |
+
# Copy all package.json files for dependency installation
|
| 9 |
+
COPY package.json pnpm-lock.yaml* ./
|
| 10 |
+
COPY apps/simulator/package.json ./apps/simulator/
|
| 11 |
+
COPY apps/worker/package.json ./apps/worker/
|
| 12 |
+
|
| 13 |
+
# Install all dependencies in the monorepo
|
| 14 |
+
RUN pnpm install
|
| 15 |
+
|
| 16 |
+
# Copy the rest of the monorepo files
|
| 17 |
+
COPY . .
|
| 18 |
+
|
| 19 |
+
# Build the simulator service
|
| 20 |
+
RUN cd apps/simulator && npm install && ./node_modules/.bin/tsc
|
| 21 |
+
|
| 22 |
+
EXPOSE 7860
|
| 23 |
+
|
| 24 |
+
# Ensure the correct entry point is used
|
| 25 |
+
CMD ["node", "apps/simulator/dist/apps/simulator/src/index.js"]
|
apps/simulator/README.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Workflow Factor OS - Simulator Service
|
| 2 |
+
|
| 3 |
+
This is the Simulator Service component of the Workflow Factor OS monorepo, deployed to Hugging Face Spaces.
|
| 4 |
+
|
| 5 |
+
## Environment Variables
|
| 6 |
+
|
| 7 |
+
The following environment variables **MUST** be set in your Hugging Face Space settings:
|
| 8 |
+
|
| 9 |
+
* `LLM_GATEWAY_URL`: The URL of your custom LLM gateway (e.g., `https://llm-routing-system.pyaesone-n8n.workers.dev/v1/chat/completions`)
|
| 10 |
+
* `LLM_API_KEY`: Your API key for the LLM gateway. This will be used as the primary LLM key.
|
| 11 |
+
* `N8N_BASE_URL`: The base URL of your n8n instance (e.g., `https://pyae1994-n8n-aiven-v1.hf.space/`)
|
| 12 |
+
* `N8N_API_KEY`: Your API key for the n8n instance.
|
| 13 |
+
* `INTERNAL_API_SECRET`: A shared secret between the Cloudflare Worker and this Simulator service for internal API authentication (e.g., `super-secret-PSA`).
|
| 14 |
+
|
| 15 |
+
## Deployment
|
| 16 |
+
|
| 17 |
+
This service is designed to be deployed as a Docker image to Hugging Face Spaces. The `Dockerfile` in this directory handles the build process.
|
| 18 |
+
|
| 19 |
+
### Build and Run Locally (for testing)
|
| 20 |
+
|
| 21 |
+
To build and run this service locally using Docker, navigate to the `apps/simulator` directory and execute:
|
| 22 |
+
|
| 23 |
+
```bash
|
| 24 |
+
docker build -t wfo-simulator .
|
| 25 |
+
docker run -p 7860:7860 -e LLM_GATEWAY_URL="your_llm_gateway_url" -e LLM_API_KEY="your_llm_api_key" -e N8N_BASE_URL="your_n8n_base_url" -e N8N_API_KEY="your_n8n_api_key" -e INTERNAL_API_SECRET="your_internal_api_secret" wfo-simulator
|
| 26 |
+
```
|
| 27 |
+
|
| 28 |
+
### Hugging Face Spaces Deployment
|
| 29 |
+
|
| 30 |
+
1. Create a new Space on Hugging Face.
|
| 31 |
+
2. Select "Docker" as the Space SDK.
|
| 32 |
+
3. Upload the contents of the `apps/simulator` directory (including the `Dockerfile` and `.dockerignore`) to your Hugging Face Space repository.
|
| 33 |
+
4. Configure the environment variables listed above in the Space settings.
|
| 34 |
+
5. Hugging Face Spaces will automatically build and deploy your Docker image.
|
| 35 |
+
|
| 36 |
+
**Note:** The service exposes port `7860` as required by Hugging Face Spaces for custom Docker applications.
|
apps/simulator/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "@wfo/simulator",
|
| 3 |
+
"version": "1.0.0",
|
| 4 |
+
"description": "External Compute Service - Heavy AI orchestration logic (Intent, Planner, Graph, Compiler, Validator, Simulator)",
|
| 5 |
+
"private": true,
|
| 6 |
+
"scripts": {
|
| 7 |
+
"dev": "tsx watch src/index.ts",
|
| 8 |
+
"build": "tsc",
|
| 9 |
+
"start": "node dist/index.js",
|
| 10 |
+
"lint": "eslint src --ext .ts"
|
| 11 |
+
},
|
| 12 |
+
"dependencies": {
|
| 13 |
+
"cors": "^2.8.5",
|
| 14 |
+
"dotenv": "^16.4.0",
|
| 15 |
+
"express": "^4.18.0",
|
| 16 |
+
"express-rate-limit": "^7.2.0",
|
| 17 |
+
"helmet": "^7.1.0",
|
| 18 |
+
"module-alias": "^2.3.4",
|
| 19 |
+
"openai": "^4.28.0",
|
| 20 |
+
"pino": "^8.17.0",
|
| 21 |
+
"pino-pretty": "^10.3.0",
|
| 22 |
+
"zod": "^3.22.0"
|
| 23 |
+
},
|
| 24 |
+
"devDependencies": {
|
| 25 |
+
"@types/cors": "^2.8.17",
|
| 26 |
+
"@types/express": "^4.17.21",
|
| 27 |
+
"@types/node": "^20.11.0",
|
| 28 |
+
"tsx": "^4.7.0",
|
| 29 |
+
"typescript": "^5.3.0"
|
| 30 |
+
}
|
| 31 |
+
}
|
apps/simulator/pnpm-lock.yaml
ADDED
|
@@ -0,0 +1,1492 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
lockfileVersion: '9.0'
|
| 2 |
+
|
| 3 |
+
settings:
|
| 4 |
+
autoInstallPeers: true
|
| 5 |
+
excludeLinksFromLockfile: false
|
| 6 |
+
|
| 7 |
+
importers:
|
| 8 |
+
|
| 9 |
+
.:
|
| 10 |
+
dependencies:
|
| 11 |
+
cors:
|
| 12 |
+
specifier: ^2.8.5
|
| 13 |
+
version: 2.8.6
|
| 14 |
+
dotenv:
|
| 15 |
+
specifier: ^16.4.0
|
| 16 |
+
version: 16.6.1
|
| 17 |
+
express:
|
| 18 |
+
specifier: ^4.18.0
|
| 19 |
+
version: 4.22.1
|
| 20 |
+
express-rate-limit:
|
| 21 |
+
specifier: ^7.2.0
|
| 22 |
+
version: 7.5.1(express@4.22.1)
|
| 23 |
+
helmet:
|
| 24 |
+
specifier: ^7.1.0
|
| 25 |
+
version: 7.2.0
|
| 26 |
+
openai:
|
| 27 |
+
specifier: ^4.28.0
|
| 28 |
+
version: 4.104.0(zod@3.25.76)
|
| 29 |
+
pino:
|
| 30 |
+
specifier: ^8.17.0
|
| 31 |
+
version: 8.21.0
|
| 32 |
+
pino-pretty:
|
| 33 |
+
specifier: ^10.3.0
|
| 34 |
+
version: 10.3.1
|
| 35 |
+
zod:
|
| 36 |
+
specifier: ^3.22.0
|
| 37 |
+
version: 3.25.76
|
| 38 |
+
devDependencies:
|
| 39 |
+
'@types/cors':
|
| 40 |
+
specifier: ^2.8.17
|
| 41 |
+
version: 2.8.19
|
| 42 |
+
'@types/express':
|
| 43 |
+
specifier: ^4.17.21
|
| 44 |
+
version: 4.17.25
|
| 45 |
+
'@types/node':
|
| 46 |
+
specifier: ^20.11.0
|
| 47 |
+
version: 20.19.40
|
| 48 |
+
tsx:
|
| 49 |
+
specifier: ^4.7.0
|
| 50 |
+
version: 4.21.0
|
| 51 |
+
typescript:
|
| 52 |
+
specifier: ^5.3.0
|
| 53 |
+
version: 5.9.3
|
| 54 |
+
|
| 55 |
+
packages:
|
| 56 |
+
|
| 57 |
+
'@esbuild/aix-ppc64@0.27.7':
|
| 58 |
+
resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==}
|
| 59 |
+
engines: {node: '>=18'}
|
| 60 |
+
cpu: [ppc64]
|
| 61 |
+
os: [aix]
|
| 62 |
+
|
| 63 |
+
'@esbuild/android-arm64@0.27.7':
|
| 64 |
+
resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==}
|
| 65 |
+
engines: {node: '>=18'}
|
| 66 |
+
cpu: [arm64]
|
| 67 |
+
os: [android]
|
| 68 |
+
|
| 69 |
+
'@esbuild/android-arm@0.27.7':
|
| 70 |
+
resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==}
|
| 71 |
+
engines: {node: '>=18'}
|
| 72 |
+
cpu: [arm]
|
| 73 |
+
os: [android]
|
| 74 |
+
|
| 75 |
+
'@esbuild/android-x64@0.27.7':
|
| 76 |
+
resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==}
|
| 77 |
+
engines: {node: '>=18'}
|
| 78 |
+
cpu: [x64]
|
| 79 |
+
os: [android]
|
| 80 |
+
|
| 81 |
+
'@esbuild/darwin-arm64@0.27.7':
|
| 82 |
+
resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==}
|
| 83 |
+
engines: {node: '>=18'}
|
| 84 |
+
cpu: [arm64]
|
| 85 |
+
os: [darwin]
|
| 86 |
+
|
| 87 |
+
'@esbuild/darwin-x64@0.27.7':
|
| 88 |
+
resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==}
|
| 89 |
+
engines: {node: '>=18'}
|
| 90 |
+
cpu: [x64]
|
| 91 |
+
os: [darwin]
|
| 92 |
+
|
| 93 |
+
'@esbuild/freebsd-arm64@0.27.7':
|
| 94 |
+
resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==}
|
| 95 |
+
engines: {node: '>=18'}
|
| 96 |
+
cpu: [arm64]
|
| 97 |
+
os: [freebsd]
|
| 98 |
+
|
| 99 |
+
'@esbuild/freebsd-x64@0.27.7':
|
| 100 |
+
resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==}
|
| 101 |
+
engines: {node: '>=18'}
|
| 102 |
+
cpu: [x64]
|
| 103 |
+
os: [freebsd]
|
| 104 |
+
|
| 105 |
+
'@esbuild/linux-arm64@0.27.7':
|
| 106 |
+
resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==}
|
| 107 |
+
engines: {node: '>=18'}
|
| 108 |
+
cpu: [arm64]
|
| 109 |
+
os: [linux]
|
| 110 |
+
|
| 111 |
+
'@esbuild/linux-arm@0.27.7':
|
| 112 |
+
resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==}
|
| 113 |
+
engines: {node: '>=18'}
|
| 114 |
+
cpu: [arm]
|
| 115 |
+
os: [linux]
|
| 116 |
+
|
| 117 |
+
'@esbuild/linux-ia32@0.27.7':
|
| 118 |
+
resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==}
|
| 119 |
+
engines: {node: '>=18'}
|
| 120 |
+
cpu: [ia32]
|
| 121 |
+
os: [linux]
|
| 122 |
+
|
| 123 |
+
'@esbuild/linux-loong64@0.27.7':
|
| 124 |
+
resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==}
|
| 125 |
+
engines: {node: '>=18'}
|
| 126 |
+
cpu: [loong64]
|
| 127 |
+
os: [linux]
|
| 128 |
+
|
| 129 |
+
'@esbuild/linux-mips64el@0.27.7':
|
| 130 |
+
resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==}
|
| 131 |
+
engines: {node: '>=18'}
|
| 132 |
+
cpu: [mips64el]
|
| 133 |
+
os: [linux]
|
| 134 |
+
|
| 135 |
+
'@esbuild/linux-ppc64@0.27.7':
|
| 136 |
+
resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==}
|
| 137 |
+
engines: {node: '>=18'}
|
| 138 |
+
cpu: [ppc64]
|
| 139 |
+
os: [linux]
|
| 140 |
+
|
| 141 |
+
'@esbuild/linux-riscv64@0.27.7':
|
| 142 |
+
resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==}
|
| 143 |
+
engines: {node: '>=18'}
|
| 144 |
+
cpu: [riscv64]
|
| 145 |
+
os: [linux]
|
| 146 |
+
|
| 147 |
+
'@esbuild/linux-s390x@0.27.7':
|
| 148 |
+
resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==}
|
| 149 |
+
engines: {node: '>=18'}
|
| 150 |
+
cpu: [s390x]
|
| 151 |
+
os: [linux]
|
| 152 |
+
|
| 153 |
+
'@esbuild/linux-x64@0.27.7':
|
| 154 |
+
resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==}
|
| 155 |
+
engines: {node: '>=18'}
|
| 156 |
+
cpu: [x64]
|
| 157 |
+
os: [linux]
|
| 158 |
+
|
| 159 |
+
'@esbuild/netbsd-arm64@0.27.7':
|
| 160 |
+
resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==}
|
| 161 |
+
engines: {node: '>=18'}
|
| 162 |
+
cpu: [arm64]
|
| 163 |
+
os: [netbsd]
|
| 164 |
+
|
| 165 |
+
'@esbuild/netbsd-x64@0.27.7':
|
| 166 |
+
resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==}
|
| 167 |
+
engines: {node: '>=18'}
|
| 168 |
+
cpu: [x64]
|
| 169 |
+
os: [netbsd]
|
| 170 |
+
|
| 171 |
+
'@esbuild/openbsd-arm64@0.27.7':
|
| 172 |
+
resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==}
|
| 173 |
+
engines: {node: '>=18'}
|
| 174 |
+
cpu: [arm64]
|
| 175 |
+
os: [openbsd]
|
| 176 |
+
|
| 177 |
+
'@esbuild/openbsd-x64@0.27.7':
|
| 178 |
+
resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==}
|
| 179 |
+
engines: {node: '>=18'}
|
| 180 |
+
cpu: [x64]
|
| 181 |
+
os: [openbsd]
|
| 182 |
+
|
| 183 |
+
'@esbuild/openharmony-arm64@0.27.7':
|
| 184 |
+
resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==}
|
| 185 |
+
engines: {node: '>=18'}
|
| 186 |
+
cpu: [arm64]
|
| 187 |
+
os: [openharmony]
|
| 188 |
+
|
| 189 |
+
'@esbuild/sunos-x64@0.27.7':
|
| 190 |
+
resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==}
|
| 191 |
+
engines: {node: '>=18'}
|
| 192 |
+
cpu: [x64]
|
| 193 |
+
os: [sunos]
|
| 194 |
+
|
| 195 |
+
'@esbuild/win32-arm64@0.27.7':
|
| 196 |
+
resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==}
|
| 197 |
+
engines: {node: '>=18'}
|
| 198 |
+
cpu: [arm64]
|
| 199 |
+
os: [win32]
|
| 200 |
+
|
| 201 |
+
'@esbuild/win32-ia32@0.27.7':
|
| 202 |
+
resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==}
|
| 203 |
+
engines: {node: '>=18'}
|
| 204 |
+
cpu: [ia32]
|
| 205 |
+
os: [win32]
|
| 206 |
+
|
| 207 |
+
'@esbuild/win32-x64@0.27.7':
|
| 208 |
+
resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==}
|
| 209 |
+
engines: {node: '>=18'}
|
| 210 |
+
cpu: [x64]
|
| 211 |
+
os: [win32]
|
| 212 |
+
|
| 213 |
+
'@types/body-parser@1.19.6':
|
| 214 |
+
resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==}
|
| 215 |
+
|
| 216 |
+
'@types/connect@3.4.38':
|
| 217 |
+
resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
|
| 218 |
+
|
| 219 |
+
'@types/cors@2.8.19':
|
| 220 |
+
resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==}
|
| 221 |
+
|
| 222 |
+
'@types/express-serve-static-core@4.19.8':
|
| 223 |
+
resolution: {integrity: sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==}
|
| 224 |
+
|
| 225 |
+
'@types/express@4.17.25':
|
| 226 |
+
resolution: {integrity: sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==}
|
| 227 |
+
|
| 228 |
+
'@types/http-errors@2.0.5':
|
| 229 |
+
resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==}
|
| 230 |
+
|
| 231 |
+
'@types/mime@1.3.5':
|
| 232 |
+
resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==}
|
| 233 |
+
|
| 234 |
+
'@types/node-fetch@2.6.13':
|
| 235 |
+
resolution: {integrity: sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==}
|
| 236 |
+
|
| 237 |
+
'@types/node@18.19.130':
|
| 238 |
+
resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==}
|
| 239 |
+
|
| 240 |
+
'@types/node@20.19.40':
|
| 241 |
+
resolution: {integrity: sha512-xxx6M2IpSTnnKcR0cMvIiohkiCx20/oRPtWGbenFygKCGl3zqUzdNjQ/1V4solq1LU+dgv0nQzeGOuqkqZGg0Q==}
|
| 242 |
+
|
| 243 |
+
'@types/qs@6.15.1':
|
| 244 |
+
resolution: {integrity: sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==}
|
| 245 |
+
|
| 246 |
+
'@types/range-parser@1.2.7':
|
| 247 |
+
resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==}
|
| 248 |
+
|
| 249 |
+
'@types/send@0.17.6':
|
| 250 |
+
resolution: {integrity: sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==}
|
| 251 |
+
|
| 252 |
+
'@types/send@1.2.1':
|
| 253 |
+
resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==}
|
| 254 |
+
|
| 255 |
+
'@types/serve-static@1.15.10':
|
| 256 |
+
resolution: {integrity: sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==}
|
| 257 |
+
|
| 258 |
+
abort-controller@3.0.0:
|
| 259 |
+
resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
|
| 260 |
+
engines: {node: '>=6.5'}
|
| 261 |
+
|
| 262 |
+
accepts@1.3.8:
|
| 263 |
+
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
|
| 264 |
+
engines: {node: '>= 0.6'}
|
| 265 |
+
|
| 266 |
+
agentkeepalive@4.6.0:
|
| 267 |
+
resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==}
|
| 268 |
+
engines: {node: '>= 8.0.0'}
|
| 269 |
+
|
| 270 |
+
array-flatten@1.1.1:
|
| 271 |
+
resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==}
|
| 272 |
+
|
| 273 |
+
asynckit@0.4.0:
|
| 274 |
+
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
| 275 |
+
|
| 276 |
+
atomic-sleep@1.0.0:
|
| 277 |
+
resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==}
|
| 278 |
+
engines: {node: '>=8.0.0'}
|
| 279 |
+
|
| 280 |
+
base64-js@1.5.1:
|
| 281 |
+
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
| 282 |
+
|
| 283 |
+
body-parser@1.20.5:
|
| 284 |
+
resolution: {integrity: sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==}
|
| 285 |
+
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
|
| 286 |
+
|
| 287 |
+
buffer@6.0.3:
|
| 288 |
+
resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
|
| 289 |
+
|
| 290 |
+
bytes@3.1.2:
|
| 291 |
+
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
|
| 292 |
+
engines: {node: '>= 0.8'}
|
| 293 |
+
|
| 294 |
+
call-bind-apply-helpers@1.0.2:
|
| 295 |
+
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
|
| 296 |
+
engines: {node: '>= 0.4'}
|
| 297 |
+
|
| 298 |
+
call-bound@1.0.4:
|
| 299 |
+
resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==}
|
| 300 |
+
engines: {node: '>= 0.4'}
|
| 301 |
+
|
| 302 |
+
colorette@2.0.20:
|
| 303 |
+
resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
|
| 304 |
+
|
| 305 |
+
combined-stream@1.0.8:
|
| 306 |
+
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
|
| 307 |
+
engines: {node: '>= 0.8'}
|
| 308 |
+
|
| 309 |
+
content-disposition@0.5.4:
|
| 310 |
+
resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==}
|
| 311 |
+
engines: {node: '>= 0.6'}
|
| 312 |
+
|
| 313 |
+
content-type@1.0.5:
|
| 314 |
+
resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==}
|
| 315 |
+
engines: {node: '>= 0.6'}
|
| 316 |
+
|
| 317 |
+
cookie-signature@1.0.7:
|
| 318 |
+
resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==}
|
| 319 |
+
|
| 320 |
+
cookie@0.7.2:
|
| 321 |
+
resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
|
| 322 |
+
engines: {node: '>= 0.6'}
|
| 323 |
+
|
| 324 |
+
cors@2.8.6:
|
| 325 |
+
resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==}
|
| 326 |
+
engines: {node: '>= 0.10'}
|
| 327 |
+
|
| 328 |
+
dateformat@4.6.3:
|
| 329 |
+
resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==}
|
| 330 |
+
|
| 331 |
+
debug@2.6.9:
|
| 332 |
+
resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
|
| 333 |
+
peerDependencies:
|
| 334 |
+
supports-color: '*'
|
| 335 |
+
peerDependenciesMeta:
|
| 336 |
+
supports-color:
|
| 337 |
+
optional: true
|
| 338 |
+
|
| 339 |
+
delayed-stream@1.0.0:
|
| 340 |
+
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
| 341 |
+
engines: {node: '>=0.4.0'}
|
| 342 |
+
|
| 343 |
+
depd@2.0.0:
|
| 344 |
+
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
|
| 345 |
+
engines: {node: '>= 0.8'}
|
| 346 |
+
|
| 347 |
+
destroy@1.2.0:
|
| 348 |
+
resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==}
|
| 349 |
+
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
|
| 350 |
+
|
| 351 |
+
dotenv@16.6.1:
|
| 352 |
+
resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==}
|
| 353 |
+
engines: {node: '>=12'}
|
| 354 |
+
|
| 355 |
+
dunder-proto@1.0.1:
|
| 356 |
+
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
| 357 |
+
engines: {node: '>= 0.4'}
|
| 358 |
+
|
| 359 |
+
ee-first@1.1.1:
|
| 360 |
+
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
|
| 361 |
+
|
| 362 |
+
encodeurl@2.0.0:
|
| 363 |
+
resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==}
|
| 364 |
+
engines: {node: '>= 0.8'}
|
| 365 |
+
|
| 366 |
+
end-of-stream@1.4.5:
|
| 367 |
+
resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==}
|
| 368 |
+
|
| 369 |
+
es-define-property@1.0.1:
|
| 370 |
+
resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
|
| 371 |
+
engines: {node: '>= 0.4'}
|
| 372 |
+
|
| 373 |
+
es-errors@1.3.0:
|
| 374 |
+
resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
|
| 375 |
+
engines: {node: '>= 0.4'}
|
| 376 |
+
|
| 377 |
+
es-object-atoms@1.1.1:
|
| 378 |
+
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
|
| 379 |
+
engines: {node: '>= 0.4'}
|
| 380 |
+
|
| 381 |
+
es-set-tostringtag@2.1.0:
|
| 382 |
+
resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
|
| 383 |
+
engines: {node: '>= 0.4'}
|
| 384 |
+
|
| 385 |
+
esbuild@0.27.7:
|
| 386 |
+
resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==}
|
| 387 |
+
engines: {node: '>=18'}
|
| 388 |
+
hasBin: true
|
| 389 |
+
|
| 390 |
+
escape-html@1.0.3:
|
| 391 |
+
resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
|
| 392 |
+
|
| 393 |
+
etag@1.8.1:
|
| 394 |
+
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
|
| 395 |
+
engines: {node: '>= 0.6'}
|
| 396 |
+
|
| 397 |
+
event-target-shim@5.0.1:
|
| 398 |
+
resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==}
|
| 399 |
+
engines: {node: '>=6'}
|
| 400 |
+
|
| 401 |
+
events@3.3.0:
|
| 402 |
+
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
|
| 403 |
+
engines: {node: '>=0.8.x'}
|
| 404 |
+
|
| 405 |
+
express-rate-limit@7.5.1:
|
| 406 |
+
resolution: {integrity: sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==}
|
| 407 |
+
engines: {node: '>= 16'}
|
| 408 |
+
peerDependencies:
|
| 409 |
+
express: '>= 4.11'
|
| 410 |
+
|
| 411 |
+
express@4.22.1:
|
| 412 |
+
resolution: {integrity: sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==}
|
| 413 |
+
engines: {node: '>= 0.10.0'}
|
| 414 |
+
|
| 415 |
+
fast-copy@3.0.2:
|
| 416 |
+
resolution: {integrity: sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==}
|
| 417 |
+
|
| 418 |
+
fast-redact@3.5.0:
|
| 419 |
+
resolution: {integrity: sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==}
|
| 420 |
+
engines: {node: '>=6'}
|
| 421 |
+
|
| 422 |
+
fast-safe-stringify@2.1.1:
|
| 423 |
+
resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==}
|
| 424 |
+
|
| 425 |
+
finalhandler@1.3.2:
|
| 426 |
+
resolution: {integrity: sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==}
|
| 427 |
+
engines: {node: '>= 0.8'}
|
| 428 |
+
|
| 429 |
+
form-data-encoder@1.7.2:
|
| 430 |
+
resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==}
|
| 431 |
+
|
| 432 |
+
form-data@4.0.5:
|
| 433 |
+
resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
|
| 434 |
+
engines: {node: '>= 6'}
|
| 435 |
+
|
| 436 |
+
formdata-node@4.4.1:
|
| 437 |
+
resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==}
|
| 438 |
+
engines: {node: '>= 12.20'}
|
| 439 |
+
|
| 440 |
+
forwarded@0.2.0:
|
| 441 |
+
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
|
| 442 |
+
engines: {node: '>= 0.6'}
|
| 443 |
+
|
| 444 |
+
fresh@0.5.2:
|
| 445 |
+
resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==}
|
| 446 |
+
engines: {node: '>= 0.6'}
|
| 447 |
+
|
| 448 |
+
fsevents@2.3.3:
|
| 449 |
+
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
| 450 |
+
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
| 451 |
+
os: [darwin]
|
| 452 |
+
|
| 453 |
+
function-bind@1.1.2:
|
| 454 |
+
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
|
| 455 |
+
|
| 456 |
+
get-intrinsic@1.3.0:
|
| 457 |
+
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
|
| 458 |
+
engines: {node: '>= 0.4'}
|
| 459 |
+
|
| 460 |
+
get-proto@1.0.1:
|
| 461 |
+
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
|
| 462 |
+
engines: {node: '>= 0.4'}
|
| 463 |
+
|
| 464 |
+
get-tsconfig@4.14.0:
|
| 465 |
+
resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==}
|
| 466 |
+
|
| 467 |
+
gopd@1.2.0:
|
| 468 |
+
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
|
| 469 |
+
engines: {node: '>= 0.4'}
|
| 470 |
+
|
| 471 |
+
has-symbols@1.1.0:
|
| 472 |
+
resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
|
| 473 |
+
engines: {node: '>= 0.4'}
|
| 474 |
+
|
| 475 |
+
has-tostringtag@1.0.2:
|
| 476 |
+
resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
|
| 477 |
+
engines: {node: '>= 0.4'}
|
| 478 |
+
|
| 479 |
+
hasown@2.0.3:
|
| 480 |
+
resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==}
|
| 481 |
+
engines: {node: '>= 0.4'}
|
| 482 |
+
|
| 483 |
+
helmet@7.2.0:
|
| 484 |
+
resolution: {integrity: sha512-ZRiwvN089JfMXokizgqEPXsl2Guk094yExfoDXR0cBYWxtBbaSww/w+vT4WEJsBW2iTUi1GgZ6swmoug3Oy4Xw==}
|
| 485 |
+
engines: {node: '>=16.0.0'}
|
| 486 |
+
|
| 487 |
+
help-me@5.0.0:
|
| 488 |
+
resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==}
|
| 489 |
+
|
| 490 |
+
http-errors@2.0.1:
|
| 491 |
+
resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==}
|
| 492 |
+
engines: {node: '>= 0.8'}
|
| 493 |
+
|
| 494 |
+
humanize-ms@1.2.1:
|
| 495 |
+
resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==}
|
| 496 |
+
|
| 497 |
+
iconv-lite@0.4.24:
|
| 498 |
+
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
|
| 499 |
+
engines: {node: '>=0.10.0'}
|
| 500 |
+
|
| 501 |
+
ieee754@1.2.1:
|
| 502 |
+
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
|
| 503 |
+
|
| 504 |
+
inherits@2.0.4:
|
| 505 |
+
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
| 506 |
+
|
| 507 |
+
ipaddr.js@1.9.1:
|
| 508 |
+
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
|
| 509 |
+
engines: {node: '>= 0.10'}
|
| 510 |
+
|
| 511 |
+
joycon@3.1.1:
|
| 512 |
+
resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==}
|
| 513 |
+
engines: {node: '>=10'}
|
| 514 |
+
|
| 515 |
+
math-intrinsics@1.1.0:
|
| 516 |
+
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
| 517 |
+
engines: {node: '>= 0.4'}
|
| 518 |
+
|
| 519 |
+
media-typer@0.3.0:
|
| 520 |
+
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
|
| 521 |
+
engines: {node: '>= 0.6'}
|
| 522 |
+
|
| 523 |
+
merge-descriptors@1.0.3:
|
| 524 |
+
resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==}
|
| 525 |
+
|
| 526 |
+
methods@1.1.2:
|
| 527 |
+
resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==}
|
| 528 |
+
engines: {node: '>= 0.6'}
|
| 529 |
+
|
| 530 |
+
mime-db@1.52.0:
|
| 531 |
+
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
|
| 532 |
+
engines: {node: '>= 0.6'}
|
| 533 |
+
|
| 534 |
+
mime-types@2.1.35:
|
| 535 |
+
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
|
| 536 |
+
engines: {node: '>= 0.6'}
|
| 537 |
+
|
| 538 |
+
mime@1.6.0:
|
| 539 |
+
resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==}
|
| 540 |
+
engines: {node: '>=4'}
|
| 541 |
+
hasBin: true
|
| 542 |
+
|
| 543 |
+
minimist@1.2.8:
|
| 544 |
+
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
|
| 545 |
+
|
| 546 |
+
ms@2.0.0:
|
| 547 |
+
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
|
| 548 |
+
|
| 549 |
+
ms@2.1.3:
|
| 550 |
+
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
| 551 |
+
|
| 552 |
+
negotiator@0.6.3:
|
| 553 |
+
resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
|
| 554 |
+
engines: {node: '>= 0.6'}
|
| 555 |
+
|
| 556 |
+
node-domexception@1.0.0:
|
| 557 |
+
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
|
| 558 |
+
engines: {node: '>=10.5.0'}
|
| 559 |
+
deprecated: Use your platform's native DOMException instead
|
| 560 |
+
|
| 561 |
+
node-fetch@2.7.0:
|
| 562 |
+
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
|
| 563 |
+
engines: {node: 4.x || >=6.0.0}
|
| 564 |
+
peerDependencies:
|
| 565 |
+
encoding: ^0.1.0
|
| 566 |
+
peerDependenciesMeta:
|
| 567 |
+
encoding:
|
| 568 |
+
optional: true
|
| 569 |
+
|
| 570 |
+
object-assign@4.1.1:
|
| 571 |
+
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
|
| 572 |
+
engines: {node: '>=0.10.0'}
|
| 573 |
+
|
| 574 |
+
object-inspect@1.13.4:
|
| 575 |
+
resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
|
| 576 |
+
engines: {node: '>= 0.4'}
|
| 577 |
+
|
| 578 |
+
on-exit-leak-free@2.1.2:
|
| 579 |
+
resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==}
|
| 580 |
+
engines: {node: '>=14.0.0'}
|
| 581 |
+
|
| 582 |
+
on-finished@2.4.1:
|
| 583 |
+
resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
|
| 584 |
+
engines: {node: '>= 0.8'}
|
| 585 |
+
|
| 586 |
+
once@1.4.0:
|
| 587 |
+
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
| 588 |
+
|
| 589 |
+
openai@4.104.0:
|
| 590 |
+
resolution: {integrity: sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA==}
|
| 591 |
+
hasBin: true
|
| 592 |
+
peerDependencies:
|
| 593 |
+
ws: ^8.18.0
|
| 594 |
+
zod: ^3.23.8
|
| 595 |
+
peerDependenciesMeta:
|
| 596 |
+
ws:
|
| 597 |
+
optional: true
|
| 598 |
+
zod:
|
| 599 |
+
optional: true
|
| 600 |
+
|
| 601 |
+
parseurl@1.3.3:
|
| 602 |
+
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
|
| 603 |
+
engines: {node: '>= 0.8'}
|
| 604 |
+
|
| 605 |
+
path-to-regexp@0.1.13:
|
| 606 |
+
resolution: {integrity: sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==}
|
| 607 |
+
|
| 608 |
+
pino-abstract-transport@1.2.0:
|
| 609 |
+
resolution: {integrity: sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q==}
|
| 610 |
+
|
| 611 |
+
pino-pretty@10.3.1:
|
| 612 |
+
resolution: {integrity: sha512-az8JbIYeN/1iLj2t0jR9DV48/LQ3RC6hZPpapKPkb84Q+yTidMCpgWxIT3N0flnBDilyBQ1luWNpOeJptjdp/g==}
|
| 613 |
+
hasBin: true
|
| 614 |
+
|
| 615 |
+
pino-std-serializers@6.2.2:
|
| 616 |
+
resolution: {integrity: sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==}
|
| 617 |
+
|
| 618 |
+
pino@8.21.0:
|
| 619 |
+
resolution: {integrity: sha512-ip4qdzjkAyDDZklUaZkcRFb2iA118H9SgRh8yzTkSQK8HilsOJF7rSY8HoW5+I0M46AZgX/pxbprf2vvzQCE0Q==}
|
| 620 |
+
hasBin: true
|
| 621 |
+
|
| 622 |
+
process-warning@3.0.0:
|
| 623 |
+
resolution: {integrity: sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==}
|
| 624 |
+
|
| 625 |
+
process@0.11.10:
|
| 626 |
+
resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==}
|
| 627 |
+
engines: {node: '>= 0.6.0'}
|
| 628 |
+
|
| 629 |
+
proxy-addr@2.0.7:
|
| 630 |
+
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
|
| 631 |
+
engines: {node: '>= 0.10'}
|
| 632 |
+
|
| 633 |
+
pump@3.0.4:
|
| 634 |
+
resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==}
|
| 635 |
+
|
| 636 |
+
qs@6.14.2:
|
| 637 |
+
resolution: {integrity: sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==}
|
| 638 |
+
engines: {node: '>=0.6'}
|
| 639 |
+
|
| 640 |
+
qs@6.15.1:
|
| 641 |
+
resolution: {integrity: sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==}
|
| 642 |
+
engines: {node: '>=0.6'}
|
| 643 |
+
|
| 644 |
+
quick-format-unescaped@4.0.4:
|
| 645 |
+
resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==}
|
| 646 |
+
|
| 647 |
+
range-parser@1.2.1:
|
| 648 |
+
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
|
| 649 |
+
engines: {node: '>= 0.6'}
|
| 650 |
+
|
| 651 |
+
raw-body@2.5.3:
|
| 652 |
+
resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==}
|
| 653 |
+
engines: {node: '>= 0.8'}
|
| 654 |
+
|
| 655 |
+
readable-stream@4.7.0:
|
| 656 |
+
resolution: {integrity: sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==}
|
| 657 |
+
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
| 658 |
+
|
| 659 |
+
real-require@0.2.0:
|
| 660 |
+
resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==}
|
| 661 |
+
engines: {node: '>= 12.13.0'}
|
| 662 |
+
|
| 663 |
+
resolve-pkg-maps@1.0.0:
|
| 664 |
+
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
|
| 665 |
+
|
| 666 |
+
safe-buffer@5.2.1:
|
| 667 |
+
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
|
| 668 |
+
|
| 669 |
+
safe-stable-stringify@2.5.0:
|
| 670 |
+
resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==}
|
| 671 |
+
engines: {node: '>=10'}
|
| 672 |
+
|
| 673 |
+
safer-buffer@2.1.2:
|
| 674 |
+
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
| 675 |
+
|
| 676 |
+
secure-json-parse@2.7.0:
|
| 677 |
+
resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==}
|
| 678 |
+
|
| 679 |
+
send@0.19.2:
|
| 680 |
+
resolution: {integrity: sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==}
|
| 681 |
+
engines: {node: '>= 0.8.0'}
|
| 682 |
+
|
| 683 |
+
serve-static@1.16.3:
|
| 684 |
+
resolution: {integrity: sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==}
|
| 685 |
+
engines: {node: '>= 0.8.0'}
|
| 686 |
+
|
| 687 |
+
setprototypeof@1.2.0:
|
| 688 |
+
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
|
| 689 |
+
|
| 690 |
+
side-channel-list@1.0.1:
|
| 691 |
+
resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==}
|
| 692 |
+
engines: {node: '>= 0.4'}
|
| 693 |
+
|
| 694 |
+
side-channel-map@1.0.1:
|
| 695 |
+
resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==}
|
| 696 |
+
engines: {node: '>= 0.4'}
|
| 697 |
+
|
| 698 |
+
side-channel-weakmap@1.0.2:
|
| 699 |
+
resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==}
|
| 700 |
+
engines: {node: '>= 0.4'}
|
| 701 |
+
|
| 702 |
+
side-channel@1.1.0:
|
| 703 |
+
resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==}
|
| 704 |
+
engines: {node: '>= 0.4'}
|
| 705 |
+
|
| 706 |
+
sonic-boom@3.8.1:
|
| 707 |
+
resolution: {integrity: sha512-y4Z8LCDBuum+PBP3lSV7RHrXscqksve/bi0as7mhwVnBW+/wUqKT/2Kb7um8yqcFy0duYbbPxzt89Zy2nOCaxg==}
|
| 708 |
+
|
| 709 |
+
split2@4.2.0:
|
| 710 |
+
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
|
| 711 |
+
engines: {node: '>= 10.x'}
|
| 712 |
+
|
| 713 |
+
statuses@2.0.2:
|
| 714 |
+
resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
|
| 715 |
+
engines: {node: '>= 0.8'}
|
| 716 |
+
|
| 717 |
+
string_decoder@1.3.0:
|
| 718 |
+
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
|
| 719 |
+
|
| 720 |
+
strip-json-comments@3.1.1:
|
| 721 |
+
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
|
| 722 |
+
engines: {node: '>=8'}
|
| 723 |
+
|
| 724 |
+
thread-stream@2.7.0:
|
| 725 |
+
resolution: {integrity: sha512-qQiRWsU/wvNolI6tbbCKd9iKaTnCXsTwVxhhKM6nctPdujTyztjlbUkUTUymidWcMnZ5pWR0ej4a0tjsW021vw==}
|
| 726 |
+
|
| 727 |
+
toidentifier@1.0.1:
|
| 728 |
+
resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
|
| 729 |
+
engines: {node: '>=0.6'}
|
| 730 |
+
|
| 731 |
+
tr46@0.0.3:
|
| 732 |
+
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
|
| 733 |
+
|
| 734 |
+
tsx@4.21.0:
|
| 735 |
+
resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==}
|
| 736 |
+
engines: {node: '>=18.0.0'}
|
| 737 |
+
hasBin: true
|
| 738 |
+
|
| 739 |
+
type-is@1.6.18:
|
| 740 |
+
resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
|
| 741 |
+
engines: {node: '>= 0.6'}
|
| 742 |
+
|
| 743 |
+
typescript@5.9.3:
|
| 744 |
+
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
|
| 745 |
+
engines: {node: '>=14.17'}
|
| 746 |
+
hasBin: true
|
| 747 |
+
|
| 748 |
+
undici-types@5.26.5:
|
| 749 |
+
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
|
| 750 |
+
|
| 751 |
+
undici-types@6.21.0:
|
| 752 |
+
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
| 753 |
+
|
| 754 |
+
unpipe@1.0.0:
|
| 755 |
+
resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
|
| 756 |
+
engines: {node: '>= 0.8'}
|
| 757 |
+
|
| 758 |
+
utils-merge@1.0.1:
|
| 759 |
+
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
|
| 760 |
+
engines: {node: '>= 0.4.0'}
|
| 761 |
+
|
| 762 |
+
vary@1.1.2:
|
| 763 |
+
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
|
| 764 |
+
engines: {node: '>= 0.8'}
|
| 765 |
+
|
| 766 |
+
web-streams-polyfill@4.0.0-beta.3:
|
| 767 |
+
resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==}
|
| 768 |
+
engines: {node: '>= 14'}
|
| 769 |
+
|
| 770 |
+
webidl-conversions@3.0.1:
|
| 771 |
+
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
| 772 |
+
|
| 773 |
+
whatwg-url@5.0.0:
|
| 774 |
+
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
|
| 775 |
+
|
| 776 |
+
wrappy@1.0.2:
|
| 777 |
+
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
| 778 |
+
|
| 779 |
+
zod@3.25.76:
|
| 780 |
+
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
|
| 781 |
+
|
| 782 |
+
snapshots:
|
| 783 |
+
|
| 784 |
+
'@esbuild/aix-ppc64@0.27.7':
|
| 785 |
+
optional: true
|
| 786 |
+
|
| 787 |
+
'@esbuild/android-arm64@0.27.7':
|
| 788 |
+
optional: true
|
| 789 |
+
|
| 790 |
+
'@esbuild/android-arm@0.27.7':
|
| 791 |
+
optional: true
|
| 792 |
+
|
| 793 |
+
'@esbuild/android-x64@0.27.7':
|
| 794 |
+
optional: true
|
| 795 |
+
|
| 796 |
+
'@esbuild/darwin-arm64@0.27.7':
|
| 797 |
+
optional: true
|
| 798 |
+
|
| 799 |
+
'@esbuild/darwin-x64@0.27.7':
|
| 800 |
+
optional: true
|
| 801 |
+
|
| 802 |
+
'@esbuild/freebsd-arm64@0.27.7':
|
| 803 |
+
optional: true
|
| 804 |
+
|
| 805 |
+
'@esbuild/freebsd-x64@0.27.7':
|
| 806 |
+
optional: true
|
| 807 |
+
|
| 808 |
+
'@esbuild/linux-arm64@0.27.7':
|
| 809 |
+
optional: true
|
| 810 |
+
|
| 811 |
+
'@esbuild/linux-arm@0.27.7':
|
| 812 |
+
optional: true
|
| 813 |
+
|
| 814 |
+
'@esbuild/linux-ia32@0.27.7':
|
| 815 |
+
optional: true
|
| 816 |
+
|
| 817 |
+
'@esbuild/linux-loong64@0.27.7':
|
| 818 |
+
optional: true
|
| 819 |
+
|
| 820 |
+
'@esbuild/linux-mips64el@0.27.7':
|
| 821 |
+
optional: true
|
| 822 |
+
|
| 823 |
+
'@esbuild/linux-ppc64@0.27.7':
|
| 824 |
+
optional: true
|
| 825 |
+
|
| 826 |
+
'@esbuild/linux-riscv64@0.27.7':
|
| 827 |
+
optional: true
|
| 828 |
+
|
| 829 |
+
'@esbuild/linux-s390x@0.27.7':
|
| 830 |
+
optional: true
|
| 831 |
+
|
| 832 |
+
'@esbuild/linux-x64@0.27.7':
|
| 833 |
+
optional: true
|
| 834 |
+
|
| 835 |
+
'@esbuild/netbsd-arm64@0.27.7':
|
| 836 |
+
optional: true
|
| 837 |
+
|
| 838 |
+
'@esbuild/netbsd-x64@0.27.7':
|
| 839 |
+
optional: true
|
| 840 |
+
|
| 841 |
+
'@esbuild/openbsd-arm64@0.27.7':
|
| 842 |
+
optional: true
|
| 843 |
+
|
| 844 |
+
'@esbuild/openbsd-x64@0.27.7':
|
| 845 |
+
optional: true
|
| 846 |
+
|
| 847 |
+
'@esbuild/openharmony-arm64@0.27.7':
|
| 848 |
+
optional: true
|
| 849 |
+
|
| 850 |
+
'@esbuild/sunos-x64@0.27.7':
|
| 851 |
+
optional: true
|
| 852 |
+
|
| 853 |
+
'@esbuild/win32-arm64@0.27.7':
|
| 854 |
+
optional: true
|
| 855 |
+
|
| 856 |
+
'@esbuild/win32-ia32@0.27.7':
|
| 857 |
+
optional: true
|
| 858 |
+
|
| 859 |
+
'@esbuild/win32-x64@0.27.7':
|
| 860 |
+
optional: true
|
| 861 |
+
|
| 862 |
+
'@types/body-parser@1.19.6':
|
| 863 |
+
dependencies:
|
| 864 |
+
'@types/connect': 3.4.38
|
| 865 |
+
'@types/node': 20.19.40
|
| 866 |
+
|
| 867 |
+
'@types/connect@3.4.38':
|
| 868 |
+
dependencies:
|
| 869 |
+
'@types/node': 20.19.40
|
| 870 |
+
|
| 871 |
+
'@types/cors@2.8.19':
|
| 872 |
+
dependencies:
|
| 873 |
+
'@types/node': 20.19.40
|
| 874 |
+
|
| 875 |
+
'@types/express-serve-static-core@4.19.8':
|
| 876 |
+
dependencies:
|
| 877 |
+
'@types/node': 20.19.40
|
| 878 |
+
'@types/qs': 6.15.1
|
| 879 |
+
'@types/range-parser': 1.2.7
|
| 880 |
+
'@types/send': 1.2.1
|
| 881 |
+
|
| 882 |
+
'@types/express@4.17.25':
|
| 883 |
+
dependencies:
|
| 884 |
+
'@types/body-parser': 1.19.6
|
| 885 |
+
'@types/express-serve-static-core': 4.19.8
|
| 886 |
+
'@types/qs': 6.15.1
|
| 887 |
+
'@types/serve-static': 1.15.10
|
| 888 |
+
|
| 889 |
+
'@types/http-errors@2.0.5': {}
|
| 890 |
+
|
| 891 |
+
'@types/mime@1.3.5': {}
|
| 892 |
+
|
| 893 |
+
'@types/node-fetch@2.6.13':
|
| 894 |
+
dependencies:
|
| 895 |
+
'@types/node': 20.19.40
|
| 896 |
+
form-data: 4.0.5
|
| 897 |
+
|
| 898 |
+
'@types/node@18.19.130':
|
| 899 |
+
dependencies:
|
| 900 |
+
undici-types: 5.26.5
|
| 901 |
+
|
| 902 |
+
'@types/node@20.19.40':
|
| 903 |
+
dependencies:
|
| 904 |
+
undici-types: 6.21.0
|
| 905 |
+
|
| 906 |
+
'@types/qs@6.15.1': {}
|
| 907 |
+
|
| 908 |
+
'@types/range-parser@1.2.7': {}
|
| 909 |
+
|
| 910 |
+
'@types/send@0.17.6':
|
| 911 |
+
dependencies:
|
| 912 |
+
'@types/mime': 1.3.5
|
| 913 |
+
'@types/node': 20.19.40
|
| 914 |
+
|
| 915 |
+
'@types/send@1.2.1':
|
| 916 |
+
dependencies:
|
| 917 |
+
'@types/node': 20.19.40
|
| 918 |
+
|
| 919 |
+
'@types/serve-static@1.15.10':
|
| 920 |
+
dependencies:
|
| 921 |
+
'@types/http-errors': 2.0.5
|
| 922 |
+
'@types/node': 20.19.40
|
| 923 |
+
'@types/send': 0.17.6
|
| 924 |
+
|
| 925 |
+
abort-controller@3.0.0:
|
| 926 |
+
dependencies:
|
| 927 |
+
event-target-shim: 5.0.1
|
| 928 |
+
|
| 929 |
+
accepts@1.3.8:
|
| 930 |
+
dependencies:
|
| 931 |
+
mime-types: 2.1.35
|
| 932 |
+
negotiator: 0.6.3
|
| 933 |
+
|
| 934 |
+
agentkeepalive@4.6.0:
|
| 935 |
+
dependencies:
|
| 936 |
+
humanize-ms: 1.2.1
|
| 937 |
+
|
| 938 |
+
array-flatten@1.1.1: {}
|
| 939 |
+
|
| 940 |
+
asynckit@0.4.0: {}
|
| 941 |
+
|
| 942 |
+
atomic-sleep@1.0.0: {}
|
| 943 |
+
|
| 944 |
+
base64-js@1.5.1: {}
|
| 945 |
+
|
| 946 |
+
body-parser@1.20.5:
|
| 947 |
+
dependencies:
|
| 948 |
+
bytes: 3.1.2
|
| 949 |
+
content-type: 1.0.5
|
| 950 |
+
debug: 2.6.9
|
| 951 |
+
depd: 2.0.0
|
| 952 |
+
destroy: 1.2.0
|
| 953 |
+
http-errors: 2.0.1
|
| 954 |
+
iconv-lite: 0.4.24
|
| 955 |
+
on-finished: 2.4.1
|
| 956 |
+
qs: 6.15.1
|
| 957 |
+
raw-body: 2.5.3
|
| 958 |
+
type-is: 1.6.18
|
| 959 |
+
unpipe: 1.0.0
|
| 960 |
+
transitivePeerDependencies:
|
| 961 |
+
- supports-color
|
| 962 |
+
|
| 963 |
+
buffer@6.0.3:
|
| 964 |
+
dependencies:
|
| 965 |
+
base64-js: 1.5.1
|
| 966 |
+
ieee754: 1.2.1
|
| 967 |
+
|
| 968 |
+
bytes@3.1.2: {}
|
| 969 |
+
|
| 970 |
+
call-bind-apply-helpers@1.0.2:
|
| 971 |
+
dependencies:
|
| 972 |
+
es-errors: 1.3.0
|
| 973 |
+
function-bind: 1.1.2
|
| 974 |
+
|
| 975 |
+
call-bound@1.0.4:
|
| 976 |
+
dependencies:
|
| 977 |
+
call-bind-apply-helpers: 1.0.2
|
| 978 |
+
get-intrinsic: 1.3.0
|
| 979 |
+
|
| 980 |
+
colorette@2.0.20: {}
|
| 981 |
+
|
| 982 |
+
combined-stream@1.0.8:
|
| 983 |
+
dependencies:
|
| 984 |
+
delayed-stream: 1.0.0
|
| 985 |
+
|
| 986 |
+
content-disposition@0.5.4:
|
| 987 |
+
dependencies:
|
| 988 |
+
safe-buffer: 5.2.1
|
| 989 |
+
|
| 990 |
+
content-type@1.0.5: {}
|
| 991 |
+
|
| 992 |
+
cookie-signature@1.0.7: {}
|
| 993 |
+
|
| 994 |
+
cookie@0.7.2: {}
|
| 995 |
+
|
| 996 |
+
cors@2.8.6:
|
| 997 |
+
dependencies:
|
| 998 |
+
object-assign: 4.1.1
|
| 999 |
+
vary: 1.1.2
|
| 1000 |
+
|
| 1001 |
+
dateformat@4.6.3: {}
|
| 1002 |
+
|
| 1003 |
+
debug@2.6.9:
|
| 1004 |
+
dependencies:
|
| 1005 |
+
ms: 2.0.0
|
| 1006 |
+
|
| 1007 |
+
delayed-stream@1.0.0: {}
|
| 1008 |
+
|
| 1009 |
+
depd@2.0.0: {}
|
| 1010 |
+
|
| 1011 |
+
destroy@1.2.0: {}
|
| 1012 |
+
|
| 1013 |
+
dotenv@16.6.1: {}
|
| 1014 |
+
|
| 1015 |
+
dunder-proto@1.0.1:
|
| 1016 |
+
dependencies:
|
| 1017 |
+
call-bind-apply-helpers: 1.0.2
|
| 1018 |
+
es-errors: 1.3.0
|
| 1019 |
+
gopd: 1.2.0
|
| 1020 |
+
|
| 1021 |
+
ee-first@1.1.1: {}
|
| 1022 |
+
|
| 1023 |
+
encodeurl@2.0.0: {}
|
| 1024 |
+
|
| 1025 |
+
end-of-stream@1.4.5:
|
| 1026 |
+
dependencies:
|
| 1027 |
+
once: 1.4.0
|
| 1028 |
+
|
| 1029 |
+
es-define-property@1.0.1: {}
|
| 1030 |
+
|
| 1031 |
+
es-errors@1.3.0: {}
|
| 1032 |
+
|
| 1033 |
+
es-object-atoms@1.1.1:
|
| 1034 |
+
dependencies:
|
| 1035 |
+
es-errors: 1.3.0
|
| 1036 |
+
|
| 1037 |
+
es-set-tostringtag@2.1.0:
|
| 1038 |
+
dependencies:
|
| 1039 |
+
es-errors: 1.3.0
|
| 1040 |
+
get-intrinsic: 1.3.0
|
| 1041 |
+
has-tostringtag: 1.0.2
|
| 1042 |
+
hasown: 2.0.3
|
| 1043 |
+
|
| 1044 |
+
esbuild@0.27.7:
|
| 1045 |
+
optionalDependencies:
|
| 1046 |
+
'@esbuild/aix-ppc64': 0.27.7
|
| 1047 |
+
'@esbuild/android-arm': 0.27.7
|
| 1048 |
+
'@esbuild/android-arm64': 0.27.7
|
| 1049 |
+
'@esbuild/android-x64': 0.27.7
|
| 1050 |
+
'@esbuild/darwin-arm64': 0.27.7
|
| 1051 |
+
'@esbuild/darwin-x64': 0.27.7
|
| 1052 |
+
'@esbuild/freebsd-arm64': 0.27.7
|
| 1053 |
+
'@esbuild/freebsd-x64': 0.27.7
|
| 1054 |
+
'@esbuild/linux-arm': 0.27.7
|
| 1055 |
+
'@esbuild/linux-arm64': 0.27.7
|
| 1056 |
+
'@esbuild/linux-ia32': 0.27.7
|
| 1057 |
+
'@esbuild/linux-loong64': 0.27.7
|
| 1058 |
+
'@esbuild/linux-mips64el': 0.27.7
|
| 1059 |
+
'@esbuild/linux-ppc64': 0.27.7
|
| 1060 |
+
'@esbuild/linux-riscv64': 0.27.7
|
| 1061 |
+
'@esbuild/linux-s390x': 0.27.7
|
| 1062 |
+
'@esbuild/linux-x64': 0.27.7
|
| 1063 |
+
'@esbuild/netbsd-arm64': 0.27.7
|
| 1064 |
+
'@esbuild/netbsd-x64': 0.27.7
|
| 1065 |
+
'@esbuild/openbsd-arm64': 0.27.7
|
| 1066 |
+
'@esbuild/openbsd-x64': 0.27.7
|
| 1067 |
+
'@esbuild/openharmony-arm64': 0.27.7
|
| 1068 |
+
'@esbuild/sunos-x64': 0.27.7
|
| 1069 |
+
'@esbuild/win32-arm64': 0.27.7
|
| 1070 |
+
'@esbuild/win32-ia32': 0.27.7
|
| 1071 |
+
'@esbuild/win32-x64': 0.27.7
|
| 1072 |
+
|
| 1073 |
+
escape-html@1.0.3: {}
|
| 1074 |
+
|
| 1075 |
+
etag@1.8.1: {}
|
| 1076 |
+
|
| 1077 |
+
event-target-shim@5.0.1: {}
|
| 1078 |
+
|
| 1079 |
+
events@3.3.0: {}
|
| 1080 |
+
|
| 1081 |
+
express-rate-limit@7.5.1(express@4.22.1):
|
| 1082 |
+
dependencies:
|
| 1083 |
+
express: 4.22.1
|
| 1084 |
+
|
| 1085 |
+
express@4.22.1:
|
| 1086 |
+
dependencies:
|
| 1087 |
+
accepts: 1.3.8
|
| 1088 |
+
array-flatten: 1.1.1
|
| 1089 |
+
body-parser: 1.20.5
|
| 1090 |
+
content-disposition: 0.5.4
|
| 1091 |
+
content-type: 1.0.5
|
| 1092 |
+
cookie: 0.7.2
|
| 1093 |
+
cookie-signature: 1.0.7
|
| 1094 |
+
debug: 2.6.9
|
| 1095 |
+
depd: 2.0.0
|
| 1096 |
+
encodeurl: 2.0.0
|
| 1097 |
+
escape-html: 1.0.3
|
| 1098 |
+
etag: 1.8.1
|
| 1099 |
+
finalhandler: 1.3.2
|
| 1100 |
+
fresh: 0.5.2
|
| 1101 |
+
http-errors: 2.0.1
|
| 1102 |
+
merge-descriptors: 1.0.3
|
| 1103 |
+
methods: 1.1.2
|
| 1104 |
+
on-finished: 2.4.1
|
| 1105 |
+
parseurl: 1.3.3
|
| 1106 |
+
path-to-regexp: 0.1.13
|
| 1107 |
+
proxy-addr: 2.0.7
|
| 1108 |
+
qs: 6.14.2
|
| 1109 |
+
range-parser: 1.2.1
|
| 1110 |
+
safe-buffer: 5.2.1
|
| 1111 |
+
send: 0.19.2
|
| 1112 |
+
serve-static: 1.16.3
|
| 1113 |
+
setprototypeof: 1.2.0
|
| 1114 |
+
statuses: 2.0.2
|
| 1115 |
+
type-is: 1.6.18
|
| 1116 |
+
utils-merge: 1.0.1
|
| 1117 |
+
vary: 1.1.2
|
| 1118 |
+
transitivePeerDependencies:
|
| 1119 |
+
- supports-color
|
| 1120 |
+
|
| 1121 |
+
fast-copy@3.0.2: {}
|
| 1122 |
+
|
| 1123 |
+
fast-redact@3.5.0: {}
|
| 1124 |
+
|
| 1125 |
+
fast-safe-stringify@2.1.1: {}
|
| 1126 |
+
|
| 1127 |
+
finalhandler@1.3.2:
|
| 1128 |
+
dependencies:
|
| 1129 |
+
debug: 2.6.9
|
| 1130 |
+
encodeurl: 2.0.0
|
| 1131 |
+
escape-html: 1.0.3
|
| 1132 |
+
on-finished: 2.4.1
|
| 1133 |
+
parseurl: 1.3.3
|
| 1134 |
+
statuses: 2.0.2
|
| 1135 |
+
unpipe: 1.0.0
|
| 1136 |
+
transitivePeerDependencies:
|
| 1137 |
+
- supports-color
|
| 1138 |
+
|
| 1139 |
+
form-data-encoder@1.7.2: {}
|
| 1140 |
+
|
| 1141 |
+
form-data@4.0.5:
|
| 1142 |
+
dependencies:
|
| 1143 |
+
asynckit: 0.4.0
|
| 1144 |
+
combined-stream: 1.0.8
|
| 1145 |
+
es-set-tostringtag: 2.1.0
|
| 1146 |
+
hasown: 2.0.3
|
| 1147 |
+
mime-types: 2.1.35
|
| 1148 |
+
|
| 1149 |
+
formdata-node@4.4.1:
|
| 1150 |
+
dependencies:
|
| 1151 |
+
node-domexception: 1.0.0
|
| 1152 |
+
web-streams-polyfill: 4.0.0-beta.3
|
| 1153 |
+
|
| 1154 |
+
forwarded@0.2.0: {}
|
| 1155 |
+
|
| 1156 |
+
fresh@0.5.2: {}
|
| 1157 |
+
|
| 1158 |
+
fsevents@2.3.3:
|
| 1159 |
+
optional: true
|
| 1160 |
+
|
| 1161 |
+
function-bind@1.1.2: {}
|
| 1162 |
+
|
| 1163 |
+
get-intrinsic@1.3.0:
|
| 1164 |
+
dependencies:
|
| 1165 |
+
call-bind-apply-helpers: 1.0.2
|
| 1166 |
+
es-define-property: 1.0.1
|
| 1167 |
+
es-errors: 1.3.0
|
| 1168 |
+
es-object-atoms: 1.1.1
|
| 1169 |
+
function-bind: 1.1.2
|
| 1170 |
+
get-proto: 1.0.1
|
| 1171 |
+
gopd: 1.2.0
|
| 1172 |
+
has-symbols: 1.1.0
|
| 1173 |
+
hasown: 2.0.3
|
| 1174 |
+
math-intrinsics: 1.1.0
|
| 1175 |
+
|
| 1176 |
+
get-proto@1.0.1:
|
| 1177 |
+
dependencies:
|
| 1178 |
+
dunder-proto: 1.0.1
|
| 1179 |
+
es-object-atoms: 1.1.1
|
| 1180 |
+
|
| 1181 |
+
get-tsconfig@4.14.0:
|
| 1182 |
+
dependencies:
|
| 1183 |
+
resolve-pkg-maps: 1.0.0
|
| 1184 |
+
|
| 1185 |
+
gopd@1.2.0: {}
|
| 1186 |
+
|
| 1187 |
+
has-symbols@1.1.0: {}
|
| 1188 |
+
|
| 1189 |
+
has-tostringtag@1.0.2:
|
| 1190 |
+
dependencies:
|
| 1191 |
+
has-symbols: 1.1.0
|
| 1192 |
+
|
| 1193 |
+
hasown@2.0.3:
|
| 1194 |
+
dependencies:
|
| 1195 |
+
function-bind: 1.1.2
|
| 1196 |
+
|
| 1197 |
+
helmet@7.2.0: {}
|
| 1198 |
+
|
| 1199 |
+
help-me@5.0.0: {}
|
| 1200 |
+
|
| 1201 |
+
http-errors@2.0.1:
|
| 1202 |
+
dependencies:
|
| 1203 |
+
depd: 2.0.0
|
| 1204 |
+
inherits: 2.0.4
|
| 1205 |
+
setprototypeof: 1.2.0
|
| 1206 |
+
statuses: 2.0.2
|
| 1207 |
+
toidentifier: 1.0.1
|
| 1208 |
+
|
| 1209 |
+
humanize-ms@1.2.1:
|
| 1210 |
+
dependencies:
|
| 1211 |
+
ms: 2.1.3
|
| 1212 |
+
|
| 1213 |
+
iconv-lite@0.4.24:
|
| 1214 |
+
dependencies:
|
| 1215 |
+
safer-buffer: 2.1.2
|
| 1216 |
+
|
| 1217 |
+
ieee754@1.2.1: {}
|
| 1218 |
+
|
| 1219 |
+
inherits@2.0.4: {}
|
| 1220 |
+
|
| 1221 |
+
ipaddr.js@1.9.1: {}
|
| 1222 |
+
|
| 1223 |
+
joycon@3.1.1: {}
|
| 1224 |
+
|
| 1225 |
+
math-intrinsics@1.1.0: {}
|
| 1226 |
+
|
| 1227 |
+
media-typer@0.3.0: {}
|
| 1228 |
+
|
| 1229 |
+
merge-descriptors@1.0.3: {}
|
| 1230 |
+
|
| 1231 |
+
methods@1.1.2: {}
|
| 1232 |
+
|
| 1233 |
+
mime-db@1.52.0: {}
|
| 1234 |
+
|
| 1235 |
+
mime-types@2.1.35:
|
| 1236 |
+
dependencies:
|
| 1237 |
+
mime-db: 1.52.0
|
| 1238 |
+
|
| 1239 |
+
mime@1.6.0: {}
|
| 1240 |
+
|
| 1241 |
+
minimist@1.2.8: {}
|
| 1242 |
+
|
| 1243 |
+
ms@2.0.0: {}
|
| 1244 |
+
|
| 1245 |
+
ms@2.1.3: {}
|
| 1246 |
+
|
| 1247 |
+
negotiator@0.6.3: {}
|
| 1248 |
+
|
| 1249 |
+
node-domexception@1.0.0: {}
|
| 1250 |
+
|
| 1251 |
+
node-fetch@2.7.0:
|
| 1252 |
+
dependencies:
|
| 1253 |
+
whatwg-url: 5.0.0
|
| 1254 |
+
|
| 1255 |
+
object-assign@4.1.1: {}
|
| 1256 |
+
|
| 1257 |
+
object-inspect@1.13.4: {}
|
| 1258 |
+
|
| 1259 |
+
on-exit-leak-free@2.1.2: {}
|
| 1260 |
+
|
| 1261 |
+
on-finished@2.4.1:
|
| 1262 |
+
dependencies:
|
| 1263 |
+
ee-first: 1.1.1
|
| 1264 |
+
|
| 1265 |
+
once@1.4.0:
|
| 1266 |
+
dependencies:
|
| 1267 |
+
wrappy: 1.0.2
|
| 1268 |
+
|
| 1269 |
+
openai@4.104.0(zod@3.25.76):
|
| 1270 |
+
dependencies:
|
| 1271 |
+
'@types/node': 18.19.130
|
| 1272 |
+
'@types/node-fetch': 2.6.13
|
| 1273 |
+
abort-controller: 3.0.0
|
| 1274 |
+
agentkeepalive: 4.6.0
|
| 1275 |
+
form-data-encoder: 1.7.2
|
| 1276 |
+
formdata-node: 4.4.1
|
| 1277 |
+
node-fetch: 2.7.0
|
| 1278 |
+
optionalDependencies:
|
| 1279 |
+
zod: 3.25.76
|
| 1280 |
+
transitivePeerDependencies:
|
| 1281 |
+
- encoding
|
| 1282 |
+
|
| 1283 |
+
parseurl@1.3.3: {}
|
| 1284 |
+
|
| 1285 |
+
path-to-regexp@0.1.13: {}
|
| 1286 |
+
|
| 1287 |
+
pino-abstract-transport@1.2.0:
|
| 1288 |
+
dependencies:
|
| 1289 |
+
readable-stream: 4.7.0
|
| 1290 |
+
split2: 4.2.0
|
| 1291 |
+
|
| 1292 |
+
pino-pretty@10.3.1:
|
| 1293 |
+
dependencies:
|
| 1294 |
+
colorette: 2.0.20
|
| 1295 |
+
dateformat: 4.6.3
|
| 1296 |
+
fast-copy: 3.0.2
|
| 1297 |
+
fast-safe-stringify: 2.1.1
|
| 1298 |
+
help-me: 5.0.0
|
| 1299 |
+
joycon: 3.1.1
|
| 1300 |
+
minimist: 1.2.8
|
| 1301 |
+
on-exit-leak-free: 2.1.2
|
| 1302 |
+
pino-abstract-transport: 1.2.0
|
| 1303 |
+
pump: 3.0.4
|
| 1304 |
+
readable-stream: 4.7.0
|
| 1305 |
+
secure-json-parse: 2.7.0
|
| 1306 |
+
sonic-boom: 3.8.1
|
| 1307 |
+
strip-json-comments: 3.1.1
|
| 1308 |
+
|
| 1309 |
+
pino-std-serializers@6.2.2: {}
|
| 1310 |
+
|
| 1311 |
+
pino@8.21.0:
|
| 1312 |
+
dependencies:
|
| 1313 |
+
atomic-sleep: 1.0.0
|
| 1314 |
+
fast-redact: 3.5.0
|
| 1315 |
+
on-exit-leak-free: 2.1.2
|
| 1316 |
+
pino-abstract-transport: 1.2.0
|
| 1317 |
+
pino-std-serializers: 6.2.2
|
| 1318 |
+
process-warning: 3.0.0
|
| 1319 |
+
quick-format-unescaped: 4.0.4
|
| 1320 |
+
real-require: 0.2.0
|
| 1321 |
+
safe-stable-stringify: 2.5.0
|
| 1322 |
+
sonic-boom: 3.8.1
|
| 1323 |
+
thread-stream: 2.7.0
|
| 1324 |
+
|
| 1325 |
+
process-warning@3.0.0: {}
|
| 1326 |
+
|
| 1327 |
+
process@0.11.10: {}
|
| 1328 |
+
|
| 1329 |
+
proxy-addr@2.0.7:
|
| 1330 |
+
dependencies:
|
| 1331 |
+
forwarded: 0.2.0
|
| 1332 |
+
ipaddr.js: 1.9.1
|
| 1333 |
+
|
| 1334 |
+
pump@3.0.4:
|
| 1335 |
+
dependencies:
|
| 1336 |
+
end-of-stream: 1.4.5
|
| 1337 |
+
once: 1.4.0
|
| 1338 |
+
|
| 1339 |
+
qs@6.14.2:
|
| 1340 |
+
dependencies:
|
| 1341 |
+
side-channel: 1.1.0
|
| 1342 |
+
|
| 1343 |
+
qs@6.15.1:
|
| 1344 |
+
dependencies:
|
| 1345 |
+
side-channel: 1.1.0
|
| 1346 |
+
|
| 1347 |
+
quick-format-unescaped@4.0.4: {}
|
| 1348 |
+
|
| 1349 |
+
range-parser@1.2.1: {}
|
| 1350 |
+
|
| 1351 |
+
raw-body@2.5.3:
|
| 1352 |
+
dependencies:
|
| 1353 |
+
bytes: 3.1.2
|
| 1354 |
+
http-errors: 2.0.1
|
| 1355 |
+
iconv-lite: 0.4.24
|
| 1356 |
+
unpipe: 1.0.0
|
| 1357 |
+
|
| 1358 |
+
readable-stream@4.7.0:
|
| 1359 |
+
dependencies:
|
| 1360 |
+
abort-controller: 3.0.0
|
| 1361 |
+
buffer: 6.0.3
|
| 1362 |
+
events: 3.3.0
|
| 1363 |
+
process: 0.11.10
|
| 1364 |
+
string_decoder: 1.3.0
|
| 1365 |
+
|
| 1366 |
+
real-require@0.2.0: {}
|
| 1367 |
+
|
| 1368 |
+
resolve-pkg-maps@1.0.0: {}
|
| 1369 |
+
|
| 1370 |
+
safe-buffer@5.2.1: {}
|
| 1371 |
+
|
| 1372 |
+
safe-stable-stringify@2.5.0: {}
|
| 1373 |
+
|
| 1374 |
+
safer-buffer@2.1.2: {}
|
| 1375 |
+
|
| 1376 |
+
secure-json-parse@2.7.0: {}
|
| 1377 |
+
|
| 1378 |
+
send@0.19.2:
|
| 1379 |
+
dependencies:
|
| 1380 |
+
debug: 2.6.9
|
| 1381 |
+
depd: 2.0.0
|
| 1382 |
+
destroy: 1.2.0
|
| 1383 |
+
encodeurl: 2.0.0
|
| 1384 |
+
escape-html: 1.0.3
|
| 1385 |
+
etag: 1.8.1
|
| 1386 |
+
fresh: 0.5.2
|
| 1387 |
+
http-errors: 2.0.1
|
| 1388 |
+
mime: 1.6.0
|
| 1389 |
+
ms: 2.1.3
|
| 1390 |
+
on-finished: 2.4.1
|
| 1391 |
+
range-parser: 1.2.1
|
| 1392 |
+
statuses: 2.0.2
|
| 1393 |
+
transitivePeerDependencies:
|
| 1394 |
+
- supports-color
|
| 1395 |
+
|
| 1396 |
+
serve-static@1.16.3:
|
| 1397 |
+
dependencies:
|
| 1398 |
+
encodeurl: 2.0.0
|
| 1399 |
+
escape-html: 1.0.3
|
| 1400 |
+
parseurl: 1.3.3
|
| 1401 |
+
send: 0.19.2
|
| 1402 |
+
transitivePeerDependencies:
|
| 1403 |
+
- supports-color
|
| 1404 |
+
|
| 1405 |
+
setprototypeof@1.2.0: {}
|
| 1406 |
+
|
| 1407 |
+
side-channel-list@1.0.1:
|
| 1408 |
+
dependencies:
|
| 1409 |
+
es-errors: 1.3.0
|
| 1410 |
+
object-inspect: 1.13.4
|
| 1411 |
+
|
| 1412 |
+
side-channel-map@1.0.1:
|
| 1413 |
+
dependencies:
|
| 1414 |
+
call-bound: 1.0.4
|
| 1415 |
+
es-errors: 1.3.0
|
| 1416 |
+
get-intrinsic: 1.3.0
|
| 1417 |
+
object-inspect: 1.13.4
|
| 1418 |
+
|
| 1419 |
+
side-channel-weakmap@1.0.2:
|
| 1420 |
+
dependencies:
|
| 1421 |
+
call-bound: 1.0.4
|
| 1422 |
+
es-errors: 1.3.0
|
| 1423 |
+
get-intrinsic: 1.3.0
|
| 1424 |
+
object-inspect: 1.13.4
|
| 1425 |
+
side-channel-map: 1.0.1
|
| 1426 |
+
|
| 1427 |
+
side-channel@1.1.0:
|
| 1428 |
+
dependencies:
|
| 1429 |
+
es-errors: 1.3.0
|
| 1430 |
+
object-inspect: 1.13.4
|
| 1431 |
+
side-channel-list: 1.0.1
|
| 1432 |
+
side-channel-map: 1.0.1
|
| 1433 |
+
side-channel-weakmap: 1.0.2
|
| 1434 |
+
|
| 1435 |
+
sonic-boom@3.8.1:
|
| 1436 |
+
dependencies:
|
| 1437 |
+
atomic-sleep: 1.0.0
|
| 1438 |
+
|
| 1439 |
+
split2@4.2.0: {}
|
| 1440 |
+
|
| 1441 |
+
statuses@2.0.2: {}
|
| 1442 |
+
|
| 1443 |
+
string_decoder@1.3.0:
|
| 1444 |
+
dependencies:
|
| 1445 |
+
safe-buffer: 5.2.1
|
| 1446 |
+
|
| 1447 |
+
strip-json-comments@3.1.1: {}
|
| 1448 |
+
|
| 1449 |
+
thread-stream@2.7.0:
|
| 1450 |
+
dependencies:
|
| 1451 |
+
real-require: 0.2.0
|
| 1452 |
+
|
| 1453 |
+
toidentifier@1.0.1: {}
|
| 1454 |
+
|
| 1455 |
+
tr46@0.0.3: {}
|
| 1456 |
+
|
| 1457 |
+
tsx@4.21.0:
|
| 1458 |
+
dependencies:
|
| 1459 |
+
esbuild: 0.27.7
|
| 1460 |
+
get-tsconfig: 4.14.0
|
| 1461 |
+
optionalDependencies:
|
| 1462 |
+
fsevents: 2.3.3
|
| 1463 |
+
|
| 1464 |
+
type-is@1.6.18:
|
| 1465 |
+
dependencies:
|
| 1466 |
+
media-typer: 0.3.0
|
| 1467 |
+
mime-types: 2.1.35
|
| 1468 |
+
|
| 1469 |
+
typescript@5.9.3: {}
|
| 1470 |
+
|
| 1471 |
+
undici-types@5.26.5: {}
|
| 1472 |
+
|
| 1473 |
+
undici-types@6.21.0: {}
|
| 1474 |
+
|
| 1475 |
+
unpipe@1.0.0: {}
|
| 1476 |
+
|
| 1477 |
+
utils-merge@1.0.1: {}
|
| 1478 |
+
|
| 1479 |
+
vary@1.1.2: {}
|
| 1480 |
+
|
| 1481 |
+
web-streams-polyfill@4.0.0-beta.3: {}
|
| 1482 |
+
|
| 1483 |
+
webidl-conversions@3.0.1: {}
|
| 1484 |
+
|
| 1485 |
+
whatwg-url@5.0.0:
|
| 1486 |
+
dependencies:
|
| 1487 |
+
tr46: 0.0.3
|
| 1488 |
+
webidl-conversions: 3.0.1
|
| 1489 |
+
|
| 1490 |
+
wrappy@1.0.2: {}
|
| 1491 |
+
|
| 1492 |
+
zod@3.25.76: {}
|
apps/simulator/pnpm-workspace.yaml
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
allowBuilds:
|
| 2 |
+
esbuild: set this to true or false
|
apps/simulator/src/agents/compiler.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Workflow Compiler Agent β UPGRADED
|
| 3 |
+
* Converts WorkflowGraph (IR) β valid n8n JSON
|
| 4 |
+
* Schema-aware, version-aware, credential-aware
|
| 5 |
+
* Provider-agnostic LLM Gateway β NO direct OpenAI dependency
|
| 6 |
+
* STRICT: Only uses nodes from registry. Zero hallucinated params.
|
| 7 |
+
*/
|
| 8 |
+
import { type LLMGateway } from '@wfo/integrations/llm-providers/index';
|
| 9 |
+
import type { WorkflowGraph, WorkflowIntent, N8nWorkflow } from '../types/workflow';
|
| 10 |
+
import { COMPILER_PROMPT } from '../prompts/compiler';
|
| 11 |
+
import { N8N_NODE_REGISTRY, isValidNodeType } from '../knowledge/nodeRegistry';
|
| 12 |
+
|
| 13 |
+
export class WorkflowCompiler {
|
| 14 |
+
private llm: LLMGateway;
|
| 15 |
+
|
| 16 |
+
constructor(llm: LLMGateway) {
|
| 17 |
+
this.llm = llm;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
async compile(graph: WorkflowGraph, intent: WorkflowIntent): Promise<N8nWorkflow> {
|
| 21 |
+
// Build node schema context from registry (ONLY nodes used in graph)
|
| 22 |
+
const nodeTypes = [...new Set(graph.nodes.map((n) => n.n8nNodeType))];
|
| 23 |
+
const validNodeTypes = nodeTypes.filter(isValidNodeType);
|
| 24 |
+
const nodeSchemas = validNodeTypes
|
| 25 |
+
.map((type) => N8N_NODE_REGISTRY[type])
|
| 26 |
+
.filter(Boolean)
|
| 27 |
+
.map((s) => JSON.stringify(s, null, 2))
|
| 28 |
+
.join('\n\n');
|
| 29 |
+
|
| 30 |
+
const workflow = await this.llm.completeJSON<N8nWorkflow>([
|
| 31 |
+
{ role: 'system', content: COMPILER_PROMPT },
|
| 32 |
+
{
|
| 33 |
+
role: 'user',
|
| 34 |
+
content: `Compile this WorkflowGraph to a valid n8n JSON workflow.
|
| 35 |
+
|
| 36 |
+
GRAPH:
|
| 37 |
+
${JSON.stringify(graph, null, 2)}
|
| 38 |
+
|
| 39 |
+
INTENT:
|
| 40 |
+
${JSON.stringify(intent, null, 2)}
|
| 41 |
+
|
| 42 |
+
REGISTERED NODE SCHEMAS (use ONLY these β parameters MUST match schema):
|
| 43 |
+
${nodeSchemas || '(No pre-built schemas β use standard n8n defaults)'}
|
| 44 |
+
|
| 45 |
+
CRITICAL COMPILATION RULES:
|
| 46 |
+
1. active MUST be false β NEVER true
|
| 47 |
+
2. All node IDs must be unique UUID strings (format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)
|
| 48 |
+
3. connections MUST wire ALL graph edges correctly
|
| 49 |
+
4. NEVER invent parameters not in the node schema above
|
| 50 |
+
5. Every node MUST have real expressions for dynamic fields β NO static placeholder text
|
| 51 |
+
6. Add retryOnFail: true, maxTries: 3, waitBetweenTries: 1000 on all critical/external nodes
|
| 52 |
+
7. Add meaningful node notes explaining what each node does
|
| 53 |
+
8. USE these expression patterns:
|
| 54 |
+
- Input from trigger: {{$json?.body?.fieldName ?? $json?.fieldName ?? ""}}
|
| 55 |
+
- Previous node: {{$node["NodeName"].json?.field ?? ""}}
|
| 56 |
+
- Array item: {{$json?.items?.[0]?.value ?? ""}}
|
| 57 |
+
- Conditional: {{$json?.status === "active" ? "yes" : "no"}}
|
| 58 |
+
9. SET nodes MUST have explicit field mappings β NOT empty values array
|
| 59 |
+
10. IF nodes MUST have real boolean conditions referencing actual fields
|
| 60 |
+
11. CODE nodes MUST contain real JavaScript logic β NOT placeholder comments
|
| 61 |
+
12. AI Agent nodes MUST include system message AND dynamic user message from previous node data
|
| 62 |
+
13. Return ONLY complete, importable n8n JSON workflow`,
|
| 63 |
+
},
|
| 64 |
+
], {
|
| 65 |
+
temperature: 0.0,
|
| 66 |
+
retries: 3,
|
| 67 |
+
});
|
| 68 |
+
|
| 69 |
+
return this.ensureSafeDefaults(workflow);
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
/**
|
| 73 |
+
* Enforce safety defaults regardless of LLM output
|
| 74 |
+
* CRITICAL: active = false, proper settings, error handling
|
| 75 |
+
*/
|
| 76 |
+
private ensureSafeDefaults(workflow: N8nWorkflow): N8nWorkflow {
|
| 77 |
+
return {
|
| 78 |
+
...workflow,
|
| 79 |
+
active: false, // NEVER activate on compile β ABSOLUTE safety rule
|
| 80 |
+
settings: {
|
| 81 |
+
callerPolicy: 'workflowsFromSameOwner',
|
| 82 |
+
errorWorkflow: workflow.settings?.errorWorkflow,
|
| 83 |
+
timezone: workflow.settings?.timezone ?? 'UTC',
|
| 84 |
+
...workflow.settings,
|
| 85 |
+
// Force these even if LLM overrides:
|
| 86 |
+
executionOrder: 'v1',
|
| 87 |
+
saveManualExecutions: true,
|
| 88 |
+
},
|
| 89 |
+
nodes: workflow.nodes.map((node) => ({
|
| 90 |
+
...node,
|
| 91 |
+
onError: node.onError ?? 'continueErrorOutput',
|
| 92 |
+
// Ensure critical external nodes have retry
|
| 93 |
+
retryOnFail: node.retryOnFail ?? false,
|
| 94 |
+
maxTries: node.maxTries ?? 3,
|
| 95 |
+
waitBetweenTries: node.waitBetweenTries ?? 1000,
|
| 96 |
+
})),
|
| 97 |
+
};
|
| 98 |
+
}
|
| 99 |
+
}
|
apps/simulator/src/agents/credentialIntelligence.ts
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Credential Intelligence System β UPGRADED
|
| 3 |
+
* Provider-agnostic, uses Node Registry for credential mapping
|
| 4 |
+
* Inspects workflow to identify required credentials
|
| 5 |
+
* Scans existing n8n credentials and maps types
|
| 6 |
+
* Detects missing credentials and blocks activation if needed
|
| 7 |
+
*/
|
| 8 |
+
import type {
|
| 9 |
+
N8nWorkflow,
|
| 10 |
+
CredentialAnalysis,
|
| 11 |
+
RequiredCredential,
|
| 12 |
+
AvailableCredential,
|
| 13 |
+
MissingCredential,
|
| 14 |
+
} from '../types/workflow';
|
| 15 |
+
import { N8N_NODE_REGISTRY } from '../knowledge/nodeRegistry';
|
| 16 |
+
|
| 17 |
+
// Extended node β credential map (derived from registry + additional mappings)
|
| 18 |
+
const NODE_CREDENTIAL_MAP: Record<string, string | undefined> = {
|
| 19 |
+
'n8n-nodes-base.telegram': 'telegramApi',
|
| 20 |
+
'n8n-nodes-base.telegramTrigger': 'telegramApi',
|
| 21 |
+
'n8n-nodes-base.openAi': 'openAiApi',
|
| 22 |
+
'@n8n/n8n-nodes-langchain.openAi': 'openAiApi',
|
| 23 |
+
'@n8n/n8n-nodes-langchain.agent': undefined, // depends on sub-node
|
| 24 |
+
'@n8n/n8n-nodes-langchain.lmChatAnthropic': 'anthropicApi',
|
| 25 |
+
'@n8n/n8n-nodes-langchain.memoryBufferWindow': undefined,
|
| 26 |
+
'@n8n/n8n-nodes-langchain.toolCode': undefined,
|
| 27 |
+
'n8n-nodes-base.gmail': 'googleOAuth2Api',
|
| 28 |
+
'n8n-nodes-base.emailReadImap': 'imap',
|
| 29 |
+
'n8n-nodes-base.googleSheets': 'googleSheetsOAuth2Api',
|
| 30 |
+
'n8n-nodes-base.slack': 'slackApi',
|
| 31 |
+
'n8n-nodes-base.slackTrigger': 'slackOAuth2Api',
|
| 32 |
+
'n8n-nodes-base.airtable': 'airtableTokenApi',
|
| 33 |
+
'n8n-nodes-base.notion': 'notionApi',
|
| 34 |
+
'n8n-nodes-base.notionTrigger': 'notionApi',
|
| 35 |
+
'n8n-nodes-base.github': 'githubApi',
|
| 36 |
+
'n8n-nodes-base.githubTrigger': 'githubApi',
|
| 37 |
+
'n8n-nodes-base.stripe': 'stripeApi',
|
| 38 |
+
'n8n-nodes-base.hubspot': 'hubspotApi',
|
| 39 |
+
'n8n-nodes-base.postgres': 'postgres',
|
| 40 |
+
'n8n-nodes-base.mysql': 'mySql',
|
| 41 |
+
'n8n-nodes-base.mongodb': 'mongoDb',
|
| 42 |
+
'n8n-nodes-base.redis': 'redis',
|
| 43 |
+
// No credential needed
|
| 44 |
+
'n8n-nodes-base.httpRequest': undefined,
|
| 45 |
+
'n8n-nodes-base.webhook': undefined,
|
| 46 |
+
'n8n-nodes-base.scheduleTrigger': undefined,
|
| 47 |
+
'n8n-nodes-base.manualTrigger': undefined,
|
| 48 |
+
'n8n-nodes-base.code': undefined,
|
| 49 |
+
'n8n-nodes-base.set': undefined,
|
| 50 |
+
'n8n-nodes-base.if': undefined,
|
| 51 |
+
'n8n-nodes-base.switch': undefined,
|
| 52 |
+
'n8n-nodes-base.merge': undefined,
|
| 53 |
+
'n8n-nodes-base.splitInBatches': undefined,
|
| 54 |
+
'n8n-nodes-base.itemLists': undefined,
|
| 55 |
+
'n8n-nodes-base.filter': undefined,
|
| 56 |
+
'n8n-nodes-base.noOp': undefined,
|
| 57 |
+
'n8n-nodes-base.stopAndError': undefined,
|
| 58 |
+
'n8n-nodes-base.wait': undefined,
|
| 59 |
+
'n8n-nodes-base.respondToWebhook': undefined,
|
| 60 |
+
'n8n-nodes-base.stickyNote': undefined,
|
| 61 |
+
};
|
| 62 |
+
|
| 63 |
+
const CREDENTIAL_SETUP_INSTRUCTIONS: Record<string, string> = {
|
| 64 |
+
telegramApi: 'Create a Telegram Bot via @BotFather, copy the bot token, then add "Telegram API" credential in n8n Settings β Credentials.',
|
| 65 |
+
openAiApi: 'Generate an API key at https://platform.openai.com/api-keys, then add "OpenAI" credential in n8n.',
|
| 66 |
+
anthropicApi: 'Get your API key at https://console.anthropic.com/, then add "Anthropic" credential in n8n.',
|
| 67 |
+
googleOAuth2Api: 'Set up a Google Cloud project, enable Gmail API, create OAuth2 credentials, then add "Google OAuth2 API" in n8n.',
|
| 68 |
+
googleSheetsOAuth2Api: 'Set up Google Cloud project, enable Sheets API, create OAuth2 credentials, then add "Google Sheets OAuth2 API" in n8n.',
|
| 69 |
+
slackApi: 'Create a Slack App at https://api.slack.com/apps with required scopes, then add "Slack API" credential in n8n.',
|
| 70 |
+
slackOAuth2Api: 'Create a Slack App with OAuth2, then add "Slack OAuth2 API" credential in n8n.',
|
| 71 |
+
airtableTokenApi: 'Generate a Personal Access Token at https://airtable.com/account, then add "Airtable Token API" in n8n.',
|
| 72 |
+
notionApi: 'Create an integration at https://www.notion.so/my-integrations, then add "Notion API" credential in n8n.',
|
| 73 |
+
githubApi: 'Generate a Personal Access Token at https://github.com/settings/tokens, then add "GitHub API" in n8n.',
|
| 74 |
+
stripeApi: 'Get your API key from https://dashboard.stripe.com/apikeys, then add "Stripe API" in n8n.',
|
| 75 |
+
hubspotApi: 'Create a Private App in HubSpot settings, copy the access token, then add "HubSpot API" in n8n.',
|
| 76 |
+
postgres: 'Provide your PostgreSQL host, port, database, username, and password in "Postgres" credential in n8n.',
|
| 77 |
+
mySql: 'Provide your MySQL host, port, database, username, and password in "MySQL" credential in n8n.',
|
| 78 |
+
mongoDb: 'Provide your MongoDB connection string in "MongoDB" credential in n8n.',
|
| 79 |
+
redis: 'Provide your Redis host, port, and optional password in "Redis" credential in n8n.',
|
| 80 |
+
imap: 'Provide your IMAP server host, port, username, and password in "IMAP" credential in n8n.',
|
| 81 |
+
};
|
| 82 |
+
|
| 83 |
+
export class CredentialIntelligence {
|
| 84 |
+
private n8nBaseUrl: string;
|
| 85 |
+
private n8nApiKey: string;
|
| 86 |
+
|
| 87 |
+
constructor(n8nBaseUrl: string, n8nApiKey: string) {
|
| 88 |
+
this.n8nBaseUrl = n8nBaseUrl;
|
| 89 |
+
this.n8nApiKey = n8nApiKey;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
async analyse(jobId: string, workflow: N8nWorkflow): Promise<CredentialAnalysis> {
|
| 93 |
+
// βββ 1. Identify required credentials from workflow nodes ββββββββββββββ
|
| 94 |
+
const required: RequiredCredential[] = [];
|
| 95 |
+
const requiredTypes = new Set<string>();
|
| 96 |
+
|
| 97 |
+
workflow.nodes.forEach((node) => {
|
| 98 |
+
// Check registry-based credential (more accurate)
|
| 99 |
+
const registryDef = N8N_NODE_REGISTRY[node.type];
|
| 100 |
+
const credType = registryDef?.credentialType ?? NODE_CREDENTIAL_MAP[node.type];
|
| 101 |
+
|
| 102 |
+
if (credType !== undefined && credType !== '') {
|
| 103 |
+
if (!requiredTypes.has(credType)) {
|
| 104 |
+
required.push({
|
| 105 |
+
nodeId: node.id,
|
| 106 |
+
nodeName: node.name,
|
| 107 |
+
credentialType: credType,
|
| 108 |
+
credentialDisplayName: registryDef?.credentialDisplayName,
|
| 109 |
+
required: true,
|
| 110 |
+
});
|
| 111 |
+
requiredTypes.add(credType);
|
| 112 |
+
}
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
// Also check node.credentials field (explicitly set by compiler)
|
| 116 |
+
if (node.credentials) {
|
| 117 |
+
Object.keys(node.credentials).forEach((cType) => {
|
| 118 |
+
if (!requiredTypes.has(cType)) {
|
| 119 |
+
required.push({
|
| 120 |
+
nodeId: node.id,
|
| 121 |
+
nodeName: node.name,
|
| 122 |
+
credentialType: cType,
|
| 123 |
+
required: true,
|
| 124 |
+
});
|
| 125 |
+
requiredTypes.add(cType);
|
| 126 |
+
}
|
| 127 |
+
});
|
| 128 |
+
}
|
| 129 |
+
});
|
| 130 |
+
|
| 131 |
+
// βββ 2. Fetch available credentials from n8n βββββββββββββββββββββββββββ
|
| 132 |
+
let available: AvailableCredential[] = [];
|
| 133 |
+
let n8nReachable = false;
|
| 134 |
+
try {
|
| 135 |
+
const resp = await fetch(`${this.n8nBaseUrl}/api/v1/credentials`, {
|
| 136 |
+
headers: { 'X-N8N-API-KEY': this.n8nApiKey },
|
| 137 |
+
signal: AbortSignal.timeout(8000),
|
| 138 |
+
});
|
| 139 |
+
if (resp.ok) {
|
| 140 |
+
const data = await resp.json() as { data: AvailableCredential[] };
|
| 141 |
+
available = data.data ?? [];
|
| 142 |
+
n8nReachable = true;
|
| 143 |
+
}
|
| 144 |
+
} catch {
|
| 145 |
+
// n8n not accessible β non-fatal, report as warning
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
const availableTypes = new Set(available.map((c) => c.type));
|
| 149 |
+
|
| 150 |
+
// βββ 3. Identify missing credentials ββββββββββββββββββββββββββββββββββ
|
| 151 |
+
const missingTypeMap = new Map<string, string[]>();
|
| 152 |
+
required.forEach((req) => {
|
| 153 |
+
// Only report as missing if n8n is reachable and credential is not there
|
| 154 |
+
if (n8nReachable && !availableTypes.has(req.credentialType)) {
|
| 155 |
+
const existing = missingTypeMap.get(req.credentialType) ?? [];
|
| 156 |
+
if (!existing.includes(req.nodeName)) existing.push(req.nodeName);
|
| 157 |
+
missingTypeMap.set(req.credentialType, existing);
|
| 158 |
+
}
|
| 159 |
+
});
|
| 160 |
+
|
| 161 |
+
const missing: MissingCredential[] = [...missingTypeMap.entries()].map(([credType, nodeNames]) => ({
|
| 162 |
+
credentialType: credType,
|
| 163 |
+
requiredByNodes: nodeNames,
|
| 164 |
+
setupInstructions: CREDENTIAL_SETUP_INSTRUCTIONS[credType] ?? `Set up "${credType}" credential in n8n Settings β Credentials.`,
|
| 165 |
+
}));
|
| 166 |
+
|
| 167 |
+
return {
|
| 168 |
+
jobId,
|
| 169 |
+
allCredentialsPresent: missing.length === 0,
|
| 170 |
+
n8nReachable,
|
| 171 |
+
required,
|
| 172 |
+
available,
|
| 173 |
+
missing,
|
| 174 |
+
analysedAt: new Date().toISOString(),
|
| 175 |
+
};
|
| 176 |
+
}
|
| 177 |
+
}
|
apps/simulator/src/agents/graphEngine.ts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Workflow Graph Engine (IR Layer) β UPGRADED
|
| 3 |
+
* Builds an internal graph representation BEFORE compiling to n8n JSON
|
| 4 |
+
* Provider-agnostic LLM Gateway β NO direct OpenAI dependency
|
| 5 |
+
* Validates all node types against the real Node Registry before returning
|
| 6 |
+
*/
|
| 7 |
+
import { type LLMGateway } from '@wfo/integrations/llm-providers/index';
|
| 8 |
+
import type { WorkflowIntent, WorkflowArchitecturePlan, WorkflowGraph } from '../types/workflow';
|
| 9 |
+
import { GRAPH_ENGINE_PROMPT } from '../prompts/graphEngine';
|
| 10 |
+
import { isValidNodeType, getRegistryNodeList } from '../knowledge/nodeRegistry';
|
| 11 |
+
|
| 12 |
+
export class WorkflowGraphEngine {
|
| 13 |
+
private llm: LLMGateway;
|
| 14 |
+
|
| 15 |
+
constructor(llm: LLMGateway) {
|
| 16 |
+
this.llm = llm;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
async buildGraph(
|
| 20 |
+
userRequest: string,
|
| 21 |
+
intent: WorkflowIntent,
|
| 22 |
+
plan: WorkflowArchitecturePlan,
|
| 23 |
+
): Promise<WorkflowGraph> {
|
| 24 |
+
const registryList = getRegistryNodeList();
|
| 25 |
+
|
| 26 |
+
const graph = await this.llm.completeJSON<WorkflowGraph>([
|
| 27 |
+
{ role: 'system', content: GRAPH_ENGINE_PROMPT },
|
| 28 |
+
{
|
| 29 |
+
role: 'user',
|
| 30 |
+
content: `Build a WorkflowGraph IR for this workflow:
|
| 31 |
+
|
| 32 |
+
REQUEST: ${userRequest}
|
| 33 |
+
|
| 34 |
+
INTENT:
|
| 35 |
+
${JSON.stringify(intent, null, 2)}
|
| 36 |
+
|
| 37 |
+
ARCHITECTURE PLAN:
|
| 38 |
+
${JSON.stringify(plan, null, 2)}
|
| 39 |
+
|
| 40 |
+
AVAILABLE NODE TYPES (USE ONLY THESE β NO OTHERS):
|
| 41 |
+
${registryList}
|
| 42 |
+
|
| 43 |
+
CRITICAL RULES:
|
| 44 |
+
- Every node MUST have n8nNodeType from the list above ONLY
|
| 45 |
+
- NEVER invent node types not in the list
|
| 46 |
+
- Every non-trigger node MUST have at least one incoming edge
|
| 47 |
+
- Every node MUST have meaningful parameters β NO empty nodes
|
| 48 |
+
- DataContracts must define what JSON fields flow between nodes
|
| 49 |
+
- Use real expressions: {{$json?.field ?? ""}} β NOT placeholder text
|
| 50 |
+
- Position nodes in clean left-to-right layout (x increases by 220 per step)
|
| 51 |
+
- Return a complete WorkflowGraph JSON`,
|
| 52 |
+
},
|
| 53 |
+
], {
|
| 54 |
+
temperature: 0.0,
|
| 55 |
+
retries: 3,
|
| 56 |
+
});
|
| 57 |
+
|
| 58 |
+
return this.validateAndOptimizeGraph(graph);
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
/**
|
| 62 |
+
* Validates all node types against registry, removes unknown nodes,
|
| 63 |
+
* optimises graph structure (orphan removal, position cleanup)
|
| 64 |
+
*/
|
| 65 |
+
private validateAndOptimizeGraph(graph: WorkflowGraph): WorkflowGraph {
|
| 66 |
+
const unknownNodes: string[] = [];
|
| 67 |
+
|
| 68 |
+
// Filter out any hallucinated node types
|
| 69 |
+
const validNodes = graph.nodes.filter((node) => {
|
| 70 |
+
if (!isValidNodeType(node.n8nNodeType)) {
|
| 71 |
+
unknownNodes.push(`${node.label} (${node.n8nNodeType})`);
|
| 72 |
+
return false;
|
| 73 |
+
}
|
| 74 |
+
return true;
|
| 75 |
+
});
|
| 76 |
+
|
| 77 |
+
if (unknownNodes.length > 0) {
|
| 78 |
+
console.warn(
|
| 79 |
+
`[GraphEngine] REJECTED ${unknownNodes.length} unknown node type(s): ${unknownNodes.join(', ')}`,
|
| 80 |
+
);
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
const validNodeIds = new Set(validNodes.map((n) => n.id));
|
| 84 |
+
|
| 85 |
+
// Remove edges that reference removed nodes
|
| 86 |
+
const validEdges = graph.edges.filter(
|
| 87 |
+
(e) => validNodeIds.has(e.sourceNodeId) && validNodeIds.has(e.targetNodeId),
|
| 88 |
+
);
|
| 89 |
+
|
| 90 |
+
// Remove orphaned non-trigger nodes
|
| 91 |
+
const optimizedNodes = validNodes.filter((node) => {
|
| 92 |
+
const hasIncoming = validEdges.some((e) => e.targetNodeId === node.id);
|
| 93 |
+
const hasOutgoing = validEdges.some((e) => e.sourceNodeId === node.id);
|
| 94 |
+
const isTrigger = node.layer === 'trigger';
|
| 95 |
+
return isTrigger || hasIncoming || hasOutgoing;
|
| 96 |
+
});
|
| 97 |
+
|
| 98 |
+
return {
|
| 99 |
+
...graph,
|
| 100 |
+
nodes: optimizedNodes,
|
| 101 |
+
edges: validEdges,
|
| 102 |
+
metadata: {
|
| 103 |
+
...graph.metadata,
|
| 104 |
+
version: '2.0.0',
|
| 105 |
+
createdAt: new Date().toISOString(),
|
| 106 |
+
unknownNodesRejected: unknownNodes,
|
| 107 |
+
},
|
| 108 |
+
};
|
| 109 |
+
}
|
| 110 |
+
}
|
apps/simulator/src/agents/intentInterpreter.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Intent Interpreter Agent β UPGRADED
|
| 3 |
+
* Deeply understands user's workflow request using provider-agnostic LLM Gateway
|
| 4 |
+
* NO direct OpenAI dependency
|
| 5 |
+
*/
|
| 6 |
+
import { type LLMGateway } from '@wfo/integrations/llm-providers/index';
|
| 7 |
+
import type { WorkflowIntent } from '../types/workflow';
|
| 8 |
+
import { INTENT_INTERPRETER_PROMPT } from '../prompts/intentInterpreter';
|
| 9 |
+
|
| 10 |
+
export class IntentInterpreterAgent {
|
| 11 |
+
private llm: LLMGateway;
|
| 12 |
+
|
| 13 |
+
constructor(llm: LLMGateway) {
|
| 14 |
+
this.llm = llm;
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
async interpret(userRequest: string): Promise<WorkflowIntent> {
|
| 18 |
+
const raw = await this.llm.completeJSON<Partial<WorkflowIntent>>([
|
| 19 |
+
{ role: 'system', content: INTENT_INTERPRETER_PROMPT },
|
| 20 |
+
{
|
| 21 |
+
role: 'user',
|
| 22 |
+
content: `Analyse this workflow request and return structured JSON:\n\n${userRequest}`,
|
| 23 |
+
},
|
| 24 |
+
], {
|
| 25 |
+
temperature: 0.1,
|
| 26 |
+
retries: 3,
|
| 27 |
+
});
|
| 28 |
+
|
| 29 |
+
return this.validateAndNormalize(raw);
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
private validateAndNormalize(raw: Partial<WorkflowIntent>): WorkflowIntent {
|
| 33 |
+
return {
|
| 34 |
+
workflowType: raw.workflowType ?? 'general',
|
| 35 |
+
requiresAI: raw.requiresAI ?? false,
|
| 36 |
+
integrations: Array.isArray(raw.integrations) ? raw.integrations : [],
|
| 37 |
+
riskLevel: raw.riskLevel ?? 'medium',
|
| 38 |
+
requiresHumanApproval: raw.requiresHumanApproval ?? true,
|
| 39 |
+
syncVsAsync: raw.syncVsAsync ?? 'sync',
|
| 40 |
+
scalingRequirements: raw.scalingRequirements ?? 'low',
|
| 41 |
+
identifiedRisks: Array.isArray(raw.identifiedRisks) ? raw.identifiedRisks : [],
|
| 42 |
+
estimatedComplexity: raw.estimatedComplexity ?? 'moderate',
|
| 43 |
+
domain: raw.domain ?? 'general',
|
| 44 |
+
triggerType: raw.triggerType ?? 'webhook',
|
| 45 |
+
triggerHint: raw.triggerHint,
|
| 46 |
+
};
|
| 47 |
+
}
|
| 48 |
+
}
|
apps/simulator/src/agents/simulator.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Dry Run Simulation Engine β UPGRADED
|
| 3 |
+
* Simulates workflow execution with mock data BEFORE deployment
|
| 4 |
+
* Provider-agnostic LLM Gateway β NO direct OpenAI dependency
|
| 5 |
+
* Detects: runtime issues, null references, failure points, chaos scenarios
|
| 6 |
+
*/
|
| 7 |
+
import { type LLMGateway } from '@wfo/integrations/llm-providers/index';
|
| 8 |
+
import type {
|
| 9 |
+
N8nWorkflow,
|
| 10 |
+
WorkflowGraph,
|
| 11 |
+
WorkflowArchitecturePlan,
|
| 12 |
+
SimulationReport,
|
| 13 |
+
} from '../types/workflow';
|
| 14 |
+
import { SIMULATOR_PROMPT } from '../prompts/simulator';
|
| 15 |
+
import { MockDataGenerator } from '../services/mockDataGenerator';
|
| 16 |
+
|
| 17 |
+
export class DryRunSimulator {
|
| 18 |
+
private llm: LLMGateway;
|
| 19 |
+
private mockDataGen: MockDataGenerator;
|
| 20 |
+
|
| 21 |
+
constructor(llm: LLMGateway) {
|
| 22 |
+
this.llm = llm;
|
| 23 |
+
this.mockDataGen = new MockDataGenerator(llm);
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
async simulate(
|
| 27 |
+
jobId: string,
|
| 28 |
+
workflow: N8nWorkflow,
|
| 29 |
+
graph: WorkflowGraph,
|
| 30 |
+
plan: WorkflowArchitecturePlan,
|
| 31 |
+
): Promise<SimulationReport> {
|
| 32 |
+
// Generate realistic mock data for trigger node
|
| 33 |
+
const mockData = await this.mockDataGen.generate(workflow, graph);
|
| 34 |
+
|
| 35 |
+
const result = await this.llm.completeJSON<Partial<SimulationReport>>([
|
| 36 |
+
{ role: 'system', content: SIMULATOR_PROMPT },
|
| 37 |
+
{
|
| 38 |
+
role: 'user',
|
| 39 |
+
content: `Simulate the execution of this n8n workflow using the provided mock data.
|
| 40 |
+
|
| 41 |
+
WORKFLOW:
|
| 42 |
+
${JSON.stringify(workflow, null, 2)}
|
| 43 |
+
|
| 44 |
+
GRAPH (IR):
|
| 45 |
+
${JSON.stringify(graph, null, 2)}
|
| 46 |
+
|
| 47 |
+
ARCHITECTURE PLAN:
|
| 48 |
+
${JSON.stringify(plan, null, 2)}
|
| 49 |
+
|
| 50 |
+
MOCK INPUT DATA:
|
| 51 |
+
${JSON.stringify(mockData, null, 2)}
|
| 52 |
+
|
| 53 |
+
Produce a SimulationReport JSON with:
|
| 54 |
+
- Full executionTrace (one step per node, with realistic input/output data)
|
| 55 |
+
- Identified predictedFailurePoints (null references, missing fields, credential errors, API limits)
|
| 56 |
+
- ChaosTestResults: simulate timeout, null_data, malformed_input, expired_credentials, rate_limit, network_error
|
| 57 |
+
- readinessScore (0-100): based on trace success rate, failure severity, chaos resilience
|
| 58 |
+
- riskAnalysis: overallRisk, specific risks, mitigations
|
| 59 |
+
- passed: true only if readinessScore >= 70 AND no critical failure points
|
| 60 |
+
- deploymentBlockers: list of issues that MUST be fixed before deployment`,
|
| 61 |
+
},
|
| 62 |
+
], {
|
| 63 |
+
temperature: 0.1,
|
| 64 |
+
retries: 3,
|
| 65 |
+
});
|
| 66 |
+
|
| 67 |
+
return this.normalizeReport(jobId, result, mockData);
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
private normalizeReport(
|
| 71 |
+
jobId: string,
|
| 72 |
+
raw: Partial<SimulationReport>,
|
| 73 |
+
mockData: Record<string, unknown>,
|
| 74 |
+
): SimulationReport {
|
| 75 |
+
const readinessScore = Math.min(100, Math.max(0, raw.readinessScore ?? 0));
|
| 76 |
+
return {
|
| 77 |
+
jobId,
|
| 78 |
+
passed: raw.passed ?? readinessScore >= 70,
|
| 79 |
+
readinessScore,
|
| 80 |
+
executionTrace: Array.isArray(raw.executionTrace) ? raw.executionTrace : [],
|
| 81 |
+
riskAnalysis: raw.riskAnalysis ?? { overallRisk: 'medium', risks: [], mitigations: [] },
|
| 82 |
+
predictedFailurePoints: Array.isArray(raw.predictedFailurePoints) ? raw.predictedFailurePoints : [],
|
| 83 |
+
chaosTestResults: Array.isArray(raw.chaosTestResults) ? raw.chaosTestResults : [],
|
| 84 |
+
deploymentBlockers: Array.isArray(raw.deploymentBlockers) ? raw.deploymentBlockers : [],
|
| 85 |
+
mockDataUsed: mockData,
|
| 86 |
+
simulatedAt: new Date().toISOString(),
|
| 87 |
+
};
|
| 88 |
+
}
|
| 89 |
+
}
|
apps/simulator/src/agents/validator.ts
ADDED
|
@@ -0,0 +1,486 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Validation Engine Agent β UPGRADED
|
| 3 |
+
* Multi-stage: Schema β Credential β Graph β Expression β Reliability β NodeRegistry
|
| 4 |
+
* Provider-agnostic LLM Gateway β NO direct OpenAI dependency
|
| 5 |
+
* Strict: rejects unknown node types, validates real expressions, checks completeness
|
| 6 |
+
*/
|
| 7 |
+
import { type LLMGateway } from '@wfo/integrations/llm-providers/index';
|
| 8 |
+
import type {
|
| 9 |
+
N8nWorkflow,
|
| 10 |
+
WorkflowGraph,
|
| 11 |
+
ValidationReport,
|
| 12 |
+
ValidationSection,
|
| 13 |
+
ValidationIssue,
|
| 14 |
+
} from '../types/workflow';
|
| 15 |
+
import { isValidNodeType, getNodeDef } from '../knowledge/nodeRegistry';
|
| 16 |
+
|
| 17 |
+
export class ValidationEngine {
|
| 18 |
+
private llm: LLMGateway;
|
| 19 |
+
private n8nBaseUrl: string;
|
| 20 |
+
private n8nApiKey: string;
|
| 21 |
+
|
| 22 |
+
constructor(llm: LLMGateway, n8nBaseUrl: string, n8nApiKey: string) {
|
| 23 |
+
this.llm = llm;
|
| 24 |
+
this.n8nBaseUrl = n8nBaseUrl;
|
| 25 |
+
this.n8nApiKey = n8nApiKey;
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
async validate(jobId: string, workflow: N8nWorkflow, graph: WorkflowGraph): Promise<ValidationReport> {
|
| 29 |
+
const [
|
| 30 |
+
schemaValidation,
|
| 31 |
+
nodeRegistryValidation,
|
| 32 |
+
graphValidation,
|
| 33 |
+
expressionValidation,
|
| 34 |
+
dataFlowValidation,
|
| 35 |
+
reliabilityValidation,
|
| 36 |
+
credentialValidation,
|
| 37 |
+
] = await Promise.all([
|
| 38 |
+
this.validateSchema(workflow),
|
| 39 |
+
this.validateNodeRegistry(workflow),
|
| 40 |
+
this.validateGraph(graph),
|
| 41 |
+
this.validateExpressions(workflow),
|
| 42 |
+
this.validateDataFlow(workflow, graph),
|
| 43 |
+
this.validateReliability(workflow, graph),
|
| 44 |
+
this.validateCredentials(workflow),
|
| 45 |
+
]);
|
| 46 |
+
|
| 47 |
+
const allIssues = [
|
| 48 |
+
...schemaValidation.issues,
|
| 49 |
+
...nodeRegistryValidation.issues,
|
| 50 |
+
...graphValidation.issues,
|
| 51 |
+
...expressionValidation.issues,
|
| 52 |
+
...dataFlowValidation.issues,
|
| 53 |
+
...reliabilityValidation.issues,
|
| 54 |
+
...credentialValidation.issues,
|
| 55 |
+
];
|
| 56 |
+
|
| 57 |
+
const errors = allIssues.filter((i) => i.severity === 'error');
|
| 58 |
+
const warnings = allIssues.filter((i) => i.severity === 'warning');
|
| 59 |
+
|
| 60 |
+
const overallScore = Math.round(
|
| 61 |
+
schemaValidation.score * 0.15 +
|
| 62 |
+
nodeRegistryValidation.score * 0.20 +
|
| 63 |
+
credentialValidation.score * 0.15 +
|
| 64 |
+
graphValidation.score * 0.15 +
|
| 65 |
+
expressionValidation.score * 0.15 +
|
| 66 |
+
dataFlowValidation.score * 0.10 +
|
| 67 |
+
reliabilityValidation.score * 0.10,
|
| 68 |
+
);
|
| 69 |
+
|
| 70 |
+
return {
|
| 71 |
+
jobId,
|
| 72 |
+
valid: errors.length === 0,
|
| 73 |
+
overallScore,
|
| 74 |
+
schemaValidation,
|
| 75 |
+
credentialValidation,
|
| 76 |
+
graphValidation,
|
| 77 |
+
expressionValidation,
|
| 78 |
+
reliabilityValidation,
|
| 79 |
+
nodeRegistryValidation,
|
| 80 |
+
dataFlowValidation,
|
| 81 |
+
issues: errors,
|
| 82 |
+
warnings,
|
| 83 |
+
validatedAt: new Date().toISOString(),
|
| 84 |
+
};
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
// βββ A. Schema Validation βββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 88 |
+
private async validateSchema(workflow: N8nWorkflow): Promise<ValidationSection> {
|
| 89 |
+
const issues: ValidationIssue[] = [];
|
| 90 |
+
|
| 91 |
+
if (!workflow.name || workflow.name.trim() === '') {
|
| 92 |
+
issues.push(this.issue('schema-001', 'error', 'Schema', undefined,
|
| 93 |
+
'Workflow must have a name', 'Add a descriptive workflow name'));
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
if (!workflow.nodes || workflow.nodes.length === 0) {
|
| 97 |
+
issues.push(this.issue('schema-002', 'error', 'Schema', undefined,
|
| 98 |
+
'Workflow has no nodes', 'Add at least one trigger node'));
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
if ((workflow.active as any) === true) {
|
| 102 |
+
issues.push(this.issue('schema-active', 'error', 'Schema', undefined,
|
| 103 |
+
'Workflow active must be false β NEVER deploy as active',
|
| 104 |
+
'Set active: false β activation requires human approval'));
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
workflow.nodes?.forEach((node) => {
|
| 108 |
+
if (!node.type) {
|
| 109 |
+
issues.push(this.issue('schema-003', 'error', 'Schema', node.id,
|
| 110 |
+
`Node "${node.name}" is missing type`, 'Provide a valid n8n node type'));
|
| 111 |
+
}
|
| 112 |
+
if (!node.typeVersion || node.typeVersion < 1) {
|
| 113 |
+
issues.push(this.issue('schema-004', 'warning', 'Schema', node.id,
|
| 114 |
+
`Node "${node.name}" missing typeVersion`, 'Add typeVersion: 1 or higher'));
|
| 115 |
+
}
|
| 116 |
+
if (!node.position || !Array.isArray(node.position) || node.position.length !== 2) {
|
| 117 |
+
issues.push(this.issue('schema-005', 'warning', 'Schema', node.id,
|
| 118 |
+
`Node "${node.name}" missing position [x, y]`, 'Add [x, y] position array'));
|
| 119 |
+
}
|
| 120 |
+
if (!node.id || node.id.trim() === '') {
|
| 121 |
+
issues.push(this.issue('schema-006', 'error', 'Schema', undefined,
|
| 122 |
+
`Node "${node.name}" is missing an ID`, 'Add a unique UUID string as node ID'));
|
| 123 |
+
}
|
| 124 |
+
});
|
| 125 |
+
|
| 126 |
+
// Check for duplicate node IDs
|
| 127 |
+
const nodeIds = workflow.nodes?.map((n) => n.id) ?? [];
|
| 128 |
+
const seen = new Set<string>();
|
| 129 |
+
nodeIds.forEach((id) => {
|
| 130 |
+
if (seen.has(id)) {
|
| 131 |
+
issues.push(this.issue('schema-007', 'error', 'Schema', id,
|
| 132 |
+
`Duplicate node ID detected: "${id}"`, 'Ensure all node IDs are unique UUIDs'));
|
| 133 |
+
}
|
| 134 |
+
seen.add(id);
|
| 135 |
+
});
|
| 136 |
+
|
| 137 |
+
const score = Math.max(0, 100 - errors(issues) * 20 - warnings(issues) * 5);
|
| 138 |
+
return { passed: errors(issues) === 0, score, issues };
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
// βββ B. Node Registry Validation (CRITICAL) ββββββββββββββββββββββββββββββ
|
| 142 |
+
private async validateNodeRegistry(workflow: N8nWorkflow): Promise<ValidationSection> {
|
| 143 |
+
const issues: ValidationIssue[] = [];
|
| 144 |
+
|
| 145 |
+
workflow.nodes?.forEach((node) => {
|
| 146 |
+
if (!node.type) return;
|
| 147 |
+
|
| 148 |
+
if (!isValidNodeType(node.type)) {
|
| 149 |
+
issues.push(this.issue(`registry-unknown-${node.id}`, 'error', 'NodeRegistry', node.id,
|
| 150 |
+
`UNKNOWN node type "${node.type}" β not in n8n registry. This will fail on import.`,
|
| 151 |
+
'Replace with a valid node type from the registry'));
|
| 152 |
+
return;
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
const def = getNodeDef(node.type);
|
| 156 |
+
if (!def) return;
|
| 157 |
+
|
| 158 |
+
// Check required parameters are present
|
| 159 |
+
def.requiredParameters.forEach((param) => {
|
| 160 |
+
const paramsStr = JSON.stringify(node.parameters ?? {});
|
| 161 |
+
if (!paramsStr.includes(`"${param}"`)) {
|
| 162 |
+
issues.push(this.issue(`registry-param-${node.id}-${param}`, 'warning', 'NodeRegistry', node.id,
|
| 163 |
+
`Node "${node.name}" may be missing required parameter: "${param}"`,
|
| 164 |
+
`Add "${param}" parameter to node "${node.name}"`));
|
| 165 |
+
}
|
| 166 |
+
});
|
| 167 |
+
|
| 168 |
+
// Check node has parameters (no completely empty nodes)
|
| 169 |
+
if (!node.parameters || Object.keys(node.parameters).length === 0) {
|
| 170 |
+
if (def.requiredParameters.length > 0) {
|
| 171 |
+
issues.push(this.issue(`registry-empty-${node.id}`, 'error', 'NodeRegistry', node.id,
|
| 172 |
+
`Node "${node.name}" has empty parameters β this will not execute correctly`,
|
| 173 |
+
`Add required parameters: ${def.requiredParameters.join(', ')}`));
|
| 174 |
+
}
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
// Verify typeVersion matches registry
|
| 178 |
+
if (node.typeVersion && node.typeVersion > def.typeVersion) {
|
| 179 |
+
issues.push(this.issue(`registry-version-${node.id}`, 'warning', 'NodeRegistry', node.id,
|
| 180 |
+
`Node "${node.name}" uses typeVersion ${node.typeVersion} but registry has v${def.typeVersion}`,
|
| 181 |
+
`Use typeVersion: ${def.typeVersion} for "${node.type}"`));
|
| 182 |
+
}
|
| 183 |
+
});
|
| 184 |
+
|
| 185 |
+
const score = Math.max(0, 100 - errors(issues) * 25 - warnings(issues) * 8);
|
| 186 |
+
return { passed: errors(issues) === 0, score, issues };
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
// βββ C. Credential Validation βββββββββββββββββββββββββββββββββββββββββββββ
|
| 190 |
+
private async validateCredentials(workflow: N8nWorkflow): Promise<ValidationSection> {
|
| 191 |
+
const issues: ValidationIssue[] = [];
|
| 192 |
+
|
| 193 |
+
let availableCredTypes = new Set<string>();
|
| 194 |
+
try {
|
| 195 |
+
const resp = await fetch(`${this.n8nBaseUrl}/api/v1/credentials`, {
|
| 196 |
+
headers: { 'X-N8N-API-KEY': this.n8nApiKey },
|
| 197 |
+
signal: AbortSignal.timeout(5000),
|
| 198 |
+
});
|
| 199 |
+
if (resp.ok) {
|
| 200 |
+
const data = await resp.json() as { data: Array<{ type: string }> };
|
| 201 |
+
availableCredTypes = new Set(data.data.map((c) => c.type));
|
| 202 |
+
}
|
| 203 |
+
} catch {
|
| 204 |
+
issues.push(this.issue('cred-001', 'warning', 'Credentials', undefined,
|
| 205 |
+
'Could not connect to n8n to verify credentials β verify manually',
|
| 206 |
+
'Ensure n8n is accessible at configured URL'));
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
workflow.nodes?.forEach((node) => {
|
| 210 |
+
// Check registry-defined credential
|
| 211 |
+
const def = getNodeDef(node.type);
|
| 212 |
+
if (def?.credentialType) {
|
| 213 |
+
if (availableCredTypes.size > 0 && !availableCredTypes.has(def.credentialType)) {
|
| 214 |
+
issues.push(this.issue(`cred-missing-${node.id}`, 'error', 'Credentials', node.id,
|
| 215 |
+
`Node "${node.name}" requires credential type "${def.credentialType}" which is not in n8n`,
|
| 216 |
+
`Create a "${def.credentialDisplayName ?? def.credentialType}" credential in n8n`));
|
| 217 |
+
}
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
// Also check node.credentials object
|
| 221 |
+
if (node.credentials) {
|
| 222 |
+
Object.entries(node.credentials).forEach(([credType]) => {
|
| 223 |
+
if (availableCredTypes.size > 0 && !availableCredTypes.has(credType)) {
|
| 224 |
+
issues.push(this.issue(`cred-node-${node.id}-${credType}`, 'error', 'Credentials', node.id,
|
| 225 |
+
`Node "${node.name}" references credential "${credType}" not found in n8n`,
|
| 226 |
+
`Create a "${credType}" credential in n8n`));
|
| 227 |
+
}
|
| 228 |
+
});
|
| 229 |
+
}
|
| 230 |
+
});
|
| 231 |
+
|
| 232 |
+
const score = Math.max(0, 100 - errors(issues) * 25);
|
| 233 |
+
return { passed: errors(issues) === 0, score, issues };
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
// βββ D. Graph Validation ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 237 |
+
private async validateGraph(graph: WorkflowGraph): Promise<ValidationSection> {
|
| 238 |
+
const issues: ValidationIssue[] = [];
|
| 239 |
+
const nodeIds = new Set(graph.nodes.map((n) => n.id));
|
| 240 |
+
|
| 241 |
+
// Orphan detection (non-trigger nodes with no incoming edges)
|
| 242 |
+
graph.nodes.forEach((node) => {
|
| 243 |
+
if (node.layer !== 'trigger') {
|
| 244 |
+
const hasIncoming = graph.edges.some((e) => e.targetNodeId === node.id);
|
| 245 |
+
if (!hasIncoming) {
|
| 246 |
+
issues.push(this.issue('graph-orphan-' + node.id, 'error', 'Graph', node.id,
|
| 247 |
+
`Node "${node.label}" is orphaned β no incoming edges`,
|
| 248 |
+
'Connect this node to the workflow or remove it'));
|
| 249 |
+
}
|
| 250 |
+
}
|
| 251 |
+
});
|
| 252 |
+
|
| 253 |
+
// Dead-end detection (non-terminal nodes with no outgoing edges)
|
| 254 |
+
graph.nodes.forEach((node) => {
|
| 255 |
+
const isTerminal = ['monitoring', 'utility'].includes(node.layer ?? '');
|
| 256 |
+
const hasOutgoing = graph.edges.some((e) => e.sourceNodeId === node.id);
|
| 257 |
+
if (!isTerminal && !hasOutgoing && node.layer !== 'trigger') {
|
| 258 |
+
// Warning only β some nodes legitimately end flows
|
| 259 |
+
issues.push(this.issue('graph-deadend-' + node.id, 'warning', 'Graph', node.id,
|
| 260 |
+
`Node "${node.label}" has no outgoing edges β is this intentional?`,
|
| 261 |
+
'Connect to a next step or add a terminal node'));
|
| 262 |
+
}
|
| 263 |
+
});
|
| 264 |
+
|
| 265 |
+
// Edge validity
|
| 266 |
+
graph.edges.forEach((edge) => {
|
| 267 |
+
if (!nodeIds.has(edge.sourceNodeId)) {
|
| 268 |
+
issues.push(this.issue('graph-edge-src-' + edge.id, 'error', 'Graph', undefined,
|
| 269 |
+
`Edge "${edge.id}" references missing source node "${edge.sourceNodeId}"`,
|
| 270 |
+
'Fix edge source reference'));
|
| 271 |
+
}
|
| 272 |
+
if (!nodeIds.has(edge.targetNodeId)) {
|
| 273 |
+
issues.push(this.issue('graph-edge-tgt-' + edge.id, 'error', 'Graph', undefined,
|
| 274 |
+
`Edge "${edge.id}" references missing target node "${edge.targetNodeId}"`,
|
| 275 |
+
'Fix edge target reference'));
|
| 276 |
+
}
|
| 277 |
+
});
|
| 278 |
+
|
| 279 |
+
// Trigger check
|
| 280 |
+
const hasTrigger = graph.nodes.some((n) => n.layer === 'trigger');
|
| 281 |
+
if (!hasTrigger) {
|
| 282 |
+
issues.push(this.issue('graph-no-trigger', 'error', 'Graph', undefined,
|
| 283 |
+
'Workflow has no trigger node',
|
| 284 |
+
'Add a trigger layer node: Webhook, Schedule, Telegram Trigger, etc.'));
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
const score = Math.max(0, 100 - errors(issues) * 20 - warnings(issues) * 5);
|
| 288 |
+
return { passed: errors(issues) === 0, score, issues };
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
// βββ E. Expression Validation (UPGRADED) βββββββββββββββββββββββββββββββββ
|
| 292 |
+
private async validateExpressions(workflow: N8nWorkflow): Promise<ValidationSection> {
|
| 293 |
+
const issues: ValidationIssue[] = [];
|
| 294 |
+
const expressionRegex = /\{\{(.+?)\}\}/g;
|
| 295 |
+
|
| 296 |
+
workflow.nodes?.forEach((node) => {
|
| 297 |
+
const paramsStr = JSON.stringify(node.parameters ?? {});
|
| 298 |
+
const matches = [...paramsStr.matchAll(expressionRegex)];
|
| 299 |
+
|
| 300 |
+
// Check for completely static nodes that should have expressions
|
| 301 |
+
const def = getNodeDef(node.type);
|
| 302 |
+
if (def && matches.length === 0 && def.requiredParameters.length > 0 && node.type !== 'n8n-nodes-base.manualTrigger') {
|
| 303 |
+
const isTrigger = def.isTrigger;
|
| 304 |
+
if (!isTrigger && !['n8n-nodes-base.noOp', 'n8n-nodes-base.stickyNote'].includes(node.type)) {
|
| 305 |
+
issues.push(this.issue(`expr-static-${node.id}`, 'warning', 'Expressions', node.id,
|
| 306 |
+
`Node "${node.name}" has no dynamic expressions β likely using static/hardcoded values`,
|
| 307 |
+
'Add dynamic expressions like {{$json?.field ?? ""}} to reference upstream data'));
|
| 308 |
+
}
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
matches.forEach((match) => {
|
| 312 |
+
const expr = match[1]?.trim() ?? '';
|
| 313 |
+
|
| 314 |
+
// Detect unsafe dot-access without optional chaining
|
| 315 |
+
if (
|
| 316 |
+
expr.includes('$json.') ||
|
| 317 |
+
expr.includes('$node[') && !expr.includes('?.') && !expr.includes(' ?? ')
|
| 318 |
+
) {
|
| 319 |
+
issues.push(this.issue(
|
| 320 |
+
`expr-unsafe-${node.id}-${Buffer.from(expr).toString('base64').slice(0, 8)}`,
|
| 321 |
+
'warning', 'Expressions', node.id,
|
| 322 |
+
`Expression "{{${expr}}}" may throw if value is null β use optional chaining`,
|
| 323 |
+
`Change to: {{${expr.replace(/\./g, '?.')} ?? ''}}`));
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
+
// Detect empty expressions
|
| 327 |
+
if (expr === '' || expr === '""' || expr === "''") {
|
| 328 |
+
issues.push(this.issue(`expr-empty-${node.id}`, 'error', 'Expressions', node.id,
|
| 329 |
+
`Node "${node.name}" has empty expression {{${expr}}}`,
|
| 330 |
+
'Replace with a real expression referencing upstream data'));
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
// Detect placeholder text patterns
|
| 334 |
+
const placeholderPatterns = ['YOUR_', 'REPLACE_ME', 'TODO', 'PLACEHOLDER', 'example.com', 'XXXX'];
|
| 335 |
+
placeholderPatterns.forEach((p) => {
|
| 336 |
+
if (expr.toUpperCase().includes(p)) {
|
| 337 |
+
issues.push(this.issue(`expr-placeholder-${node.id}`, 'error', 'Expressions', node.id,
|
| 338 |
+
`Node "${node.name}" contains placeholder text in expression: "${expr}"`,
|
| 339 |
+
'Replace placeholder with actual value or expression'));
|
| 340 |
+
}
|
| 341 |
+
});
|
| 342 |
+
});
|
| 343 |
+
|
| 344 |
+
// Detect placeholder strings in parameters (outside expressions)
|
| 345 |
+
const plainParams = JSON.stringify(node.parameters ?? {});
|
| 346 |
+
const placeholderStrings = ['YOUR_', 'REPLACE_ME', 'example.com', 'your-', 'my-webhook-path'];
|
| 347 |
+
placeholderStrings.forEach((p) => {
|
| 348 |
+
if (plainParams.includes(p)) {
|
| 349 |
+
issues.push(this.issue(`param-placeholder-${node.id}-${p}`, 'warning', 'Expressions', node.id,
|
| 350 |
+
`Node "${node.name}" may contain placeholder value: "${p}"`,
|
| 351 |
+
'Replace placeholder with real configuration value'));
|
| 352 |
+
}
|
| 353 |
+
});
|
| 354 |
+
});
|
| 355 |
+
|
| 356 |
+
const score = Math.max(0, 100 - errors(issues) * 25 - warnings(issues) * 5);
|
| 357 |
+
return { passed: errors(issues) === 0, score, issues };
|
| 358 |
+
}
|
| 359 |
+
|
| 360 |
+
// βββ F. Data Flow Validation (NEW) βββββββββββββββββββββββββββββββββββββββ
|
| 361 |
+
private async validateDataFlow(workflow: N8nWorkflow, graph: WorkflowGraph): Promise<ValidationSection> {
|
| 362 |
+
const issues: ValidationIssue[] = [];
|
| 363 |
+
|
| 364 |
+
// Check that connected nodes actually reference upstream data
|
| 365 |
+
graph.edges.forEach((edge) => {
|
| 366 |
+
const targetNode = workflow.nodes.find((n) =>
|
| 367 |
+
n.name === graph.nodes.find((gn) => gn.id === edge.targetNodeId)?.label,
|
| 368 |
+
);
|
| 369 |
+
|
| 370 |
+
if (targetNode?.parameters) {
|
| 371 |
+
const paramsStr = JSON.stringify(targetNode.parameters);
|
| 372 |
+
const hasExpression = paramsStr.includes('{{') || paramsStr.includes('$json') || paramsStr.includes('$node');
|
| 373 |
+
|
| 374 |
+
// If node has inputs but no expressions at all, it's likely disconnected data-flow
|
| 375 |
+
const def = getNodeDef(targetNode.type);
|
| 376 |
+
if (
|
| 377 |
+
def &&
|
| 378 |
+
!def.isTrigger &&
|
| 379 |
+
!hasExpression &&
|
| 380 |
+
!['n8n-nodes-base.noOp', 'n8n-nodes-base.stickyNote', 'n8n-nodes-base.wait'].includes(targetNode.type)
|
| 381 |
+
) {
|
| 382 |
+
issues.push(this.issue(`dataflow-${edge.id}`, 'warning', 'DataFlow', targetNode.id,
|
| 383 |
+
`Node "${targetNode.name}" receives data but has no expressions referencing upstream fields`,
|
| 384 |
+
'Add expressions like {{$json?.fieldName ?? ""}} to consume upstream data'));
|
| 385 |
+
}
|
| 386 |
+
}
|
| 387 |
+
});
|
| 388 |
+
|
| 389 |
+
// SET node β verify values array is not empty
|
| 390 |
+
workflow.nodes?.filter((n) => n.type === 'n8n-nodes-base.set').forEach((node) => {
|
| 391 |
+
const values = (node.parameters as Record<string, unknown>)?.fields;
|
| 392 |
+
if (!values || JSON.stringify(values) === '{"values":[]}' || JSON.stringify(values) === '[]') {
|
| 393 |
+
issues.push(this.issue(`dataflow-set-empty-${node.id}`, 'error', 'DataFlow', node.id,
|
| 394 |
+
`SET node "${node.name}" has empty fields β it will output nothing`,
|
| 395 |
+
'Add field mappings: name β expression pairs'));
|
| 396 |
+
}
|
| 397 |
+
});
|
| 398 |
+
|
| 399 |
+
// CODE node β verify jsCode is not a placeholder
|
| 400 |
+
workflow.nodes?.filter((n) => n.type === 'n8n-nodes-base.code').forEach((node) => {
|
| 401 |
+
const jsCode = (node.parameters as Record<string, unknown>)?.jsCode as string | undefined;
|
| 402 |
+
if (!jsCode || jsCode.trim() === '' || jsCode.includes('// TODO') || jsCode === 'return [];') {
|
| 403 |
+
issues.push(this.issue(`dataflow-code-${node.id}`, 'error', 'DataFlow', node.id,
|
| 404 |
+
`Code node "${node.name}" has empty or placeholder code`,
|
| 405 |
+
'Add real JavaScript logic that processes $input.all() items'));
|
| 406 |
+
}
|
| 407 |
+
});
|
| 408 |
+
|
| 409 |
+
// IF node β verify conditions reference real fields
|
| 410 |
+
workflow.nodes?.filter((n) => n.type === 'n8n-nodes-base.if').forEach((node) => {
|
| 411 |
+
const conditions = (node.parameters as Record<string, unknown>)?.conditions as Record<string, unknown> | undefined;
|
| 412 |
+
const condList = (conditions?.conditions as unknown[]) ?? [];
|
| 413 |
+
if (condList.length === 0) {
|
| 414 |
+
issues.push(this.issue(`dataflow-if-${node.id}`, 'error', 'DataFlow', node.id,
|
| 415 |
+
`IF node "${node.name}" has no conditions defined`,
|
| 416 |
+
'Add at least one condition referencing a real upstream JSON field'));
|
| 417 |
+
}
|
| 418 |
+
});
|
| 419 |
+
|
| 420 |
+
const score = Math.max(0, 100 - errors(issues) * 20 - warnings(issues) * 8);
|
| 421 |
+
return { passed: errors(issues) === 0, score, issues };
|
| 422 |
+
}
|
| 423 |
+
|
| 424 |
+
// βββ G. Reliability Validation βββββββββββββββββββββββββββββββββββββββββββ
|
| 425 |
+
private async validateReliability(workflow: N8nWorkflow, graph: WorkflowGraph): Promise<ValidationSection> {
|
| 426 |
+
const issues: ValidationIssue[] = [];
|
| 427 |
+
|
| 428 |
+
// Retry policy on critical/external nodes
|
| 429 |
+
const criticalNodes = graph.nodes.filter((n) => n.isCritical);
|
| 430 |
+
criticalNodes.forEach((node) => {
|
| 431 |
+
const n8nNode = workflow.nodes.find((n) => n.name === node.label);
|
| 432 |
+
if (n8nNode && !n8nNode.retryOnFail) {
|
| 433 |
+
issues.push(this.issue(`rel-retry-${node.id}`, 'warning', 'Reliability', node.id,
|
| 434 |
+
`Critical node "${node.label}" has no retry policy`,
|
| 435 |
+
'Add retryOnFail: true, maxTries: 3, waitBetweenTries: 1000'));
|
| 436 |
+
}
|
| 437 |
+
});
|
| 438 |
+
|
| 439 |
+
// Check external API nodes have retry
|
| 440 |
+
workflow.nodes?.forEach((node) => {
|
| 441 |
+
const def = getNodeDef(node.type);
|
| 442 |
+
if (def?.supportsRetry && !node.retryOnFail) {
|
| 443 |
+
issues.push(this.issue(`rel-external-retry-${node.id}`, 'warning', 'Reliability', node.id,
|
| 444 |
+
`External node "${node.name}" (${node.type}) has no retry β may fail on transient errors`,
|
| 445 |
+
'Add retryOnFail: true, maxTries: 3, waitBetweenTries: 1000'));
|
| 446 |
+
}
|
| 447 |
+
});
|
| 448 |
+
|
| 449 |
+
// Error workflow
|
| 450 |
+
if (!workflow.settings?.errorWorkflow) {
|
| 451 |
+
issues.push(this.issue('rel-error-workflow', 'warning', 'Reliability', undefined,
|
| 452 |
+
'No error workflow configured',
|
| 453 |
+
'Set settings.errorWorkflow to an alerting/notification workflow ID'));
|
| 454 |
+
}
|
| 455 |
+
|
| 456 |
+
// Monitoring nodes
|
| 457 |
+
const hasMonitoring = graph.nodes.some((n) => n.layer === 'monitoring');
|
| 458 |
+
if (!hasMonitoring) {
|
| 459 |
+
issues.push(this.issue('rel-monitoring', 'warning', 'Reliability', undefined,
|
| 460 |
+
'No monitoring/observability layer nodes detected',
|
| 461 |
+
'Add logging or notification nodes for observability'));
|
| 462 |
+
}
|
| 463 |
+
|
| 464 |
+
const score = Math.max(0, 100 - errors(issues) * 20 - warnings(issues) * 8);
|
| 465 |
+
return { passed: errors(issues) === 0, score, issues };
|
| 466 |
+
}
|
| 467 |
+
|
| 468 |
+
private issue(
|
| 469 |
+
id: string,
|
| 470 |
+
severity: ValidationIssue['severity'],
|
| 471 |
+
category: string,
|
| 472 |
+
nodeId: string | undefined,
|
| 473 |
+
message: string,
|
| 474 |
+
suggestion: string,
|
| 475 |
+
): ValidationIssue {
|
| 476 |
+
return { id, severity, category, nodeId, message, suggestion };
|
| 477 |
+
}
|
| 478 |
+
}
|
| 479 |
+
|
| 480 |
+
// helpers
|
| 481 |
+
function errors(issues: ValidationIssue[]): number {
|
| 482 |
+
return issues.filter((i) => i.severity === 'error').length;
|
| 483 |
+
}
|
| 484 |
+
function warnings(issues: ValidationIssue[]): number {
|
| 485 |
+
return issues.filter((i) => i.severity === 'warning').length;
|
| 486 |
+
}
|
apps/simulator/src/agents/workflowPlanner.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Workflow Planner Agent β UPGRADED
|
| 3 |
+
* Designs workflows like a senior engineer using layered architecture
|
| 4 |
+
* Provider-agnostic LLM Gateway (no direct OpenAI dependency)
|
| 5 |
+
*/
|
| 6 |
+
import { type LLMGateway } from '@wfo/integrations/llm-providers/index';
|
| 7 |
+
import type { WorkflowIntent, WorkflowArchitecturePlan } from '../types/workflow';
|
| 8 |
+
import { WORKFLOW_PLANNER_PROMPT } from '../prompts/workflowPlanner';
|
| 9 |
+
|
| 10 |
+
export class WorkflowPlannerAgent {
|
| 11 |
+
private llm: LLMGateway;
|
| 12 |
+
|
| 13 |
+
constructor(llm: LLMGateway) {
|
| 14 |
+
this.llm = llm;
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
async plan(userRequest: string, intent: WorkflowIntent): Promise<WorkflowArchitecturePlan> {
|
| 18 |
+
return this.llm.completeJSON<WorkflowArchitecturePlan>([
|
| 19 |
+
{ role: 'system', content: WORKFLOW_PLANNER_PROMPT },
|
| 20 |
+
{
|
| 21 |
+
role: 'user',
|
| 22 |
+
content: `Design a production-grade workflow architecture for this request:
|
| 23 |
+
|
| 24 |
+
REQUEST: ${userRequest}
|
| 25 |
+
|
| 26 |
+
INTENT ANALYSIS:
|
| 27 |
+
${JSON.stringify(intent, null, 2)}
|
| 28 |
+
|
| 29 |
+
Return a complete WorkflowArchitecturePlan JSON following the specification.`,
|
| 30 |
+
},
|
| 31 |
+
], {
|
| 32 |
+
temperature: 0.1,
|
| 33 |
+
retries: 3,
|
| 34 |
+
});
|
| 35 |
+
}
|
| 36 |
+
}
|
apps/simulator/src/index.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Simulator Service - Main Entry Point β UPGRADED
|
| 3 |
+
* External Node.js compute service (Express.js)
|
| 4 |
+
* Handles all CPU-intensive AI orchestration logic offloaded from Cloudflare Worker
|
| 5 |
+
* Provider-agnostic LLM Gateway β NO direct OpenAI SDK dependency
|
| 6 |
+
*/
|
| 7 |
+
import * as moduleAlias from 'module-alias';
|
| 8 |
+
import path from 'path';
|
| 9 |
+
|
| 10 |
+
// Register module aliases for production
|
| 11 |
+
moduleAlias.addAliases({
|
| 12 |
+
'@wfo/core': path.join(__dirname, '../../../core'),
|
| 13 |
+
'@wfo/integrations': path.join(__dirname, '../../../integrations'),
|
| 14 |
+
'@wfo/validation': path.join(__dirname, '../../../validation'),
|
| 15 |
+
'@wfo/observability': path.join(__dirname, '../../../observability'),
|
| 16 |
+
'@wfo/knowledge': path.join(__dirname, '../../../knowledge'),
|
| 17 |
+
'@wfo/config': path.join(__dirname, '../../../config'),
|
| 18 |
+
'@wfo/apps/worker': path.join(__dirname, '../../../apps/worker'),
|
| 19 |
+
});
|
| 20 |
+
|
| 21 |
+
import express from 'express';
|
| 22 |
+
import helmet from 'helmet';
|
| 23 |
+
import cors from 'cors';
|
| 24 |
+
import { pino } from 'pino';
|
| 25 |
+
import 'dotenv/config';
|
| 26 |
+
|
| 27 |
+
import { internalAuthMiddleware } from './middleware/internalAuth';
|
| 28 |
+
import { generateRoute } from './routes/generate';
|
| 29 |
+
import { validateRoute } from './routes/validate';
|
| 30 |
+
import { simulateRoute } from './routes/simulate';
|
| 31 |
+
|
| 32 |
+
export const logger = pino({
|
| 33 |
+
transport: {
|
| 34 |
+
target: 'pino-pretty',
|
| 35 |
+
options: { colorize: true },
|
| 36 |
+
},
|
| 37 |
+
level: process.env['LOG_LEVEL'] ?? 'info',
|
| 38 |
+
});
|
| 39 |
+
|
| 40 |
+
const app: express.Application = express();
|
| 41 |
+
|
| 42 |
+
// βββ Security Middleware ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 43 |
+
app.use(helmet());
|
| 44 |
+
app.use(cors({ origin: process.env['WORKER_ORIGIN'] ?? '*' }));
|
| 45 |
+
app.use(express.json({ limit: '4mb' }));
|
| 46 |
+
|
| 47 |
+
// βββ Internal Auth (shared secret with CF Worker) ββββββββββββββββββββββββββββ
|
| 48 |
+
app.use('/process', internalAuthMiddleware);
|
| 49 |
+
|
| 50 |
+
// βββ Routes βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 51 |
+
app.use('/process/generate', generateRoute);
|
| 52 |
+
app.use('/process/validate', validateRoute);
|
| 53 |
+
app.use('/process/simulate', simulateRoute);
|
| 54 |
+
|
| 55 |
+
// βββ Health (public) ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 56 |
+
app.get('/health', (_req, res) => {
|
| 57 |
+
res.json({
|
| 58 |
+
status: 'ok',
|
| 59 |
+
service: 'wfo-simulator',
|
| 60 |
+
version: '2.0.0',
|
| 61 |
+
uptime: process.uptime(),
|
| 62 |
+
capabilities: {
|
| 63 |
+
llmGateway: 'provider-agnostic (openai | anthropic | openai-compatible | custom)',
|
| 64 |
+
swarmOrchestration: 'parallel multi-agent workflow design (up to 5 candidates)',
|
| 65 |
+
selfHealing: 'automatic validation fix + AI-assisted healing',
|
| 66 |
+
webhookAutoBind: 'auto trigger detection and configuration',
|
| 67 |
+
memorySystem: 'in-process pattern learning and anti-pattern prevention',
|
| 68 |
+
nodeRegistry: `strict β ${Object.keys(require('./knowledge/nodeRegistry').N8N_NODE_REGISTRY).length} verified node types`,
|
| 69 |
+
},
|
| 70 |
+
config: {
|
| 71 |
+
llmProvider: process.env['LLM_PROVIDER'] ?? 'openai',
|
| 72 |
+
llmModel: process.env['LLM_MODEL'] ?? 'gpt-4o',
|
| 73 |
+
llmGatewayUrl: process.env['LLM_GATEWAY_URL'] ?? 'https://api.openai.com/v1',
|
| 74 |
+
hasFallback: !!(process.env['LLM_FALLBACK_URL'] && process.env['LLM_FALLBACK_KEY']),
|
| 75 |
+
},
|
| 76 |
+
});
|
| 77 |
+
});
|
| 78 |
+
|
| 79 |
+
// βββ Global Error Handler βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 80 |
+
app.use((err: Error, _req: express.Request, res: express.Response, _next: express.NextFunction) => {
|
| 81 |
+
logger.error({ err }, 'Unhandled error');
|
| 82 |
+
res.status(500).json({ success: false, error: err.message });
|
| 83 |
+
});
|
| 84 |
+
|
| 85 |
+
const PORT = Number(process.env["PORT"] ?? 7860);
|
| 86 |
+
app.listen(PORT, () => {
|
| 87 |
+
logger.info(`[Simulator v2.0] Running on port ${PORT}`);
|
| 88 |
+
logger.info(`[Simulator] LLM provider: ${process.env['LLM_PROVIDER'] ?? 'openai'}`);
|
| 89 |
+
logger.info(`[Simulator] LLM model: ${process.env['LLM_MODEL'] ?? 'gpt-4o'}`);
|
| 90 |
+
logger.info(`[Simulator] Swarm: enabled (up to 5 parallel designs)`);
|
| 91 |
+
logger.info(`[Simulator] Self-Healing: enabled`);
|
| 92 |
+
logger.info(`[Simulator] Memory: enabled`);
|
| 93 |
+
logger.info(`[Simulator] Webhook Auto-Bind: enabled`);
|
| 94 |
+
});
|
| 95 |
+
|
| 96 |
+
export default app;
|
apps/simulator/src/knowledge/nodeRegistry.ts
ADDED
|
@@ -0,0 +1,1078 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* n8n Node Registry β PRODUCTION GRADE
|
| 3 |
+
* Real node definitions: type, version, parameters schema, credentials, I/O structure
|
| 4 |
+
* ZERO hallucinated nodes. ZERO placeholder types. ZERO unknown nodes.
|
| 5 |
+
* Source: n8n official node definitions (github.com/n8n-io/n8n)
|
| 6 |
+
*/
|
| 7 |
+
|
| 8 |
+
export interface NodeRegistryEntry {
|
| 9 |
+
type: string;
|
| 10 |
+
displayName: string;
|
| 11 |
+
typeVersion: number;
|
| 12 |
+
group: string[];
|
| 13 |
+
description: string;
|
| 14 |
+
isTrigger: boolean;
|
| 15 |
+
credentialType?: string;
|
| 16 |
+
credentialDisplayName?: string;
|
| 17 |
+
inputs: string[];
|
| 18 |
+
outputs: string[];
|
| 19 |
+
outputNames?: string[];
|
| 20 |
+
requiredParameters: string[];
|
| 21 |
+
optionalParameters: string[];
|
| 22 |
+
defaultParameters: Record<string, unknown>;
|
| 23 |
+
executionBehavior: 'once' | 'perItem' | 'trigger' | 'webhook';
|
| 24 |
+
supportsRetry: boolean;
|
| 25 |
+
layer: 'trigger' | 'transform' | 'ai' | 'integration' | 'control' | 'database' | 'utility';
|
| 26 |
+
tags: string[];
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
export const N8N_NODE_REGISTRY: Record<string, NodeRegistryEntry> = {
|
| 30 |
+
|
| 31 |
+
// βββ TRIGGER NODES ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 32 |
+
|
| 33 |
+
'n8n-nodes-base.webhook': {
|
| 34 |
+
type: 'n8n-nodes-base.webhook',
|
| 35 |
+
displayName: 'Webhook',
|
| 36 |
+
typeVersion: 2,
|
| 37 |
+
group: ['trigger'],
|
| 38 |
+
description: 'Starts the workflow when a webhook is received',
|
| 39 |
+
isTrigger: true,
|
| 40 |
+
inputs: [],
|
| 41 |
+
outputs: ['main'],
|
| 42 |
+
requiredParameters: ['httpMethod', 'path'],
|
| 43 |
+
optionalParameters: ['responseMode', 'responseData', 'responseCode', 'responseHeaders', 'rawBody', 'options'],
|
| 44 |
+
defaultParameters: {
|
| 45 |
+
httpMethod: 'POST',
|
| 46 |
+
path: '={{$runIndex === 0 ? "auto-webhook-" + $workflow.id : ""}}',
|
| 47 |
+
responseMode: 'onReceived',
|
| 48 |
+
responseData: 'allEntries',
|
| 49 |
+
},
|
| 50 |
+
executionBehavior: 'webhook',
|
| 51 |
+
supportsRetry: false,
|
| 52 |
+
layer: 'trigger',
|
| 53 |
+
tags: ['trigger', 'http', 'webhook'],
|
| 54 |
+
},
|
| 55 |
+
|
| 56 |
+
'n8n-nodes-base.scheduleTrigger': {
|
| 57 |
+
type: 'n8n-nodes-base.scheduleTrigger',
|
| 58 |
+
displayName: 'Schedule Trigger',
|
| 59 |
+
typeVersion: 1,
|
| 60 |
+
group: ['trigger'],
|
| 61 |
+
description: 'Triggers workflow on a schedule (cron or interval)',
|
| 62 |
+
isTrigger: true,
|
| 63 |
+
inputs: [],
|
| 64 |
+
outputs: ['main'],
|
| 65 |
+
requiredParameters: ['rule'],
|
| 66 |
+
optionalParameters: [],
|
| 67 |
+
defaultParameters: {
|
| 68 |
+
rule: {
|
| 69 |
+
interval: [{ field: 'hours', hoursInterval: 1 }],
|
| 70 |
+
},
|
| 71 |
+
},
|
| 72 |
+
executionBehavior: 'trigger',
|
| 73 |
+
supportsRetry: false,
|
| 74 |
+
layer: 'trigger',
|
| 75 |
+
tags: ['trigger', 'cron', 'schedule'],
|
| 76 |
+
},
|
| 77 |
+
|
| 78 |
+
'n8n-nodes-base.manualTrigger': {
|
| 79 |
+
type: 'n8n-nodes-base.manualTrigger',
|
| 80 |
+
displayName: 'Manual Trigger',
|
| 81 |
+
typeVersion: 1,
|
| 82 |
+
group: ['trigger'],
|
| 83 |
+
description: 'Starts workflow execution manually via the UI',
|
| 84 |
+
isTrigger: true,
|
| 85 |
+
inputs: [],
|
| 86 |
+
outputs: ['main'],
|
| 87 |
+
requiredParameters: [],
|
| 88 |
+
optionalParameters: [],
|
| 89 |
+
defaultParameters: {},
|
| 90 |
+
executionBehavior: 'trigger',
|
| 91 |
+
supportsRetry: false,
|
| 92 |
+
layer: 'trigger',
|
| 93 |
+
tags: ['trigger', 'manual'],
|
| 94 |
+
},
|
| 95 |
+
|
| 96 |
+
'n8n-nodes-base.telegramTrigger': {
|
| 97 |
+
type: 'n8n-nodes-base.telegramTrigger',
|
| 98 |
+
displayName: 'Telegram Trigger',
|
| 99 |
+
typeVersion: 1,
|
| 100 |
+
group: ['trigger'],
|
| 101 |
+
description: 'Starts workflow when a Telegram message or event is received',
|
| 102 |
+
isTrigger: true,
|
| 103 |
+
credentialType: 'telegramApi',
|
| 104 |
+
credentialDisplayName: 'Telegram API',
|
| 105 |
+
inputs: [],
|
| 106 |
+
outputs: ['main'],
|
| 107 |
+
requiredParameters: ['updates'],
|
| 108 |
+
optionalParameters: ['additionalFields'],
|
| 109 |
+
defaultParameters: {
|
| 110 |
+
updates: ['message'],
|
| 111 |
+
},
|
| 112 |
+
executionBehavior: 'webhook',
|
| 113 |
+
supportsRetry: false,
|
| 114 |
+
layer: 'trigger',
|
| 115 |
+
tags: ['trigger', 'telegram', 'messaging'],
|
| 116 |
+
},
|
| 117 |
+
|
| 118 |
+
'n8n-nodes-base.emailReadImap': {
|
| 119 |
+
type: 'n8n-nodes-base.emailReadImap',
|
| 120 |
+
displayName: 'Email Trigger (IMAP)',
|
| 121 |
+
typeVersion: 2,
|
| 122 |
+
group: ['trigger'],
|
| 123 |
+
description: 'Triggers workflow when a new email is received via IMAP',
|
| 124 |
+
isTrigger: true,
|
| 125 |
+
credentialType: 'imap',
|
| 126 |
+
credentialDisplayName: 'IMAP',
|
| 127 |
+
inputs: [],
|
| 128 |
+
outputs: ['main'],
|
| 129 |
+
requiredParameters: ['mailbox', 'action'],
|
| 130 |
+
optionalParameters: ['downloadAttachments', 'format', 'options'],
|
| 131 |
+
defaultParameters: {
|
| 132 |
+
mailbox: 'INBOX',
|
| 133 |
+
action: 'read',
|
| 134 |
+
},
|
| 135 |
+
executionBehavior: 'trigger',
|
| 136 |
+
supportsRetry: false,
|
| 137 |
+
layer: 'trigger',
|
| 138 |
+
tags: ['trigger', 'email', 'imap'],
|
| 139 |
+
},
|
| 140 |
+
|
| 141 |
+
'n8n-nodes-base.githubTrigger': {
|
| 142 |
+
type: 'n8n-nodes-base.githubTrigger',
|
| 143 |
+
displayName: 'GitHub Trigger',
|
| 144 |
+
typeVersion: 1,
|
| 145 |
+
group: ['trigger'],
|
| 146 |
+
description: 'Triggers on GitHub events (push, PR, issue, etc.)',
|
| 147 |
+
isTrigger: true,
|
| 148 |
+
credentialType: 'githubApi',
|
| 149 |
+
credentialDisplayName: 'GitHub API',
|
| 150 |
+
inputs: [],
|
| 151 |
+
outputs: ['main'],
|
| 152 |
+
requiredParameters: ['owner', 'repository', 'events'],
|
| 153 |
+
optionalParameters: [],
|
| 154 |
+
defaultParameters: {
|
| 155 |
+
events: ['push'],
|
| 156 |
+
},
|
| 157 |
+
executionBehavior: 'webhook',
|
| 158 |
+
supportsRetry: false,
|
| 159 |
+
layer: 'trigger',
|
| 160 |
+
tags: ['trigger', 'github', 'devops'],
|
| 161 |
+
},
|
| 162 |
+
|
| 163 |
+
'n8n-nodes-base.slackTrigger': {
|
| 164 |
+
type: 'n8n-nodes-base.slackTrigger',
|
| 165 |
+
displayName: 'Slack Trigger',
|
| 166 |
+
typeVersion: 1,
|
| 167 |
+
group: ['trigger'],
|
| 168 |
+
description: 'Triggers on Slack events (messages, reactions, etc.)',
|
| 169 |
+
isTrigger: true,
|
| 170 |
+
credentialType: 'slackOAuth2Api',
|
| 171 |
+
credentialDisplayName: 'Slack OAuth2 API',
|
| 172 |
+
inputs: [],
|
| 173 |
+
outputs: ['main'],
|
| 174 |
+
requiredParameters: ['trigger'],
|
| 175 |
+
optionalParameters: ['channelId', 'watchedEvent'],
|
| 176 |
+
defaultParameters: {
|
| 177 |
+
trigger: 'any_message',
|
| 178 |
+
},
|
| 179 |
+
executionBehavior: 'webhook',
|
| 180 |
+
supportsRetry: false,
|
| 181 |
+
layer: 'trigger',
|
| 182 |
+
tags: ['trigger', 'slack', 'messaging'],
|
| 183 |
+
},
|
| 184 |
+
|
| 185 |
+
'n8n-nodes-base.notionTrigger': {
|
| 186 |
+
type: 'n8n-nodes-base.notionTrigger',
|
| 187 |
+
displayName: 'Notion Trigger',
|
| 188 |
+
typeVersion: 1,
|
| 189 |
+
group: ['trigger'],
|
| 190 |
+
description: 'Triggers when a Notion database page is created or updated',
|
| 191 |
+
isTrigger: true,
|
| 192 |
+
credentialType: 'notionApi',
|
| 193 |
+
credentialDisplayName: 'Notion API',
|
| 194 |
+
inputs: [],
|
| 195 |
+
outputs: ['main'],
|
| 196 |
+
requiredParameters: ['databaseId', 'event'],
|
| 197 |
+
optionalParameters: ['simple'],
|
| 198 |
+
defaultParameters: {
|
| 199 |
+
event: 'page_added',
|
| 200 |
+
},
|
| 201 |
+
executionBehavior: 'trigger',
|
| 202 |
+
supportsRetry: false,
|
| 203 |
+
layer: 'trigger',
|
| 204 |
+
tags: ['trigger', 'notion', 'productivity'],
|
| 205 |
+
},
|
| 206 |
+
|
| 207 |
+
// βββ AI / LLM NODES βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 208 |
+
|
| 209 |
+
'@n8n/n8n-nodes-langchain.agent': {
|
| 210 |
+
type: '@n8n/n8n-nodes-langchain.agent',
|
| 211 |
+
displayName: 'AI Agent',
|
| 212 |
+
typeVersion: 1,
|
| 213 |
+
group: ['transform'],
|
| 214 |
+
description: 'AI Agent powered by LangChain with tools and memory',
|
| 215 |
+
isTrigger: false,
|
| 216 |
+
inputs: ['main', 'ai_tool', 'ai_memory', 'ai_languageModel'],
|
| 217 |
+
outputs: ['main'],
|
| 218 |
+
requiredParameters: ['text', 'options'],
|
| 219 |
+
optionalParameters: ['systemMessage', 'promptType'],
|
| 220 |
+
defaultParameters: {
|
| 221 |
+
text: '={{ $json?.message ?? $json?.text ?? $json?.body?.message ?? "" }}',
|
| 222 |
+
options: {
|
| 223 |
+
systemMessage: 'You are a helpful AI assistant. Be concise and accurate.',
|
| 224 |
+
},
|
| 225 |
+
},
|
| 226 |
+
executionBehavior: 'perItem',
|
| 227 |
+
supportsRetry: true,
|
| 228 |
+
layer: 'ai',
|
| 229 |
+
tags: ['ai', 'agent', 'langchain', 'llm'],
|
| 230 |
+
},
|
| 231 |
+
|
| 232 |
+
'@n8n/n8n-nodes-langchain.openAi': {
|
| 233 |
+
type: '@n8n/n8n-nodes-langchain.openAi',
|
| 234 |
+
displayName: 'OpenAI Chat Model',
|
| 235 |
+
typeVersion: 1,
|
| 236 |
+
group: ['transform'],
|
| 237 |
+
description: 'OpenAI Chat Model for use with AI Agent or Chain nodes',
|
| 238 |
+
isTrigger: false,
|
| 239 |
+
credentialType: 'openAiApi',
|
| 240 |
+
credentialDisplayName: 'OpenAI API',
|
| 241 |
+
inputs: ['ai_languageModel'],
|
| 242 |
+
outputs: ['ai_languageModel'],
|
| 243 |
+
requiredParameters: ['model'],
|
| 244 |
+
optionalParameters: ['options'],
|
| 245 |
+
defaultParameters: {
|
| 246 |
+
model: { __rl: true, value: 'gpt-4o', mode: 'list', cachedResultName: 'GPT-4o' },
|
| 247 |
+
options: {
|
| 248 |
+
maxTokens: 2048,
|
| 249 |
+
temperature: 0.7,
|
| 250 |
+
},
|
| 251 |
+
},
|
| 252 |
+
executionBehavior: 'once',
|
| 253 |
+
supportsRetry: true,
|
| 254 |
+
layer: 'ai',
|
| 255 |
+
tags: ['ai', 'openai', 'llm', 'langchain'],
|
| 256 |
+
},
|
| 257 |
+
|
| 258 |
+
'@n8n/n8n-nodes-langchain.lmChatAnthropic': {
|
| 259 |
+
type: '@n8n/n8n-nodes-langchain.lmChatAnthropic',
|
| 260 |
+
displayName: 'Anthropic Chat Model',
|
| 261 |
+
typeVersion: 1,
|
| 262 |
+
group: ['transform'],
|
| 263 |
+
description: 'Anthropic Claude model for use with AI Agent or Chain nodes',
|
| 264 |
+
isTrigger: false,
|
| 265 |
+
credentialType: 'anthropicApi',
|
| 266 |
+
credentialDisplayName: 'Anthropic API',
|
| 267 |
+
inputs: ['ai_languageModel'],
|
| 268 |
+
outputs: ['ai_languageModel'],
|
| 269 |
+
requiredParameters: ['model'],
|
| 270 |
+
optionalParameters: ['options'],
|
| 271 |
+
defaultParameters: {
|
| 272 |
+
model: 'claude-3-5-sonnet-20241022',
|
| 273 |
+
options: {
|
| 274 |
+
maxTokens: 2048,
|
| 275 |
+
temperature: 0.7,
|
| 276 |
+
},
|
| 277 |
+
},
|
| 278 |
+
executionBehavior: 'once',
|
| 279 |
+
supportsRetry: true,
|
| 280 |
+
layer: 'ai',
|
| 281 |
+
tags: ['ai', 'anthropic', 'claude', 'llm', 'langchain'],
|
| 282 |
+
},
|
| 283 |
+
|
| 284 |
+
'@n8n/n8n-nodes-langchain.memoryBufferWindow': {
|
| 285 |
+
type: '@n8n/n8n-nodes-langchain.memoryBufferWindow',
|
| 286 |
+
displayName: 'Window Buffer Memory',
|
| 287 |
+
typeVersion: 1,
|
| 288 |
+
group: ['transform'],
|
| 289 |
+
description: 'Keeps last N messages in memory for AI Agent context',
|
| 290 |
+
isTrigger: false,
|
| 291 |
+
inputs: ['ai_memory'],
|
| 292 |
+
outputs: ['ai_memory'],
|
| 293 |
+
requiredParameters: [],
|
| 294 |
+
optionalParameters: ['sessionKey', 'sessionIdType', 'contextWindowLength'],
|
| 295 |
+
defaultParameters: {
|
| 296 |
+
contextWindowLength: 10,
|
| 297 |
+
},
|
| 298 |
+
executionBehavior: 'once',
|
| 299 |
+
supportsRetry: false,
|
| 300 |
+
layer: 'ai',
|
| 301 |
+
tags: ['ai', 'memory', 'langchain'],
|
| 302 |
+
},
|
| 303 |
+
|
| 304 |
+
'@n8n/n8n-nodes-langchain.toolCode': {
|
| 305 |
+
type: '@n8n/n8n-nodes-langchain.toolCode',
|
| 306 |
+
displayName: 'Code Tool',
|
| 307 |
+
typeVersion: 1,
|
| 308 |
+
group: ['transform'],
|
| 309 |
+
description: 'Custom JavaScript tool for AI Agent',
|
| 310 |
+
isTrigger: false,
|
| 311 |
+
inputs: ['ai_tool'],
|
| 312 |
+
outputs: ['ai_tool'],
|
| 313 |
+
requiredParameters: ['name', 'description', 'jsCode'],
|
| 314 |
+
optionalParameters: [],
|
| 315 |
+
defaultParameters: {
|
| 316 |
+
name: 'custom_tool',
|
| 317 |
+
description: 'Describe what this tool does so the AI agent knows when to use it',
|
| 318 |
+
jsCode: '// Tool logic here\nreturn { result: "success" };',
|
| 319 |
+
},
|
| 320 |
+
executionBehavior: 'once',
|
| 321 |
+
supportsRetry: false,
|
| 322 |
+
layer: 'ai',
|
| 323 |
+
tags: ['ai', 'tool', 'langchain', 'code'],
|
| 324 |
+
},
|
| 325 |
+
|
| 326 |
+
'n8n-nodes-base.openAi': {
|
| 327 |
+
type: 'n8n-nodes-base.openAi',
|
| 328 |
+
displayName: 'OpenAI',
|
| 329 |
+
typeVersion: 1,
|
| 330 |
+
group: ['transform'],
|
| 331 |
+
description: 'Use the OpenAI API (Chat, Image, Audio, etc.)',
|
| 332 |
+
isTrigger: false,
|
| 333 |
+
credentialType: 'openAiApi',
|
| 334 |
+
credentialDisplayName: 'OpenAI API',
|
| 335 |
+
inputs: ['main'],
|
| 336 |
+
outputs: ['main'],
|
| 337 |
+
requiredParameters: ['resource', 'operation'],
|
| 338 |
+
optionalParameters: ['model', 'messages', 'options'],
|
| 339 |
+
defaultParameters: {
|
| 340 |
+
resource: 'chat',
|
| 341 |
+
operation: 'complete',
|
| 342 |
+
model: { __rl: true, value: 'gpt-4o', mode: 'list', cachedResultName: 'GPT-4o' },
|
| 343 |
+
},
|
| 344 |
+
executionBehavior: 'perItem',
|
| 345 |
+
supportsRetry: true,
|
| 346 |
+
layer: 'ai',
|
| 347 |
+
tags: ['ai', 'openai', 'llm'],
|
| 348 |
+
},
|
| 349 |
+
|
| 350 |
+
// βββ TRANSFORM / LOGIC NODES βββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 351 |
+
|
| 352 |
+
'n8n-nodes-base.code': {
|
| 353 |
+
type: 'n8n-nodes-base.code',
|
| 354 |
+
displayName: 'Code',
|
| 355 |
+
typeVersion: 2,
|
| 356 |
+
group: ['transform'],
|
| 357 |
+
description: 'Run custom JavaScript or Python code on items',
|
| 358 |
+
isTrigger: false,
|
| 359 |
+
inputs: ['main'],
|
| 360 |
+
outputs: ['main'],
|
| 361 |
+
requiredParameters: ['jsCode'],
|
| 362 |
+
optionalParameters: ['mode', 'language', 'pythonCode'],
|
| 363 |
+
defaultParameters: {
|
| 364 |
+
mode: 'runOnceForAllItems',
|
| 365 |
+
language: 'javaScript',
|
| 366 |
+
jsCode: `// Available: $input, $json, $items(), $env
|
| 367 |
+
const items = $input.all();
|
| 368 |
+
return items.map(item => {
|
| 369 |
+
const data = item.json;
|
| 370 |
+
return {
|
| 371 |
+
json: {
|
| 372 |
+
...data,
|
| 373 |
+
processed: true,
|
| 374 |
+
processedAt: new Date().toISOString(),
|
| 375 |
+
}
|
| 376 |
+
};
|
| 377 |
+
});`,
|
| 378 |
+
},
|
| 379 |
+
executionBehavior: 'once',
|
| 380 |
+
supportsRetry: false,
|
| 381 |
+
layer: 'transform',
|
| 382 |
+
tags: ['code', 'javascript', 'transform'],
|
| 383 |
+
},
|
| 384 |
+
|
| 385 |
+
'n8n-nodes-base.set': {
|
| 386 |
+
type: 'n8n-nodes-base.set',
|
| 387 |
+
displayName: 'Edit Fields (Set)',
|
| 388 |
+
typeVersion: 3,
|
| 389 |
+
group: ['transform'],
|
| 390 |
+
description: 'Set, add, or remove fields on items',
|
| 391 |
+
isTrigger: false,
|
| 392 |
+
inputs: ['main'],
|
| 393 |
+
outputs: ['main'],
|
| 394 |
+
requiredParameters: ['fields'],
|
| 395 |
+
optionalParameters: ['options', 'include'],
|
| 396 |
+
defaultParameters: {
|
| 397 |
+
mode: 'manual',
|
| 398 |
+
fields: {
|
| 399 |
+
values: [
|
| 400 |
+
{
|
| 401 |
+
name: 'outputField',
|
| 402 |
+
value: '={{ $json?.inputField ?? "" }}',
|
| 403 |
+
},
|
| 404 |
+
],
|
| 405 |
+
},
|
| 406 |
+
},
|
| 407 |
+
executionBehavior: 'perItem',
|
| 408 |
+
supportsRetry: false,
|
| 409 |
+
layer: 'transform',
|
| 410 |
+
tags: ['transform', 'set', 'fields'],
|
| 411 |
+
},
|
| 412 |
+
|
| 413 |
+
'n8n-nodes-base.if': {
|
| 414 |
+
type: 'n8n-nodes-base.if',
|
| 415 |
+
displayName: 'IF',
|
| 416 |
+
typeVersion: 2,
|
| 417 |
+
group: ['transform'],
|
| 418 |
+
description: 'Split items into two branches based on conditions',
|
| 419 |
+
isTrigger: false,
|
| 420 |
+
inputs: ['main'],
|
| 421 |
+
outputs: ['main', 'main'],
|
| 422 |
+
outputNames: ['true', 'false'],
|
| 423 |
+
requiredParameters: ['conditions'],
|
| 424 |
+
optionalParameters: ['combinator'],
|
| 425 |
+
defaultParameters: {
|
| 426 |
+
conditions: {
|
| 427 |
+
options: {
|
| 428 |
+
caseSensitive: true,
|
| 429 |
+
leftValue: '',
|
| 430 |
+
typeValidation: 'strict',
|
| 431 |
+
},
|
| 432 |
+
conditions: [
|
| 433 |
+
{
|
| 434 |
+
id: 'condition-1',
|
| 435 |
+
leftValue: '={{ $json?.status ?? "" }}',
|
| 436 |
+
rightValue: 'success',
|
| 437 |
+
operator: {
|
| 438 |
+
type: 'string',
|
| 439 |
+
operation: 'equals',
|
| 440 |
+
},
|
| 441 |
+
},
|
| 442 |
+
],
|
| 443 |
+
combinator: 'and',
|
| 444 |
+
},
|
| 445 |
+
},
|
| 446 |
+
executionBehavior: 'perItem',
|
| 447 |
+
supportsRetry: false,
|
| 448 |
+
layer: 'control',
|
| 449 |
+
tags: ['control', 'condition', 'branch'],
|
| 450 |
+
},
|
| 451 |
+
|
| 452 |
+
'n8n-nodes-base.switch': {
|
| 453 |
+
type: 'n8n-nodes-base.switch',
|
| 454 |
+
displayName: 'Switch',
|
| 455 |
+
typeVersion: 3,
|
| 456 |
+
group: ['transform'],
|
| 457 |
+
description: 'Route items to different outputs based on rules',
|
| 458 |
+
isTrigger: false,
|
| 459 |
+
inputs: ['main'],
|
| 460 |
+
outputs: ['main'],
|
| 461 |
+
requiredParameters: ['rules'],
|
| 462 |
+
optionalParameters: ['mode', 'fallbackOutput'],
|
| 463 |
+
defaultParameters: {
|
| 464 |
+
mode: 'rules',
|
| 465 |
+
rules: {
|
| 466 |
+
values: [
|
| 467 |
+
{
|
| 468 |
+
conditions: {
|
| 469 |
+
conditions: [
|
| 470 |
+
{
|
| 471 |
+
leftValue: '={{ $json?.type ?? "" }}',
|
| 472 |
+
rightValue: 'typeA',
|
| 473 |
+
operator: { type: 'string', operation: 'equals' },
|
| 474 |
+
},
|
| 475 |
+
],
|
| 476 |
+
combinator: 'and',
|
| 477 |
+
},
|
| 478 |
+
renameOutput: false,
|
| 479 |
+
},
|
| 480 |
+
],
|
| 481 |
+
},
|
| 482 |
+
fallbackOutput: 'none',
|
| 483 |
+
},
|
| 484 |
+
executionBehavior: 'perItem',
|
| 485 |
+
supportsRetry: false,
|
| 486 |
+
layer: 'control',
|
| 487 |
+
tags: ['control', 'switch', 'routing'],
|
| 488 |
+
},
|
| 489 |
+
|
| 490 |
+
'n8n-nodes-base.merge': {
|
| 491 |
+
type: 'n8n-nodes-base.merge',
|
| 492 |
+
displayName: 'Merge',
|
| 493 |
+
typeVersion: 3,
|
| 494 |
+
group: ['transform'],
|
| 495 |
+
description: 'Merge data from multiple branches',
|
| 496 |
+
isTrigger: false,
|
| 497 |
+
inputs: ['main', 'main'],
|
| 498 |
+
outputs: ['main'],
|
| 499 |
+
requiredParameters: ['mode'],
|
| 500 |
+
optionalParameters: ['joinMode', 'clashHandling', 'options'],
|
| 501 |
+
defaultParameters: {
|
| 502 |
+
mode: 'append',
|
| 503 |
+
},
|
| 504 |
+
executionBehavior: 'once',
|
| 505 |
+
supportsRetry: false,
|
| 506 |
+
layer: 'control',
|
| 507 |
+
tags: ['control', 'merge', 'combine'],
|
| 508 |
+
},
|
| 509 |
+
|
| 510 |
+
'n8n-nodes-base.splitInBatches': {
|
| 511 |
+
type: 'n8n-nodes-base.splitInBatches',
|
| 512 |
+
displayName: 'Split In Batches',
|
| 513 |
+
typeVersion: 3,
|
| 514 |
+
group: ['transform'],
|
| 515 |
+
description: 'Split large arrays into smaller batches for processing',
|
| 516 |
+
isTrigger: false,
|
| 517 |
+
inputs: ['main'],
|
| 518 |
+
outputs: ['main', 'main'],
|
| 519 |
+
outputNames: ['loop', 'done'],
|
| 520 |
+
requiredParameters: ['batchSize'],
|
| 521 |
+
optionalParameters: ['options'],
|
| 522 |
+
defaultParameters: {
|
| 523 |
+
batchSize: 10,
|
| 524 |
+
options: {},
|
| 525 |
+
},
|
| 526 |
+
executionBehavior: 'once',
|
| 527 |
+
supportsRetry: false,
|
| 528 |
+
layer: 'control',
|
| 529 |
+
tags: ['control', 'batch', 'loop', 'array'],
|
| 530 |
+
},
|
| 531 |
+
|
| 532 |
+
'n8n-nodes-base.itemLists': {
|
| 533 |
+
type: 'n8n-nodes-base.itemLists',
|
| 534 |
+
displayName: 'Item Lists',
|
| 535 |
+
typeVersion: 3,
|
| 536 |
+
group: ['transform'],
|
| 537 |
+
description: 'Manipulate arrays: split, aggregate, deduplicate, sort',
|
| 538 |
+
isTrigger: false,
|
| 539 |
+
inputs: ['main'],
|
| 540 |
+
outputs: ['main'],
|
| 541 |
+
requiredParameters: ['operation'],
|
| 542 |
+
optionalParameters: ['fieldToSplitOut', 'fieldToAggregate', 'sortFieldsUi', 'options'],
|
| 543 |
+
defaultParameters: {
|
| 544 |
+
operation: 'splitOutItems',
|
| 545 |
+
fieldToSplitOut: 'items',
|
| 546 |
+
include: 'noOtherFields',
|
| 547 |
+
},
|
| 548 |
+
executionBehavior: 'once',
|
| 549 |
+
supportsRetry: false,
|
| 550 |
+
layer: 'transform',
|
| 551 |
+
tags: ['transform', 'array', 'list'],
|
| 552 |
+
},
|
| 553 |
+
|
| 554 |
+
'n8n-nodes-base.filter': {
|
| 555 |
+
type: 'n8n-nodes-base.filter',
|
| 556 |
+
displayName: 'Filter',
|
| 557 |
+
typeVersion: 1,
|
| 558 |
+
group: ['transform'],
|
| 559 |
+
description: 'Filter items based on conditions, keeping only matching items',
|
| 560 |
+
isTrigger: false,
|
| 561 |
+
inputs: ['main'],
|
| 562 |
+
outputs: ['main'],
|
| 563 |
+
requiredParameters: ['conditions'],
|
| 564 |
+
optionalParameters: [],
|
| 565 |
+
defaultParameters: {
|
| 566 |
+
conditions: {
|
| 567 |
+
options: { caseSensitive: true, typeValidation: 'strict' },
|
| 568 |
+
conditions: [
|
| 569 |
+
{
|
| 570 |
+
id: 'filter-1',
|
| 571 |
+
leftValue: '={{ $json?.active ?? false }}',
|
| 572 |
+
rightValue: true,
|
| 573 |
+
operator: { type: 'boolean', operation: 'equals' },
|
| 574 |
+
},
|
| 575 |
+
],
|
| 576 |
+
combinator: 'and',
|
| 577 |
+
},
|
| 578 |
+
},
|
| 579 |
+
executionBehavior: 'perItem',
|
| 580 |
+
supportsRetry: false,
|
| 581 |
+
layer: 'transform',
|
| 582 |
+
tags: ['transform', 'filter', 'condition'],
|
| 583 |
+
},
|
| 584 |
+
|
| 585 |
+
// βββ HTTP / EXTERNAL API βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 586 |
+
|
| 587 |
+
'n8n-nodes-base.httpRequest': {
|
| 588 |
+
type: 'n8n-nodes-base.httpRequest',
|
| 589 |
+
displayName: 'HTTP Request',
|
| 590 |
+
typeVersion: 4,
|
| 591 |
+
group: ['output'],
|
| 592 |
+
description: 'Make HTTP requests to any API endpoint',
|
| 593 |
+
isTrigger: false,
|
| 594 |
+
inputs: ['main'],
|
| 595 |
+
outputs: ['main'],
|
| 596 |
+
requiredParameters: ['method', 'url'],
|
| 597 |
+
optionalParameters: ['authentication', 'sendHeaders', 'sendQuery', 'sendBody', 'options'],
|
| 598 |
+
defaultParameters: {
|
| 599 |
+
method: 'GET',
|
| 600 |
+
url: '',
|
| 601 |
+
options: {
|
| 602 |
+
timeout: 10000,
|
| 603 |
+
redirect: {
|
| 604 |
+
redirect: { followRedirects: true, maxRedirects: 3 },
|
| 605 |
+
},
|
| 606 |
+
},
|
| 607 |
+
},
|
| 608 |
+
executionBehavior: 'perItem',
|
| 609 |
+
supportsRetry: true,
|
| 610 |
+
layer: 'integration',
|
| 611 |
+
tags: ['http', 'api', 'rest', 'integration'],
|
| 612 |
+
},
|
| 613 |
+
|
| 614 |
+
// βββ MESSAGING βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 615 |
+
|
| 616 |
+
'n8n-nodes-base.telegram': {
|
| 617 |
+
type: 'n8n-nodes-base.telegram',
|
| 618 |
+
displayName: 'Telegram',
|
| 619 |
+
typeVersion: 1,
|
| 620 |
+
group: ['output'],
|
| 621 |
+
description: 'Send messages, photos, documents via Telegram Bot API',
|
| 622 |
+
isTrigger: false,
|
| 623 |
+
credentialType: 'telegramApi',
|
| 624 |
+
credentialDisplayName: 'Telegram API',
|
| 625 |
+
inputs: ['main'],
|
| 626 |
+
outputs: ['main'],
|
| 627 |
+
requiredParameters: ['operation', 'chatId'],
|
| 628 |
+
optionalParameters: ['text', 'photo', 'document', 'additionalFields'],
|
| 629 |
+
defaultParameters: {
|
| 630 |
+
operation: 'sendMessage',
|
| 631 |
+
chatId: '={{ $json?.message?.chat?.id ?? $json?.chatId ?? "" }}',
|
| 632 |
+
text: '={{ $json?.responseText ?? $json?.output ?? "" }}',
|
| 633 |
+
additionalFields: {
|
| 634 |
+
parse_mode: 'HTML',
|
| 635 |
+
},
|
| 636 |
+
},
|
| 637 |
+
executionBehavior: 'perItem',
|
| 638 |
+
supportsRetry: true,
|
| 639 |
+
layer: 'integration',
|
| 640 |
+
tags: ['telegram', 'messaging', 'bot'],
|
| 641 |
+
},
|
| 642 |
+
|
| 643 |
+
'n8n-nodes-base.slack': {
|
| 644 |
+
type: 'n8n-nodes-base.slack',
|
| 645 |
+
displayName: 'Slack',
|
| 646 |
+
typeVersion: 2,
|
| 647 |
+
group: ['output'],
|
| 648 |
+
description: 'Send messages and interact with Slack',
|
| 649 |
+
isTrigger: false,
|
| 650 |
+
credentialType: 'slackApi',
|
| 651 |
+
credentialDisplayName: 'Slack API',
|
| 652 |
+
inputs: ['main'],
|
| 653 |
+
outputs: ['main'],
|
| 654 |
+
requiredParameters: ['resource', 'operation', 'channel'],
|
| 655 |
+
optionalParameters: ['text', 'blocksUi', 'attachments', 'otherOptions'],
|
| 656 |
+
defaultParameters: {
|
| 657 |
+
resource: 'message',
|
| 658 |
+
operation: 'post',
|
| 659 |
+
channel: '',
|
| 660 |
+
text: '={{ $json?.message ?? $json?.text ?? "" }}',
|
| 661 |
+
},
|
| 662 |
+
executionBehavior: 'perItem',
|
| 663 |
+
supportsRetry: true,
|
| 664 |
+
layer: 'integration',
|
| 665 |
+
tags: ['slack', 'messaging', 'notification'],
|
| 666 |
+
},
|
| 667 |
+
|
| 668 |
+
'n8n-nodes-base.gmail': {
|
| 669 |
+
type: 'n8n-nodes-base.gmail',
|
| 670 |
+
displayName: 'Gmail',
|
| 671 |
+
typeVersion: 2,
|
| 672 |
+
group: ['output'],
|
| 673 |
+
description: 'Send and manage emails via Gmail',
|
| 674 |
+
isTrigger: false,
|
| 675 |
+
credentialType: 'googleOAuth2Api',
|
| 676 |
+
credentialDisplayName: 'Google OAuth2 API',
|
| 677 |
+
inputs: ['main'],
|
| 678 |
+
outputs: ['main'],
|
| 679 |
+
requiredParameters: ['operation', 'sendTo', 'subject'],
|
| 680 |
+
optionalParameters: ['message', 'attachmentsUi', 'options'],
|
| 681 |
+
defaultParameters: {
|
| 682 |
+
operation: 'send',
|
| 683 |
+
sendTo: '',
|
| 684 |
+
subject: '',
|
| 685 |
+
message: '',
|
| 686 |
+
options: {},
|
| 687 |
+
},
|
| 688 |
+
executionBehavior: 'perItem',
|
| 689 |
+
supportsRetry: true,
|
| 690 |
+
layer: 'integration',
|
| 691 |
+
tags: ['email', 'gmail', 'google'],
|
| 692 |
+
},
|
| 693 |
+
|
| 694 |
+
// βββ PRODUCTIVITY / STORAGE ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 695 |
+
|
| 696 |
+
'n8n-nodes-base.googleSheets': {
|
| 697 |
+
type: 'n8n-nodes-base.googleSheets',
|
| 698 |
+
displayName: 'Google Sheets',
|
| 699 |
+
typeVersion: 4,
|
| 700 |
+
group: ['output'],
|
| 701 |
+
description: 'Read, write, and manage Google Sheets data',
|
| 702 |
+
isTrigger: false,
|
| 703 |
+
credentialType: 'googleSheetsOAuth2Api',
|
| 704 |
+
credentialDisplayName: 'Google Sheets OAuth2 API',
|
| 705 |
+
inputs: ['main'],
|
| 706 |
+
outputs: ['main'],
|
| 707 |
+
requiredParameters: ['operation', 'documentId', 'sheetName'],
|
| 708 |
+
optionalParameters: ['columns', 'filtersUI', 'options'],
|
| 709 |
+
defaultParameters: {
|
| 710 |
+
operation: 'appendOrUpdate',
|
| 711 |
+
documentId: { __rl: true, value: '', mode: 'id' },
|
| 712 |
+
sheetName: { __rl: true, value: 'Sheet1', mode: 'name' },
|
| 713 |
+
},
|
| 714 |
+
executionBehavior: 'perItem',
|
| 715 |
+
supportsRetry: true,
|
| 716 |
+
layer: 'integration',
|
| 717 |
+
tags: ['google', 'sheets', 'spreadsheet', 'storage'],
|
| 718 |
+
},
|
| 719 |
+
|
| 720 |
+
'n8n-nodes-base.airtable': {
|
| 721 |
+
type: 'n8n-nodes-base.airtable',
|
| 722 |
+
displayName: 'Airtable',
|
| 723 |
+
typeVersion: 2,
|
| 724 |
+
group: ['output'],
|
| 725 |
+
description: 'Read, write, update, and delete Airtable records',
|
| 726 |
+
isTrigger: false,
|
| 727 |
+
credentialType: 'airtableTokenApi',
|
| 728 |
+
credentialDisplayName: 'Airtable Token API',
|
| 729 |
+
inputs: ['main'],
|
| 730 |
+
outputs: ['main'],
|
| 731 |
+
requiredParameters: ['operation', 'base', 'table'],
|
| 732 |
+
optionalParameters: ['fields', 'filterByFormula', 'sort', 'options'],
|
| 733 |
+
defaultParameters: {
|
| 734 |
+
operation: 'create',
|
| 735 |
+
base: { __rl: true, value: '', mode: 'id' },
|
| 736 |
+
table: { __rl: true, value: '', mode: 'id' },
|
| 737 |
+
},
|
| 738 |
+
executionBehavior: 'perItem',
|
| 739 |
+
supportsRetry: true,
|
| 740 |
+
layer: 'integration',
|
| 741 |
+
tags: ['airtable', 'database', 'storage'],
|
| 742 |
+
},
|
| 743 |
+
|
| 744 |
+
'n8n-nodes-base.notion': {
|
| 745 |
+
type: 'n8n-nodes-base.notion',
|
| 746 |
+
displayName: 'Notion',
|
| 747 |
+
typeVersion: 2,
|
| 748 |
+
group: ['output'],
|
| 749 |
+
description: 'Create, read, and update Notion pages and databases',
|
| 750 |
+
isTrigger: false,
|
| 751 |
+
credentialType: 'notionApi',
|
| 752 |
+
credentialDisplayName: 'Notion API',
|
| 753 |
+
inputs: ['main'],
|
| 754 |
+
outputs: ['main'],
|
| 755 |
+
requiredParameters: ['resource', 'operation'],
|
| 756 |
+
optionalParameters: ['pageId', 'databaseId', 'title', 'properties', 'blockUi'],
|
| 757 |
+
defaultParameters: {
|
| 758 |
+
resource: 'page',
|
| 759 |
+
operation: 'create',
|
| 760 |
+
},
|
| 761 |
+
executionBehavior: 'perItem',
|
| 762 |
+
supportsRetry: true,
|
| 763 |
+
layer: 'integration',
|
| 764 |
+
tags: ['notion', 'productivity', 'database'],
|
| 765 |
+
},
|
| 766 |
+
|
| 767 |
+
// βββ DATABASE ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 768 |
+
|
| 769 |
+
'n8n-nodes-base.postgres': {
|
| 770 |
+
type: 'n8n-nodes-base.postgres',
|
| 771 |
+
displayName: 'Postgres',
|
| 772 |
+
typeVersion: 2,
|
| 773 |
+
group: ['output'],
|
| 774 |
+
description: 'Execute SQL queries and interact with PostgreSQL databases',
|
| 775 |
+
isTrigger: false,
|
| 776 |
+
credentialType: 'postgres',
|
| 777 |
+
credentialDisplayName: 'Postgres',
|
| 778 |
+
inputs: ['main'],
|
| 779 |
+
outputs: ['main'],
|
| 780 |
+
requiredParameters: ['operation'],
|
| 781 |
+
optionalParameters: ['table', 'query', 'columns', 'additionalFields', 'options'],
|
| 782 |
+
defaultParameters: {
|
| 783 |
+
operation: 'insert',
|
| 784 |
+
table: { __rl: true, value: '', mode: 'name' },
|
| 785 |
+
},
|
| 786 |
+
executionBehavior: 'perItem',
|
| 787 |
+
supportsRetry: true,
|
| 788 |
+
layer: 'database',
|
| 789 |
+
tags: ['database', 'sql', 'postgres'],
|
| 790 |
+
},
|
| 791 |
+
|
| 792 |
+
'n8n-nodes-base.mysql': {
|
| 793 |
+
type: 'n8n-nodes-base.mysql',
|
| 794 |
+
displayName: 'MySQL',
|
| 795 |
+
typeVersion: 2,
|
| 796 |
+
group: ['output'],
|
| 797 |
+
description: 'Execute queries and interact with MySQL databases',
|
| 798 |
+
isTrigger: false,
|
| 799 |
+
credentialType: 'mySql',
|
| 800 |
+
credentialDisplayName: 'MySQL',
|
| 801 |
+
inputs: ['main'],
|
| 802 |
+
outputs: ['main'],
|
| 803 |
+
requiredParameters: ['operation'],
|
| 804 |
+
optionalParameters: ['table', 'query', 'columns', 'options'],
|
| 805 |
+
defaultParameters: {
|
| 806 |
+
operation: 'insert',
|
| 807 |
+
table: '',
|
| 808 |
+
},
|
| 809 |
+
executionBehavior: 'perItem',
|
| 810 |
+
supportsRetry: true,
|
| 811 |
+
layer: 'database',
|
| 812 |
+
tags: ['database', 'sql', 'mysql'],
|
| 813 |
+
},
|
| 814 |
+
|
| 815 |
+
'n8n-nodes-base.redis': {
|
| 816 |
+
type: 'n8n-nodes-base.redis',
|
| 817 |
+
displayName: 'Redis',
|
| 818 |
+
typeVersion: 1,
|
| 819 |
+
group: ['output'],
|
| 820 |
+
description: 'Get, set, and manage Redis key-value data',
|
| 821 |
+
isTrigger: false,
|
| 822 |
+
credentialType: 'redis',
|
| 823 |
+
credentialDisplayName: 'Redis',
|
| 824 |
+
inputs: ['main'],
|
| 825 |
+
outputs: ['main'],
|
| 826 |
+
requiredParameters: ['operation'],
|
| 827 |
+
optionalParameters: ['key', 'value', 'expire', 'keyType'],
|
| 828 |
+
defaultParameters: {
|
| 829 |
+
operation: 'get',
|
| 830 |
+
key: '',
|
| 831 |
+
},
|
| 832 |
+
executionBehavior: 'perItem',
|
| 833 |
+
supportsRetry: true,
|
| 834 |
+
layer: 'database',
|
| 835 |
+
tags: ['database', 'cache', 'redis'],
|
| 836 |
+
},
|
| 837 |
+
|
| 838 |
+
// βββ CRM / BUSINESS ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 839 |
+
|
| 840 |
+
'n8n-nodes-base.hubspot': {
|
| 841 |
+
type: 'n8n-nodes-base.hubspot',
|
| 842 |
+
displayName: 'HubSpot',
|
| 843 |
+
typeVersion: 2,
|
| 844 |
+
group: ['output'],
|
| 845 |
+
description: 'Manage contacts, deals, companies, and tickets in HubSpot CRM',
|
| 846 |
+
isTrigger: false,
|
| 847 |
+
credentialType: 'hubspotApi',
|
| 848 |
+
credentialDisplayName: 'HubSpot API',
|
| 849 |
+
inputs: ['main'],
|
| 850 |
+
outputs: ['main'],
|
| 851 |
+
requiredParameters: ['resource', 'operation'],
|
| 852 |
+
optionalParameters: ['contactId', 'dealId', 'additionalFields'],
|
| 853 |
+
defaultParameters: {
|
| 854 |
+
resource: 'contact',
|
| 855 |
+
operation: 'create',
|
| 856 |
+
},
|
| 857 |
+
executionBehavior: 'perItem',
|
| 858 |
+
supportsRetry: true,
|
| 859 |
+
layer: 'integration',
|
| 860 |
+
tags: ['crm', 'hubspot', 'sales'],
|
| 861 |
+
},
|
| 862 |
+
|
| 863 |
+
'n8n-nodes-base.stripe': {
|
| 864 |
+
type: 'n8n-nodes-base.stripe',
|
| 865 |
+
displayName: 'Stripe',
|
| 866 |
+
typeVersion: 1,
|
| 867 |
+
group: ['output'],
|
| 868 |
+
description: 'Manage customers, charges, subscriptions via Stripe',
|
| 869 |
+
isTrigger: false,
|
| 870 |
+
credentialType: 'stripeApi',
|
| 871 |
+
credentialDisplayName: 'Stripe API',
|
| 872 |
+
inputs: ['main'],
|
| 873 |
+
outputs: ['main'],
|
| 874 |
+
requiredParameters: ['resource', 'operation'],
|
| 875 |
+
optionalParameters: ['customerId', 'amount', 'currency', 'additionalFields'],
|
| 876 |
+
defaultParameters: {
|
| 877 |
+
resource: 'customer',
|
| 878 |
+
operation: 'get',
|
| 879 |
+
},
|
| 880 |
+
executionBehavior: 'perItem',
|
| 881 |
+
supportsRetry: true,
|
| 882 |
+
layer: 'integration',
|
| 883 |
+
tags: ['payment', 'stripe', 'ecommerce'],
|
| 884 |
+
},
|
| 885 |
+
|
| 886 |
+
'n8n-nodes-base.github': {
|
| 887 |
+
type: 'n8n-nodes-base.github',
|
| 888 |
+
displayName: 'GitHub',
|
| 889 |
+
typeVersion: 1,
|
| 890 |
+
group: ['output'],
|
| 891 |
+
description: 'Interact with GitHub repositories, issues, PRs',
|
| 892 |
+
isTrigger: false,
|
| 893 |
+
credentialType: 'githubApi',
|
| 894 |
+
credentialDisplayName: 'GitHub API',
|
| 895 |
+
inputs: ['main'],
|
| 896 |
+
outputs: ['main'],
|
| 897 |
+
requiredParameters: ['resource', 'operation', 'owner', 'repository'],
|
| 898 |
+
optionalParameters: ['title', 'body', 'labels', 'additionalParameters'],
|
| 899 |
+
defaultParameters: {
|
| 900 |
+
resource: 'issue',
|
| 901 |
+
operation: 'create',
|
| 902 |
+
owner: '',
|
| 903 |
+
repository: '',
|
| 904 |
+
},
|
| 905 |
+
executionBehavior: 'perItem',
|
| 906 |
+
supportsRetry: true,
|
| 907 |
+
layer: 'integration',
|
| 908 |
+
tags: ['github', 'devops', 'git'],
|
| 909 |
+
},
|
| 910 |
+
|
| 911 |
+
// βββ UTILITY NODES βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 912 |
+
|
| 913 |
+
'n8n-nodes-base.noOp': {
|
| 914 |
+
type: 'n8n-nodes-base.noOp',
|
| 915 |
+
displayName: 'No Operation, do nothing',
|
| 916 |
+
typeVersion: 1,
|
| 917 |
+
group: ['utility'],
|
| 918 |
+
description: 'Passes items through without modification (useful as placeholders)',
|
| 919 |
+
isTrigger: false,
|
| 920 |
+
inputs: ['main'],
|
| 921 |
+
outputs: ['main'],
|
| 922 |
+
requiredParameters: [],
|
| 923 |
+
optionalParameters: [],
|
| 924 |
+
defaultParameters: {},
|
| 925 |
+
executionBehavior: 'perItem',
|
| 926 |
+
supportsRetry: false,
|
| 927 |
+
layer: 'utility',
|
| 928 |
+
tags: ['utility', 'passthrough'],
|
| 929 |
+
},
|
| 930 |
+
|
| 931 |
+
'n8n-nodes-base.stopAndError': {
|
| 932 |
+
type: 'n8n-nodes-base.stopAndError',
|
| 933 |
+
displayName: 'Stop and Error',
|
| 934 |
+
typeVersion: 1,
|
| 935 |
+
group: ['utility'],
|
| 936 |
+
description: 'Stop workflow execution and throw a custom error',
|
| 937 |
+
isTrigger: false,
|
| 938 |
+
inputs: ['main'],
|
| 939 |
+
outputs: [],
|
| 940 |
+
requiredParameters: ['errorType'],
|
| 941 |
+
optionalParameters: ['errorMessage', 'errorObject'],
|
| 942 |
+
defaultParameters: {
|
| 943 |
+
errorType: 'errorMessage',
|
| 944 |
+
errorMessage: '={{ "Workflow stopped: " + ($json?.reason ?? "unknown error") }}',
|
| 945 |
+
},
|
| 946 |
+
executionBehavior: 'perItem',
|
| 947 |
+
supportsRetry: false,
|
| 948 |
+
layer: 'utility',
|
| 949 |
+
tags: ['utility', 'error', 'stop'],
|
| 950 |
+
},
|
| 951 |
+
|
| 952 |
+
'n8n-nodes-base.wait': {
|
| 953 |
+
type: 'n8n-nodes-base.wait',
|
| 954 |
+
displayName: 'Wait',
|
| 955 |
+
typeVersion: 1,
|
| 956 |
+
group: ['utility'],
|
| 957 |
+
description: 'Pause workflow execution for a specified time',
|
| 958 |
+
isTrigger: false,
|
| 959 |
+
inputs: ['main'],
|
| 960 |
+
outputs: ['main'],
|
| 961 |
+
requiredParameters: ['amount', 'unit'],
|
| 962 |
+
optionalParameters: [],
|
| 963 |
+
defaultParameters: {
|
| 964 |
+
amount: 1,
|
| 965 |
+
unit: 'seconds',
|
| 966 |
+
},
|
| 967 |
+
executionBehavior: 'once',
|
| 968 |
+
supportsRetry: false,
|
| 969 |
+
layer: 'utility',
|
| 970 |
+
tags: ['utility', 'wait', 'delay'],
|
| 971 |
+
},
|
| 972 |
+
|
| 973 |
+
'n8n-nodes-base.respondToWebhook': {
|
| 974 |
+
type: 'n8n-nodes-base.respondToWebhook',
|
| 975 |
+
displayName: 'Respond to Webhook',
|
| 976 |
+
typeVersion: 1,
|
| 977 |
+
group: ['utility'],
|
| 978 |
+
description: 'Send a custom response back to the webhook caller',
|
| 979 |
+
isTrigger: false,
|
| 980 |
+
inputs: ['main'],
|
| 981 |
+
outputs: ['main'],
|
| 982 |
+
requiredParameters: ['respondWith'],
|
| 983 |
+
optionalParameters: ['responseBody', 'responseCode', 'responseHeaders', 'options'],
|
| 984 |
+
defaultParameters: {
|
| 985 |
+
respondWith: 'json',
|
| 986 |
+
responseBody: '={{ JSON.stringify({ success: true, data: $json }) }}',
|
| 987 |
+
responseCode: 200,
|
| 988 |
+
},
|
| 989 |
+
executionBehavior: 'once',
|
| 990 |
+
supportsRetry: false,
|
| 991 |
+
layer: 'utility',
|
| 992 |
+
tags: ['webhook', 'response', 'utility'],
|
| 993 |
+
},
|
| 994 |
+
|
| 995 |
+
'n8n-nodes-base.stickyNote': {
|
| 996 |
+
type: 'n8n-nodes-base.stickyNote',
|
| 997 |
+
displayName: 'Sticky Note',
|
| 998 |
+
typeVersion: 1,
|
| 999 |
+
group: ['annotation'],
|
| 1000 |
+
description: 'Add documentation notes to the workflow canvas',
|
| 1001 |
+
isTrigger: false,
|
| 1002 |
+
inputs: [],
|
| 1003 |
+
outputs: [],
|
| 1004 |
+
requiredParameters: ['content'],
|
| 1005 |
+
optionalParameters: ['height', 'width', 'color'],
|
| 1006 |
+
defaultParameters: {
|
| 1007 |
+
content: '## Workflow Documentation\n\nDescribe this section here.',
|
| 1008 |
+
height: 200,
|
| 1009 |
+
width: 300,
|
| 1010 |
+
color: 4,
|
| 1011 |
+
},
|
| 1012 |
+
executionBehavior: 'once',
|
| 1013 |
+
supportsRetry: false,
|
| 1014 |
+
layer: 'utility',
|
| 1015 |
+
tags: ['documentation', 'annotation'],
|
| 1016 |
+
},
|
| 1017 |
+
};
|
| 1018 |
+
|
| 1019 |
+
// βββ Registry Lookup Helpers ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1020 |
+
|
| 1021 |
+
/**
|
| 1022 |
+
* Get a node definition. Returns undefined if node type is not in registry.
|
| 1023 |
+
* NEVER return a fallback for unknown types β callers must handle undefined.
|
| 1024 |
+
*/
|
| 1025 |
+
export function getNodeDef(nodeType: string): NodeRegistryEntry | undefined {
|
| 1026 |
+
return N8N_NODE_REGISTRY[nodeType];
|
| 1027 |
+
}
|
| 1028 |
+
|
| 1029 |
+
/**
|
| 1030 |
+
* Validate that a node type exists in the registry
|
| 1031 |
+
*/
|
| 1032 |
+
export function isValidNodeType(nodeType: string): boolean {
|
| 1033 |
+
return nodeType in N8N_NODE_REGISTRY;
|
| 1034 |
+
}
|
| 1035 |
+
|
| 1036 |
+
/**
|
| 1037 |
+
* Get all trigger node types
|
| 1038 |
+
*/
|
| 1039 |
+
export function getTriggerNodeTypes(): string[] {
|
| 1040 |
+
return Object.entries(N8N_NODE_REGISTRY)
|
| 1041 |
+
.filter(([, def]) => def.isTrigger)
|
| 1042 |
+
.map(([type]) => type);
|
| 1043 |
+
}
|
| 1044 |
+
|
| 1045 |
+
/**
|
| 1046 |
+
* Get all node types for a given tag
|
| 1047 |
+
*/
|
| 1048 |
+
export function getNodeTypesByTag(tag: string): string[] {
|
| 1049 |
+
return Object.entries(N8N_NODE_REGISTRY)
|
| 1050 |
+
.filter(([, def]) => def.tags.includes(tag))
|
| 1051 |
+
.map(([type]) => type);
|
| 1052 |
+
}
|
| 1053 |
+
|
| 1054 |
+
/**
|
| 1055 |
+
* Get credential type required for a node type
|
| 1056 |
+
*/
|
| 1057 |
+
export function getRequiredCredential(nodeType: string): string | undefined {
|
| 1058 |
+
return N8N_NODE_REGISTRY[nodeType]?.credentialType;
|
| 1059 |
+
}
|
| 1060 |
+
|
| 1061 |
+
/**
|
| 1062 |
+
* Get all registered node types as a list (for LLM context injection)
|
| 1063 |
+
*/
|
| 1064 |
+
export function getRegistryNodeList(): string {
|
| 1065 |
+
return Object.entries(N8N_NODE_REGISTRY)
|
| 1066 |
+
.map(([type, def]) => `- ${type} (v${def.typeVersion}): ${def.description}`)
|
| 1067 |
+
.join('\n');
|
| 1068 |
+
}
|
| 1069 |
+
|
| 1070 |
+
/**
|
| 1071 |
+
* Get all trigger node types as formatted string for prompts
|
| 1072 |
+
*/
|
| 1073 |
+
export function getTriggerNodeList(): string {
|
| 1074 |
+
return Object.entries(N8N_NODE_REGISTRY)
|
| 1075 |
+
.filter(([, def]) => def.isTrigger)
|
| 1076 |
+
.map(([type, def]) => `- ${type}: ${def.description}`)
|
| 1077 |
+
.join('\n');
|
| 1078 |
+
}
|
apps/simulator/src/middleware/internalAuth.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Internal Auth Middleware
|
| 3 |
+
* Validates shared secret between CF Worker and Simulator service
|
| 4 |
+
*/
|
| 5 |
+
import type { Request, Response, NextFunction } from 'express';
|
| 6 |
+
|
| 7 |
+
export function internalAuthMiddleware(req: Request, res: Response, next: NextFunction): void {
|
| 8 |
+
const secret = req.headers['x-internal-secret'];
|
| 9 |
+
const expected = process.env['INTERNAL_API_SECRET'];
|
| 10 |
+
|
| 11 |
+
if (!expected) {
|
| 12 |
+
// No secret configured β skip auth in development
|
| 13 |
+
if (process.env['NODE_ENV'] !== 'production') {
|
| 14 |
+
next();
|
| 15 |
+
return;
|
| 16 |
+
}
|
| 17 |
+
res.status(500).json({ success: false, error: 'INTERNAL_API_SECRET not configured' });
|
| 18 |
+
return;
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
if (!secret || secret !== expected) {
|
| 22 |
+
res.status(401).json({ success: false, error: 'Unauthorized β invalid internal secret' });
|
| 23 |
+
return;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
next();
|
| 27 |
+
}
|
apps/simulator/src/prompts/compiler.ts
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Compiler System Prompt β UPGRADED
|
| 3 |
+
* Role: Convert WorkflowGraph IR to valid, importable n8n JSON
|
| 4 |
+
* Strict expression rules, real data flow, no static placeholders
|
| 5 |
+
*/
|
| 6 |
+
export const COMPILER_PROMPT = `You are a senior n8n workflow compiler. Convert a WorkflowGraph IR into a valid, importable n8n JSON workflow.
|
| 7 |
+
|
| 8 |
+
COMPILATION RULES (NON-NEGOTIABLE):
|
| 9 |
+
|
| 10 |
+
1. active MUST always be false β NEVER set active: true under any circumstances
|
| 11 |
+
2. All node IDs must be unique UUID strings (format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)
|
| 12 |
+
3. typeVersion must match actual n8n node version from the schema provided
|
| 13 |
+
4. connections MUST properly map all graph edges:
|
| 14 |
+
{ "NodeDisplayName": { "main": [[{ "node": "TargetDisplayName", "type": "main", "index": 0 }]] } }
|
| 15 |
+
5. Node positions MUST be [x, y] tuple arrays (not objects)
|
| 16 |
+
6. Add retryOnFail: true, maxTries: 3, waitBetweenTries: 1000 to ALL external API nodes
|
| 17 |
+
7. Add meaningful notes to EVERY node (what it does, what data flows through)
|
| 18 |
+
8. NEVER invent parameters not in the schema β only use real n8n node parameters
|
| 19 |
+
9. settings MUST include: { "executionOrder": "v1", "saveManualExecutions": true, "timezone": "UTC" }
|
| 20 |
+
10. onError: "continueErrorOutput" on all error-boundary nodes
|
| 21 |
+
|
| 22 |
+
EXPRESSION RULES (MANDATORY β NO EXCEPTIONS):
|
| 23 |
+
All dynamic fields MUST use real n8n expressions:
|
| 24 |
+
|
| 25 |
+
β
CORRECT expressions:
|
| 26 |
+
- {{$json?.message ?? ""}} β safe access with nullish coalescing
|
| 27 |
+
- {{$json?.body?.text ?? $json?.text ?? ""}} β multi-path with fallback
|
| 28 |
+
- {{$node["NodeName"].json?.field ?? ""}} β cross-node reference
|
| 29 |
+
- {{$json?.items?.length > 0 ? "yes" : "no"}} β conditional
|
| 30 |
+
- {{$json?.chatId ?? $json?.message?.chat?.id ?? ""}} β multiple fallback paths
|
| 31 |
+
- {{new Date().toISOString()}} β dynamic date
|
| 32 |
+
- {{$json?.price * 1.1}} β arithmetic
|
| 33 |
+
|
| 34 |
+
β WRONG (never use these):
|
| 35 |
+
- "" β empty string
|
| 36 |
+
- "YOUR_VALUE_HERE" β placeholder
|
| 37 |
+
- "example.com" β example URLs
|
| 38 |
+
- "REPLACE_ME" β placeholder
|
| 39 |
+
- {{$json.field}} β without optional chaining (may throw null error)
|
| 40 |
+
|
| 41 |
+
NODE-SPECIFIC RULES:
|
| 42 |
+
|
| 43 |
+
SET NODE β MUST have explicit field mappings:
|
| 44 |
+
{
|
| 45 |
+
"mode": "manual",
|
| 46 |
+
"fields": {
|
| 47 |
+
"values": [
|
| 48 |
+
{ "name": "userMessage", "value": "={{$json?.body?.message ?? $json?.message ?? ''}}" },
|
| 49 |
+
{ "name": "chatId", "value": "={{$json?.body?.chat?.id ?? $json?.chatId ?? ''}}" }
|
| 50 |
+
]
|
| 51 |
+
}
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
IF NODE β MUST have real conditions referencing actual fields:
|
| 55 |
+
{
|
| 56 |
+
"conditions": {
|
| 57 |
+
"conditions": [
|
| 58 |
+
{
|
| 59 |
+
"id": "cond-1",
|
| 60 |
+
"leftValue": "={{$json?.status ?? ''}}",
|
| 61 |
+
"rightValue": "active",
|
| 62 |
+
"operator": { "type": "string", "operation": "equals" }
|
| 63 |
+
}
|
| 64 |
+
],
|
| 65 |
+
"combinator": "and"
|
| 66 |
+
}
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
CODE NODE β MUST have real JavaScript logic:
|
| 70 |
+
jsCode: "const items = $input.all();\nreturn items.map(item => ({\n json: {\n ...item.json,\n processed: true,\n processedAt: new Date().toISOString()\n }\n}));"
|
| 71 |
+
|
| 72 |
+
AI AGENT NODE β MUST have system message + dynamic user input:
|
| 73 |
+
{
|
| 74 |
+
"text": "={{$json?.userMessage ?? $json?.message ?? ''}}",
|
| 75 |
+
"options": {
|
| 76 |
+
"systemMessage": "You are a helpful assistant. Context: {{$json?.context ?? 'None'}}. Be concise and accurate."
|
| 77 |
+
}
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
SWITCH NODE β MUST have real rules:
|
| 81 |
+
{
|
| 82 |
+
"mode": "rules",
|
| 83 |
+
"rules": {
|
| 84 |
+
"values": [{
|
| 85 |
+
"conditions": {
|
| 86 |
+
"conditions": [{ "leftValue": "={{$json?.type ?? ''}}", "rightValue": "A", "operator": { "type": "string", "operation": "equals" } }],
|
| 87 |
+
"combinator": "and"
|
| 88 |
+
},
|
| 89 |
+
"renameOutput": false
|
| 90 |
+
}]
|
| 91 |
+
}
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
n8n WORKFLOW JSON SCHEMA:
|
| 95 |
+
{
|
| 96 |
+
"name": "Workflow Name",
|
| 97 |
+
"active": false,
|
| 98 |
+
"nodes": [
|
| 99 |
+
{
|
| 100 |
+
"id": "uuid-string",
|
| 101 |
+
"name": "Display Name",
|
| 102 |
+
"type": "n8n-nodes-base.webhook",
|
| 103 |
+
"typeVersion": 2,
|
| 104 |
+
"position": [0, 300],
|
| 105 |
+
"parameters": { ... real params with expressions ... },
|
| 106 |
+
"credentials": {},
|
| 107 |
+
"onError": "continueErrorOutput",
|
| 108 |
+
"retryOnFail": false,
|
| 109 |
+
"maxTries": 3,
|
| 110 |
+
"waitBetweenTries": 1000,
|
| 111 |
+
"notes": "What this node does and why it is here"
|
| 112 |
+
}
|
| 113 |
+
],
|
| 114 |
+
"connections": {
|
| 115 |
+
"Display Name": {
|
| 116 |
+
"main": [[{ "node": "Next Node Display Name", "type": "main", "index": 0 }]]
|
| 117 |
+
}
|
| 118 |
+
},
|
| 119 |
+
"settings": {
|
| 120 |
+
"executionOrder": "v1",
|
| 121 |
+
"saveManualExecutions": true,
|
| 122 |
+
"timezone": "UTC"
|
| 123 |
+
},
|
| 124 |
+
"tags": [],
|
| 125 |
+
"meta": {}
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
RETURN ONLY valid JSON. No markdown. No text outside the JSON object.`;
|
apps/simulator/src/prompts/graphEngine.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Graph Engine System Prompt β UPGRADED
|
| 3 |
+
* Role: Build Internal Representation (IR) graph from architecture plan
|
| 4 |
+
* Strict: ONLY registry node types, real expressions, complete data flow
|
| 5 |
+
*/
|
| 6 |
+
export const GRAPH_ENGINE_PROMPT = `You are a workflow graph compiler. Your job is to build a WorkflowGraph intermediate representation (IR) from an architecture plan.
|
| 7 |
+
|
| 8 |
+
The graph is the single source of truth before compilation to n8n JSON.
|
| 9 |
+
|
| 10 |
+
STRICT NODE TYPE RULES:
|
| 11 |
+
- You MUST use ONLY n8nNodeType values from the AVAILABLE NODE TYPES list provided
|
| 12 |
+
- NEVER invent node types. NEVER use "custom", "?", or placeholder types
|
| 13 |
+
- If you need an HTTP call, use: n8n-nodes-base.httpRequest
|
| 14 |
+
- If you need AI, use: @n8n/n8n-nodes-langchain.agent (with sub-nodes for model/memory)
|
| 15 |
+
- If you need Telegram, use: n8n-nodes-base.telegram (send) or n8n-nodes-base.telegramTrigger (receive)
|
| 16 |
+
|
| 17 |
+
GRAPH STRUCTURE RULES:
|
| 18 |
+
1. Every node MUST have a valid n8nNodeType from the registry
|
| 19 |
+
2. Every non-trigger node MUST have at least one incoming edge
|
| 20 |
+
3. Trigger node MUST be at layer "trigger" and position {x: 0, y: 300}
|
| 21 |
+
4. Positions must form a clean left-to-right layout (x increases by 220 per level)
|
| 22 |
+
5. DataContracts MUST define input/output JSON fields with real field names
|
| 23 |
+
6. RetryPolicy MUST be set for all nodes where isCritical: true
|
| 24 |
+
7. Fallback paths MUST be defined for critical nodes
|
| 25 |
+
|
| 26 |
+
DATA CONTRACT RULES:
|
| 27 |
+
- inputs: list of JSON fields this node expects from previous node
|
| 28 |
+
- outputs: list of JSON fields this node produces for next node
|
| 29 |
+
- Use real field names matching actual n8n node output (e.g., message, body, text, chatId)
|
| 30 |
+
|
| 31 |
+
EXPRESSION REQUIREMENTS:
|
| 32 |
+
- keyParameters MUST use real n8n expressions:
|
| 33 |
+
- {{$json?.field ?? "default"}} β safe field access with default
|
| 34 |
+
- {{$node["NodeName"].json?.field ?? ""}} β cross-node reference
|
| 35 |
+
- {{$json?.items?.length > 0 ? "yes" : "no"}} β conditional
|
| 36 |
+
- NEVER use static values in parameter fields that should be dynamic
|
| 37 |
+
|
| 38 |
+
RETURN this exact JSON structure:
|
| 39 |
+
{
|
| 40 |
+
"workflowId": "wf-uuid-here",
|
| 41 |
+
"name": "Workflow Name",
|
| 42 |
+
"nodes": [
|
| 43 |
+
{
|
| 44 |
+
"id": "node-uuid",
|
| 45 |
+
"label": "Display Name",
|
| 46 |
+
"n8nNodeType": "exact.node.type.from.registry",
|
| 47 |
+
"layer": "trigger|validation|transform|ai|integration|control|database|utility|monitoring",
|
| 48 |
+
"description": "What this node does specifically",
|
| 49 |
+
"isCritical": true/false,
|
| 50 |
+
"position": { "x": 0, "y": 300 },
|
| 51 |
+
"retryPolicy": { "maxRetries": 3, "retryDelayMs": 1000, "retryOn": ["timeout", "5xx"] },
|
| 52 |
+
"fallbackNodeId": "node-id-or-null",
|
| 53 |
+
"dataContract": {
|
| 54 |
+
"inputs": ["field1", "field2"],
|
| 55 |
+
"outputs": ["outputField1", "outputField2"]
|
| 56 |
+
},
|
| 57 |
+
"keyParameters": {
|
| 58 |
+
"paramName": "={{$json?.field ?? ''}}"
|
| 59 |
+
}
|
| 60 |
+
}
|
| 61 |
+
],
|
| 62 |
+
"edges": [
|
| 63 |
+
{
|
| 64 |
+
"id": "edge-uuid",
|
| 65 |
+
"sourceNodeId": "source-node-id",
|
| 66 |
+
"targetNodeId": "target-node-id",
|
| 67 |
+
"outputIndex": 0,
|
| 68 |
+
"inputIndex": 0,
|
| 69 |
+
"label": "on success"
|
| 70 |
+
}
|
| 71 |
+
],
|
| 72 |
+
"metadata": {
|
| 73 |
+
"domain": "workflow domain",
|
| 74 |
+
"version": "2.0.0",
|
| 75 |
+
"estimatedNodes": number,
|
| 76 |
+
"estimatedDuration": "per execution estimate e.g. 2-5s",
|
| 77 |
+
"riskLevel": "low|medium|high|critical"
|
| 78 |
+
}
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
CRITICAL: Return ONLY valid JSON. No markdown. No text outside JSON.`;
|
apps/simulator/src/prompts/intentInterpreter.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Intent Interpreter System Prompt β UPGRADED
|
| 3 |
+
* Role: Understand workflow requests deeply like a senior workflow engineer
|
| 4 |
+
* Now includes triggerType detection for Webhook Auto-Bind system
|
| 5 |
+
*/
|
| 6 |
+
export const INTENT_INTERPRETER_PROMPT = `You are a senior workflow engineer and systems architect specializing in n8n automation platforms.
|
| 7 |
+
|
| 8 |
+
Your job is to deeply understand a user's workflow request and produce a structured analysis.
|
| 9 |
+
|
| 10 |
+
You must think like:
|
| 11 |
+
- A systems architect who identifies integrations, dependencies, and risks
|
| 12 |
+
- A DevOps engineer who identifies reliability and scaling concerns
|
| 13 |
+
- A security engineer who identifies credential and safety requirements
|
| 14 |
+
|
| 15 |
+
ANALYSIS FRAMEWORK:
|
| 16 |
+
1. Identify the workflow domain: sales_automation | ai_workflow | data_pipeline | webhook_system | notification | crm_integration | devops | ecommerce | general
|
| 17 |
+
2. Identify all external integrations (e.g. Telegram, OpenAI, Gmail, Slack, Airtable, Notion, GitHub, Stripe, HubSpot, Postgres)
|
| 18 |
+
3. Assess risk level: low (simple CRUD) | medium (external APIs, AI) | high (financial, critical data) | critical (multi-system with irreversible effects)
|
| 19 |
+
4. Determine sync vs async: sync = real-time response required | async = fire-and-forget background | hybrid = both
|
| 20 |
+
5. Identify if AI/LLM processing is required
|
| 21 |
+
6. Identify scaling requirements based on expected volume
|
| 22 |
+
7. List all identified risks: API timeouts, credential expiry, webhook duplication, AI output unreliability, rate limits, data loss
|
| 23 |
+
8. Assess complexity: simple (1-3 nodes) | moderate (4-8 nodes) | complex (9+ nodes or sub-workflows)
|
| 24 |
+
9. Determine if human approval is required before activation
|
| 25 |
+
10. CRITICAL β Detect the trigger type from the request:
|
| 26 |
+
- "telegram" β n8n-nodes-base.telegramTrigger
|
| 27 |
+
- "slack" β n8n-nodes-base.slackTrigger
|
| 28 |
+
- "email" / "inbox" / "imap" β n8n-nodes-base.emailReadImap
|
| 29 |
+
- "github" / "push" / "pull request" β n8n-nodes-base.githubTrigger
|
| 30 |
+
- "notion" β n8n-nodes-base.notionTrigger
|
| 31 |
+
- "schedule" / "cron" / "daily" / "hourly" / "every X" β n8n-nodes-base.scheduleTrigger
|
| 32 |
+
- "webhook" / "api call" / "http request" / "form submission" β n8n-nodes-base.webhook
|
| 33 |
+
- "manual" / "test" β n8n-nodes-base.manualTrigger
|
| 34 |
+
|
| 35 |
+
RETURN this exact JSON structure:
|
| 36 |
+
{
|
| 37 |
+
"workflowType": "string - descriptive type name e.g. 'Telegram AI Customer Support Bot'",
|
| 38 |
+
"requiresAI": boolean,
|
| 39 |
+
"integrations": ["array", "of", "service", "names"],
|
| 40 |
+
"riskLevel": "low|medium|high|critical",
|
| 41 |
+
"requiresHumanApproval": boolean,
|
| 42 |
+
"syncVsAsync": "sync|async|hybrid",
|
| 43 |
+
"scalingRequirements": "low|medium|high",
|
| 44 |
+
"identifiedRisks": ["risk description 1", "risk description 2"],
|
| 45 |
+
"estimatedComplexity": "simple|moderate|complex",
|
| 46 |
+
"domain": "one of the domain types above",
|
| 47 |
+
"triggerType": "telegram|slack|email|github|notion|schedule|webhook|manual",
|
| 48 |
+
"triggerHint": "brief explanation of why this trigger was detected"
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
CRITICAL: Return ONLY valid JSON. No markdown. No explanation text.`;
|
apps/simulator/src/prompts/simulator.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Simulator System Prompt β UPGRADED
|
| 3 |
+
* Role: Simulate n8n workflow execution with mock data
|
| 4 |
+
* Strict: trace every node, detect real failure points, chaos testing
|
| 5 |
+
*/
|
| 6 |
+
export const SIMULATOR_PROMPT = `You are an n8n workflow execution simulator. Your job is to dry-run a workflow and predict exactly how it will behave in production.
|
| 7 |
+
|
| 8 |
+
You are NOT executing the workflow β you are SIMULATING it step by step with provided mock data.
|
| 9 |
+
|
| 10 |
+
SIMULATION FRAMEWORK:
|
| 11 |
+
|
| 12 |
+
For each node in the workflow (in execution order):
|
| 13 |
+
1. Show the INPUT data (what enters the node)
|
| 14 |
+
2. Apply the node's logic/transformation
|
| 15 |
+
3. Show the OUTPUT data (what exits the node)
|
| 16 |
+
4. Flag any potential issues (null references, missing fields, type errors)
|
| 17 |
+
5. Rate the step: "success" | "warning" | "failure"
|
| 18 |
+
|
| 19 |
+
CHAOS TESTING REQUIREMENTS:
|
| 20 |
+
Simulate these failure scenarios:
|
| 21 |
+
- "timeout" β external API takes >10s to respond
|
| 22 |
+
- "null_data" β upstream node returns null/empty
|
| 23 |
+
- "malformed_input" β input has wrong types/missing required fields
|
| 24 |
+
- "expired_credentials" β credential token is invalid/expired
|
| 25 |
+
- "rate_limit" β external API returns 429 Too Many Requests
|
| 26 |
+
- "network_error" β connection refused or DNS failure
|
| 27 |
+
|
| 28 |
+
For each chaos test:
|
| 29 |
+
- Was the node resilient? (retryOnFail, error handling)
|
| 30 |
+
- What happens downstream if this node fails?
|
| 31 |
+
- Is there a fallback path?
|
| 32 |
+
|
| 33 |
+
SCORING CRITERIA (readinessScore 0-100):
|
| 34 |
+
- 100: All nodes execute correctly, all chaos tests handled, full error coverage
|
| 35 |
+
- 80-99: Minor warnings, good error handling, no critical failures
|
| 36 |
+
- 60-79: Some warnings, basic error handling, recoverable failures
|
| 37 |
+
- 40-59: Multiple warnings, poor error handling, some unrecoverable failures
|
| 38 |
+
- 0-39: Critical failures, empty nodes, broken expressions, no error handling
|
| 39 |
+
|
| 40 |
+
readinessScore FORMULA:
|
| 41 |
+
- Base: 70 (assume basic workflow works)
|
| 42 |
+
- +15 if all expressions are safe (optional chaining)
|
| 43 |
+
- +10 if all critical nodes have retryOnFail
|
| 44 |
+
- +5 if error handling paths exist
|
| 45 |
+
- -20 per critical failure point
|
| 46 |
+
- -10 per unhandled null reference risk
|
| 47 |
+
- -5 per missing retry on external node
|
| 48 |
+
|
| 49 |
+
DEPLOYMENT BLOCKERS (must be fixed before deployment):
|
| 50 |
+
- Any node with empty parameters
|
| 51 |
+
- Any broken expression that would throw at runtime
|
| 52 |
+
- Unknown node types
|
| 53 |
+
- Missing required credentials
|
| 54 |
+
- active: true (must always be false)
|
| 55 |
+
|
| 56 |
+
RETURN this exact JSON structure:
|
| 57 |
+
{
|
| 58 |
+
"passed": boolean,
|
| 59 |
+
"readinessScore": number (0-100),
|
| 60 |
+
"executionTrace": [
|
| 61 |
+
{
|
| 62 |
+
"stepNumber": 1,
|
| 63 |
+
"nodeId": "uuid",
|
| 64 |
+
"nodeName": "Display Name",
|
| 65 |
+
"nodeType": "n8n-nodes-base.xxx",
|
| 66 |
+
"status": "success|warning|failure",
|
| 67 |
+
"inputData": { ... mock input json ... },
|
| 68 |
+
"outputData": { ... simulated output json ... },
|
| 69 |
+
"executionTimeMs": number,
|
| 70 |
+
"issues": ["issue description if any"],
|
| 71 |
+
"notes": "what happened in this step"
|
| 72 |
+
}
|
| 73 |
+
],
|
| 74 |
+
"predictedFailurePoints": [
|
| 75 |
+
{
|
| 76 |
+
"nodeId": "uuid",
|
| 77 |
+
"nodeName": "Name",
|
| 78 |
+
"failureType": "null_reference|timeout|credential_error|expression_error|missing_field|type_error",
|
| 79 |
+
"probability": "low|medium|high",
|
| 80 |
+
"description": "exactly what will fail and why",
|
| 81 |
+
"mitigation": "how to fix or handle this"
|
| 82 |
+
}
|
| 83 |
+
],
|
| 84 |
+
"chaosTestResults": [
|
| 85 |
+
{
|
| 86 |
+
"scenario": "timeout|null_data|malformed_input|expired_credentials|rate_limit|network_error",
|
| 87 |
+
"passed": boolean,
|
| 88 |
+
"affectedNode": "node name",
|
| 89 |
+
"description": "what happens during this scenario",
|
| 90 |
+
"resilient": boolean,
|
| 91 |
+
"fallbackActivated": boolean
|
| 92 |
+
}
|
| 93 |
+
],
|
| 94 |
+
"riskAnalysis": {
|
| 95 |
+
"overallRisk": "low|medium|high|critical",
|
| 96 |
+
"risks": ["risk description 1", "risk description 2"],
|
| 97 |
+
"mitigations": ["mitigation 1", "mitigation 2"]
|
| 98 |
+
},
|
| 99 |
+
"deploymentBlockers": ["blocker 1 β must fix before deployment", "blocker 2"],
|
| 100 |
+
"recommendations": ["improvement suggestion 1", "improvement suggestion 2"]
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
CRITICAL: Return ONLY valid JSON. No markdown. No text outside the JSON object.`;
|
apps/simulator/src/prompts/workflowPlanner.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Workflow Planner System Prompt β UPGRADED
|
| 3 |
+
* Role: Design production-grade n8n workflow architectures
|
| 4 |
+
* Senior engineer thinking: reliability, observability, error handling, scalability
|
| 5 |
+
*/
|
| 6 |
+
export const WORKFLOW_PLANNER_PROMPT = `You are a senior n8n workflow architect with 10+ years of production automation experience.
|
| 7 |
+
|
| 8 |
+
Your job is to design complete, production-grade workflow architectures that a junior engineer can implement without guessing.
|
| 9 |
+
|
| 10 |
+
ARCHITECTURE LAYERS (think in layers):
|
| 11 |
+
1. TRIGGER β The entry point (Webhook, Telegram, Schedule, Email, GitHub, etc.)
|
| 12 |
+
2. VALIDATION β Input validation (IF node, Code node checks)
|
| 13 |
+
3. TRANSFORM β Data preparation, normalization (SET, Code)
|
| 14 |
+
4. QUEUE / RATE LIMIT β Batch management (SplitInBatches, Wait)
|
| 15 |
+
5. PROCESSING / AI β Core business logic (HTTP, Code, AI Agent)
|
| 16 |
+
6. DECISION β Routing based on results (IF, Switch)
|
| 17 |
+
7. PERSISTENCE β Save results (Sheets, Airtable, Notion, Postgres, Airtable)
|
| 18 |
+
8. NOTIFICATION β Alerts, confirmations (Telegram, Slack, Gmail)
|
| 19 |
+
9. MONITORING β Success/failure tracking (logging nodes, Slack alerts)
|
| 20 |
+
10. ERROR HANDLING β Fallback paths, retry, stop-and-error
|
| 21 |
+
|
| 22 |
+
DESIGN PRINCIPLES:
|
| 23 |
+
- NEVER design workflows with placeholder nodes ("Custom HTTP", "API Node", "Process Data")
|
| 24 |
+
- ALWAYS use real n8n node names from the registry
|
| 25 |
+
- EVERY external API call MUST have retry (retryOnFail: true, maxTries: 3)
|
| 26 |
+
- EVERY workflow MUST have a monitoring/notification layer
|
| 27 |
+
- EVERY branch MUST be handled (no dead paths)
|
| 28 |
+
- EVERY SET node MUST have explicit field mappings
|
| 29 |
+
- EVERY IF node MUST have real boolean conditions
|
| 30 |
+
- EVERY AI Agent MUST have proper system message and dynamic user input
|
| 31 |
+
- USE real n8n expressions: {{$json?.field ?? "default"}} format
|
| 32 |
+
- NEVER use empty field values, never use placeholder text
|
| 33 |
+
|
| 34 |
+
RISK ANALYSIS REQUIREMENTS:
|
| 35 |
+
- List every external dependency
|
| 36 |
+
- List every credential required
|
| 37 |
+
- List every potential failure point
|
| 38 |
+
- Specify retry strategy for each critical node
|
| 39 |
+
- Recommend error workflow integration
|
| 40 |
+
|
| 41 |
+
RETURN this exact JSON structure:
|
| 42 |
+
{
|
| 43 |
+
"name": "Descriptive workflow name",
|
| 44 |
+
"description": "What this workflow does end-to-end",
|
| 45 |
+
"triggerNode": "exact n8n node type string",
|
| 46 |
+
"triggerDescription": "How this workflow is triggered",
|
| 47 |
+
"layers": [
|
| 48 |
+
{
|
| 49 |
+
"layer": "trigger|validation|transform|queue|processing|ai|decision|persistence|notification|monitoring|error_handling",
|
| 50 |
+
"nodes": ["NodeDisplayName1", "NodeDisplayName2"],
|
| 51 |
+
"description": "What this layer does"
|
| 52 |
+
}
|
| 53 |
+
],
|
| 54 |
+
"nodeList": [
|
| 55 |
+
{
|
| 56 |
+
"displayName": "Node Display Name",
|
| 57 |
+
"n8nNodeType": "exact.node.type",
|
| 58 |
+
"layer": "layer name",
|
| 59 |
+
"purpose": "What this specific node does",
|
| 60 |
+
"isCritical": true/false,
|
| 61 |
+
"requiresCredential": "credential type or null",
|
| 62 |
+
"keyParameters": { "param": "value or expression" }
|
| 63 |
+
}
|
| 64 |
+
],
|
| 65 |
+
"dataFlow": [
|
| 66 |
+
"Step 1: Webhook receives POST with {message, userId}",
|
| 67 |
+
"Step 2: SET extracts message β {{$json?.body?.message ?? ''}}"
|
| 68 |
+
],
|
| 69 |
+
"riskAnalysis": {
|
| 70 |
+
"overallRisk": "low|medium|high|critical",
|
| 71 |
+
"externalDependencies": ["Service1", "Service2"],
|
| 72 |
+
"requiredCredentials": ["credType1", "credType2"],
|
| 73 |
+
"failurePoints": ["possible failure 1"],
|
| 74 |
+
"retryStrategy": "description of retry approach"
|
| 75 |
+
},
|
| 76 |
+
"estimatedNodes": number,
|
| 77 |
+
"complexity": "simple|moderate|complex"
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
CRITICAL: Return ONLY valid JSON. No markdown. No text outside JSON.`;
|
apps/simulator/src/routes/generate.ts
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Simulator Generate Route β UPGRADED
|
| 3 |
+
* Full pipeline: Intent β Swarm Planning β Graph β Compile β Validate β Self-Heal β Simulate β Credential Analysis
|
| 4 |
+
* Provider-agnostic LLM Gateway, Swarm multi-agent, Self-Healing, Memory/Learning, Webhook Auto-Bind
|
| 5 |
+
*/
|
| 6 |
+
import { Router } from 'express';
|
| 7 |
+
import { z } from 'zod';
|
| 8 |
+
import { logger } from '../index';
|
| 9 |
+
import { IntentInterpreterAgent } from '../agents/intentInterpreter';
|
| 10 |
+
import { WorkflowGraphEngine } from '../agents/graphEngine';
|
| 11 |
+
import { WorkflowCompiler } from '../agents/compiler';
|
| 12 |
+
import { ValidationEngine } from '../agents/validator';
|
| 13 |
+
import { DryRunSimulator } from '../agents/simulator';
|
| 14 |
+
import { CredentialIntelligence } from '../agents/credentialIntelligence';
|
| 15 |
+
import { QualityScorer } from '../services/qualityScorer';
|
| 16 |
+
import { SwarmOrchestrator } from '../services/swarmOrchestrator';
|
| 17 |
+
import { SelfHealingSystem } from '../services/selfHealing';
|
| 18 |
+
import { webhookAutoBind } from '../services/webhookAutoBind';
|
| 19 |
+
import { workflowMemory } from '../services/memorySystem';
|
| 20 |
+
import { createRequestLLM } from '../services/llmClient';
|
| 21 |
+
import type { GenerateWorkflowResponse } from '../types/workflow';
|
| 22 |
+
|
| 23 |
+
export const generateRoute: Router = Router();
|
| 24 |
+
|
| 25 |
+
const GenerateSchema = z.object({
|
| 26 |
+
jobId: z.string(),
|
| 27 |
+
request: z.string().min(10),
|
| 28 |
+
options: z.object({
|
| 29 |
+
skipSimulation: z.boolean().default(false),
|
| 30 |
+
requireApproval: z.boolean().default(true),
|
| 31 |
+
swarmSize: z.number().min(1).max(5).default(3),
|
| 32 |
+
skipSwarm: z.boolean().default(false),
|
| 33 |
+
skipSelfHealing: z.boolean().default(false),
|
| 34 |
+
}).default({}),
|
| 35 |
+
// LLM config β provider agnostic (apiKey for backward compat with CF Worker)
|
| 36 |
+
llmApiKey: z.string().optional(),
|
| 37 |
+
openaiApiKey: z.string().optional(), // legacy support
|
| 38 |
+
llmModel: z.string().optional(),
|
| 39 |
+
llmGatewayUrl: z.string().optional(),
|
| 40 |
+
llmProvider: z.string().optional(),
|
| 41 |
+
n8nBaseUrl: z.string(),
|
| 42 |
+
n8nApiKey: z.string(),
|
| 43 |
+
});
|
| 44 |
+
|
| 45 |
+
generateRoute.post('/', async (req, res) => {
|
| 46 |
+
const parsed = GenerateSchema.safeParse(req.body);
|
| 47 |
+
if (!parsed.success) {
|
| 48 |
+
res.status(400).json({ success: false, error: 'Invalid request', details: parsed.error.flatten() });
|
| 49 |
+
return;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
const { jobId, request, options, n8nBaseUrl, n8nApiKey } = parsed.data;
|
| 53 |
+
|
| 54 |
+
// Resolve API key (support both new LLM_API_KEY and legacy OPENAI_API_KEY)
|
| 55 |
+
const apiKey = parsed.data.llmApiKey ?? parsed.data.openaiApiKey ?? process.env['LLM_API_KEY'] ?? process.env['OPENAI_API_KEY'] ?? '';
|
| 56 |
+
if (!apiKey) {
|
| 57 |
+
res.status(400).json({ success: false, error: 'LLM API key not provided. Set llmApiKey in request or LLM_API_KEY env var.' });
|
| 58 |
+
return;
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
// Create provider-agnostic LLM gateway
|
| 62 |
+
const llm = createRequestLLM(apiKey, parsed.data.llmModel);
|
| 63 |
+
|
| 64 |
+
logger.info({ jobId }, '[Generate] Starting upgraded pipeline (Swarm + Self-Healing + Memory)');
|
| 65 |
+
|
| 66 |
+
try {
|
| 67 |
+
// βββ Stage 1: Intent Interpretation ββββββββββββββββββββββββββββββββββββ
|
| 68 |
+
logger.info({ jobId }, '[Generate] Stage 1: Intent Interpretation');
|
| 69 |
+
const intentAgent = new IntentInterpreterAgent(llm);
|
| 70 |
+
const intent = await intentAgent.interpret(request);
|
| 71 |
+
|
| 72 |
+
// βββ Stage 2: Webhook Auto-Bind trigger detection βββββββββββββββββββββββ
|
| 73 |
+
logger.info({ jobId }, '[Generate] Stage 2: Webhook Auto-Bind trigger detection');
|
| 74 |
+
const triggerDetection = webhookAutoBind.detectTrigger(request, intent);
|
| 75 |
+
intent.triggerType = triggerDetection.triggerType;
|
| 76 |
+
intent.triggerHint = `Auto-detected: ${triggerDetection.nodeType} (confidence: ${triggerDetection.confidence})`;
|
| 77 |
+
|
| 78 |
+
// βββ Stage 3: Memory context retrieval βββββββββββββββββββββββββββββββββ
|
| 79 |
+
logger.info({ jobId }, '[Generate] Stage 3: Memory context retrieval');
|
| 80 |
+
const memoryContext = workflowMemory.buildMemoryContext(intent.domain, intent.integrations);
|
| 81 |
+
|
| 82 |
+
// Augment request with memory context for better planning
|
| 83 |
+
const augmentedRequest = memoryContext
|
| 84 |
+
? `${request}\n\n${memoryContext}`
|
| 85 |
+
: request;
|
| 86 |
+
|
| 87 |
+
// βββ Stage 4: Swarm Planning (parallel designs) or single plan βββββββββ
|
| 88 |
+
let finalPlan: import('../types/workflow').WorkflowArchitecturePlan;
|
| 89 |
+
let finalGraph: import('../types/workflow').WorkflowGraph;
|
| 90 |
+
let finalWorkflow: import('../types/workflow').N8nWorkflow;
|
| 91 |
+
let swarmResult: import('../services/swarmOrchestrator').SwarmResult | undefined;
|
| 92 |
+
|
| 93 |
+
if (!options.skipSwarm && options.swarmSize > 1) {
|
| 94 |
+
logger.info({ jobId, swarmSize: options.swarmSize }, '[Generate] Stage 4: Swarm multi-agent planning');
|
| 95 |
+
const swarm = new SwarmOrchestrator(llm);
|
| 96 |
+
swarmResult = await swarm.runSwarm(augmentedRequest, intent, n8nBaseUrl, n8nApiKey, options.swarmSize);
|
| 97 |
+
|
| 98 |
+
finalPlan = swarmResult.winner.plan;
|
| 99 |
+
finalGraph = swarmResult.winner.graph;
|
| 100 |
+
finalWorkflow = swarmResult.winner.workflow;
|
| 101 |
+
} else {
|
| 102 |
+
// Single-agent path (faster, less thorough)
|
| 103 |
+
logger.info({ jobId }, '[Generate] Stage 4: Single-agent planning (swarm disabled)');
|
| 104 |
+
const { WorkflowPlannerAgent } = await import('../agents/workflowPlanner');
|
| 105 |
+
const planner = new WorkflowPlannerAgent(llm);
|
| 106 |
+
finalPlan = await planner.plan(augmentedRequest, intent);
|
| 107 |
+
|
| 108 |
+
const graphEngine = new WorkflowGraphEngine(llm);
|
| 109 |
+
finalGraph = await graphEngine.buildGraph(augmentedRequest, intent, finalPlan);
|
| 110 |
+
|
| 111 |
+
const compiler = new WorkflowCompiler(llm);
|
| 112 |
+
finalWorkflow = await compiler.compile(finalGraph, intent);
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
// βββ Stage 5: Inject auto-configured trigger ββββββββββββββββββββββββββββ
|
| 116 |
+
logger.info({ jobId }, '[Generate] Stage 5: Webhook Auto-Bind trigger injection');
|
| 117 |
+
const triggerConfig = webhookAutoBind.buildTriggerConfig(
|
| 118 |
+
triggerDetection.nodeType,
|
| 119 |
+
request,
|
| 120 |
+
intent,
|
| 121 |
+
finalWorkflow.name ?? 'workflow',
|
| 122 |
+
);
|
| 123 |
+
finalGraph = webhookAutoBind.injectTriggerIntoGraph(finalGraph, triggerConfig, triggerDetection.nodeType);
|
| 124 |
+
|
| 125 |
+
// βββ Stage 6: Validation ββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 126 |
+
logger.info({ jobId }, '[Generate] Stage 6: Validation');
|
| 127 |
+
const validator = new ValidationEngine(llm, n8nBaseUrl, n8nApiKey);
|
| 128 |
+
let validationReport = await validator.validate(jobId, finalWorkflow, finalGraph);
|
| 129 |
+
|
| 130 |
+
// βββ Stage 7: Self-Healing (if validation failed) βββββββββββββββββββββββ
|
| 131 |
+
let healingResult: import('../services/selfHealing').HealingResult | undefined;
|
| 132 |
+
if (!validationReport.valid && !options.skipSelfHealing) {
|
| 133 |
+
logger.info({ jobId, errors: validationReport.issues.length }, '[Generate] Stage 7: Self-Healing');
|
| 134 |
+
const healer = new SelfHealingSystem(llm);
|
| 135 |
+
healingResult = await healer.heal(finalWorkflow, finalGraph, validationReport);
|
| 136 |
+
|
| 137 |
+
if (healingResult.healed && healingResult.healedWorkflow) {
|
| 138 |
+
finalWorkflow = healingResult.healedWorkflow;
|
| 139 |
+
// Re-validate after healing
|
| 140 |
+
validationReport = await validator.validate(jobId, finalWorkflow, finalGraph);
|
| 141 |
+
logger.info({ jobId, valid: validationReport.valid }, '[Generate] Re-validation after self-healing');
|
| 142 |
+
}
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
// βββ Stage 8: Dry-Run Simulation (unless skipped) ββββββββββββββββββββββ
|
| 146 |
+
let simulationReport: import('../types/workflow').SimulationReport | undefined;
|
| 147 |
+
if (!options.skipSimulation) {
|
| 148 |
+
logger.info({ jobId }, '[Generate] Stage 8: Dry-Run Simulation');
|
| 149 |
+
const simulator = new DryRunSimulator(llm);
|
| 150 |
+
simulationReport = await simulator.simulate(jobId, finalWorkflow, finalGraph, finalPlan);
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
// βββ Stage 9: Credential Analysis ββββββββββββββββββββββββββββββββββββββ
|
| 154 |
+
logger.info({ jobId }, '[Generate] Stage 9: Credential Analysis');
|
| 155 |
+
const credentialIntel = new CredentialIntelligence(n8nBaseUrl, n8nApiKey);
|
| 156 |
+
const credentialAnalysis = await credentialIntel.analyse(jobId, finalWorkflow);
|
| 157 |
+
|
| 158 |
+
// βββ Stage 10: Quality Scoring ββββββββββββββββββββββββββββββββββββββββββ
|
| 159 |
+
const qualityScorer = new QualityScorer();
|
| 160 |
+
const deploymentReadiness = qualityScorer.scoreDeploymentReadiness(
|
| 161 |
+
validationReport,
|
| 162 |
+
simulationReport,
|
| 163 |
+
credentialAnalysis,
|
| 164 |
+
);
|
| 165 |
+
const qualityScore = qualityScorer.calculateQualityScore(validationReport, simulationReport);
|
| 166 |
+
|
| 167 |
+
// βββ Stage 11: Memory storage βββββββββββββββββββββββββββββββββββββββββββ
|
| 168 |
+
const nodeTypes = finalWorkflow.nodes.map((n) => n.type);
|
| 169 |
+
if (validationReport.valid && (simulationReport?.passed ?? true)) {
|
| 170 |
+
workflowMemory.storeSuccessPattern(
|
| 171 |
+
jobId,
|
| 172 |
+
intent.domain,
|
| 173 |
+
intent.workflowType,
|
| 174 |
+
intent.integrations,
|
| 175 |
+
nodeTypes,
|
| 176 |
+
finalPlan.description ?? '',
|
| 177 |
+
qualityScore.overallScore,
|
| 178 |
+
validationReport.overallScore,
|
| 179 |
+
simulationReport?.readinessScore ?? 0,
|
| 180 |
+
);
|
| 181 |
+
} else {
|
| 182 |
+
workflowMemory.storeFailurePattern(
|
| 183 |
+
jobId,
|
| 184 |
+
intent.domain,
|
| 185 |
+
intent.workflowType,
|
| 186 |
+
intent.integrations,
|
| 187 |
+
nodeTypes,
|
| 188 |
+
finalPlan.description ?? '',
|
| 189 |
+
validationReport.overallScore,
|
| 190 |
+
validationReport.issues.map((i) => i.message),
|
| 191 |
+
);
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
const finalState = (() => {
|
| 195 |
+
if (simulationReport?.passed && validationReport.valid) return 'simulated';
|
| 196 |
+
if (validationReport.valid) return 'validated';
|
| 197 |
+
return 'generated';
|
| 198 |
+
})();
|
| 199 |
+
|
| 200 |
+
logger.info({ jobId, state: finalState, score: qualityScore.overallScore }, '[Generate] Pipeline complete');
|
| 201 |
+
|
| 202 |
+
const response: GenerateWorkflowResponse = {
|
| 203 |
+
success: true,
|
| 204 |
+
jobId,
|
| 205 |
+
state: finalState as GenerateWorkflowResponse['state'],
|
| 206 |
+
message: `Workflow generation pipeline complete. State: ${finalState}. Quality score: ${qualityScore.overallScore}/100`,
|
| 207 |
+
architecturePlan: finalPlan,
|
| 208 |
+
graph: finalGraph,
|
| 209 |
+
compiledWorkflow: finalWorkflow,
|
| 210 |
+
validationReport,
|
| 211 |
+
simulationReport,
|
| 212 |
+
credentialAnalysis,
|
| 213 |
+
deploymentReadiness,
|
| 214 |
+
qualityScore,
|
| 215 |
+
healingResult,
|
| 216 |
+
swarmResult: swarmResult ? {
|
| 217 |
+
swarmSize: swarmResult.swarmSize,
|
| 218 |
+
generatedAt: swarmResult.generatedAt,
|
| 219 |
+
winnerStrategy: swarmResult.winner.planningStrategy,
|
| 220 |
+
winnerScore: swarmResult.winner.qualityScore,
|
| 221 |
+
candidateScores: swarmResult.allCandidates.map((c) => ({
|
| 222 |
+
id: c.id,
|
| 223 |
+
strategy: c.planningStrategy,
|
| 224 |
+
score: c.qualityScore,
|
| 225 |
+
})),
|
| 226 |
+
selectionReason: swarmResult.winner.selectionReason,
|
| 227 |
+
} : undefined,
|
| 228 |
+
triggerDetection: {
|
| 229 |
+
nodeType: triggerDetection.nodeType,
|
| 230 |
+
triggerType: triggerDetection.triggerType,
|
| 231 |
+
confidence: triggerDetection.confidence,
|
| 232 |
+
webhookPath: triggerConfig.webhookPath,
|
| 233 |
+
notes: triggerConfig.notes,
|
| 234 |
+
},
|
| 235 |
+
memoryStats: workflowMemory.getStats(),
|
| 236 |
+
};
|
| 237 |
+
|
| 238 |
+
res.json(response);
|
| 239 |
+
} catch (err) {
|
| 240 |
+
const message = err instanceof Error ? err.message : 'Pipeline error';
|
| 241 |
+
logger.error({ jobId, err }, '[Generate] Pipeline failed');
|
| 242 |
+
res.status(500).json({ success: false, error: message, jobId });
|
| 243 |
+
}
|
| 244 |
+
});
|
apps/simulator/src/routes/simulate.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Simulator Simulate Route β UPGRADED
|
| 3 |
+
* Provider-agnostic dry-run simulation using LLM Gateway
|
| 4 |
+
*/
|
| 5 |
+
import { Router } from 'express';
|
| 6 |
+
import { z } from 'zod';
|
| 7 |
+
import { logger } from '../index';
|
| 8 |
+
import { DryRunSimulator } from '../agents/simulator';
|
| 9 |
+
import { createRequestLLM } from '../services/llmClient';
|
| 10 |
+
import type { N8nWorkflow, WorkflowGraph, WorkflowArchitecturePlan } from '../types/workflow';
|
| 11 |
+
|
| 12 |
+
export const simulateRoute: Router = Router();
|
| 13 |
+
|
| 14 |
+
const SimulateSchema = z.object({
|
| 15 |
+
jobId: z.string(),
|
| 16 |
+
workflow: z.record(z.unknown()),
|
| 17 |
+
graph: z.record(z.unknown()),
|
| 18 |
+
architecturePlan: z.record(z.unknown()).optional(),
|
| 19 |
+
mockData: z.record(z.unknown()).optional(),
|
| 20 |
+
llmApiKey: z.string().optional(),
|
| 21 |
+
openaiApiKey: z.string().optional(),
|
| 22 |
+
llmModel: z.string().optional(),
|
| 23 |
+
});
|
| 24 |
+
|
| 25 |
+
simulateRoute.post('/', async (req, res) => {
|
| 26 |
+
const parsed = SimulateSchema.safeParse(req.body);
|
| 27 |
+
if (!parsed.success) {
|
| 28 |
+
res.status(400).json({ success: false, error: 'Invalid request', details: parsed.error.flatten() });
|
| 29 |
+
return;
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
const { jobId, workflow, graph, architecturePlan } = parsed.data;
|
| 33 |
+
const apiKey = parsed.data.llmApiKey ?? parsed.data.openaiApiKey ?? process.env['LLM_API_KEY'] ?? process.env['OPENAI_API_KEY'] ?? '';
|
| 34 |
+
|
| 35 |
+
logger.info({ jobId }, '[Simulate] Starting dry-run simulation');
|
| 36 |
+
|
| 37 |
+
try {
|
| 38 |
+
const llm = createRequestLLM(apiKey);
|
| 39 |
+
const simulator = new DryRunSimulator(llm);
|
| 40 |
+
|
| 41 |
+
const simulationReport = await simulator.simulate(
|
| 42 |
+
jobId,
|
| 43 |
+
workflow as any as N8nWorkflow,
|
| 44 |
+
graph as any as WorkflowGraph,
|
| 45 |
+
(architecturePlan ?? {}) as any as WorkflowArchitecturePlan,
|
| 46 |
+
);
|
| 47 |
+
|
| 48 |
+
logger.info({
|
| 49 |
+
jobId,
|
| 50 |
+
passed: simulationReport.passed,
|
| 51 |
+
score: simulationReport.readinessScore,
|
| 52 |
+
}, '[Simulate] Complete');
|
| 53 |
+
|
| 54 |
+
res.json({ success: true, simulationReport });
|
| 55 |
+
} catch (err) {
|
| 56 |
+
const message = err instanceof Error ? err.message : 'Simulation error';
|
| 57 |
+
logger.error({ jobId, err }, '[Simulate] Failed');
|
| 58 |
+
res.status(500).json({ success: false, error: message, jobId });
|
| 59 |
+
}
|
| 60 |
+
});
|
apps/simulator/src/routes/validate.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Simulator Validate Route β UPGRADED
|
| 3 |
+
* Provider-agnostic validation using LLM Gateway
|
| 4 |
+
* Full: Schema + NodeRegistry + Graph + Expression + DataFlow + Reliability + Credential
|
| 5 |
+
*/
|
| 6 |
+
import { Router } from 'express';
|
| 7 |
+
import { z } from 'zod';
|
| 8 |
+
import { logger } from '../index';
|
| 9 |
+
import { ValidationEngine } from '../agents/validator';
|
| 10 |
+
import { createRequestLLM } from '../services/llmClient';
|
| 11 |
+
import type { N8nWorkflow, WorkflowGraph } from '../types/workflow';
|
| 12 |
+
|
| 13 |
+
export const validateRoute: Router = Router();
|
| 14 |
+
|
| 15 |
+
const ValidateSchema = z.object({
|
| 16 |
+
jobId: z.string(),
|
| 17 |
+
workflow: z.record(z.unknown()),
|
| 18 |
+
graph: z.record(z.unknown()),
|
| 19 |
+
llmApiKey: z.string().optional(),
|
| 20 |
+
openaiApiKey: z.string().optional(),
|
| 21 |
+
llmModel: z.string().optional(),
|
| 22 |
+
n8nBaseUrl: z.string(),
|
| 23 |
+
n8nApiKey: z.string(),
|
| 24 |
+
final: z.boolean().optional().default(false),
|
| 25 |
+
});
|
| 26 |
+
|
| 27 |
+
validateRoute.post('/', async (req, res) => {
|
| 28 |
+
const parsed = ValidateSchema.safeParse(req.body);
|
| 29 |
+
if (!parsed.success) {
|
| 30 |
+
res.status(400).json({ success: false, error: 'Invalid request', details: parsed.error.flatten() });
|
| 31 |
+
return;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
const { jobId, workflow, graph, n8nBaseUrl, n8nApiKey, final: isFinal } = parsed.data;
|
| 35 |
+
const apiKey = parsed.data.llmApiKey ?? parsed.data.openaiApiKey ?? process.env['LLM_API_KEY'] ?? process.env['OPENAI_API_KEY'] ?? '';
|
| 36 |
+
|
| 37 |
+
logger.info({ jobId, final: isFinal }, '[Validate] Starting validation');
|
| 38 |
+
|
| 39 |
+
try {
|
| 40 |
+
const llm = createRequestLLM(apiKey);
|
| 41 |
+
const validator = new ValidationEngine(llm, n8nBaseUrl, n8nApiKey);
|
| 42 |
+
const validationReport = await validator.validate(
|
| 43 |
+
jobId,
|
| 44 |
+
workflow as any as N8nWorkflow,
|
| 45 |
+
graph as any as WorkflowGraph,
|
| 46 |
+
);
|
| 47 |
+
|
| 48 |
+
logger.info({
|
| 49 |
+
jobId,
|
| 50 |
+
valid: validationReport.valid,
|
| 51 |
+
score: validationReport.overallScore,
|
| 52 |
+
errors: validationReport.issues.length,
|
| 53 |
+
}, '[Validate] Complete');
|
| 54 |
+
|
| 55 |
+
res.json({ success: true, validationReport });
|
| 56 |
+
} catch (err) {
|
| 57 |
+
const message = err instanceof Error ? err.message : 'Validation error';
|
| 58 |
+
logger.error({ jobId, err }, '[Validate] Failed');
|
| 59 |
+
res.status(500).json({ success: false, error: message, jobId });
|
| 60 |
+
}
|
| 61 |
+
});
|
apps/simulator/src/services/llmClient.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* LLM Client Factory for Simulator Service
|
| 3 |
+
* Provides a configured LLMGateway instance from process.env
|
| 4 |
+
* Completely removes direct OpenAI SDK dependency
|
| 5 |
+
*/
|
| 6 |
+
import { LLMGateway, type LLMGatewayConfig } from '@wfo/integrations/llm-providers/index';
|
| 7 |
+
|
| 8 |
+
let _instance: LLMGateway | null = null;
|
| 9 |
+
|
| 10 |
+
export function getSimulatorLLM(): LLMGateway {
|
| 11 |
+
if (_instance) return _instance;
|
| 12 |
+
|
| 13 |
+
const env = process.env;
|
| 14 |
+
const provider = (env['LLM_PROVIDER'] ?? 'openai') as LLMGatewayConfig['primary']['provider'];
|
| 15 |
+
const baseUrl = env['LLM_GATEWAY_URL'] ?? 'https://api.openai.com/v1';
|
| 16 |
+
const apiKey = env['LLM_API_KEY'] ?? env['OPENAI_API_KEY'] ?? '';
|
| 17 |
+
const defaultModel = env['LLM_MODEL'] ?? 'gpt-4o';
|
| 18 |
+
|
| 19 |
+
if (!apiKey) {
|
| 20 |
+
throw new Error(
|
| 21 |
+
'LLM API key not configured. Set LLM_API_KEY (or OPENAI_API_KEY for legacy) in environment.',
|
| 22 |
+
);
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
const config: LLMGatewayConfig = {
|
| 26 |
+
primary: { provider, baseUrl, apiKey, defaultModel },
|
| 27 |
+
defaultRetries: 3,
|
| 28 |
+
defaultRetryDelayMs: 1000,
|
| 29 |
+
defaultTimeoutMs: 90000,
|
| 30 |
+
};
|
| 31 |
+
|
| 32 |
+
// Optional fallback provider
|
| 33 |
+
const fallbackUrl = env['LLM_FALLBACK_URL'];
|
| 34 |
+
const fallbackKey = env['LLM_FALLBACK_KEY'];
|
| 35 |
+
const fallbackModel = env['LLM_FALLBACK_MODEL'] ?? defaultModel;
|
| 36 |
+
if (fallbackUrl && fallbackKey) {
|
| 37 |
+
config.fallback = {
|
| 38 |
+
provider: 'openai-compatible',
|
| 39 |
+
baseUrl: fallbackUrl,
|
| 40 |
+
apiKey: fallbackKey,
|
| 41 |
+
defaultModel: fallbackModel,
|
| 42 |
+
};
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
_instance = new LLMGateway(config);
|
| 46 |
+
return _instance;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
/**
|
| 50 |
+
* Create a per-request LLM client from a provided API key
|
| 51 |
+
* Used when the API key is passed per-request (from CF Worker)
|
| 52 |
+
*/
|
| 53 |
+
export function createRequestLLM(apiKey: string, model?: string): LLMGateway {
|
| 54 |
+
const env = process.env;
|
| 55 |
+
const provider = (env['LLM_PROVIDER'] ?? 'openai') as LLMGatewayConfig['primary']['provider'];
|
| 56 |
+
const baseUrl = env['LLM_GATEWAY_URL'] ?? 'https://api.openai.com/v1';
|
| 57 |
+
|
| 58 |
+
return new LLMGateway({
|
| 59 |
+
primary: {
|
| 60 |
+
provider,
|
| 61 |
+
baseUrl,
|
| 62 |
+
apiKey,
|
| 63 |
+
defaultModel: model ?? env['LLM_MODEL'] ?? 'gpt-4o',
|
| 64 |
+
},
|
| 65 |
+
defaultRetries: 3,
|
| 66 |
+
defaultRetryDelayMs: 1000,
|
| 67 |
+
defaultTimeoutMs: 90000,
|
| 68 |
+
});
|
| 69 |
+
}
|
apps/simulator/src/services/memorySystem.ts
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Memory / Learning System β NEW
|
| 3 |
+
* Stores successful workflow patterns, failed patterns, high-performing architectures
|
| 4 |
+
* Influences node selection, planning, retry strategy, error prevention
|
| 5 |
+
* In-process store (can be extended with Redis/KV for persistence)
|
| 6 |
+
*/
|
| 7 |
+
|
| 8 |
+
export interface WorkflowPattern {
|
| 9 |
+
id: string;
|
| 10 |
+
type: 'success' | 'failure';
|
| 11 |
+
domain: string;
|
| 12 |
+
workflowType: string;
|
| 13 |
+
integrations: string[];
|
| 14 |
+
nodeTypes: string[];
|
| 15 |
+
architectureDescription: string;
|
| 16 |
+
qualityScore: number;
|
| 17 |
+
validationScore: number;
|
| 18 |
+
simulationScore: number;
|
| 19 |
+
keyLessons: string[];
|
| 20 |
+
antiPatterns?: string[];
|
| 21 |
+
createdAt: string;
|
| 22 |
+
useCount: number;
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
export interface ArchitectureTemplate {
|
| 26 |
+
id: string;
|
| 27 |
+
name: string;
|
| 28 |
+
domain: string;
|
| 29 |
+
triggers: string[];
|
| 30 |
+
nodeSequence: string[];
|
| 31 |
+
avgQualityScore: number;
|
| 32 |
+
successRate: number;
|
| 33 |
+
description: string;
|
| 34 |
+
usageCount: number;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
// βββ In-Process Memory Store ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 38 |
+
class MemoryStore {
|
| 39 |
+
private patterns: Map<string, WorkflowPattern> = new Map();
|
| 40 |
+
private templates: Map<string, ArchitectureTemplate> = new Map();
|
| 41 |
+
private fixStrategies: Map<string, string> = new Map();
|
| 42 |
+
|
| 43 |
+
// βββ Initialize with built-in high-performing templates ββββββββββββββββββ
|
| 44 |
+
constructor() {
|
| 45 |
+
this.seedBuiltinTemplates();
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
// βββ Pattern Management βββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 49 |
+
|
| 50 |
+
storeSuccessPattern(
|
| 51 |
+
jobId: string,
|
| 52 |
+
domain: string,
|
| 53 |
+
workflowType: string,
|
| 54 |
+
integrations: string[],
|
| 55 |
+
nodeTypes: string[],
|
| 56 |
+
architectureDescription: string,
|
| 57 |
+
qualityScore: number,
|
| 58 |
+
validationScore: number,
|
| 59 |
+
simulationScore: number,
|
| 60 |
+
): void {
|
| 61 |
+
const id = `success-${jobId}`;
|
| 62 |
+
this.patterns.set(id, {
|
| 63 |
+
id,
|
| 64 |
+
type: 'success',
|
| 65 |
+
domain,
|
| 66 |
+
workflowType,
|
| 67 |
+
integrations,
|
| 68 |
+
nodeTypes,
|
| 69 |
+
architectureDescription,
|
| 70 |
+
qualityScore,
|
| 71 |
+
validationScore,
|
| 72 |
+
simulationScore,
|
| 73 |
+
keyLessons: this.extractLessons('success', nodeTypes, qualityScore),
|
| 74 |
+
createdAt: new Date().toISOString(),
|
| 75 |
+
useCount: 0,
|
| 76 |
+
});
|
| 77 |
+
this.updateTemplate(domain, nodeTypes, qualityScore, true);
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
storeFailurePattern(
|
| 81 |
+
jobId: string,
|
| 82 |
+
domain: string,
|
| 83 |
+
workflowType: string,
|
| 84 |
+
integrations: string[],
|
| 85 |
+
nodeTypes: string[],
|
| 86 |
+
architectureDescription: string,
|
| 87 |
+
qualityScore: number,
|
| 88 |
+
failureReasons: string[],
|
| 89 |
+
): void {
|
| 90 |
+
const id = `failure-${jobId}`;
|
| 91 |
+
this.patterns.set(id, {
|
| 92 |
+
id,
|
| 93 |
+
type: 'failure',
|
| 94 |
+
domain,
|
| 95 |
+
workflowType,
|
| 96 |
+
integrations,
|
| 97 |
+
nodeTypes,
|
| 98 |
+
architectureDescription,
|
| 99 |
+
qualityScore,
|
| 100 |
+
validationScore: 0,
|
| 101 |
+
simulationScore: 0,
|
| 102 |
+
keyLessons: [],
|
| 103 |
+
antiPatterns: failureReasons,
|
| 104 |
+
createdAt: new Date().toISOString(),
|
| 105 |
+
useCount: 0,
|
| 106 |
+
});
|
| 107 |
+
this.updateTemplate(domain, nodeTypes, qualityScore, false);
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
storeFixStrategy(issuePattern: string, fixDescription: string): void {
|
| 111 |
+
const key = this.normalizeKey(issuePattern);
|
| 112 |
+
this.fixStrategies.set(key, fixDescription);
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
// βββ Memory Retrieval βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 116 |
+
|
| 117 |
+
getRelevantPatterns(domain: string, integrations: string[]): WorkflowPattern[] {
|
| 118 |
+
return [...this.patterns.values()]
|
| 119 |
+
.filter((p) =>
|
| 120 |
+
p.domain === domain ||
|
| 121 |
+
integrations.some((i) => p.integrations.includes(i)),
|
| 122 |
+
)
|
| 123 |
+
.sort((a, b) => b.qualityScore - a.qualityScore)
|
| 124 |
+
.slice(0, 5);
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
getHighPerformingTemplates(domain: string): ArchitectureTemplate[] {
|
| 128 |
+
return [...this.templates.values()]
|
| 129 |
+
.filter((t) => t.domain === domain || t.domain === 'general')
|
| 130 |
+
.sort((a, b) => b.avgQualityScore - a.avgQualityScore)
|
| 131 |
+
.slice(0, 3);
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
getSuccessPatterns(): WorkflowPattern[] {
|
| 135 |
+
return [...this.patterns.values()]
|
| 136 |
+
.filter((p) => p.type === 'success' && p.qualityScore >= 80)
|
| 137 |
+
.sort((a, b) => b.qualityScore - a.qualityScore)
|
| 138 |
+
.slice(0, 10);
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
getAntiPatterns(): string[] {
|
| 142 |
+
return [...this.patterns.values()]
|
| 143 |
+
.filter((p) => p.type === 'failure')
|
| 144 |
+
.flatMap((p) => p.antiPatterns ?? [])
|
| 145 |
+
.filter((p, i, arr) => arr.indexOf(p) === i) // unique
|
| 146 |
+
.slice(0, 20);
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
getFixStrategy(issuePattern: string): string | undefined {
|
| 150 |
+
return this.fixStrategies.get(this.normalizeKey(issuePattern));
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
/**
|
| 154 |
+
* Build memory context string for LLM injection
|
| 155 |
+
*/
|
| 156 |
+
buildMemoryContext(domain: string, integrations: string[]): string {
|
| 157 |
+
const patterns = this.getRelevantPatterns(domain, integrations);
|
| 158 |
+
const templates = this.getHighPerformingTemplates(domain);
|
| 159 |
+
const antiPatterns = this.getAntiPatterns();
|
| 160 |
+
|
| 161 |
+
const lines: string[] = [];
|
| 162 |
+
|
| 163 |
+
if (patterns.length > 0) {
|
| 164 |
+
lines.push('=== SUCCESSFUL WORKFLOW PATTERNS FROM MEMORY ===');
|
| 165 |
+
patterns
|
| 166 |
+
.filter((p) => p.type === 'success')
|
| 167 |
+
.slice(0, 3)
|
| 168 |
+
.forEach((p) => {
|
| 169 |
+
lines.push(`- Domain: ${p.domain}, Integrations: ${p.integrations.join(', ')}, Score: ${p.qualityScore}/100`);
|
| 170 |
+
lines.push(` Architecture: ${p.architectureDescription}`);
|
| 171 |
+
lines.push(` Nodes: ${p.nodeTypes.join(' β ')}`);
|
| 172 |
+
lines.push(` Lessons: ${p.keyLessons.join('; ')}`);
|
| 173 |
+
});
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
if (templates.length > 0) {
|
| 177 |
+
lines.push('\n=== HIGH-PERFORMING ARCHITECTURE TEMPLATES ===');
|
| 178 |
+
templates.forEach((t) => {
|
| 179 |
+
lines.push(`- ${t.name}: ${t.description}`);
|
| 180 |
+
lines.push(` Node Sequence: ${t.nodeSequence.join(' β ')}`);
|
| 181 |
+
lines.push(` Avg Score: ${t.avgQualityScore}/100, Success Rate: ${Math.round(t.successRate * 100)}%`);
|
| 182 |
+
});
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
if (antiPatterns.length > 0) {
|
| 186 |
+
lines.push('\n=== KNOWN ANTI-PATTERNS TO AVOID ===');
|
| 187 |
+
antiPatterns.slice(0, 5).forEach((ap) => lines.push(`- AVOID: ${ap}`));
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
return lines.length > 0 ? lines.join('\n') : '';
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
getStats(): {
|
| 194 |
+
totalPatterns: number;
|
| 195 |
+
successPatterns: number;
|
| 196 |
+
failurePatterns: number;
|
| 197 |
+
templates: number;
|
| 198 |
+
avgSuccessScore: number;
|
| 199 |
+
fixStrategies: number;
|
| 200 |
+
} {
|
| 201 |
+
const all = [...this.patterns.values()];
|
| 202 |
+
const successes = all.filter((p) => p.type === 'success');
|
| 203 |
+
const avgScore = successes.length > 0
|
| 204 |
+
? Math.round(successes.reduce((sum, p) => sum + p.qualityScore, 0) / successes.length)
|
| 205 |
+
: 0;
|
| 206 |
+
|
| 207 |
+
return {
|
| 208 |
+
totalPatterns: all.length,
|
| 209 |
+
successPatterns: successes.length,
|
| 210 |
+
failurePatterns: all.filter((p) => p.type === 'failure').length,
|
| 211 |
+
templates: this.templates.size,
|
| 212 |
+
avgSuccessScore: avgScore,
|
| 213 |
+
fixStrategies: this.fixStrategies.size,
|
| 214 |
+
};
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
// βββ Private helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 218 |
+
|
| 219 |
+
private updateTemplate(domain: string, nodeTypes: string[], score: number, success: boolean): void {
|
| 220 |
+
const key = `${domain}-${nodeTypes.slice(0, 3).join('-')}`;
|
| 221 |
+
const existing = this.templates.get(key);
|
| 222 |
+
|
| 223 |
+
if (existing) {
|
| 224 |
+
const total = existing.usageCount + 1;
|
| 225 |
+
const successCount = success
|
| 226 |
+
? Math.round(existing.successRate * existing.usageCount) + 1
|
| 227 |
+
: Math.round(existing.successRate * existing.usageCount);
|
| 228 |
+
this.templates.set(key, {
|
| 229 |
+
...existing,
|
| 230 |
+
avgQualityScore: Math.round((existing.avgQualityScore * existing.usageCount + score) / total),
|
| 231 |
+
successRate: successCount / total,
|
| 232 |
+
usageCount: total,
|
| 233 |
+
});
|
| 234 |
+
}
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
private extractLessons(type: string, nodeTypes: string[], score: number): string[] {
|
| 238 |
+
const lessons: string[] = [];
|
| 239 |
+
if (score >= 90) lessons.push('High-quality architecture with proper error handling');
|
| 240 |
+
if (nodeTypes.includes('@n8n/n8n-nodes-langchain.agent')) {
|
| 241 |
+
lessons.push('AI Agent with proper system message and dynamic user input');
|
| 242 |
+
}
|
| 243 |
+
if (nodeTypes.includes('n8n-nodes-base.splitInBatches')) {
|
| 244 |
+
lessons.push('Batch processing with SplitInBatches for array handling');
|
| 245 |
+
}
|
| 246 |
+
return lessons;
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
private normalizeKey(text: string): string {
|
| 250 |
+
return text.toLowerCase().replace(/[^a-z0-9]/g, '-').slice(0, 80);
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
// βββ Built-in high-performing templates βββββββββββββββββββββββββββββββββββ
|
| 254 |
+
private seedBuiltinTemplates(): void {
|
| 255 |
+
const builtins: ArchitectureTemplate[] = [
|
| 256 |
+
{
|
| 257 |
+
id: 'telegram-ai-bot',
|
| 258 |
+
name: 'Telegram AI Bot',
|
| 259 |
+
domain: 'ai_workflow',
|
| 260 |
+
triggers: ['n8n-nodes-base.telegramTrigger'],
|
| 261 |
+
nodeSequence: [
|
| 262 |
+
'n8n-nodes-base.telegramTrigger',
|
| 263 |
+
'n8n-nodes-base.set',
|
| 264 |
+
'@n8n/n8n-nodes-langchain.agent',
|
| 265 |
+
'n8n-nodes-base.set',
|
| 266 |
+
'n8n-nodes-base.telegram',
|
| 267 |
+
],
|
| 268 |
+
avgQualityScore: 92,
|
| 269 |
+
successRate: 0.95,
|
| 270 |
+
description: 'Telegram bot with AI Agent response β SET for input prep, AI Agent, SET for output formatting, Telegram send',
|
| 271 |
+
usageCount: 0,
|
| 272 |
+
},
|
| 273 |
+
{
|
| 274 |
+
id: 'webhook-to-integration',
|
| 275 |
+
name: 'Webhook β Validate β Process β Notify',
|
| 276 |
+
domain: 'webhook_system',
|
| 277 |
+
triggers: ['n8n-nodes-base.webhook'],
|
| 278 |
+
nodeSequence: [
|
| 279 |
+
'n8n-nodes-base.webhook',
|
| 280 |
+
'n8n-nodes-base.if',
|
| 281 |
+
'n8n-nodes-base.set',
|
| 282 |
+
'n8n-nodes-base.httpRequest',
|
| 283 |
+
'n8n-nodes-base.slack',
|
| 284 |
+
'n8n-nodes-base.respondToWebhook',
|
| 285 |
+
],
|
| 286 |
+
avgQualityScore: 88,
|
| 287 |
+
successRate: 0.92,
|
| 288 |
+
description: 'Webhook trigger β validate input with IF β SET fields β call external API β notify Slack β respond to caller',
|
| 289 |
+
usageCount: 0,
|
| 290 |
+
},
|
| 291 |
+
{
|
| 292 |
+
id: 'scheduled-data-pipeline',
|
| 293 |
+
name: 'Scheduled Data Pipeline',
|
| 294 |
+
domain: 'data_pipeline',
|
| 295 |
+
triggers: ['n8n-nodes-base.scheduleTrigger'],
|
| 296 |
+
nodeSequence: [
|
| 297 |
+
'n8n-nodes-base.scheduleTrigger',
|
| 298 |
+
'n8n-nodes-base.httpRequest',
|
| 299 |
+
'n8n-nodes-base.code',
|
| 300 |
+
'n8n-nodes-base.splitInBatches',
|
| 301 |
+
'n8n-nodes-base.set',
|
| 302 |
+
'n8n-nodes-base.googleSheets',
|
| 303 |
+
],
|
| 304 |
+
avgQualityScore: 87,
|
| 305 |
+
successRate: 0.90,
|
| 306 |
+
description: 'Cron trigger β fetch data via HTTP β transform with Code β batch process β SET fields β store to Sheets',
|
| 307 |
+
usageCount: 0,
|
| 308 |
+
},
|
| 309 |
+
{
|
| 310 |
+
id: 'email-to-crm',
|
| 311 |
+
name: 'Email Trigger β CRM Update',
|
| 312 |
+
domain: 'crm_integration',
|
| 313 |
+
triggers: ['n8n-nodes-base.emailReadImap'],
|
| 314 |
+
nodeSequence: [
|
| 315 |
+
'n8n-nodes-base.emailReadImap',
|
| 316 |
+
'n8n-nodes-base.set',
|
| 317 |
+
'n8n-nodes-base.if',
|
| 318 |
+
'n8n-nodes-base.hubspot',
|
| 319 |
+
'n8n-nodes-base.slack',
|
| 320 |
+
],
|
| 321 |
+
avgQualityScore: 85,
|
| 322 |
+
successRate: 0.88,
|
| 323 |
+
description: 'Email trigger β extract fields β IF condition β update HubSpot β notify team via Slack',
|
| 324 |
+
usageCount: 0,
|
| 325 |
+
},
|
| 326 |
+
];
|
| 327 |
+
|
| 328 |
+
builtins.forEach((t) => this.templates.set(t.id, t));
|
| 329 |
+
}
|
| 330 |
+
}
|
| 331 |
+
|
| 332 |
+
// βββ Singleton export βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 333 |
+
export const workflowMemory = new MemoryStore();
|
apps/simulator/src/services/mockDataGenerator.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Mock Data Generator Service β UPGRADED
|
| 3 |
+
* Generates realistic mock inputs for dry-run simulation
|
| 4 |
+
* Provider-agnostic LLM Gateway β NO direct OpenAI dependency
|
| 5 |
+
*/
|
| 6 |
+
import { type LLMGateway } from '@wfo/integrations/llm-providers/index';
|
| 7 |
+
import type { N8nWorkflow, WorkflowGraph } from '../types/workflow';
|
| 8 |
+
|
| 9 |
+
export class MockDataGenerator {
|
| 10 |
+
private llm: LLMGateway;
|
| 11 |
+
|
| 12 |
+
constructor(llm: LLMGateway) {
|
| 13 |
+
this.llm = llm;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
async generate(
|
| 17 |
+
workflow: N8nWorkflow,
|
| 18 |
+
graph: WorkflowGraph,
|
| 19 |
+
): Promise<Record<string, unknown>> {
|
| 20 |
+
const triggerNode = workflow.nodes.find((n) =>
|
| 21 |
+
n.type.toLowerCase().includes('trigger') ||
|
| 22 |
+
n.type.toLowerCase().includes('webhook'),
|
| 23 |
+
);
|
| 24 |
+
|
| 25 |
+
return this.llm.completeJSON<Record<string, unknown>>([
|
| 26 |
+
{
|
| 27 |
+
role: 'system',
|
| 28 |
+
content: `You are a test data engineer. Generate realistic mock data for workflow simulation.
|
| 29 |
+
Always include multiple scenarios: normal, malformed, null_payload, partial_payload, edge_case.
|
| 30 |
+
Data must match the actual fields the workflow nodes expect based on the trigger type.`,
|
| 31 |
+
},
|
| 32 |
+
{
|
| 33 |
+
role: 'user',
|
| 34 |
+
content: `Generate mock input data for this workflow's trigger node.
|
| 35 |
+
|
| 36 |
+
TRIGGER NODE: ${JSON.stringify(triggerNode ?? workflow.nodes[0], null, 2)}
|
| 37 |
+
WORKFLOW NAME: ${workflow.name}
|
| 38 |
+
WORKFLOW DOMAIN: ${graph.metadata.domain}
|
| 39 |
+
|
| 40 |
+
Return JSON:
|
| 41 |
+
{
|
| 42 |
+
"normal": { ... realistic happy-path data matching trigger output ... },
|
| 43 |
+
"malformed": { ... intentionally broken/wrong-type data ... },
|
| 44 |
+
"null_payload": null,
|
| 45 |
+
"partial_payload": { ... some required fields missing ... },
|
| 46 |
+
"edge_case": { ... boundary conditions, very long strings, unicode, empty arrays ... }
|
| 47 |
+
}`,
|
| 48 |
+
},
|
| 49 |
+
], {
|
| 50 |
+
temperature: 0.3,
|
| 51 |
+
retries: 2,
|
| 52 |
+
}).catch(() => ({
|
| 53 |
+
normal: {},
|
| 54 |
+
malformed: {},
|
| 55 |
+
null_payload: null,
|
| 56 |
+
partial_payload: {},
|
| 57 |
+
edge_case: {},
|
| 58 |
+
error: 'Mock data generation failed β using empty defaults',
|
| 59 |
+
}));
|
| 60 |
+
}
|
| 61 |
+
}
|
apps/simulator/src/services/qualityScorer.ts
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Quality Scorer Service β UPGRADED
|
| 3 |
+
* Evaluates workflow quality across multiple dimensions
|
| 4 |
+
* Produces deployment readiness score with detailed breakdown
|
| 5 |
+
*/
|
| 6 |
+
import type {
|
| 7 |
+
ValidationReport,
|
| 8 |
+
SimulationReport,
|
| 9 |
+
CredentialAnalysis,
|
| 10 |
+
DeploymentReadiness,
|
| 11 |
+
WorkflowQualityScore,
|
| 12 |
+
} from '../types/workflow';
|
| 13 |
+
|
| 14 |
+
export class QualityScorer {
|
| 15 |
+
scoreDeploymentReadiness(
|
| 16 |
+
validation: ValidationReport,
|
| 17 |
+
simulation: SimulationReport | undefined,
|
| 18 |
+
credentials: CredentialAnalysis,
|
| 19 |
+
): DeploymentReadiness {
|
| 20 |
+
const blockingIssues: string[] = [];
|
| 21 |
+
const recommendations: string[] = [];
|
| 22 |
+
|
| 23 |
+
// Gate 1: Validation
|
| 24 |
+
if (!validation.valid) {
|
| 25 |
+
const errorCount = validation.issues.length;
|
| 26 |
+
blockingIssues.push(`Validation failed with ${errorCount} error(s): ${validation.issues.map((i) => i.message).slice(0, 3).join('; ')}`);
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
// Gate 2: Node Registry (unknown nodes = blocking)
|
| 30 |
+
if (validation.nodeRegistryValidation && !validation.nodeRegistryValidation.passed) {
|
| 31 |
+
const unknownErrors = validation.nodeRegistryValidation.issues.filter((i) => i.severity === 'error');
|
| 32 |
+
if (unknownErrors.length > 0) {
|
| 33 |
+
blockingIssues.push(`${unknownErrors.length} unknown/invalid node type(s) detected β must use only registered n8n node types`);
|
| 34 |
+
}
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
// Gate 3: Simulation
|
| 38 |
+
if (simulation && !simulation.passed) {
|
| 39 |
+
blockingIssues.push(`Dry-run simulation failed (readiness: ${simulation.readinessScore}/100)`);
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
// Gate 4: Deployment blockers from simulation
|
| 43 |
+
if (simulation?.deploymentBlockers && simulation.deploymentBlockers.length > 0) {
|
| 44 |
+
simulation.deploymentBlockers.slice(0, 3).forEach((blocker) => {
|
| 45 |
+
blockingIssues.push(`Simulation blocker: ${blocker}`);
|
| 46 |
+
});
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
// Gate 5: Credentials (warning only β some deployments don't have n8n access)
|
| 50 |
+
if (!credentials.allCredentialsPresent && credentials.n8nReachable) {
|
| 51 |
+
const missingTypes = credentials.missing.map((m) => m.credentialType).join(', ');
|
| 52 |
+
blockingIssues.push(`Missing credentials in n8n: ${missingTypes}`);
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
// Recommendations
|
| 56 |
+
if (validation.warnings.length > 0) {
|
| 57 |
+
recommendations.push(`Address ${validation.warnings.length} validation warning(s) for production-grade quality`);
|
| 58 |
+
}
|
| 59 |
+
if (simulation?.predictedFailurePoints && simulation.predictedFailurePoints.length > 0) {
|
| 60 |
+
recommendations.push(`Review ${simulation.predictedFailurePoints.length} predicted failure point(s)`);
|
| 61 |
+
}
|
| 62 |
+
if (simulation?.recommendations) {
|
| 63 |
+
recommendations.push(...simulation.recommendations.slice(0, 3));
|
| 64 |
+
}
|
| 65 |
+
if (!credentials.allCredentialsPresent && !credentials.n8nReachable) {
|
| 66 |
+
recommendations.push('Verify all required credentials are configured in n8n before activation');
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
// Confidence score calculation
|
| 70 |
+
const validationScore = validation.overallScore;
|
| 71 |
+
const simulationScore = simulation?.readinessScore ?? 50;
|
| 72 |
+
const credentialScore = credentials.allCredentialsPresent ? 100 : (credentials.n8nReachable ? 0 : 50);
|
| 73 |
+
const nodeRegistryScore = validation.nodeRegistryValidation?.score ?? 100;
|
| 74 |
+
const dataFlowScore = validation.dataFlowValidation?.score ?? 100;
|
| 75 |
+
|
| 76 |
+
const confidenceScore = Math.round(
|
| 77 |
+
validationScore * 0.30 +
|
| 78 |
+
simulationScore * 0.30 +
|
| 79 |
+
credentialScore * 0.15 +
|
| 80 |
+
nodeRegistryScore * 0.15 +
|
| 81 |
+
dataFlowScore * 0.10,
|
| 82 |
+
);
|
| 83 |
+
|
| 84 |
+
const riskLevel = (() => {
|
| 85 |
+
if (confidenceScore >= 85) return 'low' as const;
|
| 86 |
+
if (confidenceScore >= 65) return 'medium' as const;
|
| 87 |
+
if (confidenceScore >= 40) return 'high' as const;
|
| 88 |
+
return 'critical' as const;
|
| 89 |
+
})();
|
| 90 |
+
|
| 91 |
+
return {
|
| 92 |
+
deploymentReady: blockingIssues.length === 0,
|
| 93 |
+
confidenceScore,
|
| 94 |
+
riskLevel,
|
| 95 |
+
blockingIssues,
|
| 96 |
+
recommendations,
|
| 97 |
+
};
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
calculateQualityScore(
|
| 101 |
+
validation: ValidationReport,
|
| 102 |
+
simulation: SimulationReport | undefined,
|
| 103 |
+
): WorkflowQualityScore {
|
| 104 |
+
const reliabilityScore = validation.reliabilityValidation.score;
|
| 105 |
+
const observabilityScore = Math.min(100, reliabilityScore + 10);
|
| 106 |
+
const retryCoverage = Math.max(0, reliabilityScore - 10);
|
| 107 |
+
const expressionSafety = validation.expressionValidation.score;
|
| 108 |
+
const credentialReadiness = validation.credentialValidation.score;
|
| 109 |
+
const branchingQuality = validation.graphValidation.score;
|
| 110 |
+
const deploymentSafety = validation.schemaValidation.score;
|
| 111 |
+
const nodeRegistryScore = validation.nodeRegistryValidation?.score ?? 100;
|
| 112 |
+
const dataFlowScore = validation.dataFlowValidation?.score ?? 100;
|
| 113 |
+
|
| 114 |
+
const maintainabilityScore = Math.round((branchingQuality + deploymentSafety + nodeRegistryScore) / 3);
|
| 115 |
+
|
| 116 |
+
const overallScore = Math.round(
|
| 117 |
+
reliabilityScore * 0.15 +
|
| 118 |
+
expressionSafety * 0.15 +
|
| 119 |
+
credentialReadiness * 0.10 +
|
| 120 |
+
branchingQuality * 0.10 +
|
| 121 |
+
deploymentSafety * 0.10 +
|
| 122 |
+
nodeRegistryScore * 0.20 +
|
| 123 |
+
dataFlowScore * 0.10 +
|
| 124 |
+
observabilityScore * 0.10,
|
| 125 |
+
);
|
| 126 |
+
|
| 127 |
+
const riskLevel = overallScore >= 85 ? 'low' as const
|
| 128 |
+
: overallScore >= 65 ? 'medium' as const
|
| 129 |
+
: overallScore >= 40 ? 'high' as const
|
| 130 |
+
: 'critical' as const;
|
| 131 |
+
|
| 132 |
+
return {
|
| 133 |
+
overallScore,
|
| 134 |
+
reliabilityScore,
|
| 135 |
+
maintainabilityScore,
|
| 136 |
+
observabilityScore,
|
| 137 |
+
retryCoverage,
|
| 138 |
+
expressionSafety,
|
| 139 |
+
credentialReadiness,
|
| 140 |
+
branchingQuality,
|
| 141 |
+
deploymentSafety,
|
| 142 |
+
riskLevel,
|
| 143 |
+
scoreExplanations: {
|
| 144 |
+
overall: `Overall quality: ${overallScore}/100 (${riskLevel} risk)`,
|
| 145 |
+
reliability: `Retry policies and fallback coverage: ${reliabilityScore}/100`,
|
| 146 |
+
maintainability: `Graph structure, naming, registry compliance: ${maintainabilityScore}/100`,
|
| 147 |
+
observability: `Logging and monitoring coverage: ${observabilityScore}/100`,
|
| 148 |
+
expressionSafety: `Expression null-safety (optional chaining): ${expressionSafety}/100`,
|
| 149 |
+
credentialReadiness: `Credential availability: ${credentialReadiness}/100`,
|
| 150 |
+
nodeRegistry: `Registry compliance (no unknown nodes): ${nodeRegistryScore}/100`,
|
| 151 |
+
dataFlow: `Data flow correctness (expressions, SET, IF, CODE): ${dataFlowScore}/100`,
|
| 152 |
+
},
|
| 153 |
+
};
|
| 154 |
+
}
|
| 155 |
+
}
|
apps/simulator/src/services/selfHealing.ts
ADDED
|
@@ -0,0 +1,377 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Self-Healing System β NEW
|
| 3 |
+
* Detects root cause of workflow failures and applies corrective transformations
|
| 4 |
+
* Stores failure patterns in memory, prevents repeat failures
|
| 5 |
+
* Integrates with validation + simulation pipeline
|
| 6 |
+
*/
|
| 7 |
+
import { type LLMGateway } from '@wfo/integrations/llm-providers/index';
|
| 8 |
+
import type { ValidationReport, SimulationReport, N8nWorkflow, WorkflowGraph } from '../types/workflow';
|
| 9 |
+
import { isValidNodeType } from '../knowledge/nodeRegistry';
|
| 10 |
+
|
| 11 |
+
export interface HealingAction {
|
| 12 |
+
type: 'replace_node' | 'fix_expression' | 'add_parameter' | 'fix_connection' | 'add_retry' | 'remove_node';
|
| 13 |
+
targetNodeId?: string;
|
| 14 |
+
description: string;
|
| 15 |
+
applied: boolean;
|
| 16 |
+
before?: unknown;
|
| 17 |
+
after?: unknown;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
export interface HealingResult {
|
| 21 |
+
healed: boolean;
|
| 22 |
+
actionsApplied: HealingAction[];
|
| 23 |
+
remainingIssues: string[];
|
| 24 |
+
healedWorkflow?: N8nWorkflow;
|
| 25 |
+
confidence: number; // 0-100
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
export interface FailurePattern {
|
| 29 |
+
id: string;
|
| 30 |
+
pattern: string;
|
| 31 |
+
rootCause: string;
|
| 32 |
+
fixStrategy: string;
|
| 33 |
+
occurrences: number;
|
| 34 |
+
lastSeen: string;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
// In-memory failure store (persists across requests in a single process)
|
| 38 |
+
const failureMemory: Map<string, FailurePattern> = new Map();
|
| 39 |
+
|
| 40 |
+
export class SelfHealingSystem {
|
| 41 |
+
private llm: LLMGateway;
|
| 42 |
+
|
| 43 |
+
constructor(llm: LLMGateway) {
|
| 44 |
+
this.llm = llm;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
/**
|
| 48 |
+
* Attempt to auto-heal a workflow based on validation and simulation reports
|
| 49 |
+
*/
|
| 50 |
+
async heal(
|
| 51 |
+
workflow: N8nWorkflow,
|
| 52 |
+
graph: WorkflowGraph,
|
| 53 |
+
validationReport: ValidationReport,
|
| 54 |
+
simulationReport?: SimulationReport,
|
| 55 |
+
): Promise<HealingResult> {
|
| 56 |
+
const actions: HealingAction[] = [];
|
| 57 |
+
let healedWorkflow = structuredClone(workflow);
|
| 58 |
+
|
| 59 |
+
// βββ Phase 1: Rule-based deterministic healing ββββββββββββββββββββββββ
|
| 60 |
+
const ruleBasedActions = this.applyRuleBasedFixes(healedWorkflow, validationReport);
|
| 61 |
+
actions.push(...ruleBasedActions.actions);
|
| 62 |
+
healedWorkflow = ruleBasedActions.workflow;
|
| 63 |
+
|
| 64 |
+
// βββ Phase 2: AI-assisted healing for complex issues ββββββββββββββββββ
|
| 65 |
+
const complexIssues = validationReport.issues.filter((i) =>
|
| 66 |
+
i.severity === 'error' &&
|
| 67 |
+
!actions.some((a) => a.targetNodeId === i.nodeId && a.applied),
|
| 68 |
+
);
|
| 69 |
+
|
| 70 |
+
if (complexIssues.length > 0) {
|
| 71 |
+
try {
|
| 72 |
+
const aiHealing = await this.applyAIHealing(healedWorkflow, graph, complexIssues, simulationReport);
|
| 73 |
+
actions.push(...aiHealing.actions);
|
| 74 |
+
if (aiHealing.healedWorkflow) {
|
| 75 |
+
healedWorkflow = aiHealing.healedWorkflow;
|
| 76 |
+
}
|
| 77 |
+
} catch (err) {
|
| 78 |
+
console.warn('[SelfHealing] AI healing phase failed:', err);
|
| 79 |
+
}
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
// βββ Phase 3: Record failure pattern βββββββββββββββββββββββββββββββββ
|
| 83 |
+
this.recordFailurePattern(validationReport, simulationReport, actions);
|
| 84 |
+
|
| 85 |
+
const appliedCount = actions.filter((a) => a.applied).length;
|
| 86 |
+
const remainingErrors = validationReport.issues.filter((i) =>
|
| 87 |
+
i.severity === 'error' &&
|
| 88 |
+
!actions.some((a) => a.targetNodeId === i.nodeId && a.applied),
|
| 89 |
+
);
|
| 90 |
+
|
| 91 |
+
return {
|
| 92 |
+
healed: appliedCount > 0 && remainingErrors.length === 0,
|
| 93 |
+
actionsApplied: actions.filter((a) => a.applied),
|
| 94 |
+
remainingIssues: remainingErrors.map((i) => i.message),
|
| 95 |
+
healedWorkflow: appliedCount > 0 ? healedWorkflow : undefined,
|
| 96 |
+
confidence: Math.round((appliedCount / Math.max(1, actions.length)) * 100),
|
| 97 |
+
};
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
// βββ Rule-Based Fixes (deterministic, no LLM needed) βββββββββββββββββββββ
|
| 101 |
+
private applyRuleBasedFixes(
|
| 102 |
+
workflow: N8nWorkflow,
|
| 103 |
+
report: ValidationReport,
|
| 104 |
+
): { workflow: N8nWorkflow; actions: HealingAction[] } {
|
| 105 |
+
const actions: HealingAction[] = [];
|
| 106 |
+
|
| 107 |
+
// Fix 1: active must be false
|
| 108 |
+
if ((workflow.active as any) === true) {
|
| 109 |
+
workflow.active = false;
|
| 110 |
+
actions.push({
|
| 111 |
+
type: 'fix_expression',
|
| 112 |
+
description: 'Set active: false (safety rule β never auto-activate)',
|
| 113 |
+
applied: true,
|
| 114 |
+
before: true,
|
| 115 |
+
after: false,
|
| 116 |
+
});
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
// Fix 2: Missing node IDs β generate UUID-like IDs
|
| 120 |
+
workflow.nodes.forEach((node) => {
|
| 121 |
+
if (!node.id || node.id.trim() === '') {
|
| 122 |
+
const newId = generateId();
|
| 123 |
+
actions.push({
|
| 124 |
+
type: 'add_parameter',
|
| 125 |
+
targetNodeId: node.name,
|
| 126 |
+
description: `Generated missing ID for node "${node.name}"`,
|
| 127 |
+
applied: true,
|
| 128 |
+
before: node.id,
|
| 129 |
+
after: newId,
|
| 130 |
+
});
|
| 131 |
+
node.id = newId;
|
| 132 |
+
}
|
| 133 |
+
});
|
| 134 |
+
|
| 135 |
+
// Fix 3: Missing typeVersion β set to 1
|
| 136 |
+
workflow.nodes.forEach((node) => {
|
| 137 |
+
if (!node.typeVersion || node.typeVersion < 1) {
|
| 138 |
+
actions.push({
|
| 139 |
+
type: 'add_parameter',
|
| 140 |
+
targetNodeId: node.id,
|
| 141 |
+
description: `Added missing typeVersion: 1 to node "${node.name}"`,
|
| 142 |
+
applied: true,
|
| 143 |
+
before: node.typeVersion,
|
| 144 |
+
after: 1,
|
| 145 |
+
});
|
| 146 |
+
node.typeVersion = 1;
|
| 147 |
+
}
|
| 148 |
+
});
|
| 149 |
+
|
| 150 |
+
// Fix 4: Missing position β assign default grid positions
|
| 151 |
+
workflow.nodes.forEach((node, i) => {
|
| 152 |
+
if (!node.position || !Array.isArray(node.position) || node.position.length !== 2) {
|
| 153 |
+
const newPos: [number, number] = [i * 220, 300];
|
| 154 |
+
actions.push({
|
| 155 |
+
type: 'fix_expression',
|
| 156 |
+
targetNodeId: node.id,
|
| 157 |
+
description: `Set default position for node "${node.name}"`,
|
| 158 |
+
applied: true,
|
| 159 |
+
before: node.position,
|
| 160 |
+
after: newPos,
|
| 161 |
+
});
|
| 162 |
+
node.position = newPos;
|
| 163 |
+
}
|
| 164 |
+
});
|
| 165 |
+
|
| 166 |
+
// Fix 5: Add retry to external nodes that support it
|
| 167 |
+
workflow.nodes.forEach((node) => {
|
| 168 |
+
const externalTypes = [
|
| 169 |
+
'n8n-nodes-base.httpRequest',
|
| 170 |
+
'n8n-nodes-base.telegram',
|
| 171 |
+
'n8n-nodes-base.slack',
|
| 172 |
+
'n8n-nodes-base.gmail',
|
| 173 |
+
'n8n-nodes-base.openAi',
|
| 174 |
+
'@n8n/n8n-nodes-langchain.agent',
|
| 175 |
+
'n8n-nodes-base.googleSheets',
|
| 176 |
+
'n8n-nodes-base.airtable',
|
| 177 |
+
'n8n-nodes-base.notion',
|
| 178 |
+
'n8n-nodes-base.postgres',
|
| 179 |
+
'n8n-nodes-base.hubspot',
|
| 180 |
+
'n8n-nodes-base.stripe',
|
| 181 |
+
'n8n-nodes-base.github',
|
| 182 |
+
];
|
| 183 |
+
|
| 184 |
+
if (externalTypes.includes(node.type) && !node.retryOnFail) {
|
| 185 |
+
actions.push({
|
| 186 |
+
type: 'add_retry',
|
| 187 |
+
targetNodeId: node.id,
|
| 188 |
+
description: `Added retry policy to external node "${node.name}"`,
|
| 189 |
+
applied: true,
|
| 190 |
+
before: { retryOnFail: false },
|
| 191 |
+
after: { retryOnFail: true, maxTries: 3, waitBetweenTries: 1000 },
|
| 192 |
+
});
|
| 193 |
+
node.retryOnFail = true;
|
| 194 |
+
node.maxTries = node.maxTries ?? 3;
|
| 195 |
+
node.waitBetweenTries = node.waitBetweenTries ?? 1000;
|
| 196 |
+
}
|
| 197 |
+
});
|
| 198 |
+
|
| 199 |
+
// Fix 6: onError field default
|
| 200 |
+
workflow.nodes.forEach((node) => {
|
| 201 |
+
if (!node.onError) {
|
| 202 |
+
node.onError = 'continueErrorOutput';
|
| 203 |
+
actions.push({
|
| 204 |
+
type: 'add_parameter',
|
| 205 |
+
targetNodeId: node.id,
|
| 206 |
+
description: `Set onError: continueErrorOutput for node "${node.name}"`,
|
| 207 |
+
applied: true,
|
| 208 |
+
before: undefined,
|
| 209 |
+
after: 'continueErrorOutput',
|
| 210 |
+
});
|
| 211 |
+
}
|
| 212 |
+
});
|
| 213 |
+
|
| 214 |
+
// Fix 7: Remove nodes with unknown types from registry
|
| 215 |
+
const unknownNodes = workflow.nodes.filter((n) => n.type && !isValidNodeType(n.type));
|
| 216 |
+
unknownNodes.forEach((node) => {
|
| 217 |
+
actions.push({
|
| 218 |
+
type: 'remove_node',
|
| 219 |
+
targetNodeId: node.id,
|
| 220 |
+
description: `REJECTED unknown node type "${node.type}" for node "${node.name}" β not in registry`,
|
| 221 |
+
applied: true,
|
| 222 |
+
before: node.type,
|
| 223 |
+
after: null,
|
| 224 |
+
});
|
| 225 |
+
});
|
| 226 |
+
if (unknownNodes.length > 0) {
|
| 227 |
+
const unknownIds = new Set(unknownNodes.map((n) => n.id));
|
| 228 |
+
workflow.nodes = workflow.nodes.filter((n) => !unknownIds.has(n.id));
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
// Fix 8: Ensure settings are complete
|
| 232 |
+
if (!workflow.settings) {
|
| 233 |
+
workflow.settings = {};
|
| 234 |
+
actions.push({
|
| 235 |
+
type: 'add_parameter',
|
| 236 |
+
description: 'Added missing workflow settings object',
|
| 237 |
+
applied: true,
|
| 238 |
+
before: undefined,
|
| 239 |
+
after: {},
|
| 240 |
+
});
|
| 241 |
+
}
|
| 242 |
+
if (!workflow.settings.executionOrder) {
|
| 243 |
+
workflow.settings.executionOrder = 'v1';
|
| 244 |
+
actions.push({
|
| 245 |
+
type: 'add_parameter',
|
| 246 |
+
description: 'Set settings.executionOrder: v1',
|
| 247 |
+
applied: true,
|
| 248 |
+
});
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
return { workflow, actions };
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
// βββ AI-Assisted Healing (for complex expression / logic issues) ββββββββββ
|
| 255 |
+
private async applyAIHealing(
|
| 256 |
+
workflow: N8nWorkflow,
|
| 257 |
+
graph: WorkflowGraph,
|
| 258 |
+
issues: Array<{ message: string; nodeId?: string; suggestion: string; category: string }>,
|
| 259 |
+
simulationReport?: SimulationReport,
|
| 260 |
+
): Promise<{ actions: HealingAction[]; healedWorkflow?: N8nWorkflow }> {
|
| 261 |
+
// Find the historical fix if available
|
| 262 |
+
const historicalFixes = this.getRelevantHistoricalFixes(issues.map((i) => i.message));
|
| 263 |
+
|
| 264 |
+
const result = await this.llm.completeJSON<{
|
| 265 |
+
fixedWorkflow: N8nWorkflow;
|
| 266 |
+
actionsApplied: Array<{ nodeId?: string; description: string; type: string }>;
|
| 267 |
+
}>([
|
| 268 |
+
{
|
| 269 |
+
role: 'system',
|
| 270 |
+
content: `You are an n8n workflow self-healing system. Your job is to fix broken workflows.
|
| 271 |
+
Fix ONLY the identified issues. Do not change anything else.
|
| 272 |
+
NEVER set active: true.
|
| 273 |
+
ALWAYS use real expressions like {{$json?.field ?? ""}}.
|
| 274 |
+
Return the complete fixed workflow JSON.`,
|
| 275 |
+
},
|
| 276 |
+
{
|
| 277 |
+
role: 'user',
|
| 278 |
+
content: `Fix these workflow issues:
|
| 279 |
+
|
| 280 |
+
CURRENT WORKFLOW:
|
| 281 |
+
${JSON.stringify(workflow, null, 2)}
|
| 282 |
+
|
| 283 |
+
ISSUES TO FIX:
|
| 284 |
+
${issues.map((i, idx) => `${idx + 1}. [${i.category}] ${i.message}\n Fix: ${i.suggestion}`).join('\n')}
|
| 285 |
+
|
| 286 |
+
${simulationReport ? `SIMULATION FAILURES:\n${JSON.stringify(simulationReport.predictedFailurePoints, null, 2)}` : ''}
|
| 287 |
+
|
| 288 |
+
${historicalFixes.length > 0 ? `HISTORICAL FIX STRATEGIES:\n${historicalFixes.join('\n')}` : ''}
|
| 289 |
+
|
| 290 |
+
Return JSON:
|
| 291 |
+
{
|
| 292 |
+
"fixedWorkflow": { ... complete fixed n8n workflow ... },
|
| 293 |
+
"actionsApplied": [{ "nodeId": "...", "description": "what was fixed", "type": "fix_type" }]
|
| 294 |
+
}`,
|
| 295 |
+
},
|
| 296 |
+
], {
|
| 297 |
+
temperature: 0.0,
|
| 298 |
+
retries: 2,
|
| 299 |
+
});
|
| 300 |
+
|
| 301 |
+
const actions: HealingAction[] = (result.actionsApplied ?? []).map((a) => ({
|
| 302 |
+
type: a.type as HealingAction['type'],
|
| 303 |
+
targetNodeId: a.nodeId,
|
| 304 |
+
description: a.description,
|
| 305 |
+
applied: true,
|
| 306 |
+
}));
|
| 307 |
+
|
| 308 |
+
return {
|
| 309 |
+
actions,
|
| 310 |
+
healedWorkflow: result.fixedWorkflow ? {
|
| 311 |
+
...result.fixedWorkflow,
|
| 312 |
+
active: false, // Always enforce safety
|
| 313 |
+
} : undefined,
|
| 314 |
+
};
|
| 315 |
+
}
|
| 316 |
+
|
| 317 |
+
// βββ Failure Pattern Memory βββββββββββββββββββββββββββββββββββββββββββββββ
|
| 318 |
+
private recordFailurePattern(
|
| 319 |
+
validation: ValidationReport,
|
| 320 |
+
simulation: SimulationReport | undefined,
|
| 321 |
+
actions: HealingAction[],
|
| 322 |
+
): void {
|
| 323 |
+
validation.issues.forEach((issue) => {
|
| 324 |
+
const key = `${issue.category}-${issue.id}`;
|
| 325 |
+
const existing = failureMemory.get(key);
|
| 326 |
+
const action = actions.find((a) => a.targetNodeId === issue.nodeId);
|
| 327 |
+
|
| 328 |
+
if (existing) {
|
| 329 |
+
failureMemory.set(key, {
|
| 330 |
+
...existing,
|
| 331 |
+
occurrences: existing.occurrences + 1,
|
| 332 |
+
lastSeen: new Date().toISOString(),
|
| 333 |
+
fixStrategy: action?.description ?? existing.fixStrategy,
|
| 334 |
+
});
|
| 335 |
+
} else {
|
| 336 |
+
failureMemory.set(key, {
|
| 337 |
+
id: key,
|
| 338 |
+
pattern: issue.message,
|
| 339 |
+
rootCause: issue.category,
|
| 340 |
+
fixStrategy: action?.description ?? issue.suggestion,
|
| 341 |
+
occurrences: 1,
|
| 342 |
+
lastSeen: new Date().toISOString(),
|
| 343 |
+
});
|
| 344 |
+
}
|
| 345 |
+
});
|
| 346 |
+
}
|
| 347 |
+
|
| 348 |
+
private getRelevantHistoricalFixes(issueMessages: string[]): string[] {
|
| 349 |
+
const fixes: string[] = [];
|
| 350 |
+
failureMemory.forEach((pattern) => {
|
| 351 |
+
const isRelevant = issueMessages.some(
|
| 352 |
+
(msg) => msg.toLowerCase().includes(pattern.rootCause.toLowerCase()),
|
| 353 |
+
);
|
| 354 |
+
if (isRelevant && pattern.occurrences > 1) {
|
| 355 |
+
fixes.push(`Pattern: "${pattern.pattern}" β Fix: "${pattern.fixStrategy}" (seen ${pattern.occurrences}x)`);
|
| 356 |
+
}
|
| 357 |
+
});
|
| 358 |
+
return fixes.slice(0, 5); // Top 5 most relevant
|
| 359 |
+
}
|
| 360 |
+
|
| 361 |
+
/**
|
| 362 |
+
* Get current failure memory stats (for reporting)
|
| 363 |
+
*/
|
| 364 |
+
getMemoryStats(): { totalPatterns: number; topFailures: FailurePattern[] } {
|
| 365 |
+
const patterns = [...failureMemory.values()];
|
| 366 |
+
const sorted = patterns.sort((a, b) => b.occurrences - a.occurrences);
|
| 367 |
+
return {
|
| 368 |
+
totalPatterns: patterns.length,
|
| 369 |
+
topFailures: sorted.slice(0, 10),
|
| 370 |
+
};
|
| 371 |
+
}
|
| 372 |
+
}
|
| 373 |
+
|
| 374 |
+
function generateId(): string {
|
| 375 |
+
const hex = () => Math.floor(Math.random() * 0x10000).toString(16).padStart(4, '0');
|
| 376 |
+
return `${hex()}${hex()}-${hex()}-${hex()}-${hex()}-${hex()}${hex()}${hex()}`;
|
| 377 |
+
}
|
apps/simulator/src/services/swarmOrchestrator.ts
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Swarm Orchestrator β NEW
|
| 3 |
+
* Generates multiple workflow designs in parallel, selects the best via scoring
|
| 4 |
+
* Multi-agent swarm: each agent produces an independent design candidate
|
| 5 |
+
* Best candidate wins based on quality scoring
|
| 6 |
+
*/
|
| 7 |
+
import { type LLMGateway } from '@wfo/integrations/llm-providers/index';
|
| 8 |
+
import type {
|
| 9 |
+
WorkflowIntent,
|
| 10 |
+
WorkflowArchitecturePlan,
|
| 11 |
+
WorkflowGraph,
|
| 12 |
+
N8nWorkflow,
|
| 13 |
+
ValidationReport,
|
| 14 |
+
SimulationReport,
|
| 15 |
+
} from '../types/workflow';
|
| 16 |
+
import { WorkflowPlannerAgent } from '../agents/workflowPlanner';
|
| 17 |
+
import { WorkflowGraphEngine } from '../agents/graphEngine';
|
| 18 |
+
import { WorkflowCompiler } from '../agents/compiler';
|
| 19 |
+
import { ValidationEngine } from '../agents/validator';
|
| 20 |
+
import { QualityScorer } from './qualityScorer';
|
| 21 |
+
|
| 22 |
+
export interface SwarmCandidate {
|
| 23 |
+
id: string;
|
| 24 |
+
plan: WorkflowArchitecturePlan;
|
| 25 |
+
graph: WorkflowGraph;
|
| 26 |
+
workflow: N8nWorkflow;
|
| 27 |
+
validationReport: ValidationReport;
|
| 28 |
+
qualityScore: number;
|
| 29 |
+
planningStrategy: string;
|
| 30 |
+
selectionReason?: string;
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
export interface SwarmResult {
|
| 34 |
+
winner: SwarmCandidate;
|
| 35 |
+
allCandidates: SwarmCandidate[];
|
| 36 |
+
swarmSize: number;
|
| 37 |
+
generatedAt: string;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
export class SwarmOrchestrator {
|
| 41 |
+
private llm: LLMGateway;
|
| 42 |
+
private qualityScorer: QualityScorer;
|
| 43 |
+
|
| 44 |
+
constructor(llm: LLMGateway) {
|
| 45 |
+
this.llm = llm;
|
| 46 |
+
this.qualityScorer = new QualityScorer();
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
/**
|
| 50 |
+
* Run swarm: generate N parallel workflow designs, pick the best
|
| 51 |
+
* @param swarmSize Number of parallel designs (default: 3)
|
| 52 |
+
*/
|
| 53 |
+
async runSwarm(
|
| 54 |
+
userRequest: string,
|
| 55 |
+
intent: WorkflowIntent,
|
| 56 |
+
n8nBaseUrl: string,
|
| 57 |
+
n8nApiKey: string,
|
| 58 |
+
swarmSize = 3,
|
| 59 |
+
): Promise<SwarmResult> {
|
| 60 |
+
const strategies = this.getSwarmStrategies(intent).slice(0, swarmSize);
|
| 61 |
+
|
| 62 |
+
console.log(`[Swarm] Generating ${strategies.length} parallel workflow designs...`);
|
| 63 |
+
|
| 64 |
+
// βββ Generate all candidates in parallel βββββββββββββββββββββββββββββ
|
| 65 |
+
const candidateResults = await Promise.allSettled(
|
| 66 |
+
strategies.map((strategy, idx) =>
|
| 67 |
+
this.generateCandidate(
|
| 68 |
+
`candidate-${idx + 1}`,
|
| 69 |
+
userRequest,
|
| 70 |
+
intent,
|
| 71 |
+
strategy,
|
| 72 |
+
n8nBaseUrl,
|
| 73 |
+
n8nApiKey,
|
| 74 |
+
),
|
| 75 |
+
),
|
| 76 |
+
);
|
| 77 |
+
|
| 78 |
+
const candidates: SwarmCandidate[] = candidateResults
|
| 79 |
+
.filter((r): r is PromiseFulfilledResult<SwarmCandidate> => r.status === 'fulfilled')
|
| 80 |
+
.map((r) => r.value);
|
| 81 |
+
|
| 82 |
+
if (candidates.length === 0) {
|
| 83 |
+
throw new Error('Swarm: all candidates failed to generate. Check LLM connectivity.');
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
// βββ Score and rank all candidates βββββββββββββββββββββββββββββββββββ
|
| 87 |
+
const ranked = candidates
|
| 88 |
+
.sort((a, b) => b.qualityScore - a.qualityScore);
|
| 89 |
+
|
| 90 |
+
const winner = ranked[0]!;
|
| 91 |
+
winner.selectionReason = this.buildSelectionReason(winner, ranked);
|
| 92 |
+
|
| 93 |
+
console.log(
|
| 94 |
+
`[Swarm] Winner: ${winner.id} (score: ${winner.qualityScore}) from ${candidates.length} candidates`,
|
| 95 |
+
);
|
| 96 |
+
|
| 97 |
+
return {
|
| 98 |
+
winner,
|
| 99 |
+
allCandidates: ranked,
|
| 100 |
+
swarmSize: candidates.length,
|
| 101 |
+
generatedAt: new Date().toISOString(),
|
| 102 |
+
};
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
// βββ Generate a single candidate βββββββββββββββββββββββββββββββββββββββββ
|
| 106 |
+
private async generateCandidate(
|
| 107 |
+
id: string,
|
| 108 |
+
userRequest: string,
|
| 109 |
+
intent: WorkflowIntent,
|
| 110 |
+
strategy: { name: string; plannerHint: string; compilerHint: string },
|
| 111 |
+
n8nBaseUrl: string,
|
| 112 |
+
n8nApiKey: string,
|
| 113 |
+
): Promise<SwarmCandidate> {
|
| 114 |
+
// Each candidate uses the same LLM but with different strategy prompts
|
| 115 |
+
const planner = new WorkflowPlannerAgent(this.llm);
|
| 116 |
+
const graphEngine = new WorkflowGraphEngine(this.llm);
|
| 117 |
+
const compiler = new WorkflowCompiler(this.llm);
|
| 118 |
+
const validator = new ValidationEngine(this.llm, n8nBaseUrl, n8nApiKey);
|
| 119 |
+
|
| 120 |
+
// Planner with strategy-specific hint injected into the request
|
| 121 |
+
const augmentedRequest = `${userRequest}\n\n[DESIGN STRATEGY: ${strategy.plannerHint}]`;
|
| 122 |
+
const plan = await planner.plan(augmentedRequest, intent);
|
| 123 |
+
const graph = await graphEngine.buildGraph(userRequest, intent, plan);
|
| 124 |
+
const workflow = await compiler.compile(graph, intent);
|
| 125 |
+
|
| 126 |
+
// Run validation to score this candidate
|
| 127 |
+
const validationReport = await validator.validate(id, workflow, graph);
|
| 128 |
+
|
| 129 |
+
// Calculate quality score
|
| 130 |
+
const qualityScore = this.scoreCandidate(validationReport, workflow, graph);
|
| 131 |
+
|
| 132 |
+
return {
|
| 133 |
+
id,
|
| 134 |
+
plan,
|
| 135 |
+
graph,
|
| 136 |
+
workflow,
|
| 137 |
+
validationReport,
|
| 138 |
+
qualityScore,
|
| 139 |
+
planningStrategy: strategy.name,
|
| 140 |
+
};
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
// βββ Strategy definitions βββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 144 |
+
private getSwarmStrategies(intent: WorkflowIntent): Array<{
|
| 145 |
+
name: string;
|
| 146 |
+
plannerHint: string;
|
| 147 |
+
compilerHint: string;
|
| 148 |
+
}> {
|
| 149 |
+
const baseStrategies = [
|
| 150 |
+
{
|
| 151 |
+
name: 'Reliability-First',
|
| 152 |
+
plannerHint: 'Focus on maximum reliability: retry policies on all external nodes, error handling on every branch, monitoring nodes, and fallback paths for every critical operation.',
|
| 153 |
+
compilerHint: 'Maximize retry policies, add error output branches, include monitoring nodes',
|
| 154 |
+
},
|
| 155 |
+
{
|
| 156 |
+
name: 'Simplicity-First',
|
| 157 |
+
plannerHint: 'Focus on minimal node count with clean linear flow. Avoid over-engineering. Use the minimum necessary nodes to achieve the goal reliably.',
|
| 158 |
+
compilerHint: 'Keep node count minimal, prefer linear flow, avoid unnecessary branches',
|
| 159 |
+
},
|
| 160 |
+
{
|
| 161 |
+
name: 'Observability-First',
|
| 162 |
+
plannerHint: 'Focus on full observability: add logging at every stage, notification nodes for success and failure, detailed SET nodes for data tracking, and comprehensive audit trail.',
|
| 163 |
+
compilerHint: 'Add monitoring nodes, notification steps, detailed logging with SET nodes',
|
| 164 |
+
},
|
| 165 |
+
];
|
| 166 |
+
|
| 167 |
+
// Add AI-specific strategy if needed
|
| 168 |
+
if (intent.requiresAI) {
|
| 169 |
+
baseStrategies.push({
|
| 170 |
+
name: 'AI-Optimized',
|
| 171 |
+
plannerHint: 'Optimize for AI/LLM processing: proper context passing, memory nodes for conversation history, tool-enabled agent architecture, and robust AI output parsing.',
|
| 172 |
+
compilerHint: 'Use AI Agent with memory, proper system/user message separation, output parsing',
|
| 173 |
+
});
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
// Add domain-specific strategy
|
| 177 |
+
if (intent.domain === 'data_pipeline') {
|
| 178 |
+
baseStrategies.push({
|
| 179 |
+
name: 'Pipeline-Optimized',
|
| 180 |
+
plannerHint: 'Optimize for data pipeline: batch processing, deduplication, efficient array handling with SplitInBatches, and data validation at each stage.',
|
| 181 |
+
compilerHint: 'Use SplitInBatches for arrays, ItemLists for dedup, Filter for validation',
|
| 182 |
+
});
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
return baseStrategies;
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
// βββ Candidate scoring ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 189 |
+
private scoreCandidate(
|
| 190 |
+
validation: ValidationReport,
|
| 191 |
+
workflow: N8nWorkflow,
|
| 192 |
+
graph: WorkflowGraph,
|
| 193 |
+
): number {
|
| 194 |
+
let score = validation.overallScore;
|
| 195 |
+
|
| 196 |
+
// Bonus: has monitoring nodes
|
| 197 |
+
const hasMonitoring = graph.nodes.some((n) => n.layer === 'monitoring');
|
| 198 |
+
if (hasMonitoring) score = Math.min(100, score + 5);
|
| 199 |
+
|
| 200 |
+
// Bonus: all external nodes have retry
|
| 201 |
+
const externalNodes = workflow.nodes.filter((n) => !n.type.includes('trigger') && !n.type.includes('noOp'));
|
| 202 |
+
const allHaveRetry = externalNodes.every((n) => n.retryOnFail);
|
| 203 |
+
if (allHaveRetry && externalNodes.length > 0) score = Math.min(100, score + 5);
|
| 204 |
+
|
| 205 |
+
// Bonus: has expressions (not static)
|
| 206 |
+
const paramsStr = JSON.stringify(workflow.nodes.map((n) => n.parameters));
|
| 207 |
+
const expressionCount = (paramsStr.match(/\{\{/g) ?? []).length;
|
| 208 |
+
if (expressionCount >= 3) score = Math.min(100, score + 5);
|
| 209 |
+
|
| 210 |
+
// Penalty: unknown nodes present
|
| 211 |
+
const unknownRejected = ((graph.metadata as any)?.unknownNodesRejected as string[]) ?? [];
|
| 212 |
+
if (unknownRejected.length > 0) score = Math.max(0, score - 20);
|
| 213 |
+
|
| 214 |
+
// Penalty: active: true (should never happen but check)
|
| 215 |
+
if ((workflow.active as any) === true) score = 0;
|
| 216 |
+
|
| 217 |
+
return Math.round(score);
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
private buildSelectionReason(winner: SwarmCandidate, ranked: SwarmCandidate[]): string {
|
| 221 |
+
const others = ranked.slice(1);
|
| 222 |
+
const gaps = others.map((c) => `${c.id}(${c.qualityScore})`).join(', ');
|
| 223 |
+
return `Selected "${winner.planningStrategy}" strategy with score ${winner.qualityScore}/100. ` +
|
| 224 |
+
`Outperformed: ${gaps || 'no other candidates'}. ` +
|
| 225 |
+
`Validation: ${winner.validationReport.valid ? 'PASSED' : 'FAILED'}. ` +
|
| 226 |
+
`Issues: ${winner.validationReport.issues.length} errors, ${winner.validationReport.warnings.length} warnings.`;
|
| 227 |
+
}
|
| 228 |
+
}
|
apps/simulator/src/services/webhookAutoBind.ts
ADDED
|
@@ -0,0 +1,409 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Webhook Auto-Bind System β NEW
|
| 3 |
+
* Automatically detects trigger type from intent and assigns correct trigger node
|
| 4 |
+
* Auto-generates webhook paths, auto-configures trigger settings
|
| 5 |
+
* User NEVER manually defines triggers
|
| 6 |
+
*/
|
| 7 |
+
import type { WorkflowIntent, WorkflowGraph } from '../types/workflow';
|
| 8 |
+
import { getNodeDef } from '../knowledge/nodeRegistry';
|
| 9 |
+
|
| 10 |
+
export interface TriggerConfig {
|
| 11 |
+
nodeType: string;
|
| 12 |
+
displayName: string;
|
| 13 |
+
parameters: Record<string, unknown>;
|
| 14 |
+
credentials?: Record<string, { id: string; name: string }>;
|
| 15 |
+
webhookPath?: string;
|
| 16 |
+
notes: string;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
// βββ Trigger detection rules (ordered by specificity) βββββββββββββββββββββββββ
|
| 20 |
+
const TRIGGER_RULES: Array<{
|
| 21 |
+
keywords: string[];
|
| 22 |
+
integrations?: string[];
|
| 23 |
+
domain?: string[];
|
| 24 |
+
triggerType: string;
|
| 25 |
+
nodeType: string;
|
| 26 |
+
priority: number;
|
| 27 |
+
}> = [
|
| 28 |
+
// Telegram
|
| 29 |
+
{
|
| 30 |
+
keywords: ['telegram', 'bot', 'message', 'chat'],
|
| 31 |
+
integrations: ['telegram'],
|
| 32 |
+
triggerType: 'telegram',
|
| 33 |
+
nodeType: 'n8n-nodes-base.telegramTrigger',
|
| 34 |
+
priority: 100,
|
| 35 |
+
},
|
| 36 |
+
// Slack
|
| 37 |
+
{
|
| 38 |
+
keywords: ['slack', 'channel', 'workspace'],
|
| 39 |
+
integrations: ['slack'],
|
| 40 |
+
triggerType: 'slack',
|
| 41 |
+
nodeType: 'n8n-nodes-base.slackTrigger',
|
| 42 |
+
priority: 100,
|
| 43 |
+
},
|
| 44 |
+
// GitHub
|
| 45 |
+
{
|
| 46 |
+
keywords: ['github', 'push', 'pull request', 'issue', 'commit', 'repository'],
|
| 47 |
+
integrations: ['github'],
|
| 48 |
+
triggerType: 'github',
|
| 49 |
+
nodeType: 'n8n-nodes-base.githubTrigger',
|
| 50 |
+
priority: 100,
|
| 51 |
+
},
|
| 52 |
+
// Email (IMAP)
|
| 53 |
+
{
|
| 54 |
+
keywords: ['email', 'inbox', 'imap', 'mail received', 'new email', 'receive email'],
|
| 55 |
+
integrations: ['email', 'imap', 'gmail'],
|
| 56 |
+
triggerType: 'email',
|
| 57 |
+
nodeType: 'n8n-nodes-base.emailReadImap',
|
| 58 |
+
priority: 90,
|
| 59 |
+
},
|
| 60 |
+
// Notion
|
| 61 |
+
{
|
| 62 |
+
keywords: ['notion', 'database page', 'notion page'],
|
| 63 |
+
integrations: ['notion'],
|
| 64 |
+
triggerType: 'notion',
|
| 65 |
+
nodeType: 'n8n-nodes-base.notionTrigger',
|
| 66 |
+
priority: 90,
|
| 67 |
+
},
|
| 68 |
+
// Schedule / Cron
|
| 69 |
+
{
|
| 70 |
+
keywords: [
|
| 71 |
+
'every hour', 'every day', 'daily', 'hourly', 'weekly', 'monthly', 'schedule',
|
| 72 |
+
'cron', 'at midnight', 'every morning', 'every night', 'periodic', 'interval',
|
| 73 |
+
'every minute', 'every 5 minutes', 'nightly', 'scheduled',
|
| 74 |
+
],
|
| 75 |
+
triggerType: 'schedule',
|
| 76 |
+
nodeType: 'n8n-nodes-base.scheduleTrigger',
|
| 77 |
+
priority: 80,
|
| 78 |
+
},
|
| 79 |
+
// Webhook (general HTTP)
|
| 80 |
+
{
|
| 81 |
+
keywords: [
|
| 82 |
+
'webhook', 'api call', 'http request', 'post request', 'form submission',
|
| 83 |
+
'when called', 'endpoint', 'receive data', 'api endpoint', 'rest api',
|
| 84 |
+
'when a request', 'when triggered via',
|
| 85 |
+
],
|
| 86 |
+
triggerType: 'webhook',
|
| 87 |
+
nodeType: 'n8n-nodes-base.webhook',
|
| 88 |
+
priority: 70,
|
| 89 |
+
},
|
| 90 |
+
// Manual (fallback)
|
| 91 |
+
{
|
| 92 |
+
keywords: ['manually', 'manual trigger', 'on demand', 'test', 'demo'],
|
| 93 |
+
triggerType: 'manual',
|
| 94 |
+
nodeType: 'n8n-nodes-base.manualTrigger',
|
| 95 |
+
priority: 10,
|
| 96 |
+
},
|
| 97 |
+
];
|
| 98 |
+
|
| 99 |
+
export class WebhookAutoBindSystem {
|
| 100 |
+
|
| 101 |
+
/**
|
| 102 |
+
* Detect the appropriate trigger node type from intent and request text
|
| 103 |
+
*/
|
| 104 |
+
detectTrigger(
|
| 105 |
+
userRequest: string,
|
| 106 |
+
intent: WorkflowIntent,
|
| 107 |
+
): { nodeType: string; triggerType: string; confidence: 'high' | 'medium' | 'low' } {
|
| 108 |
+
const requestLower = userRequest.toLowerCase();
|
| 109 |
+
const integrationLower = intent.integrations.map((i) => i.toLowerCase());
|
| 110 |
+
|
| 111 |
+
let bestMatch: (typeof TRIGGER_RULES)[0] | undefined;
|
| 112 |
+
let bestScore = 0;
|
| 113 |
+
|
| 114 |
+
for (const rule of TRIGGER_RULES) {
|
| 115 |
+
let score = 0;
|
| 116 |
+
|
| 117 |
+
// Keyword matches in request
|
| 118 |
+
const keywordMatches = rule.keywords.filter((kw) => requestLower.includes(kw));
|
| 119 |
+
score += keywordMatches.length * 10;
|
| 120 |
+
|
| 121 |
+
// Integration matches
|
| 122 |
+
if (rule.integrations) {
|
| 123 |
+
const integrationMatches = rule.integrations.filter((i) =>
|
| 124 |
+
integrationLower.some((intent_i) => intent_i.includes(i) || i.includes(intent_i)),
|
| 125 |
+
);
|
| 126 |
+
score += integrationMatches.length * 20;
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
// Exact trigger type hint from intent
|
| 130 |
+
if (intent.triggerType && intent.triggerType === rule.triggerType) {
|
| 131 |
+
score += 50;
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
// Apply rule priority as tiebreaker
|
| 135 |
+
score += rule.priority * 0.1;
|
| 136 |
+
|
| 137 |
+
if (score > bestScore) {
|
| 138 |
+
bestScore = score;
|
| 139 |
+
bestMatch = rule;
|
| 140 |
+
}
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
// Default to webhook if nothing matched
|
| 144 |
+
const nodeType = bestMatch?.nodeType ?? 'n8n-nodes-base.webhook';
|
| 145 |
+
const triggerType = bestMatch?.triggerType ?? 'webhook';
|
| 146 |
+
const confidence: 'high' | 'medium' | 'low' =
|
| 147 |
+
bestScore >= 20 ? 'high' : bestScore >= 10 ? 'medium' : 'low';
|
| 148 |
+
|
| 149 |
+
return { nodeType, triggerType, confidence };
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
/**
|
| 153 |
+
* Generate complete trigger node configuration
|
| 154 |
+
*/
|
| 155 |
+
buildTriggerConfig(
|
| 156 |
+
nodeType: string,
|
| 157 |
+
userRequest: string,
|
| 158 |
+
intent: WorkflowIntent,
|
| 159 |
+
workflowName: string,
|
| 160 |
+
): TriggerConfig {
|
| 161 |
+
switch (nodeType) {
|
| 162 |
+
case 'n8n-nodes-base.webhook':
|
| 163 |
+
return this.buildWebhookConfig(workflowName, intent);
|
| 164 |
+
|
| 165 |
+
case 'n8n-nodes-base.telegramTrigger':
|
| 166 |
+
return this.buildTelegramTriggerConfig();
|
| 167 |
+
|
| 168 |
+
case 'n8n-nodes-base.scheduleTrigger':
|
| 169 |
+
return this.buildScheduleConfig(userRequest);
|
| 170 |
+
|
| 171 |
+
case 'n8n-nodes-base.emailReadImap':
|
| 172 |
+
return this.buildEmailTriggerConfig();
|
| 173 |
+
|
| 174 |
+
case 'n8n-nodes-base.slackTrigger':
|
| 175 |
+
return this.buildSlackTriggerConfig();
|
| 176 |
+
|
| 177 |
+
case 'n8n-nodes-base.githubTrigger':
|
| 178 |
+
return this.buildGitHubTriggerConfig();
|
| 179 |
+
|
| 180 |
+
case 'n8n-nodes-base.notionTrigger':
|
| 181 |
+
return this.buildNotionTriggerConfig();
|
| 182 |
+
|
| 183 |
+
case 'n8n-nodes-base.manualTrigger':
|
| 184 |
+
return this.buildManualTriggerConfig();
|
| 185 |
+
|
| 186 |
+
default:
|
| 187 |
+
return this.buildWebhookConfig(workflowName, intent);
|
| 188 |
+
}
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
/**
|
| 192 |
+
* Inject auto-configured trigger into a WorkflowGraph as the first node
|
| 193 |
+
*/
|
| 194 |
+
injectTriggerIntoGraph(
|
| 195 |
+
graph: WorkflowGraph,
|
| 196 |
+
triggerConfig: TriggerConfig,
|
| 197 |
+
triggerNodeType: string,
|
| 198 |
+
): WorkflowGraph {
|
| 199 |
+
const def = getNodeDef(triggerNodeType);
|
| 200 |
+
const triggerId = 'trigger-auto-' + Date.now();
|
| 201 |
+
|
| 202 |
+
// Check if graph already has a trigger node from the detected type
|
| 203 |
+
const existingTrigger = graph.nodes.find((n) => n.layer === 'trigger');
|
| 204 |
+
|
| 205 |
+
if (existingTrigger) {
|
| 206 |
+
// Update existing trigger with proper config
|
| 207 |
+
const updatedNodes = graph.nodes.map((node) => {
|
| 208 |
+
if (node.layer !== 'trigger') return node;
|
| 209 |
+
return {
|
| 210 |
+
...node,
|
| 211 |
+
n8nNodeType: triggerNodeType,
|
| 212 |
+
label: triggerConfig.displayName,
|
| 213 |
+
autoConfigured: true,
|
| 214 |
+
webhookPath: triggerConfig.webhookPath,
|
| 215 |
+
};
|
| 216 |
+
});
|
| 217 |
+
return { ...graph, nodes: updatedNodes };
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
// Insert new trigger at front
|
| 221 |
+
const newTriggerNode = {
|
| 222 |
+
id: triggerId,
|
| 223 |
+
label: triggerConfig.displayName,
|
| 224 |
+
n8nNodeType: triggerNodeType,
|
| 225 |
+
layer: 'trigger' as const,
|
| 226 |
+
description: triggerConfig.notes,
|
| 227 |
+
isCritical: true,
|
| 228 |
+
autoConfigured: true,
|
| 229 |
+
webhookPath: triggerConfig.webhookPath,
|
| 230 |
+
position: { x: 0, y: 300 },
|
| 231 |
+
parameters: triggerConfig.parameters,
|
| 232 |
+
retryPolicy: undefined,
|
| 233 |
+
dataContract: {
|
| 234 |
+
inputs: [],
|
| 235 |
+
outputs: ['body', 'headers', 'query', 'params'],
|
| 236 |
+
},
|
| 237 |
+
};
|
| 238 |
+
|
| 239 |
+
// Shift existing nodes to the right
|
| 240 |
+
const shiftedNodes = graph.nodes.map((node) => ({
|
| 241 |
+
...node,
|
| 242 |
+
position: { x: (node.position?.x ?? 0) + 220, y: node.position?.y ?? 300 },
|
| 243 |
+
}));
|
| 244 |
+
|
| 245 |
+
// Connect trigger to first non-trigger node
|
| 246 |
+
const firstNode = shiftedNodes[0];
|
| 247 |
+
const newEdge = firstNode ? {
|
| 248 |
+
id: `edge-trigger-to-${firstNode.id}`,
|
| 249 |
+
sourceNodeId: triggerId,
|
| 250 |
+
targetNodeId: firstNode.id,
|
| 251 |
+
outputIndex: 0,
|
| 252 |
+
inputIndex: 0,
|
| 253 |
+
label: 'on receive',
|
| 254 |
+
} : undefined;
|
| 255 |
+
|
| 256 |
+
return {
|
| 257 |
+
...graph,
|
| 258 |
+
nodes: [newTriggerNode, ...shiftedNodes],
|
| 259 |
+
edges: newEdge ? [newEdge, ...graph.edges] : graph.edges,
|
| 260 |
+
};
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
// βββ Trigger config builders ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 264 |
+
|
| 265 |
+
private buildWebhookConfig(workflowName: string, intent: WorkflowIntent): TriggerConfig {
|
| 266 |
+
const slug = workflowName
|
| 267 |
+
.toLowerCase()
|
| 268 |
+
.replace(/[^a-z0-9]+/g, '-')
|
| 269 |
+
.replace(/^-+|-+$/g, '')
|
| 270 |
+
.slice(0, 40);
|
| 271 |
+
const path = `wfo/${slug}`;
|
| 272 |
+
|
| 273 |
+
return {
|
| 274 |
+
nodeType: 'n8n-nodes-base.webhook',
|
| 275 |
+
displayName: 'Webhook Trigger',
|
| 276 |
+
parameters: {
|
| 277 |
+
httpMethod: intent.syncVsAsync === 'sync' ? 'POST' : 'POST',
|
| 278 |
+
path,
|
| 279 |
+
responseMode: intent.syncVsAsync === 'sync' ? 'lastNode' : 'onReceived',
|
| 280 |
+
responseData: 'allEntries',
|
| 281 |
+
options: {},
|
| 282 |
+
},
|
| 283 |
+
webhookPath: path,
|
| 284 |
+
notes: `Auto-configured webhook trigger. Path: /webhook/${path}. Method: POST. Response: ${intent.syncVsAsync === 'sync' ? 'synchronous (after processing)' : 'immediate acknowledgement'}.`,
|
| 285 |
+
};
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
private buildTelegramTriggerConfig(): TriggerConfig {
|
| 289 |
+
return {
|
| 290 |
+
nodeType: 'n8n-nodes-base.telegramTrigger',
|
| 291 |
+
displayName: 'Telegram Trigger',
|
| 292 |
+
parameters: {
|
| 293 |
+
updates: ['message', 'callback_query'],
|
| 294 |
+
},
|
| 295 |
+
credentials: {
|
| 296 |
+
telegramApi: { id: '', name: 'Telegram Bot API' },
|
| 297 |
+
},
|
| 298 |
+
notes: 'Auto-configured Telegram trigger. Listens for messages and callback queries. Requires telegramApi credential.',
|
| 299 |
+
};
|
| 300 |
+
}
|
| 301 |
+
|
| 302 |
+
private buildScheduleConfig(userRequest: string): TriggerConfig {
|
| 303 |
+
// Detect schedule from request text
|
| 304 |
+
const req = userRequest.toLowerCase();
|
| 305 |
+
let interval: Record<string, unknown>[] = [{ field: 'hours', hoursInterval: 1 }];
|
| 306 |
+
|
| 307 |
+
if (req.includes('every minute')) interval = [{ field: 'minutes', minutesInterval: 1 }];
|
| 308 |
+
else if (req.includes('every 5 minute')) interval = [{ field: 'minutes', minutesInterval: 5 }];
|
| 309 |
+
else if (req.includes('every 15 minute')) interval = [{ field: 'minutes', minutesInterval: 15 }];
|
| 310 |
+
else if (req.includes('every 30 minute')) interval = [{ field: 'minutes', minutesInterval: 30 }];
|
| 311 |
+
else if (req.includes('every hour') || req.includes('hourly')) interval = [{ field: 'hours', hoursInterval: 1 }];
|
| 312 |
+
else if (req.includes('every 6 hour')) interval = [{ field: 'hours', hoursInterval: 6 }];
|
| 313 |
+
else if (req.includes('every 12 hour')) interval = [{ field: 'hours', hoursInterval: 12 }];
|
| 314 |
+
else if (req.includes('daily') || req.includes('every day') || req.includes('nightly') || req.includes('midnight')) {
|
| 315 |
+
interval = [{ field: 'cronExpression', expression: '0 0 * * *' }];
|
| 316 |
+
}
|
| 317 |
+
else if (req.includes('weekly') || req.includes('every week')) {
|
| 318 |
+
interval = [{ field: 'cronExpression', expression: '0 9 * * 1' }]; // Monday 9am
|
| 319 |
+
}
|
| 320 |
+
else if (req.includes('monthly') || req.includes('every month')) {
|
| 321 |
+
interval = [{ field: 'cronExpression', expression: '0 9 1 * *' }]; // 1st of month 9am
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
return {
|
| 325 |
+
nodeType: 'n8n-nodes-base.scheduleTrigger',
|
| 326 |
+
displayName: 'Schedule Trigger',
|
| 327 |
+
parameters: {
|
| 328 |
+
rule: { interval },
|
| 329 |
+
},
|
| 330 |
+
notes: `Auto-configured schedule trigger. Interval detected from request: ${JSON.stringify(interval)}.`,
|
| 331 |
+
};
|
| 332 |
+
}
|
| 333 |
+
|
| 334 |
+
private buildEmailTriggerConfig(): TriggerConfig {
|
| 335 |
+
return {
|
| 336 |
+
nodeType: 'n8n-nodes-base.emailReadImap',
|
| 337 |
+
displayName: 'Email Trigger (IMAP)',
|
| 338 |
+
parameters: {
|
| 339 |
+
mailbox: 'INBOX',
|
| 340 |
+
action: 'read',
|
| 341 |
+
downloadAttachments: false,
|
| 342 |
+
format: 'simple',
|
| 343 |
+
options: {},
|
| 344 |
+
},
|
| 345 |
+
credentials: {
|
| 346 |
+
imap: { id: '', name: 'IMAP Account' },
|
| 347 |
+
},
|
| 348 |
+
notes: 'Auto-configured IMAP email trigger. Monitors INBOX for new emails. Requires IMAP credential.',
|
| 349 |
+
};
|
| 350 |
+
}
|
| 351 |
+
|
| 352 |
+
private buildSlackTriggerConfig(): TriggerConfig {
|
| 353 |
+
return {
|
| 354 |
+
nodeType: 'n8n-nodes-base.slackTrigger',
|
| 355 |
+
displayName: 'Slack Trigger',
|
| 356 |
+
parameters: {
|
| 357 |
+
trigger: 'any_message',
|
| 358 |
+
},
|
| 359 |
+
credentials: {
|
| 360 |
+
slackOAuth2Api: { id: '', name: 'Slack OAuth2' },
|
| 361 |
+
},
|
| 362 |
+
notes: 'Auto-configured Slack trigger. Listens for messages in configured channels. Requires Slack OAuth2 credential.',
|
| 363 |
+
};
|
| 364 |
+
}
|
| 365 |
+
|
| 366 |
+
private buildGitHubTriggerConfig(): TriggerConfig {
|
| 367 |
+
return {
|
| 368 |
+
nodeType: 'n8n-nodes-base.githubTrigger',
|
| 369 |
+
displayName: 'GitHub Trigger',
|
| 370 |
+
parameters: {
|
| 371 |
+
owner: '',
|
| 372 |
+
repository: '',
|
| 373 |
+
events: ['push'],
|
| 374 |
+
},
|
| 375 |
+
credentials: {
|
| 376 |
+
githubApi: { id: '', name: 'GitHub API' },
|
| 377 |
+
},
|
| 378 |
+
notes: 'Auto-configured GitHub trigger. Listens for push events. Set owner and repository in parameters.',
|
| 379 |
+
};
|
| 380 |
+
}
|
| 381 |
+
|
| 382 |
+
private buildNotionTriggerConfig(): TriggerConfig {
|
| 383 |
+
return {
|
| 384 |
+
nodeType: 'n8n-nodes-base.notionTrigger',
|
| 385 |
+
displayName: 'Notion Trigger',
|
| 386 |
+
parameters: {
|
| 387 |
+
databaseId: '',
|
| 388 |
+
event: 'page_added',
|
| 389 |
+
simple: true,
|
| 390 |
+
},
|
| 391 |
+
credentials: {
|
| 392 |
+
notionApi: { id: '', name: 'Notion API' },
|
| 393 |
+
},
|
| 394 |
+
notes: 'Auto-configured Notion trigger. Monitors a database for new pages. Set databaseId in parameters.',
|
| 395 |
+
};
|
| 396 |
+
}
|
| 397 |
+
|
| 398 |
+
private buildManualTriggerConfig(): TriggerConfig {
|
| 399 |
+
return {
|
| 400 |
+
nodeType: 'n8n-nodes-base.manualTrigger',
|
| 401 |
+
displayName: 'Manual Trigger',
|
| 402 |
+
parameters: {},
|
| 403 |
+
notes: 'Manual trigger β start this workflow by clicking "Execute Workflow" in n8n.',
|
| 404 |
+
};
|
| 405 |
+
}
|
| 406 |
+
}
|
| 407 |
+
|
| 408 |
+
// βββ Singleton export βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 409 |
+
export const webhookAutoBind = new WebhookAutoBindSystem();
|
apps/simulator/src/types/workflow.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Shared Types for Simulator Service
|
| 3 |
+
* Re-exports from worker types for consistency
|
| 4 |
+
*/
|
| 5 |
+
export type {
|
| 6 |
+
WorkflowIntent,
|
| 7 |
+
WorkflowArchitecturePlan,
|
| 8 |
+
WorkflowGraph,
|
| 9 |
+
GraphNode,
|
| 10 |
+
GraphEdge,
|
| 11 |
+
WorkflowMetadata,
|
| 12 |
+
WorkflowLayer,
|
| 13 |
+
N8nWorkflow,
|
| 14 |
+
N8nNode,
|
| 15 |
+
N8nConnections,
|
| 16 |
+
ValidationReport,
|
| 17 |
+
ValidationSection,
|
| 18 |
+
ValidationIssue,
|
| 19 |
+
SimulationReport,
|
| 20 |
+
ExecutionTraceStep,
|
| 21 |
+
FailurePoint,
|
| 22 |
+
ChaosTestResult,
|
| 23 |
+
CredentialAnalysis,
|
| 24 |
+
RequiredCredential,
|
| 25 |
+
AvailableCredential,
|
| 26 |
+
MissingCredential,
|
| 27 |
+
DeploymentReadiness,
|
| 28 |
+
WorkflowQualityScore,
|
| 29 |
+
GenerateWorkflowResponse,
|
| 30 |
+
RetryPolicy,
|
| 31 |
+
DataContract,
|
| 32 |
+
RiskLevel,
|
| 33 |
+
Risk,
|
| 34 |
+
RiskAnalysis,
|
| 35 |
+
DependencyAnalysis,
|
| 36 |
+
SubworkflowPlan,
|
| 37 |
+
TradeoffExplanation,
|
| 38 |
+
QualityTargets,
|
| 39 |
+
WorkflowDomain,
|
| 40 |
+
WorkflowLifecycleState,
|
| 41 |
+
ExecutionPlanStep,
|
| 42 |
+
WorkflowJob,
|
| 43 |
+
DeploymentResult,
|
| 44 |
+
ActivationResult,
|
| 45 |
+
ApprovalRequest,
|
| 46 |
+
AuditEvent,
|
| 47 |
+
SwarmResultSummary,
|
| 48 |
+
TriggerDetectionResult,
|
| 49 |
+
HealingResultSummary,
|
| 50 |
+
} from '@wfo/apps/worker/src/types/workflow';
|
apps/simulator/tsconfig.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
// "extends": "../../tsconfig.json",
|
| 3 |
+
"compilerOptions": {
|
| 4 |
+
"target": "ES2022",
|
| 5 |
+
"module": "CommonJS",
|
| 6 |
+
"moduleResolution": "node",
|
| 7 |
+
"lib": ["ES2022"],
|
| 8 |
+
"outDir": "./dist",
|
| 9 |
+
"rootDir": "../../",
|
| 10 |
+
|
| 11 |
+
"esModuleInterop": true,
|
| 12 |
+
"allowSyntheticDefaultImports": true,
|
| 13 |
+
"resolveJsonModule": true,
|
| 14 |
+
"paths": {
|
| 15 |
+
"@wfo/apps/worker/*": ["../../apps/worker/*"],
|
| 16 |
+
"@wfo/core/*": ["../../core/*"],
|
| 17 |
+
"@wfo/integrations/*": ["../../integrations/*"],
|
| 18 |
+
"@wfo/validation/*": ["../../validation/*"],
|
| 19 |
+
"@wfo/observability/*": ["../../observability/*"],
|
| 20 |
+
"@wfo/knowledge/*": ["../../knowledge/*"],
|
| 21 |
+
"@wfo/config/*": ["../../config/*"]
|
| 22 |
+
}
|
| 23 |
+
},
|
| 24 |
+
"include": ["src/**/*", "../../core/**/*", "../../integrations/**/*", "../../validation/**/*", "../../observability/**/*", "../../knowledge/**/*", "../../config/**/*", "../../apps/worker/src/types/**/*"],
|
| 25 |
+
"exclude": ["node_modules", "dist"]
|
| 26 |
+
}
|
apps/worker/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "@wfo/worker",
|
| 3 |
+
"version": "1.0.0",
|
| 4 |
+
"description": "Cloudflare Worker - API Gateway & Orchestrator (thin layer)",
|
| 5 |
+
"private": true,
|
| 6 |
+
"scripts": {
|
| 7 |
+
"dev": "wrangler dev",
|
| 8 |
+
"build": "wrangler deploy --dry-run",
|
| 9 |
+
"deploy": "wrangler deploy",
|
| 10 |
+
"typecheck": "tsc --noEmit"
|
| 11 |
+
},
|
| 12 |
+
"dependencies": {
|
| 13 |
+
"hono": "^4.0.0",
|
| 14 |
+
"zod": "^3.22.0"
|
| 15 |
+
},
|
| 16 |
+
"devDependencies": {
|
| 17 |
+
"@cloudflare/workers-types": "^4.20240208.0",
|
| 18 |
+
"wrangler": "^3.28.0",
|
| 19 |
+
"typescript": "^5.3.0"
|
| 20 |
+
}
|
| 21 |
+
}
|
apps/worker/src/index.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Cloudflare Worker - Main Entry Point
|
| 3 |
+
* Architecture: Thin API Gateway + Orchestrator
|
| 4 |
+
* Heavy logic is offloaded to external compute services (simulator, planner, compiler, validator)
|
| 5 |
+
*
|
| 6 |
+
* CF Workers Free Plan constraints:
|
| 7 |
+
* - 10ms CPU time limit per request
|
| 8 |
+
* - 128MB memory limit
|
| 9 |
+
* - No Cloudflare Queues
|
| 10 |
+
* - Limited KV operations
|
| 11 |
+
*/
|
| 12 |
+
|
| 13 |
+
import { Hono } from 'hono';
|
| 14 |
+
import { cors } from 'hono/cors';
|
| 15 |
+
import { logger } from 'hono/logger';
|
| 16 |
+
import { generateWorkflowRoute } from './routes/generate';
|
| 17 |
+
import { validateWorkflowRoute } from './routes/validate';
|
| 18 |
+
import { simulateWorkflowRoute } from './routes/simulate';
|
| 19 |
+
import { deployWorkflowRoute } from './routes/deploy';
|
| 20 |
+
import { approvalRoute } from './routes/approval';
|
| 21 |
+
import { activateWorkflowRoute } from './routes/activate';
|
| 22 |
+
import { reportsRoute } from './routes/reports';
|
| 23 |
+
import { healthRoute } from './routes/health';
|
| 24 |
+
import { authMiddleware } from './middleware/auth';
|
| 25 |
+
import { rateLimitMiddleware } from './middleware/rateLimit';
|
| 26 |
+
import { errorHandler } from './middleware/errorHandler';
|
| 27 |
+
import type { Env } from './types/env';
|
| 28 |
+
|
| 29 |
+
const app = new Hono<{ Bindings: Env }>();
|
| 30 |
+
|
| 31 |
+
// βββ Global Middleware ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 32 |
+
app.use('*', cors({
|
| 33 |
+
origin: ['https://your-frontend.pages.dev', 'http://localhost:3000'],
|
| 34 |
+
allowHeaders: ['Content-Type', 'Authorization', 'X-API-Key'],
|
| 35 |
+
allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
| 36 |
+
maxAge: 86400,
|
| 37 |
+
}));
|
| 38 |
+
|
| 39 |
+
app.use('*', logger());
|
| 40 |
+
app.use('*', errorHandler);
|
| 41 |
+
|
| 42 |
+
// βββ Public Routes ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 43 |
+
app.route('/health', healthRoute);
|
| 44 |
+
|
| 45 |
+
// βββ Protected Routes (require API auth) βββββββββββββββββββββββββββββββββββββ
|
| 46 |
+
app.use('/api/*', authMiddleware);
|
| 47 |
+
app.use('/api/*', rateLimitMiddleware);
|
| 48 |
+
|
| 49 |
+
app.route('/api/workflows', generateWorkflowRoute);
|
| 50 |
+
app.route('/api/workflows', validateWorkflowRoute);
|
| 51 |
+
app.route('/api/workflows', simulateWorkflowRoute);
|
| 52 |
+
app.route('/api/workflows', deployWorkflowRoute);
|
| 53 |
+
app.route('/api/workflows', approvalRoute);
|
| 54 |
+
app.route('/api/workflows', activateWorkflowRoute);
|
| 55 |
+
app.route('/api/reports', reportsRoute);
|
| 56 |
+
|
| 57 |
+
// βββ 404 Handler βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 58 |
+
app.notFound((c) => {
|
| 59 |
+
return c.json({
|
| 60 |
+
success: false,
|
| 61 |
+
error: 'Route not found',
|
| 62 |
+
available_routes: [
|
| 63 |
+
'POST /api/workflows/generate',
|
| 64 |
+
'POST /api/workflows/validate',
|
| 65 |
+
'POST /api/workflows/simulate',
|
| 66 |
+
'POST /api/workflows/deploy',
|
| 67 |
+
'POST /api/workflows/approve',
|
| 68 |
+
'POST /api/workflows/activate',
|
| 69 |
+
'GET /api/reports/workflow/:id',
|
| 70 |
+
'GET /api/reports/validation/:id',
|
| 71 |
+
'GET /api/reports/simulation/:id',
|
| 72 |
+
'GET /health',
|
| 73 |
+
],
|
| 74 |
+
}, 404);
|
| 75 |
+
});
|
| 76 |
+
|
| 77 |
+
export default app;
|
apps/worker/src/middleware/auth.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Auth Middleware
|
| 3 |
+
* Validates X-API-Key header for protected routes
|
| 4 |
+
* Lightweight to stay within 10ms CF Workers CPU budget
|
| 5 |
+
*/
|
| 6 |
+
import type { MiddlewareHandler } from 'hono';
|
| 7 |
+
import type { Env } from '../types/env';
|
| 8 |
+
|
| 9 |
+
export const authMiddleware: MiddlewareHandler<{ Bindings: Env }> = async (c, next) => {
|
| 10 |
+
const apiKey = c.req.header('X-API-Key') ?? c.req.header('Authorization')?.replace('Bearer ', '');
|
| 11 |
+
|
| 12 |
+
if (!apiKey) {
|
| 13 |
+
return c.json({ success: false, error: 'Missing API key. Provide X-API-Key header.' }, 401);
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
// Constant-time comparison to prevent timing attacks
|
| 17 |
+
const expected = c.env.INTERNAL_API_SECRET;
|
| 18 |
+
if (!safeCompare(apiKey, expected)) {
|
| 19 |
+
return c.json({ success: false, error: 'Invalid API key.' }, 403);
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
await next();
|
| 23 |
+
};
|
| 24 |
+
|
| 25 |
+
function safeCompare(a: string, b: string): boolean {
|
| 26 |
+
if (a.length !== b.length) return false;
|
| 27 |
+
let result = 0;
|
| 28 |
+
for (let i = 0; i < a.length; i++) {
|
| 29 |
+
result |= (a.charCodeAt(i) ^ b.charCodeAt(i));
|
| 30 |
+
}
|
| 31 |
+
return result === 0;
|
| 32 |
+
}
|
apps/worker/src/middleware/errorHandler.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Error Handler Middleware
|
| 3 |
+
* Global error boundary for unhandled exceptions
|
| 4 |
+
*/
|
| 5 |
+
import type { MiddlewareHandler } from 'hono';
|
| 6 |
+
import type { Env } from '../types/env';
|
| 7 |
+
|
| 8 |
+
export const errorHandler: MiddlewareHandler<{ Bindings: Env }> = async (c, next) => {
|
| 9 |
+
try {
|
| 10 |
+
await next();
|
| 11 |
+
} catch (err) {
|
| 12 |
+
const message = err instanceof Error ? err.message : 'Internal server error';
|
| 13 |
+
console.error('[WorkerError]', { path: c.req.path, error: message });
|
| 14 |
+
return c.json({ success: false, error: message }, 500);
|
| 15 |
+
}
|
| 16 |
+
};
|
apps/worker/src/middleware/rateLimit.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Rate Limit Middleware
|
| 3 |
+
* Uses KV store for sliding window rate limiting
|
| 4 |
+
* Lightweight - primarily I/O bound (KV read/write), minimal CPU
|
| 5 |
+
*/
|
| 6 |
+
import type { MiddlewareHandler } from 'hono';
|
| 7 |
+
import type { Env } from '../types/env';
|
| 8 |
+
|
| 9 |
+
const RATE_LIMIT_WINDOW_MS = 60_000; // 1 minute
|
| 10 |
+
const RATE_LIMIT_MAX_REQUESTS = 20; // 20 requests per minute per IP
|
| 11 |
+
|
| 12 |
+
export const rateLimitMiddleware: MiddlewareHandler<{ Bindings: Env }> = async (c, next) => {
|
| 13 |
+
const ip = c.req.header('CF-Connecting-IP') ?? 'unknown';
|
| 14 |
+
const key = `ratelimit:${ip}`;
|
| 15 |
+
|
| 16 |
+
const raw = await c.env.WFO_CACHE.get(key, 'text');
|
| 17 |
+
const now = Date.now();
|
| 18 |
+
|
| 19 |
+
let count = 1;
|
| 20 |
+
if (raw) {
|
| 21 |
+
const entry = JSON.parse(raw) as { count: number; windowStart: number };
|
| 22 |
+
if (now - entry.windowStart < RATE_LIMIT_WINDOW_MS) {
|
| 23 |
+
count = entry.count + 1;
|
| 24 |
+
if (count > RATE_LIMIT_MAX_REQUESTS) {
|
| 25 |
+
return c.json({
|
| 26 |
+
success: false,
|
| 27 |
+
error: 'Rate limit exceeded. Max 20 requests/minute.',
|
| 28 |
+
retryAfter: Math.ceil((entry.windowStart + RATE_LIMIT_WINDOW_MS - now) / 1000),
|
| 29 |
+
}, 429);
|
| 30 |
+
}
|
| 31 |
+
await c.env.WFO_CACHE.put(key, JSON.stringify({ count, windowStart: entry.windowStart }), {
|
| 32 |
+
expirationTtl: 120,
|
| 33 |
+
});
|
| 34 |
+
} else {
|
| 35 |
+
// New window
|
| 36 |
+
await c.env.WFO_CACHE.put(key, JSON.stringify({ count: 1, windowStart: now }), {
|
| 37 |
+
expirationTtl: 120,
|
| 38 |
+
});
|
| 39 |
+
}
|
| 40 |
+
} else {
|
| 41 |
+
await c.env.WFO_CACHE.put(key, JSON.stringify({ count: 1, windowStart: now }), {
|
| 42 |
+
expirationTtl: 120,
|
| 43 |
+
});
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
c.res.headers.set('X-RateLimit-Limit', String(RATE_LIMIT_MAX_REQUESTS));
|
| 47 |
+
c.res.headers.set('X-RateLimit-Remaining', String(Math.max(0, RATE_LIMIT_MAX_REQUESTS - count)));
|
| 48 |
+
|
| 49 |
+
await next();
|
| 50 |
+
};
|
apps/worker/src/routes/activate.ts
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* POST /api/workflows/activate
|
| 3 |
+
* Activation Engine - final safety gate
|
| 4 |
+
* Only activates workflows that have been:
|
| 5 |
+
* 1. Validated β
|
| 6 |
+
* 2. Simulated β
|
| 7 |
+
* 3. Deployed β
|
| 8 |
+
* 4. Human Approved β
|
| 9 |
+
* 5. Final credential check β
|
| 10 |
+
*
|
| 11 |
+
* NEVER auto-activate. NEVER skip approval gate.
|
| 12 |
+
*/
|
| 13 |
+
import { Hono } from 'hono';
|
| 14 |
+
import { z } from 'zod';
|
| 15 |
+
import type { Env } from '../types/env';
|
| 16 |
+
import type { WorkflowJob, ActivationResult } from '../types/workflow';
|
| 17 |
+
import { createAuditEvent } from '../utils/audit';
|
| 18 |
+
|
| 19 |
+
export const activateWorkflowRoute = new Hono<{ Bindings: Env }>();
|
| 20 |
+
|
| 21 |
+
const ActivateRequestSchema = z.object({
|
| 22 |
+
jobId: z.string().min(1),
|
| 23 |
+
});
|
| 24 |
+
|
| 25 |
+
activateWorkflowRoute.post('/activate', async (c) => {
|
| 26 |
+
const body = await c.req.json().catch(() => null);
|
| 27 |
+
if (!body) return c.json({ success: false, error: 'Invalid JSON body' }, 400);
|
| 28 |
+
|
| 29 |
+
const parsed = ActivateRequestSchema.safeParse(body);
|
| 30 |
+
if (!parsed.success) {
|
| 31 |
+
return c.json({ success: false, error: 'Validation failed', details: parsed.error.flatten() }, 400);
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
const { jobId } = parsed.data;
|
| 35 |
+
const raw = await c.env.WFO_CACHE.get(`job:${jobId}`, 'text');
|
| 36 |
+
if (!raw) return c.json({ success: false, error: `Job ${jobId} not found` }, 404);
|
| 37 |
+
|
| 38 |
+
const job = JSON.parse(raw) as WorkflowJob;
|
| 39 |
+
|
| 40 |
+
// βββ Final Safety Gates ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 41 |
+
if (job.state !== 'approved') {
|
| 42 |
+
const reason = job.state === 'rejected'
|
| 43 |
+
? 'Workflow was rejected by reviewer.'
|
| 44 |
+
: job.state === 'awaiting_approval'
|
| 45 |
+
? 'Workflow is awaiting human approval. Cannot activate yet.'
|
| 46 |
+
: `Workflow is not approved. Current state: ${job.state}`;
|
| 47 |
+
|
| 48 |
+
return c.json({ success: false, error: reason, currentState: job.state }, 409);
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
if (!job.deploymentResult?.n8nWorkflowId) {
|
| 52 |
+
return c.json({ success: false, error: 'Workflow not deployed to n8n. Run /deploy first.' }, 409);
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
if (job.approvalRequest?.status !== 'approved') {
|
| 56 |
+
return c.json({
|
| 57 |
+
success: false,
|
| 58 |
+
error: 'Human approval required before activation. No exceptions.',
|
| 59 |
+
}, 403);
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
// βββ Final Credential Check ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 63 |
+
const credResp = await fetch(`${c.env.N8N_BASE_URL}/api/v1/credentials`, {
|
| 64 |
+
headers: { 'X-N8N-API-KEY': c.env.N8N_API_KEY },
|
| 65 |
+
signal: AbortSignal.timeout(8000),
|
| 66 |
+
}).catch(() => null);
|
| 67 |
+
|
| 68 |
+
const credCheckPassed = credResp?.ok ?? false;
|
| 69 |
+
|
| 70 |
+
// βββ Final Validation Check via simulator ββββββββββββββββββββββββββββββββ
|
| 71 |
+
const validationResp = await fetch(`${c.env.SIMULATOR_URL}/process/validate`, {
|
| 72 |
+
method: 'POST',
|
| 73 |
+
headers: {
|
| 74 |
+
'Content-Type': 'application/json',
|
| 75 |
+
'X-Internal-Secret': c.env.INTERNAL_API_SECRET,
|
| 76 |
+
},
|
| 77 |
+
body: JSON.stringify({
|
| 78 |
+
jobId,
|
| 79 |
+
workflow: job.compiledWorkflow,
|
| 80 |
+
graph: job.graph,
|
| 81 |
+
final: true,
|
| 82 |
+
}),
|
| 83 |
+
signal: AbortSignal.timeout(10000),
|
| 84 |
+
}).catch(() => null);
|
| 85 |
+
|
| 86 |
+
const finalValidationPassed = validationResp?.ok ?? false;
|
| 87 |
+
|
| 88 |
+
if (!finalValidationPassed) {
|
| 89 |
+
const activationResult: ActivationResult = {
|
| 90 |
+
jobId,
|
| 91 |
+
n8nWorkflowId: job.deploymentResult.n8nWorkflowId,
|
| 92 |
+
activationStatus: 'failed',
|
| 93 |
+
finalValidationPassed: false,
|
| 94 |
+
finalCredentialCheckPassed: credCheckPassed,
|
| 95 |
+
activatedAt: new Date().toISOString(),
|
| 96 |
+
diagnostics: 'Final pre-activation validation failed. Workflow not activated.',
|
| 97 |
+
};
|
| 98 |
+
|
| 99 |
+
await updateJob(c.env.WFO_CACHE, jobId, job, { activationResult, state: 'deployed' });
|
| 100 |
+
return c.json({ success: false, error: 'Pre-activation validation failed.', activationResult, jobId }, 500);
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
// βββ Activate via n8n API βββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 104 |
+
const activateResp = await fetch(
|
| 105 |
+
`${c.env.N8N_BASE_URL}/api/v1/workflows/${job.deploymentResult.n8nWorkflowId}/activate`,
|
| 106 |
+
{
|
| 107 |
+
method: 'POST',
|
| 108 |
+
headers: { 'X-N8N-API-KEY': c.env.N8N_API_KEY },
|
| 109 |
+
signal: AbortSignal.timeout(10000),
|
| 110 |
+
},
|
| 111 |
+
).catch((err) => { throw new Error(`n8n activation failed: ${err instanceof Error ? err.message : 'unknown'}`); });
|
| 112 |
+
|
| 113 |
+
if (!activateResp.ok) {
|
| 114 |
+
const errText = await activateResp.text();
|
| 115 |
+
const activationResult: ActivationResult = {
|
| 116 |
+
jobId,
|
| 117 |
+
n8nWorkflowId: job.deploymentResult.n8nWorkflowId,
|
| 118 |
+
activationStatus: 'failed',
|
| 119 |
+
finalValidationPassed,
|
| 120 |
+
finalCredentialCheckPassed: credCheckPassed,
|
| 121 |
+
activatedAt: new Date().toISOString(),
|
| 122 |
+
diagnostics: `n8n API activation error ${activateResp.status}: ${errText}`,
|
| 123 |
+
};
|
| 124 |
+
|
| 125 |
+
await updateJob(c.env.WFO_CACHE, jobId, job, { activationResult, state: 'deployed' });
|
| 126 |
+
return c.json({ success: false, error: activationResult.diagnostics, activationResult, jobId }, 500);
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
const activationResult: ActivationResult = {
|
| 130 |
+
jobId,
|
| 131 |
+
n8nWorkflowId: job.deploymentResult.n8nWorkflowId,
|
| 132 |
+
activationStatus: 'activated',
|
| 133 |
+
finalValidationPassed,
|
| 134 |
+
finalCredentialCheckPassed: credCheckPassed,
|
| 135 |
+
activatedAt: new Date().toISOString(),
|
| 136 |
+
};
|
| 137 |
+
|
| 138 |
+
await updateJob(c.env.WFO_CACHE, jobId, job, { activationResult, state: 'activated' });
|
| 139 |
+
|
| 140 |
+
return c.json({
|
| 141 |
+
success: true,
|
| 142 |
+
jobId,
|
| 143 |
+
n8nWorkflowId: job.deploymentResult.n8nWorkflowId,
|
| 144 |
+
activationResult,
|
| 145 |
+
message: 'Workflow successfully activated in n8n.',
|
| 146 |
+
});
|
| 147 |
+
});
|
| 148 |
+
|
| 149 |
+
async function updateJob(
|
| 150 |
+
kv: KVNamespace,
|
| 151 |
+
jobId: string,
|
| 152 |
+
job: WorkflowJob,
|
| 153 |
+
updates: { activationResult: ActivationResult; state: WorkflowJob['state'] },
|
| 154 |
+
) {
|
| 155 |
+
const updated: WorkflowJob = {
|
| 156 |
+
...job,
|
| 157 |
+
...updates,
|
| 158 |
+
updatedAt: new Date().toISOString(),
|
| 159 |
+
auditLog: [
|
| 160 |
+
...job.auditLog,
|
| 161 |
+
createAuditEvent(jobId, `activation.${updates.activationResult.activationStatus}`, 'system', {
|
| 162 |
+
n8nWorkflowId: updates.activationResult.n8nWorkflowId,
|
| 163 |
+
}),
|
| 164 |
+
],
|
| 165 |
+
};
|
| 166 |
+
await kv.put(`job:${jobId}`, JSON.stringify(updated), { expirationTtl: 86400 * 7 });
|
| 167 |
+
}
|
apps/worker/src/routes/approval.ts
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* POST /api/workflows/approve
|
| 3 |
+
* Human Approval System - core safety gate
|
| 4 |
+
* No workflow may activate without explicit human approval
|
| 5 |
+
*
|
| 6 |
+
* GET /api/workflows/approve/:jobId β get approval request details
|
| 7 |
+
* POST /api/workflows/approve β submit approval decision
|
| 8 |
+
*/
|
| 9 |
+
import { Hono } from 'hono';
|
| 10 |
+
import { z } from 'zod';
|
| 11 |
+
import type { Env } from '../types/env';
|
| 12 |
+
import type { WorkflowJob, ApprovalRequest } from '../types/workflow';
|
| 13 |
+
import { createAuditEvent } from '../utils/audit';
|
| 14 |
+
|
| 15 |
+
export const approvalRoute = new Hono<{ Bindings: Env }>();
|
| 16 |
+
|
| 17 |
+
const ApprovalRequestSchema = z.object({
|
| 18 |
+
jobId: z.string().min(1),
|
| 19 |
+
});
|
| 20 |
+
|
| 21 |
+
const ApprovalDecisionSchema = z.object({
|
| 22 |
+
jobId: z.string().min(1),
|
| 23 |
+
decision: z.enum(['approved', 'rejected']),
|
| 24 |
+
reviewerNote: z.string().max(1000).optional(),
|
| 25 |
+
});
|
| 26 |
+
|
| 27 |
+
// βββ GET approval request details ββββββββββββββββββββββββββββββββββββββββββββ
|
| 28 |
+
approvalRoute.get('/approve/:jobId', async (c) => {
|
| 29 |
+
const jobId = c.req.param('jobId');
|
| 30 |
+
const raw = await c.env.WFO_CACHE.get(`job:${jobId}`, 'text');
|
| 31 |
+
if (!raw) return c.json({ success: false, error: `Job ${jobId} not found` }, 404);
|
| 32 |
+
|
| 33 |
+
const job = JSON.parse(raw) as WorkflowJob;
|
| 34 |
+
if (!job.approvalRequest) {
|
| 35 |
+
return c.json({ success: false, error: 'No approval request found for this job.' }, 404);
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
return c.json({
|
| 39 |
+
success: true,
|
| 40 |
+
jobId,
|
| 41 |
+
state: job.state,
|
| 42 |
+
approvalRequest: job.approvalRequest,
|
| 43 |
+
validationReport: job.validationReport,
|
| 44 |
+
simulationReport: job.simulationReport,
|
| 45 |
+
credentialAnalysis: job.credentialAnalysis,
|
| 46 |
+
architecturePlan: job.architecturePlan,
|
| 47 |
+
});
|
| 48 |
+
});
|
| 49 |
+
|
| 50 |
+
// βββ POST request approval (after deploy) ββββββββββββββββββββββββββββββββββββ
|
| 51 |
+
approvalRoute.post('/request-approval', async (c) => {
|
| 52 |
+
const body = await c.req.json().catch(() => null);
|
| 53 |
+
if (!body) return c.json({ success: false, error: 'Invalid JSON body' }, 400);
|
| 54 |
+
|
| 55 |
+
const parsed = ApprovalRequestSchema.safeParse(body);
|
| 56 |
+
if (!parsed.success) {
|
| 57 |
+
return c.json({ success: false, error: 'Validation failed', details: parsed.error.flatten() }, 400);
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
const { jobId } = parsed.data;
|
| 61 |
+
const raw = await c.env.WFO_CACHE.get(`job:${jobId}`, 'text');
|
| 62 |
+
if (!raw) return c.json({ success: false, error: `Job ${jobId} not found` }, 404);
|
| 63 |
+
|
| 64 |
+
const job = JSON.parse(raw) as WorkflowJob;
|
| 65 |
+
|
| 66 |
+
if (job.state !== 'deployed') {
|
| 67 |
+
return c.json({
|
| 68 |
+
success: false,
|
| 69 |
+
error: `Job must be in 'deployed' state before requesting approval. Current state: ${job.state}`,
|
| 70 |
+
}, 409);
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
const credentialStatus = job.credentialAnalysis?.allCredentialsPresent ? 'complete' : 'incomplete';
|
| 74 |
+
|
| 75 |
+
const approvalRequest: ApprovalRequest = {
|
| 76 |
+
jobId,
|
| 77 |
+
requestedAt: new Date().toISOString(),
|
| 78 |
+
expiresAt: new Date(Date.now() + 86400 * 1000).toISOString(), // 24h
|
| 79 |
+
architectureSummary: job.architecturePlan?.description ?? 'No architecture plan available',
|
| 80 |
+
riskSummary: job.architecturePlan?.riskAnalysis.overallRisk ?? 'unknown',
|
| 81 |
+
readinessScore: job.simulationReport?.readinessScore ?? 0,
|
| 82 |
+
validationScore: job.validationReport?.overallScore ?? 0,
|
| 83 |
+
simulationScore: job.simulationReport?.readinessScore ?? 0,
|
| 84 |
+
credentialStatus,
|
| 85 |
+
status: 'pending',
|
| 86 |
+
};
|
| 87 |
+
|
| 88 |
+
const updated: WorkflowJob = {
|
| 89 |
+
...job,
|
| 90 |
+
state: 'awaiting_approval',
|
| 91 |
+
approvalRequest,
|
| 92 |
+
updatedAt: new Date().toISOString(),
|
| 93 |
+
auditLog: [
|
| 94 |
+
...job.auditLog,
|
| 95 |
+
createAuditEvent(jobId, 'approval.requested', 'system', { credentialStatus }),
|
| 96 |
+
],
|
| 97 |
+
};
|
| 98 |
+
|
| 99 |
+
await c.env.WFO_CACHE.put(`job:${jobId}`, JSON.stringify(updated), { expirationTtl: 86400 * 7 });
|
| 100 |
+
|
| 101 |
+
return c.json({
|
| 102 |
+
success: true,
|
| 103 |
+
jobId,
|
| 104 |
+
approvalRequest,
|
| 105 |
+
message: 'Approval request created. A human reviewer must approve before activation.',
|
| 106 |
+
reviewUrl: `/api/workflows/approve/${jobId}`,
|
| 107 |
+
});
|
| 108 |
+
});
|
| 109 |
+
|
| 110 |
+
// βββ POST submit approval decision βββββββββββββββββββββββββββββββββββββββββββ
|
| 111 |
+
approvalRoute.post('/approve', async (c) => {
|
| 112 |
+
const body = await c.req.json().catch(() => null);
|
| 113 |
+
if (!body) return c.json({ success: false, error: 'Invalid JSON body' }, 400);
|
| 114 |
+
|
| 115 |
+
const parsed = ApprovalDecisionSchema.safeParse(body);
|
| 116 |
+
if (!parsed.success) {
|
| 117 |
+
return c.json({ success: false, error: 'Validation failed', details: parsed.error.flatten() }, 400);
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
const { jobId, decision, reviewerNote } = parsed.data;
|
| 121 |
+
const raw = await c.env.WFO_CACHE.get(`job:${jobId}`, 'text');
|
| 122 |
+
if (!raw) return c.json({ success: false, error: `Job ${jobId} not found` }, 404);
|
| 123 |
+
|
| 124 |
+
const job = JSON.parse(raw) as WorkflowJob;
|
| 125 |
+
|
| 126 |
+
if (job.state !== 'awaiting_approval') {
|
| 127 |
+
return c.json({
|
| 128 |
+
success: false,
|
| 129 |
+
error: `Job is not awaiting approval. Current state: ${job.state}`,
|
| 130 |
+
}, 409);
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
if (!job.approvalRequest) {
|
| 134 |
+
return c.json({ success: false, error: 'No pending approval request found.' }, 409);
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
// Check expiry
|
| 138 |
+
if (new Date(job.approvalRequest.expiresAt) < new Date()) {
|
| 139 |
+
return c.json({ success: false, error: 'Approval request has expired. Re-run /request-approval.' }, 410);
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
const updatedApproval: ApprovalRequest = {
|
| 143 |
+
...job.approvalRequest,
|
| 144 |
+
status: decision,
|
| 145 |
+
reviewerNote,
|
| 146 |
+
respondedAt: new Date().toISOString(),
|
| 147 |
+
};
|
| 148 |
+
|
| 149 |
+
const newState = decision === 'approved' ? 'approved' : 'rejected';
|
| 150 |
+
|
| 151 |
+
const updated: WorkflowJob = {
|
| 152 |
+
...job,
|
| 153 |
+
state: newState,
|
| 154 |
+
approvalRequest: updatedApproval,
|
| 155 |
+
updatedAt: new Date().toISOString(),
|
| 156 |
+
auditLog: [
|
| 157 |
+
...job.auditLog,
|
| 158 |
+
createAuditEvent(jobId, `approval.${decision}`, 'user', { reviewerNote }),
|
| 159 |
+
],
|
| 160 |
+
};
|
| 161 |
+
|
| 162 |
+
await c.env.WFO_CACHE.put(`job:${jobId}`, JSON.stringify(updated), { expirationTtl: 86400 * 7 });
|
| 163 |
+
|
| 164 |
+
const message = decision === 'approved'
|
| 165 |
+
? 'Workflow approved. You may now call /activate to activate it in n8n.'
|
| 166 |
+
: 'Workflow rejected. Deployment will not be activated.';
|
| 167 |
+
|
| 168 |
+
return c.json({ success: true, jobId, decision, newState, message });
|
| 169 |
+
});
|
apps/worker/src/routes/deploy.ts
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* POST /api/workflows/deploy
|
| 3 |
+
* Deploy workflow to n8n via REST API
|
| 4 |
+
* Worker handles this directly (mostly external API calls, minimal CPU)
|
| 5 |
+
*
|
| 6 |
+
* Pipeline gate: Validate β Simulate β Deploy
|
| 7 |
+
* Deployment β Activation (never auto-activate)
|
| 8 |
+
*/
|
| 9 |
+
import { Hono } from 'hono';
|
| 10 |
+
import { z } from 'zod';
|
| 11 |
+
import type { Env } from '../types/env';
|
| 12 |
+
import type { WorkflowJob, DeploymentResult } from '../types/workflow';
|
| 13 |
+
import { createAuditEvent } from '../utils/audit';
|
| 14 |
+
|
| 15 |
+
export const deployWorkflowRoute = new Hono<{ Bindings: Env }>();
|
| 16 |
+
|
| 17 |
+
const DeployRequestSchema = z.object({
|
| 18 |
+
jobId: z.string().min(1),
|
| 19 |
+
});
|
| 20 |
+
|
| 21 |
+
deployWorkflowRoute.post('/deploy', async (c) => {
|
| 22 |
+
const body = await c.req.json().catch(() => null);
|
| 23 |
+
if (!body) return c.json({ success: false, error: 'Invalid JSON body' }, 400);
|
| 24 |
+
|
| 25 |
+
const parsed = DeployRequestSchema.safeParse(body);
|
| 26 |
+
if (!parsed.success) {
|
| 27 |
+
return c.json({ success: false, error: 'Validation failed', details: parsed.error.flatten() }, 400);
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
const { jobId } = parsed.data;
|
| 31 |
+
const raw = await c.env.WFO_CACHE.get(`job:${jobId}`, 'text');
|
| 32 |
+
if (!raw) return c.json({ success: false, error: `Job ${jobId} not found` }, 404);
|
| 33 |
+
|
| 34 |
+
const job = JSON.parse(raw) as WorkflowJob;
|
| 35 |
+
|
| 36 |
+
// βββ Pipeline Gates ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 37 |
+
if (!job.compiledWorkflow) {
|
| 38 |
+
return c.json({ success: false, error: 'Workflow not compiled. Run /generate first.' }, 409);
|
| 39 |
+
}
|
| 40 |
+
if (!job.validationReport?.valid) {
|
| 41 |
+
return c.json({
|
| 42 |
+
success: false,
|
| 43 |
+
error: 'Workflow failed validation. Fix issues before deploying.',
|
| 44 |
+
validationReport: job.validationReport,
|
| 45 |
+
}, 409);
|
| 46 |
+
}
|
| 47 |
+
if (!job.simulationReport?.passed) {
|
| 48 |
+
return c.json({
|
| 49 |
+
success: false,
|
| 50 |
+
error: 'Workflow failed dry-run simulation. Fix issues before deploying.',
|
| 51 |
+
simulationReport: job.simulationReport,
|
| 52 |
+
}, 409);
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
// βββ Credential check via n8n API ββββββββββββββββββββββββββββββββββββββββ
|
| 56 |
+
const credResp = await fetch(`${c.env.N8N_BASE_URL}/api/v1/credentials`, {
|
| 57 |
+
headers: { 'X-N8N-API-KEY': c.env.N8N_API_KEY },
|
| 58 |
+
signal: AbortSignal.timeout(8000),
|
| 59 |
+
}).catch(() => null);
|
| 60 |
+
|
| 61 |
+
let availableCredentials: Array<{ id: string; name: string; type: string }> = [];
|
| 62 |
+
if (credResp?.ok) {
|
| 63 |
+
const credData = await credResp.json() as { data: typeof availableCredentials };
|
| 64 |
+
availableCredentials = credData.data ?? [];
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
// βββ Deploy to n8n (create or update) ββββββββββββββββββββββββββββββββββββ
|
| 68 |
+
const workflowPayload = {
|
| 69 |
+
...job.compiledWorkflow,
|
| 70 |
+
active: false, // NEVER activate on deploy
|
| 71 |
+
};
|
| 72 |
+
|
| 73 |
+
let n8nResponse: Response;
|
| 74 |
+
let n8nWorkflowId = job.deploymentResult?.n8nWorkflowId;
|
| 75 |
+
|
| 76 |
+
try {
|
| 77 |
+
if (n8nWorkflowId) {
|
| 78 |
+
// Update existing
|
| 79 |
+
n8nResponse = await fetch(`${c.env.N8N_BASE_URL}/api/v1/workflows/${n8nWorkflowId}`, {
|
| 80 |
+
method: 'PUT',
|
| 81 |
+
headers: {
|
| 82 |
+
'Content-Type': 'application/json',
|
| 83 |
+
'X-N8N-API-KEY': c.env.N8N_API_KEY,
|
| 84 |
+
},
|
| 85 |
+
body: JSON.stringify(workflowPayload),
|
| 86 |
+
signal: AbortSignal.timeout(10000),
|
| 87 |
+
});
|
| 88 |
+
} else {
|
| 89 |
+
// Create new
|
| 90 |
+
n8nResponse = await fetch(`${c.env.N8N_BASE_URL}/api/v1/workflows`, {
|
| 91 |
+
method: 'POST',
|
| 92 |
+
headers: {
|
| 93 |
+
'Content-Type': 'application/json',
|
| 94 |
+
'X-N8N-API-KEY': c.env.N8N_API_KEY,
|
| 95 |
+
},
|
| 96 |
+
body: JSON.stringify(workflowPayload),
|
| 97 |
+
signal: AbortSignal.timeout(10000),
|
| 98 |
+
});
|
| 99 |
+
}
|
| 100 |
+
} catch (err) {
|
| 101 |
+
const msg = err instanceof Error ? err.message : 'n8n API unreachable';
|
| 102 |
+
return c.json({ success: false, error: `Deployment failed: ${msg}`, jobId }, 502);
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
if (!n8nResponse.ok) {
|
| 106 |
+
const errText = await n8nResponse.text();
|
| 107 |
+
const deploymentResult: DeploymentResult = {
|
| 108 |
+
jobId,
|
| 109 |
+
status: 'failed',
|
| 110 |
+
deployedAt: new Date().toISOString(),
|
| 111 |
+
errorMessage: `n8n API ${n8nResponse.status}: ${errText}`,
|
| 112 |
+
};
|
| 113 |
+
|
| 114 |
+
await updateJobInKV(c.env.WFO_CACHE, jobId, job, {
|
| 115 |
+
state: 'simulated', // revert to pre-deploy state
|
| 116 |
+
deploymentResult,
|
| 117 |
+
event: 'deployment.failed',
|
| 118 |
+
});
|
| 119 |
+
|
| 120 |
+
return c.json({ success: false, error: deploymentResult.errorMessage, deploymentResult, jobId }, 500);
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
const n8nData = await n8nResponse.json() as { id: string };
|
| 124 |
+
n8nWorkflowId = n8nData.id;
|
| 125 |
+
|
| 126 |
+
const deploymentResult: DeploymentResult = {
|
| 127 |
+
jobId,
|
| 128 |
+
n8nWorkflowId,
|
| 129 |
+
status: 'success',
|
| 130 |
+
deployedAt: new Date().toISOString(),
|
| 131 |
+
};
|
| 132 |
+
|
| 133 |
+
await updateJobInKV(c.env.WFO_CACHE, jobId, job, {
|
| 134 |
+
state: 'deployed',
|
| 135 |
+
deploymentResult,
|
| 136 |
+
event: 'deployment.success',
|
| 137 |
+
credentialAnalysis: {
|
| 138 |
+
jobId,
|
| 139 |
+
allCredentialsPresent: availableCredentials.length > 0,
|
| 140 |
+
required: [],
|
| 141 |
+
available: availableCredentials,
|
| 142 |
+
missing: [],
|
| 143 |
+
analysedAt: new Date().toISOString(),
|
| 144 |
+
},
|
| 145 |
+
});
|
| 146 |
+
|
| 147 |
+
return c.json({
|
| 148 |
+
success: true,
|
| 149 |
+
jobId,
|
| 150 |
+
n8nWorkflowId,
|
| 151 |
+
deploymentResult,
|
| 152 |
+
message: 'Workflow deployed successfully. It is NOT yet activated. Submit /approve to request activation.',
|
| 153 |
+
});
|
| 154 |
+
});
|
| 155 |
+
|
| 156 |
+
async function updateJobInKV(
|
| 157 |
+
kv: KVNamespace,
|
| 158 |
+
jobId: string,
|
| 159 |
+
job: WorkflowJob,
|
| 160 |
+
updates: Partial<WorkflowJob> & { event: string },
|
| 161 |
+
) {
|
| 162 |
+
const { event, ...rest } = updates;
|
| 163 |
+
const updated: WorkflowJob = {
|
| 164 |
+
...job,
|
| 165 |
+
...rest,
|
| 166 |
+
updatedAt: new Date().toISOString(),
|
| 167 |
+
auditLog: [
|
| 168 |
+
...job.auditLog,
|
| 169 |
+
createAuditEvent(jobId, event, 'system', rest as Record<string, unknown>),
|
| 170 |
+
],
|
| 171 |
+
};
|
| 172 |
+
await kv.put(`job:${jobId}`, JSON.stringify(updated), { expirationTtl: 86400 * 7 });
|
| 173 |
+
}
|
apps/worker/src/routes/generate.ts
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* POST /api/workflows/generate β UPGRADED
|
| 3 |
+
* CF Worker role: Thin orchestrator + API gateway
|
| 4 |
+
* Passes LLM config to simulator (provider-agnostic, no direct OpenAI dep)
|
| 5 |
+
*/
|
| 6 |
+
import { Hono } from 'hono';
|
| 7 |
+
import { z } from 'zod';
|
| 8 |
+
import type { Env } from '../types/env';
|
| 9 |
+
import type { WorkflowJob, GenerateWorkflowResponse } from '../types/workflow';
|
| 10 |
+
import { createAuditEvent } from '../utils/audit';
|
| 11 |
+
import { generateJobId } from '../utils/ids';
|
| 12 |
+
|
| 13 |
+
export const generateWorkflowRoute = new Hono<{ Bindings: Env }>();
|
| 14 |
+
|
| 15 |
+
const GenerateRequestSchema = z.object({
|
| 16 |
+
request: z.string().min(10, 'Request must be at least 10 characters').max(2000),
|
| 17 |
+
options: z.object({
|
| 18 |
+
skipSimulation: z.boolean().optional().default(false),
|
| 19 |
+
requireApproval: z.boolean().optional().default(true),
|
| 20 |
+
swarmSize: z.number().min(1).max(5).optional().default(3),
|
| 21 |
+
skipSwarm: z.boolean().optional().default(false),
|
| 22 |
+
skipSelfHealing: z.boolean().optional().default(false),
|
| 23 |
+
}).optional().default({}),
|
| 24 |
+
});
|
| 25 |
+
|
| 26 |
+
generateWorkflowRoute.post('/generate', async (c) => {
|
| 27 |
+
// βββ Parse & validate request βββββββββββββββββββββββββββββββββββββββββββββ
|
| 28 |
+
const body = await c.req.json().catch(() => null);
|
| 29 |
+
if (!body) return c.json({ success: false, error: 'Invalid JSON body' }, 400);
|
| 30 |
+
|
| 31 |
+
const parsed = GenerateRequestSchema.safeParse(body);
|
| 32 |
+
if (!parsed.success) {
|
| 33 |
+
return c.json({ success: false, error: 'Validation failed', details: parsed.error.flatten() }, 400);
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
const { request, options } = parsed.data;
|
| 37 |
+
const jobId = generateJobId();
|
| 38 |
+
|
| 39 |
+
// βββ Create initial job in KV βββββββββββββββββββββββββββββββββββββββββββββ
|
| 40 |
+
const job: WorkflowJob = {
|
| 41 |
+
id: jobId,
|
| 42 |
+
userRequest: request,
|
| 43 |
+
state: 'draft',
|
| 44 |
+
createdAt: new Date().toISOString(),
|
| 45 |
+
updatedAt: new Date().toISOString(),
|
| 46 |
+
auditLog: [createAuditEvent(jobId, 'job.created', 'user', { request })],
|
| 47 |
+
};
|
| 48 |
+
|
| 49 |
+
await c.env.WFO_CACHE.put(`job:${jobId}`, JSON.stringify(job), {
|
| 50 |
+
expirationTtl: 86400 * 7,
|
| 51 |
+
});
|
| 52 |
+
|
| 53 |
+
// βββ Build LLM config for simulator (provider-agnostic) ββββββββββββββββββ
|
| 54 |
+
// Resolve API key: prefer new LLM_API_KEY, fallback to legacy OPENAI_API_KEY
|
| 55 |
+
const llmApiKey = c.env.LLM_API_KEY || c.env.OPENAI_API_KEY || '';
|
| 56 |
+
const llmModel = c.env.LLM_MODEL || 'gpt-4o';
|
| 57 |
+
const llmGatewayUrl = c.env.LLM_GATEWAY_URL || 'https://api.openai.com/v1';
|
| 58 |
+
const llmProvider = c.env.LLM_PROVIDER || 'openai';
|
| 59 |
+
|
| 60 |
+
if (!llmApiKey) {
|
| 61 |
+
return c.json({
|
| 62 |
+
success: false,
|
| 63 |
+
error: 'LLM API key not configured. Set LLM_API_KEY secret in wrangler.',
|
| 64 |
+
jobId,
|
| 65 |
+
}, 500);
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
// βββ Offload to simulator service (heavy CPU/LLM work) βββββββββββββββββββ
|
| 69 |
+
const simulatorPayload = {
|
| 70 |
+
jobId,
|
| 71 |
+
request,
|
| 72 |
+
options,
|
| 73 |
+
// Provider-agnostic LLM config
|
| 74 |
+
llmApiKey,
|
| 75 |
+
llmModel,
|
| 76 |
+
llmGatewayUrl,
|
| 77 |
+
llmProvider,
|
| 78 |
+
// Legacy field for backward compat with older simulator versions
|
| 79 |
+
openaiApiKey: llmApiKey,
|
| 80 |
+
// n8n config
|
| 81 |
+
n8nBaseUrl: c.env.N8N_BASE_URL,
|
| 82 |
+
n8nApiKey: c.env.N8N_API_KEY,
|
| 83 |
+
};
|
| 84 |
+
|
| 85 |
+
let simulatorResponse: Response;
|
| 86 |
+
try {
|
| 87 |
+
simulatorResponse = await fetch(`${c.env.SIMULATOR_URL}/process/generate`, {
|
| 88 |
+
method: 'POST',
|
| 89 |
+
headers: {
|
| 90 |
+
'Content-Type': 'application/json',
|
| 91 |
+
'X-Internal-Secret': c.env.INTERNAL_API_SECRET,
|
| 92 |
+
},
|
| 93 |
+
body: JSON.stringify(simulatorPayload),
|
| 94 |
+
signal: AbortSignal.timeout(55000), // 55s β allow swarm + healing time
|
| 95 |
+
});
|
| 96 |
+
} catch (err) {
|
| 97 |
+
const errMsg = err instanceof Error ? err.message : 'Simulator unreachable';
|
| 98 |
+
await updateJobState(c.env.WFO_CACHE, jobId, 'draft', errMsg);
|
| 99 |
+
return c.json({ success: false, error: `Simulator service error: ${errMsg}`, jobId }, 502);
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
if (!simulatorResponse.ok) {
|
| 103 |
+
const errText = await simulatorResponse.text();
|
| 104 |
+
return c.json({ success: false, error: `Simulator returned ${simulatorResponse.status}: ${errText}`, jobId }, 502);
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
const result = await simulatorResponse.json() as GenerateWorkflowResponse;
|
| 108 |
+
|
| 109 |
+
// βββ Store full result in KV ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 110 |
+
const updatedJob: WorkflowJob = {
|
| 111 |
+
...job,
|
| 112 |
+
state: result.state,
|
| 113 |
+
updatedAt: new Date().toISOString(),
|
| 114 |
+
architecturePlan: result.architecturePlan,
|
| 115 |
+
graph: result.graph,
|
| 116 |
+
compiledWorkflow: result.compiledWorkflow,
|
| 117 |
+
validationReport: result.validationReport,
|
| 118 |
+
simulationReport: result.simulationReport,
|
| 119 |
+
credentialAnalysis: result.credentialAnalysis,
|
| 120 |
+
qualityScore: result.qualityScore,
|
| 121 |
+
auditLog: [
|
| 122 |
+
...job.auditLog,
|
| 123 |
+
createAuditEvent(jobId, 'job.generated', 'ai', {
|
| 124 |
+
state: result.state,
|
| 125 |
+
qualityScore: result.qualityScore?.overallScore,
|
| 126 |
+
swarmSize: result.swarmResult?.swarmSize,
|
| 127 |
+
triggerType: result.triggerDetection?.triggerType,
|
| 128 |
+
}),
|
| 129 |
+
],
|
| 130 |
+
};
|
| 131 |
+
|
| 132 |
+
await c.env.WFO_CACHE.put(`job:${jobId}`, JSON.stringify(updatedJob), {
|
| 133 |
+
expirationTtl: 86400 * 7,
|
| 134 |
+
});
|
| 135 |
+
|
| 136 |
+
return c.json({ success: true, jobId, ...result }, 200);
|
| 137 |
+
});
|
| 138 |
+
|
| 139 |
+
async function updateJobState(kv: KVNamespace, jobId: string, state: string, error?: string) {
|
| 140 |
+
const raw = await kv.get(`job:${jobId}`, 'text');
|
| 141 |
+
if (!raw) return;
|
| 142 |
+
const job = JSON.parse(raw) as WorkflowJob;
|
| 143 |
+
await kv.put(`job:${jobId}`, JSON.stringify({
|
| 144 |
+
...job,
|
| 145 |
+
state,
|
| 146 |
+
updatedAt: new Date().toISOString(),
|
| 147 |
+
error,
|
| 148 |
+
}), { expirationTtl: 86400 * 7 });
|
| 149 |
+
}
|
apps/worker/src/routes/health.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* GET /health β UPGRADED
|
| 3 |
+
* Public health check endpoint with system capabilities info
|
| 4 |
+
*/
|
| 5 |
+
import { Hono } from 'hono';
|
| 6 |
+
import type { Env } from '../types/env';
|
| 7 |
+
|
| 8 |
+
export const healthRoute = new Hono<{ Bindings: Env }>();
|
| 9 |
+
|
| 10 |
+
healthRoute.get('/', async (c) => {
|
| 11 |
+
return c.json({
|
| 12 |
+
status: 'ok',
|
| 13 |
+
service: 'workflow-factor-os',
|
| 14 |
+
version: '2.0.0',
|
| 15 |
+
environment: c.env.ENVIRONMENT,
|
| 16 |
+
timestamp: new Date().toISOString(),
|
| 17 |
+
capabilities: {
|
| 18 |
+
llmGateway: 'provider-agnostic (openai | anthropic | openai-compatible | custom)',
|
| 19 |
+
llmProvider: c.env.LLM_PROVIDER ?? 'openai',
|
| 20 |
+
llmModel: c.env.LLM_MODEL ?? 'gpt-4o',
|
| 21 |
+
swarmOrchestration: 'parallel multi-agent design (up to 5 candidates)',
|
| 22 |
+
selfHealing: 'automatic validation fix + AI-assisted healing',
|
| 23 |
+
webhookAutoBind: 'auto-trigger detection and configuration',
|
| 24 |
+
memorySystem: 'in-process pattern learning and anti-pattern prevention',
|
| 25 |
+
nodeRegistry: 'strict β only verified n8n node types allowed',
|
| 26 |
+
safetyGates: 'validate β simulate β deploy β approve β activate',
|
| 27 |
+
},
|
| 28 |
+
endpoints: {
|
| 29 |
+
generate: 'POST /api/workflows/generate',
|
| 30 |
+
validate: 'POST /api/workflows/validate',
|
| 31 |
+
simulate: 'POST /api/workflows/simulate',
|
| 32 |
+
deploy: 'POST /api/workflows/deploy',
|
| 33 |
+
requestApproval: 'POST /api/workflows/request-approval',
|
| 34 |
+
approve: 'POST /api/workflows/approve',
|
| 35 |
+
activate: 'POST /api/workflows/activate',
|
| 36 |
+
reports: {
|
| 37 |
+
workflow: 'GET /api/reports/workflow/:id',
|
| 38 |
+
validation: 'GET /api/reports/validation/:id',
|
| 39 |
+
simulation: 'GET /api/reports/simulation/:id',
|
| 40 |
+
credential: 'GET /api/reports/credential/:id',
|
| 41 |
+
audit: 'GET /api/reports/audit/:id',
|
| 42 |
+
},
|
| 43 |
+
},
|
| 44 |
+
safetyPolicy: {
|
| 45 |
+
autoActivation: 'DISABLED β human approval always required',
|
| 46 |
+
partialWorkflows: 'REJECTED β never deploy partially valid workflows',
|
| 47 |
+
unknownNodes: 'REJECTED β only registered n8n node types allowed',
|
| 48 |
+
activeOnDeploy: 'FORCED FALSE β active: false enforced at all stages',
|
| 49 |
+
},
|
| 50 |
+
});
|
| 51 |
+
});
|
apps/worker/src/routes/reports.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* GET /api/reports/workflow/:id
|
| 3 |
+
* GET /api/reports/validation/:id
|
| 4 |
+
* GET /api/reports/simulation/:id
|
| 5 |
+
* GET /api/reports/credential/:id
|
| 6 |
+
* GET /api/reports/audit/:id
|
| 7 |
+
*/
|
| 8 |
+
import { Hono } from 'hono';
|
| 9 |
+
import type { Env } from '../types/env';
|
| 10 |
+
import type { WorkflowJob } from '../types/workflow';
|
| 11 |
+
|
| 12 |
+
export const reportsRoute = new Hono<{ Bindings: Env }>();
|
| 13 |
+
|
| 14 |
+
// Full workflow job report
|
| 15 |
+
reportsRoute.get('/workflow/:id', async (c) => {
|
| 16 |
+
const id = c.req.param('id');
|
| 17 |
+
const raw = await c.env.WFO_CACHE.get(`job:${id}`, 'text');
|
| 18 |
+
if (!raw) return c.json({ success: false, error: `Job ${id} not found` }, 404);
|
| 19 |
+
const job = JSON.parse(raw) as WorkflowJob;
|
| 20 |
+
return c.json({ success: true, job });
|
| 21 |
+
});
|
| 22 |
+
|
| 23 |
+
// Validation report only
|
| 24 |
+
reportsRoute.get('/validation/:id', async (c) => {
|
| 25 |
+
const id = c.req.param('id');
|
| 26 |
+
const raw = await c.env.WFO_CACHE.get(`job:${id}`, 'text');
|
| 27 |
+
if (!raw) return c.json({ success: false, error: `Job ${id} not found` }, 404);
|
| 28 |
+
const job = JSON.parse(raw) as WorkflowJob;
|
| 29 |
+
if (!job.validationReport) {
|
| 30 |
+
return c.json({ success: false, error: 'No validation report available yet.' }, 404);
|
| 31 |
+
}
|
| 32 |
+
return c.json({ success: true, jobId: id, validationReport: job.validationReport });
|
| 33 |
+
});
|
| 34 |
+
|
| 35 |
+
// Simulation report only
|
| 36 |
+
reportsRoute.get('/simulation/:id', async (c) => {
|
| 37 |
+
const id = c.req.param('id');
|
| 38 |
+
const raw = await c.env.WFO_CACHE.get(`job:${id}`, 'text');
|
| 39 |
+
if (!raw) return c.json({ success: false, error: `Job ${id} not found` }, 404);
|
| 40 |
+
const job = JSON.parse(raw) as WorkflowJob;
|
| 41 |
+
if (!job.simulationReport) {
|
| 42 |
+
return c.json({ success: false, error: 'No simulation report available yet.' }, 404);
|
| 43 |
+
}
|
| 44 |
+
return c.json({ success: true, jobId: id, simulationReport: job.simulationReport });
|
| 45 |
+
});
|
| 46 |
+
|
| 47 |
+
// Credential analysis
|
| 48 |
+
reportsRoute.get('/credential/:id', async (c) => {
|
| 49 |
+
const id = c.req.param('id');
|
| 50 |
+
const raw = await c.env.WFO_CACHE.get(`job:${id}`, 'text');
|
| 51 |
+
if (!raw) return c.json({ success: false, error: `Job ${id} not found` }, 404);
|
| 52 |
+
const job = JSON.parse(raw) as WorkflowJob;
|
| 53 |
+
if (!job.credentialAnalysis) {
|
| 54 |
+
return c.json({ success: false, error: 'No credential analysis available yet.' }, 404);
|
| 55 |
+
}
|
| 56 |
+
return c.json({ success: true, jobId: id, credentialAnalysis: job.credentialAnalysis });
|
| 57 |
+
});
|
| 58 |
+
|
| 59 |
+
// Audit trail
|
| 60 |
+
reportsRoute.get('/audit/:id', async (c) => {
|
| 61 |
+
const id = c.req.param('id');
|
| 62 |
+
const raw = await c.env.WFO_CACHE.get(`job:${id}`, 'text');
|
| 63 |
+
if (!raw) return c.json({ success: false, error: `Job ${id} not found` }, 404);
|
| 64 |
+
const job = JSON.parse(raw) as WorkflowJob;
|
| 65 |
+
return c.json({
|
| 66 |
+
success: true,
|
| 67 |
+
jobId: id,
|
| 68 |
+
state: job.state,
|
| 69 |
+
auditLog: job.auditLog,
|
| 70 |
+
createdAt: job.createdAt,
|
| 71 |
+
updatedAt: job.updatedAt,
|
| 72 |
+
});
|
| 73 |
+
});
|
apps/worker/src/routes/simulate.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* POST /api/workflows/simulate
|
| 3 |
+
* Dry-run simulation - offloaded entirely to external compute service
|
| 4 |
+
* CF Worker just proxies the request and stores results in KV
|
| 5 |
+
*/
|
| 6 |
+
import { Hono } from 'hono';
|
| 7 |
+
import { z } from 'zod';
|
| 8 |
+
import type { Env } from '../types/env';
|
| 9 |
+
import type { WorkflowJob } from '../types/workflow';
|
| 10 |
+
import { createAuditEvent } from '../utils/audit';
|
| 11 |
+
|
| 12 |
+
export const simulateWorkflowRoute = new Hono<{ Bindings: Env }>();
|
| 13 |
+
|
| 14 |
+
const SimulateRequestSchema = z.object({
|
| 15 |
+
jobId: z.string().min(1),
|
| 16 |
+
mockData: z.record(z.unknown()).optional(),
|
| 17 |
+
});
|
| 18 |
+
|
| 19 |
+
simulateWorkflowRoute.post('/simulate', async (c) => {
|
| 20 |
+
const body = await c.req.json().catch(() => null);
|
| 21 |
+
if (!body) return c.json({ success: false, error: 'Invalid JSON body' }, 400);
|
| 22 |
+
|
| 23 |
+
const parsed = SimulateRequestSchema.safeParse(body);
|
| 24 |
+
if (!parsed.success) {
|
| 25 |
+
return c.json({ success: false, error: 'Validation failed', details: parsed.error.flatten() }, 400);
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
const { jobId, mockData } = parsed.data;
|
| 29 |
+
const raw = await c.env.WFO_CACHE.get(`job:${jobId}`, 'text');
|
| 30 |
+
if (!raw) return c.json({ success: false, error: `Job ${jobId} not found` }, 404);
|
| 31 |
+
|
| 32 |
+
const job = JSON.parse(raw) as WorkflowJob;
|
| 33 |
+
|
| 34 |
+
if (!job.compiledWorkflow || !job.graph) {
|
| 35 |
+
return c.json({
|
| 36 |
+
success: false,
|
| 37 |
+
error: 'Workflow not compiled or validated. Run /generate and /validate first.',
|
| 38 |
+
}, 409);
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
if (job.validationReport && !job.validationReport.valid) {
|
| 42 |
+
return c.json({
|
| 43 |
+
success: false,
|
| 44 |
+
error: 'Cannot simulate an invalid workflow. Fix validation issues first.',
|
| 45 |
+
validationReport: job.validationReport,
|
| 46 |
+
}, 409);
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
// Offload simulation to external compute (CPU-intensive)
|
| 50 |
+
const resp = await fetch(`${c.env.SIMULATOR_URL}/process/simulate`, {
|
| 51 |
+
method: 'POST',
|
| 52 |
+
headers: {
|
| 53 |
+
'Content-Type': 'application/json',
|
| 54 |
+
'X-Internal-Secret': c.env.INTERNAL_API_SECRET,
|
| 55 |
+
},
|
| 56 |
+
body: JSON.stringify({
|
| 57 |
+
jobId,
|
| 58 |
+
workflow: job.compiledWorkflow,
|
| 59 |
+
graph: job.graph,
|
| 60 |
+
architecturePlan: job.architecturePlan,
|
| 61 |
+
mockData: mockData ?? {},
|
| 62 |
+
}),
|
| 63 |
+
signal: AbortSignal.timeout(25000),
|
| 64 |
+
}).catch((e) => { throw new Error(`Simulator error: ${e instanceof Error ? e.message : 'unknown'}`); });
|
| 65 |
+
|
| 66 |
+
if (!resp.ok) {
|
| 67 |
+
return c.json({ success: false, error: `Simulator returned ${resp.status}` }, 502);
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
const result = await resp.json() as { simulationReport: WorkflowJob['simulationReport'] };
|
| 71 |
+
|
| 72 |
+
const updated: WorkflowJob = {
|
| 73 |
+
...job,
|
| 74 |
+
state: result.simulationReport?.passed ? 'simulated' : 'validated',
|
| 75 |
+
simulationReport: result.simulationReport,
|
| 76 |
+
updatedAt: new Date().toISOString(),
|
| 77 |
+
auditLog: [
|
| 78 |
+
...job.auditLog,
|
| 79 |
+
createAuditEvent(jobId, 'simulation.completed', 'system', {
|
| 80 |
+
passed: result.simulationReport?.passed,
|
| 81 |
+
readinessScore: result.simulationReport?.readinessScore,
|
| 82 |
+
}),
|
| 83 |
+
],
|
| 84 |
+
};
|
| 85 |
+
|
| 86 |
+
await c.env.WFO_CACHE.put(`job:${jobId}`, JSON.stringify(updated), { expirationTtl: 86400 * 7 });
|
| 87 |
+
|
| 88 |
+
return c.json({ success: true, jobId, simulationReport: result.simulationReport });
|
| 89 |
+
});
|
apps/worker/src/routes/validate.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* POST /api/workflows/validate
|
| 3 |
+
* Triggers validation on an already-generated workflow job
|
| 4 |
+
* Offloads to external simulator service
|
| 5 |
+
*/
|
| 6 |
+
import { Hono } from 'hono';
|
| 7 |
+
import { z } from 'zod';
|
| 8 |
+
import type { Env } from '../types/env';
|
| 9 |
+
import type { WorkflowJob } from '../types/workflow';
|
| 10 |
+
import { createAuditEvent } from '../utils/audit';
|
| 11 |
+
|
| 12 |
+
export const validateWorkflowRoute = new Hono<{ Bindings: Env }>();
|
| 13 |
+
|
| 14 |
+
const ValidateRequestSchema = z.object({
|
| 15 |
+
jobId: z.string().min(1),
|
| 16 |
+
});
|
| 17 |
+
|
| 18 |
+
validateWorkflowRoute.post('/validate', async (c) => {
|
| 19 |
+
const body = await c.req.json().catch(() => null);
|
| 20 |
+
if (!body) return c.json({ success: false, error: 'Invalid JSON body' }, 400);
|
| 21 |
+
|
| 22 |
+
const parsed = ValidateRequestSchema.safeParse(body);
|
| 23 |
+
if (!parsed.success) {
|
| 24 |
+
return c.json({ success: false, error: 'Validation failed', details: parsed.error.flatten() }, 400);
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
const { jobId } = parsed.data;
|
| 28 |
+
const raw = await c.env.WFO_CACHE.get(`job:${jobId}`, 'text');
|
| 29 |
+
if (!raw) return c.json({ success: false, error: `Job ${jobId} not found` }, 404);
|
| 30 |
+
|
| 31 |
+
const job = JSON.parse(raw) as WorkflowJob;
|
| 32 |
+
if (!job.compiledWorkflow) {
|
| 33 |
+
return c.json({ success: false, error: 'Workflow not yet compiled. Run /generate first.' }, 409);
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
// Offload validation to external compute service
|
| 37 |
+
const resp = await fetch(`${c.env.SIMULATOR_URL}/process/validate`, {
|
| 38 |
+
method: 'POST',
|
| 39 |
+
headers: {
|
| 40 |
+
'Content-Type': 'application/json',
|
| 41 |
+
'X-Internal-Secret': c.env.INTERNAL_API_SECRET,
|
| 42 |
+
},
|
| 43 |
+
body: JSON.stringify({
|
| 44 |
+
jobId,
|
| 45 |
+
workflow: job.compiledWorkflow,
|
| 46 |
+
graph: job.graph,
|
| 47 |
+
n8nBaseUrl: c.env.N8N_BASE_URL,
|
| 48 |
+
n8nApiKey: c.env.N8N_API_KEY,
|
| 49 |
+
}),
|
| 50 |
+
signal: AbortSignal.timeout(15000),
|
| 51 |
+
}).catch((e) => { throw new Error(`Simulator error: ${e instanceof Error ? e.message : 'unknown'}`); });
|
| 52 |
+
|
| 53 |
+
if (!resp.ok) {
|
| 54 |
+
return c.json({ success: false, error: `Simulator returned ${resp.status}` }, 502);
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
const result = await resp.json() as { validationReport: WorkflowJob['validationReport'] };
|
| 58 |
+
|
| 59 |
+
// Update job in KV
|
| 60 |
+
const updated: WorkflowJob = {
|
| 61 |
+
...job,
|
| 62 |
+
state: result.validationReport?.valid ? 'validated' : 'generated',
|
| 63 |
+
validationReport: result.validationReport,
|
| 64 |
+
updatedAt: new Date().toISOString(),
|
| 65 |
+
auditLog: [
|
| 66 |
+
...job.auditLog,
|
| 67 |
+
createAuditEvent(jobId, 'validation.completed', 'system', {
|
| 68 |
+
valid: result.validationReport?.valid,
|
| 69 |
+
score: result.validationReport?.overallScore,
|
| 70 |
+
}),
|
| 71 |
+
],
|
| 72 |
+
};
|
| 73 |
+
|
| 74 |
+
await c.env.WFO_CACHE.put(`job:${jobId}`, JSON.stringify(updated), { expirationTtl: 86400 * 7 });
|
| 75 |
+
|
| 76 |
+
return c.json({ success: true, jobId, validationReport: result.validationReport });
|
| 77 |
+
});
|
apps/worker/src/types/env.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Cloudflare Worker Environment Bindings β UPGRADED
|
| 3 |
+
* Provider-agnostic LLM config (no direct OpenAI dependency)
|
| 4 |
+
* Supports any OpenAI-compatible endpoint, Anthropic, or custom gateway
|
| 5 |
+
*/
|
| 6 |
+
type KVNamespace = any;
|
| 7 |
+
export interface Env {
|
| 8 |
+
// βββ KV Namespaces ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 9 |
+
WFO_CACHE: KVNamespace; // workflow jobs, reports, approval states
|
| 10 |
+
|
| 11 |
+
// βββ LLM Gateway Secrets (provider-agnostic) βββββββββββββββββββββββββββββ
|
| 12 |
+
LLM_API_KEY: string; // provider API key (OpenAI, Anthropic, or custom)
|
| 13 |
+
LLM_GATEWAY_URL: string; // e.g. https://api.openai.com/v1 or custom endpoint
|
| 14 |
+
LLM_MODEL: string; // e.g. gpt-4o, claude-3-5-sonnet-20241022, etc.
|
| 15 |
+
LLM_PROVIDER: string; // openai | anthropic | openai-compatible | custom
|
| 16 |
+
|
| 17 |
+
// βββ Legacy fallback (for backward compatibility) βββββββββββββββββββββββββ
|
| 18 |
+
OPENAI_API_KEY: string; // deprecated β use LLM_API_KEY instead
|
| 19 |
+
|
| 20 |
+
// βββ Fallback LLM provider (optional) ββββββββββββββββββββββββββββββββββββ
|
| 21 |
+
LLM_FALLBACK_URL: string; // fallback gateway URL (optional)
|
| 22 |
+
LLM_FALLBACK_KEY: string; // fallback API key (optional)
|
| 23 |
+
LLM_FALLBACK_MODEL: string; // fallback model (optional)
|
| 24 |
+
|
| 25 |
+
// βββ n8n Instance ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 26 |
+
N8N_API_KEY: string; // n8n REST API key
|
| 27 |
+
INTERNAL_API_SECRET: string; // shared secret between Worker β Simulator service
|
| 28 |
+
|
| 29 |
+
// βββ Env Vars (wrangler.toml [vars]) βββββββββββββββββββββββββββββββββββββ
|
| 30 |
+
ENVIRONMENT: 'development' | 'production';
|
| 31 |
+
SIMULATOR_URL: string; // external compute service URL (Express/Node.js)
|
| 32 |
+
N8N_BASE_URL: string; // n8n instance base URL
|
| 33 |
+
}
|
apps/worker/src/types/workflow.ts
ADDED
|
@@ -0,0 +1,499 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Shared Workflow Types β UPGRADED
|
| 3 |
+
* Complete type definitions for all workflow stages
|
| 4 |
+
* Supports Swarm, Self-Healing, Memory, WebhookAutoBind outputs
|
| 5 |
+
*/
|
| 6 |
+
|
| 7 |
+
// βββ Domain / Meta types ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 8 |
+
|
| 9 |
+
export type WorkflowDomain =
|
| 10 |
+
| 'sales_automation'
|
| 11 |
+
| 'ai_workflow'
|
| 12 |
+
| 'data_pipeline'
|
| 13 |
+
| 'webhook_system'
|
| 14 |
+
| 'notification'
|
| 15 |
+
| 'crm_integration'
|
| 16 |
+
| 'devops'
|
| 17 |
+
| 'ecommerce'
|
| 18 |
+
| 'general';
|
| 19 |
+
|
| 20 |
+
export type RiskLevel = 'low' | 'medium' | 'high' | 'critical';
|
| 21 |
+
|
| 22 |
+
export type WorkflowLifecycleState =
|
| 23 |
+
| 'draft'
|
| 24 |
+
| 'generated'
|
| 25 |
+
| 'validated'
|
| 26 |
+
| 'simulated'
|
| 27 |
+
| 'deployed'
|
| 28 |
+
| 'awaiting_approval'
|
| 29 |
+
| 'approved'
|
| 30 |
+
| 'rejected'
|
| 31 |
+
| 'activated'
|
| 32 |
+
| 'failed';
|
| 33 |
+
|
| 34 |
+
export type WorkflowLayer =
|
| 35 |
+
| 'trigger'
|
| 36 |
+
| 'validation'
|
| 37 |
+
| 'transform'
|
| 38 |
+
| 'ai'
|
| 39 |
+
| 'integration'
|
| 40 |
+
| 'control'
|
| 41 |
+
| 'database'
|
| 42 |
+
| 'utility'
|
| 43 |
+
| 'monitoring'
|
| 44 |
+
| 'error_handling';
|
| 45 |
+
|
| 46 |
+
// βββ Intent ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 47 |
+
|
| 48 |
+
export interface WorkflowIntent {
|
| 49 |
+
workflowType: string;
|
| 50 |
+
requiresAI: boolean;
|
| 51 |
+
integrations: string[];
|
| 52 |
+
riskLevel: RiskLevel;
|
| 53 |
+
requiresHumanApproval: boolean;
|
| 54 |
+
syncVsAsync: 'sync' | 'async' | 'hybrid';
|
| 55 |
+
scalingRequirements: 'low' | 'medium' | 'high';
|
| 56 |
+
identifiedRisks: string[];
|
| 57 |
+
estimatedComplexity: 'simple' | 'moderate' | 'complex';
|
| 58 |
+
domain: WorkflowDomain;
|
| 59 |
+
triggerType?: string; // Auto-detected trigger type
|
| 60 |
+
triggerHint?: string; // Why this trigger was detected
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
// βββ Architecture Plan ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 64 |
+
|
| 65 |
+
export interface ExecutionPlanStep {
|
| 66 |
+
step: number;
|
| 67 |
+
layer: string;
|
| 68 |
+
nodes: string[];
|
| 69 |
+
description: string;
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
export interface Risk {
|
| 73 |
+
type: string;
|
| 74 |
+
probability: RiskLevel;
|
| 75 |
+
impact: RiskLevel;
|
| 76 |
+
description: string;
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
export interface RiskAnalysis {
|
| 80 |
+
overallRisk: RiskLevel;
|
| 81 |
+
risks: Risk[];
|
| 82 |
+
mitigations: string[];
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
export interface TradeoffExplanation {
|
| 86 |
+
option: string;
|
| 87 |
+
pros: string[];
|
| 88 |
+
cons: string[];
|
| 89 |
+
recommendation: string;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
export interface QualityTargets {
|
| 93 |
+
reliability: number;
|
| 94 |
+
performance: string;
|
| 95 |
+
observability: string;
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
export interface SubworkflowPlan {
|
| 99 |
+
name: string;
|
| 100 |
+
trigger: string;
|
| 101 |
+
purpose: string;
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
export interface DependencyAnalysis {
|
| 105 |
+
external: string[];
|
| 106 |
+
credentials: string[];
|
| 107 |
+
requiredPermissions: string[];
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
export interface WorkflowArchitecturePlan {
|
| 111 |
+
name: string;
|
| 112 |
+
description: string;
|
| 113 |
+
triggerNode: string;
|
| 114 |
+
triggerDescription: string;
|
| 115 |
+
layers: ExecutionPlanStep[];
|
| 116 |
+
nodeList: Array<{
|
| 117 |
+
displayName: string;
|
| 118 |
+
n8nNodeType: string;
|
| 119 |
+
layer: string;
|
| 120 |
+
purpose: string;
|
| 121 |
+
isCritical: boolean;
|
| 122 |
+
requiresCredential: string | null;
|
| 123 |
+
keyParameters?: Record<string, unknown>;
|
| 124 |
+
}>;
|
| 125 |
+
dataFlow: string[];
|
| 126 |
+
riskAnalysis: RiskAnalysis;
|
| 127 |
+
estimatedNodes: number;
|
| 128 |
+
complexity: 'simple' | 'moderate' | 'complex';
|
| 129 |
+
qualityTargets?: QualityTargets;
|
| 130 |
+
tradeoffs?: TradeoffExplanation[];
|
| 131 |
+
subworkflows?: SubworkflowPlan[];
|
| 132 |
+
dependencyAnalysis?: DependencyAnalysis;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
// βββ Graph (IR) βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 136 |
+
|
| 137 |
+
export interface RetryPolicy {
|
| 138 |
+
maxRetries: number;
|
| 139 |
+
retryDelayMs: number;
|
| 140 |
+
retryOn: string[];
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
export interface DataContract {
|
| 144 |
+
inputs: string[];
|
| 145 |
+
outputs: string[];
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
export interface GraphNode {
|
| 149 |
+
id: string;
|
| 150 |
+
label: string;
|
| 151 |
+
n8nNodeType: string;
|
| 152 |
+
layer: WorkflowLayer;
|
| 153 |
+
description: string;
|
| 154 |
+
isCritical: boolean;
|
| 155 |
+
position: { x: number; y: number };
|
| 156 |
+
retryPolicy?: RetryPolicy;
|
| 157 |
+
fallbackNodeId?: string;
|
| 158 |
+
dataContract?: DataContract;
|
| 159 |
+
keyParameters?: Record<string, unknown>;
|
| 160 |
+
autoConfigured?: boolean;
|
| 161 |
+
webhookPath?: string;
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
export interface GraphEdge {
|
| 165 |
+
id: string;
|
| 166 |
+
sourceNodeId: string;
|
| 167 |
+
targetNodeId: string;
|
| 168 |
+
outputIndex: number;
|
| 169 |
+
inputIndex: number;
|
| 170 |
+
label?: string;
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
export interface WorkflowMetadata {
|
| 174 |
+
domain: string;
|
| 175 |
+
version: string;
|
| 176 |
+
createdAt: string;
|
| 177 |
+
estimatedNodes?: number;
|
| 178 |
+
estimatedDuration?: string;
|
| 179 |
+
riskLevel?: RiskLevel;
|
| 180 |
+
unknownNodesRejected?: string[];
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
export interface WorkflowGraph {
|
| 184 |
+
workflowId: string;
|
| 185 |
+
name: string;
|
| 186 |
+
nodes: GraphNode[];
|
| 187 |
+
edges: GraphEdge[];
|
| 188 |
+
metadata: WorkflowMetadata;
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
// βββ n8n Workflow JSON ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 192 |
+
|
| 193 |
+
export interface N8nNode {
|
| 194 |
+
id: string;
|
| 195 |
+
name: string;
|
| 196 |
+
type: string;
|
| 197 |
+
typeVersion: number;
|
| 198 |
+
position: [number, number];
|
| 199 |
+
parameters: Record<string, unknown>;
|
| 200 |
+
credentials?: Record<string, { id: string; name: string }>;
|
| 201 |
+
onError?: string;
|
| 202 |
+
retryOnFail?: boolean;
|
| 203 |
+
maxTries?: number;
|
| 204 |
+
waitBetweenTries?: number;
|
| 205 |
+
notes?: string;
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
export interface N8nConnections {
|
| 209 |
+
[nodeName: string]: {
|
| 210 |
+
main: Array<Array<{ node: string; type: string; index: number }>>;
|
| 211 |
+
};
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
export interface N8nWorkflow {
|
| 215 |
+
name: string;
|
| 216 |
+
active: false; // ALWAYS false β never auto-activate
|
| 217 |
+
nodes: N8nNode[];
|
| 218 |
+
connections: N8nConnections;
|
| 219 |
+
settings?: {
|
| 220 |
+
executionOrder?: string;
|
| 221 |
+
saveManualExecutions?: boolean;
|
| 222 |
+
timezone?: string;
|
| 223 |
+
errorWorkflow?: string;
|
| 224 |
+
callerPolicy?: string;
|
| 225 |
+
};
|
| 226 |
+
tags?: string[];
|
| 227 |
+
meta?: Record<string, unknown>;
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
// βββ Validation βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 231 |
+
|
| 232 |
+
export interface ValidationIssue {
|
| 233 |
+
id: string;
|
| 234 |
+
severity: 'error' | 'warning' | 'info';
|
| 235 |
+
category: string;
|
| 236 |
+
nodeId?: string;
|
| 237 |
+
message: string;
|
| 238 |
+
suggestion: string;
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
export interface ValidationSection {
|
| 242 |
+
passed: boolean;
|
| 243 |
+
score: number;
|
| 244 |
+
issues: ValidationIssue[];
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
export interface ValidationReport {
|
| 248 |
+
jobId: string;
|
| 249 |
+
valid: boolean;
|
| 250 |
+
overallScore: number;
|
| 251 |
+
schemaValidation: ValidationSection;
|
| 252 |
+
credentialValidation: ValidationSection;
|
| 253 |
+
graphValidation: ValidationSection;
|
| 254 |
+
expressionValidation: ValidationSection;
|
| 255 |
+
reliabilityValidation: ValidationSection;
|
| 256 |
+
nodeRegistryValidation?: ValidationSection; // NEW
|
| 257 |
+
dataFlowValidation?: ValidationSection; // NEW
|
| 258 |
+
issues: ValidationIssue[]; // errors only
|
| 259 |
+
warnings: ValidationIssue[];
|
| 260 |
+
validatedAt: string;
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
// βββ Simulation βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 264 |
+
|
| 265 |
+
export interface ExecutionTraceStep {
|
| 266 |
+
stepNumber: number;
|
| 267 |
+
nodeId: string;
|
| 268 |
+
nodeName: string;
|
| 269 |
+
nodeType: string;
|
| 270 |
+
status: 'success' | 'warning' | 'failure';
|
| 271 |
+
inputData: Record<string, unknown>;
|
| 272 |
+
outputData: Record<string, unknown>;
|
| 273 |
+
executionTimeMs: number;
|
| 274 |
+
issues?: string[];
|
| 275 |
+
notes?: string;
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
export interface FailurePoint {
|
| 279 |
+
nodeId: string;
|
| 280 |
+
nodeName: string;
|
| 281 |
+
failureType: string;
|
| 282 |
+
probability: RiskLevel;
|
| 283 |
+
description: string;
|
| 284 |
+
mitigation: string;
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
export interface ChaosTestResult {
|
| 288 |
+
scenario: 'timeout' | 'null_data' | 'malformed_input' | 'expired_credentials' | 'rate_limit' | 'network_error';
|
| 289 |
+
passed: boolean;
|
| 290 |
+
affectedNode: string;
|
| 291 |
+
description: string;
|
| 292 |
+
resilient: boolean;
|
| 293 |
+
fallbackActivated: boolean;
|
| 294 |
+
}
|
| 295 |
+
|
| 296 |
+
export interface SimulationReport {
|
| 297 |
+
jobId: string;
|
| 298 |
+
passed: boolean;
|
| 299 |
+
readinessScore: number;
|
| 300 |
+
executionTrace: ExecutionTraceStep[];
|
| 301 |
+
riskAnalysis: RiskAnalysis;
|
| 302 |
+
predictedFailurePoints: FailurePoint[];
|
| 303 |
+
chaosTestResults: ChaosTestResult[];
|
| 304 |
+
deploymentBlockers?: string[]; // NEW
|
| 305 |
+
recommendations?: string[]; // NEW
|
| 306 |
+
mockDataUsed: Record<string, unknown>;
|
| 307 |
+
simulatedAt: string;
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
// βββ Credentials βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 311 |
+
|
| 312 |
+
export interface RequiredCredential {
|
| 313 |
+
nodeId: string;
|
| 314 |
+
nodeName: string;
|
| 315 |
+
credentialType: string;
|
| 316 |
+
credentialDisplayName?: string;
|
| 317 |
+
required: boolean;
|
| 318 |
+
}
|
| 319 |
+
|
| 320 |
+
export interface AvailableCredential {
|
| 321 |
+
id: string;
|
| 322 |
+
name: string;
|
| 323 |
+
type: string;
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
+
export interface MissingCredential {
|
| 327 |
+
credentialType: string;
|
| 328 |
+
requiredByNodes: string[];
|
| 329 |
+
setupInstructions: string;
|
| 330 |
+
}
|
| 331 |
+
|
| 332 |
+
export interface CredentialAnalysis {
|
| 333 |
+
jobId: string;
|
| 334 |
+
allCredentialsPresent: boolean;
|
| 335 |
+
n8nReachable?: boolean; // NEW
|
| 336 |
+
required: RequiredCredential[];
|
| 337 |
+
available: AvailableCredential[];
|
| 338 |
+
missing: MissingCredential[];
|
| 339 |
+
analysedAt: string;
|
| 340 |
+
}
|
| 341 |
+
|
| 342 |
+
// βββ Quality Scoring βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 343 |
+
|
| 344 |
+
export interface DeploymentReadiness {
|
| 345 |
+
deploymentReady: boolean;
|
| 346 |
+
confidenceScore: number;
|
| 347 |
+
riskLevel: RiskLevel;
|
| 348 |
+
blockingIssues: string[];
|
| 349 |
+
recommendations: string[];
|
| 350 |
+
}
|
| 351 |
+
|
| 352 |
+
export interface WorkflowQualityScore {
|
| 353 |
+
overallScore: number;
|
| 354 |
+
reliabilityScore: number;
|
| 355 |
+
maintainabilityScore: number;
|
| 356 |
+
observabilityScore: number;
|
| 357 |
+
retryCoverage: number;
|
| 358 |
+
expressionSafety: number;
|
| 359 |
+
credentialReadiness: number;
|
| 360 |
+
branchingQuality: number;
|
| 361 |
+
deploymentSafety: number;
|
| 362 |
+
riskLevel: RiskLevel;
|
| 363 |
+
scoreExplanations: Record<string, string>;
|
| 364 |
+
}
|
| 365 |
+
|
| 366 |
+
// βββ Deployment βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 367 |
+
|
| 368 |
+
export interface DeploymentResult {
|
| 369 |
+
jobId: string;
|
| 370 |
+
n8nWorkflowId?: string;
|
| 371 |
+
status: 'success' | 'failed';
|
| 372 |
+
deployedAt: string;
|
| 373 |
+
errorMessage?: string;
|
| 374 |
+
}
|
| 375 |
+
|
| 376 |
+
export interface ActivationResult {
|
| 377 |
+
jobId: string;
|
| 378 |
+
n8nWorkflowId: string;
|
| 379 |
+
activationStatus: 'activated' | 'failed';
|
| 380 |
+
finalValidationPassed: boolean;
|
| 381 |
+
finalCredentialCheckPassed: boolean;
|
| 382 |
+
activatedAt: string;
|
| 383 |
+
diagnostics?: string;
|
| 384 |
+
}
|
| 385 |
+
|
| 386 |
+
export interface ApprovalRequest {
|
| 387 |
+
jobId: string;
|
| 388 |
+
requestedAt: string;
|
| 389 |
+
expiresAt: string;
|
| 390 |
+
architectureSummary: string;
|
| 391 |
+
riskSummary: string;
|
| 392 |
+
readinessScore: number;
|
| 393 |
+
validationScore: number;
|
| 394 |
+
simulationScore: number;
|
| 395 |
+
credentialStatus: 'complete' | 'incomplete';
|
| 396 |
+
status: 'pending' | 'approved' | 'rejected';
|
| 397 |
+
reviewerNote?: string;
|
| 398 |
+
respondedAt?: string;
|
| 399 |
+
}
|
| 400 |
+
|
| 401 |
+
// βββ Audit ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 402 |
+
|
| 403 |
+
export interface AuditEvent {
|
| 404 |
+
id: string;
|
| 405 |
+
jobId: string;
|
| 406 |
+
event: string;
|
| 407 |
+
actor: 'user' | 'ai' | 'system';
|
| 408 |
+
timestamp: string;
|
| 409 |
+
data?: Record<string, unknown>;
|
| 410 |
+
}
|
| 411 |
+
|
| 412 |
+
// βββ Swarm Result Summary βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 413 |
+
|
| 414 |
+
export interface SwarmResultSummary {
|
| 415 |
+
swarmSize: number;
|
| 416 |
+
generatedAt: string;
|
| 417 |
+
winnerStrategy: string;
|
| 418 |
+
winnerScore: number;
|
| 419 |
+
candidateScores: Array<{ id: string; strategy: string; score: number }>;
|
| 420 |
+
selectionReason?: string;
|
| 421 |
+
}
|
| 422 |
+
|
| 423 |
+
// βββ Trigger Detection ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 424 |
+
|
| 425 |
+
export interface TriggerDetectionResult {
|
| 426 |
+
nodeType: string;
|
| 427 |
+
triggerType: string;
|
| 428 |
+
confidence: 'high' | 'medium' | 'low';
|
| 429 |
+
webhookPath?: string;
|
| 430 |
+
notes: string;
|
| 431 |
+
}
|
| 432 |
+
|
| 433 |
+
// βββ Self-Healing Summary βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 434 |
+
|
| 435 |
+
export interface HealingResultSummary {
|
| 436 |
+
healed: boolean;
|
| 437 |
+
actionsApplied: Array<{
|
| 438 |
+
type: string;
|
| 439 |
+
targetNodeId?: string;
|
| 440 |
+
description: string;
|
| 441 |
+
applied: boolean;
|
| 442 |
+
}>;
|
| 443 |
+
remainingIssues: string[];
|
| 444 |
+
confidence: number;
|
| 445 |
+
}
|
| 446 |
+
|
| 447 |
+
// βββ Job βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 448 |
+
|
| 449 |
+
export interface WorkflowJob {
|
| 450 |
+
id: string;
|
| 451 |
+
userRequest: string;
|
| 452 |
+
state: WorkflowLifecycleState;
|
| 453 |
+
createdAt: string;
|
| 454 |
+
updatedAt: string;
|
| 455 |
+
intent?: WorkflowIntent;
|
| 456 |
+
architecturePlan?: WorkflowArchitecturePlan;
|
| 457 |
+
graph?: WorkflowGraph;
|
| 458 |
+
compiledWorkflow?: N8nWorkflow;
|
| 459 |
+
validationReport?: ValidationReport;
|
| 460 |
+
simulationReport?: SimulationReport;
|
| 461 |
+
credentialAnalysis?: CredentialAnalysis;
|
| 462 |
+
deploymentResult?: DeploymentResult;
|
| 463 |
+
activationResult?: ActivationResult;
|
| 464 |
+
approvalRequest?: ApprovalRequest;
|
| 465 |
+
qualityScore?: WorkflowQualityScore; // NEW
|
| 466 |
+
healingResult?: HealingResultSummary; // NEW
|
| 467 |
+
swarmResult?: SwarmResultSummary; // NEW
|
| 468 |
+
triggerDetection?: TriggerDetectionResult; // NEW
|
| 469 |
+
auditLog: AuditEvent[];
|
| 470 |
+
error?: string;
|
| 471 |
+
}
|
| 472 |
+
|
| 473 |
+
// βββ API Response βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 474 |
+
|
| 475 |
+
export interface GenerateWorkflowResponse {
|
| 476 |
+
success: boolean;
|
| 477 |
+
jobId: string;
|
| 478 |
+
state: 'generated' | 'validated' | 'simulated';
|
| 479 |
+
message: string;
|
| 480 |
+
architecturePlan?: WorkflowArchitecturePlan;
|
| 481 |
+
graph?: WorkflowGraph;
|
| 482 |
+
compiledWorkflow?: N8nWorkflow;
|
| 483 |
+
validationReport?: ValidationReport;
|
| 484 |
+
simulationReport?: SimulationReport;
|
| 485 |
+
credentialAnalysis?: CredentialAnalysis;
|
| 486 |
+
deploymentReadiness?: DeploymentReadiness;
|
| 487 |
+
qualityScore?: WorkflowQualityScore;
|
| 488 |
+
healingResult?: HealingResultSummary;
|
| 489 |
+
swarmResult?: SwarmResultSummary;
|
| 490 |
+
triggerDetection?: TriggerDetectionResult;
|
| 491 |
+
memoryStats?: {
|
| 492 |
+
totalPatterns: number;
|
| 493 |
+
successPatterns: number;
|
| 494 |
+
failurePatterns: number;
|
| 495 |
+
templates: number;
|
| 496 |
+
avgSuccessScore: number;
|
| 497 |
+
fixStrategies: number;
|
| 498 |
+
};
|
| 499 |
+
}
|