@johntalton/http-core 1.0.2 → 1.0.5

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,7 +1,7 @@
1
1
  {
2
2
  "name": "@johntalton/http-core",
3
3
  "type": "module",
4
- "version": "1.0.2",
4
+ "version": "1.0.5",
5
5
  "license": "MIT",
6
6
  "exports": {
7
7
  ".": "./src/index.js"
@@ -15,7 +15,7 @@
15
15
  "scripts": {
16
16
  },
17
17
  "dependencies": {
18
- "@johntalton/http-util": "^5.0.1",
18
+ "@johntalton/http-util": "^6.0.0",
19
19
  "@johntalton/sse-util": "^1.0.0"
20
20
  }
21
21
  }
package/src/epilogue.js CHANGED
@@ -24,7 +24,7 @@ function addSSEPortHandler(stream, port, streamId, shutdownSignal) {
24
24
  port.close()
25
25
  }))
26
26
 
27
- shutdownSignal.addEventListener('abort', signalHandler)
27
+ shutdownSignal.addEventListener('abort', signalHandler, { once: true })
28
28
 
29
29
  // ServerSentEvents.messageToEventStreamLines({
30
30
  // comment: 'Welcome',
@@ -47,24 +47,25 @@ function addSSEPortHandler(stream, port, streamId, shutdownSignal) {
47
47
  export function epilogue(state) {
48
48
  const { type, stream, meta, streamId } = state
49
49
 
50
- meta.customHeaders.push([ 'X-Request-Id', streamId ])
50
+ meta.customHeaders?.push([ 'X-Request-Id', streamId ])
51
51
 
52
52
  switch(type) {
53
53
  //
54
54
  case 'trace': { Response.trace(stream, state.method, state.url, state.headers, meta) } break
55
55
  //
56
56
  case 'preflight': { Response.preflight(stream, state.methods, state.supportedQueryTypes, undefined, meta) } break
57
- // case 'no-content': { Response.noContent(stream, state.etag, meta)} break
57
+ case 'no-content': { Response.noContent(stream, state.etag, meta)} break
58
58
  // case 'accepted': { Response.accepted(stream, meta) } break
59
59
  case 'created': { Response.created(stream, new URL(state.location, meta.origin), state.etag, meta) } break
60
60
  case 'not-modified': { Response.notModified(stream, state.etag, state.age, { priv: true, maxAge: 60 }, meta) } break
61
61
 
62
62
  //
63
63
  // case 'multiple-choices': { Response.multipleChoices(stream, meta) } break
64
- // case 'gone': { Response.gone(stream, meta) } break
65
- // case 'moved-permanently': { Response.movedPermanently(stream, state.location, meta) } break
66
- // case 'see-other': { Response.seeOther(stream, state.location, meta) } break
67
- // case 'temporary-redirect': { Response.temporaryRedirect(stream, state.location, meta) } break
64
+ case 'gone': { Response.gone(stream, meta) } break
65
+ case 'moved-permanently': { Response.movedPermanently(stream, state.location, meta) } break
66
+ case 'see-other': { Response.seeOther(stream, state.location, meta) } break
67
+ case 'temporary-redirect': { Response.temporaryRedirect(stream, state.location, meta) } break
68
+ case 'permanent-redirect': { Response.permanentRedirect(stream, state.location, meta) } break
68
69
 
69
70
  //
70
71
  case '404': { Response.notFound(stream, state.message, meta) } break
@@ -75,13 +76,14 @@ export function epilogue(state) {
75
76
  case 'unprocessable': { Response.unprocessable(stream, meta) } break
76
77
  case 'precondition-failed': { Response.preconditionFailed(stream, meta) } break
77
78
  case 'not-satisfiable': { Response.rangeNotSatisfiable(stream, { size: state.contentLength }, meta) } break
78
- // case 'content-too-large': { Response.contentTooLarge(stream, meta) } break
79
- // case 'insufficient-storage': { Response.insufficientStorage(stream, meta) } break
80
- // case 'too-many-requests': { Response.tooManyRequests(stream, state.limit, state.policies, meta) } break
81
- // case 'unauthorized': { Response.unauthorized(stream, meta) } break
79
+ case 'content-too-large': { Response.contentTooLarge(stream, meta) } break
80
+ case 'insufficient-storage': { Response.insufficientStorage(stream, meta) } break
81
+ case 'too-many-requests': { Response.tooManyRequests(stream, state.limit, state.policies, meta) } break
82
+ case 'unauthorized': { Response.unauthorized(stream, state.challenge, meta) } break
83
+ case 'forbidden': { Response.forbidden(stream, meta) } break
82
84
  case 'unavailable': { Response.unavailable(stream, state.message, state.retryAfter, meta)} break
83
85
  case 'not-implemented': { Response.notImplemented(stream, state.message, meta)} break
84
- // case 'timeout': { Response.timeout(stream, meta) } break
86
+ case 'timeout': { Response.timeout(stream, meta) } break
85
87
 
86
88
  //
87
89
  case 'sse': {
package/src/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ /** biome-ignore-all lint/nursery/noExcessiveLinesPerFile: legacy */
1
2
  import crypto from 'node:crypto'
2
3
  import fs from 'node:fs'
3
4
  import http2 from 'node:http2'
@@ -41,14 +42,48 @@ export const KNOWN_METHODS = [
41
42
 
42
43
  /** @import { Metadata } from '@johntalton/http-util/response' */
43
44
  /** @import { BodyFuture } from '@johntalton/http-util/body' */
44
- /** @import { EtagItem, IMFFixDate, ContentRangeDirective } from '@johntalton/http-util/headers' */
45
+ /** @import { EtagItem, IMFFixDate, ContentRangeDirective, RateLimitPolicyInfo, RateLimitInfo, ChallengeItem } from '@johntalton/http-util/headers' */
45
46
  /** @import { SendBody } from '@johntalton/http-util/response' */
47
+ /** @import { SecFetchSite, SecFetchMode, SecFetchDest } from '@johntalton/http-util/headers' */
48
+
46
49
 
47
50
  /** @typedef {(state: RouteRequest|RouteAction) => Promise<RouteAction>} Router */
48
51
 
49
52
  /** @typedef {'request'} RouteTypeRequest */
50
- /** @typedef {'partial-bytes'|'bytes'|'json'|'404'|'sse'|'error'|'preflight'|'not-allowed'|'trace'|'created'|'unsupported-media'|'not-modified'|'precondition-failed'|'unprocessable'|'not-acceptable'|'conflict'|'not-implemented'|'unavailable'|'not-satisfiable'} RouteType */
51
- /** @typedef {'GET'|'HEAD'|'POST'|'PUT'|'OPTIONS'|'DELETE'|'TRACE'} RouteMethod */
53
+ /** @typedef {
54
+ 'partial-bytes' |
55
+ 'bytes' |
56
+ 'json' |
57
+ '404' |
58
+ 'sse' |
59
+ 'error' |
60
+ 'preflight' |
61
+ 'not-allowed' |
62
+ 'trace' |
63
+ 'created' |
64
+ 'unsupported-media' |
65
+ 'not-modified' |
66
+ 'precondition-failed' |
67
+ 'unprocessable' |
68
+ 'not-acceptable' |
69
+ 'conflict' |
70
+ 'not-implemented' |
71
+ 'unavailable' |
72
+ 'not-satisfiable' |
73
+ 'see-other' |
74
+ 'temporary-redirect' |
75
+ 'permanent-redirect' |
76
+ 'moved-permanently' |
77
+ 'gone' |
78
+ 'no-content' |
79
+ 'content-too-large' |
80
+ 'insufficient-storage' |
81
+ 'too-many-requests' |
82
+ 'unauthorized' |
83
+ 'forbidden' |
84
+ 'timeout'
85
+ } RouteType */
86
+ /** @typedef {'GET'|'HEAD'|'POST'|'PUT'|'PATCH'|'OPTIONS'|'DELETE'|'TRACE'|'QUERY'} RouteMethod */
52
87
 
53
88
  /** @typedef {string & { readonly _brand: 'sid' }} StreamID */
54
89
 
@@ -77,6 +112,7 @@ export const KNOWN_METHODS = [
77
112
  * @property {RouteRequestAccept} accept
78
113
  * @property {RouteRemoteClient} client
79
114
  * @property {RouteConditions} conditions
115
+ * @property {SecFetchMetadata} secFetchMetadata
80
116
  * @property {string} SNI
81
117
  */
82
118
  /** @typedef {RouteBase & RouteRequestBase} RouteRequest */
@@ -132,6 +168,14 @@ export const KNOWN_METHODS = [
132
168
  * @property {IMFFixDate|EtagItem|undefined} [range]
133
169
  */
134
170
 
171
+ /**
172
+ * @typedef {Object} SecFetchMetadata
173
+ * @property {SecFetchSite|undefined} site
174
+ * @property {SecFetchMode|undefined} mode
175
+ * @property {SecFetchDest|undefined} dest
176
+ */
177
+
178
+
135
179
  /**
136
180
  * @typedef {Object} RoutePreflightBase
137
181
  * @property {'preflight'} type
@@ -277,6 +321,85 @@ export const KNOWN_METHODS = [
277
321
  */
278
322
  /** @typedef {RouteBase & RouteNotSatisfiableBase} RouteNotSatisfiable */
279
323
 
324
+ /**
325
+ * @typedef {Object} RouteSeeOtherBase
326
+ * @property {'see-other'} type
327
+ * @property {URL} location
328
+ */
329
+ /** @typedef {RouteBase & RouteSeeOtherBase} RouteSeeOther */
330
+
331
+ /**
332
+ * @typedef {Object} RouteTemporaryRedirectBase
333
+ * @property {'temporary-redirect'} type
334
+ * @property {URL} location
335
+ */
336
+ /** @typedef {RouteBase & RouteTemporaryRedirectBase} RouteTemporaryRedirect */
337
+
338
+ /**
339
+ * @typedef {Object} RoutePermanentRedirectBase
340
+ * @property {'permanent-redirect'} type
341
+ * @property {URL} location
342
+ */
343
+ /** @typedef {RouteBase & RoutePermanentRedirectBase} RoutePermanentRedirect */
344
+
345
+ /**
346
+ * @typedef {Object} RouteMovedPermanentlyBase
347
+ * @property {'moved-permanently'} type
348
+ * @property {URL} location
349
+ */
350
+ /** @typedef {RouteBase & RouteMovedPermanentlyBase} RouteMovedPermanently */
351
+
352
+ /**
353
+ * @typedef {Object} RouteNoContentBase
354
+ * @property {'no-content'} type
355
+ * @property {EtagItem|undefined} [etag]
356
+ */
357
+ /** @typedef {RouteBase & RouteNoContentBase} RouteNoContent */
358
+
359
+ /**
360
+ * @typedef {Object} RouteGoneBase
361
+ * @property {'gone'} type
362
+ */
363
+ /** @typedef {RouteBase & RouteGoneBase} RouteGone */
364
+
365
+ /**
366
+ * @typedef {Object} RouteContentTooLargeBase
367
+ * @property {'content-too-large'} type
368
+ */
369
+ /** @typedef {RouteBase & RouteContentTooLargeBase} RouteContentTooLarge */
370
+
371
+ /**
372
+ * @typedef {Object} RouteInsufficientStorageBase
373
+ * @property {'insufficient-storage'} type
374
+ */
375
+ /** @typedef {RouteBase & RouteInsufficientStorageBase} RouteInsufficientStorage */
376
+
377
+ /**
378
+ * @typedef {Object} RouteTooManyRequestsBase
379
+ * @property {'too-many-requests'} type
380
+ * @property {RateLimitInfo} limit
381
+ * @property {Array<RateLimitPolicyInfo>} policies
382
+ */
383
+ /** @typedef {RouteBase & RouteTooManyRequestsBase} RouteTooManyRequests */
384
+
385
+ /**
386
+ * @typedef {Object} RouteUnauthorizedBase
387
+ * @property {'unauthorized'} type
388
+ * @property {Array<ChallengeItem>} challenge
389
+ */
390
+ /** @typedef {RouteBase & RouteUnauthorizedBase} RouteUnauthorized */
391
+
392
+ /**
393
+ * @typedef {Object} RouteForbiddenBase
394
+ * @property {'forbidden'} type
395
+ */
396
+ /** @typedef {RouteBase & RouteForbiddenBase} RouteForbidden */
397
+
398
+ /**
399
+ * @typedef {Object} RouteTimeoutBase
400
+ * @property {'timeout'} type
401
+ */
402
+ /** @typedef {RouteBase & RouteTimeoutBase} RouteTimeout */
280
403
 
281
404
  /**
282
405
  * @typedef {Object} RouteSSEBase
@@ -307,7 +430,19 @@ export const KNOWN_METHODS = [
307
430
  RouteNotImplemented |
308
431
  RouteUnavailable |
309
432
  RoutePartialBytes |
310
- RouteNotSatisfiable
433
+ RouteNotSatisfiable |
434
+ RouteSeeOther |
435
+ RouteTemporaryRedirect |
436
+ RoutePermanentRedirect |
437
+ RouteMovedPermanently |
438
+ RouteNoContent |
439
+ RouteGone |
440
+ RouteContentTooLarge |
441
+ RouteInsufficientStorage |
442
+ RouteTooManyRequests |
443
+ RouteUnauthorized |
444
+ RouteForbidden |
445
+ RouteTimeout
311
446
  } RouteAction */
312
447
 
313
448
  /** @typedef {Record<string, string|undefined>} RouteMatches */
package/src/preamble.js CHANGED
@@ -1,3 +1,4 @@
1
+ /** biome-ignore-all lint/nursery/noExcessiveLinesPerFile: does all the work */
1
2
  import http2 from 'node:http2'
2
3
  import { TLSSocket } from 'node:tls'
3
4
 
@@ -8,6 +9,7 @@ import {
8
9
  AcceptLanguage,
9
10
 
10
11
  Conditional,
12
+ ContentType,
11
13
  ETag,
12
14
 
13
15
  FORWARDED_KEY_FOR,
@@ -19,18 +21,21 @@ import {
19
21
  MIME_TYPE_MESSAGE_HTTP,
20
22
  MIME_TYPE_TEXT,
21
23
  MIME_TYPE_XML,
22
- parseContentType
24
+ SecFetch
23
25
  } from '@johntalton/http-util/headers'
24
26
  import {
25
27
  ENCODER_MAP,
26
28
  HTTP_HEADER_FORWARDED,
27
- HTTP_HEADER_ORIGIN
29
+ HTTP_HEADER_ORIGIN,
30
+ HTTP_HEADER_SEC_FETCH_DEST,
31
+ HTTP_HEADER_SEC_FETCH_MODE,
32
+ HTTP_HEADER_SEC_FETCH_SITE
28
33
  } from '@johntalton/http-util/response'
29
34
 
30
35
  import { isValidHeader, isValidLikeHeader, isValidMethod } from './index.js'
31
36
 
32
37
  /** @import { ServerHttp2Stream, IncomingHttpHeaders } from 'node:http2' */
33
- /** @import { Config, RouteRequest, RouteAction, StreamID, RouteConditions } from './index.js' */
38
+ /** @import { Config, RouteRequest, RouteAction, StreamID, RouteConditions, SecFetchMetadata } from './index.js' */
34
39
 
35
40
  const { HTTP2_METHOD_OPTIONS, HTTP2_METHOD_TRACE } = http2.constants
36
41
 
@@ -142,9 +147,9 @@ export function preamble(config, streamId, stream, headers, servername, shutdown
142
147
  // const secUA = header[HTTP_HEADER_SEC_CH_UA]
143
148
  // const secPlatform = header[HTTP_HEADER_SEC_CH_PLATFORM]
144
149
  // const secMobile = header[HTTP_HEADER_SEC_CH_MOBILE]
145
- // const secFetchSite = header[HTTP_HEADER_SEC_FETCH_SITE]
146
- // const secFetchMode = header[HTTP_HEADER_SEC_FETCH_MODE]
147
- // const secFetchDest = header[HTTP_HEADER_SEC_FETCH_DEST]
150
+ const secFetchSite = headers[HTTP_HEADER_SEC_FETCH_SITE]
151
+ const secFetchMode = headers[HTTP_HEADER_SEC_FETCH_MODE]
152
+ const secFetchDest = headers[HTTP_HEADER_SEC_FETCH_DEST]
148
153
 
149
154
  //
150
155
  const allowedOrigin = (ALLOWED_ORIGINS.includes('*') || ((origin !== undefined) && URL.canParse(origin) && ALLOWED_ORIGINS.includes(origin))) ? origin : undefined
@@ -200,6 +205,15 @@ export function preamble(config, streamId, stream, headers, servername, shutdown
200
205
  //
201
206
  const requestUrl = new URL(fullPathAndQuery, `${scheme}://${authority}`)
202
207
 
208
+ // Sec Fetch Metadata
209
+ /** @type {SecFetchMetadata} */
210
+ const secFetchMetadata = {
211
+ site: SecFetch.parseSite(secFetchSite),
212
+ mode: SecFetch.parseMode(secFetchMode),
213
+ dest: SecFetch.parseDestination(secFetchDest)
214
+ }
215
+
216
+
203
217
  //
204
218
  /** @type {RouteConditions} */
205
219
  const conditions = {
@@ -240,7 +254,7 @@ export function preamble(config, streamId, stream, headers, servername, shutdown
240
254
  //
241
255
  // content negotiation
242
256
  //
243
- const contentType = parseContentType(fullContentType)
257
+ const contentType = ContentType.parse(fullContentType)
244
258
  const acceptedEncoding = AcceptEncoding.select(fullAcceptEncoding, DEFAULT_SUPPORTED_ENCODINGS)
245
259
  const accept = Accept.select(fullAccept, DEFAULT_SUPPORTED_MIME_TYPES)
246
260
  const acceptedLanguage = AcceptLanguage.select(fullAcceptLanguage, DEFAULT_SUPPORTED_LANGUAGES)
@@ -255,7 +269,7 @@ export function preamble(config, streamId, stream, headers, servername, shutdown
255
269
  //
256
270
  if(method === HTTP2_METHOD_TRACE) {
257
271
  if(!ALLOW_TRACE) { return { ...state, type: 'not-allowed', method, methods: [], url: requestUrl }}
258
- const maxForwardsValue = maxForwards !== undefined ? Number.parseInt(maxForwards) : 0
272
+ const maxForwardsValue = maxForwards === undefined ? 0 : Number.parseInt(maxForwards)
259
273
  const preambleEnd = performance.now()
260
274
  state.meta.performance.push({ name: 'preamble-trace', duration: preambleEnd - preambleStart })
261
275
  if(acceptObject.type !== MIME_TYPE_MESSAGE_HTTP) { return { ...state, type: 'not-acceptable', acceptableMediaTypes: [ MIME_TYPE_MESSAGE_HTTP ] } }
@@ -294,6 +308,7 @@ export function preamble(config, streamId, stream, headers, servername, shutdown
294
308
  body,
295
309
  // tokens,
296
310
  conditions,
311
+ secFetchMetadata,
297
312
  accept: acceptObject,
298
313
  client: { family, ip, port },
299
314
  SNI