@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/dist/index.js
CHANGED
|
@@ -401,10 +401,9 @@ import { z as z3 } from "zod";
|
|
|
401
401
|
var YouVersionPlatformConfiguration = class {
|
|
402
402
|
static _appKey = null;
|
|
403
403
|
static _installationId = null;
|
|
404
|
-
static _accessToken = null;
|
|
405
404
|
static _apiHost = "api.youversion.com";
|
|
406
|
-
static
|
|
407
|
-
static
|
|
405
|
+
static _refreshTokenKey = null;
|
|
406
|
+
static _expiryDateKey = null;
|
|
408
407
|
static getOrSetInstallationId() {
|
|
409
408
|
if (typeof window === "undefined") {
|
|
410
409
|
return "";
|
|
@@ -417,6 +416,44 @@ var YouVersionPlatformConfiguration = class {
|
|
|
417
416
|
localStorage.setItem("x-yvp-installation-id", newId);
|
|
418
417
|
return newId;
|
|
419
418
|
}
|
|
419
|
+
static saveAuthData(accessToken, refreshToken, idToken, expiryDate) {
|
|
420
|
+
if (accessToken !== null) {
|
|
421
|
+
localStorage.setItem("accessToken", accessToken);
|
|
422
|
+
} else {
|
|
423
|
+
localStorage.removeItem("accessToken");
|
|
424
|
+
}
|
|
425
|
+
if (refreshToken !== null) {
|
|
426
|
+
localStorage.setItem("refreshToken", refreshToken);
|
|
427
|
+
} else {
|
|
428
|
+
localStorage.removeItem("refreshToken");
|
|
429
|
+
}
|
|
430
|
+
if (idToken !== null) {
|
|
431
|
+
localStorage.setItem("idToken", idToken);
|
|
432
|
+
} else {
|
|
433
|
+
localStorage.removeItem("idToken");
|
|
434
|
+
}
|
|
435
|
+
if (expiryDate !== null) {
|
|
436
|
+
localStorage.setItem("expiryDate", expiryDate.toISOString());
|
|
437
|
+
} else {
|
|
438
|
+
localStorage.removeItem("expiryDate");
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
static clearAuthTokens() {
|
|
442
|
+
this.saveAuthData(null, null, null, null);
|
|
443
|
+
}
|
|
444
|
+
static get accessToken() {
|
|
445
|
+
return localStorage.getItem("accessToken");
|
|
446
|
+
}
|
|
447
|
+
static get refreshToken() {
|
|
448
|
+
return localStorage.getItem("refreshToken");
|
|
449
|
+
}
|
|
450
|
+
static get idToken() {
|
|
451
|
+
return localStorage.getItem("idToken");
|
|
452
|
+
}
|
|
453
|
+
static get tokenExpiryDate() {
|
|
454
|
+
const dateString = localStorage.getItem("expiryDate");
|
|
455
|
+
return dateString ? new Date(dateString) : null;
|
|
456
|
+
}
|
|
420
457
|
static get appKey() {
|
|
421
458
|
return this._appKey;
|
|
422
459
|
}
|
|
@@ -432,32 +469,23 @@ var YouVersionPlatformConfiguration = class {
|
|
|
432
469
|
static set installationId(value) {
|
|
433
470
|
this._installationId = value || this.getOrSetInstallationId();
|
|
434
471
|
}
|
|
435
|
-
static setAccessToken(token) {
|
|
436
|
-
if (token !== null && (typeof token !== "string" || token.trim().length === 0)) {
|
|
437
|
-
throw new Error("Access token must be a non-empty string or null");
|
|
438
|
-
}
|
|
439
|
-
this._accessToken = token;
|
|
440
|
-
}
|
|
441
|
-
static get accessToken() {
|
|
442
|
-
return this._accessToken;
|
|
443
|
-
}
|
|
444
472
|
static get apiHost() {
|
|
445
473
|
return this._apiHost;
|
|
446
474
|
}
|
|
447
475
|
static set apiHost(value) {
|
|
448
476
|
this._apiHost = value;
|
|
449
477
|
}
|
|
450
|
-
static get
|
|
451
|
-
return this.
|
|
478
|
+
static get refreshTokenKey() {
|
|
479
|
+
return this._refreshTokenKey;
|
|
452
480
|
}
|
|
453
|
-
static set
|
|
454
|
-
this.
|
|
481
|
+
static set refreshTokenKey(value) {
|
|
482
|
+
this._refreshTokenKey = value;
|
|
455
483
|
}
|
|
456
|
-
static get
|
|
457
|
-
return this.
|
|
484
|
+
static get expiryDateKey() {
|
|
485
|
+
return this._expiryDateKey;
|
|
458
486
|
}
|
|
459
|
-
static set
|
|
460
|
-
this.
|
|
487
|
+
static set expiryDateKey(value) {
|
|
488
|
+
this._expiryDateKey = value;
|
|
461
489
|
}
|
|
462
490
|
};
|
|
463
491
|
|
|
@@ -582,78 +610,6 @@ var HighlightsClient = class {
|
|
|
582
610
|
}
|
|
583
611
|
};
|
|
584
612
|
|
|
585
|
-
// src/authentication.ts
|
|
586
|
-
var AuthClient = class {
|
|
587
|
-
client;
|
|
588
|
-
/**
|
|
589
|
-
* Creates an instance of AuthClient.
|
|
590
|
-
* @param client - The ApiClient instance to use for requests.
|
|
591
|
-
*/
|
|
592
|
-
constructor(client) {
|
|
593
|
-
this.client = client;
|
|
594
|
-
}
|
|
595
|
-
/**
|
|
596
|
-
* Retrieves the current authenticated user.
|
|
597
|
-
*
|
|
598
|
-
* @param lat - The long access token (LAT) used for authentication.
|
|
599
|
-
* @returns A promise that resolves to the authenticated User.
|
|
600
|
-
*/
|
|
601
|
-
async getUser(lat) {
|
|
602
|
-
return this.client.get(`/auth/me`, { lat });
|
|
603
|
-
}
|
|
604
|
-
};
|
|
605
|
-
|
|
606
|
-
// src/AuthenticationStrategy.ts
|
|
607
|
-
var AuthenticationStrategyRegistry = class {
|
|
608
|
-
static strategy = null;
|
|
609
|
-
/**
|
|
610
|
-
* Registers a platform-specific authentication strategy
|
|
611
|
-
*
|
|
612
|
-
* @param strategy - The authentication strategy to register
|
|
613
|
-
* @throws Error if strategy is null, undefined, or missing required methods
|
|
614
|
-
*/
|
|
615
|
-
static register(strategy) {
|
|
616
|
-
if (!strategy) {
|
|
617
|
-
throw new Error("Authentication strategy cannot be null or undefined");
|
|
618
|
-
}
|
|
619
|
-
if (typeof strategy.authenticate !== "function") {
|
|
620
|
-
throw new Error("Authentication strategy must implement authenticate method");
|
|
621
|
-
}
|
|
622
|
-
this.strategy = strategy;
|
|
623
|
-
}
|
|
624
|
-
/**
|
|
625
|
-
* Gets the currently registered authentication strategy
|
|
626
|
-
*
|
|
627
|
-
* @returns The registered authentication strategy
|
|
628
|
-
* @throws Error if no strategy has been registered
|
|
629
|
-
*/
|
|
630
|
-
static get() {
|
|
631
|
-
if (!this.strategy) {
|
|
632
|
-
throw new Error(
|
|
633
|
-
"No authentication strategy registered. Please register a platform-specific strategy using AuthenticationStrategyRegistry.register()"
|
|
634
|
-
);
|
|
635
|
-
}
|
|
636
|
-
return this.strategy;
|
|
637
|
-
}
|
|
638
|
-
/**
|
|
639
|
-
* Checks if a strategy is currently registered
|
|
640
|
-
*
|
|
641
|
-
* @returns true if a strategy is registered, false otherwise
|
|
642
|
-
*/
|
|
643
|
-
static isRegistered() {
|
|
644
|
-
return this.strategy !== null;
|
|
645
|
-
}
|
|
646
|
-
/**
|
|
647
|
-
* Resets the registry by removing the current strategy
|
|
648
|
-
*
|
|
649
|
-
* This method is primarily intended for testing scenarios
|
|
650
|
-
* where you need to clean up between test cases.
|
|
651
|
-
*/
|
|
652
|
-
static reset() {
|
|
653
|
-
this.strategy = null;
|
|
654
|
-
}
|
|
655
|
-
};
|
|
656
|
-
|
|
657
613
|
// src/StorageStrategy.ts
|
|
658
614
|
var SessionStorageStrategy = class {
|
|
659
615
|
setItem(key, value) {
|
|
@@ -698,96 +654,122 @@ var MemoryStorageStrategy = class {
|
|
|
698
654
|
}
|
|
699
655
|
};
|
|
700
656
|
|
|
701
|
-
// src/
|
|
702
|
-
var
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
constructor(options) {
|
|
711
|
-
this.callbackPath = options?.callbackPath ?? "/auth/callback";
|
|
712
|
-
this.redirectUri = options?.redirectUri ?? window.location.origin + this.callbackPath;
|
|
713
|
-
this.timeout = options?.timeout ?? 3e5;
|
|
714
|
-
this.storage = options?.storage ?? new SessionStorageStrategy();
|
|
715
|
-
}
|
|
716
|
-
async authenticate(authUrl) {
|
|
717
|
-
authUrl.searchParams.set("redirect_uri", this.redirectUri);
|
|
718
|
-
return this.authenticateWithRedirect(authUrl);
|
|
719
|
-
}
|
|
720
|
-
/**
|
|
721
|
-
* Call this method when your app loads to handle the redirect callback
|
|
722
|
-
*/
|
|
723
|
-
static handleCallback(callbackPath = "/auth/callback") {
|
|
724
|
-
const currentUrl = new URL(window.location.href);
|
|
725
|
-
if (currentUrl.pathname === callbackPath && currentUrl.searchParams.has("status")) {
|
|
726
|
-
const callbackUrl = new URL(currentUrl.toString());
|
|
727
|
-
if (_WebAuthenticationStrategy.pendingAuthResolve) {
|
|
728
|
-
_WebAuthenticationStrategy.pendingAuthResolve(callbackUrl);
|
|
729
|
-
_WebAuthenticationStrategy.cleanup();
|
|
730
|
-
} else {
|
|
731
|
-
const storageStrategy2 = new SessionStorageStrategy();
|
|
732
|
-
storageStrategy2.setItem("youversion-auth-callback", callbackUrl.toString());
|
|
733
|
-
}
|
|
734
|
-
const storageStrategy = new SessionStorageStrategy();
|
|
735
|
-
const returnUrl = storageStrategy.getItem("youversion-auth-return") ?? "/";
|
|
736
|
-
storageStrategy.removeItem("youversion-auth-return");
|
|
737
|
-
window.history.replaceState({}, "", returnUrl);
|
|
738
|
-
return true;
|
|
657
|
+
// src/YouVersionUserInfo.ts
|
|
658
|
+
var YouVersionUserInfo = class {
|
|
659
|
+
name;
|
|
660
|
+
userId;
|
|
661
|
+
email;
|
|
662
|
+
avatarUrlFormat;
|
|
663
|
+
constructor(data) {
|
|
664
|
+
if (!data || typeof data !== "object") {
|
|
665
|
+
throw new Error("Invalid user data provided");
|
|
739
666
|
}
|
|
740
|
-
|
|
667
|
+
this.name = data.name;
|
|
668
|
+
this.userId = data.id;
|
|
669
|
+
this.email = data.email;
|
|
670
|
+
this.avatarUrlFormat = data.avatar_url;
|
|
741
671
|
}
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
672
|
+
getAvatarUrl(width = 200, height = 200) {
|
|
673
|
+
if (!this.avatarUrlFormat) {
|
|
674
|
+
return null;
|
|
675
|
+
}
|
|
676
|
+
let urlString = this.avatarUrlFormat;
|
|
677
|
+
if (urlString.startsWith("//")) {
|
|
678
|
+
urlString = "https:" + urlString;
|
|
679
|
+
}
|
|
680
|
+
urlString = urlString.replace("{width}", width.toString());
|
|
681
|
+
urlString = urlString.replace("{height}", height.toString());
|
|
682
|
+
try {
|
|
683
|
+
return new URL(urlString);
|
|
684
|
+
} catch {
|
|
685
|
+
return null;
|
|
749
686
|
}
|
|
750
|
-
_WebAuthenticationStrategy.pendingAuthResolve = null;
|
|
751
|
-
_WebAuthenticationStrategy.pendingAuthReject = null;
|
|
752
687
|
}
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
688
|
+
get avatarUrl() {
|
|
689
|
+
return this.getAvatarUrl();
|
|
690
|
+
}
|
|
691
|
+
};
|
|
692
|
+
|
|
693
|
+
// src/SignInWithYouVersionPKCE.ts
|
|
694
|
+
var SignInWithYouVersionPKCEAuthorizationRequestBuilder = class {
|
|
695
|
+
static async make(appKey, permissions, redirectURL) {
|
|
696
|
+
const codeVerifier = this.randomURLSafeString(32);
|
|
697
|
+
const codeChallenge = await this.codeChallenge(codeVerifier);
|
|
698
|
+
const state = this.randomURLSafeString(24);
|
|
699
|
+
const nonce = this.randomURLSafeString(24);
|
|
700
|
+
const parameters = {
|
|
701
|
+
codeVerifier,
|
|
702
|
+
codeChallenge,
|
|
703
|
+
state,
|
|
704
|
+
nonce
|
|
705
|
+
};
|
|
706
|
+
const url = this.authorizeURL(appKey, permissions, redirectURL, parameters);
|
|
707
|
+
return { url, parameters };
|
|
708
|
+
}
|
|
709
|
+
static authorizeURL(appKey, permissions, redirectURL, parameters) {
|
|
710
|
+
const components = new URL(`https://${YouVersionPlatformConfiguration.apiHost}/auth/authorize`);
|
|
711
|
+
const redirectUrlString = redirectURL.toString().endsWith("/") ? redirectURL.toString().slice(0, -1) : redirectURL.toString();
|
|
712
|
+
const queryParams = new URLSearchParams({
|
|
713
|
+
response_type: "code",
|
|
714
|
+
client_id: appKey,
|
|
715
|
+
redirect_uri: redirectUrlString,
|
|
716
|
+
nonce: parameters.nonce,
|
|
717
|
+
state: parameters.state,
|
|
718
|
+
code_challenge: parameters.codeChallenge,
|
|
719
|
+
code_challenge_method: "S256"
|
|
720
|
+
});
|
|
721
|
+
const installId = YouVersionPlatformConfiguration.installationId;
|
|
722
|
+
if (installId) {
|
|
723
|
+
queryParams.set("x-yvp-installation-id", installId);
|
|
766
724
|
}
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
return
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
725
|
+
const scopeValue = this.scopeValue(permissions);
|
|
726
|
+
if (scopeValue) {
|
|
727
|
+
queryParams.set("scope", scopeValue);
|
|
728
|
+
}
|
|
729
|
+
components.search = queryParams.toString();
|
|
730
|
+
return components;
|
|
731
|
+
}
|
|
732
|
+
static tokenURLRequest(code, codeVerifier, redirectUri) {
|
|
733
|
+
const apiHost = YouVersionPlatformConfiguration.apiHost;
|
|
734
|
+
const appKey = YouVersionPlatformConfiguration.appKey;
|
|
735
|
+
const url = new URL(`https://${apiHost}/auth/token`);
|
|
736
|
+
const parameters = new URLSearchParams({
|
|
737
|
+
grant_type: "authorization_code",
|
|
738
|
+
code,
|
|
739
|
+
redirect_uri: redirectUri,
|
|
740
|
+
client_id: appKey ?? "",
|
|
741
|
+
code_verifier: codeVerifier
|
|
742
|
+
});
|
|
743
|
+
return new Request(url, {
|
|
744
|
+
method: "POST",
|
|
745
|
+
body: parameters,
|
|
746
|
+
headers: {
|
|
747
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
788
748
|
}
|
|
789
749
|
});
|
|
790
750
|
}
|
|
751
|
+
static randomURLSafeString(byteCount) {
|
|
752
|
+
const bytes = new Uint8Array(byteCount);
|
|
753
|
+
crypto.getRandomValues(bytes);
|
|
754
|
+
return this.base64URLEncodedString(bytes);
|
|
755
|
+
}
|
|
756
|
+
static async codeChallenge(verifier) {
|
|
757
|
+
const data = new TextEncoder().encode(verifier);
|
|
758
|
+
const digest = await crypto.subtle.digest("SHA-256", data);
|
|
759
|
+
return this.base64URLEncodedString(new Uint8Array(digest));
|
|
760
|
+
}
|
|
761
|
+
static base64URLEncodedString(data) {
|
|
762
|
+
const base64 = btoa(String.fromCharCode.apply(null, Array.from(data)));
|
|
763
|
+
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
764
|
+
}
|
|
765
|
+
static scopeValue(permissions) {
|
|
766
|
+
const scopeArray = Array.from(permissions).sort();
|
|
767
|
+
let scopeWithOpenID = scopeArray.join(" ");
|
|
768
|
+
if (!scopeWithOpenID.split(" ").includes("openid")) {
|
|
769
|
+
scopeWithOpenID += (scopeWithOpenID === "" ? "" : " ") + "openid";
|
|
770
|
+
}
|
|
771
|
+
return scopeWithOpenID || null;
|
|
772
|
+
}
|
|
791
773
|
};
|
|
792
774
|
|
|
793
775
|
// src/SignInWithYouVersionResult.ts
|
|
@@ -800,72 +782,336 @@ var SignInWithYouVersionPermission = {
|
|
|
800
782
|
};
|
|
801
783
|
var SignInWithYouVersionResult = class {
|
|
802
784
|
accessToken;
|
|
785
|
+
expiryDate;
|
|
786
|
+
refreshToken;
|
|
787
|
+
idToken;
|
|
803
788
|
permissions;
|
|
804
|
-
errorMsg;
|
|
805
789
|
yvpUserId;
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
this.permissions = [];
|
|
830
|
-
this.errorMsg = "Authentication failed";
|
|
831
|
-
this.yvpUserId = null;
|
|
832
|
-
}
|
|
790
|
+
name;
|
|
791
|
+
profilePicture;
|
|
792
|
+
email;
|
|
793
|
+
constructor({
|
|
794
|
+
accessToken,
|
|
795
|
+
expiresIn,
|
|
796
|
+
refreshToken,
|
|
797
|
+
idToken,
|
|
798
|
+
permissions,
|
|
799
|
+
yvpUserId,
|
|
800
|
+
name,
|
|
801
|
+
profilePicture,
|
|
802
|
+
email
|
|
803
|
+
}) {
|
|
804
|
+
this.accessToken = accessToken;
|
|
805
|
+
this.expiryDate = expiresIn ? new Date(Date.now() + expiresIn * 1e3) : /* @__PURE__ */ new Date();
|
|
806
|
+
this.refreshToken = refreshToken;
|
|
807
|
+
this.idToken = idToken;
|
|
808
|
+
this.permissions = permissions;
|
|
809
|
+
this.yvpUserId = yvpUserId;
|
|
810
|
+
this.name = name;
|
|
811
|
+
this.profilePicture = profilePicture;
|
|
812
|
+
this.email = email;
|
|
833
813
|
}
|
|
834
814
|
};
|
|
835
815
|
|
|
836
|
-
// src/
|
|
837
|
-
var
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
816
|
+
// src/Users.ts
|
|
817
|
+
var YouVersionAPIUsers = class {
|
|
818
|
+
/**
|
|
819
|
+
* Presents the YouVersion login flow to the user and returns the login result upon completion.
|
|
820
|
+
*
|
|
821
|
+
* This function authenticates the user with YouVersion, requesting the specified required and optional permissions.
|
|
822
|
+
* The function redirects to the YouVersion authorization URL and expects the callback to be handled separately.
|
|
823
|
+
*
|
|
824
|
+
* @param permissions - The set of permissions that must be granted by the user for successful login.
|
|
825
|
+
* @param redirectURL - The URL to redirect back to after authentication.
|
|
826
|
+
* @throws An error if authentication fails or configuration is invalid.
|
|
827
|
+
*/
|
|
828
|
+
static async signIn(permissions, redirectURL) {
|
|
829
|
+
const appKey = YouVersionPlatformConfiguration.appKey;
|
|
830
|
+
if (!appKey) {
|
|
831
|
+
throw new Error("YouVersionPlatformConfiguration.appKey must be set before calling signIn");
|
|
845
832
|
}
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
833
|
+
const authorizationRequest = await SignInWithYouVersionPKCEAuthorizationRequestBuilder.make(
|
|
834
|
+
appKey,
|
|
835
|
+
permissions,
|
|
836
|
+
new URL(redirectURL)
|
|
837
|
+
);
|
|
838
|
+
localStorage.setItem(
|
|
839
|
+
"youversion-auth-code-verifier",
|
|
840
|
+
authorizationRequest.parameters.codeVerifier
|
|
841
|
+
);
|
|
842
|
+
const redirectUrlString = redirectURL.toString().endsWith("/") ? redirectURL.toString().slice(0, -1) : redirectURL.toString();
|
|
843
|
+
localStorage.setItem("youversion-auth-redirect-uri", redirectUrlString);
|
|
844
|
+
localStorage.setItem("youversion-auth-state", authorizationRequest.parameters.state);
|
|
845
|
+
window.location.href = authorizationRequest.url.toString();
|
|
850
846
|
}
|
|
851
|
-
|
|
852
|
-
|
|
847
|
+
/**
|
|
848
|
+
* Handles the OAuth callback after user authentication.
|
|
849
|
+
*
|
|
850
|
+
* Call this method when your app loads to check if the current URL contains
|
|
851
|
+
* an OAuth callback with authorization code. If found, it exchanges the code
|
|
852
|
+
* for tokens and stores them.
|
|
853
|
+
*
|
|
854
|
+
* @returns Promise<SignInWithYouVersionResult | null> - SignInWithYouVersionResult if callback was handled, null otherwise
|
|
855
|
+
* @throws An error if token exchange fails
|
|
856
|
+
*/
|
|
857
|
+
static async handleAuthCallback() {
|
|
858
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
859
|
+
const code = urlParams.get("code");
|
|
860
|
+
const state = urlParams.get("state");
|
|
861
|
+
const error = urlParams.get("error");
|
|
862
|
+
if (!state && !error) {
|
|
853
863
|
return null;
|
|
854
864
|
}
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
865
|
+
if (error) {
|
|
866
|
+
const errorDescription = urlParams.get("error_description") || error;
|
|
867
|
+
throw new Error(`OAuth authentication failed: ${errorDescription}`);
|
|
868
|
+
}
|
|
869
|
+
const storedState = localStorage.getItem("youversion-auth-state");
|
|
870
|
+
if (state !== storedState) {
|
|
871
|
+
throw new Error("Invalid state parameter - possible CSRF attack");
|
|
872
|
+
}
|
|
873
|
+
if (!code && state) {
|
|
874
|
+
this.obtainLocation(window.location.href, state);
|
|
875
|
+
}
|
|
876
|
+
const codeVerifier = localStorage.getItem("youversion-auth-code-verifier");
|
|
877
|
+
const redirectUri = localStorage.getItem("youversion-auth-redirect-uri");
|
|
878
|
+
if (!code || !codeVerifier || !redirectUri) {
|
|
879
|
+
throw new Error("Missing required authentication parameters");
|
|
858
880
|
}
|
|
859
|
-
urlString = urlString.replace("{width}", width.toString());
|
|
860
|
-
urlString = urlString.replace("{height}", height.toString());
|
|
861
881
|
try {
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
882
|
+
const tokenRequest = SignInWithYouVersionPKCEAuthorizationRequestBuilder.tokenURLRequest(
|
|
883
|
+
code,
|
|
884
|
+
codeVerifier,
|
|
885
|
+
redirectUri
|
|
886
|
+
);
|
|
887
|
+
const response = await fetch(tokenRequest);
|
|
888
|
+
if (!response.ok) {
|
|
889
|
+
throw new Error(`Token exchange failed: ${response.status} ${response.statusText}`);
|
|
890
|
+
}
|
|
891
|
+
const responseText = await response.text();
|
|
892
|
+
const tokens = JSON.parse(responseText);
|
|
893
|
+
const result = this.extractSignInResult(tokens);
|
|
894
|
+
YouVersionPlatformConfiguration.saveAuthData(
|
|
895
|
+
result.accessToken || null,
|
|
896
|
+
result.refreshToken || null,
|
|
897
|
+
result.idToken || null,
|
|
898
|
+
result.expiryDate || null
|
|
899
|
+
);
|
|
900
|
+
localStorage.removeItem("youversion-auth-code-verifier");
|
|
901
|
+
localStorage.removeItem("youversion-auth-redirect-uri");
|
|
902
|
+
localStorage.removeItem("youversion-auth-state");
|
|
903
|
+
const cleanUrl = new URL(window.location.href);
|
|
904
|
+
cleanUrl.search = "";
|
|
905
|
+
window.history.replaceState({}, "", cleanUrl.toString());
|
|
906
|
+
return result;
|
|
907
|
+
} catch (error2) {
|
|
908
|
+
localStorage.removeItem("youversion-auth-code-verifier");
|
|
909
|
+
localStorage.removeItem("youversion-auth-redirect-uri");
|
|
910
|
+
localStorage.removeItem("youversion-auth-state");
|
|
911
|
+
throw error2;
|
|
865
912
|
}
|
|
866
913
|
}
|
|
867
|
-
|
|
868
|
-
|
|
914
|
+
/**
|
|
915
|
+
* Redirects to the server callback endpoint to obtain authorization code
|
|
916
|
+
*/
|
|
917
|
+
static obtainLocation(callbackURL, state) {
|
|
918
|
+
const url = new URL(callbackURL);
|
|
919
|
+
const params = new URLSearchParams(url.search);
|
|
920
|
+
if (params.get("state") !== state) {
|
|
921
|
+
throw new Error("Invalid state parameter");
|
|
922
|
+
}
|
|
923
|
+
const serverCallbackUrl = new URL(
|
|
924
|
+
`https://${YouVersionPlatformConfiguration.apiHost}/auth/callback`
|
|
925
|
+
);
|
|
926
|
+
params.forEach((value, key) => {
|
|
927
|
+
serverCallbackUrl.searchParams.set(key, value);
|
|
928
|
+
});
|
|
929
|
+
window.location.href = serverCallbackUrl.toString();
|
|
930
|
+
}
|
|
931
|
+
/**
|
|
932
|
+
* Extracts sign-in result from token response
|
|
933
|
+
*/
|
|
934
|
+
static extractSignInResult(tokens) {
|
|
935
|
+
const idClaims = this.decodeJWT(tokens.id_token);
|
|
936
|
+
const permissions = tokens.scope.split(" ").map((p) => p.trim()).filter((p) => p.length > 0).filter(
|
|
937
|
+
(p) => Object.values(SignInWithYouVersionPermission).includes(
|
|
938
|
+
p
|
|
939
|
+
)
|
|
940
|
+
);
|
|
941
|
+
const resultData = {
|
|
942
|
+
accessToken: tokens.access_token,
|
|
943
|
+
expiresIn: tokens.expires_in,
|
|
944
|
+
refreshToken: tokens.refresh_token,
|
|
945
|
+
idToken: tokens.id_token,
|
|
946
|
+
permissions,
|
|
947
|
+
yvpUserId: idClaims.sub,
|
|
948
|
+
name: idClaims.name,
|
|
949
|
+
profilePicture: idClaims.profile_picture,
|
|
950
|
+
email: idClaims.email
|
|
951
|
+
};
|
|
952
|
+
return new SignInWithYouVersionResult(resultData);
|
|
953
|
+
}
|
|
954
|
+
/**
|
|
955
|
+
* Decodes JWT payload for UI display purposes.
|
|
956
|
+
*
|
|
957
|
+
* Note: We intentionally do not verify the JWT signature here because:
|
|
958
|
+
*
|
|
959
|
+
* 1. YouVersion's backend verifies all tokens on API requests
|
|
960
|
+
* 2. This decoded data is only used for UI display
|
|
961
|
+
* 3. No security decisions are made based on these claims
|
|
962
|
+
*
|
|
963
|
+
* @private
|
|
964
|
+
*/
|
|
965
|
+
static decodeJWT(token) {
|
|
966
|
+
const segments = token.split(".");
|
|
967
|
+
if (segments.length !== 3) {
|
|
968
|
+
return {};
|
|
969
|
+
}
|
|
970
|
+
let base64 = segments[1]?.replace(/-/g, "+").replace(/_/g, "/");
|
|
971
|
+
while (base64 && base64.length % 4 !== 0) {
|
|
972
|
+
base64 += "=";
|
|
973
|
+
}
|
|
974
|
+
try {
|
|
975
|
+
if (base64) {
|
|
976
|
+
const data = atob(base64);
|
|
977
|
+
return JSON.parse(data);
|
|
978
|
+
} else {
|
|
979
|
+
return {};
|
|
980
|
+
}
|
|
981
|
+
} catch (error) {
|
|
982
|
+
if (process.env.NODE_ENV === "development") {
|
|
983
|
+
console.error("JWT decode failed:", error);
|
|
984
|
+
}
|
|
985
|
+
return {};
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
static signOut() {
|
|
989
|
+
YouVersionPlatformConfiguration.clearAuthTokens();
|
|
990
|
+
}
|
|
991
|
+
/**
|
|
992
|
+
* Retrieves user information for the authenticated user by decoding the provided JWT access token.
|
|
993
|
+
*
|
|
994
|
+
* This function extracts the user's profile information directly from the JWT token payload.
|
|
995
|
+
*
|
|
996
|
+
* @param accessToken - The JWT access token obtained from the login process.
|
|
997
|
+
* @returns A Promise resolving to a YouVersionUserInfo object containing the user's profile information.
|
|
998
|
+
* @throws An error if the access token is invalid or cannot be decoded.
|
|
999
|
+
*/
|
|
1000
|
+
static userInfo(idToken) {
|
|
1001
|
+
if (!idToken || typeof idToken !== "string") {
|
|
1002
|
+
throw new Error("Invalid access token: must be a non-empty string");
|
|
1003
|
+
}
|
|
1004
|
+
try {
|
|
1005
|
+
const claims = this.decodeJWT(idToken);
|
|
1006
|
+
if (!claims || Object.keys(claims).length === 0) {
|
|
1007
|
+
throw new Error("Invalid JWT token: Unable to decode token payload");
|
|
1008
|
+
}
|
|
1009
|
+
const userInfoData = {
|
|
1010
|
+
id: claims.sub,
|
|
1011
|
+
name: claims.name,
|
|
1012
|
+
avatar_url: claims.profile_picture,
|
|
1013
|
+
email: claims.email
|
|
1014
|
+
};
|
|
1015
|
+
return new YouVersionUserInfo(userInfoData);
|
|
1016
|
+
} catch (error) {
|
|
1017
|
+
if (error instanceof Error) {
|
|
1018
|
+
throw new Error(`Failed to decode user information from JWT: ${error.message}`);
|
|
1019
|
+
} else {
|
|
1020
|
+
throw new Error("Failed to decode user information from JWT: Unknown error");
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
/**
|
|
1025
|
+
* Refreshes the access token using the stored refresh token.
|
|
1026
|
+
*
|
|
1027
|
+
* @returns Promise<SignInWithYouVersionResult | null> - New tokens if refresh succeeds, null otherwise
|
|
1028
|
+
* @throws An error if refresh fails or no refresh token is available
|
|
1029
|
+
*/
|
|
1030
|
+
static async refreshTokens() {
|
|
1031
|
+
const refreshToken = YouVersionPlatformConfiguration.refreshToken;
|
|
1032
|
+
const appKey = YouVersionPlatformConfiguration.appKey;
|
|
1033
|
+
const existingIdToken = YouVersionPlatformConfiguration.idToken;
|
|
1034
|
+
if (!refreshToken || !existingIdToken) {
|
|
1035
|
+
throw new Error("No refresh token or id token available");
|
|
1036
|
+
}
|
|
1037
|
+
if (!appKey) {
|
|
1038
|
+
throw new Error(
|
|
1039
|
+
"YouVersionPlatformConfiguration.appKey must be set before refreshing tokens"
|
|
1040
|
+
);
|
|
1041
|
+
}
|
|
1042
|
+
try {
|
|
1043
|
+
const url = new URL(`https://${YouVersionPlatformConfiguration.apiHost}/auth/token`);
|
|
1044
|
+
const parameters = new URLSearchParams({
|
|
1045
|
+
grant_type: "refresh_token",
|
|
1046
|
+
refresh_token: refreshToken,
|
|
1047
|
+
client_id: appKey
|
|
1048
|
+
});
|
|
1049
|
+
const request = new Request(url, {
|
|
1050
|
+
method: "POST",
|
|
1051
|
+
body: parameters,
|
|
1052
|
+
headers: {
|
|
1053
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
1054
|
+
}
|
|
1055
|
+
});
|
|
1056
|
+
const response = await fetch(request);
|
|
1057
|
+
if (!response.ok) {
|
|
1058
|
+
throw new Error(`Token refresh failed: ${response.status} ${response.statusText}`);
|
|
1059
|
+
}
|
|
1060
|
+
const tokens = await response.json();
|
|
1061
|
+
const result = new SignInWithYouVersionResult({
|
|
1062
|
+
accessToken: tokens.access_token,
|
|
1063
|
+
expiresIn: tokens.expires_in,
|
|
1064
|
+
refreshToken: tokens.refresh_token,
|
|
1065
|
+
idToken: existingIdToken,
|
|
1066
|
+
permissions: tokens.scope.split(" ").map((p) => p.trim()).filter((p) => p.length > 0).filter(
|
|
1067
|
+
(p) => Object.values(SignInWithYouVersionPermission).includes(
|
|
1068
|
+
p
|
|
1069
|
+
)
|
|
1070
|
+
)
|
|
1071
|
+
});
|
|
1072
|
+
YouVersionPlatformConfiguration.saveAuthData(
|
|
1073
|
+
result.accessToken || null,
|
|
1074
|
+
result.refreshToken || null,
|
|
1075
|
+
result.idToken || null,
|
|
1076
|
+
result.expiryDate || null
|
|
1077
|
+
);
|
|
1078
|
+
return result;
|
|
1079
|
+
} catch (error) {
|
|
1080
|
+
if (error instanceof Error) {
|
|
1081
|
+
throw new Error(`Token refresh failed: ${error.message}`);
|
|
1082
|
+
} else {
|
|
1083
|
+
throw new Error("Token refresh failed: Unknown error");
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
/**
|
|
1088
|
+
* Checks if the current access token is expired or about to expire.
|
|
1089
|
+
*
|
|
1090
|
+
* @returns true if token is expired or about to expire
|
|
1091
|
+
*/
|
|
1092
|
+
static isTokenExpired() {
|
|
1093
|
+
const expiryDate = YouVersionPlatformConfiguration.tokenExpiryDate;
|
|
1094
|
+
if (!expiryDate) {
|
|
1095
|
+
return true;
|
|
1096
|
+
}
|
|
1097
|
+
return (/* @__PURE__ */ new Date()).getTime() >= expiryDate.getTime();
|
|
1098
|
+
}
|
|
1099
|
+
/**
|
|
1100
|
+
* Refreshes the access token if it's expired or about to expire.
|
|
1101
|
+
*
|
|
1102
|
+
* @returns Promise<boolean> - true if refresh was successful or not needed, false if failed
|
|
1103
|
+
*/
|
|
1104
|
+
static async refreshTokenIfNeeded() {
|
|
1105
|
+
if (!this.isTokenExpired()) {
|
|
1106
|
+
return true;
|
|
1107
|
+
}
|
|
1108
|
+
try {
|
|
1109
|
+
const result = await this.refreshTokens();
|
|
1110
|
+
return !!result;
|
|
1111
|
+
} catch {
|
|
1112
|
+
YouVersionPlatformConfiguration.clearAuthTokens();
|
|
1113
|
+
return false;
|
|
1114
|
+
}
|
|
869
1115
|
}
|
|
870
1116
|
};
|
|
871
1117
|
|
|
@@ -926,119 +1172,6 @@ var URLBuilder = class {
|
|
|
926
1172
|
);
|
|
927
1173
|
}
|
|
928
1174
|
}
|
|
929
|
-
static userURL(accessToken) {
|
|
930
|
-
if (typeof accessToken !== "string" || accessToken.trim().length === 0) {
|
|
931
|
-
throw new Error("accessToken must be a non-empty string");
|
|
932
|
-
}
|
|
933
|
-
try {
|
|
934
|
-
const url = new URL(this.baseURL);
|
|
935
|
-
url.pathname = "/auth/me";
|
|
936
|
-
const searchParams = new URLSearchParams();
|
|
937
|
-
searchParams.append("lat", accessToken);
|
|
938
|
-
url.search = searchParams.toString();
|
|
939
|
-
return url;
|
|
940
|
-
} catch (error) {
|
|
941
|
-
throw new Error(
|
|
942
|
-
`Failed to construct user URL: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
943
|
-
);
|
|
944
|
-
}
|
|
945
|
-
}
|
|
946
|
-
};
|
|
947
|
-
|
|
948
|
-
// src/Users.ts
|
|
949
|
-
var MAX_RETRY_ATTEMPTS = 3;
|
|
950
|
-
var RETRY_DELAY_MS = 1e3;
|
|
951
|
-
var YouVersionAPIUsers = class {
|
|
952
|
-
/**
|
|
953
|
-
* Presents the YouVersion login flow to the user and returns the login result upon completion.
|
|
954
|
-
*
|
|
955
|
-
* This function authenticates the user with YouVersion, requesting the specified required and optional permissions.
|
|
956
|
-
* The function returns a promise that resolves when the user completes or cancels the login flow,
|
|
957
|
-
* returning the login result containing the authorization code and granted permissions.
|
|
958
|
-
*
|
|
959
|
-
* @param requiredPermissions - The set of permissions that must be granted by the user for successful login.
|
|
960
|
-
* @param optionalPermissions - The set of permissions that will be requested from the user but are not required for successful login.
|
|
961
|
-
* @returns A Promise resolving to a SignInWithYouVersionResult containing the authorization code and granted permissions upon successful login.
|
|
962
|
-
* @throws An error if authentication fails or is cancelled by the user.
|
|
963
|
-
*/
|
|
964
|
-
static async signIn(requiredPermissions, optionalPermissions) {
|
|
965
|
-
if (!requiredPermissions || !(requiredPermissions instanceof Set)) {
|
|
966
|
-
throw new Error("Invalid requiredPermissions: must be a Set");
|
|
967
|
-
}
|
|
968
|
-
if (!optionalPermissions || !(optionalPermissions instanceof Set)) {
|
|
969
|
-
throw new Error("Invalid optionalPermissions: must be a Set");
|
|
970
|
-
}
|
|
971
|
-
const appKey = YouVersionPlatformConfiguration.appKey;
|
|
972
|
-
if (!appKey) {
|
|
973
|
-
throw new Error("YouVersionPlatformConfiguration.appKey must be set before calling signIn");
|
|
974
|
-
}
|
|
975
|
-
const url = URLBuilder.authURL(appKey, requiredPermissions, optionalPermissions);
|
|
976
|
-
const strategy = AuthenticationStrategyRegistry.get();
|
|
977
|
-
const callbackUrl = await strategy.authenticate(url);
|
|
978
|
-
const result = new SignInWithYouVersionResult(callbackUrl);
|
|
979
|
-
if (result.accessToken) {
|
|
980
|
-
YouVersionPlatformConfiguration.setAccessToken(result.accessToken);
|
|
981
|
-
}
|
|
982
|
-
return result;
|
|
983
|
-
}
|
|
984
|
-
static signOut() {
|
|
985
|
-
YouVersionPlatformConfiguration.setAccessToken(null);
|
|
986
|
-
}
|
|
987
|
-
/**
|
|
988
|
-
* Retrieves user information for the authenticated user using the provided access token.
|
|
989
|
-
*
|
|
990
|
-
* This function fetches the user's profile information from the YouVersion API, decoding it into a YouVersionUserInfo model.
|
|
991
|
-
*
|
|
992
|
-
* @param accessToken - The access token obtained from the login process.
|
|
993
|
-
* @returns A Promise resolving to a YouVersionUserInfo object containing the user's profile information.
|
|
994
|
-
* @throws An error if the URL is invalid, the network request fails, or the response cannot be decoded.
|
|
995
|
-
*/
|
|
996
|
-
static async userInfo(accessToken) {
|
|
997
|
-
if (!accessToken || typeof accessToken !== "string") {
|
|
998
|
-
throw new Error("Invalid access token: must be a non-empty string");
|
|
999
|
-
}
|
|
1000
|
-
if (YouVersionPlatformConfiguration.isPreviewMode && accessToken === "preview") {
|
|
1001
|
-
return YouVersionPlatformConfiguration.previewUserInfo || new YouVersionUserInfo({
|
|
1002
|
-
first_name: "Preview",
|
|
1003
|
-
last_name: "User",
|
|
1004
|
-
id: "preview-user",
|
|
1005
|
-
avatar_url: void 0
|
|
1006
|
-
});
|
|
1007
|
-
}
|
|
1008
|
-
const url = URLBuilder.userURL(accessToken);
|
|
1009
|
-
const request = YouVersionAPI.addStandardHeaders(url);
|
|
1010
|
-
let lastError = null;
|
|
1011
|
-
for (let attempt = 1; attempt <= MAX_RETRY_ATTEMPTS; attempt++) {
|
|
1012
|
-
try {
|
|
1013
|
-
const response = await fetch(request);
|
|
1014
|
-
if (response.status === 401) {
|
|
1015
|
-
throw new Error(
|
|
1016
|
-
"Authentication failed: Invalid or expired access token. Please sign in again."
|
|
1017
|
-
);
|
|
1018
|
-
}
|
|
1019
|
-
if (response.status === 403) {
|
|
1020
|
-
throw new Error("Access denied: Insufficient permissions to retrieve user information");
|
|
1021
|
-
}
|
|
1022
|
-
if (response.status !== 200) {
|
|
1023
|
-
throw new Error(
|
|
1024
|
-
`Failed to retrieve user information: Server responded with status ${response.status}`
|
|
1025
|
-
);
|
|
1026
|
-
}
|
|
1027
|
-
const data = await response.json();
|
|
1028
|
-
return data;
|
|
1029
|
-
} catch (error) {
|
|
1030
|
-
lastError = error instanceof Error ? error : new Error("Failed to parse server response");
|
|
1031
|
-
if (error instanceof Error && (error.message.includes("401") || error.message.includes("403"))) {
|
|
1032
|
-
throw error;
|
|
1033
|
-
}
|
|
1034
|
-
if (attempt < MAX_RETRY_ATTEMPTS) {
|
|
1035
|
-
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS * attempt));
|
|
1036
|
-
continue;
|
|
1037
|
-
}
|
|
1038
|
-
}
|
|
1039
|
-
}
|
|
1040
|
-
throw lastError || new Error("Failed to retrieve user information after multiple attempts");
|
|
1041
|
-
}
|
|
1042
1175
|
};
|
|
1043
1176
|
|
|
1044
1177
|
// src/utils/constants.ts
|
|
@@ -1257,8 +1390,6 @@ var BOOK_CANON = {
|
|
|
1257
1390
|
};
|
|
1258
1391
|
export {
|
|
1259
1392
|
ApiClient,
|
|
1260
|
-
AuthClient,
|
|
1261
|
-
AuthenticationStrategyRegistry,
|
|
1262
1393
|
BOOK_CANON,
|
|
1263
1394
|
BOOK_IDS,
|
|
1264
1395
|
BibleClient,
|
|
@@ -1269,7 +1400,6 @@ export {
|
|
|
1269
1400
|
SignInWithYouVersionPermission,
|
|
1270
1401
|
SignInWithYouVersionResult,
|
|
1271
1402
|
URLBuilder,
|
|
1272
|
-
WebAuthenticationStrategy,
|
|
1273
1403
|
YouVersionAPI,
|
|
1274
1404
|
YouVersionAPIUsers,
|
|
1275
1405
|
YouVersionPlatformConfiguration,
|