@geekmidas/constructs 10.0.0-alpha.106 → 10.0.0-alpha.108
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.
- package/dist/Construct-BQALFCh2.d.cts.map +1 -1
- package/dist/{Construct-B1vGBPme.mjs → Construct-CKUA-CTA.mjs} +3 -2
- package/dist/Construct-CKUA-CTA.mjs.map +1 -0
- package/dist/{Construct-DUSXuTHP.cjs → Construct-DEOVauyA.cjs} +3 -2
- package/dist/Construct-DEOVauyA.cjs.map +1 -0
- package/dist/Construct-QXnl3VqT.d.mts.map +1 -1
- package/dist/{Cron-uYAHPnp7.d.mts → Cron-CQJqqkhY.d.mts} +2 -2
- package/dist/{Cron-uYAHPnp7.d.mts.map → Cron-CQJqqkhY.d.mts.map} +1 -1
- package/dist/{CronBuilder-C0Ek1-RA.cjs → CronBuilder-BXTG10u5.cjs} +3 -3
- package/dist/{CronBuilder-C0Ek1-RA.cjs.map → CronBuilder-BXTG10u5.cjs.map} +1 -1
- package/dist/{CronBuilder-Dmix_Pen.mjs → CronBuilder-D3sF9tS_.mjs} +3 -3
- package/dist/{CronBuilder-Dmix_Pen.mjs.map → CronBuilder-D3sF9tS_.mjs.map} +1 -1
- package/dist/{CronBuilder-DaSUm2Na.d.mts → CronBuilder-Xx_zakuc.d.mts} +4 -4
- package/dist/{CronBuilder-DaSUm2Na.d.mts.map → CronBuilder-Xx_zakuc.d.mts.map} +1 -1
- package/dist/Endpoint-BWyKCa2x.d.cts.map +1 -1
- package/dist/{Endpoint-C3r2OzOD.d.mts → Endpoint-CrE9Ppdk.d.mts} +6 -6
- package/dist/{Endpoint-C3r2OzOD.d.mts.map → Endpoint-CrE9Ppdk.d.mts.map} +1 -1
- package/dist/{Endpoint-BW-nFdL_.mjs → Endpoint-gPXhHKhJ.mjs} +11 -4
- package/dist/Endpoint-gPXhHKhJ.mjs.map +1 -0
- package/dist/{Endpoint-B_UHlLxv.cjs → Endpoint-yhR2T2Wt.cjs} +11 -4
- package/dist/Endpoint-yhR2T2Wt.cjs.map +1 -0
- package/dist/{EndpointFactory-C9eVhDKF.d.mts → EndpointFactory-BV-nfnaP.d.mts} +5 -5
- package/dist/{EndpointFactory-C9eVhDKF.d.mts.map → EndpointFactory-BV-nfnaP.d.mts.map} +1 -1
- package/dist/{EndpointFactory-DeZTvDjU.cjs → EndpointFactory-BVv6EIDu.cjs} +4 -4
- package/dist/{EndpointFactory-DeZTvDjU.cjs.map → EndpointFactory-BVv6EIDu.cjs.map} +1 -1
- package/dist/{EndpointFactory-Bbmx4vXc.mjs → EndpointFactory-Cs8IHvwF.mjs} +4 -4
- package/dist/{EndpointFactory-Bbmx4vXc.mjs.map → EndpointFactory-Cs8IHvwF.mjs.map} +1 -1
- package/dist/{HonoEndpointAdaptor-eCKyEOl_.cjs → HonoEndpointAdaptor-CN1jjUBJ.cjs} +11 -13
- package/dist/HonoEndpointAdaptor-CN1jjUBJ.cjs.map +1 -0
- package/dist/{HonoEndpointAdaptor-RSvRnXSl.mjs → HonoEndpointAdaptor-euH1oSdY.mjs} +7 -9
- package/dist/HonoEndpointAdaptor-euH1oSdY.mjs.map +1 -0
- package/dist/{Queue-A3wfD0it.cjs → Queue-BhuofM3k.cjs} +2 -2
- package/dist/{Queue-A3wfD0it.cjs.map → Queue-BhuofM3k.cjs.map} +1 -1
- package/dist/{Queue-D6USGS4q.mjs → Queue-cmhqEBOB.mjs} +2 -2
- package/dist/{Queue-D6USGS4q.mjs.map → Queue-cmhqEBOB.mjs.map} +1 -1
- package/dist/{QueueBuilder-QHsnVt1P.d.mts → QueueBuilder-Cx9lKC5d.d.mts} +2 -2
- package/dist/{QueueBuilder-QHsnVt1P.d.mts.map → QueueBuilder-Cx9lKC5d.d.mts.map} +1 -1
- package/dist/{QueueBuilder-Cue73M4t.mjs → QueueBuilder-DXu1qxT1.mjs} +2 -2
- package/dist/{QueueBuilder-Cue73M4t.mjs.map → QueueBuilder-DXu1qxT1.mjs.map} +1 -1
- package/dist/{QueueBuilder-BjuF0UBl.cjs → QueueBuilder-DdU2klPu.cjs} +2 -2
- package/dist/{QueueBuilder-BjuF0UBl.cjs.map → QueueBuilder-DdU2klPu.cjs.map} +1 -1
- package/dist/{SubscriberBuilder-CuCMFpYW.d.mts → SubscriberBuilder-4DWe9feX.d.mts} +2 -2
- package/dist/{SubscriberBuilder-CuCMFpYW.d.mts.map → SubscriberBuilder-4DWe9feX.d.mts.map} +1 -1
- package/dist/{SubscriberBuilder-CDy4nHCz.mjs → SubscriberBuilder-DRXfxWn_.mjs} +2 -2
- package/dist/{SubscriberBuilder-CDy4nHCz.mjs.map → SubscriberBuilder-DRXfxWn_.mjs.map} +1 -1
- package/dist/{SubscriberBuilder-D_owVoHo.cjs → SubscriberBuilder-M2EAgQhC.cjs} +2 -2
- package/dist/{SubscriberBuilder-D_owVoHo.cjs.map → SubscriberBuilder-M2EAgQhC.cjs.map} +1 -1
- package/dist/{Topic-D74b5cgs.mjs → Topic-D2R0r24C.mjs} +2 -2
- package/dist/{Topic-D74b5cgs.mjs.map → Topic-D2R0r24C.mjs.map} +1 -1
- package/dist/{Topic-GHxcU8AG.cjs → Topic-DwS3QO0m.cjs} +2 -2
- package/dist/{Topic-GHxcU8AG.cjs.map → Topic-DwS3QO0m.cjs.map} +1 -1
- package/dist/adaptors/aws.cjs +19 -15
- package/dist/adaptors/aws.cjs.map +1 -1
- package/dist/adaptors/aws.d.cts +2 -0
- package/dist/adaptors/aws.d.cts.map +1 -1
- package/dist/adaptors/aws.d.mts +5 -3
- package/dist/adaptors/aws.d.mts.map +1 -1
- package/dist/adaptors/aws.mjs +17 -13
- package/dist/adaptors/aws.mjs.map +1 -1
- package/dist/adaptors/hono.cjs +1 -1
- package/dist/adaptors/hono.d.cts.map +1 -1
- package/dist/adaptors/hono.d.mts +2 -2
- package/dist/adaptors/hono.d.mts.map +1 -1
- package/dist/adaptors/hono.mjs +1 -1
- package/dist/adaptors/testing.cjs +13 -14
- package/dist/adaptors/testing.cjs.map +1 -1
- package/dist/adaptors/testing.d.cts.map +1 -1
- package/dist/adaptors/testing.d.mts +4 -4
- package/dist/adaptors/testing.d.mts.map +1 -1
- package/dist/adaptors/testing.mjs +11 -12
- package/dist/adaptors/testing.mjs.map +1 -1
- package/dist/{auth-CcuceLYo.d.mts → auth-D_0788uW.d.mts} +4 -4
- package/dist/{auth-CcuceLYo.d.mts.map → auth-D_0788uW.d.mts.map} +1 -1
- package/dist/auth.cjs +1 -1
- package/dist/auth.d.mts +1 -1
- package/dist/auth.mjs +1 -1
- package/dist/{cache-CXh6GIgb.d.mts → cache-5QZzh5Er.d.mts} +2 -2
- package/dist/{cache-CXh6GIgb.d.mts.map → cache-5QZzh5Er.d.mts.map} +1 -1
- package/dist/cache.d.mts +1 -1
- package/dist/{construct-interface-DluK3_da.d.mts → construct-interface-ZvYQbivO.d.mts} +2 -2
- package/dist/{construct-interface-DluK3_da.d.mts.map → construct-interface-ZvYQbivO.d.mts.map} +1 -1
- package/dist/construct-interface.d.mts +1 -1
- package/dist/credential.d.mts +1 -1
- package/dist/crons/index.cjs +3 -3
- package/dist/crons/index.d.mts +2 -2
- package/dist/crons/index.mjs +3 -3
- package/dist/database/kysely.cjs +1 -1
- package/dist/database/kysely.d.mts +1 -1
- package/dist/database/kysely.mjs +1 -1
- package/dist/email.d.mts +2 -2
- package/dist/encryption.d.mts +1 -1
- package/dist/endpoints/index.cjs +3 -3
- package/dist/endpoints/index.d.mts +2 -2
- package/dist/endpoints/index.mjs +3 -3
- package/dist/external-api.cjs +1 -1
- package/dist/external-api.d.mts +1 -1
- package/dist/external-api.mjs +1 -1
- package/dist/{fake-BcHTPz7N.d.mts → fake-DqKo8INJ.d.mts} +2 -2
- package/dist/{fake-BcHTPz7N.d.mts.map → fake-DqKo8INJ.d.mts.map} +1 -1
- package/dist/file-server.d.mts +1 -1
- package/dist/functions/index.cjs +1 -1
- package/dist/functions/index.d.mts +1 -1
- package/dist/functions/index.mjs +1 -1
- package/dist/{functions-BFizikJU.mjs → functions-CTI-hDem.mjs} +2 -2
- package/dist/{functions-BFizikJU.mjs.map → functions-CTI-hDem.mjs.map} +1 -1
- package/dist/{functions-CfzJG9hK.cjs → functions-D7O5SGVc.cjs} +2 -2
- package/dist/{functions-CfzJG9hK.cjs.map → functions-D7O5SGVc.cjs.map} +1 -1
- package/dist/{index-CTkKOer4.d.mts → index-B13kQB_U.d.mts} +2 -2
- package/dist/{index-CTkKOer4.d.mts.map → index-B13kQB_U.d.mts.map} +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{kysely-CO_Dk43W.cjs → kysely-B3zfg9ti.cjs} +2 -2
- package/dist/{kysely-CO_Dk43W.cjs.map → kysely-B3zfg9ti.cjs.map} +1 -1
- package/dist/{kysely-Bii4l6eP.mjs → kysely-B_0BAr0T.mjs} +2 -2
- package/dist/{kysely-Bii4l6eP.mjs.map → kysely-B_0BAr0T.mjs.map} +1 -1
- package/dist/{kysely-DUqC3Ie0.d.mts → kysely-DKwnl0nf.d.mts} +4 -4
- package/dist/{kysely-DUqC3Ie0.d.mts.map → kysely-DKwnl0nf.d.mts.map} +1 -1
- package/dist/mobile-app.d.mts +1 -1
- package/dist/object-storage.d.mts +1 -1
- package/dist/queue/index.cjs +2 -2
- package/dist/queue/index.d.mts +1 -1
- package/dist/queue/index.mjs +2 -2
- package/dist/random-password.d.mts +1 -1
- package/dist/{rateLimit-BwHcMECR.cjs → requestLogger-BZcXwdBs.cjs} +33 -1
- package/dist/requestLogger-BZcXwdBs.cjs.map +1 -0
- package/dist/{rateLimit-DbOTsP01.mjs → requestLogger-pA4gi3E1.mjs} +28 -2
- package/dist/requestLogger-pA4gi3E1.mjs.map +1 -0
- package/dist/rest-api.cjs +1 -1
- package/dist/rest-api.d.mts +4 -4
- package/dist/rest-api.mjs +1 -1
- package/dist/site.d.mts +2 -2
- package/dist/subscribers/index.cjs +2 -2
- package/dist/subscribers/index.d.mts +1 -1
- package/dist/subscribers/index.mjs +2 -2
- package/dist/{telemetry-DRtT1_0A.d.mts → telemetry-BEoBn6GO.d.mts} +2 -2
- package/dist/{telemetry-DRtT1_0A.d.mts.map → telemetry-BEoBn6GO.d.mts.map} +1 -1
- package/dist/telemetry.d.mts +1 -1
- package/dist/topic/index.cjs +1 -1
- package/dist/topic/index.mjs +1 -1
- package/dist/{tracing-CxaZ98IB.mjs → tracing-Bbj1uoFJ.mjs} +19 -2
- package/dist/tracing-Bbj1uoFJ.mjs.map +1 -0
- package/dist/{tracing-DPMidF_r.cjs → tracing-C0ooU0V2.cjs} +24 -1
- package/dist/tracing-C0ooU0V2.cjs.map +1 -0
- package/dist/worker.cjs +4 -4
- package/dist/worker.d.mts +8 -8
- package/dist/worker.mjs +4 -4
- package/package.json +31 -31
- package/dist/Construct-B1vGBPme.mjs.map +0 -1
- package/dist/Construct-DUSXuTHP.cjs.map +0 -1
- package/dist/Endpoint-BW-nFdL_.mjs.map +0 -1
- package/dist/Endpoint-B_UHlLxv.cjs.map +0 -1
- package/dist/HonoEndpointAdaptor-RSvRnXSl.mjs.map +0 -1
- package/dist/HonoEndpointAdaptor-eCKyEOl_.cjs.map +0 -1
- package/dist/rateLimit-BwHcMECR.cjs.map +0 -1
- package/dist/rateLimit-DbOTsP01.mjs.map +0 -1
- package/dist/tracing-CxaZ98IB.mjs.map +0 -1
- package/dist/tracing-DPMidF_r.cjs.map +0 -1
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"HonoEndpointAdaptor-RSvRnXSl.mjs","names":["honoLogger"],"sources":["../src/endpoints/helpers.ts","../src/endpoints/parseHonoQuery.ts","../src/endpoints/HonoEndpointAdaptor.ts"],"sourcesContent":["import path from 'node:path';\nimport type { Service } from '@geekmidas/services';\nimport fg from 'fast-glob';\nimport type { HttpMethod } from '../types';\nimport { Endpoint } from './Endpoint';\n\n// Re-export utility functions\n\n/**\n * Recursively finds the project root directory by looking for lock files.\n * Traverses up the directory tree until it finds a package manager lock file.\n *\n * @param cwd - The current working directory to start searching from\n * @returns Promise resolving to the absolute path of the project root\n *\n * @example\n * ```typescript\n * const projectRoot = await getProjectRoot(process.cwd());\n * console.log(`Project root: ${projectRoot}`);\n * // Output: Project root: /Users/user/my-project\n * ```\n */\nexport async function getProjectRoot(cwd: string): Promise<string> {\n\tif (cwd === '/') {\n\t\treturn cwd;\n\t}\n\n\tconst stream = fg.stream(\n\t\t['yarn.lock', 'pnpm-lock.yaml', 'package-lock.json', 'deno.lock'],\n\t\t{ dot: true, cwd },\n\t);\n\n\tlet isRoot = false;\n\n\tfor await (const _ of stream) {\n\t\tisRoot = true;\n\t\tbreak;\n\t}\n\n\tif (isRoot) {\n\t\treturn cwd;\n\t}\n\n\treturn getProjectRoot(path.resolve(cwd, '..'));\n}\n\n/**\n * Discovers and imports all Endpoint instances from the specified route patterns.\n * Uses fast-glob to find files matching the patterns and extracts exported Endpoints.\n *\n * @template TServices - Array of service types used by the endpoints\n * @param routes - Array of glob patterns to match route files (e.g., ['src/routes/*.ts'])\n * @param cwd - The current working directory to resolve paths from\n * @returns Promise resolving to an array of Endpoint instances found in the matched files\n *\n * @example\n * ```typescript\n * // Find all endpoints in the routes directory\n * const endpoints = await getEndpointsFromRoutes(\n * ['src/routes/**\\/*.ts'],\n * process.cwd()\n * );\n *\n * // Register endpoints with your server\n * for (const endpoint of endpoints) {\n * server.register(endpoint);\n * }\n * ```\n *\n * @remarks\n * - Only exports that are valid Endpoint instances are included\n * - Files are imported dynamically, so they must be valid ES modules\n * - The function filters out non-Endpoint exports automatically\n */\nexport async function getEndpointsFromRoutes<TServices extends Service[]>(\n\troutes: string[],\n\tcwd: string,\n): Promise<Endpoint<string, HttpMethod, any, any, TServices>[]> {\n\tconst stream = fg.stream(routes, { cwd });\n\n\tconst endpoints: Endpoint<string, HttpMethod, any, any, TServices>[] = [];\n\n\tfor await (const f of stream) {\n\t\t// Resolve the absolute path for the route file\n\t\tconst routePath = path.resolve(cwd, f.toString());\n\t\t// Dynamically import the route module\n\t\tconst route = await import(routePath);\n\n\t\t// Filter exported values to find only Endpoint instances\n\t\tconst handlers = Object.values(route).filter((value) => {\n\t\t\treturn Endpoint.isEndpoint(value);\n\t\t}) as unknown as Endpoint<string, HttpMethod, any, any, TServices>[];\n\n\t\tendpoints.push(...handlers);\n\t}\n\n\treturn endpoints;\n}\n","import type { Context } from 'hono';\nimport qs from 'qs';\n\n/**\n * Parse Hono query parameters using qs bracket notation\n * Supports nested objects, arrays, and deep nesting via standard bracket syntax\n */\nexport function parseHonoQuery(c: Context): Record<string, any> {\n\tconst url = c.req.url;\n\tconst queryIndex = url.indexOf('?');\n\tif (queryIndex === -1) {\n\t\treturn {};\n\t}\n\tconst queryString = url.slice(queryIndex + 1);\n\treturn qs.parse(queryString) as Record<string, any>;\n}\n","import type { AuditableAction, AuditStorage } from '@geekmidas/audit';\nimport type { EnvironmentParser } from '@geekmidas/envkit';\nimport { type HttpError, isHttpError, wrapError } from '@geekmidas/errors';\nimport type { EventPublisher } from '@geekmidas/events';\nimport type { Logger } from '@geekmidas/logger';\nimport {\n\trunWithRequestContext,\n\ttype Service,\n\tServiceDiscovery,\n\ttype ServiceRecord,\n} from '@geekmidas/services';\nimport type { StandardSchemaV1 } from '@standard-schema/spec';\nimport { type Context, Hono } from 'hono';\nimport { setCookie } from 'hono/cookie';\nimport { logger as honoLogger } from 'hono/logger';\nimport { timing } from 'hono/timing';\nimport type {\n\tContentfulStatusCode,\n\tRedirectStatusCode,\n} from 'hono/utils/http-status';\nimport { validator } from 'hono/validator';\nimport { publishConstructEvents } from '../publisher';\nimport type { HttpMethod, LowerHttpMethod } from '../types';\nimport type { MappedAudit } from './audit';\nimport {\n\tEndpoint,\n\ttype EndpointContext,\n\ttype EndpointSchemas,\n\tResponseBuilder,\n} from './Endpoint';\nimport { getEndpointsFromRoutes } from './helpers';\nimport { createHonoCookies, createHonoHeaders } from './lazyAccessors';\nimport { loadRls } from './optionalPeers';\nimport { parseHonoQuery } from './parseHonoQuery';\nimport {\n\tcreateAuditContext,\n\texecuteWithAuditTransaction,\n} from './processAudits';\nimport { enforceRateLimit } from './rateLimit';\nimport { readRequestBody } from './readRequestBody';\n\nexport interface HonoEndpointOptions {\n\t/**\n\t * Path where OpenAPI documentation will be served.\n\t * Set to false to disable docs route.\n\t * @default '/docs'\n\t */\n\tdocsPath?: string | false;\n\t/**\n\t * OpenAPI schema options\n\t */\n\topenApiOptions?: {\n\t\ttitle?: string;\n\t\tversion?: string;\n\t\tdescription?: string;\n\t};\n}\n\n/**\n * Feature flags for an endpoint, analyzed once at route registration time.\n * This avoids per-request feature detection overhead.\n */\ninterface EndpointFeatures {\n\thasServices: boolean;\n\thasDatabase: boolean;\n\thasBodyValidation: boolean;\n\thasQueryValidation: boolean;\n\thasParamValidation: boolean;\n\thasAudits: boolean;\n\thasEvents: boolean;\n\thasRateLimit: boolean;\n\thasRls: boolean;\n}\n\n/**\n * Case-insensitive check for a `content-type` header in a metadata headers map.\n */\nfunction hasContentTypeHeader(headers: Record<string, string>): boolean {\n\tfor (const key of Object.keys(headers)) {\n\t\tif (key.toLowerCase() === 'content-type') return true;\n\t}\n\treturn false;\n}\n\n/**\n * Analyze endpoint features at registration time (not per-request)\n */\nfunction analyzeEndpointFeatures(\n\tendpoint: Endpoint<\n\t\tany,\n\t\tany,\n\t\tany,\n\t\tany,\n\t\tany,\n\t\tany,\n\t\tany,\n\t\tany,\n\t\tany,\n\t\tany,\n\t\tany,\n\t\tany\n\t>,\n): EndpointFeatures {\n\treturn {\n\t\thasServices: endpoint.services.length > 0,\n\t\thasDatabase: !!endpoint.databaseService,\n\t\thasBodyValidation: !!endpoint.input?.body,\n\t\thasQueryValidation: !!endpoint.input?.query,\n\t\thasParamValidation: !!endpoint.input?.params,\n\t\t// Audit context needed if declarative audits OR auditor storage service configured (for manual audits)\n\t\thasAudits:\n\t\t\t(endpoint.audits?.length ?? 0) > 0 || !!endpoint.auditorStorageService,\n\t\thasEvents: (endpoint.events?.length ?? 0) > 0,\n\t\thasRateLimit: !!endpoint.rateLimit,\n\t\thasRls: !!endpoint.rlsConfig && !endpoint.rlsBypass,\n\t};\n}\n\nexport class HonoEndpoint<\n\tTRoute extends string,\n\tTMethod extends HttpMethod,\n\tTInput extends EndpointSchemas = {},\n\tTOutSchema extends StandardSchemaV1 | undefined = undefined,\n\tTServices extends Service[] = [],\n\tTLogger extends Logger = Logger,\n\tTSession = unknown,\n\tTAuditStorage extends AuditStorage | undefined = undefined,\n\tTAuditStorageServiceName extends string = string,\n\tTAuditAction extends AuditableAction<string, unknown> = AuditableAction<\n\t\tstring,\n\t\tunknown\n\t>,\n\tTDatabase = undefined,\n\tTDatabaseServiceName extends string = string,\n> {\n\tconstructor(\n\t\tprivate readonly endpoint: Endpoint<\n\t\t\tTRoute,\n\t\t\tTMethod,\n\t\t\tTInput,\n\t\t\tTOutSchema,\n\t\t\tTServices,\n\t\t\tTLogger,\n\t\t\tTSession,\n\t\t\tTAuditStorage,\n\t\t\tTAuditStorageServiceName,\n\t\t\tTAuditAction,\n\t\t\tTDatabase,\n\t\t\tTDatabaseServiceName\n\t\t>,\n\t) {}\n\n\tstatic isDev = process.env.NODE_ENV === 'development';\n\n\tstatic async validate<T extends StandardSchemaV1>(\n\t\tc: Context<any, string, {}>,\n\t\tdata: unknown,\n\t\tschema?: T,\n\t) {\n\t\tif (!schema) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst parsed = await Endpoint.validate(schema, data);\n\n\t\tif (parsed.issues) {\n\t\t\treturn c.json(parsed.issues, 422);\n\t\t}\n\n\t\treturn parsed.value;\n\t}\n\taddRoute(\n\t\tserviceDiscovery: ServiceDiscovery<ServiceRecord<TServices>>,\n\t\tapp: Hono,\n\t): void {\n\t\tHonoEndpoint.addRoute(this.endpoint, serviceDiscovery, app);\n\t}\n\n\t/**\n\t * @deprecated Global event middleware is no longer used.\n\t * Events are now published per-route only for endpoints that have events configured.\n\t * This method is kept for backward compatibility but does nothing.\n\t */\n\tstatic applyEventMiddleware(\n\t\t_app: Hono,\n\t\t_serviceDiscovery: ServiceDiscovery<any>,\n\t) {\n\t\t// No-op: Event publishing is now handled per-route in addRoute\n\t\t// This avoids running middleware on every request including 404s\n\t}\n\n\tstatic async fromRoutes<TLogger extends Logger, TServices extends Service[]>(\n\t\troutes: string[],\n\t\tenvParser: EnvironmentParser<{}>,\n\t\tapp = new Hono(),\n\t\t_logger: TLogger,\n\t\tcwd = process.cwd(),\n\t\toptions?: HonoEndpointOptions,\n\t): Promise<Hono> {\n\t\tconst endpoints = await getEndpointsFromRoutes<TServices>(routes, cwd);\n\t\tconst serviceDiscovery =\n\t\t\tServiceDiscovery.getInstance<ServiceRecord<TServices>>(envParser);\n\n\t\tHonoEndpoint.addRoutes(endpoints, serviceDiscovery, app, options);\n\n\t\treturn app;\n\t}\n\n\tstatic addRoutes<\n\t\tTServices extends Service[] = [],\n\t\tTLogger extends Logger = Logger,\n\t>(\n\t\tendpoints: Endpoint<string, HttpMethod, any, any, TServices, TLogger>[],\n\t\tserviceDiscovery: ServiceDiscovery<ServiceRecord<TServices>>,\n\t\tapp: Hono,\n\t\toptions?: HonoEndpointOptions,\n\t): void {\n\t\t// Add timing middleware (always enabled)\n\t\tapp.use('*', timing());\n\n\t\t// Add logger middleware in development mode\n\n\t\tif (HonoEndpoint.isDev) {\n\t\t\tapp.use('*', honoLogger());\n\t\t}\n\n\t\t// Add docs route if not disabled\n\t\tconst docsPath =\n\t\t\toptions?.docsPath !== false ? options?.docsPath || '/docs' : null;\n\t\tif (docsPath) {\n\t\t\tHonoEndpoint.addDocsRoute(\n\t\t\t\tendpoints,\n\t\t\t\tapp,\n\t\t\t\tdocsPath,\n\t\t\t\toptions?.openApiOptions,\n\t\t\t);\n\t\t}\n\n\t\t// Sort endpoints to ensure static routes come before dynamic ones\n\t\tconst sortedEndpoints = endpoints.sort((a, b) => {\n\t\t\tconst aSegments = a.route.split('/');\n\t\t\tconst bSegments = b.route.split('/');\n\n\t\t\t// Compare each segment\n\t\t\tfor (let i = 0; i < Math.max(aSegments.length, bSegments.length); i++) {\n\t\t\t\tconst aSegment = aSegments[i] || '';\n\t\t\t\tconst bSegment = bSegments[i] || '';\n\n\t\t\t\t// If one is dynamic and the other is not, static comes first\n\t\t\t\tconst aIsDynamic = aSegment.startsWith(':');\n\t\t\t\tconst bIsDynamic = bSegment.startsWith(':');\n\n\t\t\t\tif (!aIsDynamic && bIsDynamic) return -1;\n\t\t\t\tif (aIsDynamic && !bIsDynamic) return 1;\n\n\t\t\t\t// If both are the same type, compare alphabetically\n\t\t\t\tif (aSegment !== bSegment) {\n\t\t\t\t\treturn aSegment.localeCompare(bSegment);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn 0;\n\t\t});\n\n\t\t// Note: Global event middleware removed for performance\n\t\t// Events are now published per-route only for endpoints with events configured\n\t\tfor (const endpoint of sortedEndpoints) {\n\t\t\tHonoEndpoint.addRoute(endpoint, serviceDiscovery, app);\n\t\t}\n\t}\n\n\t// biome-ignore lint/suspicious/useAdjacentOverloadSignatures: not an overload — the instance `addRoute` above registers this adaptor's own endpoint, this one registers any endpoint on a given app. They share a name because they do the same thing to different subjects.\n\tstatic addRoute<\n\t\tTRoute extends string,\n\t\tTMethod extends HttpMethod,\n\t\tTInput extends EndpointSchemas = {},\n\t\tTOutSchema extends StandardSchemaV1 | undefined = undefined,\n\t\tTServices extends Service[] = [],\n\t\tTLogger extends Logger = Logger,\n\t\tTSession = unknown,\n\t\tTAuditStorage extends AuditStorage | undefined = undefined,\n\t\tTAuditStorageServiceName extends string = string,\n\t\tTAuditAction extends AuditableAction<string, unknown> = AuditableAction<\n\t\t\tstring,\n\t\t\tunknown\n\t\t>,\n\t\tTDatabase = undefined,\n\t\tTDatabaseServiceName extends string = string,\n\t>(\n\t\tendpoint: Endpoint<\n\t\t\tTRoute,\n\t\t\tTMethod,\n\t\t\tTInput,\n\t\t\tTOutSchema,\n\t\t\tTServices,\n\t\t\tTLogger,\n\t\t\tTSession,\n\t\t\tTAuditStorage,\n\t\t\tTAuditStorageServiceName,\n\t\t\tTAuditAction,\n\t\t\tTDatabase,\n\t\t\tTDatabaseServiceName\n\t\t>,\n\t\tserviceDiscovery: ServiceDiscovery<ServiceRecord<TServices>>,\n\t\tapp: Hono,\n\t): void {\n\t\tconst { route } = endpoint;\n\t\tconst method = endpoint.method.toLowerCase() as LowerHttpMethod<TMethod>;\n\n\t\t// Analyze endpoint features once at registration time (not per-request)\n\t\tconst features = analyzeEndpointFeatures(endpoint);\n\n\t\t// Build validators array - only add validators for schemas that exist\n\t\tconst validators: any[] = [];\n\n\t\tif (features.hasBodyValidation) {\n\t\t\t// Read by its Content-Type — JSON, an HTML form, text — and kept\n\t\t\t// where a JSON validator keeps it, so the handler reads it as before.\n\t\t\tvalidators.push(async (c: Context, next: () => Promise<void>) => {\n\t\t\t\tlet raw: unknown;\n\t\t\t\ttry {\n\t\t\t\t\traw = await readRequestBody(c.req, c.req.header('content-type'), {\n\t\t\t\t\t\tuntyped: 'empty',\n\t\t\t\t\t});\n\t\t\t\t} catch (error) {\n\t\t\t\t\tif (!isHttpError(error)) throw error;\n\t\t\t\t\tsetErrorHeaders(c, error);\n\t\t\t\t\treturn c.json(\n\t\t\t\t\t\terrorBody(error),\n\t\t\t\t\t\terror.statusCode as ContentfulStatusCode,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tconst body = await HonoEndpoint.validate(c, raw, endpoint.input?.body);\n\t\t\t\tif (body instanceof Response) return body;\n\t\t\t\tc.req.addValidatedData('json', body as {});\n\t\t\t\tawait next();\n\t\t\t});\n\t\t}\n\n\t\tif (features.hasQueryValidation) {\n\t\t\tvalidators.push(\n\t\t\t\tvalidator('query', (_, c) => {\n\t\t\t\t\tconst parsedQuery = parseHonoQuery(c);\n\t\t\t\t\treturn HonoEndpoint.validate(c, parsedQuery, endpoint.input?.query);\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\n\t\tif (features.hasParamValidation) {\n\t\t\tvalidators.push(\n\t\t\t\tvalidator('param', (params, c) =>\n\t\t\t\t\tHonoEndpoint.validate(c, params, endpoint.input?.params),\n\t\t\t\t),\n\t\t\t);\n\t\t}\n\n\t\t// Main handler\n\t\tconst handler = async (c: Context) => {\n\t\t\t// Request context setup\n\t\t\tconst startTime = Date.now();\n\t\t\tconst requestId = c.req.header('X-Request-ID') ?? crypto.randomUUID();\n\n\t\t\tconst logger = endpoint.logger.child({\n\t\t\t\trequestId,\n\t\t\t\tendpoint: endpoint.fullPath,\n\t\t\t\troute: endpoint.route,\n\t\t\t\thost: c.req.header('host'),\n\t\t\t\tmethod: endpoint.method,\n\t\t\t\tpath: c.req.path,\n\t\t\t}) as TLogger;\n\n\t\t\t// Set response header\n\t\t\tc.header('X-Request-ID', requestId);\n\n\t\t\t// Wrap entire handler in request context for services to access\n\t\t\treturn runWithRequestContext(\n\t\t\t\t{\n\t\t\t\t\tlogger,\n\t\t\t\t\trequestId,\n\t\t\t\t\tstartTime,\n\t\t\t\t\toperation: `${endpoint.method} ${endpoint.route}`,\n\t\t\t\t},\n\t\t\t\tasync () => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\t// Lazy accessors - no upfront parsing, use native Hono methods\n\t\t\t\t\t\tconst header = createHonoHeaders(c);\n\t\t\t\t\t\tconst cookie = createHonoCookies(c);\n\n\t\t\t\t\t\t// Only register services if endpoint has any\n\t\t\t\t\t\tconst services = features.hasServices\n\t\t\t\t\t\t\t? await serviceDiscovery.register(endpoint.services)\n\t\t\t\t\t\t\t: ({} as ServiceRecord<TServices>);\n\n\t\t\t\t\t\t// Resolve database service only if configured\n\t\t\t\t\t\tconst rawDb = features.hasDatabase\n\t\t\t\t\t\t\t? await serviceDiscovery\n\t\t\t\t\t\t\t\t\t.register([endpoint.databaseService!])\n\t\t\t\t\t\t\t\t\t.then(\n\t\t\t\t\t\t\t\t\t\t(s) =>\n\t\t\t\t\t\t\t\t\t\t\ts[\n\t\t\t\t\t\t\t\t\t\t\t\tendpoint.databaseService?.serviceName as keyof typeof s\n\t\t\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t: undefined;\n\n\t\t\t\t\t\t// Extract session (defaults to empty object)\n\t\t\t\t\t\tconst session = await endpoint.getSession({\n\t\t\t\t\t\t\tservices,\n\t\t\t\t\t\t\tlogger,\n\t\t\t\t\t\t\theader,\n\t\t\t\t\t\t\tcookie,\n\t\t\t\t\t\t\t...(rawDb !== undefined && { db: rawDb }),\n\t\t\t\t\t\t} as any);\n\n\t\t\t\t\t\t// Check authorization (defaults to true)\n\t\t\t\t\t\tconst isAuthorized = await endpoint.authorize({\n\t\t\t\t\t\t\theader,\n\t\t\t\t\t\t\tcookie,\n\t\t\t\t\t\t\tservices,\n\t\t\t\t\t\t\tlogger,\n\t\t\t\t\t\t\tsession,\n\t\t\t\t\t\t\t...(rawDb !== undefined && { db: rawDb }),\n\t\t\t\t\t\t\tbody: features.hasBodyValidation\n\t\t\t\t\t\t\t\t? (c.req.valid as any)('json')\n\t\t\t\t\t\t\t\t: undefined,\n\t\t\t\t\t\t\tquery: features.hasQueryValidation\n\t\t\t\t\t\t\t\t? (c.req.valid as any)('query')\n\t\t\t\t\t\t\t\t: undefined,\n\t\t\t\t\t\t\tparams: features.hasParamValidation\n\t\t\t\t\t\t\t\t? (c.req.valid as any)('param')\n\t\t\t\t\t\t\t\t: undefined,\n\t\t\t\t\t\t} as any);\n\n\t\t\t\t\t\tif (!isAuthorized) {\n\t\t\t\t\t\t\tlogger.warn('Forbidden access attempt');\n\t\t\t\t\t\t\treturn c.json({ error: 'Forbidden' }, 403);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Check rate limit only if configured\n\t\t\t\t\t\tif (features.hasRateLimit) {\n\t\t\t\t\t\t\tconst rateLimitHeaders = await enforceRateLimit(\n\t\t\t\t\t\t\t\tendpoint.rateLimit!,\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\theader,\n\t\t\t\t\t\t\t\t\tservices,\n\t\t\t\t\t\t\t\t\tlogger,\n\t\t\t\t\t\t\t\t\tsession,\n\t\t\t\t\t\t\t\t\tpath: c.req.path,\n\t\t\t\t\t\t\t\t\tmethod: endpoint.method,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\tfor (const [key, value] of Object.entries(rateLimitHeaders)) {\n\t\t\t\t\t\t\t\tc.header(key, value);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Create audit context only if audits are configured\n\t\t\t\t\t\tconst auditContext = features.hasAudits\n\t\t\t\t\t\t\t? await createAuditContext(\n\t\t\t\t\t\t\t\t\tendpoint as any,\n\t\t\t\t\t\t\t\t\tserviceDiscovery,\n\t\t\t\t\t\t\t\t\tlogger,\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tsession,\n\t\t\t\t\t\t\t\t\t\theader,\n\t\t\t\t\t\t\t\t\t\tcookie,\n\t\t\t\t\t\t\t\t\t\tservices: services as Record<string, unknown>,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t: undefined;\n\n\t\t\t\t\t\tconst audits = features.hasAudits\n\t\t\t\t\t\t\t? (endpoint.audits as MappedAudit<TAuditAction, TOutSchema>[])\n\t\t\t\t\t\t\t: [];\n\n\t\t\t\t\t\t// Warn if declarative audits are configured but no audit storage\n\t\t\t\t\t\tif (features.hasAudits && !auditContext) {\n\t\t\t\t\t\t\tlogger.warn('No auditor storage service available');\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Extract RLS context only if configured and not bypassed\n\t\t\t\t\t\tconst rlsContext =\n\t\t\t\t\t\t\tfeatures.hasRls && rawDb !== undefined\n\t\t\t\t\t\t\t\t? await endpoint.rlsConfig?.extractor({\n\t\t\t\t\t\t\t\t\t\tservices,\n\t\t\t\t\t\t\t\t\t\tsession: session as TSession,\n\t\t\t\t\t\t\t\t\t\theader,\n\t\t\t\t\t\t\t\t\t\tcookie,\n\t\t\t\t\t\t\t\t\t\tlogger,\n\t\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t\t: undefined;\n\n\t\t\t\t\t\t// Execute handler with automatic audit transaction support\n\t\t\t\t\t\tconst result = await executeWithAuditTransaction(\n\t\t\t\t\t\t\tauditContext,\n\t\t\t\t\t\t\tasync (auditor) => {\n\t\t\t\t\t\t\t\tconst sameDatabase =\n\t\t\t\t\t\t\t\t\tauditContext?.storage?.databaseServiceName &&\n\t\t\t\t\t\t\t\t\tauditContext.storage.databaseServiceName ===\n\t\t\t\t\t\t\t\t\t\tendpoint.databaseService?.serviceName;\n\t\t\t\t\t\t\t\tconst baseDb = sameDatabase\n\t\t\t\t\t\t\t\t\t? (auditor?.getTransaction?.() ?? rawDb)\n\t\t\t\t\t\t\t\t\t: rawDb;\n\n\t\t\t\t\t\t\t\tconst executeHandler = async (db: TDatabase | undefined) => {\n\t\t\t\t\t\t\t\t\tconst responseBuilder = new ResponseBuilder();\n\t\t\t\t\t\t\t\t\tconst response = await endpoint.handler(\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\tservices,\n\t\t\t\t\t\t\t\t\t\t\tlogger,\n\t\t\t\t\t\t\t\t\t\t\tbody: features.hasBodyValidation\n\t\t\t\t\t\t\t\t\t\t\t\t? (c.req.valid as any)('json')\n\t\t\t\t\t\t\t\t\t\t\t\t: undefined,\n\t\t\t\t\t\t\t\t\t\t\tquery: features.hasQueryValidation\n\t\t\t\t\t\t\t\t\t\t\t\t? (c.req.valid as any)('query')\n\t\t\t\t\t\t\t\t\t\t\t\t: undefined,\n\t\t\t\t\t\t\t\t\t\t\tparams: features.hasParamValidation\n\t\t\t\t\t\t\t\t\t\t\t\t? (c.req.valid as any)('param')\n\t\t\t\t\t\t\t\t\t\t\t\t: undefined,\n\t\t\t\t\t\t\t\t\t\t\tsession,\n\t\t\t\t\t\t\t\t\t\t\theader,\n\t\t\t\t\t\t\t\t\t\t\tcookie,\n\t\t\t\t\t\t\t\t\t\t\tauditor,\n\t\t\t\t\t\t\t\t\t\t\tdb,\n\t\t\t\t\t\t\t\t\t\t} as unknown as EndpointContext<\n\t\t\t\t\t\t\t\t\t\t\tTInput,\n\t\t\t\t\t\t\t\t\t\t\tTServices,\n\t\t\t\t\t\t\t\t\t\t\tTLogger,\n\t\t\t\t\t\t\t\t\t\t\tTSession,\n\t\t\t\t\t\t\t\t\t\t\tTAuditAction,\n\t\t\t\t\t\t\t\t\t\t\tTDatabase,\n\t\t\t\t\t\t\t\t\t\t\tTAuditStorage\n\t\t\t\t\t\t\t\t\t\t>,\n\t\t\t\t\t\t\t\t\t\tresponseBuilder,\n\t\t\t\t\t\t\t\t\t);\n\n\t\t\t\t\t\t\t\t\tlet data = response;\n\t\t\t\t\t\t\t\t\tlet metadata = responseBuilder.getMetadata();\n\n\t\t\t\t\t\t\t\t\tif (Endpoint.hasMetadata(response)) {\n\t\t\t\t\t\t\t\t\t\tdata = response.data;\n\t\t\t\t\t\t\t\t\t\tmetadata = response.metadata;\n\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\t// A redirect has no body to check against the schema.\n\t\t\t\t\t\t\t\t\tconst redirect = Endpoint.isRedirectStatus(\n\t\t\t\t\t\t\t\t\t\tmetadata.status ?? endpoint.status,\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\tconst output =\n\t\t\t\t\t\t\t\t\t\tendpoint.outputSchema && !redirect\n\t\t\t\t\t\t\t\t\t\t\t? await endpoint.parseOutput(data)\n\t\t\t\t\t\t\t\t\t\t\t: undefined;\n\n\t\t\t\t\t\t\t\t\treturn { output, metadata, responseBuilder, redirect };\n\t\t\t\t\t\t\t\t};\n\n\t\t\t\t\t\t\t\tif (features.hasRls && rlsContext && baseDb) {\n\t\t\t\t\t\t\t\t\tconst { withRlsContext } = await loadRls();\n\t\t\t\t\t\t\t\t\treturn withRlsContext(\n\t\t\t\t\t\t\t\t\t\tbaseDb as any,\n\t\t\t\t\t\t\t\t\t\trlsContext,\n\t\t\t\t\t\t\t\t\t\tasync (trx) => executeHandler(trx as TDatabase),\n\t\t\t\t\t\t\t\t\t\t{ prefix: endpoint.rlsConfig?.prefix },\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\treturn executeHandler(baseDb as TDatabase | undefined);\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tasync (result, auditor) => {\n\t\t\t\t\t\t\t\tif (!audits?.length || result.redirect) return;\n\n\t\t\t\t\t\t\t\tfor (const audit of audits) {\n\t\t\t\t\t\t\t\t\tif (audit.when && !audit.when(result.output as any)) {\n\t\t\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tconst payload = audit.payload(result.output as any);\n\t\t\t\t\t\t\t\t\tconst entityId = audit.entityId?.(result.output as any);\n\t\t\t\t\t\t\t\t\tauditor.audit(audit.type as any, payload as any, {\n\t\t\t\t\t\t\t\t\t\ttable: audit.table,\n\t\t\t\t\t\t\t\t\t\tentityId,\n\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{ db: rawDb },\n\t\t\t\t\t\t);\n\n\t\t\t\t\t\tconst { output, metadata, redirect } = result;\n\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tlet status = endpoint.status as ContentfulStatusCode;\n\n\t\t\t\t\t\t\tif (metadata.status) {\n\t\t\t\t\t\t\t\tstatus = metadata.status as ContentfulStatusCode;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif (metadata.headers) {\n\t\t\t\t\t\t\t\tfor (const [key, value] of Object.entries(metadata.headers)) {\n\t\t\t\t\t\t\t\t\tc.header(key, value);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif (metadata.cookies) {\n\t\t\t\t\t\t\t\tfor (const [name, { value, options }] of metadata.cookies) {\n\t\t\t\t\t\t\t\t\tsetCookie(c, name, value, options);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Only publish events if configured (no global middleware overhead)\n\t\t\t\t\t\t\tif (features.hasEvents && Endpoint.isSuccessStatus(status)) {\n\t\t\t\t\t\t\t\tawait publishConstructEvents<any>(\n\t\t\t\t\t\t\t\t\tendpoint as any,\n\t\t\t\t\t\t\t\t\toutput,\n\t\t\t\t\t\t\t\t\tserviceDiscovery,\n\t\t\t\t\t\t\t\t\tlogger,\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif (HonoEndpoint.isDev) {\n\t\t\t\t\t\t\t\tlogger.info({ status, body: output }, 'Outgoing response');\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif (redirect) {\n\t\t\t\t\t\t\t\treturn c.body(null, status as RedirectStatusCode);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Respect the endpoint's declared responseType. For JSON (default)\n\t\t\t\t\t\t\t// we use c.json() which sets Content-Type and serializes. For any\n\t\t\t\t\t\t\t// other type, emit the body as-is and set the header explicitly —\n\t\t\t\t\t\t\t// metadata.headers (from r.header()) applied above still wins.\n\t\t\t\t\t\t\tif (endpoint.responseType === 'application/json') {\n\t\t\t\t\t\t\t\t// Hono 4.13 types `c.json`'s status well enough that the\n\t\t\t\t\t\t\t\t// suppression here is now itself an error.\n\t\t\t\t\t\t\t\treturn c.json(output, status);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (\n\t\t\t\t\t\t\t\t!(metadata.headers && hasContentTypeHeader(metadata.headers))\n\t\t\t\t\t\t\t) {\n\t\t\t\t\t\t\t\tc.header('content-type', endpoint.responseType);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn c.body(output as any, status);\n\t\t\t\t\t\t} catch (validationError: any) {\n\t\t\t\t\t\t\tlogger.error(\n\t\t\t\t\t\t\t\t{ err: validationError },\n\t\t\t\t\t\t\t\t'Output validation failed',\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\tconst error = wrapError(\n\t\t\t\t\t\t\t\tvalidationError,\n\t\t\t\t\t\t\t\t422,\n\t\t\t\t\t\t\t\t'Response validation failed',\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\tsetErrorHeaders(c, error);\n\t\t\t\t\t\t\tconst body = errorBody(error);\n\t\t\t\t\t\t\tif (HonoEndpoint.isDev) {\n\t\t\t\t\t\t\t\tlogger.info(\n\t\t\t\t\t\t\t\t\t{ status: error.statusCode, body },\n\t\t\t\t\t\t\t\t\t'Outgoing response',\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn c.json(body, error.statusCode as ContentfulStatusCode);\n\t\t\t\t\t\t}\n\t\t\t\t\t} catch (e: any) {\n\t\t\t\t\t\t// Under `err`, which every pino serializes as an Error — passed bare,\n\t\t\t\t\t\t// one pino does not recognise (a ZodError) is spread into the line\n\t\t\t\t\t\t// with no stack.\n\t\t\t\t\t\tlogger.error({ err: e }, 'Error processing endpoint request');\n\t\t\t\t\t\tconst error = wrapError(e, 500, 'Internal Server Error');\n\t\t\t\t\t\t// Answered here rather than rethrown, so Hono never sets it:\n\t\t\t\t\t\t// a server failure is left on the context the way Hono\n\t\t\t\t\t\t// leaves an uncaught one, for middleware — a request span\n\t\t\t\t\t\t// records it as the exception. A 4xx is the client's.\n\t\t\t\t\t\tif (error.statusCode >= 500 && e instanceof Error) c.error = e;\n\t\t\t\t\t\tsetErrorHeaders(c, error);\n\t\t\t\t\t\tconst body = errorBody(error);\n\t\t\t\t\t\tif (HonoEndpoint.isDev) {\n\t\t\t\t\t\t\tlogger.info(\n\t\t\t\t\t\t\t\t{ status: error.statusCode, body },\n\t\t\t\t\t\t\t\t'Outgoing response',\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn c.json(body, error.statusCode as ContentfulStatusCode);\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t);\n\t\t};\n\n\t\t// Register route with conditional validators.\n\t\t//\n\t\t// `route` is a generic `TRoute`, and Hono 4.13 overloads `app.get` on\n\t\t// whether the first argument is a path or a handler. A generic that is\n\t\t// only *constrained* to string does not pick the path overload, so it\n\t\t// gets matched against the handler one and reported there. Saying it is a\n\t\t// string chooses the overload that was always intended.\n\t\t//\n\t\t// The spread is why a cast is needed rather than a better type: with\n\t\t// `...validators` of unknown length, the arity is not decidable, so the\n\t\t// first overload is chosen whatever the path argument says. Naming the\n\t\t// shape we are calling states the intent the overloads cannot infer.\n\t\t(\n\t\t\tapp[method] as unknown as (\n\t\t\t\tpath: string,\n\t\t\t\t...handlers: unknown[]\n\t\t\t) => unknown\n\t\t)(route, ...validators, handler);\n\t}\n\n\tstatic addDocsRoute<\n\t\tTServices extends Service[] = [],\n\t\tTLogger extends Logger = Logger,\n\t>(\n\t\tendpoints: Endpoint<string, HttpMethod, any, any, TServices, TLogger>[],\n\t\tapp: Hono,\n\t\tdocsPath: string,\n\t\topenApiOptions?: HonoEndpointOptions['openApiOptions'],\n\t): void {\n\t\tapp.get(docsPath, async (c) => {\n\t\t\ttry {\n\t\t\t\tconst openApiSchema = await Endpoint.buildOpenApiSchema(\n\t\t\t\t\tendpoints,\n\t\t\t\t\topenApiOptions,\n\t\t\t\t);\n\n\t\t\t\treturn c.json(openApiSchema);\n\t\t\t} catch {\n\t\t\t\treturn c.json(\n\t\t\t\t\t{ error: 'Failed to generate OpenAPI documentation' },\n\t\t\t\t\t500,\n\t\t\t\t);\n\t\t\t}\n\t\t});\n\t}\n}\n\n/**\n * What a client is told about a failed request: what went wrong, and the stack\n * only under `gkm dev`. A server's stack is its file layout and its\n * dependencies' versions — for the person debugging it, not for whoever sent\n * the request. A server failure's details are the error it wrapped (an\n * environment variable it could not read, a query that failed), so they go to\n * the log and not the response either; a 4xx's details are the client's.\n */\nfunction errorBody(error: HttpError) {\n\tconst internal = error.statusCode >= 500 && !HonoEndpoint.isDev;\n\treturn {\n\t\tname: error.name,\n\t\tmessage: error.message,\n\t\tstatusCode: error.statusCode,\n\t\tstatusMessage: error.statusMessage,\n\t\tcode: error.code,\n\t\t...(internal ? {} : { details: error.details }),\n\t\t...(HonoEndpoint.isDev ? { stack: error.stack } : {}),\n\t};\n}\n\n/**\n * The headers an error says its response carries — a 429's `Retry-After` and\n * `X-RateLimit-*` — on the response that reports it.\n */\nfunction setErrorHeaders(c: Context, error: HttpError): void {\n\tfor (const [key, value] of Object.entries(error.headers ?? {})) {\n\t\tc.header(key, value);\n\t}\n}\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"HonoEndpointAdaptor-eCKyEOl_.cjs","names":["fg","path","Endpoint","Endpoint","Hono","ServiceDiscovery","timing","honoLogger","readRequestBody","isHttpError","validator","runWithRequestContext","createHonoHeaders","createHonoCookies","enforceRateLimit","createAuditContext","executeWithAuditTransaction","ResponseBuilder","loadRls","publishConstructEvents","wrapError"],"sources":["../src/endpoints/helpers.ts","../src/endpoints/parseHonoQuery.ts","../src/endpoints/HonoEndpointAdaptor.ts"],"sourcesContent":["import path from 'node:path';\nimport type { Service } from '@geekmidas/services';\nimport fg from 'fast-glob';\nimport type { HttpMethod } from '../types';\nimport { Endpoint } from './Endpoint';\n\n// Re-export utility functions\n\n/**\n * Recursively finds the project root directory by looking for lock files.\n * Traverses up the directory tree until it finds a package manager lock file.\n *\n * @param cwd - The current working directory to start searching from\n * @returns Promise resolving to the absolute path of the project root\n *\n * @example\n * ```typescript\n * const projectRoot = await getProjectRoot(process.cwd());\n * console.log(`Project root: ${projectRoot}`);\n * // Output: Project root: /Users/user/my-project\n * ```\n */\nexport async function getProjectRoot(cwd: string): Promise<string> {\n\tif (cwd === '/') {\n\t\treturn cwd;\n\t}\n\n\tconst stream = fg.stream(\n\t\t['yarn.lock', 'pnpm-lock.yaml', 'package-lock.json', 'deno.lock'],\n\t\t{ dot: true, cwd },\n\t);\n\n\tlet isRoot = false;\n\n\tfor await (const _ of stream) {\n\t\tisRoot = true;\n\t\tbreak;\n\t}\n\n\tif (isRoot) {\n\t\treturn cwd;\n\t}\n\n\treturn getProjectRoot(path.resolve(cwd, '..'));\n}\n\n/**\n * Discovers and imports all Endpoint instances from the specified route patterns.\n * Uses fast-glob to find files matching the patterns and extracts exported Endpoints.\n *\n * @template TServices - Array of service types used by the endpoints\n * @param routes - Array of glob patterns to match route files (e.g., ['src/routes/*.ts'])\n * @param cwd - The current working directory to resolve paths from\n * @returns Promise resolving to an array of Endpoint instances found in the matched files\n *\n * @example\n * ```typescript\n * // Find all endpoints in the routes directory\n * const endpoints = await getEndpointsFromRoutes(\n * ['src/routes/**\\/*.ts'],\n * process.cwd()\n * );\n *\n * // Register endpoints with your server\n * for (const endpoint of endpoints) {\n * server.register(endpoint);\n * }\n * ```\n *\n * @remarks\n * - Only exports that are valid Endpoint instances are included\n * - Files are imported dynamically, so they must be valid ES modules\n * - The function filters out non-Endpoint exports automatically\n */\nexport async function getEndpointsFromRoutes<TServices extends Service[]>(\n\troutes: string[],\n\tcwd: string,\n): Promise<Endpoint<string, HttpMethod, any, any, TServices>[]> {\n\tconst stream = fg.stream(routes, { cwd });\n\n\tconst endpoints: Endpoint<string, HttpMethod, any, any, TServices>[] = [];\n\n\tfor await (const f of stream) {\n\t\t// Resolve the absolute path for the route file\n\t\tconst routePath = path.resolve(cwd, f.toString());\n\t\t// Dynamically import the route module\n\t\tconst route = await import(routePath);\n\n\t\t// Filter exported values to find only Endpoint instances\n\t\tconst handlers = Object.values(route).filter((value) => {\n\t\t\treturn Endpoint.isEndpoint(value);\n\t\t}) as unknown as Endpoint<string, HttpMethod, any, any, TServices>[];\n\n\t\tendpoints.push(...handlers);\n\t}\n\n\treturn endpoints;\n}\n","import type { Context } from 'hono';\nimport qs from 'qs';\n\n/**\n * Parse Hono query parameters using qs bracket notation\n * Supports nested objects, arrays, and deep nesting via standard bracket syntax\n */\nexport function parseHonoQuery(c: Context): Record<string, any> {\n\tconst url = c.req.url;\n\tconst queryIndex = url.indexOf('?');\n\tif (queryIndex === -1) {\n\t\treturn {};\n\t}\n\tconst queryString = url.slice(queryIndex + 1);\n\treturn qs.parse(queryString) as Record<string, any>;\n}\n","import type { AuditableAction, AuditStorage } from '@geekmidas/audit';\nimport type { EnvironmentParser } from '@geekmidas/envkit';\nimport { type HttpError, isHttpError, wrapError } from '@geekmidas/errors';\nimport type { EventPublisher } from '@geekmidas/events';\nimport type { Logger } from '@geekmidas/logger';\nimport {\n\trunWithRequestContext,\n\ttype Service,\n\tServiceDiscovery,\n\ttype ServiceRecord,\n} from '@geekmidas/services';\nimport type { StandardSchemaV1 } from '@standard-schema/spec';\nimport { type Context, Hono } from 'hono';\nimport { setCookie } from 'hono/cookie';\nimport { logger as honoLogger } from 'hono/logger';\nimport { timing } from 'hono/timing';\nimport type {\n\tContentfulStatusCode,\n\tRedirectStatusCode,\n} from 'hono/utils/http-status';\nimport { validator } from 'hono/validator';\nimport { publishConstructEvents } from '../publisher';\nimport type { HttpMethod, LowerHttpMethod } from '../types';\nimport type { MappedAudit } from './audit';\nimport {\n\tEndpoint,\n\ttype EndpointContext,\n\ttype EndpointSchemas,\n\tResponseBuilder,\n} from './Endpoint';\nimport { getEndpointsFromRoutes } from './helpers';\nimport { createHonoCookies, createHonoHeaders } from './lazyAccessors';\nimport { loadRls } from './optionalPeers';\nimport { parseHonoQuery } from './parseHonoQuery';\nimport {\n\tcreateAuditContext,\n\texecuteWithAuditTransaction,\n} from './processAudits';\nimport { enforceRateLimit } from './rateLimit';\nimport { readRequestBody } from './readRequestBody';\n\nexport interface HonoEndpointOptions {\n\t/**\n\t * Path where OpenAPI documentation will be served.\n\t * Set to false to disable docs route.\n\t * @default '/docs'\n\t */\n\tdocsPath?: string | false;\n\t/**\n\t * OpenAPI schema options\n\t */\n\topenApiOptions?: {\n\t\ttitle?: string;\n\t\tversion?: string;\n\t\tdescription?: string;\n\t};\n}\n\n/**\n * Feature flags for an endpoint, analyzed once at route registration time.\n * This avoids per-request feature detection overhead.\n */\ninterface EndpointFeatures {\n\thasServices: boolean;\n\thasDatabase: boolean;\n\thasBodyValidation: boolean;\n\thasQueryValidation: boolean;\n\thasParamValidation: boolean;\n\thasAudits: boolean;\n\thasEvents: boolean;\n\thasRateLimit: boolean;\n\thasRls: boolean;\n}\n\n/**\n * Case-insensitive check for a `content-type` header in a metadata headers map.\n */\nfunction hasContentTypeHeader(headers: Record<string, string>): boolean {\n\tfor (const key of Object.keys(headers)) {\n\t\tif (key.toLowerCase() === 'content-type') return true;\n\t}\n\treturn false;\n}\n\n/**\n * Analyze endpoint features at registration time (not per-request)\n */\nfunction analyzeEndpointFeatures(\n\tendpoint: Endpoint<\n\t\tany,\n\t\tany,\n\t\tany,\n\t\tany,\n\t\tany,\n\t\tany,\n\t\tany,\n\t\tany,\n\t\tany,\n\t\tany,\n\t\tany,\n\t\tany\n\t>,\n): EndpointFeatures {\n\treturn {\n\t\thasServices: endpoint.services.length > 0,\n\t\thasDatabase: !!endpoint.databaseService,\n\t\thasBodyValidation: !!endpoint.input?.body,\n\t\thasQueryValidation: !!endpoint.input?.query,\n\t\thasParamValidation: !!endpoint.input?.params,\n\t\t// Audit context needed if declarative audits OR auditor storage service configured (for manual audits)\n\t\thasAudits:\n\t\t\t(endpoint.audits?.length ?? 0) > 0 || !!endpoint.auditorStorageService,\n\t\thasEvents: (endpoint.events?.length ?? 0) > 0,\n\t\thasRateLimit: !!endpoint.rateLimit,\n\t\thasRls: !!endpoint.rlsConfig && !endpoint.rlsBypass,\n\t};\n}\n\nexport class HonoEndpoint<\n\tTRoute extends string,\n\tTMethod extends HttpMethod,\n\tTInput extends EndpointSchemas = {},\n\tTOutSchema extends StandardSchemaV1 | undefined = undefined,\n\tTServices extends Service[] = [],\n\tTLogger extends Logger = Logger,\n\tTSession = unknown,\n\tTAuditStorage extends AuditStorage | undefined = undefined,\n\tTAuditStorageServiceName extends string = string,\n\tTAuditAction extends AuditableAction<string, unknown> = AuditableAction<\n\t\tstring,\n\t\tunknown\n\t>,\n\tTDatabase = undefined,\n\tTDatabaseServiceName extends string = string,\n> {\n\tconstructor(\n\t\tprivate readonly endpoint: Endpoint<\n\t\t\tTRoute,\n\t\t\tTMethod,\n\t\t\tTInput,\n\t\t\tTOutSchema,\n\t\t\tTServices,\n\t\t\tTLogger,\n\t\t\tTSession,\n\t\t\tTAuditStorage,\n\t\t\tTAuditStorageServiceName,\n\t\t\tTAuditAction,\n\t\t\tTDatabase,\n\t\t\tTDatabaseServiceName\n\t\t>,\n\t) {}\n\n\tstatic isDev = process.env.NODE_ENV === 'development';\n\n\tstatic async validate<T extends StandardSchemaV1>(\n\t\tc: Context<any, string, {}>,\n\t\tdata: unknown,\n\t\tschema?: T,\n\t) {\n\t\tif (!schema) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst parsed = await Endpoint.validate(schema, data);\n\n\t\tif (parsed.issues) {\n\t\t\treturn c.json(parsed.issues, 422);\n\t\t}\n\n\t\treturn parsed.value;\n\t}\n\taddRoute(\n\t\tserviceDiscovery: ServiceDiscovery<ServiceRecord<TServices>>,\n\t\tapp: Hono,\n\t): void {\n\t\tHonoEndpoint.addRoute(this.endpoint, serviceDiscovery, app);\n\t}\n\n\t/**\n\t * @deprecated Global event middleware is no longer used.\n\t * Events are now published per-route only for endpoints that have events configured.\n\t * This method is kept for backward compatibility but does nothing.\n\t */\n\tstatic applyEventMiddleware(\n\t\t_app: Hono,\n\t\t_serviceDiscovery: ServiceDiscovery<any>,\n\t) {\n\t\t// No-op: Event publishing is now handled per-route in addRoute\n\t\t// This avoids running middleware on every request including 404s\n\t}\n\n\tstatic async fromRoutes<TLogger extends Logger, TServices extends Service[]>(\n\t\troutes: string[],\n\t\tenvParser: EnvironmentParser<{}>,\n\t\tapp = new Hono(),\n\t\t_logger: TLogger,\n\t\tcwd = process.cwd(),\n\t\toptions?: HonoEndpointOptions,\n\t): Promise<Hono> {\n\t\tconst endpoints = await getEndpointsFromRoutes<TServices>(routes, cwd);\n\t\tconst serviceDiscovery =\n\t\t\tServiceDiscovery.getInstance<ServiceRecord<TServices>>(envParser);\n\n\t\tHonoEndpoint.addRoutes(endpoints, serviceDiscovery, app, options);\n\n\t\treturn app;\n\t}\n\n\tstatic addRoutes<\n\t\tTServices extends Service[] = [],\n\t\tTLogger extends Logger = Logger,\n\t>(\n\t\tendpoints: Endpoint<string, HttpMethod, any, any, TServices, TLogger>[],\n\t\tserviceDiscovery: ServiceDiscovery<ServiceRecord<TServices>>,\n\t\tapp: Hono,\n\t\toptions?: HonoEndpointOptions,\n\t): void {\n\t\t// Add timing middleware (always enabled)\n\t\tapp.use('*', timing());\n\n\t\t// Add logger middleware in development mode\n\n\t\tif (HonoEndpoint.isDev) {\n\t\t\tapp.use('*', honoLogger());\n\t\t}\n\n\t\t// Add docs route if not disabled\n\t\tconst docsPath =\n\t\t\toptions?.docsPath !== false ? options?.docsPath || '/docs' : null;\n\t\tif (docsPath) {\n\t\t\tHonoEndpoint.addDocsRoute(\n\t\t\t\tendpoints,\n\t\t\t\tapp,\n\t\t\t\tdocsPath,\n\t\t\t\toptions?.openApiOptions,\n\t\t\t);\n\t\t}\n\n\t\t// Sort endpoints to ensure static routes come before dynamic ones\n\t\tconst sortedEndpoints = endpoints.sort((a, b) => {\n\t\t\tconst aSegments = a.route.split('/');\n\t\t\tconst bSegments = b.route.split('/');\n\n\t\t\t// Compare each segment\n\t\t\tfor (let i = 0; i < Math.max(aSegments.length, bSegments.length); i++) {\n\t\t\t\tconst aSegment = aSegments[i] || '';\n\t\t\t\tconst bSegment = bSegments[i] || '';\n\n\t\t\t\t// If one is dynamic and the other is not, static comes first\n\t\t\t\tconst aIsDynamic = aSegment.startsWith(':');\n\t\t\t\tconst bIsDynamic = bSegment.startsWith(':');\n\n\t\t\t\tif (!aIsDynamic && bIsDynamic) return -1;\n\t\t\t\tif (aIsDynamic && !bIsDynamic) return 1;\n\n\t\t\t\t// If both are the same type, compare alphabetically\n\t\t\t\tif (aSegment !== bSegment) {\n\t\t\t\t\treturn aSegment.localeCompare(bSegment);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn 0;\n\t\t});\n\n\t\t// Note: Global event middleware removed for performance\n\t\t// Events are now published per-route only for endpoints with events configured\n\t\tfor (const endpoint of sortedEndpoints) {\n\t\t\tHonoEndpoint.addRoute(endpoint, serviceDiscovery, app);\n\t\t}\n\t}\n\n\t// biome-ignore lint/suspicious/useAdjacentOverloadSignatures: not an overload — the instance `addRoute` above registers this adaptor's own endpoint, this one registers any endpoint on a given app. They share a name because they do the same thing to different subjects.\n\tstatic addRoute<\n\t\tTRoute extends string,\n\t\tTMethod extends HttpMethod,\n\t\tTInput extends EndpointSchemas = {},\n\t\tTOutSchema extends StandardSchemaV1 | undefined = undefined,\n\t\tTServices extends Service[] = [],\n\t\tTLogger extends Logger = Logger,\n\t\tTSession = unknown,\n\t\tTAuditStorage extends AuditStorage | undefined = undefined,\n\t\tTAuditStorageServiceName extends string = string,\n\t\tTAuditAction extends AuditableAction<string, unknown> = AuditableAction<\n\t\t\tstring,\n\t\t\tunknown\n\t\t>,\n\t\tTDatabase = undefined,\n\t\tTDatabaseServiceName extends string = string,\n\t>(\n\t\tendpoint: Endpoint<\n\t\t\tTRoute,\n\t\t\tTMethod,\n\t\t\tTInput,\n\t\t\tTOutSchema,\n\t\t\tTServices,\n\t\t\tTLogger,\n\t\t\tTSession,\n\t\t\tTAuditStorage,\n\t\t\tTAuditStorageServiceName,\n\t\t\tTAuditAction,\n\t\t\tTDatabase,\n\t\t\tTDatabaseServiceName\n\t\t>,\n\t\tserviceDiscovery: ServiceDiscovery<ServiceRecord<TServices>>,\n\t\tapp: Hono,\n\t): void {\n\t\tconst { route } = endpoint;\n\t\tconst method = endpoint.method.toLowerCase() as LowerHttpMethod<TMethod>;\n\n\t\t// Analyze endpoint features once at registration time (not per-request)\n\t\tconst features = analyzeEndpointFeatures(endpoint);\n\n\t\t// Build validators array - only add validators for schemas that exist\n\t\tconst validators: any[] = [];\n\n\t\tif (features.hasBodyValidation) {\n\t\t\t// Read by its Content-Type — JSON, an HTML form, text — and kept\n\t\t\t// where a JSON validator keeps it, so the handler reads it as before.\n\t\t\tvalidators.push(async (c: Context, next: () => Promise<void>) => {\n\t\t\t\tlet raw: unknown;\n\t\t\t\ttry {\n\t\t\t\t\traw = await readRequestBody(c.req, c.req.header('content-type'), {\n\t\t\t\t\t\tuntyped: 'empty',\n\t\t\t\t\t});\n\t\t\t\t} catch (error) {\n\t\t\t\t\tif (!isHttpError(error)) throw error;\n\t\t\t\t\tsetErrorHeaders(c, error);\n\t\t\t\t\treturn c.json(\n\t\t\t\t\t\terrorBody(error),\n\t\t\t\t\t\terror.statusCode as ContentfulStatusCode,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tconst body = await HonoEndpoint.validate(c, raw, endpoint.input?.body);\n\t\t\t\tif (body instanceof Response) return body;\n\t\t\t\tc.req.addValidatedData('json', body as {});\n\t\t\t\tawait next();\n\t\t\t});\n\t\t}\n\n\t\tif (features.hasQueryValidation) {\n\t\t\tvalidators.push(\n\t\t\t\tvalidator('query', (_, c) => {\n\t\t\t\t\tconst parsedQuery = parseHonoQuery(c);\n\t\t\t\t\treturn HonoEndpoint.validate(c, parsedQuery, endpoint.input?.query);\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\n\t\tif (features.hasParamValidation) {\n\t\t\tvalidators.push(\n\t\t\t\tvalidator('param', (params, c) =>\n\t\t\t\t\tHonoEndpoint.validate(c, params, endpoint.input?.params),\n\t\t\t\t),\n\t\t\t);\n\t\t}\n\n\t\t// Main handler\n\t\tconst handler = async (c: Context) => {\n\t\t\t// Request context setup\n\t\t\tconst startTime = Date.now();\n\t\t\tconst requestId = c.req.header('X-Request-ID') ?? crypto.randomUUID();\n\n\t\t\tconst logger = endpoint.logger.child({\n\t\t\t\trequestId,\n\t\t\t\tendpoint: endpoint.fullPath,\n\t\t\t\troute: endpoint.route,\n\t\t\t\thost: c.req.header('host'),\n\t\t\t\tmethod: endpoint.method,\n\t\t\t\tpath: c.req.path,\n\t\t\t}) as TLogger;\n\n\t\t\t// Set response header\n\t\t\tc.header('X-Request-ID', requestId);\n\n\t\t\t// Wrap entire handler in request context for services to access\n\t\t\treturn runWithRequestContext(\n\t\t\t\t{\n\t\t\t\t\tlogger,\n\t\t\t\t\trequestId,\n\t\t\t\t\tstartTime,\n\t\t\t\t\toperation: `${endpoint.method} ${endpoint.route}`,\n\t\t\t\t},\n\t\t\t\tasync () => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\t// Lazy accessors - no upfront parsing, use native Hono methods\n\t\t\t\t\t\tconst header = createHonoHeaders(c);\n\t\t\t\t\t\tconst cookie = createHonoCookies(c);\n\n\t\t\t\t\t\t// Only register services if endpoint has any\n\t\t\t\t\t\tconst services = features.hasServices\n\t\t\t\t\t\t\t? await serviceDiscovery.register(endpoint.services)\n\t\t\t\t\t\t\t: ({} as ServiceRecord<TServices>);\n\n\t\t\t\t\t\t// Resolve database service only if configured\n\t\t\t\t\t\tconst rawDb = features.hasDatabase\n\t\t\t\t\t\t\t? await serviceDiscovery\n\t\t\t\t\t\t\t\t\t.register([endpoint.databaseService!])\n\t\t\t\t\t\t\t\t\t.then(\n\t\t\t\t\t\t\t\t\t\t(s) =>\n\t\t\t\t\t\t\t\t\t\t\ts[\n\t\t\t\t\t\t\t\t\t\t\t\tendpoint.databaseService?.serviceName as keyof typeof s\n\t\t\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t: undefined;\n\n\t\t\t\t\t\t// Extract session (defaults to empty object)\n\t\t\t\t\t\tconst session = await endpoint.getSession({\n\t\t\t\t\t\t\tservices,\n\t\t\t\t\t\t\tlogger,\n\t\t\t\t\t\t\theader,\n\t\t\t\t\t\t\tcookie,\n\t\t\t\t\t\t\t...(rawDb !== undefined && { db: rawDb }),\n\t\t\t\t\t\t} as any);\n\n\t\t\t\t\t\t// Check authorization (defaults to true)\n\t\t\t\t\t\tconst isAuthorized = await endpoint.authorize({\n\t\t\t\t\t\t\theader,\n\t\t\t\t\t\t\tcookie,\n\t\t\t\t\t\t\tservices,\n\t\t\t\t\t\t\tlogger,\n\t\t\t\t\t\t\tsession,\n\t\t\t\t\t\t\t...(rawDb !== undefined && { db: rawDb }),\n\t\t\t\t\t\t\tbody: features.hasBodyValidation\n\t\t\t\t\t\t\t\t? (c.req.valid as any)('json')\n\t\t\t\t\t\t\t\t: undefined,\n\t\t\t\t\t\t\tquery: features.hasQueryValidation\n\t\t\t\t\t\t\t\t? (c.req.valid as any)('query')\n\t\t\t\t\t\t\t\t: undefined,\n\t\t\t\t\t\t\tparams: features.hasParamValidation\n\t\t\t\t\t\t\t\t? (c.req.valid as any)('param')\n\t\t\t\t\t\t\t\t: undefined,\n\t\t\t\t\t\t} as any);\n\n\t\t\t\t\t\tif (!isAuthorized) {\n\t\t\t\t\t\t\tlogger.warn('Forbidden access attempt');\n\t\t\t\t\t\t\treturn c.json({ error: 'Forbidden' }, 403);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Check rate limit only if configured\n\t\t\t\t\t\tif (features.hasRateLimit) {\n\t\t\t\t\t\t\tconst rateLimitHeaders = await enforceRateLimit(\n\t\t\t\t\t\t\t\tendpoint.rateLimit!,\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\theader,\n\t\t\t\t\t\t\t\t\tservices,\n\t\t\t\t\t\t\t\t\tlogger,\n\t\t\t\t\t\t\t\t\tsession,\n\t\t\t\t\t\t\t\t\tpath: c.req.path,\n\t\t\t\t\t\t\t\t\tmethod: endpoint.method,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\tfor (const [key, value] of Object.entries(rateLimitHeaders)) {\n\t\t\t\t\t\t\t\tc.header(key, value);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Create audit context only if audits are configured\n\t\t\t\t\t\tconst auditContext = features.hasAudits\n\t\t\t\t\t\t\t? await createAuditContext(\n\t\t\t\t\t\t\t\t\tendpoint as any,\n\t\t\t\t\t\t\t\t\tserviceDiscovery,\n\t\t\t\t\t\t\t\t\tlogger,\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tsession,\n\t\t\t\t\t\t\t\t\t\theader,\n\t\t\t\t\t\t\t\t\t\tcookie,\n\t\t\t\t\t\t\t\t\t\tservices: services as Record<string, unknown>,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t: undefined;\n\n\t\t\t\t\t\tconst audits = features.hasAudits\n\t\t\t\t\t\t\t? (endpoint.audits as MappedAudit<TAuditAction, TOutSchema>[])\n\t\t\t\t\t\t\t: [];\n\n\t\t\t\t\t\t// Warn if declarative audits are configured but no audit storage\n\t\t\t\t\t\tif (features.hasAudits && !auditContext) {\n\t\t\t\t\t\t\tlogger.warn('No auditor storage service available');\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Extract RLS context only if configured and not bypassed\n\t\t\t\t\t\tconst rlsContext =\n\t\t\t\t\t\t\tfeatures.hasRls && rawDb !== undefined\n\t\t\t\t\t\t\t\t? await endpoint.rlsConfig?.extractor({\n\t\t\t\t\t\t\t\t\t\tservices,\n\t\t\t\t\t\t\t\t\t\tsession: session as TSession,\n\t\t\t\t\t\t\t\t\t\theader,\n\t\t\t\t\t\t\t\t\t\tcookie,\n\t\t\t\t\t\t\t\t\t\tlogger,\n\t\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t\t: undefined;\n\n\t\t\t\t\t\t// Execute handler with automatic audit transaction support\n\t\t\t\t\t\tconst result = await executeWithAuditTransaction(\n\t\t\t\t\t\t\tauditContext,\n\t\t\t\t\t\t\tasync (auditor) => {\n\t\t\t\t\t\t\t\tconst sameDatabase =\n\t\t\t\t\t\t\t\t\tauditContext?.storage?.databaseServiceName &&\n\t\t\t\t\t\t\t\t\tauditContext.storage.databaseServiceName ===\n\t\t\t\t\t\t\t\t\t\tendpoint.databaseService?.serviceName;\n\t\t\t\t\t\t\t\tconst baseDb = sameDatabase\n\t\t\t\t\t\t\t\t\t? (auditor?.getTransaction?.() ?? rawDb)\n\t\t\t\t\t\t\t\t\t: rawDb;\n\n\t\t\t\t\t\t\t\tconst executeHandler = async (db: TDatabase | undefined) => {\n\t\t\t\t\t\t\t\t\tconst responseBuilder = new ResponseBuilder();\n\t\t\t\t\t\t\t\t\tconst response = await endpoint.handler(\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\tservices,\n\t\t\t\t\t\t\t\t\t\t\tlogger,\n\t\t\t\t\t\t\t\t\t\t\tbody: features.hasBodyValidation\n\t\t\t\t\t\t\t\t\t\t\t\t? (c.req.valid as any)('json')\n\t\t\t\t\t\t\t\t\t\t\t\t: undefined,\n\t\t\t\t\t\t\t\t\t\t\tquery: features.hasQueryValidation\n\t\t\t\t\t\t\t\t\t\t\t\t? (c.req.valid as any)('query')\n\t\t\t\t\t\t\t\t\t\t\t\t: undefined,\n\t\t\t\t\t\t\t\t\t\t\tparams: features.hasParamValidation\n\t\t\t\t\t\t\t\t\t\t\t\t? (c.req.valid as any)('param')\n\t\t\t\t\t\t\t\t\t\t\t\t: undefined,\n\t\t\t\t\t\t\t\t\t\t\tsession,\n\t\t\t\t\t\t\t\t\t\t\theader,\n\t\t\t\t\t\t\t\t\t\t\tcookie,\n\t\t\t\t\t\t\t\t\t\t\tauditor,\n\t\t\t\t\t\t\t\t\t\t\tdb,\n\t\t\t\t\t\t\t\t\t\t} as unknown as EndpointContext<\n\t\t\t\t\t\t\t\t\t\t\tTInput,\n\t\t\t\t\t\t\t\t\t\t\tTServices,\n\t\t\t\t\t\t\t\t\t\t\tTLogger,\n\t\t\t\t\t\t\t\t\t\t\tTSession,\n\t\t\t\t\t\t\t\t\t\t\tTAuditAction,\n\t\t\t\t\t\t\t\t\t\t\tTDatabase,\n\t\t\t\t\t\t\t\t\t\t\tTAuditStorage\n\t\t\t\t\t\t\t\t\t\t>,\n\t\t\t\t\t\t\t\t\t\tresponseBuilder,\n\t\t\t\t\t\t\t\t\t);\n\n\t\t\t\t\t\t\t\t\tlet data = response;\n\t\t\t\t\t\t\t\t\tlet metadata = responseBuilder.getMetadata();\n\n\t\t\t\t\t\t\t\t\tif (Endpoint.hasMetadata(response)) {\n\t\t\t\t\t\t\t\t\t\tdata = response.data;\n\t\t\t\t\t\t\t\t\t\tmetadata = response.metadata;\n\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\t// A redirect has no body to check against the schema.\n\t\t\t\t\t\t\t\t\tconst redirect = Endpoint.isRedirectStatus(\n\t\t\t\t\t\t\t\t\t\tmetadata.status ?? endpoint.status,\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\tconst output =\n\t\t\t\t\t\t\t\t\t\tendpoint.outputSchema && !redirect\n\t\t\t\t\t\t\t\t\t\t\t? await endpoint.parseOutput(data)\n\t\t\t\t\t\t\t\t\t\t\t: undefined;\n\n\t\t\t\t\t\t\t\t\treturn { output, metadata, responseBuilder, redirect };\n\t\t\t\t\t\t\t\t};\n\n\t\t\t\t\t\t\t\tif (features.hasRls && rlsContext && baseDb) {\n\t\t\t\t\t\t\t\t\tconst { withRlsContext } = await loadRls();\n\t\t\t\t\t\t\t\t\treturn withRlsContext(\n\t\t\t\t\t\t\t\t\t\tbaseDb as any,\n\t\t\t\t\t\t\t\t\t\trlsContext,\n\t\t\t\t\t\t\t\t\t\tasync (trx) => executeHandler(trx as TDatabase),\n\t\t\t\t\t\t\t\t\t\t{ prefix: endpoint.rlsConfig?.prefix },\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\treturn executeHandler(baseDb as TDatabase | undefined);\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tasync (result, auditor) => {\n\t\t\t\t\t\t\t\tif (!audits?.length || result.redirect) return;\n\n\t\t\t\t\t\t\t\tfor (const audit of audits) {\n\t\t\t\t\t\t\t\t\tif (audit.when && !audit.when(result.output as any)) {\n\t\t\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tconst payload = audit.payload(result.output as any);\n\t\t\t\t\t\t\t\t\tconst entityId = audit.entityId?.(result.output as any);\n\t\t\t\t\t\t\t\t\tauditor.audit(audit.type as any, payload as any, {\n\t\t\t\t\t\t\t\t\t\ttable: audit.table,\n\t\t\t\t\t\t\t\t\t\tentityId,\n\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{ db: rawDb },\n\t\t\t\t\t\t);\n\n\t\t\t\t\t\tconst { output, metadata, redirect } = result;\n\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tlet status = endpoint.status as ContentfulStatusCode;\n\n\t\t\t\t\t\t\tif (metadata.status) {\n\t\t\t\t\t\t\t\tstatus = metadata.status as ContentfulStatusCode;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif (metadata.headers) {\n\t\t\t\t\t\t\t\tfor (const [key, value] of Object.entries(metadata.headers)) {\n\t\t\t\t\t\t\t\t\tc.header(key, value);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif (metadata.cookies) {\n\t\t\t\t\t\t\t\tfor (const [name, { value, options }] of metadata.cookies) {\n\t\t\t\t\t\t\t\t\tsetCookie(c, name, value, options);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Only publish events if configured (no global middleware overhead)\n\t\t\t\t\t\t\tif (features.hasEvents && Endpoint.isSuccessStatus(status)) {\n\t\t\t\t\t\t\t\tawait publishConstructEvents<any>(\n\t\t\t\t\t\t\t\t\tendpoint as any,\n\t\t\t\t\t\t\t\t\toutput,\n\t\t\t\t\t\t\t\t\tserviceDiscovery,\n\t\t\t\t\t\t\t\t\tlogger,\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif (HonoEndpoint.isDev) {\n\t\t\t\t\t\t\t\tlogger.info({ status, body: output }, 'Outgoing response');\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif (redirect) {\n\t\t\t\t\t\t\t\treturn c.body(null, status as RedirectStatusCode);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Respect the endpoint's declared responseType. For JSON (default)\n\t\t\t\t\t\t\t// we use c.json() which sets Content-Type and serializes. For any\n\t\t\t\t\t\t\t// other type, emit the body as-is and set the header explicitly —\n\t\t\t\t\t\t\t// metadata.headers (from r.header()) applied above still wins.\n\t\t\t\t\t\t\tif (endpoint.responseType === 'application/json') {\n\t\t\t\t\t\t\t\t// Hono 4.13 types `c.json`'s status well enough that the\n\t\t\t\t\t\t\t\t// suppression here is now itself an error.\n\t\t\t\t\t\t\t\treturn c.json(output, status);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (\n\t\t\t\t\t\t\t\t!(metadata.headers && hasContentTypeHeader(metadata.headers))\n\t\t\t\t\t\t\t) {\n\t\t\t\t\t\t\t\tc.header('content-type', endpoint.responseType);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn c.body(output as any, status);\n\t\t\t\t\t\t} catch (validationError: any) {\n\t\t\t\t\t\t\tlogger.error(\n\t\t\t\t\t\t\t\t{ err: validationError },\n\t\t\t\t\t\t\t\t'Output validation failed',\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\tconst error = wrapError(\n\t\t\t\t\t\t\t\tvalidationError,\n\t\t\t\t\t\t\t\t422,\n\t\t\t\t\t\t\t\t'Response validation failed',\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\tsetErrorHeaders(c, error);\n\t\t\t\t\t\t\tconst body = errorBody(error);\n\t\t\t\t\t\t\tif (HonoEndpoint.isDev) {\n\t\t\t\t\t\t\t\tlogger.info(\n\t\t\t\t\t\t\t\t\t{ status: error.statusCode, body },\n\t\t\t\t\t\t\t\t\t'Outgoing response',\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn c.json(body, error.statusCode as ContentfulStatusCode);\n\t\t\t\t\t\t}\n\t\t\t\t\t} catch (e: any) {\n\t\t\t\t\t\t// Under `err`, which every pino serializes as an Error — passed bare,\n\t\t\t\t\t\t// one pino does not recognise (a ZodError) is spread into the line\n\t\t\t\t\t\t// with no stack.\n\t\t\t\t\t\tlogger.error({ err: e }, 'Error processing endpoint request');\n\t\t\t\t\t\tconst error = wrapError(e, 500, 'Internal Server Error');\n\t\t\t\t\t\t// Answered here rather than rethrown, so Hono never sets it:\n\t\t\t\t\t\t// a server failure is left on the context the way Hono\n\t\t\t\t\t\t// leaves an uncaught one, for middleware — a request span\n\t\t\t\t\t\t// records it as the exception. A 4xx is the client's.\n\t\t\t\t\t\tif (error.statusCode >= 500 && e instanceof Error) c.error = e;\n\t\t\t\t\t\tsetErrorHeaders(c, error);\n\t\t\t\t\t\tconst body = errorBody(error);\n\t\t\t\t\t\tif (HonoEndpoint.isDev) {\n\t\t\t\t\t\t\tlogger.info(\n\t\t\t\t\t\t\t\t{ status: error.statusCode, body },\n\t\t\t\t\t\t\t\t'Outgoing response',\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn c.json(body, error.statusCode as ContentfulStatusCode);\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t);\n\t\t};\n\n\t\t// Register route with conditional validators.\n\t\t//\n\t\t// `route` is a generic `TRoute`, and Hono 4.13 overloads `app.get` on\n\t\t// whether the first argument is a path or a handler. A generic that is\n\t\t// only *constrained* to string does not pick the path overload, so it\n\t\t// gets matched against the handler one and reported there. Saying it is a\n\t\t// string chooses the overload that was always intended.\n\t\t//\n\t\t// The spread is why a cast is needed rather than a better type: with\n\t\t// `...validators` of unknown length, the arity is not decidable, so the\n\t\t// first overload is chosen whatever the path argument says. Naming the\n\t\t// shape we are calling states the intent the overloads cannot infer.\n\t\t(\n\t\t\tapp[method] as unknown as (\n\t\t\t\tpath: string,\n\t\t\t\t...handlers: unknown[]\n\t\t\t) => unknown\n\t\t)(route, ...validators, handler);\n\t}\n\n\tstatic addDocsRoute<\n\t\tTServices extends Service[] = [],\n\t\tTLogger extends Logger = Logger,\n\t>(\n\t\tendpoints: Endpoint<string, HttpMethod, any, any, TServices, TLogger>[],\n\t\tapp: Hono,\n\t\tdocsPath: string,\n\t\topenApiOptions?: HonoEndpointOptions['openApiOptions'],\n\t): void {\n\t\tapp.get(docsPath, async (c) => {\n\t\t\ttry {\n\t\t\t\tconst openApiSchema = await Endpoint.buildOpenApiSchema(\n\t\t\t\t\tendpoints,\n\t\t\t\t\topenApiOptions,\n\t\t\t\t);\n\n\t\t\t\treturn c.json(openApiSchema);\n\t\t\t} catch {\n\t\t\t\treturn c.json(\n\t\t\t\t\t{ error: 'Failed to generate OpenAPI documentation' },\n\t\t\t\t\t500,\n\t\t\t\t);\n\t\t\t}\n\t\t});\n\t}\n}\n\n/**\n * What a client is told about a failed request: what went wrong, and the stack\n * only under `gkm dev`. A server's stack is its file layout and its\n * dependencies' versions — for the person debugging it, not for whoever sent\n * the request. A server failure's details are the error it wrapped (an\n * environment variable it could not read, a query that failed), so they go to\n * the log and not the response either; a 4xx's details are the client's.\n */\nfunction errorBody(error: HttpError) {\n\tconst internal = error.statusCode >= 500 && !HonoEndpoint.isDev;\n\treturn {\n\t\tname: error.name,\n\t\tmessage: error.message,\n\t\tstatusCode: error.statusCode,\n\t\tstatusMessage: error.statusMessage,\n\t\tcode: error.code,\n\t\t...(internal ? {} : { details: error.details }),\n\t\t...(HonoEndpoint.isDev ? { stack: error.stack } : {}),\n\t};\n}\n\n/**\n * The headers an error says its response carries — a 429's `Retry-After` and\n * `X-RateLimit-*` — on the response that reports it.\n */\nfunction setErrorHeaders(c: Context, error: HttpError): void {\n\tfor (const [key, value] of Object.entries(error.headers ?? {})) {\n\t\tc.header(key, value);\n\t}\n}\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"rateLimit-BwHcMECR.cjs","names":[],"sources":["../src/endpoints/optionalPeers.ts","../src/endpoints/processAudits.ts","../src/endpoints/rateLimit.ts"],"sourcesContent":["/**\n * The optional peers an endpoint needs only for a feature it opted into.\n *\n * `@geekmidas/audit`, `@geekmidas/rate-limit` and `@geekmidas/db` are optional\n * peers of this package: an app that audits nothing, limits nothing and sets\n * no row-level security need not install them. The adaptor every endpoint is\n * served by used to import them at the top, so serving any endpoint needed\n * them all — and a production bundle of an app without them failed to build.\n * They are loaded here, when an endpoint that uses one is served, which is\n * also when the app declaring that endpoint has installed it: an auditor's\n * storage, a rate limit's store and an RLS policy's database come from these\n * packages.\n *\n * The `try` is load-bearing beyond the error it names: a bundler leaves an\n * import it cannot resolve to run time when the import is guarded, and fails\n * the build when it is not.\n */\n\n/** An endpoint uses a feature whose optional peer is not installed. */\nexport class OptionalPeerMissing extends Error {\n\tconstructor(\n\t\treadonly packageName: string,\n\t\treadonly feature: string,\n\t\tcause: unknown,\n\t) {\n\t\tsuper(\n\t\t\t`${feature} needs '${packageName}', which could not be loaded. ` +\n\t\t\t\t`Add it to the dependencies of the app that serves this endpoint.`,\n\t\t\t{ cause },\n\t\t);\n\t\tthis.name = 'OptionalPeerMissing';\n\t}\n}\n\n/** `@geekmidas/audit`, for an endpoint with an auditor. */\nexport async function loadAudit(): Promise<typeof import('@geekmidas/audit')> {\n\ttry {\n\t\treturn await import('@geekmidas/audit');\n\t} catch (cause) {\n\t\tthrow new OptionalPeerMissing(\n\t\t\t'@geekmidas/audit',\n\t\t\t'An endpoint with an auditor',\n\t\t\tcause,\n\t\t);\n\t}\n}\n\n/** `@geekmidas/rate-limit`, for an endpoint with a rate limit. */\nexport async function loadRateLimit(): Promise<\n\ttypeof import('@geekmidas/rate-limit')\n> {\n\ttry {\n\t\treturn await import('@geekmidas/rate-limit');\n\t} catch (cause) {\n\t\tthrow new OptionalPeerMissing(\n\t\t\t'@geekmidas/rate-limit',\n\t\t\t'An endpoint with a rate limit',\n\t\t\tcause,\n\t\t);\n\t}\n}\n\n/** `@geekmidas/db/rls`, for an endpoint with row-level security. */\nexport async function loadRls(): Promise<typeof import('@geekmidas/db/rls')> {\n\ttry {\n\t\treturn await import('@geekmidas/db/rls');\n\t} catch (cause) {\n\t\tthrow new OptionalPeerMissing(\n\t\t\t'@geekmidas/db',\n\t\t\t'An endpoint with row-level security',\n\t\t\tcause,\n\t\t);\n\t}\n}\n","import type {\n\tAuditActor,\n\tAuditableAction,\n\tAuditor,\n\tAuditStorage,\n} from '@geekmidas/audit';\nimport type { Logger } from '@geekmidas/logger';\nimport type { InferStandardSchema } from '@geekmidas/schema';\nimport type { Service, ServiceDiscovery } from '@geekmidas/services';\nimport type { StandardSchemaV1 } from '@standard-schema/spec';\nimport type { ActorExtractor, MappedAudit } from './audit';\nimport type { CookieFn, Endpoint, HeaderFn } from './Endpoint';\nimport { loadAudit } from './optionalPeers';\n\n/**\n * Process declarative audit definitions after successful endpoint execution.\n * Similar to publishConstructEvents for events.\n *\n * @param endpoint - The endpoint with audit configuration\n * @param response - The handler response to generate audit payloads from\n * @param serviceDiscovery - Service discovery for registering audit storage\n * @param logger - Logger for debug/error messages\n * @param ctx - Request context (session, headers, cookies, services)\n * @param existingAuditor - Optional existing auditor instance (e.g., from handler context).\n * If provided, uses this auditor (with its stored transaction).\n * If not provided, creates a new auditor.\n */\nexport async function processEndpointAudits<\n\tTServices extends Service[] = [],\n\tTSession = unknown,\n\tTLogger extends Logger = Logger,\n\tOutSchema extends StandardSchemaV1 | undefined = undefined,\n\tTAuditStorage extends AuditStorage | undefined = undefined,\n\tTAuditStorageServiceName extends string = string,\n\tTAuditAction extends AuditableAction<string, unknown> = AuditableAction<\n\t\tstring,\n\t\tunknown\n\t>,\n>(\n\tendpoint: Endpoint<\n\t\tany,\n\t\tany,\n\t\tany,\n\t\tOutSchema,\n\t\tTServices,\n\t\tTLogger,\n\t\tTSession,\n\t\tTAuditStorage,\n\t\tTAuditStorageServiceName,\n\t\tTAuditAction\n\t>,\n\tresponse: InferStandardSchema<OutSchema>,\n\tserviceDiscovery: ServiceDiscovery<any>,\n\tlogger: TLogger,\n\tctx: {\n\t\tsession: TSession;\n\t\theader: HeaderFn;\n\t\tcookie: CookieFn;\n\t\tservices: Record<string, unknown>;\n\t},\n\texistingAuditor?: Auditor<TAuditAction>,\n): Promise<void> {\n\ttry {\n\t\tconst audits = endpoint.audits as MappedAudit<TAuditAction, OutSchema>[];\n\n\t\t// If we have an existing auditor (from handler context), we need to flush\n\t\t// any manual audits it collected, even if there are no declarative audits\n\t\tconst hasExistingRecords =\n\t\t\texistingAuditor && existingAuditor.getRecords().length > 0;\n\n\t\t// Skip if no declarative audits and no existing records to flush\n\t\tif (!audits?.length && !hasExistingRecords) {\n\t\t\tlogger.debug('No audits to process');\n\t\t\treturn;\n\t\t}\n\n\t\t// If no auditor storage service and we have things to process, warn\n\t\tif (!endpoint.auditorStorageService) {\n\t\t\tif (hasExistingRecords || audits?.length) {\n\t\t\t\tlogger.warn('No auditor storage service available');\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\t// Get or create auditor\n\t\tlet auditor: Auditor<TAuditAction>;\n\n\t\tif (existingAuditor) {\n\t\t\t// Use existing auditor (preserves stored transaction and manual audits)\n\t\t\tauditor = existingAuditor;\n\t\t\tlogger.debug('Using existing auditor from handler context');\n\t\t} else {\n\t\t\t// Create new auditor (backward compatibility)\n\t\t\tconst services = await serviceDiscovery.register([\n\t\t\t\tendpoint.auditorStorageService,\n\t\t\t]);\n\t\t\tconst storage = services[\n\t\t\t\tendpoint.auditorStorageService.serviceName\n\t\t\t] as AuditStorage;\n\n\t\t\t// Extract actor if configured\n\t\t\tlet actor: AuditActor = { id: 'system', type: 'system' };\n\t\t\tif (endpoint.actorExtractor) {\n\t\t\t\ttry {\n\t\t\t\t\tactor = await (\n\t\t\t\t\t\tendpoint.actorExtractor as ActorExtractor<\n\t\t\t\t\t\t\tTServices,\n\t\t\t\t\t\t\tTSession,\n\t\t\t\t\t\t\tTLogger\n\t\t\t\t\t\t>\n\t\t\t\t\t)({\n\t\t\t\t\t\tservices: ctx.services as any,\n\t\t\t\t\t\tsession: ctx.session,\n\t\t\t\t\t\theader: ctx.header,\n\t\t\t\t\t\tcookie: ctx.cookie,\n\t\t\t\t\t\tlogger,\n\t\t\t\t\t});\n\t\t\t\t} catch (error) {\n\t\t\t\t\tlogger.error(error as Error, 'Failed to extract actor for audits');\n\t\t\t\t\t// Continue with system actor\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst { DefaultAuditor } = await loadAudit();\n\t\t\tauditor = new DefaultAuditor<TAuditAction>({\n\t\t\t\tactor,\n\t\t\t\tstorage,\n\t\t\t\tmetadata: {\n\t\t\t\t\tendpoint: endpoint.route,\n\t\t\t\t\tmethod: endpoint.method,\n\t\t\t\t},\n\t\t\t});\n\t\t}\n\n\t\t// Process each declarative audit\n\t\tif (audits?.length) {\n\t\t\tfor (const audit of audits) {\n\t\t\t\tlogger.debug({ audit: audit.type }, 'Processing declarative audit');\n\n\t\t\t\t// Check when condition\n\t\t\t\tif (audit.when && !audit.when(response as any)) {\n\t\t\t\t\tlogger.debug(\n\t\t\t\t\t\t{ audit: audit.type },\n\t\t\t\t\t\t'Audit skipped due to when condition',\n\t\t\t\t\t);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// Extract payload\n\t\t\t\tconst payload = audit.payload(response as any);\n\n\t\t\t\t// Extract entityId if configured\n\t\t\t\tconst entityId = audit.entityId?.(response as any);\n\n\t\t\t\t// Record the audit\n\t\t\t\tauditor.audit(audit.type as any, payload as any, {\n\t\t\t\t\ttable: audit.table,\n\t\t\t\t\tentityId,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\t// Flush audits to storage\n\t\t// Note: If existingAuditor has a stored transaction (via setTransaction),\n\t\t// flush() will use it automatically\n\t\tconst recordCount = auditor.getRecords().length;\n\t\tif (recordCount > 0) {\n\t\t\t// Check if auditor has a stored transaction (for logging purposes)\n\t\t\tconst trx =\n\t\t\t\t'getTransaction' in auditor\n\t\t\t\t\t? (auditor as { getTransaction(): unknown }).getTransaction()\n\t\t\t\t\t: undefined;\n\t\t\tlogger.debug(\n\t\t\t\t{ auditCount: recordCount, hasTransaction: !!trx },\n\t\t\t\t'Flushing audits',\n\t\t\t);\n\t\t\tawait auditor.flush();\n\t\t}\n\t} catch (error) {\n\t\tlogger.error(error as Error, 'Failed to process audits');\n\t\t// Don't rethrow - audit failures shouldn't fail the request\n\t}\n}\n\n/**\n * Context for audit-aware handler execution.\n */\nexport interface AuditExecutionContext<\n\tTAuditAction extends AuditableAction<string, unknown> = AuditableAction<\n\t\tstring,\n\t\tunknown\n\t>,\n> {\n\t/** The auditor instance for recording audits */\n\tauditor: Auditor<TAuditAction>;\n\t/** The audit storage instance */\n\tstorage: AuditStorage;\n}\n\n/**\n * Create audit context for handler execution.\n * Returns the auditor and storage for use in the handler.\n *\n * @param endpoint - The endpoint with audit configuration\n * @param serviceDiscovery - Service discovery for getting audit storage\n * @param logger - Logger for debug/error messages\n * @param ctx - Request context for actor extraction\n * @returns Audit context with auditor and storage, or undefined if not configured\n */\nexport async function createAuditContext<\n\tTServices extends Service[] = [],\n\tTSession = unknown,\n\tTLogger extends Logger = Logger,\n\tTAuditStorage extends AuditStorage | undefined = undefined,\n\tTAuditStorageServiceName extends string = string,\n\tTAuditAction extends AuditableAction<string, unknown> = AuditableAction<\n\t\tstring,\n\t\tunknown\n\t>,\n\tTDatabase = undefined,\n\tTDatabaseServiceName extends string = string,\n>(\n\tendpoint: Endpoint<\n\t\tany,\n\t\tany,\n\t\tany,\n\t\tany,\n\t\tTServices,\n\t\tTLogger,\n\t\tTSession,\n\t\tTAuditStorage,\n\t\tTAuditStorageServiceName,\n\t\tTAuditAction,\n\t\tTDatabase,\n\t\tTDatabaseServiceName\n\t>,\n\tserviceDiscovery: ServiceDiscovery<any>,\n\tlogger: TLogger,\n\tctx: {\n\t\tsession: TSession;\n\t\theader: HeaderFn;\n\t\tcookie: CookieFn;\n\t\tservices: Record<string, unknown>;\n\t},\n): Promise<AuditExecutionContext<TAuditAction> | undefined> {\n\tif (!endpoint.auditorStorageService) {\n\t\treturn undefined;\n\t}\n\n\tconst services = await serviceDiscovery.register([\n\t\tendpoint.auditorStorageService,\n\t]);\n\tconst storage = services[\n\t\tendpoint.auditorStorageService.serviceName\n\t] as AuditStorage;\n\n\t// Extract actor if configured\n\tlet actor: AuditActor = { id: 'system', type: 'system' };\n\tif (endpoint.actorExtractor) {\n\t\ttry {\n\t\t\tactor = await (\n\t\t\t\tendpoint.actorExtractor as ActorExtractor<TServices, TSession, TLogger>\n\t\t\t)({\n\t\t\t\tservices: ctx.services as any,\n\t\t\t\tsession: ctx.session,\n\t\t\t\theader: ctx.header,\n\t\t\t\tcookie: ctx.cookie,\n\t\t\t\tlogger,\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tlogger.error(error as Error, 'Failed to extract actor for audits');\n\t\t}\n\t}\n\n\tconst { DefaultAuditor } = await loadAudit();\n\tconst auditor = new DefaultAuditor<TAuditAction>({\n\t\tactor,\n\t\tstorage,\n\t\tmetadata: {\n\t\t\tendpoint: endpoint.route,\n\t\t\tmethod: endpoint.method,\n\t\t},\n\t});\n\n\treturn { auditor, storage };\n}\n\n/**\n * Options for executeWithAuditTransaction.\n */\nexport interface ExecuteWithAuditTransactionOptions {\n\t/**\n\t * Database connection to use for the transaction.\n\t * If this is already a transaction, it will be reused instead of creating a nested one.\n\t * If not provided, the storage's internal database is used.\n\t */\n\tdb?: unknown;\n}\n\n/**\n * Execute a handler with automatic audit transaction support.\n * If the audit storage provides a withTransaction method, wraps execution\n * in a transaction so audits are atomic with handler's database operations.\n *\n * This is database-agnostic - each storage implementation provides its own\n * transaction handling based on the underlying database (Kysely, Drizzle, etc.).\n *\n * If the db parameter is provided and is already a transaction, the storage\n * will reuse it instead of creating a nested transaction (similar to\n * packages/db/src/kysely.ts#withTransaction).\n *\n * @param auditContext - The audit context from createAuditContext\n * @param handler - The handler function to execute (receives auditor)\n * @param onComplete - Called after handler with response, to process declarative audits\n * @param options - Optional configuration including database connection\n * @returns The handler result\n */\nexport async function executeWithAuditTransaction<\n\tT,\n\tTAuditAction extends AuditableAction<string, unknown> = AuditableAction<\n\t\tstring,\n\t\tunknown\n\t>,\n>(\n\tauditContext: AuditExecutionContext<TAuditAction> | undefined,\n\thandler: (auditor?: Auditor<TAuditAction>) => Promise<T>,\n\tonComplete?: (response: T, auditor: Auditor<TAuditAction>) => Promise<void>,\n\toptions?: ExecuteWithAuditTransactionOptions,\n): Promise<T> {\n\t// No audit context - just run handler\n\tif (!auditContext) {\n\t\treturn handler(undefined);\n\t}\n\n\tconst { auditor, storage } = auditContext;\n\n\t// Check if storage provides a transaction wrapper\n\tif (storage.withTransaction) {\n\t\t// Wrap in transaction - audits are atomic with handler operations\n\t\t// The storage's withTransaction handles setTransaction and flush\n\t\t// Pass db so existing transactions are reused\n\t\treturn storage.withTransaction(\n\t\t\tauditor,\n\t\t\tasync () => {\n\t\t\t\tconst response = await handler(auditor);\n\n\t\t\t\t// Process declarative audits within the transaction\n\t\t\t\tif (onComplete) {\n\t\t\t\t\tawait onComplete(response, auditor);\n\t\t\t\t}\n\n\t\t\t\treturn response;\n\t\t\t},\n\t\t\toptions?.db,\n\t\t);\n\t}\n\n\t// No transaction support - run handler and flush audits after\n\tconst response = await handler(auditor);\n\n\tif (onComplete) {\n\t\tawait onComplete(response, auditor);\n\t}\n\n\t// Flush audits (no transaction)\n\tawait auditor.flush();\n\n\treturn response;\n}\n","import type { RateLimitConfig, RateLimitContext } from '@geekmidas/rate-limit';\nimport { loadRateLimit } from './optionalPeers';\n\n/**\n * An endpoint's `.rateLimit()`, checked the one way every adaptor checks it —\n * Hono, Lambda and the test adaptor — after the request is authorized.\n *\n * Under the limit, it answers the headers the response carries:\n * `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` (and\n * `Retry-After` on the last request the window allows).\n *\n * @throws {TooManyRequestsError} over the limit — `@geekmidas/errors`' 429,\n * whose `headers` hold `Retry-After` (seconds until the window resets) and the\n * same `X-RateLimit-*`, which every adaptor sets on the error response.\n */\nexport async function enforceRateLimit(\n\tconfig: RateLimitConfig,\n\tctx: RateLimitContext<any, any, any>,\n): Promise<Record<string, string>> {\n\tconst { checkRateLimit, getRateLimitHeaders } = await loadRateLimit();\n\tconst info = await checkRateLimit(config, ctx);\n\n\tconst headers: Record<string, string> = {};\n\tfor (const [key, value] of Object.entries(\n\t\tgetRateLimitHeaders(info, config),\n\t)) {\n\t\tif (value !== undefined) headers[key] = value;\n\t}\n\treturn headers;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAmBA,IAAa,sBAAb,cAAyC,MAAM;CAEpC;CACA;CAFV,YACC,aACA,SACA,OACC;EACD,MACC,GAAG,QAAQ,UAAU,YAAY,iGAEjC,EAAE,MAAM,CACT;EARS,KAAA,cAAA;EACA,KAAA,UAAA;EAQT,KAAK,OAAO;CACb;AACD;;AAGA,eAAsB,YAAwD;CAC7E,IAAI;EACH,OAAO,MAAM,OAAO;CACrB,SAAS,OAAO;EACf,MAAM,IAAI,oBACT,oBACA,+BACA,KACD;CACD;AACD;;AAGA,eAAsB,gBAEpB;CACD,IAAI;EACH,OAAO,MAAM,OAAO;CACrB,SAAS,OAAO;EACf,MAAM,IAAI,oBACT,yBACA,iCACA,KACD;CACD;AACD;;AAGA,eAAsB,UAAuD;CAC5E,IAAI;EACH,OAAO,MAAM,OAAO;CACrB,SAAS,OAAO;EACf,MAAM,IAAI,oBACT,iBACA,uCACA,KACD;CACD;AACD;;;;;;;;;;;;;ACwIA,eAAsB,mBAarB,UAcA,kBACA,QACA,KAM2D;CAC3D,IAAI,CAAC,SAAS,uBACb;CAMD,MAAM,WAAU,MAHO,iBAAiB,SAAS,CAChD,SAAS,qBACV,CAAC,EAAA,CAEA,SAAS,sBAAsB;CAIhC,IAAI,QAAoB;EAAE,IAAI;EAAU,MAAM;CAAS;CACvD,IAAI,SAAS,gBACZ,IAAI;EACH,QAAQ,MACP,SAAS,eACR;GACD,UAAU,IAAI;GACd,SAAS,IAAI;GACb,QAAQ,IAAI;GACZ,QAAQ,IAAI;GACZ;EACD,CAAC;CACF,SAAS,OAAO;EACf,OAAO,MAAM,OAAgB,oCAAoC;CAClE;CAGD,MAAM,EAAE,mBAAmB,MAAM,UAAU;CAU3C,OAAO;EAAE,SAAA,IATW,eAA6B;GAChD;GACA;GACA,UAAU;IACT,UAAU,SAAS;IACnB,QAAQ,SAAS;GAClB;EACD,CAEe;EAAG;CAAQ;AAC3B;;;;;;;;;;;;;;;;;;;AAgCA,eAAsB,4BAOrB,cACA,SACA,YACA,SACa;CAEb,IAAI,CAAC,cACJ,OAAO,QAAQ,KAAA,CAAS;CAGzB,MAAM,EAAE,SAAS,YAAY;CAG7B,IAAI,QAAQ,iBAIX,OAAO,QAAQ,gBACd,SACA,YAAY;EACX,MAAM,WAAW,MAAM,QAAQ,OAAO;EAGtC,IAAI,YACH,MAAM,WAAW,UAAU,OAAO;EAGnC,OAAO;CACR,GACA,SAAS,EACV;CAID,MAAM,WAAW,MAAM,QAAQ,OAAO;CAEtC,IAAI,YACH,MAAM,WAAW,UAAU,OAAO;CAInC,MAAM,QAAQ,MAAM;CAEpB,OAAO;AACR;;;;;;;;;;;;;;;ACjWA,eAAsB,iBACrB,QACA,KACkC;CAClC,MAAM,EAAE,gBAAgB,wBAAwB,MAAM,cAAc;CACpE,MAAM,OAAO,MAAM,eAAe,QAAQ,GAAG;CAE7C,MAAM,UAAkC,CAAC;CACzC,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QACjC,oBAAoB,MAAM,MAAM,CACjC,GACC,IAAI,UAAU,KAAA,GAAW,QAAQ,OAAO;CAEzC,OAAO;AACR"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"rateLimit-DbOTsP01.mjs","names":[],"sources":["../src/endpoints/optionalPeers.ts","../src/endpoints/processAudits.ts","../src/endpoints/rateLimit.ts"],"sourcesContent":["/**\n * The optional peers an endpoint needs only for a feature it opted into.\n *\n * `@geekmidas/audit`, `@geekmidas/rate-limit` and `@geekmidas/db` are optional\n * peers of this package: an app that audits nothing, limits nothing and sets\n * no row-level security need not install them. The adaptor every endpoint is\n * served by used to import them at the top, so serving any endpoint needed\n * them all — and a production bundle of an app without them failed to build.\n * They are loaded here, when an endpoint that uses one is served, which is\n * also when the app declaring that endpoint has installed it: an auditor's\n * storage, a rate limit's store and an RLS policy's database come from these\n * packages.\n *\n * The `try` is load-bearing beyond the error it names: a bundler leaves an\n * import it cannot resolve to run time when the import is guarded, and fails\n * the build when it is not.\n */\n\n/** An endpoint uses a feature whose optional peer is not installed. */\nexport class OptionalPeerMissing extends Error {\n\tconstructor(\n\t\treadonly packageName: string,\n\t\treadonly feature: string,\n\t\tcause: unknown,\n\t) {\n\t\tsuper(\n\t\t\t`${feature} needs '${packageName}', which could not be loaded. ` +\n\t\t\t\t`Add it to the dependencies of the app that serves this endpoint.`,\n\t\t\t{ cause },\n\t\t);\n\t\tthis.name = 'OptionalPeerMissing';\n\t}\n}\n\n/** `@geekmidas/audit`, for an endpoint with an auditor. */\nexport async function loadAudit(): Promise<typeof import('@geekmidas/audit')> {\n\ttry {\n\t\treturn await import('@geekmidas/audit');\n\t} catch (cause) {\n\t\tthrow new OptionalPeerMissing(\n\t\t\t'@geekmidas/audit',\n\t\t\t'An endpoint with an auditor',\n\t\t\tcause,\n\t\t);\n\t}\n}\n\n/** `@geekmidas/rate-limit`, for an endpoint with a rate limit. */\nexport async function loadRateLimit(): Promise<\n\ttypeof import('@geekmidas/rate-limit')\n> {\n\ttry {\n\t\treturn await import('@geekmidas/rate-limit');\n\t} catch (cause) {\n\t\tthrow new OptionalPeerMissing(\n\t\t\t'@geekmidas/rate-limit',\n\t\t\t'An endpoint with a rate limit',\n\t\t\tcause,\n\t\t);\n\t}\n}\n\n/** `@geekmidas/db/rls`, for an endpoint with row-level security. */\nexport async function loadRls(): Promise<typeof import('@geekmidas/db/rls')> {\n\ttry {\n\t\treturn await import('@geekmidas/db/rls');\n\t} catch (cause) {\n\t\tthrow new OptionalPeerMissing(\n\t\t\t'@geekmidas/db',\n\t\t\t'An endpoint with row-level security',\n\t\t\tcause,\n\t\t);\n\t}\n}\n","import type {\n\tAuditActor,\n\tAuditableAction,\n\tAuditor,\n\tAuditStorage,\n} from '@geekmidas/audit';\nimport type { Logger } from '@geekmidas/logger';\nimport type { InferStandardSchema } from '@geekmidas/schema';\nimport type { Service, ServiceDiscovery } from '@geekmidas/services';\nimport type { StandardSchemaV1 } from '@standard-schema/spec';\nimport type { ActorExtractor, MappedAudit } from './audit';\nimport type { CookieFn, Endpoint, HeaderFn } from './Endpoint';\nimport { loadAudit } from './optionalPeers';\n\n/**\n * Process declarative audit definitions after successful endpoint execution.\n * Similar to publishConstructEvents for events.\n *\n * @param endpoint - The endpoint with audit configuration\n * @param response - The handler response to generate audit payloads from\n * @param serviceDiscovery - Service discovery for registering audit storage\n * @param logger - Logger for debug/error messages\n * @param ctx - Request context (session, headers, cookies, services)\n * @param existingAuditor - Optional existing auditor instance (e.g., from handler context).\n * If provided, uses this auditor (with its stored transaction).\n * If not provided, creates a new auditor.\n */\nexport async function processEndpointAudits<\n\tTServices extends Service[] = [],\n\tTSession = unknown,\n\tTLogger extends Logger = Logger,\n\tOutSchema extends StandardSchemaV1 | undefined = undefined,\n\tTAuditStorage extends AuditStorage | undefined = undefined,\n\tTAuditStorageServiceName extends string = string,\n\tTAuditAction extends AuditableAction<string, unknown> = AuditableAction<\n\t\tstring,\n\t\tunknown\n\t>,\n>(\n\tendpoint: Endpoint<\n\t\tany,\n\t\tany,\n\t\tany,\n\t\tOutSchema,\n\t\tTServices,\n\t\tTLogger,\n\t\tTSession,\n\t\tTAuditStorage,\n\t\tTAuditStorageServiceName,\n\t\tTAuditAction\n\t>,\n\tresponse: InferStandardSchema<OutSchema>,\n\tserviceDiscovery: ServiceDiscovery<any>,\n\tlogger: TLogger,\n\tctx: {\n\t\tsession: TSession;\n\t\theader: HeaderFn;\n\t\tcookie: CookieFn;\n\t\tservices: Record<string, unknown>;\n\t},\n\texistingAuditor?: Auditor<TAuditAction>,\n): Promise<void> {\n\ttry {\n\t\tconst audits = endpoint.audits as MappedAudit<TAuditAction, OutSchema>[];\n\n\t\t// If we have an existing auditor (from handler context), we need to flush\n\t\t// any manual audits it collected, even if there are no declarative audits\n\t\tconst hasExistingRecords =\n\t\t\texistingAuditor && existingAuditor.getRecords().length > 0;\n\n\t\t// Skip if no declarative audits and no existing records to flush\n\t\tif (!audits?.length && !hasExistingRecords) {\n\t\t\tlogger.debug('No audits to process');\n\t\t\treturn;\n\t\t}\n\n\t\t// If no auditor storage service and we have things to process, warn\n\t\tif (!endpoint.auditorStorageService) {\n\t\t\tif (hasExistingRecords || audits?.length) {\n\t\t\t\tlogger.warn('No auditor storage service available');\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\t// Get or create auditor\n\t\tlet auditor: Auditor<TAuditAction>;\n\n\t\tif (existingAuditor) {\n\t\t\t// Use existing auditor (preserves stored transaction and manual audits)\n\t\t\tauditor = existingAuditor;\n\t\t\tlogger.debug('Using existing auditor from handler context');\n\t\t} else {\n\t\t\t// Create new auditor (backward compatibility)\n\t\t\tconst services = await serviceDiscovery.register([\n\t\t\t\tendpoint.auditorStorageService,\n\t\t\t]);\n\t\t\tconst storage = services[\n\t\t\t\tendpoint.auditorStorageService.serviceName\n\t\t\t] as AuditStorage;\n\n\t\t\t// Extract actor if configured\n\t\t\tlet actor: AuditActor = { id: 'system', type: 'system' };\n\t\t\tif (endpoint.actorExtractor) {\n\t\t\t\ttry {\n\t\t\t\t\tactor = await (\n\t\t\t\t\t\tendpoint.actorExtractor as ActorExtractor<\n\t\t\t\t\t\t\tTServices,\n\t\t\t\t\t\t\tTSession,\n\t\t\t\t\t\t\tTLogger\n\t\t\t\t\t\t>\n\t\t\t\t\t)({\n\t\t\t\t\t\tservices: ctx.services as any,\n\t\t\t\t\t\tsession: ctx.session,\n\t\t\t\t\t\theader: ctx.header,\n\t\t\t\t\t\tcookie: ctx.cookie,\n\t\t\t\t\t\tlogger,\n\t\t\t\t\t});\n\t\t\t\t} catch (error) {\n\t\t\t\t\tlogger.error(error as Error, 'Failed to extract actor for audits');\n\t\t\t\t\t// Continue with system actor\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst { DefaultAuditor } = await loadAudit();\n\t\t\tauditor = new DefaultAuditor<TAuditAction>({\n\t\t\t\tactor,\n\t\t\t\tstorage,\n\t\t\t\tmetadata: {\n\t\t\t\t\tendpoint: endpoint.route,\n\t\t\t\t\tmethod: endpoint.method,\n\t\t\t\t},\n\t\t\t});\n\t\t}\n\n\t\t// Process each declarative audit\n\t\tif (audits?.length) {\n\t\t\tfor (const audit of audits) {\n\t\t\t\tlogger.debug({ audit: audit.type }, 'Processing declarative audit');\n\n\t\t\t\t// Check when condition\n\t\t\t\tif (audit.when && !audit.when(response as any)) {\n\t\t\t\t\tlogger.debug(\n\t\t\t\t\t\t{ audit: audit.type },\n\t\t\t\t\t\t'Audit skipped due to when condition',\n\t\t\t\t\t);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// Extract payload\n\t\t\t\tconst payload = audit.payload(response as any);\n\n\t\t\t\t// Extract entityId if configured\n\t\t\t\tconst entityId = audit.entityId?.(response as any);\n\n\t\t\t\t// Record the audit\n\t\t\t\tauditor.audit(audit.type as any, payload as any, {\n\t\t\t\t\ttable: audit.table,\n\t\t\t\t\tentityId,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\t// Flush audits to storage\n\t\t// Note: If existingAuditor has a stored transaction (via setTransaction),\n\t\t// flush() will use it automatically\n\t\tconst recordCount = auditor.getRecords().length;\n\t\tif (recordCount > 0) {\n\t\t\t// Check if auditor has a stored transaction (for logging purposes)\n\t\t\tconst trx =\n\t\t\t\t'getTransaction' in auditor\n\t\t\t\t\t? (auditor as { getTransaction(): unknown }).getTransaction()\n\t\t\t\t\t: undefined;\n\t\t\tlogger.debug(\n\t\t\t\t{ auditCount: recordCount, hasTransaction: !!trx },\n\t\t\t\t'Flushing audits',\n\t\t\t);\n\t\t\tawait auditor.flush();\n\t\t}\n\t} catch (error) {\n\t\tlogger.error(error as Error, 'Failed to process audits');\n\t\t// Don't rethrow - audit failures shouldn't fail the request\n\t}\n}\n\n/**\n * Context for audit-aware handler execution.\n */\nexport interface AuditExecutionContext<\n\tTAuditAction extends AuditableAction<string, unknown> = AuditableAction<\n\t\tstring,\n\t\tunknown\n\t>,\n> {\n\t/** The auditor instance for recording audits */\n\tauditor: Auditor<TAuditAction>;\n\t/** The audit storage instance */\n\tstorage: AuditStorage;\n}\n\n/**\n * Create audit context for handler execution.\n * Returns the auditor and storage for use in the handler.\n *\n * @param endpoint - The endpoint with audit configuration\n * @param serviceDiscovery - Service discovery for getting audit storage\n * @param logger - Logger for debug/error messages\n * @param ctx - Request context for actor extraction\n * @returns Audit context with auditor and storage, or undefined if not configured\n */\nexport async function createAuditContext<\n\tTServices extends Service[] = [],\n\tTSession = unknown,\n\tTLogger extends Logger = Logger,\n\tTAuditStorage extends AuditStorage | undefined = undefined,\n\tTAuditStorageServiceName extends string = string,\n\tTAuditAction extends AuditableAction<string, unknown> = AuditableAction<\n\t\tstring,\n\t\tunknown\n\t>,\n\tTDatabase = undefined,\n\tTDatabaseServiceName extends string = string,\n>(\n\tendpoint: Endpoint<\n\t\tany,\n\t\tany,\n\t\tany,\n\t\tany,\n\t\tTServices,\n\t\tTLogger,\n\t\tTSession,\n\t\tTAuditStorage,\n\t\tTAuditStorageServiceName,\n\t\tTAuditAction,\n\t\tTDatabase,\n\t\tTDatabaseServiceName\n\t>,\n\tserviceDiscovery: ServiceDiscovery<any>,\n\tlogger: TLogger,\n\tctx: {\n\t\tsession: TSession;\n\t\theader: HeaderFn;\n\t\tcookie: CookieFn;\n\t\tservices: Record<string, unknown>;\n\t},\n): Promise<AuditExecutionContext<TAuditAction> | undefined> {\n\tif (!endpoint.auditorStorageService) {\n\t\treturn undefined;\n\t}\n\n\tconst services = await serviceDiscovery.register([\n\t\tendpoint.auditorStorageService,\n\t]);\n\tconst storage = services[\n\t\tendpoint.auditorStorageService.serviceName\n\t] as AuditStorage;\n\n\t// Extract actor if configured\n\tlet actor: AuditActor = { id: 'system', type: 'system' };\n\tif (endpoint.actorExtractor) {\n\t\ttry {\n\t\t\tactor = await (\n\t\t\t\tendpoint.actorExtractor as ActorExtractor<TServices, TSession, TLogger>\n\t\t\t)({\n\t\t\t\tservices: ctx.services as any,\n\t\t\t\tsession: ctx.session,\n\t\t\t\theader: ctx.header,\n\t\t\t\tcookie: ctx.cookie,\n\t\t\t\tlogger,\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tlogger.error(error as Error, 'Failed to extract actor for audits');\n\t\t}\n\t}\n\n\tconst { DefaultAuditor } = await loadAudit();\n\tconst auditor = new DefaultAuditor<TAuditAction>({\n\t\tactor,\n\t\tstorage,\n\t\tmetadata: {\n\t\t\tendpoint: endpoint.route,\n\t\t\tmethod: endpoint.method,\n\t\t},\n\t});\n\n\treturn { auditor, storage };\n}\n\n/**\n * Options for executeWithAuditTransaction.\n */\nexport interface ExecuteWithAuditTransactionOptions {\n\t/**\n\t * Database connection to use for the transaction.\n\t * If this is already a transaction, it will be reused instead of creating a nested one.\n\t * If not provided, the storage's internal database is used.\n\t */\n\tdb?: unknown;\n}\n\n/**\n * Execute a handler with automatic audit transaction support.\n * If the audit storage provides a withTransaction method, wraps execution\n * in a transaction so audits are atomic with handler's database operations.\n *\n * This is database-agnostic - each storage implementation provides its own\n * transaction handling based on the underlying database (Kysely, Drizzle, etc.).\n *\n * If the db parameter is provided and is already a transaction, the storage\n * will reuse it instead of creating a nested transaction (similar to\n * packages/db/src/kysely.ts#withTransaction).\n *\n * @param auditContext - The audit context from createAuditContext\n * @param handler - The handler function to execute (receives auditor)\n * @param onComplete - Called after handler with response, to process declarative audits\n * @param options - Optional configuration including database connection\n * @returns The handler result\n */\nexport async function executeWithAuditTransaction<\n\tT,\n\tTAuditAction extends AuditableAction<string, unknown> = AuditableAction<\n\t\tstring,\n\t\tunknown\n\t>,\n>(\n\tauditContext: AuditExecutionContext<TAuditAction> | undefined,\n\thandler: (auditor?: Auditor<TAuditAction>) => Promise<T>,\n\tonComplete?: (response: T, auditor: Auditor<TAuditAction>) => Promise<void>,\n\toptions?: ExecuteWithAuditTransactionOptions,\n): Promise<T> {\n\t// No audit context - just run handler\n\tif (!auditContext) {\n\t\treturn handler(undefined);\n\t}\n\n\tconst { auditor, storage } = auditContext;\n\n\t// Check if storage provides a transaction wrapper\n\tif (storage.withTransaction) {\n\t\t// Wrap in transaction - audits are atomic with handler operations\n\t\t// The storage's withTransaction handles setTransaction and flush\n\t\t// Pass db so existing transactions are reused\n\t\treturn storage.withTransaction(\n\t\t\tauditor,\n\t\t\tasync () => {\n\t\t\t\tconst response = await handler(auditor);\n\n\t\t\t\t// Process declarative audits within the transaction\n\t\t\t\tif (onComplete) {\n\t\t\t\t\tawait onComplete(response, auditor);\n\t\t\t\t}\n\n\t\t\t\treturn response;\n\t\t\t},\n\t\t\toptions?.db,\n\t\t);\n\t}\n\n\t// No transaction support - run handler and flush audits after\n\tconst response = await handler(auditor);\n\n\tif (onComplete) {\n\t\tawait onComplete(response, auditor);\n\t}\n\n\t// Flush audits (no transaction)\n\tawait auditor.flush();\n\n\treturn response;\n}\n","import type { RateLimitConfig, RateLimitContext } from '@geekmidas/rate-limit';\nimport { loadRateLimit } from './optionalPeers';\n\n/**\n * An endpoint's `.rateLimit()`, checked the one way every adaptor checks it —\n * Hono, Lambda and the test adaptor — after the request is authorized.\n *\n * Under the limit, it answers the headers the response carries:\n * `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` (and\n * `Retry-After` on the last request the window allows).\n *\n * @throws {TooManyRequestsError} over the limit — `@geekmidas/errors`' 429,\n * whose `headers` hold `Retry-After` (seconds until the window resets) and the\n * same `X-RateLimit-*`, which every adaptor sets on the error response.\n */\nexport async function enforceRateLimit(\n\tconfig: RateLimitConfig,\n\tctx: RateLimitContext<any, any, any>,\n): Promise<Record<string, string>> {\n\tconst { checkRateLimit, getRateLimitHeaders } = await loadRateLimit();\n\tconst info = await checkRateLimit(config, ctx);\n\n\tconst headers: Record<string, string> = {};\n\tfor (const [key, value] of Object.entries(\n\t\tgetRateLimitHeaders(info, config),\n\t)) {\n\t\tif (value !== undefined) headers[key] = value;\n\t}\n\treturn headers;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAmBA,IAAa,sBAAb,cAAyC,MAAM;CAEpC;CACA;CAFV,YACC,aACA,SACA,OACC;EACD,MACC,GAAG,QAAQ,UAAU,YAAY,iGAEjC,EAAE,MAAM,CACT;EARS,KAAA,cAAA;EACA,KAAA,UAAA;EAQT,KAAK,OAAO;CACb;AACD;;AAGA,eAAsB,YAAwD;CAC7E,IAAI;EACH,OAAO,MAAM,OAAO;CACrB,SAAS,OAAO;EACf,MAAM,IAAI,oBACT,oBACA,+BACA,KACD;CACD;AACD;;AAGA,eAAsB,gBAEpB;CACD,IAAI;EACH,OAAO,MAAM,OAAO;CACrB,SAAS,OAAO;EACf,MAAM,IAAI,oBACT,yBACA,iCACA,KACD;CACD;AACD;;AAGA,eAAsB,UAAuD;CAC5E,IAAI;EACH,OAAO,MAAM,OAAO;CACrB,SAAS,OAAO;EACf,MAAM,IAAI,oBACT,iBACA,uCACA,KACD;CACD;AACD;;;;;;;;;;;;;ACwIA,eAAsB,mBAarB,UAcA,kBACA,QACA,KAM2D;CAC3D,IAAI,CAAC,SAAS,uBACb;CAMD,MAAM,WAAU,MAHO,iBAAiB,SAAS,CAChD,SAAS,qBACV,CAAC,EAAA,CAEA,SAAS,sBAAsB;CAIhC,IAAI,QAAoB;EAAE,IAAI;EAAU,MAAM;CAAS;CACvD,IAAI,SAAS,gBACZ,IAAI;EACH,QAAQ,MACP,SAAS,eACR;GACD,UAAU,IAAI;GACd,SAAS,IAAI;GACb,QAAQ,IAAI;GACZ,QAAQ,IAAI;GACZ;EACD,CAAC;CACF,SAAS,OAAO;EACf,OAAO,MAAM,OAAgB,oCAAoC;CAClE;CAGD,MAAM,EAAE,mBAAmB,MAAM,UAAU;CAU3C,OAAO;EAAE,SAAA,IATW,eAA6B;GAChD;GACA;GACA,UAAU;IACT,UAAU,SAAS;IACnB,QAAQ,SAAS;GAClB;EACD,CAEe;EAAG;CAAQ;AAC3B;;;;;;;;;;;;;;;;;;;AAgCA,eAAsB,4BAOrB,cACA,SACA,YACA,SACa;CAEb,IAAI,CAAC,cACJ,OAAO,QAAQ,KAAA,CAAS;CAGzB,MAAM,EAAE,SAAS,YAAY;CAG7B,IAAI,QAAQ,iBAIX,OAAO,QAAQ,gBACd,SACA,YAAY;EACX,MAAM,WAAW,MAAM,QAAQ,OAAO;EAGtC,IAAI,YACH,MAAM,WAAW,UAAU,OAAO;EAGnC,OAAO;CACR,GACA,SAAS,EACV;CAID,MAAM,WAAW,MAAM,QAAQ,OAAO;CAEtC,IAAI,YACH,MAAM,WAAW,UAAU,OAAO;CAInC,MAAM,QAAQ,MAAM;CAEpB,OAAO;AACR;;;;;;;;;;;;;;;ACjWA,eAAsB,iBACrB,QACA,KACkC;CAClC,MAAM,EAAE,gBAAgB,wBAAwB,MAAM,cAAc;CACpE,MAAM,OAAO,MAAM,eAAe,QAAQ,GAAG;CAE7C,MAAM,UAAkC,CAAC;CACzC,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QACjC,oBAAoB,MAAM,MAAM,CACjC,GACC,IAAI,UAAU,KAAA,GAAW,QAAQ,OAAO;CAEzC,OAAO;AACR"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"tracing-CxaZ98IB.mjs","names":[],"sources":["../src/tracing.ts"],"sourcesContent":["/**\n * Where the constructs meet OpenTelemetry, beyond the spans `@geekmidas/events`\n * starts for the brokers: the trace context in a Lambda's records, the spans\n * around an `ExternalApi`'s client, and the context a call to a sibling\n * service carries.\n *\n * Everything goes through the global API, so without a registered provider\n * none of it records anything.\n *\n * @module\n */\nimport { channel } from 'node:diagnostics_channel';\nimport { carrierFromAttributes, type TraceCarrier } from '@geekmidas/events';\nimport {\n\tcontext,\n\tpropagation,\n\tSpanKind,\n\tSpanStatusCode,\n\ttrace,\n} from '@opentelemetry/api';\n\n/**\n * The trace context a Lambda record carries: an SQS record's message\n * attributes, an SNS record's, or those of the SNS envelope an SQS\n * subscription delivers in its body.\n */\nexport function recordCarrier(record: unknown): TraceCarrier | undefined {\n\tconst r = record as {\n\t\tmessageAttributes?: Record<string, unknown>;\n\t\tbody?: string;\n\t\tSns?: { MessageAttributes?: Record<string, unknown> };\n\t};\n\tif (r?.Sns) return carrierFromAttributes(r.Sns.MessageAttributes);\n\n\tconst own = carrierFromAttributes(r?.messageAttributes);\n\tif (own || typeof r?.body !== 'string') return own;\n\ttry {\n\t\tconst body = JSON.parse(r.body);\n\t\treturn body?.Type === 'Notification'\n\t\t\t? carrierFromAttributes(body.MessageAttributes)\n\t\t\t: undefined;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/**\n * `client`, with a span around each of its methods: `<api>.<method>`, carrying\n * the API's name. An outbound `fetch` the method makes is traced already, and\n * becomes this span's child, so the name is on the trace without anything\n * knowing which URL belongs to which API.\n *\n * Only the client's own methods: what they return is the client's business.\n * Methods are called on the client itself, never on the proxy, so private\n * fields keep working.\n */\nexport function traceClient<T>(\n\tapi: string,\n\tclient: T,\n\tattributes: Record<string, string> = {},\n): T {\n\tif (!client || (typeof client !== 'object' && typeof client !== 'function')) {\n\t\treturn client;\n\t}\n\tconst wrapped = new Map<PropertyKey, unknown>();\n\treturn new Proxy(client as object, {\n\t\tget(target, property, receiver) {\n\t\t\tconst value = Reflect.get(target, property, receiver);\n\t\t\tif (typeof value !== 'function' || typeof property === 'symbol') {\n\t\t\t\treturn value;\n\t\t\t}\n\t\t\tif (property === 'constructor' || property === 'then') return value;\n\n\t\t\tconst cached = wrapped.get(property);\n\t\t\tif (cached && (cached as { original?: unknown }).original === value) {\n\t\t\t\treturn cached;\n\t\t\t}\n\t\t\tconst traced = Object.assign(\n\t\t\t\t(...args: unknown[]) =>\n\t\t\t\t\tcallTraced(api, property, attributes, () =>\n\t\t\t\t\t\t(value as (...a: unknown[]) => unknown).apply(target, args),\n\t\t\t\t\t),\n\t\t\t\t{ original: value },\n\t\t\t);\n\t\t\twrapped.set(property, traced);\n\t\t\treturn traced;\n\t\t},\n\t}) as T;\n}\n\nfunction callTraced(\n\tapi: string,\n\tmethod: string,\n\tattributes: Record<string, string>,\n\tcall: () => unknown,\n): unknown {\n\treturn trace.getTracer('@geekmidas/constructs').startActiveSpan(\n\t\t`${api}.${method}`,\n\t\t{\n\t\t\tkind: SpanKind.INTERNAL,\n\t\t\tattributes: {\n\t\t\t\t'gkm.external_api.name': api,\n\t\t\t\t'gkm.external_api.method': method,\n\t\t\t\t...attributes,\n\t\t\t},\n\t\t},\n\t\t(span) => {\n\t\t\tconst fail = (error: unknown) => {\n\t\t\t\tif (error instanceof Error) span.recordException(error);\n\t\t\t\tspan.setStatus({ code: SpanStatusCode.ERROR });\n\t\t\t\tspan.end();\n\t\t\t};\n\t\t\tlet result: unknown;\n\t\t\ttry {\n\t\t\t\tresult = call();\n\t\t\t} catch (error) {\n\t\t\t\tfail(error);\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t\t// Only a native promise is waited on, and the caller gets it back as\n\t\t\t// it was: a lazy thenable runs on `then`, and a promise subclass has\n\t\t\t// methods a chained promise would not.\n\t\t\tif (result instanceof Promise) {\n\t\t\t\tresult.then(\n\t\t\t\t\t() => span.end(),\n\t\t\t\t\t(error) => fail(error),\n\t\t\t\t);\n\t\t\t\treturn result;\n\t\t\t}\n\t\t\tspan.end();\n\t\t\treturn result;\n\t\t},\n\t);\n}\n\n/**\n * Where an instrumentation of `fetch` hears of each request — undici's own\n * diagnostics channel, which OpenTelemetry's undici instrumentation (and every\n * APM built on it) subscribes to.\n */\nconst FETCH_REQUESTS = channel('undici:request:create');\n\n/**\n * The active trace context, written into `headers` for a call this process\n * makes to one of its own services — the auth server's session check — so the\n * callee's span joins the caller's trace.\n *\n * Through the global propagator, so it is a no-op without one. And not at all\n * when a `fetch` instrumentation is listening: that opens the request's own\n * CLIENT span and writes its `traceparent` by appending a header, so a second\n * one here would reach the callee as two values joined — `a, b` — which no\n * propagator reads, and the trace would break instead of joining.\n */\nexport function injectTraceContext(headers: Headers): void {\n\tif (FETCH_REQUESTS.hasSubscribers) return;\n\tpropagation.inject(context.active(), headers, {\n\t\tset: (carrier, key, value) => carrier.set(key, value),\n\t});\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AA0BA,SAAgB,cAAc,QAA2C;CACxE,MAAM,IAAI;CAKV,IAAI,GAAG,KAAK,OAAO,sBAAsB,EAAE,IAAI,iBAAiB;CAEhE,MAAM,MAAM,sBAAsB,GAAG,iBAAiB;CACtD,IAAI,OAAO,OAAO,GAAG,SAAS,UAAU,OAAO;CAC/C,IAAI;EACH,MAAM,OAAO,KAAK,MAAM,EAAE,IAAI;EAC9B,OAAO,MAAM,SAAS,iBACnB,sBAAsB,KAAK,iBAAiB,IAC5C,KAAA;CACJ,QAAQ;EACP;CACD;AACD;;;;;;;;;;;AAYA,SAAgB,YACf,KACA,QACA,aAAqC,CAAC,GAClC;CACJ,IAAI,CAAC,UAAW,OAAO,WAAW,YAAY,OAAO,WAAW,YAC/D,OAAO;CAER,MAAM,0BAAU,IAAI,IAA0B;CAC9C,OAAO,IAAI,MAAM,QAAkB,EAClC,IAAI,QAAQ,UAAU,UAAU;EAC/B,MAAM,QAAQ,QAAQ,IAAI,QAAQ,UAAU,QAAQ;EACpD,IAAI,OAAO,UAAU,cAAc,OAAO,aAAa,UACtD,OAAO;EAER,IAAI,aAAa,iBAAiB,aAAa,QAAQ,OAAO;EAE9D,MAAM,SAAS,QAAQ,IAAI,QAAQ;EACnC,IAAI,UAAW,OAAkC,aAAa,OAC7D,OAAO;EAER,MAAM,SAAS,OAAO,QACpB,GAAG,SACH,WAAW,KAAK,UAAU,kBACxB,MAAuC,MAAM,QAAQ,IAAI,CAC3D,GACD,EAAE,UAAU,MAAM,CACnB;EACA,QAAQ,IAAI,UAAU,MAAM;EAC5B,OAAO;CACR,EACD,CAAC;AACF;AAEA,SAAS,WACR,KACA,QACA,YACA,MACU;CACV,OAAO,MAAM,UAAU,uBAAuB,CAAC,CAAC,gBAC/C,GAAG,IAAI,GAAG,UACV;EACC,MAAM,SAAS;EACf,YAAY;GACX,yBAAyB;GACzB,2BAA2B;GAC3B,GAAG;EACJ;CACD,IACC,SAAS;EACT,MAAM,QAAQ,UAAmB;GAChC,IAAI,iBAAiB,OAAO,KAAK,gBAAgB,KAAK;GACtD,KAAK,UAAU,EAAE,MAAM,eAAe,MAAM,CAAC;GAC7C,KAAK,IAAI;EACV;EACA,IAAI;EACJ,IAAI;GACH,SAAS,KAAK;EACf,SAAS,OAAO;GACf,KAAK,KAAK;GACV,MAAM;EACP;EAIA,IAAI,kBAAkB,SAAS;GAC9B,OAAO,WACA,KAAK,IAAI,IACd,UAAU,KAAK,KAAK,CACtB;GACA,OAAO;EACR;EACA,KAAK,IAAI;EACT,OAAO;CACR,CACD;AACD;;;;;;AAOA,MAAM,iBAAiB,QAAQ,uBAAuB;;;;;;;;;;;;AAatD,SAAgB,mBAAmB,SAAwB;CAC1D,IAAI,eAAe,gBAAgB;CACnC,YAAY,OAAO,QAAQ,OAAO,GAAG,SAAS,EAC7C,MAAM,SAAS,KAAK,UAAU,QAAQ,IAAI,KAAK,KAAK,EACrD,CAAC;AACF"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"tracing-DPMidF_r.cjs","names":["carrierFromAttributes","trace","SpanKind","SpanStatusCode","channel","context"],"sources":["../src/tracing.ts"],"sourcesContent":["/**\n * Where the constructs meet OpenTelemetry, beyond the spans `@geekmidas/events`\n * starts for the brokers: the trace context in a Lambda's records, the spans\n * around an `ExternalApi`'s client, and the context a call to a sibling\n * service carries.\n *\n * Everything goes through the global API, so without a registered provider\n * none of it records anything.\n *\n * @module\n */\nimport { channel } from 'node:diagnostics_channel';\nimport { carrierFromAttributes, type TraceCarrier } from '@geekmidas/events';\nimport {\n\tcontext,\n\tpropagation,\n\tSpanKind,\n\tSpanStatusCode,\n\ttrace,\n} from '@opentelemetry/api';\n\n/**\n * The trace context a Lambda record carries: an SQS record's message\n * attributes, an SNS record's, or those of the SNS envelope an SQS\n * subscription delivers in its body.\n */\nexport function recordCarrier(record: unknown): TraceCarrier | undefined {\n\tconst r = record as {\n\t\tmessageAttributes?: Record<string, unknown>;\n\t\tbody?: string;\n\t\tSns?: { MessageAttributes?: Record<string, unknown> };\n\t};\n\tif (r?.Sns) return carrierFromAttributes(r.Sns.MessageAttributes);\n\n\tconst own = carrierFromAttributes(r?.messageAttributes);\n\tif (own || typeof r?.body !== 'string') return own;\n\ttry {\n\t\tconst body = JSON.parse(r.body);\n\t\treturn body?.Type === 'Notification'\n\t\t\t? carrierFromAttributes(body.MessageAttributes)\n\t\t\t: undefined;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/**\n * `client`, with a span around each of its methods: `<api>.<method>`, carrying\n * the API's name. An outbound `fetch` the method makes is traced already, and\n * becomes this span's child, so the name is on the trace without anything\n * knowing which URL belongs to which API.\n *\n * Only the client's own methods: what they return is the client's business.\n * Methods are called on the client itself, never on the proxy, so private\n * fields keep working.\n */\nexport function traceClient<T>(\n\tapi: string,\n\tclient: T,\n\tattributes: Record<string, string> = {},\n): T {\n\tif (!client || (typeof client !== 'object' && typeof client !== 'function')) {\n\t\treturn client;\n\t}\n\tconst wrapped = new Map<PropertyKey, unknown>();\n\treturn new Proxy(client as object, {\n\t\tget(target, property, receiver) {\n\t\t\tconst value = Reflect.get(target, property, receiver);\n\t\t\tif (typeof value !== 'function' || typeof property === 'symbol') {\n\t\t\t\treturn value;\n\t\t\t}\n\t\t\tif (property === 'constructor' || property === 'then') return value;\n\n\t\t\tconst cached = wrapped.get(property);\n\t\t\tif (cached && (cached as { original?: unknown }).original === value) {\n\t\t\t\treturn cached;\n\t\t\t}\n\t\t\tconst traced = Object.assign(\n\t\t\t\t(...args: unknown[]) =>\n\t\t\t\t\tcallTraced(api, property, attributes, () =>\n\t\t\t\t\t\t(value as (...a: unknown[]) => unknown).apply(target, args),\n\t\t\t\t\t),\n\t\t\t\t{ original: value },\n\t\t\t);\n\t\t\twrapped.set(property, traced);\n\t\t\treturn traced;\n\t\t},\n\t}) as T;\n}\n\nfunction callTraced(\n\tapi: string,\n\tmethod: string,\n\tattributes: Record<string, string>,\n\tcall: () => unknown,\n): unknown {\n\treturn trace.getTracer('@geekmidas/constructs').startActiveSpan(\n\t\t`${api}.${method}`,\n\t\t{\n\t\t\tkind: SpanKind.INTERNAL,\n\t\t\tattributes: {\n\t\t\t\t'gkm.external_api.name': api,\n\t\t\t\t'gkm.external_api.method': method,\n\t\t\t\t...attributes,\n\t\t\t},\n\t\t},\n\t\t(span) => {\n\t\t\tconst fail = (error: unknown) => {\n\t\t\t\tif (error instanceof Error) span.recordException(error);\n\t\t\t\tspan.setStatus({ code: SpanStatusCode.ERROR });\n\t\t\t\tspan.end();\n\t\t\t};\n\t\t\tlet result: unknown;\n\t\t\ttry {\n\t\t\t\tresult = call();\n\t\t\t} catch (error) {\n\t\t\t\tfail(error);\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t\t// Only a native promise is waited on, and the caller gets it back as\n\t\t\t// it was: a lazy thenable runs on `then`, and a promise subclass has\n\t\t\t// methods a chained promise would not.\n\t\t\tif (result instanceof Promise) {\n\t\t\t\tresult.then(\n\t\t\t\t\t() => span.end(),\n\t\t\t\t\t(error) => fail(error),\n\t\t\t\t);\n\t\t\t\treturn result;\n\t\t\t}\n\t\t\tspan.end();\n\t\t\treturn result;\n\t\t},\n\t);\n}\n\n/**\n * Where an instrumentation of `fetch` hears of each request — undici's own\n * diagnostics channel, which OpenTelemetry's undici instrumentation (and every\n * APM built on it) subscribes to.\n */\nconst FETCH_REQUESTS = channel('undici:request:create');\n\n/**\n * The active trace context, written into `headers` for a call this process\n * makes to one of its own services — the auth server's session check — so the\n * callee's span joins the caller's trace.\n *\n * Through the global propagator, so it is a no-op without one. And not at all\n * when a `fetch` instrumentation is listening: that opens the request's own\n * CLIENT span and writes its `traceparent` by appending a header, so a second\n * one here would reach the callee as two values joined — `a, b` — which no\n * propagator reads, and the trace would break instead of joining.\n */\nexport function injectTraceContext(headers: Headers): void {\n\tif (FETCH_REQUESTS.hasSubscribers) return;\n\tpropagation.inject(context.active(), headers, {\n\t\tset: (carrier, key, value) => carrier.set(key, value),\n\t});\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AA0BA,SAAgB,cAAc,QAA2C;CACxE,MAAM,IAAI;CAKV,IAAI,GAAG,KAAK,QAAA,GAAOA,kBAAAA,sBAAAA,CAAsB,EAAE,IAAI,iBAAiB;CAEhE,MAAM,OAAA,GAAMA,kBAAAA,sBAAAA,CAAsB,GAAG,iBAAiB;CACtD,IAAI,OAAO,OAAO,GAAG,SAAS,UAAU,OAAO;CAC/C,IAAI;EACH,MAAM,OAAO,KAAK,MAAM,EAAE,IAAI;EAC9B,OAAO,MAAM,SAAS,kBAAA,GACnBA,kBAAAA,sBAAAA,CAAsB,KAAK,iBAAiB,IAC5C,KAAA;CACJ,QAAQ;EACP;CACD;AACD;;;;;;;;;;;AAYA,SAAgB,YACf,KACA,QACA,aAAqC,CAAC,GAClC;CACJ,IAAI,CAAC,UAAW,OAAO,WAAW,YAAY,OAAO,WAAW,YAC/D,OAAO;CAER,MAAM,0BAAU,IAAI,IAA0B;CAC9C,OAAO,IAAI,MAAM,QAAkB,EAClC,IAAI,QAAQ,UAAU,UAAU;EAC/B,MAAM,QAAQ,QAAQ,IAAI,QAAQ,UAAU,QAAQ;EACpD,IAAI,OAAO,UAAU,cAAc,OAAO,aAAa,UACtD,OAAO;EAER,IAAI,aAAa,iBAAiB,aAAa,QAAQ,OAAO;EAE9D,MAAM,SAAS,QAAQ,IAAI,QAAQ;EACnC,IAAI,UAAW,OAAkC,aAAa,OAC7D,OAAO;EAER,MAAM,SAAS,OAAO,QACpB,GAAG,SACH,WAAW,KAAK,UAAU,kBACxB,MAAuC,MAAM,QAAQ,IAAI,CAC3D,GACD,EAAE,UAAU,MAAM,CACnB;EACA,QAAQ,IAAI,UAAU,MAAM;EAC5B,OAAO;CACR,EACD,CAAC;AACF;AAEA,SAAS,WACR,KACA,QACA,YACA,MACU;CACV,OAAOC,mBAAAA,MAAM,UAAU,uBAAuB,CAAC,CAAC,gBAC/C,GAAG,IAAI,GAAG,UACV;EACC,MAAMC,mBAAAA,SAAS;EACf,YAAY;GACX,yBAAyB;GACzB,2BAA2B;GAC3B,GAAG;EACJ;CACD,IACC,SAAS;EACT,MAAM,QAAQ,UAAmB;GAChC,IAAI,iBAAiB,OAAO,KAAK,gBAAgB,KAAK;GACtD,KAAK,UAAU,EAAE,MAAMC,mBAAAA,eAAe,MAAM,CAAC;GAC7C,KAAK,IAAI;EACV;EACA,IAAI;EACJ,IAAI;GACH,SAAS,KAAK;EACf,SAAS,OAAO;GACf,KAAK,KAAK;GACV,MAAM;EACP;EAIA,IAAI,kBAAkB,SAAS;GAC9B,OAAO,WACA,KAAK,IAAI,IACd,UAAU,KAAK,KAAK,CACtB;GACA,OAAO;EACR;EACA,KAAK,IAAI;EACT,OAAO;CACR,CACD;AACD;;;;;;AAOA,MAAM,kBAAA,GAAiBC,yBAAAA,QAAAA,CAAQ,uBAAuB;;;;;;;;;;;;AAatD,SAAgB,mBAAmB,SAAwB;CAC1D,IAAI,eAAe,gBAAgB;CACnC,mBAAA,YAAY,OAAOC,mBAAAA,QAAQ,OAAO,GAAG,SAAS,EAC7C,MAAM,SAAS,KAAK,UAAU,QAAQ,IAAI,KAAK,KAAK,EACrD,CAAC;AACF"}
|