@znight/http 0.0.0-stage → 0.1.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.
Files changed (88) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +2333 -2
  3. package/dist/adapter-CGSceVEN.d.mts +876 -0
  4. package/dist/compiler.d.mts +156 -0
  5. package/dist/compiler.mjs +2180 -0
  6. package/dist/compiler.mjs.map +1 -0
  7. package/dist/express.d.mts +39 -0
  8. package/dist/express.mjs +135 -0
  9. package/dist/express.mjs.map +1 -0
  10. package/dist/fastify.d.mts +33 -0
  11. package/dist/fastify.mjs +191 -0
  12. package/dist/fastify.mjs.map +1 -0
  13. package/dist/hono.d.mts +49 -0
  14. package/dist/hono.mjs +319 -0
  15. package/dist/hono.mjs.map +1 -0
  16. package/dist/index.d.mts +1254 -0
  17. package/dist/index.mjs +5618 -0
  18. package/dist/index.mjs.map +1 -0
  19. package/dist/node-BUnkdSsa.mjs +183 -0
  20. package/dist/node-BUnkdSsa.mjs.map +1 -0
  21. package/dist/node-io-BY1ZDLRQ.mjs +1566 -0
  22. package/dist/node-io-BY1ZDLRQ.mjs.map +1 -0
  23. package/dist/node.d.mts +30 -0
  24. package/dist/node.mjs +1 -0
  25. package/dist/path-JI87C4k8.d.mts +109 -0
  26. package/dist/path.d.mts +2 -0
  27. package/dist/path.mjs +245 -0
  28. package/dist/path.mjs.map +1 -0
  29. package/dist/size-D8rsQcV0.mjs +184 -0
  30. package/dist/size-D8rsQcV0.mjs.map +1 -0
  31. package/dist/utils-DCbu3a4w.mjs +27 -0
  32. package/dist/utils-DCbu3a4w.mjs.map +1 -0
  33. package/package.json +142 -4
  34. package/src/adapter.ts +240 -0
  35. package/src/adapters/express.ts +201 -0
  36. package/src/adapters/fastify.ts +280 -0
  37. package/src/adapters/hono.ts +488 -0
  38. package/src/adapters/node-io.ts +368 -0
  39. package/src/adapters/node.ts +135 -0
  40. package/src/adapters/shared.ts +508 -0
  41. package/src/body.ts +135 -0
  42. package/src/coerce.ts +195 -0
  43. package/src/compiler/analyze.ts +1513 -0
  44. package/src/compiler/ast.ts +90 -0
  45. package/src/compiler/coercion.ts +267 -0
  46. package/src/compiler/generate.ts +374 -0
  47. package/src/compiler/index.ts +76 -0
  48. package/src/compiler/model.ts +260 -0
  49. package/src/compiler/route-decorators.ts +405 -0
  50. package/src/compiler/validate.ts +288 -0
  51. package/src/context.ts +533 -0
  52. package/src/cookie-settings.ts +68 -0
  53. package/src/cookie-signer.ts +106 -0
  54. package/src/cookies.ts +726 -0
  55. package/src/cors.ts +349 -0
  56. package/src/decisions.ts +331 -0
  57. package/src/decorators.ts +486 -0
  58. package/src/event-channel.ts +197 -0
  59. package/src/exceptions.ts +257 -0
  60. package/src/files.ts +429 -0
  61. package/src/headers.ts +39 -0
  62. package/src/index.ts +185 -0
  63. package/src/inject-sse.ts +309 -0
  64. package/src/interfaces.ts +101 -0
  65. package/src/metadata.ts +162 -0
  66. package/src/mime.ts +157 -0
  67. package/src/multipart.ts +597 -0
  68. package/src/option-error.ts +31 -0
  69. package/src/path.ts +288 -0
  70. package/src/pipeline.ts +918 -0
  71. package/src/plugin-routes.ts +188 -0
  72. package/src/plugin.ts +1432 -0
  73. package/src/proxy.ts +236 -0
  74. package/src/query.ts +30 -0
  75. package/src/response.ts +240 -0
  76. package/src/router.ts +119 -0
  77. package/src/security-headers.ts +161 -0
  78. package/src/serialize.ts +264 -0
  79. package/src/size.ts +66 -0
  80. package/src/sse-parse.ts +194 -0
  81. package/src/sse.ts +650 -0
  82. package/src/standard-schema.ts +81 -0
  83. package/src/status.ts +78 -0
  84. package/src/stream.ts +200 -0
  85. package/src/tokens.ts +21 -0
  86. package/src/uploads.ts +176 -0
  87. package/src/utils.ts +31 -0
  88. package/src/validate.ts +65 -0
@@ -0,0 +1,162 @@
1
+ /**
2
+ * HTTP section of the runtime metadata (`metadata.extensions.http`), generated by the compiler
3
+ * extension `@znight/http/compiler` and consumed by {@link HttpPlugin}. See
4
+ * docs/internals/architecture.md §9.1. Hand-written metadata (tests) must follow the same shape.
5
+ */
6
+ import type { Token } from '@znight/core'
7
+ import type { HttpContext } from './context.ts'
8
+
9
+ /** Route methods. `ALL` registers the route for GET, HEAD, POST, PUT, PATCH, DELETE and OPTIONS. */
10
+ export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS' | 'ALL'
11
+
12
+ /**
13
+ * Where a handler parameter comes from (`file` / `files`: `@UploadedFile` / `@UploadedFiles`, the
14
+ * `name` being the multipart field; `cookie` / `signedCookie`: `@Cookie` / `@SignedCookie`, the
15
+ * `name` being the cookie name).
16
+ */
17
+ export type HttpParamSource =
18
+ | 'param'
19
+ | 'query'
20
+ | 'body'
21
+ | 'headers'
22
+ | 'request'
23
+ | 'response'
24
+ | 'context'
25
+ | 'file'
26
+ | 'files'
27
+ | 'cookie'
28
+ | 'signedCookie'
29
+
30
+ /** A handler parameter (informational: extraction, coercion and validation happen in `invoke`). */
31
+ export interface HttpParamDefinition {
32
+ /** Position in the handler signature. */
33
+ readonly index: number
34
+ readonly source: HttpParamSource
35
+ /** Parameter / query key / header name (lower case) / multipart field. Absent: the whole object (every field). */
36
+ readonly name?: string
37
+ /** Serialised declared type (`number`, `string[]`, `'asc' | 'desc'`...). */
38
+ readonly type?: string
39
+ readonly optional?: boolean
40
+ /** `true` when the value is validated with a schema. */
41
+ readonly schema?: boolean
42
+ }
43
+
44
+ /**
45
+ * Files accepted by an upload parameter (`@UploadedFile` / `@UploadedFiles`). The plugin enforces
46
+ * these rules while the multipart body is read; the presence of required files is checked by
47
+ * `invoke` (the `uploads` helpers).
48
+ */
49
+ export interface HttpUploadDefinition {
50
+ /** Multipart field. Absent: files of any field. */
51
+ readonly field?: string
52
+ /** `true` for `@UploadedFiles` (several files). */
53
+ readonly multiple?: boolean
54
+ /** `true` when a file must be sent (informational, checked by `invoke`). */
55
+ readonly required?: boolean
56
+ /** Maximum size of each file in bytes (`Infinity`: no limit). Default: plugin `uploads.maxFileSize`. */
57
+ readonly maxSize?: number
58
+ /** Maximum number of files (`@UploadedFiles`). Default: plugin `uploads.maxFiles` (1 for `@UploadedFile`). */
59
+ readonly maxFiles?: number
60
+ /** Accepted media types (`image/png`, `image/*`), compared with the type declared by the client. */
61
+ readonly mimeTypes?: readonly string[]
62
+ }
63
+
64
+ /** Options of an `@Sse` route (`HttpRouteDefinition.sse`). */
65
+ export interface HttpSseDefinition {
66
+ /** Heartbeat interval in ms (≥ 1 000); `0` disables it. Absent: the plugin option `sse.heartbeat`. */
67
+ readonly heartbeat?: number
68
+ /** `retry` sent first (ms). Absent: the plugin option `sse.retry`. */
69
+ readonly retry?: number
70
+ /** The stream ends gracefully after this many ms (≥ 1 000). */
71
+ readonly maxDuration?: number
72
+ }
73
+
74
+ /** A route: one decorated method of a controller. */
75
+ export interface HttpRouteDefinition {
76
+ readonly method: HttpMethod
77
+ /** Path of the route decorator, normalised (`/:id`). */
78
+ readonly path: string
79
+ /** `joinPaths(controller.basePath, path)` (without the plugin `prefix`). */
80
+ readonly fullPath: string
81
+ /** Name of the handler method. */
82
+ readonly handler: string
83
+ /** `@HttpCode(status)`. */
84
+ readonly status?: number
85
+ /** `@Header(name, value)` entries (names in lower case). */
86
+ readonly headers?: Readonly<Record<string, string>>
87
+ /**
88
+ * Route metadata (`createRouteDecorator`), merged over the controller metadata at runtime, one
89
+ * decision per group: the route's entry of a group replaces the controller's entries of that group
90
+ * (see {@link HttpMetadata.groups}).
91
+ */
92
+ readonly meta: Readonly<Record<string, unknown>>
93
+ /** Route guards (provider tokens), run after global and controller guards. */
94
+ readonly guards: readonly Token[]
95
+ /** Route interceptors (provider tokens), innermost. */
96
+ readonly interceptors: readonly Token[]
97
+ readonly params: readonly HttpParamDefinition[]
98
+ /**
99
+ * `@BodyLimit` (route or controller): maximum body size in bytes (`Infinity`: no limit). Default:
100
+ * the plugin `bodyLimit`, or for upload routes the sum of the file limits plus `bodyLimit`.
101
+ */
102
+ readonly bodyLimit?: number
103
+ /**
104
+ * Upload rules (one per `@UploadedFile` / `@UploadedFiles` parameter). Routes with uploads parse
105
+ * `multipart/form-data` bodies before the interceptors and answer 415 to other non-empty bodies.
106
+ */
107
+ readonly uploads?: readonly HttpUploadDefinition[]
108
+ /** `true` when the handler takes `@Res()` and writes the response itself. */
109
+ readonly rawResponse?: boolean
110
+ /**
111
+ * `@Sse` route (GET only): the handler result (an `AsyncIterable`, an `Iterable` or a
112
+ * `ReadableStream` of `SseEvent`) is served as an event stream; an `HttpResponse` or a Web
113
+ * `Response` is sent as usual. `{}` without options.
114
+ */
115
+ readonly sse?: HttpSseDefinition
116
+ /**
117
+ * Calls the handler: extracts, coerces and validates every argument from the context, then calls
118
+ * the method on the controller instance. May return a promise.
119
+ */
120
+ // biome-ignore lint/suspicious/noExplicitAny: generated functions take the concrete controller class
121
+ readonly invoke: (controller: any, ctx: HttpContext) => unknown
122
+ }
123
+
124
+ /** A controller: a provider whose methods are routes. */
125
+ export interface HttpControllerDefinition {
126
+ /** Provider token of the controller class. */
127
+ readonly token: Token
128
+ /** Normalised base path (`/users`, `/` when absent). */
129
+ readonly basePath: string
130
+ /** Controller guards (provider tokens). */
131
+ readonly guards: readonly Token[]
132
+ /** Controller interceptors (provider tokens). */
133
+ readonly interceptors: readonly Token[]
134
+ /** Controller metadata (`createRouteDecorator` on the class). */
135
+ readonly meta: Readonly<Record<string, unknown>>
136
+ readonly routes: readonly HttpRouteDefinition[]
137
+ }
138
+
139
+ /** A route decorator group (`createRouteDecorator(key, { group, required })`). */
140
+ export interface HttpRouteGroupDefinition {
141
+ /** Keys of the decorators of the group, sorted. */
142
+ readonly keys: readonly string[]
143
+ /** Every route must carry a decision of the group. */
144
+ readonly required: boolean
145
+ }
146
+
147
+ /** `metadata.extensions.http`. */
148
+ export interface HttpMetadata {
149
+ readonly controllers: readonly HttpControllerDefinition[]
150
+ /**
151
+ * Route decorator groups that need to be known at runtime (required, holding several keys, or
152
+ * named differently from their key): group → `{ keys, required }`. A key belongs to one group at
153
+ * most; a key listed nowhere is its own group. Absent when no group needs it.
154
+ */
155
+ readonly groups?: Readonly<Record<string, HttpRouteGroupDefinition>>
156
+ }
157
+
158
+ declare module '@znight/core' {
159
+ interface ZnightExtensionsMetadata {
160
+ readonly http?: HttpMetadata
161
+ }
162
+ }
package/src/mime.ts ADDED
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Media types of files: a small extension table for downloads and `type/*` patterns for upload
3
+ * filters. This module is **pure** (no imports, no I/O): the compiler extension validates the
4
+ * `mimeTypes` option of `@UploadedFile` with it.
5
+ */
6
+
7
+ /** Common extensions → media type (text types get `charset=utf-8` in {@link contentTypeFor}). */
8
+ const TYPES: Readonly<Record<string, string>> = {
9
+ // text
10
+ txt: 'text/plain',
11
+ text: 'text/plain',
12
+ log: 'text/plain',
13
+ md: 'text/markdown',
14
+ markdown: 'text/markdown',
15
+ csv: 'text/csv',
16
+ tsv: 'text/tab-separated-values',
17
+ htm: 'text/html',
18
+ html: 'text/html',
19
+ css: 'text/css',
20
+ xml: 'application/xml',
21
+ js: 'text/javascript',
22
+ mjs: 'text/javascript',
23
+ cjs: 'text/javascript',
24
+ json: 'application/json',
25
+ map: 'application/json',
26
+ jsonld: 'application/ld+json',
27
+ webmanifest: 'application/manifest+json',
28
+ yaml: 'application/yaml',
29
+ yml: 'application/yaml',
30
+ ics: 'text/calendar',
31
+ vtt: 'text/vtt',
32
+ // images
33
+ png: 'image/png',
34
+ jpg: 'image/jpeg',
35
+ jpeg: 'image/jpeg',
36
+ gif: 'image/gif',
37
+ webp: 'image/webp',
38
+ avif: 'image/avif',
39
+ svg: 'image/svg+xml',
40
+ ico: 'image/x-icon',
41
+ bmp: 'image/bmp',
42
+ tif: 'image/tiff',
43
+ tiff: 'image/tiff',
44
+ heic: 'image/heic',
45
+ // audio / video
46
+ mp3: 'audio/mpeg',
47
+ wav: 'audio/wav',
48
+ ogg: 'audio/ogg',
49
+ oga: 'audio/ogg',
50
+ flac: 'audio/flac',
51
+ aac: 'audio/aac',
52
+ m4a: 'audio/mp4',
53
+ opus: 'audio/opus',
54
+ mp4: 'video/mp4',
55
+ m4v: 'video/mp4',
56
+ webm: 'video/webm',
57
+ ogv: 'video/ogg',
58
+ mov: 'video/quicktime',
59
+ avi: 'video/x-msvideo',
60
+ mkv: 'video/x-matroska',
61
+ // fonts
62
+ woff: 'font/woff',
63
+ woff2: 'font/woff2',
64
+ ttf: 'font/ttf',
65
+ otf: 'font/otf',
66
+ // documents and archives
67
+ pdf: 'application/pdf',
68
+ rtf: 'application/rtf',
69
+ doc: 'application/msword',
70
+ docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
71
+ xls: 'application/vnd.ms-excel',
72
+ xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
73
+ ppt: 'application/vnd.ms-powerpoint',
74
+ pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
75
+ odt: 'application/vnd.oasis.opendocument.text',
76
+ ods: 'application/vnd.oasis.opendocument.spreadsheet',
77
+ odp: 'application/vnd.oasis.opendocument.presentation',
78
+ epub: 'application/epub+zip',
79
+ zip: 'application/zip',
80
+ gz: 'application/gzip',
81
+ tgz: 'application/gzip',
82
+ tar: 'application/x-tar',
83
+ bz2: 'application/x-bzip2',
84
+ xz: 'application/x-xz',
85
+ '7z': 'application/x-7z-compressed',
86
+ rar: 'application/vnd.rar',
87
+ wasm: 'application/wasm',
88
+ bin: 'application/octet-stream',
89
+ }
90
+
91
+ /** Default media type of files without a known extension. */
92
+ export const OCTET_STREAM = 'application/octet-stream'
93
+
94
+ /**
95
+ * Media type of a file name from its extension (`report.pdf` → `application/pdf`, text types with
96
+ * `; charset=utf-8`), `undefined` when the extension is unknown.
97
+ */
98
+ export function contentTypeFor(filename: string): string | undefined {
99
+ const dot = filename.lastIndexOf('.')
100
+ if (dot === -1 || dot === filename.length - 1) return undefined
101
+ const separator = Math.max(filename.lastIndexOf('/'), filename.lastIndexOf('\\'))
102
+ if (dot < separator) return undefined
103
+ const extension = filename.slice(dot + 1).toLowerCase()
104
+ // own keys only: `.constructor` / `.__proto__` must not resolve to `Object.prototype` members
105
+ if (!Object.hasOwn(TYPES, extension)) return undefined
106
+ const type = TYPES[extension] as string
107
+ return isTextual(type) ? `${type}; charset=utf-8` : type
108
+ }
109
+
110
+ function isTextual(type: string): boolean {
111
+ return (
112
+ type.startsWith('text/') ||
113
+ type === 'application/json' ||
114
+ type === 'application/ld+json' ||
115
+ type === 'application/manifest+json' ||
116
+ type === 'application/xml' ||
117
+ type === 'application/yaml' ||
118
+ type === 'image/svg+xml'
119
+ )
120
+ }
121
+
122
+ /** `type/subtype` token characters (RFC 9110). */
123
+ const TOKEN = "[!#$%&'*+\\-.^_`|~0-9A-Za-z]+"
124
+ const MEDIA_TYPE = new RegExp(`^${TOKEN}/${TOKEN}$`)
125
+
126
+ /**
127
+ * Whether `pattern` is a valid media type filter: `type/subtype`, `type/*` or `*\/*`
128
+ * (case-insensitive, without parameters).
129
+ */
130
+ export function isMediaTypePattern(pattern: string): boolean {
131
+ if (!MEDIA_TYPE.test(pattern)) return false
132
+ const [type, subtype] = pattern.split('/') as [string, string]
133
+ // `*` is only a wildcard on its own: `image/*`, `*/*` (`*/png` and `image/png*` are rejected)
134
+ if (type.includes('*') && type !== '*') return false
135
+ if (subtype.includes('*') && subtype !== '*') return false
136
+ return type !== '*' || subtype === '*'
137
+ }
138
+
139
+ /** The essence of a media type: lower-case `type/subtype` without parameters (`Text/Plain; charset=x` → `text/plain`). */
140
+ export function mediaTypeEssence(type: string): string {
141
+ const separator = type.indexOf(';')
142
+ return (separator === -1 ? type : type.slice(0, separator)).trim().toLowerCase()
143
+ }
144
+
145
+ /** Whether the media type `type` (parameters ignored) matches one of `patterns` (`image/png`, `image/*`, `*\/*`). */
146
+ export function matchesMediaType(type: string, patterns: readonly string[]): boolean {
147
+ const essence = mediaTypeEssence(type)
148
+ const slash = essence.indexOf('/')
149
+ if (slash <= 0 || slash === essence.length - 1) return false
150
+ const major = essence.slice(0, slash)
151
+ for (const raw of patterns) {
152
+ const pattern = raw.toLowerCase()
153
+ if (pattern === '*/*' || pattern === essence) return true
154
+ if (pattern.endsWith('/*') && pattern.slice(0, -2) === major) return true
155
+ }
156
+ return false
157
+ }