@youversion/platform-core 0.6.0 → 0.8.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 +46 -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 +2 -1
- 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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@youversion/platform-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public",
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
|
|
41
41
|
"build": "tsup src/index.ts --format cjs,esm --dts",
|
|
42
42
|
"lint": "eslint . --max-warnings 0",
|
|
43
|
+
"typecheck": "tsc --noEmit",
|
|
43
44
|
"test": "dotenv -e .env.local -- vitest run",
|
|
44
45
|
"test:watch": "dotenv -e .env.local -- vitest",
|
|
45
46
|
"test:coverage": "dotenv -e .env.local -- vitest run --coverage"
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { YouVersionPlatformConfiguration } from './YouVersionPlatformConfiguration';
|
|
2
|
+
import type { SignInWithYouVersionPermissionValues } from './types/auth';
|
|
3
|
+
|
|
4
|
+
type SignInWithYouVersionPKCEParameters = {
|
|
5
|
+
readonly codeVerifier: string;
|
|
6
|
+
readonly codeChallenge: string;
|
|
7
|
+
readonly state: string;
|
|
8
|
+
readonly nonce: string;
|
|
9
|
+
};
|
|
10
|
+
type SignInWithYouVersionPKCEAuthorizationRequest = {
|
|
11
|
+
readonly url: URL;
|
|
12
|
+
readonly parameters: SignInWithYouVersionPKCEParameters;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export class SignInWithYouVersionPKCEAuthorizationRequestBuilder {
|
|
16
|
+
public static async make(
|
|
17
|
+
appKey: string,
|
|
18
|
+
permissions: Set<SignInWithYouVersionPermissionValues>,
|
|
19
|
+
redirectURL: URL,
|
|
20
|
+
): Promise<SignInWithYouVersionPKCEAuthorizationRequest> {
|
|
21
|
+
const codeVerifier = this.randomURLSafeString(32);
|
|
22
|
+
const codeChallenge = await this.codeChallenge(codeVerifier);
|
|
23
|
+
const state = this.randomURLSafeString(24);
|
|
24
|
+
const nonce = this.randomURLSafeString(24);
|
|
25
|
+
|
|
26
|
+
const parameters: SignInWithYouVersionPKCEParameters = {
|
|
27
|
+
codeVerifier,
|
|
28
|
+
codeChallenge,
|
|
29
|
+
state,
|
|
30
|
+
nonce,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const url = this.authorizeURL(appKey, permissions, redirectURL, parameters);
|
|
34
|
+
|
|
35
|
+
return { url, parameters };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private static authorizeURL(
|
|
39
|
+
appKey: string,
|
|
40
|
+
permissions: Set<SignInWithYouVersionPermissionValues>,
|
|
41
|
+
redirectURL: URL,
|
|
42
|
+
parameters: SignInWithYouVersionPKCEParameters,
|
|
43
|
+
): URL {
|
|
44
|
+
const components = new URL(`https://${YouVersionPlatformConfiguration.apiHost}/auth/authorize`);
|
|
45
|
+
|
|
46
|
+
const redirectUrlString = redirectURL.toString().endsWith('/')
|
|
47
|
+
? redirectURL.toString().slice(0, -1)
|
|
48
|
+
: redirectURL.toString();
|
|
49
|
+
const queryParams = new URLSearchParams({
|
|
50
|
+
response_type: 'code',
|
|
51
|
+
client_id: appKey,
|
|
52
|
+
redirect_uri: redirectUrlString,
|
|
53
|
+
nonce: parameters.nonce,
|
|
54
|
+
state: parameters.state,
|
|
55
|
+
code_challenge: parameters.codeChallenge,
|
|
56
|
+
code_challenge_method: 'S256',
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const installId = YouVersionPlatformConfiguration.installationId;
|
|
60
|
+
if (installId) {
|
|
61
|
+
queryParams.set('x-yvp-installation-id', installId);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const scopeValue = this.scopeValue(permissions);
|
|
65
|
+
if (scopeValue) {
|
|
66
|
+
queryParams.set('scope', scopeValue);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
components.search = queryParams.toString();
|
|
70
|
+
return components;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
public static tokenURLRequest(code: string, codeVerifier: string, redirectUri: string): Request {
|
|
74
|
+
const apiHost = YouVersionPlatformConfiguration.apiHost;
|
|
75
|
+
const appKey = YouVersionPlatformConfiguration.appKey;
|
|
76
|
+
const url = new URL(`https://${apiHost}/auth/token`);
|
|
77
|
+
|
|
78
|
+
const parameters = new URLSearchParams({
|
|
79
|
+
grant_type: 'authorization_code',
|
|
80
|
+
code: code,
|
|
81
|
+
redirect_uri: redirectUri,
|
|
82
|
+
client_id: appKey ?? '',
|
|
83
|
+
code_verifier: codeVerifier,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
return new Request(url, {
|
|
87
|
+
method: 'POST',
|
|
88
|
+
body: parameters,
|
|
89
|
+
headers: {
|
|
90
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private static randomURLSafeString(byteCount: number): string {
|
|
96
|
+
const bytes = new Uint8Array(byteCount);
|
|
97
|
+
crypto.getRandomValues(bytes);
|
|
98
|
+
return this.base64URLEncodedString(bytes);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private static async codeChallenge(verifier: string): Promise<string> {
|
|
102
|
+
const data = new TextEncoder().encode(verifier);
|
|
103
|
+
const digest = await crypto.subtle.digest('SHA-256', data);
|
|
104
|
+
return this.base64URLEncodedString(new Uint8Array(digest));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private static base64URLEncodedString(data: Uint8Array): string {
|
|
108
|
+
const base64 = btoa(String.fromCharCode.apply(null, Array.from(data)));
|
|
109
|
+
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private static scopeValue(permissions: Set<SignInWithYouVersionPermissionValues>): string | null {
|
|
113
|
+
const scopeArray = Array.from(permissions).sort();
|
|
114
|
+
let scopeWithOpenID = scopeArray.join(' ');
|
|
115
|
+
|
|
116
|
+
if (!scopeWithOpenID.split(' ').includes('openid')) {
|
|
117
|
+
scopeWithOpenID += (scopeWithOpenID === '' ? '' : ' ') + 'openid';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return scopeWithOpenID || null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -8,46 +8,47 @@ export const SignInWithYouVersionPermission = {
|
|
|
8
8
|
bibleActivity: 'bible_activity',
|
|
9
9
|
} as const;
|
|
10
10
|
|
|
11
|
+
type SignInWithYouVersionResultProps = {
|
|
12
|
+
accessToken?: string;
|
|
13
|
+
expiresIn?: number;
|
|
14
|
+
refreshToken?: string;
|
|
15
|
+
idToken?: string;
|
|
16
|
+
permissions?: SignInWithYouVersionPermissionValues[];
|
|
17
|
+
yvpUserId?: string;
|
|
18
|
+
name?: string;
|
|
19
|
+
profilePicture?: string;
|
|
20
|
+
email?: string;
|
|
21
|
+
};
|
|
11
22
|
export class SignInWithYouVersionResult {
|
|
12
|
-
public readonly accessToken: string |
|
|
13
|
-
public readonly
|
|
14
|
-
public readonly
|
|
15
|
-
public readonly
|
|
23
|
+
public readonly accessToken: string | undefined;
|
|
24
|
+
public readonly expiryDate: Date | undefined;
|
|
25
|
+
public readonly refreshToken: string | undefined;
|
|
26
|
+
public readonly idToken: string | undefined;
|
|
27
|
+
public readonly permissions: SignInWithYouVersionPermissionValues[] | undefined;
|
|
28
|
+
public readonly yvpUserId: string | undefined;
|
|
29
|
+
public readonly name: string | undefined;
|
|
30
|
+
public readonly profilePicture: string | undefined;
|
|
31
|
+
public readonly email: string | undefined;
|
|
16
32
|
|
|
17
|
-
constructor(
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
this.accessToken = latValue;
|
|
38
|
-
this.permissions = perms;
|
|
39
|
-
this.errorMsg = null;
|
|
40
|
-
this.yvpUserId = userId;
|
|
41
|
-
} else if (status === 'canceled') {
|
|
42
|
-
this.accessToken = null;
|
|
43
|
-
this.permissions = [];
|
|
44
|
-
this.errorMsg = null;
|
|
45
|
-
this.yvpUserId = null;
|
|
46
|
-
} else {
|
|
47
|
-
this.accessToken = null;
|
|
48
|
-
this.permissions = [];
|
|
49
|
-
this.errorMsg = 'Authentication failed';
|
|
50
|
-
this.yvpUserId = null;
|
|
51
|
-
}
|
|
33
|
+
constructor({
|
|
34
|
+
accessToken,
|
|
35
|
+
expiresIn,
|
|
36
|
+
refreshToken,
|
|
37
|
+
idToken,
|
|
38
|
+
permissions,
|
|
39
|
+
yvpUserId,
|
|
40
|
+
name,
|
|
41
|
+
profilePicture,
|
|
42
|
+
email,
|
|
43
|
+
}: SignInWithYouVersionResultProps) {
|
|
44
|
+
this.accessToken = accessToken;
|
|
45
|
+
this.expiryDate = expiresIn ? new Date(Date.now() + expiresIn * 1000) : new Date();
|
|
46
|
+
this.refreshToken = refreshToken;
|
|
47
|
+
this.idToken = idToken;
|
|
48
|
+
this.permissions = permissions;
|
|
49
|
+
this.yvpUserId = yvpUserId;
|
|
50
|
+
this.name = name;
|
|
51
|
+
this.profilePicture = profilePicture;
|
|
52
|
+
this.email = email;
|
|
52
53
|
}
|
|
53
54
|
}
|
package/src/URLBuilder.ts
CHANGED
|
@@ -47,25 +47,4 @@ export class URLBuilder {
|
|
|
47
47
|
);
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
|
-
|
|
51
|
-
static userURL(accessToken: string): URL {
|
|
52
|
-
if (typeof accessToken !== 'string' || accessToken.trim().length === 0) {
|
|
53
|
-
throw new Error('accessToken must be a non-empty string');
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
try {
|
|
57
|
-
const url = new URL(this.baseURL);
|
|
58
|
-
url.pathname = '/auth/me';
|
|
59
|
-
|
|
60
|
-
const searchParams = new URLSearchParams();
|
|
61
|
-
searchParams.append('lat', accessToken);
|
|
62
|
-
|
|
63
|
-
url.search = searchParams.toString();
|
|
64
|
-
return url;
|
|
65
|
-
} catch (error) {
|
|
66
|
-
throw new Error(
|
|
67
|
-
`Failed to construct user URL: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
68
|
-
);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
50
|
}
|