@giaeulate/baas-sdk 1.2.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
@@ -2,14 +2,31 @@
2
2
  var HttpClient = class {
3
3
  url;
4
4
  apiKey = "";
5
+ keyType;
5
6
  token = null;
7
+ refreshToken = null;
6
8
  environment = "prod";
7
9
  _onForceLogout = null;
8
- constructor(url, apiKey) {
10
+ _refreshing = null;
11
+ constructor(url, apiKey, options) {
9
12
  let baseUrl = url || (typeof window !== "undefined" ? window.location.origin : "http://localhost:8080");
10
13
  this.url = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
11
14
  if (apiKey) this.apiKey = apiKey;
12
- this.token = this.cleanValue(localStorage.getItem("baas_token"));
15
+ this.keyType = options?.keyType ?? "service_role";
16
+ this.warnIfUnsafeKey();
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;
19
+ }
20
+ /** SECURITY: a service_role key bypasses RLS and grants admin over every
21
+ * tenant. If it ends up in a browser bundle, anyone can extract it. Warn
22
+ * loudly when a non-anon key is constructed in a browser context. */
23
+ warnIfUnsafeKey() {
24
+ const inBrowser = typeof window !== "undefined" && typeof document !== "undefined";
25
+ if (inBrowser && this.apiKey && this.keyType !== "anon") {
26
+ console.warn(
27
+ '[baas-sdk] SECURITY WARNING: a non-anon (service_role) key is running in a browser. It bypasses Row-Level Security and grants admin over ALL tenants, and is extractable from the bundle. Mint an anon key in the dashboard and construct the client with { keyType: "anon" }.'
28
+ );
29
+ }
13
30
  }
14
31
  /**
15
32
  * Set the auth token manually (e.g. restoring from SecureStore in React Native).
@@ -17,12 +34,59 @@ var HttpClient = class {
17
34
  */
18
35
  setToken(token) {
19
36
  this.token = token;
37
+ if (typeof localStorage === "undefined") return;
20
38
  if (token) {
21
39
  localStorage.setItem("baas_token", token);
22
40
  } else {
23
41
  localStorage.removeItem("baas_token");
24
42
  }
25
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
+ }
26
90
  /**
27
91
  * Register a callback invoked on forced logout (401).
28
92
  * Use this in React Native instead of the default window.location redirect.
@@ -63,7 +127,7 @@ var HttpClient = class {
63
127
  /**
64
128
  * Core HTTP request method - DRY principle
65
129
  */
66
- async request(endpoint, options = {}) {
130
+ async request(endpoint, options = {}, _isRetry = false) {
67
131
  const { method = "GET", body, headers: customHeaders, skipAuth = false } = options;
68
132
  const headers = { ...this.getHeaders(), ...customHeaders };
69
133
  const fetchOptions = {
@@ -81,12 +145,22 @@ var HttpClient = class {
81
145
  const data = await res.json().catch(() => null);
82
146
  if (!res.ok) {
83
147
  if (res.status === 401 && !skipAuth) {
148
+ if (!_isRetry && await this.tryRefresh()) {
149
+ return this.request(endpoint, options, true);
150
+ }
84
151
  this.forceLogout();
85
152
  }
86
153
  if (res.status === 403 && data?.error === "ip_not_allowed") {
87
154
  this.handleIPBlocked(data.ip);
88
155
  }
89
- return { error: data?.error || `Request failed: ${res.status}`, ...data };
156
+ const rateLimited = res.status === 429;
157
+ const retryHint = res.headers.get("retry-after") || res.headers.get("x-ratelimit-reset");
158
+ return {
159
+ error: data?.error || `Request failed: ${res.status}`,
160
+ status: res.status,
161
+ ...rateLimited ? { rateLimited: true, retryAfter: retryHint ? parseInt(retryHint, 10) : void 0 } : {},
162
+ ...data
163
+ };
90
164
  }
91
165
  return data;
92
166
  }
@@ -118,7 +192,11 @@ var HttpClient = class {
118
192
  }
119
193
  logout() {
120
194
  this.token = null;
121
- 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
+ }
122
200
  }
123
201
  forceLogout() {
124
202
  this.logout();
@@ -203,10 +281,49 @@ var QueryBuilder = class {
203
281
  this.queryParams.set(column, `not.${operator}.${value}`);
204
282
  return this;
205
283
  }
284
+ /** Array/jsonb/range contains (@>). Arrays become a `{a,b}` literal. */
285
+ contains(column, values) {
286
+ this.queryParams.set(column, `cs.${this.arrayLiteral(values)}`);
287
+ return this;
288
+ }
289
+ /** Array/jsonb/range contained-by (<@). */
290
+ containedBy(column, values) {
291
+ this.queryParams.set(column, `cd.${this.arrayLiteral(values)}`);
292
+ return this;
293
+ }
294
+ /** Array/range overlap (&&). */
295
+ overlaps(column, values) {
296
+ this.queryParams.set(column, `ov.${this.arrayLiteral(values)}`);
297
+ return this;
298
+ }
299
+ /** Full-text search (@@). type: fts|plfts|phfts|wfts (tsquery flavour). */
300
+ textSearch(column, query, type = "fts") {
301
+ this.queryParams.set(column, `${type}.${query}`);
302
+ return this;
303
+ }
304
+ arrayLiteral(values) {
305
+ return Array.isArray(values) ? `{${values.join(",")}}` : `${values}`;
306
+ }
307
+ /** OR group: `or=(c1,c2)`. Members are dotted `col.op.value` strings. */
206
308
  or(filters) {
207
309
  this.queryParams.set("or", `(${filters})`);
208
310
  return this;
209
311
  }
312
+ /** AND group: `and=(c1,c2)` — e.g. a range `qty.gt.5,qty.lt.20`. */
313
+ and(filters) {
314
+ this.queryParams.set("and", `(${filters})`);
315
+ return this;
316
+ }
317
+ /** Negated AND group: `not.and=(...)`. */
318
+ notAnd(filters) {
319
+ this.queryParams.set("not.and", `(${filters})`);
320
+ return this;
321
+ }
322
+ /** Negated OR group: `not.or=(...)`. */
323
+ notOr(filters) {
324
+ this.queryParams.set("not.or", `(${filters})`);
325
+ return this;
326
+ }
210
327
  order(column, { ascending = true } = {}) {
211
328
  this.queryParams.set("order", `${column}.${ascending ? "asc" : "desc"}`);
212
329
  return this;
@@ -250,6 +367,34 @@ var QueryBuilder = class {
250
367
  if (res.status === 204) return { success: true };
251
368
  return await this.handleResponse(res);
252
369
  }
370
+ /** Insert-or-update on conflict. [onConflict] is the conflict-target column(s). */
371
+ async upsert(data, onConflict) {
372
+ const res = await fetch(`${this.url}/api/v1/data/${this.table}/upsert`, {
373
+ method: "POST",
374
+ headers: this.getHeaders(),
375
+ body: JSON.stringify({ ...data, on_conflict: onConflict })
376
+ });
377
+ return await this.handleResponse(res);
378
+ }
379
+ /**
380
+ * Count rows matching the current filters (PostgREST parity). Sends
381
+ * `Prefer: count=exact` and parses the total from the `Content-Range`
382
+ * response header. Returns `{ count, error, status }`.
383
+ */
384
+ async count() {
385
+ const res = await fetch(`${this.url}/api/v1/data/${this.table}?${this.queryParams.toString()}`, {
386
+ headers: { ...this.getHeaders(), Prefer: "count=exact", "Cache-Control": "no-cache" },
387
+ cache: "no-store"
388
+ });
389
+ if (!res.ok) {
390
+ if (res.status === 401) this.client.forceLogout();
391
+ const body = await res.json().catch(() => null);
392
+ return { count: 0, error: body?.error || "Request failed", status: res.status };
393
+ }
394
+ const cr = res.headers.get("content-range");
395
+ const total = cr && cr.includes("/") ? parseInt(cr.split("/").pop() || "", 10) : NaN;
396
+ return { count: Number.isNaN(total) ? 0 : total, error: null, status: res.status };
397
+ }
253
398
  getHeaders() {
254
399
  return this.client.getHeaders();
255
400
  }
@@ -292,18 +437,65 @@ function createAuthModule(client) {
292
437
  throw new Error(data?.error || `Login failed: ${res.status}`);
293
438
  }
294
439
  if (data.token) {
295
- client.token = data.token;
296
- localStorage.setItem("baas_token", data.token);
440
+ client.setToken(data.token);
441
+ }
442
+ if (data.refresh_token) {
443
+ client.setRefreshToken(data.refresh_token);
297
444
  }
298
445
  return data;
299
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);
454
+ return data;
455
+ },
300
456
  async logout() {
301
457
  try {
302
458
  await post("/api/logout");
303
459
  } finally {
304
- client.token = null;
305
- localStorage.removeItem("baas_token");
460
+ client.logout();
461
+ }
462
+ },
463
+ // Anonymous sign-in (Supabase parity). Response carries access_token (+ a
464
+ // back-compat `token`); store whichever is present.
465
+ async signInAnonymous() {
466
+ const data = await post("/api/auth/anonymous");
467
+ const tok = data?.access_token || data?.token;
468
+ if (tok) {
469
+ client.token = tok;
470
+ localStorage.setItem("baas_token", tok);
471
+ }
472
+ return data;
473
+ },
474
+ // Passwordless: request a magic link + 6-digit email OTP. Always resolves on
475
+ // a well-formed request (no account enumeration).
476
+ async requestOtp(email, createUser = true) {
477
+ return post("/api/auth/otp", { email, create_user: createUser });
478
+ },
479
+ // Verify a passwordless OTP/magic-link and complete sign-in.
480
+ async verifyOtp(type, email, token) {
481
+ const data = await post("/api/auth/verify", { type, email, token });
482
+ const tok = data?.access_token || data?.token;
483
+ if (tok) {
484
+ client.token = tok;
485
+ localStorage.setItem("baas_token", tok);
486
+ }
487
+ return data;
488
+ },
489
+ // Upgrade the current anonymous guest to a real account (preserves user id).
490
+ // Requires the anonymous Bearer to be set (call after signInAnonymous).
491
+ async upgradeAnonymous(email, password) {
492
+ const data = await post("/api/auth/upgrade", { email, password });
493
+ const tok = data?.access_token || data?.token;
494
+ if (tok) {
495
+ client.token = tok;
496
+ localStorage.setItem("baas_token", tok);
306
497
  }
498
+ return data;
307
499
  },
308
500
  async verifyMFA(mfaToken, code) {
309
501
  const data = await post("/api/mfa/finalize", { mfa_token: mfaToken, code });
@@ -502,6 +694,7 @@ function createDatabaseModule(client) {
502
694
  function createStorageModule(client) {
503
695
  const get = (endpoint) => client.get(endpoint);
504
696
  const post = (endpoint, body) => client.post(endpoint, body);
697
+ const put = (endpoint, body) => client.put(endpoint, body);
505
698
  const del = (endpoint) => client.delete(endpoint);
506
699
  async function uploadFile(fileOrTable, fileOrBucketId) {
507
700
  let file;
@@ -536,14 +729,24 @@ function createStorageModule(client) {
536
729
  async listFiles() {
537
730
  return get("/api/storage/files");
538
731
  },
539
- async getFile(fileId) {
540
- return get(`/api/storage/files/${fileId}`);
732
+ async getFile(fileId, expiresIn) {
733
+ const q = expiresIn && expiresIn > 0 ? `?expiresIn=${expiresIn}` : "";
734
+ return get(`/api/storage/files/${fileId}${q}`);
735
+ },
736
+ async createSignedUrl(fileId, expiresIn) {
737
+ const res = await get(
738
+ `/api/storage/files/${fileId}${expiresIn && expiresIn > 0 ? `?expiresIn=${expiresIn}` : ""}`
739
+ );
740
+ return { signedUrl: res?.url };
541
741
  },
542
742
  async deleteFile(fileId) {
543
743
  return del(`/api/storage/files/${fileId}`);
544
744
  },
545
- async createBucket(name, isPublic = false) {
546
- 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 });
747
+ },
748
+ async updateBucket(bucketId, opts) {
749
+ return put(`/api/storage/buckets/${bucketId}`, { is_public: opts.isPublic, max_bytes: opts.maxBytes });
547
750
  },
548
751
  async listBuckets() {
549
752
  return get("/api/storage/buckets");
@@ -556,6 +759,17 @@ function createStorageModule(client) {
556
759
  },
557
760
  async listBucketFiles(bucketId) {
558
761
  return get(`/api/storage/buckets/${bucketId}/files`);
762
+ },
763
+ async download(fileId) {
764
+ const res = await fetch(`${client.url}/api/storage/files/${fileId}/download`, {
765
+ headers: client.getHeaders("")
766
+ });
767
+ if (!res.ok) {
768
+ if (res.status === 401) client.forceLogout();
769
+ const err = await res.json().catch(() => ({ error: `Download failed: ${res.status}` }));
770
+ throw new Error(err.error ?? `Download failed: ${res.status}`);
771
+ }
772
+ return res.blob();
559
773
  }
560
774
  };
561
775
  }
@@ -656,7 +870,9 @@ function createFunctionsModule(client) {
656
870
  return del(`/api/functions/${id}`);
657
871
  },
658
872
  async update(id, code, options) {
659
- return put(`/api/functions/${id}`, { code, ...options });
873
+ const current = await get(`/api/functions/${id}`);
874
+ const name = current?.name ?? current?.data?.name;
875
+ return post("/api/functions", { name, code, ...options });
660
876
  },
661
877
  async executeCode(code, options) {
662
878
  return post("/api/functions/execute", { code, ...options });
@@ -747,9 +963,18 @@ function createEmailModule(client) {
747
963
  async getConfig() {
748
964
  return get("/api/email/config");
749
965
  },
966
+ async listConfigs() {
967
+ return get("/api/email/configs");
968
+ },
750
969
  async saveConfig(config) {
751
970
  return post("/api/email/config", config);
752
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
+ },
753
978
  async createTemplate(template) {
754
979
  return post("/api/email/templates", template);
755
980
  },
@@ -802,7 +1027,9 @@ function createGraphQLModule(client) {
802
1027
  return post("/api/graphql", { query, variables });
803
1028
  },
804
1029
  async getSchema() {
805
- return get("/api/graphql/schema");
1030
+ return post("/api/graphql", {
1031
+ query: "{ __schema { queryType { name } mutationType { name } types { name kind } } }"
1032
+ });
806
1033
  },
807
1034
  getPlaygroundUrl() {
808
1035
  return `${client.url}/api/graphql/playground`;
@@ -848,6 +1075,7 @@ function createMetricsModule(client) {
848
1075
  // src/modules/audit.ts
849
1076
  function createAuditModule(client) {
850
1077
  const get = (endpoint) => client.get(endpoint);
1078
+ const del = (endpoint) => client.delete(endpoint);
851
1079
  return {
852
1080
  async list(options) {
853
1081
  const params = new URLSearchParams();
@@ -864,6 +1092,43 @@ function createAuditModule(client) {
864
1092
  },
865
1093
  async getActions() {
866
1094
  return get("/api/audit/actions");
1095
+ },
1096
+ async exportLogs(format, options) {
1097
+ const params = new URLSearchParams();
1098
+ params.set("format", format);
1099
+ if (options?.action) params.set("action", options.action);
1100
+ if (options?.method) params.set("method", options.method);
1101
+ if (options?.path) params.set("path", options.path);
1102
+ if (options?.status_code) params.set("status_code", options.status_code.toString());
1103
+ if (options?.start_date) params.set("start_date", options.start_date);
1104
+ if (options?.end_date) params.set("end_date", options.end_date);
1105
+ const res = await fetch(`${client.url}/api/audit/export?${params.toString()}`, {
1106
+ headers: client.getHeaders(),
1107
+ credentials: "include"
1108
+ });
1109
+ if (!res.ok) {
1110
+ const data = await res.json();
1111
+ return { error: data.error || `Export failed: ${res.status}` };
1112
+ }
1113
+ const blob = await res.blob();
1114
+ const url = window.URL.createObjectURL(blob);
1115
+ const a = document.createElement("a");
1116
+ a.href = url;
1117
+ a.download = `audit_logs_${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.${format}`;
1118
+ document.body.appendChild(a);
1119
+ a.click();
1120
+ window.URL.revokeObjectURL(url);
1121
+ document.body.removeChild(a);
1122
+ return { success: true };
1123
+ },
1124
+ async purgePreview(olderThanDays) {
1125
+ return get(`/api/audit/purge/preview?older_than_days=${olderThanDays}`);
1126
+ },
1127
+ async purge(olderThanDays) {
1128
+ return del(`/api/audit/purge?older_than_days=${olderThanDays}`);
1129
+ },
1130
+ async clearAll() {
1131
+ return del(`/api/audit/clear`);
867
1132
  }
868
1133
  };
869
1134
  }
@@ -964,6 +1229,16 @@ var RealtimeService = class {
964
1229
  }
965
1230
  socket = null;
966
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();
967
1242
  reconnectAttempts = 0;
968
1243
  maxReconnectAttempts = 5;
969
1244
  reconnectInterval = 3e3;
@@ -989,6 +1264,15 @@ var RealtimeService = class {
989
1264
  socket.onopen = () => {
990
1265
  console.log("BaaS Realtime: Connected");
991
1266
  this.reconnectAttempts = 0;
1267
+ for (const table of this.subscribers.keys()) {
1268
+ this.sendSub("subscribe", table);
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
+ }
992
1276
  resolve();
993
1277
  };
994
1278
  socket.onmessage = (event) => {
@@ -1024,13 +1308,15 @@ var RealtimeService = class {
1024
1308
  /**
1025
1309
  * Subscribe to changes on a specific table
1026
1310
  */
1027
- async subscribe(table, action, callback) {
1311
+ async subscribe(table, action, callback, filter) {
1028
1312
  await this.connect();
1029
- if (!this.subscribers.has(table)) {
1313
+ const isNewTable = !this.subscribers.has(table);
1314
+ if (isNewTable) {
1030
1315
  this.subscribers.set(table, /* @__PURE__ */ new Set());
1031
1316
  }
1032
- const sub = { action, callback };
1317
+ const sub = { action, callback, filter: filter && Object.keys(filter).length > 0 ? filter : void 0 };
1033
1318
  this.subscribers.get(table).add(sub);
1319
+ if (table !== "*") this.syncServerFilter(table, isNewTable);
1034
1320
  return {
1035
1321
  unsubscribe: () => {
1036
1322
  const tableSubs = this.subscribers.get(table);
@@ -1038,32 +1324,194 @@ var RealtimeService = class {
1038
1324
  tableSubs.delete(sub);
1039
1325
  if (tableSubs.size === 0) {
1040
1326
  this.subscribers.delete(table);
1327
+ this.filters.delete(table);
1328
+ if (table !== "*") this.sendSub("unsubscribe", table);
1329
+ } else if (table !== "*") {
1330
+ this.syncServerFilter(table, false);
1041
1331
  }
1042
1332
  }
1043
1333
  }
1044
1334
  };
1045
1335
  }
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). */
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) {
1380
+ if (this.socket?.readyState === WebSocket.OPEN) {
1381
+ this.socket.send(JSON.stringify(frame));
1382
+ }
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
+ }
1046
1431
  /**
1047
1432
  * Handle incoming CDC events from the server
1048
1433
  */
1049
1434
  handleEvent(payload) {
1050
- const tableSubs = this.subscribers.get(payload.table);
1051
- if (tableSubs) {
1052
- for (const sub of tableSubs) {
1053
- if (sub.action === "*" || sub.action.toLowerCase() === payload.action.toLowerCase()) {
1054
- 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);
1055
1475
  }
1056
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;
1057
1481
  }
1058
- const wildcardSubs = this.subscribers.get("*");
1059
- if (wildcardSubs) {
1060
- for (const sub of wildcardSubs) {
1061
- if (sub.action === "*" || sub.action.toLowerCase() === payload.action.toLowerCase()) {
1062
- sub.callback(payload);
1063
- }
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;
1064
1505
  }
1506
+ default:
1507
+ return false;
1065
1508
  }
1066
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
+ }
1067
1515
  /**
1068
1516
  * Attempt to reconnect on connection loss
1069
1517
  */
@@ -1101,8 +1549,17 @@ function createRealtimeModule(client) {
1101
1549
  typeof WebSocket !== "undefined" ? WebSocket : void 0
1102
1550
  );
1103
1551
  return {
1104
- async subscribe(table, action, callback) {
1105
- 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);
1106
1563
  }
1107
1564
  };
1108
1565
  }
@@ -1125,6 +1582,9 @@ function createApiKeysModule(client) {
1125
1582
  async delete(keyId) {
1126
1583
  return del(`/api/api-keys/${keyId}`);
1127
1584
  },
1585
+ async setBrand(keyId, emailConfigId) {
1586
+ return post(`/api/api-keys/${keyId}/brand`, { email_config_id: emailConfigId });
1587
+ },
1128
1588
  async getInstanceToken() {
1129
1589
  return get("/api/api-keys/instance");
1130
1590
  },
@@ -1214,6 +1674,44 @@ function createIPWhitelistModule(client) {
1214
1674
  };
1215
1675
  }
1216
1676
 
1677
+ // src/modules/cache.ts
1678
+ function createCacheModule(client) {
1679
+ const get = (endpoint) => client.get(endpoint);
1680
+ const post = (endpoint, body) => client.post(endpoint, body);
1681
+ const patch = (endpoint, body) => client.patch(endpoint, body);
1682
+ const del = (endpoint) => client.delete(endpoint);
1683
+ return {
1684
+ async getStatus() {
1685
+ return get("/api/cache/status");
1686
+ },
1687
+ async getStats() {
1688
+ return get("/api/cache/stats");
1689
+ },
1690
+ async listPolicies() {
1691
+ return get("/api/cache/policies");
1692
+ },
1693
+ async updatePolicy(namespace, body) {
1694
+ return patch(`/api/cache/policies/${encodeURIComponent(namespace)}`, body);
1695
+ },
1696
+ async removePolicy(namespace) {
1697
+ return del(`/api/cache/policies/${encodeURIComponent(namespace)}`);
1698
+ },
1699
+ async invalidateNamespace(namespace) {
1700
+ return post(`/api/cache/invalidate/${encodeURIComponent(namespace)}`);
1701
+ },
1702
+ async invalidateAll() {
1703
+ return post("/api/cache/invalidate-all");
1704
+ },
1705
+ async listKeys(prefix, cursor = 0, count = 100) {
1706
+ const params = new URLSearchParams({ prefix, cursor: String(cursor), count: String(count) });
1707
+ return get(`/api/cache/keys?${params}`);
1708
+ },
1709
+ async inspectKey(key) {
1710
+ return get(`/api/cache/keys/inspect?key=${encodeURIComponent(key)}`);
1711
+ }
1712
+ };
1713
+ }
1714
+
1217
1715
  // src/client.ts
1218
1716
  var BaasClient = class extends HttpClient {
1219
1717
  // Feature modules
@@ -1240,8 +1738,9 @@ var BaasClient = class extends HttpClient {
1240
1738
  corsOrigins;
1241
1739
  policies;
1242
1740
  ipWhitelist;
1243
- constructor(url, apiKey) {
1244
- super(url, apiKey);
1741
+ cache;
1742
+ constructor(url, apiKey, options) {
1743
+ super(url, apiKey, options);
1245
1744
  this.auth = createAuthModule(this);
1246
1745
  this.users = createUsersModule(this);
1247
1746
  this.database = createDatabaseModule(this);
@@ -1265,6 +1764,7 @@ var BaasClient = class extends HttpClient {
1265
1764
  this.corsOrigins = createCorsOriginsModule(this);
1266
1765
  this.policies = createPoliciesModule(this);
1267
1766
  this.ipWhitelist = createIPWhitelistModule(this);
1767
+ this.cache = createCacheModule(this);
1268
1768
  }
1269
1769
  /**
1270
1770
  * Create a query builder for fluent data queries
@@ -1272,6 +1772,18 @@ var BaasClient = class extends HttpClient {
1272
1772
  from(table) {
1273
1773
  return new QueryBuilder(table, this.url, this);
1274
1774
  }
1775
+ /**
1776
+ * Invoke a PL/pgSQL function (PostgREST `.rpc()` parity). Runs under the
1777
+ * caller's RLS context; args bind by name and are $N-safe. Pass [params] for
1778
+ * POST (named args in the body) or set `opts.get = true` for the GET variant.
1779
+ */
1780
+ async rpc(fn, params, opts) {
1781
+ if (opts?.get) {
1782
+ const qs = params ? "?" + new URLSearchParams(Object.entries(params).map(([k, v]) => [k, String(v)])).toString() : "";
1783
+ return this.get(`/api/v1/rpc/${fn}${qs}`);
1784
+ }
1785
+ return this.post(`/api/v1/rpc/${fn}`, params ?? {});
1786
+ }
1275
1787
  // ============================================
1276
1788
  // BACKWARD COMPATIBILITY METHODS
1277
1789
  // These delegate to the appropriate modules
@@ -1391,8 +1903,8 @@ var BaasClient = class extends HttpClient {
1391
1903
  async deleteStorageFile(fileId) {
1392
1904
  return this.storage.deleteFile(fileId);
1393
1905
  }
1394
- async createStorageBucket(name, isPublic) {
1395
- return this.storage.createBucket(name, isPublic);
1906
+ async createStorageBucket(name, isPublic, maxBytes) {
1907
+ return this.storage.createBucket(name, isPublic, maxBytes);
1396
1908
  }
1397
1909
  async listStorageBuckets() {
1398
1910
  return this.storage.listBuckets();
@@ -1423,6 +1935,9 @@ var BaasClient = class extends HttpClient {
1423
1935
  async deleteApiKey(keyId) {
1424
1936
  return this.apiKeys.delete(keyId);
1425
1937
  }
1938
+ async setApiKeyBrand(keyId, emailConfigId) {
1939
+ return this.apiKeys.setBrand(keyId, emailConfigId);
1940
+ }
1426
1941
  async getInstanceToken() {
1427
1942
  return this.apiKeys.getInstanceToken();
1428
1943
  }
@@ -1497,9 +2012,18 @@ var BaasClient = class extends HttpClient {
1497
2012
  async getEmailConfig() {
1498
2013
  return this.email.getConfig();
1499
2014
  }
2015
+ async listEmailConfigs() {
2016
+ return this.email.listConfigs();
2017
+ }
1500
2018
  async saveEmailConfig(config) {
1501
2019
  return this.email.saveConfig(config);
1502
2020
  }
2021
+ async setDefaultEmailConfig(id) {
2022
+ return this.email.setDefaultConfig(id);
2023
+ }
2024
+ async deleteEmailConfig(id) {
2025
+ return this.email.deleteConfig(id);
2026
+ }
1503
2027
  async createEmailTemplate(template) {
1504
2028
  return this.email.createTemplate(template);
1505
2029
  }
@@ -1639,9 +2163,21 @@ var BaasClient = class extends HttpClient {
1639
2163
  async getAuditActions() {
1640
2164
  return this.audit.getActions();
1641
2165
  }
2166
+ async exportAuditLogs(format, options) {
2167
+ return this.audit.exportLogs(format, options);
2168
+ }
2169
+ async previewPurgeAuditLogs(olderThanDays) {
2170
+ return this.audit.purgePreview(olderThanDays);
2171
+ }
2172
+ async purgeAuditLogs(olderThanDays) {
2173
+ return this.audit.purge(olderThanDays);
2174
+ }
2175
+ async clearAllAuditLogs() {
2176
+ return this.audit.clearAll();
2177
+ }
1642
2178
  // Realtime shortcuts
1643
- subscribe(table, action, callback) {
1644
- return this.realtime.subscribe(table, action, callback);
2179
+ subscribe(table, action, callback, filter) {
2180
+ return this.realtime.subscribe(table, action, callback, filter);
1645
2181
  }
1646
2182
  // Environment shortcuts
1647
2183
  async getEnvironmentStatus() {
@@ -1692,3 +2228,4 @@ export {
1692
2228
  HttpClient,
1693
2229
  QueryBuilder
1694
2230
  };
2231
+ //# sourceMappingURL=index.js.map