@cleverbrush/server 0.0.0-beta-20260413195755

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.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/ActionResult.ts","../src/Endpoint.ts","../src/ProblemDetails.ts","../src/HttpError.ts","../src/RequestContext.ts","../src/route.ts","../src/Server.ts","../src/ContentNegotiator.ts","../src/MiddlewarePipeline.ts","../src/ParameterResolver.ts","../src/Router.ts"],"sourcesContent":["import type * as http from 'node:http';\nimport type { Readable } from 'node:stream';\nimport type { ContentNegotiator } from './ContentNegotiator.js';\n\n// ---------------------------------------------------------------------------\n// Base\n// ---------------------------------------------------------------------------\n\n/**\n * Abstract base for all HTTP action results.\n *\n * Instead of writing directly to `res`, handlers return an `ActionResult`\n * instance. The server calls `executeAsync()` after the middleware pipeline\n * completes, ensuring consistent error handling and content negotiation.\n *\n * Use the static factory methods (`ActionResult.ok()`, `.created()`, etc.)\n * rather than constructing subclasses directly.\n *\n * @example\n * ```ts\n * server.handle(GetUser, ({ params }) => {\n * const user = db.find(params.id);\n * if (!user) throw new NotFoundError();\n * return ActionResult.ok(user);\n * });\n * ```\n */\nexport abstract class ActionResult {\n abstract executeAsync(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n contentNegotiator: ContentNegotiator\n ): Promise<void>;\n\n // -----------------------------------------------------------------------\n // Factory methods\n // -----------------------------------------------------------------------\n\n /** 200 OK — serializes value using content negotiation. */\n static ok(body: unknown, headers?: Record<string, string>): JsonResult {\n return new JsonResult(body, 200, headers);\n }\n\n /** 201 Created — serializes value using content negotiation. */\n static created(\n body: unknown,\n location?: string,\n headers?: Record<string, string>\n ): JsonResult {\n const h: Record<string, string> = { ...headers };\n if (location) h['location'] = location;\n return new JsonResult(body, 201, h);\n }\n\n /** 204 No Content. */\n static noContent(): NoContentResult {\n return new NoContentResult();\n }\n\n /** Temporary (302) or permanent (301) redirect. */\n static redirect(url: string, permanent = false): RedirectResult {\n return new RedirectResult(url, permanent);\n }\n\n /** Explicit JSON response — always uses application/json regardless of Accept. */\n static json(\n body: unknown,\n status = 200,\n headers?: Record<string, string>\n ): JsonResult {\n return new JsonResult(body, status, headers);\n }\n\n /** Send a file buffer as a download attachment. */\n static file(\n content: Buffer | Uint8Array,\n fileName: string,\n contentType = 'application/octet-stream'\n ): FileResult {\n return new FileResult(content, fileName, contentType);\n }\n\n /** Arbitrary string body with an explicit content type. */\n static content(\n body: string,\n contentType: string,\n status = 200\n ): ContentResult {\n return new ContentResult(body, contentType, status);\n }\n\n /** Pipe a Readable stream to the response. */\n static stream(\n readable: Readable,\n contentType: string,\n fileName?: string\n ): StreamResult {\n return new StreamResult(readable, contentType, fileName);\n }\n\n /** Bare status code with no body. */\n static status(\n status: number,\n headers?: Record<string, string>\n ): StatusCodeResult {\n return new StatusCodeResult(status, headers);\n }\n}\n\n// ---------------------------------------------------------------------------\n// JsonResult\n// ---------------------------------------------------------------------------\n\n/**\n * Serializes a value and writes it as JSON with `content-type: application/json`,\n * bypassing content negotiation entirely.\n *\n * Created by `ActionResult.json()`.\n * `ActionResult.ok()` and `ActionResult.created()` produce a {@link JsonResult}\n * that goes through content negotiation instead.\n */\nexport class JsonResult extends ActionResult {\n readonly body: unknown;\n readonly status: number;\n readonly headers: Record<string, string>;\n\n constructor(body: unknown, status = 200, headers?: Record<string, string>) {\n super();\n this.body = body;\n this.status = status;\n this.headers = headers ?? {};\n }\n\n async executeAsync(\n _req: http.IncomingMessage,\n res: http.ServerResponse,\n _contentNegotiator: ContentNegotiator\n ): Promise<void> {\n for (const [key, value] of Object.entries(this.headers)) {\n res.setHeader(key, value);\n }\n\n if (this.body === null || this.body === undefined) {\n res.writeHead(this.status);\n res.end();\n return;\n }\n\n res.writeHead(this.status, { 'content-type': 'application/json' });\n res.end(JSON.stringify(this.body));\n }\n}\n\n// ---------------------------------------------------------------------------\n// FileResult\n// ---------------------------------------------------------------------------\n\n/**\n * Sends a binary buffer as a file download attachment.\n * Created by `ActionResult.file()`.\n */\nexport class FileResult extends ActionResult {\n readonly content: Buffer | Uint8Array;\n readonly fileName: string;\n readonly contentType: string;\n\n constructor(\n content: Buffer | Uint8Array,\n fileName: string,\n contentType = 'application/octet-stream'\n ) {\n super();\n this.content = content;\n this.fileName = fileName;\n this.contentType = contentType;\n }\n\n async executeAsync(\n _req: http.IncomingMessage,\n res: http.ServerResponse,\n _contentNegotiator: ContentNegotiator\n ): Promise<void> {\n res.writeHead(200, {\n 'content-type': this.contentType,\n 'content-disposition': `attachment; filename=\"${this.fileName}\"`,\n 'content-length': String(this.content.byteLength)\n });\n res.end(this.content);\n }\n}\n\n// ---------------------------------------------------------------------------\n// ContentResult\n// ---------------------------------------------------------------------------\n\n/**\n * Writes an arbitrary string body with a specific content type and status.\n * Created by `ActionResult.content()`.\n */\nexport class ContentResult extends ActionResult {\n readonly body: string;\n readonly contentType: string;\n readonly status: number;\n\n constructor(body: string, contentType: string, status = 200) {\n super();\n this.body = body;\n this.contentType = contentType;\n this.status = status;\n }\n\n async executeAsync(\n _req: http.IncomingMessage,\n res: http.ServerResponse,\n _contentNegotiator: ContentNegotiator\n ): Promise<void> {\n res.writeHead(this.status, { 'content-type': this.contentType });\n res.end(this.body);\n }\n}\n\n// ---------------------------------------------------------------------------\n// StreamResult\n// ---------------------------------------------------------------------------\n\n/**\n * Pipes a `Readable` stream to the HTTP response.\n * Created by `ActionResult.stream()`.\n */\nexport class StreamResult extends ActionResult {\n readonly readable: Readable;\n readonly contentType: string;\n readonly fileName: string | undefined;\n\n constructor(readable: Readable, contentType: string, fileName?: string) {\n super();\n this.readable = readable;\n this.contentType = contentType;\n this.fileName = fileName;\n }\n\n async executeAsync(\n _req: http.IncomingMessage,\n res: http.ServerResponse,\n _contentNegotiator: ContentNegotiator\n ): Promise<void> {\n const headers: Record<string, string> = {\n 'content-type': this.contentType\n };\n if (this.fileName) {\n headers['content-disposition'] =\n `attachment; filename=\"${this.fileName}\"`;\n }\n res.writeHead(200, headers);\n\n await new Promise<void>((resolve, reject) => {\n this.readable.on('error', reject);\n res.on('error', reject);\n this.readable.on('end', resolve);\n this.readable.pipe(res, { end: true });\n });\n }\n}\n\n// ---------------------------------------------------------------------------\n// StatusCodeResult\n// ---------------------------------------------------------------------------\n\n/**\n * Responds with a bare HTTP status code and no body.\n * Created by `ActionResult.status()`.\n */\nexport class StatusCodeResult extends ActionResult {\n readonly status: number;\n readonly headers: Record<string, string>;\n\n constructor(status: number, headers?: Record<string, string>) {\n super();\n this.status = status;\n this.headers = headers ?? {};\n }\n\n async executeAsync(\n _req: http.IncomingMessage,\n res: http.ServerResponse,\n _contentNegotiator: ContentNegotiator\n ): Promise<void> {\n res.writeHead(this.status, this.headers);\n res.end();\n }\n}\n\n// ---------------------------------------------------------------------------\n// RedirectResult\n// ---------------------------------------------------------------------------\n\n/**\n * Redirects the client to a new URL.\n * Uses 302 (temporary) by default; pass `permanent = true` for 301.\n * Created by `ActionResult.redirect()`.\n */\nexport class RedirectResult extends ActionResult {\n readonly url: string;\n readonly permanent: boolean;\n\n constructor(url: string, permanent = false) {\n super();\n this.url = url;\n this.permanent = permanent;\n }\n\n async executeAsync(\n _req: http.IncomingMessage,\n res: http.ServerResponse,\n _contentNegotiator: ContentNegotiator\n ): Promise<void> {\n res.writeHead(this.permanent ? 301 : 302, { location: this.url });\n res.end();\n }\n}\n\n// ---------------------------------------------------------------------------\n// NoContentResult\n// ---------------------------------------------------------------------------\n\n/**\n * Responds with 204 No Content and no body.\n * Created by `ActionResult.noContent()`.\n */\nexport class NoContentResult extends ActionResult {\n async executeAsync(\n _req: http.IncomingMessage,\n res: http.ServerResponse,\n _contentNegotiator: ContentNegotiator\n ): Promise<void> {\n res.writeHead(204);\n res.end();\n }\n}\n","import type {\n InferType,\n ObjectSchemaBuilder,\n ParseStringSchemaBuilder,\n SchemaBuilder\n} from '@cleverbrush/schema';\nimport type { ActionResult } from './ActionResult.js';\nimport type { RequestContext } from './RequestContext.js';\n\n// ---------------------------------------------------------------------------\n// Simplify — flattens intersection types for clean IDE tooltips\n// ---------------------------------------------------------------------------\n\ntype Simplify<T> = { [K in keyof T]: T[K] } & {};\n\n// ---------------------------------------------------------------------------\n// ActionContext — assembles the typed argument for a handler\n// ---------------------------------------------------------------------------\n\ntype HasKeys<T> = keyof T extends never ? false : true;\n\ntype ActionContextParts<TParams, TBody, TQuery, THeaders, TPrincipal> = {\n context: RequestContext;\n} & (HasKeys<TParams> extends true ? { params: TParams } : {}) &\n (TBody extends undefined ? {} : { body: TBody }) &\n (HasKeys<TQuery> extends true ? { query: TQuery } : {}) &\n (HasKeys<THeaders> extends true ? { headers: THeaders } : {}) &\n (TPrincipal extends undefined ? {} : { principal: TPrincipal });\n\n/**\n * The fully-typed argument object passed to endpoint handlers.\n *\n * The shape is inferred from the `EndpointBuilder` chain — only the keys\n * actually configured (body, query, headers, params, principal) are present.\n */\nexport type ActionContext<E> =\n E extends EndpointBuilder<\n infer TParams,\n infer TBody,\n infer TQuery,\n infer THeaders,\n any,\n infer TPrincipal,\n any,\n any\n >\n ? Simplify<\n ActionContextParts<TParams, TBody, TQuery, THeaders, TPrincipal>\n >\n : never;\n\n// ---------------------------------------------------------------------------\n// InferServices — maps { name: SchemaBuilder } to { name: InferType<Schema> }\n// ---------------------------------------------------------------------------\n\ntype InferServices<T> = {\n [K in keyof T]: T[K] extends SchemaBuilder<any, any, any, any, any>\n ? InferType<T[K]>\n : never;\n};\n\n/**\n * Extracts the injected service schemas map from an `EndpointBuilder` type.\n * Used internally by the `Handler` type to derive the `services` argument.\n */\nexport type ServiceSchemas<E> =\n E extends EndpointBuilder<\n any,\n any,\n any,\n any,\n infer TServices,\n any,\n any,\n any\n >\n ? TServices\n : {};\n\ntype ResponseType<E> =\n E extends EndpointBuilder<\n any,\n any,\n any,\n any,\n any,\n any,\n any,\n infer TResponse\n >\n ? TResponse\n : any;\n\n// ---------------------------------------------------------------------------\n// Handler — the action function type, inferred from an endpoint\n// ---------------------------------------------------------------------------\n\n/**\n * The handler function type inferred from an `EndpointBuilder`.\n *\n * When the endpoint has injected services, the handler receives a second\n * `services` argument with all resolved service instances.\n */\nexport type Handler<E> =\n HasKeys<ServiceSchemas<E>> extends true\n ? (\n arg: ActionContext<E>,\n services: Simplify<InferServices<ServiceSchemas<E>>>\n ) =>\n | ResponseType<E>\n | ActionResult\n | Promise<ResponseType<E> | ActionResult>\n : (\n arg: ActionContext<E>\n ) =>\n | ResponseType<E>\n | ActionResult\n | Promise<ResponseType<E> | ActionResult>;\n\n// ---------------------------------------------------------------------------\n// EndpointBuilder — immutable builder for endpoint definitions\n// ---------------------------------------------------------------------------\n\ntype RoutePath = string | ParseStringSchemaBuilder<any, any, any, any, any>;\n\n/**\n * Snapshot of all configuration set on an `EndpointBuilder`.\n * Used by the server for routing and by `@cleverbrush/server-openapi` for\n * spec generation.\n */\nexport interface EndpointMetadata {\n readonly method: string;\n readonly basePath: string;\n readonly pathTemplate: RoutePath;\n readonly bodySchema: SchemaBuilder<any, any, any, any, any> | null;\n readonly querySchema: ObjectSchemaBuilder<\n any,\n any,\n any,\n any,\n any,\n any,\n any\n > | null;\n readonly headerSchema: ObjectSchemaBuilder<\n any,\n any,\n any,\n any,\n any,\n any,\n any\n > | null;\n readonly serviceSchemas: Record<\n string,\n SchemaBuilder<any, any, any, any, any>\n > | null;\n /**\n * Authorization roles required for this endpoint.\n * - `null` → no authorization required (public)\n * - `[]` → any authenticated user\n * - `['admin', ...]` → user must have at least one of these roles\n */\n readonly authRoles: readonly string[] | null;\n readonly summary: string | null;\n readonly description: string | null;\n readonly tags: readonly string[];\n readonly operationId: string | null;\n readonly deprecated: boolean;\n readonly responseSchema: SchemaBuilder<any, any, any, any, any> | null;\n}\n\n/**\n * Immutable, fluent builder for HTTP endpoint definitions.\n *\n * All methods return a new builder instance — the original is never mutated.\n * Use the {@link endpoint} singleton (or {@link createEndpoints}) to obtain\n * the first builder in the chain.\n *\n * @example\n * ```ts\n * const GetUser = endpoint\n * .get('/api/users')\n * .query(object({ id: number().coerce() }))\n * .authorize(UserPrincipal, 'admin')\n * .returns(UserSchema)\n * .summary('Get a user by ID');\n * ```\n */\nexport class EndpointBuilder<\n TParams = {},\n TBody = undefined,\n TQuery = {},\n THeaders = {},\n TServices = {},\n TPrincipal = undefined,\n TRoles extends string = string,\n TResponse = any\n> {\n readonly #method: string;\n readonly #basePath: string;\n readonly #pathTemplate: RoutePath;\n readonly #bodySchema: SchemaBuilder<any, any, any, any, any> | null;\n readonly #querySchema: ObjectSchemaBuilder<\n any,\n any,\n any,\n any,\n any,\n any,\n any\n > | null;\n readonly #headerSchema: ObjectSchemaBuilder<\n any,\n any,\n any,\n any,\n any,\n any,\n any\n > | null;\n readonly #serviceSchemas: Record<\n string,\n SchemaBuilder<any, any, any, any, any>\n > | null;\n readonly #authRoles: readonly string[] | null;\n readonly #summary: string | null;\n readonly #description: string | null;\n readonly #tags: readonly string[];\n readonly #operationId: string | null;\n readonly #deprecated: boolean;\n readonly #responseSchema: SchemaBuilder<any, any, any, any, any> | null;\n\n constructor(\n method: string,\n basePath: string,\n pathTemplate: RoutePath,\n bodySchema: SchemaBuilder<any, any, any, any, any> | null,\n querySchema: ObjectSchemaBuilder<\n any,\n any,\n any,\n any,\n any,\n any,\n any\n > | null,\n headerSchema: ObjectSchemaBuilder<\n any,\n any,\n any,\n any,\n any,\n any,\n any\n > | null,\n serviceSchemas: Record<\n string,\n SchemaBuilder<any, any, any, any, any>\n > | null = null,\n authRoles: readonly string[] | null = null,\n summary: string | null = null,\n description: string | null = null,\n tags: readonly string[] = [],\n operationId: string | null = null,\n deprecated: boolean = false,\n responseSchema: SchemaBuilder<any, any, any, any, any> | null = null\n ) {\n this.#method = method;\n this.#basePath = basePath;\n this.#pathTemplate = pathTemplate;\n this.#bodySchema = bodySchema;\n this.#querySchema = querySchema;\n this.#headerSchema = headerSchema;\n this.#serviceSchemas = serviceSchemas;\n this.#authRoles = authRoles;\n this.#summary = summary;\n this.#description = description;\n this.#tags = tags;\n this.#operationId = operationId;\n this.#deprecated = deprecated;\n this.#responseSchema = responseSchema;\n }\n\n /** Define the request body schema. Validation failures return 422 Problem Details. */\n /** Define the request body schema. Validation failures return 422 Problem Details. */\n body<TSchema extends SchemaBuilder<any, any, any, any, any>>(\n schema: TSchema\n ): EndpointBuilder<\n TParams,\n InferType<TSchema>,\n TQuery,\n THeaders,\n TServices,\n TPrincipal,\n TRoles,\n TResponse\n > {\n return new EndpointBuilder(\n this.#method,\n this.#basePath,\n this.#pathTemplate,\n schema,\n this.#querySchema,\n this.#headerSchema,\n this.#serviceSchemas,\n this.#authRoles,\n this.#summary,\n this.#description,\n this.#tags,\n this.#operationId,\n this.#deprecated,\n this.#responseSchema\n );\n }\n\n /** Define the query string schema (must be an object schema). Validation failures return 422. */\n query<\n TSchema extends ObjectSchemaBuilder<any, any, any, any, any, any, any>\n >(\n schema: TSchema\n ): EndpointBuilder<\n TParams,\n TBody,\n InferType<TSchema>,\n THeaders,\n TServices,\n TPrincipal,\n TRoles,\n TResponse\n > {\n return new EndpointBuilder(\n this.#method,\n this.#basePath,\n this.#pathTemplate,\n this.#bodySchema,\n schema,\n this.#headerSchema,\n this.#serviceSchemas,\n this.#authRoles,\n this.#summary,\n this.#description,\n this.#tags,\n this.#operationId,\n this.#deprecated,\n this.#responseSchema\n );\n }\n\n /** Define an expected request headers schema (must be an object schema). */\n headers<\n TSchema extends ObjectSchemaBuilder<any, any, any, any, any, any, any>\n >(\n schema: TSchema\n ): EndpointBuilder<\n TParams,\n TBody,\n TQuery,\n InferType<TSchema>,\n TServices,\n TPrincipal,\n TRoles,\n TResponse\n > {\n return new EndpointBuilder(\n this.#method,\n this.#basePath,\n this.#pathTemplate,\n this.#bodySchema,\n this.#querySchema,\n schema,\n this.#serviceSchemas,\n this.#authRoles,\n this.#summary,\n this.#description,\n this.#tags,\n this.#operationId,\n this.#deprecated,\n this.#responseSchema\n );\n }\n\n /** Declare DI services to be resolved per-request and passed as the second handler argument. */\n inject<\n TSchemas extends Record<string, SchemaBuilder<any, any, any, any, any>>\n >(\n schemas: TSchemas\n ): EndpointBuilder<\n TParams,\n TBody,\n TQuery,\n THeaders,\n TSchemas,\n TPrincipal,\n TRoles,\n TResponse\n > {\n return new EndpointBuilder(\n this.#method,\n this.#basePath,\n this.#pathTemplate,\n this.#bodySchema,\n this.#querySchema,\n this.#headerSchema,\n schemas,\n this.#authRoles,\n this.#summary,\n this.#description,\n this.#tags,\n this.#operationId,\n this.#deprecated,\n this.#responseSchema\n );\n }\n\n /**\n * Mark this endpoint as requiring authorization.\n *\n * Overloads:\n * - `authorize(principalSchema, ...roles)` — typed principal, optional role requirements\n * - `authorize(...roles)` — untyped principal (`unknown`), optional role requirements\n *\n * If no roles are specified, any authenticated user is allowed.\n */\n authorize<TSchema extends SchemaBuilder<any, any, any, any, any>>(\n principalSchema: TSchema,\n ...roles: TRoles[]\n ): EndpointBuilder<\n TParams,\n TBody,\n TQuery,\n THeaders,\n TServices,\n InferType<TSchema>,\n TRoles,\n TResponse\n >;\n authorize(\n ...roles: TRoles[]\n ): EndpointBuilder<\n TParams,\n TBody,\n TQuery,\n THeaders,\n TServices,\n unknown,\n TRoles,\n TResponse\n >;\n authorize(\n ...args: unknown[]\n ): EndpointBuilder<\n TParams,\n TBody,\n TQuery,\n THeaders,\n TServices,\n any,\n TRoles,\n TResponse\n > {\n let roles: string[];\n if (\n args.length > 0 &&\n typeof args[0] === 'object' &&\n args[0] !== null &&\n 'introspect' in args[0]\n ) {\n // First argument is a schema — remaining are roles\n roles = args.slice(1) as string[];\n } else {\n roles = args as string[];\n }\n\n // Merge with inherited auth roles\n const merged = this.#authRoles ? [...this.#authRoles, ...roles] : roles;\n\n return new EndpointBuilder(\n this.#method,\n this.#basePath,\n this.#pathTemplate,\n this.#bodySchema,\n this.#querySchema,\n this.#headerSchema,\n this.#serviceSchemas,\n merged,\n this.#summary,\n this.#description,\n this.#tags,\n this.#operationId,\n this.#deprecated,\n this.#responseSchema\n );\n }\n\n /**\n * Declare the response type for OpenAPI spec generation.\n *\n * Overloads:\n * - `returns<T>()` — generic type only, no runtime schema\n * - `returns(schema)` — provides a schema for spec generation and type inference\n */\n returns<T>(): EndpointBuilder<\n TParams,\n TBody,\n TQuery,\n THeaders,\n TServices,\n TPrincipal,\n TRoles,\n T\n >;\n returns<TSchema extends SchemaBuilder<any, any, any, any, any>>(\n schema: TSchema\n ): EndpointBuilder<\n TParams,\n TBody,\n TQuery,\n THeaders,\n TServices,\n TPrincipal,\n TRoles,\n InferType<TSchema>\n >;\n returns(\n _schema?: unknown\n ): EndpointBuilder<any, any, any, any, any, any, any, any> {\n const schema =\n _schema != null &&\n typeof _schema === 'object' &&\n 'introspect' in _schema\n ? (_schema as SchemaBuilder<any, any, any, any, any>)\n : null;\n return new EndpointBuilder(\n this.#method,\n this.#basePath,\n this.#pathTemplate,\n this.#bodySchema,\n this.#querySchema,\n this.#headerSchema,\n this.#serviceSchemas,\n this.#authRoles,\n this.#summary,\n this.#description,\n this.#tags,\n this.#operationId,\n this.#deprecated,\n schema ?? this.#responseSchema\n );\n }\n\n /** Short, human-readable summary for OpenAPI operation objects. */\n summary(\n text: string\n ): EndpointBuilder<\n TParams,\n TBody,\n TQuery,\n THeaders,\n TServices,\n TPrincipal,\n TRoles,\n TResponse\n > {\n return new EndpointBuilder(\n this.#method,\n this.#basePath,\n this.#pathTemplate,\n this.#bodySchema,\n this.#querySchema,\n this.#headerSchema,\n this.#serviceSchemas,\n this.#authRoles,\n text,\n this.#description,\n this.#tags,\n this.#operationId,\n this.#deprecated,\n this.#responseSchema\n );\n }\n\n /** Longer description for OpenAPI operation objects. Supports Markdown. */\n description(\n text: string\n ): EndpointBuilder<\n TParams,\n TBody,\n TQuery,\n THeaders,\n TServices,\n TPrincipal,\n TRoles,\n TResponse\n > {\n return new EndpointBuilder(\n this.#method,\n this.#basePath,\n this.#pathTemplate,\n this.#bodySchema,\n this.#querySchema,\n this.#headerSchema,\n this.#serviceSchemas,\n this.#authRoles,\n this.#summary,\n text,\n this.#tags,\n this.#operationId,\n this.#deprecated,\n this.#responseSchema\n );\n }\n\n /** OpenAPI tags grouping this operation in generated documentation. */\n tags(\n ...tags: string[]\n ): EndpointBuilder<\n TParams,\n TBody,\n TQuery,\n THeaders,\n TServices,\n TPrincipal,\n TRoles,\n TResponse\n > {\n return new EndpointBuilder(\n this.#method,\n this.#basePath,\n this.#pathTemplate,\n this.#bodySchema,\n this.#querySchema,\n this.#headerSchema,\n this.#serviceSchemas,\n this.#authRoles,\n this.#summary,\n this.#description,\n tags,\n this.#operationId,\n this.#deprecated,\n this.#responseSchema\n );\n }\n\n /** A unique, stable identifier for this operation in OpenAPI spec. */\n operationId(\n id: string\n ): EndpointBuilder<\n TParams,\n TBody,\n TQuery,\n THeaders,\n TServices,\n TPrincipal,\n TRoles,\n TResponse\n > {\n return new EndpointBuilder(\n this.#method,\n this.#basePath,\n this.#pathTemplate,\n this.#bodySchema,\n this.#querySchema,\n this.#headerSchema,\n this.#serviceSchemas,\n this.#authRoles,\n this.#summary,\n this.#description,\n this.#tags,\n id,\n this.#deprecated,\n this.#responseSchema\n );\n }\n\n /** Mark this endpoint as deprecated in OpenAPI spec output. */\n deprecated(): EndpointBuilder<\n TParams,\n TBody,\n TQuery,\n THeaders,\n TServices,\n TPrincipal,\n TRoles,\n TResponse\n > {\n return new EndpointBuilder(\n this.#method,\n this.#basePath,\n this.#pathTemplate,\n this.#bodySchema,\n this.#querySchema,\n this.#headerSchema,\n this.#serviceSchemas,\n this.#authRoles,\n this.#summary,\n this.#description,\n this.#tags,\n this.#operationId,\n true,\n this.#responseSchema\n );\n }\n\n /** Return an immutable snapshot of this builder's configuration as {@link EndpointMetadata}. */\n introspect(): EndpointMetadata {\n return {\n method: this.#method,\n basePath: this.#basePath,\n pathTemplate: this.#pathTemplate,\n bodySchema: this.#bodySchema,\n querySchema: this.#querySchema,\n headerSchema: this.#headerSchema,\n serviceSchemas: this.#serviceSchemas,\n authRoles: this.#authRoles,\n summary: this.#summary,\n description: this.#description,\n tags: this.#tags,\n operationId: this.#operationId,\n deprecated: this.#deprecated,\n responseSchema: this.#responseSchema\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// endpoint factory — creates EndpointBuilder instances\n// ---------------------------------------------------------------------------\n\n/**\n * Optional OpenAPI metadata fields accepted by `createEndpoint` / `createEndpoints`.\n */\nexport type EndpointMetadataDescriptors = {\n readonly summary?: string;\n readonly description?: string;\n readonly tags?: string[];\n readonly operationId?: string;\n readonly deprecated?: boolean;\n};\n\nfunction createEndpoint<TParams>(\n method: string,\n basePath: string,\n pathTemplate?: ParseStringSchemaBuilder<TParams, any, any, any, any>,\n authRoles?: readonly string[] | null,\n meta?: EndpointMetadataDescriptors\n): EndpointBuilder<TParams extends undefined ? {} : TParams>;\n\nfunction createEndpoint(\n method: string,\n basePath: string,\n pathTemplate?: RoutePath,\n authRoles?: readonly string[] | null,\n meta?: EndpointMetadataDescriptors\n): EndpointBuilder<any> {\n return new EndpointBuilder(\n method,\n basePath,\n pathTemplate ?? '/',\n null,\n null,\n null,\n null,\n authRoles ?? null,\n meta?.summary ?? null,\n meta?.description ?? null,\n meta?.tags ?? [],\n meta?.operationId ?? null,\n meta?.deprecated ?? false,\n null\n );\n}\n\n// ---------------------------------------------------------------------------\n// ScopedEndpointFactory — resource-scoped endpoint creation\n// ---------------------------------------------------------------------------\n\ntype ScopedEndpointFactoryMethods<\n TPrincipal,\n TRoles extends string = string\n> = {\n get<TParams = {}>(\n pathTemplate?: ParseStringSchemaBuilder<TParams, any, any, any, any>\n ): EndpointBuilder<TParams, undefined, {}, {}, {}, TPrincipal, TRoles>;\n post<TParams = {}>(\n pathTemplate?: ParseStringSchemaBuilder<TParams, any, any, any, any>\n ): EndpointBuilder<TParams, undefined, {}, {}, {}, TPrincipal, TRoles>;\n put<TParams = {}>(\n pathTemplate?: ParseStringSchemaBuilder<TParams, any, any, any, any>\n ): EndpointBuilder<TParams, undefined, {}, {}, {}, TPrincipal, TRoles>;\n patch<TParams = {}>(\n pathTemplate?: ParseStringSchemaBuilder<TParams, any, any, any, any>\n ): EndpointBuilder<TParams, undefined, {}, {}, {}, TPrincipal, TRoles>;\n delete<TParams = {}>(\n pathTemplate?: ParseStringSchemaBuilder<TParams, any, any, any, any>\n ): EndpointBuilder<TParams, undefined, {}, {}, {}, TPrincipal, TRoles>;\n head<TParams = {}>(\n pathTemplate?: ParseStringSchemaBuilder<TParams, any, any, any, any>\n ): EndpointBuilder<TParams, undefined, {}, {}, {}, TPrincipal, TRoles>;\n options<TParams = {}>(\n pathTemplate?: ParseStringSchemaBuilder<TParams, any, any, any, any>\n ): EndpointBuilder<TParams, undefined, {}, {}, {}, TPrincipal, TRoles>;\n};\n\nexport type ScopedEndpointFactory<TRoles extends string = string> =\n ScopedEndpointFactoryMethods<undefined, TRoles> & {\n /**\n * Returns a new resource factory where all endpoints inherit\n * the given authorization requirements.\n *\n * - `authorize(principalSchema, ...roles)` — typed principal\n * - `authorize(...roles)` — untyped principal\n */\n authorize<TSchema extends SchemaBuilder<any, any, any, any, any>>(\n principalSchema: TSchema,\n ...roles: TRoles[]\n ): ScopedEndpointFactoryMethods<InferType<TSchema>, TRoles>;\n authorize(\n ...roles: TRoles[]\n ): ScopedEndpointFactoryMethods<unknown, TRoles>;\n };\n\nfunction createScopedFactoryMethods(\n basePath: string,\n authRoles: readonly string[] | null\n): ScopedEndpointFactoryMethods<any> {\n return {\n get: (pathTemplate?) =>\n createEndpoint('GET', basePath, pathTemplate, authRoles),\n post: (pathTemplate?) =>\n createEndpoint('POST', basePath, pathTemplate, authRoles),\n put: (pathTemplate?) =>\n createEndpoint('PUT', basePath, pathTemplate, authRoles),\n patch: (pathTemplate?) =>\n createEndpoint('PATCH', basePath, pathTemplate, authRoles),\n delete: (pathTemplate?) =>\n createEndpoint('DELETE', basePath, pathTemplate, authRoles),\n head: (pathTemplate?) =>\n createEndpoint('HEAD', basePath, pathTemplate, authRoles),\n options: (pathTemplate?) =>\n createEndpoint('OPTIONS', basePath, pathTemplate, authRoles)\n };\n}\n\nfunction createScopedFactory(basePath: string): ScopedEndpointFactory {\n return {\n ...createScopedFactoryMethods(basePath, null),\n authorize(...args: unknown[]): ScopedEndpointFactoryMethods<any> {\n let roles: string[];\n if (\n args.length > 0 &&\n typeof args[0] === 'object' &&\n args[0] !== null &&\n 'introspect' in args[0]\n ) {\n roles = args.slice(1) as string[];\n } else {\n roles = args as string[];\n }\n return createScopedFactoryMethods(basePath, roles);\n }\n };\n}\n\n// ---------------------------------------------------------------------------\n// EndpointFactory — top-level endpoint creation\n// ---------------------------------------------------------------------------\n\ntype EndpointFactory<TRoles extends string = string> = {\n get<TParams = {}>(\n basePath: string,\n pathTemplate?: ParseStringSchemaBuilder<TParams, any, any, any, any>\n ): EndpointBuilder<TParams, undefined, {}, {}, {}, undefined, TRoles>;\n post<TParams = {}>(\n basePath: string,\n pathTemplate?: ParseStringSchemaBuilder<TParams, any, any, any, any>\n ): EndpointBuilder<TParams, undefined, {}, {}, {}, undefined, TRoles>;\n put<TParams = {}>(\n basePath: string,\n pathTemplate?: ParseStringSchemaBuilder<TParams, any, any, any, any>\n ): EndpointBuilder<TParams, undefined, {}, {}, {}, undefined, TRoles>;\n patch<TParams = {}>(\n basePath: string,\n pathTemplate?: ParseStringSchemaBuilder<TParams, any, any, any, any>\n ): EndpointBuilder<TParams, undefined, {}, {}, {}, undefined, TRoles>;\n delete<TParams = {}>(\n basePath: string,\n pathTemplate?: ParseStringSchemaBuilder<TParams, any, any, any, any>\n ): EndpointBuilder<TParams, undefined, {}, {}, {}, undefined, TRoles>;\n head<TParams = {}>(\n basePath: string,\n pathTemplate?: ParseStringSchemaBuilder<TParams, any, any, any, any>\n ): EndpointBuilder<TParams, undefined, {}, {}, {}, undefined, TRoles>;\n options<TParams = {}>(\n basePath: string,\n pathTemplate?: ParseStringSchemaBuilder<TParams, any, any, any, any>\n ): EndpointBuilder<TParams, undefined, {}, {}, {}, undefined, TRoles>;\n resource(basePath: string): ScopedEndpointFactory<TRoles>;\n};\n\n/**\n * Create a role-constrained endpoint factory. Roles are defined as a plain\n * `as const` object whose *values* become the string-literal union accepted\n * by `authorize()`.\n *\n * @example\n * ```ts\n * const Roles = { admin: 'admin', editor: 'editor' } as const;\n * const ep = createEndpoints(Roles);\n * ep.get('/api/admin').authorize(IPrincipal, 'admin'); // ✓\n * ep.get('/api/admin').authorize(IPrincipal, 'typo'); // ✗ type error\n * ```\n */\nexport function createEndpoints<const T extends Record<string, string>>(\n _roles: T\n): EndpointFactory<T[keyof T]> {\n return endpoint as EndpointFactory<T[keyof T]>;\n}\n\n/**\n * The global endpoint factory singleton.\n *\n * Creates `EndpointBuilder` instances for each HTTP method. Use\n * {@link createEndpoints} to get a role-constrained version.\n *\n * @example\n * ```ts\n * import { endpoint } from '@cleverbrush/server';\n *\n * const GetUsers = endpoint.get('/api/users');\n * const CreateUser = endpoint.post('/api/users').body(CreateUserSchema);\n * ```\n */\nexport const endpoint: EndpointFactory = {\n get: (basePath, pathTemplate?) =>\n createEndpoint('GET', basePath, pathTemplate),\n post: (basePath, pathTemplate?) =>\n createEndpoint('POST', basePath, pathTemplate),\n put: (basePath, pathTemplate?) =>\n createEndpoint('PUT', basePath, pathTemplate),\n patch: (basePath, pathTemplate?) =>\n createEndpoint('PATCH', basePath, pathTemplate),\n delete: (basePath, pathTemplate?) =>\n createEndpoint('DELETE', basePath, pathTemplate),\n head: (basePath, pathTemplate?) =>\n createEndpoint('HEAD', basePath, pathTemplate),\n options: (basePath, pathTemplate?) =>\n createEndpoint('OPTIONS', basePath, pathTemplate),\n resource: createScopedFactory\n};\n","// ---------------------------------------------------------------------------\n// RFC 9457 Problem Details\n// ---------------------------------------------------------------------------\n\n/**\n * An RFC 9457 (formerly RFC 7807) Problem Details object.\n *\n * Provides machine-readable error information in HTTP API responses.\n * Serialized as `application/problem+json`.\n *\n * @see {@link https://www.rfc-editor.org/rfc/rfc9457 RFC 9457}\n */\nexport interface ProblemDetails {\n readonly type: string;\n readonly status: number;\n readonly title: string;\n readonly detail?: string;\n readonly instance?: string;\n readonly [extension: string]: unknown;\n}\n\nconst STATUS_TITLES: Record<number, string> = {\n 400: 'Bad Request',\n 401: 'Unauthorized',\n 403: 'Forbidden',\n 404: 'Not Found',\n 405: 'Method Not Allowed',\n 409: 'Conflict',\n 415: 'Unsupported Media Type',\n 422: 'Unprocessable Content',\n 500: 'Internal Server Error',\n 503: 'Service Unavailable'\n};\n\n/**\n * Create a {@link ProblemDetails} object for the given HTTP status code.\n *\n * @param status - HTTP status code (e.g. 400, 404, 500).\n * @param title - Short, human-readable summary. Defaults to a standard phrase\n * for common status codes.\n * @param detail - Longer explanation specific to this occurrence.\n * @param extensions - Extra fields merged into the object (RFC 9457 §3.1).\n */\nexport function createProblemDetails(\n status: number,\n title?: string,\n detail?: string,\n extensions?: Record<string, unknown>\n): ProblemDetails {\n return {\n type: `https://httpstatuses.com/${status}`,\n status,\n title: title ?? STATUS_TITLES[status] ?? 'Error',\n ...(detail !== undefined ? { detail } : {}),\n ...extensions\n };\n}\n\n/**\n * A single field-level validation error, used in validation Problem Details\n * responses. `pointer` follows JSON Pointer syntax (RFC 6901).\n *\n * @example `{ pointer: '/body/email', detail: 'Must be a valid email address' }`\n */\nexport interface ValidationErrorItem {\n readonly pointer: string;\n readonly detail: string;\n}\n\n/**\n * Create a 400 Bad Request Problem Details object listing all validation\n * field errors.\n *\n * @param errors - Array of per-field errors with JSON Pointer paths.\n */\nexport function createValidationProblemDetails(\n errors: readonly ValidationErrorItem[]\n): ProblemDetails {\n return createProblemDetails(\n 400,\n 'Bad Request',\n 'One or more validation errors occurred.',\n { errors }\n );\n}\n\n/**\n * Serialize a {@link ProblemDetails} object to a JSON string.\n */\nexport function serializeProblemDetails(pd: ProblemDetails): string {\n return JSON.stringify(pd);\n}\n\n/** The MIME type for Problem Details JSON responses (`application/problem+json`). */\nexport const PROBLEM_JSON_CONTENT_TYPE = 'application/problem+json';\n","import { createProblemDetails, type ProblemDetails } from './ProblemDetails.js';\n\n/**\n * Base class for HTTP errors thrown from endpoint handlers.\n *\n * Instances are automatically caught by the server and serialized as\n * RFC 9457 Problem Details (`application/problem+json`) responses.\n *\n * @example\n * ```ts\n * throw new HttpError(429, 'Too Many Requests', 'Rate limit exceeded.');\n * ```\n */\nexport class HttpError extends Error {\n readonly status: number;\n readonly title: string;\n readonly detail?: string;\n readonly extensions?: Record<string, unknown>;\n\n constructor(\n status: number,\n title?: string,\n detail?: string,\n extensions?: Record<string, unknown>\n ) {\n super(detail ?? title ?? `HTTP ${status}`);\n this.name = 'HttpError';\n this.status = status;\n this.title = title ?? `HTTP ${status}`;\n this.detail = detail;\n this.extensions = extensions;\n }\n\n /** Converts this error into an RFC 9457 {@link ProblemDetails} object. */\n toProblemDetails(): ProblemDetails {\n return createProblemDetails(\n this.status,\n this.title,\n this.detail,\n this.extensions\n );\n }\n}\n\n/** Thrown when a requested resource cannot be found. Produces a 404 response. */\nexport class NotFoundError extends HttpError {\n constructor(detail?: string) {\n super(404, 'Not Found', detail);\n this.name = 'NotFoundError';\n }\n}\n\n/** Thrown when the request is malformed or fails validation. Produces a 400 response. */\nexport class BadRequestError extends HttpError {\n constructor(detail?: string) {\n super(400, 'Bad Request', detail);\n this.name = 'BadRequestError';\n }\n}\n\n/** Thrown when the request lacks valid authentication credentials. Produces a 401 response. */\nexport class UnauthorizedError extends HttpError {\n constructor(detail?: string) {\n super(401, 'Unauthorized', detail);\n this.name = 'UnauthorizedError';\n }\n}\n\n/** Thrown when the authenticated principal lacks permission. Produces a 403 response. */\nexport class ForbiddenError extends HttpError {\n constructor(detail?: string) {\n super(403, 'Forbidden', detail);\n this.name = 'ForbiddenError';\n }\n}\n\n/** Thrown when the request conflicts with the current state of the resource. Produces a 409 response. */\nexport class ConflictError extends HttpError {\n constructor(detail?: string) {\n super(409, 'Conflict', detail);\n this.name = 'ConflictError';\n }\n}\n","import type { IncomingMessage, ServerResponse } from 'node:http';\nimport { URL } from 'node:url';\nimport type { IServiceProvider } from '@cleverbrush/di';\nimport {\n any,\n boolean,\n func,\n object,\n promise,\n record,\n string\n} from '@cleverbrush/schema';\n\n/**\n * IRequestContext — the schema definition for the request context.\n * Serves as both a DI key and a type definition.\n */\nexport const IRequestContext = object({\n method: string(),\n url: string(),\n pathParams: record(string(), string()),\n queryParams: record(string(), string()),\n headers: record(string(), string()),\n items: any(),\n body: func().hasReturnType(promise(any())),\n json: func().hasReturnType(promise(any())),\n responded: boolean()\n});\n\n/**\n * Per-request context object passed to every middleware and endpoint handler.\n *\n * Provides typed access to path/query parameters, headers, the request body,\n * and the DI service provider for the current request scope.\n *\n * @example\n * ```ts\n * const middleware: Middleware = async (ctx, next) => {\n * ctx.items.set('startTime', Date.now());\n * await next();\n * };\n * ```\n */\nexport class RequestContext {\n readonly request: IncomingMessage;\n readonly response: ServerResponse;\n readonly url: URL;\n readonly method: string;\n readonly headers: Record<string, string>;\n readonly items: Map<string, unknown> = new Map();\n\n #pathParams: Record<string, string> = {};\n /** @internal — overridable for testing */\n _queryParams?: Record<string, string>;\n #services?: IServiceProvider;\n #bodyBuffer: Buffer | null = null;\n #bodyRead = false;\n #jsonCache: unknown = undefined;\n #jsonParsed = false;\n responded = false;\n\n /**\n * The authenticated principal for this request.\n * Set by authentication middleware; typed as `unknown` at the\n * RequestContext level — handlers receive a fully typed version\n * via `ActionContext.principal`.\n */\n principal: unknown = undefined;\n\n constructor(request: IncomingMessage, response: ServerResponse) {\n this.request = request;\n this.response = response;\n this.method = (request.method ?? 'GET').toUpperCase();\n\n // Parse URL — use a placeholder host for relative URLs\n const rawUrl = request.url ?? '/';\n this.url = new URL(\n rawUrl,\n `http://${request.headers.host ?? 'localhost'}`\n );\n\n // Build headers record (lowercased keys, string values)\n const headers: Record<string, string> = {};\n for (const [key, value] of Object.entries(request.headers)) {\n if (typeof value === 'string') {\n headers[key] = value;\n } else if (Array.isArray(value)) {\n headers[key] = value.join(', ');\n }\n }\n this.headers = headers;\n }\n\n /** Path parameters extracted from the matched route template. */\n get pathParams(): Record<string, string> {\n return this.#pathParams;\n }\n\n set pathParams(value: Record<string, string>) {\n this.#pathParams = value;\n }\n\n /** Parsed query string parameters from the request URL. */\n get queryParams(): Record<string, string> {\n if (this._queryParams) return this._queryParams;\n const params: Record<string, string> = {};\n for (const [key, value] of this.url.searchParams) {\n params[key] = value;\n }\n return params;\n }\n\n /** The DI service provider scoped to this request. Set by the server before invoking the handler. */\n get services(): IServiceProvider | undefined {\n return this.#services;\n }\n\n set services(value: IServiceProvider) {\n this.#services = value;\n }\n\n /** Read and buffer the raw request body. Result is cached after the first call. */\n async body(): Promise<Buffer> {\n if (this.#bodyRead) return this.#bodyBuffer!;\n\n this.#bodyBuffer = await new Promise<Buffer>((resolve, reject) => {\n const chunks: Buffer[] = [];\n this.request.on('data', (chunk: Buffer) => chunks.push(chunk));\n this.request.on('end', () => resolve(Buffer.concat(chunks)));\n this.request.on('error', reject);\n });\n this.#bodyRead = true;\n return this.#bodyBuffer;\n }\n\n /** Read, buffer, and JSON-parse the request body. Result is cached after the first call. */\n async json(): Promise<unknown> {\n if (this.#jsonParsed) return this.#jsonCache;\n\n const buf = await this.body();\n const text = buf.toString('utf-8');\n this.#jsonCache = text.length > 0 ? JSON.parse(text) : undefined;\n this.#jsonParsed = true;\n return this.#jsonCache;\n }\n}\n","import {\n type InferType,\n type ObjectSchemaBuilder,\n object,\n type ParseStringSchemaBuilder,\n type ParseStringTemplateTag,\n type PropertyDescriptor,\n type PropertyDescriptorTree,\n parseString,\n type SchemaBuilder\n} from '@cleverbrush/schema';\n\ntype RouteTemplateTag<\n TProps extends Record<string, SchemaBuilder<any, any, any, any, any>>\n> = (\n strings: TemplateStringsArray,\n ...selectors: Array<\n (\n tree: PropertyDescriptorTree<\n ObjectSchemaBuilder<\n TProps,\n true,\n false,\n undefined,\n false,\n {},\n []\n >,\n ObjectSchemaBuilder<\n TProps,\n true,\n false,\n undefined,\n false,\n {},\n []\n >,\n string | number | boolean | Date\n >\n ) => PropertyDescriptor<\n ObjectSchemaBuilder<TProps, true, false, undefined, false, {}, []>,\n any,\n any\n >\n >\n) => ParseStringSchemaBuilder<\n InferType<\n ObjectSchemaBuilder<TProps, true, false, undefined, false, {}, []>\n >\n>;\n\nfunction createRouteTag<\n TProps extends Record<string, SchemaBuilder<any, any, any, any, any>>\n>(props: TProps): RouteTemplateTag<TProps> {\n type TSchema = ObjectSchemaBuilder<\n TProps,\n true,\n false,\n undefined,\n false,\n {},\n []\n >;\n const objectSchema = object(props) as unknown as TSchema;\n\n return ((strings: TemplateStringsArray, ...selectors: any[]) =>\n parseString(objectSchema, ($t: ParseStringTemplateTag<TSchema>) =>\n ($t as any)(strings, ...selectors)\n )) as any;\n}\n\n// Overload: route`/some/path` — used directly as a tagged template (no params)\nexport function route(\n strings: TemplateStringsArray,\n ...selectors: never[]\n): ParseStringSchemaBuilder<\n InferType<ObjectSchemaBuilder<{}, true, false, undefined, false, {}, []>>\n>;\n\n// Overload: route() — called with no args, returns a tagged template (no params)\nexport function route(): RouteTemplateTag<{}>;\n\n// Overload: route({ id: number() }) — called with props, returns a tagged template\nexport function route<\n TProps extends Record<string, SchemaBuilder<any, any, any, any, any>>\n>(props: TProps): RouteTemplateTag<TProps>;\n\n/**\n * Concise shorthand for defining a typed path template.\n *\n * @example With parameters\n * ```ts\n * const TodoById = route({ id: number().coerce() })`/${t => t.id}`;\n * ```\n *\n * @example Static path (no parameters)\n * ```ts\n * const Path = route`/some/path`;\n * // or\n * const Path = route()`/some/path`;\n * ```\n *\n * @param propsOrStrings - Either a property map for typed path segments,\n * or a `TemplateStringsArray` when used directly as a tagged template.\n * @returns A `ParseStringSchemaBuilder`, or a tagged-template function\n * that produces one.\n */\nexport function route(propsOrStrings?: any, ..._rest: any[]): any {\n // route`/some/path` — called as tagged template directly\n if (\n propsOrStrings != null &&\n Array.isArray((propsOrStrings as TemplateStringsArray).raw)\n ) {\n return createRouteTag({})(propsOrStrings as TemplateStringsArray);\n }\n\n // route() or route({...})\n return createRouteTag(propsOrStrings ?? {});\n}\n","import * as http from 'node:http';\nimport * as https from 'node:https';\nimport type {\n AuthenticationContext,\n AuthenticationScheme,\n AuthorizationPolicy\n} from '@cleverbrush/auth';\nimport {\n AuthorizationService,\n PolicyBuilder,\n Principal,\n parseCookies,\n requireRole\n} from '@cleverbrush/auth';\nimport { ServiceCollection, type ServiceProvider } from '@cleverbrush/di';\nimport { ActionResult, JsonResult } from './ActionResult.js';\nimport { ContentNegotiator } from './ContentNegotiator.js';\nimport type { EndpointBuilder, Handler } from './Endpoint.js';\nimport { HttpError } from './HttpError.js';\nimport { MiddlewarePipeline } from './MiddlewarePipeline.js';\nimport { needsBody, resolveArgs } from './ParameterResolver.js';\nimport {\n createProblemDetails,\n PROBLEM_JSON_CONTENT_TYPE,\n serializeProblemDetails\n} from './ProblemDetails.js';\nimport { RequestContext } from './RequestContext.js';\nimport { Router } from './Router.js';\nimport type {\n ContentTypeHandler,\n EndpointRegistration,\n Middleware,\n ServerOptions\n} from './types.js';\n\n// ---------------------------------------------------------------------------\n// Authentication / Authorization Config Types\n// ---------------------------------------------------------------------------\n\n/**\n * Authentication configuration passed to `ServerBuilder.useAuthentication()`.\n *\n * At least one scheme must be listed. The `defaultScheme` name must match\n * one of the registered scheme `name` values — it is used when no specific\n * scheme is requested.\n */\nexport interface AuthenticationConfig {\n /** Name of the default scheme to use (must match a scheme's `name`). */\n defaultScheme: string;\n /** Registered authentication schemes. */\n schemes: AuthenticationScheme<any>[];\n}\n\n/**\n * Authorization configuration passed to `ServerBuilder.useAuthorization()`.\n *\n * Named policies can be referenced by string in future `authorize('policy-name')`\n * calls (currently resolved at startup time).\n */\nexport interface AuthorizationConfig {\n /** Named policies (looked up by `authorize('policy-name')` — future use). */\n policies?: Record<string, (builder: PolicyBuilder) => void>;\n}\n\n/**\n * Fluent builder for constructing and starting an HTTP server.\n *\n * @example\n * ```ts\n * const server = new ServerBuilder();\n *\n * server\n * .services(svc => svc.addSingleton(IDb, () => new Db()))\n * .use(loggingMiddleware)\n * .handle(GetUser, ({ params }) => db.find(params.id));\n *\n * await server.listen(3000);\n * ```\n */\nexport class ServerBuilder {\n readonly #serviceCollection = new ServiceCollection();\n readonly #registrations: EndpointRegistration[] = [];\n readonly #globalMiddlewares: Middleware[] = [];\n readonly #contentNegotiator = new ContentNegotiator();\n #options: ServerOptions = {};\n #authConfig: AuthenticationConfig | null = null;\n #authzConfig: AuthorizationConfig | null = null;\n #healthcheck = false;\n\n /**\n * Configure the DI service collection.\n *\n * @param configureFn - Receives the `ServiceCollection` for registrations.\n */\n services(configureFn: (svc: ServiceCollection) => void): this {\n configureFn(this.#serviceCollection);\n return this;\n }\n\n /**\n * Add a global middleware that runs for every request.\n * Middleware is executed in the order it is added.\n */\n use(middleware: Middleware): this {\n this.#globalMiddlewares.push(middleware);\n return this;\n }\n\n /**\n * Register an additional content type handler for content negotiation.\n * JSON is registered by default.\n */\n contentType(handler: ContentTypeHandler): this {\n this.#contentNegotiator.register(handler);\n return this;\n }\n\n /**\n * Enable authentication with one or more schemes.\n * Registers a global middleware that authenticates every request and\n * sets `ctx.principal`.\n */\n useAuthentication(config: AuthenticationConfig): this {\n this.#authConfig = config;\n return this;\n }\n\n /**\n * Enable authorization enforcement.\n * Registers a global middleware that checks endpoint `authorize()`\n * metadata against the authenticated principal.\n * Must be called after `useAuthentication()`.\n */\n useAuthorization(config?: AuthorizationConfig): this {\n this.#authzConfig = config ?? {};\n return this;\n }\n\n /**\n * Enable the `GET /health` endpoint that returns `{ ok: true }` (200).\n * Useful for load balancer and container readiness probes.\n */\n withHealthcheck(): this {\n this.#healthcheck = true;\n return this;\n }\n\n /**\n * Register an endpoint and its handler.\n *\n * @param endpointDef - An `EndpointBuilder` instance (e.g. from `endpoint.get(...)`).\n * @param handler - The typed handler function.\n * @param options - Optional per-endpoint middleware.\n */\n handle<E extends EndpointBuilder<any, any, any, any, any, any, any, any>>(\n endpointDef: E,\n handler: Handler<E>,\n options?: { middlewares?: Middleware[] }\n ): this {\n this.#registrations.push({\n endpoint: endpointDef.introspect(),\n handler,\n middlewares: options?.middlewares\n });\n return this;\n }\n\n /**\n * Returns a snapshot of all registered endpoints.\n * Useful for generating OpenAPI specs or other documentation.\n */\n getRegistrations(): readonly EndpointRegistration[] {\n return [...this.#registrations];\n }\n\n /**\n * Returns the authentication configuration, or `null` if\n * `useAuthentication()` has not been called.\n */\n getAuthenticationConfig(): AuthenticationConfig | null {\n return this.#authConfig;\n }\n\n /**\n * Start listening on the given port and host. Resolves with the running\n * {@link Server} instance.\n *\n * @param port - TCP port (default: `ServerOptions.port ?? 3000`).\n * @param host - Bind address (default: `ServerOptions.host ?? '0.0.0.0'`).\n */\n async listen(port?: number, host?: string): Promise<Server> {\n const router = new Router();\n\n for (const reg of this.#registrations) {\n router.addRoute(reg);\n }\n\n const serviceProvider = this.#serviceCollection.buildServiceProvider({\n validateScopes: false\n });\n\n // Build auth middleware stack\n const authMiddlewares: Middleware[] = [];\n\n if (this.#authConfig) {\n authMiddlewares.push(\n createAuthenticationMiddleware(this.#authConfig)\n );\n }\n\n if (this.#authzConfig !== null) {\n const policies = new Map<string, AuthorizationPolicy>();\n if (this.#authzConfig.policies) {\n for (const [name, configureFn] of Object.entries(\n this.#authzConfig.policies\n )) {\n const builder = new PolicyBuilder();\n configureFn(builder);\n policies.set(name, builder.build(name));\n }\n }\n const authzService = new AuthorizationService(policies);\n authMiddlewares.push(\n createAuthorizationMiddleware(authzService, this.#authConfig)\n );\n }\n\n // Auth middlewares go before user-registered global middlewares\n const allMiddlewares = [...authMiddlewares, ...this.#globalMiddlewares];\n\n const server = new Server(\n router,\n serviceProvider,\n this.#contentNegotiator,\n allMiddlewares,\n this.#healthcheck\n );\n\n const listenPort = port ?? this.#options.port ?? 3000;\n const listenHost = host ?? this.#options.host ?? '0.0.0.0';\n\n await server.start(listenPort, listenHost, this.#options);\n return server;\n }\n}\n\n/**\n * The running HTTP/HTTPS server instance returned by `ServerBuilder.listen()`.\n *\n * Use `close()` to gracefully shut down the server.\n */\nexport class Server {\n readonly #router: Router;\n readonly #serviceProvider: ServiceProvider;\n readonly #contentNegotiator: ContentNegotiator;\n readonly #globalMiddlewares: Middleware[];\n readonly #healthcheck: boolean;\n #httpServer: http.Server | https.Server | null = null;\n\n constructor(\n router: Router,\n serviceProvider: ServiceProvider,\n contentNegotiator: ContentNegotiator,\n globalMiddlewares: Middleware[],\n healthcheck = false\n ) {\n this.#router = router;\n this.#serviceProvider = serviceProvider;\n this.#contentNegotiator = contentNegotiator;\n this.#globalMiddlewares = globalMiddlewares;\n this.#healthcheck = healthcheck;\n }\n\n /**\n * Start listening. Called internally by `ServerBuilder.listen()` after\n * the server is fully configured.\n */\n async start(\n port: number,\n host: string,\n options: ServerOptions\n ): Promise<void> {\n const handler = (\n req: http.IncomingMessage,\n res: http.ServerResponse\n ) => {\n this.#handleRequest(req, res).catch((_err: unknown) => {\n if (!res.headersSent) {\n res.writeHead(500, {\n 'content-type': PROBLEM_JSON_CONTENT_TYPE\n });\n res.end(serializeProblemDetails(createProblemDetails(500)));\n }\n });\n };\n\n if (options.https) {\n this.#httpServer = https.createServer(\n { key: options.https.key, cert: options.https.cert },\n handler\n );\n } else {\n this.#httpServer = http.createServer(handler);\n }\n\n await new Promise<void>(resolve => {\n this.#httpServer!.listen(port, host, resolve);\n });\n }\n\n /** Gracefully stop the server and free the TCP port. */\n async close(): Promise<void> {\n if (!this.#httpServer) return;\n await new Promise<void>((resolve, reject) => {\n this.#httpServer!.close((err: Error | undefined) => {\n if (err) reject(err);\n else resolve();\n });\n });\n this.#httpServer = null;\n }\n\n /**\n * The bound address after `listen()` resolves.\n * Returns `null` if the server has been closed or not yet started.\n */\n get address(): { port: number; host: string } | null {\n const addr = this.#httpServer?.address();\n if (!addr || typeof addr === 'string') return null;\n return { port: addr.port, host: addr.address };\n }\n\n async #handleRequest(\n req: http.IncomingMessage,\n res: http.ServerResponse\n ): Promise<void> {\n const scope = this.#serviceProvider.createScope();\n\n try {\n const ctx = new RequestContext(req, res);\n const urlPath = ctx.url.pathname;\n const method = ctx.method;\n\n if (\n this.#healthcheck &&\n method === 'GET' &&\n urlPath === '/health'\n ) {\n res.writeHead(200);\n res.end();\n return;\n }\n\n const routeResult = this.#router.match(method, urlPath);\n\n if (!routeResult.match) {\n if (routeResult.badRequest) {\n const pd = createProblemDetails(400);\n res.writeHead(400, {\n 'content-type': PROBLEM_JSON_CONTENT_TYPE\n });\n res.end(serializeProblemDetails(pd));\n return;\n }\n\n if (routeResult.methodNotAllowed) {\n const pd = createProblemDetails(405, 'Method Not Allowed');\n res.writeHead(405, {\n 'content-type': PROBLEM_JSON_CONTENT_TYPE,\n allow: routeResult.allowedMethods!.join(', ')\n });\n res.end(serializeProblemDetails(pd));\n return;\n }\n\n const pd = createProblemDetails(404);\n res.writeHead(404, {\n 'content-type': PROBLEM_JSON_CONTENT_TYPE\n });\n res.end(serializeProblemDetails(pd));\n return;\n }\n\n const { registration, parsedPath } = routeResult.match;\n const meta = registration.endpoint;\n\n // Set path params on context (raw string form for middleware)\n if (parsedPath) {\n const rawParams: Record<string, string> = {};\n flattenToStrings(parsedPath, '', rawParams);\n ctx.pathParams = rawParams;\n }\n\n ctx.services = scope.serviceProvider;\n\n // Store endpoint metadata for authorization middleware\n ctx.items.set('__endpoint_meta', meta);\n\n // Build middleware pipeline\n const pipeline = new MiddlewarePipeline();\n for (const mw of this.#globalMiddlewares) {\n pipeline.add(mw);\n }\n if (registration.middlewares) {\n for (const mw of registration.middlewares) {\n pipeline.add(mw);\n }\n }\n\n await pipeline.execute(ctx, async () => {\n if (ctx.responded) return;\n\n // Parse body if needed\n let parsedBody: unknown;\n if (needsBody(meta)) {\n const contentType = req.headers['content-type'];\n const ctHandler =\n this.#contentNegotiator.selectRequestHandler(\n contentType\n );\n if (ctHandler) {\n const rawBody = await ctx.body();\n const bodyText = rawBody.toString('utf-8');\n if (bodyText.length > 0) {\n try {\n parsedBody = ctHandler.deserialize(bodyText);\n } catch {\n const pd = createProblemDetails(\n 400,\n 'Malformed request body'\n );\n res.writeHead(400, {\n 'content-type': PROBLEM_JSON_CONTENT_TYPE\n });\n res.end(serializeProblemDetails(pd));\n ctx.responded = true;\n return;\n }\n }\n } else if (contentType) {\n const pd = createProblemDetails(415);\n res.writeHead(415, {\n 'content-type': PROBLEM_JSON_CONTENT_TYPE\n });\n res.end(serializeProblemDetails(pd));\n ctx.responded = true;\n return;\n }\n }\n\n // Resolve parameters\n const resolveResult = await resolveArgs(\n meta,\n parsedPath,\n ctx,\n parsedBody\n );\n if (!resolveResult.valid) {\n res.writeHead(400, {\n 'content-type': PROBLEM_JSON_CONTENT_TYPE\n });\n res.end(\n serializeProblemDetails(resolveResult.problemDetails)\n );\n ctx.responded = true;\n return;\n }\n\n // Call handler\n let result = registration.handler(...resolveResult.args);\n if (result instanceof Promise) {\n result = await result;\n }\n\n if (ctx.responded) return;\n await this.#sendResult(req, res, result);\n ctx.responded = true;\n });\n } catch (err) {\n if (res.headersSent) return;\n\n if (err instanceof HttpError) {\n const pd = err.toProblemDetails();\n res.writeHead(pd.status, {\n 'content-type': PROBLEM_JSON_CONTENT_TYPE\n });\n res.end(serializeProblemDetails(pd));\n } else {\n console.error('[server] Unhandled error:', err);\n const pd = createProblemDetails(500);\n res.writeHead(500, {\n 'content-type': PROBLEM_JSON_CONTENT_TYPE\n });\n res.end(serializeProblemDetails(pd));\n }\n } finally {\n try {\n await scope.asyncDispose();\n } catch {\n // Swallow disposal errors\n }\n }\n }\n\n async #sendResult(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n result: unknown\n ): Promise<void> {\n if (result instanceof ActionResult) {\n await result.executeAsync(req, res, this.#contentNegotiator);\n } else if (result === null || result === undefined) {\n res.writeHead(204);\n res.end();\n } else {\n await new JsonResult(result, 200).executeAsync(\n req,\n res,\n this.#contentNegotiator\n );\n }\n }\n}\n\nexport function createServer(options?: ServerOptions): ServerBuilder {\n const builder = new ServerBuilder();\n if (options) {\n (builder as any).__options = options;\n }\n return builder;\n}\n\n/** Flatten a nested object to a flat Record<string, string> for raw pathParams */\nfunction flattenToStrings(\n obj: Record<string, any>,\n prefix: string,\n result: Record<string, string>\n): void {\n for (const [key, value] of Object.entries(obj)) {\n const fullKey = prefix ? `${prefix}.${key}` : key;\n if (\n value !== null &&\n typeof value === 'object' &&\n !Array.isArray(value)\n ) {\n flattenToStrings(value, fullKey, result);\n } else {\n result[fullKey] = String(value);\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Authentication Middleware\n// ---------------------------------------------------------------------------\n\nfunction createAuthenticationMiddleware(\n config: AuthenticationConfig\n): Middleware {\n const schemeMap = new Map<string, AuthenticationScheme<any>>();\n for (const scheme of config.schemes) {\n schemeMap.set(scheme.name, scheme);\n }\n\n return async (ctx, next) => {\n const scheme = schemeMap.get(config.defaultScheme);\n if (!scheme) {\n // No matching scheme — leave principal as anonymous\n ctx.principal = Principal.anonymous();\n await next();\n return;\n }\n\n // Build transport-agnostic auth context\n const authCtx: AuthenticationContext = {\n headers: ctx.headers,\n cookies: parseCookies(ctx.headers['cookie'] ?? ''),\n items: ctx.items\n };\n\n const result = await scheme.authenticate(authCtx);\n\n if (result.succeeded) {\n ctx.principal = result.principal;\n } else {\n ctx.principal = Principal.anonymous();\n }\n\n await next();\n };\n}\n\n// ---------------------------------------------------------------------------\n// Authorization Middleware\n// ---------------------------------------------------------------------------\n\nfunction createAuthorizationMiddleware(\n authzService: AuthorizationService,\n authConfig: AuthenticationConfig | null\n): Middleware {\n // Collect challenge headers from schemes for 401 responses\n const challengeHeaders: Record<string, string> = {};\n if (authConfig) {\n for (const scheme of authConfig.schemes) {\n if (scheme.challenge) {\n const ch = scheme.challenge();\n challengeHeaders[ch.headerName.toLowerCase()] = ch.headerValue;\n }\n }\n }\n\n return async (ctx, next) => {\n const meta = ctx.items.get('__endpoint_meta') as\n | import('./Endpoint.js').EndpointMetadata\n | undefined;\n\n // No auth metadata or authRoles is null → public endpoint\n if (!meta || meta.authRoles === null) {\n await next();\n return;\n }\n\n // Endpoint requires auth — check principal\n const principal = ctx.principal;\n\n if (\n !principal ||\n !(principal instanceof Principal) ||\n !principal.isAuthenticated\n ) {\n // 401 Unauthorized\n const pd = createProblemDetails(401, 'Unauthorized');\n const headers: Record<string, string> = {\n 'content-type': PROBLEM_JSON_CONTENT_TYPE,\n ...challengeHeaders\n };\n ctx.response.writeHead(401, headers);\n ctx.response.end(serializeProblemDetails(pd));\n ctx.responded = true;\n return;\n }\n\n // If roles are specified, check them\n if (meta.authRoles.length > 0) {\n const result = await authzService.authorize(principal, [\n requireRole(...meta.authRoles)\n ]);\n if (!result.allowed) {\n const pd = createProblemDetails(403, 'Forbidden');\n ctx.response.writeHead(403, {\n 'content-type': PROBLEM_JSON_CONTENT_TYPE\n });\n ctx.response.end(serializeProblemDetails(pd));\n ctx.responded = true;\n return;\n }\n }\n\n // For typed handler access — set the principal value\n if (principal instanceof Principal) {\n ctx.principal = principal.value;\n }\n\n await next();\n };\n}\n","import type { ContentTypeHandler } from './types.js';\n\nconst JSON_HANDLER: ContentTypeHandler = {\n mimeType: 'application/json',\n serialize(value: unknown): string {\n return JSON.stringify(value);\n },\n deserialize(raw: string): unknown {\n return JSON.parse(raw);\n }\n};\n\ninterface ParsedAccept {\n mimeType: string;\n quality: number;\n}\n\nfunction parseAcceptHeader(accept: string): ParsedAccept[] {\n return accept\n .split(',')\n .map(part => {\n const trimmed = part.trim();\n const [mimeType, ...params] = trimmed.split(';').map(s => s.trim());\n let quality = 1;\n for (const p of params) {\n const [key, val] = p.split('=');\n if (key?.trim() === 'q' && val) {\n quality = parseFloat(val);\n if (Number.isNaN(quality)) quality = 1;\n }\n }\n return { mimeType: mimeType.toLowerCase(), quality };\n })\n .sort((a, b) => b.quality - a.quality);\n}\n\n/**\n * Selects the appropriate serializer/deserializer for a request or response\n * based on the `Accept` / `Content-Type` HTTP headers.\n *\n * JSON is registered by default. Additional handlers can be added with\n * `register()` or via `ServerBuilder.contentType()`.\n */\nexport class ContentNegotiator {\n readonly #handlers: Map<string, ContentTypeHandler> = new Map();\n\n constructor() {\n this.register(JSON_HANDLER);\n }\n\n /**\n * Register a new content type handler.\n * If a handler for the same MIME type was already registered it is replaced.\n */\n register(handler: ContentTypeHandler): void {\n this.#handlers.set(handler.mimeType.toLowerCase(), handler);\n }\n\n /**\n * Select the best response serializer for the given `Accept` header value.\n *\n * Returns `null` if no registered handler can satisfy the request;\n * the server will respond with 406 Not Acceptable in that case.\n */\n selectResponseHandler(acceptHeader?: string): ContentTypeHandler | null {\n if (!acceptHeader)\n return this.#handlers.get('application/json') ?? null;\n\n const parsed = parseAcceptHeader(acceptHeader);\n for (const { mimeType } of parsed) {\n if (mimeType === '*/*') {\n return this.#handlers.get('application/json') ?? null;\n }\n const handler = this.#handlers.get(mimeType);\n if (handler) return handler;\n }\n\n return null;\n }\n\n /**\n * Select the deserializer for an incoming `Content-Type` header.\n *\n * Returns `null` if the content type is not recognised; the server will\n * respond with 415 Unsupported Media Type in that case.\n */\n selectRequestHandler(\n contentTypeHeader?: string\n ): ContentTypeHandler | null {\n if (!contentTypeHeader) return null;\n\n // Extract mime type (ignore charset, boundary, etc.)\n const mimeType = contentTypeHeader.split(';')[0].trim().toLowerCase();\n return this.#handlers.get(mimeType) ?? null;\n }\n}\n","import type { RequestContext } from './RequestContext.js';\nimport type { Middleware } from './types.js';\n\n/**\n * Executes a chain of {@link Middleware} functions in order, then invokes\n * a final handler when `next()` is called by every middleware in the chain.\n *\n * Middleware can short-circuit the chain by not calling `next()`.\n */\nexport class MiddlewarePipeline {\n readonly #middlewares: Middleware[] = [];\n\n /** Append a middleware to the end of the pipeline. */\n add(middleware: Middleware): void {\n this.#middlewares.push(middleware);\n }\n\n /**\n * Execute the pipeline with the given `context`, calling each middleware\n * in order and finally invoking `finalHandler`.\n */\n async execute(\n context: RequestContext,\n finalHandler: () => Promise<void>\n ): Promise<void> {\n let index = 0;\n\n const next = async (): Promise<void> => {\n if (index < this.#middlewares.length) {\n const middleware = this.#middlewares[index++];\n await middleware(context, next);\n } else {\n await finalHandler();\n }\n };\n\n await next();\n }\n}\n","import type { SchemaBuilder } from '@cleverbrush/schema';\nimport type { EndpointMetadata } from './Endpoint.js';\nimport type { ProblemDetails, ValidationErrorItem } from './ProblemDetails.js';\nimport { createValidationProblemDetails } from './ProblemDetails.js';\nimport type { RequestContext } from './RequestContext.js';\n\n/**\n * Result returned by `resolveArgs()`. When `valid` is `false` the\n * `problemDetails` payload should be sent as a 400 response.\n */\nexport type ResolveResult =\n | { valid: true; args: unknown[] }\n | { valid: false; problemDetails: ProblemDetails };\n\n/**\n * Returns true if the endpoint declares a body schema.\n */\nexport function needsBody(meta: EndpointMetadata): boolean {\n return meta.bodySchema != null;\n}\n\n/**\n * Resolve the action context object for an endpoint-based handler.\n *\n * Builds `{ context, params?, body?, query?, headers? }` based on\n * what the endpoint declares.\n */\nexport async function resolveArgs(\n meta: EndpointMetadata,\n parsedPath: Record<string, any> | null,\n context: RequestContext,\n parsedBody: unknown\n): Promise<ResolveResult> {\n const errors: ValidationErrorItem[] = [];\n const contextObj: Record<string, unknown> = {};\n\n // Always provide context\n contextObj.context = context;\n\n // Principal — from authentication middleware (if endpoint requires auth)\n if (meta.authRoles !== null && context.principal !== undefined) {\n contextObj.principal = context.principal;\n }\n\n // Params — from parsed path (already validated by ParseStringSchemaBuilder)\n if (parsedPath && Object.keys(parsedPath).length > 0) {\n contextObj.params = parsedPath;\n }\n\n // Body — validate against endpoint's body schema\n if (meta.bodySchema) {\n const result = await meta.bodySchema.validateAsync(parsedBody, {\n doNotStopOnFirstError: true\n });\n if (result.valid) {\n contextObj.body = result.object;\n } else {\n const getInvalidProperties =\n typeof (result as any).getInvalidProperties === 'function'\n ? ((result as any)\n .getInvalidProperties as () => ReadonlyArray<{\n errors: ReadonlyArray<string>;\n descriptor: { toJsonPointer: () => string };\n }>)\n : null;\n\n let errorsAdded = false;\n if (getInvalidProperties) {\n for (const prop of getInvalidProperties()) {\n const pointer = prop.descriptor.toJsonPointer();\n for (const msg of prop.errors) {\n errors.push({\n pointer: `/body${pointer}`,\n detail: msg\n });\n errorsAdded = true;\n }\n }\n }\n // Fallback: required-check failures surface in result.errors\n // but not in the property descriptor map (e.g. null body)\n if (!errorsAdded) {\n for (const err of result.errors ?? []) {\n errors.push({ pointer: '/body', detail: err.message });\n }\n }\n }\n }\n\n // Query — validate against endpoint's query schema\n if (meta.querySchema) {\n const queryIntro = meta.querySchema.introspect() as any;\n if (queryIntro.type !== 'object' || !queryIntro.properties) {\n throw new Error(\n 'Endpoint query schema must be an object schema whose properties map to query parameter names.'\n );\n }\n const queryProps = queryIntro.properties as Record<\n string,\n SchemaBuilder<any, any, any, any, any>\n >;\n const queryObj: Record<string, unknown> = {};\n for (const [qName, qSchema] of Object.entries(queryProps)) {\n const raw = context.queryParams[qName];\n const result = await qSchema.validateAsync(raw, {\n doNotStopOnFirstError: true\n });\n if (result.valid) {\n queryObj[qName] = result.object;\n } else {\n for (const err of result.errors ?? []) {\n errors.push({\n pointer: `/query/${qName}`,\n detail: err.message\n });\n }\n }\n }\n contextObj.query = queryObj;\n }\n\n // Headers — validate against endpoint's header schema\n if (meta.headerSchema) {\n const headersIntro = meta.headerSchema.introspect() as any;\n if (headersIntro.type !== 'object' || !headersIntro.properties) {\n throw new Error(\n 'Endpoint headers schema must be an object schema whose properties map to header names.'\n );\n }\n const headerProps = headersIntro.properties as Record<\n string,\n SchemaBuilder<any, any, any, any, any>\n >;\n const headersObj: Record<string, unknown> = {};\n for (const [hName, hSchema] of Object.entries(headerProps)) {\n const raw = context.headers[hName.toLowerCase()];\n const result = await hSchema.validateAsync(raw, {\n doNotStopOnFirstError: true\n });\n if (result.valid) {\n headersObj[hName] = result.object;\n } else {\n for (const err of result.errors ?? []) {\n errors.push({\n pointer: `/headers/${hName}`,\n detail: err.message\n });\n }\n }\n }\n contextObj.headers = headersObj;\n }\n\n if (errors.length > 0) {\n return {\n valid: false,\n problemDetails: createValidationProblemDetails(errors)\n };\n }\n\n // Services — resolve declared dependencies from the DI container\n if (meta.serviceSchemas) {\n if (!context.services) {\n throw new Error(\n 'Endpoint declares .inject() dependencies but no service provider is available. ' +\n 'Register services via createServer().services() before handling this endpoint.'\n );\n }\n const servicesObj: Record<string, unknown> = {};\n for (const [name, schema] of Object.entries(meta.serviceSchemas)) {\n servicesObj[name] = context.services.get(schema);\n }\n return { valid: true, args: [contextObj, servicesObj] };\n }\n\n return { valid: true, args: [contextObj] };\n}\n","import type { ParseStringSchemaBuilder } from '@cleverbrush/schema';\nimport type { EndpointRegistration, RouteMatch } from './types.js';\n\ninterface RegisteredRoute {\n readonly basePath: string;\n readonly routePath:\n | string\n | ParseStringSchemaBuilder<any, any, any, any, any>;\n readonly registration: EndpointRegistration;\n}\n\nfunction normalizePath(p: string): string {\n // Use decodeURI (not decodeURIComponent) so that reserved characters such\n // as %2F (encoded slash) are kept encoded and do not alter path segmentation.\n // Throws URIError on malformed percent-encoding – callers that process\n // untrusted input (e.g. match()) must catch that and return a 400.\n const decoded = decodeURI(p);\n if (decoded.length > 1 && decoded.endsWith('/')) {\n return decoded.slice(0, -1);\n }\n return decoded;\n}\n\nfunction isParseStringSchema(\n p: string | ParseStringSchemaBuilder<any, any, any, any, any>\n): p is ParseStringSchemaBuilder<any, any, any, any, any> {\n return typeof p !== 'string' && typeof (p as any).validate === 'function';\n}\n\n/**\n * Radix-style HTTP router that maps method + path to endpoint registrations.\n *\n * Both static string paths (exact-match only) and `ParseStringSchemaBuilder`\n * typed path templates are supported. For dynamic path parameters use\n * `route()` / `parseString()` templates rather than colon-param strings.\n */\nexport class Router {\n readonly #routes: Map<string, RegisteredRoute[]> = new Map();\n\n /**\n * Register an endpoint with the router.\n */\n addRoute(registration: EndpointRegistration): void {\n const { method, basePath, pathTemplate } = registration.endpoint;\n const upperMethod = method.toUpperCase();\n const normalizedBase = normalizePath(basePath);\n\n const route: RegisteredRoute = {\n basePath: normalizedBase,\n routePath: pathTemplate,\n registration\n };\n\n if (!this.#routes.has(upperMethod)) {\n this.#routes.set(upperMethod, []);\n }\n this.#routes.get(upperMethod)!.push(route);\n }\n\n /**\n * Match an incoming HTTP method and URL to a registered endpoint.\n *\n * Returns:\n * - `{ match }` — a successful match with parsed path parameters.\n * - `{ match: null, methodNotAllowed: true, allowedMethods }` — path matches\n * but the method does not (405 Method Not Allowed).\n * - `{ match: null, methodNotAllowed: false }` — no match at all (404).\n * - `{ match: null, methodNotAllowed: false, badRequest: true }` — the URL\n * contains malformed percent-encoding (caller should respond with 400).\n */\n match(\n method: string,\n url: string\n ): {\n match: RouteMatch | null;\n methodNotAllowed: boolean;\n badRequest?: boolean;\n allowedMethods?: string[];\n } {\n let normalized: string;\n try {\n normalized = normalizePath(url);\n } catch {\n // URIError from decodeURI – malformed percent-encoding in the URL\n return { match: null, methodNotAllowed: false, badRequest: true };\n }\n const upperMethod = method.toUpperCase();\n\n // Try exact method match first\n const methodRoutes = this.#routes.get(upperMethod);\n if (methodRoutes) {\n for (const route of methodRoutes) {\n const result = this.#tryMatch(route, normalized);\n if (result) return { match: result, methodNotAllowed: false };\n }\n }\n\n // Check if any other method matches this path (405 detection)\n const allowedMethods: string[] = [];\n for (const [m, routes] of this.#routes) {\n if (m === upperMethod) continue;\n for (const route of routes) {\n if (this.#tryMatch(route, normalized)) {\n allowedMethods.push(m);\n break;\n }\n }\n }\n\n if (allowedMethods.length > 0) {\n return { match: null, methodNotAllowed: true, allowedMethods };\n }\n\n return { match: null, methodNotAllowed: false };\n }\n\n #tryMatch(\n route: RegisteredRoute,\n normalizedUrl: string\n ): RouteMatch | null {\n const { basePath, routePath } = route;\n\n // Check basePath prefix\n if (basePath && !normalizedUrl.startsWith(basePath)) {\n return null;\n }\n\n const remainder = basePath\n ? normalizedUrl.slice(basePath.length)\n : normalizedUrl;\n\n if (isParseStringSchema(routePath)) {\n // Dynamic route: validate remainder via parseString schema\n const result = routePath.validate(remainder);\n if (result.valid) {\n return {\n registration: route.registration,\n parsedPath: result.object as Record<string, any>\n };\n }\n return null;\n }\n\n // Static route: exact match\n const normalizedRoutePath = normalizePath(routePath);\n const normalizedRemainder = remainder.length === 0 ? '/' : remainder;\n\n if (normalizedRemainder === normalizedRoutePath) {\n return {\n registration: route.registration,\n parsedPath: null\n };\n }\n\n return null;\n }\n}\n"],"mappings":"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","names":["ActionResult","body","headers","JsonResult","location","h","NoContentResult","url","permanent","RedirectResult","status","content","fileName","contentType","FileResult","ContentResult","readable","StreamResult","StatusCodeResult","_req","res","_contentNegotiator","key","value","resolve","reject","EndpointBuilder","_EndpointBuilder","#method","#basePath","#pathTemplate","#bodySchema","#querySchema","#headerSchema","#serviceSchemas","#authRoles","#summary","#description","#tags","#operationId","#deprecated","#responseSchema","method","basePath","pathTemplate","bodySchema","querySchema","headerSchema","serviceSchemas","authRoles","summary","description","tags","operationId","deprecated","responseSchema","schema","schemas","args","roles","merged","_schema","text","id","createEndpoint","meta","createScopedFactoryMethods","createScopedFactory","createEndpoints","_roles","endpoint","STATUS_TITLES","createProblemDetails","status","title","detail","extensions","createValidationProblemDetails","errors","serializeProblemDetails","pd","PROBLEM_JSON_CONTENT_TYPE","HttpError","status","title","detail","extensions","createProblemDetails","NotFoundError","BadRequestError","UnauthorizedError","ForbiddenError","ConflictError","URL","any","boolean","func","object","promise","record","string","IRequestContext","RequestContext","#pathParams","#services","#bodyBuffer","#bodyRead","#jsonCache","#jsonParsed","request","response","rawUrl","headers","key","value","params","resolve","reject","chunks","chunk","text","object","parseString","createRouteTag","props","objectSchema","strings","selectors","$t","route","propsOrStrings","_rest","http","https","AuthorizationService","PolicyBuilder","Principal","parseCookies","requireRole","ServiceCollection","JSON_HANDLER","value","raw","parseAcceptHeader","accept","part","trimmed","mimeType","params","s","quality","p","key","val","a","b","ContentNegotiator","#handlers","handler","acceptHeader","parsed","contentTypeHeader","MiddlewarePipeline","#middlewares","middleware","context","finalHandler","index","next","needsBody","meta","resolveArgs","parsedPath","context","parsedBody","errors","contextObj","result","getInvalidProperties","errorsAdded","prop","pointer","msg","err","queryIntro","queryProps","queryObj","qName","qSchema","raw","headersIntro","headerProps","headersObj","hName","hSchema","createValidationProblemDetails","servicesObj","name","schema","normalizePath","p","decoded","isParseStringSchema","Router","#routes","registration","method","basePath","pathTemplate","upperMethod","route","url","normalized","methodRoutes","result","#tryMatch","allowedMethods","m","routes","normalizedUrl","routePath","remainder","normalizedRoutePath","ServerBuilder","#serviceCollection","ServiceCollection","#registrations","#globalMiddlewares","#contentNegotiator","ContentNegotiator","#options","#authConfig","#authzConfig","#healthcheck","configureFn","middleware","handler","config","endpointDef","options","port","host","router","Router","reg","serviceProvider","authMiddlewares","createAuthenticationMiddleware","policies","name","builder","PolicyBuilder","authzService","AuthorizationService","createAuthorizationMiddleware","allMiddlewares","server","Server","listenPort","listenHost","#router","#serviceProvider","#httpServer","contentNegotiator","globalMiddlewares","healthcheck","req","res","#handleRequest","_err","PROBLEM_JSON_CONTENT_TYPE","serializeProblemDetails","createProblemDetails","resolve","reject","err","addr","scope","ctx","RequestContext","urlPath","method","routeResult","pd","registration","parsedPath","meta","rawParams","flattenToStrings","pipeline","MiddlewarePipeline","mw","parsedBody","needsBody","contentType","ctHandler","bodyText","resolveResult","resolveArgs","result","#sendResult","HttpError","ActionResult","JsonResult","createServer","obj","prefix","key","value","fullKey","schemeMap","scheme","next","Principal","authCtx","parseCookies","authConfig","challengeHeaders","ch","principal","headers","requireRole"]}
@@ -0,0 +1,8 @@
1
+ import { type InferType, type ObjectSchemaBuilder, type ParseStringSchemaBuilder, type PropertyDescriptor, type PropertyDescriptorTree, type SchemaBuilder } from '@cleverbrush/schema';
2
+ type RouteTemplateTag<TProps extends Record<string, SchemaBuilder<any, any, any, any, any>>> = (strings: TemplateStringsArray, ...selectors: Array<(tree: PropertyDescriptorTree<ObjectSchemaBuilder<TProps, true, false, undefined, false, {}, [
3
+ ]>, ObjectSchemaBuilder<TProps, true, false, undefined, false, {}, [
4
+ ]>, string | number | boolean | Date>) => PropertyDescriptor<ObjectSchemaBuilder<TProps, true, false, undefined, false, {}, []>, any, any>>) => ParseStringSchemaBuilder<InferType<ObjectSchemaBuilder<TProps, true, false, undefined, false, {}, []>>>;
5
+ export declare function route(strings: TemplateStringsArray, ...selectors: never[]): ParseStringSchemaBuilder<InferType<ObjectSchemaBuilder<{}, true, false, undefined, false, {}, []>>>;
6
+ export declare function route(): RouteTemplateTag<{}>;
7
+ export declare function route<TProps extends Record<string, SchemaBuilder<any, any, any, any, any>>>(props: TProps): RouteTemplateTag<TProps>;
8
+ export {};
@@ -0,0 +1,66 @@
1
+ import type { EndpointMetadata } from './Endpoint.js';
2
+ import type { RequestContext } from './RequestContext.js';
3
+ /**
4
+ * A registered endpoint pairing its metadata (method, path, schemas) with
5
+ * the handler function and any per-endpoint middleware.
6
+ */
7
+ export interface EndpointRegistration {
8
+ readonly endpoint: EndpointMetadata;
9
+ readonly handler: (...args: any[]) => any;
10
+ readonly middlewares?: readonly Middleware[];
11
+ }
12
+ /**
13
+ * The result of a successful router lookup: the matched endpoint registration
14
+ * and any parsed path parameters extracted from the URL.
15
+ */
16
+ export interface RouteMatch {
17
+ readonly registration: EndpointRegistration;
18
+ readonly parsedPath: Record<string, any> | null;
19
+ }
20
+ /**
21
+ * A pluggable serializer/deserializer for a specific MIME type.
22
+ * Register instances with `ServerBuilder.contentType()` or
23
+ * `ContentNegotiator.register()` to extend content negotiation.
24
+ *
25
+ * @example
26
+ * ```ts
27
+ * const msgpackHandler: ContentTypeHandler = {
28
+ * mimeType: 'application/msgpack',
29
+ * serialize: (value) => encode(value),
30
+ * deserialize: (raw) => decode(Buffer.from(raw))
31
+ * };
32
+ * server.contentType(msgpackHandler);
33
+ * ```
34
+ */
35
+ export interface ContentTypeHandler {
36
+ readonly mimeType: string;
37
+ serialize(value: unknown): string;
38
+ deserialize(raw: string): unknown;
39
+ }
40
+ /**
41
+ * A middleware function in the request pipeline.
42
+ *
43
+ * Call `next()` to pass control to the next middleware or the endpoint
44
+ * handler. If `next()` is not called, the pipeline short-circuits.
45
+ *
46
+ * @example
47
+ * ```ts
48
+ * const logger: Middleware = async (ctx, next) => {
49
+ * console.log(ctx.method, ctx.url.pathname);
50
+ * await next();
51
+ * };
52
+ * ```
53
+ */
54
+ export type Middleware = (context: RequestContext, next: () => Promise<void>) => Promise<void>;
55
+ /**
56
+ * Configuration options passed to `ServerBuilder.listen()` or the `Server`
57
+ * constructor. All fields are optional; sensible defaults are applied.
58
+ */
59
+ export interface ServerOptions {
60
+ readonly port?: number;
61
+ readonly host?: string;
62
+ readonly https?: {
63
+ readonly key: string;
64
+ readonly cert: string;
65
+ };
66
+ }
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "author": "Andrew Zolotukhin <andrew_zol@cleverbrush.com>",
3
+ "bugs": {
4
+ "url": "https://github.com/cleverbrush/framework/issues",
5
+ "email": "andrew_zol@cleverbrush.com"
6
+ },
7
+ "dependencies": {
8
+ "@cleverbrush/schema": "0.0.0-beta-20260413195755",
9
+ "@cleverbrush/di": "0.0.0-beta-20260413195755",
10
+ "@cleverbrush/auth": "0.0.0-beta-20260413195755"
11
+ },
12
+ "description": "Schema-first HTTP server framework — schema-driven controllers, DI, auto-validation, RFC 9457 errors",
13
+ "files": [
14
+ "dist"
15
+ ],
16
+ "homepage": "https://docs.cleverbrush.com/server",
17
+ "keywords": [
18
+ "http",
19
+ "server",
20
+ "schema",
21
+ "dependency injection",
22
+ "validation",
23
+ "typescript",
24
+ "cleverbrush"
25
+ ],
26
+ "license": "BSD 3-Clause",
27
+ "main": "./dist/index.js",
28
+ "exports": {
29
+ ".": {
30
+ "types": "./dist/index.d.ts",
31
+ "import": "./dist/index.js"
32
+ }
33
+ },
34
+ "sideEffects": false,
35
+ "name": "@cleverbrush/server",
36
+ "readme": "https://github.com/cleverbrush/framework/tree/master/libs/server#readme",
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "github:cleverbrush/framework"
40
+ },
41
+ "scripts": {
42
+ "watch": "tsc --build tsconfig.build.json --watch",
43
+ "build": "tsup && tsc --project tsconfig.build.json --emitDeclarationOnly",
44
+ "clean": "rm -rf dist tsconfig.build.tsbuildinfo"
45
+ },
46
+ "type": "module",
47
+ "types": "./dist/index.d.ts",
48
+ "version": "0.0.0-beta-20260413195755"
49
+ }