@johntalton/http-util 4.1.1 → 5.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/README.md +232 -21
- package/package.json +1 -1
- package/src/accept-encoding.js +1 -1
- package/src/accept-language.js +1 -1
- package/src/accept-util.js +1 -1
- package/src/accept.js +2 -5
- package/src/body.js +13 -15
- package/src/cache-control.js +6 -1
- package/src/clear-site-data.js +59 -0
- package/src/conditional.js +37 -42
- package/src/content-disposition.js +12 -7
- package/src/content-range.js +59 -0
- package/src/content-type.js +17 -14
- package/src/forwarded.js +1 -1
- package/src/index.js +6 -1
- package/src/multipart.js +81 -33
- package/src/preference.js +205 -0
- package/src/range.js +154 -0
- package/src/response/accepted.js +1 -0
- package/src/response/bytes.js +27 -0
- package/src/response/conflict.js +1 -0
- package/src/response/content-too-large.js +16 -0
- package/src/response/created.js +2 -1
- package/src/response/defs.js +15 -1
- package/src/response/error.js +1 -0
- package/src/response/forbidden.js +17 -0
- package/src/response/gone.js +16 -0
- package/src/response/header-util.js +2 -1
- package/src/response/im-a-teapot.js +16 -0
- package/src/response/index.js +17 -0
- package/src/response/insufficient-storage.js +16 -0
- package/src/response/json.js +6 -53
- package/src/response/moved-permanently.js +23 -0
- package/src/response/multiple-choices.js +21 -0
- package/src/response/no-content.js +2 -1
- package/src/response/not-acceptable.js +2 -1
- package/src/response/not-allowed.js +1 -0
- package/src/response/not-found.js +1 -0
- package/src/response/not-implemented.js +1 -0
- package/src/response/not-modified.js +3 -2
- package/src/response/partial-content.js +71 -0
- package/src/response/permanent-redirect.js +23 -0
- package/src/response/precondition-failed.js +1 -0
- package/src/response/preflight.js +21 -5
- package/src/response/range-not-satisfiable.js +28 -0
- package/src/response/response.js +26 -0
- package/src/response/see-other.js +23 -0
- package/src/response/send-util.js +137 -6
- package/src/response/sse.js +4 -3
- package/src/response/temporary-redirect.js +23 -0
- package/src/response/timeout.js +1 -0
- package/src/response/too-many-requests.js +1 -0
- package/src/response/trace.js +8 -4
- package/src/response/unauthorized.js +1 -0
- package/src/response/unavailable.js +1 -0
- package/src/response/unprocessable.js +1 -0
- package/src/response/unsupported-media.js +15 -4
package/README.md
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
Set of utilities to aid in building from-scratch [node:http2](https://nodejs.org/docs/latest/api/http2.html) stream compatible services.
|
|
4
4
|
|
|
5
|
-
- Header parsers
|
|
6
|
-
- Stream Response methods
|
|
7
|
-
- Body parser
|
|
5
|
+
- [Header parsers](#header-parsers)
|
|
6
|
+
- [Stream Response methods](#response)
|
|
7
|
+
- [Body parser](#body)
|
|
8
8
|
|
|
9
9
|
## Header Parsers
|
|
10
10
|
### From Client:
|
|
@@ -44,24 +44,37 @@ const bestMatchingType = Accept.select(acceptHeader, supportedType)
|
|
|
44
44
|
|
|
45
45
|
All responders take in a `stream` as well as a metadata object to hint on servername and origin strings etc.
|
|
46
46
|
|
|
47
|
-
- `sendAccepted`
|
|
48
|
-
- `sendConflict`
|
|
49
|
-
- `
|
|
50
|
-
- `
|
|
51
|
-
- `
|
|
52
|
-
- `
|
|
53
|
-
- `
|
|
54
|
-
- `
|
|
55
|
-
- `
|
|
56
|
-
- `
|
|
57
|
-
- `
|
|
58
|
-
- `
|
|
59
|
-
- `
|
|
60
|
-
- `
|
|
61
|
-
- `
|
|
62
|
-
- `
|
|
63
|
-
- `
|
|
64
|
-
- `
|
|
47
|
+
- [`sendAccepted`](#responseaccepted)
|
|
48
|
+
- [`sendConflict`](#responseconflict)
|
|
49
|
+
- [`sendContentTooLarge`](#)
|
|
50
|
+
- [`sendCreated`](#responsecreated)
|
|
51
|
+
- [`sendError`](#responseerror) - 500
|
|
52
|
+
- [`sendGone`](#)
|
|
53
|
+
- [`sendImATeapot`](#)
|
|
54
|
+
- [`sendInsufficientStorage`](#)
|
|
55
|
+
- [`sendJSON_Encoded`](#responsejson) - Standard Ok response with encoding
|
|
56
|
+
- [`sendMovedPermanently`](#)
|
|
57
|
+
- [`sendMultipleChoices`](#)
|
|
58
|
+
- [`sendNoContent`](#responsenocontent)
|
|
59
|
+
- [`sendNotAcceptable`](#responsenotacceptable)
|
|
60
|
+
- [`sendNotAllowed`](#responsenotallowed) - Method not supported / allowed
|
|
61
|
+
- [`sendNotFound`](#responsenotfound) - 404
|
|
62
|
+
- [`sendNotImplemented`](#responsenotimplemented)
|
|
63
|
+
- [`sendNotModified`](#responsenotmodified)
|
|
64
|
+
- [`sendPartialContent`](#)
|
|
65
|
+
- [`sendPreconditionFailed`](#responsepreconditionfailed)
|
|
66
|
+
- [`sendPreflight`](#responsepreflight) - Response to OPTIONS with CORS headers
|
|
67
|
+
- [`sendRangeNotSatisfiable`](#)
|
|
68
|
+
- [`sendSeeOther`](#)
|
|
69
|
+
- [`sendTemporaryRedirect`](#)
|
|
70
|
+
- [`sendTimeout`](#responsetimeout)
|
|
71
|
+
- [`sendTooManyRequests`](#responsetoomanyrequests) - Rate limit response (429)
|
|
72
|
+
- [`sendTrace`](#responsetrace)
|
|
73
|
+
- [`sendUnauthorized`](#responseunauthorized) - Unauthorized
|
|
74
|
+
- [`sendUnavailable`](#responseunavailable)
|
|
75
|
+
- [`sendUnprocessable`](#responseunprocessable)
|
|
76
|
+
- [`sendUnsupportedMediaType`](#responseunsupportedmediatype)
|
|
77
|
+
- [`sendSSE`](#responsesse) - SSE header (leave the `stream` open)
|
|
65
78
|
|
|
66
79
|
Responses allow for optional CORS headers as well as Server Timing meta data.
|
|
67
80
|
|
|
@@ -102,3 +115,201 @@ const futureBody = requestBody(stream, {
|
|
|
102
115
|
const body = futureBody.json()
|
|
103
116
|
|
|
104
117
|
```
|
|
118
|
+
|
|
119
|
+
## Response method API
|
|
120
|
+
|
|
121
|
+
All response methods take in the `stream` and a `meta` property.
|
|
122
|
+
|
|
123
|
+
Additional parameters are specific to each return type.
|
|
124
|
+
|
|
125
|
+
Each return type semantic may also expose header (in addition to the standard headers) to the calling code as needed.
|
|
126
|
+
|
|
127
|
+
Some common objects can be passed such as `EtagItem`, `CacheControlOptions` and `Metadata` have their own structure.
|
|
128
|
+
|
|
129
|
+
Many parameters accept `undefined` to skip/ignore the usage
|
|
130
|
+
|
|
131
|
+
### Response.accepted
|
|
132
|
+
|
|
133
|
+
Parameters:
|
|
134
|
+
- stream
|
|
135
|
+
- meta
|
|
136
|
+
|
|
137
|
+
### Response.conflict
|
|
138
|
+
|
|
139
|
+
Parameters:
|
|
140
|
+
- stream
|
|
141
|
+
- meta
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
### Response.created
|
|
145
|
+
|
|
146
|
+
Parameters:
|
|
147
|
+
- stream
|
|
148
|
+
- location
|
|
149
|
+
- etag
|
|
150
|
+
- meta
|
|
151
|
+
|
|
152
|
+
Additional Exposed Headers:
|
|
153
|
+
- location
|
|
154
|
+
|
|
155
|
+
### Response.error
|
|
156
|
+
|
|
157
|
+
Parameters:
|
|
158
|
+
- stream
|
|
159
|
+
- meta
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
### Response.json
|
|
163
|
+
|
|
164
|
+
Parameters:
|
|
165
|
+
- stream
|
|
166
|
+
- object
|
|
167
|
+
- encoding - undefined | 'identity' | 'br' | 'gzip' | 'deflate' | 'zstd'
|
|
168
|
+
- etag
|
|
169
|
+
- age
|
|
170
|
+
- cacheControl
|
|
171
|
+
- supportedQueryTypes - undefined | Array of supported query types
|
|
172
|
+
- meta
|
|
173
|
+
|
|
174
|
+
Additional Exposed Headers
|
|
175
|
+
- age
|
|
176
|
+
- accept-query
|
|
177
|
+
|
|
178
|
+
### Response.noContent
|
|
179
|
+
|
|
180
|
+
Parameters:
|
|
181
|
+
- stream
|
|
182
|
+
- etag
|
|
183
|
+
- meta
|
|
184
|
+
|
|
185
|
+
### Response.notAcceptable
|
|
186
|
+
|
|
187
|
+
Parameters:
|
|
188
|
+
- stream
|
|
189
|
+
- supportedTypes
|
|
190
|
+
- meta
|
|
191
|
+
|
|
192
|
+
### Response.notAllowed
|
|
193
|
+
|
|
194
|
+
Parameters:
|
|
195
|
+
- stream
|
|
196
|
+
- methods - Array of allowed methods
|
|
197
|
+
- meta
|
|
198
|
+
|
|
199
|
+
Additional Exposed Headers:
|
|
200
|
+
- allow
|
|
201
|
+
|
|
202
|
+
### Response.notFound
|
|
203
|
+
|
|
204
|
+
Parameters:
|
|
205
|
+
- stream
|
|
206
|
+
- message
|
|
207
|
+
- meta
|
|
208
|
+
|
|
209
|
+
### Response.notImplemented
|
|
210
|
+
|
|
211
|
+
Parameters:
|
|
212
|
+
- stream
|
|
213
|
+
- message
|
|
214
|
+
- meta
|
|
215
|
+
|
|
216
|
+
### Response.notModified
|
|
217
|
+
|
|
218
|
+
Parameters:
|
|
219
|
+
- stream
|
|
220
|
+
- etag
|
|
221
|
+
- age
|
|
222
|
+
- cacheControl
|
|
223
|
+
- meta
|
|
224
|
+
|
|
225
|
+
Additional Exposed Headers:
|
|
226
|
+
- age
|
|
227
|
+
|
|
228
|
+
### Response.preconditionFailed
|
|
229
|
+
|
|
230
|
+
Parameters:
|
|
231
|
+
- stream
|
|
232
|
+
- meta
|
|
233
|
+
|
|
234
|
+
### Response.preflight
|
|
235
|
+
|
|
236
|
+
Parameters:
|
|
237
|
+
- stream
|
|
238
|
+
- methods
|
|
239
|
+
- supportedQueryTypes - undefined | Array of supported types
|
|
240
|
+
- meta
|
|
241
|
+
|
|
242
|
+
Additional Exposed Headers:
|
|
243
|
+
- accept-query
|
|
244
|
+
|
|
245
|
+
### Response.sse
|
|
246
|
+
|
|
247
|
+
Parameters:
|
|
248
|
+
- stream
|
|
249
|
+
- meta
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
### Response.timeout
|
|
253
|
+
|
|
254
|
+
Parameters:
|
|
255
|
+
- stream
|
|
256
|
+
- meta
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
### Response.tooManyRequests
|
|
260
|
+
|
|
261
|
+
Parameters:
|
|
262
|
+
- stream
|
|
263
|
+
- limitInfo - `RateLimitInfo`
|
|
264
|
+
- policies - Array of `RateLimitPolicyInfo`
|
|
265
|
+
- meta
|
|
266
|
+
|
|
267
|
+
Additional Exposed Headers:
|
|
268
|
+
- retry-after
|
|
269
|
+
- rate-limit
|
|
270
|
+
- rate-limit-policy
|
|
271
|
+
|
|
272
|
+
### Response.trace
|
|
273
|
+
|
|
274
|
+
Parameters:
|
|
275
|
+
- stream
|
|
276
|
+
- method
|
|
277
|
+
- url
|
|
278
|
+
- headers
|
|
279
|
+
- meta
|
|
280
|
+
|
|
281
|
+
### Response.unauthorized
|
|
282
|
+
|
|
283
|
+
Parameters:
|
|
284
|
+
- stream
|
|
285
|
+
- meta
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
### Response.unavailable
|
|
289
|
+
|
|
290
|
+
Parameters:
|
|
291
|
+
- stream
|
|
292
|
+
- message
|
|
293
|
+
- retryAfter
|
|
294
|
+
- meta
|
|
295
|
+
|
|
296
|
+
Additional Exposed Headers:
|
|
297
|
+
- retry-after
|
|
298
|
+
|
|
299
|
+
### Response.unprocessable
|
|
300
|
+
|
|
301
|
+
Parameters:
|
|
302
|
+
- stream
|
|
303
|
+
- meta
|
|
304
|
+
|
|
305
|
+
### Response.unsupportedMediaType
|
|
306
|
+
|
|
307
|
+
Parameters:
|
|
308
|
+
- stream
|
|
309
|
+
- acceptableMediaTypes
|
|
310
|
+
- supportedQueryTypes
|
|
311
|
+
- meta
|
|
312
|
+
|
|
313
|
+
Additional Exposed Headers:
|
|
314
|
+
- accept-query
|
|
315
|
+
- accept-post
|
package/package.json
CHANGED
package/src/accept-encoding.js
CHANGED
|
@@ -21,7 +21,7 @@ export class AcceptEncoding {
|
|
|
21
21
|
*/
|
|
22
22
|
static select(acceptEncodingHeader, supportedTypes) {
|
|
23
23
|
const accepts = AcceptEncoding.parse(acceptEncodingHeader)
|
|
24
|
-
return
|
|
24
|
+
return AcceptEncoding.selectFrom(accepts, supportedTypes)
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
/**
|
package/src/accept-language.js
CHANGED
|
@@ -23,7 +23,7 @@ export class AcceptLanguage {
|
|
|
23
23
|
*/
|
|
24
24
|
static select(acceptLanguageHeader, supportedTypes) {
|
|
25
25
|
const accepts = AcceptLanguage.parse(acceptLanguageHeader)
|
|
26
|
-
return
|
|
26
|
+
return AcceptLanguage.selectFrom(accepts, supportedTypes)
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
/**
|
package/src/accept-util.js
CHANGED
|
@@ -39,7 +39,7 @@ export function parseAcceptStyleHeader(header, wellKnown) {
|
|
|
39
39
|
}))
|
|
40
40
|
|
|
41
41
|
if(!parameters.has(QUALITY)) { parameters.set(QUALITY, DEFAULT_QUALITY_STRING) }
|
|
42
|
-
const quality = parseFloat(parameters.get(QUALITY) ?? DEFAULT_QUALITY_STRING)
|
|
42
|
+
const quality = Number.parseFloat(parameters.get(QUALITY) ?? DEFAULT_QUALITY_STRING)
|
|
43
43
|
|
|
44
44
|
return {
|
|
45
45
|
name,
|
package/src/accept.js
CHANGED
|
@@ -54,7 +54,6 @@ export class Accept {
|
|
|
54
54
|
const qualityB = entryB.quality ?? 0
|
|
55
55
|
const qualityA = entryA.quality ?? 0
|
|
56
56
|
return qualityB - qualityA
|
|
57
|
-
// return entryB.quality - entryA.quality
|
|
58
57
|
})
|
|
59
58
|
}
|
|
60
59
|
|
|
@@ -64,7 +63,7 @@ export class Accept {
|
|
|
64
63
|
*/
|
|
65
64
|
static select(acceptHeader, supportedTypes) {
|
|
66
65
|
const accepts = Accept.parse(acceptHeader)
|
|
67
|
-
return
|
|
66
|
+
return Accept.selectFrom(accepts, supportedTypes)
|
|
68
67
|
}
|
|
69
68
|
|
|
70
69
|
/**
|
|
@@ -84,9 +83,7 @@ export class Accept {
|
|
|
84
83
|
quality
|
|
85
84
|
}
|
|
86
85
|
})
|
|
87
|
-
.filter(best =>
|
|
88
|
-
return best.supportedTypes.length > 0
|
|
89
|
-
})
|
|
86
|
+
.filter(best => best.supportedTypes.length > 0)
|
|
90
87
|
|
|
91
88
|
if(bests.length === 0) { return undefined }
|
|
92
89
|
const [ first ] = bests
|
package/src/body.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
CHARSET_UTF8,
|
|
3
|
+
MIME_TYPE_MULTIPART_FORM_DATA,
|
|
4
|
+
MIME_TYPE_URL_FORM_DATA
|
|
5
|
+
} from './content-type.js'
|
|
2
6
|
import { Multipart } from './multipart.js'
|
|
3
7
|
|
|
4
|
-
export const
|
|
8
|
+
export const BYTE_PER_K = 1024
|
|
9
|
+
export const DEFAULT_BYTE_LIMIT = BYTE_PER_K * BYTE_PER_K //
|
|
5
10
|
|
|
6
|
-
/**
|
|
7
|
-
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* @import { ContentType } from './content-type.js'
|
|
12
|
-
*/
|
|
11
|
+
/** @import { Readable } from 'node:stream' */
|
|
12
|
+
/** @import { ContentType } from './content-type.js' */
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* @typedef {Object} BodyOptions
|
|
@@ -32,7 +32,6 @@ export const DEFAULT_BYTE_LIMIT = 1024 * 1024 //
|
|
|
32
32
|
* @property { () => Promise<any> } json
|
|
33
33
|
*/
|
|
34
34
|
|
|
35
|
-
|
|
36
35
|
/**
|
|
37
36
|
* @param {Readable} stream
|
|
38
37
|
* @param {BodyOptions} [options]
|
|
@@ -45,7 +44,7 @@ export function requestBody(stream, options) {
|
|
|
45
44
|
const charset = options?.contentType?.charset ?? CHARSET_UTF8
|
|
46
45
|
const contentType = options?.contentType
|
|
47
46
|
|
|
48
|
-
const invalidContentLength = (contentLength === undefined || isNaN(contentLength))
|
|
47
|
+
const invalidContentLength = (contentLength === undefined || Number.isNaN(contentLength))
|
|
49
48
|
// if(contentLength > byteLimit) {
|
|
50
49
|
// console.log(contentLength, invalidContentLength)
|
|
51
50
|
// throw new Error('contentLength exceeds limit')
|
|
@@ -92,7 +91,6 @@ export function requestBody(stream, options) {
|
|
|
92
91
|
// return
|
|
93
92
|
}
|
|
94
93
|
|
|
95
|
-
|
|
96
94
|
const listener = () => {
|
|
97
95
|
stats.closed = true
|
|
98
96
|
controller.error(new Error('Abort Signal'))
|
|
@@ -281,7 +279,7 @@ export async function bodyJSON(reader, charset) {
|
|
|
281
279
|
* @param {ReadableStream} reader
|
|
282
280
|
* @param {ContentType} contentType
|
|
283
281
|
*/
|
|
284
|
-
async function _bodyFormData_Multipart(reader, contentType) {
|
|
282
|
+
export async function _bodyFormData_Multipart(reader, contentType) {
|
|
285
283
|
const MULTIPART_FORM_DATA_BOUNDARY_PARAMETER = 'boundary'
|
|
286
284
|
|
|
287
285
|
const text = await bodyText(reader, contentType.charset)
|
|
@@ -295,7 +293,7 @@ async function _bodyFormData_Multipart(reader, contentType) {
|
|
|
295
293
|
* @param {ReadableStream} reader
|
|
296
294
|
* @param {ContentType} contentType
|
|
297
295
|
*/
|
|
298
|
-
async function _bodyFormData_URL(reader, contentType) {
|
|
296
|
+
export async function _bodyFormData_URL(reader, contentType) {
|
|
299
297
|
const text = await bodyText(reader, contentType.charset)
|
|
300
298
|
const sp = new URLSearchParams(text)
|
|
301
299
|
const formData = new FormData()
|
|
@@ -323,4 +321,4 @@ export async function bodyFormData(reader, contentType) {
|
|
|
323
321
|
}
|
|
324
322
|
|
|
325
323
|
throw new TypeError('unknown mime type for form data')
|
|
326
|
-
}
|
|
324
|
+
}
|
package/src/cache-control.js
CHANGED
|
@@ -12,10 +12,12 @@
|
|
|
12
12
|
|
|
13
13
|
export class CacheControl {
|
|
14
14
|
/**
|
|
15
|
-
* @param {CacheControlOptions} options
|
|
15
|
+
* @param {CacheControlOptions|undefined} options
|
|
16
16
|
* @returns {string|undefined}
|
|
17
17
|
*/
|
|
18
18
|
static encode(options) {
|
|
19
|
+
if(options === undefined) { return undefined }
|
|
20
|
+
|
|
19
21
|
const {
|
|
20
22
|
pub,
|
|
21
23
|
priv,
|
|
@@ -46,6 +48,9 @@ export class CacheControl {
|
|
|
46
48
|
result.push(`stale-if-error=${staleIfError}`)
|
|
47
49
|
}
|
|
48
50
|
|
|
51
|
+
//
|
|
52
|
+
if(result.length === 0) { return undefined }
|
|
53
|
+
|
|
49
54
|
return result.join(', ')
|
|
50
55
|
}
|
|
51
56
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
|
|
2
|
+
/** @type {'*'} */
|
|
3
|
+
export const WILDCARD = '*'
|
|
4
|
+
|
|
5
|
+
export const CSD_DIRECTIVE_SEPARATOR = ','
|
|
6
|
+
export const CSD_QUOTE = '"'
|
|
7
|
+
|
|
8
|
+
export const CSD_DIRECTIVE_CACHE = 'cache'
|
|
9
|
+
export const CSD_DIRECTIVE_CLIENT_HINTS = 'clientHints'
|
|
10
|
+
export const CSD_DIRECTIVE_COOKIES = 'cookies'
|
|
11
|
+
export const CSD_DIRECTIVE_EXECUTION_CONTEXTS = 'executionContexts'
|
|
12
|
+
export const CSD_DIRECTIVE_PREFETCH_CACHE = 'prefetchCache'
|
|
13
|
+
export const CSD_DIRECTIVE_PRERENDER_CACHE = 'prerenderCache'
|
|
14
|
+
export const CSD_DIRECTIVE_STORAGE = 'storage'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @typedef {Object} ClearOptions
|
|
18
|
+
* @property {boolean} cache
|
|
19
|
+
* @property {boolean} clientHints
|
|
20
|
+
* @property {boolean} cookies
|
|
21
|
+
* @property {boolean} executionContext
|
|
22
|
+
* @property {boolean} prefetchCache
|
|
23
|
+
* @property {boolean} prerenderCache
|
|
24
|
+
* @property {boolean} storage
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
export class SiteData {
|
|
28
|
+
/**
|
|
29
|
+
* @param {Partial<ClearOptions>|true|'*'|undefined} directives
|
|
30
|
+
*/
|
|
31
|
+
static encode(directives) {
|
|
32
|
+
if(directives === undefined) { return undefined }
|
|
33
|
+
if(directives === true) { return WILDCARD }
|
|
34
|
+
if(directives === WILDCARD) { return WILDCARD }
|
|
35
|
+
|
|
36
|
+
const result = []
|
|
37
|
+
if(directives.cache === true) { result.push(CSD_DIRECTIVE_CACHE) }
|
|
38
|
+
if(directives.clientHints === true) { result.push(CSD_DIRECTIVE_CLIENT_HINTS) }
|
|
39
|
+
if(directives.cookies === true) { result.push(CSD_DIRECTIVE_COOKIES) }
|
|
40
|
+
if(directives.executionContext === true) { result.push(CSD_DIRECTIVE_EXECUTION_CONTEXTS) }
|
|
41
|
+
if(directives.prefetchCache === true) { result.push(CSD_DIRECTIVE_PREFETCH_CACHE) }
|
|
42
|
+
if(directives.prerenderCache === true) { result.push(CSD_DIRECTIVE_PRERENDER_CACHE) }
|
|
43
|
+
if(directives.storage === true) { result.push(CSD_DIRECTIVE_STORAGE) }
|
|
44
|
+
|
|
45
|
+
return result
|
|
46
|
+
.map(item => `${CSD_QUOTE}${item}${CSD_QUOTE}`)
|
|
47
|
+
.join(CSD_DIRECTIVE_SEPARATOR)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// console.log(SiteData.encode())
|
|
52
|
+
// console.log(SiteData.encode({}))
|
|
53
|
+
// console.log(SiteData.encode(false))
|
|
54
|
+
// console.log(SiteData.encode('true'))
|
|
55
|
+
// console.log(SiteData.encode(true))
|
|
56
|
+
// console.log(SiteData.encode('*'))
|
|
57
|
+
// console.log(SiteData.encode({ storage: false }))
|
|
58
|
+
// console.log(SiteData.encode({ storage: true }))
|
|
59
|
+
// console.log(SiteData.encode({ storage: true, cookies: true }))
|
package/src/conditional.js
CHANGED
|
@@ -49,6 +49,9 @@ export const DATE_SEPARATOR = ','
|
|
|
49
49
|
export const DATE_TIME_SEPARATOR = ':'
|
|
50
50
|
export const DATE_ZONE = 'GMT'
|
|
51
51
|
|
|
52
|
+
export const MINIMUM_YEAR = 1900
|
|
53
|
+
export const MAXIMUM_DAY = 31
|
|
54
|
+
|
|
52
55
|
/**
|
|
53
56
|
* @param {string} etag
|
|
54
57
|
*/
|
|
@@ -102,6 +105,31 @@ export class ETag {
|
|
|
102
105
|
* @returns {AnyEtagItem}
|
|
103
106
|
*/
|
|
104
107
|
static any() { return ANY_ETAG_ITEM }
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* @param {string|undefined} raw
|
|
111
|
+
* @returns {EtagItem|undefined}
|
|
112
|
+
*/
|
|
113
|
+
static parse(raw) {
|
|
114
|
+
if(raw === undefined) { return undefined }
|
|
115
|
+
|
|
116
|
+
const rawEtag = raw.trim()
|
|
117
|
+
const weak = rawEtag.startsWith(CONDITION_ETAG_WEAK_PREFIX)
|
|
118
|
+
const quotedEtag = weak ? rawEtag.substring(CONDITION_ETAG_WEAK_PREFIX.length) : rawEtag
|
|
119
|
+
|
|
120
|
+
if(quotedEtag === CONDITION_ETAG_ANY) { return ANY_ETAG_ITEM }
|
|
121
|
+
|
|
122
|
+
if(!isQuoted(quotedEtag)) { return undefined }
|
|
123
|
+
const etag = stripQuotes(quotedEtag)
|
|
124
|
+
if(!isValidEtag(etag)) { return undefined }
|
|
125
|
+
if(etag === CONDITION_ETAG_ANY) { return undefined }
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
weak,
|
|
129
|
+
any: false,
|
|
130
|
+
etag
|
|
131
|
+
}
|
|
132
|
+
}
|
|
105
133
|
}
|
|
106
134
|
|
|
107
135
|
export class Conditional {
|
|
@@ -131,40 +159,7 @@ export class Conditional {
|
|
|
131
159
|
if(matchHeader === undefined) { return [] }
|
|
132
160
|
|
|
133
161
|
return matchHeader.split(CONDITION_ETAG_SEPARATOR)
|
|
134
|
-
.map(
|
|
135
|
-
.map(etag => {
|
|
136
|
-
if(etag.startsWith(CONDITION_ETAG_WEAK_PREFIX)) {
|
|
137
|
-
// weak
|
|
138
|
-
return {
|
|
139
|
-
weak: true,
|
|
140
|
-
etag: etag.substring(CONDITION_ETAG_WEAK_PREFIX.length)
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// strong
|
|
145
|
-
return {
|
|
146
|
-
weak: false,
|
|
147
|
-
etag
|
|
148
|
-
}
|
|
149
|
-
})
|
|
150
|
-
.map(item => {
|
|
151
|
-
if(item.etag === CONDITION_ETAG_ANY) { return ANY_ETAG_ITEM }
|
|
152
|
-
|
|
153
|
-
// validated quoted
|
|
154
|
-
if(!isQuoted(item.etag)) { return undefined }
|
|
155
|
-
const etag = stripQuotes(item.etag)
|
|
156
|
-
if(!isValidEtag(etag)) { return undefined }
|
|
157
|
-
if(etag === CONDITION_ETAG_ANY) { return undefined }
|
|
158
|
-
|
|
159
|
-
/** @type {WeakEtagItem | NotWeakEtagItem} */
|
|
160
|
-
const result = {
|
|
161
|
-
weak: item.weak,
|
|
162
|
-
any: false,
|
|
163
|
-
etag
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
return result
|
|
167
|
-
})
|
|
162
|
+
.map(ETag.parse)
|
|
168
163
|
.filter(item => item !== undefined)
|
|
169
164
|
}
|
|
170
165
|
|
|
@@ -209,7 +204,7 @@ export class Conditional {
|
|
|
209
204
|
// <day-name>, <day> <month> <year> <hour>:<minute>:<second> GMT
|
|
210
205
|
// day-name "," SP date1 SP time-of-day SP GMT
|
|
211
206
|
|
|
212
|
-
if(matchHeader.length
|
|
207
|
+
if(matchHeader.length !== 29) { return undefined }
|
|
213
208
|
|
|
214
209
|
//
|
|
215
210
|
const spaces = [
|
|
@@ -238,12 +233,12 @@ export class Conditional {
|
|
|
238
233
|
|
|
239
234
|
//
|
|
240
235
|
const dayName = matchHeader.substring(0, 3)
|
|
241
|
-
const day = parseInt(matchHeader.substring(5, 7))
|
|
236
|
+
const day = Number.parseInt(matchHeader.substring(5, 7))
|
|
242
237
|
const month = matchHeader.substring(8, 11)
|
|
243
|
-
const year = parseInt(matchHeader.substring(12, 16))
|
|
244
|
-
const hour = parseInt(matchHeader.substring(17, 19))
|
|
245
|
-
const minute = parseInt(matchHeader.substring(20, 22))
|
|
246
|
-
const second = parseInt(matchHeader.substring(23, 25))
|
|
238
|
+
const year = Number.parseInt(matchHeader.substring(12, 16))
|
|
239
|
+
const hour = Number.parseInt(matchHeader.substring(17, 19))
|
|
240
|
+
const minute = Number.parseInt(matchHeader.substring(20, 22))
|
|
241
|
+
const second = Number.parseInt(matchHeader.substring(23, 25))
|
|
247
242
|
|
|
248
243
|
//
|
|
249
244
|
if(!DATE_DAYS.includes(dayName)) { return undefined }
|
|
@@ -255,8 +250,8 @@ export class Conditional {
|
|
|
255
250
|
if(!Number.isInteger(second)) { return undefined }
|
|
256
251
|
|
|
257
252
|
//
|
|
258
|
-
if(day >
|
|
259
|
-
if(year <
|
|
253
|
+
if(day > MAXIMUM_DAY || day <= 0) { return undefined }
|
|
254
|
+
if(year < MINIMUM_YEAR) { return undefined }
|
|
260
255
|
if(hour > 24 || hour < 0) { return undefined }
|
|
261
256
|
if(minute > 60 || minute < 0) { return undefined }
|
|
262
257
|
if(second > 60 || second < 0) { return undefined }
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @typedef {Object} Disposition
|
|
3
3
|
* @property {string} disposition
|
|
4
|
-
* @property {Map<string, string>} parameters
|
|
5
|
-
* @property {string} [name]
|
|
6
|
-
* @property {string} [filename]
|
|
4
|
+
* @property {Map<string, string|undefined>} parameters
|
|
5
|
+
* @property {string|undefined} [name]
|
|
6
|
+
* @property {string|undefined} [filename]
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
export const DISPOSITION_SEPARATOR = {
|
|
@@ -15,17 +15,22 @@ export const DISPOSITION_PARAM_NAME = 'name'
|
|
|
15
15
|
export const DISPOSITION_PARAM_FILENAME = 'filename'
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
|
-
* @param {string} contentDispositionHeader
|
|
18
|
+
* @param {string|undefined} contentDispositionHeader
|
|
19
19
|
* @returns {Disposition|undefined}
|
|
20
20
|
*/
|
|
21
21
|
export function parseContentDisposition(contentDispositionHeader) {
|
|
22
22
|
if(contentDispositionHeader === undefined) { return undefined }
|
|
23
23
|
|
|
24
24
|
const [ disposition, ...parameterSet ] = contentDispositionHeader.trim().split(DISPOSITION_SEPARATOR.PARAMETER).map(entry => entry.trim())
|
|
25
|
+
if(disposition === undefined) { return undefined }
|
|
25
26
|
const parameters = new Map(parameterSet.map(parameter => {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
const [ key, value ] = parameter.split(DISPOSITION_SEPARATOR.KVP).map(p => p.trim())
|
|
28
|
+
if(key === undefined) { return undefined }
|
|
29
|
+
return { key, value }
|
|
30
|
+
})
|
|
31
|
+
.filter(item => item !== undefined)
|
|
32
|
+
.map(({ key, value }) => ([ key, value ])))
|
|
33
|
+
|
|
29
34
|
|
|
30
35
|
const name = parameters.get(DISPOSITION_PARAM_NAME)
|
|
31
36
|
const filename = parameters.get(DISPOSITION_PARAM_FILENAME)
|