@mitway/sdk 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -161,6 +161,7 @@ var Logger = class {
161
161
 
162
162
  // src/lib/token-manager.ts
163
163
  var CSRF_TOKEN_COOKIE = "mitway_baas_csrf_token";
164
+ var DEFAULT_STORAGE_KEY = "mitway_baas_session";
164
165
  function getCsrfToken() {
165
166
  if (typeof document === "undefined") return null;
166
167
  const match = document.cookie.split(";").find((c) => c.trim().startsWith(`${CSRF_TOKEN_COOKIE}=`));
@@ -180,13 +181,24 @@ function clearCsrfToken() {
180
181
  }
181
182
  var TokenManager = class {
182
183
  accessToken = null;
184
+ refreshToken = null;
183
185
  user = null;
186
+ persistSession;
187
+ storageKey;
184
188
  /** Fired when the access token changes (used by long-lived consumers). */
185
189
  onTokenChange = null;
190
+ constructor(opts) {
191
+ this.persistSession = opts?.persistSession ?? true;
192
+ this.storageKey = opts?.storageKey ?? DEFAULT_STORAGE_KEY;
193
+ }
186
194
  saveSession(session) {
187
195
  const tokenChanged = session.accessToken !== this.accessToken;
188
196
  this.accessToken = session.accessToken;
189
197
  this.user = session.user;
198
+ if (session.refreshToken !== void 0) {
199
+ this.refreshToken = session.refreshToken ?? null;
200
+ }
201
+ this.persist();
190
202
  if (tokenChanged && this.onTokenChange) {
191
203
  this.onTokenChange();
192
204
  }
@@ -195,6 +207,7 @@ var TokenManager = class {
195
207
  if (!this.accessToken || !this.user) return null;
196
208
  return {
197
209
  accessToken: this.accessToken,
210
+ refreshToken: this.refreshToken ?? void 0,
198
211
  user: this.user
199
212
  };
200
213
  }
@@ -204,24 +217,76 @@ var TokenManager = class {
204
217
  setAccessToken(token) {
205
218
  const tokenChanged = token !== this.accessToken;
206
219
  this.accessToken = token;
220
+ this.persist();
207
221
  if (tokenChanged && this.onTokenChange) {
208
222
  this.onTokenChange();
209
223
  }
210
224
  }
225
+ getRefreshToken() {
226
+ return this.refreshToken;
227
+ }
228
+ setRefreshToken(token) {
229
+ this.refreshToken = token;
230
+ this.persist();
231
+ }
211
232
  getUser() {
212
233
  return this.user;
213
234
  }
214
235
  setUser(user) {
215
236
  this.user = user;
237
+ this.persist();
216
238
  }
217
239
  clearSession() {
218
240
  const hadToken = this.accessToken !== null;
219
241
  this.accessToken = null;
242
+ this.refreshToken = null;
220
243
  this.user = null;
244
+ this.removePersisted();
221
245
  if (hadToken && this.onTokenChange) {
222
246
  this.onTokenChange();
223
247
  }
224
248
  }
249
+ /**
250
+ * Restore the session from localStorage. Returns true if a persisted
251
+ * session was found and loaded into memory.
252
+ */
253
+ restoreSession() {
254
+ if (!this.persistSession || typeof localStorage === "undefined") return false;
255
+ try {
256
+ const raw = localStorage.getItem(this.storageKey);
257
+ if (!raw) return false;
258
+ const stored = JSON.parse(raw);
259
+ if (!stored.accessToken || !stored.user) return false;
260
+ this.accessToken = stored.accessToken;
261
+ this.refreshToken = stored.refreshToken ?? null;
262
+ this.user = stored.user;
263
+ return true;
264
+ } catch {
265
+ return false;
266
+ }
267
+ }
268
+ persist() {
269
+ if (!this.persistSession || typeof localStorage === "undefined") return;
270
+ if (!this.accessToken || !this.user) return;
271
+ try {
272
+ const data = {
273
+ accessToken: this.accessToken,
274
+ user: this.user
275
+ };
276
+ if (this.refreshToken) {
277
+ data.refreshToken = this.refreshToken;
278
+ }
279
+ localStorage.setItem(this.storageKey, JSON.stringify(data));
280
+ } catch {
281
+ }
282
+ }
283
+ removePersisted() {
284
+ if (!this.persistSession || typeof localStorage === "undefined") return;
285
+ try {
286
+ localStorage.removeItem(this.storageKey);
287
+ } catch {
288
+ }
289
+ }
225
290
  };
226
291
 
227
292
  // src/lib/auth-envelope.ts
@@ -528,6 +593,29 @@ var HttpClient = class {
528
593
  throw error;
529
594
  }
530
595
  }
596
+ /**
597
+ * Low-level fetch helper for binary bodies (uploads) and streamed responses
598
+ * (downloads). Applies the current Bearer token (user session → anon key
599
+ * fallback) plus any configured default headers, resolves `path` against
600
+ * `baseUrl`, and returns the raw `Response` — it does NOT unwrap the
601
+ * `{ data, error }` envelope, so the caller is responsible for status
602
+ * checking and parsing.
603
+ *
604
+ * Used by the storage module for object upload/download paths where the
605
+ * body or response is not JSON.
606
+ */
607
+ async rawFetch(path, init = {}) {
608
+ const url = this.buildUrl(path);
609
+ const headers = new Headers(init.headers ?? {});
610
+ for (const [k, v] of Object.entries(this.defaultHeaders)) {
611
+ if (!headers.has(k)) headers.set(k, v);
612
+ }
613
+ if (!headers.has("Authorization")) {
614
+ const token = this.userToken ?? this.anonKey;
615
+ if (token) headers.set("Authorization", `Bearer ${token}`);
616
+ }
617
+ return this.fetch(url, { ...init, headers });
618
+ }
531
619
  get(path, options) {
532
620
  return this.request("GET", path, options);
533
621
  }
@@ -618,6 +706,7 @@ var Auth = class {
618
706
  saveSessionFromResponse(response) {
619
707
  const session = {
620
708
  accessToken: response.accessToken,
709
+ refreshToken: response.refreshToken,
621
710
  user: response.user
622
711
  };
623
712
  if (response.csrfToken) {
@@ -711,6 +800,70 @@ var Auth = class {
711
800
  return wrapError(error, "Session refresh failed");
712
801
  }
713
802
  }
803
+ /**
804
+ * Restore the session from localStorage and validate it with the backend.
805
+ * Call this once on app startup (e.g. in a React AuthProvider useEffect).
806
+ *
807
+ * Flow:
808
+ * 1. Read persisted session from localStorage.
809
+ * 2. Populate in-memory state (TokenManager + HttpClient).
810
+ * 3. Validate with `GET /api/auth/sessions/current`.
811
+ * - If the access token expired, the HttpClient auto-refresh kicks in
812
+ * using the persisted refresh token (sent in the POST body, not
813
+ * cookies — works cross-site).
814
+ * 4. Return the validated user or an error.
815
+ *
816
+ * If no persisted session exists, returns `{ data: null, error }` — the
817
+ * app should show the login page.
818
+ */
819
+ async initialize() {
820
+ const restored = this.tokenManager.restoreSession();
821
+ if (!restored) {
822
+ return {
823
+ data: null,
824
+ error: new MitwayBaasError("No persisted session", 0, "NO_SESSION")
825
+ };
826
+ }
827
+ const session = this.tokenManager.getSession();
828
+ if (!session) {
829
+ return {
830
+ data: null,
831
+ error: new MitwayBaasError("No persisted session", 0, "NO_SESSION")
832
+ };
833
+ }
834
+ this.http.setAuthToken(session.accessToken);
835
+ const refreshToken = this.tokenManager.getRefreshToken();
836
+ if (refreshToken) {
837
+ this.http.setRefreshToken(refreshToken);
838
+ }
839
+ try {
840
+ const response = await this.http.get(
841
+ "/api/auth/sessions/current"
842
+ );
843
+ if (response?.user) {
844
+ this.tokenManager.setUser(response.user);
845
+ return {
846
+ data: {
847
+ user: response.user,
848
+ accessToken: session.accessToken
849
+ },
850
+ error: null
851
+ };
852
+ }
853
+ this.tokenManager.clearSession();
854
+ this.http.setAuthToken(null);
855
+ this.http.setRefreshToken(null);
856
+ return {
857
+ data: null,
858
+ error: new MitwayBaasError("Invalid session", 401, "INVALID_SESSION")
859
+ };
860
+ } catch (error) {
861
+ this.tokenManager.clearSession();
862
+ this.http.setAuthToken(null);
863
+ this.http.setRefreshToken(null);
864
+ return wrapError(error, "Session restore failed");
865
+ }
866
+ }
714
867
  /**
715
868
  * Get the current in-memory session, or null if the user is not signed in.
716
869
  * Synchronous — does not hit the network.
@@ -1477,6 +1630,373 @@ var Realtime = class {
1477
1630
  }
1478
1631
  };
1479
1632
 
1633
+ // src/modules/storage.ts
1634
+ function bucketFromWire(row) {
1635
+ return {
1636
+ id: row.id,
1637
+ name: row.name,
1638
+ public: row.public,
1639
+ fileSizeLimitBytes: row.file_size_limit_bytes,
1640
+ allowedMimeTypes: row.allowed_mime_types,
1641
+ createdAt: row.created_at,
1642
+ updatedAt: row.updated_at
1643
+ };
1644
+ }
1645
+ function objectFromWire(row, bucketName) {
1646
+ return {
1647
+ id: row.id,
1648
+ bucket: bucketName,
1649
+ key: row.key,
1650
+ size: row.size,
1651
+ mimeType: row.mime_type,
1652
+ etag: row.etag,
1653
+ cacheControl: row.cache_control,
1654
+ contentDisposition: row.content_disposition,
1655
+ uploadedBy: row.uploaded_by,
1656
+ uploadedAt: row.uploaded_at,
1657
+ updatedAt: row.updated_at
1658
+ };
1659
+ }
1660
+ function configFromWire(row) {
1661
+ return {
1662
+ defaultFileSizeLimitBytes: row.default_file_size_limit_bytes,
1663
+ maxFileSizeLimitBytes: row.max_file_size_limit_bytes,
1664
+ tenantStorageQuotaBytes: row.tenant_storage_quota_bytes,
1665
+ reservedSpaceBytes: row.reserved_space_bytes,
1666
+ signedUrlDefaultTtlSec: row.signed_url_default_ttl_sec,
1667
+ signedUrlMaxTtlSec: row.signed_url_max_ttl_sec
1668
+ };
1669
+ }
1670
+ function encodeKey(key) {
1671
+ return key.split("/").map(encodeURIComponent).join("/");
1672
+ }
1673
+ function wrapError2(err, fallback) {
1674
+ if (err instanceof MitwayBaasError) return { data: null, error: err };
1675
+ return {
1676
+ data: null,
1677
+ error: new MitwayBaasError(
1678
+ err instanceof Error ? err.message : fallback,
1679
+ 0,
1680
+ "STORAGE_ERROR"
1681
+ )
1682
+ };
1683
+ }
1684
+ async function readEnvelopeError(response) {
1685
+ let code = "STORAGE_ERROR";
1686
+ let message = `HTTP ${response.status}`;
1687
+ try {
1688
+ const body = await response.json();
1689
+ if (body && body.error) {
1690
+ code = body.error.code ?? code;
1691
+ message = body.error.message ?? message;
1692
+ }
1693
+ } catch {
1694
+ }
1695
+ return new MitwayBaasError(message, response.status, code);
1696
+ }
1697
+ var StorageBucketClient = class {
1698
+ constructor(http, bucketName) {
1699
+ this.http = http;
1700
+ this.bucketName = bucketName;
1701
+ }
1702
+ http;
1703
+ bucketName;
1704
+ bucketBase() {
1705
+ return `/api/storage/buckets/${encodeURIComponent(this.bucketName)}`;
1706
+ }
1707
+ objectPath(key) {
1708
+ return `${this.bucketBase()}/objects/${encodeKey(key)}`;
1709
+ }
1710
+ async upload(key, body, opts = {}) {
1711
+ try {
1712
+ const method = opts.upsert ? "PUT" : "POST";
1713
+ const headers = {
1714
+ "Content-Type": opts.contentType ?? "application/octet-stream"
1715
+ };
1716
+ if (opts.cacheControl) headers["Cache-Control"] = opts.cacheControl;
1717
+ if (opts.contentDisposition)
1718
+ headers["Content-Disposition"] = opts.contentDisposition;
1719
+ const response = await this.http.rawFetch(this.objectPath(key), {
1720
+ method,
1721
+ headers,
1722
+ body,
1723
+ signal: opts.abortSignal
1724
+ });
1725
+ if (!response.ok) {
1726
+ return { data: null, error: await readEnvelopeError(response) };
1727
+ }
1728
+ const parsed = await response.json();
1729
+ if (parsed.error || !parsed.data) {
1730
+ return {
1731
+ data: null,
1732
+ error: new MitwayBaasError(
1733
+ parsed.error?.message ?? "Upload failed",
1734
+ response.status,
1735
+ parsed.error?.code ?? "STORAGE_ERROR"
1736
+ )
1737
+ };
1738
+ }
1739
+ return { data: objectFromWire(parsed.data, this.bucketName), error: null };
1740
+ } catch (err) {
1741
+ return wrapError2(err, "Upload failed");
1742
+ }
1743
+ }
1744
+ async download(key, opts = {}) {
1745
+ try {
1746
+ const headers = {};
1747
+ if (opts.range) {
1748
+ headers["Range"] = `bytes=${opts.range.start}-${opts.range.end}`;
1749
+ }
1750
+ const response = await this.http.rawFetch(this.objectPath(key), {
1751
+ method: "GET",
1752
+ headers,
1753
+ signal: opts.abortSignal
1754
+ });
1755
+ if (!response.ok) {
1756
+ return { data: null, error: await readEnvelopeError(response) };
1757
+ }
1758
+ const blob = await response.blob();
1759
+ return { data: blob, error: null };
1760
+ } catch (err) {
1761
+ return wrapError2(err, "Download failed");
1762
+ }
1763
+ }
1764
+ async getStream(key, opts = {}) {
1765
+ try {
1766
+ const headers = {};
1767
+ if (opts.range) {
1768
+ headers["Range"] = `bytes=${opts.range.start}-${opts.range.end}`;
1769
+ }
1770
+ const response = await this.http.rawFetch(this.objectPath(key), {
1771
+ method: "GET",
1772
+ headers,
1773
+ signal: opts.abortSignal
1774
+ });
1775
+ if (!response.ok) {
1776
+ return { data: null, error: await readEnvelopeError(response) };
1777
+ }
1778
+ if (!response.body) {
1779
+ return {
1780
+ data: null,
1781
+ error: new MitwayBaasError(
1782
+ "Response body is not a stream",
1783
+ response.status,
1784
+ "STORAGE_ERROR"
1785
+ )
1786
+ };
1787
+ }
1788
+ return {
1789
+ data: response.body,
1790
+ error: null
1791
+ };
1792
+ } catch (err) {
1793
+ return wrapError2(err, "Download failed");
1794
+ }
1795
+ }
1796
+ async remove(keys) {
1797
+ try {
1798
+ const results = await Promise.allSettled(
1799
+ keys.map(
1800
+ (key) => this.http.rawFetch(this.objectPath(key), { method: "DELETE" })
1801
+ )
1802
+ );
1803
+ const removed = [];
1804
+ const errors = [];
1805
+ for (let i = 0; i < keys.length; i++) {
1806
+ const key = keys[i];
1807
+ const r = results[i];
1808
+ if (r.status === "fulfilled" && r.value.ok) {
1809
+ removed.push(key);
1810
+ } else if (r.status === "fulfilled") {
1811
+ errors.push(`${key}: HTTP ${r.value.status}`);
1812
+ } else {
1813
+ const msg = r.reason instanceof Error ? r.reason.message : String(r.reason);
1814
+ errors.push(`${key}: ${msg}`);
1815
+ }
1816
+ }
1817
+ if (errors.length > 0) {
1818
+ return {
1819
+ data: null,
1820
+ error: new MitwayBaasError(
1821
+ `Failed to delete some objects: ${errors.join("; ")}`,
1822
+ 0,
1823
+ "STORAGE_ERROR"
1824
+ )
1825
+ };
1826
+ }
1827
+ return { data: { removed }, error: null };
1828
+ } catch (err) {
1829
+ return wrapError2(err, "Delete failed");
1830
+ }
1831
+ }
1832
+ async list(opts = {}) {
1833
+ try {
1834
+ const params = {};
1835
+ if (opts.prefix !== void 0) params.prefix = opts.prefix;
1836
+ if (opts.limit !== void 0) params.limit = String(opts.limit);
1837
+ if (opts.startAfter !== void 0) params.start_after = opts.startAfter;
1838
+ const rows = await this.http.get(
1839
+ `${this.bucketBase()}/objects`,
1840
+ { params }
1841
+ );
1842
+ return {
1843
+ data: rows.map((r) => objectFromWire(r, this.bucketName)),
1844
+ error: null
1845
+ };
1846
+ } catch (err) {
1847
+ return wrapError2(err, "List failed");
1848
+ }
1849
+ }
1850
+ async copy(fromKey, toKey, toBucket) {
1851
+ try {
1852
+ const row = await this.http.post(
1853
+ `${this.objectPath(fromKey)}/copy`,
1854
+ {
1855
+ dest_bucket: toBucket ?? this.bucketName,
1856
+ dest_key: toKey
1857
+ }
1858
+ );
1859
+ return {
1860
+ data: objectFromWire(row, toBucket ?? this.bucketName),
1861
+ error: null
1862
+ };
1863
+ } catch (err) {
1864
+ return wrapError2(err, "Copy failed");
1865
+ }
1866
+ }
1867
+ async move(fromKey, toKey, toBucket) {
1868
+ try {
1869
+ const row = await this.http.post(
1870
+ `${this.objectPath(fromKey)}/move`,
1871
+ {
1872
+ dest_bucket: toBucket ?? this.bucketName,
1873
+ dest_key: toKey
1874
+ }
1875
+ );
1876
+ return {
1877
+ data: objectFromWire(row, toBucket ?? this.bucketName),
1878
+ error: null
1879
+ };
1880
+ } catch (err) {
1881
+ return wrapError2(err, "Move failed");
1882
+ }
1883
+ }
1884
+ async createSignedUrl(key, opts = {}) {
1885
+ try {
1886
+ const body = {};
1887
+ if (opts.expiresIn !== void 0) body.expires_in = opts.expiresIn;
1888
+ const wire = await this.http.post(`${this.objectPath(key)}/sign`, body);
1889
+ return {
1890
+ data: {
1891
+ url: wire.url,
1892
+ token: wire.token,
1893
+ expiresAt: wire.expiresAt
1894
+ },
1895
+ error: null
1896
+ };
1897
+ } catch (err) {
1898
+ return wrapError2(err, "Sign failed");
1899
+ }
1900
+ }
1901
+ getPublicUrl(key) {
1902
+ const url = `${this.http.baseUrl.replace(/\/$/, "")}${this.objectPath(key)}`;
1903
+ return { data: { url } };
1904
+ }
1905
+ };
1906
+ var Storage = class {
1907
+ constructor(http) {
1908
+ this.http = http;
1909
+ }
1910
+ http;
1911
+ /** Scope subsequent operations to a single bucket. */
1912
+ from(bucketName) {
1913
+ return new StorageBucketClient(this.http, bucketName);
1914
+ }
1915
+ // --- Admin (require service_role) ---
1916
+ async listBuckets() {
1917
+ try {
1918
+ const rows = await this.http.get("/api/storage/buckets");
1919
+ return { data: rows.map(bucketFromWire), error: null };
1920
+ } catch (err) {
1921
+ return wrapError2(err, "listBuckets failed");
1922
+ }
1923
+ }
1924
+ async getBucket(name) {
1925
+ try {
1926
+ const row = await this.http.get(
1927
+ `/api/storage/buckets/${encodeURIComponent(name)}`
1928
+ );
1929
+ return { data: bucketFromWire(row), error: null };
1930
+ } catch (err) {
1931
+ return wrapError2(err, "getBucket failed");
1932
+ }
1933
+ }
1934
+ async createBucket(name, opts = {}) {
1935
+ try {
1936
+ const body = { name };
1937
+ if (opts.public !== void 0) body.public = opts.public;
1938
+ if (opts.fileSizeLimitBytes !== void 0)
1939
+ body.file_size_limit_bytes = opts.fileSizeLimitBytes;
1940
+ if (opts.allowedMimeTypes !== void 0)
1941
+ body.allowed_mime_types = opts.allowedMimeTypes;
1942
+ const row = await this.http.post(
1943
+ "/api/storage/buckets",
1944
+ body
1945
+ );
1946
+ return { data: bucketFromWire(row), error: null };
1947
+ } catch (err) {
1948
+ return wrapError2(err, "createBucket failed");
1949
+ }
1950
+ }
1951
+ async updateBucket(name, opts) {
1952
+ try {
1953
+ const body = {};
1954
+ if (opts.public !== void 0) body.public = opts.public;
1955
+ if (opts.fileSizeLimitBytes !== void 0)
1956
+ body.file_size_limit_bytes = opts.fileSizeLimitBytes;
1957
+ if (opts.allowedMimeTypes !== void 0)
1958
+ body.allowed_mime_types = opts.allowedMimeTypes;
1959
+ const row = await this.http.patch(
1960
+ `/api/storage/buckets/${encodeURIComponent(name)}`,
1961
+ body
1962
+ );
1963
+ return { data: bucketFromWire(row), error: null };
1964
+ } catch (err) {
1965
+ return wrapError2(err, "updateBucket failed");
1966
+ }
1967
+ }
1968
+ async deleteBucket(name) {
1969
+ try {
1970
+ await this.http.delete(
1971
+ `/api/storage/buckets/${encodeURIComponent(name)}`
1972
+ );
1973
+ return { data: null, error: null };
1974
+ } catch (err) {
1975
+ return wrapError2(err, "deleteBucket failed");
1976
+ }
1977
+ }
1978
+ async emptyBucket(name) {
1979
+ try {
1980
+ const result = await this.http.post(
1981
+ `/api/storage/buckets/${encodeURIComponent(name)}/empty`,
1982
+ {}
1983
+ );
1984
+ return { data: result, error: null };
1985
+ } catch (err) {
1986
+ return wrapError2(err, "emptyBucket failed");
1987
+ }
1988
+ }
1989
+ // --- Storage config (service_role) ---
1990
+ async getConfig() {
1991
+ try {
1992
+ const row = await this.http.get("/api/storage/config");
1993
+ return { data: configFromWire(row), error: null };
1994
+ } catch (err) {
1995
+ return wrapError2(err, "getConfig failed");
1996
+ }
1997
+ }
1998
+ };
1999
+
1480
2000
  // src/client.ts
1481
2001
  var MitwayBaasClient = class {
1482
2002
  http;
@@ -1484,9 +2004,13 @@ var MitwayBaasClient = class {
1484
2004
  auth;
1485
2005
  database;
1486
2006
  realtime;
2007
+ storage;
1487
2008
  constructor(config = {}) {
1488
2009
  const logger = new Logger(config.debug);
1489
- this.tokenManager = new TokenManager();
2010
+ this.tokenManager = new TokenManager({
2011
+ persistSession: config.persistSession,
2012
+ storageKey: config.storageKey
2013
+ });
1490
2014
  this.http = new HttpClient(config, this.tokenManager, logger);
1491
2015
  this.auth = new Auth(this.http, this.tokenManager);
1492
2016
  this.database = new Database(this.http, this.tokenManager, config.anonKey);
@@ -1496,6 +2020,7 @@ var MitwayBaasClient = class {
1496
2020
  config.anonKey,
1497
2021
  config.realtime
1498
2022
  );
2023
+ this.storage = new Storage(this.http);
1499
2024
  }
1500
2025
  /**
1501
2026
  * Escape hatch for callers that need to make custom requests against the
@@ -1520,6 +2045,8 @@ export {
1520
2045
  MitwayBaasError,
1521
2046
  Realtime,
1522
2047
  RealtimeChannel,
2048
+ Storage,
2049
+ StorageBucketClient,
1523
2050
  TokenManager,
1524
2051
  createClient,
1525
2052
  index_default as default