@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.
Files changed (84) hide show
  1. package/README.md +54 -7
  2. package/package.json +26 -6
  3. package/src/body.js +8 -8
  4. package/src/{response/defs.js → defs.js} +35 -1
  5. package/src/{accept-encoding.js → headers/accept-encoding.js} +11 -14
  6. package/src/{accept-language.js → headers/accept-language.js} +14 -9
  7. package/src/headers/accept.js +86 -0
  8. package/src/{cache-control.js → headers/cache-control.js} +0 -6
  9. package/src/{clear-site-data.js → headers/clear-site-data.js} +4 -10
  10. package/src/headers/client-hints.js +88 -0
  11. package/src/{conditional.js → headers/conditional.js} +190 -117
  12. package/src/headers/content-disposition.js +44 -0
  13. package/src/{content-range.js → headers/content-range.js} +1 -18
  14. package/src/headers/content-type.js +101 -0
  15. package/src/{forwarded.js → headers/forwarded.js} +8 -56
  16. package/src/{index.js → headers/index.js} +4 -2
  17. package/src/headers/link.js +34 -0
  18. package/src/{multipart.js → headers/multipart.js} +22 -13
  19. package/src/{preference.js → headers/preference.js} +3 -58
  20. package/src/{range.js → headers/range.js} +4 -32
  21. package/src/{rate-limit.js → headers/rate-limit.js} +6 -1
  22. package/src/{server-timing.js → headers/server-timing.js} +3 -16
  23. package/src/headers/strict-transport-security.js +39 -0
  24. package/src/{accept-util.js → headers/util/accept-util.js} +8 -14
  25. package/src/headers/util/index.js +7 -0
  26. package/src/headers/util/kvp.js +79 -0
  27. package/src/headers/util/mime.js +77 -0
  28. package/src/headers/util/whitespace.js +8 -0
  29. package/src/{www-authenticate.js → headers/www-authenticate.js} +1 -1
  30. package/src/response/{accepted.js → 2xx/accepted.js} +2 -2
  31. package/src/response/2xx/bytes.js +62 -0
  32. package/src/response/2xx/created.js +49 -0
  33. package/src/response/2xx/json.js +60 -0
  34. package/src/response/2xx/no-content.js +45 -0
  35. package/src/response/2xx/partial-content.js +101 -0
  36. package/src/response/{preflight.js → 2xx/preflight.js} +29 -10
  37. package/src/response/{sse.js → 2xx/sse.js} +2 -2
  38. package/src/response/{trace.js → 2xx/trace.js} +3 -3
  39. package/src/response/3xx/found.js +23 -0
  40. package/src/response/{moved-permanently.js → 3xx/moved-permanently.js} +2 -2
  41. package/src/response/{multiple-choices.js → 3xx/multiple-choices.js} +2 -3
  42. package/src/response/3xx/not-modified.js +59 -0
  43. package/src/response/{permanent-redirect.js → 3xx/permanent-redirect.js} +2 -2
  44. package/src/response/{see-other.js → 3xx/see-other.js} +2 -2
  45. package/src/response/{temporary-redirect.js → 3xx/temporary-redirect.js} +2 -2
  46. package/src/response/4xx/bad-request.js +19 -0
  47. package/src/response/{conflict.js → 4xx/conflict.js} +2 -2
  48. package/src/response/{content-too-large.js → 4xx/content-too-large.js} +2 -2
  49. package/src/response/{forbidden.js → 4xx/forbidden.js} +3 -2
  50. package/src/response/{gone.js → 4xx/gone.js} +2 -2
  51. package/src/response/{im-a-teapot.js → 4xx/im-a-teapot.js} +2 -2
  52. package/src/response/{not-acceptable.js → 4xx/not-acceptable.js} +14 -3
  53. package/src/response/4xx/not-allowed.js +34 -0
  54. package/src/response/{not-found.js → 4xx/not-found.js} +3 -3
  55. package/src/response/4xx/payment-required.js +17 -0
  56. package/src/response/4xx/precondition-failed.js +45 -0
  57. package/src/response/{range-not-satisfiable.js → 4xx/range-not-satisfiable.js} +15 -4
  58. package/src/response/{timeout.js → 4xx/timeout.js} +2 -2
  59. package/src/response/{too-many-requests.js → 4xx/too-many-requests.js} +22 -5
  60. package/src/response/{unauthorized.js → 4xx/unauthorized.js} +5 -5
  61. package/src/response/{unprocessable.js → 4xx/unprocessable.js} +2 -2
  62. package/src/response/{unsupported-media.js → 4xx/unsupported-media.js} +21 -4
  63. package/src/response/{error.js → 5xx/error.js} +3 -3
  64. package/src/response/{insufficient-storage.js → 5xx/insufficient-storage.js} +2 -2
  65. package/src/response/{not-implemented.js → 5xx/not-implemented.js} +4 -4
  66. package/src/response/{unavailable.js → 5xx/unavailable.js} +16 -4
  67. package/src/response/header-util.js +2 -2
  68. package/src/response/index.js +39 -35
  69. package/src/response/response.js +40 -34
  70. package/src/response/send-util.js +32 -21
  71. package/src/accept.js +0 -122
  72. package/src/content-disposition.js +0 -57
  73. package/src/content-type.js +0 -148
  74. package/src/link.js +0 -35
  75. package/src/response/bytes.js +0 -27
  76. package/src/response/created.js +0 -28
  77. package/src/response/json.js +0 -28
  78. package/src/response/no-content.js +0 -25
  79. package/src/response/not-allowed.js +0 -23
  80. package/src/response/not-modified.js +0 -35
  81. package/src/response/partial-content.js +0 -71
  82. package/src/response/precondition-failed.js +0 -16
  83. /package/src/{fetch-metadata.js → headers/fetch-metadata.js} +0 -0
  84. /package/src/{quote.js → headers/util/quote.js} +0 -0
@@ -1,4 +1,6 @@
1
- import { isQuoted, stripQuotes } from './quote.js'
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} IMFFixDate
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
- // %x21 / %x23-7E and %x80-FF
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(!isValidEtag(etag)) { throw new Error('invalid etag format') }
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(!isValidEtag(etag)) { throw new Error('invalid etag format') }
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(!isValidEtag(etag)) { return undefined }
110
- if(etag === CONDITION_ETAG_ANY) { return undefined }
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(!isValidEtag(etagItem.etag)) { return undefined }
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>} etagItemList
291
+ * @param {Array<EtagItem>|undefined} etagItemList
153
292
  */
154
293
  static hasAny(etagItemList) {
155
- return etagItemList.find(item => item.any) !== undefined
294
+ return etagItemList?.find(item => item.any) !== undefined
156
295
  }
157
296
 
158
297
  /**
159
- * @param {Array<EtagItem>} etagItemList
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.find(item => item.etag === etag) !== undefined
302
+ return etagItemList?.find(item => item.etag === etag) !== undefined
164
303
  }
165
304
 
166
305
  /**
167
- * @param {IMFFixDate|undefined} fixDate
306
+ * @param {IMFFixDateInput|string|undefined} reference
168
307
  * @returns {string|undefined}
169
308
  */
170
- static encodeFixDate(fixDate) {
171
- if(fixDate === undefined) { return undefined }
172
- if(fixDate.date !== undefined) { return fixDate.date.toUTCString() }
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
- const { year, month, day, hour, minute, second } = fixDate
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
- date: new Date(Date.UTC(year, DATE_MONTHS.indexOf(month), day, hour, minute, second)),
254
- // temporal:
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 "./response/defs.js"
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 => new Map(single
34
- .trim()
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'