@dunx/http 2.4.0 → 2.5.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.
package/dist/index.js CHANGED
@@ -1,17 +1,21 @@
1
1
  // @bun
2
2
  import {
3
3
  HttpStatusCode,
4
+ TRACEPARENT_HEADER,
5
+ TRACESTATE_HEADER,
6
+ TraceContext,
4
7
  __decorateElement,
5
8
  __decoratorMetadata,
6
9
  __decoratorStart,
7
10
  __privateAdd,
8
11
  __privateGet,
9
12
  __runInitializers
10
- } from "./chunk-sz4pvqxy.js";
13
+ } from "./chunk-jh7jk0bn.js";
11
14
 
12
15
  // src/route/marker.ts
13
16
  var ROUTE = Symbol.for("dunx.route");
14
17
  var CONTROLLER = Symbol.for("dunx.controller");
18
+ var defaultStatusFor = (method) => method === "POST" ? HttpStatusCode.CREATED : HttpStatusCode.OK;
15
19
  var resolvePath = (path) => typeof path === "function" ? path() : path;
16
20
  var markRoute = (target, meta) => {
17
21
  Object.defineProperty(target, ROUTE, { value: meta, configurable: true });
@@ -1036,6 +1040,7 @@ class RequestLoggingMiddleware {
1036
1040
  #ignorePrefix;
1037
1041
  #correlateIgnored;
1038
1042
  #correlate;
1043
+ #trace;
1039
1044
  constructor(logger, context, options = {}) {
1040
1045
  this.logger = logger;
1041
1046
  this.context = context;
@@ -1046,6 +1051,7 @@ class RequestLoggingMiddleware {
1046
1051
  this.#ignorePrefix = options.ignorePrefix ?? [];
1047
1052
  this.#correlateIgnored = options.correlateIgnored ?? false;
1048
1053
  this.#correlate = options.correlate ?? true;
1054
+ this.#trace = options.trace ?? false;
1049
1055
  }
1050
1056
  #ignored(path) {
1051
1057
  if (this.#ignore.size > 0 && this.#ignore.has(path))
@@ -1071,6 +1077,14 @@ class RequestLoggingMiddleware {
1071
1077
  flow: "http",
1072
1078
  context: `${ctx.controller}.${ctx.handler}`
1073
1079
  };
1080
+ if (this.#trace) {
1081
+ const trace = TraceContext.adopt(req, requestId);
1082
+ scope.traceId = trace.traceId;
1083
+ scope.spanId = trace.spanId;
1084
+ if (trace.parentSpanId !== undefined) {
1085
+ scope.parentSpanId = trace.parentSpanId;
1086
+ }
1087
+ }
1074
1088
  return this.#correlate ? this.context.runWithContext(scope, () => this.#begin(req, ctx, url, mark, path, requestId, started, next, undefined)) : this.#begin(req, ctx, url, mark, path, requestId, started, next, scope);
1075
1089
  }
1076
1090
  #begin(req, ctx, url, mark, path, requestId, started, next, scope) {
@@ -1319,7 +1333,7 @@ var toResponse = (value, status) => {
1319
1333
  }
1320
1334
  return Response.json(value, { status });
1321
1335
  };
1322
- var statusFor = (route) => route.options?.status ?? (route.method === "POST" ? HttpStatusCode.CREATED : HttpStatusCode.OK);
1336
+ var statusFor = (route) => route.options?.status ?? defaultStatusFor(route.method);
1323
1337
  var assertNoCollisions = (discovered) => {
1324
1338
  const owners = new Map;
1325
1339
  for (const route of discovered) {
@@ -1810,6 +1824,274 @@ StaticModule = __decorateElement(_init, 0, "StaticModule", _dec, StaticModule);
1810
1824
  __runInitializers(_init, 1, StaticModule);
1811
1825
  __decoratorMetadata(_init, StaticModule);
1812
1826
  let _StaticModule = StaticModule;
1827
+ // src/compression/negotiate.ts
1828
+ var quality = (params) => {
1829
+ for (const param of params) {
1830
+ const [key, value] = param.split("=");
1831
+ if (key?.trim().toLowerCase() !== "q")
1832
+ continue;
1833
+ const q = Number.parseFloat(value ?? "");
1834
+ return Number.isFinite(q) && q >= 0 && q <= 1 ? q : 1;
1835
+ }
1836
+ return 1;
1837
+ };
1838
+ var negotiate = (header, offered) => {
1839
+ if (header === null)
1840
+ return;
1841
+ const accepted = new Map;
1842
+ for (const element of header.split(",")) {
1843
+ const [name, ...params] = element.split(";");
1844
+ const token = name?.trim().toLowerCase();
1845
+ if (token === undefined || token === "")
1846
+ continue;
1847
+ accepted.set(token, quality(params));
1848
+ }
1849
+ const wildcard = accepted.get("*");
1850
+ let best;
1851
+ let bestQuality = 0;
1852
+ for (const encoding of offered) {
1853
+ const q = accepted.get(encoding) ?? wildcard ?? 0;
1854
+ if (q > bestQuality) {
1855
+ best = encoding;
1856
+ bestQuality = q;
1857
+ }
1858
+ }
1859
+ return best;
1860
+ };
1861
+
1862
+ // src/compression/options.ts
1863
+ var CompressionEncoding = Object.freeze({
1864
+ ZSTD: "zstd",
1865
+ GZIP: "gzip"
1866
+ });
1867
+ var COMPRESSIBLE = new Set([
1868
+ "application/graphql",
1869
+ "application/graphql-response+json",
1870
+ "application/javascript",
1871
+ "application/json",
1872
+ "application/manifest+json",
1873
+ "application/wasm",
1874
+ "application/x-javascript",
1875
+ "application/x-ndjson",
1876
+ "application/xml",
1877
+ "image/svg+xml"
1878
+ ]);
1879
+ var isCompressibleType = (contentType) => {
1880
+ if (contentType === null)
1881
+ return false;
1882
+ const type = contentType.split(";")[0]?.trim().toLowerCase() ?? "";
1883
+ if (type.startsWith("text/"))
1884
+ return true;
1885
+ if (type.endsWith("+json") || type.endsWith("+xml"))
1886
+ return true;
1887
+ return COMPRESSIBLE.has(type);
1888
+ };
1889
+ var encodable = (encoding) => {
1890
+ const sync = encoding === CompressionEncoding.ZSTD ? Bun.zstdCompressSync : Bun.gzipSync;
1891
+ if (typeof sync !== "function")
1892
+ return false;
1893
+ try {
1894
+ new CompressionStream(encoding);
1895
+ return true;
1896
+ } catch {
1897
+ return false;
1898
+ }
1899
+ };
1900
+
1901
+ class CompressionOptions {
1902
+ encodings;
1903
+ threshold;
1904
+ filter;
1905
+ constructor(init = {}) {
1906
+ this.encodings = init.encodings ?? [
1907
+ CompressionEncoding.ZSTD,
1908
+ CompressionEncoding.GZIP
1909
+ ];
1910
+ const missing = this.encodings.filter((encoding) => !encodable(encoding));
1911
+ if (missing.length > 0) {
1912
+ throw new Error(`Bun ${Bun.version} cannot encode ${missing.join(", ")}. ` + "Pass `encodings` without it, or upgrade Bun.");
1913
+ }
1914
+ this.threshold = init.threshold ?? 1024;
1915
+ this.filter = init.filter ?? isCompressibleType;
1916
+ }
1917
+ }
1918
+ Object.defineProperty(CompressionOptions, Symbol.for("dunx.deps"), {
1919
+ value: () => [{ unresolved: "init: CompressionOptionsInit = {}" }]
1920
+ });
1921
+
1922
+ // src/compression/compression.ts
1923
+ var BODYLESS = new Set([204, 205, 304]);
1924
+ var BUFFER_LIMIT = 1024 * 1024;
1925
+ var buffer = async (body, limit) => {
1926
+ const reader = body.getReader();
1927
+ const chunks = [];
1928
+ let size = 0;
1929
+ for (;; ) {
1930
+ const { done, value } = await reader.read();
1931
+ if (done)
1932
+ break;
1933
+ chunks.push(value);
1934
+ size += value.byteLength;
1935
+ if (size > limit) {
1936
+ return {
1937
+ rest: new ReadableStream({
1938
+ start: (controller) => {
1939
+ for (const chunk of chunks)
1940
+ controller.enqueue(chunk);
1941
+ },
1942
+ pull: async (controller) => {
1943
+ const next = await reader.read();
1944
+ if (next.done)
1945
+ controller.close();
1946
+ else
1947
+ controller.enqueue(next.value);
1948
+ },
1949
+ cancel: (reason) => reader.cancel(reason)
1950
+ })
1951
+ };
1952
+ }
1953
+ }
1954
+ const bytes = new Uint8Array(size);
1955
+ let offset = 0;
1956
+ for (const chunk of chunks) {
1957
+ bytes.set(chunk, offset);
1958
+ offset += chunk.byteLength;
1959
+ }
1960
+ return { bytes };
1961
+ };
1962
+ var declaredLength = (headers) => {
1963
+ const raw = headers.get("content-length");
1964
+ if (raw === null)
1965
+ return;
1966
+ const length = Number.parseInt(raw, 10);
1967
+ return Number.isFinite(length) ? length : undefined;
1968
+ };
1969
+ var weakenETag = (headers) => {
1970
+ const etag = headers.get("etag");
1971
+ if (etag !== null && !etag.startsWith("W/"))
1972
+ headers.set("etag", `W/${etag}`);
1973
+ };
1974
+ var varyOnEncoding = (headers) => {
1975
+ const existing = headers.get("vary");
1976
+ if (existing === null) {
1977
+ headers.set("vary", "accept-encoding");
1978
+ return;
1979
+ }
1980
+ if (existing.trim() === "*")
1981
+ return;
1982
+ const listed = existing.split(",").some((field) => field.trim().toLowerCase() === "accept-encoding");
1983
+ if (!listed)
1984
+ headers.set("vary", `${existing}, accept-encoding`);
1985
+ };
1986
+ var encodeSync = (encoding, data) => {
1987
+ switch (encoding) {
1988
+ case CompressionEncoding.ZSTD:
1989
+ return Bun.zstdCompressSync(data);
1990
+ case CompressionEncoding.GZIP:
1991
+ return Bun.gzipSync(data);
1992
+ }
1993
+ };
1994
+
1995
+ class Compression {
1996
+ #options;
1997
+ constructor(options) {
1998
+ this.#options = options;
1999
+ }
2000
+ #considers(res) {
2001
+ if (BODYLESS.has(res.status) || res.status === 206)
2002
+ return false;
2003
+ if (res.headers.has("content-encoding"))
2004
+ return false;
2005
+ if (res.headers.get("cache-control")?.includes("no-transform") === true) {
2006
+ return false;
2007
+ }
2008
+ return this.#options.filter(res.headers.get("content-type"));
2009
+ }
2010
+ async handle(req, _ctx, next) {
2011
+ const res = await next();
2012
+ const body = res.body;
2013
+ if (body === null || !this.#considers(res))
2014
+ return res;
2015
+ varyOnEncoding(res.headers);
2016
+ const encoding = negotiate(req.headers.get("accept-encoding"), this.#options.encodings);
2017
+ if (encoding === undefined)
2018
+ return res;
2019
+ const declared = declaredLength(res.headers);
2020
+ if (declared !== undefined && declared < this.#options.threshold)
2021
+ return res;
2022
+ const headers = new Headers(res.headers);
2023
+ headers.set("content-encoding", encoding);
2024
+ weakenETag(headers);
2025
+ const source = declared !== undefined && declared > BUFFER_LIMIT ? { rest: body } : await buffer(body, BUFFER_LIMIT);
2026
+ if ("rest" in source) {
2027
+ headers.delete("content-length");
2028
+ return new Response(source.rest.pipeThrough(new CompressionStream(encoding)), { status: res.status, statusText: res.statusText, headers });
2029
+ }
2030
+ if (source.bytes.byteLength < this.#options.threshold) {
2031
+ const passthrough = new Headers(res.headers);
2032
+ passthrough.set("content-length", String(source.bytes.byteLength));
2033
+ return new Response(source.bytes, {
2034
+ status: res.status,
2035
+ statusText: res.statusText,
2036
+ headers: passthrough
2037
+ });
2038
+ }
2039
+ const encoded = encodeSync(encoding, source.bytes);
2040
+ headers.set("content-length", String(encoded.byteLength));
2041
+ return new Response(encoded, {
2042
+ status: res.status,
2043
+ statusText: res.statusText,
2044
+ headers
2045
+ });
2046
+ }
2047
+ }
2048
+ Object.defineProperty(Compression, Symbol.for("dunx.deps"), {
2049
+ value: () => [CompressionOptions]
2050
+ });
2051
+ // src/compression/module.ts
2052
+ import {
2053
+ Module as Module2,
2054
+ provide as provide3
2055
+ } from "@dunx/core";
2056
+ var middleware = () => provide3(Compression, {
2057
+ useFactory: (options) => new Compression(options),
2058
+ inject: [CompressionOptions]
2059
+ });
2060
+ var _dec = [
2061
+ Module2({})
2062
+ ];
2063
+ var _init = __decoratorStart(undefined);
2064
+
2065
+ class CompressionModule {
2066
+ static forRoot(init = {}) {
2067
+ return {
2068
+ module: CompressionModule,
2069
+ exports: [CompressionOptions, Compression],
2070
+ providers: [
2071
+ provide3(CompressionOptions, { useValue: new CompressionOptions(init) }),
2072
+ middleware()
2073
+ ]
2074
+ };
2075
+ }
2076
+ static forRootAsync(config) {
2077
+ return {
2078
+ module: CompressionModule,
2079
+ ...config.imports && { imports: config.imports },
2080
+ exports: [CompressionOptions, Compression],
2081
+ providers: [
2082
+ provide3(CompressionOptions, {
2083
+ useFactory: async (...deps) => new CompressionOptions(await config.useFactory(...deps)),
2084
+ inject: config.inject ?? []
2085
+ }),
2086
+ middleware()
2087
+ ]
2088
+ };
2089
+ }
2090
+ }
2091
+ CompressionModule = __decorateElement(_init, 0, "CompressionModule", _dec, CompressionModule);
2092
+ __runInitializers(_init, 1, CompressionModule);
2093
+ __decoratorMetadata(_init, CompressionModule);
2094
+ let _CompressionModule = CompressionModule;
1813
2095
  // src/throttle/decorators.ts
1814
2096
  var THROTTLE = metaKey("throttle");
1815
2097
  var SKIP_THROTTLE = metaKey("skip-throttle");
@@ -1995,20 +2277,20 @@ Object.defineProperty(ThrottleGuard, Symbol.for("dunx.deps"), {
1995
2277
  // src/throttle/module.ts
1996
2278
  import {
1997
2279
  Logger as Logger6,
1998
- Module as Module2,
1999
- provide as provide3
2280
+ Module as Module3,
2281
+ provide as provide4
2000
2282
  } from "@dunx/core";
2001
2283
  var EXPORTS = [ThrottleOptions, ThrottleStore, ThrottleGuard];
2002
- var guard = () => provide3(ThrottleGuard, {
2284
+ var guard = () => provide4(ThrottleGuard, {
2003
2285
  useFactory: (options, store, address, logger) => new ThrottleGuard(options, store, address, logger),
2004
2286
  inject: [ThrottleOptions, ThrottleStore, ClientAddress, Logger6]
2005
2287
  });
2006
- var store = () => provide3(ThrottleStore, {
2288
+ var store = () => provide4(ThrottleStore, {
2007
2289
  useFactory: (options) => options.store ?? new MemoryThrottleStore,
2008
2290
  inject: [ThrottleOptions]
2009
2291
  });
2010
2292
  var _dec = [
2011
- Module2({})
2293
+ Module3({})
2012
2294
  ];
2013
2295
  var _init = __decoratorStart(undefined);
2014
2296
 
@@ -2019,7 +2301,7 @@ class ThrottleModule {
2019
2301
  global: true,
2020
2302
  exports: EXPORTS,
2021
2303
  providers: [
2022
- provide3(ThrottleOptions, { useValue: new ThrottleOptions(init) }),
2304
+ provide4(ThrottleOptions, { useValue: new ThrottleOptions(init) }),
2023
2305
  store(),
2024
2306
  guard()
2025
2307
  ]
@@ -2032,7 +2314,7 @@ class ThrottleModule {
2032
2314
  ...config.imports && { imports: config.imports },
2033
2315
  exports: EXPORTS,
2034
2316
  providers: [
2035
- provide3(ThrottleOptions, {
2317
+ provide4(ThrottleOptions, {
2036
2318
  useFactory: async (...deps) => new ThrottleOptions(await config.useFactory(...deps)),
2037
2319
  inject: config.inject ?? []
2038
2320
  }),
@@ -2471,8 +2753,8 @@ Object.defineProperty(DiskIndicator, Symbol.for("dunx.deps"), {
2471
2753
  });
2472
2754
  // src/health/module.ts
2473
2755
  import {
2474
- Module as Module3,
2475
- provide as provide4
2756
+ Module as Module4,
2757
+ provide as provide5
2476
2758
  } from "@dunx/core";
2477
2759
 
2478
2760
  // src/health/readiness.ts
@@ -2519,15 +2801,15 @@ Object.defineProperty(Readiness, Symbol.for("dunx.deps"), {
2519
2801
  // src/health/module.ts
2520
2802
  var wiring = (options) => [
2521
2803
  ...options,
2522
- provide4(ReadinessOptions, {
2804
+ provide5(ReadinessOptions, {
2523
2805
  useFactory: (opts) => new ReadinessOptions({ drainDelayMs: opts.drainDelayMs }),
2524
2806
  inject: [HealthOptions]
2525
2807
  }),
2526
- provide4(Readiness, {
2808
+ provide5(Readiness, {
2527
2809
  useFactory: (opts) => new Readiness(opts),
2528
2810
  inject: [ReadinessOptions]
2529
2811
  }),
2530
- provide4(HealthRegistry, {
2812
+ provide5(HealthRegistry, {
2531
2813
  useFactory: (opts, readiness) => new HealthRegistry(opts, readiness),
2532
2814
  inject: [HealthOptions, Readiness]
2533
2815
  })
@@ -2535,7 +2817,7 @@ var wiring = (options) => [
2535
2817
  var surface = [HealthOptions, HealthRegistry, Readiness];
2536
2818
  var controllerFor = (documented) => documented ? HealthController : HiddenHealthController;
2537
2819
  var _dec = [
2538
- Module3({})
2820
+ Module4({})
2539
2821
  ];
2540
2822
  var _init = __decoratorStart(undefined);
2541
2823
 
@@ -2546,7 +2828,7 @@ class HealthModule {
2546
2828
  module: HealthModule,
2547
2829
  ...options.routes ? { controllers: [controllerFor(options.documented)] } : {},
2548
2830
  exports: surface,
2549
- providers: wiring([provide4(HealthOptions, { useValue: options })])
2831
+ providers: wiring([provide5(HealthOptions, { useValue: options })])
2550
2832
  };
2551
2833
  }
2552
2834
  static forRootAsync(config) {
@@ -2556,7 +2838,7 @@ class HealthModule {
2556
2838
  ...config.routes ?? true ? { controllers: [controllerFor(config.documented ?? true)] } : {},
2557
2839
  exports: surface,
2558
2840
  providers: wiring([
2559
- provide4(HealthOptions, {
2841
+ provide5(HealthOptions, {
2560
2842
  useFactory: async (...deps) => new HealthOptions(await config.useFactory(...deps)),
2561
2843
  inject: config.inject ?? []
2562
2844
  })
@@ -2571,6 +2853,10 @@ let _HealthModule = HealthModule;
2571
2853
  export {
2572
2854
  ApiHidden,
2573
2855
  ClientAddress,
2856
+ Compression,
2857
+ CompressionEncoding,
2858
+ CompressionModule,
2859
+ CompressionOptions,
2574
2860
  Controller,
2575
2861
  DEFAULT_RELAY_CHANNEL,
2576
2862
  DatabaseIndicator,
@@ -2626,11 +2912,14 @@ export {
2626
2912
  StaticModule,
2627
2913
  StaticOptions,
2628
2914
  THROTTLE,
2915
+ TRACEPARENT_HEADER,
2916
+ TRACESTATE_HEADER,
2629
2917
  Throttle,
2630
2918
  ThrottleGuard,
2631
2919
  ThrottleModule,
2632
2920
  ThrottleOptions,
2633
2921
  ThrottleStore,
2922
+ TraceContext,
2634
2923
  UNMATCHED,
2635
2924
  UseGuards,
2636
2925
  ValidationError,
@@ -2647,6 +2936,7 @@ export {
2647
2936
  decodeRelay,
2648
2937
  defaultErrorMapper,
2649
2938
  defaultRelayUrl,
2939
+ defaultStatusFor,
2650
2940
  discoverGateway,
2651
2941
  discoverGateways,
2652
2942
  discoverRoutes,
@@ -2655,6 +2945,7 @@ export {
2655
2945
  errorMapper,
2656
2946
  gatewaysOf,
2657
2947
  guardsOf,
2948
+ isCompressibleType,
2658
2949
  isErrorFilter,
2659
2950
  isGateway,
2660
2951
  joinPath,
@@ -2662,6 +2953,7 @@ export {
2662
2953
  meta,
2663
2954
  metaKey,
2664
2955
  metaOf,
2956
+ negotiate,
2665
2957
  normalizePath,
2666
2958
  normalizePrefix,
2667
2959
  observe,
@@ -2671,6 +2963,3 @@ export {
2671
2963
  withCors,
2672
2964
  withUpgradeRoutes
2673
2965
  };
2674
-
2675
- //# debugId=1A8278039B82BC0664756E2164756E21
2676
- //# sourceMappingURL=index.js.map
@@ -1,10 +1,10 @@
1
1
  import { type RoutePath } from './marker.js';
2
- import type { Input, RouteSchemas } from './schema.js';
2
+ import type { Input, Returns, RouteSchemas } from './schema.js';
3
3
  type ControllerTarget = abstract new (...args: never[]) => object;
4
4
  export declare const Controller: (prefix?: string) => <T extends ControllerTarget>(target: T) => T;
5
- export declare const Get: <const O extends RouteSchemas>(path?: RoutePath, options?: O) => <M extends (input: Input<O>) => unknown>(value: M, _context: ClassMethodDecoratorContext) => M;
6
- export declare const Post: <const O extends RouteSchemas>(path?: RoutePath, options?: O) => <M extends (input: Input<O>) => unknown>(value: M, _context: ClassMethodDecoratorContext) => M;
7
- export declare const Put: <const O extends RouteSchemas>(path?: RoutePath, options?: O) => <M extends (input: Input<O>) => unknown>(value: M, _context: ClassMethodDecoratorContext) => M;
8
- export declare const Patch: <const O extends RouteSchemas>(path?: RoutePath, options?: O) => <M extends (input: Input<O>) => unknown>(value: M, _context: ClassMethodDecoratorContext) => M;
9
- export declare const Delete: <const O extends RouteSchemas>(path?: RoutePath, options?: O) => <M extends (input: Input<O>) => unknown>(value: M, _context: ClassMethodDecoratorContext) => M;
5
+ export declare const Get: <const O extends RouteSchemas>(path?: RoutePath, options?: O | undefined) => <H extends (input: Input<O>) => Returns<O, "GET">>(value: H, _context: ClassMethodDecoratorContext) => H;
6
+ export declare const Post: <const O extends RouteSchemas>(path?: RoutePath, options?: O | undefined) => <H extends (input: Input<O>) => Returns<O, "POST">>(value: H, _context: ClassMethodDecoratorContext) => H;
7
+ export declare const Put: <const O extends RouteSchemas>(path?: RoutePath, options?: O | undefined) => <H extends (input: Input<O>) => Returns<O, "PUT">>(value: H, _context: ClassMethodDecoratorContext) => H;
8
+ export declare const Patch: <const O extends RouteSchemas>(path?: RoutePath, options?: O | undefined) => <H extends (input: Input<O>) => Returns<O, "PATCH">>(value: H, _context: ClassMethodDecoratorContext) => H;
9
+ export declare const Delete: <const O extends RouteSchemas>(path?: RoutePath, options?: O | undefined) => <H extends (input: Input<O>) => Returns<O, "DELETE">>(value: H, _context: ClassMethodDecoratorContext) => H;
10
10
  export {};
@@ -1,5 +1,18 @@
1
+ import { HttpStatusCode } from '../server/status.js';
1
2
  import type { RouteSchemas } from './schema.js';
2
3
  export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
4
+ /**
5
+ * The success status a route answers with when `options.status` is absent.
6
+ * `buildRoutes` and `@dunx/openapi`'s `statusOf` both read it, so the rule is
7
+ * stated once rather than in each of them.
8
+ */
9
+ export declare const defaultStatusFor: (method: HttpMethod) => number;
10
+ /**
11
+ * The type-level twin of {@link defaultStatusFor}, derived from the same constants
12
+ * so the two cannot drift. `Returns` needs it to know which `response` entry a
13
+ * handler is being held to.
14
+ */
15
+ export type DefaultStatus<M extends HttpMethod> = M extends 'POST' ? typeof HttpStatusCode.CREATED : typeof HttpStatusCode.OK;
3
16
  /**
4
17
  * A literal path, or a thunk read at **discovery** rather than at decoration.
5
18
  *
@@ -1,4 +1,5 @@
1
1
  import type { BunRequest } from 'bun';
2
+ import type { DefaultStatus, HttpMethod } from './marker.js';
2
3
  /**
3
4
  * Standard Schema v1, restated rather than depended on. The spec is an
4
5
  * *interface*, not a runtime: `@standard-schema/spec` ships nothing but these
@@ -65,11 +66,13 @@ export interface RouteSchemas {
65
66
  * } as const satisfies RouteSchemas;
66
67
  * ```
67
68
  *
68
- * **Never validated.** It documents the response; it does not enforce it.
69
- * Running a validation pass over every response body would be a per-request
70
- * cost paid for a documentation feature, which is the wrong trade - the
71
- * handler's own return type is what checks the answer, at compile time and for
72
- * free. Nothing in the request path reads this key.
69
+ * **Never validated at runtime, checked at compile time.** Running a validation
70
+ * pass over every response body would be a per-request cost paid for a
71
+ * documentation feature. The handler's own return type carries the check
72
+ * instead: the verb decorators constrain it against the entry for the success
73
+ * status, so a handler answering with a different shape is a `TS1241` naming
74
+ * the mismatched property. See {@link Returns}. Nothing in the request path
75
+ * reads this key.
73
76
  *
74
77
  * A plain {@link JsonSchema} is accepted here too, and only here: a JSON Schema
75
78
  * needs no conversion, so documenting a response costs no validator. `$id` names
@@ -83,8 +86,10 @@ export interface RouteSchemas {
83
86
  * }
84
87
  * ```
85
88
  */
86
- readonly response?: Readonly<Record<number, StandardSchemaV1 | JsonSchema>>;
89
+ readonly response?: ResponseMap;
87
90
  }
91
+ /** `response` keyed by status code. Named so {@link Returns} can constrain it. */
92
+ export type ResponseMap = Readonly<Record<number, StandardSchemaV1 | JsonSchema>>;
88
93
  /**
89
94
  * The handler's parameter type, derived from its own options object. It has to be
90
95
  * written out - a standard method decorator can *check* a parameter's type but
@@ -118,6 +123,58 @@ export type Input<O extends RouteSchemas> = {
118
123
  } ? {
119
124
  readonly params: InferOutput<P>;
120
125
  } : unknown);
126
+ /**
127
+ * The status a handler's return type is held to: an explicit `options.status`,
128
+ * else the verb's default. Widened to `number` without `as const`, which is what
129
+ * turns the check off rather than misapplying it.
130
+ */
131
+ type SuccessStatus<O extends RouteSchemas, M extends HttpMethod> = O extends {
132
+ status: infer S extends number;
133
+ } ? S : DefaultStatus<M>;
134
+ /**
135
+ * A plain {@link JsonSchema} carries no type to infer, so it becomes `unknown` and
136
+ * absorbs whatever the handler returns. That is the escape hatch for a response
137
+ * whose shape no schema value describes.
138
+ */
139
+ type Declared<S> = [InferOutput<S>] extends [never] ? unknown : Serialised<InferOutput<S>>;
140
+ /**
141
+ * The declared shape as JSON will present it, which is the same shape with every
142
+ * array made readonly.
143
+ *
144
+ * `z.array()` infers a mutable `T[]`, and `readonly T[]` is not assignable to it -
145
+ * so a repository method returning `readonly User[]`, the correct signature for
146
+ * something that must not be mutated, would fail against a document it satisfies.
147
+ * Mutability does not survive `Response.json`, so it is not part of the contract.
148
+ *
149
+ * Only arrays need the rewrite; TypeScript already ignores a property's `readonly`
150
+ * modifier when checking assignability. The object branch is how nested arrays are
151
+ * reached, and functions are returned untouched because mapping over one would
152
+ * discard its call signature.
153
+ */
154
+ type Serialised<T> = T extends readonly (infer E)[] ? readonly Serialised<E>[] : T extends (...args: never[]) => unknown ? T : T extends object ? {
155
+ readonly [K in keyof T]: Serialised<T[K]>;
156
+ } : T;
157
+ /**
158
+ * What a handler may return, given its own options object and its verb.
159
+ *
160
+ * A route decorator can *check* a handler's type but cannot *infer* it
161
+ * (docs/architecture/constraints.md), and that cuts both ways: this is the return
162
+ * half of the same guarantee `Input<O>` gives the parameter. Declaring
163
+ * `response: { 200: User }` stops being documentation a handler can contradict.
164
+ *
165
+ * `Response` is always allowed - it is the escape hatch `buildRoutes` passes
166
+ * through untouched. So is a promise of either. Nothing is checked when the
167
+ * success status has no `response` entry.
168
+ */
169
+ export type Returns<O extends RouteSchemas, M extends HttpMethod> = SuccessBody<O, M> | Response | Promise<SuccessBody<O, M> | Response>;
170
+ /**
171
+ * `infer R extends ResponseMap` is load bearing: without the constraint the
172
+ * narrowed `O` inside the branch is `{ response: R } & O`, whose `response` no
173
+ * longer satisfies `RouteSchemas`, and `SuccessStatus<O, M>` fails with `TS2344`.
174
+ */
175
+ type SuccessBody<O extends RouteSchemas, M extends HttpMethod> = O extends {
176
+ response: infer R extends ResponseMap;
177
+ } ? SuccessStatus<O, M> extends keyof R ? Declared<R[SuccessStatus<O, M>]> : unknown : unknown;
121
178
  /** What the framework actually hands a handler; `Input<O>` is its typed view. */
122
179
  export interface RouteInput {
123
180
  readonly req: BunRequest;
@@ -125,3 +182,4 @@ export interface RouteInput {
125
182
  readonly query?: unknown;
126
183
  readonly params?: unknown;
127
184
  }
185
+ export {};
@@ -97,6 +97,20 @@ export interface RequestLoggingOptions {
97
97
  * is for.
98
98
  */
99
99
  readonly correlate?: boolean;
100
+ /**
101
+ * Adopt W3C Trace Context, so `traceId`, `spanId` and `parentSpanId` join
102
+ * `requestId` on every line the request writes. Default **`false`**.
103
+ *
104
+ * On, an inbound `traceparent` is honoured and this service becomes a child
105
+ * span of the caller's; off, nothing reads the header and nothing is minted.
106
+ * It costs a header read and 8 random bytes per request, which is not worth
107
+ * paying in a service with nothing to correlate against - and `requestId`
108
+ * already spans two dunx services on its own.
109
+ *
110
+ * `@dunx/http/client` sends the adopted trace upstream, so turning this on at
111
+ * both ends is what makes one trace cover both.
112
+ */
113
+ readonly trace?: boolean;
100
114
  }
101
115
  /**
102
116
  * One structured entry per request, carrying the request and its response.
@@ -1,6 +1,6 @@
1
1
  import { type Ctor, type ModuleRef } from '@dunx/core';
2
2
  import type { DiscoveredRoute } from '../route/discover.js';
3
- import type { HttpMethod } from '../route/marker.js';
3
+ import { type HttpMethod } from '../route/marker.js';
4
4
  import type { UpgradeHandler } from '../ws/adapter.js';
5
5
  import { type CorsOptions } from './cors.js';
6
6
  import { type ErrorMapper } from './errors.js';