@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.
- package/LICENSE +21 -0
- package/README.md +2333 -2
- package/dist/adapter-CGSceVEN.d.mts +876 -0
- package/dist/compiler.d.mts +156 -0
- package/dist/compiler.mjs +2180 -0
- package/dist/compiler.mjs.map +1 -0
- package/dist/express.d.mts +39 -0
- package/dist/express.mjs +135 -0
- package/dist/express.mjs.map +1 -0
- package/dist/fastify.d.mts +33 -0
- package/dist/fastify.mjs +191 -0
- package/dist/fastify.mjs.map +1 -0
- package/dist/hono.d.mts +49 -0
- package/dist/hono.mjs +319 -0
- package/dist/hono.mjs.map +1 -0
- package/dist/index.d.mts +1254 -0
- package/dist/index.mjs +5618 -0
- package/dist/index.mjs.map +1 -0
- package/dist/node-BUnkdSsa.mjs +183 -0
- package/dist/node-BUnkdSsa.mjs.map +1 -0
- package/dist/node-io-BY1ZDLRQ.mjs +1566 -0
- package/dist/node-io-BY1ZDLRQ.mjs.map +1 -0
- package/dist/node.d.mts +30 -0
- package/dist/node.mjs +1 -0
- package/dist/path-JI87C4k8.d.mts +109 -0
- package/dist/path.d.mts +2 -0
- package/dist/path.mjs +245 -0
- package/dist/path.mjs.map +1 -0
- package/dist/size-D8rsQcV0.mjs +184 -0
- package/dist/size-D8rsQcV0.mjs.map +1 -0
- package/dist/utils-DCbu3a4w.mjs +27 -0
- package/dist/utils-DCbu3a4w.mjs.map +1 -0
- package/package.json +142 -4
- package/src/adapter.ts +240 -0
- package/src/adapters/express.ts +201 -0
- package/src/adapters/fastify.ts +280 -0
- package/src/adapters/hono.ts +488 -0
- package/src/adapters/node-io.ts +368 -0
- package/src/adapters/node.ts +135 -0
- package/src/adapters/shared.ts +508 -0
- package/src/body.ts +135 -0
- package/src/coerce.ts +195 -0
- package/src/compiler/analyze.ts +1513 -0
- package/src/compiler/ast.ts +90 -0
- package/src/compiler/coercion.ts +267 -0
- package/src/compiler/generate.ts +374 -0
- package/src/compiler/index.ts +76 -0
- package/src/compiler/model.ts +260 -0
- package/src/compiler/route-decorators.ts +405 -0
- package/src/compiler/validate.ts +288 -0
- package/src/context.ts +533 -0
- package/src/cookie-settings.ts +68 -0
- package/src/cookie-signer.ts +106 -0
- package/src/cookies.ts +726 -0
- package/src/cors.ts +349 -0
- package/src/decisions.ts +331 -0
- package/src/decorators.ts +486 -0
- package/src/event-channel.ts +197 -0
- package/src/exceptions.ts +257 -0
- package/src/files.ts +429 -0
- package/src/headers.ts +39 -0
- package/src/index.ts +185 -0
- package/src/inject-sse.ts +309 -0
- package/src/interfaces.ts +101 -0
- package/src/metadata.ts +162 -0
- package/src/mime.ts +157 -0
- package/src/multipart.ts +597 -0
- package/src/option-error.ts +31 -0
- package/src/path.ts +288 -0
- package/src/pipeline.ts +918 -0
- package/src/plugin-routes.ts +188 -0
- package/src/plugin.ts +1432 -0
- package/src/proxy.ts +236 -0
- package/src/query.ts +30 -0
- package/src/response.ts +240 -0
- package/src/router.ts +119 -0
- package/src/security-headers.ts +161 -0
- package/src/serialize.ts +264 -0
- package/src/size.ts +66 -0
- package/src/sse-parse.ts +194 -0
- package/src/sse.ts +650 -0
- package/src/standard-schema.ts +81 -0
- package/src/status.ts +78 -0
- package/src/stream.ts +200 -0
- package/src/tokens.ts +21 -0
- package/src/uploads.ts +176 -0
- package/src/utils.ts +31 -0
- package/src/validate.ts +65 -0
package/src/metadata.ts
ADDED
|
@@ -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
|
+
}
|