@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
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@znight/http/fastify`: serves Znight routes with [Fastify](https://fastify.dev) 5 (optional
|
|
3
|
+
* peer dependency `fastify`). Routing uses Fastify's router; body parsing, validation and
|
|
4
|
+
* serialisation stay Znight's, so responses are identical to the other adapters.
|
|
5
|
+
*/
|
|
6
|
+
import type {
|
|
7
|
+
AdapterCloseOptions,
|
|
8
|
+
AdapterHandler,
|
|
9
|
+
AdapterReply,
|
|
10
|
+
AdapterRequest,
|
|
11
|
+
AdapterRoute,
|
|
12
|
+
FallbackHandler,
|
|
13
|
+
HttpAdapter,
|
|
14
|
+
HttpServerAddress,
|
|
15
|
+
InjectRequest,
|
|
16
|
+
InjectResponse,
|
|
17
|
+
InjectStreamRequest,
|
|
18
|
+
InjectStreamResponse,
|
|
19
|
+
} from '../adapter.ts'
|
|
20
|
+
|
|
21
|
+
import { Readable } from 'node:stream'
|
|
22
|
+
import Fastify, {
|
|
23
|
+
type FastifyInstance,
|
|
24
|
+
type FastifyReply,
|
|
25
|
+
type FastifyRequest,
|
|
26
|
+
type FastifyServerOptions,
|
|
27
|
+
} from 'fastify'
|
|
28
|
+
|
|
29
|
+
import { toFastifyPath } from '../path.ts'
|
|
30
|
+
import { discardStream } from '../stream.ts'
|
|
31
|
+
import {
|
|
32
|
+
ConnectionTracker,
|
|
33
|
+
isEncrypted,
|
|
34
|
+
nodeBodyStream,
|
|
35
|
+
normalizeNodeHeaders,
|
|
36
|
+
onResponseAbort,
|
|
37
|
+
readNodeBody,
|
|
38
|
+
withNativeCookies,
|
|
39
|
+
writeNodeReply,
|
|
40
|
+
} from './node-io.ts'
|
|
41
|
+
import {
|
|
42
|
+
bufferInjectStream,
|
|
43
|
+
type LightInjectOptions,
|
|
44
|
+
lightInjectOptions,
|
|
45
|
+
lightStreamResponse,
|
|
46
|
+
serverAddress,
|
|
47
|
+
} from './shared.ts'
|
|
48
|
+
|
|
49
|
+
/** Options of {@link fastifyAdapter}. */
|
|
50
|
+
export interface FastifyAdapterOptions {
|
|
51
|
+
/**
|
|
52
|
+
* An existing instance (with your plugins and hooks). Znight registers its routes in an
|
|
53
|
+
* encapsulated child context (its own body handling and 404 handler) and closes the instance on
|
|
54
|
+
* shutdown. Malformed URLs are then answered by Fastify's `frameworkErrors` handler.
|
|
55
|
+
*/
|
|
56
|
+
readonly instance?: FastifyInstance
|
|
57
|
+
/**
|
|
58
|
+
* Options of the instance created when `instance` is absent. Znight sets `exposeHeadRoutes: false`
|
|
59
|
+
* (HEAD is served by Znight), a `maxParamLength` of 8 KiB and a `frameworkErrors` handler.
|
|
60
|
+
*/
|
|
61
|
+
readonly options?: FastifyServerOptions
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The Fastify adapter. */
|
|
65
|
+
export interface FastifyAdapter extends HttpAdapter {
|
|
66
|
+
readonly name: 'fastify'
|
|
67
|
+
/** The Fastify instance. */
|
|
68
|
+
readonly instance: FastifyInstance
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Creates a Fastify adapter.
|
|
73
|
+
*
|
|
74
|
+
* ```ts
|
|
75
|
+
* import { fastifyAdapter } from '@znight/http/fastify'
|
|
76
|
+
*
|
|
77
|
+
* HttpPlugin.configure({ adapter: fastifyAdapter({ options: { trustProxy: true } }) })
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
80
|
+
export function fastifyAdapter(options: FastifyAdapterOptions = {}): FastifyAdapter {
|
|
81
|
+
return new FastifyHttpAdapter(options)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const MAX_PARAM_LENGTH = 8192
|
|
85
|
+
|
|
86
|
+
/** `content-type` headers hidden from Fastify (see `#register`), by request. */
|
|
87
|
+
const HIDDEN_CONTENT_TYPES = new WeakMap<object, string>()
|
|
88
|
+
|
|
89
|
+
class FastifyHttpAdapter implements FastifyAdapter {
|
|
90
|
+
readonly name = 'fastify' as const
|
|
91
|
+
readonly instance: FastifyInstance
|
|
92
|
+
readonly #routes: AdapterRoute[] = []
|
|
93
|
+
readonly #tracker: ConnectionTracker
|
|
94
|
+
#fallback: FallbackHandler | undefined
|
|
95
|
+
#ready: Promise<void> | undefined
|
|
96
|
+
#closing: Promise<void> | undefined
|
|
97
|
+
|
|
98
|
+
constructor(options: FastifyAdapterOptions) {
|
|
99
|
+
if (options.instance === undefined) {
|
|
100
|
+
const serverOptions = options.options ?? {}
|
|
101
|
+
this.instance = Fastify({
|
|
102
|
+
exposeHeadRoutes: false,
|
|
103
|
+
frameworkErrors: (error, request, reply) => {
|
|
104
|
+
this.#reply(reply, this.#dispatchFallback(request, reply, error)).catch(() => undefined)
|
|
105
|
+
},
|
|
106
|
+
...serverOptions,
|
|
107
|
+
routerOptions: { maxParamLength: MAX_PARAM_LENGTH, ...serverOptions.routerOptions },
|
|
108
|
+
})
|
|
109
|
+
} else {
|
|
110
|
+
this.instance = options.instance
|
|
111
|
+
}
|
|
112
|
+
this.#tracker = new ConnectionTracker(this.instance.server)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
registerRoute(route: AdapterRoute): void {
|
|
116
|
+
if (this.#ready !== undefined) throw new Error('fastifyAdapter: routes must be registered before listen()/inject()')
|
|
117
|
+
this.#routes.push(route)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
setFallbackHandler(handler: FallbackHandler): void {
|
|
121
|
+
this.#fallback = handler
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
#prepare(): Promise<void> {
|
|
125
|
+
this.#ready ??= this.#register()
|
|
126
|
+
return this.#ready
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async #register(): Promise<void> {
|
|
130
|
+
const routes = this.#routes
|
|
131
|
+
await this.instance.register((child, _options, done) => {
|
|
132
|
+
// Znight reads and parses bodies itself: accept every content type without consuming it
|
|
133
|
+
child.removeAllContentTypeParsers()
|
|
134
|
+
child.addContentTypeParser('*', (_request, _payload, done) => done(null, undefined))
|
|
135
|
+
// Fastify answers 415 to a content type it cannot parse (`foo`, an empty one) before the route
|
|
136
|
+
// runs: hide such a header from Fastify (Znight reads it, and parses the body as bytes)
|
|
137
|
+
child.addHook('onRequest', (request, _reply, done) => {
|
|
138
|
+
const contentType = request.headers['content-type']
|
|
139
|
+
if (contentType !== undefined && request.mediaType === undefined) {
|
|
140
|
+
HIDDEN_CONTENT_TYPES.set(request.raw, contentType)
|
|
141
|
+
delete request.raw.headers['content-type']
|
|
142
|
+
}
|
|
143
|
+
done()
|
|
144
|
+
})
|
|
145
|
+
for (const route of routes) {
|
|
146
|
+
const handler = route.handler
|
|
147
|
+
child.route({
|
|
148
|
+
method: route.method,
|
|
149
|
+
url: toFastifyPath(route.path),
|
|
150
|
+
exposeHeadRoute: false,
|
|
151
|
+
handler: (request, reply) => this.#reply(reply, handler(adapterRequest(request, reply))),
|
|
152
|
+
})
|
|
153
|
+
}
|
|
154
|
+
child.setNotFoundHandler((request, reply) =>
|
|
155
|
+
this.#reply(reply, this.#dispatchFallback(request, reply, undefined)),
|
|
156
|
+
)
|
|
157
|
+
child.setErrorHandler((error, request, reply) =>
|
|
158
|
+
this.#reply(reply, this.#dispatchFallback(request, reply, error)),
|
|
159
|
+
)
|
|
160
|
+
done()
|
|
161
|
+
})
|
|
162
|
+
await this.instance.ready()
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
#dispatchFallback(request: FastifyRequest, reply: FastifyReply, error: unknown): Promise<AdapterReply> {
|
|
166
|
+
if (this.#fallback === undefined) return Promise.reject(error ?? new Error('No fallback handler'))
|
|
167
|
+
return this.#fallback(adapterRequest(request, undefined, reply), error)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async #reply(reply: FastifyReply, pending: ReturnType<AdapterHandler>): Promise<FastifyReply> {
|
|
171
|
+
let result: AdapterReply
|
|
172
|
+
try {
|
|
173
|
+
result = await pending
|
|
174
|
+
} catch {
|
|
175
|
+
result = {
|
|
176
|
+
status: 500,
|
|
177
|
+
headers: { 'content-type': 'application/json; charset=utf-8' },
|
|
178
|
+
body: new TextEncoder().encode('{"statusCode":500,"error":"Internal Server Error"}'),
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (result.raw === true || reply.sent || reply.raw.destroyed) {
|
|
182
|
+
discardStream(result.body instanceof ReadableStream ? result.body : null)
|
|
183
|
+
return reply
|
|
184
|
+
}
|
|
185
|
+
if (result.eventStream === true) {
|
|
186
|
+
// no onSend hook (compression, ETag...) may rewrite an event stream: write it on the raw
|
|
187
|
+
// response, with the headers Fastify hooks already set (`onRequest`...) under the plugin's,
|
|
188
|
+
// and their cookies before the plugin's (as reply.header('set-cookie') does on the other path)
|
|
189
|
+
const native = lowerCaseHeaders(reply.getHeaders())
|
|
190
|
+
const headers = { ...native, ...withNativeCookies(native['set-cookie'], result.headers) }
|
|
191
|
+
reply.hijack()
|
|
192
|
+
void writeNodeReply(reply.raw, { ...result, headers })
|
|
193
|
+
return reply
|
|
194
|
+
}
|
|
195
|
+
reply.code(result.status)
|
|
196
|
+
for (const name of Object.keys(result.headers)) reply.header(name, result.headers[name])
|
|
197
|
+
const body = result.body
|
|
198
|
+
if (body === null) return reply.send()
|
|
199
|
+
if (body instanceof Uint8Array) return reply.send(Buffer.from(body.buffer, body.byteOffset, body.byteLength))
|
|
200
|
+
const stream = body instanceof Blob ? body.stream() : body
|
|
201
|
+
return reply.send(Readable.fromWeb(stream as import('node:stream/web').ReadableStream))
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async listen(port: number, host: string): Promise<HttpServerAddress> {
|
|
205
|
+
await this.#prepare()
|
|
206
|
+
this.#closing = undefined
|
|
207
|
+
await this.instance.listen({ port, host })
|
|
208
|
+
const address = this.instance.server.address()
|
|
209
|
+
if (address === null || typeof address === 'string') return serverAddress(host, port)
|
|
210
|
+
return serverAddress(address.address, address.port, address.family)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
close(options: AdapterCloseOptions = {}): Promise<void> {
|
|
214
|
+
if (this.#closing === undefined) {
|
|
215
|
+
this.#tracker.closeIdle()
|
|
216
|
+
this.#closing = this.instance.close()
|
|
217
|
+
}
|
|
218
|
+
if (options.force === true) {
|
|
219
|
+
this.instance.server.closeAllConnections?.()
|
|
220
|
+
this.#tracker.destroyAll()
|
|
221
|
+
}
|
|
222
|
+
return this.#closing
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
inject(request: InjectRequest): Promise<InjectResponse> {
|
|
226
|
+
return bufferInjectStream(stream => this.injectStream(stream), request)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async injectStream(request: InjectStreamRequest): Promise<InjectStreamResponse> {
|
|
230
|
+
await this.#prepare()
|
|
231
|
+
const options: LightInjectOptions = await lightInjectOptions(request)
|
|
232
|
+
// Fastify forwards the options to light-my-request (`payloadAsStream`)
|
|
233
|
+
const inject = this.instance.inject as unknown as (options: LightInjectOptions) => Promise<unknown>
|
|
234
|
+
const response = await inject.call(this.instance, options)
|
|
235
|
+
return lightStreamResponse(response as Parameters<typeof lightStreamResponse>[0], request.signal)
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function lowerCaseHeaders(
|
|
240
|
+
headers: Readonly<Record<string, string | number | readonly string[] | undefined>>,
|
|
241
|
+
): Record<string, string | string[]> {
|
|
242
|
+
const result: Record<string, string | string[]> = {}
|
|
243
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
244
|
+
if (value !== undefined) result[name.toLowerCase()] = typeof value === 'object' ? [...value] : String(value)
|
|
245
|
+
}
|
|
246
|
+
return result
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function adapterRequest(
|
|
250
|
+
request: FastifyRequest,
|
|
251
|
+
reply: FastifyReply | undefined,
|
|
252
|
+
response: FastifyReply | undefined = reply,
|
|
253
|
+
): AdapterRequest {
|
|
254
|
+
const hidden = HIDDEN_CONTENT_TYPES.get(request.raw)
|
|
255
|
+
if (hidden !== undefined) {
|
|
256
|
+
// Fastify is done with the content type: restore it (for Znight and `@Req()`)
|
|
257
|
+
HIDDEN_CONTENT_TYPES.delete(request.raw)
|
|
258
|
+
request.raw.headers['content-type'] = hidden
|
|
259
|
+
}
|
|
260
|
+
let body: Promise<Uint8Array> | undefined
|
|
261
|
+
return {
|
|
262
|
+
method: request.raw.method ?? request.method,
|
|
263
|
+
url: request.raw.url ?? request.url,
|
|
264
|
+
headers: normalizeNodeHeaders(request.headers),
|
|
265
|
+
ip: request.ip,
|
|
266
|
+
remoteAddress: request.raw.socket?.remoteAddress,
|
|
267
|
+
encrypted: isEncrypted(request.raw),
|
|
268
|
+
raw: request,
|
|
269
|
+
rawResponse: reply,
|
|
270
|
+
readBody: (limit: number): Promise<Uint8Array> => {
|
|
271
|
+
body ??= readNodeBody(request.raw, limit)
|
|
272
|
+
return body
|
|
273
|
+
},
|
|
274
|
+
streamBody: (limit: number): ReadableStream<Uint8Array> => nodeBodyStream(request.raw, limit),
|
|
275
|
+
rawStatus: (): number | undefined => reply?.statusCode,
|
|
276
|
+
onAbort: (listener: () => void): void => {
|
|
277
|
+
if (response !== undefined) onResponseAbort(response.raw, listener)
|
|
278
|
+
},
|
|
279
|
+
}
|
|
280
|
+
}
|