@giaeulate/baas-sdk 1.3.0 → 1.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
@@ -4,8 +4,10 @@ var HttpClient = class {
4
4
  apiKey = "";
5
5
  keyType;
6
6
  token = null;
7
+ refreshToken = null;
7
8
  environment = "prod";
8
9
  _onForceLogout = null;
10
+ _refreshing = null;
9
11
  constructor(url, apiKey, options) {
10
12
  let baseUrl = url || (typeof window !== "undefined" ? window.location.origin : "http://localhost:8080");
11
13
  this.url = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
@@ -13,6 +15,7 @@ var HttpClient = class {
13
15
  this.keyType = options?.keyType ?? "service_role";
14
16
  this.warnIfUnsafeKey();
15
17
  this.token = typeof localStorage !== "undefined" ? this.cleanValue(localStorage.getItem("baas_token")) : null;
18
+ this.refreshToken = typeof localStorage !== "undefined" ? this.cleanValue(localStorage.getItem("baas_refresh_token")) : null;
16
19
  }
17
20
  /** SECURITY: a service_role key bypasses RLS and grants admin over every
18
21
  * tenant. If it ends up in a browser bundle, anyone can extract it. Warn
@@ -31,12 +34,59 @@ var HttpClient = class {
31
34
  */
32
35
  setToken(token) {
33
36
  this.token = token;
37
+ if (typeof localStorage === "undefined") return;
34
38
  if (token) {
35
39
  localStorage.setItem("baas_token", token);
36
40
  } else {
37
41
  localStorage.removeItem("baas_token");
38
42
  }
39
43
  }
44
+ /** Persist the rotating refresh token (used by auto-refresh on 401). In the
45
+ * browser the server also sets an HttpOnly `baas_refresh` cookie, so storing
46
+ * it here is mainly for non-cookie clients (React Native). */
47
+ setRefreshToken(token) {
48
+ this.refreshToken = token;
49
+ if (typeof localStorage === "undefined") return;
50
+ if (token) {
51
+ localStorage.setItem("baas_refresh_token", token);
52
+ } else {
53
+ localStorage.removeItem("baas_refresh_token");
54
+ }
55
+ }
56
+ getRefreshToken() {
57
+ const ls = typeof localStorage !== "undefined" ? localStorage.getItem("baas_refresh_token") : null;
58
+ return this.cleanValue(this.refreshToken || ls);
59
+ }
60
+ /** Attempts a single token refresh, deduped across concurrent 401s. Sends the
61
+ * stored refresh token (falls back to the HttpOnly cookie in browsers). */
62
+ tryRefresh() {
63
+ if (!this._refreshing) {
64
+ this._refreshing = this.doRefresh().finally(() => {
65
+ this._refreshing = null;
66
+ });
67
+ }
68
+ return this._refreshing;
69
+ }
70
+ async doRefresh() {
71
+ try {
72
+ const body = {};
73
+ const rt = this.getRefreshToken();
74
+ if (rt) body.refresh_token = rt;
75
+ const res = await fetch(`${this.url}/api/auth/refresh`, {
76
+ method: "POST",
77
+ headers: { "Content-Type": "application/json", apikey: this.apiKey },
78
+ credentials: "include",
79
+ body: JSON.stringify(body)
80
+ });
81
+ if (!res.ok) return false;
82
+ const data = await res.json().catch(() => null);
83
+ if (data?.token) this.setToken(data.token);
84
+ if (data?.refresh_token) this.setRefreshToken(data.refresh_token);
85
+ return Boolean(data?.token);
86
+ } catch {
87
+ return false;
88
+ }
89
+ }
40
90
  /**
41
91
  * Register a callback invoked on forced logout (401).
42
92
  * Use this in React Native instead of the default window.location redirect.
@@ -77,7 +127,7 @@ var HttpClient = class {
77
127
  /**
78
128
  * Core HTTP request method - DRY principle
79
129
  */
80
- async request(endpoint, options = {}) {
130
+ async request(endpoint, options = {}, _isRetry = false) {
81
131
  const { method = "GET", body, headers: customHeaders, skipAuth = false } = options;
82
132
  const headers = { ...this.getHeaders(), ...customHeaders };
83
133
  const fetchOptions = {
@@ -95,6 +145,9 @@ var HttpClient = class {
95
145
  const data = await res.json().catch(() => null);
96
146
  if (!res.ok) {
97
147
  if (res.status === 401 && !skipAuth) {
148
+ if (!_isRetry && await this.tryRefresh()) {
149
+ return this.request(endpoint, options, true);
150
+ }
98
151
  this.forceLogout();
99
152
  }
100
153
  if (res.status === 403 && data?.error === "ip_not_allowed") {
@@ -139,7 +192,11 @@ var HttpClient = class {
139
192
  }
140
193
  logout() {
141
194
  this.token = null;
142
- localStorage.removeItem("baas_token");
195
+ this.refreshToken = null;
196
+ if (typeof localStorage !== "undefined") {
197
+ localStorage.removeItem("baas_token");
198
+ localStorage.removeItem("baas_refresh_token");
199
+ }
143
200
  }
144
201
  forceLogout() {
145
202
  this.logout();
@@ -380,17 +437,27 @@ function createAuthModule(client) {
380
437
  throw new Error(data?.error || `Login failed: ${res.status}`);
381
438
  }
382
439
  if (data.token) {
383
- client.token = data.token;
384
- localStorage.setItem("baas_token", data.token);
440
+ client.setToken(data.token);
385
441
  }
442
+ if (data.refresh_token) {
443
+ client.setRefreshToken(data.refresh_token);
444
+ }
445
+ return data;
446
+ },
447
+ /** Manually refresh the access token via the rotating refresh token. The
448
+ * HttpClient also does this automatically on a 401. */
449
+ async refresh() {
450
+ const rt = client.refreshToken;
451
+ const data = await post("/api/auth/refresh", rt ? { refresh_token: rt } : {});
452
+ if (data?.token) client.setToken(data.token);
453
+ if (data?.refresh_token) client.setRefreshToken(data.refresh_token);
386
454
  return data;
387
455
  },
388
456
  async logout() {
389
457
  try {
390
458
  await post("/api/logout");
391
459
  } finally {
392
- client.token = null;
393
- localStorage.removeItem("baas_token");
460
+ client.logout();
394
461
  }
395
462
  },
396
463
  // Anonymous sign-in (Supabase parity). Response carries access_token (+ a
@@ -675,11 +742,11 @@ function createStorageModule(client) {
675
742
  async deleteFile(fileId) {
676
743
  return del(`/api/storage/files/${fileId}`);
677
744
  },
678
- async createBucket(name, isPublic = false) {
679
- return post("/api/storage/buckets", { name, is_public: isPublic });
745
+ async createBucket(name, isPublic = false, maxBytes) {
746
+ return post("/api/storage/buckets", { name, is_public: isPublic, max_bytes: maxBytes });
680
747
  },
681
748
  async updateBucket(bucketId, opts) {
682
- return put(`/api/storage/buckets/${bucketId}`, { is_public: opts.isPublic });
749
+ return put(`/api/storage/buckets/${bucketId}`, { is_public: opts.isPublic, max_bytes: opts.maxBytes });
683
750
  },
684
751
  async listBuckets() {
685
752
  return get("/api/storage/buckets");
@@ -896,9 +963,18 @@ function createEmailModule(client) {
896
963
  async getConfig() {
897
964
  return get("/api/email/config");
898
965
  },
966
+ async listConfigs() {
967
+ return get("/api/email/configs");
968
+ },
899
969
  async saveConfig(config) {
900
970
  return post("/api/email/config", config);
901
971
  },
972
+ async setDefaultConfig(id) {
973
+ return post(`/api/email/configs/${id}/default`);
974
+ },
975
+ async deleteConfig(id) {
976
+ return del(`/api/email/configs/${id}`);
977
+ },
902
978
  async createTemplate(template) {
903
979
  return post("/api/email/templates", template);
904
980
  },
@@ -1050,6 +1126,9 @@ function createAuditModule(client) {
1050
1126
  },
1051
1127
  async purge(olderThanDays) {
1052
1128
  return del(`/api/audit/purge?older_than_days=${olderThanDays}`);
1129
+ },
1130
+ async clearAll() {
1131
+ return del(`/api/audit/clear`);
1053
1132
  }
1054
1133
  };
1055
1134
  }
@@ -1150,6 +1229,16 @@ var RealtimeService = class {
1150
1229
  }
1151
1230
  socket = null;
1152
1231
  subscribers = /* @__PURE__ */ new Map();
1232
+ // The server-side value filter currently REGISTERED per table (e.g.
1233
+ // { status: 'eq.pending' }). The server keeps one filter per (connection,table),
1234
+ // so this is only set when every subscriber on the table shares the same filter;
1235
+ // otherwise the table runs unfiltered server-side and each subscriber's filter is
1236
+ // applied client-side in handleEvent (see computeServerFilter).
1237
+ filters = /* @__PURE__ */ new Map();
1238
+ // Ephemeral Broadcast / Presence channel handlers.
1239
+ broadcastHandlers = /* @__PURE__ */ new Map();
1240
+ presenceHandlers = /* @__PURE__ */ new Map();
1241
+ presenceStates = /* @__PURE__ */ new Map();
1153
1242
  reconnectAttempts = 0;
1154
1243
  maxReconnectAttempts = 5;
1155
1244
  reconnectInterval = 3e3;
@@ -1178,6 +1267,12 @@ var RealtimeService = class {
1178
1267
  for (const table of this.subscribers.keys()) {
1179
1268
  this.sendSub("subscribe", table);
1180
1269
  }
1270
+ for (const channel of this.broadcastHandlers.keys()) {
1271
+ this.send({ event: "broadcast_subscribe", channel });
1272
+ }
1273
+ for (const channel of this.presenceHandlers.keys()) {
1274
+ this.send({ event: "presence_subscribe", channel, state: this.presenceStates.get(channel) ?? {} });
1275
+ }
1181
1276
  resolve();
1182
1277
  };
1183
1278
  socket.onmessage = (event) => {
@@ -1213,17 +1308,15 @@ var RealtimeService = class {
1213
1308
  /**
1214
1309
  * Subscribe to changes on a specific table
1215
1310
  */
1216
- async subscribe(table, action, callback) {
1311
+ async subscribe(table, action, callback, filter) {
1217
1312
  await this.connect();
1218
1313
  const isNewTable = !this.subscribers.has(table);
1219
1314
  if (isNewTable) {
1220
1315
  this.subscribers.set(table, /* @__PURE__ */ new Set());
1221
1316
  }
1222
- const sub = { action, callback };
1317
+ const sub = { action, callback, filter: filter && Object.keys(filter).length > 0 ? filter : void 0 };
1223
1318
  this.subscribers.get(table).add(sub);
1224
- if (isNewTable && table !== "*") {
1225
- this.sendSub("subscribe", table);
1226
- }
1319
+ if (table !== "*") this.syncServerFilter(table, isNewTable);
1227
1320
  return {
1228
1321
  unsubscribe: () => {
1229
1322
  const tableSubs = this.subscribers.get(table);
@@ -1231,39 +1324,194 @@ var RealtimeService = class {
1231
1324
  tableSubs.delete(sub);
1232
1325
  if (tableSubs.size === 0) {
1233
1326
  this.subscribers.delete(table);
1327
+ this.filters.delete(table);
1234
1328
  if (table !== "*") this.sendSub("unsubscribe", table);
1329
+ } else if (table !== "*") {
1330
+ this.syncServerFilter(table, false);
1235
1331
  }
1236
1332
  }
1237
1333
  }
1238
1334
  };
1239
1335
  }
1240
- /** Send a subscribe/unsubscribe control frame to the server. */
1336
+ /**
1337
+ * Recompute the effective server-side filter for a table and (re)register it
1338
+ * when it changed (or when force is set, e.g. a brand-new table). The server
1339
+ * stores exactly one filter per (connection, table): a plain `subscribe`
1340
+ * clears it, a filtered `subscribe` replaces it.
1341
+ */
1342
+ syncServerFilter(table, force) {
1343
+ const desired = this.computeServerFilter(table);
1344
+ const current = this.filters.get(table);
1345
+ const changed = JSON.stringify(desired ?? null) !== JSON.stringify(current ?? null);
1346
+ if (!force && !changed) return;
1347
+ if (desired) this.filters.set(table, desired);
1348
+ else this.filters.delete(table);
1349
+ this.sendSub("subscribe", table);
1350
+ }
1351
+ /**
1352
+ * The server-side filter safe to apply for a table: the shared filter iff
1353
+ * EVERY subscriber requested the identical (non-empty) filter. If any
1354
+ * subscriber is unfiltered or filters conflict, returns undefined so the
1355
+ * server delivers all rows and each subscriber filters client-side.
1356
+ */
1357
+ computeServerFilter(table) {
1358
+ const subs = this.subscribers.get(table);
1359
+ if (!subs || subs.size === 0) return void 0;
1360
+ let chosen;
1361
+ for (const sub of subs) {
1362
+ const key = sub.filter && Object.keys(sub.filter).length > 0 ? JSON.stringify(sub.filter) : "";
1363
+ if (key === "") return void 0;
1364
+ if (chosen === void 0) chosen = key;
1365
+ else if (chosen !== key) return void 0;
1366
+ }
1367
+ return chosen ? JSON.parse(chosen) : void 0;
1368
+ }
1369
+ /** Send a subscribe/unsubscribe control frame to the server (with any filter). */
1241
1370
  sendSub(event, table) {
1371
+ const frame = { event, table };
1372
+ if (event === "subscribe") {
1373
+ const filter = this.filters.get(table);
1374
+ if (filter) frame.filter = filter;
1375
+ }
1376
+ this.send(frame);
1377
+ }
1378
+ /** Send an arbitrary control frame if the socket is open. */
1379
+ send(frame) {
1242
1380
  if (this.socket?.readyState === WebSocket.OPEN) {
1243
- this.socket.send(JSON.stringify({ event, table }));
1381
+ this.socket.send(JSON.stringify(frame));
1244
1382
  }
1245
1383
  }
1384
+ /**
1385
+ * Subscribe to an ephemeral Broadcast channel (no DB, no RLS — UI sync/chat).
1386
+ */
1387
+ async subscribeBroadcast(channel, callback) {
1388
+ await this.connect();
1389
+ const isNew = !this.broadcastHandlers.has(channel);
1390
+ if (isNew) this.broadcastHandlers.set(channel, /* @__PURE__ */ new Set());
1391
+ const handlers = this.broadcastHandlers.get(channel);
1392
+ handlers.add(callback);
1393
+ if (isNew) this.send({ event: "broadcast_subscribe", channel });
1394
+ return {
1395
+ unsubscribe: () => {
1396
+ handlers.delete(callback);
1397
+ if (handlers.size === 0) {
1398
+ this.broadcastHandlers.delete(channel);
1399
+ this.send({ event: "broadcast_unsubscribe", channel });
1400
+ }
1401
+ }
1402
+ };
1403
+ }
1404
+ /** Publish a message to a Broadcast channel. */
1405
+ broadcast(channel, payload) {
1406
+ this.send({ event: "broadcast", channel, payload });
1407
+ }
1408
+ /**
1409
+ * Join a Presence channel. The callback receives sync/join/leave events with
1410
+ * the current member list. Returns a leave() handle.
1411
+ */
1412
+ async subscribePresence(channel, state, callback) {
1413
+ await this.connect();
1414
+ const isNew = !this.presenceHandlers.has(channel);
1415
+ if (isNew) this.presenceHandlers.set(channel, /* @__PURE__ */ new Set());
1416
+ this.presenceStates.set(channel, state ?? {});
1417
+ const handlers = this.presenceHandlers.get(channel);
1418
+ handlers.add(callback);
1419
+ if (isNew) this.send({ event: "presence_subscribe", channel, state: state ?? {} });
1420
+ return {
1421
+ leave: () => {
1422
+ handlers.delete(callback);
1423
+ if (handlers.size === 0) {
1424
+ this.presenceHandlers.delete(channel);
1425
+ this.presenceStates.delete(channel);
1426
+ this.send({ event: "presence_unsubscribe", channel });
1427
+ }
1428
+ }
1429
+ };
1430
+ }
1246
1431
  /**
1247
1432
  * Handle incoming CDC events from the server
1248
1433
  */
1249
1434
  handleEvent(payload) {
1250
- const tableSubs = this.subscribers.get(payload.table);
1251
- if (tableSubs) {
1252
- for (const sub of tableSubs) {
1253
- if (sub.action === "*" || sub.action.toLowerCase() === payload.action.toLowerCase()) {
1254
- sub.callback(payload);
1435
+ const kind = payload.type;
1436
+ if (kind === "broadcast") {
1437
+ const handlers = this.broadcastHandlers.get(payload.channel);
1438
+ if (handlers) for (const cb of handlers) cb(payload.payload);
1439
+ return;
1440
+ }
1441
+ if (kind === "presence") {
1442
+ const handlers = this.presenceHandlers.get(payload.channel);
1443
+ if (handlers) for (const cb of handlers) cb(payload);
1444
+ return;
1445
+ }
1446
+ this.dispatchToSubs(this.subscribers.get(payload.table), payload);
1447
+ this.dispatchToSubs(this.subscribers.get("*"), payload);
1448
+ }
1449
+ /**
1450
+ * Fire matching subscribers. Each subscriber's own filter is re-checked
1451
+ * client-side: when subscribers on a table disagree, the server delivers all
1452
+ * rows unfiltered, so client-side filtering is what keeps each callback scoped.
1453
+ */
1454
+ dispatchToSubs(subs, payload) {
1455
+ if (!subs) return;
1456
+ for (const sub of subs) {
1457
+ if (sub.action !== "*" && sub.action.toLowerCase() !== payload.action.toLowerCase()) continue;
1458
+ if (sub.filter && !this.matchFilter(payload.record, sub.filter)) continue;
1459
+ sub.callback(payload);
1460
+ }
1461
+ }
1462
+ /** Evaluate a PostgREST-style filter against a record (mirrors hub_filter.go). */
1463
+ matchFilter(record, filter) {
1464
+ if (record == null) return false;
1465
+ for (const col of Object.keys(filter)) {
1466
+ const spec = filter[col];
1467
+ let op = "eq";
1468
+ let val = spec;
1469
+ const i = spec.indexOf(".");
1470
+ if (i > 0) {
1471
+ const cand = spec.slice(0, i);
1472
+ if (["eq", "neq", "gt", "gte", "lt", "lte", "in", "like"].includes(cand)) {
1473
+ op = cand;
1474
+ val = spec.slice(i + 1);
1255
1475
  }
1256
1476
  }
1477
+ const actualRaw = record[col];
1478
+ if (actualRaw === void 0) return false;
1479
+ const actual = actualRaw === null ? "" : String(actualRaw);
1480
+ if (!this.evalPred(op, actual, val)) return false;
1257
1481
  }
1258
- const wildcardSubs = this.subscribers.get("*");
1259
- if (wildcardSubs) {
1260
- for (const sub of wildcardSubs) {
1261
- if (sub.action === "*" || sub.action.toLowerCase() === payload.action.toLowerCase()) {
1262
- sub.callback(payload);
1263
- }
1482
+ return true;
1483
+ }
1484
+ evalPred(op, actual, val) {
1485
+ switch (op) {
1486
+ case "eq":
1487
+ return actual === val;
1488
+ case "neq":
1489
+ return actual !== val;
1490
+ case "in":
1491
+ return val.split(",").includes(actual);
1492
+ case "like":
1493
+ return this.likeMatch(actual, val);
1494
+ case "gt":
1495
+ case "gte":
1496
+ case "lt":
1497
+ case "lte": {
1498
+ const a = parseFloat(actual);
1499
+ const b = parseFloat(val);
1500
+ if (Number.isNaN(a) || Number.isNaN(b)) return false;
1501
+ if (op === "gt") return a > b;
1502
+ if (op === "gte") return a >= b;
1503
+ if (op === "lt") return a < b;
1504
+ return a <= b;
1264
1505
  }
1506
+ default:
1507
+ return false;
1265
1508
  }
1266
1509
  }
1510
+ /** SQL LIKE with '%' (any run) and '_' (any single char), anchored, case-sensitive. */
1511
+ likeMatch(s, pattern) {
1512
+ const re = "^" + pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/%/g, ".*").replace(/_/g, ".") + "$";
1513
+ return new RegExp(re).test(s);
1514
+ }
1267
1515
  /**
1268
1516
  * Attempt to reconnect on connection loss
1269
1517
  */
@@ -1301,8 +1549,17 @@ function createRealtimeModule(client) {
1301
1549
  typeof WebSocket !== "undefined" ? WebSocket : void 0
1302
1550
  );
1303
1551
  return {
1304
- async subscribe(table, action, callback) {
1305
- return service.subscribe(table, action, callback);
1552
+ async subscribe(table, action, callback, filter) {
1553
+ return service.subscribe(table, action, callback, filter);
1554
+ },
1555
+ async subscribeBroadcast(channel, callback) {
1556
+ return service.subscribeBroadcast(channel, callback);
1557
+ },
1558
+ broadcast(channel, payload) {
1559
+ service.broadcast(channel, payload);
1560
+ },
1561
+ async subscribePresence(channel, state, callback) {
1562
+ return service.subscribePresence(channel, state, callback);
1306
1563
  }
1307
1564
  };
1308
1565
  }
@@ -1325,6 +1582,9 @@ function createApiKeysModule(client) {
1325
1582
  async delete(keyId) {
1326
1583
  return del(`/api/api-keys/${keyId}`);
1327
1584
  },
1585
+ async setBrand(keyId, emailConfigId) {
1586
+ return post(`/api/api-keys/${keyId}/brand`, { email_config_id: emailConfigId });
1587
+ },
1328
1588
  async getInstanceToken() {
1329
1589
  return get("/api/api-keys/instance");
1330
1590
  },
@@ -1643,8 +1903,8 @@ var BaasClient = class extends HttpClient {
1643
1903
  async deleteStorageFile(fileId) {
1644
1904
  return this.storage.deleteFile(fileId);
1645
1905
  }
1646
- async createStorageBucket(name, isPublic) {
1647
- return this.storage.createBucket(name, isPublic);
1906
+ async createStorageBucket(name, isPublic, maxBytes) {
1907
+ return this.storage.createBucket(name, isPublic, maxBytes);
1648
1908
  }
1649
1909
  async listStorageBuckets() {
1650
1910
  return this.storage.listBuckets();
@@ -1675,6 +1935,9 @@ var BaasClient = class extends HttpClient {
1675
1935
  async deleteApiKey(keyId) {
1676
1936
  return this.apiKeys.delete(keyId);
1677
1937
  }
1938
+ async setApiKeyBrand(keyId, emailConfigId) {
1939
+ return this.apiKeys.setBrand(keyId, emailConfigId);
1940
+ }
1678
1941
  async getInstanceToken() {
1679
1942
  return this.apiKeys.getInstanceToken();
1680
1943
  }
@@ -1749,9 +2012,18 @@ var BaasClient = class extends HttpClient {
1749
2012
  async getEmailConfig() {
1750
2013
  return this.email.getConfig();
1751
2014
  }
2015
+ async listEmailConfigs() {
2016
+ return this.email.listConfigs();
2017
+ }
1752
2018
  async saveEmailConfig(config) {
1753
2019
  return this.email.saveConfig(config);
1754
2020
  }
2021
+ async setDefaultEmailConfig(id) {
2022
+ return this.email.setDefaultConfig(id);
2023
+ }
2024
+ async deleteEmailConfig(id) {
2025
+ return this.email.deleteConfig(id);
2026
+ }
1755
2027
  async createEmailTemplate(template) {
1756
2028
  return this.email.createTemplate(template);
1757
2029
  }
@@ -1900,9 +2172,12 @@ var BaasClient = class extends HttpClient {
1900
2172
  async purgeAuditLogs(olderThanDays) {
1901
2173
  return this.audit.purge(olderThanDays);
1902
2174
  }
2175
+ async clearAllAuditLogs() {
2176
+ return this.audit.clearAll();
2177
+ }
1903
2178
  // Realtime shortcuts
1904
- subscribe(table, action, callback) {
1905
- return this.realtime.subscribe(table, action, callback);
2179
+ subscribe(table, action, callback, filter) {
2180
+ return this.realtime.subscribe(table, action, callback, filter);
1906
2181
  }
1907
2182
  // Environment shortcuts
1908
2183
  async getEnvironmentStatus() {