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