@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,158 @@
1
+ import EncryptionManager from './encryption_manager.ts'
2
+
3
+ /**
4
+ * Encryption helper — the primary API for all cryptographic operations.
5
+ *
6
+ * Uses the configured APP_KEY for symmetric encryption and HMAC signing.
7
+ * Password hashing uses argon2id via Bun.password (no key needed).
8
+ *
9
+ * @example
10
+ * import { encrypt } from '@stravigor/core/encryption'
11
+ *
12
+ * // Encrypt & decrypt strings
13
+ * const encrypted = encrypt.encrypt('sensitive data')
14
+ * const decrypted = encrypt.decrypt(encrypted)
15
+ *
16
+ * // Seal & unseal objects (JSON + encryption)
17
+ * const token = encrypt.seal({ userId: 123 })
18
+ * const data = encrypt.unseal<{ userId: number }>(token)
19
+ *
20
+ * // Password hashing
21
+ * const hash = await encrypt.hash('password123')
22
+ * const valid = await encrypt.verify('password123', hash)
23
+ *
24
+ * // HMAC signing
25
+ * const sig = encrypt.sign('payload')
26
+ * const ok = encrypt.verifySignature('payload', sig)
27
+ */
28
+ export const encrypt = {
29
+ /**
30
+ * Encrypt a string using AES-256-GCM. Returns a base64url-encoded payload.
31
+ *
32
+ * @example
33
+ * const encrypted = encrypt.encrypt('my secret')
34
+ * // Store `encrypted` in the database — it's a safe, opaque string
35
+ */
36
+ encrypt(plaintext: string): string {
37
+ return EncryptionManager.encrypt(plaintext)
38
+ },
39
+
40
+ /**
41
+ * Decrypt a payload encrypted with `encrypt()`.
42
+ * Supports key rotation — tries previous keys if the current key fails.
43
+ *
44
+ * @example
45
+ * const original = encrypt.decrypt(encrypted) // 'my secret'
46
+ */
47
+ decrypt(payload: string): string {
48
+ return EncryptionManager.decrypt(payload)
49
+ },
50
+
51
+ /**
52
+ * Encrypt and serialize an object. Perfect for tamper-proof cookies or tokens.
53
+ *
54
+ * @example
55
+ * const token = encrypt.seal({ userId: 123, role: 'admin' })
56
+ * // Send `token` to the client — it's encrypted and tamper-proof
57
+ */
58
+ seal(data: unknown): string {
59
+ return EncryptionManager.seal(data)
60
+ },
61
+
62
+ /**
63
+ * Decrypt and deserialize an object sealed with `seal()`.
64
+ *
65
+ * @example
66
+ * const data = encrypt.unseal<{ userId: number }>(token)
67
+ * console.log(data.userId) // 123
68
+ */
69
+ unseal<T = unknown>(payload: string): T {
70
+ return EncryptionManager.unseal<T>(payload)
71
+ },
72
+
73
+ /**
74
+ * Hash a password using argon2id. Returns an encoded hash string.
75
+ *
76
+ * @example
77
+ * const hash = await encrypt.hash(formData.password)
78
+ * await db.sql`UPDATE users SET password = ${hash} WHERE id = ${userId}`
79
+ */
80
+ hash(password: string): Promise<string> {
81
+ return EncryptionManager.hash(password)
82
+ },
83
+
84
+ /**
85
+ * Verify a password against a hash. Works with argon2id and bcrypt.
86
+ *
87
+ * @example
88
+ * const valid = await encrypt.verify(formData.password, user.password)
89
+ * if (!valid) throw new Error('Invalid credentials')
90
+ */
91
+ verify(password: string, hash: string): Promise<boolean> {
92
+ return EncryptionManager.verify(password, hash)
93
+ },
94
+
95
+ /**
96
+ * Create an HMAC-SHA256 signature. Returns a hex string.
97
+ *
98
+ * @example
99
+ * const sig = encrypt.sign(`${webhookId}:${timestamp}:${body}`)
100
+ * response.headers.set('X-Signature', sig)
101
+ */
102
+ sign(data: string): string {
103
+ return EncryptionManager.sign(data)
104
+ },
105
+
106
+ /**
107
+ * Verify an HMAC-SHA256 signature (timing-safe).
108
+ * Supports key rotation — tries previous keys if the current key fails.
109
+ *
110
+ * @example
111
+ * const valid = encrypt.verifySignature(body, req.headers.get('X-Signature')!)
112
+ * if (!valid) return new Response('Invalid signature', { status: 401 })
113
+ */
114
+ verifySignature(data: string, signature: string): boolean {
115
+ return EncryptionManager.verifySignature(data, signature)
116
+ },
117
+
118
+ /**
119
+ * SHA-256 hash. Returns a hex string.
120
+ *
121
+ * @example
122
+ * const checksum = encrypt.sha256(fileContents)
123
+ */
124
+ sha256(data: string): string {
125
+ return EncryptionManager.sha256(data)
126
+ },
127
+
128
+ /**
129
+ * SHA-512 hash. Returns a hex string.
130
+ *
131
+ * @example
132
+ * const hash = encrypt.sha512(data)
133
+ */
134
+ sha512(data: string): string {
135
+ return EncryptionManager.sha512(data)
136
+ },
137
+
138
+ /**
139
+ * Generate a random hex string. Default: 32 bytes → 64 hex chars.
140
+ *
141
+ * @example
142
+ * const apiKey = encrypt.random() // 64-char hex (32 bytes)
143
+ * const shortToken = encrypt.random(16) // 32-char hex (16 bytes)
144
+ */
145
+ random(bytes: number = 32): string {
146
+ return EncryptionManager.random(bytes)
147
+ },
148
+
149
+ /**
150
+ * Generate raw random bytes.
151
+ *
152
+ * @example
153
+ * const iv = encrypt.randomBytes(12)
154
+ */
155
+ randomBytes(bytes: number = 32): Uint8Array {
156
+ return EncryptionManager.randomBytes(bytes)
157
+ },
158
+ }
@@ -0,0 +1,3 @@
1
+ export { default, default as EncryptionManager } from './encryption_manager.ts'
2
+ export { encrypt } from './helpers.ts'
3
+ export type { EncryptionConfig } from './types.ts'
@@ -0,0 +1,6 @@
1
+ export interface EncryptionConfig {
2
+ /** The application key used for symmetric encryption and HMAC signing. Required. */
3
+ key: string
4
+ /** Previous keys for rotation — tried on decrypt/unseal failure before giving up. */
5
+ previousKeys: string[]
6
+ }
@@ -0,0 +1,101 @@
1
+ /** A function that handles an event payload. */
2
+ export type Listener<T = any> = (payload: T) => void | Promise<void>
3
+
4
+ /**
5
+ * In-memory event bus — publish/subscribe with async support.
6
+ *
7
+ * All methods are static. No DI, no database, no configuration required.
8
+ * Listeners are awaited in parallel when an event is emitted.
9
+ * If any listener throws, the remaining listeners still execute
10
+ * and the first error is re-thrown after all complete.
11
+ *
12
+ * @example
13
+ * Emitter.on<{ user: User }>('user.registered', async ({ user }) => {
14
+ * await sendWelcomeEmail(user)
15
+ * })
16
+ *
17
+ * await Emitter.emit('user.registered', { user })
18
+ */
19
+ export default class Emitter {
20
+ private static _listeners = new Map<string, Set<Listener>>()
21
+ private static _once = new WeakSet<Listener>()
22
+
23
+ /** Register a listener for an event. */
24
+ static on<T = any>(event: string, listener: Listener<T>): void {
25
+ let set = Emitter._listeners.get(event)
26
+ if (!set) {
27
+ set = new Set()
28
+ Emitter._listeners.set(event, set)
29
+ }
30
+ set.add(listener)
31
+ }
32
+
33
+ /** Register a listener that is automatically removed after its first invocation. */
34
+ static once<T = any>(event: string, listener: Listener<T>): void {
35
+ Emitter._once.add(listener)
36
+ Emitter.on(event, listener)
37
+ }
38
+
39
+ /** Remove a specific listener for an event. */
40
+ static off<T = any>(event: string, listener: Listener<T>): void {
41
+ const set = Emitter._listeners.get(event)
42
+ if (!set) return
43
+ set.delete(listener)
44
+ if (set.size === 0) Emitter._listeners.delete(event)
45
+ }
46
+
47
+ /** Remove all listeners for a specific event, or all listeners entirely. */
48
+ static removeAllListeners(event?: string): void {
49
+ if (event) {
50
+ Emitter._listeners.delete(event)
51
+ } else {
52
+ Emitter._listeners.clear()
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Emit an event. All registered listeners are invoked in parallel.
58
+ *
59
+ * If any listener throws, the other listeners still execute.
60
+ * After all listeners settle, the first error is re-thrown.
61
+ */
62
+ static async emit<T = any>(event: string, payload?: T): Promise<void> {
63
+ const set = Emitter._listeners.get(event)
64
+ if (!set || set.size === 0) return
65
+
66
+ const listeners = [...set]
67
+
68
+ // Remove one-time listeners before calling (avoids re-entrancy issues)
69
+ for (const fn of listeners) {
70
+ if (Emitter._once.has(fn)) {
71
+ Emitter._once.delete(fn)
72
+ set.delete(fn)
73
+ }
74
+ }
75
+ if (set.size === 0) Emitter._listeners.delete(event)
76
+
77
+ const results = await Promise.allSettled(
78
+ listeners.map(fn => {
79
+ try {
80
+ return fn(payload as T)
81
+ } catch (err) {
82
+ return Promise.reject(err)
83
+ }
84
+ })
85
+ )
86
+
87
+ const firstError = results.find((r): r is PromiseRejectedResult => r.status === 'rejected')
88
+ if (firstError) throw firstError.reason
89
+ }
90
+
91
+ /** Return the number of listeners registered for an event. */
92
+ static listenerCount(event: string): number {
93
+ return Emitter._listeners.get(event)?.size ?? 0
94
+ }
95
+
96
+ /** Clear all state. Intended for test teardown. */
97
+ static reset(): void {
98
+ Emitter._listeners.clear()
99
+ Emitter._once = new WeakSet()
100
+ }
101
+ }
@@ -0,0 +1,2 @@
1
+ export { default as Emitter } from './emitter.ts'
2
+ export type { Listener } from './emitter.ts'
@@ -0,0 +1,75 @@
1
+ import { StravError } from './strav_error.ts'
2
+
3
+ /**
4
+ * Thrown when a framework service is used before being configured
5
+ * or when an unknown driver/store/transport is requested.
6
+ *
7
+ * @example
8
+ * throw new ConfigurationError('CacheManager not configured. Resolve it through the container first.')
9
+ */
10
+ export class ConfigurationError extends StravError {}
11
+
12
+ /**
13
+ * Thrown by `findOrFail` / `firstOrFail` when a model record is not found.
14
+ * The ExceptionHandler renders this as a 404.
15
+ *
16
+ * @example
17
+ * throw new ModelNotFoundError('User', id)
18
+ */
19
+ export class ModelNotFoundError extends StravError {
20
+ constructor(
21
+ public readonly model: string,
22
+ public readonly id?: unknown
23
+ ) {
24
+ super(
25
+ id !== undefined
26
+ ? `${model} with ID ${id} not found`
27
+ : `${model} not found`
28
+ )
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Thrown on database operation failures (migrations, queries).
34
+ *
35
+ * @example
36
+ * throw new DatabaseError(`Migration ${version} failed: ${cause}`)
37
+ */
38
+ export class DatabaseError extends StravError {}
39
+
40
+ /**
41
+ * Thrown on encryption/decryption/signing failures.
42
+ *
43
+ * @example
44
+ * throw new EncryptionError('Decryption failed: invalid payload or key.')
45
+ */
46
+ export class EncryptionError extends StravError {}
47
+
48
+ /**
49
+ * Thrown on view template compilation or rendering failures.
50
+ *
51
+ * @example
52
+ * throw new TemplateError(`Unclosed expression at line ${line}`)
53
+ */
54
+ export class TemplateError extends StravError {}
55
+
56
+ /**
57
+ * Thrown when an external service (AI provider, mail transport, webhook) returns an error.
58
+ * The ExceptionHandler renders this as a 502.
59
+ *
60
+ * @example
61
+ * throw new ExternalServiceError('Anthropic', 429, 'Rate limited')
62
+ */
63
+ export class ExternalServiceError extends StravError {
64
+ constructor(
65
+ public readonly service: string,
66
+ public readonly statusCode?: number,
67
+ public readonly responseBody?: string
68
+ ) {
69
+ super(
70
+ statusCode
71
+ ? `${service} error (${statusCode}): ${responseBody ?? 'unknown'}`
72
+ : `${service} error: ${responseBody ?? 'unknown'}`
73
+ )
74
+ }
75
+ }
@@ -0,0 +1,126 @@
1
+ import type Context from '../http/context.ts'
2
+ import { HttpException, ValidationError, RateLimitError } from './http_exception.ts'
3
+ import { ModelNotFoundError, ConfigurationError, ExternalServiceError } from './errors.ts'
4
+
5
+ /** Converts an error into an HTTP Response. */
6
+ export type RenderFn = (error: any, ctx?: Context) => Response
7
+
8
+ /** Reports an error (logging, external services, etc.). */
9
+ export type ReportFn = (error: Error, ctx?: Context) => void
10
+
11
+ /**
12
+ * Converts thrown errors into HTTP responses.
13
+ *
14
+ * Register with the router to catch all unhandled exceptions:
15
+ *
16
+ * @example
17
+ * import { ExceptionHandler } from '@stravigor/core/exceptions'
18
+ *
19
+ * const handler = new ExceptionHandler(config.get('app.env') === 'local')
20
+ * handler.report((error, ctx) => logger.error(error.message, { path: ctx?.path }))
21
+ * router.useExceptionHandler(handler)
22
+ */
23
+ export class ExceptionHandler {
24
+ private renderers = new Map<Function, RenderFn>()
25
+ private reporters: ReportFn[] = []
26
+
27
+ constructor(private isDev = false) {}
28
+
29
+ /**
30
+ * Register a custom renderer for a specific error class.
31
+ * Custom renderers take precedence over built-in rendering.
32
+ *
33
+ * @example
34
+ * handler.render(PaymentError, (error) => {
35
+ * return Response.json({ error: error.message, code: error.code }, { status: 402 })
36
+ * })
37
+ */
38
+ render<T extends Error>(
39
+ errorClass: new (...args: any[]) => T,
40
+ fn: (error: T, ctx?: Context) => Response
41
+ ): this {
42
+ this.renderers.set(errorClass, fn as RenderFn)
43
+ return this
44
+ }
45
+
46
+ /**
47
+ * Register a reporter. Runs for every error before rendering.
48
+ *
49
+ * @example
50
+ * handler.report((error, ctx) => {
51
+ * logger.error(error.message, { path: ctx?.path, stack: error.stack })
52
+ * })
53
+ */
54
+ report(fn: ReportFn): this {
55
+ this.reporters.push(fn)
56
+ return this
57
+ }
58
+
59
+ /** Convert a thrown error into an HTTP Response. */
60
+ handle(error: unknown, ctx?: Context): Response {
61
+ const err = error instanceof Error ? error : new Error(String(error))
62
+
63
+ // Run reporters
64
+ for (const reporter of this.reporters) {
65
+ try {
66
+ reporter(err, ctx)
67
+ } catch {
68
+ // Reporters must not throw
69
+ }
70
+ }
71
+
72
+ // Check custom renderers (walk the prototype chain)
73
+ let proto = err.constructor
74
+ while (proto && proto !== Object) {
75
+ const renderer = this.renderers.get(proto)
76
+ if (renderer) return renderer(err, ctx)
77
+ proto = Object.getPrototypeOf(proto)
78
+ }
79
+
80
+ // Built-in rendering
81
+ if (err instanceof ValidationError) {
82
+ return json({ error: err.message, errors: err.errors }, err.status)
83
+ }
84
+
85
+ if (err instanceof RateLimitError) {
86
+ const headers: Record<string, string> = { 'Content-Type': 'application/json' }
87
+ if (err.retryAfter !== undefined) {
88
+ headers['Retry-After'] = String(err.retryAfter)
89
+ }
90
+ return new Response(JSON.stringify({ error: err.message }), {
91
+ status: err.status,
92
+ headers,
93
+ })
94
+ }
95
+
96
+ if (err instanceof HttpException) {
97
+ return json({ error: err.message }, err.status)
98
+ }
99
+
100
+ if (err instanceof ModelNotFoundError) {
101
+ return json({ error: err.message }, 404)
102
+ }
103
+
104
+ if (err instanceof ConfigurationError) {
105
+ // Always show config errors — they're developer mistakes
106
+ return json({ error: err.message }, 500)
107
+ }
108
+
109
+ if (err instanceof ExternalServiceError) {
110
+ return json({ error: 'Service unavailable' }, 502)
111
+ }
112
+
113
+ // Unknown error
114
+ if (this.isDev) {
115
+ return json({ error: err.message, stack: err.stack }, 500)
116
+ }
117
+ return json({ error: 'Internal Server Error' }, 500)
118
+ }
119
+ }
120
+
121
+ function json(data: unknown, status: number): Response {
122
+ return new Response(JSON.stringify(data), {
123
+ status,
124
+ headers: { 'Content-Type': 'application/json' },
125
+ })
126
+ }
@@ -0,0 +1,25 @@
1
+ import { HttpException, ValidationError } from './http_exception.ts'
2
+
3
+ /**
4
+ * Throw an HTTP exception. Caught by the ExceptionHandler and rendered
5
+ * as a JSON error response.
6
+ *
7
+ * @example
8
+ * import { abort } from '@stravigor/core/exceptions'
9
+ *
10
+ * const project = await Project.find(id)
11
+ * if (!project) abort(404, 'Project not found')
12
+ *
13
+ * abort(403, 'You cannot access this resource')
14
+ *
15
+ * // Validation errors with structured field data
16
+ * abort(422, { email: ['Required'], name: ['Too short'] })
17
+ */
18
+ export function abort(status: 422, errors: Record<string, string[]>): never
19
+ export function abort(status: number, message?: string): never
20
+ export function abort(status: number, messageOrErrors?: string | Record<string, string[]>): never {
21
+ if (status === 422 && typeof messageOrErrors === 'object') {
22
+ throw new ValidationError(messageOrErrors)
23
+ }
24
+ throw new HttpException(status, typeof messageOrErrors === 'string' ? messageOrErrors : undefined)
25
+ }
@@ -0,0 +1,129 @@
1
+ import { StravError } from './strav_error.ts'
2
+
3
+ const STATUS_MESSAGES: Record<number, string> = {
4
+ 400: 'Bad Request',
5
+ 401: 'Unauthenticated',
6
+ 403: 'Forbidden',
7
+ 404: 'Not Found',
8
+ 409: 'Conflict',
9
+ 422: 'Validation Failed',
10
+ 429: 'Too Many Requests',
11
+ 500: 'Internal Server Error',
12
+ }
13
+
14
+ /**
15
+ * HTTP-aware exception. Throw from handlers or middleware to produce
16
+ * an error response with the given status code.
17
+ *
18
+ * @example
19
+ * throw new HttpException(403, 'You cannot access this resource')
20
+ */
21
+ export class HttpException extends StravError {
22
+ constructor(
23
+ public readonly status: number,
24
+ message?: string,
25
+ public readonly body?: unknown
26
+ ) {
27
+ super(message ?? STATUS_MESSAGES[status] ?? `HTTP Error ${status}`)
28
+ }
29
+ }
30
+
31
+ /**
32
+ * 400 Bad Request.
33
+ *
34
+ * @example
35
+ * throw new BadRequestError('Malformed JSON in request body')
36
+ */
37
+ export class BadRequestError extends HttpException {
38
+ constructor(message?: string) {
39
+ super(400, message)
40
+ }
41
+ }
42
+
43
+ /**
44
+ * 401 Unauthenticated.
45
+ *
46
+ * @example
47
+ * throw new AuthenticationError()
48
+ * throw new AuthenticationError('Token expired')
49
+ */
50
+ export class AuthenticationError extends HttpException {
51
+ constructor(message?: string) {
52
+ super(401, message)
53
+ }
54
+ }
55
+
56
+ /**
57
+ * 403 Forbidden.
58
+ *
59
+ * @example
60
+ * throw new AuthorizationError('You do not own this resource')
61
+ */
62
+ export class AuthorizationError extends HttpException {
63
+ constructor(message?: string) {
64
+ super(403, message)
65
+ }
66
+ }
67
+
68
+ /**
69
+ * 404 Not Found.
70
+ *
71
+ * @example
72
+ * throw new NotFoundError('Project not found')
73
+ */
74
+ export class NotFoundError extends HttpException {
75
+ constructor(message?: string) {
76
+ super(404, message)
77
+ }
78
+ }
79
+
80
+ /**
81
+ * 409 Conflict.
82
+ *
83
+ * @example
84
+ * throw new ConflictError('A project with this slug already exists')
85
+ */
86
+ export class ConflictError extends HttpException {
87
+ constructor(message?: string) {
88
+ super(409, message)
89
+ }
90
+ }
91
+
92
+ /**
93
+ * 422 Validation Failed — carries structured field errors.
94
+ *
95
+ * @example
96
+ * throw new ValidationError({ email: ['Required'], name: ['Too short'] })
97
+ */
98
+ export class ValidationError extends HttpException {
99
+ constructor(
100
+ public readonly errors: Record<string, string[]>,
101
+ message?: string
102
+ ) {
103
+ super(422, message ?? 'Validation Failed', errors)
104
+ }
105
+ }
106
+
107
+ /**
108
+ * 429 Too Many Requests — optionally carries a retry-after value.
109
+ *
110
+ * @example
111
+ * throw new RateLimitError(60) // retry after 60 seconds
112
+ */
113
+ export class RateLimitError extends HttpException {
114
+ constructor(public readonly retryAfter?: number, message?: string) {
115
+ super(429, message)
116
+ }
117
+ }
118
+
119
+ /**
120
+ * 500 Internal Server Error.
121
+ *
122
+ * @example
123
+ * throw new ServerError('Unexpected state in payment processor')
124
+ */
125
+ export class ServerError extends HttpException {
126
+ constructor(message?: string) {
127
+ super(500, message)
128
+ }
129
+ }
@@ -0,0 +1,23 @@
1
+ export { StravError } from './strav_error.ts'
2
+ export {
3
+ HttpException,
4
+ BadRequestError,
5
+ AuthenticationError,
6
+ AuthorizationError,
7
+ NotFoundError,
8
+ ConflictError,
9
+ ValidationError,
10
+ RateLimitError,
11
+ ServerError,
12
+ } from './http_exception.ts'
13
+ export {
14
+ ConfigurationError,
15
+ ModelNotFoundError,
16
+ DatabaseError,
17
+ EncryptionError,
18
+ TemplateError,
19
+ ExternalServiceError,
20
+ } from './errors.ts'
21
+ export { ExceptionHandler } from './exception_handler.ts'
22
+ export { abort } from './helpers.ts'
23
+ export type { RenderFn, ReportFn } from './exception_handler.ts'
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Base error class for all Strav framework errors.
3
+ *
4
+ * Enables `instanceof StravError` to catch any framework error.
5
+ */
6
+ export class StravError extends Error {
7
+ constructor(message: string) {
8
+ super(message)
9
+ this.name = this.constructor.name
10
+ }
11
+ }