@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,66 @@
1
+ import type { MiddlewareFn } from "./middleware.ts"
2
+
3
+ export type Timing = {
4
+ end(name: string): void
5
+ start(name: string, description?: string): void
6
+ }
7
+
8
+ type TimingEntry = {
9
+ description?: string
10
+ end?: number
11
+ start: number
12
+ }
13
+
14
+ /** Strip characters that would break Server-Timing header syntax */
15
+ const unsafeNameRe = /[;,="]/g
16
+ function sanitizeName(name: string): string {
17
+ return name.replace(unsafeNameRe, "")
18
+ }
19
+
20
+ function escapeDescription(desc: string): string {
21
+ return desc.replace(/\\/g, "\\\\").replace(/"/g, '\\"')
22
+ }
23
+
24
+ export function serverTiming(): MiddlewareFn<{}, { timing: Timing }> {
25
+ const mw: MiddlewareFn<{}, { timing: Timing }> = async (_ctx, next) => {
26
+ const requestStart = performance.now()
27
+ const entries = new Map<string, TimingEntry>()
28
+
29
+ const timing: Timing = {
30
+ end(name: string) {
31
+ const entry = entries.get(name)
32
+ if (entry) {
33
+ entry.end = performance.now()
34
+ }
35
+ },
36
+ start(name: string, description?: string) {
37
+ entries.set(name, { description, start: performance.now() })
38
+ },
39
+ }
40
+
41
+ const response = await next({ timing })
42
+
43
+ /* auto-close any unclosed timings */
44
+ const now = performance.now()
45
+ const parts: string[] = []
46
+ for (const [name, entry] of entries) {
47
+ const dur = ((entry.end ?? now) - entry.start).toFixed(2)
48
+ let part = sanitizeName(name)
49
+ if (entry.description) {
50
+ part += `;desc="${escapeDescription(entry.description)}"`
51
+ }
52
+ part += `;dur=${dur}`
53
+ parts.push(part)
54
+ }
55
+
56
+ /* always include total request duration */
57
+ const totalDur = (performance.now() - requestStart).toFixed(2)
58
+ parts.push(`total;dur=${totalDur}`)
59
+
60
+ response.headers.set("server-timing", parts.join(", "))
61
+
62
+ return response
63
+ }
64
+
65
+ return mw
66
+ }
package/src/static.ts ADDED
@@ -0,0 +1,60 @@
1
+ import type { MiddlewareFn } from "./middleware.ts"
2
+
3
+ type StaticConfig<TCtx> = {
4
+ resolve: (ctx: TCtx, filePath: string) => Response | null | Promise<Response | null>
5
+ prefix?: string
6
+ headers?: Record<string, string> | ((filePath: string) => Record<string, string>)
7
+ rewritePath?: (filePath: string, ctx: TCtx) => string
8
+ }
9
+
10
+ const TRAVERSAL = /(?:^|\/)\.\.\//
11
+
12
+ export function staticFiles<TCtx extends { req: Request }>(
13
+ config: StaticConfig<TCtx>,
14
+ ): MiddlewareFn<TCtx, {}> {
15
+ const prefix = config.prefix ?? "/"
16
+
17
+ const mw: MiddlewareFn<TCtx, {}> = async (ctx, next) => {
18
+ const method = ctx.req.method
19
+ if (method !== "GET" && method !== "HEAD") return next()
20
+
21
+ /* fast path extraction — no new URL() */
22
+ const raw = ctx.req.url
23
+ const protoEnd = raw.indexOf("//")
24
+ const pathStart = protoEnd === -1 ? 0 : raw.indexOf("/", protoEnd + 2)
25
+ const qIdx = pathStart === -1 ? -1 : raw.indexOf("?", pathStart)
26
+ let path: string
27
+ if (pathStart === -1) {
28
+ path = "/"
29
+ } else if (qIdx === -1) {
30
+ path = raw.substring(pathStart)
31
+ } else {
32
+ path = raw.substring(pathStart, qIdx)
33
+ }
34
+
35
+ if (!path.startsWith(prefix)) return next()
36
+
37
+ const stripped = prefix === "/" ? path : path.substring(prefix.length)
38
+ const filePath = stripped === "" ? "/" : stripped
39
+
40
+ /* path traversal guard — reject encoded and literal .. segments */
41
+ const decoded = decodeURIComponent(filePath)
42
+ if (TRAVERSAL.test(decoded)) return next()
43
+
44
+ const resolved = config.rewritePath ? config.rewritePath(filePath, ctx) : filePath
45
+ const response = await config.resolve(ctx, resolved)
46
+ if (response === null) return next()
47
+
48
+ /* apply extra headers */
49
+ if (config.headers) {
50
+ const extra = typeof config.headers === "function" ? config.headers(filePath) : config.headers
51
+ for (const key in extra) {
52
+ response.headers.set(key, extra[key])
53
+ }
54
+ }
55
+
56
+ return response
57
+ }
58
+
59
+ return mw
60
+ }
@@ -0,0 +1,139 @@
1
+ import type { HoneyError } from "../error.ts"
2
+
3
+ type Span = {
4
+ addEvent(name: string, attributes?: Record<string, unknown>): void
5
+ end(): void
6
+ setAttribute(key: string, value: unknown): void
7
+ }
8
+
9
+ type Tracer = {
10
+ startSpan(name: string): Span
11
+ }
12
+
13
+ type TelemetryAdapter = {
14
+ onError?(ctx: { duration: number; error: HoneyError; method: string; path: string }): void
15
+ onHandler?(ctx: { duration: number; method: string; path: string; status: number }): void
16
+ onMethodNotAllowed?(ctx: { allowed: string[]; method: string; path: string; req: Request }): void
17
+ onMiddleware?(ctx: { duration: number; error?: unknown; name: string }): void
18
+ onNotFound?(ctx: { method: string; path: string; req: Request }): void
19
+ onRequest?(ctx: { env: unknown; req: Request }): void
20
+ onResponse?(ctx: { duration: number; req: Request; status: number }): void
21
+ onRoute?(ctx: {
22
+ method: string
23
+ params: Record<string, string>
24
+ path: string
25
+ req: Request
26
+ }): void
27
+ }
28
+
29
+ export function otelAdapter(options: { tracer: Tracer }): TelemetryAdapter {
30
+ const spans = new WeakMap<Request, Span>()
31
+
32
+ return {
33
+ onError(ctx) {
34
+ try {
35
+ /*
36
+ * onError doesn't receive req — error details are best captured
37
+ * via a standalone span. Per-request root span is in onResponse.
38
+ */
39
+ const span = options.tracer.startSpan("http.error")
40
+ span.setAttribute("error.key", ctx.error.errorKey)
41
+ span.setAttribute("error.status", ctx.error.status)
42
+ span.setAttribute("http.duration_ms", ctx.duration)
43
+ span.end()
44
+ } catch {
45
+ /* swallow */
46
+ }
47
+ },
48
+
49
+ onHandler(ctx) {
50
+ try {
51
+ const span = options.tracer.startSpan("http.handler")
52
+ span.setAttribute("http.method", ctx.method)
53
+ span.setAttribute("http.route", ctx.path)
54
+ span.setAttribute("http.status_code", ctx.status)
55
+ span.setAttribute("http.duration_ms", ctx.duration)
56
+ span.end()
57
+ } catch {
58
+ /* swallow */
59
+ }
60
+ },
61
+
62
+ onMethodNotAllowed(ctx) {
63
+ try {
64
+ const span = spans.get(ctx.req)
65
+ if (span) {
66
+ span.addEvent("method_not_allowed", {
67
+ "http.allowed_methods": ctx.allowed.join(", "),
68
+ "http.method": ctx.method,
69
+ "http.route": ctx.path,
70
+ })
71
+ }
72
+ } catch {
73
+ /* swallow */
74
+ }
75
+ },
76
+
77
+ onMiddleware(ctx) {
78
+ try {
79
+ const span = options.tracer.startSpan(`middleware.${ctx.name}`)
80
+ span.setAttribute("middleware.name", ctx.name)
81
+ span.setAttribute("middleware.duration_ms", ctx.duration)
82
+ if (ctx.error) span.addEvent("error")
83
+ span.end()
84
+ } catch {
85
+ /* swallow */
86
+ }
87
+ },
88
+
89
+ onNotFound(ctx) {
90
+ try {
91
+ const span = spans.get(ctx.req)
92
+ if (span) {
93
+ span.addEvent("not_found", {
94
+ "http.method": ctx.method,
95
+ "http.path": ctx.path,
96
+ })
97
+ }
98
+ } catch {
99
+ /* swallow */
100
+ }
101
+ },
102
+
103
+ onRequest(ctx) {
104
+ try {
105
+ const span = options.tracer.startSpan("http.request")
106
+ span.setAttribute("http.url", ctx.req.url)
107
+ spans.set(ctx.req, span)
108
+ } catch {
109
+ /* swallow */
110
+ }
111
+ },
112
+
113
+ onResponse(ctx) {
114
+ try {
115
+ const span = spans.get(ctx.req)
116
+ if (span) {
117
+ span.setAttribute("http.status_code", ctx.status)
118
+ span.setAttribute("http.duration_ms", ctx.duration)
119
+ span.end()
120
+ spans.delete(ctx.req)
121
+ }
122
+ } catch {
123
+ /* swallow */
124
+ }
125
+ },
126
+
127
+ onRoute(ctx) {
128
+ try {
129
+ const span = spans.get(ctx.req)
130
+ if (span) {
131
+ span.setAttribute("http.method", ctx.method)
132
+ span.setAttribute("http.route", ctx.path)
133
+ }
134
+ } catch {
135
+ /* swallow */
136
+ }
137
+ },
138
+ }
139
+ }
package/src/testing.ts ADDED
@@ -0,0 +1,114 @@
1
+ import type { Honey } from "./index.ts"
2
+
3
+ type TestRequestOptions = {
4
+ form?: Record<string, string>
5
+ headers?: Record<string, string>
6
+ json?: unknown
7
+ search?: Record<string, string>
8
+ }
9
+
10
+ type TestClient = {
11
+ delete(path: string, opts?: TestRequestOptions): Promise<Response>
12
+ get(path: string, opts?: TestRequestOptions): Promise<Response>
13
+ head(path: string, opts?: TestRequestOptions): Promise<Response>
14
+ options(path: string, opts?: TestRequestOptions): Promise<Response>
15
+ patch(path: string, opts?: TestRequestOptions): Promise<Response>
16
+ post(path: string, opts?: TestRequestOptions): Promise<Response>
17
+ put(path: string, opts?: TestRequestOptions): Promise<Response>
18
+ request(method: string, path: string, opts?: TestRequestOptions): Promise<Response>
19
+ }
20
+
21
+ function buildRequest(
22
+ method: string,
23
+ path: string,
24
+ baseUrl: string,
25
+ opts?: TestRequestOptions,
26
+ ): Request {
27
+ const url = new URL(path, baseUrl)
28
+
29
+ if (opts?.search) {
30
+ for (const [k, v] of Object.entries(opts.search)) {
31
+ url.searchParams.set(k, v)
32
+ }
33
+ }
34
+
35
+ const headers = new Headers(opts?.headers)
36
+ let body: BodyInit | null = null
37
+
38
+ if (opts?.json !== undefined) {
39
+ headers.set("content-type", "application/json")
40
+ body = JSON.stringify(opts.json)
41
+ } else if (opts?.form) {
42
+ const formData = new FormData()
43
+ for (const [k, v] of Object.entries(opts.form)) {
44
+ formData.append(k, v)
45
+ }
46
+ body = formData
47
+ }
48
+
49
+ return new Request(url.toString(), { body, headers, method })
50
+ }
51
+
52
+ type TestClientOptions<TEnv> = {
53
+ cookies?: boolean
54
+ env: TEnv
55
+ }
56
+
57
+ function parseCookieName(setCookie: string): { name: string; value: string } | null {
58
+ const eq = setCookie.indexOf("=")
59
+ if (eq === -1) return null
60
+ const name = setCookie.slice(0, eq).trim()
61
+ const semi = setCookie.indexOf(";", eq)
62
+ const value = semi === -1 ? setCookie.slice(eq + 1) : setCookie.slice(eq + 1, semi)
63
+ return { name, value }
64
+ }
65
+
66
+ export function testClient<TEnv>(
67
+ app: Honey<TEnv, unknown, unknown, unknown, unknown, string, string>,
68
+ options: TestClientOptions<TEnv>,
69
+ ): TestClient {
70
+ const baseUrl = "http://localhost"
71
+ const jar = new Map<string, string>()
72
+
73
+ async function doRequest(
74
+ method: string,
75
+ path: string,
76
+ opts?: TestRequestOptions,
77
+ ): Promise<Response> {
78
+ const mergedHeaders = { ...opts?.headers }
79
+
80
+ if (options.cookies && jar.size > 0) {
81
+ const existing = mergedHeaders.cookie ?? ""
82
+ const jarStr = Array.from(jar.entries())
83
+ .map(([k, v]) => `${k}=${v}`)
84
+ .join("; ")
85
+ mergedHeaders.cookie = existing ? `${existing}; ${jarStr}` : jarStr
86
+ }
87
+
88
+ const req = buildRequest(method, path, baseUrl, { ...opts, headers: mergedHeaders })
89
+ const res = await app.fetch(req, options.env)
90
+
91
+ if (options.cookies) {
92
+ const setCookies = res.headers.getSetCookie()
93
+ for (const sc of setCookies) {
94
+ const parsed = parseCookieName(sc)
95
+ if (parsed) {
96
+ jar.set(parsed.name, parsed.value)
97
+ }
98
+ }
99
+ }
100
+
101
+ return res
102
+ }
103
+
104
+ return {
105
+ delete: (path, opts) => doRequest("DELETE", path, opts),
106
+ get: (path, opts) => doRequest("GET", path, opts),
107
+ head: (path, opts) => doRequest("HEAD", path, opts),
108
+ options: (path, opts) => doRequest("OPTIONS", path, opts),
109
+ patch: (path, opts) => doRequest("PATCH", path, opts),
110
+ post: (path, opts) => doRequest("POST", path, opts),
111
+ put: (path, opts) => doRequest("PUT", path, opts),
112
+ request: (method, path, opts) => doRequest(method, path, opts),
113
+ }
114
+ }
package/src/timeout.ts ADDED
@@ -0,0 +1,36 @@
1
+ import { HoneyError } from "./error.ts"
2
+ import type { MiddlewareFn } from "./middleware.ts"
3
+ import { EK, SK } from "./types.ts"
4
+
5
+ type TimeoutOptions = {
6
+ duration: number
7
+ }
8
+
9
+ export function timeout(opts: TimeoutOptions): MiddlewareFn<{}, {}> {
10
+ const { duration } = opts
11
+
12
+ const mw: MiddlewareFn<{}, {}> = (_ctx, next) => {
13
+ return new Promise<Response>((resolve, reject) => {
14
+ const timer = setTimeout(() => {
15
+ reject(
16
+ new HoneyError({
17
+ errorKey: EK.request_timeout,
18
+ status: SK.gateway_timeout,
19
+ }),
20
+ )
21
+ }, duration)
22
+
23
+ Promise.resolve(next())
24
+ .then((res) => {
25
+ clearTimeout(timer)
26
+ resolve(res)
27
+ })
28
+ .catch((err) => {
29
+ clearTimeout(timer)
30
+ reject(err)
31
+ })
32
+ })
33
+ }
34
+
35
+ return mw
36
+ }