@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.
- package/lib/commonjs/ui/context/OxyContext.js +15 -2
- package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
- package/lib/commonjs/ui/hooks/useAuthFetch.js +4 -3
- package/lib/commonjs/ui/hooks/useAuthFetch.js.map +1 -1
- package/lib/module/ui/context/OxyContext.js +15 -2
- package/lib/module/ui/context/OxyContext.js.map +1 -1
- package/lib/module/ui/hooks/useAuthFetch.js +4 -3
- package/lib/module/ui/hooks/useAuthFetch.js.map +1 -1
- package/lib/typescript/ui/context/OxyContext.d.ts +1 -0
- package/lib/typescript/ui/context/OxyContext.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/ui/hooks/authfetch-integration.test.ts +197 -0
- package/src/__tests__/ui/hooks/backward-compatibility.test.ts +159 -0
- package/src/__tests__/ui/hooks/real-world-scenarios.test.ts +224 -0
- package/src/__tests__/ui/hooks/url-resolution.test.ts +129 -0
- package/src/__tests__/ui/hooks/useAuthFetch-separation.test.ts +69 -0
- package/src/ui/context/OxyContext.tsx +16 -2
- package/src/ui/hooks/useAuthFetch.ts +3 -3
|
@@ -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
|
-
|
|
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
|
-
}, [
|
|
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,
|
|
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) => {
|