@fluxbase/sdk 0.0.1-rc.27 → 0.0.1-rc.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -208,8 +208,8 @@ var FluxbaseAuth = class {
208
208
  return { data: { subscription } };
209
209
  }
210
210
  /**
211
- * Sign in with email and password
212
- * Returns AuthSession if successful, or SignInWith2FAResponse if 2FA is required
211
+ * Sign in with email and password (Supabase-compatible)
212
+ * Returns { user, session } if successful, or SignInWith2FAResponse if 2FA is required
213
213
  */
214
214
  async signIn(credentials) {
215
215
  return wrapAsync(async () => {
@@ -223,19 +223,21 @@ var FluxbaseAuth = class {
223
223
  expires_at: Date.now() + authResponse.expires_in * 1e3
224
224
  };
225
225
  this.setSessionInternal(session);
226
- return session;
226
+ return { user: session.user, session };
227
227
  });
228
228
  }
229
229
  /**
230
- * Sign in with email and password
230
+ * Sign in with email and password (Supabase-compatible)
231
231
  * Alias for signIn() to maintain compatibility with common authentication patterns
232
- * Returns AuthSession if successful, or SignInWith2FAResponse if 2FA is required
232
+ * Returns { user, session } if successful, or SignInWith2FAResponse if 2FA is required
233
233
  */
234
234
  async signInWithPassword(credentials) {
235
235
  return this.signIn(credentials);
236
236
  }
237
237
  /**
238
- * Sign up with email and password
238
+ * Sign up with email and password (Supabase-compatible)
239
+ * Returns session when email confirmation is disabled
240
+ * Returns null session when email confirmation is required
239
241
  */
240
242
  async signUp(credentials) {
241
243
  return wrapAsync(async () => {
@@ -243,12 +245,15 @@ var FluxbaseAuth = class {
243
245
  "/api/v1/auth/signup",
244
246
  credentials
245
247
  );
246
- const session = {
247
- ...response,
248
- expires_at: Date.now() + response.expires_in * 1e3
249
- };
250
- this.setSessionInternal(session);
251
- return { user: session.user, session };
248
+ if (response.access_token && response.refresh_token) {
249
+ const session = {
250
+ ...response,
251
+ expires_at: Date.now() + response.expires_in * 1e3
252
+ };
253
+ this.setSessionInternal(session);
254
+ return { user: response.user, session };
255
+ }
256
+ return { user: response.user, session: null };
252
257
  });
253
258
  }
254
259
  /**
@@ -283,7 +288,7 @@ var FluxbaseAuth = class {
283
288
  expires_at: Date.now() + response.expires_in * 1e3
284
289
  };
285
290
  this.setSessionInternal(session, "TOKEN_REFRESHED");
286
- return { session, user: session.user };
291
+ return { user: session.user, session };
287
292
  });
288
293
  }
289
294
  /**
@@ -340,8 +345,9 @@ var FluxbaseAuth = class {
340
345
  });
341
346
  }
342
347
  /**
343
- * Setup 2FA for the current user
344
- * Returns TOTP secret and QR code URL
348
+ * Setup 2FA for the current user (Supabase-compatible)
349
+ * Enrolls a new MFA factor and returns TOTP details
350
+ * @returns Promise with factor id, type, and TOTP setup details
345
351
  */
346
352
  async setup2FA() {
347
353
  return wrapAsync(async () => {
@@ -354,8 +360,10 @@ var FluxbaseAuth = class {
354
360
  });
355
361
  }
356
362
  /**
357
- * Enable 2FA after verifying the TOTP code
358
- * Returns backup codes that should be saved by the user
363
+ * Enable 2FA after verifying the TOTP code (Supabase-compatible)
364
+ * Verifies the TOTP code and returns new tokens with MFA session
365
+ * @param code - TOTP code from authenticator app
366
+ * @returns Promise with access_token, refresh_token, and user
359
367
  */
360
368
  async enable2FA(code) {
361
369
  return wrapAsync(async () => {
@@ -369,8 +377,10 @@ var FluxbaseAuth = class {
369
377
  });
370
378
  }
371
379
  /**
372
- * Disable 2FA for the current user
373
- * Requires password confirmation
380
+ * Disable 2FA for the current user (Supabase-compatible)
381
+ * Unenrolls the MFA factor
382
+ * @param password - User password for confirmation
383
+ * @returns Promise with unenrolled factor id
374
384
  */
375
385
  async disable2FA(password) {
376
386
  return wrapAsync(async () => {
@@ -384,7 +394,9 @@ var FluxbaseAuth = class {
384
394
  });
385
395
  }
386
396
  /**
387
- * Check 2FA status for the current user
397
+ * Check 2FA status for the current user (Supabase-compatible)
398
+ * Lists all enrolled MFA factors
399
+ * @returns Promise with all factors and TOTP factors
388
400
  */
389
401
  async get2FAStatus() {
390
402
  return wrapAsync(async () => {
@@ -397,8 +409,10 @@ var FluxbaseAuth = class {
397
409
  });
398
410
  }
399
411
  /**
400
- * Verify 2FA code during login
412
+ * Verify 2FA code during login (Supabase-compatible)
401
413
  * Call this after signIn returns requires_2fa: true
414
+ * @param request - User ID and TOTP code
415
+ * @returns Promise with access_token, refresh_token, and user
402
416
  */
403
417
  async verify2FA(request) {
404
418
  return wrapAsync(async () => {
@@ -406,31 +420,36 @@ var FluxbaseAuth = class {
406
420
  "/api/v1/auth/2fa/verify",
407
421
  request
408
422
  );
409
- const session = {
410
- ...response,
411
- expires_at: Date.now() + response.expires_in * 1e3
412
- };
413
- this.setSessionInternal(session, "MFA_CHALLENGE_VERIFIED");
414
- return { user: session.user, session };
423
+ if (response.access_token && response.refresh_token) {
424
+ const session = {
425
+ user: response.user,
426
+ access_token: response.access_token,
427
+ refresh_token: response.refresh_token,
428
+ expires_in: response.expires_in || 3600,
429
+ expires_at: Date.now() + (response.expires_in || 3600) * 1e3
430
+ };
431
+ this.setSessionInternal(session, "MFA_CHALLENGE_VERIFIED");
432
+ }
433
+ return response;
415
434
  });
416
435
  }
417
436
  /**
418
- * Send password reset email
437
+ * Send password reset email (Supabase-compatible)
419
438
  * Sends a password reset link to the provided email address
420
439
  * @param email - Email address to send reset link to
440
+ * @returns Promise with OTP-style response
421
441
  */
422
442
  async sendPasswordReset(email) {
423
443
  return wrapAsync(async () => {
424
- return await this.fetch.post(
425
- "/api/v1/auth/password/reset",
426
- { email }
427
- );
444
+ await this.fetch.post("/api/v1/auth/password/reset", { email });
445
+ return { user: null, session: null };
428
446
  });
429
447
  }
430
448
  /**
431
449
  * Supabase-compatible alias for sendPasswordReset()
432
450
  * @param email - Email address to send reset link to
433
451
  * @param _options - Optional redirect configuration (currently not used)
452
+ * @returns Promise with OTP-style response
434
453
  */
435
454
  async resetPasswordForEmail(email, _options) {
436
455
  return this.sendPasswordReset(email);
@@ -451,36 +470,42 @@ var FluxbaseAuth = class {
451
470
  });
452
471
  }
453
472
  /**
454
- * Reset password with token
473
+ * Reset password with token (Supabase-compatible)
455
474
  * Complete the password reset process with a valid token
456
475
  * @param token - Password reset token
457
476
  * @param newPassword - New password to set
477
+ * @returns Promise with user and new session
458
478
  */
459
479
  async resetPassword(token, newPassword) {
460
480
  return wrapAsync(async () => {
461
- return await this.fetch.post(
481
+ const response = await this.fetch.post(
462
482
  "/api/v1/auth/password/reset/confirm",
463
483
  {
464
484
  token,
465
485
  new_password: newPassword
466
486
  }
467
487
  );
488
+ const session = {
489
+ ...response,
490
+ expires_at: Date.now() + response.expires_in * 1e3
491
+ };
492
+ this.setSessionInternal(session, "PASSWORD_RECOVERY");
493
+ return { user: session.user, session };
468
494
  });
469
495
  }
470
496
  /**
471
- * Send magic link for passwordless authentication
497
+ * Send magic link for passwordless authentication (Supabase-compatible)
472
498
  * @param email - Email address to send magic link to
473
499
  * @param options - Optional configuration for magic link
500
+ * @returns Promise with OTP-style response
474
501
  */
475
502
  async sendMagicLink(email, options) {
476
503
  return wrapAsync(async () => {
477
- return await this.fetch.post(
478
- "/api/v1/auth/magiclink",
479
- {
480
- email,
481
- redirect_to: options?.redirect_to
482
- }
483
- );
504
+ await this.fetch.post("/api/v1/auth/magiclink", {
505
+ email,
506
+ redirect_to: options?.redirect_to
507
+ });
508
+ return { user: null, session: null };
484
509
  });
485
510
  }
486
511
  /**
@@ -663,10 +688,14 @@ var FluxbaseAuth = class {
663
688
 
664
689
  // src/realtime.ts
665
690
  var RealtimeChannel = class {
666
- constructor(url, channelName, token = null) {
691
+ constructor(url, channelName, token = null, config = {}) {
667
692
  this.ws = null;
668
693
  this.callbacks = /* @__PURE__ */ new Map();
694
+ this.presenceCallbacks = /* @__PURE__ */ new Map();
695
+ this.broadcastCallbacks = /* @__PURE__ */ new Map();
669
696
  this.subscriptionConfig = null;
697
+ this._presenceState = {};
698
+ this.myPresenceKey = null;
670
699
  this.reconnectAttempts = 0;
671
700
  this.maxReconnectAttempts = 10;
672
701
  this.reconnectDelay = 1e3;
@@ -674,6 +703,7 @@ var RealtimeChannel = class {
674
703
  this.url = url;
675
704
  this.channelName = channelName;
676
705
  this.token = token;
706
+ this.config = config;
677
707
  }
678
708
  // Implementation
679
709
  on(event, configOrCallback, callback) {
@@ -686,6 +716,20 @@ var RealtimeChannel = class {
686
716
  this.callbacks.set(eventType, /* @__PURE__ */ new Set());
687
717
  }
688
718
  this.callbacks.get(eventType).add(actualCallback);
719
+ } else if (event === "broadcast" && typeof configOrCallback !== "function") {
720
+ const config = configOrCallback;
721
+ const actualCallback = callback;
722
+ if (!this.broadcastCallbacks.has(config.event)) {
723
+ this.broadcastCallbacks.set(config.event, /* @__PURE__ */ new Set());
724
+ }
725
+ this.broadcastCallbacks.get(config.event).add(actualCallback);
726
+ } else if (event === "presence" && typeof configOrCallback !== "function") {
727
+ const config = configOrCallback;
728
+ const actualCallback = callback;
729
+ if (!this.presenceCallbacks.has(config.event)) {
730
+ this.presenceCallbacks.set(config.event, /* @__PURE__ */ new Set());
731
+ }
732
+ this.presenceCallbacks.get(config.event).add(actualCallback);
689
733
  } else {
690
734
  const actualEvent = event;
691
735
  const actualCallback = configOrCallback;
@@ -735,7 +779,7 @@ var RealtimeChannel = class {
735
779
  async unsubscribe(timeout) {
736
780
  return new Promise((resolve) => {
737
781
  if (this.ws) {
738
- this.send({
782
+ this.sendMessage({
739
783
  type: "unsubscribe",
740
784
  channel: this.channelName
741
785
  });
@@ -758,6 +802,131 @@ var RealtimeChannel = class {
758
802
  }
759
803
  });
760
804
  }
805
+ /**
806
+ * Send a broadcast message to all subscribers on this channel
807
+ *
808
+ * @param message - Broadcast message with type, event, and payload
809
+ * @returns Promise resolving to status
810
+ *
811
+ * @example
812
+ * ```typescript
813
+ * await channel.send({
814
+ * type: 'broadcast',
815
+ * event: 'cursor-pos',
816
+ * payload: { x: 100, y: 200 }
817
+ * })
818
+ * ```
819
+ */
820
+ async send(message) {
821
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
822
+ return "error";
823
+ }
824
+ try {
825
+ this.ws.send(
826
+ JSON.stringify({
827
+ type: "broadcast",
828
+ channel: this.channelName,
829
+ event: message.event,
830
+ payload: message.payload
831
+ })
832
+ );
833
+ if (this.config.broadcast?.ack) {
834
+ return "ok";
835
+ }
836
+ return "ok";
837
+ } catch (error) {
838
+ console.error("[Fluxbase Realtime] Failed to send broadcast:", error);
839
+ return "error";
840
+ }
841
+ }
842
+ /**
843
+ * Track user presence on this channel
844
+ *
845
+ * @param state - Presence state to track
846
+ * @returns Promise resolving to status
847
+ *
848
+ * @example
849
+ * ```typescript
850
+ * await channel.track({
851
+ * user_id: 123,
852
+ * status: 'online'
853
+ * })
854
+ * ```
855
+ */
856
+ async track(state) {
857
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
858
+ return "error";
859
+ }
860
+ try {
861
+ if (!this.myPresenceKey) {
862
+ this.myPresenceKey = this.config.presence?.key || `presence-${Math.random().toString(36).substr(2, 9)}`;
863
+ }
864
+ this.ws.send(
865
+ JSON.stringify({
866
+ type: "presence",
867
+ channel: this.channelName,
868
+ event: "track",
869
+ payload: {
870
+ key: this.myPresenceKey,
871
+ state
872
+ }
873
+ })
874
+ );
875
+ return "ok";
876
+ } catch (error) {
877
+ console.error("[Fluxbase Realtime] Failed to track presence:", error);
878
+ return "error";
879
+ }
880
+ }
881
+ /**
882
+ * Stop tracking presence on this channel
883
+ *
884
+ * @returns Promise resolving to status
885
+ *
886
+ * @example
887
+ * ```typescript
888
+ * await channel.untrack()
889
+ * ```
890
+ */
891
+ async untrack() {
892
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
893
+ return "error";
894
+ }
895
+ if (!this.myPresenceKey) {
896
+ return "ok";
897
+ }
898
+ try {
899
+ this.ws.send(
900
+ JSON.stringify({
901
+ type: "presence",
902
+ channel: this.channelName,
903
+ event: "untrack",
904
+ payload: {
905
+ key: this.myPresenceKey
906
+ }
907
+ })
908
+ );
909
+ this.myPresenceKey = null;
910
+ return "ok";
911
+ } catch (error) {
912
+ console.error("[Fluxbase Realtime] Failed to untrack presence:", error);
913
+ return "error";
914
+ }
915
+ }
916
+ /**
917
+ * Get current presence state for all users on this channel
918
+ *
919
+ * @returns Current presence state
920
+ *
921
+ * @example
922
+ * ```typescript
923
+ * const state = channel.presenceState()
924
+ * console.log('Online users:', Object.keys(state).length)
925
+ * ```
926
+ */
927
+ presenceState() {
928
+ return { ...this._presenceState };
929
+ }
761
930
  /**
762
931
  * Internal: Connect to WebSocket
763
932
  */
@@ -782,7 +951,7 @@ var RealtimeChannel = class {
782
951
  if (this.subscriptionConfig) {
783
952
  subscribeMessage.config = this.subscriptionConfig;
784
953
  }
785
- this.send(subscribeMessage);
954
+ this.sendMessage(subscribeMessage);
786
955
  this.startHeartbeat();
787
956
  };
788
957
  this.ws.onmessage = (event) => {
@@ -815,7 +984,7 @@ var RealtimeChannel = class {
815
984
  /**
816
985
  * Internal: Send a message
817
986
  */
818
- send(message) {
987
+ sendMessage(message) {
819
988
  if (this.ws && this.ws.readyState === WebSocket.OPEN) {
820
989
  this.ws.send(JSON.stringify(message));
821
990
  }
@@ -826,11 +995,18 @@ var RealtimeChannel = class {
826
995
  handleMessage(message) {
827
996
  switch (message.type) {
828
997
  case "heartbeat":
829
- this.send({ type: "heartbeat" });
998
+ this.ws?.send(JSON.stringify({ type: "heartbeat" }));
830
999
  break;
831
1000
  case "broadcast":
832
- if (message.payload) {
833
- this.handleBroadcast(message.payload);
1001
+ if (message.broadcast) {
1002
+ this.handleBroadcastMessage(message.broadcast);
1003
+ } else if (message.payload) {
1004
+ this.handlePostgresChanges(message.payload);
1005
+ }
1006
+ break;
1007
+ case "presence":
1008
+ if (message.presence) {
1009
+ this.handlePresenceMessage(message.presence);
834
1010
  }
835
1011
  break;
836
1012
  case "ack":
@@ -844,7 +1020,48 @@ var RealtimeChannel = class {
844
1020
  /**
845
1021
  * Internal: Handle broadcast message
846
1022
  */
847
- handleBroadcast(payload) {
1023
+ handleBroadcastMessage(message) {
1024
+ const event = message.event;
1025
+ const payload = {
1026
+ event,
1027
+ payload: message.payload
1028
+ };
1029
+ if (!this.config.broadcast?.self && message.self) {
1030
+ return;
1031
+ }
1032
+ const callbacks = this.broadcastCallbacks.get(event);
1033
+ if (callbacks) {
1034
+ callbacks.forEach((callback) => callback(payload));
1035
+ }
1036
+ const wildcardCallbacks = this.broadcastCallbacks.get("*");
1037
+ if (wildcardCallbacks) {
1038
+ wildcardCallbacks.forEach((callback) => callback(payload));
1039
+ }
1040
+ }
1041
+ /**
1042
+ * Internal: Handle presence message
1043
+ */
1044
+ handlePresenceMessage(message) {
1045
+ const event = message.event;
1046
+ const payload = {
1047
+ event,
1048
+ key: message.key,
1049
+ newPresences: message.newPresences,
1050
+ leftPresences: message.leftPresences,
1051
+ currentPresences: message.currentPresences || this._presenceState
1052
+ };
1053
+ if (message.currentPresences) {
1054
+ this._presenceState = message.currentPresences;
1055
+ }
1056
+ const callbacks = this.presenceCallbacks.get(event);
1057
+ if (callbacks) {
1058
+ callbacks.forEach((callback) => callback(payload));
1059
+ }
1060
+ }
1061
+ /**
1062
+ * Internal: Handle postgres_changes message
1063
+ */
1064
+ handlePostgresChanges(payload) {
848
1065
  const supabasePayload = {
849
1066
  eventType: payload.type || payload.eventType,
850
1067
  schema: payload.schema,
@@ -868,7 +1085,7 @@ var RealtimeChannel = class {
868
1085
  */
869
1086
  startHeartbeat() {
870
1087
  this.heartbeatInterval = setInterval(() => {
871
- this.send({ type: "heartbeat" });
1088
+ this.sendMessage({ type: "heartbeat" });
872
1089
  }, 3e4);
873
1090
  }
874
1091
  /**
@@ -905,17 +1122,57 @@ var FluxbaseRealtime = class {
905
1122
  this.token = token;
906
1123
  }
907
1124
  /**
908
- * Create or get a channel
1125
+ * Create or get a channel with optional configuration
1126
+ *
909
1127
  * @param channelName - Channel name (e.g., 'table:public.products')
1128
+ * @param config - Optional channel configuration
1129
+ * @returns RealtimeChannel instance
1130
+ *
1131
+ * @example
1132
+ * ```typescript
1133
+ * const channel = realtime.channel('room-1', {
1134
+ * broadcast: { self: true, ack: true },
1135
+ * presence: { key: 'user-123' }
1136
+ * })
1137
+ * ```
910
1138
  */
911
- channel(channelName) {
912
- if (this.channels.has(channelName)) {
913
- return this.channels.get(channelName);
1139
+ channel(channelName, config) {
1140
+ const configKey = config ? JSON.stringify(config) : "";
1141
+ const key = `${channelName}:${configKey}`;
1142
+ if (this.channels.has(key)) {
1143
+ return this.channels.get(key);
914
1144
  }
915
- const channel = new RealtimeChannel(this.url, channelName, this.token);
916
- this.channels.set(channelName, channel);
1145
+ const channel = new RealtimeChannel(
1146
+ this.url,
1147
+ channelName,
1148
+ this.token,
1149
+ config
1150
+ );
1151
+ this.channels.set(key, channel);
917
1152
  return channel;
918
1153
  }
1154
+ /**
1155
+ * Remove a specific channel
1156
+ *
1157
+ * @param channel - The channel to remove
1158
+ * @returns Promise resolving to status
1159
+ *
1160
+ * @example
1161
+ * ```typescript
1162
+ * const channel = realtime.channel('room-1')
1163
+ * await realtime.removeChannel(channel)
1164
+ * ```
1165
+ */
1166
+ async removeChannel(channel) {
1167
+ await channel.unsubscribe();
1168
+ for (const [key, ch] of this.channels.entries()) {
1169
+ if (ch === channel) {
1170
+ this.channels.delete(key);
1171
+ return "ok";
1172
+ }
1173
+ }
1174
+ return "error";
1175
+ }
919
1176
  /**
920
1177
  * Remove all channels
921
1178
  */
@@ -925,8 +1182,9 @@ var FluxbaseRealtime = class {
925
1182
  }
926
1183
  /**
927
1184
  * Update auth token for all channels
1185
+ * @param token - The new auth token
928
1186
  */
929
- setToken(token) {
1187
+ setAuth(token) {
930
1188
  this.token = token;
931
1189
  }
932
1190
  };
@@ -4363,7 +4621,7 @@ var FluxbaseClient = class {
4363
4621
  const originalSetAuthToken = this.fetch.setAuthToken.bind(this.fetch);
4364
4622
  this.fetch.setAuthToken = (token) => {
4365
4623
  originalSetAuthToken(token);
4366
- this.realtime.setToken(token);
4624
+ this.realtime.setAuth(token);
4367
4625
  };
4368
4626
  }
4369
4627
  /**
@@ -4387,37 +4645,48 @@ var FluxbaseClient = class {
4387
4645
  */
4388
4646
  setAuthToken(token) {
4389
4647
  this.fetch.setAuthToken(token);
4390
- this.realtime.setToken(token);
4648
+ this.realtime.setAuth(token);
4391
4649
  }
4392
4650
  /**
4393
- * Create or get a realtime channel (Supabase-compatible alias)
4394
- *
4395
- * This is a convenience method that delegates to client.realtime.channel().
4396
- * Both patterns work identically:
4397
- * - client.channel('room-1') - Supabase-style
4398
- * - client.realtime.channel('room-1') - Fluxbase-style
4651
+ * Create or get a realtime channel (Supabase-compatible)
4399
4652
  *
4400
4653
  * @param name - Channel name
4654
+ * @param config - Optional channel configuration
4401
4655
  * @returns RealtimeChannel instance
4402
4656
  *
4403
4657
  * @example
4404
4658
  * ```typescript
4405
- * // Supabase-compatible usage
4406
- * const channel = client.channel('room-1')
4407
- * .on('postgres_changes', {
4408
- * event: '*',
4409
- * schema: 'public',
4410
- * table: 'messages'
4411
- * }, (payload) => {
4412
- * console.log('Change:', payload)
4659
+ * const channel = client.channel('room-1', {
4660
+ * broadcast: { self: true },
4661
+ * presence: { key: 'user-123' }
4662
+ * })
4663
+ * .on('broadcast', { event: 'message' }, (payload) => {
4664
+ * console.log('Message:', payload)
4413
4665
  * })
4414
4666
  * .subscribe()
4415
4667
  * ```
4416
4668
  *
4417
4669
  * @category Realtime
4418
4670
  */
4419
- channel(name) {
4420
- return this.realtime.channel(name);
4671
+ channel(name, config) {
4672
+ return this.realtime.channel(name, config);
4673
+ }
4674
+ /**
4675
+ * Remove a realtime channel (Supabase-compatible)
4676
+ *
4677
+ * @param channel - The channel to remove
4678
+ * @returns Promise resolving to status
4679
+ *
4680
+ * @example
4681
+ * ```typescript
4682
+ * const channel = client.channel('room-1')
4683
+ * await client.removeChannel(channel)
4684
+ * ```
4685
+ *
4686
+ * @category Realtime
4687
+ */
4688
+ removeChannel(channel) {
4689
+ return this.realtime.removeChannel(channel);
4421
4690
  }
4422
4691
  /**
4423
4692
  * Get the internal HTTP client