@johntalton/http-util 5.1.6 → 6.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/README.md +54 -7
- package/package.json +26 -6
- package/src/body.js +8 -8
- package/src/{response/defs.js → defs.js} +35 -1
- package/src/{accept-encoding.js → headers/accept-encoding.js} +11 -14
- package/src/{accept-language.js → headers/accept-language.js} +14 -9
- package/src/headers/accept.js +86 -0
- package/src/{cache-control.js → headers/cache-control.js} +0 -6
- package/src/{clear-site-data.js → headers/clear-site-data.js} +4 -10
- package/src/headers/client-hints.js +88 -0
- package/src/{conditional.js → headers/conditional.js} +190 -117
- package/src/headers/content-disposition.js +44 -0
- package/src/{content-range.js → headers/content-range.js} +1 -18
- package/src/headers/content-type.js +101 -0
- package/src/{forwarded.js → headers/forwarded.js} +8 -56
- package/src/{index.js → headers/index.js} +4 -2
- package/src/headers/link.js +34 -0
- package/src/{multipart.js → headers/multipart.js} +22 -13
- package/src/{preference.js → headers/preference.js} +3 -58
- package/src/{range.js → headers/range.js} +4 -32
- package/src/{rate-limit.js → headers/rate-limit.js} +6 -1
- package/src/{server-timing.js → headers/server-timing.js} +3 -16
- package/src/headers/strict-transport-security.js +39 -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 +8 -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/2xx/bytes.js +62 -0
- package/src/response/2xx/created.js +49 -0
- package/src/response/2xx/json.js +60 -0
- package/src/response/2xx/no-content.js +45 -0
- package/src/response/2xx/partial-content.js +101 -0
- package/src/response/{preflight.js → 2xx/preflight.js} +29 -10
- package/src/response/{sse.js → 2xx/sse.js} +2 -2
- package/src/response/{trace.js → 2xx/trace.js} +3 -3
- package/src/response/3xx/found.js +23 -0
- 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/3xx/not-modified.js +59 -0
- 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/4xx/bad-request.js +19 -0
- 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} +14 -3
- package/src/response/4xx/not-allowed.js +34 -0
- package/src/response/{not-found.js → 4xx/not-found.js} +3 -3
- package/src/response/4xx/payment-required.js +17 -0
- package/src/response/4xx/precondition-failed.js +45 -0
- package/src/response/{range-not-satisfiable.js → 4xx/range-not-satisfiable.js} +15 -4
- package/src/response/{timeout.js → 4xx/timeout.js} +2 -2
- package/src/response/{too-many-requests.js → 4xx/too-many-requests.js} +22 -5
- package/src/response/{unauthorized.js → 4xx/unauthorized.js} +5 -5
- package/src/response/{unprocessable.js → 4xx/unprocessable.js} +2 -2
- package/src/response/{unsupported-media.js → 4xx/unsupported-media.js} +21 -4
- 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} +16 -4
- package/src/response/header-util.js +2 -2
- package/src/response/index.js +39 -35
- package/src/response/response.js +40 -34
- package/src/response/send-util.js +32 -21
- package/src/accept.js +0 -122
- package/src/content-disposition.js +0 -57
- package/src/content-type.js +0 -148
- package/src/link.js +0 -35
- package/src/response/bytes.js +0 -27
- package/src/response/created.js +0 -28
- package/src/response/json.js +0 -28
- package/src/response/no-content.js +0 -25
- package/src/response/not-allowed.js +0 -23
- package/src/response/not-modified.js +0 -35
- package/src/response/partial-content.js +0 -71
- package/src/response/precondition-failed.js +0 -16
- /package/src/{fetch-metadata.js → headers/fetch-metadata.js} +0 -0
- /package/src/{quote.js → headers/util/quote.js} +0 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {Object} LinkItem
|
|
3
|
+
* @property {URL|string} url
|
|
4
|
+
* @property {string|undefined} [relation]
|
|
5
|
+
* @property {Map<string, string>|undefined} [parameters]
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export class Link {
|
|
9
|
+
/**
|
|
10
|
+
* @param {LinkItem} link
|
|
11
|
+
*/
|
|
12
|
+
static *#encode(link) {
|
|
13
|
+
const encodedUri = (link.url instanceof URL) ? link.url : encodeURI(link.url)
|
|
14
|
+
|
|
15
|
+
yield `<${encodedUri}>`
|
|
16
|
+
if(link.relation !== undefined) { yield `rel="${link.relation}"` }
|
|
17
|
+
if(link.parameters === undefined) { return }
|
|
18
|
+
for(const [ key, value ] of link.parameters) {
|
|
19
|
+
yield `${key}="${value}"`
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {Array<LinkItem>|LinkItem|undefined} links
|
|
25
|
+
*/
|
|
26
|
+
static encode(links) {
|
|
27
|
+
if(links === undefined) { return undefined }
|
|
28
|
+
const linkAry = Array.isArray(links) ? links : [ links ]
|
|
29
|
+
if(linkAry.length === 0) { return undefined }
|
|
30
|
+
return linkAry
|
|
31
|
+
.map(link => [ ...Link.#encode(link) ].join('; '))
|
|
32
|
+
.join(', ')
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { ReadableStream } from 'node:stream/web'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { isQuoted, stripQuotes } from '../headers/util/quote.js'
|
|
4
|
+
import { ContentDisposition } from './content-disposition.js'
|
|
4
5
|
import { ContentRange } from './content-range.js'
|
|
5
|
-
import {
|
|
6
|
+
import { ContentType } from './content-type.js'
|
|
6
7
|
|
|
7
8
|
/** @import { ContentRangeDirective } from './content-range.js' */
|
|
8
|
-
/** @import { SendBody } from '
|
|
9
|
+
/** @import { SendBody } from '../defs.js' */
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* @typedef {Object} MultipartBytePart
|
|
@@ -49,11 +50,14 @@ export class Multipart {
|
|
|
49
50
|
* @param {string} text
|
|
50
51
|
* @param {string} boundary
|
|
51
52
|
* @param {string} [_charset='utf8']
|
|
53
|
+
* @returns {FormData}
|
|
52
54
|
*/
|
|
53
55
|
static parse_FormData(text, boundary, _charset = 'utf8') {
|
|
54
|
-
// console.log({ boundary, text })
|
|
55
56
|
const formData = new FormData()
|
|
56
57
|
|
|
58
|
+
if(text === undefined) { return formData }
|
|
59
|
+
if(boundary === undefined) { return formData }
|
|
60
|
+
|
|
57
61
|
if(text === '') {
|
|
58
62
|
// empty body
|
|
59
63
|
return formData
|
|
@@ -94,17 +98,16 @@ export class Multipart {
|
|
|
94
98
|
const name = rawName?.toLowerCase()
|
|
95
99
|
// console.log('header', name, value)
|
|
96
100
|
if(name === MULTIPART_HEADER.CONTENT_TYPE) {
|
|
97
|
-
const
|
|
98
|
-
//
|
|
101
|
+
const contentType = ContentType.parse(value)
|
|
102
|
+
//console.log({ contentType })
|
|
99
103
|
}
|
|
100
104
|
else if(name === MULTIPART_HEADER.CONTENT_DISPOSITION) {
|
|
101
|
-
const disposition =
|
|
105
|
+
const disposition = ContentDisposition.parse(value)
|
|
102
106
|
if(disposition?.disposition !== DISPOSITION_FORM_DATA) {
|
|
103
107
|
throw new Error('disposition not form-data')
|
|
104
108
|
}
|
|
105
109
|
|
|
106
|
-
|
|
107
|
-
partName = disposition.name?.slice(1, -1)
|
|
110
|
+
partName = isQuoted(disposition.name) ? stripQuotes(disposition.name) : disposition.name
|
|
108
111
|
}
|
|
109
112
|
else {
|
|
110
113
|
// unsupported part header - ignore
|
|
@@ -150,6 +153,7 @@ export class Multipart {
|
|
|
150
153
|
|
|
151
154
|
return new ReadableStream({
|
|
152
155
|
type: 'bytes',
|
|
156
|
+
// async pull(controller) {},
|
|
153
157
|
async start(controller) {
|
|
154
158
|
const encoder = new TextEncoder()
|
|
155
159
|
|
|
@@ -158,9 +162,9 @@ export class Multipart {
|
|
|
158
162
|
controller.enqueue(encoder.encode(`${MULTIPART_HEADER.CONTENT_TYPE}: ${contentType}${MULTIPART_SEPARATOR}`))
|
|
159
163
|
controller.enqueue(encoder.encode(`${MULTIPART_HEADER.CONTENT_RANGE}: ${ContentRange.encode({ ...part.range, size: contentLength })}${MULTIPART_SEPARATOR}`))
|
|
160
164
|
controller.enqueue(encoder.encode(MULTIPART_SEPARATOR))
|
|
161
|
-
// controller.enqueue(encoder.encode(MULTIPART_SEPARATOR))
|
|
162
165
|
|
|
163
166
|
if(part.obj instanceof ReadableStream) {
|
|
167
|
+
// biome-ignore lint/performance/noAwaitInLoops: readable
|
|
164
168
|
for await (const chunk of part.obj) {
|
|
165
169
|
if(chunk instanceof ArrayBuffer || ArrayBuffer.isView(chunk)) {
|
|
166
170
|
controller.enqueue(chunk)
|
|
@@ -174,7 +178,10 @@ export class Multipart {
|
|
|
174
178
|
}
|
|
175
179
|
}
|
|
176
180
|
}
|
|
177
|
-
else if(part.obj instanceof ArrayBuffer
|
|
181
|
+
else if(part.obj instanceof ArrayBuffer) {
|
|
182
|
+
controller.enqueue(new Uint8Array(part.obj))
|
|
183
|
+
}
|
|
184
|
+
else if(ArrayBuffer.isView(part.obj)) {
|
|
178
185
|
controller.enqueue(part.obj)
|
|
179
186
|
}
|
|
180
187
|
else if(typeof part.obj === 'string'){
|
|
@@ -182,7 +189,7 @@ export class Multipart {
|
|
|
182
189
|
}
|
|
183
190
|
else {
|
|
184
191
|
// console.log('error', typeof part.obj, part.obj)
|
|
185
|
-
|
|
192
|
+
controller.error(new Error('unknown part type'))
|
|
186
193
|
}
|
|
187
194
|
|
|
188
195
|
controller.enqueue(encoder.encode(MULTIPART_SEPARATOR))
|
|
@@ -190,7 +197,9 @@ export class Multipart {
|
|
|
190
197
|
|
|
191
198
|
controller.enqueue(encoder.encode(boundaryEnd))
|
|
192
199
|
|
|
193
|
-
|
|
200
|
+
// controller.enqueue(encoder.encode(MULTIPART_SEPARATOR))
|
|
201
|
+
|
|
202
|
+
controller.close()
|
|
194
203
|
}
|
|
195
204
|
})
|
|
196
205
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// https://datatracker.ietf.org/doc/html/rfc7240
|
|
2
2
|
// https://www.rfc-editor.org/rfc/rfc7240#section-3
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { KVP } from './util/kvp.js'
|
|
5
|
+
import { isQuoted, stripQuotes } from './util/quote.js'
|
|
5
6
|
|
|
6
7
|
export const PREFERENCE_SEPARATOR = {
|
|
7
8
|
PREFERENCE: ',',
|
|
@@ -53,26 +54,13 @@ export class Preferences {
|
|
|
53
54
|
|
|
54
55
|
const preferences = new Map(header.split(PREFERENCE_SEPARATOR.PREFERENCE)
|
|
55
56
|
.map(pref => {
|
|
56
|
-
const
|
|
57
|
+
const { name: kvp, parameters } = KVP.parse(pref) ?? { parameters: new Map() }
|
|
57
58
|
const [ key, rawValue ] = kvp?.split(PREFERENCE_SEPARATOR.KVP) ?? []
|
|
58
59
|
|
|
59
60
|
if(key === undefined) { return {} }
|
|
60
61
|
const valueOrEmpty = isQuoted(rawValue) ? stripQuotes(rawValue) : rawValue
|
|
61
62
|
const value = (valueOrEmpty !== '') ? valueOrEmpty : undefined
|
|
62
63
|
|
|
63
|
-
const parameters = new Map(params
|
|
64
|
-
.map(param => {
|
|
65
|
-
const [ pKey, rawPValue ] = param.split(PREFERENCE_SEPARATOR.PARAM_KVP)
|
|
66
|
-
if(pKey === undefined) { return {} }
|
|
67
|
-
const trimmedRawPValue = rawPValue?.trim()
|
|
68
|
-
const pValueOrEmpty = isQuoted(trimmedRawPValue) ? stripQuotes(trimmedRawPValue) : trimmedRawPValue
|
|
69
|
-
const pValue = (pValueOrEmpty !== '') ? pValueOrEmpty : undefined
|
|
70
|
-
return { key: pKey.trim(), value: pValue }
|
|
71
|
-
})
|
|
72
|
-
.filter(item => item.key !== undefined)
|
|
73
|
-
.map(item => ([ item.key, item.value ]))
|
|
74
|
-
)
|
|
75
|
-
|
|
76
64
|
return { key, value, parameters }
|
|
77
65
|
})
|
|
78
66
|
.filter(item => item.key !== undefined)
|
|
@@ -140,46 +128,3 @@ export class AppliedPreferences {
|
|
|
140
128
|
return AppliedPreferences.#encode_Map(applied)
|
|
141
129
|
}
|
|
142
130
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
// console.log(AppliedPreferences.encode(undefined))
|
|
146
|
-
// console.log(AppliedPreferences.encode({ }))
|
|
147
|
-
// console.log(AppliedPreferences.encode({ wait: 10 }))
|
|
148
|
-
// console.log(AppliedPreferences.encode({ asynchronous: undefined }))
|
|
149
|
-
// console.log(AppliedPreferences.encode({ asynchronous: false }))
|
|
150
|
-
// console.log(AppliedPreferences.encode({ asynchronous: true }))
|
|
151
|
-
// console.log(AppliedPreferences.encode({ preferences: new Map([
|
|
152
|
-
// [ 'respond-async', { value: undefined } ]
|
|
153
|
-
// ]) }))
|
|
154
|
-
// console.log(AppliedPreferences.encode({
|
|
155
|
-
// asynchronous: false,
|
|
156
|
-
// preferences: new Map([
|
|
157
|
-
// [ 'respond-async', { value: 'fake' } ]
|
|
158
|
-
// ]) }))
|
|
159
|
-
// console.log(AppliedPreferences.encode({ asynchronous: true, wait: 100 }))
|
|
160
|
-
// console.log(AppliedPreferences.encode({
|
|
161
|
-
// representation: DIRECTIVE_REPRESENTATION_MINIMAL,
|
|
162
|
-
// preferences: new Map([
|
|
163
|
-
// ['foo', { value: 'bar', parameters: new Map([ [ 'biz', 'bang' ] ]) } ],
|
|
164
|
-
// [ 'fake', undefined ]
|
|
165
|
-
// ])
|
|
166
|
-
// }))
|
|
167
|
-
|
|
168
|
-
// console.log(Preferences.parse('handling=lenient, wait=100, respond-async'))
|
|
169
|
-
// console.log(Preferences.parse(' foo; bar')?.preferences)
|
|
170
|
-
// console.log(Preferences.parse(' foo; bar=""')?.preferences)
|
|
171
|
-
// console.log(Preferences.parse(' foo=""; bar')?.preferences)
|
|
172
|
-
// console.log(Preferences.parse(' foo =""; bar;biz; bang ')?.preferences)
|
|
173
|
-
// console.log(Preferences.parse('return=minimal; foo="some parameter"')?.preferences)
|
|
174
|
-
|
|
175
|
-
// console.log(Preferences.parse('timezone=America/Los_Angeles'))
|
|
176
|
-
// console.log(Preferences.parse('return=headers-only'))
|
|
177
|
-
// console.log(Preferences.parse('return=minimal'))
|
|
178
|
-
// console.log(Preferences.parse('return=representation'))
|
|
179
|
-
// console.log(Preferences.parse('respond-async, wait=10`'))
|
|
180
|
-
// console.log(Preferences.parse('priority=5'))
|
|
181
|
-
// console.log(Preferences.parse('foo; bar'))
|
|
182
|
-
// console.log(Preferences.parse('foo; bar=""'))
|
|
183
|
-
// console.log(Preferences.parse('foo=""; bar'))
|
|
184
|
-
// console.log(Preferences.parse('handling=lenient, wait=100, respond-async'))
|
|
185
|
-
// console.log(Preferences.parse('return=minimal; foo="some parameter"'))
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { RANGE_UNITS_BYTES } from
|
|
1
|
+
import { RANGE_UNITS_BYTES } from '../defs.js'
|
|
2
|
+
|
|
3
|
+
/** @import { AcceptRangeUnits } from '../defs.js' */
|
|
2
4
|
|
|
3
5
|
export const RANGE_EQUAL = '='
|
|
4
6
|
export const RANGE_SEPARATOR = '-'
|
|
@@ -32,7 +34,7 @@ export const RANGE_EMPTY = ''
|
|
|
32
34
|
/**
|
|
33
35
|
* @template RV
|
|
34
36
|
* @typedef {Object} RangeDirective
|
|
35
|
-
* @property {
|
|
37
|
+
* @property {AcceptRangeUnits|undefined} units
|
|
36
38
|
* @property {Array<RV>} ranges
|
|
37
39
|
*/
|
|
38
40
|
|
|
@@ -122,33 +124,3 @@ export class Range {
|
|
|
122
124
|
}
|
|
123
125
|
}
|
|
124
126
|
}
|
|
125
|
-
|
|
126
|
-
// console.log(Range.parse(''))
|
|
127
|
-
// console.log(Range.parse('='))
|
|
128
|
-
// console.log(Range.parse('foo'))
|
|
129
|
-
// console.log(Range.parse('bytes'))
|
|
130
|
-
// console.log(Range.parse('bytes='))
|
|
131
|
-
// console.log(Range.parse('bytes=-'))
|
|
132
|
-
// console.log(Range.parse('bytes=foo'))
|
|
133
|
-
// console.log(Range.parse('bytes=0-foo'))
|
|
134
|
-
// console.log(Range.parse('bytes=0-0xff'))
|
|
135
|
-
// console.log()
|
|
136
|
-
// console.log(Range.parse('bytes=1024-'))
|
|
137
|
-
// console.log(Range.parse('bytes=-1024'))
|
|
138
|
-
// console.log(Range.parse('bytes=0-1024'))
|
|
139
|
-
// console.log()
|
|
140
|
-
// console.log(Range.parse('bytes=0-0,-1'))
|
|
141
|
-
// console.log(Range.parse('bytes=0-1024, -1024'))
|
|
142
|
-
// console.log(Range.parse('bytes= 0-999, 4500-5499, -1000'))
|
|
143
|
-
// console.log(Range.parse('bytes=500-600,601-999'))
|
|
144
|
-
|
|
145
|
-
// console.log('------')
|
|
146
|
-
// console.log(Range.normalize(Range.parse('bytes=1024-'), 5000))
|
|
147
|
-
// console.log(Range.normalize(Range.parse('bytes=-1024'), 5000))
|
|
148
|
-
// console.log(Range.normalize(Range.parse('bytes=0-1024'), 5000))
|
|
149
|
-
// console.log(Range.normalize(Range.parse('bytes=0-0,-1'), 10000)) // 0 and 9999
|
|
150
|
-
// console.log(Range.normalize(Range.parse('bytes=0-1024, -1024'), 5000))
|
|
151
|
-
// console.log(Range.normalize(Range.parse('bytes= 0-999, 4500-5499, -1000'), 5000))
|
|
152
|
-
// console.log(Range.normalize(Range.parse('bytes=500-600,601-999'), 5000))
|
|
153
|
-
|
|
154
|
-
// console.log(Range.normalize(Range.parse('bytes=-500'), 10000)) // 9500-9999
|
|
@@ -45,6 +45,7 @@ export class RateLimit {
|
|
|
45
45
|
* @param {RateLimitInfo} limitInfo
|
|
46
46
|
*/
|
|
47
47
|
static from(limitInfo) {
|
|
48
|
+
if(limitInfo === undefined) { return undefined }
|
|
48
49
|
const { name, remaining, resetSeconds, partitionKey } = limitInfo
|
|
49
50
|
|
|
50
51
|
if(name === undefined || remaining === undefined) { return undefined }
|
|
@@ -61,8 +62,12 @@ export class RateLimitPolicy {
|
|
|
61
62
|
*/
|
|
62
63
|
static from(...policies) {
|
|
63
64
|
if(policies === undefined) { return undefined }
|
|
65
|
+
if(policies.length === 0) { return undefined }
|
|
64
66
|
|
|
65
|
-
|
|
67
|
+
const remainingPolicies = policies.filter(pol => pol !== undefined)
|
|
68
|
+
if(remainingPolicies.length === 0) { return undefined }
|
|
69
|
+
|
|
70
|
+
return remainingPolicies
|
|
66
71
|
.filter(policy => policy.name !== undefined && policy.quota !== undefined)
|
|
67
72
|
.map(policy => {
|
|
68
73
|
const {
|
|
@@ -19,7 +19,7 @@ export const SERVER_TIMING_SEPARATOR = {
|
|
|
19
19
|
|
|
20
20
|
export class ServerTiming {
|
|
21
21
|
/**
|
|
22
|
-
* @param {Array<TimingsInfo
|
|
22
|
+
* @param {Array<TimingsInfo>|undefined} timings
|
|
23
23
|
*/
|
|
24
24
|
static encode(timings) {
|
|
25
25
|
if(timings === undefined) { return undefined }
|
|
@@ -28,24 +28,11 @@ export class ServerTiming {
|
|
|
28
28
|
return timings
|
|
29
29
|
.map(({ name, duration, description }) => [
|
|
30
30
|
`${name}`,
|
|
31
|
-
description
|
|
32
|
-
duration
|
|
31
|
+
description === undefined ? undefined : `${SERVER_TIMING_KEY_DESCRIPTION}${SERVER_TIMING_SEPARATOR.KVP}"${description}"`,
|
|
32
|
+
duration === undefined ? undefined : `${SERVER_TIMING_KEY_DURATION}${SERVER_TIMING_SEPARATOR.KVP}${Math.trunc(duration * 10) / 10}`
|
|
33
33
|
]
|
|
34
34
|
.filter(item => item !== undefined)
|
|
35
35
|
.join(SERVER_TIMING_SEPARATOR.PARAMETER))
|
|
36
36
|
.join(SERVER_TIMING_SEPARATOR.METRIC)
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
// console.log(ServerTiming.encode([{ name: 'missedCache' }]))
|
|
42
|
-
// console.log(ServerTiming.encode([{ name: 'cpu', duration: 2.4 }]))
|
|
43
|
-
|
|
44
|
-
// // cache;desc="Cache Read";dur=23.2
|
|
45
|
-
// console.log(ServerTiming.encode([{ name: 'cache', duration: 23.2, description: "Cache Read" }]))
|
|
46
|
-
|
|
47
|
-
// // db;dur=53, app;dur=47.2
|
|
48
|
-
// console.log(ServerTiming.encode([
|
|
49
|
-
// { name: 'db', duration: 54 },
|
|
50
|
-
// { name: 'app', duration: 47.2 }
|
|
51
|
-
// ]))
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
export const STS_MAX_AGE = 'max-age'
|
|
4
|
+
export const STS_INCLUDE_SUBDOMAIN = 'includeSubDomains'
|
|
5
|
+
export const STS_PRELOAD = 'preload'
|
|
6
|
+
|
|
7
|
+
export const STS_MIN_AGE_FOR_PRELOAD_SECS = 60 * 60 * 24 * 365 // 31536000
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {Object} StrictTransportSecurityOptions
|
|
12
|
+
* @property {number} maxAge
|
|
13
|
+
* @property {boolean|undefined} [includeSubDomains]
|
|
14
|
+
* @property {boolean|undefined} [preload]
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export class StrictTransportSecurity {
|
|
18
|
+
/**
|
|
19
|
+
* @param {StrictTransportSecurityOptions} sts
|
|
20
|
+
*/
|
|
21
|
+
static *#encode(sts) {
|
|
22
|
+
if(!Number.isFinite(sts.maxAge)) {
|
|
23
|
+
throw new Error('invalid max-age')
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const maxAge = sts.preload ? Math.max(STS_MIN_AGE_FOR_PRELOAD_SECS, sts.maxAge) : sts.maxAge
|
|
27
|
+
yield `${STS_MAX_AGE}=${maxAge}`
|
|
28
|
+
if(sts.includeSubDomains) { yield STS_INCLUDE_SUBDOMAIN }
|
|
29
|
+
if(sts.preload) { yield STS_PRELOAD }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {StrictTransportSecurityOptions} sts
|
|
34
|
+
*/
|
|
35
|
+
static encode(sts) {
|
|
36
|
+
if(sts === undefined) { return undefined }
|
|
37
|
+
return [ ...StrictTransportSecurity.#encode(sts) ].join('; ')
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
import { KVP } from './kvp.js'
|
|
2
|
+
|
|
1
3
|
export const QUALITY = 'q'
|
|
2
4
|
export const SEPARATOR = {
|
|
3
|
-
MEDIA_RANGE: ','
|
|
4
|
-
PARAMETER: ';',
|
|
5
|
-
KVP: '='
|
|
5
|
+
MEDIA_RANGE: ','
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
export const DEFAULT_QUALITY_STRING = '1'
|
|
@@ -29,16 +29,10 @@ export function parseAcceptStyleHeader(header, wellKnown) {
|
|
|
29
29
|
.trim()
|
|
30
30
|
.split(SEPARATOR.MEDIA_RANGE)
|
|
31
31
|
.map(mediaRange => {
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const parameters = new Map(parametersSet.map(parameter => {
|
|
37
|
-
const [ key, value ] = parameter.split(SEPARATOR.KVP).map(p => p.trim())
|
|
38
|
-
return [ key, value ]
|
|
39
|
-
}))
|
|
32
|
+
const { name, parameters } = KVP.parse(mediaRange) ?? { parameters: new Map() }
|
|
33
|
+
if(name === undefined) { return undefined }
|
|
34
|
+
if(name === '') { return undefined }
|
|
40
35
|
|
|
41
|
-
if(!parameters.has(QUALITY)) { parameters.set(QUALITY, DEFAULT_QUALITY_STRING) }
|
|
42
36
|
const quality = Number.parseFloat(parameters.get(QUALITY) ?? DEFAULT_QUALITY_STRING)
|
|
43
37
|
|
|
44
38
|
return {
|
|
@@ -47,9 +41,9 @@ export function parseAcceptStyleHeader(header, wellKnown) {
|
|
|
47
41
|
parameters
|
|
48
42
|
}
|
|
49
43
|
})
|
|
50
|
-
.filter(entry => entry
|
|
44
|
+
.filter(entry => entry !== undefined)
|
|
51
45
|
.sort((entryA, entryB) => {
|
|
52
46
|
// B - A descending order
|
|
53
47
|
return entryB.quality - entryA.quality
|
|
54
48
|
})
|
|
55
|
-
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** biome-ignore-all lint/performance/noBarrelFile: entry point */
|
|
2
|
+
/** biome-ignore-all lint/performance/noReExportAll: entry point */
|
|
3
|
+
export * from './accept-util.js'
|
|
4
|
+
export * from './kvp.js'
|
|
5
|
+
export * from './mime.js'
|
|
6
|
+
export * from './quote.js'
|
|
7
|
+
export * from './whitespace.js'
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { hasSpecialChar } from './mime.js'
|
|
2
|
+
import { isQuoted, stripQuotes } from './quote.js'
|
|
3
|
+
|
|
4
|
+
export const DEFAULT_DELIMITER = ';'
|
|
5
|
+
export const KVP_DELIMITER = '='
|
|
6
|
+
export const KVP_EMPTY = ''
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
export class KVP {
|
|
10
|
+
/**
|
|
11
|
+
* @param {Array<string>|undefined} params
|
|
12
|
+
* @param {Array<string>|undefined} [acceptableKeys=undefined]
|
|
13
|
+
*/
|
|
14
|
+
static #parse(params, acceptableKeys = undefined) {
|
|
15
|
+
const parameters = new Map()
|
|
16
|
+
|
|
17
|
+
if(params === undefined) { return parameters }
|
|
18
|
+
|
|
19
|
+
for(const kvp of params) {
|
|
20
|
+
const [ rawKey, rawValue ] = kvp
|
|
21
|
+
.split(KVP_DELIMITER)
|
|
22
|
+
.map(p => p.trim())
|
|
23
|
+
|
|
24
|
+
if(rawKey === undefined) { continue }
|
|
25
|
+
if(rawKey === KVP_EMPTY) { continue }
|
|
26
|
+
const key = rawKey.toLowerCase()
|
|
27
|
+
if(hasSpecialChar(key)) { continue }
|
|
28
|
+
|
|
29
|
+
if(acceptableKeys !== undefined && !acceptableKeys.includes(key)) { continue }
|
|
30
|
+
|
|
31
|
+
const unquotedValue = isQuoted(rawValue) ? stripQuotes(rawValue) : rawValue
|
|
32
|
+
const value = unquotedValue === KVP_EMPTY ? undefined : unquotedValue
|
|
33
|
+
|
|
34
|
+
if(!parameters.has(key)) {
|
|
35
|
+
parameters.set(key, value)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return parameters
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @param {string|undefined} str
|
|
44
|
+
* @param {Array<string>|undefined} [acceptableKeys=undefined]
|
|
45
|
+
*/
|
|
46
|
+
static parse(str, acceptableKeys = undefined, delimiter = DEFAULT_DELIMITER) {
|
|
47
|
+
if(str === undefined) { return undefined }
|
|
48
|
+
if(str === KVP_EMPTY) { return undefined }
|
|
49
|
+
|
|
50
|
+
const [ name, ...params ] = str
|
|
51
|
+
.trim()
|
|
52
|
+
.split(delimiter)
|
|
53
|
+
.map(p => p.trim())
|
|
54
|
+
|
|
55
|
+
const parameters = KVP.#parse(params, acceptableKeys)
|
|
56
|
+
|
|
57
|
+
return { name, parameters }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @param {string|undefined} str
|
|
62
|
+
* @param {Array<string>|undefined} [acceptableKeys=undefined]
|
|
63
|
+
*/
|
|
64
|
+
static parseParameters(str, acceptableKeys = undefined, delimiter = DEFAULT_DELIMITER) {
|
|
65
|
+
if(str === undefined) { return undefined }
|
|
66
|
+
if(str === KVP_EMPTY) { return undefined }
|
|
67
|
+
|
|
68
|
+
const params = str
|
|
69
|
+
.trim()
|
|
70
|
+
.split(delimiter)
|
|
71
|
+
.map(p => p.trim())
|
|
72
|
+
|
|
73
|
+
const parameters = KVP.#parse(params, acceptableKeys)
|
|
74
|
+
|
|
75
|
+
return { parameters }
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
* @typedef {Object} MimeItem
|
|
4
|
+
* @property {string} mimetype
|
|
5
|
+
* @property {string} type
|
|
6
|
+
* @property {string} subtype
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const MIME_SEPARATOR = { SUBTYPE: '/' }
|
|
10
|
+
|
|
11
|
+
export const MIME_ANY = '*'
|
|
12
|
+
|
|
13
|
+
//
|
|
14
|
+
export const SPECIAL_CHARS = [
|
|
15
|
+
// special
|
|
16
|
+
'(', ')',
|
|
17
|
+
'<', '>',
|
|
18
|
+
'[', ']',
|
|
19
|
+
'{', '}',
|
|
20
|
+
'@', ',', ';', ':',
|
|
21
|
+
'\\', '"', '/', '?', '=', // '.',
|
|
22
|
+
// '%', // '!', '$', '&', // # ^ * | ~ `
|
|
23
|
+
// space
|
|
24
|
+
' ', '\u000B', '\u000C',
|
|
25
|
+
// control
|
|
26
|
+
'\n', '\r', '\t'
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {string|undefined} value
|
|
31
|
+
*/
|
|
32
|
+
export function hasSpecialChar(value) {
|
|
33
|
+
if(value === undefined) { return false }
|
|
34
|
+
for(const special of SPECIAL_CHARS) {
|
|
35
|
+
if(value.includes(special)) { return true }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return false
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export class Mime {
|
|
42
|
+
/**
|
|
43
|
+
* @param {string|undefined} name
|
|
44
|
+
* @returns {MimeItem|undefined}
|
|
45
|
+
*/
|
|
46
|
+
static parse(name) {
|
|
47
|
+
// console.log('mime::parse', name)
|
|
48
|
+
if(name === undefined) { return undefined }
|
|
49
|
+
if(name === '') { return undefined }
|
|
50
|
+
|
|
51
|
+
const parts = name
|
|
52
|
+
.trim() // leading space of type and trailing of subtype
|
|
53
|
+
.split(MIME_SEPARATOR.SUBTYPE)
|
|
54
|
+
.map(t => t.toLowerCase()) // all type/subtypes should be lower
|
|
55
|
+
|
|
56
|
+
// protect against multiple slashes
|
|
57
|
+
if(parts.length > 2) { return undefined }
|
|
58
|
+
|
|
59
|
+
const [ type, candidateSubtype ] = parts
|
|
60
|
+
|
|
61
|
+
if(type === undefined) { return undefined }
|
|
62
|
+
if(type === '') { return undefined }
|
|
63
|
+
if(hasSpecialChar(type)) { return undefined }
|
|
64
|
+
|
|
65
|
+
// if(candidateSubtype === undefined) { return undefined }
|
|
66
|
+
// if(candidateSubtype === '') { return undefined }
|
|
67
|
+
if(hasSpecialChar(candidateSubtype)) { return undefined }
|
|
68
|
+
|
|
69
|
+
const subtype = (candidateSubtype === '') ? MIME_ANY : (candidateSubtype ?? MIME_ANY)
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
mimetype: `${type}${MIME_SEPARATOR.SUBTYPE}${subtype}`,
|
|
73
|
+
type,
|
|
74
|
+
subtype
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -99,7 +99,7 @@ export class Challenge {
|
|
|
99
99
|
return `${key}=${value}`
|
|
100
100
|
})
|
|
101
101
|
|
|
102
|
-
const params = parameters
|
|
102
|
+
const params = parameters === undefined ? '' : [ ...parameters ].join(',')
|
|
103
103
|
|
|
104
104
|
return `${challenge.scheme} ${params}`
|
|
105
105
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import http2 from 'node:http2'
|
|
2
2
|
|
|
3
|
-
import { send } from '
|
|
3
|
+
import { send } from '../send-util.js'
|
|
4
4
|
|
|
5
5
|
/** @import { ServerHttp2Stream } from 'node:http2' */
|
|
6
|
-
/** @import { Metadata } from '
|
|
6
|
+
/** @import { Metadata } from '../../defs.js' */
|
|
7
7
|
|
|
8
8
|
const { HTTP_STATUS_ACCEPTED } = http2.constants
|
|
9
9
|
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import http2 from 'node:http2'
|
|
2
|
+
|
|
3
|
+
import { send_bytes } from '../send-util.js'
|
|
4
|
+
|
|
5
|
+
/** @import { ServerHttp2Stream } from 'node:http2' */
|
|
6
|
+
/** @import { AcceptRangeUnits, SendContent, SendInfo, Metadata, SendBody } from '../../defs.js' */
|
|
7
|
+
/** @import { EtagItem, IMFFixDateInput } from '../../headers/conditional.js' */
|
|
8
|
+
/** @import { CacheControlOptions } from '../../headers/cache-control.js' */
|
|
9
|
+
|
|
10
|
+
const { HTTP_STATUS_OK } = http2.constants
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {ServerHttp2Stream} stream
|
|
14
|
+
* @param {SendBody|undefined} obj
|
|
15
|
+
* @param {string|undefined} contentType
|
|
16
|
+
* @param {number|undefined} contentLength
|
|
17
|
+
* @param {string|undefined} encoding
|
|
18
|
+
* @param {EtagItem|undefined} etag
|
|
19
|
+
* @param {IMFFixDateInput|string|undefined} lastModified
|
|
20
|
+
* @param {number|undefined} age
|
|
21
|
+
* @param {CacheControlOptions} cacheControl
|
|
22
|
+
* @param {AcceptRangeUnits|undefined} acceptRanges
|
|
23
|
+
* @param {Metadata} meta
|
|
24
|
+
*/
|
|
25
|
+
export function sendBytes(stream, contentType, obj, contentLength, encoding, etag, lastModified, age, cacheControl, acceptRanges, meta) {
|
|
26
|
+
_sendBytes(stream, obj, {
|
|
27
|
+
contentType,
|
|
28
|
+
contentLength,
|
|
29
|
+
encoding,
|
|
30
|
+
etag,
|
|
31
|
+
lastModified,
|
|
32
|
+
age,
|
|
33
|
+
cacheControl
|
|
34
|
+
}, {
|
|
35
|
+
acceptRanges
|
|
36
|
+
}, meta)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @param {ServerHttp2Stream} stream
|
|
41
|
+
* @param {SendBody|undefined} obj
|
|
42
|
+
* @param {Omit<SendContent, 'rangeDirective'>} content
|
|
43
|
+
* @param {Pick<SendInfo, 'acceptRanges'>} info
|
|
44
|
+
* @param {Metadata} meta
|
|
45
|
+
*/
|
|
46
|
+
export function _sendBytes(stream, obj, content, info, meta) {
|
|
47
|
+
const {
|
|
48
|
+
contentType,
|
|
49
|
+
contentLength,
|
|
50
|
+
encoding,
|
|
51
|
+
etag,
|
|
52
|
+
lastModified,
|
|
53
|
+
age,
|
|
54
|
+
cacheControl,
|
|
55
|
+
} = content
|
|
56
|
+
|
|
57
|
+
const { acceptRanges } = info
|
|
58
|
+
|
|
59
|
+
const supportedQueryType = undefined
|
|
60
|
+
const range = undefined
|
|
61
|
+
send_bytes(stream, HTTP_STATUS_OK, contentType, obj, range, contentLength, encoding, etag, lastModified, age, cacheControl, acceptRanges, supportedQueryType, meta)
|
|
62
|
+
}
|