@insforge/sdk 1.2.0-dev.2 → 1.2.1-dev.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;
@@ -318,6 +649,7 @@ function cleanUrlParams(...params) {
318
649
  }
319
650
 
320
651
  // src/modules/auth/auth.ts
652
+ var import_shared_schemas = require("@insforge/shared-schemas");
321
653
  var Auth = class {
322
654
  constructor(http, tokenManager, options = {}) {
323
655
  this.http = http;
@@ -464,18 +796,23 @@ var Auth = class {
464
796
  async signInWithOAuth(options) {
465
797
  try {
466
798
  const { provider, redirectTo, skipBrowserRedirect } = options;
799
+ const providerKey = encodeURIComponent(provider.toLowerCase());
467
800
  const codeVerifier = generateCodeVerifier();
468
801
  const codeChallenge = await generateCodeChallenge(codeVerifier);
469
802
  storePkceVerifier(codeVerifier);
470
803
  const params = { code_challenge: codeChallenge };
471
804
  if (redirectTo) params.redirect_uri = redirectTo;
472
- const response = await this.http.get(`/api/auth/oauth/${provider}`, { params });
805
+ const isBuiltInProvider = import_shared_schemas.oAuthProvidersSchema.options.includes(
806
+ providerKey
807
+ );
808
+ const oauthPath = isBuiltInProvider ? `/api/auth/oauth/${providerKey}` : `/api/auth/oauth/custom/${providerKey}`;
809
+ const response = await this.http.get(oauthPath, { params });
473
810
  if (!this.isServerMode() && typeof window !== "undefined" && !skipBrowserRedirect) {
474
811
  window.location.href = response.authUrl;
475
812
  return { data: {}, error: null };
476
813
  }
477
814
  return {
478
- data: { url: response.authUrl, provider, codeVerifier },
815
+ data: { url: response.authUrl, provider: providerKey, codeVerifier },
479
816
  error: null
480
817
  };
481
818
  } catch (error) {
@@ -1778,7 +2115,8 @@ var Emails = class {
1778
2115
  // src/client.ts
1779
2116
  var InsForgeClient = class {
1780
2117
  constructor(config = {}) {
1781
- this.http = new HttpClient(config);
2118
+ const logger = new Logger(config.debug);
2119
+ this.http = new HttpClient(config, logger);
1782
2120
  this.tokenManager = new TokenManager();
1783
2121
  if (config.edgeFunctionToken) {
1784
2122
  this.http.setAuthToken(config.edgeFunctionToken);
@@ -1831,6 +2169,7 @@ var index_default = InsForgeClient;
1831
2169
  HttpClient,
1832
2170
  InsForgeClient,
1833
2171
  InsForgeError,
2172
+ Logger,
1834
2173
  Realtime,
1835
2174
  Storage,
1836
2175
  StorageBucket,