@resultdev/sdk 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,20 +1,17 @@
1
- import {
2
- ERROR_CODES,
3
- oAuthProvidersSchema
4
- } from "./chunk-Y7VCTXPN.js";
5
-
6
- // ../../node_modules/.bun/@insforge+sdk@1.4.4/node_modules/@insforge/sdk/dist/index.mjs
7
- import { PostgrestClient } from "@supabase/postgrest-js";
8
- var InsForgeError = class _InsForgeError extends Error {
1
+ // src/types.ts
2
+ var ResultError = class _ResultError extends Error {
3
+ statusCode;
4
+ error;
5
+ nextActions;
9
6
  constructor(message, statusCode, error, nextActions) {
10
7
  super(message);
11
- this.name = "InsForgeError";
8
+ this.name = "ResultError";
12
9
  this.statusCode = statusCode;
13
10
  this.error = error;
14
11
  this.nextActions = nextActions;
15
12
  }
16
13
  static fromApiError(apiError) {
17
- return new _InsForgeError(
14
+ return new _ResultError(
18
15
  apiError.message,
19
16
  apiError.statusCode,
20
17
  apiError.error,
@@ -22,7 +19,54 @@ var InsForgeError = class _InsForgeError extends Error {
22
19
  );
23
20
  }
24
21
  };
25
- var SENSITIVE_HEADERS = ["authorization", "x-api-key", "cookie", "set-cookie"];
22
+
23
+ // src/lib/jwt.ts
24
+ function decodeBase64Url(input) {
25
+ const normalized = input.replace(/-/g, "+").replace(/_/g, "/");
26
+ const padded = normalized.padEnd(
27
+ normalized.length + (4 - normalized.length % 4) % 4,
28
+ "="
29
+ );
30
+ const binary = atob(padded);
31
+ const bytes2 = Uint8Array.from(binary, (char) => char.charCodeAt(0));
32
+ return new TextDecoder().decode(bytes2);
33
+ }
34
+ function getJwtExpiration(token) {
35
+ if (!token) {
36
+ return null;
37
+ }
38
+ const [, payload] = token.split(".");
39
+ if (!payload) {
40
+ return null;
41
+ }
42
+ try {
43
+ const parsed = JSON.parse(decodeBase64Url(payload));
44
+ if (typeof parsed.exp !== "number" || !Number.isFinite(parsed.exp)) {
45
+ return null;
46
+ }
47
+ return new Date(parsed.exp * 1e3);
48
+ } catch {
49
+ return null;
50
+ }
51
+ }
52
+ function isJwtExpiredOrExpiring(token, leewaySeconds = 60) {
53
+ if (!token) {
54
+ return false;
55
+ }
56
+ const expires = getJwtExpiration(token);
57
+ if (!expires) {
58
+ return true;
59
+ }
60
+ return expires.getTime() <= Date.now() + leewaySeconds * 1e3;
61
+ }
62
+
63
+ // src/lib/logger.ts
64
+ var SENSITIVE_HEADERS = [
65
+ "authorization",
66
+ "x-api-key",
67
+ "cookie",
68
+ "set-cookie"
69
+ ];
26
70
  var SENSITIVE_BODY_KEYS = [
27
71
  "password",
28
72
  "token",
@@ -97,6 +141,9 @@ function formatBody(body) {
97
141
  }
98
142
  }
99
143
  var Logger = class {
144
+ /** Whether debug logging is currently enabled */
145
+ enabled;
146
+ customLog;
100
147
  /**
101
148
  * Creates a new Logger instance.
102
149
  * @param debug - Set to true to enable console logging, or pass a custom log function
@@ -119,7 +166,7 @@ var Logger = class {
119
166
  if (!this.enabled) {
120
167
  return;
121
168
  }
122
- const formatted = `[InsForge Debug] ${message}`;
169
+ const formatted = `[Result Debug] ${message}`;
123
170
  if (this.customLog) {
124
171
  this.customLog(formatted, ...args);
125
172
  } else {
@@ -135,7 +182,7 @@ var Logger = class {
135
182
  if (!this.enabled) {
136
183
  return;
137
184
  }
138
- const formatted = `[InsForge Debug] ${message}`;
185
+ const formatted = `[Result Debug] ${message}`;
139
186
  if (this.customLog) {
140
187
  this.customLog(formatted, ...args);
141
188
  } else {
@@ -151,7 +198,7 @@ var Logger = class {
151
198
  if (!this.enabled) {
152
199
  return;
153
200
  }
154
- const formatted = `[InsForge Debug] ${message}`;
201
+ const formatted = `[Result Debug] ${message}`;
155
202
  if (this.customLog) {
156
203
  this.customLog(formatted, ...args);
157
204
  } else {
@@ -176,7 +223,7 @@ var Logger = class {
176
223
  }
177
224
  const formattedBody = formatBody(sanitizeBody(body));
178
225
  if (formattedBody) {
179
- const truncated = formattedBody.length > 1e3 ? formattedBody.slice(0, 1e3) + "... [truncated]" : formattedBody;
226
+ const truncated = formattedBody.length > 1e3 ? `${formattedBody.slice(0, 1e3)}... [truncated]` : formattedBody;
180
227
  parts.push(` Body: ${truncated}`);
181
228
  }
182
229
  this.log(parts.join("\n"));
@@ -197,7 +244,7 @@ var Logger = class {
197
244
  const parts = [`\u2190 ${method} ${url} ${status} (${durationMs}ms)`];
198
245
  const formattedBody = formatBody(sanitizeBody(body));
199
246
  if (formattedBody) {
200
- const truncated = formattedBody.length > 1e3 ? formattedBody.slice(0, 1e3) + "... [truncated]" : formattedBody;
247
+ const truncated = formattedBody.length > 1e3 ? `${formattedBody.slice(0, 1e3)}... [truncated]` : formattedBody;
201
248
  parts.push(` Body: ${truncated}`);
202
249
  }
203
250
  if (status >= 400) {
@@ -207,12 +254,14 @@ var Logger = class {
207
254
  }
208
255
  }
209
256
  };
257
+
258
+ // src/lib/token-manager.ts
210
259
  var AuthChangeEvent = {
211
260
  SIGNED_IN: "signedIn",
212
261
  SIGNED_OUT: "signedOut",
213
262
  TOKEN_REFRESHED: "tokenRefreshed"
214
263
  };
215
- var CSRF_TOKEN_COOKIE = "insforge_csrf_token";
264
+ var CSRF_TOKEN_COOKIE = "result_csrf_token";
216
265
  function getCsrfToken() {
217
266
  if (typeof document === "undefined") {
218
267
  return null;
@@ -239,11 +288,10 @@ function clearCsrfToken() {
239
288
  document.cookie = `${CSRF_TOKEN_COOKIE}=; path=/; max-age=0; SameSite=Lax${secure}`;
240
289
  }
241
290
  var TokenManager = class {
242
- constructor() {
243
- this.accessToken = null;
244
- this.user = null;
245
- this.authStateChangeCallbacks = /* @__PURE__ */ new Map();
246
- }
291
+ // In-memory storage
292
+ accessToken = null;
293
+ user = null;
294
+ authStateChangeCallbacks = /* @__PURE__ */ new Map();
247
295
  /**
248
296
  * Save session in memory
249
297
  */
@@ -321,41 +369,8 @@ var TokenManager = class {
321
369
  }
322
370
  }
323
371
  };
324
- function decodeBase64Url(input) {
325
- const normalized = input.replace(/-/g, "+").replace(/_/g, "/");
326
- const padded = normalized.padEnd(normalized.length + (4 - normalized.length % 4) % 4, "=");
327
- const binary = atob(padded);
328
- const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
329
- return new TextDecoder().decode(bytes);
330
- }
331
- function getJwtExpiration(token) {
332
- if (!token) {
333
- return null;
334
- }
335
- const [, payload] = token.split(".");
336
- if (!payload) {
337
- return null;
338
- }
339
- try {
340
- const parsed = JSON.parse(decodeBase64Url(payload));
341
- if (typeof parsed.exp !== "number" || !Number.isFinite(parsed.exp)) {
342
- return null;
343
- }
344
- return new Date(parsed.exp * 1e3);
345
- } catch {
346
- return null;
347
- }
348
- }
349
- function isJwtExpiredOrExpiring(token, leewaySeconds = 60) {
350
- if (!token) {
351
- return false;
352
- }
353
- const expires = getJwtExpiration(token);
354
- if (!expires) {
355
- return true;
356
- }
357
- return expires.getTime() <= Date.now() + leewaySeconds * 1e3;
358
- }
372
+
373
+ // src/lib/http-client.ts
359
374
  var RETRYABLE_STATUS_CODES = /* @__PURE__ */ new Set([500, 502, 503, 504]);
360
375
  var IDEMPOTENT_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
361
376
  var REFRESHABLE_AUTH_ERROR_CODES = /* @__PURE__ */ new Set(["AUTH_UNAUTHORIZED", "PGRST301"]);
@@ -385,7 +400,7 @@ async function parseResponse(response) {
385
400
  data = await response.text();
386
401
  }
387
402
  } catch (parseErr) {
388
- throw new InsForgeError(
403
+ throw new ResultError(
389
404
  `Failed to parse response body: ${parseErr?.message || "Unknown error"}`,
390
405
  response.status,
391
406
  response.ok ? "PARSE_ERROR" : "REQUEST_FAILED"
@@ -393,8 +408,8 @@ async function parseResponse(response) {
393
408
  }
394
409
  if (!response.ok) {
395
410
  if (data && typeof data === "object" && "error" in data) {
396
- data.statusCode ?? (data.statusCode = data.status ?? response.status);
397
- const error = InsForgeError.fromApiError(data);
411
+ data.statusCode ??= data.status ?? response.status;
412
+ const error = ResultError.fromApiError(data);
398
413
  Object.keys(data).forEach((key) => {
399
414
  if (key !== "error" && key !== "message" && key !== "statusCode") {
400
415
  error[key] = data[key];
@@ -402,7 +417,7 @@ async function parseResponse(response) {
402
417
  });
403
418
  throw error;
404
419
  }
405
- throw new InsForgeError(
420
+ throw new ResultError(
406
421
  `Request failed: ${response.statusText}`,
407
422
  response.status,
408
423
  "REQUEST_FAILED"
@@ -411,6 +426,20 @@ async function parseResponse(response) {
411
426
  return data;
412
427
  }
413
428
  var HttpClient = class {
429
+ baseUrl;
430
+ fetch;
431
+ config;
432
+ defaultHeaders;
433
+ anonKey;
434
+ userToken = null;
435
+ logger;
436
+ isRefreshing = false;
437
+ refreshPromise = null;
438
+ tokenManager;
439
+ refreshToken = null;
440
+ timeout;
441
+ retryCount;
442
+ retryDelay;
414
443
  /**
415
444
  * Creates a new HttpClient instance.
416
445
  * @param config - SDK configuration including baseUrl, timeout, retry settings, and fetch implementation.
@@ -418,10 +447,6 @@ var HttpClient = class {
418
447
  * @param logger - Optional logger instance for request/response debugging.
419
448
  */
420
449
  constructor(config, tokenManager, logger) {
421
- this.userToken = null;
422
- this.isRefreshing = false;
423
- this.refreshPromise = null;
424
- this.refreshToken = null;
425
450
  this.config = config;
426
451
  this.baseUrl = config.baseUrl || "http://localhost:7130";
427
452
  this.fetch = config.fetch || (globalThis.fetch ? globalThis.fetch.bind(globalThis) : void 0);
@@ -469,7 +494,7 @@ var HttpClient = class {
469
494
  * @returns Delay in milliseconds.
470
495
  */
471
496
  computeRetryDelay(attempt) {
472
- const base = this.retryDelay * Math.pow(2, attempt - 1);
497
+ const base = this.retryDelay * 2 ** (attempt - 1);
473
498
  const jitter = base * (0.85 + Math.random() * 0.3);
474
499
  return Math.round(jitter);
475
500
  }
@@ -477,19 +502,29 @@ var HttpClient = class {
477
502
  return statusCode === 401 && REFRESHABLE_AUTH_ERROR_CODES.has(errorCode ?? "") && !this.config.isServerMode && !this.config.accessToken && !this.config.edgeFunctionToken && !options.skipAuthRefresh && authToken !== null;
478
503
  }
479
504
  async fetchWithRetry(args) {
480
- const { method, url, headers, body, fetchOptions, callerSignal, maxAttempts } = args;
505
+ const {
506
+ method,
507
+ url,
508
+ headers,
509
+ body,
510
+ fetchOptions,
511
+ callerSignal,
512
+ maxAttempts
513
+ } = args;
481
514
  let lastError;
482
515
  for (let attempt = 0; attempt <= maxAttempts; attempt++) {
483
516
  if (attempt > 0) {
484
517
  const delay = this.computeRetryDelay(attempt);
485
- this.logger.warn(`Retry ${attempt}/${maxAttempts} for ${method} ${url} in ${delay}ms`);
518
+ this.logger.warn(
519
+ `Retry ${attempt}/${maxAttempts} for ${method} ${url} in ${delay}ms`
520
+ );
486
521
  if (callerSignal?.aborted) {
487
522
  throw callerSignal.reason;
488
523
  }
489
524
  await new Promise((resolve, reject) => {
490
525
  const onAbort = () => {
491
526
  clearTimeout(timer2);
492
- reject(callerSignal.reason);
527
+ reject(callerSignal?.reason);
493
528
  };
494
529
  const timer2 = setTimeout(() => {
495
530
  if (callerSignal) {
@@ -507,13 +542,13 @@ var HttpClient = class {
507
542
  if (this.timeout > 0 || callerSignal) {
508
543
  controller = new AbortController();
509
544
  if (this.timeout > 0) {
510
- timer = setTimeout(() => controller.abort(), this.timeout);
545
+ timer = setTimeout(() => controller?.abort(), this.timeout);
511
546
  }
512
547
  if (callerSignal) {
513
548
  if (callerSignal.aborted) {
514
549
  controller.abort(callerSignal.reason);
515
550
  } else {
516
- const onCallerAbort = () => controller.abort(callerSignal.reason);
551
+ const onCallerAbort = () => controller?.abort(callerSignal.reason);
517
552
  callerSignal.addEventListener("abort", onCallerAbort, {
518
553
  once: true
519
554
  });
@@ -540,7 +575,7 @@ var HttpClient = class {
540
575
  clearTimeout(timer);
541
576
  }
542
577
  await response.body?.cancel();
543
- lastError = new InsForgeError(
578
+ lastError = new ResultError(
544
579
  `Server error: ${response.status} ${response.statusText}`,
545
580
  response.status,
546
581
  "SERVER_ERROR"
@@ -556,8 +591,8 @@ var HttpClient = class {
556
591
  clearTimeout(timer);
557
592
  }
558
593
  if (err?.name === "AbortError") {
559
- if (controller && controller.signal.aborted && this.timeout > 0 && !callerSignal?.aborted) {
560
- throw new InsForgeError(
594
+ if (controller?.signal.aborted && this.timeout > 0 && !callerSignal?.aborted) {
595
+ throw new ResultError(
561
596
  `Request timed out after ${this.timeout}ms`,
562
597
  408,
563
598
  "REQUEST_TIMEOUT"
@@ -569,14 +604,18 @@ var HttpClient = class {
569
604
  lastError = err;
570
605
  continue;
571
606
  }
572
- throw new InsForgeError(
607
+ throw new ResultError(
573
608
  `Network request failed: ${err?.message || "Unknown error"}`,
574
609
  0,
575
610
  "NETWORK_ERROR"
576
611
  );
577
612
  }
578
613
  }
579
- throw lastError || new InsForgeError("Request failed after all retry attempts", 0, "NETWORK_ERROR");
614
+ throw lastError || new ResultError(
615
+ "Request failed after all retry attempts",
616
+ 0,
617
+ "NETWORK_ERROR"
618
+ );
580
619
  }
581
620
  /**
582
621
  * Performs an HTTP request with automatic retry and timeout handling.
@@ -586,7 +625,7 @@ var HttpClient = class {
586
625
  * @param path - API path relative to the base URL.
587
626
  * @param options - Optional request configuration including headers, body, and query params.
588
627
  * @returns Parsed response data.
589
- * @throws {InsForgeError} On timeout, network failure, or HTTP error responses.
628
+ * @throws {ResultError} On timeout, network failure, or HTTP error responses.
590
629
  */
591
630
  async handleRequest(method, path, options = {}, tokenOverride) {
592
631
  const {
@@ -606,14 +645,14 @@ var HttpClient = class {
606
645
  };
607
646
  const authToken = tokenOverride ?? this.userToken ?? this.anonKey;
608
647
  if (authToken) {
609
- requestHeaders["Authorization"] = `Bearer ${authToken}`;
648
+ requestHeaders.Authorization = `Bearer ${authToken}`;
610
649
  }
611
650
  const processedBody = serializeBody(method, body, requestHeaders);
612
651
  const setRequestHeader = (key, value) => {
613
652
  if (key.toLowerCase() === "authorization") {
614
- delete requestHeaders["Authorization"];
615
- delete requestHeaders["authorization"];
616
- requestHeaders["Authorization"] = value;
653
+ delete requestHeaders.Authorization;
654
+ delete requestHeaders.authorization;
655
+ requestHeaders.Authorization = value;
617
656
  return;
618
657
  }
619
658
  requestHeaders[key] = value;
@@ -645,7 +684,7 @@ var HttpClient = class {
645
684
  try {
646
685
  data = await parseResponse(response);
647
686
  } catch (err) {
648
- if (err instanceof InsForgeError) {
687
+ if (err instanceof ResultError) {
649
688
  this.logger.logResponse(
650
689
  method,
651
690
  url,
@@ -656,15 +695,31 @@ var HttpClient = class {
656
695
  }
657
696
  throw err;
658
697
  }
659
- this.logger.logResponse(method, url, response.status, Date.now() - startTime, data);
698
+ this.logger.logResponse(
699
+ method,
700
+ url,
701
+ response.status,
702
+ Date.now() - startTime,
703
+ data
704
+ );
660
705
  return data;
661
706
  }
662
707
  async request(method, path, options = {}) {
663
708
  const tokenUsed = this.userToken;
664
709
  try {
665
- return await this.handleRequest(method, path, { ...options }, tokenUsed);
710
+ return await this.handleRequest(
711
+ method,
712
+ path,
713
+ { ...options },
714
+ tokenUsed
715
+ );
666
716
  } catch (error) {
667
- if (!(error instanceof InsForgeError) || !this.shouldRefreshAccessToken(error.statusCode, error.error, tokenUsed, options)) {
717
+ if (!(error instanceof ResultError) || !this.shouldRefreshAccessToken(
718
+ error.statusCode,
719
+ error.error,
720
+ tokenUsed,
721
+ options
722
+ )) {
668
723
  throw error;
669
724
  }
670
725
  if (tokenUsed !== this.userToken) {
@@ -684,7 +739,7 @@ var HttpClient = class {
684
739
  try {
685
740
  await this.refreshAndSaveSession();
686
741
  } catch (error2) {
687
- if (error2 instanceof InsForgeError && (error2.statusCode === 401 || error2.statusCode === 403)) {
742
+ if (error2 instanceof ResultError && (error2.statusCode === 401 || error2.statusCode === 403)) {
688
743
  this.clearAuthSession();
689
744
  }
690
745
  throw error2;
@@ -749,7 +804,12 @@ var HttpClient = class {
749
804
  callerSignal,
750
805
  maxAttempts
751
806
  });
752
- this.logger.logResponse(method, url, response.status, Date.now() - startTime);
807
+ this.logger.logResponse(
808
+ method,
809
+ url,
810
+ response.status,
811
+ Date.now() - startTime
812
+ );
753
813
  let errorCode = null;
754
814
  if (response.status === 401) {
755
815
  try {
@@ -763,7 +823,12 @@ var HttpClient = class {
763
823
  } catch {
764
824
  }
765
825
  }
766
- if (!this.shouldRefreshAccessToken(response.status, errorCode, tokenUsed, options)) {
826
+ if (!this.shouldRefreshAccessToken(
827
+ response.status,
828
+ errorCode,
829
+ tokenUsed,
830
+ options
831
+ )) {
767
832
  return response;
768
833
  }
769
834
  if (tokenUsed !== this.userToken) {
@@ -782,7 +847,7 @@ var HttpClient = class {
782
847
  try {
783
848
  newTokenData = await this.refreshAndSaveSession();
784
849
  } catch (error) {
785
- if (error instanceof InsForgeError && (error.statusCode === 401 || error.statusCode === 403)) {
850
+ if (error instanceof ResultError && (error.statusCode === 401 || error.statusCode === 403)) {
786
851
  this.clearAuthSession();
787
852
  }
788
853
  throw error;
@@ -827,7 +892,7 @@ var HttpClient = class {
827
892
  const headers = { ...this.defaultHeaders };
828
893
  const authToken = this.userToken || this.anonKey;
829
894
  if (authToken) {
830
- headers["Authorization"] = `Bearer ${authToken}`;
895
+ headers.Authorization = `Bearer ${authToken}`;
831
896
  }
832
897
  return headers;
833
898
  }
@@ -871,7 +936,7 @@ var HttpClient = class {
871
936
  const refreshed = await this.refreshAndSaveSession();
872
937
  return refreshed.accessToken;
873
938
  } catch (error) {
874
- if (error instanceof InsForgeError && (error.statusCode === 401 || error.statusCode === 403) && this.userToken === accessToken) {
939
+ if (error instanceof ResultError && (error.statusCode === 401 || error.statusCode === 403) && this.userToken === accessToken) {
875
940
  this.clearAuthSession();
876
941
  }
877
942
  throw error;
@@ -880,7 +945,10 @@ var HttpClient = class {
880
945
  async refreshAndSaveSession() {
881
946
  const newTokenData = await this.refreshAccessToken();
882
947
  this.setAuthToken(newTokenData.accessToken);
883
- this.tokenManager.saveSession(newTokenData, AuthChangeEvent.TOKEN_REFRESHED);
948
+ this.tokenManager.saveSession(
949
+ newTokenData,
950
+ AuthChangeEvent.TOKEN_REFRESHED
951
+ );
884
952
  if (newTokenData.csrfToken) {
885
953
  setCsrfToken(newTokenData.csrfToken);
886
954
  }
@@ -896,1328 +964,1514 @@ var HttpClient = class {
896
964
  clearCsrfToken();
897
965
  }
898
966
  };
899
- var PKCE_VERIFIER_KEY = "insforge_pkce_verifier";
900
- async function getWebCrypto() {
901
- const webCrypto = globalThis.crypto;
902
- if (typeof webCrypto?.getRandomValues === "function" && webCrypto.subtle) {
903
- return webCrypto;
904
- }
905
- if (typeof process !== "undefined" && process.versions?.node) {
906
- const { webcrypto } = await import("crypto");
907
- return webcrypto;
908
- }
909
- throw new Error("Web Crypto API is not available in this environment");
910
- }
911
- function base64UrlEncode(buffer) {
912
- const base64 = btoa(String.fromCharCode(...buffer));
913
- return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
914
- }
915
- async function generateCodeVerifier() {
916
- const webCrypto = await getWebCrypto();
917
- const array = new Uint8Array(32);
918
- webCrypto.getRandomValues(array);
919
- return base64UrlEncode(array);
920
- }
921
- async function generateCodeChallenge(verifier) {
922
- const webCrypto = await getWebCrypto();
923
- const encoder = new TextEncoder();
924
- const data = encoder.encode(verifier);
925
- const hash = await webCrypto.subtle.digest("SHA-256", data);
926
- return base64UrlEncode(new Uint8Array(hash));
927
- }
928
- function storePkceVerifier(verifier) {
929
- if (typeof sessionStorage !== "undefined") {
930
- sessionStorage.setItem(PKCE_VERIFIER_KEY, verifier);
967
+
968
+ // src/modules/ai.ts
969
+ var SUPPORTED_AUDIO_FORMATS = [
970
+ "wav",
971
+ "mp3",
972
+ "aiff",
973
+ "aac",
974
+ "ogg",
975
+ "flac",
976
+ "m4a"
977
+ ];
978
+ var MULTIMODAL_MODEL = "google/gemini-2.5-flash";
979
+ function summarizeContent(messages) {
980
+ const summary = {
981
+ hasImage: false,
982
+ hasAudio: false,
983
+ badAudioFormats: []
984
+ };
985
+ for (const message of messages) {
986
+ if (message.images?.length) {
987
+ summary.hasImage = true;
988
+ }
989
+ if (!Array.isArray(message.content)) {
990
+ continue;
991
+ }
992
+ for (const part of message.content) {
993
+ if (part?.type === "image_url") {
994
+ summary.hasImage = true;
995
+ } else if (part?.type === "input_audio") {
996
+ summary.hasAudio = true;
997
+ const format2 = part.input_audio?.format;
998
+ if (format2 && !SUPPORTED_AUDIO_FORMATS.includes(format2)) {
999
+ summary.badAudioFormats.push(format2);
1000
+ }
1001
+ }
1002
+ }
931
1003
  }
1004
+ return summary;
932
1005
  }
933
- function retrievePkceVerifier() {
934
- if (typeof sessionStorage === "undefined") {
935
- return null;
1006
+ function assertSupportedContent(model, summary) {
1007
+ if (summary.badAudioFormats.length) {
1008
+ throw new ResultError(
1009
+ `Audio format "${summary.badAudioFormats[0]}" is not supported. Supported formats: ${SUPPORTED_AUDIO_FORMATS.join(", ")}. Browser MediaRecorder produces webm/opus, which the gateway rejects - capture PCM with the Web Audio API and encode a 16 kHz mono WAV instead.`,
1010
+ 400,
1011
+ "AI_INVALID_INPUT"
1012
+ );
936
1013
  }
937
- const verifier = sessionStorage.getItem(PKCE_VERIFIER_KEY);
938
- if (verifier) {
939
- sessionStorage.removeItem(PKCE_VERIFIER_KEY);
1014
+ if (summary.hasAudio && model.startsWith("openai/")) {
1015
+ throw new ResultError(
1016
+ `Model "${model}" does not accept audio input on this gateway. Use ${MULTIMODAL_MODEL} (verified for audio).`,
1017
+ 400,
1018
+ "AI_INVALID_INPUT"
1019
+ );
940
1020
  }
941
- return verifier;
942
1021
  }
943
- function wrapError(error, fallbackMessage) {
944
- if (error instanceof InsForgeError) {
945
- return { data: null, error };
946
- }
947
- return {
948
- data: null,
949
- error: new InsForgeError(
950
- error instanceof Error ? error.message : fallbackMessage,
951
- 500,
952
- "UNEXPECTED_ERROR"
953
- )
954
- };
1022
+ function enrichUpstreamError(error, summary) {
1023
+ if (!(error instanceof ResultError) || error.error !== "AI_UPSTREAM_UNAVAILABLE" || !summary.hasImage && !summary.hasAudio) {
1024
+ return error;
1025
+ }
1026
+ const kind = summary.hasImage && summary.hasAudio ? "image and audio" : summary.hasImage ? "image" : "audio";
1027
+ return new ResultError(
1028
+ `${error.message} This request included ${kind} input; if the error persists, use ${MULTIMODAL_MODEL} (verified for vision and audio).`,
1029
+ error.statusCode,
1030
+ error.error
1031
+ );
955
1032
  }
956
- function cleanUrlParams(...params) {
957
- if (typeof window === "undefined") {
958
- return;
1033
+ var AI = class {
1034
+ chat;
1035
+ images;
1036
+ embeddings;
1037
+ constructor(http) {
1038
+ this.chat = new Chat(http);
1039
+ this.images = new Images(http);
1040
+ this.embeddings = new Embeddings(http);
959
1041
  }
960
- const url = new URL(window.location.href);
961
- params.forEach((p) => url.searchParams.delete(p));
962
- window.history.replaceState({}, document.title, url.toString());
963
- }
964
- var Auth = class {
965
- constructor(http, tokenManager, options = {}) {
966
- this.http = http;
967
- this.tokenManager = tokenManager;
968
- this.options = options;
969
- this.authCallbackHandled = options.detectOAuthCallback === false ? Promise.resolve() : this.detectAuthCallback();
1042
+ };
1043
+ var Chat = class {
1044
+ completions;
1045
+ constructor(http) {
1046
+ this.completions = new ChatCompletions(http);
970
1047
  }
971
- isServerMode() {
972
- return !!this.options.isServerMode;
1048
+ };
1049
+ var ChatCompletions = class {
1050
+ constructor(http) {
1051
+ this.http = http;
973
1052
  }
974
- /** Subscribe to SDK authentication state changes. */
975
- onAuthStateChange(callback) {
976
- return this.tokenManager.onAuthStateChange(callback);
1053
+ http;
1054
+ async create(params) {
1055
+ const contentSummary = summarizeContent(params.messages ?? []);
1056
+ assertSupportedContent(params.model, contentSummary);
1057
+ const backendParams = {
1058
+ model: params.model,
1059
+ messages: params.messages,
1060
+ temperature: params.temperature,
1061
+ maxTokens: params.maxTokens,
1062
+ topP: params.topP,
1063
+ stream: params.stream,
1064
+ // New plugin options
1065
+ webSearch: params.webSearch,
1066
+ fileParser: params.fileParser,
1067
+ thinking: params.thinking,
1068
+ // Tool calling options
1069
+ tools: params.tools,
1070
+ toolChoice: params.toolChoice,
1071
+ parallelToolCalls: params.parallelToolCalls
1072
+ };
1073
+ if (params.stream) {
1074
+ const headers = this.http.getHeaders();
1075
+ headers["Content-Type"] = "application/json";
1076
+ const response2 = await this.http.fetch(
1077
+ `${this.http.baseUrl}/api/ai/chat/completion`,
1078
+ {
1079
+ method: "POST",
1080
+ headers,
1081
+ body: JSON.stringify(backendParams)
1082
+ }
1083
+ );
1084
+ if (!response2.ok) {
1085
+ let body = null;
1086
+ try {
1087
+ body = await response2.json();
1088
+ } catch {
1089
+ }
1090
+ throw enrichUpstreamError(
1091
+ new ResultError(
1092
+ body?.message || body?.error || "Stream request failed",
1093
+ body?.statusCode ?? response2.status,
1094
+ body?.error || "AI_STREAM_ERROR"
1095
+ ),
1096
+ contentSummary
1097
+ );
1098
+ }
1099
+ return this.parseSSEStream(response2, params.model);
1100
+ }
1101
+ let response;
1102
+ try {
1103
+ response = await this.http.post("/api/ai/chat/completion", backendParams);
1104
+ } catch (error) {
1105
+ throw enrichUpstreamError(error, contentSummary);
1106
+ }
1107
+ const content = response.text || "";
1108
+ return {
1109
+ id: `chatcmpl-${Date.now()}`,
1110
+ object: "chat.completion",
1111
+ created: Math.floor(Date.now() / 1e3),
1112
+ model: response.metadata?.model,
1113
+ choices: [
1114
+ {
1115
+ index: 0,
1116
+ message: {
1117
+ role: "assistant",
1118
+ content,
1119
+ // Include tool_calls if present (from tool calling)
1120
+ ...response.tool_calls?.length && {
1121
+ tool_calls: response.tool_calls
1122
+ },
1123
+ // Include annotations if present (from web search or file parsing)
1124
+ ...response.annotations?.length && {
1125
+ annotations: response.annotations
1126
+ }
1127
+ },
1128
+ finish_reason: response.tool_calls?.length ? "tool_calls" : "stop"
1129
+ }
1130
+ ],
1131
+ usage: {
1132
+ prompt_tokens: response.metadata?.usage?.promptTokens || 0,
1133
+ completion_tokens: response.metadata?.usage?.completionTokens || 0,
1134
+ total_tokens: response.metadata?.usage?.totalTokens || 0
1135
+ }
1136
+ };
977
1137
  }
978
1138
  /**
979
- * Save session from API response
980
- * Handles token storage, CSRF token, and HTTP auth header
1139
+ * Parse SSE stream into async iterable of OpenAI-like chunks
981
1140
  */
982
- saveSessionFromResponse(response, event = AuthChangeEvent.SIGNED_IN) {
983
- if (!response.accessToken || !response.user) {
984
- return false;
985
- }
986
- const session = {
987
- accessToken: response.accessToken,
988
- user: response.user
989
- };
990
- if (!this.isServerMode() && response.csrfToken) {
991
- setCsrfToken(response.csrfToken);
992
- }
993
- if (!this.isServerMode()) {
994
- this.tokenManager.saveSession(session, event);
995
- }
996
- this.http.setAuthToken(response.accessToken);
997
- this.http.setRefreshToken(response.refreshToken ?? null);
998
- return true;
999
- }
1000
- // ============================================================================
1001
- // OAuth Callback Detection (runs on initialization)
1002
- // ============================================================================
1003
- /**
1004
- * Detect and handle OAuth callback parameters in URL
1005
- * Supports PKCE flow (insforge_code)
1006
- */
1007
- async detectAuthCallback() {
1008
- if (this.isServerMode() || typeof window === "undefined") {
1009
- return;
1141
+ async *parseSSEStream(response, model) {
1142
+ const reader = response.body?.getReader();
1143
+ if (!reader) {
1144
+ throw new ResultError(
1145
+ "Streaming response has no body",
1146
+ 500,
1147
+ "AI_UPSTREAM_UNAVAILABLE"
1148
+ );
1010
1149
  }
1150
+ const decoder = new TextDecoder();
1151
+ let buffer = "";
1011
1152
  try {
1012
- const params = new URLSearchParams(window.location.search);
1013
- const error = params.get("error");
1014
- if (error) {
1015
- cleanUrlParams("error");
1016
- console.debug("OAuth callback error:", error);
1017
- return;
1018
- }
1019
- const code = params.get("insforge_code");
1020
- if (code) {
1021
- cleanUrlParams("insforge_code");
1022
- const { error: exchangeError } = await this.exchangeOAuthCode(code);
1023
- if (exchangeError) {
1024
- console.debug("OAuth code exchange failed:", exchangeError.message);
1153
+ while (true) {
1154
+ const { done, value } = await reader.read();
1155
+ if (done) {
1156
+ break;
1025
1157
  }
1026
- return;
1027
- }
1028
- } catch (error) {
1029
- console.debug("OAuth callback detection skipped:", error);
1030
- }
1031
- }
1032
- // ============================================================================
1033
- // Sign Up / Sign In / Sign Out
1034
- // ============================================================================
1035
- async signUp(request) {
1036
- try {
1037
- const response = await this.http.post(
1038
- this.isServerMode() ? "/api/auth/users?client_type=mobile" : "/api/auth/users",
1039
- request,
1040
- { credentials: "include", skipAuthRefresh: true }
1041
- );
1042
- if (response.accessToken && response.user) {
1043
- this.saveSessionFromResponse(response);
1044
- }
1045
- if (response.refreshToken) {
1046
- this.http.setRefreshToken(response.refreshToken);
1047
- }
1048
- return { data: response, error: null };
1049
- } catch (error) {
1050
- return wrapError(error, "An unexpected error occurred during sign up");
1051
- }
1052
- }
1053
- async signInWithPassword(request) {
1054
- try {
1055
- const response = await this.http.post(
1056
- this.isServerMode() ? "/api/auth/sessions?client_type=mobile" : "/api/auth/sessions",
1057
- request,
1058
- { credentials: "include", skipAuthRefresh: true }
1059
- );
1060
- this.saveSessionFromResponse(response);
1061
- if (response.refreshToken) {
1062
- this.http.setRefreshToken(response.refreshToken);
1063
- }
1064
- return { data: response, error: null };
1065
- } catch (error) {
1066
- return wrapError(error, "An unexpected error occurred during sign in");
1067
- }
1068
- }
1069
- async signOut() {
1070
- try {
1071
- try {
1072
- const serverMode = this.isServerMode();
1073
- const csrfToken = !serverMode ? getCsrfToken() : null;
1074
- await this.http.post(
1075
- serverMode ? "/api/auth/logout?client_type=mobile" : "/api/auth/logout",
1076
- void 0,
1077
- {
1078
- credentials: "include",
1079
- skipAuthRefresh: true,
1080
- ...csrfToken ? { headers: { "X-CSRF-Token": csrfToken } } : {}
1158
+ buffer += decoder.decode(value, { stream: true });
1159
+ const lines = buffer.split("\n");
1160
+ buffer = lines.pop() || "";
1161
+ for (const line of lines) {
1162
+ if (line.startsWith("data: ")) {
1163
+ const dataStr = line.slice(6).trim();
1164
+ if (dataStr) {
1165
+ try {
1166
+ const data = JSON.parse(dataStr);
1167
+ if (data.chunk || data.content) {
1168
+ yield {
1169
+ id: `chatcmpl-${Date.now()}`,
1170
+ object: "chat.completion.chunk",
1171
+ created: Math.floor(Date.now() / 1e3),
1172
+ model,
1173
+ choices: [
1174
+ {
1175
+ index: 0,
1176
+ delta: {
1177
+ content: data.chunk || data.content
1178
+ },
1179
+ finish_reason: null
1180
+ }
1181
+ ]
1182
+ };
1183
+ }
1184
+ if (data.tool_calls?.length) {
1185
+ yield {
1186
+ id: `chatcmpl-${Date.now()}`,
1187
+ object: "chat.completion.chunk",
1188
+ created: Math.floor(Date.now() / 1e3),
1189
+ model,
1190
+ choices: [
1191
+ {
1192
+ index: 0,
1193
+ delta: {
1194
+ tool_calls: data.tool_calls
1195
+ },
1196
+ finish_reason: "tool_calls"
1197
+ }
1198
+ ]
1199
+ };
1200
+ }
1201
+ if (data.annotations?.length) {
1202
+ yield {
1203
+ id: `chatcmpl-${Date.now()}`,
1204
+ object: "chat.completion.chunk",
1205
+ created: Math.floor(Date.now() / 1e3),
1206
+ model,
1207
+ choices: [
1208
+ {
1209
+ index: 0,
1210
+ delta: {
1211
+ annotations: data.annotations
1212
+ },
1213
+ finish_reason: null
1214
+ }
1215
+ ]
1216
+ };
1217
+ }
1218
+ if (data.tokenUsage) {
1219
+ yield {
1220
+ id: `chatcmpl-${Date.now()}`,
1221
+ object: "chat.completion.chunk",
1222
+ created: Math.floor(Date.now() / 1e3),
1223
+ model,
1224
+ choices: [],
1225
+ usage: {
1226
+ prompt_tokens: data.tokenUsage.promptTokens || 0,
1227
+ completion_tokens: data.tokenUsage.completionTokens || 0,
1228
+ total_tokens: data.tokenUsage.totalTokens || 0
1229
+ }
1230
+ };
1231
+ }
1232
+ if (data.done) {
1233
+ reader.releaseLock();
1234
+ return;
1235
+ }
1236
+ } catch {
1237
+ console.warn("Failed to parse SSE data:", dataStr);
1238
+ }
1239
+ }
1081
1240
  }
1082
- );
1083
- } catch {
1084
- }
1085
- this.tokenManager.clearSession();
1086
- this.http.setAuthToken(null);
1087
- this.http.setRefreshToken(null);
1088
- if (!this.isServerMode()) {
1089
- clearCsrfToken();
1090
- }
1091
- return { error: null };
1092
- } catch {
1093
- return {
1094
- error: new InsForgeError("Failed to sign out", 500, "SIGNOUT_ERROR")
1095
- };
1096
- }
1097
- }
1098
- async signInWithOAuth(providerOrOptions, options) {
1099
- try {
1100
- let signInOptions;
1101
- if (typeof providerOrOptions === "object") {
1102
- signInOptions = providerOrOptions;
1103
- } else if (options) {
1104
- signInOptions = { provider: providerOrOptions, ...options };
1105
- } else {
1106
- return {
1107
- data: {},
1108
- error: new InsForgeError(
1109
- "OAuth sign-in options are required",
1110
- 400,
1111
- ERROR_CODES.INVALID_INPUT
1112
- )
1113
- };
1114
- }
1115
- if (!signInOptions || !signInOptions.redirectTo) {
1116
- return {
1117
- data: {},
1118
- error: new InsForgeError("Redirect URI is required", 400, ERROR_CODES.INVALID_INPUT)
1119
- };
1120
- }
1121
- const { provider } = signInOptions;
1122
- const providerKey = encodeURIComponent(provider.toLowerCase());
1123
- const codeVerifier = await generateCodeVerifier();
1124
- const codeChallenge = await generateCodeChallenge(codeVerifier);
1125
- storePkceVerifier(codeVerifier);
1126
- const params = {
1127
- ...signInOptions.additionalParams ?? {},
1128
- redirect_uri: signInOptions.redirectTo,
1129
- code_challenge: codeChallenge
1130
- };
1131
- const isBuiltInProvider = oAuthProvidersSchema.options.includes(
1132
- providerKey
1133
- );
1134
- const oauthPath = isBuiltInProvider ? `/api/auth/oauth/${providerKey}` : `/api/auth/oauth/custom/${providerKey}`;
1135
- const response = await this.http.get(oauthPath, {
1136
- params,
1137
- skipAuthRefresh: true
1138
- });
1139
- if (!this.isServerMode() && typeof window !== "undefined" && !signInOptions.skipBrowserRedirect) {
1140
- window.location.href = response.authUrl;
1141
- return { data: {}, error: null };
1142
- }
1143
- return {
1144
- data: { url: response.authUrl, provider: providerKey, codeVerifier },
1145
- error: null
1146
- };
1147
- } catch (error) {
1148
- if (error instanceof InsForgeError) {
1149
- return { data: {}, error };
1241
+ }
1150
1242
  }
1151
- return {
1152
- data: {},
1153
- error: new InsForgeError(
1154
- "An unexpected error occurred during OAuth initialization",
1155
- 500,
1156
- "UNEXPECTED_ERROR"
1157
- )
1158
- };
1243
+ } finally {
1244
+ reader.releaseLock();
1159
1245
  }
1160
1246
  }
1161
- /**
1162
- * Exchange OAuth authorization code for tokens (PKCE flow)
1163
- * Called automatically on initialization when insforge_code is in URL
1164
- */
1165
- async exchangeOAuthCode(code, codeVerifier) {
1166
- try {
1167
- const verifier = codeVerifier ?? retrievePkceVerifier();
1168
- if (!verifier) {
1169
- return {
1170
- data: null,
1171
- error: new InsForgeError(
1172
- "PKCE code verifier not found. Ensure signInWithOAuth was called in the same browser session.",
1173
- 400,
1174
- "PKCE_VERIFIER_MISSING"
1175
- )
1176
- };
1177
- }
1178
- const request = {
1179
- code,
1180
- code_verifier: verifier
1181
- };
1182
- const response = await this.http.post(
1183
- this.isServerMode() ? "/api/auth/oauth/exchange?client_type=mobile" : "/api/auth/oauth/exchange",
1184
- request,
1185
- { credentials: "include", skipAuthRefresh: true }
1186
- );
1187
- this.saveSessionFromResponse(response);
1188
- return {
1189
- data: response,
1190
- error: null
1191
- };
1192
- } catch (error) {
1193
- return wrapError(error, "An unexpected error occurred during OAuth code exchange");
1194
- }
1247
+ };
1248
+ var Embeddings = class {
1249
+ constructor(http) {
1250
+ this.http = http;
1195
1251
  }
1252
+ http;
1196
1253
  /**
1197
- * Sign in with an ID token from a native SDK (Google One Tap, etc.)
1198
- * Use this for native mobile apps or Google One Tap on web.
1254
+ * Create embeddings for text input - OpenAI-like response format
1199
1255
  *
1200
- * @param credentials.provider - The identity provider (currently only 'google' is supported)
1201
- * @param credentials.token - The ID token from the native SDK
1202
- */
1203
- async signInWithIdToken(credentials) {
1204
- try {
1205
- const { provider, token } = credentials;
1206
- const response = await this.http.post(
1207
- "/api/auth/id-token?client_type=mobile",
1208
- { provider, token },
1209
- { credentials: "include", skipAuthRefresh: true }
1210
- );
1211
- this.saveSessionFromResponse(response);
1212
- if (response.refreshToken) {
1213
- this.http.setRefreshToken(response.refreshToken);
1214
- }
1215
- return {
1216
- data: response,
1217
- error: null
1218
- };
1219
- } catch (error) {
1220
- return wrapError(error, "An unexpected error occurred during ID token sign in");
1221
- }
1222
- }
1223
- // ============================================================================
1224
- // Session Management
1225
- // ============================================================================
1226
- /**
1227
- * Refresh the current auth session.
1256
+ * @example
1257
+ * ```typescript
1258
+ * // Single text input
1259
+ * const response = await client.ai.embeddings.create({
1260
+ * model: 'openai/text-embedding-3-small',
1261
+ * input: 'Hello world'
1262
+ * });
1263
+ * console.log(response.data[0].embedding); // number[]
1228
1264
  *
1229
- * Browser mode:
1230
- * - Uses httpOnly refresh cookie and optional CSRF header.
1265
+ * // Multiple text inputs
1266
+ * const response = await client.ai.embeddings.create({
1267
+ * model: 'openai/text-embedding-3-small',
1268
+ * input: ['Hello world', 'Goodbye world']
1269
+ * });
1270
+ * response.data.forEach((item, i) => {
1271
+ * console.log(`Embedding ${i}:`, item.embedding.slice(0, 5)); // First 5 dimensions
1272
+ * });
1231
1273
  *
1232
- * Legacy server mode (`isServerMode: true`):
1233
- * - Uses mobile auth flow and requires `refreshToken` in request body.
1274
+ * // With custom dimensions (if supported by model)
1275
+ * const response = await client.ai.embeddings.create({
1276
+ * model: 'openai/text-embedding-3-small',
1277
+ * input: 'Hello world',
1278
+ * dimensions: 256
1279
+ * });
1234
1280
  *
1235
- * SSR apps should prefer `createRefreshAuthRouter()` / `refreshAuth()` from
1236
- * `@insforge/sdk/ssr`.
1237
- */
1238
- async refreshSession(options) {
1239
- try {
1240
- if (this.isServerMode() && !options?.refreshToken) {
1241
- return {
1242
- data: null,
1243
- error: new InsForgeError(
1244
- "refreshToken is required when refreshing session in server mode",
1245
- 400,
1246
- ERROR_CODES.AUTH_UNAUTHORIZED
1247
- )
1248
- };
1249
- }
1250
- const csrfToken = !this.isServerMode() ? getCsrfToken() : null;
1251
- const response = await this.http.post(
1252
- this.isServerMode() ? "/api/auth/refresh?client_type=mobile" : "/api/auth/refresh",
1253
- this.isServerMode() ? { refresh_token: options?.refreshToken } : void 0,
1254
- {
1255
- headers: csrfToken ? { "X-CSRF-Token": csrfToken } : {},
1256
- credentials: "include",
1257
- skipAuthRefresh: true
1258
- }
1259
- );
1260
- if (response.accessToken) {
1261
- this.saveSessionFromResponse(response, AuthChangeEvent.TOKEN_REFRESHED);
1281
+ * // With base64 encoding format
1282
+ * const response = await client.ai.embeddings.create({
1283
+ * model: 'openai/text-embedding-3-small',
1284
+ * input: 'Hello world',
1285
+ * encoding_format: 'base64'
1286
+ * });
1287
+ * ```
1288
+ */
1289
+ async create(params) {
1290
+ const response = await this.http.post(
1291
+ "/api/ai/embeddings",
1292
+ params
1293
+ );
1294
+ return {
1295
+ object: response.object,
1296
+ data: response.data,
1297
+ model: response.metadata?.model,
1298
+ usage: response.metadata?.usage ? {
1299
+ prompt_tokens: response.metadata.usage.promptTokens || 0,
1300
+ total_tokens: response.metadata.usage.totalTokens || 0
1301
+ } : {
1302
+ prompt_tokens: 0,
1303
+ total_tokens: 0
1262
1304
  }
1263
- return { data: response, error: null };
1264
- } catch (error) {
1265
- return wrapError(error, "An unexpected error occurred during session refresh");
1266
- }
1305
+ };
1306
+ }
1307
+ };
1308
+ var Images = class {
1309
+ constructor(http) {
1310
+ this.http = http;
1267
1311
  }
1312
+ http;
1268
1313
  /**
1269
- * Get current user, automatically waits for pending OAuth callback
1314
+ * Generate images - OpenAI-like response format
1315
+ *
1316
+ * @example
1317
+ * ```typescript
1318
+ * // Text-to-image
1319
+ * const response = await client.ai.images.generate({
1320
+ * model: 'dall-e-3',
1321
+ * prompt: 'A sunset over mountains',
1322
+ * });
1323
+ * console.log(response.data[0].b64_json);
1324
+ *
1325
+ * // Image-to-image (with input images)
1326
+ * const response = await client.ai.images.generate({
1327
+ * model: 'stable-diffusion-xl',
1328
+ * prompt: 'Transform this into a watercolor painting',
1329
+ * images: [
1330
+ * { url: 'https://example.com/input.jpg' },
1331
+ * // or base64-encoded Data URI:
1332
+ * { url: 'data:image/jpeg;base64,/9j/4AAQ...' }
1333
+ * ]
1334
+ * });
1335
+ * ```
1270
1336
  */
1271
- async getCurrentUser() {
1272
- await this.authCallbackHandled;
1273
- try {
1274
- if (this.isServerMode()) {
1275
- const accessToken = this.tokenManager.getAccessToken();
1276
- if (!accessToken) {
1277
- return { data: { user: null }, error: null };
1278
- }
1279
- this.http.setAuthToken(accessToken);
1280
- const response = await this.http.get("/api/auth/sessions/current");
1281
- const user = response.user ?? null;
1282
- return { data: { user }, error: null };
1283
- }
1284
- const session = this.tokenManager.getSession();
1285
- if (session) {
1286
- this.http.setAuthToken(session.accessToken);
1287
- return { data: { user: session.user }, error: null };
1288
- }
1289
- if (typeof window !== "undefined") {
1290
- const { data: refreshed, error: refreshError } = await this.refreshSession();
1291
- if (refreshError) {
1292
- return { data: { user: null }, error: refreshError };
1293
- }
1294
- if (refreshed?.accessToken) {
1295
- return { data: { user: refreshed.user ?? null }, error: null };
1337
+ async generate(params) {
1338
+ const response = await this.http.post(
1339
+ "/api/ai/image/generation",
1340
+ params
1341
+ );
1342
+ let data = [];
1343
+ if (response.images && response.images.length > 0) {
1344
+ data = response.images.map((img) => ({
1345
+ b64_json: img.imageUrl.replace(/^data:image\/\w+;base64,/, ""),
1346
+ content: response.text
1347
+ }));
1348
+ } else if (response.text) {
1349
+ data = [{ content: response.text }];
1350
+ }
1351
+ return {
1352
+ created: Math.floor(Date.now() / 1e3),
1353
+ data,
1354
+ ...response.metadata?.usage && {
1355
+ usage: {
1356
+ total_tokens: response.metadata.usage.totalTokens || 0,
1357
+ input_tokens: response.metadata.usage.promptTokens || 0,
1358
+ output_tokens: response.metadata.usage.completionTokens || 0
1296
1359
  }
1297
1360
  }
1298
- return { data: { user: null }, error: null };
1299
- } catch (error) {
1300
- if (error instanceof InsForgeError) {
1301
- return { data: { user: null }, error };
1302
- }
1303
- return {
1304
- data: { user: null },
1305
- error: new InsForgeError(
1306
- "An unexpected error occurred while getting user",
1307
- 500,
1308
- "UNEXPECTED_ERROR"
1309
- )
1310
- };
1361
+ };
1362
+ }
1363
+ };
1364
+
1365
+ // src/modules/analytics.ts
1366
+ var DEFAULT_ENDPOINT = "https://beta.result.dev/api/wa";
1367
+ var IDLE_MS = 30 * 60 * 1e3;
1368
+ var Analytics = class {
1369
+ siteId;
1370
+ endpoint;
1371
+ options;
1372
+ active = false;
1373
+ lastUrl = null;
1374
+ left = false;
1375
+ referrer = "";
1376
+ // localStorage with an in-memory fallback for private-mode browsers.
1377
+ mem = {};
1378
+ constructor(options) {
1379
+ this.options = options ?? { siteId: "" };
1380
+ this.siteId = this.options.siteId ?? "";
1381
+ this.endpoint = this.options.endpoint || DEFAULT_ENDPOINT;
1382
+ if (!this.siteId || typeof window === "undefined" || navigator.webdriver) {
1383
+ return;
1384
+ }
1385
+ if (window.rwa) {
1386
+ return;
1387
+ }
1388
+ if (this.read("rwa_disable") === "1") {
1389
+ return;
1390
+ }
1391
+ const domains = (this.options.domains ?? []).map(
1392
+ (domain) => domain.trim().toLowerCase().replace(/^www\./, "")
1393
+ ).filter(Boolean);
1394
+ const here = location.hostname.toLowerCase().replace(/^www\./, "");
1395
+ if (domains.length && domains.indexOf(here) === -1) {
1396
+ return;
1397
+ }
1398
+ this.active = true;
1399
+ this.referrer = document.referrer || "";
1400
+ window.rwa = {
1401
+ track: (name, props) => this.track(name, props),
1402
+ identify: (id) => this.identify(id)
1403
+ };
1404
+ const doc = document;
1405
+ if (doc.prerendering) {
1406
+ document.addEventListener("prerenderingchange", () => this.start(), {
1407
+ once: true
1408
+ });
1409
+ } else {
1410
+ this.start();
1311
1411
  }
1312
1412
  }
1313
- // ============================================================================
1314
- // Profile Management
1315
- // ============================================================================
1316
- async getProfile(userId) {
1317
- try {
1318
- const response = await this.http.get(`/api/auth/profiles/${userId}`);
1319
- return { data: response, error: null };
1320
- } catch (error) {
1321
- return wrapError(error, "An unexpected error occurred while fetching user profile");
1413
+ /** Send a custom event. No-op outside the browser or when inactive. */
1414
+ track(name, props) {
1415
+ if (!this.active || typeof name !== "string" || !name) {
1416
+ return;
1322
1417
  }
1418
+ const event = {
1419
+ e: "ev",
1420
+ n: name.slice(0, 64),
1421
+ dt: document.title
1422
+ };
1423
+ if (props && typeof props === "object") {
1424
+ event.p = props;
1425
+ }
1426
+ this.post(event);
1323
1427
  }
1324
- async setProfile(profile) {
1325
- try {
1326
- const response = await this.http.patch("/api/auth/profiles/current", {
1327
- profile
1328
- });
1329
- const currentUser = this.tokenManager.getUser();
1330
- if (!this.isServerMode() && currentUser && response.profile !== void 0) {
1331
- this.tokenManager.setUser({
1332
- ...currentUser,
1333
- profile: response.profile
1334
- });
1335
- }
1336
- return { data: response, error: null };
1337
- } catch (error) {
1338
- return wrapError(error, "An unexpected error occurred while updating user profile");
1428
+ /** Attach your own user id to subsequent events. */
1429
+ identify(id) {
1430
+ if (!this.active || typeof id !== "string" || !id) {
1431
+ return;
1339
1432
  }
1433
+ this.write("rwa_uid", id.slice(0, 64));
1340
1434
  }
1341
- // ============================================================================
1342
- // Email Verification
1343
- // ============================================================================
1344
- async resendVerificationEmail(request) {
1435
+ // -- internals -------------------------------------------------------------
1436
+ read(key) {
1345
1437
  try {
1346
- const response = await this.http.post("/api/auth/email/send-verification", request, {
1347
- skipAuthRefresh: true
1348
- });
1349
- return { data: response, error: null };
1350
- } catch (error) {
1351
- return wrapError(error, "An unexpected error occurred while sending verification email");
1438
+ return localStorage.getItem(key) || this.mem[key] || "";
1439
+ } catch {
1440
+ return this.mem[key] || "";
1352
1441
  }
1353
1442
  }
1354
- async verifyEmail(request) {
1443
+ write(key, value) {
1444
+ this.mem[key] = value;
1355
1445
  try {
1356
- const response = await this.http.post(
1357
- this.isServerMode() ? "/api/auth/email/verify?client_type=mobile" : "/api/auth/email/verify",
1358
- request,
1359
- { credentials: "include", skipAuthRefresh: true }
1360
- );
1361
- this.saveSessionFromResponse(response);
1362
- if (response.refreshToken) {
1363
- this.http.setRefreshToken(response.refreshToken);
1364
- }
1365
- return { data: response, error: null };
1366
- } catch (error) {
1367
- return wrapError(error, "An unexpected error occurred while verifying email");
1446
+ localStorage.setItem(key, value);
1447
+ } catch {
1368
1448
  }
1369
1449
  }
1370
- // ============================================================================
1371
- // Password Reset
1372
- // ============================================================================
1373
- async sendResetPasswordEmail(request) {
1374
- try {
1375
- const response = await this.http.post("/api/auth/email/send-reset-password", request, {
1376
- skipAuthRefresh: true
1450
+ visitor() {
1451
+ let visitor = this.read("rwa_id");
1452
+ if (!visitor) {
1453
+ visitor = uuid4();
1454
+ this.write("rwa_id", visitor);
1455
+ }
1456
+ return visitor;
1457
+ }
1458
+ session() {
1459
+ const parts = this.read("rwa_s").split(".");
1460
+ let id = parts[0];
1461
+ const last = Number(parts[1]) || 0;
1462
+ const now = Date.now();
1463
+ if (!id || now - last > IDLE_MS) {
1464
+ id = uuid7();
1465
+ }
1466
+ this.write("rwa_s", `${id}.${now}`);
1467
+ return id;
1468
+ }
1469
+ pageUrl() {
1470
+ let url = location.href;
1471
+ if (this.options.excludeHash) {
1472
+ url = url.split("#")[0];
1473
+ }
1474
+ if (this.options.excludeSearch) {
1475
+ const hash = this.options.excludeHash ? "" : url.split("#")[1] || "";
1476
+ url = url.split("#")[0].split("?")[0] + (hash ? `#${hash}` : "");
1477
+ }
1478
+ return url.slice(0, 4096);
1479
+ }
1480
+ post(event) {
1481
+ event.u = event.u || this.pageUrl();
1482
+ event.d = this.visitor();
1483
+ event.s = this.session();
1484
+ event.t = Date.now();
1485
+ event.w = innerWidth;
1486
+ event.h = innerHeight;
1487
+ event.sw = screen.width;
1488
+ event.sh = screen.height;
1489
+ const uid = this.read("rwa_uid");
1490
+ if (uid) {
1491
+ event.uid = uid;
1492
+ }
1493
+ const body = JSON.stringify({ site: this.siteId, events: [event] });
1494
+ if (!navigator.sendBeacon?.(this.endpoint, body)) {
1495
+ fetch(this.endpoint, {
1496
+ method: "POST",
1497
+ body,
1498
+ keepalive: true,
1499
+ mode: "no-cors"
1500
+ }).catch(() => {
1377
1501
  });
1378
- return { data: response, error: null };
1379
- } catch (error) {
1380
- return wrapError(error, "An unexpected error occurred while sending password reset email");
1381
1502
  }
1382
1503
  }
1383
- async exchangeResetPasswordToken(request) {
1384
- try {
1385
- const response = await this.http.post(
1386
- "/api/auth/email/exchange-reset-password-token",
1387
- request,
1388
- { skipAuthRefresh: true }
1389
- );
1390
- return { data: response, error: null };
1391
- } catch (error) {
1392
- return wrapError(error, "An unexpected error occurred while verifying reset code");
1504
+ pageview() {
1505
+ const url = this.pageUrl();
1506
+ if (url === this.lastUrl) {
1507
+ return;
1393
1508
  }
1509
+ const from = this.lastUrl ? this.lastUrl : this.referrer;
1510
+ this.lastUrl = url;
1511
+ this.left = false;
1512
+ this.post({ e: "pv", u: url, r: from, dt: document.title });
1394
1513
  }
1395
- async resetPassword(request) {
1396
- try {
1397
- const response = await this.http.post(
1398
- "/api/auth/email/reset-password",
1399
- request,
1400
- { skipAuthRefresh: true }
1401
- );
1402
- return { data: response, error: null };
1403
- } catch (error) {
1404
- return wrapError(error, "An unexpected error occurred while resetting password");
1514
+ pageleave() {
1515
+ if (this.left || !this.lastUrl) {
1516
+ return;
1517
+ }
1518
+ this.left = true;
1519
+ this.post({ e: "pl", u: this.lastUrl });
1520
+ }
1521
+ start() {
1522
+ const onNav = () => {
1523
+ setTimeout(() => this.pageview(), 0);
1524
+ };
1525
+ if (this.options.autoTrack !== false) {
1526
+ const push = history.pushState;
1527
+ const replace = history.replaceState;
1528
+ history.pushState = function(...args) {
1529
+ push.apply(this, args);
1530
+ onNav();
1531
+ };
1532
+ history.replaceState = function(...args) {
1533
+ replace.apply(this, args);
1534
+ onNav();
1535
+ };
1536
+ addEventListener("popstate", onNav);
1537
+ addEventListener("hashchange", onNav);
1538
+ addEventListener("pageshow", (event) => {
1539
+ if (event.persisted) {
1540
+ this.lastUrl = null;
1541
+ this.pageview();
1542
+ }
1543
+ });
1544
+ this.pageview();
1405
1545
  }
1546
+ addEventListener("pagehide", () => this.pageleave());
1547
+ document.addEventListener("visibilitychange", () => {
1548
+ if (document.visibilityState === "hidden") {
1549
+ this.pageleave();
1550
+ } else {
1551
+ this.left = false;
1552
+ }
1553
+ });
1554
+ document.addEventListener(
1555
+ "click",
1556
+ (clickEvent) => {
1557
+ const source = clickEvent.target;
1558
+ const target = source?.closest ? source.closest("[data-wa-event]") : null;
1559
+ if (!target) {
1560
+ return;
1561
+ }
1562
+ const name = target.getAttribute("data-wa-event");
1563
+ if (!name) {
1564
+ return;
1565
+ }
1566
+ const props = {};
1567
+ let any = false;
1568
+ for (const attr of Array.from(target.attributes)) {
1569
+ if (attr.name.indexOf("data-wa-event-") === 0) {
1570
+ props[attr.name.slice(14)] = attr.value;
1571
+ any = true;
1572
+ }
1573
+ }
1574
+ this.track(name, any ? props : void 0);
1575
+ },
1576
+ true
1577
+ );
1578
+ }
1579
+ };
1580
+ function bytes() {
1581
+ const b = new Uint8Array(16);
1582
+ crypto.getRandomValues(b);
1583
+ return b;
1584
+ }
1585
+ function format(b, version) {
1586
+ b[6] = b[6] & 15 | version << 4;
1587
+ b[8] = b[8] & 63 | 128;
1588
+ let hex = "";
1589
+ for (let i = 0; i < 16; i++) {
1590
+ hex += (b[i] + 256).toString(16).slice(1);
1591
+ if (i === 3 || i === 5 || i === 7 || i === 9) hex += "-";
1592
+ }
1593
+ return hex;
1594
+ }
1595
+ function uuid4() {
1596
+ return format(bytes(), 4);
1597
+ }
1598
+ function uuid7() {
1599
+ const b = bytes();
1600
+ let t = Date.now();
1601
+ for (let i = 5; i >= 0; i--) {
1602
+ b[i] = t & 255;
1603
+ t = Math.floor(t / 256);
1604
+ }
1605
+ return format(b, 7);
1606
+ }
1607
+
1608
+ // src/types/api.ts
1609
+ var ERROR_CODE_VALUES = [
1610
+ // Auth
1611
+ "AUTH_INVALID_EMAIL",
1612
+ "AUTH_WEAK_PASSWORD",
1613
+ "AUTH_INVALID_CREDENTIALS",
1614
+ "AUTH_INVALID_API_KEY",
1615
+ "AUTH_EMAIL_EXISTS",
1616
+ "AUTH_USER_NOT_FOUND",
1617
+ "AUTH_OAUTH_CONFIG_ALREADY_EXISTS",
1618
+ "AUTH_OAUTH_CONFIG_ERROR",
1619
+ "AUTH_OAUTH_CONFIG_NOT_FOUND",
1620
+ "AUTH_UNSUPPORTED_PROVIDER",
1621
+ "AUTH_TOKEN_EXPIRED",
1622
+ "AUTH_UNAUTHORIZED",
1623
+ "AUTH_NEED_VERIFICATION",
1624
+ "AUTH_SIGNUP_DISABLED",
1625
+ "AUTH_VERIFICATION_EMAIL_DELIVERY_FAILED",
1626
+ // Database
1627
+ "DATABASE_INVALID_PARAMETER",
1628
+ "DATABASE_VALIDATION_ERROR",
1629
+ "DATABASE_CONSTRAINT_VIOLATION",
1630
+ "DATABASE_NOT_FOUND",
1631
+ "DATABASE_DUPLICATE",
1632
+ "DATABASE_MIGRATION_ALREADY_EXISTS",
1633
+ "DATABASE_PERMISSION_DENIED",
1634
+ "DATABASE_INTERNAL_ERROR",
1635
+ "DATABASE_FORBIDDEN",
1636
+ // Storage
1637
+ "STORAGE_ALREADY_EXISTS",
1638
+ "STORAGE_INVALID_PARAMETER",
1639
+ "STORAGE_INVALID_FILE_TYPE",
1640
+ "STORAGE_INSUFFICIENT_QUOTA",
1641
+ "STORAGE_NOT_FOUND",
1642
+ "STORAGE_PERMISSION_DENIED",
1643
+ "S3_ACCESS_KEY_LIMIT_EXCEEDED",
1644
+ "S3_ACCESS_KEY_NOT_FOUND",
1645
+ "S3_PROTOCOL_UNAVAILABLE",
1646
+ // Realtime
1647
+ "REALTIME_CHANNEL_NOT_FOUND",
1648
+ "REALTIME_CONNECTION_FAILED",
1649
+ "REALTIME_INVALID_CHANNEL_REQUEST",
1650
+ "REALTIME_INVALID_CHANNEL_PATTERN",
1651
+ "REALTIME_INVALID_EVENT",
1652
+ "REALTIME_NOT_SUBSCRIBED",
1653
+ "REALTIME_UNAUTHORIZED",
1654
+ // AI
1655
+ "AI_INVALID_API_KEY",
1656
+ "AI_INVALID_MODEL",
1657
+ "AI_UPSTREAM_UNAVAILABLE",
1658
+ // Analytics
1659
+ "ANALYTICS_NOT_CONNECTED",
1660
+ "ANALYTICS_UNAVAILABLE",
1661
+ // Logs
1662
+ "LOGS_AWS_NOT_CONFIGURED",
1663
+ "LOG_NOT_FOUND",
1664
+ // Compute
1665
+ "COMPUTE_CLOUD_UNAVAILABLE",
1666
+ "COMPUTE_NOT_CONFIGURED",
1667
+ "COMPUTE_PROVIDER_ERROR",
1668
+ "COMPUTE_SERVICE_NOT_FOUND",
1669
+ "COMPUTE_MACHINE_NOT_FOUND",
1670
+ "COMPUTE_SERVICE_NOT_CONFIGURED",
1671
+ "COMPUTE_SERVICE_DEPLOY_FAILED",
1672
+ "COMPUTE_SERVICE_ALREADY_EXISTS",
1673
+ "COMPUTE_SERVICE_START_FAILED",
1674
+ "COMPUTE_SERVICE_STOP_FAILED",
1675
+ "COMPUTE_SERVICE_DELETE_FAILED",
1676
+ "COMPUTE_REGION_CHANGE_NOT_SUPPORTED",
1677
+ "COMPUTE_QUOTA_EXCEEDED",
1678
+ // Billing
1679
+ "BILLING_INSUFFICIENT_BALANCE",
1680
+ // Email
1681
+ "EMAIL_PROVIDER_NOT_CONFIGURED",
1682
+ "EMAIL_SMTP_CONNECTION_FAILED",
1683
+ "EMAIL_SMTP_SEND_FAILED",
1684
+ "EMAIL_TEMPLATE_NOT_FOUND",
1685
+ // Deployments
1686
+ "DEPLOYMENT_ALREADY_EXISTS",
1687
+ "DEPLOYMENT_INVALID_FILE",
1688
+ "DEPLOYMENT_NOT_FOUND",
1689
+ "DEPLOYMENT_UPLOAD_CANCELED",
1690
+ "DOMAIN_ALREADY_EXISTS",
1691
+ "DOMAIN_INVALID",
1692
+ "DOMAIN_NOT_FOUND",
1693
+ "ENVIRONMENT_VARIABLE_NOT_FOUND",
1694
+ // Docs
1695
+ "DOCS_NOT_FOUND",
1696
+ // Functions
1697
+ "FUNCTION_ALREADY_EXISTS",
1698
+ "FUNCTION_DEPLOYMENT_NOT_FOUND",
1699
+ "FUNCTION_NOT_FOUND",
1700
+ // Schedules
1701
+ "SCHEDULE_INVALID_CRON",
1702
+ "SCHEDULE_NOT_FOUND",
1703
+ // Payments
1704
+ "PAYMENT_CHECKOUT_ALREADY_EXISTS",
1705
+ "PAYMENT_CONFIG_INVALID",
1706
+ "PAYMENT_CONFIG_NOT_FOUND",
1707
+ "PAYMENT_NOT_FOUND",
1708
+ "PAYMENT_METHOD_DECLINED",
1709
+ "PAYMENT_PRICE_NOT_FOUND",
1710
+ "PAYMENT_PRODUCT_NOT_FOUND",
1711
+ // Secrets
1712
+ "SECRET_ALREADY_EXISTS",
1713
+ "SECRET_NOT_FOUND",
1714
+ // General
1715
+ "MISSING_FIELD",
1716
+ "ALREADY_EXISTS",
1717
+ "INVALID_INPUT",
1718
+ "NOT_FOUND",
1719
+ "UNKNOWN_ERROR",
1720
+ "INTERNAL_ERROR",
1721
+ "TOO_MANY_REQUESTS",
1722
+ "FORBIDDEN",
1723
+ "RATE_LIMITED",
1724
+ "NOT_IMPLEMENTED",
1725
+ "UPSTREAM_FAILURE"
1726
+ ];
1727
+ var ERROR_CODES = Object.fromEntries(
1728
+ ERROR_CODE_VALUES.map((code) => [code, code])
1729
+ );
1730
+ var OAUTH_PROVIDERS = [
1731
+ "google",
1732
+ "github",
1733
+ "discord",
1734
+ "linkedin",
1735
+ "facebook",
1736
+ "instagram",
1737
+ "tiktok",
1738
+ "apple",
1739
+ "x",
1740
+ "spotify",
1741
+ "microsoft"
1742
+ ];
1743
+
1744
+ // src/modules/auth/helpers.ts
1745
+ var PKCE_VERIFIER_KEY = "result_pkce_verifier";
1746
+ async function getWebCrypto() {
1747
+ const webCrypto = globalThis.crypto;
1748
+ if (typeof webCrypto?.getRandomValues === "function" && webCrypto.subtle) {
1749
+ return webCrypto;
1750
+ }
1751
+ if (typeof process !== "undefined" && process.versions?.node) {
1752
+ const { webcrypto } = await import("crypto");
1753
+ return webcrypto;
1754
+ }
1755
+ throw new Error("Web Crypto API is not available in this environment");
1756
+ }
1757
+ function base64UrlEncode(buffer) {
1758
+ const base64 = btoa(String.fromCharCode(...buffer));
1759
+ return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1760
+ }
1761
+ async function generateCodeVerifier() {
1762
+ const webCrypto = await getWebCrypto();
1763
+ const array = new Uint8Array(32);
1764
+ webCrypto.getRandomValues(array);
1765
+ return base64UrlEncode(array);
1766
+ }
1767
+ async function generateCodeChallenge(verifier) {
1768
+ const webCrypto = await getWebCrypto();
1769
+ const encoder = new TextEncoder();
1770
+ const data = encoder.encode(verifier);
1771
+ const hash = await webCrypto.subtle.digest("SHA-256", data);
1772
+ return base64UrlEncode(new Uint8Array(hash));
1773
+ }
1774
+ function storePkceVerifier(verifier) {
1775
+ if (typeof sessionStorage !== "undefined") {
1776
+ sessionStorage.setItem(PKCE_VERIFIER_KEY, verifier);
1406
1777
  }
1407
- // ============================================================================
1408
- // Configuration
1409
- // ============================================================================
1410
- async getPublicAuthConfig() {
1411
- try {
1412
- const response = await this.http.get("/api/auth/public-config", {
1413
- skipAuthRefresh: true
1414
- });
1415
- return { data: response, error: null };
1416
- } catch (error) {
1417
- return wrapError(error, "An unexpected error occurred while fetching auth configuration");
1418
- }
1778
+ }
1779
+ function retrievePkceVerifier() {
1780
+ if (typeof sessionStorage === "undefined") {
1781
+ return null;
1419
1782
  }
1420
- };
1421
- function createInsForgePostgrestFetch(httpClient) {
1422
- return async (input, init) => {
1423
- const url = typeof input === "string" ? input : input.toString();
1424
- const urlObj = new URL(url);
1425
- const pathname = urlObj.pathname.slice(1);
1426
- const rpcMatch = pathname.match(/^rpc\/(.+)$/);
1427
- const endpoint = rpcMatch ? `/api/database/rpc/${rpcMatch[1]}` : `/api/database/records/${pathname}`;
1428
- const insforgeUrl = `${httpClient.baseUrl}${endpoint}${urlObj.search}`;
1429
- const headers = new Headers(httpClient.getHeaders());
1430
- new Headers(init?.headers).forEach((value, key) => {
1431
- headers.set(key, value);
1432
- });
1433
- const response = await httpClient.rawFetch(insforgeUrl, {
1434
- ...init,
1435
- headers
1436
- });
1437
- return response;
1783
+ const verifier = sessionStorage.getItem(PKCE_VERIFIER_KEY);
1784
+ if (verifier) {
1785
+ sessionStorage.removeItem(PKCE_VERIFIER_KEY);
1786
+ }
1787
+ return verifier;
1788
+ }
1789
+ function wrapError(error, fallbackMessage) {
1790
+ if (error instanceof ResultError) {
1791
+ return { data: null, error };
1792
+ }
1793
+ return {
1794
+ data: null,
1795
+ error: new ResultError(
1796
+ error instanceof Error ? error.message : fallbackMessage,
1797
+ 500,
1798
+ "UNEXPECTED_ERROR"
1799
+ )
1438
1800
  };
1439
1801
  }
1440
- var Database = class {
1441
- constructor(httpClient, defaultSchema) {
1442
- this.postgrest = new PostgrestClient("http://dummy", {
1443
- fetch: createInsForgePostgrestFetch(httpClient),
1444
- headers: {},
1445
- ...defaultSchema ? { schema: defaultSchema } : {}
1446
- });
1802
+ function cleanUrlParams(...params) {
1803
+ if (typeof window === "undefined") {
1804
+ return;
1447
1805
  }
1448
- /**
1449
- * Select a non-default Postgres schema for the chained query. Maps to
1450
- * PostgREST's `Accept-Profile` (reads) / `Content-Profile` (writes) header.
1451
- * The schema must be exposed by the backend.
1452
- *
1453
- * @example
1454
- * const { data } = await client.database
1455
- * .schema('analytics')
1456
- * .from('events')
1457
- * .select('*');
1458
- *
1459
- * @example
1460
- * await client.database.schema('analytics').rpc('rollup', { day: '2026-01-01' });
1461
- */
1462
- schema(schemaName) {
1463
- return this.postgrest.schema(schemaName);
1806
+ const url = new URL(window.location.href);
1807
+ for (const p of params) {
1808
+ url.searchParams.delete(p);
1464
1809
  }
1465
- /**
1466
- * Create a query builder for a table
1467
- *
1468
- * @example
1469
- * // Basic query
1470
- * const { data, error } = await client.database
1471
- * .from('posts')
1472
- * .select('*')
1473
- * .eq('user_id', userId);
1474
- *
1475
- * // With count (Supabase style!)
1476
- * const { data, error, count } = await client.database
1477
- * .from('posts')
1478
- * .select('*', { count: 'exact' })
1479
- * .range(0, 9);
1480
- *
1481
- * // Just get count, no data
1482
- * const { count } = await client.database
1483
- * .from('posts')
1484
- * .select('*', { count: 'exact', head: true });
1485
- *
1486
- * // Complex queries with OR
1487
- * const { data } = await client.database
1488
- * .from('posts')
1489
- * .select('*, users!inner(*)')
1490
- * .or('status.eq.active,status.eq.pending');
1491
- *
1492
- * // All features work:
1493
- * - Nested selects
1494
- * - Foreign key expansion
1495
- * - OR/AND/NOT conditions
1496
- * - Count with head
1497
- * - Range pagination
1498
- * - Upserts
1499
- */
1500
- from(table) {
1501
- return this.postgrest.from(table);
1810
+ window.history.replaceState({}, document.title, url.toString());
1811
+ }
1812
+
1813
+ // src/modules/auth/auth.ts
1814
+ var Auth = class {
1815
+ constructor(http, tokenManager, options = {}) {
1816
+ this.http = http;
1817
+ this.tokenManager = tokenManager;
1818
+ this.options = options;
1819
+ this.authCallbackHandled = options.detectOAuthCallback === false ? Promise.resolve() : this.detectAuthCallback();
1820
+ }
1821
+ http;
1822
+ tokenManager;
1823
+ options;
1824
+ authCallbackHandled;
1825
+ isServerMode() {
1826
+ return !!this.options.isServerMode;
1827
+ }
1828
+ /** Subscribe to SDK authentication state changes. */
1829
+ onAuthStateChange(callback) {
1830
+ return this.tokenManager.onAuthStateChange(callback);
1502
1831
  }
1503
1832
  /**
1504
- * Call a PostgreSQL function (RPC)
1505
- *
1506
- * @example
1507
- * // Call a function with parameters
1508
- * const { data, error } = await client.database
1509
- * .rpc('get_user_stats', { user_id: 123 });
1510
- *
1511
- * // Call a function with no parameters
1512
- * const { data, error } = await client.database
1513
- * .rpc('get_all_active_users');
1514
- *
1515
- * // With options (head, count, get)
1516
- * const { data, count } = await client.database
1517
- * .rpc('search_posts', { query: 'hello' }, { count: 'exact' });
1833
+ * Save session from API response
1834
+ * Handles token storage, CSRF token, and HTTP auth header
1518
1835
  */
1519
- rpc(fn, args, options) {
1520
- return this.postgrest.rpc(fn, args, options);
1521
- }
1522
- };
1523
- var StorageBucket = class {
1524
- constructor(bucketName, http) {
1525
- this.bucketName = bucketName;
1526
- this.http = http;
1836
+ saveSessionFromResponse(response, event = AuthChangeEvent.SIGNED_IN) {
1837
+ if (!response.accessToken || !response.user) {
1838
+ return false;
1839
+ }
1840
+ const session = {
1841
+ accessToken: response.accessToken,
1842
+ user: response.user
1843
+ };
1844
+ if (!this.isServerMode() && response.csrfToken) {
1845
+ setCsrfToken(response.csrfToken);
1846
+ }
1847
+ if (!this.isServerMode()) {
1848
+ this.tokenManager.saveSession(session, event);
1849
+ }
1850
+ this.http.setAuthToken(response.accessToken);
1851
+ this.http.setRefreshToken(response.refreshToken ?? null);
1852
+ return true;
1527
1853
  }
1854
+ // ============================================================================
1855
+ // OAuth Callback Detection (runs on initialization)
1856
+ // ============================================================================
1528
1857
  /**
1529
- * Upload a file with a specific key
1530
- * Uses the upload strategy from backend (direct or presigned)
1531
- * @param path - The object key/path
1532
- * @param file - File or Blob to upload
1858
+ * Detect and handle OAuth callback parameters in URL
1859
+ * Supports the PKCE flow (OAuth callback code in the URL)
1533
1860
  */
1534
- async upload(path, file) {
1861
+ async detectAuthCallback() {
1862
+ if (this.isServerMode() || typeof window === "undefined") {
1863
+ return;
1864
+ }
1535
1865
  try {
1536
- const strategyResponse = await this.http.post(
1537
- `/api/storage/buckets/${this.bucketName}/upload-strategy`,
1538
- {
1539
- filename: path,
1540
- contentType: file.type || "application/octet-stream",
1541
- size: file.size
1542
- }
1543
- );
1544
- if (strategyResponse.method === "presigned") {
1545
- return await this.uploadWithPresignedUrl(strategyResponse, file);
1866
+ const params = new URLSearchParams(window.location.search);
1867
+ const error = params.get("error");
1868
+ if (error) {
1869
+ cleanUrlParams("error");
1870
+ console.debug("OAuth callback error:", error);
1871
+ return;
1546
1872
  }
1547
- if (strategyResponse.method === "direct") {
1548
- const formData = new FormData();
1549
- formData.append("file", file);
1550
- const response = await this.http.request(
1551
- "PUT",
1552
- `/api/storage/buckets/${this.bucketName}/objects/${encodeURIComponent(path)}`,
1553
- {
1554
- body: formData,
1555
- headers: {
1556
- // Don't set Content-Type, let browser set multipart boundary
1557
- }
1558
- }
1559
- );
1560
- return { data: response, error: null };
1873
+ const code = params.get("insforge_code");
1874
+ if (code) {
1875
+ cleanUrlParams("insforge_code");
1876
+ const { error: exchangeError } = await this.exchangeOAuthCode(code);
1877
+ if (exchangeError) {
1878
+ console.debug("OAuth code exchange failed:", exchangeError.message);
1879
+ }
1880
+ return;
1561
1881
  }
1562
- throw new InsForgeError(
1563
- `Unsupported upload method: ${strategyResponse.method}`,
1564
- 500,
1565
- "STORAGE_ERROR"
1566
- );
1567
1882
  } catch (error) {
1568
- return {
1569
- data: null,
1570
- error: error instanceof InsForgeError ? error : new InsForgeError("Upload failed", 500, "STORAGE_ERROR")
1571
- };
1883
+ console.debug("OAuth callback detection skipped:", error);
1572
1884
  }
1573
1885
  }
1574
- /**
1575
- * Upload a file with auto-generated key
1576
- * Uses the upload strategy from backend (direct or presigned)
1577
- * @param file - File or Blob to upload
1578
- */
1579
- async uploadAuto(file) {
1886
+ // ============================================================================
1887
+ // Sign Up / Sign In / Sign Out
1888
+ // ============================================================================
1889
+ async signUp(request) {
1580
1890
  try {
1581
- const filename = file instanceof File ? file.name : "file";
1582
- const strategyResponse = await this.http.post(
1583
- `/api/storage/buckets/${this.bucketName}/upload-strategy`,
1584
- {
1585
- filename,
1586
- contentType: file.type || "application/octet-stream",
1587
- size: file.size
1588
- }
1589
- );
1590
- if (strategyResponse.method === "presigned") {
1591
- return await this.uploadWithPresignedUrl(strategyResponse, file);
1592
- }
1593
- if (strategyResponse.method === "direct") {
1594
- const formData = new FormData();
1595
- formData.append("file", file);
1596
- const response = await this.http.request(
1597
- "POST",
1598
- `/api/storage/buckets/${this.bucketName}/objects`,
1599
- {
1600
- body: formData,
1601
- headers: {
1602
- // Don't set Content-Type, let browser set multipart boundary
1603
- }
1604
- }
1605
- );
1606
- return { data: response, error: null };
1607
- }
1608
- throw new InsForgeError(
1609
- `Unsupported upload method: ${strategyResponse.method}`,
1610
- 500,
1611
- "STORAGE_ERROR"
1891
+ const response = await this.http.post(
1892
+ this.isServerMode() ? "/api/auth/users?client_type=mobile" : "/api/auth/users",
1893
+ request,
1894
+ { credentials: "include", skipAuthRefresh: true }
1612
1895
  );
1896
+ if (response.accessToken && response.user) {
1897
+ this.saveSessionFromResponse(response);
1898
+ }
1899
+ if (response.refreshToken) {
1900
+ this.http.setRefreshToken(response.refreshToken);
1901
+ }
1902
+ return { data: response, error: null };
1613
1903
  } catch (error) {
1614
- return {
1615
- data: null,
1616
- error: error instanceof InsForgeError ? error : new InsForgeError("Upload failed", 500, "STORAGE_ERROR")
1617
- };
1904
+ return wrapError(error, "An unexpected error occurred during sign up");
1618
1905
  }
1619
1906
  }
1620
- /**
1621
- * Internal method to handle presigned URL uploads
1622
- */
1623
- async uploadWithPresignedUrl(strategy, file) {
1907
+ async signInWithPassword(request) {
1624
1908
  try {
1625
- const formData = new FormData();
1626
- if (strategy.fields) {
1627
- Object.entries(strategy.fields).forEach(([key, value]) => {
1628
- formData.append(key, value);
1629
- });
1909
+ const response = await this.http.post(
1910
+ this.isServerMode() ? "/api/auth/sessions?client_type=mobile" : "/api/auth/sessions",
1911
+ request,
1912
+ { credentials: "include", skipAuthRefresh: true }
1913
+ );
1914
+ this.saveSessionFromResponse(response);
1915
+ if (response.refreshToken) {
1916
+ this.http.setRefreshToken(response.refreshToken);
1630
1917
  }
1631
- formData.append("file", file);
1632
- const uploadResponse = await fetch(strategy.uploadUrl, {
1633
- method: "POST",
1634
- body: formData
1635
- });
1636
- if (!uploadResponse.ok) {
1637
- throw new InsForgeError(
1638
- `Upload to storage failed: ${uploadResponse.statusText}`,
1639
- uploadResponse.status,
1640
- "STORAGE_ERROR"
1918
+ return { data: response, error: null };
1919
+ } catch (error) {
1920
+ return wrapError(error, "An unexpected error occurred during sign in");
1921
+ }
1922
+ }
1923
+ async signOut() {
1924
+ try {
1925
+ try {
1926
+ const serverMode = this.isServerMode();
1927
+ const csrfToken = !serverMode ? getCsrfToken() : null;
1928
+ await this.http.post(
1929
+ serverMode ? "/api/auth/logout?client_type=mobile" : "/api/auth/logout",
1930
+ void 0,
1931
+ {
1932
+ credentials: "include",
1933
+ skipAuthRefresh: true,
1934
+ ...csrfToken ? { headers: { "X-CSRF-Token": csrfToken } } : {}
1935
+ }
1641
1936
  );
1937
+ } catch {
1642
1938
  }
1643
- if (strategy.confirmRequired && strategy.confirmUrl) {
1644
- const confirmResponse = await this.http.post(strategy.confirmUrl, {
1645
- size: file.size,
1646
- contentType: file.type || "application/octet-stream"
1647
- });
1648
- return { data: confirmResponse, error: null };
1939
+ this.tokenManager.clearSession();
1940
+ this.http.setAuthToken(null);
1941
+ this.http.setRefreshToken(null);
1942
+ if (!this.isServerMode()) {
1943
+ clearCsrfToken();
1649
1944
  }
1945
+ return { error: null };
1946
+ } catch {
1650
1947
  return {
1651
- data: {
1652
- key: strategy.key,
1653
- bucket: this.bucketName,
1654
- size: file.size,
1655
- mimeType: file.type || "application/octet-stream",
1656
- uploadedAt: (/* @__PURE__ */ new Date()).toISOString(),
1657
- url: this.getPublicUrl(strategy.key).data.publicUrl
1658
- },
1659
- error: null
1948
+ error: new ResultError("Failed to sign out", 500, "SIGNOUT_ERROR")
1660
1949
  };
1661
- } catch (error) {
1662
- throw error instanceof InsForgeError ? error : new InsForgeError("Presigned upload failed", 500, "STORAGE_ERROR");
1663
1950
  }
1664
1951
  }
1665
- /**
1666
- * Download a file
1667
- * Uses the download strategy from backend (direct or presigned)
1668
- * @param path - The object key/path
1669
- * Returns the file as a Blob
1670
- */
1671
- async download(path) {
1952
+ async signInWithOAuth(providerOrOptions, options) {
1672
1953
  try {
1673
- const encodedKey = encodeURIComponent(path);
1674
- let strategyResponse;
1675
- try {
1676
- strategyResponse = await this.http.get(
1677
- `/api/storage/buckets/${this.bucketName}/download-strategy/objects/${encodedKey}`
1678
- );
1679
- } catch (err) {
1680
- const status = err instanceof InsForgeError ? err.statusCode : void 0;
1681
- if (status === 404 || status === 405) {
1682
- strategyResponse = await this.http.post(
1683
- `/api/storage/buckets/${this.bucketName}/objects/${encodedKey}/download-strategy`,
1684
- {}
1685
- );
1686
- } else {
1687
- throw err;
1688
- }
1954
+ let signInOptions;
1955
+ if (typeof providerOrOptions === "object") {
1956
+ signInOptions = providerOrOptions;
1957
+ } else if (options) {
1958
+ signInOptions = { provider: providerOrOptions, ...options };
1959
+ } else {
1960
+ return {
1961
+ data: {},
1962
+ error: new ResultError(
1963
+ "OAuth sign-in options are required",
1964
+ 400,
1965
+ ERROR_CODES.INVALID_INPUT
1966
+ )
1967
+ };
1689
1968
  }
1690
- const downloadUrl = strategyResponse.url;
1691
- const headers = {};
1692
- if (strategyResponse.method === "direct") {
1693
- Object.assign(headers, this.http.getHeaders());
1969
+ if (!signInOptions || !signInOptions.redirectTo) {
1970
+ return {
1971
+ data: {},
1972
+ error: new ResultError(
1973
+ "Redirect URI is required",
1974
+ 400,
1975
+ ERROR_CODES.INVALID_INPUT
1976
+ )
1977
+ };
1694
1978
  }
1695
- const response = await fetch(downloadUrl, {
1696
- method: "GET",
1697
- headers
1979
+ const { provider } = signInOptions;
1980
+ const providerKey = encodeURIComponent(provider.toLowerCase());
1981
+ const codeVerifier = await generateCodeVerifier();
1982
+ const codeChallenge = await generateCodeChallenge(codeVerifier);
1983
+ storePkceVerifier(codeVerifier);
1984
+ const params = {
1985
+ ...signInOptions.additionalParams ?? {},
1986
+ redirect_uri: signInOptions.redirectTo,
1987
+ code_challenge: codeChallenge
1988
+ };
1989
+ const isBuiltInProvider = OAUTH_PROVIDERS.includes(
1990
+ providerKey
1991
+ );
1992
+ const oauthPath = isBuiltInProvider ? `/api/auth/oauth/${providerKey}` : `/api/auth/oauth/custom/${providerKey}`;
1993
+ const response = await this.http.get(oauthPath, {
1994
+ params,
1995
+ skipAuthRefresh: true
1698
1996
  });
1699
- if (!response.ok) {
1700
- try {
1701
- const error = await response.json();
1702
- throw InsForgeError.fromApiError(error);
1703
- } catch {
1704
- throw new InsForgeError(
1705
- `Download failed: ${response.statusText}`,
1706
- response.status,
1707
- "STORAGE_ERROR"
1708
- );
1709
- }
1997
+ if (!this.isServerMode() && typeof window !== "undefined" && !signInOptions.skipBrowserRedirect) {
1998
+ window.location.href = response.authUrl;
1999
+ return { data: {}, error: null };
1710
2000
  }
1711
- const blob = await response.blob();
1712
- return { data: blob, error: null };
2001
+ return {
2002
+ data: { url: response.authUrl, provider: providerKey, codeVerifier },
2003
+ error: null
2004
+ };
1713
2005
  } catch (error) {
2006
+ if (error instanceof ResultError) {
2007
+ return { data: {}, error };
2008
+ }
1714
2009
  return {
1715
- data: null,
1716
- error: error instanceof InsForgeError ? error : new InsForgeError("Download failed", 500, "STORAGE_ERROR")
2010
+ data: {},
2011
+ error: new ResultError(
2012
+ "An unexpected error occurred during OAuth initialization",
2013
+ 500,
2014
+ "UNEXPECTED_ERROR"
2015
+ )
1717
2016
  };
1718
2017
  }
1719
2018
  }
1720
2019
  /**
1721
- * Get the public URL for an object in a public bucket.
1722
- *
1723
- * Pure string construction — no network call, no auth. The URL only resolves
1724
- * if the bucket is public; for private objects use {@link createSignedUrl}.
1725
- *
1726
- * @param path - The object key/path
1727
- * @returns `{ data: { publicUrl }, error }` — matches the external SDK pattern,
1728
- * so `const { data } = getPublicUrl(path)` then `data.publicUrl`.
1729
- */
1730
- getPublicUrl(path) {
1731
- const publicUrl = `${this.http.baseUrl}/api/storage/buckets/${this.bucketName}/objects/${encodeURIComponent(path)}`;
1732
- return { data: { publicUrl }, error: null };
1733
- }
1734
- /**
1735
- * Resolve a download strategy (signed or direct URL) for an object with a
1736
- * caller-supplied TTL. Prefers the canonical GET route and falls back to the
1737
- * legacy POST alias so signed-URL creation still works against older backends
1738
- * that predate the GET route (they return 404/405 for it). A genuine
1739
- * "object not found" (STORAGE_NOT_FOUND) is not retried.
2020
+ * Exchange OAuth authorization code for tokens (PKCE flow)
2021
+ * Called automatically on initialization when the OAuth callback code is in the URL
1740
2022
  */
1741
- async requestDownloadStrategy(path, expiresIn) {
1742
- const encoded = encodeURIComponent(path);
2023
+ async exchangeOAuthCode(code, codeVerifier) {
1743
2024
  try {
1744
- return await this.http.get(
1745
- `/api/storage/buckets/${this.bucketName}/download-strategy/objects/${encoded}`,
1746
- { params: { expiresIn: expiresIn.toString() } }
2025
+ const verifier = codeVerifier ?? retrievePkceVerifier();
2026
+ if (!verifier) {
2027
+ return {
2028
+ data: null,
2029
+ error: new ResultError(
2030
+ "PKCE code verifier not found. Ensure signInWithOAuth was called in the same browser session.",
2031
+ 400,
2032
+ "PKCE_VERIFIER_MISSING"
2033
+ )
2034
+ };
2035
+ }
2036
+ const request = {
2037
+ code,
2038
+ code_verifier: verifier
2039
+ };
2040
+ const response = await this.http.post(
2041
+ this.isServerMode() ? "/api/auth/oauth/exchange?client_type=mobile" : "/api/auth/oauth/exchange",
2042
+ request,
2043
+ { credentials: "include", skipAuthRefresh: true }
1747
2044
  );
2045
+ this.saveSessionFromResponse(response);
2046
+ return {
2047
+ data: response,
2048
+ error: null
2049
+ };
1748
2050
  } catch (error) {
1749
- const status = error instanceof InsForgeError ? error.statusCode : void 0;
1750
- const isMissingRoute = (status === 404 || status === 405) && !(error instanceof InsForgeError && error.error === "STORAGE_NOT_FOUND");
1751
- if (!isMissingRoute) {
1752
- throw error;
1753
- }
1754
- return await this.http.post(
1755
- `/api/storage/buckets/${this.bucketName}/objects/${encoded}/download-strategy`,
1756
- { expiresIn }
2051
+ return wrapError(
2052
+ error,
2053
+ "An unexpected error occurred during OAuth code exchange"
1757
2054
  );
1758
2055
  }
1759
2056
  }
1760
2057
  /**
1761
- * Create a signed URL for an object.
1762
- *
1763
- * Returns a time-limited, credential-free URL that can be handed directly to
1764
- * a browser (`<img src>`), an email, or a third party — no SDK or session is
1765
- * needed to fetch it. Authorization is enforced when the URL is minted (the
1766
- * caller must be allowed to read the object), so the resulting link is a
1767
- * pre-authorized capability scoped to this one object until it expires.
2058
+ * Sign in with an ID token from a native SDK (Google One Tap, etc.)
2059
+ * Use this for native mobile apps or Google One Tap on web.
1768
2060
  *
1769
- * @param path - The object key/path
1770
- * @param expiresIn - Lifetime in seconds (default 3600 = 1h, max 604800 = 7d).
1771
- * Honored for private buckets; public buckets return their long-lived URL.
2061
+ * @param credentials.provider - The identity provider (currently only 'google' is supported)
2062
+ * @param credentials.token - The ID token from the native SDK
1772
2063
  */
1773
- async createSignedUrl(path, expiresIn = 3600) {
2064
+ async signInWithIdToken(credentials) {
1774
2065
  try {
1775
- const strategy = await this.requestDownloadStrategy(path, expiresIn);
2066
+ const { provider, token } = credentials;
2067
+ const response = await this.http.post(
2068
+ "/api/auth/id-token?client_type=mobile",
2069
+ { provider, token },
2070
+ { credentials: "include", skipAuthRefresh: true }
2071
+ );
2072
+ this.saveSessionFromResponse(response);
2073
+ if (response.refreshToken) {
2074
+ this.http.setRefreshToken(response.refreshToken);
2075
+ }
1776
2076
  return {
1777
- data: {
1778
- signedUrl: strategy.url,
1779
- expiresAt: strategy.expiresAt ? new Date(strategy.expiresAt).toISOString() : null
1780
- },
2077
+ data: response,
1781
2078
  error: null
1782
2079
  };
1783
2080
  } catch (error) {
1784
- return {
1785
- data: null,
1786
- error: error instanceof InsForgeError ? error : new InsForgeError("Failed to create signed URL", 500, "STORAGE_ERROR")
1787
- };
2081
+ return wrapError(
2082
+ error,
2083
+ "An unexpected error occurred during ID token sign in"
2084
+ );
1788
2085
  }
1789
2086
  }
2087
+ // ============================================================================
2088
+ // Session Management
2089
+ // ============================================================================
1790
2090
  /**
1791
- * Create signed URLs for multiple objects in a single call.
2091
+ * Refresh the current auth session.
1792
2092
  *
1793
- * Each entry resolves independently: a failure on one key (not found / not
1794
- * permitted) is reported on that entry's `error` without failing the rest.
2093
+ * Browser mode:
2094
+ * - Uses httpOnly refresh cookie and optional CSRF header.
1795
2095
  *
1796
- * @param paths - The object keys/paths
1797
- * @param expiresIn - Lifetime in seconds (default 3600 = 1h, max 604800 = 7d)
2096
+ * Legacy server mode (`isServerMode: true`):
2097
+ * - Uses mobile auth flow and requires `refreshToken` in request body.
2098
+ *
2099
+ * SSR apps should prefer `createRefreshAuthRouter()` / `refreshAuth()` from
2100
+ * `@resultdev/sdk/ssr`.
1798
2101
  */
1799
- async createSignedUrls(paths, expiresIn = 3600) {
2102
+ async refreshSession(options) {
1800
2103
  try {
1801
- const data = await Promise.all(
1802
- paths.map(async (path) => {
1803
- const { data: signed, error } = await this.createSignedUrl(path, expiresIn);
1804
- return {
1805
- path,
1806
- signedUrl: signed?.signedUrl ?? null,
1807
- error: error ? error.message : null
1808
- };
1809
- })
2104
+ if (this.isServerMode() && !options?.refreshToken) {
2105
+ return {
2106
+ data: null,
2107
+ error: new ResultError(
2108
+ "refreshToken is required when refreshing session in server mode",
2109
+ 400,
2110
+ ERROR_CODES.AUTH_UNAUTHORIZED
2111
+ )
2112
+ };
2113
+ }
2114
+ const csrfToken = !this.isServerMode() ? getCsrfToken() : null;
2115
+ const response = await this.http.post(
2116
+ this.isServerMode() ? "/api/auth/refresh?client_type=mobile" : "/api/auth/refresh",
2117
+ this.isServerMode() ? { refresh_token: options?.refreshToken } : void 0,
2118
+ {
2119
+ headers: csrfToken ? { "X-CSRF-Token": csrfToken } : {},
2120
+ credentials: "include",
2121
+ skipAuthRefresh: true
2122
+ }
1810
2123
  );
1811
- return { data, error: null };
2124
+ if (response.accessToken) {
2125
+ this.saveSessionFromResponse(response, AuthChangeEvent.TOKEN_REFRESHED);
2126
+ }
2127
+ return { data: response, error: null };
1812
2128
  } catch (error) {
1813
- return {
1814
- data: null,
1815
- error: error instanceof InsForgeError ? error : new InsForgeError("Failed to create signed URLs", 500, "STORAGE_ERROR")
1816
- };
2129
+ return wrapError(
2130
+ error,
2131
+ "An unexpected error occurred during session refresh"
2132
+ );
1817
2133
  }
1818
2134
  }
1819
2135
  /**
1820
- * List objects in the bucket
1821
- * @param prefix - Filter by key prefix
1822
- * @param search - Search in file names
1823
- * @param limit - Maximum number of results (default: 100, max: 1000)
1824
- * @param offset - Number of results to skip
2136
+ * Get current user, automatically waits for pending OAuth callback
1825
2137
  */
1826
- async list(options) {
2138
+ async getCurrentUser() {
2139
+ await this.authCallbackHandled;
1827
2140
  try {
1828
- const params = {};
1829
- if (options?.prefix) {
1830
- params.prefix = options.prefix;
2141
+ if (this.isServerMode()) {
2142
+ const accessToken = this.tokenManager.getAccessToken();
2143
+ if (!accessToken) {
2144
+ return { data: { user: null }, error: null };
2145
+ }
2146
+ this.http.setAuthToken(accessToken);
2147
+ const response = await this.http.get(
2148
+ "/api/auth/sessions/current"
2149
+ );
2150
+ const user = response.user ?? null;
2151
+ return { data: { user }, error: null };
1831
2152
  }
1832
- if (options?.search) {
1833
- params.search = options.search;
2153
+ const session = this.tokenManager.getSession();
2154
+ if (session) {
2155
+ this.http.setAuthToken(session.accessToken);
2156
+ return { data: { user: session.user }, error: null };
1834
2157
  }
1835
- if (options?.limit) {
1836
- params.limit = options.limit.toString();
2158
+ if (typeof window !== "undefined") {
2159
+ const { data: refreshed, error: refreshError } = await this.refreshSession();
2160
+ if (refreshError) {
2161
+ return { data: { user: null }, error: refreshError };
2162
+ }
2163
+ if (refreshed?.accessToken) {
2164
+ return { data: { user: refreshed.user ?? null }, error: null };
2165
+ }
1837
2166
  }
1838
- if (options?.offset) {
1839
- params.offset = options.offset.toString();
2167
+ return { data: { user: null }, error: null };
2168
+ } catch (error) {
2169
+ if (error instanceof ResultError) {
2170
+ return { data: { user: null }, error };
1840
2171
  }
2172
+ return {
2173
+ data: { user: null },
2174
+ error: new ResultError(
2175
+ "An unexpected error occurred while getting user",
2176
+ 500,
2177
+ "UNEXPECTED_ERROR"
2178
+ )
2179
+ };
2180
+ }
2181
+ }
2182
+ // ============================================================================
2183
+ // Profile Management
2184
+ // ============================================================================
2185
+ async getProfile(userId) {
2186
+ try {
1841
2187
  const response = await this.http.get(
1842
- `/api/storage/buckets/${this.bucketName}/objects`,
1843
- { params }
2188
+ `/api/auth/profiles/${userId}`
1844
2189
  );
1845
2190
  return { data: response, error: null };
1846
2191
  } catch (error) {
1847
- return {
1848
- data: null,
1849
- error: error instanceof InsForgeError ? error : new InsForgeError("List failed", 500, "STORAGE_ERROR")
1850
- };
2192
+ return wrapError(
2193
+ error,
2194
+ "An unexpected error occurred while fetching user profile"
2195
+ );
1851
2196
  }
1852
2197
  }
1853
- /**
1854
- * Delete a file
1855
- * @param path - The object key/path
1856
- */
1857
- async remove(path) {
2198
+ async setProfile(profile) {
1858
2199
  try {
1859
- const response = await this.http.delete(
1860
- `/api/storage/buckets/${this.bucketName}/objects/${encodeURIComponent(path)}`
2200
+ const response = await this.http.patch(
2201
+ "/api/auth/profiles/current",
2202
+ {
2203
+ profile
2204
+ }
1861
2205
  );
2206
+ const currentUser = this.tokenManager.getUser();
2207
+ if (!this.isServerMode() && currentUser && response.profile !== void 0) {
2208
+ this.tokenManager.setUser({
2209
+ ...currentUser,
2210
+ profile: response.profile
2211
+ });
2212
+ }
1862
2213
  return { data: response, error: null };
1863
2214
  } catch (error) {
1864
- return {
1865
- data: null,
1866
- error: error instanceof InsForgeError ? error : new InsForgeError("Delete failed", 500, "STORAGE_ERROR")
1867
- };
2215
+ return wrapError(
2216
+ error,
2217
+ "An unexpected error occurred while updating user profile"
2218
+ );
1868
2219
  }
1869
2220
  }
1870
- };
1871
- var Storage = class {
1872
- constructor(http) {
1873
- this.http = http;
2221
+ // ============================================================================
2222
+ // Email Verification
2223
+ // ============================================================================
2224
+ async resendVerificationEmail(request) {
2225
+ try {
2226
+ const response = await this.http.post("/api/auth/email/send-verification", request, {
2227
+ skipAuthRefresh: true
2228
+ });
2229
+ return { data: response, error: null };
2230
+ } catch (error) {
2231
+ return wrapError(
2232
+ error,
2233
+ "An unexpected error occurred while sending verification email"
2234
+ );
2235
+ }
1874
2236
  }
1875
- /**
1876
- * Get a bucket instance for operations
1877
- * @param bucketName - Name of the bucket
1878
- */
1879
- from(bucketName) {
1880
- return new StorageBucket(bucketName, this.http);
2237
+ async verifyEmail(request) {
2238
+ try {
2239
+ const response = await this.http.post(
2240
+ this.isServerMode() ? "/api/auth/email/verify?client_type=mobile" : "/api/auth/email/verify",
2241
+ request,
2242
+ { credentials: "include", skipAuthRefresh: true }
2243
+ );
2244
+ this.saveSessionFromResponse(response);
2245
+ if (response.refreshToken) {
2246
+ this.http.setRefreshToken(response.refreshToken);
2247
+ }
2248
+ return { data: response, error: null };
2249
+ } catch (error) {
2250
+ return wrapError(
2251
+ error,
2252
+ "An unexpected error occurred while verifying email"
2253
+ );
2254
+ }
1881
2255
  }
1882
- };
1883
- var AI = class {
1884
- constructor(http) {
1885
- this.http = http;
1886
- this.chat = new Chat(http);
1887
- this.images = new Images(http);
1888
- this.embeddings = new Embeddings(http);
2256
+ // ============================================================================
2257
+ // Password Reset
2258
+ // ============================================================================
2259
+ async sendResetPasswordEmail(request) {
2260
+ try {
2261
+ const response = await this.http.post("/api/auth/email/send-reset-password", request, {
2262
+ skipAuthRefresh: true
2263
+ });
2264
+ return { data: response, error: null };
2265
+ } catch (error) {
2266
+ return wrapError(
2267
+ error,
2268
+ "An unexpected error occurred while sending password reset email"
2269
+ );
2270
+ }
1889
2271
  }
1890
- };
1891
- var Chat = class {
1892
- constructor(http) {
1893
- this.completions = new ChatCompletions(http);
2272
+ async exchangeResetPasswordToken(request) {
2273
+ try {
2274
+ const response = await this.http.post(
2275
+ "/api/auth/email/exchange-reset-password-token",
2276
+ request,
2277
+ { skipAuthRefresh: true }
2278
+ );
2279
+ return { data: response, error: null };
2280
+ } catch (error) {
2281
+ return wrapError(
2282
+ error,
2283
+ "An unexpected error occurred while verifying reset code"
2284
+ );
2285
+ }
2286
+ }
2287
+ async resetPassword(request) {
2288
+ try {
2289
+ const response = await this.http.post(
2290
+ "/api/auth/email/reset-password",
2291
+ request,
2292
+ { skipAuthRefresh: true }
2293
+ );
2294
+ return { data: response, error: null };
2295
+ } catch (error) {
2296
+ return wrapError(
2297
+ error,
2298
+ "An unexpected error occurred while resetting password"
2299
+ );
2300
+ }
2301
+ }
2302
+ // ============================================================================
2303
+ // Configuration
2304
+ // ============================================================================
2305
+ async getPublicAuthConfig() {
2306
+ try {
2307
+ const response = await this.http.get(
2308
+ "/api/auth/public-config",
2309
+ {
2310
+ skipAuthRefresh: true
2311
+ }
2312
+ );
2313
+ return { data: response, error: null };
2314
+ } catch (error) {
2315
+ return wrapError(
2316
+ error,
2317
+ "An unexpected error occurred while fetching auth configuration"
2318
+ );
2319
+ }
1894
2320
  }
1895
2321
  };
1896
- var ChatCompletions = class {
1897
- constructor(http) {
1898
- this.http = http;
2322
+
2323
+ // src/modules/database-postgrest.ts
2324
+ import { PostgrestClient } from "@supabase/postgrest-js";
2325
+ function createResultPostgrestFetch(httpClient) {
2326
+ return async (input, init) => {
2327
+ const url = typeof input === "string" ? input : input.toString();
2328
+ const urlObj = new URL(url);
2329
+ const pathname = urlObj.pathname.slice(1);
2330
+ const rpcMatch = pathname.match(/^rpc\/(.+)$/);
2331
+ const endpoint = rpcMatch ? `/api/database/rpc/${rpcMatch[1]}` : `/api/database/records/${pathname}`;
2332
+ const backendUrl = `${httpClient.baseUrl}${endpoint}${urlObj.search}`;
2333
+ const headers = new Headers(httpClient.getHeaders());
2334
+ new Headers(init?.headers).forEach((value, key) => {
2335
+ headers.set(key, value);
2336
+ });
2337
+ const response = await httpClient.rawFetch(backendUrl, {
2338
+ ...init,
2339
+ headers
2340
+ });
2341
+ return response;
2342
+ };
2343
+ }
2344
+ var Database = class {
2345
+ postgrest;
2346
+ constructor(httpClient, defaultSchema) {
2347
+ this.postgrest = new PostgrestClient("http://dummy", {
2348
+ fetch: createResultPostgrestFetch(httpClient),
2349
+ headers: {},
2350
+ ...defaultSchema ? { schema: defaultSchema } : {}
2351
+ });
1899
2352
  }
1900
2353
  /**
1901
- * Create a chat completion - OpenAI-like response format
2354
+ * Select a non-default Postgres schema for the chained query. Maps to
2355
+ * PostgREST's `Accept-Profile` (reads) / `Content-Profile` (writes) header.
2356
+ * The schema must be exposed by the backend.
1902
2357
  *
1903
2358
  * @example
1904
- * ```typescript
1905
- * // Non-streaming
1906
- * const completion = await client.ai.chat.completions.create({
1907
- * model: 'gpt-4',
1908
- * messages: [{ role: 'user', content: 'Hello!' }]
1909
- * });
1910
- * console.log(completion.choices[0].message.content);
2359
+ * const { data } = await client.database
2360
+ * .schema('analytics')
2361
+ * .from('events')
2362
+ * .select('*');
1911
2363
  *
1912
- * // With images (OpenAI-compatible format)
1913
- * const response = await client.ai.chat.completions.create({
1914
- * model: 'gpt-4-vision',
1915
- * messages: [{
1916
- * role: 'user',
1917
- * content: [
1918
- * { type: 'text', text: 'What is in this image?' },
1919
- * { type: 'image_url', image_url: { url: 'https://example.com/image.jpg' } }
1920
- * ]
1921
- * }]
1922
- * });
2364
+ * @example
2365
+ * await client.database.schema('analytics').rpc('rollup', { day: '2026-01-01' });
2366
+ */
2367
+ schema(schemaName) {
2368
+ return this.postgrest.schema(schemaName);
2369
+ }
2370
+ /**
2371
+ * Create a query builder for a table
1923
2372
  *
1924
- * // With PDF files
1925
- * const pdfResponse = await client.ai.chat.completions.create({
1926
- * model: 'anthropic/claude-3.5-sonnet',
1927
- * messages: [{
1928
- * role: 'user',
1929
- * content: [
1930
- * { type: 'text', text: 'Summarize this document' },
1931
- * { type: 'file', file: { filename: 'doc.pdf', file_data: 'https://example.com/doc.pdf' } }
1932
- * ]
1933
- * }],
1934
- * fileParser: { enabled: true, pdf: { engine: 'mistral-ocr' } }
1935
- * });
2373
+ * @example
2374
+ * // Basic query
2375
+ * const { data, error } = await client.database
2376
+ * .from('posts')
2377
+ * .select('*')
2378
+ * .eq('user_id', userId);
1936
2379
  *
1937
- * // With web search
1938
- * const searchResponse = await client.ai.chat.completions.create({
1939
- * model: 'openai/gpt-4',
1940
- * messages: [{ role: 'user', content: 'What are the latest news about AI?' }],
1941
- * webSearch: { enabled: true, maxResults: 5 }
1942
- * });
1943
- * // Access citations from response.choices[0].message.annotations
2380
+ * // With count (Supabase style!)
2381
+ * const { data, error, count } = await client.database
2382
+ * .from('posts')
2383
+ * .select('*', { count: 'exact' })
2384
+ * .range(0, 9);
1944
2385
  *
1945
- * // With thinking/reasoning mode (Anthropic models)
1946
- * const thinkingResponse = await client.ai.chat.completions.create({
1947
- * model: 'anthropic/claude-3.5-sonnet',
1948
- * messages: [{ role: 'user', content: 'Solve this complex math problem...' }],
1949
- * thinking: true
1950
- * });
2386
+ * // Just get count, no data
2387
+ * const { count } = await client.database
2388
+ * .from('posts')
2389
+ * .select('*', { count: 'exact', head: true });
1951
2390
  *
1952
- * // Streaming - returns async iterable
1953
- * const stream = await client.ai.chat.completions.create({
1954
- * model: 'gpt-4',
1955
- * messages: [{ role: 'user', content: 'Tell me a story' }],
1956
- * stream: true
1957
- * });
2391
+ * // Complex queries with OR
2392
+ * const { data } = await client.database
2393
+ * .from('posts')
2394
+ * .select('*, users!inner(*)')
2395
+ * .or('status.eq.active,status.eq.pending');
1958
2396
  *
1959
- * for await (const chunk of stream) {
1960
- * if (chunk.choices[0]?.delta?.content) {
1961
- * process.stdout.write(chunk.choices[0].delta.content);
1962
- * }
1963
- * }
1964
- * ```
1965
- */
1966
- async create(params) {
1967
- const backendParams = {
1968
- model: params.model,
1969
- messages: params.messages,
1970
- temperature: params.temperature,
1971
- maxTokens: params.maxTokens,
1972
- topP: params.topP,
1973
- stream: params.stream,
1974
- // New plugin options
1975
- webSearch: params.webSearch,
1976
- fileParser: params.fileParser,
1977
- thinking: params.thinking,
1978
- // Tool calling options
1979
- tools: params.tools,
1980
- toolChoice: params.toolChoice,
1981
- parallelToolCalls: params.parallelToolCalls
1982
- };
1983
- if (params.stream) {
1984
- const headers = this.http.getHeaders();
1985
- headers["Content-Type"] = "application/json";
1986
- const response2 = await this.http.fetch(`${this.http.baseUrl}/api/ai/chat/completion`, {
1987
- method: "POST",
1988
- headers,
1989
- body: JSON.stringify(backendParams)
1990
- });
1991
- if (!response2.ok) {
1992
- const error = await response2.json();
1993
- throw new Error(error.error || "Stream request failed");
1994
- }
1995
- return this.parseSSEStream(response2, params.model);
1996
- }
1997
- const response = await this.http.post(
1998
- "/api/ai/chat/completion",
1999
- backendParams
2000
- );
2001
- const content = response.text || "";
2002
- return {
2003
- id: `chatcmpl-${Date.now()}`,
2004
- object: "chat.completion",
2005
- created: Math.floor(Date.now() / 1e3),
2006
- model: response.metadata?.model,
2007
- choices: [
2008
- {
2009
- index: 0,
2010
- message: {
2011
- role: "assistant",
2012
- content,
2013
- // Include tool_calls if present (from tool calling)
2014
- ...response.tool_calls?.length && { tool_calls: response.tool_calls },
2015
- // Include annotations if present (from web search or file parsing)
2016
- ...response.annotations?.length && { annotations: response.annotations }
2017
- },
2018
- finish_reason: response.tool_calls?.length ? "tool_calls" : "stop"
2019
- }
2020
- ],
2021
- usage: response.metadata?.usage || {
2022
- prompt_tokens: 0,
2023
- completion_tokens: 0,
2024
- total_tokens: 0
2025
- }
2026
- };
2027
- }
2028
- /**
2029
- * Parse SSE stream into async iterable of OpenAI-like chunks
2397
+ * // All features work:
2398
+ * - Nested selects
2399
+ * - Foreign key expansion
2400
+ * - OR/AND/NOT conditions
2401
+ * - Count with head
2402
+ * - Range pagination
2403
+ * - Upserts
2030
2404
  */
2031
- async *parseSSEStream(response, model) {
2032
- const reader = response.body.getReader();
2033
- const decoder = new TextDecoder();
2034
- let buffer = "";
2035
- try {
2036
- while (true) {
2037
- const { done, value } = await reader.read();
2038
- if (done) {
2039
- break;
2040
- }
2041
- buffer += decoder.decode(value, { stream: true });
2042
- const lines = buffer.split("\n");
2043
- buffer = lines.pop() || "";
2044
- for (const line of lines) {
2045
- if (line.startsWith("data: ")) {
2046
- const dataStr = line.slice(6).trim();
2047
- if (dataStr) {
2048
- try {
2049
- const data = JSON.parse(dataStr);
2050
- if (data.chunk || data.content) {
2051
- yield {
2052
- id: `chatcmpl-${Date.now()}`,
2053
- object: "chat.completion.chunk",
2054
- created: Math.floor(Date.now() / 1e3),
2055
- model,
2056
- choices: [
2057
- {
2058
- index: 0,
2059
- delta: {
2060
- content: data.chunk || data.content
2061
- },
2062
- finish_reason: null
2063
- }
2064
- ]
2065
- };
2066
- }
2067
- if (data.tool_calls?.length) {
2068
- yield {
2069
- id: `chatcmpl-${Date.now()}`,
2070
- object: "chat.completion.chunk",
2071
- created: Math.floor(Date.now() / 1e3),
2072
- model,
2073
- choices: [
2074
- {
2075
- index: 0,
2076
- delta: {
2077
- tool_calls: data.tool_calls
2078
- },
2079
- finish_reason: "tool_calls"
2080
- }
2081
- ]
2082
- };
2083
- }
2084
- if (data.done) {
2085
- reader.releaseLock();
2086
- return;
2087
- }
2088
- } catch {
2089
- console.warn("Failed to parse SSE data:", dataStr);
2090
- }
2091
- }
2092
- }
2093
- }
2094
- }
2095
- } finally {
2096
- reader.releaseLock();
2097
- }
2098
- }
2099
- };
2100
- var Embeddings = class {
2101
- constructor(http) {
2102
- this.http = http;
2405
+ from(table) {
2406
+ return this.postgrest.from(table);
2103
2407
  }
2104
2408
  /**
2105
- * Create embeddings for text input - OpenAI-like response format
2409
+ * Call a PostgreSQL function (RPC)
2106
2410
  *
2107
2411
  * @example
2108
- * ```typescript
2109
- * // Single text input
2110
- * const response = await client.ai.embeddings.create({
2111
- * model: 'openai/text-embedding-3-small',
2112
- * input: 'Hello world'
2113
- * });
2114
- * console.log(response.data[0].embedding); // number[]
2115
- *
2116
- * // Multiple text inputs
2117
- * const response = await client.ai.embeddings.create({
2118
- * model: 'openai/text-embedding-3-small',
2119
- * input: ['Hello world', 'Goodbye world']
2120
- * });
2121
- * response.data.forEach((item, i) => {
2122
- * console.log(`Embedding ${i}:`, item.embedding.slice(0, 5)); // First 5 dimensions
2123
- * });
2124
- *
2125
- * // With custom dimensions (if supported by model)
2126
- * const response = await client.ai.embeddings.create({
2127
- * model: 'openai/text-embedding-3-small',
2128
- * input: 'Hello world',
2129
- * dimensions: 256
2130
- * });
2412
+ * // Call a function with parameters
2413
+ * const { data, error } = await client.database
2414
+ * .rpc('get_user_stats', { user_id: 123 });
2131
2415
  *
2132
- * // With base64 encoding format
2133
- * const response = await client.ai.embeddings.create({
2134
- * model: 'openai/text-embedding-3-small',
2135
- * input: 'Hello world',
2136
- * encoding_format: 'base64'
2137
- * });
2138
- * ```
2139
- */
2140
- async create(params) {
2141
- const response = await this.http.post("/api/ai/embeddings", params);
2142
- return {
2143
- object: response.object,
2144
- data: response.data,
2145
- model: response.metadata?.model,
2146
- usage: response.metadata?.usage ? {
2147
- prompt_tokens: response.metadata.usage.promptTokens || 0,
2148
- total_tokens: response.metadata.usage.totalTokens || 0
2149
- } : {
2150
- prompt_tokens: 0,
2151
- total_tokens: 0
2152
- }
2153
- };
2416
+ * // Call a function with no parameters
2417
+ * const { data, error } = await client.database
2418
+ * .rpc('get_all_active_users');
2419
+ *
2420
+ * // With options (head, count, get)
2421
+ * const { data, count } = await client.database
2422
+ * .rpc('search_posts', { query: 'hello' }, { count: 'exact' });
2423
+ */
2424
+ rpc(fn, args, options) {
2425
+ return this.postgrest.rpc(fn, args, options);
2154
2426
  }
2155
2427
  };
2156
- var Images = class {
2428
+
2429
+ // src/modules/email.ts
2430
+ var Emails = class {
2431
+ http;
2157
2432
  constructor(http) {
2158
2433
  this.http = http;
2159
2434
  }
2160
2435
  /**
2161
- * Generate images - OpenAI-like response format
2162
- *
2163
- * @example
2164
- * ```typescript
2165
- * // Text-to-image
2166
- * const response = await client.ai.images.generate({
2167
- * model: 'dall-e-3',
2168
- * prompt: 'A sunset over mountains',
2169
- * });
2170
- * console.log(response.data[0].b64_json);
2171
- *
2172
- * // Image-to-image (with input images)
2173
- * const response = await client.ai.images.generate({
2174
- * model: 'stable-diffusion-xl',
2175
- * prompt: 'Transform this into a watercolor painting',
2176
- * images: [
2177
- * { url: 'https://example.com/input.jpg' },
2178
- * // or base64-encoded Data URI:
2179
- * { url: 'data:image/jpeg;base64,/9j/4AAQ...' }
2180
- * ]
2181
- * });
2182
- * ```
2436
+ * Send a custom HTML email
2437
+ * @param options Email options including recipients, subject, and HTML content
2183
2438
  */
2184
- async generate(params) {
2185
- const response = await this.http.post(
2186
- "/api/ai/image/generation",
2187
- params
2188
- );
2189
- let data = [];
2190
- if (response.images && response.images.length > 0) {
2191
- data = response.images.map((img) => ({
2192
- b64_json: img.imageUrl.replace(/^data:image\/\w+;base64,/, ""),
2193
- content: response.text
2194
- }));
2195
- } else if (response.text) {
2196
- data = [{ content: response.text }];
2197
- }
2198
- return {
2199
- created: Math.floor(Date.now() / 1e3),
2200
- data,
2201
- ...response.metadata?.usage && {
2202
- usage: {
2203
- total_tokens: response.metadata.usage.totalTokens || 0,
2204
- input_tokens: response.metadata.usage.promptTokens || 0,
2205
- output_tokens: response.metadata.usage.completionTokens || 0
2206
- }
2439
+ async send(options) {
2440
+ try {
2441
+ const data = await this.http.post(
2442
+ "/api/email/send-raw",
2443
+ options
2444
+ );
2445
+ return { data, error: null };
2446
+ } catch (error) {
2447
+ if (error instanceof Error && error.name === "AbortError") {
2448
+ throw error;
2207
2449
  }
2208
- };
2450
+ return {
2451
+ data: null,
2452
+ error: error instanceof ResultError ? error : new ResultError(
2453
+ error instanceof Error ? error.message : "Email send failed",
2454
+ 500,
2455
+ "EMAIL_ERROR"
2456
+ )
2457
+ };
2458
+ }
2209
2459
  }
2210
2460
  };
2461
+
2462
+ // src/modules/functions.ts
2211
2463
  var Functions = class _Functions {
2464
+ http;
2465
+ functionsUrl;
2212
2466
  constructor(http, functionsUrl) {
2213
2467
  this.http = http;
2214
- this.functionsUrl = functionsUrl || _Functions.deriveSubhostingUrl(http.baseUrl);
2468
+ this.functionsUrl = functionsUrl;
2215
2469
  }
2216
2470
  /**
2217
- * Derive the subhosting URL from the base URL.
2218
- * Base URL pattern: https://{appKey}.{region}.insforge.app
2219
- * Functions URL: https://{appKey}.functions.insforge.app
2220
- * Only applies to .insforge.app domains.
2471
+ * Derive the legacy subhosting URL from the base URL
2472
+ * ({appKey}.{region}.<domain> -> {appKey}.functions.<domain>).
2473
+ * Only used to recognize when a configured functionsUrl still points at
2474
+ * the local deployment, so in-process dispatch can short-circuit it.
2221
2475
  */
2222
2476
  static deriveSubhostingUrl(baseUrl) {
2223
2477
  try {
@@ -2236,7 +2490,7 @@ var Functions = class _Functions {
2236
2490
  * placeholder; the router only reads pathname.
2237
2491
  */
2238
2492
  buildInProcessRequest(slug, method, body, callerHeaders) {
2239
- const url = new URL("/" + slug, "http://insforge.local").toString();
2493
+ const url = new URL(`/${slug}`, "http://backend.local").toString();
2240
2494
  const headers = { ...this.http.getHeaders() };
2241
2495
  const reqBody = serializeBody(method, body, headers);
2242
2496
  Object.assign(headers, callerHeaders);
@@ -2250,11 +2504,13 @@ var Functions = class _Functions {
2250
2504
  * Invoke an Edge Function.
2251
2505
  *
2252
2506
  * Dispatch order:
2253
- * 1. If `globalThis.__insforge_dispatch__` is present, call it in-process.
2254
- * This avoids Deno Subhosting's 508 Loop Detected when one bundled
2255
- * function invokes another inside the same deployment.
2256
- * 2. Otherwise, try the configured subhosting URL.
2257
- * 3. On 404 from subhosting, fall back to the proxy path.
2507
+ * 1. If the platform's in-process dispatch hook is present and no foreign
2508
+ * functionsUrl is configured, call it directly (function-to-function
2509
+ * calls inside the same deployment).
2510
+ * 2. If a custom functionsUrl is configured, try it; fall back to the
2511
+ * proxy path on 404 or network failure.
2512
+ * 3. Otherwise, invoke through the main host's /functions/{slug} proxy.
2513
+ * This route works from browsers too (correct CORS).
2258
2514
  *
2259
2515
  * @param slug The function slug to invoke
2260
2516
  * @param options Request options
@@ -2263,7 +2519,7 @@ var Functions = class _Functions {
2263
2519
  const { method = "POST", body, headers = {} } = options;
2264
2520
  const dispatch = globalThis.__insforge_dispatch__;
2265
2521
  const localFunctionsUrl = _Functions.deriveSubhostingUrl(this.http.baseUrl);
2266
- if (typeof dispatch === "function" && !!localFunctionsUrl && this.functionsUrl === localFunctionsUrl) {
2522
+ if (typeof dispatch === "function" && (this.functionsUrl === void 0 || !!localFunctionsUrl && this.functionsUrl === localFunctionsUrl)) {
2267
2523
  try {
2268
2524
  const req = this.buildInProcessRequest(slug, method, body, headers);
2269
2525
  const res = await dispatch(req);
@@ -2275,7 +2531,7 @@ var Functions = class _Functions {
2275
2531
  }
2276
2532
  return {
2277
2533
  data: null,
2278
- error: error instanceof InsForgeError ? error : new InsForgeError(
2534
+ error: error instanceof ResultError ? error : new ResultError(
2279
2535
  error instanceof Error ? error.message : "Function invocation failed",
2280
2536
  500,
2281
2537
  "FUNCTION_ERROR"
@@ -2285,20 +2541,25 @@ var Functions = class _Functions {
2285
2541
  }
2286
2542
  if (this.functionsUrl) {
2287
2543
  try {
2288
- const data = await this.http.request(method, `${this.functionsUrl}/${slug}`, {
2289
- body,
2290
- headers
2291
- });
2544
+ const data = await this.http.request(
2545
+ method,
2546
+ `${this.functionsUrl}/${slug}`,
2547
+ {
2548
+ body,
2549
+ headers
2550
+ }
2551
+ );
2292
2552
  return { data, error: null };
2293
2553
  } catch (error) {
2294
2554
  if (error instanceof Error && error.name === "AbortError") {
2295
2555
  throw error;
2296
2556
  }
2297
- if (error instanceof InsForgeError && error.statusCode === 404) {
2557
+ const unreachable = error instanceof ResultError && (error.statusCode === 404 || error.statusCode === 0);
2558
+ if (unreachable) {
2298
2559
  } else {
2299
2560
  return {
2300
2561
  data: null,
2301
- error: error instanceof InsForgeError ? error : new InsForgeError(
2562
+ error: error instanceof ResultError ? error : new ResultError(
2302
2563
  error instanceof Error ? error.message : "Function invocation failed",
2303
2564
  500,
2304
2565
  "FUNCTION_ERROR"
@@ -2317,7 +2578,7 @@ var Functions = class _Functions {
2317
2578
  }
2318
2579
  return {
2319
2580
  data: null,
2320
- error: error instanceof InsForgeError ? error : new InsForgeError(
2581
+ error: error instanceof ResultError ? error : new ResultError(
2321
2582
  error instanceof Error ? error.message : "Function invocation failed",
2322
2583
  500,
2323
2584
  "FUNCTION_ERROR"
@@ -2326,26 +2587,43 @@ var Functions = class _Functions {
2326
2587
  }
2327
2588
  }
2328
2589
  };
2590
+
2591
+ // src/modules/realtime.ts
2329
2592
  var CONNECT_TIMEOUT = 1e4;
2330
2593
  var SUBSCRIBE_TIMEOUT = 1e4;
2594
+ var CHANNEL_PREFIX = "realtime:";
2595
+ function normalizeChannelMeta(message) {
2596
+ const channel = message?.meta?.channel;
2597
+ if (typeof channel !== "string" || !channel.startsWith(CHANNEL_PREFIX)) {
2598
+ return message;
2599
+ }
2600
+ return {
2601
+ ...message,
2602
+ meta: { ...message.meta, channel: channel.slice(CHANNEL_PREFIX.length) }
2603
+ };
2604
+ }
2331
2605
  var Realtime = class {
2332
2606
  constructor(baseUrl, tokenManager, anonKey, getValidAccessToken = async () => tokenManager.getAccessToken()) {
2333
2607
  this.baseUrl = baseUrl;
2334
2608
  this.tokenManager = tokenManager;
2335
2609
  this.anonKey = anonKey;
2336
2610
  this.getValidAccessToken = getValidAccessToken;
2337
- this.socket = null;
2338
- this.connectPromise = null;
2339
- this.connectionAttempt = null;
2340
- this.nextConnectionAttemptId = 0;
2341
- this.subscriptions = /* @__PURE__ */ new Map();
2342
- this.eventListeners = /* @__PURE__ */ new Map();
2343
2611
  this.tokenManager.onAuthStateChange((event) => {
2344
2612
  if (event !== AuthChangeEvent.TOKEN_REFRESHED) {
2345
2613
  this.reconnectForAuthChange();
2346
2614
  }
2347
2615
  });
2348
2616
  }
2617
+ baseUrl;
2618
+ tokenManager;
2619
+ anonKey;
2620
+ getValidAccessToken;
2621
+ socket = null;
2622
+ connectPromise = null;
2623
+ connectionAttempt = null;
2624
+ nextConnectionAttemptId = 0;
2625
+ subscriptions = /* @__PURE__ */ new Map();
2626
+ eventListeners = /* @__PURE__ */ new Map();
2349
2627
  notifyListeners(event, payload) {
2350
2628
  for (const callback of this.eventListeners.get(event) ?? []) {
2351
2629
  try {
@@ -2445,8 +2723,9 @@ var Realtime = class {
2445
2723
  if (event === "realtime:error") {
2446
2724
  return;
2447
2725
  }
2448
- this.applyPresenceEvent(event, message);
2449
- this.notifyListeners(event, message);
2726
+ const normalized = normalizeChannelMeta(message);
2727
+ this.applyPresenceEvent(event, normalized);
2728
+ this.notifyListeners(event, normalized);
2450
2729
  };
2451
2730
  this.connectionAttempt = { id: attemptId, socket, cancel: fail };
2452
2731
  socket.on("connect", onConnect);
@@ -2510,7 +2789,10 @@ var Realtime = class {
2510
2789
  {
2511
2790
  ok: false,
2512
2791
  channel: subscription.channel,
2513
- error: { code: "DISCONNECTED", message: "Connection lost before subscription completed" }
2792
+ error: {
2793
+ code: "DISCONNECTED",
2794
+ message: "Connection lost before subscription completed"
2795
+ }
2514
2796
  },
2515
2797
  true
2516
2798
  );
@@ -2533,7 +2815,10 @@ var Realtime = class {
2533
2815
  return Promise.resolve({
2534
2816
  ok: false,
2535
2817
  channel,
2536
- error: { code: "CONNECTION_FAILED", message: "Not connected to realtime server" }
2818
+ error: {
2819
+ code: "CONNECTION_FAILED",
2820
+ message: "Not connected to realtime server"
2821
+ }
2537
2822
  });
2538
2823
  }
2539
2824
  subscription.status = "pending";
@@ -2564,21 +2849,28 @@ var Realtime = class {
2564
2849
  );
2565
2850
  }
2566
2851
  }, SUBSCRIBE_TIMEOUT);
2567
- socket.emit("realtime:subscribe", { channel }, (response) => {
2568
- if (this.subscriptions.get(channel) !== subscription || subscription.epoch !== epoch) {
2569
- return;
2570
- }
2571
- if (response.ok) {
2572
- subscription.status = "subscribed";
2573
- subscription.members = new Map(
2574
- response.presence.members.map((member) => [member.presenceId, member])
2575
- );
2576
- } else {
2577
- subscription.status = "rejected";
2578
- subscription.members.clear();
2852
+ socket.emit(
2853
+ "realtime:subscribe",
2854
+ { channel },
2855
+ (response) => {
2856
+ if (this.subscriptions.get(channel) !== subscription || subscription.epoch !== epoch) {
2857
+ return;
2858
+ }
2859
+ if (response.ok) {
2860
+ subscription.status = "subscribed";
2861
+ subscription.members = new Map(
2862
+ response.presence.members.map((member) => [
2863
+ member.presenceId,
2864
+ member
2865
+ ])
2866
+ );
2867
+ } else {
2868
+ subscription.status = "rejected";
2869
+ subscription.members.clear();
2870
+ }
2871
+ this.settleSubscription(subscription, response, false);
2579
2872
  }
2580
- this.settleSubscription(subscription, response, false);
2581
- });
2873
+ );
2582
2874
  });
2583
2875
  return subscription.pending;
2584
2876
  }
@@ -2627,10 +2919,19 @@ var Realtime = class {
2627
2919
  return subscription.pending;
2628
2920
  }
2629
2921
  if (subscription.status === "subscribed") {
2630
- return { ok: true, channel, presence: { members: [...subscription.members.values()] } };
2922
+ return {
2923
+ ok: true,
2924
+ channel,
2925
+ presence: { members: [...subscription.members.values()] }
2926
+ };
2631
2927
  }
2632
2928
  } else {
2633
- subscription = { channel, epoch: 0, status: "pending", members: /* @__PURE__ */ new Map() };
2929
+ subscription = {
2930
+ channel,
2931
+ epoch: 0,
2932
+ status: "pending",
2933
+ members: /* @__PURE__ */ new Map()
2934
+ };
2634
2935
  this.subscriptions.set(channel, subscription);
2635
2936
  }
2636
2937
  if (!this.socket?.connected) {
@@ -2641,7 +2942,11 @@ var Realtime = class {
2641
2942
  this.subscriptions.delete(channel);
2642
2943
  }
2643
2944
  const message = error instanceof Error ? error.message : "Connection failed";
2644
- return { ok: false, channel, error: { code: "CONNECTION_FAILED", message } };
2945
+ return {
2946
+ ok: false,
2947
+ channel,
2948
+ error: { code: "CONNECTION_FAILED", message }
2949
+ };
2645
2950
  }
2646
2951
  }
2647
2952
  return subscription.pending ?? this.requestSubscription(channel, subscription);
@@ -2657,7 +2962,10 @@ var Realtime = class {
2657
2962
  {
2658
2963
  ok: false,
2659
2964
  channel,
2660
- error: { code: "SUBSCRIPTION_CANCELLED", message: "Subscription cancelled" }
2965
+ error: {
2966
+ code: "SUBSCRIPTION_CANCELLED",
2967
+ message: "Subscription cancelled"
2968
+ }
2661
2969
  },
2662
2970
  true
2663
2971
  );
@@ -2665,12 +2973,31 @@ var Realtime = class {
2665
2973
  this.socket.emit("realtime:unsubscribe", { channel });
2666
2974
  }
2667
2975
  }
2976
+ /**
2977
+ * Publish an event to a channel.
2978
+ *
2979
+ * Delivery notes (verified against a live backend):
2980
+ * - Payload fields arrive spread onto the received message itself, next to
2981
+ * `meta` - not nested under a `payload` key. Publishing `{ text: "hi" }`
2982
+ * means receivers read `msg.text`.
2983
+ * - The sender receives its own message too. Deduplicate with
2984
+ * `msg.meta.messageId` (or `msg.meta.senderId`) if you also update local
2985
+ * state optimistically.
2986
+ */
2668
2987
  async publish(channel, event, payload) {
2669
2988
  if (!this.socket?.connected) {
2670
- throw new Error("Not connected to realtime server. Call connect() first.");
2989
+ throw new Error(
2990
+ "Not connected to realtime server. Call connect() first."
2991
+ );
2671
2992
  }
2672
2993
  this.socket.emit("realtime:publish", { channel, event, payload });
2673
2994
  }
2995
+ /**
2996
+ * Listen for an event by name, across all subscribed channels.
2997
+ * Filter by channel inside the callback: `msg.meta.channel` always matches
2998
+ * the exact name passed to subscribe() (the SDK strips the server's
2999
+ * transport prefix before delivery).
3000
+ */
2674
3001
  on(event, callback) {
2675
3002
  const listeners = this.eventListeners.get(event) ?? /* @__PURE__ */ new Set();
2676
3003
  listeners.add(callback);
@@ -2696,201 +3023,545 @@ var Realtime = class {
2696
3023
  getPresenceState(channel) {
2697
3024
  return [...this.subscriptions.get(channel)?.members.values() ?? []];
2698
3025
  }
2699
- };
2700
- var Emails = class {
2701
- constructor(http) {
2702
- this.http = http;
3026
+ };
3027
+
3028
+ // src/modules/storage.ts
3029
+ function generateObjectKey(filename) {
3030
+ const dotIndex = filename.lastIndexOf(".");
3031
+ const hasExt = dotIndex > 0;
3032
+ const ext = hasExt ? filename.slice(dotIndex) : "";
3033
+ const base = hasExt ? filename.slice(0, dotIndex) : filename;
3034
+ const sanitizedBase = base.replace(/[^a-zA-Z0-9-_]/g, "-").slice(0, 32) || "file";
3035
+ const timestamp = Date.now();
3036
+ const random = Math.random().toString(36).slice(2, 8);
3037
+ return `${sanitizedBase}-${timestamp}-${random}${ext}`;
3038
+ }
3039
+ var StorageBucket = class {
3040
+ constructor(bucketName, http) {
3041
+ this.bucketName = bucketName;
3042
+ this.http = http;
3043
+ }
3044
+ bucketName;
3045
+ http;
3046
+ /**
3047
+ * Upload a file to a specific key.
3048
+ * Uses the upload strategy from the backend (direct or presigned).
3049
+ * Standard PUT semantics: uploading to an existing key replaces the
3050
+ * current object in place.
3051
+ * @param path - The object key/path
3052
+ * @param file - File or Blob to upload
3053
+ */
3054
+ async upload(path, file) {
3055
+ try {
3056
+ const strategyResponse = await this.http.post(
3057
+ `/api/storage/buckets/${this.bucketName}/upload-strategy`,
3058
+ {
3059
+ filename: path,
3060
+ contentType: file.type || "application/octet-stream",
3061
+ size: file.size
3062
+ }
3063
+ );
3064
+ if (strategyResponse.method === "presigned") {
3065
+ return await this.uploadWithPresignedUrl(strategyResponse, file);
3066
+ }
3067
+ if (strategyResponse.method === "direct") {
3068
+ const formData = new FormData();
3069
+ formData.append("file", file);
3070
+ const response = await this.http.request(
3071
+ "PUT",
3072
+ `/api/storage/buckets/${this.bucketName}/objects/${encodeURIComponent(path)}`,
3073
+ {
3074
+ body: formData,
3075
+ headers: {
3076
+ // Don't set Content-Type, let browser set multipart boundary
3077
+ }
3078
+ }
3079
+ );
3080
+ return { data: response, error: null };
3081
+ }
3082
+ throw new ResultError(
3083
+ `Unsupported upload method: ${strategyResponse.method}`,
3084
+ 500,
3085
+ "STORAGE_ERROR"
3086
+ );
3087
+ } catch (error) {
3088
+ return {
3089
+ data: null,
3090
+ error: error instanceof ResultError ? error : new ResultError("Upload failed", 500, "STORAGE_ERROR")
3091
+ };
3092
+ }
3093
+ }
3094
+ /**
3095
+ * Upload a file under an automatically generated, collision-free key.
3096
+ * The key is derived client-side from the filename (sanitized base +
3097
+ * timestamp + random suffix) and uploaded through the standard
3098
+ * {@link upload} path, so repeated uploads of the same file never
3099
+ * overwrite each other. Reads the filename structurally to avoid assuming
3100
+ * a global `File` (which Node 18 does not expose).
3101
+ * @param file - File or Blob to upload
3102
+ */
3103
+ async uploadAuto(file) {
3104
+ const filename = "name" in file && typeof file.name === "string" ? file.name : "file";
3105
+ return this.upload(generateObjectKey(filename), file);
3106
+ }
3107
+ /**
3108
+ * Internal method to handle presigned URL uploads
3109
+ */
3110
+ async uploadWithPresignedUrl(strategy, file) {
3111
+ try {
3112
+ const formData = new FormData();
3113
+ if (strategy.fields) {
3114
+ Object.entries(strategy.fields).forEach(([key, value]) => {
3115
+ formData.append(key, value);
3116
+ });
3117
+ }
3118
+ formData.append("file", file);
3119
+ const uploadResponse = await fetch(strategy.uploadUrl, {
3120
+ method: "POST",
3121
+ body: formData
3122
+ });
3123
+ if (!uploadResponse.ok) {
3124
+ throw new ResultError(
3125
+ `Upload to storage failed: ${uploadResponse.statusText}`,
3126
+ uploadResponse.status,
3127
+ "STORAGE_ERROR"
3128
+ );
3129
+ }
3130
+ if (strategy.confirmRequired && strategy.confirmUrl) {
3131
+ const confirmResponse = await this.http.post(
3132
+ strategy.confirmUrl,
3133
+ {
3134
+ size: file.size,
3135
+ contentType: file.type || "application/octet-stream"
3136
+ }
3137
+ );
3138
+ return { data: confirmResponse, error: null };
3139
+ }
3140
+ return {
3141
+ data: {
3142
+ key: strategy.key,
3143
+ bucket: this.bucketName,
3144
+ size: file.size,
3145
+ mimeType: file.type || "application/octet-stream",
3146
+ uploadedAt: (/* @__PURE__ */ new Date()).toISOString(),
3147
+ url: this.getPublicUrl(strategy.key).data?.publicUrl
3148
+ },
3149
+ error: null
3150
+ };
3151
+ } catch (error) {
3152
+ throw error instanceof ResultError ? error : new ResultError("Presigned upload failed", 500, "STORAGE_ERROR");
3153
+ }
3154
+ }
3155
+ /**
3156
+ * Download a file
3157
+ * Uses the download strategy from backend (direct or presigned)
3158
+ * @param path - The object key/path
3159
+ * Returns the file as a Blob
3160
+ */
3161
+ async download(path) {
3162
+ try {
3163
+ const encodedKey = encodeURIComponent(path);
3164
+ let strategyResponse;
3165
+ try {
3166
+ strategyResponse = await this.http.get(
3167
+ `/api/storage/buckets/${this.bucketName}/download-strategy/objects/${encodedKey}`
3168
+ );
3169
+ } catch (err) {
3170
+ const status = err instanceof ResultError ? err.statusCode : void 0;
3171
+ if (status === 404 || status === 405) {
3172
+ strategyResponse = await this.http.post(
3173
+ `/api/storage/buckets/${this.bucketName}/objects/${encodedKey}/download-strategy`,
3174
+ {}
3175
+ );
3176
+ } else {
3177
+ throw err;
3178
+ }
3179
+ }
3180
+ const downloadUrl = strategyResponse.url;
3181
+ const headers = {};
3182
+ if (strategyResponse.method === "direct") {
3183
+ Object.assign(headers, this.http.getHeaders());
3184
+ }
3185
+ const response = await fetch(downloadUrl, {
3186
+ method: "GET",
3187
+ headers
3188
+ });
3189
+ if (!response.ok) {
3190
+ try {
3191
+ const error = await response.json();
3192
+ throw ResultError.fromApiError(error);
3193
+ } catch {
3194
+ throw new ResultError(
3195
+ `Download failed: ${response.statusText}`,
3196
+ response.status,
3197
+ "STORAGE_ERROR"
3198
+ );
3199
+ }
3200
+ }
3201
+ const blob = await response.blob();
3202
+ return { data: blob, error: null };
3203
+ } catch (error) {
3204
+ return {
3205
+ data: null,
3206
+ error: error instanceof ResultError ? error : new ResultError("Download failed", 500, "STORAGE_ERROR")
3207
+ };
3208
+ }
3209
+ }
3210
+ /**
3211
+ * Get the public URL for an object in a public bucket.
3212
+ *
3213
+ * Pure string construction — no network call, no auth. The URL only resolves
3214
+ * if the bucket is public; for private objects use {@link createSignedUrl}.
3215
+ *
3216
+ * @param path - The object key/path
3217
+ * @returns `{ data: { publicUrl }, error }` — matches the external SDK pattern,
3218
+ * so `const { data } = getPublicUrl(path)` then `data.publicUrl`.
3219
+ */
3220
+ getPublicUrl(path) {
3221
+ const publicUrl = `${this.http.baseUrl}/api/storage/buckets/${this.bucketName}/objects/${encodeURIComponent(path)}`;
3222
+ return { data: { publicUrl }, error: null };
2703
3223
  }
2704
3224
  /**
2705
- * Send a custom HTML email
2706
- * @param options Email options including recipients, subject, and HTML content
3225
+ * Resolve a download strategy (signed or direct URL) for an object with a
3226
+ * caller-supplied TTL. Prefers the canonical GET route and falls back to the
3227
+ * legacy POST alias so signed-URL creation still works against older backends
3228
+ * that predate the GET route (they return 404/405 for it). A genuine
3229
+ * "object not found" (STORAGE_NOT_FOUND) is not retried.
2707
3230
  */
2708
- async send(options) {
3231
+ async requestDownloadStrategy(path, expiresIn) {
3232
+ const encoded = encodeURIComponent(path);
2709
3233
  try {
2710
- const data = await this.http.post("/api/email/send-raw", options);
2711
- return { data, error: null };
3234
+ return await this.http.get(
3235
+ `/api/storage/buckets/${this.bucketName}/download-strategy/objects/${encoded}`,
3236
+ { params: { expiresIn: expiresIn.toString() } }
3237
+ );
2712
3238
  } catch (error) {
2713
- if (error instanceof Error && error.name === "AbortError") {
3239
+ const status = error instanceof ResultError ? error.statusCode : void 0;
3240
+ const isMissingRoute = (status === 404 || status === 405) && !(error instanceof ResultError && error.error === "STORAGE_NOT_FOUND");
3241
+ if (!isMissingRoute) {
2714
3242
  throw error;
2715
3243
  }
3244
+ return await this.http.post(
3245
+ `/api/storage/buckets/${this.bucketName}/objects/${encoded}/download-strategy`,
3246
+ { expiresIn }
3247
+ );
3248
+ }
3249
+ }
3250
+ /**
3251
+ * Create a signed URL for an object.
3252
+ *
3253
+ * Returns a time-limited, credential-free URL that can be handed directly to
3254
+ * a browser (`<img src>`), an email, or a third party — no SDK or session is
3255
+ * needed to fetch it. Authorization is enforced when the URL is minted (the
3256
+ * caller must be allowed to read the object), so the resulting link is a
3257
+ * pre-authorized capability scoped to this one object until it expires.
3258
+ *
3259
+ * @param path - The object key/path
3260
+ * @param expiresIn - Lifetime in seconds (default 3600 = 1h, max 604800 = 7d).
3261
+ * Honored for private buckets; public buckets return their long-lived URL.
3262
+ */
3263
+ async createSignedUrl(path, expiresIn = 3600) {
3264
+ try {
3265
+ const strategy = await this.requestDownloadStrategy(path, expiresIn);
3266
+ return {
3267
+ data: {
3268
+ signedUrl: strategy.url,
3269
+ expiresAt: strategy.expiresAt ? new Date(strategy.expiresAt).toISOString() : null
3270
+ },
3271
+ error: null
3272
+ };
3273
+ } catch (error) {
2716
3274
  return {
2717
3275
  data: null,
2718
- error: error instanceof InsForgeError ? error : new InsForgeError(
2719
- error instanceof Error ? error.message : "Email send failed",
3276
+ error: error instanceof ResultError ? error : new ResultError(
3277
+ "Failed to create signed URL",
2720
3278
  500,
2721
- "EMAIL_ERROR"
3279
+ "STORAGE_ERROR"
2722
3280
  )
2723
3281
  };
2724
3282
  }
2725
3283
  }
2726
- };
2727
- function providerEnvironmentPath(provider, environment) {
2728
- return `/api/payments/${provider}/${encodeURIComponent(environment)}`;
2729
- }
2730
- var StripePayments = class {
2731
- constructor(http) {
2732
- this.http = http;
2733
- }
2734
3284
  /**
2735
- * Create a Stripe Checkout Session through the InsForge backend.
3285
+ * Create signed URLs for multiple objects in a single call.
2736
3286
  *
2737
- * @example
2738
- * ```typescript
2739
- * const { data, error } = await client.payments.stripe.createCheckoutSession('test', {
2740
- * mode: 'payment',
2741
- * lineItems: [{ priceId: 'price_123', quantity: 1 }],
2742
- * successUrl: `${window.location.origin}/success`,
2743
- * cancelUrl: `${window.location.origin}/pricing`
2744
- * });
3287
+ * Each entry resolves independently: a failure on one key (not found / not
3288
+ * permitted) is reported on that entry's `error` without failing the rest.
2745
3289
  *
2746
- * if (!error && data.checkoutSession.url) {
2747
- * window.location.assign(data.checkoutSession.url);
2748
- * }
2749
- * ```
3290
+ * @param paths - The object keys/paths
3291
+ * @param expiresIn - Lifetime in seconds (default 3600 = 1h, max 604800 = 7d)
2750
3292
  */
2751
- async createCheckoutSession(environment, request) {
3293
+ async createSignedUrls(paths, expiresIn = 3600) {
2752
3294
  try {
2753
- const data = await this.http.post(
2754
- `${providerEnvironmentPath("stripe", environment)}/checkout-sessions`,
2755
- request,
2756
- { idempotent: !!request.idempotencyKey }
3295
+ const data = await Promise.all(
3296
+ paths.map(async (path) => {
3297
+ const { data: signed, error } = await this.createSignedUrl(
3298
+ path,
3299
+ expiresIn
3300
+ );
3301
+ return {
3302
+ path,
3303
+ signedUrl: signed?.signedUrl ?? null,
3304
+ error: error ? error.message : null
3305
+ };
3306
+ })
2757
3307
  );
2758
3308
  return { data, error: null };
2759
3309
  } catch (error) {
2760
- return wrapError(
2761
- error,
2762
- "Stripe checkout session creation failed"
2763
- );
3310
+ return {
3311
+ data: null,
3312
+ error: error instanceof ResultError ? error : new ResultError(
3313
+ "Failed to create signed URLs",
3314
+ 500,
3315
+ "STORAGE_ERROR"
3316
+ )
3317
+ };
2764
3318
  }
2765
3319
  }
2766
3320
  /**
2767
- * Create a Stripe Billing Portal Session for a mapped billing subject.
3321
+ * List objects in the bucket
3322
+ * @param prefix - Filter by key prefix
3323
+ * @param search - Search in file names
3324
+ * @param limit - Maximum number of results (default: 100, max: 1000)
3325
+ * @param offset - Number of results to skip
2768
3326
  */
2769
- async createCustomerPortalSession(environment, request) {
3327
+ async list(options) {
2770
3328
  try {
2771
- const data = await this.http.post(
2772
- `${providerEnvironmentPath("stripe", environment)}/customer-portal-sessions`,
2773
- request
3329
+ const params = {};
3330
+ if (options?.prefix) {
3331
+ params.prefix = options.prefix;
3332
+ }
3333
+ if (options?.search) {
3334
+ params.search = options.search;
3335
+ }
3336
+ if (options?.limit) {
3337
+ params.limit = options.limit.toString();
3338
+ }
3339
+ if (options?.offset) {
3340
+ params.offset = options.offset.toString();
3341
+ }
3342
+ const response = await this.http.get(
3343
+ `/api/storage/buckets/${this.bucketName}/objects`,
3344
+ { params }
2774
3345
  );
2775
- return { data, error: null };
3346
+ return { data: response, error: null };
2776
3347
  } catch (error) {
2777
- return wrapError(
2778
- error,
2779
- "Stripe customer portal session creation failed"
3348
+ return {
3349
+ data: null,
3350
+ error: error instanceof ResultError ? error : new ResultError("List failed", 500, "STORAGE_ERROR")
3351
+ };
3352
+ }
3353
+ }
3354
+ /**
3355
+ * Delete one or more files.
3356
+ *
3357
+ * A string uses the single-object endpoint. An array uses the batch endpoint,
3358
+ * which accepts at most 1000 keys and returns one result per key.
3359
+ *
3360
+ * @param pathOrPaths - One object key/path or an array of object keys/paths
3361
+ */
3362
+ async remove(pathOrPaths) {
3363
+ try {
3364
+ const response = Array.isArray(pathOrPaths) ? await this.http.delete(
3365
+ `/api/storage/buckets/${this.bucketName}/objects`,
3366
+ { body: { keys: pathOrPaths } }
3367
+ ) : await this.http.delete(
3368
+ `/api/storage/buckets/${this.bucketName}/objects/${encodeURIComponent(pathOrPaths)}`
2780
3369
  );
3370
+ return { data: response, error: null };
3371
+ } catch (error) {
3372
+ return {
3373
+ data: null,
3374
+ error: error instanceof ResultError ? error : new ResultError("Delete failed", 500, "STORAGE_ERROR")
3375
+ };
2781
3376
  }
2782
3377
  }
2783
3378
  };
2784
- var RazorpayPayments = class {
3379
+ var Storage = class {
2785
3380
  constructor(http) {
2786
3381
  this.http = http;
2787
3382
  }
2788
- async createOrder(environment, request) {
2789
- try {
2790
- const data = await this.http.post(
2791
- `${providerEnvironmentPath("razorpay", environment)}/orders`,
2792
- request
2793
- );
2794
- return { data, error: null };
2795
- } catch (error) {
2796
- return wrapError(error, "Razorpay order creation failed");
2797
- }
3383
+ http;
3384
+ /**
3385
+ * Get a bucket instance for operations
3386
+ * @param bucketName - Name of the bucket
3387
+ */
3388
+ from(bucketName) {
3389
+ return new StorageBucket(bucketName, this.http);
2798
3390
  }
2799
- async verifyOrder(environment, request) {
2800
- try {
2801
- const data = await this.http.post(
2802
- `${providerEnvironmentPath("razorpay", environment)}/orders/verify`,
2803
- request
2804
- );
2805
- return { data, error: null };
2806
- } catch (error) {
2807
- return wrapError(error, "Razorpay order verification failed");
2808
- }
3391
+ };
3392
+
3393
+ // src/modules/support.ts
3394
+ var DEFAULT_ENDPOINT2 = "https://beta.result.dev";
3395
+ var Support = class {
3396
+ handle;
3397
+ endpoint;
3398
+ // Fallback for non-browser runtimes and private mode.
3399
+ mem = { threads: [] };
3400
+ constructor(options) {
3401
+ this.handle = options?.handle ?? "";
3402
+ this.endpoint = (options?.endpoint || DEFAULT_ENDPOINT2).replace(/\/$/, "");
2809
3403
  }
2810
- async createSubscription(environment, request) {
2811
- try {
2812
- const data = await this.http.post(
2813
- `${providerEnvironmentPath("razorpay", environment)}/subscriptions`,
2814
- request
2815
- );
2816
- return { data, error: null };
2817
- } catch (error) {
2818
- return wrapError(
2819
- error,
2820
- "Razorpay subscription creation failed"
2821
- );
2822
- }
3404
+ /**
3405
+ * Start a new support conversation. The returned access key is stored
3406
+ * locally so `getThread`/`sendMessage` work without passing it around.
3407
+ */
3408
+ async startThread(options) {
3409
+ const guard = this.requireHandle();
3410
+ if (guard) return { data: null, error: guard };
3411
+ const { data, error } = await this.request(
3412
+ "POST",
3413
+ "/api/support/threads",
3414
+ {
3415
+ username: this.handle,
3416
+ email: options.email,
3417
+ name: options.name,
3418
+ message: options.message
3419
+ }
3420
+ );
3421
+ if (data) {
3422
+ this.remember(data.id, data.accessKey, options);
3423
+ }
3424
+ return { data, error };
3425
+ }
3426
+ /** Fetch a thread (status + full message history) with its stored access key. */
3427
+ async getThread(threadId, accessKey) {
3428
+ const guard = this.requireHandle();
3429
+ if (guard) return { data: null, error: guard };
3430
+ const key = accessKey ?? this.keyFor(threadId);
3431
+ if (!key) {
3432
+ return { data: null, error: this.missingKey(threadId) };
3433
+ }
3434
+ const { data, error } = await this.request(
3435
+ "GET",
3436
+ `/api/support/threads/${encodeURIComponent(threadId)}?key=${encodeURIComponent(key)}`
3437
+ );
3438
+ return { data: data?.thread ?? null, error };
3439
+ }
3440
+ /** Send a visitor reply on an existing thread (reopens it if closed). */
3441
+ async sendMessage(threadId, message, accessKey) {
3442
+ const guard = this.requireHandle();
3443
+ if (guard) return { data: null, error: guard };
3444
+ const key = accessKey ?? this.keyFor(threadId);
3445
+ if (!key) {
3446
+ return { data: null, error: this.missingKey(threadId) };
3447
+ }
3448
+ const { data, error } = await this.request(
3449
+ "POST",
3450
+ `/api/support/threads/${encodeURIComponent(threadId)}/messages`,
3451
+ { message, key }
3452
+ );
3453
+ return { data, error };
3454
+ }
3455
+ /** Threads started from this browser (ids only; fetch each with getThread). */
3456
+ listThreads() {
3457
+ return this.load().threads.map(({ id }) => ({ id }));
3458
+ }
3459
+ /** The contact details used when this browser last started a thread. */
3460
+ getContact() {
3461
+ return this.load().contact;
3462
+ }
3463
+ // -- internals -------------------------------------------------------------
3464
+ requireHandle() {
3465
+ if (this.handle) return null;
3466
+ return new ResultError(
3467
+ "Missing support handle. Pass support: { handle } to createClient().",
3468
+ 400,
3469
+ "MISSING_FIELD"
3470
+ );
2823
3471
  }
2824
- async verifySubscription(environment, request) {
2825
- try {
2826
- const data = await this.http.post(
2827
- `${providerEnvironmentPath("razorpay", environment)}/subscriptions/verify`,
2828
- request
2829
- );
2830
- return { data, error: null };
2831
- } catch (error) {
2832
- return wrapError(
2833
- error,
2834
- "Razorpay subscription verification failed"
2835
- );
2836
- }
3472
+ missingKey(threadId) {
3473
+ return new ResultError(
3474
+ `No access key stored for thread ${threadId}. Pass the accessKey returned by startThread().`,
3475
+ 401,
3476
+ "AUTH_UNAUTHORIZED"
3477
+ );
2837
3478
  }
2838
- async cancelSubscription(environment, subscriptionId, request = {}) {
3479
+ async request(method, path, body) {
2839
3480
  try {
2840
- const data = await this.http.post(
2841
- `${providerEnvironmentPath("razorpay", environment)}/subscriptions/${encodeURIComponent(
2842
- subscriptionId
2843
- )}/cancel`,
2844
- request
2845
- );
2846
- return { data, error: null };
3481
+ const response = await fetch(`${this.endpoint}${path}`, {
3482
+ method,
3483
+ headers: body ? { "Content-Type": "application/json" } : void 0,
3484
+ body: body ? JSON.stringify(body) : void 0
3485
+ });
3486
+ const payload = await response.json().catch(() => null);
3487
+ if (!response.ok) {
3488
+ return {
3489
+ data: null,
3490
+ error: new ResultError(
3491
+ payload?.error?.message ?? `Support request failed (${response.status})`,
3492
+ response.status,
3493
+ payload?.error?.code ?? "UNKNOWN_ERROR"
3494
+ )
3495
+ };
3496
+ }
3497
+ return { data: payload, error: null };
2847
3498
  } catch (error) {
2848
- return wrapError(
2849
- error,
2850
- "Razorpay subscription cancellation failed"
2851
- );
3499
+ if (error instanceof Error && error.name === "AbortError") {
3500
+ throw error;
3501
+ }
3502
+ return {
3503
+ data: null,
3504
+ error: new ResultError(
3505
+ error instanceof Error ? error.message : "Support request failed",
3506
+ 500,
3507
+ "UNKNOWN_ERROR"
3508
+ )
3509
+ };
2852
3510
  }
2853
3511
  }
2854
- async pauseSubscription(environment, subscriptionId) {
3512
+ storageKey() {
3513
+ return `rsup_${this.handle}`;
3514
+ }
3515
+ load() {
2855
3516
  try {
2856
- const data = await this.http.post(
2857
- `${providerEnvironmentPath("razorpay", environment)}/subscriptions/${encodeURIComponent(
2858
- subscriptionId
2859
- )}/pause`,
2860
- {}
2861
- );
2862
- return { data, error: null };
2863
- } catch (error) {
2864
- return wrapError(
2865
- error,
2866
- "Razorpay subscription pause failed"
2867
- );
3517
+ const raw = localStorage.getItem(this.storageKey());
3518
+ if (raw) {
3519
+ const parsed = JSON.parse(raw);
3520
+ if (parsed && Array.isArray(parsed.threads)) {
3521
+ return parsed;
3522
+ }
3523
+ }
3524
+ } catch {
2868
3525
  }
3526
+ return this.mem;
2869
3527
  }
2870
- async resumeSubscription(environment, subscriptionId) {
3528
+ save(state) {
3529
+ this.mem = state;
2871
3530
  try {
2872
- const data = await this.http.post(
2873
- `${providerEnvironmentPath("razorpay", environment)}/subscriptions/${encodeURIComponent(
2874
- subscriptionId
2875
- )}/resume`,
2876
- {}
2877
- );
2878
- return { data, error: null };
2879
- } catch (error) {
2880
- return wrapError(
2881
- error,
2882
- "Razorpay subscription resume failed"
2883
- );
3531
+ localStorage.setItem(this.storageKey(), JSON.stringify(state));
3532
+ } catch {
2884
3533
  }
2885
3534
  }
2886
- };
2887
- var Payments = class {
2888
- constructor(http) {
2889
- this.stripe = new StripePayments(http);
2890
- this.razorpay = new RazorpayPayments(http);
3535
+ keyFor(threadId) {
3536
+ return this.load().threads.find((thread) => thread.id === threadId)?.key;
3537
+ }
3538
+ remember(id, key, options) {
3539
+ const state = this.load();
3540
+ const threads = state.threads.filter((thread) => thread.id !== id);
3541
+ threads.unshift({ id, key });
3542
+ this.save({
3543
+ contact: {
3544
+ email: options.email,
3545
+ name: options.name ?? state.contact?.name
3546
+ },
3547
+ threads
3548
+ });
2891
3549
  }
2892
3550
  };
2893
- var InsForgeClient = class {
3551
+
3552
+ // src/client.ts
3553
+ var ResultClient = class {
3554
+ http;
3555
+ tokenManager;
3556
+ auth;
3557
+ database;
3558
+ storage;
3559
+ ai;
3560
+ functions;
3561
+ realtime;
3562
+ emails;
3563
+ analytics;
3564
+ support;
2894
3565
  constructor(config = {}) {
2895
3566
  const logger = new Logger(config.debug);
2896
3567
  this.tokenManager = new TokenManager();
@@ -2915,7 +3586,8 @@ var InsForgeClient = class {
2915
3586
  () => this.http.getValidAccessToken()
2916
3587
  );
2917
3588
  this.emails = new Emails(this.http);
2918
- this.payments = new Payments(this.http);
3589
+ this.analytics = new Analytics(config.analytics);
3590
+ this.support = new Support(config.support);
2919
3591
  }
2920
3592
  /**
2921
3593
  * Get the underlying HTTP client for custom requests
@@ -2939,16 +3611,16 @@ var InsForgeClient = class {
2939
3611
  *
2940
3612
  * Use this when an external auth provider (Better Auth, Clerk, Auth0,
2941
3613
  * WorkOS, Kinde, Stytch, …) issues the JWT and you need to keep the
2942
- * long-lived InsForge client in sync. Without this, you'd have to call
3614
+ * long-lived Result client in sync. Without this, you'd have to call
2943
3615
  * `client.getHttpClient().setAuthToken(token)` AND reach into the private
2944
3616
  * realtime token manager separately.
2945
3617
  *
2946
3618
  * @example
2947
3619
  * ```typescript
2948
- * import { AuthChangeEvent } from '@insforge/sdk';
3620
+ * import { AuthChangeEvent } from '@resultdev/sdk';
2949
3621
  *
2950
3622
  * // Refresh a third-party-issued JWT periodically
2951
- * const { token } = await fetch('/api/insforge-token').then((r) => r.json());
3623
+ * const { token } = await fetch('/api/backend-token').then((r) => r.json());
2952
3624
  * client.setAccessToken(token, AuthChangeEvent.TOKEN_REFRESHED);
2953
3625
  *
2954
3626
  * // Sign-out
@@ -2963,17 +3635,11 @@ var InsForgeClient = class {
2963
3635
  this.tokenManager.setAccessToken(token, event);
2964
3636
  }
2965
3637
  }
2966
- /**
2967
- * Future modules will be added here:
2968
- * - database: Database operations
2969
- * - storage: File storage operations
2970
- * - functions: Serverless functions
2971
- * - tables: Table management
2972
- * - metadata: Backend metadata
2973
- */
2974
3638
  };
3639
+
3640
+ // src/index.ts
2975
3641
  function createClient(config = {}) {
2976
- return new InsForgeClient(config);
3642
+ return new ResultClient(config);
2977
3643
  }
2978
3644
  function createAdminClient(config) {
2979
3645
  const { apiKey: rawApiKey, ...clientConfig } = config ?? {};
@@ -2981,27 +3647,32 @@ function createAdminClient(config) {
2981
3647
  if (!apiKey) {
2982
3648
  throw new Error("Missing apiKey. Pass apiKey to createAdminClient().");
2983
3649
  }
2984
- return new InsForgeClient({
3650
+ return new ResultClient({
2985
3651
  ...clientConfig,
2986
3652
  accessToken: apiKey,
2987
3653
  isServerMode: true
2988
3654
  });
2989
3655
  }
3656
+ var src_default = ResultClient;
2990
3657
  export {
2991
3658
  AI,
3659
+ Analytics,
2992
3660
  Auth,
2993
3661
  AuthChangeEvent,
2994
3662
  Database,
3663
+ ERROR_CODES,
2995
3664
  Emails,
2996
3665
  Functions,
2997
3666
  HttpClient,
2998
- InsForgeClient,
2999
- InsForgeError,
3000
3667
  Logger,
3001
- Payments,
3668
+ OAUTH_PROVIDERS,
3002
3669
  Realtime,
3670
+ ResultClient,
3671
+ ResultError,
3003
3672
  Storage,
3004
3673
  StorageBucket,
3674
+ Support,
3005
3675
  createAdminClient,
3006
- createClient
3676
+ createClient,
3677
+ src_default as default
3007
3678
  };