Files
jerapah-flow/packages/server/workflow-http-routes.js
T
nsrbandCursor db884808f2 refactor: split oversized modules and start server src/stores layout
Consolidate format helpers under lib/format, split React Query hooks by domain, extract AuthPicker/ConfigRefHint and HTTP trigger routing, demote file-private exports, and move KV/secrets/variables/profiles/http-auths stores under src/stores with root re-exports.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-22 06:16:38 +07:00

160 lines
5.0 KiB
JavaScript

import { HTTP_METHODS } from "@jerapah-flow/shared";
import {
checkAnyHttpAuth,
resolveAuthMechanisms,
resolveUnauthorizedSpec,
sendHttpPageOrJson,
sendSuccessPage,
} from "./http-trigger-auth.js";
/**
* Rebuild METHOD+path → workflow map from loaded workflows.
*
* @param {Map<string, { owner: string, workflow: any }>} workflows
* @param {Map<string, { key: string, owner: string, trigger: any }>} httpRoutes
* @param {{
* namespacedPath: (owner: string, path: unknown) => string,
* log: { debug: Function, warn: Function },
* }} deps
*/
export function rebuildHttpRoutes(workflows, httpRoutes, { namespacedPath, log }) {
httpRoutes.clear();
for (const [key, { owner, workflow }] of workflows) {
if (workflow.enabled === false) {
log.debug(`Skipping disabled workflow HTTP triggers (${key})`);
continue;
}
for (const trigger of workflow.triggers ?? []) {
if (trigger.type !== "HTTP") continue;
const method = String(trigger.method ?? "POST").toUpperCase();
const url = namespacedPath(owner, trigger.path);
const routeKey = `${method} ${url}`;
if (httpRoutes.has(routeKey)) {
log.warn(`Skipping duplicate HTTP trigger ${routeKey} (${key})`);
continue;
}
httpRoutes.set(routeKey, { key, owner, trigger });
log.debug(`Mapped HTTP trigger ${routeKey} (${key})`);
}
}
}
/**
* Register the /u/* Fastify wildcard once; subsequent rebuilds only refresh the map.
*
* @param {import("fastify").FastifyInstance} server
* @param {(req: import("fastify").FastifyRequest, reply: import("fastify").FastifyReply) => any} handler
* @param {{ registered: boolean }} state
* @param {{ log: { debug: Function } }} deps
*/
export function ensureHttpWildcardRoute(server, handler, state, { log }) {
if (state.registered) return;
state.registered = true;
server.route({
method: HTTP_METHODS,
url: "/u/*",
handler,
});
log.debug("Registered HTTP trigger wildcard dispatcher /u/*");
}
/**
* Build the HTTP trigger request handler bound to registry state.
*
* @param {{
* httpRoutes: Map<string, { key: string, owner: string, trigger: any }>,
* workflows: Map<string, { owner: string, workflow: any }>,
* namespacedPath: (owner: string, path: unknown) => string,
* enqueueWorkflow: (key: string, ctx: any, trigger: any) => Promise<any>,
* }} deps
*/
export function createHttpTriggerHandler({
httpRoutes,
workflows,
namespacedPath,
enqueueWorkflow,
}) {
/**
* @param {import("fastify").FastifyRequest} req
* @param {import("fastify").FastifyReply} reply
*/
return async function dispatchHttpTrigger(req, reply) {
const wildcard = /** @type {{ "*": string }} */ (req.params)["*"] ?? "";
const url = `/u/${String(wildcard).replace(/^\/+/, "")}`;
const method = String(req.method ?? "GET").toUpperCase();
const routeKey = `${method} ${url}`;
const mapped = httpRoutes.get(routeKey);
if (!mapped) {
return reply.code(404).send({ error: "not found" });
}
const entry = workflows.get(mapped.key);
if (!entry || entry.workflow?.enabled === false) {
return reply.code(404).send({ error: "workflow disabled" });
}
// Prefer live trigger from current workflow YAML (auth/response edits)
const liveTrigger =
(entry.workflow.triggers ?? []).find((t) => {
if (t?.type !== "HTTP") return false;
const m = String(t.method ?? "POST").toUpperCase();
const p = namespacedPath(entry.owner, t.path);
return m === method && p === url;
}) ?? mapped.trigger;
if (
liveTrigger.auth != null &&
liveTrigger.auth !== false &&
!(Array.isArray(liveTrigger.auth) && liveTrigger.auth.length === 0)
) {
const mechanisms = await resolveAuthMechanisms(liveTrigger.auth);
if (mechanisms.length === 0) {
const { status, pageName } = resolveUnauthorizedSpec(liveTrigger, null);
return sendHttpPageOrJson(reply, status, pageName, {
error: "unauthorized",
});
}
const ok = await checkAnyHttpAuth(req, mechanisms, {
owner: entry.owner,
workflowKey: mapped.key,
});
if (!ok) {
const { status, pageName } = resolveUnauthorizedSpec(
liveTrigger,
mechanisms[0],
);
return sendHttpPageOrJson(reply, status, pageName, {
error: "unauthorized",
});
}
}
const result = await enqueueWorkflow(
mapped.key,
{ data: req.body },
{ type: "http", detail: `${method} ${url}` },
);
if (result.status === "failed") {
return reply.code(result.runId ? 500 : 404).send({
runId: result.runId,
status: result.status,
error: result.error,
});
}
const defaultBody = {
runId: result.runId,
status: result.status,
};
if (typeof liveTrigger.response === "string" && liveTrigger.response) {
return sendSuccessPage(reply, liveTrigger.response, defaultBody);
}
return reply.code(202).send(defaultBody);
};
}