@znight/http 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,8 @@
1
+ import { RoutePathError, joinPaths, normalizePath, parseRoutePath } from "./path.mjs";
1
2
  import { Readable } from "node:stream";
2
3
  import { pipeline } from "node:stream/promises";
3
4
  import { AsyncLocalStorage } from "node:async_hooks";
5
+ import { ErrorCode, ZnightError } from "@znight/core";
4
6
  // src/status.ts
5
7
  /** Standard reason phrases (RFC 9110 and common extensions), used in error bodies. */
6
8
  const REASONS = {
@@ -522,7 +524,7 @@ function decodeCookieValue(value) {
522
524
  * @throws TypeError for a non-string or a string with lone surrogates (they cannot round-trip).
523
525
  */
524
526
  function encodeCookieValue(value) {
525
- if (typeof value !== "string") throw new TypeError(`A cookie value must be a string (received ${describe(value)})`);
527
+ if (typeof value !== "string") throw new TypeError(`A cookie value must be a string (received ${describe$1(value)})`);
526
528
  if (!value.isWellFormed()) throw new TypeError("A cookie value must be a well-formed string (it contains a lone surrogate)");
527
529
  let encoded = "";
528
530
  let plain = 0;
@@ -681,8 +683,8 @@ function resolveAttributes(name, options) {
681
683
  partitioned: options.partitioned === true,
682
684
  priority: options.priority
683
685
  };
684
- if (attributes.sameSite === "none" && !attributes.secure) throw invalid(name, "SameSite=None requires secure: true (browsers reject it otherwise)");
685
- if (attributes.partitioned && !attributes.secure) throw invalid(name, "partitioned cookies require secure: true");
686
+ if (attributes.sameSite === "none" && !attributes.secure) throw invalid$1(name, "SameSite=None requires secure: true (browsers reject it otherwise)");
687
+ if (attributes.partitioned && !attributes.secure) throw invalid$1(name, "partitioned cookies require secure: true");
686
688
  checkPrefix(name, attributes);
687
689
  return attributes;
688
690
  }
@@ -693,19 +695,19 @@ function checkPrefix(name, attributes) {
693
695
  const http = lower.startsWith("__http-") || lower.startsWith("__host-http-");
694
696
  if (!host && !http && !lower.startsWith("__secure-")) return;
695
697
  const prefix = prefixName(host, http);
696
- if (!attributes.secure) throw invalid(name, `the ${prefix} prefix requires secure: true`);
697
- if (http && !attributes.httpOnly) throw invalid(name, `the ${prefix} prefix requires httpOnly: true`);
698
+ if (!attributes.secure) throw invalid$1(name, `the ${prefix} prefix requires secure: true`);
699
+ if (http && !attributes.httpOnly) throw invalid$1(name, `the ${prefix} prefix requires httpOnly: true`);
698
700
  if (host) {
699
- if (attributes.domain !== void 0) throw invalid(name, `the ${prefix} prefix forbids a domain`);
700
- if (attributes.path !== "/") throw invalid(name, `the ${prefix} prefix requires path: '/'`);
701
+ if (attributes.domain !== void 0) throw invalid$1(name, `the ${prefix} prefix forbids a domain`);
702
+ if (attributes.path !== "/") throw invalid$1(name, `the ${prefix} prefix requires path: '/'`);
701
703
  }
702
704
  }
703
705
  function checkName(name) {
704
- if (!isCookieName(name)) throw new TypeError(`Invalid cookie name ${describe(name)}: expected a token (letters, digits and ! # $ % & ' * + - . ^ _ \` | ~)`);
706
+ if (!isCookieName(name)) throw new TypeError(`Invalid cookie name ${describe$1(name)}: expected a token (letters, digits and ! # $ % & ' * + - . ^ _ \` | ~)`);
705
707
  }
706
708
  function encodeValue(name, value) {
707
- if (typeof value !== "string") throw invalid(name, `the value must be a string (received ${describe(value)})`);
708
- if (!value.isWellFormed()) throw invalid(name, "the value contains a lone surrogate");
709
+ if (typeof value !== "string") throw invalid$1(name, `the value must be a string (received ${describe$1(value)})`);
710
+ if (!value.isWellFormed()) throw invalid$1(name, "the value contains a lone surrogate");
709
711
  return encodeCookieValue(value);
710
712
  }
711
713
  function checkSize(name, wire) {
@@ -716,32 +718,32 @@ function checkSize(name, wire) {
716
718
  function checkOptions(name, options, defaults = false) {
717
719
  if (options === void 0) return {};
718
720
  const what = defaults ? "cookies.defaults" : "options";
719
- if (options === null || typeof options !== "object" || Array.isArray(options)) throw invalid(name, `${what} must be an object (received ${describe(options)})`);
720
- for (const key of Object.keys(options)) if (!OPTION_KEYS.has(key)) throw invalid(name, `unknown option "${key}"`);
721
+ if (options === null || typeof options !== "object" || Array.isArray(options)) throw invalid$1(name, `${what} must be an object (received ${describe$1(options)})`);
722
+ for (const key of Object.keys(options)) if (!OPTION_KEYS.has(key)) throw invalid$1(name, `unknown option "${key}"`);
721
723
  const { maxAge, expires, domain, path, secure, httpOnly, sameSite, partitioned, priority, signed } = options;
722
- if (maxAge !== void 0 && !(typeof maxAge === "number" && Number.isSafeInteger(maxAge))) throw invalid(name, `maxAge must be an integer number of seconds (received ${describe(maxAge)})`);
723
- if (expires !== void 0 && !(expires instanceof Date && !Number.isNaN(expires.getTime()))) throw invalid(name, `expires must be a valid Date (received ${describe(expires)})`);
724
+ if (maxAge !== void 0 && !(typeof maxAge === "number" && Number.isSafeInteger(maxAge))) throw invalid$1(name, `maxAge must be an integer number of seconds (received ${describe$1(maxAge)})`);
725
+ if (expires !== void 0 && !(expires instanceof Date && !Number.isNaN(expires.getTime()))) throw invalid$1(name, `expires must be a valid Date (received ${describe$1(expires)})`);
724
726
  if (expires !== void 0) {
725
727
  const year = expires.getUTCFullYear();
726
- if (year < MIN_EXPIRES_YEAR || year > MAX_EXPIRES_YEAR) throw invalid(name, `expires must be in the years ${MIN_EXPIRES_YEAR} to ${MAX_EXPIRES_YEAR} (received ${expires.toISOString()}): browsers ignore other dates; use maxAge for a long lifetime (browsers cap it at 400 days)`);
728
+ if (year < MIN_EXPIRES_YEAR || year > MAX_EXPIRES_YEAR) throw invalid$1(name, `expires must be in the years ${MIN_EXPIRES_YEAR} to ${MAX_EXPIRES_YEAR} (received ${expires.toISOString()}): browsers ignore other dates; use maxAge for a long lifetime (browsers cap it at 400 days)`);
727
729
  }
728
- if (domain !== void 0 && domain !== null && typeof domain !== "string") throw invalid(name, `domain must be a string or null (received ${describe(domain)})`);
729
- if (path !== void 0 && typeof path !== "string") throw invalid(name, `path must be a string (received ${describe(path)})`);
730
+ if (domain !== void 0 && domain !== null && typeof domain !== "string") throw invalid$1(name, `domain must be a string or null (received ${describe$1(domain)})`);
731
+ if (path !== void 0 && typeof path !== "string") throw invalid$1(name, `path must be a string (received ${describe$1(path)})`);
730
732
  for (const [key, flag] of [
731
733
  ["secure", secure],
732
734
  ["httpOnly", httpOnly],
733
735
  ["partitioned", partitioned],
734
736
  ["signed", signed]
735
- ]) if (flag !== void 0 && typeof flag !== "boolean") throw invalid(name, `${key} must be a boolean (received ${describe(flag)})`);
736
- if (sameSite !== void 0 && !Object.hasOwn(SAME_SITE, sameSite)) throw invalid(name, `sameSite must be 'strict', 'lax' or 'none' (received ${describe(sameSite)})`);
737
- if (priority !== void 0 && !Object.hasOwn(PRIORITY, priority)) throw invalid(name, `priority must be 'low', 'medium' or 'high' (received ${describe(priority)})`);
737
+ ]) if (flag !== void 0 && typeof flag !== "boolean") throw invalid$1(name, `${key} must be a boolean (received ${describe$1(flag)})`);
738
+ if (sameSite !== void 0 && !Object.hasOwn(SAME_SITE, sameSite)) throw invalid$1(name, `sameSite must be 'strict', 'lax' or 'none' (received ${describe$1(sameSite)})`);
739
+ if (priority !== void 0 && !Object.hasOwn(PRIORITY, priority)) throw invalid$1(name, `priority must be 'low', 'medium' or 'high' (received ${describe$1(priority)})`);
738
740
  return options;
739
741
  }
740
742
  /** Lower-cased, one leading `.` removed; an ASCII host name of at most 253 characters. */
741
743
  function normalizeDomain(name, domain) {
742
744
  const normalized = (domain.startsWith(".") ? domain.slice(1) : domain).toLowerCase();
743
745
  if (normalized.length > 1024) throw new RangeError(`Invalid cookie "${name}": the domain exceeds ${MAX_COOKIE_ATTRIBUTE_SIZE} bytes`);
744
- if (!isHostName(normalized)) throw invalid(name, `invalid domain ${describe(domain)}: expected an ASCII host name (letters, digits, -, dot-separated labels; IDNs in punycode)`);
746
+ if (!isHostName(normalized)) throw invalid$1(name, `invalid domain ${describe$1(domain)}: expected an ASCII host name (letters, digits, -, dot-separated labels; IDNs in punycode)`);
745
747
  return normalized;
746
748
  }
747
749
  function isHostName(domain) {
@@ -767,7 +769,7 @@ function checkPath(name, path) {
767
769
  const code = path.charCodeAt(index);
768
770
  valid = code >= 32 && code <= 126 && code !== 59;
769
771
  }
770
- if (!valid) throw invalid(name, `invalid path ${describe(path)}: expected "/..." made of visible ASCII characters without ";"`);
772
+ if (!valid) throw invalid$1(name, `invalid path ${describe$1(path)}: expected "/..." made of visible ASCII characters without ";"`);
771
773
  }
772
774
  /** The options without `undefined` values (they would erase defaults when spread). */
773
775
  function definedOnly(options) {
@@ -789,7 +791,7 @@ function parseSetCookie(line) {
789
791
  const pair = parts[0];
790
792
  const equals = pair.indexOf("=");
791
793
  const name = equals === -1 ? "" : trim(pair, 0, equals);
792
- if (name === "") throw new TypeError(`Invalid Set-Cookie line ${describe(line)}: missing name=value`);
794
+ if (name === "") throw new TypeError(`Invalid Set-Cookie line ${describe$1(line)}: missing name=value`);
793
795
  const info = {
794
796
  name,
795
797
  value: decodeCookieValue(unquote(trim(pair, equals + 1, pair.length))),
@@ -868,7 +870,7 @@ function prefixName(host, http) {
868
870
  if (host) return http ? "__Host-Http-" : "__Host-";
869
871
  return http ? "__Http-" : "__Secure-";
870
872
  }
871
- function invalid(name, reason) {
873
+ function invalid$1(name, reason) {
872
874
  return /* @__PURE__ */ new TypeError(`Invalid cookie "${name}": ${reason}`);
873
875
  }
874
876
  function byteLength(text) {
@@ -881,7 +883,7 @@ function byteLength(text) {
881
883
  }
882
884
  return size;
883
885
  }
884
- function describe(value) {
886
+ function describe$1(value) {
885
887
  if (typeof value === "string") return JSON.stringify(value.length > 64 ? `${value.slice(0, 64)}…` : value);
886
888
  if (value instanceof Date) return "an invalid Date";
887
889
  if (value === null) return "null";
@@ -976,6 +978,19 @@ function injectHeaders(headers) {
976
978
  }
977
979
  return normalized;
978
980
  }
981
+ /** Builds an {@link InjectResponse} (headers normalised by {@link injectHeaders}, cookies parsed). */
982
+ function injectResponse(status, headers, bytes) {
983
+ const normalized = injectHeaders(headers);
984
+ const body = DECODER.decode(bytes);
985
+ return {
986
+ status,
987
+ headers: normalized,
988
+ body,
989
+ bytes,
990
+ cookies: parseSetCookies(normalized["set-cookie"]),
991
+ json: () => JSON.parse(body)
992
+ };
993
+ }
979
994
  /** Whether a `content-type` is `text/event-stream`. */
980
995
  function isEventStreamType(contentType) {
981
996
  if (typeof contentType !== "string") return false;
@@ -1609,6 +1624,13 @@ var ConnectionTracker = class ConnectionTracker {
1609
1624
  for (const socket of this.#sockets) socket.destroy();
1610
1625
  }
1611
1626
  };
1627
+ /**
1628
+ * Marks a socket taken over by a WebSocket transport (RFC 0003) as busy: a graceful close of the
1629
+ * server leaves it to its transport, which closes it with a close frame; a forced close destroys it.
1630
+ */
1631
+ function markUpgraded(socket) {
1632
+ socket[ACTIVE] = 1;
1633
+ }
1612
1634
  /** Closes a `node:http` server: graceful (idle connections ended), then forced when asked. */
1613
1635
  function closeServer(server, tracker, closing, force) {
1614
1636
  const promise = closing ?? new Promise((resolve) => {
@@ -1626,6 +1648,582 @@ function closeServer(server, tracker, closing, force) {
1626
1648
  }
1627
1649
  return promise;
1628
1650
  }
1629
- export { TooManyRequestsException as $, parseSetCookie as A, BadGatewayException as B, DEFAULT_COOKIE_OPTIONS as C, givenCookieKey as D, encodeCookieValue as E, discardStream as F, GoneException as G, ConflictException as H, isByteStream as I, MethodNotAllowedException as J, HttpException as K, isStreamSource as L, serializeCookie as M, serializeResponseCookie as N, isCookieName as O, captureContext as P, ServiceUnavailableException as Q, toByteStream as R, webBodyStream as S, decodeCookieValue as T, ForbiddenException as U, BadRequestException as V, GatewayTimeoutException as W, NotImplementedException as X, NotFoundException as Y, PayloadTooLargeException as Z, lightInjectOptions as _, nodeBodyStream as a, issue as at, readWebStream as b, readNodeBody as c, isValidStatus as ct, writeNodeReply as d, UnauthorizedException as et, bufferInjectStream as f, isEventStreamType as g, injectNodeStream as h, nodeAdapterRequest as i, isHttpException as it, serializeClearCookie as j, parseCookieHeader as k, withNativeCookies as l, statusText as lt, injectFetchStream as m, closeServer as n, UnsupportedMediaTypeException as nt, normalizeNodeHeaders as o, toIssueDetail as ot, exceedsDeclaredLength as p, InternalServerErrorException as q, isEncrypted as r, ValidationException as rt, onResponseAbort as s, isBodylessStatus as st, ConnectionTracker as t, UnprocessableEntityException as tt, writeNodeFailure as u, lightStreamResponse as v, checkCookie as w, serverAddress as x, payloadTooLarge as y, trackStream as z };
1651
+ // src/decorators.ts
1652
+ function marker() {}
1653
+ const classMarker = marker;
1654
+ const methodMarker = marker;
1655
+ const parameterMarker = marker;
1656
+ const classOrMethodMarker = marker;
1657
+ /**
1658
+ * Declares a controller: a provider (singleton by default) whose decorated methods are routes.
1659
+ *
1660
+ * ```ts
1661
+ * @Controller('/users')
1662
+ * export class UserController {
1663
+ * constructor(private readonly users: UserService) {}
1664
+ *
1665
+ * @Get('/:id')
1666
+ * find(@Param('id') id: number) {
1667
+ * return this.users.findById(id)
1668
+ * }
1669
+ * }
1670
+ * ```
1671
+ *
1672
+ * @param pathOrOptions static: base path, or `{ path?, scope? }`.
1673
+ */
1674
+ function Controller(_pathOrOptions) {
1675
+ return classMarker;
1676
+ }
1677
+ /** `GET` route. `path` (static) is relative to the controller path; default: the controller path. */
1678
+ function Get(_path) {
1679
+ return methodMarker;
1680
+ }
1681
+ /** `POST` route (default status 201). */
1682
+ function Post(_path) {
1683
+ return methodMarker;
1684
+ }
1685
+ /** `PUT` route. */
1686
+ function Put(_path) {
1687
+ return methodMarker;
1688
+ }
1689
+ /** `PATCH` route. */
1690
+ function Patch(_path) {
1691
+ return methodMarker;
1692
+ }
1693
+ /** `DELETE` route. */
1694
+ function Delete(_path) {
1695
+ return methodMarker;
1696
+ }
1697
+ /** `HEAD` route (`GET` routes already answer `HEAD` requests). */
1698
+ function Head(_path) {
1699
+ return methodMarker;
1700
+ }
1701
+ /** `OPTIONS` route (paths without one answer `OPTIONS` with `204` and an `allow` header). */
1702
+ function Options(_path) {
1703
+ return methodMarker;
1704
+ }
1705
+ /** Route for GET, HEAD, POST, PUT, PATCH, DELETE and OPTIONS (methods without a dedicated route on the same path). */
1706
+ function All(_path) {
1707
+ return methodMarker;
1708
+ }
1709
+ function Sse(_pathOrOptions, _options) {
1710
+ return methodMarker;
1711
+ }
1712
+ function Param(_nameOrSchema, _schema) {
1713
+ return parameterMarker;
1714
+ }
1715
+ function Query(_nameOrSchema, _schema) {
1716
+ return parameterMarker;
1717
+ }
1718
+ function Cookie(_nameOrSchema, _schema) {
1719
+ return parameterMarker;
1720
+ }
1721
+ /**
1722
+ * Injects the **verified** value of a signed cookie (set with `{ signed: true }`, plugin option
1723
+ * `cookies.secrets`): absent, tampered or signed with a retired key, the cookie does not exist
1724
+ * (400 `Missing cookie "name"` unless the parameter is optional). Same coercion and schema rules as
1725
+ * {@link Cookie}. A route reading a signed cookie fails the boot when `cookies.secrets` is not set.
1726
+ *
1727
+ * @param name static: cookie name.
1728
+ * @param schema value: a Standard Schema (its output is injected).
1729
+ */
1730
+ function SignedCookie(_name, _schema) {
1731
+ return parameterMarker;
1732
+ }
1733
+ /**
1734
+ * Injects the parsed request body, validated with `schema` when given (the schema output is
1735
+ * injected: defaults, transforms, stripped keys). A non-primitive body without schema is reported
1736
+ * by the compiler (ZNIGHT017).
1737
+ *
1738
+ * @param schema value: a Standard Schema (Zod, Valibot, ArkType...).
1739
+ */
1740
+ function Body(_schema) {
1741
+ return parameterMarker;
1742
+ }
1743
+ /**
1744
+ * Injects a request header (case-insensitive `name`, coerced to the parameter type) or, without
1745
+ * name, every header (`Record<string, string | undefined>`).
1746
+ *
1747
+ * @param name static: header name.
1748
+ */
1749
+ function Headers$1(_name) {
1750
+ return parameterMarker;
1751
+ }
1752
+ function UploadedFile(_fieldOrOptions, _options) {
1753
+ return parameterMarker;
1754
+ }
1755
+ function UploadedFiles(_fieldOrOptions, _options) {
1756
+ return parameterMarker;
1757
+ }
1758
+ /**
1759
+ * Sets the maximum request body size of a route, or of every route of a controller (the route wins),
1760
+ * over the plugin `bodyLimit` (and over the limit computed for upload routes). `Infinity` disables
1761
+ * the limit, e.g. for a route streaming its body with `ctx.bodyStream()`.
1762
+ *
1763
+ * ```ts
1764
+ * @Put('/files/:name')
1765
+ * @BodyLimit('5gb')
1766
+ * async store(@Param('name') name: string, @Ctx() ctx: HttpContext) {
1767
+ * await pipeline(Readable.fromWeb(ctx.bodyStream()), createWriteStream(join(DIR, safeName(name))))
1768
+ * }
1769
+ * ```
1770
+ *
1771
+ * @param size static: bytes (`10_485_760`) or a size with a unit (`'10mb'`, `'512kb'`; `kb` = 1024 bytes).
1772
+ */
1773
+ function BodyLimit(_size) {
1774
+ return classOrMethodMarker;
1775
+ }
1776
+ /** Injects the native request of the adapter (escape hatch; the compiler warns). */
1777
+ function Req() {
1778
+ return parameterMarker;
1779
+ }
1780
+ /**
1781
+ * Injects the native response of the adapter (escape hatch; the compiler warns). The handler then
1782
+ * writes the response itself, and must have finished when its promise settles.
1783
+ */
1784
+ function Res() {
1785
+ return parameterMarker;
1786
+ }
1787
+ /** Injects the {@link HttpContext} of the request. */
1788
+ function Ctx() {
1789
+ return parameterMarker;
1790
+ }
1791
+ /**
1792
+ * Sets the status of successful responses (wins over `HttpResponse.status` and the defaults).
1793
+ *
1794
+ * @param status static: an integer between 100 and 599.
1795
+ */
1796
+ function HttpCode(_status) {
1797
+ return methodMarker;
1798
+ }
1799
+ /**
1800
+ * Adds a header to successful responses. Repeat the decorator for several headers.
1801
+ *
1802
+ * @param name static: header name.
1803
+ * @param value static: header value.
1804
+ */
1805
+ function Header(_name, _value) {
1806
+ return methodMarker;
1807
+ }
1808
+ /**
1809
+ * Protects a controller (every route) or a route with guards (providers implementing
1810
+ * {@link CanActivate}). Order: global guards → controller guards → route guards, each list in
1811
+ * declaration order.
1812
+ *
1813
+ * @param guards value: guard classes.
1814
+ */
1815
+ function UseGuard(..._guards) {
1816
+ return classOrMethodMarker;
1817
+ }
1818
+ /**
1819
+ * Wraps a controller (every route) or a route with interceptors (providers implementing
1820
+ * {@link HttpInterceptor}). Order: global → controller → route, the first one being the outermost.
1821
+ *
1822
+ * @param interceptors value: interceptor classes.
1823
+ */
1824
+ function UseInterceptor(..._interceptors) {
1825
+ return classOrMethodMarker;
1826
+ }
1827
+ /**
1828
+ * Validates the request body with `schema`: sugar for `@Body(schema)` on the body parameter (using
1829
+ * both is ZNIGHT005).
1830
+ *
1831
+ * @param schema value: a Standard Schema.
1832
+ */
1833
+ function Validate(_schema) {
1834
+ return methodMarker;
1835
+ }
1836
+ /**
1837
+ * Grammar of an explicit decision group name (internal; the compiler extension has the same rule).
1838
+ * A decorator without `group` is its own group, named after its key: any non-empty key is valid.
1839
+ */
1840
+ const GROUP_NAME = /^[A-Za-z][\w.-]*$/;
1841
+ /** `true` when `group` is a valid group of `key`: the key itself (the default) or a valid name. */
1842
+ function isGroupOf(group, key) {
1843
+ return typeof group === "string" && (group === key || GROUP_NAME.test(group));
1844
+ }
1845
+ /**
1846
+ * Creates a metadata decorator for controllers and routes. The compiler stores its argument
1847
+ * (`value`, statically evaluated or an exported reference; `true` when omitted) in
1848
+ * `meta[key]`, read by guards and interceptors through `ctx.route.meta` or `decorator.get(ctx)`.
1849
+ *
1850
+ * ```ts
1851
+ * export const Public = createRouteDecorator('public', { group: 'access', required: true })
1852
+ * export const Authenticated = createRouteDecorator('authenticated', { group: 'access', required: true })
1853
+ * export const Roles = createRouteDecorator<readonly string[]>('roles', { group: 'access', required: true })
1854
+ *
1855
+ * @Controller('/books')
1856
+ * @Roles(['reader']) // the decision of every route of the controller...
1857
+ * export class BookController {
1858
+ * @Get('/:id/cover')
1859
+ * @Public() // ...unless the route has its own decision of the group
1860
+ * cover() {}
1861
+ * }
1862
+ *
1863
+ * // in a guard
1864
+ * if (Public.get(ctx)) return true
1865
+ * const roles = Roles.get(ctx) ?? []
1866
+ * ```
1867
+ *
1868
+ * Decorators of one `group` are alternatives: two of them on the same class or method are
1869
+ * ZNIGHT005, and the route's decision replaces the controller's (without `group`, each decorator is
1870
+ * its own group: the route value wins over the controller value). With `required: true`, a route
1871
+ * without decision fails the build (ZNIGHT023).
1872
+ *
1873
+ * @param key static: unique metadata key.
1874
+ * @param options static: `{ group?, required? }`.
1875
+ */
1876
+ function createRouteDecorator(key, options) {
1877
+ if (typeof key !== "string" || key === "") throw new TypeError(`createRouteDecorator(): the key must be a non-empty string (received ${String(key)})`);
1878
+ if (options !== void 0 && (options === null || typeof options !== "object")) throw new TypeError(`createRouteDecorator('${key}'): options must be an object { group?, required? }`);
1879
+ const group = options?.group ?? key;
1880
+ if (!isGroupOf(group, key)) throw new TypeError(`createRouteDecorator('${key}'): "group" must be a name made of letters, digits, _ . - starting with a letter (received ${String(group)})`);
1881
+ const required = options?.required ?? false;
1882
+ if (typeof required !== "boolean") throw new TypeError(`createRouteDecorator('${key}'): "required" must be a boolean (received ${String(required)})`);
1883
+ const decorator = (..._args) => classOrMethodMarker;
1884
+ return Object.assign(decorator, {
1885
+ key,
1886
+ group,
1887
+ required,
1888
+ get: (ctx) => ctx.route?.meta[key],
1889
+ meta: (...args) => Object.freeze({
1890
+ key,
1891
+ value: args.length === 0 ? true : args[0],
1892
+ group,
1893
+ required
1894
+ })
1895
+ });
1896
+ }
1897
+ // src/upgrade.ts
1898
+ /** Calls `httpUpgrades()` on every other plugin implementing it, in plugin order, and validates the routes. */
1899
+ function collectPluginUpgrades(plugins, self, context) {
1900
+ const routes = [];
1901
+ const bunWebSocket = {};
1902
+ for (const plugin of plugins) {
1903
+ if (plugin === self) continue;
1904
+ const provider = plugin;
1905
+ if (typeof provider.httpUpgrades !== "function") continue;
1906
+ const name = String(provider.name);
1907
+ const result = provider.httpUpgrades(context);
1908
+ const declared = result?.routes;
1909
+ if (!Array.isArray(declared)) throw invalid(name, `httpUpgrades() must return { routes: [...] } (received ${describe(result)})`);
1910
+ for (const route of declared) routes.push(validateRoute(plugin, name, route, context.prefix));
1911
+ const settings = result.bunWebSocket;
1912
+ if (settings !== void 0) Object.assign(bunWebSocket, settings);
1913
+ }
1914
+ return {
1915
+ routes,
1916
+ bunWebSocket
1917
+ };
1918
+ }
1919
+ function validateRoute(instance, plugin, value, prefix) {
1920
+ if (value === null || typeof value !== "object") throw invalid(plugin, `invalid upgrade route ${describe(value)}`);
1921
+ const route = value;
1922
+ if (typeof route.name !== "string" || route.name === "") throw invalid(plugin, `the upgrade route ${String(route.path)} needs a name`);
1923
+ const where = `${String(route.path)} (${route.name})`;
1924
+ if (typeof route.path !== "string") throw invalid(plugin, `the upgrade route ${where} has no path`);
1925
+ if (typeof route.accept !== "function") throw invalid(plugin, `the upgrade route ${where} has no accept function`);
1926
+ if (route.precheck !== void 0 && typeof route.precheck !== "function") throw invalid(plugin, `the precheck of the upgrade route ${where} must be a function`);
1927
+ let path;
1928
+ try {
1929
+ path = parseRoutePath(joinPaths(prefix, route.path));
1930
+ } catch (error) {
1931
+ if (!(error instanceof RoutePathError)) throw error;
1932
+ throw invalid(plugin, `invalid path of the upgrade route ${where}: ${error.issue.message}`, error);
1933
+ }
1934
+ const meta = route.meta ?? [];
1935
+ if (!Array.isArray(meta)) throw invalid(plugin, `the meta of the upgrade route ${where} must be an array of entries`);
1936
+ const entries = meta.map((entry) => metaEntry(plugin, where, entry));
1937
+ const guards = route.guards ?? [];
1938
+ if (!Array.isArray(guards)) throw invalid(plugin, `the guards of the upgrade route ${where} must be an array`);
1939
+ const timeoutMs = route.timeoutMs;
1940
+ if (timeoutMs !== void 0 && !(typeof timeoutMs === "number" && timeoutMs > 0)) throw invalid(plugin, `invalid timeoutMs ${describe(timeoutMs)} of the upgrade route ${where}`);
1941
+ return {
1942
+ plugin,
1943
+ instance,
1944
+ path,
1945
+ relativePath: normalizePath(route.path),
1946
+ name: route.name,
1947
+ owner: route.owner ?? plugin,
1948
+ meta: entries,
1949
+ guards,
1950
+ timeoutMs: timeoutMs === Number.POSITIVE_INFINITY ? void 0 : timeoutMs,
1951
+ precheck: route.precheck?.bind(route),
1952
+ accept: route.accept.bind(route)
1953
+ };
1954
+ }
1955
+ function metaEntry(plugin, where, value) {
1956
+ const entry = value ?? {};
1957
+ if (value === null || typeof value !== "object" || typeof entry.key !== "string" || entry.key === "") throw invalid(plugin, `invalid meta entry ${describe(value)} of the upgrade route ${where}`);
1958
+ const group = entry.group ?? entry.key;
1959
+ const required = entry.required ?? false;
1960
+ if (!isGroupOf(group, entry.key) || typeof required !== "boolean") throw invalid(plugin, `invalid group of the meta entry "${entry.key}" of the upgrade route ${where}`);
1961
+ return {
1962
+ key: entry.key,
1963
+ value: entry.value,
1964
+ group,
1965
+ required
1966
+ };
1967
+ }
1968
+ function invalid(plugin, reason, cause) {
1969
+ return new ZnightError(ErrorCode.InvalidRoute, `HttpPlugin: plugin "${plugin}": ${reason}`, { ...cause === void 0 ? {} : { cause } });
1970
+ }
1971
+ function describe(value) {
1972
+ try {
1973
+ return typeof value === "function" ? "a function" : JSON.stringify(value) ?? String(value);
1974
+ } catch {
1975
+ return typeof value;
1976
+ }
1977
+ }
1978
+ const KEY = /^[A-Za-z0-9+/]{22}==$/;
1979
+ /** Whether a request asks for a WebSocket upgrade (`upgrade: websocket`, any case). */
1980
+ function isWebSocketUpgrade(headers) {
1981
+ const upgrade = headers["upgrade"];
1982
+ return upgrade !== void 0 && upgrade.length === 9 && upgrade.toLowerCase() === "websocket";
1983
+ }
1984
+ /**
1985
+ * The protocol checks of a handshake: `GET`, `sec-websocket-version: 13` (`426` with the supported
1986
+ * version otherwise) and a base64 16-byte `sec-websocket-key` (`400`).
1987
+ */
1988
+ function checkHandshake(ctx) {
1989
+ if (ctx.method !== "GET") throw new BadRequestException("A WebSocket handshake must be a GET request");
1990
+ if (ctx.headers["sec-websocket-version"] !== "13") throw new HttpException(426, "Unsupported WebSocket version", void 0, { headers: { "sec-websocket-version": "13" } });
1991
+ const key = ctx.headers["sec-websocket-key"];
1992
+ if (key === void 0 || !KEY.test(key.trim())) throw new BadRequestException("Invalid Sec-WebSocket-Key");
1993
+ }
1994
+ /** Internal: the pending upgrade of a handshake, created by the pipeline. */
1995
+ var PendingUpgradeState = class {
1996
+ native;
1997
+ #headers;
1998
+ /** The transport completed the handshake (`101`). */
1999
+ accepted = false;
2000
+ /** The handshake timed out: the transport must not complete it. */
2001
+ expired = false;
2002
+ held;
2003
+ constructor(native, headers) {
2004
+ this.native = native;
2005
+ this.#headers = headers;
2006
+ }
2007
+ get headers() {
2008
+ return this.#headers();
2009
+ }
2010
+ hold(closed) {
2011
+ if (this.held !== void 0) throw new Error("PendingUpgrade.hold() can only be called once");
2012
+ this.accepted = true;
2013
+ this.held = closed.then(() => void 0, () => void 0);
2014
+ }
2015
+ };
2016
+ // src/adapters/upgrade-io.ts
2017
+ const EMPTY = /* @__PURE__ */ new Uint8Array(0);
2018
+ /** A handshake has no body: Znight never reads one. */
2019
+ const NO_BODY = Promise.resolve(EMPTY);
2020
+ /** The `IncomingMessage` of a `node:http` upgrade as an {@link AdapterRequest}. */
2021
+ var NodeUpgradeRequest = class {
2022
+ method;
2023
+ url;
2024
+ headers;
2025
+ ip;
2026
+ remoteAddress;
2027
+ encrypted;
2028
+ raw;
2029
+ rawResponse;
2030
+ constructor(request, socket) {
2031
+ const peer = socket.remoteAddress ?? request.socket?.remoteAddress;
2032
+ this.method = request.method ?? "GET";
2033
+ this.url = request.url ?? "/";
2034
+ this.headers = normalizeNodeHeaders(request.headers);
2035
+ this.ip = peer;
2036
+ this.remoteAddress = peer;
2037
+ this.encrypted = request.socket?.encrypted === true;
2038
+ this.raw = request;
2039
+ this.rawResponse = socket;
2040
+ }
2041
+ readBody() {
2042
+ return NO_BODY;
2043
+ }
2044
+ hasBody() {
2045
+ return false;
2046
+ }
2047
+ };
2048
+ /** The Fetch `Request` of a `Bun.serve()` upgrade as an {@link AdapterRequest}. */
2049
+ var FetchUpgradeRequest = class {
2050
+ method;
2051
+ url;
2052
+ headers;
2053
+ ip;
2054
+ remoteAddress;
2055
+ encrypted;
2056
+ raw;
2057
+ rawResponse = void 0;
2058
+ constructor(request, url, headers, address) {
2059
+ this.method = request.method;
2060
+ this.url = url;
2061
+ this.headers = headers;
2062
+ this.ip = address;
2063
+ this.remoteAddress = address;
2064
+ this.encrypted = request.url.startsWith("https:");
2065
+ this.raw = request;
2066
+ }
2067
+ readBody() {
2068
+ return NO_BODY;
2069
+ }
2070
+ hasBody() {
2071
+ return false;
2072
+ }
2073
+ };
2074
+ /** An in-memory handshake (`HttpPlugin.upgradeInMemory()`) as an {@link AdapterRequest}. */
2075
+ var MemoryUpgradeRequest = class {
2076
+ method;
2077
+ url;
2078
+ headers;
2079
+ ip = "127.0.0.1";
2080
+ remoteAddress = "127.0.0.1";
2081
+ encrypted = false;
2082
+ raw = void 0;
2083
+ rawResponse = void 0;
2084
+ constructor(method, url, headers) {
2085
+ this.method = method;
2086
+ this.url = url;
2087
+ this.headers = headers;
2088
+ }
2089
+ readBody() {
2090
+ return NO_BODY;
2091
+ }
2092
+ hasBody() {
2093
+ return false;
2094
+ }
2095
+ };
2096
+ /** The bytes of a reply body (refusals are small: read whole). */
2097
+ async function replyBytes(reply) {
2098
+ const body = reply.body;
2099
+ if (body === null) return EMPTY;
2100
+ if (body instanceof Uint8Array) return body;
2101
+ if (body instanceof Blob) return new Uint8Array(await body.arrayBuffer());
2102
+ const chunks = [];
2103
+ let size = 0;
2104
+ for await (const chunk of body) {
2105
+ chunks.push(chunk);
2106
+ size += chunk.byteLength;
2107
+ }
2108
+ return concat(chunks, size);
2109
+ }
2110
+ /** Whether a header value can be written on a raw socket (no CR/LF injection). */
2111
+ function safeHeader(value) {
2112
+ return !/[\r\n]/.test(value);
2113
+ }
2114
+ /**
2115
+ * Writes a refused handshake as a raw HTTP/1.1 response on the upgrade socket, then ends it
2116
+ * (`connection: close`). Never throws.
2117
+ */
2118
+ async function writeRawRefusal(socket, reply) {
2119
+ try {
2120
+ const body = await replyBytes(reply);
2121
+ let head = `HTTP/1.1 ${reply.status} ${statusText(reply.status)}\r\n`;
2122
+ const headers = { ...reply.headers };
2123
+ headers["connection"] = "close";
2124
+ headers["content-length"] = String(body.byteLength);
2125
+ for (const [name, value] of Object.entries(headers)) for (const item of typeof value === "string" ? [value] : value) if (safeHeader(name) && safeHeader(item)) head += `${name}: ${item}\r\n`;
2126
+ head += "\r\n";
2127
+ if (socket.destroyed) return;
2128
+ socket.write(head);
2129
+ if (body.byteLength > 0) socket.write(body);
2130
+ socket.end();
2131
+ setTimeout(() => socket.destroy(), 1e3).unref?.();
2132
+ } catch {
2133
+ socket.destroy();
2134
+ }
2135
+ }
2136
+ /** The fallback reply when the pipeline failed (never expected). */
2137
+ const FAILURE = Object.freeze({
2138
+ status: 500,
2139
+ headers: {},
2140
+ body: null
2141
+ });
2142
+ /**
2143
+ * The upgrade capability of an adapter built on a `node:http` server (RFC 0003 §8.2.3): upgrade
2144
+ * requests of the gateway paths are routed to the pipeline through `shouldUpgradeCallback`
2145
+ * (composed with the server's own) and an `'upgrade'` listener; other upgrade requests are left to
2146
+ * the server (its own listeners, or ordinary requests).
2147
+ */
2148
+ var NodeUpgrades = class {
2149
+ kind = "node";
2150
+ #options;
2151
+ #attached = /* @__PURE__ */ new WeakSet();
2152
+ /** Whether {@link enable} was called. */
2153
+ get enabled() {
2154
+ return this.#options !== void 0;
2155
+ }
2156
+ enable(options) {
2157
+ this.#options = options;
2158
+ }
2159
+ /** Routes the gateway upgrades of `server` to the pipeline (once per server; no-op when not enabled). */
2160
+ attach(server) {
2161
+ const options = this.#options;
2162
+ if (options === void 0 || this.#attached.has(server)) return;
2163
+ this.#attached.add(server);
2164
+ const matches = (request) => {
2165
+ if (!isWebSocketUpgrade(request.headers)) return false;
2166
+ const url = request.url ?? "/";
2167
+ const query = url.indexOf("?");
2168
+ return options.match(query === -1 ? url : url.slice(0, query));
2169
+ };
2170
+ const target = server;
2171
+ const previous = target.shouldUpgradeCallback;
2172
+ const composable = typeof previous === "function";
2173
+ const listener = (request, socket, head) => {
2174
+ if (!matches(request)) {
2175
+ if (!composable && server.listenerCount("upgrade") === 1) writeRawRefusal(socket, {
2176
+ status: 404,
2177
+ headers: {},
2178
+ body: null
2179
+ });
2180
+ return;
2181
+ }
2182
+ socket.on("error", () => socket.destroy());
2183
+ markUpgraded(socket);
2184
+ const native = {
2185
+ kind: "node",
2186
+ request,
2187
+ socket,
2188
+ head
2189
+ };
2190
+ options.handle(new NodeUpgradeRequest(request, socket), native).then((reply) => {
2191
+ if (reply !== void 0) writeRawRefusal(socket, reply);
2192
+ }, () => void writeRawRefusal(socket, FAILURE));
2193
+ };
2194
+ if (previous !== void 0) target.shouldUpgradeCallback = function(request) {
2195
+ if (matches(request)) return true;
2196
+ const self = this ?? server;
2197
+ const view = Object.create(self, { listenerCount: { value: (name) => self.listenerCount(name) - (name === "upgrade" ? 1 : 0) } });
2198
+ return previous.call(view, request) === true;
2199
+ };
2200
+ server.on("upgrade", listener);
2201
+ }
2202
+ };
2203
+ /**
2204
+ * The `websocket` option of `Bun.serve()`: the plugins' settings, and handlers dispatching every
2205
+ * event to the {@link BunSocketHandlers} given as `data` to `server.upgrade()`.
2206
+ */
2207
+ function bunWebSocketOption(settings) {
2208
+ return Object.freeze({
2209
+ ...settings,
2210
+ open: (ws) => ws.data.open(ws),
2211
+ message: (ws, message) => ws.data.message(ws, message),
2212
+ close: (ws, code, reason) => ws.data.close(ws, code, reason),
2213
+ drain: (ws) => ws.data.drain(ws),
2214
+ pong: (ws) => ws.data.pong?.(ws)
2215
+ });
2216
+ }
2217
+ /** Builds the `Response` of a refused `Bun.serve()` upgrade. */
2218
+ async function refusalResponse(reply) {
2219
+ const headers = new Headers();
2220
+ for (const [name, value] of Object.entries(reply.headers)) for (const item of typeof value === "string" ? [value] : value) headers.append(name, item);
2221
+ const body = await replyBytes(reply);
2222
+ return new Response(body.byteLength === 0 ? null : body, {
2223
+ status: reply.status,
2224
+ headers
2225
+ });
2226
+ }
2227
+ export { exceedsDeclaredLength as $, Req as A, ConflictException as At, isGroupOf as B, ServiceUnavailableException as Bt, HttpCode as C, discardStream as Ct, Post as D, trackStream as Dt, Patch as E, toByteStream as Et, UploadedFiles as F, InternalServerErrorException as Ft, nodeBodyStream as G, ValidationException as Gt, closeServer as H, UnauthorizedException as Ht, UseGuard as I, MethodNotAllowedException as It, readNodeBody as J, toIssueDetail as Jt, normalizeNodeHeaders as K, isHttpException as Kt, UseInterceptor as L, NotFoundException as Lt, SignedCookie as M, GatewayTimeoutException as Mt, Sse as N, GoneException as Nt, Put as O, BadGatewayException as Ot, UploadedFile as P, HttpException as Pt, bufferInjectStream as Q, Validate as R, NotImplementedException as Rt, Headers$1 as S, captureContext as St, Param as T, isStreamSource as Tt, isEncrypted as U, UnprocessableEntityException as Ut, ConnectionTracker as V, TooManyRequestsException as Vt, nodeAdapterRequest as W, UnsupportedMediaTypeException as Wt, writeNodeFailure as X, isValidStatus as Xt, withNativeCookies as Y, isBodylessStatus as Yt, writeNodeReply as Z, statusText as Zt, Delete as _, parseCookieHeader as _t, refusalResponse as a, lightStreamResponse as at, Head as b, serializeCookie as bt, checkHandshake as c, readWebStream as ct, All as d, DEFAULT_COOKIE_OPTIONS as dt, injectFetchStream as et, Body as f, checkCookie as ft, Ctx as g, isCookieName as gt, Cookie as h, givenCookieKey as ht, bunWebSocketOption as i, lightInjectOptions as it, Res as j, ForbiddenException as jt, Query as k, BadRequestException as kt, collectPluginUpgrades as l, serverAddress as lt, Controller as m, encodeCookieValue as mt, MemoryUpgradeRequest as n, injectResponse as nt, replyBytes as o, normalizeInject as ot, BodyLimit as p, decodeCookieValue as pt, onResponseAbort as q, issue as qt, NodeUpgrades as r, isEventStreamType as rt, PendingUpgradeState as s, payloadTooLarge as st, FetchUpgradeRequest as t, injectNodeStream as tt, isWebSocketUpgrade as u, webBodyStream as ut, GROUP_NAME as v, parseSetCookie as vt, Options as w, isByteStream as wt, Header as x, serializeResponseCookie as xt, Get as y, serializeClearCookie as yt, createRouteDecorator as z, PayloadTooLargeException as zt };
1630
2228
 
1631
- //# sourceMappingURL=node-io-BkdRSQeg.mjs.map
2229
+ //# sourceMappingURL=upgrade-io-dTMDDmBi.mjs.map