@insforge/sdk 1.2.10 → 1.3.0-ssr.1

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/ssr.js ADDED
@@ -0,0 +1,3219 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/ssr.ts
21
+ var ssr_exports = {};
22
+ __export(ssr_exports, {
23
+ DEFAULT_ACCESS_TOKEN_COOKIE: () => DEFAULT_ACCESS_TOKEN_COOKIE,
24
+ DEFAULT_REFRESH_TOKEN_COOKIE: () => DEFAULT_REFRESH_TOKEN_COOKIE,
25
+ accessTokenCookieOptions: () => accessTokenCookieOptions,
26
+ clearAuthCookies: () => clearAuthCookies,
27
+ createBrowserClient: () => createBrowserClient,
28
+ createRefreshAuthRouter: () => createRefreshAuthRouter,
29
+ createServerClient: () => createServerClient,
30
+ getAccessTokenCookieName: () => getAccessTokenCookieName,
31
+ getRefreshTokenCookieName: () => getRefreshTokenCookieName,
32
+ refreshAuth: () => refreshAuth,
33
+ refreshTokenCookieOptions: () => refreshTokenCookieOptions,
34
+ setAuthCookies: () => setAuthCookies,
35
+ updateSession: () => updateSession
36
+ });
37
+ module.exports = __toCommonJS(ssr_exports);
38
+
39
+ // src/types.ts
40
+ var InsForgeError = class _InsForgeError extends Error {
41
+ constructor(message, statusCode, error, nextActions) {
42
+ super(message);
43
+ this.name = "InsForgeError";
44
+ this.statusCode = statusCode;
45
+ this.error = error;
46
+ this.nextActions = nextActions;
47
+ }
48
+ static fromApiError(apiError) {
49
+ return new _InsForgeError(
50
+ apiError.message,
51
+ apiError.statusCode,
52
+ apiError.error,
53
+ apiError.nextActions
54
+ );
55
+ }
56
+ };
57
+
58
+ // src/lib/logger.ts
59
+ var SENSITIVE_HEADERS = ["authorization", "x-api-key", "cookie", "set-cookie"];
60
+ var SENSITIVE_BODY_KEYS = [
61
+ "password",
62
+ "token",
63
+ "accesstoken",
64
+ "refreshtoken",
65
+ "authorization",
66
+ "secret",
67
+ "apikey",
68
+ "api_key",
69
+ "email",
70
+ "ssn",
71
+ "creditcard",
72
+ "credit_card"
73
+ ];
74
+ function redactHeaders(headers) {
75
+ const redacted = {};
76
+ for (const [key, value] of Object.entries(headers)) {
77
+ if (SENSITIVE_HEADERS.includes(key.toLowerCase())) {
78
+ redacted[key] = "***REDACTED***";
79
+ } else {
80
+ redacted[key] = value;
81
+ }
82
+ }
83
+ return redacted;
84
+ }
85
+ function sanitizeBody(body) {
86
+ if (body === null || body === void 0) return body;
87
+ if (typeof body === "string") {
88
+ try {
89
+ const parsed = JSON.parse(body);
90
+ return sanitizeBody(parsed);
91
+ } catch {
92
+ return body;
93
+ }
94
+ }
95
+ if (Array.isArray(body)) return body.map(sanitizeBody);
96
+ if (typeof body === "object") {
97
+ const sanitized = {};
98
+ for (const [key, value] of Object.entries(body)) {
99
+ if (SENSITIVE_BODY_KEYS.includes(key.toLowerCase().replace(/[-_]/g, ""))) {
100
+ sanitized[key] = "***REDACTED***";
101
+ } else {
102
+ sanitized[key] = sanitizeBody(value);
103
+ }
104
+ }
105
+ return sanitized;
106
+ }
107
+ return body;
108
+ }
109
+ function formatBody(body) {
110
+ if (body === void 0 || body === null) return "";
111
+ if (typeof body === "string") {
112
+ try {
113
+ return JSON.stringify(JSON.parse(body), null, 2);
114
+ } catch {
115
+ return body;
116
+ }
117
+ }
118
+ if (typeof FormData !== "undefined" && body instanceof FormData) {
119
+ return "[FormData]";
120
+ }
121
+ try {
122
+ return JSON.stringify(body, null, 2);
123
+ } catch {
124
+ return "[Unserializable body]";
125
+ }
126
+ }
127
+ var Logger = class {
128
+ /**
129
+ * Creates a new Logger instance.
130
+ * @param debug - Set to true to enable console logging, or pass a custom log function
131
+ */
132
+ constructor(debug) {
133
+ if (typeof debug === "function") {
134
+ this.enabled = true;
135
+ this.customLog = debug;
136
+ } else {
137
+ this.enabled = !!debug;
138
+ this.customLog = null;
139
+ }
140
+ }
141
+ /**
142
+ * Logs a debug message at the info level.
143
+ * @param message - The message to log
144
+ * @param args - Additional arguments to pass to the log function
145
+ */
146
+ log(message, ...args) {
147
+ if (!this.enabled) return;
148
+ const formatted = `[InsForge Debug] ${message}`;
149
+ if (this.customLog) {
150
+ this.customLog(formatted, ...args);
151
+ } else {
152
+ console.log(formatted, ...args);
153
+ }
154
+ }
155
+ /**
156
+ * Logs a debug message at the warning level.
157
+ * @param message - The message to log
158
+ * @param args - Additional arguments to pass to the log function
159
+ */
160
+ warn(message, ...args) {
161
+ if (!this.enabled) return;
162
+ const formatted = `[InsForge Debug] ${message}`;
163
+ if (this.customLog) {
164
+ this.customLog(formatted, ...args);
165
+ } else {
166
+ console.warn(formatted, ...args);
167
+ }
168
+ }
169
+ /**
170
+ * Logs a debug message at the error level.
171
+ * @param message - The message to log
172
+ * @param args - Additional arguments to pass to the log function
173
+ */
174
+ error(message, ...args) {
175
+ if (!this.enabled) return;
176
+ const formatted = `[InsForge Debug] ${message}`;
177
+ if (this.customLog) {
178
+ this.customLog(formatted, ...args);
179
+ } else {
180
+ console.error(formatted, ...args);
181
+ }
182
+ }
183
+ /**
184
+ * Logs an outgoing HTTP request with method, URL, headers, and body.
185
+ * Sensitive headers and body fields are automatically redacted.
186
+ * @param method - HTTP method (GET, POST, etc.)
187
+ * @param url - The full request URL
188
+ * @param headers - Request headers (sensitive values will be redacted)
189
+ * @param body - Request body (sensitive fields will be masked)
190
+ */
191
+ logRequest(method, url, headers, body) {
192
+ if (!this.enabled) return;
193
+ const parts = [
194
+ `\u2192 ${method} ${url}`
195
+ ];
196
+ if (headers && Object.keys(headers).length > 0) {
197
+ parts.push(` Headers: ${JSON.stringify(redactHeaders(headers))}`);
198
+ }
199
+ const formattedBody = formatBody(sanitizeBody(body));
200
+ if (formattedBody) {
201
+ const truncated = formattedBody.length > 1e3 ? formattedBody.slice(0, 1e3) + "... [truncated]" : formattedBody;
202
+ parts.push(` Body: ${truncated}`);
203
+ }
204
+ this.log(parts.join("\n"));
205
+ }
206
+ /**
207
+ * Logs an incoming HTTP response with method, URL, status, duration, and body.
208
+ * Error responses (4xx/5xx) are logged at the error level.
209
+ * @param method - HTTP method (GET, POST, etc.)
210
+ * @param url - The full request URL
211
+ * @param status - HTTP response status code
212
+ * @param durationMs - Request duration in milliseconds
213
+ * @param body - Response body (sensitive fields will be masked, large bodies truncated)
214
+ */
215
+ logResponse(method, url, status, durationMs, body) {
216
+ if (!this.enabled) return;
217
+ const parts = [
218
+ `\u2190 ${method} ${url} ${status} (${durationMs}ms)`
219
+ ];
220
+ const formattedBody = formatBody(sanitizeBody(body));
221
+ if (formattedBody) {
222
+ const truncated = formattedBody.length > 1e3 ? formattedBody.slice(0, 1e3) + "... [truncated]" : formattedBody;
223
+ parts.push(` Body: ${truncated}`);
224
+ }
225
+ if (status >= 400) {
226
+ this.error(parts.join("\n"));
227
+ } else {
228
+ this.log(parts.join("\n"));
229
+ }
230
+ }
231
+ };
232
+
233
+ // src/lib/token-manager.ts
234
+ var CSRF_TOKEN_COOKIE = "insforge_csrf_token";
235
+ function getCsrfToken() {
236
+ if (typeof document === "undefined") return null;
237
+ const match = document.cookie.split(";").find((c) => c.trim().startsWith(`${CSRF_TOKEN_COOKIE}=`));
238
+ if (!match) return null;
239
+ return match.split("=")[1] || null;
240
+ }
241
+ function setCsrfToken(token) {
242
+ if (typeof document === "undefined") return;
243
+ const maxAge = 7 * 24 * 60 * 60;
244
+ const secure = typeof window !== "undefined" && window.location.protocol === "https:" ? "; Secure" : "";
245
+ document.cookie = `${CSRF_TOKEN_COOKIE}=${encodeURIComponent(token)}; path=/; max-age=${maxAge}; SameSite=Lax${secure}`;
246
+ }
247
+ function clearCsrfToken() {
248
+ if (typeof document === "undefined") return;
249
+ const secure = typeof window !== "undefined" && window.location.protocol === "https:" ? "; Secure" : "";
250
+ document.cookie = `${CSRF_TOKEN_COOKIE}=; path=/; max-age=0; SameSite=Lax${secure}`;
251
+ }
252
+ var TokenManager = class {
253
+ constructor() {
254
+ // In-memory storage
255
+ this.accessToken = null;
256
+ this.user = null;
257
+ // Callback for token changes (used by realtime to reconnect with new token)
258
+ this.onTokenChange = null;
259
+ }
260
+ /**
261
+ * Save session in memory
262
+ */
263
+ saveSession(session) {
264
+ const tokenChanged = session.accessToken !== this.accessToken;
265
+ this.accessToken = session.accessToken;
266
+ this.user = session.user;
267
+ if (tokenChanged && this.onTokenChange) {
268
+ this.onTokenChange();
269
+ }
270
+ }
271
+ /**
272
+ * Get current session
273
+ */
274
+ getSession() {
275
+ if (!this.accessToken || !this.user) return null;
276
+ return {
277
+ accessToken: this.accessToken,
278
+ user: this.user
279
+ };
280
+ }
281
+ /**
282
+ * Get access token
283
+ */
284
+ getAccessToken() {
285
+ return this.accessToken;
286
+ }
287
+ /**
288
+ * Set access token
289
+ */
290
+ setAccessToken(token) {
291
+ const tokenChanged = token !== this.accessToken;
292
+ this.accessToken = token;
293
+ if (tokenChanged && this.onTokenChange) {
294
+ this.onTokenChange();
295
+ }
296
+ }
297
+ /**
298
+ * Get user
299
+ */
300
+ getUser() {
301
+ return this.user;
302
+ }
303
+ /**
304
+ * Set user
305
+ */
306
+ setUser(user) {
307
+ this.user = user;
308
+ }
309
+ /**
310
+ * Clear in-memory session
311
+ */
312
+ clearSession() {
313
+ const hadToken = this.accessToken !== null;
314
+ this.accessToken = null;
315
+ this.user = null;
316
+ if (hadToken && this.onTokenChange) {
317
+ this.onTokenChange();
318
+ }
319
+ }
320
+ };
321
+
322
+ // src/lib/http-client.ts
323
+ var RETRYABLE_STATUS_CODES = /* @__PURE__ */ new Set([500, 502, 503, 504]);
324
+ var IDEMPOTENT_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
325
+ var REFRESHABLE_AUTH_ERROR_CODES = /* @__PURE__ */ new Set([
326
+ "AUTH_UNAUTHORIZED",
327
+ "PGRST301"
328
+ ]);
329
+ function serializeBody(method, body, headers) {
330
+ if (body === void 0) return void 0;
331
+ if (method === "GET" || method === "HEAD") return void 0;
332
+ if (typeof FormData !== "undefined" && body instanceof FormData) {
333
+ return body;
334
+ }
335
+ headers["Content-Type"] = "application/json;charset=UTF-8";
336
+ return JSON.stringify(body);
337
+ }
338
+ async function parseResponse(response) {
339
+ if (response.status === 204) return void 0;
340
+ let data;
341
+ const contentType = response.headers.get("content-type");
342
+ try {
343
+ if (contentType?.includes("json")) {
344
+ data = await response.json();
345
+ } else {
346
+ data = await response.text();
347
+ }
348
+ } catch (parseErr) {
349
+ throw new InsForgeError(
350
+ `Failed to parse response body: ${parseErr?.message || "Unknown error"}`,
351
+ response.status,
352
+ response.ok ? "PARSE_ERROR" : "REQUEST_FAILED"
353
+ );
354
+ }
355
+ if (!response.ok) {
356
+ if (data && typeof data === "object" && "error" in data) {
357
+ data.statusCode ?? (data.statusCode = data.status ?? response.status);
358
+ const error = InsForgeError.fromApiError(data);
359
+ Object.keys(data).forEach((key) => {
360
+ if (key !== "error" && key !== "message" && key !== "statusCode") {
361
+ error[key] = data[key];
362
+ }
363
+ });
364
+ throw error;
365
+ }
366
+ throw new InsForgeError(
367
+ `Request failed: ${response.statusText}`,
368
+ response.status,
369
+ "REQUEST_FAILED"
370
+ );
371
+ }
372
+ return data;
373
+ }
374
+ var HttpClient = class {
375
+ /**
376
+ * Creates a new HttpClient instance.
377
+ * @param config - SDK configuration including baseUrl, timeout, retry settings, and fetch implementation.
378
+ * @param tokenManager - Token manager for session persistence.
379
+ * @param logger - Optional logger instance for request/response debugging.
380
+ */
381
+ constructor(config, tokenManager, logger) {
382
+ this.userToken = null;
383
+ this.isRefreshing = false;
384
+ this.refreshPromise = null;
385
+ this.refreshToken = null;
386
+ this.config = config;
387
+ this.baseUrl = config.baseUrl || "http://localhost:7130";
388
+ this.fetch = config.fetch || (globalThis.fetch ? globalThis.fetch.bind(globalThis) : void 0);
389
+ this.anonKey = config.anonKey;
390
+ this.defaultHeaders = {
391
+ ...config.headers
392
+ };
393
+ this.tokenManager = tokenManager ?? new TokenManager();
394
+ this.logger = logger || new Logger(false);
395
+ this.timeout = config.timeout ?? 3e4;
396
+ this.retryCount = config.retryCount ?? 3;
397
+ this.retryDelay = config.retryDelay ?? 500;
398
+ if (!this.fetch) {
399
+ throw new Error(
400
+ "Fetch is not available. Please provide a fetch implementation in the config."
401
+ );
402
+ }
403
+ }
404
+ /**
405
+ * Builds a full URL from a path and optional query parameters.
406
+ * Normalizes PostgREST select parameters for proper syntax.
407
+ */
408
+ buildUrl(path, params) {
409
+ const url = new URL(path, this.baseUrl);
410
+ if (params) {
411
+ Object.entries(params).forEach(([key, value]) => {
412
+ if (key === "select") {
413
+ let normalizedValue = value.replace(/\s+/g, " ").trim();
414
+ normalizedValue = normalizedValue.replace(/\s*\(\s*/g, "(").replace(/\s*\)\s*/g, ")").replace(/\(\s+/g, "(").replace(/\s+\)/g, ")").replace(/,\s+(?=[^()]*\))/g, ",");
415
+ url.searchParams.append(key, normalizedValue);
416
+ } else {
417
+ url.searchParams.append(key, value);
418
+ }
419
+ });
420
+ }
421
+ return url.toString();
422
+ }
423
+ /** Checks if an HTTP status code is eligible for retry (5xx server errors). */
424
+ isRetryableStatus(status) {
425
+ return RETRYABLE_STATUS_CODES.has(status);
426
+ }
427
+ /**
428
+ * Computes the delay before the next retry using exponential backoff with jitter.
429
+ * @param attempt - The current retry attempt number (1-based).
430
+ * @returns Delay in milliseconds.
431
+ */
432
+ computeRetryDelay(attempt) {
433
+ const base = this.retryDelay * Math.pow(2, attempt - 1);
434
+ const jitter = base * (0.85 + Math.random() * 0.3);
435
+ return Math.round(jitter);
436
+ }
437
+ shouldRefreshAccessToken(statusCode, errorCode, authToken, options = {}) {
438
+ return statusCode === 401 && REFRESHABLE_AUTH_ERROR_CODES.has(errorCode ?? "") && !this.config.isServerMode && !this.config.edgeFunctionToken && !options.skipAuthRefresh && authToken !== null;
439
+ }
440
+ async fetchWithRetry(args) {
441
+ const {
442
+ method,
443
+ url,
444
+ headers,
445
+ body,
446
+ fetchOptions,
447
+ callerSignal,
448
+ maxAttempts
449
+ } = args;
450
+ let lastError;
451
+ for (let attempt = 0; attempt <= maxAttempts; attempt++) {
452
+ if (attempt > 0) {
453
+ const delay = this.computeRetryDelay(attempt);
454
+ this.logger.warn(
455
+ `Retry ${attempt}/${maxAttempts} for ${method} ${url} in ${delay}ms`
456
+ );
457
+ if (callerSignal?.aborted) throw callerSignal.reason;
458
+ await new Promise((resolve, reject) => {
459
+ const onAbort = () => {
460
+ clearTimeout(timer2);
461
+ reject(callerSignal.reason);
462
+ };
463
+ const timer2 = setTimeout(() => {
464
+ if (callerSignal)
465
+ callerSignal.removeEventListener("abort", onAbort);
466
+ resolve();
467
+ }, delay);
468
+ if (callerSignal) {
469
+ callerSignal.addEventListener("abort", onAbort, { once: true });
470
+ }
471
+ });
472
+ }
473
+ let controller;
474
+ let timer;
475
+ if (this.timeout > 0 || callerSignal) {
476
+ controller = new AbortController();
477
+ if (this.timeout > 0) {
478
+ timer = setTimeout(() => controller.abort(), this.timeout);
479
+ }
480
+ if (callerSignal) {
481
+ if (callerSignal.aborted) {
482
+ controller.abort(callerSignal.reason);
483
+ } else {
484
+ const onCallerAbort = () => controller.abort(callerSignal.reason);
485
+ callerSignal.addEventListener("abort", onCallerAbort, {
486
+ once: true
487
+ });
488
+ controller.signal.addEventListener(
489
+ "abort",
490
+ () => {
491
+ callerSignal.removeEventListener("abort", onCallerAbort);
492
+ },
493
+ { once: true }
494
+ );
495
+ }
496
+ }
497
+ }
498
+ try {
499
+ const response = await this.fetch(url, {
500
+ method,
501
+ headers,
502
+ body,
503
+ ...fetchOptions,
504
+ ...controller ? { signal: controller.signal } : {}
505
+ });
506
+ if (this.isRetryableStatus(response.status) && attempt < maxAttempts) {
507
+ if (timer !== void 0) clearTimeout(timer);
508
+ await response.body?.cancel();
509
+ lastError = new InsForgeError(
510
+ `Server error: ${response.status} ${response.statusText}`,
511
+ response.status,
512
+ "SERVER_ERROR"
513
+ );
514
+ continue;
515
+ }
516
+ if (timer !== void 0) clearTimeout(timer);
517
+ return response;
518
+ } catch (err) {
519
+ if (timer !== void 0) clearTimeout(timer);
520
+ if (err?.name === "AbortError") {
521
+ if (controller && controller.signal.aborted && this.timeout > 0 && !callerSignal?.aborted) {
522
+ throw new InsForgeError(
523
+ `Request timed out after ${this.timeout}ms`,
524
+ 408,
525
+ "REQUEST_TIMEOUT"
526
+ );
527
+ }
528
+ throw err;
529
+ }
530
+ if (attempt < maxAttempts) {
531
+ lastError = err;
532
+ continue;
533
+ }
534
+ throw new InsForgeError(
535
+ `Network request failed: ${err?.message || "Unknown error"}`,
536
+ 0,
537
+ "NETWORK_ERROR"
538
+ );
539
+ }
540
+ }
541
+ throw lastError || new InsForgeError(
542
+ "Request failed after all retry attempts",
543
+ 0,
544
+ "NETWORK_ERROR"
545
+ );
546
+ }
547
+ /**
548
+ * Performs an HTTP request with automatic retry and timeout handling.
549
+ * Retries on network errors and 5xx server errors with exponential backoff.
550
+ * Client errors (4xx) and timeouts are thrown immediately without retry.
551
+ * @param method - HTTP method (GET, POST, PUT, PATCH, DELETE).
552
+ * @param path - API path relative to the base URL.
553
+ * @param options - Optional request configuration including headers, body, and query params.
554
+ * @returns Parsed response data.
555
+ * @throws {InsForgeError} On timeout, network failure, or HTTP error responses.
556
+ */
557
+ async handleRequest(method, path, options = {}, tokenOverride) {
558
+ const {
559
+ params,
560
+ headers = {},
561
+ body,
562
+ skipAuthRefresh: _skipAuthRefresh,
563
+ signal: callerSignal,
564
+ ...fetchOptions
565
+ } = options;
566
+ const url = this.buildUrl(path, params);
567
+ const startTime = Date.now();
568
+ const canRetry = IDEMPOTENT_METHODS.has(method.toUpperCase()) || options.idempotent === true;
569
+ const maxAttempts = canRetry ? this.retryCount : 0;
570
+ const requestHeaders = {
571
+ ...this.defaultHeaders
572
+ };
573
+ const authToken = tokenOverride ?? this.userToken ?? this.anonKey;
574
+ if (authToken) {
575
+ requestHeaders["Authorization"] = `Bearer ${authToken}`;
576
+ }
577
+ const processedBody = serializeBody(method, body, requestHeaders);
578
+ const setRequestHeader = (key, value) => {
579
+ if (key.toLowerCase() === "authorization") {
580
+ delete requestHeaders["Authorization"];
581
+ delete requestHeaders["authorization"];
582
+ requestHeaders["Authorization"] = value;
583
+ return;
584
+ }
585
+ requestHeaders[key] = value;
586
+ };
587
+ if (headers instanceof Headers) {
588
+ headers.forEach((value, key) => {
589
+ setRequestHeader(key, value);
590
+ });
591
+ } else if (Array.isArray(headers)) {
592
+ headers.forEach(([key, value]) => {
593
+ setRequestHeader(key, value);
594
+ });
595
+ } else {
596
+ Object.entries(headers).forEach(([key, value]) => {
597
+ setRequestHeader(key, value);
598
+ });
599
+ }
600
+ this.logger.logRequest(method, url, requestHeaders, processedBody);
601
+ const response = await this.fetchWithRetry({
602
+ method,
603
+ url,
604
+ headers: requestHeaders,
605
+ body: processedBody,
606
+ fetchOptions,
607
+ callerSignal,
608
+ maxAttempts
609
+ });
610
+ let data;
611
+ try {
612
+ data = await parseResponse(response);
613
+ } catch (err) {
614
+ if (err instanceof InsForgeError) {
615
+ this.logger.logResponse(
616
+ method,
617
+ url,
618
+ err.statusCode || response.status,
619
+ Date.now() - startTime,
620
+ err
621
+ );
622
+ }
623
+ throw err;
624
+ }
625
+ this.logger.logResponse(
626
+ method,
627
+ url,
628
+ response.status,
629
+ Date.now() - startTime,
630
+ data
631
+ );
632
+ return data;
633
+ }
634
+ async request(method, path, options = {}) {
635
+ const tokenUsed = this.userToken;
636
+ try {
637
+ return await this.handleRequest(
638
+ method,
639
+ path,
640
+ { ...options },
641
+ tokenUsed
642
+ );
643
+ } catch (error) {
644
+ if (!(error instanceof InsForgeError) || !this.shouldRefreshAccessToken(
645
+ error.statusCode,
646
+ error.error,
647
+ tokenUsed,
648
+ options
649
+ )) {
650
+ throw error;
651
+ }
652
+ if (tokenUsed !== this.userToken) {
653
+ if (this.userToken === null) {
654
+ throw error;
655
+ }
656
+ return await this.handleRequest(
657
+ method,
658
+ path,
659
+ {
660
+ ...options,
661
+ skipAuthRefresh: true
662
+ },
663
+ this.userToken
664
+ );
665
+ }
666
+ try {
667
+ await this.refreshAndSaveSession();
668
+ } catch (error2) {
669
+ if (error2 instanceof InsForgeError && (error2.statusCode === 401 || error2.statusCode === 403)) {
670
+ this.clearAuthSession();
671
+ }
672
+ throw error2;
673
+ }
674
+ return await this.handleRequest(method, path, {
675
+ ...options,
676
+ skipAuthRefresh: true
677
+ });
678
+ }
679
+ }
680
+ /**
681
+ * Performs an SDK-configured fetch and returns the raw Response.
682
+ * This is used by clients such as postgrest-js that need to own response
683
+ * parsing while still sharing SDK auth and refresh behavior.
684
+ */
685
+ async rawFetch(input, init, options = {}) {
686
+ const request = typeof Request !== "undefined" && input instanceof Request ? input : void 0;
687
+ const {
688
+ method: initMethod,
689
+ headers: initHeaders,
690
+ body: initBody,
691
+ signal: initSignal,
692
+ ...fetchOptions
693
+ } = init ?? {};
694
+ const method = initMethod ?? request?.method ?? "GET";
695
+ const url = request?.url ?? input.toString();
696
+ const startTime = Date.now();
697
+ const tokenUsed = this.userToken;
698
+ const headers = new Headers({
699
+ ...this.defaultHeaders
700
+ });
701
+ const authToken = tokenUsed ?? this.anonKey;
702
+ if (authToken) {
703
+ headers.set("Authorization", `Bearer ${authToken}`);
704
+ }
705
+ request?.headers.forEach((value, key) => {
706
+ headers.set(key, value);
707
+ });
708
+ new Headers(initHeaders).forEach((value, key) => {
709
+ headers.set(key, value);
710
+ });
711
+ const requestHeaders = {};
712
+ headers.forEach((value, key) => {
713
+ requestHeaders[key] = value;
714
+ });
715
+ const sourceBody = initBody ?? request?.body ?? void 0;
716
+ let body = sourceBody;
717
+ let retryInit = init;
718
+ if (typeof ReadableStream !== "undefined" && sourceBody instanceof ReadableStream) {
719
+ body = await new Response(sourceBody).arrayBuffer();
720
+ retryInit = { ...init ?? {}, body };
721
+ }
722
+ const callerSignal = initSignal ?? request?.signal;
723
+ const maxAttempts = IDEMPOTENT_METHODS.has(method.toUpperCase()) ? this.retryCount : 0;
724
+ this.logger.logRequest(method, url, requestHeaders, body);
725
+ const response = await this.fetchWithRetry({
726
+ method,
727
+ url,
728
+ headers: requestHeaders,
729
+ body,
730
+ fetchOptions,
731
+ callerSignal,
732
+ maxAttempts
733
+ });
734
+ this.logger.logResponse(
735
+ method,
736
+ url,
737
+ response.status,
738
+ Date.now() - startTime
739
+ );
740
+ let errorCode = null;
741
+ if (response.status === 401) {
742
+ try {
743
+ const data = await response.clone().json();
744
+ if (data && typeof data === "object") {
745
+ const candidate = data.error ?? data.code;
746
+ if (typeof candidate === "string") {
747
+ errorCode = candidate;
748
+ }
749
+ }
750
+ } catch {
751
+ }
752
+ }
753
+ if (!this.shouldRefreshAccessToken(
754
+ response.status,
755
+ errorCode,
756
+ tokenUsed,
757
+ options
758
+ )) {
759
+ return response;
760
+ }
761
+ if (tokenUsed !== this.userToken) {
762
+ if (this.userToken === null) {
763
+ return response;
764
+ }
765
+ const retryHeaders2 = new Headers(initHeaders);
766
+ retryHeaders2.set("Authorization", `Bearer ${this.userToken}`);
767
+ return await this.rawFetch(
768
+ input,
769
+ { ...retryInit, headers: retryHeaders2 },
770
+ { skipAuthRefresh: true }
771
+ );
772
+ }
773
+ let newTokenData;
774
+ try {
775
+ newTokenData = await this.refreshAndSaveSession();
776
+ } catch (error) {
777
+ if (error instanceof InsForgeError && (error.statusCode === 401 || error.statusCode === 403)) {
778
+ this.clearAuthSession();
779
+ }
780
+ throw error;
781
+ }
782
+ const retryHeaders = new Headers(initHeaders);
783
+ retryHeaders.set("Authorization", `Bearer ${newTokenData.accessToken}`);
784
+ return await this.rawFetch(
785
+ input,
786
+ { ...retryInit, headers: retryHeaders },
787
+ { skipAuthRefresh: true }
788
+ );
789
+ }
790
+ /** Performs a GET request. */
791
+ get(path, options) {
792
+ return this.request("GET", path, options);
793
+ }
794
+ /** Performs a POST request with an optional JSON body. */
795
+ post(path, body, options) {
796
+ return this.request("POST", path, { ...options, body });
797
+ }
798
+ /** Performs a PUT request with an optional JSON body. */
799
+ put(path, body, options) {
800
+ return this.request("PUT", path, { ...options, body });
801
+ }
802
+ /** Performs a PATCH request with an optional JSON body. */
803
+ patch(path, body, options) {
804
+ return this.request("PATCH", path, { ...options, body });
805
+ }
806
+ /** Performs a DELETE request. */
807
+ delete(path, options) {
808
+ return this.request("DELETE", path, options);
809
+ }
810
+ /** Sets or clears the user authentication token for subsequent requests. */
811
+ setAuthToken(token) {
812
+ this.userToken = token;
813
+ }
814
+ setRefreshToken(token) {
815
+ this.refreshToken = token;
816
+ }
817
+ /** Returns the current default headers including the authorization header if set. */
818
+ getHeaders() {
819
+ const headers = { ...this.defaultHeaders };
820
+ const authToken = this.userToken || this.anonKey;
821
+ if (authToken) {
822
+ headers["Authorization"] = `Bearer ${authToken}`;
823
+ }
824
+ return headers;
825
+ }
826
+ async refreshAccessToken() {
827
+ if (this.isRefreshing) {
828
+ return this.refreshPromise;
829
+ }
830
+ this.isRefreshing = true;
831
+ this.refreshPromise = (async () => {
832
+ try {
833
+ const csrfToken = getCsrfToken();
834
+ const body = this.refreshToken ? { refreshToken: this.refreshToken } : void 0;
835
+ const response = await this.handleRequest(
836
+ "POST",
837
+ this.refreshToken ? "/api/auth/refresh?client_type=mobile" : "/api/auth/refresh",
838
+ {
839
+ body,
840
+ headers: csrfToken ? { "X-CSRF-Token": csrfToken } : {},
841
+ credentials: "include"
842
+ }
843
+ );
844
+ return response;
845
+ } finally {
846
+ this.isRefreshing = false;
847
+ this.refreshPromise = null;
848
+ }
849
+ })();
850
+ return this.refreshPromise;
851
+ }
852
+ async refreshAndSaveSession() {
853
+ const newTokenData = await this.refreshAccessToken();
854
+ this.setAuthToken(newTokenData.accessToken);
855
+ this.tokenManager.saveSession(newTokenData);
856
+ if (newTokenData.csrfToken) {
857
+ setCsrfToken(newTokenData.csrfToken);
858
+ }
859
+ if (newTokenData.refreshToken) {
860
+ this.setRefreshToken(newTokenData.refreshToken);
861
+ }
862
+ return newTokenData;
863
+ }
864
+ clearAuthSession() {
865
+ this.tokenManager.clearSession();
866
+ this.userToken = null;
867
+ this.refreshToken = null;
868
+ clearCsrfToken();
869
+ }
870
+ };
871
+
872
+ // src/modules/auth/helpers.ts
873
+ var PKCE_VERIFIER_KEY = "insforge_pkce_verifier";
874
+ function base64UrlEncode(buffer) {
875
+ const base64 = btoa(String.fromCharCode(...buffer));
876
+ return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
877
+ }
878
+ function generateCodeVerifier() {
879
+ const array = new Uint8Array(32);
880
+ crypto.getRandomValues(array);
881
+ return base64UrlEncode(array);
882
+ }
883
+ async function generateCodeChallenge(verifier) {
884
+ const encoder = new TextEncoder();
885
+ const data = encoder.encode(verifier);
886
+ const hash = await crypto.subtle.digest("SHA-256", data);
887
+ return base64UrlEncode(new Uint8Array(hash));
888
+ }
889
+ function storePkceVerifier(verifier) {
890
+ if (typeof sessionStorage !== "undefined") {
891
+ sessionStorage.setItem(PKCE_VERIFIER_KEY, verifier);
892
+ }
893
+ }
894
+ function retrievePkceVerifier() {
895
+ if (typeof sessionStorage === "undefined") {
896
+ return null;
897
+ }
898
+ const verifier = sessionStorage.getItem(PKCE_VERIFIER_KEY);
899
+ if (verifier) {
900
+ sessionStorage.removeItem(PKCE_VERIFIER_KEY);
901
+ }
902
+ return verifier;
903
+ }
904
+ function wrapError(error, fallbackMessage) {
905
+ if (error instanceof InsForgeError) {
906
+ return { data: null, error };
907
+ }
908
+ return {
909
+ data: null,
910
+ error: new InsForgeError(
911
+ error instanceof Error ? error.message : fallbackMessage,
912
+ 500,
913
+ "UNEXPECTED_ERROR"
914
+ )
915
+ };
916
+ }
917
+ function cleanUrlParams(...params) {
918
+ if (typeof window === "undefined") {
919
+ return;
920
+ }
921
+ const url = new URL(window.location.href);
922
+ params.forEach((p) => url.searchParams.delete(p));
923
+ window.history.replaceState({}, document.title, url.toString());
924
+ }
925
+
926
+ // src/modules/auth/auth.ts
927
+ var import_shared_schemas = require("@insforge/shared-schemas");
928
+ var Auth = class {
929
+ constructor(http, tokenManager, options = {}) {
930
+ this.http = http;
931
+ this.tokenManager = tokenManager;
932
+ this.options = options;
933
+ this.authCallbackHandled = this.detectAuthCallback();
934
+ }
935
+ isServerMode() {
936
+ return !!this.options.isServerMode;
937
+ }
938
+ /**
939
+ * Save session from API response
940
+ * Handles token storage, CSRF token, and HTTP auth header
941
+ */
942
+ saveSessionFromResponse(response) {
943
+ if (!response.accessToken || !response.user) {
944
+ return false;
945
+ }
946
+ const session = {
947
+ accessToken: response.accessToken,
948
+ user: response.user
949
+ };
950
+ if (!this.isServerMode() && response.csrfToken) {
951
+ setCsrfToken(response.csrfToken);
952
+ }
953
+ if (!this.isServerMode()) {
954
+ this.tokenManager.saveSession(session);
955
+ }
956
+ this.http.setAuthToken(response.accessToken);
957
+ this.http.setRefreshToken(response.refreshToken ?? null);
958
+ return true;
959
+ }
960
+ // ============================================================================
961
+ // OAuth Callback Detection (runs on initialization)
962
+ // ============================================================================
963
+ /**
964
+ * Detect and handle OAuth callback parameters in URL
965
+ * Supports PKCE flow (insforge_code)
966
+ */
967
+ async detectAuthCallback() {
968
+ if (this.isServerMode() || typeof window === "undefined") return;
969
+ try {
970
+ const params = new URLSearchParams(window.location.search);
971
+ const error = params.get("error");
972
+ if (error) {
973
+ cleanUrlParams("error");
974
+ console.debug("OAuth callback error:", error);
975
+ return;
976
+ }
977
+ const code = params.get("insforge_code");
978
+ if (code) {
979
+ cleanUrlParams("insforge_code");
980
+ const { error: exchangeError } = await this.exchangeOAuthCode(code);
981
+ if (exchangeError) {
982
+ console.debug("OAuth code exchange failed:", exchangeError.message);
983
+ }
984
+ return;
985
+ }
986
+ } catch (error) {
987
+ console.debug("OAuth callback detection skipped:", error);
988
+ }
989
+ }
990
+ // ============================================================================
991
+ // Sign Up / Sign In / Sign Out
992
+ // ============================================================================
993
+ async signUp(request) {
994
+ try {
995
+ const response = await this.http.post(
996
+ this.isServerMode() ? "/api/auth/users?client_type=mobile" : "/api/auth/users",
997
+ request,
998
+ { credentials: "include", skipAuthRefresh: true }
999
+ );
1000
+ if (response.accessToken && response.user) {
1001
+ this.saveSessionFromResponse(response);
1002
+ }
1003
+ if (response.refreshToken) {
1004
+ this.http.setRefreshToken(response.refreshToken);
1005
+ }
1006
+ return { data: response, error: null };
1007
+ } catch (error) {
1008
+ return wrapError(error, "An unexpected error occurred during sign up");
1009
+ }
1010
+ }
1011
+ async signInWithPassword(request) {
1012
+ try {
1013
+ const response = await this.http.post(
1014
+ this.isServerMode() ? "/api/auth/sessions?client_type=mobile" : "/api/auth/sessions",
1015
+ request,
1016
+ { credentials: "include", skipAuthRefresh: true }
1017
+ );
1018
+ this.saveSessionFromResponse(response);
1019
+ if (response.refreshToken) {
1020
+ this.http.setRefreshToken(response.refreshToken);
1021
+ }
1022
+ return { data: response, error: null };
1023
+ } catch (error) {
1024
+ return wrapError(error, "An unexpected error occurred during sign in");
1025
+ }
1026
+ }
1027
+ async signOut() {
1028
+ try {
1029
+ try {
1030
+ await this.http.post(
1031
+ this.isServerMode() ? "/api/auth/logout?client_type=mobile" : "/api/auth/logout",
1032
+ void 0,
1033
+ { credentials: "include", skipAuthRefresh: true }
1034
+ );
1035
+ } catch {
1036
+ }
1037
+ this.tokenManager.clearSession();
1038
+ this.http.setAuthToken(null);
1039
+ this.http.setRefreshToken(null);
1040
+ if (!this.isServerMode()) {
1041
+ clearCsrfToken();
1042
+ }
1043
+ return { error: null };
1044
+ } catch {
1045
+ return {
1046
+ error: new InsForgeError("Failed to sign out", 500, "SIGNOUT_ERROR")
1047
+ };
1048
+ }
1049
+ }
1050
+ // ============================================================================
1051
+ // OAuth Authentication
1052
+ // ============================================================================
1053
+ /**
1054
+ * Sign in with OAuth provider using PKCE flow
1055
+ */
1056
+ async signInWithOAuth(options) {
1057
+ try {
1058
+ const { provider, redirectTo, skipBrowserRedirect } = options;
1059
+ const providerKey = encodeURIComponent(provider.toLowerCase());
1060
+ const codeVerifier = generateCodeVerifier();
1061
+ const codeChallenge = await generateCodeChallenge(codeVerifier);
1062
+ storePkceVerifier(codeVerifier);
1063
+ const params = { code_challenge: codeChallenge };
1064
+ if (redirectTo) params.redirect_uri = redirectTo;
1065
+ const isBuiltInProvider = import_shared_schemas.oAuthProvidersSchema.options.includes(
1066
+ providerKey
1067
+ );
1068
+ const oauthPath = isBuiltInProvider ? `/api/auth/oauth/${providerKey}` : `/api/auth/oauth/custom/${providerKey}`;
1069
+ const response = await this.http.get(oauthPath, {
1070
+ params,
1071
+ skipAuthRefresh: true
1072
+ });
1073
+ if (!this.isServerMode() && typeof window !== "undefined" && !skipBrowserRedirect) {
1074
+ window.location.href = response.authUrl;
1075
+ return { data: {}, error: null };
1076
+ }
1077
+ return {
1078
+ data: { url: response.authUrl, provider: providerKey, codeVerifier },
1079
+ error: null
1080
+ };
1081
+ } catch (error) {
1082
+ if (error instanceof InsForgeError) {
1083
+ return { data: {}, error };
1084
+ }
1085
+ return {
1086
+ data: {},
1087
+ error: new InsForgeError(
1088
+ "An unexpected error occurred during OAuth initialization",
1089
+ 500,
1090
+ "UNEXPECTED_ERROR"
1091
+ )
1092
+ };
1093
+ }
1094
+ }
1095
+ /**
1096
+ * Exchange OAuth authorization code for tokens (PKCE flow)
1097
+ * Called automatically on initialization when insforge_code is in URL
1098
+ */
1099
+ async exchangeOAuthCode(code, codeVerifier) {
1100
+ try {
1101
+ const verifier = codeVerifier ?? retrievePkceVerifier();
1102
+ if (!verifier) {
1103
+ return {
1104
+ data: null,
1105
+ error: new InsForgeError(
1106
+ "PKCE code verifier not found. Ensure signInWithOAuth was called in the same browser session.",
1107
+ 400,
1108
+ "PKCE_VERIFIER_MISSING"
1109
+ )
1110
+ };
1111
+ }
1112
+ const request = {
1113
+ code,
1114
+ code_verifier: verifier
1115
+ };
1116
+ const response = await this.http.post(
1117
+ this.isServerMode() ? "/api/auth/oauth/exchange?client_type=mobile" : "/api/auth/oauth/exchange",
1118
+ request,
1119
+ { credentials: "include", skipAuthRefresh: true }
1120
+ );
1121
+ this.saveSessionFromResponse(response);
1122
+ return {
1123
+ data: response,
1124
+ error: null
1125
+ };
1126
+ } catch (error) {
1127
+ return wrapError(
1128
+ error,
1129
+ "An unexpected error occurred during OAuth code exchange"
1130
+ );
1131
+ }
1132
+ }
1133
+ /**
1134
+ * Sign in with an ID token from a native SDK (Google One Tap, etc.)
1135
+ * Use this for native mobile apps or Google One Tap on web.
1136
+ *
1137
+ * @param credentials.provider - The identity provider (currently only 'google' is supported)
1138
+ * @param credentials.token - The ID token from the native SDK
1139
+ */
1140
+ async signInWithIdToken(credentials) {
1141
+ try {
1142
+ const { provider, token } = credentials;
1143
+ const response = await this.http.post(
1144
+ "/api/auth/id-token?client_type=mobile",
1145
+ { provider, token },
1146
+ { credentials: "include", skipAuthRefresh: true }
1147
+ );
1148
+ this.saveSessionFromResponse(response);
1149
+ if (response.refreshToken) {
1150
+ this.http.setRefreshToken(response.refreshToken);
1151
+ }
1152
+ return {
1153
+ data: response,
1154
+ error: null
1155
+ };
1156
+ } catch (error) {
1157
+ return wrapError(
1158
+ error,
1159
+ "An unexpected error occurred during ID token sign in"
1160
+ );
1161
+ }
1162
+ }
1163
+ // ============================================================================
1164
+ // Session Management
1165
+ // ============================================================================
1166
+ /**
1167
+ * Refresh the current auth session.
1168
+ *
1169
+ * Browser mode:
1170
+ * - Uses httpOnly refresh cookie and optional CSRF header.
1171
+ *
1172
+ * Legacy server mode (`isServerMode: true`):
1173
+ * - Uses mobile auth flow and requires `refreshToken` in request body.
1174
+ *
1175
+ * SSR apps should prefer `createRefreshAuthRouter()` / `refreshAuth()` from
1176
+ * `@insforge/sdk/ssr`.
1177
+ */
1178
+ async refreshSession(options) {
1179
+ try {
1180
+ if (this.isServerMode() && !options?.refreshToken) {
1181
+ return {
1182
+ data: null,
1183
+ error: new InsForgeError(
1184
+ "refreshToken is required when refreshing session in server mode",
1185
+ 400,
1186
+ import_shared_schemas.ERROR_CODES.AUTH_UNAUTHORIZED
1187
+ )
1188
+ };
1189
+ }
1190
+ const csrfToken = !this.isServerMode() ? getCsrfToken() : null;
1191
+ const response = await this.http.post(
1192
+ this.isServerMode() ? "/api/auth/refresh?client_type=mobile" : "/api/auth/refresh",
1193
+ this.isServerMode() ? { refresh_token: options?.refreshToken } : void 0,
1194
+ {
1195
+ headers: csrfToken ? { "X-CSRF-Token": csrfToken } : {},
1196
+ credentials: "include",
1197
+ skipAuthRefresh: true
1198
+ }
1199
+ );
1200
+ if (response.accessToken) {
1201
+ this.saveSessionFromResponse(response);
1202
+ }
1203
+ return { data: response, error: null };
1204
+ } catch (error) {
1205
+ return wrapError(
1206
+ error,
1207
+ "An unexpected error occurred during session refresh"
1208
+ );
1209
+ }
1210
+ }
1211
+ /**
1212
+ * Get current user, automatically waits for pending OAuth callback
1213
+ */
1214
+ async getCurrentUser() {
1215
+ await this.authCallbackHandled;
1216
+ try {
1217
+ if (this.isServerMode()) {
1218
+ const accessToken = this.tokenManager.getAccessToken();
1219
+ if (!accessToken) return { data: { user: null }, error: null };
1220
+ this.http.setAuthToken(accessToken);
1221
+ const response = await this.http.get(
1222
+ "/api/auth/sessions/current"
1223
+ );
1224
+ const user = response.user ?? null;
1225
+ return { data: { user }, error: null };
1226
+ }
1227
+ const session = this.tokenManager.getSession();
1228
+ if (session) {
1229
+ this.http.setAuthToken(session.accessToken);
1230
+ return { data: { user: session.user }, error: null };
1231
+ }
1232
+ if (typeof window !== "undefined") {
1233
+ const { data: refreshed, error: refreshError } = await this.refreshSession();
1234
+ if (refreshError) {
1235
+ return { data: { user: null }, error: refreshError };
1236
+ }
1237
+ if (refreshed?.accessToken) {
1238
+ return { data: { user: refreshed.user ?? null }, error: null };
1239
+ }
1240
+ }
1241
+ return { data: { user: null }, error: null };
1242
+ } catch (error) {
1243
+ if (error instanceof InsForgeError) {
1244
+ return { data: { user: null }, error };
1245
+ }
1246
+ return {
1247
+ data: { user: null },
1248
+ error: new InsForgeError(
1249
+ "An unexpected error occurred while getting user",
1250
+ 500,
1251
+ "UNEXPECTED_ERROR"
1252
+ )
1253
+ };
1254
+ }
1255
+ }
1256
+ // ============================================================================
1257
+ // Profile Management
1258
+ // ============================================================================
1259
+ async getProfile(userId) {
1260
+ try {
1261
+ const response = await this.http.get(
1262
+ `/api/auth/profiles/${userId}`
1263
+ );
1264
+ return { data: response, error: null };
1265
+ } catch (error) {
1266
+ return wrapError(
1267
+ error,
1268
+ "An unexpected error occurred while fetching user profile"
1269
+ );
1270
+ }
1271
+ }
1272
+ async setProfile(profile) {
1273
+ try {
1274
+ const response = await this.http.patch(
1275
+ "/api/auth/profiles/current",
1276
+ {
1277
+ profile
1278
+ }
1279
+ );
1280
+ const currentUser = this.tokenManager.getUser();
1281
+ if (!this.isServerMode() && currentUser && response.profile !== void 0) {
1282
+ this.tokenManager.setUser({
1283
+ ...currentUser,
1284
+ profile: response.profile
1285
+ });
1286
+ }
1287
+ return { data: response, error: null };
1288
+ } catch (error) {
1289
+ return wrapError(
1290
+ error,
1291
+ "An unexpected error occurred while updating user profile"
1292
+ );
1293
+ }
1294
+ }
1295
+ // ============================================================================
1296
+ // Email Verification
1297
+ // ============================================================================
1298
+ async resendVerificationEmail(request) {
1299
+ try {
1300
+ const response = await this.http.post("/api/auth/email/send-verification", request, {
1301
+ skipAuthRefresh: true
1302
+ });
1303
+ return { data: response, error: null };
1304
+ } catch (error) {
1305
+ return wrapError(
1306
+ error,
1307
+ "An unexpected error occurred while sending verification email"
1308
+ );
1309
+ }
1310
+ }
1311
+ async verifyEmail(request) {
1312
+ try {
1313
+ const response = await this.http.post(
1314
+ this.isServerMode() ? "/api/auth/email/verify?client_type=mobile" : "/api/auth/email/verify",
1315
+ request,
1316
+ { credentials: "include", skipAuthRefresh: true }
1317
+ );
1318
+ this.saveSessionFromResponse(response);
1319
+ if (response.refreshToken) {
1320
+ this.http.setRefreshToken(response.refreshToken);
1321
+ }
1322
+ return { data: response, error: null };
1323
+ } catch (error) {
1324
+ return wrapError(
1325
+ error,
1326
+ "An unexpected error occurred while verifying email"
1327
+ );
1328
+ }
1329
+ }
1330
+ // ============================================================================
1331
+ // Password Reset
1332
+ // ============================================================================
1333
+ async sendResetPasswordEmail(request) {
1334
+ try {
1335
+ const response = await this.http.post("/api/auth/email/send-reset-password", request, {
1336
+ skipAuthRefresh: true
1337
+ });
1338
+ return { data: response, error: null };
1339
+ } catch (error) {
1340
+ return wrapError(
1341
+ error,
1342
+ "An unexpected error occurred while sending password reset email"
1343
+ );
1344
+ }
1345
+ }
1346
+ async exchangeResetPasswordToken(request) {
1347
+ try {
1348
+ const response = await this.http.post(
1349
+ "/api/auth/email/exchange-reset-password-token",
1350
+ request,
1351
+ { skipAuthRefresh: true }
1352
+ );
1353
+ return { data: response, error: null };
1354
+ } catch (error) {
1355
+ return wrapError(
1356
+ error,
1357
+ "An unexpected error occurred while verifying reset code"
1358
+ );
1359
+ }
1360
+ }
1361
+ async resetPassword(request) {
1362
+ try {
1363
+ const response = await this.http.post(
1364
+ "/api/auth/email/reset-password",
1365
+ request,
1366
+ { skipAuthRefresh: true }
1367
+ );
1368
+ return { data: response, error: null };
1369
+ } catch (error) {
1370
+ return wrapError(
1371
+ error,
1372
+ "An unexpected error occurred while resetting password"
1373
+ );
1374
+ }
1375
+ }
1376
+ // ============================================================================
1377
+ // Configuration
1378
+ // ============================================================================
1379
+ async getPublicAuthConfig() {
1380
+ try {
1381
+ const response = await this.http.get(
1382
+ "/api/auth/public-config",
1383
+ { skipAuthRefresh: true }
1384
+ );
1385
+ return { data: response, error: null };
1386
+ } catch (error) {
1387
+ return wrapError(
1388
+ error,
1389
+ "An unexpected error occurred while fetching auth configuration"
1390
+ );
1391
+ }
1392
+ }
1393
+ };
1394
+
1395
+ // src/modules/database-postgrest.ts
1396
+ var import_postgrest_js = require("@supabase/postgrest-js");
1397
+ function createInsForgePostgrestFetch(httpClient) {
1398
+ return async (input, init) => {
1399
+ const url = typeof input === "string" ? input : input.toString();
1400
+ const urlObj = new URL(url);
1401
+ const pathname = urlObj.pathname.slice(1);
1402
+ const rpcMatch = pathname.match(/^rpc\/(.+)$/);
1403
+ const endpoint = rpcMatch ? `/api/database/rpc/${rpcMatch[1]}` : `/api/database/records/${pathname}`;
1404
+ const insforgeUrl = `${httpClient.baseUrl}${endpoint}${urlObj.search}`;
1405
+ const headers = new Headers(httpClient.getHeaders());
1406
+ new Headers(init?.headers).forEach((value, key) => {
1407
+ headers.set(key, value);
1408
+ });
1409
+ const response = await httpClient.rawFetch(insforgeUrl, {
1410
+ ...init,
1411
+ headers
1412
+ });
1413
+ return response;
1414
+ };
1415
+ }
1416
+ var Database = class {
1417
+ constructor(httpClient) {
1418
+ this.postgrest = new import_postgrest_js.PostgrestClient("http://dummy", {
1419
+ fetch: createInsForgePostgrestFetch(httpClient),
1420
+ headers: {}
1421
+ });
1422
+ }
1423
+ /**
1424
+ * Create a query builder for a table
1425
+ *
1426
+ * @example
1427
+ * // Basic query
1428
+ * const { data, error } = await client.database
1429
+ * .from('posts')
1430
+ * .select('*')
1431
+ * .eq('user_id', userId);
1432
+ *
1433
+ * // With count (Supabase style!)
1434
+ * const { data, error, count } = await client.database
1435
+ * .from('posts')
1436
+ * .select('*', { count: 'exact' })
1437
+ * .range(0, 9);
1438
+ *
1439
+ * // Just get count, no data
1440
+ * const { count } = await client.database
1441
+ * .from('posts')
1442
+ * .select('*', { count: 'exact', head: true });
1443
+ *
1444
+ * // Complex queries with OR
1445
+ * const { data } = await client.database
1446
+ * .from('posts')
1447
+ * .select('*, users!inner(*)')
1448
+ * .or('status.eq.active,status.eq.pending');
1449
+ *
1450
+ * // All features work:
1451
+ * - Nested selects
1452
+ * - Foreign key expansion
1453
+ * - OR/AND/NOT conditions
1454
+ * - Count with head
1455
+ * - Range pagination
1456
+ * - Upserts
1457
+ */
1458
+ from(table) {
1459
+ return this.postgrest.from(table);
1460
+ }
1461
+ /**
1462
+ * Call a PostgreSQL function (RPC)
1463
+ *
1464
+ * @example
1465
+ * // Call a function with parameters
1466
+ * const { data, error } = await client.database
1467
+ * .rpc('get_user_stats', { user_id: 123 });
1468
+ *
1469
+ * // Call a function with no parameters
1470
+ * const { data, error } = await client.database
1471
+ * .rpc('get_all_active_users');
1472
+ *
1473
+ * // With options (head, count, get)
1474
+ * const { data, count } = await client.database
1475
+ * .rpc('search_posts', { query: 'hello' }, { count: 'exact' });
1476
+ */
1477
+ rpc(fn, args, options) {
1478
+ return this.postgrest.rpc(fn, args, options);
1479
+ }
1480
+ };
1481
+
1482
+ // src/modules/storage.ts
1483
+ var StorageBucket = class {
1484
+ constructor(bucketName, http) {
1485
+ this.bucketName = bucketName;
1486
+ this.http = http;
1487
+ }
1488
+ /**
1489
+ * Upload a file with a specific key
1490
+ * Uses the upload strategy from backend (direct or presigned)
1491
+ * @param path - The object key/path
1492
+ * @param file - File or Blob to upload
1493
+ */
1494
+ async upload(path, file) {
1495
+ try {
1496
+ const strategyResponse = await this.http.post(
1497
+ `/api/storage/buckets/${this.bucketName}/upload-strategy`,
1498
+ {
1499
+ filename: path,
1500
+ contentType: file.type || "application/octet-stream",
1501
+ size: file.size
1502
+ }
1503
+ );
1504
+ if (strategyResponse.method === "presigned") {
1505
+ return await this.uploadWithPresignedUrl(strategyResponse, file);
1506
+ }
1507
+ if (strategyResponse.method === "direct") {
1508
+ const formData = new FormData();
1509
+ formData.append("file", file);
1510
+ const response = await this.http.request(
1511
+ "PUT",
1512
+ `/api/storage/buckets/${this.bucketName}/objects/${encodeURIComponent(path)}`,
1513
+ {
1514
+ body: formData,
1515
+ headers: {
1516
+ // Don't set Content-Type, let browser set multipart boundary
1517
+ }
1518
+ }
1519
+ );
1520
+ return { data: response, error: null };
1521
+ }
1522
+ throw new InsForgeError(
1523
+ `Unsupported upload method: ${strategyResponse.method}`,
1524
+ 500,
1525
+ "STORAGE_ERROR"
1526
+ );
1527
+ } catch (error) {
1528
+ return {
1529
+ data: null,
1530
+ error: error instanceof InsForgeError ? error : new InsForgeError(
1531
+ "Upload failed",
1532
+ 500,
1533
+ "STORAGE_ERROR"
1534
+ )
1535
+ };
1536
+ }
1537
+ }
1538
+ /**
1539
+ * Upload a file with auto-generated key
1540
+ * Uses the upload strategy from backend (direct or presigned)
1541
+ * @param file - File or Blob to upload
1542
+ */
1543
+ async uploadAuto(file) {
1544
+ try {
1545
+ const filename = file instanceof File ? file.name : "file";
1546
+ const strategyResponse = await this.http.post(
1547
+ `/api/storage/buckets/${this.bucketName}/upload-strategy`,
1548
+ {
1549
+ filename,
1550
+ contentType: file.type || "application/octet-stream",
1551
+ size: file.size
1552
+ }
1553
+ );
1554
+ if (strategyResponse.method === "presigned") {
1555
+ return await this.uploadWithPresignedUrl(strategyResponse, file);
1556
+ }
1557
+ if (strategyResponse.method === "direct") {
1558
+ const formData = new FormData();
1559
+ formData.append("file", file);
1560
+ const response = await this.http.request(
1561
+ "POST",
1562
+ `/api/storage/buckets/${this.bucketName}/objects`,
1563
+ {
1564
+ body: formData,
1565
+ headers: {
1566
+ // Don't set Content-Type, let browser set multipart boundary
1567
+ }
1568
+ }
1569
+ );
1570
+ return { data: response, error: null };
1571
+ }
1572
+ throw new InsForgeError(
1573
+ `Unsupported upload method: ${strategyResponse.method}`,
1574
+ 500,
1575
+ "STORAGE_ERROR"
1576
+ );
1577
+ } catch (error) {
1578
+ return {
1579
+ data: null,
1580
+ error: error instanceof InsForgeError ? error : new InsForgeError(
1581
+ "Upload failed",
1582
+ 500,
1583
+ "STORAGE_ERROR"
1584
+ )
1585
+ };
1586
+ }
1587
+ }
1588
+ /**
1589
+ * Internal method to handle presigned URL uploads
1590
+ */
1591
+ async uploadWithPresignedUrl(strategy, file) {
1592
+ try {
1593
+ const formData = new FormData();
1594
+ if (strategy.fields) {
1595
+ Object.entries(strategy.fields).forEach(([key, value]) => {
1596
+ formData.append(key, value);
1597
+ });
1598
+ }
1599
+ formData.append("file", file);
1600
+ const uploadResponse = await fetch(strategy.uploadUrl, {
1601
+ method: "POST",
1602
+ body: formData
1603
+ });
1604
+ if (!uploadResponse.ok) {
1605
+ throw new InsForgeError(
1606
+ `Upload to storage failed: ${uploadResponse.statusText}`,
1607
+ uploadResponse.status,
1608
+ "STORAGE_ERROR"
1609
+ );
1610
+ }
1611
+ if (strategy.confirmRequired && strategy.confirmUrl) {
1612
+ const confirmResponse = await this.http.post(
1613
+ strategy.confirmUrl,
1614
+ {
1615
+ size: file.size,
1616
+ contentType: file.type || "application/octet-stream"
1617
+ }
1618
+ );
1619
+ return { data: confirmResponse, error: null };
1620
+ }
1621
+ return {
1622
+ data: {
1623
+ key: strategy.key,
1624
+ bucket: this.bucketName,
1625
+ size: file.size,
1626
+ mimeType: file.type || "application/octet-stream",
1627
+ uploadedAt: (/* @__PURE__ */ new Date()).toISOString(),
1628
+ url: this.getPublicUrl(strategy.key)
1629
+ },
1630
+ error: null
1631
+ };
1632
+ } catch (error) {
1633
+ throw error instanceof InsForgeError ? error : new InsForgeError(
1634
+ "Presigned upload failed",
1635
+ 500,
1636
+ "STORAGE_ERROR"
1637
+ );
1638
+ }
1639
+ }
1640
+ /**
1641
+ * Download a file
1642
+ * Uses the download strategy from backend (direct or presigned)
1643
+ * @param path - The object key/path
1644
+ * Returns the file as a Blob
1645
+ */
1646
+ async download(path) {
1647
+ try {
1648
+ const encodedKey = encodeURIComponent(path);
1649
+ let strategyResponse;
1650
+ try {
1651
+ strategyResponse = await this.http.get(
1652
+ `/api/storage/buckets/${this.bucketName}/download-strategy/objects/${encodedKey}`
1653
+ );
1654
+ } catch (err) {
1655
+ const status = err instanceof InsForgeError ? err.statusCode : void 0;
1656
+ if (status === 404 || status === 405) {
1657
+ strategyResponse = await this.http.post(
1658
+ `/api/storage/buckets/${this.bucketName}/objects/${encodedKey}/download-strategy`,
1659
+ {}
1660
+ );
1661
+ } else {
1662
+ throw err;
1663
+ }
1664
+ }
1665
+ const downloadUrl = strategyResponse.url;
1666
+ const headers = {};
1667
+ if (strategyResponse.method === "direct") {
1668
+ Object.assign(headers, this.http.getHeaders());
1669
+ }
1670
+ const response = await fetch(downloadUrl, {
1671
+ method: "GET",
1672
+ headers
1673
+ });
1674
+ if (!response.ok) {
1675
+ try {
1676
+ const error = await response.json();
1677
+ throw InsForgeError.fromApiError(error);
1678
+ } catch {
1679
+ throw new InsForgeError(
1680
+ `Download failed: ${response.statusText}`,
1681
+ response.status,
1682
+ "STORAGE_ERROR"
1683
+ );
1684
+ }
1685
+ }
1686
+ const blob = await response.blob();
1687
+ return { data: blob, error: null };
1688
+ } catch (error) {
1689
+ return {
1690
+ data: null,
1691
+ error: error instanceof InsForgeError ? error : new InsForgeError(
1692
+ "Download failed",
1693
+ 500,
1694
+ "STORAGE_ERROR"
1695
+ )
1696
+ };
1697
+ }
1698
+ }
1699
+ /**
1700
+ * Get public URL for a file
1701
+ * @param path - The object key/path
1702
+ */
1703
+ getPublicUrl(path) {
1704
+ return `${this.http.baseUrl}/api/storage/buckets/${this.bucketName}/objects/${encodeURIComponent(path)}`;
1705
+ }
1706
+ /**
1707
+ * List objects in the bucket
1708
+ * @param prefix - Filter by key prefix
1709
+ * @param search - Search in file names
1710
+ * @param limit - Maximum number of results (default: 100, max: 1000)
1711
+ * @param offset - Number of results to skip
1712
+ */
1713
+ async list(options) {
1714
+ try {
1715
+ const params = {};
1716
+ if (options?.prefix) params.prefix = options.prefix;
1717
+ if (options?.search) params.search = options.search;
1718
+ if (options?.limit) params.limit = options.limit.toString();
1719
+ if (options?.offset) params.offset = options.offset.toString();
1720
+ const response = await this.http.get(
1721
+ `/api/storage/buckets/${this.bucketName}/objects`,
1722
+ { params }
1723
+ );
1724
+ return { data: response, error: null };
1725
+ } catch (error) {
1726
+ return {
1727
+ data: null,
1728
+ error: error instanceof InsForgeError ? error : new InsForgeError(
1729
+ "List failed",
1730
+ 500,
1731
+ "STORAGE_ERROR"
1732
+ )
1733
+ };
1734
+ }
1735
+ }
1736
+ /**
1737
+ * Delete a file
1738
+ * @param path - The object key/path
1739
+ */
1740
+ async remove(path) {
1741
+ try {
1742
+ const response = await this.http.delete(
1743
+ `/api/storage/buckets/${this.bucketName}/objects/${encodeURIComponent(path)}`
1744
+ );
1745
+ return { data: response, error: null };
1746
+ } catch (error) {
1747
+ return {
1748
+ data: null,
1749
+ error: error instanceof InsForgeError ? error : new InsForgeError(
1750
+ "Delete failed",
1751
+ 500,
1752
+ "STORAGE_ERROR"
1753
+ )
1754
+ };
1755
+ }
1756
+ }
1757
+ };
1758
+ var Storage = class {
1759
+ constructor(http) {
1760
+ this.http = http;
1761
+ }
1762
+ /**
1763
+ * Get a bucket instance for operations
1764
+ * @param bucketName - Name of the bucket
1765
+ */
1766
+ from(bucketName) {
1767
+ return new StorageBucket(bucketName, this.http);
1768
+ }
1769
+ };
1770
+
1771
+ // src/modules/ai.ts
1772
+ var AI = class {
1773
+ constructor(http) {
1774
+ this.http = http;
1775
+ this.chat = new Chat(http);
1776
+ this.images = new Images(http);
1777
+ this.embeddings = new Embeddings(http);
1778
+ }
1779
+ };
1780
+ var Chat = class {
1781
+ constructor(http) {
1782
+ this.completions = new ChatCompletions(http);
1783
+ }
1784
+ };
1785
+ var ChatCompletions = class {
1786
+ constructor(http) {
1787
+ this.http = http;
1788
+ }
1789
+ /**
1790
+ * Create a chat completion - OpenAI-like response format
1791
+ *
1792
+ * @example
1793
+ * ```typescript
1794
+ * // Non-streaming
1795
+ * const completion = await client.ai.chat.completions.create({
1796
+ * model: 'gpt-4',
1797
+ * messages: [{ role: 'user', content: 'Hello!' }]
1798
+ * });
1799
+ * console.log(completion.choices[0].message.content);
1800
+ *
1801
+ * // With images (OpenAI-compatible format)
1802
+ * const response = await client.ai.chat.completions.create({
1803
+ * model: 'gpt-4-vision',
1804
+ * messages: [{
1805
+ * role: 'user',
1806
+ * content: [
1807
+ * { type: 'text', text: 'What is in this image?' },
1808
+ * { type: 'image_url', image_url: { url: 'https://example.com/image.jpg' } }
1809
+ * ]
1810
+ * }]
1811
+ * });
1812
+ *
1813
+ * // With PDF files
1814
+ * const pdfResponse = await client.ai.chat.completions.create({
1815
+ * model: 'anthropic/claude-3.5-sonnet',
1816
+ * messages: [{
1817
+ * role: 'user',
1818
+ * content: [
1819
+ * { type: 'text', text: 'Summarize this document' },
1820
+ * { type: 'file', file: { filename: 'doc.pdf', file_data: 'https://example.com/doc.pdf' } }
1821
+ * ]
1822
+ * }],
1823
+ * fileParser: { enabled: true, pdf: { engine: 'mistral-ocr' } }
1824
+ * });
1825
+ *
1826
+ * // With web search
1827
+ * const searchResponse = await client.ai.chat.completions.create({
1828
+ * model: 'openai/gpt-4',
1829
+ * messages: [{ role: 'user', content: 'What are the latest news about AI?' }],
1830
+ * webSearch: { enabled: true, maxResults: 5 }
1831
+ * });
1832
+ * // Access citations from response.choices[0].message.annotations
1833
+ *
1834
+ * // With thinking/reasoning mode (Anthropic models)
1835
+ * const thinkingResponse = await client.ai.chat.completions.create({
1836
+ * model: 'anthropic/claude-3.5-sonnet',
1837
+ * messages: [{ role: 'user', content: 'Solve this complex math problem...' }],
1838
+ * thinking: true
1839
+ * });
1840
+ *
1841
+ * // Streaming - returns async iterable
1842
+ * const stream = await client.ai.chat.completions.create({
1843
+ * model: 'gpt-4',
1844
+ * messages: [{ role: 'user', content: 'Tell me a story' }],
1845
+ * stream: true
1846
+ * });
1847
+ *
1848
+ * for await (const chunk of stream) {
1849
+ * if (chunk.choices[0]?.delta?.content) {
1850
+ * process.stdout.write(chunk.choices[0].delta.content);
1851
+ * }
1852
+ * }
1853
+ * ```
1854
+ */
1855
+ async create(params) {
1856
+ const backendParams = {
1857
+ model: params.model,
1858
+ messages: params.messages,
1859
+ temperature: params.temperature,
1860
+ maxTokens: params.maxTokens,
1861
+ topP: params.topP,
1862
+ stream: params.stream,
1863
+ // New plugin options
1864
+ webSearch: params.webSearch,
1865
+ fileParser: params.fileParser,
1866
+ thinking: params.thinking,
1867
+ // Tool calling options
1868
+ tools: params.tools,
1869
+ toolChoice: params.toolChoice,
1870
+ parallelToolCalls: params.parallelToolCalls
1871
+ };
1872
+ if (params.stream) {
1873
+ const headers = this.http.getHeaders();
1874
+ headers["Content-Type"] = "application/json";
1875
+ const response2 = await this.http.fetch(
1876
+ `${this.http.baseUrl}/api/ai/chat/completion`,
1877
+ {
1878
+ method: "POST",
1879
+ headers,
1880
+ body: JSON.stringify(backendParams)
1881
+ }
1882
+ );
1883
+ if (!response2.ok) {
1884
+ const error = await response2.json();
1885
+ throw new Error(error.error || "Stream request failed");
1886
+ }
1887
+ return this.parseSSEStream(response2, params.model);
1888
+ }
1889
+ const response = await this.http.post(
1890
+ "/api/ai/chat/completion",
1891
+ backendParams
1892
+ );
1893
+ const content = response.text || "";
1894
+ return {
1895
+ id: `chatcmpl-${Date.now()}`,
1896
+ object: "chat.completion",
1897
+ created: Math.floor(Date.now() / 1e3),
1898
+ model: response.metadata?.model,
1899
+ choices: [
1900
+ {
1901
+ index: 0,
1902
+ message: {
1903
+ role: "assistant",
1904
+ content,
1905
+ // Include tool_calls if present (from tool calling)
1906
+ ...response.tool_calls?.length && { tool_calls: response.tool_calls },
1907
+ // Include annotations if present (from web search or file parsing)
1908
+ ...response.annotations?.length && { annotations: response.annotations }
1909
+ },
1910
+ finish_reason: response.tool_calls?.length ? "tool_calls" : "stop"
1911
+ }
1912
+ ],
1913
+ usage: response.metadata?.usage || {
1914
+ prompt_tokens: 0,
1915
+ completion_tokens: 0,
1916
+ total_tokens: 0
1917
+ }
1918
+ };
1919
+ }
1920
+ /**
1921
+ * Parse SSE stream into async iterable of OpenAI-like chunks
1922
+ */
1923
+ async *parseSSEStream(response, model) {
1924
+ const reader = response.body.getReader();
1925
+ const decoder = new TextDecoder();
1926
+ let buffer = "";
1927
+ try {
1928
+ while (true) {
1929
+ const { done, value } = await reader.read();
1930
+ if (done) break;
1931
+ buffer += decoder.decode(value, { stream: true });
1932
+ const lines = buffer.split("\n");
1933
+ buffer = lines.pop() || "";
1934
+ for (const line of lines) {
1935
+ if (line.startsWith("data: ")) {
1936
+ const dataStr = line.slice(6).trim();
1937
+ if (dataStr) {
1938
+ try {
1939
+ const data = JSON.parse(dataStr);
1940
+ if (data.chunk || data.content) {
1941
+ yield {
1942
+ id: `chatcmpl-${Date.now()}`,
1943
+ object: "chat.completion.chunk",
1944
+ created: Math.floor(Date.now() / 1e3),
1945
+ model,
1946
+ choices: [
1947
+ {
1948
+ index: 0,
1949
+ delta: {
1950
+ content: data.chunk || data.content
1951
+ },
1952
+ finish_reason: null
1953
+ }
1954
+ ]
1955
+ };
1956
+ }
1957
+ if (data.tool_calls?.length) {
1958
+ yield {
1959
+ id: `chatcmpl-${Date.now()}`,
1960
+ object: "chat.completion.chunk",
1961
+ created: Math.floor(Date.now() / 1e3),
1962
+ model,
1963
+ choices: [
1964
+ {
1965
+ index: 0,
1966
+ delta: {
1967
+ tool_calls: data.tool_calls
1968
+ },
1969
+ finish_reason: "tool_calls"
1970
+ }
1971
+ ]
1972
+ };
1973
+ }
1974
+ if (data.done) {
1975
+ reader.releaseLock();
1976
+ return;
1977
+ }
1978
+ } catch (e) {
1979
+ console.warn("Failed to parse SSE data:", dataStr);
1980
+ }
1981
+ }
1982
+ }
1983
+ }
1984
+ }
1985
+ } finally {
1986
+ reader.releaseLock();
1987
+ }
1988
+ }
1989
+ };
1990
+ var Embeddings = class {
1991
+ constructor(http) {
1992
+ this.http = http;
1993
+ }
1994
+ /**
1995
+ * Create embeddings for text input - OpenAI-like response format
1996
+ *
1997
+ * @example
1998
+ * ```typescript
1999
+ * // Single text input
2000
+ * const response = await client.ai.embeddings.create({
2001
+ * model: 'openai/text-embedding-3-small',
2002
+ * input: 'Hello world'
2003
+ * });
2004
+ * console.log(response.data[0].embedding); // number[]
2005
+ *
2006
+ * // Multiple text inputs
2007
+ * const response = await client.ai.embeddings.create({
2008
+ * model: 'openai/text-embedding-3-small',
2009
+ * input: ['Hello world', 'Goodbye world']
2010
+ * });
2011
+ * response.data.forEach((item, i) => {
2012
+ * console.log(`Embedding ${i}:`, item.embedding.slice(0, 5)); // First 5 dimensions
2013
+ * });
2014
+ *
2015
+ * // With custom dimensions (if supported by model)
2016
+ * const response = await client.ai.embeddings.create({
2017
+ * model: 'openai/text-embedding-3-small',
2018
+ * input: 'Hello world',
2019
+ * dimensions: 256
2020
+ * });
2021
+ *
2022
+ * // With base64 encoding format
2023
+ * const response = await client.ai.embeddings.create({
2024
+ * model: 'openai/text-embedding-3-small',
2025
+ * input: 'Hello world',
2026
+ * encoding_format: 'base64'
2027
+ * });
2028
+ * ```
2029
+ */
2030
+ async create(params) {
2031
+ const response = await this.http.post(
2032
+ "/api/ai/embeddings",
2033
+ params
2034
+ );
2035
+ return {
2036
+ object: response.object,
2037
+ data: response.data,
2038
+ model: response.metadata?.model,
2039
+ usage: response.metadata?.usage ? {
2040
+ prompt_tokens: response.metadata.usage.promptTokens || 0,
2041
+ total_tokens: response.metadata.usage.totalTokens || 0
2042
+ } : {
2043
+ prompt_tokens: 0,
2044
+ total_tokens: 0
2045
+ }
2046
+ };
2047
+ }
2048
+ };
2049
+ var Images = class {
2050
+ constructor(http) {
2051
+ this.http = http;
2052
+ }
2053
+ /**
2054
+ * Generate images - OpenAI-like response format
2055
+ *
2056
+ * @example
2057
+ * ```typescript
2058
+ * // Text-to-image
2059
+ * const response = await client.ai.images.generate({
2060
+ * model: 'dall-e-3',
2061
+ * prompt: 'A sunset over mountains',
2062
+ * });
2063
+ * console.log(response.images[0].url);
2064
+ *
2065
+ * // Image-to-image (with input images)
2066
+ * const response = await client.ai.images.generate({
2067
+ * model: 'stable-diffusion-xl',
2068
+ * prompt: 'Transform this into a watercolor painting',
2069
+ * images: [
2070
+ * { url: 'https://example.com/input.jpg' },
2071
+ * // or base64-encoded Data URI:
2072
+ * { url: 'data:image/jpeg;base64,/9j/4AAQ...' }
2073
+ * ]
2074
+ * });
2075
+ * ```
2076
+ */
2077
+ async generate(params) {
2078
+ const response = await this.http.post(
2079
+ "/api/ai/image/generation",
2080
+ params
2081
+ );
2082
+ let data = [];
2083
+ if (response.images && response.images.length > 0) {
2084
+ data = response.images.map((img) => ({
2085
+ b64_json: img.imageUrl.replace(/^data:image\/\w+;base64,/, ""),
2086
+ content: response.text
2087
+ }));
2088
+ } else if (response.text) {
2089
+ data = [{ content: response.text }];
2090
+ }
2091
+ return {
2092
+ created: Math.floor(Date.now() / 1e3),
2093
+ data,
2094
+ ...response.metadata?.usage && {
2095
+ usage: {
2096
+ total_tokens: response.metadata.usage.totalTokens || 0,
2097
+ input_tokens: response.metadata.usage.promptTokens || 0,
2098
+ output_tokens: response.metadata.usage.completionTokens || 0
2099
+ }
2100
+ }
2101
+ };
2102
+ }
2103
+ };
2104
+
2105
+ // src/modules/functions.ts
2106
+ var Functions = class _Functions {
2107
+ constructor(http, functionsUrl) {
2108
+ this.http = http;
2109
+ this.functionsUrl = functionsUrl || _Functions.deriveSubhostingUrl(http.baseUrl);
2110
+ }
2111
+ /**
2112
+ * Derive the subhosting URL from the base URL.
2113
+ * Base URL pattern: https://{appKey}.{region}.insforge.app
2114
+ * Functions URL: https://{appKey}.functions.insforge.app
2115
+ * Only applies to .insforge.app domains.
2116
+ */
2117
+ static deriveSubhostingUrl(baseUrl) {
2118
+ try {
2119
+ const { hostname } = new URL(baseUrl);
2120
+ if (!hostname.endsWith(".insforge.app")) return void 0;
2121
+ const appKey = hostname.split(".")[0];
2122
+ return `https://${appKey}.functions.insforge.app`;
2123
+ } catch {
2124
+ return void 0;
2125
+ }
2126
+ }
2127
+ /**
2128
+ * Build a Request for in-process dispatch. The host is a non-routable
2129
+ * placeholder; the router only reads pathname.
2130
+ */
2131
+ buildInProcessRequest(slug, method, body, callerHeaders) {
2132
+ const url = new URL("/" + slug, "http://insforge.local").toString();
2133
+ const headers = { ...this.http.getHeaders() };
2134
+ const reqBody = serializeBody(method, body, headers);
2135
+ Object.assign(headers, callerHeaders);
2136
+ return new Request(url, {
2137
+ method,
2138
+ headers,
2139
+ body: reqBody
2140
+ });
2141
+ }
2142
+ /**
2143
+ * Invoke an Edge Function.
2144
+ *
2145
+ * Dispatch order:
2146
+ * 1. If `globalThis.__insforge_dispatch__` is present, call it in-process.
2147
+ * This avoids Deno Subhosting's 508 Loop Detected when one bundled
2148
+ * function invokes another inside the same deployment.
2149
+ * 2. Otherwise, try the configured subhosting URL.
2150
+ * 3. On 404 from subhosting, fall back to the proxy path.
2151
+ *
2152
+ * @param slug The function slug to invoke
2153
+ * @param options Request options
2154
+ */
2155
+ async invoke(slug, options = {}) {
2156
+ const { method = "POST", body, headers = {} } = options;
2157
+ const dispatch = globalThis.__insforge_dispatch__;
2158
+ const localFunctionsUrl = _Functions.deriveSubhostingUrl(this.http.baseUrl);
2159
+ if (typeof dispatch === "function" && !!localFunctionsUrl && this.functionsUrl === localFunctionsUrl) {
2160
+ try {
2161
+ const req = this.buildInProcessRequest(slug, method, body, headers);
2162
+ const res = await dispatch(req);
2163
+ const data = await parseResponse(res);
2164
+ return { data, error: null };
2165
+ } catch (error) {
2166
+ if (error instanceof Error && error.name === "AbortError") throw error;
2167
+ return {
2168
+ data: null,
2169
+ error: error instanceof InsForgeError ? error : new InsForgeError(
2170
+ error instanceof Error ? error.message : "Function invocation failed",
2171
+ 500,
2172
+ "FUNCTION_ERROR"
2173
+ )
2174
+ };
2175
+ }
2176
+ }
2177
+ if (this.functionsUrl) {
2178
+ try {
2179
+ const data = await this.http.request(method, `${this.functionsUrl}/${slug}`, {
2180
+ body,
2181
+ headers
2182
+ });
2183
+ return { data, error: null };
2184
+ } catch (error) {
2185
+ if (error instanceof Error && error.name === "AbortError") throw error;
2186
+ if (error instanceof InsForgeError && error.statusCode === 404) {
2187
+ } else {
2188
+ return {
2189
+ data: null,
2190
+ error: error instanceof InsForgeError ? error : new InsForgeError(
2191
+ error instanceof Error ? error.message : "Function invocation failed",
2192
+ 500,
2193
+ "FUNCTION_ERROR"
2194
+ )
2195
+ };
2196
+ }
2197
+ }
2198
+ }
2199
+ try {
2200
+ const path = `/functions/${slug}`;
2201
+ const data = await this.http.request(method, path, { body, headers });
2202
+ return { data, error: null };
2203
+ } catch (error) {
2204
+ if (error instanceof Error && error.name === "AbortError") throw error;
2205
+ return {
2206
+ data: null,
2207
+ error: error instanceof InsForgeError ? error : new InsForgeError(
2208
+ error instanceof Error ? error.message : "Function invocation failed",
2209
+ 500,
2210
+ "FUNCTION_ERROR"
2211
+ )
2212
+ };
2213
+ }
2214
+ }
2215
+ };
2216
+
2217
+ // src/modules/realtime.ts
2218
+ var import_socket = require("socket.io-client");
2219
+ var CONNECT_TIMEOUT = 1e4;
2220
+ var Realtime = class {
2221
+ constructor(baseUrl, tokenManager, anonKey) {
2222
+ this.socket = null;
2223
+ this.connectPromise = null;
2224
+ this.subscribedChannels = /* @__PURE__ */ new Set();
2225
+ this.eventListeners = /* @__PURE__ */ new Map();
2226
+ this.baseUrl = baseUrl;
2227
+ this.tokenManager = tokenManager;
2228
+ this.anonKey = anonKey;
2229
+ this.tokenManager.onTokenChange = () => this.onTokenChange();
2230
+ }
2231
+ notifyListeners(event, payload) {
2232
+ const listeners = this.eventListeners.get(event);
2233
+ if (!listeners) return;
2234
+ for (const cb of listeners) {
2235
+ try {
2236
+ cb(payload);
2237
+ } catch (err) {
2238
+ console.error(`Error in ${event} callback:`, err);
2239
+ }
2240
+ }
2241
+ }
2242
+ /**
2243
+ * Connect to the realtime server
2244
+ * @returns Promise that resolves when connected
2245
+ */
2246
+ connect() {
2247
+ if (this.socket?.connected) {
2248
+ return Promise.resolve();
2249
+ }
2250
+ if (this.connectPromise) {
2251
+ return this.connectPromise;
2252
+ }
2253
+ this.connectPromise = new Promise((resolve, reject) => {
2254
+ const token = this.tokenManager.getAccessToken() ?? this.anonKey;
2255
+ this.socket = (0, import_socket.io)(this.baseUrl, {
2256
+ transports: ["websocket"],
2257
+ auth: token ? { token } : void 0
2258
+ });
2259
+ let initialConnection = true;
2260
+ let timeoutId = null;
2261
+ const cleanup = () => {
2262
+ if (timeoutId) {
2263
+ clearTimeout(timeoutId);
2264
+ timeoutId = null;
2265
+ }
2266
+ };
2267
+ timeoutId = setTimeout(() => {
2268
+ if (initialConnection) {
2269
+ initialConnection = false;
2270
+ this.connectPromise = null;
2271
+ this.socket?.disconnect();
2272
+ this.socket = null;
2273
+ reject(new Error(`Connection timeout after ${CONNECT_TIMEOUT}ms`));
2274
+ }
2275
+ }, CONNECT_TIMEOUT);
2276
+ this.socket.on("connect", () => {
2277
+ cleanup();
2278
+ for (const channel of this.subscribedChannels) {
2279
+ this.socket.emit("realtime:subscribe", { channel });
2280
+ }
2281
+ this.notifyListeners("connect");
2282
+ if (initialConnection) {
2283
+ initialConnection = false;
2284
+ this.connectPromise = null;
2285
+ resolve();
2286
+ }
2287
+ });
2288
+ this.socket.on("connect_error", (error) => {
2289
+ cleanup();
2290
+ this.notifyListeners("connect_error", error);
2291
+ if (initialConnection) {
2292
+ initialConnection = false;
2293
+ this.connectPromise = null;
2294
+ reject(error);
2295
+ }
2296
+ });
2297
+ this.socket.on("disconnect", (reason) => {
2298
+ this.notifyListeners("disconnect", reason);
2299
+ });
2300
+ this.socket.on("realtime:error", (error) => {
2301
+ this.notifyListeners("error", error);
2302
+ });
2303
+ this.socket.onAny((event, message) => {
2304
+ if (event === "realtime:error") return;
2305
+ this.notifyListeners(event, message);
2306
+ });
2307
+ });
2308
+ return this.connectPromise;
2309
+ }
2310
+ /**
2311
+ * Disconnect from the realtime server
2312
+ */
2313
+ disconnect() {
2314
+ if (this.socket) {
2315
+ this.socket.disconnect();
2316
+ this.socket = null;
2317
+ }
2318
+ this.subscribedChannels.clear();
2319
+ }
2320
+ /**
2321
+ * Handle token changes (e.g., after auth refresh)
2322
+ * Updates socket auth so reconnects use the new token
2323
+ * If connected, triggers reconnect to apply new token immediately
2324
+ */
2325
+ onTokenChange() {
2326
+ const token = this.tokenManager.getAccessToken() ?? this.anonKey;
2327
+ if (this.socket) {
2328
+ this.socket.auth = token ? { token } : {};
2329
+ }
2330
+ if (this.socket && (this.socket.connected || this.connectPromise)) {
2331
+ this.socket.disconnect();
2332
+ this.socket.connect();
2333
+ }
2334
+ }
2335
+ /**
2336
+ * Check if connected to the realtime server
2337
+ */
2338
+ get isConnected() {
2339
+ return this.socket?.connected ?? false;
2340
+ }
2341
+ /**
2342
+ * Get the current connection state
2343
+ */
2344
+ get connectionState() {
2345
+ if (!this.socket) return "disconnected";
2346
+ if (this.socket.connected) return "connected";
2347
+ return "connecting";
2348
+ }
2349
+ /**
2350
+ * Get the socket ID (if connected)
2351
+ */
2352
+ get socketId() {
2353
+ return this.socket?.id;
2354
+ }
2355
+ /**
2356
+ * Subscribe to a channel
2357
+ *
2358
+ * Automatically connects if not already connected.
2359
+ *
2360
+ * @param channel - Channel name (e.g., 'orders:123', 'broadcast')
2361
+ * @returns Promise with the subscription response
2362
+ */
2363
+ async subscribe(channel) {
2364
+ if (this.subscribedChannels.has(channel)) {
2365
+ return { ok: true, channel, presence: { members: [] } };
2366
+ }
2367
+ if (!this.socket?.connected) {
2368
+ try {
2369
+ await this.connect();
2370
+ } catch (error) {
2371
+ const message = error instanceof Error ? error.message : "Connection failed";
2372
+ return { ok: false, channel, error: { code: "CONNECTION_FAILED", message } };
2373
+ }
2374
+ }
2375
+ return new Promise((resolve) => {
2376
+ this.socket.emit("realtime:subscribe", { channel }, (response) => {
2377
+ if (response.ok) {
2378
+ this.subscribedChannels.add(channel);
2379
+ }
2380
+ resolve(response);
2381
+ });
2382
+ });
2383
+ }
2384
+ /**
2385
+ * Unsubscribe from a channel (fire-and-forget)
2386
+ *
2387
+ * @param channel - Channel name to unsubscribe from
2388
+ */
2389
+ unsubscribe(channel) {
2390
+ this.subscribedChannels.delete(channel);
2391
+ if (this.socket?.connected) {
2392
+ this.socket.emit("realtime:unsubscribe", { channel });
2393
+ }
2394
+ }
2395
+ /**
2396
+ * Publish a message to a channel
2397
+ *
2398
+ * @param channel - Channel name
2399
+ * @param event - Event name
2400
+ * @param payload - Message payload
2401
+ */
2402
+ async publish(channel, event, payload) {
2403
+ if (!this.socket?.connected) {
2404
+ throw new Error("Not connected to realtime server. Call connect() first.");
2405
+ }
2406
+ this.socket.emit("realtime:publish", { channel, event, payload });
2407
+ }
2408
+ /**
2409
+ * Listen for events
2410
+ *
2411
+ * Reserved event names:
2412
+ * - 'connect' - Fired when connected to the server
2413
+ * - 'connect_error' - Fired when connection fails (payload: Error)
2414
+ * - 'disconnect' - Fired when disconnected (payload: reason string)
2415
+ * - 'error' - Fired when a realtime error occurs (payload: RealtimeErrorPayload)
2416
+ *
2417
+ * All other events receive a `SocketMessage` payload with metadata.
2418
+ *
2419
+ * @param event - Event name to listen for
2420
+ * @param callback - Callback function when event is received
2421
+ */
2422
+ on(event, callback) {
2423
+ if (!this.eventListeners.has(event)) {
2424
+ this.eventListeners.set(event, /* @__PURE__ */ new Set());
2425
+ }
2426
+ this.eventListeners.get(event).add(callback);
2427
+ }
2428
+ /**
2429
+ * Remove a listener for a specific event
2430
+ *
2431
+ * @param event - Event name
2432
+ * @param callback - The callback function to remove
2433
+ */
2434
+ off(event, callback) {
2435
+ const listeners = this.eventListeners.get(event);
2436
+ if (listeners) {
2437
+ listeners.delete(callback);
2438
+ if (listeners.size === 0) {
2439
+ this.eventListeners.delete(event);
2440
+ }
2441
+ }
2442
+ }
2443
+ /**
2444
+ * Listen for an event only once, then automatically remove the listener
2445
+ *
2446
+ * @param event - Event name to listen for
2447
+ * @param callback - Callback function when event is received
2448
+ */
2449
+ once(event, callback) {
2450
+ const wrapper = (payload) => {
2451
+ this.off(event, wrapper);
2452
+ callback(payload);
2453
+ };
2454
+ this.on(event, wrapper);
2455
+ }
2456
+ /**
2457
+ * Get all currently subscribed channels
2458
+ *
2459
+ * @returns Array of channel names
2460
+ */
2461
+ getSubscribedChannels() {
2462
+ return Array.from(this.subscribedChannels);
2463
+ }
2464
+ };
2465
+
2466
+ // src/modules/email.ts
2467
+ var Emails = class {
2468
+ constructor(http) {
2469
+ this.http = http;
2470
+ }
2471
+ /**
2472
+ * Send a custom HTML email
2473
+ * @param options Email options including recipients, subject, and HTML content
2474
+ */
2475
+ async send(options) {
2476
+ try {
2477
+ const data = await this.http.post(
2478
+ "/api/email/send-raw",
2479
+ options
2480
+ );
2481
+ return { data, error: null };
2482
+ } catch (error) {
2483
+ if (error instanceof Error && error.name === "AbortError") throw error;
2484
+ return {
2485
+ data: null,
2486
+ error: error instanceof InsForgeError ? error : new InsForgeError(
2487
+ error instanceof Error ? error.message : "Email send failed",
2488
+ 500,
2489
+ "EMAIL_ERROR"
2490
+ )
2491
+ };
2492
+ }
2493
+ }
2494
+ };
2495
+
2496
+ // src/modules/payments.ts
2497
+ var Payments = class {
2498
+ constructor(http) {
2499
+ this.http = http;
2500
+ }
2501
+ /**
2502
+ * Create a Stripe Checkout Session through the InsForge backend.
2503
+ *
2504
+ * @example
2505
+ * ```typescript
2506
+ * const { data, error } = await client.payments.createCheckoutSession('test', {
2507
+ * mode: 'payment',
2508
+ * lineItems: [{ stripePriceId: 'price_123', quantity: 1 }],
2509
+ * successUrl: `${window.location.origin}/success`,
2510
+ * cancelUrl: `${window.location.origin}/pricing`
2511
+ * });
2512
+ *
2513
+ * if (!error && data.checkoutSession.url) {
2514
+ * window.location.assign(data.checkoutSession.url);
2515
+ * }
2516
+ * ```
2517
+ */
2518
+ async createCheckoutSession(environment, request) {
2519
+ try {
2520
+ const data = await this.http.post(
2521
+ `/api/payments/${encodeURIComponent(environment)}/checkout-sessions`,
2522
+ request,
2523
+ { idempotent: !!request.idempotencyKey }
2524
+ );
2525
+ return { data, error: null };
2526
+ } catch (error) {
2527
+ return wrapError(
2528
+ error,
2529
+ "Checkout session creation failed"
2530
+ );
2531
+ }
2532
+ }
2533
+ /**
2534
+ * Create a Stripe Billing Portal Session for a mapped billing subject.
2535
+ */
2536
+ async createCustomerPortalSession(environment, request) {
2537
+ try {
2538
+ const data = await this.http.post(
2539
+ `/api/payments/${encodeURIComponent(environment)}/customer-portal-sessions`,
2540
+ request
2541
+ );
2542
+ return { data, error: null };
2543
+ } catch (error) {
2544
+ return wrapError(
2545
+ error,
2546
+ "Customer portal session creation failed"
2547
+ );
2548
+ }
2549
+ }
2550
+ };
2551
+
2552
+ // src/client.ts
2553
+ var InsForgeClient = class {
2554
+ constructor(config = {}) {
2555
+ const logger = new Logger(config.debug);
2556
+ this.tokenManager = new TokenManager();
2557
+ this.http = new HttpClient(config, this.tokenManager, logger);
2558
+ if (config.edgeFunctionToken) {
2559
+ this.http.setAuthToken(config.edgeFunctionToken);
2560
+ this.tokenManager.setAccessToken(config.edgeFunctionToken);
2561
+ }
2562
+ this.auth = new Auth(this.http, this.tokenManager, {
2563
+ isServerMode: config.isServerMode ?? !!config.edgeFunctionToken
2564
+ });
2565
+ this.database = new Database(this.http);
2566
+ this.storage = new Storage(this.http);
2567
+ this.ai = new AI(this.http);
2568
+ this.functions = new Functions(this.http, config.functionsUrl);
2569
+ this.realtime = new Realtime(
2570
+ this.http.baseUrl,
2571
+ this.tokenManager,
2572
+ config.anonKey
2573
+ );
2574
+ this.emails = new Emails(this.http);
2575
+ this.payments = new Payments(this.http);
2576
+ }
2577
+ /**
2578
+ * Get the underlying HTTP client for custom requests
2579
+ *
2580
+ * @example
2581
+ * ```typescript
2582
+ * const httpClient = client.getHttpClient();
2583
+ * const customData = await httpClient.get('/api/custom-endpoint');
2584
+ * ```
2585
+ */
2586
+ getHttpClient() {
2587
+ return this.http;
2588
+ }
2589
+ /**
2590
+ * Set the access token used by every SDK surface. Updates both the HTTP
2591
+ * client (database / storage / functions / AI / emails) and the realtime
2592
+ * token manager (which fires `onTokenChange` to reconnect the WebSocket
2593
+ * with the new bearer). Pass `null` to clear.
2594
+ *
2595
+ * Use this when an external auth provider (Better Auth, Clerk, Auth0,
2596
+ * WorkOS, Kinde, Stytch, …) issues the JWT and you need to keep the
2597
+ * long-lived InsForge client in sync. Without this, you'd have to call
2598
+ * `client.getHttpClient().setAuthToken(token)` AND reach into the private
2599
+ * `client.realtime.tokenManager.setAccessToken(token)` separately —
2600
+ * forgetting the second one silently breaks realtime auth.
2601
+ *
2602
+ * @example
2603
+ * ```typescript
2604
+ * // Refresh a third-party-issued JWT periodically
2605
+ * const { token } = await fetch('/api/insforge-token').then((r) => r.json());
2606
+ * client.setAccessToken(token);
2607
+ *
2608
+ * // Sign-out
2609
+ * client.setAccessToken(null);
2610
+ * ```
2611
+ */
2612
+ setAccessToken(token) {
2613
+ this.http.setAuthToken(token);
2614
+ if (token === null) {
2615
+ this.tokenManager.clearSession();
2616
+ } else {
2617
+ this.tokenManager.setAccessToken(token);
2618
+ }
2619
+ }
2620
+ /**
2621
+ * Future modules will be added here:
2622
+ * - database: Database operations
2623
+ * - storage: File storage operations
2624
+ * - functions: Serverless functions
2625
+ * - tables: Table management
2626
+ * - metadata: Backend metadata
2627
+ */
2628
+ };
2629
+
2630
+ // src/lib/jwt.ts
2631
+ function decodeBase64Url(input) {
2632
+ const normalized = input.replace(/-/g, "+").replace(/_/g, "/");
2633
+ const padded = normalized.padEnd(
2634
+ normalized.length + (4 - normalized.length % 4) % 4,
2635
+ "="
2636
+ );
2637
+ if (typeof atob === "function") {
2638
+ return atob(padded);
2639
+ }
2640
+ return Buffer.from(padded, "base64").toString("utf8");
2641
+ }
2642
+ function getJwtExpiration(token) {
2643
+ if (!token) return null;
2644
+ const [, payload] = token.split(".");
2645
+ if (!payload) return null;
2646
+ try {
2647
+ const parsed = JSON.parse(decodeBase64Url(payload));
2648
+ if (typeof parsed.exp !== "number" || !Number.isFinite(parsed.exp)) {
2649
+ return null;
2650
+ }
2651
+ return new Date(parsed.exp * 1e3);
2652
+ } catch {
2653
+ return null;
2654
+ }
2655
+ }
2656
+ function isJwtExpiredOrExpiring(token, leewaySeconds = 60) {
2657
+ const expires = getJwtExpiration(token);
2658
+ if (!expires) return false;
2659
+ return expires.getTime() <= Date.now() + leewaySeconds * 1e3;
2660
+ }
2661
+
2662
+ // src/ssr/browser-client.ts
2663
+ var import_shared_schemas2 = require("@insforge/shared-schemas");
2664
+
2665
+ // src/ssr/cookies.ts
2666
+ var DEFAULT_ACCESS_TOKEN_COOKIE = "insforge_access_token";
2667
+ var DEFAULT_REFRESH_TOKEN_COOKIE = "insforge_refresh_token";
2668
+ var EXPIRED_DATE = /* @__PURE__ */ new Date(0);
2669
+ function getAccessTokenCookieName(names) {
2670
+ return names?.accessToken ?? DEFAULT_ACCESS_TOKEN_COOKIE;
2671
+ }
2672
+ function getRefreshTokenCookieName(names) {
2673
+ return names?.refreshToken ?? DEFAULT_REFRESH_TOKEN_COOKIE;
2674
+ }
2675
+ function getCookieValue(cookies, name) {
2676
+ if (!cookies) return null;
2677
+ const value = cookies.get(name);
2678
+ if (typeof value === "string") return value || null;
2679
+ if (value && typeof value.value === "string") return value.value || null;
2680
+ return null;
2681
+ }
2682
+ function getCookieValueFromHeader(cookieHeader, name) {
2683
+ if (!cookieHeader) return null;
2684
+ const parts = cookieHeader.split(";");
2685
+ for (const part of parts) {
2686
+ const [rawName, ...rawValue] = part.trim().split("=");
2687
+ if (rawName !== name) continue;
2688
+ try {
2689
+ return decodeURIComponent(rawValue.join("="));
2690
+ } catch {
2691
+ return rawValue.join("=");
2692
+ }
2693
+ }
2694
+ return null;
2695
+ }
2696
+ function getBrowserCookie(name) {
2697
+ if (typeof document === "undefined") return null;
2698
+ return getCookieValueFromHeader(document.cookie, name);
2699
+ }
2700
+ function defaultCookieOptions() {
2701
+ const secure = typeof process !== "undefined" ? process.env.NODE_ENV === "production" : typeof location !== "undefined" && location.protocol === "https:";
2702
+ return {
2703
+ path: "/",
2704
+ sameSite: "lax",
2705
+ secure
2706
+ };
2707
+ }
2708
+ function accessTokenCookieOptions(token, overrides) {
2709
+ return {
2710
+ ...defaultCookieOptions(),
2711
+ httpOnly: false,
2712
+ expires: getJwtExpiration(token) ?? void 0,
2713
+ ...overrides
2714
+ };
2715
+ }
2716
+ function refreshTokenCookieOptions(token, overrides) {
2717
+ return {
2718
+ ...defaultCookieOptions(),
2719
+ httpOnly: true,
2720
+ expires: getJwtExpiration(token) ?? void 0,
2721
+ ...overrides
2722
+ };
2723
+ }
2724
+ function expiredCookieOptions(overrides) {
2725
+ return {
2726
+ ...defaultCookieOptions(),
2727
+ expires: EXPIRED_DATE,
2728
+ maxAge: 0,
2729
+ ...overrides
2730
+ };
2731
+ }
2732
+ function setCookie(cookies, name, value, options) {
2733
+ if (!cookies?.set) return;
2734
+ cookies.set(name, value, options);
2735
+ }
2736
+ function deleteCookie(cookies, name, options) {
2737
+ if (!cookies) return;
2738
+ if (cookies.delete) {
2739
+ cookies.delete(name, options);
2740
+ return;
2741
+ }
2742
+ cookies.set?.(name, "", expiredCookieOptions(options));
2743
+ }
2744
+ function serializeCookie(name, value, options = {}) {
2745
+ const parts = [`${encodeURIComponent(name)}=${encodeURIComponent(value)}`];
2746
+ if (options.maxAge !== void 0) parts.push(`Max-Age=${options.maxAge}`);
2747
+ if (options.domain) parts.push(`Domain=${options.domain}`);
2748
+ if (options.path) parts.push(`Path=${options.path}`);
2749
+ if (options.expires) parts.push(`Expires=${options.expires.toUTCString()}`);
2750
+ if (options.httpOnly) parts.push("HttpOnly");
2751
+ if (options.secure) parts.push("Secure");
2752
+ if (options.sameSite) {
2753
+ const sameSite = options.sameSite.charAt(0).toUpperCase() + options.sameSite.slice(1);
2754
+ parts.push(`SameSite=${sameSite}`);
2755
+ }
2756
+ return parts.join("; ");
2757
+ }
2758
+ function appendSetCookie(headers, name, value, options) {
2759
+ headers.append("Set-Cookie", serializeCookie(name, value, options));
2760
+ }
2761
+ function setAuthCookies(target, tokens, settings = {}) {
2762
+ const accessName = getAccessTokenCookieName(settings.names);
2763
+ const refreshName = getRefreshTokenCookieName(settings.names);
2764
+ const accessOptions = accessTokenCookieOptions(
2765
+ tokens.accessToken,
2766
+ settings.options?.accessToken
2767
+ );
2768
+ if (target instanceof Headers) {
2769
+ appendSetCookie(target, accessName, tokens.accessToken, accessOptions);
2770
+ if (tokens.refreshToken) {
2771
+ appendSetCookie(
2772
+ target,
2773
+ refreshName,
2774
+ tokens.refreshToken,
2775
+ refreshTokenCookieOptions(
2776
+ tokens.refreshToken,
2777
+ settings.options?.refreshToken
2778
+ )
2779
+ );
2780
+ }
2781
+ return;
2782
+ }
2783
+ setCookie(target, accessName, tokens.accessToken, accessOptions);
2784
+ if (tokens.refreshToken) {
2785
+ setCookie(
2786
+ target,
2787
+ refreshName,
2788
+ tokens.refreshToken,
2789
+ refreshTokenCookieOptions(
2790
+ tokens.refreshToken,
2791
+ settings.options?.refreshToken
2792
+ )
2793
+ );
2794
+ }
2795
+ }
2796
+ function clearAuthCookies(target, settings = {}) {
2797
+ const accessName = getAccessTokenCookieName(settings.names);
2798
+ const refreshName = getRefreshTokenCookieName(settings.names);
2799
+ const accessOptions = expiredCookieOptions(settings.options?.accessToken);
2800
+ const refreshOptions = expiredCookieOptions(settings.options?.refreshToken);
2801
+ if (target instanceof Headers) {
2802
+ appendSetCookie(target, accessName, "", accessOptions);
2803
+ appendSetCookie(target, refreshName, "", refreshOptions);
2804
+ return;
2805
+ }
2806
+ deleteCookie(target, accessName, accessOptions);
2807
+ deleteCookie(target, refreshName, refreshOptions);
2808
+ }
2809
+
2810
+ // src/ssr/browser-client.ts
2811
+ async function parseRefreshResponse(response) {
2812
+ const contentType = response.headers.get("content-type");
2813
+ if (contentType?.includes("json")) {
2814
+ return await response.json();
2815
+ }
2816
+ return await response.text();
2817
+ }
2818
+ function toRefreshError(response, body) {
2819
+ if (body && typeof body === "object") {
2820
+ const errorBody = body;
2821
+ return new InsForgeError(
2822
+ typeof errorBody.message === "string" ? errorBody.message : "Failed to refresh auth session",
2823
+ typeof errorBody.statusCode === "number" ? errorBody.statusCode : response.status,
2824
+ typeof errorBody.error === "string" ? errorBody.error : import_shared_schemas2.ERROR_CODES.UNKNOWN_ERROR
2825
+ );
2826
+ }
2827
+ return new InsForgeError(
2828
+ typeof body === "string" && body ? body : "Failed to refresh auth session",
2829
+ response.status,
2830
+ import_shared_schemas2.ERROR_CODES.UNKNOWN_ERROR
2831
+ );
2832
+ }
2833
+ async function readErrorCode(response) {
2834
+ if (response.status !== 401) return null;
2835
+ try {
2836
+ const body = await response.clone().json();
2837
+ if (!body || typeof body !== "object") return null;
2838
+ const candidate = body.error ?? body.code;
2839
+ return typeof candidate === "string" ? candidate : null;
2840
+ } catch {
2841
+ return null;
2842
+ }
2843
+ }
2844
+ function isRefreshableErrorCode(code) {
2845
+ return code === import_shared_schemas2.ERROR_CODES.AUTH_UNAUTHORIZED || code === import_shared_schemas2.ERROR_CODES.AUTH_TOKEN_EXPIRED || code === "PGRST301";
2846
+ }
2847
+ function withAuthHeader(init, token) {
2848
+ const headers = new Headers(init?.headers);
2849
+ headers.set("Authorization", `Bearer ${token}`);
2850
+ return {
2851
+ ...init,
2852
+ headers
2853
+ };
2854
+ }
2855
+ function createBrowserClient(options = {}) {
2856
+ let { baseUrl, anonKey } = options;
2857
+ try {
2858
+ baseUrl || (baseUrl = process.env.NEXT_PUBLIC_INSFORGE_URL);
2859
+ anonKey || (anonKey = process.env.NEXT_PUBLIC_INSFORGE_ANON_KEY);
2860
+ } catch {
2861
+ }
2862
+ if (!baseUrl || !anonKey) {
2863
+ throw new Error(
2864
+ "Missing InsForge baseUrl or anonKey. Pass baseUrl and anonKey to createBrowserClient() or set NEXT_PUBLIC_INSFORGE_URL and NEXT_PUBLIC_INSFORGE_ANON_KEY."
2865
+ );
2866
+ }
2867
+ let accessToken = getBrowserCookie(
2868
+ getAccessTokenCookieName(options.names)
2869
+ );
2870
+ const refreshUrl = options.refreshUrl ?? "/api/auth/refresh";
2871
+ const fetchImpl = options.fetch ?? (globalThis.fetch ? globalThis.fetch.bind(globalThis) : void 0);
2872
+ let client;
2873
+ let sessionChecked = false;
2874
+ let refreshPromise = null;
2875
+ const refreshFromRoute = () => {
2876
+ if (refreshPromise) return refreshPromise;
2877
+ refreshPromise = (async () => {
2878
+ if (!fetchImpl) {
2879
+ throw new Error(
2880
+ "Fetch is not available. Please provide a fetch implementation."
2881
+ );
2882
+ }
2883
+ const response = await fetchImpl(refreshUrl, {
2884
+ method: "POST",
2885
+ credentials: "include",
2886
+ headers: { Accept: "application/json" }
2887
+ });
2888
+ const body = await parseRefreshResponse(response);
2889
+ if (!response.ok) {
2890
+ const error = toRefreshError(response, body);
2891
+ if (response.status === 401 && (error.error === import_shared_schemas2.ERROR_CODES.AUTH_UNAUTHORIZED || error.error === import_shared_schemas2.ERROR_CODES.AUTH_TOKEN_EXPIRED)) {
2892
+ accessToken = null;
2893
+ client?.setAccessToken(null);
2894
+ return null;
2895
+ }
2896
+ throw error;
2897
+ }
2898
+ if (!body || typeof body !== "object") return null;
2899
+ const refreshBody = body;
2900
+ if (!refreshBody.accessToken || !refreshBody.user) return null;
2901
+ accessToken = refreshBody.accessToken;
2902
+ client?.setAccessToken(refreshBody.accessToken);
2903
+ return refreshBody;
2904
+ })().finally(() => {
2905
+ sessionChecked = true;
2906
+ refreshPromise = null;
2907
+ });
2908
+ return refreshPromise;
2909
+ };
2910
+ const shouldSkipRefresh = (input) => {
2911
+ const url = typeof input === "string" ? input : input.toString();
2912
+ return url === refreshUrl || url.endsWith(refreshUrl);
2913
+ };
2914
+ const ssrFetch = async (input, init) => {
2915
+ if (!fetchImpl) {
2916
+ throw new Error(
2917
+ "Fetch is not available. Please provide a fetch implementation."
2918
+ );
2919
+ }
2920
+ if (shouldSkipRefresh(input)) {
2921
+ return fetchImpl(input, init);
2922
+ }
2923
+ let requestInit = init;
2924
+ if (!accessToken && !sessionChecked || isJwtExpiredOrExpiring(accessToken, options.refreshLeewaySeconds)) {
2925
+ const refreshed2 = await refreshFromRoute().catch(() => null);
2926
+ if (refreshed2?.accessToken) {
2927
+ requestInit = withAuthHeader(init, refreshed2.accessToken);
2928
+ }
2929
+ }
2930
+ const response = await fetchImpl(input, requestInit);
2931
+ const errorCode = await readErrorCode(response);
2932
+ if (!isRefreshableErrorCode(errorCode)) {
2933
+ return response;
2934
+ }
2935
+ const refreshed = await refreshFromRoute();
2936
+ if (!refreshed?.accessToken) {
2937
+ client.setAccessToken(null);
2938
+ return response;
2939
+ }
2940
+ return fetchImpl(input, withAuthHeader(init, refreshed.accessToken));
2941
+ };
2942
+ client = new InsForgeClient({
2943
+ ...options,
2944
+ baseUrl,
2945
+ anonKey,
2946
+ fetch: ssrFetch
2947
+ });
2948
+ const setAccessToken = client.setAccessToken.bind(client);
2949
+ client.setAccessToken = (token) => {
2950
+ accessToken = token;
2951
+ setAccessToken(token);
2952
+ };
2953
+ if (accessToken) {
2954
+ client.setAccessToken(accessToken);
2955
+ }
2956
+ if (!accessToken || isJwtExpiredOrExpiring(accessToken, options.refreshLeewaySeconds)) {
2957
+ void refreshFromRoute().catch(() => void 0);
2958
+ }
2959
+ return client;
2960
+ }
2961
+
2962
+ // src/ssr/server-client.ts
2963
+ function createServerClient(options = {}) {
2964
+ let { baseUrl, anonKey } = options;
2965
+ try {
2966
+ baseUrl || (baseUrl = process.env.NEXT_PUBLIC_INSFORGE_URL);
2967
+ anonKey || (anonKey = process.env.NEXT_PUBLIC_INSFORGE_ANON_KEY);
2968
+ } catch {
2969
+ }
2970
+ if (!baseUrl || !anonKey) {
2971
+ throw new Error(
2972
+ "Missing InsForge baseUrl or anonKey. Pass baseUrl and anonKey to createServerClient() or set NEXT_PUBLIC_INSFORGE_URL and NEXT_PUBLIC_INSFORGE_ANON_KEY."
2973
+ );
2974
+ }
2975
+ const accessToken = options.accessToken ?? getCookieValue(
2976
+ options.cookies,
2977
+ getAccessTokenCookieName(options.names)
2978
+ );
2979
+ return new InsForgeClient({
2980
+ ...options,
2981
+ baseUrl,
2982
+ anonKey,
2983
+ isServerMode: true,
2984
+ edgeFunctionToken: accessToken ?? void 0
2985
+ });
2986
+ }
2987
+
2988
+ // src/ssr/refresh.ts
2989
+ var import_shared_schemas3 = require("@insforge/shared-schemas");
2990
+ function jsonResponse(body, init = {}, headers = new Headers(init.headers)) {
2991
+ headers.set("Content-Type", "application/json");
2992
+ return new Response(JSON.stringify(body), {
2993
+ ...init,
2994
+ headers
2995
+ });
2996
+ }
2997
+ function normalizeError(error) {
2998
+ if (error instanceof InsForgeError) return error;
2999
+ if (error && typeof error === "object") {
3000
+ const body = error;
3001
+ return new InsForgeError(
3002
+ typeof body.message === "string" ? body.message : "Failed to refresh auth session",
3003
+ typeof body.statusCode === "number" ? body.statusCode : 500,
3004
+ typeof body.error === "string" ? body.error : import_shared_schemas3.ERROR_CODES.UNKNOWN_ERROR
3005
+ );
3006
+ }
3007
+ return new InsForgeError(
3008
+ error instanceof Error ? error.message : "Failed to refresh auth session",
3009
+ 500,
3010
+ import_shared_schemas3.ERROR_CODES.UNKNOWN_ERROR
3011
+ );
3012
+ }
3013
+ async function readJson(response) {
3014
+ const contentType = response.headers.get("content-type");
3015
+ if (!contentType?.includes("json")) return null;
3016
+ return response.json();
3017
+ }
3018
+ function readRefreshToken(options) {
3019
+ if (options.refreshToken) return options.refreshToken;
3020
+ const refreshCookieName = getRefreshTokenCookieName(options.names);
3021
+ const cookieValue = getCookieValue(options.cookies, refreshCookieName);
3022
+ if (cookieValue) return cookieValue;
3023
+ return getCookieValueFromHeader(
3024
+ options.request?.headers.get("cookie"),
3025
+ refreshCookieName
3026
+ );
3027
+ }
3028
+ async function refreshAuth(options = {}) {
3029
+ const headers = new Headers();
3030
+ const refreshToken = readRefreshToken(options);
3031
+ if (!refreshToken) {
3032
+ clearAuthCookies(headers, options);
3033
+ const error2 = new InsForgeError(
3034
+ "Refresh token cookie is missing",
3035
+ 401,
3036
+ import_shared_schemas3.ERROR_CODES.AUTH_UNAUTHORIZED
3037
+ );
3038
+ return {
3039
+ response: jsonResponse(
3040
+ {
3041
+ error: error2.error,
3042
+ message: error2.message,
3043
+ statusCode: error2.statusCode
3044
+ },
3045
+ { status: error2.statusCode },
3046
+ headers
3047
+ ),
3048
+ data: null,
3049
+ accessToken: null,
3050
+ refreshToken: null,
3051
+ error: error2
3052
+ };
3053
+ }
3054
+ let { baseUrl, anonKey } = options;
3055
+ try {
3056
+ baseUrl || (baseUrl = process.env.NEXT_PUBLIC_INSFORGE_URL);
3057
+ anonKey || (anonKey = process.env.NEXT_PUBLIC_INSFORGE_ANON_KEY);
3058
+ } catch {
3059
+ }
3060
+ if (!baseUrl || !anonKey) {
3061
+ throw new Error(
3062
+ "Missing InsForge baseUrl or anonKey. Pass baseUrl and anonKey to refreshAuth() or set NEXT_PUBLIC_INSFORGE_URL and NEXT_PUBLIC_INSFORGE_ANON_KEY."
3063
+ );
3064
+ }
3065
+ const fetchImpl = options.fetch ?? (globalThis.fetch ? globalThis.fetch.bind(globalThis) : void 0);
3066
+ if (!fetchImpl) {
3067
+ throw new Error(
3068
+ "Fetch is not available. Please provide a fetch implementation."
3069
+ );
3070
+ }
3071
+ const requestHeaders = new Headers(options.headers);
3072
+ requestHeaders.set("Authorization", `Bearer ${anonKey}`);
3073
+ requestHeaders.set("Content-Type", "application/json");
3074
+ requestHeaders.set("Accept", "application/json");
3075
+ let data = null;
3076
+ let error = null;
3077
+ try {
3078
+ const response = await fetchImpl(
3079
+ new URL("/api/auth/refresh?client_type=mobile", baseUrl).toString(),
3080
+ {
3081
+ method: "POST",
3082
+ headers: requestHeaders,
3083
+ body: JSON.stringify({ refresh_token: refreshToken })
3084
+ }
3085
+ );
3086
+ const body = await readJson(response);
3087
+ if (!response.ok) {
3088
+ error = normalizeError(
3089
+ body ?? {
3090
+ message: "Failed to refresh auth session",
3091
+ statusCode: response.status,
3092
+ error: import_shared_schemas3.ERROR_CODES.UNKNOWN_ERROR
3093
+ }
3094
+ );
3095
+ } else {
3096
+ data = body;
3097
+ }
3098
+ } catch (caught) {
3099
+ error = normalizeError(caught);
3100
+ }
3101
+ if (error || !data?.accessToken) {
3102
+ clearAuthCookies(headers, options);
3103
+ const normalized = normalizeError(error);
3104
+ return {
3105
+ response: jsonResponse(
3106
+ {
3107
+ error: normalized.error,
3108
+ message: normalized.message,
3109
+ statusCode: normalized.statusCode
3110
+ },
3111
+ { status: normalized.statusCode || 500 },
3112
+ headers
3113
+ ),
3114
+ data: null,
3115
+ accessToken: null,
3116
+ refreshToken: null,
3117
+ error: normalized
3118
+ };
3119
+ }
3120
+ const nextRefreshToken = data.refreshToken ?? refreshToken;
3121
+ setAuthCookies(
3122
+ headers,
3123
+ {
3124
+ accessToken: data.accessToken,
3125
+ refreshToken: nextRefreshToken
3126
+ },
3127
+ options
3128
+ );
3129
+ const responseBody = {
3130
+ accessToken: data.accessToken,
3131
+ user: data.user,
3132
+ csrfToken: data.csrfToken
3133
+ };
3134
+ return {
3135
+ response: jsonResponse(responseBody, { status: 200 }, headers),
3136
+ data: responseBody,
3137
+ accessToken: data.accessToken,
3138
+ refreshToken: nextRefreshToken,
3139
+ error: null
3140
+ };
3141
+ }
3142
+ function createRefreshAuthRouter(options = {}) {
3143
+ return {
3144
+ POST: async (request) => (await refreshAuth({ ...options, request })).response
3145
+ };
3146
+ }
3147
+
3148
+ // src/ssr/update-session.ts
3149
+ async function updateSession(options) {
3150
+ const accessCookieName = getAccessTokenCookieName(options.names);
3151
+ const refreshCookieName = getRefreshTokenCookieName(options.names);
3152
+ const accessToken = getCookieValue(
3153
+ options.requestCookies,
3154
+ accessCookieName
3155
+ );
3156
+ if (accessToken && !isJwtExpiredOrExpiring(accessToken, options.refreshLeewaySeconds)) {
3157
+ return {
3158
+ refreshed: false,
3159
+ accessToken,
3160
+ error: null
3161
+ };
3162
+ }
3163
+ const refreshToken = getCookieValue(
3164
+ options.requestCookies,
3165
+ refreshCookieName
3166
+ );
3167
+ if (!refreshToken) {
3168
+ if (accessToken) {
3169
+ clearAuthCookies(options.requestCookies, options);
3170
+ clearAuthCookies(options.responseCookies, options);
3171
+ }
3172
+ return {
3173
+ refreshed: false,
3174
+ accessToken: null,
3175
+ error: null
3176
+ };
3177
+ }
3178
+ const result = await refreshAuth({
3179
+ ...options,
3180
+ refreshToken
3181
+ });
3182
+ if (result.error || !result.accessToken) {
3183
+ clearAuthCookies(options.requestCookies, options);
3184
+ clearAuthCookies(options.responseCookies, options);
3185
+ return {
3186
+ refreshed: false,
3187
+ accessToken: null,
3188
+ error: result.error
3189
+ };
3190
+ }
3191
+ const tokens = {
3192
+ accessToken: result.accessToken,
3193
+ refreshToken: result.refreshToken ?? refreshToken
3194
+ };
3195
+ setAuthCookies(options.requestCookies, tokens, options);
3196
+ setAuthCookies(options.responseCookies, tokens, options);
3197
+ return {
3198
+ refreshed: true,
3199
+ accessToken: result.accessToken,
3200
+ error: null
3201
+ };
3202
+ }
3203
+ // Annotate the CommonJS export names for ESM import in node:
3204
+ 0 && (module.exports = {
3205
+ DEFAULT_ACCESS_TOKEN_COOKIE,
3206
+ DEFAULT_REFRESH_TOKEN_COOKIE,
3207
+ accessTokenCookieOptions,
3208
+ clearAuthCookies,
3209
+ createBrowserClient,
3210
+ createRefreshAuthRouter,
3211
+ createServerClient,
3212
+ getAccessTokenCookieName,
3213
+ getRefreshTokenCookieName,
3214
+ refreshAuth,
3215
+ refreshTokenCookieOptions,
3216
+ setAuthCookies,
3217
+ updateSession
3218
+ });
3219
+ //# sourceMappingURL=ssr.js.map