@forklaunch/core 0.7.3 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/cache/index.ts","../../../src/cache/cacheKey.ts","../../../src/cache/redisTtlCache.ts","../../../src/http/middleware/request/cors.middleware.ts","../../../src/http/middleware/request/createContext.middleware.ts","../../../src/http/telemetry/constants.ts","../../../src/http/guards/isExpressLikeSchemaHandler.ts","../../../src/http/middleware/request/auth.middleware.ts","../../../src/services/getEnvVar.ts","../../../src/http/telemetry/openTelemetryCollector.ts","../../../src/http/guards/isForklaunchRequest.ts","../../../src/http/telemetry/pinoLogger.ts","../../../src/http/middleware/request/parse.middleware.ts","../../../src/http/middleware/response/parse.middleware.ts","../../../src/http/telemetry/recordMetric.ts","../../../src/services/configInjector.ts","../../../src/http/telemetry/evaluateTelemetryOptions.ts"],"sourcesContent":["export * from './cacheKey';\nexport * from './interfaces/ttlCache.interface';\nexport * from './redisTtlCache';\nexport * from './types/ttlCacheRecord.types';\n","/**\n * Creates a function that generates cache keys with a given prefix.\n *\n * @param {string} cacheKeyPrefix - The prefix to use for all cache keys\n * @returns {Function} A function that takes an ID and returns a formatted cache key\n * @example\n * const createUserCacheKey = createCacheKey('user');\n * const key = createUserCacheKey('123'); // Returns 'user:123'\n */\nexport const createCacheKey = (cacheKeyPrefix: string) => (id: string) => {\n return `${cacheKeyPrefix}:${id}`;\n};\n","import { createClient, RedisClientOptions } from 'redis';\nimport {\n evaluateTelemetryOptions,\n MetricsDefinition,\n TelemetryOptions\n} from '../http';\nimport { OpenTelemetryCollector } from '../http/telemetry/openTelemetryCollector';\nimport { TtlCache } from './interfaces/ttlCache.interface';\nimport { TtlCacheRecord } from './types/ttlCacheRecord.types';\n\n/**\n * Type representing a raw reply from Redis commands.\n * Can be a string, number, Buffer, null, undefined, or array of raw replies.\n */\ntype RedisCommandRawReply =\n | string\n | number\n | Buffer\n | null\n | undefined\n | Array<RedisCommandRawReply>;\n\n/**\n * Class representing a Redis-based TTL (Time-To-Live) cache.\n * Implements the TtlCache interface to provide caching functionality with automatic expiration.\n */\nexport class RedisTtlCache implements TtlCache {\n private client;\n private telemetryOptions;\n\n /**\n * Creates an instance of RedisTtlCache.\n *\n * @param {number} ttlMilliseconds - The default Time-To-Live in milliseconds for cache entries\n * @param {OpenTelemetryCollector<MetricsDefinition>} openTelemetryCollector - Collector for OpenTelemetry metrics\n * @param {RedisClientOptions} hostingOptions - Configuration options for the Redis client\n * @param {TelemetryOptions} telemetryOptions - Configuration options for telemetry\n */\n constructor(\n private ttlMilliseconds: number,\n private openTelemetryCollector: OpenTelemetryCollector<MetricsDefinition>,\n hostingOptions: RedisClientOptions,\n telemetryOptions: TelemetryOptions\n ) {\n this.telemetryOptions = evaluateTelemetryOptions(telemetryOptions);\n this.client = createClient(hostingOptions);\n if (this.telemetryOptions.enabled.logging) {\n this.client.on('error', (err) => this.openTelemetryCollector.error(err));\n this.client.connect().catch(this.openTelemetryCollector.error);\n }\n }\n\n /**\n * Parses a raw Redis reply into the expected type.\n * Handles null values, arrays, buffers, and JSON strings.\n *\n * @template T - The expected type of the parsed value\n * @param {RedisCommandRawReply} value - The raw value from Redis to parse\n * @returns {T} The parsed value cast to type T\n */\n private parseValue<T>(value: RedisCommandRawReply): T {\n if (value == null) {\n return null as T;\n }\n\n if (Array.isArray(value)) {\n return value.map((v) => this.parseValue<T>(v)) as T;\n }\n\n if (Buffer.isBuffer(value)) {\n return value.toJSON() as T;\n }\n\n switch (typeof value) {\n case 'object':\n case 'string':\n return JSON.parse(value);\n case 'number':\n return value as T;\n }\n }\n\n /**\n * Puts a record into the Redis cache.\n *\n * @template T - The type of value being cached\n * @param {TtlCacheRecord<T>} param0 - The cache record containing key, value and optional TTL\n * @param {string} param0.key - The key to store the value under\n * @param {T} param0.value - The value to cache\n * @param {number} [param0.ttlMilliseconds] - Optional TTL in milliseconds, defaults to constructor value\n * @returns {Promise<void>} A promise that resolves when the value is cached\n */\n async putRecord<T>({\n key,\n value,\n ttlMilliseconds = this.ttlMilliseconds\n }: TtlCacheRecord<T>): Promise<void> {\n if (this.telemetryOptions.enabled.logging) {\n this.openTelemetryCollector.info(`Putting record into cache: ${key}`);\n }\n await this.client.set(key, JSON.stringify(value), {\n PX: ttlMilliseconds\n });\n }\n\n /**\n * Puts multiple records into the Redis cache in a single transaction.\n *\n * @template T - The type of values being cached\n * @param {TtlCacheRecord<T>[]} cacheRecords - Array of cache records to store\n * @returns {Promise<void>} A promise that resolves when all values are cached\n */\n async putBatchRecords<T>(cacheRecords: TtlCacheRecord<T>[]): Promise<void> {\n let multiCommand = this.client.multi();\n for (const { key, value, ttlMilliseconds } of cacheRecords) {\n multiCommand = multiCommand.set(key, JSON.stringify(value), {\n PX: ttlMilliseconds || this.ttlMilliseconds\n });\n }\n await multiCommand.exec();\n }\n\n /**\n * Adds a value to the left end of a Redis list.\n *\n * @template T - The type of value being enqueued\n * @param {string} queueName - The name of the Redis list\n * @param {T} value - The value to add to the list\n * @returns {Promise<void>} A promise that resolves when the value is enqueued\n */\n async enqueueRecord<T>(queueName: string, value: T): Promise<void> {\n await this.client.lPush(queueName, JSON.stringify(value));\n }\n\n /**\n * Adds multiple values to the left end of a Redis list in a single transaction.\n *\n * @template T - The type of values being enqueued\n * @param {string} queueName - The name of the Redis list\n * @param {T[]} values - Array of values to add to the list\n * @returns {Promise<void>} A promise that resolves when all values are enqueued\n */\n async enqueueBatchRecords<T>(queueName: string, values: T[]): Promise<void> {\n let multiCommand = this.client.multi();\n for (const value of values) {\n multiCommand = multiCommand.lPush(queueName, JSON.stringify(value));\n }\n await multiCommand.exec();\n }\n\n /**\n * Deletes a record from the Redis cache.\n *\n * @param {string} cacheRecordKey - The key of the record to delete\n * @returns {Promise<void>} A promise that resolves when the record is deleted\n */\n async deleteRecord(cacheRecordKey: string): Promise<void> {\n await this.client.del(cacheRecordKey);\n }\n\n /**\n * Deletes multiple records from the Redis cache in a single transaction.\n *\n * @param {string[]} cacheRecordKeys - Array of keys to delete\n * @returns {Promise<void>} A promise that resolves when all records are deleted\n */\n async deleteBatchRecords(cacheRecordKeys: string[]): Promise<void> {\n let multiCommand = this.client.multi();\n for (const key of cacheRecordKeys) {\n multiCommand = multiCommand.del(key);\n }\n await multiCommand.exec();\n }\n\n /**\n * Removes and returns the rightmost element from a Redis list.\n *\n * @template T - The type of value being dequeued\n * @param {string} queueName - The name of the Redis list\n * @returns {Promise<T>} A promise that resolves with the dequeued value\n * @throws {Error} If the queue is empty\n */\n async dequeueRecord<T>(queueName: string): Promise<T> {\n const value = await this.client.rPop(queueName);\n if (value === null) {\n throw new Error(`Queue is empty: ${queueName}`);\n }\n return JSON.parse(value);\n }\n\n /**\n * Removes and returns multiple elements from the right end of a Redis list.\n *\n * @template T - The type of values being dequeued\n * @param {string} queueName - The name of the Redis list\n * @param {number} pageSize - Maximum number of elements to dequeue\n * @returns {Promise<T[]>} A promise that resolves with an array of dequeued values\n */\n async dequeueBatchRecords<T>(\n queueName: string,\n pageSize: number\n ): Promise<T[]> {\n let multiCommand = this.client.multi();\n for (let i = 0; i < pageSize; i++) {\n multiCommand = multiCommand.rPop(queueName);\n }\n const values = await multiCommand.exec();\n return values.map((value) => this.parseValue<T>(value)).filter(Boolean);\n }\n\n /**\n * Reads a record from the Redis cache.\n *\n * @template T - The type of value being read\n * @param {string} cacheRecordKey - The key of the record to read\n * @returns {Promise<TtlCacheRecord<T>>} A promise that resolves with the cache record\n * @throws {Error} If the record is not found\n */\n async readRecord<T>(cacheRecordKey: string): Promise<TtlCacheRecord<T>> {\n const [value, ttl] = await this.client\n .multi()\n .get(cacheRecordKey)\n .ttl(cacheRecordKey)\n .exec();\n if (value === null) {\n throw new Error(`Record not found for key: ${cacheRecordKey}`);\n }\n\n return {\n key: cacheRecordKey,\n value: this.parseValue<T>(value),\n ttlMilliseconds: this.parseValue<number>(ttl) * 1000\n };\n }\n\n /**\n * Reads multiple records from the Redis cache.\n *\n * @template T - The type of values being read\n * @param {string[] | string} cacheRecordKeysOrPrefix - Array of keys to read, or a prefix pattern\n * @returns {Promise<TtlCacheRecord<T>[]>} A promise that resolves with an array of cache records\n */\n async readBatchRecords<T>(\n cacheRecordKeysOrPrefix: string[] | string\n ): Promise<TtlCacheRecord<T>[]> {\n const keys = Array.isArray(cacheRecordKeysOrPrefix)\n ? cacheRecordKeysOrPrefix\n : await this.client.keys(cacheRecordKeysOrPrefix + '*');\n let multiCommand = this.client.multi();\n for (const key of keys) {\n multiCommand = multiCommand.get(key);\n multiCommand = multiCommand.ttl(key);\n }\n const values = await multiCommand.exec();\n return values.reduce<TtlCacheRecord<T>[]>((acc, value, index) => {\n if (index % 2 === 0) {\n const maybeValue = this.parseValue<T>(value);\n const ttl = this.parseValue<number>(values[index + 1]);\n if (maybeValue && ttl) {\n acc.push({\n key: keys[index / 2],\n value: maybeValue,\n ttlMilliseconds: ttl * 1000\n });\n }\n }\n return acc;\n }, []);\n }\n\n /**\n * Lists all keys in the Redis cache that match a pattern prefix.\n *\n * @param {string} pattern_prefix - The prefix pattern to match keys against\n * @returns {Promise<string[]>} A promise that resolves with an array of matching keys\n */\n async listKeys(pattern_prefix: string): Promise<string[]> {\n const keys = await this.client.keys(pattern_prefix + '*');\n return keys;\n }\n\n /**\n * Checks if a record exists in the Redis cache.\n *\n * @param {string} cacheRecordKey - The key to check\n * @returns {Promise<boolean>} A promise that resolves with true if the record exists, false otherwise\n */\n async peekRecord(cacheRecordKey: string): Promise<boolean> {\n const result = await this.client.exists(cacheRecordKey);\n return result === 1;\n }\n\n /**\n * Checks if multiple records exist in the Redis cache.\n *\n * @param {string[] | string} cacheRecordKeysOrPrefix - Array of keys to check, or a prefix pattern\n * @returns {Promise<boolean[]>} A promise that resolves with an array of existence booleans\n */\n async peekBatchRecords(\n cacheRecordKeysOrPrefix: string[] | string\n ): Promise<boolean[]> {\n const keys = Array.isArray(cacheRecordKeysOrPrefix)\n ? cacheRecordKeysOrPrefix\n : await this.client.keys(cacheRecordKeysOrPrefix + '*');\n let multiCommand = this.client.multi();\n for (const key of keys) {\n multiCommand = multiCommand.exists(key);\n }\n const results = await multiCommand.exec();\n return results.map((result) => result === 1);\n }\n\n /**\n * Peeks at a record in the Redis cache.\n *\n * @template T - The type of value being peeked at\n * @param {string} queueName - The name of the Redis queue\n * @returns {Promise<T>} A promise that resolves with the peeked value\n */\n async peekQueueRecord<T>(queueName: string): Promise<T> {\n const value = await this.client.lRange(queueName, 0, 0);\n return this.parseValue<T>(value[0]);\n }\n\n /**\n * Peeks at multiple records in the Redis cache.\n *\n * @template T - The type of values being peeked at\n * @param {string} queueName - The name of the Redis queue\n * @param {number} pageSize - The number of records to peek at\n * @returns {Promise<T[]>} A promise that resolves with an array of peeked values\n */\n async peekQueueRecords<T>(queueName: string, pageSize: number): Promise<T[]> {\n const values = await this.client.lRange(queueName, 0, pageSize - 1);\n return values.map((value) => this.parseValue<T>(value)).filter(Boolean);\n }\n\n /**\n * Gracefully disconnects from the Redis server.\n *\n * @returns {Promise<void>} A promise that resolves when the connection is closed\n */\n async disconnect(): Promise<void> {\n await this.client.quit();\n }\n\n /**\n * Gets the default Time-To-Live value in milliseconds.\n *\n * @returns {number} The default TTL in milliseconds\n */\n getTtlMilliseconds(): number {\n return this.ttlMilliseconds;\n }\n\n /**\n * Gets the underlying Redis client instance.\n *\n * @returns {typeof this.client} The Redis client instance\n */\n getClient(): typeof this.client {\n return this.client;\n }\n}\n","import { AnySchemaValidator } from '@forklaunch/validator';\nimport corsMiddleware from 'cors';\nimport { ParsedQs } from 'qs';\nimport {\n ForklaunchNextFunction,\n ForklaunchRequest,\n ForklaunchResHeaders,\n ForklaunchResponse\n} from '../../types/apiDefinition.types';\nimport { ParamsDictionary } from '../../types/contractDetails.types';\n\n/**\n * Cors middleware handler\n *\n * @param req - Express-like request object\n * @param res - Express-like response object\n * @param next - Express-like next function\n */\nexport function cors<\n SV extends AnySchemaValidator,\n P extends ParamsDictionary,\n ResBodyMap extends Record<number, unknown>,\n ReqBody extends Record<string, unknown>,\n ReqQuery extends ParsedQs,\n ReqHeaders extends Record<string, string>,\n ResHeaders extends Record<string, string>,\n LocalsObj extends Record<string, unknown>\n>(\n req: ForklaunchRequest<SV, P, ReqBody, ReqQuery, ReqHeaders>,\n res: ForklaunchResponse<\n ResBodyMap,\n ForklaunchResHeaders & ResHeaders,\n LocalsObj\n >,\n next?: ForklaunchNextFunction\n) {\n if (req.method === 'OPTIONS') {\n res.cors = true;\n }\n corsMiddleware()(req, res, next ?? (() => {}));\n}\n","import { AnySchemaValidator } from '@forklaunch/validator';\nimport { trace } from '@opentelemetry/api';\nimport { v4 } from 'uuid';\nimport { ATTR_CORRELATION_ID } from '../../telemetry/constants';\nimport {\n ExpressLikeSchemaHandler,\n ForklaunchNextFunction\n} from '../../types/apiDefinition.types';\nimport {\n Body,\n HeadersObject,\n ParamsObject,\n QueryObject,\n ResponsesObject\n} from '../../types/contractDetails.types';\n\n/**\n * Middleware to create and add a request context.\n *\n * @template SV - A type that extends AnySchemaValidator.\n * @template Request - A type that extends ForklaunchRequest.\n * @template Response - A type that extends ForklaunchResponse.\n * @template NextFunction - A type that extends ForklaunchNextFunction.\n * @param {SV} schemaValidator - The schema validator.\n * @returns {Function} - Middleware function to create request context.\n */\nexport function createContext<\n SV extends AnySchemaValidator,\n P extends ParamsObject<SV>,\n ResBodyMap extends ResponsesObject<SV>,\n ReqBody extends Body<SV>,\n ReqQuery extends QueryObject<SV>,\n ReqHeaders extends HeadersObject<SV>,\n ResHeaders extends HeadersObject<SV>,\n LocalsObj extends Record<string, unknown>\n>(\n schemaValidator: SV\n): ExpressLikeSchemaHandler<\n SV,\n P,\n ResBodyMap,\n ReqBody,\n ReqQuery,\n ReqHeaders,\n ResHeaders,\n LocalsObj,\n unknown,\n unknown,\n ForklaunchNextFunction\n> {\n return function setContext(req, res, next?) {\n req.schemaValidator = schemaValidator;\n\n let correlationId = v4();\n\n if (req.headers['x-correlation-id']) {\n correlationId = req.headers['x-correlation-id'];\n }\n\n res.setHeader('x-correlation-id', correlationId);\n\n req.context = {\n correlationId: correlationId\n };\n\n const span = trace.getActiveSpan();\n if (span != null) {\n req.context.span = span;\n req.context.span?.setAttribute(ATTR_CORRELATION_ID, correlationId);\n }\n\n next?.();\n };\n}\n","import {\n ATTR_HTTP_REQUEST_METHOD,\n ATTR_HTTP_RESPONSE_STATUS_CODE,\n ATTR_HTTP_ROUTE,\n ATTR_SERVICE_NAME\n} from '@opentelemetry/semantic-conventions';\n\nexport const ATTR_API_NAME = 'api.name';\nexport const ATTR_CORRELATION_ID = 'correlation.id';\n\nexport {\n ATTR_HTTP_REQUEST_METHOD,\n ATTR_HTTP_RESPONSE_STATUS_CODE,\n ATTR_HTTP_ROUTE,\n ATTR_SERVICE_NAME\n};\n","import { extractArgumentNames } from '@forklaunch/common';\nimport { AnySchemaValidator } from '@forklaunch/validator';\nimport { ExpressLikeSchemaHandler } from '../types/apiDefinition.types';\nimport {\n Body,\n HeadersObject,\n ParamsObject,\n QueryObject,\n ResponsesObject\n} from '../types/contractDetails.types';\n\n/**\n * Type guard that checks if an object is an Express-like schema handler.\n * A schema handler is a function that takes up to 3 arguments (request, response, next)\n * and is used for middleware or route handlers in an Express-like application.\n *\n * @template SV - A type that extends AnySchemaValidator\n * @template P - The type of route parameters\n * @template ResBodyMap - The type of response body mapping\n * @template ReqBody - The type of request body\n * @template ReqQuery - The type of request query parameters\n * @template ReqHeaders - The type of request headers\n * @template ResHeaders - The type of response headers\n * @template LocalsObj - The type of locals object\n * @template BaseRequest - The base request type\n * @template BaseResponse - The base response type\n * @template NextFunction - The type of next function\n * @param {unknown} middleware - The object to check\n * @returns {boolean} A type predicate indicating whether the object is an ExpressLikeSchemaHandler\n *\n * @example\n * ```ts\n * if (isExpressLikeSchemaHandler(middleware)) {\n * // middleware is now typed as ExpressLikeSchemaHandler\n * middleware(req, res, next);\n * }\n * ```\n */\nexport function isExpressLikeSchemaHandler<\n SV extends AnySchemaValidator,\n P extends ParamsObject<SV>,\n ResBodyMap extends ResponsesObject<SV>,\n ReqBody extends Body<SV>,\n ReqQuery extends QueryObject<SV>,\n ReqHeaders extends HeadersObject<SV>,\n ResHeaders extends HeadersObject<SV>,\n LocalsObj extends Record<string, unknown>,\n BaseRequest,\n BaseResponse,\n NextFunction\n>(\n middleware: unknown\n): middleware is ExpressLikeSchemaHandler<\n SV,\n P,\n ResBodyMap,\n ReqBody,\n ReqQuery,\n ReqHeaders,\n ResHeaders,\n LocalsObj,\n BaseRequest,\n BaseResponse,\n NextFunction\n> {\n const extractedArgumentNames =\n typeof middleware === 'function' &&\n new Set(\n extractArgumentNames(middleware).map((argumentName) =>\n argumentName.toLowerCase()\n )\n );\n return extractedArgumentNames && extractedArgumentNames.size <= 3;\n}\n","import { AnySchemaValidator } from '@forklaunch/validator';\nimport { jwtVerify } from 'jose';\nimport { ParsedQs } from 'qs';\nimport {\n ForklaunchNextFunction,\n ForklaunchRequest,\n ForklaunchResponse,\n MapParamsSchema,\n MapReqBodySchema,\n MapReqHeadersSchema,\n MapReqQuerySchema,\n MapResBodyMapSchema,\n MapResHeadersSchema,\n ResolvedForklaunchRequest\n} from '../../types/apiDefinition.types';\nimport {\n AuthMethods,\n Body,\n HeadersObject,\n ParamsDictionary,\n ParamsObject,\n QueryObject,\n ResponsesObject\n} from '../../types/contractDetails.types';\n\nconst invalidAuthorizationTokenFormat = [\n 401,\n 'Invalid Authorization token format.'\n] as const;\nconst invalidAuthorizationSubject = [\n 403,\n 'Invalid Authorization subject.'\n] as const;\nconst invalidAuthorizationTokenPermissions = [\n 403,\n 'Invalid Authorization permissions.'\n] as const;\nconst invalidAuthorizationTokenRoles = [\n 403,\n 'Invalid Authorization roles.'\n] as const;\nconst invalidAuthorizationToken = [\n 403,\n 'Invalid Authorization token.'\n] as const;\nconst invalidAuthorizationLogin = [\n 403,\n 'Invalid Authorization login.'\n] as const;\n\n/**\n * Checks the authorization token for validity.\n *\n * @param {AuthMethod} [authorizationMethod] - The method of authorization.\n * @param {string} [authorizationToken] - The authorization string.\n * @returns {Promise<[401 | 403, string] | string | undefined>} - The result of the authorization check.\n */\nasync function checkAuthorizationToken<\n SV extends AnySchemaValidator,\n P extends ParamsDictionary,\n ReqBody extends Record<string, unknown>,\n ReqQuery extends ParsedQs,\n ReqHeaders extends Record<string, string>,\n BaseRequest\n>(\n authorizationMethod: AuthMethods<\n SV,\n P,\n ReqBody,\n ReqQuery,\n ReqHeaders,\n BaseRequest\n >,\n authorizationToken?: string,\n req?: ResolvedForklaunchRequest<\n SV,\n P,\n ReqBody,\n ReqQuery,\n ReqHeaders,\n BaseRequest\n >\n): Promise<readonly [401 | 403 | 500, string] | undefined> {\n if (authorizationToken == null) {\n return [401, 'No Authorization token provided.'];\n }\n\n const [tokenPrefix, token] = authorizationToken.split(' ');\n\n let resourceId;\n\n switch (authorizationMethod.method) {\n case 'jwt': {\n if (tokenPrefix !== 'Bearer') {\n return invalidAuthorizationTokenFormat;\n }\n\n try {\n const decodedJwt = await jwtVerify(\n token,\n new TextEncoder().encode(\n // TODO: Check this at application startup if there is any route with jwt checking\n process.env.JWT_SECRET\n )\n );\n\n if (!decodedJwt.payload.sub) {\n return invalidAuthorizationSubject;\n }\n\n resourceId = decodedJwt.payload.sub;\n } catch (error) {\n (\n req as ForklaunchRequest<SV, P, ReqBody, ReqQuery, ReqHeaders>\n ).openTelemetryCollector.error(error);\n return invalidAuthorizationToken;\n }\n\n break;\n }\n case 'basic': {\n if (authorizationToken !== 'Basic') {\n return invalidAuthorizationTokenFormat;\n }\n\n const [username, password] = Buffer.from(token, 'base64')\n .toString('utf-8')\n .split(':');\n\n if (!username || !password) {\n return invalidAuthorizationTokenFormat;\n }\n\n if (!authorizationMethod.login(username, password)) {\n return invalidAuthorizationLogin;\n }\n\n resourceId = username;\n break;\n }\n case 'other':\n if (tokenPrefix !== authorizationMethod.tokenPrefix) {\n return invalidAuthorizationTokenFormat;\n }\n\n resourceId = authorizationMethod.decodeResource(token);\n\n break;\n }\n\n if (\n authorizationMethod.allowedPermissions ||\n authorizationMethod.forbiddenPermissions\n ) {\n if (!authorizationMethod.mapPermissions) {\n return [500, 'No permission mapping function provided.'];\n }\n\n const resourcePermissions = await authorizationMethod.mapPermissions(\n resourceId,\n req\n );\n\n if (authorizationMethod.allowedPermissions) {\n if (\n resourcePermissions.intersection(authorizationMethod.allowedPermissions)\n .size === 0\n ) {\n return invalidAuthorizationTokenPermissions;\n }\n }\n\n if (authorizationMethod.forbiddenPermissions) {\n if (\n resourcePermissions.intersection(\n authorizationMethod.forbiddenPermissions\n ).size !== 0\n ) {\n return invalidAuthorizationTokenPermissions;\n }\n }\n }\n\n if (authorizationMethod.allowedRoles || authorizationMethod.forbiddenRoles) {\n if (!authorizationMethod.mapRoles) {\n return [500, 'No role mapping function provided.'];\n }\n\n const resourceRoles = await authorizationMethod.mapRoles(resourceId, req);\n\n if (authorizationMethod.allowedRoles) {\n if (\n resourceRoles.intersection(authorizationMethod.allowedRoles).size === 0\n ) {\n return invalidAuthorizationTokenRoles;\n }\n }\n\n if (authorizationMethod.forbiddenRoles) {\n if (\n resourceRoles.intersection(authorizationMethod.forbiddenRoles).size !==\n 0\n ) {\n return invalidAuthorizationTokenRoles;\n }\n }\n }\n\n return [401, 'Invalid Authorization method.'];\n}\n\n/**\n * Middleware to parse request authorization.\n *\n * @template SV - A type that extends AnySchemaValidator.\n * @template Request - A type that extends ForklaunchRequest.\n * @template Response - A type that extends ForklaunchResponse.\n * @template NextFunction - A type that extends ForklaunchNextFunction.\n * @param {Request} req - The request object.\n * @param {Response} res - The response object.\n * @param {NextFunction} [next] - The next middleware function.\n */\nexport async function parseRequestAuth<\n SV extends AnySchemaValidator,\n P extends ParamsObject<SV>,\n ResBodyMap extends ResponsesObject<SV>,\n ReqBody extends Body<SV>,\n ReqQuery extends QueryObject<SV>,\n ReqHeaders extends HeadersObject<SV>,\n ResHeaders extends HeadersObject<SV>,\n LocalsObj extends Record<string, unknown>\n>(\n req: ForklaunchRequest<\n SV,\n MapParamsSchema<SV, P>,\n MapReqBodySchema<SV, ReqBody>,\n MapReqQuerySchema<SV, ReqQuery>,\n MapReqHeadersSchema<SV, ReqHeaders>\n >,\n res: ForklaunchResponse<\n MapResBodyMapSchema<SV, ResBodyMap>,\n MapResHeadersSchema<SV, ResHeaders>,\n LocalsObj\n >,\n next?: ForklaunchNextFunction\n) {\n const auth = req.contractDetails.auth as AuthMethods<\n SV,\n MapParamsSchema<SV, P>,\n MapReqBodySchema<SV, ReqBody>,\n MapReqQuerySchema<SV, ReqQuery>,\n MapReqHeadersSchema<SV, ReqHeaders>,\n unknown\n >;\n\n if (auth) {\n const [error, message] =\n (await checkAuthorizationToken<\n SV,\n MapParamsSchema<SV, P>,\n MapReqBodySchema<SV, ReqBody>,\n MapReqQuerySchema<SV, ReqQuery>,\n MapReqHeadersSchema<SV, ReqHeaders>,\n unknown\n >(\n auth,\n req.headers[\n (auth.method === 'other' ? auth.headerName : undefined) ??\n 'Authorization'\n ],\n req\n )) ?? [];\n if (error != null) {\n res.status(error).send(message);\n next?.(new Error(message));\n }\n }\n\n next?.();\n}\n","// This is a simple function that returns the value of an environment variable.\n// It casts a potentially undefined value to a string, since it will be validated in order to be bootstrapped.\n\nexport function getEnvVar(name: string): string {\n const value = process.env[name];\n return value as string;\n}\n","import { HyperExpressInstrumentation } from '@forklaunch/opentelemetry-instrumentation-hyper-express';\nimport {\n Counter,\n Gauge,\n Histogram,\n metrics,\n ObservableCounter,\n ObservableGauge,\n ObservableUpDownCounter,\n UpDownCounter\n} from '@opentelemetry/api';\nimport { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-http';\nimport { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http';\nimport { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';\nimport { ExpressInstrumentation } from '@opentelemetry/instrumentation-express';\nimport { HttpInstrumentation } from '@opentelemetry/instrumentation-http';\nimport { resourceFromAttributes } from '@opentelemetry/resources';\nimport { BatchLogRecordProcessor } from '@opentelemetry/sdk-logs';\nimport { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';\nimport { NodeSDK } from '@opentelemetry/sdk-node';\nimport {\n ATTR_HTTP_REQUEST_METHOD,\n ATTR_HTTP_RESPONSE_STATUS_CODE,\n ATTR_HTTP_ROUTE,\n ATTR_SERVICE_NAME\n} from '@opentelemetry/semantic-conventions';\nimport dotenv from 'dotenv';\nimport { LevelWithSilent, LevelWithSilentOrString } from 'pino';\nimport { v4 } from 'uuid';\nimport { getEnvVar } from '../../services/getEnvVar';\nimport { isForklaunchRequest } from '../guards/isForklaunchRequest';\nimport {\n LogFn,\n LoggerMeta,\n MetricsDefinition,\n MetricType\n} from '../types/openTelemetryCollector.types';\nimport { ATTR_API_NAME, ATTR_CORRELATION_ID } from './constants';\nimport { logger } from './pinoLogger';\n\nexport class OpenTelemetryCollector<\n AppliedMetricsDefinition extends MetricsDefinition\n> {\n private readonly uuid = v4();\n private readonly logger;\n private readonly metrics: Record<\n keyof AppliedMetricsDefinition,\n | Counter\n | Gauge\n | Histogram\n | UpDownCounter\n | ObservableCounter\n | ObservableGauge\n | ObservableUpDownCounter\n >;\n\n // scoped creation and create this in middleware when api execute. Also add correlation id\n constructor(\n private readonly serviceName: string,\n level?: LevelWithSilentOrString,\n metricDefinitions?: AppliedMetricsDefinition\n ) {\n this.logger = logger(level || 'info');\n\n this.metrics = {} as Record<\n keyof AppliedMetricsDefinition,\n MetricType<AppliedMetricsDefinition[keyof AppliedMetricsDefinition]>\n >;\n\n for (const [metricId, metricType] of Object.entries(\n metricDefinitions ?? {}\n )) {\n switch (metricType) {\n case 'counter':\n this.metrics[metricId as keyof AppliedMetricsDefinition] = metrics\n .getMeter(this.serviceName)\n .createCounter(metricId);\n break;\n case 'gauge':\n this.metrics[metricId as keyof AppliedMetricsDefinition] = metrics\n .getMeter(this.serviceName)\n .createGauge(metricId);\n break;\n case 'histogram':\n this.metrics[metricId as keyof AppliedMetricsDefinition] = metrics\n .getMeter(this.serviceName)\n .createHistogram(metricId);\n break;\n case 'upDownCounter':\n this.metrics[metricId as keyof AppliedMetricsDefinition] = metrics\n .getMeter(this.serviceName)\n .createUpDownCounter(metricId);\n break;\n case 'observableCounter':\n this.metrics[metricId as keyof AppliedMetricsDefinition] = metrics\n .getMeter(this.serviceName)\n .createObservableCounter(metricId);\n break;\n case 'observableGauge':\n this.metrics[metricId as keyof AppliedMetricsDefinition] = metrics\n .getMeter(this.serviceName)\n .createObservableGauge(metricId);\n break;\n case 'observableUpDownCounter':\n this.metrics[metricId as keyof AppliedMetricsDefinition] = metrics\n .getMeter(this.serviceName)\n .createObservableUpDownCounter(metricId);\n break;\n }\n }\n\n this.log(\n 'info',\n 'OpenTelemetry Collector (Traces + Logs + Metrics) started'\n );\n }\n\n log(level: LevelWithSilent, ...args: (string | unknown | LoggerMeta)[]) {\n this.logger.log(level, ...args);\n }\n\n info: LogFn = (\n msg: string | unknown | LoggerMeta,\n ...args: (string | unknown | LoggerMeta)[]\n ) => {\n this.logger.log('info', msg, ...args);\n };\n\n error: LogFn = (\n msg: string | unknown | LoggerMeta,\n ...args: (string | unknown | LoggerMeta)[]\n ) => {\n this.logger.log('error', msg, ...args);\n };\n\n warn: LogFn = (\n msg: string | unknown | LoggerMeta,\n ...args: (string | unknown | LoggerMeta)[]\n ) => {\n this.logger.log('warn', msg, ...args);\n };\n\n debug: LogFn = (\n msg: string | unknown | LoggerMeta,\n ...args: (string | unknown | LoggerMeta)[]\n ) => {\n this.logger.log('debug', msg, ...args);\n };\n\n trace: LogFn = (\n msg: string | unknown | LoggerMeta,\n ...args: (string | unknown | LoggerMeta)[]\n ) => {\n this.logger.log('trace', msg, ...args);\n };\n\n getMetric<T extends keyof AppliedMetricsDefinition>(\n metricId: T\n ): MetricType<AppliedMetricsDefinition[T]> {\n return this.metrics[metricId] as MetricType<AppliedMetricsDefinition[T]>;\n }\n}\n\ndotenv.config({ path: getEnvVar('ENV_FILE_PATH') });\n\nnew NodeSDK({\n resource: resourceFromAttributes({\n [ATTR_SERVICE_NAME]: getEnvVar('OTEL_SERVICE_NAME')\n }),\n traceExporter: new OTLPTraceExporter({\n url: `${\n getEnvVar('OTEL_EXPORTER_OTLP_ENDPOINT') ?? 'http://localhost:4318'\n }/v1/traces`\n }),\n metricReader: new PeriodicExportingMetricReader({\n exporter: new OTLPMetricExporter({\n url: `${\n getEnvVar('OTEL_EXPORTER_OTLP_ENDPOINT') ?? 'http://localhost:4318'\n }/v1/metrics`\n }),\n exportIntervalMillis: 5000\n }),\n logRecordProcessors: [\n new BatchLogRecordProcessor(\n new OTLPLogExporter({\n url: `${\n getEnvVar('OTEL_EXPORTER_OTLP_ENDPOINT') ?? 'http://localhost:4318'\n }/v1/logs`\n })\n )\n ],\n instrumentations: [\n new HttpInstrumentation({\n applyCustomAttributesOnSpan: (span, request) => {\n span.setAttribute(\n 'service.name',\n getEnvVar('OTEL_SERVICE_NAME') ?? 'unknown'\n );\n if (isForklaunchRequest(request)) {\n span.setAttribute('api.name', request.contractDetails?.name);\n }\n }\n }),\n new ExpressInstrumentation(),\n new HyperExpressInstrumentation()\n ]\n}).start();\n\n// begin static metrics -- these have to be in here in order to instantiate these after the SDK is instantiated\nexport const httpRequestsTotalCounter = metrics\n .getMeter(getEnvVar('OTEL_SERVICE_NAME') || 'unknown')\n .createCounter<{\n [ATTR_SERVICE_NAME]: string;\n [ATTR_API_NAME]: string;\n [ATTR_CORRELATION_ID]: string;\n [ATTR_HTTP_REQUEST_METHOD]: string;\n [ATTR_HTTP_ROUTE]: string;\n [ATTR_HTTP_RESPONSE_STATUS_CODE]: number;\n }>('http_requests_total', {\n description: 'Number of HTTP requests'\n });\n\nexport const httpServerDurationHistogram = metrics\n .getMeter(getEnvVar('OTEL_SERVICE_NAME') || 'unknown')\n .createHistogram<{\n [ATTR_SERVICE_NAME]: string;\n [ATTR_API_NAME]: string;\n [ATTR_HTTP_REQUEST_METHOD]: string;\n [ATTR_HTTP_ROUTE]: string;\n [ATTR_HTTP_RESPONSE_STATUS_CODE]: number;\n }>('http_server_duration', {\n description: 'Duration of HTTP server requests',\n unit: 's'\n });\n","import { AnySchemaValidator } from '@forklaunch/validator';\nimport { ParsedQs } from 'qs';\nimport { ForklaunchRequest } from '../types/apiDefinition.types';\nimport { ParamsDictionary } from '../types/contractDetails.types';\n\n/**\n * Type guard that checks if an object is a Forklaunch request.\n * A Forklaunch request is an object that contains contract details and follows the Forklaunch request structure.\n *\n * @template SV - A type that extends AnySchemaValidator\n * @template P - The type of route parameters that extends ParamsDictionary\n * @template ReqBody - The type of request body that extends Record<string, unknown>\n * @template ReqQuery - The type of request query parameters that extends ParsedQs\n * @template ReqHeaders - The type of request headers that extends Record<string, string>\n * @param {unknown} request - The object to check\n * @returns {boolean} A type predicate indicating whether the object is a ForklaunchRequest\n *\n * @example\n * ```ts\n * if (isForklaunchRequest(request)) {\n * // request is now typed as ForklaunchRequest\n * const { contractDetails } = request;\n * }\n * ```\n */\nexport function isForklaunchRequest<\n SV extends AnySchemaValidator,\n P extends ParamsDictionary,\n ReqBody extends Record<string, unknown>,\n ReqQuery extends ParsedQs,\n ReqHeaders extends Record<string, string>\n>(\n request: unknown\n): request is ForklaunchRequest<SV, P, ReqBody, ReqQuery, ReqHeaders> {\n return (\n request != null &&\n typeof request === 'object' &&\n 'contractDetails' in request\n );\n}\n","import { isNever, safeStringify } from '@forklaunch/common';\nimport { trace } from '@opentelemetry/api';\nimport { AnyValueMap, logs } from '@opentelemetry/api-logs';\nimport pino, { LevelWithSilent, LevelWithSilentOrString, Logger } from 'pino';\nimport PinoPretty from 'pino-pretty';\nimport { isLoggerMeta } from '../guards/isLoggerMeta';\nimport { LogFn, LoggerMeta } from '../types/openTelemetryCollector.types';\n\nexport function meta(meta: Record<string, unknown>) {\n return meta as LoggerMeta;\n}\n\nfunction mapSeverity(level: LevelWithSilent) {\n switch (level) {\n case 'silent':\n return 0;\n case 'trace':\n return 1;\n case 'debug':\n return 5;\n case 'info':\n return 9;\n case 'warn':\n return 13;\n case 'error':\n return 17;\n case 'fatal':\n return 21;\n default:\n isNever(level);\n return 0;\n }\n}\n\nclass PinoLogger {\n private pinoLogger: Logger;\n private meta: AnyValueMap;\n private prettyPrinter = PinoPretty.prettyFactory({\n colorize: true\n });\n\n constructor(level: LevelWithSilentOrString, meta: AnyValueMap = {}) {\n this.pinoLogger = pino({\n level: level || 'info',\n formatters: {\n level(label) {\n return { level: label };\n }\n },\n timestamp: pino.stdTimeFunctions.isoTime,\n transport: {\n target: 'pino-pretty',\n options: { colorize: true }\n }\n });\n this.meta = meta;\n }\n\n log(level: LevelWithSilent, ...args: (string | unknown | LoggerMeta)[]) {\n let meta: AnyValueMap = {};\n\n const filteredArgs = args\n .filter((arg) => {\n if (isLoggerMeta(arg)) {\n Object.assign(meta, arg);\n return false;\n }\n return true;\n })\n .map(safeStringify) as Parameters<pino.LogFn>;\n\n const activeSpan = trace.getActiveSpan();\n if (activeSpan) {\n const activeSpanContext = activeSpan.spanContext();\n meta.trace_id = activeSpanContext.traceId;\n meta.span_id = activeSpanContext.spanId;\n meta.trace_flags = activeSpanContext.traceFlags;\n meta = {\n // @ts-expect-error accessing private property\n ...activeSpan.attributes,\n ...meta\n };\n }\n\n meta = {\n 'api.name': 'none',\n 'correlation.id': 'none',\n ...meta\n };\n\n this.pinoLogger[level](...filteredArgs);\n logs.getLogger(process.env.OTEL_SERVICE_NAME ?? 'unknown').emit({\n severityText: level,\n severityNumber: mapSeverity(level),\n body: this.prettyPrinter(filteredArgs),\n attributes: { ...this.meta, ...meta }\n });\n }\n\n error: LogFn = (\n msg: string | unknown | LoggerMeta,\n ...args: (string | unknown | LoggerMeta)[]\n ) => this.log('error', msg, ...args);\n\n info: LogFn = (\n msg: string | unknown | LoggerMeta,\n ...args: (string | unknown | LoggerMeta)[]\n ) => this.log('info', msg, ...args);\n\n debug: LogFn = (\n msg: string | unknown | LoggerMeta,\n ...args: (string | unknown | LoggerMeta)[]\n ) => this.log('debug', msg, ...args);\n warn: LogFn = (\n msg: string | unknown | LoggerMeta,\n ...args: (string | unknown | LoggerMeta)[]\n ) => this.log('warn', msg, ...args);\n\n trace: LogFn = (\n msg: string | unknown | LoggerMeta,\n ...args: (string | unknown | LoggerMeta)[]\n ) => this.log('trace', msg, ...args);\n\n child(meta: AnyValueMap = {}) {\n return new PinoLogger(this.pinoLogger.level, { ...this.meta, ...meta });\n }\n\n getBaseLogger() {\n return this.pinoLogger;\n }\n}\n\nexport function logger(level: LevelWithSilentOrString, meta: AnyValueMap = {}) {\n return new PinoLogger(level, meta);\n}\n","import {\n AnySchemaValidator,\n prettyPrintParseErrors,\n SchemaValidator\n} from '@forklaunch/validator';\nimport { ParsedQs } from 'qs';\nimport { hasSend } from '../../guards/hasSend';\nimport { isResponseShape } from '../../guards/isResponseShape';\nimport {\n ForklaunchNextFunction,\n ForklaunchRequest,\n ForklaunchResponse\n} from '../../types/apiDefinition.types';\nimport { ParamsDictionary } from '../../types/contractDetails.types';\n\n/**\n * Pre-handler function to parse and validate input.\n *\n * @template SV - A type that extends AnySchemaValidator.\n * @template Request - A type that extends ForklaunchRequest.\n * @template Response - A type that extends ForklaunchResponse.\n * @template NextFunction - A type that extends ForklaunchNextFunction.\n * @param {Request} req - The request object.\n * @param {Response} res - The response object.\n * @param {NextFunction} [next] - The next middleware function.\n */\nexport function parse<\n SV extends AnySchemaValidator,\n P extends ParamsDictionary,\n ResBodyMap extends Record<number, unknown>,\n ReqBody extends Record<string, unknown>,\n ReqQuery extends ParsedQs,\n ReqHeaders extends Record<string, string>,\n ResHeaders extends Record<string, string>,\n LocalsObj extends Record<string, unknown>\n>(\n req: ForklaunchRequest<SV, P, ReqBody, ReqQuery, ReqHeaders>,\n res: ForklaunchResponse<ResBodyMap, ResHeaders, LocalsObj>,\n next?: ForklaunchNextFunction\n) {\n const request = {\n params: req.params,\n query: req.query,\n headers: req.headers,\n body: req.body\n };\n\n const parsedRequest = (req.schemaValidator as SchemaValidator).parse(\n req.requestSchema,\n request\n );\n\n if (\n parsedRequest.ok &&\n isResponseShape<P, ReqHeaders, ReqQuery, ReqBody>(parsedRequest.value)\n ) {\n req.body = parsedRequest.value.body;\n req.params = parsedRequest.value.params;\n Object.defineProperty(req, 'query', {\n value: parsedRequest.value.query,\n writable: false,\n enumerable: true,\n configurable: false\n });\n req.headers = parsedRequest.value.headers;\n }\n if (!parsedRequest.ok) {\n switch (req.contractDetails.options?.requestValidation) {\n default:\n case 'error':\n res.type('application/json');\n res.status(400);\n if (hasSend(res)) {\n res.send(\n `${prettyPrintParseErrors(\n parsedRequest.errors,\n 'Request'\n )}\\n\\nCorrelation id: ${\n req.context.correlationId ?? 'No correlation ID'\n }`\n );\n } else {\n next?.(new Error('Request is not sendable.'));\n }\n return;\n case 'warning':\n req.openTelemetryCollector.warn(\n prettyPrintParseErrors(parsedRequest.errors, 'Request')\n );\n break;\n case 'none':\n break;\n }\n }\n\n next?.();\n}\n","import {\n AnySchemaValidator,\n prettyPrintParseErrors,\n SchemaValidator\n} from '@forklaunch/validator';\nimport { ParsedQs } from 'qs';\nimport { hasSend } from '../../guards/hasSend';\nimport {\n ForklaunchNextFunction,\n ForklaunchRequest,\n ForklaunchResHeaders,\n ForklaunchResponse\n} from '../../types/apiDefinition.types';\nimport { ParamsDictionary } from '../../types/contractDetails.types';\n\n/**\n * Middleware to parse and validate the response according to the provided schema.\n *\n * This function validates the response against a schema provided by the request's schema validator.\n * If the response does not conform to the schema, the behavior is determined by the `responseValidation`\n * option in `req.contractDetails.options`:\n * - `'error'` (default): Calls `next` with an error.\n * - `'warning'`: Logs a warning to the console.\n * - `'none'`: Silently continues without action.\n *\n * @template SV - The type of the schema validator used in the request.\n * @template P - The type of the parameters dictionary used in the request.\n * @template ResBodyMap - A record type mapping status codes to response body types.\n * @template ReqBody - The type of the request body.\n * @template ReqQuery - The type of the parsed query string.\n * @template ReqHeaders - The type of the request headers.\n * @template ResHeaders - The type of the response headers, extended from `ForklaunchResHeaders`.\n * @template LocalsObj - The type of the locals object in the response.\n *\n * @param {ForklaunchRequest<SV, P, ReqBody, ReqQuery, ReqHeaders>} req - The request object, containing the schema validator and other request data.\n * @param {ForklaunchResponse<ResBodyMap, ForklaunchResHeaders & ResHeaders, LocalsObj>} res - The response object, including headers and body data.\n * @param {ForklaunchNextFunction} [next] - The next middleware function to be called. If not provided, the function will return after processing.\n *\n * @returns {void} This function does not return a value.\n */\nexport function parse<\n SV extends AnySchemaValidator,\n P extends ParamsDictionary,\n ResBodyMap extends Record<number, unknown>,\n ReqBody extends Record<string, unknown>,\n ReqQuery extends ParsedQs,\n ReqHeaders extends Record<string, string>,\n ResHeaders extends Record<string, string>,\n LocalsObj extends Record<string, unknown>\n>(\n req: ForklaunchRequest<SV, P, ReqBody, ReqQuery, ReqHeaders>,\n res: ForklaunchResponse<\n ResBodyMap,\n ForklaunchResHeaders & ResHeaders,\n LocalsObj\n >,\n next?: ForklaunchNextFunction\n) {\n const { headers, responses } = res.responseSchemas;\n\n const parsedResponse = (req.schemaValidator as SchemaValidator).parse(\n responses?.[res.statusCode],\n res.bodyData\n );\n\n const parsedHeaders = (req.schemaValidator as SchemaValidator).parse(\n headers ?? req.schemaValidator.unknown,\n res.getHeaders()\n );\n const parseErrors: string[] = [];\n if (!parsedHeaders.ok) {\n const headerErrors = prettyPrintParseErrors(parsedHeaders.errors, 'Header');\n if (headerErrors) {\n parseErrors.push(headerErrors);\n }\n }\n\n if (!parsedResponse.ok) {\n const responseErrors = prettyPrintParseErrors(\n parsedResponse.errors,\n 'Response'\n );\n if (responseErrors) {\n parseErrors.push(responseErrors);\n }\n }\n\n if (parseErrors.length > 0) {\n switch (req.contractDetails.options?.responseValidation) {\n default:\n case 'error':\n res.type('text/plain');\n res.status(400);\n if (hasSend(res)) {\n res.send(\n `Invalid response:\\n${parseErrors.join(\n '\\n\\n'\n )}\\n\\nCorrelation id: ${\n req.context.correlationId ?? 'No correlation ID'\n }`\n );\n } else {\n next?.(new Error('Response is not sendable.'));\n }\n // next?.(new Error(`Invalid response:\\n${parseErrors.join('\\n\\n')}`));\n // break;\n return;\n case 'warning':\n req.openTelemetryCollector.warn(\n `Invalid response:\\n${parseErrors.join('\\n\\n')}`\n );\n break;\n case 'none':\n break;\n }\n }\n next?.();\n}\n","import { AnySchemaValidator } from '@forklaunch/validator';\nimport {\n ATTR_HTTP_REQUEST_METHOD,\n ATTR_HTTP_RESPONSE_STATUS_CODE,\n ATTR_HTTP_ROUTE,\n ATTR_SERVICE_NAME\n} from '@opentelemetry/semantic-conventions';\nimport { ParsedQs } from 'qs';\nimport { ForklaunchRequest, ForklaunchResponse, ParamsDictionary } from '..';\nimport { getEnvVar } from '../../services';\nimport { ATTR_API_NAME, ATTR_CORRELATION_ID } from './constants';\nimport { httpRequestsTotalCounter } from './openTelemetryCollector';\n\nexport function recordMetric<\n SV extends AnySchemaValidator,\n P extends ParamsDictionary,\n ReqBody extends Record<string, unknown>,\n ReqQuery extends ParsedQs,\n ResBodyMap extends Record<string, unknown>,\n ReqHeaders extends Record<string, string>,\n ResHeaders extends Record<string, string>,\n LocalsObj extends Record<string, unknown>\n>(\n req: ForklaunchRequest<SV, P, ReqBody, ReqQuery, ReqHeaders>,\n res: ForklaunchResponse<ResBodyMap, ResHeaders, LocalsObj>\n) {\n if (res.metricRecorded) {\n return;\n }\n\n httpRequestsTotalCounter.add(1, {\n [ATTR_SERVICE_NAME]: getEnvVar('OTEL_SERVICE_NAME'),\n [ATTR_API_NAME]: req.contractDetails?.name,\n [ATTR_CORRELATION_ID]: req.context.correlationId,\n [ATTR_HTTP_REQUEST_METHOD]: req.method,\n [ATTR_HTTP_ROUTE]: req.originalPath,\n [ATTR_HTTP_RESPONSE_STATUS_CODE]: res.statusCode || 0\n });\n res.metricRecorded = true;\n}\n","import { extractArgumentNames, isNever } from '@forklaunch/common';\nimport {\n AnySchemaValidator,\n IdiomaticSchema,\n ParseResult,\n prettyPrintParseErrors,\n SchemaValidator\n} from '@forklaunch/validator';\nimport { isConstructed } from './guards/isConstructed';\nimport { isConstructedSingleton } from './guards/isConstructedSingleton';\nimport { isConstructor } from './guards/isConstructor';\nimport {\n ConfigValidator,\n Constructed,\n ConstructedSingleton,\n Lifetime,\n ResolvedConfigValidator,\n SchemaConstructor,\n SchemaFunction,\n Singleton\n} from './types/configInjector.types';\n\nexport function createConfigInjector<\n SV extends AnySchemaValidator,\n CV extends ConfigValidator<SV>\n>(\n schemaValidator: SV,\n dependenciesDefinition: {\n [K in keyof CV]:\n | Singleton<\n CV[K],\n Omit<ResolvedConfigValidator<SV, CV>, K>,\n ResolvedConfigValidator<SV, CV>[K]\n >\n | Constructed<\n CV[K],\n Omit<ResolvedConfigValidator<SV, CV>, K>,\n ResolvedConfigValidator<SV, CV>[K]\n >;\n }\n) {\n return new ConfigInjector<SV, CV>(\n schemaValidator,\n dependenciesDefinition\n ).load();\n}\n\nexport class ConfigInjector<\n SV extends AnySchemaValidator,\n CV extends ConfigValidator<SV>\n> {\n instances: {\n [K in keyof CV]?: ResolvedConfigValidator<SV, CV>[K];\n } = {};\n\n readonly configShapes: CV;\n\n load(inheritedScopeInstances?: {\n [K in keyof CV]?: ResolvedConfigValidator<SV, CV>[K];\n }): this {\n for (const token in inheritedScopeInstances) {\n this.instances[token] = inheritedScopeInstances[token];\n }\n\n for (const token in this.dependenciesDefinition) {\n const definition = this.dependenciesDefinition[token];\n if (\n definition.lifetime === Lifetime.Singleton &&\n !this.instances[token]\n ) {\n if (\n isConstructedSingleton<\n CV[typeof token],\n Omit<ResolvedConfigValidator<SV, CV>, typeof token>,\n ResolvedConfigValidator<SV, CV>[typeof token]\n >(definition)\n ) {\n this.instances[token] = this.resolveInstance<typeof token>(\n token,\n definition\n );\n } else {\n this.instances[token] = definition.value;\n }\n }\n }\n return this;\n }\n\n private resolveInstance<T extends keyof CV>(\n token: T,\n definition:\n | ConstructedSingleton<\n CV[T],\n Omit<ResolvedConfigValidator<SV, CV>, T>,\n ResolvedConfigValidator<SV, CV>[T]\n >\n | Constructed<\n CV[T],\n Omit<ResolvedConfigValidator<SV, CV>, T>,\n ResolvedConfigValidator<SV, CV>[T]\n >,\n context?: Record<string, unknown>,\n resolutionPath: (keyof CV)[] = []\n ): ResolvedConfigValidator<SV, CV>[T] {\n const injectorArgument = extractArgumentNames(definition.factory)[0];\n // short circuit as no args\n if (!injectorArgument || injectorArgument === '_args') {\n return definition.factory(\n {} as Omit<ResolvedConfigValidator<SV, CV>, T>,\n this.resolve.bind(this),\n context ?? ({} as Record<string, unknown>)\n );\n }\n\n if (!injectorArgument.startsWith('{') || !injectorArgument.endsWith('}')) {\n throw new Error(\n `Invalid injector argument for ${String(\n token\n )}: ${injectorArgument}. Please use object destructuring syntax: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment.`\n );\n }\n const resolvedArguments = Object.fromEntries(\n injectorArgument\n .replace('{', '')\n .replace('}', '')\n .split(',')\n .map((arg) => arg.split(':')[0].trim())\n .map((arg) => {\n const newResolutionPath = [...resolutionPath, token];\n if (resolutionPath.includes(arg)) {\n throw new Error(\n `Circular dependency detected: ${newResolutionPath.join(\n ' -> '\n )} -> ${arg}`\n );\n }\n const resolvedArg = this.resolve(arg, context, newResolutionPath);\n return [arg, resolvedArg];\n })\n ) as unknown as Omit<ResolvedConfigValidator<SV, CV>, T>;\n return definition.factory(\n resolvedArguments,\n this.resolve.bind(this),\n context ?? ({} as Record<string, unknown>)\n );\n }\n\n constructor(\n private schemaValidator: SV,\n private dependenciesDefinition: {\n [K in keyof CV]: (\n | Singleton<\n CV[K],\n Omit<ResolvedConfigValidator<SV, CV>, K>,\n ResolvedConfigValidator<SV, CV>[K]\n >\n | Constructed<\n CV[K],\n Omit<ResolvedConfigValidator<SV, CV>, K>,\n ResolvedConfigValidator<SV, CV>[K]\n >\n ) & {\n type: CV[K];\n };\n }\n ) {\n this.configShapes = Object.entries(this.dependenciesDefinition).reduce(\n (acc, [key, { type }]) => ({\n ...acc,\n [key]: type\n }),\n {} as Record<keyof CV, CV[keyof CV]>\n ) as CV;\n }\n\n safeValidateConfigSingletons(): ParseResult<ValidConfigInjector<SV, CV>> {\n const validNonSchemaSingletons = Object.entries(this.configShapes).reduce<\n ParseResult<ResolvedConfigValidator<SV, CV>>\n >(\n (acc, [key, value]) => {\n if (\n this.dependenciesDefinition[key].lifetime === Lifetime.Singleton &&\n !(this.schemaValidator as SchemaValidator).isSchema<\n SchemaFunction<SV> | SchemaConstructor<SV> | IdiomaticSchema<SV>\n >(value) &&\n isConstructor(value)\n ) {\n if (!(this.instances[key] instanceof value)) {\n const expected = value.name;\n const receivedValue: unknown = this.instances[key];\n const received = isConstructed(receivedValue)\n ? receivedValue.constructor.name\n : typeof receivedValue;\n\n if (acc.ok) {\n acc = {\n ok: false,\n errors: []\n };\n }\n acc.errors?.push({\n message: `Expected ${expected}, received ${received}`,\n path: [key]\n });\n } else {\n if (acc.ok) {\n acc = {\n ok: true,\n value: {\n ...acc.value,\n [key]: this.instances[key]\n }\n };\n }\n }\n return acc;\n }\n return acc;\n },\n {\n ok: true,\n value: {} as ResolvedConfigValidator<SV, CV>\n }\n );\n\n const singletons = Object.fromEntries(\n Object.entries(this.configShapes).filter(\n ([key, value]) =>\n this.dependenciesDefinition[key].lifetime === Lifetime.Singleton &&\n (this.schemaValidator as SchemaValidator).isSchema(value)\n )\n );\n const schemaSingletonParseResult = (\n this.schemaValidator as SchemaValidator\n ).parse(\n (this.schemaValidator as SchemaValidator).schemify(singletons),\n Object.fromEntries(\n Object.keys(singletons).map((key) => {\n const dependency = this.dependenciesDefinition[key];\n return [\n key,\n dependency.lifetime === Lifetime.Singleton\n ? this.instances[key]\n : undefined\n ];\n })\n )\n );\n\n const configKeys = Object.keys(this.configShapes);\n\n return validNonSchemaSingletons.ok && schemaSingletonParseResult.ok\n ? {\n ok: true as const,\n value: new ValidConfigInjector<SV, CV>(\n this.schemaValidator,\n this.dependenciesDefinition\n ).load({ ...this.instances })\n }\n : {\n ok: false as const,\n errors: [\n ...(!validNonSchemaSingletons.ok && validNonSchemaSingletons.errors\n ? validNonSchemaSingletons.errors\n : []),\n ...(!schemaSingletonParseResult.ok &&\n schemaSingletonParseResult.errors\n ? schemaSingletonParseResult.errors\n : [])\n ].sort(\n (a, b) =>\n configKeys.indexOf(a.path[0]) - configKeys.indexOf(b.path[0])\n )\n };\n }\n\n validateConfigSingletons(configName: string): ValidConfigInjector<SV, CV> {\n const safeValidateResult = this.safeValidateConfigSingletons();\n\n if (safeValidateResult.ok) {\n return safeValidateResult.value;\n }\n\n throw new Error(\n prettyPrintParseErrors(safeValidateResult.errors, configName)\n );\n }\n\n resolve<T extends keyof CV>(\n token: T,\n context?: Record<string, unknown>,\n resolutionPath: (keyof CV)[] = []\n ): ResolvedConfigValidator<SV, CV>[T] {\n const instance = this.instances[token];\n if (!instance) {\n const definition = this.dependenciesDefinition[token];\n\n if (!definition) {\n throw new Error(`Unable to resolve dependency ${String(token)}`);\n }\n\n switch (definition.lifetime) {\n case Lifetime.Singleton: {\n if (\n isConstructedSingleton<\n CV[T],\n Omit<ResolvedConfigValidator<SV, CV>, T>,\n ResolvedConfigValidator<SV, CV>[T]\n >(definition) &&\n !this.instances[token]\n ) {\n this.instances[token] = this.resolveInstance<T>(\n token,\n definition,\n context,\n resolutionPath\n );\n }\n return this.instances[token] as ResolvedConfigValidator<SV, CV>[T];\n }\n case Lifetime.Scoped: {\n if (\n !isConstructed<\n CV[T],\n Omit<ResolvedConfigValidator<SV, CV>, T>,\n ResolvedConfigValidator<SV, CV>[T]\n >(definition)\n ) {\n throw new Error(\n `Invalid dependency definition for ${String(token)}`\n );\n }\n\n const scopedInstance = this.resolveInstance<T>(\n token,\n definition,\n context,\n resolutionPath\n );\n this.instances[token] = scopedInstance;\n return scopedInstance;\n }\n case Lifetime.Transient: {\n if (\n !isConstructed<\n CV[T],\n Omit<ResolvedConfigValidator<SV, CV>, T>,\n ResolvedConfigValidator<SV, CV>[T]\n >(definition)\n ) {\n throw new Error(\n `Invalid dependency definition for ${String(token)}`\n );\n }\n\n return this.resolveInstance<T>(\n token,\n definition,\n context,\n resolutionPath\n );\n }\n default: {\n isNever(definition);\n throw new Error(\n `Unable to resolve lifetime for dependency ${String(\n token\n )}, ${resolutionPath}`\n );\n }\n }\n } else {\n return instance;\n }\n }\n\n scopedResolver<T extends keyof CV>(\n token: T,\n context?: Record<string, unknown>,\n resolutionPath: (keyof CV)[] = []\n ): (scope?: ConfigInjector<SV, CV>) => ResolvedConfigValidator<SV, CV>[T] {\n return (scope) =>\n (scope ?? this.createScope()).resolve<T>(token, context, resolutionPath);\n }\n\n createScope(): ConfigInjector<SV, CV> {\n return new ConfigInjector<SV, CV>(\n this.schemaValidator,\n this.dependenciesDefinition\n ).load({ ...this.instances });\n }\n\n dispose(): void {\n this.instances = {};\n this.load();\n }\n\n chain<ChainedCV extends ConfigValidator<SV>>(dependenciesDefinition: {\n [K in keyof ChainedCV]: {\n type: ChainedCV[K];\n } & (\n | Singleton<\n ChainedCV[K],\n Omit<ResolvedConfigValidator<SV, CV & ChainedCV>, K>,\n ResolvedConfigValidator<SV, ChainedCV>[K]\n >\n | Constructed<\n ChainedCV[K],\n Omit<ResolvedConfigValidator<SV, CV & ChainedCV>, K>,\n ResolvedConfigValidator<SV, ChainedCV>[K]\n >\n );\n }): ConfigInjector<SV, CV & ChainedCV> {\n return new ConfigInjector<SV, CV>(this.schemaValidator, {\n ...this.dependenciesDefinition,\n ...dependenciesDefinition\n }).load({ ...this.instances }) as unknown as ConfigInjector<\n SV,\n CV & ChainedCV\n >;\n }\n\n tokens(): {\n [K in keyof CV]: K;\n } {\n return Object.fromEntries(\n Object.keys(this.dependenciesDefinition).map((key) => [key, key])\n ) as {\n [K in keyof CV]: K;\n };\n }\n}\n\nexport class ValidConfigInjector<\n SV extends AnySchemaValidator,\n CV extends ConfigValidator<SV>\n> extends ConfigInjector<SV, CV> {\n validConfigInjector!: void;\n}\n","import { TelemetryOptions } from '../types/openTelemetryCollector.types';\n\nexport function evaluateTelemetryOptions(telemetryOptions: TelemetryOptions) {\n return {\n enabled:\n typeof telemetryOptions.enabled === 'boolean'\n ? {\n metrics: telemetryOptions.enabled,\n tracing: telemetryOptions.enabled,\n logging: telemetryOptions.enabled\n }\n : {\n metrics: telemetryOptions.enabled.metrics,\n tracing: telemetryOptions.enabled.tracing,\n logging: telemetryOptions.enabled.logging\n },\n level: telemetryOptions.level\n };\n}\n"],"mappings":"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","names":["import_api","import_semantic_conventions","import_uuid","import_common","import_api","dotenv","import_validator","import_semantic_conventions","import_common","import_validator"]}
1
+ {"version":3,"sources":["../../../src/cache/index.ts","../../../src/cache/cacheKey.ts","../../../src/cache/redisTtlCache.ts","../../../src/http/middleware/request/cors.middleware.ts","../../../src/http/middleware/request/createContext.middleware.ts","../../../src/http/telemetry/constants.ts","../../../src/http/guards/isExpressLikeSchemaHandler.ts","../../../src/http/middleware/request/auth.middleware.ts","../../../src/services/getEnvVar.ts","../../../src/http/telemetry/openTelemetryCollector.ts","../../../src/http/guards/isForklaunchRequest.ts","../../../src/http/telemetry/pinoLogger.ts","../../../src/http/middleware/request/parse.middleware.ts","../../../src/http/middleware/response/parse.middleware.ts","../../../src/http/middleware/response/enrichExpressLikeSend.middleware.ts","../../../src/http/telemetry/recordMetric.ts","../../../src/services/configInjector.ts","../../../src/http/telemetry/evaluateTelemetryOptions.ts"],"sourcesContent":["export * from './cacheKey';\nexport * from './interfaces/ttlCache.interface';\nexport * from './redisTtlCache';\nexport * from './types/ttlCacheRecord.types';\n","/**\n * Creates a function that generates cache keys with a given prefix.\n *\n * @param {string} cacheKeyPrefix - The prefix to use for all cache keys\n * @returns {Function} A function that takes an ID and returns a formatted cache key\n * @example\n * const createUserCacheKey = createCacheKey('user');\n * const key = createUserCacheKey('123'); // Returns 'user:123'\n */\nexport const createCacheKey = (cacheKeyPrefix: string) => (id: string) => {\n return `${cacheKeyPrefix}:${id}`;\n};\n","import { safeParse, safeStringify } from '@forklaunch/common';\nimport { createClient, RedisClientOptions } from 'redis';\nimport {\n evaluateTelemetryOptions,\n MetricsDefinition,\n TelemetryOptions\n} from '../http';\nimport { OpenTelemetryCollector } from '../http/telemetry/openTelemetryCollector';\nimport { TtlCache } from './interfaces/ttlCache.interface';\nimport { TtlCacheRecord } from './types/ttlCacheRecord.types';\n\n/**\n * Type representing a raw reply from Redis commands.\n * Can be a string, number, Buffer, null, undefined, or array of raw replies.\n */\ntype RedisCommandRawReply =\n | string\n | number\n | Buffer\n | null\n | undefined\n | Array<RedisCommandRawReply>;\n\n/**\n * Class representing a Redis-based TTL (Time-To-Live) cache.\n * Implements the TtlCache interface to provide caching functionality with automatic expiration.\n */\nexport class RedisTtlCache implements TtlCache {\n private client;\n private telemetryOptions;\n\n /**\n * Creates an instance of RedisTtlCache.\n *\n * @param {number} ttlMilliseconds - The default Time-To-Live in milliseconds for cache entries\n * @param {OpenTelemetryCollector<MetricsDefinition>} openTelemetryCollector - Collector for OpenTelemetry metrics\n * @param {RedisClientOptions} hostingOptions - Configuration options for the Redis client\n * @param {TelemetryOptions} telemetryOptions - Configuration options for telemetry\n */\n constructor(\n private ttlMilliseconds: number,\n private openTelemetryCollector: OpenTelemetryCollector<MetricsDefinition>,\n hostingOptions: RedisClientOptions,\n telemetryOptions: TelemetryOptions\n ) {\n this.telemetryOptions = evaluateTelemetryOptions(telemetryOptions);\n this.client = createClient(hostingOptions);\n if (this.telemetryOptions.enabled.logging) {\n this.client.on('error', (err) => this.openTelemetryCollector.error(err));\n this.client.connect().catch(this.openTelemetryCollector.error);\n }\n }\n\n /**\n * Parses a raw Redis reply into the expected type.\n * Handles null values, arrays, buffers, and JSON strings.\n *\n * @template T - The expected type of the parsed value\n * @param {RedisCommandRawReply} value - The raw value from Redis to parse\n * @returns {T} The parsed value cast to type T\n */\n private parseValue<T>(value: RedisCommandRawReply): T {\n if (value == null) {\n return null as T;\n }\n\n if (Array.isArray(value)) {\n return value.map((v) => this.parseValue<T>(v)) as T;\n }\n\n if (Buffer.isBuffer(value)) {\n return value.toJSON() as T;\n }\n\n switch (typeof value) {\n case 'object':\n case 'string':\n return safeParse(value) as T;\n case 'number':\n return value as T;\n }\n }\n\n /**\n * Puts a record into the Redis cache.\n *\n * @template T - The type of value being cached\n * @param {TtlCacheRecord<T>} param0 - The cache record containing key, value and optional TTL\n * @param {string} param0.key - The key to store the value under\n * @param {T} param0.value - The value to cache\n * @param {number} [param0.ttlMilliseconds] - Optional TTL in milliseconds, defaults to constructor value\n * @returns {Promise<void>} A promise that resolves when the value is cached\n */\n async putRecord<T>({\n key,\n value,\n ttlMilliseconds = this.ttlMilliseconds\n }: TtlCacheRecord<T>): Promise<void> {\n if (this.telemetryOptions.enabled.logging) {\n this.openTelemetryCollector.info(`Putting record into cache: ${key}`);\n }\n await this.client.set(key, safeStringify(value), {\n PX: ttlMilliseconds\n });\n }\n\n /**\n * Puts multiple records into the Redis cache in a single transaction.\n *\n * @template T - The type of values being cached\n * @param {TtlCacheRecord<T>[]} cacheRecords - Array of cache records to store\n * @returns {Promise<void>} A promise that resolves when all values are cached\n */\n async putBatchRecords<T>(cacheRecords: TtlCacheRecord<T>[]): Promise<void> {\n const multiCommand = this.client.multi();\n for (const { key, value, ttlMilliseconds } of cacheRecords) {\n multiCommand.set(key, safeStringify(value), {\n PX: ttlMilliseconds || this.ttlMilliseconds\n });\n }\n await multiCommand.exec();\n }\n\n /**\n * Adds a value to the left end of a Redis list.\n *\n * @template T - The type of value being enqueued\n * @param {string} queueName - The name of the Redis list\n * @param {T} value - The value to add to the list\n * @returns {Promise<void>} A promise that resolves when the value is enqueued\n */\n async enqueueRecord<T>(queueName: string, value: T): Promise<void> {\n await this.client.lPush(queueName, safeStringify(value));\n }\n\n /**\n * Adds multiple values to the left end of a Redis list in a single transaction.\n *\n * @template T - The type of values being enqueued\n * @param {string} queueName - The name of the Redis list\n * @param {T[]} values - Array of values to add to the list\n * @returns {Promise<void>} A promise that resolves when all values are enqueued\n */\n async enqueueBatchRecords<T>(queueName: string, values: T[]): Promise<void> {\n const multiCommand = this.client.multi();\n for (const value of values) {\n multiCommand.lPush(queueName, safeStringify(value));\n }\n await multiCommand.exec();\n }\n\n /**\n * Deletes a record from the Redis cache.\n *\n * @param {string} cacheRecordKey - The key of the record to delete\n * @returns {Promise<void>} A promise that resolves when the record is deleted\n */\n async deleteRecord(cacheRecordKey: string): Promise<void> {\n await this.client.del(cacheRecordKey);\n }\n\n /**\n * Deletes multiple records from the Redis cache in a single transaction.\n *\n * @param {string[]} cacheRecordKeys - Array of keys to delete\n * @returns {Promise<void>} A promise that resolves when all records are deleted\n */\n async deleteBatchRecords(cacheRecordKeys: string[]): Promise<void> {\n const multiCommand = this.client.multi();\n for (const key of cacheRecordKeys) {\n multiCommand.del(key);\n }\n await multiCommand.exec();\n }\n\n /**\n * Removes and returns the rightmost element from a Redis list.\n *\n * @template T - The type of value being dequeued\n * @param {string} queueName - The name of the Redis list\n * @returns {Promise<T>} A promise that resolves with the dequeued value\n * @throws {Error} If the queue is empty\n */\n async dequeueRecord<T>(queueName: string): Promise<T> {\n const value = await this.client.rPop(queueName);\n if (value === null) {\n throw new Error(`Queue is empty: ${queueName}`);\n }\n return safeParse(value) as T;\n }\n\n /**\n * Removes and returns multiple elements from the right end of a Redis list.\n *\n * @template T - The type of values being dequeued\n * @param {string} queueName - The name of the Redis list\n * @param {number} pageSize - Maximum number of elements to dequeue\n * @returns {Promise<T[]>} A promise that resolves with an array of dequeued values\n */\n async dequeueBatchRecords<T>(\n queueName: string,\n pageSize: number\n ): Promise<T[]> {\n const multiCommand = this.client.multi();\n for (let i = 0; i < pageSize; i++) {\n multiCommand.rPop(queueName);\n }\n const values = await multiCommand.exec();\n return values\n .map((value) =>\n this.parseValue<T>(value as unknown as RedisCommandRawReply)\n )\n .filter(Boolean);\n }\n\n /**\n * Reads a record from the Redis cache.\n *\n * @template T - The type of value being read\n * @param {string} cacheRecordKey - The key of the record to read\n * @returns {Promise<TtlCacheRecord<T>>} A promise that resolves with the cache record\n * @throws {Error} If the record is not found\n */\n async readRecord<T>(cacheRecordKey: string): Promise<TtlCacheRecord<T>> {\n const [value, ttl] = await this.client\n .multi()\n .get(cacheRecordKey)\n .ttl(cacheRecordKey)\n .exec();\n if (value === null) {\n throw new Error(`Record not found for key: ${cacheRecordKey}`);\n }\n\n return {\n key: cacheRecordKey,\n value: this.parseValue<T>(value as unknown as RedisCommandRawReply),\n ttlMilliseconds:\n this.parseValue<number>(ttl as unknown as RedisCommandRawReply) * 1000\n };\n }\n\n /**\n * Reads multiple records from the Redis cache.\n *\n * @template T - The type of values being read\n * @param {string[] | string} cacheRecordKeysOrPrefix - Array of keys to read, or a prefix pattern\n * @returns {Promise<TtlCacheRecord<T>[]>} A promise that resolves with an array of cache records\n */\n async readBatchRecords<T>(\n cacheRecordKeysOrPrefix: string[] | string\n ): Promise<TtlCacheRecord<T>[]> {\n const keys = Array.isArray(cacheRecordKeysOrPrefix)\n ? cacheRecordKeysOrPrefix\n : await this.client.keys(cacheRecordKeysOrPrefix + '*');\n const multiCommand = this.client.multi();\n for (const key of keys) {\n multiCommand.get(key);\n multiCommand.ttl(key);\n }\n const values = await multiCommand.exec();\n return values.reduce<TtlCacheRecord<T>[]>((acc, value, index) => {\n if (index % 2 === 0) {\n const maybeValue = this.parseValue<T>(\n value as unknown as RedisCommandRawReply\n );\n const ttl = this.parseValue<number>(\n values[index + 1] as unknown as RedisCommandRawReply\n );\n if (maybeValue && ttl) {\n acc.push({\n key: keys[index / 2],\n value: maybeValue,\n ttlMilliseconds: ttl * 1000\n });\n }\n }\n return acc;\n }, []);\n }\n\n /**\n * Lists all keys in the Redis cache that match a pattern prefix.\n *\n * @param {string} pattern_prefix - The prefix pattern to match keys against\n * @returns {Promise<string[]>} A promise that resolves with an array of matching keys\n */\n async listKeys(pattern_prefix: string): Promise<string[]> {\n const keys = await this.client.keys(pattern_prefix + '*');\n return keys;\n }\n\n /**\n * Checks if a record exists in the Redis cache.\n *\n * @param {string} cacheRecordKey - The key to check\n * @returns {Promise<boolean>} A promise that resolves with true if the record exists, false otherwise\n */\n async peekRecord(cacheRecordKey: string): Promise<boolean> {\n const result = await this.client.exists(cacheRecordKey);\n return result === 1;\n }\n\n /**\n * Checks if multiple records exist in the Redis cache.\n *\n * @param {string[] | string} cacheRecordKeysOrPrefix - Array of keys to check, or a prefix pattern\n * @returns {Promise<boolean[]>} A promise that resolves with an array of existence booleans\n */\n async peekBatchRecords(\n cacheRecordKeysOrPrefix: string[] | string\n ): Promise<boolean[]> {\n const keys = Array.isArray(cacheRecordKeysOrPrefix)\n ? cacheRecordKeysOrPrefix\n : await this.client.keys(cacheRecordKeysOrPrefix + '*');\n const multiCommand = this.client.multi();\n for (const key of keys) {\n multiCommand.exists(key);\n }\n const results = await multiCommand.exec();\n return results.map((result) => (result as unknown as number) === 1);\n }\n\n /**\n * Peeks at a record in the Redis cache.\n *\n * @template T - The type of value being peeked at\n * @param {string} queueName - The name of the Redis queue\n * @returns {Promise<T>} A promise that resolves with the peeked value\n */\n async peekQueueRecord<T>(queueName: string): Promise<T> {\n const value = await this.client.lRange(queueName, 0, 0);\n return this.parseValue<T>(value[0]);\n }\n\n /**\n * Peeks at multiple records in the Redis cache.\n *\n * @template T - The type of values being peeked at\n * @param {string} queueName - The name of the Redis queue\n * @param {number} pageSize - The number of records to peek at\n * @returns {Promise<T[]>} A promise that resolves with an array of peeked values\n */\n async peekQueueRecords<T>(queueName: string, pageSize: number): Promise<T[]> {\n const values = await this.client.lRange(queueName, 0, pageSize - 1);\n return values.map((value) => this.parseValue<T>(value)).filter(Boolean);\n }\n\n /**\n * Gracefully disconnects from the Redis server.\n *\n * @returns {Promise<void>} A promise that resolves when the connection is closed\n */\n async disconnect(): Promise<void> {\n await this.client.quit();\n }\n\n /**\n * Gets the default Time-To-Live value in milliseconds.\n *\n * @returns {number} The default TTL in milliseconds\n */\n getTtlMilliseconds(): number {\n return this.ttlMilliseconds;\n }\n\n /**\n * Gets the underlying Redis client instance.\n *\n * @returns {typeof this.client} The Redis client instance\n */\n getClient(): typeof this.client {\n return this.client;\n }\n}\n","import { AnySchemaValidator } from '@forklaunch/validator';\nimport corsMiddleware from 'cors';\nimport { ParsedQs } from 'qs';\nimport {\n ForklaunchNextFunction,\n ForklaunchRequest,\n ForklaunchResHeaders,\n ForklaunchResponse\n} from '../../types/apiDefinition.types';\nimport { ParamsDictionary } from '../../types/contractDetails.types';\n\n/**\n * Cors middleware handler\n *\n * @param req - Express-like request object\n * @param res - Express-like response object\n * @param next - Express-like next function\n */\nexport function cors<\n SV extends AnySchemaValidator,\n P extends ParamsDictionary,\n ResBodyMap extends Record<number, unknown>,\n ReqBody extends Record<string, unknown>,\n ReqQuery extends ParsedQs,\n ReqHeaders extends Record<string, string>,\n ResHeaders extends Record<string, string>,\n LocalsObj extends Record<string, unknown>\n>(\n req: ForklaunchRequest<SV, P, ReqBody, ReqQuery, ReqHeaders>,\n res: ForklaunchResponse<\n unknown,\n ResBodyMap,\n ForklaunchResHeaders & ResHeaders,\n LocalsObj\n >,\n next?: ForklaunchNextFunction\n) {\n if (req.method === 'OPTIONS') {\n res.cors = true;\n }\n corsMiddleware()(req, res, next ?? (() => {}));\n}\n","import { AnySchemaValidator } from '@forklaunch/validator';\nimport { trace } from '@opentelemetry/api';\nimport { v4 } from 'uuid';\nimport { ATTR_CORRELATION_ID } from '../../telemetry/constants';\nimport {\n ExpressLikeSchemaHandler,\n ForklaunchNextFunction\n} from '../../types/apiDefinition.types';\nimport {\n Body,\n HeadersObject,\n ParamsObject,\n QueryObject,\n ResponsesObject\n} from '../../types/contractDetails.types';\n\n/**\n * Middleware to create and add a request context.\n *\n * @template SV - A type that extends AnySchemaValidator.\n * @template Request - A type that extends ForklaunchRequest.\n * @template Response - A type that extends ForklaunchResponse.\n * @template NextFunction - A type that extends ForklaunchNextFunction.\n * @param {SV} schemaValidator - The schema validator.\n * @returns {Function} - Middleware function to create request context.\n */\nexport function createContext<\n SV extends AnySchemaValidator,\n P extends ParamsObject<SV>,\n ResBodyMap extends ResponsesObject<SV>,\n ReqBody extends Body<SV>,\n ReqQuery extends QueryObject<SV>,\n ReqHeaders extends HeadersObject<SV>,\n ResHeaders extends HeadersObject<SV>,\n LocalsObj extends Record<string, unknown>\n>(\n schemaValidator: SV\n): ExpressLikeSchemaHandler<\n SV,\n P,\n ResBodyMap,\n ReqBody,\n ReqQuery,\n ReqHeaders,\n ResHeaders,\n LocalsObj,\n unknown,\n unknown,\n ForklaunchNextFunction\n> {\n return function setContext(req, res, next?) {\n req.schemaValidator = schemaValidator;\n\n let correlationId = v4();\n\n if (req.headers['x-correlation-id']) {\n correlationId = req.headers['x-correlation-id'];\n }\n\n res.setHeader('x-correlation-id', correlationId);\n\n req.context = {\n correlationId: correlationId\n };\n\n const span = trace.getActiveSpan();\n if (span != null) {\n req.context.span = span;\n req.context.span?.setAttribute(ATTR_CORRELATION_ID, correlationId);\n }\n\n next?.();\n };\n}\n","import {\n ATTR_HTTP_REQUEST_METHOD,\n ATTR_HTTP_RESPONSE_STATUS_CODE,\n ATTR_HTTP_ROUTE,\n ATTR_SERVICE_NAME\n} from '@opentelemetry/semantic-conventions';\n\nexport const ATTR_API_NAME = 'api.name';\nexport const ATTR_CORRELATION_ID = 'correlation.id';\n\nexport {\n ATTR_HTTP_REQUEST_METHOD,\n ATTR_HTTP_RESPONSE_STATUS_CODE,\n ATTR_HTTP_ROUTE,\n ATTR_SERVICE_NAME\n};\n","import { extractArgumentNames } from '@forklaunch/common';\nimport { AnySchemaValidator } from '@forklaunch/validator';\nimport { ExpressLikeSchemaHandler } from '../types/apiDefinition.types';\nimport {\n Body,\n HeadersObject,\n ParamsObject,\n QueryObject,\n ResponsesObject\n} from '../types/contractDetails.types';\n\n/**\n * Type guard that checks if an object is an Express-like schema handler.\n * A schema handler is a function that takes up to 3 arguments (request, response, next)\n * and is used for middleware or route handlers in an Express-like application.\n *\n * @template SV - A type that extends AnySchemaValidator\n * @template P - The type of route parameters\n * @template ResBodyMap - The type of response body mapping\n * @template ReqBody - The type of request body\n * @template ReqQuery - The type of request query parameters\n * @template ReqHeaders - The type of request headers\n * @template ResHeaders - The type of response headers\n * @template LocalsObj - The type of locals object\n * @template BaseRequest - The base request type\n * @template BaseResponse - The base response type\n * @template NextFunction - The type of next function\n * @param {unknown} middleware - The object to check\n * @returns {boolean} A type predicate indicating whether the object is an ExpressLikeSchemaHandler\n *\n * @example\n * ```ts\n * if (isExpressLikeSchemaHandler(middleware)) {\n * // middleware is now typed as ExpressLikeSchemaHandler\n * middleware(req, res, next);\n * }\n * ```\n */\nexport function isExpressLikeSchemaHandler<\n SV extends AnySchemaValidator,\n P extends ParamsObject<SV>,\n ResBodyMap extends ResponsesObject<SV>,\n ReqBody extends Body<SV>,\n ReqQuery extends QueryObject<SV>,\n ReqHeaders extends HeadersObject<SV>,\n ResHeaders extends HeadersObject<SV>,\n LocalsObj extends Record<string, unknown>,\n BaseRequest,\n BaseResponse,\n NextFunction\n>(\n middleware: unknown\n): middleware is ExpressLikeSchemaHandler<\n SV,\n P,\n ResBodyMap,\n ReqBody,\n ReqQuery,\n ReqHeaders,\n ResHeaders,\n LocalsObj,\n BaseRequest,\n BaseResponse,\n NextFunction\n> {\n const extractedArgumentNames =\n typeof middleware === 'function' &&\n new Set(\n extractArgumentNames(middleware).map((argumentName) =>\n argumentName.toLowerCase()\n )\n );\n return extractedArgumentNames && extractedArgumentNames.size <= 3;\n}\n","import { AnySchemaValidator } from '@forklaunch/validator';\nimport { jwtVerify } from 'jose';\nimport { ParsedQs } from 'qs';\nimport {\n ForklaunchNextFunction,\n ForklaunchRequest,\n ForklaunchResponse,\n MapParamsSchema,\n MapReqBodySchema,\n MapReqHeadersSchema,\n MapReqQuerySchema,\n MapResBodyMapSchema,\n MapResHeadersSchema,\n ResolvedForklaunchRequest\n} from '../../types/apiDefinition.types';\nimport {\n AuthMethods,\n Body,\n HeadersObject,\n ParamsDictionary,\n ParamsObject,\n QueryObject,\n ResponsesObject\n} from '../../types/contractDetails.types';\n\nconst invalidAuthorizationTokenFormat = [\n 401,\n 'Invalid Authorization token format.'\n] as const;\nconst invalidAuthorizationSubject = [\n 403,\n 'Invalid Authorization subject.'\n] as const;\nconst invalidAuthorizationTokenPermissions = [\n 403,\n 'Invalid Authorization permissions.'\n] as const;\nconst invalidAuthorizationTokenRoles = [\n 403,\n 'Invalid Authorization roles.'\n] as const;\nconst invalidAuthorizationToken = [\n 403,\n 'Invalid Authorization token.'\n] as const;\nconst invalidAuthorizationLogin = [\n 403,\n 'Invalid Authorization login.'\n] as const;\n\n/**\n * Checks the authorization token for validity.\n *\n * @param {AuthMethod} [authorizationMethod] - The method of authorization.\n * @param {string} [authorizationToken] - The authorization string.\n * @returns {Promise<[401 | 403, string] | string | undefined>} - The result of the authorization check.\n */\nasync function checkAuthorizationToken<\n SV extends AnySchemaValidator,\n P extends ParamsDictionary,\n ReqBody extends Record<string, unknown>,\n ReqQuery extends ParsedQs,\n ReqHeaders extends Record<string, string>,\n BaseRequest\n>(\n authorizationMethod: AuthMethods<\n SV,\n P,\n ReqBody,\n ReqQuery,\n ReqHeaders,\n BaseRequest\n >,\n authorizationToken?: string,\n req?: ResolvedForklaunchRequest<\n SV,\n P,\n ReqBody,\n ReqQuery,\n ReqHeaders,\n BaseRequest\n >\n): Promise<readonly [401 | 403 | 500, string] | undefined> {\n if (authorizationToken == null) {\n return [401, 'No Authorization token provided.'];\n }\n\n const [tokenPrefix, token] = authorizationToken.split(' ');\n\n let resourceId;\n\n switch (authorizationMethod.method) {\n case 'jwt': {\n if (tokenPrefix !== 'Bearer') {\n return invalidAuthorizationTokenFormat;\n }\n\n try {\n const decodedJwt = await jwtVerify(\n token,\n new TextEncoder().encode(\n // TODO: Check this at application startup if there is any route with jwt checking\n process.env.JWT_SECRET\n )\n );\n\n if (!decodedJwt.payload.sub) {\n return invalidAuthorizationSubject;\n }\n\n resourceId = decodedJwt.payload.sub;\n } catch (error) {\n (\n req as ForklaunchRequest<SV, P, ReqBody, ReqQuery, ReqHeaders>\n ).openTelemetryCollector.error(error);\n return invalidAuthorizationToken;\n }\n\n break;\n }\n case 'basic': {\n if (authorizationToken !== 'Basic') {\n return invalidAuthorizationTokenFormat;\n }\n\n const [username, password] = Buffer.from(token, 'base64')\n .toString('utf-8')\n .split(':');\n\n if (!username || !password) {\n return invalidAuthorizationTokenFormat;\n }\n\n if (!authorizationMethod.login(username, password)) {\n return invalidAuthorizationLogin;\n }\n\n resourceId = username;\n break;\n }\n case 'other':\n if (tokenPrefix !== authorizationMethod.tokenPrefix) {\n return invalidAuthorizationTokenFormat;\n }\n\n resourceId = authorizationMethod.decodeResource(token);\n\n break;\n }\n\n if (\n authorizationMethod.allowedPermissions ||\n authorizationMethod.forbiddenPermissions\n ) {\n if (!authorizationMethod.mapPermissions) {\n return [500, 'No permission mapping function provided.'];\n }\n\n const resourcePermissions = await authorizationMethod.mapPermissions(\n resourceId,\n req\n );\n\n if (authorizationMethod.allowedPermissions) {\n if (\n resourcePermissions.intersection(authorizationMethod.allowedPermissions)\n .size === 0\n ) {\n return invalidAuthorizationTokenPermissions;\n }\n }\n\n if (authorizationMethod.forbiddenPermissions) {\n if (\n resourcePermissions.intersection(\n authorizationMethod.forbiddenPermissions\n ).size !== 0\n ) {\n return invalidAuthorizationTokenPermissions;\n }\n }\n }\n\n if (authorizationMethod.allowedRoles || authorizationMethod.forbiddenRoles) {\n if (!authorizationMethod.mapRoles) {\n return [500, 'No role mapping function provided.'];\n }\n\n const resourceRoles = await authorizationMethod.mapRoles(resourceId, req);\n\n if (authorizationMethod.allowedRoles) {\n if (\n resourceRoles.intersection(authorizationMethod.allowedRoles).size === 0\n ) {\n return invalidAuthorizationTokenRoles;\n }\n }\n\n if (authorizationMethod.forbiddenRoles) {\n if (\n resourceRoles.intersection(authorizationMethod.forbiddenRoles).size !==\n 0\n ) {\n return invalidAuthorizationTokenRoles;\n }\n }\n }\n\n return [401, 'Invalid Authorization method.'];\n}\n\n/**\n * Middleware to parse request authorization.\n *\n * @template SV - A type that extends AnySchemaValidator.\n * @template Request - A type that extends ForklaunchRequest.\n * @template Response - A type that extends ForklaunchResponse.\n * @template NextFunction - A type that extends ForklaunchNextFunction.\n * @param {Request} req - The request object.\n * @param {Response} res - The response object.\n * @param {NextFunction} [next] - The next middleware function.\n */\nexport async function parseRequestAuth<\n SV extends AnySchemaValidator,\n P extends ParamsObject<SV>,\n ResBodyMap extends ResponsesObject<SV>,\n ReqBody extends Body<SV>,\n ReqQuery extends QueryObject<SV>,\n ReqHeaders extends HeadersObject<SV>,\n ResHeaders extends HeadersObject<SV>,\n LocalsObj extends Record<string, unknown>\n>(\n req: ForklaunchRequest<\n SV,\n MapParamsSchema<SV, P>,\n MapReqBodySchema<SV, ReqBody>,\n MapReqQuerySchema<SV, ReqQuery>,\n MapReqHeadersSchema<SV, ReqHeaders>\n >,\n res: ForklaunchResponse<\n unknown,\n MapResBodyMapSchema<SV, ResBodyMap>,\n MapResHeadersSchema<SV, ResHeaders>,\n LocalsObj\n >,\n next?: ForklaunchNextFunction\n) {\n const auth = req.contractDetails.auth as AuthMethods<\n SV,\n MapParamsSchema<SV, P>,\n MapReqBodySchema<SV, ReqBody>,\n MapReqQuerySchema<SV, ReqQuery>,\n MapReqHeadersSchema<SV, ReqHeaders>,\n unknown\n >;\n\n if (auth) {\n const [error, message] =\n (await checkAuthorizationToken<\n SV,\n MapParamsSchema<SV, P>,\n MapReqBodySchema<SV, ReqBody>,\n MapReqQuerySchema<SV, ReqQuery>,\n MapReqHeadersSchema<SV, ReqHeaders>,\n unknown\n >(\n auth,\n req.headers[\n (auth.method === 'other' ? auth.headerName : undefined) ??\n 'Authorization'\n ],\n req\n )) ?? [];\n if (error != null) {\n res.type('text/plain');\n res.status(error).send(message as never);\n next?.(new Error(message));\n }\n }\n\n next?.();\n}\n","// This is a simple function that returns the value of an environment variable.\n// It casts a potentially undefined value to a string, since it will be validated in order to be bootstrapped.\n\nexport function getEnvVar(name: string): string {\n const value = process.env[name];\n return value as string;\n}\n","import { HyperExpressInstrumentation } from '@forklaunch/opentelemetry-instrumentation-hyper-express';\nimport {\n Counter,\n Gauge,\n Histogram,\n metrics,\n ObservableCounter,\n ObservableGauge,\n ObservableUpDownCounter,\n UpDownCounter\n} from '@opentelemetry/api';\nimport { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-http';\nimport { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http';\nimport { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';\nimport { ExpressInstrumentation } from '@opentelemetry/instrumentation-express';\nimport { HttpInstrumentation } from '@opentelemetry/instrumentation-http';\nimport { resourceFromAttributes } from '@opentelemetry/resources';\nimport { BatchLogRecordProcessor } from '@opentelemetry/sdk-logs';\nimport { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';\nimport { NodeSDK } from '@opentelemetry/sdk-node';\nimport {\n ATTR_HTTP_REQUEST_METHOD,\n ATTR_HTTP_RESPONSE_STATUS_CODE,\n ATTR_HTTP_ROUTE,\n ATTR_SERVICE_NAME\n} from '@opentelemetry/semantic-conventions';\nimport dotenv from 'dotenv';\nimport { LevelWithSilent, LevelWithSilentOrString } from 'pino';\nimport { v4 } from 'uuid';\nimport { getEnvVar } from '../../services/getEnvVar';\nimport { isForklaunchRequest } from '../guards/isForklaunchRequest';\nimport {\n LogFn,\n LoggerMeta,\n MetricsDefinition,\n MetricType\n} from '../types/openTelemetryCollector.types';\nimport { ATTR_API_NAME, ATTR_CORRELATION_ID } from './constants';\nimport { logger } from './pinoLogger';\n\nexport class OpenTelemetryCollector<\n AppliedMetricsDefinition extends MetricsDefinition\n> {\n private readonly uuid = v4();\n private readonly logger;\n private readonly metrics: Record<\n keyof AppliedMetricsDefinition,\n | Counter\n | Gauge\n | Histogram\n | UpDownCounter\n | ObservableCounter\n | ObservableGauge\n | ObservableUpDownCounter\n >;\n\n // scoped creation and create this in middleware when api execute. Also add correlation id\n constructor(\n private readonly serviceName: string,\n level?: LevelWithSilentOrString,\n metricDefinitions?: AppliedMetricsDefinition\n ) {\n this.logger = logger(level || 'info');\n\n this.metrics = {} as Record<\n keyof AppliedMetricsDefinition,\n MetricType<AppliedMetricsDefinition[keyof AppliedMetricsDefinition]>\n >;\n\n for (const [metricId, metricType] of Object.entries(\n metricDefinitions ?? {}\n )) {\n switch (metricType) {\n case 'counter':\n this.metrics[metricId as keyof AppliedMetricsDefinition] = metrics\n .getMeter(this.serviceName)\n .createCounter(metricId);\n break;\n case 'gauge':\n this.metrics[metricId as keyof AppliedMetricsDefinition] = metrics\n .getMeter(this.serviceName)\n .createGauge(metricId);\n break;\n case 'histogram':\n this.metrics[metricId as keyof AppliedMetricsDefinition] = metrics\n .getMeter(this.serviceName)\n .createHistogram(metricId);\n break;\n case 'upDownCounter':\n this.metrics[metricId as keyof AppliedMetricsDefinition] = metrics\n .getMeter(this.serviceName)\n .createUpDownCounter(metricId);\n break;\n case 'observableCounter':\n this.metrics[metricId as keyof AppliedMetricsDefinition] = metrics\n .getMeter(this.serviceName)\n .createObservableCounter(metricId);\n break;\n case 'observableGauge':\n this.metrics[metricId as keyof AppliedMetricsDefinition] = metrics\n .getMeter(this.serviceName)\n .createObservableGauge(metricId);\n break;\n case 'observableUpDownCounter':\n this.metrics[metricId as keyof AppliedMetricsDefinition] = metrics\n .getMeter(this.serviceName)\n .createObservableUpDownCounter(metricId);\n break;\n }\n }\n\n this.log(\n 'info',\n 'OpenTelemetry Collector (Traces + Logs + Metrics) started'\n );\n }\n\n log(level: LevelWithSilent, ...args: (string | unknown | LoggerMeta)[]) {\n this.logger.log(level, ...args);\n }\n\n info: LogFn = (\n msg: string | unknown | LoggerMeta,\n ...args: (string | unknown | LoggerMeta)[]\n ) => {\n this.logger.log('info', msg, ...args);\n };\n\n error: LogFn = (\n msg: string | unknown | LoggerMeta,\n ...args: (string | unknown | LoggerMeta)[]\n ) => {\n this.logger.log('error', msg, ...args);\n };\n\n warn: LogFn = (\n msg: string | unknown | LoggerMeta,\n ...args: (string | unknown | LoggerMeta)[]\n ) => {\n this.logger.log('warn', msg, ...args);\n };\n\n debug: LogFn = (\n msg: string | unknown | LoggerMeta,\n ...args: (string | unknown | LoggerMeta)[]\n ) => {\n this.logger.log('debug', msg, ...args);\n };\n\n trace: LogFn = (\n msg: string | unknown | LoggerMeta,\n ...args: (string | unknown | LoggerMeta)[]\n ) => {\n this.logger.log('trace', msg, ...args);\n };\n\n getMetric<T extends keyof AppliedMetricsDefinition>(\n metricId: T\n ): MetricType<AppliedMetricsDefinition[T]> {\n return this.metrics[metricId] as MetricType<AppliedMetricsDefinition[T]>;\n }\n}\n\ndotenv.config({ path: getEnvVar('ENV_FILE_PATH') });\n\nnew NodeSDK({\n resource: resourceFromAttributes({\n [ATTR_SERVICE_NAME]: getEnvVar('OTEL_SERVICE_NAME')\n }),\n traceExporter: new OTLPTraceExporter({\n url: `${\n getEnvVar('OTEL_EXPORTER_OTLP_ENDPOINT') ?? 'http://localhost:4318'\n }/v1/traces`\n }),\n metricReader: new PeriodicExportingMetricReader({\n exporter: new OTLPMetricExporter({\n url: `${\n getEnvVar('OTEL_EXPORTER_OTLP_ENDPOINT') ?? 'http://localhost:4318'\n }/v1/metrics`\n }),\n exportIntervalMillis: 5000\n }),\n logRecordProcessors: [\n new BatchLogRecordProcessor(\n new OTLPLogExporter({\n url: `${\n getEnvVar('OTEL_EXPORTER_OTLP_ENDPOINT') ?? 'http://localhost:4318'\n }/v1/logs`\n })\n )\n ],\n instrumentations: [\n new HttpInstrumentation({\n applyCustomAttributesOnSpan: (span, request) => {\n span.setAttribute(\n 'service.name',\n getEnvVar('OTEL_SERVICE_NAME') ?? 'unknown'\n );\n if (isForklaunchRequest(request)) {\n span.setAttribute('api.name', request.contractDetails?.name);\n }\n }\n }),\n new ExpressInstrumentation(),\n new HyperExpressInstrumentation()\n ]\n}).start();\n\n// begin static metrics -- these have to be in here in order to instantiate these after the SDK is instantiated\nexport const httpRequestsTotalCounter = metrics\n .getMeter(getEnvVar('OTEL_SERVICE_NAME') || 'unknown')\n .createCounter<{\n [ATTR_SERVICE_NAME]: string;\n [ATTR_API_NAME]: string;\n [ATTR_CORRELATION_ID]: string;\n [ATTR_HTTP_REQUEST_METHOD]: string;\n [ATTR_HTTP_ROUTE]: string;\n [ATTR_HTTP_RESPONSE_STATUS_CODE]: number;\n }>('http_requests_total', {\n description: 'Number of HTTP requests'\n });\n\nexport const httpServerDurationHistogram = metrics\n .getMeter(getEnvVar('OTEL_SERVICE_NAME') || 'unknown')\n .createHistogram<{\n [ATTR_SERVICE_NAME]: string;\n [ATTR_API_NAME]: string;\n [ATTR_HTTP_REQUEST_METHOD]: string;\n [ATTR_HTTP_ROUTE]: string;\n [ATTR_HTTP_RESPONSE_STATUS_CODE]: number;\n }>('http_server_duration', {\n description: 'Duration of HTTP server requests',\n unit: 's'\n });\n","import { AnySchemaValidator } from '@forklaunch/validator';\nimport { ParsedQs } from 'qs';\nimport { ForklaunchRequest } from '../types/apiDefinition.types';\nimport { ParamsDictionary } from '../types/contractDetails.types';\n\n/**\n * Type guard that checks if an object is a Forklaunch request.\n * A Forklaunch request is an object that contains contract details and follows the Forklaunch request structure.\n *\n * @template SV - A type that extends AnySchemaValidator\n * @template P - The type of route parameters that extends ParamsDictionary\n * @template ReqBody - The type of request body that extends Record<string, unknown>\n * @template ReqQuery - The type of request query parameters that extends ParsedQs\n * @template ReqHeaders - The type of request headers that extends Record<string, string>\n * @param {unknown} request - The object to check\n * @returns {boolean} A type predicate indicating whether the object is a ForklaunchRequest\n *\n * @example\n * ```ts\n * if (isForklaunchRequest(request)) {\n * // request is now typed as ForklaunchRequest\n * const { contractDetails } = request;\n * }\n * ```\n */\nexport function isForklaunchRequest<\n SV extends AnySchemaValidator,\n P extends ParamsDictionary,\n ReqBody extends Record<string, unknown>,\n ReqQuery extends ParsedQs,\n ReqHeaders extends Record<string, string>\n>(\n request: unknown\n): request is ForklaunchRequest<SV, P, ReqBody, ReqQuery, ReqHeaders> {\n return (\n request != null &&\n typeof request === 'object' &&\n 'contractDetails' in request\n );\n}\n","import { isNever, safeStringify } from '@forklaunch/common';\nimport { trace } from '@opentelemetry/api';\nimport { AnyValueMap, logs } from '@opentelemetry/api-logs';\nimport pino, { LevelWithSilent, LevelWithSilentOrString, Logger } from 'pino';\nimport PinoPretty from 'pino-pretty';\nimport { isLoggerMeta } from '../guards/isLoggerMeta';\nimport { LogFn, LoggerMeta } from '../types/openTelemetryCollector.types';\n\nexport function meta(meta: Record<string, unknown>) {\n return meta as LoggerMeta;\n}\n\nfunction mapSeverity(level: LevelWithSilent) {\n switch (level) {\n case 'silent':\n return 0;\n case 'trace':\n return 1;\n case 'debug':\n return 5;\n case 'info':\n return 9;\n case 'warn':\n return 13;\n case 'error':\n return 17;\n case 'fatal':\n return 21;\n default:\n isNever(level);\n return 0;\n }\n}\n\nclass PinoLogger {\n private pinoLogger: Logger;\n private meta: AnyValueMap;\n private prettyPrinter = PinoPretty.prettyFactory({\n colorize: true\n });\n\n constructor(level: LevelWithSilentOrString, meta: AnyValueMap = {}) {\n this.pinoLogger = pino({\n level: level || 'info',\n formatters: {\n level(label) {\n return { level: label };\n }\n },\n timestamp: pino.stdTimeFunctions.isoTime,\n transport: {\n target: 'pino-pretty',\n options: { colorize: true }\n }\n });\n this.meta = meta;\n }\n\n log(level: LevelWithSilent, ...args: (string | unknown | LoggerMeta)[]) {\n let meta: AnyValueMap = {};\n\n const filteredArgs = args\n .filter((arg) => {\n if (isLoggerMeta(arg)) {\n Object.assign(meta, arg);\n return false;\n }\n return true;\n })\n .map(safeStringify) as Parameters<pino.LogFn>;\n\n const activeSpan = trace.getActiveSpan();\n if (activeSpan) {\n const activeSpanContext = activeSpan.spanContext();\n meta.trace_id = activeSpanContext.traceId;\n meta.span_id = activeSpanContext.spanId;\n meta.trace_flags = activeSpanContext.traceFlags;\n meta = {\n // @ts-expect-error accessing private property\n ...activeSpan.attributes,\n ...meta\n };\n }\n\n meta = {\n 'api.name': 'none',\n 'correlation.id': 'none',\n ...meta\n };\n\n this.pinoLogger[level](...filteredArgs);\n logs.getLogger(process.env.OTEL_SERVICE_NAME ?? 'unknown').emit({\n severityText: level,\n severityNumber: mapSeverity(level),\n body: this.prettyPrinter(filteredArgs),\n attributes: { ...this.meta, ...meta }\n });\n }\n\n error: LogFn = (\n msg: string | unknown | LoggerMeta,\n ...args: (string | unknown | LoggerMeta)[]\n ) => this.log('error', msg, ...args);\n\n info: LogFn = (\n msg: string | unknown | LoggerMeta,\n ...args: (string | unknown | LoggerMeta)[]\n ) => this.log('info', msg, ...args);\n\n debug: LogFn = (\n msg: string | unknown | LoggerMeta,\n ...args: (string | unknown | LoggerMeta)[]\n ) => this.log('debug', msg, ...args);\n warn: LogFn = (\n msg: string | unknown | LoggerMeta,\n ...args: (string | unknown | LoggerMeta)[]\n ) => this.log('warn', msg, ...args);\n\n trace: LogFn = (\n msg: string | unknown | LoggerMeta,\n ...args: (string | unknown | LoggerMeta)[]\n ) => this.log('trace', msg, ...args);\n\n child(meta: AnyValueMap = {}) {\n return new PinoLogger(this.pinoLogger.level, { ...this.meta, ...meta });\n }\n\n getBaseLogger() {\n return this.pinoLogger;\n }\n}\n\nexport function logger(level: LevelWithSilentOrString, meta: AnyValueMap = {}) {\n return new PinoLogger(level, meta);\n}\n","import {\n AnySchemaValidator,\n prettyPrintParseErrors,\n SchemaValidator\n} from '@forklaunch/validator';\nimport { ParsedQs } from 'qs';\nimport { hasSend } from '../../guards/hasSend';\nimport { isRequestShape } from '../../guards/isRequestShape';\nimport {\n ForklaunchNextFunction,\n ForklaunchRequest,\n ForklaunchResponse\n} from '../../types/apiDefinition.types';\nimport { ParamsDictionary } from '../../types/contractDetails.types';\n\n/**\n * Pre-handler function to parse and validate input.\n *\n * @template SV - A type that extends AnySchemaValidator.\n * @template Request - A type that extends ForklaunchRequest.\n * @template Response - A type that extends ForklaunchResponse.\n * @template NextFunction - A type that extends ForklaunchNextFunction.\n * @param {Request} req - The request object.\n * @param {Response} res - The response object.\n * @param {NextFunction} [next] - The next middleware function.\n */\nexport function parse<\n SV extends AnySchemaValidator,\n P extends ParamsDictionary,\n ResBodyMap extends Record<number, unknown>,\n ReqBody extends Record<string, unknown>,\n ReqQuery extends ParsedQs,\n ReqHeaders extends Record<string, string>,\n ResHeaders extends Record<string, string>,\n LocalsObj extends Record<string, unknown>\n>(\n req: ForklaunchRequest<SV, P, ReqBody, ReqQuery, ReqHeaders>,\n res: ForklaunchResponse<unknown, ResBodyMap, ResHeaders, LocalsObj>,\n next?: ForklaunchNextFunction\n) {\n const request = {\n params: req.params,\n query: req.query,\n headers: req.headers,\n body: req.body\n };\n\n const parsedRequest = (req.schemaValidator as SchemaValidator).parse(\n req.requestSchema,\n request\n );\n\n if (\n parsedRequest.ok &&\n isRequestShape<P, ReqHeaders, ReqQuery, ReqBody>(parsedRequest.value)\n ) {\n req.body = parsedRequest.value.body;\n req.params = parsedRequest.value.params;\n Object.defineProperty(req, 'query', {\n value: parsedRequest.value.query,\n writable: false,\n enumerable: true,\n configurable: false\n });\n req.headers = parsedRequest.value.headers ?? {};\n }\n if (!parsedRequest.ok) {\n switch (req.contractDetails.options?.requestValidation) {\n default:\n case 'error':\n res.type('application/json');\n res.status(400);\n if (hasSend(res)) {\n res.send(\n `${prettyPrintParseErrors(\n parsedRequest.errors,\n 'Request'\n )}\\n\\nCorrelation id: ${\n req.context.correlationId ?? 'No correlation ID'\n }`\n );\n } else {\n next?.(new Error('Request is not sendable.'));\n }\n return;\n case 'warning':\n req.openTelemetryCollector.warn(\n prettyPrintParseErrors(parsedRequest.errors, 'Request')\n );\n break;\n case 'none':\n break;\n }\n }\n\n next?.();\n}\n","import {\n AnySchemaValidator,\n prettyPrintParseErrors,\n SchemaValidator\n} from '@forklaunch/validator';\nimport { ParsedQs } from 'qs';\nimport { hasSend } from '../../guards/hasSend';\nimport {\n ForklaunchNextFunction,\n ForklaunchRequest,\n ForklaunchResHeaders,\n ForklaunchResponse\n} from '../../types/apiDefinition.types';\nimport { ParamsDictionary } from '../../types/contractDetails.types';\n\n/**\n * Middleware to parse and validate the response according to the provided schema.\n *\n * This function validates the response against a schema provided by the request's schema validator.\n * If the response does not conform to the schema, the behavior is determined by the `responseValidation`\n * option in `req.contractDetails.options`:\n * - `'error'` (default): Calls `next` with an error.\n * - `'warning'`: Logs a warning to the console.\n * - `'none'`: Silently continues without action.\n *\n * @template SV - The type of the schema validator used in the request.\n * @template P - The type of the parameters dictionary used in the request.\n * @template ResBodyMap - A record type mapping status codes to response body types.\n * @template ReqBody - The type of the request body.\n * @template ReqQuery - The type of the parsed query string.\n * @template ReqHeaders - The type of the request headers.\n * @template ResHeaders - The type of the response headers, extended from `ForklaunchResHeaders`.\n * @template LocalsObj - The type of the locals object in the response.\n *\n * @param {ForklaunchRequest<SV, P, ReqBody, ReqQuery, ReqHeaders>} req - The request object, containing the schema validator and other request data.\n * @param {ForklaunchResponse<ResBodyMap, ForklaunchResHeaders & ResHeaders, LocalsObj>} res - The response object, including headers and body data.\n * @param {ForklaunchNextFunction} [next] - The next middleware function to be called. If not provided, the function will return after processing.\n *\n * @returns {void} This function does not return a value.\n */\nexport function parse<\n SV extends AnySchemaValidator,\n P extends ParamsDictionary,\n ResBodyMap extends Record<number, unknown>,\n ReqBody extends Record<string, unknown>,\n ReqQuery extends ParsedQs,\n ReqHeaders extends Record<string, string>,\n ResHeaders extends Record<string, string>,\n LocalsObj extends Record<string, unknown>\n>(\n req: ForklaunchRequest<SV, P, ReqBody, ReqQuery, ReqHeaders>,\n res: ForklaunchResponse<\n unknown,\n ResBodyMap,\n ForklaunchResHeaders & ResHeaders,\n LocalsObj\n >,\n next?: ForklaunchNextFunction\n) {\n const { headers, responses } = res.responseSchemas;\n\n const statusCode = Number(res.statusCode);\n\n const parsedResponse = (req.schemaValidator as SchemaValidator).parse(\n responses?.[statusCode],\n res.bodyData\n );\n\n const parsedHeaders = (req.schemaValidator as SchemaValidator).parse(\n headers ?? req.schemaValidator.unknown,\n res.getHeaders()\n );\n const parseErrors: string[] = [];\n if (!parsedHeaders.ok) {\n const headerErrors = prettyPrintParseErrors(parsedHeaders.errors, 'Header');\n if (headerErrors) {\n parseErrors.push(headerErrors);\n }\n }\n\n if (!parsedResponse.ok) {\n const responseErrors = prettyPrintParseErrors(\n parsedResponse.errors,\n 'Response'\n );\n if (responseErrors) {\n parseErrors.push(responseErrors);\n }\n }\n\n if (parseErrors.length > 0) {\n switch (req.contractDetails.options?.responseValidation) {\n default:\n case 'error':\n res.type('text/plain');\n res.status(500);\n if (hasSend(res)) {\n res.send(\n `Invalid response:\\n${parseErrors.join(\n '\\n\\n'\n )}\\n\\nCorrelation id: ${\n req.context.correlationId ?? 'No correlation ID'\n }`\n );\n } else {\n next?.(new Error('Response is not sendable.'));\n }\n return;\n case 'warning':\n req.openTelemetryCollector.warn(\n `Invalid response:\\n${parseErrors.join('\\n\\n')}`\n );\n break;\n case 'none':\n break;\n }\n }\n next?.();\n}\n","import { AnySchemaValidator } from '@forklaunch/validator';\nimport { ParsedQs } from 'qs';\nimport { parse } from './parse.middleware';\n\nimport {\n isAsyncGenerator,\n isNever,\n isNodeJsWriteableStream,\n isRecord,\n readableStreamToAsyncIterable,\n safeStringify\n} from '@forklaunch/common';\nimport { Readable, Transform } from 'stream';\nimport { discriminateResponseBodies } from '../../router/discriminateBody';\nimport { logger } from '../../telemetry/pinoLogger';\nimport { recordMetric } from '../../telemetry/recordMetric';\nimport {\n ForklaunchRequest,\n ForklaunchResHeaders,\n ForklaunchResponse,\n ForklaunchSendableData,\n ForklaunchStatusResponse\n} from '../../types/apiDefinition.types';\nimport { ParamsDictionary } from '../../types/contractDetails.types';\n\n/**\n * Enhances the Express-like `send` method with additional logic for response handling and validation.\n *\n * This function intercepts the `send` method to provide custom behavior, including response validation\n * through the `parseResponse` middleware. If the response status is 404, it sends a \"Not Found\" message.\n * If the response validation fails, it sends a parsing error message. Otherwise, it calls the original `send`\n * method with the provided data.\n *\n * @template SV - The type of the schema validator used in the request.\n * @template P - The type of the parameters dictionary used in the request.\n * @template ResBodyMap - A record type mapping status codes to response body types.\n * @template ReqBody - The type of the request body.\n * @template ReqQuery - The type of the parsed query string.\n * @template ReqHeaders - The type of the request headers.\n * @template ResHeaders - The type of the response headers, extended from `ForklaunchResHeaders`.\n * @template LocalsObj - The type of the locals object in the response.\n *\n * @param {unknown} instance - The context (typically `this`) in which the original `send` method is called.\n * @param {ForklaunchRequest<SV, P, ReqBody, ReqQuery, ReqHeaders>} req - The request object, containing the schema validator and other request data.\n * @param {ForklaunchResponse<ResBodyMap, ForklaunchResHeaders & ResHeaders, LocalsObj>} res - The response object, including headers and body data.\n * @param {Function} originalSend - The original `send` method from the response object, to be called after custom logic.\n * @param {string | ArrayBuffer | ArrayBufferView | NodeJS.ReadableStream | null | undefined} data - The data to be sent as the response body.\n * @param {boolean} shouldEnrich - A flag indicating whether the response should be sent immediately.\n *\n * @returns {unknown} The return value of the original `send` method, typically the response itself.\n */\nexport function enrichExpressLikeSend<\n SV extends AnySchemaValidator,\n P extends ParamsDictionary,\n ResBodyMap extends Record<number, unknown>,\n ReqBody extends Record<string, unknown>,\n ReqQuery extends ParsedQs,\n ReqHeaders extends Record<string, string>,\n ResHeaders extends Record<string, string>,\n LocalsObj extends Record<string, unknown>\n>(\n instance: unknown,\n req: ForklaunchRequest<SV, P, ReqBody, ReqQuery, ReqHeaders>,\n res: ForklaunchResponse<\n unknown,\n ResBodyMap,\n ForklaunchResHeaders & ResHeaders,\n LocalsObj\n >,\n originalOperation:\n | ForklaunchStatusResponse<ForklaunchSendableData>['json']\n | ForklaunchStatusResponse<ForklaunchSendableData>['jsonp'],\n originalSend: ForklaunchStatusResponse<ForklaunchSendableData>['send'],\n data:\n | ForklaunchSendableData\n | File\n | Blob\n | AsyncGenerator<Record<string, unknown>>\n | null\n | undefined,\n shouldEnrich: boolean\n) {\n let errorSent = false;\n\n if (data == null) {\n originalSend.call(instance, data);\n return;\n }\n\n if (res.statusCode === 404) {\n res.type('text/plain');\n res.status(404);\n logger('error').error('Not Found');\n originalSend.call(instance, 'Not Found');\n errorSent = true;\n }\n\n const responseBodies = discriminateResponseBodies(\n req.schemaValidator,\n req.contractDetails.responses\n );\n\n if (\n responseBodies != null &&\n responseBodies[Number(res.statusCode)] != null\n ) {\n res.type(responseBodies[Number(res.statusCode)].contentType);\n }\n\n if (data instanceof File || data instanceof Blob) {\n if (data instanceof File) {\n res.setHeader(\n 'Content-Disposition',\n `attachment; filename=\"${data.name}\"` as ResHeaders['Content-Disposition']\n );\n }\n if (isNodeJsWriteableStream(res)) {\n Readable.from(readableStreamToAsyncIterable(data.stream())).pipe(\n res as unknown as NodeJS.WritableStream\n );\n } else {\n res.type('text/plain');\n res.status(500);\n originalSend.call(instance, 'Not a NodeJS WritableStream');\n errorSent = true;\n }\n } else if (isAsyncGenerator(data)) {\n let firstPass = true;\n const transformer = new Transform({\n objectMode: true,\n transform(\n chunk: Record<string, unknown>,\n _encoding: unknown,\n callback: (error?: Error | null, data?: unknown) => void\n ) {\n if (firstPass) {\n res.bodyData = chunk;\n parse(req, res, (err?: unknown) => {\n if (err) {\n let errorString = (err as Error).message;\n if (res.locals.errorMessage) {\n errorString += `\\n------------------\\n${res.locals.errorMessage}`;\n }\n logger('error').error(errorString);\n res.type('text/plain');\n res.status(500);\n originalSend.call(instance, errorString);\n errorSent = true;\n callback(new Error(errorString));\n }\n });\n firstPass = false;\n }\n if (!errorSent) {\n let data = '';\n for (const [key, value] of Object.entries(chunk)) {\n data += `${key}: ${typeof value === 'string' ? value : safeStringify(value)}\\n`;\n }\n data += '\\n';\n callback(null, data);\n }\n }\n });\n if (isNodeJsWriteableStream(res)) {\n Readable.from(data).pipe(transformer).pipe(res);\n } else {\n res.type('text/plain');\n res.status(500);\n originalSend.call(instance, 'Not a NodeJS WritableStream');\n errorSent = true;\n }\n } else {\n const parserType = responseBodies?.[Number(res.statusCode)]?.parserType;\n\n res.bodyData = data;\n if (isRecord(data)) {\n switch (parserType) {\n case 'json':\n res.bodyData = 'json' in data ? data.json : data;\n break;\n case 'text':\n res.bodyData = 'text' in data ? data.text : data;\n break;\n case 'file':\n res.bodyData = 'file' in data ? data.file : data;\n break;\n case 'serverSentEvent':\n res.bodyData = 'event' in data ? data.event : data;\n break;\n case 'multipart':\n res.bodyData = 'multipart' in data ? data.multipart : data;\n break;\n case undefined:\n res.bodyData = data;\n break;\n default:\n isNever(parserType);\n res.bodyData = data;\n break;\n }\n }\n\n parse(req, res, (err?: unknown) => {\n if (err) {\n let errorString = (err as Error).message;\n if (res.locals.errorMessage) {\n errorString += `\\n------------------\\n${res.locals.errorMessage}`;\n }\n logger('error').error(errorString);\n res.type('text/plain');\n res.status(500);\n originalSend.call(instance, errorString);\n errorSent = true;\n }\n });\n\n if (!errorSent) {\n if (typeof data === 'string') {\n res.type('text/plain');\n originalSend.call(instance, data);\n } else if (!(data instanceof File)) {\n res.sent = true;\n originalOperation.call(instance, data);\n }\n }\n }\n\n if (shouldEnrich) {\n recordMetric<\n SV,\n P,\n ReqBody,\n ReqQuery,\n ResBodyMap,\n ReqHeaders,\n ForklaunchResHeaders & ResHeaders,\n LocalsObj\n >(req, res);\n }\n}\n","import { AnySchemaValidator } from '@forklaunch/validator';\nimport {\n ATTR_HTTP_REQUEST_METHOD,\n ATTR_HTTP_RESPONSE_STATUS_CODE,\n ATTR_HTTP_ROUTE,\n ATTR_SERVICE_NAME\n} from '@opentelemetry/semantic-conventions';\nimport { ParsedQs } from 'qs';\nimport { ForklaunchRequest, ForklaunchResponse, ParamsDictionary } from '..';\nimport { getEnvVar } from '../../services';\nimport { ATTR_API_NAME, ATTR_CORRELATION_ID } from './constants';\nimport { httpRequestsTotalCounter } from './openTelemetryCollector';\n\nexport function recordMetric<\n SV extends AnySchemaValidator,\n P extends ParamsDictionary,\n ReqBody extends Record<string, unknown>,\n ReqQuery extends ParsedQs,\n ResBodyMap extends Record<string, unknown>,\n ReqHeaders extends Record<string, string>,\n ResHeaders extends Record<string, string>,\n LocalsObj extends Record<string, unknown>\n>(\n req: ForklaunchRequest<SV, P, ReqBody, ReqQuery, ReqHeaders>,\n res: ForklaunchResponse<unknown, ResBodyMap, ResHeaders, LocalsObj>\n) {\n if (res.metricRecorded) {\n return;\n }\n\n httpRequestsTotalCounter.add(1, {\n [ATTR_SERVICE_NAME]: getEnvVar('OTEL_SERVICE_NAME'),\n [ATTR_API_NAME]: req.contractDetails?.name,\n [ATTR_CORRELATION_ID]: req.context.correlationId,\n [ATTR_HTTP_REQUEST_METHOD]: req.method,\n [ATTR_HTTP_ROUTE]: req.originalPath,\n [ATTR_HTTP_RESPONSE_STATUS_CODE]: Number(res.statusCode) || 0\n });\n res.metricRecorded = true;\n}\n","import { extractArgumentNames, isNever } from '@forklaunch/common';\nimport {\n AnySchemaValidator,\n IdiomaticSchema,\n ParseResult,\n prettyPrintParseErrors,\n SchemaValidator\n} from '@forklaunch/validator';\nimport { isConstructed } from './guards/isConstructed';\nimport { isConstructedSingleton } from './guards/isConstructedSingleton';\nimport { isConstructor } from './guards/isConstructor';\nimport {\n ConfigValidator,\n Constructed,\n ConstructedSingleton,\n Lifetime,\n ResolvedConfigValidator,\n SchemaConstructor,\n SchemaFunction,\n Singleton\n} from './types/configInjector.types';\n\nexport function createConfigInjector<\n SV extends AnySchemaValidator,\n CV extends ConfigValidator<SV>\n>(\n schemaValidator: SV,\n dependenciesDefinition: {\n [K in keyof CV]:\n | Singleton<\n CV[K],\n Omit<ResolvedConfigValidator<SV, CV>, K>,\n ResolvedConfigValidator<SV, CV>[K]\n >\n | Constructed<\n CV[K],\n Omit<ResolvedConfigValidator<SV, CV>, K>,\n ResolvedConfigValidator<SV, CV>[K]\n >;\n }\n) {\n return new ConfigInjector<SV, CV>(\n schemaValidator,\n dependenciesDefinition\n ).load();\n}\n\nexport class ConfigInjector<\n SV extends AnySchemaValidator,\n CV extends ConfigValidator<SV>\n> {\n instances: {\n [K in keyof CV]?: ResolvedConfigValidator<SV, CV>[K];\n } = {};\n\n readonly configShapes: CV;\n\n load(inheritedScopeInstances?: {\n [K in keyof CV]?: ResolvedConfigValidator<SV, CV>[K];\n }): this {\n for (const token in inheritedScopeInstances) {\n this.instances[token] = inheritedScopeInstances[token];\n }\n\n for (const token in this.dependenciesDefinition) {\n const definition = this.dependenciesDefinition[token];\n if (\n definition.lifetime === Lifetime.Singleton &&\n !this.instances[token]\n ) {\n if (\n isConstructedSingleton<\n CV[typeof token],\n Omit<ResolvedConfigValidator<SV, CV>, typeof token>,\n ResolvedConfigValidator<SV, CV>[typeof token]\n >(definition)\n ) {\n this.instances[token] = this.resolveInstance<typeof token>(\n token,\n definition\n );\n } else {\n this.instances[token] = definition.value;\n }\n }\n }\n return this;\n }\n\n private resolveInstance<T extends keyof CV>(\n token: T,\n definition:\n | ConstructedSingleton<\n CV[T],\n Omit<ResolvedConfigValidator<SV, CV>, T>,\n ResolvedConfigValidator<SV, CV>[T]\n >\n | Constructed<\n CV[T],\n Omit<ResolvedConfigValidator<SV, CV>, T>,\n ResolvedConfigValidator<SV, CV>[T]\n >,\n context?: Record<string, unknown>,\n resolutionPath: (keyof CV)[] = []\n ): ResolvedConfigValidator<SV, CV>[T] {\n const injectorArgument = extractArgumentNames(definition.factory)[0];\n // short circuit as no args\n if (!injectorArgument || injectorArgument === '_args') {\n return definition.factory(\n {} as Omit<ResolvedConfigValidator<SV, CV>, T>,\n this.resolve.bind(this),\n context ?? ({} as Record<string, unknown>)\n );\n }\n\n if (!injectorArgument.startsWith('{') || !injectorArgument.endsWith('}')) {\n throw new Error(\n `Invalid injector argument for ${String(\n token\n )}: ${injectorArgument}. Please use object destructuring syntax: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment.`\n );\n }\n const resolvedArguments = Object.fromEntries(\n injectorArgument\n .replace('{', '')\n .replace('}', '')\n .split(',')\n .map((arg) => arg.split(':')[0].trim())\n .map((arg) => {\n const newResolutionPath = [...resolutionPath, token];\n if (resolutionPath.includes(arg)) {\n throw new Error(\n `Circular dependency detected: ${newResolutionPath.join(\n ' -> '\n )} -> ${arg}`\n );\n }\n const resolvedArg = this.resolve(arg, context, newResolutionPath);\n return [arg, resolvedArg];\n })\n ) as unknown as Omit<ResolvedConfigValidator<SV, CV>, T>;\n return definition.factory(\n resolvedArguments,\n this.resolve.bind(this),\n context ?? ({} as Record<string, unknown>)\n );\n }\n\n constructor(\n private schemaValidator: SV,\n private dependenciesDefinition: {\n [K in keyof CV]: (\n | Singleton<\n CV[K],\n Omit<ResolvedConfigValidator<SV, CV>, K>,\n ResolvedConfigValidator<SV, CV>[K]\n >\n | Constructed<\n CV[K],\n Omit<ResolvedConfigValidator<SV, CV>, K>,\n ResolvedConfigValidator<SV, CV>[K]\n >\n ) & {\n type: CV[K];\n };\n }\n ) {\n this.configShapes = Object.entries(this.dependenciesDefinition).reduce(\n (acc, [key, { type }]) => ({\n ...acc,\n [key]: type\n }),\n {} as Record<keyof CV, CV[keyof CV]>\n ) as CV;\n }\n\n safeValidateConfigSingletons(): ParseResult<ValidConfigInjector<SV, CV>> {\n const validNonSchemaSingletons = Object.entries(this.configShapes).reduce<\n ParseResult<ResolvedConfigValidator<SV, CV>>\n >(\n (acc, [key, value]) => {\n if (\n this.dependenciesDefinition[key].lifetime === Lifetime.Singleton &&\n !(this.schemaValidator as SchemaValidator).isSchema<\n SchemaFunction<SV> | SchemaConstructor<SV> | IdiomaticSchema<SV>\n >(value) &&\n isConstructor(value)\n ) {\n if (!(this.instances[key] instanceof value)) {\n const expected = value.name;\n const receivedValue: unknown = this.instances[key];\n const received = isConstructed(receivedValue)\n ? receivedValue.constructor.name\n : typeof receivedValue;\n\n if (acc.ok) {\n acc = {\n ok: false,\n errors: []\n };\n }\n acc.errors?.push({\n message: `Expected ${expected}, received ${received}`,\n path: [key]\n });\n } else {\n if (acc.ok) {\n acc = {\n ok: true,\n value: {\n ...acc.value,\n [key]: this.instances[key]\n }\n };\n }\n }\n return acc;\n }\n return acc;\n },\n {\n ok: true,\n value: {} as ResolvedConfigValidator<SV, CV>\n }\n );\n\n const singletons = Object.fromEntries(\n Object.entries(this.configShapes).filter(\n ([key, value]) =>\n this.dependenciesDefinition[key].lifetime === Lifetime.Singleton &&\n (this.schemaValidator as SchemaValidator).isSchema(value)\n )\n );\n const schemaSingletonParseResult = (\n this.schemaValidator as SchemaValidator\n ).parse(\n (this.schemaValidator as SchemaValidator).schemify(singletons),\n Object.fromEntries(\n Object.keys(singletons).map((key) => {\n const dependency = this.dependenciesDefinition[key];\n return [\n key,\n dependency.lifetime === Lifetime.Singleton\n ? this.instances[key]\n : undefined\n ];\n })\n )\n );\n\n const configKeys = Object.keys(this.configShapes);\n\n return validNonSchemaSingletons.ok && schemaSingletonParseResult.ok\n ? {\n ok: true as const,\n value: new ValidConfigInjector<SV, CV>(\n this.schemaValidator,\n this.dependenciesDefinition\n ).load({ ...this.instances })\n }\n : {\n ok: false as const,\n errors: [\n ...(!validNonSchemaSingletons.ok && validNonSchemaSingletons.errors\n ? validNonSchemaSingletons.errors\n : []),\n ...(!schemaSingletonParseResult.ok &&\n schemaSingletonParseResult.errors\n ? schemaSingletonParseResult.errors\n : [])\n ].sort(\n (a, b) =>\n configKeys.indexOf(a.path[0]) - configKeys.indexOf(b.path[0])\n )\n };\n }\n\n validateConfigSingletons(configName: string): ValidConfigInjector<SV, CV> {\n const safeValidateResult = this.safeValidateConfigSingletons();\n\n if (safeValidateResult.ok) {\n return safeValidateResult.value;\n }\n\n throw new Error(\n prettyPrintParseErrors(safeValidateResult.errors, configName)\n );\n }\n\n resolve<T extends keyof CV>(\n token: T,\n context?: Record<string, unknown>,\n resolutionPath: (keyof CV)[] = []\n ): ResolvedConfigValidator<SV, CV>[T] {\n const instance = this.instances[token];\n if (!instance) {\n const definition = this.dependenciesDefinition[token];\n\n if (!definition) {\n throw new Error(`Unable to resolve dependency ${String(token)}`);\n }\n\n switch (definition.lifetime) {\n case Lifetime.Singleton: {\n if (\n isConstructedSingleton<\n CV[T],\n Omit<ResolvedConfigValidator<SV, CV>, T>,\n ResolvedConfigValidator<SV, CV>[T]\n >(definition) &&\n !this.instances[token]\n ) {\n this.instances[token] = this.resolveInstance<T>(\n token,\n definition,\n context,\n resolutionPath\n );\n }\n return this.instances[token] as ResolvedConfigValidator<SV, CV>[T];\n }\n case Lifetime.Scoped: {\n if (\n !isConstructed<\n CV[T],\n Omit<ResolvedConfigValidator<SV, CV>, T>,\n ResolvedConfigValidator<SV, CV>[T]\n >(definition)\n ) {\n throw new Error(\n `Invalid dependency definition for ${String(token)}`\n );\n }\n\n const scopedInstance = this.resolveInstance<T>(\n token,\n definition,\n context,\n resolutionPath\n );\n this.instances[token] = scopedInstance;\n return scopedInstance;\n }\n case Lifetime.Transient: {\n if (\n !isConstructed<\n CV[T],\n Omit<ResolvedConfigValidator<SV, CV>, T>,\n ResolvedConfigValidator<SV, CV>[T]\n >(definition)\n ) {\n throw new Error(\n `Invalid dependency definition for ${String(token)}`\n );\n }\n\n return this.resolveInstance<T>(\n token,\n definition,\n context,\n resolutionPath\n );\n }\n default: {\n isNever(definition);\n throw new Error(\n `Unable to resolve lifetime for dependency ${String(\n token\n )}, ${resolutionPath}`\n );\n }\n }\n } else {\n return instance;\n }\n }\n\n scopedResolver<T extends keyof CV>(\n token: T,\n context?: Record<string, unknown>,\n resolutionPath: (keyof CV)[] = []\n ): (scope?: ConfigInjector<SV, CV>) => ResolvedConfigValidator<SV, CV>[T] {\n return (scope) =>\n (scope ?? this.createScope()).resolve<T>(token, context, resolutionPath);\n }\n\n createScope(): ConfigInjector<SV, CV> {\n const singletons: Record<string, unknown> = {};\n for (const dependency in this.dependenciesDefinition) {\n if (\n this.dependenciesDefinition[dependency].lifetime === Lifetime.Singleton\n ) {\n singletons[dependency] = this.instances[dependency];\n }\n }\n return new ConfigInjector<SV, CV>(\n this.schemaValidator,\n this.dependenciesDefinition\n ).load(singletons as ResolvedConfigValidator<SV, CV>);\n }\n\n dispose(): void {\n this.instances = {};\n this.load();\n }\n\n chain<ChainedCV extends ConfigValidator<SV>>(dependenciesDefinition: {\n [K in keyof ChainedCV]: {\n type: ChainedCV[K];\n } & (\n | Singleton<\n ChainedCV[K],\n Omit<ResolvedConfigValidator<SV, CV & ChainedCV>, K>,\n ResolvedConfigValidator<SV, ChainedCV>[K]\n >\n | Constructed<\n ChainedCV[K],\n Omit<ResolvedConfigValidator<SV, CV & ChainedCV>, K>,\n ResolvedConfigValidator<SV, ChainedCV>[K]\n >\n );\n }): ConfigInjector<SV, CV & ChainedCV> {\n return new ConfigInjector<SV, CV>(this.schemaValidator, {\n ...this.dependenciesDefinition,\n ...dependenciesDefinition\n }).load({ ...this.instances }) as unknown as ConfigInjector<\n SV,\n CV & ChainedCV\n >;\n }\n\n tokens(): {\n [K in keyof CV]: K;\n } {\n return Object.fromEntries(\n Object.keys(this.dependenciesDefinition).map((key) => [key, key])\n ) as {\n [K in keyof CV]: K;\n };\n }\n}\n\nexport class ValidConfigInjector<\n SV extends AnySchemaValidator,\n CV extends ConfigValidator<SV>\n> extends ConfigInjector<SV, CV> {\n validConfigInjector!: void;\n}\n","import { TelemetryOptions } from '../types/openTelemetryCollector.types';\n\nexport function evaluateTelemetryOptions(telemetryOptions: TelemetryOptions) {\n return {\n enabled:\n typeof telemetryOptions.enabled === 'boolean'\n ? {\n metrics: telemetryOptions.enabled,\n tracing: telemetryOptions.enabled,\n logging: telemetryOptions.enabled\n }\n : {\n metrics: telemetryOptions.enabled.metrics,\n tracing: telemetryOptions.enabled.tracing,\n logging: telemetryOptions.enabled.logging\n },\n level: telemetryOptions.level\n };\n}\n"],"mappings":"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","names":["import_common","import_api","import_semantic_conventions","import_uuid","import_common","import_api","dotenv","import_validator","import_common","import_semantic_conventions","import_common","import_validator"]}
@@ -4,6 +4,7 @@ var createCacheKey = (cacheKeyPrefix) => (id) => {
4
4
  };
5
5
 
6
6
  // src/cache/redisTtlCache.ts
7
+ import { safeParse, safeStringify as safeStringify3 } from "@forklaunch/common";
7
8
  import { createClient } from "redis";
8
9
 
9
10
  // src/http/middleware/request/cors.middleware.ts
@@ -121,6 +122,16 @@ import {
121
122
  prettyPrintParseErrors as prettyPrintParseErrors2
122
123
  } from "@forklaunch/validator";
123
124
 
125
+ // src/http/middleware/response/enrichExpressLikeSend.middleware.ts
126
+ import {
127
+ isAsyncGenerator,
128
+ isNever as isNever3,
129
+ isNodeJsWriteableStream,
130
+ isRecord,
131
+ readableStreamToAsyncIterable,
132
+ safeStringify as safeStringify2
133
+ } from "@forklaunch/common";
134
+
124
135
  // src/http/telemetry/recordMetric.ts
125
136
  import {
126
137
  ATTR_HTTP_REQUEST_METHOD as ATTR_HTTP_REQUEST_METHOD3,
@@ -194,7 +205,7 @@ var RedisTtlCache = class {
194
205
  switch (typeof value) {
195
206
  case "object":
196
207
  case "string":
197
- return JSON.parse(value);
208
+ return safeParse(value);
198
209
  case "number":
199
210
  return value;
200
211
  }
@@ -217,7 +228,7 @@ var RedisTtlCache = class {
217
228
  if (this.telemetryOptions.enabled.logging) {
218
229
  this.openTelemetryCollector.info(`Putting record into cache: ${key}`);
219
230
  }
220
- await this.client.set(key, JSON.stringify(value), {
231
+ await this.client.set(key, safeStringify3(value), {
221
232
  PX: ttlMilliseconds
222
233
  });
223
234
  }
@@ -229,9 +240,9 @@ var RedisTtlCache = class {
229
240
  * @returns {Promise<void>} A promise that resolves when all values are cached
230
241
  */
231
242
  async putBatchRecords(cacheRecords) {
232
- let multiCommand = this.client.multi();
243
+ const multiCommand = this.client.multi();
233
244
  for (const { key, value, ttlMilliseconds } of cacheRecords) {
234
- multiCommand = multiCommand.set(key, JSON.stringify(value), {
245
+ multiCommand.set(key, safeStringify3(value), {
235
246
  PX: ttlMilliseconds || this.ttlMilliseconds
236
247
  });
237
248
  }
@@ -246,7 +257,7 @@ var RedisTtlCache = class {
246
257
  * @returns {Promise<void>} A promise that resolves when the value is enqueued
247
258
  */
248
259
  async enqueueRecord(queueName, value) {
249
- await this.client.lPush(queueName, JSON.stringify(value));
260
+ await this.client.lPush(queueName, safeStringify3(value));
250
261
  }
251
262
  /**
252
263
  * Adds multiple values to the left end of a Redis list in a single transaction.
@@ -257,9 +268,9 @@ var RedisTtlCache = class {
257
268
  * @returns {Promise<void>} A promise that resolves when all values are enqueued
258
269
  */
259
270
  async enqueueBatchRecords(queueName, values) {
260
- let multiCommand = this.client.multi();
271
+ const multiCommand = this.client.multi();
261
272
  for (const value of values) {
262
- multiCommand = multiCommand.lPush(queueName, JSON.stringify(value));
273
+ multiCommand.lPush(queueName, safeStringify3(value));
263
274
  }
264
275
  await multiCommand.exec();
265
276
  }
@@ -279,9 +290,9 @@ var RedisTtlCache = class {
279
290
  * @returns {Promise<void>} A promise that resolves when all records are deleted
280
291
  */
281
292
  async deleteBatchRecords(cacheRecordKeys) {
282
- let multiCommand = this.client.multi();
293
+ const multiCommand = this.client.multi();
283
294
  for (const key of cacheRecordKeys) {
284
- multiCommand = multiCommand.del(key);
295
+ multiCommand.del(key);
285
296
  }
286
297
  await multiCommand.exec();
287
298
  }
@@ -298,7 +309,7 @@ var RedisTtlCache = class {
298
309
  if (value === null) {
299
310
  throw new Error(`Queue is empty: ${queueName}`);
300
311
  }
301
- return JSON.parse(value);
312
+ return safeParse(value);
302
313
  }
303
314
  /**
304
315
  * Removes and returns multiple elements from the right end of a Redis list.
@@ -309,12 +320,14 @@ var RedisTtlCache = class {
309
320
  * @returns {Promise<T[]>} A promise that resolves with an array of dequeued values
310
321
  */
311
322
  async dequeueBatchRecords(queueName, pageSize) {
312
- let multiCommand = this.client.multi();
323
+ const multiCommand = this.client.multi();
313
324
  for (let i = 0; i < pageSize; i++) {
314
- multiCommand = multiCommand.rPop(queueName);
325
+ multiCommand.rPop(queueName);
315
326
  }
316
327
  const values = await multiCommand.exec();
317
- return values.map((value) => this.parseValue(value)).filter(Boolean);
328
+ return values.map(
329
+ (value) => this.parseValue(value)
330
+ ).filter(Boolean);
318
331
  }
319
332
  /**
320
333
  * Reads a record from the Redis cache.
@@ -344,16 +357,20 @@ var RedisTtlCache = class {
344
357
  */
345
358
  async readBatchRecords(cacheRecordKeysOrPrefix) {
346
359
  const keys = Array.isArray(cacheRecordKeysOrPrefix) ? cacheRecordKeysOrPrefix : await this.client.keys(cacheRecordKeysOrPrefix + "*");
347
- let multiCommand = this.client.multi();
360
+ const multiCommand = this.client.multi();
348
361
  for (const key of keys) {
349
- multiCommand = multiCommand.get(key);
350
- multiCommand = multiCommand.ttl(key);
362
+ multiCommand.get(key);
363
+ multiCommand.ttl(key);
351
364
  }
352
365
  const values = await multiCommand.exec();
353
366
  return values.reduce((acc, value, index) => {
354
367
  if (index % 2 === 0) {
355
- const maybeValue = this.parseValue(value);
356
- const ttl = this.parseValue(values[index + 1]);
368
+ const maybeValue = this.parseValue(
369
+ value
370
+ );
371
+ const ttl = this.parseValue(
372
+ values[index + 1]
373
+ );
357
374
  if (maybeValue && ttl) {
358
375
  acc.push({
359
376
  key: keys[index / 2],
@@ -393,9 +410,9 @@ var RedisTtlCache = class {
393
410
  */
394
411
  async peekBatchRecords(cacheRecordKeysOrPrefix) {
395
412
  const keys = Array.isArray(cacheRecordKeysOrPrefix) ? cacheRecordKeysOrPrefix : await this.client.keys(cacheRecordKeysOrPrefix + "*");
396
- let multiCommand = this.client.multi();
413
+ const multiCommand = this.client.multi();
397
414
  for (const key of keys) {
398
- multiCommand = multiCommand.exists(key);
415
+ multiCommand.exists(key);
399
416
  }
400
417
  const results = await multiCommand.exec();
401
418
  return results.map((result) => result === 1);