@oxyhq/services 5.5.8 → 5.5.9

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.
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Tests for URL resolution in separated authFetch implementation
3
+ */
4
+
5
+ describe('URL Resolution Separation', () => {
6
+ describe('resolveURL function behavior', () => {
7
+ // Mock implementation of resolveURL logic used in useAuthFetch
8
+ function resolveURL(input: RequestInfo | URL, baseURL: string): string {
9
+ if (!baseURL) {
10
+ throw new Error('Base URL not configured. Please provide a baseURL in OxyServices configuration.');
11
+ }
12
+
13
+ const url = input.toString();
14
+
15
+ // If it's already a full URL (http/https), return as is
16
+ if (url.startsWith('http://') || url.startsWith('https://')) {
17
+ return url;
18
+ }
19
+
20
+ // Normalize base URL (remove trailing slash)
21
+ const normalizedBaseURL = baseURL.replace(/\/$/, '');
22
+
23
+ // If URL starts with /, it's relative to base URL
24
+ if (url.startsWith('/')) {
25
+ return `${normalizedBaseURL}${url}`;
26
+ }
27
+
28
+ // Otherwise, append to base URL with /
29
+ return `${normalizedBaseURL}/${url}`;
30
+ }
31
+
32
+ test('should resolve relative URLs with app base URL', () => {
33
+ const appBaseURL = 'https://myapp.example.com';
34
+
35
+ expect(resolveURL('/api/users', appBaseURL)).toBe('https://myapp.example.com/api/users');
36
+ expect(resolveURL('api/users', appBaseURL)).toBe('https://myapp.example.com/api/users');
37
+ expect(resolveURL('/api/posts', appBaseURL)).toBe('https://myapp.example.com/api/posts');
38
+ });
39
+
40
+ test('should preserve absolute URLs unchanged', () => {
41
+ const appBaseURL = 'https://myapp.example.com';
42
+ const absoluteURL = 'https://external-api.example.com/data';
43
+
44
+ expect(resolveURL(absoluteURL, appBaseURL)).toBe(absoluteURL);
45
+ });
46
+
47
+ test('should handle different app base URLs independently', () => {
48
+ const appBaseURL1 = 'https://app1.example.com';
49
+ const appBaseURL2 = 'https://app2.example.com';
50
+ const oxyBaseURL = 'https://api.oxy.so';
51
+
52
+ // Same endpoint, different base URLs
53
+ expect(resolveURL('/api/users', appBaseURL1)).toBe('https://app1.example.com/api/users');
54
+ expect(resolveURL('/api/users', appBaseURL2)).toBe('https://app2.example.com/api/users');
55
+ expect(resolveURL('/api/users', oxyBaseURL)).toBe('https://api.oxy.so/api/users');
56
+ });
57
+
58
+ test('should normalize base URLs correctly', () => {
59
+ const baseURLWithSlash = 'https://myapp.example.com/';
60
+ const baseURLWithoutSlash = 'https://myapp.example.com';
61
+
62
+ expect(resolveURL('/api/users', baseURLWithSlash)).toBe('https://myapp.example.com/api/users');
63
+ expect(resolveURL('/api/users', baseURLWithoutSlash)).toBe('https://myapp.example.com/api/users');
64
+ });
65
+
66
+ test('should throw error for missing base URL', () => {
67
+ expect(() => resolveURL('/api/users', '')).toThrow('Base URL not configured');
68
+ });
69
+ });
70
+
71
+ describe('Separation scenarios', () => {
72
+ test('should demonstrate independent URL management', () => {
73
+ // Simulate the separation: internal vs public URLs
74
+ const internalOxyURL = 'https://api.oxy.so';
75
+ const publicAppURL = 'https://mention.earth/api';
76
+
77
+ // Internal module calls (would use OxyServices base URL)
78
+ const internalCall = resolveURL('/auth/validate', internalOxyURL);
79
+ expect(internalCall).toBe('https://api.oxy.so/auth/validate');
80
+
81
+ // Public authFetch calls (would use app base URL)
82
+ const publicCall = resolveURL('/api/mentions', publicAppURL);
83
+ expect(publicCall).toBe('https://mention.earth/api/api/mentions');
84
+ });
85
+
86
+ test('should support dynamic app URL changes', () => {
87
+ // Simulate changing app URL without affecting internal URL
88
+ let appBaseURL = 'https://staging.myapp.com';
89
+ const oxyBaseURL = 'https://api.oxy.so';
90
+
91
+ // Initial state
92
+ expect(resolveURL('/api/data', appBaseURL)).toBe('https://staging.myapp.com/api/data');
93
+ expect(resolveURL('/auth/me', oxyBaseURL)).toBe('https://api.oxy.so/auth/me');
94
+
95
+ // App changes its URL
96
+ appBaseURL = 'https://production.myapp.com';
97
+
98
+ // App URLs updated
99
+ expect(resolveURL('/api/data', appBaseURL)).toBe('https://production.myapp.com/api/data');
100
+ // Internal URLs unchanged
101
+ expect(resolveURL('/auth/me', oxyBaseURL)).toBe('https://api.oxy.so/auth/me');
102
+ });
103
+ });
104
+
105
+ // Helper function to mimic resolveURL from the actual implementation
106
+ function resolveURL(input: RequestInfo | URL, baseURL: string): string {
107
+ if (!baseURL) {
108
+ throw new Error('Base URL not configured. Please provide a baseURL in OxyServices configuration.');
109
+ }
110
+
111
+ const url = input.toString();
112
+
113
+ // If it's already a full URL (http/https), return as is
114
+ if (url.startsWith('http://') || url.startsWith('https://')) {
115
+ return url;
116
+ }
117
+
118
+ // Normalize base URL (remove trailing slash)
119
+ const normalizedBaseURL = baseURL.replace(/\/$/, '');
120
+
121
+ // If URL starts with /, it's relative to base URL
122
+ if (url.startsWith('/')) {
123
+ return `${normalizedBaseURL}${url}`;
124
+ }
125
+
126
+ // Otherwise, append to base URL with /
127
+ return `${normalizedBaseURL}/${url}`;
128
+ }
129
+ });
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Tests for authFetch separation between public and internal usage
3
+ */
4
+
5
+ import { OxyServices } from '../../../core';
6
+
7
+ describe('AuthFetch Separation', () => {
8
+ let oxyServices: OxyServices;
9
+
10
+ beforeEach(() => {
11
+ oxyServices = new OxyServices({
12
+ baseURL: 'https://api.oxy.so'
13
+ });
14
+ });
15
+
16
+ describe('OxyServices internal base URL isolation', () => {
17
+ test('should preserve original base URL for internal calls', () => {
18
+ const originalBaseURL = 'https://api.oxy.so';
19
+
20
+ // Verify initial state
21
+ expect(oxyServices.getBaseURL()).toBe(originalBaseURL);
22
+
23
+ // Simulate app changing its own base URL (this should not affect OxyServices)
24
+ // Note: In the new implementation, setApiUrl only affects app authFetch, not OxyServices
25
+ const appBaseURL = 'https://app-api.example.com';
26
+
27
+ // OxyServices base URL should remain unchanged
28
+ expect(oxyServices.getBaseURL()).toBe(originalBaseURL);
29
+ });
30
+
31
+ test('should handle OxyServices base URL changes separately', () => {
32
+ const originalBaseURL = 'https://api.oxy.so';
33
+ const newOxyBaseURL = 'https://new-oxy-api.example.com';
34
+
35
+ expect(oxyServices.getBaseURL()).toBe(originalBaseURL);
36
+
37
+ // This should still work for internal module updates
38
+ oxyServices.setBaseURL(newOxyBaseURL);
39
+ expect(oxyServices.getBaseURL()).toBe(newOxyBaseURL);
40
+ });
41
+
42
+ test('should maintain separation - internal vs public URLs', () => {
43
+ const oxyBaseURL = 'https://api.oxy.so';
44
+ const appBaseURL = 'https://myapp.example.com';
45
+
46
+ // OxyServices keeps its own URL for internal calls
47
+ expect(oxyServices.getBaseURL()).toBe(oxyBaseURL);
48
+
49
+ // App can have its own URL (this would be managed in context)
50
+ // The separation ensures these don't interfere with each other
51
+ expect(oxyBaseURL).not.toBe(appBaseURL);
52
+ });
53
+ });
54
+
55
+ describe('URL validation and error handling', () => {
56
+ test('should validate URLs properly', () => {
57
+ expect(() => oxyServices.setBaseURL('')).toThrow('Base URL cannot be empty');
58
+ expect(() => oxyServices.setBaseURL('not-a-url')).not.toThrow(); // axios will handle invalid URLs
59
+ });
60
+
61
+ test('should handle URL normalization', () => {
62
+ oxyServices.setBaseURL('https://api.example.com/');
63
+ expect(oxyServices.getBaseURL()).toBe('https://api.example.com/');
64
+
65
+ oxyServices.setBaseURL('https://api.example.com');
66
+ expect(oxyServices.getBaseURL()).toBe('https://api.example.com');
67
+ });
68
+ });
69
+ });
@@ -37,6 +37,7 @@ export interface OxyContextState {
37
37
 
38
38
  // API configuration
39
39
  setApiUrl: (url: string) => void;
40
+ getAppBaseURL: () => string;
40
41
 
41
42
  // Methods to directly control the bottom sheet
42
43
  showBottomSheet?: (screenOrConfig?: string | { screen: string; props?: Record<string, any> }) => void;
@@ -129,6 +130,7 @@ export const OxyContextProvider: React.FC<OxyContextProviderProps> = ({
129
130
  const [isLoading, setIsLoading] = useState(true);
130
131
  const [error, setError] = useState<string | null>(null);
131
132
  const [storage, setStorage] = useState<StorageInterface | null>(null);
133
+ const [appBaseURL, setAppBaseURL] = useState<string>(oxyServices.getBaseURL());
132
134
 
133
135
  // Storage keys (memoized to prevent infinite loops)
134
136
  const keys = useMemo(() => getSecureStorageKeys(storageKeyPrefix), [storageKeyPrefix]);
@@ -646,12 +648,23 @@ export const OxyContextProvider: React.FC<OxyContextProviderProps> = ({
646
648
  // API URL configuration
647
649
  const setApiUrl = useCallback((url: string) => {
648
650
  try {
649
- oxyServices.setBaseURL(url);
651
+ // Validate URL
652
+ if (!url) {
653
+ throw new Error('Base URL cannot be empty');
654
+ }
655
+ // Only update the app-specific base URL, not the OxyServices base URL
656
+ // This ensures internal module calls to Oxy API remain unaffected
657
+ setAppBaseURL(url);
650
658
  } catch (error) {
651
659
  console.error('Failed to update API URL:', error);
652
660
  setError(`Failed to update API URL: ${error instanceof Error ? error.message : 'Unknown error'}`);
653
661
  }
654
- }, [oxyServices]);
662
+ }, []);
663
+
664
+ // Get current app base URL
665
+ const getAppBaseURL = useCallback(() => {
666
+ return appBaseURL;
667
+ }, [appBaseURL]);
655
668
 
656
669
  // Compute comprehensive authentication status
657
670
  // This is the single source of truth for authentication across the entire app
@@ -684,6 +697,7 @@ export const OxyContextProvider: React.FC<OxyContextProviderProps> = ({
684
697
  updateDeviceName,
685
698
  oxyServices,
686
699
  setApiUrl,
700
+ getAppBaseURL,
687
701
  bottomSheetRef,
688
702
  showBottomSheet,
689
703
  hideBottomSheet,
@@ -43,7 +43,7 @@ export interface AuthFetchAPI {
43
43
  * Uses the existing OxyServices instance from useOxy context
44
44
  */
45
45
  export function useAuthFetch(): AuthFetchAPI {
46
- const { oxyServices, isAuthenticated, user, login, logout, signUp, activeSessionId, setApiUrl } = useOxy();
46
+ const { oxyServices, isAuthenticated, user, login, logout, signUp, activeSessionId, setApiUrl, getAppBaseURL } = useOxy();
47
47
 
48
48
  // Validate that we have the required dependencies
49
49
  if (!oxyServices) {
@@ -56,7 +56,7 @@ export function useAuthFetch(): AuthFetchAPI {
56
56
  throw new Error('OxyServices not initialized. Make sure to wrap your app in OxyProvider.');
57
57
  }
58
58
 
59
- const url = resolveURL(input, oxyServices.getBaseURL());
59
+ const url = resolveURL(input, getAppBaseURL());
60
60
  const options = await addAuthHeaders(init, oxyServices, activeSessionId || undefined, isAuthenticated);
61
61
 
62
62
  try {
@@ -88,7 +88,7 @@ export function useAuthFetch(): AuthFetchAPI {
88
88
  }
89
89
  throw new Error('Request failed');
90
90
  }
91
- }, [oxyServices, activeSessionId, isAuthenticated]);
91
+ }, [oxyServices, activeSessionId, isAuthenticated, getAppBaseURL]);
92
92
 
93
93
  // JSON convenience methods
94
94
  const get = useCallback(async (endpoint: string, options?: AuthFetchOptions) => {