@insforge/sdk 1.2.0-dev.1 → 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.js CHANGED
@@ -28,6 +28,7 @@ __export(index_exports, {
28
28
  HttpClient: () => HttpClient,
29
29
  InsForgeClient: () => InsForgeClient,
30
30
  InsForgeError: () => InsForgeError,
31
+ Logger: () => Logger,
31
32
  Realtime: () => Realtime,
32
33
  Storage: () => Storage,
33
34
  StorageBucket: () => StorageBucket,
@@ -56,9 +57,191 @@ var InsForgeError = class _InsForgeError extends Error {
56
57
  }
57
58
  };
58
59
 
60
+ // src/lib/logger.ts
61
+ var SENSITIVE_HEADERS = ["authorization", "x-api-key", "cookie", "set-cookie"];
62
+ var SENSITIVE_BODY_KEYS = [
63
+ "password",
64
+ "token",
65
+ "accesstoken",
66
+ "refreshtoken",
67
+ "authorization",
68
+ "secret",
69
+ "apikey",
70
+ "api_key",
71
+ "email",
72
+ "ssn",
73
+ "creditcard",
74
+ "credit_card"
75
+ ];
76
+ function redactHeaders(headers) {
77
+ const redacted = {};
78
+ for (const [key, value] of Object.entries(headers)) {
79
+ if (SENSITIVE_HEADERS.includes(key.toLowerCase())) {
80
+ redacted[key] = "***REDACTED***";
81
+ } else {
82
+ redacted[key] = value;
83
+ }
84
+ }
85
+ return redacted;
86
+ }
87
+ function sanitizeBody(body) {
88
+ if (body === null || body === void 0) return body;
89
+ if (typeof body === "string") {
90
+ try {
91
+ const parsed = JSON.parse(body);
92
+ return sanitizeBody(parsed);
93
+ } catch {
94
+ return body;
95
+ }
96
+ }
97
+ if (Array.isArray(body)) return body.map(sanitizeBody);
98
+ if (typeof body === "object") {
99
+ const sanitized = {};
100
+ for (const [key, value] of Object.entries(body)) {
101
+ if (SENSITIVE_BODY_KEYS.includes(key.toLowerCase().replace(/[-_]/g, ""))) {
102
+ sanitized[key] = "***REDACTED***";
103
+ } else {
104
+ sanitized[key] = sanitizeBody(value);
105
+ }
106
+ }
107
+ return sanitized;
108
+ }
109
+ return body;
110
+ }
111
+ function formatBody(body) {
112
+ if (body === void 0 || body === null) return "";
113
+ if (typeof body === "string") {
114
+ try {
115
+ return JSON.stringify(JSON.parse(body), null, 2);
116
+ } catch {
117
+ return body;
118
+ }
119
+ }
120
+ if (typeof FormData !== "undefined" && body instanceof FormData) {
121
+ return "[FormData]";
122
+ }
123
+ try {
124
+ return JSON.stringify(body, null, 2);
125
+ } catch {
126
+ return "[Unserializable body]";
127
+ }
128
+ }
129
+ var Logger = class {
130
+ /**
131
+ * Creates a new Logger instance.
132
+ * @param debug - Set to true to enable console logging, or pass a custom log function
133
+ */
134
+ constructor(debug) {
135
+ if (typeof debug === "function") {
136
+ this.enabled = true;
137
+ this.customLog = debug;
138
+ } else {
139
+ this.enabled = !!debug;
140
+ this.customLog = null;
141
+ }
142
+ }
143
+ /**
144
+ * Logs a debug message at the info level.
145
+ * @param message - The message to log
146
+ * @param args - Additional arguments to pass to the log function
147
+ */
148
+ log(message, ...args) {
149
+ if (!this.enabled) return;
150
+ const formatted = `[InsForge Debug] ${message}`;
151
+ if (this.customLog) {
152
+ this.customLog(formatted, ...args);
153
+ } else {
154
+ console.log(formatted, ...args);
155
+ }
156
+ }
157
+ /**
158
+ * Logs a debug message at the warning level.
159
+ * @param message - The message to log
160
+ * @param args - Additional arguments to pass to the log function
161
+ */
162
+ warn(message, ...args) {
163
+ if (!this.enabled) return;
164
+ const formatted = `[InsForge Debug] ${message}`;
165
+ if (this.customLog) {
166
+ this.customLog(formatted, ...args);
167
+ } else {
168
+ console.warn(formatted, ...args);
169
+ }
170
+ }
171
+ /**
172
+ * Logs a debug message at the error level.
173
+ * @param message - The message to log
174
+ * @param args - Additional arguments to pass to the log function
175
+ */
176
+ error(message, ...args) {
177
+ if (!this.enabled) return;
178
+ const formatted = `[InsForge Debug] ${message}`;
179
+ if (this.customLog) {
180
+ this.customLog(formatted, ...args);
181
+ } else {
182
+ console.error(formatted, ...args);
183
+ }
184
+ }
185
+ /**
186
+ * Logs an outgoing HTTP request with method, URL, headers, and body.
187
+ * Sensitive headers and body fields are automatically redacted.
188
+ * @param method - HTTP method (GET, POST, etc.)
189
+ * @param url - The full request URL
190
+ * @param headers - Request headers (sensitive values will be redacted)
191
+ * @param body - Request body (sensitive fields will be masked)
192
+ */
193
+ logRequest(method, url, headers, body) {
194
+ if (!this.enabled) return;
195
+ const parts = [
196
+ `\u2192 ${method} ${url}`
197
+ ];
198
+ if (headers && Object.keys(headers).length > 0) {
199
+ parts.push(` Headers: ${JSON.stringify(redactHeaders(headers))}`);
200
+ }
201
+ const formattedBody = formatBody(sanitizeBody(body));
202
+ if (formattedBody) {
203
+ const truncated = formattedBody.length > 1e3 ? formattedBody.slice(0, 1e3) + "... [truncated]" : formattedBody;
204
+ parts.push(` Body: ${truncated}`);
205
+ }
206
+ this.log(parts.join("\n"));
207
+ }
208
+ /**
209
+ * Logs an incoming HTTP response with method, URL, status, duration, and body.
210
+ * Error responses (4xx/5xx) are logged at the error level.
211
+ * @param method - HTTP method (GET, POST, etc.)
212
+ * @param url - The full request URL
213
+ * @param status - HTTP response status code
214
+ * @param durationMs - Request duration in milliseconds
215
+ * @param body - Response body (sensitive fields will be masked, large bodies truncated)
216
+ */
217
+ logResponse(method, url, status, durationMs, body) {
218
+ if (!this.enabled) return;
219
+ const parts = [
220
+ `\u2190 ${method} ${url} ${status} (${durationMs}ms)`
221
+ ];
222
+ const formattedBody = formatBody(sanitizeBody(body));
223
+ if (formattedBody) {
224
+ const truncated = formattedBody.length > 1e3 ? formattedBody.slice(0, 1e3) + "... [truncated]" : formattedBody;
225
+ parts.push(` Body: ${truncated}`);
226
+ }
227
+ if (status >= 400) {
228
+ this.error(parts.join("\n"));
229
+ } else {
230
+ this.log(parts.join("\n"));
231
+ }
232
+ }
233
+ };
234
+
59
235
  // src/lib/http-client.ts
236
+ var RETRYABLE_STATUS_CODES = /* @__PURE__ */ new Set([500, 502, 503, 504]);
237
+ var IDEMPOTENT_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
60
238
  var HttpClient = class {
61
- constructor(config) {
239
+ /**
240
+ * Creates a new HttpClient instance.
241
+ * @param config - SDK configuration including baseUrl, timeout, retry settings, and fetch implementation.
242
+ * @param logger - Optional logger instance for request/response debugging.
243
+ */
244
+ constructor(config, logger) {
62
245
  this.userToken = null;
63
246
  this.baseUrl = config.baseUrl || "http://localhost:7130";
64
247
  this.fetch = config.fetch || (globalThis.fetch ? globalThis.fetch.bind(globalThis) : void 0);
@@ -66,12 +249,20 @@ var HttpClient = class {
66
249
  this.defaultHeaders = {
67
250
  ...config.headers
68
251
  };
252
+ this.logger = logger || new Logger(false);
253
+ this.timeout = config.timeout ?? 3e4;
254
+ this.retryCount = config.retryCount ?? 3;
255
+ this.retryDelay = config.retryDelay ?? 500;
69
256
  if (!this.fetch) {
70
257
  throw new Error(
71
258
  "Fetch is not available. Please provide a fetch implementation in the config."
72
259
  );
73
260
  }
74
261
  }
262
+ /**
263
+ * Builds a full URL from a path and optional query parameters.
264
+ * Normalizes PostgREST select parameters for proper syntax.
265
+ */
75
266
  buildUrl(path, params) {
76
267
  const url = new URL(path, this.baseUrl);
77
268
  if (params) {
@@ -87,9 +278,36 @@ var HttpClient = class {
87
278
  }
88
279
  return url.toString();
89
280
  }
281
+ /** Checks if an HTTP status code is eligible for retry (5xx server errors). */
282
+ isRetryableStatus(status) {
283
+ return RETRYABLE_STATUS_CODES.has(status);
284
+ }
285
+ /**
286
+ * Computes the delay before the next retry using exponential backoff with jitter.
287
+ * @param attempt - The current retry attempt number (1-based).
288
+ * @returns Delay in milliseconds.
289
+ */
290
+ computeRetryDelay(attempt) {
291
+ const base = this.retryDelay * Math.pow(2, attempt - 1);
292
+ const jitter = base * (0.85 + Math.random() * 0.3);
293
+ return Math.round(jitter);
294
+ }
295
+ /**
296
+ * Performs an HTTP request with automatic retry and timeout handling.
297
+ * Retries on network errors and 5xx server errors with exponential backoff.
298
+ * Client errors (4xx) and timeouts are thrown immediately without retry.
299
+ * @param method - HTTP method (GET, POST, PUT, PATCH, DELETE).
300
+ * @param path - API path relative to the base URL.
301
+ * @param options - Optional request configuration including headers, body, and query params.
302
+ * @returns Parsed response data.
303
+ * @throws {InsForgeError} On timeout, network failure, or HTTP error responses.
304
+ */
90
305
  async request(method, path, options = {}) {
91
- const { params, headers = {}, body, ...fetchOptions } = options;
306
+ const { params, headers = {}, body, signal: callerSignal, ...fetchOptions } = options;
92
307
  const url = this.buildUrl(path, params);
308
+ const startTime = Date.now();
309
+ const canRetry = IDEMPOTENT_METHODS.has(method.toUpperCase()) || options.idempotent === true;
310
+ const maxAttempts = canRetry ? this.retryCount : 0;
93
311
  const requestHeaders = {
94
312
  ...this.defaultHeaders
95
313
  };
@@ -108,62 +326,175 @@ var HttpClient = class {
108
326
  processedBody = JSON.stringify(body);
109
327
  }
110
328
  }
111
- Object.assign(requestHeaders, headers);
112
- const response = await this.fetch(url, {
113
- method,
114
- headers: requestHeaders,
115
- body: processedBody,
116
- ...fetchOptions
117
- });
118
- if (response.status === 204) {
119
- return void 0;
120
- }
121
- let data;
122
- const contentType = response.headers.get("content-type");
123
- if (contentType?.includes("json")) {
124
- data = await response.json();
329
+ if (headers instanceof Headers) {
330
+ headers.forEach((value, key) => {
331
+ requestHeaders[key] = value;
332
+ });
333
+ } else if (Array.isArray(headers)) {
334
+ headers.forEach(([key, value]) => {
335
+ requestHeaders[key] = value;
336
+ });
125
337
  } else {
126
- data = await response.text();
338
+ Object.assign(requestHeaders, headers);
127
339
  }
128
- if (!response.ok) {
129
- if (data && typeof data === "object" && "error" in data) {
130
- if (!data.statusCode && !data.status) {
131
- data.statusCode = response.status;
340
+ this.logger.logRequest(method, url, requestHeaders, processedBody);
341
+ let lastError;
342
+ for (let attempt = 0; attempt <= maxAttempts; attempt++) {
343
+ if (attempt > 0) {
344
+ const delay = this.computeRetryDelay(attempt);
345
+ this.logger.warn(`Retry ${attempt}/${maxAttempts} for ${method} ${url} in ${delay}ms`);
346
+ if (callerSignal?.aborted) throw callerSignal.reason;
347
+ await new Promise((resolve, reject) => {
348
+ const onAbort = () => {
349
+ clearTimeout(timer2);
350
+ reject(callerSignal.reason);
351
+ };
352
+ const timer2 = setTimeout(() => {
353
+ if (callerSignal) callerSignal.removeEventListener("abort", onAbort);
354
+ resolve();
355
+ }, delay);
356
+ if (callerSignal) {
357
+ callerSignal.addEventListener("abort", onAbort, { once: true });
358
+ }
359
+ });
360
+ }
361
+ let controller;
362
+ let timer;
363
+ if (this.timeout > 0 || callerSignal) {
364
+ controller = new AbortController();
365
+ if (this.timeout > 0) {
366
+ timer = setTimeout(() => controller.abort(), this.timeout);
132
367
  }
133
- const error = InsForgeError.fromApiError(data);
134
- Object.keys(data).forEach((key) => {
135
- if (key !== "error" && key !== "message" && key !== "statusCode") {
136
- error[key] = data[key];
368
+ if (callerSignal) {
369
+ if (callerSignal.aborted) {
370
+ controller.abort(callerSignal.reason);
371
+ } else {
372
+ const onCallerAbort = () => controller.abort(callerSignal.reason);
373
+ callerSignal.addEventListener("abort", onCallerAbort, { once: true });
374
+ controller.signal.addEventListener("abort", () => {
375
+ callerSignal.removeEventListener("abort", onCallerAbort);
376
+ }, { once: true });
137
377
  }
378
+ }
379
+ }
380
+ try {
381
+ const response = await this.fetch(url, {
382
+ method,
383
+ headers: requestHeaders,
384
+ body: processedBody,
385
+ ...fetchOptions,
386
+ ...controller ? { signal: controller.signal } : {}
138
387
  });
139
- throw error;
388
+ if (this.isRetryableStatus(response.status) && attempt < maxAttempts) {
389
+ if (timer !== void 0) clearTimeout(timer);
390
+ await response.body?.cancel();
391
+ lastError = new InsForgeError(
392
+ `Server error: ${response.status} ${response.statusText}`,
393
+ response.status,
394
+ "SERVER_ERROR"
395
+ );
396
+ continue;
397
+ }
398
+ if (response.status === 204) {
399
+ if (timer !== void 0) clearTimeout(timer);
400
+ return void 0;
401
+ }
402
+ let data;
403
+ const contentType = response.headers.get("content-type");
404
+ try {
405
+ if (contentType?.includes("json")) {
406
+ data = await response.json();
407
+ } else {
408
+ data = await response.text();
409
+ }
410
+ } catch (parseErr) {
411
+ if (timer !== void 0) clearTimeout(timer);
412
+ throw new InsForgeError(
413
+ `Failed to parse response body: ${parseErr?.message || "Unknown error"}`,
414
+ response.status,
415
+ response.ok ? "PARSE_ERROR" : "REQUEST_FAILED"
416
+ );
417
+ }
418
+ if (timer !== void 0) clearTimeout(timer);
419
+ if (!response.ok) {
420
+ this.logger.logResponse(method, url, response.status, Date.now() - startTime, data);
421
+ if (data && typeof data === "object" && "error" in data) {
422
+ if (!data.statusCode && !data.status) {
423
+ data.statusCode = response.status;
424
+ }
425
+ const error = InsForgeError.fromApiError(data);
426
+ Object.keys(data).forEach((key) => {
427
+ if (key !== "error" && key !== "message" && key !== "statusCode") {
428
+ error[key] = data[key];
429
+ }
430
+ });
431
+ throw error;
432
+ }
433
+ throw new InsForgeError(
434
+ `Request failed: ${response.statusText}`,
435
+ response.status,
436
+ "REQUEST_FAILED"
437
+ );
438
+ }
439
+ this.logger.logResponse(method, url, response.status, Date.now() - startTime, data);
440
+ return data;
441
+ } catch (err) {
442
+ if (timer !== void 0) clearTimeout(timer);
443
+ if (err?.name === "AbortError") {
444
+ if (controller && controller.signal.aborted && this.timeout > 0 && !callerSignal?.aborted) {
445
+ throw new InsForgeError(
446
+ `Request timed out after ${this.timeout}ms`,
447
+ 408,
448
+ "REQUEST_TIMEOUT"
449
+ );
450
+ }
451
+ throw err;
452
+ }
453
+ if (err instanceof InsForgeError) {
454
+ throw err;
455
+ }
456
+ if (attempt < maxAttempts) {
457
+ lastError = err;
458
+ continue;
459
+ }
460
+ throw new InsForgeError(
461
+ `Network request failed: ${err?.message || "Unknown error"}`,
462
+ 0,
463
+ "NETWORK_ERROR"
464
+ );
140
465
  }
141
- throw new InsForgeError(
142
- `Request failed: ${response.statusText}`,
143
- response.status,
144
- "REQUEST_FAILED"
145
- );
146
466
  }
147
- return data;
467
+ throw lastError || new InsForgeError(
468
+ "Request failed after all retry attempts",
469
+ 0,
470
+ "NETWORK_ERROR"
471
+ );
148
472
  }
473
+ /** Performs a GET request. */
149
474
  get(path, options) {
150
475
  return this.request("GET", path, options);
151
476
  }
477
+ /** Performs a POST request with an optional JSON body. */
152
478
  post(path, body, options) {
153
479
  return this.request("POST", path, { ...options, body });
154
480
  }
481
+ /** Performs a PUT request with an optional JSON body. */
155
482
  put(path, body, options) {
156
483
  return this.request("PUT", path, { ...options, body });
157
484
  }
485
+ /** Performs a PATCH request with an optional JSON body. */
158
486
  patch(path, body, options) {
159
487
  return this.request("PATCH", path, { ...options, body });
160
488
  }
489
+ /** Performs a DELETE request. */
161
490
  delete(path, options) {
162
491
  return this.request("DELETE", path, options);
163
492
  }
493
+ /** Sets or clears the user authentication token for subsequent requests. */
164
494
  setAuthToken(token) {
165
495
  this.userToken = token;
166
496
  }
497
+ /** Returns the current default headers including the authorization header if set. */
167
498
  getHeaders() {
168
499
  const headers = { ...this.defaultHeaders };
169
500
  const authToken = this.userToken || this.anonKey;
@@ -584,7 +915,7 @@ var Auth = class {
584
915
  const csrfToken = !this.isServerMode() ? getCsrfToken() : null;
585
916
  const response = await this.http.post(
586
917
  this.isServerMode() ? "/api/auth/refresh?client_type=mobile" : "/api/auth/refresh",
587
- this.isServerMode() ? { refreshToken: options?.refreshToken } : void 0,
918
+ this.isServerMode() ? { refresh_token: options?.refreshToken } : void 0,
588
919
  {
589
920
  headers: csrfToken ? { "X-CSRF-Token": csrfToken } : {},
590
921
  credentials: "include"
@@ -1778,7 +2109,8 @@ var Emails = class {
1778
2109
  // src/client.ts
1779
2110
  var InsForgeClient = class {
1780
2111
  constructor(config = {}) {
1781
- this.http = new HttpClient(config);
2112
+ const logger = new Logger(config.debug);
2113
+ this.http = new HttpClient(config, logger);
1782
2114
  this.tokenManager = new TokenManager();
1783
2115
  if (config.edgeFunctionToken) {
1784
2116
  this.http.setAuthToken(config.edgeFunctionToken);
@@ -1831,6 +2163,7 @@ var index_default = InsForgeClient;
1831
2163
  HttpClient,
1832
2164
  InsForgeClient,
1833
2165
  InsForgeError,
2166
+ Logger,
1834
2167
  Realtime,
1835
2168
  Storage,
1836
2169
  StorageBucket,