@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.
- package/.env.example +57 -0
- package/README.md +374 -0
- package/dist/index.js +189 -0
- package/dist/mcp/index.js +1132 -0
- package/docker-compose.prod.yml +91 -0
- package/docker-compose.yml +358 -0
- package/drizzle/0000_dapper_the_professor.sql +159 -0
- package/drizzle/0001_api_keys.sql +51 -0
- package/drizzle/meta/0000_snapshot.json +1532 -0
- package/drizzle/meta/_journal.json +13 -0
- package/drizzle.config.ts +20 -0
- package/package.json +114 -0
- package/scripts/add-extraction-job.ts +122 -0
- package/scripts/benchmark-pgvector.ts +122 -0
- package/scripts/bootstrap.sh +209 -0
- package/scripts/check-runtime-pack.ts +111 -0
- package/scripts/claude-mcp-config.ts +336 -0
- package/scripts/docker-entrypoint.sh +183 -0
- package/scripts/doctor.ts +377 -0
- package/scripts/init-db.sql +33 -0
- package/scripts/install.sh +1110 -0
- package/scripts/mcp-setup.ts +271 -0
- package/scripts/migrations/001_create_pgvector_extension.sql +31 -0
- package/scripts/migrations/002_create_memory_embeddings_table.sql +75 -0
- package/scripts/migrations/003_create_hnsw_index.sql +94 -0
- package/scripts/migrations/004_create_memory_embeddings_standalone.sql +70 -0
- package/scripts/migrations/005_create_chunks_table.sql +95 -0
- package/scripts/migrations/006_create_processing_queue.sql +45 -0
- package/scripts/migrations/generate_test_data.sql +42 -0
- package/scripts/migrations/phase1_comprehensive_test.sql +204 -0
- package/scripts/migrations/run_migrations.sh +286 -0
- package/scripts/migrations/test_hnsw_index.sql +255 -0
- package/scripts/pre-commit-secrets +282 -0
- package/scripts/run-extraction-worker.ts +46 -0
- package/scripts/run-phase1-tests.sh +291 -0
- package/scripts/setup.ts +222 -0
- package/scripts/smoke-install.sh +12 -0
- package/scripts/test-health-endpoint.sh +328 -0
- package/src/api/index.ts +2 -0
- package/src/api/middleware/auth.ts +80 -0
- package/src/api/middleware/csrf.ts +308 -0
- package/src/api/middleware/errorHandler.ts +166 -0
- package/src/api/middleware/rateLimit.ts +360 -0
- package/src/api/middleware/validation.ts +514 -0
- package/src/api/routes/documents.ts +286 -0
- package/src/api/routes/profiles.ts +237 -0
- package/src/api/routes/search.ts +71 -0
- package/src/api/stores/index.ts +58 -0
- package/src/config/bootstrap-env.ts +3 -0
- package/src/config/env.ts +71 -0
- package/src/config/feature-flags.ts +25 -0
- package/src/config/index.ts +140 -0
- package/src/config/secrets.config.ts +291 -0
- package/src/db/client.ts +92 -0
- package/src/db/index.ts +73 -0
- package/src/db/postgres.ts +72 -0
- package/src/db/schema/chunks.schema.ts +31 -0
- package/src/db/schema/containers.schema.ts +46 -0
- package/src/db/schema/documents.schema.ts +49 -0
- package/src/db/schema/embeddings.schema.ts +32 -0
- package/src/db/schema/index.ts +11 -0
- package/src/db/schema/memories.schema.ts +72 -0
- package/src/db/schema/profiles.schema.ts +34 -0
- package/src/db/schema/queue.schema.ts +59 -0
- package/src/db/schema/relationships.schema.ts +42 -0
- package/src/db/schema.ts +223 -0
- package/src/db/worker-connection.ts +47 -0
- package/src/index.ts +235 -0
- package/src/mcp/CLAUDE.md +1 -0
- package/src/mcp/index.ts +1380 -0
- package/src/mcp/legacyState.ts +22 -0
- package/src/mcp/rateLimit.ts +358 -0
- package/src/mcp/resources.ts +309 -0
- package/src/mcp/results.ts +104 -0
- package/src/mcp/tools.ts +401 -0
- package/src/queues/config.ts +119 -0
- package/src/queues/index.ts +289 -0
- package/src/sdk/client.ts +225 -0
- package/src/sdk/errors.ts +266 -0
- package/src/sdk/http.ts +560 -0
- package/src/sdk/index.ts +244 -0
- package/src/sdk/resources/base.ts +65 -0
- package/src/sdk/resources/connections.ts +204 -0
- package/src/sdk/resources/documents.ts +163 -0
- package/src/sdk/resources/index.ts +10 -0
- package/src/sdk/resources/memories.ts +150 -0
- package/src/sdk/resources/search.ts +60 -0
- package/src/sdk/resources/settings.ts +36 -0
- package/src/sdk/types.ts +674 -0
- package/src/services/chunking/index.ts +451 -0
- package/src/services/chunking.service.ts +650 -0
- package/src/services/csrf.service.ts +252 -0
- package/src/services/documents.repository.ts +219 -0
- package/src/services/documents.service.ts +191 -0
- package/src/services/embedding.service.ts +404 -0
- package/src/services/extraction.service.ts +300 -0
- package/src/services/extractors/code.extractor.ts +451 -0
- package/src/services/extractors/index.ts +9 -0
- package/src/services/extractors/markdown.extractor.ts +461 -0
- package/src/services/extractors/pdf.extractor.ts +315 -0
- package/src/services/extractors/text.extractor.ts +118 -0
- package/src/services/extractors/url.extractor.ts +243 -0
- package/src/services/index.ts +235 -0
- package/src/services/ingestion.service.ts +177 -0
- package/src/services/llm/anthropic.ts +400 -0
- package/src/services/llm/base.ts +460 -0
- package/src/services/llm/contradiction-detector.service.ts +526 -0
- package/src/services/llm/heuristics.ts +148 -0
- package/src/services/llm/index.ts +309 -0
- package/src/services/llm/memory-classifier.service.ts +383 -0
- package/src/services/llm/memory-extension-detector.service.ts +523 -0
- package/src/services/llm/mock.ts +470 -0
- package/src/services/llm/openai.ts +398 -0
- package/src/services/llm/prompts.ts +438 -0
- package/src/services/llm/types.ts +373 -0
- package/src/services/memory.repository.ts +1769 -0
- package/src/services/memory.service.ts +1338 -0
- package/src/services/memory.types.ts +234 -0
- package/src/services/persistence/index.ts +295 -0
- package/src/services/pipeline.service.ts +509 -0
- package/src/services/profile.repository.ts +436 -0
- package/src/services/profile.service.ts +560 -0
- package/src/services/profile.types.ts +270 -0
- package/src/services/relationships/detector.ts +1128 -0
- package/src/services/relationships/index.ts +268 -0
- package/src/services/relationships/memory-integration.ts +459 -0
- package/src/services/relationships/strategies.ts +132 -0
- package/src/services/relationships/types.ts +370 -0
- package/src/services/search.service.ts +761 -0
- package/src/services/search.types.ts +220 -0
- package/src/services/secrets.service.ts +384 -0
- package/src/services/vectorstore/base.ts +327 -0
- package/src/services/vectorstore/index.ts +444 -0
- package/src/services/vectorstore/memory.ts +286 -0
- package/src/services/vectorstore/migration.ts +295 -0
- package/src/services/vectorstore/mock.ts +403 -0
- package/src/services/vectorstore/pgvector.ts +695 -0
- package/src/services/vectorstore/types.ts +247 -0
- package/src/startup.ts +389 -0
- package/src/types/api.types.ts +193 -0
- package/src/types/document.types.ts +103 -0
- package/src/types/index.ts +241 -0
- package/src/types/profile.base.ts +133 -0
- package/src/utils/errors.ts +447 -0
- package/src/utils/id.ts +15 -0
- package/src/utils/index.ts +101 -0
- package/src/utils/logger.ts +313 -0
- package/src/utils/sanitization.ts +501 -0
- package/src/utils/secret-validation.ts +273 -0
- package/src/utils/synonyms.ts +188 -0
- package/src/utils/validation.ts +581 -0
- package/src/workers/chunking.worker.ts +242 -0
- package/src/workers/embedding.worker.ts +358 -0
- package/src/workers/extraction.worker.ts +346 -0
- package/src/workers/indexing.worker.ts +505 -0
- package/tsconfig.json +38 -0
package/src/sdk/http.ts
ADDED
|
@@ -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
|
+
}
|