@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,412 @@
1
+ /* Rust type emitter — IR-based printer with raw-schema shims for backward compatibility. */
2
+
3
+ import { schemaToIR } from "./codegen-ir.ts"
4
+ import type { IRSchema } from "./codegen-ir.ts"
5
+ import { goPascal } from "./go-type-emitter.ts"
6
+
7
+ export const RUST_KEYWORDS = new Set([
8
+ "as", "break", "const", "continue", "crate", "else", "enum", "extern",
9
+ "false", "fn", "for", "if", "impl", "in", "let", "loop", "match", "mod",
10
+ "move", "mut", "pub", "ref", "return", "self", "Self", "static", "struct",
11
+ "super", "trait", "true", "type", "unsafe", "use", "where", "while",
12
+ "async", "await", "dyn",
13
+ /* reserved / future */
14
+ "abstract", "become", "box", "do", "final", "macro", "override", "priv",
15
+ "typeof", "unsized", "virtual", "yield", "try",
16
+ ])
17
+
18
+ export function rustPascal(name: string): string {
19
+ return goPascal(name)
20
+ }
21
+
22
+ export function rustSnake(name: string): string {
23
+ return name
24
+ .replace(/([a-z0-9])([A-Z])/g, "$1_$2")
25
+ .replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2")
26
+ .replace(/[-\s]+/g, "_")
27
+ .replace(/[^a-zA-Z0-9_]/g, "_")
28
+ .toLowerCase()
29
+ }
30
+
31
+ /** Returns a safe Rust field identifier (snake_case; reserved keywords get `_` suffix). */
32
+ export function rustIdent(name: string): string {
33
+ const snake = rustSnake(name)
34
+ if (RUST_KEYWORDS.has(snake)) return `${snake}_`
35
+ return snake
36
+ }
37
+
38
+ /** Returns a safe Rust method identifier (snake_case; reserved keywords get `r#` raw-ident prefix). */
39
+ export function rustMethodIdent(name: string): string {
40
+ const snake = rustSnake(name)
41
+ /* raw identifiers are the only valid escape for fn names — `_` suffix changes the API */
42
+ if (RUST_KEYWORDS.has(snake)) return `r#${snake}`
43
+ return snake
44
+ }
45
+
46
+ export type RustRenderUseCtx = {
47
+ parentName: string
48
+ fieldName: string
49
+ decls: Map<string, string>
50
+ circularRefs?: Set<string>
51
+ depth?: number
52
+ }
53
+
54
+ function renderHoistedStringEnum(typeName: string, enumVals: unknown[]): string {
55
+ const l: string[] = []
56
+ l.push(`#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]`)
57
+ l.push(`pub enum ${typeName} {`)
58
+ for (const raw of enumVals.map(String)) {
59
+ const variant = rustPascal(raw)
60
+ if (raw !== variant) {
61
+ l.push(`\t#[serde(rename = ${JSON.stringify(raw)})]`)
62
+ }
63
+ l.push(`\t${variant},`)
64
+ }
65
+ l.push(`}`)
66
+ /* Separate impl block so the derive list stays unchanged — derive(Default) would require
67
+ * #[default] on a variant, which breaks callers that assert the exact derive substring. */
68
+ if (enumVals.length > 0) {
69
+ const firstVariant = rustPascal(String(enumVals[0]))
70
+ l.push(``)
71
+ l.push(`impl Default for ${typeName} {`)
72
+ l.push(`\tfn default() -> Self {`)
73
+ l.push(`\t\tSelf::${firstVariant}`)
74
+ l.push(`\t}`)
75
+ l.push(`}`)
76
+ }
77
+ return l.join("\n")
78
+ }
79
+
80
+ function renderHoistedIntEnum(typeName: string, enumVals: unknown[]): string {
81
+ const l: string[] = []
82
+ l.push(`#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize_repr, Deserialize_repr)]`)
83
+ l.push(`#[repr(i64)]`)
84
+ l.push(`pub enum ${typeName} {`)
85
+ for (const v of enumVals) {
86
+ const n = Number(v)
87
+ const label = n >= 0 ? `L${n}` : `Neg${Math.abs(n)}`
88
+ l.push(`\t${label} = ${n},`)
89
+ }
90
+ l.push(`}`)
91
+ return l.join("\n")
92
+ }
93
+
94
+ function hoistEnumName(parentName: string, fieldName: string): string {
95
+ return `${rustPascal(parentName)}${rustPascal(fieldName)}`
96
+ }
97
+
98
+ function primitiveFor(t: string | undefined): string | null {
99
+ switch (t) {
100
+ case "string": return "String"
101
+ case "integer": return "i64"
102
+ case "number": return "f64"
103
+ case "boolean": return "bool"
104
+ default: return null
105
+ }
106
+ }
107
+
108
+ function constBaseType(val: unknown): string {
109
+ if (typeof val === "boolean") return "bool"
110
+ if (typeof val === "number") return Number.isInteger(val) ? "i64" : "f64"
111
+ return "String"
112
+ }
113
+
114
+ /**
115
+ * Render an IR schema as a Rust type expression for a struct field position.
116
+ * Mutates `ctx.decls` when hoisting enums or anonymous structs.
117
+ */
118
+ export function irRenderUseRust(ir: IRSchema, ctx: RustRenderUseCtx, depth = 0): string {
119
+ if (depth > 12) return "serde_json::Value"
120
+
121
+ switch (ir.kind) {
122
+ case "ref": {
123
+ const rustName = rustPascal(ir.name)
124
+ if (ctx.circularRefs?.has(rustName)) return `Box<${rustName}>`
125
+ return rustName
126
+ }
127
+
128
+ case "allOf": {
129
+ const firstRef = ir.parts.find((p) => p.kind === "ref")
130
+ if (firstRef && firstRef.kind === "ref") return rustPascal(firstRef.name)
131
+ return "serde_json::Value"
132
+ }
133
+
134
+ case "nullable": {
135
+ const innerStr = irRenderUseRust(ir.inner, ctx, depth + 1)
136
+ /* inner is already serde_json::Value — don't wrap in Option */
137
+ if (innerStr === "serde_json::Value") return "serde_json::Value"
138
+ /* idempotent: don't double-wrap */
139
+ if (innerStr.startsWith("Option<")) return innerStr
140
+ return `Option<${innerStr}>`
141
+ }
142
+
143
+ case "union": {
144
+ /* union at use-position → always serde_json::Value */
145
+ return "serde_json::Value"
146
+ }
147
+
148
+ case "scalar": {
149
+ if (ir.enum) {
150
+ const name = hoistEnumName(ctx.parentName, ctx.fieldName)
151
+ if (!ctx.decls.has(name)) {
152
+ if (ir.type === "string") {
153
+ ctx.decls.set(name, renderHoistedStringEnum(name, ir.enum as unknown[]))
154
+ } else {
155
+ ctx.decls.set(name, renderHoistedIntEnum(name, ir.enum as unknown[]))
156
+ }
157
+ }
158
+ return name
159
+ }
160
+ const prim = primitiveFor(ir.type)
161
+ if (prim) return prim
162
+ return "serde_json::Value"
163
+ }
164
+
165
+ case "const": {
166
+ return constBaseType(ir.value)
167
+ }
168
+
169
+ case "array": {
170
+ const el = irRenderUseRust(ir.items, ctx, depth + 1)
171
+ return `Vec<${el}>`
172
+ }
173
+
174
+ case "tuple": {
175
+ /* Rust emitter ignores tuple item types — always Vec<serde_json::Value> */
176
+ return "Vec<serde_json::Value>"
177
+ }
178
+
179
+ case "object": {
180
+ const { fields, additional } = ir
181
+
182
+ if (fields.length === 0) {
183
+ if (additional !== undefined && additional !== false) {
184
+ const valType = irRenderUseRust(additional, ctx, depth + 1)
185
+ return `HashMap<String, ${valType}>`
186
+ }
187
+ if (additional === false) return "serde_json::Value"
188
+ return "HashMap<String, serde_json::Value>"
189
+ }
190
+
191
+ const hoistedName = hoistEnumName(ctx.parentName, ctx.fieldName)
192
+ if (!ctx.decls.has(hoistedName)) {
193
+ /* placeholder prevents infinite recursion on self-referential schemas */
194
+ ctx.decls.set(hoistedName, "")
195
+ const hoisted = irRenderTopLevelRust(hoistedName, ir, ctx.decls)
196
+ ctx.decls.set(hoistedName, hoisted)
197
+ }
198
+ return hoistedName
199
+ }
200
+
201
+ case "binary": {
202
+ return "String"
203
+ }
204
+
205
+ case "unknown": {
206
+ return "serde_json::Value"
207
+ }
208
+ }
209
+ }
210
+
211
+ /**
212
+ * `raw` 4th arg is for the const-with-declared-type branch only: IR `const` kind
213
+ * strips the type field; `raw.type` provides the override.
214
+ */
215
+ export function irRenderTopLevelRust(
216
+ name: string,
217
+ ir: IRSchema,
218
+ decls: Map<string, string>,
219
+ raw?: Record<string, unknown>,
220
+ ): string {
221
+ const typeName = rustPascal(name)
222
+ const circularRefs = new Set([typeName])
223
+
224
+ if (ir.kind === "scalar" && ir.enum) {
225
+ if (ir.type === "string") {
226
+ return renderHoistedStringEnum(typeName, ir.enum as unknown[])
227
+ }
228
+ return renderHoistedIntEnum(typeName, ir.enum as unknown[])
229
+ }
230
+
231
+ if (ir.kind === "const") {
232
+ const rawType = raw?.type as string | undefined
233
+ const base = rawType ? (primitiveFor(rawType) ?? "String") : constBaseType(ir.value)
234
+ return [
235
+ `#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]`,
236
+ `pub struct ${typeName}(pub ${base});`,
237
+ ].join("\n")
238
+ }
239
+
240
+ if (ir.kind === "allOf") {
241
+ const l: string[] = []
242
+ l.push(`#[derive(Debug, Clone, Serialize, Deserialize)]`)
243
+ l.push(`pub struct ${typeName} {`)
244
+ for (const part of ir.parts) {
245
+ if (part.kind === "ref") {
246
+ const refName = rustPascal(part.name)
247
+ l.push(`\t#[serde(flatten)]`)
248
+ l.push(`\tpub ${rustIdent(refName.toLowerCase())}: ${refName},`)
249
+ continue
250
+ }
251
+ if (part.kind !== "object") continue
252
+ const sorted = part.fields.slice().sort((a, b) => a.name.localeCompare(b.name))
253
+ for (const field of sorted) {
254
+ const fieldIdent = rustIdent(field.name)
255
+ const ft = irRenderUseRust(field.schema, {
256
+ circularRefs,
257
+ decls,
258
+ depth: 1,
259
+ fieldName: field.name,
260
+ parentName: typeName,
261
+ })
262
+ const finalType = !field.required && !ft.startsWith("Option<") ? `Option<${ft}>` : ft
263
+ const needsRename = fieldIdent !== field.name
264
+ const needsSkip = !field.required
265
+ if (needsRename) {
266
+ l.push(`\t#[serde(rename = ${JSON.stringify(field.name)})]`)
267
+ }
268
+ if (needsSkip) {
269
+ l.push(`\t#[serde(skip_serializing_if = "Option::is_none")]`)
270
+ }
271
+ l.push(`\tpub ${fieldIdent}: ${finalType},`)
272
+ }
273
+ }
274
+ l.push(`}`)
275
+ return l.join("\n")
276
+ }
277
+
278
+ if (ir.kind === "union" && ir.discriminator) {
279
+ const discProp = ir.discriminator.propertyName
280
+ const l: string[] = []
281
+ l.push(`#[derive(Debug, Clone, Serialize, Deserialize)]`)
282
+ l.push(`#[serde(tag = ${JSON.stringify(discProp)})]`)
283
+ l.push(`pub enum ${typeName} {`)
284
+ for (const v of ir.variants) {
285
+ if (v.kind === "ref") {
286
+ const refName = rustPascal(v.name)
287
+ l.push(`\t${refName}(${refName}),`)
288
+ } else {
289
+ l.push(`\tVariant(serde_json::Value),`)
290
+ }
291
+ }
292
+ l.push(`}`)
293
+ return l.join("\n")
294
+ }
295
+
296
+ if (ir.kind === "union") {
297
+ const l: string[] = []
298
+ l.push(`#[derive(Debug, Clone, Serialize, Deserialize)]`)
299
+ l.push(`#[serde(untagged)]`)
300
+ l.push(`pub enum ${typeName} {`)
301
+ for (let i = 0; i < ir.variants.length; i++) {
302
+ const v = ir.variants[i]
303
+ if (v.kind === "ref") {
304
+ const refName = rustPascal(v.name)
305
+ l.push(`\tVariant${i}(${refName}),`)
306
+ } else {
307
+ const inner = irRenderUseRust(v, {
308
+ circularRefs,
309
+ decls,
310
+ depth: 1,
311
+ fieldName: `variant${i}`,
312
+ parentName: typeName,
313
+ })
314
+ const finalType = inner === "serde_json::Value" ? "serde_json::Value" : inner
315
+ l.push(`\tVariant${i}(${finalType}),`)
316
+ }
317
+ }
318
+ l.push(`}`)
319
+ return l.join("\n")
320
+ }
321
+
322
+ if (ir.kind === "object") {
323
+ const { fields, additional } = ir
324
+
325
+ if (fields.length === 0 && additional !== undefined && additional !== false) {
326
+ const valType = irRenderUseRust(additional, {
327
+ circularRefs,
328
+ decls,
329
+ fieldName: "Value",
330
+ parentName: typeName,
331
+ })
332
+ return `pub type ${typeName} = HashMap<String, ${valType}>;`
333
+ }
334
+
335
+ if (fields.length === 0) {
336
+ if (additional === false) return `#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct ${typeName};`
337
+ return `pub type ${typeName} = HashMap<String, serde_json::Value>;`
338
+ }
339
+
340
+ const sorted = fields.slice().sort((a, b) => a.name.localeCompare(b.name))
341
+ const l: string[] = []
342
+ l.push(`#[derive(Debug, Clone, Serialize, Deserialize)]`)
343
+ l.push(`pub struct ${typeName} {`)
344
+ for (const field of sorted) {
345
+ const fieldIdent = rustIdent(field.name)
346
+ const ft = irRenderUseRust(field.schema, {
347
+ circularRefs,
348
+ decls,
349
+ depth: 1,
350
+ fieldName: field.name,
351
+ parentName: typeName,
352
+ })
353
+ let finalType: string
354
+ if (field.required) {
355
+ finalType = circularRefs.has(ft) && !ft.startsWith("Box<") ? `Box<${ft}>` : ft
356
+ } else {
357
+ finalType = ft.startsWith("Option<") ? ft : `Option<${ft}>`
358
+ }
359
+ const needsRename = fieldIdent !== field.name
360
+ const needsSkip = !field.required
361
+ if (needsRename) {
362
+ l.push(`\t#[serde(rename = ${JSON.stringify(field.name)})]`)
363
+ }
364
+ if (needsSkip) {
365
+ l.push(`\t#[serde(skip_serializing_if = "Option::is_none")]`)
366
+ }
367
+ l.push(`\tpub ${fieldIdent}: ${finalType},`)
368
+ }
369
+
370
+ if (additional) {
371
+ const valType = irRenderUseRust(additional, {
372
+ circularRefs,
373
+ decls,
374
+ depth: 1,
375
+ fieldName: "Extra",
376
+ parentName: typeName,
377
+ })
378
+ l.push(`\t#[serde(flatten)]`)
379
+ l.push(`\tpub extra: HashMap<String, ${valType}>,`)
380
+ }
381
+
382
+ l.push(`}`)
383
+ return l.join("\n")
384
+ }
385
+
386
+ const aliased = irRenderUseRust(ir, {
387
+ circularRefs,
388
+ decls,
389
+ fieldName: "Value",
390
+ parentName: typeName,
391
+ })
392
+ return `pub type ${typeName} = ${aliased};`
393
+ }
394
+
395
+ /**
396
+ * Render a schema as a Rust type expression for a struct field position.
397
+ * Mutates `ctx.decls` when hoisting enums or anonymous structs.
398
+ */
399
+ export function renderUseRust(
400
+ schema: Record<string, unknown> | undefined,
401
+ ctx: RustRenderUseCtx,
402
+ ): string {
403
+ return irRenderUseRust(schemaToIR(schema), ctx, ctx.depth ?? 0)
404
+ }
405
+
406
+ export function renderTopLevelRust(
407
+ name: string,
408
+ schema: Record<string, unknown>,
409
+ decls: Map<string, string>,
410
+ ): string {
411
+ return irRenderTopLevelRust(name, schemaToIR(schema), decls, schema)
412
+ }
@@ -0,0 +1,69 @@
1
+ import type { MiddlewareFn } from "./middleware.ts"
2
+
3
+ type SecureHeadersOptions = {
4
+ contentSecurityPolicy?: string
5
+ crossOriginEmbedderPolicy?: string
6
+ crossOriginOpenerPolicy?: string
7
+ crossOriginResourcePolicy?: string
8
+ permissionsPolicy?: string
9
+ referrerPolicy?: false | string
10
+ strictTransportSecurity?: string
11
+ xContentTypeOptions?: false | string
12
+ xFrameOptions?: false | string
13
+ xXssProtection?: false | string
14
+ }
15
+
16
+ export function secureHeaders(options?: SecureHeadersOptions): MiddlewareFn<{}, {}> {
17
+ const opts = options ?? {}
18
+
19
+ const headers: Array<[string, string]> = []
20
+
21
+ const xContentType = opts.xContentTypeOptions
22
+ if (xContentType !== false) {
23
+ headers.push(["x-content-type-options", xContentType ?? "nosniff"])
24
+ }
25
+
26
+ const xFrame = opts.xFrameOptions
27
+ if (xFrame !== false) {
28
+ headers.push(["x-frame-options", xFrame ?? "SAMEORIGIN"])
29
+ }
30
+
31
+ const referrer = opts.referrerPolicy
32
+ if (referrer !== false) {
33
+ headers.push(["referrer-policy", referrer ?? "strict-origin-when-cross-origin"])
34
+ }
35
+
36
+ const xss = opts.xXssProtection
37
+ if (xss !== false) {
38
+ headers.push(["x-xss-protection", xss ?? "0"])
39
+ }
40
+
41
+ if (opts.contentSecurityPolicy) {
42
+ headers.push(["content-security-policy", opts.contentSecurityPolicy])
43
+ }
44
+ if (opts.strictTransportSecurity) {
45
+ headers.push(["strict-transport-security", opts.strictTransportSecurity])
46
+ }
47
+ if (opts.permissionsPolicy) {
48
+ headers.push(["permissions-policy", opts.permissionsPolicy])
49
+ }
50
+ if (opts.crossOriginOpenerPolicy) {
51
+ headers.push(["cross-origin-opener-policy", opts.crossOriginOpenerPolicy])
52
+ }
53
+ if (opts.crossOriginResourcePolicy) {
54
+ headers.push(["cross-origin-resource-policy", opts.crossOriginResourcePolicy])
55
+ }
56
+ if (opts.crossOriginEmbedderPolicy) {
57
+ headers.push(["cross-origin-embedder-policy", opts.crossOriginEmbedderPolicy])
58
+ }
59
+
60
+ const mw: MiddlewareFn<{}, {}> = async (_ctx, next) => {
61
+ const response = await next()
62
+ for (const [name, value] of headers) {
63
+ response.headers.set(name, value)
64
+ }
65
+ return response
66
+ }
67
+
68
+ return mw
69
+ }
@@ -0,0 +1,8 @@
1
+ import { startHoneyServer } from "./serve.ts"
2
+ import { registerServeRuntime } from "./serve-slot.ts"
3
+
4
+ export function enableServe(): void {
5
+ registerServeRuntime((app, options) => startHoneyServer(app as never, options))
6
+ }
7
+
8
+ enableServe()
@@ -0,0 +1,20 @@
1
+ import type { HoneyServeOptions, ServeHandle } from "./serve.ts"
2
+
3
+ export type ServeStart = (app: unknown, options?: HoneyServeOptions) => Promise<ServeHandle>
4
+
5
+ const MISSING = 'Honey.serve() requires `import "@lovrozagar/honey/serve"` in the app entry.'
6
+
7
+ let runtime: ServeStart | undefined
8
+
9
+ export function registerServeRuntime(next: ServeStart): void {
10
+ runtime = next
11
+ }
12
+
13
+ export function resetServeRuntime(): void {
14
+ runtime = undefined
15
+ }
16
+
17
+ export function getServeRuntime(): ServeStart {
18
+ if (!runtime) throw new Error(MISSING)
19
+ return runtime
20
+ }
package/src/serve.ts ADDED
@@ -0,0 +1,141 @@
1
+ import type { Honey } from "./index.ts"
2
+ import { cors, type CORSOptions } from "./cors.ts"
3
+ import { detectRuntime, type ServeRuntime } from "./detect-runtime.ts"
4
+
5
+ export type { ServeRuntime }
6
+ export { detectRuntime }
7
+
8
+ export type ServeHandle = {
9
+ close(): Promise<void>
10
+ hostname: string
11
+ port: number
12
+ runtime: Exclude<ServeRuntime, "cloudflare">
13
+ url: string
14
+ }
15
+
16
+ export type HoneyServeOptions = {
17
+ cors?: boolean | CORSOptions
18
+ env?: Record<string, unknown>
19
+ hostname?: string
20
+ port?: number
21
+ runtime?: ServeRuntime
22
+ }
23
+
24
+ const CF_SERVE_ERROR =
25
+ "Honey.serve() cannot run on Cloudflare Workers. Export fetch:\n\n" +
26
+ "export default {\n" +
27
+ " fetch: (req, env, ctx) => app.fetch(req, env, ctx),\n" +
28
+ "}\n"
29
+
30
+ function publicHost(hostname: string): string {
31
+ return hostname === "0.0.0.0" || hostname === "::" ? "127.0.0.1" : hostname
32
+ }
33
+
34
+ export async function startHoneyServer(
35
+ app: Honey<Record<string, unknown>>,
36
+ options: HoneyServeOptions = {},
37
+ ): Promise<ServeHandle> {
38
+ const runtime = options.runtime ?? detectRuntime()
39
+ if (runtime === "cloudflare") {
40
+ throw new Error(CF_SERVE_ERROR)
41
+ }
42
+
43
+ let listening: Honey<Record<string, unknown>> = app
44
+ if (options.cors) {
45
+ const corsOpts = options.cors === true ? undefined : options.cors
46
+ listening = app.use(cors(corsOpts)) as Honey<Record<string, unknown>>
47
+ }
48
+
49
+ const hostname = options.hostname ?? (runtime === "deno" ? "127.0.0.1" : "0.0.0.0")
50
+ const port = options.port ?? 3000
51
+ const env = (options.env ?? {}) as Record<string, unknown>
52
+
53
+ if (runtime === "bun") {
54
+ const { bunWebSocket } = await import("./ws/bun.ts")
55
+ const bunWs = bunWebSocket()
56
+ listening.wsAdapter(bunWs)
57
+ const BunNs = (globalThis as unknown as { Bun: { serve: (opts: Record<string, unknown>) => BunServer } }).Bun
58
+ const server = BunNs.serve({
59
+ fetch: (req: Request, server: unknown) => listening.fetch(req, { ...env, server }),
60
+ hostname,
61
+ port,
62
+ websocket: bunWs.websocket,
63
+ })
64
+ const bound = server.port
65
+ return {
66
+ async close() {
67
+ await Promise.resolve(server.stop(true))
68
+ },
69
+ hostname,
70
+ port: bound,
71
+ runtime,
72
+ url: `http://${publicHost(hostname)}:${bound}`,
73
+ }
74
+ }
75
+
76
+ if (runtime === "deno") {
77
+ const { denoWebSocket } = await import("./ws/deno.ts")
78
+ listening.wsAdapter(denoWebSocket())
79
+ const DenoNs = (
80
+ globalThis as unknown as {
81
+ Deno: {
82
+ serve: (
83
+ opts: { hostname: string; port: number; signal?: AbortSignal },
84
+ handler: (req: Request) => Response | Promise<Response>,
85
+ ) => { addr?: { port?: number }; finished?: Promise<void>; shutdown?: () => Promise<void> }
86
+ }
87
+ }
88
+ ).Deno
89
+ const ac = new AbortController()
90
+ const server = DenoNs.serve(
91
+ { hostname, port, signal: ac.signal },
92
+ (req) => listening.fetch(req, env),
93
+ )
94
+ const bound = server.addr?.port ?? port
95
+ return {
96
+ async close() {
97
+ ac.abort()
98
+ if (server.shutdown) {
99
+ await Promise.race([
100
+ server.shutdown(),
101
+ new Promise<void>((r) => setTimeout(r, 1_000)),
102
+ ])
103
+ return
104
+ }
105
+ await Promise.race([
106
+ server.finished ?? Promise.resolve(),
107
+ new Promise<void>((r) => setTimeout(r, 1_000)),
108
+ ])
109
+ },
110
+ hostname,
111
+ port: bound,
112
+ runtime,
113
+ url: `http://${publicHost(hostname)}:${bound}`,
114
+ }
115
+ }
116
+
117
+ const { nodeWebSocket } = await import("./ws/node.ts")
118
+ const { serve } = await import("./node.ts")
119
+ listening.wsAdapter(nodeWebSocket())
120
+ const server = serve(listening as never, { env, hostname, port })
121
+ await new Promise<void>((resolve, reject) => {
122
+ server.once("listening", () => resolve())
123
+ server.once("error", reject)
124
+ })
125
+ const addr = server.address()
126
+ const bound = typeof addr === "object" && addr !== null ? addr.port : port
127
+ return {
128
+ async close() {
129
+ await server.shutdown(1_000)
130
+ },
131
+ hostname,
132
+ port: bound,
133
+ runtime: "node",
134
+ url: `http://${publicHost(hostname)}:${bound}`,
135
+ }
136
+ }
137
+
138
+ type BunServer = {
139
+ port: number
140
+ stop(closeActiveConnections?: boolean): void | Promise<void>
141
+ }