@debros/orama 0.122.4-nightly
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/LICENSE +21 -0
- package/README.md +665 -0
- package/dist/index.d.ts +1334 -0
- package/dist/index.js +2553 -0
- package/dist/index.js.map +1 -0
- package/package.json +82 -0
- package/src/auth/client.ts +276 -0
- package/src/auth/index.ts +3 -0
- package/src/auth/types.ts +62 -0
- package/src/cache/client.ts +203 -0
- package/src/cache/index.ts +14 -0
- package/src/core/http.ts +541 -0
- package/src/core/index.ts +10 -0
- package/src/core/interfaces/IAuthStrategy.ts +28 -0
- package/src/core/interfaces/IHttpTransport.ts +73 -0
- package/src/core/interfaces/IRetryPolicy.ts +20 -0
- package/src/core/interfaces/IWebSocketClient.ts +60 -0
- package/src/core/interfaces/index.ts +4 -0
- package/src/core/transport/AuthHeaderStrategy.ts +108 -0
- package/src/core/transport/RequestLogger.ts +116 -0
- package/src/core/transport/RequestRetryPolicy.ts +53 -0
- package/src/core/transport/TLSConfiguration.ts +53 -0
- package/src/core/transport/index.ts +4 -0
- package/src/core/ws.ts +246 -0
- package/src/db/client.ts +126 -0
- package/src/db/index.ts +13 -0
- package/src/db/qb.ts +111 -0
- package/src/db/repository.ts +128 -0
- package/src/db/types.ts +67 -0
- package/src/errors.ts +38 -0
- package/src/functions/client.ts +62 -0
- package/src/functions/index.ts +2 -0
- package/src/functions/types.ts +21 -0
- package/src/index.ts +201 -0
- package/src/network/client.ts +119 -0
- package/src/network/index.ts +7 -0
- package/src/pubsub/client.ts +361 -0
- package/src/pubsub/index.ts +12 -0
- package/src/pubsub/types.ts +46 -0
- package/src/storage/client.ts +272 -0
- package/src/storage/index.ts +7 -0
- package/src/utils/codec.ts +68 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/platform.ts +44 -0
- package/src/utils/retry.ts +58 -0
- package/src/vault/auth.ts +98 -0
- package/src/vault/client.ts +197 -0
- package/src/vault/crypto/aes.ts +271 -0
- package/src/vault/crypto/hkdf.ts +42 -0
- package/src/vault/crypto/index.ts +27 -0
- package/src/vault/crypto/shamir.ts +173 -0
- package/src/vault/index.ts +65 -0
- package/src/vault/quorum.ts +16 -0
- package/src/vault/transport/fanout.ts +94 -0
- package/src/vault/transport/guardian.ts +285 -0
- package/src/vault/transport/index.ts +19 -0
- package/src/vault/transport/types.ts +101 -0
- package/src/vault/types.ts +62 -0
package/src/core/http.ts
ADDED
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
import { SDKError } from "../errors";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Context provided to the onNetworkError callback
|
|
5
|
+
*/
|
|
6
|
+
export interface NetworkErrorContext {
|
|
7
|
+
method: "GET" | "POST" | "PUT" | "DELETE" | "WS";
|
|
8
|
+
path: string;
|
|
9
|
+
isRetry: boolean;
|
|
10
|
+
attempt: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Callback invoked when a network error occurs.
|
|
15
|
+
* Use this to trigger gateway failover or other error handling.
|
|
16
|
+
*/
|
|
17
|
+
export type NetworkErrorCallback = (
|
|
18
|
+
error: SDKError,
|
|
19
|
+
context: NetworkErrorContext
|
|
20
|
+
) => void;
|
|
21
|
+
|
|
22
|
+
export interface HttpClientConfig {
|
|
23
|
+
baseURL: string;
|
|
24
|
+
timeout?: number;
|
|
25
|
+
maxRetries?: number;
|
|
26
|
+
retryDelayMs?: number;
|
|
27
|
+
fetch?: typeof fetch;
|
|
28
|
+
/**
|
|
29
|
+
* Enable debug logging (includes full SQL queries and args). Default: false
|
|
30
|
+
*/
|
|
31
|
+
debug?: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Callback invoked on network errors (after all retries exhausted).
|
|
34
|
+
* Use this to trigger gateway failover at the application layer.
|
|
35
|
+
*/
|
|
36
|
+
onNetworkError?: NetworkErrorCallback;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Create a fetch function with proper TLS configuration for staging certificates
|
|
41
|
+
* In Node.js, we need to configure TLS to accept Let's Encrypt staging certificates
|
|
42
|
+
*/
|
|
43
|
+
function createFetchWithTLSConfig(): typeof fetch {
|
|
44
|
+
// Check if we're in a Node.js environment
|
|
45
|
+
if (typeof process !== "undefined" && process.versions?.node) {
|
|
46
|
+
// For testing/staging/development: allow staging certificates
|
|
47
|
+
// Let's Encrypt staging certificates are self-signed and not trusted by default
|
|
48
|
+
const isDevelopmentOrStaging =
|
|
49
|
+
process.env.NODE_ENV !== "production" ||
|
|
50
|
+
process.env.DEBROS_ALLOW_STAGING_CERTS === "true" ||
|
|
51
|
+
process.env.DEBROS_USE_HTTPS === "true";
|
|
52
|
+
|
|
53
|
+
if (isDevelopmentOrStaging) {
|
|
54
|
+
// Allow self-signed/staging certificates
|
|
55
|
+
// WARNING: Only use this in development/testing environments
|
|
56
|
+
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return globalThis.fetch;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export class HttpClient {
|
|
63
|
+
private baseURL: string;
|
|
64
|
+
private timeout: number;
|
|
65
|
+
private maxRetries: number;
|
|
66
|
+
private retryDelayMs: number;
|
|
67
|
+
private fetch: typeof fetch;
|
|
68
|
+
private apiKey?: string;
|
|
69
|
+
private jwt?: string;
|
|
70
|
+
private debug: boolean;
|
|
71
|
+
private onNetworkError?: NetworkErrorCallback;
|
|
72
|
+
|
|
73
|
+
constructor(config: HttpClientConfig) {
|
|
74
|
+
this.baseURL = config.baseURL.replace(/\/$/, "");
|
|
75
|
+
this.timeout = config.timeout ?? 60000;
|
|
76
|
+
this.maxRetries = config.maxRetries ?? 3;
|
|
77
|
+
this.retryDelayMs = config.retryDelayMs ?? 1000;
|
|
78
|
+
// Use provided fetch or create one with proper TLS configuration for staging certificates
|
|
79
|
+
this.fetch = config.fetch ?? createFetchWithTLSConfig();
|
|
80
|
+
this.debug = config.debug ?? false;
|
|
81
|
+
this.onNetworkError = config.onNetworkError;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Set the network error callback
|
|
86
|
+
*/
|
|
87
|
+
setOnNetworkError(callback: NetworkErrorCallback | undefined): void {
|
|
88
|
+
this.onNetworkError = callback;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
setApiKey(apiKey?: string) {
|
|
92
|
+
this.apiKey = apiKey;
|
|
93
|
+
// Don't clear JWT - allow both to coexist
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
setJwt(jwt?: string) {
|
|
97
|
+
this.jwt = jwt;
|
|
98
|
+
// Don't clear API key - allow both to coexist
|
|
99
|
+
if (typeof console !== "undefined") {
|
|
100
|
+
console.log(
|
|
101
|
+
"[HttpClient] JWT set:",
|
|
102
|
+
!!jwt,
|
|
103
|
+
"API key still present:",
|
|
104
|
+
!!this.apiKey
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private getAuthHeaders(path: string): Record<string, string> {
|
|
110
|
+
const headers: Record<string, string> = {};
|
|
111
|
+
|
|
112
|
+
// For database, pubsub, proxy, and cache operations, ONLY use API key to avoid JWT user context
|
|
113
|
+
// interfering with namespace-level authorization
|
|
114
|
+
const isDbOperation = path.includes("/v1/rqlite/");
|
|
115
|
+
const isPubSubOperation = path.includes("/v1/pubsub/");
|
|
116
|
+
const isProxyOperation = path.includes("/v1/proxy/");
|
|
117
|
+
const isCacheOperation = path.includes("/v1/cache/");
|
|
118
|
+
|
|
119
|
+
// For auth operations, prefer API key over JWT to ensure proper authentication
|
|
120
|
+
const isAuthOperation = path.includes("/v1/auth/");
|
|
121
|
+
|
|
122
|
+
if (
|
|
123
|
+
isDbOperation ||
|
|
124
|
+
isPubSubOperation ||
|
|
125
|
+
isProxyOperation ||
|
|
126
|
+
isCacheOperation
|
|
127
|
+
) {
|
|
128
|
+
// For database/pubsub/proxy/cache operations: use only API key (preferred for namespace operations)
|
|
129
|
+
if (this.apiKey) {
|
|
130
|
+
headers["X-API-Key"] = this.apiKey;
|
|
131
|
+
} else if (this.jwt) {
|
|
132
|
+
// Fallback to JWT if no API key
|
|
133
|
+
headers["Authorization"] = `Bearer ${this.jwt}`;
|
|
134
|
+
}
|
|
135
|
+
} else if (isAuthOperation) {
|
|
136
|
+
// For auth operations: prefer API key over JWT (auth endpoints should use explicit API key)
|
|
137
|
+
if (this.apiKey) {
|
|
138
|
+
headers["X-API-Key"] = this.apiKey;
|
|
139
|
+
}
|
|
140
|
+
if (this.jwt) {
|
|
141
|
+
headers["Authorization"] = `Bearer ${this.jwt}`;
|
|
142
|
+
}
|
|
143
|
+
} else {
|
|
144
|
+
// For other operations: send both JWT and API key
|
|
145
|
+
if (this.jwt) {
|
|
146
|
+
headers["Authorization"] = `Bearer ${this.jwt}`;
|
|
147
|
+
}
|
|
148
|
+
if (this.apiKey) {
|
|
149
|
+
headers["X-API-Key"] = this.apiKey;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return headers;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private getAuthToken(): string | undefined {
|
|
156
|
+
return this.jwt || this.apiKey;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
getApiKey(): string | undefined {
|
|
160
|
+
return this.apiKey;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Get the base URL
|
|
165
|
+
*/
|
|
166
|
+
getBaseURL(): string {
|
|
167
|
+
return this.baseURL;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async request<T = any>(
|
|
171
|
+
method: "GET" | "POST" | "PUT" | "DELETE",
|
|
172
|
+
path: string,
|
|
173
|
+
options: {
|
|
174
|
+
body?: any;
|
|
175
|
+
headers?: Record<string, string>;
|
|
176
|
+
query?: Record<string, string | number | boolean>;
|
|
177
|
+
timeout?: number; // Per-request timeout override
|
|
178
|
+
} = {}
|
|
179
|
+
): Promise<T> {
|
|
180
|
+
const startTime = performance.now(); // Track request start time
|
|
181
|
+
const url = new URL(this.baseURL + path);
|
|
182
|
+
if (options.query) {
|
|
183
|
+
Object.entries(options.query).forEach(([key, value]) => {
|
|
184
|
+
url.searchParams.append(key, String(value));
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const headers: Record<string, string> = {
|
|
189
|
+
"Content-Type": "application/json",
|
|
190
|
+
...this.getAuthHeaders(path),
|
|
191
|
+
...options.headers,
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const controller = new AbortController();
|
|
195
|
+
const requestTimeout = options.timeout ?? this.timeout; // Use override or default
|
|
196
|
+
const timeoutId = setTimeout(() => controller.abort(), requestTimeout);
|
|
197
|
+
|
|
198
|
+
const fetchOptions: RequestInit = {
|
|
199
|
+
method,
|
|
200
|
+
headers,
|
|
201
|
+
signal: controller.signal,
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
if (options.body !== undefined) {
|
|
205
|
+
fetchOptions.body = JSON.stringify(options.body);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Extract and log SQL query details for rqlite operations
|
|
209
|
+
const isRqliteOperation = path.includes("/v1/rqlite/");
|
|
210
|
+
let queryDetails: string | null = null;
|
|
211
|
+
if (isRqliteOperation && options.body) {
|
|
212
|
+
try {
|
|
213
|
+
const body =
|
|
214
|
+
typeof options.body === "string"
|
|
215
|
+
? JSON.parse(options.body)
|
|
216
|
+
: options.body;
|
|
217
|
+
|
|
218
|
+
if (body.sql) {
|
|
219
|
+
// Direct SQL query (query/exec endpoints)
|
|
220
|
+
queryDetails = `SQL: ${body.sql}`;
|
|
221
|
+
if (body.args && body.args.length > 0) {
|
|
222
|
+
queryDetails += ` | Args: [${body.args
|
|
223
|
+
.map((a: any) => (typeof a === "string" ? `"${a}"` : a))
|
|
224
|
+
.join(", ")}]`;
|
|
225
|
+
}
|
|
226
|
+
} else if (body.table) {
|
|
227
|
+
// Table-based query (find/find-one/select endpoints)
|
|
228
|
+
queryDetails = `Table: ${body.table}`;
|
|
229
|
+
if (body.criteria && Object.keys(body.criteria).length > 0) {
|
|
230
|
+
queryDetails += ` | Criteria: ${JSON.stringify(body.criteria)}`;
|
|
231
|
+
}
|
|
232
|
+
if (body.options) {
|
|
233
|
+
queryDetails += ` | Options: ${JSON.stringify(body.options)}`;
|
|
234
|
+
}
|
|
235
|
+
if (body.select) {
|
|
236
|
+
queryDetails += ` | Select: ${JSON.stringify(body.select)}`;
|
|
237
|
+
}
|
|
238
|
+
if (body.where) {
|
|
239
|
+
queryDetails += ` | Where: ${JSON.stringify(body.where)}`;
|
|
240
|
+
}
|
|
241
|
+
if (body.limit) {
|
|
242
|
+
queryDetails += ` | Limit: ${body.limit}`;
|
|
243
|
+
}
|
|
244
|
+
if (body.offset) {
|
|
245
|
+
queryDetails += ` | Offset: ${body.offset}`;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
} catch (e) {
|
|
249
|
+
// Failed to parse body, ignore
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
try {
|
|
254
|
+
const result = await this.requestWithRetry(
|
|
255
|
+
url.toString(),
|
|
256
|
+
fetchOptions,
|
|
257
|
+
0,
|
|
258
|
+
startTime
|
|
259
|
+
);
|
|
260
|
+
const duration = performance.now() - startTime;
|
|
261
|
+
if (typeof console !== "undefined") {
|
|
262
|
+
const logMessage = `[HttpClient] ${method} ${path} completed in ${duration.toFixed(
|
|
263
|
+
2
|
|
264
|
+
)}ms`;
|
|
265
|
+
if (queryDetails && this.debug) {
|
|
266
|
+
console.log(logMessage);
|
|
267
|
+
console.log(`[HttpClient] ${queryDetails}`);
|
|
268
|
+
} else {
|
|
269
|
+
console.log(logMessage);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return result;
|
|
273
|
+
} catch (error) {
|
|
274
|
+
const duration = performance.now() - startTime;
|
|
275
|
+
if (typeof console !== "undefined") {
|
|
276
|
+
// For 404 errors on find-one calls, log at warn level (not error) since "not found" is expected
|
|
277
|
+
// Application layer handles these cases in try-catch blocks
|
|
278
|
+
const is404FindOne =
|
|
279
|
+
path === "/v1/rqlite/find-one" &&
|
|
280
|
+
error instanceof SDKError &&
|
|
281
|
+
error.httpStatus === 404;
|
|
282
|
+
|
|
283
|
+
if (is404FindOne) {
|
|
284
|
+
// Log as warning for visibility, but not as error since it's expected behavior
|
|
285
|
+
console.warn(
|
|
286
|
+
`[HttpClient] ${method} ${path} returned 404 after ${duration.toFixed(
|
|
287
|
+
2
|
|
288
|
+
)}ms (expected for optional lookups)`
|
|
289
|
+
);
|
|
290
|
+
} else {
|
|
291
|
+
const errorMessage = `[HttpClient] ${method} ${path} failed after ${duration.toFixed(
|
|
292
|
+
2
|
|
293
|
+
)}ms:`;
|
|
294
|
+
console.error(errorMessage, error);
|
|
295
|
+
if (queryDetails && this.debug) {
|
|
296
|
+
console.error(`[HttpClient] ${queryDetails}`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Call the network error callback if configured
|
|
302
|
+
// This allows the app to trigger gateway failover
|
|
303
|
+
if (this.onNetworkError) {
|
|
304
|
+
// Convert native errors (TypeError, AbortError) to SDKError for the callback
|
|
305
|
+
const sdkError =
|
|
306
|
+
error instanceof SDKError
|
|
307
|
+
? error
|
|
308
|
+
: new SDKError(
|
|
309
|
+
error instanceof Error ? error.message : String(error),
|
|
310
|
+
0, // httpStatus 0 indicates network-level failure
|
|
311
|
+
"NETWORK_ERROR"
|
|
312
|
+
);
|
|
313
|
+
this.onNetworkError(sdkError, {
|
|
314
|
+
method,
|
|
315
|
+
path,
|
|
316
|
+
isRetry: false,
|
|
317
|
+
attempt: this.maxRetries, // All retries exhausted
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
throw error;
|
|
322
|
+
} finally {
|
|
323
|
+
clearTimeout(timeoutId);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
private async requestWithRetry(
|
|
328
|
+
url: string,
|
|
329
|
+
options: RequestInit,
|
|
330
|
+
attempt: number = 0,
|
|
331
|
+
startTime?: number // Track start time for timing across retries
|
|
332
|
+
): Promise<any> {
|
|
333
|
+
try {
|
|
334
|
+
const response = await this.fetch(url, options);
|
|
335
|
+
|
|
336
|
+
if (!response.ok) {
|
|
337
|
+
let body: any;
|
|
338
|
+
try {
|
|
339
|
+
body = await response.json();
|
|
340
|
+
} catch {
|
|
341
|
+
body = { error: response.statusText };
|
|
342
|
+
}
|
|
343
|
+
throw SDKError.fromResponse(response.status, body);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Request succeeded - return response
|
|
347
|
+
const contentType = response.headers.get("content-type");
|
|
348
|
+
if (contentType?.includes("application/json")) {
|
|
349
|
+
return response.json();
|
|
350
|
+
}
|
|
351
|
+
return response.text();
|
|
352
|
+
} catch (error) {
|
|
353
|
+
const isRetryableError =
|
|
354
|
+
error instanceof SDKError &&
|
|
355
|
+
[408, 429, 500, 502, 503, 504].includes(error.httpStatus);
|
|
356
|
+
|
|
357
|
+
// Retry on same gateway for retryable HTTP errors
|
|
358
|
+
if (isRetryableError && attempt < this.maxRetries) {
|
|
359
|
+
if (typeof console !== "undefined") {
|
|
360
|
+
console.warn(
|
|
361
|
+
`[HttpClient] Retrying request (attempt ${attempt + 1}/${this.maxRetries})`
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
await new Promise((resolve) =>
|
|
365
|
+
setTimeout(resolve, this.retryDelayMs * (attempt + 1))
|
|
366
|
+
);
|
|
367
|
+
return this.requestWithRetry(url, options, attempt + 1, startTime);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// All retries exhausted - throw error for app to handle
|
|
371
|
+
throw error;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
async get<T = any>(
|
|
376
|
+
path: string,
|
|
377
|
+
options?: Omit<Parameters<typeof this.request>[2], "body">
|
|
378
|
+
): Promise<T> {
|
|
379
|
+
return this.request<T>("GET", path, options);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
async post<T = any>(
|
|
383
|
+
path: string,
|
|
384
|
+
body?: any,
|
|
385
|
+
options?: Omit<Parameters<typeof this.request>[2], "body">
|
|
386
|
+
): Promise<T> {
|
|
387
|
+
return this.request<T>("POST", path, { ...options, body });
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
async put<T = any>(
|
|
391
|
+
path: string,
|
|
392
|
+
body?: any,
|
|
393
|
+
options?: Omit<Parameters<typeof this.request>[2], "body">
|
|
394
|
+
): Promise<T> {
|
|
395
|
+
return this.request<T>("PUT", path, { ...options, body });
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
async delete<T = any>(
|
|
399
|
+
path: string,
|
|
400
|
+
options?: Omit<Parameters<typeof this.request>[2], "body">
|
|
401
|
+
): Promise<T> {
|
|
402
|
+
return this.request<T>("DELETE", path, options);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Upload a file using multipart/form-data
|
|
407
|
+
* This is a special method for file uploads that bypasses JSON serialization
|
|
408
|
+
*/
|
|
409
|
+
async uploadFile<T = any>(
|
|
410
|
+
path: string,
|
|
411
|
+
formData: FormData,
|
|
412
|
+
options?: {
|
|
413
|
+
timeout?: number;
|
|
414
|
+
}
|
|
415
|
+
): Promise<T> {
|
|
416
|
+
const startTime = performance.now(); // Track upload start time
|
|
417
|
+
const url = new URL(this.baseURL + path);
|
|
418
|
+
const headers: Record<string, string> = {
|
|
419
|
+
...this.getAuthHeaders(path),
|
|
420
|
+
// Don't set Content-Type - browser will set it with boundary
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
const controller = new AbortController();
|
|
424
|
+
const requestTimeout = options?.timeout ?? this.timeout * 5; // 5x timeout for uploads
|
|
425
|
+
const timeoutId = setTimeout(() => controller.abort(), requestTimeout);
|
|
426
|
+
|
|
427
|
+
const fetchOptions: RequestInit = {
|
|
428
|
+
method: "POST",
|
|
429
|
+
headers,
|
|
430
|
+
body: formData,
|
|
431
|
+
signal: controller.signal,
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
try {
|
|
435
|
+
const result = await this.requestWithRetry(
|
|
436
|
+
url.toString(),
|
|
437
|
+
fetchOptions,
|
|
438
|
+
0,
|
|
439
|
+
startTime
|
|
440
|
+
);
|
|
441
|
+
const duration = performance.now() - startTime;
|
|
442
|
+
if (typeof console !== "undefined") {
|
|
443
|
+
console.log(
|
|
444
|
+
`[HttpClient] POST ${path} (upload) completed in ${duration.toFixed(
|
|
445
|
+
2
|
|
446
|
+
)}ms`
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
return result;
|
|
450
|
+
} catch (error) {
|
|
451
|
+
const duration = performance.now() - startTime;
|
|
452
|
+
if (typeof console !== "undefined") {
|
|
453
|
+
console.error(
|
|
454
|
+
`[HttpClient] POST ${path} (upload) failed after ${duration.toFixed(
|
|
455
|
+
2
|
|
456
|
+
)}ms:`,
|
|
457
|
+
error
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Call the network error callback if configured
|
|
462
|
+
if (this.onNetworkError) {
|
|
463
|
+
const sdkError =
|
|
464
|
+
error instanceof SDKError
|
|
465
|
+
? error
|
|
466
|
+
: new SDKError(
|
|
467
|
+
error instanceof Error ? error.message : String(error),
|
|
468
|
+
0,
|
|
469
|
+
"NETWORK_ERROR"
|
|
470
|
+
);
|
|
471
|
+
this.onNetworkError(sdkError, {
|
|
472
|
+
method: "POST",
|
|
473
|
+
path,
|
|
474
|
+
isRetry: false,
|
|
475
|
+
attempt: this.maxRetries,
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
throw error;
|
|
480
|
+
} finally {
|
|
481
|
+
clearTimeout(timeoutId);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Get a binary response (returns Response object for streaming)
|
|
487
|
+
*/
|
|
488
|
+
async getBinary(path: string): Promise<Response> {
|
|
489
|
+
const url = new URL(this.baseURL + path);
|
|
490
|
+
const headers: Record<string, string> = {
|
|
491
|
+
...this.getAuthHeaders(path),
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
const controller = new AbortController();
|
|
495
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout * 5); // 5x timeout for downloads
|
|
496
|
+
|
|
497
|
+
const fetchOptions: RequestInit = {
|
|
498
|
+
method: "GET",
|
|
499
|
+
headers,
|
|
500
|
+
signal: controller.signal,
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
try {
|
|
504
|
+
const response = await this.fetch(url.toString(), fetchOptions);
|
|
505
|
+
if (!response.ok) {
|
|
506
|
+
clearTimeout(timeoutId);
|
|
507
|
+
const errorBody = await response.json().catch(() => ({
|
|
508
|
+
error: response.statusText,
|
|
509
|
+
}));
|
|
510
|
+
throw SDKError.fromResponse(response.status, errorBody);
|
|
511
|
+
}
|
|
512
|
+
return response;
|
|
513
|
+
} catch (error) {
|
|
514
|
+
clearTimeout(timeoutId);
|
|
515
|
+
|
|
516
|
+
// Call the network error callback if configured
|
|
517
|
+
if (this.onNetworkError) {
|
|
518
|
+
const sdkError =
|
|
519
|
+
error instanceof SDKError
|
|
520
|
+
? error
|
|
521
|
+
: new SDKError(
|
|
522
|
+
error instanceof Error ? error.message : String(error),
|
|
523
|
+
0,
|
|
524
|
+
"NETWORK_ERROR"
|
|
525
|
+
);
|
|
526
|
+
this.onNetworkError(sdkError, {
|
|
527
|
+
method: "GET",
|
|
528
|
+
path,
|
|
529
|
+
isRetry: false,
|
|
530
|
+
attempt: 0,
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
throw error;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
getToken(): string | undefined {
|
|
539
|
+
return this.getAuthToken();
|
|
540
|
+
}
|
|
541
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { HttpClient, type HttpClientConfig, type NetworkErrorCallback, type NetworkErrorContext } from "./http";
|
|
2
|
+
export { WSClient, type WSClientConfig } from "./ws";
|
|
3
|
+
export type { IHttpTransport, RequestOptions } from "./interfaces/IHttpTransport";
|
|
4
|
+
export type { IWebSocketClient } from "./interfaces/IWebSocketClient";
|
|
5
|
+
export type { IAuthStrategy, RequestContext } from "./interfaces/IAuthStrategy";
|
|
6
|
+
export type { IRetryPolicy } from "./interfaces/IRetryPolicy";
|
|
7
|
+
export { PathBasedAuthStrategy } from "./transport/AuthHeaderStrategy";
|
|
8
|
+
export { ExponentialBackoffRetryPolicy } from "./transport/RequestRetryPolicy";
|
|
9
|
+
export { RequestLogger } from "./transport/RequestLogger";
|
|
10
|
+
export { TLSConfiguration } from "./transport/TLSConfiguration";
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request context for authentication
|
|
3
|
+
*/
|
|
4
|
+
export interface RequestContext {
|
|
5
|
+
path: string;
|
|
6
|
+
method: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Authentication strategy interface
|
|
11
|
+
* Provides abstraction for different authentication header strategies
|
|
12
|
+
*/
|
|
13
|
+
export interface IAuthStrategy {
|
|
14
|
+
/**
|
|
15
|
+
* Get authentication headers for a request
|
|
16
|
+
*/
|
|
17
|
+
getHeaders(context: RequestContext): Record<string, string>;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Set API key
|
|
21
|
+
*/
|
|
22
|
+
setApiKey(apiKey?: string): void;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Set JWT token
|
|
26
|
+
*/
|
|
27
|
+
setJwt(jwt?: string): void;
|
|
28
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP Request options
|
|
3
|
+
*/
|
|
4
|
+
export interface RequestOptions {
|
|
5
|
+
headers?: Record<string, string>;
|
|
6
|
+
query?: Record<string, string | number | boolean>;
|
|
7
|
+
timeout?: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* HTTP Transport abstraction interface
|
|
12
|
+
* Provides a testable abstraction layer for HTTP operations
|
|
13
|
+
*/
|
|
14
|
+
export interface IHttpTransport {
|
|
15
|
+
/**
|
|
16
|
+
* Perform GET request
|
|
17
|
+
*/
|
|
18
|
+
get<T = any>(path: string, options?: RequestOptions): Promise<T>;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Perform POST request
|
|
22
|
+
*/
|
|
23
|
+
post<T = any>(path: string, body?: any, options?: RequestOptions): Promise<T>;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Perform PUT request
|
|
27
|
+
*/
|
|
28
|
+
put<T = any>(path: string, body?: any, options?: RequestOptions): Promise<T>;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Perform DELETE request
|
|
32
|
+
*/
|
|
33
|
+
delete<T = any>(path: string, options?: RequestOptions): Promise<T>;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Upload file using multipart/form-data
|
|
37
|
+
*/
|
|
38
|
+
uploadFile<T = any>(
|
|
39
|
+
path: string,
|
|
40
|
+
formData: FormData,
|
|
41
|
+
options?: { timeout?: number }
|
|
42
|
+
): Promise<T>;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Get binary response (returns Response object for streaming)
|
|
46
|
+
*/
|
|
47
|
+
getBinary(path: string): Promise<Response>;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get base URL
|
|
51
|
+
*/
|
|
52
|
+
getBaseURL(): string;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Get API key
|
|
56
|
+
*/
|
|
57
|
+
getApiKey(): string | undefined;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get current token (JWT or API key)
|
|
61
|
+
*/
|
|
62
|
+
getToken(): string | undefined;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Set API key for authentication
|
|
66
|
+
*/
|
|
67
|
+
setApiKey(apiKey?: string): void;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Set JWT token for authentication
|
|
71
|
+
*/
|
|
72
|
+
setJwt(jwt?: string): void;
|
|
73
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retry policy interface
|
|
3
|
+
* Provides abstraction for retry logic and backoff strategies
|
|
4
|
+
*/
|
|
5
|
+
export interface IRetryPolicy {
|
|
6
|
+
/**
|
|
7
|
+
* Determine if request should be retried
|
|
8
|
+
*/
|
|
9
|
+
shouldRetry(error: any, attempt: number): boolean;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Get delay before next retry attempt (in milliseconds)
|
|
13
|
+
*/
|
|
14
|
+
getDelay(attempt: number): number;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Get maximum number of retry attempts
|
|
18
|
+
*/
|
|
19
|
+
getMaxRetries(): number;
|
|
20
|
+
}
|