@lovrozagar/honey 0.1.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.
- package/README.md +51 -0
- package/docs/api-ref/go.md +399 -0
- package/docs/api-ref/python.md +399 -0
- package/docs/api-ref/rust.md +399 -0
- package/docs/api-ref/typescript.md +399 -0
- package/docs/invalidate-spec.md +303 -0
- package/docs/sdk.md +722 -0
- package/package.json +120 -0
- package/src/accepts.ts +78 -0
- package/src/body-limit.ts +100 -0
- package/src/build/index.ts +174 -0
- package/src/cli-go/auth.go +15 -0
- package/src/cli-go/config.go +165 -0
- package/src/cli-go/errors.go +57 -0
- package/src/cli-go/multipart.go +55 -0
- package/src/cli-go/output.go +109 -0
- package/src/cli-go/root.go +30 -0
- package/src/cli-go/stream.go +88 -0
- package/src/cli-go/version.go +5 -0
- package/src/cli.ts +204 -0
- package/src/client/error.ts +27 -0
- package/src/client/http.ts +502 -0
- package/src/client/index.ts +127 -0
- package/src/client/sdk.ts +265 -0
- package/src/client/sse.ts +102 -0
- package/src/client/types.ts +211 -0
- package/src/client/ws.ts +115 -0
- package/src/client-go/errors.go +207 -0
- package/src/client-go/invalidation.go +373 -0
- package/src/client-go/realtime.go +317 -0
- package/src/client-go/result.go +20 -0
- package/src/client-go/runtime.go +413 -0
- package/src/client-go/sse.go +149 -0
- package/src/client-go/transport.go +452 -0
- package/src/client-go/ws.go +104 -0
- package/src/client-mcp/README.md +10 -0
- package/src/client-mcp/server.ts +94 -0
- package/src/client-mcp/types.ts +19 -0
- package/src/client-python/_errors.py +115 -0
- package/src/client-python/_invalidation.py +578 -0
- package/src/client-python/_realtime.py +156 -0
- package/src/client-python/_runtime.py +469 -0
- package/src/client-python/_sse.py +99 -0
- package/src/client-python/_transport.py +335 -0
- package/src/client-python/_ws.py +131 -0
- package/src/client-python/py.typed +0 -0
- package/src/client-rust/errors.rs +200 -0
- package/src/client-rust/invalidation.rs +441 -0
- package/src/client-rust/invalidation_sync.rs +306 -0
- package/src/client-rust/realtime.rs +695 -0
- package/src/client-rust/result.rs +22 -0
- package/src/client-rust/runtime.rs +547 -0
- package/src/client-rust/runtime_sync.rs +452 -0
- package/src/client-rust/sse.rs +167 -0
- package/src/client-rust/ws.rs +143 -0
- package/src/cloudflare/realtime.ts +236 -0
- package/src/codegen-go-cli.ts +1180 -0
- package/src/codegen-go.ts +1010 -0
- package/src/codegen-hash.ts +8 -0
- package/src/codegen-ir.ts +517 -0
- package/src/codegen-loaders.ts +53 -0
- package/src/codegen-mcp.ts +364 -0
- package/src/codegen-python.ts +1028 -0
- package/src/codegen-rust.ts +1652 -0
- package/src/codegen-sanitize.ts +67 -0
- package/src/codegen-schema-naming.ts +146 -0
- package/src/codegen.ts +5015 -0
- package/src/context.ts +210 -0
- package/src/cookie-sign.ts +58 -0
- package/src/cookie.ts +89 -0
- package/src/cors.ts +96 -0
- package/src/crypto.ts +19 -0
- package/src/csrf.ts +54 -0
- package/src/curl-logger.ts +235 -0
- package/src/detect-runtime.ts +19 -0
- package/src/error.ts +52 -0
- package/src/errors.ts +87 -0
- package/src/etag.ts +74 -0
- package/src/feature-load.ts +23 -0
- package/src/go-type-emitter.ts +404 -0
- package/src/honey-response.ts +354 -0
- package/src/i18n-slot.ts +20 -0
- package/src/i18n.ts +234 -0
- package/src/index.ts +3843 -0
- package/src/init.ts +149 -0
- package/src/input.ts +8 -0
- package/src/ip-restrict.ts +192 -0
- package/src/logger.ts +149 -0
- package/src/middleware.ts +199 -0
- package/src/node-request.ts +240 -0
- package/src/node.ts +212 -0
- package/src/openapi/register.ts +19 -0
- package/src/openapi/scalar.ts +33 -0
- package/src/openapi/spec-factory.ts +46 -0
- package/src/openapi/spec.ts +31 -0
- package/src/openapi/swagger.ts +34 -0
- package/src/plugin.ts +771 -0
- package/src/powered-by.ts +17 -0
- package/src/pretty-json.ts +39 -0
- package/src/proxy.ts +155 -0
- package/src/python-type-emitter.ts +144 -0
- package/src/realtime/buffer.ts +101 -0
- package/src/realtime/bus.ts +93 -0
- package/src/realtime/longpoll.ts +148 -0
- package/src/realtime/protocol.ts +128 -0
- package/src/realtime/route.ts +104 -0
- package/src/realtime/runtime.ts +430 -0
- package/src/request-id.ts +23 -0
- package/src/request-to-curl.ts +34 -0
- package/src/response.ts +414 -0
- package/src/rust-type-emitter.ts +412 -0
- package/src/secure-headers.ts +69 -0
- package/src/serve-register.ts +8 -0
- package/src/serve-slot.ts +20 -0
- package/src/serve.ts +141 -0
- package/src/server-timing.ts +66 -0
- package/src/static.ts +60 -0
- package/src/telemetry/otel.ts +139 -0
- package/src/testing.ts +114 -0
- package/src/timeout.ts +36 -0
- package/src/tree.ts +464 -0
- package/src/ts-type-emitter.ts +106 -0
- package/src/type-emitter.ts +524 -0
- package/src/type-extractor.ts +749 -0
- package/src/types.ts +638 -0
- package/src/validation.ts +300 -0
- package/src/ws/bun.ts +89 -0
- package/src/ws/cloudflare.ts +133 -0
- package/src/ws/deno.ts +109 -0
- package/src/ws/node.ts +161 -0
- package/src/yaml.ts +80 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import type { IncomingMessage } from "node:http"
|
|
2
|
+
import { Readable } from "node:stream"
|
|
3
|
+
import { NODE_OUTBOUND } from "./honey-response.ts"
|
|
4
|
+
|
|
5
|
+
/** bodyLimit uses this to swap the inbound stream without `new Request(req)`. */
|
|
6
|
+
export const REPLACE_BODY = Symbol.for("honey.replaceBody")
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Headers view over IncomingMessage. `get`/`has` read Node's already-parsed
|
|
10
|
+
* map. A real `Headers` is built only if something iterates or mutates.
|
|
11
|
+
*/
|
|
12
|
+
export class NodeHeaders {
|
|
13
|
+
#incoming: IncomingMessage
|
|
14
|
+
#native: Headers | null = null
|
|
15
|
+
|
|
16
|
+
constructor(incoming: IncomingMessage) {
|
|
17
|
+
this.#incoming = incoming
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
append(name: string, value: string): void {
|
|
21
|
+
this.#ensureNative().append(name, value)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
delete(name: string): void {
|
|
25
|
+
this.#ensureNative().delete(name)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
get(name: string): string | null {
|
|
29
|
+
if (this.#native) return this.#native.get(name)
|
|
30
|
+
const raw = this.#incoming.headers[name.toLowerCase()]
|
|
31
|
+
if (raw === undefined) return null
|
|
32
|
+
return Array.isArray(raw) ? raw.join(", ") : raw
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
getSetCookie(): string[] {
|
|
36
|
+
return this.#ensureNative().getSetCookie()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
has(name: string): boolean {
|
|
40
|
+
if (this.#native) return this.#native.has(name)
|
|
41
|
+
return this.#incoming.headers[name.toLowerCase()] !== undefined
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
set(name: string, value: string): void {
|
|
45
|
+
this.#ensureNative().set(name, value)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
forEach(callback: (value: string, key: string, parent: Headers) => void, thisArg?: unknown): void {
|
|
49
|
+
this.#ensureNative().forEach(callback, thisArg)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
keys(): IterableIterator<string> {
|
|
53
|
+
return this.#ensureNative().keys()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
values(): IterableIterator<string> {
|
|
57
|
+
return this.#ensureNative().values()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
entries(): IterableIterator<[string, string]> {
|
|
61
|
+
return this.#ensureNative().entries()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
[Symbol.iterator](): IterableIterator<[string, string]> {
|
|
65
|
+
return this.#ensureNative()[Symbol.iterator]()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
get [Symbol.toStringTag](): string {
|
|
69
|
+
return "Headers"
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
#ensureNative(): Headers {
|
|
73
|
+
if (this.#native) return this.#native
|
|
74
|
+
const headers = new Headers()
|
|
75
|
+
const raw = this.#incoming.rawHeaders
|
|
76
|
+
for (let i = 0; i < raw.length; i += 2) {
|
|
77
|
+
const name = raw[i]
|
|
78
|
+
if (name.charCodeAt(0) === 58) continue
|
|
79
|
+
headers.append(name, raw[i + 1])
|
|
80
|
+
}
|
|
81
|
+
this.#native = headers
|
|
82
|
+
return headers
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Request-shaped wrapper around IncomingMessage. Avoids `new Request()`
|
|
88
|
+
* until a body method needs the real Fetch object (formData / blob / clone).
|
|
89
|
+
*/
|
|
90
|
+
export class NodeRequest {
|
|
91
|
+
readonly [NODE_OUTBOUND] = true
|
|
92
|
+
readonly cache = "default" as RequestCache
|
|
93
|
+
readonly credentials = "same-origin" as RequestCredentials
|
|
94
|
+
readonly destination = "" as RequestDestination
|
|
95
|
+
readonly headers: NodeHeaders
|
|
96
|
+
readonly integrity = ""
|
|
97
|
+
readonly keepalive = false
|
|
98
|
+
readonly method: string
|
|
99
|
+
readonly mode = "cors" as RequestMode
|
|
100
|
+
readonly redirect = "follow" as RequestRedirect
|
|
101
|
+
readonly referrer = "about:client"
|
|
102
|
+
readonly referrerPolicy = "" as ReferrerPolicy
|
|
103
|
+
readonly url: string
|
|
104
|
+
|
|
105
|
+
#incoming: IncomingMessage
|
|
106
|
+
#hasBody: boolean
|
|
107
|
+
#webBody: ReadableStream<Uint8Array> | null | undefined
|
|
108
|
+
#bodyUsed = false
|
|
109
|
+
#fetch: Request | null = null
|
|
110
|
+
#signal: AbortSignal | null = null
|
|
111
|
+
|
|
112
|
+
constructor(incoming: IncomingMessage) {
|
|
113
|
+
this.#incoming = incoming
|
|
114
|
+
const host = incoming.headers.host ?? "localhost"
|
|
115
|
+
this.url = `http://${host}${incoming.url ?? "/"}`
|
|
116
|
+
this.method = (incoming.method ?? "GET").toUpperCase()
|
|
117
|
+
this.headers = new NodeHeaders(incoming)
|
|
118
|
+
this.#hasBody = this.method !== "GET" && this.method !== "HEAD"
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
get body(): ReadableStream<Uint8Array> | null {
|
|
122
|
+
if (!this.#hasBody) return null
|
|
123
|
+
if (this.#webBody === undefined) {
|
|
124
|
+
this.#webBody = Readable.toWeb(this.#incoming) as ReadableStream<Uint8Array>
|
|
125
|
+
}
|
|
126
|
+
return this.#webBody
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
get bodyUsed(): boolean {
|
|
130
|
+
return this.#bodyUsed || this.#incoming.readableEnded
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
get signal(): AbortSignal {
|
|
134
|
+
if (this.#signal) return this.#signal
|
|
135
|
+
const ac = new AbortController()
|
|
136
|
+
this.#incoming.once("aborted", () => ac.abort())
|
|
137
|
+
this.#signal = ac.signal
|
|
138
|
+
return this.#signal
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
get duplex(): "half" {
|
|
142
|
+
return "half"
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
[REPLACE_BODY](stream: ReadableStream<Uint8Array>): void {
|
|
146
|
+
this.#webBody = stream
|
|
147
|
+
this.#hasBody = true
|
|
148
|
+
this.#fetch = null
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async arrayBuffer(): Promise<ArrayBuffer> {
|
|
152
|
+
const buf = await this.#readIncoming()
|
|
153
|
+
return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength) as ArrayBuffer
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async blob(): Promise<Blob> {
|
|
157
|
+
return this.#asFetch().blob()
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async bytes(): Promise<Uint8Array> {
|
|
161
|
+
const buf = await this.#readIncoming()
|
|
162
|
+
return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
clone(): Request {
|
|
166
|
+
return this.#asFetch().clone()
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async formData(): Promise<FormData> {
|
|
170
|
+
return this.#asFetch().formData()
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async json(): Promise<unknown> {
|
|
174
|
+
const text = await this.text()
|
|
175
|
+
return text.length === 0 ? null : JSON.parse(text)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async text(): Promise<string> {
|
|
179
|
+
const buf = await this.#readIncoming()
|
|
180
|
+
return buf.toString("utf8")
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
#asFetch(): Request {
|
|
184
|
+
if (this.#fetch) return this.#fetch
|
|
185
|
+
this.#fetch = new Request(this.url, {
|
|
186
|
+
body: this.body,
|
|
187
|
+
duplex: this.#hasBody ? "half" : undefined,
|
|
188
|
+
headers: this.#incoming.headers as HeadersInit,
|
|
189
|
+
method: this.method,
|
|
190
|
+
signal: this.signal,
|
|
191
|
+
} as RequestInit)
|
|
192
|
+
this.#bodyUsed = true
|
|
193
|
+
return this.#fetch
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async #readIncoming(): Promise<Buffer> {
|
|
197
|
+
this.#bodyUsed = true
|
|
198
|
+
if (!this.#hasBody) return Buffer.alloc(0)
|
|
199
|
+
if (this.#fetch) return Buffer.from(await this.#fetch.arrayBuffer())
|
|
200
|
+
if (this.#webBody) return streamToBuffer(this.#webBody)
|
|
201
|
+
const chunks: Buffer[] = []
|
|
202
|
+
for await (const chunk of this.#incoming) {
|
|
203
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk)
|
|
204
|
+
}
|
|
205
|
+
return chunks.length === 1 ? chunks[0] : Buffer.concat(chunks)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function streamToBuffer(stream: ReadableStream<Uint8Array>): Promise<Buffer> {
|
|
210
|
+
const reader = stream.getReader()
|
|
211
|
+
const chunks: Buffer[] = []
|
|
212
|
+
while (true) {
|
|
213
|
+
const { done, value } = await reader.read()
|
|
214
|
+
if (done) break
|
|
215
|
+
chunks.push(Buffer.from(value))
|
|
216
|
+
}
|
|
217
|
+
return chunks.length === 1 ? chunks[0] : Buffer.concat(chunks)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const REQUEST_HAS_INSTANCE = Symbol.for("honey.requestHasInstance")
|
|
221
|
+
|
|
222
|
+
function installRequestHasInstance(): void {
|
|
223
|
+
const ctor = Request as typeof Request & { [REQUEST_HAS_INSTANCE]?: boolean }
|
|
224
|
+
if (ctor[REQUEST_HAS_INSTANCE]) return
|
|
225
|
+
const original = Request[Symbol.hasInstance]
|
|
226
|
+
Object.defineProperty(Request, Symbol.hasInstance, {
|
|
227
|
+
configurable: true,
|
|
228
|
+
value(this: Function, value: unknown) {
|
|
229
|
+
if (this === Request && value instanceof NodeRequest) return true
|
|
230
|
+
return original.call(this, value)
|
|
231
|
+
},
|
|
232
|
+
})
|
|
233
|
+
ctor[REQUEST_HAS_INSTANCE] = true
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
installRequestHasInstance()
|
|
237
|
+
|
|
238
|
+
export function incomingToNodeRequest(req: IncomingMessage): Request {
|
|
239
|
+
return new NodeRequest(req) as unknown as Request
|
|
240
|
+
}
|
package/src/node.ts
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import type { IncomingMessage, Server, ServerResponse } from "node:http"
|
|
2
|
+
import { createServer } from "node:http"
|
|
3
|
+
import { Readable, type Duplex } from "node:stream"
|
|
4
|
+
import { pipeline } from "node:stream/promises"
|
|
5
|
+
import type { Honey } from "./index.ts"
|
|
6
|
+
import { isHoneyResponse } from "./honey-response.ts"
|
|
7
|
+
import { incomingToNodeRequest } from "./node-request.ts"
|
|
8
|
+
|
|
9
|
+
type ServeOptions<TEnv> = {
|
|
10
|
+
env: TEnv
|
|
11
|
+
hostname?: string
|
|
12
|
+
port?: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Buffer known-size bodies up to this many bytes, then `res.end(buf)`. */
|
|
16
|
+
const BUFFER_BODY_MAX = 256 * 1024
|
|
17
|
+
const RAW_BODY = Symbol.for("honey.rawBody")
|
|
18
|
+
|
|
19
|
+
function incomingToRequest(req: IncomingMessage): Request {
|
|
20
|
+
return incomingToNodeRequest(req)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function collectNodeHeaders(
|
|
24
|
+
response: Response,
|
|
25
|
+
extra?: Record<string, string>,
|
|
26
|
+
): Record<string, string | string[]> {
|
|
27
|
+
const headerObj: Record<string, string | string[]> = {}
|
|
28
|
+
let hasSetCookie = false
|
|
29
|
+
response.headers.forEach((value, key) => {
|
|
30
|
+
if (key === "set-cookie") {
|
|
31
|
+
hasSetCookie = true
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
headerObj[key] = value
|
|
35
|
+
})
|
|
36
|
+
if (hasSetCookie) {
|
|
37
|
+
const setCookies = response.headers.getSetCookie()
|
|
38
|
+
if (setCookies.length > 0) headerObj["set-cookie"] = setCookies
|
|
39
|
+
}
|
|
40
|
+
if (extra) Object.assign(headerObj, extra)
|
|
41
|
+
return headerObj
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function shouldBufferBody(response: Response): boolean {
|
|
45
|
+
const rawLen = response.headers.get("content-length")
|
|
46
|
+
if (rawLen !== null) {
|
|
47
|
+
const len = Number(rawLen)
|
|
48
|
+
return Number.isFinite(len) && len <= BUFFER_BODY_MAX
|
|
49
|
+
}
|
|
50
|
+
const ct = response.headers.get("content-type") ?? ""
|
|
51
|
+
if (ct.startsWith("text/event-stream")) return false
|
|
52
|
+
return (
|
|
53
|
+
ct.startsWith("application/json") ||
|
|
54
|
+
ct.startsWith("text/plain") ||
|
|
55
|
+
ct.startsWith("text/html") ||
|
|
56
|
+
ct.startsWith("text/csv") ||
|
|
57
|
+
ct.startsWith("application/xml")
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function rawBodyOf(response: Response): string | Uint8Array | undefined {
|
|
62
|
+
return (response as Response & { [RAW_BODY]?: string | Uint8Array })[RAW_BODY]
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function responseToNode(response: Response, res: ServerResponse): Promise<void> {
|
|
66
|
+
if (isHoneyResponse(response)) {
|
|
67
|
+
const raw = response.rawBody
|
|
68
|
+
if (typeof raw === "string" || raw instanceof Uint8Array) {
|
|
69
|
+
const byteLength = typeof raw === "string" ? Buffer.byteLength(raw) : raw.byteLength
|
|
70
|
+
res.writeHead(response.status, { ...response.plainHeaders, "content-length": String(byteLength) })
|
|
71
|
+
res.end(raw)
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
const stream = response.body
|
|
75
|
+
if (stream === null) {
|
|
76
|
+
res.writeHead(response.status, response.plainHeaders)
|
|
77
|
+
res.end()
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
res.writeHead(response.status, response.plainHeaders)
|
|
81
|
+
try {
|
|
82
|
+
await pipeline(Readable.fromWeb(stream as import("node:stream/web").ReadableStream), res)
|
|
83
|
+
} catch {
|
|
84
|
+
if (!res.destroyed) res.destroy()
|
|
85
|
+
}
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const raw = rawBodyOf(response)
|
|
90
|
+
if (typeof raw === "string" || raw instanceof Uint8Array) {
|
|
91
|
+
const byteLength = typeof raw === "string" ? Buffer.byteLength(raw) : raw.byteLength
|
|
92
|
+
res.writeHead(response.status, collectNodeHeaders(response, { "content-length": String(byteLength) }))
|
|
93
|
+
res.end(raw)
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const body = response.body
|
|
98
|
+
if (body === null) {
|
|
99
|
+
res.writeHead(response.status, collectNodeHeaders(response))
|
|
100
|
+
res.end()
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (shouldBufferBody(response)) {
|
|
105
|
+
const buf = Buffer.from(await response.arrayBuffer())
|
|
106
|
+
if (res.destroyed) return
|
|
107
|
+
res.writeHead(
|
|
108
|
+
response.status,
|
|
109
|
+
collectNodeHeaders(response, { "content-length": String(buf.byteLength) }),
|
|
110
|
+
)
|
|
111
|
+
res.end(buf)
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
res.writeHead(response.status, collectNodeHeaders(response))
|
|
116
|
+
try {
|
|
117
|
+
await pipeline(Readable.fromWeb(body as import("node:stream/web").ReadableStream), res)
|
|
118
|
+
} catch {
|
|
119
|
+
if (!res.destroyed) res.destroy()
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export type HoneyServer = Server & {
|
|
124
|
+
shutdown(timeout?: number): Promise<void>
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function serve<TEnv>(
|
|
128
|
+
app: Honey<TEnv, unknown, unknown, unknown, unknown, string, string>,
|
|
129
|
+
options: ServeOptions<TEnv>,
|
|
130
|
+
): HoneyServer {
|
|
131
|
+
const env = options.env
|
|
132
|
+
let inflight = 0
|
|
133
|
+
let draining = false
|
|
134
|
+
let drainResolve: (() => void) | null = null
|
|
135
|
+
|
|
136
|
+
const server = createServer(async (req, res) => {
|
|
137
|
+
if (draining) {
|
|
138
|
+
res.writeHead(503)
|
|
139
|
+
res.end("Service Unavailable")
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
inflight++
|
|
143
|
+
try {
|
|
144
|
+
const request = incomingToRequest(req)
|
|
145
|
+
const maybe = app.fetch(request, env)
|
|
146
|
+
const response = maybe instanceof Promise ? await maybe : maybe
|
|
147
|
+
await responseToNode(response, res)
|
|
148
|
+
} catch {
|
|
149
|
+
if (!res.headersSent) {
|
|
150
|
+
res.writeHead(500)
|
|
151
|
+
res.end("Internal Server Error")
|
|
152
|
+
} else if (!res.destroyed) {
|
|
153
|
+
res.destroy()
|
|
154
|
+
}
|
|
155
|
+
} finally {
|
|
156
|
+
inflight--
|
|
157
|
+
if (draining && inflight === 0) {
|
|
158
|
+
drainResolve?.()
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
/* WebSocket upgrade handling */
|
|
164
|
+
server.on("upgrade", async (req: IncomingMessage, socket: Duplex, head: Buffer) => {
|
|
165
|
+
try {
|
|
166
|
+
const request = incomingToRequest(req)
|
|
167
|
+
const envWithUpgrade = { ...env, __nodeUpgrade: { head, req, socket } } as TEnv
|
|
168
|
+
const maybe = app.fetch(request, envWithUpgrade)
|
|
169
|
+
const response = maybe instanceof Promise ? await maybe : maybe
|
|
170
|
+
|
|
171
|
+
if (response.status !== 101) {
|
|
172
|
+
const text = await response.text()
|
|
173
|
+
socket.write(
|
|
174
|
+
`HTTP/1.1 ${response.status} ${response.statusText ?? "Error"}\r\n` +
|
|
175
|
+
"content-type: application/json\r\n" +
|
|
176
|
+
`\r\n${text}`,
|
|
177
|
+
)
|
|
178
|
+
socket.destroy()
|
|
179
|
+
}
|
|
180
|
+
} catch {
|
|
181
|
+
socket.write("HTTP/1.1 500 Internal Server Error\r\n\r\n")
|
|
182
|
+
socket.destroy()
|
|
183
|
+
}
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
server.listen(options?.port ?? 0, options?.hostname)
|
|
187
|
+
|
|
188
|
+
const honeyServer = server as HoneyServer
|
|
189
|
+
honeyServer.shutdown = (timeout?: number): Promise<void> => {
|
|
190
|
+
draining = true
|
|
191
|
+
|
|
192
|
+
return new Promise<void>((resolve) => {
|
|
193
|
+
server.close(() => {
|
|
194
|
+
if (inflight === 0) {
|
|
195
|
+
resolve()
|
|
196
|
+
return
|
|
197
|
+
}
|
|
198
|
+
drainResolve = resolve
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
if (timeout !== undefined) {
|
|
202
|
+
setTimeout(() => {
|
|
203
|
+
drainResolve = null
|
|
204
|
+
server.closeAllConnections()
|
|
205
|
+
resolve()
|
|
206
|
+
}, timeout)
|
|
207
|
+
}
|
|
208
|
+
})
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return honeyServer
|
|
212
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { generateManifest, generateOpenApi } from "../codegen.ts"
|
|
2
|
+
import { toYaml } from "../yaml.ts"
|
|
3
|
+
import { scalar } from "./scalar.ts"
|
|
4
|
+
import { swagger } from "./swagger.ts"
|
|
5
|
+
import { registerOpenApiRuntime } from "./spec-factory.ts"
|
|
6
|
+
|
|
7
|
+
export function enableOpenApi(): void {
|
|
8
|
+
registerOpenApiRuntime({
|
|
9
|
+
docsUi: (kind, specUrl) =>
|
|
10
|
+
kind === "swagger" ? swagger({ url: specUrl }) : scalar({ url: specUrl }),
|
|
11
|
+
generateManifest: (app) => Promise.resolve(generateManifest(app as never)),
|
|
12
|
+
generateOpenApi: (app, options) => generateOpenApi(app as never, options),
|
|
13
|
+
toYaml,
|
|
14
|
+
})
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
enableOpenApi()
|
|
18
|
+
|
|
19
|
+
export { spec } from "./spec.ts"
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { TypedResponse } from "../response.ts"
|
|
2
|
+
|
|
3
|
+
type ScalarOptions = {
|
|
4
|
+
url: string
|
|
5
|
+
[key: string]: unknown
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function scalar(
|
|
9
|
+
options: ScalarOptions,
|
|
10
|
+
): (ctx: { res: { html(sk: "ok", body: string): TypedResponse } }) => TypedResponse {
|
|
11
|
+
const { url, ...rest } = options
|
|
12
|
+
const config = JSON.stringify({ url, ...rest })
|
|
13
|
+
|
|
14
|
+
const html = `<!DOCTYPE html>
|
|
15
|
+
<html>
|
|
16
|
+
<head>
|
|
17
|
+
<meta charset="utf-8" />
|
|
18
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
19
|
+
<title>API Reference</title>
|
|
20
|
+
</head>
|
|
21
|
+
<body>
|
|
22
|
+
<div id="app"></div>
|
|
23
|
+
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
|
|
24
|
+
<script>
|
|
25
|
+
Scalar.createApiReference(document.getElementById("app"), ${config})
|
|
26
|
+
</script>
|
|
27
|
+
</body>
|
|
28
|
+
</html>`
|
|
29
|
+
|
|
30
|
+
const handler = (ctx: { res: { html(sk: "ok", body: string): TypedResponse } }) => ctx.res.html("ok", html)
|
|
31
|
+
Object.defineProperty(handler, Symbol.for("honey.internal"), { value: true })
|
|
32
|
+
return handler
|
|
33
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** Slot for runtime spec/manifest generation. Filled by `import "@lovrozagar/honey/openapi"`. */
|
|
2
|
+
|
|
3
|
+
export type OpenApiGenerate = (
|
|
4
|
+
app: unknown,
|
|
5
|
+
options: {
|
|
6
|
+
filterRoutes?: (route: { meta: unknown; method: string; path: string }) => boolean
|
|
7
|
+
info: { description?: string; title: string; version: string }
|
|
8
|
+
securitySchemes?: Record<string, unknown>
|
|
9
|
+
},
|
|
10
|
+
) => Promise<unknown>
|
|
11
|
+
|
|
12
|
+
export type ManifestGenerate = (app: unknown) => Promise<unknown>
|
|
13
|
+
|
|
14
|
+
export type YamlGenerate = (value: unknown) => string
|
|
15
|
+
|
|
16
|
+
import type { HoneyRes } from "../response.ts"
|
|
17
|
+
|
|
18
|
+
export type DocsUi = (
|
|
19
|
+
kind: "scalar" | "swagger",
|
|
20
|
+
specUrl: string,
|
|
21
|
+
) => (ctx: { res: HoneyRes }) => Response | Promise<Response>
|
|
22
|
+
|
|
23
|
+
export type OpenApiRuntime = {
|
|
24
|
+
docsUi: DocsUi
|
|
25
|
+
generateManifest: ManifestGenerate
|
|
26
|
+
generateOpenApi: OpenApiGenerate
|
|
27
|
+
toYaml: YamlGenerate
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const MISSING =
|
|
31
|
+
'Runtime OpenAPI/manifest generation requires `import "@lovrozagar/honey/openapi"` in the app entry.'
|
|
32
|
+
|
|
33
|
+
let runtime: OpenApiRuntime | undefined
|
|
34
|
+
|
|
35
|
+
export function registerOpenApiRuntime(next: OpenApiRuntime): void {
|
|
36
|
+
runtime = next
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function resetOpenApiRuntime(): void {
|
|
40
|
+
runtime = undefined
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function getOpenApiRuntime(): OpenApiRuntime {
|
|
44
|
+
if (!runtime) throw new Error(MISSING)
|
|
45
|
+
return runtime
|
|
46
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { generateOpenApi } from "../codegen.ts"
|
|
2
|
+
import type { OpenApiRouteInfo } from "../codegen.ts"
|
|
3
|
+
import type { DefaultMeta } from "../types.ts"
|
|
4
|
+
import type { TypedResponse } from "../response.ts"
|
|
5
|
+
|
|
6
|
+
type SpecOptions<TMeta = Record<string, unknown> | null> = {
|
|
7
|
+
description?: string
|
|
8
|
+
filterRoutes?: (route: OpenApiRouteInfo<DefaultMeta & TMeta>) => boolean
|
|
9
|
+
securitySchemes?: Record<string, unknown>
|
|
10
|
+
title: string
|
|
11
|
+
version: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function spec<TMeta = Record<string, unknown> | null>(
|
|
15
|
+
options: SpecOptions<TMeta>,
|
|
16
|
+
): (ctx: { res: { json(sk: "ok", data: unknown): TypedResponse } }) => TypedResponse | Promise<TypedResponse> {
|
|
17
|
+
let cached: string | null = null
|
|
18
|
+
|
|
19
|
+
const handler = async (ctx: { res: { json(sk: "ok", data: unknown): TypedResponse } }) => {
|
|
20
|
+
if (cached === null) {
|
|
21
|
+
const desc = Object.getOwnPropertyDescriptor(handler, Symbol.for("honey.app"))
|
|
22
|
+
const openApiSpec = desc
|
|
23
|
+
? await generateOpenApi(desc.value, { filterRoutes: options.filterRoutes, info: options, securitySchemes: options.securitySchemes })
|
|
24
|
+
: {}
|
|
25
|
+
cached = JSON.stringify(openApiSpec)
|
|
26
|
+
}
|
|
27
|
+
return ctx.res.json("ok", JSON.parse(cached))
|
|
28
|
+
}
|
|
29
|
+
Object.defineProperty(handler, Symbol.for("honey.internal"), { value: true })
|
|
30
|
+
return handler
|
|
31
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { TypedResponse } from "../response.ts"
|
|
2
|
+
|
|
3
|
+
type SwaggerOptions = {
|
|
4
|
+
url: string
|
|
5
|
+
[key: string]: unknown
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function swagger(
|
|
9
|
+
options: SwaggerOptions,
|
|
10
|
+
): (ctx: { res: { html(sk: "ok", body: string): TypedResponse } }) => TypedResponse {
|
|
11
|
+
const { url, ...rest } = options
|
|
12
|
+
const config = JSON.stringify({ dom_id: "#swagger-ui", url, ...rest })
|
|
13
|
+
|
|
14
|
+
const html = `<!DOCTYPE html>
|
|
15
|
+
<html>
|
|
16
|
+
<head>
|
|
17
|
+
<meta charset="utf-8" />
|
|
18
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
19
|
+
<title>Swagger UI</title>
|
|
20
|
+
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist/swagger-ui.css" />
|
|
21
|
+
</head>
|
|
22
|
+
<body>
|
|
23
|
+
<div id="swagger-ui"></div>
|
|
24
|
+
<script src="https://unpkg.com/swagger-ui-dist/swagger-ui-bundle.js"></script>
|
|
25
|
+
<script>
|
|
26
|
+
SwaggerUIBundle(${config})
|
|
27
|
+
</script>
|
|
28
|
+
</body>
|
|
29
|
+
</html>`
|
|
30
|
+
|
|
31
|
+
const handler = (ctx: { res: { html(sk: "ok", body: string): TypedResponse } }) => ctx.res.html("ok", html)
|
|
32
|
+
Object.defineProperty(handler, Symbol.for("honey.internal"), { value: true })
|
|
33
|
+
return handler
|
|
34
|
+
}
|