@insforge/sdk 1.2.3 → 1.2.5-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.mjs ADDED
@@ -0,0 +1,2322 @@
1
+ // src/types.ts
2
+ var InsForgeError = class _InsForgeError extends Error {
3
+ constructor(message, statusCode, error, nextActions) {
4
+ super(message);
5
+ this.name = "InsForgeError";
6
+ this.statusCode = statusCode;
7
+ this.error = error;
8
+ this.nextActions = nextActions;
9
+ }
10
+ static fromApiError(apiError) {
11
+ return new _InsForgeError(
12
+ apiError.message,
13
+ apiError.statusCode,
14
+ apiError.error,
15
+ apiError.nextActions
16
+ );
17
+ }
18
+ };
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
+
195
+ // src/lib/token-manager.ts
196
+ var CSRF_TOKEN_COOKIE = "insforge_csrf_token";
197
+ function getCsrfToken() {
198
+ if (typeof document === "undefined") return null;
199
+ const match = document.cookie.split(";").find((c) => c.trim().startsWith(`${CSRF_TOKEN_COOKIE}=`));
200
+ if (!match) return null;
201
+ return match.split("=")[1] || null;
202
+ }
203
+ function setCsrfToken(token) {
204
+ if (typeof document === "undefined") return;
205
+ const maxAge = 7 * 24 * 60 * 60;
206
+ const secure = typeof window !== "undefined" && window.location.protocol === "https:" ? "; Secure" : "";
207
+ document.cookie = `${CSRF_TOKEN_COOKIE}=${encodeURIComponent(token)}; path=/; max-age=${maxAge}; SameSite=Lax${secure}`;
208
+ }
209
+ function clearCsrfToken() {
210
+ if (typeof document === "undefined") return;
211
+ const secure = typeof window !== "undefined" && window.location.protocol === "https:" ? "; Secure" : "";
212
+ document.cookie = `${CSRF_TOKEN_COOKIE}=; path=/; max-age=0; SameSite=Lax${secure}`;
213
+ }
214
+ var TokenManager = class {
215
+ constructor() {
216
+ // In-memory storage
217
+ this.accessToken = null;
218
+ this.user = null;
219
+ // Callback for token changes (used by realtime to reconnect with new token)
220
+ this.onTokenChange = null;
221
+ }
222
+ /**
223
+ * Save session in memory
224
+ */
225
+ saveSession(session) {
226
+ const tokenChanged = session.accessToken !== this.accessToken;
227
+ this.accessToken = session.accessToken;
228
+ this.user = session.user;
229
+ if (tokenChanged && this.onTokenChange) {
230
+ this.onTokenChange();
231
+ }
232
+ }
233
+ /**
234
+ * Get current session
235
+ */
236
+ getSession() {
237
+ if (!this.accessToken || !this.user) return null;
238
+ return {
239
+ accessToken: this.accessToken,
240
+ user: this.user
241
+ };
242
+ }
243
+ /**
244
+ * Get access token
245
+ */
246
+ getAccessToken() {
247
+ return this.accessToken;
248
+ }
249
+ /**
250
+ * Set access token
251
+ */
252
+ setAccessToken(token) {
253
+ const tokenChanged = token !== this.accessToken;
254
+ this.accessToken = token;
255
+ if (tokenChanged && this.onTokenChange) {
256
+ this.onTokenChange();
257
+ }
258
+ }
259
+ /**
260
+ * Get user
261
+ */
262
+ getUser() {
263
+ return this.user;
264
+ }
265
+ /**
266
+ * Set user
267
+ */
268
+ setUser(user) {
269
+ this.user = user;
270
+ }
271
+ /**
272
+ * Clear in-memory session
273
+ */
274
+ clearSession() {
275
+ const hadToken = this.accessToken !== null;
276
+ this.accessToken = null;
277
+ this.user = null;
278
+ if (hadToken && this.onTokenChange) {
279
+ this.onTokenChange();
280
+ }
281
+ }
282
+ };
283
+
284
+ // src/lib/http-client.ts
285
+ var RETRYABLE_STATUS_CODES = /* @__PURE__ */ new Set([500, 502, 503, 504]);
286
+ var IDEMPOTENT_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
287
+ function serializeBody(method, body, headers) {
288
+ if (body === void 0) return void 0;
289
+ if (method === "GET" || method === "HEAD") return void 0;
290
+ if (typeof FormData !== "undefined" && body instanceof FormData) {
291
+ return body;
292
+ }
293
+ headers["Content-Type"] = "application/json;charset=UTF-8";
294
+ return JSON.stringify(body);
295
+ }
296
+ async function parseResponse(response) {
297
+ if (response.status === 204) return void 0;
298
+ let data;
299
+ const contentType = response.headers.get("content-type");
300
+ try {
301
+ if (contentType?.includes("json")) {
302
+ data = await response.json();
303
+ } else {
304
+ data = await response.text();
305
+ }
306
+ } catch (parseErr) {
307
+ throw new InsForgeError(
308
+ `Failed to parse response body: ${parseErr?.message || "Unknown error"}`,
309
+ response.status,
310
+ response.ok ? "PARSE_ERROR" : "REQUEST_FAILED"
311
+ );
312
+ }
313
+ if (!response.ok) {
314
+ if (data && typeof data === "object" && "error" in data) {
315
+ data.statusCode ?? (data.statusCode = data.status ?? response.status);
316
+ const error = InsForgeError.fromApiError(data);
317
+ Object.keys(data).forEach((key) => {
318
+ if (key !== "error" && key !== "message" && key !== "statusCode") {
319
+ error[key] = data[key];
320
+ }
321
+ });
322
+ throw error;
323
+ }
324
+ throw new InsForgeError(
325
+ `Request failed: ${response.statusText}`,
326
+ response.status,
327
+ "REQUEST_FAILED"
328
+ );
329
+ }
330
+ return data;
331
+ }
332
+ var HttpClient = class {
333
+ /**
334
+ * Creates a new HttpClient instance.
335
+ * @param config - SDK configuration including baseUrl, timeout, retry settings, and fetch implementation.
336
+ * @param tokenManager - Token manager for session persistence.
337
+ * @param logger - Optional logger instance for request/response debugging.
338
+ */
339
+ constructor(config, tokenManager, logger) {
340
+ this.userToken = null;
341
+ this.autoRefreshToken = true;
342
+ this.isRefreshing = false;
343
+ this.refreshPromise = null;
344
+ this.refreshToken = null;
345
+ this.baseUrl = config.baseUrl || "http://localhost:7130";
346
+ this.autoRefreshToken = config.autoRefreshToken ?? true;
347
+ this.fetch = config.fetch || (globalThis.fetch ? globalThis.fetch.bind(globalThis) : void 0);
348
+ this.anonKey = config.anonKey;
349
+ this.defaultHeaders = {
350
+ ...config.headers
351
+ };
352
+ this.tokenManager = tokenManager ?? new TokenManager();
353
+ this.logger = logger || new Logger(false);
354
+ this.timeout = config.timeout ?? 3e4;
355
+ this.retryCount = config.retryCount ?? 3;
356
+ this.retryDelay = config.retryDelay ?? 500;
357
+ if (!this.fetch) {
358
+ throw new Error(
359
+ "Fetch is not available. Please provide a fetch implementation in the config."
360
+ );
361
+ }
362
+ }
363
+ /**
364
+ * Builds a full URL from a path and optional query parameters.
365
+ * Normalizes PostgREST select parameters for proper syntax.
366
+ */
367
+ buildUrl(path, params) {
368
+ const url = new URL(path, this.baseUrl);
369
+ if (params) {
370
+ Object.entries(params).forEach(([key, value]) => {
371
+ if (key === "select") {
372
+ let normalizedValue = value.replace(/\s+/g, " ").trim();
373
+ normalizedValue = normalizedValue.replace(/\s*\(\s*/g, "(").replace(/\s*\)\s*/g, ")").replace(/\(\s+/g, "(").replace(/\s+\)/g, ")").replace(/,\s+(?=[^()]*\))/g, ",");
374
+ url.searchParams.append(key, normalizedValue);
375
+ } else {
376
+ url.searchParams.append(key, value);
377
+ }
378
+ });
379
+ }
380
+ return url.toString();
381
+ }
382
+ /** Checks if an HTTP status code is eligible for retry (5xx server errors). */
383
+ isRetryableStatus(status) {
384
+ return RETRYABLE_STATUS_CODES.has(status);
385
+ }
386
+ /**
387
+ * Computes the delay before the next retry using exponential backoff with jitter.
388
+ * @param attempt - The current retry attempt number (1-based).
389
+ * @returns Delay in milliseconds.
390
+ */
391
+ computeRetryDelay(attempt) {
392
+ const base = this.retryDelay * Math.pow(2, attempt - 1);
393
+ const jitter = base * (0.85 + Math.random() * 0.3);
394
+ return Math.round(jitter);
395
+ }
396
+ /**
397
+ * Performs an HTTP request with automatic retry and timeout handling.
398
+ * Retries on network errors and 5xx server errors with exponential backoff.
399
+ * Client errors (4xx) and timeouts are thrown immediately without retry.
400
+ * @param method - HTTP method (GET, POST, PUT, PATCH, DELETE).
401
+ * @param path - API path relative to the base URL.
402
+ * @param options - Optional request configuration including headers, body, and query params.
403
+ * @returns Parsed response data.
404
+ * @throws {InsForgeError} On timeout, network failure, or HTTP error responses.
405
+ */
406
+ async handleRequest(method, path, options = {}) {
407
+ const {
408
+ params,
409
+ headers = {},
410
+ body,
411
+ signal: callerSignal,
412
+ ...fetchOptions
413
+ } = options;
414
+ const url = this.buildUrl(path, params);
415
+ const startTime = Date.now();
416
+ const canRetry = IDEMPOTENT_METHODS.has(method.toUpperCase()) || options.idempotent === true;
417
+ const maxAttempts = canRetry ? this.retryCount : 0;
418
+ const requestHeaders = {
419
+ ...this.defaultHeaders
420
+ };
421
+ const authToken = this.userToken || this.anonKey;
422
+ if (authToken) {
423
+ requestHeaders["Authorization"] = `Bearer ${authToken}`;
424
+ }
425
+ const processedBody = serializeBody(method, body, requestHeaders);
426
+ if (headers instanceof Headers) {
427
+ headers.forEach((value, key) => {
428
+ requestHeaders[key] = value;
429
+ });
430
+ } else if (Array.isArray(headers)) {
431
+ headers.forEach(([key, value]) => {
432
+ requestHeaders[key] = value;
433
+ });
434
+ } else {
435
+ Object.assign(requestHeaders, headers);
436
+ }
437
+ this.logger.logRequest(method, url, requestHeaders, processedBody);
438
+ let lastError;
439
+ for (let attempt = 0; attempt <= maxAttempts; attempt++) {
440
+ if (attempt > 0) {
441
+ const delay = this.computeRetryDelay(attempt);
442
+ this.logger.warn(
443
+ `Retry ${attempt}/${maxAttempts} for ${method} ${url} in ${delay}ms`
444
+ );
445
+ if (callerSignal?.aborted) throw callerSignal.reason;
446
+ await new Promise((resolve, reject) => {
447
+ const onAbort = () => {
448
+ clearTimeout(timer2);
449
+ reject(callerSignal.reason);
450
+ };
451
+ const timer2 = setTimeout(() => {
452
+ if (callerSignal)
453
+ callerSignal.removeEventListener("abort", onAbort);
454
+ resolve();
455
+ }, delay);
456
+ if (callerSignal) {
457
+ callerSignal.addEventListener("abort", onAbort, { once: true });
458
+ }
459
+ });
460
+ }
461
+ let controller;
462
+ let timer;
463
+ if (this.timeout > 0 || callerSignal) {
464
+ controller = new AbortController();
465
+ if (this.timeout > 0) {
466
+ timer = setTimeout(() => controller.abort(), this.timeout);
467
+ }
468
+ if (callerSignal) {
469
+ if (callerSignal.aborted) {
470
+ controller.abort(callerSignal.reason);
471
+ } else {
472
+ const onCallerAbort = () => controller.abort(callerSignal.reason);
473
+ callerSignal.addEventListener("abort", onCallerAbort, {
474
+ once: true
475
+ });
476
+ controller.signal.addEventListener(
477
+ "abort",
478
+ () => {
479
+ callerSignal.removeEventListener("abort", onCallerAbort);
480
+ },
481
+ { once: true }
482
+ );
483
+ }
484
+ }
485
+ }
486
+ try {
487
+ const response = await this.fetch(url, {
488
+ method,
489
+ headers: requestHeaders,
490
+ body: processedBody,
491
+ ...fetchOptions,
492
+ ...controller ? { signal: controller.signal } : {}
493
+ });
494
+ if (this.isRetryableStatus(response.status) && attempt < maxAttempts) {
495
+ if (timer !== void 0) clearTimeout(timer);
496
+ await response.body?.cancel();
497
+ lastError = new InsForgeError(
498
+ `Server error: ${response.status} ${response.statusText}`,
499
+ response.status,
500
+ "SERVER_ERROR"
501
+ );
502
+ continue;
503
+ }
504
+ let data;
505
+ try {
506
+ data = await parseResponse(response);
507
+ } catch (err) {
508
+ if (timer !== void 0) clearTimeout(timer);
509
+ if (err instanceof InsForgeError) {
510
+ this.logger.logResponse(
511
+ method,
512
+ url,
513
+ err.statusCode || response.status,
514
+ Date.now() - startTime,
515
+ err
516
+ );
517
+ }
518
+ throw err;
519
+ }
520
+ if (timer !== void 0) clearTimeout(timer);
521
+ this.logger.logResponse(
522
+ method,
523
+ url,
524
+ response.status,
525
+ Date.now() - startTime,
526
+ data
527
+ );
528
+ return data;
529
+ } catch (err) {
530
+ if (timer !== void 0) clearTimeout(timer);
531
+ if (err?.name === "AbortError") {
532
+ if (controller && controller.signal.aborted && this.timeout > 0 && !callerSignal?.aborted) {
533
+ throw new InsForgeError(
534
+ `Request timed out after ${this.timeout}ms`,
535
+ 408,
536
+ "REQUEST_TIMEOUT"
537
+ );
538
+ }
539
+ throw err;
540
+ }
541
+ if (err instanceof InsForgeError) {
542
+ throw err;
543
+ }
544
+ if (attempt < maxAttempts) {
545
+ lastError = err;
546
+ continue;
547
+ }
548
+ throw new InsForgeError(
549
+ `Network request failed: ${err?.message || "Unknown error"}`,
550
+ 0,
551
+ "NETWORK_ERROR"
552
+ );
553
+ }
554
+ }
555
+ throw lastError || new InsForgeError(
556
+ "Request failed after all retry attempts",
557
+ 0,
558
+ "NETWORK_ERROR"
559
+ );
560
+ }
561
+ async request(method, path, options = {}) {
562
+ try {
563
+ return await this.handleRequest(method, path, { ...options });
564
+ } catch (error) {
565
+ if (error instanceof InsForgeError && error.statusCode === 401 && error.error === "INVALID_TOKEN" && this.autoRefreshToken) {
566
+ try {
567
+ const newTokenData = await this.handleTokenRefresh();
568
+ this.setAuthToken(newTokenData.accessToken);
569
+ this.tokenManager.saveSession(newTokenData);
570
+ if (newTokenData.csrfToken) {
571
+ setCsrfToken(newTokenData.csrfToken);
572
+ }
573
+ if (newTokenData.refreshToken) {
574
+ this.setRefreshToken(newTokenData.refreshToken);
575
+ }
576
+ return await this.handleRequest(method, path, { ...options });
577
+ } catch (error2) {
578
+ this.tokenManager.clearSession();
579
+ this.userToken = null;
580
+ this.refreshToken = null;
581
+ clearCsrfToken();
582
+ throw error2;
583
+ }
584
+ }
585
+ throw error;
586
+ }
587
+ }
588
+ /** Performs a GET request. */
589
+ get(path, options) {
590
+ return this.request("GET", path, options);
591
+ }
592
+ /** Performs a POST request with an optional JSON body. */
593
+ post(path, body, options) {
594
+ return this.request("POST", path, { ...options, body });
595
+ }
596
+ /** Performs a PUT request with an optional JSON body. */
597
+ put(path, body, options) {
598
+ return this.request("PUT", path, { ...options, body });
599
+ }
600
+ /** Performs a PATCH request with an optional JSON body. */
601
+ patch(path, body, options) {
602
+ return this.request("PATCH", path, { ...options, body });
603
+ }
604
+ /** Performs a DELETE request. */
605
+ delete(path, options) {
606
+ return this.request("DELETE", path, options);
607
+ }
608
+ /** Sets or clears the user authentication token for subsequent requests. */
609
+ setAuthToken(token) {
610
+ this.userToken = token;
611
+ }
612
+ setRefreshToken(token) {
613
+ this.refreshToken = token;
614
+ }
615
+ /** Returns the current default headers including the authorization header if set. */
616
+ getHeaders() {
617
+ const headers = { ...this.defaultHeaders };
618
+ const authToken = this.userToken || this.anonKey;
619
+ if (authToken) {
620
+ headers["Authorization"] = `Bearer ${authToken}`;
621
+ }
622
+ return headers;
623
+ }
624
+ async handleTokenRefresh() {
625
+ if (this.isRefreshing) {
626
+ return this.refreshPromise;
627
+ }
628
+ this.isRefreshing = true;
629
+ this.refreshPromise = (async () => {
630
+ try {
631
+ const csrfToken = getCsrfToken();
632
+ const body = this.refreshToken ? { refreshToken: this.refreshToken } : void 0;
633
+ const response = await this.handleRequest(
634
+ "POST",
635
+ "/api/auth/sessions/current",
636
+ {
637
+ body,
638
+ headers: csrfToken ? { "X-CSRF-Token": csrfToken } : {},
639
+ credentials: "include"
640
+ }
641
+ );
642
+ return response;
643
+ } finally {
644
+ this.isRefreshing = false;
645
+ this.refreshPromise = null;
646
+ }
647
+ })();
648
+ return this.refreshPromise;
649
+ }
650
+ };
651
+
652
+ // src/modules/auth/helpers.ts
653
+ var PKCE_VERIFIER_KEY = "insforge_pkce_verifier";
654
+ function base64UrlEncode(buffer) {
655
+ const base64 = btoa(String.fromCharCode(...buffer));
656
+ return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
657
+ }
658
+ function generateCodeVerifier() {
659
+ const array = new Uint8Array(32);
660
+ crypto.getRandomValues(array);
661
+ return base64UrlEncode(array);
662
+ }
663
+ async function generateCodeChallenge(verifier) {
664
+ const encoder = new TextEncoder();
665
+ const data = encoder.encode(verifier);
666
+ const hash = await crypto.subtle.digest("SHA-256", data);
667
+ return base64UrlEncode(new Uint8Array(hash));
668
+ }
669
+ function storePkceVerifier(verifier) {
670
+ if (typeof sessionStorage !== "undefined") {
671
+ sessionStorage.setItem(PKCE_VERIFIER_KEY, verifier);
672
+ }
673
+ }
674
+ function retrievePkceVerifier() {
675
+ if (typeof sessionStorage === "undefined") {
676
+ return null;
677
+ }
678
+ const verifier = sessionStorage.getItem(PKCE_VERIFIER_KEY);
679
+ if (verifier) {
680
+ sessionStorage.removeItem(PKCE_VERIFIER_KEY);
681
+ }
682
+ return verifier;
683
+ }
684
+ function wrapError(error, fallbackMessage) {
685
+ if (error instanceof InsForgeError) {
686
+ return { data: null, error };
687
+ }
688
+ return {
689
+ data: null,
690
+ error: new InsForgeError(
691
+ error instanceof Error ? error.message : fallbackMessage,
692
+ 500,
693
+ "UNEXPECTED_ERROR"
694
+ )
695
+ };
696
+ }
697
+ function cleanUrlParams(...params) {
698
+ if (typeof window === "undefined") {
699
+ return;
700
+ }
701
+ const url = new URL(window.location.href);
702
+ params.forEach((p) => url.searchParams.delete(p));
703
+ window.history.replaceState({}, document.title, url.toString());
704
+ }
705
+
706
+ // src/modules/auth/auth.ts
707
+ import { oAuthProvidersSchema } from "@insforge/shared-schemas";
708
+ var Auth = class {
709
+ constructor(http, tokenManager, options = {}) {
710
+ this.http = http;
711
+ this.tokenManager = tokenManager;
712
+ this.options = options;
713
+ this.authCallbackHandled = this.detectAuthCallback();
714
+ }
715
+ isServerMode() {
716
+ return !!this.options.isServerMode;
717
+ }
718
+ /**
719
+ * Save session from API response
720
+ * Handles token storage, CSRF token, and HTTP auth header
721
+ */
722
+ saveSessionFromResponse(response) {
723
+ if (!response.accessToken || !response.user) {
724
+ return false;
725
+ }
726
+ const session = {
727
+ accessToken: response.accessToken,
728
+ user: response.user
729
+ };
730
+ if (!this.isServerMode() && response.csrfToken) {
731
+ setCsrfToken(response.csrfToken);
732
+ }
733
+ if (!this.isServerMode()) {
734
+ this.tokenManager.saveSession(session);
735
+ }
736
+ this.http.setAuthToken(response.accessToken);
737
+ this.http.setRefreshToken(response.refreshToken ?? null);
738
+ return true;
739
+ }
740
+ // ============================================================================
741
+ // OAuth Callback Detection (runs on initialization)
742
+ // ============================================================================
743
+ /**
744
+ * Detect and handle OAuth callback parameters in URL
745
+ * Supports PKCE flow (insforge_code)
746
+ */
747
+ async detectAuthCallback() {
748
+ if (this.isServerMode() || typeof window === "undefined") return;
749
+ try {
750
+ const params = new URLSearchParams(window.location.search);
751
+ const error = params.get("error");
752
+ if (error) {
753
+ cleanUrlParams("error");
754
+ console.debug("OAuth callback error:", error);
755
+ return;
756
+ }
757
+ const code = params.get("insforge_code");
758
+ if (code) {
759
+ cleanUrlParams("insforge_code");
760
+ const { error: exchangeError } = await this.exchangeOAuthCode(code);
761
+ if (exchangeError) {
762
+ console.debug("OAuth code exchange failed:", exchangeError.message);
763
+ }
764
+ return;
765
+ }
766
+ } catch (error) {
767
+ console.debug("OAuth callback detection skipped:", error);
768
+ }
769
+ }
770
+ // ============================================================================
771
+ // Sign Up / Sign In / Sign Out
772
+ // ============================================================================
773
+ async signUp(request) {
774
+ try {
775
+ const response = await this.http.post(
776
+ this.isServerMode() ? "/api/auth/users?client_type=mobile" : "/api/auth/users",
777
+ request,
778
+ { credentials: "include" }
779
+ );
780
+ if (response.accessToken && response.user) {
781
+ this.saveSessionFromResponse(response);
782
+ }
783
+ if (response.refreshToken) {
784
+ this.http.setRefreshToken(response.refreshToken);
785
+ }
786
+ return { data: response, error: null };
787
+ } catch (error) {
788
+ return wrapError(error, "An unexpected error occurred during sign up");
789
+ }
790
+ }
791
+ async signInWithPassword(request) {
792
+ try {
793
+ const response = await this.http.post(
794
+ this.isServerMode() ? "/api/auth/sessions?client_type=mobile" : "/api/auth/sessions",
795
+ request,
796
+ { credentials: "include" }
797
+ );
798
+ this.saveSessionFromResponse(response);
799
+ if (response.refreshToken) {
800
+ this.http.setRefreshToken(response.refreshToken);
801
+ }
802
+ return { data: response, error: null };
803
+ } catch (error) {
804
+ return wrapError(error, "An unexpected error occurred during sign in");
805
+ }
806
+ }
807
+ async signOut() {
808
+ try {
809
+ try {
810
+ await this.http.post(
811
+ this.isServerMode() ? "/api/auth/logout?client_type=mobile" : "/api/auth/logout",
812
+ void 0,
813
+ { credentials: "include" }
814
+ );
815
+ } catch {
816
+ }
817
+ this.tokenManager.clearSession();
818
+ this.http.setAuthToken(null);
819
+ this.http.setRefreshToken(null);
820
+ if (!this.isServerMode()) {
821
+ clearCsrfToken();
822
+ }
823
+ return { error: null };
824
+ } catch {
825
+ return {
826
+ error: new InsForgeError("Failed to sign out", 500, "SIGNOUT_ERROR")
827
+ };
828
+ }
829
+ }
830
+ // ============================================================================
831
+ // OAuth Authentication
832
+ // ============================================================================
833
+ /**
834
+ * Sign in with OAuth provider using PKCE flow
835
+ */
836
+ async signInWithOAuth(options) {
837
+ try {
838
+ const { provider, redirectTo, skipBrowserRedirect } = options;
839
+ const providerKey = encodeURIComponent(provider.toLowerCase());
840
+ const codeVerifier = generateCodeVerifier();
841
+ const codeChallenge = await generateCodeChallenge(codeVerifier);
842
+ storePkceVerifier(codeVerifier);
843
+ const params = { code_challenge: codeChallenge };
844
+ if (redirectTo) params.redirect_uri = redirectTo;
845
+ const isBuiltInProvider = oAuthProvidersSchema.options.includes(
846
+ providerKey
847
+ );
848
+ const oauthPath = isBuiltInProvider ? `/api/auth/oauth/${providerKey}` : `/api/auth/oauth/custom/${providerKey}`;
849
+ const response = await this.http.get(oauthPath, {
850
+ params
851
+ });
852
+ if (!this.isServerMode() && typeof window !== "undefined" && !skipBrowserRedirect) {
853
+ window.location.href = response.authUrl;
854
+ return { data: {}, error: null };
855
+ }
856
+ return {
857
+ data: { url: response.authUrl, provider: providerKey, codeVerifier },
858
+ error: null
859
+ };
860
+ } catch (error) {
861
+ if (error instanceof InsForgeError) {
862
+ return { data: {}, error };
863
+ }
864
+ return {
865
+ data: {},
866
+ error: new InsForgeError(
867
+ "An unexpected error occurred during OAuth initialization",
868
+ 500,
869
+ "UNEXPECTED_ERROR"
870
+ )
871
+ };
872
+ }
873
+ }
874
+ /**
875
+ * Exchange OAuth authorization code for tokens (PKCE flow)
876
+ * Called automatically on initialization when insforge_code is in URL
877
+ */
878
+ async exchangeOAuthCode(code, codeVerifier) {
879
+ try {
880
+ const verifier = codeVerifier ?? retrievePkceVerifier();
881
+ if (!verifier) {
882
+ return {
883
+ data: null,
884
+ error: new InsForgeError(
885
+ "PKCE code verifier not found. Ensure signInWithOAuth was called in the same browser session.",
886
+ 400,
887
+ "PKCE_VERIFIER_MISSING"
888
+ )
889
+ };
890
+ }
891
+ const request = {
892
+ code,
893
+ code_verifier: verifier
894
+ };
895
+ const response = await this.http.post(
896
+ this.isServerMode() ? "/api/auth/oauth/exchange?client_type=mobile" : "/api/auth/oauth/exchange",
897
+ request,
898
+ { credentials: "include" }
899
+ );
900
+ this.saveSessionFromResponse(response);
901
+ return {
902
+ data: response,
903
+ error: null
904
+ };
905
+ } catch (error) {
906
+ return wrapError(
907
+ error,
908
+ "An unexpected error occurred during OAuth code exchange"
909
+ );
910
+ }
911
+ }
912
+ /**
913
+ * Sign in with an ID token from a native SDK (Google One Tap, etc.)
914
+ * Use this for native mobile apps or Google One Tap on web.
915
+ *
916
+ * @param credentials.provider - The identity provider (currently only 'google' is supported)
917
+ * @param credentials.token - The ID token from the native SDK
918
+ */
919
+ async signInWithIdToken(credentials) {
920
+ try {
921
+ const { provider, token } = credentials;
922
+ const response = await this.http.post(
923
+ "/api/auth/id-token?client_type=mobile",
924
+ { provider, token },
925
+ { credentials: "include" }
926
+ );
927
+ this.saveSessionFromResponse(response);
928
+ if (response.refreshToken) {
929
+ this.http.setRefreshToken(response.refreshToken);
930
+ }
931
+ return {
932
+ data: response,
933
+ error: null
934
+ };
935
+ } catch (error) {
936
+ return wrapError(
937
+ error,
938
+ "An unexpected error occurred during ID token sign in"
939
+ );
940
+ }
941
+ }
942
+ // ============================================================================
943
+ // Session Management
944
+ // ============================================================================
945
+ /**
946
+ * Refresh the current auth session.
947
+ *
948
+ * Browser mode:
949
+ * - Uses httpOnly refresh cookie and optional CSRF header.
950
+ *
951
+ * Server mode (`isServerMode: true`):
952
+ * - Uses mobile auth flow and requires `refreshToken` in request body.
953
+ */
954
+ async refreshSession(options) {
955
+ try {
956
+ if (this.isServerMode() && !options?.refreshToken) {
957
+ return {
958
+ data: null,
959
+ error: new InsForgeError(
960
+ "refreshToken is required when refreshing session in server mode",
961
+ 400,
962
+ "REFRESH_TOKEN_REQUIRED"
963
+ )
964
+ };
965
+ }
966
+ const csrfToken = !this.isServerMode() ? getCsrfToken() : null;
967
+ const response = await this.http.post(
968
+ this.isServerMode() ? "/api/auth/refresh?client_type=mobile" : "/api/auth/refresh",
969
+ this.isServerMode() ? { refresh_token: options?.refreshToken } : void 0,
970
+ {
971
+ headers: csrfToken ? { "X-CSRF-Token": csrfToken } : {},
972
+ credentials: "include"
973
+ }
974
+ );
975
+ if (response.accessToken) {
976
+ this.saveSessionFromResponse(response);
977
+ }
978
+ return { data: response, error: null };
979
+ } catch (error) {
980
+ return wrapError(
981
+ error,
982
+ "An unexpected error occurred during session refresh"
983
+ );
984
+ }
985
+ }
986
+ /**
987
+ * Get current user, automatically waits for pending OAuth callback
988
+ */
989
+ async getCurrentUser() {
990
+ await this.authCallbackHandled;
991
+ try {
992
+ if (this.isServerMode()) {
993
+ const accessToken = this.tokenManager.getAccessToken();
994
+ if (!accessToken) return { data: { user: null }, error: null };
995
+ this.http.setAuthToken(accessToken);
996
+ const response = await this.http.get(
997
+ "/api/auth/sessions/current"
998
+ );
999
+ const user = response.user ?? null;
1000
+ return { data: { user }, error: null };
1001
+ }
1002
+ const session = this.tokenManager.getSession();
1003
+ if (session) {
1004
+ this.http.setAuthToken(session.accessToken);
1005
+ return { data: { user: session.user }, error: null };
1006
+ }
1007
+ if (typeof window !== "undefined") {
1008
+ const { data: refreshed, error: refreshError } = await this.refreshSession();
1009
+ if (refreshError) {
1010
+ return { data: { user: null }, error: refreshError };
1011
+ }
1012
+ if (refreshed?.accessToken) {
1013
+ return { data: { user: refreshed.user ?? null }, error: null };
1014
+ }
1015
+ }
1016
+ return { data: { user: null }, error: null };
1017
+ } catch (error) {
1018
+ if (error instanceof InsForgeError) {
1019
+ return { data: { user: null }, error };
1020
+ }
1021
+ return {
1022
+ data: { user: null },
1023
+ error: new InsForgeError(
1024
+ "An unexpected error occurred while getting user",
1025
+ 500,
1026
+ "UNEXPECTED_ERROR"
1027
+ )
1028
+ };
1029
+ }
1030
+ }
1031
+ // ============================================================================
1032
+ // Profile Management
1033
+ // ============================================================================
1034
+ async getProfile(userId) {
1035
+ try {
1036
+ const response = await this.http.get(
1037
+ `/api/auth/profiles/${userId}`
1038
+ );
1039
+ return { data: response, error: null };
1040
+ } catch (error) {
1041
+ return wrapError(
1042
+ error,
1043
+ "An unexpected error occurred while fetching user profile"
1044
+ );
1045
+ }
1046
+ }
1047
+ async setProfile(profile) {
1048
+ try {
1049
+ const response = await this.http.patch(
1050
+ "/api/auth/profiles/current",
1051
+ {
1052
+ profile
1053
+ }
1054
+ );
1055
+ const currentUser = this.tokenManager.getUser();
1056
+ if (!this.isServerMode() && currentUser && response.profile !== void 0) {
1057
+ this.tokenManager.setUser({
1058
+ ...currentUser,
1059
+ profile: response.profile
1060
+ });
1061
+ }
1062
+ return { data: response, error: null };
1063
+ } catch (error) {
1064
+ return wrapError(
1065
+ error,
1066
+ "An unexpected error occurred while updating user profile"
1067
+ );
1068
+ }
1069
+ }
1070
+ // ============================================================================
1071
+ // Email Verification
1072
+ // ============================================================================
1073
+ async resendVerificationEmail(request) {
1074
+ try {
1075
+ const response = await this.http.post("/api/auth/email/send-verification", request);
1076
+ return { data: response, error: null };
1077
+ } catch (error) {
1078
+ return wrapError(
1079
+ error,
1080
+ "An unexpected error occurred while sending verification email"
1081
+ );
1082
+ }
1083
+ }
1084
+ async verifyEmail(request) {
1085
+ try {
1086
+ const response = await this.http.post(
1087
+ this.isServerMode() ? "/api/auth/email/verify?client_type=mobile" : "/api/auth/email/verify",
1088
+ request,
1089
+ { credentials: "include" }
1090
+ );
1091
+ this.saveSessionFromResponse(response);
1092
+ if (response.refreshToken) {
1093
+ this.http.setRefreshToken(response.refreshToken);
1094
+ }
1095
+ return { data: response, error: null };
1096
+ } catch (error) {
1097
+ return wrapError(
1098
+ error,
1099
+ "An unexpected error occurred while verifying email"
1100
+ );
1101
+ }
1102
+ }
1103
+ // ============================================================================
1104
+ // Password Reset
1105
+ // ============================================================================
1106
+ async sendResetPasswordEmail(request) {
1107
+ try {
1108
+ const response = await this.http.post("/api/auth/email/send-reset-password", request);
1109
+ return { data: response, error: null };
1110
+ } catch (error) {
1111
+ return wrapError(
1112
+ error,
1113
+ "An unexpected error occurred while sending password reset email"
1114
+ );
1115
+ }
1116
+ }
1117
+ async exchangeResetPasswordToken(request) {
1118
+ try {
1119
+ const response = await this.http.post(
1120
+ "/api/auth/email/exchange-reset-password-token",
1121
+ request
1122
+ );
1123
+ return { data: response, error: null };
1124
+ } catch (error) {
1125
+ return wrapError(
1126
+ error,
1127
+ "An unexpected error occurred while verifying reset code"
1128
+ );
1129
+ }
1130
+ }
1131
+ async resetPassword(request) {
1132
+ try {
1133
+ const response = await this.http.post(
1134
+ "/api/auth/email/reset-password",
1135
+ request
1136
+ );
1137
+ return { data: response, error: null };
1138
+ } catch (error) {
1139
+ return wrapError(
1140
+ error,
1141
+ "An unexpected error occurred while resetting password"
1142
+ );
1143
+ }
1144
+ }
1145
+ // ============================================================================
1146
+ // Configuration
1147
+ // ============================================================================
1148
+ async getPublicAuthConfig() {
1149
+ try {
1150
+ const response = await this.http.get(
1151
+ "/api/auth/public-config"
1152
+ );
1153
+ return { data: response, error: null };
1154
+ } catch (error) {
1155
+ return wrapError(
1156
+ error,
1157
+ "An unexpected error occurred while fetching auth configuration"
1158
+ );
1159
+ }
1160
+ }
1161
+ };
1162
+
1163
+ // src/modules/database-postgrest.ts
1164
+ import { PostgrestClient } from "@supabase/postgrest-js";
1165
+ function createInsForgePostgrestFetch(httpClient, tokenManager) {
1166
+ return async (input, init) => {
1167
+ const url = typeof input === "string" ? input : input.toString();
1168
+ const urlObj = new URL(url);
1169
+ const pathname = urlObj.pathname.slice(1);
1170
+ const rpcMatch = pathname.match(/^rpc\/(.+)$/);
1171
+ const endpoint = rpcMatch ? `/api/database/rpc/${rpcMatch[1]}` : `/api/database/records/${pathname}`;
1172
+ const insforgeUrl = `${httpClient.baseUrl}${endpoint}${urlObj.search}`;
1173
+ const token = tokenManager.getAccessToken();
1174
+ const httpHeaders = httpClient.getHeaders();
1175
+ const authToken = token || httpHeaders["Authorization"]?.replace("Bearer ", "");
1176
+ const headers = new Headers(init?.headers);
1177
+ if (authToken && !headers.has("Authorization")) {
1178
+ headers.set("Authorization", `Bearer ${authToken}`);
1179
+ }
1180
+ const response = await fetch(insforgeUrl, {
1181
+ ...init,
1182
+ headers
1183
+ });
1184
+ return response;
1185
+ };
1186
+ }
1187
+ var Database = class {
1188
+ constructor(httpClient, tokenManager) {
1189
+ this.postgrest = new PostgrestClient("http://dummy", {
1190
+ fetch: createInsForgePostgrestFetch(httpClient, tokenManager),
1191
+ headers: {}
1192
+ });
1193
+ }
1194
+ /**
1195
+ * Create a query builder for a table
1196
+ *
1197
+ * @example
1198
+ * // Basic query
1199
+ * const { data, error } = await client.database
1200
+ * .from('posts')
1201
+ * .select('*')
1202
+ * .eq('user_id', userId);
1203
+ *
1204
+ * // With count (Supabase style!)
1205
+ * const { data, error, count } = await client.database
1206
+ * .from('posts')
1207
+ * .select('*', { count: 'exact' })
1208
+ * .range(0, 9);
1209
+ *
1210
+ * // Just get count, no data
1211
+ * const { count } = await client.database
1212
+ * .from('posts')
1213
+ * .select('*', { count: 'exact', head: true });
1214
+ *
1215
+ * // Complex queries with OR
1216
+ * const { data } = await client.database
1217
+ * .from('posts')
1218
+ * .select('*, users!inner(*)')
1219
+ * .or('status.eq.active,status.eq.pending');
1220
+ *
1221
+ * // All features work:
1222
+ * - Nested selects
1223
+ * - Foreign key expansion
1224
+ * - OR/AND/NOT conditions
1225
+ * - Count with head
1226
+ * - Range pagination
1227
+ * - Upserts
1228
+ */
1229
+ from(table) {
1230
+ return this.postgrest.from(table);
1231
+ }
1232
+ /**
1233
+ * Call a PostgreSQL function (RPC)
1234
+ *
1235
+ * @example
1236
+ * // Call a function with parameters
1237
+ * const { data, error } = await client.database
1238
+ * .rpc('get_user_stats', { user_id: 123 });
1239
+ *
1240
+ * // Call a function with no parameters
1241
+ * const { data, error } = await client.database
1242
+ * .rpc('get_all_active_users');
1243
+ *
1244
+ * // With options (head, count, get)
1245
+ * const { data, count } = await client.database
1246
+ * .rpc('search_posts', { query: 'hello' }, { count: 'exact' });
1247
+ */
1248
+ rpc(fn, args, options) {
1249
+ return this.postgrest.rpc(fn, args, options);
1250
+ }
1251
+ };
1252
+
1253
+ // src/modules/storage.ts
1254
+ var StorageBucket = class {
1255
+ constructor(bucketName, http) {
1256
+ this.bucketName = bucketName;
1257
+ this.http = http;
1258
+ }
1259
+ /**
1260
+ * Upload a file with a specific key
1261
+ * Uses the upload strategy from backend (direct or presigned)
1262
+ * @param path - The object key/path
1263
+ * @param file - File or Blob to upload
1264
+ */
1265
+ async upload(path, file) {
1266
+ try {
1267
+ const strategyResponse = await this.http.post(
1268
+ `/api/storage/buckets/${this.bucketName}/upload-strategy`,
1269
+ {
1270
+ filename: path,
1271
+ contentType: file.type || "application/octet-stream",
1272
+ size: file.size
1273
+ }
1274
+ );
1275
+ if (strategyResponse.method === "presigned") {
1276
+ return await this.uploadWithPresignedUrl(strategyResponse, file);
1277
+ }
1278
+ if (strategyResponse.method === "direct") {
1279
+ const formData = new FormData();
1280
+ formData.append("file", file);
1281
+ const response = await this.http.request(
1282
+ "PUT",
1283
+ `/api/storage/buckets/${this.bucketName}/objects/${encodeURIComponent(path)}`,
1284
+ {
1285
+ body: formData,
1286
+ headers: {
1287
+ // Don't set Content-Type, let browser set multipart boundary
1288
+ }
1289
+ }
1290
+ );
1291
+ return { data: response, error: null };
1292
+ }
1293
+ throw new InsForgeError(
1294
+ `Unsupported upload method: ${strategyResponse.method}`,
1295
+ 500,
1296
+ "STORAGE_ERROR"
1297
+ );
1298
+ } catch (error) {
1299
+ return {
1300
+ data: null,
1301
+ error: error instanceof InsForgeError ? error : new InsForgeError(
1302
+ "Upload failed",
1303
+ 500,
1304
+ "STORAGE_ERROR"
1305
+ )
1306
+ };
1307
+ }
1308
+ }
1309
+ /**
1310
+ * Upload a file with auto-generated key
1311
+ * Uses the upload strategy from backend (direct or presigned)
1312
+ * @param file - File or Blob to upload
1313
+ */
1314
+ async uploadAuto(file) {
1315
+ try {
1316
+ const filename = file instanceof File ? file.name : "file";
1317
+ const strategyResponse = await this.http.post(
1318
+ `/api/storage/buckets/${this.bucketName}/upload-strategy`,
1319
+ {
1320
+ filename,
1321
+ contentType: file.type || "application/octet-stream",
1322
+ size: file.size
1323
+ }
1324
+ );
1325
+ if (strategyResponse.method === "presigned") {
1326
+ return await this.uploadWithPresignedUrl(strategyResponse, file);
1327
+ }
1328
+ if (strategyResponse.method === "direct") {
1329
+ const formData = new FormData();
1330
+ formData.append("file", file);
1331
+ const response = await this.http.request(
1332
+ "POST",
1333
+ `/api/storage/buckets/${this.bucketName}/objects`,
1334
+ {
1335
+ body: formData,
1336
+ headers: {
1337
+ // Don't set Content-Type, let browser set multipart boundary
1338
+ }
1339
+ }
1340
+ );
1341
+ return { data: response, error: null };
1342
+ }
1343
+ throw new InsForgeError(
1344
+ `Unsupported upload method: ${strategyResponse.method}`,
1345
+ 500,
1346
+ "STORAGE_ERROR"
1347
+ );
1348
+ } catch (error) {
1349
+ return {
1350
+ data: null,
1351
+ error: error instanceof InsForgeError ? error : new InsForgeError(
1352
+ "Upload failed",
1353
+ 500,
1354
+ "STORAGE_ERROR"
1355
+ )
1356
+ };
1357
+ }
1358
+ }
1359
+ /**
1360
+ * Internal method to handle presigned URL uploads
1361
+ */
1362
+ async uploadWithPresignedUrl(strategy, file) {
1363
+ try {
1364
+ const formData = new FormData();
1365
+ if (strategy.fields) {
1366
+ Object.entries(strategy.fields).forEach(([key, value]) => {
1367
+ formData.append(key, value);
1368
+ });
1369
+ }
1370
+ formData.append("file", file);
1371
+ const uploadResponse = await fetch(strategy.uploadUrl, {
1372
+ method: "POST",
1373
+ body: formData
1374
+ });
1375
+ if (!uploadResponse.ok) {
1376
+ throw new InsForgeError(
1377
+ `Upload to storage failed: ${uploadResponse.statusText}`,
1378
+ uploadResponse.status,
1379
+ "STORAGE_ERROR"
1380
+ );
1381
+ }
1382
+ if (strategy.confirmRequired && strategy.confirmUrl) {
1383
+ const confirmResponse = await this.http.post(
1384
+ strategy.confirmUrl,
1385
+ {
1386
+ size: file.size,
1387
+ contentType: file.type || "application/octet-stream"
1388
+ }
1389
+ );
1390
+ return { data: confirmResponse, error: null };
1391
+ }
1392
+ return {
1393
+ data: {
1394
+ key: strategy.key,
1395
+ bucket: this.bucketName,
1396
+ size: file.size,
1397
+ mimeType: file.type || "application/octet-stream",
1398
+ uploadedAt: (/* @__PURE__ */ new Date()).toISOString(),
1399
+ url: this.getPublicUrl(strategy.key)
1400
+ },
1401
+ error: null
1402
+ };
1403
+ } catch (error) {
1404
+ throw error instanceof InsForgeError ? error : new InsForgeError(
1405
+ "Presigned upload failed",
1406
+ 500,
1407
+ "STORAGE_ERROR"
1408
+ );
1409
+ }
1410
+ }
1411
+ /**
1412
+ * Download a file
1413
+ * Uses the download strategy from backend (direct or presigned)
1414
+ * @param path - The object key/path
1415
+ * Returns the file as a Blob
1416
+ */
1417
+ async download(path) {
1418
+ try {
1419
+ const strategyResponse = await this.http.post(
1420
+ `/api/storage/buckets/${this.bucketName}/objects/${encodeURIComponent(path)}/download-strategy`,
1421
+ { expiresIn: 3600 }
1422
+ );
1423
+ const downloadUrl = strategyResponse.url;
1424
+ const headers = {};
1425
+ if (strategyResponse.method === "direct") {
1426
+ Object.assign(headers, this.http.getHeaders());
1427
+ }
1428
+ const response = await fetch(downloadUrl, {
1429
+ method: "GET",
1430
+ headers
1431
+ });
1432
+ if (!response.ok) {
1433
+ try {
1434
+ const error = await response.json();
1435
+ throw InsForgeError.fromApiError(error);
1436
+ } catch {
1437
+ throw new InsForgeError(
1438
+ `Download failed: ${response.statusText}`,
1439
+ response.status,
1440
+ "STORAGE_ERROR"
1441
+ );
1442
+ }
1443
+ }
1444
+ const blob = await response.blob();
1445
+ return { data: blob, error: null };
1446
+ } catch (error) {
1447
+ return {
1448
+ data: null,
1449
+ error: error instanceof InsForgeError ? error : new InsForgeError(
1450
+ "Download failed",
1451
+ 500,
1452
+ "STORAGE_ERROR"
1453
+ )
1454
+ };
1455
+ }
1456
+ }
1457
+ /**
1458
+ * Get public URL for a file
1459
+ * @param path - The object key/path
1460
+ */
1461
+ getPublicUrl(path) {
1462
+ return `${this.http.baseUrl}/api/storage/buckets/${this.bucketName}/objects/${encodeURIComponent(path)}`;
1463
+ }
1464
+ /**
1465
+ * List objects in the bucket
1466
+ * @param prefix - Filter by key prefix
1467
+ * @param search - Search in file names
1468
+ * @param limit - Maximum number of results (default: 100, max: 1000)
1469
+ * @param offset - Number of results to skip
1470
+ */
1471
+ async list(options) {
1472
+ try {
1473
+ const params = {};
1474
+ if (options?.prefix) params.prefix = options.prefix;
1475
+ if (options?.search) params.search = options.search;
1476
+ if (options?.limit) params.limit = options.limit.toString();
1477
+ if (options?.offset) params.offset = options.offset.toString();
1478
+ const response = await this.http.get(
1479
+ `/api/storage/buckets/${this.bucketName}/objects`,
1480
+ { params }
1481
+ );
1482
+ return { data: response, error: null };
1483
+ } catch (error) {
1484
+ return {
1485
+ data: null,
1486
+ error: error instanceof InsForgeError ? error : new InsForgeError(
1487
+ "List failed",
1488
+ 500,
1489
+ "STORAGE_ERROR"
1490
+ )
1491
+ };
1492
+ }
1493
+ }
1494
+ /**
1495
+ * Delete a file
1496
+ * @param path - The object key/path
1497
+ */
1498
+ async remove(path) {
1499
+ try {
1500
+ const response = await this.http.delete(
1501
+ `/api/storage/buckets/${this.bucketName}/objects/${encodeURIComponent(path)}`
1502
+ );
1503
+ return { data: response, error: null };
1504
+ } catch (error) {
1505
+ return {
1506
+ data: null,
1507
+ error: error instanceof InsForgeError ? error : new InsForgeError(
1508
+ "Delete failed",
1509
+ 500,
1510
+ "STORAGE_ERROR"
1511
+ )
1512
+ };
1513
+ }
1514
+ }
1515
+ };
1516
+ var Storage = class {
1517
+ constructor(http) {
1518
+ this.http = http;
1519
+ }
1520
+ /**
1521
+ * Get a bucket instance for operations
1522
+ * @param bucketName - Name of the bucket
1523
+ */
1524
+ from(bucketName) {
1525
+ return new StorageBucket(bucketName, this.http);
1526
+ }
1527
+ };
1528
+
1529
+ // src/modules/ai.ts
1530
+ var AI = class {
1531
+ constructor(http) {
1532
+ this.http = http;
1533
+ this.chat = new Chat(http);
1534
+ this.images = new Images(http);
1535
+ this.embeddings = new Embeddings(http);
1536
+ }
1537
+ };
1538
+ var Chat = class {
1539
+ constructor(http) {
1540
+ this.completions = new ChatCompletions(http);
1541
+ }
1542
+ };
1543
+ var ChatCompletions = class {
1544
+ constructor(http) {
1545
+ this.http = http;
1546
+ }
1547
+ /**
1548
+ * Create a chat completion - OpenAI-like response format
1549
+ *
1550
+ * @example
1551
+ * ```typescript
1552
+ * // Non-streaming
1553
+ * const completion = await client.ai.chat.completions.create({
1554
+ * model: 'gpt-4',
1555
+ * messages: [{ role: 'user', content: 'Hello!' }]
1556
+ * });
1557
+ * console.log(completion.choices[0].message.content);
1558
+ *
1559
+ * // With images (OpenAI-compatible format)
1560
+ * const response = await client.ai.chat.completions.create({
1561
+ * model: 'gpt-4-vision',
1562
+ * messages: [{
1563
+ * role: 'user',
1564
+ * content: [
1565
+ * { type: 'text', text: 'What is in this image?' },
1566
+ * { type: 'image_url', image_url: { url: 'https://example.com/image.jpg' } }
1567
+ * ]
1568
+ * }]
1569
+ * });
1570
+ *
1571
+ * // With PDF files
1572
+ * const pdfResponse = await client.ai.chat.completions.create({
1573
+ * model: 'anthropic/claude-3.5-sonnet',
1574
+ * messages: [{
1575
+ * role: 'user',
1576
+ * content: [
1577
+ * { type: 'text', text: 'Summarize this document' },
1578
+ * { type: 'file', file: { filename: 'doc.pdf', file_data: 'https://example.com/doc.pdf' } }
1579
+ * ]
1580
+ * }],
1581
+ * fileParser: { enabled: true, pdf: { engine: 'mistral-ocr' } }
1582
+ * });
1583
+ *
1584
+ * // With web search
1585
+ * const searchResponse = await client.ai.chat.completions.create({
1586
+ * model: 'openai/gpt-4',
1587
+ * messages: [{ role: 'user', content: 'What are the latest news about AI?' }],
1588
+ * webSearch: { enabled: true, maxResults: 5 }
1589
+ * });
1590
+ * // Access citations from response.choices[0].message.annotations
1591
+ *
1592
+ * // With thinking/reasoning mode (Anthropic models)
1593
+ * const thinkingResponse = await client.ai.chat.completions.create({
1594
+ * model: 'anthropic/claude-3.5-sonnet',
1595
+ * messages: [{ role: 'user', content: 'Solve this complex math problem...' }],
1596
+ * thinking: true
1597
+ * });
1598
+ *
1599
+ * // Streaming - returns async iterable
1600
+ * const stream = await client.ai.chat.completions.create({
1601
+ * model: 'gpt-4',
1602
+ * messages: [{ role: 'user', content: 'Tell me a story' }],
1603
+ * stream: true
1604
+ * });
1605
+ *
1606
+ * for await (const chunk of stream) {
1607
+ * if (chunk.choices[0]?.delta?.content) {
1608
+ * process.stdout.write(chunk.choices[0].delta.content);
1609
+ * }
1610
+ * }
1611
+ * ```
1612
+ */
1613
+ async create(params) {
1614
+ const backendParams = {
1615
+ model: params.model,
1616
+ messages: params.messages,
1617
+ temperature: params.temperature,
1618
+ maxTokens: params.maxTokens,
1619
+ topP: params.topP,
1620
+ stream: params.stream,
1621
+ // New plugin options
1622
+ webSearch: params.webSearch,
1623
+ fileParser: params.fileParser,
1624
+ thinking: params.thinking,
1625
+ // Tool calling options
1626
+ tools: params.tools,
1627
+ toolChoice: params.toolChoice,
1628
+ parallelToolCalls: params.parallelToolCalls
1629
+ };
1630
+ if (params.stream) {
1631
+ const headers = this.http.getHeaders();
1632
+ headers["Content-Type"] = "application/json";
1633
+ const response2 = await this.http.fetch(
1634
+ `${this.http.baseUrl}/api/ai/chat/completion`,
1635
+ {
1636
+ method: "POST",
1637
+ headers,
1638
+ body: JSON.stringify(backendParams)
1639
+ }
1640
+ );
1641
+ if (!response2.ok) {
1642
+ const error = await response2.json();
1643
+ throw new Error(error.error || "Stream request failed");
1644
+ }
1645
+ return this.parseSSEStream(response2, params.model);
1646
+ }
1647
+ const response = await this.http.post(
1648
+ "/api/ai/chat/completion",
1649
+ backendParams
1650
+ );
1651
+ const content = response.text || "";
1652
+ return {
1653
+ id: `chatcmpl-${Date.now()}`,
1654
+ object: "chat.completion",
1655
+ created: Math.floor(Date.now() / 1e3),
1656
+ model: response.metadata?.model,
1657
+ choices: [
1658
+ {
1659
+ index: 0,
1660
+ message: {
1661
+ role: "assistant",
1662
+ content,
1663
+ // Include tool_calls if present (from tool calling)
1664
+ ...response.tool_calls?.length && { tool_calls: response.tool_calls },
1665
+ // Include annotations if present (from web search or file parsing)
1666
+ ...response.annotations?.length && { annotations: response.annotations }
1667
+ },
1668
+ finish_reason: response.tool_calls?.length ? "tool_calls" : "stop"
1669
+ }
1670
+ ],
1671
+ usage: response.metadata?.usage || {
1672
+ prompt_tokens: 0,
1673
+ completion_tokens: 0,
1674
+ total_tokens: 0
1675
+ }
1676
+ };
1677
+ }
1678
+ /**
1679
+ * Parse SSE stream into async iterable of OpenAI-like chunks
1680
+ */
1681
+ async *parseSSEStream(response, model) {
1682
+ const reader = response.body.getReader();
1683
+ const decoder = new TextDecoder();
1684
+ let buffer = "";
1685
+ try {
1686
+ while (true) {
1687
+ const { done, value } = await reader.read();
1688
+ if (done) break;
1689
+ buffer += decoder.decode(value, { stream: true });
1690
+ const lines = buffer.split("\n");
1691
+ buffer = lines.pop() || "";
1692
+ for (const line of lines) {
1693
+ if (line.startsWith("data: ")) {
1694
+ const dataStr = line.slice(6).trim();
1695
+ if (dataStr) {
1696
+ try {
1697
+ const data = JSON.parse(dataStr);
1698
+ if (data.chunk || data.content) {
1699
+ yield {
1700
+ id: `chatcmpl-${Date.now()}`,
1701
+ object: "chat.completion.chunk",
1702
+ created: Math.floor(Date.now() / 1e3),
1703
+ model,
1704
+ choices: [
1705
+ {
1706
+ index: 0,
1707
+ delta: {
1708
+ content: data.chunk || data.content
1709
+ },
1710
+ finish_reason: null
1711
+ }
1712
+ ]
1713
+ };
1714
+ }
1715
+ if (data.tool_calls?.length) {
1716
+ yield {
1717
+ id: `chatcmpl-${Date.now()}`,
1718
+ object: "chat.completion.chunk",
1719
+ created: Math.floor(Date.now() / 1e3),
1720
+ model,
1721
+ choices: [
1722
+ {
1723
+ index: 0,
1724
+ delta: {
1725
+ tool_calls: data.tool_calls
1726
+ },
1727
+ finish_reason: "tool_calls"
1728
+ }
1729
+ ]
1730
+ };
1731
+ }
1732
+ if (data.done) {
1733
+ reader.releaseLock();
1734
+ return;
1735
+ }
1736
+ } catch (e) {
1737
+ console.warn("Failed to parse SSE data:", dataStr);
1738
+ }
1739
+ }
1740
+ }
1741
+ }
1742
+ }
1743
+ } finally {
1744
+ reader.releaseLock();
1745
+ }
1746
+ }
1747
+ };
1748
+ var Embeddings = class {
1749
+ constructor(http) {
1750
+ this.http = http;
1751
+ }
1752
+ /**
1753
+ * Create embeddings for text input - OpenAI-like response format
1754
+ *
1755
+ * @example
1756
+ * ```typescript
1757
+ * // Single text input
1758
+ * const response = await client.ai.embeddings.create({
1759
+ * model: 'openai/text-embedding-3-small',
1760
+ * input: 'Hello world'
1761
+ * });
1762
+ * console.log(response.data[0].embedding); // number[]
1763
+ *
1764
+ * // Multiple text inputs
1765
+ * const response = await client.ai.embeddings.create({
1766
+ * model: 'openai/text-embedding-3-small',
1767
+ * input: ['Hello world', 'Goodbye world']
1768
+ * });
1769
+ * response.data.forEach((item, i) => {
1770
+ * console.log(`Embedding ${i}:`, item.embedding.slice(0, 5)); // First 5 dimensions
1771
+ * });
1772
+ *
1773
+ * // With custom dimensions (if supported by model)
1774
+ * const response = await client.ai.embeddings.create({
1775
+ * model: 'openai/text-embedding-3-small',
1776
+ * input: 'Hello world',
1777
+ * dimensions: 256
1778
+ * });
1779
+ *
1780
+ * // With base64 encoding format
1781
+ * const response = await client.ai.embeddings.create({
1782
+ * model: 'openai/text-embedding-3-small',
1783
+ * input: 'Hello world',
1784
+ * encoding_format: 'base64'
1785
+ * });
1786
+ * ```
1787
+ */
1788
+ async create(params) {
1789
+ const response = await this.http.post(
1790
+ "/api/ai/embeddings",
1791
+ params
1792
+ );
1793
+ return {
1794
+ object: response.object,
1795
+ data: response.data,
1796
+ model: response.metadata?.model,
1797
+ usage: response.metadata?.usage ? {
1798
+ prompt_tokens: response.metadata.usage.promptTokens || 0,
1799
+ total_tokens: response.metadata.usage.totalTokens || 0
1800
+ } : {
1801
+ prompt_tokens: 0,
1802
+ total_tokens: 0
1803
+ }
1804
+ };
1805
+ }
1806
+ };
1807
+ var Images = class {
1808
+ constructor(http) {
1809
+ this.http = http;
1810
+ }
1811
+ /**
1812
+ * Generate images - OpenAI-like response format
1813
+ *
1814
+ * @example
1815
+ * ```typescript
1816
+ * // Text-to-image
1817
+ * const response = await client.ai.images.generate({
1818
+ * model: 'dall-e-3',
1819
+ * prompt: 'A sunset over mountains',
1820
+ * });
1821
+ * console.log(response.images[0].url);
1822
+ *
1823
+ * // Image-to-image (with input images)
1824
+ * const response = await client.ai.images.generate({
1825
+ * model: 'stable-diffusion-xl',
1826
+ * prompt: 'Transform this into a watercolor painting',
1827
+ * images: [
1828
+ * { url: 'https://example.com/input.jpg' },
1829
+ * // or base64-encoded Data URI:
1830
+ * { url: 'data:image/jpeg;base64,/9j/4AAQ...' }
1831
+ * ]
1832
+ * });
1833
+ * ```
1834
+ */
1835
+ async generate(params) {
1836
+ const response = await this.http.post(
1837
+ "/api/ai/image/generation",
1838
+ params
1839
+ );
1840
+ let data = [];
1841
+ if (response.images && response.images.length > 0) {
1842
+ data = response.images.map((img) => ({
1843
+ b64_json: img.imageUrl.replace(/^data:image\/\w+;base64,/, ""),
1844
+ content: response.text
1845
+ }));
1846
+ } else if (response.text) {
1847
+ data = [{ content: response.text }];
1848
+ }
1849
+ return {
1850
+ created: Math.floor(Date.now() / 1e3),
1851
+ data,
1852
+ ...response.metadata?.usage && {
1853
+ usage: {
1854
+ total_tokens: response.metadata.usage.totalTokens || 0,
1855
+ input_tokens: response.metadata.usage.promptTokens || 0,
1856
+ output_tokens: response.metadata.usage.completionTokens || 0
1857
+ }
1858
+ }
1859
+ };
1860
+ }
1861
+ };
1862
+
1863
+ // src/modules/functions.ts
1864
+ var Functions = class _Functions {
1865
+ constructor(http, functionsUrl) {
1866
+ this.http = http;
1867
+ this.functionsUrl = functionsUrl || _Functions.deriveSubhostingUrl(http.baseUrl);
1868
+ }
1869
+ /**
1870
+ * Derive the subhosting URL from the base URL.
1871
+ * Base URL pattern: https://{appKey}.{region}.insforge.app
1872
+ * Functions URL: https://{appKey}.functions.insforge.app
1873
+ * Only applies to .insforge.app domains.
1874
+ */
1875
+ static deriveSubhostingUrl(baseUrl) {
1876
+ try {
1877
+ const { hostname } = new URL(baseUrl);
1878
+ if (!hostname.endsWith(".insforge.app")) return void 0;
1879
+ const appKey = hostname.split(".")[0];
1880
+ return `https://${appKey}.functions.insforge.app`;
1881
+ } catch {
1882
+ return void 0;
1883
+ }
1884
+ }
1885
+ /**
1886
+ * Build a Request for in-process dispatch. The host is a non-routable
1887
+ * placeholder; the router only reads pathname.
1888
+ */
1889
+ buildInProcessRequest(slug, method, body, callerHeaders) {
1890
+ const url = new URL("/" + slug, "http://insforge.local").toString();
1891
+ const headers = { ...this.http.getHeaders() };
1892
+ const reqBody = serializeBody(method, body, headers);
1893
+ Object.assign(headers, callerHeaders);
1894
+ return new Request(url, {
1895
+ method,
1896
+ headers,
1897
+ body: reqBody
1898
+ });
1899
+ }
1900
+ /**
1901
+ * Invoke an Edge Function.
1902
+ *
1903
+ * Dispatch order:
1904
+ * 1. If `globalThis.__insforge_dispatch__` is present, call it in-process.
1905
+ * This avoids Deno Subhosting's 508 Loop Detected when one bundled
1906
+ * function invokes another inside the same deployment.
1907
+ * 2. Otherwise, try the configured subhosting URL.
1908
+ * 3. On 404 from subhosting, fall back to the proxy path.
1909
+ *
1910
+ * @param slug The function slug to invoke
1911
+ * @param options Request options
1912
+ */
1913
+ async invoke(slug, options = {}) {
1914
+ const { method = "POST", body, headers = {} } = options;
1915
+ const dispatch = globalThis.__insforge_dispatch__;
1916
+ const localFunctionsUrl = _Functions.deriveSubhostingUrl(this.http.baseUrl);
1917
+ if (typeof dispatch === "function" && !!localFunctionsUrl && this.functionsUrl === localFunctionsUrl) {
1918
+ try {
1919
+ const req = this.buildInProcessRequest(slug, method, body, headers);
1920
+ const res = await dispatch(req);
1921
+ const data = await parseResponse(res);
1922
+ return { data, error: null };
1923
+ } catch (error) {
1924
+ if (error instanceof Error && error.name === "AbortError") throw error;
1925
+ return {
1926
+ data: null,
1927
+ error: error instanceof InsForgeError ? error : new InsForgeError(
1928
+ error instanceof Error ? error.message : "Function invocation failed",
1929
+ 500,
1930
+ "FUNCTION_ERROR"
1931
+ )
1932
+ };
1933
+ }
1934
+ }
1935
+ if (this.functionsUrl) {
1936
+ try {
1937
+ const data = await this.http.request(method, `${this.functionsUrl}/${slug}`, {
1938
+ body,
1939
+ headers
1940
+ });
1941
+ return { data, error: null };
1942
+ } catch (error) {
1943
+ if (error instanceof Error && error.name === "AbortError") throw error;
1944
+ if (error instanceof InsForgeError && error.statusCode === 404) {
1945
+ } else {
1946
+ return {
1947
+ data: null,
1948
+ error: error instanceof InsForgeError ? error : new InsForgeError(
1949
+ error instanceof Error ? error.message : "Function invocation failed",
1950
+ 500,
1951
+ "FUNCTION_ERROR"
1952
+ )
1953
+ };
1954
+ }
1955
+ }
1956
+ }
1957
+ try {
1958
+ const path = `/functions/${slug}`;
1959
+ const data = await this.http.request(method, path, { body, headers });
1960
+ return { data, error: null };
1961
+ } catch (error) {
1962
+ if (error instanceof Error && error.name === "AbortError") throw error;
1963
+ return {
1964
+ data: null,
1965
+ error: error instanceof InsForgeError ? error : new InsForgeError(
1966
+ error instanceof Error ? error.message : "Function invocation failed",
1967
+ 500,
1968
+ "FUNCTION_ERROR"
1969
+ )
1970
+ };
1971
+ }
1972
+ }
1973
+ };
1974
+
1975
+ // src/modules/realtime.ts
1976
+ import { io } from "socket.io-client";
1977
+ var CONNECT_TIMEOUT = 1e4;
1978
+ var Realtime = class {
1979
+ constructor(baseUrl, tokenManager, anonKey) {
1980
+ this.socket = null;
1981
+ this.connectPromise = null;
1982
+ this.subscribedChannels = /* @__PURE__ */ new Set();
1983
+ this.eventListeners = /* @__PURE__ */ new Map();
1984
+ this.baseUrl = baseUrl;
1985
+ this.tokenManager = tokenManager;
1986
+ this.anonKey = anonKey;
1987
+ this.tokenManager.onTokenChange = () => this.onTokenChange();
1988
+ }
1989
+ notifyListeners(event, payload) {
1990
+ const listeners = this.eventListeners.get(event);
1991
+ if (!listeners) return;
1992
+ for (const cb of listeners) {
1993
+ try {
1994
+ cb(payload);
1995
+ } catch (err) {
1996
+ console.error(`Error in ${event} callback:`, err);
1997
+ }
1998
+ }
1999
+ }
2000
+ /**
2001
+ * Connect to the realtime server
2002
+ * @returns Promise that resolves when connected
2003
+ */
2004
+ connect() {
2005
+ if (this.socket?.connected) {
2006
+ return Promise.resolve();
2007
+ }
2008
+ if (this.connectPromise) {
2009
+ return this.connectPromise;
2010
+ }
2011
+ this.connectPromise = new Promise((resolve, reject) => {
2012
+ const token = this.tokenManager.getAccessToken() ?? this.anonKey;
2013
+ this.socket = io(this.baseUrl, {
2014
+ transports: ["websocket"],
2015
+ auth: token ? { token } : void 0
2016
+ });
2017
+ let initialConnection = true;
2018
+ let timeoutId = null;
2019
+ const cleanup = () => {
2020
+ if (timeoutId) {
2021
+ clearTimeout(timeoutId);
2022
+ timeoutId = null;
2023
+ }
2024
+ };
2025
+ timeoutId = setTimeout(() => {
2026
+ if (initialConnection) {
2027
+ initialConnection = false;
2028
+ this.connectPromise = null;
2029
+ this.socket?.disconnect();
2030
+ this.socket = null;
2031
+ reject(new Error(`Connection timeout after ${CONNECT_TIMEOUT}ms`));
2032
+ }
2033
+ }, CONNECT_TIMEOUT);
2034
+ this.socket.on("connect", () => {
2035
+ cleanup();
2036
+ for (const channel of this.subscribedChannels) {
2037
+ this.socket.emit("realtime:subscribe", { channel });
2038
+ }
2039
+ this.notifyListeners("connect");
2040
+ if (initialConnection) {
2041
+ initialConnection = false;
2042
+ this.connectPromise = null;
2043
+ resolve();
2044
+ }
2045
+ });
2046
+ this.socket.on("connect_error", (error) => {
2047
+ cleanup();
2048
+ this.notifyListeners("connect_error", error);
2049
+ if (initialConnection) {
2050
+ initialConnection = false;
2051
+ this.connectPromise = null;
2052
+ reject(error);
2053
+ }
2054
+ });
2055
+ this.socket.on("disconnect", (reason) => {
2056
+ this.notifyListeners("disconnect", reason);
2057
+ });
2058
+ this.socket.on("realtime:error", (error) => {
2059
+ this.notifyListeners("error", error);
2060
+ });
2061
+ this.socket.onAny((event, message) => {
2062
+ if (event === "realtime:error") return;
2063
+ this.notifyListeners(event, message);
2064
+ });
2065
+ });
2066
+ return this.connectPromise;
2067
+ }
2068
+ /**
2069
+ * Disconnect from the realtime server
2070
+ */
2071
+ disconnect() {
2072
+ if (this.socket) {
2073
+ this.socket.disconnect();
2074
+ this.socket = null;
2075
+ }
2076
+ this.subscribedChannels.clear();
2077
+ }
2078
+ /**
2079
+ * Handle token changes (e.g., after auth refresh)
2080
+ * Updates socket auth so reconnects use the new token
2081
+ * If connected, triggers reconnect to apply new token immediately
2082
+ */
2083
+ onTokenChange() {
2084
+ const token = this.tokenManager.getAccessToken() ?? this.anonKey;
2085
+ if (this.socket) {
2086
+ this.socket.auth = token ? { token } : {};
2087
+ }
2088
+ if (this.socket && (this.socket.connected || this.connectPromise)) {
2089
+ this.socket.disconnect();
2090
+ this.socket.connect();
2091
+ }
2092
+ }
2093
+ /**
2094
+ * Check if connected to the realtime server
2095
+ */
2096
+ get isConnected() {
2097
+ return this.socket?.connected ?? false;
2098
+ }
2099
+ /**
2100
+ * Get the current connection state
2101
+ */
2102
+ get connectionState() {
2103
+ if (!this.socket) return "disconnected";
2104
+ if (this.socket.connected) return "connected";
2105
+ return "connecting";
2106
+ }
2107
+ /**
2108
+ * Get the socket ID (if connected)
2109
+ */
2110
+ get socketId() {
2111
+ return this.socket?.id;
2112
+ }
2113
+ /**
2114
+ * Subscribe to a channel
2115
+ *
2116
+ * Automatically connects if not already connected.
2117
+ *
2118
+ * @param channel - Channel name (e.g., 'orders:123', 'broadcast')
2119
+ * @returns Promise with the subscription response
2120
+ */
2121
+ async subscribe(channel) {
2122
+ if (this.subscribedChannels.has(channel)) {
2123
+ return { ok: true, channel };
2124
+ }
2125
+ if (!this.socket?.connected) {
2126
+ try {
2127
+ await this.connect();
2128
+ } catch (error) {
2129
+ const message = error instanceof Error ? error.message : "Connection failed";
2130
+ return { ok: false, channel, error: { code: "CONNECTION_FAILED", message } };
2131
+ }
2132
+ }
2133
+ return new Promise((resolve) => {
2134
+ this.socket.emit("realtime:subscribe", { channel }, (response) => {
2135
+ if (response.ok) {
2136
+ this.subscribedChannels.add(channel);
2137
+ }
2138
+ resolve(response);
2139
+ });
2140
+ });
2141
+ }
2142
+ /**
2143
+ * Unsubscribe from a channel (fire-and-forget)
2144
+ *
2145
+ * @param channel - Channel name to unsubscribe from
2146
+ */
2147
+ unsubscribe(channel) {
2148
+ this.subscribedChannels.delete(channel);
2149
+ if (this.socket?.connected) {
2150
+ this.socket.emit("realtime:unsubscribe", { channel });
2151
+ }
2152
+ }
2153
+ /**
2154
+ * Publish a message to a channel
2155
+ *
2156
+ * @param channel - Channel name
2157
+ * @param event - Event name
2158
+ * @param payload - Message payload
2159
+ */
2160
+ async publish(channel, event, payload) {
2161
+ if (!this.socket?.connected) {
2162
+ throw new Error("Not connected to realtime server. Call connect() first.");
2163
+ }
2164
+ this.socket.emit("realtime:publish", { channel, event, payload });
2165
+ }
2166
+ /**
2167
+ * Listen for events
2168
+ *
2169
+ * Reserved event names:
2170
+ * - 'connect' - Fired when connected to the server
2171
+ * - 'connect_error' - Fired when connection fails (payload: Error)
2172
+ * - 'disconnect' - Fired when disconnected (payload: reason string)
2173
+ * - 'error' - Fired when a realtime error occurs (payload: RealtimeErrorPayload)
2174
+ *
2175
+ * All other events receive a `SocketMessage` payload with metadata.
2176
+ *
2177
+ * @param event - Event name to listen for
2178
+ * @param callback - Callback function when event is received
2179
+ */
2180
+ on(event, callback) {
2181
+ if (!this.eventListeners.has(event)) {
2182
+ this.eventListeners.set(event, /* @__PURE__ */ new Set());
2183
+ }
2184
+ this.eventListeners.get(event).add(callback);
2185
+ }
2186
+ /**
2187
+ * Remove a listener for a specific event
2188
+ *
2189
+ * @param event - Event name
2190
+ * @param callback - The callback function to remove
2191
+ */
2192
+ off(event, callback) {
2193
+ const listeners = this.eventListeners.get(event);
2194
+ if (listeners) {
2195
+ listeners.delete(callback);
2196
+ if (listeners.size === 0) {
2197
+ this.eventListeners.delete(event);
2198
+ }
2199
+ }
2200
+ }
2201
+ /**
2202
+ * Listen for an event only once, then automatically remove the listener
2203
+ *
2204
+ * @param event - Event name to listen for
2205
+ * @param callback - Callback function when event is received
2206
+ */
2207
+ once(event, callback) {
2208
+ const wrapper = (payload) => {
2209
+ this.off(event, wrapper);
2210
+ callback(payload);
2211
+ };
2212
+ this.on(event, wrapper);
2213
+ }
2214
+ /**
2215
+ * Get all currently subscribed channels
2216
+ *
2217
+ * @returns Array of channel names
2218
+ */
2219
+ getSubscribedChannels() {
2220
+ return Array.from(this.subscribedChannels);
2221
+ }
2222
+ };
2223
+
2224
+ // src/modules/email.ts
2225
+ var Emails = class {
2226
+ constructor(http) {
2227
+ this.http = http;
2228
+ }
2229
+ /**
2230
+ * Send a custom HTML email
2231
+ * @param options Email options including recipients, subject, and HTML content
2232
+ */
2233
+ async send(options) {
2234
+ try {
2235
+ const data = await this.http.post(
2236
+ "/api/email/send-raw",
2237
+ options
2238
+ );
2239
+ return { data, error: null };
2240
+ } catch (error) {
2241
+ if (error instanceof Error && error.name === "AbortError") throw error;
2242
+ return {
2243
+ data: null,
2244
+ error: error instanceof InsForgeError ? error : new InsForgeError(
2245
+ error instanceof Error ? error.message : "Email send failed",
2246
+ 500,
2247
+ "EMAIL_ERROR"
2248
+ )
2249
+ };
2250
+ }
2251
+ }
2252
+ };
2253
+
2254
+ // src/client.ts
2255
+ var InsForgeClient = class {
2256
+ constructor(config = {}) {
2257
+ const logger = new Logger(config.debug);
2258
+ this.tokenManager = new TokenManager();
2259
+ this.http = new HttpClient(config, this.tokenManager, logger);
2260
+ if (config.edgeFunctionToken) {
2261
+ this.http.setAuthToken(config.edgeFunctionToken);
2262
+ this.tokenManager.setAccessToken(config.edgeFunctionToken);
2263
+ }
2264
+ this.auth = new Auth(this.http, this.tokenManager, {
2265
+ isServerMode: config.isServerMode ?? false
2266
+ });
2267
+ this.database = new Database(this.http, this.tokenManager);
2268
+ this.storage = new Storage(this.http);
2269
+ this.ai = new AI(this.http);
2270
+ this.functions = new Functions(this.http, config.functionsUrl);
2271
+ this.realtime = new Realtime(
2272
+ this.http.baseUrl,
2273
+ this.tokenManager,
2274
+ config.anonKey
2275
+ );
2276
+ this.emails = new Emails(this.http);
2277
+ }
2278
+ /**
2279
+ * Get the underlying HTTP client for custom requests
2280
+ *
2281
+ * @example
2282
+ * ```typescript
2283
+ * const httpClient = client.getHttpClient();
2284
+ * const customData = await httpClient.get('/api/custom-endpoint');
2285
+ * ```
2286
+ */
2287
+ getHttpClient() {
2288
+ return this.http;
2289
+ }
2290
+ /**
2291
+ * Future modules will be added here:
2292
+ * - database: Database operations
2293
+ * - storage: File storage operations
2294
+ * - functions: Serverless functions
2295
+ * - tables: Table management
2296
+ * - metadata: Backend metadata
2297
+ */
2298
+ };
2299
+
2300
+ // src/index.ts
2301
+ function createClient(config) {
2302
+ return new InsForgeClient(config);
2303
+ }
2304
+ var index_default = InsForgeClient;
2305
+ export {
2306
+ AI,
2307
+ Auth,
2308
+ Database,
2309
+ Emails,
2310
+ Functions,
2311
+ HttpClient,
2312
+ InsForgeClient,
2313
+ InsForgeError,
2314
+ Logger,
2315
+ Realtime,
2316
+ Storage,
2317
+ StorageBucket,
2318
+ TokenManager,
2319
+ createClient,
2320
+ index_default as default
2321
+ };
2322
+ //# sourceMappingURL=index.mjs.map