@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.
Files changed (131) hide show
  1. package/README.md +51 -0
  2. package/docs/api-ref/go.md +399 -0
  3. package/docs/api-ref/python.md +399 -0
  4. package/docs/api-ref/rust.md +399 -0
  5. package/docs/api-ref/typescript.md +399 -0
  6. package/docs/invalidate-spec.md +303 -0
  7. package/docs/sdk.md +722 -0
  8. package/package.json +120 -0
  9. package/src/accepts.ts +78 -0
  10. package/src/body-limit.ts +100 -0
  11. package/src/build/index.ts +174 -0
  12. package/src/cli-go/auth.go +15 -0
  13. package/src/cli-go/config.go +165 -0
  14. package/src/cli-go/errors.go +57 -0
  15. package/src/cli-go/multipart.go +55 -0
  16. package/src/cli-go/output.go +109 -0
  17. package/src/cli-go/root.go +30 -0
  18. package/src/cli-go/stream.go +88 -0
  19. package/src/cli-go/version.go +5 -0
  20. package/src/cli.ts +204 -0
  21. package/src/client/error.ts +27 -0
  22. package/src/client/http.ts +502 -0
  23. package/src/client/index.ts +127 -0
  24. package/src/client/sdk.ts +265 -0
  25. package/src/client/sse.ts +102 -0
  26. package/src/client/types.ts +211 -0
  27. package/src/client/ws.ts +115 -0
  28. package/src/client-go/errors.go +207 -0
  29. package/src/client-go/invalidation.go +373 -0
  30. package/src/client-go/realtime.go +317 -0
  31. package/src/client-go/result.go +20 -0
  32. package/src/client-go/runtime.go +413 -0
  33. package/src/client-go/sse.go +149 -0
  34. package/src/client-go/transport.go +452 -0
  35. package/src/client-go/ws.go +104 -0
  36. package/src/client-mcp/README.md +10 -0
  37. package/src/client-mcp/server.ts +94 -0
  38. package/src/client-mcp/types.ts +19 -0
  39. package/src/client-python/_errors.py +115 -0
  40. package/src/client-python/_invalidation.py +578 -0
  41. package/src/client-python/_realtime.py +156 -0
  42. package/src/client-python/_runtime.py +469 -0
  43. package/src/client-python/_sse.py +99 -0
  44. package/src/client-python/_transport.py +335 -0
  45. package/src/client-python/_ws.py +131 -0
  46. package/src/client-python/py.typed +0 -0
  47. package/src/client-rust/errors.rs +200 -0
  48. package/src/client-rust/invalidation.rs +441 -0
  49. package/src/client-rust/invalidation_sync.rs +306 -0
  50. package/src/client-rust/realtime.rs +695 -0
  51. package/src/client-rust/result.rs +22 -0
  52. package/src/client-rust/runtime.rs +547 -0
  53. package/src/client-rust/runtime_sync.rs +452 -0
  54. package/src/client-rust/sse.rs +167 -0
  55. package/src/client-rust/ws.rs +143 -0
  56. package/src/cloudflare/realtime.ts +236 -0
  57. package/src/codegen-go-cli.ts +1180 -0
  58. package/src/codegen-go.ts +1010 -0
  59. package/src/codegen-hash.ts +8 -0
  60. package/src/codegen-ir.ts +517 -0
  61. package/src/codegen-loaders.ts +53 -0
  62. package/src/codegen-mcp.ts +364 -0
  63. package/src/codegen-python.ts +1028 -0
  64. package/src/codegen-rust.ts +1652 -0
  65. package/src/codegen-sanitize.ts +67 -0
  66. package/src/codegen-schema-naming.ts +146 -0
  67. package/src/codegen.ts +5015 -0
  68. package/src/context.ts +210 -0
  69. package/src/cookie-sign.ts +58 -0
  70. package/src/cookie.ts +89 -0
  71. package/src/cors.ts +96 -0
  72. package/src/crypto.ts +19 -0
  73. package/src/csrf.ts +54 -0
  74. package/src/curl-logger.ts +235 -0
  75. package/src/detect-runtime.ts +19 -0
  76. package/src/error.ts +52 -0
  77. package/src/errors.ts +87 -0
  78. package/src/etag.ts +74 -0
  79. package/src/feature-load.ts +23 -0
  80. package/src/go-type-emitter.ts +404 -0
  81. package/src/honey-response.ts +354 -0
  82. package/src/i18n-slot.ts +20 -0
  83. package/src/i18n.ts +234 -0
  84. package/src/index.ts +3843 -0
  85. package/src/init.ts +149 -0
  86. package/src/input.ts +8 -0
  87. package/src/ip-restrict.ts +192 -0
  88. package/src/logger.ts +149 -0
  89. package/src/middleware.ts +199 -0
  90. package/src/node-request.ts +240 -0
  91. package/src/node.ts +212 -0
  92. package/src/openapi/register.ts +19 -0
  93. package/src/openapi/scalar.ts +33 -0
  94. package/src/openapi/spec-factory.ts +46 -0
  95. package/src/openapi/spec.ts +31 -0
  96. package/src/openapi/swagger.ts +34 -0
  97. package/src/plugin.ts +771 -0
  98. package/src/powered-by.ts +17 -0
  99. package/src/pretty-json.ts +39 -0
  100. package/src/proxy.ts +155 -0
  101. package/src/python-type-emitter.ts +144 -0
  102. package/src/realtime/buffer.ts +101 -0
  103. package/src/realtime/bus.ts +93 -0
  104. package/src/realtime/longpoll.ts +148 -0
  105. package/src/realtime/protocol.ts +128 -0
  106. package/src/realtime/route.ts +104 -0
  107. package/src/realtime/runtime.ts +430 -0
  108. package/src/request-id.ts +23 -0
  109. package/src/request-to-curl.ts +34 -0
  110. package/src/response.ts +414 -0
  111. package/src/rust-type-emitter.ts +412 -0
  112. package/src/secure-headers.ts +69 -0
  113. package/src/serve-register.ts +8 -0
  114. package/src/serve-slot.ts +20 -0
  115. package/src/serve.ts +141 -0
  116. package/src/server-timing.ts +66 -0
  117. package/src/static.ts +60 -0
  118. package/src/telemetry/otel.ts +139 -0
  119. package/src/testing.ts +114 -0
  120. package/src/timeout.ts +36 -0
  121. package/src/tree.ts +464 -0
  122. package/src/ts-type-emitter.ts +106 -0
  123. package/src/type-emitter.ts +524 -0
  124. package/src/type-extractor.ts +749 -0
  125. package/src/types.ts +638 -0
  126. package/src/validation.ts +300 -0
  127. package/src/ws/bun.ts +89 -0
  128. package/src/ws/cloudflare.ts +133 -0
  129. package/src/ws/deno.ts +109 -0
  130. package/src/ws/node.ts +161 -0
  131. 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
+ }