@uploadista/server 0.0.3

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 (91) hide show
  1. package/.turbo/turbo-build.log +5 -0
  2. package/.turbo/turbo-check.log +34 -0
  3. package/LICENSE +21 -0
  4. package/README.md +503 -0
  5. package/dist/auth/cache.d.ts +87 -0
  6. package/dist/auth/cache.d.ts.map +1 -0
  7. package/dist/auth/cache.js +121 -0
  8. package/dist/auth/cache.test.d.ts +2 -0
  9. package/dist/auth/cache.test.d.ts.map +1 -0
  10. package/dist/auth/cache.test.js +209 -0
  11. package/dist/auth/get-auth-credentials.d.ts +73 -0
  12. package/dist/auth/get-auth-credentials.d.ts.map +1 -0
  13. package/dist/auth/get-auth-credentials.js +55 -0
  14. package/dist/auth/index.d.ts +2 -0
  15. package/dist/auth/index.d.ts.map +1 -0
  16. package/dist/auth/index.js +1 -0
  17. package/dist/auth/jwt/index.d.ts +38 -0
  18. package/dist/auth/jwt/index.d.ts.map +1 -0
  19. package/dist/auth/jwt/index.js +36 -0
  20. package/dist/auth/jwt/types.d.ts +77 -0
  21. package/dist/auth/jwt/types.d.ts.map +1 -0
  22. package/dist/auth/jwt/types.js +1 -0
  23. package/dist/auth/jwt/validate.d.ts +58 -0
  24. package/dist/auth/jwt/validate.d.ts.map +1 -0
  25. package/dist/auth/jwt/validate.js +226 -0
  26. package/dist/auth/jwt/validate.test.d.ts +2 -0
  27. package/dist/auth/jwt/validate.test.d.ts.map +1 -0
  28. package/dist/auth/jwt/validate.test.js +492 -0
  29. package/dist/auth/service.d.ts +63 -0
  30. package/dist/auth/service.d.ts.map +1 -0
  31. package/dist/auth/service.js +43 -0
  32. package/dist/auth/service.test.d.ts +2 -0
  33. package/dist/auth/service.test.d.ts.map +1 -0
  34. package/dist/auth/service.test.js +195 -0
  35. package/dist/auth/types.d.ts +38 -0
  36. package/dist/auth/types.d.ts.map +1 -0
  37. package/dist/auth/types.js +1 -0
  38. package/dist/cache.d.ts +87 -0
  39. package/dist/cache.d.ts.map +1 -0
  40. package/dist/cache.js +121 -0
  41. package/dist/cache.test.d.ts +2 -0
  42. package/dist/cache.test.d.ts.map +1 -0
  43. package/dist/cache.test.js +209 -0
  44. package/dist/cloudflare-config.d.ts +72 -0
  45. package/dist/cloudflare-config.d.ts.map +1 -0
  46. package/dist/cloudflare-config.js +67 -0
  47. package/dist/error-types.d.ts +138 -0
  48. package/dist/error-types.d.ts.map +1 -0
  49. package/dist/error-types.js +155 -0
  50. package/dist/hono-adapter.d.ts +48 -0
  51. package/dist/hono-adapter.d.ts.map +1 -0
  52. package/dist/hono-adapter.js +58 -0
  53. package/dist/http-utils.d.ts +148 -0
  54. package/dist/http-utils.d.ts.map +1 -0
  55. package/dist/http-utils.js +233 -0
  56. package/dist/index.d.ts +9 -0
  57. package/dist/index.d.ts.map +1 -0
  58. package/dist/index.js +8 -0
  59. package/dist/layer-utils.d.ts +121 -0
  60. package/dist/layer-utils.d.ts.map +1 -0
  61. package/dist/layer-utils.js +80 -0
  62. package/dist/metrics/service.d.ts +26 -0
  63. package/dist/metrics/service.d.ts.map +1 -0
  64. package/dist/metrics/service.js +20 -0
  65. package/dist/plugins-typing.d.ts +11 -0
  66. package/dist/plugins-typing.d.ts.map +1 -0
  67. package/dist/plugins-typing.js +1 -0
  68. package/dist/service.d.ts +63 -0
  69. package/dist/service.d.ts.map +1 -0
  70. package/dist/service.js +43 -0
  71. package/dist/service.test.d.ts +2 -0
  72. package/dist/service.test.d.ts.map +1 -0
  73. package/dist/service.test.js +195 -0
  74. package/dist/types.d.ts +38 -0
  75. package/dist/types.d.ts.map +1 -0
  76. package/dist/types.js +1 -0
  77. package/package.json +47 -0
  78. package/src/auth/get-auth-credentials.ts +97 -0
  79. package/src/auth/index.ts +1 -0
  80. package/src/cache.test.ts +306 -0
  81. package/src/cache.ts +204 -0
  82. package/src/error-types.ts +172 -0
  83. package/src/http-utils.ts +264 -0
  84. package/src/index.ts +8 -0
  85. package/src/layer-utils.ts +184 -0
  86. package/src/plugins-typing.ts +57 -0
  87. package/src/service.test.ts +275 -0
  88. package/src/service.ts +78 -0
  89. package/src/types.ts +40 -0
  90. package/tsconfig.json +13 -0
  91. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,306 @@
1
+ import { Effect } from "effect";
2
+ import { describe, expect, it } from "vitest";
3
+ import {
4
+ AuthCacheService,
5
+ AuthCacheServiceLive,
6
+ NoAuthCacheServiceLive,
7
+ } from "./cache";
8
+ import type { AuthContext } from "./types";
9
+
10
+ describe("AuthCacheService", () => {
11
+ describe("AuthCacheServiceLive", () => {
12
+ it("should set and get auth context", async () => {
13
+ const authContext: AuthContext = {
14
+ clientId: "user-123",
15
+ metadata: { role: "admin" },
16
+ };
17
+
18
+ const program = Effect.gen(function* () {
19
+ const cache = yield* AuthCacheService;
20
+
21
+ // Set auth context
22
+ yield* cache.set("job-1", authContext);
23
+
24
+ // Get auth context
25
+ const retrieved = yield* cache.get("job-1");
26
+
27
+ expect(retrieved).toEqual(authContext);
28
+ });
29
+
30
+ await Effect.runPromise(
31
+ program.pipe(Effect.provide(AuthCacheServiceLive())),
32
+ );
33
+ });
34
+
35
+ it("should return null for non-existent job ID", async () => {
36
+ const program = Effect.gen(function* () {
37
+ const cache = yield* AuthCacheService;
38
+
39
+ const retrieved = yield* cache.get("non-existent");
40
+
41
+ expect(retrieved).toBeNull();
42
+ });
43
+
44
+ await Effect.runPromise(
45
+ program.pipe(Effect.provide(AuthCacheServiceLive())),
46
+ );
47
+ });
48
+
49
+ it("should delete cached auth context", async () => {
50
+ const authContext: AuthContext = {
51
+ clientId: "user-123",
52
+ };
53
+
54
+ const program = Effect.gen(function* () {
55
+ const cache = yield* AuthCacheService;
56
+
57
+ // Set and verify
58
+ yield* cache.set("job-1", authContext);
59
+ const before = yield* cache.get("job-1");
60
+ expect(before).toEqual(authContext);
61
+
62
+ // Delete
63
+ yield* cache.delete("job-1");
64
+
65
+ // Verify deleted
66
+ const after = yield* cache.get("job-1");
67
+ expect(after).toBeNull();
68
+ });
69
+
70
+ await Effect.runPromise(
71
+ program.pipe(Effect.provide(AuthCacheServiceLive())),
72
+ );
73
+ });
74
+
75
+ it("should clear all cached auth contexts", async () => {
76
+ const authContext1: AuthContext = { clientId: "user-1" };
77
+ const authContext2: AuthContext = { clientId: "user-2" };
78
+
79
+ const program = Effect.gen(function* () {
80
+ const cache = yield* AuthCacheService;
81
+
82
+ // Set multiple entries
83
+ yield* cache.set("job-1", authContext1);
84
+ yield* cache.set("job-2", authContext2);
85
+
86
+ const sizeBefore = yield* cache.size();
87
+ expect(sizeBefore).toBe(2);
88
+
89
+ // Clear all
90
+ yield* cache.clear();
91
+
92
+ const sizeAfter = yield* cache.size();
93
+ expect(sizeAfter).toBe(0);
94
+
95
+ // Verify both deleted
96
+ const job1 = yield* cache.get("job-1");
97
+ const job2 = yield* cache.get("job-2");
98
+ expect(job1).toBeNull();
99
+ expect(job2).toBeNull();
100
+ });
101
+
102
+ await Effect.runPromise(
103
+ program.pipe(Effect.provide(AuthCacheServiceLive())),
104
+ );
105
+ });
106
+
107
+ it("should track cache size", async () => {
108
+ const program = Effect.gen(function* () {
109
+ const cache = yield* AuthCacheService;
110
+
111
+ let size = yield* cache.size();
112
+ expect(size).toBe(0);
113
+
114
+ yield* cache.set("job-1", { clientId: "user-1" });
115
+ size = yield* cache.size();
116
+ expect(size).toBe(1);
117
+
118
+ yield* cache.set("job-2", { clientId: "user-2" });
119
+ size = yield* cache.size();
120
+ expect(size).toBe(2);
121
+
122
+ yield* cache.delete("job-1");
123
+ size = yield* cache.size();
124
+ expect(size).toBe(1);
125
+
126
+ yield* cache.clear();
127
+ size = yield* cache.size();
128
+ expect(size).toBe(0);
129
+ });
130
+
131
+ await Effect.runPromise(
132
+ program.pipe(Effect.provide(AuthCacheServiceLive())),
133
+ );
134
+ });
135
+
136
+ it("should evict expired entries", async () => {
137
+ // Set very short TTL for testing
138
+ const shortTtl = 50; // 50ms
139
+
140
+ const authContext: AuthContext = {
141
+ clientId: "user-123",
142
+ };
143
+
144
+ const program = Effect.gen(function* () {
145
+ const cache = yield* AuthCacheService;
146
+
147
+ // Set auth context
148
+ yield* cache.set("job-1", authContext);
149
+
150
+ // Verify it's there immediately
151
+ const immediate = yield* cache.get("job-1");
152
+ expect(immediate).toEqual(authContext);
153
+
154
+ // Wait for TTL to expire
155
+ yield* Effect.sleep(100); // Wait 100ms (longer than 50ms TTL)
156
+
157
+ // Should be evicted now
158
+ const afterExpiry = yield* cache.get("job-1");
159
+ expect(afterExpiry).toBeNull();
160
+ });
161
+
162
+ await Effect.runPromise(
163
+ program.pipe(Effect.provide(AuthCacheServiceLive({ ttl: shortTtl }))),
164
+ );
165
+ });
166
+
167
+ it("should enforce max size limit with LRU eviction", async () => {
168
+ const maxSize = 3;
169
+
170
+ const program = Effect.gen(function* () {
171
+ const cache = yield* AuthCacheService;
172
+
173
+ // Add entries up to max size
174
+ yield* cache.set("job-1", { clientId: "user-1" });
175
+ yield* Effect.sleep(10); // Small delay to ensure ordering
176
+ yield* cache.set("job-2", { clientId: "user-2" });
177
+ yield* Effect.sleep(10);
178
+ yield* cache.set("job-3", { clientId: "user-3" });
179
+
180
+ let size = yield* cache.size();
181
+ expect(size).toBe(3);
182
+
183
+ // Add one more - should evict oldest (job-1)
184
+ yield* Effect.sleep(10);
185
+ yield* cache.set("job-4", { clientId: "user-4" });
186
+
187
+ size = yield* cache.size();
188
+ expect(size).toBe(3); // Still at max size
189
+
190
+ // job-1 should be evicted (oldest)
191
+ const job1 = yield* cache.get("job-1");
192
+ expect(job1).toBeNull();
193
+
194
+ // Others should still exist
195
+ const job2 = yield* cache.get("job-2");
196
+ const job3 = yield* cache.get("job-3");
197
+ const job4 = yield* cache.get("job-4");
198
+ expect(job2).toEqual({ clientId: "user-2" });
199
+ expect(job3).toEqual({ clientId: "user-3" });
200
+ expect(job4).toEqual({ clientId: "user-4" });
201
+ });
202
+
203
+ await Effect.runPromise(
204
+ program.pipe(Effect.provide(AuthCacheServiceLive({ maxSize }))),
205
+ );
206
+ });
207
+
208
+ it("should handle multiple auth contexts independently", async () => {
209
+ const authContext1: AuthContext = {
210
+ clientId: "user-1",
211
+ metadata: { role: "admin" },
212
+ };
213
+ const authContext2: AuthContext = {
214
+ clientId: "user-2",
215
+ permissions: ["read", "write"],
216
+ };
217
+
218
+ const program = Effect.gen(function* () {
219
+ const cache = yield* AuthCacheService;
220
+
221
+ yield* cache.set("upload-123", authContext1);
222
+ yield* cache.set("flow-456", authContext2);
223
+
224
+ const upload = yield* cache.get("upload-123");
225
+ const flow = yield* cache.get("flow-456");
226
+
227
+ expect(upload).toEqual(authContext1);
228
+ expect(flow).toEqual(authContext2);
229
+ });
230
+
231
+ await Effect.runPromise(
232
+ program.pipe(Effect.provide(AuthCacheServiceLive())),
233
+ );
234
+ });
235
+
236
+ it("should update existing entry when setting same job ID", async () => {
237
+ const authContext1: AuthContext = { clientId: "user-1" };
238
+ const authContext2: AuthContext = { clientId: "user-2" };
239
+
240
+ const program = Effect.gen(function* () {
241
+ const cache = yield* AuthCacheService;
242
+
243
+ yield* cache.set("job-1", authContext1);
244
+ const first = yield* cache.get("job-1");
245
+ expect(first).toEqual(authContext1);
246
+
247
+ // Update with new auth context
248
+ yield* cache.set("job-1", authContext2);
249
+ const second = yield* cache.get("job-1");
250
+ expect(second).toEqual(authContext2);
251
+
252
+ // Should still be only 1 entry
253
+ const size = yield* cache.size();
254
+ expect(size).toBe(1);
255
+ });
256
+
257
+ await Effect.runPromise(
258
+ program.pipe(Effect.provide(AuthCacheServiceLive())),
259
+ );
260
+ });
261
+ });
262
+
263
+ describe("NoAuthCacheServiceLive", () => {
264
+ it("should not cache anything", async () => {
265
+ const authContext: AuthContext = {
266
+ clientId: "user-123",
267
+ };
268
+
269
+ const program = Effect.gen(function* () {
270
+ const cache = yield* AuthCacheService;
271
+
272
+ // Try to set
273
+ yield* cache.set("job-1", authContext);
274
+
275
+ // Should always return null
276
+ const retrieved = yield* cache.get("job-1");
277
+ expect(retrieved).toBeNull();
278
+
279
+ // Size should always be 0
280
+ const size = yield* cache.size();
281
+ expect(size).toBe(0);
282
+ });
283
+
284
+ await Effect.runPromise(
285
+ program.pipe(Effect.provide(NoAuthCacheServiceLive)),
286
+ );
287
+ });
288
+
289
+ it("should handle delete and clear without errors", async () => {
290
+ const program = Effect.gen(function* () {
291
+ const cache = yield* AuthCacheService;
292
+
293
+ // These should not throw
294
+ yield* cache.delete("non-existent");
295
+ yield* cache.clear();
296
+
297
+ const size = yield* cache.size();
298
+ expect(size).toBe(0);
299
+ });
300
+
301
+ await Effect.runPromise(
302
+ program.pipe(Effect.provide(NoAuthCacheServiceLive)),
303
+ );
304
+ });
305
+ });
306
+ });
package/src/cache.ts ADDED
@@ -0,0 +1,204 @@
1
+ import { Context, Effect, Layer } from "effect";
2
+ import type { AuthContext } from "./types";
3
+
4
+ /**
5
+ * Configuration options for the auth cache.
6
+ */
7
+ export type AuthCacheConfig = {
8
+ /**
9
+ * Maximum number of entries in the cache.
10
+ * When exceeded, oldest entries are removed (LRU eviction).
11
+ * @default 10000
12
+ */
13
+ maxSize?: number;
14
+
15
+ /**
16
+ * Time-to-live for cache entries in milliseconds.
17
+ * Entries older than this will be automatically evicted.
18
+ * @default 3600000 (1 hour)
19
+ */
20
+ ttl?: number;
21
+ };
22
+
23
+ /**
24
+ * Cache entry with auth context and timestamp.
25
+ */
26
+ type CacheEntry = {
27
+ authContext: AuthContext;
28
+ timestamp: number;
29
+ };
30
+
31
+ /**
32
+ * Auth Cache Service
33
+ *
34
+ * Provides caching of authentication contexts for upload and flow jobs.
35
+ * This allows subsequent operations (chunk uploads, flow continuations)
36
+ * to reuse the auth context from the initial request without re-authenticating.
37
+ *
38
+ * @example
39
+ * ```typescript
40
+ * import { Effect } from "effect";
41
+ * import { AuthCacheService } from "@uploadista/server";
42
+ *
43
+ * const handler = Effect.gen(function* () {
44
+ * const authCache = yield* AuthCacheService;
45
+ * const authContext = { userId: "user-123" };
46
+ *
47
+ * // Cache auth for upload
48
+ * yield* authCache.set("upload-abc", authContext);
49
+ *
50
+ * // Retrieve cached auth later
51
+ * const cached = yield* authCache.get("upload-abc");
52
+ * console.log(cached?.userId); // "user-123"
53
+ *
54
+ * // Clear when done
55
+ * yield* authCache.delete("upload-abc");
56
+ * });
57
+ * ```
58
+ */
59
+ export class AuthCacheService extends Context.Tag("AuthCacheService")<
60
+ AuthCacheService,
61
+ {
62
+ /**
63
+ * Store an auth context for a job ID.
64
+ */
65
+ readonly set: (
66
+ jobId: string,
67
+ authContext: AuthContext,
68
+ ) => Effect.Effect<void>;
69
+
70
+ /**
71
+ * Retrieve a cached auth context by job ID.
72
+ * Returns null if not found or expired.
73
+ */
74
+ readonly get: (jobId: string) => Effect.Effect<AuthContext | null>;
75
+
76
+ /**
77
+ * Delete a cached auth context by job ID.
78
+ */
79
+ readonly delete: (jobId: string) => Effect.Effect<void>;
80
+
81
+ /**
82
+ * Clear all cached auth contexts.
83
+ */
84
+ readonly clear: () => Effect.Effect<void>;
85
+
86
+ /**
87
+ * Get the current number of cached entries.
88
+ */
89
+ readonly size: () => Effect.Effect<number>;
90
+ }
91
+ >() {}
92
+
93
+ /**
94
+ * Creates an AuthCacheService Layer with in-memory storage.
95
+ *
96
+ * @param config - Optional configuration for cache behavior
97
+ * @returns Effect Layer providing AuthCacheService
98
+ */
99
+ export const AuthCacheServiceLive = (
100
+ config: AuthCacheConfig = {},
101
+ ): Layer.Layer<AuthCacheService> => {
102
+ const maxSize = config.maxSize ?? 10000;
103
+ const ttl = config.ttl ?? 3600000; // 1 hour default
104
+
105
+ // In-memory cache storage
106
+ const cache = new Map<string, CacheEntry>();
107
+
108
+ /**
109
+ * Evict expired entries based on TTL.
110
+ */
111
+ const evictExpired = (): void => {
112
+ const now = Date.now();
113
+ for (const [jobId, entry] of cache.entries()) {
114
+ if (now - entry.timestamp > ttl) {
115
+ cache.delete(jobId);
116
+ }
117
+ }
118
+ };
119
+
120
+ /**
121
+ * Enforce max size limit using LRU eviction.
122
+ * Removes oldest entry when cache exceeds max size.
123
+ */
124
+ const enforceSizeLimit = (): void => {
125
+ if (cache.size <= maxSize) return;
126
+
127
+ // Find and remove oldest entry
128
+ let oldestKey: string | null = null;
129
+ let oldestTime = Number.POSITIVE_INFINITY;
130
+
131
+ for (const [jobId, entry] of cache.entries()) {
132
+ if (entry.timestamp < oldestTime) {
133
+ oldestTime = entry.timestamp;
134
+ oldestKey = jobId;
135
+ }
136
+ }
137
+
138
+ if (oldestKey) {
139
+ cache.delete(oldestKey);
140
+ }
141
+ };
142
+
143
+ return Layer.succeed(AuthCacheService, {
144
+ set: (jobId: string, authContext: AuthContext) =>
145
+ Effect.sync(() => {
146
+ // Evict expired entries periodically
147
+ if (cache.size % 100 === 0) {
148
+ evictExpired();
149
+ }
150
+
151
+ cache.set(jobId, {
152
+ authContext,
153
+ timestamp: Date.now(),
154
+ });
155
+
156
+ // Enforce size limit after adding
157
+ enforceSizeLimit();
158
+ }),
159
+
160
+ get: (jobId: string) =>
161
+ Effect.sync(() => {
162
+ const entry = cache.get(jobId);
163
+ if (!entry) return null;
164
+
165
+ // Check if expired
166
+ const now = Date.now();
167
+ if (now - entry.timestamp > ttl) {
168
+ cache.delete(jobId);
169
+ return null;
170
+ }
171
+
172
+ return entry.authContext;
173
+ }),
174
+
175
+ delete: (jobId: string) =>
176
+ Effect.sync(() => {
177
+ cache.delete(jobId);
178
+ }),
179
+
180
+ clear: () =>
181
+ Effect.sync(() => {
182
+ cache.clear();
183
+ }),
184
+
185
+ size: () =>
186
+ Effect.sync(() => {
187
+ return cache.size;
188
+ }),
189
+ });
190
+ };
191
+
192
+ /**
193
+ * No-op implementation of AuthCacheService.
194
+ * Does not cache anything - all operations are no-ops.
195
+ * Used when caching is disabled or not needed.
196
+ */
197
+ export const NoAuthCacheServiceLive: Layer.Layer<AuthCacheService> =
198
+ Layer.succeed(AuthCacheService, {
199
+ set: () => Effect.void,
200
+ get: () => Effect.succeed(null),
201
+ delete: () => Effect.void,
202
+ clear: () => Effect.void,
203
+ size: () => Effect.succeed(0),
204
+ });
@@ -0,0 +1,172 @@
1
+ import type { UploadistaError } from "@uploadista/core/errors";
2
+
3
+ /**
4
+ * Base adapter error class for HTTP adapters.
5
+ * All adapter-specific errors should extend this class or one of its subclasses.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * throw new AdapterError("Something went wrong", 500, "INTERNAL_ERROR");
10
+ * ```
11
+ */
12
+ export class AdapterError extends Error {
13
+ constructor(
14
+ message: string,
15
+ public readonly statusCode: number = 500,
16
+ public readonly errorCode: string = "INTERNAL_ERROR",
17
+ ) {
18
+ super(message);
19
+ this.name = "AdapterError";
20
+ }
21
+ }
22
+
23
+ /**
24
+ * Validation error - indicates invalid request data or parameters.
25
+ * Returns HTTP 400 Bad Request status.
26
+ *
27
+ * @example
28
+ * ```typescript
29
+ * if (!isValidUploadId(id)) {
30
+ * throw new ValidationError("Invalid upload ID format");
31
+ * }
32
+ * ```
33
+ */
34
+ export class ValidationError extends AdapterError {
35
+ constructor(message: string) {
36
+ super(message, 400, "VALIDATION_ERROR");
37
+ this.name = "ValidationError";
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Not found error - indicates a requested resource does not exist.
43
+ * Returns HTTP 404 Not Found status.
44
+ *
45
+ * @example
46
+ * ```typescript
47
+ * if (!upload) {
48
+ * throw new NotFoundError("Upload");
49
+ * }
50
+ * ```
51
+ */
52
+ export class NotFoundError extends AdapterError {
53
+ constructor(resource: string) {
54
+ super(`${resource} not found`, 404, "NOT_FOUND");
55
+ this.name = "NotFoundError";
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Bad request error - indicates a malformed request.
61
+ * Returns HTTP 400 Bad Request status.
62
+ * Similar to ValidationError but for request structure issues.
63
+ *
64
+ * @example
65
+ * ```typescript
66
+ * try {
67
+ * const data = JSON.parse(body);
68
+ * } catch {
69
+ * throw new BadRequestError("Invalid JSON body");
70
+ * }
71
+ * ```
72
+ */
73
+ export class BadRequestError extends AdapterError {
74
+ constructor(message: string) {
75
+ super(message, 400, "BAD_REQUEST");
76
+ this.name = "BadRequestError";
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Creates a standardized error response object for AdapterError.
82
+ * Includes error message, error code, and ISO timestamp.
83
+ *
84
+ * @param error - The AdapterError to format
85
+ * @returns Standardized error response body
86
+ *
87
+ * @example
88
+ * ```typescript
89
+ * import { createErrorResponseBody } from "@uploadista/server";
90
+ *
91
+ * try {
92
+ * // ... operation
93
+ * } catch (err) {
94
+ * const errorResponse = createErrorResponseBody(err);
95
+ * res.status(err.statusCode).json(errorResponse);
96
+ * }
97
+ * ```
98
+ */
99
+ export const createErrorResponseBody = (error: AdapterError) => ({
100
+ error: error.message,
101
+ code: error.errorCode,
102
+ timestamp: new Date().toISOString(),
103
+ });
104
+
105
+ /**
106
+ * Creates a standardized error response body from UploadistaError.
107
+ * Formats core library errors for HTTP responses with optional details.
108
+ *
109
+ * @param error - The UploadistaError to format
110
+ * @returns Standardized error response body with error, code, timestamp, and optional details
111
+ *
112
+ * @example
113
+ * ```typescript
114
+ * import { createUploadistaErrorResponseBody } from "@uploadista/server";
115
+ *
116
+ * try {
117
+ * const result = yield* uploadServer.handleUpload(input);
118
+ * } catch (err) {
119
+ * if (err instanceof UploadistaError) {
120
+ * const errorResponse = createUploadistaErrorResponseBody(err);
121
+ * res.status(400).json(errorResponse);
122
+ * }
123
+ * }
124
+ * ```
125
+ */
126
+ export const createUploadistaErrorResponseBody = (error: UploadistaError) => {
127
+ const response: {
128
+ error: string;
129
+ code: string;
130
+ timestamp: string;
131
+ details?: unknown;
132
+ } = {
133
+ error: error.body,
134
+ code: error.code,
135
+ timestamp: new Date().toISOString(),
136
+ };
137
+
138
+ if (error.details !== undefined) {
139
+ response.details = error.details;
140
+ }
141
+
142
+ return response;
143
+ };
144
+
145
+ /**
146
+ * Creates a generic error response body for unknown/unexpected errors.
147
+ * Used as a fallback when error type cannot be determined.
148
+ *
149
+ * @param message - Error message to include in response (defaults to "Internal server error")
150
+ * @returns Standardized error response body with generic INTERNAL_ERROR code
151
+ *
152
+ * @example
153
+ * ```typescript
154
+ * import { createGenericErrorResponseBody } from "@uploadista/server";
155
+ *
156
+ * try {
157
+ * // ... operation
158
+ * } catch (err) {
159
+ * const errorResponse = createGenericErrorResponseBody(
160
+ * err instanceof Error ? err.message : "Unknown error"
161
+ * );
162
+ * res.status(500).json(errorResponse);
163
+ * }
164
+ * ```
165
+ */
166
+ export const createGenericErrorResponseBody = (
167
+ message = "Internal server error",
168
+ ) => ({
169
+ error: message,
170
+ code: "INTERNAL_ERROR",
171
+ timestamp: new Date().toISOString(),
172
+ });