@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
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
/** biome-ignore-all lint/nursery/noExcessiveClassesPerFile: includes helper classes */
|
|
2
|
+
/** biome-ignore-all lint/nursery/noExcessiveLinesPerFile: includes temporal and date support */
|
|
3
|
+
import { isQuoted, stripQuotes } from './util/quote.js'
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* @typedef {Object} WeakEtagItem
|
|
@@ -24,7 +26,7 @@ import { isQuoted, stripQuotes } from './quote.js'
|
|
|
24
26
|
/** @typedef {WeakEtagItem | NotWeakEtagItem | AnyEtagItem } EtagItem */
|
|
25
27
|
|
|
26
28
|
/**
|
|
27
|
-
* @typedef {Object}
|
|
29
|
+
* @typedef {Object} IMFFixDateItem
|
|
28
30
|
* @property {typeof DATE_DAYS[number]} dayName
|
|
29
31
|
* @property {number} day
|
|
30
32
|
* @property {typeof DATE_MONTHS[number]} month
|
|
@@ -32,9 +34,40 @@ import { isQuoted, stripQuotes } from './quote.js'
|
|
|
32
34
|
* @property {number} hour
|
|
33
35
|
* @property {number} minute
|
|
34
36
|
* @property {number} second
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @typedef {Object} IMFFixDateItemExtension
|
|
35
41
|
* @property {Date|undefined} [date]
|
|
42
|
+
* @property {Temporal.Instant|undefined} [instant]
|
|
36
43
|
*/
|
|
37
44
|
|
|
45
|
+
/** @typedef {IMFFixDateItem & IMFFixDateItemExtension} IMFFixDate */
|
|
46
|
+
/** @typedef {IMFFixDate|Date|Temporal.Instant|undefined} IMFFixDateInput */
|
|
47
|
+
|
|
48
|
+
export const FEATURE_TEMPORAL = typeof Temporal !== 'undefined'
|
|
49
|
+
|
|
50
|
+
export const IMF_FIX_DATE_FORMATTER = new Intl.DateTimeFormat('en-US', {
|
|
51
|
+
timeZone: 'UTC',
|
|
52
|
+
weekday: 'short',
|
|
53
|
+
year: 'numeric',
|
|
54
|
+
month: 'short',
|
|
55
|
+
day: '2-digit',
|
|
56
|
+
hour: '2-digit',
|
|
57
|
+
minute: '2-digit',
|
|
58
|
+
second: '2-digit',
|
|
59
|
+
hour12: false
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @param {IMFFixDate|Date|Temporal.Instant|undefined} reference
|
|
64
|
+
* @returns {reference is Temporal.Instant}
|
|
65
|
+
*/
|
|
66
|
+
export function isTemporalInstant(reference) {
|
|
67
|
+
if(!FEATURE_TEMPORAL) { return false }
|
|
68
|
+
return reference instanceof Temporal.Instant
|
|
69
|
+
}
|
|
70
|
+
|
|
38
71
|
export const CONDITION_ETAG_SEPARATOR = ','
|
|
39
72
|
export const CONDITION_ETAG_ANY = '*'
|
|
40
73
|
export const CONDITION_ETAG_WEAK_PREFIX = 'W/'
|
|
@@ -54,25 +87,36 @@ export const MINIMUM_YEAR = 1900
|
|
|
54
87
|
export const MAXIMUM_DAY = 31
|
|
55
88
|
|
|
56
89
|
/**
|
|
90
|
+
* @deprecated
|
|
91
|
+
* @see {@link ETag.isValid}
|
|
57
92
|
* @param {string} etag
|
|
58
93
|
*/
|
|
59
94
|
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
|
|
95
|
+
return ETag.isValid(etag)
|
|
67
96
|
}
|
|
68
97
|
|
|
69
98
|
export class ETag {
|
|
99
|
+
/**
|
|
100
|
+
* @param {string} etag
|
|
101
|
+
*/
|
|
102
|
+
static isValid(etag) {
|
|
103
|
+
if(etag === undefined) { return false }
|
|
104
|
+
|
|
105
|
+
// %x21 / %x23-7E and %x80-FF
|
|
106
|
+
for(const c of etag) {
|
|
107
|
+
if(c.charCodeAt(0) < 0x21) { return false }
|
|
108
|
+
if(c.charCodeAt(0) > 0xFF) { return false }
|
|
109
|
+
if(c === ETAG_QUOTE) { return false }
|
|
110
|
+
}
|
|
111
|
+
return true
|
|
112
|
+
}
|
|
113
|
+
|
|
70
114
|
/**
|
|
71
115
|
* @param {string} etag
|
|
72
116
|
* @returns {WeakEtagItem}
|
|
73
117
|
*/
|
|
74
118
|
static weak(etag) {
|
|
75
|
-
if(!
|
|
119
|
+
if(!ETag.isValid(etag)) { throw new Error('invalid etag format') }
|
|
76
120
|
return { any: false, weak: true, etag }
|
|
77
121
|
}
|
|
78
122
|
|
|
@@ -81,7 +125,7 @@ export class ETag {
|
|
|
81
125
|
* @returns {NotWeakEtagItem}
|
|
82
126
|
*/
|
|
83
127
|
static strong(etag) {
|
|
84
|
-
if(!
|
|
128
|
+
if(!ETag.isValid(etag)) { throw new Error('invalid etag format') }
|
|
85
129
|
return { any: false, weak: false, etag }
|
|
86
130
|
}
|
|
87
131
|
|
|
@@ -106,8 +150,9 @@ export class ETag {
|
|
|
106
150
|
if(!isQuoted(quotedEtag)) { return undefined }
|
|
107
151
|
const etag = stripQuotes(quotedEtag)
|
|
108
152
|
if(etag === undefined) { return undefined }
|
|
109
|
-
if(
|
|
110
|
-
if(etag === CONDITION_ETAG_ANY) { return
|
|
153
|
+
if(etag === '') { return undefined }
|
|
154
|
+
if(etag === CONDITION_ETAG_ANY) { return ANY_ETAG_ITEM } // todo: should this return undefined?
|
|
155
|
+
if(!ETag.isValid(etag)) { return undefined }
|
|
111
156
|
|
|
112
157
|
return {
|
|
113
158
|
weak,
|
|
@@ -117,6 +162,100 @@ export class ETag {
|
|
|
117
162
|
}
|
|
118
163
|
}
|
|
119
164
|
|
|
165
|
+
export class FixDate {
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* True if {@link test} is after (exclusive) the {@link reference}
|
|
169
|
+
* (compares are seconds precision)
|
|
170
|
+
* @param {IMFFixDateInput} reference
|
|
171
|
+
* @param {IMFFixDateInput} test
|
|
172
|
+
* @returns {boolean}
|
|
173
|
+
*/
|
|
174
|
+
static isAfter(reference, test) {
|
|
175
|
+
if(reference === undefined) { return false }
|
|
176
|
+
if(test === undefined) { return false }
|
|
177
|
+
|
|
178
|
+
if(FEATURE_TEMPORAL) {
|
|
179
|
+
/** @type {Temporal.RoundingOptions<Temporal.TimeUnit>} */
|
|
180
|
+
const precision = {
|
|
181
|
+
smallestUnit: 'second',
|
|
182
|
+
roundingMode: 'trunc'
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const referenceInstant = FixDate.toInstant(reference)?.round(precision)
|
|
186
|
+
const testInstant = FixDate.toInstant(test)?.round(precision)
|
|
187
|
+
|
|
188
|
+
if(referenceInstant === undefined) { return false }
|
|
189
|
+
if(testInstant === undefined) { return false }
|
|
190
|
+
|
|
191
|
+
return Temporal.Instant.compare(referenceInstant, testInstant) === -1
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
//
|
|
195
|
+
const referenceDate = FixDate.toDate(reference)
|
|
196
|
+
const testDate = FixDate.toDate(test)
|
|
197
|
+
|
|
198
|
+
if(referenceDate === undefined) { return false }
|
|
199
|
+
if(testDate === undefined) { return false }
|
|
200
|
+
|
|
201
|
+
// this effectively rounds to seconds
|
|
202
|
+
referenceDate.setMilliseconds(0)
|
|
203
|
+
testDate.setMilliseconds(0)
|
|
204
|
+
|
|
205
|
+
return testDate > referenceDate
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* @param {IMFFixDateInput} reference
|
|
210
|
+
* @returns {Temporal.Instant|undefined}
|
|
211
|
+
*/
|
|
212
|
+
static toInstant(reference) {
|
|
213
|
+
if(!FEATURE_TEMPORAL) { return undefined }
|
|
214
|
+
if(reference === undefined) { return undefined }
|
|
215
|
+
|
|
216
|
+
if(isTemporalInstant(reference)) { return reference }
|
|
217
|
+
if(reference instanceof Date) { return reference.toTemporalInstant() }
|
|
218
|
+
|
|
219
|
+
if(reference.instant !== undefined) { return reference.instant }
|
|
220
|
+
if(reference.date !== undefined) { return reference.date.toTemporalInstant() }
|
|
221
|
+
|
|
222
|
+
const { year, month: monthName, day, hour, minute, second } = reference
|
|
223
|
+
|
|
224
|
+
const zeroMonth = DATE_MONTHS.indexOf(monthName)
|
|
225
|
+
if(zeroMonth === -1) { return undefined }
|
|
226
|
+
const month = zeroMonth + 1
|
|
227
|
+
|
|
228
|
+
const zdt = Temporal.ZonedDateTime.from({
|
|
229
|
+
year,
|
|
230
|
+
month,
|
|
231
|
+
day,
|
|
232
|
+
hour,
|
|
233
|
+
minute,
|
|
234
|
+
second,
|
|
235
|
+
timeZone: 'UTC'
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
return zdt.toInstant()
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* @param {IMFFixDateInput} reference
|
|
243
|
+
* @returns {Date|undefined}
|
|
244
|
+
*/
|
|
245
|
+
static toDate(reference) {
|
|
246
|
+
if(reference === undefined) { return undefined }
|
|
247
|
+
|
|
248
|
+
if(reference instanceof Date) { return reference }
|
|
249
|
+
if(isTemporalInstant(reference)) { return new Date(reference.epochMilliseconds) }
|
|
250
|
+
|
|
251
|
+
if(FEATURE_TEMPORAL && (reference.instant !== undefined)) { return new Date(reference.instant.epochMilliseconds) }
|
|
252
|
+
if(reference.date !== undefined) { return reference.date }
|
|
253
|
+
|
|
254
|
+
const { year, month, day, hour, minute, second } = reference
|
|
255
|
+
return new Date(Date.UTC(year, DATE_MONTHS.indexOf(month), day, hour, minute, second))
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
120
259
|
export class Conditional {
|
|
121
260
|
/**
|
|
122
261
|
* @param {EtagItem|undefined} etagItem
|
|
@@ -130,7 +269,7 @@ export class Conditional {
|
|
|
130
269
|
}
|
|
131
270
|
|
|
132
271
|
if(etagItem.etag === CONDITION_ETAG_ANY) { return undefined }
|
|
133
|
-
if(!
|
|
272
|
+
if(!ETag.isValid(etagItem.etag)) { return undefined }
|
|
134
273
|
|
|
135
274
|
const prefix = etagItem.weak ? CONDITION_ETAG_WEAK_PREFIX : ''
|
|
136
275
|
return `${prefix}${ETAG_QUOTE}${etagItem.etag}${ETAG_QUOTE}`
|
|
@@ -149,29 +288,47 @@ export class Conditional {
|
|
|
149
288
|
}
|
|
150
289
|
|
|
151
290
|
/**
|
|
152
|
-
* @param {Array<EtagItem
|
|
291
|
+
* @param {Array<EtagItem>|undefined} etagItemList
|
|
153
292
|
*/
|
|
154
293
|
static hasAny(etagItemList) {
|
|
155
|
-
return etagItemList
|
|
294
|
+
return etagItemList?.find(item => item.any) !== undefined
|
|
156
295
|
}
|
|
157
296
|
|
|
158
297
|
/**
|
|
159
|
-
* @param {Array<EtagItem
|
|
160
|
-
* @param {string} etag
|
|
298
|
+
* @param {Array<EtagItem>|undefined} etagItemList
|
|
299
|
+
* @param {string|undefined} etag
|
|
161
300
|
*/
|
|
162
301
|
static hasEtag(etagItemList, etag) {
|
|
163
|
-
return etagItemList
|
|
302
|
+
return etagItemList?.find(item => item.etag === etag) !== undefined
|
|
164
303
|
}
|
|
165
304
|
|
|
166
305
|
/**
|
|
167
|
-
* @param {
|
|
306
|
+
* @param {IMFFixDateInput|string|undefined} reference
|
|
168
307
|
* @returns {string|undefined}
|
|
169
308
|
*/
|
|
170
|
-
static encodeFixDate(
|
|
171
|
-
if(
|
|
172
|
-
|
|
309
|
+
static encodeFixDate(reference) {
|
|
310
|
+
if(reference === undefined) { return undefined }
|
|
311
|
+
|
|
312
|
+
if(typeof reference === 'string') { return reference }
|
|
313
|
+
if(reference instanceof Date) { return reference.toUTCString() }
|
|
314
|
+
if(isTemporalInstant(reference)) {
|
|
315
|
+
|
|
316
|
+
const parts = IMF_FIX_DATE_FORMATTER.formatToParts(reference)
|
|
317
|
+
const m = new Map(parts.map(p => [ p.type, p.value ]))
|
|
318
|
+
const weekday = m.get('weekday') ?? 'ERR'
|
|
319
|
+
const day = m.get('day') ?? '00'
|
|
320
|
+
const month = m.get('month') ?? '00'
|
|
321
|
+
const year = m.get('year') ?? '0000'
|
|
322
|
+
const hour = m.get('hour') ?? '00'
|
|
323
|
+
const minute = m.get('minute') ?? '00'
|
|
324
|
+
const second = m.get('second') ?? '00'
|
|
325
|
+
|
|
326
|
+
return `${weekday}, ${day} ${month} ${year} ${hour}:${minute}:${second} ${DATE_ZONE}`
|
|
327
|
+
}
|
|
173
328
|
|
|
174
|
-
|
|
329
|
+
if(reference.date !== undefined) { return reference.date.toUTCString() }
|
|
330
|
+
|
|
331
|
+
const { year, month, day, hour, minute, second } = reference
|
|
175
332
|
const d = new Date(Date.UTC(year, DATE_MONTHS.indexOf(month), day, hour, minute, second))
|
|
176
333
|
return d.toUTCString()
|
|
177
334
|
}
|
|
@@ -241,105 +398,21 @@ export class Conditional {
|
|
|
241
398
|
if(minute > 60 || minute < 0) { return undefined }
|
|
242
399
|
if(second > 60 || second < 0) { return undefined }
|
|
243
400
|
|
|
244
|
-
|
|
245
|
-
return {
|
|
401
|
+
const fixDate = {
|
|
246
402
|
dayName,
|
|
247
403
|
day,
|
|
248
404
|
month,
|
|
249
405
|
year,
|
|
250
406
|
hour,
|
|
251
407
|
minute,
|
|
252
|
-
second
|
|
253
|
-
|
|
254
|
-
|
|
408
|
+
second
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
//
|
|
412
|
+
return {
|
|
413
|
+
...fixDate,
|
|
414
|
+
date: FixDate.toDate(fixDate),
|
|
415
|
+
instant: FixDate.toInstant(fixDate)
|
|
255
416
|
}
|
|
256
417
|
}
|
|
257
418
|
}
|
|
258
|
-
|
|
259
|
-
// Ok
|
|
260
|
-
// console.log(Conditional.encodeEtag({ any: true, weak: false, etag: '*' }))
|
|
261
|
-
// console.log(Conditional.encodeEtag({ any: true, weak: true, etag: '*' }))
|
|
262
|
-
// console.log(Conditional.encodeEtag({ any: false, weak: false, etag: 'Foo' }))
|
|
263
|
-
// console.log(Conditional.encodeEtag({ any: false, weak: true, etag: 'WeakFoo' }))
|
|
264
|
-
|
|
265
|
-
// Error
|
|
266
|
-
// console.log(Conditional.encodeEtag(undefined))
|
|
267
|
-
// console.log(Conditional.encodeEtag({ any: true, weak: false, etag: 'NotAsterisk' }))
|
|
268
|
-
// console.log(Conditional.encodeEtag({ any: false, weak: false, etag: 'Foo\tBar' }))
|
|
269
|
-
// console.log(Conditional.encodeEtag({ any: false, weak: false, etag: 'Foo"Bar' }))
|
|
270
|
-
// console.log(Conditional.encodeEtag({ any: false, weak: false, etag: '*' }))
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
// Ok
|
|
274
|
-
// console.log(Conditional.parseEtagList('"bfc13a64729c4290ef5b2c2730249c88ca92d82d"'))
|
|
275
|
-
// console.log(Conditional.parseEtagList('W/"67ab43", "54ed21", "7892dd"'))
|
|
276
|
-
// console.log(Conditional.parseEtagList('*'))
|
|
277
|
-
// console.log(Conditional.parseEtagList('"!ÿ©"'))
|
|
278
|
-
// console.log(Conditional.parseEtagList('"!","ÿ", "©"'))
|
|
279
|
-
// console.log(Conditional.parseEtagList('"!","ÿ" ,\t"©"'))
|
|
280
|
-
|
|
281
|
-
// Error
|
|
282
|
-
// console.log(Conditional.parseEtagList('"*"'))
|
|
283
|
-
// console.log(Conditional.parseEtagList('W/'))
|
|
284
|
-
// console.log(Conditional.parseEtagList('W/"'))
|
|
285
|
-
// console.log(Conditional.parseEtagList('W/""'))
|
|
286
|
-
// console.log(Conditional.parseEtagList(''))
|
|
287
|
-
// console.log(Conditional.parseEtagList('"'))
|
|
288
|
-
// console.log(Conditional.parseEtagList('""'))
|
|
289
|
-
// console.log(Conditional.parseEtagList('"""'))
|
|
290
|
-
// console.log(Conditional.parseEtagList('" "'))
|
|
291
|
-
// console.log(Conditional.parseEtagList('"\n"'))
|
|
292
|
-
// console.log(Conditional.parseEtagList('"\t"'))
|
|
293
|
-
|
|
294
|
-
//
|
|
295
|
-
// const testsOk = [
|
|
296
|
-
// 'Sun, 06 Nov 1994 08:49:37 GMT',
|
|
297
|
-
// 'Sun, 06 Nov 1994 00:00:00 GMT',
|
|
298
|
-
// 'Tue, 01 Nov 1994 00:00:00 GMT',
|
|
299
|
-
// 'Thu, 06 Nov 3000 08:49:37 GMT',
|
|
300
|
-
// 'Sun, 06 Nov 1994 23:59:59 GMT',
|
|
301
|
-
// new String('Sun, 06 Nov 1994 08:49:37 GMT'),
|
|
302
|
-
// ]
|
|
303
|
-
// for(const test of testsOk) {
|
|
304
|
-
// const result = Conditional.parseFixDate(test)
|
|
305
|
-
// if(result?.date.toUTCString() !== test.toString()) {
|
|
306
|
-
// console.log('🛑', test, result, result?.date.toUTCString())
|
|
307
|
-
// break
|
|
308
|
-
// }
|
|
309
|
-
// }
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
// const testBad = [
|
|
314
|
-
// undefined,
|
|
315
|
-
// null,
|
|
316
|
-
// {},
|
|
317
|
-
// new String(),
|
|
318
|
-
// '',
|
|
319
|
-
// 'Anything',
|
|
320
|
-
// ' , : : GMT',
|
|
321
|
-
// 'Sun, Nov : : GMT',
|
|
322
|
-
// 'Sun, 00 Nov 0000 00:00:00 GMT',
|
|
323
|
-
// 'Sun, 06 Nov 1994 08-49-37 GMT',
|
|
324
|
-
// 'Sun 06 Nov 1994 08:49:37 GMT',
|
|
325
|
-
// 'FOO, 06 Nov 1994 08:49:37 GMT',
|
|
326
|
-
// 'Sun, 32 Nov 1994 08:49:37 GMT',
|
|
327
|
-
// 'Sun, 00 Nov 1994 08:49:37 GMT',
|
|
328
|
-
// 'Sun, 06 Nov 0900 08:49:37 GMT',
|
|
329
|
-
// 'Sun, 06 Nov 1994 08:49:37 UTC',
|
|
330
|
-
// 'Sun, 06 Nov 1994 30:49:37 GMT',
|
|
331
|
-
// 'Sun,\t06 Nov 1994 08:49:37 GMT',
|
|
332
|
-
|
|
333
|
-
// 'Sunday, 06-Nov-94 08:49:37 GMT',
|
|
334
|
-
// 'Sun Nov 6 08:49:37 1994',
|
|
335
|
-
// 'Sun Nov 6 08:49:37 1994 ',
|
|
336
|
-
|
|
337
|
-
// ]
|
|
338
|
-
// for(const test of testBad) {
|
|
339
|
-
// const result = Conditional.parseFixDate(test)
|
|
340
|
-
// if(result !== undefined) {
|
|
341
|
-
// console.log('🛑', test, result)
|
|
342
|
-
// break
|
|
343
|
-
// }
|
|
344
|
-
// }
|
|
345
|
-
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { KVP } from './util/kvp.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {Object} Disposition
|
|
5
|
+
* @property {string} disposition
|
|
6
|
+
* @property {Map<string, string|undefined>} parameters
|
|
7
|
+
* @property {string|undefined} [name]
|
|
8
|
+
* @property {string|undefined} [filename]
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export const DISPOSITION_PARAM_NAME = 'name'
|
|
12
|
+
export const DISPOSITION_PARAM_FILENAME = 'filename'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @deprecated
|
|
16
|
+
* @see {@link ContentDisposition.parse}
|
|
17
|
+
* @param {string|undefined} contentDispositionHeader
|
|
18
|
+
* @returns {Disposition|undefined}
|
|
19
|
+
*/
|
|
20
|
+
export function parseContentDisposition(contentDispositionHeader) {
|
|
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
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { RANGE_UNITS_BYTES } from
|
|
1
|
+
import { RANGE_UNITS_BYTES } from '../defs.js'
|
|
2
2
|
|
|
3
3
|
export const CONTENT_RANGE_UNKNOWN = '*'
|
|
4
4
|
export const CONTENT_RANGE_SEPARATOR = '-'
|
|
@@ -40,20 +40,3 @@ export class ContentRange {
|
|
|
40
40
|
return `${units} ${rangeStr}${CONTENT_RANGE_SIZE_SEPARATOR}${size}`
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
|
-
|
|
44
|
-
// console.log(ContentRange.encode({}))
|
|
45
|
-
// console.log(ContentRange.encode({ size: '*' }))
|
|
46
|
-
// console.log(ContentRange.encode({ units: 'bytes' }))
|
|
47
|
-
// console.log(ContentRange.encode({ range: '*' }))
|
|
48
|
-
// console.log(ContentRange.encode({ range: '*', size: '*' }))
|
|
49
|
-
|
|
50
|
-
// console.log()
|
|
51
|
-
// console.log(ContentRange.encode({ range: { start: 0, end: 1024 } }))
|
|
52
|
-
// console.log(ContentRange.encode({ range: { start: 0, end: 1024 }, size: 1024 }))
|
|
53
|
-
// console.log(ContentRange.encode({ range: '*', size: 1024 }))
|
|
54
|
-
// console.log(ContentRange.encode({ size: 1024 }))
|
|
55
|
-
|
|
56
|
-
// console.log()
|
|
57
|
-
// console.log(ContentRange.encode({ units: 'bob' }))
|
|
58
|
-
// console.log(ContentRange.encode({ range: 'bob' }))
|
|
59
|
-
// console.log(ContentRange.encode({ size: 'bob' }))
|
|
@@ -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
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { KVP } from './util/kvp.js'
|
|
2
|
+
|
|
1
3
|
export const FORWARDED_KEY_BY = 'by'
|
|
2
4
|
export const FORWARDED_KEY_FOR = 'for'
|
|
3
5
|
export const FORWARDED_KEY_HOST = 'host'
|
|
@@ -13,11 +15,8 @@ export const KNOWN_FORWARDED_KEYS = [
|
|
|
13
15
|
export const SKIP_ANY = '*'
|
|
14
16
|
|
|
15
17
|
export const FORWARDED_SEPARATOR = {
|
|
16
|
-
ITEM: ','
|
|
17
|
-
ELEMENT: ';',
|
|
18
|
-
KVP: '='
|
|
18
|
+
ITEM: ','
|
|
19
19
|
}
|
|
20
|
-
|
|
21
20
|
export class Forwarded {
|
|
22
21
|
/**
|
|
23
22
|
* @param {string|undefined} header
|
|
@@ -30,26 +29,10 @@ export class Forwarded {
|
|
|
30
29
|
return header
|
|
31
30
|
.trim()
|
|
32
31
|
.split(FORWARDED_SEPARATOR.ITEM)
|
|
33
|
-
.map(single =>
|
|
34
|
-
|
|
35
|
-
.split(FORWARDED_SEPARATOR.ELEMENT)
|
|
36
|
-
.map(kvp => {
|
|
37
|
-
const [ rawKey, rawValue ] = kvp.trim().split(FORWARDED_SEPARATOR.KVP)
|
|
38
|
-
|
|
39
|
-
const key = rawKey?.trim()?.toLowerCase()
|
|
40
|
-
if (key === undefined || !acceptedKeys.includes(key)) { return undefined }
|
|
41
|
-
|
|
42
|
-
const value = rawValue?.trim()
|
|
43
|
-
if(value === undefined) { return undefined }
|
|
44
|
-
if(value.length <= 0) { return undefined }
|
|
45
|
-
|
|
46
|
-
/** @type {[string, string]} */
|
|
47
|
-
const result = [ key, value ]
|
|
48
|
-
return result
|
|
49
|
-
})
|
|
50
|
-
.filter(item => item !== undefined))
|
|
51
|
-
)
|
|
32
|
+
.map(single => KVP.parseParameters(single, acceptedKeys)?.parameters)
|
|
33
|
+
.filter(m => m !== undefined)
|
|
52
34
|
.filter(m => m.size > 0)
|
|
35
|
+
.filter(m => m.get(FORWARDED_KEY_FOR) !== undefined)
|
|
53
36
|
}
|
|
54
37
|
|
|
55
38
|
/**
|
|
@@ -58,6 +41,8 @@ export class Forwarded {
|
|
|
58
41
|
* @returns {Map<string, string>|undefined}
|
|
59
42
|
*/
|
|
60
43
|
static selectRightMost(forwardedList, skipList = []) {
|
|
44
|
+
if(forwardedList === undefined) { return undefined }
|
|
45
|
+
|
|
61
46
|
const iter = skipList[Symbol.iterator]()
|
|
62
47
|
|
|
63
48
|
for(const forwarded of forwardedList.toReversed()) {
|
|
@@ -71,39 +56,6 @@ export class Forwarded {
|
|
|
71
56
|
}
|
|
72
57
|
}
|
|
73
58
|
|
|
74
|
-
|
|
75
|
-
/*
|
|
76
|
-
const examples = [
|
|
77
|
-
{ f: [], s: [], ef: undefined },
|
|
78
|
-
|
|
79
|
-
{ f: [], s: [ '1.1.1.1' ] , ef: undefined },
|
|
80
|
-
{ f: [], s: [ '*' ] , ef: undefined },
|
|
81
|
-
|
|
82
|
-
{ f: [ { for: '1.1.1.1' } ], s: [], ef: '1.1.1.1' },
|
|
83
|
-
{ f: [ { for: '1.1.1.1' } ], s: [ '*' ], ef: undefined },
|
|
84
|
-
{ f: [ { for: '1.1.1.1' }, { for: '2.2.2.2' } ], s: [], ef: '2.2.2.2' },
|
|
85
|
-
{ f: [ { for: '1.1.1.1' }, { for: '2.2.2.2' } ], s: [ '2.2.2.2' ], ef: '1.1.1.1' },
|
|
86
|
-
{ f: [ { for: '1.1.1.1' }, { for: '2.2.2.2' }, { for: '3.3.3.3' } ], s: [ '3.3.3.3', '2.2.2.2' ], ef: '1.1.1.1' },
|
|
87
|
-
{ f: [ { for: '1.1.1.1' }, { for: '2.2.2.2' }, { for: '3.3.3.3' } ], s: [ '3.3.3.3', '*' ], ef: '1.1.1.1' },
|
|
88
|
-
{ f: [ { for: '1.1.1.1' }, { for: '2.2.2.2' }, { for: '3.3.3.3' } ], s: [ '*', '*' ], ef: '1.1.1.1' },
|
|
89
|
-
{ f: [ { for: '1.1.1.1' }, { for: '2.2.2.2' }, { for: '3.3.3.3' } ], s: [ '*', '*', '*' ], ef: undefined },
|
|
90
|
-
|
|
91
|
-
{ f: [ { for: '1.1.1.1' } ], s: [ '*', '*' ], ef: undefined },
|
|
92
|
-
|
|
93
|
-
{ f: [ { for: '1.1.1.1' }, { for: '2.2.2.2' }, { for: '3.3.3.3' } ], s: [ '3.3.3.3'], ef: '2.2.2.2' },
|
|
94
|
-
{ f: [ { for: '1.1.1.1' }, { for: '2.2.2.2' }, { for: '3.3.3.3' } ], s: [ '*'], ef: '2.2.2.2' },
|
|
95
|
-
]
|
|
96
|
-
|
|
97
|
-
for(const { f, s, ef } of examples) {
|
|
98
|
-
const result = Forwarded.selectRightMost(f.map(i => new Map(Object.entries(i))), s)
|
|
99
|
-
const resultFor = result?.get('for')
|
|
100
|
-
if(resultFor !== ef) {
|
|
101
|
-
console.log(`mismatch ${ef} !== ${resultFor}`)
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
*/
|
|
105
|
-
|
|
106
|
-
|
|
107
59
|
/*
|
|
108
60
|
const examples = [
|
|
109
61
|
null,
|
|
@@ -3,18 +3,20 @@
|
|
|
3
3
|
export * from './accept.js'
|
|
4
4
|
export * from './accept-encoding.js'
|
|
5
5
|
export * from './accept-language.js'
|
|
6
|
-
export * from './accept-util.js'
|
|
7
6
|
export * from './cache-control.js'
|
|
8
7
|
export * from './clear-site-data.js'
|
|
8
|
+
export * from './client-hints.js'
|
|
9
9
|
export * from './conditional.js'
|
|
10
10
|
export * from './content-disposition.js'
|
|
11
11
|
export * from './content-range.js'
|
|
12
12
|
export * from './content-type.js'
|
|
13
|
-
export * from './forwarded.js'
|
|
14
13
|
export * from './fetch-metadata.js'
|
|
14
|
+
export * from './forwarded.js'
|
|
15
|
+
export * from './link.js'
|
|
15
16
|
export * from './multipart.js'
|
|
16
17
|
export * from './preference.js'
|
|
17
18
|
export * from './range.js'
|
|
18
19
|
export * from './rate-limit.js'
|
|
19
20
|
export * from './server-timing.js'
|
|
21
|
+
export * from './strict-transport-security.js'
|
|
20
22
|
export * from './www-authenticate.js'
|