@vobase/core 0.9.0 → 0.11.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 (188) hide show
  1. package/package.json +9 -10
  2. package/src/__tests__/drizzle-introspection.test.ts +77 -0
  3. package/src/__tests__/e2e.test.ts +225 -0
  4. package/src/__tests__/permissions.test.ts +157 -0
  5. package/src/__tests__/rpc-types.test.ts +92 -0
  6. package/src/app.test.ts +99 -0
  7. package/src/app.ts +178 -0
  8. package/src/audit.test.ts +126 -0
  9. package/src/auth.test.ts +74 -0
  10. package/src/contracts/auth.ts +37 -0
  11. package/{dist/contracts/module.d.ts → src/contracts/module.ts} +6 -6
  12. package/src/contracts/notify.ts +47 -0
  13. package/src/contracts/permissions.ts +10 -0
  14. package/src/contracts/storage.ts +61 -0
  15. package/src/ctx.test.ts +162 -0
  16. package/src/ctx.ts +64 -0
  17. package/src/db/client.test.ts +75 -0
  18. package/src/db/client.ts +15 -0
  19. package/src/db/helpers.test.ts +147 -0
  20. package/src/db/helpers.ts +51 -0
  21. package/src/db/index.ts +8 -0
  22. package/{dist/index.d.ts → src/index.ts} +105 -6
  23. package/src/infra/circuit-breaker.test.ts +74 -0
  24. package/src/infra/circuit-breaker.ts +57 -0
  25. package/src/infra/errors.test.ts +175 -0
  26. package/src/infra/errors.ts +64 -0
  27. package/src/infra/http-client.test.ts +482 -0
  28. package/src/infra/http-client.ts +221 -0
  29. package/src/infra/index.ts +35 -0
  30. package/src/infra/job.test.ts +85 -0
  31. package/src/infra/job.ts +94 -0
  32. package/src/infra/logger.test.ts +65 -0
  33. package/src/infra/logger.ts +18 -0
  34. package/src/infra/queue.test.ts +46 -0
  35. package/src/infra/queue.ts +147 -0
  36. package/src/infra/throw-proxy.test.ts +34 -0
  37. package/src/infra/throw-proxy.ts +17 -0
  38. package/src/infra/webhooks-schema.ts +17 -0
  39. package/src/infra/webhooks.test.ts +364 -0
  40. package/src/infra/webhooks.ts +146 -0
  41. package/src/mcp/auth.test.ts +129 -0
  42. package/src/mcp/crud.test.ts +128 -0
  43. package/src/mcp/crud.ts +171 -0
  44. package/{dist/mcp/index.d.ts → src/mcp/index.ts} +0 -1
  45. package/src/mcp/server.test.ts +153 -0
  46. package/src/mcp/server.ts +178 -0
  47. package/src/middleware/audit.test.ts +169 -0
  48. package/src/module-registry.ts +18 -0
  49. package/src/module.test.ts +168 -0
  50. package/src/module.ts +111 -0
  51. package/src/modules/audit/index.ts +18 -0
  52. package/src/modules/audit/middleware.ts +33 -0
  53. package/src/modules/audit/schema.ts +35 -0
  54. package/src/modules/audit/track-changes.ts +70 -0
  55. package/src/modules/auth/audit-hooks.ts +74 -0
  56. package/src/modules/auth/index.ts +101 -0
  57. package/src/modules/auth/middleware.ts +51 -0
  58. package/src/modules/auth/permissions.ts +46 -0
  59. package/src/modules/auth/schema.ts +184 -0
  60. package/src/modules/credentials/encrypt.ts +95 -0
  61. package/src/modules/credentials/index.ts +15 -0
  62. package/src/modules/credentials/schema.ts +10 -0
  63. package/src/modules/notify/index.ts +90 -0
  64. package/src/modules/notify/notify.test.ts +145 -0
  65. package/src/modules/notify/providers/resend.ts +47 -0
  66. package/src/modules/notify/providers/smtp.ts +117 -0
  67. package/src/modules/notify/providers/waba.ts +82 -0
  68. package/src/modules/notify/schema.ts +27 -0
  69. package/src/modules/notify/service.ts +93 -0
  70. package/src/modules/sequences/index.ts +15 -0
  71. package/src/modules/sequences/next-sequence.ts +48 -0
  72. package/src/modules/sequences/schema.ts +12 -0
  73. package/src/modules/storage/index.ts +44 -0
  74. package/src/modules/storage/providers/local.ts +124 -0
  75. package/src/modules/storage/providers/s3.ts +83 -0
  76. package/src/modules/storage/routes.ts +76 -0
  77. package/src/modules/storage/schema.ts +26 -0
  78. package/src/modules/storage/service.ts +202 -0
  79. package/src/modules/storage/storage.test.ts +225 -0
  80. package/src/schemas.test.ts +44 -0
  81. package/src/schemas.ts +63 -0
  82. package/src/sequence.test.ts +56 -0
  83. package/dist/app.d.ts +0 -37
  84. package/dist/app.d.ts.map +0 -1
  85. package/dist/contracts/auth.d.ts +0 -35
  86. package/dist/contracts/auth.d.ts.map +0 -1
  87. package/dist/contracts/module.d.ts.map +0 -1
  88. package/dist/contracts/notify.d.ts +0 -46
  89. package/dist/contracts/notify.d.ts.map +0 -1
  90. package/dist/contracts/permissions.d.ts +0 -10
  91. package/dist/contracts/permissions.d.ts.map +0 -1
  92. package/dist/contracts/storage.d.ts +0 -54
  93. package/dist/contracts/storage.d.ts.map +0 -1
  94. package/dist/ctx.d.ts +0 -40
  95. package/dist/ctx.d.ts.map +0 -1
  96. package/dist/db/client.d.ts +0 -4
  97. package/dist/db/client.d.ts.map +0 -1
  98. package/dist/db/helpers.d.ts +0 -26
  99. package/dist/db/helpers.d.ts.map +0 -1
  100. package/dist/db/index.d.ts +0 -3
  101. package/dist/db/index.d.ts.map +0 -1
  102. package/dist/index.d.ts.map +0 -1
  103. package/dist/index.js +0 -89026
  104. package/dist/infra/circuit-breaker.d.ts +0 -17
  105. package/dist/infra/circuit-breaker.d.ts.map +0 -1
  106. package/dist/infra/errors.d.ts +0 -26
  107. package/dist/infra/errors.d.ts.map +0 -1
  108. package/dist/infra/http-client.d.ts +0 -31
  109. package/dist/infra/http-client.d.ts.map +0 -1
  110. package/dist/infra/index.d.ts +0 -11
  111. package/dist/infra/index.d.ts.map +0 -1
  112. package/dist/infra/job.d.ts +0 -14
  113. package/dist/infra/job.d.ts.map +0 -1
  114. package/dist/infra/logger.d.ts +0 -7
  115. package/dist/infra/logger.d.ts.map +0 -1
  116. package/dist/infra/queue.d.ts +0 -18
  117. package/dist/infra/queue.d.ts.map +0 -1
  118. package/dist/infra/throw-proxy.d.ts +0 -7
  119. package/dist/infra/throw-proxy.d.ts.map +0 -1
  120. package/dist/infra/webhooks-schema.d.ts +0 -60
  121. package/dist/infra/webhooks-schema.d.ts.map +0 -1
  122. package/dist/infra/webhooks.d.ts +0 -46
  123. package/dist/infra/webhooks.d.ts.map +0 -1
  124. package/dist/mcp/crud.d.ts +0 -12
  125. package/dist/mcp/crud.d.ts.map +0 -1
  126. package/dist/mcp/index.d.ts.map +0 -1
  127. package/dist/mcp/server.d.ts +0 -10
  128. package/dist/mcp/server.d.ts.map +0 -1
  129. package/dist/module-registry.d.ts +0 -3
  130. package/dist/module-registry.d.ts.map +0 -1
  131. package/dist/module.d.ts +0 -29
  132. package/dist/module.d.ts.map +0 -1
  133. package/dist/modules/audit/index.d.ts +0 -5
  134. package/dist/modules/audit/index.d.ts.map +0 -1
  135. package/dist/modules/audit/middleware.d.ts +0 -3
  136. package/dist/modules/audit/middleware.d.ts.map +0 -1
  137. package/dist/modules/audit/schema.d.ts +0 -247
  138. package/dist/modules/audit/schema.d.ts.map +0 -1
  139. package/dist/modules/audit/track-changes.d.ts +0 -3
  140. package/dist/modules/audit/track-changes.d.ts.map +0 -1
  141. package/dist/modules/auth/audit-hooks.d.ts +0 -6
  142. package/dist/modules/auth/audit-hooks.d.ts.map +0 -1
  143. package/dist/modules/auth/index.d.ts +0 -17
  144. package/dist/modules/auth/index.d.ts.map +0 -1
  145. package/dist/modules/auth/middleware.d.ts +0 -15
  146. package/dist/modules/auth/middleware.d.ts.map +0 -1
  147. package/dist/modules/auth/permissions.d.ts +0 -5
  148. package/dist/modules/auth/permissions.d.ts.map +0 -1
  149. package/dist/modules/auth/schema.d.ts +0 -2519
  150. package/dist/modules/auth/schema.d.ts.map +0 -1
  151. package/dist/modules/credentials/encrypt.d.ts +0 -12
  152. package/dist/modules/credentials/encrypt.d.ts.map +0 -1
  153. package/dist/modules/credentials/index.d.ts +0 -4
  154. package/dist/modules/credentials/index.d.ts.map +0 -1
  155. package/dist/modules/credentials/schema.d.ts +0 -56
  156. package/dist/modules/credentials/schema.d.ts.map +0 -1
  157. package/dist/modules/notify/index.d.ts +0 -36
  158. package/dist/modules/notify/index.d.ts.map +0 -1
  159. package/dist/modules/notify/providers/resend.d.ts +0 -7
  160. package/dist/modules/notify/providers/resend.d.ts.map +0 -1
  161. package/dist/modules/notify/providers/smtp.d.ts +0 -18
  162. package/dist/modules/notify/providers/smtp.d.ts.map +0 -1
  163. package/dist/modules/notify/providers/waba.d.ts +0 -12
  164. package/dist/modules/notify/providers/waba.d.ts.map +0 -1
  165. package/dist/modules/notify/schema.d.ts +0 -337
  166. package/dist/modules/notify/schema.d.ts.map +0 -1
  167. package/dist/modules/notify/service.d.ts +0 -22
  168. package/dist/modules/notify/service.d.ts.map +0 -1
  169. package/dist/modules/sequences/index.d.ts +0 -4
  170. package/dist/modules/sequences/index.d.ts.map +0 -1
  171. package/dist/modules/sequences/next-sequence.d.ts +0 -8
  172. package/dist/modules/sequences/next-sequence.d.ts.map +0 -1
  173. package/dist/modules/sequences/schema.d.ts +0 -72
  174. package/dist/modules/sequences/schema.d.ts.map +0 -1
  175. package/dist/modules/storage/index.d.ts +0 -24
  176. package/dist/modules/storage/index.d.ts.map +0 -1
  177. package/dist/modules/storage/providers/local.d.ts +0 -3
  178. package/dist/modules/storage/providers/local.d.ts.map +0 -1
  179. package/dist/modules/storage/providers/s3.d.ts +0 -3
  180. package/dist/modules/storage/providers/s3.d.ts.map +0 -1
  181. package/dist/modules/storage/routes.d.ts +0 -4
  182. package/dist/modules/storage/routes.d.ts.map +0 -1
  183. package/dist/modules/storage/schema.d.ts +0 -273
  184. package/dist/modules/storage/schema.d.ts.map +0 -1
  185. package/dist/modules/storage/service.d.ts +0 -35
  186. package/dist/modules/storage/service.d.ts.map +0 -1
  187. package/dist/schemas.d.ts +0 -19
  188. package/dist/schemas.d.ts.map +0 -1
@@ -0,0 +1,74 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { CircuitBreaker } from './circuit-breaker';
3
+
4
+ describe('CircuitBreaker', () => {
5
+ test('starts closed and allows requests', () => {
6
+ const cb = new CircuitBreaker({ threshold: 3, resetTimeout: 100 });
7
+ expect(cb.isOpen()).toBe(false);
8
+ expect(cb.isHalfOpen()).toBe(false);
9
+ });
10
+
11
+ test('opens after threshold failures', () => {
12
+ const cb = new CircuitBreaker({ threshold: 3, resetTimeout: 100 });
13
+ cb.recordFailure();
14
+ cb.recordFailure();
15
+ expect(cb.isOpen()).toBe(false);
16
+ cb.recordFailure();
17
+ expect(cb.isOpen()).toBe(true);
18
+ });
19
+
20
+ test('transitions to half-open after resetTimeout', async () => {
21
+ const cb = new CircuitBreaker({ threshold: 2, resetTimeout: 50 });
22
+ cb.recordFailure();
23
+ cb.recordFailure();
24
+ expect(cb.isOpen()).toBe(true);
25
+
26
+ await Bun.sleep(60);
27
+
28
+ expect(cb.isOpen()).toBe(false);
29
+ expect(cb.isHalfOpen()).toBe(true);
30
+ });
31
+
32
+ test('closes on success in half-open state', async () => {
33
+ const cb = new CircuitBreaker({ threshold: 2, resetTimeout: 50 });
34
+ cb.recordFailure();
35
+ cb.recordFailure();
36
+ expect(cb.isOpen()).toBe(true);
37
+
38
+ await Bun.sleep(60);
39
+ expect(cb.isHalfOpen()).toBe(true);
40
+
41
+ cb.recordSuccess();
42
+ expect(cb.isOpen()).toBe(false);
43
+ expect(cb.isHalfOpen()).toBe(false);
44
+ });
45
+
46
+ test('re-opens on failure in half-open state', async () => {
47
+ const cb = new CircuitBreaker({ threshold: 2, resetTimeout: 50 });
48
+ cb.recordFailure();
49
+ cb.recordFailure();
50
+
51
+ await Bun.sleep(60);
52
+ expect(cb.isHalfOpen()).toBe(true);
53
+
54
+ cb.recordFailure();
55
+ expect(cb.isOpen()).toBe(true);
56
+ expect(cb.isHalfOpen()).toBe(false);
57
+ });
58
+
59
+ test('resets failure count on success', () => {
60
+ const cb = new CircuitBreaker({ threshold: 3, resetTimeout: 100 });
61
+ cb.recordFailure();
62
+ cb.recordFailure();
63
+ expect(cb.isOpen()).toBe(false);
64
+
65
+ cb.recordSuccess();
66
+
67
+ // After reset, need threshold failures again to open
68
+ cb.recordFailure();
69
+ cb.recordFailure();
70
+ expect(cb.isOpen()).toBe(false);
71
+ cb.recordFailure();
72
+ expect(cb.isOpen()).toBe(true);
73
+ });
74
+ });
@@ -0,0 +1,57 @@
1
+ export interface CircuitBreakerOptions {
2
+ threshold: number; // failures before opening
3
+ resetTimeout: number; // ms before transitioning from open to half-open
4
+ }
5
+
6
+ type CircuitState = 'closed' | 'open' | 'half-open';
7
+
8
+ export class CircuitBreaker {
9
+ private state: CircuitState = 'closed';
10
+ private failures = 0;
11
+ private openedAt = 0;
12
+ private readonly threshold: number;
13
+ private readonly resetTimeout: number;
14
+
15
+ constructor(options: CircuitBreakerOptions) {
16
+ this.threshold = options.threshold;
17
+ this.resetTimeout = options.resetTimeout;
18
+ }
19
+
20
+ isOpen(): boolean {
21
+ if (this.state === 'open') {
22
+ // Check if we should transition to half-open
23
+ if (Date.now() - this.openedAt >= this.resetTimeout) {
24
+ this.state = 'half-open';
25
+ return false;
26
+ }
27
+ return true;
28
+ }
29
+ return false;
30
+ }
31
+
32
+ isHalfOpen(): boolean {
33
+ // isOpen() may transition state, so call it first
34
+ this.isOpen();
35
+ return this.state === 'half-open';
36
+ }
37
+
38
+ recordFailure(): void {
39
+ if (this.state === 'half-open') {
40
+ // Re-open immediately from half-open
41
+ this.state = 'open';
42
+ this.openedAt = Date.now();
43
+ return;
44
+ }
45
+
46
+ this.failures++;
47
+ if (this.failures >= this.threshold) {
48
+ this.state = 'open';
49
+ this.openedAt = Date.now();
50
+ }
51
+ }
52
+
53
+ recordSuccess(): void {
54
+ this.failures = 0;
55
+ this.state = 'closed';
56
+ }
57
+ }
@@ -0,0 +1,175 @@
1
+ import { describe, expect, it, mock } from 'bun:test';
2
+ import type { Context } from 'hono';
3
+
4
+ import {
5
+ conflict,
6
+ dbBusy,
7
+ ERROR_CODES,
8
+ errorHandler,
9
+ forbidden,
10
+ notFound,
11
+ unauthorized,
12
+ VobaseError,
13
+ validation,
14
+ } from './errors';
15
+
16
+ describe('VobaseError', () => {
17
+ it('should create an error with correct properties', () => {
18
+ const err = new VobaseError('Test error', 'INTERNAL', 500);
19
+ expect(err).toBeInstanceOf(VobaseError);
20
+ expect(err).toBeInstanceOf(Error);
21
+ expect(err.message).toBe('Test error');
22
+ expect(err.code).toBe('INTERNAL');
23
+ expect(err.statusCode).toBe(500);
24
+ expect(err.name).toBe('VobaseError');
25
+ });
26
+
27
+ it('should support details object', () => {
28
+ const details = { field: 'email', reason: 'invalid' };
29
+ const err = new VobaseError('Invalid', 'VALIDATION', 400, details);
30
+ expect(err.details).toEqual(details);
31
+ });
32
+ });
33
+
34
+ describe('Factory functions', () => {
35
+ it('unauthorized() should produce 401 error', () => {
36
+ const err = unauthorized();
37
+ expect(err).toBeInstanceOf(VobaseError);
38
+ expect(err.statusCode).toBe(401);
39
+ expect(err.code).toBe('UNAUTHORIZED');
40
+ expect(err.message).toBe('Unauthorized');
41
+ });
42
+
43
+ it('unauthorized() should accept custom message', () => {
44
+ const err = unauthorized('Access denied');
45
+ expect(err.message).toBe('Access denied');
46
+ });
47
+
48
+ it('forbidden() should produce 403 error', () => {
49
+ const err = forbidden();
50
+ expect(err.statusCode).toBe(403);
51
+ expect(err.code).toBe('FORBIDDEN');
52
+ });
53
+
54
+ it('notFound() should include resource name in message', () => {
55
+ const err = notFound('Invoice');
56
+ expect(err.statusCode).toBe(404);
57
+ expect(err.code).toBe('NOT_FOUND');
58
+ expect(err.message).toContain('Invoice');
59
+ expect(err.message).toBe('Invoice not found');
60
+ });
61
+
62
+ it('validation() should include details', () => {
63
+ const details = { field: 'email', reason: 'required' };
64
+ const err = validation(details);
65
+ expect(err.statusCode).toBe(400);
66
+ expect(err.code).toBe('VALIDATION');
67
+ expect(err.details).toEqual(details);
68
+ });
69
+
70
+ it('validation() should accept custom message', () => {
71
+ const err = validation({}, 'Custom validation error');
72
+ expect(err.message).toBe('Custom validation error');
73
+ });
74
+
75
+ it('conflict() should include resource name in message', () => {
76
+ const err = conflict('User');
77
+ expect(err.statusCode).toBe(409);
78
+ expect(err.code).toBe('CONFLICT');
79
+ expect(err.message).toContain('User');
80
+ expect(err.message).toBe('User already exists');
81
+ });
82
+
83
+ it('dbBusy() should produce 503 error', () => {
84
+ const err = dbBusy();
85
+ expect(err.statusCode).toBe(503);
86
+ expect(err.code).toBe('DB_BUSY');
87
+ });
88
+ });
89
+
90
+ describe('errorHandler', () => {
91
+ it('should handle VobaseError and return correct JSON', () => {
92
+ const mockContext = {
93
+ json: mock((data, status) => ({
94
+ data,
95
+ status,
96
+ })),
97
+ };
98
+
99
+ const err = notFound('Invoice');
100
+ errorHandler(err, mockContext as unknown as Context);
101
+
102
+ expect(mockContext.json).toHaveBeenCalledWith(
103
+ {
104
+ error: {
105
+ code: 'NOT_FOUND',
106
+ message: 'Invoice not found',
107
+ details: undefined,
108
+ },
109
+ },
110
+ 404,
111
+ );
112
+ });
113
+
114
+ it('should include details in VobaseError response', () => {
115
+ const mockContext = {
116
+ json: mock((data, status) => ({
117
+ data,
118
+ status,
119
+ })),
120
+ };
121
+
122
+ const details = { field: 'email' };
123
+ const err = validation(details, 'Invalid email');
124
+ errorHandler(err, mockContext as unknown as Context);
125
+
126
+ expect(mockContext.json).toHaveBeenCalledWith(
127
+ {
128
+ error: {
129
+ code: 'VALIDATION',
130
+ message: 'Invalid email',
131
+ details,
132
+ },
133
+ },
134
+ 400,
135
+ );
136
+ });
137
+
138
+ it('should handle unknown errors with 500', () => {
139
+ const mockContext = {
140
+ json: mock((data, status) => ({
141
+ data,
142
+ status,
143
+ })),
144
+ };
145
+
146
+ const consoleErrorMock = mock(() => {});
147
+ console.error = consoleErrorMock;
148
+
149
+ const err = new Error('Unknown error');
150
+ errorHandler(err, mockContext as unknown as Context);
151
+
152
+ expect(mockContext.json).toHaveBeenCalledWith(
153
+ {
154
+ error: {
155
+ code: 'INTERNAL',
156
+ message: 'Internal server error',
157
+ },
158
+ },
159
+ 500,
160
+ );
161
+ expect(consoleErrorMock).toHaveBeenCalled();
162
+ });
163
+ });
164
+
165
+ describe('ERROR_CODES', () => {
166
+ it('should have all required error codes', () => {
167
+ expect(ERROR_CODES.UNAUTHORIZED).toBe('UNAUTHORIZED');
168
+ expect(ERROR_CODES.FORBIDDEN).toBe('FORBIDDEN');
169
+ expect(ERROR_CODES.NOT_FOUND).toBe('NOT_FOUND');
170
+ expect(ERROR_CODES.VALIDATION).toBe('VALIDATION');
171
+ expect(ERROR_CODES.CONFLICT).toBe('CONFLICT');
172
+ expect(ERROR_CODES.INTERNAL).toBe('INTERNAL');
173
+ expect(ERROR_CODES.DB_BUSY).toBe('DB_BUSY');
174
+ });
175
+ });
@@ -0,0 +1,64 @@
1
+ import type { Context } from 'hono';
2
+ import type { ContentfulStatusCode } from 'hono/utils/http-status';
3
+
4
+ export const ERROR_CODES = {
5
+ UNAUTHORIZED: 'UNAUTHORIZED',
6
+ FORBIDDEN: 'FORBIDDEN',
7
+ NOT_FOUND: 'NOT_FOUND',
8
+ VALIDATION: 'VALIDATION',
9
+ CONFLICT: 'CONFLICT',
10
+ INTERNAL: 'INTERNAL',
11
+ DB_BUSY: 'DB_BUSY',
12
+ } as const;
13
+
14
+ export type ErrorCode = keyof typeof ERROR_CODES;
15
+
16
+ export class VobaseError extends Error {
17
+ constructor(
18
+ message: string,
19
+ public readonly code: ErrorCode,
20
+ public readonly statusCode: ContentfulStatusCode,
21
+ public readonly details?: object,
22
+ ) {
23
+ super(message);
24
+ this.name = 'VobaseError';
25
+ }
26
+ }
27
+
28
+ export const unauthorized = (message = 'Unauthorized') =>
29
+ new VobaseError(message, 'UNAUTHORIZED', 401);
30
+
31
+ export const forbidden = (message = 'Forbidden') =>
32
+ new VobaseError(message, 'FORBIDDEN', 403);
33
+
34
+ export const notFound = (resource: string) =>
35
+ new VobaseError(`${resource} not found`, 'NOT_FOUND', 404);
36
+
37
+ export const validation = (details: object, message = 'Validation failed') =>
38
+ new VobaseError(message, 'VALIDATION', 400, details);
39
+
40
+ export const conflict = (resource: string) =>
41
+ new VobaseError(`${resource} already exists`, 'CONFLICT', 409);
42
+
43
+ export const dbBusy = () =>
44
+ new VobaseError('Database busy, try again', 'DB_BUSY', 503);
45
+
46
+ export const errorHandler = (err: Error, c: Context): Response => {
47
+ if (err instanceof VobaseError) {
48
+ return c.json(
49
+ {
50
+ error: {
51
+ code: err.code,
52
+ message: err.message,
53
+ details: err.details,
54
+ },
55
+ },
56
+ err.statusCode,
57
+ );
58
+ }
59
+ console.error('Unhandled error:', err);
60
+ return c.json(
61
+ { error: { code: 'INTERNAL', message: 'Internal server error' } },
62
+ 500,
63
+ );
64
+ };