@insforge/sdk 1.2.0-dev.2 → 1.2.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/dist/index.mjs CHANGED
@@ -17,9 +17,191 @@ var InsForgeError = class _InsForgeError extends Error {
17
17
  }
18
18
  };
19
19
 
20
+ // src/lib/logger.ts
21
+ var SENSITIVE_HEADERS = ["authorization", "x-api-key", "cookie", "set-cookie"];
22
+ var SENSITIVE_BODY_KEYS = [
23
+ "password",
24
+ "token",
25
+ "accesstoken",
26
+ "refreshtoken",
27
+ "authorization",
28
+ "secret",
29
+ "apikey",
30
+ "api_key",
31
+ "email",
32
+ "ssn",
33
+ "creditcard",
34
+ "credit_card"
35
+ ];
36
+ function redactHeaders(headers) {
37
+ const redacted = {};
38
+ for (const [key, value] of Object.entries(headers)) {
39
+ if (SENSITIVE_HEADERS.includes(key.toLowerCase())) {
40
+ redacted[key] = "***REDACTED***";
41
+ } else {
42
+ redacted[key] = value;
43
+ }
44
+ }
45
+ return redacted;
46
+ }
47
+ function sanitizeBody(body) {
48
+ if (body === null || body === void 0) return body;
49
+ if (typeof body === "string") {
50
+ try {
51
+ const parsed = JSON.parse(body);
52
+ return sanitizeBody(parsed);
53
+ } catch {
54
+ return body;
55
+ }
56
+ }
57
+ if (Array.isArray(body)) return body.map(sanitizeBody);
58
+ if (typeof body === "object") {
59
+ const sanitized = {};
60
+ for (const [key, value] of Object.entries(body)) {
61
+ if (SENSITIVE_BODY_KEYS.includes(key.toLowerCase().replace(/[-_]/g, ""))) {
62
+ sanitized[key] = "***REDACTED***";
63
+ } else {
64
+ sanitized[key] = sanitizeBody(value);
65
+ }
66
+ }
67
+ return sanitized;
68
+ }
69
+ return body;
70
+ }
71
+ function formatBody(body) {
72
+ if (body === void 0 || body === null) return "";
73
+ if (typeof body === "string") {
74
+ try {
75
+ return JSON.stringify(JSON.parse(body), null, 2);
76
+ } catch {
77
+ return body;
78
+ }
79
+ }
80
+ if (typeof FormData !== "undefined" && body instanceof FormData) {
81
+ return "[FormData]";
82
+ }
83
+ try {
84
+ return JSON.stringify(body, null, 2);
85
+ } catch {
86
+ return "[Unserializable body]";
87
+ }
88
+ }
89
+ var Logger = class {
90
+ /**
91
+ * Creates a new Logger instance.
92
+ * @param debug - Set to true to enable console logging, or pass a custom log function
93
+ */
94
+ constructor(debug) {
95
+ if (typeof debug === "function") {
96
+ this.enabled = true;
97
+ this.customLog = debug;
98
+ } else {
99
+ this.enabled = !!debug;
100
+ this.customLog = null;
101
+ }
102
+ }
103
+ /**
104
+ * Logs a debug message at the info level.
105
+ * @param message - The message to log
106
+ * @param args - Additional arguments to pass to the log function
107
+ */
108
+ log(message, ...args) {
109
+ if (!this.enabled) return;
110
+ const formatted = `[InsForge Debug] ${message}`;
111
+ if (this.customLog) {
112
+ this.customLog(formatted, ...args);
113
+ } else {
114
+ console.log(formatted, ...args);
115
+ }
116
+ }
117
+ /**
118
+ * Logs a debug message at the warning level.
119
+ * @param message - The message to log
120
+ * @param args - Additional arguments to pass to the log function
121
+ */
122
+ warn(message, ...args) {
123
+ if (!this.enabled) return;
124
+ const formatted = `[InsForge Debug] ${message}`;
125
+ if (this.customLog) {
126
+ this.customLog(formatted, ...args);
127
+ } else {
128
+ console.warn(formatted, ...args);
129
+ }
130
+ }
131
+ /**
132
+ * Logs a debug message at the error level.
133
+ * @param message - The message to log
134
+ * @param args - Additional arguments to pass to the log function
135
+ */
136
+ error(message, ...args) {
137
+ if (!this.enabled) return;
138
+ const formatted = `[InsForge Debug] ${message}`;
139
+ if (this.customLog) {
140
+ this.customLog(formatted, ...args);
141
+ } else {
142
+ console.error(formatted, ...args);
143
+ }
144
+ }
145
+ /**
146
+ * Logs an outgoing HTTP request with method, URL, headers, and body.
147
+ * Sensitive headers and body fields are automatically redacted.
148
+ * @param method - HTTP method (GET, POST, etc.)
149
+ * @param url - The full request URL
150
+ * @param headers - Request headers (sensitive values will be redacted)
151
+ * @param body - Request body (sensitive fields will be masked)
152
+ */
153
+ logRequest(method, url, headers, body) {
154
+ if (!this.enabled) return;
155
+ const parts = [
156
+ `\u2192 ${method} ${url}`
157
+ ];
158
+ if (headers && Object.keys(headers).length > 0) {
159
+ parts.push(` Headers: ${JSON.stringify(redactHeaders(headers))}`);
160
+ }
161
+ const formattedBody = formatBody(sanitizeBody(body));
162
+ if (formattedBody) {
163
+ const truncated = formattedBody.length > 1e3 ? formattedBody.slice(0, 1e3) + "... [truncated]" : formattedBody;
164
+ parts.push(` Body: ${truncated}`);
165
+ }
166
+ this.log(parts.join("\n"));
167
+ }
168
+ /**
169
+ * Logs an incoming HTTP response with method, URL, status, duration, and body.
170
+ * Error responses (4xx/5xx) are logged at the error level.
171
+ * @param method - HTTP method (GET, POST, etc.)
172
+ * @param url - The full request URL
173
+ * @param status - HTTP response status code
174
+ * @param durationMs - Request duration in milliseconds
175
+ * @param body - Response body (sensitive fields will be masked, large bodies truncated)
176
+ */
177
+ logResponse(method, url, status, durationMs, body) {
178
+ if (!this.enabled) return;
179
+ const parts = [
180
+ `\u2190 ${method} ${url} ${status} (${durationMs}ms)`
181
+ ];
182
+ const formattedBody = formatBody(sanitizeBody(body));
183
+ if (formattedBody) {
184
+ const truncated = formattedBody.length > 1e3 ? formattedBody.slice(0, 1e3) + "... [truncated]" : formattedBody;
185
+ parts.push(` Body: ${truncated}`);
186
+ }
187
+ if (status >= 400) {
188
+ this.error(parts.join("\n"));
189
+ } else {
190
+ this.log(parts.join("\n"));
191
+ }
192
+ }
193
+ };
194
+
20
195
  // src/lib/http-client.ts
196
+ var RETRYABLE_STATUS_CODES = /* @__PURE__ */ new Set([500, 502, 503, 504]);
197
+ var IDEMPOTENT_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
21
198
  var HttpClient = class {
22
- constructor(config) {
199
+ /**
200
+ * Creates a new HttpClient instance.
201
+ * @param config - SDK configuration including baseUrl, timeout, retry settings, and fetch implementation.
202
+ * @param logger - Optional logger instance for request/response debugging.
203
+ */
204
+ constructor(config, logger) {
23
205
  this.userToken = null;
24
206
  this.baseUrl = config.baseUrl || "http://localhost:7130";
25
207
  this.fetch = config.fetch || (globalThis.fetch ? globalThis.fetch.bind(globalThis) : void 0);
@@ -27,12 +209,20 @@ var HttpClient = class {
27
209
  this.defaultHeaders = {
28
210
  ...config.headers
29
211
  };
212
+ this.logger = logger || new Logger(false);
213
+ this.timeout = config.timeout ?? 3e4;
214
+ this.retryCount = config.retryCount ?? 3;
215
+ this.retryDelay = config.retryDelay ?? 500;
30
216
  if (!this.fetch) {
31
217
  throw new Error(
32
218
  "Fetch is not available. Please provide a fetch implementation in the config."
33
219
  );
34
220
  }
35
221
  }
222
+ /**
223
+ * Builds a full URL from a path and optional query parameters.
224
+ * Normalizes PostgREST select parameters for proper syntax.
225
+ */
36
226
  buildUrl(path, params) {
37
227
  const url = new URL(path, this.baseUrl);
38
228
  if (params) {
@@ -48,9 +238,36 @@ var HttpClient = class {
48
238
  }
49
239
  return url.toString();
50
240
  }
241
+ /** Checks if an HTTP status code is eligible for retry (5xx server errors). */
242
+ isRetryableStatus(status) {
243
+ return RETRYABLE_STATUS_CODES.has(status);
244
+ }
245
+ /**
246
+ * Computes the delay before the next retry using exponential backoff with jitter.
247
+ * @param attempt - The current retry attempt number (1-based).
248
+ * @returns Delay in milliseconds.
249
+ */
250
+ computeRetryDelay(attempt) {
251
+ const base = this.retryDelay * Math.pow(2, attempt - 1);
252
+ const jitter = base * (0.85 + Math.random() * 0.3);
253
+ return Math.round(jitter);
254
+ }
255
+ /**
256
+ * Performs an HTTP request with automatic retry and timeout handling.
257
+ * Retries on network errors and 5xx server errors with exponential backoff.
258
+ * Client errors (4xx) and timeouts are thrown immediately without retry.
259
+ * @param method - HTTP method (GET, POST, PUT, PATCH, DELETE).
260
+ * @param path - API path relative to the base URL.
261
+ * @param options - Optional request configuration including headers, body, and query params.
262
+ * @returns Parsed response data.
263
+ * @throws {InsForgeError} On timeout, network failure, or HTTP error responses.
264
+ */
51
265
  async request(method, path, options = {}) {
52
- const { params, headers = {}, body, ...fetchOptions } = options;
266
+ const { params, headers = {}, body, signal: callerSignal, ...fetchOptions } = options;
53
267
  const url = this.buildUrl(path, params);
268
+ const startTime = Date.now();
269
+ const canRetry = IDEMPOTENT_METHODS.has(method.toUpperCase()) || options.idempotent === true;
270
+ const maxAttempts = canRetry ? this.retryCount : 0;
54
271
  const requestHeaders = {
55
272
  ...this.defaultHeaders
56
273
  };
@@ -69,62 +286,175 @@ var HttpClient = class {
69
286
  processedBody = JSON.stringify(body);
70
287
  }
71
288
  }
72
- Object.assign(requestHeaders, headers);
73
- const response = await this.fetch(url, {
74
- method,
75
- headers: requestHeaders,
76
- body: processedBody,
77
- ...fetchOptions
78
- });
79
- if (response.status === 204) {
80
- return void 0;
81
- }
82
- let data;
83
- const contentType = response.headers.get("content-type");
84
- if (contentType?.includes("json")) {
85
- data = await response.json();
289
+ if (headers instanceof Headers) {
290
+ headers.forEach((value, key) => {
291
+ requestHeaders[key] = value;
292
+ });
293
+ } else if (Array.isArray(headers)) {
294
+ headers.forEach(([key, value]) => {
295
+ requestHeaders[key] = value;
296
+ });
86
297
  } else {
87
- data = await response.text();
298
+ Object.assign(requestHeaders, headers);
88
299
  }
89
- if (!response.ok) {
90
- if (data && typeof data === "object" && "error" in data) {
91
- if (!data.statusCode && !data.status) {
92
- data.statusCode = response.status;
300
+ this.logger.logRequest(method, url, requestHeaders, processedBody);
301
+ let lastError;
302
+ for (let attempt = 0; attempt <= maxAttempts; attempt++) {
303
+ if (attempt > 0) {
304
+ const delay = this.computeRetryDelay(attempt);
305
+ this.logger.warn(`Retry ${attempt}/${maxAttempts} for ${method} ${url} in ${delay}ms`);
306
+ if (callerSignal?.aborted) throw callerSignal.reason;
307
+ await new Promise((resolve, reject) => {
308
+ const onAbort = () => {
309
+ clearTimeout(timer2);
310
+ reject(callerSignal.reason);
311
+ };
312
+ const timer2 = setTimeout(() => {
313
+ if (callerSignal) callerSignal.removeEventListener("abort", onAbort);
314
+ resolve();
315
+ }, delay);
316
+ if (callerSignal) {
317
+ callerSignal.addEventListener("abort", onAbort, { once: true });
318
+ }
319
+ });
320
+ }
321
+ let controller;
322
+ let timer;
323
+ if (this.timeout > 0 || callerSignal) {
324
+ controller = new AbortController();
325
+ if (this.timeout > 0) {
326
+ timer = setTimeout(() => controller.abort(), this.timeout);
93
327
  }
94
- const error = InsForgeError.fromApiError(data);
95
- Object.keys(data).forEach((key) => {
96
- if (key !== "error" && key !== "message" && key !== "statusCode") {
97
- error[key] = data[key];
328
+ if (callerSignal) {
329
+ if (callerSignal.aborted) {
330
+ controller.abort(callerSignal.reason);
331
+ } else {
332
+ const onCallerAbort = () => controller.abort(callerSignal.reason);
333
+ callerSignal.addEventListener("abort", onCallerAbort, { once: true });
334
+ controller.signal.addEventListener("abort", () => {
335
+ callerSignal.removeEventListener("abort", onCallerAbort);
336
+ }, { once: true });
98
337
  }
338
+ }
339
+ }
340
+ try {
341
+ const response = await this.fetch(url, {
342
+ method,
343
+ headers: requestHeaders,
344
+ body: processedBody,
345
+ ...fetchOptions,
346
+ ...controller ? { signal: controller.signal } : {}
99
347
  });
100
- throw error;
348
+ if (this.isRetryableStatus(response.status) && attempt < maxAttempts) {
349
+ if (timer !== void 0) clearTimeout(timer);
350
+ await response.body?.cancel();
351
+ lastError = new InsForgeError(
352
+ `Server error: ${response.status} ${response.statusText}`,
353
+ response.status,
354
+ "SERVER_ERROR"
355
+ );
356
+ continue;
357
+ }
358
+ if (response.status === 204) {
359
+ if (timer !== void 0) clearTimeout(timer);
360
+ return void 0;
361
+ }
362
+ let data;
363
+ const contentType = response.headers.get("content-type");
364
+ try {
365
+ if (contentType?.includes("json")) {
366
+ data = await response.json();
367
+ } else {
368
+ data = await response.text();
369
+ }
370
+ } catch (parseErr) {
371
+ if (timer !== void 0) clearTimeout(timer);
372
+ throw new InsForgeError(
373
+ `Failed to parse response body: ${parseErr?.message || "Unknown error"}`,
374
+ response.status,
375
+ response.ok ? "PARSE_ERROR" : "REQUEST_FAILED"
376
+ );
377
+ }
378
+ if (timer !== void 0) clearTimeout(timer);
379
+ if (!response.ok) {
380
+ this.logger.logResponse(method, url, response.status, Date.now() - startTime, data);
381
+ if (data && typeof data === "object" && "error" in data) {
382
+ if (!data.statusCode && !data.status) {
383
+ data.statusCode = response.status;
384
+ }
385
+ const error = InsForgeError.fromApiError(data);
386
+ Object.keys(data).forEach((key) => {
387
+ if (key !== "error" && key !== "message" && key !== "statusCode") {
388
+ error[key] = data[key];
389
+ }
390
+ });
391
+ throw error;
392
+ }
393
+ throw new InsForgeError(
394
+ `Request failed: ${response.statusText}`,
395
+ response.status,
396
+ "REQUEST_FAILED"
397
+ );
398
+ }
399
+ this.logger.logResponse(method, url, response.status, Date.now() - startTime, data);
400
+ return data;
401
+ } catch (err) {
402
+ if (timer !== void 0) clearTimeout(timer);
403
+ if (err?.name === "AbortError") {
404
+ if (controller && controller.signal.aborted && this.timeout > 0 && !callerSignal?.aborted) {
405
+ throw new InsForgeError(
406
+ `Request timed out after ${this.timeout}ms`,
407
+ 408,
408
+ "REQUEST_TIMEOUT"
409
+ );
410
+ }
411
+ throw err;
412
+ }
413
+ if (err instanceof InsForgeError) {
414
+ throw err;
415
+ }
416
+ if (attempt < maxAttempts) {
417
+ lastError = err;
418
+ continue;
419
+ }
420
+ throw new InsForgeError(
421
+ `Network request failed: ${err?.message || "Unknown error"}`,
422
+ 0,
423
+ "NETWORK_ERROR"
424
+ );
101
425
  }
102
- throw new InsForgeError(
103
- `Request failed: ${response.statusText}`,
104
- response.status,
105
- "REQUEST_FAILED"
106
- );
107
426
  }
108
- return data;
427
+ throw lastError || new InsForgeError(
428
+ "Request failed after all retry attempts",
429
+ 0,
430
+ "NETWORK_ERROR"
431
+ );
109
432
  }
433
+ /** Performs a GET request. */
110
434
  get(path, options) {
111
435
  return this.request("GET", path, options);
112
436
  }
437
+ /** Performs a POST request with an optional JSON body. */
113
438
  post(path, body, options) {
114
439
  return this.request("POST", path, { ...options, body });
115
440
  }
441
+ /** Performs a PUT request with an optional JSON body. */
116
442
  put(path, body, options) {
117
443
  return this.request("PUT", path, { ...options, body });
118
444
  }
445
+ /** Performs a PATCH request with an optional JSON body. */
119
446
  patch(path, body, options) {
120
447
  return this.request("PATCH", path, { ...options, body });
121
448
  }
449
+ /** Performs a DELETE request. */
122
450
  delete(path, options) {
123
451
  return this.request("DELETE", path, options);
124
452
  }
453
+ /** Sets or clears the user authentication token for subsequent requests. */
125
454
  setAuthToken(token) {
126
455
  this.userToken = token;
127
456
  }
457
+ /** Returns the current default headers including the authorization header if set. */
128
458
  getHeaders() {
129
459
  const headers = { ...this.defaultHeaders };
130
460
  const authToken = this.userToken || this.anonKey;
@@ -1739,7 +2069,8 @@ var Emails = class {
1739
2069
  // src/client.ts
1740
2070
  var InsForgeClient = class {
1741
2071
  constructor(config = {}) {
1742
- this.http = new HttpClient(config);
2072
+ const logger = new Logger(config.debug);
2073
+ this.http = new HttpClient(config, logger);
1743
2074
  this.tokenManager = new TokenManager();
1744
2075
  if (config.edgeFunctionToken) {
1745
2076
  this.http.setAuthToken(config.edgeFunctionToken);
@@ -1791,6 +2122,7 @@ export {
1791
2122
  HttpClient,
1792
2123
  InsForgeClient,
1793
2124
  InsForgeError,
2125
+ Logger,
1794
2126
  Realtime,
1795
2127
  Storage,
1796
2128
  StorageBucket,