@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/CHANGELOG.md +54 -0
- package/README.md +76 -6
- package/THIRD-PARTY-NOTICES +209 -0
- package/dist/client-CJv_awPt.d.cts +1087 -0
- package/dist/client-DrsZ1qIm.d.ts +1087 -0
- package/dist/index.cjs +2791 -9267
- package/dist/index.d.cts +10 -7494
- package/dist/index.d.ts +10 -7494
- package/dist/index.js +2117 -1446
- package/dist/middleware-BJl9JRg3.d.ts +67 -0
- package/dist/middleware-C1Z6aMvn.d.cts +67 -0
- package/dist/ssr/middleware.cjs +179 -7176
- package/dist/ssr/middleware.d.cts +2 -1881
- package/dist/ssr/middleware.d.ts +2 -1881
- package/dist/ssr/middleware.js +179 -25
- package/dist/ssr.cjs +3154 -9588
- package/dist/ssr.d.cts +66 -7432
- package/dist/ssr.d.ts +66 -7432
- package/dist/ssr.js +2562 -1846
- package/dist/types-DSVtGaIA.d.cts +672 -0
- package/dist/types-DSVtGaIA.d.ts +672 -0
- package/package.json +43 -12
- package/dist/chunk-Y7VCTXPN.js +0 -7165
package/dist/index.js
CHANGED
|
@@ -1,20 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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 = "
|
|
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
|
|
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
|
-
|
|
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 = `[
|
|
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 = `[
|
|
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 = `[
|
|
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)
|
|
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)
|
|
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 = "
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
|
|
325
|
-
|
|
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
|
|
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
|
|
397
|
-
const error =
|
|
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
|
|
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 *
|
|
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 {
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
560
|
-
throw new
|
|
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
|
|
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
|
|
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 {
|
|
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
|
|
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
|
|
615
|
-
delete requestHeaders
|
|
616
|
-
requestHeaders
|
|
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
|
|
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(
|
|
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(
|
|
710
|
+
return await this.handleRequest(
|
|
711
|
+
method,
|
|
712
|
+
path,
|
|
713
|
+
{ ...options },
|
|
714
|
+
tokenUsed
|
|
715
|
+
);
|
|
666
716
|
} catch (error) {
|
|
667
|
-
if (!(error instanceof
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
function
|
|
912
|
-
const
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
const
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
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
|
|
934
|
-
if (
|
|
935
|
-
|
|
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
|
-
|
|
938
|
-
|
|
939
|
-
|
|
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
|
|
944
|
-
if (error instanceof
|
|
945
|
-
return
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
error
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
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
|
-
|
|
957
|
-
|
|
958
|
-
|
|
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
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
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
|
-
|
|
972
|
-
|
|
1048
|
+
};
|
|
1049
|
+
var ChatCompletions = class {
|
|
1050
|
+
constructor(http) {
|
|
1051
|
+
this.http = http;
|
|
973
1052
|
}
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
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
|
-
*
|
|
980
|
-
* Handles token storage, CSRF token, and HTTP auth header
|
|
1139
|
+
* Parse SSE stream into async iterable of OpenAI-like chunks
|
|
981
1140
|
*/
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
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
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
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
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
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
|
-
|
|
1152
|
-
|
|
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
|
-
|
|
1163
|
-
|
|
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
|
-
*
|
|
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
|
-
* @
|
|
1201
|
-
*
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
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
|
-
*
|
|
1230
|
-
*
|
|
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
|
-
*
|
|
1233
|
-
*
|
|
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
|
-
*
|
|
1236
|
-
*
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
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
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1305
|
+
};
|
|
1306
|
+
}
|
|
1307
|
+
};
|
|
1308
|
+
var Images = class {
|
|
1309
|
+
constructor(http) {
|
|
1310
|
+
this.http = http;
|
|
1267
1311
|
}
|
|
1312
|
+
http;
|
|
1268
1313
|
/**
|
|
1269
|
-
*
|
|
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
|
|
1272
|
-
await this.
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
}
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
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
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
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
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
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
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
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
|
-
|
|
1343
|
-
// ============================================================================
|
|
1344
|
-
async resendVerificationEmail(request) {
|
|
1435
|
+
// -- internals -------------------------------------------------------------
|
|
1436
|
+
read(key) {
|
|
1345
1437
|
try {
|
|
1346
|
-
|
|
1347
|
-
|
|
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
|
-
|
|
1443
|
+
write(key, value) {
|
|
1444
|
+
this.mem[key] = value;
|
|
1355
1445
|
try {
|
|
1356
|
-
|
|
1357
|
-
|
|
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
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
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
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
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
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
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
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
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
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
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
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
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
|
-
|
|
1450
|
-
|
|
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
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
}
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
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
|
-
*
|
|
1530
|
-
*
|
|
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
|
|
1861
|
+
async detectAuthCallback() {
|
|
1862
|
+
if (this.isServerMode() || typeof window === "undefined") {
|
|
1863
|
+
return;
|
|
1864
|
+
}
|
|
1535
1865
|
try {
|
|
1536
|
-
const
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
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
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
const
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
*/
|
|
1579
|
-
async uploadAuto(file) {
|
|
1886
|
+
// ============================================================================
|
|
1887
|
+
// Sign Up / Sign In / Sign Out
|
|
1888
|
+
// ============================================================================
|
|
1889
|
+
async signUp(request) {
|
|
1580
1890
|
try {
|
|
1581
|
-
const
|
|
1582
|
-
|
|
1583
|
-
|
|
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
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
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
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
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
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
)
|
|
1686
|
-
}
|
|
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
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
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
|
|
1696
|
-
|
|
1697
|
-
|
|
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 (!
|
|
1700
|
-
|
|
1701
|
-
|
|
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
|
-
|
|
1712
|
-
|
|
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:
|
|
1716
|
-
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
|
-
*
|
|
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
|
|
1742
|
-
const encoded = encodeURIComponent(path);
|
|
2023
|
+
async exchangeOAuthCode(code, codeVerifier) {
|
|
1743
2024
|
try {
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
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
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
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
|
-
*
|
|
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
|
|
1770
|
-
* @param
|
|
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
|
|
2064
|
+
async signInWithIdToken(credentials) {
|
|
1774
2065
|
try {
|
|
1775
|
-
const
|
|
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
|
-
|
|
1786
|
-
|
|
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
|
-
*
|
|
2091
|
+
* Refresh the current auth session.
|
|
1792
2092
|
*
|
|
1793
|
-
*
|
|
1794
|
-
*
|
|
2093
|
+
* Browser mode:
|
|
2094
|
+
* - Uses httpOnly refresh cookie and optional CSRF header.
|
|
1795
2095
|
*
|
|
1796
|
-
*
|
|
1797
|
-
*
|
|
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
|
|
2102
|
+
async refreshSession(options) {
|
|
1800
2103
|
try {
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
2129
|
+
return wrapError(
|
|
2130
|
+
error,
|
|
2131
|
+
"An unexpected error occurred during session refresh"
|
|
2132
|
+
);
|
|
1817
2133
|
}
|
|
1818
2134
|
}
|
|
1819
2135
|
/**
|
|
1820
|
-
*
|
|
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
|
|
2138
|
+
async getCurrentUser() {
|
|
2139
|
+
await this.authCallbackHandled;
|
|
1827
2140
|
try {
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
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
|
-
|
|
1833
|
-
|
|
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 (
|
|
1836
|
-
|
|
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
|
-
|
|
1839
|
-
|
|
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/
|
|
1843
|
-
{ params }
|
|
2188
|
+
`/api/auth/profiles/${userId}`
|
|
1844
2189
|
);
|
|
1845
2190
|
return { data: response, error: null };
|
|
1846
2191
|
} catch (error) {
|
|
1847
|
-
return
|
|
1848
|
-
|
|
1849
|
-
|
|
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.
|
|
1860
|
-
|
|
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
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
2215
|
+
return wrapError(
|
|
2216
|
+
error,
|
|
2217
|
+
"An unexpected error occurred while updating user profile"
|
|
2218
|
+
);
|
|
1868
2219
|
}
|
|
1869
2220
|
}
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
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
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
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
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
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
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
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
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
1905
|
-
*
|
|
1906
|
-
*
|
|
1907
|
-
*
|
|
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
|
-
*
|
|
1913
|
-
*
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
*
|
|
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
|
-
*
|
|
1925
|
-
*
|
|
1926
|
-
*
|
|
1927
|
-
*
|
|
1928
|
-
*
|
|
1929
|
-
*
|
|
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
|
|
1938
|
-
* const
|
|
1939
|
-
*
|
|
1940
|
-
*
|
|
1941
|
-
*
|
|
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
|
-
* //
|
|
1946
|
-
* const
|
|
1947
|
-
*
|
|
1948
|
-
*
|
|
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
|
-
* //
|
|
1953
|
-
* const
|
|
1954
|
-
*
|
|
1955
|
-
*
|
|
1956
|
-
*
|
|
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
|
-
*
|
|
1960
|
-
*
|
|
1961
|
-
*
|
|
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
|
-
|
|
2032
|
-
|
|
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
|
-
*
|
|
2409
|
+
* Call a PostgreSQL function (RPC)
|
|
2106
2410
|
*
|
|
2107
2411
|
* @example
|
|
2108
|
-
*
|
|
2109
|
-
*
|
|
2110
|
-
*
|
|
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
|
-
* //
|
|
2133
|
-
* const
|
|
2134
|
-
*
|
|
2135
|
-
*
|
|
2136
|
-
*
|
|
2137
|
-
* }
|
|
2138
|
-
*
|
|
2139
|
-
*/
|
|
2140
|
-
|
|
2141
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
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
|
|
2468
|
+
this.functionsUrl = functionsUrl;
|
|
2215
2469
|
}
|
|
2216
2470
|
/**
|
|
2217
|
-
* Derive the subhosting URL from the base URL
|
|
2218
|
-
*
|
|
2219
|
-
*
|
|
2220
|
-
*
|
|
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(
|
|
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
|
|
2254
|
-
*
|
|
2255
|
-
*
|
|
2256
|
-
* 2.
|
|
2257
|
-
*
|
|
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
|
|
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(
|
|
2289
|
-
|
|
2290
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
2449
|
-
this.
|
|
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: {
|
|
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: {
|
|
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(
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
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
|
-
|
|
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 {
|
|
2922
|
+
return {
|
|
2923
|
+
ok: true,
|
|
2924
|
+
channel,
|
|
2925
|
+
presence: { members: [...subscription.members.values()] }
|
|
2926
|
+
};
|
|
2631
2927
|
}
|
|
2632
2928
|
} else {
|
|
2633
|
-
subscription = {
|
|
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 {
|
|
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: {
|
|
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(
|
|
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
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
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
|
-
*
|
|
2706
|
-
*
|
|
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
|
|
3231
|
+
async requestDownloadStrategy(path, expiresIn) {
|
|
3232
|
+
const encoded = encodeURIComponent(path);
|
|
2709
3233
|
try {
|
|
2710
|
-
|
|
2711
|
-
|
|
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
|
-
|
|
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
|
|
2719
|
-
|
|
3276
|
+
error: error instanceof ResultError ? error : new ResultError(
|
|
3277
|
+
"Failed to create signed URL",
|
|
2720
3278
|
500,
|
|
2721
|
-
"
|
|
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
|
|
3285
|
+
* Create signed URLs for multiple objects in a single call.
|
|
2736
3286
|
*
|
|
2737
|
-
*
|
|
2738
|
-
*
|
|
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
|
-
*
|
|
2747
|
-
*
|
|
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
|
|
3293
|
+
async createSignedUrls(paths, expiresIn = 3600) {
|
|
2752
3294
|
try {
|
|
2753
|
-
const data = await
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
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
|
|
2761
|
-
|
|
2762
|
-
|
|
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
|
-
*
|
|
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
|
|
3327
|
+
async list(options) {
|
|
2770
3328
|
try {
|
|
2771
|
-
const
|
|
2772
|
-
|
|
2773
|
-
|
|
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
|
|
2778
|
-
|
|
2779
|
-
|
|
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
|
|
3379
|
+
var Storage = class {
|
|
2785
3380
|
constructor(http) {
|
|
2786
3381
|
this.http = http;
|
|
2787
3382
|
}
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
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
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
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
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
}
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
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
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
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
|
|
3479
|
+
async request(method, path, body) {
|
|
2839
3480
|
try {
|
|
2840
|
-
const
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
)
|
|
2844
|
-
|
|
2845
|
-
);
|
|
2846
|
-
|
|
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
|
-
|
|
2849
|
-
error
|
|
2850
|
-
|
|
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
|
-
|
|
3512
|
+
storageKey() {
|
|
3513
|
+
return `rsup_${this.handle}`;
|
|
3514
|
+
}
|
|
3515
|
+
load() {
|
|
2855
3516
|
try {
|
|
2856
|
-
const
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
)
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
} catch
|
|
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
|
-
|
|
3528
|
+
save(state) {
|
|
3529
|
+
this.mem = state;
|
|
2871
3530
|
try {
|
|
2872
|
-
|
|
2873
|
-
|
|
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
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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 '@
|
|
3620
|
+
* import { AuthChangeEvent } from '@resultdev/sdk';
|
|
2949
3621
|
*
|
|
2950
3622
|
* // Refresh a third-party-issued JWT periodically
|
|
2951
|
-
* const { token } = await fetch('/api/
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
};
|