@stravigor/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/README.md +45 -0
  2. package/package.json +83 -0
  3. package/src/auth/access_token.ts +122 -0
  4. package/src/auth/auth.ts +86 -0
  5. package/src/auth/index.ts +7 -0
  6. package/src/auth/middleware/authenticate.ts +64 -0
  7. package/src/auth/middleware/csrf.ts +62 -0
  8. package/src/auth/middleware/guest.ts +46 -0
  9. package/src/broadcast/broadcast_manager.ts +411 -0
  10. package/src/broadcast/client.ts +302 -0
  11. package/src/broadcast/index.ts +58 -0
  12. package/src/cache/cache_manager.ts +56 -0
  13. package/src/cache/cache_store.ts +31 -0
  14. package/src/cache/helpers.ts +74 -0
  15. package/src/cache/http_cache.ts +109 -0
  16. package/src/cache/index.ts +6 -0
  17. package/src/cache/memory_store.ts +63 -0
  18. package/src/cli/bootstrap.ts +37 -0
  19. package/src/cli/commands/generate_api.ts +74 -0
  20. package/src/cli/commands/generate_key.ts +46 -0
  21. package/src/cli/commands/generate_models.ts +48 -0
  22. package/src/cli/commands/migration_compare.ts +152 -0
  23. package/src/cli/commands/migration_fresh.ts +123 -0
  24. package/src/cli/commands/migration_generate.ts +79 -0
  25. package/src/cli/commands/migration_rollback.ts +53 -0
  26. package/src/cli/commands/migration_run.ts +44 -0
  27. package/src/cli/commands/queue_flush.ts +35 -0
  28. package/src/cli/commands/queue_retry.ts +34 -0
  29. package/src/cli/commands/queue_work.ts +40 -0
  30. package/src/cli/commands/scheduler_work.ts +45 -0
  31. package/src/cli/strav.ts +33 -0
  32. package/src/config/configuration.ts +105 -0
  33. package/src/config/loaders/base_loader.ts +69 -0
  34. package/src/config/loaders/env_loader.ts +112 -0
  35. package/src/config/loaders/typescript_loader.ts +56 -0
  36. package/src/config/types.ts +8 -0
  37. package/src/core/application.ts +4 -0
  38. package/src/core/container.ts +117 -0
  39. package/src/core/index.ts +3 -0
  40. package/src/core/inject.ts +39 -0
  41. package/src/database/database.ts +54 -0
  42. package/src/database/index.ts +30 -0
  43. package/src/database/introspector.ts +446 -0
  44. package/src/database/migration/differ.ts +308 -0
  45. package/src/database/migration/file_generator.ts +125 -0
  46. package/src/database/migration/index.ts +18 -0
  47. package/src/database/migration/runner.ts +133 -0
  48. package/src/database/migration/sql_generator.ts +378 -0
  49. package/src/database/migration/tracker.ts +76 -0
  50. package/src/database/migration/types.ts +189 -0
  51. package/src/database/query_builder.ts +474 -0
  52. package/src/encryption/encryption_manager.ts +209 -0
  53. package/src/encryption/helpers.ts +158 -0
  54. package/src/encryption/index.ts +3 -0
  55. package/src/encryption/types.ts +6 -0
  56. package/src/events/emitter.ts +101 -0
  57. package/src/events/index.ts +2 -0
  58. package/src/exceptions/errors.ts +75 -0
  59. package/src/exceptions/exception_handler.ts +126 -0
  60. package/src/exceptions/helpers.ts +25 -0
  61. package/src/exceptions/http_exception.ts +129 -0
  62. package/src/exceptions/index.ts +23 -0
  63. package/src/exceptions/strav_error.ts +11 -0
  64. package/src/generators/api_generator.ts +972 -0
  65. package/src/generators/config.ts +87 -0
  66. package/src/generators/doc_generator.ts +974 -0
  67. package/src/generators/index.ts +11 -0
  68. package/src/generators/model_generator.ts +586 -0
  69. package/src/generators/route_generator.ts +188 -0
  70. package/src/generators/test_generator.ts +1666 -0
  71. package/src/helpers/crypto.ts +4 -0
  72. package/src/helpers/env.ts +50 -0
  73. package/src/helpers/identity.ts +12 -0
  74. package/src/helpers/index.ts +4 -0
  75. package/src/helpers/strings.ts +67 -0
  76. package/src/http/context.ts +215 -0
  77. package/src/http/cookie.ts +59 -0
  78. package/src/http/cors.ts +163 -0
  79. package/src/http/index.ts +16 -0
  80. package/src/http/middleware.ts +39 -0
  81. package/src/http/rate_limit.ts +173 -0
  82. package/src/http/router.ts +556 -0
  83. package/src/http/server.ts +79 -0
  84. package/src/i18n/defaults/en/validation.json +20 -0
  85. package/src/i18n/helpers.ts +72 -0
  86. package/src/i18n/i18n_manager.ts +155 -0
  87. package/src/i18n/index.ts +4 -0
  88. package/src/i18n/middleware.ts +90 -0
  89. package/src/i18n/translator.ts +96 -0
  90. package/src/i18n/types.ts +17 -0
  91. package/src/logger/index.ts +6 -0
  92. package/src/logger/logger.ts +100 -0
  93. package/src/logger/request_logger.ts +19 -0
  94. package/src/logger/sinks/console_sink.ts +24 -0
  95. package/src/logger/sinks/file_sink.ts +24 -0
  96. package/src/logger/sinks/sink.ts +36 -0
  97. package/src/mail/css_inliner.ts +79 -0
  98. package/src/mail/helpers.ts +212 -0
  99. package/src/mail/index.ts +19 -0
  100. package/src/mail/mail_manager.ts +92 -0
  101. package/src/mail/transports/log_transport.ts +69 -0
  102. package/src/mail/transports/resend_transport.ts +59 -0
  103. package/src/mail/transports/sendgrid_transport.ts +77 -0
  104. package/src/mail/transports/smtp_transport.ts +48 -0
  105. package/src/mail/types.ts +80 -0
  106. package/src/notification/base_notification.ts +67 -0
  107. package/src/notification/channels/database_channel.ts +30 -0
  108. package/src/notification/channels/discord_channel.ts +43 -0
  109. package/src/notification/channels/email_channel.ts +37 -0
  110. package/src/notification/channels/webhook_channel.ts +45 -0
  111. package/src/notification/helpers.ts +214 -0
  112. package/src/notification/index.ts +20 -0
  113. package/src/notification/notification_manager.ts +126 -0
  114. package/src/notification/types.ts +122 -0
  115. package/src/orm/base_model.ts +351 -0
  116. package/src/orm/decorators.ts +127 -0
  117. package/src/orm/index.ts +4 -0
  118. package/src/policy/authorize.ts +44 -0
  119. package/src/policy/index.ts +3 -0
  120. package/src/policy/policy_result.ts +13 -0
  121. package/src/queue/index.ts +11 -0
  122. package/src/queue/queue.ts +338 -0
  123. package/src/queue/worker.ts +197 -0
  124. package/src/scheduler/cron.ts +140 -0
  125. package/src/scheduler/index.ts +7 -0
  126. package/src/scheduler/runner.ts +116 -0
  127. package/src/scheduler/schedule.ts +183 -0
  128. package/src/scheduler/scheduler.ts +47 -0
  129. package/src/schema/database_representation.ts +122 -0
  130. package/src/schema/define_association.ts +60 -0
  131. package/src/schema/define_schema.ts +46 -0
  132. package/src/schema/field_builder.ts +155 -0
  133. package/src/schema/field_definition.ts +66 -0
  134. package/src/schema/index.ts +21 -0
  135. package/src/schema/naming.ts +19 -0
  136. package/src/schema/postgres.ts +109 -0
  137. package/src/schema/registry.ts +157 -0
  138. package/src/schema/representation_builder.ts +479 -0
  139. package/src/schema/type_builder.ts +107 -0
  140. package/src/schema/types.ts +35 -0
  141. package/src/session/index.ts +4 -0
  142. package/src/session/middleware.ts +46 -0
  143. package/src/session/session.ts +308 -0
  144. package/src/session/session_manager.ts +81 -0
  145. package/src/storage/index.ts +13 -0
  146. package/src/storage/local_driver.ts +46 -0
  147. package/src/storage/s3_driver.ts +51 -0
  148. package/src/storage/storage.ts +43 -0
  149. package/src/storage/storage_manager.ts +59 -0
  150. package/src/storage/types.ts +42 -0
  151. package/src/storage/upload.ts +91 -0
  152. package/src/validation/index.ts +18 -0
  153. package/src/validation/rules.ts +170 -0
  154. package/src/validation/validate.ts +41 -0
  155. package/src/view/cache.ts +47 -0
  156. package/src/view/client/islands.ts +50 -0
  157. package/src/view/compiler.ts +185 -0
  158. package/src/view/engine.ts +139 -0
  159. package/src/view/escape.ts +14 -0
  160. package/src/view/index.ts +13 -0
  161. package/src/view/islands/island_builder.ts +161 -0
  162. package/src/view/islands/vue_plugin.ts +140 -0
  163. package/src/view/middleware/static.ts +35 -0
  164. package/src/view/tokenizer.ts +172 -0
  165. package/tsconfig.json +4 -0
@@ -0,0 +1,4 @@
1
+ /** Generate a random hex string of the given byte length. */
2
+ export function randomHex(bytes: number): string {
3
+ return Buffer.from(crypto.getRandomValues(new Uint8Array(bytes))).toString('hex')
4
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Read an environment variable with an optional default.
3
+ *
4
+ * Bun auto-loads `.env`, so all variables are available via `process.env`.
5
+ *
6
+ * @example
7
+ * env('DB_HOST', '127.0.0.1')
8
+ * env('APP_KEY') // throws if not set and no default
9
+ */
10
+ function env(key: string, defaultValue: string): string
11
+ function env(key: string, defaultValue: null): string | null
12
+ function env(key: string): string
13
+ function env(key: string, defaultValue?: string | null): string | null {
14
+ const value = process.env[key]
15
+ if (value !== undefined) return value
16
+ if (defaultValue !== undefined) return defaultValue
17
+ throw new Error(`Environment variable "${key}" is not set`)
18
+ }
19
+
20
+ /** Read an environment variable as an integer. */
21
+ env.int = (key: string, defaultValue?: number): number => {
22
+ const raw = process.env[key]
23
+ if (raw !== undefined) {
24
+ const parsed = parseInt(raw, 10)
25
+ if (!isNaN(parsed)) return parsed
26
+ }
27
+ if (defaultValue !== undefined) return defaultValue
28
+ throw new Error(`Environment variable "${key}" is not set or not a valid integer`)
29
+ }
30
+
31
+ /** Read an environment variable as a float. */
32
+ env.float = (key: string, defaultValue?: number): number => {
33
+ const raw = process.env[key]
34
+ if (raw !== undefined) {
35
+ const parsed = parseFloat(raw)
36
+ if (!isNaN(parsed)) return parsed
37
+ }
38
+ if (defaultValue !== undefined) return defaultValue
39
+ throw new Error(`Environment variable "${key}" is not set or not a valid number`)
40
+ }
41
+
42
+ /** Read an environment variable as a boolean. Truthy: `'true'`, `'1'`, `'yes'`. */
43
+ env.bool = (key: string, defaultValue?: boolean): boolean => {
44
+ const raw = process.env[key]
45
+ if (raw !== undefined) return ['true', '1', 'yes'].includes(raw.toLowerCase())
46
+ if (defaultValue !== undefined) return defaultValue
47
+ throw new Error(`Environment variable "${key}" is not set`)
48
+ }
49
+
50
+ export { env }
@@ -0,0 +1,12 @@
1
+ import BaseModel from '../orm/base_model.ts'
2
+
3
+ /** Extract a user ID from a BaseModel instance or a raw string/number. */
4
+ export function extractUserId(user: unknown): string {
5
+ if (typeof user === 'string') return user
6
+ if (typeof user === 'number') return String(user)
7
+ if (user instanceof BaseModel) {
8
+ const ctor = user.constructor as typeof BaseModel
9
+ return String((user as unknown as Record<string, unknown>)[ctor.primaryKeyProperty])
10
+ }
11
+ throw new Error('Pass a BaseModel instance or a string/number user ID.')
12
+ }
@@ -0,0 +1,4 @@
1
+ export { toSnakeCase, toCamelCase, toPascalCase } from './strings.ts'
2
+ export { env } from './env.ts'
3
+ export { randomHex } from './crypto.ts'
4
+ export { extractUserId } from './identity.ts'
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Convert a camelCase or PascalCase string to snake_case.
3
+ *
4
+ * @example
5
+ * toSnakeCase('firstName') // 'first_name'
6
+ * toSnakeCase('HTMLParser') // 'html_parser'
7
+ * toSnakeCase('already_snake') // 'already_snake'
8
+ */
9
+ export function toSnakeCase(str: string): string {
10
+ return str
11
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2')
12
+ .replace(/([a-z\d])([A-Z])/g, '$1_$2')
13
+ .toLowerCase()
14
+ }
15
+
16
+ /**
17
+ * Convert a snake_case string to camelCase.
18
+ *
19
+ * @example
20
+ * toCamelCase('first_name') // 'firstName'
21
+ * toCamelCase('created_at') // 'createdAt'
22
+ * toCamelCase('reviewed_by_id') // 'reviewedById'
23
+ * toCamelCase('id') // 'id'
24
+ */
25
+ export function toCamelCase(str: string): string {
26
+ return str.replace(/_([a-z\d])/g, (_, c) => c.toUpperCase())
27
+ }
28
+
29
+ /**
30
+ * Convert a snake_case or plain string to PascalCase.
31
+ *
32
+ * @example
33
+ * toPascalCase('user_role') // 'UserRole'
34
+ * toPascalCase('user') // 'User'
35
+ * toPascalCase('order_item') // 'OrderItem'
36
+ */
37
+ export function toPascalCase(str: string): string {
38
+ const camel = toCamelCase(str)
39
+ return camel.charAt(0).toUpperCase() + camel.slice(1)
40
+ }
41
+
42
+ /**
43
+ * Naively pluralize an English word.
44
+ *
45
+ * Handles common suffixes (s/x/z/ch/sh → es, consonant+y → ies).
46
+ * Not a full inflection library — adequate for code-gen route paths.
47
+ *
48
+ * @example
49
+ * pluralize('user') // 'users'
50
+ * pluralize('category') // 'categories'
51
+ * pluralize('status') // 'statuses'
52
+ */
53
+ export function pluralize(word: string): string {
54
+ if (
55
+ word.endsWith('s') ||
56
+ word.endsWith('x') ||
57
+ word.endsWith('z') ||
58
+ word.endsWith('ch') ||
59
+ word.endsWith('sh')
60
+ ) {
61
+ return word + 'es'
62
+ }
63
+ if (word.endsWith('y') && !/[aeiou]y$/.test(word)) {
64
+ return word.slice(0, -1) + 'ies'
65
+ }
66
+ return word + 's'
67
+ }
@@ -0,0 +1,215 @@
1
+ import { parseCookies } from './cookie.ts'
2
+ import type ViewEngine from '../view/engine.ts'
3
+ import { ConfigurationError } from '../exceptions/errors.ts'
4
+
5
+ /**
6
+ * HTTP request context — the primary object handlers interact with.
7
+ *
8
+ * Wraps Bun's native Request and adds route params, body parsing,
9
+ * response helpers, and a type-safe state bag for middleware.
10
+ */
11
+ export default class Context {
12
+ private static _viewEngine: ViewEngine | null = null
13
+
14
+ static setViewEngine(engine: ViewEngine): void {
15
+ Context._viewEngine = engine
16
+ }
17
+
18
+ readonly url: URL
19
+ readonly method: string
20
+ readonly path: string
21
+ readonly headers: Headers
22
+
23
+ private _state = new Map<string, unknown>()
24
+ private _subdomain?: string
25
+ private _query?: URLSearchParams
26
+ private _cookies?: Map<string, string>
27
+ private _body?: unknown
28
+ private _bodyParsed = false
29
+
30
+ constructor(
31
+ readonly request: Request,
32
+ readonly params: Record<string, string> = {},
33
+ private domain: string = 'localhost'
34
+ ) {
35
+ this.url = new URL(request.url)
36
+ this.method = request.method
37
+ this.path = this.url.pathname
38
+ this.headers = request.headers
39
+ }
40
+
41
+ // ---------------------------------------------------------------------------
42
+ // Request helpers
43
+ // ---------------------------------------------------------------------------
44
+
45
+ /** Parsed query string parameters. */
46
+ get query(): URLSearchParams {
47
+ if (!this._query) this._query = this.url.searchParams
48
+ return this._query
49
+ }
50
+
51
+ /** Subdomain extracted from the Host header relative to the configured domain. */
52
+ get subdomain(): string {
53
+ if (this._subdomain !== undefined) return this._subdomain
54
+
55
+ const host = this.headers.get('host') ?? ''
56
+ const hostname = host.split(':')[0] ?? ''
57
+
58
+ if (hostname.endsWith(this.domain) && hostname.length > this.domain.length) {
59
+ this._subdomain = hostname.slice(0, -(this.domain.length + 1))
60
+ } else {
61
+ this._subdomain = ''
62
+ }
63
+
64
+ return this._subdomain
65
+ }
66
+
67
+ /** Shorthand for reading a single request header. */
68
+ header(name: string): string | null {
69
+ return this.headers.get(name)
70
+ }
71
+
72
+ /** Read a query string parameter, with optional typed default. */
73
+ qs(name: string): string | null
74
+ qs(name: string, defaultValue: number): number
75
+ qs(name: string, defaultValue: string): string
76
+ qs(name: string, defaultValue?: string | number): string | number | null {
77
+ const value = this.query.get(name)
78
+ if (value === null || value === '') return defaultValue ?? null
79
+ if (typeof defaultValue === 'number') {
80
+ const parsed = Number(value)
81
+ return Number.isNaN(parsed) ? defaultValue : parsed
82
+ }
83
+ return value
84
+ }
85
+
86
+ /** Read a cookie value by name from the Cookie header. */
87
+ cookie(name: string): string | null {
88
+ if (!this._cookies) {
89
+ this._cookies = parseCookies(this.headers.get('cookie') ?? '')
90
+ }
91
+ return this._cookies.get(name) ?? null
92
+ }
93
+
94
+ /** Extract named string fields from a form body. With no args, returns all non-file fields. */
95
+ async inputs<K extends string>(...keys: K[]): Promise<Record<K, string>> {
96
+ const form = await this.body<FormData>()
97
+ const result = {} as Record<K, string>
98
+ if (keys.length === 0) {
99
+ form.forEach((value, key) => {
100
+ if (typeof value === 'string') (result as Record<string, string>)[key] = value
101
+ })
102
+ } else {
103
+ for (const key of keys) {
104
+ const value = form.get(key)
105
+ result[key] = typeof value === 'string' ? value : ''
106
+ }
107
+ }
108
+ return result
109
+ }
110
+
111
+ /** Extract named file fields from a form body. With no args, returns all file fields. */
112
+ async files<K extends string>(...keys: K[]): Promise<Record<K, File | null>> {
113
+ const form = await this.body<FormData>()
114
+ const result = {} as Record<K, File | null>
115
+ if (keys.length === 0) {
116
+ form.forEach((value, key) => {
117
+ if (value instanceof File) (result as Record<string, File>)[key] = value
118
+ })
119
+ } else {
120
+ for (const key of keys) {
121
+ const value = form.get(key)
122
+ result[key] = value instanceof File ? value : null
123
+ }
124
+ }
125
+ return result
126
+ }
127
+
128
+ /**
129
+ * Parse the request body. Automatically detects JSON, form-data, and text.
130
+ * The result is cached — safe to call multiple times.
131
+ */
132
+ async body<T = unknown>(): Promise<T> {
133
+ if (!this._bodyParsed) {
134
+ const contentType = this.header('content-type') ?? ''
135
+
136
+ if (contentType.includes('application/json')) {
137
+ this._body = await this.request.json()
138
+ } else if (
139
+ contentType.includes('multipart/form-data') ||
140
+ contentType.includes('application/x-www-form-urlencoded')
141
+ ) {
142
+ this._body = await this.request.formData()
143
+ } else {
144
+ this._body = await this.request.text()
145
+ }
146
+
147
+ this._bodyParsed = true
148
+ }
149
+
150
+ return this._body as T
151
+ }
152
+
153
+ // ---------------------------------------------------------------------------
154
+ // Response helpers
155
+ // ---------------------------------------------------------------------------
156
+
157
+ json(data: unknown, status = 200): Response {
158
+ return new Response(JSON.stringify(data), {
159
+ status,
160
+ headers: { 'Content-Type': 'application/json' },
161
+ })
162
+ }
163
+
164
+ text(content: string, status = 200): Response {
165
+ return new Response(content, {
166
+ status,
167
+ headers: { 'Content-Type': 'text/plain' },
168
+ })
169
+ }
170
+
171
+ html(content: string, status = 200): Response {
172
+ return new Response(content, {
173
+ status,
174
+ headers: { 'Content-Type': 'text/html' },
175
+ })
176
+ }
177
+
178
+ redirect(url: string, status = 302): Response {
179
+ return new Response(null, {
180
+ status,
181
+ headers: { Location: url },
182
+ })
183
+ }
184
+
185
+ empty(status = 204): Response {
186
+ return new Response(null, { status })
187
+ }
188
+
189
+ async view(template: string, data?: Record<string, unknown>, status = 200): Promise<Response> {
190
+ if (!Context._viewEngine) {
191
+ throw new ConfigurationError('ViewEngine not configured. Register it in the container.')
192
+ }
193
+ const html = await Context._viewEngine.render(template, data)
194
+ return this.html(html, status)
195
+ }
196
+
197
+ // ---------------------------------------------------------------------------
198
+ // Middleware state
199
+ // ---------------------------------------------------------------------------
200
+
201
+ /** Store a value for downstream middleware / handlers. */
202
+ set<T>(key: string, value: T): void {
203
+ this._state.set(key, value)
204
+ }
205
+
206
+ /** Retrieve a value set by upstream middleware. */
207
+ get<T>(key: string): T
208
+ get<T1, T2>(k1: string, k2: string): [T1, T2]
209
+ get<T1, T2, T3>(k1: string, k2: string, k3: string): [T1, T2, T3]
210
+ get<T1, T2, T3, T4>(k1: string, k2: string, k3: string, k4: string): [T1, T2, T3, T4]
211
+ get(...keys: string[]): unknown {
212
+ if (keys.length === 1) return this._state.get(keys[0]!)
213
+ return keys.map(k => this._state.get(k))
214
+ }
215
+ }
@@ -0,0 +1,59 @@
1
+ /** Options for serializing a Set-Cookie header. */
2
+ export interface CookieOptions {
3
+ httpOnly?: boolean
4
+ secure?: boolean
5
+ sameSite?: 'strict' | 'lax' | 'none'
6
+ maxAge?: number // seconds
7
+ path?: string
8
+ domain?: string
9
+ }
10
+
11
+ /** Serialize a cookie name/value pair into a Set-Cookie header string. */
12
+ export function serializeCookie(name: string, value: string, options: CookieOptions = {}): string {
13
+ let cookie = `${name}=${encodeURIComponent(value)}`
14
+
15
+ if (options.httpOnly) cookie += '; HttpOnly'
16
+ if (options.secure) cookie += '; Secure'
17
+ if (options.sameSite) cookie += `; SameSite=${options.sameSite}`
18
+ if (options.maxAge !== undefined) cookie += `; Max-Age=${options.maxAge}`
19
+ cookie += `; Path=${options.path ?? '/'}`
20
+ if (options.domain) cookie += `; Domain=${options.domain}`
21
+
22
+ return cookie
23
+ }
24
+
25
+ /** Parse a Cookie header string into a map of name → value. */
26
+ export function parseCookies(header: string): Map<string, string> {
27
+ const cookies = new Map<string, string>()
28
+
29
+ for (const pair of header.split(';')) {
30
+ const eq = pair.indexOf('=')
31
+ if (eq === -1) continue
32
+ const key = pair.slice(0, eq).trim()
33
+ const value = decodeURIComponent(pair.slice(eq + 1).trim())
34
+ cookies.set(key, value)
35
+ }
36
+
37
+ return cookies
38
+ }
39
+
40
+ /** Return a new Response with a Set-Cookie header appended. */
41
+ export function withCookie(
42
+ response: Response,
43
+ name: string,
44
+ value: string,
45
+ options?: CookieOptions
46
+ ): Response {
47
+ const headers = new Headers(response.headers)
48
+ headers.append('Set-Cookie', serializeCookie(name, value, options))
49
+ return new Response(response.body, {
50
+ status: response.status,
51
+ statusText: response.statusText,
52
+ headers,
53
+ })
54
+ }
55
+
56
+ /** Return a new Response with a cookie-clearing Set-Cookie header (Max-Age=0). */
57
+ export function clearCookie(response: Response, name: string, options?: CookieOptions): Response {
58
+ return withCookie(response, name, '', { ...options, maxAge: 0 })
59
+ }
@@ -0,0 +1,163 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Types
3
+ // ---------------------------------------------------------------------------
4
+
5
+ export interface CorsOptions {
6
+ /**
7
+ * Allowed origins. Determines Access-Control-Allow-Origin.
8
+ *
9
+ * - `'*'` — allow all origins (incompatible with credentials)
10
+ * - `string` — a single exact origin
11
+ * - `string[]` — an allow-list of exact origins
12
+ * - `RegExp` — pattern tested against the request Origin header
13
+ * - `(origin: string) => boolean` — callback for custom logic
14
+ *
15
+ * @default '*'
16
+ */
17
+ origin?: string | string[] | RegExp | ((origin: string) => boolean)
18
+
19
+ /**
20
+ * Allowed HTTP methods for preflight responses.
21
+ * @default ['GET','HEAD','PUT','PATCH','POST','DELETE']
22
+ */
23
+ methods?: string[]
24
+
25
+ /**
26
+ * Allowed request headers for preflight responses.
27
+ * When unset, mirrors the Access-Control-Request-Headers from the preflight.
28
+ */
29
+ allowedHeaders?: string[]
30
+
31
+ /** Headers exposed to the browser via Access-Control-Expose-Headers. */
32
+ exposedHeaders?: string[]
33
+
34
+ /**
35
+ * Include Access-Control-Allow-Credentials: true.
36
+ * When true, origin cannot be literal `'*'` — the actual request origin is reflected.
37
+ * @default false
38
+ */
39
+ credentials?: boolean
40
+
41
+ /**
42
+ * Preflight cache duration in seconds (Access-Control-Max-Age).
43
+ * @default 86400
44
+ */
45
+ maxAge?: number
46
+ }
47
+
48
+ /** Resolved config with defaults applied. */
49
+ export interface ResolvedCorsConfig {
50
+ origin: string | string[] | RegExp | ((origin: string) => boolean)
51
+ methods: string[]
52
+ allowedHeaders?: string[]
53
+ exposedHeaders?: string[]
54
+ credentials: boolean
55
+ maxAge: number
56
+ }
57
+
58
+ // ---------------------------------------------------------------------------
59
+ // Defaults
60
+ // ---------------------------------------------------------------------------
61
+
62
+ const DEFAULTS: ResolvedCorsConfig = {
63
+ origin: '*',
64
+ methods: ['GET', 'HEAD', 'PUT', 'PATCH', 'POST', 'DELETE'],
65
+ credentials: false,
66
+ maxAge: 86400,
67
+ }
68
+
69
+ // ---------------------------------------------------------------------------
70
+ // Helpers
71
+ // ---------------------------------------------------------------------------
72
+
73
+ /** Merge user options with defaults. */
74
+ export function resolveCorsConfig(options?: CorsOptions): ResolvedCorsConfig {
75
+ return { ...DEFAULTS, ...options }
76
+ }
77
+
78
+ /**
79
+ * Determine the Access-Control-Allow-Origin value for a request origin.
80
+ * Returns `null` when the origin is not allowed.
81
+ */
82
+ export function resolveOrigin(
83
+ config: ResolvedCorsConfig,
84
+ requestOrigin: string | null
85
+ ): string | null {
86
+ const { origin, credentials } = config
87
+
88
+ if (!requestOrigin) return credentials ? null : '*'
89
+
90
+ if (origin === '*') return credentials ? requestOrigin : '*'
91
+ if (typeof origin === 'string') return origin === requestOrigin ? origin : null
92
+ if (Array.isArray(origin)) return origin.includes(requestOrigin) ? requestOrigin : null
93
+ if (origin instanceof RegExp) return origin.test(requestOrigin) ? requestOrigin : null
94
+ if (typeof origin === 'function') return origin(requestOrigin) ? requestOrigin : null
95
+
96
+ return null
97
+ }
98
+
99
+ /** Build a 204 preflight response with CORS headers. */
100
+ export function preflightResponse(
101
+ config: ResolvedCorsConfig,
102
+ requestOrigin: string | null,
103
+ requestHeaders: string | null
104
+ ): Response {
105
+ const allowedOrigin = resolveOrigin(config, requestOrigin)
106
+
107
+ if (!allowedOrigin) return new Response(null, { status: 204 })
108
+
109
+ const headers = new Headers()
110
+ headers.set('Access-Control-Allow-Origin', allowedOrigin)
111
+ headers.set('Access-Control-Allow-Methods', config.methods.join(', '))
112
+ headers.set('Access-Control-Max-Age', String(config.maxAge))
113
+
114
+ if (config.credentials) {
115
+ headers.set('Access-Control-Allow-Credentials', 'true')
116
+ }
117
+
118
+ if (config.allowedHeaders) {
119
+ headers.set('Access-Control-Allow-Headers', config.allowedHeaders.join(', '))
120
+ } else if (requestHeaders) {
121
+ headers.set('Access-Control-Allow-Headers', requestHeaders)
122
+ }
123
+
124
+ if (allowedOrigin !== '*') {
125
+ headers.set('Vary', 'Origin')
126
+ }
127
+
128
+ return new Response(null, { status: 204, headers })
129
+ }
130
+
131
+ /** Return a new Response with CORS headers appended. */
132
+ export function withCorsHeaders(
133
+ response: Response,
134
+ config: ResolvedCorsConfig,
135
+ requestOrigin: string | null
136
+ ): Response {
137
+ const allowedOrigin = resolveOrigin(config, requestOrigin)
138
+ if (!allowedOrigin) return response
139
+
140
+ const headers = new Headers(response.headers)
141
+ headers.set('Access-Control-Allow-Origin', allowedOrigin)
142
+
143
+ if (config.credentials) {
144
+ headers.set('Access-Control-Allow-Credentials', 'true')
145
+ }
146
+
147
+ if (config.exposedHeaders?.length) {
148
+ headers.set('Access-Control-Expose-Headers', config.exposedHeaders.join(', '))
149
+ }
150
+
151
+ if (allowedOrigin !== '*') {
152
+ const existing = headers.get('Vary')
153
+ if (!existing?.includes('Origin')) {
154
+ headers.set('Vary', existing ? `${existing}, Origin` : 'Origin')
155
+ }
156
+ }
157
+
158
+ return new Response(response.body, {
159
+ status: response.status,
160
+ statusText: response.statusText,
161
+ headers,
162
+ })
163
+ }
@@ -0,0 +1,16 @@
1
+ import { app } from '../core/application.ts'
2
+ import Router from './router.ts'
3
+
4
+ export { default as Context } from './context.ts'
5
+ export { default as Router } from './router.ts'
6
+ export { default as Server } from './server.ts'
7
+ export { compose } from './middleware.ts'
8
+ export { serializeCookie, parseCookies, withCookie, clearCookie } from './cookie.ts'
9
+ export { rateLimit, MemoryStore } from './rate_limit.ts'
10
+ export type { Handler, Middleware, Next } from './middleware.ts'
11
+ export type { GroupOptions, WebSocketHandlers, WebSocketData } from './router.ts'
12
+ export type { CookieOptions } from './cookie.ts'
13
+ export type { CorsOptions } from './cors.ts'
14
+ export type { RateLimitOptions, RateLimitStore, RateLimitInfo } from './rate_limit.ts'
15
+
16
+ export const router = app.resolve(Router)
@@ -0,0 +1,39 @@
1
+ import type Context from './context.ts'
2
+
3
+ /** A route handler — receives a Context and returns a Response. */
4
+ export type Handler = (ctx: Context) => Response | Promise<Response>
5
+
6
+ /** Invokes the next middleware (or the final handler) in the pipeline. */
7
+ export type Next = () => Promise<Response>
8
+
9
+ /** A middleware function — wraps a handler with before/after logic. */
10
+ export type Middleware = (ctx: Context, next: Next) => Response | Promise<Response>
11
+
12
+ /**
13
+ * Compose an array of middleware and a final handler into a single handler.
14
+ *
15
+ * Implements the onion model: each middleware wraps the next, and can
16
+ * inspect or modify the response on the way back.
17
+ *
18
+ * @example
19
+ * const handler = compose([logger, auth], finalHandler)
20
+ * const response = await handler(ctx)
21
+ */
22
+ export function compose(middleware: Middleware[], handler: Handler): Handler {
23
+ return (ctx: Context) => {
24
+ let index = -1
25
+
26
+ function dispatch(i: number): Promise<Response> {
27
+ if (i <= index) throw new Error('next() called multiple times')
28
+ index = i
29
+
30
+ if (i === middleware.length) {
31
+ return Promise.resolve(handler(ctx))
32
+ }
33
+
34
+ return Promise.resolve(middleware[i]!(ctx, () => dispatch(i + 1)))
35
+ }
36
+
37
+ return dispatch(0)
38
+ }
39
+ }