UNPKG

@intuitionrobotics/thunderstorm

Version:
137 lines 6.08 kB
import compression from 'compression'; import cors from 'cors'; import { addItemToArray, LogLevel, Module } from "@intuitionrobotics/ts-common"; import { apiDebug } from "./server-api.js"; import { ApiException } from "../../exceptions.js"; import express from "express"; import { HeaderKey_FunctionExecutionId, HeaderKey_JWT } from '../_imports.js'; const DefaultHeaders = [ 'content-type', 'content-encoding', 'x-session-id', 'authorization' ]; const ExposedHeaders = [ HeaderKey_FunctionExecutionId, HeaderKey_JWT ]; export class HttpServer_Class extends Module { express; constructor(_express, configElement) { super("HttpServer"); this.express = _express; this.setConfig(configElement); } getBaseUrl() { return this.config.baseUrl; } /** * Collapse duplicate slashes in a URL's PATH only — the query string is * preserved verbatim (a naive global replace mangles values like * `?redirect=https://...`). */ static normalizeUrlPath(url) { const queryIndex = url.indexOf("?"); const path = queryIndex === -1 ? url : url.substring(0, queryIndex); const query = queryIndex === -1 ? "" : url.substring(queryIndex); return path.replace(/\/{2,}/g, "/") + query; } /** * Collapse duplicate slashes and trim the trailing slash WITHOUT mangling * the protocol separator. (The old in-line replace turned `http://` into * `http:/` — which is why configs grew `http:////` workarounds. Both forms * normalize correctly now.) */ static normalizeBaseUrl(baseUrl) { const match = baseUrl.match(/^([a-z]+:)\/+(.*)$/i); const [protocol, rest] = match ? [`${match[1]}//`, match[2]] : ["", baseUrl]; return (protocol + rest.replace(/\/{2,}/g, "/")).replace(/\/+$/, ""); } // Express middleware/CORS wiring for the api function. Called explicitly by // ApiFunction when the instance's app is resolved — this is api-function // infra, deliberately eager (and cheap), unlike app modules which must not // override the (never-called) init(). setup() { this.setMinLevel(apiDebug.enabled ? LogLevel.Verbose : LogLevel.Info); if (this.config.baseUrl) this.config.baseUrl = HttpServer_Class.normalizeBaseUrl(this.config.baseUrl); this.express.use((req, _res, next) => { if (req) req.url = HttpServer_Class.normalizeUrlPath(req.url); next(); }); // On Cloud Functions the platform pre-parses JSON bodies before the app // runs, so this only matters for standalone/local `listen` setups. const parserLimit = this.config.bodyParserLimit; if (parserLimit) this.express.use(express.json({ limit: parserLimit })); this.express.use(compression()); const myCors = this.config.cors || {}; const corsOptions = { credentials: true, // Allow cookies/auth headers optionsSuccessStatus: 200, // DO NOT remove/change lightly: the frontend reads these off // responses (e.g. user-account's AccountModule pulls the refreshed // JWT and the function execution id via getResponseHeader). exposedHeaders: myCors.exposedHeaders || ExposedHeaders }; // Only wired when configured — otherwise the cors package keeps its // defaults: allowedHeaders reflects the preflight's // Access-Control-Request-Headers, methods = GET,HEAD,PUT,PATCH,POST,DELETE. // (These two knobs existed in config but were never connected before.) if (myCors.headers?.length) corsOptions.allowedHeaders = DefaultHeaders.reduce((toRet, item) => { if (!toRet.includes(item)) addItemToArray(toRet, item); return toRet; }, [...myCors.headers]); if (myCors.methods?.length) corsOptions.methods = myCors.methods; // Handle origin configuration if (!myCors.origins || myCors.origins.length === 0) { // No origins configured: reflect ANY origin — combined with // credentials this is wide open. Fine for local dev; deployed // environments must configure cors.origins. this.logWarning("CORS: no cors.origins configured — reflecting any origin WITH credentials. " + "Set 'HttpServer.cors.origins' (or the ApiFunction instance's config slice) in deployed environments."); corsOptions.origin = true; } else { // Single origin string or RegExp corsOptions.origin = myCors.origins; } const corsWithOptions = cors(corsOptions); this.express.use(corsWithOptions); this.express.options('*', corsWithOptions); } // v2.0 entrypoint: mount a user-built Express Router directly onto the // app. The hand-maintained routes.ts file constructs the Router using // the `mount()` helper and `Router.use(...)` for nesting, then this // method just `app.use`s it. No RouteResolver, no fs walk, no codegen // walker — Express owns the route table. // // The router mounts at the application root: on Cloud Functions the URL // prefix comes from the function name (the `api` function serves `…/api/**`). // `urlPrefix` is kept as an optional escape hatch but is normally unused. mountRouter(router, urlPrefix = "") { if (urlPrefix) this.express.use(urlPrefix, router); else this.express.use(router); } } export class HeaderKey { key; responseCode; constructor(key, responseCode = 400) { this.key = key; this.responseCode = responseCode; } get(request) { const value = request.header(this.key); if (!value) throw new ApiException(this.responseCode, `Missing expected header: ${this.key}`); return value; } } //# sourceMappingURL=HttpServer.js.map