@nifrajs/core 5.5.1 → 5.7.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.
Files changed (209) hide show
  1. package/CHANGELOG.md +246 -0
  2. package/README.md +5 -0
  3. package/dist/assurance.d.ts.map +1 -1
  4. package/dist/assurance.js.map +1 -1
  5. package/dist/binary.d.ts +15 -1
  6. package/dist/binary.d.ts.map +1 -1
  7. package/dist/binary.js +1 -0
  8. package/dist/binary.js.map +1 -1
  9. package/dist/edge-kit.d.ts +1 -0
  10. package/dist/edge-kit.d.ts.map +1 -1
  11. package/dist/edge-kit.js +1 -0
  12. package/dist/edge-kit.js.map +1 -1
  13. package/dist/errors.d.ts +1 -1
  14. package/dist/errors.d.ts.map +1 -1
  15. package/dist/errors.js.map +1 -1
  16. package/dist/evidence.d.ts +4 -0
  17. package/dist/evidence.d.ts.map +1 -1
  18. package/dist/evidence.js +4 -0
  19. package/dist/evidence.js.map +1 -1
  20. package/dist/file-type.d.ts +30 -0
  21. package/dist/file-type.d.ts.map +1 -0
  22. package/dist/file-type.js +118 -0
  23. package/dist/file-type.js.map +1 -0
  24. package/dist/index.d.ts +3 -1
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +3 -1
  27. package/dist/index.js.map +1 -1
  28. package/dist/internal/hook-origins.d.ts +2 -0
  29. package/dist/internal/hook-origins.d.ts.map +1 -1
  30. package/dist/internal/hook-origins.js +12 -0
  31. package/dist/internal/hook-origins.js.map +1 -1
  32. package/dist/internal/route-program.d.ts +11 -0
  33. package/dist/internal/route-program.d.ts.map +1 -1
  34. package/dist/internal/route-program.js +133 -85
  35. package/dist/internal/route-program.js.map +1 -1
  36. package/dist/reflection.d.ts +19 -1
  37. package/dist/reflection.d.ts.map +1 -1
  38. package/dist/reflection.js +46 -7
  39. package/dist/reflection.js.map +1 -1
  40. package/dist/response-observer.d.ts +1 -0
  41. package/dist/response-observer.d.ts.map +1 -1
  42. package/dist/response-observer.js +1 -0
  43. package/dist/response-observer.js.map +1 -1
  44. package/dist/server/answer-step.d.ts +27 -0
  45. package/dist/server/answer-step.d.ts.map +1 -0
  46. package/dist/server/answer-step.js +33 -0
  47. package/dist/server/answer-step.js.map +1 -0
  48. package/dist/server/bare-error-lane.d.ts +4 -1
  49. package/dist/server/bare-error-lane.d.ts.map +1 -1
  50. package/dist/server/bare-error-lane.js +9 -3
  51. package/dist/server/bare-error-lane.js.map +1 -1
  52. package/dist/server/base64-answers.d.ts +5 -0
  53. package/dist/server/base64-answers.d.ts.map +1 -0
  54. package/dist/server/base64-answers.js +132 -0
  55. package/dist/server/base64-answers.js.map +1 -0
  56. package/dist/server/before-handler.d.ts +10 -0
  57. package/dist/server/before-handler.d.ts.map +1 -0
  58. package/dist/server/before-handler.js +12 -0
  59. package/dist/server/before-handler.js.map +1 -0
  60. package/dist/server/body-lane.d.ts.map +1 -1
  61. package/dist/server/body-lane.js +8 -2
  62. package/dist/server/body-lane.js.map +1 -1
  63. package/dist/server/body-parser.d.ts.map +1 -1
  64. package/dist/server/body-parser.js +2 -5
  65. package/dist/server/body-parser.js.map +1 -1
  66. package/dist/server/body.d.ts +24 -8
  67. package/dist/server/body.d.ts.map +1 -1
  68. package/dist/server/body.js +86 -33
  69. package/dist/server/body.js.map +1 -1
  70. package/dist/server/core-copies.d.ts.map +1 -1
  71. package/dist/server/core-copies.js +5 -2
  72. package/dist/server/core-copies.js.map +1 -1
  73. package/dist/server/http.d.ts.map +1 -1
  74. package/dist/server/http.js +6 -17
  75. package/dist/server/http.js.map +1 -1
  76. package/dist/server/install.d.ts +2 -0
  77. package/dist/server/install.d.ts.map +1 -1
  78. package/dist/server/install.js +2 -0
  79. package/dist/server/install.js.map +1 -1
  80. package/dist/server/json-response.d.ts +3 -0
  81. package/dist/server/json-response.d.ts.map +1 -0
  82. package/dist/server/json-response.js +14 -0
  83. package/dist/server/json-response.js.map +1 -0
  84. package/dist/server/json-text.d.ts +16 -0
  85. package/dist/server/json-text.d.ts.map +1 -0
  86. package/dist/server/json-text.js +34 -0
  87. package/dist/server/json-text.js.map +1 -0
  88. package/dist/server/multipart.d.ts +3 -0
  89. package/dist/server/multipart.d.ts.map +1 -1
  90. package/dist/server/multipart.js +226 -97
  91. package/dist/server/multipart.js.map +1 -1
  92. package/dist/server/node-outcome.d.ts +5 -0
  93. package/dist/server/node-outcome.d.ts.map +1 -1
  94. package/dist/server/node-outcome.js +3 -4
  95. package/dist/server/node-outcome.js.map +1 -1
  96. package/dist/server/proto-guard.d.ts.map +1 -1
  97. package/dist/server/proto-guard.js +39 -4
  98. package/dist/server/proto-guard.js.map +1 -1
  99. package/dist/server/query.d.ts.map +1 -1
  100. package/dist/server/query.js +8 -14
  101. package/dist/server/query.js.map +1 -1
  102. package/dist/server/repeated-prefix.d.ts +17 -0
  103. package/dist/server/repeated-prefix.d.ts.map +1 -0
  104. package/dist/server/repeated-prefix.js +17 -0
  105. package/dist/server/repeated-prefix.js.map +1 -0
  106. package/dist/server/request-context.d.ts.map +1 -1
  107. package/dist/server/request-context.js +5 -5
  108. package/dist/server/request-context.js.map +1 -1
  109. package/dist/server/respond.d.ts +3 -4
  110. package/dist/server/respond.d.ts.map +1 -1
  111. package/dist/server/respond.js +45 -37
  112. package/dist/server/respond.js.map +1 -1
  113. package/dist/server/response-contract-lane.d.ts +12 -2
  114. package/dist/server/response-contract-lane.d.ts.map +1 -1
  115. package/dist/server/response-contract-lane.js +137 -9
  116. package/dist/server/response-contract-lane.js.map +1 -1
  117. package/dist/server/route-template.d.ts +10 -0
  118. package/dist/server/route-template.d.ts.map +1 -0
  119. package/dist/server/route-template.js +21 -0
  120. package/dist/server/route-template.js.map +1 -0
  121. package/dist/server/runtime-core.d.ts +7 -0
  122. package/dist/server/runtime-core.d.ts.map +1 -1
  123. package/dist/server/runtime-core.js +10 -2
  124. package/dist/server/runtime-core.js.map +1 -1
  125. package/dist/server/server-types.d.ts +17 -8
  126. package/dist/server/server-types.d.ts.map +1 -1
  127. package/dist/server/server.d.ts +27 -12
  128. package/dist/server/server.d.ts.map +1 -1
  129. package/dist/server/server.js +326 -108
  130. package/dist/server/server.js.map +1 -1
  131. package/dist/server/sse.d.ts +1 -1
  132. package/dist/server/sse.d.ts.map +1 -1
  133. package/dist/server/sse.js +7 -5
  134. package/dist/server/sse.js.map +1 -1
  135. package/dist/server/text-slots.d.ts +8 -0
  136. package/dist/server/text-slots.d.ts.map +1 -0
  137. package/dist/server/text-slots.js +19 -0
  138. package/dist/server/text-slots.js.map +1 -0
  139. package/dist/server/webhook.d.ts +1 -1
  140. package/dist/server/webhook.d.ts.map +1 -1
  141. package/dist/server/webhook.js +24 -19
  142. package/dist/server/webhook.js.map +1 -1
  143. package/dist/server/websocket.d.ts +18 -3
  144. package/dist/server/websocket.d.ts.map +1 -1
  145. package/dist/server/websocket.js +45 -5
  146. package/dist/server/websocket.js.map +1 -1
  147. package/dist/server/ws-bun.d.ts.map +1 -1
  148. package/dist/server/ws-bun.js +7 -4
  149. package/dist/server/ws-bun.js.map +1 -1
  150. package/dist/server.d.ts +2 -0
  151. package/dist/server.d.ts.map +1 -1
  152. package/dist/server.js +2 -0
  153. package/dist/server.js.map +1 -1
  154. package/dist/single-copy.d.ts +5 -0
  155. package/dist/single-copy.d.ts.map +1 -1
  156. package/dist/single-copy.js +26 -17
  157. package/dist/single-copy.js.map +1 -1
  158. package/dist/tool-cli.d.ts +64 -0
  159. package/dist/tool-cli.d.ts.map +1 -0
  160. package/dist/tool-cli.js +495 -0
  161. package/dist/tool-cli.js.map +1 -0
  162. package/dist/transport-codec.d.ts.map +1 -1
  163. package/dist/transport-codec.js +2 -1
  164. package/dist/transport-codec.js.map +1 -1
  165. package/package.json +11 -1
  166. package/src/assurance.ts +1 -0
  167. package/src/binary.ts +17 -0
  168. package/src/edge-kit.ts +1 -0
  169. package/src/errors.ts +1 -0
  170. package/src/evidence.ts +8 -0
  171. package/src/file-type.ts +130 -0
  172. package/src/index.ts +3 -1
  173. package/src/internal/hook-origins.ts +10 -0
  174. package/src/internal/route-program.ts +261 -93
  175. package/src/reflection.ts +71 -7
  176. package/src/response-observer.ts +6 -0
  177. package/src/server/answer-step.ts +68 -0
  178. package/src/server/bare-error-lane.ts +9 -4
  179. package/src/server/base64-answers.ts +138 -0
  180. package/src/server/before-handler.ts +14 -0
  181. package/src/server/body-lane.ts +19 -9
  182. package/src/server/body-parser.ts +1 -5
  183. package/src/server/body.ts +96 -36
  184. package/src/server/core-copies.ts +5 -2
  185. package/src/server/http.ts +6 -15
  186. package/src/server/install.ts +3 -0
  187. package/src/server/json-response.ts +13 -0
  188. package/src/server/json-text.ts +34 -0
  189. package/src/server/multipart.ts +232 -100
  190. package/src/server/node-outcome.ts +4 -5
  191. package/src/server/proto-guard.ts +39 -4
  192. package/src/server/query.ts +7 -13
  193. package/src/server/repeated-prefix.ts +25 -0
  194. package/src/server/request-context.ts +5 -5
  195. package/src/server/respond.ts +47 -30
  196. package/src/server/response-contract-lane.ts +132 -9
  197. package/src/server/route-template.ts +24 -0
  198. package/src/server/runtime-core.ts +12 -2
  199. package/src/server/server-types.ts +18 -6
  200. package/src/server/server.ts +361 -112
  201. package/src/server/sse.ts +8 -6
  202. package/src/server/text-slots.ts +21 -0
  203. package/src/server/webhook.ts +29 -24
  204. package/src/server/websocket.ts +57 -8
  205. package/src/server/ws-bun.ts +7 -3
  206. package/src/server.ts +2 -0
  207. package/src/single-copy.ts +31 -15
  208. package/src/tool-cli.ts +584 -0
  209. package/src/transport-codec.ts +3 -1
package/src/binary.ts CHANGED
@@ -51,6 +51,23 @@ declare const NIFRA_RAW: unique symbol
51
51
  */
52
52
  export type RawResponse<T> = Response & { readonly [NIFRA_RAW]: T }
53
53
 
54
+ /**
55
+ * Marker on a `t.bytes()` field's TYPE. Declared OPTIONAL, unlike the two above: the handler returns a
56
+ * plain `Uint8Array` (or a `Buffer`), so the field's type has to accept one. Every `Uint8Array` meets an
57
+ * optional brand, so `extends` cannot test it; `Jsonify` asks whether the key is present instead, which
58
+ * only the declared field's type answers yes to.
59
+ */
60
+ declare const NIFRA_BASE64: unique symbol
61
+
62
+ /** The key `Jsonify` looks for to tell a {@link Base64Bytes} field from any other `Uint8Array`. */
63
+ export type Base64BytesBrand = typeof NIFRA_BASE64
64
+
65
+ /** A `t.bytes()` field: a `Uint8Array` in the handler, a base64 string on the wire and in the client. */
66
+ export type Base64Bytes = Uint8Array & { readonly [NIFRA_BASE64]?: true }
67
+
68
+ export type { AnswerStages, AnswerStep, JsonReplyContext } from "./server/answer-step.ts"
69
+ export { withBase64Bytes } from "./server/base64-answers.ts"
70
+
54
71
  /** What can be sent as bytes without being re-encoded on the way out. */
55
72
  export type BinaryBody = ArrayBuffer | ArrayBufferView | Blob | ReadableStream<Uint8Array>
56
73
 
package/src/edge-kit.ts CHANGED
@@ -24,6 +24,7 @@ export { type ErrorLogDetail, emitRequestErrorLog } from "./server/bare-error-la
24
24
  export { assertByteLimit, capBodyBytes } from "./server/body.ts"
25
25
  export { type FormOrigins, readBodyFramed } from "./server/body-lane.ts"
26
26
  export { plainError } from "./server/http.ts"
27
+ export { jsonResponse } from "./server/json-response.ts"
27
28
  export {
28
29
  answerNotFound,
29
30
  type NotFoundHandler,
package/src/errors.ts CHANGED
@@ -30,6 +30,7 @@ export type RouteConfigErrorCode =
30
30
  | "INVALID_WIRE"
31
31
  | "INVALID_VALIDATION_ORDER"
32
32
  | "INVALID_BODY_LIMIT"
33
+ | "RESPONSE_ONLY_SCHEMA"
33
34
  | "PLUGIN_RECONFIGURED"
34
35
  | "MOUNT_CYCLE"
35
36
 
package/src/evidence.ts CHANGED
@@ -31,6 +31,8 @@ export interface ProjectEvidenceSchemaPart {
31
31
  readonly fields?: readonly ReflectedSchemaField[]
32
32
  /** The media types a body schema reads through a parser of its own. */
33
33
  readonly mediaTypes?: readonly string[]
34
+ /** Set when the schema reads text values with `t.query`'s string coercion. */
35
+ readonly coerces?: true
34
36
  }
35
37
 
36
38
  export interface ProjectEvidenceSchema {
@@ -46,6 +48,8 @@ export interface ProjectEvidenceSchema {
46
48
  readonly response?: ProjectEvidenceSchemaPart
47
49
  readonly errors?: Readonly<Record<string, ProjectEvidenceSchemaPart>>
48
50
  readonly sse?: ProjectEvidenceSchemaPart
51
+ /** Set when an `onValidationError` hook answers the route's validation failures. */
52
+ readonly validationErrorHook?: true
49
53
  }
50
54
 
51
55
  export interface ProjectEvidenceSourceLocation {
@@ -143,6 +147,7 @@ function schemaPart(value: SchemaReflection | undefined): ProjectEvidenceSchemaP
143
147
  ...(value.mediaTypes !== undefined
144
148
  ? { mediaTypes: Object.freeze([...value.mediaTypes].sort()) }
145
149
  : {}),
150
+ ...(value.coerces === true ? { coerces: true as const } : {}),
146
151
  })
147
152
  }
148
153
 
@@ -173,6 +178,7 @@ function schemaOf(route: ReflectedRoute): ProjectEvidenceSchema | undefined {
173
178
  ...(response !== undefined ? { response } : {}),
174
179
  ...(Object.keys(errors).length > 0 ? { errors: Object.freeze(errors) } : {}),
175
180
  ...(sse !== undefined ? { sse } : {}),
181
+ ...(source.validationErrorHook === true ? { validationErrorHook: true as const } : {}),
176
182
  }
177
183
  return Object.freeze(schema)
178
184
  }
@@ -488,6 +494,7 @@ function schemaPartToReflection(
488
494
  jsonSchema: value.jsonSchema,
489
495
  fields: value.fields,
490
496
  ...(value.mediaTypes !== undefined ? { mediaTypes: value.mediaTypes } : {}),
497
+ ...(value.coerces === true ? { coerces: true as const } : {}),
491
498
  }
492
499
  }
493
500
 
@@ -518,6 +525,7 @@ function schemaFromEvidence(schema: ProjectEvidenceSchema | undefined): Reflecte
518
525
  ...(response !== undefined ? { response } : {}),
519
526
  ...(Object.keys(errors).length > 0 ? { errors } : {}),
520
527
  ...(sse !== undefined ? { sse } : {}),
528
+ ...(schema.validationErrorHook === true ? { validationErrorHook: true as const } : {}),
521
529
  }
522
530
  }
523
531
 
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Magic-byte file-type detection - trust the bytes, not the `Content-Type` header (which a client
3
+ * sets freely). Reads only the leading bytes; imports nothing, so it is edge-safe. Covers the common
4
+ * upload types; returns `null` for anything unrecognized (incl. text formats like SVG/CSV that have no
5
+ * magic number - handle those explicitly if you accept them).
6
+ *
7
+ * The shared owner for every type-by-bytes check: `@nifrajs/schema/form`'s `t.file({ accept })` and
8
+ * `@nifrajs/uploads`'s `validateUpload` must give one file one verdict, so both read this module.
9
+ */
10
+
11
+ export interface FileType {
12
+ /** Detected MIME type, e.g. `"image/png"`. */
13
+ readonly mime: string
14
+ /** Canonical extension (no dot), e.g. `"png"`. */
15
+ readonly ext: string
16
+ }
17
+
18
+ /**
19
+ * How many leading bytes {@link detectFileType} looks at. Reading this many from the start of a file
20
+ * (`file.slice(0, FILE_TYPE_PREFIX_BYTES)`) is enough for every type it knows: it reaches the first four
21
+ * compatible brands of an ISO-BMFF `ftyp` box, where an AVIF with the generic `mif1` brand says `avif`.
22
+ */
23
+ export const FILE_TYPE_PREFIX_BYTES = 32
24
+
25
+ /**
26
+ * Every MIME type {@link detectFileType} can return. An allow-list entry outside this set (and
27
+ * outside a `type/*` wildcard that covers one of them) can never match, so check a configured
28
+ * allow-list against it up front instead of rejecting every upload at request time.
29
+ */
30
+ export const DETECTABLE_MIME_TYPES: readonly string[] = [
31
+ "image/jpeg",
32
+ "image/png",
33
+ "image/gif",
34
+ "image/webp",
35
+ "audio/wav",
36
+ "video/x-msvideo",
37
+ "image/avif",
38
+ "image/heic",
39
+ "audio/mp4",
40
+ "video/mp4",
41
+ "video/webm",
42
+ "audio/ogg",
43
+ "audio/mpeg",
44
+ "application/pdf",
45
+ "application/zip",
46
+ "application/gzip",
47
+ ]
48
+
49
+ /**
50
+ * An MPEG audio frame header: an 11-bit frame sync, then a version, layer, bitrate and sample rate that
51
+ * are not reserved values. An MP3 needs no ID3 tag, so this is how an untagged one starts. AAC's ADTS
52
+ * sync shares the first 12 bits, but its layer is always the reserved `00`.
53
+ */
54
+ function mpegAudioFrame(bytes: Uint8Array): boolean {
55
+ const [b0 = 0, b1 = 0, b2 = 0] = bytes
56
+ if (bytes.length < 4 || b0 !== 0xff || (b1 & 0xe0) !== 0xe0) return false
57
+ const version = (b1 >> 3) & 0b11
58
+ const layer = (b1 >> 1) & 0b11
59
+ const bitrate = b2 >> 4
60
+ const sampleRate = (b2 >> 2) & 0b11
61
+ return version !== 0b01 && layer !== 0b00 && bitrate !== 0b1111 && sampleRate !== 0b11
62
+ }
63
+
64
+ /** Detect a file's type from its magic bytes, or `null` if unrecognized. */
65
+ export function detectFileType(bytes: Uint8Array): FileType | null {
66
+ const at = (offset: number, ...sig: number[]): boolean =>
67
+ sig.every((byte, i) => bytes[offset + i] === byte)
68
+
69
+ // Images
70
+ if (at(0, 0xff, 0xd8, 0xff)) return { mime: "image/jpeg", ext: "jpg" }
71
+ if (at(0, 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a))
72
+ return { mime: "image/png", ext: "png" }
73
+ if (at(0, 0x47, 0x49, 0x46, 0x38)) return { mime: "image/gif", ext: "gif" }
74
+
75
+ // RIFF container (bytes 0-3 "RIFF", 8-11 the form type)
76
+ if (at(0, 0x52, 0x49, 0x46, 0x46)) {
77
+ if (at(8, 0x57, 0x45, 0x42, 0x50)) return { mime: "image/webp", ext: "webp" }
78
+ if (at(8, 0x57, 0x41, 0x56, 0x45)) return { mime: "audio/wav", ext: "wav" }
79
+ if (at(8, 0x41, 0x56, 0x49, 0x20)) return { mime: "video/x-msvideo", ext: "avi" }
80
+ }
81
+
82
+ // ISO-BMFF (`ftyp` box at offset 4) - mp4 / avif / heic / m4a, disambiguated by the major brand.
83
+ if (at(4, 0x66, 0x74, 0x79, 0x70)) {
84
+ const brandAt = (offset: number): string =>
85
+ String.fromCharCode(
86
+ bytes[offset] ?? 0,
87
+ bytes[offset + 1] ?? 0,
88
+ bytes[offset + 2] ?? 0,
89
+ bytes[offset + 3] ?? 0,
90
+ )
91
+ const brand = brandAt(8)
92
+ // The compatible brands follow the minor version, to the end of the ftyp box. Only those inside the
93
+ // prefix count: the box size is the sender's claim, and a verdict must not depend on bytes a caller
94
+ // that sniffs `FILE_TYPE_PREFIX_BYTES` never reads.
95
+ const boxEnd = Math.min(
96
+ bytes.length,
97
+ FILE_TYPE_PREFIX_BYTES,
98
+ ((bytes[0] ?? 0) << 24) | ((bytes[1] ?? 0) << 16) | ((bytes[2] ?? 0) << 8) | (bytes[3] ?? 0),
99
+ )
100
+ const compatible: string[] = []
101
+ for (let offset = 16; offset + 4 <= boxEnd; offset += 4) compatible.push(brandAt(offset))
102
+ // A HEIF-family file whose major brand is the generic `mif1`/`msf1`/`miaf` names its codec among
103
+ // the compatible brands: AVIF says `avif`/`avis`.
104
+ const avif = (name: string): boolean => name === "avif" || name === "avis"
105
+ const generic = brand === "mif1" || brand === "msf1" || brand === "miaf"
106
+ if (avif(brand) || (generic && compatible.some(avif)))
107
+ return { mime: "image/avif", ext: "avif" }
108
+ if (brand.startsWith("hei") || brand.startsWith("hev") || generic) {
109
+ return { mime: "image/heic", ext: "heic" }
110
+ }
111
+ // Audio brands share the ISO-BMFF container but are NOT video - labeling them `video/mp4` would
112
+ // both reject real audio under an `audio/*` allow-list and admit it under `video/*`. `M4A `/`M4B `
113
+ // are AAC audio / audiobooks.
114
+ if (brand === "M4A " || brand === "M4B ") return { mime: "audio/mp4", ext: "m4a" }
115
+ return { mime: "video/mp4", ext: "mp4" }
116
+ }
117
+
118
+ // A/V + archives
119
+ if (at(0, 0x1a, 0x45, 0xdf, 0xa3)) return { mime: "video/webm", ext: "webm" } // Matroska/WebM
120
+ if (at(0, 0x4f, 0x67, 0x67, 0x53)) return { mime: "audio/ogg", ext: "ogg" }
121
+ if (at(0, 0x49, 0x44, 0x33)) return { mime: "audio/mpeg", ext: "mp3" } // ID3 tag
122
+ if (mpegAudioFrame(bytes)) return { mime: "audio/mpeg", ext: "mp3" } // an untagged MP3
123
+ if (at(0, 0x25, 0x50, 0x44, 0x46)) return { mime: "application/pdf", ext: "pdf" } // "%PDF"
124
+ if (at(0, 0x50, 0x4b, 0x03, 0x04) || at(0, 0x50, 0x4b, 0x05, 0x06)) {
125
+ return { mime: "application/zip", ext: "zip" } // also docx/xlsx/odt containers
126
+ }
127
+ if (at(0, 0x1f, 0x8b)) return { mime: "application/gzip", ext: "gz" }
128
+
129
+ return null
130
+ }
package/src/index.ts CHANGED
@@ -16,7 +16,7 @@
16
16
  * read its own package.json at runtime. `scripts/version.ts` rewrites it on every release bump and
17
17
  * `check:publish` asserts it equals `@nifrajs/core`'s package version.
18
18
  */
19
- export const VERSION = "5.5.1" as const
19
+ export const VERSION = "5.7.0" as const
20
20
 
21
21
  export type Version = typeof VERSION
22
22
 
@@ -58,6 +58,7 @@ export {
58
58
  markAuthenticatedRequest,
59
59
  rejected,
60
60
  } from "./server/auth.ts"
61
+ export { beforeHandler } from "./server/before-handler.ts"
61
62
  export type {
62
63
  Context,
63
64
  Params,
@@ -119,6 +120,7 @@ export type {
119
120
  } from "./server/registry.ts"
120
121
  export { replacedRequestOf } from "./server/request-lineage.ts"
121
122
  export { writableResponse } from "./server/respond.ts"
123
+ export { routeTemplateOf } from "./server/route-template.ts"
122
124
  export {
123
125
  type PlainRender,
124
126
  type ResponseResult,
@@ -112,6 +112,16 @@ export function tagSource(wrapper: unknown, source: unknown): void {
112
112
  if (isRef(wrapper)) sources.set(wrapper, source)
113
113
  }
114
114
 
115
+ /** A wrapper that runs in `hook`'s place on one route, reported as `hook` is. */
116
+ export function tagStandIn(wrapper: unknown, hook: unknown): void {
117
+ if (!isRef(wrapper) || !isRef(hook)) return
118
+ const origin = origins.get(hook)
119
+ if (origin !== undefined) origins.set(wrapper, origin)
120
+ const scope = scopes.get(hook)
121
+ if (scope !== undefined) scopes.set(wrapper, scope)
122
+ sources.set(wrapper, hook)
123
+ }
124
+
115
125
  /** Pair each scoped wrapper with the hook it gates and the scope it gates it by. */
116
126
  export function tagScoped(
117
127
  originals: readonly unknown[],