@irvinebroque/http-rfc-utils 0.1.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 (147) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +222 -0
  3. package/dist/auth.d.ts +139 -0
  4. package/dist/auth.d.ts.map +1 -0
  5. package/dist/auth.js +991 -0
  6. package/dist/auth.js.map +1 -0
  7. package/dist/cache-status.d.ts +15 -0
  8. package/dist/cache-status.d.ts.map +1 -0
  9. package/dist/cache-status.js +152 -0
  10. package/dist/cache-status.js.map +1 -0
  11. package/dist/cache.d.ts +94 -0
  12. package/dist/cache.d.ts.map +1 -0
  13. package/dist/cache.js +244 -0
  14. package/dist/cache.js.map +1 -0
  15. package/dist/client-hints.d.ts +23 -0
  16. package/dist/client-hints.d.ts.map +1 -0
  17. package/dist/client-hints.js +81 -0
  18. package/dist/client-hints.js.map +1 -0
  19. package/dist/conditional.d.ts +97 -0
  20. package/dist/conditional.d.ts.map +1 -0
  21. package/dist/conditional.js +300 -0
  22. package/dist/conditional.js.map +1 -0
  23. package/dist/content-disposition.d.ts +23 -0
  24. package/dist/content-disposition.d.ts.map +1 -0
  25. package/dist/content-disposition.js +122 -0
  26. package/dist/content-disposition.js.map +1 -0
  27. package/dist/cookie.d.ts +43 -0
  28. package/dist/cookie.d.ts.map +1 -0
  29. package/dist/cookie.js +472 -0
  30. package/dist/cookie.js.map +1 -0
  31. package/dist/cors.d.ts +53 -0
  32. package/dist/cors.d.ts.map +1 -0
  33. package/dist/cors.js +170 -0
  34. package/dist/cors.js.map +1 -0
  35. package/dist/datetime.d.ts +53 -0
  36. package/dist/datetime.d.ts.map +1 -0
  37. package/dist/datetime.js +205 -0
  38. package/dist/datetime.js.map +1 -0
  39. package/dist/digest.d.ts +220 -0
  40. package/dist/digest.d.ts.map +1 -0
  41. package/dist/digest.js +355 -0
  42. package/dist/digest.js.map +1 -0
  43. package/dist/encoding.d.ts +14 -0
  44. package/dist/encoding.d.ts.map +1 -0
  45. package/dist/encoding.js +86 -0
  46. package/dist/encoding.js.map +1 -0
  47. package/dist/etag.d.ts +55 -0
  48. package/dist/etag.d.ts.map +1 -0
  49. package/dist/etag.js +182 -0
  50. package/dist/etag.js.map +1 -0
  51. package/dist/ext-value.d.ts +40 -0
  52. package/dist/ext-value.d.ts.map +1 -0
  53. package/dist/ext-value.js +119 -0
  54. package/dist/ext-value.js.map +1 -0
  55. package/dist/forwarded.d.ts +14 -0
  56. package/dist/forwarded.d.ts.map +1 -0
  57. package/dist/forwarded.js +93 -0
  58. package/dist/forwarded.js.map +1 -0
  59. package/dist/header-utils.d.ts +71 -0
  60. package/dist/header-utils.d.ts.map +1 -0
  61. package/dist/header-utils.js +143 -0
  62. package/dist/header-utils.js.map +1 -0
  63. package/dist/headers.d.ts +71 -0
  64. package/dist/headers.d.ts.map +1 -0
  65. package/dist/headers.js +134 -0
  66. package/dist/headers.js.map +1 -0
  67. package/dist/hsts.d.ts +15 -0
  68. package/dist/hsts.d.ts.map +1 -0
  69. package/dist/hsts.js +106 -0
  70. package/dist/hsts.js.map +1 -0
  71. package/dist/http-signatures.d.ts +202 -0
  72. package/dist/http-signatures.d.ts.map +1 -0
  73. package/dist/http-signatures.js +720 -0
  74. package/dist/http-signatures.js.map +1 -0
  75. package/dist/index.d.ts +41 -0
  76. package/dist/index.d.ts.map +1 -0
  77. package/dist/index.js +125 -0
  78. package/dist/index.js.map +1 -0
  79. package/dist/json-pointer.d.ts +97 -0
  80. package/dist/json-pointer.d.ts.map +1 -0
  81. package/dist/json-pointer.js +278 -0
  82. package/dist/json-pointer.js.map +1 -0
  83. package/dist/jsonpath.d.ts +98 -0
  84. package/dist/jsonpath.d.ts.map +1 -0
  85. package/dist/jsonpath.js +1470 -0
  86. package/dist/jsonpath.js.map +1 -0
  87. package/dist/language.d.ts +14 -0
  88. package/dist/language.d.ts.map +1 -0
  89. package/dist/language.js +95 -0
  90. package/dist/language.js.map +1 -0
  91. package/dist/link.d.ts +102 -0
  92. package/dist/link.d.ts.map +1 -0
  93. package/dist/link.js +437 -0
  94. package/dist/link.js.map +1 -0
  95. package/dist/linkset.d.ts +111 -0
  96. package/dist/linkset.d.ts.map +1 -0
  97. package/dist/linkset.js +501 -0
  98. package/dist/linkset.js.map +1 -0
  99. package/dist/negotiate.d.ts +71 -0
  100. package/dist/negotiate.d.ts.map +1 -0
  101. package/dist/negotiate.js +357 -0
  102. package/dist/negotiate.js.map +1 -0
  103. package/dist/pagination.d.ts +80 -0
  104. package/dist/pagination.d.ts.map +1 -0
  105. package/dist/pagination.js +188 -0
  106. package/dist/pagination.js.map +1 -0
  107. package/dist/prefer.d.ts +18 -0
  108. package/dist/prefer.d.ts.map +1 -0
  109. package/dist/prefer.js +93 -0
  110. package/dist/prefer.js.map +1 -0
  111. package/dist/problem.d.ts +54 -0
  112. package/dist/problem.d.ts.map +1 -0
  113. package/dist/problem.js +104 -0
  114. package/dist/problem.js.map +1 -0
  115. package/dist/proxy-status.d.ts +28 -0
  116. package/dist/proxy-status.d.ts.map +1 -0
  117. package/dist/proxy-status.js +220 -0
  118. package/dist/proxy-status.js.map +1 -0
  119. package/dist/range.d.ts +28 -0
  120. package/dist/range.d.ts.map +1 -0
  121. package/dist/range.js +243 -0
  122. package/dist/range.js.map +1 -0
  123. package/dist/response.d.ts +101 -0
  124. package/dist/response.d.ts.map +1 -0
  125. package/dist/response.js +200 -0
  126. package/dist/response.js.map +1 -0
  127. package/dist/sorting.d.ts +66 -0
  128. package/dist/sorting.d.ts.map +1 -0
  129. package/dist/sorting.js +168 -0
  130. package/dist/sorting.js.map +1 -0
  131. package/dist/structured-fields.d.ts +30 -0
  132. package/dist/structured-fields.d.ts.map +1 -0
  133. package/dist/structured-fields.js +468 -0
  134. package/dist/structured-fields.js.map +1 -0
  135. package/dist/types.d.ts +772 -0
  136. package/dist/types.d.ts.map +1 -0
  137. package/dist/types.js +8 -0
  138. package/dist/types.js.map +1 -0
  139. package/dist/uri-template.d.ts +48 -0
  140. package/dist/uri-template.d.ts.map +1 -0
  141. package/dist/uri-template.js +483 -0
  142. package/dist/uri-template.js.map +1 -0
  143. package/dist/uri.d.ts +80 -0
  144. package/dist/uri.d.ts.map +1 -0
  145. package/dist/uri.js +423 -0
  146. package/dist/uri.js.map +1 -0
  147. package/package.json +66 -0
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Client Hints utilities per RFC 8942.
3
+ * RFC 8942 §2.2, §3.1, §3.2, §4.2.
4
+ * @see https://www.rfc-editor.org/rfc/rfc8942.html#section-3.1
5
+ */
6
+ import { mergeVary } from './headers.js';
7
+ import { parseSfList, serializeSfList } from './structured-fields.js';
8
+ const SF_TOKEN = /^[a-z*][a-z0-9_\-\.\*]*$/;
9
+ /**
10
+ * Parse Accept-CH header value into a list of client hints.
11
+ */
12
+ // RFC 8942 §3.1: Accept-CH is an sf-list of sf-token values.
13
+ export function parseAcceptCH(value) {
14
+ const values = Array.isArray(value) ? value : [value];
15
+ const hints = [];
16
+ for (const header of values) {
17
+ if (!header || !header.trim()) {
18
+ return null;
19
+ }
20
+ const list = parseSfList(header);
21
+ if (!list) {
22
+ return null;
23
+ }
24
+ for (const member of list) {
25
+ if ('items' in member) {
26
+ return null;
27
+ }
28
+ if (member.params && Object.keys(member.params).length > 0) {
29
+ return null;
30
+ }
31
+ if (typeof member.value !== 'string') {
32
+ return null;
33
+ }
34
+ if (!SF_TOKEN.test(member.value)) {
35
+ return null;
36
+ }
37
+ hints.push(member.value.toLowerCase());
38
+ }
39
+ }
40
+ const seen = new Set();
41
+ const unique = [];
42
+ for (const hint of hints) {
43
+ if (!seen.has(hint)) {
44
+ seen.add(hint);
45
+ unique.push(hint);
46
+ }
47
+ }
48
+ return unique;
49
+ }
50
+ /**
51
+ * Format Accept-CH header value from a list of client hints.
52
+ */
53
+ // RFC 8942 §3.1, §4.2: Accept-CH list serialization and token validation.
54
+ export function formatAcceptCH(hints) {
55
+ const list = hints.map((hint) => {
56
+ const token = hint.toLowerCase();
57
+ if (!SF_TOKEN.test(token)) {
58
+ throw new Error('Invalid client hint token');
59
+ }
60
+ return { value: token };
61
+ });
62
+ return serializeSfList(list);
63
+ }
64
+ /**
65
+ * Filter client hints to supported values.
66
+ */
67
+ // RFC 8942 §2.2: servers ignore hints they do not understand.
68
+ export function filterClientHints(hints, supported) {
69
+ const supportedSet = new Set(supported.map((hint) => hint.toLowerCase()));
70
+ return hints
71
+ .map((hint) => hint.toLowerCase())
72
+ .filter((hint) => supportedSet.has(hint));
73
+ }
74
+ /**
75
+ * Merge client hints into Vary header value.
76
+ */
77
+ // RFC 8942 §2.2, §3.2: add negotiated hints to Vary.
78
+ export function mergeClientHintsVary(existing, hints) {
79
+ return mergeVary(existing, hints);
80
+ }
81
+ //# sourceMappingURL=client-hints.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client-hints.js","sourceRoot":"","sources":["../src/client-hints.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Conditional Requests per RFC 9110.
3
+ * RFC 9110 §13.1.1-§13.1.4, §13.2.2.
4
+ * @see https://httpwg.org/specs/rfc9110.html#conditional.requests
5
+ */
6
+ import type { ETag, ConditionalResult } from './types.js';
7
+ /**
8
+ * Parse If-None-Match header into ETags or wildcard.
9
+ *
10
+ * Format: "etag1", "etag2" OR *
11
+ *
12
+ * @param header - If-None-Match header value
13
+ * @returns Array of ETags or '*' for wildcard
14
+ */
15
+ export declare function parseIfNoneMatch(header: string): ETag[] | '*';
16
+ /**
17
+ * Parse If-Match header into ETags or wildcard.
18
+ *
19
+ * @param header - If-Match header value
20
+ * @returns Array of ETags or '*' for wildcard
21
+ */
22
+ export declare function parseIfMatch(header: string): ETag[] | '*';
23
+ /**
24
+ * Evaluate If-Match precondition.
25
+ * Uses STRONG comparison.
26
+ *
27
+ * @param header - If-Match header value
28
+ * @param current - Current resource ETag
29
+ * @returns true if precondition passes, false if should return 412
30
+ */
31
+ export declare function evaluateIfMatch(header: string, current: ETag | null): boolean;
32
+ /**
33
+ * Evaluate If-None-Match precondition.
34
+ * Uses WEAK comparison.
35
+ *
36
+ * @param header - If-None-Match header value
37
+ * @param current - Current resource ETag
38
+ * @returns true if any ETag matches (should return 304 or 412)
39
+ */
40
+ export declare function evaluateIfNoneMatch(header: string, current: ETag | null): boolean;
41
+ /**
42
+ * Evaluate If-Modified-Since precondition.
43
+ *
44
+ * @param header - If-Modified-Since header value (HTTP-date)
45
+ * @param lastModified - Current resource modification date
46
+ * @returns true if NOT modified (should return 304), false otherwise
47
+ */
48
+ export declare function evaluateIfModifiedSince(header: string, lastModified: Date | null): boolean;
49
+ /**
50
+ * Evaluate If-Unmodified-Since precondition.
51
+ *
52
+ * @param header - If-Unmodified-Since header value (HTTP-date)
53
+ * @param lastModified - Current resource modification date
54
+ * @returns true if precondition passes, false if should return 412
55
+ */
56
+ export declare function evaluateIfUnmodifiedSince(header: string, lastModified: Date | null): boolean;
57
+ /**
58
+ * Full RFC 9110 precondition evaluation.
59
+ *
60
+ * Implements the precedence algorithm from RFC 9110 Section 13.2.2:
61
+ *
62
+ * 1. If If-Match present:
63
+ * - If NONE match (strong comparison) -> 412 Precondition Failed
64
+ * - If any match -> continue
65
+ *
66
+ * 2. If If-Match absent AND If-Unmodified-Since present:
67
+ * - If resource modified after date -> 412 Precondition Failed
68
+ * - If not modified -> continue
69
+ *
70
+ * 3. If If-None-Match present:
71
+ * - If ANY match (weak comparison):
72
+ * - GET/HEAD -> 304 Not Modified
73
+ * - Other methods -> 412 Precondition Failed
74
+ * - If none match -> continue
75
+ *
76
+ * 4. If If-None-Match absent AND If-Modified-Since present (GET/HEAD only):
77
+ * - If resource NOT modified since date -> 304 Not Modified
78
+ * - If modified -> continue
79
+ *
80
+ * 5. Otherwise -> proceed with request
81
+ *
82
+ * @param request - The incoming request
83
+ * @param currentETag - Current resource ETag (null if resource doesn't exist)
84
+ * @param lastModified - Current resource modification date (null if unknown)
85
+ * @returns ConditionalResult indicating whether to proceed
86
+ */
87
+ export declare function evaluatePreconditions(request: Request, currentETag: ETag | null, lastModified: Date | null): ConditionalResult;
88
+ /**
89
+ * Handle a conditional request (backward-compatible function).
90
+ *
91
+ * @param request - The incoming request
92
+ * @param etag - Current resource ETag (already formatted with quotes)
93
+ * @param lastModified - Current resource Last-Modified date
94
+ * @returns Response (304 or 412) if condition fails, null if request should proceed
95
+ */
96
+ export declare function handleConditionalRequest(request: Request, etag: string, lastModified: Date): Response | null;
97
+ //# sourceMappingURL=conditional.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"conditional.d.ts","sourceRoot":"","sources":["../src/conditional.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AA2B1D;;;;;;;GAOG;AAEH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,EAAE,GAAG,GAAG,CAsC7D;AAED;;;;;GAKG;AAEH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,EAAE,GAAG,GAAG,CAGzD;AAED;;;;;;;GAOG;AAEH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,GAAG,IAAI,GAAG,OAAO,CAqB7E;AAED;;;;;;;GAOG;AAEH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,GAAG,IAAI,GAAG,OAAO,CAqBjF;AAED;;;;;;GAMG;AAEH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,IAAI,GAAG,IAAI,GAAG,OAAO,CAY1F;AAED;;;;;;GAMG;AAEH,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,IAAI,GAAG,IAAI,GAAG,OAAO,CAkB5F;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,wBAAgB,qBAAqB,CACjC,OAAO,EAAE,OAAO,EAChB,WAAW,EAAE,IAAI,GAAG,IAAI,EACxB,YAAY,EAAE,IAAI,GAAG,IAAI,GAC1B,iBAAiB,CAoEnB;AAED;;;;;;;GAOG;AAEH,wBAAgB,wBAAwB,CACpC,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,IAAI,GACnB,QAAQ,GAAG,IAAI,CA2BjB"}
@@ -0,0 +1,300 @@
1
+ /**
2
+ * Conditional Requests per RFC 9110.
3
+ * RFC 9110 §13.1.1-§13.1.4, §13.2.2.
4
+ * @see https://httpwg.org/specs/rfc9110.html#conditional.requests
5
+ */
6
+ import { parseETag, formatETag, compareETags } from './etag.js';
7
+ import { parseHTTPDate, formatHTTPDate } from './datetime.js';
8
+ import { defaultCorsHeaders } from './cors.js';
9
+ /**
10
+ * Check if request method is safe (GET or HEAD).
11
+ */
12
+ function isSafeMethod(method) {
13
+ const upper = method.toUpperCase();
14
+ return upper === 'GET' || upper === 'HEAD';
15
+ }
16
+ function buildConditionalHeaders(currentETag, lastModified) {
17
+ const headers = { ...defaultCorsHeaders };
18
+ if (currentETag) {
19
+ headers['ETag'] = formatETag(currentETag);
20
+ }
21
+ if (lastModified) {
22
+ headers['Last-Modified'] = formatHTTPDate(lastModified);
23
+ }
24
+ return headers;
25
+ }
26
+ /**
27
+ * Parse If-None-Match header into ETags or wildcard.
28
+ *
29
+ * Format: "etag1", "etag2" OR *
30
+ *
31
+ * @param header - If-None-Match header value
32
+ * @returns Array of ETags or '*' for wildcard
33
+ */
34
+ // RFC 9110 §13.1.2: If-None-Match field value.
35
+ export function parseIfNoneMatch(header) {
36
+ const trimmed = header.trim();
37
+ if (trimmed === '*') {
38
+ return '*';
39
+ }
40
+ if (trimmed.includes('*')) {
41
+ return [];
42
+ }
43
+ const etags = [];
44
+ // ETags are in format: "value" or W/"value"
45
+ const regex = /(?:W\/)?\"[^\"]*\"/g;
46
+ let match;
47
+ while ((match = regex.exec(trimmed)) !== null) {
48
+ const value = match[0]?.trim();
49
+ const index = match.index;
50
+ if (!value) {
51
+ continue;
52
+ }
53
+ if (value.startsWith('"') && index >= 2) {
54
+ const prefix = trimmed.slice(index - 2, index);
55
+ if (prefix === 'w/') {
56
+ continue;
57
+ }
58
+ }
59
+ const parsed = parseETag(value);
60
+ if (parsed) {
61
+ etags.push(parsed);
62
+ }
63
+ }
64
+ return etags;
65
+ }
66
+ /**
67
+ * Parse If-Match header into ETags or wildcard.
68
+ *
69
+ * @param header - If-Match header value
70
+ * @returns Array of ETags or '*' for wildcard
71
+ */
72
+ // RFC 9110 §13.1.1: If-Match field value.
73
+ export function parseIfMatch(header) {
74
+ // Same format as If-None-Match
75
+ return parseIfNoneMatch(header);
76
+ }
77
+ /**
78
+ * Evaluate If-Match precondition.
79
+ * Uses STRONG comparison.
80
+ *
81
+ * @param header - If-Match header value
82
+ * @param current - Current resource ETag
83
+ * @returns true if precondition passes, false if should return 412
84
+ */
85
+ // RFC 9110 §13.1.1, §8.8.3: Strong comparison for If-Match.
86
+ export function evaluateIfMatch(header, current) {
87
+ const parsed = parseIfMatch(header);
88
+ // Wildcard: passes if resource exists
89
+ if (parsed === '*') {
90
+ return current !== null;
91
+ }
92
+ // If resource doesn't exist, If-Match always fails
93
+ if (current === null) {
94
+ return false;
95
+ }
96
+ // If-Match passes if ANY ETag matches (strong comparison)
97
+ for (const etag of parsed) {
98
+ if (compareETags(etag, current, true)) {
99
+ return true;
100
+ }
101
+ }
102
+ return false;
103
+ }
104
+ /**
105
+ * Evaluate If-None-Match precondition.
106
+ * Uses WEAK comparison.
107
+ *
108
+ * @param header - If-None-Match header value
109
+ * @param current - Current resource ETag
110
+ * @returns true if any ETag matches (should return 304 or 412)
111
+ */
112
+ // RFC 9110 §13.1.2, §8.8.3: Weak comparison for If-None-Match.
113
+ export function evaluateIfNoneMatch(header, current) {
114
+ const parsed = parseIfNoneMatch(header);
115
+ // If resource doesn't exist, nothing can match
116
+ if (current === null) {
117
+ return false;
118
+ }
119
+ // Wildcard: matches any existing resource
120
+ if (parsed === '*') {
121
+ return true;
122
+ }
123
+ // If-None-Match triggers if ANY ETag matches (weak comparison)
124
+ for (const etag of parsed) {
125
+ if (compareETags(etag, current, false)) {
126
+ return true;
127
+ }
128
+ }
129
+ return false;
130
+ }
131
+ /**
132
+ * Evaluate If-Modified-Since precondition.
133
+ *
134
+ * @param header - If-Modified-Since header value (HTTP-date)
135
+ * @param lastModified - Current resource modification date
136
+ * @returns true if NOT modified (should return 304), false otherwise
137
+ */
138
+ // RFC 9110 §13.1.3: If-Modified-Since evaluation.
139
+ export function evaluateIfModifiedSince(header, lastModified) {
140
+ if (lastModified === null) {
141
+ return false;
142
+ }
143
+ const sinceDate = parseHTTPDate(header);
144
+ if (sinceDate === null) {
145
+ return false;
146
+ }
147
+ // Not modified if lastModified <= sinceDate
148
+ return lastModified.getTime() <= sinceDate.getTime();
149
+ }
150
+ /**
151
+ * Evaluate If-Unmodified-Since precondition.
152
+ *
153
+ * @param header - If-Unmodified-Since header value (HTTP-date)
154
+ * @param lastModified - Current resource modification date
155
+ * @returns true if precondition passes, false if should return 412
156
+ */
157
+ // RFC 9110 §13.1.4: If-Unmodified-Since evaluation.
158
+ export function evaluateIfUnmodifiedSince(header, lastModified) {
159
+ if (header.trim() === '') {
160
+ return true;
161
+ }
162
+ const sinceDate = parseHTTPDate(header);
163
+ if (sinceDate === null) {
164
+ return true;
165
+ }
166
+ if (lastModified === null) {
167
+ // If we don't know when resource was modified, we can't say it's been modified
168
+ // RFC 9110 doesn't explicitly say, but logically precondition passes
169
+ return true;
170
+ }
171
+ // Precondition passes if lastModified <= sinceDate (not modified after that date)
172
+ return lastModified.getTime() <= sinceDate.getTime();
173
+ }
174
+ /**
175
+ * Full RFC 9110 precondition evaluation.
176
+ *
177
+ * Implements the precedence algorithm from RFC 9110 Section 13.2.2:
178
+ *
179
+ * 1. If If-Match present:
180
+ * - If NONE match (strong comparison) -> 412 Precondition Failed
181
+ * - If any match -> continue
182
+ *
183
+ * 2. If If-Match absent AND If-Unmodified-Since present:
184
+ * - If resource modified after date -> 412 Precondition Failed
185
+ * - If not modified -> continue
186
+ *
187
+ * 3. If If-None-Match present:
188
+ * - If ANY match (weak comparison):
189
+ * - GET/HEAD -> 304 Not Modified
190
+ * - Other methods -> 412 Precondition Failed
191
+ * - If none match -> continue
192
+ *
193
+ * 4. If If-None-Match absent AND If-Modified-Since present (GET/HEAD only):
194
+ * - If resource NOT modified since date -> 304 Not Modified
195
+ * - If modified -> continue
196
+ *
197
+ * 5. Otherwise -> proceed with request
198
+ *
199
+ * @param request - The incoming request
200
+ * @param currentETag - Current resource ETag (null if resource doesn't exist)
201
+ * @param lastModified - Current resource modification date (null if unknown)
202
+ * @returns ConditionalResult indicating whether to proceed
203
+ */
204
+ // RFC 9110 §13.2.2: Precondition evaluation order.
205
+ export function evaluatePreconditions(request, currentETag, lastModified) {
206
+ const method = request.method;
207
+ const headers = request.headers;
208
+ const ifMatch = headers.get('If-Match');
209
+ const ifNoneMatch = headers.get('If-None-Match');
210
+ const ifModifiedSince = headers.get('If-Modified-Since');
211
+ const ifUnmodifiedSince = headers.get('If-Unmodified-Since');
212
+ // Step 1: If-Match
213
+ if (ifMatch !== null) {
214
+ if (!evaluateIfMatch(ifMatch, currentETag)) {
215
+ return {
216
+ proceed: false,
217
+ status: 412,
218
+ headers: buildConditionalHeaders(currentETag, lastModified),
219
+ };
220
+ }
221
+ // If-Match passed, continue to next steps
222
+ }
223
+ // Step 2: If-Match absent AND If-Unmodified-Since present
224
+ if (ifMatch === null && ifUnmodifiedSince !== null) {
225
+ if (!evaluateIfUnmodifiedSince(ifUnmodifiedSince, lastModified)) {
226
+ return {
227
+ proceed: false,
228
+ status: 412,
229
+ headers: buildConditionalHeaders(currentETag, lastModified),
230
+ };
231
+ }
232
+ // If-Unmodified-Since passed, continue
233
+ }
234
+ // Step 3: If-None-Match
235
+ if (ifNoneMatch !== null) {
236
+ if (evaluateIfNoneMatch(ifNoneMatch, currentETag)) {
237
+ // ETag matches - what response depends on method
238
+ if (isSafeMethod(method)) {
239
+ return {
240
+ proceed: false,
241
+ status: 304,
242
+ headers: buildConditionalHeaders(currentETag, lastModified),
243
+ };
244
+ }
245
+ else {
246
+ return {
247
+ proceed: false,
248
+ status: 412,
249
+ headers: buildConditionalHeaders(currentETag, lastModified),
250
+ };
251
+ }
252
+ }
253
+ // No match, continue
254
+ }
255
+ // Step 4: If-None-Match absent AND If-Modified-Since present (GET/HEAD only)
256
+ if (ifNoneMatch === null && ifModifiedSince !== null && isSafeMethod(method)) {
257
+ if (evaluateIfModifiedSince(ifModifiedSince, lastModified)) {
258
+ return {
259
+ proceed: false,
260
+ status: 304,
261
+ headers: buildConditionalHeaders(currentETag, lastModified),
262
+ };
263
+ }
264
+ // Modified since, continue
265
+ }
266
+ // Step 5: All preconditions passed, proceed with request
267
+ return { proceed: true };
268
+ }
269
+ /**
270
+ * Handle a conditional request (backward-compatible function).
271
+ *
272
+ * @param request - The incoming request
273
+ * @param etag - Current resource ETag (already formatted with quotes)
274
+ * @param lastModified - Current resource Last-Modified date
275
+ * @returns Response (304 or 412) if condition fails, null if request should proceed
276
+ */
277
+ // RFC 9110 §13.2.2, §15.4.5, §15.5.13: Conditional request responses.
278
+ export function handleConditionalRequest(request, etag, lastModified) {
279
+ const parsedETag = parseETag(etag);
280
+ const result = evaluatePreconditions(request, parsedETag, lastModified);
281
+ if (result.proceed) {
282
+ return null;
283
+ }
284
+ // Build response headers
285
+ const responseHeaders = {
286
+ ...result.headers,
287
+ };
288
+ if (!responseHeaders['ETag']) {
289
+ responseHeaders['ETag'] = etag;
290
+ }
291
+ if (!responseHeaders['Last-Modified']) {
292
+ responseHeaders['Last-Modified'] = formatHTTPDate(lastModified);
293
+ }
294
+ // Cache-Control and Vary should be set by the caller as they know the caching policy
295
+ return new Response(null, {
296
+ status: result.status,
297
+ headers: responseHeaders,
298
+ });
299
+ }
300
+ //# sourceMappingURL=conditional.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"conditional.js","sourceRoot":"","sources":["../src/conditional.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Content-Disposition utilities per RFC 6266 + RFC 8187.
3
+ * RFC 6266 §4, §4.3; RFC 8187 §3.2.
4
+ * @see https://www.rfc-editor.org/rfc/rfc6266.html
5
+ * @see https://www.rfc-editor.org/rfc/rfc8187.html
6
+ */
7
+ import type { ContentDisposition, DispositionParams, ParamOptions } from './types.js';
8
+ export { decodeExtValue as parseExtValue, encodeExtValue } from './ext-value.js';
9
+ /**
10
+ * Parse Content-Disposition header.
11
+ */
12
+ export declare function parseContentDisposition(header: string): ContentDisposition | null;
13
+ /**
14
+ * Format a parameter value with optional RFC 8187 encoding.
15
+ * RFC 8187 §3.2.
16
+ * @see https://www.rfc-editor.org/rfc/rfc8187.html#section-3.2
17
+ */
18
+ export declare function formatHeaderParam(value: string, options?: ParamOptions): string;
19
+ /**
20
+ * Format Content-Disposition header.
21
+ */
22
+ export declare function formatContentDisposition(type: string, params?: DispositionParams): string;
23
+ //# sourceMappingURL=content-disposition.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content-disposition.d.ts","sourceRoot":"","sources":["../src/content-disposition.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EACR,kBAAkB,EAClB,iBAAiB,EACjB,YAAY,EACf,MAAM,YAAY,CAAC;AAWpB,OAAO,EAAE,cAAc,IAAI,aAAa,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAEjF;;GAEG;AAEH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,GAAG,kBAAkB,GAAG,IAAI,CA8DjF;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,GAAE,YAAiB,GAAG,MAAM,CAOnF;AAED;;GAEG;AAEH,wBAAgB,wBAAwB,CACpC,IAAI,EAAE,MAAM,EACZ,MAAM,GAAE,iBAAsB,GAC/B,MAAM,CAwCR"}
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Content-Disposition utilities per RFC 6266 + RFC 8187.
3
+ * RFC 6266 §4, §4.3; RFC 8187 §3.2.
4
+ * @see https://www.rfc-editor.org/rfc/rfc6266.html
5
+ * @see https://www.rfc-editor.org/rfc/rfc8187.html
6
+ */
7
+ import { isEmptyHeader, splitQuotedValue, unquote, quoteIfNeeded } from './header-utils.js';
8
+ import { decodeExtValue, encodeExtValue, needsExtendedEncoding, } from './ext-value.js';
9
+ // Re-export ext-value functions for backward compatibility.
10
+ // These were previously defined locally but are duplicates of ext-value.ts.
11
+ // parseExtValue is an alias for decodeExtValue (same behavior).
12
+ export { decodeExtValue as parseExtValue, encodeExtValue } from './ext-value.js';
13
+ /**
14
+ * Parse Content-Disposition header.
15
+ */
16
+ // RFC 6266 §4, §4.3: Content-Disposition field-value parsing.
17
+ export function parseContentDisposition(header) {
18
+ if (isEmptyHeader(header)) {
19
+ return null;
20
+ }
21
+ const parts = splitQuotedValue(header, ';');
22
+ const type = parts[0]?.trim();
23
+ if (!type) {
24
+ return null;
25
+ }
26
+ const params = {};
27
+ for (let i = 1; i < parts.length; i++) {
28
+ const part = parts[i].trim();
29
+ if (!part)
30
+ continue;
31
+ const eqIndex = part.indexOf('=');
32
+ if (eqIndex === -1)
33
+ continue;
34
+ const key = part.slice(0, eqIndex).trim().toLowerCase();
35
+ const rawValue = part.slice(eqIndex + 1).trim();
36
+ let value = unquote(rawValue);
37
+ if (!key) {
38
+ continue;
39
+ }
40
+ if (key === 'filename*') {
41
+ if (params['filename*'] !== undefined) {
42
+ continue;
43
+ }
44
+ const decoded = decodeExtValue(value);
45
+ if (decoded !== null) {
46
+ value = decoded.value;
47
+ }
48
+ params['filename*'] = value;
49
+ params.filename = value;
50
+ continue;
51
+ }
52
+ if (key.endsWith('*')) {
53
+ const decoded = decodeExtValue(value);
54
+ if (decoded !== null) {
55
+ value = decoded.value;
56
+ }
57
+ }
58
+ if (key === 'filename') {
59
+ if (params['filename*'] !== undefined || params.filename !== undefined) {
60
+ continue;
61
+ }
62
+ params.filename = value;
63
+ continue;
64
+ }
65
+ if (params[key] !== undefined) {
66
+ continue;
67
+ }
68
+ params[key] = value;
69
+ }
70
+ return { type: type.toLowerCase(), params };
71
+ }
72
+ /**
73
+ * Format a parameter value with optional RFC 8187 encoding.
74
+ * RFC 8187 §3.2.
75
+ * @see https://www.rfc-editor.org/rfc/rfc8187.html#section-3.2
76
+ */
77
+ export function formatHeaderParam(value, options = {}) {
78
+ const shouldExtend = options.extended || needsExtendedEncoding(value);
79
+ if (!shouldExtend) {
80
+ return quoteIfNeeded(value);
81
+ }
82
+ return encodeExtValue(value, { language: options.language });
83
+ }
84
+ /**
85
+ * Format Content-Disposition header.
86
+ */
87
+ // RFC 6266 §4, §4.3; RFC 8187 §3.2: Content-Disposition formatting.
88
+ export function formatContentDisposition(type, params = {}) {
89
+ const parts = [type];
90
+ const filename = params.filename;
91
+ const filenameStar = params.filenameStar;
92
+ if (filename) {
93
+ parts.push(`filename=${quoteIfNeeded(filename)}`);
94
+ }
95
+ if (filenameStar) {
96
+ const encoded = formatHeaderParam(filenameStar.value, {
97
+ extended: true,
98
+ language: filenameStar.language,
99
+ });
100
+ parts.push(`filename*=${encoded}`);
101
+ }
102
+ for (const [key, rawValue] of Object.entries(params)) {
103
+ if (key === 'filename' || key === 'filenameStar') {
104
+ continue;
105
+ }
106
+ if (rawValue === undefined) {
107
+ continue;
108
+ }
109
+ if (typeof rawValue === 'string') {
110
+ parts.push(`${key}=${quoteIfNeeded(rawValue)}`);
111
+ continue;
112
+ }
113
+ const keyWithStar = key.endsWith('*') ? key : `${key}*`;
114
+ const encoded = formatHeaderParam(rawValue.value, {
115
+ extended: true,
116
+ language: rawValue.language,
117
+ });
118
+ parts.push(`${keyWithStar}=${encoded}`);
119
+ }
120
+ return parts.join('; ');
121
+ }
122
+ //# sourceMappingURL=content-disposition.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content-disposition.js","sourceRoot":"","sources":["../src/content-disposition.ts"],"names":[],"mappings":"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"}