@intuitionrobotics/thunderstorm
Version:
137 lines • 6.08 kB
JavaScript
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