@johntalton/http-util 3.0.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@johntalton/http-util",
3
- "version": "3.0.0",
3
+ "version": "4.0.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
package/src/body.js CHANGED
@@ -13,7 +13,7 @@ export const DEFAULT_BYTE_LIMIT = 1024 * 1024 //
13
13
 
14
14
  /**
15
15
  * @typedef {Object} BodyOptions
16
- * @property {AbortSignal} [signal]
16
+ * @property {AbortSignal|undefined} [signal]
17
17
  * @property {number} [byteLimit]
18
18
  * @property {number} [contentLength]
19
19
  * @property {ContentType|undefined} [contentType]
@@ -0,0 +1,57 @@
1
+ /** @typedef {'no-cache'|'no-store'|'no-transform'|'must-revalidate'|'immutable'|'must-understand'} Directives */
2
+
3
+ /**
4
+ * @typedef {Object} CacheControlOptions
5
+ * @property {boolean|undefined} [priv]
6
+ * @property {boolean|undefined} [pub]
7
+ * @property {number|undefined} [maxAge]
8
+ * @property {number|undefined} [staleWhileRevalidate]
9
+ * @property {number|undefined} [staleIfError]
10
+ * @property {Directives|Array<Directives>|undefined} [directives]
11
+ */
12
+
13
+ export class CacheControl {
14
+ /**
15
+ * @param {CacheControlOptions} options
16
+ * @returns {string|undefined}
17
+ */
18
+ static encode(options) {
19
+ const {
20
+ pub,
21
+ priv,
22
+ maxAge,
23
+ directives,
24
+ staleWhileRevalidate,
25
+ staleIfError
26
+ } = options
27
+
28
+ const result = []
29
+
30
+ if(pub !== undefined && pub && !priv) { result.push('public') }
31
+ if(priv !== undefined && priv && !pub) { result.push('private') }
32
+
33
+ if(directives !== undefined) {
34
+ result.push(...(Array.isArray(directives) ? directives : [ directives ]))
35
+ }
36
+
37
+ if(maxAge !== undefined && Number.isInteger(maxAge) && maxAge >= 0) {
38
+ result.push(`max-age=${maxAge}`)
39
+ }
40
+
41
+ if(staleWhileRevalidate !== undefined && Number.isInteger(staleWhileRevalidate) && staleWhileRevalidate >= 0) {
42
+ result.push(`stale-while-revalidate=${staleWhileRevalidate}`)
43
+ }
44
+
45
+ if(staleIfError !== undefined && Number.isInteger(staleIfError) && staleIfError >= 0) {
46
+ result.push(`stale-if-error=${staleIfError}`)
47
+ }
48
+
49
+ return result.join(', ')
50
+ }
51
+ }
52
+
53
+ // console.log(CacheControl.encode({
54
+ // priv: true,
55
+ // maxAge: 60,
56
+ // directives: ['must-revalidate', 'no-transform']
57
+ // }))
@@ -31,7 +31,7 @@
31
31
  * @property {number} hour
32
32
  * @property {number} minute
33
33
  * @property {number} second
34
- * @property {Date} date
34
+ * @property {Date|undefined} [date]
35
35
  */
36
36
 
37
37
  export const CONDITION_ETAG_SEPARATOR = ','
@@ -79,6 +79,31 @@ export function isQuoted(etag) {
79
79
  return true
80
80
  }
81
81
 
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
+ }
91
+
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
+ }
106
+
82
107
  export class Conditional {
83
108
  /**
84
109
  * @param {EtagItem|undefined} etagItem
@@ -143,6 +168,34 @@ export class Conditional {
143
168
  .filter(item => item !== undefined)
144
169
  }
145
170
 
171
+ /**
172
+ * @param {Array<EtagItem>} etagItemList
173
+ */
174
+ static hasAny(etagItemList) {
175
+ return etagItemList.find(item => item.any) !== undefined
176
+ }
177
+
178
+ /**
179
+ * @param {Array<EtagItem>} etagItemList
180
+ * @param {string} etag
181
+ */
182
+ static hasEtag(etagItemList, etag) {
183
+ return etagItemList.find(item => item.etag === etag) !== undefined
184
+ }
185
+
186
+ /**
187
+ * @param {IMFFixDate|undefined} fixDate
188
+ * @returns {string|undefined}
189
+ */
190
+ static encodeFixDate(fixDate) {
191
+ if(fixDate === undefined) { return undefined }
192
+ if(fixDate.date !== undefined) { return fixDate.date.toUTCString() }
193
+
194
+ const { year, month, day, hour, minute, second } = fixDate
195
+ const d = new Date(Date.UTC(year, DATE_MONTHS.indexOf(month), day, hour, minute, second))
196
+ return d.toUTCString()
197
+ }
198
+
146
199
  /**
147
200
  * @param {String|string|undefined} matchHeader
148
201
  * @returns {IMFFixDate|undefined}
@@ -24,6 +24,8 @@ const {
24
24
  export function coreHeaders(status, contentType, meta) {
25
25
  return {
26
26
  [HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN]: meta.origin,
27
+ // Access-Control-Expose-Headers
28
+ // Access-Control-Allow-Credentials // for non-preflight
27
29
  [HTTP2_HEADER_STATUS]: status,
28
30
  [HTTP2_HEADER_CONTENT_TYPE]: contentType,
29
31
  [HTTP2_HEADER_SERVER]: meta.servername
@@ -11,10 +11,12 @@ import {
11
11
  } from '../content-type.js'
12
12
  import { send } from './send-util.js'
13
13
  import { Conditional } from '../conditional.js'
14
+ import { CacheControl } from '../cache-control.js'
14
15
 
15
16
  /** @import { ServerHttp2Stream } from 'node:http2' */
16
17
  /** @import { Metadata } from './defs.js' */
17
18
  /** @import { EtagItem } from '../conditional.js' */
19
+ /** @import { CacheControlOptions } from '../cache-control.js' */
18
20
 
19
21
  /** @typedef { (data: string, charset: BufferEncoding) => Buffer } EncoderFun */
20
22
 
@@ -22,7 +24,8 @@ const {
22
24
  HTTP2_HEADER_CONTENT_ENCODING,
23
25
  HTTP2_HEADER_VARY,
24
26
  HTTP2_HEADER_CACHE_CONTROL,
25
- HTTP2_HEADER_ETAG
27
+ HTTP2_HEADER_ETAG,
28
+ HTTP2_HEADER_AGE
26
29
  } = http2.constants
27
30
 
28
31
  const { HTTP_STATUS_OK } = http2.constants
@@ -40,9 +43,11 @@ export const ENCODER_MAP = new Map([
40
43
  * @param {Object} obj
41
44
  * @param {string|undefined} encoding
42
45
  * @param {EtagItem|undefined} etag
46
+ * @param {number|undefined} age
47
+ * @param {CacheControlOptions} cacheControl
43
48
  * @param {Metadata} meta
44
49
  */
45
- export function sendJSON_Encoded(stream, obj, encoding, etag, meta) {
50
+ export function sendJSON_Encoded(stream, obj, encoding, etag, age, cacheControl, meta) {
46
51
  if(stream.closed) { return }
47
52
 
48
53
  const json = JSON.stringify(obj)
@@ -63,8 +68,8 @@ export function sendJSON_Encoded(stream, obj, encoding, etag, meta) {
63
68
  send(stream, HTTP_STATUS_OK, {
64
69
  [HTTP2_HEADER_CONTENT_ENCODING]: actualEncoding,
65
70
  [HTTP2_HEADER_VARY]: 'Accept, Accept-Encoding',
66
- [HTTP2_HEADER_CACHE_CONTROL]: 'private',
67
- [HTTP2_HEADER_ETAG]: Conditional.encodeEtag(etag)
68
- // [HTTP2_HEADER_AGE]: age
71
+ [HTTP2_HEADER_CACHE_CONTROL]: CacheControl.encode(cacheControl),
72
+ [HTTP2_HEADER_ETAG]: Conditional.encodeEtag(etag),
73
+ [HTTP2_HEADER_AGE]: age !== undefined ? `${age}` : undefined
69
74
  }, CONTENT_TYPE_JSON, encodedData, meta)
70
75
  }
@@ -1,10 +1,12 @@
1
1
  import http2 from 'node:http2'
2
2
  import { send } from './send-util.js'
3
3
  import { Conditional } from '../conditional.js'
4
+ import { CacheControl } from '../cache-control.js'
4
5
 
5
6
  /** @import { ServerHttp2Stream } from 'node:http2' */
6
7
  /** @import { Metadata } from './defs.js' */
7
8
  /** @import { EtagItem } from '../conditional.js' */
9
+ /** @import { CacheControlOptions } from '../cache-control.js' */
8
10
 
9
11
  const {
10
12
  HTTP2_HEADER_AGE,
@@ -19,12 +21,13 @@ const { HTTP_STATUS_NOT_MODIFIED } = http2.constants
19
21
  * @param {ServerHttp2Stream} stream
20
22
  * @param {EtagItem|undefined} etag
21
23
  * @param {number|undefined} age
24
+ * @param {CacheControlOptions} cacheControl
22
25
  * @param {Metadata} meta
23
26
  */
24
- export function sendNotModified(stream, etag, age, meta) {
27
+ export function sendNotModified(stream, etag, age, cacheControl, meta) {
25
28
  send(stream, HTTP_STATUS_NOT_MODIFIED, {
26
29
  [HTTP2_HEADER_VARY]: 'Accept, Accept-Encoding',
27
- [HTTP2_HEADER_CACHE_CONTROL]: 'private',
30
+ [HTTP2_HEADER_CACHE_CONTROL]: CacheControl.encode(cacheControl),
28
31
  [HTTP2_HEADER_ETAG]: Conditional.encodeEtag(etag),
29
32
  [HTTP2_HEADER_AGE]: age !== undefined ? `${age}` : undefined
30
33
  }, undefined, undefined, meta)
@@ -11,7 +11,10 @@ import { send } from './send-util.js'
11
11
  const {
12
12
  HTTP2_HEADER_CONTENT_TYPE,
13
13
  HTTP2_HEADER_ACCESS_CONTROL_ALLOW_METHODS,
14
- HTTP2_HEADER_ACCESS_CONTROL_ALLOW_HEADERS
14
+ HTTP2_HEADER_ACCESS_CONTROL_ALLOW_HEADERS,
15
+ HTTP2_HEADER_IF_MATCH,
16
+ HTTP2_HEADER_IF_NONE_MATCH,
17
+ HTTP2_HEADER_AUTHORIZATION
15
18
  } = http2.constants
16
19
 
17
20
  const { HTTP_STATUS_OK } = http2.constants
@@ -24,7 +27,13 @@ const { HTTP_STATUS_OK } = http2.constants
24
27
  export function sendPreflight(stream, methods, meta) {
25
28
  send(stream, HTTP_STATUS_OK, {
26
29
  [HTTP2_HEADER_ACCESS_CONTROL_ALLOW_METHODS]: methods.join(','),
27
- [HTTP2_HEADER_ACCESS_CONTROL_ALLOW_HEADERS]: ['Authorization', HTTP2_HEADER_CONTENT_TYPE].join(','),
30
+ [HTTP2_HEADER_ACCESS_CONTROL_ALLOW_HEADERS]: [
31
+ HTTP2_HEADER_IF_MATCH,
32
+ HTTP2_HEADER_IF_NONE_MATCH,
33
+ HTTP2_HEADER_AUTHORIZATION,
34
+ HTTP2_HEADER_CONTENT_TYPE
35
+ ].join(','),
28
36
  [HTTP2_HEADER_ACCESS_CONTROL_MAX_AGE]: PREFLIGHT_AGE_SECONDS
37
+ // Access-Control-Allow-Credentials
29
38
  }, undefined, undefined, meta)
30
39
  }