import { randomBytes } from "crypto"; import { createReadStream, createWriteStream, unlink } from "fs"; import type { IncomingMessage, Server, ServerResponse } from "http"; import { createServer } from "http"; import { hostname } from "os"; import { parse as parseQuerystring } from "querystring"; import { parse as parseUrl } from "url"; import { promisify } from "util"; import { generateCSharpServerSource } from "@sdkgen/csharp-generator"; import { generateDartClientSource } from "@sdkgen/dart-generator"; import { generateFSharpServerSource } from "@sdkgen/fsharp-generator"; import { generateAndroidClientSource } from "@sdkgen/kotlin-generator"; import type { AstRoot } from "@sdkgen/parser"; import { Base64PrimitiveType, BigIntPrimitiveType, BoolPrimitiveType, BytesPrimitiveType, CnpjPrimitiveType, CpfPrimitiveType, DatePrimitiveType, DateTimePrimitiveType, FloatPrimitiveType, HexPrimitiveType, HtmlPrimitiveType, IntPrimitiveType, MoneyPrimitiveType, OptionalType, RestAnnotation, StringPrimitiveType, UIntPrimitiveType, UuidPrimitiveType, VoidPrimitiveType, XmlPrimitiveType, } from "@sdkgen/parser"; import { PLAYGROUND_PUBLIC_PATH } from "@sdkgen/playground"; import { generateSwiftClientSource } from "@sdkgen/swift-generator"; import { generateBrowserClientSource, generateNodeClientSource, generateNodeServerSource } from "@sdkgen/typescript-generator"; import Busboy from "busboy"; import FileType from "file-type"; import { getClientIp } from "request-ip"; import staticFilesHandler from "serve-handler"; import type { BaseApiConfig } from "./api-config"; import type { Context, ContextReply, ContextRequest } from "./context"; import { decode, encode } from "./encode-decode"; import { Fatal } from "./error"; import { executeRequest } from "./execute"; import { setupSwagger } from "./swagger"; import { has } from "./utils"; export class SdkgenHttpServer<ExtraContextT = unknown> { public httpServer: Server; private readonly headers = new Map<string, string>(); private readonly healthChecks: Array<() => Promise<boolean>> = []; private handlers: Array<{ method: string; matcher: string | RegExp; handler(req: IncomingMessage, res: ServerResponse, body: Buffer): void; }> = []; public dynamicCorsOrigin = true; public introspection = true; public log = (message: string) => { console.log(`${new Date().toISOString()} ${message}`); }; private hasSwagger = false; private ignoredUrlPrefix = ""; private extraContext: ExtraContextT; constructor(public apiConfig: BaseApiConfig<ExtraContextT>, ...maybeExtraContext: {} extends ExtraContextT ? [{}?] : [ExtraContextT]) { this.extraContext = (maybeExtraContext[0] ?? {}) as ExtraContextT; this.httpServer = createServer(this.handleRequest.bind(this)); this.enableCors(); this.attachRestHandlers(); const targetTable = [ ["/targets/android/client.kt", (ast: AstRoot) => generateAndroidClientSource(ast, true)], ["/targets/android/client_without_callbacks.kt", (ast: AstRoot) => generateAndroidClientSource(ast, false)], ["/targets/dotnet/api.cs", generateCSharpServerSource], ["/targets/dotnet/api.fs", generateFSharpServerSource], ["/targets/flutter/client.dart", generateDartClientSource], ["/targets/ios/client.swift", (ast: AstRoot) => generateSwiftClientSource(ast, false)], ["/targets/ios/client-rx.swift", (ast: AstRoot) => generateSwiftClientSource(ast, true)], ["/targets/node/api.ts", generateNodeServerSource], ["/targets/node/client.ts", generateNodeClientSource], ["/targets/web/client.ts", generateBrowserClientSource], ] as const; for (const [path, generateFn] of targetTable) { this.addHttpHandler("GET", path, (_req, res) => { if (!this.introspection) { res.statusCode = 404; res.end(); return; } try { res.setHeader("Content-Type", "application/octet-stream"); res.write(generateFn(this.apiConfig.ast)); } catch (e) { console.error(e); res.statusCode = 500; res.write(`${e}`); } res.end(); }); } this.addHttpHandler("GET", "/ast.json", (_req, res) => { if (!this.introspection) { res.statusCode = 404; res.end(); return; } res.setHeader("Content-Type", "application/json"); res.write(JSON.stringify(apiConfig.astJson)); res.end(); }); this.addHttpHandler("GET", /^\/playground.*/u, (req, res) => { if (!this.introspection) { res.statusCode = 404; res.end(); return; } if (req.url) { req.url = req.url.endsWith("/playground") ? req.url.replace(/\/playground/u, "/index.html") : req.url.replace(/\/playground/u, ""); } staticFilesHandler(req, res, { cleanUrls: false, directoryListing: false, etag: true, public: PLAYGROUND_PUBLIC_PATH, }).catch(e => { console.error(e); res.statusCode = 500; res.write(`${e}`); res.end(); }); }); } registerHealthCheck(healthCheck: () => Promise<boolean>): void { this.healthChecks.push(healthCheck); } ignoreUrlPrefix(urlPrefix: string): void { this.ignoredUrlPrefix = urlPrefix; } async listen(port = 8000): Promise<void> { return new Promise(resolve => { this.httpServer.listen(port, () => { const addr = this.httpServer.address(); let addrString: string | undefined; if (addr === null) { addrString = undefined; } else if (typeof addr === "string") { addrString = addr; } else { addrString = `${addr.address}:${addr.port}`; } if (!addrString) { console.log(`Listening.`); resolve(); return; } console.log(`Listening on ${addrString}`); if (this.introspection) { console.log(`Access the sdkgen Playground at http://${addrString}/playground`); } if (this.hasSwagger) { console.log(`Access the REST API Swagger at http://${addrString}/swagger`); } resolve(); }); }); } async close(): Promise<void> { return promisify(this.httpServer.close.bind(this.httpServer))(); } private enableCors() { this.addHeader("Access-Control-Allow-Methods", "DELETE, HEAD, PUT, POST, PATCH, GET, OPTIONS"); this.addHeader("Access-Control-Allow-Headers", "Content-Type"); this.addHeader("Access-Control-Max-Age", "86400"); } addHeader(header: string, value: string): void { const cleanHeader = header.toLowerCase().trim(); const existing = this.headers.get(cleanHeader); if (existing) { if (!existing.includes(value)) { this.headers.set(cleanHeader, `${existing}, ${value}`); } } else { this.headers.set(cleanHeader, value); } } addHttpHandler(method: string, matcher: string | RegExp, handler: (req: IncomingMessage, res: ServerResponse, body: Buffer) => void): void { this.handlers.push({ handler, matcher, method }); } private findBestHandler(path: string, req: IncomingMessage) { const matchingHandlers = this.handlers .filter(({ method }) => method === req.method) .filter(({ matcher }) => { if (typeof matcher === "string") { return matcher === path; } return matcher.exec(path)?.[0] === path; }) .sort(({ matcher: first }, { matcher: second }) => { // Prefer string matches instead of Regexp matches if (typeof first === "string" && typeof second === "string") { return 0; } else if (typeof first === "string") { return -1; } else if (typeof second === "string") { return 1; } const firstMatch = first.exec(path); const secondMatch = second.exec(path); if (!firstMatch) { return -1; } if (!secondMatch) { return 1; } // Compute how many characters were NOT part of a capture group const firstLength = firstMatch[0].length - firstMatch.slice(1).reduce((acc, cur) => acc + cur.length, 0); const secondLength = secondMatch[0].length - secondMatch.slice(1).reduce((acc, cur) => acc + cur.length, 0); // Prefer the maximum number of non-captured characters return secondLength - firstLength; }); return matchingHandlers.length ? matchingHandlers[0] : null; } private attachRestHandlers() { function escapeRegExp(str: string) { return str.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&"); } for (const op of this.apiConfig.ast.operations) { for (const ann of op.annotations) { if (!(ann instanceof RestAnnotation)) { continue; } if (!this.hasSwagger) { setupSwagger(this); this.hasSwagger = true; } const pathFragments = ann.path.split(/\{\w+\}/u); let pathRegex = "^"; for (let i = 0; i < pathFragments.length; ++i) { if (i > 0) { pathRegex += "([^/]+?)"; } pathRegex += escapeRegExp(pathFragments[i]); } pathRegex += "/?$"; for (const header of ann.headers.keys()) { this.addHeader("Access-Control-Allow-Headers", header.toLowerCase()); } // eslint-disable-next-line @typescript-eslint/no-misused-promises this.addHttpHandler(ann.method, new RegExp(pathRegex, "u"), async (req, res, body) => { try { const args: Record<string, unknown> = {}; const files: ContextRequest["files"] = []; const { pathname, query } = parseUrl(req.url ?? ""); const match = pathname?.match(pathRegex); if (!match) { res.statusCode = 404; return; } const simpleArgs = new Map<string, string | null>(); for (let i = 0; i < ann.pathVariables.length; ++i) { const argName = ann.pathVariables[i]; const argValue = match[i + 1]; simpleArgs.set(argName, argValue); } const parsedQuery = query ? parseQuerystring(query) : {}; for (const argName of ann.queryVariables) { const argValue = parsedQuery[argName] ?? null; if (argValue === null) { continue; } simpleArgs.set(argName, Array.isArray(argValue) ? argValue.join("") : argValue); } for (const [headerName, argName] of ann.headers) { const argValue = req.headers[headerName.toLowerCase()] ?? null; if (argValue === null) { continue; } simpleArgs.set(argName, Array.isArray(argValue) ? argValue.join("") : argValue); } if (!ann.bodyVariable && req.headers["content-type"]?.match(/^application\/x-www-form-urlencoded/iu)) { const parsedBody = parseQuerystring(body.toString()); for (const argName of ann.queryVariables) { const argValue = parsedBody[argName] ?? null; if (argValue === null) { continue; } simpleArgs.set(argName, Array.isArray(argValue) ? argValue.join("") : argValue); } } else if (!ann.bodyVariable && req.headers["content-type"]?.match(/^multipart\/form-data/iu)) { const busboy = Busboy({ headers: req.headers }); const filePromises: Array<Promise<void>> = []; busboy.on("field", (field, value) => { if (ann.queryVariables.includes(field)) { simpleArgs.set(field, `${value}`); } }); busboy.on("file", (_field, stream, info) => { const tempName = randomBytes(32).toString("hex"); const writeStream = createWriteStream(tempName); filePromises.push( new Promise((resolve, reject) => { writeStream.on("error", reject); writeStream.on("close", () => { const contents = createReadStream(tempName); files.push({ contents, name: info.filename }); contents.on("open", () => { unlink(tempName, err => { if (err) { reject(err); } else { resolve(); } }); }); }); writeStream.on("open", () => { stream.pipe(writeStream); }); }), ); }); await new Promise((resolve, reject) => { busboy.on("finish", resolve); busboy.on("error", reject); busboy.write(body); }); await Promise.all(filePromises); } else if (ann.bodyVariable) { const argName = ann.bodyVariable; const arg = op.args.find(x => x.name === argName); if (/application\/json/iu.test(req.headers["content-type"] ?? "")) { args[argName] = JSON.parse(body.toString()); } else if (arg) { let { type } = arg; let solved = false; if (type instanceof OptionalType) { if (body.length === 0) { args[argName] = null; solved = true; } else { type = type.base; } } if (!solved) { if ( type instanceof BoolPrimitiveType || type instanceof IntPrimitiveType || type instanceof UIntPrimitiveType || type instanceof FloatPrimitiveType || type instanceof StringPrimitiveType || type instanceof DatePrimitiveType || type instanceof DateTimePrimitiveType || type instanceof MoneyPrimitiveType || type instanceof BigIntPrimitiveType || type instanceof CpfPrimitiveType || type instanceof CnpjPrimitiveType || type instanceof UuidPrimitiveType || type instanceof HexPrimitiveType || type instanceof Base64PrimitiveType ) { simpleArgs.set(argName, body.toString()); } else if (type instanceof BytesPrimitiveType) { args[argName] = body.toString("base64"); } else { args[argName] = JSON.parse(body.toString()); } } } } for (const [argName, argValue] of simpleArgs) { const arg = op.args.find(x => x.name === argName); if (!arg) { continue; } let { type } = arg; if (type instanceof OptionalType) { if (argValue === null) { args[argName] = null; continue; } else { type = type.base; } } else if (argValue === null) { args[argName] = argValue; continue; } if (type instanceof BoolPrimitiveType) { if (argValue === "true") { args[argName] = true; } else if (argValue === "false") { args[argName] = false; } else { args[argName] = argValue; } } else if (type instanceof UIntPrimitiveType || type instanceof IntPrimitiveType || type instanceof MoneyPrimitiveType) { args[argName] = parseInt(argValue, 10); } else if (type instanceof FloatPrimitiveType) { args[argName] = parseFloat(argValue); } else { args[argName] = argValue; } } const ip = getClientIp(req); if (!ip) { throw new Error("Couldn't determine client IP"); } const request: ContextRequest = { args, deviceInfo: { fingerprint: null, id: randomBytes(16).toString("hex"), language: null, platform: null, timezone: null, type: "rest", version: null, }, extra: {}, files, headers: req.headers, id: randomBytes(16).toString("hex"), ip, name: op.name, version: 3, }; await this.executeRequest(request, (ctx, reply) => { try { if (ctx) { for (const [headerKey, headerValue] of ctx.response.headers.entries()) { res.setHeader(headerKey, headerValue); } } if (ctx?.response.statusCode) { res.statusCode = ctx.response.statusCode; } if (reply.error) { const error = this.makeResponseError(reply.error); if (!ctx?.response.statusCode) { res.statusCode = error.type === "Fatal" ? 500 : 400; } res.setHeader("content-type", "application/json"); res.write(JSON.stringify(error)); res.end(); return; } if (req.headers.accept === "application/json") { res.setHeader("content-type", "application/json"); res.write(JSON.stringify(reply.result)); res.end(); } else { let type = op.returnType; if (type instanceof OptionalType) { if (reply.result === null) { if (!ctx?.response.statusCode) { res.statusCode = ann.method === "GET" ? 404 : 204; } res.end(); return; } type = type.base; } if ( type instanceof BoolPrimitiveType || type instanceof IntPrimitiveType || type instanceof UIntPrimitiveType || type instanceof FloatPrimitiveType || type instanceof StringPrimitiveType || type instanceof DatePrimitiveType || type instanceof DateTimePrimitiveType || type instanceof MoneyPrimitiveType || type instanceof BigIntPrimitiveType || type instanceof CpfPrimitiveType || type instanceof CnpjPrimitiveType || type instanceof UuidPrimitiveType || type instanceof HexPrimitiveType || type instanceof Base64PrimitiveType ) { res.setHeader("content-type", "text/plain"); res.write(`${reply.result}`); res.end(); } else if (type instanceof HtmlPrimitiveType) { res.setHeader("content-type", "text/html"); res.write(`${reply.result}`); res.end(); } else if (type instanceof XmlPrimitiveType) { res.setHeader("content-type", "text/xml"); res.write(`${reply.result}`); res.end(); } else if (type instanceof BytesPrimitiveType) { const buffer = Buffer.from(reply.result as string, "base64"); // eslint-disable-next-line @typescript-eslint/no-floating-promises FileType.fromBuffer(buffer) .then(fileType => { res.setHeader("content-type", fileType?.mime ?? "application/octet-stream"); }) .catch(err => { console.error(err); res.setHeader("content-type", "application/octet-stream"); }) .then(() => { res.write(buffer); res.end(); }) .catch(() => {}); } else { res.setHeader("content-type", "application/json"); res.write(JSON.stringify(reply.result)); res.end(); } } } catch (error) { console.error(error); if (!res.headersSent) { res.statusCode = 500; } res.end(); } }); } catch (error) { console.error(error); if (!res.headersSent) { res.statusCode = 500; } res.end(); } }); } } } public handleRequest = (req: IncomingMessage, res: ServerResponse) => { const hrStart = process.hrtime(); req.on("error", err => { console.error(err); res.end(); }); res.on("error", err => { console.error(err); res.end(); }); if (this.dynamicCorsOrigin && req.headers.origin) { res.setHeader("Access-Control-Allow-Origin", req.headers.origin); res.setHeader("Vary", "Origin"); } for (const [header, value] of this.headers) { if (req.method === "OPTIONS" && !header.startsWith("access-control-")) { continue; } res.setHeader(header, value); } if (req.method === "OPTIONS") { res.writeHead(200); res.end(); return; } const handleBody = (body: Buffer) => { this.handleRequestWithBody(req, res, body, hrStart).catch((e: unknown) => this.writeReply(res, null, { error: e }, hrStart)); }; // Google Cloud Functions add a rawBody property to the request object if (has(req, "rawBody") && req.rawBody instanceof Buffer) { handleBody(req.rawBody); } else { const body: Buffer[] = []; // eslint-disable-next-line @typescript-eslint/no-unsafe-argument req.on("data", chunk => body.push(chunk)); req.on("end", () => { handleBody(Buffer.concat(body)); }); } }; private async handleRequestWithBody(req: IncomingMessage, res: ServerResponse, body: Buffer, hrStart: [number, number]) { const { pathname, query } = parseUrl(req.url ?? ""); let path = pathname ?? ""; if (path.startsWith(this.ignoredUrlPrefix)) { path = path.slice(this.ignoredUrlPrefix.length); } if (!req.headers["content-type"]?.match(/application\/sdkgen/iu)) { const externalHandler = this.findBestHandler(path, req); if (externalHandler) { this.log(`HTTP ${req.method} ${path}${query ? `?${query}` : ""}`); externalHandler.handler(req, res, body); return; } } res.setHeader("Content-Type", "application/json; charset=utf-8"); if (req.method === "HEAD") { res.writeHead(200); res.end(); return; } if (req.method === "GET") { if (path !== "/") { res.writeHead(404); res.end(); return; } let ok = true; try { for (const healthCheck of this.healthChecks) { if (!ok) { break; } ok = await healthCheck(); } } catch (e) { ok = false; } res.statusCode = ok ? 200 : 500; res.write(JSON.stringify({ ok })); res.end(); return; } if (req.method !== "POST") { res.writeHead(400); res.end(); return; } const clientIp = getClientIp(req); if (!clientIp) { this.writeReply( res, null, { error: new Fatal("Couldn't determine client IP"), }, hrStart, ); return; } const request = this.parseRequest(req, body.toString(), clientIp); if (!request) { this.writeReply( res, null, { error: new Fatal("Couldn't parse request"), }, hrStart, ); return; } await this.executeRequest(request, (ctx, reply) => this.writeReply(res, ctx, reply, hrStart)); } private async executeRequest(request: ContextRequest, writeReply: (ctx: Context | null, reply: ContextReply) => void) { const ctx: Context & ExtraContextT = { ...this.extraContext, request, response: { headers: new Map(), }, }; writeReply(ctx, await executeRequest(ctx, this.apiConfig)); } private parseRequest(req: IncomingMessage, body: string, ip: string): ContextRequest | null { switch (this.identifyRequestVersion(req, body)) { case 1: return this.parseRequestV1(req, body, ip); case 2: return this.parseRequestV2(req, body, ip); case 3: return this.parseRequestV3(req, body, ip); default: throw new Error("Failed to understand request"); } } private identifyRequestVersion(_req: IncomingMessage, body: string): number { const parsed = JSON.parse(body) as unknown; if (typeof parsed === "object" && parsed && has(parsed, "version") && typeof parsed.version === "number") { return parsed.version; } else if (typeof parsed === "object" && parsed && has(parsed, "requestId")) { return 2; } else if (typeof parsed === "object" && parsed && has(parsed, "device")) { return 1; } return 3; } // Old Sdkgen format private parseRequestV1(req: IncomingMessage, body: string, ip: string): ContextRequest { const parsed = decode( { Request: { args: "json", device: "RequestDevice", id: "string", name: "string", }, RequestDevice: { fingerprint: "string?", id: "string?", language: "string?", platform: "json?", timezone: "string?", type: "string?", version: "string?", }, } as const, "root", "Request", JSON.parse(body), ); const deviceId = parsed.device.id ?? randomBytes(20).toString("hex"); if (!parsed.args || Array.isArray(parsed.args) || typeof parsed.args !== "object") { throw new Error("Expected 'args' to be an object"); } return { args: parsed.args, deviceInfo: { fingerprint: parsed.device.fingerprint, id: deviceId, language: parsed.device.language, platform: parsed.device.platform, timezone: parsed.device.timezone, type: parsed.device.type ?? (typeof parsed.device.platform === "string" ? parsed.device.platform : ""), version: parsed.device.version, }, extra: {}, files: [], headers: req.headers, id: `${deviceId}-${parsed.id}`, ip, name: parsed.name, version: 1, }; } // Maxima sdkgen format private parseRequestV2(req: IncomingMessage, body: string, ip: string): ContextRequest { const parsed = decode( { Request: { args: "json", deviceFingerprint: "string?", deviceId: "string", info: "RequestInfo", name: "string", partnerId: "string?", requestId: "string?", sessionId: "string?", }, RequestInfo: { browserUserAgent: "string?", language: "string", type: "string", }, } as const, "root", "Request", JSON.parse(body), ); if (!parsed.args || Array.isArray(parsed.args) || typeof parsed.args !== "object") { throw new Error("Expected 'args' to be an object"); } return { args: parsed.args, deviceInfo: { fingerprint: parsed.deviceFingerprint, id: parsed.deviceId, language: parsed.info.language, platform: { browserUserAgent: parsed.info.browserUserAgent ?? null, }, timezone: null, type: parsed.info.type, version: "", }, extra: { partnerId: parsed.partnerId, sessionId: parsed.sessionId, }, files: [], headers: req.headers, id: `${parsed.deviceId}-${parsed.requestId ?? randomBytes(16).toString("hex")}`, ip, name: parsed.name, version: 2, }; } // New sdkgen format private parseRequestV3(req: IncomingMessage, body: string, ip: string): ContextRequest { const parsed = decode( { DeviceInfo: { fingerprint: "string?", id: "string?", language: "string?", platform: "json?", timezone: "string?", type: "string?", version: "string?", }, Request: { args: "json", deviceInfo: "DeviceInfo?", extra: "json?", name: "string", requestId: "string?", }, } as const, "root", "Request", JSON.parse(body), ); const deviceInfo = parsed.deviceInfo ?? { fingerprint: null, id: null, language: null, platform: null, timezone: null, type: null, version: null, }; const deviceId = deviceInfo.id ?? randomBytes(16).toString("hex"); if (!parsed.args || Array.isArray(parsed.args) || typeof parsed.args !== "object") { throw new Error("Expected 'args' to be an object"); } return { args: parsed.args, deviceInfo: { fingerprint: deviceInfo.fingerprint, id: deviceId, language: deviceInfo.language, platform: typeof deviceInfo.platform === "object" ? { ...deviceInfo.platform } : {}, timezone: deviceInfo.timezone, type: deviceInfo.type ?? "api", version: deviceInfo.version, }, extra: typeof parsed.extra === "object" ? { ...parsed.extra } : {}, files: [], headers: req.headers, id: `${deviceId}-${parsed.requestId ?? randomBytes(16).toString("hex")}`, ip, name: parsed.name, version: 3, }; } private makeResponseError(err: unknown): { message: string; type: string; data: unknown } { let type = "Fatal"; if (typeof err === "object" && err !== null && has(err, "type") && typeof err.type === "string") { ({ type } = err); } let message: string; if (typeof err === "object" && err !== null && has(err, "message") && typeof err.message === "string") { ({ message } = err); } else if (err instanceof Error) { message = err.toString(); } else if (typeof err === "object") { message = JSON.stringify(err); } else { message = `${err}`; } let data: unknown; if (typeof err === "object" && err !== null && has(err, "data")) { ({ data } = err); } const error = this.apiConfig.ast.errors.find(x => x.name === type); if (error) { if (!(error.dataType instanceof VoidPrimitiveType)) { try { data = encode(this.apiConfig.astJson.typeTable, `error.${type}`, error.dataType.name, data); } catch (encodeError) { message = `Failed to encode error ${type} because: ${encodeError}. Original message: ${message}`; type = "Fatal"; } } } else { type = "Fatal"; } return { data, message, type }; } private writeReply(res: ServerResponse, ctx: Context | null, reply: ContextReply, hrStart: [number, number]) { if (!ctx) { res.statusCode = 500; res.write( JSON.stringify({ error: this.makeResponseError(reply.error ?? new Fatal("Response without context")), }), ); res.end(); return; } const deltaTime = process.hrtime(hrStart); const duration = deltaTime[0] + deltaTime[1] * 1e-9; if (reply.error) { console.error(reply.error); } this.log(`${ctx.request.id} [${duration.toFixed(6)}s] ${ctx.request.name}() -> ${reply.error ? this.makeResponseError(reply.error).type : "OK"}`); if (ctx.response.statusCode) { res.statusCode = ctx.response.statusCode; } for (const [headerKey, headerValue] of ctx.response.headers.entries()) { res.setHeader(headerKey, headerValue); } switch (ctx.request.version) { case 1: { const response = { deviceId: ctx.request.deviceInfo.id, duration, error: reply.error ? this.makeResponseError(reply.error) : null, host: hostname(), id: ctx.request.id, ok: !reply.error, result: reply.error ? null : reply.result, }; if (response.error && !ctx.response.statusCode) { res.statusCode = this.makeResponseError(response.error).type === "Fatal" ? 500 : 400; } res.write(JSON.stringify(response)); res.end(); break; } case 2: { const response = { deviceId: ctx.request.deviceInfo.id, error: reply.error ? this.makeResponseError(reply.error) : null, ok: !reply.error, requestId: ctx.request.id, result: reply.error ? null : reply.result, sessionId: ctx.request.extra.sessionId, }; if (response.error && !ctx.response.statusCode) { res.statusCode = this.makeResponseError(response.error).type === "Fatal" ? 500 : 400; } res.write(JSON.stringify(response)); res.end(); break; } case 3: { const response = { duration, error: reply.error ? this.makeResponseError(reply.error) : null, host: hostname(), result: reply.error ? null : reply.result, }; if (response.error && !ctx.response.statusCode) { res.statusCode = this.makeResponseError(response.error).type === "Fatal" ? 500 : 400; } res.setHeader("x-request-id", ctx.request.id); res.write(JSON.stringify(response)); res.end(); break; } default: { res.statusCode = 500; res.write( JSON.stringify({ error: this.makeResponseError(reply.error ?? new Fatal("Unknown request version")), }), ); res.end(); return; } } } } // type SdkgenHttpServerConstructor<ExtraContextT = unknown> = {} extends ExtraContextT // ? ExtraContextT extends {} // ? new (apiConfig: BaseApiConfig<ExtraContextT>) => SdkgenHttpServer // : new (apiConfig: BaseApiConfig<ExtraContextT>, extraContext: ExtraContextT) => SdkgenHttpServer // : new (apiConfig: BaseApiConfig<ExtraContextT>, extraContext: ExtraContextT) => SdkgenHttpServer; // function wrap(constructor: typeof SdkgenHttpServerBase) { // return constructor as unknown as SdkgenHttpServerConstructor; // } // // eslint-disable-next-line @typescript-eslint/naming-convention // export const SdkgenHttpServer = wrap(class SdkgenHttpServer<ExtraContextT = unknown> extends SdkgenHttpServerBase<ExtraContextT> {}); // export type SdkgenHttpServer<ExtraContextT = unknown> = { // [Prop in keyof SdkgenHttpServerBase<ExtraContextT>]: SdkgenHttpServerBase<ExtraContextT>[Prop]; // };