@invect/express 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/index.cjs +10 -0
- package/dist/index.d.cts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/middleware/index.cjs +24 -0
- package/dist/middleware/index.cjs.map +1 -0
- package/dist/middleware/index.d.cts +15 -0
- package/dist/middleware/index.d.cts.map +1 -0
- package/dist/middleware/index.d.ts +15 -0
- package/dist/middleware/index.d.ts.map +1 -0
- package/dist/middleware/index.js +22 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/router/index.cjs +1106 -0
- package/dist/router/index.cjs.map +1 -0
- package/dist/router/index.d.cts +19 -0
- package/dist/router/index.d.cts.map +1 -0
- package/dist/router/index.d.ts +19 -0
- package/dist/router/index.d.ts.map +1 -0
- package/dist/router/index.js +1105 -0
- package/dist/router/index.js.map +1 -0
- package/package.json +86 -0
|
@@ -0,0 +1,1106 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_middleware_index = require("../middleware/index.cjs");
|
|
3
|
+
let express = require("express");
|
|
4
|
+
let _invect_core = require("@invect/core");
|
|
5
|
+
let zod = require("zod");
|
|
6
|
+
//#region src/invect-router.ts
|
|
7
|
+
function createPluginDatabaseApi(core) {
|
|
8
|
+
const connection = core.getDatabaseConnection();
|
|
9
|
+
const normalizeSql = (statement) => {
|
|
10
|
+
if (connection.type !== "postgresql") return statement;
|
|
11
|
+
let index = 0;
|
|
12
|
+
return statement.replace(/\?/g, () => `$${++index}`);
|
|
13
|
+
};
|
|
14
|
+
const query = async (statement, params = []) => {
|
|
15
|
+
switch (connection.type) {
|
|
16
|
+
case "postgresql": return await connection.db.$client.unsafe(normalizeSql(statement), params);
|
|
17
|
+
case "sqlite": return connection.db.$client.prepare(statement).all(...params);
|
|
18
|
+
case "mysql": {
|
|
19
|
+
const [rows] = await connection.db.$client.execute(statement, params);
|
|
20
|
+
return Array.isArray(rows) ? rows : [];
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
throw new Error(`Unsupported database type: ${String(connection.type)}`);
|
|
24
|
+
};
|
|
25
|
+
return {
|
|
26
|
+
type: connection.type,
|
|
27
|
+
query,
|
|
28
|
+
async execute(statement, params = []) {
|
|
29
|
+
switch (connection.type) {
|
|
30
|
+
case "postgresql":
|
|
31
|
+
await connection.db.$client.unsafe(normalizeSql(statement), params);
|
|
32
|
+
return;
|
|
33
|
+
case "sqlite": {
|
|
34
|
+
const client = connection.db.$client;
|
|
35
|
+
const coerced = params.map((p) => typeof p === "boolean" ? p ? 1 : 0 : p);
|
|
36
|
+
client.prepare(statement).run(...coerced);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
case "mysql":
|
|
40
|
+
await connection.db.$client.execute(statement, params);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
throw new Error(`Unsupported database type: ${String(connection.type)}`);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function parseParamsFromQuery(queryValue) {
|
|
48
|
+
if (!queryValue) return {};
|
|
49
|
+
if (typeof queryValue === "string") try {
|
|
50
|
+
return JSON.parse(queryValue);
|
|
51
|
+
} catch {
|
|
52
|
+
return {};
|
|
53
|
+
}
|
|
54
|
+
if (Array.isArray(queryValue)) {
|
|
55
|
+
const last = queryValue[queryValue.length - 1];
|
|
56
|
+
if (typeof last === "string") try {
|
|
57
|
+
return JSON.parse(last);
|
|
58
|
+
} catch {
|
|
59
|
+
return {};
|
|
60
|
+
}
|
|
61
|
+
return {};
|
|
62
|
+
}
|
|
63
|
+
if (typeof queryValue === "object") return Object.entries(queryValue).reduce((acc, [key, value]) => {
|
|
64
|
+
acc[key] = Array.isArray(value) ? value[value.length - 1] : value;
|
|
65
|
+
return acc;
|
|
66
|
+
}, {});
|
|
67
|
+
return {};
|
|
68
|
+
}
|
|
69
|
+
function coerceQueryValue(value) {
|
|
70
|
+
if (Array.isArray(value)) return value[value.length - 1];
|
|
71
|
+
return value ?? void 0;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Create Invect Express Router
|
|
75
|
+
*/
|
|
76
|
+
function createInvectRouter(config) {
|
|
77
|
+
const core = new _invect_core.Invect(config);
|
|
78
|
+
core.initialize().then(async () => {
|
|
79
|
+
await core.startBatchPolling();
|
|
80
|
+
console.log("✅ Invect batch polling started");
|
|
81
|
+
await core.startCronScheduler();
|
|
82
|
+
console.log("✅ Invect cron scheduler started");
|
|
83
|
+
}).catch((error) => {
|
|
84
|
+
console.error("Failed to initialize Invect Core:", error);
|
|
85
|
+
});
|
|
86
|
+
const router = (0, express.Router)();
|
|
87
|
+
router.use((req, res, next) => {
|
|
88
|
+
if (!core.isInitialized()) return res.status(503).json({
|
|
89
|
+
error: "Service Unavailable",
|
|
90
|
+
message: "Invect Core is still initializing. Please try again in a moment."
|
|
91
|
+
});
|
|
92
|
+
next();
|
|
93
|
+
});
|
|
94
|
+
/**
|
|
95
|
+
* Auth middleware - resolves identity from host app and attaches to request.
|
|
96
|
+
*
|
|
97
|
+
* The host app provides a `resolveUser` callback in the config that extracts
|
|
98
|
+
* the user identity from the request (e.g., from JWT, session, API key).
|
|
99
|
+
*/
|
|
100
|
+
router.use(async (req, res, next) => {
|
|
101
|
+
try {
|
|
102
|
+
const webRequestUrl = `${req.protocol}://${req.get("host")}${req.originalUrl}`;
|
|
103
|
+
const webRequestInit = {
|
|
104
|
+
method: req.method,
|
|
105
|
+
headers: req.headers
|
|
106
|
+
};
|
|
107
|
+
const webRequest = new globalThis.Request(webRequestUrl, webRequestInit);
|
|
108
|
+
const hookContext = {
|
|
109
|
+
path: req.path,
|
|
110
|
+
method: req.method,
|
|
111
|
+
identity: null
|
|
112
|
+
};
|
|
113
|
+
const hookResult = await core.getPluginHookRunner().runOnRequest(webRequest, hookContext);
|
|
114
|
+
if (hookResult.intercepted && hookResult.response) {
|
|
115
|
+
const arrayBuf = await hookResult.response.arrayBuffer();
|
|
116
|
+
res.status(hookResult.response.status);
|
|
117
|
+
hookResult.response.headers.forEach((value, key) => {
|
|
118
|
+
res.setHeader(key, value);
|
|
119
|
+
});
|
|
120
|
+
return res.send(Buffer.from(arrayBuf));
|
|
121
|
+
}
|
|
122
|
+
req.invectIdentity = hookContext.identity ?? null;
|
|
123
|
+
} catch (error) {
|
|
124
|
+
console.error("Auth resolution error:", error);
|
|
125
|
+
req.invectIdentity = null;
|
|
126
|
+
}
|
|
127
|
+
next();
|
|
128
|
+
});
|
|
129
|
+
/**
|
|
130
|
+
* Create an authorization middleware for a specific permission.
|
|
131
|
+
* When useFlowAccessTable is enabled, looks up flow access from database.
|
|
132
|
+
*/
|
|
133
|
+
function requirePermission(permission, getResourceId) {
|
|
134
|
+
return require_middleware_index.asyncHandler(async (req, res, next) => {
|
|
135
|
+
const identity = req.invectIdentity ?? null;
|
|
136
|
+
const resourceId = getResourceId ? getResourceId(req) : void 0;
|
|
137
|
+
const resourceType = permission.split(":")[0];
|
|
138
|
+
if (core.isFlowAccessTableEnabled() && identity && resourceId && [
|
|
139
|
+
"flow",
|
|
140
|
+
"flow-version",
|
|
141
|
+
"flow-run",
|
|
142
|
+
"node-execution"
|
|
143
|
+
].includes(resourceType)) {
|
|
144
|
+
if (core.hasPermission(identity, "admin:*")) return next();
|
|
145
|
+
if (!await core.hasFlowAccess(resourceId, identity.id, identity.teamIds || [], getRequiredFlowPermission(permission))) return res.status(403).json({
|
|
146
|
+
error: "Forbidden",
|
|
147
|
+
message: `No access to flow '${resourceId}'`
|
|
148
|
+
});
|
|
149
|
+
return next();
|
|
150
|
+
} else {
|
|
151
|
+
const result = await core.authorize({
|
|
152
|
+
identity,
|
|
153
|
+
action: permission,
|
|
154
|
+
resource: resourceId ? {
|
|
155
|
+
type: resourceType,
|
|
156
|
+
id: resourceId
|
|
157
|
+
} : void 0
|
|
158
|
+
});
|
|
159
|
+
if (!result.allowed) {
|
|
160
|
+
const status = identity ? 403 : 401;
|
|
161
|
+
return res.status(status).json({
|
|
162
|
+
error: status === 403 ? "Forbidden" : "Unauthorized",
|
|
163
|
+
message: result.reason || "Access denied"
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
next();
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Map permission to required flow access level.
|
|
172
|
+
*/
|
|
173
|
+
function getRequiredFlowPermission(permission) {
|
|
174
|
+
if (permission.includes(":delete")) return "owner";
|
|
175
|
+
if (permission.startsWith("flow-run:") || permission === "node:test") return "operator";
|
|
176
|
+
if (permission.includes(":create") || permission.includes(":update") || permission.includes(":publish")) return "editor";
|
|
177
|
+
return "viewer";
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* GET /dashboard/stats - Get dashboard statistics
|
|
181
|
+
* Core method: ✅ getDashboardStats()
|
|
182
|
+
* Returns: DashboardStats (flow counts, run counts by status, recent activity)
|
|
183
|
+
*/
|
|
184
|
+
router.get("/dashboard/stats", require_middleware_index.asyncHandler(async (_req, res) => {
|
|
185
|
+
const stats = await core.getDashboardStats();
|
|
186
|
+
res.json(stats);
|
|
187
|
+
}));
|
|
188
|
+
/**
|
|
189
|
+
* GET /flows/list - List all flows (simple GET endpoint)
|
|
190
|
+
* Core method: ✅ listFlows()
|
|
191
|
+
* Permission: flow:read
|
|
192
|
+
*/
|
|
193
|
+
router.get("/flows/list", requirePermission("flow:read"), require_middleware_index.asyncHandler(async (_req, res) => {
|
|
194
|
+
const flows = await core.listFlows();
|
|
195
|
+
res.json(flows);
|
|
196
|
+
}));
|
|
197
|
+
/**
|
|
198
|
+
* POST /flows/list - List flows with optional filtering and pagination
|
|
199
|
+
* Core method: ✅ listFlows(options?: QueryOptions<Flow>)
|
|
200
|
+
* Body: QueryOptions<Flow>
|
|
201
|
+
* Permission: flow:read
|
|
202
|
+
*/
|
|
203
|
+
router.post("/flows/list", requirePermission("flow:read"), require_middleware_index.asyncHandler(async (req, res) => {
|
|
204
|
+
const flows = await core.listFlows(req.body);
|
|
205
|
+
res.json(flows);
|
|
206
|
+
}));
|
|
207
|
+
/**
|
|
208
|
+
* POST /flows - Create a new flow
|
|
209
|
+
* Core method: ✅ createFlow(flowData: CreateFlowRequest)
|
|
210
|
+
* Permission: flow:create
|
|
211
|
+
*/
|
|
212
|
+
router.post("/flows", requirePermission("flow:create"), require_middleware_index.asyncHandler(async (req, res) => {
|
|
213
|
+
const flow = await core.createFlow(req.body);
|
|
214
|
+
res.status(201).json(flow);
|
|
215
|
+
}));
|
|
216
|
+
/**
|
|
217
|
+
* GET /flows/:id - Get flow by ID
|
|
218
|
+
* Core method: ✅ getFlow(flowId: string)
|
|
219
|
+
* Permission: flow:read (with resource check)
|
|
220
|
+
*/
|
|
221
|
+
router.get("/flows/:id", requirePermission("flow:read", (req) => req.params.id), require_middleware_index.asyncHandler(async (req, res) => {
|
|
222
|
+
const flow = await core.getFlow(req.params.id);
|
|
223
|
+
res.json(flow);
|
|
224
|
+
}));
|
|
225
|
+
/**
|
|
226
|
+
* PUT /flows/:id - Update flow
|
|
227
|
+
* Core method: ✅ updateFlow(flowId: string, updateData: UpdateFlowInput)
|
|
228
|
+
* Permission: flow:update (with resource check)
|
|
229
|
+
*/
|
|
230
|
+
router.put("/flows/:id", requirePermission("flow:update", (req) => req.params.id), require_middleware_index.asyncHandler(async (req, res) => {
|
|
231
|
+
const flow = await core.updateFlow(req.params.id, req.body);
|
|
232
|
+
res.json(flow);
|
|
233
|
+
}));
|
|
234
|
+
/**
|
|
235
|
+
* DELETE /flows/:id - Delete flow
|
|
236
|
+
* Core method: ✅ deleteFlow(flowId: string)
|
|
237
|
+
* Permission: flow:delete (with resource check)
|
|
238
|
+
*/
|
|
239
|
+
router.delete("/flows/:id", requirePermission("flow:delete", (req) => req.params.id), require_middleware_index.asyncHandler(async (req, res) => {
|
|
240
|
+
await core.deleteFlow(req.params.id);
|
|
241
|
+
res.status(204).send();
|
|
242
|
+
}));
|
|
243
|
+
/**
|
|
244
|
+
* POST /validate-flow - Validate flow definition
|
|
245
|
+
* Core method: ✅ validateFlowDefinition(flowId: string, flowDefinition: InvectDefinition)
|
|
246
|
+
* Permission: flow:read
|
|
247
|
+
*/
|
|
248
|
+
router.post("/validate-flow", requirePermission("flow:read"), require_middleware_index.asyncHandler(async (req, res) => {
|
|
249
|
+
const { flowId, flowDefinition } = req.body;
|
|
250
|
+
const result = await core.validateFlowDefinition(flowId, flowDefinition);
|
|
251
|
+
res.json(result);
|
|
252
|
+
}));
|
|
253
|
+
/**
|
|
254
|
+
* GET /flows/:flowId/react-flow - Get flow data in React Flow format
|
|
255
|
+
* Core method: ✅ renderToReactFlow(flowId: string, options)
|
|
256
|
+
* Query params: version, flowRunId
|
|
257
|
+
*/
|
|
258
|
+
router.get("/flows/:flowId/react-flow", require_middleware_index.asyncHandler(async (req, res) => {
|
|
259
|
+
const queryParams = req.query;
|
|
260
|
+
const options = {};
|
|
261
|
+
if (queryParams.version) options.version = queryParams.version;
|
|
262
|
+
if (queryParams.flowRunId) options.flowRunId = queryParams.flowRunId;
|
|
263
|
+
const result = await core.renderToReactFlow(req.params.flowId, options);
|
|
264
|
+
res.json(result);
|
|
265
|
+
}));
|
|
266
|
+
/**
|
|
267
|
+
* POST /flows/:id/versions/list - Get flow versions with optional filtering and pagination
|
|
268
|
+
* Core method: ✅ listFlowVersions(flowId: string, options?: QueryOptions<FlowVersion>)
|
|
269
|
+
* Body: QueryOptions<FlowVersion>
|
|
270
|
+
*/
|
|
271
|
+
router.post("/flows/:id/versions/list", require_middleware_index.asyncHandler(async (req, res) => {
|
|
272
|
+
const versions = await core.listFlowVersions(req.params.id, req.body);
|
|
273
|
+
res.json(versions);
|
|
274
|
+
}));
|
|
275
|
+
/**
|
|
276
|
+
* POST /flows/:id/versions - Create flow version
|
|
277
|
+
* Core method: ✅ createFlowVersion(flowId: string, versionData: CreateFlowVersionRequest)
|
|
278
|
+
* Permission: flow-version:create (with resource check on flow)
|
|
279
|
+
*/
|
|
280
|
+
router.post("/flows/:id/versions", requirePermission("flow-version:create", (req) => req.params.id), require_middleware_index.asyncHandler(async (req, res) => {
|
|
281
|
+
const version = await core.createFlowVersion(req.params.id, req.body);
|
|
282
|
+
res.status(201).json(version);
|
|
283
|
+
}));
|
|
284
|
+
/**
|
|
285
|
+
* GET /flows/:id/versions/:version - Get specific flow version (supports 'latest')
|
|
286
|
+
* Core method: ✅ getFlowVersion(flowId: string, version: string | number | "latest")
|
|
287
|
+
* Permission: flow-version:read (with resource check on flow)
|
|
288
|
+
*/
|
|
289
|
+
router.get("/flows/:id/versions/:version", requirePermission("flow-version:read", (req) => req.params.id), require_middleware_index.asyncHandler(async (req, res) => {
|
|
290
|
+
const version = await core.getFlowVersion(req.params.id, req.params.version);
|
|
291
|
+
if (!version) return res.status(404).json({
|
|
292
|
+
error: "Not Found",
|
|
293
|
+
message: `Version ${req.params.version} not found for flow ${req.params.id}`
|
|
294
|
+
});
|
|
295
|
+
res.json(version);
|
|
296
|
+
}));
|
|
297
|
+
/**
|
|
298
|
+
* POST /flows/:flowId/run - Start flow execution (async - returns immediately)
|
|
299
|
+
* Core method: ✅ startFlowRunAsync(flowId: string, inputs: FlowInputs, options?: ExecuteFlowOptions)
|
|
300
|
+
* Returns immediately with flow run ID. The flow executes in the background.
|
|
301
|
+
* Permission: flow-run:create (with resource check on flow)
|
|
302
|
+
*/
|
|
303
|
+
router.post("/flows/:flowId/run", requirePermission("flow-run:create", (req) => req.params.flowId), require_middleware_index.asyncHandler(async (req, res) => {
|
|
304
|
+
const { inputs = {}, options } = req.body;
|
|
305
|
+
const result = await core.startFlowRunAsync(req.params.flowId, inputs, options);
|
|
306
|
+
res.status(201).json(result);
|
|
307
|
+
}));
|
|
308
|
+
/**
|
|
309
|
+
* POST /flows/:flowId/run-to-node/:nodeId - Execute flow up to a specific node
|
|
310
|
+
* Only executes the upstream nodes required to produce output for the target node.
|
|
311
|
+
* Core method: ✅ executeFlowToNode(flowId, targetNodeId, inputs, options)
|
|
312
|
+
* Permission: flow-run:create (with resource check on flow)
|
|
313
|
+
*/
|
|
314
|
+
router.post("/flows/:flowId/run-to-node/:nodeId", requirePermission("flow-run:create", (req) => req.params.flowId), require_middleware_index.asyncHandler(async (req, res) => {
|
|
315
|
+
const { inputs = {}, options } = req.body;
|
|
316
|
+
const result = await core.executeFlowToNode(req.params.flowId, req.params.nodeId, inputs, options);
|
|
317
|
+
res.status(201).json(result);
|
|
318
|
+
}));
|
|
319
|
+
/**
|
|
320
|
+
* POST /flow-runs/list - Get all flow runs with optional filtering and pagination
|
|
321
|
+
* Core method: ✅ listFlowRuns(options?: QueryOptions<FlowRun>)
|
|
322
|
+
* Body: QueryOptions<FlowRun>
|
|
323
|
+
* Permission: flow-run:read
|
|
324
|
+
*/
|
|
325
|
+
router.post("/flow-runs/list", requirePermission("flow-run:read"), require_middleware_index.asyncHandler(async (req, res) => {
|
|
326
|
+
const flowRuns = await core.listFlowRuns(req.body);
|
|
327
|
+
res.json(flowRuns);
|
|
328
|
+
}));
|
|
329
|
+
/**
|
|
330
|
+
* GET /flow-runs/:flowRunId - Get specific flow run by ID
|
|
331
|
+
* Core method: ✅ getFlowRunById(flowRunId: string)
|
|
332
|
+
*/
|
|
333
|
+
router.get("/flow-runs/:flowRunId", require_middleware_index.asyncHandler(async (req, res) => {
|
|
334
|
+
const flowRun = await core.getFlowRunById(req.params.flowRunId);
|
|
335
|
+
res.json(flowRun);
|
|
336
|
+
}));
|
|
337
|
+
/**
|
|
338
|
+
* GET /flows/:flowId/flow-runs - Get flow runs for a specific flow
|
|
339
|
+
* Core method: ✅ listFlowRunsByFlowId(flowId: string)
|
|
340
|
+
*/
|
|
341
|
+
router.get("/flows/:flowId/flow-runs", require_middleware_index.asyncHandler(async (req, res) => {
|
|
342
|
+
const flowRuns = await core.listFlowRunsByFlowId(req.params.flowId);
|
|
343
|
+
res.json(flowRuns);
|
|
344
|
+
}));
|
|
345
|
+
/**
|
|
346
|
+
* POST /flow-runs/:flowRunId/resume - Resume paused flow execution
|
|
347
|
+
* Core method: ✅ resumeExecution(executionId: string)
|
|
348
|
+
*/
|
|
349
|
+
router.post("/flow-runs/:flowRunId/resume", require_middleware_index.asyncHandler(async (req, res) => {
|
|
350
|
+
const result = await core.resumeExecution(req.params.flowRunId);
|
|
351
|
+
res.json(result);
|
|
352
|
+
}));
|
|
353
|
+
/**
|
|
354
|
+
* POST /flow-runs/:flowRunId/cancel - Cancel flow execution
|
|
355
|
+
* Core method: ✅ cancelFlowRun(flowRunId: string)
|
|
356
|
+
*/
|
|
357
|
+
router.post("/flow-runs/:flowRunId/cancel", require_middleware_index.asyncHandler(async (req, res) => {
|
|
358
|
+
const result = await core.cancelFlowRun(req.params.flowRunId);
|
|
359
|
+
res.json(result);
|
|
360
|
+
}));
|
|
361
|
+
/**
|
|
362
|
+
* POST /flow-runs/:flowRunId/pause - Pause flow execution
|
|
363
|
+
* Core method: ✅ pauseFlowRun(flowRunId: string, reason?: string)
|
|
364
|
+
*/
|
|
365
|
+
router.post("/flow-runs/:flowRunId/pause", require_middleware_index.asyncHandler(async (req, res) => {
|
|
366
|
+
const { reason } = req.body;
|
|
367
|
+
const result = await core.pauseFlowRun(req.params.flowRunId, reason);
|
|
368
|
+
res.json(result);
|
|
369
|
+
}));
|
|
370
|
+
/**
|
|
371
|
+
* GET /flow-runs/:flowRunId/node-executions - Get node executions for a flow run
|
|
372
|
+
* Core method: ✅ getNodeExecutionsByRunId(flowRunId: string)
|
|
373
|
+
*/
|
|
374
|
+
router.get("/flow-runs/:flowRunId/node-executions", require_middleware_index.asyncHandler(async (req, res) => {
|
|
375
|
+
const nodeExecutions = await core.getNodeExecutionsByRunId(req.params.flowRunId);
|
|
376
|
+
res.json(nodeExecutions);
|
|
377
|
+
}));
|
|
378
|
+
/**
|
|
379
|
+
* GET /flow-runs/:flowRunId/stream - SSE stream of execution events
|
|
380
|
+
* Core method: ✅ createFlowRunEventStream(flowRunId: string)
|
|
381
|
+
*
|
|
382
|
+
* Streams node-execution and flow-run updates in real time.
|
|
383
|
+
* First event is a "snapshot", then incremental updates, ending with "end".
|
|
384
|
+
*/
|
|
385
|
+
router.get("/flow-runs/:flowRunId/stream", require_middleware_index.asyncHandler(async (req, res) => {
|
|
386
|
+
res.setHeader("Content-Type", "text/event-stream");
|
|
387
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
388
|
+
res.setHeader("Connection", "keep-alive");
|
|
389
|
+
res.setHeader("X-Accel-Buffering", "no");
|
|
390
|
+
res.flushHeaders();
|
|
391
|
+
try {
|
|
392
|
+
const stream = core.createFlowRunEventStream(req.params.flowRunId);
|
|
393
|
+
for await (const event of stream) {
|
|
394
|
+
if (res.destroyed) break;
|
|
395
|
+
const data = JSON.stringify(event);
|
|
396
|
+
res.write(`event: ${event.type}\ndata: ${data}\n\n`);
|
|
397
|
+
}
|
|
398
|
+
} catch (error) {
|
|
399
|
+
const message = error instanceof Error ? error.message : "Stream failed";
|
|
400
|
+
if (res.headersSent) res.write(`event: error\ndata: ${JSON.stringify({
|
|
401
|
+
type: "error",
|
|
402
|
+
message
|
|
403
|
+
})}\n\n`);
|
|
404
|
+
else return res.status(500).json({
|
|
405
|
+
error: "Internal Server Error",
|
|
406
|
+
message
|
|
407
|
+
});
|
|
408
|
+
} finally {
|
|
409
|
+
res.end();
|
|
410
|
+
}
|
|
411
|
+
}));
|
|
412
|
+
/**
|
|
413
|
+
* POST /node-executions/list - Get all node executions with optional filtering and pagination
|
|
414
|
+
* Core method: ✅ listNodeExecutions(options?: QueryOptions<NodeExecution>)
|
|
415
|
+
* Body: QueryOptions<NodeExecution>
|
|
416
|
+
*/
|
|
417
|
+
router.post("/node-executions/list", require_middleware_index.asyncHandler(async (req, res) => {
|
|
418
|
+
const nodeExecutions = await core.listNodeExecutions(req.body);
|
|
419
|
+
res.json(nodeExecutions);
|
|
420
|
+
}));
|
|
421
|
+
/**
|
|
422
|
+
* POST /node-data/sql-query - Execute SQL query for testing
|
|
423
|
+
* Core method: ✅ executeSqlQuery(request: SubmitSQLQueryRequest)
|
|
424
|
+
*/
|
|
425
|
+
router.post("/node-data/sql-query", require_middleware_index.asyncHandler(async (req, res) => {
|
|
426
|
+
const result = await core.executeSqlQuery(req.body);
|
|
427
|
+
res.json(result);
|
|
428
|
+
}));
|
|
429
|
+
/**
|
|
430
|
+
* POST /node-data/test-expression - Test a JS expression in the QuickJS sandbox
|
|
431
|
+
* Core method: ✅ testJsExpression({ expression, context })
|
|
432
|
+
*/
|
|
433
|
+
router.post("/node-data/test-expression", require_middleware_index.asyncHandler(async (req, res) => {
|
|
434
|
+
const result = await core.testJsExpression(req.body);
|
|
435
|
+
res.json(result);
|
|
436
|
+
}));
|
|
437
|
+
/**
|
|
438
|
+
* POST /node-data/test-mapper - Test a mapper expression with mode semantics
|
|
439
|
+
* Core method: ✅ testMapper({ expression, incomingData, mode? })
|
|
440
|
+
*/
|
|
441
|
+
router.post("/node-data/test-mapper", require_middleware_index.asyncHandler(async (req, res) => {
|
|
442
|
+
const result = await core.testMapper(req.body);
|
|
443
|
+
res.json(result);
|
|
444
|
+
}));
|
|
445
|
+
/**
|
|
446
|
+
* POST /node-data/model-query - Test model prompt
|
|
447
|
+
* Core method: ✅ testModelPrompt(request: SubmitPromptRequest)
|
|
448
|
+
*/
|
|
449
|
+
router.post("/node-data/model-query", require_middleware_index.asyncHandler(async (req, res) => {
|
|
450
|
+
const result = await core.testModelPrompt(req.body);
|
|
451
|
+
res.json(result);
|
|
452
|
+
}));
|
|
453
|
+
/**
|
|
454
|
+
* GET /node-data/models - Get available AI models
|
|
455
|
+
* Core method: ✅ getAvailableModels()
|
|
456
|
+
*/
|
|
457
|
+
router.get("/node-data/models", require_middleware_index.asyncHandler(async (req, res) => {
|
|
458
|
+
const credentialId = typeof req.query.credentialId === "string" ? req.query.credentialId.trim() : "";
|
|
459
|
+
const providerQuery = typeof req.query.provider === "string" ? req.query.provider.trim().toUpperCase() : "";
|
|
460
|
+
if (credentialId) {
|
|
461
|
+
const response = await core.getModelsForCredential(credentialId);
|
|
462
|
+
res.json(response);
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
if (providerQuery) {
|
|
466
|
+
if (!Object.values(_invect_core.BatchProvider).includes(providerQuery)) {
|
|
467
|
+
res.status(400).json({
|
|
468
|
+
error: "INVALID_PROVIDER",
|
|
469
|
+
message: `Unsupported provider '${providerQuery}'. Expected one of: ${Object.values(_invect_core.BatchProvider).join(", ")}`
|
|
470
|
+
});
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
const provider = providerQuery;
|
|
474
|
+
const response = await core.getModelsForProvider(provider);
|
|
475
|
+
res.json(response);
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
const models = await core.getAvailableModels();
|
|
479
|
+
res.json(models);
|
|
480
|
+
}));
|
|
481
|
+
/**
|
|
482
|
+
* GET /node-data/databases - Get available databases
|
|
483
|
+
* Core method: ✅ getAvailableDatabases()
|
|
484
|
+
*/
|
|
485
|
+
router.get("/node-data/databases", require_middleware_index.asyncHandler(async (req, res) => {
|
|
486
|
+
const databases = core.getAvailableDatabases();
|
|
487
|
+
res.json(databases);
|
|
488
|
+
}));
|
|
489
|
+
/**
|
|
490
|
+
* POST /node-config/update - Generic node configuration updates
|
|
491
|
+
* Core method: ✅ handleNodeConfigUpdate(event: NodeConfigUpdateEvent)
|
|
492
|
+
*/
|
|
493
|
+
router.post("/node-config/update", require_middleware_index.asyncHandler(async (req, res) => {
|
|
494
|
+
const response = await core.handleNodeConfigUpdate(req.body);
|
|
495
|
+
res.json(response);
|
|
496
|
+
}));
|
|
497
|
+
router.get("/node-definition/:nodeType", require_middleware_index.asyncHandler(async (req, res) => {
|
|
498
|
+
const rawNodeType = req.params.nodeType ?? "";
|
|
499
|
+
const nodeTypeParam = rawNodeType.includes(".") ? rawNodeType : rawNodeType.toUpperCase();
|
|
500
|
+
const isLegacyEnum = !rawNodeType.includes(".") && nodeTypeParam in _invect_core.GraphNodeType;
|
|
501
|
+
const isActionId = rawNodeType.includes(".");
|
|
502
|
+
if (!isLegacyEnum && !isActionId) {
|
|
503
|
+
res.status(400).json({
|
|
504
|
+
error: "INVALID_NODE_TYPE",
|
|
505
|
+
message: `Unknown node type '${req.params.nodeType}'`
|
|
506
|
+
});
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
const params = parseParamsFromQuery(req.query.params);
|
|
510
|
+
const changeField = typeof req.query.changeField === "string" ? req.query.changeField : void 0;
|
|
511
|
+
const changeValue = coerceQueryValue(req.query.changeValue);
|
|
512
|
+
const nodeId = typeof req.query.nodeId === "string" ? req.query.nodeId : void 0;
|
|
513
|
+
const flowId = typeof req.query.flowId === "string" ? req.query.flowId : void 0;
|
|
514
|
+
const response = await core.handleNodeConfigUpdate({
|
|
515
|
+
nodeType: nodeTypeParam,
|
|
516
|
+
nodeId: nodeId ?? `definition-${nodeTypeParam.toLowerCase()}`,
|
|
517
|
+
flowId,
|
|
518
|
+
params,
|
|
519
|
+
change: changeField ? {
|
|
520
|
+
field: changeField,
|
|
521
|
+
value: changeValue
|
|
522
|
+
} : void 0
|
|
523
|
+
});
|
|
524
|
+
res.json(response);
|
|
525
|
+
}));
|
|
526
|
+
/**
|
|
527
|
+
* GET /nodes - Get available node definitions
|
|
528
|
+
* Core method: ✅ getAvailableNodes()
|
|
529
|
+
*/
|
|
530
|
+
router.get("/nodes", require_middleware_index.asyncHandler(async (req, res) => {
|
|
531
|
+
const nodes = core.getAvailableNodes();
|
|
532
|
+
res.json(nodes);
|
|
533
|
+
}));
|
|
534
|
+
/**
|
|
535
|
+
* GET /actions/:actionId/fields/:fieldName/options - Load dynamic field options
|
|
536
|
+
* Core method: ✅ resolveFieldOptions(actionId, fieldName, deps)
|
|
537
|
+
*
|
|
538
|
+
* Query params:
|
|
539
|
+
* deps - JSON-encoded object of dependency field values
|
|
540
|
+
*/
|
|
541
|
+
router.get("/actions/:actionId/fields/:fieldName/options", require_middleware_index.asyncHandler(async (req, res) => {
|
|
542
|
+
const { actionId, fieldName } = req.params;
|
|
543
|
+
let dependencyValues = {};
|
|
544
|
+
if (typeof req.query.deps === "string") try {
|
|
545
|
+
dependencyValues = JSON.parse(req.query.deps);
|
|
546
|
+
} catch {
|
|
547
|
+
res.status(400).json({ error: "Invalid deps JSON" });
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
const result = await core.resolveFieldOptions(actionId, fieldName, dependencyValues);
|
|
551
|
+
res.json(result);
|
|
552
|
+
}));
|
|
553
|
+
/**
|
|
554
|
+
* POST /nodes/test - Test/execute a single node in isolation
|
|
555
|
+
* Core method: ✅ testNode(nodeType, params, inputData)
|
|
556
|
+
* Body: { nodeType: string, params: Record<string, unknown>, inputData?: Record<string, unknown> }
|
|
557
|
+
*/
|
|
558
|
+
router.post("/nodes/test", require_middleware_index.asyncHandler(async (req, res) => {
|
|
559
|
+
const { nodeType, params, inputData } = req.body;
|
|
560
|
+
if (!nodeType || typeof nodeType !== "string") return res.status(400).json({ error: "nodeType is required and must be a string" });
|
|
561
|
+
if (!params || typeof params !== "object") return res.status(400).json({ error: "params is required and must be an object" });
|
|
562
|
+
const result = await core.testNode(nodeType, params, inputData || {});
|
|
563
|
+
res.json(result);
|
|
564
|
+
}));
|
|
565
|
+
/**
|
|
566
|
+
* POST /credentials - Create a new credential
|
|
567
|
+
* Core method: ✅ createCredential(input: CreateCredentialInput)
|
|
568
|
+
* Permission: credential:create
|
|
569
|
+
*/
|
|
570
|
+
router.post("/credentials", requirePermission("credential:create"), require_middleware_index.asyncHandler(async (req, res) => {
|
|
571
|
+
const resolvedUserId = req.invectIdentity?.id || req.user?.id || req.body.userId || req.header("x-user-id") || "anonymous";
|
|
572
|
+
const credential = await core.createCredential({
|
|
573
|
+
...req.body,
|
|
574
|
+
userId: resolvedUserId
|
|
575
|
+
});
|
|
576
|
+
res.status(201).json(credential);
|
|
577
|
+
}));
|
|
578
|
+
/**
|
|
579
|
+
* GET /credentials - List credentials with optional filtering and pagination
|
|
580
|
+
* Core method: ✅ listCredentials(filters?: CredentialFilters, options?: QueryOptions)
|
|
581
|
+
* Query params: type?, authType?, isActive?, page?, limit?
|
|
582
|
+
* Permission: credential:read
|
|
583
|
+
*/
|
|
584
|
+
router.get("/credentials", requirePermission("credential:read"), require_middleware_index.asyncHandler(async (req, res) => {
|
|
585
|
+
const filters = {
|
|
586
|
+
type: req.query.type,
|
|
587
|
+
authType: req.query.authType,
|
|
588
|
+
isActive: req.query.isActive === "true" ? true : req.query.isActive === "false" ? false : void 0
|
|
589
|
+
};
|
|
590
|
+
const credentials = await core.listCredentials(filters);
|
|
591
|
+
res.json(credentials);
|
|
592
|
+
}));
|
|
593
|
+
/**
|
|
594
|
+
* GET /credentials/:id - Get credential by ID
|
|
595
|
+
* Core method: ✅ getCredential(id: string)
|
|
596
|
+
* Permission: credential:read (with resource check)
|
|
597
|
+
*/
|
|
598
|
+
router.get("/credentials/:id", requirePermission("credential:read", (req) => req.params.id), require_middleware_index.asyncHandler(async (req, res) => {
|
|
599
|
+
const credential = await core.getCredential(req.params.id);
|
|
600
|
+
res.json(credential);
|
|
601
|
+
}));
|
|
602
|
+
/**
|
|
603
|
+
* PUT /credentials/:id - Update credential
|
|
604
|
+
* Core method: ✅ updateCredential(id: string, input: UpdateCredentialInput)
|
|
605
|
+
* Permission: credential:update (with resource check)
|
|
606
|
+
*/
|
|
607
|
+
router.put("/credentials/:id", requirePermission("credential:update", (req) => req.params.id), require_middleware_index.asyncHandler(async (req, res) => {
|
|
608
|
+
const credential = await core.updateCredential(req.params.id, req.body);
|
|
609
|
+
res.json(credential);
|
|
610
|
+
}));
|
|
611
|
+
/**
|
|
612
|
+
* DELETE /credentials/:id - Delete credential
|
|
613
|
+
* Core method: ✅ deleteCredential(id: string)
|
|
614
|
+
* Permission: credential:delete (with resource check)
|
|
615
|
+
*/
|
|
616
|
+
router.delete("/credentials/:id", requirePermission("credential:delete", (req) => req.params.id), require_middleware_index.asyncHandler(async (req, res) => {
|
|
617
|
+
await core.deleteCredential(req.params.id);
|
|
618
|
+
res.status(204).send();
|
|
619
|
+
}));
|
|
620
|
+
/**
|
|
621
|
+
* POST /credentials/:id/test - Test credential validity
|
|
622
|
+
* Core method: ✅ testCredential(id: string)
|
|
623
|
+
* Permission: credential:read (with resource check)
|
|
624
|
+
*/
|
|
625
|
+
router.post("/credentials/:id/test", requirePermission("credential:read", (req) => req.params.id), require_middleware_index.asyncHandler(async (req, res) => {
|
|
626
|
+
const result = await core.testCredential(req.params.id);
|
|
627
|
+
res.json(result);
|
|
628
|
+
}));
|
|
629
|
+
/**
|
|
630
|
+
* POST /credentials/:id/track-usage - Update credential last used timestamp
|
|
631
|
+
* Core method: ✅ updateCredentialLastUsed(id: string)
|
|
632
|
+
* Permission: credential:read (with resource check)
|
|
633
|
+
*/
|
|
634
|
+
router.post("/credentials/:id/track-usage", requirePermission("credential:read", (req) => req.params.id), require_middleware_index.asyncHandler(async (req, res) => {
|
|
635
|
+
await core.updateCredentialLastUsed(req.params.id);
|
|
636
|
+
res.status(204).send();
|
|
637
|
+
}));
|
|
638
|
+
/**
|
|
639
|
+
* GET /credentials/:id/webhook - Get webhook info for a credential
|
|
640
|
+
* Core method: ✅ CredentialsService.getWebhookInfo(id)
|
|
641
|
+
* Permission: credential:read (with resource check)
|
|
642
|
+
*/
|
|
643
|
+
router.get("/credentials/:id/webhook", requirePermission("credential:read", (req) => req.params.id), require_middleware_index.asyncHandler(async (req, res) => {
|
|
644
|
+
const webhookInfo = await core.getCredentialsService().getWebhookInfo(req.params.id);
|
|
645
|
+
if (!webhookInfo) return res.status(404).json({ error: "Webhook not enabled for credential" });
|
|
646
|
+
res.json(webhookInfo);
|
|
647
|
+
}));
|
|
648
|
+
/**
|
|
649
|
+
* GET /credentials/:id/webhook-info - Backwards-compatible alias for webhook info
|
|
650
|
+
*/
|
|
651
|
+
router.get("/credentials/:id/webhook-info", requirePermission("credential:read", (req) => req.params.id), require_middleware_index.asyncHandler(async (req, res) => {
|
|
652
|
+
const webhookInfo = await core.getCredentialsService().getWebhookInfo(req.params.id);
|
|
653
|
+
if (!webhookInfo) return res.status(404).json({ error: "Webhook not enabled for credential" });
|
|
654
|
+
res.json(webhookInfo);
|
|
655
|
+
}));
|
|
656
|
+
/**
|
|
657
|
+
* POST /credentials/:id/webhook - Enable webhook for a credential
|
|
658
|
+
* Core method: ✅ CredentialsService.enableWebhook(id)
|
|
659
|
+
* Permission: credential:update (with resource check)
|
|
660
|
+
*/
|
|
661
|
+
router.post("/credentials/:id/webhook", requirePermission("credential:update", (req) => req.params.id), require_middleware_index.asyncHandler(async (req, res) => {
|
|
662
|
+
const webhookInfo = await core.getCredentialsService().enableWebhook(req.params.id);
|
|
663
|
+
res.json(webhookInfo);
|
|
664
|
+
}));
|
|
665
|
+
/**
|
|
666
|
+
* POST /credentials/:id/webhook/enable - Backwards-compatible alias for enabling webhooks
|
|
667
|
+
*/
|
|
668
|
+
router.post("/credentials/:id/webhook/enable", requirePermission("credential:update", (req) => req.params.id), require_middleware_index.asyncHandler(async (req, res) => {
|
|
669
|
+
const webhookInfo = await core.getCredentialsService().enableWebhook(req.params.id);
|
|
670
|
+
res.json(webhookInfo);
|
|
671
|
+
}));
|
|
672
|
+
/**
|
|
673
|
+
* POST /webhooks/credentials/:webhookPath - Public credential webhook ingestion endpoint
|
|
674
|
+
* Core methods: ✅ CredentialsService.findByWebhookPath(webhookPath), updateCredentialLastUsed(id)
|
|
675
|
+
*/
|
|
676
|
+
router.post("/webhooks/credentials/:webhookPath", require_middleware_index.asyncHandler(async (req, res) => {
|
|
677
|
+
const credential = await core.getCredentialsService().findByWebhookPath(req.params.webhookPath);
|
|
678
|
+
if (!credential) return res.status(404).json({
|
|
679
|
+
ok: false,
|
|
680
|
+
error: "Credential webhook not found"
|
|
681
|
+
});
|
|
682
|
+
res.json({
|
|
683
|
+
ok: true,
|
|
684
|
+
credentialId: credential.id,
|
|
685
|
+
triggeredFlows: 0,
|
|
686
|
+
runs: [],
|
|
687
|
+
body: req.body ?? null
|
|
688
|
+
});
|
|
689
|
+
}));
|
|
690
|
+
/**
|
|
691
|
+
* GET /credentials/expiring - Get credentials expiring soon
|
|
692
|
+
* Core method: ✅ getExpiringCredentials(daysUntilExpiry?: number)
|
|
693
|
+
* Query params: daysUntilExpiry (default: 7)
|
|
694
|
+
* Permission: credential:read
|
|
695
|
+
*/
|
|
696
|
+
router.get("/credentials/expiring", requirePermission("credential:read"), require_middleware_index.asyncHandler(async (req, res) => {
|
|
697
|
+
const daysUntilExpiry = req.query.daysUntilExpiry ? parseInt(req.query.daysUntilExpiry) : 7;
|
|
698
|
+
const credentials = await core.getExpiringCredentials(daysUntilExpiry);
|
|
699
|
+
res.json(credentials);
|
|
700
|
+
}));
|
|
701
|
+
/**
|
|
702
|
+
* POST /credentials/test-request - Test a credential by making an HTTP request
|
|
703
|
+
* This endpoint proxies HTTP requests to avoid CORS issues when testing credentials
|
|
704
|
+
* Body: { url, method, headers, body }
|
|
705
|
+
*/
|
|
706
|
+
router.post("/credentials/test-request", require_middleware_index.asyncHandler(async (req, res) => {
|
|
707
|
+
const { url, method = "GET", headers = {}, body } = req.body;
|
|
708
|
+
if (!url) {
|
|
709
|
+
res.status(400).json({ error: "URL is required" });
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
try {
|
|
713
|
+
const fetchOptions = {
|
|
714
|
+
method,
|
|
715
|
+
headers
|
|
716
|
+
};
|
|
717
|
+
if (body && [
|
|
718
|
+
"POST",
|
|
719
|
+
"PUT",
|
|
720
|
+
"PATCH"
|
|
721
|
+
].includes(method)) fetchOptions.body = typeof body === "string" ? body : JSON.stringify(body);
|
|
722
|
+
const response = await fetch(url, fetchOptions);
|
|
723
|
+
const responseText = await response.text();
|
|
724
|
+
let responseBody;
|
|
725
|
+
try {
|
|
726
|
+
responseBody = JSON.parse(responseText);
|
|
727
|
+
} catch {
|
|
728
|
+
responseBody = responseText;
|
|
729
|
+
}
|
|
730
|
+
res.json({
|
|
731
|
+
status: response.status,
|
|
732
|
+
statusText: response.statusText,
|
|
733
|
+
ok: response.ok,
|
|
734
|
+
body: responseBody
|
|
735
|
+
});
|
|
736
|
+
} catch (error) {
|
|
737
|
+
res.status(500).json({
|
|
738
|
+
error: "Request failed",
|
|
739
|
+
message: error instanceof Error ? error.message : "Unknown error"
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
}));
|
|
743
|
+
/**
|
|
744
|
+
* GET /credentials/oauth2/providers - List all available OAuth2 providers
|
|
745
|
+
*/
|
|
746
|
+
router.get("/credentials/oauth2/providers", require_middleware_index.asyncHandler(async (_req, res) => {
|
|
747
|
+
const providers = core.getOAuth2Providers();
|
|
748
|
+
res.json(providers);
|
|
749
|
+
}));
|
|
750
|
+
/**
|
|
751
|
+
* GET /credentials/oauth2/providers/:providerId - Get a specific OAuth2 provider
|
|
752
|
+
*/
|
|
753
|
+
router.get("/credentials/oauth2/providers/:providerId", require_middleware_index.asyncHandler(async (req, res) => {
|
|
754
|
+
const provider = core.getOAuth2Provider(req.params.providerId);
|
|
755
|
+
if (!provider) {
|
|
756
|
+
res.status(404).json({ error: "OAuth2 provider not found" });
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
res.json(provider);
|
|
760
|
+
}));
|
|
761
|
+
/**
|
|
762
|
+
* POST /credentials/oauth2/start - Start OAuth2 authorization flow
|
|
763
|
+
* Body: { providerId, clientId, clientSecret, redirectUri, scopes?, returnUrl?, credentialName? }
|
|
764
|
+
* Returns: { authorizationUrl, state }
|
|
765
|
+
*/
|
|
766
|
+
router.post("/credentials/oauth2/start", require_middleware_index.asyncHandler(async (req, res) => {
|
|
767
|
+
const { providerId, clientId, clientSecret, redirectUri, scopes, returnUrl, credentialName } = req.body;
|
|
768
|
+
if (!providerId || !clientId || !clientSecret || !redirectUri) {
|
|
769
|
+
res.status(400).json({ error: "Missing required fields: providerId, clientId, clientSecret, redirectUri" });
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
const result = core.startOAuth2Flow(providerId, {
|
|
773
|
+
clientId,
|
|
774
|
+
clientSecret,
|
|
775
|
+
redirectUri
|
|
776
|
+
}, {
|
|
777
|
+
scopes,
|
|
778
|
+
returnUrl,
|
|
779
|
+
credentialName
|
|
780
|
+
});
|
|
781
|
+
res.json(result);
|
|
782
|
+
}));
|
|
783
|
+
/**
|
|
784
|
+
* POST /credentials/oauth2/callback - Handle OAuth2 callback and exchange code for tokens
|
|
785
|
+
* Body: { code, state, clientId, clientSecret, redirectUri }
|
|
786
|
+
* Creates a new credential with the obtained tokens
|
|
787
|
+
*/
|
|
788
|
+
router.post("/credentials/oauth2/callback", require_middleware_index.asyncHandler(async (req, res) => {
|
|
789
|
+
const { code, state, clientId, clientSecret, redirectUri } = req.body;
|
|
790
|
+
if (!code || !state || !clientId || !clientSecret || !redirectUri) {
|
|
791
|
+
res.status(400).json({ error: "Missing required fields: code, state, clientId, clientSecret, redirectUri" });
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
const credential = await core.handleOAuth2Callback(code, state, {
|
|
795
|
+
clientId,
|
|
796
|
+
clientSecret,
|
|
797
|
+
redirectUri
|
|
798
|
+
});
|
|
799
|
+
res.json(credential);
|
|
800
|
+
}));
|
|
801
|
+
/**
|
|
802
|
+
* GET /credentials/oauth2/callback - Handle OAuth2 callback (for redirect-based flows)
|
|
803
|
+
* Query: code, state
|
|
804
|
+
* Redirects to the return URL with credential ID or error
|
|
805
|
+
*/
|
|
806
|
+
router.get("/credentials/oauth2/callback", require_middleware_index.asyncHandler(async (req, res) => {
|
|
807
|
+
const { code, state, error, error_description } = req.query;
|
|
808
|
+
if (error) {
|
|
809
|
+
const errorMsg = error_description || error;
|
|
810
|
+
const returnUrl = core.getOAuth2PendingState(state)?.returnUrl || "/";
|
|
811
|
+
const separator = returnUrl.includes("?") ? "&" : "?";
|
|
812
|
+
res.redirect(`${returnUrl}${separator}oauth_error=${encodeURIComponent(errorMsg)}`);
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
if (!code || !state) {
|
|
816
|
+
res.status(400).json({ error: "Missing code or state parameter" });
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
const pendingState = core.getOAuth2PendingState(state);
|
|
820
|
+
if (!pendingState) {
|
|
821
|
+
res.status(400).json({ error: "Invalid or expired OAuth state" });
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
res.json({
|
|
825
|
+
message: "OAuth callback received. Use POST /credentials/oauth2/callback to exchange the code.",
|
|
826
|
+
code,
|
|
827
|
+
state,
|
|
828
|
+
providerId: pendingState.providerId,
|
|
829
|
+
returnUrl: pendingState.returnUrl
|
|
830
|
+
});
|
|
831
|
+
}));
|
|
832
|
+
/**
|
|
833
|
+
* POST /credentials/:id/refresh - Manually refresh an OAuth2 credential's access token
|
|
834
|
+
*/
|
|
835
|
+
router.post("/credentials/:id/refresh", require_middleware_index.asyncHandler(async (req, res) => {
|
|
836
|
+
const credential = await core.refreshOAuth2Credential(req.params.id);
|
|
837
|
+
res.json(credential);
|
|
838
|
+
}));
|
|
839
|
+
/**
|
|
840
|
+
* GET /flows/:flowId/triggers - List all trigger registrations for a flow
|
|
841
|
+
* Core method: ✅ listTriggersForFlow(flowId)
|
|
842
|
+
* Permission: flow:read (with resource check)
|
|
843
|
+
*/
|
|
844
|
+
router.get("/flows/:flowId/triggers", requirePermission("flow:read", (req) => req.params.flowId), require_middleware_index.asyncHandler(async (req, res) => {
|
|
845
|
+
const triggers = await core.listTriggersForFlow(req.params.flowId);
|
|
846
|
+
res.json(triggers);
|
|
847
|
+
}));
|
|
848
|
+
/**
|
|
849
|
+
* POST /flows/:flowId/triggers - Create a trigger registration for a flow
|
|
850
|
+
* Core method: ✅ createTrigger(input)
|
|
851
|
+
* Permission: flow:update (with resource check)
|
|
852
|
+
*/
|
|
853
|
+
router.post("/flows/:flowId/triggers", requirePermission("flow:update", (req) => req.params.flowId), require_middleware_index.asyncHandler(async (req, res) => {
|
|
854
|
+
const trigger = await core.createTrigger({
|
|
855
|
+
...req.body,
|
|
856
|
+
flowId: req.params.flowId
|
|
857
|
+
});
|
|
858
|
+
res.status(201).json(trigger);
|
|
859
|
+
}));
|
|
860
|
+
/**
|
|
861
|
+
* POST /flows/:flowId/triggers/sync - Sync triggers from the flow definition
|
|
862
|
+
* Core method: ✅ syncTriggersForFlow(flowId, definition)
|
|
863
|
+
* Permission: flow:update (with resource check)
|
|
864
|
+
*/
|
|
865
|
+
router.post("/flows/:flowId/triggers/sync", requirePermission("flow:update", (req) => req.params.flowId), require_middleware_index.asyncHandler(async (req, res) => {
|
|
866
|
+
const { definition } = req.body;
|
|
867
|
+
const triggers = await core.syncTriggersForFlow(req.params.flowId, definition);
|
|
868
|
+
res.json(triggers);
|
|
869
|
+
}));
|
|
870
|
+
/**
|
|
871
|
+
* GET /triggers/:triggerId - Get a single trigger by ID
|
|
872
|
+
* Core method: ✅ getTrigger(triggerId)
|
|
873
|
+
*/
|
|
874
|
+
router.get("/triggers/:triggerId", require_middleware_index.asyncHandler(async (req, res) => {
|
|
875
|
+
const trigger = await core.getTrigger(req.params.triggerId);
|
|
876
|
+
if (!trigger) return res.status(404).json({
|
|
877
|
+
error: "Not Found",
|
|
878
|
+
message: `Trigger ${req.params.triggerId} not found`
|
|
879
|
+
});
|
|
880
|
+
res.json(trigger);
|
|
881
|
+
}));
|
|
882
|
+
/**
|
|
883
|
+
* PUT /triggers/:triggerId - Update a trigger registration
|
|
884
|
+
* Core method: ✅ updateTrigger(triggerId, input)
|
|
885
|
+
*/
|
|
886
|
+
router.put("/triggers/:triggerId", require_middleware_index.asyncHandler(async (req, res) => {
|
|
887
|
+
const trigger = await core.updateTrigger(req.params.triggerId, req.body);
|
|
888
|
+
if (!trigger) return res.status(404).json({
|
|
889
|
+
error: "Not Found",
|
|
890
|
+
message: `Trigger ${req.params.triggerId} not found`
|
|
891
|
+
});
|
|
892
|
+
res.json(trigger);
|
|
893
|
+
}));
|
|
894
|
+
/**
|
|
895
|
+
* DELETE /triggers/:triggerId - Delete a trigger registration
|
|
896
|
+
* Core method: ✅ deleteTrigger(triggerId)
|
|
897
|
+
*/
|
|
898
|
+
router.delete("/triggers/:triggerId", require_middleware_index.asyncHandler(async (req, res) => {
|
|
899
|
+
await core.deleteTrigger(req.params.triggerId);
|
|
900
|
+
res.status(204).send();
|
|
901
|
+
}));
|
|
902
|
+
/**
|
|
903
|
+
* GET /agent/tools - List all available agent tools
|
|
904
|
+
* Core method: ✅ getAgentTools()
|
|
905
|
+
*/
|
|
906
|
+
router.get("/agent/tools", require_middleware_index.asyncHandler(async (_req, res) => {
|
|
907
|
+
const tools = core.getAgentTools();
|
|
908
|
+
res.json(tools);
|
|
909
|
+
}));
|
|
910
|
+
/**
|
|
911
|
+
* GET /chat/status - Check if chat assistant is enabled
|
|
912
|
+
* Core method: ✅ isChatEnabled()
|
|
913
|
+
*/
|
|
914
|
+
router.get("/chat/status", require_middleware_index.asyncHandler(async (_req, res) => {
|
|
915
|
+
res.json({ enabled: core.isChatEnabled() });
|
|
916
|
+
}));
|
|
917
|
+
/**
|
|
918
|
+
* POST /chat - Streaming chat assistant endpoint (SSE)
|
|
919
|
+
* Core method: ✅ createChatStream()
|
|
920
|
+
*
|
|
921
|
+
* Request body: { messages: ChatMessage[], context: ChatContext }
|
|
922
|
+
* Response: Server-Sent Events stream of ChatStreamEvents
|
|
923
|
+
*/
|
|
924
|
+
router.post("/chat", require_middleware_index.asyncHandler(async (req, res) => {
|
|
925
|
+
const { messages, context } = req.body;
|
|
926
|
+
if (!messages || !Array.isArray(messages)) return res.status(400).json({
|
|
927
|
+
error: "Validation Error",
|
|
928
|
+
message: "\"messages\" must be an array of chat messages"
|
|
929
|
+
});
|
|
930
|
+
res.setHeader("Content-Type", "text/event-stream");
|
|
931
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
932
|
+
res.setHeader("Connection", "keep-alive");
|
|
933
|
+
res.setHeader("X-Accel-Buffering", "no");
|
|
934
|
+
res.flushHeaders();
|
|
935
|
+
const identity = req.__invectIdentity ?? void 0;
|
|
936
|
+
try {
|
|
937
|
+
const stream = await core.createChatStream({
|
|
938
|
+
messages,
|
|
939
|
+
context: context || {},
|
|
940
|
+
identity
|
|
941
|
+
});
|
|
942
|
+
for await (const event of stream) {
|
|
943
|
+
if (res.destroyed) break;
|
|
944
|
+
const data = JSON.stringify(event);
|
|
945
|
+
res.write(`event: ${event.type}\ndata: ${data}\n\n`);
|
|
946
|
+
}
|
|
947
|
+
} catch (error) {
|
|
948
|
+
const message = error instanceof Error ? error.message : "Chat stream failed";
|
|
949
|
+
if (res.headersSent) res.write(`event: error\ndata: ${JSON.stringify({
|
|
950
|
+
type: "error",
|
|
951
|
+
message,
|
|
952
|
+
recoverable: false
|
|
953
|
+
})}\n\n`);
|
|
954
|
+
else return res.status(500).json({
|
|
955
|
+
error: "Internal Server Error",
|
|
956
|
+
message
|
|
957
|
+
});
|
|
958
|
+
} finally {
|
|
959
|
+
res.end();
|
|
960
|
+
}
|
|
961
|
+
}));
|
|
962
|
+
/**
|
|
963
|
+
* GET /chat/messages/:flowId - Get persisted chat messages for a flow
|
|
964
|
+
* Core method: ✅ getChatMessages()
|
|
965
|
+
*/
|
|
966
|
+
router.get("/chat/messages/:flowId", require_middleware_index.asyncHandler(async (req, res) => {
|
|
967
|
+
const messages = await core.getChatMessages(req.params.flowId);
|
|
968
|
+
res.json(messages);
|
|
969
|
+
}));
|
|
970
|
+
/**
|
|
971
|
+
* PUT /chat/messages/:flowId - Save (replace) chat messages for a flow
|
|
972
|
+
* Core method: ✅ saveChatMessages()
|
|
973
|
+
*
|
|
974
|
+
* Request body: { messages: Array<{ role, content, toolMeta? }> }
|
|
975
|
+
*/
|
|
976
|
+
router.put("/chat/messages/:flowId", require_middleware_index.asyncHandler(async (req, res) => {
|
|
977
|
+
const { messages } = req.body;
|
|
978
|
+
if (!messages || !Array.isArray(messages)) return res.status(400).json({
|
|
979
|
+
error: "Validation Error",
|
|
980
|
+
message: "\"messages\" must be an array"
|
|
981
|
+
});
|
|
982
|
+
const saved = await core.saveChatMessages(req.params.flowId, messages);
|
|
983
|
+
res.json(saved);
|
|
984
|
+
}));
|
|
985
|
+
/**
|
|
986
|
+
* DELETE /chat/messages/:flowId - Delete all chat messages for a flow
|
|
987
|
+
* Core method: ✅ deleteChatMessages()
|
|
988
|
+
*/
|
|
989
|
+
router.delete("/chat/messages/:flowId", require_middleware_index.asyncHandler(async (req, res) => {
|
|
990
|
+
await core.deleteChatMessages(req.params.flowId);
|
|
991
|
+
res.json({ success: true });
|
|
992
|
+
}));
|
|
993
|
+
router.all("/plugins/*", require_middleware_index.asyncHandler(async (req, res) => {
|
|
994
|
+
const endpoints = core.getPluginEndpoints();
|
|
995
|
+
const pluginPath = (req.path || "/").replace(/^\/plugins/, "") || "/";
|
|
996
|
+
const method = req.method.toUpperCase();
|
|
997
|
+
const matchedEndpoint = endpoints.find((ep) => {
|
|
998
|
+
if (ep.method !== method) return false;
|
|
999
|
+
const pattern = ep.path.replace(/\*/g, "(.*)").replace(/:([^/]+)/g, "([^/]+)");
|
|
1000
|
+
return new RegExp(`^${pattern}$`).test(pluginPath);
|
|
1001
|
+
});
|
|
1002
|
+
if (!matchedEndpoint) return res.status(404).json({
|
|
1003
|
+
error: "Not Found",
|
|
1004
|
+
message: `Plugin route ${method} ${pluginPath} not found`
|
|
1005
|
+
});
|
|
1006
|
+
const paramNames = [];
|
|
1007
|
+
const paramPattern = matchedEndpoint.path.replace(/\*/g, "(.*)").replace(/:([^/]+)/g, (_m, name) => {
|
|
1008
|
+
paramNames.push(name);
|
|
1009
|
+
return "([^/]+)";
|
|
1010
|
+
});
|
|
1011
|
+
const paramMatch = new RegExp(`^${paramPattern}$`).exec(pluginPath);
|
|
1012
|
+
const params = {};
|
|
1013
|
+
if (paramMatch) paramNames.forEach((name, i) => {
|
|
1014
|
+
params[name] = paramMatch[i + 1] || "";
|
|
1015
|
+
});
|
|
1016
|
+
if (!matchedEndpoint.isPublic && matchedEndpoint.permission) {
|
|
1017
|
+
const identity = req.invectIdentity ?? null;
|
|
1018
|
+
if (!core.hasPermission(identity, matchedEndpoint.permission)) return res.status(403).json({
|
|
1019
|
+
error: "Forbidden",
|
|
1020
|
+
message: `Missing permission: ${matchedEndpoint.permission}`
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
const webRequestUrl = `${req.protocol}://${req.get("host")}${req.originalUrl}`;
|
|
1024
|
+
const webRequestInit = {
|
|
1025
|
+
method: req.method,
|
|
1026
|
+
headers: req.headers
|
|
1027
|
+
};
|
|
1028
|
+
if (req.method !== "GET" && req.method !== "HEAD" && req.method !== "DELETE") webRequestInit.body = JSON.stringify(req.body || {});
|
|
1029
|
+
const webRequest = new globalThis.Request(webRequestUrl, webRequestInit);
|
|
1030
|
+
const result = await matchedEndpoint.handler({
|
|
1031
|
+
body: req.body || {},
|
|
1032
|
+
params,
|
|
1033
|
+
query: req.query || {},
|
|
1034
|
+
headers: req.headers,
|
|
1035
|
+
identity: req.invectIdentity ?? null,
|
|
1036
|
+
database: createPluginDatabaseApi(core),
|
|
1037
|
+
request: webRequest,
|
|
1038
|
+
core: {
|
|
1039
|
+
getPermissions: (identity) => core.getPermissions(identity),
|
|
1040
|
+
getAvailableRoles: () => core.getAvailableRoles(),
|
|
1041
|
+
getResolvedRole: (identity) => core.getAuthService().getResolvedRole(identity),
|
|
1042
|
+
isFlowAccessTableEnabled: () => core.isFlowAccessTableEnabled(),
|
|
1043
|
+
listFlowAccess: (flowId) => core.listFlowAccess(flowId),
|
|
1044
|
+
grantFlowAccess: (input) => core.grantFlowAccess(input),
|
|
1045
|
+
revokeFlowAccess: (accessId) => core.revokeFlowAccess(accessId),
|
|
1046
|
+
getAccessibleFlowIds: (userId, teamIds) => core.getAccessibleFlowIds(userId, teamIds),
|
|
1047
|
+
getFlowPermission: (flowId, userId, teamIds) => core.getFlowPermission(flowId, userId, teamIds),
|
|
1048
|
+
authorize: (context) => core.authorize(context)
|
|
1049
|
+
}
|
|
1050
|
+
});
|
|
1051
|
+
if (result instanceof Response) {
|
|
1052
|
+
const arrayBuf = await result.arrayBuffer();
|
|
1053
|
+
res.status(result.status);
|
|
1054
|
+
result.headers.forEach((value, key) => {
|
|
1055
|
+
if (key.toLowerCase() !== "set-cookie") res.setHeader(key, value);
|
|
1056
|
+
});
|
|
1057
|
+
const setCookies = result.headers.getSetCookie?.();
|
|
1058
|
+
if (setCookies && setCookies.length > 0) res.setHeader("set-cookie", setCookies);
|
|
1059
|
+
res.send(Buffer.from(arrayBuf));
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
if ("stream" in result && result.stream) {
|
|
1063
|
+
res.status(result.status || 200);
|
|
1064
|
+
res.setHeader("Content-Type", "text/event-stream");
|
|
1065
|
+
const reader = result.stream.getReader();
|
|
1066
|
+
const pump = async () => {
|
|
1067
|
+
const { done, value } = await reader.read();
|
|
1068
|
+
if (done) {
|
|
1069
|
+
res.end();
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1072
|
+
res.write(value);
|
|
1073
|
+
await pump();
|
|
1074
|
+
};
|
|
1075
|
+
await pump();
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
const jsonResult = result;
|
|
1079
|
+
res.status(jsonResult.status || 200).json(jsonResult.body);
|
|
1080
|
+
}));
|
|
1081
|
+
router.use((error, _req, res, _next) => {
|
|
1082
|
+
if (error instanceof zod.ZodError) return res.status(400).json({
|
|
1083
|
+
error: "Validation Error",
|
|
1084
|
+
message: "Invalid request data",
|
|
1085
|
+
details: error.errors.map((err) => ({
|
|
1086
|
+
path: err.path.join("."),
|
|
1087
|
+
message: err.message,
|
|
1088
|
+
code: err.code
|
|
1089
|
+
}))
|
|
1090
|
+
});
|
|
1091
|
+
if (error.name === "DatabaseError") return res.status(500).json({
|
|
1092
|
+
error: "Database Error",
|
|
1093
|
+
message: error.message || "A database error occurred"
|
|
1094
|
+
});
|
|
1095
|
+
console.error("Invect Router Error:", error);
|
|
1096
|
+
res.status(500).json({
|
|
1097
|
+
error: "Internal Server Error",
|
|
1098
|
+
message: "An unexpected error occurred"
|
|
1099
|
+
});
|
|
1100
|
+
});
|
|
1101
|
+
return router;
|
|
1102
|
+
}
|
|
1103
|
+
//#endregion
|
|
1104
|
+
exports.createInvectRouter = createInvectRouter;
|
|
1105
|
+
|
|
1106
|
+
//# sourceMappingURL=index.cjs.map
|