@afribase/afribase-js 0.2.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.mjs ADDED
@@ -0,0 +1,1903 @@
1
+ // src/lib/fetch.ts
2
+ import crossFetch from "cross-fetch";
3
+ var _fetch = typeof globalThis !== "undefined" && globalThis.fetch ? globalThis.fetch : crossFetch;
4
+ async function resolveFetch(customFetch) {
5
+ if (customFetch) return customFetch;
6
+ return _fetch;
7
+ }
8
+ async function fetchWithAuth(url, options) {
9
+ const fetchFn = await resolveFetch(options.customFetch);
10
+ const headers = {
11
+ "apikey": options.apiKey,
12
+ ...options.headers
13
+ };
14
+ if (options.accessToken) {
15
+ headers["Authorization"] = `Bearer ${options.accessToken}`;
16
+ } else {
17
+ headers["Authorization"] = `Bearer ${options.apiKey}`;
18
+ }
19
+ return fetchFn(url, {
20
+ method: options.method || "GET",
21
+ headers,
22
+ body: options.body
23
+ });
24
+ }
25
+ async function handleResponse(response) {
26
+ if (!response.ok) {
27
+ let errorBody;
28
+ try {
29
+ errorBody = await response.json();
30
+ } catch {
31
+ errorBody = { message: await response.text() };
32
+ }
33
+ return {
34
+ data: null,
35
+ error: {
36
+ message: errorBody.message || errorBody.msg || `Request failed with status ${response.status}`,
37
+ status: response.status,
38
+ ...errorBody
39
+ }
40
+ };
41
+ }
42
+ if (response.status === 204) {
43
+ return { data: null, error: null };
44
+ }
45
+ try {
46
+ const data = await response.json();
47
+ return { data, error: null };
48
+ } catch {
49
+ return { data: null, error: null };
50
+ }
51
+ }
52
+
53
+ // src/lib/AfribaseAuthClient.ts
54
+ var STORAGE_KEY = "afribase.auth.token";
55
+ var AfribaseAuthClient = class {
56
+ constructor(options) {
57
+ this.currentSession = null;
58
+ this.refreshTimer = null;
59
+ this.listeners = /* @__PURE__ */ new Map();
60
+ this.listenerIdCounter = 0;
61
+ this.url = options.url.replace(/\/$/, "");
62
+ this.apiKey = options.apiKey;
63
+ this.autoRefresh = options.autoRefreshToken ?? true;
64
+ this.persistSession = options.persistSession ?? true;
65
+ this.customFetch = options.customFetch;
66
+ }
67
+ // ─────────────────────────────────────────────────────────────────────
68
+ // Session Management
69
+ // ─────────────────────────────────────────────────────────────────────
70
+ /**
71
+ * Initialize the auth client — restores persisted session and emits INITIAL_SESSION.
72
+ * Call this once after creating the client.
73
+ */
74
+ async initialize() {
75
+ try {
76
+ const stored = this._getStoredSession();
77
+ if (stored) {
78
+ if (this._isSessionExpired(stored)) {
79
+ const { data, error } = await this._refreshSession(stored.refresh_token);
80
+ if (!error && data) {
81
+ this._setSession(data);
82
+ this._notifyListeners("INITIAL_SESSION", data);
83
+ return { data: { session: data }, error: null };
84
+ }
85
+ this._removeSession();
86
+ this._notifyListeners("INITIAL_SESSION", null);
87
+ return { data: { session: null }, error: null };
88
+ }
89
+ this._setSession(stored);
90
+ this._notifyListeners("INITIAL_SESSION", stored);
91
+ return { data: { session: stored }, error: null };
92
+ }
93
+ this._notifyListeners("INITIAL_SESSION", null);
94
+ return { data: { session: null }, error: null };
95
+ } catch (err) {
96
+ return { data: { session: null }, error: err };
97
+ }
98
+ }
99
+ /**
100
+ * Returns the current session if it exists.
101
+ */
102
+ async getSession() {
103
+ return { data: { session: this.currentSession }, error: null };
104
+ }
105
+ /**
106
+ * Returns the current user if authenticated.
107
+ */
108
+ async getUser() {
109
+ if (!this.currentSession?.access_token) {
110
+ return { data: { user: null }, error: { message: "Not authenticated" } };
111
+ }
112
+ const resp = await fetchWithAuth(`${this.url}/user`, {
113
+ apiKey: this.apiKey,
114
+ accessToken: this.currentSession.access_token,
115
+ customFetch: this.customFetch
116
+ });
117
+ const result = await handleResponse(resp);
118
+ if (result.data && this.currentSession) {
119
+ this.currentSession.user = result.data;
120
+ }
121
+ return { data: { user: result.data }, error: result.error };
122
+ }
123
+ // ─────────────────────────────────────────────────────────────────────
124
+ // Sign Up / Sign In
125
+ // ─────────────────────────────────────────────────────────────────────
126
+ /**
127
+ * Create a new user with email/phone and password.
128
+ */
129
+ async signUp(credentials) {
130
+ const body = { password: credentials.password };
131
+ if (credentials.email) body.email = credentials.email;
132
+ if (credentials.phone) body.phone = credentials.phone;
133
+ if (credentials.options?.data) body.data = credentials.options.data;
134
+ if (credentials.options?.captchaToken) body.gotcha = credentials.options.captchaToken;
135
+ const resp = await fetchWithAuth(`${this.url}/signup`, {
136
+ method: "POST",
137
+ headers: { "Content-Type": "application/json" },
138
+ body: JSON.stringify(body),
139
+ apiKey: this.apiKey,
140
+ customFetch: this.customFetch
141
+ });
142
+ const result = await handleResponse(resp);
143
+ if (result.error) return { data: { user: null, session: null }, error: result.error };
144
+ const data = result.data;
145
+ if (data.access_token) {
146
+ const session = this._makeSession(data);
147
+ this._setSession(session);
148
+ this._notifyListeners("SIGNED_IN", session);
149
+ return { data: { user: session.user, session }, error: null };
150
+ }
151
+ return { data: { user: data, session: null }, error: null };
152
+ }
153
+ /**
154
+ * Sign in with email/phone and password.
155
+ */
156
+ async signInWithPassword(credentials) {
157
+ const body = { password: credentials.password };
158
+ if (credentials.email) body.email = credentials.email;
159
+ if (credentials.phone) body.phone = credentials.phone;
160
+ if (credentials.options?.captchaToken) body.gotcha = credentials.options.captchaToken;
161
+ const resp = await fetchWithAuth(`${this.url}/token?grant_type=password`, {
162
+ method: "POST",
163
+ headers: { "Content-Type": "application/json" },
164
+ body: JSON.stringify(body),
165
+ apiKey: this.apiKey,
166
+ customFetch: this.customFetch
167
+ });
168
+ const result = await handleResponse(resp);
169
+ if (result.error) return { data: { user: null, session: null }, error: result.error };
170
+ const session = this._makeSession(result.data);
171
+ this._setSession(session);
172
+ this._notifyListeners("SIGNED_IN", session);
173
+ return { data: { user: session.user, session }, error: null };
174
+ }
175
+ /**
176
+ * Sign in with a third-party OAuth provider.
177
+ * Returns a URL to redirect the user to.
178
+ */
179
+ async signInWithOAuth(credentials) {
180
+ const params = new URLSearchParams();
181
+ if (credentials.options?.redirectTo) params.set("redirect_to", credentials.options.redirectTo);
182
+ if (credentials.options?.scopes) params.set("scopes", credentials.options.scopes);
183
+ if (credentials.options?.queryParams) {
184
+ for (const [k, v] of Object.entries(credentials.options.queryParams)) {
185
+ params.set(k, v);
186
+ }
187
+ }
188
+ const qs = params.toString();
189
+ const url = `${this.url}/authorize?provider=${credentials.provider}${qs ? "&" + qs : ""}`;
190
+ return { data: { provider: credentials.provider, url }, error: null };
191
+ }
192
+ /**
193
+ * Sign in with a one-time password (magic link or SMS OTP).
194
+ */
195
+ async signInWithOtp(credentials) {
196
+ const body = {};
197
+ if (credentials.email) body.email = credentials.email;
198
+ if (credentials.phone) body.phone = credentials.phone;
199
+ if (credentials.options?.data) body.data = credentials.options.data;
200
+ if (credentials.options?.captchaToken) body.gotcha = credentials.options.captchaToken;
201
+ if (credentials.options?.shouldCreateUser !== void 0) body.create_user = credentials.options.shouldCreateUser;
202
+ const resp = await fetchWithAuth(`${this.url}/otp`, {
203
+ method: "POST",
204
+ headers: { "Content-Type": "application/json" },
205
+ body: JSON.stringify(body),
206
+ apiKey: this.apiKey,
207
+ customFetch: this.customFetch
208
+ });
209
+ return handleResponse(resp);
210
+ }
211
+ /**
212
+ * Verify an OTP or magic link token.
213
+ */
214
+ async verifyOtp(params) {
215
+ const body = { token: params.token, type: params.type };
216
+ if (params.email) body.email = params.email;
217
+ if (params.phone) body.phone = params.phone;
218
+ const resp = await fetchWithAuth(`${this.url}/verify`, {
219
+ method: "POST",
220
+ headers: { "Content-Type": "application/json" },
221
+ body: JSON.stringify(body),
222
+ apiKey: this.apiKey,
223
+ customFetch: this.customFetch
224
+ });
225
+ const result = await handleResponse(resp);
226
+ if (result.error) return { data: { user: null, session: null }, error: result.error };
227
+ if (result.data?.access_token) {
228
+ const session = this._makeSession(result.data);
229
+ this._setSession(session);
230
+ this._notifyListeners("SIGNED_IN", session);
231
+ return { data: { user: session.user, session }, error: null };
232
+ }
233
+ return { data: result.data, error: null };
234
+ }
235
+ /**
236
+ * Sign in anonymously — creates an anonymous user.
237
+ */
238
+ async signInAnonymously(options) {
239
+ const body = {};
240
+ if (options?.data) body.data = options.data;
241
+ if (options?.captchaToken) body.gotcha = options.captchaToken;
242
+ const resp = await fetchWithAuth(`${this.url}/signup`, {
243
+ method: "POST",
244
+ headers: { "Content-Type": "application/json" },
245
+ body: JSON.stringify(body),
246
+ apiKey: this.apiKey,
247
+ customFetch: this.customFetch
248
+ });
249
+ const result = await handleResponse(resp);
250
+ if (result.error) return { data: { user: null, session: null }, error: result.error };
251
+ if (result.data?.access_token) {
252
+ const session = this._makeSession(result.data);
253
+ this._setSession(session);
254
+ this._notifyListeners("SIGNED_IN", session);
255
+ return { data: { user: session.user, session }, error: null };
256
+ }
257
+ return { data: result.data, error: null };
258
+ }
259
+ // ─────────────────────────────────────────────────────────────────────
260
+ // Password Recovery & User Updates
261
+ // ─────────────────────────────────────────────────────────────────────
262
+ /**
263
+ * Sends a password reset email.
264
+ */
265
+ async resetPasswordForEmail(email, options) {
266
+ const body = { email };
267
+ if (options?.redirectTo) body.redirect_to = options.redirectTo;
268
+ if (options?.captchaToken) body.gotcha = options.captchaToken;
269
+ const resp = await fetchWithAuth(`${this.url}/recover`, {
270
+ method: "POST",
271
+ headers: { "Content-Type": "application/json" },
272
+ body: JSON.stringify(body),
273
+ apiKey: this.apiKey,
274
+ customFetch: this.customFetch
275
+ });
276
+ return handleResponse(resp);
277
+ }
278
+ /**
279
+ * Update the current user (email, phone, password, metadata).
280
+ */
281
+ async updateUser(attributes) {
282
+ if (!this.currentSession?.access_token) {
283
+ return { data: { user: null }, error: { message: "Not authenticated" } };
284
+ }
285
+ const resp = await fetchWithAuth(`${this.url}/user`, {
286
+ method: "PUT",
287
+ headers: { "Content-Type": "application/json" },
288
+ body: JSON.stringify(attributes),
289
+ apiKey: this.apiKey,
290
+ accessToken: this.currentSession.access_token,
291
+ customFetch: this.customFetch
292
+ });
293
+ const result = await handleResponse(resp);
294
+ if (!result.error && result.data && this.currentSession) {
295
+ this.currentSession.user = result.data;
296
+ this._persistSession(this.currentSession);
297
+ this._notifyListeners("USER_UPDATED", this.currentSession);
298
+ }
299
+ return { data: { user: result.data }, error: result.error };
300
+ }
301
+ // ─────────────────────────────────────────────────────────────────────
302
+ // Sign Out
303
+ // ─────────────────────────────────────────────────────────────────────
304
+ /**
305
+ * Sign out the current user.
306
+ */
307
+ async signOut() {
308
+ if (this.currentSession?.access_token) {
309
+ try {
310
+ await fetchWithAuth(`${this.url}/logout`, {
311
+ method: "POST",
312
+ apiKey: this.apiKey,
313
+ accessToken: this.currentSession.access_token,
314
+ customFetch: this.customFetch
315
+ });
316
+ } catch {
317
+ }
318
+ }
319
+ this._removeSession();
320
+ this._notifyListeners("SIGNED_OUT", null);
321
+ return { error: null };
322
+ }
323
+ // ─────────────────────────────────────────────────────────────────────
324
+ // Auth State Change
325
+ // ─────────────────────────────────────────────────────────────────────
326
+ /**
327
+ * Listen for auth state changes.
328
+ * Returns a subscription object with an unsubscribe method.
329
+ */
330
+ onAuthStateChange(callback) {
331
+ const id = `auth-listener-${++this.listenerIdCounter}`;
332
+ this.listeners.set(id, callback);
333
+ setTimeout(() => {
334
+ callback("INITIAL_SESSION", this.currentSession);
335
+ }, 0);
336
+ return {
337
+ data: {
338
+ subscription: {
339
+ id,
340
+ unsubscribe: () => {
341
+ this.listeners.delete(id);
342
+ }
343
+ }
344
+ }
345
+ };
346
+ }
347
+ // ─────────────────────────────────────────────────────────────────────
348
+ // Token Refresh
349
+ // ─────────────────────────────────────────────────────────────────────
350
+ /**
351
+ * Manually refresh the session token.
352
+ */
353
+ async refreshSession() {
354
+ if (!this.currentSession?.refresh_token) {
355
+ return { data: { session: null }, error: { message: "No refresh token" } };
356
+ }
357
+ const { data, error } = await this._refreshSession(this.currentSession.refresh_token);
358
+ if (error) return { data: { session: null }, error };
359
+ if (data) {
360
+ this._setSession(data);
361
+ this._notifyListeners("TOKEN_REFRESHED", data);
362
+ }
363
+ return { data: { session: data }, error: null };
364
+ }
365
+ /**
366
+ * Set the session manually (useful for server-side auth).
367
+ */
368
+ async setSession(params) {
369
+ const resp = await fetchWithAuth(`${this.url}/user`, {
370
+ apiKey: this.apiKey,
371
+ accessToken: params.access_token,
372
+ customFetch: this.customFetch
373
+ });
374
+ const result = await handleResponse(resp);
375
+ if (result.error) return { data: { session: null }, error: result.error };
376
+ const session = {
377
+ access_token: params.access_token,
378
+ refresh_token: params.refresh_token,
379
+ expires_in: 3600,
380
+ expires_at: Math.floor(Date.now() / 1e3) + 3600,
381
+ token_type: "bearer",
382
+ user: result.data
383
+ };
384
+ this._setSession(session);
385
+ this._notifyListeners("SIGNED_IN", session);
386
+ return { data: { session }, error: null };
387
+ }
388
+ // ─────────────────────────────────────────────────────────────────────
389
+ // Helpers — Access Token for other clients
390
+ // ─────────────────────────────────────────────────────────────────────
391
+ /** @internal */
392
+ get accessToken() {
393
+ return this.currentSession?.access_token ?? null;
394
+ }
395
+ // ─────────────────────────────────────────────────────────────────────
396
+ // Private Helpers
397
+ // ─────────────────────────────────────────────────────────────────────
398
+ _makeSession(data) {
399
+ const expiresAt = data.expires_at ?? Math.floor(Date.now() / 1e3) + (data.expires_in || 3600);
400
+ return {
401
+ access_token: data.access_token,
402
+ refresh_token: data.refresh_token,
403
+ expires_in: data.expires_in || 3600,
404
+ expires_at: expiresAt,
405
+ token_type: data.token_type || "bearer",
406
+ user: data.user || data
407
+ };
408
+ }
409
+ _setSession(session) {
410
+ this.currentSession = session;
411
+ this._persistSession(session);
412
+ this._scheduleRefresh(session);
413
+ }
414
+ _removeSession() {
415
+ this.currentSession = null;
416
+ if (this.refreshTimer) {
417
+ clearTimeout(this.refreshTimer);
418
+ this.refreshTimer = null;
419
+ }
420
+ this._clearStoredSession();
421
+ }
422
+ async _refreshSession(refreshToken) {
423
+ const resp = await fetchWithAuth(`${this.url}/token?grant_type=refresh_token`, {
424
+ method: "POST",
425
+ headers: { "Content-Type": "application/json" },
426
+ body: JSON.stringify({ refresh_token: refreshToken }),
427
+ apiKey: this.apiKey,
428
+ customFetch: this.customFetch
429
+ });
430
+ const result = await handleResponse(resp);
431
+ if (result.error) return { data: null, error: result.error };
432
+ return { data: this._makeSession(result.data), error: null };
433
+ }
434
+ _scheduleRefresh(session) {
435
+ if (!this.autoRefresh) return;
436
+ if (this.refreshTimer) clearTimeout(this.refreshTimer);
437
+ const expiresAt = session.expires_at ?? Math.floor(Date.now() / 1e3) + session.expires_in;
438
+ const now = Math.floor(Date.now() / 1e3);
439
+ const refreshIn = Math.max((expiresAt - now - 60) * 1e3, 1e3);
440
+ this.refreshTimer = setTimeout(async () => {
441
+ if (this.currentSession?.refresh_token) {
442
+ const { data, error } = await this._refreshSession(this.currentSession.refresh_token);
443
+ if (!error && data) {
444
+ this._setSession(data);
445
+ this._notifyListeners("TOKEN_REFRESHED", data);
446
+ } else {
447
+ this._removeSession();
448
+ this._notifyListeners("SIGNED_OUT", null);
449
+ }
450
+ }
451
+ }, refreshIn);
452
+ }
453
+ _isSessionExpired(session) {
454
+ if (!session.expires_at) return false;
455
+ return Math.floor(Date.now() / 1e3) >= session.expires_at;
456
+ }
457
+ _notifyListeners(event, session) {
458
+ for (const cb of this.listeners.values()) {
459
+ try {
460
+ cb(event, session);
461
+ } catch {
462
+ }
463
+ }
464
+ }
465
+ // Storage helpers (localStorage in browser, no-op in Node)
466
+ _persistSession(session) {
467
+ if (!this.persistSession) return;
468
+ try {
469
+ if (typeof globalThis !== "undefined" && globalThis.localStorage) {
470
+ globalThis.localStorage.setItem(STORAGE_KEY, JSON.stringify(session));
471
+ }
472
+ } catch {
473
+ }
474
+ }
475
+ _getStoredSession() {
476
+ if (!this.persistSession) return null;
477
+ try {
478
+ if (typeof globalThis !== "undefined" && globalThis.localStorage) {
479
+ const raw = globalThis.localStorage.getItem(STORAGE_KEY);
480
+ return raw ? JSON.parse(raw) : null;
481
+ }
482
+ } catch {
483
+ }
484
+ return null;
485
+ }
486
+ _clearStoredSession() {
487
+ try {
488
+ if (typeof globalThis !== "undefined" && globalThis.localStorage) {
489
+ globalThis.localStorage.removeItem(STORAGE_KEY);
490
+ }
491
+ } catch {
492
+ }
493
+ }
494
+ };
495
+
496
+ // src/lib/AfribaseRealtimeClient.ts
497
+ function getWebSocketImpl() {
498
+ if (typeof WebSocket !== "undefined") return WebSocket;
499
+ try {
500
+ const ws = globalThis.require?.("ws") ?? new Function('return require("ws")')();
501
+ return ws;
502
+ } catch {
503
+ throw new Error(
504
+ 'WebSocket is not available. In Node.js, install the "ws" package: npm install ws'
505
+ );
506
+ }
507
+ }
508
+ var WS_OPEN = 1;
509
+ var RealtimeChannel = class {
510
+ constructor(topic, params, sendMessage, removeChannel) {
511
+ this.params = params;
512
+ this.sendMessage = sendMessage;
513
+ this.removeChannel = removeChannel;
514
+ this.bindings = [];
515
+ this.presenceState = {};
516
+ this._status = "CLOSED";
517
+ this.topic = topic;
518
+ this.presenceKey = params.config?.presence?.key || "";
519
+ }
520
+ on(type, filter, callback) {
521
+ this.bindings.push({ type, filter, callback });
522
+ return this;
523
+ }
524
+ /**
525
+ * Subscribe to the channel — starts receiving events.
526
+ */
527
+ subscribe(callback) {
528
+ const joinConfig = { topic: this.topic };
529
+ const pgBindings = this.bindings.filter((b) => b.type === "postgres_changes");
530
+ if (pgBindings.length > 0) {
531
+ joinConfig.postgres_changes = pgBindings.map((b) => ({
532
+ event: b.filter?.event || "*",
533
+ schema: b.filter?.schema || "public",
534
+ table: b.filter?.table || "*",
535
+ filter: b.filter?.filter
536
+ }));
537
+ }
538
+ if (this.params.config?.broadcast) {
539
+ joinConfig.broadcast = this.params.config.broadcast;
540
+ }
541
+ if (this.params.config?.presence) {
542
+ joinConfig.presence = this.params.config.presence;
543
+ }
544
+ this.sendMessage({
545
+ topic: this.topic,
546
+ event: "phx_join",
547
+ payload: joinConfig,
548
+ ref: null
549
+ });
550
+ this._status = "SUBSCRIBED";
551
+ if (callback) {
552
+ setTimeout(() => callback("SUBSCRIBED"), 0);
553
+ }
554
+ return this;
555
+ }
556
+ /**
557
+ * Send a broadcast message to all subscribers.
558
+ */
559
+ send(payload) {
560
+ this.sendMessage({
561
+ topic: this.topic,
562
+ event: payload.event,
563
+ payload: payload.payload,
564
+ ref: null
565
+ });
566
+ return this;
567
+ }
568
+ /**
569
+ * Track presence for the current user.
570
+ */
571
+ track(payload) {
572
+ this.sendMessage({
573
+ topic: this.topic,
574
+ event: "presence",
575
+ payload: { type: "track", ...payload, key: this.presenceKey },
576
+ ref: null
577
+ });
578
+ return this;
579
+ }
580
+ /**
581
+ * Untrack presence for the current user.
582
+ */
583
+ untrack() {
584
+ this.sendMessage({
585
+ topic: this.topic,
586
+ event: "presence",
587
+ payload: { type: "untrack", key: this.presenceKey },
588
+ ref: null
589
+ });
590
+ return this;
591
+ }
592
+ /**
593
+ * Get current presence state.
594
+ */
595
+ presenceState_() {
596
+ return { ...this.presenceState };
597
+ }
598
+ /**
599
+ * Unsubscribe from this channel.
600
+ */
601
+ unsubscribe() {
602
+ this.sendMessage({
603
+ topic: this.topic,
604
+ event: "phx_leave",
605
+ payload: {},
606
+ ref: null
607
+ });
608
+ this._status = "CLOSED";
609
+ this.removeChannel(this.topic);
610
+ }
611
+ get status() {
612
+ return this._status;
613
+ }
614
+ /** @internal — called by the RealtimeClient when a message arrives */
615
+ _handleMessage(msg) {
616
+ if (msg.payload?.type === "postgres_changes") {
617
+ const pgPayload = msg.payload;
618
+ for (const binding of this.bindings) {
619
+ if (binding.type !== "postgres_changes") continue;
620
+ const f = binding.filter;
621
+ if (f?.schema && f.schema !== pgPayload.schema) continue;
622
+ if (f?.table && f.table !== pgPayload.table) continue;
623
+ if (f?.event && f.event !== "*" && f.event !== pgPayload.eventType) continue;
624
+ binding.callback(pgPayload);
625
+ }
626
+ return;
627
+ }
628
+ if (msg.payload?.type === "presence_state" || msg.payload?.type === "presence_diff") {
629
+ if (msg.payload.type === "presence_state") {
630
+ this.presenceState = msg.payload.state || {};
631
+ } else if (msg.payload.type === "presence_diff") {
632
+ const { joins, leaves } = msg.payload;
633
+ if (joins) {
634
+ for (const [key, val] of Object.entries(joins)) {
635
+ this.presenceState[key] = val.metas;
636
+ }
637
+ }
638
+ if (leaves) {
639
+ for (const key of Object.keys(leaves)) {
640
+ delete this.presenceState[key];
641
+ }
642
+ }
643
+ }
644
+ for (const binding of this.bindings) {
645
+ if (binding.type !== "presence") continue;
646
+ if (binding.filter?.event === "sync" || binding.filter?.event === msg.payload.type) {
647
+ binding.callback({
648
+ type: msg.payload.type,
649
+ presenceState: this.presenceState,
650
+ joins: msg.payload.joins,
651
+ leaves: msg.payload.leaves
652
+ });
653
+ }
654
+ }
655
+ return;
656
+ }
657
+ for (const binding of this.bindings) {
658
+ if (binding.type !== "broadcast") continue;
659
+ if (binding.filter?.event === msg.event || binding.filter?.event === "*") {
660
+ binding.callback({ type: "broadcast", event: msg.event, payload: msg.payload });
661
+ }
662
+ }
663
+ }
664
+ };
665
+ var AfribaseRealtimeClient = class {
666
+ constructor(options) {
667
+ this.ws = null;
668
+ this.channels = /* @__PURE__ */ new Map();
669
+ this.heartbeatTimer = null;
670
+ this.reconnectTimer = null;
671
+ this.reconnectAttempts = 0;
672
+ this.maxReconnectAttempts = 10;
673
+ this.reconnectBaseDelay = 1e3;
674
+ this.connected = false;
675
+ this.url = options.url.replace(/^http/, "ws").replace(/\/$/, "") + "/realtime/v1/websocket";
676
+ this.apiKey = options.apiKey;
677
+ this.accessToken = options.accessToken ?? null;
678
+ }
679
+ /**
680
+ * Set the access token (called by AfribaseClient when auth state changes).
681
+ */
682
+ setAuth(token) {
683
+ this.accessToken = token;
684
+ if (this.ws && this.ws.readyState === WS_OPEN) {
685
+ this._send({
686
+ topic: "phoenix",
687
+ event: "access_token",
688
+ payload: { access_token: token },
689
+ ref: null
690
+ });
691
+ }
692
+ }
693
+ /**
694
+ * Create a channel for a given topic.
695
+ */
696
+ channel(topic, options = {}) {
697
+ const normalizedTopic = topic.startsWith("realtime:") ? topic : `realtime:${topic}`;
698
+ if (this.channels.has(normalizedTopic)) {
699
+ return this.channels.get(normalizedTopic);
700
+ }
701
+ const ch = new RealtimeChannel(
702
+ normalizedTopic,
703
+ options,
704
+ (msg) => this._send(msg),
705
+ (t) => this.channels.delete(t)
706
+ );
707
+ this.channels.set(normalizedTopic, ch);
708
+ if (!this.ws) {
709
+ this._connect();
710
+ }
711
+ return ch;
712
+ }
713
+ /**
714
+ * Remove all channels and disconnect.
715
+ */
716
+ removeAllChannels() {
717
+ for (const ch of this.channels.values()) {
718
+ ch.unsubscribe();
719
+ }
720
+ this.channels.clear();
721
+ this._disconnect();
722
+ }
723
+ /**
724
+ * Disconnect the WebSocket.
725
+ */
726
+ disconnect() {
727
+ this._disconnect();
728
+ }
729
+ // ─────────────────────────────────────────────────────────────────────
730
+ // Private
731
+ // ─────────────────────────────────────────────────────────────────────
732
+ _connect() {
733
+ if (this.ws) return;
734
+ try {
735
+ const WSImpl = getWebSocketImpl();
736
+ const params = new URLSearchParams({
737
+ apikey: this.apiKey,
738
+ vsn: "1.0.0"
739
+ });
740
+ if (this.accessToken) {
741
+ params.set("token", this.accessToken);
742
+ }
743
+ this.ws = new WSImpl(`${this.url}?${params.toString()}`);
744
+ } catch (err) {
745
+ this._scheduleReconnect();
746
+ return;
747
+ }
748
+ this.ws.onopen = () => {
749
+ this.connected = true;
750
+ this.reconnectAttempts = 0;
751
+ this._startHeartbeat();
752
+ for (const ch of this.channels.values()) {
753
+ if (ch.status === "CLOSED") {
754
+ ch.subscribe();
755
+ }
756
+ }
757
+ };
758
+ this.ws.onmessage = (ev) => {
759
+ try {
760
+ const msg = JSON.parse(typeof ev.data === "string" ? ev.data : ev.data.toString());
761
+ this._handleMessage(msg);
762
+ } catch {
763
+ }
764
+ };
765
+ this.ws.onclose = () => {
766
+ this.connected = false;
767
+ this._stopHeartbeat();
768
+ this.ws = null;
769
+ if (this.channels.size > 0) {
770
+ this._scheduleReconnect();
771
+ }
772
+ };
773
+ this.ws.onerror = () => {
774
+ };
775
+ }
776
+ _disconnect() {
777
+ if (this.reconnectTimer) {
778
+ clearTimeout(this.reconnectTimer);
779
+ this.reconnectTimer = null;
780
+ }
781
+ this._stopHeartbeat();
782
+ if (this.ws) {
783
+ this.ws.onclose = null;
784
+ this.ws.close();
785
+ this.ws = null;
786
+ }
787
+ this.connected = false;
788
+ }
789
+ _send(msg) {
790
+ if (this.ws && this.ws.readyState === WS_OPEN) {
791
+ this.ws.send(JSON.stringify(msg));
792
+ }
793
+ }
794
+ _handleMessage(msg) {
795
+ if (msg.topic === "phoenix" && msg.event === "phx_reply") return;
796
+ const ch = this.channels.get(msg.topic);
797
+ if (ch) {
798
+ ch._handleMessage(msg);
799
+ }
800
+ }
801
+ _startHeartbeat() {
802
+ this._stopHeartbeat();
803
+ this.heartbeatTimer = setInterval(() => {
804
+ this._send({ topic: "phoenix", event: "heartbeat", payload: {}, ref: null });
805
+ }, 3e4);
806
+ }
807
+ _stopHeartbeat() {
808
+ if (this.heartbeatTimer) {
809
+ clearInterval(this.heartbeatTimer);
810
+ this.heartbeatTimer = null;
811
+ }
812
+ }
813
+ _scheduleReconnect() {
814
+ if (this.reconnectTimer) return;
815
+ if (this.reconnectAttempts >= this.maxReconnectAttempts) return;
816
+ const delay = Math.min(
817
+ this.reconnectBaseDelay * Math.pow(2, this.reconnectAttempts),
818
+ 3e4
819
+ );
820
+ this.reconnectAttempts++;
821
+ this.reconnectTimer = setTimeout(() => {
822
+ this.reconnectTimer = null;
823
+ this._connect();
824
+ }, delay);
825
+ }
826
+ };
827
+
828
+ // src/lib/AfribaseStorageClient.ts
829
+ var AfribaseStorageClient = class {
830
+ constructor(options) {
831
+ this.url = options.url.replace(/\/$/, "");
832
+ this.apiKey = options.apiKey;
833
+ this.getAccessToken = options.getAccessToken;
834
+ this.customFetch = options.customFetch;
835
+ }
836
+ /**
837
+ * Get a reference to a specific bucket for file operations.
838
+ */
839
+ from(bucketId) {
840
+ return new StorageFileApi(
841
+ this.url,
842
+ bucketId,
843
+ this.apiKey,
844
+ this.getAccessToken,
845
+ this.customFetch
846
+ );
847
+ }
848
+ // ─────────────────────────────────────────────────────────────────────
849
+ // Bucket Management
850
+ // ─────────────────────────────────────────────────────────────────────
851
+ /**
852
+ * List all buckets.
853
+ */
854
+ async listBuckets() {
855
+ const resp = await fetchWithAuth(`${this.url}/bucket`, {
856
+ apiKey: this.apiKey,
857
+ accessToken: this.getAccessToken(),
858
+ customFetch: this.customFetch
859
+ });
860
+ return handleResponse(resp);
861
+ }
862
+ /**
863
+ * Get a bucket by ID.
864
+ */
865
+ async getBucket(id) {
866
+ const resp = await fetchWithAuth(`${this.url}/bucket/${id}`, {
867
+ apiKey: this.apiKey,
868
+ accessToken: this.getAccessToken(),
869
+ customFetch: this.customFetch
870
+ });
871
+ return handleResponse(resp);
872
+ }
873
+ /**
874
+ * Create a new bucket.
875
+ */
876
+ async createBucket(id, options) {
877
+ const resp = await fetchWithAuth(`${this.url}/bucket`, {
878
+ method: "POST",
879
+ headers: { "Content-Type": "application/json" },
880
+ body: JSON.stringify({
881
+ id,
882
+ name: id,
883
+ public: options?.public ?? false,
884
+ file_size_limit: options?.fileSizeLimit,
885
+ allowed_mime_types: options?.allowedMimeTypes
886
+ }),
887
+ apiKey: this.apiKey,
888
+ accessToken: this.getAccessToken(),
889
+ customFetch: this.customFetch
890
+ });
891
+ return handleResponse(resp);
892
+ }
893
+ /**
894
+ * Update a bucket.
895
+ */
896
+ async updateBucket(id, options) {
897
+ const resp = await fetchWithAuth(`${this.url}/bucket/${id}`, {
898
+ method: "PUT",
899
+ headers: { "Content-Type": "application/json" },
900
+ body: JSON.stringify({
901
+ public: options.public,
902
+ file_size_limit: options.fileSizeLimit,
903
+ allowed_mime_types: options.allowedMimeTypes
904
+ }),
905
+ apiKey: this.apiKey,
906
+ accessToken: this.getAccessToken(),
907
+ customFetch: this.customFetch
908
+ });
909
+ return handleResponse(resp);
910
+ }
911
+ /**
912
+ * Delete a bucket (must be empty).
913
+ */
914
+ async deleteBucket(id) {
915
+ const resp = await fetchWithAuth(`${this.url}/bucket/${id}`, {
916
+ method: "DELETE",
917
+ apiKey: this.apiKey,
918
+ accessToken: this.getAccessToken(),
919
+ customFetch: this.customFetch
920
+ });
921
+ return handleResponse(resp);
922
+ }
923
+ /**
924
+ * Empty a bucket (remove all files).
925
+ */
926
+ async emptyBucket(id) {
927
+ const resp = await fetchWithAuth(`${this.url}/bucket/${id}/empty`, {
928
+ method: "POST",
929
+ apiKey: this.apiKey,
930
+ accessToken: this.getAccessToken(),
931
+ customFetch: this.customFetch
932
+ });
933
+ return handleResponse(resp);
934
+ }
935
+ };
936
+ var StorageFileApi = class {
937
+ constructor(storageUrl, bucketId, apiKey, getAccessToken, customFetch) {
938
+ this.storageUrl = storageUrl;
939
+ this.bucketId = bucketId;
940
+ this.apiKey = apiKey;
941
+ this.getAccessToken = getAccessToken;
942
+ this.customFetch = customFetch;
943
+ }
944
+ /**
945
+ * Upload a file to the bucket.
946
+ */
947
+ async upload(path, fileBody, options) {
948
+ const contentType = options?.contentType || "application/octet-stream";
949
+ const headers = {
950
+ "Content-Type": contentType
951
+ };
952
+ if (options?.cacheControl) headers["Cache-Control"] = options.cacheControl;
953
+ if (options?.upsert) headers["x-upsert"] = "true";
954
+ const resp = await fetchWithAuth(
955
+ `${this.storageUrl}/object/${this.bucketId}/${path}`,
956
+ {
957
+ method: "POST",
958
+ headers,
959
+ body: fileBody,
960
+ apiKey: this.apiKey,
961
+ accessToken: this.getAccessToken(),
962
+ customFetch: this.customFetch
963
+ }
964
+ );
965
+ const result = await handleResponse(resp);
966
+ if (result.error) return { data: null, error: result.error };
967
+ return {
968
+ data: {
969
+ path,
970
+ id: result.data?.id ?? result.data?.Key,
971
+ fullPath: `${this.bucketId}/${path}`
972
+ },
973
+ error: null
974
+ };
975
+ }
976
+ /**
977
+ * Update (replace) an existing file.
978
+ */
979
+ async update(path, fileBody, options) {
980
+ const contentType = options?.contentType || "application/octet-stream";
981
+ const headers = {
982
+ "Content-Type": contentType,
983
+ "x-upsert": "true"
984
+ };
985
+ if (options?.cacheControl) headers["Cache-Control"] = options.cacheControl;
986
+ const resp = await fetchWithAuth(
987
+ `${this.storageUrl}/object/${this.bucketId}/${path}`,
988
+ {
989
+ method: "POST",
990
+ headers,
991
+ body: fileBody,
992
+ apiKey: this.apiKey,
993
+ accessToken: this.getAccessToken(),
994
+ customFetch: this.customFetch
995
+ }
996
+ );
997
+ const result = await handleResponse(resp);
998
+ if (result.error) return { data: null, error: result.error };
999
+ return {
1000
+ data: {
1001
+ path,
1002
+ id: result.data?.id ?? result.data?.Key,
1003
+ fullPath: `${this.bucketId}/${path}`
1004
+ },
1005
+ error: null
1006
+ };
1007
+ }
1008
+ /**
1009
+ * Download a file from the bucket.
1010
+ */
1011
+ async download(path, options) {
1012
+ const params = new URLSearchParams();
1013
+ if (options?.transform) {
1014
+ if (options.transform.width) params.set("width", String(options.transform.width));
1015
+ if (options.transform.height) params.set("height", String(options.transform.height));
1016
+ if (options.transform.resize) params.set("resize", options.transform.resize);
1017
+ if (options.transform.quality) params.set("quality", String(options.transform.quality));
1018
+ if (options.transform.format) params.set("format", options.transform.format);
1019
+ }
1020
+ const qs = params.toString();
1021
+ const url = `${this.storageUrl}/object/${this.bucketId}/${path}${qs ? "?" + qs : ""}`;
1022
+ const fetchFn = this.customFetch || (globalThis.fetch ?? (await import("cross-fetch")).default);
1023
+ const headers = {
1024
+ "apikey": this.apiKey
1025
+ };
1026
+ const token = this.getAccessToken();
1027
+ if (token) headers["Authorization"] = `Bearer ${token}`;
1028
+ else headers["Authorization"] = `Bearer ${this.apiKey}`;
1029
+ const resp = await fetchFn(url, { headers });
1030
+ if (!resp.ok) {
1031
+ let errorBody;
1032
+ try {
1033
+ errorBody = await resp.json();
1034
+ } catch {
1035
+ errorBody = { message: `Download failed with status ${resp.status}` };
1036
+ }
1037
+ return { data: null, error: errorBody };
1038
+ }
1039
+ const blob = await resp.blob();
1040
+ return { data: blob, error: null };
1041
+ }
1042
+ /**
1043
+ * List objects in the bucket.
1044
+ */
1045
+ async list(path, options) {
1046
+ const body = {
1047
+ prefix: path || ""
1048
+ };
1049
+ if (options?.limit) body.limit = options.limit;
1050
+ if (options?.offset) body.offset = options.offset;
1051
+ if (options?.sortBy) body.sortBy = options.sortBy;
1052
+ const resp = await fetchWithAuth(`${this.storageUrl}/object/list/${this.bucketId}`, {
1053
+ method: "POST",
1054
+ headers: { "Content-Type": "application/json" },
1055
+ body: JSON.stringify(body),
1056
+ apiKey: this.apiKey,
1057
+ accessToken: this.getAccessToken(),
1058
+ customFetch: this.customFetch
1059
+ });
1060
+ return handleResponse(resp);
1061
+ }
1062
+ /**
1063
+ * Move a file to a new path within the same bucket.
1064
+ */
1065
+ async move(fromPath, toPath) {
1066
+ const resp = await fetchWithAuth(`${this.storageUrl}/object/move`, {
1067
+ method: "POST",
1068
+ headers: { "Content-Type": "application/json" },
1069
+ body: JSON.stringify({
1070
+ bucketId: this.bucketId,
1071
+ sourceKey: fromPath,
1072
+ destinationKey: toPath
1073
+ }),
1074
+ apiKey: this.apiKey,
1075
+ accessToken: this.getAccessToken(),
1076
+ customFetch: this.customFetch
1077
+ });
1078
+ return handleResponse(resp);
1079
+ }
1080
+ /**
1081
+ * Copy a file to a new path within the same bucket.
1082
+ */
1083
+ async copy(fromPath, toPath) {
1084
+ const resp = await fetchWithAuth(`${this.storageUrl}/object/copy`, {
1085
+ method: "POST",
1086
+ headers: { "Content-Type": "application/json" },
1087
+ body: JSON.stringify({
1088
+ bucketId: this.bucketId,
1089
+ sourceKey: fromPath,
1090
+ destinationKey: toPath
1091
+ }),
1092
+ apiKey: this.apiKey,
1093
+ accessToken: this.getAccessToken(),
1094
+ customFetch: this.customFetch
1095
+ });
1096
+ return handleResponse(resp);
1097
+ }
1098
+ /**
1099
+ * Remove one or more files from the bucket.
1100
+ */
1101
+ async remove(paths) {
1102
+ const resp = await fetchWithAuth(`${this.storageUrl}/object/${this.bucketId}`, {
1103
+ method: "DELETE",
1104
+ headers: { "Content-Type": "application/json" },
1105
+ body: JSON.stringify({ prefixes: paths }),
1106
+ apiKey: this.apiKey,
1107
+ accessToken: this.getAccessToken(),
1108
+ customFetch: this.customFetch
1109
+ });
1110
+ return handleResponse(resp);
1111
+ }
1112
+ /**
1113
+ * Create a signed URL for temporary access to a private file.
1114
+ */
1115
+ async createSignedUrl(path, expiresIn, options) {
1116
+ const body = {
1117
+ expiresIn
1118
+ };
1119
+ if (options?.download) {
1120
+ body.download = typeof options.download === "string" ? options.download : true;
1121
+ }
1122
+ if (options?.transform) body.transform = options.transform;
1123
+ const resp = await fetchWithAuth(`${this.storageUrl}/object/sign/${this.bucketId}/${path}`, {
1124
+ method: "POST",
1125
+ headers: { "Content-Type": "application/json" },
1126
+ body: JSON.stringify(body),
1127
+ apiKey: this.apiKey,
1128
+ accessToken: this.getAccessToken(),
1129
+ customFetch: this.customFetch
1130
+ });
1131
+ const result = await handleResponse(resp);
1132
+ if (result.error) return { data: null, error: result.error };
1133
+ const signedUrl = result.data?.signedURL || result.data?.signedUrl;
1134
+ return {
1135
+ data: {
1136
+ signedUrl: signedUrl?.startsWith("http") ? signedUrl : `${this.storageUrl}${signedUrl}`,
1137
+ path,
1138
+ token: result.data?.token || ""
1139
+ },
1140
+ error: null
1141
+ };
1142
+ }
1143
+ /**
1144
+ * Create signed URLs for multiple files.
1145
+ */
1146
+ async createSignedUrls(paths, expiresIn) {
1147
+ const resp = await fetchWithAuth(`${this.storageUrl}/object/sign/${this.bucketId}`, {
1148
+ method: "POST",
1149
+ headers: { "Content-Type": "application/json" },
1150
+ body: JSON.stringify({ expiresIn, paths }),
1151
+ apiKey: this.apiKey,
1152
+ accessToken: this.getAccessToken(),
1153
+ customFetch: this.customFetch
1154
+ });
1155
+ const result = await handleResponse(resp);
1156
+ if (result.error) return { data: null, error: result.error };
1157
+ const urls = (result.data || []).map((item) => ({
1158
+ signedUrl: item.signedURL?.startsWith("http") ? item.signedURL : `${this.storageUrl}${item.signedURL}`,
1159
+ path: item.path,
1160
+ token: item.token || ""
1161
+ }));
1162
+ return { data: urls, error: null };
1163
+ }
1164
+ /**
1165
+ * Get the public URL for a file in a public bucket.
1166
+ */
1167
+ getPublicUrl(path, options) {
1168
+ const params = new URLSearchParams();
1169
+ if (options?.download) {
1170
+ params.set("download", typeof options.download === "string" ? options.download : "");
1171
+ }
1172
+ if (options?.transform) {
1173
+ if (options.transform.width) params.set("width", String(options.transform.width));
1174
+ if (options.transform.height) params.set("height", String(options.transform.height));
1175
+ if (options.transform.resize) params.set("resize", options.transform.resize);
1176
+ if (options.transform.quality) params.set("quality", String(options.transform.quality));
1177
+ if (options.transform.format) params.set("format", options.transform.format);
1178
+ }
1179
+ const qs = params.toString();
1180
+ const publicUrl = `${this.storageUrl}/object/public/${this.bucketId}/${path}${qs ? "?" + qs : ""}`;
1181
+ return { data: { publicUrl } };
1182
+ }
1183
+ };
1184
+
1185
+ // src/lib/AfribaseFunctionsClient.ts
1186
+ var AfribaseFunctionsClient = class {
1187
+ constructor(options) {
1188
+ this.url = options.url.replace(/\/$/, "");
1189
+ this.apiKey = options.apiKey;
1190
+ this.getAccessToken = options.getAccessToken;
1191
+ this.customFetch = options.customFetch;
1192
+ }
1193
+ /**
1194
+ * Invoke an edge function by name.
1195
+ *
1196
+ * @example
1197
+ * const { data, error } = await client.functions.invoke('hello-world', {
1198
+ * body: { name: 'Afribase' },
1199
+ * });
1200
+ */
1201
+ async invoke(functionName, options) {
1202
+ const method = options?.method || "POST";
1203
+ const headers = {
1204
+ ...options?.headers
1205
+ };
1206
+ let body = void 0;
1207
+ if (options?.body !== void 0) {
1208
+ if (typeof options.body === "string" || options.body instanceof Blob || options.body instanceof ArrayBuffer || options.body instanceof FormData) {
1209
+ body = options.body;
1210
+ } else {
1211
+ headers["Content-Type"] = headers["Content-Type"] || "application/json";
1212
+ body = JSON.stringify(options.body);
1213
+ }
1214
+ }
1215
+ if (options?.region) {
1216
+ headers["x-region"] = options.region;
1217
+ }
1218
+ const resp = await fetchWithAuth(`${this.url}/${functionName}`, {
1219
+ method,
1220
+ headers,
1221
+ body,
1222
+ apiKey: this.apiKey,
1223
+ accessToken: this.getAccessToken(),
1224
+ customFetch: this.customFetch
1225
+ });
1226
+ const contentType = resp.headers.get("content-type") || "";
1227
+ if (contentType.includes("application/json")) {
1228
+ return handleResponse(resp);
1229
+ }
1230
+ if (!resp.ok) {
1231
+ const text2 = await resp.text();
1232
+ return {
1233
+ data: null,
1234
+ error: { message: text2, status: resp.status }
1235
+ };
1236
+ }
1237
+ const text = await resp.text();
1238
+ return { data: text, error: null };
1239
+ }
1240
+ };
1241
+
1242
+ // src/lib/AfribaseQueryBuilder.ts
1243
+ var AfribaseQueryBuilder = class {
1244
+ constructor(url, options) {
1245
+ this._method = null;
1246
+ this._body = void 0;
1247
+ this._isSingle = false;
1248
+ this._isMaybeSingle = false;
1249
+ this._isHead = false;
1250
+ this._countType = null;
1251
+ this._url = url;
1252
+ this._apiKey = options.apiKey;
1253
+ this._accessToken = options.accessToken;
1254
+ this._customFetch = options.customFetch;
1255
+ this._query = new URLSearchParams();
1256
+ this._headers = {
1257
+ "Content-Type": "application/json",
1258
+ "Prefer": "return=representation"
1259
+ };
1260
+ }
1261
+ // ─────────────────────────────────────────────────────────────────────
1262
+ // CRUD Operations
1263
+ // ─────────────────────────────────────────────────────────────────────
1264
+ /**
1265
+ * Perform a SELECT query.
1266
+ */
1267
+ select(columns = "*", options) {
1268
+ this._method = "GET";
1269
+ this._query.set("select", columns);
1270
+ if (options?.count) {
1271
+ this._countType = options.count;
1272
+ this._headers["Prefer"] = `count=${options.count}`;
1273
+ }
1274
+ if (options?.head) {
1275
+ this._isHead = true;
1276
+ this._method = "HEAD";
1277
+ }
1278
+ return this;
1279
+ }
1280
+ /**
1281
+ * Perform an INSERT.
1282
+ */
1283
+ insert(values, options) {
1284
+ this._method = "POST";
1285
+ this._body = values;
1286
+ const prefer = ["return=representation"];
1287
+ if (options?.count) {
1288
+ this._countType = options.count;
1289
+ prefer.push(`count=${options.count}`);
1290
+ }
1291
+ if (options?.defaultToNull === false) prefer.push("missing=default");
1292
+ this._headers["Prefer"] = prefer.join(",");
1293
+ return this;
1294
+ }
1295
+ /**
1296
+ * Perform an UPSERT (insert or update on conflict).
1297
+ */
1298
+ upsert(values, options) {
1299
+ this._method = "POST";
1300
+ this._body = values;
1301
+ const prefer = ["return=representation", "resolution=merge-duplicates"];
1302
+ if (options?.ignoreDuplicates) {
1303
+ prefer[1] = "resolution=ignore-duplicates";
1304
+ }
1305
+ if (options?.count) {
1306
+ this._countType = options.count;
1307
+ prefer.push(`count=${options.count}`);
1308
+ }
1309
+ if (options?.defaultToNull === false) prefer.push("missing=default");
1310
+ if (options?.onConflict) this._query.set("on_conflict", options.onConflict);
1311
+ this._headers["Prefer"] = prefer.join(",");
1312
+ return this;
1313
+ }
1314
+ /**
1315
+ * Perform an UPDATE (must be combined with filters).
1316
+ */
1317
+ update(values, options) {
1318
+ this._method = "PATCH";
1319
+ this._body = values;
1320
+ const prefer = ["return=representation"];
1321
+ if (options?.count) {
1322
+ this._countType = options.count;
1323
+ prefer.push(`count=${options.count}`);
1324
+ }
1325
+ this._headers["Prefer"] = prefer.join(",");
1326
+ return this;
1327
+ }
1328
+ /**
1329
+ * Perform a DELETE (must be combined with filters).
1330
+ */
1331
+ delete(options) {
1332
+ this._method = "DELETE";
1333
+ const prefer = ["return=representation"];
1334
+ if (options?.count) {
1335
+ this._countType = options.count;
1336
+ prefer.push(`count=${options.count}`);
1337
+ }
1338
+ this._headers["Prefer"] = prefer.join(",");
1339
+ return this;
1340
+ }
1341
+ // ─────────────────────────────────────────────────────────────────────
1342
+ // Filters
1343
+ // ─────────────────────────────────────────────────────────────────────
1344
+ eq(column, value) {
1345
+ this._query.append(column, `eq.${value}`);
1346
+ return this;
1347
+ }
1348
+ neq(column, value) {
1349
+ this._query.append(column, `neq.${value}`);
1350
+ return this;
1351
+ }
1352
+ gt(column, value) {
1353
+ this._query.append(column, `gt.${value}`);
1354
+ return this;
1355
+ }
1356
+ gte(column, value) {
1357
+ this._query.append(column, `gte.${value}`);
1358
+ return this;
1359
+ }
1360
+ lt(column, value) {
1361
+ this._query.append(column, `lt.${value}`);
1362
+ return this;
1363
+ }
1364
+ lte(column, value) {
1365
+ this._query.append(column, `lte.${value}`);
1366
+ return this;
1367
+ }
1368
+ like(column, pattern) {
1369
+ this._query.append(column, `like.${pattern}`);
1370
+ return this;
1371
+ }
1372
+ ilike(column, pattern) {
1373
+ this._query.append(column, `ilike.${pattern}`);
1374
+ return this;
1375
+ }
1376
+ is(column, value) {
1377
+ this._query.append(column, `is.${value}`);
1378
+ return this;
1379
+ }
1380
+ in(column, values) {
1381
+ const cleanedValues = values.map(
1382
+ (v) => typeof v === "string" ? `"${v}"` : v
1383
+ );
1384
+ this._query.append(column, `in.(${cleanedValues.join(",")})`);
1385
+ return this;
1386
+ }
1387
+ contains(column, value) {
1388
+ if (Array.isArray(value)) {
1389
+ this._query.append(column, `cs.{${value.join(",")}}`);
1390
+ } else if (typeof value === "object") {
1391
+ this._query.append(column, `cs.${JSON.stringify(value)}`);
1392
+ } else {
1393
+ this._query.append(column, `cs.${value}`);
1394
+ }
1395
+ return this;
1396
+ }
1397
+ containedBy(column, value) {
1398
+ if (Array.isArray(value)) {
1399
+ this._query.append(column, `cd.{${value.join(",")}}`);
1400
+ } else if (typeof value === "object") {
1401
+ this._query.append(column, `cd.${JSON.stringify(value)}`);
1402
+ } else {
1403
+ this._query.append(column, `cd.${value}`);
1404
+ }
1405
+ return this;
1406
+ }
1407
+ /**
1408
+ * Full text search using to_tsquery.
1409
+ */
1410
+ textSearch(column, query, options) {
1411
+ let op = "fts";
1412
+ if (options?.type === "plain") op = "plfts";
1413
+ else if (options?.type === "phrase") op = "phfts";
1414
+ else if (options?.type === "websearch") op = "wfts";
1415
+ const configStr = options?.config ? `(${options.config})` : "";
1416
+ this._query.append(column, `${op}${configStr}.${query}`);
1417
+ return this;
1418
+ }
1419
+ /**
1420
+ * Negate a filter.
1421
+ */
1422
+ not(column, operator, value) {
1423
+ this._query.append(column, `not.${operator}.${value}`);
1424
+ return this;
1425
+ }
1426
+ /**
1427
+ * Combine filters with OR.
1428
+ */
1429
+ or(filters, options) {
1430
+ const key = options?.foreignTable ? `${options.foreignTable}.or` : "or";
1431
+ this._query.append(key, `(${filters})`);
1432
+ return this;
1433
+ }
1434
+ /**
1435
+ * Generic filter — any PostgREST operator.
1436
+ */
1437
+ filter(column, operator, value) {
1438
+ this._query.append(column, `${operator}.${value}`);
1439
+ return this;
1440
+ }
1441
+ /**
1442
+ * Match multiple column values (shorthand for multiple .eq).
1443
+ */
1444
+ match(query) {
1445
+ for (const [key, value] of Object.entries(query)) {
1446
+ this._query.append(key, `eq.${value}`);
1447
+ }
1448
+ return this;
1449
+ }
1450
+ // ─────────────────────────────────────────────────────────────────────
1451
+ // Modifiers
1452
+ // ─────────────────────────────────────────────────────────────────────
1453
+ /**
1454
+ * Order results.
1455
+ */
1456
+ order(column, options) {
1457
+ const direction = options?.ascending === false ? "desc" : "asc";
1458
+ const nulls = options?.nullsFirst ? ".nullsfirst" : ".nullslast";
1459
+ const key = options?.foreignTable ? `${options.foreignTable}.order` : "order";
1460
+ this._query.append(key, `${column}.${direction}${nulls}`);
1461
+ return this;
1462
+ }
1463
+ /**
1464
+ * Limit the number of results.
1465
+ */
1466
+ limit(count, options) {
1467
+ const key = options?.foreignTable ? `${options.foreignTable}.limit` : "limit";
1468
+ this._query.set(key, String(count));
1469
+ return this;
1470
+ }
1471
+ /**
1472
+ * Paginate with offset.
1473
+ */
1474
+ range(from, to, options) {
1475
+ const key = options?.foreignTable ? `${options.foreignTable}.offset` : "offset";
1476
+ this._query.set(key, String(from));
1477
+ const limitKey = options?.foreignTable ? `${options.foreignTable}.limit` : "limit";
1478
+ this._query.set(limitKey, String(to - from + 1));
1479
+ this._headers["Range"] = `${from}-${to}`;
1480
+ this._headers["Range-Unit"] = "items";
1481
+ return this;
1482
+ }
1483
+ /**
1484
+ * Return a single row (throws if 0 or 2+ rows).
1485
+ */
1486
+ single() {
1487
+ this._isSingle = true;
1488
+ this._headers["Accept"] = "application/vnd.pgrst.object+json";
1489
+ return this;
1490
+ }
1491
+ /**
1492
+ * Return at most one row (returns null if 0 rows).
1493
+ */
1494
+ maybeSingle() {
1495
+ this._isMaybeSingle = true;
1496
+ this._headers["Accept"] = "application/vnd.pgrst.object+json";
1497
+ return this;
1498
+ }
1499
+ /**
1500
+ * Return as CSV.
1501
+ */
1502
+ csv() {
1503
+ this._headers["Accept"] = "text/csv";
1504
+ return this;
1505
+ }
1506
+ /**
1507
+ * Return a GeoJSON response.
1508
+ */
1509
+ geojson() {
1510
+ this._headers["Accept"] = "application/geo+json";
1511
+ return this;
1512
+ }
1513
+ /**
1514
+ * Limit the fields returned (PostgREST explain).
1515
+ */
1516
+ explain(options) {
1517
+ const parts = ["application/vnd.pgrst.plan"];
1518
+ if (options?.analyze) parts.push("analyze");
1519
+ if (options?.verbose) parts.push("verbose");
1520
+ if (options?.settings) parts.push("settings");
1521
+ if (options?.buffers) parts.push("buffers");
1522
+ if (options?.format) parts.push(options.format);
1523
+ this._headers["Accept"] = parts.join("+");
1524
+ return this;
1525
+ }
1526
+ // ─────────────────────────────────────────────────────────────────────
1527
+ // Execute
1528
+ // ─────────────────────────────────────────────────────────────────────
1529
+ /**
1530
+ * Execute the query and return the results.
1531
+ */
1532
+ async then(onfulfilled, onrejected) {
1533
+ let res;
1534
+ try {
1535
+ res = await this._execute();
1536
+ } catch (err) {
1537
+ if (onrejected) return Promise.resolve(onrejected(err));
1538
+ throw err;
1539
+ }
1540
+ if (onfulfilled) return Promise.resolve(onfulfilled(res));
1541
+ return res;
1542
+ }
1543
+ async _execute() {
1544
+ const method = this._method || "GET";
1545
+ const queryString = this._query.toString();
1546
+ const requestUrl = queryString ? `${this._url}?${queryString}` : this._url;
1547
+ const resp = await fetchWithAuth(requestUrl, {
1548
+ method,
1549
+ headers: this._headers,
1550
+ body: this._body ? JSON.stringify(this._body) : void 0,
1551
+ apiKey: this._apiKey,
1552
+ accessToken: this._accessToken,
1553
+ customFetch: this._customFetch
1554
+ });
1555
+ let count = null;
1556
+ if (this._countType) {
1557
+ const contentRange = resp.headers.get("content-range");
1558
+ if (contentRange) {
1559
+ const match2 = contentRange.match(/\/(\d+)/);
1560
+ if (match2) count = parseInt(match2[1], 10);
1561
+ }
1562
+ }
1563
+ if (!resp.ok) {
1564
+ let errorBody;
1565
+ try {
1566
+ errorBody = await resp.json();
1567
+ } catch {
1568
+ errorBody = { message: await resp.text() };
1569
+ }
1570
+ return {
1571
+ data: null,
1572
+ error: {
1573
+ message: errorBody.message || `Request failed with status ${resp.status}`,
1574
+ details: errorBody.details || "",
1575
+ hint: errorBody.hint || "",
1576
+ code: errorBody.code || String(resp.status)
1577
+ },
1578
+ count,
1579
+ status: resp.status,
1580
+ statusText: resp.statusText
1581
+ };
1582
+ }
1583
+ if (this._isHead || method === "HEAD") {
1584
+ return { data: null, error: null, count, status: resp.status, statusText: resp.statusText };
1585
+ }
1586
+ if (resp.status === 204) {
1587
+ return { data: null, error: null, count, status: resp.status, statusText: resp.statusText };
1588
+ }
1589
+ let data;
1590
+ const contentType = resp.headers.get("content-type") || "";
1591
+ if (contentType.includes("text/csv") || contentType.includes("geo+json") || contentType.includes("pgrst.plan")) {
1592
+ data = await resp.text();
1593
+ } else {
1594
+ data = await resp.json();
1595
+ }
1596
+ if (this._isMaybeSingle && (resp.status === 406 || data === null)) {
1597
+ return { data: null, error: null, count, status: 200, statusText: "OK" };
1598
+ }
1599
+ return { data, error: null, count, status: resp.status, statusText: resp.statusText };
1600
+ }
1601
+ };
1602
+ var AfribaseRpcBuilder = class {
1603
+ constructor(url, fnName, params, options) {
1604
+ this._isSingle = false;
1605
+ this._countType = null;
1606
+ this._url = `${url}/rpc/${fnName}`;
1607
+ this._apiKey = options.apiKey;
1608
+ this._accessToken = options.accessToken;
1609
+ this._customFetch = options.customFetch;
1610
+ this._params = params;
1611
+ this._query = new URLSearchParams();
1612
+ this._headers = {
1613
+ "Content-Type": "application/json",
1614
+ "Prefer": "return=representation"
1615
+ };
1616
+ this._method = options.get ? "GET" : "POST";
1617
+ if (options.count) {
1618
+ this._countType = options.count;
1619
+ this._headers["Prefer"] = `count=${options.count}`;
1620
+ }
1621
+ if (this._method === "GET" && params) {
1622
+ for (const [key, value] of Object.entries(params)) {
1623
+ this._query.set(key, String(value));
1624
+ }
1625
+ }
1626
+ }
1627
+ single() {
1628
+ this._isSingle = true;
1629
+ this._headers["Accept"] = "application/vnd.pgrst.object+json";
1630
+ return this;
1631
+ }
1632
+ maybeSingle() {
1633
+ this._headers["Accept"] = "application/vnd.pgrst.object+json";
1634
+ return this;
1635
+ }
1636
+ select(columns = "*") {
1637
+ this._query.set("select", columns);
1638
+ return this;
1639
+ }
1640
+ order(column, options) {
1641
+ const direction = options?.ascending === false ? "desc" : "asc";
1642
+ this._query.append("order", `${column}.${direction}`);
1643
+ return this;
1644
+ }
1645
+ limit(count) {
1646
+ this._query.set("limit", String(count));
1647
+ return this;
1648
+ }
1649
+ range(from, to) {
1650
+ this._query.set("offset", String(from));
1651
+ this._query.set("limit", String(to - from + 1));
1652
+ this._headers["Range"] = `${from}-${to}`;
1653
+ this._headers["Range-Unit"] = "items";
1654
+ return this;
1655
+ }
1656
+ eq(column, value) {
1657
+ this._query.append(column, `eq.${value}`);
1658
+ return this;
1659
+ }
1660
+ filter(column, operator, value) {
1661
+ this._query.append(column, `${operator}.${value}`);
1662
+ return this;
1663
+ }
1664
+ async then(onfulfilled, onrejected) {
1665
+ let res;
1666
+ try {
1667
+ res = await this._execute();
1668
+ } catch (err) {
1669
+ if (onrejected) return Promise.resolve(onrejected(err));
1670
+ throw err;
1671
+ }
1672
+ if (onfulfilled) return Promise.resolve(onfulfilled(res));
1673
+ return res;
1674
+ }
1675
+ async _execute() {
1676
+ const queryString = this._query.toString();
1677
+ const requestUrl = queryString ? `${this._url}?${queryString}` : this._url;
1678
+ const resp = await fetchWithAuth(requestUrl, {
1679
+ method: this._method,
1680
+ headers: this._headers,
1681
+ body: this._method === "POST" ? JSON.stringify(this._params) : void 0,
1682
+ apiKey: this._apiKey,
1683
+ accessToken: this._accessToken,
1684
+ customFetch: this._customFetch
1685
+ });
1686
+ let count = null;
1687
+ if (this._countType) {
1688
+ const contentRange = resp.headers.get("content-range");
1689
+ if (contentRange) {
1690
+ const match2 = contentRange.match(/\/(\d+)/);
1691
+ if (match2) count = parseInt(match2[1], 10);
1692
+ }
1693
+ }
1694
+ if (!resp.ok) {
1695
+ let errorBody;
1696
+ try {
1697
+ errorBody = await resp.json();
1698
+ } catch {
1699
+ errorBody = { message: await resp.text() };
1700
+ }
1701
+ return {
1702
+ data: null,
1703
+ error: {
1704
+ message: errorBody.message || `RPC failed with status ${resp.status}`,
1705
+ details: errorBody.details || "",
1706
+ hint: errorBody.hint || "",
1707
+ code: errorBody.code || String(resp.status)
1708
+ },
1709
+ count,
1710
+ status: resp.status,
1711
+ statusText: resp.statusText
1712
+ };
1713
+ }
1714
+ if (resp.status === 204) {
1715
+ return { data: null, error: null, count, status: 204, statusText: "No Content" };
1716
+ }
1717
+ const data = await resp.json();
1718
+ return { data, error: null, count, status: resp.status, statusText: resp.statusText };
1719
+ }
1720
+ };
1721
+
1722
+ // src/lib/modular.ts
1723
+ async function db(builder, ...ops) {
1724
+ let current = builder;
1725
+ for (const op of ops) {
1726
+ current = op(current);
1727
+ }
1728
+ return current;
1729
+ }
1730
+ var select = (columns = "*", options) => (b) => b.select(columns, options);
1731
+ var insert = (values, options) => (b) => b.insert(values, options);
1732
+ var update = (values, options) => (b) => b.update(values, options);
1733
+ var upsert = (values, options) => (b) => b.upsert(values, options);
1734
+ var del = (options) => (b) => b.delete(options);
1735
+ var eq = (column, value) => (b) => b.eq(column, value);
1736
+ var neq = (column, value) => (b) => b.neq(column, value);
1737
+ var gt = (column, value) => (b) => b.gt(column, value);
1738
+ var gte = (column, value) => (b) => b.gte(column, value);
1739
+ var lt = (column, value) => (b) => b.lt(column, value);
1740
+ var lte = (column, value) => (b) => b.lte(column, value);
1741
+ var like = (column, pattern) => (b) => b.like(column, pattern);
1742
+ var ilike = (column, pattern) => (b) => b.ilike(column, pattern);
1743
+ var is = (column, value) => (b) => b.is(column, value);
1744
+ var isIn = (column, values) => (b) => b.in(column, values);
1745
+ var contains = (column, value) => (b) => b.contains(column, value);
1746
+ var containedBy = (column, value) => (b) => b.containedBy(column, value);
1747
+ var textSearch = (column, query, options) => (b) => b.textSearch(column, query, options);
1748
+ var not = (column, operator, value) => (b) => b.not(column, operator, value);
1749
+ var or = (filters, options) => (b) => b.or(filters, options);
1750
+ var match = (query) => (b) => b.match(query);
1751
+ var order = (column, options) => (b) => b.order(column, options);
1752
+ var limit = (count, options) => (b) => b.limit(count, options);
1753
+ var range = (from, to, options) => (b) => b.range(from, to, options);
1754
+ var single = () => (b) => b.single();
1755
+ var maybeSingle = () => (b) => b.maybeSingle();
1756
+ var csv = () => (b) => b.csv();
1757
+ var geojson = () => (b) => b.geojson();
1758
+ var explain = (options) => (b) => b.explain(options);
1759
+
1760
+ // src/index.ts
1761
+ var AfribaseClient = class {
1762
+ constructor(baseUrl, anonKey, options) {
1763
+ this.baseUrl = baseUrl.replace(/\/$/, "");
1764
+ this.anonKey = anonKey;
1765
+ this.customFetch = options?.fetch;
1766
+ const authUrl = options?.authUrl || `${this.baseUrl}/auth/v1`;
1767
+ this.restUrl = options?.restUrl || `${this.baseUrl}/rest/v1`;
1768
+ const realtimeUrl = options?.realtimeUrl || this.baseUrl;
1769
+ const storageUrl = options?.storageUrl || `${this.baseUrl}/storage/v1`;
1770
+ const functionsUrl = options?.functionsUrl || `${this.baseUrl}/functions/v1`;
1771
+ this.auth = new AfribaseAuthClient({
1772
+ url: authUrl,
1773
+ apiKey: anonKey,
1774
+ autoRefreshToken: options?.autoRefreshToken ?? true,
1775
+ persistSession: options?.persistSession ?? true,
1776
+ customFetch: this.customFetch
1777
+ });
1778
+ this.realtimeClient = new AfribaseRealtimeClient({
1779
+ url: realtimeUrl,
1780
+ apiKey: anonKey,
1781
+ accessToken: this.auth.accessToken
1782
+ });
1783
+ this.storage = new AfribaseStorageClient({
1784
+ url: storageUrl,
1785
+ apiKey: anonKey,
1786
+ getAccessToken: () => this.auth.accessToken,
1787
+ customFetch: this.customFetch
1788
+ });
1789
+ this.functions = new AfribaseFunctionsClient({
1790
+ url: functionsUrl,
1791
+ apiKey: anonKey,
1792
+ getAccessToken: () => this.auth.accessToken,
1793
+ customFetch: this.customFetch
1794
+ });
1795
+ this.auth.onAuthStateChange((_event, session) => {
1796
+ this.realtimeClient.setAuth(session?.access_token ?? null);
1797
+ });
1798
+ this.auth.initialize();
1799
+ }
1800
+ // ─────────────────────────────────────────────────────────────────────
1801
+ // Database (PostgREST)
1802
+ // ─────────────────────────────────────────────────────────────────────
1803
+ /**
1804
+ * Query a table via PostgREST.
1805
+ *
1806
+ * @example
1807
+ * const { data } = await afribase.from('users').select('*').eq('id', 1);
1808
+ * const { data } = await afribase.from('posts').select('*, comments(*)').order('created_at', { ascending: false }).limit(10);
1809
+ */
1810
+ from(table) {
1811
+ return new AfribaseQueryBuilder(`${this.restUrl}/${table}`, {
1812
+ apiKey: this.anonKey,
1813
+ accessToken: this.auth.accessToken,
1814
+ customFetch: this.customFetch
1815
+ });
1816
+ }
1817
+ /**
1818
+ * Call a Postgres function (RPC).
1819
+ *
1820
+ * @example
1821
+ * const { data } = await afribase.rpc('get_top_users', { limit_count: 10 });
1822
+ */
1823
+ rpc(fn, params = {}, options) {
1824
+ return new AfribaseRpcBuilder(this.restUrl, fn, params, {
1825
+ apiKey: this.anonKey,
1826
+ accessToken: this.auth.accessToken,
1827
+ customFetch: this.customFetch,
1828
+ head: options?.head,
1829
+ count: options?.count,
1830
+ get: options?.get
1831
+ });
1832
+ }
1833
+ // ─────────────────────────────────────────────────────────────────────
1834
+ // Realtime
1835
+ // ─────────────────────────────────────────────────────────────────────
1836
+ /**
1837
+ * Create a realtime channel.
1838
+ *
1839
+ * @example
1840
+ * const channel = afribase.channel('room:lobby');
1841
+ * channel.on('broadcast', { event: 'message' }, (payload) => console.log(payload)).subscribe();
1842
+ */
1843
+ channel(name, options) {
1844
+ return this.realtimeClient.channel(name, options);
1845
+ }
1846
+ /**
1847
+ * Remove all realtime channels and disconnect.
1848
+ */
1849
+ removeAllChannels() {
1850
+ this.realtimeClient.removeAllChannels();
1851
+ }
1852
+ /**
1853
+ * Get the underlying realtime client.
1854
+ */
1855
+ get realtime() {
1856
+ return this.realtimeClient;
1857
+ }
1858
+ };
1859
+ var createClient = (baseUrl, anonKey, options) => {
1860
+ return new AfribaseClient(baseUrl, anonKey, options);
1861
+ };
1862
+ export {
1863
+ AfribaseAuthClient,
1864
+ AfribaseClient,
1865
+ AfribaseFunctionsClient,
1866
+ AfribaseQueryBuilder,
1867
+ AfribaseRealtimeClient,
1868
+ AfribaseRpcBuilder,
1869
+ AfribaseStorageClient,
1870
+ RealtimeChannel,
1871
+ StorageFileApi,
1872
+ containedBy,
1873
+ contains,
1874
+ createClient,
1875
+ csv,
1876
+ db,
1877
+ del,
1878
+ eq,
1879
+ explain,
1880
+ geojson,
1881
+ gt,
1882
+ gte,
1883
+ ilike,
1884
+ insert,
1885
+ is,
1886
+ isIn,
1887
+ like,
1888
+ limit,
1889
+ lt,
1890
+ lte,
1891
+ match,
1892
+ maybeSingle,
1893
+ neq,
1894
+ not,
1895
+ or,
1896
+ order,
1897
+ range,
1898
+ select,
1899
+ single,
1900
+ textSearch,
1901
+ update,
1902
+ upsert
1903
+ };