@youversion/platform-core 0.6.0 → 0.7.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/.turbo/turbo-build.log +8 -8
- package/CHANGELOG.md +33 -0
- package/dist/index.cjs +473 -346
- package/dist/index.d.cts +106 -134
- package/dist/index.d.ts +106 -134
- package/dist/index.js +473 -343
- package/package.json +4 -3
- package/src/SignInWithYouVersionPKCE.ts +122 -0
- package/src/SignInWithYouVersionResult.ts +40 -39
- package/src/URLBuilder.ts +0 -21
- package/src/Users.ts +375 -94
- package/src/YouVersionPlatformConfiguration.ts +69 -25
- package/src/YouVersionUserInfo.ts +6 -6
- package/src/__tests__/SignInWithYouVersionPKCE.test.ts +418 -0
- package/src/__tests__/SignInWithYouVersionResult.test.ts +28 -0
- package/src/__tests__/StorageStrategy.test.ts +0 -72
- package/src/__tests__/URLBuilder.test.ts +0 -100
- package/src/__tests__/Users.test.ts +737 -0
- package/src/__tests__/YouVersionPlatformConfiguration.test.ts +192 -30
- package/src/__tests__/YouVersionUserInfo.test.ts +347 -0
- package/src/__tests__/highlights.test.ts +12 -12
- package/src/__tests__/mocks/browser.ts +90 -0
- package/src/__tests__/mocks/configuration.ts +53 -0
- package/src/__tests__/mocks/jwt.ts +93 -0
- package/src/__tests__/mocks/tokens.ts +69 -0
- package/src/index.ts +0 -3
- package/src/types/auth.ts +1 -0
- package/tsconfig.build.json +1 -1
- package/tsconfig.json +1 -1
- package/src/AuthenticationStrategy.ts +0 -78
- package/src/WebAuthenticationStrategy.ts +0 -127
- package/src/__tests__/authentication.test.ts +0 -185
- package/src/authentication.ts +0 -27
|
@@ -3,7 +3,7 @@ import { ApiClient } from '../client';
|
|
|
3
3
|
import { HighlightsClient } from '../highlights';
|
|
4
4
|
import { YouVersionPlatformConfiguration } from '../YouVersionPlatformConfiguration';
|
|
5
5
|
|
|
6
|
-
describe('HighlightsClient', () => {
|
|
6
|
+
describe.skip('HighlightsClient', () => {
|
|
7
7
|
let apiClient: ApiClient;
|
|
8
8
|
let highlightsClient: HighlightsClient;
|
|
9
9
|
|
|
@@ -15,12 +15,12 @@ describe('HighlightsClient', () => {
|
|
|
15
15
|
});
|
|
16
16
|
highlightsClient = new HighlightsClient(apiClient);
|
|
17
17
|
// Set a default token for tests that don't explicitly pass one
|
|
18
|
-
YouVersionPlatformConfiguration.
|
|
18
|
+
YouVersionPlatformConfiguration.saveAuthData('test-token', null, null, null);
|
|
19
19
|
});
|
|
20
20
|
|
|
21
21
|
afterEach(() => {
|
|
22
22
|
// Clean up token after each test
|
|
23
|
-
YouVersionPlatformConfiguration.
|
|
23
|
+
YouVersionPlatformConfiguration.saveAuthData(null, null, null, null);
|
|
24
24
|
vi.clearAllMocks(); // Reset all mocked calls between tests
|
|
25
25
|
});
|
|
26
26
|
|
|
@@ -96,7 +96,7 @@ describe('HighlightsClient', () => {
|
|
|
96
96
|
});
|
|
97
97
|
|
|
98
98
|
it('should include lat parameter when token auto-retrieved from config', async () => {
|
|
99
|
-
YouVersionPlatformConfiguration.
|
|
99
|
+
YouVersionPlatformConfiguration.saveAuthData('config-token', null, null, null);
|
|
100
100
|
const fetchSpy = vi.spyOn(global, 'fetch');
|
|
101
101
|
|
|
102
102
|
const highlights = await highlightsClient.getHighlights({ version_id: 1 });
|
|
@@ -112,7 +112,7 @@ describe('HighlightsClient', () => {
|
|
|
112
112
|
});
|
|
113
113
|
|
|
114
114
|
it('should throw an error when no token is available', async () => {
|
|
115
|
-
YouVersionPlatformConfiguration.
|
|
115
|
+
YouVersionPlatformConfiguration.saveAuthData(null, null, null, null);
|
|
116
116
|
|
|
117
117
|
await expect(highlightsClient.getHighlights({ version_id: 1 })).rejects.toThrow(
|
|
118
118
|
'Authentication required. Please provide a token or sign in before accessing highlights.',
|
|
@@ -120,7 +120,7 @@ describe('HighlightsClient', () => {
|
|
|
120
120
|
});
|
|
121
121
|
|
|
122
122
|
it('should use explicit token over config token', async () => {
|
|
123
|
-
YouVersionPlatformConfiguration.
|
|
123
|
+
YouVersionPlatformConfiguration.saveAuthData('config-token', null, null, null);
|
|
124
124
|
const fetchSpy = vi.spyOn(global, 'fetch');
|
|
125
125
|
const highlights = await highlightsClient.getHighlights({ version_id: 1 }, 'explicit-token');
|
|
126
126
|
|
|
@@ -269,7 +269,7 @@ describe('HighlightsClient', () => {
|
|
|
269
269
|
});
|
|
270
270
|
|
|
271
271
|
it('should include lat parameter when token auto-retrieved from config', async () => {
|
|
272
|
-
YouVersionPlatformConfiguration.
|
|
272
|
+
YouVersionPlatformConfiguration.saveAuthData('config-token', null, null, null);
|
|
273
273
|
const fetchSpy = vi.spyOn(global, 'fetch');
|
|
274
274
|
const highlight = await highlightsClient.createHighlight({
|
|
275
275
|
version_id: 111,
|
|
@@ -291,7 +291,7 @@ describe('HighlightsClient', () => {
|
|
|
291
291
|
});
|
|
292
292
|
|
|
293
293
|
it('should throw an error when no token is available', async () => {
|
|
294
|
-
YouVersionPlatformConfiguration.
|
|
294
|
+
YouVersionPlatformConfiguration.saveAuthData(null, null, null, null);
|
|
295
295
|
|
|
296
296
|
await expect(
|
|
297
297
|
highlightsClient.createHighlight({
|
|
@@ -305,7 +305,7 @@ describe('HighlightsClient', () => {
|
|
|
305
305
|
});
|
|
306
306
|
|
|
307
307
|
it('should use explicit token over config token', async () => {
|
|
308
|
-
YouVersionPlatformConfiguration.
|
|
308
|
+
YouVersionPlatformConfiguration.saveAuthData('config-token', null, null, null);
|
|
309
309
|
const fetchSpy = vi.spyOn(global, 'fetch');
|
|
310
310
|
const highlight = await highlightsClient.createHighlight(
|
|
311
311
|
{
|
|
@@ -432,7 +432,7 @@ describe('HighlightsClient', () => {
|
|
|
432
432
|
});
|
|
433
433
|
|
|
434
434
|
it('should include lat parameter when token auto-retrieved from config', async () => {
|
|
435
|
-
YouVersionPlatformConfiguration.
|
|
435
|
+
YouVersionPlatformConfiguration.saveAuthData('config-token', null, null, null);
|
|
436
436
|
let capturedStatus: number | undefined;
|
|
437
437
|
const originalFetch = global.fetch;
|
|
438
438
|
const fetchSpy = vi.spyOn(global, 'fetch').mockImplementation(async (...args) => {
|
|
@@ -453,7 +453,7 @@ describe('HighlightsClient', () => {
|
|
|
453
453
|
});
|
|
454
454
|
|
|
455
455
|
it('should throw an error when no token is available', async () => {
|
|
456
|
-
YouVersionPlatformConfiguration.
|
|
456
|
+
YouVersionPlatformConfiguration.saveAuthData(null, null, null, null);
|
|
457
457
|
|
|
458
458
|
await expect(highlightsClient.deleteHighlight('MAT.1.1')).rejects.toThrow(
|
|
459
459
|
'Authentication required. Please provide a token or sign in before accessing highlights.',
|
|
@@ -461,7 +461,7 @@ describe('HighlightsClient', () => {
|
|
|
461
461
|
});
|
|
462
462
|
|
|
463
463
|
it('should use explicit token over config token', async () => {
|
|
464
|
-
YouVersionPlatformConfiguration.
|
|
464
|
+
YouVersionPlatformConfiguration.saveAuthData('config-token', null, null, null);
|
|
465
465
|
let capturedStatus: number | undefined;
|
|
466
466
|
const originalFetch = global.fetch;
|
|
467
467
|
const fetchSpy = vi.spyOn(global, 'fetch').mockImplementation(async (...args) => {
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Creates a mock window.location object for testing
|
|
5
|
+
*/
|
|
6
|
+
export const createMockLocation = (): { href: string; search: string } => ({
|
|
7
|
+
href: '',
|
|
8
|
+
search: '',
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Creates a mock window.history object for testing
|
|
13
|
+
*/
|
|
14
|
+
export const createMockHistory = (): { replaceState: ReturnType<typeof vi.fn> } => ({
|
|
15
|
+
replaceState: vi.fn(),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Creates a mock window object for testing
|
|
20
|
+
*/
|
|
21
|
+
export const createMockWindow = (): {
|
|
22
|
+
location: ReturnType<typeof createMockLocation>;
|
|
23
|
+
history: ReturnType<typeof createMockHistory>;
|
|
24
|
+
} => ({
|
|
25
|
+
location: createMockLocation(),
|
|
26
|
+
history: createMockHistory(),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Creates a mock localStorage object for testing
|
|
31
|
+
*/
|
|
32
|
+
export const createMockLocalStorage = (): {
|
|
33
|
+
getItem: ReturnType<typeof vi.fn>;
|
|
34
|
+
setItem: ReturnType<typeof vi.fn>;
|
|
35
|
+
removeItem: ReturnType<typeof vi.fn>;
|
|
36
|
+
clear: ReturnType<typeof vi.fn>;
|
|
37
|
+
} => ({
|
|
38
|
+
getItem: vi.fn(),
|
|
39
|
+
setItem: vi.fn(),
|
|
40
|
+
removeItem: vi.fn(),
|
|
41
|
+
clear: vi.fn(),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Creates a mock crypto API for testing
|
|
46
|
+
*/
|
|
47
|
+
export const createMockCrypto = (): {
|
|
48
|
+
getRandomValues: ReturnType<typeof vi.fn>;
|
|
49
|
+
randomUUID: ReturnType<typeof vi.fn>;
|
|
50
|
+
subtle: { digest: ReturnType<typeof vi.fn> };
|
|
51
|
+
} => ({
|
|
52
|
+
getRandomValues: vi.fn(),
|
|
53
|
+
randomUUID: vi.fn(() => 'test-installation-id-123'),
|
|
54
|
+
subtle: {
|
|
55
|
+
digest: vi.fn(),
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Sets up all browser-related mocks for testing
|
|
61
|
+
* @returns Object containing references to all created mocks
|
|
62
|
+
*/
|
|
63
|
+
export const setupBrowserMocks = (): {
|
|
64
|
+
window: ReturnType<typeof createMockWindow>;
|
|
65
|
+
localStorage: ReturnType<typeof createMockLocalStorage>;
|
|
66
|
+
crypto: ReturnType<typeof createMockCrypto>;
|
|
67
|
+
btoa: ReturnType<typeof vi.fn>;
|
|
68
|
+
atob: ReturnType<typeof vi.fn>;
|
|
69
|
+
} => {
|
|
70
|
+
const window = createMockWindow();
|
|
71
|
+
const localStorage = createMockLocalStorage();
|
|
72
|
+
const crypto = createMockCrypto();
|
|
73
|
+
const btoa = vi.fn();
|
|
74
|
+
const atob = vi.fn();
|
|
75
|
+
|
|
76
|
+
vi.stubGlobal('window', window);
|
|
77
|
+
vi.stubGlobal('localStorage', localStorage);
|
|
78
|
+
vi.stubGlobal('crypto', crypto);
|
|
79
|
+
vi.stubGlobal('btoa', btoa);
|
|
80
|
+
vi.stubGlobal('atob', atob);
|
|
81
|
+
|
|
82
|
+
return { window, localStorage, crypto, btoa, atob };
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Cleans up all browser-related mocks
|
|
87
|
+
*/
|
|
88
|
+
export const cleanupBrowserMocks = (): void => {
|
|
89
|
+
vi.unstubAllGlobals();
|
|
90
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Creates a mock YouVersionPlatformConfiguration for testing
|
|
5
|
+
* Maintains internal state for access tokens and configuration
|
|
6
|
+
*/
|
|
7
|
+
export const createMockPlatformConfiguration = (): {
|
|
8
|
+
accessToken: string | null;
|
|
9
|
+
idToken: string | null;
|
|
10
|
+
refreshToken: string | null;
|
|
11
|
+
appKey: string;
|
|
12
|
+
apiHost: string;
|
|
13
|
+
installationId: string | null;
|
|
14
|
+
expiryDate: Date | null;
|
|
15
|
+
clearAuthTokens: ReturnType<typeof vi.fn>;
|
|
16
|
+
saveAuthData: ReturnType<typeof vi.fn>;
|
|
17
|
+
} => {
|
|
18
|
+
const config = {
|
|
19
|
+
accessToken: null as string | null,
|
|
20
|
+
idToken: null as string | null,
|
|
21
|
+
refreshToken: null as string | null,
|
|
22
|
+
appKey: '',
|
|
23
|
+
apiHost: 'test-api.example.com',
|
|
24
|
+
installationId: null as string | null,
|
|
25
|
+
expiryDate: null as Date | null,
|
|
26
|
+
clearAuthTokens: vi.fn(function (this: typeof config) {
|
|
27
|
+
this.accessToken = null;
|
|
28
|
+
this.idToken = null;
|
|
29
|
+
this.refreshToken = null;
|
|
30
|
+
this.expiryDate = null;
|
|
31
|
+
}),
|
|
32
|
+
saveAuthData: vi.fn(function (
|
|
33
|
+
this: typeof config,
|
|
34
|
+
accessToken: string | null,
|
|
35
|
+
refreshToken: string | null,
|
|
36
|
+
idToken: string | null,
|
|
37
|
+
expiryDate?: Date | null,
|
|
38
|
+
) {
|
|
39
|
+
this.accessToken = accessToken;
|
|
40
|
+
this.refreshToken = refreshToken;
|
|
41
|
+
this.idToken = idToken;
|
|
42
|
+
if (expiryDate !== undefined) {
|
|
43
|
+
this.expiryDate = expiryDate;
|
|
44
|
+
}
|
|
45
|
+
}),
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// Bind methods to maintain context
|
|
49
|
+
config.clearAuthTokens = config.clearAuthTokens.bind(config);
|
|
50
|
+
config.saveAuthData = config.saveAuthData.bind(config);
|
|
51
|
+
|
|
52
|
+
return config;
|
|
53
|
+
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Creates a mock JWT token for testing
|
|
5
|
+
* @param payload The JWT payload to encode
|
|
6
|
+
* @returns A mock JWT string with the format header.payload.signature
|
|
7
|
+
*/
|
|
8
|
+
export const createMockJWT = (payload: Record<string, unknown>): string => {
|
|
9
|
+
// Use a simple encoding for testing (not real base64)
|
|
10
|
+
const header = 'mockHeader';
|
|
11
|
+
const body = JSON.stringify(payload);
|
|
12
|
+
const signature = 'mock-signature';
|
|
13
|
+
return `${header}.${body}.${signature}`;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Creates a realistic-looking JWT token with proper base64url encoding
|
|
18
|
+
* @param payload The JWT payload
|
|
19
|
+
* @returns A properly formatted JWT token
|
|
20
|
+
*/
|
|
21
|
+
export const createRealisticJWT = (payload: Record<string, unknown>): string => {
|
|
22
|
+
const header = { alg: 'HS256', typ: 'JWT' };
|
|
23
|
+
|
|
24
|
+
// Simple base64url encoding for testing
|
|
25
|
+
const base64urlEncode = (str: string) => {
|
|
26
|
+
if (typeof btoa !== 'undefined') {
|
|
27
|
+
return btoa(str).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
|
|
28
|
+
}
|
|
29
|
+
// Fallback for test environment
|
|
30
|
+
return Buffer.from(str)
|
|
31
|
+
.toString('base64')
|
|
32
|
+
.replace(/=/g, '')
|
|
33
|
+
.replace(/\+/g, '-')
|
|
34
|
+
.replace(/\//g, '_');
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const headerEncoded = base64urlEncode(JSON.stringify(header));
|
|
38
|
+
const payloadEncoded = base64urlEncode(JSON.stringify(payload));
|
|
39
|
+
const signature = 'invalid-signature'; // Mock signature
|
|
40
|
+
|
|
41
|
+
return `${headerEncoded}.${payloadEncoded}.${signature}`;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Sets up JWT-related mocks for atob/btoa functions
|
|
46
|
+
* @param payload The expected JWT payload to decode
|
|
47
|
+
*/
|
|
48
|
+
export const setupJWTMocks = (
|
|
49
|
+
payload: Record<string, unknown> = {},
|
|
50
|
+
): { atob: ReturnType<typeof vi.fn>; btoa: ReturnType<typeof vi.fn> } => {
|
|
51
|
+
const atobMock = vi.fn((str: string) => {
|
|
52
|
+
// Handle base64 padding
|
|
53
|
+
let padded = str;
|
|
54
|
+
while (padded.length % 4 !== 0) {
|
|
55
|
+
padded += '=';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// For testing, just return the payload
|
|
59
|
+
if (str.includes('eyJ')) {
|
|
60
|
+
// Looks like a real base64 JWT part
|
|
61
|
+
return JSON.stringify(payload);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return str;
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const btoaMock = vi.fn((str: string) => {
|
|
68
|
+
// Simple base64 encoding for testing
|
|
69
|
+
return `base64_${str.length}`;
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
vi.stubGlobal('atob', atobMock);
|
|
73
|
+
vi.stubGlobal('btoa', btoaMock);
|
|
74
|
+
|
|
75
|
+
return { atob: atobMock, btoa: btoaMock };
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Creates a mock JWT with standard claims
|
|
80
|
+
* @param overrides Optional claim overrides
|
|
81
|
+
*/
|
|
82
|
+
export const createMockJWTWithClaims = (overrides: Record<string, unknown> = {}): string => {
|
|
83
|
+
const defaultClaims = {
|
|
84
|
+
sub: '1234567890',
|
|
85
|
+
name: 'John Doe',
|
|
86
|
+
email: 'john@example.com',
|
|
87
|
+
profile_picture: 'https://example.com/avatar.jpg',
|
|
88
|
+
iat: 1516239022,
|
|
89
|
+
exp: 1516242622,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
return createRealisticJWT({ ...defaultClaims, ...overrides });
|
|
93
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Creates a mock OAuth token response for testing
|
|
5
|
+
* @param overrides Optional properties to override the default values
|
|
6
|
+
*/
|
|
7
|
+
export const createMockTokenResponse = (
|
|
8
|
+
overrides: Record<string, unknown> = {},
|
|
9
|
+
): Record<string, unknown> => ({
|
|
10
|
+
access_token: 'access-token-123',
|
|
11
|
+
expires_in: 3600,
|
|
12
|
+
id_token:
|
|
13
|
+
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJlbWFpbCI6ImpvaG5AZXhhbXBsZS5jb20iLCJwcm9maWxlX3BpY3R1cmUiOiJodHRwczovL2V4YW1wbGUuY29tL2F2YXRhci5qcGcifQ.invalid-signature',
|
|
14
|
+
refresh_token: 'refresh-token-456',
|
|
15
|
+
scope: 'bibles highlights openid',
|
|
16
|
+
token_type: 'Bearer',
|
|
17
|
+
...overrides,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Creates a mock fetch response for testing
|
|
22
|
+
* @param data The response data
|
|
23
|
+
* @param options Optional properties to override the default response
|
|
24
|
+
*/
|
|
25
|
+
export const createMockFetchResponse = (
|
|
26
|
+
data: unknown,
|
|
27
|
+
options: Record<string, unknown> = {},
|
|
28
|
+
): Record<string, unknown> => ({
|
|
29
|
+
ok: true,
|
|
30
|
+
status: 200,
|
|
31
|
+
statusText: 'OK',
|
|
32
|
+
json: vi.fn().mockResolvedValue(data),
|
|
33
|
+
text: vi.fn().mockResolvedValue(JSON.stringify(data)),
|
|
34
|
+
clone: vi.fn(() => ({
|
|
35
|
+
text: vi.fn().mockResolvedValue(JSON.stringify(data)),
|
|
36
|
+
})),
|
|
37
|
+
...options,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Creates a mock error fetch response for testing
|
|
42
|
+
* @param status The HTTP status code
|
|
43
|
+
* @param statusText The HTTP status text
|
|
44
|
+
*/
|
|
45
|
+
export const createMockErrorFetchResponse = (
|
|
46
|
+
status: number,
|
|
47
|
+
statusText: string,
|
|
48
|
+
): Record<string, unknown> => ({
|
|
49
|
+
ok: false,
|
|
50
|
+
status,
|
|
51
|
+
statusText,
|
|
52
|
+
json: vi.fn().mockRejectedValue(new Error('Response not JSON')),
|
|
53
|
+
text: vi.fn().mockResolvedValue(''),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Creates a mock refresh token response for testing
|
|
58
|
+
* @param overrides Optional properties to override the default values
|
|
59
|
+
*/
|
|
60
|
+
export const createMockRefreshTokenResponse = (
|
|
61
|
+
overrides: Record<string, unknown> = {},
|
|
62
|
+
): Record<string, unknown> => ({
|
|
63
|
+
access_token: 'new-access-token',
|
|
64
|
+
expires_in: 3600,
|
|
65
|
+
refresh_token: 'new-refresh-token',
|
|
66
|
+
scope: 'bibles highlights openid',
|
|
67
|
+
token_type: 'Bearer',
|
|
68
|
+
...overrides,
|
|
69
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -6,9 +6,6 @@ export {
|
|
|
6
6
|
type GetHighlightsOptions,
|
|
7
7
|
type DeleteHighlightOptions,
|
|
8
8
|
} from './highlights';
|
|
9
|
-
export { AuthClient } from './authentication';
|
|
10
|
-
export * from './AuthenticationStrategy';
|
|
11
|
-
export { WebAuthenticationStrategy } from './WebAuthenticationStrategy';
|
|
12
9
|
export * from './StorageStrategy';
|
|
13
10
|
export * from './Users';
|
|
14
11
|
export * from './YouVersionUserInfo';
|
package/src/types/auth.ts
CHANGED
package/tsconfig.build.json
CHANGED
package/tsconfig.json
CHANGED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Platform-agnostic authentication strategy interface
|
|
3
|
-
*
|
|
4
|
-
* Implementations should handle platform-specific authentication flows
|
|
5
|
-
* and return the callback URL containing the authentication result.
|
|
6
|
-
*/
|
|
7
|
-
export interface AuthenticationStrategy {
|
|
8
|
-
/**
|
|
9
|
-
* Opens the authentication flow and returns the callback URL
|
|
10
|
-
*
|
|
11
|
-
* @param authUrl - The YouVersion authorization URL to open
|
|
12
|
-
* @returns Promise that resolves to the callback URL with auth result
|
|
13
|
-
* @throws Error if authentication fails or is cancelled
|
|
14
|
-
*/
|
|
15
|
-
authenticate(authUrl: URL): Promise<URL>;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Registry for platform-specific authentication strategies
|
|
20
|
-
*
|
|
21
|
-
* This singleton registry manages the current authentication strategy
|
|
22
|
-
* and ensures only one strategy is active at a time.
|
|
23
|
-
*/
|
|
24
|
-
export class AuthenticationStrategyRegistry {
|
|
25
|
-
private static strategy: AuthenticationStrategy | null = null;
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Registers a platform-specific authentication strategy
|
|
29
|
-
*
|
|
30
|
-
* @param strategy - The authentication strategy to register
|
|
31
|
-
* @throws Error if strategy is null, undefined, or missing required methods
|
|
32
|
-
*/
|
|
33
|
-
static register(strategy: AuthenticationStrategy): void {
|
|
34
|
-
if (!strategy) {
|
|
35
|
-
throw new Error('Authentication strategy cannot be null or undefined');
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
if (typeof strategy.authenticate !== 'function') {
|
|
39
|
-
throw new Error('Authentication strategy must implement authenticate method');
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
this.strategy = strategy;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Gets the currently registered authentication strategy
|
|
47
|
-
*
|
|
48
|
-
* @returns The registered authentication strategy
|
|
49
|
-
* @throws Error if no strategy has been registered
|
|
50
|
-
*/
|
|
51
|
-
static get(): AuthenticationStrategy {
|
|
52
|
-
if (!this.strategy) {
|
|
53
|
-
throw new Error(
|
|
54
|
-
'No authentication strategy registered. Please register a platform-specific strategy using AuthenticationStrategyRegistry.register()',
|
|
55
|
-
);
|
|
56
|
-
}
|
|
57
|
-
return this.strategy;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Checks if a strategy is currently registered
|
|
62
|
-
*
|
|
63
|
-
* @returns true if a strategy is registered, false otherwise
|
|
64
|
-
*/
|
|
65
|
-
static isRegistered(): boolean {
|
|
66
|
-
return this.strategy !== null;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Resets the registry by removing the current strategy
|
|
71
|
-
*
|
|
72
|
-
* This method is primarily intended for testing scenarios
|
|
73
|
-
* where you need to clean up between test cases.
|
|
74
|
-
*/
|
|
75
|
-
static reset(): void {
|
|
76
|
-
this.strategy = null;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
import type { AuthenticationStrategy } from './AuthenticationStrategy';
|
|
2
|
-
import { SessionStorageStrategy, type StorageStrategy } from './StorageStrategy';
|
|
3
|
-
|
|
4
|
-
/* Web-based authentication strategy
|
|
5
|
-
* Uses redirect flow
|
|
6
|
-
*/
|
|
7
|
-
export class WebAuthenticationStrategy implements AuthenticationStrategy {
|
|
8
|
-
private redirectUri: string;
|
|
9
|
-
private callbackPath: string;
|
|
10
|
-
private timeout: number;
|
|
11
|
-
private storage: StorageStrategy;
|
|
12
|
-
private static pendingAuthResolve: ((url: URL) => void) | null = null;
|
|
13
|
-
private static pendingAuthReject: ((error: Error) => void) | null = null;
|
|
14
|
-
private static timeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
15
|
-
|
|
16
|
-
constructor(options?: {
|
|
17
|
-
redirectUri?: string;
|
|
18
|
-
callbackPath?: string;
|
|
19
|
-
timeout?: number;
|
|
20
|
-
storage?: StorageStrategy;
|
|
21
|
-
}) {
|
|
22
|
-
this.callbackPath = options?.callbackPath ?? '/auth/callback';
|
|
23
|
-
this.redirectUri = options?.redirectUri ?? window.location.origin + this.callbackPath;
|
|
24
|
-
this.timeout = options?.timeout ?? 300000; // 5 minutes default
|
|
25
|
-
this.storage = options?.storage ?? new SessionStorageStrategy();
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
async authenticate(authUrl: URL): Promise<URL> {
|
|
29
|
-
// Update the redirect URI in the auth URL
|
|
30
|
-
authUrl.searchParams.set('redirect_uri', this.redirectUri);
|
|
31
|
-
|
|
32
|
-
return this.authenticateWithRedirect(authUrl);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Call this method when your app loads to handle the redirect callback
|
|
37
|
-
*/
|
|
38
|
-
static handleCallback(callbackPath: string = '/auth/callback'): boolean {
|
|
39
|
-
const currentUrl = new URL(window.location.href);
|
|
40
|
-
|
|
41
|
-
// Check if this is a callback URL
|
|
42
|
-
if (currentUrl.pathname === callbackPath && currentUrl.searchParams.has('status')) {
|
|
43
|
-
// For web apps, use the HTTP URL directly (no need to convert to youversionauth scheme)
|
|
44
|
-
const callbackUrl = new URL(currentUrl.toString());
|
|
45
|
-
|
|
46
|
-
if (WebAuthenticationStrategy.pendingAuthResolve) {
|
|
47
|
-
WebAuthenticationStrategy.pendingAuthResolve(callbackUrl);
|
|
48
|
-
WebAuthenticationStrategy.cleanup();
|
|
49
|
-
} else {
|
|
50
|
-
// Store the callback URL for later retrieval
|
|
51
|
-
const storageStrategy = new SessionStorageStrategy();
|
|
52
|
-
storageStrategy.setItem('youversion-auth-callback', callbackUrl.toString());
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const storageStrategy = new SessionStorageStrategy();
|
|
56
|
-
const returnUrl = storageStrategy.getItem('youversion-auth-return') ?? '/';
|
|
57
|
-
storageStrategy.removeItem('youversion-auth-return');
|
|
58
|
-
window.history.replaceState({}, '', returnUrl);
|
|
59
|
-
|
|
60
|
-
return true;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return false;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Clean up pending authentication state
|
|
68
|
-
*/
|
|
69
|
-
static cleanup(): void {
|
|
70
|
-
if (WebAuthenticationStrategy.timeoutId) {
|
|
71
|
-
clearTimeout(WebAuthenticationStrategy.timeoutId);
|
|
72
|
-
WebAuthenticationStrategy.timeoutId = null;
|
|
73
|
-
}
|
|
74
|
-
WebAuthenticationStrategy.pendingAuthResolve = null;
|
|
75
|
-
WebAuthenticationStrategy.pendingAuthReject = null;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Retrieve stored callback result if available
|
|
80
|
-
*/
|
|
81
|
-
static getStoredCallback(): URL | null {
|
|
82
|
-
const storageStrategy = new SessionStorageStrategy();
|
|
83
|
-
const stored = storageStrategy.getItem('youversion-auth-callback');
|
|
84
|
-
if (stored) {
|
|
85
|
-
storageStrategy.removeItem('youversion-auth-callback');
|
|
86
|
-
try {
|
|
87
|
-
return new URL(stored);
|
|
88
|
-
} catch {
|
|
89
|
-
return null;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
return null;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
private authenticateWithRedirect(authUrl: URL): Promise<URL> {
|
|
96
|
-
// Clean up any existing state
|
|
97
|
-
WebAuthenticationStrategy.cleanup();
|
|
98
|
-
|
|
99
|
-
// Store return URL in configured storage
|
|
100
|
-
this.storage.setItem('youversion-auth-return', window.location.href);
|
|
101
|
-
|
|
102
|
-
// Set up the promise that will be resolved when we come back
|
|
103
|
-
return new Promise((resolve, reject) => {
|
|
104
|
-
WebAuthenticationStrategy.pendingAuthResolve = resolve;
|
|
105
|
-
WebAuthenticationStrategy.pendingAuthReject = reject;
|
|
106
|
-
|
|
107
|
-
// Set up timeout
|
|
108
|
-
WebAuthenticationStrategy.timeoutId = setTimeout(() => {
|
|
109
|
-
WebAuthenticationStrategy.cleanup();
|
|
110
|
-
reject(new Error('Authentication timeout'));
|
|
111
|
-
}, this.timeout);
|
|
112
|
-
|
|
113
|
-
// Handle cases where navigation might fail
|
|
114
|
-
try {
|
|
115
|
-
// Redirect to auth URL
|
|
116
|
-
window.location.href = authUrl.toString();
|
|
117
|
-
} catch (error) {
|
|
118
|
-
WebAuthenticationStrategy.cleanup();
|
|
119
|
-
reject(
|
|
120
|
-
new Error(
|
|
121
|
-
`Failed to navigate to auth URL: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
122
|
-
),
|
|
123
|
-
);
|
|
124
|
-
}
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
}
|