@johntalton/http-util 2.0.5 → 3.0.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@johntalton/http-util",
3
- "version": "2.0.5",
3
+ "version": "3.0.1",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,8 +1,47 @@
1
+
2
+ /**
3
+ * @typedef {Object} WeakEtagItem
4
+ * @property {true} weak
5
+ * @property {false} any
6
+ * @property {string} etag
7
+ */
8
+
9
+ /**
10
+ * @typedef {Object} AnyEtagItem
11
+ * @property {boolean} weak
12
+ * @property {true} any
13
+ * @property {'*'} etag
14
+ */
15
+
16
+ /**
17
+ * @typedef {Object} NotWeakEtagItem
18
+ * @property {false} weak
19
+ * @property {false} any
20
+ * @property {string} etag
21
+ */
22
+
23
+ /** @typedef {WeakEtagItem | NotWeakEtagItem | AnyEtagItem } EtagItem */
24
+
25
+ /**
26
+ * @typedef {Object} IMFFixDate
27
+ * @property {typeof DATE_DAYS[number]} dayName
28
+ * @property {number} day
29
+ * @property {typeof DATE_MONTHS[number]} month
30
+ * @property {number} year
31
+ * @property {number} hour
32
+ * @property {number} minute
33
+ * @property {number} second
34
+ * @property {Date} date
35
+ */
36
+
1
37
  export const CONDITION_ETAG_SEPARATOR = ','
2
38
  export const CONDITION_ETAG_ANY = '*'
3
39
  export const CONDITION_ETAG_WEAK_PREFIX = 'W/'
4
40
  export const ETAG_QUOTE = '"'
5
41
 
42
+ /** @type {AnyEtagItem} */
43
+ export const ANY_ETAG_ITEM = { any: true, weak: false, etag: CONDITION_ETAG_ANY }
44
+
6
45
  export const DATE_SPACE = ' '
7
46
  export const DATE_DAYS = [ 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun' ]
8
47
  export const DATE_MONTHS = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ]
@@ -40,26 +79,50 @@ export function isQuoted(etag) {
40
79
  return true
41
80
  }
42
81
 
43
- /**
44
- * @typedef {Object} EtagItem
45
- * @property {boolean} weak
46
- * @property {boolean} any
47
- * @property {string} etag
48
- */
82
+ export class ETag {
83
+ /**
84
+ * @param {string} etag
85
+ * @returns {WeakEtagItem}
86
+ */
87
+ static weak(etag) {
88
+ if(!isValidEtag(etag)) { throw new Error('invalid etag format') }
89
+ return { any: false, weak: true, etag }
90
+ }
49
91
 
50
- /**
51
- * @typedef {Object} IMFFixDate
52
- * @property {typeof DATE_DAYS[number]} dayName
53
- * @property {number} day
54
- * @property {typeof DATE_MONTHS[number]} month
55
- * @property {number} year
56
- * @property {number} hour
57
- * @property {number} minute
58
- * @property {number} second
59
- * @property {Date} date
60
- */
92
+ /**
93
+ * @param {string} etag
94
+ * @returns {NotWeakEtagItem}
95
+ */
96
+ static strong(etag) {
97
+ if(!isValidEtag(etag)) { throw new Error('invalid etag format') }
98
+ return { any: false, weak: false, etag }
99
+ }
100
+
101
+ /**
102
+ * @returns {AnyEtagItem}
103
+ */
104
+ static any() { return ANY_ETAG_ITEM }
105
+ }
61
106
 
62
107
  export class Conditional {
108
+ /**
109
+ * @param {EtagItem|undefined} etagItem
110
+ * @returns {string|undefined}
111
+ */
112
+ static encodeEtag(etagItem) {
113
+ if(etagItem === undefined) { return undefined }
114
+ if(etagItem.any) {
115
+ if(etagItem.etag !== CONDITION_ETAG_ANY) { return undefined }
116
+ return CONDITION_ETAG_ANY
117
+ }
118
+
119
+ if(etagItem.etag === CONDITION_ETAG_ANY) { return undefined }
120
+ if(!isValidEtag(etagItem.etag)) { return undefined }
121
+
122
+ const prefix = etagItem.weak ? CONDITION_ETAG_WEAK_PREFIX : ''
123
+ return `${prefix}${ETAG_QUOTE}${etagItem.etag}${ETAG_QUOTE}`
124
+ }
125
+
63
126
  /**
64
127
  * @param {string|undefined} matchHeader
65
128
  * @returns {Array<EtagItem>}
@@ -85,12 +148,7 @@ export class Conditional {
85
148
  }
86
149
  })
87
150
  .map(item => {
88
- if(item.etag === CONDITION_ETAG_ANY) {
89
- return {
90
- ...item,
91
- any: true
92
- }
93
- }
151
+ if(item.etag === CONDITION_ETAG_ANY) { return ANY_ETAG_ITEM }
94
152
 
95
153
  // validated quoted
96
154
  if(!isQuoted(item.etag)) { return undefined }
@@ -98,11 +156,14 @@ export class Conditional {
98
156
  if(!isValidEtag(etag)) { return undefined }
99
157
  if(etag === CONDITION_ETAG_ANY) { return undefined }
100
158
 
101
- return {
159
+ /** @type {WeakEtagItem | NotWeakEtagItem} */
160
+ const result = {
102
161
  weak: item.weak,
103
162
  any: false,
104
163
  etag
105
164
  }
165
+
166
+ return result
106
167
  })
107
168
  .filter(item => item !== undefined)
108
169
  }
@@ -187,6 +248,20 @@ export class Conditional {
187
248
  }
188
249
  }
189
250
 
251
+ // Ok
252
+ // console.log(Conditional.encodeEtag({ any: true, weak: false, etag: '*' }))
253
+ // console.log(Conditional.encodeEtag({ any: true, weak: true, etag: '*' }))
254
+ // console.log(Conditional.encodeEtag({ any: false, weak: false, etag: 'Foo' }))
255
+ // console.log(Conditional.encodeEtag({ any: false, weak: true, etag: 'WeakFoo' }))
256
+
257
+ // Error
258
+ // console.log(Conditional.encodeEtag(undefined))
259
+ // console.log(Conditional.encodeEtag({ any: true, weak: false, etag: 'NotAsterisk' }))
260
+ // console.log(Conditional.encodeEtag({ any: false, weak: false, etag: 'Foo\tBar' }))
261
+ // console.log(Conditional.encodeEtag({ any: false, weak: false, etag: 'Foo"Bar' }))
262
+ // console.log(Conditional.encodeEtag({ any: false, weak: false, etag: '*' }))
263
+
264
+
190
265
  // Ok
191
266
  // console.log(Conditional.parseEtagList('"bfc13a64729c4290ef5b2c2730249c88ca92d82d"'))
192
267
  // console.log(Conditional.parseEtagList('W/"67ab43", "54ed21", "7892dd"'))
@@ -225,6 +300,8 @@ export class Conditional {
225
300
  // }
226
301
  // }
227
302
 
303
+
304
+
228
305
  // const testBad = [
229
306
  // undefined,
230
307
  // null,
@@ -1,11 +1,14 @@
1
1
  import http2 from 'node:http2'
2
2
  import { send } from './send-util.js'
3
+ import { Conditional } from '../conditional.js'
3
4
 
4
5
  /** @import { ServerHttp2Stream } from 'node:http2' */
5
6
  /** @import { Metadata } from './defs.js' */
7
+ /** @import { EtagItem } from '../conditional.js' */
6
8
 
7
9
  const {
8
- HTTP2_HEADER_LOCATION
10
+ HTTP2_HEADER_LOCATION,
11
+ HTTP2_HEADER_ETAG
9
12
  } = http2.constants
10
13
 
11
14
  const { HTTP_STATUS_CREATED } = http2.constants
@@ -13,10 +16,12 @@ const { HTTP_STATUS_CREATED } = http2.constants
13
16
  /**
14
17
  * @param {ServerHttp2Stream} stream
15
18
  * @param {URL} location
19
+ * @param {EtagItem|undefined} etag
16
20
  * @param {Metadata} meta
17
21
  */
18
- export function sendCreated(stream, location, meta) {
22
+ export function sendCreated(stream, location, etag, meta) {
19
23
  send(stream, HTTP_STATUS_CREATED, {
20
- [HTTP2_HEADER_LOCATION]: location.href
24
+ [HTTP2_HEADER_LOCATION]: location.href,
25
+ [HTTP2_HEADER_ETAG]: Conditional.encodeEtag(etag)
21
26
  }, undefined, undefined, meta)
22
27
  }
@@ -23,7 +23,6 @@ export const PREFLIGHT_AGE_SECONDS = '500'
23
23
  * @property {Array<TimingsInfo>} performance
24
24
  * @property {string|undefined} servername
25
25
  * @property {string|undefined} origin
26
- * @property {string|undefined} [etag]
27
26
  */
28
27
 
29
28
  /**
@@ -10,9 +10,11 @@ import {
10
10
  CONTENT_TYPE_JSON
11
11
  } from '../content-type.js'
12
12
  import { send } from './send-util.js'
13
+ import { Conditional } from '../conditional.js'
13
14
 
14
15
  /** @import { ServerHttp2Stream } from 'node:http2' */
15
16
  /** @import { Metadata } from './defs.js' */
17
+ /** @import { EtagItem } from '../conditional.js' */
16
18
 
17
19
  /** @typedef { (data: string, charset: BufferEncoding) => Buffer } EncoderFun */
18
20
 
@@ -37,9 +39,10 @@ export const ENCODER_MAP = new Map([
37
39
  * @param {ServerHttp2Stream} stream
38
40
  * @param {Object} obj
39
41
  * @param {string|undefined} encoding
42
+ * @param {EtagItem|undefined} etag
40
43
  * @param {Metadata} meta
41
44
  */
42
- export function sendJSON_Encoded(stream, obj, encoding, meta) {
45
+ export function sendJSON_Encoded(stream, obj, encoding, etag, meta) {
43
46
  if(stream.closed) { return }
44
47
 
45
48
  const json = JSON.stringify(obj)
@@ -61,7 +64,7 @@ export function sendJSON_Encoded(stream, obj, encoding, meta) {
61
64
  [HTTP2_HEADER_CONTENT_ENCODING]: actualEncoding,
62
65
  [HTTP2_HEADER_VARY]: 'Accept, Accept-Encoding',
63
66
  [HTTP2_HEADER_CACHE_CONTROL]: 'private',
64
- [HTTP2_HEADER_ETAG]: `"${meta.etag}"`
67
+ [HTTP2_HEADER_ETAG]: Conditional.encodeEtag(etag)
65
68
  // [HTTP2_HEADER_AGE]: age
66
69
  }, CONTENT_TYPE_JSON, encodedData, meta)
67
70
  }
@@ -1,8 +1,10 @@
1
1
  import http2 from 'node:http2'
2
2
  import { send } from './send-util.js'
3
+ import { Conditional } from '../conditional.js'
3
4
 
4
5
  /** @import { ServerHttp2Stream } from 'node:http2' */
5
6
  /** @import { Metadata } from './defs.js' */
7
+ /** @import { EtagItem } from '../conditional.js' */
6
8
 
7
9
  const {
8
10
  HTTP2_HEADER_ETAG
@@ -12,10 +14,11 @@ const { HTTP_STATUS_NO_CONTENT } = http2.constants
12
14
 
13
15
  /**
14
16
  * @param {ServerHttp2Stream} stream
17
+ * @param {EtagItem|undefined} etag
15
18
  * @param {Metadata} meta
16
19
  */
17
- export function sendNoContent(stream, meta) {
20
+ export function sendNoContent(stream, etag, meta) {
18
21
  send(stream, HTTP_STATUS_NO_CONTENT, {
19
- [HTTP2_HEADER_ETAG]: `"${meta.etag}"`
22
+ [HTTP2_HEADER_ETAG]: Conditional.encodeEtag(etag)
20
23
  }, undefined, undefined, meta)
21
24
  }
@@ -1,8 +1,10 @@
1
1
  import http2 from 'node:http2'
2
2
  import { send } from './send-util.js'
3
+ import { Conditional } from '../conditional.js'
3
4
 
4
5
  /** @import { ServerHttp2Stream } from 'node:http2' */
5
6
  /** @import { Metadata } from './defs.js' */
7
+ /** @import { EtagItem } from '../conditional.js' */
6
8
 
7
9
  const {
8
10
  HTTP2_HEADER_AGE,
@@ -15,14 +17,15 @@ const { HTTP_STATUS_NOT_MODIFIED } = http2.constants
15
17
 
16
18
  /**
17
19
  * @param {ServerHttp2Stream} stream
18
- * @param {number} age
20
+ * @param {EtagItem|undefined} etag
21
+ * @param {number|undefined} age
19
22
  * @param {Metadata} meta
20
23
  */
21
- export function sendNotModified(stream, age, meta) {
24
+ export function sendNotModified(stream, etag, age, meta) {
22
25
  send(stream, HTTP_STATUS_NOT_MODIFIED, {
23
26
  [HTTP2_HEADER_VARY]: 'Accept, Accept-Encoding',
24
27
  [HTTP2_HEADER_CACHE_CONTROL]: 'private',
25
- [HTTP2_HEADER_ETAG]: `"${meta.etag}"`,
26
- [HTTP2_HEADER_AGE]: `${age}`
28
+ [HTTP2_HEADER_ETAG]: Conditional.encodeEtag(etag),
29
+ [HTTP2_HEADER_AGE]: age !== undefined ? `${age}` : undefined
27
30
  }, undefined, undefined, meta)
28
31
  }
@@ -10,6 +10,7 @@ import { send } from './send-util.js'
10
10
 
11
11
  /** @import { ServerHttp2Stream } from 'node:http2' */
12
12
  /** @import { Metadata } from './defs.js' */
13
+ /** @import { RateLimitInfo, RateLimitPolicyInfo } from '../rate-limit.js' */
13
14
 
14
15
  const {
15
16
  HTTP2_HEADER_RETRY_AFTER
@@ -19,17 +20,17 @@ const { HTTP_STATUS_TOO_MANY_REQUESTS } = http2.constants
19
20
 
20
21
  /**
21
22
  * @param {ServerHttp2Stream} stream
22
- * @param {*} limitInfo
23
- * @param {Array<any>} policies
23
+ * @param {RateLimitInfo} limitInfo
24
+ * @param {Array<RateLimitPolicyInfo>} policies
24
25
  * @param {Metadata} meta
25
26
  */
26
27
  export function sendTooManyRequests(stream, limitInfo, policies, meta) {
27
28
  send(stream, HTTP_STATUS_TOO_MANY_REQUESTS, {
28
- [HTTP2_HEADER_RETRY_AFTER]: limitInfo.retryAfterS,
29
+ [HTTP2_HEADER_RETRY_AFTER]: `${limitInfo.resetSeconds}`,
29
30
  [HTTP_HEADER_RATE_LIMIT]: RateLimit.from(limitInfo),
30
31
  [HTTP_HEADER_RATE_LIMIT_POLICY]: RateLimitPolicy.from(...policies)
31
32
  },
32
33
  CONTENT_TYPE_TEXT,
33
- `Retry After ${limitInfo.retryAfterS} Seconds`,
34
+ `Retry After ${limitInfo.resetSeconds} Seconds`,
34
35
  meta)
35
36
  }