@mitway/sdk 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1,2092 +1,5 @@
1
- "use strict";
2
- var __defProp = Object.defineProperty;
3
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
- var __getOwnPropNames = Object.getOwnPropertyNames;
5
- var __hasOwnProp = Object.prototype.hasOwnProperty;
6
- var __export = (target, all) => {
7
- for (var name in all)
8
- __defProp(target, name, { get: all[name], enumerable: true });
9
- };
10
- var __copyProps = (to, from, except, desc) => {
11
- if (from && typeof from === "object" || typeof from === "function") {
12
- for (let key of __getOwnPropNames(from))
13
- if (!__hasOwnProp.call(to, key) && key !== except)
14
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
- }
16
- return to;
17
- };
18
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
-
20
- // src/index.ts
21
- var index_exports = {};
22
- __export(index_exports, {
23
- Auth: () => Auth,
24
- Database: () => Database,
25
- HttpClient: () => HttpClient,
26
- Logger: () => Logger,
27
- MitwayBaasClient: () => MitwayBaasClient,
28
- MitwayBaasError: () => MitwayBaasError,
29
- Realtime: () => Realtime,
30
- RealtimeChannel: () => RealtimeChannel,
31
- Storage: () => Storage,
32
- StorageBucketClient: () => StorageBucketClient,
33
- TokenManager: () => TokenManager,
34
- createClient: () => createClient,
35
- default: () => index_default
36
- });
37
- module.exports = __toCommonJS(index_exports);
38
-
39
- // src/types.ts
40
- var MitwayBaasError = class _MitwayBaasError extends Error {
41
- statusCode;
42
- error;
43
- nextActions;
44
- constructor(message, statusCode, error, nextActions) {
45
- super(message);
46
- this.name = "MitwayBaasError";
47
- this.statusCode = statusCode;
48
- this.error = error;
49
- this.nextActions = nextActions;
50
- }
51
- static fromApiError(apiError) {
52
- return new _MitwayBaasError(
53
- apiError.message,
54
- apiError.statusCode,
55
- apiError.error,
56
- apiError.nextActions
57
- );
58
- }
59
- };
60
-
61
- // src/lib/logger.ts
62
- var SENSITIVE_HEADERS = ["authorization", "x-api-key", "cookie", "set-cookie"];
63
- var SENSITIVE_BODY_KEYS = [
64
- "password",
65
- "token",
66
- "accesstoken",
67
- "refreshtoken",
68
- "authorization",
69
- "secret",
70
- "apikey",
71
- "api_key",
72
- "email",
73
- "ssn",
74
- "creditcard",
75
- "credit_card"
76
- ];
77
- function redactHeaders(headers) {
78
- const redacted = {};
79
- for (const [key, value] of Object.entries(headers)) {
80
- if (SENSITIVE_HEADERS.includes(key.toLowerCase())) {
81
- redacted[key] = "***REDACTED***";
82
- } else {
83
- redacted[key] = value;
84
- }
85
- }
86
- return redacted;
87
- }
88
- function sanitizeBody(body) {
89
- if (body === null || body === void 0) return body;
90
- if (typeof body === "string") {
91
- try {
92
- const parsed = JSON.parse(body);
93
- return sanitizeBody(parsed);
94
- } catch {
95
- return body;
96
- }
97
- }
98
- if (Array.isArray(body)) return body.map(sanitizeBody);
99
- if (typeof body === "object") {
100
- const sanitized = {};
101
- for (const [key, value] of Object.entries(body)) {
102
- if (SENSITIVE_BODY_KEYS.includes(key.toLowerCase().replace(/[-_]/g, ""))) {
103
- sanitized[key] = "***REDACTED***";
104
- } else {
105
- sanitized[key] = sanitizeBody(value);
106
- }
107
- }
108
- return sanitized;
109
- }
110
- return body;
111
- }
112
- function formatBody(body) {
113
- if (body === void 0 || body === null) return "";
114
- if (typeof body === "string") {
115
- try {
116
- return JSON.stringify(JSON.parse(body), null, 2);
117
- } catch {
118
- return body;
119
- }
120
- }
121
- if (typeof FormData !== "undefined" && body instanceof FormData) {
122
- return "[FormData]";
123
- }
124
- try {
125
- return JSON.stringify(body, null, 2);
126
- } catch {
127
- return "[Unserializable body]";
128
- }
129
- }
130
- var Logger = class {
131
- enabled;
132
- customLog;
133
- constructor(debug) {
134
- if (typeof debug === "function") {
135
- this.enabled = true;
136
- this.customLog = debug;
137
- } else {
138
- this.enabled = !!debug;
139
- this.customLog = null;
140
- }
141
- }
142
- log(message, ...args) {
143
- if (!this.enabled) return;
144
- const formatted = `[MITWAY-BaaS Debug] ${message}`;
145
- if (this.customLog) {
146
- this.customLog(formatted, ...args);
147
- } else {
148
- console.log(formatted, ...args);
149
- }
150
- }
151
- warn(message, ...args) {
152
- if (!this.enabled) return;
153
- const formatted = `[MITWAY-BaaS Debug] ${message}`;
154
- if (this.customLog) {
155
- this.customLog(formatted, ...args);
156
- } else {
157
- console.warn(formatted, ...args);
158
- }
159
- }
160
- error(message, ...args) {
161
- if (!this.enabled) return;
162
- const formatted = `[MITWAY-BaaS Debug] ${message}`;
163
- if (this.customLog) {
164
- this.customLog(formatted, ...args);
165
- } else {
166
- console.error(formatted, ...args);
167
- }
168
- }
169
- logRequest(method, url, headers, body) {
170
- if (!this.enabled) return;
171
- const parts = [`\u2192 ${method} ${url}`];
172
- if (headers && Object.keys(headers).length > 0) {
173
- parts.push(` Headers: ${JSON.stringify(redactHeaders(headers))}`);
174
- }
175
- const formattedBody = formatBody(sanitizeBody(body));
176
- if (formattedBody) {
177
- const truncated = formattedBody.length > 1e3 ? formattedBody.slice(0, 1e3) + "... [truncated]" : formattedBody;
178
- parts.push(` Body: ${truncated}`);
179
- }
180
- this.log(parts.join("\n"));
181
- }
182
- logResponse(method, url, status, durationMs, body) {
183
- if (!this.enabled) return;
184
- const parts = [
185
- `\u2190 ${method} ${url} ${status} (${durationMs}ms)`
186
- ];
187
- const formattedBody = formatBody(sanitizeBody(body));
188
- if (formattedBody) {
189
- const truncated = formattedBody.length > 1e3 ? formattedBody.slice(0, 1e3) + "... [truncated]" : formattedBody;
190
- parts.push(` Body: ${truncated}`);
191
- }
192
- if (status >= 400) {
193
- this.error(parts.join("\n"));
194
- } else {
195
- this.log(parts.join("\n"));
196
- }
197
- }
198
- };
199
-
200
- // src/lib/token-manager.ts
201
- var CSRF_TOKEN_COOKIE = "mitway_baas_csrf_token";
202
- var DEFAULT_STORAGE_KEY = "mitway_baas_session";
203
- function getCsrfToken() {
204
- if (typeof document === "undefined") return null;
205
- const match = document.cookie.split(";").find((c) => c.trim().startsWith(`${CSRF_TOKEN_COOKIE}=`));
206
- if (!match) return null;
207
- return match.split("=")[1] || null;
208
- }
209
- function setCsrfToken(token) {
210
- if (typeof document === "undefined") return;
211
- const maxAge = 7 * 24 * 60 * 60;
212
- const secure = typeof window !== "undefined" && window.location.protocol === "https:" ? "; Secure" : "";
213
- document.cookie = `${CSRF_TOKEN_COOKIE}=${encodeURIComponent(token)}; path=/; max-age=${maxAge}; SameSite=Lax${secure}`;
214
- }
215
- function clearCsrfToken() {
216
- if (typeof document === "undefined") return;
217
- const secure = typeof window !== "undefined" && window.location.protocol === "https:" ? "; Secure" : "";
218
- document.cookie = `${CSRF_TOKEN_COOKIE}=; path=/; max-age=0; SameSite=Lax${secure}`;
219
- }
220
- var TokenManager = class {
221
- accessToken = null;
222
- refreshToken = null;
223
- user = null;
224
- persistSession;
225
- storageKey;
226
- /** Fired when the access token changes (used by long-lived consumers). */
227
- onTokenChange = null;
228
- constructor(opts) {
229
- this.persistSession = opts?.persistSession ?? true;
230
- this.storageKey = opts?.storageKey ?? DEFAULT_STORAGE_KEY;
231
- }
232
- saveSession(session) {
233
- const tokenChanged = session.accessToken !== this.accessToken;
234
- this.accessToken = session.accessToken;
235
- this.user = session.user;
236
- if (session.refreshToken !== void 0) {
237
- this.refreshToken = session.refreshToken ?? null;
238
- }
239
- this.persist();
240
- if (tokenChanged && this.onTokenChange) {
241
- this.onTokenChange();
242
- }
243
- }
244
- getSession() {
245
- if (!this.accessToken || !this.user) return null;
246
- return {
247
- accessToken: this.accessToken,
248
- refreshToken: this.refreshToken ?? void 0,
249
- user: this.user
250
- };
251
- }
252
- getAccessToken() {
253
- return this.accessToken;
254
- }
255
- setAccessToken(token) {
256
- const tokenChanged = token !== this.accessToken;
257
- this.accessToken = token;
258
- this.persist();
259
- if (tokenChanged && this.onTokenChange) {
260
- this.onTokenChange();
261
- }
262
- }
263
- getRefreshToken() {
264
- return this.refreshToken;
265
- }
266
- setRefreshToken(token) {
267
- this.refreshToken = token;
268
- this.persist();
269
- }
270
- getUser() {
271
- return this.user;
272
- }
273
- setUser(user) {
274
- this.user = user;
275
- this.persist();
276
- }
277
- clearSession() {
278
- const hadToken = this.accessToken !== null;
279
- this.accessToken = null;
280
- this.refreshToken = null;
281
- this.user = null;
282
- this.removePersisted();
283
- if (hadToken && this.onTokenChange) {
284
- this.onTokenChange();
285
- }
286
- }
287
- /**
288
- * Restore the session from localStorage. Returns true if a persisted
289
- * session was found and loaded into memory.
290
- */
291
- restoreSession() {
292
- if (!this.persistSession || typeof localStorage === "undefined") return false;
293
- try {
294
- const raw = localStorage.getItem(this.storageKey);
295
- if (!raw) return false;
296
- const stored = JSON.parse(raw);
297
- if (!stored.accessToken || !stored.user) return false;
298
- this.accessToken = stored.accessToken;
299
- this.refreshToken = stored.refreshToken ?? null;
300
- this.user = stored.user;
301
- return true;
302
- } catch {
303
- return false;
304
- }
305
- }
306
- persist() {
307
- if (!this.persistSession || typeof localStorage === "undefined") return;
308
- if (!this.accessToken || !this.user) return;
309
- try {
310
- const data = {
311
- accessToken: this.accessToken,
312
- user: this.user
313
- };
314
- if (this.refreshToken) {
315
- data.refreshToken = this.refreshToken;
316
- }
317
- localStorage.setItem(this.storageKey, JSON.stringify(data));
318
- } catch {
319
- }
320
- }
321
- removePersisted() {
322
- if (!this.persistSession || typeof localStorage === "undefined") return;
323
- try {
324
- localStorage.removeItem(this.storageKey);
325
- } catch {
326
- }
327
- }
328
- };
329
-
330
- // src/lib/auth-envelope.ts
331
- function normalizeAuthPayload(raw) {
332
- if (!raw || typeof raw !== "object") return raw;
333
- const src = raw;
334
- const out = { ...src };
335
- let mutated = false;
336
- if ("access_token" in src && !("accessToken" in src)) {
337
- out.accessToken = src.access_token;
338
- delete out.access_token;
339
- mutated = true;
340
- }
341
- if ("csrf_token" in src && !("csrfToken" in src)) {
342
- out.csrfToken = src.csrf_token;
343
- delete out.csrf_token;
344
- mutated = true;
345
- }
346
- if ("refresh_token" in src && !("refreshToken" in src)) {
347
- out.refreshToken = src.refresh_token;
348
- delete out.refresh_token;
349
- mutated = true;
350
- }
351
- return mutated ? out : raw;
352
- }
353
-
354
- // src/lib/http-client.ts
355
- var RETRYABLE_STATUS_CODES = /* @__PURE__ */ new Set([500, 502, 503, 504]);
356
- var IDEMPOTENT_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
357
- var HttpClient = class {
358
- baseUrl;
359
- fetch;
360
- defaultHeaders;
361
- anonKey;
362
- userToken = null;
363
- logger;
364
- autoRefreshToken = true;
365
- isRefreshing = false;
366
- refreshPromise = null;
367
- tokenManager;
368
- refreshToken = null;
369
- timeout;
370
- retryCount;
371
- retryDelay;
372
- constructor(config, tokenManager, logger) {
373
- this.baseUrl = config.baseUrl || "http://localhost:7130";
374
- this.autoRefreshToken = config.autoRefreshToken ?? true;
375
- this.fetch = config.fetch || (globalThis.fetch ? globalThis.fetch.bind(globalThis) : void 0);
376
- this.anonKey = config.anonKey;
377
- this.defaultHeaders = {
378
- ...config.headers
379
- };
380
- this.tokenManager = tokenManager ?? new TokenManager();
381
- this.logger = logger || new Logger(false);
382
- this.timeout = config.timeout ?? 3e4;
383
- this.retryCount = config.retryCount ?? 3;
384
- this.retryDelay = config.retryDelay ?? 500;
385
- if (!this.fetch) {
386
- throw new Error(
387
- "Fetch is not available. Provide a fetch implementation in the SDK config."
388
- );
389
- }
390
- }
391
- buildUrl(path, params) {
392
- const url = new URL(path, this.baseUrl);
393
- if (params) {
394
- Object.entries(params).forEach(([key, value]) => {
395
- if (key === "select") {
396
- let normalizedValue = value.replace(/\s+/g, " ").trim();
397
- normalizedValue = normalizedValue.replace(/\s*\(\s*/g, "(").replace(/\s*\)\s*/g, ")").replace(/\(\s+/g, "(").replace(/\s+\)/g, ")").replace(/,\s+(?=[^()]*\))/g, ",");
398
- url.searchParams.append(key, normalizedValue);
399
- } else {
400
- url.searchParams.append(key, value);
401
- }
402
- });
403
- }
404
- return url.toString();
405
- }
406
- isRetryableStatus(status) {
407
- return RETRYABLE_STATUS_CODES.has(status);
408
- }
409
- computeRetryDelay(attempt) {
410
- const base = this.retryDelay * Math.pow(2, attempt - 1);
411
- const jitter = base * (0.85 + Math.random() * 0.3);
412
- return Math.round(jitter);
413
- }
414
- async handleRequest(method, path, options = {}) {
415
- const {
416
- params,
417
- headers = {},
418
- body,
419
- signal: callerSignal,
420
- ...fetchOptions
421
- } = options;
422
- const url = this.buildUrl(path, params);
423
- const startTime = Date.now();
424
- const canRetry = IDEMPOTENT_METHODS.has(method.toUpperCase()) || options.idempotent === true;
425
- const maxAttempts = canRetry ? this.retryCount : 0;
426
- const requestHeaders = {
427
- ...this.defaultHeaders
428
- };
429
- const authToken = this.userToken || this.anonKey;
430
- if (authToken) {
431
- requestHeaders["Authorization"] = `Bearer ${authToken}`;
432
- }
433
- let processedBody;
434
- if (body !== void 0) {
435
- if (typeof FormData !== "undefined" && body instanceof FormData) {
436
- processedBody = body;
437
- } else {
438
- if (method !== "GET") {
439
- requestHeaders["Content-Type"] = "application/json;charset=UTF-8";
440
- }
441
- processedBody = JSON.stringify(body);
442
- }
443
- }
444
- if (headers instanceof Headers) {
445
- headers.forEach((value, key) => {
446
- requestHeaders[key] = value;
447
- });
448
- } else if (Array.isArray(headers)) {
449
- headers.forEach(([key, value]) => {
450
- requestHeaders[key] = value;
451
- });
452
- } else {
453
- Object.assign(requestHeaders, headers);
454
- }
455
- this.logger.logRequest(method, url, requestHeaders, processedBody);
456
- let lastError;
457
- for (let attempt = 0; attempt <= maxAttempts; attempt++) {
458
- if (attempt > 0) {
459
- const delay = this.computeRetryDelay(attempt);
460
- this.logger.warn(
461
- `Retry ${attempt}/${maxAttempts} for ${method} ${url} in ${delay}ms`
462
- );
463
- if (callerSignal?.aborted) throw callerSignal.reason;
464
- await new Promise((resolve, reject) => {
465
- const onAbort = () => {
466
- clearTimeout(timer2);
467
- reject(callerSignal.reason);
468
- };
469
- const timer2 = setTimeout(() => {
470
- if (callerSignal)
471
- callerSignal.removeEventListener("abort", onAbort);
472
- resolve();
473
- }, delay);
474
- if (callerSignal) {
475
- callerSignal.addEventListener("abort", onAbort, { once: true });
476
- }
477
- });
478
- }
479
- let controller;
480
- let timer;
481
- if (this.timeout > 0 || callerSignal) {
482
- controller = new AbortController();
483
- if (this.timeout > 0) {
484
- timer = setTimeout(() => controller.abort(), this.timeout);
485
- }
486
- if (callerSignal) {
487
- if (callerSignal.aborted) {
488
- controller.abort(callerSignal.reason);
489
- } else {
490
- const onCallerAbort = () => controller.abort(callerSignal.reason);
491
- callerSignal.addEventListener("abort", onCallerAbort, { once: true });
492
- controller.signal.addEventListener(
493
- "abort",
494
- () => {
495
- callerSignal.removeEventListener("abort", onCallerAbort);
496
- },
497
- { once: true }
498
- );
499
- }
500
- }
501
- }
502
- try {
503
- const response = await this.fetch(url, {
504
- method,
505
- headers: requestHeaders,
506
- body: processedBody,
507
- ...fetchOptions,
508
- ...controller ? { signal: controller.signal } : {}
509
- });
510
- if (this.isRetryableStatus(response.status) && attempt < maxAttempts) {
511
- if (timer !== void 0) clearTimeout(timer);
512
- await response.body?.cancel();
513
- lastError = new MitwayBaasError(
514
- `Server error: ${response.status} ${response.statusText}`,
515
- response.status,
516
- "SERVER_ERROR"
517
- );
518
- continue;
519
- }
520
- if (response.status === 204) {
521
- if (timer !== void 0) clearTimeout(timer);
522
- return void 0;
523
- }
524
- let data;
525
- const contentType = response.headers.get("content-type");
526
- try {
527
- if (contentType?.includes("json")) {
528
- data = await response.json();
529
- } else {
530
- data = await response.text();
531
- }
532
- } catch (parseErr) {
533
- if (timer !== void 0) clearTimeout(timer);
534
- throw new MitwayBaasError(
535
- `Failed to parse response body: ${parseErr?.message || "Unknown error"}`,
536
- response.status,
537
- response.ok ? "PARSE_ERROR" : "REQUEST_FAILED"
538
- );
539
- }
540
- if (timer !== void 0) clearTimeout(timer);
541
- if (!response.ok) {
542
- this.logger.logResponse(
543
- method,
544
- url,
545
- response.status,
546
- Date.now() - startTime,
547
- data
548
- );
549
- if (data && typeof data === "object" && "error" in data && data.error !== null && typeof data.error === "object") {
550
- const envErr = data.error;
551
- throw new MitwayBaasError(
552
- envErr.message || response.statusText || "Request failed",
553
- envErr.statusCode || response.status,
554
- envErr.code || envErr.error || "REQUEST_FAILED",
555
- envErr.nextActions
556
- );
557
- }
558
- throw new MitwayBaasError(
559
- `Request failed: ${response.statusText}`,
560
- response.status,
561
- "REQUEST_FAILED"
562
- );
563
- }
564
- this.logger.logResponse(
565
- method,
566
- url,
567
- response.status,
568
- Date.now() - startTime,
569
- data
570
- );
571
- if (data && typeof data === "object" && "data" in data && "error" in data && data.error === null) {
572
- return data.data;
573
- }
574
- return data;
575
- } catch (err) {
576
- if (timer !== void 0) clearTimeout(timer);
577
- if (err?.name === "AbortError") {
578
- if (controller && controller.signal.aborted && this.timeout > 0 && !callerSignal?.aborted) {
579
- throw new MitwayBaasError(
580
- `Request timed out after ${this.timeout}ms`,
581
- 408,
582
- "REQUEST_TIMEOUT"
583
- );
584
- }
585
- throw err;
586
- }
587
- if (err instanceof MitwayBaasError) {
588
- throw err;
589
- }
590
- if (attempt < maxAttempts) {
591
- lastError = err;
592
- continue;
593
- }
594
- throw new MitwayBaasError(
595
- `Network request failed: ${err?.message || "Unknown error"}`,
596
- 0,
597
- "NETWORK_ERROR"
598
- );
599
- }
600
- }
601
- throw lastError || new MitwayBaasError(
602
- "Request failed after all retry attempts",
603
- 0,
604
- "NETWORK_ERROR"
605
- );
606
- }
607
- async request(method, path, options = {}) {
608
- try {
609
- return await this.handleRequest(method, path, { ...options });
610
- } catch (error) {
611
- if (error instanceof MitwayBaasError && error.statusCode === 401 && error.error === "INVALID_TOKEN" && this.autoRefreshToken) {
612
- try {
613
- const newTokenData = await this.handleTokenRefresh();
614
- this.setAuthToken(newTokenData.accessToken);
615
- this.tokenManager.saveSession(newTokenData);
616
- if (newTokenData.csrfToken) {
617
- setCsrfToken(newTokenData.csrfToken);
618
- }
619
- if (newTokenData.refreshToken) {
620
- this.setRefreshToken(newTokenData.refreshToken);
621
- }
622
- return await this.handleRequest(method, path, { ...options });
623
- } catch (refreshError) {
624
- this.tokenManager.clearSession();
625
- this.userToken = null;
626
- this.refreshToken = null;
627
- clearCsrfToken();
628
- throw refreshError;
629
- }
630
- }
631
- throw error;
632
- }
633
- }
634
- /**
635
- * Low-level fetch helper for binary bodies (uploads) and streamed responses
636
- * (downloads). Applies the current Bearer token (user session → anon key
637
- * fallback) plus any configured default headers, resolves `path` against
638
- * `baseUrl`, and returns the raw `Response` — it does NOT unwrap the
639
- * `{ data, error }` envelope, so the caller is responsible for status
640
- * checking and parsing.
641
- *
642
- * Used by the storage module for object upload/download paths where the
643
- * body or response is not JSON.
644
- */
645
- async rawFetch(path, init = {}) {
646
- const url = this.buildUrl(path);
647
- const headers = new Headers(init.headers ?? {});
648
- for (const [k, v] of Object.entries(this.defaultHeaders)) {
649
- if (!headers.has(k)) headers.set(k, v);
650
- }
651
- if (!headers.has("Authorization")) {
652
- const token = this.userToken ?? this.anonKey;
653
- if (token) headers.set("Authorization", `Bearer ${token}`);
654
- }
655
- return this.fetch(url, { ...init, headers });
656
- }
657
- get(path, options) {
658
- return this.request("GET", path, options);
659
- }
660
- post(path, body, options) {
661
- return this.request("POST", path, { ...options, body });
662
- }
663
- put(path, body, options) {
664
- return this.request("PUT", path, { ...options, body });
665
- }
666
- patch(path, body, options) {
667
- return this.request("PATCH", path, { ...options, body });
668
- }
669
- delete(path, options) {
670
- return this.request("DELETE", path, options);
671
- }
672
- setAuthToken(token) {
673
- this.userToken = token;
674
- }
675
- setRefreshToken(token) {
676
- this.refreshToken = token;
677
- }
678
- getHeaders() {
679
- const headers = { ...this.defaultHeaders };
680
- const authToken = this.userToken || this.anonKey;
681
- if (authToken) {
682
- headers["Authorization"] = `Bearer ${authToken}`;
683
- }
684
- return headers;
685
- }
686
- /**
687
- * Refresh the current session by calling the MITWAY-BaaS refresh endpoint.
688
- * Note: the route is `/api/auth/refresh` (POST), not the InsForge
689
- * `/api/auth/sessions/current` route. Returns the new access token + user.
690
- */
691
- async handleTokenRefresh() {
692
- if (this.isRefreshing) {
693
- return this.refreshPromise;
694
- }
695
- this.isRefreshing = true;
696
- this.refreshPromise = (async () => {
697
- try {
698
- const csrfToken = getCsrfToken();
699
- const body = this.refreshToken ? { refreshToken: this.refreshToken } : void 0;
700
- const response = await this.handleRequest(
701
- "POST",
702
- "/api/auth/refresh",
703
- {
704
- body,
705
- headers: csrfToken ? { "X-CSRF-Token": csrfToken } : {},
706
- credentials: "include"
707
- }
708
- );
709
- return normalizeAuthPayload(response);
710
- } finally {
711
- this.isRefreshing = false;
712
- this.refreshPromise = null;
713
- }
714
- })();
715
- return this.refreshPromise;
716
- }
717
- };
718
-
719
- // src/modules/auth.ts
720
- function wrapError(error, fallbackMessage) {
721
- if (error instanceof MitwayBaasError) {
722
- return { data: null, error };
723
- }
724
- return {
725
- data: null,
726
- error: new MitwayBaasError(
727
- error instanceof Error ? error.message : fallbackMessage,
728
- 500,
729
- "AUTH_ERROR"
730
- )
731
- };
732
- }
733
- var Auth = class {
734
- constructor(http, tokenManager) {
735
- this.http = http;
736
- this.tokenManager = tokenManager;
737
- }
738
- http;
739
- tokenManager;
740
- /**
741
- * Persist the session in memory + HttpClient defaults so subsequent
742
- * requests carry the new bearer token automatically.
743
- */
744
- saveSessionFromResponse(response) {
745
- const session = {
746
- accessToken: response.accessToken,
747
- refreshToken: response.refreshToken,
748
- user: response.user
749
- };
750
- if (response.csrfToken) {
751
- setCsrfToken(response.csrfToken);
752
- }
753
- this.tokenManager.saveSession(session);
754
- this.http.setAuthToken(response.accessToken);
755
- this.http.setRefreshToken(response.refreshToken ?? null);
756
- }
757
- /**
758
- * Create a new user account and start a session.
759
- *
760
- * @example
761
- * const { data, error } = await client.auth.signUp({
762
- * email: 'a@b.com',
763
- * password: 'a-strong-password',
764
- * name: 'Alice'
765
- * });
766
- */
767
- async signUp(request) {
768
- try {
769
- const raw = await this.http.post(
770
- "/api/auth/register",
771
- request,
772
- { credentials: "include" }
773
- );
774
- const response = normalizeAuthPayload(raw);
775
- if (response?.accessToken && response.user) {
776
- this.saveSessionFromResponse(response);
777
- }
778
- return { data: response, error: null };
779
- } catch (error) {
780
- return wrapError(error, "Sign up failed");
781
- }
782
- }
783
- /**
784
- * Sign in with email + password and start a session.
785
- */
786
- async signInWithPassword(request) {
787
- try {
788
- const raw = await this.http.post(
789
- "/api/auth/login",
790
- request,
791
- { credentials: "include" }
792
- );
793
- const response = normalizeAuthPayload(raw);
794
- if (response?.accessToken && response.user) {
795
- this.saveSessionFromResponse(response);
796
- }
797
- return { data: response, error: null };
798
- } catch (error) {
799
- return wrapError(error, "Sign in failed");
800
- }
801
- }
802
- /**
803
- * End the current session. Clears in-memory state even if the backend
804
- * call fails (network/offline).
805
- */
806
- async signOut() {
807
- try {
808
- try {
809
- await this.http.post("/api/auth/logout", void 0, {
810
- credentials: "include"
811
- });
812
- } catch {
813
- }
814
- this.tokenManager.clearSession();
815
- this.http.setAuthToken(null);
816
- this.http.setRefreshToken(null);
817
- clearCsrfToken();
818
- return { error: null };
819
- } catch {
820
- return {
821
- error: new MitwayBaasError("Failed to sign out", 500, "SIGNOUT_ERROR")
822
- };
823
- }
824
- }
825
- /**
826
- * Manually refresh the current session. The HttpClient will call this
827
- * automatically on 401 INVALID_TOKEN responses; consumers usually do
828
- * not need to call it directly.
829
- */
830
- async refreshSession() {
831
- try {
832
- const response = await this.http.handleTokenRefresh();
833
- if (response?.accessToken && response.user) {
834
- this.saveSessionFromResponse(response);
835
- }
836
- return { data: response, error: null };
837
- } catch (error) {
838
- return wrapError(error, "Session refresh failed");
839
- }
840
- }
841
- /**
842
- * Restore the session from localStorage and validate it with the backend.
843
- * Call this once on app startup (e.g. in a React AuthProvider useEffect).
844
- *
845
- * Flow:
846
- * 1. Read persisted session from localStorage.
847
- * 2. Populate in-memory state (TokenManager + HttpClient).
848
- * 3. Validate with `GET /api/auth/sessions/current`.
849
- * - If the access token expired, the HttpClient auto-refresh kicks in
850
- * using the persisted refresh token (sent in the POST body, not
851
- * cookies — works cross-site).
852
- * 4. Return the validated user or an error.
853
- *
854
- * If no persisted session exists, returns `{ data: null, error }` — the
855
- * app should show the login page.
856
- */
857
- async initialize() {
858
- const restored = this.tokenManager.restoreSession();
859
- if (!restored) {
860
- return {
861
- data: null,
862
- error: new MitwayBaasError("No persisted session", 0, "NO_SESSION")
863
- };
864
- }
865
- const session = this.tokenManager.getSession();
866
- if (!session) {
867
- return {
868
- data: null,
869
- error: new MitwayBaasError("No persisted session", 0, "NO_SESSION")
870
- };
871
- }
872
- this.http.setAuthToken(session.accessToken);
873
- const refreshToken = this.tokenManager.getRefreshToken();
874
- if (refreshToken) {
875
- this.http.setRefreshToken(refreshToken);
876
- }
877
- try {
878
- const response = await this.http.get(
879
- "/api/auth/sessions/current"
880
- );
881
- if (response?.user) {
882
- this.tokenManager.setUser(response.user);
883
- return {
884
- data: {
885
- user: response.user,
886
- accessToken: session.accessToken
887
- },
888
- error: null
889
- };
890
- }
891
- this.tokenManager.clearSession();
892
- this.http.setAuthToken(null);
893
- this.http.setRefreshToken(null);
894
- return {
895
- data: null,
896
- error: new MitwayBaasError("Invalid session", 401, "INVALID_SESSION")
897
- };
898
- } catch (error) {
899
- this.tokenManager.clearSession();
900
- this.http.setAuthToken(null);
901
- this.http.setRefreshToken(null);
902
- return wrapError(error, "Session restore failed");
903
- }
904
- }
905
- /**
906
- * Get the current in-memory session, or null if the user is not signed in.
907
- * Synchronous — does not hit the network.
908
- */
909
- getSession() {
910
- return this.tokenManager.getSession();
911
- }
912
- /**
913
- * Get the current in-memory user, or null if not signed in.
914
- */
915
- getUser() {
916
- return this.tokenManager.getUser();
917
- }
918
- /**
919
- * Fetch the current user from the backend. Unlike getUser() which reads
920
- * from memory, this makes a network request and returns the latest data.
921
- */
922
- async getCurrentUser() {
923
- try {
924
- const response = await this.http.get(
925
- "/api/auth/sessions/current"
926
- );
927
- if (response?.user) {
928
- const session = {
929
- accessToken: this.tokenManager.getSession()?.accessToken ?? "",
930
- user: response.user
931
- };
932
- this.tokenManager.saveSession(session);
933
- }
934
- return { data: response, error: null };
935
- } catch (error) {
936
- return wrapError(error, "Failed to get current user");
937
- }
938
- }
939
- /**
940
- * Get a user's profile by ID. Requires authentication.
941
- */
942
- async getProfile(userId) {
943
- try {
944
- const response = await this.http.get(`/api/auth/profiles/${encodeURIComponent(userId)}`);
945
- return { data: response, error: null };
946
- } catch (error) {
947
- return wrapError(
948
- error,
949
- "Failed to get profile"
950
- );
951
- }
952
- }
953
- /**
954
- * Update the current user's profile. Merges with existing profile data —
955
- * only the fields you pass are updated, existing fields are preserved.
956
- */
957
- async setProfile(profile) {
958
- try {
959
- const response = await this.http.patch("/api/auth/profiles/current", { profile });
960
- return { data: response, error: null };
961
- } catch (error) {
962
- return wrapError(
963
- error,
964
- "Failed to update profile"
965
- );
966
- }
967
- }
968
- };
969
-
970
- // src/modules/database.ts
971
- var import_postgrest_js = require("@supabase/postgrest-js");
972
- function createMitwayBaasFetch(httpClient, tokenManager, anonKey) {
973
- return async (input, init) => {
974
- const url = typeof input === "string" ? input : input.toString();
975
- const urlObj = new URL(url);
976
- const pathname = urlObj.pathname.startsWith("/") ? urlObj.pathname.slice(1) : urlObj.pathname;
977
- const rpcMatch = pathname.match(/^rpc\/(.+)$/);
978
- const endpoint = rpcMatch ? `/api/database/rpc/${rpcMatch[1]}` : `/api/database/records/${pathname}`;
979
- const targetUrl = `${httpClient.baseUrl}${endpoint}${urlObj.search}`;
980
- const headers = new Headers(init?.headers);
981
- if (!headers.has("Authorization")) {
982
- const token = tokenManager.getAccessToken() ?? anonKey;
983
- if (token) {
984
- headers.set("Authorization", `Bearer ${token}`);
985
- }
986
- }
987
- return fetch(targetUrl, { ...init, headers });
988
- };
989
- }
990
- var Database = class {
991
- postgrest;
992
- httpClient;
993
- constructor(httpClient, tokenManager, anonKey) {
994
- this.httpClient = httpClient;
995
- this.postgrest = new import_postgrest_js.PostgrestClient("http://dummy", {
996
- fetch: createMitwayBaasFetch(httpClient, tokenManager, anonKey),
997
- headers: {}
998
- });
999
- }
1000
- /**
1001
- * Build a PostgREST query against a table.
1002
- *
1003
- * @example
1004
- * const { data, error } = await client.database
1005
- * .from('posts')
1006
- * .select('*')
1007
- * .eq('user_id', userId)
1008
- * .order('created_at', { ascending: false })
1009
- * .limit(10);
1010
- *
1011
- * @example
1012
- * const { data, error } = await client.database
1013
- * .from('posts')
1014
- * .insert({ title: 'Hello', content: 'World' })
1015
- * .select()
1016
- * .single();
1017
- */
1018
- from(table) {
1019
- if (!table || typeof table !== "string") {
1020
- throw new MitwayBaasError(
1021
- "Database.from(table) requires a non-empty string",
1022
- 400,
1023
- "INVALID_TABLE_NAME"
1024
- );
1025
- }
1026
- return this.postgrest.from(table);
1027
- }
1028
- /**
1029
- * Call a PostgreSQL stored function (RPC).
1030
- *
1031
- * @example
1032
- * const { data, error } = await client.database
1033
- * .rpc('get_user_stats', { user_id: 123 });
1034
- */
1035
- rpc(fn, args, options) {
1036
- return this.postgrest.rpc(fn, args, options);
1037
- }
1038
- /**
1039
- * The backend base URL the database client is targeting. Useful for
1040
- * debugging — the actual PostgREST instance is internal and not
1041
- * reachable by the SDK directly.
1042
- */
1043
- getUrl() {
1044
- return this.httpClient.baseUrl;
1045
- }
1046
- };
1047
-
1048
- // src/modules/realtime.ts
1049
- var import_socket = require("socket.io-client");
1050
- var PRESENCE_HEARTBEAT_MS = 2e4;
1051
- function makeChannelError(code, message) {
1052
- const err = new Error(message);
1053
- err.code = code;
1054
- return err;
1055
- }
1056
- var DEFAULT_CONNECT_TIMEOUT_MS = 1e4;
1057
- var RealtimeChannel = class {
1058
- constructor(topic, realtime, options = {}) {
1059
- this.topic = topic;
1060
- this.realtime = realtime;
1061
- this.options = options;
1062
- }
1063
- topic;
1064
- realtime;
1065
- bindings = [];
1066
- state = "closed";
1067
- statusCallback = null;
1068
- /** Local presence state mirror — populated from `presence_state` /
1069
- * `presence_join` / `presence_leave` events. Read via `presenceState()`. */
1070
- presence = {};
1071
- /** Latest state this client has tracked. Non-null means the heartbeat
1072
- * timer is active and we'll re-emit this state every TTL/2. */
1073
- trackedState = null;
1074
- presenceHeartbeat = null;
1075
- /** Timestamp of the most recently received broadcast on this channel —
1076
- * used as the `since` anchor on replay after reconnect. ISO-8601. */
1077
- lastBroadcastTimestamp = null;
1078
- /** Configuration from `channel(topic, opts)`. Frozen at construction. */
1079
- options;
1080
- /** Whether this channel was opened as `private: true`. */
1081
- get isPrivate() {
1082
- return this.options.config?.private === true;
1083
- }
1084
- /** The user-supplied presence key, if any. */
1085
- get presenceKey() {
1086
- return this.options.config?.presence?.key;
1087
- }
1088
- /** Internal — exposed for Realtime to drive resubscription after a
1089
- * network hiccup. Returns the current lifecycle state. */
1090
- _state() {
1091
- return this.state;
1092
- }
1093
- /** Internal — called by `Realtime` when Socket.IO reconnects after a
1094
- * drop. Re-runs the registration flow; the backend assigns fresh
1095
- * subscription_ids and Socket.IO rejoins the per-subscription rooms,
1096
- * so events resume without developer intervention. The user-provided
1097
- * statusCallback (from the original subscribe()) fires again with
1098
- * 'SUBSCRIBED' or 'CHANNEL_ERROR' so the app can reflect state. */
1099
- async _rejoinAfterReconnect() {
1100
- if (this.state === "closed") {
1101
- return;
1102
- }
1103
- for (const b of this.bindings) {
1104
- if (b.type === "postgres_changes") {
1105
- b.subscriptionId = void 0;
1106
- }
1107
- }
1108
- this.state = "joining";
1109
- try {
1110
- await this.registerAllBindings();
1111
- if (this.trackedState) {
1112
- const socket = this.realtime._getSocket();
1113
- const key = this.presenceKey;
1114
- socket?.emit(
1115
- "realtime:presence:track",
1116
- key !== void 0 ? { channel: this.topic, state: this.trackedState, key } : { channel: this.topic, state: this.trackedState }
1117
- );
1118
- }
1119
- if (this.lastBroadcastTimestamp && this.bindings.some((b) => b.type === "broadcast")) {
1120
- void this.replay({ since: this.lastBroadcastTimestamp }).catch(() => void 0);
1121
- }
1122
- this.state = "joined";
1123
- this.statusCallback?.("SUBSCRIBED");
1124
- } catch (err) {
1125
- this.state = "errored";
1126
- this.statusCallback?.(
1127
- "CHANNEL_ERROR",
1128
- makeChannelError("REJOIN_FAILED", err instanceof Error ? err.message : String(err))
1129
- );
1130
- }
1131
- }
1132
- // ── implementation signature (not in public type surface).
1133
- // The callback type is intentionally broad: each overload above pins a
1134
- // specific payload shape, but TypeScript overload resolution needs the
1135
- // implementation to accept the union of every narrow callback without
1136
- // the contravariance conflict (TS2394). `any` is the standard escape
1137
- // hatch for this exact pattern and is confined to this one line — the
1138
- // public surface users see is strictly typed via the overloads.
1139
- on(type, filter, callback) {
1140
- if (type === "postgres_changes") {
1141
- this.bindings.push({
1142
- type: "postgres_changes",
1143
- filter,
1144
- callback
1145
- });
1146
- } else if (type === "broadcast") {
1147
- this.bindings.push({
1148
- type: "broadcast",
1149
- filter,
1150
- callback
1151
- });
1152
- } else {
1153
- this.bindings.push({
1154
- type: "presence",
1155
- filter,
1156
- callback
1157
- });
1158
- }
1159
- return this;
1160
- }
1161
- // -------------------------------------------------------------------------
1162
- // Presence — track / untrack / state accessor
1163
- // -------------------------------------------------------------------------
1164
- /**
1165
- * Register or refresh this client's presence entry on the channel. Safe
1166
- * to call many times — state replaces (not merges). Starts a heartbeat
1167
- * timer at TTL/2 so the entry stays alive while the socket is open.
1168
- * The channel must be `subscribe()`d first (the server enforces this).
1169
- */
1170
- async track(state) {
1171
- const socket = this.realtime._getSocket();
1172
- if (!socket) {
1173
- throw new MitwayBaasError("Socket not connected", 503, "NOT_CONNECTED");
1174
- }
1175
- this.trackedState = state;
1176
- const key = this.presenceKey;
1177
- socket.emit(
1178
- "realtime:presence:track",
1179
- key !== void 0 ? { channel: this.topic, state, key } : { channel: this.topic, state }
1180
- );
1181
- if (!this.presenceHeartbeat) {
1182
- this.presenceHeartbeat = setInterval(() => {
1183
- const s = this.realtime._getSocket();
1184
- if (s && this.trackedState) {
1185
- s.emit(
1186
- "realtime:presence:track",
1187
- key !== void 0 ? { channel: this.topic, state: this.trackedState, key } : { channel: this.topic, state: this.trackedState }
1188
- );
1189
- }
1190
- }, PRESENCE_HEARTBEAT_MS);
1191
- const h = this.presenceHeartbeat;
1192
- h.unref?.();
1193
- }
1194
- }
1195
- /**
1196
- * Remove this client's presence entry immediately, stopping the
1197
- * heartbeat. Safe if the client never called `track`.
1198
- */
1199
- untrack() {
1200
- this.trackedState = null;
1201
- if (this.presenceHeartbeat) {
1202
- clearInterval(this.presenceHeartbeat);
1203
- this.presenceHeartbeat = null;
1204
- }
1205
- const socket = this.realtime._getSocket();
1206
- socket?.emit("realtime:presence:untrack", { channel: this.topic });
1207
- }
1208
- /** Snapshot of the current presence state on this channel. Keys are
1209
- * opaque client identifiers (server-assigned). Re-read inside your
1210
- * `.on('presence', { event: 'sync' }, ...)` handler. */
1211
- presenceState() {
1212
- return this.presence;
1213
- }
1214
- /**
1215
- * Register all bindings with the server:
1216
- * * For each `broadcast` binding, ensure we're subscribed to the topic
1217
- * (one `realtime:subscribe` for the channel as a whole).
1218
- * * For each `postgres_changes` binding, emit
1219
- * `realtime:postgres_changes:subscribe` and record the assigned
1220
- * subscription_id.
1221
- *
1222
- * `statusCallback` is invoked with `'SUBSCRIBED'` when every binding
1223
- * has ack'd, or with `'CHANNEL_ERROR' | 'TIMED_OUT'` on failure.
1224
- */
1225
- subscribe(statusCallback) {
1226
- this.statusCallback = statusCallback ?? null;
1227
- if (this.state === "joining" || this.state === "joined") {
1228
- return this;
1229
- }
1230
- this.state = "joining";
1231
- void this.realtime.connect().then(
1232
- async () => {
1233
- try {
1234
- await this.registerAllBindings();
1235
- this.state = "joined";
1236
- this.statusCallback?.("SUBSCRIBED");
1237
- } catch (err) {
1238
- this.state = "errored";
1239
- const message = err instanceof Error ? err.message : String(err);
1240
- this.statusCallback?.(
1241
- "CHANNEL_ERROR",
1242
- makeChannelError("SUBSCRIBE_FAILED", message)
1243
- );
1244
- }
1245
- },
1246
- (err) => {
1247
- this.state = "errored";
1248
- this.statusCallback?.(
1249
- "CHANNEL_ERROR",
1250
- makeChannelError("CONNECT_FAILED", err.message)
1251
- );
1252
- }
1253
- );
1254
- return this;
1255
- }
1256
- /**
1257
- * Tear down every binding: unsubscribe from the broadcast topic (if
1258
- * any broadcast bindings exist) and remove every postgres_changes
1259
- * subscription by id. Safe to call when state is already closed.
1260
- */
1261
- async unsubscribe() {
1262
- if (this.state === "closed") {
1263
- return;
1264
- }
1265
- const socket = this.realtime._getSocket();
1266
- if (this.presenceHeartbeat) {
1267
- clearInterval(this.presenceHeartbeat);
1268
- this.presenceHeartbeat = null;
1269
- }
1270
- if (!socket) {
1271
- this.trackedState = null;
1272
- this.state = "closed";
1273
- return;
1274
- }
1275
- if (this.trackedState) {
1276
- socket.emit("realtime:presence:untrack", { channel: this.topic });
1277
- this.trackedState = null;
1278
- }
1279
- const pcBindings = this.bindings.filter(
1280
- (b) => b.type === "postgres_changes"
1281
- );
1282
- for (const b of pcBindings) {
1283
- if (b.subscriptionId) {
1284
- socket.emit("realtime:postgres_changes:unsubscribe", {
1285
- subscription_id: b.subscriptionId
1286
- });
1287
- b.subscriptionId = void 0;
1288
- }
1289
- }
1290
- if (this.bindings.some((b) => b.type === "broadcast" || b.type === "presence")) {
1291
- socket.emit("realtime:unsubscribe", { channel: this.topic });
1292
- }
1293
- this.realtime._detachChannel(this);
1294
- this.state = "closed";
1295
- this.statusCallback?.("CLOSED");
1296
- }
1297
- /**
1298
- * Publish a broadcast event to the topic. Broadcasts are always
1299
- * ephemeral — the server fans out to every subscribed socket in memory,
1300
- * with no DB persistence or webhook fan-out. For audited / durable
1301
- * events, write to your own application table and enable
1302
- * `postgres_changes` on it; the SDK will surface the INSERT as a
1303
- * `postgres_changes` event without a separate channel.send call.
1304
- *
1305
- * The returned promise always resolves with the server ack, so callers
1306
- * can `await channel.send(...)` to confirm delivery + get the server-
1307
- * assigned `message_id`. There's no performance cost — Socket.IO piggy-
1308
- * backs the ack on the same frame. Callers that don't need it just
1309
- * don't await.
1310
- */
1311
- async send(args) {
1312
- if (args.type !== "broadcast") {
1313
- throw new MitwayBaasError(
1314
- 'Only "broadcast" sends are supported \u2014 DB changes flow via your DB writes, not channel.send()',
1315
- 400,
1316
- "UNSUPPORTED_SEND_TYPE"
1317
- );
1318
- }
1319
- const RESERVED_BROADCAST_NAMES = /* @__PURE__ */ new Set([
1320
- "postgres_changes",
1321
- "presence_state",
1322
- "presence_join",
1323
- "presence_leave"
1324
- ]);
1325
- if (RESERVED_BROADCAST_NAMES.has(args.event)) {
1326
- throw new MitwayBaasError(
1327
- `"${args.event}" is a reserved event name \u2014 pick a different name for broadcast events`,
1328
- 400,
1329
- "RESERVED_EVENT_NAME"
1330
- );
1331
- }
1332
- const socket = this.realtime._getSocket();
1333
- if (!socket) {
1334
- throw new MitwayBaasError("Socket not connected", 503, "NOT_CONNECTED");
1335
- }
1336
- const self = this.options.config?.broadcast?.self;
1337
- const wirePayload = {
1338
- channel: this.topic,
1339
- event: args.event,
1340
- payload: args.payload
1341
- };
1342
- if (self === false) {
1343
- wirePayload.self = false;
1344
- }
1345
- return await new Promise((resolve) => {
1346
- socket.emit(
1347
- "realtime:publish",
1348
- wirePayload,
1349
- (ack) => {
1350
- resolve(ack);
1351
- }
1352
- );
1353
- });
1354
- }
1355
- /**
1356
- * Replay SQL-originated broadcasts on this topic since the given
1357
- * timestamp. Delivered only to this socket (same envelope format as
1358
- * live broadcasts; the SDK routes them through `.on('broadcast', ...)`
1359
- * bindings just like the real-time path). Backend caps the window at
1360
- * 24 h and the limit at 1000.
1361
- */
1362
- async replay(args) {
1363
- const socket = this.realtime._getSocket();
1364
- if (!socket) {
1365
- throw new MitwayBaasError("Socket not connected", 503, "NOT_CONNECTED");
1366
- }
1367
- socket.emit("realtime:broadcast:replay", {
1368
- channel: this.topic,
1369
- since: args.since,
1370
- limit: args.limit,
1371
- private: this.isPrivate
1372
- });
1373
- }
1374
- /** Internal — called by Realtime's event router on every incoming event. */
1375
- _dispatch(event, envelope) {
1376
- if (event === "postgres_changes") {
1377
- const pcEvent = envelope;
1378
- for (const b of this.bindings) {
1379
- if (b.type !== "postgres_changes") {
1380
- continue;
1381
- }
1382
- if (!b.subscriptionId || !pcEvent.ids.includes(b.subscriptionId)) {
1383
- continue;
1384
- }
1385
- const matchesEvent = b.filter.event === "*" || b.filter.event === pcEvent.data.eventType;
1386
- if (!matchesEvent) {
1387
- continue;
1388
- }
1389
- try {
1390
- b.callback(pcEvent.data);
1391
- } catch {
1392
- }
1393
- }
1394
- return;
1395
- }
1396
- if (event === "presence_state" || event === "presence_join" || event === "presence_leave") {
1397
- const e = envelope;
1398
- if (e.channel !== this.topic) {
1399
- return;
1400
- }
1401
- if (event === "presence_state" && e.state) {
1402
- this.presence = { ...e.state };
1403
- this.firePresence({ event: "sync", state: this.presence });
1404
- } else if (event === "presence_join" && e.joins) {
1405
- Object.assign(this.presence, e.joins);
1406
- this.firePresence({ event: "join", joins: e.joins });
1407
- } else if (event === "presence_leave" && e.leaves) {
1408
- for (const key of Object.keys(e.leaves)) {
1409
- delete this.presence[key];
1410
- }
1411
- this.firePresence({ event: "leave", leaves: e.leaves });
1412
- }
1413
- return;
1414
- }
1415
- const meta = envelope.meta;
1416
- if (meta?.timestamp) {
1417
- if (!this.lastBroadcastTimestamp || meta.timestamp > this.lastBroadcastTimestamp) {
1418
- this.lastBroadcastTimestamp = meta.timestamp;
1419
- }
1420
- }
1421
- for (const b of this.bindings) {
1422
- if (b.type !== "broadcast") {
1423
- continue;
1424
- }
1425
- if (b.filter.event !== event) {
1426
- continue;
1427
- }
1428
- const { meta: _meta, ...payload } = envelope;
1429
- try {
1430
- b.callback({ type: "broadcast", event, payload });
1431
- } catch {
1432
- }
1433
- }
1434
- }
1435
- firePresence(payload) {
1436
- for (const b of this.bindings) {
1437
- if (b.type !== "presence") {
1438
- continue;
1439
- }
1440
- if (b.filter.event !== payload.event) {
1441
- continue;
1442
- }
1443
- try {
1444
- if (payload.event === "sync") {
1445
- b.callback();
1446
- } else if (payload.event === "join") {
1447
- b.callback(payload);
1448
- } else {
1449
- b.callback(payload);
1450
- }
1451
- } catch {
1452
- }
1453
- }
1454
- }
1455
- async registerAllBindings() {
1456
- const socket = this.realtime._getSocket();
1457
- if (!socket) {
1458
- throw new Error("Socket not available");
1459
- }
1460
- const needsRoomJoin = this.bindings.some(
1461
- (b) => b.type === "broadcast" || b.type === "presence"
1462
- );
1463
- if (needsRoomJoin) {
1464
- await new Promise((resolve, reject) => {
1465
- socket.emit(
1466
- "realtime:subscribe",
1467
- { channel: this.topic, private: this.isPrivate },
1468
- (ack) => {
1469
- if (ack.status === "ok") {
1470
- resolve();
1471
- } else {
1472
- reject(new Error(ack.error?.message ?? "subscribe failed"));
1473
- }
1474
- }
1475
- );
1476
- });
1477
- }
1478
- const pcBindings = this.bindings.filter(
1479
- (b) => b.type === "postgres_changes"
1480
- );
1481
- await Promise.all(
1482
- pcBindings.map(
1483
- (b) => new Promise((resolve, reject) => {
1484
- socket.emit(
1485
- "realtime:postgres_changes:subscribe",
1486
- {
1487
- event: b.filter.event,
1488
- schema: b.filter.schema ?? "public",
1489
- table: b.filter.table,
1490
- filter: b.filter.filter
1491
- },
1492
- (ack) => {
1493
- if (ack.status === "ok" && ack.subscription_id) {
1494
- b.subscriptionId = ack.subscription_id;
1495
- resolve();
1496
- } else {
1497
- reject(
1498
- new Error(ack.error?.message ?? "postgres_changes subscribe failed")
1499
- );
1500
- }
1501
- }
1502
- );
1503
- })
1504
- )
1505
- );
1506
- }
1507
- };
1508
- var Realtime = class {
1509
- socket = null;
1510
- baseUrl;
1511
- options;
1512
- anonKey;
1513
- tokenManager;
1514
- channels = /* @__PURE__ */ new Map();
1515
- connecting = null;
1516
- /** Flips to `true` once the initial handshake resolves. Differentiates
1517
- * the first `connect` event (part of `openSocket`) from subsequent
1518
- * reconnect events (which should trigger auto-resubscribe). */
1519
- firstConnected = false;
1520
- constructor(baseUrl, tokenManager, anonKey, options = {}) {
1521
- this.baseUrl = baseUrl;
1522
- this.tokenManager = tokenManager;
1523
- this.anonKey = anonKey;
1524
- this.options = options;
1525
- }
1526
- get isConnected() {
1527
- return this.socket?.connected === true;
1528
- }
1529
- get socketId() {
1530
- return this.socket?.id;
1531
- }
1532
- /**
1533
- * Get (or create) a channel for `topic`. Channels are cached so multiple
1534
- * `.channel('same')` calls return the same instance.
1535
- *
1536
- * The optional `opts` argument lets the caller configure the channel:
1537
- * * `config.private` — enable subscribe-side authorization against
1538
- * `realtime.authorize_subscribe(...)` on the tenant DB.
1539
- * * `config.broadcast.self` — `false` excludes the sender from the
1540
- * fan-out (defaults to `true`).
1541
- * * `config.presence.key` — stable presence key to group multiple
1542
- * tabs of the same user under one entry.
1543
- *
1544
- * `channel.send()` always resolves with the server ack (see its own
1545
- * docstring); there is no separate opt-in needed.
1546
- *
1547
- * Options are locked in when the channel is first created; subsequent
1548
- * `.channel('same')` calls with different opts are ignored. Pass a
1549
- * different topic to get a different-configured channel.
1550
- */
1551
- channel(topic, opts) {
1552
- const existing = this.channels.get(topic);
1553
- if (existing) {
1554
- return existing;
1555
- }
1556
- const channel = new RealtimeChannel(topic, this, opts);
1557
- this.channels.set(topic, channel);
1558
- return channel;
1559
- }
1560
- connect() {
1561
- if (this.isConnected) {
1562
- return Promise.resolve();
1563
- }
1564
- if (this.connecting) {
1565
- return this.connecting;
1566
- }
1567
- this.connecting = this.openSocket();
1568
- return this.connecting;
1569
- }
1570
- /**
1571
- * Close the socket. Channels are left as-is so they can re-subscribe
1572
- * on the next `connect()` — useful for auth token refresh flows.
1573
- */
1574
- disconnect() {
1575
- if (!this.socket) {
1576
- return;
1577
- }
1578
- this.socket.disconnect();
1579
- this.socket = null;
1580
- this.firstConnected = false;
1581
- }
1582
- // -------------------------------------------------------------------------
1583
- // internals used by RealtimeChannel
1584
- // -------------------------------------------------------------------------
1585
- /* istanbul ignore next — tested via channel integration */
1586
- _getSocket() {
1587
- return this.socket;
1588
- }
1589
- _detachChannel(channel) {
1590
- this.channels.delete(channel.topic);
1591
- }
1592
- openSocket() {
1593
- const token = this.tokenManager.getAccessToken() ?? this.anonKey;
1594
- if (!token) {
1595
- const err = new MitwayBaasError(
1596
- "Realtime requires an access token or anonKey",
1597
- 401,
1598
- "AUTH_INVALID_API_KEY"
1599
- );
1600
- this.connecting = null;
1601
- return Promise.reject(err);
1602
- }
1603
- const timeoutMs = this.options.timeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS;
1604
- const socket = (0, import_socket.io)(this.baseUrl, {
1605
- path: this.options.path,
1606
- transports: this.options.transports ?? ["websocket"],
1607
- auth: { token, ...this.options.extraAuth ?? {} },
1608
- reconnection: true,
1609
- timeout: timeoutMs
1610
- });
1611
- this.socket = socket;
1612
- socket.onAny((event, ...args) => this.dispatch(event, args));
1613
- socket.on("connect", () => {
1614
- if (!this.firstConnected) {
1615
- return;
1616
- }
1617
- for (const channel of this.channels.values()) {
1618
- const state = channel._state();
1619
- if (state === "joined" || state === "errored") {
1620
- void channel._rejoinAfterReconnect();
1621
- }
1622
- }
1623
- });
1624
- return new Promise((resolve, reject) => {
1625
- const timer = setTimeout(() => {
1626
- socket.off("connect", onConnect);
1627
- socket.off("connect_error", onConnectError);
1628
- this.connecting = null;
1629
- reject(
1630
- new MitwayBaasError(
1631
- `Realtime connection timeout after ${timeoutMs}ms`,
1632
- 408,
1633
- "CONNECTION_TIMEOUT"
1634
- )
1635
- );
1636
- }, timeoutMs);
1637
- const clear = () => {
1638
- clearTimeout(timer);
1639
- socket.off("connect", onConnect);
1640
- socket.off("connect_error", onConnectError);
1641
- };
1642
- const onConnect = () => {
1643
- clear();
1644
- this.connecting = null;
1645
- this.firstConnected = true;
1646
- resolve();
1647
- };
1648
- const onConnectError = (err) => {
1649
- clear();
1650
- this.connecting = null;
1651
- reject(new MitwayBaasError(err.message, 0, "CONNECTION_FAILED"));
1652
- };
1653
- socket.once("connect", onConnect);
1654
- socket.once("connect_error", onConnectError);
1655
- });
1656
- }
1657
- dispatch(event, args) {
1658
- if (event === "postgres_changes") {
1659
- const envelope2 = args[0] ?? {};
1660
- this.channels.forEach((ch) => ch._dispatch("postgres_changes", envelope2));
1661
- return;
1662
- }
1663
- if (event === "connect" || event === "disconnect" || event === "connect_error" || event === "error" || event === "realtime:error" || event === "realtime:shutdown") {
1664
- return;
1665
- }
1666
- const envelope = args[0] ?? {};
1667
- this.channels.forEach((ch) => ch._dispatch(event, envelope));
1668
- }
1669
- };
1670
-
1671
- // src/modules/storage.ts
1672
- function bucketFromWire(row) {
1673
- return {
1674
- id: row.id,
1675
- name: row.name,
1676
- public: row.public,
1677
- fileSizeLimitBytes: row.file_size_limit_bytes,
1678
- allowedMimeTypes: row.allowed_mime_types,
1679
- createdAt: row.created_at,
1680
- updatedAt: row.updated_at
1681
- };
1682
- }
1683
- function objectFromWire(row, bucketName) {
1684
- return {
1685
- id: row.id,
1686
- bucket: bucketName,
1687
- key: row.key,
1688
- size: row.size,
1689
- mimeType: row.mime_type,
1690
- etag: row.etag,
1691
- cacheControl: row.cache_control,
1692
- contentDisposition: row.content_disposition,
1693
- uploadedBy: row.uploaded_by,
1694
- uploadedAt: row.uploaded_at,
1695
- updatedAt: row.updated_at
1696
- };
1697
- }
1698
- function configFromWire(row) {
1699
- return {
1700
- defaultFileSizeLimitBytes: row.default_file_size_limit_bytes,
1701
- maxFileSizeLimitBytes: row.max_file_size_limit_bytes,
1702
- tenantStorageQuotaBytes: row.tenant_storage_quota_bytes,
1703
- reservedSpaceBytes: row.reserved_space_bytes,
1704
- signedUrlDefaultTtlSec: row.signed_url_default_ttl_sec,
1705
- signedUrlMaxTtlSec: row.signed_url_max_ttl_sec
1706
- };
1707
- }
1708
- function encodeKey(key) {
1709
- return key.split("/").map(encodeURIComponent).join("/");
1710
- }
1711
- function wrapError2(err, fallback) {
1712
- if (err instanceof MitwayBaasError) return { data: null, error: err };
1713
- return {
1714
- data: null,
1715
- error: new MitwayBaasError(
1716
- err instanceof Error ? err.message : fallback,
1717
- 0,
1718
- "STORAGE_ERROR"
1719
- )
1720
- };
1721
- }
1722
- async function readEnvelopeError(response) {
1723
- let code = "STORAGE_ERROR";
1724
- let message = `HTTP ${response.status}`;
1725
- try {
1726
- const body = await response.json();
1727
- if (body && body.error) {
1728
- code = body.error.code ?? code;
1729
- message = body.error.message ?? message;
1730
- }
1731
- } catch {
1732
- }
1733
- return new MitwayBaasError(message, response.status, code);
1734
- }
1735
- var StorageBucketClient = class {
1736
- constructor(http, bucketName) {
1737
- this.http = http;
1738
- this.bucketName = bucketName;
1739
- }
1740
- http;
1741
- bucketName;
1742
- bucketBase() {
1743
- return `/api/storage/buckets/${encodeURIComponent(this.bucketName)}`;
1744
- }
1745
- objectPath(key) {
1746
- return `${this.bucketBase()}/objects/${encodeKey(key)}`;
1747
- }
1748
- async upload(key, body, opts = {}) {
1749
- try {
1750
- const method = opts.upsert ? "PUT" : "POST";
1751
- const headers = {
1752
- "Content-Type": opts.contentType ?? "application/octet-stream"
1753
- };
1754
- if (opts.cacheControl) headers["Cache-Control"] = opts.cacheControl;
1755
- if (opts.contentDisposition)
1756
- headers["Content-Disposition"] = opts.contentDisposition;
1757
- const response = await this.http.rawFetch(this.objectPath(key), {
1758
- method,
1759
- headers,
1760
- body,
1761
- signal: opts.abortSignal
1762
- });
1763
- if (!response.ok) {
1764
- return { data: null, error: await readEnvelopeError(response) };
1765
- }
1766
- const parsed = await response.json();
1767
- if (parsed.error || !parsed.data) {
1768
- return {
1769
- data: null,
1770
- error: new MitwayBaasError(
1771
- parsed.error?.message ?? "Upload failed",
1772
- response.status,
1773
- parsed.error?.code ?? "STORAGE_ERROR"
1774
- )
1775
- };
1776
- }
1777
- return { data: objectFromWire(parsed.data, this.bucketName), error: null };
1778
- } catch (err) {
1779
- return wrapError2(err, "Upload failed");
1780
- }
1781
- }
1782
- async download(key, opts = {}) {
1783
- try {
1784
- const headers = {};
1785
- if (opts.range) {
1786
- headers["Range"] = `bytes=${opts.range.start}-${opts.range.end}`;
1787
- }
1788
- const response = await this.http.rawFetch(this.objectPath(key), {
1789
- method: "GET",
1790
- headers,
1791
- signal: opts.abortSignal
1792
- });
1793
- if (!response.ok) {
1794
- return { data: null, error: await readEnvelopeError(response) };
1795
- }
1796
- const blob = await response.blob();
1797
- return { data: blob, error: null };
1798
- } catch (err) {
1799
- return wrapError2(err, "Download failed");
1800
- }
1801
- }
1802
- async getStream(key, opts = {}) {
1803
- try {
1804
- const headers = {};
1805
- if (opts.range) {
1806
- headers["Range"] = `bytes=${opts.range.start}-${opts.range.end}`;
1807
- }
1808
- const response = await this.http.rawFetch(this.objectPath(key), {
1809
- method: "GET",
1810
- headers,
1811
- signal: opts.abortSignal
1812
- });
1813
- if (!response.ok) {
1814
- return { data: null, error: await readEnvelopeError(response) };
1815
- }
1816
- if (!response.body) {
1817
- return {
1818
- data: null,
1819
- error: new MitwayBaasError(
1820
- "Response body is not a stream",
1821
- response.status,
1822
- "STORAGE_ERROR"
1823
- )
1824
- };
1825
- }
1826
- return {
1827
- data: response.body,
1828
- error: null
1829
- };
1830
- } catch (err) {
1831
- return wrapError2(err, "Download failed");
1832
- }
1833
- }
1834
- async remove(keys) {
1835
- try {
1836
- const results = await Promise.allSettled(
1837
- keys.map(
1838
- (key) => this.http.rawFetch(this.objectPath(key), { method: "DELETE" })
1839
- )
1840
- );
1841
- const removed = [];
1842
- const errors = [];
1843
- for (let i = 0; i < keys.length; i++) {
1844
- const key = keys[i];
1845
- const r = results[i];
1846
- if (r.status === "fulfilled" && r.value.ok) {
1847
- removed.push(key);
1848
- } else if (r.status === "fulfilled") {
1849
- errors.push(`${key}: HTTP ${r.value.status}`);
1850
- } else {
1851
- const msg = r.reason instanceof Error ? r.reason.message : String(r.reason);
1852
- errors.push(`${key}: ${msg}`);
1853
- }
1854
- }
1855
- if (errors.length > 0) {
1856
- return {
1857
- data: null,
1858
- error: new MitwayBaasError(
1859
- `Failed to delete some objects: ${errors.join("; ")}`,
1860
- 0,
1861
- "STORAGE_ERROR"
1862
- )
1863
- };
1864
- }
1865
- return { data: { removed }, error: null };
1866
- } catch (err) {
1867
- return wrapError2(err, "Delete failed");
1868
- }
1869
- }
1870
- async list(opts = {}) {
1871
- try {
1872
- const params = {};
1873
- if (opts.prefix !== void 0) params.prefix = opts.prefix;
1874
- if (opts.limit !== void 0) params.limit = String(opts.limit);
1875
- if (opts.startAfter !== void 0) params.start_after = opts.startAfter;
1876
- const rows = await this.http.get(
1877
- `${this.bucketBase()}/objects`,
1878
- { params }
1879
- );
1880
- return {
1881
- data: rows.map((r) => objectFromWire(r, this.bucketName)),
1882
- error: null
1883
- };
1884
- } catch (err) {
1885
- return wrapError2(err, "List failed");
1886
- }
1887
- }
1888
- async copy(fromKey, toKey, toBucket) {
1889
- try {
1890
- const row = await this.http.post(
1891
- `${this.objectPath(fromKey)}/copy`,
1892
- {
1893
- dest_bucket: toBucket ?? this.bucketName,
1894
- dest_key: toKey
1895
- }
1896
- );
1897
- return {
1898
- data: objectFromWire(row, toBucket ?? this.bucketName),
1899
- error: null
1900
- };
1901
- } catch (err) {
1902
- return wrapError2(err, "Copy failed");
1903
- }
1904
- }
1905
- async move(fromKey, toKey, toBucket) {
1906
- try {
1907
- const row = await this.http.post(
1908
- `${this.objectPath(fromKey)}/move`,
1909
- {
1910
- dest_bucket: toBucket ?? this.bucketName,
1911
- dest_key: toKey
1912
- }
1913
- );
1914
- return {
1915
- data: objectFromWire(row, toBucket ?? this.bucketName),
1916
- error: null
1917
- };
1918
- } catch (err) {
1919
- return wrapError2(err, "Move failed");
1920
- }
1921
- }
1922
- async createSignedUrl(key, opts = {}) {
1923
- try {
1924
- const body = {};
1925
- if (opts.expiresIn !== void 0) body.expires_in = opts.expiresIn;
1926
- const wire = await this.http.post(`${this.objectPath(key)}/sign`, body);
1927
- return {
1928
- data: {
1929
- url: wire.url,
1930
- token: wire.token,
1931
- expiresAt: wire.expiresAt
1932
- },
1933
- error: null
1934
- };
1935
- } catch (err) {
1936
- return wrapError2(err, "Sign failed");
1937
- }
1938
- }
1939
- getPublicUrl(key) {
1940
- const url = `${this.http.baseUrl.replace(/\/$/, "")}${this.objectPath(key)}`;
1941
- return { data: { url } };
1942
- }
1943
- };
1944
- var Storage = class {
1945
- constructor(http) {
1946
- this.http = http;
1947
- }
1948
- http;
1949
- /** Scope subsequent operations to a single bucket. */
1950
- from(bucketName) {
1951
- return new StorageBucketClient(this.http, bucketName);
1952
- }
1953
- // --- Admin (require service_role) ---
1954
- async listBuckets() {
1955
- try {
1956
- const rows = await this.http.get("/api/storage/buckets");
1957
- return { data: rows.map(bucketFromWire), error: null };
1958
- } catch (err) {
1959
- return wrapError2(err, "listBuckets failed");
1960
- }
1961
- }
1962
- async getBucket(name) {
1963
- try {
1964
- const row = await this.http.get(
1965
- `/api/storage/buckets/${encodeURIComponent(name)}`
1966
- );
1967
- return { data: bucketFromWire(row), error: null };
1968
- } catch (err) {
1969
- return wrapError2(err, "getBucket failed");
1970
- }
1971
- }
1972
- async createBucket(name, opts = {}) {
1973
- try {
1974
- const body = { name };
1975
- if (opts.public !== void 0) body.public = opts.public;
1976
- if (opts.fileSizeLimitBytes !== void 0)
1977
- body.file_size_limit_bytes = opts.fileSizeLimitBytes;
1978
- if (opts.allowedMimeTypes !== void 0)
1979
- body.allowed_mime_types = opts.allowedMimeTypes;
1980
- const row = await this.http.post(
1981
- "/api/storage/buckets",
1982
- body
1983
- );
1984
- return { data: bucketFromWire(row), error: null };
1985
- } catch (err) {
1986
- return wrapError2(err, "createBucket failed");
1987
- }
1988
- }
1989
- async updateBucket(name, opts) {
1990
- try {
1991
- const body = {};
1992
- if (opts.public !== void 0) body.public = opts.public;
1993
- if (opts.fileSizeLimitBytes !== void 0)
1994
- body.file_size_limit_bytes = opts.fileSizeLimitBytes;
1995
- if (opts.allowedMimeTypes !== void 0)
1996
- body.allowed_mime_types = opts.allowedMimeTypes;
1997
- const row = await this.http.patch(
1998
- `/api/storage/buckets/${encodeURIComponent(name)}`,
1999
- body
2000
- );
2001
- return { data: bucketFromWire(row), error: null };
2002
- } catch (err) {
2003
- return wrapError2(err, "updateBucket failed");
2004
- }
2005
- }
2006
- async deleteBucket(name) {
2007
- try {
2008
- await this.http.delete(
2009
- `/api/storage/buckets/${encodeURIComponent(name)}`
2010
- );
2011
- return { data: null, error: null };
2012
- } catch (err) {
2013
- return wrapError2(err, "deleteBucket failed");
2014
- }
2015
- }
2016
- async emptyBucket(name) {
2017
- try {
2018
- const result = await this.http.post(
2019
- `/api/storage/buckets/${encodeURIComponent(name)}/empty`,
2020
- {}
2021
- );
2022
- return { data: result, error: null };
2023
- } catch (err) {
2024
- return wrapError2(err, "emptyBucket failed");
2025
- }
2026
- }
2027
- // --- Storage config (service_role) ---
2028
- async getConfig() {
2029
- try {
2030
- const row = await this.http.get("/api/storage/config");
2031
- return { data: configFromWire(row), error: null };
2032
- } catch (err) {
2033
- return wrapError2(err, "getConfig failed");
2034
- }
2035
- }
2036
- };
2037
-
2038
- // src/client.ts
2039
- var MitwayBaasClient = class {
2040
- http;
2041
- tokenManager;
2042
- auth;
2043
- database;
2044
- realtime;
2045
- storage;
2046
- constructor(config = {}) {
2047
- const logger = new Logger(config.debug);
2048
- this.tokenManager = new TokenManager({
2049
- persistSession: config.persistSession,
2050
- storageKey: config.storageKey
2051
- });
2052
- this.http = new HttpClient(config, this.tokenManager, logger);
2053
- this.auth = new Auth(this.http, this.tokenManager);
2054
- this.database = new Database(this.http, this.tokenManager, config.anonKey);
2055
- this.realtime = new Realtime(
2056
- this.http.baseUrl,
2057
- this.tokenManager,
2058
- config.anonKey,
2059
- config.realtime
2060
- );
2061
- this.storage = new Storage(this.http);
2062
- }
2063
- /**
2064
- * Escape hatch for callers that need to make custom requests against the
2065
- * backend without going through `auth` or `database`.
2066
- */
2067
- getHttpClient() {
2068
- return this.http;
2069
- }
2070
- };
2071
-
2072
- // src/index.ts
2073
- function createClient(config) {
2074
- return new MitwayBaasClient(config);
2075
- }
2076
- var index_default = MitwayBaasClient;
2077
- // Annotate the CommonJS export names for ESM import in node:
2078
- 0 && (module.exports = {
2079
- Auth,
2080
- Database,
2081
- HttpClient,
2082
- Logger,
2083
- MitwayBaasClient,
2084
- MitwayBaasError,
2085
- Realtime,
2086
- RealtimeChannel,
2087
- Storage,
2088
- StorageBucketClient,
2089
- TokenManager,
2090
- createClient
2091
- });
2092
- //# sourceMappingURL=index.cjs.map
1
+ 'use strict';Object.defineProperty(exports,'__esModule',{value:true});var postgrestJs=require('@supabase/postgrest-js'),socket_ioClient=require('socket.io-client');var c=class n extends Error{statusCode;error;nextActions;constructor(e,t,r,s){super(e),this.name="MitwayBaasError",this.statusCode=t,this.error=r,this.nextActions=s;}static fromApiError(e){return new n(e.message,e.statusCode,e.error,e.nextActions)}};var X=["authorization","x-api-key","cookie","set-cookie"],Z=["password","token","accesstoken","refreshtoken","authorization","secret","apikey","api_key","email","ssn","creditcard","credit_card"];function ee(n){let e={};for(let[t,r]of Object.entries(n))X.includes(t.toLowerCase())?e[t]="***REDACTED***":e[t]=r;return e}function C(n){if(n==null)return n;if(typeof n=="string")try{let e=JSON.parse(n);return C(e)}catch{return n}if(Array.isArray(n))return n.map(C);if(typeof n=="object"){let e={};for(let[t,r]of Object.entries(n))Z.includes(t.toLowerCase().replace(/[-_]/g,""))?e[t]="***REDACTED***":e[t]=C(r);return e}return n}function G(n){if(n==null)return "";if(typeof n=="string")try{return JSON.stringify(JSON.parse(n),null,2)}catch{return n}if(typeof FormData<"u"&&n instanceof FormData)return "[FormData]";try{return JSON.stringify(n,null,2)}catch{return "[Unserializable body]"}}var R=class{enabled;customLog;constructor(e){typeof e=="function"?(this.enabled=true,this.customLog=e):(this.enabled=!!e,this.customLog=null);}log(e,...t){if(!this.enabled)return;let r=`[MITWAY-BaaS Debug] ${e}`;this.customLog?this.customLog(r,...t):console.log(r,...t);}warn(e,...t){if(!this.enabled)return;let r=`[MITWAY-BaaS Debug] ${e}`;this.customLog?this.customLog(r,...t):console.warn(r,...t);}error(e,...t){if(!this.enabled)return;let r=`[MITWAY-BaaS Debug] ${e}`;this.customLog?this.customLog(r,...t):console.error(r,...t);}logRequest(e,t,r,s){if(!this.enabled)return;let i=[`\u2192 ${e} ${t}`];r&&Object.keys(r).length>0&&i.push(` Headers: ${JSON.stringify(ee(r))}`);let a=G(C(s));if(a){let o=a.length>1e3?a.slice(0,1e3)+"... [truncated]":a;i.push(` Body: ${o}`);}this.log(i.join(`
2
+ `));}logResponse(e,t,r,s,i){if(!this.enabled)return;let a=[`\u2190 ${e} ${t} ${r} (${s}ms)`],o=G(C(i));if(o){let g=o.length>1e3?o.slice(0,1e3)+"... [truncated]":o;a.push(` Body: ${g}`);}r>=400?this.error(a.join(`
3
+ `)):this.log(a.join(`
4
+ `));}};var K="mitway_baas_csrf_token",te="mitway_baas_session";function V(){return typeof localStorage<"u"?{getItem:e=>{try{return localStorage.getItem(e)}catch{return null}},setItem:(e,t)=>{try{localStorage.setItem(e,t);}catch{}},removeItem:e=>{try{localStorage.removeItem(e);}catch{}}}:{getItem:()=>null,setItem:()=>{},removeItem:()=>{}}}var re={getItem:()=>null,setItem:()=>{},removeItem:()=>{}};function Y(){if(typeof document>"u")return null;let n=document.cookie.split(";").find(e=>e.trim().startsWith(`${K}=`));return n&&n.split("=")[1]||null}function F(n){if(typeof document>"u")return;let e=10080*60,t=typeof window<"u"&&window.location.protocol==="https:"?"; Secure":"";document.cookie=`${K}=${encodeURIComponent(n)}; path=/; max-age=${e}; SameSite=Lax${t}`;}function D(){if(typeof document>"u")return;let n=typeof window<"u"&&window.location.protocol==="https:"?"; Secure":"";document.cookie=`${K}=; path=/; max-age=0; SameSite=Lax${n}`;}var S=class{accessToken=null;refreshToken=null;user=null;persistSession;storageKey;storage;onTokenChange=null;constructor(e){this.persistSession=e?.persistSession??true,this.storageKey=e?.storageKey??te,this.storage=this.persistSession?e?.storage??V():re,(e?.multiTab??true)&&this.persistSession&&typeof window<"u"&&typeof localStorage<"u"&&window.addEventListener("storage",this.handleStorageEvent);}handleStorageEvent=e=>{e.key===this.storageKey&&(typeof localStorage<"u"&&e.storageArea!==localStorage||this.syncFromStorage(e.newValue));};syncFromStorage(e){let t=this.accessToken;if(e===null)this.accessToken=null,this.refreshToken=null,this.user=null;else try{let r=JSON.parse(e);if(!r.accessToken||!r.user)return;this.accessToken=r.accessToken,this.refreshToken=r.refreshToken??null,this.user=r.user;}catch{return}t!==this.accessToken&&this.onTokenChange&&this.onTokenChange();}saveSession(e){let t=e.accessToken!==this.accessToken;this.accessToken=e.accessToken,this.user=e.user,e.refreshToken!==void 0&&(this.refreshToken=e.refreshToken??null),this.persist(),t&&this.onTokenChange&&this.onTokenChange();}getSession(){return !this.accessToken||!this.user?null:{accessToken:this.accessToken,refreshToken:this.refreshToken??void 0,user:this.user}}getAccessToken(){return this.accessToken}setAccessToken(e){let t=e!==this.accessToken;this.accessToken=e,this.persist(),t&&this.onTokenChange&&this.onTokenChange();}getRefreshToken(){return this.refreshToken}setRefreshToken(e){this.refreshToken=e,this.persist();}getUser(){return this.user}setUser(e){this.user=e,this.persist();}clearSession(){let e=this.accessToken!==null;this.accessToken=null,this.refreshToken=null,this.user=null,this.removePersisted(),e&&this.onTokenChange&&this.onTokenChange();}restoreSession(){if(!this.persistSession)return false;try{let e=this.storage.getItem(this.storageKey);if(!e)return !1;let t=JSON.parse(e);return !t.accessToken||!t.user?!1:(this.accessToken=t.accessToken,this.refreshToken=t.refreshToken??null,this.user=t.user,!0)}catch{return false}}persist(){if(!this.persistSession||!this.accessToken||!this.user)return;let e={accessToken:this.accessToken,user:this.user};this.refreshToken&&(e.refreshToken=this.refreshToken),this.storage.setItem(this.storageKey,JSON.stringify(e));}removePersisted(){this.persistSession&&this.storage.removeItem(this.storageKey);}};function P(n){if(!n||typeof n!="object")return n;let e=n,t={...e},r=false;return "access_token"in e&&!("accessToken"in e)&&(t.accessToken=e.access_token,delete t.access_token,r=true),"csrf_token"in e&&!("csrfToken"in e)&&(t.csrfToken=e.csrf_token,delete t.csrf_token,r=true),"refresh_token"in e&&!("refreshToken"in e)&&(t.refreshToken=e.refresh_token,delete t.refresh_token,r=true),r?t:n}var se=new Set([500,502,503,504]),ne=new Set(["GET","HEAD","PUT","DELETE","OPTIONS"]),_=class{baseUrl;fetch;defaultHeaders;anonKey;userToken=null;logger;autoRefreshToken=true;isRefreshing=false;refreshPromise=null;tokenManager;refreshToken=null;timeout;retryCount;retryDelay;constructor(e,t,r){if(this.baseUrl=e.baseUrl||"http://localhost:7130",this.autoRefreshToken=e.autoRefreshToken??true,this.fetch=e.fetch||(globalThis.fetch?globalThis.fetch.bind(globalThis):void 0),this.anonKey=e.anonKey,this.defaultHeaders={...e.headers},this.tokenManager=t??new S,this.logger=r||new R(false),this.timeout=e.timeout??3e4,this.retryCount=e.retryCount??3,this.retryDelay=e.retryDelay??500,!this.fetch)throw new Error("Fetch is not available. Provide a fetch implementation in the SDK config.")}buildUrl(e,t){let r=new URL(e,this.baseUrl);return t&&Object.entries(t).forEach(([s,i])=>{if(s==="select"){let a=i.replace(/\s+/g," ").trim();a=a.replace(/\s*\(\s*/g,"(").replace(/\s*\)\s*/g,")").replace(/\(\s+/g,"(").replace(/\s+\)/g,")").replace(/,\s+(?=[^()]*\))/g,","),r.searchParams.append(s,a);}else r.searchParams.append(s,i);}),r.toString()}isRetryableStatus(e){return se.has(e)}computeRetryDelay(e){let r=this.retryDelay*Math.pow(2,e-1)*(.85+Math.random()*.3);return Math.round(r)}async handleRequest(e,t,r={}){let{params:s,headers:i={},body:a,signal:o,...g}=r,p=this.buildUrl(t,s),w=Date.now(),T=ne.has(e.toUpperCase())||r.idempotent===true?this.retryCount:0,b={...this.defaultHeaders},J=this.userToken||this.anonKey;J&&(b.Authorization=`Bearer ${J}`);let M;a!==void 0&&(typeof FormData<"u"&&a instanceof FormData?M=a:(e!=="GET"&&(b["Content-Type"]="application/json;charset=UTF-8"),M=JSON.stringify(a))),i instanceof Headers?i.forEach((f,h)=>{b[h]=f;}):Array.isArray(i)?i.forEach(([f,h])=>{b[f]=h;}):Object.assign(b,i),this.logger.logRequest(e,p,b,M);let H;for(let f=0;f<=T;f++){if(f>0){let l=this.computeRetryDelay(f);if(this.logger.warn(`Retry ${f}/${T} for ${e} ${p} in ${l}ms`),o?.aborted)throw o.reason;await new Promise((u,q)=>{let y=()=>{clearTimeout(Q),q(o.reason);},Q=setTimeout(()=>{o&&o.removeEventListener("abort",y),u();},l);o&&o.addEventListener("abort",y,{once:true});});}let h,m;if((this.timeout>0||o)&&(h=new AbortController,this.timeout>0&&(m=setTimeout(()=>h.abort(),this.timeout)),o))if(o.aborted)h.abort(o.reason);else {let l=()=>h.abort(o.reason);o.addEventListener("abort",l,{once:true}),h.signal.addEventListener("abort",()=>{o.removeEventListener("abort",l);},{once:true});}try{let l=await this.fetch(p,{method:e,headers:b,body:M,...g,...h?{signal:h.signal}:{}});if(this.isRetryableStatus(l.status)&&f<T){m!==void 0&&clearTimeout(m),await l.body?.cancel(),H=new c(`Server error: ${l.status} ${l.statusText}`,l.status,"SERVER_ERROR");continue}if(l.status===204){m!==void 0&&clearTimeout(m);return}let u,q=l.headers.get("content-type");try{q?.includes("json")?u=await l.json():u=await l.text();}catch(y){throw m!==void 0&&clearTimeout(m),new c(`Failed to parse response body: ${y?.message||"Unknown error"}`,l.status,l.ok?"PARSE_ERROR":"REQUEST_FAILED")}if(m!==void 0&&clearTimeout(m),!l.ok){if(this.logger.logResponse(e,p,l.status,Date.now()-w,u),u&&typeof u=="object"&&"error"in u&&u.error!==null&&typeof u.error=="object"){let y=u.error;throw new c(y.message||l.statusText||"Request failed",y.statusCode||l.status,y.code||y.error||"REQUEST_FAILED",y.nextActions)}throw new c(`Request failed: ${l.statusText}`,l.status,"REQUEST_FAILED")}return this.logger.logResponse(e,p,l.status,Date.now()-w,u),u&&typeof u=="object"&&"data"in u&&"error"in u&&u.error===null?u.data:u}catch(l){if(m!==void 0&&clearTimeout(m),l?.name==="AbortError")throw h&&h.signal.aborted&&this.timeout>0&&!o?.aborted?new c(`Request timed out after ${this.timeout}ms`,408,"REQUEST_TIMEOUT"):l;if(l instanceof c)throw l;if(f<T){H=l;continue}throw new c(`Network request failed: ${l?.message||"Unknown error"}`,0,"NETWORK_ERROR")}}throw H||new c("Request failed after all retry attempts",0,"NETWORK_ERROR")}async request(e,t,r={}){try{return await this.handleRequest(e,t,{...r})}catch(s){if(s instanceof c&&s.statusCode===401&&s.error==="INVALID_TOKEN"&&this.autoRefreshToken)try{let i=await this.handleTokenRefresh();return this.setAuthToken(i.accessToken),this.tokenManager.saveSession(i),i.csrfToken&&F(i.csrfToken),i.refreshToken&&this.setRefreshToken(i.refreshToken),await this.handleRequest(e,t,{...r})}catch(i){throw this.tokenManager.clearSession(),this.userToken=null,this.refreshToken=null,D(),i}throw s}}async rawFetch(e,t={}){let r=this.buildUrl(e),s=new Headers(t.headers??{});for(let[i,a]of Object.entries(this.defaultHeaders))s.has(i)||s.set(i,a);if(!s.has("Authorization")){let i=this.userToken??this.anonKey;i&&s.set("Authorization",`Bearer ${i}`);}return this.fetch(r,{...t,headers:s})}get(e,t){return this.request("GET",e,t)}post(e,t,r){return this.request("POST",e,{...r,body:t})}put(e,t,r){return this.request("PUT",e,{...r,body:t})}patch(e,t,r){return this.request("PATCH",e,{...r,body:t})}delete(e,t){return this.request("DELETE",e,t)}setAuthToken(e){this.userToken=e;}setRefreshToken(e){this.refreshToken=e;}getHeaders(){let e={...this.defaultHeaders},t=this.userToken||this.anonKey;return t&&(e.Authorization=`Bearer ${t}`),e}async handleTokenRefresh(){return this.isRefreshing?this.refreshPromise:(this.isRefreshing=true,this.refreshPromise=(async()=>{try{let e=Y(),t=this.refreshToken?{refreshToken:this.refreshToken}:void 0,r=await this.handleRequest("POST","/api/auth/refresh",{body:t,headers:e?{"X-CSRF-Token":e}:{},credentials:"include"});return P(r)}finally{this.isRefreshing=false,this.refreshPromise=null;}})(),this.refreshPromise)}};function E(n,e){return n instanceof c?{data:null,error:n}:{data:null,error:new c(n instanceof Error?n.message:e,500,"AUTH_ERROR")}}var A=class{constructor(e,t){this.http=e;this.tokenManager=t;this.tokenManager.onTokenChange=()=>this._emitFromTokenChange();}http;tokenManager;stateChangeListeners=new Set;lastEmittedUserId=null;lastEmittedAccessToken=null;onAuthStateChange(e){return this.stateChangeListeners.add(e),{unsubscribe:()=>{this.stateChangeListeners.delete(e);}}}emit(e,t){for(let r of this.stateChangeListeners)try{r(e,t);}catch{}}_emitFromTokenChange(){let e=this.tokenManager.getSession(),t=this.lastEmittedUserId,r=this.lastEmittedAccessToken;if(!e){t!==null&&(this.lastEmittedUserId=null,this.lastEmittedAccessToken=null,this.emit("SIGNED_OUT",null));return}let s=e.user.id;if(t===null){this.lastEmittedUserId=s,this.lastEmittedAccessToken=e.accessToken,this.emit("SIGNED_IN",e);return}if(t!==s){this.lastEmittedUserId=null,this.lastEmittedAccessToken=null,this.emit("SIGNED_OUT",null),this.lastEmittedUserId=s,this.lastEmittedAccessToken=e.accessToken,this.emit("SIGNED_IN",e);return}r!==e.accessToken&&(this.lastEmittedAccessToken=e.accessToken,this.emit("TOKEN_REFRESHED",e));}saveSessionFromResponse(e){let t={accessToken:e.accessToken,refreshToken:e.refreshToken,user:e.user};e.csrfToken&&F(e.csrfToken),this.tokenManager.saveSession(t),this.http.setAuthToken(e.accessToken),this.http.setRefreshToken(e.refreshToken??null);}async signUp(e){try{let t=await this.http.post("/api/auth/register",e,{credentials:"include"}),r=P(t);return r?.accessToken&&r.user&&this.saveSessionFromResponse(r),{data:r,error:null}}catch(t){return E(t,"Sign up failed")}}async signInWithPassword(e){try{let t=await this.http.post("/api/auth/login",e,{credentials:"include"}),r=P(t);return r?.accessToken&&r.user&&this.saveSessionFromResponse(r),{data:r,error:null}}catch(t){return E(t,"Sign in failed")}}async signOut(){try{try{await this.http.post("/api/auth/logout",void 0,{credentials:"include"});}catch{}return this.tokenManager.clearSession(),this.http.setAuthToken(null),this.http.setRefreshToken(null),D(),{error:null}}catch{return {error:new c("Failed to sign out",500,"SIGNOUT_ERROR")}}}async refreshSession(){try{let e=await this.http.handleTokenRefresh();return e?.accessToken&&e.user&&this.saveSessionFromResponse(e),{data:e,error:null}}catch(e){return E(e,"Session refresh failed")}}async initialize(){if(!this.tokenManager.restoreSession())return this.emit("INITIAL_SESSION",null),{data:null,error:new c("No persisted session",0,"NO_SESSION")};let t=this.tokenManager.getSession();if(!t)return this.emit("INITIAL_SESSION",null),{data:null,error:new c("No persisted session",0,"NO_SESSION")};this.http.setAuthToken(t.accessToken);let r=this.tokenManager.getRefreshToken();r&&this.http.setRefreshToken(r),this.lastEmittedUserId=t.user.id,this.lastEmittedAccessToken=t.accessToken;try{let s=await this.http.get("/api/auth/sessions/current");if(s?.user){this.tokenManager.setUser(s.user);let i=this.tokenManager.getSession();return this.emit("INITIAL_SESSION",i),{data:{user:s.user,accessToken:t.accessToken},error:null}}return this.tokenManager.clearSession(),this.http.setAuthToken(null),this.http.setRefreshToken(null),this.emit("INITIAL_SESSION",null),{data:null,error:new c("Invalid session",401,"INVALID_SESSION")}}catch(s){return this.tokenManager.clearSession(),this.http.setAuthToken(null),this.http.setRefreshToken(null),this.emit("INITIAL_SESSION",null),E(s,"Session restore failed")}}getSession(){return this.tokenManager.getSession()}getUser(){return this.tokenManager.getUser()}async getCurrentUser(){try{let e=await this.http.get("/api/auth/sessions/current");if(e?.user){let t={accessToken:this.tokenManager.getSession()?.accessToken??"",user:e.user};this.tokenManager.saveSession(t);}return {data:e,error:null}}catch(e){return E(e,"Failed to get current user")}}async getProfile(e){try{return {data:await this.http.get(`/api/auth/profiles/${encodeURIComponent(e)}`),error:null}}catch(t){return E(t,"Failed to get profile")}}async setProfile(e){try{let t=await this.http.patch("/api/auth/profiles/current",{profile:e}),r=this.tokenManager.getUser();if(t?.profile&&r){let s={...r,profile:t.profile};this.tokenManager.setUser(s),this.emit("USER_UPDATED",this.tokenManager.getSession());}return {data:t,error:null}}catch(t){return E(t,"Failed to update profile")}}};function oe(n,e,t){return async(r,s)=>{let i=typeof r=="string"?r:r.toString(),a=new URL(i),o=a.pathname.startsWith("/")?a.pathname.slice(1):a.pathname,g=o.match(/^rpc\/(.+)$/),p=g?`/api/database/rpc/${g[1]}`:`/api/database/records/${o}`,w=`${n.baseUrl}${p}${a.search}`,x=new Headers(s?.headers);if(!x.has("Authorization")){let T=e.getAccessToken()??t;T&&x.set("Authorization",`Bearer ${T}`);}return fetch(w,{...s,headers:x})}}var O=class{postgrest;httpClient;constructor(e,t,r){this.httpClient=e,this.postgrest=new postgrestJs.PostgrestClient("http://dummy",{fetch:oe(e,t,r),headers:{}});}from(e){if(!e||typeof e!="string")throw new c("Database.from(table) requires a non-empty string",400,"INVALID_TABLE_NAME");return this.postgrest.from(e)}rpc(e,t,r){return this.postgrest.rpc(e,t,r)}getUrl(){return this.httpClient.baseUrl}};var ce=2e4;function z(n,e){let t=new Error(e);return t.code=n,t}var le=1e4,N=class{constructor(e,t,r={}){this.topic=e;this.realtime=t;this.options=r;}topic;realtime;bindings=[];state="closed";statusCallback=null;presence={};trackedState=null;presenceHeartbeat=null;lastBroadcastTimestamp=null;options;get isPrivate(){return this.options.config?.private===true}get presenceKey(){return this.options.config?.presence?.key}_state(){return this.state}async _rejoinAfterReconnect(){if(this.state!=="closed"){for(let e of this.bindings)e.type==="postgres_changes"&&(e.subscriptionId=void 0);this.state="joining";try{if(await this.registerAllBindings(),this.trackedState){let e=this.realtime._getSocket(),t=this.presenceKey;e?.emit("realtime:presence:track",t!==void 0?{channel:this.topic,state:this.trackedState,key:t}:{channel:this.topic,state:this.trackedState});}this.lastBroadcastTimestamp&&this.bindings.some(e=>e.type==="broadcast")&&this.replay({since:this.lastBroadcastTimestamp}).catch(()=>{}),this.state="joined",this.statusCallback?.("SUBSCRIBED");}catch(e){this.state="errored",this.statusCallback?.("CHANNEL_ERROR",z("REJOIN_FAILED",e instanceof Error?e.message:String(e)));}}}on(e,t,r){return e==="postgres_changes"?this.bindings.push({type:"postgres_changes",filter:t,callback:r}):e==="broadcast"?this.bindings.push({type:"broadcast",filter:t,callback:r}):this.bindings.push({type:"presence",filter:t,callback:r}),this}async track(e){let t=this.realtime._getSocket();if(!t)throw new c("Socket not connected",503,"NOT_CONNECTED");this.trackedState=e;let r=this.presenceKey;t.emit("realtime:presence:track",r!==void 0?{channel:this.topic,state:e,key:r}:{channel:this.topic,state:e}),this.presenceHeartbeat||(this.presenceHeartbeat=setInterval(()=>{let i=this.realtime._getSocket();i&&this.trackedState&&i.emit("realtime:presence:track",r!==void 0?{channel:this.topic,state:this.trackedState,key:r}:{channel:this.topic,state:this.trackedState});},ce),this.presenceHeartbeat.unref?.());}untrack(){this.trackedState=null,this.presenceHeartbeat&&(clearInterval(this.presenceHeartbeat),this.presenceHeartbeat=null),this.realtime._getSocket()?.emit("realtime:presence:untrack",{channel:this.topic});}presenceState(){return this.presence}subscribe(e){return this.statusCallback=e??null,this.state==="joining"||this.state==="joined"?this:(this.state="joining",this.realtime.connect().then(async()=>{try{await this.registerAllBindings(),this.state="joined",this.statusCallback?.("SUBSCRIBED");}catch(t){this.state="errored";let r=t instanceof Error?t.message:String(t);this.statusCallback?.("CHANNEL_ERROR",z("SUBSCRIBE_FAILED",r));}},t=>{this.state="errored",this.statusCallback?.("CHANNEL_ERROR",z("CONNECT_FAILED",t.message));}),this)}async unsubscribe(){if(this.state==="closed")return;let e=this.realtime._getSocket();if(this.presenceHeartbeat&&(clearInterval(this.presenceHeartbeat),this.presenceHeartbeat=null),!e){this.trackedState=null,this.state="closed";return}this.trackedState&&(e.emit("realtime:presence:untrack",{channel:this.topic}),this.trackedState=null);let t=this.bindings.filter(r=>r.type==="postgres_changes");for(let r of t)r.subscriptionId&&(e.emit("realtime:postgres_changes:unsubscribe",{subscription_id:r.subscriptionId}),r.subscriptionId=void 0);this.bindings.some(r=>r.type==="broadcast"||r.type==="presence")&&e.emit("realtime:unsubscribe",{channel:this.topic}),this.realtime._detachChannel(this),this.state="closed",this.statusCallback?.("CLOSED");}async send(e){if(e.type!=="broadcast")throw new c('Only "broadcast" sends are supported \u2014 DB changes flow via your DB writes, not channel.send()',400,"UNSUPPORTED_SEND_TYPE");if(new Set(["postgres_changes","presence_state","presence_join","presence_leave"]).has(e.event))throw new c(`"${e.event}" is a reserved event name \u2014 pick a different name for broadcast events`,400,"RESERVED_EVENT_NAME");let r=this.realtime._getSocket();if(!r)throw new c("Socket not connected",503,"NOT_CONNECTED");let s=this.options.config?.broadcast?.self,i={channel:this.topic,event:e.event,payload:e.payload};return s===false&&(i.self=false),await new Promise(a=>{r.emit("realtime:publish",i,o=>{a(o);});})}async replay(e){let t=this.realtime._getSocket();if(!t)throw new c("Socket not connected",503,"NOT_CONNECTED");t.emit("realtime:broadcast:replay",{channel:this.topic,since:e.since,limit:e.limit,private:this.isPrivate});}_dispatch(e,t){if(e==="postgres_changes"){let s=t;for(let i of this.bindings)if(!(i.type!=="postgres_changes"||!i.subscriptionId||!s.ids.includes(i.subscriptionId)||!(i.filter.event==="*"||i.filter.event===s.data.eventType)))try{i.callback(s.data);}catch{}return}if(e==="presence_state"||e==="presence_join"||e==="presence_leave"){let s=t;if(s.channel!==this.topic)return;if(e==="presence_state"&&s.state)this.presence={...s.state},this.firePresence({event:"sync",state:this.presence});else if(e==="presence_join"&&s.joins)Object.assign(this.presence,s.joins),this.firePresence({event:"join",joins:s.joins});else if(e==="presence_leave"&&s.leaves){for(let i of Object.keys(s.leaves))delete this.presence[i];this.firePresence({event:"leave",leaves:s.leaves});}return}let r=t.meta;r?.timestamp&&(!this.lastBroadcastTimestamp||r.timestamp>this.lastBroadcastTimestamp)&&(this.lastBroadcastTimestamp=r.timestamp);for(let s of this.bindings){if(s.type!=="broadcast"||s.filter.event!==e)continue;let{meta:i,...a}=t;try{s.callback({type:"broadcast",event:e,payload:a});}catch{}}}firePresence(e){for(let t of this.bindings)if(t.type==="presence"&&t.filter.event===e.event)try{e.event==="sync"?t.callback():(e.event,t.callback(e));}catch{}}async registerAllBindings(){let e=this.realtime._getSocket();if(!e)throw new Error("Socket not available");this.bindings.some(s=>s.type==="broadcast"||s.type==="presence")&&await new Promise((s,i)=>{e.emit("realtime:subscribe",{channel:this.topic,private:this.isPrivate},a=>{a.status==="ok"?s():i(new Error(a.error?.message??"subscribe failed"));});});let r=this.bindings.filter(s=>s.type==="postgres_changes");await Promise.all(r.map(s=>new Promise((i,a)=>{e.emit("realtime:postgres_changes:subscribe",{event:s.filter.event,schema:s.filter.schema??"public",table:s.filter.table,filter:s.filter.filter},o=>{o.status==="ok"&&o.subscription_id?(s.subscriptionId=o.subscription_id,i()):a(new Error(o.error?.message??"postgres_changes subscribe failed"));});})));}},B=class{socket=null;baseUrl;options;anonKey;tokenManager;channels=new Map;connecting=null;firstConnected=false;constructor(e,t,r,s={}){this.baseUrl=e,this.tokenManager=t,this.anonKey=r,this.options=s;}get isConnected(){return this.socket?.connected===true}get socketId(){return this.socket?.id}channel(e,t){let r=this.channels.get(e);if(r)return r;let s=new N(e,this,t);return this.channels.set(e,s),s}connect(){return this.isConnected?Promise.resolve():this.connecting?this.connecting:(this.connecting=this.openSocket(),this.connecting)}disconnect(){this.socket&&(this.socket.disconnect(),this.socket=null,this.firstConnected=false);}_getSocket(){return this.socket}_detachChannel(e){this.channels.delete(e.topic);}openSocket(){let e=this.tokenManager.getAccessToken()??this.anonKey;if(!e){let s=new c("Realtime requires an access token or anonKey",401,"AUTH_INVALID_API_KEY");return this.connecting=null,Promise.reject(s)}let t=this.options.timeoutMs??le,r=socket_ioClient.io(this.baseUrl,{path:this.options.path,transports:this.options.transports??["websocket"],auth:{token:e,...this.options.extraAuth??{}},reconnection:true,timeout:t});return this.socket=r,r.onAny((s,...i)=>this.dispatch(s,i)),r.on("connect",()=>{if(this.firstConnected)for(let s of this.channels.values()){let i=s._state();(i==="joined"||i==="errored")&&s._rejoinAfterReconnect();}}),new Promise((s,i)=>{let a=setTimeout(()=>{r.off("connect",g),r.off("connect_error",p),this.connecting=null,i(new c(`Realtime connection timeout after ${t}ms`,408,"CONNECTION_TIMEOUT"));},t),o=()=>{clearTimeout(a),r.off("connect",g),r.off("connect_error",p);},g=()=>{o(),this.connecting=null,this.firstConnected=true,s();},p=w=>{o(),this.connecting=null,i(new c(w.message,0,"CONNECTION_FAILED"));};r.once("connect",g),r.once("connect_error",p);})}dispatch(e,t){if(e==="postgres_changes"){let s=t[0]??{};this.channels.forEach(i=>i._dispatch("postgres_changes",s));return}if(e==="connect"||e==="disconnect"||e==="connect_error"||e==="error"||e==="realtime:error"||e==="realtime:shutdown")return;let r=t[0]??{};this.channels.forEach(s=>s._dispatch(e,r));}};function L(n){return {id:n.id,name:n.name,public:n.public,fileSizeLimitBytes:n.file_size_limit_bytes,allowedMimeTypes:n.allowed_mime_types,createdAt:n.created_at,updatedAt:n.updated_at}}function j(n,e){return {id:n.id,bucket:e,key:n.key,size:n.size,mimeType:n.mime_type,etag:n.etag,cacheControl:n.cache_control,contentDisposition:n.content_disposition,uploadedBy:n.uploaded_by,uploadedAt:n.uploaded_at,updatedAt:n.updated_at}}function ue(n){return {defaultFileSizeLimitBytes:n.default_file_size_limit_bytes,maxFileSizeLimitBytes:n.max_file_size_limit_bytes,tenantStorageQuotaBytes:n.tenant_storage_quota_bytes,reservedSpaceBytes:n.reserved_space_bytes,signedUrlDefaultTtlSec:n.signed_url_default_ttl_sec,signedUrlMaxTtlSec:n.signed_url_max_ttl_sec}}function de(n){return n.split("/").map(encodeURIComponent).join("/")}function d(n,e){return n instanceof c?{data:null,error:n}:{data:null,error:new c(n instanceof Error?n.message:e,0,"STORAGE_ERROR")}}async function W(n){let e="STORAGE_ERROR",t=`HTTP ${n.status}`;try{let r=await n.json();r&&r.error&&(e=r.error.code??e,t=r.error.message??t);}catch{}return new c(t,n.status,e)}var $=class{constructor(e,t){this.http=e;this.bucketName=t;}http;bucketName;bucketBase(){return `/api/storage/buckets/${encodeURIComponent(this.bucketName)}`}objectPath(e){return `${this.bucketBase()}/objects/${de(e)}`}async upload(e,t,r={}){try{let s=r.upsert?"PUT":"POST",i={"Content-Type":r.contentType??"application/octet-stream"};r.cacheControl&&(i["Cache-Control"]=r.cacheControl),r.contentDisposition&&(i["Content-Disposition"]=r.contentDisposition);let a=await this.http.rawFetch(this.objectPath(e),{method:s,headers:i,body:t,signal:r.abortSignal});if(!a.ok)return {data:null,error:await W(a)};let o=await a.json();return o.error||!o.data?{data:null,error:new c(o.error?.message??"Upload failed",a.status,o.error?.code??"STORAGE_ERROR")}:{data:j(o.data,this.bucketName),error:null}}catch(s){return d(s,"Upload failed")}}async download(e,t={}){try{let r={};t.range&&(r.Range=`bytes=${t.range.start}-${t.range.end}`);let s=await this.http.rawFetch(this.objectPath(e),{method:"GET",headers:r,signal:t.abortSignal});return s.ok?{data:await s.blob(),error:null}:{data:null,error:await W(s)}}catch(r){return d(r,"Download failed")}}async getStream(e,t={}){try{let r={};t.range&&(r.Range=`bytes=${t.range.start}-${t.range.end}`);let s=await this.http.rawFetch(this.objectPath(e),{method:"GET",headers:r,signal:t.abortSignal});return s.ok?s.body?{data:s.body,error:null}:{data:null,error:new c("Response body is not a stream",s.status,"STORAGE_ERROR")}:{data:null,error:await W(s)}}catch(r){return d(r,"Download failed")}}async remove(e){try{let t=await Promise.allSettled(e.map(i=>this.http.rawFetch(this.objectPath(i),{method:"DELETE"}))),r=[],s=[];for(let i=0;i<e.length;i++){let a=e[i],o=t[i];if(o.status==="fulfilled"&&o.value.ok)r.push(a);else if(o.status==="fulfilled")s.push(`${a}: HTTP ${o.value.status}`);else {let g=o.reason instanceof Error?o.reason.message:String(o.reason);s.push(`${a}: ${g}`);}}return s.length>0?{data:null,error:new c(`Failed to delete some objects: ${s.join("; ")}`,0,"STORAGE_ERROR")}:{data:{removed:r},error:null}}catch(t){return d(t,"Delete failed")}}async list(e={}){try{let t={};return e.prefix!==void 0&&(t.prefix=e.prefix),e.limit!==void 0&&(t.limit=String(e.limit)),e.startAfter!==void 0&&(t.start_after=e.startAfter),{data:(await this.http.get(`${this.bucketBase()}/objects`,{params:t})).map(s=>j(s,this.bucketName)),error:null}}catch(t){return d(t,"List failed")}}async copy(e,t,r){try{let s=await this.http.post(`${this.objectPath(e)}/copy`,{dest_bucket:r??this.bucketName,dest_key:t});return {data:j(s,r??this.bucketName),error:null}}catch(s){return d(s,"Copy failed")}}async move(e,t,r){try{let s=await this.http.post(`${this.objectPath(e)}/move`,{dest_bucket:r??this.bucketName,dest_key:t});return {data:j(s,r??this.bucketName),error:null}}catch(s){return d(s,"Move failed")}}async createSignedUrl(e,t={}){try{let r={};t.expiresIn!==void 0&&(r.expires_in=t.expiresIn);let s=await this.http.post(`${this.objectPath(e)}/sign`,r);return {data:{url:s.url,token:s.token,expiresAt:s.expiresAt},error:null}}catch(r){return d(r,"Sign failed")}}getPublicUrl(e){return {data:{url:`${this.http.baseUrl.replace(/\/$/,"")}${this.objectPath(e)}`}}}},I=class{constructor(e){this.http=e;}http;from(e){return new $(this.http,e)}async listBuckets(){try{return {data:(await this.http.get("/api/storage/buckets")).map(L),error:null}}catch(e){return d(e,"listBuckets failed")}}async getBucket(e){try{let t=await this.http.get(`/api/storage/buckets/${encodeURIComponent(e)}`);return {data:L(t),error:null}}catch(t){return d(t,"getBucket failed")}}async createBucket(e,t={}){try{let r={name:e};t.public!==void 0&&(r.public=t.public),t.fileSizeLimitBytes!==void 0&&(r.file_size_limit_bytes=t.fileSizeLimitBytes),t.allowedMimeTypes!==void 0&&(r.allowed_mime_types=t.allowedMimeTypes);let s=await this.http.post("/api/storage/buckets",r);return {data:L(s),error:null}}catch(r){return d(r,"createBucket failed")}}async updateBucket(e,t){try{let r={};t.public!==void 0&&(r.public=t.public),t.fileSizeLimitBytes!==void 0&&(r.file_size_limit_bytes=t.fileSizeLimitBytes),t.allowedMimeTypes!==void 0&&(r.allowed_mime_types=t.allowedMimeTypes);let s=await this.http.patch(`/api/storage/buckets/${encodeURIComponent(e)}`,r);return {data:L(s),error:null}}catch(r){return d(r,"updateBucket failed")}}async deleteBucket(e){try{return await this.http.delete(`/api/storage/buckets/${encodeURIComponent(e)}`),{data:null,error:null}}catch(t){return d(t,"deleteBucket failed")}}async emptyBucket(e){try{return {data:await this.http.post(`/api/storage/buckets/${encodeURIComponent(e)}/empty`,{}),error:null}}catch(t){return d(t,"emptyBucket failed")}}async getConfig(){try{let e=await this.http.get("/api/storage/config");return {data:ue(e),error:null}}catch(e){return d(e,"getConfig failed")}}};function he(n){return {key:n.key,digest:n.digest,updatedAt:n.updated_at}}function k(n,e){return n instanceof c?{data:null,error:n}:{data:null,error:new c(n instanceof Error?n.message:e,0,"FUNCTIONS_ERROR")}}function ge(n){return typeof n=="object"&&n!==null&&!ArrayBuffer.isView(n)&&!(n instanceof Blob)&&!(n instanceof FormData)&&!(n instanceof URLSearchParams)&&!(n instanceof ReadableStream)}var U=class{constructor(e){this.http=e;}http;async list(){try{return {data:await this.http.get("/api/functions"),error:null}}catch(e){return k(e,"list failed")}}async get(e){try{return {data:await this.http.get(`/api/functions/${encodeURIComponent(e)}`),error:null}}catch(t){return k(t,"get failed")}}async create(e){try{return {data:await this.http.post("/api/functions",e),error:null}}catch(t){return k(t,"create failed")}}async update(e,t){try{return {data:await this.http.put(`/api/functions/${encodeURIComponent(e)}`,t),error:null}}catch(r){return k(r,"update failed")}}async remove(e){try{return await this.http.delete(`/api/functions/${encodeURIComponent(e)}`),{data:{deleted:!0},error:null}}catch(t){return k(t,"remove failed")}}async invoke(e,t={}){try{let r=t.method??"POST",s={...t.headers},i;return t.body!==void 0&&t.body!==null&&(ge(t.body)?(i=JSON.stringify(t.body),s["Content-Type"]||(s["Content-Type"]="application/json")):i=t.body),{data:await this.http.rawFetch(`/api/invoke/${encodeURIComponent(e)}`,{method:r,headers:s,body:i}),error:null}}catch(r){return k(r,"invoke failed")}}async listSecrets(){try{return {data:(await this.http.get("/api/functions/secrets")).secrets.map(he),error:null}}catch(e){return k(e,"listSecrets failed")}}async setSecrets(e){try{return await this.http.put("/api/functions/secrets",{secrets:e}),{data:{saved:!0},error:null}}catch(t){return k(t,"setSecrets failed")}}async deleteSecret(e){try{return await this.http.delete(`/api/functions/secrets/${encodeURIComponent(e)}`),{data:{deleted:!0},error:null}}catch(t){return k(t,"deleteSecret failed")}}};var v=class{http;tokenManager;auth;database;realtime;storage;functions;constructor(e={}){let t=new R(e.debug);this.tokenManager=new S({persistSession:e.persistSession,storageKey:e.storageKey,storage:e.storage,multiTab:e.multiTab}),this.http=new _(e,this.tokenManager,t),this.auth=new A(this.http,this.tokenManager),this.database=new O(this.http,this.tokenManager,e.anonKey),this.realtime=new B(this.http.baseUrl,this.tokenManager,e.anonKey,e.realtime),this.storage=new I(this.http),this.functions=new U(this.http);}getHttpClient(){return this.http}};function Qe(n){return new v(n)}var Xe=v;
5
+ exports.Auth=A;exports.Database=O;exports.Functions=U;exports.HttpClient=_;exports.Logger=R;exports.MitwayBaasClient=v;exports.MitwayBaasError=c;exports.Realtime=B;exports.RealtimeChannel=N;exports.Storage=I;exports.StorageBucketClient=$;exports.TokenManager=S;exports.createClient=Qe;exports.createLocalStorageAdapter=V;exports.default=Xe;