@johntalton/http-util 5.1.6 → 6.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.
- package/README.md +54 -7
- package/package.json +11 -5
- package/src/body.js +8 -8
- package/src/{response/defs.js → defs.js} +2 -1
- package/src/{accept-encoding.js → headers/accept-encoding.js} +2 -2
- package/src/{accept-language.js → headers/accept-language.js} +4 -5
- package/src/{accept.js → headers/accept.js} +18 -26
- package/src/headers/client-hints.js +81 -0
- package/src/{conditional.js → headers/conditional.js} +21 -12
- package/src/{content-disposition.js → headers/content-disposition.js} +26 -25
- package/src/{content-range.js → headers/content-range.js} +1 -1
- package/src/headers/content-type.js +101 -0
- package/src/{forwarded.js → headers/forwarded.js} +6 -23
- package/src/{index.js → headers/index.js} +4 -2
- package/src/{multipart.js → headers/multipart.js} +6 -5
- package/src/{preference.js → headers/preference.js} +3 -15
- package/src/{range.js → headers/range.js} +1 -1
- package/src/{server-timing.js → headers/server-timing.js} +2 -2
- package/src/headers/strict-transport-security.js +38 -0
- package/src/{accept-util.js → headers/util/accept-util.js} +8 -14
- package/src/headers/util/index.js +7 -0
- package/src/headers/util/kvp.js +79 -0
- package/src/headers/util/mime.js +77 -0
- package/src/headers/util/whitespace.js +6 -0
- package/src/{www-authenticate.js → headers/www-authenticate.js} +1 -1
- package/src/response/{accepted.js → 2xx/accepted.js} +2 -2
- package/src/response/{bytes.js → 2xx/bytes.js} +5 -5
- package/src/response/{created.js → 2xx/created.js} +4 -4
- package/src/response/{json.js → 2xx/json.js} +5 -5
- package/src/response/{no-content.js → 2xx/no-content.js} +4 -4
- package/src/response/{partial-content.js → 2xx/partial-content.js} +9 -9
- package/src/response/{preflight.js → 2xx/preflight.js} +4 -4
- package/src/response/{sse.js → 2xx/sse.js} +2 -2
- package/src/response/{trace.js → 2xx/trace.js} +3 -3
- package/src/response/{moved-permanently.js → 3xx/moved-permanently.js} +2 -2
- package/src/response/{multiple-choices.js → 3xx/multiple-choices.js} +2 -3
- package/src/response/{not-modified.js → 3xx/not-modified.js} +7 -7
- package/src/response/{permanent-redirect.js → 3xx/permanent-redirect.js} +2 -2
- package/src/response/{see-other.js → 3xx/see-other.js} +2 -2
- package/src/response/{temporary-redirect.js → 3xx/temporary-redirect.js} +2 -2
- package/src/response/{conflict.js → 4xx/conflict.js} +2 -2
- package/src/response/{content-too-large.js → 4xx/content-too-large.js} +2 -2
- package/src/response/{forbidden.js → 4xx/forbidden.js} +3 -2
- package/src/response/{gone.js → 4xx/gone.js} +2 -2
- package/src/response/{im-a-teapot.js → 4xx/im-a-teapot.js} +2 -2
- package/src/response/{not-acceptable.js → 4xx/not-acceptable.js} +3 -3
- package/src/response/{not-allowed.js → 4xx/not-allowed.js} +2 -2
- package/src/response/{not-found.js → 4xx/not-found.js} +3 -3
- package/src/response/{precondition-failed.js → 4xx/precondition-failed.js} +2 -2
- package/src/response/{range-not-satisfiable.js → 4xx/range-not-satisfiable.js} +4 -4
- package/src/response/{timeout.js → 4xx/timeout.js} +2 -2
- package/src/response/{too-many-requests.js → 4xx/too-many-requests.js} +5 -5
- package/src/response/{unauthorized.js → 4xx/unauthorized.js} +4 -4
- package/src/response/{unprocessable.js → 4xx/unprocessable.js} +2 -2
- package/src/response/{unsupported-media.js → 4xx/unsupported-media.js} +3 -3
- package/src/response/{error.js → 5xx/error.js} +3 -3
- package/src/response/{insufficient-storage.js → 5xx/insufficient-storage.js} +2 -2
- package/src/response/{not-implemented.js → 5xx/not-implemented.js} +4 -4
- package/src/response/{unavailable.js → 5xx/unavailable.js} +4 -4
- package/src/response/header-util.js +2 -2
- package/src/response/index.js +35 -35
- package/src/response/response.js +34 -34
- package/src/response/send-util.js +11 -13
- package/src/content-type.js +0 -148
- /package/src/{cache-control.js → headers/cache-control.js} +0 -0
- /package/src/{clear-site-data.js → headers/clear-site-data.js} +0 -0
- /package/src/{fetch-metadata.js → headers/fetch-metadata.js} +0 -0
- /package/src/{link.js → headers/link.js} +0 -0
- /package/src/{rate-limit.js → headers/rate-limit.js} +0 -0
- /package/src/{quote.js → headers/util/quote.js} +0 -0
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Set of utilities to aid in building from-scratch [node:http2](https://nodejs.org
|
|
|
6
6
|
- [Stream Response methods](#response)
|
|
7
7
|
- [Body parser](#body)
|
|
8
8
|
|
|
9
|
-
## Header Parsers
|
|
9
|
+
## Header Parsers / Encoders
|
|
10
10
|
### From Client:
|
|
11
11
|
- Accept Encoding - `parse` and `select` based on server/client values
|
|
12
12
|
- Accept Language - `parse` and `select`
|
|
@@ -17,10 +17,21 @@ Set of utilities to aid in building from-scratch [node:http2](https://nodejs.org
|
|
|
17
17
|
- Multipart - parse into `FormData`
|
|
18
18
|
- Content Disposition - for use inside of Multipart
|
|
19
19
|
- Conditionals - Etag / FixDate for IfMatch, IfModifiedSince etc
|
|
20
|
+
- Client Hints
|
|
21
|
+
- Forwarded
|
|
22
|
+
- Applied Preferences
|
|
20
23
|
|
|
21
24
|
### Server Sent:
|
|
22
25
|
- Rate Limit
|
|
23
26
|
- Server Timing
|
|
27
|
+
- Cache Control
|
|
28
|
+
- Clear Site Data
|
|
29
|
+
- Conditional (Etag / IMF Date)
|
|
30
|
+
- Content Range
|
|
31
|
+
- Preferences
|
|
32
|
+
- WWW Auth
|
|
33
|
+
- STS
|
|
34
|
+
|
|
24
35
|
|
|
25
36
|
|
|
26
37
|
```javascript
|
|
@@ -49,11 +60,12 @@ All responders take in a `stream` as well as a metadata object to hint on server
|
|
|
49
60
|
- [`sendContentTooLarge`](#)
|
|
50
61
|
- [`sendCreated`](#responsecreated)
|
|
51
62
|
- [`sendError`](#responseerror) - 500
|
|
52
|
-
- [`
|
|
63
|
+
- [`sendForbidden`](#responseforbidden)
|
|
64
|
+
- [`sendGone`](#responsegone)
|
|
53
65
|
- [`sendImATeapot`](#)
|
|
54
66
|
- [`sendInsufficientStorage`](#)
|
|
55
67
|
- [`sendJSON_Encoded`](#responsejson) - Standard Ok response with encoding
|
|
56
|
-
- [`sendMovedPermanently`](#)
|
|
68
|
+
- [`sendMovedPermanently`](#responsemovedpermanently)
|
|
57
69
|
- [`sendMultipleChoices`](#)
|
|
58
70
|
- [`sendNoContent`](#responsenocontent)
|
|
59
71
|
- [`sendNotAcceptable`](#responsenotacceptable)
|
|
@@ -62,9 +74,10 @@ All responders take in a `stream` as well as a metadata object to hint on server
|
|
|
62
74
|
- [`sendNotImplemented`](#responsenotimplemented)
|
|
63
75
|
- [`sendNotModified`](#responsenotmodified)
|
|
64
76
|
- [`sendPartialContent`](#)
|
|
77
|
+
- [`sendPermanentRedirect`](#)
|
|
65
78
|
- [`sendPreconditionFailed`](#responsepreconditionfailed)
|
|
66
79
|
- [`sendPreflight`](#responsepreflight) - Response to OPTIONS with CORS headers
|
|
67
|
-
- [`sendRangeNotSatisfiable`](#)
|
|
80
|
+
- [`sendRangeNotSatisfiable`](#responserangenotsatisfiable)
|
|
68
81
|
- [`sendSeeOther`](#)
|
|
69
82
|
- [`sendTemporaryRedirect`](#)
|
|
70
83
|
- [`sendTimeout`](#responsetimeout)
|
|
@@ -145,8 +158,8 @@ Parameters:
|
|
|
145
158
|
|
|
146
159
|
Parameters:
|
|
147
160
|
- stream
|
|
148
|
-
- location
|
|
149
|
-
- etag
|
|
161
|
+
- location - URL to newly create object
|
|
162
|
+
- etag - etag of the related object
|
|
150
163
|
- meta
|
|
151
164
|
|
|
152
165
|
Additional Exposed Headers:
|
|
@@ -158,6 +171,27 @@ Parameters:
|
|
|
158
171
|
- stream
|
|
159
172
|
- meta
|
|
160
173
|
|
|
174
|
+
### Response.forbidden
|
|
175
|
+
|
|
176
|
+
Parameters:
|
|
177
|
+
- stream
|
|
178
|
+
- meta
|
|
179
|
+
|
|
180
|
+
### Response.gone
|
|
181
|
+
|
|
182
|
+
Parameters:
|
|
183
|
+
- stream
|
|
184
|
+
- meta
|
|
185
|
+
|
|
186
|
+
### Response.movedPermanently
|
|
187
|
+
|
|
188
|
+
Parameters:
|
|
189
|
+
- steam
|
|
190
|
+
- location
|
|
191
|
+
- meta
|
|
192
|
+
|
|
193
|
+
Additional Exposed Headers:
|
|
194
|
+
- location
|
|
161
195
|
|
|
162
196
|
### Response.json
|
|
163
197
|
|
|
@@ -186,7 +220,7 @@ Parameters:
|
|
|
186
220
|
|
|
187
221
|
Parameters:
|
|
188
222
|
- stream
|
|
189
|
-
- supportedTypes
|
|
223
|
+
- supportedTypes - array of supported types (mime/lang/encodings etc)
|
|
190
224
|
- meta
|
|
191
225
|
|
|
192
226
|
### Response.notAllowed
|
|
@@ -237,10 +271,22 @@ Parameters:
|
|
|
237
271
|
- stream
|
|
238
272
|
- methods
|
|
239
273
|
- supportedQueryTypes - undefined | Array of supported types
|
|
274
|
+
- acceptRanges
|
|
240
275
|
- meta
|
|
241
276
|
|
|
242
277
|
Additional Exposed Headers:
|
|
243
278
|
- accept-query
|
|
279
|
+
- accept-ranges
|
|
280
|
+
|
|
281
|
+
### Response.rangeNotSatisfiable
|
|
282
|
+
|
|
283
|
+
Parameters:
|
|
284
|
+
- stream
|
|
285
|
+
- directive - specify size of acceptable range of bytes
|
|
286
|
+
- meta
|
|
287
|
+
|
|
288
|
+
Additional Exposed Headers:
|
|
289
|
+
- content-range
|
|
244
290
|
|
|
245
291
|
### Response.sse
|
|
246
292
|
|
|
@@ -282,6 +328,7 @@ Parameters:
|
|
|
282
328
|
|
|
283
329
|
Parameters:
|
|
284
330
|
- stream
|
|
331
|
+
- challenge
|
|
285
332
|
- meta
|
|
286
333
|
|
|
287
334
|
|
package/package.json
CHANGED
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@johntalton/http-util",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.0.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"imports": {
|
|
7
|
+
"#util": "./src/headers/util/index.js"
|
|
8
|
+
},
|
|
6
9
|
"exports": {
|
|
7
|
-
".": "./src/index.js",
|
|
8
|
-
"./headers": "./src/index.js",
|
|
10
|
+
".": "./src/headers/index.js",
|
|
11
|
+
"./headers": "./src/headers/index.js",
|
|
9
12
|
"./body": "./src/body.js",
|
|
10
13
|
"./response": "./src/response/index.js",
|
|
11
|
-
"./response/object": "./src/response/response.js"
|
|
14
|
+
"./response/object": "./src/response/response.js",
|
|
15
|
+
"./util": "./src/headers/util/index.js"
|
|
12
16
|
},
|
|
13
17
|
"files": [
|
|
14
18
|
"src/*.js",
|
|
15
|
-
"src/response
|
|
19
|
+
"src/response/**/*.js",
|
|
20
|
+
"src/headers/*.js",
|
|
21
|
+
"src/headers/util/*.js"
|
|
16
22
|
],
|
|
17
23
|
"repository": {
|
|
18
24
|
"url": "https://github.com/johntalton/http-util"
|
package/src/body.js
CHANGED
|
@@ -2,28 +2,28 @@ import {
|
|
|
2
2
|
CHARSET_UTF8,
|
|
3
3
|
MIME_TYPE_MULTIPART_FORM_DATA,
|
|
4
4
|
MIME_TYPE_URL_FORM_DATA
|
|
5
|
-
} from './content-type.js'
|
|
6
|
-
import { Multipart } from './multipart.js'
|
|
5
|
+
} from './headers/content-type.js'
|
|
6
|
+
import { Multipart } from './headers/multipart.js'
|
|
7
7
|
|
|
8
8
|
export const BYTE_PER_K = 1024
|
|
9
9
|
export const DEFAULT_BYTE_LIMIT = BYTE_PER_K * BYTE_PER_K //
|
|
10
10
|
|
|
11
11
|
/** @import { Readable } from 'node:stream' */
|
|
12
|
-
/** @import {
|
|
12
|
+
/** @import { ContentTypeItem } from './headers/content-type.js' */
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* @typedef {Object} BodyOptions
|
|
16
16
|
* @property {AbortSignal|undefined} [signal]
|
|
17
17
|
* @property {number|undefined} [byteLimit]
|
|
18
18
|
* @property {number|undefined} [contentLength]
|
|
19
|
-
* @property {
|
|
19
|
+
* @property {ContentTypeItem|undefined} [contentType]
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* @typedef {Object} BodyFuture
|
|
24
24
|
* @property {number} duration
|
|
25
25
|
* @property {ReadableStream} body
|
|
26
|
-
* @property {
|
|
26
|
+
* @property {ContentTypeItem|undefined} contentType
|
|
27
27
|
* @property { (mimetype: string) => Promise<Blob> } blob
|
|
28
28
|
* @property { () => Promise<ArrayBufferLike> } arrayBuffer
|
|
29
29
|
* @property { () => Promise<Uint8Array> } bytes
|
|
@@ -277,7 +277,7 @@ export async function bodyJSON(reader, charset) {
|
|
|
277
277
|
|
|
278
278
|
/**
|
|
279
279
|
* @param {ReadableStream} reader
|
|
280
|
-
* @param {
|
|
280
|
+
* @param {ContentTypeItem} contentType
|
|
281
281
|
*/
|
|
282
282
|
export async function _bodyFormData_Multipart(reader, contentType) {
|
|
283
283
|
const MULTIPART_FORM_DATA_BOUNDARY_PARAMETER = 'boundary'
|
|
@@ -291,7 +291,7 @@ export async function _bodyFormData_Multipart(reader, contentType) {
|
|
|
291
291
|
|
|
292
292
|
/**
|
|
293
293
|
* @param {ReadableStream} reader
|
|
294
|
-
* @param {
|
|
294
|
+
* @param {ContentTypeItem} contentType
|
|
295
295
|
*/
|
|
296
296
|
export async function _bodyFormData_URL(reader, contentType) {
|
|
297
297
|
const text = await bodyText(reader, contentType.charset)
|
|
@@ -307,7 +307,7 @@ export async function _bodyFormData_URL(reader, contentType) {
|
|
|
307
307
|
|
|
308
308
|
/**
|
|
309
309
|
* @param {ReadableStream} reader
|
|
310
|
-
* @param {
|
|
310
|
+
* @param {ContentTypeItem|undefined} contentType
|
|
311
311
|
*/
|
|
312
312
|
export async function bodyFormData(reader, contentType) {
|
|
313
313
|
if(contentType === undefined) { throw new Error('undefined content type for form data') }
|
|
@@ -26,7 +26,7 @@ export const RANGE_UNITS_BYTES = 'bytes'
|
|
|
26
26
|
/** @type {'none'} */
|
|
27
27
|
export const RANGE_UNITS_NONE = 'none'
|
|
28
28
|
|
|
29
|
-
/** @import { TimingsInfo } from '
|
|
29
|
+
/** @import { TimingsInfo } from './headers/server-timing.js' */
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
32
|
* @typedef {`X-${string}`} CustomHeaderKey
|
|
@@ -46,3 +46,4 @@ export const RANGE_UNITS_NONE = 'none'
|
|
|
46
46
|
* @property {boolean} [bom]
|
|
47
47
|
*/
|
|
48
48
|
|
|
49
|
+
/** @typedef {ArrayBufferLike|ArrayBufferView|ReadableStream|string} SendBody */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { parseAcceptStyleHeader } from './accept-util.js'
|
|
1
|
+
import { parseAcceptStyleHeader } from './util/accept-util.js'
|
|
2
2
|
|
|
3
|
-
/** @import { AcceptStyleItem } from './accept-util.js' */
|
|
3
|
+
/** @import { AcceptStyleItem } from './util/accept-util.js' */
|
|
4
4
|
|
|
5
5
|
export const WELL_KNOWN_ENCODINGS = new Map([
|
|
6
6
|
[ 'gzip, deflate, br, zstd', [ { name: 'gzip' }, { name: 'deflate' }, { name: 'br' }, { name: 'zstd' } ] ],
|
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
import { parseAcceptStyleHeader } from './accept-util.js'
|
|
1
|
+
import { parseAcceptStyleHeader } from './util/accept-util.js'
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* @import { AcceptStyleItem } from './accept-util.js'
|
|
5
|
-
*/
|
|
3
|
+
/** @import { AcceptStyleItem } from './util/accept-util.js' */
|
|
6
4
|
|
|
7
5
|
export const WELL_KNOWN_LANGUAGES = new Map([
|
|
8
6
|
[ 'en-US,en;q=0.5', [ { name: 'en-US', quality: 1 }, { name: 'en', quality: 0.5 } ] ],
|
|
9
|
-
[ 'en-US,en;q=0.9', [ { name: 'en-US', quality: 1 }, { name: 'en', quality: 0.9 } ] ]
|
|
7
|
+
[ 'en-US,en;q=0.9', [ { name: 'en-US', quality: 1 }, { name: 'en', quality: 0.9 } ] ],
|
|
8
|
+
// [ '*', [ { name: '*', quality: 1 } ]]
|
|
10
9
|
])
|
|
11
10
|
|
|
12
11
|
export class AcceptLanguage {
|
|
@@ -1,22 +1,10 @@
|
|
|
1
|
-
import { parseAcceptStyleHeader } from './accept-util.js'
|
|
1
|
+
import { parseAcceptStyleHeader } from './util/accept-util.js'
|
|
2
|
+
import { MIME_ANY, Mime } from './util/mime.js'
|
|
2
3
|
|
|
3
|
-
/**
|
|
4
|
-
|
|
5
|
-
*/
|
|
4
|
+
/** @import { AcceptStyleItem } from './util/accept-util.js' */
|
|
5
|
+
/** @import { MimeItem } from './util/mime.js' */
|
|
6
6
|
|
|
7
|
-
/**
|
|
8
|
-
* @typedef {Object} AcceptExtensionItem
|
|
9
|
-
* @property {string} mimetype
|
|
10
|
-
* @property {string} type
|
|
11
|
-
* @property {string} subtype
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* @typedef {AcceptStyleItem & AcceptExtensionItem} AcceptItem
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
export const ACCEPT_SEPARATOR = { SUBTYPE: '/' }
|
|
19
|
-
export const ACCEPT_ANY = '*'
|
|
7
|
+
/** @typedef {AcceptStyleItem & MimeItem} AcceptItem */
|
|
20
8
|
|
|
21
9
|
export const WELL_KNOWN = new Map([
|
|
22
10
|
[ '*/*', [ { name: '*/*', quality: 1 } ] ],
|
|
@@ -31,22 +19,23 @@ export class Accept {
|
|
|
31
19
|
static parse(acceptHeader) {
|
|
32
20
|
return parseAcceptStyleHeader(acceptHeader, WELL_KNOWN)
|
|
33
21
|
.map(({ name, quality, parameters }) => {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
22
|
+
|
|
23
|
+
const mime = Mime.parse(name)
|
|
24
|
+
if(mime === undefined) { return undefined }
|
|
37
25
|
|
|
38
26
|
return {
|
|
39
|
-
|
|
40
|
-
|
|
27
|
+
name,
|
|
28
|
+
...mime,
|
|
41
29
|
quality,
|
|
42
30
|
parameters
|
|
43
31
|
}
|
|
44
32
|
})
|
|
33
|
+
.filter(entry => entry !== undefined)
|
|
45
34
|
.sort((entryA, entryB) => {
|
|
46
35
|
if(entryA.quality === entryB.quality) {
|
|
47
36
|
// prefer things with less ANY
|
|
48
|
-
const specificityA = (entryA.type ===
|
|
49
|
-
const specificityB = (entryB.type ===
|
|
37
|
+
const specificityA = (entryA.type === MIME_ANY ? 1 : 0) + (entryA.subtype === MIME_ANY ? 1 : 0)
|
|
38
|
+
const specificityB = (entryB.type === MIME_ANY ? 1 : 0) + (entryB.subtype === MIME_ANY ? 1 : 0)
|
|
50
39
|
return specificityA - specificityB
|
|
51
40
|
}
|
|
52
41
|
|
|
@@ -74,8 +63,9 @@ export class Accept {
|
|
|
74
63
|
const bests = accepts.map(accept => {
|
|
75
64
|
const { type, subtype, quality } = accept
|
|
76
65
|
const st = supportedTypes.filter(supportedType => {
|
|
77
|
-
const
|
|
78
|
-
|
|
66
|
+
const supportedMime = Mime.parse(supportedType)
|
|
67
|
+
if(supportedMime === undefined) { return false }
|
|
68
|
+
return ((supportedMime.type === type || type === MIME_ANY) && (supportedMime.subtype === subtype || subtype === MIME_ANY))
|
|
79
69
|
})
|
|
80
70
|
|
|
81
71
|
return {
|
|
@@ -95,6 +85,8 @@ export class Accept {
|
|
|
95
85
|
|
|
96
86
|
// console.log(Accept.parse('text/html, application/xhtml+xml, application/xml;q=0.9, image/webp, text/*;q=.8, */*;q=0.7'))
|
|
97
87
|
// console.log(Accept.select('text/html, application/xhtml+xml, application/xml;q=0.9, image/webp, text/*;q=.8, */*;q=0.7', [ 'application/json', 'text/plain' ]))
|
|
88
|
+
// console.log(Accept.parse('*'))
|
|
89
|
+
|
|
98
90
|
|
|
99
91
|
// const tests = [
|
|
100
92
|
// undefined,
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
export const CLIENT_HINT_USER_AGENT = 'Sec-CH-UA'
|
|
4
|
+
export const CLIENT_HINT_ARCHITECTURE = 'Sec-CH-UA-Arch'
|
|
5
|
+
export const CLIENT_HINT_BITNESS = 'Sec-CH-UA-Bitness'
|
|
6
|
+
export const CLIENT_HINT_FORM_FACTORS = 'Sec-CH-UA-Form-Factors'
|
|
7
|
+
// export const CLIENT_HINT_ = 'Sec-CH-UA-Full-Version'
|
|
8
|
+
export const CLIENT_HINT_VERSION_LIST = 'Sec-CH-UA-Full-Version-List'
|
|
9
|
+
export const CLIENT_HINT_MOBILE = 'Sec-CH-UA-Mobile'
|
|
10
|
+
export const CLIENT_HINT_MODEL = 'Sec-CH-UA-Model'
|
|
11
|
+
export const CLIENT_HINT_PLATFORM = 'Sec-CH-UA-Platform'
|
|
12
|
+
export const CLIENT_HINT_PLATFORM_VERSION = 'Sec-CH-UA-Platform-Version'
|
|
13
|
+
export const CLIENT_HINT_WOW64 = 'Sec-CH-UA-WoW64'
|
|
14
|
+
export const CLIENT_HINT_PREFERS_COLOR_SCHEME = 'Sec-CH-Prefers-Color-Scheme'
|
|
15
|
+
export const CLIENT_HINT_PREFERS_REDUCED_MOTION = 'Sec-CH-Prefers-Reduced-Motion'
|
|
16
|
+
export const CLIENT_HINT_PREFERS_REDUCED_TRANSPARENCY = 'Sec-CH-Prefers-Reduced-Transparency'
|
|
17
|
+
export const CLIENT_HINT_DEVICE_MEMORY = 'Sec-CH-Device-Memory'
|
|
18
|
+
export const CLIENT_HINT_DEVICE_PIXEL_RATIO = 'Sec-CH-DPR'
|
|
19
|
+
export const CLIENT_HINT_VIEWPORT_HEIGHT = 'Sec-CH-Viewport-Height'
|
|
20
|
+
export const CLIENT_HINT_VIEWPORT_WIDTH = 'Sec-CH-Viewport-Width'
|
|
21
|
+
export const CLIENT_HINT_IMAGE_WIDTH = 'Sec-CH-Width'
|
|
22
|
+
export const CLIENT_HINT_DOWNLINK = 'Downlink'
|
|
23
|
+
export const CLIENT_HINT_EFFECTIVE_CONNECTION_TYPE = 'ECT'
|
|
24
|
+
export const CLIENT_HINT_ROUND_TRIP_TIME = 'RTT'
|
|
25
|
+
export const CLIENT_HINT_SAVE_DATA = 'Save-Data'
|
|
26
|
+
export const CLIENT_HINT_GLOBAL_PRIVACY_CONTROL = 'Sec-GPC'
|
|
27
|
+
|
|
28
|
+
export const KNOWN_CLIENT_HINTS = [
|
|
29
|
+
CLIENT_HINT_USER_AGENT,
|
|
30
|
+
CLIENT_HINT_ARCHITECTURE,
|
|
31
|
+
CLIENT_HINT_BITNESS,
|
|
32
|
+
CLIENT_HINT_FORM_FACTORS,
|
|
33
|
+
CLIENT_HINT_VERSION_LIST,
|
|
34
|
+
CLIENT_HINT_MOBILE,
|
|
35
|
+
CLIENT_HINT_MODEL,
|
|
36
|
+
CLIENT_HINT_PLATFORM,
|
|
37
|
+
CLIENT_HINT_PLATFORM_VERSION,
|
|
38
|
+
CLIENT_HINT_WOW64,
|
|
39
|
+
CLIENT_HINT_PREFERS_COLOR_SCHEME,
|
|
40
|
+
CLIENT_HINT_PREFERS_REDUCED_MOTION,
|
|
41
|
+
CLIENT_HINT_PREFERS_REDUCED_TRANSPARENCY,
|
|
42
|
+
CLIENT_HINT_DEVICE_MEMORY,
|
|
43
|
+
CLIENT_HINT_DEVICE_PIXEL_RATIO,
|
|
44
|
+
CLIENT_HINT_VIEWPORT_HEIGHT,
|
|
45
|
+
CLIENT_HINT_VIEWPORT_WIDTH,
|
|
46
|
+
CLIENT_HINT_IMAGE_WIDTH,
|
|
47
|
+
CLIENT_HINT_DOWNLINK,
|
|
48
|
+
CLIENT_HINT_EFFECTIVE_CONNECTION_TYPE,
|
|
49
|
+
CLIENT_HINT_ROUND_TRIP_TIME,
|
|
50
|
+
CLIENT_HINT_SAVE_DATA,
|
|
51
|
+
CLIENT_HINT_GLOBAL_PRIVACY_CONTROL
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
export const KNOWN_FORM_FACTORS = [
|
|
55
|
+
'Desktop',
|
|
56
|
+
'Automotive',
|
|
57
|
+
'Mobile',
|
|
58
|
+
'Tablet',
|
|
59
|
+
'XR',
|
|
60
|
+
'EInk',
|
|
61
|
+
'Watch'
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
export const CLIENT_HINT_TRUE = '?1'
|
|
65
|
+
export const CLIENT_HINT_FALSE = '?0'
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
export class ClientHints {
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @param {Array<String>} hints
|
|
72
|
+
*/
|
|
73
|
+
static encode(hints) {
|
|
74
|
+
return hints
|
|
75
|
+
.filter(hint => KNOWN_CLIENT_HINTS.includes(hint))
|
|
76
|
+
.join(', ')
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isQuoted, stripQuotes } from './quote.js'
|
|
1
|
+
import { isQuoted, stripQuotes } from './util/quote.js'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* @typedef {Object} WeakEtagItem
|
|
@@ -54,25 +54,34 @@ export const MINIMUM_YEAR = 1900
|
|
|
54
54
|
export const MAXIMUM_DAY = 31
|
|
55
55
|
|
|
56
56
|
/**
|
|
57
|
+
* @deprecated
|
|
58
|
+
* @see {@link ETag.isValid}
|
|
57
59
|
* @param {string} etag
|
|
58
60
|
*/
|
|
59
61
|
export function isValidEtag(etag) {
|
|
60
|
-
|
|
61
|
-
for(const c of etag) {
|
|
62
|
-
if(c.charCodeAt(0) < 0x21) { return false }
|
|
63
|
-
if(c.charCodeAt(0) > 0xFF) { return false }
|
|
64
|
-
if(c === ETAG_QUOTE) { return false }
|
|
65
|
-
}
|
|
66
|
-
return true
|
|
62
|
+
return ETag.isValid(etag)
|
|
67
63
|
}
|
|
68
64
|
|
|
69
65
|
export class ETag {
|
|
66
|
+
/**
|
|
67
|
+
* @param {string} etag
|
|
68
|
+
*/
|
|
69
|
+
static isValid(etag) {
|
|
70
|
+
// %x21 / %x23-7E and %x80-FF
|
|
71
|
+
for(const c of etag) {
|
|
72
|
+
if(c.charCodeAt(0) < 0x21) { return false }
|
|
73
|
+
if(c.charCodeAt(0) > 0xFF) { return false }
|
|
74
|
+
if(c === ETAG_QUOTE) { return false }
|
|
75
|
+
}
|
|
76
|
+
return true
|
|
77
|
+
}
|
|
78
|
+
|
|
70
79
|
/**
|
|
71
80
|
* @param {string} etag
|
|
72
81
|
* @returns {WeakEtagItem}
|
|
73
82
|
*/
|
|
74
83
|
static weak(etag) {
|
|
75
|
-
if(!
|
|
84
|
+
if(!ETag.isValid(etag)) { throw new Error('invalid etag format') }
|
|
76
85
|
return { any: false, weak: true, etag }
|
|
77
86
|
}
|
|
78
87
|
|
|
@@ -81,7 +90,7 @@ export class ETag {
|
|
|
81
90
|
* @returns {NotWeakEtagItem}
|
|
82
91
|
*/
|
|
83
92
|
static strong(etag) {
|
|
84
|
-
if(!
|
|
93
|
+
if(!ETag.isValid(etag)) { throw new Error('invalid etag format') }
|
|
85
94
|
return { any: false, weak: false, etag }
|
|
86
95
|
}
|
|
87
96
|
|
|
@@ -106,7 +115,7 @@ export class ETag {
|
|
|
106
115
|
if(!isQuoted(quotedEtag)) { return undefined }
|
|
107
116
|
const etag = stripQuotes(quotedEtag)
|
|
108
117
|
if(etag === undefined) { return undefined }
|
|
109
|
-
if(!
|
|
118
|
+
if(!ETag.isValid(etag)) { return undefined }
|
|
110
119
|
if(etag === CONDITION_ETAG_ANY) { return undefined }
|
|
111
120
|
|
|
112
121
|
return {
|
|
@@ -130,7 +139,7 @@ export class Conditional {
|
|
|
130
139
|
}
|
|
131
140
|
|
|
132
141
|
if(etagItem.etag === CONDITION_ETAG_ANY) { return undefined }
|
|
133
|
-
if(!
|
|
142
|
+
if(!ETag.isValid(etagItem.etag)) { return undefined }
|
|
134
143
|
|
|
135
144
|
const prefix = etagItem.weak ? CONDITION_ETAG_WEAK_PREFIX : ''
|
|
136
145
|
return `${prefix}${ETAG_QUOTE}${etagItem.etag}${ETAG_QUOTE}`
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { KVP } from './util/kvp.js'
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* @typedef {Object} Disposition
|
|
3
5
|
* @property {string} disposition
|
|
@@ -6,39 +8,38 @@
|
|
|
6
8
|
* @property {string|undefined} [filename]
|
|
7
9
|
*/
|
|
8
10
|
|
|
9
|
-
export const DISPOSITION_SEPARATOR = {
|
|
10
|
-
PARAMETER: ';',
|
|
11
|
-
KVP: '='
|
|
12
|
-
}
|
|
13
|
-
|
|
14
11
|
export const DISPOSITION_PARAM_NAME = 'name'
|
|
15
12
|
export const DISPOSITION_PARAM_FILENAME = 'filename'
|
|
16
13
|
|
|
17
14
|
/**
|
|
15
|
+
* @deprecated
|
|
16
|
+
* @see {@link ContentDisposition.parse}
|
|
18
17
|
* @param {string|undefined} contentDispositionHeader
|
|
19
18
|
* @returns {Disposition|undefined}
|
|
20
19
|
*/
|
|
21
20
|
export function parseContentDisposition(contentDispositionHeader) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
21
|
+
return ContentDisposition.parse(contentDispositionHeader)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class ContentDisposition {
|
|
25
|
+
/**
|
|
26
|
+
* @param {string|undefined} header
|
|
27
|
+
* @returns {Disposition|undefined}
|
|
28
|
+
*/
|
|
29
|
+
static parse(header) {
|
|
30
|
+
if(header === undefined) { return undefined }
|
|
31
|
+
|
|
32
|
+
const { name: disposition, parameters } = KVP.parse(header) ?? { parameters: new Map() }
|
|
33
|
+
if(disposition === undefined) { return undefined }
|
|
34
|
+
|
|
35
|
+
const name = parameters.get(DISPOSITION_PARAM_NAME)
|
|
36
|
+
const filename = parameters.get(DISPOSITION_PARAM_FILENAME)
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
disposition,
|
|
40
|
+
parameters,
|
|
41
|
+
name, filename
|
|
42
|
+
}
|
|
42
43
|
}
|
|
43
44
|
}
|
|
44
45
|
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { KVP } from './util/kvp.js'
|
|
2
|
+
import { MIME_ANY, Mime } from './util/mime.js'
|
|
3
|
+
|
|
4
|
+
/** @import { MimeItem } from './util/mime.js' */
|
|
5
|
+
|
|
6
|
+
export const MIME_TYPE_JSON = 'application/json'
|
|
7
|
+
export const MIME_TYPE_TEXT = 'text/plain'
|
|
8
|
+
export const MIME_TYPE_EVENT_STREAM = 'text/event-stream'
|
|
9
|
+
export const MIME_TYPE_XML = 'application/xml'
|
|
10
|
+
export const MIME_TYPE_URL_FORM_DATA = 'application/x-www-form-urlencoded'
|
|
11
|
+
export const MIME_TYPE_MULTIPART_FORM_DATA = 'multipart/form-data'
|
|
12
|
+
export const MIME_TYPE_MULTIPART_RANGE = 'multipart/byteranges'
|
|
13
|
+
export const MIME_TYPE_OCTET_STREAM = 'application/octet-stream'
|
|
14
|
+
export const MIME_TYPE_MESSAGE_HTTP = 'message/http'
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
export const KNOWN_CONTENT_TYPES = [
|
|
18
|
+
'application', 'audio', 'image', 'message',
|
|
19
|
+
'multipart','text', 'video'
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
export const TYPE_X_TOKEN_PREFIX = 'X-'
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @typedef {Object} ContentTypeExtension
|
|
26
|
+
* @property {string} name
|
|
27
|
+
* @property {string} [charset]
|
|
28
|
+
* @property {Map<string, string>} parameters
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/** @typedef {MimeItem & ContentTypeExtension} ContentTypeItem */
|
|
32
|
+
|
|
33
|
+
export const CONTENT_TYPE_SEPARATOR = {
|
|
34
|
+
SUBTYPE: '/',
|
|
35
|
+
PARAMETER: ';',
|
|
36
|
+
KVP: '='
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const CHARSET_UTF8 = 'utf8'
|
|
40
|
+
export const CHARSET = 'charset'
|
|
41
|
+
export const PARAMETER_CHARSET_UTF8 = `${CHARSET}${CONTENT_TYPE_SEPARATOR.KVP}${CHARSET_UTF8}`
|
|
42
|
+
export const CONTENT_TYPE_JSON = `${MIME_TYPE_JSON}${CONTENT_TYPE_SEPARATOR.PARAMETER}${PARAMETER_CHARSET_UTF8}`
|
|
43
|
+
export const CONTENT_TYPE_TEXT = `${MIME_TYPE_TEXT}${CONTENT_TYPE_SEPARATOR.PARAMETER}${PARAMETER_CHARSET_UTF8}`
|
|
44
|
+
export const CONTENT_TYPE_MESSAGE_HTTP = `${MIME_TYPE_MESSAGE_HTTP}${CONTENT_TYPE_SEPARATOR.PARAMETER}${PARAMETER_CHARSET_UTF8}`
|
|
45
|
+
|
|
46
|
+
/** @type {ContentTypeItem} */
|
|
47
|
+
export const WELL_KNOWN_JSON = {
|
|
48
|
+
mimetype: MIME_TYPE_JSON,
|
|
49
|
+
name: MIME_TYPE_JSON,
|
|
50
|
+
type: 'application',
|
|
51
|
+
subtype: 'json',
|
|
52
|
+
charset: 'utf8',
|
|
53
|
+
parameters: new Map()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const WELL_KNOWN_CONTENT_TYPES = new Map([
|
|
57
|
+
[ 'application/json', WELL_KNOWN_JSON ],
|
|
58
|
+
[ 'application/json;charset=utf8', WELL_KNOWN_JSON ]
|
|
59
|
+
])
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @deprecated
|
|
63
|
+
* @see {@link ContentType.parse}
|
|
64
|
+
* @param {string|undefined} contentTypeHeader
|
|
65
|
+
* @returns {ContentTypeItem|undefined}
|
|
66
|
+
*/
|
|
67
|
+
export function parseContentType(contentTypeHeader) {
|
|
68
|
+
return ContentType.parse(contentTypeHeader)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export class ContentType {
|
|
72
|
+
/**
|
|
73
|
+
* @param {string|undefined} header
|
|
74
|
+
* @returns {ContentTypeItem|undefined}
|
|
75
|
+
*/
|
|
76
|
+
static parse(header) {
|
|
77
|
+
if(header === undefined) { return undefined }
|
|
78
|
+
if(header === null) { return undefined }
|
|
79
|
+
|
|
80
|
+
const wellKnown = WELL_KNOWN_CONTENT_TYPES.get(header)
|
|
81
|
+
if(wellKnown !== undefined) { return wellKnown }
|
|
82
|
+
|
|
83
|
+
const { name, parameters } = KVP.parse(header) ?? { parameters: new Map() }
|
|
84
|
+
if(name === undefined) { return undefined }
|
|
85
|
+
|
|
86
|
+
const mimetype = Mime.parse(name)
|
|
87
|
+
if(mimetype === undefined) { return undefined }
|
|
88
|
+
|
|
89
|
+
if(mimetype.type === MIME_ANY) { return undefined }
|
|
90
|
+
if(mimetype.subtype === MIME_ANY) { return undefined }
|
|
91
|
+
|
|
92
|
+
const charset = parameters?.get(CHARSET)
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
...mimetype,
|
|
96
|
+
name,
|
|
97
|
+
charset,
|
|
98
|
+
parameters
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|