@interopio/gateway-server
Version:
[](https://www.npmjs.com/package/@interopio/gateway-server)
4 lines • 597 kB
Source Map (JSON)
{
"version": 3,
"sources": ["../src/index.ts", "../src/server.ts", "../src/server/address.ts", "../src/logger.ts", "../src/gateway/ws/core.ts", "../src/gateway/gateway-manager.ts", "../src/gateway/config.ts", "../src/common/compose.ts", "../src/http/exchange.ts", "../src/http/media-type.ts", "../src/utils/mime-type-utils.ts", "../src/http/headers.ts", "../src/http/status.ts", "../src/server/exchange.ts", "../src/http/server/path.ts", "../src/http/server/exchange.ts", "../src/server/monitoring.ts", "../src/server/server-header.ts", "../src/server/ws-client-verify.ts", "../src/server/util/matchers.ts", "../src/app/route.ts", "../src/server/cors.ts", "../src/web/cors/utils.ts", "../src/app/cors.ts", "../src/server/security/types.ts", "../src/server/security/http-headers.ts", "../src/server/security/entry-point-failure-handler.ts", "../src/server/security/http-basic-entry-point.ts", "../src/server/security/http-basic-converter.ts", "../src/server/security/security-context.ts", "../src/server/security/authentication-filter.ts", "../src/server/security/http-status-entry-point.ts", "../src/server/security/delegating-entry-point.ts", "../src/server/security/delegating-success-handler.ts", "../src/server/security/http-basic.ts", "../src/server/security/oauth2/token-error.ts", "../src/server/security/oauth2/token-converter.ts", "../src/server/security/oauth2/token-entry-point.ts", "../src/server/security/oauth2/jwt-auth-manager.ts", "../src/server/security/oauth2-resource-server.ts", "../src/server/security/config.ts", "../src/server/security/error-filter.ts", "../src/server/security/delegating-authorization-manager.ts", "../src/server/security/authorization-filter.ts", "../src/server/security/exchange-filter.ts", "../src/server/security/x509-converter.ts", "../src/server/security/preauth/x509.ts", "../src/server/security/crypto/password.ts", "../src/server/security/crypto/argon2.ts", "../src/server/security/crypto/index.ts", "../src/server/security/users/types.ts", "../src/server/security/users.ts", "../src/server/security/x509.ts", "../src/web/server/handler.ts", "../src/server/security/web.ts", "../src/server/security/users/memory.ts", "../src/app/auth.ts", "../src/server/handler.ts", "../src/server/ws.ts", "../src/server/socket.ts", "../src/server/ws-pings.ts", "../src/server/ssl/config.ts", "../src/codec/hints.ts", "../src/codec/encoder.ts", "../src/http/codec/core.ts", "../src/http/codec/multipart.ts", "../src/http/codec/config.ts", "../src/manage/server.ts", "../src/manage/client.ts", "../src/server/welcome-page.ts", "../src/web/server/adapter.ts", "../src/web/util/client.ts", "../src/web/async/fn/server/request.ts", "../src/web/util/pattern/internal/path-element.ts", "../src/web/util/pattern/internal/separator-path-element.ts", "../src/web/util/pattern/internal/wildcard-path-element.ts", "../src/web/util/pattern/internal/capture-segments-path-element.ts", "../src/web/util/pattern/internal/wildcard-segments-path-element.ts", "../src/web/util/pattern/path-pattern.ts", "../src/web/util/pattern/pattern-parse-error.ts", "../src/web/util/pattern/internal/capture-variable-path-element.ts", "../src/web/util/pattern/internal/single-char-wildcarded-path-element.ts", "../src/web/util/pattern/internal/regex-path-element.ts", "../src/web/util/pattern/internal/literal-path-element.ts", "../src/web/util/pattern/internal/path-pattern-parser.ts", "../src/web/util/pattern/path-pattern-parser.ts", "../src/web/async/fn/server/router.ts", "../src/web/async/fn/server/response.ts", "../src/server/error.ts", "../src/web/error.ts", "../src/web/server/error.ts", "../src/web/async/handler.ts", "../src/web/async/fn/server/handler.ts"],
"sourcesContent": ["import * as GatewayServer from './server.ts';\n\nexport { GatewayServer };\nexport default GatewayServer.Factory;\n", "import http from 'node:http';\nimport https from 'node:https';\nimport type { AddressInfo, Socket } from 'node:net';\nimport { AsyncLocalStorage } from 'node:async_hooks';\nimport wsGateway from './gateway/ws/core.ts';\nimport { GatewayManager } from './gateway/gateway-manager.ts';\nimport { compose } from './common/compose.ts';\nimport {\n ExtendedHttpIncomingMessage,\n ExtendedHttpServerResponse,\n HttpServerRequest,\n HttpServerResponse\n} from './server/exchange.ts';\nimport getLogger from './logger.ts';\nimport { addressAndPort, localIp, portRange } from './server/address.ts';\nimport * as monitoring from './server/monitoring.ts';\nimport type { ServerConfig } from '../types/config.d.ts';\nimport { GatewayServer } from '@interopio/gateway-server';\nimport serverHeader from './server/server-header.ts';\nimport { configure, type RouteConfig } from './app/route.ts';\nimport { httpSecurity } from './app/auth.ts';\nimport type { ServerConfigurer, ServerHttpResponse, ServerWebExchange } from '../types/web/server.d.ts';\nimport { HttpHeadResponseDecorator } from './server/handler.ts';\nimport { initRoute } from './server/ws.ts';\nimport { type SocketRoute, webSockets } from './server/socket.ts';\nimport { HttpStatus } from './http/status.ts';\nimport { secureContextOptions } from './server/ssl/config.ts';\nimport codecConfig from './http/codec/config.ts';\nimport { gatewayManagementServer } from './manage/server.ts';\n\nconst logger = getLogger('app');\n\ntype RequestListener<\n Request extends typeof ExtendedHttpIncomingMessage = typeof ExtendedHttpIncomingMessage,\n Response extends typeof ExtendedHttpServerResponse<InstanceType<Request>> = typeof ExtendedHttpServerResponse,\n> = (req: InstanceType<Request>, resOrUpgradeHead: (InstanceType<Response> & { req: InstanceType<Request> }) | Buffer<ArrayBufferLike>) => void;\n\ntype ServerOptions = http.ServerOptions<typeof ExtendedHttpIncomingMessage, typeof ExtendedHttpServerResponse>;\n\nasync function createListener(builder: WebHttpHandlerBuilder,\n onSocketError: (err: Error) => void): Promise<RequestListener> {\n\n const httpHandler = builder.build();\n\n return async (req: ExtendedHttpIncomingMessage, resOrUpgradeHead: ExtendedHttpServerResponse | Buffer<ArrayBufferLike>) => {\n req.socket.addListener('error', onSocketError);\n let res: ExtendedHttpServerResponse;\n if (resOrUpgradeHead instanceof ExtendedHttpServerResponse) {\n res = resOrUpgradeHead;\n }\n else {\n req.upgradeHead = resOrUpgradeHead;\n res = new ExtendedHttpServerResponse(req);\n res.assignSocket(req.socket);\n }\n try {\n const request = new HttpServerRequest(req);\n const response = new HttpServerResponse(res);\n const decoratedResponse: ServerHttpResponse = request.method === 'HEAD' ? new HttpHeadResponseDecorator(response) : response;\n const hp: Promise<void> = httpHandler(request, decoratedResponse)\n try {\n await hp;\n if (logger.enabledFor('trace')) {\n logger.debug(`${request.id} handling completed.`);\n }\n }\n catch (e) {\n if (e instanceof Error && e['code'] === 'ERR_INVALID_URL') {\n throw e; // Let the outer catch handle this and return 400 Bad Request\n }\n if (logger.enabledFor('trace')) {\n logger.debug(`${request.id} failed to complete: ${e instanceof Error ? e.message : e}`);\n }\n }\n }\n catch (e) {\n if (e instanceof Error && e['code'] === 'ERR_INVALID_URL') {\n if (logger.enabledFor('debug')) {\n logger.debug(`failed to get request URI: ${e.message}`);\n }\n res.statusCode = 400; // Bad Request\n res.end(http.STATUS_CODES[400]);\n return Promise.resolve();\n }\n throw e;\n }\n };\n}\n\nfunction promisify<T>(fn: (callback?: (err?: Error) => void) => T): Promise<T> {\n return new Promise<T>((resolve, reject) => {\n const r = fn((err?: Error) => {\n if (err) {\n reject(err);\n } else {\n resolve(r);\n }\n });\n });\n}\n\nfunction memoryMonitor(config?: ServerConfig['memory']) {\n if (config) {\n return monitoring.start({\n memoryLimit: config.memory_limit,\n dumpLocation: config.dump_location,\n dumpPrefix: config.dump_prefix,\n reportInterval: config.report_interval,\n maxBackups: config.max_backups\n });\n }\n}\n\nimport info from '@interopio/gateway-server/package.json' with { type: 'json' };\nimport welcomePage from './server/welcome-page.ts';\nimport { WebHttpHandlerBuilder } from './web/server/adapter.ts';\nimport { DefaultErrorAttributes, DefaultWebErrorHandler } from './server/error.ts';\nimport { WebFilterChainProxy } from './server/security/web.ts';\nimport { ResponseStatusError } from './web/server/error.ts';\nimport { DispatcherHandler } from './web/async/handler.ts';\nimport {\n HandlerFunctionAdapter,\n RouterFunctionMapping,\n ServerResponseResultHandler\n} from './web/async/fn/server/handler.ts';\nexport const VERSION = `${info.name} - v${info.version}`;\n\nasync function initBuilder(context: RouteConfig) {\n const storage = context.storage;\n const security = await httpSecurity(context);\n // websocket upgrade handler\n const sockets = webSockets(context);\n const codecConfigurer = codecConfig.serverConfigurer.create();\n codecConfigurer.defaultCodecs.enableLoggingRequestDetails(true);\n const welcomePageRouterFunctionMapping = new RouterFunctionMapping();\n // welcomePageRouterFunctionMapping.init(await welcomePage(context.resourcesConfig?.locations));\n const h = new DispatcherHandler(\n [welcomePageRouterFunctionMapping],\n [new HandlerFunctionAdapter()],\n [new ServerResponseResultHandler(codecConfigurer.writers)]\n )\n const handler = {\n handle: compose<ServerWebExchange>(\n serverHeader(VERSION, context.serverHeader),\n ...sockets,\n ...context.middleware,\n // health check\n async ({ request, response }, next) => {\n if (request.method === 'GET' && request.path === '/health') {\n response.setStatusCode(HttpStatus.OK);\n const buffer = Buffer.from('UP', 'utf-8');\n response.headers.set('Content-Type', 'text/plain; charset=utf-8');\n await response.body(buffer);\n }\n else {\n await next();\n }\n },\n await welcomePage(context.resourcesConfig?.locations),\n // not found\n async ({ response }, _next) => {\n throw new ResponseStatusError(HttpStatus.NOT_FOUND);\n // response.setStatusCode(HttpStatus.NOT_FOUND);\n // await response.end();\n }\n )\n };\n\n return new WebHttpHandlerBuilder(handler)\n .filter(new WebFilterChainProxy(security))\n .codecConfigurer(codecConfigurer)\n .exceptionHandler(\n new DefaultWebErrorHandler(\n codecConfigurer.readers,\n codecConfigurer.writers,\n new DefaultErrorAttributes()))\n .storage(storage);\n}\n\nexport const Factory = async (options: ServerConfig): Promise<GatewayServer.Server> => {\n const ssl = options.ssl;\n const host = options.host;\n\n // Build serverCertificate config if auth.x509.key is provided\n const serverCertificate = options.auth?.x509?.key ? {\n host: host ?? 'localhost',\n key: options.auth.x509.key,\n passphrase: options.auth.x509.passphrase\n } : undefined;\n\n const createServer = ssl\n ? (options: ServerOptions, handler: RequestListener) => https.createServer({...options, ...secureContextOptions(ssl, serverCertificate)}, handler)\n : (options: ServerOptions, handler: RequestListener) => http.createServer(options, handler);\n const monitor = memoryMonitor(options.memory);\n const context: RouteConfig = {\n middleware: [],\n corsConfig: options.cors,\n cors: [],\n authConfig: options.auth,\n authorize: [],\n storage: new AsyncLocalStorage<{ exchange: ServerWebExchange }>(),\n sockets: new Map<string, SocketRoute>(),\n resourcesConfig: options.resources,\n };\n\n // Create gateway manager to handle default and per-principal gateway instances\n const gatewayManager = new GatewayManager({\n baseConfig: { ...(options.gateway) },\n scope: options.gateway?.scope ?? 'principal'\n });\n\n if (options.gateway) {\n const path = options.gateway.route ? (options.gateway.route === '/' ? undefined : options.gateway.route) : undefined;\n\n await configure(async (configurer: ServerConfigurer) => {\n configurer.socket({ path, factory: wsGateway.bind(gatewayManager), options: options.gateway })\n }, options, context);\n\n }\n if (options.app) {\n await configure(options.app, options, context)\n }\n\n const ports = portRange(options.port ?? 0);\n const onSocketError = (err: Error) => logger.error(`socket error: ${err}`, err);\n const builder = await initBuilder(context);\n\n const listener = await createListener(builder, onSocketError);\n\n const serverP = new Promise<http.Server<typeof ExtendedHttpIncomingMessage, typeof ExtendedHttpServerResponse>>((resolve, reject) => {\n\n const server = createServer({\n IncomingMessage: ExtendedHttpIncomingMessage,\n ServerResponse: ExtendedHttpServerResponse,\n ...options.http\n }, listener);\n\n server.on('error', (e: Error) => {\n if (e['code'] === 'EADDRINUSE') {\n logger.debug(`port ${e['port']} already in use on address ${e['address']}`);\n const {value: port} = ports.next();\n if (port) {\n logger.info(`retry starting server on port ${port} and host ${host ?? '<unspecified>'}`);\n server.close();\n server.listen(port, host);\n } else {\n logger.warn(`all configured port(s) ${options.port} are in use. closing...`);\n server.close();\n reject(e);\n }\n } else {\n logger.error(`server error: ${e.message}`, e);\n reject(e);\n }\n });\n server\n .on('listening', async () => {\n const info = server.address() as AddressInfo;\n\n for (const [path, route] of context.sockets) {\n const endpoint = `${ssl ? 'wss' : 'ws'}://${localIp}:${info.port}${path}`;\n await initRoute(path, route, endpoint, context.storage, onSocketError);\n }\n logger.info(`http server listening on ${ ssl ? 'https' : 'http' }://${addressAndPort(info)}`);\n resolve(server);\n });\n server\n .on('upgrade', (req: ExtendedHttpIncomingMessage, _socket: Socket, head: Buffer<ArrayBufferLike>) => {\n try {\n listener(req, head);\n } catch (err) {\n logger.error(`upgrade error: ${err}`, err);\n }\n })\n .on('close', async () => {\n logger.info(`http server closed.`);\n });\n try {\n const {value: port} = ports.next();\n server.listen(port, host);\n } catch (e) {\n logger.error(`error starting web socket server`, e);\n reject(e instanceof Error ? e : new Error(`listen failed: ${e}`));\n }\n });\n const server = await serverP;\n const closeActions = new Array<() => Promise<void>>();\n const closeSockets = async () => {\n for (const [path, route] of context.sockets) {\n try {\n if (route.close !== undefined) {\n await route.close();\n }\n } catch (e) {\n logger.warn(`error closing route ${path}`, e);\n }\n }\n };\n const closeServer = async () => {\n await promisify(cb => {\n server.closeAllConnections();\n server.close(cb);\n });\n };\n closeActions.push(closeSockets);\n closeActions.push(closeServer);\n\n closeActions.push(async () => {\n if (monitor) {\n await monitoring.stop(monitor);\n }\n });\n closeActions.push(async () => {\n // Stop all gateways (default + per-principal)\n await gatewayManager.stop();\n });\n const fullServer = new class implements GatewayServer.Server {\n // Expose the gateway manager which implements ScopedGateway\n readonly gateway = gatewayManager;\n\n get address(): AddressInfo | null {\n const address = server.address();\n return typeof address === 'object' ? address : null;\n }\n\n async close(): Promise<void> {\n const errors = new Array<unknown>();\n for (const action of closeActions) {\n try {\n await action();\n }\n catch (e) {\n errors.push(e);\n logger.warn(`error during server shutdown`, e);\n }\n }\n if (errors.length === 1) {\n throw errors[0];\n }\n else if (errors.length > 1) {\n throw new AggregateError(errors, 'multiple errors during server shutdown');\n }\n }\n }\n\n if (options.management?.server.path) {\n const managementServer =\n await gatewayManagementServer({\n options: options.management.server,\n info: async () => ({\n pid: process.pid,\n type: 'gateway-server',\n ...(gatewayManager.info())\n }),\n logger: logger.child('management'),\n shutdown: options.management.commands?.['shutdown'].enabled ? async () => {\n if (logger.enabledFor('info')) {\n logger.info(`stopping gateway-server...`);\n }\n await fullServer.close();\n } : undefined\n });\n closeActions.push(async () => {\n await promisify((cb) => {\n managementServer.close(cb);\n });\n });\n }\n\n return fullServer;\n}\n", "import type { AddressInfo } from 'node:net';\nimport { type NetworkInterfaceInfo, networkInterfaces } from 'node:os';\n\nconst PORT_RANGE_MATCHER = /^(\\d+|(0x[\\da-f]+))(-(\\d+|(0x[\\da-f]+)))?$/i;\nfunction validPort(port: number) {\n if (port > 0xFFFF) throw new Error(`bad port ${port}`);\n return port;\n}\n\n/**\n * parse port range. port can be number or string representing comma separated\n * list of port ranges for e.g. \"3434,8380-8385\"\n * @param port\n */\nexport function* portRange(port: number | string): Generator<number> {\n if (typeof port === 'string') {\n for (const portRange of port.split(',')) {\n const trimmed = portRange.trim();\n const matchResult = PORT_RANGE_MATCHER.exec(trimmed);\n if (matchResult) {\n const start = parseInt(matchResult[1]);\n const end = parseInt(matchResult[4] ?? matchResult[1]);\n for (let i = validPort(start); i < validPort(end) + 1; i++) {\n yield i;\n }\n }\n else {\n throw new Error(`'${portRange}' is not a valid port or range.`);\n }\n }\n } else {\n yield validPort(port);\n }\n}\n\nexport const localIp = (() => {\n function first<T>(a: T[]): T | undefined {\n return a.length > 0 ? a[0] : undefined;\n }\n const addresses = Object.values(networkInterfaces())\n .flatMap((details?: NetworkInterfaceInfo[]) => {\n return (details ?? []).filter((info) => info.family === 'IPv4');\n }).reduce((acc, info) => {\n acc[info.internal ? 'internal' : 'external'].push(info);\n return acc;\n }, {internal: [] as NetworkInterfaceInfo[], external: [] as NetworkInterfaceInfo[]});\n return (first(addresses.internal) ?? first(addresses.external))?.address;\n})();\n\nexport function addressAndPort(address?: AddressInfo) {\n if (address) {\n if (address.family === 'IPv6') {\n return `[${address.address}]:${address.port}`;\n }\n return `${address.address}:${address.port}`;\n }\n}\n", "import * as GatewayLogging from '@interopio/gateway/logging/core';\n\nexport type Logger = GatewayLogging.Logger;\n\nexport default function getLogger(name: string): Logger {\n return GatewayLogging.getLogger(`gateway.server.${name}`);\n}\n\n// This function is used to ensure that RegExp objects are displayed correctly when logging.\nexport function regexAwareReplacer<T>(_key: string, value: T): string | T {\n return value instanceof RegExp ? value.toString() : value;\n}\n", "import { addressAndPort } from '../../server/address.ts';\nimport type { WebSocket } from 'ws';\nimport getLogger from '../../logger.ts';\nimport type { Authentication } from '../../server/security/types.ts';\nimport { IOGateway } from '@interopio/gateway';\nimport { GatewayManager } from '../gateway-manager.ts';\n\nimport type {ServerWebSocketHandler} from '../../../types/web/server';\nimport type {AddressInfo} from 'node:net';\nimport {AsyncLocalStorage} from 'node:async_hooks';\n\nconst log = getLogger('ws');\nconst codec = IOGateway.Encoding.json<IOGateway.Message>();\n\nfunction principalName(authentication: Authentication): string | undefined {\n let name: string | undefined;\n if (authentication.authenticated) {\n name = authentication.name;\n if (name === undefined) {\n if (authentication['principal'] !== undefined) {\n const principal = authentication['principal'];\n if (typeof principal === 'object' && principal !== null && ('username' in principal || 'name' in principal)) {\n name = (principal as { username?: string }).username ?? (principal as { name?: string }).name;\n }\n if (name === undefined) {\n if (principal === undefined || principal === null) {\n name = '';\n }\n else {\n name = String(principal);\n }\n }\n }\n }\n }\n return name;\n}\n\nfunction initClient(gateway: IOGateway.Gateway,\n socket: WebSocket,\n authenticationPromise: () => Promise<Authentication | undefined>,\n remoteAddress?: AddressInfo\n): IOGateway.GatewayClient<string> | undefined {\n const key = addressAndPort(remoteAddress);\n const host = remoteAddress?.address ?? '<unknown>';\n const opts = {\n key,\n host,\n codec,\n onAuthenticate: async (): Promise<IOGateway.Auth.AuthenticationSuccess> => {\n const authentication = (await authenticationPromise());\n if (authentication?.authenticated) {\n return {type: 'success', user: principalName(authentication)};\n }\n throw new Error(`no valid client authentication ${key}`);\n },\n onPing: () => {\n socket.ping((err?: Error) => {\n if (err) {\n log.warn(`failed to ping ${key}`, err);\n }\n else {\n log.info(`ping sent to ${key}`);\n }\n });\n },\n onDisconnect: (reason: 'inactive' | 'shutdown') => {\n switch (reason) {\n case 'inactive': {\n log.warn(`no heartbeat (ping) received from ${key}, closing socket`);\n socket.close(4001, 'ping expected');\n break;\n }\n case 'shutdown': {\n socket.close(1001, 'shutdown');\n break;\n }\n }\n }\n };\n try {\n return gateway.client((data) => socket.send(data), opts as IOGateway.GatewayClientOptions<string>);\n } catch (err) {\n log.warn(`${key} failed to create client`, err);\n }\n}\n\nasync function create(this: GatewayManager, environment: {\n endpoint: string,\n storage?: AsyncLocalStorage<{ }>\n}): Promise<ServerWebSocketHandler> {\n log.info(`starting gateway on ${environment.endpoint}`);\n await this.start(environment);\n\n\n return async ({socket, handshake}) => {\n const { logPrefix, remoteAddress, principal: principalPromise } = handshake;\n const user = (await principalPromise())?.name;\n log.info(`${logPrefix}connected on gw as ${user ?? '<anonymous>'}`);\n\n const gw = await this.getGateway(user);\n const client = initClient(gw, socket, principalPromise, remoteAddress);\n if (!client) {\n log.error(`${logPrefix}gw client init failed`);\n socket.terminate();\n return;\n }\n socket.on('error', (err: Error) => {\n log.error(`${logPrefix}websocket error: ${err}`, err);\n });\n\n const contextFn = environment.storage !== undefined ? AsyncLocalStorage.snapshot() : undefined;\n socket.on('message', (data, _isBinary) => {\n if (Array.isArray(data)) {\n data = Buffer.concat(data);\n }\n // JSON.parse will invoke abstract operation ToString (coerce it) prior parsing (https://262.ecma-international.org/5.1/#sec-15.12.2)\n if (contextFn !== undefined) {\n contextFn(() => client.send(data as unknown as string))\n }\n else {\n client.send(data as unknown as string);\n }\n\n });\n socket.on('close', (code) => {\n log.info(`${logPrefix}disconnected from gw. code: ${code}`);\n client.close();\n });\n }\n}\n\nexport default create;\n", "import { IOGateway } from '@interopio/gateway';\nimport getLogger from '../logger.ts';\nimport type { GatewayServer } from '../../gateway-server.ts';\nimport { processGatewayConfig } from './config.ts';\n\nconst log = getLogger('gateway-manager');\n\nfunction generateRandomGatewayId(): string {\n return globalThis.crypto.randomUUID().replaceAll('-', '');\n}\n\n\nexport interface GatewayManagerConfig {\n /**\n * Base gateway configuration to use for all gateway instances.\n */\n baseConfig: IOGateway.GatewayConfig & {\n metrics?: IOGateway.MetricsConfig & { enabled?: boolean },\n mesh?: IOGateway.MeshConfig & { enabled?: boolean }\n };\n\n /**\n * Gateway scope - determines how gateway instances are managed:\n * - 'principal': Each authenticated principal gets their own gateway instance.\n * A default (shared) gateway instance handles all unauthenticated connections.\n * - 'singleton': All connections share the same gateway instance\n *\n * Default: 'principal'\n */\n scope?: 'singleton' | 'principal';\n}\n\n/**\n * Manages multiple Gateway instances - one default and optionally one per authenticated principal.\n * Each gateway instance gets a unique node ID to ensure proper isolation.\n */\nexport class GatewayManager implements GatewayServer.ScopedGateway {\n readonly #defaultGatewayId: string;\n readonly #defaultGateway: IOGateway.Gateway;\n readonly #managedGateways = new Map<string, IOGateway.Gateway>();\n readonly #principalToGatewayId = new Map<string, string>();\n readonly #config: GatewayManagerConfig;\n #started = false;\n #environment?: { endpoint?: string };\n\n constructor(config: GatewayManagerConfig) {\n this.#config = {\n baseConfig: processGatewayConfig(config.baseConfig),\n scope: config.scope ?? 'principal'\n }\n\n // Use configured gateway ID or generate a random one for the default gateway\n this.#defaultGatewayId = config.baseConfig.node ?? generateRandomGatewayId();\n\n if (log.enabledFor('debug')) {\n log.debug(`creating default gateway with gateway id: ${this.#defaultGatewayId}`);\n }\n\n this.#defaultGateway = IOGateway.Factory({...this.#config.baseConfig, node: this.#defaultGatewayId});\n }\n\n /**\n * Starts the default gateway.\n */\n async start(environment?: { endpoint?: string }): Promise<this> {\n if (this.#started) {\n return this;\n }\n\n // Store environment for use with per-principal gateways\n this.#environment = environment;\n\n log.debug('starting default gateway');\n await this.#defaultGateway.start(environment);\n this.#started = true;\n return this;\n }\n\n /**\n * Gets a gateway instance for the given principal.\n * If scope is 'singleton' or no principal is provided, returns the default gateway.\n * Otherwise, creates a new gateway instance for the principal if it doesn't exist.\n */\n async getGateway(principal?: string): Promise<IOGateway.Gateway> {\n // If scope is singleton or no principal, use default\n if (this.#config.scope === 'singleton' || !principal) {\n return this.#defaultGateway;\n }\n\n // Check if we already have a gateway for this principal\n const gatewayId = this.#principalToGatewayId.get(principal);\n let gateway = gatewayId ? this.#managedGateways.get(gatewayId) : undefined;\n\n if (!gateway) {\n if (log.enabledFor('debug')) {\n log.debug(`no existing gateway for principal '${principal}', creating new one`);\n }\n gateway = await this.#createPrincipalGateway(principal);\n } else {\n if (log.enabledFor('debug')) {\n log.debug(`reusing existing gateway for principal '${principal}'`);\n }\n }\n\n return gateway;\n }\n\n /**\n * Creates a new gateway instance for a specific principal with a unique gateway ID.\n */\n async #createPrincipalGateway(principal: string): Promise<IOGateway.Gateway> {\n const gatewayId = generateRandomGatewayId();\n const config = {\n ...this.#config.baseConfig,\n node: gatewayId\n };\n\n if (log.enabledFor('debug')) {\n log.debug(`creating gateway for principal '${principal}' with gateway id: ${config.node}`);\n }\n const gateway = IOGateway.Factory(config);\n\n // Track the principal-to-gateway ID mapping\n this.#principalToGatewayId.set(principal, gatewayId);\n this.#managedGateways.set(gatewayId, gateway);\n\n // Start the gateway with the same environment as the default gateway\n await gateway.start(this.#environment);\n\n return gateway;\n }\n\n // ScopedGateway interface methods\n\n /**\n * Get all gateway instances including the default.\n */\n getGateways(): Map<string, IOGateway.Gateway> {\n const allGateways = new Map<string, IOGateway.Gateway>(this.#managedGateways);\n allGateways.set(this.#defaultGatewayId, this.#defaultGateway);\n return allGateways;\n }\n\n /**\n * Gets information about specific or all active gateways.\n * - If no gatewayId is provided: returns aggregated info for all gateways (default + managed).\n * - If default gateway ID is provided: returns info only for the default gateway.\n * - If a non-default gateway ID is provided: returns info only for that specific gateway. If not found, throws an error.\n */\n info(gatewayId?: string): Record<string, unknown> {\n // Case 1: Specific non-default gateway ID provided\n if (gatewayId && this.#defaultGatewayId !== gatewayId) {\n const gateway = this.#managedGateways.get(gatewayId);\n if (gateway) {\n return gateway.info();\n } else {\n throw new Error(`no gateway found with ID: ${gatewayId}`);\n }\n }\n\n // Case 2: Default gateway ID explicitly provided - return only default info\n if (gatewayId === this.#defaultGatewayId) {\n return this.#defaultGateway.info();\n }\n\n // Case 3: No gateway ID provided - return aggregated info\n return {\n ...this.#defaultGateway.info(),\n managedGateways: this.#managedGateways.size,\n scope: this.#config.scope\n };\n }\n\n /**\n * Stops specific or all gateway instances.\n * - If no gatewayId is provided: stops all gateways (default + managed), returning the stopped default.\n * - If default gateway ID is provided: stops only the default gateway, leaving managed gateways running.\n * - If a non-default gateway ID is provided: stops only that specific gateway. If not found, throws an error.\n * @param gatewayId - ID of the gateway to stop.\n * @returns stopped gateway instance.\n */\n async stop(gatewayId?: string): Promise<IOGateway.Gateway> {\n // Case 1: Specific non-default gateway ID provided\n if (gatewayId && this.#defaultGatewayId !== gatewayId) {\n const gateway = this.#managedGateways.get(gatewayId);\n if (gateway) {\n log.info(`stopping gateway with ID: ${gatewayId}`);\n await gateway.stop();\n this.#managedGateways.delete(gatewayId);\n\n // Remove principal mapping for this gateway\n for (const [principal, gId] of this.#principalToGatewayId.entries()) {\n if (gId === gatewayId) {\n this.#principalToGatewayId.delete(principal);\n break;\n }\n }\n\n return gateway;\n } else {\n throw new Error(`no gateway found with ID: ${gatewayId}`);\n }\n }\n\n // Case 2: Default gateway ID explicitly provided - stop only default\n if (gatewayId === this.#defaultGatewayId) {\n log.debug('stopping default gateway (managed gateways will continue running)');\n await this.#defaultGateway.stop();\n this.#started = false;\n return this.#defaultGateway;\n }\n\n // Case 3: No gateway ID provided - stop all gateways\n log.info(`stopping all gateways (1 default + ${this.#managedGateways.size} managed)`);\n\n // Stop all managed gateways\n for (const [id, gateway] of this.#managedGateways.entries()) {\n if (log.enabledFor('debug')) {\n log.debug(`stopping gateway with ID: ${id}`);\n }\n await gateway.stop();\n }\n this.#managedGateways.clear();\n this.#principalToGatewayId.clear();\n\n log.debug('stopping default gateway');\n await this.#defaultGateway.stop();\n this.#started = false;\n return this.#defaultGateway;\n }\n\n\n /**\n * Get the gateway ID for a specific principal.\n */\n getPrincipalGatewayId(principal: string): string | undefined {\n return this.#principalToGatewayId.get(principal);\n }\n\n /**\n * Get all principal-to-gateway ID mappings.\n */\n getPrincipalGatewayIds(): Map<string, string> {\n return new Map(this.#principalToGatewayId);\n }\n\n\n // Gateway interface delegation to defaultGateway\n\n /**\n * Get the default gateway instance.\n */\n getDefaultGateway(): IOGateway.Gateway {\n return this.#defaultGateway;\n }\n\n /**\n * Create a gateway client with the specified handler.\n * Delegates to the default gateway.\n */\n client<T = IOGateway.Message>(\n handler: (this: IOGateway.GatewayClient<T>, msg: T) => void,\n opts?: IOGateway.GatewayClientOptions<T>\n ): IOGateway.GatewayClient<T> {\n return this.#defaultGateway.client(handler, opts);\n }\n\n /**\n * Connect to the gateway with the specified handler.\n * Delegates to the default gateway.\n * @deprecated\n */\n async connect(\n handler: (client: IOGateway.GatewayClient, msg: IOGateway.Message) => void\n ): Promise<IOGateway.GatewayClient> {\n return this.#defaultGateway.connect(handler);\n }\n\n /**\n * Gets the number of active principal gateways.\n */\n getPrincipalCount(): number {\n return this.#managedGateways.size;\n }\n}\n", "import { IOGateway } from '@interopio/gateway';\n\n/**\n * Processes visibility rules by converting string matchers to RegExp where appropriate.\n */\nexport function processVisibilityRules<K extends string>(rules?: IOGateway.VisibilityRule<K>[]) {\n if (rules !== undefined) {\n return rules.map(rule => {\n const result = {...rule} as IOGateway.VisibilityRule<K>;\n\n // Process all keys in the rule that might be matchers (context, method, domain, etc.)\n // Exclude known non-matcher properties: identity and restrictions\n for (const [key, value] of Object.entries(rule)) {\n if (key !== 'identity' && key !== 'restrictions' && value !== undefined) {\n // Value should be a Matcher type (string, RegExp, or array)\n (result as Record<K, IOGateway.Filtering.Matcher>)[key] = IOGateway.Filtering.regexify(value as IOGateway.Filtering.Matcher);\n }\n }\n\n // Process identity matchers separately\n if (rule.identity !== undefined) {\n result.identity = {};\n for (const [key, value] of Object.entries(rule.identity)) {\n result.identity[key] = IOGateway.Filtering.regexify(value);\n }\n }\n\n return result;\n });\n }\n}\n\n/**\n * Processes metric filters by converting matchers to RegExp and merging deprecated properties.\n */\nexport function processMetricFilters(filters?: IOGateway.MetricFilters): IOGateway.MetricFilters | undefined {\n if (!filters) {\n return undefined;\n }\n\n const result = {...filters};\n\n if (filters.publishers) {\n result.publishers = filters.publishers.map(publisher => {\n const { metrics: originalMetrics, identity: originalIdentity, ...rest } = publisher;\n\n // Build result object with proper typing\n type ProcessedPublisher = Omit<typeof publisher, 'metrics' | 'identity'> & {\n identity: Record<string, IOGateway.Filtering.Matcher>;\n metrics: {\n allow?: IOGateway.Filtering.Matcher[];\n block?: IOGateway.Filtering.Matcher[];\n };\n };\n\n const processedPublisher = { ...rest } as ProcessedPublisher;\n\n // Process identity matchers (always set, even if empty)\n processedPublisher.identity = {};\n if (originalIdentity) {\n for (const [key, value] of Object.entries(originalIdentity)) {\n processedPublisher.identity[key] = IOGateway.Filtering.regexify(value);\n }\n }\n\n // Process metrics matchers (allow, block, whitelist, blacklist)\n // Note: whitelist/blacklist are deprecated, merge them with allow/block\n processedPublisher.metrics = {};\n if (originalMetrics) {\n const allow = (originalMetrics.allow ?? originalMetrics.whitelist ?? []).map(m => IOGateway.Filtering.regexify(m));\n const block = (originalMetrics.block ?? originalMetrics.blacklist ?? []).map(m => IOGateway.Filtering.regexify(m));\n\n // Only include non-empty arrays to avoid meaningless empty filters\n if (allow.length > 0) {\n processedPublisher.metrics.allow = allow;\n }\n if (block.length > 0) {\n processedPublisher.metrics.block = block;\n }\n }\n\n return processedPublisher as typeof publisher;\n });\n }\n\n return result;\n}\n\n/**\n * Processes publisher configuration by processing its filters.\n */\nexport function processPublisherConfig<T extends IOGateway.BasicMetricsPublisherConfig>(publisherConfig?: T): T | undefined {\n if (!publisherConfig) {\n return undefined;\n }\n\n const result = {...publisherConfig};\n\n if (publisherConfig.filters) {\n result.filters = processMetricFilters(publisherConfig.filters);\n }\n\n return result;\n}\n\n/**\n * Processes mesh configuration, returning undefined if disabled.\n */\nexport function processMeshConfig(meshConfig?: IOGateway.MeshConfig & { enabled?: boolean }): IOGateway.MeshConfig | undefined {\n if (meshConfig?.enabled === false) {\n return undefined;\n }\n return meshConfig;\n}\n\n/**\n * Processes metrics configuration by handling filters and publisher configs.\n */\nexport function processMetricsConfig(config?: IOGateway.MetricsConfig & { enabled?: boolean }): IOGateway.MetricsConfig | undefined {\n if (config === undefined || config?.enabled === false) {\n return undefined;\n }\n const result = { ...(config) };\n\n // Process top-level filters\n if (config.filters) {\n result.filters = processMetricFilters(config.filters);\n }\n\n // Process file publisher config\n if (config.file) {\n result.file = processPublisherConfig(config.file);\n }\n\n // Process rest publisher config\n if (config.rest) {\n result.rest = processPublisherConfig(config.rest);\n }\n\n // Process publishers array (which can contain custom publishers with filters)\n if (config.publishers) {\n result.publishers = config.publishers.map(publisher => {\n if (typeof publisher === 'string') {\n return [publisher];\n }\n // It's a CustomMetricsPublisherConfig\n const c = processPublisherConfig(publisher);\n if (c !== undefined) {\n return [c];\n }\n return [];\n }).flat();\n }\n\n return result;\n}\n\n/**\n * Processes gateway configuration by converting string matchers to RegExp and handling special flags.\n * This ensures that configurations loaded from JSON files work correctly.\n */\nexport function processGatewayConfig(config: IOGateway.GatewayConfig & {\n metrics?: IOGateway.MetricsConfig & { enabled?: boolean },\n mesh?: IOGateway.MeshConfig & { enabled?: boolean }\n}): IOGateway.GatewayConfig {\n // regexify in case config is coming from JSON config file\n const result = {...config};\n\n if (config.contexts) {\n result.contexts = {\n ...config.contexts,\n visibility: processVisibilityRules(config.contexts.visibility)\n };\n }\n\n if (config.methods) {\n result.methods = {\n ...config.methods,\n visibility: processVisibilityRules(config.methods.visibility)\n };\n }\n\n if (config.peers) {\n result.peers = {\n ...config.peers,\n visibility: processVisibilityRules(config.peers.visibility)\n };\n }\n\n if (config.metrics) {\n result.metrics = processMetricsConfig(config.metrics);\n }\n\n if (config.mesh) {\n result.mesh = processMeshConfig(config.mesh);\n }\n\n return result;\n}\n", "type MiddlewareFunction<T> = ((ctx: T, next: (ctx?: T) => Promise<void>) => Promise<void>) | ((ctx: T) => Promise<void>);\n\n// https://github.com/koajs/compose/blob/master/index.js\n/**\n * @typeParam T - Type of context passed through the middleware\n * @param middleware middleware stack\n * @return {MiddlewareFunction}\n */\nexport function compose<T>(...middleware: MiddlewareFunction<T>[]): (ctx: T) => Promise<void> {\n if (!Array.isArray(middleware)) {\n throw new Error('middleware must be array!');\n }\n const fns = middleware.flat();\n for (const fn of fns) {\n if (typeof fn !== 'function') {\n throw new Error('middleware must be compose of functions!');\n }\n }\n return async function (ctx: T, next?: (ctx?: T) => Promise<void>) {\n const dispatch = async (i: number, dispatchedCtx: T): Promise<void> => {\n const fn = i === fns.length ? next : fns[i];\n if (fn === undefined) {\n return;\n }\n let nextCalled = false;\n let nextResolved = false;\n const nextFn = async (nextCtx?: T) => {\n if (nextCalled) {\n throw new Error('next() called multiple times');\n }\n nextCalled = true;\n try {\n return await dispatch(i + 1, nextCtx ?? dispatchedCtx);\n }\n finally {\n nextResolved = true;\n }\n };\n const result = await fn(dispatchedCtx, nextFn);\n if (nextCalled && !nextResolved) {\n throw new Error('middleware resolved before downstream.\\n\\t You are probably missing an await or return statement in your middleware function.');\n }\n return result;\n }\n\n return dispatch(0, ctx);\n }\n}\n", "import type { HttpCookie, HttpHeaders, ResponseCookie } from '../../types/web/http';\nimport { type AddressInfo, isIP } from 'node:net';\nimport { Cookie } from 'tough-cookie';\n\nfunction parseHost(headers: HttpHeaders, defaultHost?: string): string | undefined {\n let host = headers.get('x-forwarded-host');\n if (Array.isArray(host)) {\n host = host[0];\n }\n if (host) {\n const port = headers.one('x-forwarded-port');\n if (port) {\n host = `${host}:${port}`;\n }\n }\n host ??= headers.one('host');\n if (Array.isArray(host)) {\n host = host[0];\n }\n if (host) {\n return (host as string).split(',', 1)[0].trim();\n }\n\n return defaultHost;\n}\n\nfunction isForwardedSsl(headers: HttpHeaders): boolean {\n const v = headers.one('x-forwarded-ssl');\n return (typeof v === 'string') && (v.toLowerCase() === 'on');\n}\n\nfunction parseProtocol(headers: HttpHeaders, defaultProtocol: string): string {\n let proto = headers.get('x-forwarded-proto');\n\n if (Array.isArray(proto)) {\n proto = proto[0];\n }\n if (proto !== undefined) {\n return (proto as string).split(',', 1)[0].trim();\n }\n else if (isForwardedSsl(headers)) {\n return 'https';\n }\n return defaultProtocol;\n}\n\nfunction parseRemoteAddress(url: URL, headers: HttpHeaders, remoteAddress?: AddressInfo) : AddressInfo | undefined {\n const port = remoteAddress ? remoteAddress.port : 'https:' === url.protocol ? 443 : 80;\n\n let host = headers.one('x-forwarded-for');\n if (Array.isArray(host)) {\n host = host[0];\n }\n if (host !== undefined) {\n host = (host as string).split(',', 1)[0].trim();\n return { address: host, port: Number(port), family: isIP(host) === 6 ? 'IPv6' : 'IPv4' };\n }\n}\n\nexport abstract class AbstractHttpMessage<Headers extends HttpHeaders = HttpHeaders> {\n readonly #headers: Headers;\n protected constructor(headers: Headers) {\n this.#headers = headers;\n }\n get headers(): Headers {\n return this.#headers;\n }\n}\n\nexport abstract class AbstractHttpRequest<Headers extends HttpHeaders = HttpHeaders> extends AbstractHttpMessage<Headers> {\n private static logIdCounter = 0;\n #id?: string\n\n get id(): string {\n if (this.#id === undefined) {\n this.#id = `${this.initId()}-${++AbstractHttpRequest.logIdCounter}`;\n\n }\n return this.#id;\n }\n\n protected initId(): string {\n return 'request';\n }\n\n get cookies(): HttpCookie[] {\n return parseCookies(this.headers);\n }\n\n protected parseHost(defaultHost?: string): string | undefined {\n return parseHost(this.headers, defaultHost);\n }\n\n protected parseProtocol(defaultProtocol: string): string {\n return parseProtocol(this.headers, defaultProtocol);\n }\n\n abstract get URL(): URL;\n\n protected parseRemoteAddress(remoteAddress?: AddressInfo) {\n return parseRemoteAddress(this.URL, this.headers, remoteAddress);\n }\n}\n\nexport abstract class AbstractHttpResponse<Headers extends HttpHeaders = HttpHeaders> extends AbstractHttpMessage<Headers> {\n get cookies(): ResponseCookie[] {\n return parseResponseCookies(this.headers);\n }\n\n protected setCookieValue(responseCookie: ResponseCookie): string {\n const cookie = new Cookie({\n key: responseCookie.name,\n value: responseCookie.value,\n maxAge: responseCookie.maxAge,\n domain: responseCookie.domain,\n path: responseCookie.path,\n