@fluojs/http 1.1.2 → 3.0.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.
Files changed (125) hide show
  1. package/README.ko.md +430 -15
  2. package/README.md +437 -15
  3. package/dist/access-log-observer.d.ts +77 -0
  4. package/dist/access-log-observer.d.ts.map +1 -0
  5. package/dist/access-log-observer.js +171 -0
  6. package/dist/adapter.d.ts +9 -0
  7. package/dist/adapter.d.ts.map +1 -1
  8. package/dist/adapter.js +15 -1
  9. package/dist/adapters/binding.d.ts.map +1 -1
  10. package/dist/adapters/binding.js +18 -16
  11. package/dist/adapters/dto-binding-plan.d.ts +2 -2
  12. package/dist/adapters/dto-binding-plan.d.ts.map +1 -1
  13. package/dist/adapters/dto-binding-plan.js +13 -4
  14. package/dist/adapters/dto-validation-adapter.d.ts +2 -1
  15. package/dist/adapters/dto-validation-adapter.d.ts.map +1 -1
  16. package/dist/adapters/dto-validation-adapter.js +2 -2
  17. package/dist/byte-range-response.d.ts +71 -0
  18. package/dist/byte-range-response.d.ts.map +1 -0
  19. package/dist/byte-range-response.js +347 -0
  20. package/dist/client-identity.d.ts +8 -4
  21. package/dist/client-identity.d.ts.map +1 -1
  22. package/dist/client-identity.js +12 -93
  23. package/dist/compiled-route-identity.d.ts +17 -0
  24. package/dist/compiled-route-identity.d.ts.map +1 -0
  25. package/dist/compiled-route-identity.js +29 -0
  26. package/dist/connection.d.ts +67 -0
  27. package/dist/connection.d.ts.map +1 -0
  28. package/dist/connection.js +480 -0
  29. package/dist/context/request-context-node-store.d.ts +6 -0
  30. package/dist/context/request-context-node-store.d.ts.map +1 -1
  31. package/dist/context/request-context-node-store.js +14 -0
  32. package/dist/context/request-context.d.ts +5 -5
  33. package/dist/context/request-context.d.ts.map +1 -1
  34. package/dist/context/request-context.js +8 -83
  35. package/dist/context/request-id.d.ts +10 -0
  36. package/dist/context/request-id.d.ts.map +1 -0
  37. package/dist/context/request-id.js +27 -0
  38. package/dist/context/sse.d.ts +13 -0
  39. package/dist/context/sse.d.ts.map +1 -1
  40. package/dist/context/sse.js +36 -6
  41. package/dist/cookie-helpers.d.ts +47 -0
  42. package/dist/cookie-helpers.d.ts.map +1 -0
  43. package/dist/cookie-helpers.js +166 -0
  44. package/dist/decorators.d.ts +52 -19
  45. package/dist/decorators.d.ts.map +1 -1
  46. package/dist/decorators.js +57 -10
  47. package/dist/dispatch/conditional-request-policy.d.ts +67 -0
  48. package/dist/dispatch/conditional-request-policy.d.ts.map +1 -0
  49. package/dist/dispatch/conditional-request-policy.js +297 -0
  50. package/dist/dispatch/dispatch-content-negotiation.d.ts +8 -0
  51. package/dist/dispatch/dispatch-content-negotiation.d.ts.map +1 -1
  52. package/dist/dispatch/dispatch-content-negotiation.js +89 -42
  53. package/dist/dispatch/dispatch-error-negotiation.d.ts +26 -0
  54. package/dist/dispatch/dispatch-error-negotiation.d.ts.map +1 -0
  55. package/dist/dispatch/dispatch-error-negotiation.js +110 -0
  56. package/dist/dispatch/dispatch-error-policy.d.ts +1 -10
  57. package/dist/dispatch/dispatch-error-policy.d.ts.map +1 -1
  58. package/dist/dispatch/dispatch-error-policy.js +1 -33
  59. package/dist/dispatch/dispatch-error-representation.d.ts +18 -0
  60. package/dist/dispatch/dispatch-error-representation.d.ts.map +1 -0
  61. package/dist/dispatch/dispatch-error-representation.js +125 -0
  62. package/dist/dispatch/dispatch-response-metadata.d.ts +10 -0
  63. package/dist/dispatch/dispatch-response-metadata.d.ts.map +1 -0
  64. package/dist/dispatch/dispatch-response-metadata.js +12 -0
  65. package/dist/dispatch/dispatch-response-policy.d.ts +33 -5
  66. package/dist/dispatch/dispatch-response-policy.d.ts.map +1 -1
  67. package/dist/dispatch/dispatch-response-policy.js +149 -18
  68. package/dist/dispatch/dispatcher.d.ts +5 -1
  69. package/dist/dispatch/dispatcher.d.ts.map +1 -1
  70. package/dist/dispatch/dispatcher.js +178 -107
  71. package/dist/dispatch/fast-path/debug-visibility.d.ts.map +1 -1
  72. package/dist/dispatch/fast-path/debug-visibility.js +6 -3
  73. package/dist/dispatch/fast-path/dispatcher-state.d.ts +10 -0
  74. package/dist/dispatch/fast-path/dispatcher-state.d.ts.map +1 -0
  75. package/dist/dispatch/fast-path/dispatcher-state.js +52 -0
  76. package/dist/dispatch/fast-path/eligibility-checker.d.ts.map +1 -1
  77. package/dist/dispatch/fast-path/eligibility-checker.js +27 -15
  78. package/dist/dispatch/fast-path/eligibility.d.ts +16 -10
  79. package/dist/dispatch/fast-path/eligibility.d.ts.map +1 -1
  80. package/dist/dispatch/fast-path/eligibility.js +10 -4
  81. package/dist/dispatch/fast-path/fast-path-executor.d.ts +14 -1
  82. package/dist/dispatch/fast-path/fast-path-executor.d.ts.map +1 -1
  83. package/dist/dispatch/fast-path/fast-path-executor.js +23 -6
  84. package/dist/dispatch/request-abort.d.ts +22 -0
  85. package/dist/dispatch/request-abort.d.ts.map +1 -0
  86. package/dist/dispatch/request-abort.js +30 -0
  87. package/dist/dispatch/response-integration.d.ts +55 -0
  88. package/dist/dispatch/response-integration.d.ts.map +1 -0
  89. package/dist/dispatch/response-integration.js +57 -0
  90. package/dist/errors.d.ts +10 -0
  91. package/dist/errors.d.ts.map +1 -1
  92. package/dist/errors.js +19 -0
  93. package/dist/guards.d.ts +1 -1
  94. package/dist/guards.d.ts.map +1 -1
  95. package/dist/header-helpers.d.ts +59 -0
  96. package/dist/header-helpers.d.ts.map +1 -0
  97. package/dist/header-helpers.js +185 -0
  98. package/dist/index.d.ts +1 -16
  99. package/dist/index.d.ts.map +1 -1
  100. package/dist/index.js +4 -15
  101. package/dist/index.portable.d.ts +25 -0
  102. package/dist/index.portable.d.ts.map +1 -0
  103. package/dist/index.portable.js +21 -0
  104. package/dist/internal.d.ts +3 -1
  105. package/dist/internal.d.ts.map +1 -1
  106. package/dist/internal.js +2 -0
  107. package/dist/mapping.d.ts.map +1 -1
  108. package/dist/mapping.js +100 -15
  109. package/dist/middleware/correlation.d.ts +9 -1
  110. package/dist/middleware/correlation.d.ts.map +1 -1
  111. package/dist/middleware/correlation.js +25 -14
  112. package/dist/middleware/cors.d.ts.map +1 -1
  113. package/dist/middleware/cors.js +7 -8
  114. package/dist/middleware/middleware.d.ts +3 -3
  115. package/dist/middleware/middleware.d.ts.map +1 -1
  116. package/dist/middleware/rate-limit.d.ts +13 -2
  117. package/dist/middleware/rate-limit.d.ts.map +1 -1
  118. package/dist/middleware/rate-limit.js +1 -0
  119. package/dist/static-assets.d.ts +77 -0
  120. package/dist/static-assets.d.ts.map +1 -0
  121. package/dist/static-assets.js +270 -0
  122. package/dist/types.d.ts +225 -17
  123. package/dist/types.d.ts.map +1 -1
  124. package/dist/types.js +71 -1
  125. package/package.json +19 -9
@@ -1,10 +1,12 @@
1
1
  import { defineControllerMetadata, defineDtoFieldBindingMetadata, defineRouteMetadata, ensureMetadataSymbol, getControllerMetadata, getDtoFieldBindingMetadata, getRouteMetadata, getStandardMetadataBag as readStandardMetadataBag } from '@fluojs/core/internal';
2
+ import { InvalidHttpMethodError } from './errors.js';
2
3
  import { validateRoutePath } from './route-path.js';
3
4
  const standardControllerMetadataKey = Symbol.for('fluo.standard.controller');
4
5
  const standardRouteMetadataKey = Symbol.for('fluo.standard.route');
5
6
  const standardDtoBindingMetadataKey = Symbol.for('fluo.standard.dto-binding');
6
7
  const legacyRouteMetadataStore = new WeakMap();
7
8
  const legacyDtoBindingMetadataStore = new WeakMap();
9
+ const httpMethodTokenPattern = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
8
10
  ensureMetadataSymbol();
9
11
  function normalizeProducesMediaTypes(mediaTypes) {
10
12
  const normalized = [];
@@ -17,6 +19,16 @@ function normalizeProducesMediaTypes(mediaTypes) {
17
19
  }
18
20
  return normalized;
19
21
  }
22
+ function normalizeHttpRouteMethod(method) {
23
+ if (!httpMethodTokenPattern.test(method)) {
24
+ throw new InvalidHttpMethodError(`HTTP route method "${method}" must be a non-empty HTTP token.`);
25
+ }
26
+ const normalized = method.toUpperCase();
27
+ if (normalized === 'ALL') {
28
+ throw new InvalidHttpMethodError('HTTP route method "ALL" is reserved for the @All() wildcard decorator.');
29
+ }
30
+ return normalized;
31
+ }
20
32
  function mergeUnique(existing, values) {
21
33
  const merged = [...(existing ?? [])];
22
34
  for (const value of values) {
@@ -185,7 +197,7 @@ function mergeStandardDtoBinding(metadata, propertyKey, partial) {
185
197
  });
186
198
  }
187
199
  function createRouteDecorator(method, produces) {
188
- return path => {
200
+ return (path = '') => {
189
201
  validateRoutePath(path, `@${method}() path`);
190
202
  const decorator = (valueOrTarget, contextOrPropertyKey) => {
191
203
  if (isStandardDecoratorContext(contextOrPropertyKey)) {
@@ -302,66 +314,89 @@ export function Version(version) {
302
314
  /**
303
315
  * Registers a `GET` route handler.
304
316
  *
305
- * @param path Route path relative to the controller base path.
317
+ * @param path Relative route path; omission or `undefined` uses `''`, preserving the controller prefix.
306
318
  * @returns A method decorator that registers a `GET` handler mapping.
307
319
  */
308
320
  export const Get = createRouteDecorator('GET');
309
321
  /**
310
322
  * Registers a server-sent events route handler as `GET` with `text/event-stream` produces metadata.
311
323
  *
312
- * @param path Route path relative to the controller base path.
324
+ * @param path Relative route path; omission or `undefined` uses `''`, preserving the controller prefix.
313
325
  * @returns A method decorator that registers a `GET` SSE handler mapping.
314
326
  *
315
327
  * @remarks
316
328
  * Handlers may return `SseResponse` for manual control or `AsyncIterable<SseMessage<T> | T>` for managed dispatcher streaming.
317
329
  */
318
330
  export const Sse = createRouteDecorator('GET', ['text/event-stream']);
331
+ /**
332
+ * Registers a route handler for one validated HTTP method token.
333
+ *
334
+ * @param method HTTP method token, canonicalized to uppercase before metadata registration.
335
+ * @param path Relative route path; omission or `undefined` uses `''`, preserving the controller prefix.
336
+ * @returns A method decorator that registers the method-specific handler mapping.
337
+ *
338
+ * @throws {InvalidHttpMethodError} When `method` is empty, contains non-token characters, or is the reserved `ALL` sentinel.
339
+ *
340
+ * @remarks
341
+ * Adapter wire support remains runtime-specific. `CONNECT` is outside ordinary routing conformance,
342
+ * and custom methods are not implied to be OpenAPI Path Item operations.
343
+ */
344
+ export function Route(method, path = '') {
345
+ return createRouteDecorator(normalizeHttpRouteMethod(method))(path);
346
+ }
347
+ /**
348
+ * Registers an RFC `QUERY` route handler.
349
+ *
350
+ * @param path Relative route path; omission or `undefined` uses `''`, preserving the controller prefix.
351
+ * @returns A method decorator that registers a `QUERY` handler mapping.
352
+ */
353
+ export const Query = (path = '') => Route('QUERY', path);
319
354
  /**
320
355
  * Registers a `POST` route handler.
321
356
  *
322
- * @param path Route path relative to the controller base path.
357
+ * @param path Relative route path; omission or `undefined` uses `''`, preserving the controller prefix.
323
358
  * @returns A method decorator that registers a `POST` handler mapping.
324
359
  */
325
360
  export const Post = createRouteDecorator('POST');
326
361
  /**
327
362
  * Registers a `PUT` route handler.
328
363
  *
329
- * @param path Route path relative to the controller base path.
364
+ * @param path Relative route path; omission or `undefined` uses `''`, preserving the controller prefix.
330
365
  * @returns A method decorator that registers a `PUT` handler mapping.
331
366
  */
332
367
  export const Put = createRouteDecorator('PUT');
333
368
  /**
334
369
  * Registers a `PATCH` route handler.
335
370
  *
336
- * @param path Route path relative to the controller base path.
371
+ * @param path Relative route path; omission or `undefined` uses `''`, preserving the controller prefix.
337
372
  * @returns A method decorator that registers a `PATCH` handler mapping.
338
373
  */
339
374
  export const Patch = createRouteDecorator('PATCH');
340
375
  /**
341
376
  * Registers a `DELETE` route handler.
342
377
  *
343
- * @param path Route path relative to the controller base path.
378
+ * @param path Relative route path; omission or `undefined` uses `''`, preserving the controller prefix.
344
379
  * @returns A method decorator that registers a `DELETE` handler mapping.
345
380
  */
346
381
  export const Delete = createRouteDecorator('DELETE');
347
382
  /**
348
383
  * Registers an `OPTIONS` route handler.
349
384
  *
350
- * @param path Route path relative to the controller base path.
385
+ * @param path Relative route path; omission or `undefined` uses `''`, preserving the controller prefix.
351
386
  * @returns A method decorator that registers an `OPTIONS` handler mapping.
352
387
  */
353
388
  export const Options = createRouteDecorator('OPTIONS');
354
389
  /**
355
390
  * Registers a `HEAD` route handler.
356
391
  *
357
- * @param path Route path relative to the controller base path.
392
+ * @param path Relative route path; omission or `undefined` uses `''`, preserving the controller prefix.
358
393
  * @returns A method decorator that registers a `HEAD` handler mapping.
359
394
  */
360
395
  export const Head = createRouteDecorator('HEAD');
361
396
  /**
362
397
  * Registers a route handler that matches all HTTP methods.
363
398
  *
364
- * @param path Route path relative to the controller base path.
399
+ * @param path Relative route path; omission or `undefined` uses `''`, preserving the controller prefix.
365
400
  * @returns A method decorator that registers an all-method handler mapping.
366
401
  */
367
402
  export const All = createRouteDecorator('ALL');
@@ -447,6 +482,18 @@ export const FromCookie = createDtoFieldDecorator('cookie');
447
482
  * @returns A field decorator that marks the binding source as `body`.
448
483
  */
449
484
  export const FromBody = createDtoFieldDecorator('body');
485
+ /**
486
+ * Binds a DTO field from portable multipart files.
487
+ *
488
+ * The binding always yields a readonly array when the request exposes a file
489
+ * collection, preserving adapter order and filtering by the optional field name.
490
+ * An absent file collection remains absent so `@Optional()` can opt out of the
491
+ * normal required-field error.
492
+ *
493
+ * @param key Optional multipart field-name filter. Defaults to the DTO field name.
494
+ * @returns A field decorator that marks the binding source as `files`.
495
+ */
496
+ export const FromFiles = createDtoFieldDecorator('files');
450
497
 
451
498
  /**
452
499
  * Marks a DTO field binding as optional.
@@ -0,0 +1,67 @@
1
+ import type { ConditionalRequestContext, ConditionalRequestOptions, FrameworkResponse, FrameworkResponseSendOptions, ResponseValidators } from '../types.js';
2
+ /** Dispatcher outcome selected by RFC conditional request evaluation. */
3
+ export type ConditionalRequestOutcome = 'not-modified' | 'precondition-failed' | 'proceed';
4
+ /** Result of one dispatcher-owned conditional request evaluation. */
5
+ export interface ConditionalRequestResult {
6
+ /** Whether dispatch continues, produces 304, or produces 412. */
7
+ readonly outcome: ConditionalRequestOutcome;
8
+ /** Current representation validators that must remain visible to adapters. */
9
+ readonly validators: ResponseValidators | undefined;
10
+ }
11
+ /**
12
+ * Parses one RFC HTTP-date value using the supplied RFC850 reference year.
13
+ *
14
+ * @internal This seam keeps two-digit RFC850 year tests deterministic.
15
+ * @param header Raw HTTP-date header value.
16
+ * @param referenceYear Year used to expand an RFC850 two-digit year.
17
+ * @returns Parsed UTC timestamp in milliseconds, or `undefined` for invalid input.
18
+ */
19
+ export declare function parseHttpDate(header: string | undefined, referenceYear?: number): number | undefined;
20
+ /**
21
+ * Determines whether `If-Range` permits applying a requested byte range.
22
+ *
23
+ * A missing field permits the range. Entity-tag validation requires an exact
24
+ * strong match; valid dates match when the selected representation has not
25
+ * changed since that instant. All malformed or unavailable validators fall
26
+ * back to the complete representation.
27
+ *
28
+ * @param request Incoming request whose `If-Range` field is evaluated.
29
+ * @param validators Selected response validators used to match `If-Range`.
30
+ * @returns `true` when `If-Range` is absent or matches the selected representation.
31
+ */
32
+ export declare function matchesIfRange(request: ConditionalRequestContext['request'], validators: ResponseValidators | undefined): boolean;
33
+ /**
34
+ * Resolve the RFC validator precedence result before the selected route executes.
35
+ *
36
+ * @param options Dispatcher conditional request configuration.
37
+ * @param context Matched request and route descriptor.
38
+ * @returns The dispatch outcome and validators to preserve in the response.
39
+ */
40
+ export declare function resolveConditionalRequest(options: ConditionalRequestOptions, context: ConditionalRequestContext): Promise<ConditionalRequestResult>;
41
+ /**
42
+ * Evaluates conditional request fields against an already selected representation.
43
+ *
44
+ * @param request Adapter-normalized request carrying conditional request fields.
45
+ * @param resolution Current representation existence and validators.
46
+ * @returns The selected conditional outcome and validators.
47
+ */
48
+ export declare function resolveConditionalRequestRepresentation(request: ConditionalRequestContext['request'], resolution: Awaited<ReturnType<ConditionalRequestOptions['resolve']>>): ConditionalRequestResult;
49
+ /**
50
+ * Applies selected response validators through the portable response facade.
51
+ *
52
+ * @param response Mutable adapter-normalized response.
53
+ * @param validators Current representation validators, when available.
54
+ * @returns Nothing. The response receives only validator metadata.
55
+ */
56
+ export declare function applyResponseValidators(response: FrameworkResponse, validators: ResponseValidators | undefined): void;
57
+ /**
58
+ * Writes a bodyless conditional response through every supported adapter facade.
59
+ *
60
+ * @param response Mutable adapter-normalized response.
61
+ * @param outcome Selected non-proceed conditional request outcome.
62
+ * @param validators Current representation validators.
63
+ * @param options Optional adapter response-write controls.
64
+ * @returns A promise that settles after the adapter accepts the bodyless response.
65
+ */
66
+ export declare function writeConditionalResponse(response: FrameworkResponse, outcome: Exclude<ConditionalRequestOutcome, 'proceed'>, validators: ResponseValidators | undefined, options?: FrameworkResponseSendOptions): Promise<void>;
67
+ //# sourceMappingURL=conditional-request-policy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"conditional-request-policy.d.ts","sourceRoot":"","sources":["../../src/dispatch/conditional-request-policy.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,yBAAyB,EACzB,yBAAyB,EAEzB,iBAAiB,EACjB,4BAA4B,EAC5B,kBAAkB,EACnB,MAAM,aAAa,CAAC;AAErB,yEAAyE;AACzE,MAAM,MAAM,yBAAyB,GAAG,cAAc,GAAG,qBAAqB,GAAG,SAAS,CAAC;AAE3F,qEAAqE;AACrE,MAAM,WAAW,wBAAwB;IACvC,iEAAiE;IACjE,QAAQ,CAAC,OAAO,EAAE,yBAAyB,CAAC;IAC5C,8EAA8E;IAC9E,QAAQ,CAAC,UAAU,EAAE,kBAAkB,GAAG,SAAS,CAAC;CACrD;AA+LD;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,aAAa,SAA8B,GAC1C,MAAM,GAAG,SAAS,CA+DpB;AAeD;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,yBAAyB,CAAC,SAAS,CAAC,EAC7C,UAAU,EAAE,kBAAkB,GAAG,SAAS,GACzC,OAAO,CAsBT;AAED;;;;;;GAMG;AACH,wBAAsB,yBAAyB,CAC7C,OAAO,EAAE,yBAAyB,EAClC,OAAO,EAAE,yBAAyB,GACjC,OAAO,CAAC,wBAAwB,CAAC,CAGnC;AAED;;;;;;GAMG;AACH,wBAAgB,uCAAuC,CACrD,OAAO,EAAE,yBAAyB,CAAC,SAAS,CAAC,EAC7C,UAAU,EAAE,OAAO,CAAC,UAAU,CAAC,yBAAyB,CAAC,SAAS,CAAC,CAAC,CAAC,GACpE,wBAAwB,CAiD1B;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,iBAAiB,EAC3B,UAAU,EAAE,kBAAkB,GAAG,SAAS,GACzC,IAAI,CAUN;AACD;;;;;;;;GAQG;AACH,wBAAsB,wBAAwB,CAC5C,QAAQ,EAAE,iBAAiB,EAC3B,OAAO,EAAE,OAAO,CAAC,yBAAyB,EAAE,SAAS,CAAC,EACtD,UAAU,EAAE,kBAAkB,GAAG,SAAS,EAC1C,OAAO,CAAC,EAAE,4BAA4B,GACrC,OAAO,CAAC,IAAI,CAAC,CAIf"}
@@ -0,0 +1,297 @@
1
+ import { readFirstNonEmptyRequestHeaderValue } from '../header-helpers.js';
2
+
3
+ /** Dispatcher outcome selected by RFC conditional request evaluation. */
4
+
5
+ /** Result of one dispatcher-owned conditional request evaluation. */
6
+
7
+ function formatEntityTag(tag) {
8
+ return `${tag.strength === 'weak' ? 'W/' : ''}"${tag.opaqueValue}"`;
9
+ }
10
+ const INVALID_ENTITY_TAG_LIST = {
11
+ kind: 'invalid'
12
+ };
13
+ const HTTP_MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
14
+ const HTTP_WEEKDAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
15
+ const HTTP_WEEKDAY_NAMES = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
16
+ function isOptionalWhitespace(character) {
17
+ return character === ' ' || character === '\t';
18
+ }
19
+ function isEntityTagCharacter(character) {
20
+ const code = character.charCodeAt(0);
21
+ return code === 0x21 || code >= 0x23 && code <= 0x7e || code >= 0x80 && code <= 0xff;
22
+ }
23
+ function isValidEntityTag(tag) {
24
+ return [...tag.opaqueValue].every(isEntityTagCharacter);
25
+ }
26
+ function parseEntityTagList(value) {
27
+ let position = 0;
28
+ const skipOptionalWhitespace = () => {
29
+ while (isOptionalWhitespace(value[position])) {
30
+ position += 1;
31
+ }
32
+ };
33
+ skipOptionalWhitespace();
34
+ if (value[position] === '*') {
35
+ position += 1;
36
+ skipOptionalWhitespace();
37
+ return position === value.length ? {
38
+ kind: 'wildcard'
39
+ } : INVALID_ENTITY_TAG_LIST;
40
+ }
41
+ const tags = [];
42
+ while (position < value.length) {
43
+ const weak = value.startsWith('W/', position);
44
+ if (weak) {
45
+ position += 2;
46
+ }
47
+ if (value[position] !== '"') {
48
+ return INVALID_ENTITY_TAG_LIST;
49
+ }
50
+ position += 1;
51
+ const opaqueStart = position;
52
+ while (position < value.length && value[position] !== '"') {
53
+ if (!isEntityTagCharacter(value[position])) {
54
+ return INVALID_ENTITY_TAG_LIST;
55
+ }
56
+ position += 1;
57
+ }
58
+ if (value[position] !== '"') {
59
+ return INVALID_ENTITY_TAG_LIST;
60
+ }
61
+ tags.push({
62
+ opaqueValue: value.slice(opaqueStart, position),
63
+ strength: weak ? 'weak' : 'strong'
64
+ });
65
+ position += 1;
66
+ skipOptionalWhitespace();
67
+ if (position === value.length) {
68
+ return {
69
+ kind: 'tags',
70
+ tags
71
+ };
72
+ }
73
+ if (value[position] !== ',') {
74
+ return INVALID_ENTITY_TAG_LIST;
75
+ }
76
+ position += 1;
77
+ skipOptionalWhitespace();
78
+ if (position === value.length) {
79
+ return INVALID_ENTITY_TAG_LIST;
80
+ }
81
+ }
82
+ return INVALID_ENTITY_TAG_LIST;
83
+ }
84
+ function matchesEntityTag(parsed, current, comparison, resourceExists) {
85
+ if (parsed.kind === 'wildcard') {
86
+ return resourceExists;
87
+ }
88
+ if (parsed.kind !== 'tags' || !current || !isValidEntityTag(current)) {
89
+ return false;
90
+ }
91
+ return parsed.tags.some(requested => requested.opaqueValue === current.opaqueValue && (comparison === 'weak' || requested.strength === 'strong' && current.strength === 'strong'));
92
+ }
93
+ function parseMonth(value) {
94
+ const month = HTTP_MONTHS.indexOf(value);
95
+ return month === -1 ? undefined : month;
96
+ }
97
+ function parseDecimal(value) {
98
+ if (!/^\d+$/.test(value)) {
99
+ return undefined;
100
+ }
101
+ return Number(value);
102
+ }
103
+ function parseClock(hour, minute, second) {
104
+ const parsedHour = parseDecimal(hour);
105
+ const parsedMinute = parseDecimal(minute);
106
+ const parsedSecond = parseDecimal(second);
107
+ if (parsedHour === undefined || parsedMinute === undefined || parsedSecond === undefined || parsedHour > 23 || parsedMinute > 59 || parsedSecond > 59) {
108
+ return undefined;
109
+ }
110
+ return {
111
+ hour: parsedHour,
112
+ minute: parsedMinute,
113
+ second: parsedSecond
114
+ };
115
+ }
116
+ function createHttpDateTimestamp(weekday, day, month, year, hour, minute, second, weekdayNames) {
117
+ const parsedDay = parseDecimal(day);
118
+ const parsedMonth = parseMonth(month);
119
+ const parsedYear = parseDecimal(year);
120
+ const clock = parseClock(hour, minute, second);
121
+ if (parsedDay === undefined || parsedMonth === undefined || parsedYear === undefined || clock === undefined || parsedDay < 1 || parsedDay > 31 || !weekdayNames.includes(weekday)) {
122
+ return undefined;
123
+ }
124
+ const date = new Date(0);
125
+ date.setUTCFullYear(parsedYear, parsedMonth, parsedDay);
126
+ date.setUTCHours(clock.hour, clock.minute, clock.second, 0);
127
+ if (date.getUTCFullYear() !== parsedYear || date.getUTCMonth() !== parsedMonth || date.getUTCDate() !== parsedDay || weekdayNames[date.getUTCDay()] !== weekday) {
128
+ return undefined;
129
+ }
130
+ return date.getTime();
131
+ }
132
+
133
+ /**
134
+ * Parses one RFC HTTP-date value using the supplied RFC850 reference year.
135
+ *
136
+ * @internal This seam keeps two-digit RFC850 year tests deterministic.
137
+ * @param header Raw HTTP-date header value.
138
+ * @param referenceYear Year used to expand an RFC850 two-digit year.
139
+ * @returns Parsed UTC timestamp in milliseconds, or `undefined` for invalid input.
140
+ */
141
+ export function parseHttpDate(header, referenceYear = new Date().getUTCFullYear()) {
142
+ if (!header) {
143
+ return undefined;
144
+ }
145
+ const imfFixdate = /^(Sun|Mon|Tue|Wed|Thu|Fri|Sat), (\d{2}) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) (\d{4}) (\d{2}):(\d{2}):(\d{2}) GMT$/.exec(header);
146
+ if (imfFixdate) {
147
+ return createHttpDateTimestamp(imfFixdate[1], imfFixdate[2], imfFixdate[3], imfFixdate[4], imfFixdate[5], imfFixdate[6], imfFixdate[7], HTTP_WEEKDAYS);
148
+ }
149
+ const rfc850 = /^(Sunday|Monday|Tuesday|Wednesday|Thursday|Friday|Saturday), (\d{2})-(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)-(\d{2}) (\d{2}):(\d{2}):(\d{2}) GMT$/.exec(header);
150
+ if (rfc850) {
151
+ const twoDigitYear = parseDecimal(rfc850[4]);
152
+ if (twoDigitYear === undefined) {
153
+ return undefined;
154
+ }
155
+ let year = Math.floor(referenceYear / 100) * 100 + twoDigitYear;
156
+ if (year > referenceYear + 50) {
157
+ year -= 100;
158
+ }
159
+ return createHttpDateTimestamp(rfc850[1], rfc850[2], rfc850[3], String(year), rfc850[5], rfc850[6], rfc850[7], HTTP_WEEKDAY_NAMES);
160
+ }
161
+ const asctime = /^(Sun|Mon|Tue|Wed|Thu|Fri|Sat) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) ( {1}\d|\d{2}) (\d{2}):(\d{2}):(\d{2}) (\d{4})$/.exec(header);
162
+ if (!asctime) {
163
+ return undefined;
164
+ }
165
+ return createHttpDateTimestamp(asctime[1], asctime[3].trim(), asctime[2], asctime[7], asctime[4], asctime[5], asctime[6], HTTP_WEEKDAYS);
166
+ }
167
+ function normalizeLastModified(lastModified) {
168
+ if (!lastModified || Number.isNaN(lastModified.getTime())) {
169
+ return undefined;
170
+ }
171
+ return Math.floor(lastModified.getTime() / 1_000) * 1_000;
172
+ }
173
+ function isSafeMethod(method) {
174
+ const normalized = method.toUpperCase();
175
+ return normalized === 'GET' || normalized === 'HEAD';
176
+ }
177
+
178
+ /**
179
+ * Determines whether `If-Range` permits applying a requested byte range.
180
+ *
181
+ * A missing field permits the range. Entity-tag validation requires an exact
182
+ * strong match; valid dates match when the selected representation has not
183
+ * changed since that instant. All malformed or unavailable validators fall
184
+ * back to the complete representation.
185
+ *
186
+ * @param request Incoming request whose `If-Range` field is evaluated.
187
+ * @param validators Selected response validators used to match `If-Range`.
188
+ * @returns `true` when `If-Range` is absent or matches the selected representation.
189
+ */
190
+ export function matchesIfRange(request, validators) {
191
+ const ifRange = readFirstNonEmptyRequestHeaderValue(request, 'if-range');
192
+ if (!ifRange) {
193
+ return true;
194
+ }
195
+ const tags = parseEntityTagList(ifRange);
196
+ if (tags.kind === 'tags' && tags.tags.length === 1) {
197
+ const [tag] = tags.tags;
198
+ return tag?.strength === 'strong' && validators?.etag?.strength === 'strong' && tag.opaqueValue === validators.etag.opaqueValue;
199
+ }
200
+ const ifRangeDate = parseHttpDate(ifRange);
201
+ const lastModified = normalizeLastModified(validators?.lastModified);
202
+ return ifRangeDate !== undefined && lastModified !== undefined && lastModified <= ifRangeDate;
203
+ }
204
+
205
+ /**
206
+ * Resolve the RFC validator precedence result before the selected route executes.
207
+ *
208
+ * @param options Dispatcher conditional request configuration.
209
+ * @param context Matched request and route descriptor.
210
+ * @returns The dispatch outcome and validators to preserve in the response.
211
+ */
212
+ export async function resolveConditionalRequest(options, context) {
213
+ const resolution = await options.resolve(context);
214
+ return resolveConditionalRequestRepresentation(context.request, resolution);
215
+ }
216
+
217
+ /**
218
+ * Evaluates conditional request fields against an already selected representation.
219
+ *
220
+ * @param request Adapter-normalized request carrying conditional request fields.
221
+ * @param resolution Current representation existence and validators.
222
+ * @returns The selected conditional outcome and validators.
223
+ */
224
+ export function resolveConditionalRequestRepresentation(request, resolution) {
225
+ const validators = resolution.exists ? resolution.validators : undefined;
226
+ const ifMatch = parseEntityTagList(readFirstNonEmptyRequestHeaderValue(request, 'if-match') ?? '');
227
+ if (ifMatch.kind !== 'invalid') {
228
+ if (!matchesEntityTag(ifMatch, validators?.etag, 'strong', resolution.exists)) {
229
+ return {
230
+ outcome: 'precondition-failed',
231
+ validators
232
+ };
233
+ }
234
+ } else {
235
+ const lastModified = normalizeLastModified(validators?.lastModified);
236
+ const ifUnmodifiedSince = parseHttpDate(readFirstNonEmptyRequestHeaderValue(request, 'if-unmodified-since'));
237
+ if (ifUnmodifiedSince !== undefined && lastModified !== undefined && lastModified > ifUnmodifiedSince) {
238
+ return {
239
+ outcome: 'precondition-failed',
240
+ validators
241
+ };
242
+ }
243
+ }
244
+ const ifNoneMatch = parseEntityTagList(readFirstNonEmptyRequestHeaderValue(request, 'if-none-match') ?? '');
245
+ if (ifNoneMatch.kind !== 'invalid') {
246
+ if (matchesEntityTag(ifNoneMatch, validators?.etag, 'weak', resolution.exists)) {
247
+ return {
248
+ outcome: isSafeMethod(request.method) ? 'not-modified' : 'precondition-failed',
249
+ validators
250
+ };
251
+ }
252
+ } else {
253
+ const lastModified = normalizeLastModified(validators?.lastModified);
254
+ const ifModifiedSince = parseHttpDate(readFirstNonEmptyRequestHeaderValue(request, 'if-modified-since'));
255
+ if (isSafeMethod(request.method) && ifModifiedSince !== undefined && lastModified !== undefined && lastModified <= ifModifiedSince) {
256
+ return {
257
+ outcome: 'not-modified',
258
+ validators
259
+ };
260
+ }
261
+ }
262
+ return {
263
+ outcome: 'proceed',
264
+ validators
265
+ };
266
+ }
267
+
268
+ /**
269
+ * Applies selected response validators through the portable response facade.
270
+ *
271
+ * @param response Mutable adapter-normalized response.
272
+ * @param validators Current representation validators, when available.
273
+ * @returns Nothing. The response receives only validator metadata.
274
+ */
275
+ export function applyResponseValidators(response, validators) {
276
+ if (validators?.etag && isValidEntityTag(validators.etag)) {
277
+ response.setHeader('ETag', formatEntityTag(validators.etag));
278
+ }
279
+ const lastModified = normalizeLastModified(validators?.lastModified);
280
+ if (lastModified !== undefined) {
281
+ response.setHeader('Last-Modified', new Date(lastModified).toUTCString());
282
+ }
283
+ }
284
+ /**
285
+ * Writes a bodyless conditional response through every supported adapter facade.
286
+ *
287
+ * @param response Mutable adapter-normalized response.
288
+ * @param outcome Selected non-proceed conditional request outcome.
289
+ * @param validators Current representation validators.
290
+ * @param options Optional adapter response-write controls.
291
+ * @returns A promise that settles after the adapter accepts the bodyless response.
292
+ */
293
+ export async function writeConditionalResponse(response, outcome, validators, options) {
294
+ applyResponseValidators(response, validators);
295
+ response.setStatus(outcome === 'not-modified' ? 304 : 412);
296
+ await response.send(undefined, options);
297
+ }
@@ -1,3 +1,4 @@
1
+ import { NotAcceptableException } from '../exceptions.js';
1
2
  import type { ContentNegotiationOptions, FrameworkRequest, HandlerDescriptor, ResponseFormatter } from '../types.js';
2
3
  /**
3
4
  * Describes the resolved content negotiation contract.
@@ -7,6 +8,13 @@ export interface ResolvedContentNegotiation {
7
8
  formatters: ResponseFormatter[];
8
9
  normalizedMediaTypes: string[];
9
10
  }
11
+ /**
12
+ * Determines whether an error originated from response content negotiation.
13
+ *
14
+ * @param error Candidate error emitted while selecting a response formatter.
15
+ * @returns Whether the error requires `Vary: Accept` on its representation.
16
+ */
17
+ export declare function isContentNegotiationNotAcceptableException(error: unknown): error is NotAcceptableException;
10
18
  /**
11
19
  * Resolve content negotiation.
12
20
  *
@@ -1 +1 @@
1
- {"version":3,"file":"dispatch-content-negotiation.d.ts","sourceRoot":"","sources":["../../src/dispatch/dispatch-content-negotiation.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,yBAAyB,EACzB,gBAAgB,EAChB,iBAAiB,EACjB,iBAAiB,EAClB,MAAM,aAAa,CAAC;AAQrB;;GAEG;AACH,MAAM,WAAW,0BAA0B;IACzC,gBAAgB,EAAE,iBAAiB,CAAC;IACpC,UAAU,EAAE,iBAAiB,EAAE,CAAC;IAChC,oBAAoB,EAAE,MAAM,EAAE,CAAC;CAChC;AA2GD;;;;;GAKG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,yBAAyB,GAAG,SAAS,GAAG,0BAA0B,GAAG,SAAS,CA+BhI;AAqCD;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,iBAAiB,EAC1B,OAAO,EAAE,gBAAgB,EACzB,kBAAkB,EAAE,0BAA0B,GAC7C,iBAAiB,CA2CnB"}
1
+ {"version":3,"file":"dispatch-content-negotiation.d.ts","sourceRoot":"","sources":["../../src/dispatch/dispatch-content-negotiation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAE1D,OAAO,KAAK,EACV,yBAAyB,EACzB,gBAAgB,EAChB,iBAAiB,EACjB,iBAAiB,EAClB,MAAM,aAAa,CAAC;AASrB;;GAEG;AACH,MAAM,WAAW,0BAA0B;IACzC,gBAAgB,EAAE,iBAAiB,CAAC;IACpC,UAAU,EAAE,iBAAiB,EAAE,CAAC;IAChC,oBAAoB,EAAE,MAAM,EAAE,CAAC;CAChC;AAsBD;;;;;GAKG;AACH,wBAAgB,0CAA0C,CACxD,KAAK,EAAE,OAAO,GACb,KAAK,IAAI,sBAAsB,CAEjC;AA4ID;;;;;GAKG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,yBAAyB,GAAG,SAAS,GAAG,0BAA0B,GAAG,SAAS,CA+BhI;AAqCD;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,iBAAiB,EAC1B,OAAO,EAAE,gBAAgB,EACzB,kBAAkB,EAAE,0BAA0B,GAC7C,iBAAiB,CA4DnB"}