@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.
- package/LICENSE +21 -0
- package/README.md +222 -0
- package/dist/auth.d.ts +139 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +991 -0
- package/dist/auth.js.map +1 -0
- package/dist/cache-status.d.ts +15 -0
- package/dist/cache-status.d.ts.map +1 -0
- package/dist/cache-status.js +152 -0
- package/dist/cache-status.js.map +1 -0
- package/dist/cache.d.ts +94 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +244 -0
- package/dist/cache.js.map +1 -0
- package/dist/client-hints.d.ts +23 -0
- package/dist/client-hints.d.ts.map +1 -0
- package/dist/client-hints.js +81 -0
- package/dist/client-hints.js.map +1 -0
- package/dist/conditional.d.ts +97 -0
- package/dist/conditional.d.ts.map +1 -0
- package/dist/conditional.js +300 -0
- package/dist/conditional.js.map +1 -0
- package/dist/content-disposition.d.ts +23 -0
- package/dist/content-disposition.d.ts.map +1 -0
- package/dist/content-disposition.js +122 -0
- package/dist/content-disposition.js.map +1 -0
- package/dist/cookie.d.ts +43 -0
- package/dist/cookie.d.ts.map +1 -0
- package/dist/cookie.js +472 -0
- package/dist/cookie.js.map +1 -0
- package/dist/cors.d.ts +53 -0
- package/dist/cors.d.ts.map +1 -0
- package/dist/cors.js +170 -0
- package/dist/cors.js.map +1 -0
- package/dist/datetime.d.ts +53 -0
- package/dist/datetime.d.ts.map +1 -0
- package/dist/datetime.js +205 -0
- package/dist/datetime.js.map +1 -0
- package/dist/digest.d.ts +220 -0
- package/dist/digest.d.ts.map +1 -0
- package/dist/digest.js +355 -0
- package/dist/digest.js.map +1 -0
- package/dist/encoding.d.ts +14 -0
- package/dist/encoding.d.ts.map +1 -0
- package/dist/encoding.js +86 -0
- package/dist/encoding.js.map +1 -0
- package/dist/etag.d.ts +55 -0
- package/dist/etag.d.ts.map +1 -0
- package/dist/etag.js +182 -0
- package/dist/etag.js.map +1 -0
- package/dist/ext-value.d.ts +40 -0
- package/dist/ext-value.d.ts.map +1 -0
- package/dist/ext-value.js +119 -0
- package/dist/ext-value.js.map +1 -0
- package/dist/forwarded.d.ts +14 -0
- package/dist/forwarded.d.ts.map +1 -0
- package/dist/forwarded.js +93 -0
- package/dist/forwarded.js.map +1 -0
- package/dist/header-utils.d.ts +71 -0
- package/dist/header-utils.d.ts.map +1 -0
- package/dist/header-utils.js +143 -0
- package/dist/header-utils.js.map +1 -0
- package/dist/headers.d.ts +71 -0
- package/dist/headers.d.ts.map +1 -0
- package/dist/headers.js +134 -0
- package/dist/headers.js.map +1 -0
- package/dist/hsts.d.ts +15 -0
- package/dist/hsts.d.ts.map +1 -0
- package/dist/hsts.js +106 -0
- package/dist/hsts.js.map +1 -0
- package/dist/http-signatures.d.ts +202 -0
- package/dist/http-signatures.d.ts.map +1 -0
- package/dist/http-signatures.js +720 -0
- package/dist/http-signatures.js.map +1 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +125 -0
- package/dist/index.js.map +1 -0
- package/dist/json-pointer.d.ts +97 -0
- package/dist/json-pointer.d.ts.map +1 -0
- package/dist/json-pointer.js +278 -0
- package/dist/json-pointer.js.map +1 -0
- package/dist/jsonpath.d.ts +98 -0
- package/dist/jsonpath.d.ts.map +1 -0
- package/dist/jsonpath.js +1470 -0
- package/dist/jsonpath.js.map +1 -0
- package/dist/language.d.ts +14 -0
- package/dist/language.d.ts.map +1 -0
- package/dist/language.js +95 -0
- package/dist/language.js.map +1 -0
- package/dist/link.d.ts +102 -0
- package/dist/link.d.ts.map +1 -0
- package/dist/link.js +437 -0
- package/dist/link.js.map +1 -0
- package/dist/linkset.d.ts +111 -0
- package/dist/linkset.d.ts.map +1 -0
- package/dist/linkset.js +501 -0
- package/dist/linkset.js.map +1 -0
- package/dist/negotiate.d.ts +71 -0
- package/dist/negotiate.d.ts.map +1 -0
- package/dist/negotiate.js +357 -0
- package/dist/negotiate.js.map +1 -0
- package/dist/pagination.d.ts +80 -0
- package/dist/pagination.d.ts.map +1 -0
- package/dist/pagination.js +188 -0
- package/dist/pagination.js.map +1 -0
- package/dist/prefer.d.ts +18 -0
- package/dist/prefer.d.ts.map +1 -0
- package/dist/prefer.js +93 -0
- package/dist/prefer.js.map +1 -0
- package/dist/problem.d.ts +54 -0
- package/dist/problem.d.ts.map +1 -0
- package/dist/problem.js +104 -0
- package/dist/problem.js.map +1 -0
- package/dist/proxy-status.d.ts +28 -0
- package/dist/proxy-status.d.ts.map +1 -0
- package/dist/proxy-status.js +220 -0
- package/dist/proxy-status.js.map +1 -0
- package/dist/range.d.ts +28 -0
- package/dist/range.d.ts.map +1 -0
- package/dist/range.js +243 -0
- package/dist/range.js.map +1 -0
- package/dist/response.d.ts +101 -0
- package/dist/response.d.ts.map +1 -0
- package/dist/response.js +200 -0
- package/dist/response.js.map +1 -0
- package/dist/sorting.d.ts +66 -0
- package/dist/sorting.d.ts.map +1 -0
- package/dist/sorting.js +168 -0
- package/dist/sorting.js.map +1 -0
- package/dist/structured-fields.d.ts +30 -0
- package/dist/structured-fields.d.ts.map +1 -0
- package/dist/structured-fields.js +468 -0
- package/dist/structured-fields.js.map +1 -0
- package/dist/types.d.ts +772 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -0
- package/dist/uri-template.d.ts +48 -0
- package/dist/uri-template.d.ts.map +1 -0
- package/dist/uri-template.js +483 -0
- package/dist/uri-template.js.map +1 -0
- package/dist/uri.d.ts +80 -0
- package/dist/uri.d.ts.map +1 -0
- package/dist/uri.js +423 -0
- package/dist/uri.js.map +1 -0
- 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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"}
|