@johntalton/http-core 1.0.3 → 1.0.7

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.3",
4
+ "version": "1.0.7",
5
5
  "license": "MIT",
6
6
  "exports": {
7
7
  ".": "./src/index.js"
@@ -13,9 +13,13 @@
13
13
  "url": "https://github.com/johntalton/http-core"
14
14
  },
15
15
  "scripts": {
16
+ "lint": "biome check"
16
17
  },
17
18
  "dependencies": {
18
- "@johntalton/http-util": "^5.1.2",
19
+ "@johntalton/http-util": "^6.1.0",
19
20
  "@johntalton/sse-util": "^1.0.0"
21
+ },
22
+ "devDependencies": {
23
+ "@biomejs/biome": "^2.4.15"
20
24
  }
21
25
  }
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,17 +47,17 @@ 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, state.lastModified, meta)} break
58
58
  // case 'accepted': { Response.accepted(stream, meta) } break
59
- case 'created': { Response.created(stream, new URL(state.location, meta.origin), state.etag, meta) } break
60
- case 'not-modified': { Response.notModified(stream, state.etag, state.age, { priv: true, maxAge: 60 }, meta) } break
59
+ case 'created': { Response.created(stream, new URL(state.location, meta.origin), state.etag, state.lastModified, meta) } break
60
+ case 'not-modified': { Response.notModified(stream, state.etag, state.lastModified, state.age, state.cacheControl ?? {}, meta) } break
61
61
 
62
62
  //
63
63
  // case 'multiple-choices': { Response.multipleChoices(stream, meta) } break
@@ -74,7 +74,7 @@ export function epilogue(state) {
74
74
  case 'not-acceptable': { Response.notAcceptable(stream, state.acceptableMediaTypes ?? [], meta)} break
75
75
  case 'unsupported-media': { Response.unsupportedMediaType(stream, state.acceptableMediaTypes, state.supportedQueryTypes, meta) } break
76
76
  case 'unprocessable': { Response.unprocessable(stream, meta) } break
77
- case 'precondition-failed': { Response.preconditionFailed(stream, meta) } break
77
+ case 'precondition-failed': { Response.preconditionFailed(stream, state.etag, state.lastModified, meta) } break
78
78
  case 'not-satisfiable': { Response.rangeNotSatisfiable(stream, { size: state.contentLength }, meta) } break
79
79
  case 'content-too-large': { Response.contentTooLarge(stream, meta) } break
80
80
  case 'insufficient-storage': { Response.insufficientStorage(stream, meta) } break
@@ -94,10 +94,10 @@ export function epilogue(state) {
94
94
  }
95
95
  break
96
96
  case 'json': {
97
- const { obj, accept, etag } = state
97
+ const { obj, accept, etag, lastModified } = state
98
98
 
99
99
  if(accept.type === MIME_TYPE_JSON) {
100
- Response.json(stream, obj, accept.encoding, etag, state.age, { priv: true, maxAge: 60 }, state.supportedQueryTypes, meta)
100
+ Response.json(stream, obj, accept.encoding, etag, lastModified, state.age, state.cacheControl ?? {}, state.supportedQueryTypes, meta)
101
101
  }
102
102
  else {
103
103
  // todo: but we did process the request - is that ok?
@@ -105,8 +105,8 @@ export function epilogue(state) {
105
105
  }
106
106
  }
107
107
  break
108
- case 'partial-bytes': { Response.partialContent(stream, state.contentType, state.objs, state.contentLength, undefined, state.etag, state.age, { maxAge: state.maxAge }, meta) } break
109
- case 'bytes': { Response.bytes(stream, state.contentType, state.obj, state.contentLength, 'identity', state.etag, state.age, { maxAge: state.maxAge }, state.acceptRanges, meta) } break
108
+ case 'partial-bytes': { Response.partialContent(stream, state.contentType, state.objs, state.contentLength, undefined, state.etag, state.lastModified, state.age, state.cacheControl ?? {}, meta) } break
109
+ case 'bytes': { Response.bytes(stream, state.contentType, state.obj, state.contentLength, 'identity', state.etag, state.lastModified, state.age, state.cacheControl ?? {}, state.acceptRanges, meta) } break
110
110
 
111
111
  //
112
112
  case 'error': {
package/src/index.js CHANGED
@@ -42,8 +42,19 @@ export const KNOWN_METHODS = [
42
42
 
43
43
  /** @import { Metadata } from '@johntalton/http-util/response' */
44
44
  /** @import { BodyFuture } from '@johntalton/http-util/body' */
45
- /** @import { EtagItem, IMFFixDate, ContentRangeDirective, RateLimitPolicyInfo, RateLimitInfo, ChallengeItem } from '@johntalton/http-util/headers' */
45
+ /** @import {
46
+ EtagItem,
47
+ IMFFixDate,
48
+ IMFFixDateInput,
49
+ ContentRangeDirective,
50
+ RateLimitPolicyInfo,
51
+ RateLimitInfo,
52
+ ChallengeItem,
53
+ CacheControlOptions
54
+ } from '@johntalton/http-util/headers' */
46
55
  /** @import { SendBody } from '@johntalton/http-util/response' */
56
+ /** @import { SecFetchSite, SecFetchMode, SecFetchDest } from '@johntalton/http-util/headers' */
57
+
47
58
 
48
59
  /** @typedef {(state: RouteRequest|RouteAction) => Promise<RouteAction>} Router */
49
60
 
@@ -100,6 +111,20 @@ export const KNOWN_METHODS = [
100
111
  * @property {AbortSignal} shutdownSignal
101
112
  */
102
113
 
114
+ /**
115
+ * @typedef {Object} RouteRequestAccept
116
+ * @property {string|undefined} type
117
+ * @property {string|undefined} encoding
118
+ * @property {string|undefined} language
119
+ */
120
+
121
+ /**
122
+ * @typedef {Object} RouteRemoteClient
123
+ * @property {string|undefined} family
124
+ * @property {string|undefined} ip
125
+ * @property {number|undefined} port
126
+ */
127
+
103
128
  /**
104
129
  * @typedef {Object} RouteRequestBase
105
130
  * @property {'request'} type
@@ -110,6 +135,7 @@ export const KNOWN_METHODS = [
110
135
  * @property {RouteRequestAccept} accept
111
136
  * @property {RouteRemoteClient} client
112
137
  * @property {RouteConditions} conditions
138
+ * @property {SecFetchMetadata} secFetchMetadata
113
139
  * @property {string} SNI
114
140
  */
115
141
  /** @typedef {RouteBase & RouteRequestBase} RouteRequest */
@@ -138,24 +164,9 @@ export const KNOWN_METHODS = [
138
164
  * @property {URL} url
139
165
  * @property {IncomingHttpHeaders} headers
140
166
  * @property {number} maxForwards
141
- * @property {RouteRequestAccept} accept
142
167
  */
143
168
  /** @typedef {RouteBase & RouteTraceBase} RouteTrace */
144
169
 
145
- /**
146
- * @typedef {Object} RouteRequestAccept
147
- * @property {string|undefined} type
148
- * @property {string|undefined} encoding
149
- * @property {string|undefined} language
150
- */
151
-
152
- /**
153
- * @typedef {Object} RouteRemoteClient
154
- * @property {string|undefined} family
155
- * @property {string|undefined} ip
156
- * @property {number|undefined} port
157
- */
158
-
159
170
  /**
160
171
  * @typedef {Object} RouteConditions
161
172
  * @property {Array<EtagItem>} match
@@ -165,6 +176,14 @@ export const KNOWN_METHODS = [
165
176
  * @property {IMFFixDate|EtagItem|undefined} [range]
166
177
  */
167
178
 
179
+ /**
180
+ * @typedef {Object} SecFetchMetadata
181
+ * @property {SecFetchSite|undefined} site
182
+ * @property {SecFetchMode|undefined} mode
183
+ * @property {SecFetchDest|undefined} dest
184
+ */
185
+
186
+
168
187
  /**
169
188
  * @typedef {Object} RoutePreflightBase
170
189
  * @property {'preflight'} type
@@ -180,9 +199,10 @@ export const KNOWN_METHODS = [
180
199
  * @property {'json'} type
181
200
  * @property {RouteRequestAccept} accept
182
201
  * @property {Record<any, any>} obj
183
- * @property {IMFFixDate|string|undefined} [lastModified]
202
+ * @property {IMFFixDateInput|string|undefined} [lastModified]
184
203
  * @property {EtagItem|undefined} [etag]
185
204
  * @property {number|undefined} [age]
205
+ * @property {CacheControlOptions|undefined} [cacheControl]
186
206
  * @property {Array<string>|undefined} [supportedQueryTypes]
187
207
  */
188
208
  /** @typedef {RouteBase & RouteJSONBase} RouteJSON */
@@ -201,6 +221,7 @@ export const KNOWN_METHODS = [
201
221
  * @property {'created'} type
202
222
  * @property {URL|string} location
203
223
  * @property {EtagItem|undefined} [etag]
224
+ * @property {IMFFixDateInput|string|undefined} [lastModified]
204
225
  */
205
226
  /** @typedef {RouteBase & RouteCreatedBase} RouteCreated */
206
227
 
@@ -217,7 +238,9 @@ export const KNOWN_METHODS = [
217
238
  * @property {'not-modified'} type
218
239
  * @property {number} age
219
240
  * @property {EtagItem|undefined} [etag]
241
+ * @property {IMFFixDateInput|string|undefined} [lastModified]
220
242
  * @property {number|undefined} [age]
243
+ * @property {CacheControlOptions|undefined} [cacheControl]
221
244
  */
222
245
  /** @typedef {RouteBase & RouteNotModifiedBase} RouteNotModified */
223
246
 
@@ -225,6 +248,7 @@ export const KNOWN_METHODS = [
225
248
  * @typedef {Object} RoutePreconditionFailedBase
226
249
  * @property {'precondition-failed'} type
227
250
  * @property {EtagItem|undefined} [etag]
251
+ * @property {IMFFixDateInput|string|undefined} [lastModified]
228
252
  */
229
253
  /** @typedef {RouteBase & RoutePreconditionFailedBase} RoutePreconditionFailed */
230
254
 
@@ -272,10 +296,10 @@ export const KNOWN_METHODS = [
272
296
  * @property {string} contentType
273
297
  * @property {number|undefined} [contentLength]
274
298
  * @property {SendBody|undefined} obj
275
- * @property {IMFFixDate|string|undefined} [lastModified]
299
+ * @property {IMFFixDateInput|string|undefined} [lastModified]
276
300
  * @property {EtagItem|undefined} [etag]
277
301
  * @property {number|undefined} [age]
278
- * @property {number|undefined} [maxAge]
302
+ * @property {CacheControlOptions|undefined} [cacheControl]
279
303
  * @property {'bytes'|'none'|undefined} [acceptRanges]
280
304
  */
281
305
  /** @typedef {RouteBase & RouteBytesBase} RouteBytes */
@@ -298,8 +322,9 @@ export const KNOWN_METHODS = [
298
322
  * @property {string} contentType
299
323
  * @property {number|undefined} [contentLength]
300
324
  * @property {EtagItem|undefined} [etag]
325
+ * @property {IMFFixDateInput|string|undefined} [lastModified]
301
326
  * @property {number|undefined} [age]
302
- * @property {number|undefined} [maxAge]
327
+ * @property {CacheControlOptions|undefined} [cacheControl]
303
328
  */
304
329
  /** @typedef {RouteBase & RoutePartialBytesBase} RoutePartialBytes */
305
330
 
@@ -342,6 +367,7 @@ export const KNOWN_METHODS = [
342
367
  * @typedef {Object} RouteNoContentBase
343
368
  * @property {'no-content'} type
344
369
  * @property {EtagItem|undefined} [etag]
370
+ * @property {IMFFixDateInput|string|undefined} [lastModified]
345
371
  */
346
372
  /** @typedef {RouteBase & RouteNoContentBase} RouteNoContent */
347
373
 
@@ -396,7 +422,6 @@ export const KNOWN_METHODS = [
396
422
  * @property {boolean} active
397
423
  * @property {boolean} bom
398
424
  * @property {MessagePort} port
399
- * @property {RouteRequestAccept} accept
400
425
  */
401
426
  /** @typedef {RouteBase & RouteSSEBase} RouteSSE */
402
427
 
@@ -604,7 +629,14 @@ export class H2CoreServer {
604
629
  if(!isServerStream(stream)) { return }
605
630
 
606
631
  // const start = performance.now()
607
- const state = preamble(this.#h2Options.config, streamId, stream, headers, this.#h2Options.serverName, this.#controller.signal)
632
+ const state = preamble({
633
+ config: this.#h2Options.config,
634
+ streamId,
635
+ stream,
636
+ shutdownSignal: this.#controller.signal
637
+ },
638
+ headers,
639
+ this.#h2Options.serverName)
608
640
  router(state)
609
641
  .then(epilogue)
610
642
  .catch(e => epilogue({ ...state, type: 'error', cause: e.message, error: e }))
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
 
@@ -94,16 +99,24 @@ const BODY_BYTE_LENGTH = BYTE_PER_K * BYTE_PER_K
94
99
  // }
95
100
 
96
101
  /**
97
- * @param {Config} config
98
- * @param {StreamID} streamId
99
- * @param {ServerHttp2Stream} stream
102
+ * @typedef {Object} PreState
103
+ * @property {Config} config
104
+ * @property {StreamID} streamId
105
+ * @property {ServerHttp2Stream} stream
106
+ * @property {AbortSignal} shutdownSignal
107
+ */
108
+
109
+ /**
110
+ * @param {PreState} preState
100
111
  * @param {IncomingHttpHeaders} headers
101
112
  * @param {string|undefined} servername
102
- * @param {AbortSignal} shutdownSignal
103
113
  * @returns {RouteRequest|RouteAction}
104
114
  */
105
- export function preamble(config, streamId, stream, headers, servername, shutdownSignal) {
115
+
116
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: work horse
117
+ export function preamble(preState, headers, servername) {
106
118
  const preambleStart = performance.now()
119
+ const { stream, shutdownSignal } = preState
107
120
 
108
121
  //
109
122
  const method = headers[HTTP2_HEADER_METHOD]
@@ -142,9 +155,9 @@ export function preamble(config, streamId, stream, headers, servername, shutdown
142
155
  // const secUA = header[HTTP_HEADER_SEC_CH_UA]
143
156
  // const secPlatform = header[HTTP_HEADER_SEC_CH_PLATFORM]
144
157
  // 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]
158
+ const secFetchSite = headers[HTTP_HEADER_SEC_FETCH_SITE]
159
+ const secFetchMode = headers[HTTP_HEADER_SEC_FETCH_MODE]
160
+ const secFetchDest = headers[HTTP_HEADER_SEC_FETCH_DEST]
148
161
 
149
162
  //
150
163
  const allowedOrigin = (ALLOWED_ORIGINS.includes('*') || ((origin !== undefined) && URL.canParse(origin) && ALLOWED_ORIGINS.includes(origin))) ? origin : undefined
@@ -154,16 +167,13 @@ export function preamble(config, streamId, stream, headers, servername, shutdown
154
167
  const state = {
155
168
  type: 'error',
156
169
  cause: 'initialize',
157
- config,
158
- streamId,
159
- stream,
170
+ ...preState,
160
171
  meta: {
161
172
  servername,
162
173
  performance: [],
163
174
  origin: allowedOrigin,
164
175
  customHeaders: []
165
- },
166
- shutdownSignal
176
+ }
167
177
  }
168
178
 
169
179
  if(shutdownSignal.aborted) {
@@ -200,6 +210,15 @@ export function preamble(config, streamId, stream, headers, servername, shutdown
200
210
  //
201
211
  const requestUrl = new URL(fullPathAndQuery, `${scheme}://${authority}`)
202
212
 
213
+ // Sec Fetch Metadata
214
+ /** @type {SecFetchMetadata} */
215
+ const secFetchMetadata = {
216
+ site: SecFetch.parseSite(secFetchSite),
217
+ mode: SecFetch.parseMode(secFetchMode),
218
+ dest: SecFetch.parseDestination(secFetchDest)
219
+ }
220
+
221
+
203
222
  //
204
223
  /** @type {RouteConditions} */
205
224
  const conditions = {
@@ -240,7 +259,7 @@ export function preamble(config, streamId, stream, headers, servername, shutdown
240
259
  //
241
260
  // content negotiation
242
261
  //
243
- const contentType = parseContentType(fullContentType)
262
+ const contentType = ContentType.parse(fullContentType)
244
263
  const acceptedEncoding = AcceptEncoding.select(fullAcceptEncoding, DEFAULT_SUPPORTED_ENCODINGS)
245
264
  const accept = Accept.select(fullAccept, DEFAULT_SUPPORTED_MIME_TYPES)
246
265
  const acceptedLanguage = AcceptLanguage.select(fullAcceptLanguage, DEFAULT_SUPPORTED_LANGUAGES)
@@ -255,11 +274,11 @@ export function preamble(config, streamId, stream, headers, servername, shutdown
255
274
  //
256
275
  if(method === HTTP2_METHOD_TRACE) {
257
276
  if(!ALLOW_TRACE) { return { ...state, type: 'not-allowed', method, methods: [], url: requestUrl }}
258
- const maxForwardsValue = maxForwards !== undefined ? Number.parseInt(maxForwards) : 0
277
+ const maxForwardsValue = maxForwards === undefined ? 0 : Number.parseInt(maxForwards)
259
278
  const preambleEnd = performance.now()
260
279
  state.meta.performance.push({ name: 'preamble-trace', duration: preambleEnd - preambleStart })
261
280
  if(acceptObject.type !== MIME_TYPE_MESSAGE_HTTP) { return { ...state, type: 'not-acceptable', acceptableMediaTypes: [ MIME_TYPE_MESSAGE_HTTP ] } }
262
- return { ...state, type: 'trace', method, headers, url: requestUrl, maxForwards: maxForwardsValue, accept: acceptObject }
281
+ return { ...state, type: 'trace', method, headers, url: requestUrl, maxForwards: maxForwardsValue }
263
282
  }
264
283
 
265
284
  //
@@ -294,6 +313,7 @@ export function preamble(config, streamId, stream, headers, servername, shutdown
294
313
  body,
295
314
  // tokens,
296
315
  conditions,
316
+ secFetchMetadata,
297
317
  accept: acceptObject,
298
318
  client: { family, ip, port },
299
319
  SNI