UNPKG

@moostjs/event-http

Version:
726 lines (718 loc) 24.5 kB
import { current } from "@wooksjs/event-core"; import { HttpError, HttpError as HttpError$1, WooksHttp, WooksHttpResponse, createHttpApp, httpKind, useAuthorization, useCookies, useHeaders, useHttpContext, useRequest, useResponse, useUrlParams } from "@wooksjs/event-http"; import { Before, Intercept, Interceptor, Overtake, Resolve, TInterceptorPriority, defineBeforeInterceptor, defineMoostEventHandler, getMoostMate } from "moost"; import { useBody } from "@wooksjs/http-body"; import { createProvideRegistry } from "@prostojs/infact"; import { Server } from "http"; import { Server as Server$1 } from "https"; //#region packages/event-http/src/auth-guard.ts function _define_property$1(obj, key, value) { if (key in obj) Object.defineProperty(obj, key, { value, enumerable: true, configurable: true, writable: true }); else obj[key] = value; return obj; } function _ts_decorate(decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; } function _ts_metadata(k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); } function _ts_param(paramIndex, decorator) { return function(target, key) { decorator(target, key, paramIndex); }; } /** Extracts auth credentials from the current request based on transport declarations. Throws 401 if none found. */ function extractTransports(declaration) { const ctx = current(); const result = {}; let found = false; if (declaration.bearer) { const auth = useAuthorization(ctx); if (auth.is("bearer")) { result.bearer = auth.credentials(); found = true; } } if (declaration.basic) { const auth = useAuthorization(ctx); if (auth.is("basic")) { result.basic = auth.basicCredentials(); found = true; } } if (declaration.cookie) { const { getCookie } = useCookies(ctx); const val = getCookie(declaration.cookie.name); if (val) { result.cookie = val; found = true; } } if (declaration.apiKey) { const { name, in: location } = declaration.apiKey; if (location === "header") { const headers = useHeaders(ctx); if (headers[name.toLowerCase()]) { result.apiKey = headers[name.toLowerCase()]; found = true; } } else if (location === "query") { const { params } = useUrlParams(ctx); const val = params().get(name); if (val) { result.apiKey = String(val); found = true; } } else if (location === "cookie") { const { getCookie } = useCookies(ctx); const val = getCookie(name); if (val) { result.apiKey = val; found = true; } } } if (!found) throw new HttpError$1(401, "No authentication credentials provided"); return result; } /** * Creates a functional auth guard interceptor. * Extracts credentials from declared transports and passes them to the handler. * Return a value from the handler to short-circuit with that response. */ function defineAuthGuard(transports, handler) { const def = defineBeforeInterceptor((reply) => { const result = handler(extractTransports(transports)); if (result !== null && result !== void 0 && typeof result.then === "function") return result.then((r) => { if (r !== void 0) reply(r); }); if (result !== void 0) reply(result); }, TInterceptorPriority.GUARD); return Object.assign(def, { __authTransports: transports }); } var AuthGuard = class { __intercept(reply) { const ctor = this.constructor; const extracted = extractTransports(ctor.transports); const result = this.handle(extracted); if (result !== null && result !== void 0 && typeof result.then === "function") return result.then((r) => { if (r !== void 0) reply(r); }); if (result !== void 0) reply(result); } }; _define_property$1(AuthGuard, "transports", void 0); _define_property$1(AuthGuard, "priority", TInterceptorPriority.GUARD); _ts_decorate([ Before(), _ts_param(0, Overtake()), _ts_metadata("design:type", Function), _ts_metadata("design:paramtypes", [typeof TOvertakeFn === "undefined" ? Object : TOvertakeFn]), _ts_metadata("design:returntype", void 0) ], AuthGuard.prototype, "__intercept", null); AuthGuard = _ts_decorate([Interceptor(TInterceptorPriority.GUARD)], AuthGuard); /** * Registers an auth guard as an interceptor and stores its transport * declarations in metadata for swagger auto-discovery. * * Accepts either a functional guard from `defineAuthGuard()` or a * class-based guard extending `AuthGuard`. */ function Authenticate(handler) { const mate = getMoostMate(); const isClass = typeof handler === "function"; const transports = isClass ? handler.transports : handler.__authTransports; const priority = isClass ? handler.priority : handler.priority || TInterceptorPriority.GUARD; const name = isClass ? handler.name : "AuthGuard"; return mate.apply(mate.decorate("interceptors", { handler, priority, name }, true), mate.decorate("authTransports", transports, true)); } //#endregion //#region packages/event-http/src/decorators/http-method.decorator.ts /** Base decorator for registering an HTTP route handler with an explicit method. */ function HttpMethod(method, path) { return getMoostMate().decorate("handlers", { method, path, type: "HTTP" }, true); } /** Register a catch-all route handler matching any HTTP method. */ const All = (path) => HttpMethod("*", path); /** Register a GET route handler. */ const Get = (path) => HttpMethod("GET", path); /** Register a POST route handler. */ const Post = (path) => HttpMethod("POST", path); /** Register a PUT route handler. */ const Put = (path) => HttpMethod("PUT", path); /** Register a DELETE route handler. */ const Delete = (path) => HttpMethod("DELETE", path); /** Register a PATCH route handler. */ const Patch = (path) => HttpMethod("PATCH", path); /** * Register an UPGRADE route handler for WebSocket upgrade requests. * * Use together with `WooksWs` (injected via DI) to complete the handshake: * ```ts * @Upgrade('/ws') * handleUpgrade(ws: WooksWs) { * return ws.upgrade() * } * ``` */ const Upgrade = (path) => HttpMethod("UPGRADE", path); //#endregion //#region packages/event-http/src/decorators/resolve.decorator.ts function createRef(getter, setter) { return new Proxy({}, { get: (_target, prop) => prop === "value" ? getter() : void 0, set: (_target, prop, v) => { if (prop === "value") setter(v); return true; } }); } /** * Ref to the Response Status * @decorator * @paramType TStatusRef */ const StatusRef = () => Resolve((metas, level) => { const response = useResponse(); if (level === "PARAM") return createRef(() => response.status, (v) => { response.status = v; }); if (level === "PROP" && metas.instance && metas.key) { const initialValue = metas.instance[metas.key]; Object.defineProperty(metas.instance, metas.key, { get: () => response.status, set: (v) => { response.status = v; }, configurable: true }); return typeof initialValue === "number" ? initialValue : 200; } }, "statusCode"); /** * Ref to the Response Header * @decorator * @param name - header name * @paramType THeaderRef */ const HeaderRef = (name) => Resolve((metas, level) => { const response = useResponse(); if (level === "PARAM") return createRef(() => response.getHeader(name), (v) => response.setHeader(name, v)); if (level === "PROP" && metas.instance && metas.key) { const initialValue = metas.instance[metas.key]; Object.defineProperty(metas.instance, metas.key, { get: () => response.getHeader(name), set: (v) => response.setHeader(name, v), configurable: true }); return typeof initialValue === "string" ? initialValue : ""; } }, name); /** * Ref to the Response Cookie * @decorator * @param name - cookie name * @paramType TCookieRef */ const CookieRef = (name) => Resolve((metas, level) => { const response = useResponse(); if (level === "PARAM") return createRef(() => response.getCookie(name)?.value, (v) => response.setCookie(name, v ?? "")); if (level === "PROP" && metas.instance && metas.key) { const initialValue = metas.instance[metas.key]; Object.defineProperty(metas.instance, metas.key, { get: () => response.getCookie(name)?.value, set: (v) => response.setCookie(name, v ?? ""), configurable: true }); return typeof initialValue === "string" ? initialValue : ""; } }, name); /** * Ref to the Response Cookie Attributes * @decorator * @param name - cookie name * @paramType TCookieAttributes */ const CookieAttrsRef = (name) => Resolve((metas, level) => { const response = useResponse(); const getAttrs = () => response.getCookie(name)?.attrs ?? {}; const setAttrs = (v) => { const existing = response.getCookie(name); response.setCookie(name, existing?.value ?? "", v); }; if (level === "PARAM") return createRef(getAttrs, setAttrs); if (level === "PROP" && metas.instance && metas.key) { const initialValue = metas.instance[metas.key]; Object.defineProperty(metas.instance, metas.key, { get: getAttrs, set: setAttrs, configurable: true }); return typeof initialValue === "object" ? initialValue : {}; } }, name); /** * Parse Authorisation Header * @decorator * @param name - define what to take from the Auth header * @paramType string */ function Authorization(name) { return Resolve(() => { const auth = useAuthorization(); switch (name) { case "username": return auth.is("basic") ? auth.basicCredentials()?.username : void 0; case "password": return auth.is("basic") ? auth.basicCredentials()?.password : void 0; case "bearer": return auth.is("bearer") ? auth.authorization : void 0; case "raw": return auth.credentials(); case "type": return auth.type(); default: return; } }, "authorization"); } /** * Get Request Header Value * @decorator * @param name - header name * @paramType string */ function Header(name) { return Resolve(() => { return useHeaders()[name]; }, `header: ${name}`); } /** * Get Request Cookie Value * @decorator * @param name - cookie name * @paramType string */ function Cookie(name) { return Resolve(() => useCookies().getCookie(name), `cookie: ${name}`); } /** * Get Query Item value or the whole parsed Query as an object * @decorator * @param name - query item name (optional) * @paramType string | object */ function Query(name) { const isItem = !!name; const _name = isItem ? name : "Query"; return getMoostMate().apply(getMoostMate().decorate("paramSource", isItem ? "QUERY_ITEM" : "QUERY"), getMoostMate().decorate("paramName", _name), Resolve(() => { const { toJson, params } = useUrlParams(); if (isItem) { const value = params().get(name); return value === null ? void 0 : value; } const json = toJson(); return Object.keys(json).length > 0 ? json : void 0; }, _name)); } /** * Get Requested URL * @decorator * @paramType string */ function Url() { return Resolve(() => useRequest().url || "", "url"); } /** * Get Requested HTTP Method * @decorator * @paramType string */ function Method() { return Resolve(() => useRequest().method, "http_method"); } /** * Get Raw Request Instance * @decorator * @paramType IncomingMessage */ function Req() { return Resolve(() => useRequest().raw, "request"); } /** * Get Raw Response Instance * @decorator * @param opts (optional) { passthrough: boolean } * @paramType ServerResponse */ function Res(opts) { return Resolve(() => useResponse().getRawRes(opts?.passthrough), "response"); } /** * Get Request Unique Identificator (UUID) * @decorator * @paramType string */ function ReqId() { return Resolve(() => useRequest().reqId(), "reqId"); } /** * Get Request IP Address * @decorator * @paramType string */ function Ip(opts) { return Resolve(() => useRequest().getIp(opts), "ip"); } /** * Get Request IP Address list * @decorator * @paramType string[] */ function IpList() { return Resolve(() => useRequest().getIpList(), "ipList"); } /** * Get Parsed Request Body * @decorator * @paramType object | string | unknown */ function Body() { return getMoostMate().apply(getMoostMate().decorate("paramSource", "BODY"), Resolve(() => useBody().parseBody(), "body")); } /** * Get Raw Request Body Buffer * @decorator * @paramType Promise<Buffer> */ function RawBody() { return Resolve(() => useBody().rawBody(), "body"); } //#endregion //#region packages/event-http/src/decorators/set.decorator.ts const setHeaderInterceptor = (name, value, opts) => { const cb = () => { const response = useResponse(); if ((!response.getHeader(name) || opts?.force) && (!opts?.status || opts.status === response.status)) response.setHeader(name, value); }; const def = { priority: TInterceptorPriority.AFTER_ALL }; if (opts?.when !== "error") def.after = cb; if (opts?.when === "always" || opts?.when === "error") def.error = cb; return def; }; /** * Set Header for Request Handler * * ```ts * import { Get, SetHeader } from '@moostjs/event-http'; * import { Controller } from 'moost'; * * @Controller() * export class ExampleController { * @Get('test') * // setting header for request handler * @SetHeader('x-server', 'my-server') * testHandler() { * return '...' * } * } * ``` * * ```ts * import { Get, SetHeader } from '@moostjs/event-http'; * import { Controller } from 'moost'; * * @Controller() * export class ExampleController { * @Get('test') * // setting header only if status = 400 * @SetHeader('content-type', 'text/plain', { status: 400 }) * testHandler() { * return '...' * } * } * ``` * * @param name name of header * @param value value for header * @param options options { status?: number, force?: boolean } */ function SetHeader(...args) { return Intercept(setHeaderInterceptor(...args)); } const setCookieInterceptor = (name, value, attrs) => ({ after() { const response = useResponse(); if (!response.getCookie(name)) response.setCookie(name, value, attrs); }, priority: TInterceptorPriority.AFTER_ALL }); /** * Set Cookie for Request Handler * ```ts * import { Get, SetCookie } from '@moostjs/event-http'; * import { Controller } from 'moost'; * * @Controller() * export class ExampleController { * @Get('test') * // setting 'my-cookie' = 'value' with maxAge of 10 minutes * @SetCookie('my-cookie', 'value', { maxAge: '10m' }) * testHandler() { * return '...' * } * } * ``` * * @param name name of cookie * @param value value for cookie * @param attrs cookie attributes */ function SetCookie(...args) { return Intercept(setCookieInterceptor(...args)); } const setStatusInterceptor = (code, opts) => ({ after() { const response = useResponse(); if (!response.status || opts?.force) response.status = code; }, priority: TInterceptorPriority.AFTER_ALL }); /** * Set Response Status for Request Handler * * ```ts * import { Get, SetStatus } from '@moostjs/event-http'; * import { Controller } from 'moost'; * * @Controller() * export class ExampleController { * @Get('test') * @SetStatus(201) * testHandler() { * return '...' * } * } * ``` * @param code number * @param opts optional { force?: boolean } */ function SetStatus(...args) { return Intercept(setStatusInterceptor(...args)); } //#endregion //#region packages/event-http/src/decorators/limits.decorator.ts /** * Creates an interceptor that sets the maximum allowed inflated body size in bytes. * * @param n - Maximum body size in bytes after decompression. * @returns Interceptor def to enforce the limit. */ const globalBodySizeLimit = (n) => defineBeforeInterceptor(() => { useRequest().setMaxInflated(n); }, TInterceptorPriority.BEFORE_ALL); /** * Creates an interceptor that sets the maximum allowed compressed body size in bytes. * * @param n - Maximum body size in bytes before decompression. * @returns Interceptor def to enforce the limit. */ const globalCompressedBodySizeLimit = (n) => defineBeforeInterceptor(() => { useRequest().setMaxCompressed(n); }, TInterceptorPriority.BEFORE_ALL); /** * Creates an interceptor that sets the timeout for reading the request body. * * @param n - Timeout in milliseconds. * @returns Interceptor def to enforce the timeout. */ const globalBodyReadTimeoutMs = (n) => defineBeforeInterceptor(() => { useRequest().setReadTimeoutMs(n); }, TInterceptorPriority.BEFORE_ALL); /** * Decorator to limit the maximum inflated body size for the request. Default: 10 MB * * @param n - Maximum body size in bytes after decompression. */ const BodySizeLimit = (n) => Intercept(globalBodySizeLimit(n)); /** * Decorator to limit the maximum compressed body size for the request. Default: 1 MB * * @param n - Maximum body size in bytes before decompression. */ const CompressedBodySizeLimit = (n) => Intercept(globalCompressedBodySizeLimit(n)); /** * Decorator to set a timeout (in milliseconds) for reading the request body. Default: 10 s * * @param n - Timeout duration in milliseconds. */ const BodyReadTimeoutMs = (n) => Intercept(globalBodyReadTimeoutMs(n)); //#endregion //#region packages/event-http/src/event-http.ts function _define_property(obj, key, value) { if (key in obj) Object.defineProperty(obj, key, { value, enumerable: true, configurable: true, writable: true }); else obj[key] = value; return obj; } const LOGGER_TITLE = "moost-http"; /** * ## Moost HTTP Adapter * * Moost Adapter for HTTP events * * ```ts * │ // HTTP server example * │ import { MoostHttp, Get } from '@moostjs/event-http' * │ import { Moost, Param } from 'moost' * │ * │ class MyServer extends Moost { * │ @Get('test/:name') * │ test(@Param('name') name: string) { * │ return { message: `Hello ${name}!` } * │ } * │ } * │ * │ const app = new MyServer() * │ const http = new MoostHttp() * │ app.adapter(http).listen(3000, () => { * │ app.getLogger('MyApp').log('Up on port 3000') * │ }) * │ app.init() * ``` */ var MoostHttp = class { getHttpApp() { return this.httpApp; } getServerCb(onNoMatch) { return this.httpApp.getServerCb(onNoMatch); } /** * Programmatic route invocation using the Web Standard fetch API. * Goes through the full Moost pipeline: DI scoping, interceptors, * argument resolution, pipes, validation, and handler execution. * * When called from within an existing HTTP context (e.g. during SSR), * identity headers (authorization, cookie) are automatically forwarded. */ fetch(request) { return this.httpApp.fetch(request); } /** * Convenience wrapper for programmatic route invocation. * Accepts a URL string (relative paths auto-prefixed with `http://localhost`), * URL object, or Request, plus optional `RequestInit`. * * Returns `null` when no route matches the request. */ request(input, init) { return this.httpApp.request(input, init); } /** * Runs `fn` inside an HTTP event context seeded from a real `(req, res)` pair, * without route dispatch. Nested `fetch()`/`request()` calls made during `fn` * see this context as their caller, so `forwardHeaders` and parent `Set-Cookie` * propagation apply. Never writes to `res` — apply buffered response state * (e.g. `response.getSetCookieStrings()`) yourself. */ withHttpContext(req, res, fn) { return this.httpApp.withHttpContext(req, res, fn); } listen(port, hostname, backlog, listeningListener) { return this.httpApp.listen(port, hostname, backlog, listeningListener); } async onNotFound() { return defineMoostEventHandler({ loggerTitle: LOGGER_TITLE, getIterceptorHandler: () => this.moost?.getGlobalInterceptorHandler(), getControllerInstance: () => this.moost, callControllerMethod: () => new HttpError$1(404, "Resource Not Found"), targetPath: "", handlerType: "__SYSTEM__" })(); } onInit(moost) { this.moost = moost; } getProvideRegistry() { return createProvideRegistry([WooksHttp, () => this.getHttpApp()], ["WooksHttp", () => this.getHttpApp()], [Server, () => this.getHttpApp().getServer()], [Server$1, () => this.getHttpApp().getServer()]); } getLogger() { return this.getHttpApp().getLogger("[moost-http]"); } bindHandler(opts) { let fn; for (const handler of opts.handlers) { if (handler.type !== "HTTP") continue; const httpPath = handler.path; const path = typeof httpPath === "string" ? httpPath : typeof opts.method === "string" ? opts.method : ""; const targetPath = `${`${opts.prefix || ""}/${path}`.replaceAll(/\/\/+/g, "/")}${path.endsWith("//") ? "/" : ""}`; fn = defineMoostEventHandler({ loggerTitle: LOGGER_TITLE, getIterceptorHandler: opts.getIterceptorHandler, getControllerInstance: opts.getInstance, controllerMethod: opts.method, controllerName: opts.controllerName, resolveArgs: opts.resolveArgs, manualUnscope: true, hooks: { init: ({ unscope }) => { const { raw } = useRequest(); raw.on("end", unscope); } }, targetPath, controllerPrefix: opts.prefix, handlerType: handler.type }); const routerBinding = handler.method === "UPGRADE" ? this.httpApp.upgrade(targetPath, fn) : this.httpApp.on(handler.method, targetPath, fn); const { getPath: pathBuilder } = routerBinding; const id = (getMoostMate().read(opts.fakeInstance, opts.method) || {}).id || opts.method; if (id) { const methods = this.pathBuilders[id] = this.pathBuilders[id] || {}; if (handler.method === "*") { methods.GET = pathBuilder; methods.PUT = pathBuilder; methods.PATCH = pathBuilder; methods.POST = pathBuilder; methods.DELETE = pathBuilder; } else methods[handler.method] = pathBuilder; } opts.logHandler(`(${handler.method})${targetPath}`); const args = routerBinding.getArgs(); const params = {}; args.forEach((a) => params[a] = `{${a}}`); opts.register(handler, routerBinding.getPath(params), args); } } constructor(httpApp) { _define_property(this, "name", "http"); _define_property(this, "httpApp", void 0); _define_property(this, "pathBuilders", {}); _define_property(this, "moost", void 0); WooksHttpResponse.registerFramework({ image: "https://moost.org/moost-full-logo.svg", link: "https://moost.org/", poweredBy: "moostjs", version: "0.6.34" }); if (httpApp && httpApp instanceof WooksHttp) this.httpApp = httpApp; else if (httpApp) this.httpApp = createHttpApp({ ...httpApp, onNotFound: this.onNotFound.bind(this) }); else this.httpApp = createHttpApp({ onNotFound: this.onNotFound.bind(this) }); } }; //#endregion //#region packages/event-http/src/local-fetch.ts /** * Patches `globalThis.fetch` so that requests to local paths (starting with `/`) * are routed through `MoostHttp` in-process instead of making a network request. * Falls back to the original `fetch` when no Moost route matches. * * Useful for SSR scenarios where server-side code needs to call its own API endpoints * without the overhead of a TCP round-trip. * * @returns A teardown function that restores the original `globalThis.fetch`. */ function enableLocalFetch(http) { const originalFetch = globalThis.fetch; globalThis.fetch = async (input, init) => { if (typeof input === "string") { if (input.startsWith("/")) { const response = await http.request(input, init); if (response) return response; } } else if (input instanceof URL) { if (isLocalOrigin(input)) { const response = await http.request(input, init); if (response) return response; } } else if (isLocalOrigin(new URL(input.url))) { const response = await http.fetch(input); if (response) return response; } return originalFetch(input, init); }; return () => { globalThis.fetch = originalFetch; }; } function isLocalOrigin(url) { return url.hostname === "localhost" || url.hostname === "127.0.0.1"; } //#endregion export { All, AuthGuard, Authenticate, Authorization, Body, BodyReadTimeoutMs, BodySizeLimit, CompressedBodySizeLimit, Cookie, CookieAttrsRef, CookieRef, Delete, Get, Header, HeaderRef, HttpError, HttpMethod, Ip, IpList, Method, MoostHttp, Patch, Post, Put, Query, RawBody, Req, ReqId, Res, SetCookie, SetHeader, SetStatus, StatusRef, Upgrade, Url, defineAuthGuard, enableLocalFetch, extractTransports, globalBodyReadTimeoutMs, globalBodySizeLimit, globalCompressedBodySizeLimit, httpKind, useHttpContext };