@scalemule/nextjs 0.0.31 → 0.0.33

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.d.mts CHANGED
@@ -1,5 +1,6 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import { ReactNode } from 'react';
3
+ import { RealtimeService } from '@scalemule/sdk';
3
4
  import { ScaleMuleClient } from './client.mjs';
4
5
  export { ClientConfig, RequestOptions, createClient } from './client.mjs';
5
6
  import { S as ScaleMuleConfig, U as User, L as LoginResponse, A as ApiError, a as UseAuthReturn, b as UseBillingReturn, c as ListFilesParams, d as UseContentReturn, e as UseUserReturn, f as UseAnalyticsOptions, g as UseAnalyticsReturn } from './index-BoENfro3.mjs';
@@ -8,6 +9,8 @@ export { h as AccountBalance, i as AnalyticsEvent, j as ApiResponse, B as BatchT
8
9
  interface ScaleMuleContextValue {
9
10
  /** The API client instance */
10
11
  client: ScaleMuleClient;
12
+ /** Base SDK realtime service — shared singleton for WebSocket connections */
13
+ realtime: RealtimeService;
11
14
  /** Current authenticated user */
12
15
  user: User | null;
13
16
  /** Set the current user */
@@ -154,58 +157,45 @@ interface RealtimeMessage<T = unknown> {
154
157
  }
155
158
  type RealtimeStatus = 'disconnected' | 'connecting' | 'connected' | 'reconnecting';
156
159
  interface UseRealtimeOptions {
157
- /** Auto-connect on mount (default: true) */
160
+ /** Channels to subscribe to */
161
+ channels?: string[];
162
+ /** Called when a message arrives on any subscribed channel */
163
+ onMessage?: (channel: string, data: unknown) => void;
164
+ /** Auto-connect on mount (default: true) — subscribing auto-connects */
158
165
  autoConnect?: boolean;
159
- /** Events to subscribe to (default: all) */
160
- events?: RealtimeEvent[];
161
- /** Reconnect on disconnect (default: true) */
162
- autoReconnect?: boolean;
163
- /** Max reconnect attempts (default: 5) */
164
- maxReconnectAttempts?: number;
165
- /** Reconnect delay in ms (default: 1000, doubles each attempt) */
166
- reconnectDelay?: number;
167
166
  }
168
167
  interface UseRealtimeReturn {
169
168
  /** Current connection status */
170
169
  status: RealtimeStatus;
171
- /** Last error */
172
- error: ApiError | null;
173
- /** Connect to realtime */
174
- connect: () => void;
175
- /** Disconnect from realtime */
176
- disconnect: () => void;
177
- /** Subscribe to an event */
178
- subscribe: <T>(event: RealtimeEvent, callback: (data: T) => void) => () => void;
179
- /** Send a message (if supported) */
180
- send: (event: string, data: unknown) => void;
181
170
  /** Last received message */
182
- lastMessage: RealtimeMessage | null;
171
+ lastMessage: {
172
+ channel: string;
173
+ data: unknown;
174
+ } | null;
175
+ /** Manually disconnect */
176
+ disconnect: () => void;
177
+ /** Subscribe to an additional channel (auto-connects) */
178
+ subscribe: (channel: string, callback?: (data: unknown) => void) => () => void;
179
+ /** Publish data to a channel */
180
+ publish: (channel: string, data: unknown) => void;
183
181
  }
184
182
  /**
185
- * Real-time updates hook via WebSocket
183
+ * Real-time updates hook via WebSocket.
186
184
  *
187
- * Provides live updates for user state, files, and notifications.
185
+ * Uses the base SDK's RealtimeService (shared singleton created in the provider)
186
+ * to ensure correct protocol handling and single WebSocket connection per page.
188
187
  *
189
188
  * @example
190
189
  * ```tsx
191
- * function Dashboard() {
192
- * const { status, subscribe } = useRealtime()
193
- *
194
- * useEffect(() => {
195
- * // Subscribe to file uploads
196
- * const unsubscribe = subscribe('file.uploaded', (data) => {
197
- * console.log('New file uploaded:', data)
198
- * refreshFiles()
199
- * })
200
- *
201
- * return () => unsubscribe()
202
- * }, [subscribe])
190
+ * function ChatNotifications() {
191
+ * const { status, lastMessage } = useRealtime({
192
+ * channels: ['chat:room-1', 'notifications'],
193
+ * onMessage: (channel, data) => {
194
+ * console.log(`${channel}:`, data)
195
+ * },
196
+ * })
203
197
  *
204
- * return (
205
- * <div>
206
- * <span>Status: {status}</span>
207
- * </div>
208
- * )
198
+ * return <div>Status: {status}</div>
209
199
  * }
210
200
  * ```
211
201
  */
@@ -294,6 +284,8 @@ interface UsePushNotificationsOptions {
294
284
  serviceWorkerUrl?: string;
295
285
  /** Push proxy URL (default: '/api/push') */
296
286
  pushProxyUrl?: string;
287
+ /** Where the user subscribed (e.g., 'landing_prompt', 'post_signup', 'settings') */
288
+ registrationSource?: string;
297
289
  /** Called when a push notification is received while app is in foreground */
298
290
  onNotification?: (data: unknown) => void;
299
291
  }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import { ReactNode } from 'react';
3
+ import { RealtimeService } from '@scalemule/sdk';
3
4
  import { ScaleMuleClient } from './client.js';
4
5
  export { ClientConfig, RequestOptions, createClient } from './client.js';
5
6
  import { S as ScaleMuleConfig, U as User, L as LoginResponse, A as ApiError, a as UseAuthReturn, b as UseBillingReturn, c as ListFilesParams, d as UseContentReturn, e as UseUserReturn, f as UseAnalyticsOptions, g as UseAnalyticsReturn } from './index-BoENfro3.js';
@@ -8,6 +9,8 @@ export { h as AccountBalance, i as AnalyticsEvent, j as ApiResponse, B as BatchT
8
9
  interface ScaleMuleContextValue {
9
10
  /** The API client instance */
10
11
  client: ScaleMuleClient;
12
+ /** Base SDK realtime service — shared singleton for WebSocket connections */
13
+ realtime: RealtimeService;
11
14
  /** Current authenticated user */
12
15
  user: User | null;
13
16
  /** Set the current user */
@@ -154,58 +157,45 @@ interface RealtimeMessage<T = unknown> {
154
157
  }
155
158
  type RealtimeStatus = 'disconnected' | 'connecting' | 'connected' | 'reconnecting';
156
159
  interface UseRealtimeOptions {
157
- /** Auto-connect on mount (default: true) */
160
+ /** Channels to subscribe to */
161
+ channels?: string[];
162
+ /** Called when a message arrives on any subscribed channel */
163
+ onMessage?: (channel: string, data: unknown) => void;
164
+ /** Auto-connect on mount (default: true) — subscribing auto-connects */
158
165
  autoConnect?: boolean;
159
- /** Events to subscribe to (default: all) */
160
- events?: RealtimeEvent[];
161
- /** Reconnect on disconnect (default: true) */
162
- autoReconnect?: boolean;
163
- /** Max reconnect attempts (default: 5) */
164
- maxReconnectAttempts?: number;
165
- /** Reconnect delay in ms (default: 1000, doubles each attempt) */
166
- reconnectDelay?: number;
167
166
  }
168
167
  interface UseRealtimeReturn {
169
168
  /** Current connection status */
170
169
  status: RealtimeStatus;
171
- /** Last error */
172
- error: ApiError | null;
173
- /** Connect to realtime */
174
- connect: () => void;
175
- /** Disconnect from realtime */
176
- disconnect: () => void;
177
- /** Subscribe to an event */
178
- subscribe: <T>(event: RealtimeEvent, callback: (data: T) => void) => () => void;
179
- /** Send a message (if supported) */
180
- send: (event: string, data: unknown) => void;
181
170
  /** Last received message */
182
- lastMessage: RealtimeMessage | null;
171
+ lastMessage: {
172
+ channel: string;
173
+ data: unknown;
174
+ } | null;
175
+ /** Manually disconnect */
176
+ disconnect: () => void;
177
+ /** Subscribe to an additional channel (auto-connects) */
178
+ subscribe: (channel: string, callback?: (data: unknown) => void) => () => void;
179
+ /** Publish data to a channel */
180
+ publish: (channel: string, data: unknown) => void;
183
181
  }
184
182
  /**
185
- * Real-time updates hook via WebSocket
183
+ * Real-time updates hook via WebSocket.
186
184
  *
187
- * Provides live updates for user state, files, and notifications.
185
+ * Uses the base SDK's RealtimeService (shared singleton created in the provider)
186
+ * to ensure correct protocol handling and single WebSocket connection per page.
188
187
  *
189
188
  * @example
190
189
  * ```tsx
191
- * function Dashboard() {
192
- * const { status, subscribe } = useRealtime()
193
- *
194
- * useEffect(() => {
195
- * // Subscribe to file uploads
196
- * const unsubscribe = subscribe('file.uploaded', (data) => {
197
- * console.log('New file uploaded:', data)
198
- * refreshFiles()
199
- * })
200
- *
201
- * return () => unsubscribe()
202
- * }, [subscribe])
190
+ * function ChatNotifications() {
191
+ * const { status, lastMessage } = useRealtime({
192
+ * channels: ['chat:room-1', 'notifications'],
193
+ * onMessage: (channel, data) => {
194
+ * console.log(`${channel}:`, data)
195
+ * },
196
+ * })
203
197
  *
204
- * return (
205
- * <div>
206
- * <span>Status: {status}</span>
207
- * </div>
208
- * )
198
+ * return <div>Status: {status}</div>
209
199
  * }
210
200
  * ```
211
201
  */
@@ -294,6 +284,8 @@ interface UsePushNotificationsOptions {
294
284
  serviceWorkerUrl?: string;
295
285
  /** Push proxy URL (default: '/api/push') */
296
286
  pushProxyUrl?: string;
287
+ /** Where the user subscribed (e.g., 'landing_prompt', 'post_signup', 'settings') */
288
+ registrationSource?: string;
297
289
  /** Called when a push notification is received while app is in foreground */
298
290
  onNotification?: (data: unknown) => void;
299
291
  }
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  'use strict';
2
2
 
3
3
  var react = require('react');
4
- var jsxRuntime = require('react/jsx-runtime');
5
4
  var sdk = require('@scalemule/sdk');
5
+ var jsxRuntime = require('react/jsx-runtime');
6
6
 
7
7
  // src/provider.tsx
8
8
 
@@ -836,6 +836,24 @@ function ScaleMuleProvider({
836
836
  }),
837
837
  [apiKey, applicationId, environment, gatewayUrl, debug, storage, authProxyUrl]
838
838
  );
839
+ const baseClient = react.useMemo(() => {
840
+ const resolvedGatewayUrl = gatewayUrl || (environment === "dev" ? "https://api-dev.scalemule.com" : "https://api.scalemule.com");
841
+ return new sdk.ScaleMule({
842
+ apiKey,
843
+ applicationId,
844
+ baseUrl: resolvedGatewayUrl,
845
+ environment,
846
+ debug
847
+ });
848
+ }, [apiKey, applicationId, environment, gatewayUrl, debug]);
849
+ react.useEffect(() => {
850
+ const token = client.getSessionToken();
851
+ if (token) {
852
+ baseClient.setAccessToken(token);
853
+ } else {
854
+ baseClient.clearAccessToken();
855
+ }
856
+ }, [client, baseClient, user]);
839
857
  react.useEffect(() => {
840
858
  let mounted = true;
841
859
  async function initialize() {
@@ -923,6 +941,7 @@ function ScaleMuleProvider({
923
941
  const value = react.useMemo(
924
942
  () => ({
925
943
  client,
944
+ realtime: baseClient.realtime,
926
945
  user,
927
946
  setUser: handleSetUser,
928
947
  initializing,
@@ -935,7 +954,7 @@ function ScaleMuleProvider({
935
954
  environment: environment || void 0,
936
955
  bootstrapFlags
937
956
  }),
938
- [client, user, handleSetUser, initializing, error, analyticsProxyUrl, authProxyUrl, publishableKey, gatewayUrl, environment, bootstrapFlags]
957
+ [client, baseClient, user, handleSetUser, initializing, error, analyticsProxyUrl, authProxyUrl, publishableKey, gatewayUrl, environment, bootstrapFlags]
939
958
  );
940
959
  return /* @__PURE__ */ jsxRuntime.jsx(ScaleMuleContext.Provider, { value, children });
941
960
  }
@@ -2231,187 +2250,79 @@ function useUser() {
2231
2250
  [user, loading, localError, update, changePassword, changeEmail, deleteAccount, exportData]
2232
2251
  );
2233
2252
  }
2234
- function useRealtime(options = {}) {
2235
- const {
2236
- autoConnect = true,
2237
- events,
2238
- autoReconnect = true,
2239
- maxReconnectAttempts = 5,
2240
- reconnectDelay = 1e3
2241
- } = options;
2242
- const { client, user, setUser } = useScaleMule();
2253
+ function useRealtime(options) {
2254
+ const { realtime } = useScaleMule();
2243
2255
  const [status, setStatus] = react.useState("disconnected");
2244
- const [error, setError] = react.useState(null);
2245
2256
  const [lastMessage, setLastMessage] = react.useState(null);
2246
- const wsRef = react.useRef(null);
2247
- const reconnectAttempts = react.useRef(0);
2248
- const reconnectTimeout = react.useRef(null);
2249
- const subscribersRef = react.useRef(/* @__PURE__ */ new Map());
2250
- const getWebSocketUrl = react.useCallback(() => {
2251
- const gatewayUrl = client.getGatewayUrl();
2252
- const wsUrl = gatewayUrl.replace(/^https?:\/\//, "wss://").replace(/^http:\/\//, "ws://");
2253
- return `${wsUrl}/v1/realtime`;
2254
- }, [client]);
2255
- const handleMessage = react.useCallback(
2256
- (event) => {
2257
- try {
2258
- const message = JSON.parse(event.data);
2259
- setLastMessage(message);
2260
- switch (message.event) {
2261
- case "user.updated":
2262
- if (user && message.data.id === user.id) {
2263
- setUser(message.data);
2264
- }
2265
- break;
2266
- case "session.expired":
2267
- client.clearSession();
2268
- setUser(null);
2269
- break;
2270
- }
2271
- const subscribers = subscribersRef.current.get(message.event);
2272
- if (subscribers) {
2273
- subscribers.forEach((callback) => callback(message.data));
2274
- }
2275
- const wildcardSubscribers = subscribersRef.current.get("*");
2276
- if (wildcardSubscribers) {
2277
- wildcardSubscribers.forEach((callback) => callback(message));
2278
- }
2279
- } catch (err) {
2280
- console.error("[ScaleMule Realtime] Failed to parse message:", err);
2281
- }
2282
- },
2283
- [client, user, setUser]
2284
- );
2285
- const connect = react.useCallback(() => {
2286
- if (wsRef.current?.readyState === WebSocket.OPEN) {
2287
- return;
2288
- }
2289
- if (!user) {
2290
- setError({ code: "NOT_AUTHENTICATED", message: "Must be logged in to connect" });
2291
- return;
2292
- }
2293
- const applicationId = client.getApplicationId();
2294
- if (!applicationId) {
2295
- setError({ code: "MISSING_APP_ID", message: "applicationId is required for realtime features. Add it to your ScaleMuleProvider config." });
2296
- return;
2297
- }
2298
- setStatus("connecting");
2299
- setError(null);
2300
- const url = getWebSocketUrl();
2301
- try {
2302
- const ws = new WebSocket(url);
2303
- ws.onopen = () => {
2304
- const sessionToken = client.getSessionToken();
2305
- ws.send(JSON.stringify({
2306
- type: "auth",
2307
- token: sessionToken,
2308
- app_id: applicationId
2309
- }));
2310
- };
2311
- ws.onmessage = (event) => {
2312
- try {
2313
- const message = JSON.parse(event.data);
2314
- if (message.type === "auth_success") {
2315
- setStatus("connected");
2316
- reconnectAttempts.current = 0;
2317
- if (events && events.length > 0) {
2318
- ws.send(JSON.stringify({ type: "subscribe", events }));
2319
- }
2320
- return;
2321
- }
2322
- if (message.type === "error") {
2323
- setError({ code: "AUTH_ERROR", message: message.message || "Authentication failed" });
2324
- setStatus("disconnected");
2325
- ws.close(1e3);
2326
- return;
2327
- }
2328
- handleMessage(event);
2329
- } catch (err) {
2330
- console.error("[ScaleMule Realtime] Failed to parse message:", err);
2331
- }
2332
- };
2333
- ws.onerror = () => {
2334
- setError({ code: "WEBSOCKET_ERROR", message: "Connection error" });
2335
- };
2336
- ws.onclose = (event) => {
2337
- setStatus("disconnected");
2338
- wsRef.current = null;
2339
- if (autoReconnect && event.code !== 1e3 && reconnectAttempts.current < maxReconnectAttempts) {
2340
- setStatus("reconnecting");
2341
- const delay = reconnectDelay * Math.pow(2, reconnectAttempts.current);
2342
- reconnectAttempts.current++;
2343
- reconnectTimeout.current = setTimeout(() => {
2344
- connect();
2345
- }, delay);
2346
- }
2347
- };
2348
- wsRef.current = ws;
2349
- } catch (err) {
2350
- setError({
2351
- code: "WEBSOCKET_CONNECT_FAILED",
2352
- message: err instanceof Error ? err.message : "Failed to connect"
2353
- });
2354
- setStatus("disconnected");
2355
- }
2356
- }, [user, client, getWebSocketUrl, events, handleMessage, autoReconnect, maxReconnectAttempts, reconnectDelay]);
2257
+ const manualUnsubscribesRef = react.useRef([]);
2258
+ const autoUnsubscribesRef = react.useRef([]);
2259
+ const onMessageRef = react.useRef(void 0);
2260
+ const channelSignature = (options?.channels ?? []).join("");
2261
+ react.useEffect(() => {
2262
+ onMessageRef.current = options?.onMessage;
2263
+ }, [options?.onMessage]);
2357
2264
  const disconnect = react.useCallback(() => {
2358
- if (reconnectTimeout.current) {
2359
- clearTimeout(reconnectTimeout.current);
2360
- reconnectTimeout.current = null;
2361
- }
2362
- if (wsRef.current) {
2363
- wsRef.current.close(1e3, "Client disconnect");
2364
- wsRef.current = null;
2365
- }
2366
- setStatus("disconnected");
2367
- reconnectAttempts.current = 0;
2368
- }, []);
2265
+ realtime.disconnect();
2266
+ }, [realtime]);
2369
2267
  const subscribe = react.useCallback(
2370
- (event, callback) => {
2371
- if (!subscribersRef.current.has(event)) {
2372
- subscribersRef.current.set(event, /* @__PURE__ */ new Set());
2373
- }
2374
- const typedCallback = callback;
2375
- subscribersRef.current.get(event).add(typedCallback);
2268
+ (channel, callback) => {
2269
+ const unsub = realtime.subscribe(channel, (data) => {
2270
+ setLastMessage({ channel, data });
2271
+ callback?.(data);
2272
+ onMessageRef.current?.(channel, data);
2273
+ });
2274
+ manualUnsubscribesRef.current.push(unsub);
2376
2275
  return () => {
2377
- const subscribers = subscribersRef.current.get(event);
2378
- if (subscribers) {
2379
- subscribers.delete(typedCallback);
2380
- if (subscribers.size === 0) {
2381
- subscribersRef.current.delete(event);
2382
- }
2383
- }
2276
+ manualUnsubscribesRef.current = manualUnsubscribesRef.current.filter((fn) => fn !== unsub);
2277
+ unsub();
2384
2278
  };
2385
2279
  },
2386
- []
2280
+ [realtime]
2387
2281
  );
2388
- const send = react.useCallback((event, data) => {
2389
- if (wsRef.current?.readyState === WebSocket.OPEN) {
2390
- wsRef.current.send(JSON.stringify({ event, data }));
2391
- } else {
2392
- console.warn("[ScaleMule Realtime] Cannot send - not connected");
2393
- }
2394
- }, []);
2282
+ const publish = react.useCallback(
2283
+ (channel, data) => {
2284
+ realtime.publish(channel, data);
2285
+ },
2286
+ [realtime]
2287
+ );
2288
+ react.useEffect(() => {
2289
+ const unsub = realtime.onStatusChange((newStatus) => {
2290
+ setStatus(newStatus);
2291
+ });
2292
+ return unsub;
2293
+ }, [realtime]);
2395
2294
  react.useEffect(() => {
2396
- if (autoConnect && user) {
2397
- connect();
2295
+ for (const unsub of autoUnsubscribesRef.current) {
2296
+ unsub();
2297
+ }
2298
+ autoUnsubscribesRef.current = [];
2299
+ for (const channel of options?.channels ?? []) {
2300
+ const unsub = realtime.subscribe(channel, (data) => {
2301
+ setLastMessage({ channel, data });
2302
+ onMessageRef.current?.(channel, data);
2303
+ });
2304
+ autoUnsubscribesRef.current.push(unsub);
2398
2305
  }
2399
2306
  return () => {
2400
- disconnect();
2307
+ for (const unsub of autoUnsubscribesRef.current) {
2308
+ unsub();
2309
+ }
2310
+ autoUnsubscribesRef.current = [];
2401
2311
  };
2402
- }, [autoConnect, user, connect, disconnect]);
2403
- return react.useMemo(
2404
- () => ({
2405
- status,
2406
- error,
2407
- connect,
2408
- disconnect,
2409
- subscribe,
2410
- send,
2411
- lastMessage
2412
- }),
2413
- [status, error, connect, disconnect, subscribe, send, lastMessage]
2414
- );
2312
+ }, [realtime, channelSignature]);
2313
+ react.useEffect(() => {
2314
+ return () => {
2315
+ for (const unsub of manualUnsubscribesRef.current) {
2316
+ unsub();
2317
+ }
2318
+ manualUnsubscribesRef.current = [];
2319
+ for (const unsub of autoUnsubscribesRef.current) {
2320
+ unsub();
2321
+ }
2322
+ autoUnsubscribesRef.current = [];
2323
+ };
2324
+ }, []);
2325
+ return { status, lastMessage, disconnect, subscribe, publish };
2415
2326
  }
2416
2327
 
2417
2328
  // src/hooks/event-dedup.ts
@@ -3060,7 +2971,7 @@ function getCsrfToken() {
3060
2971
  return match ? decodeURIComponent(match[1]) : "";
3061
2972
  }
3062
2973
  function usePushNotifications(options = {}) {
3063
- const { serviceWorkerUrl = "/sw.js", pushProxyUrl = "/api/push", onNotification } = options;
2974
+ const { serviceWorkerUrl = "/sw.js", pushProxyUrl = "/api/push", registrationSource, onNotification } = options;
3064
2975
  const { user } = useScaleMule();
3065
2976
  const [isSupported, setIsSupported] = react.useState(false);
3066
2977
  const [permission, setPermission] = react.useState("unsupported");
@@ -3127,7 +3038,7 @@ function usePushNotifications(options = {}) {
3127
3038
  const manager = react.useMemo(() => {
3128
3039
  if (typeof window === "undefined") return null;
3129
3040
  try {
3130
- return new sdk.WebPushManager({ fetcher, serviceWorkerUrl });
3041
+ return new sdk.WebPushManager({ fetcher, serviceWorkerUrl, registrationSource });
3131
3042
  } catch {
3132
3043
  return null;
3133
3044
  }
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { createContext, useState, useMemo, useEffect, useCallback, useContext, useRef } from 'react';
2
+ import { ScaleMule, WebPushManager } from '@scalemule/sdk';
2
3
  import { jsx } from 'react/jsx-runtime';
3
- import { WebPushManager } from '@scalemule/sdk';
4
4
 
5
5
  // src/provider.tsx
6
6
 
@@ -834,6 +834,24 @@ function ScaleMuleProvider({
834
834
  }),
835
835
  [apiKey, applicationId, environment, gatewayUrl, debug, storage, authProxyUrl]
836
836
  );
837
+ const baseClient = useMemo(() => {
838
+ const resolvedGatewayUrl = gatewayUrl || (environment === "dev" ? "https://api-dev.scalemule.com" : "https://api.scalemule.com");
839
+ return new ScaleMule({
840
+ apiKey,
841
+ applicationId,
842
+ baseUrl: resolvedGatewayUrl,
843
+ environment,
844
+ debug
845
+ });
846
+ }, [apiKey, applicationId, environment, gatewayUrl, debug]);
847
+ useEffect(() => {
848
+ const token = client.getSessionToken();
849
+ if (token) {
850
+ baseClient.setAccessToken(token);
851
+ } else {
852
+ baseClient.clearAccessToken();
853
+ }
854
+ }, [client, baseClient, user]);
837
855
  useEffect(() => {
838
856
  let mounted = true;
839
857
  async function initialize() {
@@ -921,6 +939,7 @@ function ScaleMuleProvider({
921
939
  const value = useMemo(
922
940
  () => ({
923
941
  client,
942
+ realtime: baseClient.realtime,
924
943
  user,
925
944
  setUser: handleSetUser,
926
945
  initializing,
@@ -933,7 +952,7 @@ function ScaleMuleProvider({
933
952
  environment: environment || void 0,
934
953
  bootstrapFlags
935
954
  }),
936
- [client, user, handleSetUser, initializing, error, analyticsProxyUrl, authProxyUrl, publishableKey, gatewayUrl, environment, bootstrapFlags]
955
+ [client, baseClient, user, handleSetUser, initializing, error, analyticsProxyUrl, authProxyUrl, publishableKey, gatewayUrl, environment, bootstrapFlags]
937
956
  );
938
957
  return /* @__PURE__ */ jsx(ScaleMuleContext.Provider, { value, children });
939
958
  }
@@ -2229,187 +2248,79 @@ function useUser() {
2229
2248
  [user, loading, localError, update, changePassword, changeEmail, deleteAccount, exportData]
2230
2249
  );
2231
2250
  }
2232
- function useRealtime(options = {}) {
2233
- const {
2234
- autoConnect = true,
2235
- events,
2236
- autoReconnect = true,
2237
- maxReconnectAttempts = 5,
2238
- reconnectDelay = 1e3
2239
- } = options;
2240
- const { client, user, setUser } = useScaleMule();
2251
+ function useRealtime(options) {
2252
+ const { realtime } = useScaleMule();
2241
2253
  const [status, setStatus] = useState("disconnected");
2242
- const [error, setError] = useState(null);
2243
2254
  const [lastMessage, setLastMessage] = useState(null);
2244
- const wsRef = useRef(null);
2245
- const reconnectAttempts = useRef(0);
2246
- const reconnectTimeout = useRef(null);
2247
- const subscribersRef = useRef(/* @__PURE__ */ new Map());
2248
- const getWebSocketUrl = useCallback(() => {
2249
- const gatewayUrl = client.getGatewayUrl();
2250
- const wsUrl = gatewayUrl.replace(/^https?:\/\//, "wss://").replace(/^http:\/\//, "ws://");
2251
- return `${wsUrl}/v1/realtime`;
2252
- }, [client]);
2253
- const handleMessage = useCallback(
2254
- (event) => {
2255
- try {
2256
- const message = JSON.parse(event.data);
2257
- setLastMessage(message);
2258
- switch (message.event) {
2259
- case "user.updated":
2260
- if (user && message.data.id === user.id) {
2261
- setUser(message.data);
2262
- }
2263
- break;
2264
- case "session.expired":
2265
- client.clearSession();
2266
- setUser(null);
2267
- break;
2268
- }
2269
- const subscribers = subscribersRef.current.get(message.event);
2270
- if (subscribers) {
2271
- subscribers.forEach((callback) => callback(message.data));
2272
- }
2273
- const wildcardSubscribers = subscribersRef.current.get("*");
2274
- if (wildcardSubscribers) {
2275
- wildcardSubscribers.forEach((callback) => callback(message));
2276
- }
2277
- } catch (err) {
2278
- console.error("[ScaleMule Realtime] Failed to parse message:", err);
2279
- }
2280
- },
2281
- [client, user, setUser]
2282
- );
2283
- const connect = useCallback(() => {
2284
- if (wsRef.current?.readyState === WebSocket.OPEN) {
2285
- return;
2286
- }
2287
- if (!user) {
2288
- setError({ code: "NOT_AUTHENTICATED", message: "Must be logged in to connect" });
2289
- return;
2290
- }
2291
- const applicationId = client.getApplicationId();
2292
- if (!applicationId) {
2293
- setError({ code: "MISSING_APP_ID", message: "applicationId is required for realtime features. Add it to your ScaleMuleProvider config." });
2294
- return;
2295
- }
2296
- setStatus("connecting");
2297
- setError(null);
2298
- const url = getWebSocketUrl();
2299
- try {
2300
- const ws = new WebSocket(url);
2301
- ws.onopen = () => {
2302
- const sessionToken = client.getSessionToken();
2303
- ws.send(JSON.stringify({
2304
- type: "auth",
2305
- token: sessionToken,
2306
- app_id: applicationId
2307
- }));
2308
- };
2309
- ws.onmessage = (event) => {
2310
- try {
2311
- const message = JSON.parse(event.data);
2312
- if (message.type === "auth_success") {
2313
- setStatus("connected");
2314
- reconnectAttempts.current = 0;
2315
- if (events && events.length > 0) {
2316
- ws.send(JSON.stringify({ type: "subscribe", events }));
2317
- }
2318
- return;
2319
- }
2320
- if (message.type === "error") {
2321
- setError({ code: "AUTH_ERROR", message: message.message || "Authentication failed" });
2322
- setStatus("disconnected");
2323
- ws.close(1e3);
2324
- return;
2325
- }
2326
- handleMessage(event);
2327
- } catch (err) {
2328
- console.error("[ScaleMule Realtime] Failed to parse message:", err);
2329
- }
2330
- };
2331
- ws.onerror = () => {
2332
- setError({ code: "WEBSOCKET_ERROR", message: "Connection error" });
2333
- };
2334
- ws.onclose = (event) => {
2335
- setStatus("disconnected");
2336
- wsRef.current = null;
2337
- if (autoReconnect && event.code !== 1e3 && reconnectAttempts.current < maxReconnectAttempts) {
2338
- setStatus("reconnecting");
2339
- const delay = reconnectDelay * Math.pow(2, reconnectAttempts.current);
2340
- reconnectAttempts.current++;
2341
- reconnectTimeout.current = setTimeout(() => {
2342
- connect();
2343
- }, delay);
2344
- }
2345
- };
2346
- wsRef.current = ws;
2347
- } catch (err) {
2348
- setError({
2349
- code: "WEBSOCKET_CONNECT_FAILED",
2350
- message: err instanceof Error ? err.message : "Failed to connect"
2351
- });
2352
- setStatus("disconnected");
2353
- }
2354
- }, [user, client, getWebSocketUrl, events, handleMessage, autoReconnect, maxReconnectAttempts, reconnectDelay]);
2255
+ const manualUnsubscribesRef = useRef([]);
2256
+ const autoUnsubscribesRef = useRef([]);
2257
+ const onMessageRef = useRef(void 0);
2258
+ const channelSignature = (options?.channels ?? []).join("");
2259
+ useEffect(() => {
2260
+ onMessageRef.current = options?.onMessage;
2261
+ }, [options?.onMessage]);
2355
2262
  const disconnect = useCallback(() => {
2356
- if (reconnectTimeout.current) {
2357
- clearTimeout(reconnectTimeout.current);
2358
- reconnectTimeout.current = null;
2359
- }
2360
- if (wsRef.current) {
2361
- wsRef.current.close(1e3, "Client disconnect");
2362
- wsRef.current = null;
2363
- }
2364
- setStatus("disconnected");
2365
- reconnectAttempts.current = 0;
2366
- }, []);
2263
+ realtime.disconnect();
2264
+ }, [realtime]);
2367
2265
  const subscribe = useCallback(
2368
- (event, callback) => {
2369
- if (!subscribersRef.current.has(event)) {
2370
- subscribersRef.current.set(event, /* @__PURE__ */ new Set());
2371
- }
2372
- const typedCallback = callback;
2373
- subscribersRef.current.get(event).add(typedCallback);
2266
+ (channel, callback) => {
2267
+ const unsub = realtime.subscribe(channel, (data) => {
2268
+ setLastMessage({ channel, data });
2269
+ callback?.(data);
2270
+ onMessageRef.current?.(channel, data);
2271
+ });
2272
+ manualUnsubscribesRef.current.push(unsub);
2374
2273
  return () => {
2375
- const subscribers = subscribersRef.current.get(event);
2376
- if (subscribers) {
2377
- subscribers.delete(typedCallback);
2378
- if (subscribers.size === 0) {
2379
- subscribersRef.current.delete(event);
2380
- }
2381
- }
2274
+ manualUnsubscribesRef.current = manualUnsubscribesRef.current.filter((fn) => fn !== unsub);
2275
+ unsub();
2382
2276
  };
2383
2277
  },
2384
- []
2278
+ [realtime]
2385
2279
  );
2386
- const send = useCallback((event, data) => {
2387
- if (wsRef.current?.readyState === WebSocket.OPEN) {
2388
- wsRef.current.send(JSON.stringify({ event, data }));
2389
- } else {
2390
- console.warn("[ScaleMule Realtime] Cannot send - not connected");
2391
- }
2392
- }, []);
2280
+ const publish = useCallback(
2281
+ (channel, data) => {
2282
+ realtime.publish(channel, data);
2283
+ },
2284
+ [realtime]
2285
+ );
2286
+ useEffect(() => {
2287
+ const unsub = realtime.onStatusChange((newStatus) => {
2288
+ setStatus(newStatus);
2289
+ });
2290
+ return unsub;
2291
+ }, [realtime]);
2393
2292
  useEffect(() => {
2394
- if (autoConnect && user) {
2395
- connect();
2293
+ for (const unsub of autoUnsubscribesRef.current) {
2294
+ unsub();
2295
+ }
2296
+ autoUnsubscribesRef.current = [];
2297
+ for (const channel of options?.channels ?? []) {
2298
+ const unsub = realtime.subscribe(channel, (data) => {
2299
+ setLastMessage({ channel, data });
2300
+ onMessageRef.current?.(channel, data);
2301
+ });
2302
+ autoUnsubscribesRef.current.push(unsub);
2396
2303
  }
2397
2304
  return () => {
2398
- disconnect();
2305
+ for (const unsub of autoUnsubscribesRef.current) {
2306
+ unsub();
2307
+ }
2308
+ autoUnsubscribesRef.current = [];
2399
2309
  };
2400
- }, [autoConnect, user, connect, disconnect]);
2401
- return useMemo(
2402
- () => ({
2403
- status,
2404
- error,
2405
- connect,
2406
- disconnect,
2407
- subscribe,
2408
- send,
2409
- lastMessage
2410
- }),
2411
- [status, error, connect, disconnect, subscribe, send, lastMessage]
2412
- );
2310
+ }, [realtime, channelSignature]);
2311
+ useEffect(() => {
2312
+ return () => {
2313
+ for (const unsub of manualUnsubscribesRef.current) {
2314
+ unsub();
2315
+ }
2316
+ manualUnsubscribesRef.current = [];
2317
+ for (const unsub of autoUnsubscribesRef.current) {
2318
+ unsub();
2319
+ }
2320
+ autoUnsubscribesRef.current = [];
2321
+ };
2322
+ }, []);
2323
+ return { status, lastMessage, disconnect, subscribe, publish };
2413
2324
  }
2414
2325
 
2415
2326
  // src/hooks/event-dedup.ts
@@ -3058,7 +2969,7 @@ function getCsrfToken() {
3058
2969
  return match ? decodeURIComponent(match[1]) : "";
3059
2970
  }
3060
2971
  function usePushNotifications(options = {}) {
3061
- const { serviceWorkerUrl = "/sw.js", pushProxyUrl = "/api/push", onNotification } = options;
2972
+ const { serviceWorkerUrl = "/sw.js", pushProxyUrl = "/api/push", registrationSource, onNotification } = options;
3062
2973
  const { user } = useScaleMule();
3063
2974
  const [isSupported, setIsSupported] = useState(false);
3064
2975
  const [permission, setPermission] = useState("unsupported");
@@ -3125,7 +3036,7 @@ function usePushNotifications(options = {}) {
3125
3036
  const manager = useMemo(() => {
3126
3037
  if (typeof window === "undefined") return null;
3127
3038
  try {
3128
- return new WebPushManager({ fetcher, serviceWorkerUrl });
3039
+ return new WebPushManager({ fetcher, serviceWorkerUrl, registrationSource });
3129
3040
  } catch {
3130
3041
  return null;
3131
3042
  }
@@ -231,7 +231,7 @@ interface AuthRoutesConfig {
231
231
  email: string;
232
232
  }) => void | Promise<void>;
233
233
  }
234
- type RouteHandler$1 = (request: Request, context: {
234
+ type RouteHandler$2 = (request: Request, context: {
235
235
  params: Promise<{
236
236
  scalemule?: string[];
237
237
  }>;
@@ -264,10 +264,10 @@ type RouteHandler$1 = (request: Request, context: {
264
264
  * - POST /api/auth/verify-email - Verify email
265
265
  */
266
266
  declare function createAuthRoutes(config?: AuthRoutesConfig): {
267
- GET: RouteHandler$1;
268
- POST: RouteHandler$1;
269
- DELETE: RouteHandler$1;
270
- PATCH: RouteHandler$1;
267
+ GET: RouteHandler$2;
268
+ POST: RouteHandler$2;
269
+ DELETE: RouteHandler$2;
270
+ PATCH: RouteHandler$2;
271
271
  };
272
272
  interface AnalyticsTrackingGateConfig {
273
273
  /** Feature flag key used to decide whether to forward analytics */
@@ -346,7 +346,7 @@ interface AnalyticsRoutesConfig {
346
346
  * ```
347
347
  */
348
348
  declare function createAnalyticsRoutes(config?: AnalyticsRoutesConfig): {
349
- POST: RouteHandler$1;
349
+ POST: RouteHandler$2;
350
350
  };
351
351
 
352
352
  /**
@@ -374,15 +374,49 @@ interface PushRoutesConfig {
374
374
  /** Enable CSRF validation on POST/PUT/DELETE (default: true) */
375
375
  csrf?: boolean;
376
376
  }
377
- type RouteHandler = (request: Request, context: {
377
+ type RouteHandler$1 = (request: Request, context: {
378
378
  params: Promise<{
379
379
  action?: string[];
380
380
  }>;
381
381
  }) => Promise<Response>;
382
382
  declare function createPushRoutes(config: PushRoutesConfig): {
383
+ GET: RouteHandler$1;
384
+ POST: RouteHandler$1;
385
+ PUT: RouteHandler$1;
386
+ DELETE: RouteHandler$1;
387
+ };
388
+
389
+ /**
390
+ * Notification Proxy Routes
391
+ *
392
+ * Drop-in route handlers for Next.js App Router that proxy notification
393
+ * requests to the ScaleMule gateway. Follows the same pattern as createPushRoutes().
394
+ *
395
+ * @example
396
+ * ```ts
397
+ * // app/api/notifications/[...action]/route.ts
398
+ * import { createNotificationRoutes } from '@scalemule/nextjs/server'
399
+ *
400
+ * export const { GET, PATCH, DELETE } = createNotificationRoutes({
401
+ * apiKey: process.env.SCALEMULE_API_KEY!,
402
+ * gatewayUrl: process.env.SCALEMULE_API_URL!,
403
+ * })
404
+ * ```
405
+ */
406
+ interface NotificationRoutesConfig {
407
+ /** Server-side secret API key */
408
+ apiKey: string;
409
+ /** Gateway URL (e.g., https://api.scalemule.com) */
410
+ gatewayUrl: string;
411
+ }
412
+ type RouteHandler = (request: Request, context: {
413
+ params: Promise<{
414
+ action?: string[];
415
+ }>;
416
+ }) => Promise<Response>;
417
+ declare function createNotificationRoutes(config: NotificationRoutesConfig): {
383
418
  GET: RouteHandler;
384
- POST: RouteHandler;
385
- PUT: RouteHandler;
419
+ PATCH: RouteHandler;
386
420
  DELETE: RouteHandler;
387
421
  };
388
422
 
@@ -977,4 +1011,4 @@ declare function invalidateBundleCache(key?: string): void;
977
1011
  */
978
1012
  declare function prefetchBundles(keys: string[]): Promise<void>;
979
1013
 
980
- export { type AnalyticsRoutesConfig, type AnalyticsTrackingGateConfig, type AuthMiddlewareConfig, type AuthRoutesConfig, CSRF_COOKIE_NAME, CSRF_HEADER_NAME, type HandlerContext, type HandlerOptions, type MySqlBundle, OAUTH_STATE_COOKIE_NAME, type OAuthBundle, type PostgresBundle, type PushRoutesConfig, type RedisBundle, type S3Bundle, SESSION_COOKIE_NAME, ScaleMuleError, ServerConfig, type SessionCookieOptions, type SessionData, type SmtpBundle, USER_ID_COOKIE_NAME, apiHandler, buildClientContextHeaders, buildFlagContext, clearOAuthState, clearSession, configureBundles, configureSecrets, createAnalyticsRoutes, createAuthMiddleware, createAuthRoutes, createPushRoutes, errorCodeToStatus, extractClientContext, extractClientContextFromReq, generateCSRFToken, getAppSecret, getAppSecretOrDefault, getBootstrapFlags, getBundle, getCSRFToken, getMySqlBundle, getOAuthBundle, getPostgresBundle, getRedisBundle, getS3Bundle, getSession, getSessionFromRequest, getSmtpBundle, invalidateBundleCache, invalidateSecretCache, prefetchBundles, prefetchSecrets, requireAppSecret, requireBundle, requireSession, setOAuthState, unwrap, validateCSRFToken, validateCSRFTokenAsync, validateOAuthState, validateOAuthStateAsync, withAuth, withCSRFProtection, withCSRFToken, withSession };
1014
+ export { type AnalyticsRoutesConfig, type AnalyticsTrackingGateConfig, type AuthMiddlewareConfig, type AuthRoutesConfig, CSRF_COOKIE_NAME, CSRF_HEADER_NAME, type HandlerContext, type HandlerOptions, type MySqlBundle, type NotificationRoutesConfig, OAUTH_STATE_COOKIE_NAME, type OAuthBundle, type PostgresBundle, type PushRoutesConfig, type RedisBundle, type S3Bundle, SESSION_COOKIE_NAME, ScaleMuleError, ServerConfig, type SessionCookieOptions, type SessionData, type SmtpBundle, USER_ID_COOKIE_NAME, apiHandler, buildClientContextHeaders, buildFlagContext, clearOAuthState, clearSession, configureBundles, configureSecrets, createAnalyticsRoutes, createAuthMiddleware, createAuthRoutes, createNotificationRoutes, createPushRoutes, errorCodeToStatus, extractClientContext, extractClientContextFromReq, generateCSRFToken, getAppSecret, getAppSecretOrDefault, getBootstrapFlags, getBundle, getCSRFToken, getMySqlBundle, getOAuthBundle, getPostgresBundle, getRedisBundle, getS3Bundle, getSession, getSessionFromRequest, getSmtpBundle, invalidateBundleCache, invalidateSecretCache, prefetchBundles, prefetchSecrets, requireAppSecret, requireBundle, requireSession, setOAuthState, unwrap, validateCSRFToken, validateCSRFTokenAsync, validateOAuthState, validateOAuthStateAsync, withAuth, withCSRFProtection, withCSRFToken, withSession };
@@ -231,7 +231,7 @@ interface AuthRoutesConfig {
231
231
  email: string;
232
232
  }) => void | Promise<void>;
233
233
  }
234
- type RouteHandler$1 = (request: Request, context: {
234
+ type RouteHandler$2 = (request: Request, context: {
235
235
  params: Promise<{
236
236
  scalemule?: string[];
237
237
  }>;
@@ -264,10 +264,10 @@ type RouteHandler$1 = (request: Request, context: {
264
264
  * - POST /api/auth/verify-email - Verify email
265
265
  */
266
266
  declare function createAuthRoutes(config?: AuthRoutesConfig): {
267
- GET: RouteHandler$1;
268
- POST: RouteHandler$1;
269
- DELETE: RouteHandler$1;
270
- PATCH: RouteHandler$1;
267
+ GET: RouteHandler$2;
268
+ POST: RouteHandler$2;
269
+ DELETE: RouteHandler$2;
270
+ PATCH: RouteHandler$2;
271
271
  };
272
272
  interface AnalyticsTrackingGateConfig {
273
273
  /** Feature flag key used to decide whether to forward analytics */
@@ -346,7 +346,7 @@ interface AnalyticsRoutesConfig {
346
346
  * ```
347
347
  */
348
348
  declare function createAnalyticsRoutes(config?: AnalyticsRoutesConfig): {
349
- POST: RouteHandler$1;
349
+ POST: RouteHandler$2;
350
350
  };
351
351
 
352
352
  /**
@@ -374,15 +374,49 @@ interface PushRoutesConfig {
374
374
  /** Enable CSRF validation on POST/PUT/DELETE (default: true) */
375
375
  csrf?: boolean;
376
376
  }
377
- type RouteHandler = (request: Request, context: {
377
+ type RouteHandler$1 = (request: Request, context: {
378
378
  params: Promise<{
379
379
  action?: string[];
380
380
  }>;
381
381
  }) => Promise<Response>;
382
382
  declare function createPushRoutes(config: PushRoutesConfig): {
383
+ GET: RouteHandler$1;
384
+ POST: RouteHandler$1;
385
+ PUT: RouteHandler$1;
386
+ DELETE: RouteHandler$1;
387
+ };
388
+
389
+ /**
390
+ * Notification Proxy Routes
391
+ *
392
+ * Drop-in route handlers for Next.js App Router that proxy notification
393
+ * requests to the ScaleMule gateway. Follows the same pattern as createPushRoutes().
394
+ *
395
+ * @example
396
+ * ```ts
397
+ * // app/api/notifications/[...action]/route.ts
398
+ * import { createNotificationRoutes } from '@scalemule/nextjs/server'
399
+ *
400
+ * export const { GET, PATCH, DELETE } = createNotificationRoutes({
401
+ * apiKey: process.env.SCALEMULE_API_KEY!,
402
+ * gatewayUrl: process.env.SCALEMULE_API_URL!,
403
+ * })
404
+ * ```
405
+ */
406
+ interface NotificationRoutesConfig {
407
+ /** Server-side secret API key */
408
+ apiKey: string;
409
+ /** Gateway URL (e.g., https://api.scalemule.com) */
410
+ gatewayUrl: string;
411
+ }
412
+ type RouteHandler = (request: Request, context: {
413
+ params: Promise<{
414
+ action?: string[];
415
+ }>;
416
+ }) => Promise<Response>;
417
+ declare function createNotificationRoutes(config: NotificationRoutesConfig): {
383
418
  GET: RouteHandler;
384
- POST: RouteHandler;
385
- PUT: RouteHandler;
419
+ PATCH: RouteHandler;
386
420
  DELETE: RouteHandler;
387
421
  };
388
422
 
@@ -977,4 +1011,4 @@ declare function invalidateBundleCache(key?: string): void;
977
1011
  */
978
1012
  declare function prefetchBundles(keys: string[]): Promise<void>;
979
1013
 
980
- export { type AnalyticsRoutesConfig, type AnalyticsTrackingGateConfig, type AuthMiddlewareConfig, type AuthRoutesConfig, CSRF_COOKIE_NAME, CSRF_HEADER_NAME, type HandlerContext, type HandlerOptions, type MySqlBundle, OAUTH_STATE_COOKIE_NAME, type OAuthBundle, type PostgresBundle, type PushRoutesConfig, type RedisBundle, type S3Bundle, SESSION_COOKIE_NAME, ScaleMuleError, ServerConfig, type SessionCookieOptions, type SessionData, type SmtpBundle, USER_ID_COOKIE_NAME, apiHandler, buildClientContextHeaders, buildFlagContext, clearOAuthState, clearSession, configureBundles, configureSecrets, createAnalyticsRoutes, createAuthMiddleware, createAuthRoutes, createPushRoutes, errorCodeToStatus, extractClientContext, extractClientContextFromReq, generateCSRFToken, getAppSecret, getAppSecretOrDefault, getBootstrapFlags, getBundle, getCSRFToken, getMySqlBundle, getOAuthBundle, getPostgresBundle, getRedisBundle, getS3Bundle, getSession, getSessionFromRequest, getSmtpBundle, invalidateBundleCache, invalidateSecretCache, prefetchBundles, prefetchSecrets, requireAppSecret, requireBundle, requireSession, setOAuthState, unwrap, validateCSRFToken, validateCSRFTokenAsync, validateOAuthState, validateOAuthStateAsync, withAuth, withCSRFProtection, withCSRFToken, withSession };
1014
+ export { type AnalyticsRoutesConfig, type AnalyticsTrackingGateConfig, type AuthMiddlewareConfig, type AuthRoutesConfig, CSRF_COOKIE_NAME, CSRF_HEADER_NAME, type HandlerContext, type HandlerOptions, type MySqlBundle, type NotificationRoutesConfig, OAUTH_STATE_COOKIE_NAME, type OAuthBundle, type PostgresBundle, type PushRoutesConfig, type RedisBundle, type S3Bundle, SESSION_COOKIE_NAME, ScaleMuleError, ServerConfig, type SessionCookieOptions, type SessionData, type SmtpBundle, USER_ID_COOKIE_NAME, apiHandler, buildClientContextHeaders, buildFlagContext, clearOAuthState, clearSession, configureBundles, configureSecrets, createAnalyticsRoutes, createAuthMiddleware, createAuthRoutes, createNotificationRoutes, createPushRoutes, errorCodeToStatus, extractClientContext, extractClientContextFromReq, generateCSRFToken, getAppSecret, getAppSecretOrDefault, getBootstrapFlags, getBundle, getCSRFToken, getMySqlBundle, getOAuthBundle, getPostgresBundle, getRedisBundle, getS3Bundle, getSession, getSessionFromRequest, getSmtpBundle, invalidateBundleCache, invalidateSecretCache, prefetchBundles, prefetchSecrets, requireAppSecret, requireBundle, requireSession, setOAuthState, unwrap, validateCSRFToken, validateCSRFTokenAsync, validateOAuthState, validateOAuthStateAsync, withAuth, withCSRFProtection, withCSRFToken, withSession };
@@ -1732,6 +1732,73 @@ function createPushRoutes(config) {
1732
1732
  return { GET, POST, PUT, DELETE };
1733
1733
  }
1734
1734
 
1735
+ // src/server/notifications.ts
1736
+ function createNotificationRoutes(config) {
1737
+ const { apiKey, gatewayUrl } = config;
1738
+ async function proxyToGateway(request, method, subPath) {
1739
+ const targetUrl = `${gatewayUrl}/v1/notifications/${subPath}`;
1740
+ const headers2 = {
1741
+ "x-api-key": apiKey,
1742
+ "Content-Type": "application/json"
1743
+ };
1744
+ const session = getSessionFromRequest(request);
1745
+ if (session?.sessionToken) {
1746
+ headers2["Authorization"] = `Bearer ${session.sessionToken}`;
1747
+ }
1748
+ const clientContext = extractClientContext(
1749
+ request
1750
+ );
1751
+ const contextHeaders = buildClientContextHeaders(clientContext);
1752
+ Object.assign(headers2, contextHeaders);
1753
+ const workspaceId = request.headers.get("x-sm-workspace-id");
1754
+ if (workspaceId) {
1755
+ headers2["x-sm-workspace-id"] = workspaceId;
1756
+ }
1757
+ const fetchOptions = {
1758
+ method,
1759
+ headers: headers2
1760
+ };
1761
+ if (method === "PATCH") {
1762
+ try {
1763
+ const body = await request.text();
1764
+ if (body) {
1765
+ fetchOptions.body = body;
1766
+ }
1767
+ } catch {
1768
+ }
1769
+ }
1770
+ let targetUrlWithQuery = targetUrl;
1771
+ if (method === "GET") {
1772
+ const url = new URL(request.url);
1773
+ if (url.search) {
1774
+ targetUrlWithQuery = `${targetUrl}${url.search}`;
1775
+ }
1776
+ }
1777
+ const response = await fetch(targetUrlWithQuery, fetchOptions);
1778
+ const responseBody = await response.text();
1779
+ return new Response(responseBody, {
1780
+ status: response.status,
1781
+ headers: { "Content-Type": "application/json" }
1782
+ });
1783
+ }
1784
+ const GET = async (request, context) => {
1785
+ const params = await context.params;
1786
+ const subPath = params.action?.join("/") || "";
1787
+ return proxyToGateway(request, "GET", subPath);
1788
+ };
1789
+ const PATCH = async (request, context) => {
1790
+ const params = await context.params;
1791
+ const subPath = params.action?.join("/") || "";
1792
+ return proxyToGateway(request, "PATCH", subPath);
1793
+ };
1794
+ const DELETE = async (request, context) => {
1795
+ const params = await context.params;
1796
+ const subPath = params.action?.join("/") || "";
1797
+ return proxyToGateway(request, "DELETE", subPath);
1798
+ };
1799
+ return { GET, PATCH, DELETE };
1800
+ }
1801
+
1735
1802
  // src/server/errors.ts
1736
1803
  var ScaleMuleError = class extends Error {
1737
1804
  constructor(code, message, status = 400, details) {
@@ -2355,6 +2422,7 @@ exports.configureSecrets = configureSecrets;
2355
2422
  exports.createAnalyticsRoutes = createAnalyticsRoutes;
2356
2423
  exports.createAuthMiddleware = createAuthMiddleware;
2357
2424
  exports.createAuthRoutes = createAuthRoutes;
2425
+ exports.createNotificationRoutes = createNotificationRoutes;
2358
2426
  exports.createPushRoutes = createPushRoutes;
2359
2427
  exports.createServerClient = createServerClient;
2360
2428
  exports.createWebhookHandler = createWebhookHandler;
@@ -1730,6 +1730,73 @@ function createPushRoutes(config) {
1730
1730
  return { GET, POST, PUT, DELETE };
1731
1731
  }
1732
1732
 
1733
+ // src/server/notifications.ts
1734
+ function createNotificationRoutes(config) {
1735
+ const { apiKey, gatewayUrl } = config;
1736
+ async function proxyToGateway(request, method, subPath) {
1737
+ const targetUrl = `${gatewayUrl}/v1/notifications/${subPath}`;
1738
+ const headers2 = {
1739
+ "x-api-key": apiKey,
1740
+ "Content-Type": "application/json"
1741
+ };
1742
+ const session = getSessionFromRequest(request);
1743
+ if (session?.sessionToken) {
1744
+ headers2["Authorization"] = `Bearer ${session.sessionToken}`;
1745
+ }
1746
+ const clientContext = extractClientContext(
1747
+ request
1748
+ );
1749
+ const contextHeaders = buildClientContextHeaders(clientContext);
1750
+ Object.assign(headers2, contextHeaders);
1751
+ const workspaceId = request.headers.get("x-sm-workspace-id");
1752
+ if (workspaceId) {
1753
+ headers2["x-sm-workspace-id"] = workspaceId;
1754
+ }
1755
+ const fetchOptions = {
1756
+ method,
1757
+ headers: headers2
1758
+ };
1759
+ if (method === "PATCH") {
1760
+ try {
1761
+ const body = await request.text();
1762
+ if (body) {
1763
+ fetchOptions.body = body;
1764
+ }
1765
+ } catch {
1766
+ }
1767
+ }
1768
+ let targetUrlWithQuery = targetUrl;
1769
+ if (method === "GET") {
1770
+ const url = new URL(request.url);
1771
+ if (url.search) {
1772
+ targetUrlWithQuery = `${targetUrl}${url.search}`;
1773
+ }
1774
+ }
1775
+ const response = await fetch(targetUrlWithQuery, fetchOptions);
1776
+ const responseBody = await response.text();
1777
+ return new Response(responseBody, {
1778
+ status: response.status,
1779
+ headers: { "Content-Type": "application/json" }
1780
+ });
1781
+ }
1782
+ const GET = async (request, context) => {
1783
+ const params = await context.params;
1784
+ const subPath = params.action?.join("/") || "";
1785
+ return proxyToGateway(request, "GET", subPath);
1786
+ };
1787
+ const PATCH = async (request, context) => {
1788
+ const params = await context.params;
1789
+ const subPath = params.action?.join("/") || "";
1790
+ return proxyToGateway(request, "PATCH", subPath);
1791
+ };
1792
+ const DELETE = async (request, context) => {
1793
+ const params = await context.params;
1794
+ const subPath = params.action?.join("/") || "";
1795
+ return proxyToGateway(request, "DELETE", subPath);
1796
+ };
1797
+ return { GET, PATCH, DELETE };
1798
+ }
1799
+
1733
1800
  // src/server/errors.ts
1734
1801
  var ScaleMuleError = class extends Error {
1735
1802
  constructor(code, message, status = 400, details) {
@@ -2336,4 +2403,4 @@ async function prefetchBundles(keys) {
2336
2403
  await Promise.all(keys.map((key) => getBundle(key)));
2337
2404
  }
2338
2405
 
2339
- export { CSRF_COOKIE_NAME, CSRF_HEADER_NAME, OAUTH_STATE_COOKIE_NAME, SESSION_COOKIE_NAME, ScaleMuleError, ScaleMuleServer, USER_ID_COOKIE_NAME, apiHandler, buildClientContextHeaders, buildFlagContext, clearOAuthState, clearSession, configureBundles, configureSecrets, createAnalyticsRoutes, createAuthMiddleware, createAuthRoutes, createPushRoutes, createServerClient, createWebhookHandler, createWebhookRoutes, errorCodeToStatus, extractClientContext, extractClientContextFromReq, generateCSRFToken, getAppSecret, getAppSecretOrDefault, getBootstrapFlags, getBundle, getCSRFToken, getMySqlBundle, getOAuthBundle, getPostgresBundle, getRedisBundle, getS3Bundle, getSession, getSessionFromRequest, getSmtpBundle, invalidateBundleCache, invalidateSecretCache, parseWebhookEvent, prefetchBundles, prefetchSecrets, registerVideoWebhook, requireAppSecret, requireBundle, requireSession, resolveGatewayUrl, setOAuthState, unwrap, validateCSRFToken, validateCSRFTokenAsync, validateOAuthState, validateOAuthStateAsync, verifyWebhookSignature, withAuth, withCSRFProtection, withCSRFToken, withSession };
2406
+ export { CSRF_COOKIE_NAME, CSRF_HEADER_NAME, OAUTH_STATE_COOKIE_NAME, SESSION_COOKIE_NAME, ScaleMuleError, ScaleMuleServer, USER_ID_COOKIE_NAME, apiHandler, buildClientContextHeaders, buildFlagContext, clearOAuthState, clearSession, configureBundles, configureSecrets, createAnalyticsRoutes, createAuthMiddleware, createAuthRoutes, createNotificationRoutes, createPushRoutes, createServerClient, createWebhookHandler, createWebhookRoutes, errorCodeToStatus, extractClientContext, extractClientContextFromReq, generateCSRFToken, getAppSecret, getAppSecretOrDefault, getBootstrapFlags, getBundle, getCSRFToken, getMySqlBundle, getOAuthBundle, getPostgresBundle, getRedisBundle, getS3Bundle, getSession, getSessionFromRequest, getSmtpBundle, invalidateBundleCache, invalidateSecretCache, parseWebhookEvent, prefetchBundles, prefetchSecrets, registerVideoWebhook, requireAppSecret, requireBundle, requireSession, resolveGatewayUrl, setOAuthState, unwrap, validateCSRFToken, validateCSRFTokenAsync, validateOAuthState, validateOAuthStateAsync, verifyWebhookSignature, withAuth, withCSRFProtection, withCSRFToken, withSession };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scalemule/nextjs",
3
- "version": "0.0.31",
3
+ "version": "0.0.33",
4
4
  "description": "ScaleMule SDK for Next.js applications - authentication, storage, and user management",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -52,7 +52,7 @@
52
52
  "clean": "rm -rf dist"
53
53
  },
54
54
  "dependencies": {
55
- "@scalemule/sdk": ">=0.0.23"
55
+ "@scalemule/sdk": "file:../sdk"
56
56
  },
57
57
  "peerDependencies": {
58
58
  "next": ">=14.0.0",