@twelvehart/supermemory-runtime 1.0.0-next.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 (156) hide show
  1. package/.env.example +57 -0
  2. package/README.md +374 -0
  3. package/dist/index.js +189 -0
  4. package/dist/mcp/index.js +1132 -0
  5. package/docker-compose.prod.yml +91 -0
  6. package/docker-compose.yml +358 -0
  7. package/drizzle/0000_dapper_the_professor.sql +159 -0
  8. package/drizzle/0001_api_keys.sql +51 -0
  9. package/drizzle/meta/0000_snapshot.json +1532 -0
  10. package/drizzle/meta/_journal.json +13 -0
  11. package/drizzle.config.ts +20 -0
  12. package/package.json +114 -0
  13. package/scripts/add-extraction-job.ts +122 -0
  14. package/scripts/benchmark-pgvector.ts +122 -0
  15. package/scripts/bootstrap.sh +209 -0
  16. package/scripts/check-runtime-pack.ts +111 -0
  17. package/scripts/claude-mcp-config.ts +336 -0
  18. package/scripts/docker-entrypoint.sh +183 -0
  19. package/scripts/doctor.ts +377 -0
  20. package/scripts/init-db.sql +33 -0
  21. package/scripts/install.sh +1110 -0
  22. package/scripts/mcp-setup.ts +271 -0
  23. package/scripts/migrations/001_create_pgvector_extension.sql +31 -0
  24. package/scripts/migrations/002_create_memory_embeddings_table.sql +75 -0
  25. package/scripts/migrations/003_create_hnsw_index.sql +94 -0
  26. package/scripts/migrations/004_create_memory_embeddings_standalone.sql +70 -0
  27. package/scripts/migrations/005_create_chunks_table.sql +95 -0
  28. package/scripts/migrations/006_create_processing_queue.sql +45 -0
  29. package/scripts/migrations/generate_test_data.sql +42 -0
  30. package/scripts/migrations/phase1_comprehensive_test.sql +204 -0
  31. package/scripts/migrations/run_migrations.sh +286 -0
  32. package/scripts/migrations/test_hnsw_index.sql +255 -0
  33. package/scripts/pre-commit-secrets +282 -0
  34. package/scripts/run-extraction-worker.ts +46 -0
  35. package/scripts/run-phase1-tests.sh +291 -0
  36. package/scripts/setup.ts +222 -0
  37. package/scripts/smoke-install.sh +12 -0
  38. package/scripts/test-health-endpoint.sh +328 -0
  39. package/src/api/index.ts +2 -0
  40. package/src/api/middleware/auth.ts +80 -0
  41. package/src/api/middleware/csrf.ts +308 -0
  42. package/src/api/middleware/errorHandler.ts +166 -0
  43. package/src/api/middleware/rateLimit.ts +360 -0
  44. package/src/api/middleware/validation.ts +514 -0
  45. package/src/api/routes/documents.ts +286 -0
  46. package/src/api/routes/profiles.ts +237 -0
  47. package/src/api/routes/search.ts +71 -0
  48. package/src/api/stores/index.ts +58 -0
  49. package/src/config/bootstrap-env.ts +3 -0
  50. package/src/config/env.ts +71 -0
  51. package/src/config/feature-flags.ts +25 -0
  52. package/src/config/index.ts +140 -0
  53. package/src/config/secrets.config.ts +291 -0
  54. package/src/db/client.ts +92 -0
  55. package/src/db/index.ts +73 -0
  56. package/src/db/postgres.ts +72 -0
  57. package/src/db/schema/chunks.schema.ts +31 -0
  58. package/src/db/schema/containers.schema.ts +46 -0
  59. package/src/db/schema/documents.schema.ts +49 -0
  60. package/src/db/schema/embeddings.schema.ts +32 -0
  61. package/src/db/schema/index.ts +11 -0
  62. package/src/db/schema/memories.schema.ts +72 -0
  63. package/src/db/schema/profiles.schema.ts +34 -0
  64. package/src/db/schema/queue.schema.ts +59 -0
  65. package/src/db/schema/relationships.schema.ts +42 -0
  66. package/src/db/schema.ts +223 -0
  67. package/src/db/worker-connection.ts +47 -0
  68. package/src/index.ts +235 -0
  69. package/src/mcp/CLAUDE.md +1 -0
  70. package/src/mcp/index.ts +1380 -0
  71. package/src/mcp/legacyState.ts +22 -0
  72. package/src/mcp/rateLimit.ts +358 -0
  73. package/src/mcp/resources.ts +309 -0
  74. package/src/mcp/results.ts +104 -0
  75. package/src/mcp/tools.ts +401 -0
  76. package/src/queues/config.ts +119 -0
  77. package/src/queues/index.ts +289 -0
  78. package/src/sdk/client.ts +225 -0
  79. package/src/sdk/errors.ts +266 -0
  80. package/src/sdk/http.ts +560 -0
  81. package/src/sdk/index.ts +244 -0
  82. package/src/sdk/resources/base.ts +65 -0
  83. package/src/sdk/resources/connections.ts +204 -0
  84. package/src/sdk/resources/documents.ts +163 -0
  85. package/src/sdk/resources/index.ts +10 -0
  86. package/src/sdk/resources/memories.ts +150 -0
  87. package/src/sdk/resources/search.ts +60 -0
  88. package/src/sdk/resources/settings.ts +36 -0
  89. package/src/sdk/types.ts +674 -0
  90. package/src/services/chunking/index.ts +451 -0
  91. package/src/services/chunking.service.ts +650 -0
  92. package/src/services/csrf.service.ts +252 -0
  93. package/src/services/documents.repository.ts +219 -0
  94. package/src/services/documents.service.ts +191 -0
  95. package/src/services/embedding.service.ts +404 -0
  96. package/src/services/extraction.service.ts +300 -0
  97. package/src/services/extractors/code.extractor.ts +451 -0
  98. package/src/services/extractors/index.ts +9 -0
  99. package/src/services/extractors/markdown.extractor.ts +461 -0
  100. package/src/services/extractors/pdf.extractor.ts +315 -0
  101. package/src/services/extractors/text.extractor.ts +118 -0
  102. package/src/services/extractors/url.extractor.ts +243 -0
  103. package/src/services/index.ts +235 -0
  104. package/src/services/ingestion.service.ts +177 -0
  105. package/src/services/llm/anthropic.ts +400 -0
  106. package/src/services/llm/base.ts +460 -0
  107. package/src/services/llm/contradiction-detector.service.ts +526 -0
  108. package/src/services/llm/heuristics.ts +148 -0
  109. package/src/services/llm/index.ts +309 -0
  110. package/src/services/llm/memory-classifier.service.ts +383 -0
  111. package/src/services/llm/memory-extension-detector.service.ts +523 -0
  112. package/src/services/llm/mock.ts +470 -0
  113. package/src/services/llm/openai.ts +398 -0
  114. package/src/services/llm/prompts.ts +438 -0
  115. package/src/services/llm/types.ts +373 -0
  116. package/src/services/memory.repository.ts +1769 -0
  117. package/src/services/memory.service.ts +1338 -0
  118. package/src/services/memory.types.ts +234 -0
  119. package/src/services/persistence/index.ts +295 -0
  120. package/src/services/pipeline.service.ts +509 -0
  121. package/src/services/profile.repository.ts +436 -0
  122. package/src/services/profile.service.ts +560 -0
  123. package/src/services/profile.types.ts +270 -0
  124. package/src/services/relationships/detector.ts +1128 -0
  125. package/src/services/relationships/index.ts +268 -0
  126. package/src/services/relationships/memory-integration.ts +459 -0
  127. package/src/services/relationships/strategies.ts +132 -0
  128. package/src/services/relationships/types.ts +370 -0
  129. package/src/services/search.service.ts +761 -0
  130. package/src/services/search.types.ts +220 -0
  131. package/src/services/secrets.service.ts +384 -0
  132. package/src/services/vectorstore/base.ts +327 -0
  133. package/src/services/vectorstore/index.ts +444 -0
  134. package/src/services/vectorstore/memory.ts +286 -0
  135. package/src/services/vectorstore/migration.ts +295 -0
  136. package/src/services/vectorstore/mock.ts +403 -0
  137. package/src/services/vectorstore/pgvector.ts +695 -0
  138. package/src/services/vectorstore/types.ts +247 -0
  139. package/src/startup.ts +389 -0
  140. package/src/types/api.types.ts +193 -0
  141. package/src/types/document.types.ts +103 -0
  142. package/src/types/index.ts +241 -0
  143. package/src/types/profile.base.ts +133 -0
  144. package/src/utils/errors.ts +447 -0
  145. package/src/utils/id.ts +15 -0
  146. package/src/utils/index.ts +101 -0
  147. package/src/utils/logger.ts +313 -0
  148. package/src/utils/sanitization.ts +501 -0
  149. package/src/utils/secret-validation.ts +273 -0
  150. package/src/utils/synonyms.ts +188 -0
  151. package/src/utils/validation.ts +581 -0
  152. package/src/workers/chunking.worker.ts +242 -0
  153. package/src/workers/embedding.worker.ts +358 -0
  154. package/src/workers/extraction.worker.ts +346 -0
  155. package/src/workers/indexing.worker.ts +505 -0
  156. package/tsconfig.json +38 -0
@@ -0,0 +1,560 @@
1
+ /**
2
+ * Supermemory SDK HTTP Client
3
+ * Handles API requests with retry logic and error handling
4
+ */
5
+
6
+ import {
7
+ APIError,
8
+ APIConnectionError,
9
+ APIConnectionTimeoutError,
10
+ APIUserAbortError,
11
+ isRetryableError,
12
+ } from './errors.js'
13
+ import type { ClientOptions, RequestOptions, Uploadable, ToFileOptions, LogLevel, Logger } from './types.js'
14
+
15
+ // ============================================================================
16
+ // Constants
17
+ // ============================================================================
18
+
19
+ const DEFAULT_BASE_URL = 'https://api.supermemory.ai'
20
+ const DEFAULT_TIMEOUT = 60000 // 1 minute
21
+ const DEFAULT_MAX_RETRIES = 2
22
+
23
+ // ============================================================================
24
+ // Logger Implementation
25
+ // ============================================================================
26
+
27
+ const LOG_LEVELS: Record<LogLevel, number> = {
28
+ debug: 0,
29
+ info: 1,
30
+ warn: 2,
31
+ error: 3,
32
+ off: 4,
33
+ }
34
+
35
+ class DefaultLogger implements Logger {
36
+ private level: number
37
+
38
+ constructor(logLevel: LogLevel = 'warn') {
39
+ this.level = LOG_LEVELS[logLevel]
40
+ }
41
+
42
+ debug(message: string, ...args: unknown[]): void {
43
+ if (this.level <= LOG_LEVELS.debug) {
44
+ console.debug(`[supermemory:debug] ${message}`, ...args)
45
+ }
46
+ }
47
+
48
+ info(message: string, ...args: unknown[]): void {
49
+ if (this.level <= LOG_LEVELS.info) {
50
+ console.info(`[supermemory:info] ${message}`, ...args)
51
+ }
52
+ }
53
+
54
+ warn(message: string, ...args: unknown[]): void {
55
+ if (this.level <= LOG_LEVELS.warn) {
56
+ console.warn(`[supermemory:warn] ${message}`, ...args)
57
+ }
58
+ }
59
+
60
+ error(message: string, ...args: unknown[]): void {
61
+ if (this.level <= LOG_LEVELS.error) {
62
+ console.error(`[supermemory:error] ${message}`, ...args)
63
+ }
64
+ }
65
+ }
66
+
67
+ // ============================================================================
68
+ // API Promise Implementation
69
+ // ============================================================================
70
+
71
+ /**
72
+ * A promise that wraps API responses with additional methods
73
+ */
74
+ export class APIPromise<T> extends Promise<T> {
75
+ private _responsePromise: Promise<Response>
76
+ private _response?: Response
77
+
78
+ // Ensure Promise methods return regular Promises, not APIPromise instances
79
+ static override get [Symbol.species]() {
80
+ return Promise
81
+ }
82
+
83
+ constructor(responsePromise: Promise<Response>, parseResponse: (response: Response) => Promise<T>) {
84
+ // Handle the case where constructor is called with executor function
85
+ // (happens when Promise methods like .then() create new instances)
86
+ if (typeof responsePromise === 'function') {
87
+ // This is being called as a regular Promise with an executor
88
+ super(
89
+ responsePromise as unknown as (
90
+ resolve: (value: T | PromiseLike<T>) => void,
91
+ reject: (reason?: unknown) => void
92
+ ) => void
93
+ )
94
+ this._responsePromise = Promise.resolve(new Response())
95
+ return
96
+ }
97
+
98
+ let resolveOuter: (value: T | PromiseLike<T>) => void
99
+ let rejectOuter: (reason?: unknown) => void
100
+
101
+ super((resolve, reject) => {
102
+ resolveOuter = resolve
103
+ rejectOuter = reject
104
+ })
105
+
106
+ this._responsePromise = responsePromise
107
+
108
+ // Execute the promise chain using Promise.resolve to ensure proper async handling
109
+ Promise.resolve(responsePromise)
110
+ .then(async (response) => {
111
+ this._response = response
112
+ try {
113
+ const parsed = await parseResponse(response)
114
+ resolveOuter!(parsed)
115
+ } catch (err) {
116
+ rejectOuter!(err)
117
+ }
118
+ })
119
+ .catch((err) => {
120
+ rejectOuter!(err)
121
+ })
122
+ }
123
+
124
+ /**
125
+ * Get the raw Response object (available after headers are received)
126
+ */
127
+ asResponse(): Promise<Response> {
128
+ return this._responsePromise
129
+ }
130
+
131
+ /**
132
+ * Get both the parsed data and raw response
133
+ */
134
+ async withResponse(): Promise<{ data: T; response: Response }> {
135
+ const [data, response] = await Promise.all([this, this._responsePromise])
136
+ return { data, response }
137
+ }
138
+ }
139
+
140
+ // ============================================================================
141
+ // HTTP Client Implementation
142
+ // ============================================================================
143
+
144
+ export class HTTPClient {
145
+ private apiKey: string
146
+ private baseURL: string
147
+ private timeout: number
148
+ private maxRetries: number
149
+ private defaultHeaders: Record<string, string>
150
+ private defaultQuery: Record<string, string>
151
+ private fetchFn: typeof fetch
152
+ private fetchOptions: RequestInit
153
+ private logger: Logger
154
+
155
+ constructor(options: ClientOptions = {}) {
156
+ // Determine API key from options or environment
157
+ this.apiKey = options.apiKey || this.getEnvApiKey()
158
+ if (!this.apiKey) {
159
+ throw new Error('API key is required. Set SUPERMEMORY_API_KEY environment variable or pass apiKey option.')
160
+ }
161
+
162
+ this.baseURL = (options.baseURL || DEFAULT_BASE_URL).replace(/\/$/, '')
163
+ this.timeout = options.timeout ?? DEFAULT_TIMEOUT
164
+ this.maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES
165
+ this.defaultHeaders = options.defaultHeaders || {}
166
+ this.defaultQuery = options.defaultQuery || {}
167
+ this.fetchFn = options.fetch || globalThis.fetch
168
+ this.fetchOptions = options.fetchOptions || {}
169
+ this.logger = options.logger || new DefaultLogger(options.logLevel)
170
+ }
171
+
172
+ private getEnvApiKey(): string {
173
+ // Check various environments for the API key
174
+ if (typeof process !== 'undefined' && process.env) {
175
+ return process.env.SUPERMEMORY_API_KEY || ''
176
+ }
177
+ // Browser environment - check for global
178
+ if (typeof globalThis !== 'undefined') {
179
+ const global = globalThis as Record<string, unknown>
180
+ if (global.SUPERMEMORY_API_KEY) {
181
+ return String(global.SUPERMEMORY_API_KEY)
182
+ }
183
+ }
184
+ return ''
185
+ }
186
+
187
+ /**
188
+ * Build the full URL with query parameters
189
+ */
190
+ private buildURL(path: string, query?: Record<string, unknown>): string {
191
+ const url = new URL(path.startsWith('/') ? path : `/${path}`, this.baseURL)
192
+
193
+ // Add default query parameters
194
+ for (const [key, value] of Object.entries(this.defaultQuery)) {
195
+ url.searchParams.set(key, value)
196
+ }
197
+
198
+ // Add request-specific query parameters
199
+ if (query) {
200
+ for (const [key, value] of Object.entries(query)) {
201
+ if (value !== undefined && value !== null) {
202
+ if (Array.isArray(value)) {
203
+ value.forEach((v) => url.searchParams.append(key, String(v)))
204
+ } else {
205
+ url.searchParams.set(key, String(value))
206
+ }
207
+ }
208
+ }
209
+ }
210
+
211
+ return url.toString()
212
+ }
213
+
214
+ /**
215
+ * Build request headers
216
+ */
217
+ private buildHeaders(customHeaders?: Record<string, string>, hasBody?: boolean, isMultipart?: boolean): Headers {
218
+ const headers = new Headers()
219
+
220
+ // Set default headers
221
+ headers.set('Authorization', `Bearer ${this.apiKey}`)
222
+ headers.set('Accept', 'application/json')
223
+
224
+ if (hasBody && !isMultipart) {
225
+ headers.set('Content-Type', 'application/json')
226
+ }
227
+
228
+ // Add default headers from options
229
+ for (const [key, value] of Object.entries(this.defaultHeaders)) {
230
+ headers.set(key, value)
231
+ }
232
+
233
+ // Add custom headers
234
+ if (customHeaders) {
235
+ for (const [key, value] of Object.entries(customHeaders)) {
236
+ headers.set(key, value)
237
+ }
238
+ }
239
+
240
+ return headers
241
+ }
242
+
243
+ /**
244
+ * Execute a request with retry logic
245
+ */
246
+ private async executeWithRetry(
247
+ method: string,
248
+ path: string,
249
+ options: {
250
+ body?: unknown
251
+ query?: Record<string, unknown>
252
+ requestOptions?: RequestOptions
253
+ isMultipart?: boolean
254
+ } = {}
255
+ ): Promise<Response> {
256
+ const { body, query, requestOptions = {}, isMultipart } = options
257
+ const timeout = requestOptions.timeout ?? this.timeout
258
+ const maxRetries = requestOptions.maxRetries ?? this.maxRetries
259
+
260
+ const url = this.buildURL(path, query)
261
+ const headers = this.buildHeaders(requestOptions.headers, body !== undefined, isMultipart)
262
+
263
+ let lastError: Error | undefined
264
+
265
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
266
+ try {
267
+ this.logger.debug(`Request attempt ${attempt + 1}: ${method} ${url}`)
268
+
269
+ const controller = new AbortController()
270
+ const timeoutId = setTimeout(() => controller.abort(), timeout)
271
+
272
+ // Merge signals if provided
273
+ if (requestOptions.signal) {
274
+ requestOptions.signal.addEventListener('abort', () => controller.abort())
275
+ }
276
+
277
+ const requestInit: RequestInit = {
278
+ ...this.fetchOptions,
279
+ method,
280
+ headers,
281
+ signal: controller.signal,
282
+ }
283
+
284
+ if (body !== undefined) {
285
+ if (isMultipart && body instanceof FormData) {
286
+ requestInit.body = body
287
+ } else {
288
+ requestInit.body = JSON.stringify(body)
289
+ }
290
+ }
291
+
292
+ const response = await this.fetchFn(url, requestInit)
293
+ clearTimeout(timeoutId)
294
+
295
+ this.logger.debug(`Response: ${response.status} ${response.statusText}`)
296
+
297
+ // Return response for further processing
298
+ return response
299
+ } catch (err) {
300
+ lastError = err as Error
301
+
302
+ // Handle abort errors
303
+ if (err instanceof Error && err.name === 'AbortError') {
304
+ if (requestOptions.signal?.aborted) {
305
+ throw new APIUserAbortError('Request was aborted by user')
306
+ }
307
+ throw new APIConnectionTimeoutError({ message: 'Request timed out' })
308
+ }
309
+
310
+ // Check if we should retry
311
+ if (attempt < maxRetries && isRetryableError(lastError)) {
312
+ const delay = this.calculateRetryDelay(attempt, lastError)
313
+ this.logger.warn(`Request failed, retrying in ${delay}ms...`, lastError)
314
+ await this.sleep(delay)
315
+ continue
316
+ }
317
+
318
+ throw new APIConnectionError({
319
+ message: `Connection failed: ${lastError.message}`,
320
+ cause: lastError,
321
+ })
322
+ }
323
+ }
324
+
325
+ throw new APIConnectionError({
326
+ message: `Request failed after ${maxRetries + 1} attempts`,
327
+ cause: lastError,
328
+ })
329
+ }
330
+
331
+ /**
332
+ * Calculate retry delay with exponential backoff
333
+ */
334
+ private calculateRetryDelay(attempt: number, error?: Error): number {
335
+ // Base delay of 500ms, doubled for each attempt
336
+ let delay = 500 * Math.pow(2, attempt)
337
+
338
+ // Add jitter to prevent thundering herd
339
+ delay += Math.random() * 500
340
+
341
+ // Cap at 30 seconds
342
+ delay = Math.min(delay, 30000)
343
+
344
+ // Use retry-after header if available
345
+ if (error && 'retryAfter' in error) {
346
+ const retryAfter = (error as { retryAfter?: number }).retryAfter
347
+ if (retryAfter) {
348
+ delay = retryAfter * 1000
349
+ }
350
+ }
351
+
352
+ return delay
353
+ }
354
+
355
+ /**
356
+ * Sleep for the specified duration
357
+ */
358
+ private sleep(ms: number): Promise<void> {
359
+ return new Promise((resolve) => setTimeout(resolve, ms))
360
+ }
361
+
362
+ /**
363
+ * Parse a response and handle errors
364
+ */
365
+ private async parseResponse<T>(response: Response): Promise<T> {
366
+ // Handle non-2xx responses
367
+ if (!response.ok) {
368
+ let errorBody: unknown
369
+ try {
370
+ errorBody = await response.json()
371
+ } catch {
372
+ errorBody = await response.text().catch(() => undefined)
373
+ }
374
+
375
+ const message =
376
+ typeof errorBody === 'object' && errorBody !== null
377
+ ? (errorBody as Record<string, unknown>).message || (errorBody as Record<string, unknown>).error
378
+ : undefined
379
+
380
+ throw APIError.generate(
381
+ response.status,
382
+ errorBody,
383
+ typeof message === 'string' ? message : undefined,
384
+ response.headers
385
+ )
386
+ }
387
+
388
+ // Handle empty responses
389
+ if (response.status === 204 || response.headers.get('content-length') === '0') {
390
+ return undefined as T
391
+ }
392
+
393
+ // Parse JSON response
394
+ try {
395
+ return (await response.json()) as T
396
+ } catch {
397
+ throw new APIError(response.status, null, 'Failed to parse response as JSON', response.headers)
398
+ }
399
+ }
400
+
401
+ /**
402
+ * Make a GET request
403
+ */
404
+ get<T>(path: string, options?: { query?: Record<string, unknown>; requestOptions?: RequestOptions }): APIPromise<T> {
405
+ const responsePromise = this.executeWithRetry('GET', path, options)
406
+ return new APIPromise(responsePromise, (res) => this.parseResponse<T>(res))
407
+ }
408
+
409
+ /**
410
+ * Make a POST request
411
+ */
412
+ post<T>(
413
+ path: string,
414
+ options?: {
415
+ body?: unknown
416
+ query?: Record<string, unknown>
417
+ requestOptions?: RequestOptions
418
+ isMultipart?: boolean
419
+ }
420
+ ): APIPromise<T> {
421
+ const responsePromise = this.executeWithRetry('POST', path, options)
422
+ return new APIPromise(responsePromise, (res) => this.parseResponse<T>(res))
423
+ }
424
+
425
+ /**
426
+ * Make a PUT request
427
+ */
428
+ put<T>(
429
+ path: string,
430
+ options?: {
431
+ body?: unknown
432
+ query?: Record<string, unknown>
433
+ requestOptions?: RequestOptions
434
+ }
435
+ ): APIPromise<T> {
436
+ const responsePromise = this.executeWithRetry('PUT', path, options)
437
+ return new APIPromise(responsePromise, (res) => this.parseResponse<T>(res))
438
+ }
439
+
440
+ /**
441
+ * Make a PATCH request
442
+ */
443
+ patch<T>(
444
+ path: string,
445
+ options?: {
446
+ body?: unknown
447
+ query?: Record<string, unknown>
448
+ requestOptions?: RequestOptions
449
+ }
450
+ ): APIPromise<T> {
451
+ const responsePromise = this.executeWithRetry('PATCH', path, options)
452
+ return new APIPromise(responsePromise, (res) => this.parseResponse<T>(res))
453
+ }
454
+
455
+ /**
456
+ * Make a DELETE request
457
+ */
458
+ delete<T>(
459
+ path: string,
460
+ options?: {
461
+ body?: unknown
462
+ query?: Record<string, unknown>
463
+ requestOptions?: RequestOptions
464
+ }
465
+ ): APIPromise<T> {
466
+ const responsePromise = this.executeWithRetry('DELETE', path, options)
467
+ return new APIPromise(responsePromise, (res) => this.parseResponse<T>(res))
468
+ }
469
+
470
+ /**
471
+ * Upload a file
472
+ */
473
+ uploadFile<T>(
474
+ path: string,
475
+ file: Uploadable,
476
+ options?: {
477
+ fieldName?: string
478
+ filename?: string
479
+ additionalFields?: Record<string, string>
480
+ requestOptions?: RequestOptions
481
+ }
482
+ ): APIPromise<T> {
483
+ const formData = new FormData()
484
+ const fieldName = options?.fieldName || 'file'
485
+
486
+ // Convert various file types to Blob for FormData
487
+ if (file instanceof Blob || file instanceof File) {
488
+ formData.append(fieldName, file, options?.filename)
489
+ } else if (file instanceof ArrayBuffer) {
490
+ const blob = new Blob([file])
491
+ formData.append(fieldName, blob, options?.filename || 'file')
492
+ } else if (file instanceof Uint8Array) {
493
+ const blob = new Blob([new Uint8Array(file)])
494
+ formData.append(fieldName, blob, options?.filename || 'file')
495
+ } else if (typeof Buffer !== 'undefined' && Buffer.isBuffer(file)) {
496
+ const blob = new Blob([new Uint8Array(file)])
497
+ formData.append(fieldName, blob, options?.filename || 'file')
498
+ } else {
499
+ // For streams, we need to read them into a buffer first
500
+ throw new Error('Stream uploads are not supported in this context. Use toFile() to convert streams.')
501
+ }
502
+
503
+ // Add additional form fields
504
+ if (options?.additionalFields) {
505
+ for (const [key, value] of Object.entries(options.additionalFields)) {
506
+ formData.append(key, value)
507
+ }
508
+ }
509
+
510
+ const responsePromise = this.executeWithRetry('POST', path, {
511
+ body: formData,
512
+ isMultipart: true,
513
+ requestOptions: options?.requestOptions,
514
+ })
515
+
516
+ return new APIPromise(responsePromise, (res) => this.parseResponse<T>(res))
517
+ }
518
+ }
519
+
520
+ // ============================================================================
521
+ // Utility Functions
522
+ // ============================================================================
523
+
524
+ /**
525
+ * Convert various input types to a File object
526
+ */
527
+ export async function toFile(content: Uploadable | string, name?: string, options?: ToFileOptions): Promise<File> {
528
+ const filename = options?.filename || name || 'file'
529
+ const contentType = options?.contentType || 'application/octet-stream'
530
+
531
+ let blob: Blob
532
+
533
+ if (typeof content === 'string') {
534
+ // Assume it's a path or URL
535
+ if (typeof globalThis.fetch !== 'undefined' && content.startsWith('http')) {
536
+ const response = await fetch(content)
537
+ blob = await response.blob()
538
+ } else {
539
+ // In Node.js, would need to read file
540
+ throw new Error('File path reading not supported in browser environment')
541
+ }
542
+ } else if (content instanceof File) {
543
+ return content
544
+ } else if (content instanceof Blob) {
545
+ blob = content
546
+ } else if (content instanceof ArrayBuffer) {
547
+ blob = new Blob([content], { type: contentType })
548
+ } else if (content instanceof Uint8Array) {
549
+ blob = new Blob([new Uint8Array(content)], { type: contentType })
550
+ } else if (typeof Buffer !== 'undefined' && Buffer.isBuffer(content)) {
551
+ blob = new Blob([new Uint8Array(content)], { type: contentType })
552
+ } else {
553
+ throw new Error('Unsupported content type for toFile')
554
+ }
555
+
556
+ return new File([blob], filename, {
557
+ type: options?.contentType || blob.type || contentType,
558
+ lastModified: options?.lastModified,
559
+ })
560
+ }