UNPKG

@interopio/gateway-server

Version:
1,709 lines (1,695 loc) 105 kB
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