@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.cjs CHANGED
@@ -30,14 +30,31 @@ module.exports = __toCommonJS(index_exports);
30
30
  var HttpClient = class {
31
31
  url;
32
32
  apiKey = "";
33
+ keyType;
33
34
  token = null;
35
+ refreshToken = null;
34
36
  environment = "prod";
35
37
  _onForceLogout = null;
36
- constructor(url, apiKey) {
38
+ _refreshing = null;
39
+ constructor(url, apiKey, options) {
37
40
  let baseUrl = url || (typeof window !== "undefined" ? window.location.origin : "http://localhost:8080");
38
41
  this.url = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
39
42
  if (apiKey) this.apiKey = apiKey;
40
- this.token = this.cleanValue(localStorage.getItem("baas_token"));
43
+ this.keyType = options?.keyType ?? "service_role";
44
+ this.warnIfUnsafeKey();
45
+ this.token = typeof localStorage !== "undefined" ? this.cleanValue(localStorage.getItem("baas_token")) : null;
46
+ this.refreshToken = typeof localStorage !== "undefined" ? this.cleanValue(localStorage.getItem("baas_refresh_token")) : null;
47
+ }
48
+ /** SECURITY: a service_role key bypasses RLS and grants admin over every
49
+ * tenant. If it ends up in a browser bundle, anyone can extract it. Warn
50
+ * loudly when a non-anon key is constructed in a browser context. */
51
+ warnIfUnsafeKey() {
52
+ const inBrowser = typeof window !== "undefined" && typeof document !== "undefined";
53
+ if (inBrowser && this.apiKey && this.keyType !== "anon") {
54
+ console.warn(
55
+ '[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" }.'
56
+ );
57
+ }
41
58
  }
42
59
  /**
43
60
  * Set the auth token manually (e.g. restoring from SecureStore in React Native).
@@ -45,12 +62,59 @@ var HttpClient = class {
45
62
  */
46
63
  setToken(token) {
47
64
  this.token = token;
65
+ if (typeof localStorage === "undefined") return;
48
66
  if (token) {
49
67
  localStorage.setItem("baas_token", token);
50
68
  } else {
51
69
  localStorage.removeItem("baas_token");
52
70
  }
53
71
  }
72
+ /** Persist the rotating refresh token (used by auto-refresh on 401). In the
73
+ * browser the server also sets an HttpOnly `baas_refresh` cookie, so storing
74
+ * it here is mainly for non-cookie clients (React Native). */
75
+ setRefreshToken(token) {
76
+ this.refreshToken = token;
77
+ if (typeof localStorage === "undefined") return;
78
+ if (token) {
79
+ localStorage.setItem("baas_refresh_token", token);
80
+ } else {
81
+ localStorage.removeItem("baas_refresh_token");
82
+ }
83
+ }
84
+ getRefreshToken() {
85
+ const ls = typeof localStorage !== "undefined" ? localStorage.getItem("baas_refresh_token") : null;
86
+ return this.cleanValue(this.refreshToken || ls);
87
+ }
88
+ /** Attempts a single token refresh, deduped across concurrent 401s. Sends the
89
+ * stored refresh token (falls back to the HttpOnly cookie in browsers). */
90
+ tryRefresh() {
91
+ if (!this._refreshing) {
92
+ this._refreshing = this.doRefresh().finally(() => {
93
+ this._refreshing = null;
94
+ });
95
+ }
96
+ return this._refreshing;
97
+ }
98
+ async doRefresh() {
99
+ try {
100
+ const body = {};
101
+ const rt = this.getRefreshToken();
102
+ if (rt) body.refresh_token = rt;
103
+ const res = await fetch(`${this.url}/api/auth/refresh`, {
104
+ method: "POST",
105
+ headers: { "Content-Type": "application/json", apikey: this.apiKey },
106
+ credentials: "include",
107
+ body: JSON.stringify(body)
108
+ });
109
+ if (!res.ok) return false;
110
+ const data = await res.json().catch(() => null);
111
+ if (data?.token) this.setToken(data.token);
112
+ if (data?.refresh_token) this.setRefreshToken(data.refresh_token);
113
+ return Boolean(data?.token);
114
+ } catch {
115
+ return false;
116
+ }
117
+ }
54
118
  /**
55
119
  * Register a callback invoked on forced logout (401).
56
120
  * Use this in React Native instead of the default window.location redirect.
@@ -91,7 +155,7 @@ var HttpClient = class {
91
155
  /**
92
156
  * Core HTTP request method - DRY principle
93
157
  */
94
- async request(endpoint, options = {}) {
158
+ async request(endpoint, options = {}, _isRetry = false) {
95
159
  const { method = "GET", body, headers: customHeaders, skipAuth = false } = options;
96
160
  const headers = { ...this.getHeaders(), ...customHeaders };
97
161
  const fetchOptions = {
@@ -109,12 +173,22 @@ var HttpClient = class {
109
173
  const data = await res.json().catch(() => null);
110
174
  if (!res.ok) {
111
175
  if (res.status === 401 && !skipAuth) {
176
+ if (!_isRetry && await this.tryRefresh()) {
177
+ return this.request(endpoint, options, true);
178
+ }
112
179
  this.forceLogout();
113
180
  }
114
181
  if (res.status === 403 && data?.error === "ip_not_allowed") {
115
182
  this.handleIPBlocked(data.ip);
116
183
  }
117
- return { error: data?.error || `Request failed: ${res.status}`, ...data };
184
+ const rateLimited = res.status === 429;
185
+ const retryHint = res.headers.get("retry-after") || res.headers.get("x-ratelimit-reset");
186
+ return {
187
+ error: data?.error || `Request failed: ${res.status}`,
188
+ status: res.status,
189
+ ...rateLimited ? { rateLimited: true, retryAfter: retryHint ? parseInt(retryHint, 10) : void 0 } : {},
190
+ ...data
191
+ };
118
192
  }
119
193
  return data;
120
194
  }
@@ -146,7 +220,11 @@ var HttpClient = class {
146
220
  }
147
221
  logout() {
148
222
  this.token = null;
149
- localStorage.removeItem("baas_token");
223
+ this.refreshToken = null;
224
+ if (typeof localStorage !== "undefined") {
225
+ localStorage.removeItem("baas_token");
226
+ localStorage.removeItem("baas_refresh_token");
227
+ }
150
228
  }
151
229
  forceLogout() {
152
230
  this.logout();
@@ -231,10 +309,49 @@ var QueryBuilder = class {
231
309
  this.queryParams.set(column, `not.${operator}.${value}`);
232
310
  return this;
233
311
  }
312
+ /** Array/jsonb/range contains (@>). Arrays become a `{a,b}` literal. */
313
+ contains(column, values) {
314
+ this.queryParams.set(column, `cs.${this.arrayLiteral(values)}`);
315
+ return this;
316
+ }
317
+ /** Array/jsonb/range contained-by (<@). */
318
+ containedBy(column, values) {
319
+ this.queryParams.set(column, `cd.${this.arrayLiteral(values)}`);
320
+ return this;
321
+ }
322
+ /** Array/range overlap (&&). */
323
+ overlaps(column, values) {
324
+ this.queryParams.set(column, `ov.${this.arrayLiteral(values)}`);
325
+ return this;
326
+ }
327
+ /** Full-text search (@@). type: fts|plfts|phfts|wfts (tsquery flavour). */
328
+ textSearch(column, query, type = "fts") {
329
+ this.queryParams.set(column, `${type}.${query}`);
330
+ return this;
331
+ }
332
+ arrayLiteral(values) {
333
+ return Array.isArray(values) ? `{${values.join(",")}}` : `${values}`;
334
+ }
335
+ /** OR group: `or=(c1,c2)`. Members are dotted `col.op.value` strings. */
234
336
  or(filters) {
235
337
  this.queryParams.set("or", `(${filters})`);
236
338
  return this;
237
339
  }
340
+ /** AND group: `and=(c1,c2)` — e.g. a range `qty.gt.5,qty.lt.20`. */
341
+ and(filters) {
342
+ this.queryParams.set("and", `(${filters})`);
343
+ return this;
344
+ }
345
+ /** Negated AND group: `not.and=(...)`. */
346
+ notAnd(filters) {
347
+ this.queryParams.set("not.and", `(${filters})`);
348
+ return this;
349
+ }
350
+ /** Negated OR group: `not.or=(...)`. */
351
+ notOr(filters) {
352
+ this.queryParams.set("not.or", `(${filters})`);
353
+ return this;
354
+ }
238
355
  order(column, { ascending = true } = {}) {
239
356
  this.queryParams.set("order", `${column}.${ascending ? "asc" : "desc"}`);
240
357
  return this;
@@ -278,6 +395,34 @@ var QueryBuilder = class {
278
395
  if (res.status === 204) return { success: true };
279
396
  return await this.handleResponse(res);
280
397
  }
398
+ /** Insert-or-update on conflict. [onConflict] is the conflict-target column(s). */
399
+ async upsert(data, onConflict) {
400
+ const res = await fetch(`${this.url}/api/v1/data/${this.table}/upsert`, {
401
+ method: "POST",
402
+ headers: this.getHeaders(),
403
+ body: JSON.stringify({ ...data, on_conflict: onConflict })
404
+ });
405
+ return await this.handleResponse(res);
406
+ }
407
+ /**
408
+ * Count rows matching the current filters (PostgREST parity). Sends
409
+ * `Prefer: count=exact` and parses the total from the `Content-Range`
410
+ * response header. Returns `{ count, error, status }`.
411
+ */
412
+ async count() {
413
+ const res = await fetch(`${this.url}/api/v1/data/${this.table}?${this.queryParams.toString()}`, {
414
+ headers: { ...this.getHeaders(), Prefer: "count=exact", "Cache-Control": "no-cache" },
415
+ cache: "no-store"
416
+ });
417
+ if (!res.ok) {
418
+ if (res.status === 401) this.client.forceLogout();
419
+ const body = await res.json().catch(() => null);
420
+ return { count: 0, error: body?.error || "Request failed", status: res.status };
421
+ }
422
+ const cr = res.headers.get("content-range");
423
+ const total = cr && cr.includes("/") ? parseInt(cr.split("/").pop() || "", 10) : NaN;
424
+ return { count: Number.isNaN(total) ? 0 : total, error: null, status: res.status };
425
+ }
281
426
  getHeaders() {
282
427
  return this.client.getHeaders();
283
428
  }
@@ -320,18 +465,65 @@ function createAuthModule(client) {
320
465
  throw new Error(data?.error || `Login failed: ${res.status}`);
321
466
  }
322
467
  if (data.token) {
323
- client.token = data.token;
324
- localStorage.setItem("baas_token", data.token);
468
+ client.setToken(data.token);
469
+ }
470
+ if (data.refresh_token) {
471
+ client.setRefreshToken(data.refresh_token);
325
472
  }
326
473
  return data;
327
474
  },
475
+ /** Manually refresh the access token via the rotating refresh token. The
476
+ * HttpClient also does this automatically on a 401. */
477
+ async refresh() {
478
+ const rt = client.refreshToken;
479
+ const data = await post("/api/auth/refresh", rt ? { refresh_token: rt } : {});
480
+ if (data?.token) client.setToken(data.token);
481
+ if (data?.refresh_token) client.setRefreshToken(data.refresh_token);
482
+ return data;
483
+ },
328
484
  async logout() {
329
485
  try {
330
486
  await post("/api/logout");
331
487
  } finally {
332
- client.token = null;
333
- localStorage.removeItem("baas_token");
488
+ client.logout();
489
+ }
490
+ },
491
+ // Anonymous sign-in (Supabase parity). Response carries access_token (+ a
492
+ // back-compat `token`); store whichever is present.
493
+ async signInAnonymous() {
494
+ const data = await post("/api/auth/anonymous");
495
+ const tok = data?.access_token || data?.token;
496
+ if (tok) {
497
+ client.token = tok;
498
+ localStorage.setItem("baas_token", tok);
499
+ }
500
+ return data;
501
+ },
502
+ // Passwordless: request a magic link + 6-digit email OTP. Always resolves on
503
+ // a well-formed request (no account enumeration).
504
+ async requestOtp(email, createUser = true) {
505
+ return post("/api/auth/otp", { email, create_user: createUser });
506
+ },
507
+ // Verify a passwordless OTP/magic-link and complete sign-in.
508
+ async verifyOtp(type, email, token) {
509
+ const data = await post("/api/auth/verify", { type, email, token });
510
+ const tok = data?.access_token || data?.token;
511
+ if (tok) {
512
+ client.token = tok;
513
+ localStorage.setItem("baas_token", tok);
514
+ }
515
+ return data;
516
+ },
517
+ // Upgrade the current anonymous guest to a real account (preserves user id).
518
+ // Requires the anonymous Bearer to be set (call after signInAnonymous).
519
+ async upgradeAnonymous(email, password) {
520
+ const data = await post("/api/auth/upgrade", { email, password });
521
+ const tok = data?.access_token || data?.token;
522
+ if (tok) {
523
+ client.token = tok;
524
+ localStorage.setItem("baas_token", tok);
334
525
  }
526
+ return data;
335
527
  },
336
528
  async verifyMFA(mfaToken, code) {
337
529
  const data = await post("/api/mfa/finalize", { mfa_token: mfaToken, code });
@@ -530,6 +722,7 @@ function createDatabaseModule(client) {
530
722
  function createStorageModule(client) {
531
723
  const get = (endpoint) => client.get(endpoint);
532
724
  const post = (endpoint, body) => client.post(endpoint, body);
725
+ const put = (endpoint, body) => client.put(endpoint, body);
533
726
  const del = (endpoint) => client.delete(endpoint);
534
727
  async function uploadFile(fileOrTable, fileOrBucketId) {
535
728
  let file;
@@ -564,14 +757,24 @@ function createStorageModule(client) {
564
757
  async listFiles() {
565
758
  return get("/api/storage/files");
566
759
  },
567
- async getFile(fileId) {
568
- return get(`/api/storage/files/${fileId}`);
760
+ async getFile(fileId, expiresIn) {
761
+ const q = expiresIn && expiresIn > 0 ? `?expiresIn=${expiresIn}` : "";
762
+ return get(`/api/storage/files/${fileId}${q}`);
763
+ },
764
+ async createSignedUrl(fileId, expiresIn) {
765
+ const res = await get(
766
+ `/api/storage/files/${fileId}${expiresIn && expiresIn > 0 ? `?expiresIn=${expiresIn}` : ""}`
767
+ );
768
+ return { signedUrl: res?.url };
569
769
  },
570
770
  async deleteFile(fileId) {
571
771
  return del(`/api/storage/files/${fileId}`);
572
772
  },
573
- async createBucket(name, isPublic = false) {
574
- return post("/api/storage/buckets", { name, is_public: isPublic });
773
+ async createBucket(name, isPublic = false, maxBytes) {
774
+ return post("/api/storage/buckets", { name, is_public: isPublic, max_bytes: maxBytes });
775
+ },
776
+ async updateBucket(bucketId, opts) {
777
+ return put(`/api/storage/buckets/${bucketId}`, { is_public: opts.isPublic, max_bytes: opts.maxBytes });
575
778
  },
576
779
  async listBuckets() {
577
780
  return get("/api/storage/buckets");
@@ -584,6 +787,17 @@ function createStorageModule(client) {
584
787
  },
585
788
  async listBucketFiles(bucketId) {
586
789
  return get(`/api/storage/buckets/${bucketId}/files`);
790
+ },
791
+ async download(fileId) {
792
+ const res = await fetch(`${client.url}/api/storage/files/${fileId}/download`, {
793
+ headers: client.getHeaders("")
794
+ });
795
+ if (!res.ok) {
796
+ if (res.status === 401) client.forceLogout();
797
+ const err = await res.json().catch(() => ({ error: `Download failed: ${res.status}` }));
798
+ throw new Error(err.error ?? `Download failed: ${res.status}`);
799
+ }
800
+ return res.blob();
587
801
  }
588
802
  };
589
803
  }
@@ -684,7 +898,9 @@ function createFunctionsModule(client) {
684
898
  return del(`/api/functions/${id}`);
685
899
  },
686
900
  async update(id, code, options) {
687
- return put(`/api/functions/${id}`, { code, ...options });
901
+ const current = await get(`/api/functions/${id}`);
902
+ const name = current?.name ?? current?.data?.name;
903
+ return post("/api/functions", { name, code, ...options });
688
904
  },
689
905
  async executeCode(code, options) {
690
906
  return post("/api/functions/execute", { code, ...options });
@@ -775,9 +991,18 @@ function createEmailModule(client) {
775
991
  async getConfig() {
776
992
  return get("/api/email/config");
777
993
  },
994
+ async listConfigs() {
995
+ return get("/api/email/configs");
996
+ },
778
997
  async saveConfig(config) {
779
998
  return post("/api/email/config", config);
780
999
  },
1000
+ async setDefaultConfig(id) {
1001
+ return post(`/api/email/configs/${id}/default`);
1002
+ },
1003
+ async deleteConfig(id) {
1004
+ return del(`/api/email/configs/${id}`);
1005
+ },
781
1006
  async createTemplate(template) {
782
1007
  return post("/api/email/templates", template);
783
1008
  },
@@ -830,7 +1055,9 @@ function createGraphQLModule(client) {
830
1055
  return post("/api/graphql", { query, variables });
831
1056
  },
832
1057
  async getSchema() {
833
- return get("/api/graphql/schema");
1058
+ return post("/api/graphql", {
1059
+ query: "{ __schema { queryType { name } mutationType { name } types { name kind } } }"
1060
+ });
834
1061
  },
835
1062
  getPlaygroundUrl() {
836
1063
  return `${client.url}/api/graphql/playground`;
@@ -876,6 +1103,7 @@ function createMetricsModule(client) {
876
1103
  // src/modules/audit.ts
877
1104
  function createAuditModule(client) {
878
1105
  const get = (endpoint) => client.get(endpoint);
1106
+ const del = (endpoint) => client.delete(endpoint);
879
1107
  return {
880
1108
  async list(options) {
881
1109
  const params = new URLSearchParams();
@@ -892,6 +1120,43 @@ function createAuditModule(client) {
892
1120
  },
893
1121
  async getActions() {
894
1122
  return get("/api/audit/actions");
1123
+ },
1124
+ async exportLogs(format, options) {
1125
+ const params = new URLSearchParams();
1126
+ params.set("format", format);
1127
+ if (options?.action) params.set("action", options.action);
1128
+ if (options?.method) params.set("method", options.method);
1129
+ if (options?.path) params.set("path", options.path);
1130
+ if (options?.status_code) params.set("status_code", options.status_code.toString());
1131
+ if (options?.start_date) params.set("start_date", options.start_date);
1132
+ if (options?.end_date) params.set("end_date", options.end_date);
1133
+ const res = await fetch(`${client.url}/api/audit/export?${params.toString()}`, {
1134
+ headers: client.getHeaders(),
1135
+ credentials: "include"
1136
+ });
1137
+ if (!res.ok) {
1138
+ const data = await res.json();
1139
+ return { error: data.error || `Export failed: ${res.status}` };
1140
+ }
1141
+ const blob = await res.blob();
1142
+ const url = window.URL.createObjectURL(blob);
1143
+ const a = document.createElement("a");
1144
+ a.href = url;
1145
+ a.download = `audit_logs_${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.${format}`;
1146
+ document.body.appendChild(a);
1147
+ a.click();
1148
+ window.URL.revokeObjectURL(url);
1149
+ document.body.removeChild(a);
1150
+ return { success: true };
1151
+ },
1152
+ async purgePreview(olderThanDays) {
1153
+ return get(`/api/audit/purge/preview?older_than_days=${olderThanDays}`);
1154
+ },
1155
+ async purge(olderThanDays) {
1156
+ return del(`/api/audit/purge?older_than_days=${olderThanDays}`);
1157
+ },
1158
+ async clearAll() {
1159
+ return del(`/api/audit/clear`);
895
1160
  }
896
1161
  };
897
1162
  }
@@ -992,6 +1257,16 @@ var RealtimeService = class {
992
1257
  }
993
1258
  socket = null;
994
1259
  subscribers = /* @__PURE__ */ new Map();
1260
+ // The server-side value filter currently REGISTERED per table (e.g.
1261
+ // { status: 'eq.pending' }). The server keeps one filter per (connection,table),
1262
+ // so this is only set when every subscriber on the table shares the same filter;
1263
+ // otherwise the table runs unfiltered server-side and each subscriber's filter is
1264
+ // applied client-side in handleEvent (see computeServerFilter).
1265
+ filters = /* @__PURE__ */ new Map();
1266
+ // Ephemeral Broadcast / Presence channel handlers.
1267
+ broadcastHandlers = /* @__PURE__ */ new Map();
1268
+ presenceHandlers = /* @__PURE__ */ new Map();
1269
+ presenceStates = /* @__PURE__ */ new Map();
995
1270
  reconnectAttempts = 0;
996
1271
  maxReconnectAttempts = 5;
997
1272
  reconnectInterval = 3e3;
@@ -1017,6 +1292,15 @@ var RealtimeService = class {
1017
1292
  socket.onopen = () => {
1018
1293
  console.log("BaaS Realtime: Connected");
1019
1294
  this.reconnectAttempts = 0;
1295
+ for (const table of this.subscribers.keys()) {
1296
+ this.sendSub("subscribe", table);
1297
+ }
1298
+ for (const channel of this.broadcastHandlers.keys()) {
1299
+ this.send({ event: "broadcast_subscribe", channel });
1300
+ }
1301
+ for (const channel of this.presenceHandlers.keys()) {
1302
+ this.send({ event: "presence_subscribe", channel, state: this.presenceStates.get(channel) ?? {} });
1303
+ }
1020
1304
  resolve();
1021
1305
  };
1022
1306
  socket.onmessage = (event) => {
@@ -1052,13 +1336,15 @@ var RealtimeService = class {
1052
1336
  /**
1053
1337
  * Subscribe to changes on a specific table
1054
1338
  */
1055
- async subscribe(table, action, callback) {
1339
+ async subscribe(table, action, callback, filter) {
1056
1340
  await this.connect();
1057
- if (!this.subscribers.has(table)) {
1341
+ const isNewTable = !this.subscribers.has(table);
1342
+ if (isNewTable) {
1058
1343
  this.subscribers.set(table, /* @__PURE__ */ new Set());
1059
1344
  }
1060
- const sub = { action, callback };
1345
+ const sub = { action, callback, filter: filter && Object.keys(filter).length > 0 ? filter : void 0 };
1061
1346
  this.subscribers.get(table).add(sub);
1347
+ if (table !== "*") this.syncServerFilter(table, isNewTable);
1062
1348
  return {
1063
1349
  unsubscribe: () => {
1064
1350
  const tableSubs = this.subscribers.get(table);
@@ -1066,32 +1352,194 @@ var RealtimeService = class {
1066
1352
  tableSubs.delete(sub);
1067
1353
  if (tableSubs.size === 0) {
1068
1354
  this.subscribers.delete(table);
1355
+ this.filters.delete(table);
1356
+ if (table !== "*") this.sendSub("unsubscribe", table);
1357
+ } else if (table !== "*") {
1358
+ this.syncServerFilter(table, false);
1069
1359
  }
1070
1360
  }
1071
1361
  }
1072
1362
  };
1073
1363
  }
1364
+ /**
1365
+ * Recompute the effective server-side filter for a table and (re)register it
1366
+ * when it changed (or when force is set, e.g. a brand-new table). The server
1367
+ * stores exactly one filter per (connection, table): a plain `subscribe`
1368
+ * clears it, a filtered `subscribe` replaces it.
1369
+ */
1370
+ syncServerFilter(table, force) {
1371
+ const desired = this.computeServerFilter(table);
1372
+ const current = this.filters.get(table);
1373
+ const changed = JSON.stringify(desired ?? null) !== JSON.stringify(current ?? null);
1374
+ if (!force && !changed) return;
1375
+ if (desired) this.filters.set(table, desired);
1376
+ else this.filters.delete(table);
1377
+ this.sendSub("subscribe", table);
1378
+ }
1379
+ /**
1380
+ * The server-side filter safe to apply for a table: the shared filter iff
1381
+ * EVERY subscriber requested the identical (non-empty) filter. If any
1382
+ * subscriber is unfiltered or filters conflict, returns undefined so the
1383
+ * server delivers all rows and each subscriber filters client-side.
1384
+ */
1385
+ computeServerFilter(table) {
1386
+ const subs = this.subscribers.get(table);
1387
+ if (!subs || subs.size === 0) return void 0;
1388
+ let chosen;
1389
+ for (const sub of subs) {
1390
+ const key = sub.filter && Object.keys(sub.filter).length > 0 ? JSON.stringify(sub.filter) : "";
1391
+ if (key === "") return void 0;
1392
+ if (chosen === void 0) chosen = key;
1393
+ else if (chosen !== key) return void 0;
1394
+ }
1395
+ return chosen ? JSON.parse(chosen) : void 0;
1396
+ }
1397
+ /** Send a subscribe/unsubscribe control frame to the server (with any filter). */
1398
+ sendSub(event, table) {
1399
+ const frame = { event, table };
1400
+ if (event === "subscribe") {
1401
+ const filter = this.filters.get(table);
1402
+ if (filter) frame.filter = filter;
1403
+ }
1404
+ this.send(frame);
1405
+ }
1406
+ /** Send an arbitrary control frame if the socket is open. */
1407
+ send(frame) {
1408
+ if (this.socket?.readyState === WebSocket.OPEN) {
1409
+ this.socket.send(JSON.stringify(frame));
1410
+ }
1411
+ }
1412
+ /**
1413
+ * Subscribe to an ephemeral Broadcast channel (no DB, no RLS — UI sync/chat).
1414
+ */
1415
+ async subscribeBroadcast(channel, callback) {
1416
+ await this.connect();
1417
+ const isNew = !this.broadcastHandlers.has(channel);
1418
+ if (isNew) this.broadcastHandlers.set(channel, /* @__PURE__ */ new Set());
1419
+ const handlers = this.broadcastHandlers.get(channel);
1420
+ handlers.add(callback);
1421
+ if (isNew) this.send({ event: "broadcast_subscribe", channel });
1422
+ return {
1423
+ unsubscribe: () => {
1424
+ handlers.delete(callback);
1425
+ if (handlers.size === 0) {
1426
+ this.broadcastHandlers.delete(channel);
1427
+ this.send({ event: "broadcast_unsubscribe", channel });
1428
+ }
1429
+ }
1430
+ };
1431
+ }
1432
+ /** Publish a message to a Broadcast channel. */
1433
+ broadcast(channel, payload) {
1434
+ this.send({ event: "broadcast", channel, payload });
1435
+ }
1436
+ /**
1437
+ * Join a Presence channel. The callback receives sync/join/leave events with
1438
+ * the current member list. Returns a leave() handle.
1439
+ */
1440
+ async subscribePresence(channel, state, callback) {
1441
+ await this.connect();
1442
+ const isNew = !this.presenceHandlers.has(channel);
1443
+ if (isNew) this.presenceHandlers.set(channel, /* @__PURE__ */ new Set());
1444
+ this.presenceStates.set(channel, state ?? {});
1445
+ const handlers = this.presenceHandlers.get(channel);
1446
+ handlers.add(callback);
1447
+ if (isNew) this.send({ event: "presence_subscribe", channel, state: state ?? {} });
1448
+ return {
1449
+ leave: () => {
1450
+ handlers.delete(callback);
1451
+ if (handlers.size === 0) {
1452
+ this.presenceHandlers.delete(channel);
1453
+ this.presenceStates.delete(channel);
1454
+ this.send({ event: "presence_unsubscribe", channel });
1455
+ }
1456
+ }
1457
+ };
1458
+ }
1074
1459
  /**
1075
1460
  * Handle incoming CDC events from the server
1076
1461
  */
1077
1462
  handleEvent(payload) {
1078
- const tableSubs = this.subscribers.get(payload.table);
1079
- if (tableSubs) {
1080
- for (const sub of tableSubs) {
1081
- if (sub.action === "*" || sub.action.toLowerCase() === payload.action.toLowerCase()) {
1082
- sub.callback(payload);
1463
+ const kind = payload.type;
1464
+ if (kind === "broadcast") {
1465
+ const handlers = this.broadcastHandlers.get(payload.channel);
1466
+ if (handlers) for (const cb of handlers) cb(payload.payload);
1467
+ return;
1468
+ }
1469
+ if (kind === "presence") {
1470
+ const handlers = this.presenceHandlers.get(payload.channel);
1471
+ if (handlers) for (const cb of handlers) cb(payload);
1472
+ return;
1473
+ }
1474
+ this.dispatchToSubs(this.subscribers.get(payload.table), payload);
1475
+ this.dispatchToSubs(this.subscribers.get("*"), payload);
1476
+ }
1477
+ /**
1478
+ * Fire matching subscribers. Each subscriber's own filter is re-checked
1479
+ * client-side: when subscribers on a table disagree, the server delivers all
1480
+ * rows unfiltered, so client-side filtering is what keeps each callback scoped.
1481
+ */
1482
+ dispatchToSubs(subs, payload) {
1483
+ if (!subs) return;
1484
+ for (const sub of subs) {
1485
+ if (sub.action !== "*" && sub.action.toLowerCase() !== payload.action.toLowerCase()) continue;
1486
+ if (sub.filter && !this.matchFilter(payload.record, sub.filter)) continue;
1487
+ sub.callback(payload);
1488
+ }
1489
+ }
1490
+ /** Evaluate a PostgREST-style filter against a record (mirrors hub_filter.go). */
1491
+ matchFilter(record, filter) {
1492
+ if (record == null) return false;
1493
+ for (const col of Object.keys(filter)) {
1494
+ const spec = filter[col];
1495
+ let op = "eq";
1496
+ let val = spec;
1497
+ const i = spec.indexOf(".");
1498
+ if (i > 0) {
1499
+ const cand = spec.slice(0, i);
1500
+ if (["eq", "neq", "gt", "gte", "lt", "lte", "in", "like"].includes(cand)) {
1501
+ op = cand;
1502
+ val = spec.slice(i + 1);
1083
1503
  }
1084
1504
  }
1505
+ const actualRaw = record[col];
1506
+ if (actualRaw === void 0) return false;
1507
+ const actual = actualRaw === null ? "" : String(actualRaw);
1508
+ if (!this.evalPred(op, actual, val)) return false;
1085
1509
  }
1086
- const wildcardSubs = this.subscribers.get("*");
1087
- if (wildcardSubs) {
1088
- for (const sub of wildcardSubs) {
1089
- if (sub.action === "*" || sub.action.toLowerCase() === payload.action.toLowerCase()) {
1090
- sub.callback(payload);
1091
- }
1510
+ return true;
1511
+ }
1512
+ evalPred(op, actual, val) {
1513
+ switch (op) {
1514
+ case "eq":
1515
+ return actual === val;
1516
+ case "neq":
1517
+ return actual !== val;
1518
+ case "in":
1519
+ return val.split(",").includes(actual);
1520
+ case "like":
1521
+ return this.likeMatch(actual, val);
1522
+ case "gt":
1523
+ case "gte":
1524
+ case "lt":
1525
+ case "lte": {
1526
+ const a = parseFloat(actual);
1527
+ const b = parseFloat(val);
1528
+ if (Number.isNaN(a) || Number.isNaN(b)) return false;
1529
+ if (op === "gt") return a > b;
1530
+ if (op === "gte") return a >= b;
1531
+ if (op === "lt") return a < b;
1532
+ return a <= b;
1092
1533
  }
1534
+ default:
1535
+ return false;
1093
1536
  }
1094
1537
  }
1538
+ /** SQL LIKE with '%' (any run) and '_' (any single char), anchored, case-sensitive. */
1539
+ likeMatch(s, pattern) {
1540
+ const re = "^" + pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/%/g, ".*").replace(/_/g, ".") + "$";
1541
+ return new RegExp(re).test(s);
1542
+ }
1095
1543
  /**
1096
1544
  * Attempt to reconnect on connection loss
1097
1545
  */
@@ -1129,8 +1577,17 @@ function createRealtimeModule(client) {
1129
1577
  typeof WebSocket !== "undefined" ? WebSocket : void 0
1130
1578
  );
1131
1579
  return {
1132
- async subscribe(table, action, callback) {
1133
- return service.subscribe(table, action, callback);
1580
+ async subscribe(table, action, callback, filter) {
1581
+ return service.subscribe(table, action, callback, filter);
1582
+ },
1583
+ async subscribeBroadcast(channel, callback) {
1584
+ return service.subscribeBroadcast(channel, callback);
1585
+ },
1586
+ broadcast(channel, payload) {
1587
+ service.broadcast(channel, payload);
1588
+ },
1589
+ async subscribePresence(channel, state, callback) {
1590
+ return service.subscribePresence(channel, state, callback);
1134
1591
  }
1135
1592
  };
1136
1593
  }
@@ -1153,6 +1610,9 @@ function createApiKeysModule(client) {
1153
1610
  async delete(keyId) {
1154
1611
  return del(`/api/api-keys/${keyId}`);
1155
1612
  },
1613
+ async setBrand(keyId, emailConfigId) {
1614
+ return post(`/api/api-keys/${keyId}/brand`, { email_config_id: emailConfigId });
1615
+ },
1156
1616
  async getInstanceToken() {
1157
1617
  return get("/api/api-keys/instance");
1158
1618
  },
@@ -1242,6 +1702,44 @@ function createIPWhitelistModule(client) {
1242
1702
  };
1243
1703
  }
1244
1704
 
1705
+ // src/modules/cache.ts
1706
+ function createCacheModule(client) {
1707
+ const get = (endpoint) => client.get(endpoint);
1708
+ const post = (endpoint, body) => client.post(endpoint, body);
1709
+ const patch = (endpoint, body) => client.patch(endpoint, body);
1710
+ const del = (endpoint) => client.delete(endpoint);
1711
+ return {
1712
+ async getStatus() {
1713
+ return get("/api/cache/status");
1714
+ },
1715
+ async getStats() {
1716
+ return get("/api/cache/stats");
1717
+ },
1718
+ async listPolicies() {
1719
+ return get("/api/cache/policies");
1720
+ },
1721
+ async updatePolicy(namespace, body) {
1722
+ return patch(`/api/cache/policies/${encodeURIComponent(namespace)}`, body);
1723
+ },
1724
+ async removePolicy(namespace) {
1725
+ return del(`/api/cache/policies/${encodeURIComponent(namespace)}`);
1726
+ },
1727
+ async invalidateNamespace(namespace) {
1728
+ return post(`/api/cache/invalidate/${encodeURIComponent(namespace)}`);
1729
+ },
1730
+ async invalidateAll() {
1731
+ return post("/api/cache/invalidate-all");
1732
+ },
1733
+ async listKeys(prefix, cursor = 0, count = 100) {
1734
+ const params = new URLSearchParams({ prefix, cursor: String(cursor), count: String(count) });
1735
+ return get(`/api/cache/keys?${params}`);
1736
+ },
1737
+ async inspectKey(key) {
1738
+ return get(`/api/cache/keys/inspect?key=${encodeURIComponent(key)}`);
1739
+ }
1740
+ };
1741
+ }
1742
+
1245
1743
  // src/client.ts
1246
1744
  var BaasClient = class extends HttpClient {
1247
1745
  // Feature modules
@@ -1268,8 +1766,9 @@ var BaasClient = class extends HttpClient {
1268
1766
  corsOrigins;
1269
1767
  policies;
1270
1768
  ipWhitelist;
1271
- constructor(url, apiKey) {
1272
- super(url, apiKey);
1769
+ cache;
1770
+ constructor(url, apiKey, options) {
1771
+ super(url, apiKey, options);
1273
1772
  this.auth = createAuthModule(this);
1274
1773
  this.users = createUsersModule(this);
1275
1774
  this.database = createDatabaseModule(this);
@@ -1293,6 +1792,7 @@ var BaasClient = class extends HttpClient {
1293
1792
  this.corsOrigins = createCorsOriginsModule(this);
1294
1793
  this.policies = createPoliciesModule(this);
1295
1794
  this.ipWhitelist = createIPWhitelistModule(this);
1795
+ this.cache = createCacheModule(this);
1296
1796
  }
1297
1797
  /**
1298
1798
  * Create a query builder for fluent data queries
@@ -1300,6 +1800,18 @@ var BaasClient = class extends HttpClient {
1300
1800
  from(table) {
1301
1801
  return new QueryBuilder(table, this.url, this);
1302
1802
  }
1803
+ /**
1804
+ * Invoke a PL/pgSQL function (PostgREST `.rpc()` parity). Runs under the
1805
+ * caller's RLS context; args bind by name and are $N-safe. Pass [params] for
1806
+ * POST (named args in the body) or set `opts.get = true` for the GET variant.
1807
+ */
1808
+ async rpc(fn, params, opts) {
1809
+ if (opts?.get) {
1810
+ const qs = params ? "?" + new URLSearchParams(Object.entries(params).map(([k, v]) => [k, String(v)])).toString() : "";
1811
+ return this.get(`/api/v1/rpc/${fn}${qs}`);
1812
+ }
1813
+ return this.post(`/api/v1/rpc/${fn}`, params ?? {});
1814
+ }
1303
1815
  // ============================================
1304
1816
  // BACKWARD COMPATIBILITY METHODS
1305
1817
  // These delegate to the appropriate modules
@@ -1419,8 +1931,8 @@ var BaasClient = class extends HttpClient {
1419
1931
  async deleteStorageFile(fileId) {
1420
1932
  return this.storage.deleteFile(fileId);
1421
1933
  }
1422
- async createStorageBucket(name, isPublic) {
1423
- return this.storage.createBucket(name, isPublic);
1934
+ async createStorageBucket(name, isPublic, maxBytes) {
1935
+ return this.storage.createBucket(name, isPublic, maxBytes);
1424
1936
  }
1425
1937
  async listStorageBuckets() {
1426
1938
  return this.storage.listBuckets();
@@ -1451,6 +1963,9 @@ var BaasClient = class extends HttpClient {
1451
1963
  async deleteApiKey(keyId) {
1452
1964
  return this.apiKeys.delete(keyId);
1453
1965
  }
1966
+ async setApiKeyBrand(keyId, emailConfigId) {
1967
+ return this.apiKeys.setBrand(keyId, emailConfigId);
1968
+ }
1454
1969
  async getInstanceToken() {
1455
1970
  return this.apiKeys.getInstanceToken();
1456
1971
  }
@@ -1525,9 +2040,18 @@ var BaasClient = class extends HttpClient {
1525
2040
  async getEmailConfig() {
1526
2041
  return this.email.getConfig();
1527
2042
  }
2043
+ async listEmailConfigs() {
2044
+ return this.email.listConfigs();
2045
+ }
1528
2046
  async saveEmailConfig(config) {
1529
2047
  return this.email.saveConfig(config);
1530
2048
  }
2049
+ async setDefaultEmailConfig(id) {
2050
+ return this.email.setDefaultConfig(id);
2051
+ }
2052
+ async deleteEmailConfig(id) {
2053
+ return this.email.deleteConfig(id);
2054
+ }
1531
2055
  async createEmailTemplate(template) {
1532
2056
  return this.email.createTemplate(template);
1533
2057
  }
@@ -1667,9 +2191,21 @@ var BaasClient = class extends HttpClient {
1667
2191
  async getAuditActions() {
1668
2192
  return this.audit.getActions();
1669
2193
  }
2194
+ async exportAuditLogs(format, options) {
2195
+ return this.audit.exportLogs(format, options);
2196
+ }
2197
+ async previewPurgeAuditLogs(olderThanDays) {
2198
+ return this.audit.purgePreview(olderThanDays);
2199
+ }
2200
+ async purgeAuditLogs(olderThanDays) {
2201
+ return this.audit.purge(olderThanDays);
2202
+ }
2203
+ async clearAllAuditLogs() {
2204
+ return this.audit.clearAll();
2205
+ }
1670
2206
  // Realtime shortcuts
1671
- subscribe(table, action, callback) {
1672
- return this.realtime.subscribe(table, action, callback);
2207
+ subscribe(table, action, callback, filter) {
2208
+ return this.realtime.subscribe(table, action, callback, filter);
1673
2209
  }
1674
2210
  // Environment shortcuts
1675
2211
  async getEnvironmentStatus() {
@@ -1721,3 +2257,4 @@ var BaasClient = class extends HttpClient {
1721
2257
  HttpClient,
1722
2258
  QueryBuilder
1723
2259
  });
2260
+ //# sourceMappingURL=index.cjs.map