@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.
- package/README.md +230 -0
- package/dist/ActionResult.d.ts +119 -0
- package/dist/ContentNegotiator.d.ts +31 -0
- package/dist/Endpoint.d.ts +203 -0
- package/dist/HttpError.d.ts +41 -0
- package/dist/MiddlewarePipeline.d.ts +18 -0
- package/dist/ParameterResolver.d.ts +25 -0
- package/dist/ProblemDetails.d.ts +49 -0
- package/dist/RequestContext.d.ts +64 -0
- package/dist/Router.d.ts +32 -0
- package/dist/Server.d.ts +135 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/route.d.ts +8 -0
- package/dist/types.d.ts +66 -0
- package/package.json +49 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An RFC 9457 (formerly RFC 7807) Problem Details object.
|
|
3
|
+
*
|
|
4
|
+
* Provides machine-readable error information in HTTP API responses.
|
|
5
|
+
* Serialized as `application/problem+json`.
|
|
6
|
+
*
|
|
7
|
+
* @see {@link https://www.rfc-editor.org/rfc/rfc9457 RFC 9457}
|
|
8
|
+
*/
|
|
9
|
+
export interface ProblemDetails {
|
|
10
|
+
readonly type: string;
|
|
11
|
+
readonly status: number;
|
|
12
|
+
readonly title: string;
|
|
13
|
+
readonly detail?: string;
|
|
14
|
+
readonly instance?: string;
|
|
15
|
+
readonly [extension: string]: unknown;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Create a {@link ProblemDetails} object for the given HTTP status code.
|
|
19
|
+
*
|
|
20
|
+
* @param status - HTTP status code (e.g. 400, 404, 500).
|
|
21
|
+
* @param title - Short, human-readable summary. Defaults to a standard phrase
|
|
22
|
+
* for common status codes.
|
|
23
|
+
* @param detail - Longer explanation specific to this occurrence.
|
|
24
|
+
* @param extensions - Extra fields merged into the object (RFC 9457 §3.1).
|
|
25
|
+
*/
|
|
26
|
+
export declare function createProblemDetails(status: number, title?: string, detail?: string, extensions?: Record<string, unknown>): ProblemDetails;
|
|
27
|
+
/**
|
|
28
|
+
* A single field-level validation error, used in validation Problem Details
|
|
29
|
+
* responses. `pointer` follows JSON Pointer syntax (RFC 6901).
|
|
30
|
+
*
|
|
31
|
+
* @example `{ pointer: '/body/email', detail: 'Must be a valid email address' }`
|
|
32
|
+
*/
|
|
33
|
+
export interface ValidationErrorItem {
|
|
34
|
+
readonly pointer: string;
|
|
35
|
+
readonly detail: string;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Create a 400 Bad Request Problem Details object listing all validation
|
|
39
|
+
* field errors.
|
|
40
|
+
*
|
|
41
|
+
* @param errors - Array of per-field errors with JSON Pointer paths.
|
|
42
|
+
*/
|
|
43
|
+
export declare function createValidationProblemDetails(errors: readonly ValidationErrorItem[]): ProblemDetails;
|
|
44
|
+
/**
|
|
45
|
+
* Serialize a {@link ProblemDetails} object to a JSON string.
|
|
46
|
+
*/
|
|
47
|
+
export declare function serializeProblemDetails(pd: ProblemDetails): string;
|
|
48
|
+
/** The MIME type for Problem Details JSON responses (`application/problem+json`). */
|
|
49
|
+
export declare const PROBLEM_JSON_CONTENT_TYPE = "application/problem+json";
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
import { URL } from 'node:url';
|
|
3
|
+
import type { IServiceProvider } from '@cleverbrush/di';
|
|
4
|
+
/**
|
|
5
|
+
* IRequestContext — the schema definition for the request context.
|
|
6
|
+
* Serves as both a DI key and a type definition.
|
|
7
|
+
*/
|
|
8
|
+
export declare const IRequestContext: import("@cleverbrush/schema").ExtendedObject<{
|
|
9
|
+
method: import("@cleverbrush/schema").ExtendedString<string>;
|
|
10
|
+
url: import("@cleverbrush/schema").ExtendedString<string>;
|
|
11
|
+
pathParams: import("@cleverbrush/schema").ExtendedRecord<import("@cleverbrush/schema").ExtendedString<string>, import("@cleverbrush/schema").ExtendedString<string>>;
|
|
12
|
+
queryParams: import("@cleverbrush/schema").ExtendedRecord<import("@cleverbrush/schema").ExtendedString<string>, import("@cleverbrush/schema").ExtendedString<string>>;
|
|
13
|
+
headers: import("@cleverbrush/schema").ExtendedRecord<import("@cleverbrush/schema").ExtendedString<string>, import("@cleverbrush/schema").ExtendedString<string>>;
|
|
14
|
+
items: import("@cleverbrush/schema").ExtendedAny;
|
|
15
|
+
body: import("@cleverbrush/schema").FunctionSchemaBuilder<true, false, undefined, false, {}, [], import("@cleverbrush/schema").ExtendedPromise<import("@cleverbrush/schema").ExtendedAny>, (...args: any[]) => Promise<any>>;
|
|
16
|
+
json: import("@cleverbrush/schema").FunctionSchemaBuilder<true, false, undefined, false, {}, [], import("@cleverbrush/schema").ExtendedPromise<import("@cleverbrush/schema").ExtendedAny>, (...args: any[]) => Promise<any>>;
|
|
17
|
+
responded: import("@cleverbrush/schema").ExtendedBoolean;
|
|
18
|
+
}>;
|
|
19
|
+
/**
|
|
20
|
+
* Per-request context object passed to every middleware and endpoint handler.
|
|
21
|
+
*
|
|
22
|
+
* Provides typed access to path/query parameters, headers, the request body,
|
|
23
|
+
* and the DI service provider for the current request scope.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```ts
|
|
27
|
+
* const middleware: Middleware = async (ctx, next) => {
|
|
28
|
+
* ctx.items.set('startTime', Date.now());
|
|
29
|
+
* await next();
|
|
30
|
+
* };
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export declare class RequestContext {
|
|
34
|
+
#private;
|
|
35
|
+
readonly request: IncomingMessage;
|
|
36
|
+
readonly response: ServerResponse;
|
|
37
|
+
readonly url: URL;
|
|
38
|
+
readonly method: string;
|
|
39
|
+
readonly headers: Record<string, string>;
|
|
40
|
+
readonly items: Map<string, unknown>;
|
|
41
|
+
/** @internal — overridable for testing */
|
|
42
|
+
_queryParams?: Record<string, string>;
|
|
43
|
+
responded: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* The authenticated principal for this request.
|
|
46
|
+
* Set by authentication middleware; typed as `unknown` at the
|
|
47
|
+
* RequestContext level — handlers receive a fully typed version
|
|
48
|
+
* via `ActionContext.principal`.
|
|
49
|
+
*/
|
|
50
|
+
principal: unknown;
|
|
51
|
+
constructor(request: IncomingMessage, response: ServerResponse);
|
|
52
|
+
/** Path parameters extracted from the matched route template. */
|
|
53
|
+
get pathParams(): Record<string, string>;
|
|
54
|
+
set pathParams(value: Record<string, string>);
|
|
55
|
+
/** Parsed query string parameters from the request URL. */
|
|
56
|
+
get queryParams(): Record<string, string>;
|
|
57
|
+
/** The DI service provider scoped to this request. Set by the server before invoking the handler. */
|
|
58
|
+
get services(): IServiceProvider | undefined;
|
|
59
|
+
set services(value: IServiceProvider);
|
|
60
|
+
/** Read and buffer the raw request body. Result is cached after the first call. */
|
|
61
|
+
body(): Promise<Buffer>;
|
|
62
|
+
/** Read, buffer, and JSON-parse the request body. Result is cached after the first call. */
|
|
63
|
+
json(): Promise<unknown>;
|
|
64
|
+
}
|
package/dist/Router.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { EndpointRegistration, RouteMatch } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Radix-style HTTP router that maps method + path to endpoint registrations.
|
|
4
|
+
*
|
|
5
|
+
* Both static string paths (exact-match only) and `ParseStringSchemaBuilder`
|
|
6
|
+
* typed path templates are supported. For dynamic path parameters use
|
|
7
|
+
* `route()` / `parseString()` templates rather than colon-param strings.
|
|
8
|
+
*/
|
|
9
|
+
export declare class Router {
|
|
10
|
+
#private;
|
|
11
|
+
/**
|
|
12
|
+
* Register an endpoint with the router.
|
|
13
|
+
*/
|
|
14
|
+
addRoute(registration: EndpointRegistration): void;
|
|
15
|
+
/**
|
|
16
|
+
* Match an incoming HTTP method and URL to a registered endpoint.
|
|
17
|
+
*
|
|
18
|
+
* Returns:
|
|
19
|
+
* - `{ match }` — a successful match with parsed path parameters.
|
|
20
|
+
* - `{ match: null, methodNotAllowed: true, allowedMethods }` — path matches
|
|
21
|
+
* but the method does not (405 Method Not Allowed).
|
|
22
|
+
* - `{ match: null, methodNotAllowed: false }` — no match at all (404).
|
|
23
|
+
* - `{ match: null, methodNotAllowed: false, badRequest: true }` — the URL
|
|
24
|
+
* contains malformed percent-encoding (caller should respond with 400).
|
|
25
|
+
*/
|
|
26
|
+
match(method: string, url: string): {
|
|
27
|
+
match: RouteMatch | null;
|
|
28
|
+
methodNotAllowed: boolean;
|
|
29
|
+
badRequest?: boolean;
|
|
30
|
+
allowedMethods?: string[];
|
|
31
|
+
};
|
|
32
|
+
}
|
package/dist/Server.d.ts
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import type { AuthenticationScheme } from '@cleverbrush/auth';
|
|
2
|
+
import { PolicyBuilder } from '@cleverbrush/auth';
|
|
3
|
+
import { ServiceCollection, type ServiceProvider } from '@cleverbrush/di';
|
|
4
|
+
import { ContentNegotiator } from './ContentNegotiator.js';
|
|
5
|
+
import type { EndpointBuilder, Handler } from './Endpoint.js';
|
|
6
|
+
import { Router } from './Router.js';
|
|
7
|
+
import type { ContentTypeHandler, EndpointRegistration, Middleware, ServerOptions } from './types.js';
|
|
8
|
+
/**
|
|
9
|
+
* Authentication configuration passed to `ServerBuilder.useAuthentication()`.
|
|
10
|
+
*
|
|
11
|
+
* At least one scheme must be listed. The `defaultScheme` name must match
|
|
12
|
+
* one of the registered scheme `name` values — it is used when no specific
|
|
13
|
+
* scheme is requested.
|
|
14
|
+
*/
|
|
15
|
+
export interface AuthenticationConfig {
|
|
16
|
+
/** Name of the default scheme to use (must match a scheme's `name`). */
|
|
17
|
+
defaultScheme: string;
|
|
18
|
+
/** Registered authentication schemes. */
|
|
19
|
+
schemes: AuthenticationScheme<any>[];
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Authorization configuration passed to `ServerBuilder.useAuthorization()`.
|
|
23
|
+
*
|
|
24
|
+
* Named policies can be referenced by string in future `authorize('policy-name')`
|
|
25
|
+
* calls (currently resolved at startup time).
|
|
26
|
+
*/
|
|
27
|
+
export interface AuthorizationConfig {
|
|
28
|
+
/** Named policies (looked up by `authorize('policy-name')` — future use). */
|
|
29
|
+
policies?: Record<string, (builder: PolicyBuilder) => void>;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Fluent builder for constructing and starting an HTTP server.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```ts
|
|
36
|
+
* const server = new ServerBuilder();
|
|
37
|
+
*
|
|
38
|
+
* server
|
|
39
|
+
* .services(svc => svc.addSingleton(IDb, () => new Db()))
|
|
40
|
+
* .use(loggingMiddleware)
|
|
41
|
+
* .handle(GetUser, ({ params }) => db.find(params.id));
|
|
42
|
+
*
|
|
43
|
+
* await server.listen(3000);
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export declare class ServerBuilder {
|
|
47
|
+
#private;
|
|
48
|
+
/**
|
|
49
|
+
* Configure the DI service collection.
|
|
50
|
+
*
|
|
51
|
+
* @param configureFn - Receives the `ServiceCollection` for registrations.
|
|
52
|
+
*/
|
|
53
|
+
services(configureFn: (svc: ServiceCollection) => void): this;
|
|
54
|
+
/**
|
|
55
|
+
* Add a global middleware that runs for every request.
|
|
56
|
+
* Middleware is executed in the order it is added.
|
|
57
|
+
*/
|
|
58
|
+
use(middleware: Middleware): this;
|
|
59
|
+
/**
|
|
60
|
+
* Register an additional content type handler for content negotiation.
|
|
61
|
+
* JSON is registered by default.
|
|
62
|
+
*/
|
|
63
|
+
contentType(handler: ContentTypeHandler): this;
|
|
64
|
+
/**
|
|
65
|
+
* Enable authentication with one or more schemes.
|
|
66
|
+
* Registers a global middleware that authenticates every request and
|
|
67
|
+
* sets `ctx.principal`.
|
|
68
|
+
*/
|
|
69
|
+
useAuthentication(config: AuthenticationConfig): this;
|
|
70
|
+
/**
|
|
71
|
+
* Enable authorization enforcement.
|
|
72
|
+
* Registers a global middleware that checks endpoint `authorize()`
|
|
73
|
+
* metadata against the authenticated principal.
|
|
74
|
+
* Must be called after `useAuthentication()`.
|
|
75
|
+
*/
|
|
76
|
+
useAuthorization(config?: AuthorizationConfig): this;
|
|
77
|
+
/**
|
|
78
|
+
* Enable the `GET /health` endpoint that returns `{ ok: true }` (200).
|
|
79
|
+
* Useful for load balancer and container readiness probes.
|
|
80
|
+
*/
|
|
81
|
+
withHealthcheck(): this;
|
|
82
|
+
/**
|
|
83
|
+
* Register an endpoint and its handler.
|
|
84
|
+
*
|
|
85
|
+
* @param endpointDef - An `EndpointBuilder` instance (e.g. from `endpoint.get(...)`).
|
|
86
|
+
* @param handler - The typed handler function.
|
|
87
|
+
* @param options - Optional per-endpoint middleware.
|
|
88
|
+
*/
|
|
89
|
+
handle<E extends EndpointBuilder<any, any, any, any, any, any, any, any>>(endpointDef: E, handler: Handler<E>, options?: {
|
|
90
|
+
middlewares?: Middleware[];
|
|
91
|
+
}): this;
|
|
92
|
+
/**
|
|
93
|
+
* Returns a snapshot of all registered endpoints.
|
|
94
|
+
* Useful for generating OpenAPI specs or other documentation.
|
|
95
|
+
*/
|
|
96
|
+
getRegistrations(): readonly EndpointRegistration[];
|
|
97
|
+
/**
|
|
98
|
+
* Returns the authentication configuration, or `null` if
|
|
99
|
+
* `useAuthentication()` has not been called.
|
|
100
|
+
*/
|
|
101
|
+
getAuthenticationConfig(): AuthenticationConfig | null;
|
|
102
|
+
/**
|
|
103
|
+
* Start listening on the given port and host. Resolves with the running
|
|
104
|
+
* {@link Server} instance.
|
|
105
|
+
*
|
|
106
|
+
* @param port - TCP port (default: `ServerOptions.port ?? 3000`).
|
|
107
|
+
* @param host - Bind address (default: `ServerOptions.host ?? '0.0.0.0'`).
|
|
108
|
+
*/
|
|
109
|
+
listen(port?: number, host?: string): Promise<Server>;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* The running HTTP/HTTPS server instance returned by `ServerBuilder.listen()`.
|
|
113
|
+
*
|
|
114
|
+
* Use `close()` to gracefully shut down the server.
|
|
115
|
+
*/
|
|
116
|
+
export declare class Server {
|
|
117
|
+
#private;
|
|
118
|
+
constructor(router: Router, serviceProvider: ServiceProvider, contentNegotiator: ContentNegotiator, globalMiddlewares: Middleware[], healthcheck?: boolean);
|
|
119
|
+
/**
|
|
120
|
+
* Start listening. Called internally by `ServerBuilder.listen()` after
|
|
121
|
+
* the server is fully configured.
|
|
122
|
+
*/
|
|
123
|
+
start(port: number, host: string, options: ServerOptions): Promise<void>;
|
|
124
|
+
/** Gracefully stop the server and free the TCP port. */
|
|
125
|
+
close(): Promise<void>;
|
|
126
|
+
/**
|
|
127
|
+
* The bound address after `listen()` resolves.
|
|
128
|
+
* Returns `null` if the server has been closed or not yet started.
|
|
129
|
+
*/
|
|
130
|
+
get address(): {
|
|
131
|
+
port: number;
|
|
132
|
+
host: string;
|
|
133
|
+
} | null;
|
|
134
|
+
}
|
|
135
|
+
export declare function createServer(options?: ServerOptions): ServerBuilder;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { ActionResult, ContentResult, FileResult, JsonResult, NoContentResult, RedirectResult, StatusCodeResult, StreamResult } from './ActionResult.js';
|
|
2
|
+
export { type ActionContext, createEndpoints, EndpointBuilder, type EndpointMetadata, type EndpointMetadataDescriptors, endpoint, type Handler, type ScopedEndpointFactory } from './Endpoint.js';
|
|
3
|
+
export { BadRequestError, ConflictError, ForbiddenError, HttpError, NotFoundError, UnauthorizedError } from './HttpError.js';
|
|
4
|
+
export { createProblemDetails, createValidationProblemDetails, type ProblemDetails, type ValidationErrorItem } from './ProblemDetails.js';
|
|
5
|
+
export { IRequestContext, RequestContext } from './RequestContext.js';
|
|
6
|
+
export { route } from './route.js';
|
|
7
|
+
export { type AuthenticationConfig, type AuthorizationConfig, createServer, Server, ServerBuilder } from './Server.js';
|
|
8
|
+
export type { ContentTypeHandler, EndpointRegistration, Middleware, ServerOptions } from './types.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var g=class{static ok(e,t){return new R(e,200,t)}static created(e,t,r){let s={...r};return t&&(s.location=t),new R(e,201,s)}static noContent(){return new C}static redirect(e,t=!1){return new A(e,t)}static json(e,t=200,r){return new R(e,t,r)}static file(e,t,r="application/octet-stream"){return new v(e,t,r)}static content(e,t,r=200){return new B(e,t,r)}static stream(e,t,r){return new E(e,t,r)}static status(e,t){return new x(e,t)}},R=class extends g{body;status;headers;constructor(e,t=200,r){super(),this.body=e,this.status=t,this.headers=r??{}}async executeAsync(e,t,r){for(let[s,i]of Object.entries(this.headers))t.setHeader(s,i);if(this.body===null||this.body===void 0){t.writeHead(this.status),t.end();return}t.writeHead(this.status,{"content-type":"application/json"}),t.end(JSON.stringify(this.body))}},v=class extends g{content;fileName;contentType;constructor(e,t,r="application/octet-stream"){super(),this.content=e,this.fileName=t,this.contentType=r}async executeAsync(e,t,r){t.writeHead(200,{"content-type":this.contentType,"content-disposition":`attachment; filename="${this.fileName}"`,"content-length":String(this.content.byteLength)}),t.end(this.content)}},B=class extends g{body;contentType;status;constructor(e,t,r=200){super(),this.body=e,this.contentType=t,this.status=r}async executeAsync(e,t,r){t.writeHead(this.status,{"content-type":this.contentType}),t.end(this.body)}},E=class extends g{readable;contentType;fileName;constructor(e,t,r){super(),this.readable=e,this.contentType=t,this.fileName=r}async executeAsync(e,t,r){let s={"content-type":this.contentType};this.fileName&&(s["content-disposition"]=`attachment; filename="${this.fileName}"`),t.writeHead(200,s),await new Promise((i,a)=>{this.readable.on("error",a),t.on("error",a),this.readable.on("end",i),this.readable.pipe(t,{end:!0})})}},x=class extends g{status;headers;constructor(e,t){super(),this.status=e,this.headers=t??{}}async executeAsync(e,t,r){t.writeHead(this.status,this.headers),t.end()}},A=class extends g{url;permanent;constructor(e,t=!1){super(),this.url=e,this.permanent=t}async executeAsync(e,t,r){t.writeHead(this.permanent?301:302,{location:this.url}),t.end()}},C=class extends g{async executeAsync(e,t,r){t.writeHead(204),t.end()}};var M=class n{#e;#n;#r;#i;#s;#t;#a;#o;#l;#c;#h;#u;#p;#d;constructor(e,t,r,s,i,a,d=null,o=null,c=null,h=null,m=[],l=null,u=!1,P=null){this.#e=e,this.#n=t,this.#r=r,this.#i=s,this.#s=i,this.#t=a,this.#a=d,this.#o=o,this.#l=c,this.#c=h,this.#h=m,this.#u=l,this.#p=u,this.#d=P}body(e){return new n(this.#e,this.#n,this.#r,e,this.#s,this.#t,this.#a,this.#o,this.#l,this.#c,this.#h,this.#u,this.#p,this.#d)}query(e){return new n(this.#e,this.#n,this.#r,this.#i,e,this.#t,this.#a,this.#o,this.#l,this.#c,this.#h,this.#u,this.#p,this.#d)}headers(e){return new n(this.#e,this.#n,this.#r,this.#i,this.#s,e,this.#a,this.#o,this.#l,this.#c,this.#h,this.#u,this.#p,this.#d)}inject(e){return new n(this.#e,this.#n,this.#r,this.#i,this.#s,this.#t,e,this.#o,this.#l,this.#c,this.#h,this.#u,this.#p,this.#d)}authorize(...e){let t;e.length>0&&typeof e[0]=="object"&&e[0]!==null&&"introspect"in e[0]?t=e.slice(1):t=e;let r=this.#o?[...this.#o,...t]:t;return new n(this.#e,this.#n,this.#r,this.#i,this.#s,this.#t,this.#a,r,this.#l,this.#c,this.#h,this.#u,this.#p,this.#d)}returns(e){let t=e!=null&&typeof e=="object"&&"introspect"in e?e:null;return new n(this.#e,this.#n,this.#r,this.#i,this.#s,this.#t,this.#a,this.#o,this.#l,this.#c,this.#h,this.#u,this.#p,t??this.#d)}summary(e){return new n(this.#e,this.#n,this.#r,this.#i,this.#s,this.#t,this.#a,this.#o,e,this.#c,this.#h,this.#u,this.#p,this.#d)}description(e){return new n(this.#e,this.#n,this.#r,this.#i,this.#s,this.#t,this.#a,this.#o,this.#l,e,this.#h,this.#u,this.#p,this.#d)}tags(...e){return new n(this.#e,this.#n,this.#r,this.#i,this.#s,this.#t,this.#a,this.#o,this.#l,this.#c,e,this.#u,this.#p,this.#d)}operationId(e){return new n(this.#e,this.#n,this.#r,this.#i,this.#s,this.#t,this.#a,this.#o,this.#l,this.#c,this.#h,e,this.#p,this.#d)}deprecated(){return new n(this.#e,this.#n,this.#r,this.#i,this.#s,this.#t,this.#a,this.#o,this.#l,this.#c,this.#h,this.#u,!0,this.#d)}introspect(){return{method:this.#e,basePath:this.#n,pathTemplate:this.#r,bodySchema:this.#i,querySchema:this.#s,headerSchema:this.#t,serviceSchemas:this.#a,authRoles:this.#o,summary:this.#l,description:this.#c,tags:this.#h,operationId:this.#u,deprecated:this.#p,responseSchema:this.#d}}};function p(n,e,t,r,s){return new M(n,e,t??"/",null,null,null,null,r??null,s?.summary??null,s?.description??null,s?.tags??[],s?.operationId??null,s?.deprecated??!1,null)}function W(n,e){return{get:t=>p("GET",n,t,e),post:t=>p("POST",n,t,e),put:t=>p("PUT",n,t,e),patch:t=>p("PATCH",n,t,e),delete:t=>p("DELETE",n,t,e),head:t=>p("HEAD",n,t,e),options:t=>p("OPTIONS",n,t,e)}}function oe(n){return{...W(n,null),authorize(...e){let t;return e.length>0&&typeof e[0]=="object"&&e[0]!==null&&"introspect"in e[0]?t=e.slice(1):t=e,W(n,t)}}}function de(n){return Y}var Y={get:(n,e)=>p("GET",n,e),post:(n,e)=>p("POST",n,e),put:(n,e)=>p("PUT",n,e),patch:(n,e)=>p("PATCH",n,e),delete:(n,e)=>p("DELETE",n,e),head:(n,e)=>p("HEAD",n,e),options:(n,e)=>p("OPTIONS",n,e),resource:oe};var le={400:"Bad Request",401:"Unauthorized",403:"Forbidden",404:"Not Found",405:"Method Not Allowed",409:"Conflict",415:"Unsupported Media Type",422:"Unprocessable Content",500:"Internal Server Error",503:"Service Unavailable"};function y(n,e,t,r){return{type:`https://httpstatuses.com/${n}`,status:n,title:e??le[n]??"Error",...t!==void 0?{detail:t}:{},...r}}function O(n){return y(400,"Bad Request","One or more validation errors occurred.",{errors:n})}function T(n){return JSON.stringify(n)}var f="application/problem+json";var S=class extends Error{status;title;detail;extensions;constructor(e,t,r,s){super(r??t??`HTTP ${e}`),this.name="HttpError",this.status=e,this.title=t??`HTTP ${e}`,this.detail=r,this.extensions=s}toProblemDetails(){return y(this.status,this.title,this.detail,this.extensions)}},D=class extends S{constructor(e){super(404,"Not Found",e),this.name="NotFoundError"}},z=class extends S{constructor(e){super(400,"Bad Request",e),this.name="BadRequestError"}},F=class extends S{constructor(e){super(401,"Unauthorized",e),this.name="UnauthorizedError"}},_=class extends S{constructor(e){super(403,"Forbidden",e),this.name="ForbiddenError"}},Q=class extends S{constructor(e){super(409,"Conflict",e),this.name="ConflictError"}};import{URL as ce}from"url";import{any as U,boolean as he,func as X,object as ue,promise as Z,record as J,string as w}from"@cleverbrush/schema";var pe=ue({method:w(),url:w(),pathParams:J(w(),w()),queryParams:J(w(),w()),headers:J(w(),w()),items:U(),body:X().hasReturnType(Z(U())),json:X().hasReturnType(Z(U())),responded:he()}),b=class{request;response;url;method;headers;items=new Map;#e={};_queryParams;#n;#r=null;#i=!1;#s=void 0;#t=!1;responded=!1;principal=void 0;constructor(e,t){this.request=e,this.response=t,this.method=(e.method??"GET").toUpperCase();let r=e.url??"/";this.url=new ce(r,`http://${e.headers.host??"localhost"}`);let s={};for(let[i,a]of Object.entries(e.headers))typeof a=="string"?s[i]=a:Array.isArray(a)&&(s[i]=a.join(", "));this.headers=s}get pathParams(){return this.#e}set pathParams(e){this.#e=e}get queryParams(){if(this._queryParams)return this._queryParams;let e={};for(let[t,r]of this.url.searchParams)e[t]=r;return e}get services(){return this.#n}set services(e){this.#n=e}async body(){return this.#i?this.#r:(this.#r=await new Promise((e,t)=>{let r=[];this.request.on("data",s=>r.push(s)),this.request.on("end",()=>e(Buffer.concat(r))),this.request.on("error",t)}),this.#i=!0,this.#r)}async json(){if(this.#t)return this.#s;let t=(await this.body()).toString("utf-8");return this.#s=t.length>0?JSON.parse(t):void 0,this.#t=!0,this.#s}};import{object as ye,parseString as me}from"@cleverbrush/schema";function ee(n){let e=ye(n);return((t,...r)=>me(e,s=>s(t,...r)))}function ge(n,...e){return n!=null&&Array.isArray(n.raw)?ee({})(n):ee(n??{})}import*as re from"http";import*as se from"https";import{AuthorizationService as Pe,PolicyBuilder as Re,Principal as q,parseCookies as we,requireRole as be}from"@cleverbrush/auth";import{ServiceCollection as ve}from"@cleverbrush/di";var Te={mimeType:"application/json",serialize(n){return JSON.stringify(n)},deserialize(n){return JSON.parse(n)}};function fe(n){return n.split(",").map(e=>{let t=e.trim(),[r,...s]=t.split(";").map(a=>a.trim()),i=1;for(let a of s){let[d,o]=a.split("=");d?.trim()==="q"&&o&&(i=parseFloat(o),Number.isNaN(i)&&(i=1))}return{mimeType:r.toLowerCase(),quality:i}}).sort((e,t)=>t.quality-e.quality)}var H=class{#e=new Map;constructor(){this.register(Te)}register(e){this.#e.set(e.mimeType.toLowerCase(),e)}selectResponseHandler(e){if(!e)return this.#e.get("application/json")??null;let t=fe(e);for(let{mimeType:r}of t){if(r==="*/*")return this.#e.get("application/json")??null;let s=this.#e.get(r);if(s)return s}return null}selectRequestHandler(e){if(!e)return null;let t=e.split(";")[0].trim().toLowerCase();return this.#e.get(t)??null}};var j=class{#e=[];add(e){this.#e.push(e)}async execute(e,t){let r=0,s=async()=>{if(r<this.#e.length){let i=this.#e[r++];await i(e,s)}else await t()};await s()}};function te(n){return n.bodySchema!=null}async function ne(n,e,t,r){let s=[],i={};if(i.context=t,n.authRoles!==null&&t.principal!==void 0&&(i.principal=t.principal),e&&Object.keys(e).length>0&&(i.params=e),n.bodySchema){let a=await n.bodySchema.validateAsync(r,{doNotStopOnFirstError:!0});if(a.valid)i.body=a.object;else{let d=typeof a.getInvalidProperties=="function"?a.getInvalidProperties:null,o=!1;if(d)for(let c of d()){let h=c.descriptor.toJsonPointer();for(let m of c.errors)s.push({pointer:`/body${h}`,detail:m}),o=!0}if(!o)for(let c of a.errors??[])s.push({pointer:"/body",detail:c.message})}}if(n.querySchema){let a=n.querySchema.introspect();if(a.type!=="object"||!a.properties)throw new Error("Endpoint query schema must be an object schema whose properties map to query parameter names.");let d=a.properties,o={};for(let[c,h]of Object.entries(d)){let m=t.queryParams[c],l=await h.validateAsync(m,{doNotStopOnFirstError:!0});if(l.valid)o[c]=l.object;else for(let u of l.errors??[])s.push({pointer:`/query/${c}`,detail:u.message})}i.query=o}if(n.headerSchema){let a=n.headerSchema.introspect();if(a.type!=="object"||!a.properties)throw new Error("Endpoint headers schema must be an object schema whose properties map to header names.");let d=a.properties,o={};for(let[c,h]of Object.entries(d)){let m=t.headers[c.toLowerCase()],l=await h.validateAsync(m,{doNotStopOnFirstError:!0});if(l.valid)o[c]=l.object;else for(let u of l.errors??[])s.push({pointer:`/headers/${c}`,detail:u.message})}i.headers=o}if(s.length>0)return{valid:!1,problemDetails:O(s)};if(n.serviceSchemas){if(!t.services)throw new Error("Endpoint declares .inject() dependencies but no service provider is available. Register services via createServer().services() before handling this endpoint.");let a={};for(let[d,o]of Object.entries(n.serviceSchemas))a[d]=t.services.get(o);return{valid:!0,args:[i,a]}}return{valid:!0,args:[i]}}function L(n){let e=decodeURI(n);return e.length>1&&e.endsWith("/")?e.slice(0,-1):e}function Se(n){return typeof n!="string"&&typeof n.validate=="function"}var N=class{#e=new Map;addRoute(e){let{method:t,basePath:r,pathTemplate:s}=e.endpoint,i=t.toUpperCase(),d={basePath:L(r),routePath:s,registration:e};this.#e.has(i)||this.#e.set(i,[]),this.#e.get(i).push(d)}match(e,t){let r;try{r=L(t)}catch{return{match:null,methodNotAllowed:!1,badRequest:!0}}let s=e.toUpperCase(),i=this.#e.get(s);if(i)for(let d of i){let o=this.#n(d,r);if(o)return{match:o,methodNotAllowed:!1}}let a=[];for(let[d,o]of this.#e)if(d!==s){for(let c of o)if(this.#n(c,r)){a.push(d);break}}return a.length>0?{match:null,methodNotAllowed:!0,allowedMethods:a}:{match:null,methodNotAllowed:!1}}#n(e,t){let{basePath:r,routePath:s}=e;if(r&&!t.startsWith(r))return null;let i=r?t.slice(r.length):t;if(Se(s)){let o=s.validate(i);return o.valid?{registration:e.registration,parsedPath:o.object}:null}let a=L(s);return(i.length===0?"/":i)===a?{registration:e.registration,parsedPath:null}:null}};var I=class{#e=new ve;#n=[];#r=[];#i=new H;#s={};#t=null;#a=null;#o=!1;services(e){return e(this.#e),this}use(e){return this.#r.push(e),this}contentType(e){return this.#i.register(e),this}useAuthentication(e){return this.#t=e,this}useAuthorization(e){return this.#a=e??{},this}withHealthcheck(){return this.#o=!0,this}handle(e,t,r){return this.#n.push({endpoint:e.introspect(),handler:t,middlewares:r?.middlewares}),this}getRegistrations(){return[...this.#n]}getAuthenticationConfig(){return this.#t}async listen(e,t){let r=new N;for(let h of this.#n)r.addRoute(h);let s=this.#e.buildServiceProvider({validateScopes:!1}),i=[];if(this.#t&&i.push(Ee(this.#t)),this.#a!==null){let h=new Map;if(this.#a.policies)for(let[l,u]of Object.entries(this.#a.policies)){let P=new Re;u(P),h.set(l,P.build(l))}let m=new Pe(h);i.push(xe(m,this.#t))}let a=[...i,...this.#r],d=new k(r,s,this.#i,a,this.#o),o=e??this.#s.port??3e3,c=t??this.#s.host??"0.0.0.0";return await d.start(o,c,this.#s),d}},k=class{#e;#n;#r;#i;#s;#t=null;constructor(e,t,r,s,i=!1){this.#e=e,this.#n=t,this.#r=r,this.#i=s,this.#s=i}async start(e,t,r){let s=(i,a)=>{this.#a(i,a).catch(d=>{a.headersSent||(a.writeHead(500,{"content-type":f}),a.end(T(y(500))))})};r.https?this.#t=se.createServer({key:r.https.key,cert:r.https.cert},s):this.#t=re.createServer(s),await new Promise(i=>{this.#t.listen(e,t,i)})}async close(){this.#t&&(await new Promise((e,t)=>{this.#t.close(r=>{r?t(r):e()})}),this.#t=null)}get address(){let e=this.#t?.address();return!e||typeof e=="string"?null:{port:e.port,host:e.address}}async#a(e,t){let r=this.#n.createScope();try{let s=new b(e,t),i=s.url.pathname,a=s.method;if(this.#s&&a==="GET"&&i==="/health"){t.writeHead(200),t.end();return}let d=this.#e.match(a,i);if(!d.match){if(d.badRequest){let u=y(400);t.writeHead(400,{"content-type":f}),t.end(T(u));return}if(d.methodNotAllowed){let u=y(405,"Method Not Allowed");t.writeHead(405,{"content-type":f,allow:d.allowedMethods.join(", ")}),t.end(T(u));return}let l=y(404);t.writeHead(404,{"content-type":f}),t.end(T(l));return}let{registration:o,parsedPath:c}=d.match,h=o.endpoint;if(c){let l={};ie(c,"",l),s.pathParams=l}s.services=r.serviceProvider,s.items.set("__endpoint_meta",h);let m=new j;for(let l of this.#i)m.add(l);if(o.middlewares)for(let l of o.middlewares)m.add(l);await m.execute(s,async()=>{if(s.responded)return;let l;if(te(h)){let K=e.headers["content-type"],$=this.#r.selectRequestHandler(K);if($){let G=(await s.body()).toString("utf-8");if(G.length>0)try{l=$.deserialize(G)}catch{let ae=y(400,"Malformed request body");t.writeHead(400,{"content-type":f}),t.end(T(ae)),s.responded=!0;return}}else if(K){let V=y(415);t.writeHead(415,{"content-type":f}),t.end(T(V)),s.responded=!0;return}}let u=await ne(h,c,s,l);if(!u.valid){t.writeHead(400,{"content-type":f}),t.end(T(u.problemDetails)),s.responded=!0;return}let P=o.handler(...u.args);P instanceof Promise&&(P=await P),!s.responded&&(await this.#o(e,t,P),s.responded=!0)})}catch(s){if(t.headersSent)return;if(s instanceof S){let i=s.toProblemDetails();t.writeHead(i.status,{"content-type":f}),t.end(T(i))}else{console.error("[server] Unhandled error:",s);let i=y(500);t.writeHead(500,{"content-type":f}),t.end(T(i))}}finally{try{await r.asyncDispose()}catch{}}}async#o(e,t,r){r instanceof g?await r.executeAsync(e,t,this.#r):r==null?(t.writeHead(204),t.end()):await new R(r,200).executeAsync(e,t,this.#r)}};function Be(n){let e=new I;return n&&(e.__options=n),e}function ie(n,e,t){for(let[r,s]of Object.entries(n)){let i=e?`${e}.${r}`:r;s!==null&&typeof s=="object"&&!Array.isArray(s)?ie(s,i,t):t[i]=String(s)}}function Ee(n){let e=new Map;for(let t of n.schemes)e.set(t.name,t);return async(t,r)=>{let s=e.get(n.defaultScheme);if(!s){t.principal=q.anonymous(),await r();return}let i={headers:t.headers,cookies:we(t.headers.cookie??""),items:t.items},a=await s.authenticate(i);a.succeeded?t.principal=a.principal:t.principal=q.anonymous(),await r()}}function xe(n,e){let t={};if(e){for(let r of e.schemes)if(r.challenge){let s=r.challenge();t[s.headerName.toLowerCase()]=s.headerValue}}return async(r,s)=>{let i=r.items.get("__endpoint_meta");if(!i||i.authRoles===null){await s();return}let a=r.principal;if(!a||!(a instanceof q)||!a.isAuthenticated){let d=y(401,"Unauthorized"),o={"content-type":f,...t};r.response.writeHead(401,o),r.response.end(T(d)),r.responded=!0;return}if(i.authRoles.length>0&&!(await n.authorize(a,[be(...i.authRoles)])).allowed){let o=y(403,"Forbidden");r.response.writeHead(403,{"content-type":f}),r.response.end(T(o)),r.responded=!0;return}a instanceof q&&(r.principal=a.value),await s()}}export{g as ActionResult,z as BadRequestError,Q as ConflictError,B as ContentResult,M as EndpointBuilder,v as FileResult,_ as ForbiddenError,S as HttpError,pe as IRequestContext,R as JsonResult,C as NoContentResult,D as NotFoundError,A as RedirectResult,b as RequestContext,k as Server,I as ServerBuilder,x as StatusCodeResult,E as StreamResult,F as UnauthorizedError,de as createEndpoints,y as createProblemDetails,Be as createServer,O as createValidationProblemDetails,Y as endpoint,ge as route};
|
|
2
|
+
//# sourceMappingURL=index.js.map
|