@mitway/sdk 0.2.4 → 0.4.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
@@ -528,6 +528,29 @@ var HttpClient = class {
528
528
  throw error;
529
529
  }
530
530
  }
531
+ /**
532
+ * Low-level fetch helper for binary bodies (uploads) and streamed responses
533
+ * (downloads). Applies the current Bearer token (user session → anon key
534
+ * fallback) plus any configured default headers, resolves `path` against
535
+ * `baseUrl`, and returns the raw `Response` — it does NOT unwrap the
536
+ * `{ data, error }` envelope, so the caller is responsible for status
537
+ * checking and parsing.
538
+ *
539
+ * Used by the storage module for object upload/download paths where the
540
+ * body or response is not JSON.
541
+ */
542
+ async rawFetch(path, init = {}) {
543
+ const url = this.buildUrl(path);
544
+ const headers = new Headers(init.headers ?? {});
545
+ for (const [k, v] of Object.entries(this.defaultHeaders)) {
546
+ if (!headers.has(k)) headers.set(k, v);
547
+ }
548
+ if (!headers.has("Authorization")) {
549
+ const token = this.userToken ?? this.anonKey;
550
+ if (token) headers.set("Authorization", `Bearer ${token}`);
551
+ }
552
+ return this.fetch(url, { ...init, headers });
553
+ }
531
554
  get(path, options) {
532
555
  return this.request("GET", path, options);
533
556
  }
@@ -857,6 +880,11 @@ var Database = class {
857
880
  // src/modules/realtime.ts
858
881
  import { io } from "socket.io-client";
859
882
  var PRESENCE_HEARTBEAT_MS = 2e4;
883
+ function makeChannelError(code, message) {
884
+ const err = new Error(message);
885
+ err.code = code;
886
+ return err;
887
+ }
860
888
  var DEFAULT_CONNECT_TIMEOUT_MS = 1e4;
861
889
  var RealtimeChannel = class {
862
890
  constructor(topic, realtime, options = {}) {
@@ -927,12 +955,19 @@ var RealtimeChannel = class {
927
955
  this.statusCallback?.("SUBSCRIBED");
928
956
  } catch (err) {
929
957
  this.state = "errored";
930
- this.statusCallback?.("CHANNEL_ERROR", {
931
- code: "REJOIN_FAILED",
932
- message: err instanceof Error ? err.message : String(err)
933
- });
958
+ this.statusCallback?.(
959
+ "CHANNEL_ERROR",
960
+ makeChannelError("REJOIN_FAILED", err instanceof Error ? err.message : String(err))
961
+ );
934
962
  }
935
963
  }
964
+ // ── implementation signature (not in public type surface).
965
+ // The callback type is intentionally broad: each overload above pins a
966
+ // specific payload shape, but TypeScript overload resolution needs the
967
+ // implementation to accept the union of every narrow callback without
968
+ // the contravariance conflict (TS2394). `any` is the standard escape
969
+ // hatch for this exact pattern and is confined to this one line — the
970
+ // public surface users see is strictly typed via the overloads.
936
971
  on(type, filter, callback) {
937
972
  if (type === "postgres_changes") {
938
973
  this.bindings.push({
@@ -1034,18 +1069,18 @@ var RealtimeChannel = class {
1034
1069
  } catch (err) {
1035
1070
  this.state = "errored";
1036
1071
  const message = err instanceof Error ? err.message : String(err);
1037
- this.statusCallback?.("CHANNEL_ERROR", {
1038
- code: "SUBSCRIBE_FAILED",
1039
- message
1040
- });
1072
+ this.statusCallback?.(
1073
+ "CHANNEL_ERROR",
1074
+ makeChannelError("SUBSCRIBE_FAILED", message)
1075
+ );
1041
1076
  }
1042
1077
  },
1043
1078
  (err) => {
1044
1079
  this.state = "errored";
1045
- this.statusCallback?.("CHANNEL_ERROR", {
1046
- code: "CONNECT_FAILED",
1047
- message: err.message
1048
- });
1080
+ this.statusCallback?.(
1081
+ "CHANNEL_ERROR",
1082
+ makeChannelError("CONNECT_FAILED", err.message)
1083
+ );
1049
1084
  }
1050
1085
  );
1051
1086
  return this;
@@ -1098,6 +1133,12 @@ var RealtimeChannel = class {
1098
1133
  * events, write to your own application table and enable
1099
1134
  * `postgres_changes` on it; the SDK will surface the INSERT as a
1100
1135
  * `postgres_changes` event without a separate channel.send call.
1136
+ *
1137
+ * The returned promise always resolves with the server ack, so callers
1138
+ * can `await channel.send(...)` to confirm delivery + get the server-
1139
+ * assigned `message_id`. There's no performance cost — Socket.IO piggy-
1140
+ * backs the ack on the same frame. Callers that don't need it just
1141
+ * don't await.
1101
1142
  */
1102
1143
  async send(args) {
1103
1144
  if (args.type !== "broadcast") {
@@ -1124,9 +1165,7 @@ var RealtimeChannel = class {
1124
1165
  if (!socket) {
1125
1166
  throw new MitwayBaasError("Socket not connected", 503, "NOT_CONNECTED");
1126
1167
  }
1127
- const broadcastCfg = this.options.config?.broadcast;
1128
- const wantAck = broadcastCfg?.ack === true;
1129
- const self = broadcastCfg?.self;
1168
+ const self = this.options.config?.broadcast?.self;
1130
1169
  const wirePayload = {
1131
1170
  channel: this.topic,
1132
1171
  event: args.event,
@@ -1135,10 +1174,6 @@ var RealtimeChannel = class {
1135
1174
  if (self === false) {
1136
1175
  wirePayload.self = false;
1137
1176
  }
1138
- if (!wantAck) {
1139
- socket.emit("realtime:publish", wirePayload);
1140
- return;
1141
- }
1142
1177
  return await new Promise((resolve) => {
1143
1178
  socket.emit(
1144
1179
  "realtime:publish",
@@ -1179,7 +1214,7 @@ var RealtimeChannel = class {
1179
1214
  if (!b.subscriptionId || !pcEvent.ids.includes(b.subscriptionId)) {
1180
1215
  continue;
1181
1216
  }
1182
- const matchesEvent = b.filter.event === "*" || b.filter.event === pcEvent.data.type;
1217
+ const matchesEvent = b.filter.event === "*" || b.filter.event === pcEvent.data.eventType;
1183
1218
  if (!matchesEvent) {
1184
1219
  continue;
1185
1220
  }
@@ -1238,7 +1273,13 @@ var RealtimeChannel = class {
1238
1273
  continue;
1239
1274
  }
1240
1275
  try {
1241
- b.callback(payload);
1276
+ if (payload.event === "sync") {
1277
+ b.callback();
1278
+ } else if (payload.event === "join") {
1279
+ b.callback(payload);
1280
+ } else {
1281
+ b.callback(payload);
1282
+ }
1242
1283
  } catch {
1243
1284
  }
1244
1285
  }
@@ -1257,7 +1298,7 @@ var RealtimeChannel = class {
1257
1298
  "realtime:subscribe",
1258
1299
  { channel: this.topic, private: this.isPrivate },
1259
1300
  (ack) => {
1260
- if (ack.ok) {
1301
+ if (ack.status === "ok") {
1261
1302
  resolve();
1262
1303
  } else {
1263
1304
  reject(new Error(ack.error?.message ?? "subscribe failed"));
@@ -1281,7 +1322,7 @@ var RealtimeChannel = class {
1281
1322
  filter: b.filter.filter
1282
1323
  },
1283
1324
  (ack) => {
1284
- if (ack.ok && ack.subscription_id) {
1325
+ if (ack.status === "ok" && ack.subscription_id) {
1285
1326
  b.subscriptionId = ack.subscription_id;
1286
1327
  resolve();
1287
1328
  } else {
@@ -1327,13 +1368,14 @@ var Realtime = class {
1327
1368
  * The optional `opts` argument lets the caller configure the channel:
1328
1369
  * * `config.private` — enable subscribe-side authorization against
1329
1370
  * `realtime.authorize_subscribe(...)` on the tenant DB.
1330
- * * `config.broadcast.ack` — `channel.send()` resolves with the
1331
- * server's ack (message_id) instead of fire-and-forget.
1332
1371
  * * `config.broadcast.self` — `false` excludes the sender from the
1333
1372
  * fan-out (defaults to `true`).
1334
1373
  * * `config.presence.key` — stable presence key to group multiple
1335
1374
  * tabs of the same user under one entry.
1336
1375
  *
1376
+ * `channel.send()` always resolves with the server ack (see its own
1377
+ * docstring); there is no separate opt-in needed.
1378
+ *
1337
1379
  * Options are locked in when the channel is first created; subsequent
1338
1380
  * `.channel('same')` calls with different opts are ignored. Pass a
1339
1381
  * different topic to get a different-configured channel.
@@ -1458,6 +1500,373 @@ var Realtime = class {
1458
1500
  }
1459
1501
  };
1460
1502
 
1503
+ // src/modules/storage.ts
1504
+ function bucketFromWire(row) {
1505
+ return {
1506
+ id: row.id,
1507
+ name: row.name,
1508
+ public: row.public,
1509
+ fileSizeLimitBytes: row.file_size_limit_bytes,
1510
+ allowedMimeTypes: row.allowed_mime_types,
1511
+ createdAt: row.created_at,
1512
+ updatedAt: row.updated_at
1513
+ };
1514
+ }
1515
+ function objectFromWire(row, bucketName) {
1516
+ return {
1517
+ id: row.id,
1518
+ bucket: bucketName,
1519
+ key: row.key,
1520
+ size: row.size,
1521
+ mimeType: row.mime_type,
1522
+ etag: row.etag,
1523
+ cacheControl: row.cache_control,
1524
+ contentDisposition: row.content_disposition,
1525
+ uploadedBy: row.uploaded_by,
1526
+ uploadedAt: row.uploaded_at,
1527
+ updatedAt: row.updated_at
1528
+ };
1529
+ }
1530
+ function configFromWire(row) {
1531
+ return {
1532
+ defaultFileSizeLimitBytes: row.default_file_size_limit_bytes,
1533
+ maxFileSizeLimitBytes: row.max_file_size_limit_bytes,
1534
+ tenantStorageQuotaBytes: row.tenant_storage_quota_bytes,
1535
+ reservedSpaceBytes: row.reserved_space_bytes,
1536
+ signedUrlDefaultTtlSec: row.signed_url_default_ttl_sec,
1537
+ signedUrlMaxTtlSec: row.signed_url_max_ttl_sec
1538
+ };
1539
+ }
1540
+ function encodeKey(key) {
1541
+ return key.split("/").map(encodeURIComponent).join("/");
1542
+ }
1543
+ function wrapError2(err, fallback) {
1544
+ if (err instanceof MitwayBaasError) return { data: null, error: err };
1545
+ return {
1546
+ data: null,
1547
+ error: new MitwayBaasError(
1548
+ err instanceof Error ? err.message : fallback,
1549
+ 0,
1550
+ "STORAGE_ERROR"
1551
+ )
1552
+ };
1553
+ }
1554
+ async function readEnvelopeError(response) {
1555
+ let code = "STORAGE_ERROR";
1556
+ let message = `HTTP ${response.status}`;
1557
+ try {
1558
+ const body = await response.json();
1559
+ if (body && body.error) {
1560
+ code = body.error.code ?? code;
1561
+ message = body.error.message ?? message;
1562
+ }
1563
+ } catch {
1564
+ }
1565
+ return new MitwayBaasError(message, response.status, code);
1566
+ }
1567
+ var StorageBucketClient = class {
1568
+ constructor(http, bucketName) {
1569
+ this.http = http;
1570
+ this.bucketName = bucketName;
1571
+ }
1572
+ http;
1573
+ bucketName;
1574
+ bucketBase() {
1575
+ return `/api/storage/buckets/${encodeURIComponent(this.bucketName)}`;
1576
+ }
1577
+ objectPath(key) {
1578
+ return `${this.bucketBase()}/objects/${encodeKey(key)}`;
1579
+ }
1580
+ async upload(key, body, opts = {}) {
1581
+ try {
1582
+ const method = opts.upsert ? "PUT" : "POST";
1583
+ const headers = {
1584
+ "Content-Type": opts.contentType ?? "application/octet-stream"
1585
+ };
1586
+ if (opts.cacheControl) headers["Cache-Control"] = opts.cacheControl;
1587
+ if (opts.contentDisposition)
1588
+ headers["Content-Disposition"] = opts.contentDisposition;
1589
+ const response = await this.http.rawFetch(this.objectPath(key), {
1590
+ method,
1591
+ headers,
1592
+ body,
1593
+ signal: opts.abortSignal
1594
+ });
1595
+ if (!response.ok) {
1596
+ return { data: null, error: await readEnvelopeError(response) };
1597
+ }
1598
+ const parsed = await response.json();
1599
+ if (parsed.error || !parsed.data) {
1600
+ return {
1601
+ data: null,
1602
+ error: new MitwayBaasError(
1603
+ parsed.error?.message ?? "Upload failed",
1604
+ response.status,
1605
+ parsed.error?.code ?? "STORAGE_ERROR"
1606
+ )
1607
+ };
1608
+ }
1609
+ return { data: objectFromWire(parsed.data, this.bucketName), error: null };
1610
+ } catch (err) {
1611
+ return wrapError2(err, "Upload failed");
1612
+ }
1613
+ }
1614
+ async download(key, opts = {}) {
1615
+ try {
1616
+ const headers = {};
1617
+ if (opts.range) {
1618
+ headers["Range"] = `bytes=${opts.range.start}-${opts.range.end}`;
1619
+ }
1620
+ const response = await this.http.rawFetch(this.objectPath(key), {
1621
+ method: "GET",
1622
+ headers,
1623
+ signal: opts.abortSignal
1624
+ });
1625
+ if (!response.ok) {
1626
+ return { data: null, error: await readEnvelopeError(response) };
1627
+ }
1628
+ const blob = await response.blob();
1629
+ return { data: blob, error: null };
1630
+ } catch (err) {
1631
+ return wrapError2(err, "Download failed");
1632
+ }
1633
+ }
1634
+ async getStream(key, opts = {}) {
1635
+ try {
1636
+ const headers = {};
1637
+ if (opts.range) {
1638
+ headers["Range"] = `bytes=${opts.range.start}-${opts.range.end}`;
1639
+ }
1640
+ const response = await this.http.rawFetch(this.objectPath(key), {
1641
+ method: "GET",
1642
+ headers,
1643
+ signal: opts.abortSignal
1644
+ });
1645
+ if (!response.ok) {
1646
+ return { data: null, error: await readEnvelopeError(response) };
1647
+ }
1648
+ if (!response.body) {
1649
+ return {
1650
+ data: null,
1651
+ error: new MitwayBaasError(
1652
+ "Response body is not a stream",
1653
+ response.status,
1654
+ "STORAGE_ERROR"
1655
+ )
1656
+ };
1657
+ }
1658
+ return {
1659
+ data: response.body,
1660
+ error: null
1661
+ };
1662
+ } catch (err) {
1663
+ return wrapError2(err, "Download failed");
1664
+ }
1665
+ }
1666
+ async remove(keys) {
1667
+ try {
1668
+ const results = await Promise.allSettled(
1669
+ keys.map(
1670
+ (key) => this.http.rawFetch(this.objectPath(key), { method: "DELETE" })
1671
+ )
1672
+ );
1673
+ const removed = [];
1674
+ const errors = [];
1675
+ for (let i = 0; i < keys.length; i++) {
1676
+ const key = keys[i];
1677
+ const r = results[i];
1678
+ if (r.status === "fulfilled" && r.value.ok) {
1679
+ removed.push(key);
1680
+ } else if (r.status === "fulfilled") {
1681
+ errors.push(`${key}: HTTP ${r.value.status}`);
1682
+ } else {
1683
+ const msg = r.reason instanceof Error ? r.reason.message : String(r.reason);
1684
+ errors.push(`${key}: ${msg}`);
1685
+ }
1686
+ }
1687
+ if (errors.length > 0) {
1688
+ return {
1689
+ data: null,
1690
+ error: new MitwayBaasError(
1691
+ `Failed to delete some objects: ${errors.join("; ")}`,
1692
+ 0,
1693
+ "STORAGE_ERROR"
1694
+ )
1695
+ };
1696
+ }
1697
+ return { data: { removed }, error: null };
1698
+ } catch (err) {
1699
+ return wrapError2(err, "Delete failed");
1700
+ }
1701
+ }
1702
+ async list(opts = {}) {
1703
+ try {
1704
+ const params = {};
1705
+ if (opts.prefix !== void 0) params.prefix = opts.prefix;
1706
+ if (opts.limit !== void 0) params.limit = String(opts.limit);
1707
+ if (opts.startAfter !== void 0) params.start_after = opts.startAfter;
1708
+ const rows = await this.http.get(
1709
+ `${this.bucketBase()}/objects`,
1710
+ { params }
1711
+ );
1712
+ return {
1713
+ data: rows.map((r) => objectFromWire(r, this.bucketName)),
1714
+ error: null
1715
+ };
1716
+ } catch (err) {
1717
+ return wrapError2(err, "List failed");
1718
+ }
1719
+ }
1720
+ async copy(fromKey, toKey, toBucket) {
1721
+ try {
1722
+ const row = await this.http.post(
1723
+ `${this.objectPath(fromKey)}/copy`,
1724
+ {
1725
+ dest_bucket: toBucket ?? this.bucketName,
1726
+ dest_key: toKey
1727
+ }
1728
+ );
1729
+ return {
1730
+ data: objectFromWire(row, toBucket ?? this.bucketName),
1731
+ error: null
1732
+ };
1733
+ } catch (err) {
1734
+ return wrapError2(err, "Copy failed");
1735
+ }
1736
+ }
1737
+ async move(fromKey, toKey, toBucket) {
1738
+ try {
1739
+ const row = await this.http.post(
1740
+ `${this.objectPath(fromKey)}/move`,
1741
+ {
1742
+ dest_bucket: toBucket ?? this.bucketName,
1743
+ dest_key: toKey
1744
+ }
1745
+ );
1746
+ return {
1747
+ data: objectFromWire(row, toBucket ?? this.bucketName),
1748
+ error: null
1749
+ };
1750
+ } catch (err) {
1751
+ return wrapError2(err, "Move failed");
1752
+ }
1753
+ }
1754
+ async createSignedUrl(key, opts = {}) {
1755
+ try {
1756
+ const body = {};
1757
+ if (opts.expiresIn !== void 0) body.expires_in = opts.expiresIn;
1758
+ const wire = await this.http.post(`${this.objectPath(key)}/sign`, body);
1759
+ return {
1760
+ data: {
1761
+ url: wire.url,
1762
+ token: wire.token,
1763
+ expiresAt: wire.expiresAt
1764
+ },
1765
+ error: null
1766
+ };
1767
+ } catch (err) {
1768
+ return wrapError2(err, "Sign failed");
1769
+ }
1770
+ }
1771
+ getPublicUrl(key) {
1772
+ const url = `${this.http.baseUrl.replace(/\/$/, "")}${this.objectPath(key)}`;
1773
+ return { data: { url } };
1774
+ }
1775
+ };
1776
+ var Storage = class {
1777
+ constructor(http) {
1778
+ this.http = http;
1779
+ }
1780
+ http;
1781
+ /** Scope subsequent operations to a single bucket. */
1782
+ from(bucketName) {
1783
+ return new StorageBucketClient(this.http, bucketName);
1784
+ }
1785
+ // --- Admin (require service_role) ---
1786
+ async listBuckets() {
1787
+ try {
1788
+ const rows = await this.http.get("/api/storage/buckets");
1789
+ return { data: rows.map(bucketFromWire), error: null };
1790
+ } catch (err) {
1791
+ return wrapError2(err, "listBuckets failed");
1792
+ }
1793
+ }
1794
+ async getBucket(name) {
1795
+ try {
1796
+ const row = await this.http.get(
1797
+ `/api/storage/buckets/${encodeURIComponent(name)}`
1798
+ );
1799
+ return { data: bucketFromWire(row), error: null };
1800
+ } catch (err) {
1801
+ return wrapError2(err, "getBucket failed");
1802
+ }
1803
+ }
1804
+ async createBucket(name, opts = {}) {
1805
+ try {
1806
+ const body = { name };
1807
+ if (opts.public !== void 0) body.public = opts.public;
1808
+ if (opts.fileSizeLimitBytes !== void 0)
1809
+ body.file_size_limit_bytes = opts.fileSizeLimitBytes;
1810
+ if (opts.allowedMimeTypes !== void 0)
1811
+ body.allowed_mime_types = opts.allowedMimeTypes;
1812
+ const row = await this.http.post(
1813
+ "/api/storage/buckets",
1814
+ body
1815
+ );
1816
+ return { data: bucketFromWire(row), error: null };
1817
+ } catch (err) {
1818
+ return wrapError2(err, "createBucket failed");
1819
+ }
1820
+ }
1821
+ async updateBucket(name, opts) {
1822
+ try {
1823
+ const body = {};
1824
+ if (opts.public !== void 0) body.public = opts.public;
1825
+ if (opts.fileSizeLimitBytes !== void 0)
1826
+ body.file_size_limit_bytes = opts.fileSizeLimitBytes;
1827
+ if (opts.allowedMimeTypes !== void 0)
1828
+ body.allowed_mime_types = opts.allowedMimeTypes;
1829
+ const row = await this.http.patch(
1830
+ `/api/storage/buckets/${encodeURIComponent(name)}`,
1831
+ body
1832
+ );
1833
+ return { data: bucketFromWire(row), error: null };
1834
+ } catch (err) {
1835
+ return wrapError2(err, "updateBucket failed");
1836
+ }
1837
+ }
1838
+ async deleteBucket(name) {
1839
+ try {
1840
+ await this.http.delete(
1841
+ `/api/storage/buckets/${encodeURIComponent(name)}`
1842
+ );
1843
+ return { data: null, error: null };
1844
+ } catch (err) {
1845
+ return wrapError2(err, "deleteBucket failed");
1846
+ }
1847
+ }
1848
+ async emptyBucket(name) {
1849
+ try {
1850
+ const result = await this.http.post(
1851
+ `/api/storage/buckets/${encodeURIComponent(name)}/empty`,
1852
+ {}
1853
+ );
1854
+ return { data: result, error: null };
1855
+ } catch (err) {
1856
+ return wrapError2(err, "emptyBucket failed");
1857
+ }
1858
+ }
1859
+ // --- Storage config (service_role) ---
1860
+ async getConfig() {
1861
+ try {
1862
+ const row = await this.http.get("/api/storage/config");
1863
+ return { data: configFromWire(row), error: null };
1864
+ } catch (err) {
1865
+ return wrapError2(err, "getConfig failed");
1866
+ }
1867
+ }
1868
+ };
1869
+
1461
1870
  // src/client.ts
1462
1871
  var MitwayBaasClient = class {
1463
1872
  http;
@@ -1465,6 +1874,7 @@ var MitwayBaasClient = class {
1465
1874
  auth;
1466
1875
  database;
1467
1876
  realtime;
1877
+ storage;
1468
1878
  constructor(config = {}) {
1469
1879
  const logger = new Logger(config.debug);
1470
1880
  this.tokenManager = new TokenManager();
@@ -1477,6 +1887,7 @@ var MitwayBaasClient = class {
1477
1887
  config.anonKey,
1478
1888
  config.realtime
1479
1889
  );
1890
+ this.storage = new Storage(this.http);
1480
1891
  }
1481
1892
  /**
1482
1893
  * Escape hatch for callers that need to make custom requests against the
@@ -1501,6 +1912,8 @@ export {
1501
1912
  MitwayBaasError,
1502
1913
  Realtime,
1503
1914
  RealtimeChannel,
1915
+ Storage,
1916
+ StorageBucketClient,
1504
1917
  TokenManager,
1505
1918
  createClient,
1506
1919
  index_default as default