mirror of
https://github.com/vercel/workflow.git
synced 2026-09-14 19:59:43 +08:00
141 lines
3.9 KiB
TypeScript
141 lines
3.9 KiB
TypeScript
import { readFileSync } from 'node:fs';
|
|
import { pathToFileURL } from 'node:url';
|
|
import { All, Controller, Get, Post, Req, Res } from '@nestjs/common';
|
|
import { join } from 'pathe';
|
|
|
|
// Module-level state for configuration
|
|
let configuredOutDir: string | null = null;
|
|
|
|
/**
|
|
* Configure the workflow controller with the output directory
|
|
*/
|
|
export function configureWorkflowController(outDir: string): void {
|
|
configuredOutDir = outDir;
|
|
}
|
|
|
|
/**
|
|
* Convert Express/Fastify request to Web API Request
|
|
*/
|
|
function toWebRequest(req: any): Request {
|
|
// Works for both Express and Fastify
|
|
const protocol =
|
|
req.protocol ?? (req.raw?.socket?.encrypted ? 'https' : 'http');
|
|
const host = req.headers.host ?? req.hostname;
|
|
const url = req.originalUrl ?? req.url;
|
|
const fullUrl = `${protocol}://${host}${url}`;
|
|
|
|
const headers = req.headers;
|
|
const method = req.method;
|
|
const body = req.body;
|
|
|
|
return new globalThis.Request(fullUrl, {
|
|
method,
|
|
headers,
|
|
body:
|
|
method !== 'GET' && method !== 'HEAD'
|
|
? body === undefined
|
|
? undefined
|
|
: typeof body === 'string'
|
|
? body
|
|
: JSON.stringify(body)
|
|
: undefined,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Send Web API Response back via Express/Fastify response
|
|
*/
|
|
async function sendWebResponse(
|
|
res: any,
|
|
webResponse: globalThis.Response
|
|
): Promise<void> {
|
|
const status = webResponse.status;
|
|
const headers: Record<string, string> = {};
|
|
webResponse.headers.forEach((value, key) => {
|
|
headers[key] = value;
|
|
});
|
|
const body = await webResponse.text();
|
|
|
|
// Works for both Express and Fastify
|
|
if (typeof res.code === 'function') {
|
|
// Fastify
|
|
res.code(status).headers(headers).send(body);
|
|
} else {
|
|
// Express - use res.end() instead of res.send() to avoid automatic charset addition
|
|
res.status(status);
|
|
for (const [key, value] of Object.entries(headers)) {
|
|
res.setHeader(key, value);
|
|
}
|
|
// Use res.end() to send the body without Express modifying headers
|
|
res.end(body);
|
|
}
|
|
}
|
|
|
|
function getOutDir(): string {
|
|
if (!configuredOutDir) {
|
|
throw new Error(
|
|
'WorkflowController not configured. Call configureWorkflowController first.'
|
|
);
|
|
}
|
|
return configuredOutDir;
|
|
}
|
|
|
|
/**
|
|
* Controller that handles the well-known workflow endpoints.
|
|
* Dynamically imports the generated bundles and handles request/response conversion.
|
|
*/
|
|
@Controller('.well-known/workflow/v1')
|
|
export class WorkflowController {
|
|
@Post('flow')
|
|
async handleFlow(@Req() req: any, @Res() res: any) {
|
|
const outDir = getOutDir();
|
|
// Import step registrations (side effects) before the combined handler
|
|
await import(pathToFileURL(join(outDir, 'steps.mjs')).href);
|
|
const { POST } = await import(
|
|
pathToFileURL(join(outDir, 'workflows.mjs')).href
|
|
);
|
|
const webRequest = toWebRequest(req);
|
|
const webResponse = await POST(webRequest);
|
|
await sendWebResponse(res, webResponse);
|
|
}
|
|
|
|
@All('webhook/:token')
|
|
async handleWebhook(@Req() req: any, @Res() res: any) {
|
|
const outDir = getOutDir();
|
|
const { POST } = await import(
|
|
pathToFileURL(join(outDir, 'webhook.mjs')).href
|
|
);
|
|
const webRequest = toWebRequest(req);
|
|
const webResponse = await POST(webRequest);
|
|
await sendWebResponse(res, webResponse);
|
|
}
|
|
|
|
@Get('manifest.json')
|
|
async handleManifest(@Res() res: any) {
|
|
if (process.env.WORKFLOW_PUBLIC_MANIFEST !== '1') {
|
|
if (typeof res.code === 'function') {
|
|
res.code(404).send('');
|
|
} else {
|
|
res.status(404).end('');
|
|
}
|
|
return;
|
|
}
|
|
const outDir = getOutDir();
|
|
let manifest: string;
|
|
try {
|
|
manifest = readFileSync(join(outDir, 'manifest.json'), 'utf-8');
|
|
} catch {
|
|
if (typeof res.code === 'function') {
|
|
res.code(404).send('');
|
|
} else {
|
|
res.status(404).end('');
|
|
}
|
|
return;
|
|
}
|
|
const webResponse = new Response(manifest, {
|
|
headers: { 'content-type': 'application/json' },
|
|
});
|
|
await sendWebResponse(res, webResponse);
|
|
}
|
|
}
|