bunigniter 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +229 -0
  3. package/dist/LICENSE +21 -0
  4. package/dist/README.md +229 -0
  5. package/dist/base/controller.ts +324 -0
  6. package/dist/base/index.ts +5 -0
  7. package/dist/base/service.ts +21 -0
  8. package/dist/cli/index.ts +318 -0
  9. package/dist/cli/list-routes.ts +72 -0
  10. package/dist/cli/repl.ts +461 -0
  11. package/dist/cli/templates.ts +283 -0
  12. package/dist/client/index.ts +159 -0
  13. package/dist/db/drizzle.ts +550 -0
  14. package/dist/db/validators.ts +229 -0
  15. package/dist/edge-builder.ts +120 -0
  16. package/dist/edge.ts +69 -0
  17. package/dist/helpers/cache.ts +173 -0
  18. package/dist/helpers/cors.ts +103 -0
  19. package/dist/helpers/csrf.ts +155 -0
  20. package/dist/helpers/debug.ts +158 -0
  21. package/dist/helpers/env.ts +147 -0
  22. package/dist/helpers/handler.ts +158 -0
  23. package/dist/helpers/http.ts +194 -0
  24. package/dist/helpers/image.ts +217 -0
  25. package/dist/helpers/jwt.ts +147 -0
  26. package/dist/helpers/logger.ts +96 -0
  27. package/dist/helpers/mail.ts +272 -0
  28. package/dist/helpers/middleware-loader.ts +116 -0
  29. package/dist/helpers/middleware.ts +57 -0
  30. package/dist/helpers/modules.ts +115 -0
  31. package/dist/helpers/openapi.ts +140 -0
  32. package/dist/helpers/pagination.ts +159 -0
  33. package/dist/helpers/queue.ts +186 -0
  34. package/dist/helpers/request-context.ts +13 -0
  35. package/dist/helpers/request.ts +376 -0
  36. package/dist/helpers/schedule.ts +173 -0
  37. package/dist/helpers/session-middleware.ts +89 -0
  38. package/dist/helpers/session.ts +286 -0
  39. package/dist/helpers/sse.ts +90 -0
  40. package/dist/helpers/throttle.ts +156 -0
  41. package/dist/helpers/upload.ts +417 -0
  42. package/dist/helpers/validator.ts +287 -0
  43. package/dist/helpers/ws.ts +123 -0
  44. package/dist/index.ts +221 -0
  45. package/dist/package.json +70 -0
  46. package/dist/router/file-router.ts +541 -0
  47. package/dist/router/server-router.ts +103 -0
  48. package/dist/view/page.ts +96 -0
  49. package/dist/view/renderer.tsx +390 -0
  50. package/dist/view/view-response.ts +10 -0
  51. package/package.json +70 -0
@@ -0,0 +1,229 @@
1
+ /**
2
+ * Schema-Derived Validators — Void-style createInsertSchema / createSelectSchema / createUpdateSchema.
3
+ *
4
+ * Generates Zod schemas from Drizzle table definitions for use with
5
+ * `defineHandler.withValidator()`.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * // db/schema.ts
10
+ * import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'
11
+ * import { createInsertSchema } from 'nexusts/db'
12
+ *
13
+ * export const users = sqliteTable('users', {
14
+ * id: integer('id').primaryKey({ autoIncrement: true }),
15
+ * name: text('name').notNull(),
16
+ * email: text('email').notNull().unique(),
17
+ * role: text('role').notNull().default('user'),
18
+ * })
19
+ *
20
+ * export const insertUserSchema = createInsertSchema(users, {
21
+ * name: (schema) => schema.min(2),
22
+ * email: (schema) => schema.email(),
23
+ * })
24
+ * ```
25
+ *
26
+ * Then in a route handler:
27
+ * ```ts
28
+ * import { defineHandler } from 'nexusts'
29
+ * import { insertUserSchema } from '@schema'
30
+ *
31
+ * export const POST = defineHandler.withValidator({
32
+ * body: insertUserSchema,
33
+ * })(async (c, { body }) => {
34
+ * return db.insert(users).values(body).returning()
35
+ * })
36
+ * ```
37
+ */
38
+ import { z } from 'zod'
39
+
40
+ /**
41
+ * Column type info extracted from a Drizzle table column definition.
42
+ */
43
+ interface ColumnInfo {
44
+ name: string
45
+ type: string
46
+ notNull: boolean
47
+ hasDefault: boolean
48
+ isPrimaryKey: boolean
49
+ isAutoIncrement: boolean
50
+ }
51
+
52
+ /**
53
+ * Infer column info from a Drizzle table's `_` internal structure.
54
+ * This works with drizzle-orm >= 0.45.
55
+ */
56
+ function getColumns(table: any): ColumnInfo[] {
57
+ const cols: ColumnInfo[] = []
58
+ // Drizzle stores columns in table[Symbol.for('drizzle:columns')]
59
+ const drizzleCols = table?.[Symbol.for('drizzle:columns')] ?? table?.['_'] ?? {}
60
+
61
+ for (const [name, col] of Object.entries<any>(drizzleCols)) {
62
+ cols.push({
63
+ name,
64
+ type: col?.type ?? 'text',
65
+ notNull: col?.notNull ?? false,
66
+ hasDefault: col?.hasDefault ?? false,
67
+ isPrimaryKey: col?.primaryKey ?? false,
68
+ isAutoIncrement: col?.autoIncrement ?? false,
69
+ })
70
+ }
71
+
72
+ // Fallback: try to get columns from the table's structure
73
+ if (cols.length === 0) {
74
+ for (const key of Object.keys(table)) {
75
+ if (key.startsWith('_') || key === 'name' || key === 'Symbol') continue
76
+ const col = table[key]
77
+ if (col && typeof col === 'object' && col.name) {
78
+ cols.push({
79
+ name: col.name,
80
+ type: col.type ?? 'text',
81
+ notNull: col.notNull ?? false,
82
+ hasDefault: col.hasDefault ?? false,
83
+ isPrimaryKey: col.primaryKey ?? false,
84
+ isAutoIncrement: col.autoIncrement ?? false,
85
+ })
86
+ }
87
+ }
88
+ }
89
+
90
+ return cols
91
+ }
92
+
93
+ /**
94
+ * Map Drizzle column types to Zod schemas.
95
+ */
96
+ function columnToZod(col: ColumnInfo): z.ZodTypeAny {
97
+ let schema: z.ZodTypeAny
98
+
99
+ switch (col.type) {
100
+ case 'number':
101
+ case 'integer':
102
+ case 'serial':
103
+ schema = z.number()
104
+ break
105
+ case 'boolean':
106
+ schema = z.boolean()
107
+ break
108
+ case 'json':
109
+ case 'jsonb':
110
+ schema = z.record(z.any())
111
+ break
112
+ case 'text':
113
+ case 'varchar':
114
+ case 'char':
115
+ case 'string':
116
+ default:
117
+ schema = z.string()
118
+ break
119
+ }
120
+
121
+ // Apply nullability
122
+ if (!col.notNull) {
123
+ schema = schema.nullable().optional()
124
+ }
125
+ if (col.isAutoIncrement) {
126
+ schema = schema.optional()
127
+ }
128
+
129
+ return schema
130
+ }
131
+
132
+ /**
133
+ * Create a Zod schema for INSERT operations.
134
+ * Auto-generated columns (id, timestamps with defaults) become optional.
135
+ *
136
+ * @param table - Drizzle table definition
137
+ * @param refinements - Optional field-level refinements (e.g. `.min(2)`, `.email()`)
138
+ * @returns Zod object schema
139
+ *
140
+ * @example
141
+ * ```ts
142
+ * export const insertUserSchema = createInsertSchema(users, {
143
+ * name: (schema) => schema.min(2),
144
+ * email: (schema) => schema.email(),
145
+ * })
146
+ * ```
147
+ */
148
+ export function createInsertSchema<T extends Record<string, any>>(
149
+ table: any,
150
+ refinements?: Partial<{
151
+ [K in keyof T]: (schema: z.ZodTypeAny) => z.ZodTypeAny
152
+ }>
153
+ ): z.ZodObject<any> {
154
+ const columns = getColumns(table)
155
+ const shape: Record<string, z.ZodTypeAny> = {}
156
+
157
+ for (const col of columns) {
158
+ // Skip auto-increment primary keys (DB generates them)
159
+ if (col.isAutoIncrement) continue
160
+
161
+ let schema = columnToZod(col)
162
+
163
+ // Apply refinements
164
+ if (refinements && refinements[col.name as keyof typeof refinements]) {
165
+ const refine = refinements[col.name as keyof typeof refinements]!
166
+ schema = refine(schema)
167
+ }
168
+
169
+ shape[col.name] = schema
170
+ }
171
+
172
+ return z.object(shape)
173
+ }
174
+
175
+ /**
176
+ * Create a Zod schema for SELECT operations.
177
+ * All columns are optional (you might not select all).
178
+ *
179
+ * @param table - Drizzle table definition
180
+ * @returns Zod object schema
181
+ *
182
+ * @example
183
+ * ```ts
184
+ * export const selectUserSchema = createSelectSchema(users)
185
+ * ```
186
+ */
187
+ export function createSelectSchema(table: any): z.ZodObject<any> {
188
+ const columns = getColumns(table)
189
+ const shape: Record<string, z.ZodTypeAny> = {}
190
+
191
+ for (const col of columns) {
192
+ shape[col.name] = columnToZod(col).optional()
193
+ }
194
+
195
+ return z.object(shape)
196
+ }
197
+
198
+ /**
199
+ * Create a Zod schema for UPDATE operations.
200
+ * All fields are optional (partial update).
201
+ *
202
+ * @param table - Drizzle table definition
203
+ * @param refinements - Optional field-level refinements
204
+ * @returns Zod object schema
205
+ *
206
+ * @example
207
+ * ```ts
208
+ * export const updateUserSchema = createUpdateSchema(users, {
209
+ * email: (schema) => schema.email(),
210
+ * })
211
+ * ```
212
+ */
213
+ export function createUpdateSchema<T extends Record<string, any>>(
214
+ table: any,
215
+ refinements?: Partial<{
216
+ [K in keyof T]: (schema: z.ZodTypeAny) => z.ZodTypeAny
217
+ }>
218
+ ): z.ZodObject<any> {
219
+ const insertSchema = createInsertSchema(table, refinements)
220
+ const shape = insertSchema.shape
221
+
222
+ // Make all fields optional
223
+ const optionalShape: Record<string, z.ZodTypeAny> = {}
224
+ for (const [key, schema] of Object.entries(shape)) {
225
+ optionalShape[key] = (schema as z.ZodTypeAny).optional()
226
+ }
227
+
228
+ return z.object(optionalShape)
229
+ }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Edge routes builder — generates pre-compiled route handlers for Edge runtimes.
3
+ *
4
+ * Usage: `bun run bi build:edge`
5
+ *
6
+ * This scans `pages/` and generates `edge-app.ts` — a static file with
7
+ * all routes pre-registered. Works on Cloudflare Workers, Deno, etc.
8
+ */
9
+ import { readdirSync, statSync, writeFileSync, existsSync, readFileSync } from 'node:fs'
10
+ import { join, basename } from 'node:path'
11
+
12
+ const CWD = process.cwd()
13
+
14
+ export async function buildEdgeRoutes(): Promise<void> {
15
+ // Scan routes/ for controllers (was pages/, now routes/)
16
+ const routesDir = join(CWD, 'routes')
17
+ if (!existsSync(routesDir)) {
18
+ console.error('[build] No routes/ directory found.')
19
+ return
20
+ }
21
+
22
+ const files = scanDir(routesDir)
23
+ const imports: string[] = []
24
+ const routes: string[] = []
25
+
26
+ for (const file of files) {
27
+ if (file.endsWith('.server.ts')) continue
28
+
29
+ const fullPath = join(routesDir, file)
30
+ const s = statSync(fullPath)
31
+ if (!s.isFile()) continue
32
+
33
+ // Read the file to find the exported class name
34
+ const content = readFileSync(fullPath, 'utf-8')
35
+ const classMatch = content.match(/export class (\w+) extends Controller/)
36
+ const name = classMatch ? classMatch[1] : 'UnknownController'
37
+ const importPath = `./routes/${file}`
38
+ const isIndex = basename(file, '.ts') === 'index'
39
+ const prefix = process.env.ROUTER_PREFIX ?? '/api'
40
+
41
+ // Build URL path
42
+ let urlPath
43
+ if (isIndex) {
44
+ urlPath = prefix
45
+ } else {
46
+ urlPath = file
47
+ .replace(/\.ts$/, '')
48
+ .replace(/\.\.\./g, '*')
49
+ .replace(/\[(\w+)\]/g, ':$1')
50
+ urlPath = `${prefix}/${urlPath}`
51
+ }
52
+
53
+ imports.push(`import { ${name} } from '${importPath}'`)
54
+
55
+ const methodMap: Record<string, string> = {
56
+ index: 'GET', show: 'GET', create: 'POST',
57
+ update: 'PUT', destroy: 'DELETE',
58
+ }
59
+ const idMethods = new Set(['show', 'update', 'destroy'])
60
+
61
+ // Only generate routes for methods that exist on the controller
62
+ for (const [method, verb] of Object.entries(methodMap)) {
63
+ if (!content.includes(`async ${method}`)) continue
64
+
65
+ const isIdMethod = idMethods.has(method)
66
+ const methodPath = isIdMethod ? `${urlPath}/:id` : urlPath
67
+ const httpMethod = verb.toLowerCase()
68
+
69
+ routes.push(` app.${httpMethod}('${methodPath}', async (ctx) => {
70
+ const ctrl = new ${name}()
71
+ ;(ctrl as any).ctx = ctx
72
+ const id = ctx.params?.id ? Number(ctx.params.id) : undefined
73
+ const result = await ctrl.${method}(${isIdMethod ? 'id' : ''})
74
+ if (result instanceof Response) return result
75
+ return new Response(JSON.stringify(result ?? {}), {
76
+ headers: { 'content-type': 'application/json' }
77
+ })
78
+ })`)
79
+ }
80
+ }
81
+
82
+ const output = `// Auto-generated edge routes — do not edit manually.
83
+ // Generated by: bun run bi build:edge
84
+
85
+ import { Elysia } from 'elysia'
86
+
87
+ ${imports.join('\n')}
88
+
89
+ const app = new Elysia()
90
+
91
+ // Health check
92
+ app.get('/health', () => new Response(JSON.stringify({
93
+ status: 'ok',
94
+ runtime: typeof Bun !== 'undefined' ? 'bun' : 'edge',
95
+ }), { headers: { 'content-type': 'application/json' }}))
96
+
97
+ // Routes
98
+ ${routes.join('\n\n')}
99
+
100
+ export default app
101
+ `
102
+
103
+ const outPath = join(CWD, 'edge-app.ts')
104
+ writeFileSync(outPath, output, 'utf-8')
105
+ console.log(`[build] Edge routes written to ${outPath}`)
106
+ console.log(`[build] ${files.length} controllers, ${routes.length} routes`)
107
+ }
108
+
109
+ function scanDir(dir: string, baseDir = ''): string[] {
110
+ const files: string[] = []
111
+ const entries = readdirSync(dir, { withFileTypes: true })
112
+ for (const entry of entries) {
113
+ if (entry.isDirectory()) {
114
+ files.push(...scanDir(join(dir, entry.name), baseDir ? `${baseDir}/${entry.name}` : entry.name))
115
+ } else if (entry.isFile() && entry.name.endsWith('.ts') && !entry.name.startsWith('_')) {
116
+ files.push(baseDir ? `${baseDir}/${entry.name}` : entry.name)
117
+ }
118
+ }
119
+ return files.sort()
120
+ }
package/dist/edge.ts ADDED
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Edge entry point — for Cloudflare Workers, Deno, and other Edge runtimes.
3
+ *
4
+ * Unlike the Bun entry point (`src/index.ts`), this version:
5
+ * - Does NOT use file-based routing (pages are pre-registered)
6
+ * - Does NOT use node:fs modules
7
+ * - Uses Elysia v2's web-standard adapter
8
+ * - Exposes `fetch` handler directly
9
+ *
10
+ * Usage (Cloudflare Workers):
11
+ * ```ts
12
+ * import app from './src/edge'
13
+ * export default { fetch: app.fetch }
14
+ * ```
15
+ *
16
+ * Usage (Deno):
17
+ * ```ts
18
+ * import app from './src/edge'
19
+ * Deno.serve(app.fetch)
20
+ * ```
21
+ */
22
+ import { Elysia } from 'elysia'
23
+ import { applyMiddleware } from './helpers/middleware'
24
+
25
+ /**
26
+ * Create an edge-compatible application.
27
+ * Routes must be registered manually or via a pre-built router.
28
+ */
29
+ export function createEdgeApp(config?: { middleware?: any }) {
30
+ const app = new Elysia()
31
+
32
+ // Apply middleware
33
+ applyMiddleware(app, config?.middleware)
34
+
35
+ // Health check
36
+ app.get('/health', () => new Response(JSON.stringify({
37
+ status: 'ok',
38
+ runtime: typeof Bun !== 'undefined' ? 'bun' :
39
+ typeof (globalThis as any).Deno !== 'undefined' ? 'deno' :
40
+ typeof (globalThis as any).navigator !== 'undefined' ? 'cloudflare' :
41
+ 'unknown',
42
+ timestamp: new Date().toISOString(),
43
+ }), {
44
+ headers: { 'content-type': 'application/json' },
45
+ }))
46
+
47
+ return app
48
+ }
49
+
50
+ /**
51
+ * Register a route directly (edge-compatible, no filesystem access).
52
+ *
53
+ * @example
54
+ * ```ts
55
+ * import { createEdgeApp, register } from './src/edge'
56
+ * const app = createEdgeApp()
57
+ * register(app, 'GET', '/api/hello', () => new Response('Hello Edge!'))
58
+ * export default { fetch: app.fetch }
59
+ * ```
60
+ */
61
+ export function register(
62
+ app: Elysia,
63
+ method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH',
64
+ path: string,
65
+ handler: (...args: any[]) => any
66
+ ): void {
67
+ const lower = method.toLowerCase() as 'get' | 'post' | 'put' | 'delete' | 'patch'
68
+ ;(app as any)[lower](path, handler)
69
+ }
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Cache — CodeIgniter-style key-value cache with TTL support.
3
+ *
4
+ * @example
5
+ * ```ts
6
+ * // In a controller
7
+ * const users = this.cache.get('users_list')
8
+ * if (!users) {
9
+ * const data = await this.db.query('SELECT * FROM users')
10
+ * this.cache.set('users_list', data, 300) // 5 min TTL
11
+ * }
12
+ * ```
13
+ */
14
+ export interface CacheOptions {
15
+ /** Default TTL in seconds. Default: 60 */
16
+ defaultTtl?: number
17
+
18
+ /** Max cache entries. Default: 1000 */
19
+ maxEntries?: number
20
+ }
21
+
22
+ interface CacheEntry {
23
+ data: any
24
+ expiresAt: number
25
+ }
26
+
27
+ /** In-memory cache store. */
28
+ const store = new Map<string, CacheEntry>()
29
+
30
+ /** Periodic cleanup. */
31
+ let cleanupTimer: Timer | null = null
32
+ function ensureCleanup(interval = 30000) {
33
+ if (cleanupTimer) return
34
+ cleanupTimer = setInterval(() => {
35
+ const now = Date.now()
36
+ for (const [key, entry] of store) {
37
+ if (entry.expiresAt <= now) store.delete(key)
38
+ }
39
+ }, interval)
40
+ }
41
+
42
+ /**
43
+ * Cache service — get/set/delete/remember with TTL.
44
+ *
45
+ * Usage in a Controller (injected by the framework):
46
+ * ```ts
47
+ * this.cache.get('key')
48
+ * this.cache.set('key', value, 300)
49
+ * ```
50
+ */
51
+ export class Cache {
52
+ private defaultTtl: number
53
+ private maxEntries: number
54
+
55
+ constructor(options: CacheOptions = {}) {
56
+ this.defaultTtl = options.defaultTtl ?? 60
57
+ this.maxEntries = options.maxEntries ?? 1000
58
+ ensureCleanup()
59
+ }
60
+
61
+ /**
62
+ * Get a cached value.
63
+ * Returns undefined if key doesn't exist or is expired.
64
+ */
65
+ get<T = any>(key: string): T | undefined {
66
+ const entry = store.get(key)
67
+ if (!entry) return undefined
68
+ if (entry.expiresAt <= Date.now()) {
69
+ store.delete(key)
70
+ return undefined
71
+ }
72
+ return entry.data as T
73
+ }
74
+
75
+ /**
76
+ * Set a cached value with optional TTL.
77
+ *
78
+ * @param key - Cache key
79
+ * @param value - Value to store
80
+ * @param ttl - Time to live in seconds. Default: config.defaultTtl
81
+ */
82
+ set(key: string, value: any, ttl?: number): void {
83
+ if (store.size >= this.maxEntries) {
84
+ // Evict oldest entry
85
+ const oldest = store.entries().next().value
86
+ if (oldest) store.delete(oldest[0])
87
+ }
88
+
89
+ store.set(key, {
90
+ data: value,
91
+ expiresAt: Date.now() + (ttl ?? this.defaultTtl) * 1000,
92
+ })
93
+ }
94
+
95
+ /**
96
+ * Delete a cached value.
97
+ */
98
+ delete(key: string): void {
99
+ store.delete(key)
100
+ }
101
+
102
+ /**
103
+ * Clear all cached values.
104
+ */
105
+ clear(): void {
106
+ store.clear()
107
+ }
108
+
109
+ /**
110
+ * Check if a key exists and is not expired.
111
+ */
112
+ has(key: string): boolean {
113
+ const entry = store.get(key)
114
+ if (!entry) return false
115
+ if (entry.expiresAt <= Date.now()) {
116
+ store.delete(key)
117
+ return false
118
+ }
119
+ return true
120
+ }
121
+
122
+ /**
123
+ * Remember — get or set via a callback.
124
+ *
125
+ * @example
126
+ * ```ts
127
+ * const users = await this.cache.remember('users', 300, async () => {
128
+ * return this.db.query('SELECT * FROM users')
129
+ * })
130
+ * ```
131
+ */
132
+ async remember<T>(key: string, ttl: number, callback: () => Promise<T>): Promise<T> {
133
+ const cached = this.get<T>(key)
134
+ if (cached !== undefined) return cached
135
+
136
+ const value = await callback()
137
+ this.set(key, value, ttl)
138
+ return value
139
+ }
140
+
141
+ /**
142
+ * Increment a numeric value.
143
+ */
144
+ increment(key: string, amount = 1): number {
145
+ const current = this.get<number>(key) ?? 0
146
+ const newValue = current + amount
147
+ this.set(key, newValue)
148
+ return newValue
149
+ }
150
+
151
+ /**
152
+ * Decrement a numeric value.
153
+ */
154
+ decrement(key: string, amount = 1): number {
155
+ return this.increment(key, -amount)
156
+ }
157
+
158
+ /** Number of cached entries. */
159
+ get size(): number {
160
+ return store.size
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Create a shared cache instance.
166
+ */
167
+ let _instance: Cache | null = null
168
+ export function createCache(options?: CacheOptions): Cache {
169
+ if (!_instance) {
170
+ _instance = new Cache(options)
171
+ }
172
+ return _instance
173
+ }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * CORS middleware — Cross-Origin Resource Sharing.
3
+ *
4
+ * @example
5
+ * ```ts
6
+ * // config/app.ts
7
+ * export default {
8
+ * middleware: {
9
+ * cors: {
10
+ * origin: ['https://myapp.com', 'http://localhost:5173'],
11
+ * methods: ['GET', 'POST', 'PUT', 'DELETE'],
12
+ * }
13
+ * }
14
+ * }
15
+ * ```
16
+ */
17
+ import { Elysia } from 'elysia'
18
+
19
+ export interface CORSOptions {
20
+ /** Allowed origins. Default: '*' */
21
+ origin?: string | string[] | ((origin: string) => boolean | string | undefined)
22
+
23
+ /** Allowed methods. Default: 'GET,POST,PUT,PATCH,DELETE,OPTIONS' */
24
+ methods?: string
25
+
26
+ /** Allowed headers. Default: 'Content-Type,Authorization,X-Inertia' */
27
+ allowedHeaders?: string
28
+
29
+ /** Expose headers. */
30
+ exposeHeaders?: string
31
+
32
+ /** Allow credentials (cookies). Default: true */
33
+ credentials?: boolean
34
+
35
+ /** Max age for preflight cache (seconds). Default: 86400 */
36
+ maxAge?: number
37
+ }
38
+
39
+ /**
40
+ * Create a CORS middleware plugin.
41
+ */
42
+ export function corsMiddleware(options: CORSOptions = {}) {
43
+ const {
44
+ origin = '*',
45
+ methods = 'GET,POST,PUT,PATCH,DELETE,OPTIONS',
46
+ allowedHeaders = 'Content-Type,Authorization,X-Inertia,X-Requested-With',
47
+ credentials = true,
48
+ maxAge = 86400,
49
+ exposeHeaders,
50
+ } = options
51
+
52
+ const app = new Elysia({ name: 'nexus-cors' })
53
+
54
+ app.derive(async (ctx: any) => {
55
+ const requestOrigin = ctx.request.headers.get('origin')
56
+
57
+ // Determine allowed origin
58
+ let allowOrigin = '*'
59
+ if (origin === '*') {
60
+ allowOrigin = requestOrigin ?? '*'
61
+ } else if (typeof origin === 'string') {
62
+ allowOrigin = origin
63
+ } else if (Array.isArray(origin)) {
64
+ if (requestOrigin && origin.includes(requestOrigin)) {
65
+ allowOrigin = requestOrigin
66
+ }
67
+ } else if (typeof origin === 'function') {
68
+ const result = origin(requestOrigin ?? '')
69
+ if (result) allowOrigin = typeof result === 'string' ? result : requestOrigin ?? '*'
70
+ }
71
+
72
+ // Handle preflight
73
+ if (ctx.request.method === 'OPTIONS') {
74
+ return new Response(null, {
75
+ status: 204,
76
+ headers: {
77
+ 'Access-Control-Allow-Origin': allowOrigin,
78
+ 'Access-Control-Allow-Methods': methods,
79
+ 'Access-Control-Allow-Headers': allowedHeaders,
80
+ 'Access-Control-Max-Age': String(maxAge),
81
+ ...(credentials ? { 'Access-Control-Allow-Credentials': 'true' } : {}),
82
+ },
83
+ })
84
+ }
85
+
86
+ // Store for later use
87
+ return { _corsOrigin: allowOrigin }
88
+ })
89
+
90
+ app.afterResponse((ctx: any) => {
91
+ const allowOrigin = ctx._corsOrigin ?? '*'
92
+ ctx.set.headers ??= {}
93
+ ctx.set.headers['Access-Control-Allow-Origin'] = allowOrigin
94
+ if (credentials && allowOrigin !== '*') {
95
+ ctx.set.headers['Access-Control-Allow-Credentials'] = 'true'
96
+ }
97
+ if (exposeHeaders) {
98
+ ctx.set.headers['Access-Control-Expose-Headers'] = exposeHeaders
99
+ }
100
+ })
101
+
102
+ return app
103
+ }