@interopio/gateway-server
Version:
1,709 lines (1,695 loc) • 105 kB
JavaScript
var __defProp = Object.defineProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
// src/server.ts
var server_exports = {};
__export(server_exports, {
Factory: () => Factory
});
import http2 from "node:http";
import https from "node:https";
import { readFileSync } from "node:fs";
import { AsyncLocalStorage as AsyncLocalStorage4 } from "node:async_hooks";
import { IOGateway as IOGateway7 } from "@interopio/gateway";
// src/logger.ts
import { IOGateway } from "@interopio/gateway";
function getLogger(name) {
return IOGateway.Logging.getLogger(`gateway.server.${name}`);
}
function regexAwareReplacer(_key, value) {
return value instanceof RegExp ? value.toString() : value;
}
// src/gateway/ws/core.ts
import { IOGateway as IOGateway2 } from "@interopio/gateway";
import { AsyncLocalStorage } from "node:async_hooks";
var GatewayEncoders = IOGateway2.Encoding;
var log = getLogger("ws");
var codec = GatewayEncoders.json();
function principalName(authentication) {
let name;
if (authentication.authenticated) {
name = authentication.name;
if (name === void 0) {
if (authentication["principal"] !== void 0) {
const principal = authentication["principal"];
if (typeof principal === "object") {
name = principal.name;
}
if (name === void 0) {
if (principal === void 0) {
name = "";
} else {
name = String(principal);
}
}
}
}
}
return name;
}
function initClient(socket, authenticationPromise, remoteAddress) {
const key = `${remoteAddress?.address}:${remoteAddress?.port}`;
const host = remoteAddress?.address ?? "<unknown>";
const opts = {
key,
host,
codec,
onAuthenticate: async () => {
const authentication = await authenticationPromise();
if (authentication?.authenticated) {
return { type: "success", user: principalName(authentication) };
}
throw new Error(`no valid client authentication ${key}`);
},
onPing: () => {
socket.ping((err) => {
if (err) {
log.warn(`failed to ping ${key}`, err);
} else {
log.info(`ping sent to ${key}`);
}
});
},
onDisconnect: (reason) => {
switch (reason) {
case "inactive": {
log.warn(`no heartbeat (ping) received from ${key}, closing socket`);
socket.close(4001, "ping expected");
break;
}
case "shutdown": {
socket.close(1001, "shutdown");
break;
}
}
}
};
try {
return this.client((data) => socket.send(data), opts);
} catch (err) {
log.warn(`${key} failed to create client`, err);
}
}
async function create(environment) {
log.info(`starting gateway on ${environment.endpoint}`);
await this.start(environment);
return async ({ socket, handshake }) => {
const { logPrefix, remoteAddress, principal: principalPromise } = handshake;
log.info(`${logPrefix}connected on gw using ${remoteAddress?.address}`);
const client = initClient.call(this, socket, principalPromise, remoteAddress);
if (!client) {
log.error(`${logPrefix}gw client init failed`);
socket.terminate();
return;
}
socket.on("error", (err) => {
log.error(`${logPrefix}websocket error: ${err}`, err);
});
const contextFn = environment.storage !== void 0 ? AsyncLocalStorage.snapshot() : void 0;
socket.on("message", (data, _isBinary) => {
if (Array.isArray(data)) {
data = Buffer.concat(data);
}
if (contextFn !== void 0) {
contextFn(() => client.send(data));
} else {
client.send(data);
}
});
socket.on("close", (code) => {
log.info(`${logPrefix}disconnected from gw. code: ${code}`);
client.close();
});
};
}
var core_default = create;
// src/common/compose.ts
function compose(...middleware) {
if (!Array.isArray(middleware)) {
throw new Error("middleware must be array!");
}
const fns = middleware.flat();
for (const fn of fns) {
if (typeof fn !== "function") {
throw new Error("middleware must be compose of functions!");
}
}
return async function(ctx, next) {
const dispatch = async (i, dispatchedCtx) => {
const fn = i === fns.length ? next : fns[i];
if (fn === void 0) {
return;
}
let nextCalled = false;
let nextResolved = false;
const nextFn = async (nextCtx) => {
if (nextCalled) {
throw new Error("next() called multiple times");
}
nextCalled = true;
try {
return await dispatch(i + 1, nextCtx ?? dispatchedCtx);
} finally {
nextResolved = true;
}
};
const result = await fn(dispatchedCtx, nextFn);
if (nextCalled && !nextResolved) {
throw new Error("middleware resolved before downstream.\n You are probably missing an await or return statement in your middleware function.");
}
return result;
};
return dispatch(0, ctx);
};
}
// src/http/exchange.ts
import { Cookie } from "tough-cookie";
function parseHost(headers2, defaultHost) {
let host = headers2.get("x-forwarded-for");
if (host === void 0) {
host = headers2.get("x-forwarded-host");
if (Array.isArray(host)) {
host = host[0];
}
if (host) {
const port = headers2.one("x-forwarded-port");
if (port) {
host = `${host}:${port}`;
}
}
host ??= headers2.one("host");
}
if (Array.isArray(host)) {
host = host[0];
}
if (host) {
return host.split(",", 1)[0].trim();
}
return defaultHost;
}
function parseProtocol(headers2, defaultProtocol) {
let proto = headers2.get("x-forwarded-proto");
if (Array.isArray(proto)) {
proto = proto[0];
}
if (proto !== void 0) {
return proto.split(",", 1)[0].trim();
}
return defaultProtocol;
}
var AbstractHttpMessage = class {
#headers;
constructor(headers2) {
this.#headers = headers2;
}
get headers() {
return this.#headers;
}
};
var AbstractHttpRequest = class _AbstractHttpRequest extends AbstractHttpMessage {
static logIdCounter = 0;
#id;
get id() {
if (this.#id === void 0) {
this.#id = `${this.initId()}-${++_AbstractHttpRequest.logIdCounter}`;
}
return this.#id;
}
initId() {
return "request";
}
get cookies() {
return parseCookies(this.headers);
}
parseHost(defaultHost) {
return parseHost(this.headers, defaultHost);
}
parseProtocol(defaultProtocol) {
return parseProtocol(this.headers, defaultProtocol);
}
};
var AbstractHttpResponse = class extends AbstractHttpMessage {
get cookies() {
return parseResponseCookies(this.headers);
}
setCookieValue(responseCookie) {
const cookie = new Cookie({
key: responseCookie.name,
value: responseCookie.value,
maxAge: responseCookie.maxAge,
domain: responseCookie.domain,
path: responseCookie.path,
secure: responseCookie.secure,
httpOnly: responseCookie.httpOnly,
sameSite: responseCookie.sameSite
});
return cookie.toString();
}
};
function parseHeader(value) {
const list = [];
{
let start2 = 0;
let end = 0;
for (let i = 0; i < value.length; i++) {
switch (value.charCodeAt(i)) {
case 32:
if (start2 === end) {
start2 = end = i + 1;
}
break;
case 44:
list.push(value.slice(start2, end));
start2 = end = i + 1;
break;
default:
end = end + 1;
break;
}
}
list.push(value.slice(start2, end));
}
return list;
}
function toList(values) {
if (typeof values === "string") {
values = [values];
}
if (typeof values === "number") {
values = [String(values)];
}
const list = [];
if (values) {
for (const value of values) {
if (value) {
list.push(...parseHeader(value));
}
}
}
return list;
}
function parseCookies(headers2) {
return headers2.list("cookie").map((s) => s.split(";").map((cs) => Cookie.parse(cs))).flat(1).filter((tc) => tc !== void 0).map((tc) => {
const result = Object.freeze({ name: tc.key, value: tc.value });
return result;
});
}
function parseResponseCookies(headers2) {
return headers2.list("set-cookie").map((cookie) => {
const parsed = Cookie.parse(cookie);
if (parsed) {
const result = { name: parsed.key, value: parsed.value, maxAge: Number(parsed.maxAge ?? -1) };
if (parsed.httpOnly) result.httpOnly = true;
if (parsed.domain) result.domain = parsed.domain;
if (parsed.path) result.path = parsed.path;
if (parsed.secure) result.secure = true;
if (parsed.httpOnly) result.httpOnly = true;
if (parsed.sameSite) result.sameSite = parsed.sameSite;
return Object.freeze(result);
}
}).filter((cookie) => cookie !== void 0);
}
var AbstractHttpHeaders = class {
constructor() {
}
toList(name) {
const values = this.get(name);
return toList(values);
}
};
var MapHttpHeaders = class extends Map {
get(name) {
return super.get(name.toLowerCase());
}
one(name) {
return this.get(name)?.[0];
}
list(name) {
const values = super.get(name.toLowerCase());
return toList(values);
}
set(name, value) {
if (typeof value === "number") {
value = String(value);
}
if (typeof value === "string") {
value = [value];
}
if (value) {
return super.set(name.toLowerCase(), value);
} else {
super.delete(name.toLowerCase());
return this;
}
}
add(name, value) {
const prev = super.get(name.toLowerCase());
if (typeof value === "string") {
value = [value];
}
if (prev) {
value = prev.concat(value);
}
this.set(name, value);
return this;
}
};
// src/http/status.ts
var DefaultHttpStatusCode = class {
#value;
constructor(value) {
this.#value = value;
}
get value() {
return this.#value;
}
toString() {
return this.#value.toString();
}
};
var HttpStatus = class _HttpStatus {
static CONTINUE = new _HttpStatus(100, "Continue");
static SWITCHING_PROTOCOLS = new _HttpStatus(101, "Switching Protocols");
// 2xx Success
static OK = new _HttpStatus(200, "OK");
static CREATED = new _HttpStatus(201, "Created");
static ACCEPTED = new _HttpStatus(202, "Accepted");
static NON_AUTHORITATIVE_INFORMATION = new _HttpStatus(203, "Non-Authoritative Information");
static NO_CONTENT = new _HttpStatus(204, "No Content");
static RESET_CONTENT = new _HttpStatus(205, "Reset Content");
static PARTIAL_CONTENT = new _HttpStatus(206, "Partial Content");
static MULTI_STATUS = new _HttpStatus(207, "Multi-Status");
static IM_USED = new _HttpStatus(226, "IM Used");
// 3xx Redirection
static MULTIPLE_CHOICES = new _HttpStatus(300, "Multiple Choices");
static MOVED_PERMANENTLY = new _HttpStatus(301, "Moved Permanently");
// 4xx Client Error
static BAD_REQUEST = new _HttpStatus(400, "Bad Request");
static UNAUTHORIZED = new _HttpStatus(401, "Unauthorized");
static FORBIDDEN = new _HttpStatus(403, "Forbidden");
static NOT_FOUND = new _HttpStatus(404, "Not Found");
static METHOD_NOT_ALLOWED = new _HttpStatus(405, "Method Not Allowed");
static NOT_ACCEPTABLE = new _HttpStatus(406, "Not Acceptable");
static PROXY_AUTHENTICATION_REQUIRED = new _HttpStatus(407, "Proxy Authentication Required");
static REQUEST_TIMEOUT = new _HttpStatus(408, "Request Timeout");
static CONFLICT = new _HttpStatus(409, "Conflict");
static GONE = new _HttpStatus(410, "Gone");
static LENGTH_REQUIRED = new _HttpStatus(411, "Length Required");
static PRECONDITION_FAILED = new _HttpStatus(412, "Precondition Failed");
static PAYLOAD_TOO_LARGE = new _HttpStatus(413, "Payload Too Large");
static URI_TOO_LONG = new _HttpStatus(414, "URI Too Long");
static UNSUPPORTED_MEDIA_TYPE = new _HttpStatus(415, "Unsupported Media Type");
static EXPECTATION_FAILED = new _HttpStatus(417, "Expectation Failed");
static IM_A_TEAPOT = new _HttpStatus(418, "I'm a teapot");
static TOO_EARLY = new _HttpStatus(425, "Too Early");
static UPGRADE_REQUIRED = new _HttpStatus(426, "Upgrade Required");
static PRECONDITION_REQUIRED = new _HttpStatus(428, "Precondition Required");
static TOO_MANY_REQUESTS = new _HttpStatus(429, "Too Many Requests");
static REQUEST_HEADER_FIELDS_TOO_LARGE = new _HttpStatus(431, "Request Header Fields Too Large");
static UNAVAILABLE_FOR_LEGAL_REASONS = new _HttpStatus(451, "Unavailable For Legal Reasons");
// 5xx Server Error
static INTERNAL_SERVER_ERROR = new _HttpStatus(500, "Internal Server Error");
static NOT_IMPLEMENTED = new _HttpStatus(501, "Not Implemented");
static BAD_GATEWAY = new _HttpStatus(502, "Bad Gateway");
static SERVICE_UNAVAILABLE = new _HttpStatus(503, "Service Unavailable");
static GATEWAY_TIMEOUT = new _HttpStatus(504, "Gateway Timeout");
static HTTP_VERSION_NOT_SUPPORTED = new _HttpStatus(505, "HTTP Version Not Supported");
static VARIANT_ALSO_NEGOTIATES = new _HttpStatus(506, "Variant Also Negotiates");
static INSUFFICIENT_STORAGE = new _HttpStatus(507, "Insufficient Storage");
static LOOP_DETECTED = new _HttpStatus(508, "Loop Detected");
static NOT_EXTENDED = new _HttpStatus(510, "Not Extended");
static NETWORK_AUTHENTICATION_REQUIRED = new _HttpStatus(511, "Network Authentication Required");
static #VALUES = [];
static {
Object.keys(_HttpStatus).filter((key) => key !== "VALUES" && key !== "resolve").forEach((key) => {
const value = _HttpStatus[key];
if (value instanceof _HttpStatus) {
Object.defineProperty(value, "name", { enumerable: true, value: key, writable: false });
_HttpStatus.#VALUES.push(value);
}
});
}
static resolve(code) {
for (const status of _HttpStatus.#VALUES) {
if (status.value === code) {
return status;
}
}
}
#value;
#phrase;
constructor(value, phrase) {
this.#value = value;
this.#phrase = phrase;
}
get value() {
return this.#value;
}
get phrase() {
return this.#phrase;
}
toString() {
return `${this.#value} ${this["name"]}`;
}
};
function httpStatusCode(value) {
if (typeof value === "number") {
if (value < 100 || value > 999) {
throw new Error(`status code ${value} should be in range 100-999`);
}
const status = HttpStatus.resolve(value);
if (status !== void 0) {
return status;
}
return new DefaultHttpStatusCode(value);
}
return value;
}
// src/server/exchange.ts
import http from "node:http";
var ExtendedHttpIncomingMessage = class extends http.IncomingMessage {
// circular reference to the exchange
exchange;
upgradeHead;
get urlBang() {
return this.url;
}
get socketEncrypted() {
return this.socket["encrypted"] === true;
}
};
var ExtendedHttpServerResponse = class extends http.ServerResponse {
markHeadersSent() {
this["_header"] = true;
}
getRawHeaderNames() {
return super["getRawHeaderNames"]();
}
};
var AbstractServerHttpRequest = class extends AbstractHttpRequest {
};
var AbstractServerHttpResponse = class extends AbstractHttpResponse {
#cookies = [];
#statusCode;
#state = "new";
#commitActions = [];
setStatusCode(statusCode) {
if (this.#state === "committed") {
return false;
} else {
this.#statusCode = statusCode;
return true;
}
}
setRawStatusCode(statusCode) {
return this.setStatusCode(statusCode === void 0 ? void 0 : httpStatusCode(statusCode));
}
get statusCode() {
return this.#statusCode;
}
addCookie(cookie) {
if (this.#state === "committed") {
throw new Error(`Cannot add cookie ${JSON.stringify(cookie)} because HTTP response has already been committed`);
}
this.#cookies.push(cookie);
return this;
}
beforeCommit(action) {
this.#commitActions.push(action);
}
get commited() {
const state = this.#state;
return state !== "new" && state !== "commit-action-failed";
}
async body(body) {
if (body instanceof ReadableStream) {
throw new Error("ReadableStream body not supported yet");
}
const buffer = await body;
try {
return await this.doCommit(async () => {
return await this.bodyInternal(Promise.resolve(buffer));
}).catch((error) => {
throw error;
});
} catch (error) {
throw error;
}
}
async end() {
if (!this.commited) {
return this.doCommit(async () => {
return await this.bodyInternal(Promise.resolve());
});
} else {
return Promise.resolve(false);
}
}
doCommit(writeAction) {
const state = this.#state;
let allActions = Promise.resolve();
if (state === "new") {
this.#state = "committing";
if (this.#commitActions.length > 0) {
allActions = this.#commitActions.reduce(
(acc, cur) => acc.then(() => cur()),
Promise.resolve()
).catch((error) => {
const state2 = this.#state;
if (state2 === "committing") {
this.#state = "commit-action-failed";
}
});
}
} else if (state === "commit-action-failed") {
this.#state = "committing";
} else {
return Promise.resolve(false);
}
allActions = allActions.then(() => {
this.applyStatusCode();
this.applyHeaders();
this.applyCookies();
this.#state = "committed";
});
return allActions.then(async () => {
return writeAction !== void 0 ? await writeAction() : true;
});
}
applyStatusCode() {
}
applyHeaders() {
}
applyCookies() {
}
};
var HttpServerRequest = class extends AbstractServerHttpRequest {
#url;
#cookies;
#req;
constructor(req) {
super(new IncomingMessageHeaders(req));
this.#req = req;
}
getNativeRequest() {
return this.#req;
}
get upgrade() {
return this.#req["upgrade"];
}
get http2() {
return this.#req.httpVersionMajor >= 2;
}
get path() {
return this.URL?.pathname;
}
get URL() {
this.#url ??= new URL(this.#req.urlBang, `${this.protocol}://${this.host}`);
return this.#url;
}
get query() {
return this.URL?.search;
}
get method() {
return this.#req.method;
}
get host() {
let dh = void 0;
if (this.#req.httpVersionMajor >= 2) {
dh = this.#req.headers[":authority"];
}
dh ??= this.#req.socket.remoteAddress;
return super.parseHost(dh);
}
get protocol() {
let dp = void 0;
if (this.#req.httpVersionMajor > 2) {
dp = this.#req.headers[":scheme"];
}
dp ??= this.#req.socketEncrypted ? "https" : "http";
return super.parseProtocol(dp);
}
get socket() {
return this.#req.socket;
}
get remoteAddress() {
const family = this.#req.socket.remoteFamily;
const address = this.#req.socket.remoteAddress;
const port = this.#req.socket.remotePort;
if (!family || !address || !port) {
return void 0;
}
return { family, address, port };
}
get cookies() {
this.#cookies ??= super.cookies;
return this.#cookies;
}
get body() {
return http.IncomingMessage.toWeb(this.#req);
}
async blob() {
const chunks = [];
if (this.body !== void 0) {
for await (const chunk of this.body) {
chunks.push(chunk);
}
}
return new Blob(chunks, { type: this.headers.one("content-type") || "application/octet-stream" });
}
async text() {
const blob = await this.blob();
return await blob.text();
}
async formData() {
const blob = await this.blob();
const text = await blob.text();
return new URLSearchParams(text);
}
async json() {
const blob = await this.blob();
if (blob.size === 0) {
return void 0;
}
const text = await blob.text();
return JSON.parse(text);
}
initId() {
const remoteIp = this.#req.socket.remoteAddress;
if (!remoteIp) {
throw new Error("Socket has no remote address");
}
return `${remoteIp}:${this.#req.socket.remotePort}`;
}
};
var IncomingMessageHeaders = class extends AbstractHttpHeaders {
#msg;
constructor(msg) {
super();
this.#msg = msg;
}
has(name) {
return this.#msg.headers[name] !== void 0;
}
get(name) {
return this.#msg.headers[name];
}
list(name) {
return super.toList(name);
}
one(name) {
const value = this.#msg.headers[name];
if (Array.isArray(value)) {
return value[0];
}
return value;
}
keys() {
return Object.keys(this.#msg.headers).values();
}
};
var OutgoingMessageHeaders = class extends AbstractHttpHeaders {
#msg;
constructor(msg) {
super();
this.#msg = msg;
}
has(name) {
return this.#msg.hasHeader(name);
}
keys() {
return this.#msg.getHeaderNames().values();
}
get(name) {
return this.#msg.getHeader(name);
}
one(name) {
const value = this.#msg.getHeader(name);
if (Array.isArray(value)) {
return value[0];
}
return value;
}
set(name, value) {
if (!this.#msg.headersSent) {
if (Array.isArray(value)) {
value = value.map((v) => typeof v === "number" ? String(v) : v);
} else if (typeof value === "number") {
value = String(value);
}
if (value) {
this.#msg.setHeader(name, value);
} else {
this.#msg.removeHeader(name);
}
}
return this;
}
add(name, value) {
if (!this.#msg.headersSent) {
this.#msg.appendHeader(name, value);
}
return this;
}
list(name) {
return super.toList(name);
}
};
var HttpServerResponse = class extends AbstractServerHttpResponse {
#res;
constructor(res) {
super(new OutgoingMessageHeaders(res));
this.#res = res;
}
getNativeResponse() {
return this.#res;
}
get statusCode() {
const status = super.statusCode;
return status ?? { value: this.#res.statusCode };
}
applyStatusCode() {
const status = super.statusCode;
if (status !== void 0) {
this.#res.statusCode = status.value;
}
}
addCookie(cookie) {
this.headers.add("Set-Cookie", super.setCookieValue(cookie));
return this;
}
async bodyInternal(body) {
if (!this.#res.headersSent) {
if (body instanceof ReadableStream) {
throw new Error("ReadableStream body not supported in response");
} else {
const chunk = await body;
return await new Promise((resolve, reject) => {
try {
if (chunk === void 0) {
this.#res.end(() => {
resolve(true);
});
} else {
if (!this.headers.has("content-length")) {
if (typeof chunk === "string") {
this.headers.set("content-length", Buffer.byteLength(chunk));
} else if (chunk instanceof Blob) {
this.headers.set("content-length", chunk.size);
} else {
this.headers.set("content-length", chunk.byteLength);
}
}
this.#res.end(chunk, () => {
resolve(true);
});
}
} catch (e) {
reject(e instanceof Error ? e : new Error(`end failed: ${e}`));
}
});
}
} else {
return false;
}
}
};
var ServerHttpRequestDecorator = class _ServerHttpRequestDecorator {
#delegate;
constructor(request) {
this.#delegate = request;
}
get delegate() {
return this.#delegate;
}
get id() {
return this.#delegate.id;
}
get method() {
return this.#delegate.method;
}
get path() {
return this.#delegate.path;
}
get protocol() {
return this.#delegate.protocol;
}
get host() {
return this.#delegate.host;
}
get URL() {
return this.#delegate.URL;
}
get headers() {
return this.#delegate.headers;
}
get cookies() {
return this.#delegate.cookies;
}
get remoteAddress() {
return this.#delegate.remoteAddress;
}
get upgrade() {
return this.#delegate.upgrade;
}
get body() {
return this.#delegate.body;
}
async blob() {
return await this.#delegate.blob();
}
async text() {
return await this.#delegate.text();
}
async formData() {
return await this.#delegate.formData();
}
async json() {
return await this.#delegate.json();
}
toString() {
return `${_ServerHttpRequestDecorator.name} [delegate: ${this.delegate.toString()}]`;
}
static getNativeRequest(request) {
if (request instanceof AbstractServerHttpRequest) {
return request.getNativeRequest();
} else if (request instanceof _ServerHttpRequestDecorator) {
return _ServerHttpRequestDecorator.getNativeRequest(request.delegate);
} else {
throw new Error(`Cannot get native request from ${request.constructor.name}`);
}
}
};
var ServerHttpResponseDecorator = class _ServerHttpResponseDecorator {
#delegate;
constructor(response) {
this.#delegate = response;
}
get delegate() {
return this.#delegate;
}
setStatusCode(statusCode) {
return this.delegate.setStatusCode(statusCode);
}
setRawStatusCode(statusCode) {
return this.delegate.setRawStatusCode(statusCode);
}
get statusCode() {
return this.delegate.statusCode;
}
get cookies() {
return this.delegate.cookies;
}
addCookie(cookie) {
this.delegate.addCookie(cookie);
return this;
}
async end() {
return await this.delegate.end();
}
async body(body) {
return await this.#delegate.body(body);
}
get headers() {
return this.#delegate.headers;
}
toString() {
return `${_ServerHttpResponseDecorator.name} [delegate: ${this.delegate.toString()}]`;
}
static getNativeResponse(response) {
if (response instanceof AbstractServerHttpResponse) {
return response.getNativeResponse();
} else if (response instanceof _ServerHttpResponseDecorator) {
return _ServerHttpResponseDecorator.getNativeResponse(response.delegate);
} else {
throw new Error(`Cannot get native response from ${response.constructor.name}`);
}
}
};
var ServerWebExchangeDecorator = class _ServerWebExchangeDecorator {
#delegate;
constructor(exchange) {
this.#delegate = exchange;
}
get delegate() {
return this.#delegate;
}
get request() {
return this.#delegate.request;
}
get response() {
return this.#delegate.response;
}
attribute(name) {
return this.#delegate.attribute(name);
}
principal() {
return this.#delegate.principal();
}
get logPrefix() {
return this.#delegate.logPrefix;
}
toString() {
return `${_ServerWebExchangeDecorator.name} [delegate: ${this.delegate}]`;
}
};
var DefaultWebExchange = class {
request;
response;
#attributes = {};
#logId;
#logPrefix = "";
constructor(request, response) {
this.#attributes[LOG_ID_ATTRIBUTE] = request.id;
this.request = request;
this.response = response;
}
get method() {
return this.request.method;
}
get path() {
return this.request.path;
}
get attributes() {
return this.#attributes;
}
attribute(name) {
return this.attributes[name];
}
principal() {
return Promise.resolve(void 0);
}
get logPrefix() {
const value = this.attribute(LOG_ID_ATTRIBUTE);
if (this.#logId !== value) {
this.#logId = value;
this.#logPrefix = value !== void 0 ? `[${value}] ` : "";
}
return this.#logPrefix;
}
};
var LOG_ID_ATTRIBUTE = "io.interop.gateway.server.log_id";
// src/server/address.ts
import { networkInterfaces } from "node:os";
var PORT_RANGE_MATCHER = /^(\d+|(0x[\da-f]+))(-(\d+|(0x[\da-f]+)))?$/i;
function validPort(port) {
if (port > 65535) throw new Error(`bad port ${port}`);
return port;
}
function* portRange(port) {
if (typeof port === "string") {
for (const portRange2 of port.split(",")) {
const trimmed = portRange2.trim();
const matchResult = PORT_RANGE_MATCHER.exec(trimmed);
if (matchResult) {
const start2 = parseInt(matchResult[1]);
const end = parseInt(matchResult[4] ?? matchResult[1]);
for (let i = validPort(start2); i < validPort(end) + 1; i++) {
yield i;
}
} else {
throw new Error(`'${portRange2}' is not a valid port or range.`);
}
}
} else {
yield validPort(port);
}
}
var localIp = (() => {
function first(a) {
return a.length > 0 ? a[0] : void 0;
}
const addresses = Object.values(networkInterfaces()).flatMap((details) => {
return (details ?? []).filter((info2) => info2.family === "IPv4");
}).reduce((acc, info2) => {
acc[info2.internal ? "internal" : "external"].push(info2);
return acc;
}, { internal: [], external: [] });
return (first(addresses.internal) ?? first(addresses.external))?.address;
})();
// src/server/monitoring.ts
import { getHeapStatistics, writeHeapSnapshot } from "node:v8";
import { access, mkdir, rename, unlink } from "node:fs/promises";
var log2 = getLogger("monitoring");
var DEFAULT_OPTIONS = {
memoryLimit: 1024 * 1024 * 1024,
// 1GB
reportInterval: 10 * 60 * 1e3,
// 10 min
dumpLocation: ".",
// current folder
maxBackups: 10,
dumpPrefix: "Heap"
};
function fetchStats() {
return getHeapStatistics();
}
async function dumpHeap(opts) {
const prefix = opts.dumpPrefix ?? "Heap";
const target = `${opts.dumpLocation}/${prefix}.heapsnapshot`;
if (log2.enabledFor("debug")) {
log2.debug(`starting heap dump in ${target}`);
}
await fileExists(opts.dumpLocation).catch(async (_) => {
if (log2.enabledFor("debug")) {
log2.debug(`dump location ${opts.dumpLocation} does not exists. Will try to create it`);
}
try {
await mkdir(opts.dumpLocation, { recursive: true });
log2.info(`dump location dir ${opts.dumpLocation} successfully created`);
} catch (e) {
log2.error(`failed to create dump location ${opts.dumpLocation}`);
}
});
const dumpFileName = writeHeapSnapshot(target);
log2.info(`heap dumped`);
try {
log2.debug(`rolling snapshot backups`);
const lastFileName = `${opts.dumpLocation}/${prefix}.${opts.maxBackups}.heapsnapshot`;
await fileExists(lastFileName).then(async () => {
if (log2.enabledFor("debug")) {
log2.debug(`deleting ${lastFileName}`);
}
try {
await unlink(lastFileName);
} catch (e) {
log2.warn(`failed to delete ${lastFileName}`, e);
}
}).catch(() => {
});
for (let i = opts.maxBackups - 1; i > 0; i--) {
const currentFileName = `${opts.dumpLocation}/${prefix}.${i}.heapsnapshot`;
const nextFileName = `${opts.dumpLocation}/${prefix}.${i + 1}.heapsnapshot`;
await fileExists(currentFileName).then(async () => {
try {
await rename(currentFileName, nextFileName);
} catch (e) {
log2.warn(`failed to rename ${currentFileName} to ${nextFileName}`, e);
}
}).catch(() => {
});
}
const firstFileName = `${opts.dumpLocation}/${prefix}.${1}.heapsnapshot`;
try {
await rename(dumpFileName, firstFileName);
} catch (e) {
log2.warn(`failed to rename ${dumpFileName} to ${firstFileName}`, e);
}
log2.debug("snapshots rolled");
} catch (e) {
log2.error("error rolling backups", e);
throw e;
}
}
async function fileExists(path) {
if (log2.enabledFor("trace")) {
log2.debug(`checking file ${path}`);
}
await access(path);
}
async function processStats(stats, state, opts) {
if (log2.enabledFor("debug")) {
log2.debug(`processing heap stats ${JSON.stringify(stats)}`);
}
const limit = Math.min(opts.memoryLimit, 0.95 * stats.heap_size_limit);
const used = stats.used_heap_size;
log2.info(`heap stats ${JSON.stringify(stats)}`);
if (used >= limit) {
log2.warn(`used heap ${used} bytes exceeds memory limit ${limit} bytes`);
if (state.memoryLimitExceeded) {
delete state.snapshot;
} else {
state.memoryLimitExceeded = true;
state.snapshot = true;
}
await dumpHeap(opts);
} else {
state.memoryLimitExceeded = false;
delete state.snapshot;
}
}
function start(opts) {
const merged = { ...DEFAULT_OPTIONS, ...opts };
let stopped = false;
const state = { memoryLimitExceeded: false };
const report = async () => {
const stats = fetchStats();
await processStats(stats, state, merged);
};
const interval = setInterval(report, merged.reportInterval);
const channel = async (command) => {
if (!stopped) {
command ??= "run";
switch (command) {
case "run": {
await report();
break;
}
case "dump": {
await dumpHeap(merged);
break;
}
case "stop": {
stopped = true;
clearInterval(interval);
log2.info("exit memory diagnostic");
break;
}
}
}
return stopped;
};
return { ...merged, channel };
}
async function run({ channel }, command) {
if (!await channel(command)) {
log2.warn(`cannot execute command "${command}" already closed`);
}
}
async function stop(m) {
return await run(m, "stop");
}
// src/server/server-header.ts
import info from "@interopio/gateway-server/package.json" with { type: "json" };
var serverHeader = (server) => {
server ??= `${info.name} - v${info.version}`;
return async ({ response }, next) => {
if (server !== false && !response.headers.has("server")) {
response.headers.set("Server", server);
}
await next();
};
};
var server_header_default = (server) => serverHeader(server);
// src/server/ws-client-verify.ts
import { IOGateway as IOGateway3 } from "@interopio/gateway";
var log3 = getLogger("gateway.ws.client-verify");
function acceptsMissing(originFilters) {
switch (originFilters.missing) {
case "allow":
// fall-through
case "whitelist":
return true;
case "block":
// fall-through
case "blacklist":
return false;
default:
return false;
}
}
function tryMatch(originFilters, origin) {
const block = originFilters.block ?? originFilters["blacklist"];
const allow = originFilters.allow ?? originFilters["whitelist"];
if (block.length > 0 && IOGateway3.Filtering.valuesMatch(block, origin)) {
log3.warn(`origin ${origin} matches block filter`);
return false;
} else if (allow.length > 0 && IOGateway3.Filtering.valuesMatch(allow, origin)) {
if (log3.enabledFor("debug")) {
log3.debug(`origin ${origin} matches allow filter`);
}
return true;
}
}
function acceptsNonMatched(originFilters) {
switch (originFilters.non_matched) {
case "allow":
// fall-through
case "whitelist":
return true;
case "block":
// fall-through
case "blacklist":
return false;
default:
return false;
}
}
function acceptsOrigin(origin, originFilters) {
if (!originFilters) {
return true;
}
if (!origin) {
return acceptsMissing(originFilters);
} else {
const matchResult = tryMatch(originFilters, origin);
if (matchResult) {
return matchResult;
} else {
return acceptsNonMatched(originFilters);
}
}
}
function regexifyOriginFilters(originFilters) {
if (originFilters) {
const block = (originFilters.block ?? originFilters.blacklist ?? []).map(IOGateway3.Filtering.regexify);
const allow = (originFilters.allow ?? originFilters.whitelist ?? []).map(IOGateway3.Filtering.regexify);
return {
non_matched: originFilters.non_matched ?? "allow",
missing: originFilters.missing ?? "allow",
allow,
block
};
}
}
// src/server/util/matchers.ts
var or = (matchers) => {
return async (exchange) => {
for (const matcher of matchers) {
const result = await matcher(exchange);
if (result.match) {
return match();
}
}
return NO_MATCH;
};
};
var and = (matchers) => {
const matcher = async (exchange) => {
for (const matcher2 of matchers) {
const result = await matcher2(exchange);
if (!result.match) {
return NO_MATCH;
}
}
return match();
};
matcher.toString = () => `and(${matchers.map((m) => m.toString()).join(", ")})`;
return matcher;
};
var not = (matcher) => {
return async (exchange) => {
const result = await matcher(exchange);
return result.match ? NO_MATCH : match();
};
};
var anyExchange = async (_exchange) => {
return match();
};
anyExchange.toString = () => "any-exchange";
var EMPTY_OBJECT = Object.freeze({});
var NO_MATCH = Object.freeze({ match: false, variables: EMPTY_OBJECT });
var match = (variables = EMPTY_OBJECT) => {
return { match: true, variables };
};
var pattern = (pattern2, opts) => {
const method = opts?.method;
const matcher = async (exchange) => {
const request = exchange.request;
const path = request.path;
if (method !== void 0 && request.method !== method) {
return NO_MATCH;
}
if (typeof pattern2 === "string") {
if (path === pattern2) {
return match();
}
return NO_MATCH;
} else {
const match2 = pattern2.exec(path);
if (match2 === null) {
return NO_MATCH;
}
return { match: true, variables: { ...match2.groups } };
}
};
matcher.toString = () => {
return `pattern(${pattern2.toString()}, method=${method ?? "<any>"})`;
};
return matcher;
};
var mediaType = (opts) => {
const shouldIgnore = (requestedMediaType) => {
if (opts.ignoredMediaTypes !== void 0) {
for (const ignoredMediaType of opts.ignoredMediaTypes) {
if (requestedMediaType === ignoredMediaType || ignoredMediaType === "*/*") {
return true;
}
}
}
return false;
};
return async (exchange) => {
const request = exchange.request;
let requestMediaTypes;
try {
requestMediaTypes = request.headers.list("accept");
} catch (e) {
return NO_MATCH;
}
for (const requestedMediaType of requestMediaTypes) {
if (shouldIgnore(requestedMediaType)) {
continue;
}
for (const mediaType2 of opts.mediaTypes) {
if (requestedMediaType.startsWith(mediaType2)) {
return match();
}
}
}
return NO_MATCH;
};
};
var upgradeMatcher = async ({ request }) => {
const upgrade = request.upgrade && request.headers.one("upgrade")?.toLowerCase() === "websocket";
return upgrade ? match() : NO_MATCH;
};
upgradeMatcher.toString = () => "websocket upgrade";
// src/app/route.ts
import { IOGateway as IOGateway4 } from "@interopio/gateway";
async function configure(app, config, routes) {
const applyCors = (request, options) => {
if (options?.cors) {
const cors = options.cors === true ? {
allowOrigins: options.origins?.allow?.map(IOGateway4.Filtering.regexify),
allowMethods: request.method === void 0 ? ["*"] : [request.method],
allowCredentials: options.authorize?.access !== "permitted" ? true : void 0
} : options.cors;
const path = request.path;
routes.cors.push([path, cors]);
}
};
const configurer = new class {
handle(...handlers) {
handlers.forEach(({ request, options, handler }) => {
const matcher = pattern(IOGateway4.Filtering.regexify(request.path), { method: request.method });
if (options?.authorize) {
routes.authorize.push([matcher, options.authorize]);
}
applyCors(request, options);
const middleware = async (exchange, next) => {
const { match: match2, variables } = await matcher(exchange);
if (match2) {
await handler(exchange, variables);
} else {
await next();
}
};
routes.middleware.push(middleware);
});
}
socket(...sockets) {
for (const { path, factory, options } of sockets) {
const route = path ?? "/";
routes.sockets.set(route, {
default: path === void 0,
ping: options?.ping,
factory,
maxConnections: options?.maxConnections,
authorize: options?.authorize,
originFilters: regexifyOriginFilters(options?.origins)
});
}
}
}();
await app(configurer, config);
}
// src/server/cors.ts
import { IOGateway as IOGateway5 } from "@interopio/gateway";
function isSameOrigin(request) {
const origin = request.headers.one("origin");
if (origin === void 0) {
return true;
}
const url = request.URL;
const actualProtocol = url.protocol;
const actualHost = url.host;
const originUrl = URL.parse(origin);
const originHost = originUrl?.host;
const originProtocol = originUrl?.protocol;
return actualProtocol === originProtocol && actualHost === originHost;
}
function isCorsRequest(request) {
return request.headers.has("origin") && !isSameOrigin(request);
}
function isPreFlightRequest(request) {
return request.method === "OPTIONS" && request.headers.has("origin") && request.headers.has("access-control-request-method");
}
var VARY_HEADERS = ["Origin", "Access-Control-Request-Method", "Access-Control-Request-Headers"];
var processRequest = (exchange, config) => {
const { request, response } = exchange;
const responseHeaders = response.headers;
if (!responseHeaders.has("Vary")) {
responseHeaders.set("Vary", VARY_HEADERS.join(", "));
} else {
const varyHeaders = responseHeaders.list("Vary");
for (const header of VARY_HEADERS) {
if (!varyHeaders.find((h) => h === header)) {
varyHeaders.push(header);
}
}
responseHeaders.set("Vary", varyHeaders.join(", "));
}
try {
if (!isCorsRequest(request)) {
return true;
}
} catch (e) {
if (logger.enabledFor("debug")) {
logger.debug(`reject: origin is malformed`);
}
rejectRequest(response);
return false;
}
if (responseHeaders.has("access-control-allow-origin")) {
if (logger.enabledFor("trace")) {
logger.debug(`skip: already contains "Access-Control-Allow-Origin"`);
}
return true;
}
const preFlightRequest = isPreFlightRequest(request);
if (config) {
return handleInternal(exchange, config, preFlightRequest);
}
if (preFlightRequest) {
rejectRequest(response);
return false;
}
return true;
};
var DEFAULT_PERMIT_ALL = ["*"];
var DEFAULT_PERMIT_METHODS = ["GET", "HEAD", "POST"];
var PERMIT_DEFAULT_CONFIG = {
allowOrigins: DEFAULT_PERMIT_ALL,
allowMethods: DEFAULT_PERMIT_METHODS,
allowHeaders: DEFAULT_PERMIT_ALL,
maxAge: 1800
// 30 minutes
};
function validateCorsConfig(config) {
if (config) {
const allowHeaders = config.allowHeaders;
if (allowHeaders && allowHeaders !== ALL) {
config = {
...config,
allowHeaders: allowHeaders.map((header) => header.toLowerCase())
};
}
const allowOrigins = config.allowOrigins;
if (allowOrigins) {
if (allowOrigins === "*") {
validateAllowCredentials(config);
validateAllowPrivateNetwork(config);
} else {
config = {
...config,
allowOrigins: allowOrigins.map((origin) => {
if (typeof origin === "string" && origin !== ALL) {
origin = IOGateway5.Filtering.regexify(origin);
if (typeof origin === "string") {
return trimTrailingSlash(origin).toLowerCase();
}
}
return origin;
})
};
}
}
return config;
}
}
function combine(source, other) {
if (other === void 0) {
return source !== void 0 ? source === ALL ? [ALL] : source : [];
}
if (source === void 0) {
return other === ALL ? [ALL] : other;
}
if (source == DEFAULT_PERMIT_ALL || source === DEFAULT_PERMIT_METHODS) {
return other === ALL ? [ALL] : other;
}
if (other == DEFAULT_PERMIT_ALL || other === DEFAULT_PERMIT_METHODS) {
return source === ALL ? [ALL] : source;
}
if (source === ALL || source.includes(ALL) || other === ALL || other.includes(ALL)) {
return [ALL];
}
const combined = /* @__PURE__ */ new Set();
source.forEach((v) => combined.add(v));
other.forEach((v) => combined.add(v));
return Array.from(combined);
}
var combineCorsConfig = (source, other) => {
if (other === void 0) {
return source;
}
const config = {
allowOrigins: combine(source.allowOrigins, other?.allowOrigins),
allowMethods: combine(source.allowMethods, other?.allowMethods),
allowHeaders: combine(source.allowHeaders, other?.allowHeaders),
exposeHeaders: combine(source.exposeHeaders, other?.exposeHeaders),
allowCredentials: other?.allowCredentials ?? source.allowCredentials,
allowPrivateNetwork: other?.allowPrivateNetwork ?? source.allowPrivateNetwork,
maxAge: other?.maxAge ?? source.maxAge
};
return config;
};
var corsFilter = (opts) => {
const source = opts.corsConfigSource;
const processor = opts.corsProcessor ?? processRequest;
return async (ctx, next) => {
const config = await source(ctx);
const isValid = processor(ctx, config);
if (!isValid || isPreFlightRequest(ctx.request)) {
return;
} else {
await next();
}
};
};
var cors_default = corsFilter;
var logger = getLogger("cors");
function rejectRequest(response) {
response.setStatusCode(HttpStatus.FORBIDDEN);
}
function handleInternal(exchange, config, preFlightRequest) {
const { request, response } = exchange;
const responseHeaders = response.headers;
const requestOrigin = request.headers.one("origin");
const allowOrigin = checkOrigin(config, requestOrigin);
if (allowOrigin === void 0) {
if (logger.enabledFor("debug")) {
logger.debug(`reject: '${requestOrigin}' origin is not allowed`);
}
rejectRequest(response);
return false;
}
const requestMethod = getMethodToUse(request, preFlightRequest);
const allowMethods = checkMethods(config, requestMethod);
if (allowMethods === void 0) {
if (logger.enabledFor("debug")) {
logger.debug(`reject: HTTP '${requestMethod}' is not allowed`);
}
rejectRequest(response);
return false;
}
const requestHeaders = getHeadersToUse(request, preFlightRequest);
const allowHeaders = checkHeaders(config, requestHeaders);
if (preFlightRequest && allowHeaders === void 0) {
if (logger.enabledFor("debug")) {
logger.debug(`reject: headers '${requestHeaders}' are not allowed`);
}
rejectRequest(response);
return false;
}
responseHeaders.set("Access-Control-Allow-Origin", allowOrigin);
if (preFlightRequest) {
responseHeaders.set("Access-Control-Allow-Methods", allowMethods.join(","));
}
if (preFlightRequest && allowHeaders !== void 0 && allowHeaders.length > 0) {
responseHeaders.set("Access-Control-Allow-Headers", allowHeaders.join(", "));
}
const exposeHeaders = config.exposeHeaders;
if (exposeHeaders && exposeHeaders.length > 0) {
responseHeaders.set("Access-Control-Expose-Headers", exposeHeaders.join(", "));
}
if (config.allowCredentials) {
responseHeaders.set("Access-Control-Allow-Credentials", "true");
}
if (config.allowPrivateNetwork && request.headers.one("access-control-request-private-network") === "true") {
responseHeaders.set("Access-Control-Allow-Private-Network", "true");
}
if (preFlightRequest && config.maxAge !== void 0) {
responseHeaders.set("Access-Control-Max-Age", config.maxAge.toString());
}
return true;
}
var ALL = "*";
var DEFAULT_METHODS = ["GET", "HEAD"];
function validateAllowCredentials(config) {
if (config.allowCredentials === true && config.allowOrigins === ALL) {
throw new Error(`when allowCredentials is true allowOrigins cannot be "*"`);
}
}
function validateAllowPrivateNetwork(config) {
if (config.allowPrivateNetwork === true && config.allowOrigins === ALL) {
throw new Error(`when allowPrivateNetwork is true allowOrigins cannot be "*"`);
}
}
function checkOrigin(config, origin) {
if (origin) {
const allowedOrigins = config.allowOrigins;
if (allowedOrigins) {
if (allowedOrigins === ALL) {
validateAllowCredentials(config);
validateAllowPrivateNetwork(config);
return ALL;
}
const originToCheck = trimTrailingSlash(origin.toLowerCase());
for (const allowedOrigin of allowedOrigins) {
if (allowedOrigin === ALL || IOGateway5.Filtering.valueMatches(allowedOrigin, originToCheck)) {
return origin;
}
}
}
}
}
function checkMethods(config, requestMethod) {
if (requestMethod) {
const allowedMethods = config.allowMethods ?? DEFAULT_METHODS;
if (allowedMethods === ALL) {
return [requestMethod];
}
if (IOGateway5.Filtering.valuesMatch(allowedMethods, requestMethod)) {
return allowedMethods;
}
}
}
function checkHeaders(config, requestHeaders) {
if (requestHeaders === void 0) {
return;
}
if (requestHeaders.length == 0) {
return [];
}
const allowedHeaders = config.allowHeaders;
if (allowedHeaders === void 0) {
return;
}
const allowAnyHeader = allowedHeaders === ALL || allowedHeaders.includes(ALL);
const result = [];
for (const requestHeader of requestHeaders) {
const value = requestHeader?.trim();
if (value) {
if (allowAnyHeader) {
result.push(value);
} else {
for (const allowedHeader of allowedHeaders) {
if (value.toLowerCase() === allowedHeader) {
result.push(value);
break;
}
}
}
}
}
if (result.length > 0) {
return result;
}
}
function trimTrailingSlash(origin) {
return origin.endsWith("/") ? origin.slice(0, -1) : origin;
}
function getMethodToUse(request, isPreFlig