@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
package/src/init.ts ADDED
@@ -0,0 +1,149 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"
2
+ import { basename, dirname, join } from "node:path"
3
+ import { fileURLToPath } from "node:url"
4
+
5
+ export type InitFlags = {
6
+ cf: boolean
7
+ force: boolean
8
+ }
9
+
10
+ type PackageJson = {
11
+ dependencies?: Record<string, string>
12
+ name?: string
13
+ private?: boolean
14
+ scripts?: Record<string, string>
15
+ type?: string
16
+ }
17
+
18
+ const APP_TS = `import { honey } from "@lovrozagar/honey"
19
+
20
+ export const app = honey()
21
+ .get("/health")
22
+ .handler((ctx) => ctx.res.text("ok", "ok"))
23
+ .openapi({ docs: "scalar", title: "Honey", version: "0.0.1" })
24
+ `
25
+
26
+ const SERVER_TS = `import { app } from "./app.ts"
27
+
28
+ const port = Number(process.env.PORT ?? 3000)
29
+ await app.serve({ port })
30
+ `
31
+
32
+ const VITE_CONFIG_TS = `import { honey } from "@lovrozagar/honey/plugin"
33
+
34
+ export default {
35
+ plugins: [
36
+ honey({
37
+ app: "src/app.ts",
38
+ }),
39
+ ],
40
+ }
41
+ `
42
+
43
+ const WORKER_TS = `import { cfWebSocket } from "@lovrozagar/honey/ws/cloudflare"
44
+ import { app } from "./app.ts"
45
+
46
+ app.wsAdapter(cfWebSocket())
47
+
48
+ export default {
49
+ fetch: (req: Request, env: Record<string, unknown>, ctx: unknown) => app.fetch(req, env, ctx),
50
+ }
51
+ `
52
+
53
+ export function parseInitFlags(args: string[]): InitFlags {
54
+ return {
55
+ cf: args.includes("--cf"),
56
+ force: args.includes("--force"),
57
+ }
58
+ }
59
+
60
+ export function runInit(cwd: string, flags: InitFlags): void {
61
+ const files: Record<string, string> = {
62
+ "src/app.ts": APP_TS,
63
+ "src/server.ts": SERVER_TS,
64
+ "vite.config.ts": VITE_CONFIG_TS,
65
+ }
66
+ if (flags.cf) {
67
+ files["src/worker.ts"] = WORKER_TS
68
+ files["wrangler.jsonc"] = wranglerJsonc(packageNameFromDir(cwd))
69
+ }
70
+
71
+ const collisions = Object.keys(files).filter((rel) => existsSync(join(cwd, rel)))
72
+ if (collisions.length > 0 && !flags.force) {
73
+ throw new Error(`${collisions[0]} already exists. Use --force to overwrite.`)
74
+ }
75
+
76
+ for (const [rel, contents] of Object.entries(files)) {
77
+ writeText(join(cwd, rel), contents)
78
+ }
79
+ writePackageJson(cwd)
80
+ console.log("honey: initialized")
81
+ }
82
+
83
+ function wranglerJsonc(name: string): string {
84
+ return `{
85
+ "compatibility_date": "2026-01-20",
86
+ "compatibility_flags": ["nodejs_compat"],
87
+ "main": "src/worker.ts",
88
+ "name": ${JSON.stringify(name)},
89
+ "workers_dev": true
90
+ }
91
+ `
92
+ }
93
+
94
+ function writePackageJson(cwd: string): void {
95
+ const path = join(cwd, "package.json")
96
+ const existing = existsSync(path) ? readPackageJson(path) : {}
97
+ const next: PackageJson = {
98
+ ...existing,
99
+ name: existing.name ?? packageNameFromDir(cwd),
100
+ private: existing.private ?? true,
101
+ type: existing.type ?? "module",
102
+ scripts: {
103
+ ...existing.scripts,
104
+ dev: "bun --watch src/server.ts",
105
+ generate: "honey generate",
106
+ },
107
+ dependencies: {
108
+ ...existing.dependencies,
109
+ honey: existing.dependencies?.honey ?? `^${honeyVersion()}`,
110
+ },
111
+ }
112
+ writeText(path, `${JSON.stringify(next, null, "\t")}\n`)
113
+ }
114
+
115
+ function readPackageJson(path: string): PackageJson {
116
+ let parsed: unknown
117
+ try {
118
+ parsed = JSON.parse(readFileSync(path, "utf-8"))
119
+ } catch {
120
+ throw new Error(`package.json is not valid JSON`)
121
+ }
122
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
123
+ throw new Error("package.json must be an object")
124
+ }
125
+ return parsed as PackageJson
126
+ }
127
+
128
+ function writeText(path: string, contents: string): void {
129
+ mkdirSync(dirname(path), { recursive: true })
130
+ writeFileSync(path, contents.endsWith("\n") ? contents : `${contents}\n`, "utf-8")
131
+ }
132
+
133
+ function packageNameFromDir(dir: string): string {
134
+ const base = basename(dir)
135
+ .toLowerCase()
136
+ .replace(/[^a-z0-9._-]+/g, "-")
137
+ .replace(/^[-.]+|[-.]+$/g, "")
138
+ return base || "honey-app"
139
+ }
140
+
141
+ function honeyVersion(): string {
142
+ try {
143
+ const pkgPath = join(dirname(fileURLToPath(import.meta.url)), "..", "package.json")
144
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")) as { version?: string }
145
+ return typeof pkg.version === "string" ? pkg.version : "0.0.1"
146
+ } catch {
147
+ return "0.0.1"
148
+ }
149
+ }
package/src/input.ts ADDED
@@ -0,0 +1,8 @@
1
+ export type ReadableStreamSchema<T> = {
2
+ readonly _tag: "readableStream"
3
+ readonly schema: T
4
+ }
5
+
6
+ export function readableStream<T>(schema: T): ReadableStreamSchema<T> {
7
+ return { _tag: "readableStream", schema }
8
+ }
@@ -0,0 +1,192 @@
1
+ import { HoneyError } from "./error.ts"
2
+ import type { MiddlewareFn } from "./middleware.ts"
3
+ import { EK, SK } from "./types.ts"
4
+
5
+ type IpRestrictOptions = {
6
+ allowList?: string[]
7
+ denyList?: string[]
8
+ getIp?: (req: Request) => string | null
9
+ trustProxy?: boolean
10
+ }
11
+
12
+ function stripBrackets(ip: string): string {
13
+ if (ip.startsWith("[") && ip.endsWith("]")) {
14
+ return ip.slice(1, -1)
15
+ }
16
+ return ip
17
+ }
18
+
19
+ function defaultGetIp(req: Request): string | null {
20
+ const cfIp = req.headers.get("cf-connecting-ip")
21
+ if (cfIp) return stripBrackets(cfIp.trim())
22
+
23
+ return null
24
+ }
25
+
26
+ function proxyAwareGetIp(req: Request): string | null {
27
+ const cfIp = req.headers.get("cf-connecting-ip")
28
+ if (cfIp) return stripBrackets(cfIp.trim())
29
+
30
+ const xff = req.headers.get("x-forwarded-for")
31
+ if (xff) return stripBrackets(xff.split(",")[0].trim())
32
+
33
+ const realIp = req.headers.get("x-real-ip")
34
+ if (realIp) return stripBrackets(realIp.trim())
35
+
36
+ return null
37
+ }
38
+
39
+ function parseIpv4(ip: string): number | null {
40
+ const parts = ip.split(".")
41
+ if (parts.length !== 4) return null
42
+ let result = 0
43
+ for (const part of parts) {
44
+ const n = Number.parseInt(part, 10)
45
+ if (Number.isNaN(n) || n < 0 || n > 255) return null
46
+ result = (result << 8) | n
47
+ }
48
+ return result >>> 0
49
+ }
50
+
51
+ type Ipv4CidrRule = {
52
+ base: number
53
+ mask: number
54
+ v6: false
55
+ }
56
+
57
+ type Ipv6CidrRule = {
58
+ base: bigint
59
+ mask: bigint
60
+ v6: true
61
+ }
62
+
63
+ type CidrRule = Ipv4CidrRule | Ipv6CidrRule
64
+
65
+ type ParsedRule = {
66
+ cidr?: CidrRule
67
+ exact: string
68
+ }
69
+
70
+ function expandIpv6(ip: string): bigint | null {
71
+ let addr = ip
72
+
73
+ /* handle ::ffff:1.2.3.4 mapped IPv4 */
74
+ const v4Suffix = addr.match(/:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/)
75
+ if (v4Suffix) {
76
+ const v4 = parseIpv4(v4Suffix[1])
77
+ if (v4 === null) return null
78
+ addr = `${addr.slice(0, addr.lastIndexOf(":"))}:${((v4 >>> 16) & 0xffff).toString(16)}:${(v4 & 0xffff).toString(16)}`
79
+ }
80
+
81
+ /* expand :: */
82
+ const halves = addr.split("::")
83
+ if (halves.length > 2) return null
84
+
85
+ let groups: string[]
86
+ if (halves.length === 2) {
87
+ const left = halves[0] === "" ? [] : halves[0].split(":")
88
+ const right = halves[1] === "" ? [] : halves[1].split(":")
89
+ const fill = 8 - left.length - right.length
90
+ if (fill < 0) return null
91
+ groups = [...left, ...Array.from<string>({ length: fill }).fill("0"), ...right]
92
+ } else {
93
+ groups = addr.split(":")
94
+ }
95
+
96
+ if (groups.length !== 8) return null
97
+
98
+ let result = 0n
99
+ for (const g of groups) {
100
+ const n = Number.parseInt(g, 16)
101
+ if (Number.isNaN(n) || n < 0 || n > 0xffff) return null
102
+ result = (result << 16n) | BigInt(n)
103
+ }
104
+ return result
105
+ }
106
+
107
+ function isIpv6(ip: string): boolean {
108
+ return ip.includes(":")
109
+ }
110
+
111
+ function parseRule(rule: string): ParsedRule {
112
+ const slashIdx = rule.indexOf("/")
113
+ if (slashIdx === -1) {
114
+ return { exact: rule }
115
+ }
116
+
117
+ const ip = rule.slice(0, slashIdx)
118
+ const bits = Number.parseInt(rule.slice(slashIdx + 1), 10)
119
+
120
+ if (isIpv6(ip)) {
121
+ const base = expandIpv6(ip)
122
+ if (base === null || Number.isNaN(bits) || bits < 0 || bits > 128) {
123
+ return { exact: rule }
124
+ }
125
+ const mask = bits === 0 ? 0n : ((1n << BigInt(bits)) - 1n) << BigInt(128 - bits)
126
+ return { cidr: { base: base & mask, mask, v6: true }, exact: rule }
127
+ }
128
+
129
+ const base = parseIpv4(ip)
130
+ if (base === null || Number.isNaN(bits) || bits < 0 || bits > 32) {
131
+ return { exact: rule }
132
+ }
133
+
134
+ const mask = bits === 0 ? 0 : (~0 << (32 - bits)) >>> 0
135
+ return { cidr: { base: (base & mask) >>> 0, mask, v6: false }, exact: rule }
136
+ }
137
+
138
+ function matchesRule(ip: string, rule: ParsedRule): boolean {
139
+ if (rule.cidr) {
140
+ if (rule.cidr.v6) {
141
+ const parsed = expandIpv6(ip)
142
+ if (parsed === null) return false
143
+ return (parsed & rule.cidr.mask) === rule.cidr.base
144
+ }
145
+ const parsed = parseIpv4(ip)
146
+ if (parsed === null) return false
147
+ return (parsed & rule.cidr.mask) >>> 0 === rule.cidr.base
148
+ }
149
+ return ip === rule.exact
150
+ }
151
+
152
+ function throwForbidden(): never {
153
+ throw new HoneyError({
154
+ errorKey: EK.forbidden,
155
+ status: SK.forbidden,
156
+ })
157
+ }
158
+
159
+ export function ipRestrict(opts: IpRestrictOptions): MiddlewareFn<{ req: Request }, {}> {
160
+ const getIp = opts.getIp ?? (opts.trustProxy === true ? proxyAwareGetIp : defaultGetIp)
161
+ const denyRules = opts.denyList?.map(parseRule) ?? []
162
+ const allowRules = opts.allowList?.map(parseRule) ?? []
163
+ const hasAllowList = allowRules.length > 0
164
+
165
+ const mw: MiddlewareFn<{ req: Request }, {}> = (ctx, next) => {
166
+ const ip = getIp(ctx.req)
167
+
168
+ /* deny list checked first */
169
+ if (ip !== null) {
170
+ for (const rule of denyRules) {
171
+ if (matchesRule(ip, rule)) throwForbidden()
172
+ }
173
+ }
174
+
175
+ /* allow list: if set, IP must match at least one rule */
176
+ if (hasAllowList) {
177
+ if (ip === null) throwForbidden()
178
+ let allowed = false
179
+ for (const rule of allowRules) {
180
+ if (matchesRule(ip, rule)) {
181
+ allowed = true
182
+ break
183
+ }
184
+ }
185
+ if (!allowed) throwForbidden()
186
+ }
187
+
188
+ return next()
189
+ }
190
+
191
+ return mw
192
+ }
package/src/logger.ts ADDED
@@ -0,0 +1,149 @@
1
+ import type { MiddlewareFn } from "./middleware.ts"
2
+
3
+ /* ── log levels (pino-compatible numeric values) ── */
4
+
5
+ type LogLevel = "debug" | "error" | "fatal" | "info" | "trace" | "warn"
6
+
7
+ const LEVELS: Record<LogLevel, number> = {
8
+ debug: 20,
9
+ error: 50,
10
+ fatal: 60,
11
+ info: 30,
12
+ trace: 10,
13
+ warn: 40,
14
+ }
15
+
16
+ /* ── logger instance ── */
17
+
18
+ type LoggerInstance = {
19
+ child(bindings: Record<string, unknown>): LoggerInstance
20
+ debug(msg: string): void
21
+ debug(obj: Record<string, unknown>, msg?: string): void
22
+ error(msg: string): void
23
+ error(obj: Record<string, unknown>, msg?: string): void
24
+ fatal(msg: string): void
25
+ fatal(obj: Record<string, unknown>, msg?: string): void
26
+ info(msg: string): void
27
+ info(obj: Record<string, unknown>, msg?: string): void
28
+ level: LogLevel
29
+ trace(msg: string): void
30
+ trace(obj: Record<string, unknown>, msg?: string): void
31
+ warn(msg: string): void
32
+ warn(obj: Record<string, unknown>, msg?: string): void
33
+ }
34
+
35
+ type CreateLoggerOptions = {
36
+ base?: Record<string, unknown>
37
+ level?: LogLevel
38
+ write?: (line: string) => void
39
+ }
40
+
41
+ function createLogger(opts?: CreateLoggerOptions): LoggerInstance {
42
+ const threshold = LEVELS[opts?.level ?? "info"]
43
+ const write = opts?.write ?? ((line: string) => console.log(line))
44
+ const base = opts?.base ?? {}
45
+
46
+ function emit(level: number, first: Record<string, unknown> | string, second?: string): void {
47
+ if (level < threshold) return
48
+ const obj = typeof first === "string"
49
+ ? { ...base, level, msg: first, time: Date.now() }
50
+ : { ...base, ...first, level, msg: second ?? "", time: Date.now() }
51
+ write(JSON.stringify(obj))
52
+ }
53
+
54
+ const instance: LoggerInstance = {
55
+ child(bindings: Record<string, unknown>): LoggerInstance {
56
+ return createLogger({
57
+ base: { ...base, ...bindings },
58
+ level: opts?.level,
59
+ write,
60
+ })
61
+ },
62
+ debug: (first: Record<string, unknown> | string, second?: string) => emit(LEVELS.debug, first, second),
63
+ error: (first: Record<string, unknown> | string, second?: string) => emit(LEVELS.error, first, second),
64
+ fatal: (first: Record<string, unknown> | string, second?: string) => emit(LEVELS.fatal, first, second),
65
+ info: (first: Record<string, unknown> | string, second?: string) => emit(LEVELS.info, first, second),
66
+ level: opts?.level ?? "info",
67
+ trace: (first: Record<string, unknown> | string, second?: string) => emit(LEVELS.trace, first, second),
68
+ warn: (first: Record<string, unknown> | string, second?: string) => emit(LEVELS.warn, first, second),
69
+ }
70
+
71
+ return instance
72
+ }
73
+
74
+ /* ── request lifecycle middleware ── */
75
+
76
+ type LogData = {
77
+ duration: number
78
+ method: string
79
+ path: string
80
+ requestId: string | null
81
+ status: number
82
+ }
83
+
84
+ type LoggerOptions = {
85
+ instance?: LoggerInstance
86
+ log?: (data: LogData) => void
87
+ skip?: (data: LogData) => boolean
88
+ }
89
+
90
+ function defaultLog(data: LogData): void {
91
+ const rid = data.requestId ? ` ${data.requestId}` : ""
92
+ console.log(`${data.method} ${data.path} ${data.status} ${data.duration.toFixed(2)}ms${rid}`)
93
+ }
94
+
95
+ const noop = () => {}
96
+ const noopLogger: LoggerInstance = createLogger({ level: "fatal", write: noop })
97
+
98
+ function logger(
99
+ options?: LoggerOptions,
100
+ ): MiddlewareFn<{ path: string, req: Request }, { log: LoggerInstance }> {
101
+ const log = options?.log ?? defaultLog
102
+ const skip = options?.skip
103
+ const instance = options?.instance
104
+
105
+ return async (ctx, next) => {
106
+ const start = performance.now()
107
+ const method = ctx.req.method
108
+ const path = ctx.path
109
+ const rid = (ctx as Record<string, unknown>)["requestId"] as string | null ?? null
110
+
111
+ const child = instance
112
+ ? instance.child({
113
+ method,
114
+ path,
115
+ ...(rid ? { requestId: rid } : {}),
116
+ })
117
+ : noopLogger
118
+
119
+ if (instance) {
120
+ child.info(`--> ${method} ${path}`)
121
+ }
122
+
123
+ const response = await next({ log: child })
124
+
125
+ const duration = performance.now() - start
126
+ const data: LogData = {
127
+ duration,
128
+ method,
129
+ path,
130
+ requestId: rid,
131
+ status: response.status,
132
+ }
133
+
134
+ if (skip?.(data)) {
135
+ return response
136
+ }
137
+
138
+ if (instance) {
139
+ child.info({ duration, status: response.status }, `<-- ${method} ${path}`)
140
+ } else {
141
+ log(data)
142
+ }
143
+
144
+ return response
145
+ }
146
+ }
147
+
148
+ export { createLogger, logger }
149
+ export type { CreateLoggerOptions, LogData, LoggerInstance, LoggerOptions, LogLevel }
@@ -0,0 +1,199 @@
1
+ /** phantom wrapper — at runtime it's just Response, carries TAdds for inference */
2
+ export type MiddlewareResult<TAdds = {}> = Response & {
3
+ readonly __adds?: TAdds;
4
+ };
5
+
6
+ export type MiddlewareFn<
7
+ TCtx = {},
8
+ TAdds = {},
9
+ TErrors extends string = string,
10
+ > = ((
11
+ ctx: TCtx,
12
+ next: {
13
+ <T>(additions: T): Promise<MiddlewareResult<T>>;
14
+ (): Promise<MiddlewareResult<{}>>;
15
+ },
16
+ ) => Promise<MiddlewareResult<TAdds>>) & {
17
+ errors?: readonly TErrors[];
18
+ };
19
+
20
+ /** runtime type for stored middleware — erased generics, used internally */
21
+ export type RuntimeMiddleware = (
22
+ ctx: Record<string, unknown>,
23
+ next: (additions?: Record<string, unknown>) => Promise<Response>,
24
+ ) => Promise<Response>;
25
+
26
+ /**
27
+ * Create middleware with error keys constrained to a factory.
28
+ * Error keys are stored on the function and auto-accumulated by .use().
29
+ */
30
+ export function defineMiddleware<
31
+ TReqs,
32
+ TAdds,
33
+ TFactory extends Record<string, (...args: never[]) => unknown>,
34
+ TKeys extends keyof TFactory & string,
35
+ >(opts: {
36
+ errors?: [TFactory, ...TKeys[]];
37
+ fn: MiddlewareFn<TReqs, TAdds>;
38
+ }): MiddlewareFn<TReqs, TAdds, TKeys> {
39
+ const fn = opts.fn as MiddlewareFn<TReqs, TAdds, TKeys>;
40
+ if (opts.errors) {
41
+ const [, ...keys] = opts.errors;
42
+ Object.defineProperty(fn, "errors", { value: keys });
43
+ }
44
+ return fn;
45
+ }
46
+
47
+ type NextFn = {
48
+ <T>(additions: T): Promise<MiddlewareResult<T>>;
49
+ (): Promise<MiddlewareResult<{}>>;
50
+ };
51
+
52
+ /** Extract TAdds from callback return type — distributes over unions */
53
+ type ExtractAdds<T> = T extends Promise<MiddlewareResult<infer A>> ? A : never
54
+
55
+ /**
56
+ * Middleware factory — annotate ctx for TReqs, TAdds inferred from return type:
57
+ * createMiddleware((ctx: { env: { DB: D1Database } }, next) => next({ db }))
58
+ */
59
+ export function createMiddleware<TReqs = {}, TRet extends Promise<Response> = Promise<MiddlewareResult<{}>>>(
60
+ fn: (ctx: TReqs, next: NextFn) => TRet,
61
+ ): MiddlewareFn<TReqs, ExtractAdds<TRet>> {
62
+ return fn as MiddlewareFn<TReqs, ExtractAdds<TRet>>
63
+ }
64
+
65
+ /**
66
+ * Context properties that middleware additions must not overwrite.
67
+ * These are set by the framework during request construction.
68
+ * TypeScript prevents this at compile time; this guard catches runtime bypasses.
69
+ */
70
+ const RESERVED_CTX_KEYS = new Set([
71
+ "background",
72
+ "cookies",
73
+ "env",
74
+ "headers",
75
+ "params",
76
+ "req",
77
+ "res",
78
+ "search",
79
+ ]);
80
+
81
+ /**
82
+ * Compile a middleware chain + handler into a single function at registration time.
83
+ * Eliminates per-request recursive dispatch, closure allocation, and Promise.resolve wrapping.
84
+ */
85
+ export function compileChain(
86
+ middlewares: RuntimeMiddleware[],
87
+ handler: (ctx: object) => Response | Promise<Response>,
88
+ ): (ctx: object) => Response | Promise<Response> {
89
+ if (middlewares.length === 0) {
90
+ /* sync fast path — avoids microtask overhead for sync handlers */
91
+ return (ctx) => {
92
+ const result = handler(ctx);
93
+ if (result instanceof Promise) {
94
+ return result.then(validateResponse);
95
+ }
96
+ if (result === undefined || result === null) {
97
+ throw new Error(
98
+ "handler must return a Response — did you forget 'return ctx.res...()'?",
99
+ );
100
+ }
101
+ return result;
102
+ };
103
+ }
104
+
105
+ if (middlewares.length === 1) {
106
+ const mw = middlewares[0];
107
+ if (!mw) {
108
+ throw new Error("middleware chain invariant violated");
109
+ }
110
+ return (ctx) => {
111
+ const mwCtx = ctx as Record<string, unknown>;
112
+ let called = false;
113
+ return Promise.resolve(
114
+ mw(mwCtx, (additions?: Record<string, unknown>) => {
115
+ if (called) throw new Error("next() called multiple times");
116
+ called = true;
117
+ if (additions) mergeAdditions(ctx, additions);
118
+ return Promise.resolve(handler(ctx)).then(validateResponse);
119
+ }),
120
+ ).then(validateMiddlewareResponse);
121
+ };
122
+ }
123
+
124
+ /* general case — pre-bind middleware array, no per-request index tracking */
125
+ return (ctx) => executeChain(middlewares, ctx, handler);
126
+ }
127
+
128
+ function mergeAdditions(ctx: object, additions: Record<string, unknown>): void {
129
+ for (const key in additions) {
130
+ if (!RESERVED_CTX_KEYS.has(key)) {
131
+ (ctx as Record<string, unknown>)[key] = additions[key];
132
+ }
133
+ }
134
+ }
135
+
136
+ function validateResponse(result: Response): Response {
137
+ if (result === undefined || result === null) {
138
+ throw new Error(
139
+ "handler must return a Response — did you forget 'return ctx.res...()'?",
140
+ );
141
+ }
142
+ return result;
143
+ }
144
+
145
+ function validateMiddlewareResponse(result: Response): Response {
146
+ if (result === undefined || result === null) {
147
+ throw new Error(
148
+ "middleware must return a Response — did you forget 'return next(...)'?",
149
+ );
150
+ }
151
+ return result;
152
+ }
153
+
154
+ export function executeChain(
155
+ middlewares: RuntimeMiddleware[],
156
+ ctx: object,
157
+ handler: (ctx: object) => Response | Promise<Response>,
158
+ ): Promise<Response> {
159
+ let index = 0;
160
+ /* middleware runtime type uses Record<string, unknown> — object satisfies at runtime */
161
+ const mwCtx = ctx as Record<string, unknown>;
162
+
163
+ function dispatch(): Promise<Response> {
164
+ if (index >= middlewares.length) {
165
+ return Promise.resolve(handler(ctx)).then((result) => {
166
+ if (result === undefined || result === null) {
167
+ throw new Error(
168
+ "handler must return a Response — did you forget 'return ctx.res...()'?",
169
+ );
170
+ }
171
+ return result;
172
+ });
173
+ }
174
+ const mw = middlewares[index++];
175
+ if (!mw) {
176
+ throw new Error("middleware chain invariant violated");
177
+ }
178
+ let called = false;
179
+ return Promise.resolve(
180
+ mw(mwCtx, (additions?: Record<string, unknown>) => {
181
+ if (called) {
182
+ throw new Error("next() called multiple times");
183
+ }
184
+ called = true;
185
+ if (additions) mergeAdditions(ctx, additions);
186
+ return dispatch();
187
+ }),
188
+ ).then((result) => {
189
+ if (result === undefined || result === null) {
190
+ throw new Error(
191
+ "middleware must return a Response — did you forget 'return next(...)'?",
192
+ );
193
+ }
194
+ return result;
195
+ });
196
+ }
197
+
198
+ return dispatch();
199
+ }