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