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