{"version":3,"sources":["../../../../node_modules/tsup/assets/cjs_shims.js","../../src/sse.ts","../../src/cli/index.ts","../../src/cli/commands/build.ts","../../src/cli/commands/check.ts","../../src/cli/commands/create.ts","../../src/cli/generators/controller.ts","../../src/cli/generators/repository.ts","../../src/cli/generators/service.ts","../../src/cli/commands/db.ts","../../src/cli/commands/init.ts","../../src/cli/commands/make-docs.ts","../../src/core-exports.ts","../../src/ai.ts","../../src/app.ts","../../src/container.ts","../../src/context.ts","../../src/decorators.ts","../../src/helpers/async-handler.ts","../../src/helpers/response.ts","../../src/logger.ts","../../src/router/index.ts","../../src/ffi/index.ts","../../src/ffi/fallback.ts","../../src/router/node.ts","../../src/auth.ts","../../src/oauth/index.ts","../../src/oauth/types.ts","../../src/oauth/pkce.ts","../../src/oauth/state.ts","../../src/helpers/cookie.ts","../../src/oauth/provider.ts","../../src/oauth/helpers.ts","../../src/oauth/providers/google.ts","../../src/oauth/providers/github.ts","../../src/oauth/providers/apple.ts","../../src/base-controller.ts","../../src/base-service.ts","../../src/cache.ts","../../src/emitter.ts","../../src/helpers/async.ts","../../src/helpers/crypto.ts","../../src/helpers/date.ts","../../src/helpers/timezone.ts","../../src/helpers/id.ts","../../src/helpers/network.ts","../../src/helpers/number.ts","../../src/helpers/object.ts","../../src/helpers/password.ts","../../src/helpers/string.ts","../../src/mailer.ts","../../src/template.ts","../../src/middlewares/audit-log.ts","../../src/middlewares/body-size-limit.ts","../../src/middlewares/compress.ts","../../src/middlewares/cors.ts","../../src/middlewares/health-check.ts","../../src/middlewares/helmet.ts","../../src/middlewares/rate-limiter.ts","../../src/middlewares/request-id.ts","../../src/middlewares/response-time.ts","../../src/middlewares/timeout.ts","../../src/middlewares/rbac.ts","../../src/middlewares/validator.ts","../../src/queue.ts","../../src/payment/index.ts","../../src/payment/types.ts","../../src/payment/errors.ts","../../src/payment/helpers.ts","../../src/payment/middleware/webhook.ts","../../src/payment/drivers/stripe.ts","../../src/payment/drivers/midtrans.ts","../../src/payment/drivers/xendit.ts","../../src/payment/drivers/paypal.ts","../../src/schedule.ts","../../src/upload.ts","../../src/ws-helpers.ts"],"sourcesContent":["// Shim globals in cjs bundle\n// There's a weird bug that esbuild will always inject importMetaUrl\n// if we export it as `const importMetaUrl = ... __filename ...`\n// But using a function will not cause this issue\n\nconst getImportMetaUrl = () => \n  typeof document === \"undefined\" \n    ? new URL(`file:${__filename}`).href \n    : (document.currentScript && document.currentScript.tagName.toUpperCase() === 'SCRIPT') \n      ? document.currentScript.src \n      : new URL(\"main.js\", document.baseURI).href;\n\nexport const importMetaUrl = /* @__PURE__ */ getImportMetaUrl()\n","/**\n * Industrial SSE - Bun-only, in-memory default, pluggable to Redis/storage\n * Pattern mirip `StorageDriver` di `upload.ts:24` - default Memory, swap ke Redis tinggal inject.\n */\n\nexport interface SSEMessage {\n\t/** Event name */\n\tevent?: string;\n\t/** Message data */\n\tdata: string | object;\n\t/** Unique message ID for resumption */\n\tid?: string | number;\n}\n\n// ── Pluggable History Store (mirip StorageDriver) ──\nexport interface SSEHistoryStore {\n\t/** Persist message (dipanggil setiap send dengan id) */\n\tadd(message: SSEMessage): Promise<void> | void;\n\t/** Ambil pesan setelah lastEventId */\n\tgetAfter(lastEventId: string): Promise<SSEMessage[]> | SSEMessage[];\n\t/** Optional clear */\n\tclear?(): Promise<void> | void;\n}\n\n/** Default in-memory ring buffer 1000 msg - zero-deps */\nexport class MemorySSEHistory implements SSEHistoryStore {\n\tprivate messages: SSEMessage[] = [];\n\tconstructor(private maxSize = 1000) {}\n\tadd(message: SSEMessage): void {\n\t\t// hanya simpan yang punya id (butuh id untuk replay)\n\t\tif (message.id === undefined) return;\n\t\tthis.messages.push({ ...message });\n\t\tif (this.messages.length > this.maxSize) this.messages.shift();\n\t}\n\tgetAfter(lastEventId: string): SSEMessage[] {\n\t\tconst idx = this.messages.findIndex((m) => String(m.id) === String(lastEventId));\n\t\tif (idx === -1) return [];\n\t\treturn this.messages.slice(idx + 1);\n\t}\n\tclear(): void {\n\t\tthis.messages = [];\n\t}\n}\n\n// ── Pluggable PubSub (untuk broadcast cluster) ──\nexport interface SSEPubSub {\n\tpublish(channel: string, message: SSEMessage): Promise<void> | void;\n\tsubscribe(channel: string, handler: (msg: SSEMessage) => void): () => void;\n}\n\nexport class MemorySSEPubSub implements SSEPubSub {\n\tprivate channels = new Map<string, Set<(msg: SSEMessage) => void>>();\n\tpublish(channel: string, message: SSEMessage): void {\n\t\tconst subs = this.channels.get(channel);\n\t\tif (!subs) return;\n\t\tfor (const cb of subs) {\n\t\t\ttry {\n\t\t\t\tcb(message);\n\t\t\t} catch {}\n\t\t}\n\t}\n\tsubscribe(channel: string, handler: (msg: SSEMessage) => void): () => void {\n\t\tlet set = this.channels.get(channel);\n\t\tif (!set) {\n\t\t\tset = new Set();\n\t\t\tthis.channels.set(channel, set);\n\t\t}\n\t\tset.add(handler);\n\t\treturn () => set!.delete(handler);\n\t}\n}\n\nexport interface SSEOptions {\n\t/** Send initial connection event */\n\tsendInitial?: boolean;\n\t/** Custom event name for initial connection */\n\tinitialEvent?: string;\n\t/** Retry timeout in milliseconds (client reconnection) - default 3000 */\n\tretry?: number;\n\t/**\n\t * Maximum number of concurrent SSE connections.\n\t * When exceeded, new connections receive 503 Service Unavailable.\n\t * Set to 0 for unlimited (default).\n\t */\n\tmaxConnections?: number;\n\t/**\n\t * Callback to replay missed events on reconnection.\n\t * Receives the Last-Event-ID header value from the client.\n\t * Return an array of messages to replay.\n\t * Jika `historyStore` disediakan, callback ini opsional - store akan dipakai otomatis.\n\t */\n\tonReconnect?: (lastEventId: string) => Promise<SSEMessage[]>;\n\t/** Pluggable history store - default MemorySSEHistory (mirip StorageDriver) */\n\thistoryStore?: SSEHistoryStore;\n\t/** Custom headers tambahan */\n\theaders?: Record<string, string>;\n}\n\n/**\n * Global connection tracker for maxConnections enforcement.\n */\nconst connectionTracker = {\n\tconnections: new Set<SSE>(),\n};\n\n// Global default history (dipakai jika options.historyStore tidak diisi)\nconst defaultHistory = new MemorySSEHistory(1000);\n\nexport class SSE {\n\tprivate controller: ReadableStreamDefaultController | null = null;\n\tprivate encoder = new TextEncoder();\n\tprivate closed = false;\n\tprivate heartbeatInterval: ReturnType<typeof setInterval> | null = null;\n\tprivate lastEventId: string | null = null;\n\tprivate abortHandler: (() => void) | null = null;\n\tprivate historyStore: SSEHistoryStore;\n\n\tconstructor(\n\t\tprivate request: Request,\n\t\tprivate options: SSEOptions = {},\n\t) {\n\t\t// Capture Last-Event-ID header (case-insensitive) + query fallback untuk proxy\n\t\tconst headerId = request.headers.get(\"last-event-id\");\n\t\tconst url = new URL(request.url);\n\t\tconst queryId = url.searchParams.get(\"lastEventId\") || url.searchParams.get(\"last-event-id\");\n\t\tthis.lastEventId = headerId || queryId;\n\t\tthis.historyStore = options.historyStore ?? defaultHistory;\n\t\t// Pre-aborted check\n\t\tif (request.signal.aborted) {\n\t\t\tthis.closed = true;\n\t\t}\n\t}\n\n\t/**\n\t * Create SSE response with ReadableStream - Bun-only, no external deps\n\t */\n\tconnect(): Response {\n\t\tif (this.closed && this.request.signal.aborted) {\n\t\t\treturn new Response(null, { status: 409 });\n\t\t}\n\t\t// Check maxConnections limit\n\t\tconst maxConnections = this.options.maxConnections ?? 0;\n\t\tif (maxConnections > 0 && connectionTracker.connections.size >= maxConnections) {\n\t\t\treturn new Response(\n\t\t\t\tJSON.stringify({\n\t\t\t\t\terror: \"Service Unavailable\",\n\t\t\t\t\tmessage: \"Maximum connections reached. Please try again later.\",\n\t\t\t\t}),\n\t\t\t\t{\n\t\t\t\t\tstatus: 503,\n\t\t\t\t\theaders: { \"Content-Type\": \"application/json\" },\n\t\t\t\t},\n\t\t\t);\n\t\t}\n\n\t\t// Track this connection\n\t\tconnectionTracker.connections.add(this);\n\n\t\tconst stream = new ReadableStream({\n\t\t\tstart: (controller) => {\n\t\t\t\tthis.controller = controller;\n\n\t\t\t\t// Send initial connection event\n\t\t\t\tif (this.options.sendInitial !== false) {\n\t\t\t\t\tconst eventName = this.options.initialEvent || \"connected\";\n\t\t\t\t\tthis.sendMessage({ event: eventName, data: \"connected\" });\n\t\t\t\t}\n\n\t\t\t\t// Send retry timeout - default 3000 jika tidak diisi tapi client expect\n\t\t\t\tif (this.options.retry !== undefined) {\n\t\t\t\t\tthis.sendRaw(`retry: ${this.options.retry}\\n\\n`);\n\t\t\t\t} else {\n\t\t\t\t\tthis.sendRaw(`retry: 3000\\n\\n`);\n\t\t\t\t}\n\n\t\t\t\t// Start heartbeat 30s fixed (industrial keep-alive)\n\t\t\t\tthis.startHeartbeat();\n\n\t\t\t\t// Handle client disconnect - once:true + cleanup\n\t\t\t\tthis.abortHandler = () => this.close();\n\t\t\t\tthis.request.signal.addEventListener(\"abort\", this.abortHandler, { once: true } as AddEventListenerOptions);\n\n\t\t\t\t// Replay missed events on reconnection - pakai store dulu, fallback onReconnect\n\t\t\t\tif (this.lastEventId) {\n\t\t\t\t\t// async tanpa block initial event\n\t\t\t\t\tqueueMicrotask(() => this.replayMissedEvents());\n\t\t\t\t}\n\t\t\t},\n\t\t\tcancel: () => {\n\t\t\t\tthis.close();\n\t\t\t},\n\t\t});\n\n\t\tconst headers: Record<string, string> = {\n\t\t\t\"Content-Type\": \"text/event-stream; charset=utf-8\",\n\t\t\t\"Cache-Control\": \"no-cache, no-transform\",\n\t\t\tConnection: \"keep-alive\",\n\t\t\t\"X-Accel-Buffering\": \"no\", // Disable nginx buffering\n\t\t\t\"Content-Encoding\": \"none\",\n\t\t\t\"Cache-Control2\": \"no-cache\", // compat\n\t\t\t...this.options.headers,\n\t\t};\n\t\t// Explicit Transfer-Encoding chunked tidak perlu di Bun (auto), tapi keep header untuk proxy\n\t\treturn new Response(stream, {\n\t\t\tstatus: 200,\n\t\t\theaders: {\n\t\t\t\t\"Content-Type\": \"text/event-stream; charset=utf-8\",\n\t\t\t\t\"Cache-Control\": \"no-cache, no-transform\",\n\t\t\t\tConnection: \"keep-alive\",\n\t\t\t\t\"X-Accel-Buffering\": \"no\",\n\t\t\t\t...this.options.headers,\n\t\t\t},\n\t\t});\n\t}\n\n\t/**\n\t * Replay missed events from reconnection via historyStore / onReconnect\n\t */\n\tprivate async replayMissedEvents(): Promise<void> {\n\t\tif (!this.lastEventId) return;\n\n\t\ttry {\n\t\t\tlet missed: SSEMessage[] = [];\n\t\t\t// Prioritas: historyStore → onReconnect\n\t\t\tif (this.historyStore) {\n\t\t\t\tconst fromStore = await this.historyStore.getAfter(this.lastEventId);\n\t\t\t\tif (fromStore && fromStore.length > 0) missed = fromStore;\n\t\t\t}\n\t\t\tif (missed.length === 0 && this.options.onReconnect) {\n\t\t\t\tconst fromCb = await this.options.onReconnect(this.lastEventId);\n\t\t\t\tif (fromCb) missed = fromCb;\n\t\t\t}\n\t\t\tfor (const event of missed) {\n\t\t\t\tthis.send(event);\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tconsole.error(\"SSE reconnect replay error:\", error);\n\t\t}\n\t}\n\n\t/**\n\t * Send a message to the client - auto persist ke historyStore jika ada id\n\t */\n\tsend(message: SSEMessage): void {\n\t\tif (this.closed) return;\n\t\t// persist dulu untuk replay\n\t\tif (message.id !== undefined) {\n\t\t\ttry {\n\t\t\t\tconst r = this.historyStore.add(message);\n\t\t\t\tif (r instanceof Promise) r.catch(() => {});\n\t\t\t} catch {}\n\t\t}\n\t\tthis.sendMessage(message);\n\t}\n\n\t/**\n\t * Send a simple data message\n\t */\n\tsendData(data: string | object): void {\n\t\tthis.send({ data });\n\t}\n\n\t/**\n\t * Send a named event\n\t */\n\tsendEvent(event: string, data: string | object): void {\n\t\tthis.send({ event, data });\n\t}\n\n\t/**\n\t * Send a message with ID (for resumption)\n\t */\n\tsendWithId(id: string | number, data: string | object): void {\n\t\tthis.send({ id, data });\n\t}\n\n\t/**\n\t * Send SSE comment - untuk heartbeat custom\n\t */\n\tsendComment(comment: string): void {\n\t\tconst sanitized = String(comment).replace(/[\\r\\n]/g, \" \");\n\t\tthis.sendRaw(`: ${sanitized}\\n\\n`);\n\t}\n\n\tprivate disconnectCallbacks: Set<() => void> = new Set();\n\n\t/**\n\t * Register a callback to be called when the connection closes\n\t */\n\tonClose(callback: () => void): void {\n\t\tthis.disconnectCallbacks.add(callback);\n\t}\n\n\toffClose(callback: () => void): void {\n\t\tthis.disconnectCallbacks.delete(callback);\n\t}\n\n\t/**\n\t * Get the Last-Event-ID header value (for reconnection)\n\t */\n\tgetLastEventId(): string | null {\n\t\treturn this.lastEventId;\n\t}\n\n\t/**\n\t * Send raw string to the stream - dengan backpressure check\n\t */\n\tprivate sendRaw(raw: string): void {\n\t\tif (this.closed || !this.controller) return;\n\t\t// Backpressure: desiredSize <=0 berarti buffer penuh → drop atau tunggu\n\t\ttry {\n\t\t\tconst desired = (this.controller as unknown as { desiredSize?: number | null }).desiredSize;\n\t\t\tif (desired !== null && desired !== undefined && desired <= 0) {\n\t\t\t\tif (raw.startsWith(\":\")) return; // drop heartbeat jika penuh - jangan warn\n\t\t\t}\n\t\t\tthis.controller.enqueue(this.encoder.encode(raw));\n\t\t} catch (_error) {\n\t\t\tthis.close();\n\t\t}\n\t}\n\n\t/**\n\t * Validate event/id agar tidak break spec (no \\r \\n \\0)\n\t */\n\tprivate sanitizeField(value: string): string {\n\t\tif (/[\\r\\n\\0]/.test(value)) throw new Error(`Invalid SSE field contains newline: ${value}`);\n\t\treturn value;\n\t}\n\n\t/**\n\t * Format and send SSE message - sanitized\n\t */\n\tprivate sendMessage(message: SSEMessage): void {\n\t\tlet raw = \"\";\n\n\t\tif (message.id !== undefined) {\n\t\t\tconst idStr = String(message.id);\n\t\t\tthis.sanitizeField(idStr);\n\t\t\traw += `id: ${idStr}\\n`;\n\t\t}\n\t\tif (message.event) {\n\t\t\tthis.sanitizeField(message.event);\n\t\t\traw += `event: ${message.event}\\n`;\n\t\t}\n\n\t\tlet data: string;\n\t\tif (typeof message.data === \"object\") {\n\t\t\tif (message.data === null) data = \"null\";\n\t\t\telse data = JSON.stringify(message.data);\n\t\t} else {\n\t\t\tdata = String(message.data);\n\t\t}\n\n\t\t// SSE spec: data cannot contain single \\n, split ke multiple data: lines, handle \\r\\n\n\t\tconst dataLines = data.split(/\\r\\n|\\r|\\n/);\n\t\tfor (const line of dataLines) {\n\t\t\traw += `data: ${line}\\n`;\n\t\t}\n\n\t\traw += \"\\n\";\n\t\tthis.sendRaw(raw);\n\t}\n\n\t/**\n\t * Start heartbeat 30s fixed - keep-alive untuk LB/ALB\n\t */\n\tprivate startHeartbeat(): void {\n\t\t// 30s fixed sesuai instruksi - tidak configurable per request (industrial default)\n\t\tthis.heartbeatInterval = setInterval(() => {\n\t\t\tthis.sendRaw(\": heartbeat\\n\\n\");\n\t\t}, 30000);\n\t}\n\n\t/**\n\t * Close the SSE connection - idempotent + cleanup listener\n\t */\n\tclose(): void {\n\t\tif (this.closed) return;\n\t\tthis.closed = true;\n\n\t\t// Remove from global tracker\n\t\tconnectionTracker.connections.delete(this);\n\n\t\tfor (const cb of this.disconnectCallbacks) {\n\t\t\ttry {\n\t\t\t\tcb();\n\t\t\t} catch (e) {\n\t\t\t\tconsole.error(\"SSE onClose error:\", e);\n\t\t\t}\n\t\t}\n\t\tthis.disconnectCallbacks.clear();\n\n\t\tif (this.abortHandler) {\n\t\t\ttry {\n\t\t\t\tthis.request.signal.removeEventListener(\"abort\", this.abortHandler);\n\t\t\t} catch {}\n\t\t\tthis.abortHandler = null;\n\t\t}\n\n\t\tif (this.heartbeatInterval) {\n\t\t\tclearInterval(this.heartbeatInterval);\n\t\t\tthis.heartbeatInterval = null;\n\t\t}\n\n\t\tif (this.controller) {\n\t\t\ttry {\n\t\t\t\tthis.controller.close();\n\t\t\t} catch (_e) {}\n\t\t\tthis.controller = null;\n\t\t}\n\t}\n\n\t/**\n\t * Check if connection is still open\n\t */\n\tget isConnected(): boolean {\n\t\treturn !this.closed;\n\t}\n\n\t/**\n\t * Get the number of active SSE connections\n\t */\n\tstatic get activeConnections(): number {\n\t\treturn connectionTracker.connections.size;\n\t}\n\n\t/**\n\t * Get all active SSE connections\n\t */\n\tstatic getActiveConnections(): ReadonlySet<SSE> {\n\t\treturn connectionTracker.connections;\n\t}\n\n\t/** Graceful close all - dipakai di App.setupGracefulShutdown */\n\tstatic closeAll(): void {\n\t\tfor (const c of [...connectionTracker.connections]) c.close();\n\t}\n}\n\n/**\n * SSE Broadcaster - in-memory default, pluggable ke Redis via pubSub\n *\n * @example Memory (default)\n * ```ts\n * const broadcaster = new SSEBroadcaster();\n * app.get(\"/events\", (ctx) => { const sse = createSSE(ctx.request); broadcaster.add(sse); sse.onClose(()=>broadcaster.remove(sse)); return sse.connect(); })\n * broadcaster.broadcast(\"update\", {count:42})\n * ```\n * @example Redis pluggable (mirip StorageDriver)\n * ```ts\n * class RedisSSEPubSub implements SSEPubSub { constructor(private redis: Redis){} publish(ch,msg){ redis.publish(ch, JSON.stringify(msg)) } subscribe(ch,cb){ redis.subscribe(ch,(m)=>cb(JSON.parse(m))); return ()=>redis.unsubscribe(ch)} }\n * const broadcaster = new SSEBroadcaster({ pubSub: new RedisSSEPubSub(redis), channel: \"buntok:sse\" })\n * ```\n */\nexport interface SSEBroadcasterOptions {\n\tpubSub?: SSEPubSub;\n\tchannel?: string;\n\thistoryStore?: SSEHistoryStore;\n}\nexport class SSEBroadcaster {\n\tprivate clients = new Set<SSE>();\n\tprivate pubSub?: SSEPubSub;\n\tprivate channel: string;\n\tprivate historyStore?: SSEHistoryStore;\n\tprivate unsubscribe?: () => void;\n\n\tconstructor(opts?: SSEBroadcasterOptions | SSEPubSub) {\n\t\t// backward compat: new SSEBroadcaster(pubSub)\n\t\tif (opts && typeof (opts as SSEPubSub).publish === \"function\" && typeof (opts as SSEPubSub).subscribe === \"function\") {\n\t\t\tthis.pubSub = opts as SSEPubSub;\n\t\t\tthis.channel = \"buntok:sse\";\n\t\t} else {\n\t\t\tconst o = opts as SSEBroadcasterOptions | undefined;\n\t\t\tthis.pubSub = o?.pubSub;\n\t\t\tthis.channel = o?.channel ?? \"buntok:sse\";\n\t\t\tthis.historyStore = o?.historyStore;\n\t\t}\n\t\tif (this.pubSub) {\n\t\t\tthis.unsubscribe = this.pubSub.subscribe(this.channel, (msg) => {\n\t\t\t\t// fan-out ke local clients tanpa loop publish lagi\n\t\t\t\tfor (const c of [...this.clients]) {\n\t\t\t\t\tif (c.isConnected) c.send(msg);\n\t\t\t\t\telse this.clients.delete(c);\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t}\n\n\t/**\n\t * Add an SSE connection to the broadcaster\n\t */\n\tadd(sse: SSE): void {\n\t\tthis.clients.add(sse);\n\t}\n\n\t/**\n\t * Remove an SSE connection from the broadcaster\n\t */\n\tremove(sse: SSE): void {\n\t\tthis.clients.delete(sse);\n\t}\n\n\t/**\n\t * Broadcast a named event to all connected clients - via pubSub jika ada\n\t */\n\tbroadcast(event: string, data: string | object): void {\n\t\tconst msg: SSEMessage = { event, data };\n\t\tif (this.pubSub) {\n\t\t\tconst r = this.pubSub.publish(this.channel, msg);\n\t\t\tif (r instanceof Promise) r.catch(() => this.localBroadcast(msg));\n\t\t\telse this.localBroadcast(msg);\n\t\t\treturn;\n\t\t}\n\t\tthis.localBroadcast(msg);\n\t}\n\n\tprivate localBroadcast(msg: SSEMessage): void {\n\t\tfor (const client of [...this.clients]) {\n\t\t\tif (client.isConnected) {\n\t\t\t\tclient.send(msg as SSEMessage);\n\t\t\t} else {\n\t\t\t\tthis.clients.delete(client);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Broadcast to clients matching a predicate\n\t */\n\tbroadcastWhere(\n\t\tpredicate: (sse: SSE) => boolean,\n\t\tevent: string,\n\t\tdata: string | object,\n\t): void {\n\t\tconst msg: SSEMessage = { event, data };\n\t\t// jika pubSub ada, tetap filter lokal (predicate tidak bisa di-remote)\n\t\tfor (const client of [...this.clients]) {\n\t\t\tif (!client.isConnected) {\n\t\t\t\tthis.clients.delete(client);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (predicate(client)) {\n\t\t\t\tclient.send(msg);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Send a message to all connected clients\n\t */\n\tsendAll(message: SSEMessage): void {\n\t\tif (this.pubSub) {\n\t\t\tconst r = this.pubSub.publish(this.channel, message);\n\t\t\tif (r instanceof Promise) r.catch(() => this.localSendAll(message));\n\t\t\telse this.localSendAll(message);\n\t\t\treturn;\n\t\t}\n\t\tthis.localSendAll(message);\n\t}\n\tprivate localSendAll(message: SSEMessage): void {\n\t\tfor (const client of [...this.clients]) {\n\t\t\tif (client.isConnected) client.send(message);\n\t\t\telse this.clients.delete(client);\n\t\t}\n\t}\n\n\t/**\n\t * Close all connections + unsubscribe pubSub\n\t */\n\tcloseAll(): void {\n\t\tif (this.unsubscribe) {\n\t\t\ttry { this.unsubscribe(); } catch {}\n\t\t\tthis.unsubscribe = undefined;\n\t\t}\n\t\tfor (const client of [...this.clients]) {\n\t\t\tclient.close();\n\t\t}\n\t\tthis.clients.clear();\n\t}\n\n\t/**\n\t * Get the number of connected clients\n\t */\n\tget size(): number {\n\t\t// Clean up disconnected clients\n\t\tfor (const client of [...this.clients]) {\n\t\t\tif (!client.isConnected) {\n\t\t\t\tthis.clients.delete(client);\n\t\t\t}\n\t\t}\n\t\treturn this.clients.size;\n\t}\n\n\t/**\n\t * Check if there are any connected clients\n\t */\n\tget isEmpty(): boolean {\n\t\treturn this.size === 0;\n\t}\n}\n\n/**\n * Create SSE helper from Buntok context - Bun-only\n */\nexport function createSSE(request: Request, options?: SSEOptions): SSE {\n\treturn new SSE(request, options);\n}\n","#!/usr/bin/env bun\n\nimport { buildCommand } from \"./commands/build.js\";\nimport { checkCommand } from \"./commands/check.js\";\nimport { createCommand } from \"./commands/create.js\";\nimport { dbCommand } from \"./commands/db.js\";\nimport { initCommand } from \"./commands/init.js\";\nimport { makeDocsCommand } from \"./commands/make-docs.js\";\nimport { VERSION } from \"../core-exports.js\";\n\nfunction printBanner() {\n\tconsole.log(`\n\\x1b[36m  Buntok CLI v${VERSION}\\x1b[0m\n`);\n}\n\nfunction printUsage() {\n\tconsole.log(`\n\\x1b[36mUsage:\\x1b[0m\n  buntok <command> [arguments] [options]\n\n\\x1b[36mCommands:\\x1b[0m\n  init                   Setup project: copy SKILL.md + configure package.json + generate env.ts\n  build                  Build project for production (output → .buntok/)\n  check                  Run TypeScript type check\n  create <entity>        Generate all files for entity (repo, service, controller)\n  db <command>           Database operations (migrate, seed, reset, generate, studio, status)\n  make:docs              Generate OpenAPI documentation automatically\n\n\\x1b[36mOptions (for create command):\\x1b[0m\n  --repo                 Generate only repository\n  --service              Generate only service\n  --controller           Generate only controller\n  --dry-run              Preview files without writing\n\n\\x1b[36mAliases:\\x1b[0m\n  g, gen, generate       Shortcut for create\n\n\\x1b[36mExamples:\\x1b[0m\n  buntok init                             # Initialize project setup\n  buntok build                            # Build project for production\n  buntok check                            # Run TypeScript type check\n  buntok create user                      # Generate all files for user entity\n  buntok g user --repo --service          # Generate repository and service only\n  buntok g user --dry-run                 # Preview what would be generated\n  buntok db migrate                       # Run pending migrations\n  buntok db seed                          # Seed database\n  buntok make:docs                        # Generate OpenAPI documentation\n`);\n}\n\nexport async function main() {\n\tconst args = process.argv.slice(2);\n\tconst command = args[0];\n\tconst arg1 = args[1];\n\n\tif (!command) {\n\t\tprintBanner();\n\t\tprintUsage();\n\t\tprocess.exitCode = 1;\n\t\treturn;\n\t}\n\n\tswitch (command) {\n\t\tcase \"init\":\n\t\t\tawait initCommand();\n\t\t\tbreak;\n\t\tcase \"build\":\n\t\t\tawait buildCommand();\n\t\t\tbreak;\n\t\tcase \"check\":\n\t\t\tawait checkCommand();\n\t\t\tbreak;\n\t\tcase \"g\":\n\t\tcase \"gen\":\n\t\tcase \"generate\":\n\t\tcase \"create\":\n\t\t\tif (!arg1) {\n\t\t\t\tconsole.error(\n\t\t\t\t\t\"\\x1b[31mError: entity name is required for create command\\x1b[0m\",\n\t\t\t\t);\n\t\t\t\tprocess.exitCode = 1;\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tawait createCommand(arg1, args.slice(2));\n\t\t\tbreak;\n\t\tcase \"db\":\n\t\t\tawait dbCommand(args.slice(1));\n\t\t\tbreak;\n\t\tcase \"make:docs\":\n\t\t\tawait makeDocsCommand();\n\t\t\tbreak;\n\t\tdefault: {\n\t\t\tconst commands = [\n\t\t\t\t\"init\",\n\t\t\t\t\"build\",\n\t\t\t\t\"check\",\n\t\t\t\t\"create\",\n\t\t\t\t\"g\",\n\t\t\t\t\"db\",\n\t\t\t\t\"make:docs\",\n\t\t\t];\n\t\t\t// Simple fuzzy match: prefix or contains\n\t\t\tconst closest = commands.find(\n\t\t\t\t(c) =>\n\t\t\t\t\tc.startsWith(command) ||\n\t\t\t\t\tcommand.startsWith(c) ||\n\t\t\t\t\tc.includes(command) ||\n\t\t\t\t\tcommand.includes(c),\n\t\t\t);\n\t\t\tconst hint = closest ? `\\n  Did you mean: buntok ${closest}?` : \"\";\n\t\t\tconsole.error(`\\x1b[31mUnknown command: ${command}\\x1b[0m${hint}`);\n\t\t\tprintUsage();\n\t\t\tprocess.exitCode = 1;\n\t\t\treturn;\n\t\t}\n\t}\n}\n\nmain();\n","import { join } from \"node:path\";\nimport { existsSync } from \"node:fs\";\n\nexport async function buildCommand() {\n\tconst projectRoot = process.cwd();\n\tconst entryPoint = join(projectRoot, \"src/index.ts\");\n\tconst outDir = join(projectRoot, \".buntok\");\n\n\tif (!existsSync(entryPoint)) {\n\t\tconsole.error(\"\\x1b[31m❌ src/index.ts not found\\x1b[0m\");\n\t\tprocess.exitCode = 1;\n\t\treturn;\n\t}\n\n\tconsole.log(\"\\x1b[36m🔨 Building project...\\x1b[0m\");\n\n\tconst result = await Bun.build({\n\t\tentrypoints: [entryPoint],\n\t\toutdir: outDir,\n\t\ttarget: \"bun\",\n\t\ttsconfig: join(projectRoot, \"tsconfig.json\"),\n\t\tpackages: \"external\",\n\t});\n\n\tif (!result.success) {\n\t\tconsole.error(\"\\x1b[31m❌ Build failed:\\x1b[0m\");\n\t\tfor (const log of result.logs) {\n\t\t\tconsole.error(log);\n\t\t}\n\t\tprocess.exitCode = 1;\n\t\treturn;\n\t}\n\n\tconsole.log(\"\\x1b[32m✅ Build successful → .buntok/index.js\\x1b[0m\");\n\tconsole.log(\"\\x1b[90m  Deploy: copy .buntok/ + node_modules/ + package.json to server\\x1b[0m\");\n}\n","import { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\n\nexport async function checkCommand(): Promise<void> {\n\tconst cwd = process.cwd();\n\tconst tsconfigPath = join(cwd, \"tsconfig.json\");\n\n\tif (!existsSync(tsconfigPath)) {\n\t\tconsole.error(\n\t\t\t\"\\x1b[31mError: tsconfig.json not found. Run `buntok init` first.\\x1b[0m\",\n\t\t);\n\t\tprocess.exitCode = 1;\n\t\treturn;\n\t}\n\n\tconsole.log(\"\\x1b[36m  Running type check...\\x1b[0m\\n\");\n\n\tconst proc = Bun.spawnSync(\n\t\t[\"bunx\", \"tsc\", \"--noEmit\", \"--project\", tsconfigPath],\n\t\t{ cwd },\n\t);\n\n\tif (proc.exitCode === 0) {\n\t\tconsole.log(\"\\n\\x1b[32m  ✓ Type check passed\\x1b[0m\\n\");\n\t} else {\n\t\tconsole.log(\"\\n\\x1b[31m  ✗ Type check failed\\x1b[0m\\n\");\n\t}\n\n\tprocess.exitCode = proc.exitCode ?? 1;\n}\n","import { existsSync, readFileSync } from \"node:fs\";\nimport fs from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { generateController } from \"../generators/controller.js\";\nimport { generateRepository } from \"../generators/repository.js\";\nimport { generateService } from \"../generators/service.js\";\n\ninterface CreateOptions {\n\trepo: boolean;\n\tservice: boolean;\n\tcontroller: boolean;\n\tall: boolean;\n\tdryRun: boolean;\n}\n\nfunction parseOptions(args: string[]): CreateOptions {\n\tconst options: CreateOptions = {\n\t\trepo: false,\n\t\tservice: false,\n\t\tcontroller: false,\n\t\tall: true,\n\t\tdryRun: false,\n\t};\n\n\tfor (const arg of args) {\n\t\tswitch (arg) {\n\t\t\tcase \"--repo\":\n\t\t\t\toptions.repo = true;\n\t\t\t\toptions.all = false;\n\t\t\t\tbreak;\n\t\t\tcase \"--service\":\n\t\t\t\toptions.service = true;\n\t\t\t\toptions.all = false;\n\t\t\t\tbreak;\n\t\t\tcase \"--controller\":\n\t\t\t\toptions.controller = true;\n\t\t\t\toptions.all = false;\n\t\t\t\tbreak;\n\t\t\tcase \"--dry-run\":\n\t\t\t\toptions.dryRun = true;\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\t// If any specific option is set, don't generate all\n\tif (!options.all) {\n\t\treturn options;\n\t}\n\n\t// Generate all by default\n\treturn {\n\t\trepo: true,\n\t\tservice: true,\n\t\tcontroller: true,\n\t\tall: false,\n\t\tdryRun: options.dryRun,\n\t};\n}\n\nasync function ensureDirectories(_entityName: string) {\n\tconst dirs = [\"src/repositories\", \"src/services\", \"src/controllers\"];\n\n\tawait Promise.all(\n\t\tdirs.map(async (dir) => {\n\t\t\tif (!existsSync(dir)) {\n\t\t\t\tawait fs.mkdir(dir, { recursive: true });\n\t\t\t}\n\t\t}),\n\t);\n}\n\nfunction toPascalCase(str: string): string {\n\treturn str\n\t\t.split(/[-_]/)\n\t\t.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())\n\t\t.join(\"\");\n}\n\nexport async function createCommand(entityName: string, args: string[]) {\n\tconst options = parseOptions(args);\n\tconst pascalName = toPascalCase(entityName);\n\n\tconst prefix = options.dryRun ? \"\\x1b[33m[DRY RUN]\\x1b[0m \" : \"\";\n\tconsole.log(`\\n${prefix}\\x1b[36mCreating ${pascalName} entity...\\x1b[0m\\n`);\n\n\t// Ensure directories exist (skip in dry-run)\n\tif (!options.dryRun) {\n\t\tawait ensureDirectories(entityName);\n\t}\n\n\tconst results: string[] = [];\n\tconst generatedFiles: string[] = [];\n\tconst tasks: Promise<void>[] = [];\n\n\t// Helper to generate file asynchronously\n\tconst generateFile = async (\n\t\tpath: string,\n\t\tcontentGenerator: () => string,\n\t\ttype: string,\n\t) => {\n\t\tif (options.dryRun) {\n\t\t\tconst content = contentGenerator();\n\t\t\tresults.push(`[DRY RUN] ${type}: ${path}`);\n\t\t\tconsole.log(`\\n\\x1b[90m--- ${path} ---\\x1b[0m`);\n\t\t\tconsole.log(content);\n\t\t\tconsole.log(`\\x1b[90m--- end ---\\x1b[0m\\n`);\n\t\t\treturn;\n\t\t}\n\t\tif (!existsSync(path)) {\n\t\t\tawait fs.writeFile(path, contentGenerator());\n\t\t\tresults.push(`✓ ${type}: ${path}`);\n\t\t\tgeneratedFiles.push(path);\n\t\t} else {\n\t\t\tresults.push(`• ${type}: ${path} (already exists)`);\n\t\t}\n\t};\n\n\tif (options.repo) {\n\t\ttasks.push(\n\t\t\tgenerateFile(\n\t\t\t\tjoin(\"src/repositories\", `${entityName}.repository.ts`),\n\t\t\t\t() => generateRepository(entityName, pascalName),\n\t\t\t\t\"Repository\",\n\t\t\t),\n\t\t);\n\t}\n\n\tif (options.service) {\n\t\ttasks.push(\n\t\t\tgenerateFile(\n\t\t\t\tjoin(\"src/services\", `${entityName}.service.ts`),\n\t\t\t\t() => generateService(entityName, pascalName, options.repo),\n\t\t\t\t\"Service\",\n\t\t\t),\n\t\t);\n\t}\n\n\tif (options.controller) {\n\t\ttasks.push(\n\t\t\tgenerateFile(\n\t\t\t\tjoin(\"src/controllers\", `${entityName}.controller.ts`),\n\t\t\t\t() => generateController(entityName, pascalName, options.service),\n\t\t\t\t\"Controller\",\n\t\t\t),\n\t\t);\n\t}\n\n\t// Execute all file generations concurrently\n\tawait Promise.all(tasks);\n\n\t// Auto-format generated files with Biome if available (skip in dry-run)\n\tif (generatedFiles.length > 0 && !options.dryRun) {\n\t\tconst biomeProc = Bun.spawnSync(\n\t\t\t[\"bunx\", \"biome\", \"format\", \"--write\", ...generatedFiles],\n\t\t\t{\n\t\t\t\tstdio: [\"ignore\", \"ignore\", \"ignore\"],\n\t\t\t},\n\t\t);\n\t\tif (biomeProc.exitCode === 0) {\n\t\t\tconsole.log(\n\t\t\t\t\"\\x1b[90m✨ Auto-formatted generated files with Biome\\x1b[0m\\n\",\n\t\t\t);\n\t\t}\n\t}\n\n\t// Auto-register in src/index.ts (skip in dry-run)\n\tconst indexPath = join(\"src\", \"index.ts\");\n\tif (existsSync(indexPath) && !options.dryRun) {\n\t\tconst indexContent = readFileSync(indexPath, \"utf-8\");\n\n\t\t// Build imports\n\t\tconst imports: string[] = [];\n\t\tconst instantiations: string[] = [];\n\n\t\t// Only register repo + service if full chain (they're needed by controller)\n\t\tif (options.repo && options.service) {\n\t\t\timports.push(`import { ${pascalName}Repository } from \"@/repositories/${entityName}.repository\";`);\n\t\t\tinstantiations.push(`const ${entityName}Repository = new ${pascalName}Repository();`);\n\t\t}\n\n\t\tif (options.service) {\n\t\t\tconst repoArg = options.repo ? `(${entityName}Repository)` : \"\";\n\t\t\tinstantiations.push(`const ${entityName}Service = new ${pascalName}Service${repoArg};`);\n\t\t\tif (!options.repo) {\n\t\t\t\timports.push(`import { ${pascalName}Service } from \"@/services/${entityName}.service\";`);\n\t\t\t}\n\t\t}\n\n\t\t// Always register controller\n\t\t{\n\t\t\timports.push(`import { ${pascalName}Controller } from \"@/controllers/${entityName}.controller\";`);\n\t\t\tconst serviceArg = options.service ? `(${entityName}Service)` : \"\";\n\t\t\tinstantiations.push(`app.registerController(new ${pascalName}Controller${serviceArg});`);\n\t\t}\n\n\t\tconst importBlock = imports.join(\"\\n\");\n\t\tconst instantiationBlock = instantiations.join(\"\\n\");\n\n\t\tif (!indexContent.includes(importBlock)) {\n\t\t\tconst lines = indexContent.split(\"\\n\");\n\n\t\t\t// Find the last import line and insert after it\n\t\t\tlet lastImportIndex = -1;\n\t\t\tfor (let i = 0; i < lines.length; i++) {\n\t\t\t\tif (lines[i]?.startsWith(\"import \")) {\n\t\t\t\t\tlastImportIndex = i;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (lastImportIndex !== -1) {\n\t\t\t\tlines.splice(lastImportIndex + 1, 0, importBlock);\n\t\t\t} else {\n\t\t\t\tlines.unshift(importBlock);\n\t\t\t}\n\n\t\t\t// Find app.registerController or app.listen and insert before it\n\t\t\tconst content = lines.join(\"\\n\");\n\t\t\tconst updatedContent = content.replace(\n\t\t\t\t/(app\\.(registerController|listen)\\s*\\()/,\n\t\t\t\t`${instantiationBlock}\\n\\n$1`,\n\t\t\t);\n\n\t\t\tawait fs.writeFile(indexPath, updatedContent);\n\t\t\tconsole.log(\n\t\t\t\t`\\x1b[32m✔ Registered ${pascalName} in src/index.ts\\x1b[0m`,\n\t\t\t);\n\t\t} else {\n\t\t\tconsole.log(\n\t\t\t\t`\\x1b[90m• src/index.ts: ${pascalName} already registered\\x1b[0m`,\n\t\t\t);\n\t\t}\n\t}\n\n\t// Print results\n\tconsole.log(options.dryRun ? \"\\x1b[33mWould generate:\\x1b[0m\" : \"\\x1b[32mGenerated files:\\x1b[0m\");\n\tfor (const result of results.sort()) {\n\t\tconsole.log(`  ${result}`);\n\t}\n\n\tif (options.dryRun) {\n\t\tconsole.log(`\\n\\x1b[33mRun without --dry-run to create these files.\\x1b[0m\\n`);\n\t} else {\n\t\tconsole.log(`\n\\x1b[36mNext steps:\\x1b[0m\n  1. Start dev server: \\x1b[33mbun run dev\\x1b[0m\n`);\n\t}\n}\n","export function generateController(\n\tentityName: string,\n\tpascalName: string,\n\twithService: boolean = true,\n): string {\n\tif (withService) {\n\t\treturn `import { Controller, BaseController } from \"@buntok/core\";\nimport { ${pascalName}Service } from \"@/services/${entityName}.service\";\n\n@Controller(\"/${entityName}s\")\nexport class ${pascalName}Controller extends BaseController<${pascalName}> {\n  constructor(private readonly ${entityName}Service: ${pascalName}Service) {\n    super(${entityName}Service);\n  }\n}\n`;\n\t}\n\n\treturn `import { Controller, Get, Post, Put, Delete } from \"@buntok/core\";\nimport type { Context } from \"@buntok/core\";\n\n@Controller(\"/${entityName}s\")\nexport class ${pascalName}Controller {\n  @Get(\"/\")\n  async getAll(ctx: Context) {\n    return ctx.success([], \"Records retrieved successfully\");\n  }\n\n  @Get(\"/:id\")\n  async getById(ctx: Context) {\n    return ctx.success({ id: ctx.params.id }, \"Record retrieved successfully\");\n  }\n\n  @Post(\"/\")\n  async create(ctx: Context) {\n    const data = await ctx.body<any>();\n    return ctx.success(data, \"Record created successfully\", 201);\n  }\n\n  @Put(\"/:id\")\n  async update(ctx: Context) {\n    const data = await ctx.body<any>();\n    return ctx.success({ id: ctx.params.id, ...data }, \"Record updated successfully\");\n  }\n\n  @Delete(\"/:id\")\n  async delete(ctx: Context) {\n    return ctx.success(null, \"Record deleted successfully\");\n  }\n}\n`;\n}\n","export function generateRepository(\n\tentityName: string,\n\tpascalName: string,\n): string {\n\treturn `import { BaseRepository } from \"@buntok/prisma\";\n// Replace with your own Prisma client instance\nimport { prisma } from \"@/lib/prisma\";\nimport type { ${pascalName}, PrismaClient, Prisma } from \"@prisma/client\";\n\nexport class ${pascalName}Repository extends BaseRepository<\n  ${pascalName},\n  PrismaClient,\n  Prisma.${pascalName}CreateInput,\n  Prisma.${pascalName}UpdateInput\n> {\n  constructor() {\n    super(prisma, \"${entityName}\");\n  }\n}\n`;\n}\n","export function generateService(\n\tentityName: string,\n\tpascalName: string,\n\twithRepo: boolean = true,\n): string {\n\tif (withRepo) {\n\t\treturn `import { BaseService } from \"@buntok/core\";\nimport { ${pascalName}Repository } from \"@/repositories/${entityName}.repository\";\nimport type { ${pascalName} } from \"@prisma/client\";\n\nexport class ${pascalName}Service extends BaseService<${pascalName}> {\n  constructor(private readonly ${entityName}Repository: ${pascalName}Repository) {\n    super(${entityName}Repository);\n  }\n}\n`;\n\t}\n\n\treturn `export class ${pascalName}Service {\n  async getAll(): Promise<any[]> {\n    return [];\n  }\n\n  async getById(id: string): Promise<any> {\n    return { id };\n  }\n\n  async create(data: any): Promise<any> {\n    return data;\n  }\n\n  async update(id: string, data: any): Promise<any> {\n    return { id, ...data };\n  }\n\n  async delete(id: string): Promise<boolean> {\n    return true;\n  }\n}\n`;\n}\n","import { execSync } from \"node:child_process\";\nimport { existsSync, readFileSync } from \"node:fs\";\n\ntype DbCommand = \"migrate\" | \"seed\" | \"reset\" | \"generate\" | \"studio\" | \"status\";\n\nfunction detectOrm(): \"prisma\" | \"drizzle\" | \"typeorm\" | null {\n\t// Check for Prisma\n\tif (existsSync(\"prisma/schema.prisma\") || existsSync(\"prisma/schema.ts\")) {\n\t\treturn \"prisma\";\n\t}\n\n\t// Check for Drizzle\n\tif (existsSync(\"drizzle.config.ts\") || existsSync(\"drizzle.config.js\")) {\n\t\treturn \"drizzle\";\n\t}\n\n\t// Check for TypeORM\n\tif (existsSync(\"ormconfig.json\") || existsSync(\"ormconfig.ts\") || existsSync(\"ormconfig.js\")) {\n\t\treturn \"typeorm\";\n\t}\n\n\t// Check package.json dependencies\n\tif (existsSync(\"package.json\")) {\n\t\tconst pkg = JSON.parse(readFileSync(\"package.json\", \"utf-8\"));\n\t\tconst allDeps = { ...pkg.dependencies, ...pkg.devDependencies };\n\n\t\tif (allDeps[\"@prisma/client\"] || allDeps[\"prisma\"]) return \"prisma\";\n\t\tif (allDeps[\"drizzle-orm\"] || allDeps[\"drizzle-kit\"]) return \"drizzle\";\n\t\tif (allDeps[\"typeorm\"]) return \"typeorm\";\n\t}\n\n\treturn null;\n}\n\nfunction runCommand(cmd: string): void {\n\tconsole.log(`\\x1b[90m$ ${cmd}\\x1b[0m`);\n\ttry {\n\t\texecSync(cmd, { stdio: \"inherit\", cwd: process.cwd() });\n\t} catch {\n\t\t// Command already printed its error\n\t\tprocess.exitCode = 1;\n\t}\n}\n\nfunction prismaCommand(subcommand: string, args: string[]): void {\n\tconst cmd = `npx prisma ${subcommand} ${args.join(\" \")}`.trim();\n\trunCommand(cmd);\n}\n\nfunction drizzleCommand(subcommand: string, args: string[]): void {\n\tconst commands: Record<string, string> = {\n\t\tmigrate: \"drizzle-kit generate\",\n\t\tseed: \"drizzle-kit seed\",\n\t\treset: \"drizzle-kit drop\",\n\t\tstatus: \"drizzle-kit check\",\n\t\tgenerate: \"drizzle-kit generate\",\n\t\tstudio: \"drizzle-kit studio\",\n\t};\n\tconst cmd = `${commands[subcommand] ?? subcommand} ${args.join(\" \")}`.trim();\n\trunCommand(cmd);\n}\n\nfunction typeormCommand(subcommand: string, args: string[]): void {\n\tconst commands: Record<string, string> = {\n\t\tmigrate: \"typeorm migration:run\",\n\t\tseed: \"typeorm seed\",\n\t\treset: \"typeorm migration:revert\",\n\t\tgenerate: \"typeorm migration:generate\",\n\t\tstatus: \"typeorm migration:show\",\n\t\tstudio: \"echo 'TypeORM does not have a built-in studio command'\",\n\t};\n\tconst cmd = `${commands[subcommand] ?? subcommand} ${args.join(\" \")}`.trim();\n\trunCommand(cmd);\n}\n\nfunction printUsage(): void {\n\tconsole.log(`\n\\x1b[36mUsage:\\x1b[0m\n  buntok db <command> [args]\n\n\\x1b[36mCommands:\\x1b[0m\n  migrate [name]      Run pending migrations\n  seed                Seed database\n  reset               Reset database (drop & recreate)\n  generate            Generate migration from schema changes\n  studio              Open database GUI\n  status              Show migration status\n\n\\x1b[36mSupported ORMs:\\x1b[0m\n  Prisma, Drizzle, TypeORM (auto-detected)\n\n\\x1b[36mExamples:\\x1b[0m\n  buntok db migrate                    # Run pending migrations\n  buntok db migrate add user_table     # Create new migration\n  buntok db seed                       # Seed database\n  buntok db studio                     # Open Prisma Studio / etc\n`);\n}\n\nexport async function dbCommand(args: string[]): Promise<void> {\n\tconst subcommand = args[0] as DbCommand | undefined;\n\n\tif (!subcommand) {\n\t\tprintUsage();\n\t\treturn;\n\t}\n\n\tconst validCommands: DbCommand[] = [\"migrate\", \"seed\", \"reset\", \"generate\", \"studio\", \"status\"];\n\tif (!validCommands.includes(subcommand)) {\n\t\tconsole.error(`\\x1b[31mUnknown command: ${subcommand}\\x1b[0m`);\n\t\tprintUsage();\n\t\tprocess.exitCode = 1;\n\t\treturn;\n\t}\n\n\tconst orm = detectOrm();\n\tif (!orm) {\n\t\tconsole.error(\"\\x1b[31mCould not detect ORM.\\x1b[0m\");\n\t\tconsole.error(\"Supported: Prisma, Drizzle, TypeORM\");\n\t\tconsole.error(\"Make sure you have one installed in package.json.\");\n\t\tprocess.exitCode = 1;\n\t\treturn;\n\t}\n\n\tconsole.log(`\\x1b[36mDetected ORM: ${orm}\\x1b[0m`);\n\n\tconst subArgs = args.slice(1);\n\tswitch (orm) {\n\t\tcase \"prisma\":\n\t\t\tprismaCommand(subcommand, subArgs);\n\t\t\tbreak;\n\t\tcase \"drizzle\":\n\t\t\tdrizzleCommand(subcommand, subArgs);\n\t\t\tbreak;\n\t\tcase \"typeorm\":\n\t\t\ttypeormCommand(subcommand, subArgs);\n\t\t\tbreak;\n\t}\n}\n","import { existsSync, readFileSync, writeFileSync, mkdirSync } from \"node:fs\";\nimport { join, dirname } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { execSync } from \"node:child_process\";\nimport { createInterface } from \"node:readline\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = dirname(__filename);\n\nfunction askQuestion(question: string): Promise<boolean> {\n\treturn new Promise((resolve) => {\n\t\tconst rl = createInterface({\n\t\t\tinput: process.stdin,\n\t\t\toutput: process.stdout,\n\t\t});\n\n\t\trl.question(question, (answer) => {\n\t\t\trl.close();\n\t\t\tconst normalized = answer.trim().toLowerCase();\n\t\t\tresolve(normalized === \"y\" || normalized === \"yes\" || normalized === \"\");\n\t\t});\n\t});\n}\n\nconst REQUIRED_SCRIPTS: Record<string, string> = {\n\tdev: \"bun --watch src/index.ts\",\n\tbuild: \"bunx buntok build\",\n\tstart: \"bun .buntok/index.js\",\n\tcheck: \"bunx @biomejs/biome check --write .\",\n\tformat: \"bunx @biomejs/biome format --write .\",\n\tlint: \"bunx @biomejs/biome lint .\",\n};\n\nconst BIOME_CONFIG = {\n\tvcs: {\n\t\tenabled: true,\n\t\tclientKind: \"git\",\n\t\tuseIgnoreFile: true,\n\t},\n\tfiles: {\n\t\tignoreUnknown: true,\n\t\tincludes: [\"**\", \"!**/node_modules\", \"!**/dist\", \"!**/.buntok\", \"!**/coverage\"],\n\t},\n\tformatter: {\n\t\tenabled: true,\n\t\tindentStyle: \"tab\",\n\t\tindentWidth: 2,\n\t\tlineWidth: 100,\n\t\tlineEnding: \"lf\",\n\t},\n\tlinter: {\n\t\tenabled: true,\n\t\trules: {\n\t\t\tpreset: \"recommended\",\n\t\t\tsuspicious: {\n\t\t\t\tnoExplicitAny: \"off\",\n\t\t\t},\n\t\t},\n\t},\n\tjavascript: {\n\t\tformatter: {\n\t\t\tquoteStyle: \"double\",\n\t\t\ttrailingCommas: \"all\",\n\t\t},\n\t},\n};\n\n\nconst TSCONFIG_TEMPLATE = {\n\tcompilerOptions: {\n\t\t// Environment setup & latest features\n\t\tlib: [\"ESNext\"],\n\t\ttarget: \"ESNext\",\n\t\tmodule: \"Preserve\",\n\t\tmoduleDetection: \"force\",\n\t\tjsx: \"react-jsx\",\n\t\tallowJs: true,\n\t\ttypes: [\"bun\", \"node\"],\n\n\t\t// Bundler mode\n\t\tmoduleResolution: \"bundler\",\n\t\tallowImportingTsExtensions: true,\n\t\tverbatimModuleSyntax: true,\n\t\tnoEmit: true,\n\n\t\t// Strictness Options for TS 7+\n\t\tstrict: true,\n\t\tnoUncheckedIndexedAccess: true,\n\t\texactOptionalPropertyTypes: true,\n\t\tnoImplicitOverride: true,\n\t\tnoFallthroughCasesInSwitch: true,\n\t\tisolatedModules: true,\n\t\tskipLibCheck: true,\n\n\t\t// Path alias\n\t\tpaths: {\n\t\t\t\"@/*\": [\"./src/*\"],\n\t\t},\n\t},\n\tinclude: [\"src/**/*\"],\n\texclude: [\"node_modules\", \".buntok\"],\n};\n\nconst VSCODE_SETTINGS = {\n\t\"editor.formatOnSave\": true,\n\t\"editor.defaultFormatter\": \"biomejs.biome\",\n\t\"editor.codeActionsOnSave\": {\n\t\t\"source.fixAll.biome\": \"explicit\",\n\t\t\"source.organizeImports.biome\": \"explicit\",\n\t},\n};\n\nconst INDEX_TEMPLATE = `import { App } from \"@buntok/core\";\nimport { env } from \"./env\";\n\nexport const app = new App();\n\napp.get(\"/\", (ctx) => {\n  return ctx.json({ message: \"Hello from Buntok!\" });\n});\n\napp.listen(env.PORT);\n`;\n\nconst ENV_TS_TEMPLATE = `import { App, z } from \"@buntok/core\";\n\nexport const env = App.validateEnv({\n\tPORT: z.coerce.number().default(1212),\n\tAUTH_STORE: z.enum([\"header\", \"cookie\"]).default(\"header\"),\n\tAUTH_COOKIE: z.string().default(\"session\"),\n\tNODE_ENV: z.enum([\"development\", \"production\", \"test\"]).default(\"development\"),\n});\n`;\n\nfunction findSkillMdSource(): string | null {\n\t// 1. Relative path (works when running from @buntok/core package)\n\tconst relativePath = join(\n\t\t__dirname,\n\t\t\"..\",\n\t\t\"..\",\n\t\t\"scripts\",\n\t\t\"buntok-skill\",\n\t\t\"SKILL.md\",\n\t);\n\tif (existsSync(relativePath)) return relativePath;\n\n\t// 2. Resolve from @buntok/core package (works when running from buntok CLI)\n\ttry {\n\t\t// biome-ignore lint: dynamic import for package resolution\n\t\tconst pkgJson = require.resolve(\"@buntok/core/package.json\");\n\t\tconst pkgDir = dirname(pkgJson);\n\t\tconst skillPath = join(pkgDir, \"scripts\", \"buntok-skill\", \"SKILL.md\");\n\t\tif (existsSync(skillPath)) return skillPath;\n\t} catch {\n\t\t// package not found\n\t}\n\n\treturn null;\n}\n\nfunction copySkillMd(projectRoot: string): boolean {\n\tconst sourceSkill = findSkillMdSource();\n\n\tif (!sourceSkill) {\n\t\tconsole.warn(\n\t\t\t\"\\x1b[33m⚠ SKILL.md not found in @buntok/core, skipping.\\x1b[0m\",\n\t\t);\n\t\treturn false;\n\t}\n\n\tconst skillDir = join(projectRoot, \".agents\", \"skills\", \"buntok-skill\");\n\tconst destSkill = join(skillDir, \"SKILL.md\");\n\n\tif (!existsSync(skillDir)) {\n\t\tmkdirSync(skillDir, { recursive: true });\n\t}\n\n\twriteFileSync(destSkill, readFileSync(sourceSkill, \"utf-8\"), \"utf-8\");\n\tconsole.log(`\\x1b[32m✓ Created\\x1b[0m .agents/skills/buntok-skill/SKILL.md`);\n\treturn true;\n}\n\nfunction updatePackageJson(projectRoot: string): boolean {\n\tconst pkgPath = join(projectRoot, \"package.json\");\n\n\tlet pkg: { name?: string; scripts?: Record<string, string>; devDependencies?: Record<string, string> };\n\tif (existsSync(pkgPath)) {\n\t\tpkg = JSON.parse(readFileSync(pkgPath, \"utf-8\"));\n\t} else {\n\t\tconst folderName = projectRoot.split(\"/\").pop() || \"my-app\";\n\t\tpkg = { name: folderName, scripts: {}, devDependencies: {} };\n\t}\n\n\tif (!pkg.scripts) pkg.scripts = {};\n\n\tlet added = 0;\n\tfor (const [key, value] of Object.entries(REQUIRED_SCRIPTS)) {\n\t\tif (!pkg.scripts[key]) {\n\t\t\tpkg.scripts[key] = value;\n\t\t\tadded++;\n\t\t}\n\t}\n\n\tif (added === 0) {\n\t\tconsole.log(\"\\x1b[90m• package.json: all scripts already present\\x1b[0m\");\n\t\treturn false;\n\t}\n\n\twriteFileSync(pkgPath, JSON.stringify(pkg, null, 2) + \"\\n\", \"utf-8\");\n\tconsole.log(`\\x1b[32m✓ Updated\\x1b[0m package.json (added ${added} scripts)`);\n\treturn true;\n}\n\nfunction stripJsonComments(text: string): string {\n\t// Remove single-line comments (// ...) but not inside strings\n\treturn text.replace(/(?<![\"':].*)\\/\\/.*$/gm, \"\");\n}\n\nfunction setupTsconfig(projectRoot: string): boolean {\n\tconst tsconfigPath = join(projectRoot, \"tsconfig.json\");\n\n\tif (existsSync(tsconfigPath)) {\n\t\t// Merge paths into existing tsconfig\n\t\tconst raw = readFileSync(tsconfigPath, \"utf-8\");\n\t\tconst existing = JSON.parse(stripJsonComments(raw));\n\t\tif (!existing.compilerOptions) existing.compilerOptions = {};\n\t\tif (!existing.compilerOptions.paths) {\n\t\t\texisting.compilerOptions.paths = { \"@/*\": [\"./src/*\"] };\n\t\t\twriteFileSync(tsconfigPath, JSON.stringify(existing, null, 2) + \"\\n\", \"utf-8\");\n\t\t\tconsole.log(\"\\x1b[32m✓ Updated\\x1b[0m tsconfig.json (added path alias)\");\n\t\t\treturn true;\n\t\t}\n\t\tif (!existing.compilerOptions.paths[\"@/*\"]) {\n\t\t\texisting.compilerOptions.paths[\"@/*\"] = [\"./src/*\"];\n\t\t\twriteFileSync(tsconfigPath, JSON.stringify(existing, null, 2) + \"\\n\", \"utf-8\");\n\t\t\tconsole.log(\"\\x1b[32m✓ Updated\\x1b[0m tsconfig.json (added path alias)\");\n\t\t\treturn true;\n\t\t}\n\t\tconsole.log(\"\\x1b[90m• tsconfig.json: path alias already present\\x1b[0m\");\n\t\treturn false;\n\t}\n\n\t// Create new tsconfig\n\twriteFileSync(tsconfigPath, JSON.stringify(TSCONFIG_TEMPLATE, null, 2) + \"\\n\", \"utf-8\");\n\tconsole.log(\"\\x1b[32m✓ Created\\x1b[0m tsconfig.json\");\n\treturn true;\n}\n\nfunction setupBiome(projectRoot: string): boolean {\n\tconst biomePath = join(projectRoot, \"biome.json\");\n\n\tif (existsSync(biomePath)) {\n\t\tconsole.log(\"\\x1b[90m• biome.json: already exists, skipping\\x1b[0m\");\n\t\treturn false;\n\t}\n\n\t// Check if @biomejs/biome is installed\n\tconst pkgPath = join(projectRoot, \"package.json\");\n\tlet needsInstall = false;\n\tif (existsSync(pkgPath)) {\n\t\tconst pkg = JSON.parse(readFileSync(pkgPath, \"utf-8\"));\n\t\tconst allDeps = {\n\t\t\t...pkg.dependencies,\n\t\t\t...pkg.devDependencies,\n\t\t};\n\t\tneedsInstall = !allDeps[\"@biomejs/biome\"];\n\t}\n\n\tif (needsInstall) {\n\t\tconsole.log(\"\\x1b[90m• Installing @biomejs/biome...\\x1b[0m\");\n\t\ttry {\n\t\t\texecSync(\"bun add -d @biomejs/biome\", {\n\t\t\t\tcwd: projectRoot,\n\t\t\t\tstdio: \"ignore\",\n\t\t\t});\n\t\t\tconsole.log(\"\\x1b[32m✓ Installed\\x1b[0m @biomejs/biome\");\n\t\t} catch {\n\t\t\tconsole.warn(\n\t\t\t\t\"\\x1b[33m⚠ Failed to install @biomejs/biome, please install manually\\x1b[0m\",\n\t\t\t);\n\t\t}\n\t}\n\n\twriteFileSync(biomePath, JSON.stringify(BIOME_CONFIG, null, 2) + \"\\n\", \"utf-8\");\n\tconsole.log(\"\\x1b[32m✓ Created\\x1b[0m biome.json\");\n\treturn true;\n}\n\nfunction createIndexFile(projectRoot: string): boolean {\n\tconst srcDir = join(projectRoot, \"src\");\n\tconst indexPath = join(srcDir, \"index.ts\");\n\n\tif (existsSync(indexPath)) {\n\t\tconsole.log(\"\\x1b[90m• src/index.ts: already exists, skipping\\x1b[0m\");\n\t\treturn false;\n\t}\n\n\tif (!existsSync(srcDir)) {\n\t\tmkdirSync(srcDir, { recursive: true });\n\t}\n\n\twriteFileSync(indexPath, INDEX_TEMPLATE, \"utf-8\");\n\tconsole.log(\"\\x1b[32m✓ Created\\x1b[0m src/index.ts\");\n\treturn true;\n}\n\nfunction createEnvTsFile(projectRoot: string): boolean {\n\tconst srcDir = join(projectRoot, \"src\");\n\tconst envTsPath = join(srcDir, \"env.ts\");\n\n\tif (existsSync(envTsPath)) {\n\t\tconsole.log(\"\\x1b[90m• src/env.ts: already exists, skipping\\x1b[0m\");\n\t\treturn false;\n\t}\n\n\tif (!existsSync(srcDir)) {\n\t\tmkdirSync(srcDir, { recursive: true });\n\t}\n\n\twriteFileSync(envTsPath, ENV_TS_TEMPLATE, \"utf-8\");\n\tconsole.log(\"\\x1b[32m✓ Created\\x1b[0m src/env.ts\");\n\treturn true;\n}\n\nfunction setupVscode(projectRoot: string): boolean {\n\tconst vscodeDir = join(projectRoot, \".vscode\");\n\tconst settingsPath = join(vscodeDir, \"settings.json\");\n\n\tif (existsSync(settingsPath)) {\n\t\tconsole.log(\"\\x1b[90m• .vscode/settings.json: already exists, skipping\\x1b[0m\");\n\t\treturn false;\n\t}\n\n\tif (!existsSync(vscodeDir)) {\n\t\tmkdirSync(vscodeDir, { recursive: true });\n\t}\n\n\twriteFileSync(settingsPath, JSON.stringify(VSCODE_SETTINGS, null, 2) + \"\\n\", \"utf-8\");\n\tconsole.log(\"\\x1b[32m✓ Created\\x1b[0m .vscode/settings.json\");\n\treturn true;\n}\n\nconst ENV_CONTENT = `# Buntok Configuration\nPORT=1212\nAUTH_STORE=header\nAUTH_COOKIE=session\n`;\n\nconst ENV_EXAMPLE_CONTENT = `# Buntok Configuration\n#\n# PORT: port is using for the app\n# AUTH_STORE: Where to store/read JWT tokens\n#   - \"header\" (default): Read from Authorization: Bearer <token> header\n#   - \"cookie\": Read from HttpOnly cookie (set AUTH_COOKIE for cookie name)\n#\n# AUTH_COOKIE: Cookie name for JWT storage (only used when AUTH_STORE=cookie)\n#\nPORT=1212\nAUTH_STORE=header\nAUTH_COOKIE=session\n`;\n\nconst VERCEL_JSON_TEMPLATE = {\n\tbuilds: [\n\t\t{\n\t\t\tsrc: \"src/index.ts\",\n\t\t\tuse: \"@vercel/rest\",\n\t\t},\n\t],\n\troutes: [\n\t\t{\n\t\t\tsrc: \"/(.*)\",\n\t\t\tdest: \"src/index.ts\",\n\t\t},\n\t],\n};\n\nconst GITIGNORE_CONTENT = `# Dependencies\nnode_modules/\n\n# Build output\n.buntok/\n\n# Environment\n.env\n.env.local\n.env.*.local\n\n# IDE\n.vscode/\n.idea/\n*.swp\n*.swo\n*~\n\n# OS\n.DS_Store\nThumbs.db\n\n# Buntok\n.buntok/\n\n# Logs\n*.log\nnpm-debug.log*\n\n# Coverage\ncoverage/\n`;\n\nconst DOCKERFILE_TEMPLATE = `# Builder\nFROM oven/bun:1-alpine AS builder\nWORKDIR /app\n\nCOPY package.json bun.lock* ./\nRUN bun install --frozen-lockfile --production\n\nCOPY src/ src/\nCOPY tsconfig.json ./\nCOPY package.json ./\n\nRUN bunx buntok build\n\n# Production\nFROM oven/bun:1-distroless\nWORKDIR /app\n\nCOPY --from=builder /app/.buntok .buntok\nCOPY --from=builder /app/node_modules node_modules\nCOPY --from=builder /app/package.json ./\n\nUSER bun\nEXPOSE 1212\n\nENV NODE_ENV=production\nENV PORT=1212\n\nCMD [\"bun\", \".buntok/index.js\"]\n`;\n\nconst DOCKERIGNORE_CONTENT = `node_modules\ndist\n.buntok\n*.log\n.env\n.env.*\ncoverage\n`;\n\nfunction createEnvFiles(projectRoot: string): boolean {\n\tconst envPath = join(projectRoot, \".env\");\n\tconst envExamplePath = join(projectRoot, \".env.example\");\n\tlet created = false;\n\n\tif (!existsSync(envPath)) {\n\t\twriteFileSync(envPath, ENV_CONTENT, \"utf-8\");\n\t\tconsole.log(\"\\x1b[32m✓ Created\\x1b[0m .env\");\n\t\tcreated = true;\n\t} else {\n\t\tconsole.log(\"\\x1b[90m• .env: already exists, skipping\\x1b[0m\");\n\t}\n\n\tif (!existsSync(envExamplePath)) {\n\t\twriteFileSync(envExamplePath, ENV_EXAMPLE_CONTENT, \"utf-8\");\n\t\tconsole.log(\"\\x1b[32m✓ Created\\x1b[0m .env.example\");\n\t\tcreated = true;\n\t} else {\n\t\tconsole.log(\"\\x1b[90m• .env.example: already exists, skipping\\x1b[0m\");\n\t}\n\n\treturn created;\n}\n\nfunction createGitignore(projectRoot: string): boolean {\n\tconst gitignorePath = join(projectRoot, \".gitignore\");\n\n\tif (existsSync(gitignorePath)) {\n\t\tconsole.log(\"\\x1b[90m• .gitignore: already exists, skipping\\x1b[0m\");\n\t\treturn false;\n\t}\n\n\twriteFileSync(gitignorePath, GITIGNORE_CONTENT, \"utf-8\");\n\tconsole.log(\"\\x1b[32m✓ Created\\x1b[0m .gitignore\");\n\treturn true;\n}\n\nfunction createVercelJson(projectRoot: string): boolean {\n\tconst vercelPath = join(projectRoot, \"vercel.json\");\n\n\tif (existsSync(vercelPath)) {\n\t\tconsole.log(\"\\x1b[90m• vercel.json: already exists, skipping\\x1b[0m\");\n\t\treturn false;\n\t}\n\n\twriteFileSync(vercelPath, JSON.stringify(VERCEL_JSON_TEMPLATE, null, 2) + \"\\n\", \"utf-8\");\n\tconsole.log(\"\\x1b[32m✓ Created\\x1b[0m vercel.json\");\n\treturn true;\n}\n\nfunction createDockerfile(projectRoot: string): boolean {\n\tconst dockerfilePath = join(projectRoot, \"Dockerfile\");\n\tconst dockerignorePath = join(projectRoot, \".dockerignore\");\n\n\tlet created = false;\n\n\tif (!existsSync(dockerfilePath)) {\n\t\twriteFileSync(dockerfilePath, DOCKERFILE_TEMPLATE, \"utf-8\");\n\t\tconsole.log(\"\\x1b[32m✓ Created\\x1b[0m Dockerfile\");\n\t\tcreated = true;\n\t} else {\n\t\tconsole.log(\"\\x1b[90m• Dockerfile: already exists, skipping\\x1b[0m\");\n\t}\n\n\tif (!existsSync(dockerignorePath)) {\n\t\twriteFileSync(dockerignorePath, DOCKERIGNORE_CONTENT, \"utf-8\");\n\t\tconsole.log(\"\\x1b[32m✓ Created\\x1b[0m .dockerignore\");\n\t\tcreated = true;\n\t} else {\n\t\tconsole.log(\"\\x1b[90m• .dockerignore: already exists, skipping\\x1b[0m\");\n\t}\n\n\treturn created;\n}\n\nexport async function initCommand() {\n\tconst projectRoot = process.cwd();\n\n\tcopySkillMd(projectRoot);\n\tupdatePackageJson(projectRoot);\n\tsetupTsconfig(projectRoot);\n\tsetupBiome(projectRoot);\n\tsetupVscode(projectRoot);\n\tcreateEnvTsFile(projectRoot);\n\tcreateIndexFile(projectRoot);\n\tcreateEnvFiles(projectRoot);\n\tcreateGitignore(projectRoot);\n\n\tconst vercelPath = join(projectRoot, \"vercel.json\");\n\tif (!existsSync(vercelPath)) {\n\t\tconsole.log(\"\");\n\t\tconst useVercel = await askQuestion(\n\t\t\t\"\\x1b[36mDo you want to deploy to Vercel? (y/N): \\x1b[0m\",\n\t\t);\n\t\tif (useVercel) {\n\t\t\tcreateVercelJson(projectRoot);\n\t\t}\n\t} else {\n\t\tconsole.log(\"\\x1b[90m• vercel.json: already exists, skipping\\x1b[0m\");\n\t}\n\n\tconst dockerfilePath = join(projectRoot, \"Dockerfile\");\n\tif (!existsSync(dockerfilePath)) {\n\t\tconsole.log(\"\");\n\t\tconst useDocker = await askQuestion(\n\t\t\t\"\\x1b[36mDo you want to add Docker support? (Y/n): \\x1b[0m\",\n\t\t);\n\t\tif (useDocker) {\n\t\t\tcreateDockerfile(projectRoot);\n\t\t}\n\t} else {\n\t\tconsole.log(\"\\x1b[90m• Dockerfile: already exists, skipping\\x1b[0m\");\n\t}\n}\n","import { mkdirSync, writeFileSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\nimport { pathToFileURL } from \"node:url\";\nimport {\n\textendZodWithOpenApi,\n\tOpenAPIRegistry,\n\tOpenApiGeneratorV3,\n} from \"@asteasolutions/zod-to-openapi\";\nimport { z } from \"zod\";\n\nextendZodWithOpenApi(z);\n\n/**\n * Recursively walk a Zod schema and replace any type that zod-to-openapi\n * cannot handle (e.g. ZodFile from Zod v4) with a safe fallback so that\n * docs generation never crashes on an unsupported schema node.\n */\n// biome-ignore lint/suspicious/noExplicitAny: schema introspection requires any\nfunction sanitizeSchema(schema: any): any {\n\tif (!schema || typeof schema !== \"object\" || !schema._def) return schema;\n\n\tconst typeName: string = schema._def?.typeName ?? schema._def?.type ?? \"\";\n\n\t// ZodFile (Zod v4) → OpenAPI string format:binary\n\tif (typeName === \"ZodFile\" || typeName === \"file\") {\n\t\treturn z.string().openapi({ format: \"binary\", description: \"Binary file\" });\n\t}\n\n\t// For all other types, return as-is - zod-to-openapi handles them\n\t// Only recurse into containers we know how to handle safely\n\tif (typeName === \"ZodObject\" || typeName === \"object\") {\n\t\tconst shape =\n\t\t\ttypeof schema._def.shape === \"function\"\n\t\t\t\t? schema._def.shape()\n\t\t\t\t: (schema._def.shape ?? {});\n\t\tconst newShape: Record<string, unknown> = {};\n\t\tfor (const [key, value] of Object.entries(shape)) {\n\t\t\tnewShape[key] = sanitizeSchema(value);\n\t\t}\n\t\treturn z.object(newShape as Record<string, z.ZodTypeAny>);\n\t}\n\n\tif (typeName === \"ZodArray\" || typeName === \"array\") {\n\t\tconst element = schema._def?.element ?? schema._def?.type;\n\t\treturn z.array(sanitizeSchema(element));\n\t}\n\n\tif (typeName === \"ZodOptional\" || typeName === \"optional\") {\n\t\treturn sanitizeSchema(schema._def.innerType).optional();\n\t}\n\tif (typeName === \"ZodNullable\" || typeName === \"nullable\") {\n\t\treturn sanitizeSchema(schema._def.innerType).nullable();\n\t}\n\n\t// Default: return original schema (don't reconstruct unknown types)\n\treturn schema;\n}\n\nexport async function makeDocsCommand() {\n\tconsole.log(\"\\x1b[36mGenerating OpenAPI documentation...\\x1b[0m\");\n\n\t// Prevent the user's app from starting its HTTP server during import\n\tprocess.env.BUNTOK_DOCS_BUILD = \"1\";\n\n\tconst entryPath = resolve(process.cwd(), \"src/index.ts\");\n\n\ttry {\n\t\tconsole.log(`\\x1b[90mLoading app from ${entryPath}...\\x1b[0m`);\n\n\t\tconst userApp = await import(pathToFileURL(entryPath).href);\n\t\tconst appInstance = userApp.app || userApp.default;\n\n\t\tif (!appInstance?.openApiDocs) {\n\t\t\tthrow new Error(\n\t\t\t\t\"Could not find an exported 'app' instance in src/index.ts. Make sure you export your app: `export const app = new App();`\",\n\t\t\t);\n\t\t}\n\n\t\tconst docsConfig = appInstance._apiDocsConfig;\n\t\tconst registry = new OpenAPIRegistry();\n\n\t\tlet skipped = 0;\n\n\t\tfor (const doc of appInstance.openApiDocs) {\n\t\t\t// Convert express-style params /users/:id → OpenAPI /users/{id}\n\t\t\tconst openapiPath = doc.path.replace(/:([a-zA-Z0-9_]+)/g, \"{$1}\");\n\n\t\t\t// biome-ignore lint/suspicious/noExplicitAny: RouteConfig populated dynamically\n\t\t\tconst routeConfig: any = {\n\t\t\t\tmethod: doc.method,\n\t\t\t\tpath: openapiPath,\n\t\t\t\tresponses: {},\n\t\t\t};\n\n\t\t\tif (doc.request.params || doc.request.query || doc.request.body) {\n\t\t\t\trouteConfig.request = {};\n\t\t\t\tif (doc.request.params)\n\t\t\t\t\trouteConfig.request.params = sanitizeSchema(doc.request.params);\n\t\t\t\tif (doc.request.query)\n\t\t\t\t\trouteConfig.request.query = sanitizeSchema(doc.request.query);\n\t\t\t\tif (doc.request.body) {\n\t\t\t\t\tconst contentType = doc.request.bodyContentType || \"application/json\";\n\t\t\t\t\trouteConfig.request.body = {\n\t\t\t\t\t\tcontent: {\n\t\t\t\t\t\t\t[contentType]: { schema: sanitizeSchema(doc.request.body) },\n\t\t\t\t\t\t},\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (doc.responses.length > 0) {\n\t\t\t\tfor (const res of doc.responses) {\n\t\t\t\t\trouteConfig.responses[res.status.toString()] = {\n\t\t\t\t\t\tdescription: res.description,\n\t\t\t\t\t\tcontent: {\n\t\t\t\t\t\t\t\"application/json\": { schema: sanitizeSchema(res.schema) },\n\t\t\t\t\t\t},\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\trouteConfig.responses[\"200\"] = { description: \"Success\" };\n\t\t\t}\n\n\t\t\t// Per-route error handling: one bad schema should not abort everything\n\t\t\ttry {\n\t\t\t\tregistry.registerPath(routeConfig);\n\t\t\t} catch (err) {\n\t\t\t\tskipped++;\n\t\t\t\tconst label = `${doc.method.toUpperCase()} ${doc.path}`;\n\t\t\t\tif (err instanceof Error) {\n\t\t\t\t\tconsole.warn(`\\x1b[33m  ⚠ Skipping ${label}: ${err.message}\\x1b[0m`);\n\t\t\t\t} else {\n\t\t\t\t\tconsole.warn(`\\x1b[33m  ⚠ Skipping ${label}: unknown error\\x1b[0m`);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tconst generator = new OpenApiGeneratorV3(registry.definitions);\n\t\tconst document = generator.generateDocument({\n\t\t\topenapi: \"3.0.0\",\n\t\t\tinfo: {\n\t\t\t\tversion: docsConfig?.version ?? \"1.0.0\",\n\t\t\t\ttitle: docsConfig?.title ?? \"Buntok API Documentation\",\n\t\t\t\tdescription: docsConfig?.description ?? \"Auto-generated OpenAPI docs from Zod schemas\",\n\t\t\t},\n\t\t});\n\n\t\t// Output directory: public/docs/\n\t\tconst docsDir = resolve(process.cwd(), \"public/docs\");\n\t\tmkdirSync(docsDir, { recursive: true });\n\n\t\tconst swaggerPath = resolve(docsDir, \"swagger.json\");\n\t\twriteFileSync(swaggerPath, JSON.stringify(document, null, 2));\n\t\tconsole.log(\n\t\t\t`\\x1b[32m✔ swagger.json generated at public/docs/swagger.json\\x1b[0m`,\n\t\t);\n\n\t\tconsole.log(`\\n\\x1b[36mNext steps:\\x1b[0m`);\n\t\tconsole.log(\n\t\t\t`\\x1b[90m  Add app.apiDocs() to your src/index.ts:\\x1b[0m`,\n\t\t);\n\t\tconsole.log(\n\t\t\t`\\x1b[90m    app.apiDocs({ path: \"/docs\", title: \"My API\" });\\x1b[0m`,\n\t\t);\n\t\tconsole.log();\n\t\tprocess.exit(0);\n\t} catch (error) {\n\t\tconsole.error(\"\\x1b[31mFailed to generate docs:\\x1b[0m\");\n\t\tconsole.error(error);\n\t\tprocess.exit(1);\n\t}\n}\n","// Core\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\nlet _version = \"0.0.0-dev\";\ntry {\n\t// 1. Check framework's own package.json (dev/monorepo context)\n\tlet pkgPath = join(__dirname, \"..\", \"package.json\");\n\tif (!existsSync(pkgPath)) {\n\t\t// 2. CLI bundle context (dist/cli/)\n\t\tpkgPath = join(__dirname, \"..\", \"..\", \"package.json\");\n\t}\n\tif (!existsSync(pkgPath)) {\n\t\t// 3. User project — read from node_modules/@buntok/core/package.json\n\t\tpkgPath = join(process.cwd(), \"node_modules\", \"@buntok\", \"core\", \"package.json\");\n\t}\n\tif (existsSync(pkgPath)) {\n\t\tconst pkg = JSON.parse(readFileSync(pkgPath, \"utf-8\"));\n\t\tif (typeof pkg.version === \"string\") _version = pkg.version;\n\t}\n} catch {}\nexport const VERSION: string = _version;\n\nexport { z } from \"zod\";\n// AI\nexport {\n\tAICache,\n\tinjectSystemPrompt,\n\tstreamAI,\n} from \"./ai\";\nexport type {\n\tEnvValidationOptions,\n\tErrorHandler,\n\tExtractParams,\n\tHandler,\n\tHandlerReturn,\n\tMiddleware,\n\tNotFoundHandler,\n\tRouteContext,\n\tWSData,\n\tWSHandler,\n\tZodCtx,\n} from \"./app\";\nexport { App, type ApiDocsOptions, type StaticOptions, type WSOptions } from \"./app\";\n// Auth\nexport { JwtService, requireAuth } from \"./auth\";\n// OAuth\nexport {\n\tBaseOAuthProvider,\n\tclearOAuthCookies,\n\tcreateOAuth,\n\tcreateOAuth2AuthorizationURL,\n\tdecodeIdToken,\n\tgenerateCodeChallenge,\n\tgenerateCodeVerifier,\n\tgeneratePKCE,\n\tgetCodeVerifier,\n\tOAuthError,\n\tOAuthProviderError,\n\tOAuthStateError,\n\tOAuthTokenError,\n\tstoreOAuthState,\n\tvalidateOAuth2AuthorizationCode,\n\tverifyOAuthState,\n} from \"./oauth\";\nexport type {\n\tAppleProviderConfig,\n\tCreateAuthorizationURLOptions,\n\tOAuth2Tokens,\n\tOAuthProvider,\n\tOAuthProviderConfig,\n\tOAuthUser,\n\tValidateAuthorizationCodeOptions,\n} from \"./oauth\";\nexport { AppleProvider, type AppleUser } from \"./oauth\";\nexport { GitHubProvider, type GitHubUser } from \"./oauth\";\nexport { GoogleProvider, type GoogleUser } from \"./oauth\";\n// Base classes\nexport { BaseController } from \"./base-controller\";\nexport { BaseService } from \"./base-service\";\n// Cache\nexport { Cache, type CacheDriver, MemoryCacheDriver } from \"./cache\";\nexport {\n\ttype ClassProvider,\n\tContainer,\n\ttype FactoryProvider,\n\ttype Provider,\n\ttype Scope,\n\ttype ValueProvider,\n} from \"./container\";\nexport { Context } from \"./context\";\nexport type { ControllerMeta, RouteMeta } from \"./decorators\";\n// Emitter\nexport { emitter, EventEmitter } from \"./emitter\";\nexport type { AppEvents, EmitOptions, EventEmitterOptions } from \"./emitter\";\n// Decorators\nexport {\n\tAll,\n\tController,\n\tDelete,\n\tGet,\n\ttype GuardFn,\n\tHead,\n\tOptions,\n\tPatch,\n\tPost,\n\tPut,\n\tQuery,\n\tUse,\n\tUseGuard,\n\t// Nest-style alias (deprecated old kept)\n\tUseGuards,\n\t// New zero-cost decorators\n\tSetMetadata,\n\tPublic,\n\tHttpCode,\n\tSetHeader,\n\tHeader,\n\tRedirect,\n\tVersion,\n\tapplyDecorators,\n\tgetMetadata,\n} from \"./decorators\";\n// Native FFI\nexport { getBackend, isNativeAvailable } from \"./ffi\";\nexport type { RetryOptions } from \"./helpers/async\";\n// Async helpers\nexport { delay, retry } from \"./helpers/async\";\n// Response helpers (Elysia-style flexible return)\nexport { toResponse, toResponseMaybeAsync } from \"./helpers/response\";\n// Error helpers\nexport {\n\tasyncHandler,\n\tBadRequestError,\n\tConflictError,\n\tForbiddenError,\n\tHttpError,\n\tInternalServerError,\n\tMethodNotAllowedError,\n\tNotFoundError,\n\tServiceUnavailableError,\n\tTooManyRequestsError,\n\tUnauthorizedError,\n\tUnprocessableEntityError,\n} from \"./helpers/async-handler\";\nexport type { CookieOptions } from \"./helpers/cookie\";\n// Cookie helpers\nexport {\n\tdeleteCookie,\n\tgetCookie,\n\tgetCookies,\n\tparseCookies,\n\tserializeCookie,\n\tsetCookie,\n} from \"./helpers/cookie\";\n// Crypto helpers\nexport {\n\tdecrypt,\n\tencrypt,\n\thash,\n\thashVerify,\n\thmac,\n\tmd5,\n\trandomAlphaNumeric,\n\trandomBytes,\n\trandomHex,\n\trandomToken,\n\tsha256,\n\tsha512,\n} from \"./helpers/crypto\";\n// Date helpers\nexport {\n\taddDays,\n\tdaysBetween,\n\tendOfDay,\n\tformatDate,\n\tformatDuration,\n\tisAfter,\n\tisBefore,\n\tstartOfDay,\n\ttimeAgo,\n} from \"./helpers/date\";\n// Timezone helpers\nexport {\n\tformatGroupLabel,\n\tformatInTimezone,\n\tgetGroupLabels,\n\tgetTimezoneOffset,\n\tgetTimezoneOffsetString,\n\tgroupByTimezone,\n\tisValidTimezone,\n\tnowInTimezone,\n\tparseTime,\n\ttoISOWithTimezone,\n\ttoTimezoneParts,\n} from \"./helpers/timezone\";\nexport type { GroupByKey, GroupByTimezoneOptions } from \"./helpers/timezone\";\n// ID helpers\nexport {\n\tgenerateCode,\n\tnanoid,\n\tresetCounter,\n\tulid,\n} from \"./helpers/id\";\n// Network helpers\nexport { getClientIP, isPrivateIP, parseUserAgent } from \"./helpers/network\";\n// Number helpers\nexport {\n\tclamp,\n\tformatBytes,\n\tformatCurrency,\n\tformatNumber,\n\trandom,\n\trandomFloat,\n} from \"./helpers/number\";\n// Object helpers\nexport {\n\tchunk,\n\tdeepMerge,\n\tflatten,\n\tflattenObject,\n\tgroupBy,\n\tomit,\n\tpick,\n\tuniq,\n} from \"./helpers/object\";\n// Password helpers\nexport { hashPassword, verifyPassword } from \"./helpers/password\";\n// String helpers\nexport {\n\tcamelCase,\n\tcapitalize,\n\tkebabCase,\n\tslugify,\n\tsnakeCase,\n\ttruncate,\n} from \"./helpers/string\";\n// Logger\nexport { Logger, LogLevel, logger } from \"./logger\";\n// Mailer\nexport { Mailer, type MailerConfig, type MailOptions, type MailAttachment } from \"./mailer\";\n// Template Engine\nexport {\n\tTemplateEngine,\n\trender,\n\tregisterHelper,\n\tregisterPartial,\n\ttype TemplateOptions,\n\ttype HelperFn,\n} from \"./template\";\n// Middlewares\nexport { auditLog, type AuditLogEntry, type AuditLogOptions } from \"./middlewares/audit-log\";\nexport type { BodySizeLimitOptions } from \"./middlewares/body-size-limit\";\nexport { bodySizeLimit } from \"./middlewares/body-size-limit\";\nexport type { CompressOptions } from \"./middlewares/compress\";\nexport { compress } from \"./middlewares/compress\";\nexport type { CorsOptions } from \"./middlewares/cors\";\nexport { cors } from \"./middlewares/cors\";\nexport type {\n\tHealthCheckOptions,\n\tHealthStatus,\n} from \"./middlewares/health-check\";\nexport {\n\tcreateDatabaseCheck,\n\tcreateHealthCheck,\n\thealthCheck,\n} from \"./middlewares/health-check\";\nexport type { HelmetOptions } from \"./middlewares/helmet\";\n// Helmet\nexport { helmet } from \"./middlewares/helmet\";\nexport type { RateLimiterOptions } from \"./middlewares/rate-limiter\";\nexport {\n\trateLimiter,\n\tslidingWindowRateLimiter,\n} from \"./middlewares/rate-limiter\";\nexport type { RequestIdOptions } from \"./middlewares/request-id\";\nexport { requestId, shortId, uuid } from \"./middlewares/request-id\";\nexport type { ResponseTimeOptions } from \"./middlewares/response-time\";\nexport { responseTime } from \"./middlewares/response-time\";\n// Timeout\nexport { TimeoutError, timeout } from \"./middlewares/timeout\";\n// RBAC\nexport { requirePermission, requireRole } from \"./middlewares/rbac\";\nexport type { RequirePermissionOptions, RequireRoleOptions } from \"./middlewares/rbac\";\nexport type {\n\tBodyContentType,\n\tValidationTarget,\n\tZValidatorOptions,\n} from \"./middlewares/validator\";\nexport {\n\tvalidate,\n\tvalidateBody,\n\tvalidateParams,\n\tzResponse,\n\tzValidator,\n} from \"./middlewares/validator\";\n// Queue\nexport {\n\ttype Job,\n\ttype JobHandler,\n\tMemoryQueueDriver,\n\tQueue,\n\ttype QueueDriver,\n\ttype QueueOptions,\n} from \"./queue\";\nexport { Router } from \"./router\";\n// Payment\nexport {\n\tPaymentConfigurationError,\n\tPaymentError,\n\tPaymentIdempotencyError,\n\tPaymentProviderError,\n\tPaymentVerificationError,\n\tpaymentWebhook,\n\tcreatePayment,\n\tgenerateIdempotencyKey,\n\tnormalizeCheckoutStatus,\n\tnormalizeRefundStatus,\n\tnormalizeSubscriptionStatus,\n\tCreateCheckoutInputSchema,\n\tCreatePaymentLinkInputSchema,\n\tCreateRefundInputSchema,\n\tCreateSubscriptionInputSchema,\n\tMoneyAmountSchema,\n\tStripeDriver,\n\tMidtransDriver,\n\tXenditDriver,\n\tPayPalDriver,\n} from \"./payment\";\nexport type {\n\tCheckoutResult,\n\tCheckoutStatus,\n\tCreateCheckoutInput,\n\tCreatePaymentLinkInput,\n\tCreateRefundInput,\n\tCreateSubscriptionInput,\n\tPaymentDriver,\n\tPaymentLinkResult,\n\tPaymentOptions,\n\tRefundResult,\n\tRefundStatus,\n\tStripeDriverConfig,\n\tMidtransDriverConfig,\n\tXenditDriverConfig,\n\tPayPalDriverConfig,\n\tSubscriptionResult,\n\tSubscriptionStatus,\n\tWebhookEvent,\n\tWebhookEventType,\n\tWebhookMiddlewareOptions,\n} from \"./payment\";\n// Scheduler / CronJob\nexport {\n\tBunCronSchedulerDriver,\n\tCronJob,\n\tMemorySchedulerDriver,\n\tScheduler,\n\ttype SchedulerDriver,\n\tsetDefaultSchedulerDriver,\n} from \"./schedule\";\n// SSE - industrial, pluggable (mirip StorageDriver)\nexport type { SSEBroadcasterOptions, SSEHistoryStore, SSEMessage, SSEOptions, SSEPubSub } from \"./sse\";\nexport { MemorySSEHistory, MemorySSEPubSub, SSE, SSEBroadcaster, createSSE } from \"./sse\";\n// Upload\nexport type {\n\tImageUploadedFile,\n\tParseUploadResult,\n\tStorageDriver,\n\tUploadedFile,\n\tUploadFieldConfig,\n\tUploadOptions,\n} from \"./upload\";\nexport {\n\tdeleteUploadedFile,\n\tLocalDiskStorage,\n\tMemoryStorage,\n\thandleUploads,\n\tuploader,\n} from \"./upload\";\n// WebSocket helpers - industrial, pluggable\nexport type { RoomOptions, WSPubSub, WSRateLimitOptions, WSRateLimitStore } from \"./ws-helpers\";\nexport {\n\tMemoryWSPubSub,\n\tMemoryWSRateLimitStore,\n\tRoom,\n\tvalidateWSMessage,\n\twsAuth,\n\twsHeartbeat,\n\twsHeartbeatPong,\n\twsRateLimit,\n} from \"./ws-helpers\";\n","import type { CacheDriver } from \"./cache\";\nimport type { Context } from \"./context\";\n\n/**\n * Transforms an async iterable (like OpenAI chat completions stream) into a\n * Vercel AI SDK compatible stream response (Data Stream Protocol v1).\n *\n * @param ctx - The Buntok context\n * @param stream - An AsyncIterable representing the LLM chunks\n * @param options - Optional callbacks (e.g. onCompletion)\n * @returns A Response object with Server-Sent Events headers\n */\nexport function streamAI(\n\t_ctx: Context,\n\t// biome-ignore lint/suspicious/noExplicitAny: generic stream\n\tstream: AsyncIterable<any> | any,\n\toptions?: {\n\t\tonCompletion?: (fullText: string) => void | Promise<void>;\n\t},\n): Response {\n\tconst readable = new ReadableStream({\n\t\tasync start(controller) {\n\t\t\ttry {\n\t\t\t\tlet fullText = \"\";\n\n\t\t\t\t// Handle standard AsyncIterable (OpenAI, Anthropic standard)\n\t\t\t\tif (stream && Symbol.asyncIterator in stream) {\n\t\t\t\t\tfor await (const chunk of stream) {\n\t\t\t\t\t\t// Very naive check for OpenAI/Anthropic chunk formats\n\t\t\t\t\t\tconst content =\n\t\t\t\t\t\t\tchunk?.choices?.[0]?.delta?.content ||\n\t\t\t\t\t\t\tchunk?.message?.content ||\n\t\t\t\t\t\t\t(typeof chunk === \"string\" ? chunk : \"\");\n\n\t\t\t\t\t\tif (content) {\n\t\t\t\t\t\t\tfullText += content;\n\t\t\t\t\t\t\t// Format according to Vercel AI SDK Data Stream Protocol: 0:\"text\"\n\t\t\t\t\t\t\tconst data = `0:${JSON.stringify(content)}\\n`;\n\t\t\t\t\t\t\tcontroller.enqueue(new TextEncoder().encode(data));\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tthrow new Error(\"Provided stream is not an AsyncIterable\");\n\t\t\t\t}\n\n\t\t\t\t// Fire completion callback\n\t\t\t\tif (options?.onCompletion) {\n\t\t\t\t\tawait options.onCompletion(fullText);\n\t\t\t\t}\n\n\t\t\t\t// End of stream protocol: d:{\"finishReason\":\"stop\"}\n\t\t\t\tcontroller.enqueue(\n\t\t\t\t\tnew TextEncoder().encode(`d:{\"finishReason\":\"stop\"}\\n`),\n\t\t\t\t);\n\t\t\t\t// biome-ignore lint/suspicious/noExplicitAny: catching unknown error type\n\t\t\t} catch (e: any) {\n\t\t\t\t// Send error stream protocol: e:{\"message\":\"...\"}\n\t\t\t\tcontroller.enqueue(\n\t\t\t\t\tnew TextEncoder().encode(\n\t\t\t\t\t\t`e:${JSON.stringify({ message: e.message })}\\n`,\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t} finally {\n\t\t\t\tcontroller.close();\n\t\t\t}\n\t\t},\n\t});\n\n\treturn new Response(readable, {\n\t\theaders: {\n\t\t\t\"Content-Type\": \"text/x-unknown\", // Standard for Vercel AI stream\n\t\t\t\"Cache-Control\": \"no-cache, no-transform\",\n\t\t\tConnection: \"keep-alive\",\n\t\t\t\"x-vercel-ai-data-stream\": \"v1\",\n\t\t},\n\t});\n}\n\n/**\n * Safely injects a system prompt into an array of messages.\n * Removes any existing system prompts from user input to prevent prompt injection.\n *\n * @param messages - Array of existing chat messages\n * @param systemPrompt - The strict system instruction\n * @returns Cleaned array of messages with system prompt at the top\n */\n// biome-ignore lint/suspicious/noExplicitAny: standard AI message array\nexport function injectSystemPrompt(messages: any[], systemPrompt: string) {\n\tconst safeMessages = messages.filter((m) => m.role !== \"system\");\n\treturn [{ role: \"system\", content: systemPrompt }, ...safeMessages];\n}\n\n/**\n * Semantic Cache for AI responses using Buntok CacheDrivers.\n * It caches exact matches of chat histories to save LLM costs.\n */\nexport class AICache {\n\tconstructor(private driver: CacheDriver) {}\n\n\t/**\n\t * Generates a unique key based on the last few user messages.\n\t */\n\t// biome-ignore lint/suspicious/noExplicitAny: standard AI message array\n\tprivate hashMessages(messages: any[]): string {\n\t\tconst relevantMessages = messages\n\t\t\t.filter((m) => m.role === \"user\" || m.role === \"assistant\")\n\t\t\t.slice(-3);\n\t\tconst content = JSON.stringify(relevantMessages);\n\t\tlet hash = 0;\n\t\tfor (let i = 0; i < content.length; i++) {\n\t\t\thash = (hash << 5) - hash + content.charCodeAt(i);\n\t\t\thash = hash & hash;\n\t\t}\n\t\treturn `ai_cache_${hash}`;\n\t}\n\n\t/**\n\t * Checks if an identical conversation history has been answered recently.\n\t */\n\t// biome-ignore lint/suspicious/noExplicitAny: standard AI message array\n\tasync get(messages: any[]): Promise<string | null> {\n\t\tconst key = this.hashMessages(messages);\n\t\treturn this.driver.get<string>(key);\n\t}\n\n\t/**\n\t * Saves the generated AI response to cache.\n\t */\n\tasync set(\n\t\t// biome-ignore lint/suspicious/noExplicitAny: standard AI message array\n\t\tmessages: any[],\n\t\tresponseText: string,\n\t\tttlSeconds: number = 3600,\n\t): Promise<void> {\n\t\tconst key = this.hashMessages(messages);\n\t\tawait this.driver.set(key, responseText, ttlSeconds);\n\t}\n}\n","import { join, sep } from \"node:path\";\nimport type { Server, ServerWebSocket } from \"bun\";\nimport { z } from \"zod\";\nimport { Container } from \"./container\";\nimport { Context } from \"./context\";\nimport { getControllerMeta } from \"./decorators\";\nimport { HttpError } from \"./helpers/async-handler\";\nimport { toResponse } from \"./helpers/response\";\nimport { logger } from \"./logger\";\nimport { Router } from \"./router\";\nimport { VERSION } from \"./core-exports\";\n\nexport interface WSData<DI = Record<string, unknown>> {\n\tctx: Context<DI>;\n\thandler: WSHandler<DI>;\n}\n\nexport interface WSOptions {\n\t/** Per-message deflate — Bun native, default false */\n\tperMessageDeflate?: boolean | object;\n\t/** Max payload length bytes — default 16MB */\n\tmaxPayloadLength?: number;\n\t/** Idle timeout seconds before close — default 120, 30 untuk heartbeat */\n\tidleTimeout?: number;\n\t/** Backpressure highWaterMark */\n\tmaxBackpressure?: number;\n\t/** Publish to self? default false */\n\tpublishToSelf?: boolean;\n}\n\nexport interface WSHandler<DI = Record<string, unknown>> {\n\topen?: (ws: ServerWebSocket<WSData<DI>>) => void;\n\tmessage?: (ws: ServerWebSocket<WSData<DI>>, message: string | Buffer) => void;\n\tclose?: (\n\t\tws: ServerWebSocket<WSData<DI>>,\n\t\tcode: number,\n\t\treason: string,\n\t) => void;\n\tdrain?: (ws: ServerWebSocket<WSData<DI>>) => void;\n\tpong?: (ws: ServerWebSocket<WSData<DI>>) => void;\n\t/**\n\t * Optional authentication handler.\n\t * Called on connection open. Return null to reject the connection.\n\t * The returned data is stored in `ws.data.auth`.\n\t */\n\tauthenticate?: (ws: ServerWebSocket<WSData<DI>>) => Promise<unknown | null>;\n}\n\nexport type ExtractParams<Path extends string> =\n\tPath extends `${infer _Start}:${infer Param}/${infer Rest}`\n\t\t? { [K in Param]: string } & ExtractParams<`/${Rest}`>\n\t\t: Path extends `${infer _Start}:${infer Param}`\n\t\t\t? { [K in Param]: string }\n\t\t\t: Path extends `${infer _Start}*${infer Catchall}`\n\t\t\t\t? { [K in Catchall]: string } & { \"*\": string }\n\t\t\t\t: Record<string, never>;\n\n/**\n * @deprecated Use `ZodCtx<{ body: typeof schema; query: typeof schema }>` instead.\n * `RouteContext` has 5 generic parameters that are hard to remember.\n * `ZodCtx` only needs one and infers types automatically from your Zod schemas.\n */\nexport type RouteContext<\n\tPath extends string = string,\n\tBodyType = unknown,\n\tDI = Record<string, unknown>,\n\tQueryType = unknown,\n\tParamsType = ExtractParams<Path>,\n> = {\n\tbody(): Promise<BodyType>;\n\tvalid<T extends \"body\" | \"query\" | \"params\">(\n\t\ttarget: T,\n\t): T extends \"body\"\n\t\t? BodyType\n\t\t: T extends \"query\"\n\t\t\t? QueryType\n\t\t\t: T extends \"params\"\n\t\t\t\t? ParamsType\n\t\t\t\t: unknown;\n} & Context<DI, ParamsType>;\n\n/**\n * Helper to infer Context types from Zod schemas automatically.\n *\n * Just pass your Zod schemas — no need to remember generic parameter order.\n *\n * @example\n * const paginationSchema = z.object({ page: z.coerce.number(), limit: z.coerce.number() });\n *\n * @Controller(\"/users\")\n * export class UserController {\n *   @Get(\"/\")\n *   @Use(zValidator(\"query\", paginationSchema))\n *   async getAll(ctx: ZodCtx<{ query: typeof paginationSchema }>) {\n *     const { page, limit } = ctx.valid(\"query\"); // Fully typed!\n *   }\n * }\n */\nexport type ZodCtx<\n\t// biome-ignore lint/suspicious/noExplicitAny: Requires accessing Zod types\n\tT extends { query?: any; body?: any; params?: any },\n> = {\n\tbody(): Promise<T[\"body\"] extends { _type: infer U } ? U : T[\"body\"]>;\n\tvalid<Target extends \"body\" | \"query\" | \"params\">(\n\t\ttarget: Target,\n\t): Target extends \"body\"\n\t\t? T[\"body\"] extends { _type: infer U }\n\t\t\t? U\n\t\t\t: T[\"body\"]\n\t\t: Target extends \"query\"\n\t\t\t? T[\"query\"] extends { _type: infer U }\n\t\t\t\t? U\n\t\t\t\t: T[\"query\"]\n\t\t\t: Target extends \"params\"\n\t\t\t\t? T[\"params\"] extends { _type: infer U }\n\t\t\t\t\t? U\n\t\t\t\t\t: T[\"params\"]\n\t\t\t\t: unknown;\n} & Context<\n\tRecord<string, unknown>,\n\tT[\"params\"] extends { _type: infer U }\n\t\t? U\n\t\t: T[\"params\"] extends Record<string, string>\n\t\t\t? T[\"params\"]\n\t\t\t: Record<string, string>\n>;\n\nexport type HandlerReturn =\n\t| Response\n\t| string\n\t| number\n\t| boolean\n\t| bigint\n\t| Record<string, unknown>\n\t| unknown[]\n\t| null\n\t| undefined\n\t| void\n\t| Promise<\n\t\t\t| Response\n\t\t\t| string\n\t\t\t| number\n\t\t\t| boolean\n\t\t\t| bigint\n\t\t\t| Record<string, unknown>\n\t\t\t| unknown[]\n\t\t\t| null\n\t\t\t| undefined\n\t\t\t| void\n\t  >;\n\nexport type Handler<\n\tDI = Record<string, unknown>,\n\tPath extends string = string,\n> = (ctx: Context<DI, ExtractParams<Path>>) => HandlerReturn;\nexport type Middleware<\n\tDI = Record<string, unknown>,\n\tPath extends string = string,\n> = (\n\tctx: Context<DI, ExtractParams<Path>>,\n\tnext: () => Promise<Response> | Response,\n) => HandlerReturn;\nexport type ErrorHandler<DI = Record<string, unknown>> = (\n\terr: Error,\n\t// biome-ignore lint/suspicious/noExplicitAny: generic\n\tctx: Context<DI, any>,\n) => HandlerReturn;\nexport type NotFoundHandler<DI = Record<string, unknown>> = (\n\t// biome-ignore lint/suspicious/noExplicitAny: generic\n\tctx: Context<DI, any>,\n) => HandlerReturn;\n\nexport interface EnvValidationOptions {\n\t/**\n\t * Custom error handler called when env validation fails.\n\t * If provided, the default error printing and process.exit(1) are skipped.\n\t * Use this to send alerts to monitoring services or custom logging before exiting.\n\t *\n\t * @example\n\t * app.validateEnv(schema, {\n\t *   onError: (errors) => {\n\t *     // Send to monitoring service\n\t *     await sentry.captureException(new Error(\"Env validation failed\"));\n\t *     // Log to external service\n\t *     await logger.fatal(\"Missing env vars\", { errors });\n\t *     // Then exit manually\n\t *     process.exit(1);\n\t *   }\n\t * });\n\t */\n\tonError?: (errors: { field: string; message: string }[]) => void | never;\n}\n\nexport interface StaticOptions {\n\t/** Cache max-age in seconds (default: 3600) */\n\tmaxAge?: number;\n\t/** Override the full Cache-Control header value */\n\tcacheControl?: string;\n\t/** Enable ETag generation and conditional requests (default: true) */\n\tetag?: boolean;\n}\n\nexport interface ApiDocsOptions {\n\t/** URL path for the docs UI (default: \"/docs\") */\n\tpath?: string;\n\t/** API title displayed in the docs (default: \"API Documentation\") */\n\ttitle?: string;\n\t/** API version (default: \"1.0.0\") */\n\tversion?: string;\n\t/** API description */\n\tdescription?: string;\n\t/** Hide docs when NODE_ENV is \"production\" (default: false) */\n\tsafeOnProduction?: boolean;\n}\n\nexport class App<DI extends Record<string, unknown> = Record<string, unknown>> {\n\tprivate router: Router;\n\tprivate middlewares: Middleware<DI>[] = [];\n\tprivate compiledGlobalPipeline?: (\n\t\tctx: Context<DI>,\n\t\tfinalHandler: Handler<DI>,\n\t) => HandlerReturn;\n\tprivate iconPath: string = \"./public/favicon.ico\";\n\tprivate isListening: boolean = false;\n\tpublic di = {} as DI;\n\tprivate wsRoutes: Map<string, WSHandler<DI>> = new Map();\n\tprivate wsOpts: WSOptions = {};\n\tprivate poweredByHeaderEnabled: boolean = true;\n\tprivate _reusePort: boolean = false;\n\tprivate _skipLogResponse: boolean = false;\n\tpublic _apiDocsConfig: ApiDocsOptions | null = null;\n\t// biome-ignore lint/suspicious/noExplicitAny: Required for internal OpenAPI registry\n\tpublic openApiDocs: any[] = [];\n\tprivate container: Container | null = null;\n\n\t/**\n\t * The underlying Bun Server instance. Only available after app.listen() is called.\n\t * Can be used for server.publish() to broadcast WebSocket messages.\n\t */\n\tpublic server?: Server<WSData<DI>>;\n\n\tprivate customErrorHandler: ErrorHandler<DI> = (err, ctx) => {\n\t\tlogger.error(\"Unhandled Exception\", {\n\t\t\terror: err.message,\n\t\t\tstack: err.stack,\n\t\t});\n\t\tconst status = err instanceof HttpError ? err.status : 500;\n\t\tconst errorName =\n\t\t\tstatus === 400\n\t\t\t\t? \"Bad Request\"\n\t\t\t\t: status === 401\n\t\t\t\t\t? \"Unauthorized\"\n\t\t\t\t\t: status === 403\n\t\t\t\t\t\t? \"Forbidden\"\n\t\t\t\t\t\t: status === 404\n\t\t\t\t\t\t\t? \"Not Found\"\n\t\t\t\t\t\t\t: status === 405\n\t\t\t\t\t\t\t\t? \"Method Not Allowed\"\n\t\t\t\t\t\t\t\t: status === 409\n\t\t\t\t\t\t\t\t\t? \"Conflict\"\n\t\t\t\t\t\t\t\t\t: status === 422\n\t\t\t\t\t\t\t\t\t\t? \"Unprocessable Entity\"\n\t\t\t\t\t\t\t\t\t\t: status === 429\n\t\t\t\t\t\t\t\t\t\t\t? \"Too Many Requests\"\n\t\t\t\t\t\t\t\t\t\t\t: status === 503\n\t\t\t\t\t\t\t\t\t\t\t\t? \"Service Unavailable\"\n\t\t\t\t\t\t\t\t\t\t\t\t: \"Internal Server Error\";\n\t\treturn ctx.json(\n\t\t\t{\n\t\t\t\tsuccess: false,\n\t\t\t\terror: errorName,\n\t\t\t\tmessage:\n\t\t\t\t\tprocess.env.NODE_ENV === \"production\"\n\t\t\t\t\t\t? \"An unexpected error occurred\"\n\t\t\t\t\t\t: err.message,\n\t\t\t},\n\t\t\tstatus,\n\t\t);\n\t};\n\n\tprivate customNotFoundHandler: NotFoundHandler<DI> = (ctx) => {\n\t\treturn ctx.json(\n\t\t\t{\n\t\t\t\tsuccess: false,\n\t\t\t\terror: \"Not Found\",\n\t\t\t\tpath: new URL(ctx.request.url).pathname,\n\t\t\t},\n\t\t\t404,\n\t\t);\n\t};\n\n\tconstructor() {\n\t\tthis.router = new Router();\n\t\tthis.registerFaviconRoute();\n\t}\n\n\tpublic use(middleware: Middleware<DI>): this {\n\t\tthis.middlewares.push(middleware);\n\t\treturn this;\n\t}\n\n\tpublic set<K extends keyof DI>(key: K, value: DI[K]): this {\n\t\tthis.di[key] = value;\n\t\treturn this;\n\t}\n\n\t/**\n\t * Attach an IoC Container to the app. When set, `registerController()`\n\t * will resolve controllers via `container.resolve()` (use `FactoryProvider` for ctor deps).\n\t */\n\tpublic setContainer(container: Container): this {\n\t\tthis.container = container;\n\t\treturn this;\n\t}\n\n\t/**\n\t * Get the attached IoC Container, or create an empty one if none was set.\n\t */\n\tpublic getContainer(): Container {\n\t\tif (!this.container) {\n\t\t\tthis.container = new Container();\n\t\t}\n\t\treturn this.container;\n\t}\n\n\tpublic group(prefix: string): RouterGroup<DI> {\n\t\treturn new RouterGroup<DI>(prefix, this);\n\t}\n\n\t/**\n\t * Register a WebSocket endpoint at an exact path (no params/wildcards).\n\t * Backed directly by Bun's native WebSocket server — no polyfill or\n\t * extra abstraction layer between your handler and `Bun.serve`.\n\t *\n\t * ```ts\n\t * app.ws(\"/chat\", {\n\t *   open: (ws) => ws.subscribe(\"room\"),\n\t *   message: (ws, msg) => ws.publish(\"room\", msg),\n\t * });\n\t * ```\n\t */\n\tpublic ws(path: string, handler: WSHandler<DI>): this {\n\t\tthis.wsRoutes.set(path, handler);\n\t\treturn this;\n\t}\n\n\t/** Configure Bun WebSocket options — Bun-only, zero-deps */\n\tpublic wsOptions(opts: WSOptions): this {\n\t\tthis.wsOpts = { ...this.wsOpts, ...opts };\n\t\treturn this;\n\t}\n\n\t/**\n\t * Type-safe Environment Variables Validator (static).\n\t * Validates `process.env` against a Zod schema object and returns the typed result.\n\t * If validation fails, it prints a beautiful error to the console and exits the process (stops booting).\n\t *\n\t * @example\n\t * // Static — no App instance needed (ideal for src/env.ts)\n\t * const env = App.validateEnv({\n\t *   DATABASE_URL: z.string().url(),\n\t *   PORT: z.coerce.number().default(3000),\n\t * });\n\t *\n\t * @example\n\t * // Custom error handler - send to monitoring before exiting\n\t * const env = App.validateEnv({\n\t *   DATABASE_URL: z.string().url(),\n\t * }, {\n\t *   onError: (errors) => {\n\t *     // Send to monitoring service\n\t *     console.error(\"ENV ERROR:\", errors);\n\t *     // Exit manually after alerting\n\t *     process.exit(1);\n\t *   }\n\t * });\n\t */\n\tstatic validateEnv<T extends z.ZodRawShape>(\n\t\tschema: T,\n\t\toptions?: EnvValidationOptions,\n\t): z.infer<z.ZodObject<T>> {\n\t\tconst envSchema = z.object(schema);\n\t\tconst result = envSchema.safeParse(process.env);\n\n\t\tif (!result.success) {\n\t\t\tconst errors = result.error.issues.map((err) => ({\n\t\t\t\tfield: err.path.join(\".\"),\n\t\t\t\tmessage: err.message,\n\t\t\t}));\n\n\t\t\t// If custom handler provided, call it and skip default behavior\n\t\t\tif (options?.onError) {\n\t\t\t\toptions.onError(errors);\n\t\t\t\t// If onError didn't throw/exit, throw error for caller to handle\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Environment validation failed: ${errors.map((e) => `${e.field}: ${e.message}`).join(\", \")}`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t// Default behavior: print error and exit\n\t\t\tconsole.error(\"\\n\\x1b[41m\\x1b[37m 🚨 Buntok Environment Error \\x1b[0m\\n\");\n\t\t\tconsole.error(\n\t\t\t\t\"\\x1b[31mMissing or invalid environment variables:\\x1b[0m\\n\",\n\t\t\t);\n\n\t\t\terrors.forEach((err) => {\n\t\t\t\tconsole.error(\n\t\t\t\t\t`  \\x1b[33m❯\\x1b[0m \\x1b[36m${err.field}\\x1b[0m: \\x1b[90m${err.message}\\x1b[0m`,\n\t\t\t\t);\n\t\t\t});\n\n\t\t\tconsole.error(\"\\n\\x1b[31mServer boot aborted.\\x1b[0m\\n\");\n\t\t\tprocess.exit(1);\n\t\t}\n\n\t\treturn result.data;\n\t}\n\n\t/**\n\t * Type-safe Environment Variables Validator (instance).\n\t * Delegates to the static method. Kept for backward compatibility.\n\t *\n\t * @example\n\t * const app = new App();\n\t * const env = app.validateEnv({\n\t *   DATABASE_URL: z.string().url(),\n\t *   PORT: z.coerce.number().default(3000),\n\t * });\n\t */\n\tpublic validateEnv<T extends z.ZodRawShape>(\n\t\tschema: T,\n\t\toptions?: EnvValidationOptions,\n\t): z.infer<z.ZodObject<T>> {\n\t\treturn App.validateEnv(schema, options);\n\t}\n\n\t/**\n\t * Register all `@Get`/`@Post`/etc. routes declared on a `@Controller`\n\t * class. This is sugar over `registerRoute()` — it instantiates the\n\t * class once (at boot time, not per request) and wires each decorated\n\t * method up exactly like a manual `app.get(path, handler)` call would.\n\t *\n\t * When a Container is attached via `app.setContainer()`, `@Inject`-\n\t * annotated properties on the controller are automatically resolved\n\t * from the container before routes are bound.\n\t *\n\t * ```ts\n\t * app.registerController(UserController);\n\t * ```\n\t */\n\tpublic registerController<T extends object>(\n\t\t// biome-ignore lint/suspicious/noExplicitAny: Constructor args are unknowable\n\t\ttarget: (new (...args: any[]) => T) | T,\n\t): this {\n\t\t// Detect if target is a class (constructor) or an instance\n\t\tconst isInstance =\n\t\t\ttypeof target === \"object\" &&\n\t\t\ttarget !== null &&\n\t\t\ttypeof target === \"object\";\n\n\t\tconst ControllerClass = isInstance\n\t\t\t? // biome-ignore lint/suspicious/noExplicitAny: dynamic type narrowing\n\t\t\t\t(target as any).constructor\n\t\t\t: // biome-ignore lint/suspicious/noExplicitAny: dynamic type narrowing\n\t\t\t\t(target as new (\n\t\t\t\t\t...args: any[]\n\t\t\t\t) => T);\n\n\t\tconst meta = getControllerMeta(ControllerClass);\n\t\tif (!meta) {\n\t\t\tthrow new Error(\n\t\t\t\t`${ControllerClass.name} is not decorated with @Controller — did you forget to add it?`,\n\t\t\t);\n\t\t}\n\n\t\tlet instance: T;\n\n\t\tif (isInstance) {\n\t\t\t// Use the provided instance directly\n\t\t\tinstance = target as T;\n\t\t} else if (this.container) {\n\t\t\tconst resolved = this.container.resolve<T>(ControllerClass);\n\t\t\tinstance = resolved;\n\t\t} else {\n\t\t\tinstance = new ControllerClass();\n\t\t}\n\n\t\tconst normalizedPrefix = meta.prefix.endsWith(\"/\")\n\t\t\t? meta.prefix.slice(0, -1)\n\t\t\t: meta.prefix;\n\n\t\tfor (const route of meta.routes) {\n\t\t\tconst cleanPath = route.path === \"/\" ? \"\" : route.path;\n\t\t\tconst fullPath = `${normalizedPrefix}${cleanPath}` || \"/\";\n\t\t\t// biome-ignore lint/suspicious/noExplicitAny: dynamic method dispatch by decorated property key\n\t\t\tlet handler = (instance as any)[route.propertyKey].bind(\n\t\t\t\tinstance,\n\t\t\t) as Handler<DI>;\n\n\t\t\t// Apply zero-cost wrappers for @HttpCode/@Header/@Redirect (boot-time, no per-request alloc beyond wrapper closure)\n\t\t\tconst hasStatus = route.statusCode !== undefined;\n\t\t\tconst hasHeaders = route.headers && route.headers.length > 0;\n\t\t\tconst hasRedirect = !!route.redirect;\n\t\t\tif (hasStatus || hasHeaders || hasRedirect) {\n\t\t\t\tconst original = handler;\n\t\t\t\tconst routeMeta = route;\n\t\t\t\t// biome-ignore lint/suspicious/noExplicitAny: wrapper must accept any Context shape\n\t\t\t\thandler = ((ctx: any) => {\n\t\t\t\t\t// Redirect - static or dynamic override (Nest behavior)\n\t\t\t\t\tif (hasRedirect) {\n\t\t\t\t\t\tconst result: any = original(ctx);\n\t\t\t\t\t\t// If handler returns promise, handle async\n\t\t\t\t\t\tif (result instanceof Promise) {\n\t\t\t\t\t\t\treturn result.then((val: any) => {\n\t\t\t\t\t\t\t\tif (val && typeof val === \"object\" && \"url\" in val) {\n\t\t\t\t\t\t\t\t\tconst url = (val as any).url as string;\n\t\t\t\t\t\t\t\t\tconst sc = (val as any).statusCode ?? routeMeta.redirect!.statusCode;\n\t\t\t\t\t\t\t\t\treturn new Response(null, { status: sc, headers: { Location: url } });\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t// Also allow string return as url override\n\t\t\t\t\t\t\t\tif (typeof val === \"string\" && val.startsWith(\"/\")) {\n\t\t\t\t\t\t\t\t\treturn new Response(null, { status: routeMeta.redirect!.statusCode, headers: { Location: val } });\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t// Static redirect\n\t\t\t\t\t\t\t\treturn new Response(null, {\n\t\t\t\t\t\t\t\t\tstatus: routeMeta.redirect!.statusCode,\n\t\t\t\t\t\t\t\t\theaders: { Location: routeMeta.redirect!.url },\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (result && typeof result === \"object\" && \"url\" in result) {\n\t\t\t\t\t\t\tconst url = (result as any).url as string;\n\t\t\t\t\t\t\tconst sc = (result as any).statusCode ?? routeMeta.redirect!.statusCode;\n\t\t\t\t\t\t\treturn new Response(null, { status: sc, headers: { Location: url } });\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (typeof result === \"string\" && result.startsWith(\"/\")) {\n\t\t\t\t\t\t\treturn new Response(null, { status: routeMeta.redirect!.statusCode, headers: { Location: result } });\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn new Response(null, {\n\t\t\t\t\t\t\tstatus: routeMeta.redirect!.statusCode,\n\t\t\t\t\t\t\theaders: { Location: routeMeta.redirect!.url },\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t\tconst raw: any = original(ctx);\n\t\t\t\t\tconst apply = (res: Response): Response => {\n\t\t\t\t\t\tlet r: Response = res;\n\t\t\t\t\t\tif (hasStatus && r.status === 200) {\n\t\t\t\t\t\t\t// Override status only if default 200 (preserve explicit ctx.status etc.)\n\t\t\t\t\t\t\tr = new Response(r.body, { status: routeMeta.statusCode!, headers: r.headers });\n\t\t\t\t\t\t} else if (hasStatus && r.status === 204 && routeMeta.statusCode !== 204) {\n\t\t\t\t\t\t\t// For void returns that became 204, respect HttpCode\n\t\t\t\t\t\t\tr = new Response(r.body, { status: routeMeta.statusCode!, headers: r.headers });\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (hasHeaders) {\n\t\t\t\t\t\t\tfor (const [k, v] of routeMeta.headers!) {\n\t\t\t\t\t\t\t\tr.headers.set(k, v);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn r;\n\t\t\t\t\t};\n\t\t\t\t\tif (raw instanceof Promise) {\n\t\t\t\t\t\treturn raw.then((v: any) => apply(toResponse(v)));\n\t\t\t\t\t}\n\t\t\t\t\treturn apply(toResponse(raw));\n\t\t\t\t}) as Handler<DI>;\n\t\t\t}\n\n\t\t\tthis.registerRoute(route.method, fullPath, [\n\t\t\t\t...(route.middlewares as Middleware<DI>[]),\n\t\t\t\thandler,\n\t\t\t]);\n\t\t}\n\n\t\treturn this;\n\t}\n\n\tpublic onError(handler: ErrorHandler<DI>): this {\n\t\tthis.customErrorHandler = handler;\n\t\treturn this;\n\t}\n\n\tpublic notFound(handler: NotFoundHandler<DI>): this {\n\t\tthis.customNotFoundHandler = handler;\n\t\treturn this;\n\t}\n\n\t/**\n\t * Disable a built-in feature. Currently only `\"x-powered-by\"` is\n\t * supported, which turns off the `X-Powered-By: buntok` response header.\n\t * The header's value itself is not configurable — this only controls\n\t * whether it's sent.\n\t */\n\tpublic disable(feature: \"x-powered-by\"): this {\n\t\tif (feature === \"x-powered-by\") {\n\t\t\tthis.poweredByHeaderEnabled = false;\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * Re-enable a feature previously turned off with `disable()`.\n\t */\n\tpublic enable(feature: \"x-powered-by\"): this {\n\t\tif (feature === \"x-powered-by\") {\n\t\t\tthis.poweredByHeaderEnabled = true;\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * Enable SO_REUSEPORT for multi-process load balancing (Linux only).\n\t * When enabled, multiple instances of the app can bind to the same port,\n\t * and incoming requests are load-balanced at the kernel level.\n\t *\n\t * ⚠️ On non-Linux platforms (macOS, Windows), this option is silently ignored.\n\t * A warning will be logged in development mode to help catch environment mismatches.\n\t */\n\tpublic enableReusePort(enabled = true): this {\n\t\tthis._reusePort = enabled;\n\n\t\t// Warn in development about platform limitation\n\t\tif (enabled && process.platform !== \"linux\") {\n\t\t\tconsole.warn(\n\t\t\t\t`\\x1b[33m⚠ Warning: enableReusePort() is only supported on Linux. ` +\n\t\t\t\t`Current platform: ${process.platform}. ` +\n\t\t\t\t`This option will be ignored in production.\\x1b[0m`,\n\t\t\t);\n\t\t}\n\n\t\treturn this;\n\t}\n\n\tpublic icon(path: string): this {\n\t\tthis.iconPath = path;\n\t\treturn this;\n\t}\n\n\tprivate registerFaviconRoute(): void {\n\t\tthis.get(\"/favicon.ico\", async (_ctx) => {\n\t\t\t// Try custom path first\n\t\t\tconst customFile = Bun.file(this.iconPath);\n\t\t\tif (await customFile.exists()) {\n\t\t\t\treturn new Response(customFile);\n\t\t\t}\n\t\t\t// Fallback to built-in icon\n\t\t\tconst builtInFile = Bun.file(this.getBuiltInIconPath());\n\t\t\tif (await builtInFile.exists()) {\n\t\t\t\treturn new Response(builtInFile);\n\t\t\t}\n\t\t\t// No favicon found\n\t\t\treturn new Response(null, { status: 404 });\n\t\t});\n\t}\n\n\tprivate getBuiltInIconPath(): string {\n\t\treturn join(__dirname, \"..\", \"public\", \"favicon.ico\");\n\t}\n\n\tpublic get<Path extends string>(path: Path, handler: Handler<DI, Path>): this;\n\tpublic get<Path extends string>(\n\t\tpath: Path,\n\t\tmiddleware: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic get<Path extends string>(\n\t\tpath: Path,\n\t\tm1: Middleware<DI, Path>,\n\t\tm2: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic get<Path extends string>(\n\t\tpath: Path,\n\t\tm1: Middleware<DI, Path>,\n\t\tm2: Middleware<DI, Path>,\n\t\tm3: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic get<Path extends string>(\n\t\tpath: Path,\n\t\tm1: Middleware<DI, Path>,\n\t\tm2: Middleware<DI, Path>,\n\t\tm3: Middleware<DI, Path>,\n\t\tm4: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic get<Path extends string>(\n\t\tpath: Path,\n\t\tm1: Middleware<DI, Path>,\n\t\tm2: Middleware<DI, Path>,\n\t\tm3: Middleware<DI, Path>,\n\t\tm4: Middleware<DI, Path>,\n\t\tm5: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic get<Path extends string>(\n\t\tpath: Path,\n\t\t...handlers: Array<Middleware<DI, Path> | Handler<DI, Path>>\n\t): this {\n\t\tthis.registerRoute(\"GET\", path, handlers);\n\t\treturn this;\n\t}\n\n\tpublic post<Path extends string>(\n\t\tpath: Path,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic post<Path extends string>(\n\t\tpath: Path,\n\t\tmiddleware: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic post<Path extends string>(\n\t\tpath: Path,\n\t\tm1: Middleware<DI, Path>,\n\t\tm2: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic post<Path extends string>(\n\t\tpath: Path,\n\t\tm1: Middleware<DI, Path>,\n\t\tm2: Middleware<DI, Path>,\n\t\tm3: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic post<Path extends string>(\n\t\tpath: Path,\n\t\tm1: Middleware<DI, Path>,\n\t\tm2: Middleware<DI, Path>,\n\t\tm3: Middleware<DI, Path>,\n\t\tm4: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic post<Path extends string>(\n\t\tpath: Path,\n\t\tm1: Middleware<DI, Path>,\n\t\tm2: Middleware<DI, Path>,\n\t\tm3: Middleware<DI, Path>,\n\t\tm4: Middleware<DI, Path>,\n\t\tm5: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic post<Path extends string>(\n\t\tpath: Path,\n\t\t...handlers: Array<Middleware<DI, Path> | Handler<DI, Path>>\n\t): this {\n\t\tthis.registerRoute(\"POST\", path, handlers);\n\t\treturn this;\n\t}\n\n\tpublic put<Path extends string>(path: Path, handler: Handler<DI, Path>): this;\n\tpublic put<Path extends string>(\n\t\tpath: Path,\n\t\tmiddleware: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic put<Path extends string>(\n\t\tpath: Path,\n\t\tm1: Middleware<DI, Path>,\n\t\tm2: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic put<Path extends string>(\n\t\tpath: Path,\n\t\tm1: Middleware<DI, Path>,\n\t\tm2: Middleware<DI, Path>,\n\t\tm3: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic put<Path extends string>(\n\t\tpath: Path,\n\t\tm1: Middleware<DI, Path>,\n\t\tm2: Middleware<DI, Path>,\n\t\tm3: Middleware<DI, Path>,\n\t\tm4: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic put<Path extends string>(\n\t\tpath: Path,\n\t\tm1: Middleware<DI, Path>,\n\t\tm2: Middleware<DI, Path>,\n\t\tm3: Middleware<DI, Path>,\n\t\tm4: Middleware<DI, Path>,\n\t\tm5: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic put<Path extends string>(\n\t\tpath: Path,\n\t\t...handlers: Array<Middleware<DI, Path> | Handler<DI, Path>>\n\t): this {\n\t\tthis.registerRoute(\"PUT\", path, handlers);\n\t\treturn this;\n\t}\n\n\tpublic delete<Path extends string>(\n\t\tpath: Path,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic delete<Path extends string>(\n\t\tpath: Path,\n\t\tmiddleware: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic delete<Path extends string>(\n\t\tpath: Path,\n\t\tm1: Middleware<DI, Path>,\n\t\tm2: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic delete<Path extends string>(\n\t\tpath: Path,\n\t\tm1: Middleware<DI, Path>,\n\t\tm2: Middleware<DI, Path>,\n\t\tm3: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic delete<Path extends string>(\n\t\tpath: Path,\n\t\tm1: Middleware<DI, Path>,\n\t\tm2: Middleware<DI, Path>,\n\t\tm3: Middleware<DI, Path>,\n\t\tm4: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic delete<Path extends string>(\n\t\tpath: Path,\n\t\tm1: Middleware<DI, Path>,\n\t\tm2: Middleware<DI, Path>,\n\t\tm3: Middleware<DI, Path>,\n\t\tm4: Middleware<DI, Path>,\n\t\tm5: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic delete<Path extends string>(\n\t\tpath: Path,\n\t\t...handlers: Array<Middleware<DI, Path> | Handler<DI, Path>>\n\t): this {\n\t\tthis.registerRoute(\"DELETE\", path, handlers);\n\t\treturn this;\n\t}\n\n\tpublic options<Path extends string>(\n\t\tpath: Path,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic options<Path extends string>(\n\t\tpath: Path,\n\t\tmiddleware: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic options<Path extends string>(\n\t\tpath: Path,\n\t\tm1: Middleware<DI, Path>,\n\t\tm2: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic options<Path extends string>(\n\t\tpath: Path,\n\t\tm1: Middleware<DI, Path>,\n\t\tm2: Middleware<DI, Path>,\n\t\tm3: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic options<Path extends string>(\n\t\tpath: Path,\n\t\tm1: Middleware<DI, Path>,\n\t\tm2: Middleware<DI, Path>,\n\t\tm3: Middleware<DI, Path>,\n\t\tm4: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic options<Path extends string>(\n\t\tpath: Path,\n\t\tm1: Middleware<DI, Path>,\n\t\tm2: Middleware<DI, Path>,\n\t\tm3: Middleware<DI, Path>,\n\t\tm4: Middleware<DI, Path>,\n\t\tm5: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic options<Path extends string>(\n\t\tpath: Path,\n\t\t...handlers: Array<Middleware<DI, Path> | Handler<DI, Path>>\n\t): this {\n\t\tthis.registerRoute(\"OPTIONS\", path, handlers);\n\t\treturn this;\n\t}\n\n\tpublic query(path: string, handler: Handler<DI>): this;\n\tpublic query(\n\t\tpath: string,\n\t\tmiddleware: Middleware<DI>,\n\t\thandler: Handler<DI>,\n\t): this;\n\tpublic query(\n\t\tpath: string,\n\t\tm1: Middleware<DI>,\n\t\tm2: Middleware<DI>,\n\t\thandler: Handler<DI>,\n\t): this;\n\tpublic query(\n\t\tpath: string,\n\t\tm1: Middleware<DI>,\n\t\tm2: Middleware<DI>,\n\t\tm3: Middleware<DI>,\n\t\thandler: Handler<DI>,\n\t): this;\n\tpublic query(\n\t\tpath: string,\n\t\tm1: Middleware<DI>,\n\t\tm2: Middleware<DI>,\n\t\tm3: Middleware<DI>,\n\t\tm4: Middleware<DI>,\n\t\thandler: Handler<DI>,\n\t): this;\n\tpublic query(\n\t\tpath: string,\n\t\tm1: Middleware<DI>,\n\t\tm2: Middleware<DI>,\n\t\tm3: Middleware<DI>,\n\t\tm4: Middleware<DI>,\n\t\tm5: Middleware<DI>,\n\t\thandler: Handler<DI>,\n\t): this;\n\tpublic query(\n\t\tpath: string,\n\t\t...handlers: Array<Middleware<DI> | Handler<DI>>\n\t): this {\n\t\tthis.registerRoute(\"QUERY\", path, handlers);\n\t\treturn this;\n\t}\n\n\tpublic patch<Path extends string>(\n\t\tpath: Path,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic patch<Path extends string>(\n\t\tpath: Path,\n\t\tmiddleware: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic patch<Path extends string>(\n\t\tpath: Path,\n\t\tm1: Middleware<DI, Path>,\n\t\tm2: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic patch<Path extends string>(\n\t\tpath: Path,\n\t\tm1: Middleware<DI, Path>,\n\t\tm2: Middleware<DI, Path>,\n\t\tm3: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic patch<Path extends string>(\n\t\tpath: Path,\n\t\tm1: Middleware<DI, Path>,\n\t\tm2: Middleware<DI, Path>,\n\t\tm3: Middleware<DI, Path>,\n\t\tm4: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic patch<Path extends string>(\n\t\tpath: Path,\n\t\tm1: Middleware<DI, Path>,\n\t\tm2: Middleware<DI, Path>,\n\t\tm3: Middleware<DI, Path>,\n\t\tm4: Middleware<DI, Path>,\n\t\tm5: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic patch<Path extends string>(\n\t\tpath: Path,\n\t\t...handlers: Array<Middleware<DI, Path> | Handler<DI, Path>>\n\t): this {\n\t\tthis.registerRoute(\"PATCH\", path, handlers);\n\t\treturn this;\n\t}\n\n\tpublic head<Path extends string>(\n\t\tpath: Path,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic head<Path extends string>(\n\t\tpath: Path,\n\t\tmiddleware: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic head<Path extends string>(\n\t\tpath: Path,\n\t\tm1: Middleware<DI, Path>,\n\t\tm2: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic head<Path extends string>(\n\t\tpath: Path,\n\t\tm1: Middleware<DI, Path>,\n\t\tm2: Middleware<DI, Path>,\n\t\tm3: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic head<Path extends string>(\n\t\tpath: Path,\n\t\tm1: Middleware<DI, Path>,\n\t\tm2: Middleware<DI, Path>,\n\t\tm3: Middleware<DI, Path>,\n\t\tm4: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic head<Path extends string>(\n\t\tpath: Path,\n\t\tm1: Middleware<DI, Path>,\n\t\tm2: Middleware<DI, Path>,\n\t\tm3: Middleware<DI, Path>,\n\t\tm4: Middleware<DI, Path>,\n\t\tm5: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic head<Path extends string>(\n\t\tpath: Path,\n\t\t...handlers: Array<Middleware<DI, Path> | Handler<DI, Path>>\n\t): this {\n\t\tthis.registerRoute(\"HEAD\", path, handlers);\n\t\treturn this;\n\t}\n\n\t/**\n\t * Register a handler for all standard HTTP methods (GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS).\n\t */\n\tpublic all<Path extends string>(path: Path, handler: Handler<DI, Path>): this;\n\tpublic all<Path extends string>(\n\t\tpath: Path,\n\t\tmiddleware: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic all<Path extends string>(\n\t\tpath: Path,\n\t\tm1: Middleware<DI, Path>,\n\t\tm2: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic all<Path extends string>(\n\t\tpath: Path,\n\t\tm1: Middleware<DI, Path>,\n\t\tm2: Middleware<DI, Path>,\n\t\tm3: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic all<Path extends string>(\n\t\tpath: Path,\n\t\tm1: Middleware<DI, Path>,\n\t\tm2: Middleware<DI, Path>,\n\t\tm3: Middleware<DI, Path>,\n\t\tm4: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic all<Path extends string>(\n\t\tpath: Path,\n\t\tm1: Middleware<DI, Path>,\n\t\tm2: Middleware<DI, Path>,\n\t\tm3: Middleware<DI, Path>,\n\t\tm4: Middleware<DI, Path>,\n\t\tm5: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic all<Path extends string>(\n\t\tpath: Path,\n\t\t...handlers: Array<Middleware<DI, Path> | Handler<DI, Path>>\n\t): this {\n\t\tconst methods = [\n\t\t\t\"GET\",\n\t\t\t\"HEAD\",\n\t\t\t\"POST\",\n\t\t\t\"PUT\",\n\t\t\t\"PATCH\",\n\t\t\t\"DELETE\",\n\t\t\t\"OPTIONS\",\n\t\t];\n\t\tfor (const method of methods) {\n\t\t\tthis.registerRoute(method, path, handlers);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * Serve static files from `directory` under `routePath`.\n\t *\n\t * Rejects any request whose resolved path escapes `directory` (e.g.\n\t * `/files/../../etc/passwd`) — without this check, static serving is a\n\t * directory-traversal vulnerability.\n\t *\n\t * Supports ETag-based conditional requests (If-None-Match → 304).\n\t * ETag is computed on every response and sent to the client, enabling\n\t * efficient caching on subsequent requests.\n\t */\n\tpublic static(routePath: string, directory: string, options?: StaticOptions): this {\n\t\tconst baseDir = join(process.cwd(), directory);\n\t\tconst maxAge = options?.maxAge ?? 3600;\n\t\tconst cacheControl = options?.cacheControl ?? `public, max-age=${maxAge}`;\n\t\tconst enableEtag = options?.etag !== false;\n\n\t\tconst handler: Handler<DI> = async (ctx) => {\n\t\t\tconst requestedPath = ctx.params[\"*\"] || \"\";\n\t\t\tlet absolutePath = join(baseDir, requestedPath);\n\n\t\t\tif (absolutePath !== baseDir && !absolutePath.startsWith(baseDir + sep)) {\n\t\t\t\treturn ctx.json({ error: \"Forbidden\" }, 403);\n\t\t\t}\n\n\t\t\t// If file doesn't exist, check for directory with index.html\n\t\t\tif (!(await Bun.file(absolutePath).exists())) {\n\t\t\t\tconst indexPath = join(absolutePath, \"index.html\");\n\t\t\t\tif (await Bun.file(indexPath).exists()) {\n\t\t\t\t\tabsolutePath = indexPath;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst file = Bun.file(absolutePath);\n\n\t\t\tif (!(await file.exists())) {\n\t\t\t\treturn ctx.json({ error: \"File Not Found\" }, 404);\n\t\t\t}\n\n\t\t\tconst etag = enableEtag ? `\"${file.size}-${file.lastModified}\"` : null;\n\n\t\t\t// ETag-based conditional request (If-None-Match → 304)\n\t\t\tif (etag) {\n\t\t\t\tconst ifNoneMatch = ctx.request.headers.get(\"If-None-Match\");\n\t\t\t\tif (ifNoneMatch === etag) {\n\t\t\t\t\treturn new Response(null, {\n\t\t\t\t\t\tstatus: 304,\n\t\t\t\t\t\theaders: { ETag: etag },\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst headers: Record<string, string> = {\n\t\t\t\t\"Cache-Control\": cacheControl,\n\t\t\t};\n\t\t\tif (etag) {\n\t\t\t\theaders.ETag = etag;\n\t\t\t}\n\n\t\t\treturn new Response(file, { headers });\n\t\t};\n\n\t\tconst wildcardPath = routePath.endsWith(\"/\")\n\t\t\t? `${routePath}*`\n\t\t\t: `${routePath}/*`;\n\t\tthis.get(wildcardPath, handler);\n\t\treturn this;\n\t}\n\n\t/**\n\t * Enable interactive API documentation at the specified path.\n\t *\n\t * Serves a built-in docs UI and the generated OpenAPI spec (swagger.json).\n\t * The docs routes are registered directly on the router and do NOT appear\n\t * in the generated swagger.json.\n\t *\n\t * @example\n\t * ```ts\n\t * app.apiDocs({ path: \"/docs\", title: \"My API\", version: \"1.0.0\" });\n\t * ```\n\t */\n\tpublic apiDocs(options?: ApiDocsOptions): this {\n\t\tthis._apiDocsConfig = {\n\t\t\tpath: options?.path ?? \"/docs\",\n\t\t\ttitle: options?.title ?? \"API Documentation\",\n\t\t\tversion: options?.version ?? \"1.0.0\",\n\t\t\tdescription: options?.description,\n\t\t\tsafeOnProduction: options?.safeOnProduction ?? false,\n\t\t};\n\t\treturn this;\n\t}\n\n\tpublic registerRoute(\n\t\tmethod: string,\n\t\tpath: string,\n\t\thandlers: Array<Middleware<DI> | Handler<DI>>,\n\t): void {\n\t\tconst mainHandler = handlers[handlers.length - 1] as Handler<DI>;\n\t\tconst routeMiddlewares = handlers.slice(0, -1) as Middleware<DI>[];\n\n\t\t// Collect OpenAPI metadata\n\t\t// biome-ignore lint/suspicious/noExplicitAny: Required for flexible schema representation\n\t\tconst openApiDoc: any = {\n\t\t\tmethod: method.toLowerCase(),\n\t\t\tpath,\n\t\t\trequest: { params: null, query: null, body: null },\n\t\t\tresponses: [],\n\t\t};\n\n\t\tfor (const handler of handlers) {\n\t\t\tconst h = handler as unknown as Record<string, unknown>;\n\t\t\tif (h._isBuntokValidator) {\n\t\t\t\tconst target = h._target as string;\n\t\t\t\tconst schema = h._schema;\n\t\t\t\topenApiDoc.request[target] = schema;\n\t\t\t\tif (target === \"body\") {\n\t\t\t\t\topenApiDoc.request.bodyContentType = h._contentType as string;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (h._isBuntokResponse) {\n\t\t\t\topenApiDoc.responses.push({\n\t\t\t\t\tstatus: h._status,\n\t\t\t\t\tschema: h._schema,\n\t\t\t\t\tdescription: h._description,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\t// Always register the route in OpenAPI, even if it lacks explicit validation/responses\n\t\tthis.openApiDocs.push(openApiDoc);\n\n\t\t// AOT Compile route middlewares\n\t\tconst executionChain = this.compilePipeline(routeMiddlewares, mainHandler);\n\n\t\tthis.router.insert(method, path, executionChain);\n\t}\n\n\tprivate compilePipeline(\n\t\tmiddlewares: Middleware<DI>[],\n\t\tfinalHandler: Handler<DI>,\n\t): Handler<DI> {\n\t\tif (middlewares.length === 0) return finalHandler;\n\n\t\tconst fns = [...middlewares, finalHandler];\n\t\tlet code = `return fns[${fns.length - 1}](ctx);`;\n\t\tfor (let i = fns.length - 2; i >= 0; i--) {\n\t\t\tcode = `return fns[${i}](ctx, () => { ${code} });`;\n\t\t}\n\n\t\tconst factory = new Function(\"fns\", `return function(ctx) { ${code} }`);\n\t\treturn factory(fns) as Handler<DI>;\n\t}\n\n\t/**\n\t * Register API docs routes directly on the router.\n\t * These routes do NOT go through registerRoute() and therefore\n\t * do NOT appear in openApiDocs / swagger.json.\n\t */\n\tprivate registerApiDocsRoutes(): void {\n\t\tconst config = this._apiDocsConfig!;\n\t\tconst basePath = (config.path ?? \"/docs\").replace(/\\/+$/, \"\");\n\n\t\t// Resolve template paths relative to this package's dist directory\n\t\tconst templatesDir = new URL(\"./cli/templates\", import.meta.url).pathname;\n\n\t\t// Handler for serving the HTML docs UI — safe injection via app.apiDocs()\n\t\tconst uiHandler: Handler<DI> = async () => {\n\t\t\tconst htmlPath = join(templatesDir, \"index.html\");\n\t\t\tconst file = Bun.file(htmlPath);\n\t\t\tif (await file.exists()) {\n\t\t\t\tlet html = await file.text();\n\t\t\t\t// Safe HTML escape for <title> (prevent </title> injection)\n\t\t\t\tconst rawTitle = config.title ?? \"API Reference\";\n\t\t\t\tconst escTitleHtml = String(rawTitle)\n\t\t\t\t\t.replace(/&/g, \"&amp;\")\n\t\t\t\t\t.replace(/</g, \"&lt;\")\n\t\t\t\t\t.replace(/>/g, \"&gt;\")\n\t\t\t\t\t.replace(/\"/g, \"&quot;\");\n\t\t\t\thtml = html.replace(\n\t\t\t\t\t/<title>.*?<\\/title>/,\n\t\t\t\t\t`<title>${escTitleHtml} — API Reference</title>`,\n\t\t\t\t);\n\t\t\t\t// Safe JSON injection via JSON.stringify (handles quotes, newlines, unicode)\n\t\t\t\thtml = html.replace(\n\t\t\t\t\t/\"title\":\\s*\"API Documentation\"/,\n\t\t\t\t\t`\"title\":${JSON.stringify(config.title ?? \"API Documentation\")}`,\n\t\t\t\t);\n\t\t\t\thtml = html.replace(\n\t\t\t\t\t/\"version\":\\s*\"1\\.0\\.0\"/,\n\t\t\t\t\t`\"version\":${JSON.stringify(config.version ?? \"1.0.0\")}`,\n\t\t\t\t);\n\t\t\t\tif (config.description) {\n\t\t\t\t\thtml = html.replace(\n\t\t\t\t\t\t/\"description\":\\s*\"Loading API specification\\.\\.\\.\"/,\n\t\t\t\t\t\t`\"description\":${JSON.stringify(config.description)}`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\treturn new Response(html, {\n\t\t\t\t\theaders: { \"Content-Type\": \"text/html; charset=utf-8\" },\n\t\t\t\t});\n\t\t\t}\n\t\t\treturn new Response(\"Docs UI not found. Run 'buntok make:docs' first.\", { status: 404 });\n\t\t};\n\n\t\t// Handler for serving swagger.json (reads fresh on every request)\n\t\tconst swaggerHandler: Handler<DI> = async () => {\n\t\t\tconst swaggerPath = join(process.cwd(), \"public/docs/swagger.json\");\n\t\t\tconst file = Bun.file(swaggerPath);\n\t\t\tif (await file.exists()) {\n\t\t\t\treturn new Response(file, {\n\t\t\t\t\theaders: { \"Content-Type\": \"application/json\" },\n\t\t\t\t});\n\t\t\t}\n\t\t\treturn new Response(JSON.stringify({ error: \"swagger.json not found. Run 'buntok make:docs' first.\" }), {\n\t\t\t\tstatus: 404,\n\t\t\t\theaders: { \"Content-Type\": \"application/json\" },\n\t\t\t});\n\t\t};\n\n\t\t// Handler for serving static assets (tailwind.js, font-googles.css)\n\t\tconst assetsHandler: Handler<DI> = async (ctx) => {\n\t\t\tconst assetName = ctx.params[\"*\"] ?? \"\";\n\t\t\tconst assetPath = join(templatesDir, assetName);\n\t\t\tconst file = Bun.file(assetPath);\n\t\t\tif (await file.exists()) {\n\t\t\t\tconst ext = assetName.split(\".\").pop()?.toLowerCase() ?? \"\";\n\t\t\t\tconst contentTypes: Record<string, string> = {\n\t\t\t\t\tjs: \"application/javascript; charset=utf-8\",\n\t\t\t\t\tcss: \"text/css; charset=utf-8\",\n\t\t\t\t\tjson: \"application/json\",\n\t\t\t\t\tpng: \"image/png\",\n\t\t\t\t\tsvg: \"image/svg+xml\",\n\t\t\t\t};\n\t\t\t\treturn new Response(file, {\n\t\t\t\t\theaders: { \"Content-Type\": contentTypes[ext] || \"application/octet-stream\" },\n\t\t\t\t});\n\t\t\t}\n\t\t\treturn new Response(\"Not Found\", { status: 404 });\n\t\t};\n\n\t\t// Register routes directly on router (bypass openApiDocs)\n\t\tthis.router.insert(\"GET\", `${basePath}/swagger.json`, swaggerHandler);\n\t\tthis.router.insert(\"GET\", `${basePath}/index.html`, uiHandler);\n\t\tthis.router.insert(\"GET\", `${basePath}/*`, assetsHandler);\n\t\tthis.router.insert(\"GET\", basePath, uiHandler);\n\t}\n\n\tprivate compileGlobalPipeline(): void {\n\t\tif (this.middlewares.length === 0) {\n\t\t\tthis.compiledGlobalPipeline = (ctx, finalHandler) =>\n\t\t\t\t// biome-ignore lint/suspicious/noExplicitAny: compiled\n\t\t\t\tfinalHandler(ctx as any);\n\t\t\treturn;\n\t\t}\n\n\t\tconst fns = [...this.middlewares];\n\t\tlet code = `return finalHandler(ctx);`;\n\t\tfor (let i = fns.length - 1; i >= 0; i--) {\n\t\t\tcode = `return fns[${i}](ctx, () => { ${code} });`;\n\t\t}\n\n\t\tconst factory = new Function(\n\t\t\t\"fns\",\n\t\t\t`return function(ctx, finalHandler) { ${code} }`,\n\t\t);\n\t\tthis.compiledGlobalPipeline = factory(fns) as (\n\t\t\tctx: Context<DI>,\n\t\t\tfinalHandler: Handler<DI>,\n\t\t) => HandlerReturn;\n\t}\n\n\tprivate compileAOTRouter(): (\n\t\trequest: Request,\n\t\tserver?: Server<WSData<DI>>,\n\t) => Response | Promise<Response> {\n\t\tlet code =\n\t\t\t\"return function(request, server) {\\n\" +\n\t\t\t\"  const url = request.url;\\n\" +\n\t\t\t\"  let start = url.indexOf('/', url.indexOf('//') + 2);\\n\" +\n\t\t\t\"  if (start === -1) start = url.length;\\n\" +\n\t\t\t\"  let end = url.indexOf('?', start);\\n\" +\n\t\t\t\"  if (end === -1) end = url.length;\\n\" +\n\t\t\t\"  const pathname = start === url.length ? '/' : url.substring(start, end);\\n\";\n\n\t\tif (this.wsRoutes.size > 0) {\n\t\t\tcode +=\n\t\t\t\t\"  if (server && wsRoutes.has(pathname)) {\\n\" +\n\t\t\t\t\"    const wsHandler = wsRoutes.get(pathname);\\n\" +\n\t\t\t\t\"    const ctx = new Context(request, EMPTY_PARAMS, di);\\n\" +\n\t\t\t\t\"    const data = { ctx, handler: wsHandler };\\n\" +\n\t\t\t\t\"    const upgraded = server.upgrade(request, { data });\\n\" +\n\t\t\t\t\"    if (upgraded) return undefined;\\n\" +\n\t\t\t\t\"    return new Response('Upgrade Required', { status: 426, headers: { Connection: 'Upgrade', Upgrade: 'websocket' } });\\n\" +\n\t\t\t\t\"  }\\n\";\n\t\t}\n\n\t\tcode += \"  const method = request.method;\\n\" + \"  switch(method) {\\n\";\n\n\t\t// biome-ignore lint/suspicious/noExplicitAny: generic\n\t\tconst handlersList: any[] = [];\n\t\t// biome-ignore lint/suspicious/noExplicitAny: generic\n\t\tconst handlersMap = new Map<any, string>();\n\n\t\t// biome-ignore lint/suspicious/noExplicitAny: generic\n\t\tfunction getHandlerIndex(handler: any) {\n\t\t\tif (handlersMap.has(handler)) return handlersMap.get(handler);\n\t\t\tconst idx = handlersList.length;\n\t\t\thandlersList.push(handler);\n\t\t\thandlersMap.set(handler, `handlers[${idx}]`);\n\t\t\treturn `handlers[${idx}]`;\n\t\t}\n\n\t\t// biome-ignore lint/suspicious/noExplicitAny: generic\n\t\tconst methodPaths = new Map<string, { path: string; handler: any }[]>();\n\t\tfor (const [path, methodMap] of this.router.staticRoutes.entries()) {\n\t\t\tfor (const [method, handler] of methodMap.entries()) {\n\t\t\t\tif (!methodPaths.has(method)) methodPaths.set(method, []);\n\t\t\t\t// biome-ignore lint/style/noNonNullAssertion: guaranteed\n\t\t\t\tmethodPaths.get(method)!.push({ path, handler });\n\t\t\t}\n\t\t}\n\n\t\tfor (const [method, routes] of methodPaths.entries()) {\n\t\t\tcode += `case \"${method}\": {\\n`;\n\t\t\tcode += \"  switch(pathname) {\\n\";\n\t\t\tfor (const route of routes) {\n\t\t\t\tconst handlerRef = getHandlerIndex(route.handler);\n\t\t\t\tcode += `    case \"${route.path}\": {\\n`;\n\t\t\t\tcode += \"      const ctx = new Context(request, EMPTY_PARAMS, di);\\n\";\n\t\t\t\tcode += \"      try {\\n\";\n\t\t\t\tcode +=\n\t\t\t\t\t\"        const raw = compiledGlobalPipeline(ctx, \" +\n\t\t\t\t\thandlerRef +\n\t\t\t\t\t\");\\n\";\n\t\t\t\tif (this._skipLogResponse) {\n\t\t\t\t\tcode += \"        if (raw instanceof Promise) {\\n\";\n\t\t\t\t\tcode +=\n\t\t\t\t\t\t\"          return raw.then((v) => toResponse(v)).catch((e) => handleError(request, pathname, ctx, e));\\n\";\n\t\t\t\t\tcode += \"        }\\n\";\n\t\t\t\t\tcode += \"        return toResponse(raw);\\n\";\n\t\t\t\t} else {\n\t\t\t\t\tcode += \"        if (raw instanceof Promise) {\\n\";\n\t\t\t\t\tcode +=\n\t\t\t\t\t\t\"          return raw.then((v) => logResponse(request, pathname, toResponse(v))).catch((e) => handleError(request, pathname, ctx, e));\\n\";\n\t\t\t\t\tcode += \"        }\\n\";\n\t\t\t\t\tcode += \"        return logResponse(request, pathname, toResponse(raw));\\n\";\n\t\t\t\t}\n\t\t\t\tcode += \"      } catch (err) {\\n\";\n\t\t\t\tcode += \"        return handleError(request, pathname, ctx, err);\\n\";\n\t\t\t\tcode += \"      }\\n\";\n\t\t\t\tcode += \"    }\\n\";\n\t\t\t}\n\t\t\tcode += \"  }\\n\";\n\t\t\tcode += \"  break;\\n\";\n\t\t\tcode += \"}\\n\";\n\t\t}\n\n\t\tcode += \"  }\\n\" + \"  return fallback(request, server);\\n\" + \"};\\n\";\n\n\t\tconst factory = new Function(\n\t\t\t\"Context\",\n\t\t\t\"EMPTY_PARAMS\",\n\t\t\t\"di\",\n\t\t\t\"handlers\",\n\t\t\t\"compiledGlobalPipeline\",\n\t\t\t\"logResponse\",\n\t\t\t\"handleError\",\n\t\t\t\"fallback\",\n\t\t\t\"wsRoutes\",\n\t\t\t\"toResponse\",\n\t\t\tcode,\n\t\t);\n\n\t\tconst EMPTY_PARAMS = Object.freeze({});\n\t\treturn factory(\n\t\t\tContext,\n\t\t\tEMPTY_PARAMS,\n\t\t\tthis.di,\n\t\t\thandlersList,\n\t\t\tthis.compiledGlobalPipeline,\n\t\t\tthis.logResponse,\n\t\t\tthis.handleError,\n\t\t\tthis.fallbackHandleRequest.bind(this),\n\t\t\tthis.wsRoutes,\n\t\t\ttoResponse,\n\t\t);\n\t}\n\n\tprivate extractPathname(url: string): string {\n\t\t// Skip protocol + authority (http://localhost:1212)\n\t\tconst start = url.indexOf(\"/\", url.indexOf(\"//\") + 2);\n\t\tif (start === -1) return \"/\";\n\t\tlet end = url.indexOf(\"?\", start);\n\t\tif (end === -1) end = url.length;\n\t\treturn url.substring(start, end);\n\t}\n\n\t// Bound once per App instance instead of allocating a new closure per request\n\tprivate readonly logResponse = (\n\t\trequest: Request,\n\t\tpathname: string,\n\t\tresponse: Response,\n\t): Response => {\n\t\t// Guard against undefined response from middleware chain\n\t\tif (!response) {\n\t\t\treturn new Response(\"Internal Server Error\", { status: 500 });\n\t\t}\n\t\t// Skip building the log string entirely when request logging is off\n\t\tif (logger.logRequests) {\n\t\t\tconst status = response.status;\n\t\t\tconst logData = { status };\n\t\t\tif (status >= 500) {\n\t\t\t\tlogger.error(`${request.method} ${pathname}`, logData);\n\t\t\t} else if (status >= 400) {\n\t\t\t\tlogger.warn(`${request.method} ${pathname}`, logData);\n\t\t\t} else {\n\t\t\t\tlogger.info(`${request.method} ${pathname}`, logData);\n\t\t\t}\n\t\t}\n\t\tif (this.poweredByHeaderEnabled) {\n\t\t\tresponse.headers.set(\"X-Powered-By\", \"buntok\");\n\t\t}\n\t\treturn response;\n\t};\n\n\t/**\n\t * Normalize flexible handler return (string | object | null etc.) to Response.\n\t * Used by both AOT and fallback paths.\n\t */\n\tprivate normalizeReturn(value: unknown): Response | Promise<Response> {\n\t\tif (value instanceof Promise) {\n\t\t\treturn value.then((v) => toResponse(v));\n\t\t}\n\t\treturn toResponse(value);\n\t}\n\n\t/**\n\t * Normalize handler return then pass through logResponse.\n\t * Handles both sync and async flexible returns.\n\t */\n\tprivate readonly logNormalized = (\n\t\trequest: Request,\n\t\tpathname: string,\n\t\tvalue: unknown,\n\t): Response | Promise<Response> => {\n\t\tconst normalized = this.normalizeReturn(value);\n\t\tif (normalized instanceof Promise) {\n\t\t\treturn normalized.then((res) => this.logResponse(request, pathname, res));\n\t\t}\n\t\treturn this.logResponse(request, pathname, normalized);\n\t};\n\n\tprivate readonly handleError = (\n\t\trequest: Request,\n\t\tpathname: string,\n\t\tctx: Context<DI>,\n\t\terr: unknown,\n\t): Response | Promise<Response> => {\n\t\tconst errorObj = err instanceof Error ? err : new Error(String(err));\n\t\tlogger.error(`${request.method} ${pathname}`, {\n\t\t\terror: errorObj.message,\n\t\t});\n\t\tconst result = this.customErrorHandler(errorObj, ctx);\n\t\t// Normalize ErrorHandler flexible return as well\n\t\tconst normalized = this.normalizeReturn(result);\n\t\tif (normalized instanceof Promise) {\n\t\t\treturn normalized.then((response) => {\n\t\t\t\tif (this.poweredByHeaderEnabled) {\n\t\t\t\t\tresponse.headers.set(\"X-Powered-By\", \"buntok\");\n\t\t\t\t}\n\t\t\t\treturn response;\n\t\t\t});\n\t\t}\n\t\tif (this.poweredByHeaderEnabled) {\n\t\t\tnormalized.headers.set(\"X-Powered-By\", \"buntok\");\n\t\t}\n\t\treturn normalized;\n\t};\n\n\t/**\n\t * Dispatch a request through the app without binding to a real port.\n\t * Meant for tests: `await app.request(\"/users/1\")` is much faster than\n\t * spinning up a server and making a real HTTP call.\n\t *\n\t * Accepts the same input as the global `fetch()` / `Request` constructor.\n\t */\n\tpublic async request(\n\t\tinput: string | Request | URL,\n\t\tinit?: RequestInit,\n\t): Promise<Response> {\n\t\tif (!this.compiledGlobalPipeline) this.compileGlobalPipeline();\n\t\t// Lazy AOT — compile sekali saja (fix speed murah: sebelumnya compile tiap request)\n\t\tif (!this._aotReady) {\n\t\t\tthis._compiledAOTRouter = this.compileAOTRouter();\n\t\t\tthis._aotReady = true;\n\t\t}\n\t\tconst request =\n\t\t\tinput instanceof Request && !init\n\t\t\t\t? input\n\t\t\t\t: new Request(\n\t\t\t\t\t\ttypeof input === \"string\" && !/^https?:\\/\\//.test(input)\n\t\t\t\t\t\t\t? `http://localhost${input.startsWith(\"/\") ? \"\" : \"/\"}${input}`\n\t\t\t\t\t\t\t: // biome-ignore lint/suspicious/noExplicitAny: Standard fetch input overriding\n\t\t\t\t\t\t\t\t(input as any),\n\t\t\t\t\t\tinit,\n\t\t\t\t\t);\n\t\treturn this.handleRequest(request);\n\t}\n\n\tprivate _aotReady = false;\n\tprivate _compiledAOTRouter: (\n\t\trequest: Request,\n\t\tserver?: Server<WSData<DI>>,\n\t) => Response | Promise<Response> = this.fallbackHandleRequest.bind(this);\n\n\tpublic handleRequest(\n\t\trequest: Request,\n\t\tserver?: Server<WSData<DI>>,\n\t): Response | Promise<Response> {\n\t\treturn this._compiledAOTRouter(request, server);\n\t}\n\n\tpublic fallbackHandleRequest(\n\t\trequest: Request,\n\t\tserver?: Server<WSData<DI>>,\n\t): Response | Promise<Response> {\n\t\tconst pathname = this.extractPathname(request.url);\n\n\t\tif (server && this.wsRoutes.size > 0) {\n\t\t\tconst wsHandler = this.wsRoutes.get(pathname);\n\t\t\tif (wsHandler) {\n\t\t\t\tconst ctx = new Context(request, {}, this.di) as Context<DI>;\n\t\t\t\tconst data: WSData<DI> = { ctx, handler: wsHandler };\n\t\t\t\tconst upgraded = server.upgrade(request, { data });\n\t\t\t\tif (upgraded) {\n\t\t\t\t\treturn undefined as unknown as Response;\n\t\t\t\t}\n\t\t\t\treturn new Response(\"Upgrade Required\", {\n\t\t\t\t\tstatus: 426,\n\t\t\t\t\theaders: {\n\t\t\t\t\t\tConnection: \"Upgrade\",\n\t\t\t\t\t\tUpgrade: \"websocket\",\n\t\t\t\t\t},\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\tconst route = this.router.find(request.method, pathname);\n\n\t\tconst ctx = new Context(request, route.params, this.di) as Context<DI>;\n\n\t\tlet finalHandler = route.handler as Handler<DI>;\n\t\tif (!finalHandler) {\n\t\t\tfinalHandler = this.customNotFoundHandler;\n\t\t}\n\n\t\ttry {\n\t\t\t// biome-ignore lint/style/noNonNullAssertion: Guaranteed by compileGlobalPipeline\n\t\t\tconst result = this.compiledGlobalPipeline!(ctx, finalHandler);\n\n\t\t\tif (result instanceof Promise) {\n\t\t\t\treturn result\n\t\t\t\t\t.then((value) => this.logNormalized(request, pathname, value))\n\t\t\t\t\t.catch((err) => this.handleError(request, pathname, ctx, err));\n\t\t\t}\n\t\t\treturn this.logNormalized(request, pathname, result);\n\t\t} catch (err) {\n\t\t\treturn this.handleError(request, pathname, ctx, err);\n\t\t}\n\t}\n\n\tpublic listen(port?: number, callback?: () => void): void {\n\t\t// Skip server startup when running under make:docs\n\t\tif (process.env.BUNTOK_DOCS_BUILD === \"1\") return;\n\t\tif (this.isListening) return;\n\t\tthis.isListening = true;\n\n\t\tconst startTime = performance.now();\n\n\t\t// Pre-compute whether logResponse can be skipped entirely\n\t\tthis._skipLogResponse = !logger.logRequests && !this.poweredByHeaderEnabled;\n\n\t\t// Register API docs routes directly on router (bypasses openApiDocs)\n\t\tif (this._apiDocsConfig) {\n\t\t\tconst isProduction = process.env.NODE_ENV === \"production\";\n\t\t\tif (!(this._apiDocsConfig.safeOnProduction && isProduction)) {\n\t\t\t\tthis.registerApiDocsRoutes();\n\t\t\t}\n\t\t}\n\n\t\t// Perform AOT compilation for global middlewares\n\t\tthis.compileGlobalPipeline();\n\t\tthis._compiledAOTRouter = this.compileAOTRouter();\n\t\tthis._aotReady = true;\n\n\t\tconst finalPort = port || Number(process.env.PORT) || 1212;\n\n\t\t// biome-ignore lint/suspicious/noExplicitAny: Required for bun serve signature compatibility\n\t\tconst serveOptions: any = {\n\t\t\tport: finalPort,\n\t\t\tfetch: this._compiledAOTRouter,\n\t\t};\n\n\t\tif (this._reusePort) {\n\t\t\tserveOptions.reusePort = true;\n\t\t}\n\n\t\tif (this.wsRoutes.size > 0) {\n\t\t\tserveOptions.websocket = {\n\t\t\t\t// Bun-only native options — pluggable, zero-deps\n\t\t\t\tperMessageDeflate: this.wsOpts.perMessageDeflate ?? false,\n\t\t\t\tmaxPayloadLength: this.wsOpts.maxPayloadLength ?? 16 * 1024 * 1024,\n\t\t\t\tidleTimeout: this.wsOpts.idleTimeout ?? 120,\n\t\t\t\tmaxBackpressure: this.wsOpts.maxBackpressure,\n\t\t\t\tpublishToSelf: this.wsOpts.publishToSelf ?? false,\n\t\t\t\topen: async (ws: ServerWebSocket<WSData<DI>>) => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tif (ws.data.handler.authenticate) {\n\t\t\t\t\t\t\tconst authData = await ws.data.handler.authenticate(ws);\n\t\t\t\t\t\t\tif (authData === null) {\n\t\t\t\t\t\t\t\tws.close(4001, \"Unauthorized\");\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t(ws.data as unknown as Record<string, unknown>).auth = authData;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tws.data.handler.open?.(ws);\n\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\tlogger.error(\"WS open error\", { error: (err as Error).message });\n\t\t\t\t\t\ttry { ws.close(1011, \"Internal error\"); } catch {}\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\tmessage: (\n\t\t\t\t\tws: ServerWebSocket<WSData<DI>>,\n\t\t\t\t\tmessage: string | Buffer,\n\t\t\t\t) => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tws.data.handler.message?.(ws, message);\n\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\tlogger.error(\"WS message error\", { error: (err as Error).message });\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\tclose: (\n\t\t\t\t\tws: ServerWebSocket<WSData<DI>>,\n\t\t\t\t\tcode: number,\n\t\t\t\t\treason: string,\n\t\t\t\t) => {\n\t\t\t\t\ttry { ws.data.handler.close?.(ws, code, reason); } catch (err) {\n\t\t\t\t\t\tlogger.error(\"WS close error\", { error: (err as Error).message });\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\tdrain: (ws: ServerWebSocket<WSData<DI>>) => {\n\t\t\t\t\ttry { ws.data.handler.drain?.(ws); } catch {}\n\t\t\t\t},\n\t\t\t\tpong: (ws: ServerWebSocket<WSData<DI>>) => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\t// heartbeat pong — reset alive (fix: pong bukan message)\n\t\t\t\t\t\t(ws.data.handler as unknown as { pong?: (ws: ServerWebSocket<WSData<DI>>) => void }).pong?.(ws);\n\t\t\t\t\t\t// juga fallback untuk wsHeartbeat yang attach di ws.data\n\t\t\t\t\t\tconst hb = (ws.data as unknown as { heartbeat?: { alive: boolean } }).heartbeat;\n\t\t\t\t\t\tif (hb) hb.alive = true;\n\t\t\t\t\t} catch {}\n\t\t\t\t},\n\t\t\t} as unknown as Record<string, unknown>;\n\t\t}\n\n\t\t// Auto-increment port if already in use\n\t\tlet currentPort = finalPort;\n\t\tconst maxRetries = 10;\n\t\tfor (let attempt = 0; attempt < maxRetries; attempt++) {\n\t\t\ttry {\n\t\t\t\tserveOptions.port = currentPort;\n\t\t\t\tthis.server = Bun.serve<WSData<DI>>(serveOptions);\n\t\t\t\tbreak;\n\t\t\t} catch (err: any) {\n\t\t\t\tif (err?.code === \"EADDRINUSE\" && attempt < maxRetries - 1) {\n\t\t\t\t\tconst originalPort = currentPort;\n\t\t\t\t\tcurrentPort++;\n\t\t\t\t\tlogger.warn(\n\t\t\t\t\t\t`⚠ Port ${originalPort} is already in use, using port ${currentPort} instead`,\n\t\t\t\t\t);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tthrow err;\n\t\t\t}\n\t\t}\n\n\t\t// Setup graceful shutdown handlers\n\t\tthis.setupGracefulShutdown();\n\n\t\tconst startupTime = Math.round(performance.now() - startTime);\n\t\tconst env = process.env.NODE_ENV || \"development\";\n\t\tconsole.log(\n\t\t\t`\\n  \\x1b[36mBuntok v${VERSION}\\x1b[0m ready in \\x1b[32m${startupTime}ms\\x1b[0m\\n`,\n\t\t);\n\t\tconsole.log(`  Environment: \\x1b[33m${env}\\x1b[0m`);\n\t\tconsole.log(`  Listening on: \\x1b[36mhttp://localhost:${currentPort}\\x1b[0m\\n`);\n\n\t\tif (callback) callback();\n\t}\n\n\t/**\n\t * Setup graceful shutdown handlers for SIGTERM and SIGINT signals.\n\t * When a signal is received, the server stops accepting new connections,\n\t * waits for in-flight requests to complete, then exits cleanly.\n\t */\n\tprivate setupGracefulShutdown(): void {\n\t\tconst shutdown = async (signal: string) => {\n\t\t\tlogger.info(`\\n${signal} received. Starting graceful shutdown...`);\n\n\t\t\tif (this.server) {\n\t\t\t\t// Stop accepting new connections\n\t\t\t\tthis.server.stop(false);\n\n\t\t\t\t// Drain SSE — industrial graceful (close all streams dengan 30s heartbeat comment)\n\t\t\t\ttry {\n\t\t\t\t\tconst { SSE } = await import(\"./sse\");\n\t\t\t\t\t// kirim close event sebelum terminate\n\t\t\t\t\tfor (const c of SSE.getActiveConnections()) {\n\t\t\t\t\t\ttry { c.sendEvent(\"close\", \"Server shutting down\"); } catch {}\n\t\t\t\t\t}\n\t\t\t\t\t// beri waktu 1s untuk flush\n\t\t\t\t\tawait new Promise((r) => setTimeout(r, 1000));\n\t\t\t\t\tSSE.closeAll();\n\t\t\t\t} catch {}\n\n\t\t\t\t// Drain WS — close dengan 1001 Going Away\n\t\t\t\ttry {\n\t\t\t\t\t// broadcast ke semua topic jika pakai publish\n\t\t\t\t\ttry { this.server.publish(\"buntok:shutdown\", \"shutting down\"); } catch {}\n\t\t\t\t} catch {}\n\n\t\t\t\t// Give in-flight requests time to complete (max 30 seconds)\n\t\t\t\tconst shutdownTimeout = 30_000;\n\t\t\t\tconst forceExitTimeout = setTimeout(() => {\n\t\t\t\t\tlogger.warn(\"Forcing shutdown after timeout\");\n\t\t\t\t\tprocess.exit(1);\n\t\t\t\t}, shutdownTimeout);\n\t\t\t\t// Jangan clear langsung — biarkan timeout jadi safety net\n\t\t\t\t// Cleanup setelah graceful selesai\n\t\t\t\tsetTimeout(() => {\n\t\t\t\t\tclearTimeout(forceExitTimeout);\n\t\t\t\t\tlogger.info(\"Server shut down gracefully\");\n\t\t\t\t\tprocess.exit(0);\n\t\t\t\t}, 1500);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tlogger.info(\"Server shut down gracefully\");\n\t\t\tprocess.exit(0);\n\t\t};\n\n\t\tprocess.on(\"SIGTERM\", () => shutdown(\"SIGTERM\"));\n\t\tprocess.on(\"SIGINT\", () => shutdown(\"SIGINT\"));\n\t}\n}\n\nexport class RouterGroup<\n\tDI extends Record<string, unknown> = Record<string, unknown>,\n> {\n\tprivate prefix: string;\n\tprivate app: App<DI>;\n\tprivate groupMiddlewares: Middleware<DI>[] = [];\n\n\tconstructor(prefix: string, app: App<DI>) {\n\t\tthis.prefix = prefix.endsWith(\"/\") ? prefix.slice(0, -1) : prefix;\n\t\tthis.app = app;\n\t}\n\n\tpublic use(middleware: Middleware<DI>): this {\n\t\tthis.groupMiddlewares.push(middleware);\n\t\treturn this;\n\t}\n\n\tpublic group(prefix: string): RouterGroup<DI> {\n\t\tconst newGroup = new RouterGroup<DI>(this.prefix + prefix, this.app);\n\t\tnewGroup.groupMiddlewares = [...this.groupMiddlewares];\n\t\treturn newGroup;\n\t}\n\n\tprivate normalizePath(path: string): string {\n\t\tconst cleanPath = path === \"/\" ? \"\" : path;\n\t\treturn `${this.prefix}${cleanPath}`;\n\t}\n\n\tpublic get<Path extends string>(path: Path, handler: Handler<DI, Path>): this;\n\tpublic get<Path extends string>(\n\t\tpath: Path,\n\t\tmiddleware: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic get<Path extends string>(\n\t\tpath: Path,\n\t\tm1: Middleware<DI, Path>,\n\t\tm2: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic get<Path extends string>(\n\t\tpath: Path,\n\t\tm1: Middleware<DI, Path>,\n\t\tm2: Middleware<DI, Path>,\n\t\tm3: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic get<Path extends string>(\n\t\tpath: Path,\n\t\tm1: Middleware<DI, Path>,\n\t\tm2: Middleware<DI, Path>,\n\t\tm3: Middleware<DI, Path>,\n\t\tm4: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic get<Path extends string>(\n\t\tpath: Path,\n\t\tm1: Middleware<DI, Path>,\n\t\tm2: Middleware<DI, Path>,\n\t\tm3: Middleware<DI, Path>,\n\t\tm4: Middleware<DI, Path>,\n\t\tm5: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic get<Path extends string>(\n\t\tpath: Path,\n\t\t...handlers: Array<Middleware<DI, Path> | Handler<DI, Path>>\n\t): this {\n\t\tthis.app.registerRoute(\"GET\", this.normalizePath(path), [\n\t\t\t...this.groupMiddlewares,\n\t\t\t...handlers,\n\t\t]);\n\t\treturn this;\n\t}\n\n\tpublic post<Path extends string>(\n\t\tpath: Path,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic post<Path extends string>(\n\t\tpath: Path,\n\t\tmiddleware: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic post<Path extends string>(\n\t\tpath: Path,\n\t\tm1: Middleware<DI, Path>,\n\t\tm2: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic post<Path extends string>(\n\t\tpath: Path,\n\t\tm1: Middleware<DI, Path>,\n\t\tm2: Middleware<DI, Path>,\n\t\tm3: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic post<Path extends string>(\n\t\tpath: Path,\n\t\tm1: Middleware<DI, Path>,\n\t\tm2: Middleware<DI, Path>,\n\t\tm3: Middleware<DI, Path>,\n\t\tm4: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic post<Path extends string>(\n\t\tpath: Path,\n\t\tm1: Middleware<DI, Path>,\n\t\tm2: Middleware<DI, Path>,\n\t\tm3: Middleware<DI, Path>,\n\t\tm4: Middleware<DI, Path>,\n\t\tm5: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic post<Path extends string>(\n\t\tpath: Path,\n\t\t...handlers: Array<Middleware<DI, Path> | Handler<DI, Path>>\n\t): this {\n\t\tthis.app.registerRoute(\"POST\", this.normalizePath(path), [\n\t\t\t...this.groupMiddlewares,\n\t\t\t...handlers,\n\t\t]);\n\t\treturn this;\n\t}\n\n\tpublic put<Path extends string>(path: Path, handler: Handler<DI, Path>): this;\n\tpublic put<Path extends string>(\n\t\tpath: Path,\n\t\tmiddleware: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic put<Path extends string>(\n\t\tpath: Path,\n\t\tm1: Middleware<DI, Path>,\n\t\tm2: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic put<Path extends string>(\n\t\tpath: Path,\n\t\tm1: Middleware<DI, Path>,\n\t\tm2: Middleware<DI, Path>,\n\t\tm3: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic put<Path extends string>(\n\t\tpath: Path,\n\t\tm1: Middleware<DI, Path>,\n\t\tm2: Middleware<DI, Path>,\n\t\tm3: Middleware<DI, Path>,\n\t\tm4: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic put<Path extends string>(\n\t\tpath: Path,\n\t\tm1: Middleware<DI, Path>,\n\t\tm2: Middleware<DI, Path>,\n\t\tm3: Middleware<DI, Path>,\n\t\tm4: Middleware<DI, Path>,\n\t\tm5: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic put<Path extends string>(\n\t\tpath: Path,\n\t\t...handlers: Array<Middleware<DI, Path> | Handler<DI, Path>>\n\t): this {\n\t\tthis.app.registerRoute(\"PUT\", this.normalizePath(path), [\n\t\t\t...this.groupMiddlewares,\n\t\t\t...handlers,\n\t\t]);\n\t\treturn this;\n\t}\n\n\tpublic delete<Path extends string>(\n\t\tpath: Path,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic delete<Path extends string>(\n\t\tpath: Path,\n\t\tmiddleware: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic delete<Path extends string>(\n\t\tpath: Path,\n\t\tm1: Middleware<DI, Path>,\n\t\tm2: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic delete<Path extends string>(\n\t\tpath: Path,\n\t\tm1: Middleware<DI, Path>,\n\t\tm2: Middleware<DI, Path>,\n\t\tm3: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic delete<Path extends string>(\n\t\tpath: Path,\n\t\tm1: Middleware<DI, Path>,\n\t\tm2: Middleware<DI, Path>,\n\t\tm3: Middleware<DI, Path>,\n\t\tm4: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic delete<Path extends string>(\n\t\tpath: Path,\n\t\tm1: Middleware<DI, Path>,\n\t\tm2: Middleware<DI, Path>,\n\t\tm3: Middleware<DI, Path>,\n\t\tm4: Middleware<DI, Path>,\n\t\tm5: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic delete<Path extends string>(\n\t\tpath: Path,\n\t\t...handlers: Array<Middleware<DI, Path> | Handler<DI, Path>>\n\t): this {\n\t\tthis.app.registerRoute(\"DELETE\", this.normalizePath(path), [\n\t\t\t...this.groupMiddlewares,\n\t\t\t...handlers,\n\t\t]);\n\t\treturn this;\n\t}\n\n\t// QUERY method - like GET but with body support\n\tpublic query(path: string, handler: Handler<DI>): this;\n\tpublic query(\n\t\tpath: string,\n\t\tmiddleware: Middleware<DI>,\n\t\thandler: Handler<DI>,\n\t): this;\n\tpublic query(\n\t\tpath: string,\n\t\tm1: Middleware<DI>,\n\t\tm2: Middleware<DI>,\n\t\thandler: Handler<DI>,\n\t): this;\n\tpublic query(\n\t\tpath: string,\n\t\tm1: Middleware<DI>,\n\t\tm2: Middleware<DI>,\n\t\tm3: Middleware<DI>,\n\t\thandler: Handler<DI>,\n\t): this;\n\tpublic query(\n\t\tpath: string,\n\t\tm1: Middleware<DI>,\n\t\tm2: Middleware<DI>,\n\t\tm3: Middleware<DI>,\n\t\tm4: Middleware<DI>,\n\t\thandler: Handler<DI>,\n\t): this;\n\tpublic query(\n\t\tpath: string,\n\t\tm1: Middleware<DI>,\n\t\tm2: Middleware<DI>,\n\t\tm3: Middleware<DI>,\n\t\tm4: Middleware<DI>,\n\t\tm5: Middleware<DI>,\n\t\thandler: Handler<DI>,\n\t): this;\n\tpublic query(\n\t\tpath: string,\n\t\t...handlers: Array<Middleware<DI> | Handler<DI>>\n\t): this {\n\t\tthis.app.registerRoute(\"QUERY\", this.normalizePath(path), [\n\t\t\t...this.groupMiddlewares,\n\t\t\t...handlers,\n\t\t]);\n\t\treturn this;\n\t}\n\n\tpublic options<Path extends string>(\n\t\tpath: Path,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic options<Path extends string>(\n\t\tpath: Path,\n\t\tmiddleware: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic options<Path extends string>(\n\t\tpath: Path,\n\t\tm1: Middleware<DI, Path>,\n\t\tm2: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic options<Path extends string>(\n\t\tpath: Path,\n\t\tm1: Middleware<DI, Path>,\n\t\tm2: Middleware<DI, Path>,\n\t\tm3: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic options<Path extends string>(\n\t\tpath: Path,\n\t\tm1: Middleware<DI, Path>,\n\t\tm2: Middleware<DI, Path>,\n\t\tm3: Middleware<DI, Path>,\n\t\tm4: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic options<Path extends string>(\n\t\tpath: Path,\n\t\tm1: Middleware<DI, Path>,\n\t\tm2: Middleware<DI, Path>,\n\t\tm3: Middleware<DI, Path>,\n\t\tm4: Middleware<DI, Path>,\n\t\tm5: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic options<Path extends string>(\n\t\tpath: Path,\n\t\t...handlers: Array<Middleware<DI, Path> | Handler<DI, Path>>\n\t): this {\n\t\tthis.app.registerRoute(\"OPTIONS\", this.normalizePath(path), [\n\t\t\t...this.groupMiddlewares,\n\t\t\t...handlers,\n\t\t]);\n\t\treturn this;\n\t}\n\n\tpublic patch<Path extends string>(\n\t\tpath: Path,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic patch<Path extends string>(\n\t\tpath: Path,\n\t\tmiddleware: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic patch<Path extends string>(\n\t\tpath: Path,\n\t\tm1: Middleware<DI, Path>,\n\t\tm2: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic patch<Path extends string>(\n\t\tpath: Path,\n\t\tm1: Middleware<DI, Path>,\n\t\tm2: Middleware<DI, Path>,\n\t\tm3: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic patch<Path extends string>(\n\t\tpath: Path,\n\t\tm1: Middleware<DI, Path>,\n\t\tm2: Middleware<DI, Path>,\n\t\tm3: Middleware<DI, Path>,\n\t\tm4: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic patch<Path extends string>(\n\t\tpath: Path,\n\t\tm1: Middleware<DI, Path>,\n\t\tm2: Middleware<DI, Path>,\n\t\tm3: Middleware<DI, Path>,\n\t\tm4: Middleware<DI, Path>,\n\t\tm5: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic patch<Path extends string>(\n\t\tpath: Path,\n\t\t...handlers: Array<Middleware<DI, Path> | Handler<DI, Path>>\n\t): this {\n\t\tthis.app.registerRoute(\"PATCH\", this.normalizePath(path), [\n\t\t\t...this.groupMiddlewares,\n\t\t\t...handlers,\n\t\t]);\n\t\treturn this;\n\t}\n\n\tpublic head<Path extends string>(\n\t\tpath: Path,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic head<Path extends string>(\n\t\tpath: Path,\n\t\tmiddleware: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic head<Path extends string>(\n\t\tpath: Path,\n\t\tm1: Middleware<DI, Path>,\n\t\tm2: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic head<Path extends string>(\n\t\tpath: Path,\n\t\tm1: Middleware<DI, Path>,\n\t\tm2: Middleware<DI, Path>,\n\t\tm3: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic head<Path extends string>(\n\t\tpath: Path,\n\t\tm1: Middleware<DI, Path>,\n\t\tm2: Middleware<DI, Path>,\n\t\tm3: Middleware<DI, Path>,\n\t\tm4: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic head<Path extends string>(\n\t\tpath: Path,\n\t\tm1: Middleware<DI, Path>,\n\t\tm2: Middleware<DI, Path>,\n\t\tm3: Middleware<DI, Path>,\n\t\tm4: Middleware<DI, Path>,\n\t\tm5: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic head<Path extends string>(\n\t\tpath: Path,\n\t\t...handlers: Array<Middleware<DI, Path> | Handler<DI, Path>>\n\t): this {\n\t\tthis.app.registerRoute(\"HEAD\", this.normalizePath(path), [\n\t\t\t...this.groupMiddlewares,\n\t\t\t...handlers,\n\t\t]);\n\t\treturn this;\n\t}\n\n\t/**\n\t * Register a handler for all standard HTTP methods (GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS).\n\t */\n\tpublic all<Path extends string>(path: Path, handler: Handler<DI, Path>): this;\n\tpublic all<Path extends string>(\n\t\tpath: Path,\n\t\tmiddleware: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic all<Path extends string>(\n\t\tpath: Path,\n\t\tm1: Middleware<DI, Path>,\n\t\tm2: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic all<Path extends string>(\n\t\tpath: Path,\n\t\tm1: Middleware<DI, Path>,\n\t\tm2: Middleware<DI, Path>,\n\t\tm3: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic all<Path extends string>(\n\t\tpath: Path,\n\t\tm1: Middleware<DI, Path>,\n\t\tm2: Middleware<DI, Path>,\n\t\tm3: Middleware<DI, Path>,\n\t\tm4: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic all<Path extends string>(\n\t\tpath: Path,\n\t\tm1: Middleware<DI, Path>,\n\t\tm2: Middleware<DI, Path>,\n\t\tm3: Middleware<DI, Path>,\n\t\tm4: Middleware<DI, Path>,\n\t\tm5: Middleware<DI, Path>,\n\t\thandler: Handler<DI, Path>,\n\t): this;\n\tpublic all<Path extends string>(\n\t\tpath: Path,\n\t\t...handlers: Array<Middleware<DI, Path> | Handler<DI, Path>>\n\t): this {\n\t\tconst methods = [\n\t\t\t\"GET\",\n\t\t\t\"HEAD\",\n\t\t\t\"POST\",\n\t\t\t\"PUT\",\n\t\t\t\"PATCH\",\n\t\t\t\"DELETE\",\n\t\t\t\"OPTIONS\",\n\t\t];\n\t\tfor (const method of methods) {\n\t\t\tthis.app.registerRoute(method, this.normalizePath(path), [\n\t\t\t\t...this.groupMiddlewares,\n\t\t\t\t...handlers,\n\t\t\t]);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * Register a WebSocket endpoint within this group.\n\t * Path is prefixed with the group prefix.\n\t */\n\tpublic ws(path: string, handler: WSHandler<DI>): this {\n\t\tthis.app.ws(this.normalizePath(path), handler);\n\t\treturn this;\n\t}\n\n\t/**\n\t * Serve static files within this group.\n\t * Route path is prefixed with the group prefix.\n\t */\n\tpublic static(routePath: string, directory: string): this {\n\t\tthis.app.static(this.normalizePath(routePath), directory);\n\t\treturn this;\n\t}\n\n\t/**\n\t * Register a decorator-based controller within this group.\n\t * The controller's prefix is combined with the group prefix.\n\t *\n\t * @example\n\t * const api = app.group(\"/api/v1\");\n\t * api.registerController(UserController);\n\t * // → routes registered as /api/v1/users, /api/v1/users/:id, etc.\n\t */\n\tpublic registerController<T extends object>(\n\t\t// biome-ignore lint/suspicious/noExplicitAny: Constructor args are unknowable\n\t\ttarget: (new (...args: any[]) => T) | T,\n\t): this {\n\t\tconst isInstance =\n\t\t\ttypeof target === \"object\" &&\n\t\t\ttarget !== null &&\n\t\t\ttypeof target === \"object\";\n\n\t\tconst ControllerClass = isInstance\n\t\t\t? // biome-ignore lint/suspicious/noExplicitAny: dynamic type narrowing\n\t\t\t\t(target as any).constructor\n\t\t\t: // biome-ignore lint/suspicious/noExplicitAny: dynamic type narrowing\n\t\t\t\t(target as new (\n\t\t\t\t\t...args: any[]\n\t\t\t\t) => T);\n\n\t\tconst meta = getControllerMeta(ControllerClass);\n\t\tif (!meta) {\n\t\t\tthrow new Error(\n\t\t\t\t`${ControllerClass.name} is not decorated with @Controller — did you forget to add it?`,\n\t\t\t);\n\t\t}\n\n\t\tlet instance: T;\n\n\t\tif (isInstance) {\n\t\t\tinstance = target as T;\n\t\t} else {\n\t\t\tconst container = this.app.getContainer();\n\t\t\tif (container) {\n\t\t\t\tinstance = container.resolve<T>(ControllerClass);\n\t\t\t} else {\n\t\t\t\tinstance = new ControllerClass();\n\t\t\t}\n\t\t}\n\n\t\tconst normalizedPrefix = meta.prefix.endsWith(\"/\")\n\t\t\t? meta.prefix.slice(0, -1)\n\t\t\t: meta.prefix;\n\n\t\tfor (const route of meta.routes) {\n\t\t\tconst cleanPath = route.path === \"/\" ? \"\" : route.path;\n\t\t\tconst fullPath =\n\t\t\t\t`${this.normalizePath(normalizedPrefix)}${cleanPath}` || \"/\";\n\t\t\t// biome-ignore lint/suspicious/noExplicitAny: dynamic method dispatch by decorated property key\n\t\t\tconst handler = (instance as any)[route.propertyKey].bind(\n\t\t\t\tinstance,\n\t\t\t) as Handler<DI>;\n\n\t\t\tthis.app.registerRoute(route.method, fullPath, [\n\t\t\t\t...(route.middlewares as Middleware<DI>[]),\n\t\t\t\t...this.groupMiddlewares,\n\t\t\t\thandler,\n\t\t\t]);\n\t\t}\n\n\t\treturn this;\n\t}\n}\n","export type Scope = \"singleton\" | \"transient\";\n\n// biome-ignore lint/complexity/noBannedTypes: Token can be string or class constructor\ntype Token = string | Function;\n\n// biome-ignore lint/suspicious/noExplicitAny: Generic provider defaults require any\nexport interface ClassProvider<T = any> {\n\tuseClass: new (\n\t\t// biome-ignore lint/suspicious/noExplicitAny: Constructor args are unknowable\n\t\t...args: any[]\n\t) => T;\n\tscope?: Scope;\n}\n\n// biome-ignore lint/suspicious/noExplicitAny: Generic provider defaults require any\nexport interface ValueProvider<T = any> {\n\tuseValue: T;\n}\n\n// biome-ignore lint/suspicious/noExplicitAny: Generic provider defaults require any\nexport interface FactoryProvider<T = any> {\n\tuseFactory: (container: Container) => T;\n\tscope?: Scope;\n}\n\n// biome-ignore lint/suspicious/noExplicitAny: Generic provider defaults require any\nexport type Provider<T = any> =\n\t| ClassProvider<T>\n\t| ValueProvider<T>\n\t| FactoryProvider<T>;\n\nfunction isClassProvider(p: Provider): p is ClassProvider {\n\treturn \"useClass\" in p;\n}\n\nfunction isValueProvider(p: Provider): p is ValueProvider {\n\treturn \"useValue\" in p;\n}\n\nfunction isFactoryProvider(p: Provider): p is FactoryProvider {\n\treturn \"useFactory\" in p;\n}\n\nexport class Container {\n\t// biome-ignore lint/suspicious/noExplicitAny: Internal provider storage\n\tprivate providers = new Map<Token, Provider<any>>();\n\t// biome-ignore lint/suspicious/noExplicitAny: Cached singleton instances\n\tprivate singletons = new Map<Token, any>();\n\tprivate resolving = new Set<Token>();\n\n\t/**\n\t * Register a provider for a given token (class or string key).\n\t */\n\tregister<T>(token: Token, provider: Provider<T>): this {\n\t\tthis.providers.set(token, provider);\n\t\treturn this;\n\t}\n\n\t/**\n\t * Auto-register a class provider by its own constructor as the token.\n\t */\n\tregisterClass<T>(\n\t\tcls: new (\n\t\t\t// biome-ignore lint/suspicious/noExplicitAny: Constructor args are unknowable\n\t\t\t...args: any[]\n\t\t) => T,\n\t\tscope: Scope = \"singleton\",\n\t): this {\n\t\tthis.providers.set(cls, { useClass: cls, scope });\n\t\treturn this;\n\t}\n\n\t/**\n\t * Resolve a dependency by token. Returns cached instance for singletons.\n\t */\n\tresolve<T>(token: Token): T {\n\t\tconst cached = this.singletons.get(token);\n\t\tif (cached !== undefined) return cached as T;\n\n\t\tif (this.resolving.has(token)) {\n\t\t\tconst name = typeof token === \"function\" ? token.name : String(token);\n\t\t\tconst chain = [...this.resolving].map((t) =>\n\t\t\t\ttypeof t === \"function\" ? t.name : String(t),\n\t\t\t);\n\t\t\tthrow new Error(\n\t\t\t\t`Circular dependency detected: ${name}\\n` +\n\t\t\t\t\t`  Resolution chain: ${chain.join(\" -> \")} -> ${name}\\n` +\n\t\t\t\t\t`  Hint: Use FactoryProvider to break the cycle.`,\n\t\t\t);\n\t\t}\n\n\t\tconst provider = this.providers.get(token);\n\t\tif (!provider) {\n\t\t\tconst name = typeof token === \"function\" ? token.name : String(token);\n\t\t\tthrow new Error(\n\t\t\t\t`No provider registered for: ${name}\\n` +\n\t\t\t\t\t`  Hint: Register it with container.registerClass(${name}) or container.register(\"${name}\", { useClass: ${name} })`,\n\t\t\t);\n\t\t}\n\n\t\tthis.resolving.add(token);\n\t\t// biome-ignore lint/suspicious/noExplicitAny: Dynamic instantiation\n\t\tlet instance: any;\n\t\tlet scope: Scope = \"singleton\";\n\n\t\tif (isClassProvider(provider)) {\n\t\t\tscope = provider.scope ?? \"singleton\";\n\t\t\tinstance = new provider.useClass();\n\t\t} else if (isValueProvider(provider)) {\n\t\t\tscope = \"singleton\";\n\t\t\tinstance = provider.useValue;\n\t\t} else if (isFactoryProvider(provider)) {\n\t\t\tscope = provider.scope ?? \"singleton\";\n\t\t\tinstance = provider.useFactory(this);\n\t\t}\n\n\t\tthis.resolving.delete(token);\n\n\t\tif (scope !== \"transient\") {\n\t\t\tthis.singletons.set(token, instance);\n\t\t}\n\n\t\treturn instance as T;\n\t}\n\n\t/**\n\t * Resolve and return an instance, or undefined if not registered.\n\t */\n\tget<T>(token: Token): T | undefined {\n\t\tif (this.singletons.has(token)) return this.singletons.get(token) as T;\n\t\tif (this.providers.has(token)) return this.resolve(token);\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Check whether a token has been registered.\n\t */\n\thas(token: Token): boolean {\n\t\treturn this.providers.has(token);\n\t}\n\n\t/**\n\t * Check whether a token has been resolved and cached.\n\t */\n\thasResolved(token: Token): boolean {\n\t\treturn this.singletons.has(token);\n\t}\n\n\t/**\n\t * Force-clear all providers and cached instances.\n\t * Useful in tests.\n\t */\n\tclear(): void {\n\t\tthis.providers.clear();\n\t\tthis.singletons.clear();\n\t\tthis.resolving.clear();\n\t}\n}\n","import { createSSE, type SSE, type SSEOptions } from \"./sse\";\n\nexport class Context<\n\tDI = Record<string, unknown>,\n\tParams = Record<string, string>,\n> {\n\tpublic request: Request;\n\tpublic params: Params & Record<string, string>;\n\tpublic user?: Record<string, unknown>;\n\n\tprivate _store: Record<string, unknown> | undefined;\n\tprivate _cookies: Record<string, string> | undefined;\n\tprivate _query: Record<string, string> | undefined;\n\tprivate _validated: Record<string, unknown> | undefined;\n\tpublic readonly di: DI;\n\tpublic _afterHooks?: Array<(res: Response) => Response | undefined>;\n\n\tconstructor(request: Request, params: Record<string, string>, di: DI) {\n\t\tthis.request = request;\n\t\tthis.params = params as unknown as Params & Record<string, string>;\n\t\tthis.di = di;\n\t}\n\n\tpublic get store(): Record<string, unknown> {\n\t\tif (!this._store) {\n\t\t\tthis._store = {};\n\t\t}\n\t\treturn this._store;\n\t}\n\n\tpublic set store(value: Record<string, unknown>) {\n\t\tthis._store = value;\n\t}\n\n\tpublic get ip(): string {\n\t\tconst forwardedFor = this.request.headers.get(\"x-forwarded-for\");\n\t\tif (forwardedFor) {\n\t\t\treturn forwardedFor.split(\",\")[0]?.trim() ?? \"\";\n\t\t}\n\t\treturn \"127.0.0.1\";\n\t}\n\n\tprivate parseCookies(): Record<string, string> {\n\t\tif (this._cookies) return this._cookies;\n\n\t\tconst nativeCookies = (this.request as { cookies?: unknown }).cookies;\n\t\tif (nativeCookies && typeof nativeCookies === \"object\") {\n\t\t\tconst map = nativeCookies as Iterable<[string, string]>;\n\t\t\tconst result: Record<string, string> = {};\n\t\t\tfor (const [key, value] of map) {\n\t\t\t\tresult[key] = value;\n\t\t\t}\n\t\t\tthis._cookies = result;\n\t\t\treturn this._cookies;\n\t\t}\n\n\t\tconst cookieHeader = this.request.headers.get(\"Cookie\");\n\t\tif (!cookieHeader) {\n\t\t\tthis._cookies = {};\n\t\t\treturn this._cookies;\n\t\t}\n\n\t\tconst result: Record<string, string> = {};\n\t\tlet start = 0;\n\t\twhile (start < cookieHeader.length) {\n\t\t\tlet end = cookieHeader.indexOf(\";\", start);\n\t\t\tif (end === -1) end = cookieHeader.length;\n\n\t\t\tconst eqIdx = cookieHeader.indexOf(\"=\", start);\n\t\t\tif (eqIdx !== -1 && eqIdx < end) {\n\t\t\t\tlet keyStart = start;\n\t\t\t\twhile (keyStart < eqIdx && cookieHeader[keyStart] === \" \") keyStart++;\n\n\t\t\t\tlet valStart = eqIdx + 1;\n\t\t\t\twhile (valStart < end && cookieHeader[valStart] === \" \") valStart++;\n\n\t\t\t\tconst key = cookieHeader.substring(keyStart, eqIdx);\n\t\t\t\tconst val = cookieHeader.substring(valStart, end);\n\t\t\t\tif (key && val) result[key] = decodeURIComponent(val);\n\t\t\t}\n\t\t\tstart = end + 1;\n\t\t}\n\n\t\tthis._cookies = result;\n\t\treturn this._cookies;\n\t}\n\n\t/**\n\t * Get a cookie value from request\n\t */\n\tpublic getCookie(name: string): string | undefined {\n\t\tconst nativeCookies = (this.request as { cookies?: unknown }).cookies;\n\t\tif (\n\t\t\t!this._cookies &&\n\t\t\tnativeCookies &&\n\t\t\ttypeof (nativeCookies as { get?: unknown }).get === \"function\"\n\t\t) {\n\t\t\treturn (\n\t\t\t\t(nativeCookies as { get: (name: string) => string | null }).get(name) ??\n\t\t\t\tundefined\n\t\t\t);\n\t\t}\n\t\treturn this.parseCookies()[name];\n\t}\n\n\t/**\n\t * Get all cookies from request\n\t */\n\tpublic getCookies(): Record<string, string> {\n\t\treturn this.parseCookies();\n\t}\n\n\t/**\n\t * Parsed query string parameters - accessed as a plain property.\n\t * Parsed lazily and cached after first access.\n\t *\n\t * @example\n\t * // GET /search?q=buntok&limit=10\n\t * const { q, limit } = ctx.query;\n\t */\n\tpublic get query(): Record<string, string> {\n\t\tif (this._query) return this._query;\n\n\t\tthis._query = {};\n\t\tconst url = this.request.url;\n\t\tconst queryIdx = url.indexOf(\"?\");\n\t\tif (queryIdx !== -1) {\n\t\t\tconst queryStr = url.substring(queryIdx + 1);\n\t\t\tif (queryStr) {\n\t\t\t\tconst pairs = queryStr.split(\"&\");\n\t\t\t\tfor (let i = 0; i < pairs.length; i++) {\n\t\t\t\t\tconst pair = pairs[i] as string;\n\t\t\t\t\tif (!pair) continue;\n\t\t\t\t\tconst eqIdx = pair.indexOf(\"=\");\n\t\t\t\t\tif (eqIdx !== -1) {\n\t\t\t\t\t\tconst key = decodeURIComponent(\n\t\t\t\t\t\t\tpair.substring(0, eqIdx).replace(/\\+/g, \" \"),\n\t\t\t\t\t\t);\n\t\t\t\t\t\tconst value = decodeURIComponent(\n\t\t\t\t\t\t\tpair.substring(eqIdx + 1).replace(/\\+/g, \" \"),\n\t\t\t\t\t\t);\n\t\t\t\t\t\tthis._query[key] = value;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tconst key = decodeURIComponent(pair.replace(/\\+/g, \" \"));\n\t\t\t\t\t\tthis._query[key] = \"\";\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn this._query;\n\t}\n\n\t/**\n\t * Internal: used by validator middleware to stash parsed/validated data.\n\t * Not intended to be called directly from route handlers.\n\t */\n\tpublic setValidated(\n\t\ttarget: \"body\" | \"query\" | \"params\",\n\t\tdata: unknown,\n\t): void {\n\t\tif (!this._validated) this._validated = {};\n\t\tthis._validated[target] = data;\n\t}\n\n\t/**\n\t * Read data previously validated by `zValidator()` for the given target,\n\t * typed as `T`. Throws if no validator ran for that target, so a typo'd\n\t * target (or forgetting to add the middleware) fails loudly instead of\n\t * silently returning `undefined`.\n\t */\n\tpublic valid<T>(target: \"body\" | \"query\" | \"params\"): T {\n\t\tif (!this._validated || !(target in this._validated)) {\n\t\t\tthrow new Error(\n\t\t\t\t`ctx.valid(\"${target}\") called but no zValidator(\"${target}\", ...) middleware ran for this route`,\n\t\t\t);\n\t\t}\n\t\treturn this._validated[target] as T;\n\t}\n\n\tprivate _body: unknown | undefined;\n\n\tpublic async body<T>(): Promise<T> {\n\t\tif (this._body !== undefined) {\n\t\t\treturn this._body as T;\n\t\t}\n\t\tthis._body = await this.request.json();\n\t\treturn this._body as T;\n\t}\n\n\t// biome-ignore lint/suspicious/noExplicitAny: Undici types conflict with lib.dom FormData\n\tprivate _formData: any | undefined;\n\n\t/**\n\t * Parse and cache the multipart/form-data body.\n\t * Safe to call multiple times - the parsed FormData is cached after the first read,\n\t * so `zValidator(\"form\", ...)` and `uploader()` can both be used in the same route\n\t * without conflicting over the body stream.\n\t */\n\t// biome-ignore lint/suspicious/noExplicitAny: Undici types conflict with lib.dom FormData\n\tpublic async formData(): Promise<any> {\n\t\tif (this._formData !== undefined) {\n\t\t\treturn this._formData;\n\t\t}\n\t\tthis._formData = await this.request.formData();\n\t\treturn this._formData;\n\t}\n\n\tpublic json(data: unknown, status = 200): Response {\n\t\tif (status === 200) {\n\t\t\treturn Response.json(data);\n\t\t}\n\t\treturn Response.json(data, { status });\n\t}\n\n\tpublic success(data?: unknown, message = \"Success\", status = 200): Response {\n\t\treturn this.json({ success: true, message, data }, status);\n\t}\n\n\t/**\n\t * Standardize an offset-based pagination response.\n\t */\n\tpublic paginate<T>(\n\t\tdata: T[],\n\t\ttotal: number,\n\t\tpage: number,\n\t\tlimit: number,\n\t\tmessage = \"Success\",\n\t\tstatus = 200,\n\t): Response {\n\t\treturn this.json(\n\t\t\t{\n\t\t\t\tsuccess: true,\n\t\t\t\tmessage,\n\t\t\t\tdata,\n\t\t\t\tmeta: {\n\t\t\t\t\tcurrentPage: page,\n\t\t\t\t\tperPage: limit,\n\t\t\t\t\ttotal,\n\t\t\t\t\tlastPage: Math.ceil(total / limit),\n\t\t\t\t\thasMore: page * limit < total,\n\t\t\t\t},\n\t\t\t},\n\t\t\tstatus,\n\t\t);\n\t}\n\n\t/**\n\t * Standardize a cursor-based pagination response (infinite scroll).\n\t */\n\tpublic cursorPaginate<T>(\n\t\tdata: T[],\n\t\tnextCursor: string | number | null,\n\t\tmessage = \"Success\",\n\t\tstatus = 200,\n\t): Response {\n\t\treturn this.json(\n\t\t\t{\n\t\t\t\tsuccess: true,\n\t\t\t\tmessage,\n\t\t\t\tdata,\n\t\t\t\tmeta: {\n\t\t\t\t\tnextCursor,\n\t\t\t\t\thasMore: nextCursor !== null,\n\t\t\t\t},\n\t\t\t},\n\t\t\tstatus,\n\t\t);\n\t}\n\n\tpublic error(message: string, status = 400, details?: unknown): Response {\n\t\treturn this.json({ success: false, message, details }, status);\n\t}\n\n\tprivate static readonly TEXT_HEADERS = {\n\t\t\"Content-Type\": \"text/plain; charset=utf-8\",\n\t};\n\n\tpublic text(text: string, status = 200): Response {\n\t\tif (status === 200) {\n\t\t\treturn new Response(text);\n\t\t}\n\t\treturn new Response(text, {\n\t\t\tstatus,\n\t\t\theaders: Context.TEXT_HEADERS,\n\t\t});\n\t}\n\n\tprivate static readonly HTML_HEADERS = { \"Content-Type\": \"text/html\" };\n\t/**\n\t * Return HTML response\n\t */\n\tpublic html(html: string, status = 200): Response {\n\t\treturn new Response(html, {\n\t\t\tstatus,\n\t\t\theaders: Context.HTML_HEADERS,\n\t\t});\n\t}\n\n\tpublic onAfterResponse(hook: (res: Response) => Response | undefined): void {\n\t\tif (!this._afterHooks) this._afterHooks = [];\n\t\tthis._afterHooks.push(hook);\n\t}\n\n\t/**\n\t * Redirect to URL\n\t */\n\tpublic redirect(\n\t\turl: string,\n\t\tstatus: 301 | 302 | 303 | 307 | 308 = 302,\n\t): Response {\n\t\treturn new Response(null, {\n\t\t\tstatus,\n\t\t\theaders: { Location: url },\n\t\t});\n\t}\n\n\t/**\n\t * Return empty response with status code\n\t */\n\tpublic status(code: number): Response {\n\t\treturn new Response(null, { status: code });\n\t}\n\n\t/**\n\t * Start a Server-Sent Events (SSE) stream - Bun-only\n\t */\n\tpublic sse(\n\t\tcallback: (stream: SSE) => void | Promise<void>,\n\t\toptions?: SSEOptions,\n\t): Response {\n\t\tconst stream = createSSE(this.request, options);\n\n\t\t// queueMicrotask lebih cepat dari setTimeout(0) - tanpa macrotask latency\n\t\tqueueMicrotask(async () => {\n\t\t\ttry {\n\t\t\t\tawait callback(stream);\n\t\t\t} catch (err) {\n\t\t\t\tconsole.error(\"[SSE Error]\", err);\n\t\t\t\ttry { stream.sendEvent(\"error\", String((err as Error).message)); } catch {}\n\t\t\t\tstream.close();\n\t\t\t}\n\t\t});\n\n\t\treturn stream.connect();\n\t}\n}\n","import type { Middleware } from \"./app\";\nimport type { Context } from \"./context\";\n\n/**\n * Decorator-based controller routing, built on Stage 3 (TC39) decorators -\n * the same kind natively supported by Bun/TypeScript 5+ without setting\n * `experimentalDecorators`.\n *\n * This is a thin layer on top of the existing functional API: decorators\n * only run once at class-definition/bootstrap time, and end up calling the\n * exact same `app.registerRoute()` used by `app.get()`/`app.post()`. There\n * is zero per-request overhead - the router has no idea (and doesn't care)\n * whether a route came from a decorator or a direct `.get()` call.\n *\n * ```ts\n * @Controller(\"/users\")\n * class UserController {\n *   @Get(\"/:id\")\n *   getUser(ctx: Context) {\n *     return ctx.json({ id: ctx.params.id });\n *   }\n *\n *   @Post(\"/\")\n *   @Use(zValidator(\"body\", createUserSchema))\n *   createUser(ctx: Context) {\n *     const data = ctx.valid<{ name: string }>(\"body\");\n *     return ctx.json(data, 201);\n *   }\n * }\n *\n * app.registerController(UserController);\n * ```\n */\n\nexport interface RouteMeta {\n\tmethod: string;\n\tpath: string;\n\tpropertyKey: string;\n\tmiddlewares: Middleware[];\n\t/** @HttpCode status override (e.g. 201) */\n\tstatusCode?: number;\n\t/** @Header / @SetHeader entries */\n\theaders?: Array<[string, string]>;\n\t/** @Redirect target */\n\tredirect?: { url: string; statusCode: number };\n\t/** @Version value */\n\tversion?: string | string[];\n\t/** Generic metadata via SetMetadata */\n\tmetadata?: Map<string, unknown>;\n}\n\nexport interface ControllerMeta {\n\tprefix: string;\n\troutes: RouteMeta[];\n}\n\nlet pendingRoutes: RouteMeta[] = [];\n\n// Generic metadata registry for SetMetadata / Roles / Version\nconst metadataRegistry = new WeakMap<Function, Map<string, Map<string, unknown>>>();\nfunction getOrCreateMethodMetadata(\n\ttarget: Function,\n\tpropertyKey: string,\n): Map<string, unknown> {\n\tlet classMap = metadataRegistry.get(target);\n\tif (!classMap) {\n\t\tclassMap = new Map();\n\t\tmetadataRegistry.set(target, classMap);\n\t}\n\tlet methodMap = classMap.get(propertyKey);\n\tif (!methodMap) {\n\t\tmethodMap = new Map();\n\t\tclassMap.set(propertyKey, methodMap);\n\t}\n\treturn methodMap;\n}\n\n// biome-ignore lint/complexity/noBannedTypes: Native Decorator API uses Function\nconst controllerRegistry = new WeakMap<Function, ControllerMeta>();\n\ntype MethodDecoratorFn = (\n\t// biome-ignore lint/suspicious/noExplicitAny: Required for TS method decorator contravariance\n\toriginalMethod: (...args: any[]) => any,\n\tcontext: ClassMethodDecoratorContext,\n) => void;\n\nfunction createRouteDecorator(method: string) {\n\treturn (path: string): MethodDecoratorFn => {\n\t\treturn (_originalMethod, context) => {\n\t\t\tif (context.kind !== \"method\") {\n\t\t\t\tthrow new Error(`@${method} can only decorate methods`);\n\t\t\t}\n\t\t\tconst propertyKey = String(context.name);\n\t\t\tconst entry = pendingRoutes.find((r) => r.propertyKey === propertyKey);\n\t\t\tif (entry) {\n\t\t\t\tentry.method = method;\n\t\t\t\tentry.path = path;\n\t\t\t} else {\n\t\t\t\tpendingRoutes.push({\n\t\t\t\t\tmethod,\n\t\t\t\t\tpath,\n\t\t\t\t\tpropertyKey,\n\t\t\t\t\tmiddlewares: [],\n\t\t\t\t});\n\t\t\t}\n\t\t};\n\t};\n}\n\nexport const Get = createRouteDecorator(\"GET\");\nexport const Post = createRouteDecorator(\"POST\");\nexport const Put = createRouteDecorator(\"PUT\");\nexport const Patch = createRouteDecorator(\"PATCH\");\nexport const Delete = createRouteDecorator(\"DELETE\");\nexport const Options = createRouteDecorator(\"OPTIONS\");\nexport const Head = createRouteDecorator(\"HEAD\");\nexport const Query = createRouteDecorator(\"QUERY\");\n\n/**\n * Matches ALL HTTP methods. Useful for catch-all handlers or proxies.\n * Note: more specific method decorators (@Get, @Post, etc.) registered on the\n * same path will take precedence in the AOT-compiled router.\n */\nexport const All = createRouteDecorator(\"ALL\");\n\n/**\n * Attach middleware to a single decorated route. Stack multiple `@Use()`\n * calls to chain several - they run in the order listed, same as passing\n * multiple middleware args to `app.get(path, mw1, mw2, handler)`.\n */\n// biome-ignore lint/suspicious/noExplicitAny: Flexible middleware signature for decorators\nexport function Use(\n\tmiddleware: (ctx: any, next: any) => any,\n): MethodDecoratorFn {\n\treturn (_originalMethod, context) => {\n\t\tif (context.kind !== \"method\") {\n\t\t\tthrow new Error(\"@Use can only decorate methods\");\n\t\t}\n\t\tconst propertyKey = String(context.name);\n\t\tlet entry = pendingRoutes.find((r) => r.propertyKey === propertyKey);\n\t\tif (!entry) {\n\t\t\tentry = { method: \"\", path: \"\", propertyKey, middlewares: [] };\n\t\t\tpendingRoutes.push(entry);\n\t\t}\n\t\tentry.middlewares.unshift(middleware as Middleware);\n\t};\n}\n\nexport type GuardFn<DI = Record<string, unknown>> = (\n\tctx: Context<DI>,\n) => boolean | Promise<boolean>;\n\n/**\n * Apply one or more guards to a controller method.\n * A guard is a function that returns a boolean. If it returns false,\n * the request is rejected with a 403 Forbidden.\n */\nexport function UseGuard<DI = Record<string, unknown>>(\n\t...guards: GuardFn<DI>[]\n) {\n\t// biome-ignore lint/suspicious/noExplicitAny: Required for internal Context bypass\n\treturn Use(async (ctx: any, next: any) => {\n\t\tfor (const guard of guards) {\n\t\t\tconst passed = await guard(ctx);\n\t\t\tif (!passed) {\n\t\t\t\treturn ctx.error(\"Forbidden resource\", 403);\n\t\t\t}\n\t\t}\n\t\treturn next();\n\t});\n}\n\n/**\n * @deprecated Use `UseGuards` instead. Kept as alias to `UseGuard` for backward compatibility.\n */\nexport const UseGuards = UseGuard;\n\n// ──────────────────────────────────────────────────────────────────────────────\n// Fase 1 zero-cost Nest-style decorators - boot-time metadata, <1% AOT overhead\n// ──────────────────────────────────────────────────────────────────────────────\n\n/**\n * Set arbitrary metadata on a handler (read via `getMetadata()` / Reflector).\n * Pure WeakMap write at decoration time - zero per-request cost.\n */\nexport function SetMetadata(key: string, value: unknown): MethodDecoratorFn {\n\treturn (_originalMethod, context) => {\n\t\tif (context.kind !== \"method\") {\n\t\t\tthrow new Error(\"@SetMetadata can only decorate methods\");\n\t\t}\n\t\tconst propertyKey = String(context.name);\n\t\tlet entry = pendingRoutes.find((r) => r.propertyKey === propertyKey);\n\t\tif (!entry) {\n\t\t\tentry = { method: \"\", path: \"\", propertyKey, middlewares: [] };\n\t\t\tpendingRoutes.push(entry);\n\t\t}\n\t\tif (!entry.metadata) entry.metadata = new Map();\n\t\tentry.metadata.set(key, value);\n\t\t// Also store in global metadataRegistry for Reflector-style lookups\n\t\t// We need a class reference; store lazily and resolve via Controller prefix later\n\t\t// For now, also keep pending; final association done in @Controller\n\t};\n}\n\n/**\n * Mark route as public (skip auth). Equivalent to `SetMetadata(\"isPublic\", true)` and checked by guards.\n */\nexport function Public(): MethodDecoratorFn {\n\treturn SetMetadata(\"isPublic\", true);\n}\n\n/**\n * Override HTTP status code for handler return (e.g. POST -> 201).\n * Stored as RouteMeta.statusCode and applied at registerRoute time via wrapper.\n */\nexport function HttpCode(statusCode: number): MethodDecoratorFn {\n\treturn (_originalMethod, context) => {\n\t\tif (context.kind !== \"method\") {\n\t\t\tthrow new Error(\"@HttpCode can only decorate methods\");\n\t\t}\n\t\tconst propertyKey = String(context.name);\n\t\tlet entry = pendingRoutes.find((r) => r.propertyKey === propertyKey);\n\t\tif (!entry) {\n\t\t\tentry = { method: \"\", path: \"\", propertyKey, middlewares: [] };\n\t\t\tpendingRoutes.push(entry);\n\t\t}\n\t\tentry.statusCode = statusCode;\n\t};\n}\n\n/**\n * Set a static response header for the route (e.g. `@SetHeader(\"x-cache\", \"hit\")`).\n * Multiple usages stack.\n */\nexport function SetHeader(name: string, value: string): MethodDecoratorFn {\n\treturn (_originalMethod, context) => {\n\t\tif (context.kind !== \"method\") {\n\t\t\tthrow new Error(\"@SetHeader can only decorate methods\");\n\t\t}\n\t\tconst propertyKey = String(context.name);\n\t\tlet entry = pendingRoutes.find((r) => r.propertyKey === propertyKey);\n\t\tif (!entry) {\n\t\t\tentry = { method: \"\", path: \"\", propertyKey, middlewares: [] };\n\t\t\tpendingRoutes.push(entry);\n\t\t}\n\t\tif (!entry.headers) entry.headers = [];\n\t\tentry.headers.push([name, value]);\n\t};\n}\n\n/** @deprecated Use `SetHeader` instead. Alias kept for Nest compatibility. */\nexport const Header = SetHeader;\n\n/**\n * Redirect to URL with status (default 302). Static redirect - handler not executed.\n * If handler returns `{url, statusCode}`, it overrides decorator value (Nest behavior).\n */\nexport function Redirect(url: string, statusCode = 302): MethodDecoratorFn {\n\treturn (_originalMethod, context) => {\n\t\tif (context.kind !== \"method\") {\n\t\t\tthrow new Error(\"@Redirect can only decorate methods\");\n\t\t}\n\t\tconst propertyKey = String(context.name);\n\t\tlet entry = pendingRoutes.find((r) => r.propertyKey === propertyKey);\n\t\tif (!entry) {\n\t\t\tentry = { method: \"\", path: \"\", propertyKey, middlewares: [] };\n\t\t\tpendingRoutes.push(entry);\n\t\t}\n\t\tentry.redirect = { url, statusCode };\n\t};\n}\n\n/**\n * Set API version for route (prefix or metadata). Stored as string, applied as path prefix or metadata.\n */\nexport function Version(version: string | string[]): MethodDecoratorFn {\n\treturn (_originalMethod, context) => {\n\t\tif (context.kind !== \"method\") {\n\t\t\tthrow new Error(\"@Version can only decorate methods\");\n\t\t}\n\t\tconst propertyKey = String(context.name);\n\t\tlet entry = pendingRoutes.find((r) => r.propertyKey === propertyKey);\n\t\tif (!entry) {\n\t\t\tentry = { method: \"\", path: \"\", propertyKey, middlewares: [] };\n\t\t\tpendingRoutes.push(entry);\n\t\t}\n\t\tentry.version = version;\n\t};\n}\n\n/**\n * Compose multiple decorators into one (Nest `applyDecorators` equivalent).\n * Zero-cost boot-time helper.\n */\nexport function applyDecorators(\n\t...decorators: Array<(target: any, context: any) => void>\n): MethodDecoratorFn & ((target: Function, context: ClassDecoratorContext) => void) {\n\treturn ((target: any, context: any) => {\n\t\tfor (const dec of decorators) {\n\t\t\t(dec as any)(target, context);\n\t\t}\n\t}) as any;\n}\n\n/**\n * Retrieve metadata set via @SetMetadata (helper for guards/interceptors).\n */\nexport function getMetadata(\n\ttarget: Function,\n\tpropertyKey: string,\n\tkey: string,\n): unknown {\n\tconst classMap = metadataRegistry.get(target);\n\tif (!classMap) return undefined;\n\tconst methodMap = classMap.get(propertyKey);\n\tif (!methodMap) return undefined;\n\treturn methodMap.get(key);\n}\n\n/**\n * Marks a class as a controller and registers its accumulated `@Get`/\n * `@Post`/etc. routes under `prefix`. Must be the outermost (topmost)\n * decorator on the class so it runs after all method decorators have\n * populated the pending route list.\n */\nexport function Controller(prefix = \"\") {\n\t// biome-ignore lint/complexity/noBannedTypes: Native Decorator API uses Function\n\treturn (target: Function, context: ClassDecoratorContext): void => {\n\t\tif (context.kind !== \"class\") {\n\t\t\tthrow new Error(\"@Controller can only decorate classes\");\n\t\t}\n\t\tconst routes = pendingRoutes.filter((r) => r.method !== \"\");\n\t\tpendingRoutes = [];\n\t\tcontrollerRegistry.set(target, { prefix, routes });\n\t};\n}\n\n/**\n * Reads the routes registered on a `@Controller`-decorated class. Used\n * internally by `App.registerController()`. Walks the prototype chain\n * to merge parent class routes with child class routes.\n */\nexport function getControllerMeta(\n\t// biome-ignore lint/complexity/noBannedTypes: Native Decorator API uses Function\n\tControllerClass: Function,\n): ControllerMeta | undefined {\n\tconst meta = controllerRegistry.get(ControllerClass);\n\n\t// Walk prototype chain to get parent routes\n\tconst parentProto = Object.getPrototypeOf(ControllerClass.prototype);\n\tconst ParentClass = parentProto?.constructor;\n\n\tif (\n\t\tParentClass &&\n\t\tParentClass !== Function.prototype &&\n\t\tParentClass !== Object\n\t) {\n\t\tconst parentMeta = getControllerMeta(ParentClass);\n\t\tif (parentMeta) {\n\t\t\t// Merge: parent routes + child routes (child overrides parent on same propertyKey)\n\t\t\tconst childRoutes = meta?.routes ?? [];\n\t\t\tconst parentRoutes = parentMeta.routes;\n\n\t\t\t// Child routes override parent routes with same propertyKey\n\t\t\tconst mergedRoutes = [...parentRoutes];\n\t\t\tfor (const childRoute of childRoutes) {\n\t\t\t\tconst existingIndex = mergedRoutes.findIndex(\n\t\t\t\t\t(r) => r.propertyKey === childRoute.propertyKey,\n\t\t\t\t);\n\t\t\t\tif (existingIndex >= 0) {\n\t\t\t\t\tmergedRoutes[existingIndex] = childRoute;\n\t\t\t\t} else {\n\t\t\t\t\tmergedRoutes.push(childRoute);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\tprefix: meta?.prefix ?? parentMeta.prefix,\n\t\t\t\troutes: mergedRoutes,\n\t\t\t};\n\t\t}\n\t}\n\n\treturn meta;\n}\n\n\n","import type { Context } from \"../context\";\nimport { toResponse } from \"./response\";\n\ntype RouteHandler<DI = Record<string, unknown>> = (\n\tctx: Context<DI>,\n) => Promise<Response>;\n\nexport function asyncHandler<DI = Record<string, unknown>>(\n\t// biome-ignore lint/suspicious/noExplicitAny: flexible return\n\thandler: (ctx: Context<DI>) => Promise<any>,\n): RouteHandler<DI> {\n\treturn async (ctx: Context<DI>): Promise<Response> => {\n\t\ttry {\n\t\t\tconst result = await handler(ctx);\n\t\t\treturn toResponse(result);\n\t\t} catch (error: unknown) {\n\t\t\tconst message =\n\t\t\t\terror instanceof Error ? error.message : \"Internal Server Error\";\n\t\t\tconst status = error instanceof HttpError ? error.status : 500;\n\n\t\t\tconst errorName =\n\t\t\t\tstatus === 400\n\t\t\t\t\t? \"Bad Request\"\n\t\t\t\t\t: status === 401\n\t\t\t\t\t\t? \"Unauthorized\"\n\t\t\t\t\t\t: status === 403\n\t\t\t\t\t\t\t? \"Forbidden\"\n\t\t\t\t\t\t\t: status === 404\n\t\t\t\t\t\t\t\t? \"Not Found\"\n\t\t\t\t\t\t\t\t: status === 405\n\t\t\t\t\t\t\t\t\t? \"Method Not Allowed\"\n\t\t\t\t\t\t\t\t\t: status === 409\n\t\t\t\t\t\t\t\t\t\t? \"Conflict\"\n\t\t\t\t\t\t\t\t\t\t: status === 422\n\t\t\t\t\t\t\t\t\t\t\t? \"Unprocessable Entity\"\n\t\t\t\t\t\t\t\t\t\t\t: status === 429\n\t\t\t\t\t\t\t\t\t\t\t\t? \"Too Many Requests\"\n\t\t\t\t\t\t\t\t\t\t\t\t: status === 503\n\t\t\t\t\t\t\t\t\t\t\t\t\t? \"Service Unavailable\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t: \"Internal Server Error\";\n\n\t\t\treturn ctx.json({ success: false, error: errorName, message }, status);\n\t\t}\n\t};\n}\n\nexport class HttpError extends Error {\n\tstatus: number;\n\n\tconstructor(status: number, message: string) {\n\t\tsuper(message);\n\t\tthis.status = status;\n\t\tthis.name = \"HttpError\";\n\t}\n}\n\nexport class NotFoundError extends HttpError {\n\tconstructor(message = \"Not Found\") {\n\t\tsuper(404, message);\n\t\tthis.name = \"NotFoundError\";\n\t}\n}\n\nexport class BadRequestError extends HttpError {\n\tconstructor(message = \"Bad Request\") {\n\t\tsuper(400, message);\n\t\tthis.name = \"BadRequestError\";\n\t}\n}\n\nexport class UnauthorizedError extends HttpError {\n\tconstructor(message = \"Unauthorized\") {\n\t\tsuper(401, message);\n\t\tthis.name = \"UnauthorizedError\";\n\t}\n}\n\nexport class ForbiddenError extends HttpError {\n\tconstructor(message = \"Forbidden\") {\n\t\tsuper(403, message);\n\t\tthis.name = \"ForbiddenError\";\n\t}\n}\n\nexport class MethodNotAllowedError extends HttpError {\n\tconstructor(message = \"Method Not Allowed\") {\n\t\tsuper(405, message);\n\t\tthis.name = \"MethodNotAllowedError\";\n\t}\n}\n\nexport class ConflictError extends HttpError {\n\tconstructor(message = \"Conflict\") {\n\t\tsuper(409, message);\n\t\tthis.name = \"ConflictError\";\n\t}\n}\n\nexport class UnprocessableEntityError extends HttpError {\n\tconstructor(message = \"Unprocessable Entity\") {\n\t\tsuper(422, message);\n\t\tthis.name = \"UnprocessableEntityError\";\n\t}\n}\n\nexport class TooManyRequestsError extends HttpError {\n\tconstructor(message = \"Too Many Requests\") {\n\t\tsuper(429, message);\n\t\tthis.name = \"TooManyRequestsError\";\n\t}\n}\n\nexport class InternalServerError extends HttpError {\n\tconstructor(message = \"Internal Server Error\") {\n\t\tsuper(500, message);\n\t\tthis.name = \"InternalServerError\";\n\t}\n}\n\nexport class ServiceUnavailableError extends HttpError {\n\tconstructor(message = \"Service Unavailable\") {\n\t\tsuper(503, message);\n\t\tthis.name = \"ServiceUnavailableError\";\n\t}\n}\n","/**\n * Central response normalizer - Elysia-style flexible return.\n *\n * Allows handlers to return string | number | boolean | object | array | null | void\n * in addition to Response, and automatically serializes to correct Response.\n *\n * - Response -> passthrough\n * - null/undefined/void -> 204 No Content\n * - string -> text/plain; charset=utf-8\n * - number/boolean/bigint -> text/plain via String(value)\n * - Blob/ArrayBuffer/Uint8Array/ReadableStream -> BodyInit passthrough\n * - object/array -> application/json via Response.json\n */\n\nconst TEXT_HEADERS: Record<string, string> = {\n\t\"Content-Type\": \"text/plain; charset=utf-8\",\n};\n\nexport function toResponse(value: unknown): Response {\n\tif (value instanceof Response) return value;\n\tif (value === null || value === undefined) {\n\t\treturn new Response(null, { status: 204 });\n\t}\n\tconst t = typeof value;\n\tif (t === \"string\") {\n\t\treturn new Response(value as string, { headers: TEXT_HEADERS });\n\t}\n\tif (t === \"number\" || t === \"boolean\" || t === \"bigint\") {\n\t\treturn new Response(String(value), { headers: TEXT_HEADERS });\n\t}\n\t// Binary / stream bodies should not be JSON-stringified\n\tif (\n\t\tvalue instanceof Blob ||\n\t\tvalue instanceof ArrayBuffer ||\n\t\tvalue instanceof Uint8Array ||\n\t\tvalue instanceof ReadableStream\n\t) {\n\t\t// biome-ignore lint/suspicious/noExplicitAny: BodyInit union is DOM-specific\n\t\treturn new Response(value as any);\n\t}\n\t// Array, plain object, etc. -> JSON\n\treturn Response.json(value);\n}\n\nexport function toResponseMaybeAsync(\n\tvalue: unknown,\n): Response | Promise<Response> {\n\tif (value instanceof Promise) {\n\t\treturn value.then(toResponse);\n\t}\n\treturn toResponse(value);\n}\n","import { appendFile, existsSync, mkdirSync } from \"node:fs\";\nimport { join } from \"node:path\";\n\nexport enum LogLevel {\n\tDEBUG = 0,\n\tINFO = 1,\n\tWARN = 2,\n\tERROR = 3,\n}\n\ninterface LoggerOptions {\n\tlevel?: LogLevel;\n\tformat?: \"text\" | \"json\";\n\t// Disable request logging (useful for benchmarks)\n\tlogRequests?: boolean;\n\t// Buffer settings\n\tflushInterval?: number; // ms between auto-flush (default: 2000)\n\tflushThreshold?: number; // lines before forced flush (default: 100)\n}\n\n// Pre-computed ANSI colors for each log level\nconst COLORS: Record<number, string> = {\n\t[LogLevel.DEBUG]: \"\\x1b[34m\", // Blue\n\t[LogLevel.INFO]: \"\\x1b[32m\", // Green\n\t[LogLevel.WARN]: \"\\x1b[33m\", // Yellow\n\t[LogLevel.ERROR]: \"\\x1b[31m\", // Red\n};\nconst RESET = \"\\x1b[0m\";\nconst GRAY = \"\\x1b[90m\";\n\nexport class Logger {\n\tprivate level: LogLevel;\n\tprivate format: \"text\" | \"json\";\n\tprivate logDir: string | undefined;\n\tprivate isProd: boolean;\n\tprivate _logRequests: boolean;\n\n\t// Buffered file writer\n\tprivate fileBuffer: string[] = [];\n\tprivate flushTimer: ReturnType<typeof setInterval> | null = null;\n\tprivate flushInterval: number;\n\tprivate flushThreshold: number;\n\tprivate flushing = false;\n\n\t// Stdout/stderr buffer (non-blocking terminal output)\n\tprivate flushScheduled = false;\n\tprivate stdoutBuffer: string[] = [];\n\tprivate stderrBuffer: string[] = [];\n\n\tconstructor(options?: LoggerOptions) {\n\t\tthis.isProd = process.env.NODE_ENV === \"production\";\n\t\tthis.level =\n\t\t\toptions?.level ?? (this.isProd ? LogLevel.WARN : LogLevel.INFO);\n\t\tthis.format = options?.format ?? (this.isProd ? \"json\" : \"text\");\n\t\tthis._logRequests = options?.logRequests ?? true;\n\t\tthis.flushInterval = options?.flushInterval ?? 2000;\n\t\tthis.flushThreshold = options?.flushThreshold ?? 100;\n\n\t\tthis.logDir = process.env.LOG_DIR;\n\t\tif (this.logDir && !existsSync(this.logDir)) {\n\t\t\tmkdirSync(this.logDir, { recursive: true });\n\t\t}\n\n\t\t// Start flush timer if file logging is enabled\n\t\tif (this.logDir) {\n\t\t\tthis.flushTimer = setInterval(() => this.flushFileBuffer(), this.flushInterval);\n\t\t\t// Allow process to exit even if timer is running\n\t\t\tif (this.flushTimer && typeof this.flushTimer.unref === \"function\") {\n\t\t\t\tthis.flushTimer.unref();\n\t\t\t}\n\t\t}\n\t}\n\n\t// Flush stdout/stderr to terminal\n\tprivate flush() {\n\t\tif (this.stdoutBuffer.length > 0) {\n\t\t\tprocess.stdout.write(this.stdoutBuffer.join(\"\"));\n\t\t\tthis.stdoutBuffer.length = 0;\n\t\t}\n\t\tif (this.stderrBuffer.length > 0) {\n\t\t\tprocess.stderr.write(this.stderrBuffer.join(\"\"));\n\t\t\tthis.stderrBuffer.length = 0;\n\t\t}\n\t\tthis.flushScheduled = false;\n\t}\n\n\tprivate scheduleFlush() {\n\t\tif (this.flushScheduled) return;\n\t\tthis.flushScheduled = true;\n\t\tqueueMicrotask(() => this.flush());\n\t}\n\n\t// Flush buffered log lines to file (batch write)\n\tprivate flushFileBuffer() {\n\t\tif (this.fileBuffer.length === 0 || !this.logDir) return;\n\t\tif (this.flushing) return; // prevent concurrent writes\n\n\t\tthis.flushing = true;\n\t\tconst lines = this.fileBuffer.splice(0); // take all buffered lines\n\t\tconst data = lines.join(\"\");\n\n\t\t// Determine date from first line's timestamp\n\t\tconst now = new Date();\n\t\tconst dateStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, \"0\")}-${String(now.getDate()).padStart(2, \"0\")}`;\n\t\tconst filePath = join(this.logDir, `app-${dateStr}.log`);\n\n\t\tappendFile(filePath, data, (err) => {\n\t\t\tif (err) {\n\t\t\t\t// Write failed lines back to buffer for retry\n\t\t\t\tthis.fileBuffer.unshift(...lines);\n\t\t\t}\n\t\t\tthis.flushing = false;\n\t\t});\n\t}\n\n\t// Enqueue a line for file writing\n\tprivate enqueueFile(line: string) {\n\t\tthis.fileBuffer.push(line);\n\t\tif (this.fileBuffer.length >= this.flushThreshold) {\n\t\t\tthis.flushFileBuffer();\n\t\t}\n\t}\n\n\tprivate print(\n\t\tlevel: LogLevel,\n\t\tlevelName: string,\n\t\tmessage: string,\n\t\tmeta?: Record<string, unknown>,\n\t) {\n\t\tif (level < this.level) return;\n\n\t\tconst isStderr = level === LogLevel.ERROR;\n\n\t\tif (this.format === \"json\") {\n\t\t\tconst timestamp = new Date().toISOString();\n\t\t\tconst logEntryObj = { timestamp, level: levelName, message, ...meta };\n\t\t\tconst jsonString = JSON.stringify(logEntryObj);\n\t\t\tconst line = `${jsonString}\\n`;\n\n\t\t\t// Non-blocking: buffer and schedule flush\n\t\t\tif (isStderr) {\n\t\t\t\tthis.stderrBuffer.push(line);\n\t\t\t} else {\n\t\t\t\tthis.stdoutBuffer.push(line);\n\t\t\t}\n\t\t\tthis.scheduleFlush();\n\n\t\t\t// File logging (buffered batch write)\n\t\t\tif (this.logDir) {\n\t\t\t\tthis.enqueueFile(line);\n\t\t\t}\n\t\t} else {\n\t\t\tconst now = Date.now();\n\t\t\tconst color = COLORS[level] ?? COLORS[LogLevel.INFO];\n\n\t\t\tlet logString = `[${now}] ${color}[${levelName}]${RESET} ${message}`;\n\n\t\t\tif (meta) {\n\t\t\t\tconst keys = Object.keys(meta);\n\t\t\t\tif (keys.length > 0) {\n\t\t\t\t\tif (keys.length === 1) {\n\t\t\t\t\t\tconst val = meta[keys[0] || 0];\n\t\t\t\t\t\tlogString += ` ${GRAY}{${JSON.stringify(keys[0])}:${JSON.stringify(val)}}${RESET}`;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tlogString += ` ${GRAY}${JSON.stringify(meta)}${RESET}`;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Non-blocking: buffer and schedule flush\n\t\t\tif (isStderr) {\n\t\t\t\tthis.stderrBuffer.push(`${logString}\\n`);\n\t\t\t} else {\n\t\t\t\tthis.stdoutBuffer.push(`${logString}\\n`);\n\t\t\t}\n\t\t\tthis.scheduleFlush();\n\n\t\t\t// File logging (buffered batch write) - strip ANSI for clean log files\n\t\t\tif (this.logDir) {\n\t\t\t\tconst cleanLine = `[${now}] [${levelName}] ${message}${meta ? ` ${JSON.stringify(meta)}` : \"\"}\\n`;\n\t\t\t\tthis.enqueueFile(cleanLine);\n\t\t\t}\n\t\t}\n\t}\n\n\tpublic debug(message: string, meta?: Record<string, unknown>) {\n\t\tthis.print(LogLevel.DEBUG, \"DEBUG\", message, meta);\n\t}\n\n\tpublic info(message: string, meta?: Record<string, unknown>) {\n\t\tthis.print(LogLevel.INFO, \"INFO\", message, meta);\n\t}\n\n\tpublic warn(message: string, meta?: Record<string, unknown>) {\n\t\tthis.print(LogLevel.WARN, \"WARN\", message, meta);\n\t}\n\n\tpublic error(message: string, meta?: Record<string, unknown>) {\n\t\tthis.print(LogLevel.ERROR, \"ERROR\", message, meta);\n\t}\n\n\tpublic get logRequests(): boolean {\n\t\treturn this._logRequests;\n\t}\n\n\t// Flush all buffers (useful for graceful shutdown)\n\tpublic flushSync() {\n\t\t// Flush terminal\n\t\tif (this.stdoutBuffer.length > 0) {\n\t\t\tprocess.stdout.write(this.stdoutBuffer.join(\"\"));\n\t\t\tthis.stdoutBuffer.length = 0;\n\t\t}\n\t\tif (this.stderrBuffer.length > 0) {\n\t\t\tprocess.stderr.write(this.stderrBuffer.join(\"\"));\n\t\t\tthis.stderrBuffer.length = 0;\n\t\t}\n\t\t// Flush file buffer synchronously\n\t\tif (this.flushTimer) {\n\t\t\tclearInterval(this.flushTimer);\n\t\t\tthis.flushTimer = null;\n\t\t}\n\t\t// One final async flush for remaining file lines\n\t\tif (this.fileBuffer.length > 0 && this.logDir) {\n\t\t\tconst lines = this.fileBuffer.splice(0);\n\t\t\tconst data = lines.join(\"\");\n\t\t\tconst now = new Date();\n\t\t\tconst dateStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, \"0\")}-${String(now.getDate()).padStart(2, \"0\")}`;\n\t\t\tconst filePath = join(this.logDir, `app-${dateStr}.log`);\n\t\t\t// Synchronous append for shutdown\n\t\t\ttry {\n\t\t\t\tconst { writeFileSync, appendFileSync } = require(\"node:fs\");\n\t\t\t\tappendFileSync(filePath, data);\n\t\t\t} catch {\n\t\t\t\t// Best effort\n\t\t\t}\n\t\t}\n\t}\n}\n\n// Create logger with env-based configuration\nexport const logger = new Logger({\n\tlogRequests: process.env.LOG_REQUESTS !== \"false\",\n});\n","import { Trie as NativeTrie } from \"../ffi\";\nimport { NodeType, RouterNode } from \"./node\";\n\nexport interface LookupResult {\n\t// biome-ignore lint/suspicious/noExplicitAny: generic router handler\n\thandler: ((...args: any[]) => any) | null;\n\tparams: Record<string, string>;\n}\n\nconst EMPTY_PARAMS: Record<string, string> = Object.freeze({});\n\n// Simple fixed-size LRU cache for trie lookup results on dynamic routes.\n// Avoids re-walking the trie for the same (method, pathname) pair -\n// common in real apps where the same resource is hit repeatedly.\nconst CACHE_MAX = 2048;\n\nclass LookupCache {\n\tprivate map = new Map<string, Map<string, LookupResult>>();\n\tprivate queue: Array<{ method: string; path: string }> = [];\n\n\tget(method: string, path: string): LookupResult | undefined {\n\t\tconst methodMap = this.map.get(method);\n\t\tif (!methodMap) return undefined;\n\t\treturn methodMap.get(path);\n\t}\n\n\tset(method: string, path: string, val: LookupResult): void {\n\t\tlet methodMap = this.map.get(method);\n\t\tif (!methodMap) {\n\t\t\tmethodMap = new Map();\n\t\t\tthis.map.set(method, methodMap);\n\t\t}\n\n\t\tif (methodMap.has(path)) {\n\t\t\tmethodMap.set(path, val);\n\t\t\treturn;\n\t\t}\n\n\t\tif (this.queue.length >= CACHE_MAX) {\n\t\t\t// Evict the oldest entry\n\t\t\tconst oldest = this.queue.shift();\n\t\t\tif (oldest) {\n\t\t\t\tconst oldMethodMap = this.map.get(oldest.method);\n\t\t\t\tif (oldMethodMap) oldMethodMap.delete(oldest.path);\n\t\t\t}\n\t\t}\n\n\t\tmethodMap.set(path, val);\n\t\tthis.queue.push({ method, path });\n\t}\n}\n\nexport class Router {\n\tprivate root: RouterNode = new RouterNode(\"/\", NodeType.STATIC);\n\t// biome-ignore lint/suspicious/noExplicitAny: generic router handler\n\tpublic staticRoutes: Map<string, Map<string, (...args: any[]) => any>> =\n\t\tnew Map();\n\n\t// LRU cache for dynamic (param/catchall) route lookups\n\tprivate readonly lookupCache = new LookupCache();\n\n\t// Trie for dynamic route lookup\n\tprivate nativeTrie: NativeTrie;\n\t// biome-ignore lint/suspicious/noExplicitAny: generic router handler\n\tprivate handlerRegistry: Map<number, (...args: any[]) => any> = new Map();\n\tprivate nextHandlerId = 0;\n\n\tconstructor() {\n\t\tthis.nativeTrie = new NativeTrie();\n\t}\n\n\tprivate static splitPath(path: string): string[] {\n\t\tconst segments: string[] = [];\n\t\tlet start = 0;\n\t\tif (path[0] === \"/\") start = 1;\n\t\tfor (let i = start; i < path.length; i++) {\n\t\t\tif (path[i] === \"/\") {\n\t\t\t\tif (i > start) {\n\t\t\t\t\tsegments.push(path.substring(start, i));\n\t\t\t\t}\n\t\t\t\tstart = i + 1;\n\t\t\t}\n\t\t}\n\t\tif (start < path.length) {\n\t\t\tsegments.push(path.substring(start));\n\t\t}\n\t\treturn segments;\n\t}\n\n\tpublic insert(\n\t\tmethod: string,\n\t\tpath: string,\n\t\t// biome-ignore lint/suspicious/noExplicitAny: generic router handler\n\t\thandler: (...args: any[]) => any,\n\t): void {\n\t\tif (path === \"\") return;\n\n\t\tconst upperMethod = method.toUpperCase();\n\t\tconst segments = Router.splitPath(path);\n\t\tconst hasParams = segments.some((s) => s[0] === \":\" || s[0] === \"*\");\n\n\t\t// Fast path: store static routes in flat map\n\t\tif (!hasParams) {\n\t\t\tlet methodMap = this.staticRoutes.get(path);\n\t\t\tif (!methodMap) {\n\t\t\t\tmethodMap = new Map();\n\t\t\t\tthis.staticRoutes.set(path, methodMap);\n\t\t\t}\n\t\t\tmethodMap.set(upperMethod, handler);\n\t\t}\n\n\t\t// Register in trie for dynamic routes\n\t\tif (hasParams) {\n\t\t\tconst handlerId = this.nextHandlerId++;\n\t\t\tthis.handlerRegistry.set(handlerId, handler);\n\t\t\t// Use composite key: \"METHOD:/path/:param\"\n\t\t\tconst compositeKey = `${upperMethod}:${path}`;\n\t\t\tthis.nativeTrie.insert(compositeKey, handlerId);\n\t\t}\n\n\t\t// Also insert into JS trie for fallback / non-native\n\t\tlet currentNode = this.root;\n\n\t\tif (segments.length === 0) {\n\t\t\tthis.root.handlers.set(upperMethod, handler);\n\t\t\treturn;\n\t\t}\n\n\t\tfor (const segment of segments) {\n\t\t\tlet nodeType = NodeType.STATIC;\n\t\t\tlet paramName: string | null = null;\n\t\t\tlet cleanSegment = segment;\n\n\t\t\tif (segment.startsWith(\":\")) {\n\t\t\t\tnodeType = NodeType.PARAM;\n\t\t\t\tparamName = segment.slice(1);\n\t\t\t\tcleanSegment = \":\";\n\t\t\t} else if (segment.startsWith(\"*\")) {\n\t\t\t\tnodeType = NodeType.CATCHALL;\n\t\t\t\tparamName = segment.slice(1);\n\t\t\t\tcleanSegment = \"*\";\n\t\t\t} else {\n\t\t\t\tnodeType = NodeType.STATIC;\n\t\t\t\tcleanSegment = segment;\n\t\t\t}\n\n\t\t\tlet childNode: RouterNode | undefined;\n\n\t\t\tif (nodeType === NodeType.STATIC) {\n\t\t\t\tchildNode = currentNode.staticChildren.get(cleanSegment);\n\t\t\t} else if (nodeType === NodeType.PARAM) {\n\t\t\t\tchildNode = currentNode.paramChild ?? undefined;\n\t\t\t} else {\n\t\t\t\tchildNode = currentNode.catchAllChild ?? undefined;\n\t\t\t}\n\n\t\t\tif (!childNode) {\n\t\t\t\tchildNode = new RouterNode(cleanSegment, nodeType);\n\n\t\t\t\tif (nodeType === NodeType.PARAM) {\n\t\t\t\t\tchildNode.paramName = paramName;\n\t\t\t\t\tcurrentNode.paramChild = childNode;\n\t\t\t\t} else if (nodeType === NodeType.CATCHALL) {\n\t\t\t\t\tchildNode.paramName = paramName;\n\t\t\t\t\tcurrentNode.catchAllChild = childNode;\n\t\t\t\t} else {\n\t\t\t\t\tcurrentNode.staticChildren.set(cleanSegment, childNode);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tcurrentNode = childNode;\n\n\t\t\tif (nodeType === NodeType.CATCHALL) break;\n\t\t}\n\n\t\tcurrentNode.handlers.set(upperMethod, handler);\n\t}\n\n\tpublic find(method: string, path: string): LookupResult {\n\t\t// Fast path: try flat static route cache first (O(1))\n\t\tconst methodMap = this.staticRoutes.get(path);\n\t\tif (methodMap) {\n\t\t\tconst handler = methodMap.get(method); // method is already uppercase\n\t\t\tif (handler) {\n\t\t\t\treturn { handler, params: EMPTY_PARAMS };\n\t\t\t}\n\t\t}\n\n\t\t// LRU cache for dynamic routes - avoids re-walking the trie for\n\t\t// repeated (method, pathname) pairs (e.g. same user ID hit often)\n\t\tconst cached = this.lookupCache.get(method, path);\n\t\tif (cached) return cached;\n\n\t\t// Trie lookup for dynamic routes\n\t\t{\n\t\t\tconst compositeKey = `${method}:${path}`;\n\t\t\tconst nativeResult = this.nativeTrie.find(compositeKey);\n\n\t\t\tif (nativeResult.handlerId !== -1) {\n\t\t\t\tconst handler = this.handlerRegistry.get(nativeResult.handlerId);\n\t\t\t\tif (handler) {\n\t\t\t\t\tconst result: LookupResult = {\n\t\t\t\t\t\thandler,\n\t\t\t\t\t\tparams: nativeResult.params,\n\t\t\t\t\t};\n\t\t\t\t\tthis.lookupCache.set(method, path, result);\n\t\t\t\t\treturn result;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Slow path: JS trie traversal for param/catchall routes\n\t\tconst result: LookupResult = {\n\t\t\thandler: null,\n\t\t\tparams: null as unknown as Record<string, string>,\n\t\t};\n\t\tlet currentNode = this.root;\n\n\t\tlet start = path[0] === \"/\" ? 1 : 0;\n\t\tif (start >= path.length) {\n\t\t\tresult.handler = this.root.handlers.get(method) || null;\n\t\t\tif (result.handler) result.params = EMPTY_PARAMS;\n\t\t\telse result.params = EMPTY_PARAMS;\n\t\t\treturn result;\n\t\t}\n\n\t\twhile (start < path.length) {\n\t\t\tlet end = path.indexOf(\"/\", start);\n\t\t\tif (end === -1) end = path.length;\n\n\t\t\tif (end > start) {\n\t\t\t\tconst segment = path.substring(start, end);\n\t\t\t\tlet nextNode = currentNode.staticChildren.get(segment);\n\n\t\t\t\tif (!nextNode) {\n\t\t\t\t\tif (currentNode.paramChild) {\n\t\t\t\t\t\tnextNode = currentNode.paramChild;\n\t\t\t\t\t\tif (!result.params) result.params = {};\n\t\t\t\t\t\t// biome-ignore lint/style/noNonNullAssertion: Known to exist due to node structure\n\t\t\t\t\t\tresult.params[nextNode.paramName!] = segment;\n\t\t\t\t\t} else if (currentNode.catchAllChild) {\n\t\t\t\t\t\tnextNode = currentNode.catchAllChild;\n\t\t\t\t\t\tif (!result.params) result.params = {};\n\t\t\t\t\t\tresult.params[\"*\"] = path.substring(start);\n\t\t\t\t\t\tcurrentNode = nextNode;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t} else {\n\t\t\t\t\t\treturn { handler: null, params: EMPTY_PARAMS };\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tcurrentNode = nextNode;\n\t\t\t}\n\t\t\tstart = end + 1;\n\t\t}\n\n\t\tresult.handler = currentNode.handlers.get(method) || null;\n\t\tif (!result.params) result.params = EMPTY_PARAMS;\n\n\t\t// Cache the result for future identical lookups\n\t\tif (result.handler) {\n\t\t\tthis.lookupCache.set(method, path, result);\n\t\t}\n\n\t\treturn result;\n\t}\n}\n","import {\n\tJSTrie,\n\tjsEscapeString,\n\tjsParseCookies,\n\tjsParseQuery,\n} from \"./fallback\";\n\n// ─── Lazy Native Loading ─────────────────────────────────────────────────────\n\nlet _nativeAvailable: boolean | undefined;\n// biome-ignore lint/suspicious/noExplicitAny: FFI libs are dynamic\nlet _nativeJsonLib: any;\n// biome-ignore lint/suspicious/noExplicitAny: FFI ptr cache\nlet _ffi: any;\n\nfunction tryLoadNative(): void {\n\tif (_nativeAvailable !== undefined) return;\n\ttry {\n\t\t// biome-ignore lint: dynamic require for optional FFI\n\t\t_ffi = require(\"bun:ffi\");\n\t\t// biome-ignore lint: dynamic require\n\t\tconst fs = require(\"node:fs\");\n\t\t// biome-ignore lint: dynamic require\n\t\tconst path = require(\"node:path\");\n\n\t\tconst ext =\n\t\t\tprocess.platform === \"win32\"\n\t\t\t\t? \".dll\"\n\t\t\t\t: process.platform === \"darwin\"\n\t\t\t\t\t? \".dylib\"\n\t\t\t\t\t: \".so\";\n\t\tconst prefix = process.platform === \"win32\" ? \"\" : \"lib\";\n\n\t\tfunction findLib(name: string): string {\n\t\t\tconst candidates = [\n\t\t\t\tpath.join(\n\t\t\t\t\tprocess.cwd(),\n\t\t\t\t\t\"node_modules\",\n\t\t\t\t\t\".buntok-native\",\n\t\t\t\t\t`${prefix}${name}${ext}`,\n\t\t\t\t),\n\t\t\t\tpath.join(process.cwd(), \"dist\", \"native\", `${prefix}${name}${ext}`),\n\t\t\t\tpath.join(process.cwd(), \"zig-out\", \"lib\", `${prefix}${name}${ext}`),\n\t\t\t];\n\t\t\tfor (const p of candidates) {\n\t\t\t\tif (fs.existsSync(p)) return p;\n\t\t\t}\n\t\t\treturn \"\";\n\t\t}\n\n\t\tconst FFIType = _ffi.FFIType;\n\t\tconst jsonPath = findLib(\"buntok_json\");\n\n\t\tif (jsonPath) {\n\t\t\t_nativeJsonLib = _ffi.dlopen(jsonPath, {\n\t\t\t\tjson_init: { args: [], returns: FFIType.void },\n\t\t\t\tjson_deinit: { args: [], returns: FFIType.void },\n\t\t\t\tjson_escape_string: {\n\t\t\t\t\targs: [FFIType.ptr, FFIType.u32, FFIType.ptr, FFIType.ptr],\n\t\t\t\t\treturns: FFIType.i32,\n\t\t\t\t},\n\t\t\t});\n\t\t\t_nativeJsonLib.symbols.json_init();\n\t\t}\n\n\t\t_nativeAvailable = true;\n\t} catch {\n\t\t_nativeAvailable = false;\n\t}\n}\n\n// ─── Trie ────────────────────────────────────────────────────────────────────\n\n// Native trie removed - JS trie is faster when not doing double-walk.\n// Benchmark: native+JS was 38% SLOWER than JS-only due to syncing overhead.\n// JS engines optimize Map.get/set in native C++, so pure JS is optimal.\n\nexport class Trie {\n\tprivate jsImpl: JSTrie;\n\n\tconstructor() {\n\t\tthis.jsImpl = new JSTrie();\n\t}\n\n\tinsert(path: string, handlerId: number): void {\n\t\tthis.jsImpl.insert(path, handlerId);\n\t}\n\n\tfind(path: string): { handlerId: number; params: Record<string, string> } {\n\t\treturn this.jsImpl.find(path);\n\t}\n\n\tgetNodeCount(): number {\n\t\treturn this.jsImpl.getNodeCount();\n\t}\n}\n\n// ─── JSON Escape ─────────────────────────────────────────────────────────────\n\nexport function escapeString(str: string): string {\n\tif (_nativeAvailable && _nativeJsonLib) {\n\t\ttry {\n\t\t\tconst inputBuf = Buffer.from(str, \"utf-8\");\n\t\t\tconst outputPtrBuf = Buffer.alloc(8);\n\t\t\tconst outputLenBuf = Buffer.alloc(4);\n\n\t\t\tconst result = _nativeJsonLib.symbols.json_escape_string(\n\t\t\t\t_ffi.ptr(inputBuf),\n\t\t\t\tinputBuf.length,\n\t\t\t\t_ffi.ptr(outputPtrBuf),\n\t\t\t\t_ffi.ptr(outputLenBuf),\n\t\t\t);\n\n\t\t\tif (result === 0) {\n\t\t\t\tconst outLen = outputLenBuf.readUint32LE(0);\n\t\t\t\tconst outPtr = outputPtrBuf.readBigUInt64LE(0);\n\t\t\t\t// biome-ignore lint: reading native memory via FFI\n\t\t\t\tconst mem = new ArrayBuffer(Number(outPtr) + outLen);\n\t\t\t\tconst outBuf = Buffer.from(new Uint8Array(mem, Number(outPtr), outLen));\n\t\t\t\treturn outBuf.toString(\"utf-8\");\n\t\t\t}\n\t\t} catch {\n\t\t\t// Fall through to JS\n\t\t}\n\t}\n\treturn jsEscapeString(str);\n}\n\n// ─── Query Parser ────────────────────────────────────────────────────────────\n\nexport function parseQuery(query: string): Record<string, string> {\n\treturn jsParseQuery(query);\n}\n\n// ─── Cookie Parser ───────────────────────────────────────────────────────────\n\nexport function parseCookies(cookieHeader: string): Record<string, string> {\n\treturn jsParseCookies(cookieHeader);\n}\n\n// ─── Status ──────────────────────────────────────────────────────────────────\n\nexport function isNativeAvailable(): boolean {\n\ttryLoadNative();\n\treturn _nativeAvailable === true;\n}\n\nexport function getBackend(): \"native\" | \"js\" {\n\treturn isNativeAvailable() ? \"native\" : \"js\";\n}\n","// ─── JS Fallback: Route Trie ──────────────────────────────────────────────────\n\ntype NodeType = \"static\" | \"param\" | \"catchall\";\n\ninterface TrieNode {\n\tchildren: Map<string, TrieNode>;\n\tparamChild: TrieNode | null;\n\tcatchAllChild: TrieNode | null;\n\thandlerId: number;\n\tparamName: string | null;\n\tnodeType: NodeType;\n}\n\nfunction createNode(nodeType: NodeType = \"static\"): TrieNode {\n\treturn {\n\t\tchildren: new Map(),\n\t\tparamChild: null,\n\t\tcatchAllChild: null,\n\t\thandlerId: -1,\n\t\tparamName: null,\n\t\tnodeType,\n\t};\n}\n\nfunction splitPath(path: string): string[] {\n\tconst segments: string[] = [];\n\tlet start = path[0] === \"/\" ? 1 : 0;\n\tfor (let i = start; i < path.length; i++) {\n\t\tif (path[i] === \"/\") {\n\t\t\tif (i > start) {\n\t\t\t\tsegments.push(path.substring(start, i));\n\t\t\t}\n\t\t\tstart = i + 1;\n\t\t}\n\t}\n\tif (start < path.length) {\n\t\tsegments.push(path.substring(start));\n\t}\n\treturn segments;\n}\n\n// ─── JS Fallback: Trie Implementation ────────────────────────────────────────\n\nexport class JSTrie {\n\tprivate root: TrieNode;\n\tprivate nodeCount: number = 0;\n\n\tconstructor() {\n\t\tthis.root = createNode();\n\t}\n\n\tinsert(path: string, handlerId: number): void {\n\t\tconst segments = splitPath(path);\n\t\tlet current = this.root;\n\n\t\tfor (const segment of segments) {\n\t\t\tif (segment.length === 0) continue;\n\n\t\t\tlet nodeType: NodeType = \"static\";\n\t\t\tlet cleanSegment = segment;\n\t\t\tlet paramName: string | null = null;\n\n\t\t\tif (segment[0] === \":\") {\n\t\t\t\tnodeType = \"param\";\n\t\t\t\tparamName = segment.slice(1);\n\t\t\t\tcleanSegment = \":\";\n\t\t\t} else if (segment[0] === \"*\") {\n\t\t\t\tnodeType = \"catchall\";\n\t\t\t\tparamName = segment.slice(1);\n\t\t\t\tcleanSegment = \"*\";\n\t\t\t}\n\n\t\t\tlet nextNode: TrieNode | null = null;\n\n\t\t\tif (nodeType === \"static\") {\n\t\t\t\tnextNode = current.children.get(cleanSegment) ?? null;\n\t\t\t} else if (nodeType === \"param\") {\n\t\t\t\tnextNode = current.paramChild;\n\t\t\t} else {\n\t\t\t\tnextNode = current.catchAllChild;\n\t\t\t}\n\n\t\t\tif (!nextNode) {\n\t\t\t\tnextNode = createNode(nodeType);\n\t\t\t\tnextNode.paramName = paramName;\n\n\t\t\t\tif (nodeType === \"static\") {\n\t\t\t\t\tcurrent.children.set(cleanSegment, nextNode);\n\t\t\t\t} else if (nodeType === \"param\") {\n\t\t\t\t\tcurrent.paramChild = nextNode;\n\t\t\t\t} else {\n\t\t\t\t\tcurrent.catchAllChild = nextNode;\n\t\t\t\t}\n\t\t\t\tthis.nodeCount++;\n\t\t\t}\n\n\t\t\tcurrent = nextNode;\n\n\t\t\tif (nodeType === \"catchall\") break;\n\t\t}\n\n\t\tcurrent.handlerId = handlerId;\n\t}\n\n\tfind(path: string): { handlerId: number; params: Record<string, string> } {\n\t\tlet current = this.root;\n\t\tconst params: Record<string, string> = {};\n\t\tlet start = path[0] === \"/\" ? 1 : 0;\n\n\t\tif (start >= path.length) {\n\t\t\treturn { handlerId: current.handlerId, params };\n\t\t}\n\n\t\twhile (start < path.length) {\n\t\t\tlet end = path.indexOf(\"/\", start);\n\t\t\tif (end === -1) end = path.length;\n\n\t\t\tif (end > start) {\n\t\t\t\tconst segment = path.substring(start, end);\n\t\t\t\tlet nextNode = current.children.get(segment);\n\n\t\t\t\tif (!nextNode) {\n\t\t\t\t\tif (current.paramChild) {\n\t\t\t\t\t\tnextNode = current.paramChild;\n\t\t\t\t\t\tif (nextNode.paramName) {\n\t\t\t\t\t\t\tparams[nextNode.paramName] = segment;\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if (current.catchAllChild) {\n\t\t\t\t\t\tnextNode = current.catchAllChild;\n\t\t\t\t\t\tparams[\"*\"] = path.substring(start);\n\t\t\t\t\t\tcurrent = nextNode;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t} else {\n\t\t\t\t\t\treturn { handlerId: -1, params };\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tcurrent = nextNode;\n\t\t\t}\n\t\t\tstart = end + 1;\n\t\t}\n\n\t\treturn { handlerId: current.handlerId, params };\n\t}\n\n\tgetNodeCount(): number {\n\t\treturn this.nodeCount;\n\t}\n}\n\n// ─── JS Fallback: JSON Escape ────────────────────────────────────────────────\n\nexport function jsEscapeString(str: string): string {\n\treturn JSON.stringify(str);\n}\n\n// ─── JS Fallback: Query Parser ───────────────────────────────────────────────\n\nexport function jsParseQuery(query: string): Record<string, string> {\n\tif (!query) return {};\n\n\tconst params: Record<string, string> = {};\n\tconst pairs = query.split(\"&\");\n\n\tfor (const pair of pairs) {\n\t\tif (!pair) continue;\n\t\tconst eqIdx = pair.indexOf(\"=\");\n\t\tlet key: string;\n\t\tlet value: string;\n\n\t\tif (eqIdx === -1) {\n\t\t\tkey = pair;\n\t\t\tvalue = \"\";\n\t\t} else {\n\t\t\tkey = pair.substring(0, eqIdx);\n\t\t\tvalue = pair.substring(eqIdx + 1);\n\t\t}\n\n\t\tparams[decodeURIComponent(key)] = decodeURIComponent(value);\n\t}\n\n\treturn params;\n}\n\n// ─── JS Fallback: Cookie Parser ──────────────────────────────────────────────\n\nexport function jsParseCookies(cookieHeader: string): Record<string, string> {\n\tif (!cookieHeader) return {};\n\n\tconst cookies: Record<string, string> = {};\n\tconst pairs = cookieHeader.split(\";\");\n\n\tfor (const pair of pairs) {\n\t\tconst trimmed = pair.trim();\n\t\tif (!trimmed) continue;\n\n\t\tconst eqIdx = trimmed.indexOf(\"=\");\n\t\tlet name: string;\n\t\tlet value: string;\n\n\t\tif (eqIdx === -1) {\n\t\t\tname = trimmed;\n\t\t\tvalue = \"\";\n\t\t} else {\n\t\t\tname = trimmed.substring(0, eqIdx);\n\t\t\tvalue = trimmed.substring(eqIdx + 1);\n\t\t}\n\n\t\tcookies[name] = value;\n\t}\n\n\treturn cookies;\n}\n","export enum NodeType {\n\tSTATIC = 0,\n\tPARAM = 1,\n\tCATCHALL = 2,\n}\n\nexport interface RouteHandler {\n\tmethod: string;\n\t// biome-ignore lint/suspicious/noExplicitAny: generic router handler\n\thandler: (...args: any[]) => any;\n}\n\nexport class RouterNode {\n\tpath: string = \"\";\n\ttype: NodeType = NodeType.STATIC;\n\tstaticChildren: Map<string, RouterNode> = new Map();\n\tparamChild: RouterNode | null = null;\n\tcatchAllChild: RouterNode | null = null;\n\t// biome-ignore lint/suspicious/noExplicitAny: generic router handler\n\thandlers: Map<string, (...args: any[]) => any> = new Map();\n\tparamName: string | null = null;\n\n\tconstructor(path: string = \"\", type: NodeType = NodeType.STATIC) {\n\t\tthis.path = path;\n\t\tthis.type = type;\n\t}\n}\n","import type { Middleware } from \"./app\";\nimport type { Context } from \"./context\";\n\nconst textEncoder = new TextEncoder();\n\nfunction base64UrlEncode(buffer: ArrayBuffer | Uint8Array) {\n\treturn btoa(String.fromCharCode(...new Uint8Array(buffer)))\n\t\t.replace(/\\+/g, \"-\")\n\t\t.replace(/\\//g, \"_\")\n\t\t.replace(/=+$/, \"\");\n}\n\nfunction base64UrlDecode(str: string) {\n\tstr = str.replace(/-/g, \"+\").replace(/_/g, \"/\");\n\tconst pad = str.length % 4;\n\tif (pad) str += \"=\".repeat(4 - pad);\n\treturn Uint8Array.from(atob(str), (c) => c.charCodeAt(0));\n}\n\n/**\n * Zero-dependency, ultra-fast JWT implementation using WebCrypto API\n */\nexport class JwtService {\n\tconstructor(private secret: string) {}\n\n\tasync sign(\n\t\tpayload: Record<string, unknown>,\n\t\texpiresInSeconds?: number,\n\t): Promise<string> {\n\t\tconst header = base64UrlEncode(\n\t\t\ttextEncoder.encode(JSON.stringify({ alg: \"HS256\", typ: \"JWT\" })),\n\t\t);\n\n\t\tconst exp = expiresInSeconds\n\t\t\t? Math.floor(Date.now() / 1000) + expiresInSeconds\n\t\t\t: undefined;\n\t\tconst data = base64UrlEncode(\n\t\t\ttextEncoder.encode(JSON.stringify({ ...payload, exp })),\n\t\t);\n\n\t\tconst key = await crypto.subtle.importKey(\n\t\t\t\"raw\",\n\t\t\ttextEncoder.encode(this.secret),\n\t\t\t{ name: \"HMAC\", hash: \"SHA-256\" },\n\t\t\tfalse,\n\t\t\t[\"sign\"],\n\t\t);\n\n\t\tconst signature = await crypto.subtle.sign(\n\t\t\t\"HMAC\",\n\t\t\tkey,\n\t\t\ttextEncoder.encode(`${header}.${data}`),\n\t\t);\n\t\treturn `${header}.${data}.${base64UrlEncode(signature)}`;\n\t}\n\n\tasync verify<T = Record<string, unknown>>(token: string): Promise<T | null> {\n\t\tconst parts = token.split(\".\");\n\t\tif (parts.length !== 3) return null;\n\n\t\ttry {\n\t\t\tconst key = await crypto.subtle.importKey(\n\t\t\t\t\"raw\",\n\t\t\t\ttextEncoder.encode(this.secret),\n\t\t\t\t{ name: \"HMAC\", hash: \"SHA-256\" },\n\t\t\t\tfalse,\n\t\t\t\t[\"verify\"],\n\t\t\t);\n\n\t\t\tconst signature = base64UrlDecode(parts[2]!);\n\t\t\tconst valid = await crypto.subtle.verify(\n\t\t\t\t\"HMAC\",\n\t\t\t\tkey,\n\t\t\t\tsignature,\n\t\t\t\ttextEncoder.encode(`${parts[0]}.${parts[1]}`),\n\t\t\t);\n\n\t\t\tif (!valid) return null;\n\n\t\t\tconst payload = JSON.parse(\n\t\t\t\tnew TextDecoder().decode(base64UrlDecode(parts[1]!)),\n\t\t\t);\n\n\t\t\t// Check expiration\n\t\t\tif (payload.exp && Math.floor(Date.now() / 1000) > payload.exp) {\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\treturn payload as T;\n\t\t} catch (_e) {\n\t\t\treturn null;\n\t\t}\n\t}\n}\n\n/**\n * Middleware that extracts JWT, verifies it, and injects user payload into `ctx.user`.\n * Returns 401 if unauthorized.\n *\n * Auth source is configured via environment variables:\n * - `AUTH_STORE=header` (default): Read from Authorization: Bearer <token> header\n * - `AUTH_STORE=cookie`: Read from HttpOnly cookie (name from AUTH_COOKIE env var)\n * - Falls back to header if cookie not found\n */\nexport function requireAuth(\n\tsecret: string,\n): Middleware<Record<string, unknown>> {\n\tconst jwt = new JwtService(secret);\n\n\treturn async (\n\t\tctx: Context<Record<string, unknown>>,\n\t\tnext: () => Promise<Response> | Response,\n\t) => {\n\t\tlet token: string | undefined;\n\t\tconst authStore = process.env.AUTH_STORE ?? \"header\";\n\n\t\t// Cookie-based auth\n\t\tif (authStore === \"cookie\") {\n\t\t\tconst cookieName = process.env.AUTH_COOKIE;\n\t\t\tif (cookieName) {\n\t\t\t\ttoken = ctx.getCookie(cookieName);\n\t\t\t}\n\t\t}\n\n\t\t// Fallback to Authorization header (always try, even if AUTH_STORE=cookie)\n\t\tif (!token) {\n\t\t\tconst authHeader = ctx.request.headers.get(\"Authorization\");\n\t\t\tif (authHeader?.startsWith(\"Bearer \")) {\n\t\t\t\ttoken = authHeader.split(\" \")[1];\n\t\t\t}\n\t\t}\n\n\t\tif (!token) {\n\t\t\treturn ctx.json(\n\t\t\t\t{ error: \"Unauthorized\", message: \"Missing or invalid authentication token\" },\n\t\t\t\t401,\n\t\t\t);\n\t\t}\n\n\t\tconst user = await jwt.verify(token);\n\n\t\tif (!user) {\n\t\t\treturn ctx.json(\n\t\t\t\t{ error: \"Unauthorized\", message: \"Token is invalid or expired\" },\n\t\t\t\t401,\n\t\t\t);\n\t\t}\n\n\t\t// Inject user into context (buntok style)\n\t\tctx.user = user;\n\n\t\treturn next();\n\t};\n}\n","// ─── Types ────────────────────────────────────────────────────────────────────\nexport type {\n  OAuth2Tokens,\n  OAuthUser,\n  OAuthProviderConfig,\n  OAuthProvider,\n  CreateAuthorizationURLOptions,\n  ValidateAuthorizationCodeOptions,\n  AppleProviderConfig,\n} from \"./types\";\n\nexport {\n  OAuthError,\n  OAuthStateError,\n  OAuthTokenError,\n  OAuthProviderError,\n} from \"./types\";\n\n// ─── PKCE ─────────────────────────────────────────────────────────────────────\nexport {\n  generateCodeVerifier,\n  generateCodeChallenge,\n  generatePKCE,\n} from \"./pkce\";\n\n// ─── State Management ─────────────────────────────────────────────────────────\nexport {\n  storeOAuthState,\n  verifyOAuthState,\n  getCodeVerifier,\n  clearOAuthCookies,\n} from \"./state\";\n\n// ─── Base Provider ────────────────────────────────────────────────────────────\nexport { BaseOAuthProvider } from \"./provider\";\n\n// ─── Generic Helpers ──────────────────────────────────────────────────────────\nexport {\n  createOAuth2AuthorizationURL,\n  validateOAuth2AuthorizationCode,\n  decodeIdToken,\n} from \"./helpers\";\n\n// ─── Built-in Providers ───────────────────────────────────────────────────────\nexport { GoogleProvider, type GoogleUser } from \"./providers/google\";\nexport { GitHubProvider, type GitHubUser } from \"./providers/github\";\nexport { AppleProvider, type AppleUser } from \"./providers/apple\";\n\n// ─── Convenience Factory ──────────────────────────────────────────────────────\nimport type { OAuthProviderConfig, AppleProviderConfig } from \"./types\";\nimport { GoogleProvider } from \"./providers/google\";\nimport { GitHubProvider } from \"./providers/github\";\nimport { AppleProvider } from \"./providers/apple\";\n\n/**\n * Create OAuth providers with a simple factory API\n *\n * @example\n * ```ts\n * import { createOAuth } from \"@buntok/core\";\n *\n * const google = createOAuth.google({\n *   clientId: process.env.GOOGLE_CLIENT_ID!,\n *   clientSecret: process.env.GOOGLE_CLIENT_SECRET!,\n *   redirectURI: \"http://localhost:1212/auth/google/callback\",\n * });\n * ```\n */\nexport const createOAuth = {\n  google: (config: OAuthProviderConfig) => new GoogleProvider(config),\n  github: (config: OAuthProviderConfig) => new GitHubProvider(config),\n  apple: (config: AppleProviderConfig) => new AppleProvider(config),\n};\n","// ─── Token Types ──────────────────────────────────────────────────────────────\n\nexport interface OAuth2Tokens {\n  accessToken: string;\n  refreshToken?: string;\n  idToken?: string;\n  expiresAt?: number;\n  tokenType?: string;\n  scope?: string;\n}\n\n// ─── User Types ───────────────────────────────────────────────────────────────\n\nexport interface OAuthUser {\n  id: string;\n  name?: string;\n  email?: string;\n  emailVerified?: boolean;\n  image?: string;\n  [key: string]: unknown;\n}\n\n// ─── Provider Config ──────────────────────────────────────────────────────────\n\nexport interface OAuthProviderConfig {\n  clientId: string;\n  clientSecret: string;\n  redirectURI: string;\n  scopes?: string[];\n}\n\n// ─── Provider Interface ───────────────────────────────────────────────────────\n\nexport interface OAuthProvider {\n  readonly id: string;\n  createAuthorizationURL(state: string, codeVerifier: string): Promise<string> | string;\n  validateAuthorizationCode(\n    code: string,\n    redirectURI: string,\n    codeVerifier?: string,\n  ): Promise<OAuth2Tokens>;\n  getUserInfo(tokens: OAuth2Tokens): Promise<OAuthUser>;\n}\n\n// ─── Generic OAuth Helpers ────────────────────────────────────────────────────\n\nexport interface CreateAuthorizationURLOptions {\n  clientId: string;\n  redirectURI: string;\n  scopes?: string[];\n  state: string;\n  codeChallenge?: string;\n  additionalParams?: Record<string, string>;\n}\n\nexport interface ValidateAuthorizationCodeOptions {\n  code: string;\n  redirectURI: string;\n  clientId: string;\n  clientSecret: string;\n  codeVerifier?: string;\n  tokenEndpoint: string;\n}\n\n// ─── Apple-Specific Types ─────────────────────────────────────────────────────\n\nexport interface AppleProviderConfig extends OAuthProviderConfig {\n  teamId: string;\n  keyId: string;\n  privateKey: string;\n}\n\n// ─── Error Classes ────────────────────────────────────────────────────────────\n\nexport class OAuthError extends Error {\n  public readonly code: string;\n  public readonly provider: string;\n\n  constructor(message: string, code: string, provider: string) {\n    super(message);\n    this.name = \"OAuthError\";\n    this.code = code;\n    this.provider = provider;\n  }\n}\n\nexport class OAuthStateError extends OAuthError {\n  constructor(provider: string) {\n    super(\"OAuth state mismatch - possible CSRF attack\", \"STATE_MISMATCH\", provider);\n    this.name = \"OAuthStateError\";\n  }\n}\n\nexport class OAuthTokenError extends OAuthError {\n  constructor(message: string, provider: string) {\n    super(message, \"TOKEN_ERROR\", provider);\n    this.name = \"OAuthTokenError\";\n  }\n}\n\nexport class OAuthProviderError extends OAuthError {\n  public readonly providerError?: string;\n  public readonly providerErrorDescription?: string;\n\n  constructor(\n    message: string,\n    provider: string,\n    providerError?: string,\n    providerErrorDescription?: string,\n  ) {\n    super(message, \"PROVIDER_ERROR\", provider);\n    this.name = \"OAuthProviderError\";\n    this.providerError = providerError;\n    this.providerErrorDescription = providerErrorDescription;\n  }\n}\n","// ─── PKCE Helpers (RFC 7636) ───────────────────────────────────────────────────\n// Uses Web Crypto API (built-in Bun/Node 18+)\n\nconst CHARSET =\n  \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~\";\n\n/**\n * Generate a cryptographically random code verifier (RFC 7636 §4.1)\n * Length: 43-128 characters, using unreserved characters only\n */\nexport function generateCodeVerifier(length: number = 43): string {\n  if (length < 43 || length > 128) {\n    throw new RangeError(\"Code verifier length must be between 43 and 128\");\n  }\n\n  const bytes = new Uint8Array(length);\n  crypto.getRandomValues(bytes);\n\n  let verifier = \"\";\n  for (let i = 0; i < length; i++) {\n    verifier += CHARSET[bytes[i]! % CHARSET.length];\n  }\n  return verifier;\n}\n\n/**\n * Compute S256 code challenge from a code verifier (RFC 7636 §4.2)\n * code_challenge = BASE64URL(SHA256(code_verifier))\n */\nexport async function generateCodeChallenge(verifier: string): Promise<string> {\n  const encoder = new TextEncoder();\n  const data = encoder.encode(verifier);\n  const digest = await crypto.subtle.digest(\"SHA-256\", data);\n  return base64UrlEncode(new Uint8Array(digest));\n}\n\n/**\n * Generate PKCE code verifier + challenge pair\n */\nexport async function generatePKCE(): Promise<{\n  codeVerifier: string;\n  codeChallenge: string;\n}> {\n  const codeVerifier = generateCodeVerifier();\n  const codeChallenge = await generateCodeChallenge(codeVerifier);\n  return { codeVerifier, codeChallenge };\n}\n\n// ─── Base64URL Encoding ───────────────────────────────────────────────────────\n\nfunction base64UrlEncode(buffer: Uint8Array): string {\n  let binary = \"\";\n  for (let i = 0; i < buffer.length; i++) {\n    binary += String.fromCharCode(buffer[i]!);\n  }\n  return btoa(binary).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n","import {\n  setCookie,\n  getCookie,\n  deleteCookie,\n} from \"../helpers/cookie\";\n\n// ─── Cookie Names ─────────────────────────────────────────────────────────────\n\nconst OAUTH_STATE_COOKIE = \"__buntok_oauth_state\";\nconst OAUTH_VERIFIER_COOKIE = \"__buntok_oauth_verifier\";\n\n// ─── State Management ─────────────────────────────────────────────────────────\n\n/**\n * Store OAuth state + PKCE code verifier in HttpOnly cookies\n */\nexport function storeOAuthState(\n  response: Response,\n  state: string,\n  codeVerifier: string,\n): Response {\n  let res = setCookie(response, OAUTH_STATE_COOKIE, state, {\n    httpOnly: true,\n    secure: true,\n    sameSite: \"lax\",\n    path: \"/\",\n    maxAge: 600, // 10 minutes\n  });\n\n  res = setCookie(res, OAUTH_VERIFIER_COOKIE, codeVerifier, {\n    httpOnly: true,\n    secure: true,\n    sameSite: \"lax\",\n    path: \"/\",\n    maxAge: 600,\n  });\n\n  return res;\n}\n\n/**\n * Verify OAuth state matches (prevents CSRF)\n */\nexport function verifyOAuthState(\n  request: Request,\n  expectedState: string,\n): boolean {\n  const storedState = getCookie(request, OAUTH_STATE_COOKIE);\n  if (!storedState) return false;\n  return storedState === expectedState;\n}\n\n/**\n * Get PKCE code verifier from cookie\n */\nexport function getCodeVerifier(request: Request): string | undefined {\n  return getCookie(request, OAUTH_VERIFIER_COOKIE);\n}\n\n/**\n * Clear OAuth cookies (called automatically after callback)\n */\nexport function clearOAuthCookies(response: Response): Response {\n  let res = deleteCookie(response, OAUTH_STATE_COOKIE, {\n    httpOnly: true,\n    secure: true,\n    sameSite: \"lax\",\n    path: \"/\",\n  });\n\n  res = deleteCookie(res, OAUTH_VERIFIER_COOKIE, {\n    httpOnly: true,\n    secure: true,\n    sameSite: \"lax\",\n    path: \"/\",\n  });\n\n  return res;\n}\n","export interface CookieOptions {\n\t/** Domain to set the cookie on */\n\tdomain?: string;\n\t/** Expiration time in seconds */\n\tmaxAge?: number;\n\t/** Absolute expiration date */\n\texpires?: Date;\n\t/** Cookie path (default: /) */\n\tpath?: string;\n\t/** Secure flag (HTTPS only) */\n\tsecure?: boolean;\n\t/** HttpOnly flag (not accessible via JavaScript) */\n\thttpOnly?: boolean;\n\t/** SameSite policy */\n\tsameSite?: \"strict\" | \"lax\" | \"none\";\n\t/** Cookie partition key (for partitioned cookies) */\n\tpartitioned?: boolean;\n}\n\n/**\n * Parse cookies from Cookie header string\n */\nexport function parseCookies(cookieHeader: string): Record<string, string> {\n\tconst cookies: Record<string, string> = {};\n\n\tif (!cookieHeader) return cookies;\n\n\tconst pairs = cookieHeader.split(\";\");\n\n\tfor (const pair of pairs) {\n\t\tconst [name, ...valueParts] = pair.split(\"=\");\n\t\tif (name === undefined) continue;\n\t\tconst trimmedName = name.trim();\n\t\tconst value = valueParts.join(\"=\").trim();\n\n\t\tif (trimmedName) {\n\t\t\tcookies[trimmedName] = decodeURIComponent(value);\n\t\t}\n\t}\n\n\treturn cookies;\n}\n\n/**\n * Serialize a cookie to a string\n */\nexport function serializeCookie(\n\tname: string,\n\tvalue: string,\n\toptions: CookieOptions = {},\n): string {\n\tlet cookie = `${name}=${encodeURIComponent(value)}`;\n\n\tif (options.domain) {\n\t\tcookie += `; Domain=${options.domain}`;\n\t}\n\n\tif (options.maxAge !== undefined) {\n\t\tcookie += `; Max-Age=${options.maxAge}`;\n\t}\n\n\tif (options.expires) {\n\t\tcookie += `; Expires=${options.expires.toUTCString()}`;\n\t}\n\n\tcookie += `; Path=${options.path || \"/\"}`;\n\n\tif (options.secure) {\n\t\tcookie += \"; Secure\";\n\t}\n\n\tif (options.httpOnly) {\n\t\tcookie += \"; HttpOnly\";\n\t}\n\n\tif (options.sameSite) {\n\t\tcookie += `; SameSite=${options.sameSite.charAt(0).toUpperCase() + options.sameSite.slice(1)}`;\n\t}\n\n\tif (options.partitioned) {\n\t\tcookie += \"; Partitioned\";\n\t}\n\n\treturn cookie;\n}\n\n/**\n * Get a cookie value from request\n */\nexport function getCookie(request: Request, name: string): string | undefined {\n\tconst cookieHeader = request.headers.get(\"Cookie\");\n\tif (!cookieHeader) return undefined;\n\n\tconst cookies = parseCookies(cookieHeader);\n\treturn cookies[name];\n}\n\n/**\n * Get all cookies from request\n */\nexport function getCookies(request: Request): Record<string, string> {\n\tconst cookieHeader = request.headers.get(\"Cookie\");\n\treturn parseCookies(cookieHeader || \"\");\n}\n\n/**\n * Set a cookie on Response\n */\nexport function setCookie(\n\tresponse: Response,\n\tname: string,\n\tvalue: string,\n\toptions: CookieOptions = {},\n): Response {\n\tconst cookie = serializeCookie(name, value, options);\n\n\t// Clone response headers into a new Headers object to avoid\n\t// the body-stream being consumed by the Response constructor\n\t// when the original response's body is already locked.\n\tconst newHeaders = new Headers();\n\tresponse.headers.forEach((value, key) => {\n\t\tnewHeaders.set(key, value);\n\t});\n\tnewHeaders.append(\"Set-Cookie\", cookie);\n\n\treturn new Response(response.body, {\n\t\tstatus: response.status,\n\t\tstatusText: response.statusText,\n\t\theaders: newHeaders,\n\t});\n}\n\n/**\n * Delete a cookie (set expired)\n */\nexport function deleteCookie(\n\tresponse: Response,\n\tname: string,\n\toptions: Omit<CookieOptions, \"maxAge\" | \"expires\"> = {},\n): Response {\n\treturn setCookie(response, name, \"\", {\n\t\t...options,\n\t\tmaxAge: 0,\n\t\texpires: new Date(0),\n\t});\n}\n","import type {\n  OAuthProvider,\n  OAuthProviderConfig,\n  OAuth2Tokens,\n  OAuthUser,\n} from \"./types\";\n\n/**\n * Base class for OAuth providers.\n * Extend this to implement custom providers.\n */\nexport abstract class BaseOAuthProvider implements OAuthProvider {\n  abstract readonly id: string;\n\n  protected config: OAuthProviderConfig;\n\n  constructor(config: OAuthProviderConfig) {\n    this.config = config;\n  }\n\n  abstract createAuthorizationURL(\n    state: string,\n    codeVerifier: string,\n  ): Promise<string> | string;\n\n  abstract validateAuthorizationCode(\n    code: string,\n    redirectURI: string,\n    codeVerifier?: string,\n  ): Promise<OAuth2Tokens>;\n\n  abstract getUserInfo(tokens: OAuth2Tokens): Promise<OAuthUser>;\n}\n","import type {\n  CreateAuthorizationURLOptions,\n  ValidateAuthorizationCodeOptions,\n  OAuth2Tokens,\n} from \"./types\";\nimport { OAuthProviderError } from \"./types\";\n\n// ─── Create Authorization URL ─────────────────────────────────────────────────\n\n/**\n * Create an OAuth2 authorization URL for any provider\n */\nexport function createOAuth2AuthorizationURL(\n  authorizationEndpoint: string,\n  options: CreateAuthorizationURLOptions,\n): string {\n  const url = new URL(authorizationEndpoint);\n\n  url.searchParams.set(\"response_type\", \"code\");\n  url.searchParams.set(\"client_id\", options.clientId);\n  url.searchParams.set(\"redirect_uri\", options.redirectURI);\n  url.searchParams.set(\"state\", options.state);\n\n  if (options.scopes?.length) {\n    url.searchParams.set(\"scope\", options.scopes.join(\" \"));\n  }\n\n  if (options.codeChallenge) {\n    url.searchParams.set(\"code_challenge\", options.codeChallenge);\n    url.searchParams.set(\"code_challenge_method\", \"S256\");\n  }\n\n  if (options.additionalParams) {\n    for (const [key, value] of Object.entries(options.additionalParams)) {\n      url.searchParams.set(key, value);\n    }\n  }\n\n  return url.toString();\n}\n\n// ─── Validate Authorization Code ──────────────────────────────────────────────\n\n/**\n * Validate an OAuth2 authorization code with any provider's token endpoint\n */\nexport async function validateOAuth2AuthorizationCode(\n  options: ValidateAuthorizationCodeOptions,\n): Promise<OAuth2Tokens> {\n  const body = new URLSearchParams({\n    grant_type: \"authorization_code\",\n    code: options.code,\n    redirect_uri: options.redirectURI,\n    client_id: options.clientId,\n    client_secret: options.clientSecret,\n  });\n\n  if (options.codeVerifier) {\n    body.set(\"code_verifier\", options.codeVerifier);\n  }\n\n  const response = await fetch(options.tokenEndpoint, {\n    method: \"POST\",\n    headers: {\n      \"Content-Type\": \"application/x-www-form-urlencoded\",\n      Accept: \"application/json\",\n    },\n    body: body.toString(),\n  });\n\n  const data = (await response.json()) as Record<string, unknown>;\n\n  if (!response.ok) {\n    throw new OAuthProviderError(\n      `Token exchange failed: ${data.error_description || data.error || \"Unknown error\"}`,\n      \"unknown\",\n      data.error as string,\n      data.error_description as string,\n    );\n  }\n\n  return {\n    accessToken: data.access_token as string,\n    refreshToken: data.refresh_token as string | undefined,\n    idToken: data.id_token as string | undefined,\n    expiresAt: data.expires_in\n      ? Date.now() + (data.expires_in as number) * 1000\n      : undefined,\n    tokenType: data.token_type as string | undefined,\n    scope: data.scope as string | undefined,\n  };\n}\n\n// ─── Decode ID Token (JWT) ────────────────────────────────────────────────────\n\n/**\n * Decode an ID token (JWT) without verification.\n * For production, verify the signature using the provider's public keys.\n */\nexport function decodeIdToken(idToken: string): Record<string, unknown> {\n  const parts = idToken.split(\".\");\n  if (parts.length !== 3) {\n    throw new Error(\"Invalid ID token format\");\n  }\n\n  const payload = parts[1]!;\n  const decoded = atob(payload.replace(/-/g, \"+\").replace(/_/g, \"/\"));\n  return JSON.parse(decoded) as Record<string, unknown>;\n}\n","import { BaseOAuthProvider } from \"../provider\";\nimport { OAuthTokenError, OAuthProviderError } from \"../types\";\nimport { createOAuth2AuthorizationURL, decodeIdToken } from \"../helpers\";\nimport { generateCodeChallenge } from \"../pkce\";\nimport type {\n  OAuthProviderConfig,\n  OAuth2Tokens,\n  OAuthUser,\n} from \"../types\";\n\n// ─── Google OAuth2 / OIDC ─────────────────────────────────────────────────────\n// Docs: https://developers.google.com/identity/openid-connect/openid-connect\n\nconst AUTHORIZATION_ENDPOINT = \"https://accounts.google.com/o/oauth2/v2/auth\";\nconst TOKEN_ENDPOINT = \"https://oauth2.googleapis.com/token\";\nconst USERINFO_ENDPOINT = \"https://www.googleapis.com/oauth2/v3/userinfo\";\nconst ISSUER = \"https://accounts.google.com\";\n\nexport interface GoogleUser extends OAuthUser {\n  sub: string;\n  name?: string;\n  given_name?: string;\n  family_name?: string;\n  picture?: string;\n  email?: string;\n  email_verified?: boolean;\n  locale?: string;\n}\n\nexport class GoogleProvider extends BaseOAuthProvider {\n  readonly id = \"google\";\n\n  constructor(config: OAuthProviderConfig) {\n    super({\n      ...config,\n      scopes: config.scopes ?? [\"openid\", \"email\", \"profile\"],\n    });\n  }\n\n  async createAuthorizationURL(state: string, codeVerifier: string): Promise<string> {\n    const codeChallenge = await generateCodeChallenge(codeVerifier);\n    return createOAuth2AuthorizationURL(AUTHORIZATION_ENDPOINT, {\n      clientId: this.config.clientId,\n      redirectURI: this.config.redirectURI,\n      scopes: this.config.scopes,\n      state,\n      codeChallenge,\n    });\n  }\n\n  async validateAuthorizationCode(\n    code: string,\n    redirectURI: string,\n    codeVerifier?: string,\n  ): Promise<OAuth2Tokens> {\n    const body = new URLSearchParams({\n      grant_type: \"authorization_code\",\n      code,\n      redirect_uri: redirectURI,\n      client_id: this.config.clientId,\n      client_secret: this.config.clientSecret,\n    });\n\n    if (codeVerifier) {\n      body.set(\"code_verifier\", codeVerifier);\n    }\n\n    const response = await fetch(TOKEN_ENDPOINT, {\n      method: \"POST\",\n      headers: {\n        \"Content-Type\": \"application/x-www-form-urlencoded\",\n        Accept: \"application/json\",\n      },\n      body: body.toString(),\n    });\n\n    const data = (await response.json()) as Record<string, unknown>;\n\n    if (!response.ok) {\n      throw new OAuthTokenError(\n        `Google token exchange failed: ${data.error_description || data.error || \"Unknown error\"}`,\n        this.id,\n      );\n    }\n\n    return {\n      accessToken: data.access_token as string,\n      refreshToken: data.refresh_token as string | undefined,\n      idToken: data.id_token as string | undefined,\n      expiresAt: data.expires_in\n        ? Date.now() + (data.expires_in as number) * 1000\n        : undefined,\n      tokenType: data.token_type as string | undefined,\n      scope: data.scope as string | undefined,\n    };\n  }\n\n  async getUserInfo(tokens: OAuth2Tokens): Promise<GoogleUser> {\n    // Try to decode ID token first (OIDC - no extra API call needed)\n    if (tokens.idToken) {\n      const claims = decodeIdToken(tokens.idToken);\n\n      // Verify issuer\n      if (claims.iss !== ISSUER && claims.iss !== \"accounts.google.com\") {\n        throw new OAuthProviderError(\n          \"Invalid Google ID token issuer\",\n          this.id,\n          \"invalid_issuer\",\n        );\n      }\n\n      return {\n        id: claims.sub as string,\n        sub: claims.sub as string,\n        name: claims.name as string | undefined,\n        given_name: claims.given_name as string | undefined,\n        family_name: claims.family_name as string | undefined,\n        picture: claims.picture as string | undefined,\n        email: claims.email as string | undefined,\n        emailVerified: claims.email_verified as boolean | undefined,\n        locale: claims.locale as string | undefined,\n      };\n    }\n\n    // Fallback to userinfo endpoint\n    const response = await fetch(USERINFO_ENDPOINT, {\n      headers: {\n        Authorization: `Bearer ${tokens.accessToken}`,\n      },\n    });\n\n    if (!response.ok) {\n      throw new OAuthTokenError(\n        `Failed to fetch Google user info: ${response.status}`,\n        this.id,\n      );\n    }\n\n    const data = (await response.json()) as Record<string, unknown>;\n\n    return {\n      id: data.sub as string,\n      sub: data.sub as string,\n      name: data.name as string | undefined,\n      given_name: data.given_name as string | undefined,\n      family_name: data.family_name as string | undefined,\n      picture: data.picture as string | undefined,\n      email: data.email as string | undefined,\n      emailVerified: data.email_verified as boolean | undefined,\n      locale: data.locale as string | undefined,\n    };\n  }\n}\n","import { BaseOAuthProvider } from \"../provider\";\nimport { OAuthTokenError } from \"../types\";\nimport { createOAuth2AuthorizationURL } from \"../helpers\";\nimport type {\n  OAuthProviderConfig,\n  OAuth2Tokens,\n  OAuthUser,\n} from \"../types\";\n\n// ─── GitHub OAuth2 ────────────────────────────────────────────────────────────\n// Docs: https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps\n\nconst AUTHORIZATION_ENDPOINT = \"https://github.com/login/oauth/authorize\";\nconst TOKEN_ENDPOINT = \"https://github.com/login/oauth/access_token\";\nconst API_BASE = \"https://api.github.com\";\n\nexport interface GitHubUser extends OAuthUser {\n  login: string;\n  avatar_url?: string;\n  html_url?: string;\n  bio?: string;\n  company?: string;\n  location?: string;\n  blog?: string;\n}\n\nexport class GitHubProvider extends BaseOAuthProvider {\n  readonly id = \"github\";\n\n  constructor(config: OAuthProviderConfig) {\n    super({\n      ...config,\n      scopes: config.scopes ?? [\"user:email\"],\n    });\n  }\n\n  createAuthorizationURL(state: string, _codeVerifier: string): string {\n    // GitHub doesn't support PKCE (as of 2026)\n    return createOAuth2AuthorizationURL(AUTHORIZATION_ENDPOINT, {\n      clientId: this.config.clientId,\n      redirectURI: this.config.redirectURI,\n      scopes: this.config.scopes,\n      state,\n    });\n  }\n\n  async validateAuthorizationCode(\n    code: string,\n    _redirectURI: string,\n    _codeVerifier?: string,\n  ): Promise<OAuth2Tokens> {\n    // GitHub uses HTTP Basic Auth for token exchange\n    const credentials = btoa(\n      `${this.config.clientId}:${this.config.clientSecret}`,\n    );\n\n    const body = new URLSearchParams({\n      grant_type: \"authorization_code\",\n      code,\n    });\n\n    const response = await fetch(TOKEN_ENDPOINT, {\n      method: \"POST\",\n      headers: {\n        \"Content-Type\": \"application/x-www-form-urlencoded\",\n        Accept: \"application/json\",\n        Authorization: `Basic ${credentials}`,\n      },\n      body: body.toString(),\n    });\n\n    const data = (await response.json()) as Record<string, unknown>;\n\n    if (!response.ok) {\n      throw new OAuthTokenError(\n        `GitHub token exchange failed: ${data.error_description || data.error || \"Unknown error\"}`,\n        this.id,\n      );\n    }\n\n    return {\n      accessToken: data.access_token as string,\n      refreshToken: data.refresh_token as string | undefined,\n      tokenType: data.token_type as string | undefined,\n      scope: data.scope as string | undefined,\n    };\n  }\n\n  async getUserInfo(tokens: OAuth2Tokens): Promise<GitHubUser> {\n    // Fetch user profile\n    const userResponse = await fetch(`${API_BASE}/user`, {\n      headers: {\n        Authorization: `Bearer ${tokens.accessToken}`,\n        Accept: \"application/vnd.github+json\",\n      },\n    });\n\n    if (!userResponse.ok) {\n      throw new OAuthTokenError(\n        `Failed to fetch GitHub user: ${userResponse.status}`,\n        this.id,\n      );\n    }\n\n    const userData = (await userResponse.json()) as Record<string, unknown>;\n\n    // Fetch user emails (email may be private)\n    const emailsResponse = await fetch(`${API_BASE}/user/emails`, {\n      headers: {\n        Authorization: `Bearer ${tokens.accessToken}`,\n        Accept: \"application/vnd.github+json\",\n      },\n    });\n\n    let primaryEmail: string | undefined;\n    let emailVerified = false;\n\n    if (emailsResponse.ok) {\n      const emails = (await emailsResponse.json()) as Array<{\n        email: string;\n        primary: boolean;\n        verified: boolean;\n      }>;\n      const primary = emails.find((e) => e.primary);\n      if (primary) {\n        primaryEmail = primary.email;\n        emailVerified = primary.verified;\n      }\n    }\n\n    return {\n      id: String(userData.id),\n      name: (userData.name as string) || (userData.login as string),\n      email: primaryEmail,\n      emailVerified,\n      image: userData.avatar_url as string | undefined,\n      login: userData.login as string,\n      avatar_url: userData.avatar_url as string | undefined,\n      html_url: userData.html_url as string | undefined,\n      bio: userData.bio as string | undefined,\n      company: userData.company as string | undefined,\n      location: userData.location as string | undefined,\n      blog: userData.blog as string | undefined,\n    };\n  }\n}\n","import { BaseOAuthProvider } from \"../provider\";\nimport { OAuthTokenError, OAuthProviderError } from \"../types\";\nimport { createOAuth2AuthorizationURL, decodeIdToken } from \"../helpers\";\nimport { generateCodeChallenge } from \"../pkce\";\nimport type {\n  AppleProviderConfig,\n  OAuth2Tokens,\n  OAuthUser,\n} from \"../types\";\n\n// ─── Apple OIDC ───────────────────────────────────────────────────────────────\n// Docs: https://developer.apple.com/documentation/sign-in-with-apple\n// NOTE: Apple only sends name on FIRST authentication. Store it if you get it.\n\nconst AUTHORIZATION_ENDPOINT = \"https://appleid.apple.com/auth/authorize\";\nconst TOKEN_ENDPOINT = \"https://appleid.apple.com/auth/token\";\nconst ISSUER = \"https://appleid.apple.com\";\n\nexport interface AppleUser extends OAuthUser {\n  sub: string;\n  email?: string;\n  emailVerified?: boolean;\n  // Apple only sends these ONCE (first auth)\n  /** Raw Apple name object - only available on first authentication */\n  appleName?: {\n    firstName?: string;\n    lastName?: string;\n  };\n}\n\n// ─── JWT Helpers (for Apple client_secret) ─────────────────────────────────────\n\nfunction base64UrlEncode(data: string): string {\n  return btoa(data).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\nasync function importPrivateKey(privateKey: string): Promise<CryptoKey> {\n  // Clean the key\n  const cleaned = privateKey\n    .replace(/-----BEGIN PRIVATE KEY-----/, \"\")\n    .replace(/-----END PRIVATE KEY-----/, \"\")\n    .replace(/\\s+/g, \"\");\n\n  const binaryDer = Uint8Array.from(atob(cleaned), (c) => c.charCodeAt(0));\n\n  return crypto.subtle.importKey(\n    \"pkcs8\",\n    binaryDer,\n    { name: \"ECDSA\", namedCurve: \"P-256\" },\n    false,\n    [\"sign\"],\n  );\n}\n\nasync function signJWT(\n  payload: Record<string, unknown>,\n  privateKey: string,\n  keyId: string,\n): Promise<string> {\n  const header = {\n    alg: \"ES256\",\n    kid: keyId,\n  };\n\n  const encodedHeader = base64UrlEncode(JSON.stringify(header));\n  const encodedPayload = base64UrlEncode(JSON.stringify(payload));\n  const signingInput = `${encodedHeader}.${encodedPayload}`;\n\n  const key = await importPrivateKey(privateKey);\n  const encoder = new TextEncoder();\n  const signature = await crypto.subtle.sign(\n    { name: \"ECDSA\", hash: \"SHA-256\" },\n    key,\n    encoder.encode(signingInput),\n  );\n\n  // Convert DER signature to raw R||S format\n  const der = new Uint8Array(signature);\n  // DER: 0x30 [total] 0x02 [rLen] [r...] 0x02 [sLen] [s...]\n  const rLen = der[3]!;\n  const r = der.slice(4, 4 + rLen);\n  const sLen = der[5 + rLen]!;\n  const s = der.slice(6 + rLen, 6 + rLen + sLen);\n\n  // Pad R and S to 32 bytes each\n  const rPadded = new Uint8Array(32);\n  const sPadded = new Uint8Array(32);\n  rPadded.set(r.slice(-32), 32 - Math.min(r.length, 32));\n  sPadded.set(s.slice(-32), 32 - Math.min(s.length, 32));\n\n  // Concatenate R || S\n  const rawSignature = new Uint8Array(64);\n  rawSignature.set(rPadded, 0);\n  rawSignature.set(sPadded, 32);\n\n  const encodedSignature = base64UrlEncode(\n    String.fromCharCode(...rawSignature),\n  );\n\n  return `${signingInput}.${encodedSignature}`;\n}\n\n// ─── Apple Provider ───────────────────────────────────────────────────────────\n\nexport class AppleProvider extends BaseOAuthProvider {\n  readonly id = \"apple\";\n\n  private teamId: string;\n  private keyId: string;\n  private privateKey: string;\n\n  constructor(config: AppleProviderConfig) {\n    super({\n      ...config,\n      scopes: config.scopes ?? [\"name\", \"email\"],\n    });\n    this.teamId = config.teamId;\n    this.keyId = config.keyId;\n    this.privateKey = config.privateKey;\n  }\n\n  /**\n   * Generate client_secret JWT (required by Apple)\n   */\n  private async generateClientSecret(): Promise<string> {\n    const now = Math.floor(Date.now() / 1000);\n\n    return signJWT(\n      {\n        iss: this.teamId,\n        iat: now,\n        exp: now + 15777000, // 6 months (Apple max)\n        aud: ISSUER,\n        sub: this.config.clientId,\n      },\n      this.privateKey,\n      this.keyId,\n    );\n  }\n\n  async createAuthorizationURL(state: string, codeVerifier: string): Promise<string> {\n    // Apple uses a space-separated scope (not comma)\n    const scopes = this.config.scopes?.join(\" \") ?? \"name email\";\n    const codeChallenge = await generateCodeChallenge(codeVerifier);\n\n    return createOAuth2AuthorizationURL(AUTHORIZATION_ENDPOINT, {\n      clientId: this.config.clientId,\n      redirectURI: this.config.redirectURI,\n      scopes: [scopes],\n      state,\n      codeChallenge,\n      additionalParams: {\n        response_mode: \"form_post\",\n      },\n    });\n  }\n\n  async validateAuthorizationCode(\n    code: string,\n    redirectURI: string,\n    codeVerifier?: string,\n  ): Promise<OAuth2Tokens> {\n    const clientSecret = await this.generateClientSecret();\n\n    const body = new URLSearchParams({\n      grant_type: \"authorization_code\",\n      code,\n      redirect_uri: redirectURI,\n      client_id: this.config.clientId,\n      client_secret: clientSecret,\n    });\n\n    if (codeVerifier) {\n      body.set(\"code_verifier\", codeVerifier);\n    }\n\n    const response = await fetch(TOKEN_ENDPOINT, {\n      method: \"POST\",\n      headers: {\n        \"Content-Type\": \"application/x-www-form-urlencoded\",\n        Accept: \"application/json\",\n      },\n      body: body.toString(),\n    });\n\n    const data = (await response.json()) as Record<string, unknown>;\n\n    if (!response.ok) {\n      throw new OAuthTokenError(\n        `Apple token exchange failed: ${data.error_description || data.error || \"Unknown error\"}`,\n        this.id,\n      );\n    }\n\n    return {\n      accessToken: data.access_token as string,\n      refreshToken: data.refresh_token as string | undefined,\n      idToken: data.id_token as string | undefined,\n      expiresAt: data.expires_in\n        ? Date.now() + (data.expires_in as number) * 1000\n        : undefined,\n      tokenType: data.token_type as string | undefined,\n      scope: data.scope as string | undefined,\n    };\n  }\n\n  async getUserInfo(tokens: OAuth2Tokens): Promise<AppleUser> {\n    if (!tokens.idToken) {\n      throw new OAuthProviderError(\n        \"Apple did not return an ID token\",\n        this.id,\n        \"no_id_token\",\n      );\n    }\n\n    const claims = decodeIdToken(tokens.idToken);\n\n    // Verify issuer\n    if (claims.iss !== ISSUER) {\n      throw new OAuthProviderError(\n        \"Invalid Apple ID token issuer\",\n        this.id,\n        \"invalid_issuer\",\n      );\n    }\n\n    // Verify audience\n    if (claims.aud !== this.config.clientId) {\n      throw new OAuthProviderError(\n        \"Invalid Apple ID token audience\",\n        this.id,\n        \"invalid_audience\",\n      );\n    }\n\n    return {\n      id: claims.sub as string,\n      sub: claims.sub as string,\n      email: claims.email as string | undefined,\n      emailVerified: claims.email_verified as boolean | undefined,\n      // Apple only sends name on FIRST auth - may be undefined\n      appleName: claims.name as AppleUser[\"appleName\"],\n    };\n  }\n}\n","import type { Context } from \"./context\";\nimport { Controller, Delete, Get, Post, Put } from \"./decorators\";\n\n/**\n * Abstract base controller with built-in CRUD routes using decorators.\n *\n * Extend this class and provide a route prefix via @Controller decorator.\n * The CRUD methods are pre-decorated with @Get, @Post, @Put, @Delete.\n *\n * @example\n * ```ts\n * import { Controller, BaseController, Context } from \"@buntok/core\";\n *\n * // Simple usage (no input types needed)\n * @Controller(\"/users\")\n * class UserController extends BaseController<User> {\n *   constructor(private userService: UserService) {\n *     super(userService);\n *   }\n * }\n *\n * // With typed inputs (optional)\n * @Controller(\"/users\")\n * class UserController extends BaseController<User, CreateUserInput, UpdateUserInput> {\n *   constructor(private userService: UserService) {\n *     super(userService);\n *   }\n * }\n *\n * // Routes automatically registered:\n * // GET    /users\n * // GET    /users/:id\n * // POST   /users\n * // PUT    /users/:id\n * // DELETE /users/:id\n * ```\n */\n@Controller(\"\")\nexport abstract class BaseController<T, CreateInput = any, UpdateInput = any> {\n\tconstructor(\n\t\tprotected service: {\n\t\t\tgetAll: () => Promise<T[]>;\n\t\t\tgetById: (id: string | number) => Promise<T>;\n\t\t\tcreate: (data: CreateInput) => Promise<T>;\n\t\t\tupdate: (id: string | number, data: UpdateInput) => Promise<T>;\n\t\t\tdelete: (id: string | number) => Promise<T>;\n\t\t},\n\t) {}\n\n\t/**\n\t * Parse URL param id to the correct type.\n\t * Default: tries to convert to number, falls back to string.\n\t * Override this if you need custom id parsing (e.g., UUID).\n\t */\n\tprotected parseId(id: string): string | number {\n\t\tif (id === \"\") return id;\n\t\tconst num = Number(id);\n\t\treturn Number.isNaN(num) ? id : num;\n\t}\n\n\t@Get(\"/\")\n\tasync getAll(ctx: Context) {\n\t\tconst data = await this.service.getAll();\n\t\treturn ctx.success(data);\n\t}\n\n\t@Get(\"/:id\")\n\tasync getById(ctx: Context) {\n\t\tconst id = this.parseId(ctx.params.id ?? \"\");\n\t\tconst data = await this.service.getById(id);\n\t\treturn ctx.success(data);\n\t}\n\n\t@Post(\"/\")\n\tasync create(ctx: Context) {\n\t\tconst body = await ctx.body();\n\t\tconst data = await this.service.create(body as CreateInput);\n\t\treturn ctx.success(data, `Created successfully!.`, 201);\n\t}\n\n\t@Put(\"/:id\")\n\tasync update(ctx: Context) {\n\t\tconst id = this.parseId(ctx.params.id ?? \"\");\n\t\tconst body = await ctx.body();\n\t\tconst data = await this.service.update(id, body as UpdateInput);\n\t\treturn ctx.success(data);\n\t}\n\n\t@Delete(\"/:id\")\n\tasync delete(ctx: Context) {\n\t\tconst id = this.parseId(ctx.params.id ?? \"\");\n\t\tawait this.service.delete(id);\n\t\treturn ctx.status(204);\n\t}\n}\n","import { NotFoundError } from \"./helpers/async-handler\";\n\nexport abstract class BaseService<T, CreateInput = any, UpdateInput = any> {\n\tconstructor(\n\t\tprotected repository: {\n\t\t\tfindAll: () => Promise<T[]>;\n\t\t\tfindById: (id: string | number) => Promise<T | null>;\n\t\t\tcreate: (data: CreateInput) => Promise<T>;\n\t\t\tupdate: (id: string | number, data: UpdateInput) => Promise<T>;\n\t\t\tdelete: (id: string | number) => Promise<T>;\n\t\t\tcount: () => Promise<number>;\n\t\t},\n\t) {}\n\n\tasync getAll(): Promise<T[]> {\n\t\treturn this.repository.findAll();\n\t}\n\n\tasync getById(id: string | number): Promise<T> {\n\t\tconst item = await this.repository.findById(id);\n\t\tif (!item) throw new NotFoundError(`Data dengan id ${id} tidak ditemukan`);\n\t\treturn item;\n\t}\n\n\tasync create(data: CreateInput): Promise<T> {\n\t\treturn this.repository.create(data);\n\t}\n\n\tasync update(id: string | number, data: UpdateInput): Promise<T> {\n\t\tawait this.getById(id);\n\t\treturn this.repository.update(id, data);\n\t}\n\n\tasync delete(id: string | number): Promise<T> {\n\t\tawait this.getById(id);\n\t\treturn this.repository.delete(id);\n\t}\n\n\tasync count(): Promise<number> {\n\t\treturn this.repository.count();\n\t}\n}\n","export interface CacheDriver {\n\tget<T>(key: string): Promise<T | null>;\n\tset<T>(key: string, value: T, ttlSeconds?: number): Promise<void>;\n\tdelete(key: string): Promise<void>;\n\tclear(): Promise<void>;\n\t/** Optional: list all stored keys (used by deletePattern / keys) */\n\tkeys?(): Promise<string[]>;\n}\n\nexport class MemoryCacheDriver implements CacheDriver {\n\tprivate store = new Map<string, { value: unknown; expiry: number | null }>();\n\n\tasync get<T>(key: string): Promise<T | null> {\n\t\tconst item = this.store.get(key);\n\t\tif (!item) return null;\n\t\tif (item.expiry && Date.now() > item.expiry) {\n\t\t\tthis.store.delete(key);\n\t\t\treturn null;\n\t\t}\n\t\treturn item.value as T;\n\t}\n\n\tasync set<T>(key: string, value: T, ttlSeconds?: number): Promise<void> {\n\t\tconst expiry = ttlSeconds ? Date.now() + ttlSeconds * 1000 : null;\n\t\tthis.store.set(key, { value, expiry });\n\t}\n\n\tasync delete(key: string): Promise<void> {\n\t\tthis.store.delete(key);\n\t}\n\n\tasync clear(): Promise<void> {\n\t\tthis.store.clear();\n\t}\n\n\tasync keys(): Promise<string[]> {\n\t\tconst now = Date.now();\n\t\tconst result: string[] = [];\n\t\tfor (const [key, item] of this.store.entries()) {\n\t\t\tif (!item.expiry || now <= item.expiry) {\n\t\t\t\tresult.push(key);\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t}\n}\n\nexport class Cache {\n\tconstructor(private driver: CacheDriver = new MemoryCacheDriver()) {}\n\n\tasync get<T>(key: string): Promise<T | null> {\n\t\treturn this.driver.get<T>(key);\n\t}\n\n\tasync set<T>(key: string, value: T, ttlSeconds?: number): Promise<void> {\n\t\treturn this.driver.set(key, value, ttlSeconds);\n\t}\n\n\tasync delete(key: string): Promise<void> {\n\t\treturn this.driver.delete(key);\n\t}\n\n\tasync clear(): Promise<void> {\n\t\treturn this.driver.clear();\n\t}\n\n\t/**\n\t * Returns true if the key exists and has not expired.\n\t */\n\tasync has(key: string): Promise<boolean> {\n\t\treturn (await this.driver.get(key)) !== null;\n\t}\n\n\t/**\n\t * Get value from cache, or execute the factory function to compute and cache it.\n\t *\n\t * @example\n\t * const user = await cache.getOrSet(`user:${id}`, () => db.findUser(id), 300);\n\t */\n\tasync getOrSet<T>(\n\t\tkey: string,\n\t\tfactory: () => Promise<T>,\n\t\tttlSeconds?: number,\n\t): Promise<T> {\n\t\tconst existing = await this.get<T>(key);\n\t\tif (existing !== null) return existing;\n\t\tconst val = await factory();\n\t\tawait this.set(key, val, ttlSeconds);\n\t\treturn val;\n\t}\n\n\t/**\n\t * Atomically increment a numeric value stored at `key`.\n\t * If the key does not exist it is initialized to 0 then incremented.\n\t *\n\t * @param key - Cache key\n\t * @param amount - Amount to add (default: 1)\n\t * @param ttlSeconds - Optional TTL; only applied when the key is first created\n\t */\n\tasync increment(\n\t\tkey: string,\n\t\tamount = 1,\n\t\tttlSeconds?: number,\n\t): Promise<number> {\n\t\tconst current = (await this.get<number>(key)) ?? 0;\n\t\tconst next = current + amount;\n\t\tawait this.set(key, next, current === null ? ttlSeconds : undefined);\n\t\treturn next;\n\t}\n\n\t/**\n\t * Atomically decrement a numeric value stored at `key`.\n\t * If the key does not exist it is initialized to 0 then decremented.\n\t */\n\tasync decrement(\n\t\tkey: string,\n\t\tamount = 1,\n\t\tttlSeconds?: number,\n\t): Promise<number> {\n\t\treturn this.increment(key, -amount, ttlSeconds);\n\t}\n\n\t/**\n\t * Get multiple keys in a single call. Returns `null` for missing keys.\n\t *\n\t * @example\n\t * const [user, post] = await cache.mget([`user:1`, `post:42`]);\n\t */\n\tasync mget<T>(keys: string[]): Promise<(T | null)[]> {\n\t\treturn Promise.all(keys.map((k) => this.get<T>(k)));\n\t}\n\n\t/**\n\t * Set multiple key-value pairs with the same optional TTL.\n\t *\n\t * @example\n\t * await cache.mset([[\"user:1\", userData], [\"user:2\", user2Data]], 300);\n\t */\n\tasync mset<T>(entries: [string, T][], ttlSeconds?: number): Promise<void> {\n\t\tawait Promise.all(\n\t\t\tentries.map(([key, value]) => this.set(key, value, ttlSeconds)),\n\t\t);\n\t}\n\n\t/**\n\t * Delete all keys matching a glob-style pattern.\n\t * Requires the underlying driver to implement `keys()`.\n\t *\n\t * Supported wildcards:\n\t *  - `*` matches any sequence of characters\n\t *  - `?` matches any single character\n\t *\n\t * @example\n\t * await cache.deletePattern(\"session:*\");\n\t * await cache.deletePattern(\"tmp:??:*\");\n\t */\n\tasync deletePattern(pattern: string): Promise<number> {\n\t\tif (!this.driver.keys) {\n\t\t\tthrow new Error(\n\t\t\t\t\"deletePattern() requires the cache driver to implement keys(). \" +\n\t\t\t\t\t\"MemoryCacheDriver supports it; for Redis use SCAN + DEL.\",\n\t\t\t);\n\t\t}\n\t\tconst regex = this.globToRegex(pattern);\n\t\tconst allKeys = await this.driver.keys();\n\t\tconst matching = allKeys.filter((k) => regex.test(k));\n\t\tawait Promise.all(matching.map((k) => this.delete(k)));\n\t\treturn matching.length;\n\t}\n\n\t/**\n\t * List all keys currently stored (non-expired).\n\t * Requires the underlying driver to implement `keys()`.\n\t */\n\tasync keys(): Promise<string[]> {\n\t\tif (!this.driver.keys) {\n\t\t\tthrow new Error(\n\t\t\t\t\"keys() requires the cache driver to implement keys(). \" +\n\t\t\t\t\t\"MemoryCacheDriver supports it.\",\n\t\t\t);\n\t\t}\n\t\treturn this.driver.keys();\n\t}\n\n\t/** Convert a glob pattern to a RegExp */\n\tprivate globToRegex(pattern: string): RegExp {\n\t\tconst escaped = pattern\n\t\t\t.replace(/[.+^${}()|[\\]\\\\]/g, \"\\\\$&\") // escape regex specials\n\t\t\t.replace(/\\*/g, \".*\") // * → .*\n\t\t\t.replace(/\\?/g, \".\"); // ? → .\n\t\treturn new RegExp(`^${escaped}$`);\n\t}\n}\n","/**\n * Lightweight event emitter for loose coupling between modules.\n *\n * @example\n * ```ts\n * import { emitter } from \"buntok\";\n *\n * // Subscribe to events\n * emitter.on(\"user:created\", async ({ user, ctx }) => {\n *   await sendWelcomeEmail(user.email);\n * });\n *\n * // Emit events\n * emitter.emit(\"user:created\", { user, ctx });\n *\n * // One-time listener\n * emitter.once(\"app:ready\", () => {\n *   console.log(\"App is ready!\");\n * });\n * ```\n */\n\ntype EventMap = Record<string, any>;\n\ntype Listener<T> = (data: T) => Promise<void> | void;\n\nexport interface EmitOptions {\n\t/**\n\t * Isolate errors per listener. When true, a failing listener won't reject\n\t * the entire emit() call or affect other listeners.\n\t * Defaults to false for backward compatibility.\n\t */\n\tisolatedErrors?: boolean;\n\n\t/**\n\t * Custom error handler called when a listener throws.\n\t * Only used when `isolatedErrors` is true.\n\t */\n\tonError?: (event: string, error: unknown, listener: Listener<any>) => void;\n}\n\nexport interface EventEmitterOptions {\n\t/**\n\t * Maximum number of listeners per event.\n\t * Set to 0 or undefined for unlimited (default).\n\t * When exceeded, `on()` throws an error.\n\t */\n\tmaxListeners?: number;\n}\n\nexport class EventEmitter<TEvents extends EventMap = EventMap> {\n\tprivate listeners = new Map<keyof TEvents, Set<Listener<any>>>();\n\tprivate maxListeners: number;\n\n\tconstructor(options?: EventEmitterOptions) {\n\t\tthis.maxListeners = options?.maxListeners ?? 0;\n\t}\n\n\t/**\n\t * Subscribe to an event.\n\t * @returns Unsubscribe function\n\t */\n\ton<K extends keyof TEvents>(event: K, listener: Listener<TEvents[K]>): () => void {\n\t\tif (!this.listeners.has(event)) {\n\t\t\tthis.listeners.set(event, new Set());\n\t\t}\n\n\t\tconst eventListeners = this.listeners.get(event)!;\n\n\t\t// Check maxListeners limit\n\t\tif (this.maxListeners > 0 && eventListeners.size >= this.maxListeners) {\n\t\t\tthrow new Error(\n\t\t\t\t`Max listeners (${this.maxListeners}) exceeded for event \"${String(event)}\". ` +\n\t\t\t\t`Use increaseMaxListeners() to increase the limit.`,\n\t\t\t);\n\t\t}\n\n\t\teventListeners.add(listener);\n\n\t\t// Return unsubscribe function\n\t\treturn () => {\n\t\t\tthis.listeners.get(event)?.delete(listener);\n\t\t};\n\t}\n\n\t/**\n\t * Subscribe to an event, but only fire once.\n\t * @returns Unsubscribe function\n\t */\n\tonce<K extends keyof TEvents>(event: K, listener: Listener<TEvents[K]>): () => void {\n\t\tconst wrapper: Listener<TEvents[K]> = async (data) => {\n\t\t\tunsub();\n\t\t\tawait listener(data);\n\t\t};\n\t\tconst unsub = this.on(event, wrapper);\n\t\treturn unsub;\n\t}\n\n\t/**\n\t * Emit an event with data.\n\t * Returns a promise that resolves when all listeners have finished.\n\t *\n\t * @example\n\t * // Default behavior - all listeners run in parallel, any error rejects\n\t * await emitter.emit(\"user:created\", { user });\n\t *\n\t * @example\n\t * // With error isolation - listeners run in parallel, errors don't affect others\n\t * await emitter.emit(\"user:created\", { user }, {\n\t *   isolatedErrors: true,\n\t *   onError: (event, error, listener) => {\n\t *     console.error(`Listener failed for ${event}:`, error);\n\t *   }\n\t * });\n\t */\n\tasync emit<K extends keyof TEvents>(\n\t\tevent: K,\n\t\tdata: TEvents[K],\n\t\toptions?: EmitOptions,\n\t): Promise<void> {\n\t\tconst listeners = this.listeners.get(event);\n\t\tif (!listeners || listeners.size === 0) return;\n\n\t\tconst promises: Promise<void>[] = [];\n\t\tfor (const listener of listeners) {\n\t\t\tpromises.push(this.invokeListener(event, listener, data, options));\n\t\t}\n\n\t\tif (options?.isolatedErrors) {\n\t\t\t// Use Promise.allSettled to isolate errors\n\t\t\tawait Promise.allSettled(promises);\n\t\t} else {\n\t\t\t// Default behavior - any error rejects\n\t\t\tawait Promise.all(promises);\n\t\t}\n\t}\n\n\t/**\n\t * Emit an event serially (one listener at a time).\n\t * Stops on first error unless `isolatedErrors` is true.\n\t *\n\t * @example\n\t * // Serial execution - stops on first error\n\t * await emitter.emitSerial(\"user:created\", { user });\n\t *\n\t * @example\n\t * // With error isolation - continues despite errors\n\t * await emitter.emitSerial(\"user:created\", { user }, {\n\t *   isolatedErrors: true,\n\t *   onError: (event, error) => console.error(error),\n\t * });\n\t */\n\tasync emitSerial<K extends keyof TEvents>(\n\t\tevent: K,\n\t\tdata: TEvents[K],\n\t\toptions?: EmitOptions,\n\t): Promise<void> {\n\t\tconst listeners = this.listeners.get(event);\n\t\tif (!listeners || listeners.size === 0) return;\n\n\t\tfor (const listener of listeners) {\n\t\t\tawait this.invokeListener(event, listener, data, options);\n\t\t}\n\t}\n\n\t/**\n\t * Remove all listeners for an event, or all listeners entirely.\n\t */\n\toff<K extends keyof TEvents>(event?: K): void {\n\t\tif (event) {\n\t\t\tthis.listeners.delete(event);\n\t\t} else {\n\t\t\tthis.listeners.clear();\n\t\t}\n\t}\n\n\t/**\n\t * Get the number of listeners for an event.\n\t */\n\tlistenerCount<K extends keyof TEvents>(event: K): number {\n\t\treturn this.listeners.get(event)?.size ?? 0;\n\t}\n\n\t/**\n\t * Get all event names that have listeners.\n\t */\n\teventNames(): (string | symbol)[] {\n\t\tconst keys = Array.from(this.listeners.keys());\n\t\tconst result: (string | symbol)[] = [];\n\t\tfor (const key of keys) {\n\t\t\tif (typeof key === \"string\" || typeof key === \"symbol\") {\n\t\t\t\tresult.push(key);\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * Increase the max listeners limit for an event.\n\t * Returns the EventEmitter for chaining.\n\t */\n\tincreaseMaxListeners(maxListeners: number): this {\n\t\tthis.maxListeners = maxListeners;\n\t\treturn this;\n\t}\n\n\t/**\n\t * Get the current max listeners limit.\n\t */\n\tgetMaxListeners(): number {\n\t\treturn this.maxListeners;\n\t}\n\n\t/**\n\t * Invoke a single listener with error handling.\n\t */\n\tprivate async invokeListener<K extends keyof TEvents>(\n\t\tevent: K,\n\t\tlistener: Listener<TEvents[K]>,\n\t\tdata: TEvents[K],\n\t\toptions?: EmitOptions,\n\t): Promise<void> {\n\t\ttry {\n\t\t\tawait Promise.resolve(listener(data));\n\t\t} catch (error) {\n\t\t\tif (options?.isolatedErrors) {\n\t\t\t\toptions.onError?.(String(event), error, listener);\n\t\t\t} else {\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t}\n\t}\n}\n\n/**\n * Default emitter instance with common application events.\n *\n * @example\n * ```ts\n * import { emitter } from \"buntok\";\n *\n * emitter.on(\"user:created\", ({ user }) => {\n *   console.log(\"New user:\", user.name);\n * });\n * ```\n */\nexport interface AppEvents {\n\t// Auth events\n\t\"user:created\": { user: any; ctx?: any };\n\t\"user:updated\": { user: any; ctx?: any };\n\t\"user:deleted\": { userId: string | number; ctx?: any };\n\t\"user:logged_in\": { user: any; ctx?: any };\n\t\"user:logged_out\": { user: any; ctx?: any };\n\n\t// Request events\n\t\"request:start\": { method: string; path: string; ctx?: any };\n\t\"request:end\": { method: string; path: string; status: number; duration: number; ctx?: any };\n\t\"request:error\": { method: string; path: string; error: Error; ctx?: any };\n\n\t// App events\n\t\"app:ready\": void;\n\t\"app:shutdown\": void;\n\n\t// Custom events (extensible)\n\t[key: string]: any;\n}\n\nexport const emitter = new EventEmitter<AppEvents>();\n","/**\n * Async sleep - returns a promise that resolves after `ms` milliseconds.\n *\n * @example\n * await delay(1000); // wait 1 second\n */\nexport function delay(ms: number): Promise<void> {\n\treturn new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nexport interface RetryOptions {\n\t/** Maximum number of retries (default: 3) */\n\tretries?: number;\n\t/** Delay between retries in ms (default: 1000) */\n\tdelay?: number;\n\t/** Backoff strategy: \"fixed\" or \"exponential\" (default: \"exponential\") */\n\tbackoff?: \"fixed\" | \"exponential\";\n\t/** Function to decide if the error is retryable. Default: always retry. */\n\tonError?: (error: unknown, attempt: number) => boolean;\n}\n\n/**\n * Retry an async function with configurable backoff.\n *\n * @example\n * const data = await retry(\n *   () => fetch(\"https://api.example.com/data\").then(r => r.json()),\n *   { retries: 3, delay: 1000, backoff: \"exponential\" }\n * );\n */\nexport async function retry<T>(\n\tfn: () => Promise<T>,\n\toptions?: RetryOptions,\n): Promise<T> {\n\tconst {\n\t\tretries = 3,\n\t\tdelay: baseDelay = 1000,\n\t\tbackoff = \"exponential\",\n\t\tonError,\n\t} = options ?? {};\n\n\tlet lastError: unknown;\n\n\tfor (let attempt = 0; attempt <= retries; attempt++) {\n\t\ttry {\n\t\t\treturn await fn();\n\t\t} catch (err) {\n\t\t\tlastError = err;\n\n\t\t\tif (onError && !onError(err, attempt)) {\n\t\t\t\tthrow err;\n\t\t\t}\n\n\t\t\tif (attempt < retries) {\n\t\t\t\tconst waitTime =\n\t\t\t\t\tbackoff === \"exponential\" ? baseDelay * 2 ** attempt : baseDelay;\n\t\t\t\tawait delay(waitTime);\n\t\t\t}\n\t\t}\n\t}\n\n\tthrow lastError;\n}\n","const ENCODER = new TextEncoder();\nconst DECODER = new TextDecoder();\n\nfunction toBuffer(\n\tdata: string | ArrayBuffer | Uint8Array,\n): Uint8Array<ArrayBuffer> {\n\tif (typeof data === \"string\") return ENCODER.encode(data);\n\tif (data instanceof ArrayBuffer) return new Uint8Array(data);\n\treturn new Uint8Array(\n\t\tdata.buffer as ArrayBuffer,\n\t\tdata.byteOffset,\n\t\tdata.byteLength,\n\t);\n}\n\nfunction toHex(buffer: Uint8Array): string {\n\treturn [...buffer].map((b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n}\n\n// ── Hash ─────────────────────────────────────────────────────────────\n\nexport function hash(\n\tdata: string | ArrayBuffer | Uint8Array,\n\talgorithm: \"SHA-1\" | \"SHA-256\" | \"SHA-384\" | \"SHA-512\" = \"SHA-256\",\n): string {\n\tconst algo = algorithm.toLowerCase().replace(\"-\", \"\") as\n\t\t| \"sha1\"\n\t\t| \"sha256\"\n\t\t| \"sha384\"\n\t\t| \"sha512\";\n\tconst hasher = new Bun.CryptoHasher(algo);\n\thasher.update(toBuffer(data));\n\treturn hasher.digest(\"hex\");\n}\n\nexport function sha256(data: string | ArrayBuffer | Uint8Array): string {\n\treturn hash(data, \"SHA-256\");\n}\n\nexport function sha512(data: string | ArrayBuffer | Uint8Array): string {\n\treturn hash(data, \"SHA-512\");\n}\n\n/**\n * MD5 hash - uses a pure JS implementation since WebCrypto doesn't support MD5.\n * Intended for legacy compatibility and cache keys, NOT for security.\n */\nexport async function md5(\n\tdata: string | ArrayBuffer | Uint8Array,\n): Promise<string> {\n\tconst buf = toBuffer(data);\n\tconst T = [\n\t\t0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, 0xf57c0faf, 0x4787c62a,\n\t\t0xa8304613, 0xfd469501, 0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be,\n\t\t0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821, 0xf61e2562, 0xc040b340,\n\t\t0x265e5a51, 0xe9b6c7aa, 0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8,\n\t\t0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed, 0xa9e3e905, 0xfcefa3f8,\n\t\t0x676f02d9, 0x8d2a4c8a, 0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c,\n\t\t0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70, 0x289b7ec6, 0xeaa127fa,\n\t\t0xd4ef3085, 0x04881d05, 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665,\n\t\t0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039, 0x655b59c3, 0x8f0ccc92,\n\t\t0xffeff47d, 0x85845dd1, 0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1,\n\t\t0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391,\n\t];\n\tconst S = [\n\t\t7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 5, 9, 14, 20, 5,\n\t\t9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11,\n\t\t16, 23, 4, 11, 16, 23, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10,\n\t\t15, 21,\n\t];\n\n\tconst msg = new Uint8Array((((buf.length + 8) >>> 6) + 1) << 6);\n\tmsg.set(buf);\n\tmsg[buf.length] = 0x80;\n\tconst msgBuffer = msg.buffer as ArrayBuffer;\n\tconst view = new DataView(msgBuffer);\n\tview.setUint32(msg.length - 8, buf.length * 8, true);\n\tview.setUint32(msg.length - 4, 0, true);\n\n\tlet [a0, b0, c0, d0] = [0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476];\n\n\tfor (let i = 0; i < msg.length; i += 64) {\n\t\tconst M = new Array<number>(16).fill(0);\n\t\tfor (let j = 0; j < 16; j++) M[j] = view.getUint32(i + j * 4, true);\n\n\t\tlet [A, B, C, D] = [a0, b0, c0, d0];\n\t\tlet g = 0;\n\n\t\tfor (let j = 0; j < 64; j++) {\n\t\t\tlet F: number;\n\t\t\tif (j < 16) {\n\t\t\t\tF = (B & C) | (~B & D);\n\t\t\t\tg = j;\n\t\t\t} else if (j < 32) {\n\t\t\t\tF = (D & B) | (~D & C);\n\t\t\t\tg = (5 * j + 1) % 16;\n\t\t\t} else if (j < 48) {\n\t\t\t\tF = B ^ C ^ D;\n\t\t\t\tg = (3 * j + 5) % 16;\n\t\t\t} else {\n\t\t\t\tF = C ^ (B | ~D);\n\t\t\t\tg = (7 * j) % 16;\n\t\t\t}\n\t\t\tF = (F + A + T[j]! + M[g]!) | 0;\n\t\t\tA = D;\n\t\t\tD = C;\n\t\t\tC = B;\n\t\t\tB = (B + ((F << S[j]!) | (F >>> (32 - S[j]!)))) | 0;\n\t\t}\n\n\t\ta0 = (a0 + A) | 0;\n\t\tb0 = (b0 + B) | 0;\n\t\tc0 = (c0 + C) | 0;\n\t\td0 = (d0 + D) | 0;\n\t}\n\n\tconst result = new Uint8Array(16);\n\tconst rView = new DataView(result.buffer as ArrayBuffer);\n\trView.setUint32(0, a0, true);\n\trView.setUint32(4, b0, true);\n\trView.setUint32(8, c0, true);\n\trView.setUint32(12, d0, true);\n\treturn toHex(result);\n}\n\n// ── HMAC ─────────────────────────────────────────────────────────────\n\nexport async function hmac(\n\tdata: string | ArrayBuffer | Uint8Array,\n\tkey: string | ArrayBuffer | Uint8Array,\n\talgorithm: \"SHA-1\" | \"SHA-256\" | \"SHA-384\" | \"SHA-512\" = \"SHA-256\",\n): Promise<string> {\n\tconst cryptoKey = await crypto.subtle.importKey(\n\t\t\"raw\",\n\t\ttoBuffer(key),\n\t\t{ name: \"HMAC\", hash: algorithm },\n\t\tfalse,\n\t\t[\"sign\"],\n\t);\n\tconst signature = await crypto.subtle.sign(\"HMAC\", cryptoKey, toBuffer(data));\n\treturn toHex(new Uint8Array(signature));\n}\n\nexport async function hashVerify(\n\tdata: string | ArrayBuffer | Uint8Array,\n\texpected: string,\n\talgorithm: \"SHA-1\" | \"SHA-256\" | \"SHA-384\" | \"SHA-512\" = \"SHA-256\",\n): Promise<boolean> {\n\tconst actual = hash(data, algorithm);\n\tif (actual.length !== expected.length) return false;\n\tlet result = 0;\n\tfor (let i = 0; i < actual.length; i++) {\n\t\tresult |= actual.charCodeAt(i) ^ expected.charCodeAt(i);\n\t}\n\treturn result === 0;\n}\n\n// ── Random ───────────────────────────────────────────────────────────\n\nexport function randomBytes(length: number): Uint8Array {\n\treturn crypto.getRandomValues(new Uint8Array(length));\n}\n\nexport function randomHex(length: number): string {\n\treturn toHex(randomBytes(Math.ceil(length / 2))).slice(0, length);\n}\n\nconst ALPHA_NUM =\n\t\"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\";\n\nexport function randomAlphaNumeric(length: number): string {\n\tconst bytes = randomBytes(length);\n\treturn Array.from(bytes, (b) => ALPHA_NUM[b % ALPHA_NUM.length]).join(\"\");\n}\n\nexport function randomToken(length = 32): string {\n\tconst bytes = randomBytes(length);\n\treturn btoa(String.fromCharCode(...bytes))\n\t\t.replace(/\\+/g, \"-\")\n\t\t.replace(/\\//g, \"_\")\n\t\t.replace(/=+$/, \"\");\n}\n\n// ── Encrypt / Decrypt (AES-256-GCM) ─────────────────────────────────\n\nasync function importAesKey(\n\tkey: string | ArrayBuffer | Uint8Array,\n): Promise<CryptoKey> {\n\tconst keyData = typeof key === \"string\" ? ENCODER.encode(key) : toBuffer(key);\n\tconst rawKey =\n\t\tkeyData.length === 32\n\t\t\t? keyData\n\t\t\t: new Uint8Array(await crypto.subtle.digest(\"SHA-256\", keyData));\n\treturn crypto.subtle.importKey(\"raw\", rawKey, \"AES-GCM\", false, [\n\t\t\"encrypt\",\n\t\t\"decrypt\",\n\t]);\n}\n\nexport async function encrypt(\n\tdata: string | ArrayBuffer | Uint8Array,\n\tkey: string | ArrayBuffer | Uint8Array,\n\tiv?: Uint8Array,\n): Promise<{ ciphertext: string; iv: string }> {\n\tconst cryptoKey = await importAesKey(key);\n\tconst ivBytes = (iv ??\n\t\tcrypto.getRandomValues(new Uint8Array(12))) as Uint8Array<ArrayBuffer>;\n\tconst encrypted = await crypto.subtle.encrypt(\n\t\t{ name: \"AES-GCM\", iv: ivBytes },\n\t\tcryptoKey,\n\t\ttoBuffer(data),\n\t);\n\treturn {\n\t\tciphertext: toHex(new Uint8Array(encrypted)),\n\t\tiv: toHex(ivBytes),\n\t};\n}\n\nexport async function decrypt(\n\tciphertext: string,\n\tkey: string | ArrayBuffer | Uint8Array,\n\tiv: string,\n): Promise<string> {\n\tconst cryptoKey = await importAesKey(key);\n\tconst ivBytes = new Uint8Array(\n\t\tiv.match(/.{1,2}/g)?.map((h) => Number.parseInt(h, 16)) ?? [],\n\t);\n\tconst data = new Uint8Array(\n\t\tciphertext.match(/.{1,2}/g)?.map((h) => Number.parseInt(h, 16)) ?? [],\n\t);\n\tconst decrypted = await crypto.subtle.decrypt(\n\t\t{ name: \"AES-GCM\", iv: ivBytes },\n\t\tcryptoKey,\n\t\tdata,\n\t);\n\treturn DECODER.decode(decrypted);\n}\n","const UNITS: [string, number][] = [\n\t[\"year\", 31536000],\n\t[\"month\", 2628000],\n\t[\"week\", 604800],\n\t[\"day\", 86400],\n\t[\"hour\", 3600],\n\t[\"minute\", 60],\n\t[\"second\", 1],\n];\n\nfunction pluralize(unit: string, value: number): string {\n\treturn value === 1 ? unit : `${unit}s`;\n}\n\nfunction toTemporal(date: Date | string | number): Temporal.Instant {\n\tif (date instanceof Date) {\n\t\treturn Temporal.Instant.fromEpochMilliseconds(date.getTime());\n\t}\n\tif (typeof date === \"number\") {\n\t\treturn Temporal.Instant.fromEpochMilliseconds(date);\n\t}\n\treturn Temporal.Instant.from(date);\n}\n\n/**\n * Format a Date to ISO string or custom format.\n *\n * @example\n * formatDate(new Date()); // \"2024-01-15T10:30:00.000Z\"\n */\nexport function formatDate(date: Date | string | number): string {\n\treturn toTemporal(date).toString();\n}\n\n/**\n * Get human-readable relative time (\"3 minutes ago\", \"2 hours ago\").\n *\n * @example\n * timeAgo(new Date(Date.now() - 180000)); // \"3 minutes ago\"\n * timeAgo(new Date(Date.now() - 7200000)); // \"2 hours ago\"\n */\nexport function timeAgo(date: Date | string | number): string {\n\tconst d = date instanceof Date ? date : new Date(date);\n\tconst seconds = Math.floor((Date.now() - d.getTime()) / 1000);\n\n\tfor (const [unit, secs] of UNITS) {\n\t\tconst value = Math.floor(seconds / secs);\n\t\tif (value >= 1) {\n\t\t\treturn `${value} ${pluralize(unit, value)} ago`;\n\t\t}\n\t}\n\n\treturn \"just now\";\n}\n\n/**\n * Calculate the number of whole days between two dates.\n *\n * @example\n * daysBetween(new Date(\"2024-01-01\"), new Date(\"2024-01-15\")); // 14\n */\nexport function daysBetween(\n\tdate1: Date | string | number,\n\tdate2: Date | string | number,\n): number {\n\tconst ms1 = toTemporal(date1).epochMilliseconds;\n\tconst ms2 = toTemporal(date2).epochMilliseconds;\n\treturn Math.round((ms2 - ms1) / 86400000);\n}\n\n/**\n * Add days to a date and return a new Date.\n *\n * @example\n * addDays(new Date(\"2024-01-01\"), 30); // Date(\"2024-01-31\")\n */\nexport function addDays(\n\tdate: Date | string | number,\n\tdays: number,\n): Date {\n\tconst d = date instanceof Date ? date : new Date(date);\n\tconst result = new Date(d);\n\tresult.setDate(result.getDate() + days);\n\treturn result;\n}\n\n/**\n * Check if date1 is before date2.\n *\n * @example\n * isBefore(new Date(\"2024-01-01\"), new Date(\"2024-01-15\")); // true\n */\nexport function isBefore(\n\tdate1: Date | string | number,\n\tdate2: Date | string | number,\n): boolean {\n\treturn toTemporal(date1).epochMilliseconds < toTemporal(date2).epochMilliseconds;\n}\n\n/**\n * Check if date1 is after date2.\n *\n * @example\n * isAfter(new Date(\"2024-01-15\"), new Date(\"2024-01-01\")); // true\n */\nexport function isAfter(\n\tdate1: Date | string | number,\n\tdate2: Date | string | number,\n): boolean {\n\treturn toTemporal(date1).epochMilliseconds > toTemporal(date2).epochMilliseconds;\n}\n\n/**\n * Get the start of day (00:00:00) for a given date.\n *\n * @example\n * startOfDay(new Date(\"2024-01-15T14:30:00\")); // Date(\"2024-01-15T00:00:00\")\n */\nexport function startOfDay(date: Date | string | number): Date {\n\tconst d = date instanceof Date ? new Date(date) : new Date(date);\n\td.setHours(0, 0, 0, 0);\n\treturn d;\n}\n\n/**\n * Get the end of day (23:59:59.999) for a given date.\n *\n * @example\n * endOfDay(new Date(\"2024-01-15T14:30:00\")); // Date(\"2024-01-15T23:59:59.999\")\n */\nexport function endOfDay(date: Date | string | number): Date {\n\tconst d = date instanceof Date ? new Date(date) : new Date(date);\n\td.setHours(23, 59, 59, 999);\n\treturn d;\n}\n\n/**\n * Format a duration in seconds to human-readable string (\"2h 30m 15s\").\n *\n * @example\n * formatDuration(9015);  // \"2h 30m 15s\"\n * formatDuration(65);    // \"1m 5s\"\n * formatDuration(30);    // \"30s\"\n */\nexport function formatDuration(seconds: number): string {\n\tconst h = Math.floor(seconds / 3600);\n\tconst m = Math.floor((seconds % 3600) / 60);\n\tconst s = Math.floor(seconds % 60);\n\n\tconst parts: string[] = [];\n\tif (h > 0) parts.push(`${h}h`);\n\tif (m > 0) parts.push(`${m}m`);\n\tif (s > 0 || parts.length === 0) parts.push(`${s}s`);\n\n\treturn parts.join(\" \");\n}\n","/**\n * Timezone helper utilities for handling time conversions between server and client.\n *\n * Common use case:\n * - Server stores times in ISO +7 (Asia/Jakarta)\n * - Dashboard needs to display/group data by client's local timezone\n */\n\nfunction toDate(date: Date | string | number): Date {\n\treturn date instanceof Date ? date : new Date(date);\n}\n\n/**\n * Get the timezone offset in minutes for a given IANA timezone.\n *\n * @example\n * getTimezoneOffset(\"Asia/Jakarta\"); // -420 (UTC+7)\n * getTimezoneOffset(\"America/New_York\"); // varies (DST aware)\n */\nexport function getTimezoneOffset(timezone: string, date?: Date): number {\n\tconst d = date ?? new Date();\n\tconst utcStr = d.toLocaleString(\"en-US\", { timeZone: \"UTC\" });\n\tconst tzStr = d.toLocaleString(\"en-US\", { timeZone: timezone });\n\tconst utc = new Date(utcStr);\n\tconst tz = new Date(tzStr);\n\treturn (utc.getTime() - tz.getTime()) / 60000;\n}\n\n/**\n * Parse a time string and interpret it in a specific timezone.\n *\n * If the string has no timezone info (no Z, no +07:00), it will be\n * interpreted as being in the specified timezone.\n *\n * @example\n * // Server receives \"2026-08-24 05:00\" from client in UTC+7\n * const date = parseTime(\"2026-08-24 05:00\", \"Asia/Jakarta\");\n * date.toISOString(); // \"2026-08-23T22:00:00.000Z\" (correct UTC)\n */\nexport function parseTime(timeStr: string, timezone: string): Date {\n\t// If already has timezone info (Z or ±HH:MM), use native parsing\n\tif (/[Zz]$|[+-]\\d{2}:\\d{2}$/.test(timeStr.trim())) {\n\t\treturn new Date(timeStr);\n\t}\n\n\t// Parse as local time in target timezone\n\tconst d = new Date(timeStr);\n\tif (Number.isNaN(d.getTime())) {\n\t\tthrow new Error(`Invalid date string: \"${timeStr}\"`);\n\t}\n\n\t// Get what this date looks like in the target timezone\n\tconst offset = getTimezoneOffset(timezone, d);\n\tconst utcOffset = d.getTimezoneOffset(); // local offset in minutes (negative for east)\n\n\t// Adjust: move from local interpretation to target timezone\n\tconst adjusted = new Date(d.getTime() + (utcOffset + offset) * 60000);\n\treturn adjusted;\n}\n\n/**\n * Convert a Date to a specific timezone and return parts.\n *\n * @example\n * const parts = toTimezoneParts(new Date(\"2026-08-23T22:00:00Z\"), \"Asia/Jakarta\");\n * // { year: 2026, month: 8, day: 24, hour: 5, minute: 0, second: 0 }\n */\nexport function toTimezoneParts(date: Date, timezone: string) {\n\tconst d = toDate(date);\n\tconst parts = new Intl.DateTimeFormat(\"en-US\", {\n\t\ttimeZone: timezone,\n\t\tyear: \"numeric\",\n\t\tmonth: \"2-digit\",\n\t\tday: \"2-digit\",\n\t\thour: \"2-digit\",\n\t\tminute: \"2-digit\",\n\t\tsecond: \"2-digit\",\n\t\thour12: false,\n\t}).formatToParts(d);\n\n\tconst get = (type: string) => Number(parts.find((p) => p.type === type)?.value ?? 0);\n\n\treturn {\n\t\tyear: get(\"year\"),\n\t\tmonth: get(\"month\"),\n\t\tday: get(\"day\"),\n\t\thour: get(\"hour\"),\n\t\tminute: get(\"minute\"),\n\t\tsecond: get(\"second\"),\n\t};\n}\n\n/**\n * Format a Date in a specific timezone.\n *\n * @example\n * const date = new Date(\"2026-08-23T22:00:00Z\");\n * formatInTimezone(date, \"Asia/Jakarta\"); // \"2026-08-24 05:00:00\"\n * formatInTimezone(date, \"Asia/Jakarta\", \"short\"); // \"2026-08-24 05:00\"\n * formatInTimezone(date, \"Asia/Jakarta\", \"full\"); // \"2026-08-24 05:00:00.000\"\n */\nexport function formatInTimezone(\n\tdate: Date | string | number,\n\ttimezone: string,\n\tformat: \"short\" | \"default\" | \"full\" = \"default\",\n): string {\n\tconst d = toDate(date);\n\tconst parts = toTimezoneParts(d, timezone);\n\n\tconst pad = (n: number) => String(n).padStart(2, \"0\");\n\tconst y = parts.year;\n\tconst m = pad(parts.month);\n\tconst day = pad(parts.day);\n\tconst h = pad(parts.hour);\n\tconst min = pad(parts.minute);\n\tconst sec = pad(parts.second);\n\n\tswitch (format) {\n\t\tcase \"short\":\n\t\t\treturn `${y}-${m}-${day} ${h}:${min}`;\n\t\tcase \"full\":\n\t\t\treturn `${y}-${m}-${day} ${h}:${min}:${sec}.000`;\n\t\tdefault:\n\t\t\treturn `${y}-${m}-${day} ${h}:${min}:${sec}`;\n\t}\n}\n\n/**\n * Get current time in a specific timezone.\n *\n * @example\n * const now = nowInTimezone(\"Asia/Jakarta\");\n * // Date object representing current time, displayable in Jakarta timezone\n */\nexport function nowInTimezone(timezone: string): Date {\n\tconst now = new Date();\n\tconst parts = toTimezoneParts(now, timezone);\n\tconst reconstructed = new Date(\n\t\tDate.UTC(parts.year, parts.month - 1, parts.day, parts.hour, parts.minute, parts.second),\n\t);\n\tconst tzOffset = getTimezoneOffset(timezone, now);\n\treturn new Date(reconstructed.getTime() + tzOffset * 60000);\n}\n\n/**\n * Get the timezone offset string (e.g., \"+07:00\", \"-05:00\") for a timezone.\n *\n * @example\n * getTimezoneOffsetString(\"Asia/Jakarta\"); // \"+07:00\"\n * getTimezoneOffsetString(\"America/New_York\"); // \"-04:00\" or \"-05:00\" (DST)\n */\nexport function getTimezoneOffsetString(timezone: string, date?: Date): string {\n\tconst offset = getTimezoneOffset(timezone, date);\n\tconst sign = offset <= 0 ? \"+\" : \"-\";\n\tconst absOffset = Math.abs(offset);\n\tconst hours = Math.floor(absOffset / 60);\n\tconst minutes = absOffset % 60;\n\treturn `${sign}${String(hours).padStart(2, \"0\")}:${String(minutes).padStart(2, \"0\")}`;\n}\n\n/**\n * Convert a Date to ISO string with a specific timezone offset.\n *\n * @example\n * const date = new Date(\"2026-08-23T22:00:00Z\");\n * toISOWithTimezone(date, \"Asia/Jakarta\");\n * // \"2026-08-24T05:00:00+07:00\"\n */\nexport function toISOWithTimezone(date: Date | string | number, timezone: string): string {\n\tconst d = toDate(date);\n\tconst parts = toTimezoneParts(d, timezone);\n\tconst offset = getTimezoneOffsetString(timezone, d);\n\n\tconst pad = (n: number) => String(n).padStart(2, \"0\");\n\treturn `${parts.year}-${pad(parts.month)}-${pad(parts.day)}T${pad(parts.hour)}:${pad(parts.minute)}:${pad(parts.second)}${offset}`;\n}\n\nexport type GroupByKey = \"hour\" | \"day\" | \"month\" | \"year\";\n\nexport interface GroupByTimezoneOptions {\n\tlocale?: string;\n\tlabelFormatter?: (key: string, groupBy: GroupByKey) => string;\n}\n\nfunction defaultLabelFormatter(key: string, groupBy: GroupByKey, locale: string): string {\n\t// If key looks like a date (YYYY-MM-DD)\n\tif (/^\\d{4}-\\d{2}-\\d{2}$/.test(key)) {\n\t\tconst [y, m, d] = key.split(\"-\").map(Number) as [number, number, number];\n\t\tconst date = new Date(y, m - 1, d);\n\t\treturn new Intl.DateTimeFormat(locale, {\n\t\t\tweekday: \"long\",\n\t\t\tyear: \"numeric\",\n\t\t\tmonth: \"long\",\n\t\t\tday: \"numeric\",\n\t\t}).format(date);\n\t}\n\n\t// If key looks like YYYY-MM\n\tif (/^\\d{4}-\\d{2}$/.test(key)) {\n\t\tconst [y, m] = key.split(\"-\").map(Number) as [number, number];\n\t\tconst date = new Date(y, m - 1, 1);\n\t\treturn new Intl.DateTimeFormat(locale, {\n\t\t\tyear: \"numeric\",\n\t\t\tmonth: \"long\",\n\t\t}).format(date);\n\t}\n\n\t// If key is just a year\n\tif (/^\\d{4}$/.test(key)) {\n\t\treturn key;\n\t}\n\n\t// If key is hour (00-23)\n\tif (/^\\d{2}$/.test(key)) {\n\t\tconst hour = Number(key);\n\t\tconst date = new Date(2000, 0, 1, hour);\n\t\treturn new Intl.DateTimeFormat(locale, {\n\t\t\thour: \"numeric\",\n\t\t\thour12: false,\n\t\t}).format(date);\n\t}\n\n\treturn key;\n}\n\n/**\n * Format a group key into a human-readable label.\n *\n * @example\n * formatGroupLabel(\"2026-08-24\", \"day\", \"id\");\n * // \"Rabu, 24 Agustus 2026\"\n *\n * formatGroupLabel(\"2026-08-24\", \"day\", \"en\");\n * // \"Wednesday, August 24, 2026\"\n *\n * formatGroupLabel(\"05\", \"hour\", \"id\");\n * // \"05.00\"\n */\nexport function formatGroupLabel(\n\tkey: string,\n\tgroupBy: GroupByKey,\n\tlocale = \"en-US\",\n): string {\n\treturn defaultLabelFormatter(key, groupBy, locale);\n}\n\n/**\n * Group items by their date value in a specific timezone.\n *\n * @example\n * const transactions = [\n *   { id: 1, createdAt: \"2026-08-24T05:00:00+07:00\", amount: 100 },\n *   { id: 2, createdAt: \"2026-08-24T08:30:00+07:00\", amount: 200 },\n *   { id: 3, createdAt: \"2026-08-24T14:00:00+07:00\", amount: 150 },\n * ];\n *\n * // Group by hour in Jakarta timezone\n * const hourly = groupByTimezone(transactions, \"createdAt\", \"Asia/Jakarta\", \"hour\");\n * // Map {\n * //   \"05\" => [{ id: 1, ... }],\n * //   \"08\" => [{ id: 2, ... }],\n * //   \"14\" => [{ id: 3, ... }],\n * // }\n *\n * // Group by day\n * const daily = groupByTimezone(transactions, \"createdAt\", \"Asia/Jakarta\", \"day\");\n * // Map {\n * //   \"2026-08-24\" => [all 3 transactions],\n * // }\n *\n * // With human-readable labels\n * const dailyWithLabels = groupByTimezone(\n *   transactions, \"createdAt\", \"Asia/Jakarta\", \"day\",\n *   { locale: \"id\" }\n * );\n * // Labels: \"Rabu, 24 Agustus 2026\"\n */\nexport function groupByTimezone<T>(\n\titems: T[],\n\tdateField: keyof T,\n\ttimezone: string,\n\tgroupBy: GroupByKey = \"day\",\n\toptions?: GroupByTimezoneOptions,\n): Map<string, T[]> {\n\tconst groups = new Map<string, T[]>();\n\tconst locale = options?.locale ?? \"en-US\";\n\n\tfor (const item of items) {\n\t\tconst dateVal = item[dateField];\n\t\tif (!dateVal) continue;\n\n\t\tconst d = toDate(dateVal as Date | string | number);\n\t\tconst parts = toTimezoneParts(d, timezone);\n\t\tconst pad = (n: number) => String(n).padStart(2, \"0\");\n\n\t\tlet key: string;\n\t\tswitch (groupBy) {\n\t\t\tcase \"hour\":\n\t\t\t\tkey = `${pad(parts.hour)}`;\n\t\t\t\tbreak;\n\t\t\tcase \"month\":\n\t\t\t\tkey = `${parts.year}-${pad(parts.month)}`;\n\t\t\t\tbreak;\n\t\t\tcase \"year\":\n\t\t\t\tkey = `${parts.year}`;\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tkey = `${parts.year}-${pad(parts.month)}-${pad(parts.day)}`;\n\t\t}\n\n\t\tconst group = groups.get(key) ?? [];\n\t\tgroup.push(item);\n\t\tgroups.set(key, group);\n\t}\n\n\treturn groups;\n}\n\n/**\n * Get labels for groupByTimezone keys.\n *\n * @example\n * const hourly = groupByTimezone(transactions, \"createdAt\", \"Asia/Jakarta\", \"hour\");\n * const labels = getGroupLabels(hourly, \"hour\", \"id\");\n * // Map { \"05\" => \"05.00\", \"08\" => \"08.00\", ... }\n *\n * const daily = groupByTimezone(transactions, \"createdAt\", \"Asia/Jakarta\", \"day\");\n * const labels = getGroupLabels(daily, \"day\", \"id\");\n * // Map { \"2026-08-24\" => \"Rabu, 24 Agustus 2026\", ... }\n */\nexport function getGroupLabels(\n\tgroups: Map<string, unknown[]>,\n\tgroupBy: GroupByKey,\n\tlocale = \"en-US\",\n): Map<string, string> {\n\tconst labels = new Map<string, string>();\n\tfor (const key of groups.keys()) {\n\t\tlabels.set(key, formatGroupLabel(key, groupBy, locale));\n\t}\n\treturn labels;\n}\n\n/**\n * Check if a timezone string is valid.\n *\n * @example\n * isValidTimezone(\"Asia/Jakarta\"); // true\n * isValidTimezone(\"Invalid/Zone\"); // false\n */\nexport function isValidTimezone(timezone: string): boolean {\n\ttry {\n\t\tnew Intl.DateTimeFormat(\"en-US\", { timeZone: timezone });\n\t\treturn true;\n\t} catch {\n\t\treturn false;\n\t}\n}\n","const _ALPHANUMERIC =\n\t\"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\";\nconst urlAlphabet =\n\t\"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_\";\n\n/**\n * Generate a sequential code with prefix and auto-incrementing counter.\n *\n * @example\n * generateCode(\"T\");      // \"T0001\"\n * generateCode(\"T\");      // \"T0002\"\n * generateCode(\"INV\");    // \"INV0001\"\n * generateCode(\"ORD\", 42); // \"ORD0042\"\n */\nexport function generateCode(prefix: string, counter?: number): string {\n\tif (counter === undefined) {\n\t\t// Use module-level counter per prefix\n\t\tconst current = counters.get(prefix) ?? 0;\n\t\tcounter = current + 1;\n\t\tcounters.set(prefix, counter);\n\t}\n\treturn `${prefix}${counter.toString().padStart(4, \"0\")}`;\n}\n\nconst counters = new Map<string, number>();\n\n/**\n * Reset the auto-increment counter for a prefix.\n *\n * @example\n * resetCounter(\"T\"); // next generateCode(\"T\") will be \"T0001\"\n */\nexport function resetCounter(prefix: string): void {\n\tcounters.delete(prefix);\n}\n\n/**\n * Generate a URL-safe unique ID (nanoid-style).\n *\n * @example\n * nanoid();     // \"V1StGXR8_Z5jdHi6B-myT\"  (21 chars)\n * nanoid(10);   // \"V1StGXR8_Z5\"             (10 chars)\n */\nexport function nanoid(size = 21): string {\n\tconst bytes = crypto.getRandomValues(new Uint8Array(size));\n\treturn Array.from(bytes, (b) => urlAlphabet[b % 64]).join(\"\");\n}\n\n/**\n * Generate a ULID (Time-Ordered Unique Identifier).\n * Format: 26 chars, sortable by creation time.\n *\n * @example\n * ulid(); // \"01ARZ3NDEKTSV4RRFFQ69G5FAV\"\n */\nexport function ulid(): string {\n\tconst time = Date.now();\n\tconst timeStr = encodeTime(time, 10);\n\tconst randomStr = encodeRandom(16);\n\treturn timeStr + randomStr;\n}\n\nconst ENCODING = \"0123456789ABCDEFGHJKMNPQRSTVWXYZ\";\n\nfunction encodeTime(time: number, length: number): string {\n\tlet result = \"\";\n\tfor (let i = length - 1; i >= 0; i--) {\n\t\tresult = ENCODING[time % 32] + result;\n\t\ttime = Math.floor(time / 32);\n\t}\n\treturn result;\n}\n\nfunction encodeRandom(length: number): string {\n\tconst bytes = crypto.getRandomValues(new Uint8Array(length));\n\treturn Array.from(bytes, (b) => ENCODING[b % 32]).join(\"\");\n}\n","const PRIVATE_IP_REGEX =\n\t/^(10\\.|172\\.(1[6-9]|2\\d|3[01])\\.|192\\.168\\.|127\\.|0\\.|localhost|::1|fc|fd)/i;\n\n/**\n * Get the client IP address from a Request.\n * Checks X-Forwarded-For, X-Real-IP, then falls back to request.remoteAddress.\n */\nexport function getClientIP(request: Request): string {\n\tconst xff = request.headers.get(\"x-forwarded-for\");\n\tif (xff) {\n\t\tconst first = xff.split(\",\")[0]?.trim();\n\t\tif (first) return first;\n\t}\n\n\tconst xri = request.headers.get(\"x-real-ip\");\n\tif (xri) return xri;\n\n\t// Bun exposes remoteAddress on the request\n\t// biome-ignore lint/suspicious/noExplicitAny: Bun-specific property\n\tconst remote = (request as any).remoteAddress;\n\tif (remote) return remote;\n\n\treturn \"unknown\";\n}\n\n/**\n * Check if an IP address is a private/reserved address.\n */\nexport function isPrivateIP(ip: string): boolean {\n\treturn PRIVATE_IP_REGEX.test(ip);\n}\n\ninterface UserAgentInfo {\n\tbrowser: string;\n\tos: string;\n\tdevice: string;\n}\n\nconst BROWSER_PATTERNS: [RegExp, string][] = [\n\t[/opr\\//i, \"Opera\"],\n\t[/edg/i, \"Edge\"],\n\t[/chrome/i, \"Chrome\"],\n\t[/firefox/i, \"Firefox\"],\n\t[/safari/i, \"Safari\"],\n\t[/msie|trident/i, \"IE\"],\n];\n\nconst OS_PATTERNS: [RegExp, string][] = [\n\t[/windows/i, \"Windows\"],\n\t[/mac os/i, \"macOS\"],\n\t[/linux/i, \"Linux\"],\n\t[/android/i, \"Android\"],\n\t[/iphone|ipad|ipod/i, \"iOS\"],\n\t[/cros/i, \"Chrome OS\"],\n\t[/winphone/i, \"Windows Phone\"],\n];\n\nconst DEVICE_PATTERNS: [RegExp, string][] = [\n\t[/mobile|android.*mobile|iphone/i, \"Mobile\"],\n\t[/ipad|tablet|android(?!.*mobile)/i, \"Tablet\"],\n];\n\n/**\n * Parse the User-Agent header into a simple `{ browser, os, device }` object.\n */\nexport function parseUserAgent(request: Request): UserAgentInfo {\n\tconst ua = request.headers.get(\"user-agent\") ?? \"\";\n\n\tlet browser = \"Other\";\n\tfor (const [pattern, name] of BROWSER_PATTERNS) {\n\t\tif (pattern.test(ua)) {\n\t\t\tbrowser = name;\n\t\t\tbreak;\n\t\t}\n\t}\n\n\tlet os = \"Other\";\n\tfor (const [pattern, name] of OS_PATTERNS) {\n\t\tif (pattern.test(ua)) {\n\t\t\tos = name;\n\t\t\tbreak;\n\t\t}\n\t}\n\n\tlet device = \"Desktop\";\n\tfor (const [pattern, name] of DEVICE_PATTERNS) {\n\t\tif (pattern.test(ua)) {\n\t\t\tdevice = name;\n\t\t\tbreak;\n\t\t}\n\t}\n\n\treturn { browser, os, device };\n}\n","/**\n * Clamp a number between min and max (inclusive).\n *\n * @example\n * clamp(15, 0, 10);  // 10\n * clamp(-5, 0, 10);  // 0\n * clamp(5, 0, 10);   // 5\n */\nexport function clamp(value: number, min: number, max: number): number {\n\treturn Math.min(Math.max(value, min), max);\n}\n\n/**\n * Random integer between min and max (inclusive).\n *\n * @example\n * random(1, 100); // 42\n */\nexport function random(min: number, max: number): number {\n\treturn Math.floor(Math.random() * (max - min + 1)) + min;\n}\n\n/**\n * Random float between min and max.\n *\n * @example\n * randomFloat(1.5, 3.5); // 2.731\n */\nexport function randomFloat(min: number, max: number): number {\n\treturn Math.random() * (max - min) + min;\n}\n\n/**\n * Format a number with locale-aware separators.\n *\n * @example\n * formatNumber(1000000);       // \"1,000,000\"\n * formatNumber(1000000, \"de\"); // \"1.000.000\"\n */\nexport function formatNumber(n: number, locale = \"en-US\"): string {\n\treturn n.toLocaleString(locale);\n}\n\n/**\n * Format bytes to human-readable string.\n *\n * @example\n * formatBytes(0);          // \"0 B\"\n * formatBytes(1024);       // \"1 KB\"\n * formatBytes(1536);       // \"1.5 KB\"\n * formatBytes(1048576);    // \"1 MB\"\n */\nexport function formatBytes(bytes: number): string {\n\tif (bytes === 0) return \"0 B\";\n\tconst units = [\"B\", \"KB\", \"MB\", \"GB\", \"TB\", \"PB\"];\n\tconst i = Math.floor(Math.log(bytes) / Math.log(1024));\n\tconst size = bytes / 1024 ** i;\n\treturn `${size === 0 ? \"0\" : size.toFixed(size >= 100 ? 0 : size >= 10 ? 1 : 2)} ${units[i]}`;\n}\n\n/**\n * Format a number as currency.\n *\n * @example\n * formatCurrency(1000);          // \"$1,000.00\"\n * formatCurrency(1000, \"EUR\");   // \"€1,000.00\"\n * formatCurrency(1000, \"IDR\", \"id-ID\"); // \"Rp1.000\"\n */\nexport function formatCurrency(\n\tamount: number,\n\tcurrency = \"USD\",\n\tlocale = \"en-US\",\n): string {\n\treturn amount.toLocaleString(locale, {\n\t\tstyle: \"currency\",\n\t\tcurrency,\n\t});\n}\n","/**\n * Pick specified keys from an object.\n * `pick({ a: 1, b: 2, c: 3 }, [\"a\", \"c\"])` → `{ a: 1, c: 3 }`\n */\nexport function pick<T extends Record<string, unknown>, K extends keyof T>(\n\tobj: T,\n\tkeys: K[],\n): Pick<T, K> {\n\tconst result = {} as Pick<T, K>;\n\tfor (const key of keys) {\n\t\tif (key in obj) result[key] = obj[key];\n\t}\n\treturn result;\n}\n\n/**\n * Omit specified keys from an object.\n * `omit({ a: 1, b: 2, c: 3 }, [\"b\"])` → `{ a: 1, c: 3 }`\n */\nexport function omit<T extends Record<string, unknown>, K extends keyof T>(\n\tobj: T,\n\tkeys: K[],\n): Omit<T, K> {\n\tconst result = { ...obj };\n\tfor (const key of keys) {\n\t\tdelete result[key];\n\t}\n\treturn result as Omit<T, K>;\n}\n\n/**\n * Group an array of objects by a key or field function.\n * `groupBy([{ type: \"a\" }, { type: \"b\" }, { type: \"a\" }], \"type\")`\n * → `{ a: [{ type: \"a\" }, { type: \"a\" }], b: [{ type: \"b\" }] }`\n */\nexport function groupBy<T>(\n\tarr: T[],\n\tkey: keyof T | ((item: T) => string),\n): Record<string, T[]> {\n\tconst result: Record<string, T[]> = {};\n\tfor (const item of arr) {\n\t\tconst k = typeof key === \"function\" ? key(item) : String(item[key]);\n\t\tif (!result[k]) {\n\t\t\tresult[k] = [];\n\t\t}\n\t\tresult[k].push(item);\n\t}\n\treturn result;\n}\n\n/**\n * Return unique values from an array.\n * `uniq([1, 2, 2, 3])` → `[1, 2, 3]`\n */\nexport function uniq<T>(arr: T[]): T[] {\n\treturn [...new Set(arr)];\n}\n\n/**\n * Flatten a nested array.\n * `flatten([[1, 2], [3, [4, 5]]])` → `[1, 2, 3, 4, 5]`\n */\nexport function flatten<T>(arr: unknown[], depth = Infinity): T[] {\n\treturn arr.flat(depth) as T[];\n}\n\n/**\n * Split an array into chunks of a given size.\n * `chunk([1, 2, 3, 4, 5], 2)` → `[[1, 2], [3, 4], [5]]`\n */\nexport function chunk<T>(arr: T[], size: number): T[][] {\n\tif (size <= 0) throw new Error(\"chunk size must be > 0\");\n\tconst chunks: T[][] = [];\n\tfor (let i = 0; i < arr.length; i += size) {\n\t\tchunks.push(arr.slice(i, i + size));\n\t}\n\treturn chunks;\n}\n\n/**\n * Deep merge two objects. Arrays are replaced, not merged.\n *\n * @example\n * deepMerge({ a: 1, b: { c: 2 } }, { b: { d: 3 } });\n * // => { a: 1, b: { c: 2, d: 3 } }\n */\nexport function deepMerge<T extends Record<string, unknown>>(\n\ttarget: T,\n\t...sources: Partial<T>[]\n): T {\n\tconst result = { ...target };\n\tfor (const source of sources) {\n\t\tfor (const key of Object.keys(source) as (keyof T)[]) {\n\t\t\tconst targetVal = result[key];\n\t\t\tconst sourceVal = source[key];\n\t\t\tif (isPlainObject(targetVal) && isPlainObject(sourceVal)) {\n\t\t\t\t(result[key] as Record<string, unknown>) = deepMerge(\n\t\t\t\t\ttargetVal as Record<string, unknown>,\n\t\t\t\t\tsourceVal as Record<string, unknown>,\n\t\t\t\t);\n\t\t\t} else if (sourceVal !== undefined) {\n\t\t\t\tresult[key] = sourceVal as T[typeof key];\n\t\t\t}\n\t\t}\n\t}\n\treturn result;\n}\n\nfunction isPlainObject(val: unknown): val is Record<string, unknown> {\n\treturn typeof val === \"object\" && val !== null && !Array.isArray(val);\n}\n\n/**\n * Flatten a nested object into a single-depth object with dot-notation keys.\n *\n * @example\n * flattenObject({ a: { b: { c: 1 } } });\n * // => { \"a.b.c\": 1 }\n *\n * flattenObject({ a: { b: 1 }, c: 2 }, \"-\");\n * // => { \"a-b\": 1, c: 2 }\n */\nexport function flattenObject(\n\tobj: Record<string, unknown>,\n\tsep = \".\",\n): Record<string, unknown> {\n\tconst result: Record<string, unknown> = {};\n\n\tfunction recurse(current: Record<string, unknown>, prefix: string) {\n\t\tfor (const key of Object.keys(current)) {\n\t\t\tconst value = current[key];\n\t\t\tconst newKey = prefix ? `${prefix}${sep}${key}` : key;\n\n\t\t\tif (isPlainObject(value)) {\n\t\t\t\trecurse(value as Record<string, unknown>, newKey);\n\t\t\t} else {\n\t\t\t\tresult[newKey] = value;\n\t\t\t}\n\t\t}\n\t}\n\n\trecurse(obj, \"\");\n\treturn result;\n}\n","import { scryptSync, randomBytes, timingSafeEqual } from \"node:crypto\";\n\nconst SCRYPT_OPTIONS = {\n\tN: 16384, // 2^14 - memory cost (16 MB)\n\tr: 8, // block size\n\tp: 1, // parallelization\n};\nconst KEY_LENGTH = 64;\nconst SALT_LENGTH = 16;\n\n/**\n * Hash a password using scrypt (memory-hard algorithm).\n * Returns a string in the format `scrypt:<salt>:<hash>` (hex-encoded).\n *\n * @example\n * ```ts\n * const hashed = await hashPassword(\"mypassword\");\n * // \"scrypt:a1b2c3d4...:e5f6g7h8...\"\n * ```\n */\nexport async function hashPassword(password: string): Promise<string> {\n\tconst salt = randomBytes(SALT_LENGTH);\n\tconst hash = scryptSync(password, salt, KEY_LENGTH, SCRYPT_OPTIONS);\n\treturn `scrypt:${salt.toString(\"hex\")}:${hash.toString(\"hex\")}`;\n}\n\n/**\n * Verify a password against a stored hash string.\n * Supports both scrypt (`scrypt:<salt>:<hash>`) and legacy PBKDF2 (`iterations:salt:hash`) formats.\n * Uses timing-safe comparison.\n *\n * @example\n * ```ts\n * const valid = await verifyPassword(\"mypassword\", hashed);  // true\n * const wrong = await verifyPassword(\"wrong\", hashed);        // false\n * ```\n */\nexport async function verifyPassword(\n\tpassword: string,\n\tstored: string,\n): Promise<boolean> {\n\t// Scrypt format\n\tif (stored.startsWith(\"scrypt:\")) {\n\t\tconst parts = stored.split(\":\");\n\t\tif (parts.length !== 3) return false;\n\n\t\tconst saltHex = parts[1] as string;\n\t\tconst expectedHash = parts[2] as string;\n\t\tconst salt = Buffer.from(saltHex, \"hex\");\n\t\tconst test = scryptSync(password, salt, KEY_LENGTH, SCRYPT_OPTIONS);\n\n\t\treturn timingSafeEqual(\n\t\t\tBuffer.from(expectedHash, \"hex\"),\n\t\t\ttest,\n\t\t);\n\t}\n\n\t// Legacy PBKDF2 format\n\treturn verifyPBKDF2Legacy(password, stored);\n}\n\n// ─── PBKDF2 Legacy Support ─────────────────────────────────────────────\n\nconst PBKDF2_ITERATIONS = 100_000;\nconst PBKDF2_KEY_LENGTH = 64;\nconst PBKDF2_DIGEST = \"SHA-256\";\n\nfunction toHex(buffer: Uint8Array): string {\n\treturn [...buffer].map((b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n}\n\nfunction fromHex(hex: string): Uint8Array {\n\treturn new Uint8Array(\n\t\thex.match(/.{1,2}/g)?.map((h) => Number.parseInt(h, 16)) ?? [],\n\t);\n}\n\nasync function verifyPBKDF2Legacy(\n\tpassword: string,\n\tstored: string,\n): Promise<boolean> {\n\tconst parts = stored.split(\":\");\n\tif (parts.length < 3) return false;\n\n\tconst iterations = Number(parts[0]);\n\tconst saltHex = parts[1] as string;\n\tconst expectedHash = parts[2] as string;\n\tconst salt = fromHex(saltHex) as Uint8Array<ArrayBuffer>;\n\n\tconst key = await crypto.subtle.importKey(\n\t\t\"raw\",\n\t\tnew TextEncoder().encode(password),\n\t\t\"PBKDF2\",\n\t\tfalse,\n\t\t[\"deriveBits\"],\n\t);\n\tconst derived = await crypto.subtle.deriveBits(\n\t\t{ name: \"PBKDF2\", salt, iterations, hash: PBKDF2_DIGEST },\n\t\tkey,\n\t\tPBKDF2_KEY_LENGTH * 8,\n\t);\n\tconst actual = toHex(new Uint8Array(derived));\n\n\tif (actual.length !== expectedHash.length) return false;\n\tlet result = 0;\n\tfor (let i = 0; i < actual.length; i++) {\n\t\tresult |= actual.charCodeAt(i) ^ expectedHash.charCodeAt(i);\n\t}\n\treturn result === 0;\n}\n","/**\n * Convert a string to a URL-friendly slug.\n * `\"Hello World!\"` → `\"hello-world\"`\n */\nexport function slugify(str: string): string {\n\treturn str\n\t\t.toLowerCase()\n\t\t.trim()\n\t\t.replace(/[^\\w\\s-]/g, \"\")\n\t\t.replace(/[\\s_]+/g, \"-\")\n\t\t.replace(/-+/g, \"-\")\n\t\t.replace(/^-|-$/g, \"\");\n}\n\n/**\n * Truncate a string to a maximum length with an ellipsis suffix.\n * `\"Lorem ipsum dolor sit\"` → `\"Lorem ipsum...\"` (max=13)\n */\nexport function truncate(str: string, max = 100, suffix = \"...\"): string {\n\tif (str.length <= max) return str;\n\treturn str.slice(0, max - suffix.length) + suffix;\n}\n\n/**\n * Capitalize the first letter of a string.\n * `\"hello\"` → `\"Hello\"`\n */\nexport function capitalize(str: string): string {\n\treturn str.charAt(0).toUpperCase() + str.slice(1);\n}\n\n/**\n * Convert a string to camelCase.\n * `\"hello-world\"` → `\"helloWorld\"`\n * `\"hello_world\"` → `\"helloWorld\"`\n */\nexport function camelCase(str: string): string {\n\treturn str\n\t\t.replace(/[-_\\s]+(.)/g, (_, c: string) => c.toUpperCase())\n\t\t.replace(/^(.)/, (_, c: string) => c.toLowerCase());\n}\n\n/**\n * Convert a string to snake_case.\n * `\"helloWorld\"` → `\"hello_world\"`\n * `\"hello-world\"` → `\"hello_world\"`\n */\nexport function snakeCase(str: string): string {\n\treturn str\n\t\t.replace(/([A-Z])/g, \"_$1\")\n\t\t.replace(/[-\\s]+/g, \"_\")\n\t\t.toLowerCase()\n\t\t.replace(/^_/, \"\")\n\t\t.replace(/_+/g, \"_\");\n}\n\n/**\n * Convert a string to kebab-case.\n * `\"helloWorld\"` → `\"hello-world\"`\n * `\"hello_world\"` → `\"hello-world\"`\n */\nexport function kebabCase(str: string): string {\n\treturn snakeCase(str).replace(/_/g, \"-\");\n}\n","export interface MailAttachment {\n\tfilename: string;\n\t/** Buffer, base64 string, or file path */\n\tcontent?: Buffer | string;\n\t/** Remote URL (Resend only) */\n\tpath?: string;\n\t/** MIME type (e.g., \"application/pdf\", \"image/png\") */\n\tcontentType?: string;\n\t/** Content-ID for inline images (e.g., \"logo\" for cid:logo) */\n\tcid?: string;\n}\n\nexport interface MailOptions {\n\tfrom: string;\n\tto: string | string[];\n\t/** Carbon Copy recipients */\n\tcc?: string | string[];\n\t/** Blind Carbon Copy recipients */\n\tbcc?: string | string[];\n\t/** Reply-To address (different from from) */\n\treplyTo?: string | string[];\n\tsubject: string;\n\ttext?: string;\n\thtml?: string;\n\t/** File attachments */\n\tattachments?: MailAttachment[];\n}\n\nexport interface MailerConfig {\n\tprovider: \"resend\" | \"sendgrid\" | \"mailgun\" | \"smtp\";\n\t/** Required for Resend, SendGrid, and Mailgun */\n\tapiKey?: string;\n\t/** Required for Mailgun */\n\tdomain?: string;\n\t/** Required for SMTP */\n\tsmtp?: {\n\t\thost: string;\n\t\tport: number;\n\t\tsecure?: boolean;\n\t\tauth: {\n\t\t\tuser: string;\n\t\t\tpass: string;\n\t\t};\n\t};\n}\n\n// ─── Helpers ──────────────────────────────────────────────────────────────────\n\nfunction toBase64(content: Buffer | string): string {\n\tif (typeof content === \"string\") {\n\t\t// Assume already base64 encoded\n\t\treturn content;\n\t}\n\treturn content.toString(\"base64\");\n}\n\nfunction toArray(value: string | string[]): string[] {\n\treturn Array.isArray(value) ? value : [value];\n}\n\nfunction inferMimeType(filename: string): string {\n\tconst ext = filename.split(\".\").pop()?.toLowerCase();\n\tconst mimeMap: Record<string, string> = {\n\t\tpdf: \"application/pdf\",\n\t\tpng: \"image/png\",\n\t\tjpg: \"image/jpeg\",\n\t\tjpeg: \"image/jpeg\",\n\t\tgif: \"image/gif\",\n\t\tsvg: \"image/svg+xml\",\n\t\twebp: \"image/webp\",\n\t\ttxt: \"text/plain\",\n\t\thtml: \"text/html\",\n\t\tcss: \"text/css\",\n\t\tjs: \"application/javascript\",\n\t\tjson: \"application/json\",\n\t\tzip: \"application/zip\",\n\t\tdoc: \"application/msword\",\n\t\tdocx: \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\",\n\t\txls: \"application/vnd.ms-excel\",\n\t\txlsx: \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\",\n\t};\n\treturn mimeMap[ext || \"\"] || \"application/octet-stream\";\n}\n\n/**\n * Built-in Mailer wrapper for Buntok.\n * Supports Resend, SendGrid, and Mailgun (HTTP-based, zero-deps).\n * Also supports SMTP via dynamic import of 'nodemailer' (requires user installation).\n */\nexport class Mailer {\n\tconstructor(private config: MailerConfig) {}\n\n\t/**\n\t * Send an email asynchronously.\n\t * If you don't await this method, it acts as a background queue (fire-and-forget).\n\t */\n\tasync send(\n\t\toptions: MailOptions,\n\t): Promise<{ success: boolean; id?: string; error?: string }> {\n\t\tif (this.config.provider === \"resend\") {\n\t\t\treturn this.sendResend(options);\n\t\t}\n\n\t\tif (this.config.provider === \"sendgrid\") {\n\t\t\treturn this.sendSendGrid(options);\n\t\t}\n\n\t\tif (this.config.provider === \"mailgun\") {\n\t\t\treturn this.sendMailgun(options);\n\t\t}\n\n\t\tif (this.config.provider === \"smtp\") {\n\t\t\treturn this.sendSmtp(options);\n\t\t}\n\n\t\treturn { success: false, error: \"Unsupported provider\" };\n\t}\n\n\t// ─── Resend ──────────────────────────────────────────────────────────────\n\n\tprivate async sendResend(\n\t\toptions: MailOptions,\n\t): Promise<{ success: boolean; id?: string; error?: string }> {\n\t\ttry {\n\t\t\tconst payload: Record<string, unknown> = {\n\t\t\t\tfrom: options.from,\n\t\t\t\tto: toArray(options.to),\n\t\t\t\tsubject: options.subject,\n\t\t\t\ttext: options.text,\n\t\t\t\thtml: options.html,\n\t\t\t};\n\n\t\t\tif (options.cc) payload.cc = toArray(options.cc);\n\t\t\tif (options.bcc) payload.bcc = toArray(options.bcc);\n\t\t\tif (options.replyTo) payload.reply_to = toArray(options.replyTo);\n\n\t\t\tif (options.attachments && options.attachments.length > 0) {\n\t\t\t\tpayload.attachments = options.attachments.map((att) => {\n\t\t\t\t\tconst attachment: Record<string, unknown> = {\n\t\t\t\t\t\tfilename: att.filename,\n\t\t\t\t\t};\n\n\t\t\t\t\tif (att.path) {\n\t\t\t\t\t\tattachment.path = att.path;\n\t\t\t\t\t} else if (att.content) {\n\t\t\t\t\t\tattachment.content = toBase64(att.content);\n\t\t\t\t\t}\n\n\t\t\t\t\tif (att.contentType) {\n\t\t\t\t\t\tattachment.content_type = att.contentType;\n\t\t\t\t\t} else if (att.filename) {\n\t\t\t\t\t\tattachment.content_type = inferMimeType(att.filename);\n\t\t\t\t\t}\n\n\t\t\t\t\tif (att.cid) {\n\t\t\t\t\t\tattachment.content_id = att.cid;\n\t\t\t\t\t}\n\n\t\t\t\t\treturn attachment;\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tconst res = await fetch(\"https://api.resend.com/emails\", {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\theaders: {\n\t\t\t\t\tAuthorization: `Bearer ${this.config.apiKey}`,\n\t\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\t},\n\t\t\t\tbody: JSON.stringify(payload),\n\t\t\t});\n\n\t\t\tconst data = (await res.json()) as Record<string, unknown>;\n\t\t\tif (!res.ok) return { success: false, error: data.message as string };\n\t\t\treturn { success: true, id: data.id as string };\n\t\t} catch (err: unknown) {\n\t\t\tconst e = err as Error;\n\t\t\treturn { success: false, error: e.message };\n\t\t}\n\t}\n\n\t// ─── SendGrid ────────────────────────────────────────────────────────────\n\n\tprivate async sendSendGrid(\n\t\toptions: MailOptions,\n\t): Promise<{ success: boolean; id?: string; error?: string }> {\n\t\ttry {\n\t\t\tconst toArray = (v: string | string[]) =>\n\t\t\t\t(Array.isArray(v) ? v : [v]).map((email) => ({ email }));\n\n\t\t\tconst personalization: Record<string, unknown> = {\n\t\t\t\tto: toArray(options.to),\n\t\t\t};\n\t\t\tif (options.cc) personalization.cc = toArray(options.cc);\n\t\t\tif (options.bcc) personalization.bcc = toArray(options.bcc);\n\n\t\t\tconst payload: Record<string, unknown> = {\n\t\t\t\tpersonalizations: [personalization],\n\t\t\t\tfrom: { email: options.from },\n\t\t\t\tsubject: options.subject,\n\t\t\t};\n\n\t\t\tif (options.replyTo) {\n\t\t\t\tconst replyToArr = toArray(options.replyTo);\n\t\t\t\tif (replyToArr.length === 1) {\n\t\t\t\t\tpayload.reply_to = { email: replyToArr[0] };\n\t\t\t\t} else {\n\t\t\t\t\tpayload.reply_to_list = replyToArr.map((email) => ({ email }));\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst content = [];\n\t\t\tif (options.text)\n\t\t\t\tcontent.push({ type: \"text/plain\", value: options.text });\n\t\t\tif (options.html)\n\t\t\t\tcontent.push({ type: \"text/html\", value: options.html });\n\t\t\tpayload.content = content;\n\n\t\t\tif (options.attachments && options.attachments.length > 0) {\n\t\t\t\tpayload.attachments = options.attachments.map((att) => {\n\t\t\t\t\tconst attachment: Record<string, unknown> = {\n\t\t\t\t\t\tcontent: att.content ? toBase64(att.content) : \"\",\n\t\t\t\t\t\tfilename: att.filename,\n\t\t\t\t\t\tdisposition: att.cid ? \"inline\" : \"attachment\",\n\t\t\t\t\t};\n\n\t\t\t\t\tif (att.contentType) {\n\t\t\t\t\t\tattachment.type = att.contentType;\n\t\t\t\t\t} else if (att.filename) {\n\t\t\t\t\t\tattachment.type = inferMimeType(att.filename);\n\t\t\t\t\t}\n\n\t\t\t\t\tif (att.cid) {\n\t\t\t\t\t\tattachment.content_id = att.cid;\n\t\t\t\t\t}\n\n\t\t\t\t\treturn attachment;\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tconst res = await fetch(\"https://api.sendgrid.com/v3/mail/send\", {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\theaders: {\n\t\t\t\t\tAuthorization: `Bearer ${this.config.apiKey}`,\n\t\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\t},\n\t\t\t\tbody: JSON.stringify(payload),\n\t\t\t});\n\n\t\t\tif (!res.ok) {\n\t\t\t\tconst data = (await res.json()) as { errors?: { message: string }[] };\n\t\t\t\treturn {\n\t\t\t\t\tsuccess: false,\n\t\t\t\t\terror: data.errors?.[0]?.message || \"SendGrid Error\",\n\t\t\t\t};\n\t\t\t}\n\t\t\treturn { success: true };\n\t\t} catch (err: unknown) {\n\t\t\tconst e = err as Error;\n\t\t\treturn { success: false, error: e.message };\n\t\t}\n\t}\n\n\t// ─── Mailgun ─────────────────────────────────────────────────────────────\n\n\tprivate async sendMailgun(\n\t\toptions: MailOptions,\n\t): Promise<{ success: boolean; id?: string; error?: string }> {\n\t\ttry {\n\t\t\tif (!this.config.domain)\n\t\t\t\tthrow new Error(\"Mailgun requires a domain configured\");\n\n\t\t\tconst formData = new FormData();\n\t\t\tformData.append(\"from\", options.from);\n\n\t\t\tconst toArray = (v: string | string[]) =>\n\t\t\t\tArray.isArray(v) ? v : [v];\n\t\t\tfor (const t of toArray(options.to)) formData.append(\"to\", t);\n\t\t\tif (options.cc) for (const c of toArray(options.cc)) formData.append(\"cc\", c);\n\t\t\tif (options.bcc) for (const b of toArray(options.bcc)) formData.append(\"bcc\", b);\n\n\t\t\tformData.append(\"subject\", options.subject);\n\t\t\tif (options.text) formData.append(\"text\", options.text);\n\t\t\tif (options.html) formData.append(\"html\", options.html);\n\n\t\t\tif (options.replyTo) {\n\t\t\t\tconst replyTo = toArray(options.replyTo).join(\", \");\n\t\t\t\tformData.append(\"h:Reply-To\", replyTo);\n\t\t\t}\n\n\t\t\tif (options.attachments && options.attachments.length > 0) {\n\t\t\t\tfor (const att of options.attachments) {\n\t\t\t\t\tif (att.cid) {\n\t\t\t\t\t\t// Inline attachment\n\t\t\t\t\t\tif (att.content) {\n\t\t\t\t\t\t\tconst blob =\n\t\t\t\t\t\t\t\ttypeof att.content === \"string\"\n\t\t\t\t\t\t\t\t\t? new Blob([Uint8Array.from(atob(att.content), (c) => c.charCodeAt(0))])\n\t\t\t\t\t\t\t\t\t: new Blob([att.content]);\n\t\t\t\t\t\t\tformData.append(\"inline\", blob, att.filename);\n\t\t\t\t\t\t} else if (att.path) {\n\t\t\t\t\t\t\tconst res = await fetch(att.path);\n\t\t\t\t\t\t\tconst blob = await res.blob();\n\t\t\t\t\t\t\tformData.append(\"inline\", blob, att.filename);\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Regular attachment\n\t\t\t\t\t\tif (att.content) {\n\t\t\t\t\t\t\tconst blob =\n\t\t\t\t\t\t\t\ttypeof att.content === \"string\"\n\t\t\t\t\t\t\t\t\t? new Blob([Uint8Array.from(atob(att.content), (c) => c.charCodeAt(0))])\n\t\t\t\t\t\t\t\t\t: new Blob([att.content]);\n\t\t\t\t\t\t\tformData.append(\"attachment\", blob, att.filename);\n\t\t\t\t\t\t} else if (att.path) {\n\t\t\t\t\t\t\tconst res = await fetch(att.path);\n\t\t\t\t\t\t\tconst blob = await res.blob();\n\t\t\t\t\t\t\tformData.append(\"attachment\", blob, att.filename);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst basicAuth = btoa(`api:${this.config.apiKey}`);\n\t\t\tconst res = await fetch(\n\t\t\t\t`https://api.mailgun.net/v3/${this.config.domain}/messages`,\n\t\t\t\t{\n\t\t\t\t\tmethod: \"POST\",\n\t\t\t\t\theaders: { Authorization: `Basic ${basicAuth}` },\n\t\t\t\t\tbody: formData,\n\t\t\t\t},\n\t\t\t);\n\n\t\t\tconst data = (await res.json()) as Record<string, unknown>;\n\t\t\tif (!res.ok) return { success: false, error: data.message as string };\n\t\t\treturn { success: true, id: data.id as string };\n\t\t} catch (err: unknown) {\n\t\t\tconst e = err as Error;\n\t\t\treturn { success: false, error: e.message };\n\t\t}\n\t}\n\n\t// ─── SMTP (nodemailer) ──────────────────────────────────────────────────\n\n\tprivate async sendSmtp(\n\t\toptions: MailOptions,\n\t): Promise<{ success: boolean; id?: string; error?: string }> {\n\t\tif (!this.config.smtp)\n\t\t\treturn { success: false, error: \"SMTP configuration missing\" };\n\t\ttry {\n\t\t\t// Dynamic import to keep Buntok dependency-free\n\t\t\t// @ts-expect-error\n\t\t\tconst nodemailer = await import(\"nodemailer\");\n\t\t\tconst transporter = nodemailer.createTransport(this.config.smtp);\n\n\t\t\tconst mailOptions: Record<string, unknown> = {\n\t\t\t\tfrom: options.from,\n\t\t\t\tto: toArray(options.to).join(\",\"),\n\t\t\t\tsubject: options.subject,\n\t\t\t\ttext: options.text,\n\t\t\t\thtml: options.html,\n\t\t\t};\n\n\t\t\tif (options.cc) mailOptions.cc = toArray(options.cc).join(\",\");\n\t\t\tif (options.bcc) mailOptions.bcc = toArray(options.bcc).join(\",\");\n\t\t\tif (options.replyTo) mailOptions.replyTo = toArray(options.replyTo).join(\",\");\n\n\t\t\tif (options.attachments && options.attachments.length > 0) {\n\t\t\t\tmailOptions.attachments = options.attachments.map((att) => {\n\t\t\t\t\tconst attachment: Record<string, unknown> = {\n\t\t\t\t\t\tfilename: att.filename,\n\t\t\t\t\t};\n\n\t\t\t\t\tif (att.path) {\n\t\t\t\t\t\tattachment.path = att.path;\n\t\t\t\t\t} else if (att.content) {\n\t\t\t\t\t\tattachment.content =\n\t\t\t\t\t\t\ttypeof att.content === \"string\"\n\t\t\t\t\t\t\t\t? Buffer.from(att.content, \"base64\")\n\t\t\t\t\t\t\t\t: att.content;\n\t\t\t\t\t}\n\n\t\t\t\t\tif (att.contentType) {\n\t\t\t\t\t\tattachment.contentType = att.contentType;\n\t\t\t\t\t}\n\n\t\t\t\t\tif (att.cid) {\n\t\t\t\t\t\tattachment.cid = att.cid;\n\t\t\t\t\t\tattachment.contentDisposition = \"inline\";\n\t\t\t\t\t}\n\n\t\t\t\t\treturn attachment;\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tconst info = await transporter.sendMail(mailOptions);\n\t\t\treturn { success: true, id: info.messageId };\n\t\t} catch (err: unknown) {\n\t\t\tconst e = err as { code?: string; message: string };\n\t\t\tif (e.code === \"ERR_MODULE_NOT_FOUND\") {\n\t\t\t\treturn {\n\t\t\t\t\tsuccess: false,\n\t\t\t\t\terror: \"Nodemailer is required for SMTP. Run 'bun add nodemailer'\",\n\t\t\t\t};\n\t\t\t}\n\t\t\treturn { success: false, error: e.message };\n\t\t}\n\t}\n}\n","// ──────────────────────────────────────────────────────────────────────────────\n// Buntok Template Engine - Handlebars-like, zero-deps\n// ──────────────────────────────────────────────────────────────────────────────\n\nexport interface TemplateOptions {\n\tstrict?: boolean;\n\tescapeHtml?: boolean;\n\tonMissing?: (path: string, availableKeys: string[]) => void;\n}\n\nexport interface HelperFn {\n\t(...args: unknown[]): string;\n}\n\n// ─── AST Node Types ───────────────────────────────────────────────────────────\n\ninterface TextNode {\n\ttype: \"text\";\n\tvalue: string;\n}\n\ninterface VariableNode {\n\ttype: \"variable\";\n\tpath: string;\n\tescaped: boolean;\n}\n\ninterface BlockNode {\n\ttype: \"block\";\n\tblockType: \"if\" | \"unless\" | \"each\";\n\texpression: string;\n\tchildren: ASTNode[];\n\tinverse?: ASTNode[];\n}\n\ninterface PartialNode {\n\ttype: \"partial\";\n\tname: string;\n}\n\ninterface CommentNode {\n\ttype: \"comment\";\n\tvalue: string;\n}\n\ntype ASTNode = TextNode | VariableNode | BlockNode | PartialNode | CommentNode;\n\n// ─── Tokenizer ────────────────────────────────────────────────────────────────\n\ntype TokenType =\n\t| \"TEXT\"\n\t| \"VARIABLE\"\n\t| \"BLOCK_OPEN\"\n\t| \"BLOCK_CLOSE\"\n\t| \"PARTIAL\"\n\t| \"COMMENT\";\n\ninterface Token {\n\ttype: TokenType;\n\tvalue: string;\n\tpath?: string;\n\tescaped?: boolean;\n}\n\nfunction tokenize(template: string): Token[] {\n\tconst tokens: Token[] = [];\n\tlet pos = 0;\n\n\twhile (pos < template.length) {\n\t\tconst openIndex = template.indexOf(\"{{\", pos);\n\t\tif (openIndex === -1) {\n\t\t\ttokens.push({ type: \"TEXT\", value: template.slice(pos) });\n\t\t\tbreak;\n\t\t}\n\n\t\tif (openIndex > pos) {\n\t\t\ttokens.push({ type: \"TEXT\", value: template.slice(pos, openIndex) });\n\t\t}\n\n\t\t// Look for }}} first (triple braces), then }} (double braces)\n\t\tlet closeIndex = template.indexOf(\"}}}\", openIndex + 2);\n\t\tlet closeLen = 3;\n\t\tlet tagStart = openIndex + 2;\n\t\tif (closeIndex === -1) {\n\t\t\tcloseIndex = template.indexOf(\"}}\", openIndex + 2);\n\t\t\tcloseLen = 2;\n\t\t} else {\n\t\t\t// Triple braces: skip the third {\n\t\t\ttagStart = openIndex + 3;\n\t\t}\n\t\tif (closeIndex === -1) {\n\t\t\ttokens.push({ type: \"TEXT\", value: template.slice(openIndex) });\n\t\t\tbreak;\n\t\t}\n\n\t\tconst tagContent = template.slice(tagStart, closeIndex).trim();\n\t\tpos = closeIndex + closeLen;\n\n\t\t// Triple braces = unescaped output\n\t\tconst isTripleBrace = closeLen === 3;\n\n\t\t// Classify tag\n\t\tif (tagContent.startsWith(\"!\")) {\n\t\t\ttokens.push({ type: \"COMMENT\", value: tagContent.slice(1).trim() });\n\t\t} else if (tagContent === \"else\") {\n\t\t\ttokens.push({ type: \"BLOCK_OPEN\", value: \"else\" });\n\t\t} else if (tagContent.startsWith(\"#\")) {\n\t\t\ttokens.push({ type: \"BLOCK_OPEN\", value: tagContent.slice(1).trim() });\n\t\t} else if (tagContent.startsWith(\"/\")) {\n\t\t\ttokens.push({ type: \"BLOCK_CLOSE\", value: tagContent.slice(1).trim() });\n\t\t} else if (tagContent.startsWith(\">\")) {\n\t\t\ttokens.push({ type: \"PARTIAL\", value: tagContent.slice(1).trim() });\n\t\t} else {\n\t\t\ttokens.push({ type: \"VARIABLE\", value: tagContent, path: tagContent, escaped: !isTripleBrace });\n\t\t}\n\t}\n\n\treturn tokens;\n}\n\n// ─── Parser ───────────────────────────────────────────────────────────────────\n\nfunction parseBlockType(value: string): { blockType: \"if\" | \"unless\" | \"each\"; expression: string } {\n\tconst spaceIndex = value.indexOf(\" \");\n\tconst blockType = (spaceIndex === -1 ? value : value.slice(0, spaceIndex)) as \"if\" | \"unless\" | \"each\";\n\tconst expression = spaceIndex === -1 ? \"\" : value.slice(spaceIndex + 1).trim();\n\treturn { blockType, expression };\n}\n\nfunction parse(tokens: Token[]): ASTNode[] {\n\tconst nodes: ASTNode[] = [];\n\tlet pos = 0;\n\n\twhile (pos < tokens.length) {\n\t\tconst token = tokens[pos];\n\t\tif (!token) break;\n\n\t\tif (token.type === \"TEXT\") {\n\t\t\tnodes.push({ type: \"text\", value: token.value });\n\t\t\tpos++;\n\t\t} else if (token.type === \"VARIABLE\") {\n\t\t\tnodes.push({\n\t\t\t\ttype: \"variable\",\n\t\t\t\tpath: token.path || \"\",\n\t\t\t\tescaped: token.escaped ?? true,\n\t\t\t});\n\t\t\tpos++;\n\t\t} else if (token.type === \"BLOCK_OPEN\") {\n\t\t\tconst { blockType, expression } = parseBlockType(token.value);\n\t\t\tpos++;\n\n\t\t\t// Handle standalone {{else}} token\n\t\t\tif (blockType === (\"else\" as string)) {\n\t\t\t\t// This shouldn't happen at top level, but handle gracefully\n\t\t\t\treturn nodes;\n\t\t\t}\n\n\t\t\tconst children: ASTNode[] = [];\n\t\t\tconst inverse: ASTNode[] = [];\n\t\t\tlet depth = 1;\n\t\t\tlet inElse = false;\n\n\t\t\twhile (pos < tokens.length && depth > 0) {\n\t\t\t\tconst t = tokens[pos];\n\t\t\t\tif (!t) break;\n\n\t\t\t\tif (t.type === \"BLOCK_OPEN\" && t.value === \"else\") {\n\t\t\t\t\tinElse = true;\n\t\t\t\t\tpos++;\n\t\t\t\t} else if (t.type === \"BLOCK_OPEN\") {\n\t\t\t\t\tconst nested = parseBlockType(t.value);\n\t\t\t\t\tdepth++;\n\t\t\t\t\t// Parse nested block properly\n\t\t\t\t\tpos++;\n\t\t\t\t\tconst nestedChildren: ASTNode[] = [];\n\t\t\t\t\tconst nestedInverse: ASTNode[] = [];\n\t\t\t\t\tlet nestedDepth = 1;\n\t\t\t\t\tlet nestedInElse = false;\n\n\t\t\t\t\twhile (pos < tokens.length && nestedDepth > 0) {\n\t\t\t\t\t\tconst nt = tokens[pos];\n\t\t\t\t\t\tif (!nt) break;\n\t\t\t\t\t\tif (nt.type === \"BLOCK_OPEN\" && nt.value === \"else\") {\n\t\t\t\t\t\t\tnestedInElse = true;\n\t\t\t\t\t\t\tpos++;\n\t\t\t\t\t\t} else if (nt.type === \"BLOCK_OPEN\") {\n\t\t\t\t\t\t\tnestedDepth++;\n\t\t\t\t\t\t\tnestedChildren.push(...parse([nt]));\n\t\t\t\t\t\t\tpos++;\n\t\t\t\t\t\t} else if (nt.type === \"BLOCK_CLOSE\") {\n\t\t\t\t\t\t\tnestedDepth--;\n\t\t\t\t\t\t\tif (nestedDepth === 0) {\n\t\t\t\t\t\t\t\tpos++;\n\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tnestedInverse.push({ type: \"text\", value: `{{/${nt.value}}}` });\n\t\t\t\t\t\t\tpos++;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tif (nestedInElse) nestedInverse.push({ type: \"text\", value: nt.value });\n\t\t\t\t\t\t\telse nestedChildren.push({ type: \"text\", value: nt.value });\n\t\t\t\t\t\t\tpos++;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tconst nestedBlock: BlockNode = {\n\t\t\t\t\t\ttype: \"block\",\n\t\t\t\t\t\tblockType: nested.blockType,\n\t\t\t\t\t\texpression: nested.expression,\n\t\t\t\t\t\tchildren: nestedChildren,\n\t\t\t\t\t\tinverse: nestedInverse.length > 0 ? nestedInverse : undefined,\n\t\t\t\t\t};\n\t\t\t\t\tif (inElse) inverse.push(nestedBlock);\n\t\t\t\t\telse children.push(nestedBlock);\n\t\t\t\t} else if (t.type === \"BLOCK_CLOSE\") {\n\t\t\t\t\tdepth--;\n\t\t\t\t\tif (depth === 0) {\n\t\t\t\t\t\tpos++;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tinverse.push({ type: \"text\", value: `{{/${t.value}}}` });\n\t\t\t\t\tpos++;\n\t\t\t\t} else if (t.type === \"TEXT\") {\n\t\t\t\t\tif (inElse) {\n\t\t\t\t\t\tinverse.push({ type: \"text\", value: t.value });\n\t\t\t\t\t} else {\n\t\t\t\t\t\tchildren.push({ type: \"text\", value: t.value });\n\t\t\t\t\t}\n\t\t\t\t\tpos++;\n\t\t\t\t} else if (t.type === \"VARIABLE\") {\n\t\t\t\t\tconst node: VariableNode = {\n\t\t\t\t\t\ttype: \"variable\",\n\t\t\t\t\t\tpath: t.path || \"\",\n\t\t\t\t\t\tescaped: t.escaped ?? true,\n\t\t\t\t\t};\n\t\t\t\t\tif (inElse) inverse.push(node);\n\t\t\t\t\telse children.push(node);\n\t\t\t\t\tpos++;\n\t\t\t\t} else {\n\t\t\t\t\tpos++;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tnodes.push({\n\t\t\t\ttype: \"block\",\n\t\t\t\tblockType,\n\t\t\t\texpression,\n\t\t\t\tchildren,\n\t\t\t\tinverse: inverse.length > 0 ? inverse : undefined,\n\t\t\t});\n\t\t} else if (token.type === \"PARTIAL\") {\n\t\t\tnodes.push({ type: \"partial\", name: token.value });\n\t\t\tpos++;\n\t\t} else if (token.type === \"COMMENT\") {\n\t\t\tpos++;\n\t\t} else {\n\t\t\tpos++;\n\t\t}\n\t}\n\n\treturn nodes;\n}\n\n// ─── Renderer ─────────────────────────────────────────────────────────────────\n\nconst ESCAPE_MAP: Record<string, string> = {\n\t\"&\": \"&amp;\",\n\t\"<\": \"&lt;\",\n\t\">\": \"&gt;\",\n\t'\"': \"&quot;\",\n\t\"'\": \"&#x27;\",\n};\n\nfunction escapeHtml(str: string): string {\n\treturn str.replace(/[&<>\"']/g, (char) => ESCAPE_MAP[char] || char);\n}\n\nfunction resolvePath(path: string, context: unknown): unknown {\n\tconst parts = path.split(\".\");\n\tlet current = context;\n\n\tfor (const part of parts) {\n\t\tif (current == null || typeof current !== \"object\") {\n\t\t\treturn undefined;\n\t\t}\n\t\tcurrent = (current as Record<string, unknown>)[part];\n\t}\n\n\treturn current;\n}\n\nfunction collectKeys(obj: unknown, prefix = \"\"): string[] {\n\tconst keys: string[] = [];\n\tif (obj == null || typeof obj !== \"object\") return keys;\n\n\tfor (const [key, value] of Object.entries(obj as Record<string, unknown>)) {\n\t\tconst fullKey = prefix ? `${prefix}.${key}` : key;\n\t\tkeys.push(fullKey);\n\t\tif (value != null && typeof value === \"object\" && !Array.isArray(value)) {\n\t\t\tkeys.push(...collectKeys(value, fullKey));\n\t\t}\n\t}\n\treturn keys;\n}\n\nfunction levenshteinDistance(a: string, b: string): number {\n\tconst m = a.length;\n\tconst n = b.length;\n\tconst dp: number[][] = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0) as number[]);\n\n\tfor (let i = 0; i <= m; i++) {\n\t\tconst row = dp[i];\n\t\tif (row) row[0] = i;\n\t}\n\tfor (let j = 0; j <= n; j++) {\n\t\tconst row = dp[0];\n\t\tif (row) row[j] = j;\n\t}\n\n\tfor (let i = 1; i <= m; i++) {\n\t\tfor (let j = 1; j <= n; j++) {\n\t\t\tconst row = dp[i];\n\t\t\tconst prevRow = dp[i - 1];\n\t\t\tif (row && prevRow) {\n\t\t\t\trow[j] =\n\t\t\t\t\ta[i - 1] === b[j - 1]\n\t\t\t\t\t\t? (prevRow[j - 1] ?? 0)\n\t\t\t\t\t\t: Math.min(prevRow[j] ?? 0, row[j - 1] ?? 0, prevRow[j - 1] ?? 0) + 1;\n\t\t\t}\n\t\t}\n\t}\n\n\treturn dp[m]?.[n] ?? 0;\n}\n\nfunction suggestVariable(path: string, availableKeys: string[]): string | null {\n\tconst basePath = path.split(\".\")[0] || \"\";\n\tlet bestMatch: string | null = null;\n\tlet bestDistance = Infinity;\n\n\tfor (const key of availableKeys) {\n\t\tconst keyBase = key.split(\".\")[0] || \"\";\n\t\tconst distance = levenshteinDistance(basePath, keyBase);\n\t\tif (distance < bestDistance && distance <= 3) {\n\t\t\tbestDistance = distance;\n\t\t\tbestMatch = key;\n\t\t}\n\t}\n\n\treturn bestMatch;\n}\n\nfunction renderNodes(\n\tnodes: ASTNode[],\n\tcontext: unknown,\n\toptions: TemplateOptions,\n\tpartials: Map<string, ASTNode[]>,\n\thelpers: Map<string, HelperFn>,\n): string {\n\tconst parts: string[] = [];\n\n\tfor (const node of nodes) {\n\t\tswitch (node.type) {\n\t\t\tcase \"text\":\n\t\t\t\tparts.push(node.value);\n\t\t\t\tbreak;\n\n\t\t\tcase \"variable\": {\n\t\t\t\t// Check if this is a helper call (e.g., \"upper name\")\n\t\t\t\tconst spaceIdx = node.path.indexOf(\" \");\n\t\t\t\tif (spaceIdx !== -1) {\n\t\t\t\t\tconst helperName = node.path.slice(0, spaceIdx);\n\t\t\t\t\tconst argPath = node.path.slice(spaceIdx + 1).trim();\n\t\t\t\t\tconst helper = helpers.get(helperName);\n\t\t\t\t\tif (helper) {\n\t\t\t\t\t\tconst argValue = resolvePath(argPath, context);\n\t\t\t\t\t\tparts.push(helper(argValue));\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tconst value = resolvePath(node.path, context);\n\n\t\t\t\tif (value === undefined || value === null) {\n\t\t\t\t\tif (options.strict) {\n\t\t\t\t\t\tconst availableKeys = collectKeys(context);\n\t\t\t\t\t\tconst suggestion = suggestVariable(node.path, availableKeys);\n\t\t\t\t\t\tlet msg = `TemplateError: Variable '${node.path}' not found in context.`;\n\t\t\t\t\t\tif (availableKeys.length > 0) {\n\t\t\t\t\t\t\tmsg += `\\n  Available keys: ${availableKeys.slice(0, 10).join(\", \")}`;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (suggestion) {\n\t\t\t\t\t\t\tmsg += `\\n  Did you mean: '${suggestion}'?`;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tthrow new Error(msg);\n\t\t\t\t\t}\n\t\t\t\t\tif (options.onMissing) {\n\t\t\t\t\t\toptions.onMissing(node.path, collectKeys(context));\n\t\t\t\t\t}\n\t\t\t\t\tparts.push(\"\");\n\t\t\t\t} else if (typeof value === \"function\") {\n\t\t\t\t\tparts.push(String(value));\n\t\t\t\t} else if (node.escaped) {\n\t\t\t\t\tparts.push(escapeHtml(String(value)));\n\t\t\t\t} else {\n\t\t\t\t\tparts.push(String(value));\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"block\": {\n\t\t\t\tconst value = resolvePath(node.expression, context);\n\n\t\t\t\tif (node.blockType === \"if\") {\n\t\t\t\t\tif (value && !(Array.isArray(value) && value.length === 0)) {\n\t\t\t\t\t\tparts.push(renderNodes(node.children, context, options, partials, helpers));\n\t\t\t\t\t} else if (node.inverse) {\n\t\t\t\t\t\tparts.push(renderNodes(node.inverse, context, options, partials, helpers));\n\t\t\t\t\t}\n\t\t\t\t} else if (node.blockType === \"unless\") {\n\t\t\t\t\tif (!value || (Array.isArray(value) && value.length === 0)) {\n\t\t\t\t\t\tparts.push(renderNodes(node.children, context, options, partials, helpers));\n\t\t\t\t\t} else if (node.inverse) {\n\t\t\t\t\t\tparts.push(renderNodes(node.inverse, context, options, partials, helpers));\n\t\t\t\t\t}\n\t\t\t\t} else if (node.blockType === \"each\") {\n\t\t\t\t\tif (Array.isArray(value)) {\n\t\t\t\t\t\tfor (let i = 0; i < value.length; i++) {\n\t\t\t\t\t\t\tconst item = value[i];\n\t\t\t\t\t\t\tconst eachContext =\n\t\t\t\t\t\t\t\ttypeof item === \"object\" && item !== null\n\t\t\t\t\t\t\t\t\t? { ...item, \"@index\": i, \"@first\": i === 0, \"@last\": i === value.length - 1, \"../\": context }\n\t\t\t\t\t\t\t\t\t: { \"@value\": item, \"@index\": i, \"@first\": i === 0, \"@last\": i === value.length - 1, \"../\": context };\n\t\t\t\t\t\t\tparts.push(\n\t\t\t\t\t\t\t\trenderNodes(node.children, eachContext, options, partials, helpers),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if (node.inverse) {\n\t\t\t\t\t\tparts.push(renderNodes(node.inverse, context, options, partials, helpers));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"partial\": {\n\t\t\t\tconst partialNodes = partials.get(node.name);\n\t\t\t\tif (partialNodes) {\n\t\t\t\t\tparts.push(renderNodes(partialNodes, context, options, partials, helpers));\n\t\t\t\t} else if (options.strict) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`TemplateError: Partial '${node.name}' not found. Register it with engine.registerPartial().`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"comment\":\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\treturn parts.join(\"\");\n}\n\n// ─── TemplateEngine Class ─────────────────────────────────────────────────────\n\nexport class TemplateEngine {\n\tprivate options: TemplateOptions;\n\tprivate partials = new Map<string, ASTNode[]>();\n\tprivate helpers = new Map<string, HelperFn>();\n\n\tconstructor(options: TemplateOptions = {}) {\n\t\tthis.options = {\n\t\t\tstrict: true,\n\t\t\tescapeHtml: true,\n\t\t\t...options,\n\t\t};\n\t}\n\n\tregisterPartial(name: string, template: string): void {\n\t\tconst tokens = tokenize(template);\n\t\tthis.partials.set(name, parse(tokens));\n\t}\n\n\tregisterHelper(name: string, fn: HelperFn): void {\n\t\tthis.helpers.set(name, fn);\n\t}\n\n\trender(template: string, context: unknown): string {\n\t\tconst tokens = tokenize(template);\n\t\tconst ast = parse(tokens);\n\t\treturn renderNodes(ast, context, this.options, this.partials, this.helpers);\n\t}\n\n\tcompile(template: string): (context: unknown) => string {\n\t\tconst tokens = tokenize(template);\n\t\tconst ast = parse(tokens);\n\t\treturn (context: unknown) =>\n\t\t\trenderNodes(ast, context, this.options, this.partials, this.helpers);\n\t}\n}\n\n// ─── Convenience Functions ────────────────────────────────────────────────────\n\nconst defaultEngine = new TemplateEngine();\n\nexport function render(template: string, context: unknown): string {\n\treturn defaultEngine.render(template, context);\n}\n\nexport function registerPartial(name: string, template: string): void {\n\tdefaultEngine.registerPartial(name, template);\n}\n\nexport function registerHelper(name: string, fn: HelperFn): void {\n\tdefaultEngine.registerHelper(name, fn);\n}\n","/**\n * Audit logging middleware for tracking user actions.\n *\n * @example\n * ```ts\n * import { auditLog } from \"buntok\";\n *\n * // Log all requests\n * app.use(auditLog());\n *\n * // Log with custom storage\n * app.use(auditLog({\n *   storage: async (entry) => {\n *     await db.auditLog.create({ data: entry });\n *   },\n *   excludePaths: [\"/health\", \"/docs\"],\n *   excludeMethods: [\"OPTIONS\"],\n * }));\n * ```\n */\n\nexport interface AuditLogEntry {\n\ttimestamp: string;\n\tmethod: string;\n\tpath: string;\n\tstatus: number;\n\tduration: number;\n\tip?: string;\n\tuserId?: string | number;\n\tuserAgent?: string;\n\tbody?: any;\n\tquery?: Record<string, any>;\n\terror?: string;\n}\n\nexport interface AuditLogOptions {\n\t/** Custom storage function. If not provided, logs to console. */\n\tstorage?: (entry: AuditLogEntry) => Promise<void> | void;\n\n\t/** Paths to exclude from logging */\n\texcludePaths?: string[];\n\n\t/** Methods to exclude from logging */\n\texcludeMethods?: string[];\n\n\t/** Whether to log request body (default: false) */\n\tlogBody?: boolean;\n\n\t/** Whether to log query params (default: true) */\n\tlogQuery?: boolean;\n\n\t/** Max body size to log in bytes (default: 1024) */\n\tmaxBodySize?: number;\n}\n\nfunction shouldLog(\n\tpath: string,\n\tmethod: string,\n\toptions: AuditLogOptions,\n): boolean {\n\tif (options.excludePaths?.some((p) => path.startsWith(p))) return false;\n\tif (options.excludeMethods?.includes(method)) return false;\n\treturn true;\n}\n\nfunction truncate(str: string, max: number): string {\n\tif (str.length <= max) return str;\n\treturn `${str.slice(0, max)}...[truncated]`;\n}\n\n/**\n * Audit logging middleware.\n * Logs all requests with timing, user info, and optional body/query.\n */\nexport function auditLog(options: AuditLogOptions = {}) {\n\tconst {\n\t\tlogBody = false,\n\t\tlogQuery = true,\n\t\tmaxBodySize = 1024,\n\t} = options;\n\n\treturn async (ctx: any, next: () => Promise<Response> | Response) => {\n\t\tconst method = ctx.request.method;\n\t\tconst path = ctx.request.url;\n\t\tconst start = performance.now();\n\n\t\tif (!shouldLog(path, method, options)) {\n\t\t\treturn next();\n\t\t}\n\n\t\tlet response: Response | undefined;\n\t\ttry {\n\t\t\tresponse = await next();\n\t\t} finally {\n\t\t\tconst duration = performance.now() - start;\n\t\t\tconst status = response?.status ?? 200;\n\n\t\t\tconst entry: AuditLogEntry = {\n\t\t\t\ttimestamp: new Date().toISOString(),\n\t\t\t\tmethod,\n\t\t\t\tpath,\n\t\t\t\tstatus,\n\t\t\t\tduration: Math.round(duration * 100) / 100,\n\t\t\t\tip: ctx.request.headers?.get(\"x-forwarded-for\") ?? \"\",\n\t\t\t\tuserId: ctx.user?.id,\n\t\t\t\tuserAgent: ctx.request.headers?.get(\"user-agent\"),\n\t\t\t};\n\n\t\t\tif (logBody) {\n\t\t\t\ttry {\n\t\t\t\t\tconst cloned = ctx.request.clone();\n\t\t\t\t\tconst bodyStr = await cloned.text();\n\t\t\t\t\tentry.body = truncate(bodyStr, maxBodySize);\n\t\t\t\t} catch {\n\t\t\t\t\t// body already consumed\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (logQuery) {\n\t\t\t\ttry {\n\t\t\t\t\tconst url = new URL(ctx.request.url);\n\t\t\t\t\tconst query = Object.fromEntries(url.searchParams);\n\t\t\t\t\tif (Object.keys(query).length > 0) {\n\t\t\t\t\t\tentry.query = query;\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\t// invalid URL\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (status >= 400) {\n\t\t\t\tentry.error = ctx._error?.message;\n\t\t\t}\n\n\t\t\t// Custom storage or console\n\t\t\tif (options.storage) {\n\t\t\t\tawait options.storage(entry);\n\t\t\t} else {\n\t\t\t\tconst logFn = status >= 500\n\t\t\t\t\t? console.error\n\t\t\t\t\t: status >= 400\n\t\t\t\t\t\t? console.warn\n\t\t\t\t\t\t: console.log;\n\t\t\t\tlogFn(`[audit] ${method} ${path} ${status} ${entry.duration}ms`);\n\t\t\t}\n\t\t}\n\n\t\treturn response as Response;\n\t};\n}\n","import type { Middleware } from \"../app\";\nimport { BadRequestError } from \"../helpers/async-handler\";\n\nexport interface BodySizeLimitOptions {\n\t/**\n\t * Maximum allowed request body size in bytes.\n\t * Defaults to 10MB (10 * 1024 * 1024).\n\t */\n\tmaxSize?: number;\n\n\t/**\n\t * Custom error message when body size exceeds limit.\n\t * Defaults to: \"Request body too large. Maximum size is {maxSize} bytes.\"\n\t */\n\tmessage?: string;\n\n\t/**\n\t * HTTP status code for oversized requests.\n\t * Defaults to 413 (Payload Too Large).\n\t */\n\tstatusCode?: number;\n}\n\n/**\n * Middleware to limit incoming request body size.\n *\n * Checks the Content-Length header before the body is parsed.\n * Returns 413 (Payload Too Large) if the body exceeds the configured limit.\n *\n * @example\n * // Global: limit all requests to 1MB\n * app.use(bodySizeLimit({ maxSize: 1024 * 1024 }));\n *\n * @example\n * // Per-route: limit file upload endpoint to 50MB\n * app.post(\"/upload\", bodySizeLimit({ maxSize: 50 * 1024 * 1024 }), handler);\n *\n * @example\n * // Custom error message\n * app.use(bodySizeLimit({\n *   maxSize: 1024 * 1024,\n *   message: \"File too large. Max 1MB allowed.\",\n * }));\n */\nexport function bodySizeLimit(options?: BodySizeLimitOptions): Middleware {\n\tconst maxSize = options?.maxSize ?? 10 * 1024 * 1024; // 10MB default\n\tconst statusCode = options?.statusCode ?? 413;\n\n\treturn async (ctx, next) => {\n\t\tconst contentLength = ctx.request.headers.get(\"content-length\");\n\n\t\tif (contentLength) {\n\t\t\tconst size = Number.parseInt(contentLength, 10);\n\n\t\t\tif (!Number.isNaN(size) && size > maxSize) {\n\t\t\t\tconst message =\n\t\t\t\t\toptions?.message ??\n\t\t\t\t\t`Request body too large. Maximum size is ${maxSize} bytes.`;\n\n\t\t\t\treturn ctx.json(\n\t\t\t\t\t{\n\t\t\t\t\t\tsuccess: false,\n\t\t\t\t\t\tmessage,\n\t\t\t\t\t},\n\t\t\t\t\tstatusCode,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\treturn next();\n\t};\n}\n","import type { Middleware } from \"../app\";\n\nexport interface CompressOptions {\n\t/**\n\t * Minimum response body size (bytes) before compressing. Compressing\n\t * tiny payloads wastes CPU for zero/negative gain, so this defaults to\n\t * 1024 rather than compressing everything unconditionally.\n\t */\n\tthreshold?: number;\n\t/** Content-Type prefixes eligible for compression. */\n\ttypes?: string[];\n\t/** Brotli compression level (1-11). Default 4 gives ~85% of max compression\n\t * at 3-5x the speed of level 11. Set to 11 for maximum ratio. */\n\tbrotliLevel?: number;\n}\n\nconst DEFAULT_TYPES = [\n\t\"text/\",\n\t\"application/json\",\n\t\"application/javascript\",\n\t\"application/xml\",\n\t\"image/svg+xml\",\n];\n\nconst DEFAULT_OPTIONS: Required<CompressOptions> = {\n\tthreshold: 1024,\n\ttypes: DEFAULT_TYPES,\n\tbrotliLevel: 4,\n};\n\n/**\n * Compress response bodies with gzip or brotli, negotiated from the\n * request's `Accept-Encoding` header. Opt-in via `app.use(compress())` -\n * compression is real CPU work, so routes that don't need it (or that\n * already stream pre-compressed/binary content) shouldn't pay for it.\n *\n * Uses Bun's native `Bun.gzipSync` (Zig, not a JS gzip implementation) and\n * falls back to `node:zlib`'s brotli when the client prefers `br`.\n *\n * Brotli defaults to level 4 for a good speed/ratio tradeoff - roughly\n * 3-5x faster than level 11 with only ~15% larger output.\n */\nexport function compress(options: CompressOptions = {}): Middleware {\n\tconst opts = { ...DEFAULT_OPTIONS, ...options };\n\n\treturn async (ctx, next) => {\n\t\tconst result = await next();\n\t\tif (!(result instanceof Response) || !result.body) return result;\n\n\t\tif (result.headers.get(\"Content-Encoding\")) return result;\n\n\t\tconst contentType = result.headers.get(\"Content-Type\") || \"\";\n\t\t// Industrial: jangan compress SSE/WebSocket streaming - break spec\n\t\tif (contentType.includes(\"text/event-stream\")) return result;\n\t\tconst eligible = opts.types.some((t) => contentType.startsWith(t));\n\t\tif (!eligible) return result;\n\n\t\tconst acceptEncoding = ctx.request.headers.get(\"Accept-Encoding\") || \"\";\n\t\tconst supportsBrotli = acceptEncoding.includes(\"br\");\n\t\tconst supportsGzip = acceptEncoding.includes(\"gzip\");\n\t\tif (!supportsBrotli && !supportsGzip) return result;\n\n\t\t// Fast path: use Content-Length header to skip buffering for small responses\n\t\tconst contentLength = result.headers.get(\"Content-Length\");\n\t\tif (contentLength && Number(contentLength) < opts.threshold) {\n\t\t\treturn result;\n\t\t}\n\n\t\t// If no Content-Length, we must buffer to check size - but only compress\n\t\t// if above threshold. For small responses without Content-Length, skip\n\t\t// compression entirely to avoid unnecessary buffering + new Response.\n\t\tif (!contentLength) return result;\n\n\t\tconst buffer = new Uint8Array(await result.arrayBuffer());\n\t\tif (buffer.byteLength < opts.threshold) {\n\t\t\treturn result;\n\t\t}\n\n\t\tlet compressed: Uint8Array;\n\t\tlet encoding: string;\n\n\t\tif (supportsBrotli) {\n\t\t\tconst zlib = await import(\"node:zlib\");\n\t\t\tcompressed = zlib.brotliCompressSync(buffer, {\n\t\t\t\tparams: {\n\t\t\t\t\t[zlib.constants.BROTLI_PARAM_QUALITY]: opts.brotliLevel,\n\t\t\t\t},\n\t\t\t});\n\t\t\tencoding = \"br\";\n\t\t} else {\n\t\t\tcompressed = Bun.gzipSync(buffer);\n\t\t\tencoding = \"gzip\";\n\t\t}\n\n\t\tconst headers = new Headers(result.headers);\n\t\theaders.set(\"Content-Encoding\", encoding);\n\t\theaders.set(\"Vary\", \"Accept-Encoding\");\n\t\theaders.delete(\"Content-Length\");\n\n\t\treturn new Response(compressed, {\n\t\t\tstatus: result.status,\n\t\t\tstatusText: result.statusText,\n\t\t\theaders,\n\t\t});\n\t};\n}\n","import type { Middleware } from \"../app\";\n\nexport interface CorsOptions {\n\torigin?: string | string[] | ((origin: string) => boolean);\n\tmethods?: string[];\n\theaders?: string[];\n\tcredentials?: boolean;\n}\n\nexport const cors = (options: CorsOptions = {}): Middleware => {\n\treturn async (ctx, next) => {\n\t\tconst requestOrigin = ctx.request.headers.get(\"Origin\") || \"*\";\n\t\tlet allowedOrigin = \"*\";\n\n\t\tif (typeof options.origin === \"function\") {\n\t\t\tallowedOrigin = options.origin(requestOrigin) ? requestOrigin : \"\";\n\t\t} else if (Array.isArray(options.origin)) {\n\t\t\tallowedOrigin = options.origin.includes(requestOrigin)\n\t\t\t\t? requestOrigin\n\t\t\t\t: \"\";\n\t\t} else if (options.origin) {\n\t\t\tallowedOrigin = options.origin;\n\t\t}\n\n\t\tif (ctx.request.method === \"OPTIONS\") {\n\t\t\tconst res = new Response(null, { status: 204 });\n\t\t\tres.headers.set(\"Access-Control-Allow-Origin\", allowedOrigin);\n\t\t\tres.headers.set(\n\t\t\t\t\"Access-Control-Allow-Methods\",\n\t\t\t\t(\n\t\t\t\t\toptions.methods || [\n\t\t\t\t\t\t\"GET\",\n\t\t\t\t\t\t\"POST\",\n\t\t\t\t\t\t\"PUT\",\n\t\t\t\t\t\t\"DELETE\",\n\t\t\t\t\t\t\"PATCH\",\n\t\t\t\t\t\t\"OPTIONS\",\n\t\t\t\t\t]\n\t\t\t\t).join(\",\"),\n\t\t\t);\n\t\t\tres.headers.set(\n\t\t\t\t\"Access-Control-Allow-Headers\",\n\t\t\t\t(\n\t\t\t\t\toptions.headers || [\"Content-Type\", \"Authorization\", \"x-api-key\"]\n\t\t\t\t).join(\",\"),\n\t\t\t);\n\t\t\tif (options.credentials)\n\t\t\t\tres.headers.set(\"Access-Control-Allow-Credentials\", \"true\");\n\t\t\tres.headers.set(\"X-Powered-By\", \"Buntok\");\n\t\t\treturn res;\n\t\t}\n\n\t\tconst response = await next();\n\n\t\tresponse.headers.set(\"Access-Control-Allow-Origin\", allowedOrigin);\n\t\tresponse.headers.set(\"X-Powered-By\", \"Buntok\");\n\t\tif (options.credentials)\n\t\t\tresponse.headers.set(\"Access-Control-Allow-Credentials\", \"true\");\n\n\t\treturn response;\n\t};\n};\n","import type { Context } from \"../context\";\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype AnyApp = {\n\tget: (\n\t\tpath: string,\n\t\thandler: (ctx: Context) => Promise<Response> | Response,\n\t) => void;\n};\n\nexport interface HealthCheckOptions {\n\t/** Health check endpoint path (default: /health) */\n\tpath?: string;\n\t/** Custom health check function */\n\tcheck?: () => Promise<HealthStatus> | HealthStatus;\n\t/** Include uptime in response (default: true) */\n\tincludeUptime?: boolean;\n\t/** Include version in response */\n\tversion?: string;\n}\n\nexport interface HealthStatus {\n\tstatus: \"healthy\" | \"unhealthy\" | \"degraded\";\n\tmessage?: string;\n\tchecks?: Record<\n\t\tstring,\n\t\t{ status: string; message?: string; duration?: number }\n\t>;\n}\n\nconst startTime = Date.now();\n\n/**\n * Register health check endpoint on the app\n */\nexport function healthCheck(app: AnyApp, options: HealthCheckOptions = {}) {\n\tconst path = options.path || \"/health\";\n\n\tapp.get(path, async (ctx: Context) => {\n\t\ttry {\n\t\t\t// Run custom health check if provided\n\t\t\tlet healthStatus: HealthStatus = { status: \"healthy\" };\n\n\t\t\tif (options.check) {\n\t\t\t\thealthStatus = await options.check();\n\t\t\t}\n\n\t\t\tconst response: Record<string, unknown> = {\n\t\t\t\t...healthStatus,\n\t\t\t\ttimestamp: new Date().toISOString(),\n\t\t\t\tuptime:\n\t\t\t\t\toptions.includeUptime !== false\n\t\t\t\t\t\t? Math.floor((Date.now() - startTime) / 1000)\n\t\t\t\t\t\t: undefined,\n\t\t\t\tversion: options.version,\n\t\t\t};\n\n\t\t\t// Clean up undefined values\n\t\t\tObject.keys(response).forEach((key) => {\n\t\t\t\tif (response[key] === undefined) {\n\t\t\t\t\tdelete response[key];\n\t\t\t\t}\n\t\t\t});\n\n\t\t\tconst statusCode = healthStatus.status === \"healthy\" ? 200 : 503;\n\t\t\treturn ctx.json(response, statusCode);\n\t\t} catch (_error) {\n\t\t\treturn ctx.json(\n\t\t\t\t{\n\t\t\t\t\tstatus: \"unhealthy\",\n\t\t\t\t\terror: \"Unknown error\",\n\t\t\t\t\ttimestamp: new Date().toISOString(),\n\t\t\t\t},\n\t\t\t\t503,\n\t\t\t);\n\t\t}\n\t});\n}\n\n/**\n * Create a database health check function\n */\nexport function createDatabaseCheck(\n\tcheckFn: () => Promise<boolean>,\n): () => Promise<HealthStatus> {\n\treturn async () => {\n\t\tconst start = performance.now();\n\t\ttry {\n\t\t\tconst isHealthy = await checkFn();\n\t\t\tconst duration = performance.now() - start;\n\n\t\t\treturn {\n\t\t\t\tstatus: isHealthy ? \"healthy\" : \"unhealthy\",\n\t\t\t\tchecks: {\n\t\t\t\t\tdatabase: {\n\t\t\t\t\t\tstatus: isHealthy ? \"up\" : \"down\",\n\t\t\t\t\t\tduration: Math.round(duration),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t};\n\t\t} catch (error) {\n\t\t\treturn {\n\t\t\t\tstatus: \"unhealthy\",\n\t\t\t\tchecks: {\n\t\t\t\t\tdatabase: {\n\t\t\t\t\t\tstatus: \"down\",\n\t\t\t\t\t\tmessage:\n\t\t\t\t\t\t\terror instanceof Error ? error.message : \"Connection failed\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t};\n}\n\n/**\n * Create a combined health check function\n */\nexport function createHealthCheck(\n\tchecks: Array<{ name: string; check: () => Promise<boolean> }>,\n): () => Promise<HealthStatus> {\n\treturn async () => {\n\t\tconst results: Record<string, { status: string; duration?: number }> = {};\n\t\tlet overallStatus: \"healthy\" | \"unhealthy\" | \"degraded\" = \"healthy\";\n\n\t\tfor (const { name, check } of checks) {\n\t\t\tconst start = performance.now();\n\t\t\ttry {\n\t\t\t\tconst isHealthy = await check();\n\t\t\t\tconst duration = performance.now() - start;\n\n\t\t\t\tresults[name] = {\n\t\t\t\t\tstatus: isHealthy ? \"up\" : \"down\",\n\t\t\t\t\tduration: Math.round(duration),\n\t\t\t\t};\n\n\t\t\t\tif (!isHealthy) {\n\t\t\t\t\toverallStatus = \"unhealthy\";\n\t\t\t\t}\n\t\t\t} catch (_error) {\n\t\t\t\tresults[name] = {\n\t\t\t\t\tstatus: \"down\",\n\t\t\t\t};\n\t\t\t\toverallStatus = \"unhealthy\";\n\t\t\t}\n\t\t}\n\n\t\treturn {\n\t\t\tstatus: overallStatus,\n\t\t\tchecks: results,\n\t\t};\n\t};\n}\n","import type { Middleware } from \"../app\";\n\nexport interface HelmetOptions {\n\t/** X-Content-Type-Options (default: \"nosniff\") */\n\tcontentTypeOptions?: string;\n\t/** X-Frame-Options (default: \"DENY\") */\n\tframeOptions?: string;\n\t/** X-XSS-Protection (default: \"0\") */\n\txssProtection?: string;\n\t/** Referrer-Policy (default: \"no-referrer\") */\n\treferrerPolicy?: string;\n\t/** Strict-Transport-Security (default: \"max-age=31536000; includeSubDomains\") */\n\thsts?: string;\n\t/** X-DNS-Prefetch-Control (default: \"on\") */\n\tdnsPrefetch?: string;\n\t/** Permissions-Policy (default: \"camera=(), microphone=(), geolocation=()\") */\n\tpermissionsPolicy?: string;\n\t/** Custom headers to add */\n\tadditionalHeaders?: Record<string, string>;\n}\n\nconst DEFAULT_HEADERS: Record<string, string> = {\n\t\"X-Content-Type-Options\": \"nosniff\",\n\t\"X-Frame-Options\": \"DENY\",\n\t\"X-XSS-Protection\": \"0\",\n\t\"Referrer-Policy\": \"no-referrer\",\n\t\"Strict-Transport-Security\": \"max-age=31536000; includeSubDomains\",\n\t\"X-DNS-Prefetch-Control\": \"on\",\n\t\"Permissions-Policy\": \"camera=(), microphone=(), geolocation=()\",\n};\n\n/**\n * Security headers middleware - adds common security headers to all responses.\n *\n * @example\n * import { helmet } from \"@buntok/core\";\n *\n * // Default - all standard headers\n * app.use(helmet());\n *\n * // Custom options\n * app.use(helmet({\n *   frameOptions: \"SAMEORIGIN\",\n *   hsts: \"max-age=63072000\",\n * }));\n */\nexport function helmet(options?: HelmetOptions): Middleware {\n\tconst headers: Record<string, string> = { ...DEFAULT_HEADERS };\n\n\tif (options?.contentTypeOptions)\n\t\theaders[\"X-Content-Type-Options\"] = options.contentTypeOptions;\n\tif (options?.frameOptions) headers[\"X-Frame-Options\"] = options.frameOptions;\n\tif (options?.xssProtection)\n\t\theaders[\"X-XSS-Protection\"] = options.xssProtection;\n\tif (options?.referrerPolicy)\n\t\theaders[\"Referrer-Policy\"] = options.referrerPolicy;\n\tif (options?.hsts) headers[\"Strict-Transport-Security\"] = options.hsts;\n\tif (options?.dnsPrefetch)\n\t\theaders[\"X-DNS-Prefetch-Control\"] = options.dnsPrefetch;\n\tif (options?.permissionsPolicy)\n\t\theaders[\"Permissions-Policy\"] = options.permissionsPolicy;\n\tif (options?.additionalHeaders)\n\t\tObject.assign(headers, options.additionalHeaders);\n\n\treturn async (_ctx, next) => {\n\t\tconst response = await next();\n\t\t// Set headers directly on the response - avoids copying Headers + creating new Response\n\t\tfor (const [key, value] of Object.entries(headers)) {\n\t\t\tif (!response.headers.has(key)) {\n\t\t\t\tresponse.headers.set(key, value);\n\t\t\t}\n\t\t}\n\t\treturn response;\n\t};\n}\n","import type { Middleware } from \"../app\";\nimport type { Context } from \"../context\";\n\nexport interface RateLimiterOptions {\n\t/** Maximum requests per window */\n\tmax?: number;\n\t/** Window duration in milliseconds (default: 60000 = 1 minute) */\n\twindowMs?: number;\n\t/** Custom key function (default: IP address) */\n\tkeyGenerator?: (ctx: Context) => string;\n\t/** Custom message when rate limit exceeded */\n\tmessage?: string;\n\t/** HTTP status code when rate limit exceeded (default: 429) */\n\tstatusCode?: number;\n\t/** Skip rate limiting for certain requests */\n\tskip?: (ctx: Context) => boolean;\n\t/** Headers to include in response */\n\theaders?: boolean;\n\t/**\n\t * Where counters are stored. Defaults to an in-memory Map, which is lost\n\t * on restart and not shared across processes/instances. Pass\n\t * `sqliteStore(path)` to persist counters in a bun:sqlite database instead\n\t * (e.g. shared across a clustered deployment via a shared file, or simply\n\t * to survive restarts without adding an external dependency like Redis).\n\t */\n\tstore?: RateLimitStore<unknown>;\n}\n\ninterface RateLimitEntry {\n\tcount: number;\n\tresetTime: number;\n}\n\n/**\n * Minimal Map-like interface the rate limiters need. Implemented by both the\n * default in-memory store and the optional bun:sqlite-backed store below.\n */\nexport interface RateLimitStore<T> {\n\tget(key: string): T | undefined;\n\tset(key: string, value: T): void;\n\tdelete(key: string): void;\n\tentries(): IterableIterator<[string, T]>;\n}\n\nfunction memoryStore<T>(): RateLimitStore<T> {\n\treturn new Map<string, T>();\n}\n\n/**\n * Persist rate-limit counters in a bun:sqlite database file instead of an\n * in-memory Map. Useful when you want counters to survive process restarts,\n * or to be shared across multiple instances pointed at the same file, without\n * pulling in an external dependency like Redis.\n *\n * Requires the Bun runtime (uses `bun:sqlite`).\n */\nexport function sqliteStore<T>(\n\tpath = \"rate-limiter.sqlite\",\n): RateLimitStore<T> {\n\tconst { Database } = require(\"bun:sqlite\") as typeof import(\"bun:sqlite\");\n\tconst db = new Database(path);\n\tdb.run(\n\t\t\"CREATE TABLE IF NOT EXISTS rate_limit_entries (key TEXT PRIMARY KEY, value TEXT NOT NULL)\",\n\t);\n\n\tconst getStmt = db.query(\n\t\t\"SELECT value FROM rate_limit_entries WHERE key = ?\",\n\t);\n\tconst setStmt = db.query(\n\t\t\"INSERT INTO rate_limit_entries (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value\",\n\t);\n\tconst deleteStmt = db.query(\"DELETE FROM rate_limit_entries WHERE key = ?\");\n\tconst allStmt = db.query(\"SELECT key, value FROM rate_limit_entries\");\n\n\treturn {\n\t\tget(key: string): T | undefined {\n\t\t\tconst row = getStmt.get(key) as { value: string } | null;\n\t\t\treturn row ? (JSON.parse(row.value) as T) : undefined;\n\t\t},\n\t\tset(key: string, value: T): void {\n\t\t\tsetStmt.run(key, JSON.stringify(value));\n\t\t},\n\t\tdelete(key: string): void {\n\t\t\tdeleteStmt.run(key);\n\t\t},\n\t\t*entries(): IterableIterator<[string, T]> {\n\t\t\tconst rows = allStmt.all() as Array<{ key: string; value: string }>;\n\t\t\tfor (const row of rows) {\n\t\t\t\tyield [row.key, JSON.parse(row.value) as T];\n\t\t\t}\n\t\t},\n\t};\n}\n\nconst DEFAULT_OPTIONS: Required<\n\tOmit<RateLimiterOptions, \"keyGenerator\" | \"skip\" | \"store\">\n> = {\n\tmax: 100,\n\twindowMs: 60000,\n\tmessage: \"Too many requests, please try again later\",\n\tstatusCode: 429,\n\theaders: true,\n};\n\n/**\n * Simple in-memory rate limiter\n */\nexport function rateLimiter(options: RateLimiterOptions = {}): Middleware {\n\tconst opts = { ...DEFAULT_OPTIONS, ...options };\n\tconst store =\n\t\t(options.store as RateLimitStore<RateLimitEntry>) ??\n\t\tmemoryStore<RateLimitEntry>();\n\n\t// Cleanup old entries periodically\n\tconst cleanup = setInterval(() => {\n\t\tconst now = Date.now();\n\t\tfor (const [key, entry] of store.entries()) {\n\t\t\tif (now > entry.resetTime) {\n\t\t\t\tstore.delete(key);\n\t\t\t}\n\t\t}\n\t}, opts.windowMs);\n\n\t// Prevent cleanup from keeping process alive\n\tif (typeof cleanup === \"object\" && cleanup.unref) {\n\t\tcleanup.unref();\n\t}\n\n\tconst getKey =\n\t\toptions.keyGenerator ||\n\t\t((ctx: Context) => {\n\t\t\treturn (\n\t\t\t\tctx.request.headers.get(\"x-forwarded-for\")?.split(\",\")[0]?.trim() ||\n\t\t\t\tctx.request.headers.get(\"x-real-ip\") ||\n\t\t\t\t\"unknown\"\n\t\t\t);\n\t\t});\n\n\treturn async (ctx, next) => {\n\t\t// Skip if custom skip function returns true\n\t\tif (options.skip?.(ctx)) {\n\t\t\treturn next();\n\t\t}\n\n\t\tconst key = getKey(ctx);\n\t\tconst now = Date.now();\n\t\tconst entry = store.get(key);\n\n\t\tlet count = 1;\n\t\tlet resetTime = now + opts.windowMs;\n\n\t\tif (entry && now < entry.resetTime) {\n\t\t\tcount = entry.count + 1;\n\t\t\tresetTime = entry.resetTime;\n\t\t}\n\n\t\tstore.set(key, { count, resetTime });\n\n\t\t// Check if rate limit exceeded\n\t\tif (count > opts.max) {\n\t\t\tconst response = new Response(JSON.stringify({ error: opts.message }), {\n\t\t\t\tstatus: opts.statusCode,\n\t\t\t\theaders: {\n\t\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\t\t\"Retry-After\": String(Math.ceil((resetTime - now) / 1000)),\n\t\t\t\t\t\"X-RateLimit-Limit\": String(opts.max),\n\t\t\t\t\t\"X-RateLimit-Remaining\": \"0\",\n\t\t\t\t\t\"X-RateLimit-Reset\": String(Math.ceil(resetTime / 1000)),\n\t\t\t\t},\n\t\t\t});\n\t\t\treturn response;\n\t\t}\n\n\t\tconst result = await next();\n\n\t\t// Add rate limit headers to successful response\n\t\tif (opts.headers && result instanceof Response) {\n\t\t\tresult.headers.set(\"X-RateLimit-Limit\", String(opts.max));\n\t\t\tresult.headers.set(\n\t\t\t\t\"X-RateLimit-Remaining\",\n\t\t\t\tString(Math.max(0, opts.max - count)),\n\t\t\t);\n\t\t\tresult.headers.set(\n\t\t\t\t\"X-RateLimit-Reset\",\n\t\t\t\tString(Math.ceil(resetTime / 1000)),\n\t\t\t);\n\t\t}\n\n\t\treturn result;\n\t};\n}\n\n/**\n * Sliding window rate limiter (more accurate but uses more memory)\n */\nexport function slidingWindowRateLimiter(\n\toptions: RateLimiterOptions = {},\n): Middleware {\n\tconst opts = { ...DEFAULT_OPTIONS, ...options };\n\tconst store =\n\t\t(options.store as RateLimitStore<number[]>) ?? memoryStore<number[]>();\n\n\tconst cleanup = setInterval(() => {\n\t\tconst windowStart = Date.now() - opts.windowMs;\n\t\tfor (const [key, timestamps] of store.entries()) {\n\t\t\tconst valid = timestamps.filter((t) => t > windowStart);\n\t\t\tif (valid.length === 0) {\n\t\t\t\tstore.delete(key);\n\t\t\t} else if (valid.length !== timestamps.length) {\n\t\t\t\tstore.set(key, valid);\n\t\t\t}\n\t\t}\n\t}, opts.windowMs);\n\n\tif (typeof cleanup === \"object\" && cleanup.unref) {\n\t\tcleanup.unref();\n\t}\n\n\tconst getKey =\n\t\toptions.keyGenerator ||\n\t\t((ctx: Context) => {\n\t\t\treturn (\n\t\t\t\tctx.request.headers.get(\"x-forwarded-for\")?.split(\",\")[0]?.trim() ||\n\t\t\t\tctx.request.headers.get(\"x-real-ip\") ||\n\t\t\t\t\"unknown\"\n\t\t\t);\n\t\t});\n\n\treturn async (ctx, next) => {\n\t\tif (options.skip?.(ctx)) {\n\t\t\treturn next();\n\t\t}\n\n\t\tconst key = getKey(ctx);\n\t\tconst now = Date.now();\n\t\tconst windowStart = now - opts.windowMs;\n\n\t\t// Get existing timestamps\n\t\tconst timestamps = store.get(key) || [];\n\n\t\t// Filter to only timestamps within window\n\t\tconst validTimestamps = timestamps.filter((t) => t > windowStart);\n\n\t\tif (validTimestamps.length >= opts.max) {\n\t\t\tconst oldestTimestamp = validTimestamps[0] || 0;\n\t\t\tconst retryAfter = Math.ceil(\n\t\t\t\t(oldestTimestamp + opts.windowMs - now) / 1000,\n\t\t\t);\n\n\t\t\treturn new Response(JSON.stringify({ error: opts.message }), {\n\t\t\t\tstatus: opts.statusCode,\n\t\t\t\theaders: {\n\t\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\t\t\"Retry-After\": String(retryAfter),\n\t\t\t\t\t\"X-RateLimit-Limit\": String(opts.max),\n\t\t\t\t\t\"X-RateLimit-Remaining\": \"0\",\n\t\t\t\t},\n\t\t\t});\n\t\t}\n\n\t\t// Add current timestamp\n\t\tvalidTimestamps.push(now);\n\t\tstore.set(key, validTimestamps);\n\n\t\tconst result = await next();\n\n\t\tif (result instanceof Response) {\n\t\t\tresult.headers.set(\"X-RateLimit-Limit\", String(opts.max));\n\t\t\tresult.headers.set(\n\t\t\t\t\"X-RateLimit-Remaining\",\n\t\t\t\tString(opts.max - validTimestamps.length),\n\t\t\t);\n\t\t}\n\n\t\treturn result;\n\t};\n}\n","import type { Middleware } from \"../app\";\n\nexport interface RequestIdOptions {\n\t/** Header name (default: x-request-id) */\n\theader?: string;\n\t/** Custom generator function */\n\tgenerator?: () => string;\n\t/** Store request ID in ctx.store (default: true) */\n\tstore?: boolean;\n\t/** Property name in ctx.store (default: requestId) */\n\tstoreKey?: string;\n}\n\n/**\n * Generate a unique request ID\n */\nfunction generateRequestId(): string {\n\treturn crypto.randomUUID();\n}\n\n/**\n * Middleware to add unique request ID to every request\n */\nexport function requestId(options: RequestIdOptions = {}): Middleware {\n\tconst header = options.header || \"x-request-id\";\n\tconst storeKey = options.storeKey || \"requestId\";\n\tconst generator = options.generator || generateRequestId;\n\tconst useStore = options.store !== false;\n\n\treturn async (ctx, next) => {\n\t\t// Check for existing request ID (from upstream proxy)\n\t\tconst existingId = ctx.request.headers.get(header);\n\t\tconst id = existingId || generator();\n\n\t\t// Store in ctx.store if enabled\n\t\tif (useStore) {\n\t\t\tctx.store[storeKey] = id;\n\t\t}\n\n\t\tconst result = await next();\n\n\t\t// Add request ID to response headers\n\t\tif (result instanceof Response) {\n\t\t\tresult.headers.set(header, id);\n\t\t}\n\n\t\treturn result;\n\t};\n}\n\n/**\n * Simple UUID v4 generator (no dependencies)\n */\nexport function uuid(): string {\n\treturn \"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx\".replace(/[xy]/g, (c) => {\n\t\tconst r = (Math.random() * 16) | 0;\n\t\tconst v = c === \"x\" ? r : (r & 0x3) | 0x8;\n\t\treturn v.toString(16);\n\t});\n}\n\n/**\n * Short ID generator (8 characters)\n */\nexport function shortId(): string {\n\treturn crypto.randomUUID().replace(/-/g, \"\").slice(0, 8);\n}\n","import type { Middleware } from \"../app\";\n\nexport interface ResponseTimeOptions {\n\t/** Header name (default: x-response-time) */\n\theader?: string;\n\t/** Format: 'ms' | 's' (default: 'ms') */\n\tformat?: \"ms\" | \"s\";\n\t/** Store response time in ctx.store (default: false) */\n\tstore?: boolean;\n\t/** Property name in ctx.store (default: responseTime) */\n\tstoreKey?: string;\n}\n\n/**\n * Middleware to measure and add response time to response headers\n */\nexport function responseTime(options: ResponseTimeOptions = {}): Middleware {\n\tconst header = options.header || \"x-response-time\";\n\tconst format = options.format || \"ms\";\n\tconst storeKey = options.storeKey || \"responseTime\";\n\n\treturn async (ctx, next) => {\n\t\tconst start = performance.now();\n\n\t\tconst result = await next();\n\n\t\tconst duration = performance.now() - start;\n\t\tconst formatted =\n\t\t\tformat === \"s\"\n\t\t\t\t? `${(duration / 1000).toFixed(3)}s`\n\t\t\t\t: `${duration.toFixed(2)}ms`;\n\n\t\t// Store in ctx.store if enabled\n\t\tif (options.store) {\n\t\t\tctx.store[storeKey] = formatted;\n\t\t}\n\n\t\t// Add response time header\n\t\tif (result instanceof Response) {\n\t\t\tresult.headers.set(header, formatted);\n\t\t}\n\n\t\treturn result;\n\t};\n}\n","import type { Middleware } from \"../app\";\n\n/**\n * Request timeout middleware - automatically aborts the handler if it takes\n * longer than `ms` milliseconds, returning 408 Request Timeout.\n *\n * @example\n * import { timeout } from \"@buntok/core\";\n *\n * app.get(\"/slow\", timeout(5000), async (ctx) => {\n *   await longOperation();\n *   return ctx.json({ ok: true });\n * });\n *\n * // Custom error message\n * app.post(\"/upload\", timeout(30000, \"Upload timed out\"), handler);\n */\nexport function timeout(ms: number, message = \"Request timed out\"): Middleware {\n\treturn async (_ctx, next) => {\n\t\t// Industrial: skip timeout for SSE - SSE adalah long-lived, timeout akan bunuh koneksi 30s heartbeat\n\t\tif (_ctx.request.headers.get(\"accept\")?.includes(\"text/event-stream\")) return next();\n\t\tlet timeoutId: ReturnType<typeof setTimeout>;\n\n\t\tconst timeoutPromise = new Promise<never>((_, reject) => {\n\t\t\ttimeoutId = setTimeout(() => {\n\t\t\t\treject(new TimeoutError(message, ms));\n\t\t\t}, ms);\n\t\t});\n\n\t\ttry {\n\t\t\tconst response = await Promise.race([next(), timeoutPromise]);\n\t\t\treturn response;\n\t\t} finally {\n\t\t\tclearTimeout(timeoutId!);\n\t\t}\n\t};\n}\n\nexport class TimeoutError extends Error {\n\tpublic readonly timeoutMs: number;\n\n\tconstructor(message: string, timeoutMs: number) {\n\t\tsuper(message);\n\t\tthis.name = \"TimeoutError\";\n\t\tthis.timeoutMs = timeoutMs;\n\t}\n}\n","/**\n * Role-Based Access Control (RBAC) middleware.\n *\n * @example\n * ```ts\n * import { requireRole, requirePermission } from \"buntok\";\n *\n * // Require specific role\n * app.get(\"/admin\", requireAuth(secret), requireRole(\"admin\"), adminHandler);\n *\n * // Require any of multiple roles\n * app.get(\"/mod\", requireAuth(secret), requireRole(\"admin\", \"moderator\"), modHandler);\n *\n * // Require specific permission\n * app.delete(\"/users/:id\", requireAuth(secret), requirePermission(\"users:delete\"), deleteUser);\n *\n * // Custom role resolver\n * app.use(requireRole({\n *   resolver: (user) => user.roles,  // extract roles from JWT payload\n *   roles: [\"admin\"],\n * }));\n * ```\n */\n\ntype UserWithRoles = {\n\trole?: string;\n\troles?: string[];\n\t[key: string]: any;\n};\n\nexport interface RequireRoleOptions {\n\t/** Extract roles from user object. Default: (user) => user.role || user.roles */\n\tresolver?: (user: any) => string | string[] | undefined;\n\n\t/** Required roles (user must have at least one) */\n\troles: string[];\n\n\t/** Custom error message */\n\tmessage?: string;\n}\n\n/**\n * Middleware that requires the user to have at least one of the specified roles.\n * Must be used AFTER requireAuth middleware.\n */\nexport function requireRole(\n\t...roles: string[]\n): (ctx: any, next: () => Promise<Response> | Response) => Promise<Response> | Response;\nexport function requireRole(\n\toptions: RequireRoleOptions,\n): (ctx: any, next: () => Promise<Response> | Response) => Promise<Response> | Response;\nexport function requireRole(\n\t...args: any[]\n): (ctx: any, next: () => Promise<Response> | Response) => Promise<Response> | Response {\n\t// Parse arguments\n\tlet options: RequireRoleOptions;\n\tif (args.length === 1 && typeof args[0] === \"object\" && !Array.isArray(args[0])) {\n\t\toptions = args[0];\n\t} else {\n\t\toptions = { roles: args.flat() };\n\t}\n\n\tconst { roles: requiredRoles, resolver, message } = options;\n\n\treturn async (ctx, next) => {\n\t\tconst user = ctx.user as UserWithRoles | undefined;\n\n\t\tif (!user) {\n\t\t\treturn ctx.json({ success: false, message: \"Authentication required\" }, 401);\n\t\t}\n\n\t\t// Resolve user roles\n\t\tlet userRoles: string[];\n\t\tif (resolver) {\n\t\t\tconst resolved = resolver(user);\n\t\t\tuserRoles = Array.isArray(resolved) ? resolved : resolved ? [resolved] : [];\n\t\t} else {\n\t\t\tuserRoles = [];\n\t\t\tif (user.role) userRoles.push(user.role);\n\t\t\tif (user.roles) userRoles.push(...user.roles);\n\t\t}\n\n\t\t// Check if user has any required role\n\t\tconst hasRole = requiredRoles.some((r) => userRoles.includes(r));\n\n\t\tif (!hasRole) {\n\t\t\treturn ctx.json({\n\t\t\t\tsuccess: false,\n\t\t\t\terror: \"Forbidden\",\n\t\t\t\tmessage: message ?? `Requires one of: ${requiredRoles.join(\", \")}`,\n\t\t\t}, 403);\n\t\t}\n\n\t\treturn next();\n\t};\n}\n\nexport interface RequirePermissionOptions {\n\t/** Extract permissions from user object. Default: (user) => user.permissions */\n\tresolver?: (user: any) => string[] | undefined;\n\n\t/** Required permissions (user must have ALL) */\n\tpermissions: string[];\n\n\t/** Custom error message */\n\tmessage?: string;\n}\n\n/**\n * Middleware that requires the user to have ALL specified permissions.\n * Must be used AFTER requireAuth middleware.\n */\nexport function requirePermission(\n\t...permissions: string[]\n): (ctx: any, next: () => Promise<Response> | Response) => Promise<Response> | Response;\nexport function requirePermission(\n\toptions: RequirePermissionOptions,\n): (ctx: any, next: () => Promise<Response> | Response) => Promise<Response> | Response;\nexport function requirePermission(\n\t...args: any[]\n): (ctx: any, next: () => Promise<Response> | Response) => Promise<Response> | Response {\n\tlet options: RequirePermissionOptions;\n\tif (args.length === 1 && typeof args[0] === \"object\" && !Array.isArray(args[0])) {\n\t\toptions = args[0];\n\t} else {\n\t\toptions = { permissions: args.flat() };\n\t}\n\n\tconst { permissions: requiredPermissions, resolver, message } = options;\n\n\treturn async (ctx, next) => {\n\t\tconst user = ctx.user as UserWithRoles | undefined;\n\n\t\tif (!user) {\n\t\t\treturn ctx.json({ success: false, message: \"Authentication required\" }, 401);\n\t\t}\n\n\t\t// Resolve user permissions\n\t\tlet userPermissions: string[];\n\t\tif (resolver) {\n\t\t\tuserPermissions = resolver(user) ?? [];\n\t\t} else {\n\t\t\tuserPermissions = (user as any).permissions ?? [];\n\t\t}\n\n\t\t// Check if user has all required permissions\n\t\tconst hasAll = requiredPermissions.every((p) => userPermissions.includes(p));\n\n\t\tif (!hasAll) {\n\t\t\tconst missing = requiredPermissions.filter((p) => !userPermissions.includes(p));\n\t\t\treturn ctx.json({\n\t\t\t\tsuccess: false,\n\t\t\t\terror: \"Forbidden\",\n\t\t\t\tmessage: message ?? `Missing permissions: ${missing.join(\", \")}`,\n\t\t\t}, 403);\n\t\t}\n\n\t\treturn next();\n\t};\n}\n","import { z } from \"zod\";\nimport type { Middleware } from \"../app\";\n\nexport interface ValidatorSchema {\n\tparse: (data: unknown) => unknown;\n}\n\nexport interface ValidateOptions {\n\tbody?: ValidatorSchema;\n\tparams?: ValidatorSchema;\n}\n\n/** @deprecated Prefer `zValidator()`, which gives you `ctx.valid()` with full type inference. */\nexport const validate = (schemas: ValidateOptions): Middleware => {\n\treturn async (ctx, next) => {\n\t\tconst errors: string[] = [];\n\n\t\tif (schemas.body) {\n\t\t\ttry {\n\t\t\t\tconst bodyData = await ctx.body();\n\t\t\t\tctx.store.validatedBody = schemas.body.parse(bodyData);\n\t\t\t} catch (err: unknown) {\n\t\t\t\tconst message = err instanceof Error ? err.message : \"Invalid body\";\n\t\t\t\terrors.push(`body: ${message}`);\n\t\t\t}\n\t\t}\n\n\t\tif (schemas.params) {\n\t\t\ttry {\n\t\t\t\tctx.store.validatedParams = schemas.params.parse(ctx.params);\n\t\t\t} catch (err: unknown) {\n\t\t\t\tconst message = err instanceof Error ? err.message : \"Invalid params\";\n\t\t\t\terrors.push(`params: ${message}`);\n\t\t\t}\n\t\t}\n\n\t\tif (errors.length > 0) {\n\t\t\treturn ctx.json(\n\t\t\t\t{\n\t\t\t\t\terror: \"Validation Failed\",\n\t\t\t\t\tdetails: errors,\n\t\t\t\t},\n\t\t\t\t400,\n\t\t\t);\n\t\t}\n\n\t\treturn next();\n\t};\n};\n\n/** @deprecated Prefer `zValidator(\"body\", schema)`. */\nexport const validateBody = (schema: ValidatorSchema): Middleware => {\n\treturn validate({ body: schema });\n};\n\n/** @deprecated Prefer `zValidator(\"params\", schema)`. */\nexport const validateParams = (schema: ValidatorSchema): Middleware => {\n\treturn validate({ params: schema });\n};\n\nexport type ValidationTarget = \"body\" | \"query\" | \"params\";\n\n/**\n * Content types supported by `zValidator(\"body\", schema, { contentType })`.\n * Defaults to `application/json` when `contentType` is omitted.\n */\nexport type BodyContentType =\n\t/**\n\t * Standard JSON body - default behavior when `contentType` is omitted.\n\t *\n\t * Parsed via `JSON.parse`. Zod schema receives the parsed JavaScript object.\n\t *\n\t * @example\n\t * zValidator(\"body\", z.object({ name: z.string() }))\n\t * // equivalent to: { contentType: \"application/json\" }\n\t */\n\t| \"application/json\"\n\t/**\n\t * Parse fields from `multipart/form-data`.\n\t * Supports both text fields (as strings) and file fields (as File objects).\n\t * Validate file fields using `z.file()` (requires Zod v4+).\n\t *\n\t * Zod schema receives `Record<string, string | File>`. of all non-file fields.\n\t *\n\t * @example\n\t * zValidator(\"body\", z.object({ name: z.string() }), { contentType: \"multipart/form-data\" })\n\t */\n\t| \"multipart/form-data\"\n\t/**\n\t * Parse fields from an `application/x-www-form-urlencoded` body.\n\t * All values are strings - use `z.coerce.number()`, `z.coerce.boolean()`, etc. for coercion.\n\t *\n\t * Zod schema receives `Record<string, string>`.\n\t *\n\t * @example\n\t * zValidator(\"body\", z.object({ email: z.string().email(), age: z.coerce.number() }), {\n\t *   contentType: \"application/x-www-form-urlencoded\",\n\t * })\n\t */\n\t| \"application/x-www-form-urlencoded\"\n\t/**\n\t * Parse a plain text body.\n\t * Zod schema receives the raw body as a `string`.\n\t *\n\t * @example\n\t * zValidator(\"body\", z.string().min(1), { contentType: \"text/plain\" })\n\t */\n\t| \"text/plain\"\n\t/**\n\t * Parse an XML body (W3C standard MIME type).\n\t * Zod schema receives the raw XML as a `string` - parse it further with your preferred XML library.\n\t *\n\t * @example\n\t * zValidator(\"body\", z.string(), { contentType: \"application/xml\" })\n\t */\n\t| \"application/xml\"\n\t/**\n\t * Parse an XML body (legacy/vendor MIME type, functionally identical to `application/xml`).\n\t * Zod schema receives the raw XML as a `string`.\n\t *\n\t * @example\n\t * zValidator(\"body\", z.string(), { contentType: \"text/xml\" })\n\t */\n\t| \"text/xml\"\n\t/**\n\t * Parse a raw binary body.\n\t * Zod schema receives an `ArrayBuffer` - use `z.instanceof(ArrayBuffer)` or process it manually.\n\t *\n\t * @example\n\t * zValidator(\"body\", z.instanceof(ArrayBuffer), { contentType: \"application/octet-stream\" })\n\t */\n\t| \"application/octet-stream\";\n\nexport interface ZValidatorOptions {\n\t/**\n\t * Override the expected request body content type.\n\t * Only applies when `target` is `\"body\"`. Defaults to `application/json`.\n\t *\n\t * - `\"multipart/form-data\"` - text and file fields from FormData; validate files with `z.file()`\n\t * - `\"application/x-www-form-urlencoded\"` - URL-encoded key=value body\n\t *\n\t * Omit this option (or the entire `options` argument) for standard JSON bodies.\n\t */\n\tcontentType?: BodyContentType;\n}\n\nexport type SchemaType = z.ZodType | Record<string, z.ZodTypeAny>;\n\nfunction wrapSchema(schema: SchemaType): z.ZodType {\n\t// biome-ignore lint/suspicious/noExplicitAny: Required to check internal Zod fields\n\treturn schema instanceof z.ZodType || (schema as any)?._def\n\t\t? (schema as z.ZodType)\n\t\t: z.object(schema as Record<string, z.ZodTypeAny>);\n}\n\n/**\n * Validate request body/query/params against a Zod schema and expose the\n * parsed, typed result via `ctx.valid(target)` - no manual casting needed.\n *\n * ```ts\n * const schema = z.object({ name: z.string(), age: z.number() });\n *\n * // JSON body (default)\n * app.post(\"/users\", zValidator(\"body\", schema), (ctx) => {\n *   const data = ctx.valid<z.infer<typeof schema>>(\"body\");\n *   return ctx.json(data);\n * });\n *\n * // multipart/form-data text fields (pair with uploader() for files)\n * app.post(\"/profile\", zValidator(\"body\", schema, { contentType: \"multipart/form-data\" }), ...);\n *\n * // application/x-www-form-urlencoded\n * app.post(\"/login\", zValidator(\"body\", schema, { contentType: \"application/x-www-form-urlencoded\" }), ...);\n * ```\n *\n * On failure, responds with 422 and structured error details.\n */\nexport function zValidator(\n\ttarget: ValidationTarget,\n\tschema: SchemaType | (new () => unknown),\n\toptions?: ZValidatorOptions,\n): Middleware {\n\tconst finalSchema = wrapSchema(schema as SchemaType);\n\tconst resolvedContentType: BodyContentType =\n\t\toptions?.contentType ?? \"application/json\";\n\n\tconst middleware: Middleware = async (ctx, next) => {\n\t\tlet raw: unknown;\n\n\t\tif (target === \"body\") {\n\t\t\tif (resolvedContentType === \"multipart/form-data\") {\n\t\t\t\t// Parse both text and file fields from multipart/form-data\n\t\t\t\tconst ct = ctx.request.headers.get(\"content-type\") || \"\";\n\t\t\t\tif (!ct.includes(\"multipart/form-data\")) {\n\t\t\t\t\treturn ctx.error(\"Validation Failed\", 422, [\n\t\t\t\t\t\t{ field: \"content-type\", message: \"expected multipart/form-data content-type\" },\n\t\t\t\t\t]);\n\t\t\t\t}\n\t\t\t\ttry {\n\t\t\t\t\tconst formData = await ctx.formData();\n\t\t\t\t\tconst fields: Record<string, string | File> = {};\n\t\t\t\t\tfor (const [key, value] of formData.entries()) {\n\t\t\t\t\t\tif (value instanceof File) {\n\t\t\t\t\t\t\tfields[key] = value;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tfields[key] = value.toString();\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\traw = fields;\n\t\t\t} catch {\n\t\t\t\treturn ctx.error(\"Validation Failed\", 422, [\n\t\t\t\t\t{ field: \"body\", message: \"failed to parse multipart/form-data\" },\n\t\t\t\t]);\n\t\t\t\t}\n\t\t\t} else if (resolvedContentType === \"application/x-www-form-urlencoded\") {\n\t\t\t\tconst ct = ctx.request.headers.get(\"content-type\") || \"\";\n\t\t\tif (!ct.includes(\"application/x-www-form-urlencoded\")) {\n\t\t\t\treturn ctx.error(\"Validation Failed\", 422, [\n\t\t\t\t\t{ field: \"content-type\", message: \"expected application/x-www-form-urlencoded content-type\" },\n\t\t\t\t]);\n\t\t\t\t}\n\t\t\t\ttry {\n\t\t\t\t\tconst text = await ctx.request.text();\n\t\t\t\t\tconst params = new URLSearchParams(text);\n\t\t\t\t\tconst fields: Record<string, string> = {};\n\t\t\t\t\tfor (const [key, value] of params.entries()) {\n\t\t\t\t\t\tfields[key] = value;\n\t\t\t\t\t}\n\t\t\t\t\traw = fields;\n\t\t\t} catch {\n\t\t\t\treturn ctx.error(\"Validation Failed\", 422, [\n\t\t\t\t\t{ field: \"body\", message: \"failed to parse application/x-www-form-urlencoded\" },\n\t\t\t\t]);\n\t\t\t\t}\n\t\t\t} else if (\n\t\t\t\tresolvedContentType === \"text/plain\" ||\n\t\t\t\tresolvedContentType === \"application/xml\" ||\n\t\t\t\tresolvedContentType === \"text/xml\"\n\t\t\t) {\n\t\t\t\tconst ct = ctx.request.headers.get(\"content-type\") || \"\";\n\t\t\tif (!ct.includes(resolvedContentType)) {\n\t\t\t\treturn ctx.error(\"Validation Failed\", 422, [\n\t\t\t\t\t{ field: \"content-type\", message: `expected ${resolvedContentType} content-type` },\n\t\t\t\t]);\n\t\t\t\t}\n\t\t\t\ttry {\n\t\t\t\t\traw = await ctx.request.text();\n\t\t\t} catch {\n\t\t\t\treturn ctx.error(\"Validation Failed\", 422, [\n\t\t\t\t\t{ field: \"body\", message: `failed to read ${resolvedContentType} body` },\n\t\t\t\t]);\n\t\t\t\t}\n\t\t\t} else if (resolvedContentType === \"application/octet-stream\") {\n\t\t\t\tconst ct = ctx.request.headers.get(\"content-type\") || \"\";\n\t\t\tif (!ct.includes(\"application/octet-stream\")) {\n\t\t\t\treturn ctx.error(\"Validation Failed\", 422, [\n\t\t\t\t\t{ field: \"content-type\", message: \"expected application/octet-stream content-type\" },\n\t\t\t\t]);\n\t\t\t\t}\n\t\t\t\ttry {\n\t\t\t\t\traw = await ctx.request.arrayBuffer();\n\t\t\t} catch {\n\t\t\t\treturn ctx.error(\"Validation Failed\", 422, [\n\t\t\t\t\t{ field: \"body\", message: \"failed to read application/octet-stream body\" },\n\t\t\t\t]);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Default: application/json\n\t\t\t\ttry {\n\t\t\t\t\traw = await ctx.body();\n\t\t\t} catch {\n\t\t\t\treturn ctx.error(\"Validation Failed\", 422, [\n\t\t\t\t\t{ field: \"body\", message: \"invalid JSON\" },\n\t\t\t\t]);\n\t\t\t}\n\t\t\t}\n\t\t} else if (target === \"query\") {\n\t\t\traw = ctx.query;\n\t\t} else {\n\t\t\traw = ctx.params;\n\t\t}\n\n\t\tconst result = finalSchema.safeParse(raw);\n\t\tif (!result.success) {\n\t\t\treturn ctx.error(\n\t\t\t\t\"Validation Failed\",\n\t\t\t\t422,\n\t\t\t\tresult.error.issues.map(\n\t\t\t\t\t(issue) => ({\n\t\t\t\t\t\tfield: issue.path.join(\".\") || target,\n\t\t\t\t\t\tmessage: issue.message,\n\t\t\t\t\t}),\n\t\t\t\t),\n\t\t\t);\n\t\t}\n\n\t\tctx.setValidated(target, result.data);\n\n\t\treturn next();\n\t};\n\n\t// Attach metadata for OpenAPI generation\n\tconst mwMeta = middleware as unknown as Record<string, unknown>;\n\tmwMeta._isBuntokValidator = true;\n\tmwMeta._target = target;\n\tmwMeta._schema = finalSchema;\n\tmwMeta._contentType = target === \"body\" ? resolvedContentType : \"\";\n\n\treturn middleware;\n}\n\n/**\n * Document the response schema for OpenAPI.\n * Note: Currently this is for documentation purposes only and does not perform runtime response validation.\n */\nexport function zResponse(\n\tstatus: number,\n\tschema: SchemaType | (new () => unknown) | [new () => unknown],\n\tdescription: string = \"Success\",\n): Middleware {\n\tconst finalSchema = wrapSchema(schema as SchemaType);\n\n\t// Wrap in standard Buntok response envelope\n\tconst envelopedSchema = z.object({\n\t\tsuccess: z.boolean(),\n\t\tmessage: z.string(),\n\t\tdata: finalSchema,\n\t});\n\n\tconst middleware: Middleware = async (_ctx, next) => {\n\t\treturn next();\n\t};\n\n\t// Attach metadata for OpenAPI generation\n\tconst mwMeta = middleware as unknown as Record<string, unknown>;\n\tmwMeta._isBuntokResponse = true;\n\tmwMeta._status = status;\n\tmwMeta._schema = envelopedSchema;\n\tmwMeta._description = description;\n\n\treturn middleware;\n}\n","// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport type JobHandler<T> = (job: Job<T>) => Promise<void> | void;\n\nexport interface Job<T> {\n\t/** Auto-generated unique identifier */\n\tid: string;\n\t/** Payload passed by the caller */\n\tdata: T;\n\t/** Higher number = processed sooner (default: 0) */\n\tpriority: number;\n\t/** Milliseconds to wait before the job is eligible for processing */\n\tdelay: number;\n\t/** How many times this job has been attempted (0 = first attempt) */\n\tattempt: number;\n\t/** Timestamp when the job was added */\n\tcreatedAt: number;\n}\n\nexport interface QueueOptions {\n\t/**\n\t * Maximum number of retry attempts after the first failure.\n\t * Set to 0 (default) to disable retries.\n\t */\n\tmaxRetries?: number;\n\t/**\n\t * Base delay in milliseconds between retry attempts (default: 1000).\n\t * With `backoff: \"exponential\"` this value is doubled each attempt.\n\t */\n\tretryDelay?: number;\n\t/**\n\t * Retry backoff strategy.\n\t *  - `\"fixed\"` (default) - always waits `retryDelay` ms\n\t *  - `\"exponential\"` - waits `retryDelay * 2^attempt` ms\n\t */\n\tbackoff?: \"fixed\" | \"exponential\";\n}\n\nexport interface QueueDriver<T> {\n\tadd(data: T, opts?: { priority?: number; delay?: number }): Promise<void>;\n\tprocess(handler: JobHandler<T>): void;\n\tsize(): number;\n\tclear(): void;\n}\n\n// ─── Memory Driver ────────────────────────────────────────────────────────────\n\nexport class MemoryQueueDriver<T> implements QueueDriver<T> {\n\tprivate queue: Job<T>[] = [];\n\tprivate isProcessing = false;\n\tprivate handlers: JobHandler<T>[] = [];\n\tprivate readonly opts: Required<QueueOptions>;\n\n\tconstructor(\n\t\tpublic name: string,\n\t\toptions: QueueOptions = {},\n\t) {\n\t\tthis.opts = {\n\t\t\tmaxRetries: options.maxRetries ?? 0,\n\t\t\tretryDelay: options.retryDelay ?? 1000,\n\t\t\tbackoff: options.backoff ?? \"fixed\",\n\t\t};\n\t}\n\n\tprocess(handler: JobHandler<T>) {\n\t\tthis.handlers.push(handler);\n\t\tthis.pump();\n\t}\n\n\tasync add(\n\t\tdata: T,\n\t\topts: { priority?: number; delay?: number } = {},\n\t): Promise<void> {\n\t\tconst job: Job<T> = {\n\t\t\tid: crypto.randomUUID(),\n\t\t\tdata,\n\t\t\tpriority: opts.priority ?? 0,\n\t\t\tdelay: opts.delay ?? 0,\n\t\t\tattempt: 0,\n\t\t\tcreatedAt: Date.now(),\n\t\t};\n\n\t\tif (job.delay > 0) {\n\t\t\t// Schedule for later\n\t\t\tsetTimeout(() => {\n\t\t\t\tthis.enqueue(job);\n\t\t\t\tthis.pump();\n\t\t\t}, job.delay);\n\t\t} else {\n\t\t\tthis.enqueue(job);\n\t\t\tthis.pump();\n\t\t}\n\t}\n\n\tprivate enqueue(job: Job<T>): void {\n\t\t// Insert in priority order (higher priority first) using binary search\n\t\tif (job.priority === 0 || this.queue.length === 0) {\n\t\t\tthis.queue.push(job);\n\t\t\treturn;\n\t\t}\n\t\tlet lo = 0;\n\t\tlet hi = this.queue.length;\n\t\twhile (lo < hi) {\n\t\t\tconst mid = (lo + hi) >>> 1;\n\t\t\t// biome-ignore lint/style/noNonNullAssertion: bounds checked\n\t\t\tif (this.queue[mid]!.priority >= job.priority) lo = mid + 1;\n\t\t\telse hi = mid;\n\t\t}\n\t\tthis.queue.splice(lo, 0, job);\n\t}\n\n\tprivate retryDelay(attempt: number): number {\n\t\tif (this.opts.backoff === \"exponential\") {\n\t\t\treturn this.opts.retryDelay * 2 ** attempt;\n\t\t}\n\t\treturn this.opts.retryDelay;\n\t}\n\n\tprivate async pump(): Promise<void> {\n\t\tif (this.isProcessing || this.handlers.length === 0) return;\n\t\tthis.isProcessing = true;\n\n\t\twhile (this.queue.length > 0) {\n\t\t\tconst job = this.queue.shift();\n\t\t\tif (job === undefined) break;\n\n\t\t\tfor (const handler of this.handlers) {\n\t\t\t\ttry {\n\t\t\t\t\tawait handler(job);\n\t\t\t\t} catch (err) {\n\t\t\t\t\tconst nextAttempt = job.attempt + 1;\n\t\t\t\t\tif (nextAttempt <= this.opts.maxRetries) {\n\t\t\t\t\t\tconst delay = this.retryDelay(job.attempt);\n\t\t\t\t\t\tconsole.warn(\n\t\t\t\t\t\t\t`[Queue:${this.name}] Job ${job.id} failed (attempt ${nextAttempt}/${this.opts.maxRetries + 1}), retrying in ${delay}ms…`,\n\t\t\t\t\t\t\terr,\n\t\t\t\t\t\t);\n\t\t\t\t\t\tsetTimeout(() => {\n\t\t\t\t\t\t\tthis.enqueue({ ...job, attempt: nextAttempt });\n\t\t\t\t\t\t\tthis.pump();\n\t\t\t\t\t\t}, delay);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tconsole.error(\n\t\t\t\t\t\t\t`[Queue:${this.name}] Job ${job.id} permanently failed after ${nextAttempt} attempt(s):`,\n\t\t\t\t\t\t\terr,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Yield to the event loop between jobs to avoid blocking HTTP requests\n\t\t\tawait new Promise((resolve) => setTimeout(resolve, 0));\n\t\t}\n\n\t\tthis.isProcessing = false;\n\t}\n\n\t/** Number of jobs currently waiting in the queue */\n\tsize(): number {\n\t\treturn this.queue.length;\n\t}\n\n\t/** Remove all pending jobs from the queue */\n\tclear(): void {\n\t\tthis.queue = [];\n\t}\n}\n\n// ─── Queue (high-level API) ────────────────────────────────────────────────────\n\nexport class Queue<T = unknown> {\n\tprivate driver: QueueDriver<T>;\n\n\tconstructor(\n\t\tpublic name: string,\n\t\tdriverOrOptions?: QueueDriver<T> | QueueOptions,\n\t\toptions?: QueueOptions,\n\t) {\n\t\t// Overloads:\n\t\t//   new Queue(\"email\")\n\t\t//   new Queue(\"email\", { maxRetries: 3 })\n\t\t//   new Queue(\"email\", redisDriver)\n\t\t//   new Queue(\"email\", redisDriver, { maxRetries: 3 })\n\t\tif (\n\t\t\tdriverOrOptions &&\n\t\t\ttypeof (driverOrOptions as QueueDriver<T>).add === \"function\"\n\t\t) {\n\t\t\tthis.driver = driverOrOptions as QueueDriver<T>;\n\t\t} else {\n\t\t\tthis.driver = new MemoryQueueDriver<T>(\n\t\t\t\tname,\n\t\t\t\t(driverOrOptions as QueueOptions) ?? options ?? {},\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Add a job to the queue.\n\t *\n\t * @param data - Job payload\n\t * @param opts.priority - Higher numbers are processed first (default: 0)\n\t * @param opts.delay - Milliseconds to wait before processing (default: 0)\n\t *\n\t * @example\n\t * await emailQueue.add({ to: \"user@example.com\" }, { delay: 5000, priority: 10 });\n\t */\n\tasync add(\n\t\tdata: T,\n\t\topts: { priority?: number; delay?: number } = {},\n\t): Promise<void> {\n\t\treturn this.driver.add(data, opts);\n\t}\n\n\t/**\n\t * Register a handler that processes jobs as they arrive.\n\t * The handler now receives the full `Job<T>` object (not just data),\n\t * so you can inspect `job.id`, `job.attempt`, `job.createdAt`, etc.\n\t *\n\t * @example\n\t * emailQueue.process(async (job) => {\n\t *   console.log(`Attempt ${job.attempt + 1} for job ${job.id}`);\n\t *   await sendEmail(job.data.to, job.data.subject);\n\t * });\n\t */\n\tprocess(handler: JobHandler<T>): void {\n\t\tthis.driver.process(handler);\n\t}\n\n\t/** Number of jobs currently waiting in the queue */\n\tsize(): number {\n\t\treturn this.driver.size();\n\t}\n\n\t/** Remove all pending (not yet started) jobs */\n\tclear(): void {\n\t\tthis.driver.clear();\n\t}\n}\n","// ─── Types ────────────────────────────────────────────────────────────────────\nexport type {\n\tCheckoutResult,\n\tCheckoutStatus,\n\tCreateCheckoutInput,\n\tCreatePaymentLinkInput,\n\tCreateRefundInput,\n\tCreateSubscriptionInput,\n\tLineItem,\n\tPaymentLinkResult,\n\tPaymentOptions,\n\tRefundResult,\n\tRefundStatus,\n\tSubscriptionResult,\n\tSubscriptionStatus,\n\tWebhookEvent,\n\tWebhookEventType,\n} from \"./types\";\n\nexport {\n\tCreateCheckoutInputSchema,\n\tCreatePaymentLinkInputSchema,\n\tCreateRefundInputSchema,\n\tCreateSubscriptionInputSchema,\n\tMoneyAmountSchema,\n} from \"./types\";\n\n// ─── Errors ───────────────────────────────────────────────────────────────────\nexport {\n\tPaymentConfigurationError,\n\tPaymentError,\n\tPaymentIdempotencyError,\n\tPaymentProviderError,\n\tPaymentVerificationError,\n} from \"./errors\";\n\n// ─── Driver Interface ─────────────────────────────────────────────────────────\nexport type { PaymentDriver } from \"./driver\";\n\n// ─── Helpers ──────────────────────────────────────────────────────────────────\nexport {\n\tgenerateIdempotencyKey,\n\tnormalizeCheckoutStatus,\n\tnormalizeRefundStatus,\n\tnormalizeSubscriptionStatus,\n} from \"./helpers\";\n\n// ─── Middleware ────────────────────────────────────────────────────────────────\nexport { paymentWebhook } from \"./middleware/webhook\";\nexport type { WebhookMiddlewareOptions } from \"./middleware/webhook\";\n\n// ─── Drivers ──────────────────────────────────────────────────────────────────\nexport { StripeDriver, type StripeDriverConfig } from \"./drivers/stripe\";\nexport { MidtransDriver, type MidtransDriverConfig } from \"./drivers/midtrans\";\nexport { XenditDriver, type XenditDriverConfig } from \"./drivers/xendit\";\nexport { PayPalDriver, type PayPalDriverConfig } from \"./drivers/paypal\";\n\n// ─── Convenience Factory ──────────────────────────────────────────────────────\nimport type { PaymentDriver } from \"./driver\";\nimport type { StripeDriverConfig } from \"./drivers/stripe\";\nimport { StripeDriver } from \"./drivers/stripe\";\nimport type { MidtransDriverConfig } from \"./drivers/midtrans\";\nimport { MidtransDriver } from \"./drivers/midtrans\";\nimport type { XenditDriverConfig } from \"./drivers/xendit\";\nimport { XenditDriver } from \"./drivers/xendit\";\nimport type { PayPalDriverConfig } from \"./drivers/paypal\";\nimport { PayPalDriver } from \"./drivers/paypal\";\n\n/**\n * Create payment drivers with a simple factory API.\n *\n * @example\n * ```ts\n * import { createPayment } from \"@buntok/core\";\n *\n * const stripe = createPayment.stripe({\n *   secretKey: process.env.STRIPE_SECRET_KEY!,\n *   webhookSecret: process.env.STRIPE_WEBHOOK_SECRET!,\n * });\n *\n * const midtrans = createPayment.midtrans({\n *   serverKey: process.env.MIDTRANS_SERVER_KEY!,\n *   isProduction: false,\n * });\n * ```\n */\nexport const createPayment = {\n\tstripe: (config: StripeDriverConfig): PaymentDriver => new StripeDriver(config),\n\tmidtrans: (config: MidtransDriverConfig): PaymentDriver => new MidtransDriver(config),\n\txendit: (config: XenditDriverConfig): PaymentDriver => new XenditDriver(config),\n\tpaypal: (config: PayPalDriverConfig): PaymentDriver => new PayPalDriver(config),\n};\n","import { z } from \"zod\";\n\n// ─── Normalized Status Types ──────────────────────────────────────────────────\n\nexport type CheckoutStatus =\n\t| \"pending\"\n\t| \"processing\"\n\t| \"requires_action\"\n\t| \"completed\"\n\t| \"failed\"\n\t| \"expired\"\n\t| \"cancelled\";\n\nexport type SubscriptionStatus =\n\t| \"active\"\n\t| \"past_due\"\n\t| \"cancelled\"\n\t| \"suspended\"\n\t| \"expired\"\n\t| \"incomplete\";\n\nexport type RefundStatus =\n\t| \"pending\"\n\t| \"completed\"\n\t| \"failed\"\n\t| \"partially_refunded\";\n\nexport type WebhookEventType =\n\t| \"payment.completed\"\n\t| \"payment.failed\"\n\t| \"payment.expired\"\n\t| \"payment.created\"\n\t| \"subscription.created\"\n\t| \"subscription.cancelled\"\n\t| \"subscription.payment_succeeded\"\n\t| \"subscription.payment_failed\"\n\t| \"subscription.suspended\"\n\t| \"refund.completed\"\n\t| \"refund.failed\";\n\n// ─── Line Item ────────────────────────────────────────────────────────────────\n\nexport interface LineItem {\n\tname: string;\n\tquantity: number;\n\tamount: number;\n\tdescription?: string;\n\timageUrl?: string;\n\tmetadata?: Record<string, unknown>;\n}\n\n// ─── Input Schemas ────────────────────────────────────────────────────────────\n\nexport const MoneyAmountSchema = z.object({\n\tamount: z.number().positive(),\n\tcurrency: z.string().min(3).max(3).toUpperCase(),\n});\n\nexport const CreateCheckoutInputSchema = z.object({\n\tamount: z.number().positive(),\n\tcurrency: z.string().min(3).max(3).transform((s) => s.toUpperCase()),\n\tdescription: z.string().optional(),\n\tcustomerEmail: z.string().email().optional(),\n\tcustomerName: z.string().optional(),\n\tsuccessUrl: z.string().url().optional(),\n\tcancelUrl: z.string().url().optional(),\n\tmetadata: z.record(z.string(), z.unknown()).optional(),\n\titems: z\n\t\t.array(\n\t\t\tz.object({\n\t\t\t\tname: z.string(),\n\t\t\t\tquantity: z.number().int().positive(),\n\t\t\t\tamount: z.number().positive(),\n\t\t\t\tdescription: z.string().optional(),\n\t\t\t}),\n\t\t)\n\t\t.optional(),\n});\n\nexport const CreateRefundInputSchema = z.object({\n\tpaymentId: z.string().min(1),\n\tamount: z.number().positive().optional(),\n\treason: z.string().optional(),\n\tmetadata: z.record(z.string(), z.unknown()).optional(),\n});\n\nexport const CreateSubscriptionInputSchema = z.object({\n\tplanId: z.string().min(1),\n\tcustomerEmail: z.string().email().optional(),\n\tcustomerName: z.string().optional(),\n\tmetadata: z.record(z.string(), z.unknown()).optional(),\n\ttrialPeriodDays: z.number().int().nonnegative().optional(),\n});\n\nexport const CreatePaymentLinkInputSchema = z.object({\n\tamount: z.number().positive(),\n\tcurrency: z.string().min(3).max(3).transform((s) => s.toUpperCase()),\n\tdescription: z.string().optional(),\n\tmetadata: z.record(z.string(), z.unknown()).optional(),\n\texpiresAt: z.string().datetime().optional(),\n});\n\n// ─── Input Types ──────────────────────────────────────────────────────────────\n\nexport type CreateCheckoutInput = z.infer<typeof CreateCheckoutInputSchema>;\nexport type CreateRefundInput = z.infer<typeof CreateRefundInputSchema>;\nexport type CreateSubscriptionInput = z.infer<typeof CreateSubscriptionInputSchema>;\nexport type CreatePaymentLinkInput = z.infer<typeof CreatePaymentLinkInputSchema>;\n\n// ─── Payment Options ──────────────────────────────────────────────────────────\n\nexport interface PaymentOptions {\n\tidempotencyKey?: string;\n}\n\n// ─── Result Types ─────────────────────────────────────────────────────────────\n\nexport interface CheckoutResult {\n\tid: string;\n\tstatus: CheckoutStatus;\n\tamount: number;\n\tcurrency: string;\n\tprovider: string;\n\t/** Checkout URL for customer redirect (when status is \"pending\" or \"requires_action\") */\n\tcheckoutUrl?: string;\n\t/** Payment intent / session ID from the provider */\n\tproviderPaymentId?: string;\n\tclientSecret?: string;\n\tmetadata?: Record<string, unknown>;\n\texpiresAt?: Date;\n\tcreatedAt: Date;\n}\n\nexport interface RefundResult {\n\tid: string;\n\tstatus: RefundStatus;\n\tamount: number;\n\tcurrency: string;\n\tprovider: string;\n\tpaymentId: string;\n\treason?: string;\n\tmetadata?: Record<string, unknown>;\n\tcreatedAt: Date;\n}\n\nexport interface SubscriptionResult {\n\tid: string;\n\tstatus: SubscriptionStatus;\n\tprovider: string;\n\tplanId: string;\n\tcustomerEmail?: string;\n\tmetadata?: Record<string, unknown>;\n\tcurrentPeriodStart?: Date;\n\tcurrentPeriodEnd?: Date;\n\tcreatedAt: Date;\n}\n\nexport interface PaymentLinkResult {\n\tid: string;\n\turl: string;\n\tamount: number;\n\tcurrency: string;\n\tprovider: string;\n\tmetadata?: Record<string, unknown>;\n\texpiresAt?: Date;\n\tcreatedAt: Date;\n}\n\n// ─── Webhook Event ────────────────────────────────────────────────────────────\n\nexport interface WebhookEvent {\n\tid: string;\n\ttype: WebhookEventType;\n\tprovider: string;\n\trawData: unknown;\n\t/** The payment/subscription ID from the provider */\n\tentityId?: string;\n\tamount?: number;\n\tcurrency?: string;\n\tstatus?: string;\n\tmetadata?: Record<string, unknown>;\n\tcreatedAt: Date;\n}\n","import { HttpError } from \"../helpers/async-handler\";\n\nexport class PaymentError extends HttpError {\n\tprovider: string;\n\tproviderCode?: string;\n\n\tconstructor(\n\t\tmessage: string,\n\t\tstatus: number,\n\t\tprovider: string,\n\t\tproviderCode?: string,\n\t) {\n\t\tsuper(status, message);\n\t\tthis.name = \"PaymentError\";\n\t\tthis.provider = provider;\n\t\tthis.providerCode = providerCode;\n\t}\n}\n\nexport class PaymentVerificationError extends PaymentError {\n\tconstructor(provider: string, message = \"Webhook signature verification failed\") {\n\t\tsuper(message, 400, provider, \"VERIFICATION_FAILED\");\n\t\tthis.name = \"PaymentVerificationError\";\n\t}\n}\n\nexport class PaymentIdempotencyError extends PaymentError {\n\tconstructor(provider: string, message = \"Idempotency key reuse detected\") {\n\t\tsuper(message, 409, provider, \"IDEMPOTENCY_CONFLICT\");\n\t\tthis.name = \"PaymentIdempotencyError\";\n\t}\n}\n\nexport class PaymentProviderError extends PaymentError {\n\tconstructor(provider: string, providerCode: string, message: string) {\n\t\tsuper(message, 502, provider, providerCode);\n\t\tthis.name = \"PaymentProviderError\";\n\t}\n}\n\nexport class PaymentConfigurationError extends PaymentError {\n\tconstructor(provider: string, message = \"Invalid payment driver configuration\") {\n\t\tsuper(message, 500, provider, \"CONFIGURATION_ERROR\");\n\t\tthis.name = \"PaymentConfigurationError\";\n\t}\n}\n","import type {\n\tCheckoutStatus,\n\tRefundStatus,\n\tSubscriptionStatus,\n} from \"./types\";\n\n// ─── ID Generation ────────────────────────────────────────────────────────────\n\n/**\n * Generate a unique idempotency key for payment operations.\n * Format: `pay_{uuid}` - cryptographically random, safe for concurrent use.\n */\nexport function generateIdempotencyKey(): string {\n\treturn `pay_${crypto.randomUUID()}`;\n}\n\n// ─── Checkout Status Normalization ────────────────────────────────────────────\n\nconst STRIPE_CHECKOUT_STATUS: Record<string, CheckoutStatus> = {\n\topen: \"pending\",\n\tcomplete: \"completed\",\n\texpired: \"expired\",\n};\n\nconst STRIPE_INTENT_STATUS: Record<string, CheckoutStatus> = {\n\trequires_payment_method: \"pending\",\n\trequires_confirmation: \"pending\",\n\trequires_action: \"requires_action\",\n\tprocessing: \"processing\",\n\trequires_capture: \"processing\",\n\tsucceeded: \"completed\",\n\tcanceled: \"cancelled\",\n};\n\nconst MIDTRANS_STATUS: Record<string, CheckoutStatus> = {\n\tpending: \"pending\",\n\tsettlement: \"completed\",\n\tcapture: \"completed\",\n\tdeny: \"failed\",\n\tfailure: \"failed\",\n\tcancel: \"cancelled\",\n\texpire: \"expired\",\n};\n\nconst XENDIT_STATUS: Record<string, CheckoutStatus> = {\n\tPENDING: \"pending\",\n\tREQUIRES_ACTION: \"requires_action\",\n\tSUCCEEDED: \"completed\",\n\tFAILED: \"failed\",\n\tCANCELED: \"cancelled\",\n\tEXPIRED: \"expired\",\n};\n\nconst PAYPAL_STATUS: Record<string, CheckoutStatus> = {\n\tCREATED: \"pending\",\n\tSAVED: \"pending\",\n\tAPPROVED: \"completed\",\n\tVOIDED: \"cancelled\",\n\tCOMPLETED: \"completed\",\n\tPAYER_ACTION_REQUIRED: \"requires_action\",\n};\n\n/**\n * Normalize a provider-specific checkout status into a unified CheckoutStatus.\n *\n * @param provider - The payment provider identifier\n * @param nativeStatus - The provider's native status string\n * @returns The normalized CheckoutStatus\n */\nexport function normalizeCheckoutStatus(\n\tprovider: string,\n\tnativeStatus: string,\n): CheckoutStatus {\n\tswitch (provider) {\n\t\tcase \"stripe\": {\n\t\t\t// Stripe checkout session status OR payment intent status\n\t\t\treturn (\n\t\t\t\tSTRIPE_CHECKOUT_STATUS[nativeStatus] ??\n\t\t\t\tSTRIPE_INTENT_STATUS[nativeStatus] ??\n\t\t\t\t\"pending\"\n\t\t\t);\n\t\t}\n\t\tcase \"midtrans\":\n\t\t\treturn MIDTRANS_STATUS[nativeStatus] ?? \"pending\";\n\t\tcase \"xendit\":\n\t\t\treturn XENDIT_STATUS[nativeStatus] ?? \"pending\";\n\t\tcase \"paypal\":\n\t\t\treturn PAYPAL_STATUS[nativeStatus] ?? \"pending\";\n\t\tdefault:\n\t\t\treturn \"pending\";\n\t}\n}\n\n// ─── Refund Status Normalization ──────────────────────────────────────────────\n\nconst STRIPE_REFUND_STATUS: Record<string, RefundStatus> = {\n\tpending: \"pending\",\n\tsucceeded: \"completed\",\n\tfailed: \"failed\",\n\tpartial: \"partially_refunded\",\n};\n\nconst MIDTRANS_REFUND_STATUS: Record<string, RefundStatus> = {\n\trefund: \"completed\",\n\tpartial_refund: \"partially_refunded\",\n};\n\nconst XENDIT_REFUND_STATUS: Record<string, RefundStatus> = {\n\tPENDING: \"pending\",\n\tSUCCEEDED: \"completed\",\n\tFAILED: \"failed\",\n};\n\nconst PAYPAL_REFUND_STATUS: Record<string, RefundStatus> = {\n\tPENDING: \"pending\",\n\tCOMPLETED: \"completed\",\n\tFAILED: \"failed\",\n\tREFUNDED: \"partially_refunded\",\n\tCANCELLED: \"failed\",\n};\n\n/**\n * Normalize a provider-specific refund status into a unified RefundStatus.\n */\nexport function normalizeRefundStatus(\n\tprovider: string,\n\tnativeStatus: string,\n): RefundStatus {\n\tswitch (provider) {\n\t\tcase \"stripe\":\n\t\t\treturn STRIPE_REFUND_STATUS[nativeStatus] ?? \"pending\";\n\t\tcase \"midtrans\":\n\t\t\treturn MIDTRANS_REFUND_STATUS[nativeStatus] ?? \"pending\";\n\t\tcase \"xendit\":\n\t\t\treturn XENDIT_REFUND_STATUS[nativeStatus] ?? \"pending\";\n\t\tcase \"paypal\":\n\t\t\treturn PAYPAL_REFUND_STATUS[nativeStatus] ?? \"pending\";\n\t\tdefault:\n\t\t\treturn \"pending\";\n\t}\n}\n\n// ─── Subscription Status Normalization ────────────────────────────────────────\n\nconst STRIPE_SUBSCRIPTION_STATUS: Record<string, SubscriptionStatus> = {\n\tactive: \"active\",\n\tpast_due: \"past_due\",\n\tcanceled: \"cancelled\",\n\tunpaid: \"expired\",\n\tincomplete: \"incomplete\",\n\tincomplete_expired: \"expired\",\n\ttrialing: \"active\",\n};\n\nconst PAYPAL_SUBSCRIPTION_STATUS: Record<string, SubscriptionStatus> = {\n\tACTIVE: \"active\",\n\tCANCELLED: \"cancelled\",\n\tSUSPENDED: \"suspended\",\n\tEXPIRED: \"expired\",\n};\n\n/**\n * Normalize a provider-specific subscription status into a unified SubscriptionStatus.\n */\nexport function normalizeSubscriptionStatus(\n\tprovider: string,\n\tnativeStatus: string,\n): SubscriptionStatus {\n\tswitch (provider) {\n\t\tcase \"stripe\":\n\t\t\treturn STRIPE_SUBSCRIPTION_STATUS[nativeStatus] ?? \"active\";\n\t\tcase \"paypal\":\n\t\t\treturn PAYPAL_SUBSCRIPTION_STATUS[nativeStatus] ?? \"active\";\n\t\tdefault:\n\t\t\treturn \"active\";\n\t}\n}\n","import type { Middleware } from \"../../app\";\nimport type { Context } from \"../../context\";\nimport { PaymentVerificationError } from \"../errors\";\nimport type { PaymentDriver } from \"../driver\";\n\nexport interface WebhookMiddlewareOptions {\n\t/** The payment driver to use for verification */\n\tdriver: PaymentDriver;\n\t/** Webhook signing secret from the payment provider */\n\tsecret: string;\n\t/**\n\t * Header name that contains the signature.\n\t * Defaults vary by provider: Stripe uses \"stripe-signature\",\n\t * Midtrans uses \"x-signature\", Xendit uses \"x-callback-token\",\n\t * PayPal uses \"paypal-transmission-sig\".\n\t */\n\tsignatureHeader?: string;\n}\n\n/**\n * Middleware that verifies payment webhook signatures and injects\n * the parsed event into `ctx.store.paymentEvent`.\n *\n * @example\n * ```ts\n * import { paymentWebhook } from \"@buntok/core\";\n *\n * const stripe = new StripeDriver({ secretKey: \"sk_...\" });\n *\n * app.post(\"/webhooks/stripe\",\n *   paymentWebhook({\n *     driver: stripe,\n *     secret: process.env.STRIPE_WEBHOOK_SECRET!,\n *     signatureHeader: \"stripe-signature\",\n *   }),\n *   (ctx) => {\n *     const event = ctx.store.paymentEvent;\n *     // Handle the event...\n *     return ctx.json({ received: true });\n *   }\n * );\n * ```\n */\nexport function paymentWebhook(options: WebhookMiddlewareOptions): Middleware {\n\tconst {\n\t\tdriver,\n\t\tsecret,\n\t\tsignatureHeader = getDefaultSignatureHeader(driver.id),\n\t} = options;\n\n\treturn async (ctx: Context, next: () => Promise<Response> | Response) => {\n\t\tconst rawBody = await ctx.request.text();\n\t\tconst signature = ctx.request.headers.get(signatureHeader);\n\n\t\tif (!signature) {\n\t\t\tthrow new PaymentVerificationError(\n\t\t\t\tdriver.id,\n\t\t\t\t`Missing signature header: ${signatureHeader}`,\n\t\t\t);\n\t\t}\n\n\t\tconst isValid = await driver.verifyWebhookSignature(rawBody, signature, secret);\n\t\tif (!isValid) {\n\t\t\tthrow new PaymentVerificationError(driver.id);\n\t\t}\n\n\t\tconst event = driver.parseWebhookEvent(rawBody);\n\t\tctx.store.paymentEvent = event;\n\n\t\treturn next();\n\t};\n}\n\n/**\n * Get the default signature header name for a provider.\n */\nfunction getDefaultSignatureHeader(provider: string): string {\n\tswitch (provider) {\n\t\tcase \"stripe\":\n\t\t\treturn \"stripe-signature\";\n\t\tcase \"midtrans\":\n\t\t\treturn \"x-signature\";\n\t\tcase \"xendit\":\n\t\t\treturn \"x-callback-token\";\n\t\tcase \"paypal\":\n\t\t\treturn \"paypal-transmission-sig\";\n\t\tdefault:\n\t\t\treturn \"x-payment-signature\";\n\t}\n}\n","import type { PaymentDriver } from \"../driver\";\nimport type {\n\tCreateCheckoutInput,\n\tCreatePaymentLinkInput,\n\tCreateRefundInput,\n\tCreateSubscriptionInput,\n\tCheckoutResult,\n\tPaymentLinkResult,\n\tPaymentOptions,\n\tRefundResult,\n\tSubscriptionResult,\n\tWebhookEvent,\n} from \"../types\";\nimport { normalizeCheckoutStatus, normalizeRefundStatus, normalizeSubscriptionStatus } from \"../helpers\";\nimport {\n\tPaymentConfigurationError,\n\tPaymentProviderError,\n} from \"../errors\";\n\nexport interface StripeDriverConfig {\n\tsecretKey: string;\n\t/** Your Stripe webhook signing secret (e.g., \"whsec_...\") */\n\twebhookSecret?: string;\n\t/** API version override (default: \"2024-12-18.acacia\") */\n\tapiVersion?: string;\n}\n\nconst API_BASE = \"https://api.stripe.com/v1\";\n\nexport class StripeDriver implements PaymentDriver {\n\treadonly id = \"stripe\";\n\n\tprivate secretKey: string;\n\tprivate webhookSecret: string;\n\tprivate apiVersion: string;\n\n\tconstructor(config: StripeDriverConfig) {\n\t\tif (!config.secretKey) {\n\t\t\tthrow new PaymentConfigurationError(\n\t\t\t\tthis.id,\n\t\t\t\t\"Stripe secretKey is required\",\n\t\t\t);\n\t\t}\n\t\tthis.secretKey = config.secretKey;\n\t\tthis.webhookSecret = config.webhookSecret ?? \"\";\n\t\tthis.apiVersion = config.apiVersion ?? \"2024-12-18.acacia\";\n\t}\n\n\t// ─── HTTP ──────────────────────────────────────────────────────────────\n\n\tprivate async request<T>(\n\t\tmethod: string,\n\t\tpath: string,\n\t\tbody?: Record<string, unknown>,\n\t): Promise<T> {\n\t\tconst url = `${API_BASE}${path}`;\n\t\tconst headers: Record<string, string> = {\n\t\t\tAuthorization: `Bearer ${this.secretKey}`,\n\t\t\t\"Stripe-Version\": this.apiVersion,\n\t\t};\n\n\t\tlet res: Response;\n\t\tif (method === \"GET\") {\n\t\t\tconst params = body\n\t\t\t\t? `?${new URLSearchParams(body as Record<string, string>).toString()}`\n\t\t\t\t: \"\";\n\t\t\tres = await fetch(`${url}${params}`, { method, headers });\n\t\t} else {\n\t\t\theaders[\"Content-Type\"] = \"application/x-www-form-urlencoded\";\n\t\t\tconst formBody = body ? this.flattenParams(body) : \"\";\n\t\t\tres = await fetch(url, { method, headers, body: formBody });\n\t\t}\n\n\t\tconst data = await res.json();\n\n\t\tif (!res.ok) {\n\t\t\tconst err = data as { error?: { message?: string; type?: string; code?: string } };\n\t\t\tthrow new PaymentProviderError(\n\t\t\t\tthis.id,\n\t\t\t\terr.error?.code ?? \"api_error\",\n\t\t\t\terr.error?.message ?? \"Stripe API error\",\n\t\t\t);\n\t\t}\n\n\t\treturn data as T;\n\t}\n\n\t/** Convert nested objects to Stripe's dot-notation form params */\n\tprivate flattenParams(\n\t\tobj: Record<string, unknown>,\n\t\tprefix = \"\",\n\t): string {\n\t\tconst parts: string[] = [];\n\t\tfor (const [key, value] of Object.entries(obj)) {\n\t\t\tif (value === undefined || value === null) continue;\n\t\t\tconst fullKey = prefix ? `${prefix}[${key}]` : key;\n\t\t\tif (typeof value === \"object\" && value !== null && !Array.isArray(value)) {\n\t\t\t\tparts.push(this.flattenParams(value as Record<string, unknown>, fullKey));\n\t\t\t} else if (Array.isArray(value)) {\n\t\t\t\tfor (let i = 0; i < value.length; i++) {\n\t\t\t\t\tconst itemKey = `${fullKey}[${i}]`;\n\t\t\t\t\tif (typeof value[i] === \"object\" && value[i] !== null) {\n\t\t\t\t\t\tparts.push(\n\t\t\t\t\t\t\tthis.flattenParams(value[i] as Record<string, unknown>, itemKey),\n\t\t\t\t\t\t);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tparts.push(`${encodeURIComponent(itemKey)}=${encodeURIComponent(String(value[i]))}`);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tparts.push(`${encodeURIComponent(fullKey)}=${encodeURIComponent(String(value))}`);\n\t\t\t}\n\t\t}\n\t\treturn parts.join(\"&\");\n\t}\n\n\t// ─── Checkout ─────────────────────────────────────────────────────────\n\n\tasync createCheckout(\n\t\tinput: CreateCheckoutInput,\n\t\topts?: PaymentOptions,\n\t): Promise<CheckoutResult> {\n\t\tconst params: Record<string, unknown> = {\n\t\t\tmode: \"payment\",\n\t\t\tpayment_method_types: [\"card\"],\n\t\t\tline_items: [\n\t\t\t\t{\n\t\t\t\t\tprice_data: {\n\t\t\t\t\t\tcurrency: input.currency.toLowerCase(),\n\t\t\t\t\t\tproduct_data: {\n\t\t\t\t\t\t\tname: input.description ?? \"Payment\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tunit_amount: this.toSmallestUnit(input.amount, input.currency),\n\t\t\t\t\t},\n\t\t\t\t\tquantity: 1,\n\t\t\t\t},\n\t\t\t],\n\t\t\tsuccess_url: input.successUrl ?? \"https://example.com/success\",\n\t\t\tcancel_url: input.cancelUrl ?? \"https://example.com/cancel\",\n\t\t};\n\n\t\tif (input.customerEmail) {\n\t\t\tparams.customer_email = input.customerEmail;\n\t\t}\n\t\tif (input.metadata) {\n\t\t\tparams.metadata = input.metadata;\n\t\t}\n\t\tif (opts?.idempotencyKey) {\n\t\t\t// Passed as header, not body\n\t\t}\n\n\t\tconst res = await this.request<{\n\t\t\tid: string;\n\t\t\tstatus: string;\n\t\t\tamount_total: number | null;\n\t\t\tcurrency: string | null;\n\t\t\turl: string | null;\n\t\t\tpayment_intent: string | { id: string; client_secret: string } | null;\n\t\t\texpires_at: number;\n\t\t\tcreated: number;\n\t\t\tmetadata: Record<string, unknown>;\n\t\t}>(\"POST\", \"/checkout/sessions\", params);\n\n\t\treturn {\n\t\t\tid: res.id,\n\t\t\tstatus: normalizeCheckoutStatus(this.id, res.status),\n\t\t\tamount: input.amount,\n\t\t\tcurrency: input.currency.toUpperCase(),\n\t\t\tprovider: this.id,\n\t\t\tcheckoutUrl: res.url ?? undefined,\n\t\t\tproviderPaymentId:\n\t\t\t\ttypeof res.payment_intent === \"string\"\n\t\t\t\t\t? res.payment_intent\n\t\t\t\t\t: typeof res.payment_intent === \"object\" && res.payment_intent\n\t\t\t\t\t\t? res.payment_intent.id\n\t\t\t\t\t\t: undefined,\n\t\t\tclientSecret:\n\t\t\t\ttypeof res.payment_intent === \"object\" && res.payment_intent\n\t\t\t\t\t? res.payment_intent.client_secret\n\t\t\t\t\t: undefined,\n\t\t\tmetadata: res.metadata,\n\t\t\texpiresAt: res.expires_at ? new Date(res.expires_at * 1000) : undefined,\n\t\t\tcreatedAt: new Date(res.created * 1000),\n\t\t};\n\t}\n\n\t// ─── Refund ───────────────────────────────────────────────────────────\n\n\tasync createRefund(\n\t\tinput: CreateRefundInput,\n\t\topts?: PaymentOptions,\n\t): Promise<RefundResult> {\n\t\tconst params: Record<string, unknown> = {\n\t\t\tpayment_intent: input.paymentId,\n\t\t};\n\t\tif (input.amount) {\n\t\t\tparams.amount = this.toSmallestUnit(input.amount, \"IDR\");\n\t\t}\n\t\tif (input.reason) {\n\t\t\tparams.reason = input.reason;\n\t\t}\n\t\tif (input.metadata) {\n\t\t\tparams.metadata = input.metadata;\n\t\t}\n\n\t\tconst res = await this.request<{\n\t\t\tid: string;\n\t\t\tstatus: string;\n\t\t\tamount: number;\n\t\t\tcurrency: string;\n\t\t\tpayment_intent: string;\n\t\t\treason: string | null;\n\t\t\tcreated: number;\n\t\t\tmetadata: Record<string, unknown>;\n\t\t}>(\"POST\", \"/refunds\", params);\n\n\t\treturn {\n\t\t\tid: res.id,\n\t\t\tstatus: normalizeRefundStatus(this.id, res.status),\n\t\t\tamount: this.fromSmallestUnit(res.amount, res.currency),\n\t\t\tcurrency: res.currency.toUpperCase(),\n\t\t\tprovider: this.id,\n\t\t\tpaymentId: res.payment_intent,\n\t\t\treason: res.reason ?? undefined,\n\t\t\tmetadata: res.metadata,\n\t\t\tcreatedAt: new Date(res.created * 1000),\n\t\t};\n\t}\n\n\t// ─── Subscription ─────────────────────────────────────────────────────\n\n\tasync createSubscription(\n\t\tinput: CreateSubscriptionInput,\n\t\topts?: PaymentOptions,\n\t): Promise<SubscriptionResult> {\n\t\tconst params: Record<string, unknown> = {\n\t\t\tprice: input.planId,\n\t\t};\n\t\tif (input.customerEmail) {\n\t\t\tparams.customer_email = input.customerEmail;\n\t\t}\n\t\tif (input.metadata) {\n\t\t\tparams.metadata = input.metadata;\n\t\t}\n\t\tif (input.trialPeriodDays) {\n\t\t\tparams.trial_period_days = input.trialPeriodDays;\n\t\t}\n\n\t\tconst res = await this.request<{\n\t\t\tid: string;\n\t\t\tstatus: string;\n\t\t\tcurrent_period_start: number;\n\t\t\tcurrent_period_end: number;\n\t\t\tcreated: number;\n\t\t\tmetadata: Record<string, unknown>;\n\t\t\titems: { data: Array<{ price: { id: string } }> };\n\t\t}>(\"POST\", \"/subscriptions\", params);\n\n\t\treturn {\n\t\t\tid: res.id,\n\t\t\tstatus: normalizeSubscriptionStatus(this.id, res.status),\n\t\t\tprovider: this.id,\n\t\t\tplanId: res.items.data[0]?.price.id ?? input.planId,\n\t\t\tcustomerEmail: input.customerEmail,\n\t\t\tmetadata: res.metadata,\n\t\t\tcurrentPeriodStart: new Date(res.current_period_start * 1000),\n\t\t\tcurrentPeriodEnd: new Date(res.current_period_end * 1000),\n\t\t\tcreatedAt: new Date(res.created * 1000),\n\t\t};\n\t}\n\n\t// ─── Payment Link ─────────────────────────────────────────────────────\n\n\tasync createPaymentLink(\n\t\tinput: CreatePaymentLinkInput,\n\t): Promise<PaymentLinkResult> {\n\t\tconst params: Record<string, unknown> = {\n\t\t\tline_items: [\n\t\t\t\t{\n\t\t\t\t\tprice_data: {\n\t\t\t\t\t\tcurrency: input.currency.toLowerCase(),\n\t\t\t\t\t\tproduct_data: {\n\t\t\t\t\t\t\tname: input.description ?? \"Payment\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tunit_amount: this.toSmallestUnit(input.amount, input.currency),\n\t\t\t\t\t},\n\t\t\t\t\tquantity: 1,\n\t\t\t\t},\n\t\t\t],\n\t\t\tactive: true,\n\t\t};\n\n\t\tif (input.metadata) {\n\t\t\tparams.metadata = input.metadata;\n\t\t}\n\n\t\tconst res = await this.request<{\n\t\t\tid: string;\n\t\t\turl: string;\n\t\t\tactive: boolean;\n\t\t\tcreated: number;\n\t\t\tmetadata: Record<string, unknown>;\n\t\t}>(\"POST\", \"/payment_links\", params);\n\n\t\treturn {\n\t\t\tid: res.id,\n\t\t\turl: res.url,\n\t\t\tamount: input.amount,\n\t\t\tcurrency: input.currency.toUpperCase(),\n\t\t\tprovider: this.id,\n\t\t\tmetadata: res.metadata,\n\t\t\texpiresAt: input.expiresAt ? new Date(input.expiresAt) : undefined,\n\t\t\tcreatedAt: new Date(res.created * 1000),\n\t\t};\n\t}\n\n\t// ─── Webhook Verification ─────────────────────────────────────────────\n\n\tasync verifyWebhookSignature(\n\t\tpayload: string,\n\t\tsignature: string,\n\t\t_secret: string,\n\t): Promise<boolean> {\n\t\tconst secret = _secret || this.webhookSecret;\n\t\tif (!secret) return false;\n\n\t\ttry {\n\t\t\tconst parts = Object.fromEntries(\n\t\t\t\tsignature.split(\",\").map((p) => p.split(\"=\")),\n\t\t\t);\n\t\t\tconst timestamp = Number(parts.t);\n\t\t\tconst receivedSig = parts.v1;\n\n\t\t\tif (!timestamp || !receivedSig) return false;\n\n\t\t\t// Reject if timestamp is > 5 minutes old\n\t\t\tconst tolerance = 300;\n\t\t\tif (Math.abs(Date.now() / 1000 - timestamp) > tolerance) return false;\n\n\t\t\tconst signedPayload = `${timestamp}.${payload}`;\n\t\t\tconst computed = await this.hmacSha256(signedPayload, secret);\n\t\t\treturn computed === receivedSig;\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\tprivate async hmacSha256(message: string, secret: string): Promise<string> {\n\t\tconst encoder = new TextEncoder();\n\t\tconst key = await crypto.subtle.importKey(\n\t\t\t\"raw\",\n\t\t\tencoder.encode(secret),\n\t\t\t{ name: \"HMAC\", hash: \"SHA-256\" },\n\t\t\tfalse,\n\t\t\t[\"sign\"],\n\t\t);\n\t\tconst signature = await crypto.subtle.sign(\n\t\t\t\"HMAC\",\n\t\t\tkey,\n\t\t\tencoder.encode(message),\n\t\t);\n\t\treturn Array.from(new Uint8Array(signature))\n\t\t\t.map((b) => b.toString(16).padStart(2, \"0\"))\n\t\t\t.join(\"\");\n\t}\n\n\tparseWebhookEvent(payload: string): WebhookEvent {\n\t\tconst event = JSON.parse(payload) as {\n\t\t\tid: string;\n\t\t\ttype: string;\n\t\t\tcreated: number;\n\t\t\tdata: { object: Record<string, unknown> };\n\t\t};\n\n\t\tconst typeMap: Record<string, WebhookEvent[\"type\"]> = {\n\t\t\t\"checkout.session.completed\": \"payment.completed\",\n\t\t\t\"payment_intent.succeeded\": \"payment.completed\",\n\t\t\t\"payment_intent.payment_failed\": \"payment.failed\",\n\t\t\t\"checkout.session.expired\": \"payment.expired\",\n\t\t\t\"customer.subscription.created\": \"subscription.created\",\n\t\t\t\"customer.subscription.deleted\": \"subscription.cancelled\",\n\t\t\t\"invoice.paid\": \"subscription.payment_succeeded\",\n\t\t\t\"invoice.payment_failed\": \"subscription.payment_failed\",\n\t\t\t\"charge.refunded\": \"refund.completed\",\n\t\t};\n\n\t\tconst obj = event.data.object;\n\t\tconst normalizedType = typeMap[event.type] ?? \"payment.created\";\n\n\t\treturn {\n\t\t\tid: event.id,\n\t\t\ttype: normalizedType,\n\t\t\tprovider: this.id,\n\t\t\trawData: event,\n\t\t\tentityId: (obj.id as string) ?? undefined,\n\t\t\tamount: typeof obj.amount === \"number\" ? obj.amount / 100 : undefined,\n\t\t\tcurrency: typeof obj.currency === \"string\" ? obj.currency.toUpperCase() : undefined,\n\t\t\tstatus: typeof obj.status === \"string\" ? obj.status : undefined,\n\t\t\tmetadata: (obj.metadata as Record<string, unknown>) ?? undefined,\n\t\t\tcreatedAt: new Date(event.created * 1000),\n\t\t};\n\t}\n\n\t// ─── Currency Helpers ─────────────────────────────────────────────────\n\n\tprivate toSmallestUnit(amount: number, currency: string): number {\n\t\tconst zeroDecimal = [\"BIF\", \"CLP\", \"DJF\", \"GNF\", \"JPY\", \"KMF\", \"KRW\", \"MGA\", \"PYG\", \"RWF\", \"UGX\", \"VND\", \"VUV\", \"XAF\", \"XOF\", \"XPF\"];\n\t\treturn zeroDecimal.includes(currency.toUpperCase())\n\t\t\t? Math.round(amount)\n\t\t\t: Math.round(amount * 100);\n\t}\n\n\tprivate fromSmallestUnit(amount: number, currency: string): number {\n\t\tconst zeroDecimal = [\"BIF\", \"CLP\", \"DJF\", \"GNF\", \"JPY\", \"KMF\", \"KRW\", \"MGA\", \"PYG\", \"RWF\", \"UGX\", \"VND\", \"VUV\", \"XAF\", \"XOF\", \"XPF\"];\n\t\treturn zeroDecimal.includes(currency.toUpperCase())\n\t\t\t? amount\n\t\t\t: amount / 100;\n\t}\n}\n","import type { PaymentDriver } from \"../driver\";\nimport type {\n\tCreateCheckoutInput,\n\tCreateRefundInput,\n\tCreateSubscriptionInput,\n\tCheckoutResult,\n\tPaymentOptions,\n\tRefundResult,\n\tSubscriptionResult,\n\tWebhookEvent,\n} from \"../types\";\nimport {\n\tnormalizeCheckoutStatus,\n\tnormalizeRefundStatus,\n} from \"../helpers\";\nimport {\n\tPaymentProviderError,\n} from \"../errors\";\n\nexport interface MidtransDriverConfig {\n\t/** Your Midtrans server key (e.g., \"SB-Mid-server-...\") */\n\tserverKey: string;\n\t/** Your Midtrans client key (for frontend Snap integration) */\n\tclientKey?: string;\n\t/** Use sandbox mode (default: false) */\n\tisProduction?: boolean;\n\t/** Webhook notification URL for Midtrans to POST to */\n\tnotificationUrl?: string;\n}\n\n// ─── API Request/Response Types ───────────────────────────────────────────────\n\ninterface MidtransSnapRequest {\n\ttransaction_details: {\n\t\torder_id: string;\n\t\tgross_amount: number;\n\t};\n\tcredit_card: { secure: boolean };\n\tcustomer_details?: {\n\t\temail?: string;\n\t\tfirst_name?: string;\n\t\tlast_name?: string;\n\t};\n\titem_details?: Array<{\n\t\tid: string;\n\t\tprice: number;\n\t\tquantity: number;\n\t\tname: string;\n\t}>;\n\tcallbacks?: { finish: string };\n\tcustom_field1?: string;\n}\n\ninterface MidtransSnapResponse {\n\ttoken: string;\n\tredirect_url: string;\n}\n\ninterface MidtransRefundRequest {\n\trefund_type: \"full\" | \"partial\";\n\trefund_amount?: number;\n\treason?: string;\n}\n\ninterface MidtransRefundResponse {\n\tstatus_code: string;\n\tstatus_message: string;\n\ttransaction_status: string;\n}\n\ninterface MidtransSubscriptionRequest {\n\tname: string;\n\tamount: number;\n\tcurrency: string;\n\tinterval: string;\n\tinterval_count: number;\n\tcustomer_email?: string;\n}\n\ninterface MidtransSubscriptionResponse {\n\tid: string;\n\tstatus: string;\n\tcreated_at: string;\n}\n\ninterface MidtransWebhookBody {\n\tsignature_key: string;\n\torder_id: string;\n\tstatus_code: string;\n\ttransaction_status: string;\n\tgross_amount: string;\n\ttransaction_time: string;\n\tcustom_field1?: string;\n\t[key: string]: unknown;\n}\n\ninterface MidtransErrorResponse {\n\tstatus_code?: string;\n\tstatus_message?: string;\n\terror_messages?: string[];\n\t[key: string]: unknown;\n}\n\n// ─── Base URLs ────────────────────────────────────────────────────────────────\n\nconst SNAP_BASE_DEV = \"https://app.sandbox.midtrans.com/snap\";\nconst SNAP_BASE_PROD = \"https://app.midtrans.com/snap\";\nconst API_BASE_DEV = \"https://api.sandbox.midtrans.com/v2\";\nconst API_BASE_PROD = \"https://api.midtrans.com/v2\";\n\n// ─── Driver ───────────────────────────────────────────────────────────────────\n\nexport class MidtransDriver implements PaymentDriver {\n\treadonly id = \"midtrans\";\n\n\tprivate serverKey: string;\n\tprivate snapBase: string;\n\tprivate apiBase: string;\n\tprivate notificationUrl?: string;\n\n\tconstructor(config: MidtransDriverConfig) {\n\t\tif (!config.serverKey) {\n\t\t\tthrow new PaymentProviderError(\n\t\t\t\tthis.id,\n\t\t\t\t\"CONFIGURATION_ERROR\",\n\t\t\t\t\"Midtrans serverKey is required\",\n\t\t\t);\n\t\t}\n\t\tthis.serverKey = config.serverKey;\n\t\tconst isProd = config.isProduction ?? false;\n\t\tthis.snapBase = isProd ? SNAP_BASE_PROD : SNAP_BASE_DEV;\n\t\tthis.apiBase = isProd ? API_BASE_PROD : API_BASE_DEV;\n\t\tthis.notificationUrl = config.notificationUrl;\n\t}\n\n\t// ─── HTTP ──────────────────────────────────────────────────────────────\n\n\tprivate authHeader(): string {\n\t\tconst encoded = btoa(`${this.serverKey}:`);\n\t\treturn `Basic ${encoded}`;\n\t}\n\n\tprivate async request<T>(\n\t\tmethod: string,\n\t\tbase: string,\n\t\tpath: string,\n\t\tbody?: MidtransSnapRequest | MidtransRefundRequest | MidtransSubscriptionRequest,\n\t): Promise<T> {\n\t\tconst url = `${base}${path}`;\n\t\tconst headers: Record<string, string> = {\n\t\t\tAuthorization: this.authHeader(),\n\t\t\t\"Content-Type\": \"application/json\",\n\t\t};\n\n\t\tconst res = await fetch(url, {\n\t\t\tmethod,\n\t\t\theaders,\n\t\t\tbody: body ? JSON.stringify(body) : undefined,\n\t\t});\n\n\t\tconst data = (await res.json()) as Record<string, unknown>;\n\n\t\tif (!res.ok || data.error_messages) {\n\t\t\tconst errData = data as unknown as MidtransErrorResponse;\n\t\t\tthrow new PaymentProviderError(\n\t\t\t\tthis.id,\n\t\t\t\terrData.status_code ?? \"api_error\",\n\t\t\t\terrData.error_messages?.join(\", \") ??\n\t\t\t\t\terrData.status_message ??\n\t\t\t\t\t\"Midtrans API error\",\n\t\t\t);\n\t\t}\n\n\t\treturn data as T;\n\t}\n\n\t// ─── Checkout ─────────────────────────────────────────────────────────\n\n\tasync createCheckout(\n\t\tinput: CreateCheckoutInput,\n\t\topts?: PaymentOptions,\n\t): Promise<CheckoutResult> {\n\t\tconst orderId = opts?.idempotencyKey ?? `ORDER-${crypto.randomUUID().slice(0, 8)}`;\n\n\t\tconst params: MidtransSnapRequest = {\n\t\t\ttransaction_details: {\n\t\t\t\torder_id: orderId,\n\t\t\t\tgross_amount: input.amount,\n\t\t\t},\n\t\t\tcredit_card: {\n\t\t\t\tsecure: true,\n\t\t\t},\n\t\t};\n\n\t\tif (input.customerEmail || input.customerName) {\n\t\t\tconst nameParts = input.customerName?.split(\" \") ?? [];\n\t\t\tparams.customer_details = {\n\t\t\t\temail: input.customerEmail,\n\t\t\t\tfirst_name: nameParts[0],\n\t\t\t\tlast_name: nameParts.slice(1).join(\" \") || undefined,\n\t\t\t};\n\t\t}\n\n\t\tif (input.description) {\n\t\t\tparams.item_details = [\n\t\t\t\t{\n\t\t\t\t\tid: orderId,\n\t\t\t\t\tprice: input.amount,\n\t\t\t\t\tquantity: 1,\n\t\t\t\t\tname: input.description,\n\t\t\t\t},\n\t\t\t];\n\t\t}\n\n\t\tif (this.notificationUrl) {\n\t\t\tparams.callbacks = {\n\t\t\t\tfinish: input.successUrl ?? \"https://example.com/success\",\n\t\t\t};\n\t\t}\n\n\t\tif (input.metadata) {\n\t\t\tparams.custom_field1 = JSON.stringify(input.metadata);\n\t\t}\n\n\t\tconst res = await this.request<MidtransSnapResponse>(\n\t\t\t\"POST\",\n\t\t\tthis.snapBase,\n\t\t\t\"/transactions\",\n\t\t\tparams,\n\t\t);\n\n\t\treturn {\n\t\t\tid: orderId,\n\t\t\tstatus: \"pending\",\n\t\t\tamount: input.amount,\n\t\t\tcurrency: input.currency.toUpperCase(),\n\t\t\tprovider: this.id,\n\t\t\tcheckoutUrl: res.redirect_url,\n\t\t\tproviderPaymentId: res.token,\n\t\t\tmetadata: input.metadata,\n\t\t\tcreatedAt: new Date(),\n\t\t};\n\t}\n\n\t// ─── Refund ───────────────────────────────────────────────────────────\n\n\tasync createRefund(\n\t\tinput: CreateRefundInput,\n\t\topts?: PaymentOptions,\n\t): Promise<RefundResult> {\n\t\tconst params: MidtransRefundRequest = {\n\t\t\trefund_type: input.amount ? \"partial\" : \"full\",\n\t\t};\n\t\tif (input.amount) {\n\t\t\tparams.refund_amount = input.amount;\n\t\t}\n\t\tif (input.reason) {\n\t\t\tparams.reason = input.reason;\n\t\t}\n\n\t\tconst res = await this.request<MidtransRefundResponse>(\n\t\t\t\"POST\",\n\t\t\tthis.apiBase,\n\t\t\t`/transactions/${input.paymentId}/refund`,\n\t\t\tparams,\n\t\t);\n\n\t\treturn {\n\t\t\tid: `${input.paymentId}-refund-${crypto.randomUUID().slice(0, 8)}`,\n\t\t\tstatus: normalizeRefundStatus(this.id, res.transaction_status),\n\t\t\tamount: input.amount ?? 0,\n\t\t\tcurrency: \"IDR\",\n\t\t\tprovider: this.id,\n\t\t\tpaymentId: input.paymentId,\n\t\t\treason: input.reason,\n\t\t\tmetadata: input.metadata,\n\t\t\tcreatedAt: new Date(),\n\t\t};\n\t}\n\n\t// ─── Subscription (Midtrans Recurring) ────────────────────────────────\n\n\tasync createSubscription(\n\t\tinput: CreateSubscriptionInput,\n\t\topts?: PaymentOptions,\n\t): Promise<SubscriptionResult> {\n\t\tconst params: MidtransSubscriptionRequest = {\n\t\t\tname: input.planId,\n\t\t\tamount: 0,\n\t\t\tcurrency: \"IDR\",\n\t\t\tinterval: \"month\",\n\t\t\tinterval_count: 1,\n\t\t};\n\n\t\tif (input.customerEmail) {\n\t\t\tparams.customer_email = input.customerEmail;\n\t\t}\n\n\t\tconst res = await this.request<MidtransSubscriptionResponse>(\n\t\t\t\"POST\",\n\t\t\tthis.apiBase,\n\t\t\t\"/subscriptions\",\n\t\t\tparams,\n\t\t);\n\n\t\treturn {\n\t\t\tid: res.id,\n\t\t\tstatus: res.status === \"active\" ? \"active\" : \"incomplete\",\n\t\t\tprovider: this.id,\n\t\t\tplanId: input.planId,\n\t\t\tcustomerEmail: input.customerEmail,\n\t\t\tmetadata: input.metadata,\n\t\t\tcreatedAt: new Date(res.created_at),\n\t\t};\n\t}\n\n\t// ─── Webhook Verification ─────────────────────────────────────────────\n\n\tasync verifyWebhookSignature(\n\t\tpayload: string,\n\t\tsignature: string,\n\t\tsecret: string,\n\t): Promise<boolean> {\n\t\ttry {\n\t\t\tconst body = JSON.parse(payload) as MidtransWebhookBody;\n\t\t\tif (!body.signature_key || !body.order_id || !body.status_code) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tconst serverKey = secret || this.serverKey;\n\t\t\tconst raw = `${body.order_id}${body.status_code}${serverKey}`;\n\t\t\tconst computed = await this.sha512(raw);\n\t\t\treturn computed === body.signature_key;\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\tprivate async sha512(message: string): Promise<string> {\n\t\tconst encoder = new TextEncoder();\n\t\tconst data = encoder.encode(message);\n\t\tconst hashBuffer = await crypto.subtle.digest(\"SHA-512\", data);\n\t\treturn Array.from(new Uint8Array(hashBuffer))\n\t\t\t.map((b) => b.toString(16).padStart(2, \"0\"))\n\t\t\t.join(\"\");\n\t}\n\n\tparseWebhookEvent(payload: string): WebhookEvent {\n\t\tconst body = JSON.parse(payload) as MidtransWebhookBody;\n\n\t\tconst typeMap: Record<string, WebhookEvent[\"type\"]> = {\n\t\t\tsettlement: \"payment.completed\",\n\t\t\tcapture: \"payment.completed\",\n\t\t\tpending: \"payment.created\",\n\t\t\tdeny: \"payment.failed\",\n\t\t\tfailure: \"payment.failed\",\n\t\t\tcancel: \"payment.failed\",\n\t\t\texpire: \"payment.expired\",\n\t\t\trefund: \"refund.completed\",\n\t\t\tpartial_refund: \"refund.completed\",\n\t\t};\n\n\t\treturn {\n\t\t\tid: body.order_id,\n\t\t\ttype: typeMap[body.transaction_status] ?? \"payment.created\",\n\t\t\tprovider: this.id,\n\t\t\trawData: body,\n\t\t\tentityId: body.order_id,\n\t\t\tamount: body.gross_amount ? Number(body.gross_amount) : undefined,\n\t\t\tcurrency: \"IDR\",\n\t\t\tstatus: body.transaction_status,\n\t\t\tmetadata: body.custom_field1\n\t\t\t\t? JSON.parse(body.custom_field1)\n\t\t\t\t: undefined,\n\t\t\tcreatedAt: body.transaction_time\n\t\t\t\t? new Date(body.transaction_time)\n\t\t\t\t: new Date(),\n\t\t};\n\t}\n}\n","import type { PaymentDriver } from \"../driver\";\nimport type {\n\tCreateCheckoutInput,\n\tCreatePaymentLinkInput,\n\tCreateRefundInput,\n\tCheckoutResult,\n\tPaymentLinkResult,\n\tPaymentOptions,\n\tRefundResult,\n\tWebhookEvent,\n} from \"../types\";\nimport {\n\tnormalizeCheckoutStatus,\n\tnormalizeRefundStatus,\n} from \"../helpers\";\nimport {\n\tPaymentProviderError,\n} from \"../errors\";\n\nexport interface XenditDriverConfig {\n\t/** Your Xendit secret API key (e.g., \"xnd_development_...\") */\n\tsecretKey: string;\n\t/** Use sandbox mode (default: false) */\n\tisProduction?: boolean;\n}\n\n// ─── API Request/Response Types ───────────────────────────────────────────────\n\ninterface XenditPaymentMethod {\n\ttype: string;\n\treusability?: string;\n\tpermanent?: boolean;\n\tbank_transfer?: { bank: string };\n\tvirtual_account_info?: { permanent: boolean };\n}\n\ninterface XenditPaymentRequest {\n\treference_id: string;\n\tamount: number;\n\tcurrency: string;\n\tpayment_method: XenditPaymentMethod;\n\tdescription?: string;\n\tcustomer?: {\n\t\treference_id: string;\n\t\temail?: string;\n\t\tname?: string;\n\t};\n\tmetadata?: Record<string, unknown>;\n}\n\ninterface XenditPaymentResponse {\n\tid: string;\n\treference_id: string;\n\tstatus: string;\n\tamount: number;\n\tcurrency: string;\n\tdescription: string;\n\tcreated: string;\n\tupdated: string;\n\tactions?: Array<{ url: string }>;\n\tmetadata?: Record<string, unknown>;\n}\n\ninterface XenditRefundRequest {\n\tpayment_request_id: string;\n\tamount?: number;\n\treason?: string;\n}\n\ninterface XenditRefundResponse {\n\tid: string;\n\tpayment_request_id: string;\n\tstatus: string;\n\tamount: number;\n\tcreated: string;\n}\n\ninterface XenditErrorResponse {\n\terror_code?: string;\n\tmessage?: string;\n\terrors?: Array<{ message?: string }>;\n}\n\ninterface XenditWebhookBody {\n\tid: string;\n\treference_id?: string;\n\tstatus: string;\n\tamount: number;\n\tcurrency: string;\n\tcreated: string;\n\tupdated?: string;\n\tdescription?: string;\n\tpayment_request_id?: string;\n\tmetadata?: Record<string, unknown>;\n\t[key: string]: unknown;\n}\n\n// ─── Driver ───────────────────────────────────────────────────────────────────\n\nexport class XenditDriver implements PaymentDriver {\n\treadonly id = \"xendit\";\n\n\tprivate secretKey: string;\n\tprivate apiBase: string;\n\n\tconstructor(config: XenditDriverConfig) {\n\t\tif (!config.secretKey) {\n\t\t\tthrow new PaymentProviderError(\n\t\t\t\tthis.id,\n\t\t\t\t\"CONFIGURATION_ERROR\",\n\t\t\t\t\"Xendit secretKey is required\",\n\t\t\t);\n\t\t}\n\t\tthis.secretKey = config.secretKey;\n\t\tthis.apiBase = \"https://api.xendit.co\";\n\t}\n\n\t// ─── HTTP ──────────────────────────────────────────────────────────────\n\n\tprivate async request<T>(\n\t\tmethod: string,\n\t\tpath: string,\n\t\tbody?: XenditPaymentRequest | XenditRefundRequest,\n\t): Promise<T> {\n\t\tconst url = `${this.apiBase}${path}`;\n\t\tconst headers: Record<string, string> = {\n\t\t\tAuthorization: this.secretKey,\n\t\t\t\"Content-Type\": \"application/json\",\n\t\t};\n\n\t\tconst res = await fetch(url, {\n\t\t\tmethod,\n\t\t\theaders,\n\t\t\tbody: body ? JSON.stringify(body) : undefined,\n\t\t});\n\n\t\tconst data = (await res.json()) as Record<string, unknown>;\n\n\t\tif (!res.ok) {\n\t\t\tconst errData = data as unknown as XenditErrorResponse;\n\t\t\tthrow new PaymentProviderError(\n\t\t\t\tthis.id,\n\t\t\t\terrData.error_code ?? \"api_error\",\n\t\t\t\terrData.message ??\n\t\t\t\t\terrData.errors?.[0]?.message ??\n\t\t\t\t\t\"Xendit API error\",\n\t\t\t);\n\t\t}\n\n\t\treturn data as T;\n\t}\n\n\t// ─── Checkout (Payment Request) ───────────────────────────────────────\n\n\tasync createCheckout(\n\t\tinput: CreateCheckoutInput,\n\t\topts?: PaymentOptions,\n\t): Promise<CheckoutResult> {\n\t\tconst referenceId = opts?.idempotencyKey ?? `REF-${crypto.randomUUID().slice(0, 8)}`;\n\n\t\tconst params: XenditPaymentRequest = {\n\t\t\treference_id: referenceId,\n\t\t\tamount: input.amount,\n\t\t\tcurrency: input.currency.toUpperCase(),\n\t\t\tpayment_method: {\n\t\t\t\ttype: \"VIRTUAL_ACCOUNT\",\n\t\t\t\tpermanent: false,\n\t\t\t},\n\t\t\tdescription: input.description ?? \"Payment\",\n\t\t};\n\n\t\tif (input.customerEmail || input.customerName) {\n\t\t\tparams.customer = {\n\t\t\t\treference_id: referenceId,\n\t\t\t\temail: input.customerEmail,\n\t\t\t\tname: input.customerName,\n\t\t\t};\n\t\t}\n\n\t\tif (input.metadata) {\n\t\t\tparams.metadata = input.metadata;\n\t\t}\n\n\t\tconst res = await this.request<XenditPaymentResponse>(\n\t\t\t\"POST\",\n\t\t\t\"/payment_requests\",\n\t\t\tparams,\n\t\t);\n\n\t\tconst actions = res.actions ?? [];\n\t\tconst checkoutUrl = actions.find((a) => a.url)?.url;\n\n\t\treturn {\n\t\t\tid: res.id,\n\t\t\tstatus: normalizeCheckoutStatus(this.id, res.status),\n\t\t\tamount: res.amount,\n\t\t\tcurrency: res.currency,\n\t\t\tprovider: this.id,\n\t\t\tcheckoutUrl,\n\t\t\tproviderPaymentId: res.id,\n\t\t\tmetadata: res.metadata,\n\t\t\tcreatedAt: new Date(res.created),\n\t\t};\n\t}\n\n\t// ─── Refund ───────────────────────────────────────────────────────────\n\n\tasync createRefund(\n\t\tinput: CreateRefundInput,\n\t\topts?: PaymentOptions,\n\t): Promise<RefundResult> {\n\t\tconst params: XenditRefundRequest = {\n\t\t\tpayment_request_id: input.paymentId,\n\t\t};\n\t\tif (input.amount) {\n\t\t\tparams.amount = input.amount;\n\t\t}\n\t\tif (input.reason) {\n\t\t\tparams.reason = input.reason;\n\t\t}\n\n\t\tconst res = await this.request<XenditRefundResponse>(\n\t\t\t\"POST\",\n\t\t\t\"/refunds\",\n\t\t\tparams,\n\t\t);\n\n\t\treturn {\n\t\t\tid: res.id,\n\t\t\tstatus: normalizeRefundStatus(this.id, res.status),\n\t\t\tamount: res.amount,\n\t\t\tcurrency: \"IDR\",\n\t\t\tprovider: this.id,\n\t\t\tpaymentId: res.payment_request_id,\n\t\t\treason: input.reason,\n\t\t\tmetadata: input.metadata,\n\t\t\tcreatedAt: new Date(res.created),\n\t\t};\n\t}\n\n\t// ─── Payment Link ─────────────────────────────────────────────────────\n\n\tasync createPaymentLink(\n\t\tinput: CreatePaymentLinkInput,\n\t): Promise<PaymentLinkResult> {\n\t\tconst params: XenditPaymentRequest = {\n\t\t\treference_id: `REF-${crypto.randomUUID().slice(0, 8)}`,\n\t\t\tamount: input.amount,\n\t\t\tcurrency: input.currency.toUpperCase(),\n\t\t\tpayment_method: {\n\t\t\t\ttype: \"VIRTUAL_ACCOUNT\",\n\t\t\t},\n\t\t\tdescription: input.description ?? \"Payment\",\n\t\t};\n\n\t\tif (input.metadata) {\n\t\t\tparams.metadata = input.metadata;\n\t\t}\n\n\t\tconst res = await this.request<XenditPaymentResponse>(\n\t\t\t\"POST\",\n\t\t\t\"/payment_requests\",\n\t\t\tparams,\n\t\t);\n\n\t\tconst actions = res.actions ?? [];\n\t\tconst url = actions.find((a) => a.url)?.url ?? \"\";\n\n\t\treturn {\n\t\t\tid: res.id,\n\t\t\turl,\n\t\t\tamount: res.amount,\n\t\t\tcurrency: res.currency,\n\t\t\tprovider: this.id,\n\t\t\tmetadata: res.metadata,\n\t\t\texpiresAt: input.expiresAt ? new Date(input.expiresAt) : undefined,\n\t\t\tcreatedAt: new Date(res.created),\n\t\t};\n\t}\n\n\t// ─── Webhook Verification ─────────────────────────────────────────────\n\n\tasync verifyWebhookSignature(\n\t\tpayload: string,\n\t\tsignature: string,\n\t\tsecret: string,\n\t): Promise<boolean> {\n\t\ttry {\n\t\t\tconst encoder = new TextEncoder();\n\t\t\tconst key = await crypto.subtle.importKey(\n\t\t\t\t\"raw\",\n\t\t\t\tencoder.encode(secret),\n\t\t\t\t{ name: \"HMAC\", hash: \"SHA-256\" },\n\t\t\t\tfalse,\n\t\t\t\t[\"sign\"],\n\t\t\t);\n\t\t\tconst sig = await crypto.subtle.sign(\"HMAC\", key, encoder.encode(payload));\n\t\t\tconst computed = Array.from(new Uint8Array(sig))\n\t\t\t\t.map((b) => b.toString(16).padStart(2, \"0\"))\n\t\t\t\t.join(\"\");\n\t\t\treturn computed === signature;\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\tparseWebhookEvent(payload: string): WebhookEvent {\n\t\tconst body = JSON.parse(payload) as XenditWebhookBody;\n\n\t\tconst typeMap: Record<string, WebhookEvent[\"type\"]> = {\n\t\t\tSUCCEEDED: \"payment.completed\",\n\t\t\tPENDING: \"payment.created\",\n\t\t\tFAILED: \"payment.failed\",\n\t\t\tREQUIRES_ACTION: \"payment.failed\",\n\t\t\tCANCELLED: \"payment.failed\",\n\t\t\tEXPIRED: \"payment.expired\",\n\t\t};\n\n\t\treturn {\n\t\t\tid: body.id,\n\t\t\ttype: typeMap[body.status] ?? \"payment.created\",\n\t\t\tprovider: this.id,\n\t\t\trawData: body,\n\t\t\tentityId: body.id,\n\t\t\tamount: body.amount,\n\t\t\tcurrency: body.currency,\n\t\t\tstatus: body.status,\n\t\t\tmetadata: body.metadata,\n\t\t\tcreatedAt: new Date(body.created),\n\t\t};\n\t}\n}\n","import type { PaymentDriver } from \"../driver\";\nimport type {\n\tCreateCheckoutInput,\n\tCreateRefundInput,\n\tCreateSubscriptionInput,\n\tCheckoutResult,\n\tPaymentOptions,\n\tRefundResult,\n\tSubscriptionResult,\n\tWebhookEvent,\n} from \"../types\";\nimport {\n\tnormalizeCheckoutStatus,\n\tnormalizeRefundStatus,\n} from \"../helpers\";\nimport {\n\tPaymentConfigurationError,\n\tPaymentProviderError,\n} from \"../errors\";\n\nexport interface PayPalDriverConfig {\n\t/** PayPal client ID */\n\tclientId: string;\n\t/** PayPal client secret */\n\tclientSecret: string;\n\t/** Use sandbox mode (default: false) */\n\tisProduction?: boolean;\n}\n\n// ─── API Request/Response Types ───────────────────────────────────────────────\n\ninterface PayPalOrderRequest {\n\tintent: \"CAPTURE\";\n\tpurchase_units: Array<{\n\t\tamount: {\n\t\t\tcurrency_code: string;\n\t\t\tvalue: string;\n\t\t};\n\t\tdescription?: string;\n\t}>;\n\tapplication_context: {\n\t\tbrand_name: string;\n\t\tlanding_page: string;\n\t\tuser_action: string;\n\t\treturn_url?: string;\n\t\tcancel_url?: string;\n\t};\n}\n\ninterface PayPalOrderResponse {\n\tid: string;\n\tstatus: string;\n\tlinks: Array<{ href: string; rel: string; method: string }>;\n\tcreate_time: string;\n}\n\ninterface PayPalRefundRequest {\n\tamount?: {\n\t\tvalue: string;\n\t\tcurrency_code: string;\n\t};\n\tnote_to_payer?: string;\n}\n\ninterface PayPalRefundResponse {\n\tid: string;\n\tstatus: string;\n\tamount: { value: string; currency_code: string };\n\tnote_to_payer?: string;\n\tcreate_time: string;\n}\n\ninterface PayPalSubscriptionRequest {\n\tplan_id: string;\n\tstart_time: string;\n\tsubscriber?: {\n\t\temail_address: string;\n\t};\n}\n\ninterface PayPalSubscriptionResponse {\n\tid: string;\n\tstatus: string;\n\tcreate_time: string;\n\tplan_id?: string;\n}\n\ninterface PayPalAccessTokenResponse {\n\taccess_token: string;\n\texpires_in: number;\n}\n\ninterface PayPalErrorResponse {\n\tname?: string;\n\tmessage?: string;\n\tdetails?: Array<{ description?: string }>;\n}\n\ninterface PayPalWebhookResource {\n\tid: string;\n\tstate?: string;\n\tamount?: { total: string; currency: string };\n\tpayer?: { email_address?: string };\n\tcreate_time?: string;\n\t[key: string]: unknown;\n}\n\ninterface PayPalWebhookBody {\n\tid: string;\n\tevent_type: string;\n\tresource: PayPalWebhookResource;\n\tcreate_time: string;\n\tresource_type: string;\n\t[key: string]: unknown;\n}\n\n// ─── Base URLs ────────────────────────────────────────────────────────────────\n\nconst API_BASE_SANDBOX = \"https://api-m.sandbox.paypal.com\";\nconst API_BASE_PROD = \"https://api-m.paypal.com\";\n\n// ─── Driver ───────────────────────────────────────────────────────────────────\n\nexport class PayPalDriver implements PaymentDriver {\n\treadonly id = \"paypal\";\n\n\tprivate clientId: string;\n\tprivate clientSecret: string;\n\tprivate apiBase: string;\n\tprivate accessToken?: string;\n\tprivate tokenExpiry = 0;\n\n\tconstructor(config: PayPalDriverConfig) {\n\t\tif (!config.clientId || !config.clientSecret) {\n\t\t\tthrow new PaymentConfigurationError(\n\t\t\t\tthis.id,\n\t\t\t\t\"PayPal clientId and clientSecret are required\",\n\t\t\t);\n\t\t}\n\t\tthis.clientId = config.clientId;\n\t\tthis.clientSecret = config.clientSecret;\n\t\tthis.apiBase = config.isProduction ? API_BASE_PROD : API_BASE_SANDBOX;\n\t}\n\n\t// ─── Auth ─────────────────────────────────────────────────────────────\n\n\tprivate async getAccessToken(): Promise<string> {\n\t\tif (this.accessToken && Date.now() < this.tokenExpiry) {\n\t\t\treturn this.accessToken;\n\t\t}\n\n\t\tconst encoded = btoa(`${this.clientId}:${this.clientSecret}`);\n\t\tconst res = await fetch(`${this.apiBase}/v1/oauth2/token`, {\n\t\t\tmethod: \"POST\",\n\t\t\theaders: {\n\t\t\t\tAuthorization: `Basic ${encoded}`,\n\t\t\t\t\"Content-Type\": \"application/x-www-form-urlencoded\",\n\t\t\t},\n\t\t\tbody: \"grant_type=client_credentials\",\n\t\t});\n\n\t\tconst data = (await res.json()) as PayPalAccessTokenResponse;\n\n\t\tif (!res.ok || !data.access_token) {\n\t\t\tthrow new PaymentProviderError(\n\t\t\t\tthis.id,\n\t\t\t\t\"AUTH_ERROR\",\n\t\t\t\t\"Failed to obtain PayPal access token\",\n\t\t\t);\n\t\t}\n\n\t\tthis.accessToken = data.access_token;\n\t\tthis.tokenExpiry = Date.now() + (data.expires_in - 300) * 1000;\n\n\t\treturn this.accessToken;\n\t}\n\n\t// ─── HTTP ──────────────────────────────────────────────────────────────\n\n\tprivate async request<T>(\n\t\tmethod: string,\n\t\tpath: string,\n\t\tbody?: PayPalOrderRequest | PayPalRefundRequest | PayPalSubscriptionRequest,\n\t): Promise<T> {\n\t\tconst url = `${this.apiBase}${path}`;\n\t\tconst token = await this.getAccessToken();\n\t\tconst headers: Record<string, string> = {\n\t\t\tAuthorization: `Bearer ${token}`,\n\t\t\t\"Content-Type\": \"application/json\",\n\t\t\tPrefer: \"return=representation\",\n\t\t};\n\n\t\tconst res = await fetch(url, {\n\t\t\tmethod,\n\t\t\theaders,\n\t\t\tbody: body ? JSON.stringify(body) : undefined,\n\t\t});\n\n\t\tif (res.status === 204) {\n\t\t\treturn {} as T;\n\t\t}\n\n\t\tconst data = (await res.json()) as Record<string, unknown>;\n\n\t\tif (!res.ok) {\n\t\t\tconst errData = data as unknown as PayPalErrorResponse;\n\t\t\tthrow new PaymentProviderError(\n\t\t\t\tthis.id,\n\t\t\t\terrData.name ?? \"api_error\",\n\t\t\t\terrData.message ??\n\t\t\t\t\terrData.details?.[0]?.description ??\n\t\t\t\t\t\"PayPal API error\",\n\t\t\t);\n\t\t}\n\n\t\treturn data as T;\n\t}\n\n\t// ─── Checkout (Order) ─────────────────────────────────────────────────\n\n\tasync createCheckout(\n\t\tinput: CreateCheckoutInput,\n\t\topts?: PaymentOptions,\n\t): Promise<CheckoutResult> {\n\t\tconst params: PayPalOrderRequest = {\n\t\t\tintent: \"CAPTURE\",\n\t\t\tpurchase_units: [\n\t\t\t\t{\n\t\t\t\t\tamount: {\n\t\t\t\t\t\tcurrency_code: input.currency.toUpperCase(),\n\t\t\t\t\t\tvalue: input.amount.toFixed(2),\n\t\t\t\t\t},\n\t\t\t\t\tdescription: input.description ?? \"Payment\",\n\t\t\t\t},\n\t\t\t],\n\t\t\tapplication_context: {\n\t\t\t\tbrand_name: \"Buntok Store\",\n\t\t\t\tlanding_page: \"BILLING\",\n\t\t\t\tuser_action: \"PAY_NOW\",\n\t\t\t},\n\t\t};\n\n\t\tif (input.successUrl) {\n\t\t\tparams.application_context.return_url = input.successUrl;\n\t\t}\n\t\tif (input.cancelUrl) {\n\t\t\tparams.application_context.cancel_url = input.cancelUrl;\n\t\t}\n\n\t\tconst res = await this.request<PayPalOrderResponse>(\n\t\t\t\"POST\",\n\t\t\t\"/v2/checkout/orders\",\n\t\t\tparams,\n\t\t);\n\n\t\tconst approveLink = res.links.find((l) => l.rel === \"approve\");\n\n\t\treturn {\n\t\t\tid: res.id,\n\t\t\tstatus: normalizeCheckoutStatus(this.id, res.status),\n\t\t\tamount: input.amount,\n\t\t\tcurrency: input.currency.toUpperCase(),\n\t\t\tprovider: this.id,\n\t\t\tcheckoutUrl: approveLink?.href,\n\t\t\tproviderPaymentId: res.id,\n\t\t\tmetadata: input.metadata,\n\t\t\tcreatedAt: res.create_time ? new Date(res.create_time) : new Date(),\n\t\t};\n\t}\n\n\t// ─── Refund ───────────────────────────────────────────────────────────\n\n\tasync createRefund(\n\t\tinput: CreateRefundInput,\n\t\topts?: PaymentOptions,\n\t): Promise<RefundResult> {\n\t\tconst params: PayPalRefundRequest = {};\n\t\tif (input.amount) {\n\t\t\tparams.amount = {\n\t\t\t\tvalue: input.amount.toFixed(2),\n\t\t\t\tcurrency_code: \"USD\",\n\t\t\t};\n\t\t}\n\t\tif (input.reason) {\n\t\t\tparams.note_to_payer = input.reason;\n\t\t}\n\n\t\tconst res = await this.request<PayPalRefundResponse>(\n\t\t\t\"POST\",\n\t\t\t`/v2/payments/captures/${input.paymentId}/refund`,\n\t\t\tparams,\n\t\t);\n\n\t\tconst refundAmount = res.amount?.value ? Number(res.amount.value) : 0;\n\n\t\treturn {\n\t\t\tid: res.id,\n\t\t\tstatus: normalizeRefundStatus(this.id, res.status),\n\t\t\tamount: refundAmount,\n\t\t\tcurrency: res.amount?.currency_code ?? \"USD\",\n\t\t\tprovider: this.id,\n\t\t\tpaymentId: input.paymentId,\n\t\t\treason: res.note_to_payer ?? input.reason,\n\t\t\tmetadata: input.metadata,\n\t\t\tcreatedAt: res.create_time ? new Date(res.create_time) : new Date(),\n\t\t};\n\t}\n\n\t// ─── Subscription ─────────────────────────────────────────────────────\n\n\tasync createSubscription(\n\t\tinput: CreateSubscriptionInput,\n\t\topts?: PaymentOptions,\n\t): Promise<SubscriptionResult> {\n\t\tconst params: PayPalSubscriptionRequest = {\n\t\t\tplan_id: input.planId,\n\t\t\tstart_time: new Date(Date.now() + 60000).toISOString(),\n\t\t};\n\n\t\tif (input.customerEmail) {\n\t\t\tparams.subscriber = {\n\t\t\t\temail_address: input.customerEmail,\n\t\t\t};\n\t\t}\n\n\t\tconst res = await this.request<PayPalSubscriptionResponse>(\n\t\t\t\"POST\",\n\t\t\t\"/v1/billing/subscriptions\",\n\t\t\tparams,\n\t\t);\n\n\t\treturn {\n\t\t\tid: res.id,\n\t\t\tstatus: res.status === \"ACTIVE\" ? \"active\" : res.status === \"CANCELLED\" ? \"cancelled\" : \"incomplete\",\n\t\t\tprovider: this.id,\n\t\t\tplanId: res.plan_id ?? input.planId,\n\t\t\tcustomerEmail: input.customerEmail,\n\t\t\tmetadata: input.metadata,\n\t\t\tcreatedAt: res.create_time ? new Date(res.create_time) : new Date(),\n\t\t};\n\t}\n\n\t// ─── Webhook Verification ─────────────────────────────────────────────\n\n\tasync verifyWebhookSignature(\n\t\tpayload: string,\n\t\tsignature: string,\n\t\tsecret: string,\n\t): Promise<boolean> {\n\t\t// PayPal webhook verification via API\n\t\t// In production, use PayPal's webhook verification API:\n\t\t// POST /v1/notifications/verify-webhook-signature\n\t\t// For now, basic check\n\t\ttry {\n\t\t\tJSON.parse(payload) as PayPalWebhookBody;\n\t\t\treturn signature.length > 0;\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\tparseWebhookEvent(payload: string): WebhookEvent {\n\t\tconst body = JSON.parse(payload) as PayPalWebhookBody;\n\n\t\tconst typeMap: Record<string, WebhookEvent[\"type\"]> = {\n\t\t\t\"PAYMENT.CAPTURE.COMPLETED\": \"payment.completed\",\n\t\t\t\"PAYMENT.CAPTURE.DECLINED\": \"payment.failed\",\n\t\t\t\"PAYMENT.CAPTURE.REFUNDED\": \"refund.completed\",\n\t\t\t\"PAYMENT.CAPTURE.PENDING\": \"payment.created\",\n\t\t\t\"CHECKOUT.ORDER.APPROVED\": \"payment.completed\",\n\t\t\t\"BILLING.SUBSCRIPTION.CREATED\": \"subscription.created\",\n\t\t\t\"BILLING.SUBSCRIPTION.CANCELLED\": \"subscription.cancelled\",\n\t\t\t\"BILLING.SUBSCRIPTION.SUSPENDED\": \"subscription.suspended\",\n\t\t\t\"BILLING.SUBSCRIPTION.PAYMENT.SUCCEEDED\": \"subscription.payment_succeeded\",\n\t\t\t\"BILLING.SUBSCRIPTION.PAYMENT.FAILED\": \"subscription.payment_failed\",\n\t\t\t\"PAYMENT.REFUND.FAILED\": \"refund.failed\",\n\t\t};\n\n\t\tconst normalizedType = typeMap[body.event_type] ?? \"payment.created\";\n\t\tconst amount = body.resource.amount?.total\n\t\t\t? Number(body.resource.amount.total)\n\t\t\t: undefined;\n\n\t\treturn {\n\t\t\tid: body.id,\n\t\t\ttype: normalizedType,\n\t\t\tprovider: this.id,\n\t\t\trawData: body,\n\t\t\tentityId: body.resource.id,\n\t\t\tamount,\n\t\t\tcurrency: body.resource.amount?.currency,\n\t\t\tstatus: body.resource.state,\n\t\t\tcreatedAt: body.create_time ? new Date(body.create_time) : new Date(),\n\t\t};\n\t}\n}\n","import { Cron } from \"croner\";\n\nexport interface SchedulerDriver {\n\tschedule(\n\t\tpattern: string,\n\t\thandler: () => void | Promise<void>,\n\t\toptions?: unknown,\n\t): unknown;\n\tstopAll(): void;\n}\n\nexport class MemorySchedulerDriver implements SchedulerDriver {\n\tprivate jobs: Cron[] = [];\n\n\tschedule(\n\t\tpattern: string,\n\t\thandler: () => void | Promise<void>,\n\t\toptions?: unknown,\n\t) {\n\t\t// biome-ignore lint/suspicious/noExplicitAny: Options are passed dynamically\n\t\tconst job = new Cron(pattern, options as any, async () => {\n\t\t\ttry {\n\t\t\t\tawait handler();\n\t\t\t} catch (err) {\n\t\t\t\tconsole.error(\"[Scheduler] Job failed:\", err);\n\t\t\t}\n\t\t});\n\t\tthis.jobs.push(job);\n\t\treturn job;\n\t}\n\n\tstopAll() {\n\t\tfor (const job of this.jobs) {\n\t\t\tjob.stop();\n\t\t}\n\t}\n}\n\n/**\n * Scheduler driver that uses Bun's native `Bun.cron()` API.\n *\n * Jobs are registered with the OS scheduler (crontab on Linux, launchd on\n * macOS, Task Scheduler on Windows) - they survive process restarts and\n * don't rely on an in-memory timer.\n *\n * Requires Bun >= 1.3.11.\n *\n * @example\n * import { Scheduler, BunCronSchedulerDriver } from \"@buntok/core\";\n *\n * const scheduler = new Scheduler(new BunCronSchedulerDriver());\n * scheduler.schedule(\"0 0 * * *\", () => cleanup());\n */\nexport class BunCronSchedulerDriver implements SchedulerDriver {\n\tprivate jobs: { stop(): void }[] = [];\n\n\tschedule(\n\t\tpattern: string,\n\t\thandler: () => void | Promise<void>,\n\t\t_options?: unknown,\n\t) {\n\t\tconst job = Bun.cron(pattern, async () => {\n\t\t\ttry {\n\t\t\t\tawait handler();\n\t\t\t} catch (err) {\n\t\t\t\tconsole.error(\"[Scheduler] Job failed:\", err);\n\t\t\t}\n\t\t});\n\t\tthis.jobs.push(job);\n\t\treturn job;\n\t}\n\n\tstopAll() {\n\t\tfor (const job of this.jobs) {\n\t\t\tjob.stop();\n\t\t}\n\t}\n}\n\n// Global default driver for the decorator\nexport let defaultSchedulerDriver: SchedulerDriver =\n\tnew MemorySchedulerDriver();\n\nexport function setDefaultSchedulerDriver(driver: SchedulerDriver) {\n\tdefaultSchedulerDriver = driver;\n}\n\nexport class Scheduler {\n\tconstructor(private driver: SchedulerDriver = new MemorySchedulerDriver()) {}\n\n\tschedule(\n\t\tpattern: string,\n\t\thandler: () => void | Promise<void>,\n\t\toptions?: unknown,\n\t) {\n\t\treturn this.driver.schedule(pattern, handler, options);\n\t}\n\n\tstopAll() {\n\t\tthis.driver.stopAll();\n\t}\n}\n\n/**\n * Decorator to schedule a controller/service method as a cron job.\n *\n * Uses `context.addInitializer` so the job is bound to the **class instance**\n * rather than being scheduled at class-definition time. This means:\n *   - `this` inside the method works correctly (can access injected services).\n *   - The job is only scheduled when the class is instantiated (e.g. via\n *     `app.registerController(new MyController())`).\n *\n * @param pattern - Standard cron expression (e.g. `\"0 0 * * *\"` for daily at midnight)\n *\n * @example\n * ```ts\n * @Controller(\"/tasks\")\n * export class TaskController {\n *   constructor(private readonly cache: Cache) {}\n *\n *   @CronJob(\"0 0 * * *\")\n *   async dailyCleanup() {\n *     // `this` is the TaskController instance - fully works!\n *     await this.cache.deletePattern(\"tmp:*\");\n *   }\n * }\n * ```\n */\nexport function CronJob(pattern: string, options?: unknown) {\n\treturn (\n\t\t// biome-ignore lint/suspicious/noExplicitAny: Required for Decorator inference\n\t\toriginalMethod: (...args: any[]) => any,\n\t\tcontext: ClassMethodDecoratorContext,\n\t) => {\n\t\tif (context.kind !== \"method\") {\n\t\t\tthrow new Error(\"@CronJob can only decorate methods\");\n\t\t}\n\n\t\t// addInitializer runs after the class instance is constructed,\n\t\t// giving us `this` bound to the actual instance.\n\t\t// biome-ignore lint/suspicious/noExplicitAny: Required for 'this' binding\n\t\tcontext.addInitializer(function (this: any) {\n\t\t\tdefaultSchedulerDriver.schedule(\n\t\t\t\tpattern,\n\t\t\t\tasync () => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tawait originalMethod.call(this);\n\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\tconsole.error(`[Cron:${String(context.name)}] Job failed:`, err);\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\toptions,\n\t\t\t);\n\t\t});\n\t};\n}\n","import { mkdir, stat, unlink } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport type { Middleware } from \"./app\";\nimport type { Context } from \"./context\";\nimport { BadRequestError } from \"./helpers/async-handler\";\n\nexport interface UploadedFile {\n\t/** Original name of the file */\n\toriginalName: string;\n\t/** Generated filename without extension (e.g., \"avatar-123\") */\n\tname: string;\n\t/** File extension with dot (e.g., \".png\") */\n\text: string;\n\t/** Size of the file in bytes */\n\tsize: number;\n\t/** MIME type of the file */\n\ttype: string;\n\t/** For MemoryStorage: the actual file data as ArrayBuffer */\n\tbuffer?: ArrayBuffer;\n\t/** For LocalDiskStorage: the absolute path where it was saved */\n\tpath?: string;\n}\n\n/**\n * Extended UploadedFile for image files processed with Bun.Image.\n * Only available when `outputFormat` is set in UploadFieldConfig.\n */\nexport interface ImageUploadedFile extends UploadedFile {\n\t/** Discriminant for runtime type narrowing */\n\tkind: \"image\";\n\t/** Image width in pixels */\n\twidth: number;\n\t/** Image height in pixels */\n\theight: number;\n\t/** Detected image format (e.g., \"png\", \"jpeg\", \"webp\") */\n\tformat: string;\n\t/** MIME type before conversion (only set when outputFormat differs from input) */\n\toriginalType?: string;\n\t/** File extension before conversion (only set when outputFormat differs from input) */\n\toriginalExt?: string;\n}\n\n/** Image MIME types that Bun.Image can process */\nconst IMAGE_MIME_TYPES = new Set([\n\t\"image/jpeg\",\n\t\"image/png\",\n\t\"image/gif\",\n\t\"image/webp\",\n\t\"image/avif\",\n\t\"image/bmp\",\n\t\"image/tiff\",\n]);\n\n/** Output format → { ext, type } mapping */\nconst OUTPUT_FORMAT_MAP: Record<string, { ext: string; type: string }> = {\n\twebp: { ext: \".webp\", type: \"image/webp\" },\n\tpng: { ext: \".png\", type: \"image/png\" },\n\tjpeg: { ext: \".jpg\", type: \"image/jpeg\" },\n\tavif: { ext: \".avif\", type: \"image/avif\" },\n};\n\nfunction isImageMime(type: string): boolean {\n\treturn IMAGE_MIME_TYPES.has(type);\n}\n\nasync function getImageMetadata(\n\tfile: File,\n): Promise<{ width: number; height: number; format: string } | null> {\n\tif (!isImageMime(file.type)) return null;\n\ttry {\n\t\tconst image = new Bun.Image(file);\n\t\tconst metadata = await image.metadata();\n\t\treturn {\n\t\t\twidth: metadata.width,\n\t\t\theight: metadata.height,\n\t\t\tformat: metadata.format,\n\t\t};\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nasync function convertImage(\n\tfile: File,\n\toutputFormat: keyof typeof OUTPUT_FORMAT_MAP,\n): Promise<File> {\n\tconst image = new Bun.Image(file);\n\t// Bun.Image methods (.png(), .jpeg(), etc.) return Promise<Buffer> at runtime,\n\t// but @types/bun types them as returning Image. Cast to fix.\n\tlet buffer: Buffer;\n\n\tswitch (outputFormat) {\n\t\tcase \"png\":\n\t\t\tbuffer = (await image.png().toBuffer()) as Buffer;\n\t\t\tbreak;\n\t\tcase \"jpeg\":\n\t\t\tbuffer = (await image.jpeg().toBuffer()) as Buffer;\n\t\t\tbreak;\n\t\tcase \"webp\":\n\t\t\tbuffer = (await image.webp().toBuffer()) as Buffer;\n\t\t\tbreak;\n\t\tcase \"avif\":\n\t\t\tbuffer = (await image.avif().toBuffer()) as Buffer;\n\t\t\tbreak;\n\t\tdefault:\n\t\t\tbuffer = (await image.png().toBuffer()) as Buffer;\n\t}\n\n\tconst info = OUTPUT_FORMAT_MAP[outputFormat]!;\n\treturn new File([buffer], file.name.replace(/\\.[^.]+$/, info.ext), {\n\t\ttype: info.type,\n\t});\n}\n\nexport interface StorageDriver {\n\t/** Handle a single file from the FormData stream */\n\thandleFile(\n\t\tfile: File,\n\t\tname: string,\n\t\text: string,\n\t): Promise<UploadedFile> | UploadedFile;\n\t/** Delete a file by its path or filename. Returns true if deleted. */\n\tdeleteFile?(path: string): Promise<boolean>;\n}\n\n// Single source of truth - all supported MIME types and their magic bytes signatures.\n// The MimeType type is inferred from the keys.\nconst MAGIC_BYTES = {\n\t// Images\n\t\"image/jpeg\": [[0xff, 0xd8, 0xff]],\n\t\"image/png\": [[0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]],\n\t\"image/gif\": [[0x47, 0x49, 0x46, 0x38]], // GIF8\n\t\"image/webp\": [[0x52, 0x49, 0x46, 0x46]], // RIFF\n\t\"image/svg+xml\": [], // XML-based, no reliable magic bytes\n\t\"image/bmp\": [[0x42, 0x4d]], // BM\n\t\"image/tiff\": [\n\t\t[0x49, 0x49, 0x2a, 0x00], // Little-endian\n\t\t[0x4d, 0x4d, 0x00, 0x2a], // Big-endian\n\t],\n\t\"image/x-icon\": [\n\t\t[0x00, 0x00, 0x01, 0x00], // ICO\n\t\t[0x00, 0x00, 0x02, 0x00], // CUR\n\t],\n\t\"image/avif\": [[0x00, 0x00, 0x00]], // ftyp avif/mif1\n\t\"image/heic\": [[0x00, 0x00, 0x00]], // ftyp heic/mif1\n\t\"image/heif\": [[0x00, 0x00, 0x00]], // ftyp heif/mif1\n\n\t// Videos\n\t\"video/mp4\": [[0x00, 0x00, 0x00]], // ftyp\n\t\"video/webm\": [[0x1a, 0x45, 0xdf, 0xa3]], // EBML\n\t\"video/ogg\": [[0x4f, 0x67, 0x67, 0x53]], // OggS\n\t\"video/quicktime\": [[0x00, 0x00, 0x00]], // ftyp qt\n\t\"video/x-msvideo\": [[0x52, 0x49, 0x46, 0x46]], // RIFF\n\t\"video/x-matroska\": [[0x1a, 0x45, 0xdf, 0xa3]], // EBML\n\t\"video/mpeg\": [\n\t\t[0x00, 0x00, 0x01, 0xba],\n\t\t[0x00, 0x00, 0x01, 0xb3],\n\t],\n\n\t// Audio\n\t\"audio/mpeg\": [\n\t\t[0xff, 0xfb],\n\t\t[0xff, 0xf3],\n\t\t[0xff, 0xf2],\n\t],\n\t\"audio/mp3\": [\n\t\t[0xff, 0xfb],\n\t\t[0xff, 0xf3],\n\t\t[0xff, 0xf2],\n\t],\n\t\"audio/wav\": [[0x52, 0x49, 0x46, 0x46]], // RIFF\n\t\"audio/ogg\": [[0x4f, 0x67, 0x67, 0x53]], // OggS\n\t\"audio/webm\": [[0x1a, 0x45, 0xdf, 0xa3]], // EBML\n\t\"audio/aac\": [\n\t\t[0xff, 0xf1],\n\t\t[0xff, 0xf9],\n\t], // ADTS\n\t\"audio/flac\": [[0x66, 0x4c, 0x61, 0x43]], // fLaC\n\t\"audio/mp4\": [[0x00, 0x00, 0x00]], // ftyp\n\t\"audio/x-m4a\": [[0x00, 0x00, 0x00]], // ftyp M4A\n\n\t// Documents\n\t\"application/pdf\": [[0x25, 0x50, 0x44, 0x46]], // %PDF\n\t\"application/rtf\": [[0x7b, 0x5c, 0x72, 0x74, 0x66]], // {\\rtf\n\t\"text/plain\": [],\n\t\"text/csv\": [],\n\t\"text/html\": [],\n\t\"text/css\": [],\n\t\"text/javascript\": [],\n\t\"application/javascript\": [],\n\t\"application/json\": [],\n\t\"application/xml\": [],\n\t\"text/xml\": [],\n\n\t// Microsoft Office (OLE2)\n\t\"application/msword\": [[0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1]],\n\t\"application/vnd.ms-excel\": [\n\t\t[0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1],\n\t],\n\t\"application/vnd.ms-powerpoint\": [\n\t\t[0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1],\n\t],\n\n\t// Microsoft Office (OOXML - ZIP based)\n\t\"application/vnd.openxmlformats-officedocument.wordprocessingml.document\": [\n\t\t[0x50, 0x4b, 0x03, 0x04],\n\t],\n\t\"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\": [\n\t\t[0x50, 0x4b, 0x03, 0x04],\n\t],\n\t\"application/vnd.openxmlformats-officedocument.presentationml.presentation\": [\n\t\t[0x50, 0x4b, 0x03, 0x04],\n\t],\n\n\t// OpenDocument (ZIP based)\n\t\"application/vnd.oasis.opendocument.text\": [[0x50, 0x4b, 0x03, 0x04]],\n\t\"application/vnd.oasis.opendocument.spreadsheet\": [[0x50, 0x4b, 0x03, 0x04]],\n\t\"application/vnd.oasis.opendocument.presentation\": [[0x50, 0x4b, 0x03, 0x04]],\n\n\t// Archives\n\t\"application/zip\": [[0x50, 0x4b, 0x03, 0x04]],\n\t\"application/x-7z-compressed\": [[0x37, 0x7a, 0xaf, 0x1d]],\n\t\"application/x-rar-compressed\": [[0x52, 0x61, 0x72, 0x21, 0x1a, 0x07]],\n\t\"application/gzip\": [[0x1f, 0x8b]],\n\t\"application/x-tar\": [], // Check at offset 257: \"ustar\"\n\t\"application/x-bzip2\": [[0x42, 0x5a, 0x68]], // BZh\n\n\t// Fonts\n\t\"font/woff\": [[0x77, 0x4f, 0x46, 0x46]], // wOFF\n\t\"font/woff2\": [[0x77, 0x4f, 0x46, 0x32]], // wOF2\n\t\"font/ttf\": [\n\t\t[0x00, 0x01, 0x00, 0x00], // TrueType\n\t\t[0x4f, 0x54, 0x54, 0x4f], // OTTO (OpenType)\n\t],\n\t\"font/otf\": [[0x4f, 0x54, 0x54, 0x4f]], // OTTO\n\n\t// Other\n\t\"application/octet-stream\": [],\n} as const;\n\n/** Supported MIME types - inferred from the MAGIC_BYTES keys. */\nexport type MimeType = keyof typeof MAGIC_BYTES;\n\nexport interface UploadFieldConfig {\n\t/** Whether this file field is required. Defaults to false. */\n\trequired?: boolean;\n\t/** Max file size in bytes for this field */\n\tmaxFileSize?: number;\n\t/** Allowed MIME types for this field */\n\tallowedMimeTypes?: MimeType[];\n\t/** Custom filename generator for this field (overrides global filename) */\n\tfilename?: (\n\t\toriginalName: string,\n\t\tfile: File,\n\t) => { name: string; ext: string };\n\t/**\n\t * Force image conversion to this format on upload.\n\t * When set, the returned file type narrows to `ImageUploadedFile`\n\t * with `width`, `height`, `format`, and optional `originalType`/`originalExt`.\n\t *\n\t * Only applies to image MIME types (image/jpeg, image/png, image/gif, etc.).\n\t * Non-image files ignore this option.\n\t */\n\toutputFormat?: \"webp\" | \"png\" | \"jpeg\" | \"avif\";\n}\n\nexport interface UploadOptions {\n\t/** Destination storage driver (e.g., LocalDiskStorage or MemoryStorage) */\n\tstorage: StorageDriver;\n\t/** Global default filename generator (overridden by per-field filename) */\n\tfilename?: (\n\t\toriginalName: string,\n\t\tfile: File,\n\t) => { name: string; ext: string };\n\t/**\n\t * Global max file size in bytes.\n\t * Applied to all fields unless overridden by per-field `maxFileSize`.\n\t */\n\tmaxFileSize?: number;\n\t/**\n\t * Global allowed MIME types.\n\t * Applied to all fields unless overridden by per-field `allowedMimeTypes`.\n\t */\n\tallowedMimeTypes?: MimeType[];\n\t/**\n\t * Whitelist of accepted file field names.\n\t * Files with a field name not listed here will be silently ignored.\n\t * Each field has its own maxFileSize, allowedMimeTypes, and filename config\n\t * that override the global defaults when set.\n\t *\n\t * @example\n\t * fields: {\n\t *   avatar: { required: true, maxFileSize: 2 * 1024 * 1024, allowedMimeTypes: ['image/png'] },\n\t *   document: { required: true, maxFileSize: 10 * 1024 * 1024 },\n\t * }\n\t */\n\tfields?: Record<string, UploadFieldConfig>;\n\t/** Verify file magic bytes against expected MIME type. Defaults to true. */\n\tverifyMagicBytes?: boolean;\n}\n\nexport type UploadOptionsWithFields<\n\tF extends Record<string, UploadFieldConfig>,\n> = Omit<UploadOptions, \"fields\"> & { fields?: F };\n\nexport interface ParseUploadResult<\n\tF extends Record<string, UploadFieldConfig> = Record<\n\t\tstring,\n\t\tUploadFieldConfig\n\t>,\n> {\n\t/**\n\t * All form fields - text fields are `string`, file fields are typed based on config.\n\t *\n\t * When `outputFormat` is set, the field narrows to `ImageUploadedFile`\n\t * with `width`, `height`, `format`, and optional `originalType`/`originalExt`.\n\t *\n\t * @example\n\t * result.fields.name      // string (text field)\n\t * result.fields.avatar    // ImageUploadedFile (outputFormat: \"webp\")\n\t * result.fields.document  // UploadedFile (no outputFormat)\n\t */\n\tfields: {\n\t\t[K in keyof F]: F[K] extends { outputFormat: any }\n\t\t\t? F[K] extends { required: true }\n\t\t\t\t? ImageUploadedFile\n\t\t\t\t: ImageUploadedFile | undefined\n\t\t\t: F[K] extends { required: true }\n\t\t\t\t? UploadedFile\n\t\t\t\t: F[K] extends { maxFileSize: any } | { allowedMimeTypes: any }\n\t\t\t\t\t? UploadedFile | undefined\n\t\t\t\t\t: UploadedFile | UploadedFile[] | undefined;\n\t} & Record<string, string>;\n\t/** Array of files that were successfully uploaded/processed (all files, flattened) */\n\tfiles: (UploadedFile | ImageUploadedFile)[];\n}\n\nexport class LocalDiskStorage implements StorageDriver {\n\tconstructor(private destination: string) {}\n\n\tasync handleFile(\n\t\tfile: File,\n\t\tname: string,\n\t\text: string,\n\t): Promise<UploadedFile> {\n\t\t// Ensure directory exists\n\t\ttry {\n\t\t\tawait stat(this.destination);\n\t\t} catch {\n\t\t\tawait mkdir(this.destination, { recursive: true });\n\t\t}\n\n\t\tconst filename = `${name}${ext}`;\n\t\tconst filePath = join(this.destination, filename);\n\t\tawait Bun.write(filePath, file);\n\n\t\treturn {\n\t\t\toriginalName: file.name,\n\t\t\tname,\n\t\t\text,\n\t\t\tsize: file.size,\n\t\t\ttype: file.type,\n\t\t\tpath: filePath,\n\t\t};\n\t}\n\n\tasync deleteFile(filePath: string): Promise<boolean> {\n\t\ttry {\n\t\t\tawait unlink(filePath);\n\t\t\treturn true;\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t}\n}\n\nexport class MemoryStorage implements StorageDriver {\n\tasync handleFile(\n\t\tfile: File,\n\t\tname: string,\n\t\text: string,\n\t): Promise<UploadedFile> {\n\t\tconst buffer = await file.arrayBuffer();\n\t\treturn {\n\t\t\toriginalName: file.name,\n\t\t\tname,\n\t\t\text,\n\t\t\tsize: file.size,\n\t\t\ttype: file.type,\n\t\t\tbuffer,\n\t\t};\n\t}\n\n\tasync deleteFile(_filePath: string): Promise<boolean> {\n\t\t// MemoryStorage holds files in memory - nothing to delete on disk.\n\t\treturn true;\n\t}\n}\n\nfunction getFileExtension(originalName: string): string {\n\tconst extIndex = originalName.lastIndexOf(\".\");\n\tif (extIndex !== -1) {\n\t\treturn originalName.substring(extIndex);\n\t}\n\treturn \"\";\n}\n\nconst defaultFilenameGenerator = (originalName: string) => {\n\tconst ext = getFileExtension(originalName);\n\tconst name = ext\n\t\t? originalName.substring(0, originalName.length - ext.length)\n\t\t: originalName;\n\treturn { name: `${name}-${crypto.randomUUID()}`, ext };\n};\n\nfunction resolveFilename(\n\toriginalName: string,\n\tfile: File,\n\tfieldFn?: (originalName: string, file: File) => { name: string; ext: string },\n\tglobalFn?: (\n\t\toriginalName: string,\n\t\tfile: File,\n\t) => { name: string; ext: string },\n): { name: string; ext: string } {\n\tconst ext = getFileExtension(originalName);\n\tconst generateFn = fieldFn ?? globalFn ?? defaultFilenameGenerator;\n\tconst result = generateFn(originalName, file);\n\n\t// Ensure ext always has the dot prefix\n\tconst finalExt = result.ext.startsWith(\".\") ? result.ext : ext;\n\n\treturn { name: result.name, ext: finalExt };\n}\n\nasync function verifyFileSignature(\n\tfile: File,\n\texpectedType: string,\n): Promise<boolean> {\n\tconst signatures = MAGIC_BYTES[expectedType as keyof typeof MAGIC_BYTES];\n\n\t// No magic bytes defined for this type → skip verification\n\tif (!signatures || signatures.length === 0) {\n\t\treturn true;\n\t}\n\n\t// Read first 16 bytes (covers most signatures)\n\tconst buffer = await file.arrayBuffer();\n\tconst bytes = new Uint8Array(buffer.slice(0, 16));\n\n\t// Check if any of the signatures match\n\treturn signatures.some((sig) => sig.every((byte, i) => bytes[i] === byte));\n}\n\n/**\n * Parses `multipart/form-data` request, separating standard fields from files.\n * Streams files into the configured storage driver with validation.\n *\n * Throws `BadRequestError` if validation fails (size, MIME type, or required fields).\n *\n * @example\n * const result = await handleUploads(ctx, {\n *   storage: new LocalDiskStorage('./uploads'),\n *   maxFileSize: 2 * 1024 * 1024,\n *   allowedMimeTypes: ['image/png', 'image/jpeg'],\n *   fields: {\n *     avatar: { required: true },\n *     document: { required: true, maxFileSize: 10 * 1024 * 1024 },\n *   },\n * });\n * // result.fields.avatar  → UploadedFile (required = true)\n * // result.fields.document → UploadedFile (required = true)\n */\nexport async function handleUploads<\n\tF extends Record<string, UploadFieldConfig> = Record<\n\t\tstring,\n\t\tUploadFieldConfig\n\t>,\n>(\n\t// biome-ignore lint/suspicious/noExplicitAny: Context is generic\n\tctx: Context<any, any>,\n\toptions: UploadOptionsWithFields<F>,\n): Promise<ParseUploadResult<F>> {\n\tconst contentType = ctx.request.headers.get(\"content-type\") || \"\";\n\tif (!contentType.includes(\"multipart/form-data\")) {\n\t\tthrow new BadRequestError(\n\t\t\t\"Invalid content-type. Must be multipart/form-data\",\n\t\t);\n\t}\n\n\t// biome-ignore lint/suspicious/noExplicitAny: Undici types conflict with lib.dom FormData\n\tlet formData: any;\n\ttry {\n\t\tformData = await ctx.formData();\n\t} catch (_err) {\n\t\tthrow new BadRequestError(\"Failed to parse form data\");\n\t}\n\n\tconst textFields: Record<string, string> = {};\n\tconst files: (UploadedFile | ImageUploadedFile)[] = [];\n\tconst fileMap: Record<string, UploadedFile | ImageUploadedFile | (UploadedFile | ImageUploadedFile)[]> = {};\n\n\tconst fieldWhitelist = options.fields;\n\n\tfor (const [key, value] of formData.entries()) {\n\t\tif (value instanceof File) {\n\t\t\t// If a fields whitelist is defined, ignore unknown file fields\n\t\t\tif (fieldWhitelist && !(key in fieldWhitelist)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Per-field config\n\t\t\tconst fieldConfig = fieldWhitelist?.[key];\n\n\t\t\t// Validation: Size - per-field config overrides global\n\t\t\tconst maxFileSize = fieldConfig?.maxFileSize ?? options.maxFileSize;\n\t\t\tif (maxFileSize && value.size > maxFileSize) {\n\t\t\t\tthrow new BadRequestError(\n\t\t\t\t\t`File \"${value.name}\" in field \"${key}\" exceeds max size of ${maxFileSize} bytes`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t// Validation: MIME Type - per-field config overrides global\n\t\t\tconst allowedMimeTypes =\n\t\t\t\tfieldConfig?.allowedMimeTypes ?? options.allowedMimeTypes;\n\t\t\tif (allowedMimeTypes && !allowedMimeTypes.includes(value.type as MimeType)) {\n\t\t\t\tthrow new BadRequestError(\n\t\t\t\t\t`File type \"${value.type}\" is not allowed for field \"${key}\" (file: \"${value.name}\")`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t// Validation: Magic Bytes - verify file signature matches claimed MIME type\n\t\t\tif (options.verifyMagicBytes !== false) {\n\t\t\t\tconst isValid = await verifyFileSignature(value, value.type);\n\t\t\t\tif (!isValid) {\n\t\t\t\t\tthrow new BadRequestError(\n\t\t\t\t\t\t`File \"${value.name}\" has invalid magic bytes for type \"${value.type}\"`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Process file\n\t\t\tconst { name, ext } = resolveFilename(\n\t\t\t\tvalue.name,\n\t\t\t\tvalue,\n\t\t\t\tfieldConfig?.filename,\n\t\t\t\toptions.filename,\n\t\t\t);\n\n\t\t\t// Check if image conversion is requested\n\t\t\tconst outputFormat = fieldConfig?.outputFormat;\n\t\t\tlet fileToStore = value;\n\t\t\tlet finalExt = ext;\n\t\t\tlet finalType = value.type;\n\n\t\t\tif (outputFormat && isImageMime(value.type)) {\n\t\t\t\tconst info = OUTPUT_FORMAT_MAP[outputFormat];\n\t\t\t\tif (info) {\n\t\t\t\t\tfileToStore = await convertImage(value, outputFormat);\n\t\t\t\t\tfinalExt = info.ext;\n\t\t\t\t\tfinalType = info.type;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst uploaded = await options.storage.handleFile(fileToStore, name, finalExt);\n\n\t\t\t// Image metadata extraction + type narrowing\n\t\t\tlet result: UploadedFile | ImageUploadedFile;\n\t\t\tif (outputFormat && isImageMime(value.type)) {\n\t\t\t\tconst metadata = await getImageMetadata(fileToStore);\n\t\t\t\tconst imgResult: ImageUploadedFile = {\n\t\t\t\t\t...uploaded,\n\t\t\t\t\tkind: \"image\",\n\t\t\t\t\twidth: metadata?.width ?? 0,\n\t\t\t\t\theight: metadata?.height ?? 0,\n\t\t\t\t\tformat: OUTPUT_FORMAT_MAP[outputFormat]?.ext.slice(1) ?? outputFormat,\n\t\t\t\t\ttype: finalType,\n\t\t\t\t\text: finalExt,\n\t\t\t\t};\n\t\t\t\t// Track original type if conversion happened\n\t\t\t\tif (value.type !== finalType) {\n\t\t\t\t\timgResult.originalType = value.type;\n\t\t\t\t\timgResult.originalExt = getFileExtension(value.name);\n\t\t\t\t}\n\t\t\t\tresult = imgResult;\n\t\t\t} else {\n\t\t\t\tresult = uploaded;\n\t\t\t}\n\n\t\t\tfiles.push(result);\n\n\t\t\t// Build fileMap: if same key appears multiple times → array\n\t\t\tif (key in fileMap) {\n\t\t\t\tconst existing = fileMap[key];\n\t\t\t\tif (Array.isArray(existing)) {\n\t\t\t\t\texisting.push(result);\n\t\t\t\t} else {\n\t\t\t\t\tfileMap[key] = [existing as UploadedFile, result];\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tfileMap[key] = result;\n\t\t\t}\n\t\t} else {\n\t\t\ttextFields[key] = value.toString();\n\t\t}\n\t}\n\n\t// Validate required fields\n\tif (fieldWhitelist) {\n\t\tfor (const [fieldName, config] of Object.entries(fieldWhitelist)) {\n\t\t\tif (config.required && !(fieldName in fileMap)) {\n\t\t\t\tthrow new BadRequestError(\n\t\t\t\t\t`Required file field \"${fieldName}\" is missing`,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn {\n\t\tfields: { ...textFields, ...fileMap },\n\t\tfiles,\n\t} as ParseUploadResult<F>;\n}\n\n/**\n * Middleware that automatically parses multipart uploads and populates\n * `ctx.store.files` and `ctx.store.fields`.\n *\n * ⚠️ Throws `BadRequestError` on validation failure - ensure your route\n * handler uses `asyncHandler()` or has try/catch to propagate the error\n * to the global error handler.\n */\nexport function uploader(options: UploadOptions): Middleware {\n\treturn async (ctx, next) => {\n\t\tconst result = await handleUploads(ctx, options);\n\n\t\tctx.store.files = result.files;\n\t\tctx.store.fields = result.fields;\n\n\t\treturn next();\n\t};\n}\n\n/**\n * Delete an uploaded file from the storage driver.\n *\n * @param storage - The StorageDriver instance used for the upload\n * @param file - The UploadedFile object (needs `.path` for LocalDiskStorage)\n * @returns true if deleted, false if not found or error\n *\n * @example\n * const result = await handleUploads(ctx, options);\n * const file = result.fileMap.avatar as UploadedFile;\n * await deleteUploadedFile(options.storage, file);\n */\nexport async function deleteUploadedFile(\n\tstorage: StorageDriver,\n\tfile: UploadedFile,\n): Promise<boolean> {\n\tif (!storage.deleteFile) {\n\t\treturn false;\n\t}\n\tconst target = file.path ?? `${file.name}${file.ext}`;\n\treturn storage.deleteFile(target);\n}\n","import type { ServerWebSocket } from \"bun\";\nimport { z } from \"zod\";\nimport type { WSData, WSHandler } from \"./app\";\n\n// Extended WSData with heartbeat\ninterface WSDataWithHeartbeat<DI> extends WSData<DI> {\n\theartbeat?: {\n\t\tinterval: number;\n\t\ttimer: ReturnType<typeof setInterval> | null;\n\t\talive: boolean;\n\t};\n}\n\n// ── Pluggable PubSub (mirip StorageDriver) ──\nexport interface WSPubSub {\n\t/** Publish ke channel - memory default, Redis pluggable */\n\tpublish(channel: string, message: string | object): Promise<void> | void;\n\t/** Subscribe - return unsubscribe */\n\tsubscribe(channel: string, handler: (message: string | object) => void): () => void;\n}\n\nexport class MemoryWSPubSub implements WSPubSub {\n\tprivate channels = new Map<string, Set<(m: string | object) => void>>();\n\tpublish(channel: string, message: string | object): void {\n\t\tconst subs = this.channels.get(channel);\n\t\tif (!subs) return;\n\t\tfor (const cb of subs) {\n\t\t\ttry { cb(message); } catch {}\n\t\t}\n\t}\n\tsubscribe(channel: string, handler: (m: string | object) => void): () => void {\n\t\tlet set = this.channels.get(channel);\n\t\tif (!set) { set = new Set(); this.channels.set(channel, set); }\n\t\tset.add(handler);\n\t\treturn () => set!.delete(handler);\n\t}\n}\n\n// ── Rate limit store pluggable ──\nexport interface WSRateLimitStore {\n\tincr(key: string, windowMs: number): Promise<number> | number;\n}\n\nexport class MemoryWSRateLimitStore implements WSRateLimitStore {\n\tprivate map = new Map<string, { count: number; resetAt: number }>();\n\tincr(key: string, windowMs: number): number {\n\t\tconst now = Date.now();\n\t\tconst ent = this.map.get(key);\n\t\tif (!ent || now > ent.resetAt) {\n\t\t\tthis.map.set(key, { count: 1, resetAt: now + windowMs });\n\t\t\treturn 1;\n\t\t}\n\t\tent.count++;\n\t\treturn ent.count;\n\t}\n}\n\n/**\n * Validate a WebSocket message against a Zod schema.\n */\nexport function validateWSMessage<T>(\n\tschema: z.ZodSchema<T>,\n\tmessage: string | Buffer,\n): { success: true; data: T } | { success: false; errors: z.ZodError } {\n\tconst raw = typeof message === \"string\" ? message : message.toString();\n\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(raw);\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terrors: new z.ZodError([\n\t\t\t\t{\n\t\t\t\t\tcode: \"custom\",\n\t\t\t\t\tmessage: \"Invalid JSON\",\n\t\t\t\t\tpath: [],\n\t\t\t\t},\n\t\t\t]),\n\t\t};\n\t}\n\n\tconst result = schema.safeParse(parsed);\n\tif (result.success) {\n\t\treturn { success: true, data: result.data };\n\t}\n\treturn { success: false, errors: result.error };\n}\n\n/**\n * Room class - in-memory default, pluggable ke Redis via pubSub\n * Mirip LocalDiskStorage vs MemoryStorage pattern.\n *\n * @example Memory (default)\n * ```ts\n * const room = new Room(\"chat\");\n * app.ws(\"/chat\", { open: (ws)=> room.join(ws), message: (ws,msg)=> room.broadcast(msg, ws) })\n * ```\n * @example Redis (pluggable)\n * ```ts\n * class RedisWSPubSub implements WSPubSub { constructor(private redis: Redis){} publish(ch,msg){ redis.publish(ch, typeof msg===\"object\"?JSON.stringify(msg):msg as string)} subscribe(ch,cb){ redis.subscribe(ch,(m)=>cb(m)); return ()=>redis.unsubscribe(ch)} }\n * const room = new Room(\"chat\", { pubSub: new RedisWSPubSub(redis) })\n * ```\n */\nexport interface RoomOptions {\n\tpubSub?: WSPubSub;\n\tchannel?: string;\n}\nexport class Room<DI = Record<string, unknown>> {\n\tpublic readonly name: string;\n\tprivate members = new Set<ServerWebSocket<WSData<DI>>>();\n\tprivate pubSub?: WSPubSub;\n\tprivate channel: string;\n\tprivate unsubscribe?: () => void;\n\n\tconstructor(name: string, opts?: RoomOptions | WSPubSub) {\n\t\tthis.name = name;\n\t\tif (opts && typeof (opts as WSPubSub).publish === \"function\") {\n\t\t\tthis.pubSub = opts as WSPubSub;\n\t\t\tthis.channel = `buntok:ws:${name}`;\n\t\t} else {\n\t\t\tconst o = opts as RoomOptions | undefined;\n\t\t\tthis.pubSub = o?.pubSub;\n\t\t\tthis.channel = o?.channel ?? `buntok:ws:${name}`;\n\t\t}\n\t\tif (this.pubSub) {\n\t\t\tthis.unsubscribe = this.pubSub.subscribe(this.channel, (msg) => {\n\t\t\t\tconst data = typeof msg === \"object\" ? JSON.stringify(msg) : (msg as string);\n\t\t\t\tfor (const m of [...this.members]) {\n\t\t\t\t\tif (m.readyState === 1) {\n\t\t\t\t\t\ttry { m.send(data); } catch {}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t}\n\n\tjoin(ws: ServerWebSocket<WSData<DI>>): void {\n\t\tthis.members.add(ws);\n\t\t// Also Bun native subscribe untuk fan-out kernel (optional, not required jika pakai pubSub)\n\t\ttry { (ws as unknown as { subscribe?: (c:string)=>void }).subscribe?.(this.channel); } catch {}\n\t}\n\n\tleave(ws: ServerWebSocket<WSData<DI>>): void {\n\t\tthis.members.delete(ws);\n\t\ttry { (ws as unknown as { unsubscribe?: (c:string)=>void }).unsubscribe?.(this.channel); } catch {}\n\t}\n\n\tbroadcast(message: string | object, exclude?: ServerWebSocket<WSData<DI>>): void {\n\t\tif (this.pubSub) {\n\t\t\t// via pubSub (Redis) - akan di-fanout ke semua instance termasuk self via subscribe\n\t\t\tconst r = this.pubSub.publish(this.channel, message);\n\t\t\tif (r instanceof Promise) r.catch(() => this.localBroadcast(message, exclude));\n\t\t\treturn;\n\t\t}\n\t\tthis.localBroadcast(message, exclude);\n\t}\n\n\tprivate localBroadcast(message: string | object, exclude?: ServerWebSocket<WSData<DI>>): void {\n\t\tconst data = typeof message === \"object\" ? JSON.stringify(message) : message;\n\t\t// Backpressure check: jika bufferedAmount tinggi, skip slow client\n\t\tfor (const member of [...this.members]) {\n\t\t\tif (member !== exclude && member.readyState === 1) {\n\t\t\t\ttry {\n\t\t\t\t\tconst buffered = (member as unknown as { getBufferedAmount?: () => number }).getBufferedAmount?.() ?? 0;\n\t\t\t\t\tif (buffered > 1024 * 1024) continue; // drop jika >1MB buffered\n\t\t\t\t\tmember.send(data);\n\t\t\t\t} catch {}\n\t\t\t}\n\t\t}\n\t}\n\n\tsendAll(message: string | object): void {\n\t\tthis.broadcast(message);\n\t}\n\n\tgetMembers(): ServerWebSocket<WSData<DI>>[] {\n\t\treturn Array.from(this.members);\n\t}\n\n\tget size(): number {\n\t\treturn this.members.size;\n\t}\n\n\tget isEmpty(): boolean {\n\t\treturn this.members.size === 0;\n\t}\n\n\thas(ws: ServerWebSocket<WSData<DI>>): boolean {\n\t\treturn this.members.has(ws);\n\t}\n\n\tcloseAll(): void {\n\t\tif (this.unsubscribe) { try { this.unsubscribe(); } catch {} }\n\t\tfor (const member of [...this.members]) {\n\t\t\ttry { member.close(1000, \"Room closed\"); } catch {}\n\t\t}\n\t\tthis.members.clear();\n\t}\n\n\t/** Filtered broadcast mirip SSEBroadcaster.broadcastWhere */\n\tbroadcastWhere(predicate: (ws: ServerWebSocket<WSData<DI>>) => boolean, message: string | object): void {\n\t\tconst data = typeof message === \"object\" ? JSON.stringify(message) : message;\n\t\tfor (const member of [...this.members]) {\n\t\t\tif (member.readyState !== 1) continue;\n\t\t\tif (predicate(member)) {\n\t\t\t\ttry { member.send(data); } catch {}\n\t\t\t}\n\t\t}\n\t}\n}\n\n/**\n * WebSocket heartbeat - 30s fixed, Bun-only\n * Fix: pong event reset alive (bukan message/drain)\n */\nexport function wsHeartbeat<DI = Record<string, unknown>>(\n\tinterval = 30_000,\n): WSHandler<DI> {\n\treturn {\n\t\topen: (ws: ServerWebSocket<WSData<DI>>) => {\n\t\t\tconst wsData = ws.data as WSDataWithHeartbeat<DI>;\n\t\t\twsData.heartbeat = {\n\t\t\t\tinterval,\n\t\t\t\ttimer: null as ReturnType<typeof setInterval> | null,\n\t\t\t\talive: true,\n\t\t\t};\n\t\t\twsData.heartbeat.timer = setInterval(() => {\n\t\t\t\tconst hb = (ws.data as WSDataWithHeartbeat<DI>).heartbeat;\n\t\t\t\tif (!hb) return;\n\t\t\t\tif (!hb.alive) {\n\t\t\t\t\ttry { ws.close(4000, \"Heartbeat timeout\"); } catch {}\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\thb.alive = false;\n\t\t\t\ttry { ws.ping(); } catch {}\n\t\t\t}, interval);\n\t\t},\n\t\t// `pong` handler khusus - akan di-wire di app.ts websocket.pong\n\t\t// fallback untuk Bun versi lama: message juga reset (compat)\n\t\tmessage: (ws: ServerWebSocket<WSData<DI>>) => {\n\t\t\tconst wsData = ws.data as WSDataWithHeartbeat<DI>;\n\t\t\tif (wsData.heartbeat) wsData.heartbeat.alive = true;\n\t\t},\n\t\tclose: (ws: ServerWebSocket<WSData<DI>>) => {\n\t\t\tconst wsData = ws.data as WSDataWithHeartbeat<DI>;\n\t\t\tif (wsData.heartbeat?.timer) clearInterval(wsData.heartbeat.timer);\n\t\t},\n\t\t// drain tidak reset alive lagi (fix drain bug)\n\t} as unknown as WSHandler<DI>;\n}\n\n/** Internal helper untuk app.ts - reset pong */\nexport function wsHeartbeatPong<DI>(ws: ServerWebSocket<WSData<DI>>): void {\n\tconst wsData = ws.data as unknown as WSDataWithHeartbeat<DI>;\n\tif (wsData.heartbeat) wsData.heartbeat.alive = true;\n}\n\n/**\n * WebSocket rate limiter - in-memory default, pluggable ke Redis\n * @example\n * const limiter = wsRateLimit({ windowMs: 1000, max: 10 })\n * app.ws(\"/chat\", { ...limiter, message: (ws,msg)=>{} })\n */\nexport interface WSRateLimitOptions<DI = Record<string, unknown>> {\n\twindowMs?: number;\n\tmax?: number;\n\tstore?: WSRateLimitStore;\n\tkeyGenerator?: (ws: ServerWebSocket<WSData<DI>>) => string;\n\t/** Close code saat limit */\n\tcloseCode?: number;\n}\nexport function wsRateLimit<DI = Record<string, unknown>>(opts: WSRateLimitOptions<DI> = {}): WSHandler<DI> {\n\tconst windowMs = opts.windowMs ?? 1000;\n\tconst max = opts.max ?? 20;\n\tconst store = opts.store ?? new MemoryWSRateLimitStore();\n\tconst keyGen = opts.keyGenerator ?? ((ws: ServerWebSocket<WSData<DI>>) => (ws.data.ctx.ip || \"unknown\"));\n\tconst closeCode = opts.closeCode ?? 1013;\n\treturn {\n\t\tmessage: (ws: ServerWebSocket<WSData<DI>>) => {\n\t\t\tconst key = keyGen(ws);\n\t\t\tconst count = store.incr(key, windowMs);\n\t\t\tconst c = count instanceof Promise ? 0 : count;\n\t\t\t// async case handled via promise\n\t\t\tif (count instanceof Promise) {\n\t\t\t\tcount.then((n) => { if (n > max) try { ws.close(closeCode, \"Rate limited\"); } catch {} });\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (c > max) {\n\t\t\t\ttry { ws.close(closeCode, \"Rate limited\"); } catch {}\n\t\t\t}\n\t\t},\n\t} as WSHandler<DI>;\n}\n\n/**\n * WebSocket authentication middleware - pluggable\n */\nexport function wsAuth<DI extends Record<string, unknown>, TAuth = unknown>(\n\tauthenticate: (ws: ServerWebSocket<WSData<DI>>) => Promise<TAuth | null>,\n): WSHandler<DI> {\n\treturn {\n\t\topen: async (ws) => {\n\t\t\ttry {\n\t\t\t\tconst authData = await authenticate(ws);\n\t\t\t\tif (authData === null) {\n\t\t\t\t\tws.close(4001, \"Unauthorized\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\t(ws.data as unknown as Record<string, unknown>).auth = authData;\n\t\t\t} catch {\n\t\t\t\tws.close(4001, \"Unauthorized\");\n\t\t\t}\n\t\t},\n\t} as WSHandler<DI>;\n}\n"],"mappings":"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","names":["import_node_fs","import_node_path","import_node_fs","import_node_path","fs","import_node_fs","import_node_fs","import_node_path","import_node_child_process","__filename","__dirname","resolve","import_node_fs","import_node_path","import_node_url","document","import_node_fs","import_node_path","import_node_url","import_zod","import_node_path","import_zod","import_node_fs","import_node_path","writeFileSync","import_zod","import_zod","import_promises","import_node_path","import_zod","__dirname","printUsage"]}