krisspy-sdk 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +127 -2
- package/dist/index.d.ts +127 -2
- package/dist/index.js +342 -35
- package/dist/index.mjs +338 -34
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -26,6 +26,20 @@ interface KrisspyClientOptions {
|
|
|
26
26
|
* When false, uses legacy /data/* endpoints which require backend owner auth.
|
|
27
27
|
*/
|
|
28
28
|
useRLS?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Custom storage adapter for session persistence.
|
|
31
|
+
* Use this in React Native with AsyncStorage or MMKV.
|
|
32
|
+
* Falls back to localStorage (browser) or in-memory storage.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* import AsyncStorage from '@react-native-async-storage/async-storage'
|
|
36
|
+
* const krisspy = createClient({ ..., storage: AsyncStorage })
|
|
37
|
+
*/
|
|
38
|
+
storage?: {
|
|
39
|
+
getItem(key: string): string | null | Promise<string | null>;
|
|
40
|
+
setItem(key: string, value: string): void | Promise<void>;
|
|
41
|
+
removeItem(key: string): void | Promise<void>;
|
|
42
|
+
};
|
|
29
43
|
}
|
|
30
44
|
interface User {
|
|
31
45
|
id: string;
|
|
@@ -184,8 +198,24 @@ declare class HttpClient {
|
|
|
184
198
|
delete<T>(path: string, params?: Record<string, any>): Promise<HttpResponse<T>>;
|
|
185
199
|
}
|
|
186
200
|
|
|
201
|
+
/**
|
|
202
|
+
* Platform detection and cross-platform utilities
|
|
203
|
+
* Safe to evaluate in any JS environment (browser, Node, React Native, Expo Snack)
|
|
204
|
+
*/
|
|
205
|
+
/** Storage adapter interface - allows custom storage (AsyncStorage, MMKV, etc.) */
|
|
206
|
+
interface StorageAdapter {
|
|
207
|
+
getItem(key: string): string | null | Promise<string | null>;
|
|
208
|
+
setItem(key: string, value: string): void | Promise<void>;
|
|
209
|
+
removeItem(key: string): void | Promise<void>;
|
|
210
|
+
}
|
|
211
|
+
/** Check if running in a browser with DOM APIs */
|
|
212
|
+
declare function isBrowser(): boolean;
|
|
213
|
+
/** Check if running in React Native */
|
|
214
|
+
declare function isReactNative(): boolean;
|
|
215
|
+
|
|
187
216
|
/**
|
|
188
217
|
* Auth Module - User authentication for Krisspy backends
|
|
218
|
+
* Compatible with Browser, React Native, and Node.js
|
|
189
219
|
*/
|
|
190
220
|
|
|
191
221
|
declare class KrisspyAuth {
|
|
@@ -195,11 +225,13 @@ declare class KrisspyAuth {
|
|
|
195
225
|
private currentUser;
|
|
196
226
|
private listeners;
|
|
197
227
|
private refreshInterval?;
|
|
198
|
-
|
|
228
|
+
private storage;
|
|
229
|
+
constructor(http: HttpClient, backendId: string, storage?: StorageAdapter);
|
|
199
230
|
/**
|
|
200
231
|
* Get current session from storage
|
|
201
232
|
*/
|
|
202
233
|
private restoreSession;
|
|
234
|
+
private hydrateSession;
|
|
203
235
|
/**
|
|
204
236
|
* Store session
|
|
205
237
|
*/
|
|
@@ -299,6 +331,7 @@ declare class KrisspyAuth {
|
|
|
299
331
|
};
|
|
300
332
|
/**
|
|
301
333
|
* Set session from external source (e.g., OAuth callback)
|
|
334
|
+
* Browser-only: parses tokens from URL hash
|
|
302
335
|
*/
|
|
303
336
|
setSessionFromUrl(): Promise<AuthResponse>;
|
|
304
337
|
}
|
|
@@ -699,6 +732,80 @@ declare class KrisspyRealtime {
|
|
|
699
732
|
private log;
|
|
700
733
|
}
|
|
701
734
|
|
|
735
|
+
/**
|
|
736
|
+
* Krisspy Analytics - Lightweight event tracking module
|
|
737
|
+
*
|
|
738
|
+
* Tracks page views, sessions, custom events. Sends batched events
|
|
739
|
+
* to the analytics ingest endpoint. Works with SPAs (intercepts pushState).
|
|
740
|
+
* Browser-only module - safely no-ops in React Native / Node.js.
|
|
741
|
+
*
|
|
742
|
+
* @example
|
|
743
|
+
* const krisspy = createClient({ backendId: '...', anonKey: '...' })
|
|
744
|
+
* krisspy.analytics.init({ autoTrackPageViews: true })
|
|
745
|
+
* krisspy.analytics.track('button_click', { label: 'signup' })
|
|
746
|
+
*/
|
|
747
|
+
|
|
748
|
+
interface AnalyticsInitOptions {
|
|
749
|
+
/** Auto-track page views on init + navigation (default: true) */
|
|
750
|
+
autoTrackPageViews?: boolean;
|
|
751
|
+
/** Auto-track SPA navigation via pushState/popstate (default: true) */
|
|
752
|
+
autoTrackNavigation?: boolean;
|
|
753
|
+
/** Flush interval in ms (default: 5000) */
|
|
754
|
+
flushInterval?: number;
|
|
755
|
+
/** Enable debug logging (default: false) */
|
|
756
|
+
debug?: boolean;
|
|
757
|
+
}
|
|
758
|
+
interface AnalyticsEvent {
|
|
759
|
+
eventName: string;
|
|
760
|
+
sessionId: string;
|
|
761
|
+
userId?: string;
|
|
762
|
+
properties?: Record<string, any>;
|
|
763
|
+
timestamp: string;
|
|
764
|
+
}
|
|
765
|
+
declare class KrisspyAnalytics {
|
|
766
|
+
private http;
|
|
767
|
+
private backendId;
|
|
768
|
+
private queue;
|
|
769
|
+
private sessionId;
|
|
770
|
+
private userId;
|
|
771
|
+
private userTraits;
|
|
772
|
+
private initialized;
|
|
773
|
+
private flushTimer;
|
|
774
|
+
private options;
|
|
775
|
+
private originalPushState;
|
|
776
|
+
private popstateHandler;
|
|
777
|
+
private unloadHandler;
|
|
778
|
+
constructor(http: HttpClient, backendId: string);
|
|
779
|
+
/**
|
|
780
|
+
* Initialize analytics tracking
|
|
781
|
+
* No-ops in non-browser environments (React Native, Node.js)
|
|
782
|
+
*/
|
|
783
|
+
init(options?: AnalyticsInitOptions): void;
|
|
784
|
+
/**
|
|
785
|
+
* Track a custom event
|
|
786
|
+
*/
|
|
787
|
+
track(eventName: string, properties?: Record<string, any>): void;
|
|
788
|
+
/**
|
|
789
|
+
* Identify a user (attach userId + traits to all future events)
|
|
790
|
+
*/
|
|
791
|
+
identify(userId: string, traits?: Record<string, any>): void;
|
|
792
|
+
/**
|
|
793
|
+
* Flush event queue to the server (async)
|
|
794
|
+
*/
|
|
795
|
+
flush(): Promise<void>;
|
|
796
|
+
/**
|
|
797
|
+
* Synchronous flush using sendBeacon (for page unload)
|
|
798
|
+
*/
|
|
799
|
+
private flushSync;
|
|
800
|
+
/**
|
|
801
|
+
* Stop tracking and cleanup all listeners/timers
|
|
802
|
+
*/
|
|
803
|
+
destroy(): void;
|
|
804
|
+
private getOrCreateSessionId;
|
|
805
|
+
private getIngestUrl;
|
|
806
|
+
private setupNavigationTracking;
|
|
807
|
+
}
|
|
808
|
+
|
|
702
809
|
/**
|
|
703
810
|
* Query Builder - Supabase-style fluent API for database queries
|
|
704
811
|
*/
|
|
@@ -850,6 +957,7 @@ declare class KrisspyClient {
|
|
|
850
957
|
private _auth;
|
|
851
958
|
private _storage;
|
|
852
959
|
private _realtime;
|
|
960
|
+
private _analytics;
|
|
853
961
|
private useRLS;
|
|
854
962
|
private debug;
|
|
855
963
|
constructor(options: KrisspyClientOptions);
|
|
@@ -898,6 +1006,23 @@ declare class KrisspyClient {
|
|
|
898
1006
|
* const { error } = await krisspy.storage.from('uploads').remove(['old.jpg'])
|
|
899
1007
|
*/
|
|
900
1008
|
get storage(): KrisspyStorage;
|
|
1009
|
+
/**
|
|
1010
|
+
* Analytics module for event tracking
|
|
1011
|
+
*
|
|
1012
|
+
* @example
|
|
1013
|
+
* // Initialize tracking
|
|
1014
|
+
* krisspy.analytics.init()
|
|
1015
|
+
*
|
|
1016
|
+
* // Track custom events
|
|
1017
|
+
* krisspy.analytics.track('button_click', { label: 'signup' })
|
|
1018
|
+
*
|
|
1019
|
+
* // Identify a user
|
|
1020
|
+
* krisspy.analytics.identify('user_123', { plan: 'pro' })
|
|
1021
|
+
*
|
|
1022
|
+
* // Cleanup
|
|
1023
|
+
* krisspy.analytics.destroy()
|
|
1024
|
+
*/
|
|
1025
|
+
get analytics(): KrisspyAnalytics;
|
|
901
1026
|
/**
|
|
902
1027
|
* Create a realtime channel for subscribing to database changes
|
|
903
1028
|
*
|
|
@@ -1081,4 +1206,4 @@ declare class KrisspyClient {
|
|
|
1081
1206
|
*/
|
|
1082
1207
|
declare function createClient(options: KrisspyClientOptions): KrisspyClient;
|
|
1083
1208
|
|
|
1084
|
-
export { type AuthChangeEvent, type AuthResponse, type ChannelState, type FileObject, type FileUploadOptions, type Filter, type FilterOperator, type FunctionInvokeOptions, type FunctionResponse, HttpClient, KrisspyAuth, KrisspyClient, type KrisspyClientOptions, type KrisspyError, KrisspyRealtime, KrisspyStorage, type MutationResponse, type OAuthProvider, type OrderBy, type PostgresChangesConfig, QueryBuilder, type QueryOptions, type QueryResponse, RealtimeChannel, type RealtimeEvent, type RealtimePayload, type Session, type SignInCredentials, type SignUpCredentials, type SingleResponse, StorageBucket, type UploadAndLinkOptions, type UploadAndLinkResponse, type User, createClient };
|
|
1209
|
+
export { type AnalyticsEvent, type AnalyticsInitOptions, type AuthChangeEvent, type AuthResponse, type ChannelState, type FileObject, type FileUploadOptions, type Filter, type FilterOperator, type FunctionInvokeOptions, type FunctionResponse, HttpClient, KrisspyAnalytics, KrisspyAuth, KrisspyClient, type KrisspyClientOptions, type KrisspyError, KrisspyRealtime, KrisspyStorage, type MutationResponse, type OAuthProvider, type OrderBy, type PostgresChangesConfig, QueryBuilder, type QueryOptions, type QueryResponse, RealtimeChannel, type RealtimeEvent, type RealtimePayload, type Session, type SignInCredentials, type SignUpCredentials, type SingleResponse, type StorageAdapter, StorageBucket, type UploadAndLinkOptions, type UploadAndLinkResponse, type User, createClient, isBrowser, isReactNative };
|
package/dist/index.d.ts
CHANGED
|
@@ -26,6 +26,20 @@ interface KrisspyClientOptions {
|
|
|
26
26
|
* When false, uses legacy /data/* endpoints which require backend owner auth.
|
|
27
27
|
*/
|
|
28
28
|
useRLS?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Custom storage adapter for session persistence.
|
|
31
|
+
* Use this in React Native with AsyncStorage or MMKV.
|
|
32
|
+
* Falls back to localStorage (browser) or in-memory storage.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* import AsyncStorage from '@react-native-async-storage/async-storage'
|
|
36
|
+
* const krisspy = createClient({ ..., storage: AsyncStorage })
|
|
37
|
+
*/
|
|
38
|
+
storage?: {
|
|
39
|
+
getItem(key: string): string | null | Promise<string | null>;
|
|
40
|
+
setItem(key: string, value: string): void | Promise<void>;
|
|
41
|
+
removeItem(key: string): void | Promise<void>;
|
|
42
|
+
};
|
|
29
43
|
}
|
|
30
44
|
interface User {
|
|
31
45
|
id: string;
|
|
@@ -184,8 +198,24 @@ declare class HttpClient {
|
|
|
184
198
|
delete<T>(path: string, params?: Record<string, any>): Promise<HttpResponse<T>>;
|
|
185
199
|
}
|
|
186
200
|
|
|
201
|
+
/**
|
|
202
|
+
* Platform detection and cross-platform utilities
|
|
203
|
+
* Safe to evaluate in any JS environment (browser, Node, React Native, Expo Snack)
|
|
204
|
+
*/
|
|
205
|
+
/** Storage adapter interface - allows custom storage (AsyncStorage, MMKV, etc.) */
|
|
206
|
+
interface StorageAdapter {
|
|
207
|
+
getItem(key: string): string | null | Promise<string | null>;
|
|
208
|
+
setItem(key: string, value: string): void | Promise<void>;
|
|
209
|
+
removeItem(key: string): void | Promise<void>;
|
|
210
|
+
}
|
|
211
|
+
/** Check if running in a browser with DOM APIs */
|
|
212
|
+
declare function isBrowser(): boolean;
|
|
213
|
+
/** Check if running in React Native */
|
|
214
|
+
declare function isReactNative(): boolean;
|
|
215
|
+
|
|
187
216
|
/**
|
|
188
217
|
* Auth Module - User authentication for Krisspy backends
|
|
218
|
+
* Compatible with Browser, React Native, and Node.js
|
|
189
219
|
*/
|
|
190
220
|
|
|
191
221
|
declare class KrisspyAuth {
|
|
@@ -195,11 +225,13 @@ declare class KrisspyAuth {
|
|
|
195
225
|
private currentUser;
|
|
196
226
|
private listeners;
|
|
197
227
|
private refreshInterval?;
|
|
198
|
-
|
|
228
|
+
private storage;
|
|
229
|
+
constructor(http: HttpClient, backendId: string, storage?: StorageAdapter);
|
|
199
230
|
/**
|
|
200
231
|
* Get current session from storage
|
|
201
232
|
*/
|
|
202
233
|
private restoreSession;
|
|
234
|
+
private hydrateSession;
|
|
203
235
|
/**
|
|
204
236
|
* Store session
|
|
205
237
|
*/
|
|
@@ -299,6 +331,7 @@ declare class KrisspyAuth {
|
|
|
299
331
|
};
|
|
300
332
|
/**
|
|
301
333
|
* Set session from external source (e.g., OAuth callback)
|
|
334
|
+
* Browser-only: parses tokens from URL hash
|
|
302
335
|
*/
|
|
303
336
|
setSessionFromUrl(): Promise<AuthResponse>;
|
|
304
337
|
}
|
|
@@ -699,6 +732,80 @@ declare class KrisspyRealtime {
|
|
|
699
732
|
private log;
|
|
700
733
|
}
|
|
701
734
|
|
|
735
|
+
/**
|
|
736
|
+
* Krisspy Analytics - Lightweight event tracking module
|
|
737
|
+
*
|
|
738
|
+
* Tracks page views, sessions, custom events. Sends batched events
|
|
739
|
+
* to the analytics ingest endpoint. Works with SPAs (intercepts pushState).
|
|
740
|
+
* Browser-only module - safely no-ops in React Native / Node.js.
|
|
741
|
+
*
|
|
742
|
+
* @example
|
|
743
|
+
* const krisspy = createClient({ backendId: '...', anonKey: '...' })
|
|
744
|
+
* krisspy.analytics.init({ autoTrackPageViews: true })
|
|
745
|
+
* krisspy.analytics.track('button_click', { label: 'signup' })
|
|
746
|
+
*/
|
|
747
|
+
|
|
748
|
+
interface AnalyticsInitOptions {
|
|
749
|
+
/** Auto-track page views on init + navigation (default: true) */
|
|
750
|
+
autoTrackPageViews?: boolean;
|
|
751
|
+
/** Auto-track SPA navigation via pushState/popstate (default: true) */
|
|
752
|
+
autoTrackNavigation?: boolean;
|
|
753
|
+
/** Flush interval in ms (default: 5000) */
|
|
754
|
+
flushInterval?: number;
|
|
755
|
+
/** Enable debug logging (default: false) */
|
|
756
|
+
debug?: boolean;
|
|
757
|
+
}
|
|
758
|
+
interface AnalyticsEvent {
|
|
759
|
+
eventName: string;
|
|
760
|
+
sessionId: string;
|
|
761
|
+
userId?: string;
|
|
762
|
+
properties?: Record<string, any>;
|
|
763
|
+
timestamp: string;
|
|
764
|
+
}
|
|
765
|
+
declare class KrisspyAnalytics {
|
|
766
|
+
private http;
|
|
767
|
+
private backendId;
|
|
768
|
+
private queue;
|
|
769
|
+
private sessionId;
|
|
770
|
+
private userId;
|
|
771
|
+
private userTraits;
|
|
772
|
+
private initialized;
|
|
773
|
+
private flushTimer;
|
|
774
|
+
private options;
|
|
775
|
+
private originalPushState;
|
|
776
|
+
private popstateHandler;
|
|
777
|
+
private unloadHandler;
|
|
778
|
+
constructor(http: HttpClient, backendId: string);
|
|
779
|
+
/**
|
|
780
|
+
* Initialize analytics tracking
|
|
781
|
+
* No-ops in non-browser environments (React Native, Node.js)
|
|
782
|
+
*/
|
|
783
|
+
init(options?: AnalyticsInitOptions): void;
|
|
784
|
+
/**
|
|
785
|
+
* Track a custom event
|
|
786
|
+
*/
|
|
787
|
+
track(eventName: string, properties?: Record<string, any>): void;
|
|
788
|
+
/**
|
|
789
|
+
* Identify a user (attach userId + traits to all future events)
|
|
790
|
+
*/
|
|
791
|
+
identify(userId: string, traits?: Record<string, any>): void;
|
|
792
|
+
/**
|
|
793
|
+
* Flush event queue to the server (async)
|
|
794
|
+
*/
|
|
795
|
+
flush(): Promise<void>;
|
|
796
|
+
/**
|
|
797
|
+
* Synchronous flush using sendBeacon (for page unload)
|
|
798
|
+
*/
|
|
799
|
+
private flushSync;
|
|
800
|
+
/**
|
|
801
|
+
* Stop tracking and cleanup all listeners/timers
|
|
802
|
+
*/
|
|
803
|
+
destroy(): void;
|
|
804
|
+
private getOrCreateSessionId;
|
|
805
|
+
private getIngestUrl;
|
|
806
|
+
private setupNavigationTracking;
|
|
807
|
+
}
|
|
808
|
+
|
|
702
809
|
/**
|
|
703
810
|
* Query Builder - Supabase-style fluent API for database queries
|
|
704
811
|
*/
|
|
@@ -850,6 +957,7 @@ declare class KrisspyClient {
|
|
|
850
957
|
private _auth;
|
|
851
958
|
private _storage;
|
|
852
959
|
private _realtime;
|
|
960
|
+
private _analytics;
|
|
853
961
|
private useRLS;
|
|
854
962
|
private debug;
|
|
855
963
|
constructor(options: KrisspyClientOptions);
|
|
@@ -898,6 +1006,23 @@ declare class KrisspyClient {
|
|
|
898
1006
|
* const { error } = await krisspy.storage.from('uploads').remove(['old.jpg'])
|
|
899
1007
|
*/
|
|
900
1008
|
get storage(): KrisspyStorage;
|
|
1009
|
+
/**
|
|
1010
|
+
* Analytics module for event tracking
|
|
1011
|
+
*
|
|
1012
|
+
* @example
|
|
1013
|
+
* // Initialize tracking
|
|
1014
|
+
* krisspy.analytics.init()
|
|
1015
|
+
*
|
|
1016
|
+
* // Track custom events
|
|
1017
|
+
* krisspy.analytics.track('button_click', { label: 'signup' })
|
|
1018
|
+
*
|
|
1019
|
+
* // Identify a user
|
|
1020
|
+
* krisspy.analytics.identify('user_123', { plan: 'pro' })
|
|
1021
|
+
*
|
|
1022
|
+
* // Cleanup
|
|
1023
|
+
* krisspy.analytics.destroy()
|
|
1024
|
+
*/
|
|
1025
|
+
get analytics(): KrisspyAnalytics;
|
|
901
1026
|
/**
|
|
902
1027
|
* Create a realtime channel for subscribing to database changes
|
|
903
1028
|
*
|
|
@@ -1081,4 +1206,4 @@ declare class KrisspyClient {
|
|
|
1081
1206
|
*/
|
|
1082
1207
|
declare function createClient(options: KrisspyClientOptions): KrisspyClient;
|
|
1083
1208
|
|
|
1084
|
-
export { type AuthChangeEvent, type AuthResponse, type ChannelState, type FileObject, type FileUploadOptions, type Filter, type FilterOperator, type FunctionInvokeOptions, type FunctionResponse, HttpClient, KrisspyAuth, KrisspyClient, type KrisspyClientOptions, type KrisspyError, KrisspyRealtime, KrisspyStorage, type MutationResponse, type OAuthProvider, type OrderBy, type PostgresChangesConfig, QueryBuilder, type QueryOptions, type QueryResponse, RealtimeChannel, type RealtimeEvent, type RealtimePayload, type Session, type SignInCredentials, type SignUpCredentials, type SingleResponse, StorageBucket, type UploadAndLinkOptions, type UploadAndLinkResponse, type User, createClient };
|
|
1209
|
+
export { type AnalyticsEvent, type AnalyticsInitOptions, type AuthChangeEvent, type AuthResponse, type ChannelState, type FileObject, type FileUploadOptions, type Filter, type FilterOperator, type FunctionInvokeOptions, type FunctionResponse, HttpClient, KrisspyAnalytics, KrisspyAuth, KrisspyClient, type KrisspyClientOptions, type KrisspyError, KrisspyRealtime, KrisspyStorage, type MutationResponse, type OAuthProvider, type OrderBy, type PostgresChangesConfig, QueryBuilder, type QueryOptions, type QueryResponse, RealtimeChannel, type RealtimeEvent, type RealtimePayload, type Session, type SignInCredentials, type SignUpCredentials, type SingleResponse, type StorageAdapter, StorageBucket, type UploadAndLinkOptions, type UploadAndLinkResponse, type User, createClient, isBrowser, isReactNative };
|
package/dist/index.js
CHANGED
|
@@ -21,6 +21,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
HttpClient: () => HttpClient,
|
|
24
|
+
KrisspyAnalytics: () => KrisspyAnalytics,
|
|
24
25
|
KrisspyAuth: () => KrisspyAuth,
|
|
25
26
|
KrisspyClient: () => KrisspyClient,
|
|
26
27
|
KrisspyRealtime: () => KrisspyRealtime,
|
|
@@ -28,7 +29,9 @@ __export(index_exports, {
|
|
|
28
29
|
QueryBuilder: () => QueryBuilder,
|
|
29
30
|
RealtimeChannel: () => RealtimeChannel,
|
|
30
31
|
StorageBucket: () => StorageBucket,
|
|
31
|
-
createClient: () => createClient
|
|
32
|
+
createClient: () => createClient,
|
|
33
|
+
isBrowser: () => isBrowser,
|
|
34
|
+
isReactNative: () => isReactNative
|
|
32
35
|
});
|
|
33
36
|
module.exports = __toCommonJS(index_exports);
|
|
34
37
|
|
|
@@ -161,43 +164,136 @@ var HttpClient = class {
|
|
|
161
164
|
}
|
|
162
165
|
};
|
|
163
166
|
|
|
167
|
+
// src/platform.ts
|
|
168
|
+
function isBrowser() {
|
|
169
|
+
return typeof window !== "undefined" && typeof window.document !== "undefined" && typeof window.document.createElement === "function";
|
|
170
|
+
}
|
|
171
|
+
function isReactNative() {
|
|
172
|
+
return typeof navigator !== "undefined" && typeof navigator.product === "string" && navigator.product === "ReactNative";
|
|
173
|
+
}
|
|
174
|
+
function getLocalStorage() {
|
|
175
|
+
if (!isBrowser()) return null;
|
|
176
|
+
try {
|
|
177
|
+
const testKey = "__krisspy_test__";
|
|
178
|
+
window.localStorage.setItem(testKey, "1");
|
|
179
|
+
window.localStorage.removeItem(testKey);
|
|
180
|
+
return window.localStorage;
|
|
181
|
+
} catch {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
var MemoryStorage = class {
|
|
186
|
+
constructor() {
|
|
187
|
+
this.store = /* @__PURE__ */ new Map();
|
|
188
|
+
}
|
|
189
|
+
getItem(key) {
|
|
190
|
+
return this.store.get(key) ?? null;
|
|
191
|
+
}
|
|
192
|
+
setItem(key, value) {
|
|
193
|
+
this.store.set(key, value);
|
|
194
|
+
}
|
|
195
|
+
removeItem(key) {
|
|
196
|
+
this.store.delete(key);
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
function resolveStorage(custom) {
|
|
200
|
+
if (custom) return custom;
|
|
201
|
+
return getLocalStorage() ?? new MemoryStorage();
|
|
202
|
+
}
|
|
203
|
+
function base64Encode(input) {
|
|
204
|
+
if (typeof btoa === "function") {
|
|
205
|
+
return btoa(input);
|
|
206
|
+
}
|
|
207
|
+
if (typeof Buffer !== "undefined") {
|
|
208
|
+
return Buffer.from(input, "binary").toString("base64");
|
|
209
|
+
}
|
|
210
|
+
return manualBase64Encode(input);
|
|
211
|
+
}
|
|
212
|
+
function base64Decode(input) {
|
|
213
|
+
if (typeof atob === "function") {
|
|
214
|
+
return atob(input);
|
|
215
|
+
}
|
|
216
|
+
if (typeof Buffer !== "undefined") {
|
|
217
|
+
return Buffer.from(input, "base64").toString("binary");
|
|
218
|
+
}
|
|
219
|
+
return manualBase64Decode(input);
|
|
220
|
+
}
|
|
221
|
+
var CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
|
|
222
|
+
function manualBase64Encode(input) {
|
|
223
|
+
let output = "";
|
|
224
|
+
for (let i = 0; i < input.length; i += 3) {
|
|
225
|
+
const a = input.charCodeAt(i);
|
|
226
|
+
const b = i + 1 < input.length ? input.charCodeAt(i + 1) : 0;
|
|
227
|
+
const c = i + 2 < input.length ? input.charCodeAt(i + 2) : 0;
|
|
228
|
+
output += CHARS[a >> 2];
|
|
229
|
+
output += CHARS[(a & 3) << 4 | b >> 4];
|
|
230
|
+
output += i + 1 < input.length ? CHARS[(b & 15) << 2 | c >> 6] : "=";
|
|
231
|
+
output += i + 2 < input.length ? CHARS[c & 63] : "=";
|
|
232
|
+
}
|
|
233
|
+
return output;
|
|
234
|
+
}
|
|
235
|
+
function manualBase64Decode(input) {
|
|
236
|
+
let output = "";
|
|
237
|
+
const str = input.replace(/=+$/, "");
|
|
238
|
+
for (let i = 0; i < str.length; i += 4) {
|
|
239
|
+
const a = CHARS.indexOf(str[i]);
|
|
240
|
+
const b = CHARS.indexOf(str[i + 1]);
|
|
241
|
+
const c = CHARS.indexOf(str[i + 2]);
|
|
242
|
+
const d = CHARS.indexOf(str[i + 3]);
|
|
243
|
+
output += String.fromCharCode(a << 2 | b >> 4);
|
|
244
|
+
if (c !== -1) output += String.fromCharCode((b & 15) << 4 | c >> 2);
|
|
245
|
+
if (d !== -1) output += String.fromCharCode((c & 3) << 6 | d);
|
|
246
|
+
}
|
|
247
|
+
return output;
|
|
248
|
+
}
|
|
249
|
+
|
|
164
250
|
// src/auth.ts
|
|
165
251
|
var STORAGE_KEY = "krisspy-auth-session";
|
|
166
252
|
var KrisspyAuth = class {
|
|
167
|
-
constructor(http, backendId) {
|
|
253
|
+
constructor(http, backendId, storage) {
|
|
168
254
|
this.currentSession = null;
|
|
169
255
|
this.currentUser = null;
|
|
170
256
|
this.listeners = [];
|
|
171
257
|
this.http = http;
|
|
172
258
|
this.backendId = backendId;
|
|
259
|
+
this.storage = resolveStorage(storage);
|
|
173
260
|
this.restoreSession();
|
|
174
261
|
}
|
|
175
262
|
/**
|
|
176
263
|
* Get current session from storage
|
|
177
264
|
*/
|
|
178
265
|
restoreSession() {
|
|
179
|
-
if (typeof window === "undefined") return;
|
|
180
266
|
try {
|
|
181
|
-
const
|
|
182
|
-
if (
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
this.
|
|
267
|
+
const result = this.storage.getItem(`${STORAGE_KEY}-${this.backendId}`);
|
|
268
|
+
if (result instanceof Promise) {
|
|
269
|
+
result.then((stored) => {
|
|
270
|
+
if (stored) this.hydrateSession(stored);
|
|
271
|
+
}).catch(() => {
|
|
272
|
+
});
|
|
273
|
+
} else if (result) {
|
|
274
|
+
this.hydrateSession(result);
|
|
189
275
|
}
|
|
190
276
|
} catch (err) {
|
|
191
277
|
console.warn("[Krisspy Auth] Failed to restore session:", err);
|
|
192
278
|
}
|
|
193
279
|
}
|
|
280
|
+
hydrateSession(stored) {
|
|
281
|
+
try {
|
|
282
|
+
const session = JSON.parse(stored);
|
|
283
|
+
if (session.expires_at && Date.now() > session.expires_at * 1e3) {
|
|
284
|
+
this.clearSession();
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
this.setSession(session);
|
|
288
|
+
} catch {
|
|
289
|
+
}
|
|
290
|
+
}
|
|
194
291
|
/**
|
|
195
292
|
* Store session
|
|
196
293
|
*/
|
|
197
294
|
saveSession(session) {
|
|
198
|
-
if (typeof window === "undefined") return;
|
|
199
295
|
try {
|
|
200
|
-
|
|
296
|
+
this.storage.setItem(`${STORAGE_KEY}-${this.backendId}`, JSON.stringify(session));
|
|
201
297
|
} catch (err) {
|
|
202
298
|
console.warn("[Krisspy Auth] Failed to save session:", err);
|
|
203
299
|
}
|
|
@@ -213,8 +309,9 @@ var KrisspyAuth = class {
|
|
|
213
309
|
clearInterval(this.refreshInterval);
|
|
214
310
|
this.refreshInterval = void 0;
|
|
215
311
|
}
|
|
216
|
-
|
|
217
|
-
|
|
312
|
+
try {
|
|
313
|
+
this.storage.removeItem(`${STORAGE_KEY}-${this.backendId}`);
|
|
314
|
+
} catch {
|
|
218
315
|
}
|
|
219
316
|
}
|
|
220
317
|
/**
|
|
@@ -319,7 +416,7 @@ var KrisspyAuth = class {
|
|
|
319
416
|
if (response.error) {
|
|
320
417
|
return { data: null, error: response.error };
|
|
321
418
|
}
|
|
322
|
-
if (
|
|
419
|
+
if (isBrowser() && response.data?.url) {
|
|
323
420
|
window.location.href = response.data.url;
|
|
324
421
|
}
|
|
325
422
|
return { data: response.data, error: null };
|
|
@@ -442,9 +539,10 @@ var KrisspyAuth = class {
|
|
|
442
539
|
}
|
|
443
540
|
/**
|
|
444
541
|
* Set session from external source (e.g., OAuth callback)
|
|
542
|
+
* Browser-only: parses tokens from URL hash
|
|
445
543
|
*/
|
|
446
544
|
async setSessionFromUrl() {
|
|
447
|
-
if (
|
|
545
|
+
if (!isBrowser()) {
|
|
448
546
|
return { data: { user: null, session: null }, error: { message: "Not in browser" } };
|
|
449
547
|
}
|
|
450
548
|
const hash = window.location.hash.substring(1);
|
|
@@ -455,7 +553,7 @@ var KrisspyAuth = class {
|
|
|
455
553
|
return { data: { user: null, session: null }, error: { message: "No access token in URL" } };
|
|
456
554
|
}
|
|
457
555
|
try {
|
|
458
|
-
const payload = JSON.parse(
|
|
556
|
+
const payload = JSON.parse(base64Decode(accessToken.split(".")[1]));
|
|
459
557
|
const session = {
|
|
460
558
|
access_token: accessToken,
|
|
461
559
|
refresh_token: refreshToken ?? void 0,
|
|
@@ -763,27 +861,31 @@ var StorageBucket = class {
|
|
|
763
861
|
};
|
|
764
862
|
}
|
|
765
863
|
}
|
|
766
|
-
// Helper: Convert Blob to base64
|
|
767
|
-
blobToBase64(blob) {
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
864
|
+
// Helper: Convert Blob to base64 (cross-platform)
|
|
865
|
+
async blobToBase64(blob) {
|
|
866
|
+
if (typeof FileReader !== "undefined") {
|
|
867
|
+
return new Promise((resolve, reject) => {
|
|
868
|
+
const reader = new FileReader();
|
|
869
|
+
reader.onloadend = () => {
|
|
870
|
+
const result = reader.result;
|
|
871
|
+
const base64 = result.includes(",") ? result.split(",")[1] : result;
|
|
872
|
+
resolve(base64);
|
|
873
|
+
};
|
|
874
|
+
reader.onerror = reject;
|
|
875
|
+
reader.readAsDataURL(blob);
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
const buffer = await blob.arrayBuffer();
|
|
879
|
+
return this.arrayBufferToBase64(buffer);
|
|
778
880
|
}
|
|
779
|
-
// Helper: Convert ArrayBuffer to base64
|
|
881
|
+
// Helper: Convert ArrayBuffer to base64 (cross-platform)
|
|
780
882
|
arrayBufferToBase64(buffer) {
|
|
781
|
-
let binary = "";
|
|
782
883
|
const bytes = new Uint8Array(buffer);
|
|
884
|
+
let binary = "";
|
|
783
885
|
for (let i = 0; i < bytes.length; i++) {
|
|
784
886
|
binary += String.fromCharCode(bytes[i]);
|
|
785
887
|
}
|
|
786
|
-
return
|
|
888
|
+
return base64Encode(binary);
|
|
787
889
|
}
|
|
788
890
|
// Helper: Detect MIME type from file extension
|
|
789
891
|
detectMimeType(path) {
|
|
@@ -1144,6 +1246,188 @@ var KrisspyRealtime = class {
|
|
|
1144
1246
|
}
|
|
1145
1247
|
};
|
|
1146
1248
|
|
|
1249
|
+
// src/analytics.ts
|
|
1250
|
+
var KrisspyAnalytics = class {
|
|
1251
|
+
constructor(http, backendId) {
|
|
1252
|
+
this.queue = [];
|
|
1253
|
+
this.sessionId = null;
|
|
1254
|
+
this.userId = null;
|
|
1255
|
+
this.userTraits = {};
|
|
1256
|
+
this.initialized = false;
|
|
1257
|
+
this.flushTimer = null;
|
|
1258
|
+
this.options = {};
|
|
1259
|
+
this.originalPushState = null;
|
|
1260
|
+
this.popstateHandler = null;
|
|
1261
|
+
this.unloadHandler = null;
|
|
1262
|
+
this.http = http;
|
|
1263
|
+
this.backendId = backendId;
|
|
1264
|
+
}
|
|
1265
|
+
/**
|
|
1266
|
+
* Initialize analytics tracking
|
|
1267
|
+
* No-ops in non-browser environments (React Native, Node.js)
|
|
1268
|
+
*/
|
|
1269
|
+
init(options) {
|
|
1270
|
+
if (this.initialized) return;
|
|
1271
|
+
if (!isBrowser()) return;
|
|
1272
|
+
this.options = {
|
|
1273
|
+
autoTrackPageViews: true,
|
|
1274
|
+
autoTrackNavigation: true,
|
|
1275
|
+
flushInterval: 5e3,
|
|
1276
|
+
debug: false,
|
|
1277
|
+
...options
|
|
1278
|
+
};
|
|
1279
|
+
this.sessionId = this.getOrCreateSessionId();
|
|
1280
|
+
this.initialized = true;
|
|
1281
|
+
if (this.options.autoTrackPageViews) {
|
|
1282
|
+
this.track("session_start", {});
|
|
1283
|
+
this.track("page_view", { url: window.location.href, referrer: document.referrer });
|
|
1284
|
+
}
|
|
1285
|
+
if (this.options.autoTrackNavigation) {
|
|
1286
|
+
this.setupNavigationTracking();
|
|
1287
|
+
}
|
|
1288
|
+
this.unloadHandler = () => {
|
|
1289
|
+
this.track("session_end", {});
|
|
1290
|
+
this.flushSync();
|
|
1291
|
+
};
|
|
1292
|
+
window.addEventListener("beforeunload", this.unloadHandler);
|
|
1293
|
+
this.flushTimer = setInterval(() => this.flush(), this.options.flushInterval);
|
|
1294
|
+
if (this.options.debug) {
|
|
1295
|
+
console.log("[Krisspy Analytics] Initialized", { backendId: this.backendId, sessionId: this.sessionId });
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
/**
|
|
1299
|
+
* Track a custom event
|
|
1300
|
+
*/
|
|
1301
|
+
track(eventName, properties) {
|
|
1302
|
+
if (!this.initialized) return;
|
|
1303
|
+
const event = {
|
|
1304
|
+
eventName,
|
|
1305
|
+
sessionId: this.sessionId,
|
|
1306
|
+
userId: this.userId || void 0,
|
|
1307
|
+
properties: {
|
|
1308
|
+
...properties,
|
|
1309
|
+
...this.userId ? { _user_traits: this.userTraits } : {}
|
|
1310
|
+
},
|
|
1311
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1312
|
+
};
|
|
1313
|
+
this.queue.push(event);
|
|
1314
|
+
if (this.options.debug) {
|
|
1315
|
+
console.log("[Krisspy Analytics] Tracked:", eventName, properties);
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
/**
|
|
1319
|
+
* Identify a user (attach userId + traits to all future events)
|
|
1320
|
+
*/
|
|
1321
|
+
identify(userId, traits) {
|
|
1322
|
+
this.userId = userId;
|
|
1323
|
+
this.userTraits = traits || {};
|
|
1324
|
+
if (this.initialized) {
|
|
1325
|
+
this.track("identify", { userId, ...traits });
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
/**
|
|
1329
|
+
* Flush event queue to the server (async)
|
|
1330
|
+
*/
|
|
1331
|
+
async flush() {
|
|
1332
|
+
if (!this.queue.length) return;
|
|
1333
|
+
const events = this.queue.splice(0, 50);
|
|
1334
|
+
const payload = { appId: this.backendId, events };
|
|
1335
|
+
try {
|
|
1336
|
+
await this.http.post("/api/v1/analytics/events", payload);
|
|
1337
|
+
if (this.options.debug) {
|
|
1338
|
+
console.log(`[Krisspy Analytics] Flushed ${events.length} events`);
|
|
1339
|
+
}
|
|
1340
|
+
} catch {
|
|
1341
|
+
this.queue.unshift(...events);
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
/**
|
|
1345
|
+
* Synchronous flush using sendBeacon (for page unload)
|
|
1346
|
+
*/
|
|
1347
|
+
flushSync() {
|
|
1348
|
+
if (!this.queue.length) return;
|
|
1349
|
+
if (!isBrowser()) return;
|
|
1350
|
+
const events = this.queue.splice(0, 50);
|
|
1351
|
+
const payload = JSON.stringify({ appId: this.backendId, events });
|
|
1352
|
+
try {
|
|
1353
|
+
if (typeof navigator !== "undefined" && navigator.sendBeacon) {
|
|
1354
|
+
const blob = new Blob([payload], { type: "application/json" });
|
|
1355
|
+
navigator.sendBeacon(this.getIngestUrl(), blob);
|
|
1356
|
+
} else {
|
|
1357
|
+
fetch(this.getIngestUrl(), {
|
|
1358
|
+
method: "POST",
|
|
1359
|
+
headers: { "Content-Type": "application/json" },
|
|
1360
|
+
body: payload,
|
|
1361
|
+
keepalive: true
|
|
1362
|
+
}).catch(() => {
|
|
1363
|
+
});
|
|
1364
|
+
}
|
|
1365
|
+
} catch {
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
/**
|
|
1369
|
+
* Stop tracking and cleanup all listeners/timers
|
|
1370
|
+
*/
|
|
1371
|
+
destroy() {
|
|
1372
|
+
if (!this.initialized) return;
|
|
1373
|
+
this.flushSync();
|
|
1374
|
+
if (this.flushTimer) {
|
|
1375
|
+
clearInterval(this.flushTimer);
|
|
1376
|
+
this.flushTimer = null;
|
|
1377
|
+
}
|
|
1378
|
+
if (isBrowser()) {
|
|
1379
|
+
if (this.originalPushState) {
|
|
1380
|
+
history.pushState = this.originalPushState;
|
|
1381
|
+
this.originalPushState = null;
|
|
1382
|
+
}
|
|
1383
|
+
if (this.popstateHandler) {
|
|
1384
|
+
window.removeEventListener("popstate", this.popstateHandler);
|
|
1385
|
+
this.popstateHandler = null;
|
|
1386
|
+
}
|
|
1387
|
+
if (this.unloadHandler) {
|
|
1388
|
+
window.removeEventListener("beforeunload", this.unloadHandler);
|
|
1389
|
+
this.unloadHandler = null;
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
this.initialized = false;
|
|
1393
|
+
this.queue = [];
|
|
1394
|
+
if (this.options.debug) {
|
|
1395
|
+
console.log("[Krisspy Analytics] Destroyed");
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
// -- Private helpers --------------------------------------------------------
|
|
1399
|
+
getOrCreateSessionId() {
|
|
1400
|
+
const key = "_ka_sid";
|
|
1401
|
+
if (isBrowser()) {
|
|
1402
|
+
try {
|
|
1403
|
+
const existing = window.localStorage.getItem(key);
|
|
1404
|
+
if (existing) return existing;
|
|
1405
|
+
const id = Math.random().toString(36).substring(2, 15) + Date.now().toString(36);
|
|
1406
|
+
window.localStorage.setItem(key, id);
|
|
1407
|
+
return id;
|
|
1408
|
+
} catch {
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
return Math.random().toString(36).substring(2, 15) + Date.now().toString(36);
|
|
1412
|
+
}
|
|
1413
|
+
getIngestUrl() {
|
|
1414
|
+
return `${this.http.baseUrl}/api/v1/analytics/events`;
|
|
1415
|
+
}
|
|
1416
|
+
setupNavigationTracking() {
|
|
1417
|
+
if (!isBrowser()) return;
|
|
1418
|
+
this.originalPushState = history.pushState.bind(history);
|
|
1419
|
+
const self = this;
|
|
1420
|
+
history.pushState = function(...args) {
|
|
1421
|
+
self.originalPushState(...args);
|
|
1422
|
+
self.track("page_view", { url: window.location.href });
|
|
1423
|
+
};
|
|
1424
|
+
this.popstateHandler = () => {
|
|
1425
|
+
this.track("page_view", { url: window.location.href });
|
|
1426
|
+
};
|
|
1427
|
+
window.addEventListener("popstate", this.popstateHandler);
|
|
1428
|
+
}
|
|
1429
|
+
};
|
|
1430
|
+
|
|
1147
1431
|
// src/query-builder.ts
|
|
1148
1432
|
var QueryBuilder = class {
|
|
1149
1433
|
constructor(http, backendId, tableName, useRLS = true) {
|
|
@@ -1435,9 +1719,10 @@ var KrisspyClient = class {
|
|
|
1435
1719
|
},
|
|
1436
1720
|
debug: options.debug
|
|
1437
1721
|
});
|
|
1438
|
-
this._auth = new KrisspyAuth(this.http, this.backendId);
|
|
1722
|
+
this._auth = new KrisspyAuth(this.http, this.backendId, options.storage);
|
|
1439
1723
|
this._storage = new KrisspyStorage(this.http, this.backendId);
|
|
1440
1724
|
this._realtime = new KrisspyRealtime(this.baseUrl, this.backendId, this.debug);
|
|
1725
|
+
this._analytics = new KrisspyAnalytics(this.http, this.backendId);
|
|
1441
1726
|
this._auth.onAuthStateChange((event) => {
|
|
1442
1727
|
if (event === "SIGNED_IN") {
|
|
1443
1728
|
const session = this._auth.session();
|
|
@@ -1498,6 +1783,25 @@ var KrisspyClient = class {
|
|
|
1498
1783
|
get storage() {
|
|
1499
1784
|
return this._storage;
|
|
1500
1785
|
}
|
|
1786
|
+
/**
|
|
1787
|
+
* Analytics module for event tracking
|
|
1788
|
+
*
|
|
1789
|
+
* @example
|
|
1790
|
+
* // Initialize tracking
|
|
1791
|
+
* krisspy.analytics.init()
|
|
1792
|
+
*
|
|
1793
|
+
* // Track custom events
|
|
1794
|
+
* krisspy.analytics.track('button_click', { label: 'signup' })
|
|
1795
|
+
*
|
|
1796
|
+
* // Identify a user
|
|
1797
|
+
* krisspy.analytics.identify('user_123', { plan: 'pro' })
|
|
1798
|
+
*
|
|
1799
|
+
* // Cleanup
|
|
1800
|
+
* krisspy.analytics.destroy()
|
|
1801
|
+
*/
|
|
1802
|
+
get analytics() {
|
|
1803
|
+
return this._analytics;
|
|
1804
|
+
}
|
|
1501
1805
|
/**
|
|
1502
1806
|
* Create a realtime channel for subscribing to database changes
|
|
1503
1807
|
*
|
|
@@ -1666,6 +1970,7 @@ function createClient(options) {
|
|
|
1666
1970
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1667
1971
|
0 && (module.exports = {
|
|
1668
1972
|
HttpClient,
|
|
1973
|
+
KrisspyAnalytics,
|
|
1669
1974
|
KrisspyAuth,
|
|
1670
1975
|
KrisspyClient,
|
|
1671
1976
|
KrisspyRealtime,
|
|
@@ -1673,5 +1978,7 @@ function createClient(options) {
|
|
|
1673
1978
|
QueryBuilder,
|
|
1674
1979
|
RealtimeChannel,
|
|
1675
1980
|
StorageBucket,
|
|
1676
|
-
createClient
|
|
1981
|
+
createClient,
|
|
1982
|
+
isBrowser,
|
|
1983
|
+
isReactNative
|
|
1677
1984
|
});
|
package/dist/index.mjs
CHANGED
|
@@ -127,43 +127,136 @@ var HttpClient = class {
|
|
|
127
127
|
}
|
|
128
128
|
};
|
|
129
129
|
|
|
130
|
+
// src/platform.ts
|
|
131
|
+
function isBrowser() {
|
|
132
|
+
return typeof window !== "undefined" && typeof window.document !== "undefined" && typeof window.document.createElement === "function";
|
|
133
|
+
}
|
|
134
|
+
function isReactNative() {
|
|
135
|
+
return typeof navigator !== "undefined" && typeof navigator.product === "string" && navigator.product === "ReactNative";
|
|
136
|
+
}
|
|
137
|
+
function getLocalStorage() {
|
|
138
|
+
if (!isBrowser()) return null;
|
|
139
|
+
try {
|
|
140
|
+
const testKey = "__krisspy_test__";
|
|
141
|
+
window.localStorage.setItem(testKey, "1");
|
|
142
|
+
window.localStorage.removeItem(testKey);
|
|
143
|
+
return window.localStorage;
|
|
144
|
+
} catch {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
var MemoryStorage = class {
|
|
149
|
+
constructor() {
|
|
150
|
+
this.store = /* @__PURE__ */ new Map();
|
|
151
|
+
}
|
|
152
|
+
getItem(key) {
|
|
153
|
+
return this.store.get(key) ?? null;
|
|
154
|
+
}
|
|
155
|
+
setItem(key, value) {
|
|
156
|
+
this.store.set(key, value);
|
|
157
|
+
}
|
|
158
|
+
removeItem(key) {
|
|
159
|
+
this.store.delete(key);
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
function resolveStorage(custom) {
|
|
163
|
+
if (custom) return custom;
|
|
164
|
+
return getLocalStorage() ?? new MemoryStorage();
|
|
165
|
+
}
|
|
166
|
+
function base64Encode(input) {
|
|
167
|
+
if (typeof btoa === "function") {
|
|
168
|
+
return btoa(input);
|
|
169
|
+
}
|
|
170
|
+
if (typeof Buffer !== "undefined") {
|
|
171
|
+
return Buffer.from(input, "binary").toString("base64");
|
|
172
|
+
}
|
|
173
|
+
return manualBase64Encode(input);
|
|
174
|
+
}
|
|
175
|
+
function base64Decode(input) {
|
|
176
|
+
if (typeof atob === "function") {
|
|
177
|
+
return atob(input);
|
|
178
|
+
}
|
|
179
|
+
if (typeof Buffer !== "undefined") {
|
|
180
|
+
return Buffer.from(input, "base64").toString("binary");
|
|
181
|
+
}
|
|
182
|
+
return manualBase64Decode(input);
|
|
183
|
+
}
|
|
184
|
+
var CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
|
|
185
|
+
function manualBase64Encode(input) {
|
|
186
|
+
let output = "";
|
|
187
|
+
for (let i = 0; i < input.length; i += 3) {
|
|
188
|
+
const a = input.charCodeAt(i);
|
|
189
|
+
const b = i + 1 < input.length ? input.charCodeAt(i + 1) : 0;
|
|
190
|
+
const c = i + 2 < input.length ? input.charCodeAt(i + 2) : 0;
|
|
191
|
+
output += CHARS[a >> 2];
|
|
192
|
+
output += CHARS[(a & 3) << 4 | b >> 4];
|
|
193
|
+
output += i + 1 < input.length ? CHARS[(b & 15) << 2 | c >> 6] : "=";
|
|
194
|
+
output += i + 2 < input.length ? CHARS[c & 63] : "=";
|
|
195
|
+
}
|
|
196
|
+
return output;
|
|
197
|
+
}
|
|
198
|
+
function manualBase64Decode(input) {
|
|
199
|
+
let output = "";
|
|
200
|
+
const str = input.replace(/=+$/, "");
|
|
201
|
+
for (let i = 0; i < str.length; i += 4) {
|
|
202
|
+
const a = CHARS.indexOf(str[i]);
|
|
203
|
+
const b = CHARS.indexOf(str[i + 1]);
|
|
204
|
+
const c = CHARS.indexOf(str[i + 2]);
|
|
205
|
+
const d = CHARS.indexOf(str[i + 3]);
|
|
206
|
+
output += String.fromCharCode(a << 2 | b >> 4);
|
|
207
|
+
if (c !== -1) output += String.fromCharCode((b & 15) << 4 | c >> 2);
|
|
208
|
+
if (d !== -1) output += String.fromCharCode((c & 3) << 6 | d);
|
|
209
|
+
}
|
|
210
|
+
return output;
|
|
211
|
+
}
|
|
212
|
+
|
|
130
213
|
// src/auth.ts
|
|
131
214
|
var STORAGE_KEY = "krisspy-auth-session";
|
|
132
215
|
var KrisspyAuth = class {
|
|
133
|
-
constructor(http, backendId) {
|
|
216
|
+
constructor(http, backendId, storage) {
|
|
134
217
|
this.currentSession = null;
|
|
135
218
|
this.currentUser = null;
|
|
136
219
|
this.listeners = [];
|
|
137
220
|
this.http = http;
|
|
138
221
|
this.backendId = backendId;
|
|
222
|
+
this.storage = resolveStorage(storage);
|
|
139
223
|
this.restoreSession();
|
|
140
224
|
}
|
|
141
225
|
/**
|
|
142
226
|
* Get current session from storage
|
|
143
227
|
*/
|
|
144
228
|
restoreSession() {
|
|
145
|
-
if (typeof window === "undefined") return;
|
|
146
229
|
try {
|
|
147
|
-
const
|
|
148
|
-
if (
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
this.
|
|
230
|
+
const result = this.storage.getItem(`${STORAGE_KEY}-${this.backendId}`);
|
|
231
|
+
if (result instanceof Promise) {
|
|
232
|
+
result.then((stored) => {
|
|
233
|
+
if (stored) this.hydrateSession(stored);
|
|
234
|
+
}).catch(() => {
|
|
235
|
+
});
|
|
236
|
+
} else if (result) {
|
|
237
|
+
this.hydrateSession(result);
|
|
155
238
|
}
|
|
156
239
|
} catch (err) {
|
|
157
240
|
console.warn("[Krisspy Auth] Failed to restore session:", err);
|
|
158
241
|
}
|
|
159
242
|
}
|
|
243
|
+
hydrateSession(stored) {
|
|
244
|
+
try {
|
|
245
|
+
const session = JSON.parse(stored);
|
|
246
|
+
if (session.expires_at && Date.now() > session.expires_at * 1e3) {
|
|
247
|
+
this.clearSession();
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
this.setSession(session);
|
|
251
|
+
} catch {
|
|
252
|
+
}
|
|
253
|
+
}
|
|
160
254
|
/**
|
|
161
255
|
* Store session
|
|
162
256
|
*/
|
|
163
257
|
saveSession(session) {
|
|
164
|
-
if (typeof window === "undefined") return;
|
|
165
258
|
try {
|
|
166
|
-
|
|
259
|
+
this.storage.setItem(`${STORAGE_KEY}-${this.backendId}`, JSON.stringify(session));
|
|
167
260
|
} catch (err) {
|
|
168
261
|
console.warn("[Krisspy Auth] Failed to save session:", err);
|
|
169
262
|
}
|
|
@@ -179,8 +272,9 @@ var KrisspyAuth = class {
|
|
|
179
272
|
clearInterval(this.refreshInterval);
|
|
180
273
|
this.refreshInterval = void 0;
|
|
181
274
|
}
|
|
182
|
-
|
|
183
|
-
|
|
275
|
+
try {
|
|
276
|
+
this.storage.removeItem(`${STORAGE_KEY}-${this.backendId}`);
|
|
277
|
+
} catch {
|
|
184
278
|
}
|
|
185
279
|
}
|
|
186
280
|
/**
|
|
@@ -285,7 +379,7 @@ var KrisspyAuth = class {
|
|
|
285
379
|
if (response.error) {
|
|
286
380
|
return { data: null, error: response.error };
|
|
287
381
|
}
|
|
288
|
-
if (
|
|
382
|
+
if (isBrowser() && response.data?.url) {
|
|
289
383
|
window.location.href = response.data.url;
|
|
290
384
|
}
|
|
291
385
|
return { data: response.data, error: null };
|
|
@@ -408,9 +502,10 @@ var KrisspyAuth = class {
|
|
|
408
502
|
}
|
|
409
503
|
/**
|
|
410
504
|
* Set session from external source (e.g., OAuth callback)
|
|
505
|
+
* Browser-only: parses tokens from URL hash
|
|
411
506
|
*/
|
|
412
507
|
async setSessionFromUrl() {
|
|
413
|
-
if (
|
|
508
|
+
if (!isBrowser()) {
|
|
414
509
|
return { data: { user: null, session: null }, error: { message: "Not in browser" } };
|
|
415
510
|
}
|
|
416
511
|
const hash = window.location.hash.substring(1);
|
|
@@ -421,7 +516,7 @@ var KrisspyAuth = class {
|
|
|
421
516
|
return { data: { user: null, session: null }, error: { message: "No access token in URL" } };
|
|
422
517
|
}
|
|
423
518
|
try {
|
|
424
|
-
const payload = JSON.parse(
|
|
519
|
+
const payload = JSON.parse(base64Decode(accessToken.split(".")[1]));
|
|
425
520
|
const session = {
|
|
426
521
|
access_token: accessToken,
|
|
427
522
|
refresh_token: refreshToken ?? void 0,
|
|
@@ -729,27 +824,31 @@ var StorageBucket = class {
|
|
|
729
824
|
};
|
|
730
825
|
}
|
|
731
826
|
}
|
|
732
|
-
// Helper: Convert Blob to base64
|
|
733
|
-
blobToBase64(blob) {
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
827
|
+
// Helper: Convert Blob to base64 (cross-platform)
|
|
828
|
+
async blobToBase64(blob) {
|
|
829
|
+
if (typeof FileReader !== "undefined") {
|
|
830
|
+
return new Promise((resolve, reject) => {
|
|
831
|
+
const reader = new FileReader();
|
|
832
|
+
reader.onloadend = () => {
|
|
833
|
+
const result = reader.result;
|
|
834
|
+
const base64 = result.includes(",") ? result.split(",")[1] : result;
|
|
835
|
+
resolve(base64);
|
|
836
|
+
};
|
|
837
|
+
reader.onerror = reject;
|
|
838
|
+
reader.readAsDataURL(blob);
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
const buffer = await blob.arrayBuffer();
|
|
842
|
+
return this.arrayBufferToBase64(buffer);
|
|
744
843
|
}
|
|
745
|
-
// Helper: Convert ArrayBuffer to base64
|
|
844
|
+
// Helper: Convert ArrayBuffer to base64 (cross-platform)
|
|
746
845
|
arrayBufferToBase64(buffer) {
|
|
747
|
-
let binary = "";
|
|
748
846
|
const bytes = new Uint8Array(buffer);
|
|
847
|
+
let binary = "";
|
|
749
848
|
for (let i = 0; i < bytes.length; i++) {
|
|
750
849
|
binary += String.fromCharCode(bytes[i]);
|
|
751
850
|
}
|
|
752
|
-
return
|
|
851
|
+
return base64Encode(binary);
|
|
753
852
|
}
|
|
754
853
|
// Helper: Detect MIME type from file extension
|
|
755
854
|
detectMimeType(path) {
|
|
@@ -1110,6 +1209,188 @@ var KrisspyRealtime = class {
|
|
|
1110
1209
|
}
|
|
1111
1210
|
};
|
|
1112
1211
|
|
|
1212
|
+
// src/analytics.ts
|
|
1213
|
+
var KrisspyAnalytics = class {
|
|
1214
|
+
constructor(http, backendId) {
|
|
1215
|
+
this.queue = [];
|
|
1216
|
+
this.sessionId = null;
|
|
1217
|
+
this.userId = null;
|
|
1218
|
+
this.userTraits = {};
|
|
1219
|
+
this.initialized = false;
|
|
1220
|
+
this.flushTimer = null;
|
|
1221
|
+
this.options = {};
|
|
1222
|
+
this.originalPushState = null;
|
|
1223
|
+
this.popstateHandler = null;
|
|
1224
|
+
this.unloadHandler = null;
|
|
1225
|
+
this.http = http;
|
|
1226
|
+
this.backendId = backendId;
|
|
1227
|
+
}
|
|
1228
|
+
/**
|
|
1229
|
+
* Initialize analytics tracking
|
|
1230
|
+
* No-ops in non-browser environments (React Native, Node.js)
|
|
1231
|
+
*/
|
|
1232
|
+
init(options) {
|
|
1233
|
+
if (this.initialized) return;
|
|
1234
|
+
if (!isBrowser()) return;
|
|
1235
|
+
this.options = {
|
|
1236
|
+
autoTrackPageViews: true,
|
|
1237
|
+
autoTrackNavigation: true,
|
|
1238
|
+
flushInterval: 5e3,
|
|
1239
|
+
debug: false,
|
|
1240
|
+
...options
|
|
1241
|
+
};
|
|
1242
|
+
this.sessionId = this.getOrCreateSessionId();
|
|
1243
|
+
this.initialized = true;
|
|
1244
|
+
if (this.options.autoTrackPageViews) {
|
|
1245
|
+
this.track("session_start", {});
|
|
1246
|
+
this.track("page_view", { url: window.location.href, referrer: document.referrer });
|
|
1247
|
+
}
|
|
1248
|
+
if (this.options.autoTrackNavigation) {
|
|
1249
|
+
this.setupNavigationTracking();
|
|
1250
|
+
}
|
|
1251
|
+
this.unloadHandler = () => {
|
|
1252
|
+
this.track("session_end", {});
|
|
1253
|
+
this.flushSync();
|
|
1254
|
+
};
|
|
1255
|
+
window.addEventListener("beforeunload", this.unloadHandler);
|
|
1256
|
+
this.flushTimer = setInterval(() => this.flush(), this.options.flushInterval);
|
|
1257
|
+
if (this.options.debug) {
|
|
1258
|
+
console.log("[Krisspy Analytics] Initialized", { backendId: this.backendId, sessionId: this.sessionId });
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
/**
|
|
1262
|
+
* Track a custom event
|
|
1263
|
+
*/
|
|
1264
|
+
track(eventName, properties) {
|
|
1265
|
+
if (!this.initialized) return;
|
|
1266
|
+
const event = {
|
|
1267
|
+
eventName,
|
|
1268
|
+
sessionId: this.sessionId,
|
|
1269
|
+
userId: this.userId || void 0,
|
|
1270
|
+
properties: {
|
|
1271
|
+
...properties,
|
|
1272
|
+
...this.userId ? { _user_traits: this.userTraits } : {}
|
|
1273
|
+
},
|
|
1274
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1275
|
+
};
|
|
1276
|
+
this.queue.push(event);
|
|
1277
|
+
if (this.options.debug) {
|
|
1278
|
+
console.log("[Krisspy Analytics] Tracked:", eventName, properties);
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
/**
|
|
1282
|
+
* Identify a user (attach userId + traits to all future events)
|
|
1283
|
+
*/
|
|
1284
|
+
identify(userId, traits) {
|
|
1285
|
+
this.userId = userId;
|
|
1286
|
+
this.userTraits = traits || {};
|
|
1287
|
+
if (this.initialized) {
|
|
1288
|
+
this.track("identify", { userId, ...traits });
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
/**
|
|
1292
|
+
* Flush event queue to the server (async)
|
|
1293
|
+
*/
|
|
1294
|
+
async flush() {
|
|
1295
|
+
if (!this.queue.length) return;
|
|
1296
|
+
const events = this.queue.splice(0, 50);
|
|
1297
|
+
const payload = { appId: this.backendId, events };
|
|
1298
|
+
try {
|
|
1299
|
+
await this.http.post("/api/v1/analytics/events", payload);
|
|
1300
|
+
if (this.options.debug) {
|
|
1301
|
+
console.log(`[Krisspy Analytics] Flushed ${events.length} events`);
|
|
1302
|
+
}
|
|
1303
|
+
} catch {
|
|
1304
|
+
this.queue.unshift(...events);
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
/**
|
|
1308
|
+
* Synchronous flush using sendBeacon (for page unload)
|
|
1309
|
+
*/
|
|
1310
|
+
flushSync() {
|
|
1311
|
+
if (!this.queue.length) return;
|
|
1312
|
+
if (!isBrowser()) return;
|
|
1313
|
+
const events = this.queue.splice(0, 50);
|
|
1314
|
+
const payload = JSON.stringify({ appId: this.backendId, events });
|
|
1315
|
+
try {
|
|
1316
|
+
if (typeof navigator !== "undefined" && navigator.sendBeacon) {
|
|
1317
|
+
const blob = new Blob([payload], { type: "application/json" });
|
|
1318
|
+
navigator.sendBeacon(this.getIngestUrl(), blob);
|
|
1319
|
+
} else {
|
|
1320
|
+
fetch(this.getIngestUrl(), {
|
|
1321
|
+
method: "POST",
|
|
1322
|
+
headers: { "Content-Type": "application/json" },
|
|
1323
|
+
body: payload,
|
|
1324
|
+
keepalive: true
|
|
1325
|
+
}).catch(() => {
|
|
1326
|
+
});
|
|
1327
|
+
}
|
|
1328
|
+
} catch {
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
/**
|
|
1332
|
+
* Stop tracking and cleanup all listeners/timers
|
|
1333
|
+
*/
|
|
1334
|
+
destroy() {
|
|
1335
|
+
if (!this.initialized) return;
|
|
1336
|
+
this.flushSync();
|
|
1337
|
+
if (this.flushTimer) {
|
|
1338
|
+
clearInterval(this.flushTimer);
|
|
1339
|
+
this.flushTimer = null;
|
|
1340
|
+
}
|
|
1341
|
+
if (isBrowser()) {
|
|
1342
|
+
if (this.originalPushState) {
|
|
1343
|
+
history.pushState = this.originalPushState;
|
|
1344
|
+
this.originalPushState = null;
|
|
1345
|
+
}
|
|
1346
|
+
if (this.popstateHandler) {
|
|
1347
|
+
window.removeEventListener("popstate", this.popstateHandler);
|
|
1348
|
+
this.popstateHandler = null;
|
|
1349
|
+
}
|
|
1350
|
+
if (this.unloadHandler) {
|
|
1351
|
+
window.removeEventListener("beforeunload", this.unloadHandler);
|
|
1352
|
+
this.unloadHandler = null;
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
this.initialized = false;
|
|
1356
|
+
this.queue = [];
|
|
1357
|
+
if (this.options.debug) {
|
|
1358
|
+
console.log("[Krisspy Analytics] Destroyed");
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
// -- Private helpers --------------------------------------------------------
|
|
1362
|
+
getOrCreateSessionId() {
|
|
1363
|
+
const key = "_ka_sid";
|
|
1364
|
+
if (isBrowser()) {
|
|
1365
|
+
try {
|
|
1366
|
+
const existing = window.localStorage.getItem(key);
|
|
1367
|
+
if (existing) return existing;
|
|
1368
|
+
const id = Math.random().toString(36).substring(2, 15) + Date.now().toString(36);
|
|
1369
|
+
window.localStorage.setItem(key, id);
|
|
1370
|
+
return id;
|
|
1371
|
+
} catch {
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
return Math.random().toString(36).substring(2, 15) + Date.now().toString(36);
|
|
1375
|
+
}
|
|
1376
|
+
getIngestUrl() {
|
|
1377
|
+
return `${this.http.baseUrl}/api/v1/analytics/events`;
|
|
1378
|
+
}
|
|
1379
|
+
setupNavigationTracking() {
|
|
1380
|
+
if (!isBrowser()) return;
|
|
1381
|
+
this.originalPushState = history.pushState.bind(history);
|
|
1382
|
+
const self = this;
|
|
1383
|
+
history.pushState = function(...args) {
|
|
1384
|
+
self.originalPushState(...args);
|
|
1385
|
+
self.track("page_view", { url: window.location.href });
|
|
1386
|
+
};
|
|
1387
|
+
this.popstateHandler = () => {
|
|
1388
|
+
this.track("page_view", { url: window.location.href });
|
|
1389
|
+
};
|
|
1390
|
+
window.addEventListener("popstate", this.popstateHandler);
|
|
1391
|
+
}
|
|
1392
|
+
};
|
|
1393
|
+
|
|
1113
1394
|
// src/query-builder.ts
|
|
1114
1395
|
var QueryBuilder = class {
|
|
1115
1396
|
constructor(http, backendId, tableName, useRLS = true) {
|
|
@@ -1401,9 +1682,10 @@ var KrisspyClient = class {
|
|
|
1401
1682
|
},
|
|
1402
1683
|
debug: options.debug
|
|
1403
1684
|
});
|
|
1404
|
-
this._auth = new KrisspyAuth(this.http, this.backendId);
|
|
1685
|
+
this._auth = new KrisspyAuth(this.http, this.backendId, options.storage);
|
|
1405
1686
|
this._storage = new KrisspyStorage(this.http, this.backendId);
|
|
1406
1687
|
this._realtime = new KrisspyRealtime(this.baseUrl, this.backendId, this.debug);
|
|
1688
|
+
this._analytics = new KrisspyAnalytics(this.http, this.backendId);
|
|
1407
1689
|
this._auth.onAuthStateChange((event) => {
|
|
1408
1690
|
if (event === "SIGNED_IN") {
|
|
1409
1691
|
const session = this._auth.session();
|
|
@@ -1464,6 +1746,25 @@ var KrisspyClient = class {
|
|
|
1464
1746
|
get storage() {
|
|
1465
1747
|
return this._storage;
|
|
1466
1748
|
}
|
|
1749
|
+
/**
|
|
1750
|
+
* Analytics module for event tracking
|
|
1751
|
+
*
|
|
1752
|
+
* @example
|
|
1753
|
+
* // Initialize tracking
|
|
1754
|
+
* krisspy.analytics.init()
|
|
1755
|
+
*
|
|
1756
|
+
* // Track custom events
|
|
1757
|
+
* krisspy.analytics.track('button_click', { label: 'signup' })
|
|
1758
|
+
*
|
|
1759
|
+
* // Identify a user
|
|
1760
|
+
* krisspy.analytics.identify('user_123', { plan: 'pro' })
|
|
1761
|
+
*
|
|
1762
|
+
* // Cleanup
|
|
1763
|
+
* krisspy.analytics.destroy()
|
|
1764
|
+
*/
|
|
1765
|
+
get analytics() {
|
|
1766
|
+
return this._analytics;
|
|
1767
|
+
}
|
|
1467
1768
|
/**
|
|
1468
1769
|
* Create a realtime channel for subscribing to database changes
|
|
1469
1770
|
*
|
|
@@ -1631,6 +1932,7 @@ function createClient(options) {
|
|
|
1631
1932
|
}
|
|
1632
1933
|
export {
|
|
1633
1934
|
HttpClient,
|
|
1935
|
+
KrisspyAnalytics,
|
|
1634
1936
|
KrisspyAuth,
|
|
1635
1937
|
KrisspyClient,
|
|
1636
1938
|
KrisspyRealtime,
|
|
@@ -1638,5 +1940,7 @@ export {
|
|
|
1638
1940
|
QueryBuilder,
|
|
1639
1941
|
RealtimeChannel,
|
|
1640
1942
|
StorageBucket,
|
|
1641
|
-
createClient
|
|
1943
|
+
createClient,
|
|
1944
|
+
isBrowser,
|
|
1945
|
+
isReactNative
|
|
1642
1946
|
};
|